tokenize.ts
1 import type { Request } from "@literate.ink/utilities"; 2 import type { Configuration, Identification, Profile } from "~/models"; 3 import { defaultFetcher, type Fetcher, findValueBetween, getHeaderFromResponse } from "@literate.ink/utilities"; 4 5 import { XMLParser } from "fast-xml-parser"; 6 import { CLIENT_TYPE, SERVICE_VERSION, SOAP_URL, SOAP_USER_AGENT } from "~/core/constants"; 7 8 import { xml } from "~/core/xml"; 9 import { decodeBalance } from "~/decoders/balance"; 10 // import { setDeviceToken } from "./private/set-device-token"; 11 12 export const extractActivationURL = async (url: string, fetcher: Fetcher = defaultFetcher): Promise<string> => { 13 const response = await fetcher({ redirect: "manual", url: new URL(url) }); 14 const location = getHeaderFromResponse(response, "Location"); 15 16 if (!location) { 17 throw new Error("URL to tokenize expired"); 18 } 19 20 return location; 21 }; 22 23 // eslint-disable-next-line ts/explicit-function-return-type 24 export const tokenize = async (url: string, fetcher: Fetcher = defaultFetcher) => { 25 // encoded like this: 26 // izly://SBSCR/<identifier>/<code> 27 const parts = url.split("/"); 28 const code = parts.pop()!; 29 const identifier = parts.pop()!; 30 31 const body = xml.header + xml.envelope(` 32 <Logon xmlns="Service" id="o0" c:root="1"> 33 ${xml.property("version", SERVICE_VERSION)} 34 ${xml.property("channel", "AIZ")} 35 ${xml.property("format", "T")} 36 ${xml.property("model", "A")} 37 ${xml.property("language", "fr")} 38 ${xml.property("user", identifier)} 39 <password i:null="true" /> 40 ${xml.property("smoneyClientType", CLIENT_TYPE)} 41 ${xml.property("rooted", "0")} 42 ${xml.property("actCode", code)} 43 </Logon> 44 `); 45 46 const request: Request = { 47 content: body, 48 headers: { 49 "clientVersion": SERVICE_VERSION, 50 "Content-Type": "text/xml;charset=utf-8", 51 "smoneyClientType": CLIENT_TYPE, 52 "SOAPAction": "Service/Logon", 53 "User-Agent": SOAP_USER_AGENT 54 }, 55 method: "POST", 56 url: SOAP_URL 57 }; 58 59 const response = await fetcher(request); 60 61 const result = findValueBetween(response.content, "<LogonResult>", "</LogonResult>"); 62 if (!result) throw new Error("No <LogonResult> found in response"); 63 64 const decoded = xml.from_entities(result); 65 const parser = new XMLParser({ 66 numberParseOptions: { 67 hex: true, 68 leadingZeros: true, 69 skipLike: /[0-9]/ 70 } 71 }); 72 const { Logon } = parser.parse(decoded); 73 74 const output = { 75 balance: decodeBalance(Logon.UP), 76 77 configuration: { 78 currency: Logon.CUR, 79 moneyInMaximum: parseFloat(Logon.MONEYINMAX), 80 moneyInMinimum: parseFloat(Logon.MONEYINMIN), 81 moneyOutMaximum: parseFloat(Logon.MONEYOUTMAX), 82 moneyOutMinimum: parseFloat(Logon.MONEYOUTMIN), 83 84 paymentMaximum: parseFloat(Logon.P2PPAYMAX), 85 paymentMinimum: parseFloat(Logon.P2PPAYMIN), 86 paymentPartMaximum: parseFloat(Logon.P2PPAYPARTMAX), 87 paymentPartMinimum: parseFloat(Logon.P2PPAYPARTMIN) 88 } as Configuration, 89 90 identification: { 91 accessToken: Logon.OAUTH.ACCESS_TOKEN, 92 93 accessTokenExpiresIn: parseInt(Logon.OAUTH.EXPIRES_IN), 94 counter: 0, 95 identifier: Logon.UID, 96 nsse: Logon.NSSE, 97 98 qrCodePrivateKey: Logon.QR_CODE_PRIVATE_KEY, 99 100 refreshToken: Logon.OAUTH.REFRESH_TOKEN, 101 seed: Logon.SEED, 102 103 sessionID: Logon.SID, 104 token: Logon.TOKEN, 105 userID: Logon.USER_ID, 106 107 userPublicID: Logon.USER_PUBLIC_ID 108 } as Identification, 109 110 profile: { 111 email: Logon.EMAIL, 112 firstName: Logon.FNAME, 113 identifier: Logon.ALIAS, 114 lastName: Logon.LNAME 115 } as Profile 116 }; 117 118 // register the token for this session id 119 // EDIT: apparently only for GCM (Google Cloud Messaging) 120 // await setDeviceToken(output.identification, fetcher); 121 122 return output; 123 };