const DEFAULT_QR_SECRET = "qms-branch-qr-v1"; function getQrSecret() { return process.env.QR_TOKEN_SECRET || DEFAULT_QR_SECRET; } function toBase64Url(bytes: Uint8Array) { let binary = ""; bytes.forEach((byte) => { binary += String.fromCharCode(byte); }); return btoa(binary) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/g, ""); } function fromBase64Url(token: string) { const padded = token.replace(/-/g, "+").replace(/_/g, "/"); const padLength = (4 - (padded.length % 4)) % 4; const base64 = padded + "=".repeat(padLength); const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i += 1) { bytes[i] = binary.charCodeAt(i); } return bytes; } function xorBytes(bytes: Uint8Array, secret: string) { const key = new TextEncoder().encode(secret); return bytes.map((byte, index) => byte ^ key[index % key.length]); } export type BranchQrPayload = { tenantCode: string; branchId: number; }; export function encodeBranchQrToken( tenantCode: string, branchId: number, secret = getQrSecret() ) { const plain = `v1:${tenantCode.trim().toUpperCase()}:${Number(branchId)}`; const plainBytes = new TextEncoder().encode(plain); return toBase64Url(xorBytes(plainBytes, secret)); } export function decodeBranchQrToken( token: string, secret = getQrSecret() ): BranchQrPayload | null { try { const plain = new TextDecoder().decode( xorBytes(fromBase64Url(token), secret) ); const match = /^v1:([^:]+):(\d+)$/.exec(plain); if (!match) return null; return { tenantCode: match[1], branchId: Number(match[2]), }; } catch { return null; } }