UNPKG

@hellocoop/identifier

Version:

A module for generating and verifying Hellō identifiers with a checksum.

168 lines (137 loc) 5.7 kB
/** * Generated by build script * Contains identifier types and generator functions * * @property {() => string} sub - Hellō directed identifier - `sub` in ID token * @property {() => string} app - Hellō application identifier (client_id) - `aud` in ID token * @property {() => string} usr - Hellō internal user identifier * @property {() => string} jti - ID Token jti * @property {() => string} ses - Hellō session identifier * @property {() => string} dvc - Hellō device cookie identifier * @property {() => string} inv - Hellō invitation identifier * @property {() => string} pky - Hellō passkey identifier * @property {() => string} non - Hellō nonce identifier * @property {() => string} cod - Hellō authorization code * @property {() => string} org - Hellō organization identifier * @property {() => string} req - Internal HTTP request identifier * @property {() => string} pub - Hellō publisher identifier */ const identifierTypes = ["sub","app","usr","jti","ses","dvc","inv","pky","non","cod","org","req","pub"]; const identifierTypesSet = new Set(identifierTypes); const HELLO_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; let generateId; // Load nanoid dynamically const loadNanoid = async () => { const { customAlphabet } = await import('nanoid'); generateId = customAlphabet(HELLO_ALPHABET, 24); }; loadNanoid(); // Call the function to load nanoid immediately // identifier.js - only used during build // identifierTypesSet added by build script // generateId added by build script const HELLO_REGEX = new RegExp(`^[${HELLO_ALPHABET}]+$`); const checksum = (prefix, id) => { const pb = Buffer.from(prefix); const dib = Buffer.from(id); const a = pb[0] + dib[0] + dib[3] + dib[6] + dib[9] + dib[12] + dib[15] + dib[18]; const b = pb[1] + dib[1] + dib[4] + dib[7] + dib[10] + dib[13] + dib[16] + dib[19]; const c = pb[2] + dib[2] + dib[5] + dib[8] + dib[11] + dib[14] + dib[17] + dib[20]; return HELLO_ALPHABET[a % 62] + HELLO_ALPHABET[b % 62] + HELLO_ALPHABET[c % 62]; }; // Doh! ... gen AI failed in picking the right indexes for the checksum calculation // some ids use this checksum calculation, so we need to keep it around const oldChecksum = (prefix, id) => { const pb = Buffer.from(prefix); const dib = Buffer.from(id); const a = pb[0] + dib[0] + dib[3] + dib[7] + dib[10] + dib[14] + dib[17] + dib[21]; const b = pb[1] + dib[1] + dib[4] + dib[8] + dib[11] + dib[15] + dib[18] + dib[22]; const c = pb[2] + dib[2] + dib[5] + dib[9] + dib[12] + dib[16] + dib[19] + dib[23]; return HELLO_ALPHABET[a % 62] + HELLO_ALPHABET[b % 62] + HELLO_ALPHABET[c % 62]; }; const validate = (identifier) => { const parts = identifier.split('_'); if (parts.length !== 3) { return false; // Invalid format } const [prefix, id, providedChecksum] = parts; if (!identifierTypesSet.has(prefix)) { return false; // Unknown prefix } if (id.length !== 24 || providedChecksum.length !== 3) { return false; // Invalid length } if (!HELLO_REGEX.test(id) || !HELLO_REGEX.test(providedChecksum)) { return false; // Invalid parts } const calculatedChecksum = checksum(prefix, id); let valid = providedChecksum === calculatedChecksum; if (!valid) { // try the old checksum calculation valid = providedChecksum === oldChecksum(prefix, id); } return valid }; const nano_id = (prefix) => { if (!generateId) { throw new Error("nanoid is not ready. Ensure that loadNanoid has been called and completed."); } const id = generateId(); return `${prefix}_${id}_${checksum(prefix, id)}`; }; const generators = identifierTypes.reduce((acc, type) => { acc[type] = () => nano_id(type); return acc; }, {}); const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/; const uuidv4FromNanoid = (nanoid) => { if (!validate(nanoid)) { throw new Error(`Invalid nanoid: ${nanoid}`); } const base62 = nanoid.slice(4, -4); let num = BigInt(0); const base = BigInt(62); for (const char of base62) { num = num * base + BigInt(HELLO_ALPHABET.indexOf(char)); } const hexString = num.toString(16).padStart(32, '0'); if (hexString.length !== 32) { throw new Error(`NanoId is too long: ${nanoid}`); } const uuidv4 = `${hexString.slice(0, 8)}-${hexString.slice(8, 12)}-${hexString.slice(12, 16)}-${hexString.slice(16, 20)}-${hexString.slice(20)}`; return uuidv4; }; const nanoidFromUUIDv4 = (type, uuidv4) => { if (!identifierTypesSet.has(type)) throw new Error(`Unknown identifier type: ${type}`); if (!uuidRegex.test(uuidv4)) throw new Error(`Invalid UUIDv4: ${uuidv4}`); // Remove hyphens and convert to a BigInt const hexString = uuidv4.replace(/-/g, ''); let num = BigInt(`0x${hexString}`); // Convert to base62 let base62 = ''; const base = BigInt(HELLO_ALPHABET.length); while (num > 0n) { base62 = HELLO_ALPHABET[num % base] + base62; num = num / base; } // Pad with the first character of HELLO_ALPHABET to ensure fixed length while (base62.length < 24) { base62 = HELLO_ALPHABET[0] + base62; } const chk = checksum(type, base62); const nanoid = `${type}_${base62}_${chk}`; return nanoid; } Object.freeze(identifierTypes); generators.validate = validate; generators.checksum = checksum; generators.HELLO_ALPHABET = HELLO_ALPHABET; generators.types = identifierTypes; generators.isUUIDv4 = (id) => uuidRegex.test(id); generators.nanoidFromUUIDv4 = nanoidFromUUIDv4; generators.uuidv4FromNanoid = uuidv4FromNanoid; /* * export and modules.exports statements added by build script */ module.exports = generators;