@code_with_sachin/uusid
Version:
Ultra Unique Secure ID generator with multiple formats, encryption, hierarchical IDs, and advanced features for modern applications
453 lines (401 loc) • 17.1 kB
JavaScript
/**
* Browser entry for @code_with_sachin/uusid
*
* Uses Web Crypto (`globalThis.crypto`) instead of Node's `crypto`/`os`/`Buffer`.
* Surface mirrors the server entry, with three differences:
* - fromContent / encrypt / decrypt are async (Promise<string>)
* - WorkerPool is not exported (no Web Workers port yet)
* - getMetrics()/healthCheck() omit memoryUsage (no browser equivalent)
*
* Encrypted IDs use AES-256-GCM here; the server uses AES-256-CBC.
* Cross-entry encryption interop is NOT supported.
*/
const subtle = globalThis.crypto?.subtle;
if (!globalThis.crypto || !subtle) {
throw new Error(
'@code_with_sachin/uusid/browser requires Web Crypto (globalThis.crypto.subtle). ' +
'Use a modern browser or Node 18+.'
);
}
const randomBytes = (n) => globalThis.crypto.getRandomValues(new Uint8Array(n));
const bytesToHex = (bytes) => {
let s = '';
for (let i = 0; i < bytes.length; i++) s += bytes[i].toString(16).padStart(2, '0');
return s;
};
const hexToBytes = (hex) => {
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) {
out[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return out;
};
const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const UUID_EPOCH = new Date('1582-10-15').getTime();
class UUSIDGenerator {
constructor(options = {}) {
this.nodeId = options.nodeId || this.generateNodeId();
this.clockSeq = options.clockSeq ?? this.generateClockSeq();
this.lastTimestamp = 0;
this.sequenceCounter = 0;
this.version = '@code_with_sachin/uusid';
this.prefix = options.prefix || null;
this.separator = options.separator || '-';
this.validAfter = options.validAfter ? new Date(options.validAfter).getTime() : null;
this.validBefore = options.validBefore ? new Date(options.validBefore).getTime() : null;
this.secretKey = options.secretKey || null;
this.metrics = {
totalGenerated: 0,
startTime: Date.now(),
collisions: 0,
peakRate: 0,
currentRate: 0,
rateSamples: []
};
}
generateNodeId() {
const bytes = randomBytes(6);
bytes[0] |= 0x01;
return bytesToHex(bytes);
}
generateClockSeq() {
const bytes = randomBytes(2);
return ((bytes[0] << 8) | bytes[1]) & 0x3fff;
}
getTimestamp() {
const now = Date.now();
if (now === this.lastTimestamp) {
this.sequenceCounter++;
if (this.sequenceCounter > 0xffff) {
while (Date.now() === this.lastTimestamp) { /* busy wait */ }
this.sequenceCounter = 0;
this.lastTimestamp = Date.now();
}
} else {
this.sequenceCounter = 0;
this.lastTimestamp = now;
}
const timestamp = (this.lastTimestamp - UUID_EPOCH) * 10000;
return { timestamp, sequence: this.sequenceCounter };
}
generate() {
const { timestamp, sequence } = this.getTimestamp();
this.updateMetrics();
if (this.validAfter && Date.now() < this.validAfter) {
throw new Error('Generation not allowed: Current time is before valid-after time');
}
if (this.validBefore && Date.now() > this.validBefore) {
throw new Error('Generation not allowed: Current time is after valid-before time');
}
const absTimestamp = Math.abs(timestamp);
const timeLow = ((absTimestamp & 0xffffffff) >>> 0).toString(16).padStart(8, '0');
const timeMid = (Math.floor(absTimestamp / 0x100000000) & 0xffff).toString(16).padStart(4, '0');
const timeHigh = ((Math.floor(absTimestamp / 0x1000000000000) & 0x0fff) | 0x1000).toString(16).padStart(4, '0');
const clockSeqWithSeq = ((this.clockSeq + sequence) & 0xffff);
const clockSeqHigh = ((clockSeqWithSeq >> 8) | 0x80).toString(16).padStart(2, '0');
const clockSeqLow = (clockSeqWithSeq & 0xff).toString(16).padStart(2, '0');
const node = this.nodeId.padStart(12, '0');
const uuid = `${timeLow}${this.separator}${timeMid}${this.separator}${timeHigh}${this.separator}${clockSeqHigh}${clockSeqLow}${this.separator}${node}`;
return this.prefix ? `${this.prefix}${this.separator}${uuid}` : uuid;
}
generateBatch(count) {
const batch = [];
for (let i = 0; i < count; i++) batch.push(this.generate());
return batch;
}
generateSortedBatch(count) {
return this.generateBatch(count).sort((a, b) => this.extractTimestamp(a) - this.extractTimestamp(b));
}
base32() {
const uuid = this.generate();
const sep = new RegExp(escapeRegex(this.separator), 'g');
const hex = uuid.replace(sep, '');
return this.toBase32(hexToBytes(hex));
}
urlSafe() {
const sep = new RegExp(escapeRegex(this.separator), 'g');
return this.generate().replace(sep, '').toLowerCase();
}
compact() {
const sep = new RegExp(escapeRegex(this.separator), 'g');
return this.generate().replace(sep, '');
}
hierarchical(options = {}) {
const { levels = 3, separator = '.', parent = null } = options;
const sep = new RegExp(escapeRegex(this.separator), 'g');
const id = this.generate().replace(sep, '');
if (parent) {
return `${parent}${separator}${id.substring(0, 10)}`;
}
const parts = [];
const partLength = Math.floor(id.length / levels);
for (let i = 0; i < levels; i++) {
const start = i * partLength;
const end = i === levels - 1 ? id.length : start + partLength;
parts.push(id.substring(start, end));
}
return parts.join(separator);
}
async fromContent(content, options = {}) {
const { namespace = 'default', algorithm = 'SHA-256' } = options;
const data = new TextEncoder().encode(namespace + content);
const digest = new Uint8Array(await subtle.digest(algorithm, data));
const hex = bytesToHex(digest);
return [
hex.substring(0, 8),
hex.substring(8, 12),
hex.substring(12, 16),
hex.substring(16, 20),
hex.substring(20, 32)
].join(this.separator);
}
async _importKey(usages) {
const padded = this.secretKey.padEnd(32, '0').substring(0, 32);
const keyBytes = new TextEncoder().encode(padded);
return subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, usages);
}
async encrypt(data) {
if (!this.secretKey) throw new Error('Secret key required for encryption');
const key = await this._importKey(['encrypt']);
const iv = randomBytes(12);
const ct = new Uint8Array(
await subtle.encrypt({ name: 'AES-GCM', iv }, key, new TextEncoder().encode(data))
);
return bytesToHex(iv) + ':' + bytesToHex(ct);
}
async decrypt(encryptedData) {
if (!this.secretKey) throw new Error('Secret key required for decryption');
const parts = encryptedData.split(':');
if (parts.length !== 2) throw new Error('Invalid encrypted data format');
const key = await this._importKey(['decrypt']);
const iv = hexToBytes(parts[0]);
const ct = hexToBytes(parts[1]);
const pt = await subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
return new TextDecoder().decode(pt);
}
validate(id, options = {}) {
const { allowPrefix = true } = options;
let cleanId = id;
if (allowPrefix && this.prefix && id.startsWith(`${this.prefix}${this.separator}`)) {
cleanId = id.substring(this.prefix.length + 1);
}
const escapedSeparator = escapeRegex(this.separator);
const uuidRegex = new RegExp(
`^[0-9a-f]{8}${escapedSeparator}[0-9a-f]{4}${escapedSeparator}[0-9a-f]{4}${escapedSeparator}[0-9a-f]{4}${escapedSeparator}[0-9a-f]{12}$`,
'i'
);
if (!uuidRegex.test(cleanId)) {
return { valid: false, isValid: false, reason: 'Invalid format', version: null, entropy: 0 };
}
const hexString = cleanId.replace(new RegExp(escapedSeparator, 'g'), '');
const entropy = hexString.split('').reduce((acc, char, i, arr) => {
const charCount = arr.filter(c => c === char).length;
const probability = charCount / arr.length;
return acc - probability * Math.log2(probability);
}, 0);
try {
const timestamp = this.extractTimestamp(cleanId);
if (this.validAfter && timestamp < this.validAfter) {
return { valid: false, isValid: false, reason: 'Before valid range', version: 'uusid', entropy };
}
if (this.validBefore && timestamp > this.validBefore) {
return { valid: false, isValid: false, reason: 'After valid range', version: 'uusid', entropy };
}
} catch {
return { valid: false, isValid: false, reason: 'Invalid timestamp', version: 'uusid', entropy };
}
return { valid: true, isValid: true, version: 'uusid', entropy };
}
extractTimestamp(id) {
const escapedSeparator = escapeRegex(this.separator);
const cleanId = id.replace(new RegExp(escapedSeparator, 'g'), '').replace(/\./g, '');
if (cleanId.length < 32) throw new Error('Invalid ID format for timestamp extraction');
const timeLow = parseInt(cleanId.substring(0, 8), 16);
const timeMid = parseInt(cleanId.substring(8, 12), 16);
const timeHigh = parseInt(cleanId.substring(12, 16), 16) & 0x0fff;
const timestamp = (timeHigh * 0x1000000000000) + (timeMid * 0x100000000) + timeLow;
return UUID_EPOCH + (timestamp / 10000);
}
isInTimeRange(id, start, end) {
try {
const ts = this.extractTimestamp(id);
return ts >= new Date(start).getTime() && ts <= new Date(end).getTime();
} catch {
return false;
}
}
analyze(idArray) {
const analysis = {
total: idArray.length,
totalIds: idArray.length,
valid: 0,
invalid: 0,
duplicates: 0,
uniqueIds: 0,
timeRange: { earliest: null, latest: null },
formats: { standard: 0, prefixed: 0, custom: 0 },
errors: []
};
const seen = new Set();
const timestamps = [];
for (const id of idArray) {
if (seen.has(id)) { analysis.duplicates++; continue; }
seen.add(id);
const v = this.validate(id);
if (v.valid) {
analysis.valid++;
if (this.prefix && id.startsWith(`${this.prefix}${this.separator}`)) analysis.formats.prefixed++;
else if (id.includes(this.separator)) analysis.formats.standard++;
else analysis.formats.custom++;
try { timestamps.push(this.extractTimestamp(id)); }
catch (e) { analysis.errors.push(`Timestamp extraction failed for ${id}: ${e.message}`); }
} else {
analysis.invalid++;
analysis.errors.push(`Invalid ID ${id}: ${v.reason}`);
}
}
if (timestamps.length > 0) {
timestamps.sort((a, b) => a - b);
analysis.timeRange.earliest = new Date(timestamps[0]);
analysis.timeRange.latest = new Date(timestamps[timestamps.length - 1]);
}
analysis.uniqueIds = seen.size;
return analysis;
}
parseHierarchy(id, options = {}) {
const { separator = '.' } = options;
const parts = id.split(separator);
const depth = Math.max(0, parts.length - 3);
return {
depth,
parts,
parent: parts.length > 3 ? parts.slice(0, -1).join(separator) : null,
root: parts.slice(0, 3).join(separator),
leaf: parts[parts.length - 1]
};
}
updateMetrics() {
this.metrics.totalGenerated++;
const now = Date.now();
this.metrics.rateSamples.push(now);
const cutoff = now - 10000;
this.metrics.rateSamples = this.metrics.rateSamples.filter(t => t > cutoff);
if (this.metrics.rateSamples.length > 1) {
const span = now - this.metrics.rateSamples[0];
if (span > 0) {
this.metrics.currentRate = Math.round((this.metrics.rateSamples.length - 1) / (span / 1000));
this.metrics.peakRate = Math.max(this.metrics.peakRate, this.metrics.currentRate);
}
}
}
getMetrics() {
const uptime = Date.now() - this.metrics.startTime;
const avgRate = uptime > 0 ? this.metrics.totalGenerated / (uptime / 1000) : 0;
return {
totalGenerated: this.metrics.totalGenerated,
uptime: this.formatDuration(uptime),
averageRate: Math.round(avgRate),
currentRate: this.metrics.currentRate,
peakRate: this.metrics.peakRate,
collisions: this.metrics.collisions
};
}
async healthCheck() {
const start = Date.now();
try {
const testIds = this.generateBatch(100);
const allValid = testIds.every(id => this.validate(id).valid);
const noDuplicates = new Set(testIds).size === testIds.length;
const generationTime = Date.now() - start;
const rate = 100 / Math.max(generationTime / 1000, 0.0001);
return {
healthy: allValid && noDuplicates && rate > 10,
checks: { validation: allValid, uniqueness: noDuplicates, performance: rate > 10 },
metrics: { generationTime: `${generationTime}ms`, rate: `${Math.round(rate)} IDs/sec` },
timestamp: new Date().toISOString()
};
} catch (error) {
return { healthy: false, error: error.message, timestamp: new Date().toISOString() };
}
}
toBase32(buffer) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let result = '';
let bits = 0;
let value = 0;
for (let i = 0; i < buffer.length; i++) {
value = (value << 8) | buffer[i];
bits += 8;
while (bits >= 5) {
result += alphabet[(value >>> (bits - 5)) & 31];
bits -= 5;
}
}
if (bits > 0) result += alphabet[(value << (5 - bits)) & 31];
return result;
}
formatDuration(ms) {
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
if (ms < 3600000) return `${Math.round(ms / 60000)}m`;
return `${Math.round(ms / 3600000)}h`;
}
}
class PrefixedGenerator extends UUSIDGenerator {
constructor(prefix, options = {}) {
super({ ...options, prefix, separator: '_' });
}
}
class EncryptedGenerator extends UUSIDGenerator {
constructor(options = {}) {
if (!options.secretKey) throw new Error('Secret key required for encrypted generator');
super(options);
}
async generate() {
return this.encrypt(super.generate());
}
}
const defaultGenerator = new UUSIDGenerator();
function uusid(options) {
return options ? new UUSIDGenerator(options).generate() : defaultGenerator.generate();
}
function uusidBatch(count, options) {
return options ? new UUSIDGenerator(options).generateBatch(count) : defaultGenerator.generateBatch(count);
}
const createGenerator = (options) => new UUSIDGenerator(options);
const createPrefixedGenerator = (prefix, options) => new PrefixedGenerator(prefix, options);
const createEncryptedGenerator = (options) => new EncryptedGenerator(options);
const validate = (id, options) => defaultGenerator.validate(id, options);
const extractTimestamp = (id) => defaultGenerator.extractTimestamp(id);
const isInTimeRange = (id, start, end) => defaultGenerator.isInTimeRange(id, start, end);
const analyze = (idArray) => defaultGenerator.analyze(idArray);
const getMetrics = () => defaultGenerator.getMetrics();
const healthCheck = () => defaultGenerator.healthCheck();
const base32 = () => defaultGenerator.base32();
const urlSafe = () => defaultGenerator.urlSafe();
const compact = () => defaultGenerator.compact();
const hierarchical = (options) => defaultGenerator.hierarchical(options);
const fromContent = (content, options) => defaultGenerator.fromContent(content, options);
export {
uusid,
uusidBatch,
UUSIDGenerator,
PrefixedGenerator,
EncryptedGenerator,
createGenerator,
createPrefixedGenerator,
createEncryptedGenerator,
validate,
extractTimestamp,
isInTimeRange,
analyze,
getMetrics,
healthCheck,
base32,
urlSafe,
compact,
hierarchical,
fromContent,
defaultGenerator
};
export default uusid;