UNPKG

@hellocoop/identifier

Version:

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

183 lines (150 loc) 6.03 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 /* global HELLO_ALPHABET, identifierTypesSet, identifierTypes, generateId -- these come from ./build.js */ 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;