uniku 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cuid2/cuid2.d.mts.map +1 -1
- package/build/cuid2/cuid2.mjs +1 -1
- package/build/cuid2/cuid2.mjs.map +1 -1
- package/build/ksuid/ksuid.d.mts.map +1 -1
- package/build/ksuid/ksuid.mjs +1 -1
- package/build/ksuid/ksuid.mjs.map +1 -1
- package/build/objectid/objectid.d.mts.map +1 -1
- package/build/objectid/objectid.mjs +1 -1
- package/build/objectid/objectid.mjs.map +1 -1
- package/build/tsid/tsid.d.mts.map +1 -1
- package/build/tsid/tsid.mjs +1 -1
- package/build/tsid/tsid.mjs.map +1 -1
- package/build/ulid/ulid.d.mts.map +1 -1
- package/build/ulid/ulid.mjs +1 -1
- package/build/ulid/ulid.mjs.map +1 -1
- package/build/uuid/v4.d.mts +0 -1
- package/build/uuid/v4.d.mts.map +1 -1
- package/build/uuid/v4.mjs +1 -1
- package/build/uuid/v4.mjs.map +1 -1
- package/build/uuid/v7.d.mts +3 -0
- package/build/uuid/v7.d.mts.map +1 -1
- package/build/uuid/v7.mjs +1 -1
- package/build/uuid/v7.mjs.map +1 -1
- package/build/uuid-CnQIYoQi.mjs +2 -0
- package/build/uuid-CnQIYoQi.mjs.map +1 -0
- package/build/validation-CTNpXm94.mjs +2 -0
- package/build/validation-CTNpXm94.mjs.map +1 -0
- package/package.json +1 -1
- package/src/common/validation.ts +7 -0
- package/src/cuid2/cuid2.ts +7 -3
- package/src/ksuid/ksuid.ts +19 -17
- package/src/objectid/objectid.ts +25 -17
- package/src/tsid/tsid.ts +83 -53
- package/src/ulid/crockford.ts +2 -2
- package/src/ulid/ulid.ts +35 -24
- package/src/uuid/common/uuid.ts +12 -1
- package/src/uuid/v4.ts +25 -28
- package/src/uuid/v7.ts +75 -47
- package/build/uuid-BBjwHv7J.mjs +0 -2
- package/build/uuid-BBjwHv7J.mjs.map +0 -1
package/build/uuid/v7.d.mts
CHANGED
package/build/uuid/v7.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v7.d.mts","names":[],"sources":["../../src/uuid/v7.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"v7.d.mts","names":[],"sources":["../../src/uuid/v7.ts"],"mappings":";;;KAKY,aAAA;;;AAAZ;;EAKE,MAAA,GAAS,UAAA;EACT,KAAA;;;;EAIA,GAAA;AAAA;AAAA,KAGU,MAAA;EAAA;EAAA,cAEI,UAAA,GAAa,UAAA,EAAY,OAAA,EAAS,aAAA,cAA2B,GAAA,EAAK,IAAA,EAAM,MAAA,YAAkB,IAAA;EAAA,CACvG,OAAA,GAAU,aAAA,EAAe,GAAA,cAAiB,MAAA;EAC3C,OAAA,CAAQ,EAAA,WAAa,UAAA;EACrB,SAAA,CAAU,KAAA,EAAO,UAAA;EACjB,SAAA,CAAU,EAAA;EACV,OAAA,CAAQ,EAAA,YAAc,EAAA;EAEtB,GAAA;EAEA,GAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cA2KW,MAAA,EAAQ,MAAA"}
|
package/build/uuid/v7.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as e}from"../random-Chp-Nkzi.mjs";import{
|
|
1
|
+
import{r as e}from"../random-Chp-Nkzi.mjs";import{n as t,t as n}from"../validation-CTNpXm94.mjs";import{BufferError as r,InvalidInputError as i,ParseError as a,UniqueIdError as o}from"../errors.mjs";import{n as s,r as c,t as l}from"../uuid-CnQIYoQi.mjs";const u=/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,d=0xffffffffffff,f=4294967295,p=new Uint8Array(16),m={msecs:-1/0,seq:0};function h(e,t,n,r,i){r[i++]=t/1099511627776&255,r[i++]=t/4294967296&255,r[i++]=t/16777216&255,r[i++]=t/65536&255,r[i++]=t/256&255,r[i++]=t&255,r[i++]=112|n>>>28&15,r[i++]=n>>>20&255,r[i++]=128|n>>>14&63,r[i++]=n>>>6&255,r[i++]=n<<2&255|e[10]&3,r[i++]=e[11],r[i++]=e[12],r[i++]=e[13],r[i++]=e[14],r[i++]=e[15]}function g(e,n,i,a,o){if(!t(a,o,16))throw new r(`UUID_BUFFER_OUT_OF_BOUNDS`,`UUID byte range ${o}:${o+16-1} is out of buffer bounds`);h(e,n,i,a,o)}function _(t,r,a=0){let o=t.msecs;if(o!==void 0&&!n(o,0,d))throw new i(`UUID_TIMESTAMP_OUT_OF_RANGE`,`Timestamp must be an integer between 0 and ${d}`);let c=t.seq;if(c!==void 0&&!n(c,0,f))throw new i(`UUID_SEQUENCE_OUT_OF_RANGE`,`Sequence must be an integer between 0 and ${f}`);let l=t.random;if(l&&l.length<16)throw new i(`UUID_RANDOM_BYTES_TOO_SHORT`,`Random bytes length must be >= 16`);let u=l??e(),m=o??Date.now(),_=c??u[6]<<23|u[7]<<16|u[8]<<8|u[9];return r?(g(u,m,_,r,a),r):(h(u,m,_,p,0),s(p))}function v(t,n,r){if(t)return _(t,n,r);let i=Date.now(),a=e();return i>m.msecs?(m.seq=a[6]<<23|a[7]<<16|a[8]<<8|a[9],m.msecs=i):(m.seq=m.seq+1|0,m.seq<0&&(m.seq=0,m.msecs++)),n?(g(a,m.msecs,m.seq,n,r??0),n):(h(a,m.msecs,m.seq,p,0),s(p))}function y(e){let t=c(e),n=0;for(let e=0;e<6;e+=1)n=n*256+t[e];return n}function b(e){return typeof e==`string`&&u.test(e)}const x=Object.assign(v,{toBytes:c,fromBytes:l,timestamp:y,isValid:b,NIL:`00000000-0000-0000-0000-000000000000`,MAX:`ffffffff-ffff-ffff-ffff-ffffffffffff`});export{r as BufferError,i as InvalidInputError,a as ParseError,o as UniqueIdError,x as uuidv7};
|
|
2
2
|
//# sourceMappingURL=v7.mjs.map
|
package/build/uuid/v7.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v7.mjs","names":[],"sources":["../../src/uuid/v7.ts"],"sourcesContent":["import { rng } from '../common/random'\nimport { BufferError, InvalidInputError } from '../errors'\nimport { formatUuid, parseUuid } from './common/uuid'\n\nexport type UuidV7Options = {\n /**\n * 16 bytes of random data to use for UUID generation.\n * Note: Several bytes will be overwritten with timestamp, version, and variant data.\n */\n random?: Uint8Array\n msecs?: number\n seq?: number\n}\n\nexport type UuidV7 = {\n (): string\n <TBuf extends Uint8Array = Uint8Array>(options: UuidV7Options | undefined, buf: TBuf, offset?: number): TBuf\n (options?: UuidV7Options, buf?: undefined, offset?: number): string\n toBytes(id: string): Uint8Array\n fromBytes(bytes: Uint8Array): string\n timestamp(id: string): number\n isValid(id: unknown): id is string\n /** The nil UUID (all zeros) */\n NIL: string\n /** The max UUID (all ones) */\n MAX: string\n}\n\nconst UUID_V7_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n\n// Reusable buffer for string output path - avoids allocation per call.\n// Safe because bytes are consumed synchronously by
|
|
1
|
+
{"version":3,"file":"v7.mjs","names":[],"sources":["../../src/uuid/v7.ts"],"sourcesContent":["import { rng } from '../common/random'\nimport { isIntegerInRange, isWritableRange } from '../common/validation'\nimport { BufferError, InvalidInputError } from '../errors'\nimport { formatUuid, formatUuidUnchecked, parseUuid } from './common/uuid'\n\nexport type UuidV7Options = {\n /**\n * 16 bytes of random data to use for UUID generation.\n * Note: Several bytes will be overwritten with timestamp, version, and variant data.\n */\n random?: Uint8Array\n msecs?: number\n /**\n * Unsigned 32-bit sequence value.\n */\n seq?: number\n}\n\nexport type UuidV7 = {\n (): string\n <TBuf extends Uint8Array = Uint8Array>(options: UuidV7Options | undefined, buf: TBuf, offset?: number): TBuf\n (options?: UuidV7Options, buf?: undefined, offset?: number): string\n toBytes(id: string): Uint8Array\n fromBytes(bytes: Uint8Array): string\n timestamp(id: string): number\n isValid(id: unknown): id is string\n /** The nil UUID (all zeros) */\n NIL: string\n /** The max UUID (all ones) */\n MAX: string\n}\n\nconst UUID_V7_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\nconst UUID_BYTES = 16\nconst MAX_MSECS = 0xffffffffffff\nconst MAX_SEQ = 0xffffffff\n\n// Reusable buffer for string output path - avoids allocation per call.\n// Safe because bytes are consumed synchronously by formatUuidUnchecked().\nconst reusableBuf = new Uint8Array(UUID_BYTES)\n\ntype V7State = {\n msecs: number\n seq: number\n}\n\n/**\n * Module-level state for maintaining monotonic ordering within the same millisecond.\n *\n * IMPORTANT: This state persists across all uuidv7() calls in the module's lifetime.\n * - In serverless/edge functions with warm starts, state persists between invocations.\n * - For isolated state, pass explicit `msecs` and `seq` via options.\n * - Tests should mock Date.now() or provide explicit options for deterministic behavior.\n */\nconst state: V7State = { msecs: -Infinity, seq: 0 }\n\nfunction writeV7BytesUnchecked(rnds: Uint8Array, msecs: number, seq: number, buf: Uint8Array, offset: number): void {\n // Keep this inline on the UUID v7 hot path. Using writeTimestamp48() here\n // benchmarked slower than the direct byte writes.\n buf[offset++] = (msecs / 0x10000000000) & 0xff\n buf[offset++] = (msecs / 0x100000000) & 0xff\n buf[offset++] = (msecs / 0x1000000) & 0xff\n buf[offset++] = (msecs / 0x10000) & 0xff\n buf[offset++] = (msecs / 0x100) & 0xff\n buf[offset++] = msecs & 0xff\n\n // Set version (7) and variant (10xx), then pack sequence and random tail bytes.\n buf[offset++] = 0x70 | ((seq >>> 28) & 0x0f)\n buf[offset++] = (seq >>> 20) & 0xff\n buf[offset++] = 0x80 | ((seq >>> 14) & 0x3f)\n buf[offset++] = (seq >>> 6) & 0xff\n // Lower seq bits plus 2 random bits to complete the 128-bit payload.\n buf[offset++] = ((seq << 2) & 0xff) | (rnds[10] & 0x03)\n buf[offset++] = rnds[11]\n buf[offset++] = rnds[12]\n buf[offset++] = rnds[13]\n buf[offset++] = rnds[14]\n buf[offset++] = rnds[15]\n}\n\nfunction writeV7Bytes(rnds: Uint8Array, msecs: number, seq: number, buf: Uint8Array, offset: number): void {\n if (!isWritableRange(buf, offset, UUID_BYTES)) {\n throw new BufferError(\n 'UUID_BUFFER_OUT_OF_BOUNDS',\n `UUID byte range ${offset}:${offset + UUID_BYTES - 1} is out of buffer bounds`,\n )\n }\n writeV7BytesUnchecked(rnds, msecs, seq, buf, offset)\n}\n\nfunction v7WithOptions<TBuf extends Uint8Array = Uint8Array>(\n options: UuidV7Options,\n buf?: TBuf,\n offset = 0,\n): string | TBuf {\n const optMsecs = options.msecs\n if (optMsecs !== undefined && !isIntegerInRange(optMsecs, 0, MAX_MSECS)) {\n throw new InvalidInputError(\n 'UUID_TIMESTAMP_OUT_OF_RANGE',\n `Timestamp must be an integer between 0 and ${MAX_MSECS}`,\n )\n }\n const optSeq = options.seq\n if (optSeq !== undefined && !isIntegerInRange(optSeq, 0, MAX_SEQ)) {\n throw new InvalidInputError('UUID_SEQUENCE_OUT_OF_RANGE', `Sequence must be an integer between 0 and ${MAX_SEQ}`)\n }\n const optRandom = options.random\n if (optRandom && optRandom.length < UUID_BYTES) {\n throw new InvalidInputError('UUID_RANDOM_BYTES_TOO_SHORT', `Random bytes length must be >= ${UUID_BYTES}`)\n }\n\n const rnds = optRandom ?? rng()\n const msecs = optMsecs ?? Date.now()\n // Derive a 31-bit sequence if not provided by the caller, matching the default hot path.\n const seq = optSeq ?? (rnds[6] << 23) | (rnds[7] << 16) | (rnds[8] << 8) | rnds[9]\n\n if (buf) {\n writeV7Bytes(rnds, msecs, seq, buf, offset)\n return buf\n }\n writeV7BytesUnchecked(rnds, msecs, seq, reusableBuf, 0)\n return formatUuidUnchecked(reusableBuf)\n}\n\n/*\n * Overload: no buffer => return a UUID string.\n */\nfunction v7(options?: UuidV7Options, buf?: undefined, offset?: number): string\n/*\n * Overload: caller provides a buffer slice to fill with UUID bytes.\n */\nfunction v7<TBuf extends Uint8Array = Uint8Array>(options: UuidV7Options | undefined, buf: TBuf, offset?: number): TBuf\nfunction v7<TBuf extends Uint8Array = Uint8Array>(options?: UuidV7Options, buf?: TBuf, offset?: number): string | TBuf {\n if (options) {\n return v7WithOptions(options, buf, offset)\n }\n\n // HOT PATH: Inline state management and byte generation for best performance\n const now = Date.now()\n const rnds = rng()\n\n // Update state (inlined for performance)\n if (now > state.msecs) {\n state.seq = (rnds[6] << 23) | (rnds[7] << 16) | (rnds[8] << 8) | rnds[9]\n state.msecs = now\n } else {\n state.seq = (state.seq + 1) | 0\n if (state.seq < 0) {\n state.seq = 0\n state.msecs++\n }\n }\n\n if (buf) {\n writeV7Bytes(rnds, state.msecs, state.seq, buf, offset ?? 0)\n return buf\n }\n writeV7BytesUnchecked(rnds, state.msecs, state.seq, reusableBuf, 0)\n return formatUuidUnchecked(reusableBuf)\n}\n\nfunction timestamp(id: string): number {\n const bytes = parseUuid(id)\n let msecs = 0\n for (let i = 0; i < 6; i += 1) {\n msecs = msecs * 256 + bytes[i]\n }\n return msecs\n}\n\nfunction isValid(id: unknown): id is string {\n return typeof id === 'string' && UUID_V7_REGEX.test(id)\n}\n\n/**\n * Generate a UUID v7 string or write the bytes into a buffer.\n *\n * UUID v7 is a time-ordered UUID that embeds a Unix timestamp in milliseconds,\n * making IDs naturally sortable by creation time. Ideal for database primary keys\n * where chronological ordering improves index performance.\n *\n * @example\n * ```ts\n * import { uuidv7 } from 'uniku/uuid/v7'\n *\n * const id = uuidv7()\n * // => \"018e5e5c-7c8a-7000-8000-000000000000\"\n *\n * // Extract timestamp\n * const ts = uuidv7.timestamp(id)\n * console.log(new Date(ts))\n *\n * // Validate\n * uuidv7.isValid(id) // true\n *\n * // Convert to/from bytes\n * const bytes = uuidv7.toBytes(id)\n * const restored = uuidv7.fromBytes(bytes)\n * ```\n */\nexport const uuidv7: UuidV7 = Object.assign(v7, {\n toBytes: parseUuid,\n fromBytes: formatUuid,\n timestamp,\n isValid,\n NIL: '00000000-0000-0000-0000-000000000000',\n MAX: 'ffffffff-ffff-ffff-ffff-ffffffffffff',\n})\n\nexport { BufferError, InvalidInputError, ParseError, UniqueIdError } from '../errors'\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{BufferError as e,ParseError as t}from"./errors.mjs";function n(e){return e>=48&&e<=57?e-48:e>=65&&e<=70?e-65+10:e>=97&&e<=102?e-97+10:-1}const r=[0,0,1,1,2,2,3,3,-1,4,4,5,5,-1,6,6,7,7,-1,8,8,9,9,-1,10,10,11,11,12,12,13,13,14,14,15,15],i=[!0,!1,!0,!1,!0,!1,!0,!1,!1,!0,!1,!0,!1,!1,!0,!1,!0,!1,!1,!0,!1,!0,!1,!1,!0,!1,!0,!1,!0,!1,!0,!1,!0,!1,!0,!1],a=Array.from({length:256},(e,t)=>t.toString(16).padStart(2,`0`));function o(t){if(t.length!==16)throw new e(`UUID_BYTES_INVALID_LENGTH`,`UUID bytes must be exactly 16 bytes, got ${t.length}`);return s(t)}function s(e){return a[e[0]]+a[e[1]]+a[e[2]]+a[e[3]]+`-`+a[e[4]]+a[e[5]]+`-`+a[e[6]]+a[e[7]]+`-`+a[e[8]]+a[e[9]]+`-`+a[e[10]]+a[e[11]]+a[e[12]]+a[e[13]]+a[e[14]]+a[e[15]]}function c(e){if(e.length!==36)throw new t(`UUID_INVALID_LENGTH`,`UUID string must be 36 characters, got ${e.length}`);if(e[8]!==`-`||e[13]!==`-`||e[18]!==`-`||e[23]!==`-`)throw new t(`UUID_INVALID_SEPARATORS`,`UUID string has invalid separators at positions 8, 13, 18, 23. Received: "${e}"`);let a=new Uint8Array(16);for(let o=0;o<36;o+=1){let s=r[o];if(s===-1)continue;let c=n(e.charCodeAt(o));if(c===-1)throw new t(`UUID_INVALID_HEX_CHAR`,`UUID string contains invalid hex character at position ${o}`);i[o]?a[s]=c<<4:a[s]|=c}return a}export{s as n,c as r,o as t};
|
|
2
|
+
//# sourceMappingURL=uuid-CnQIYoQi.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uuid-CnQIYoQi.mjs","names":[],"sources":["../src/uuid/common/hex.ts","../src/uuid/common/uuid.ts"],"sourcesContent":["// ASCII code ranges for hex character validation\nconst ASCII_0 = 48 // '0'\nconst ASCII_9 = 57 // '9'\nconst ASCII_A = 65 // 'A'\nconst ASCII_F = 70 // 'F'\nconst ASCII_a = 97 // 'a'\nconst ASCII_f = 102 // 'f'\n\n/**\n * Convert a hex character ASCII code to its numeric value (0-15).\n * Returns -1 for invalid hex characters.\n */\nexport function hexValue(code: number): number {\n if (code >= ASCII_0 && code <= ASCII_9) {\n return code - ASCII_0\n }\n if (code >= ASCII_A && code <= ASCII_F) {\n return code - ASCII_A + 10\n }\n if (code >= ASCII_a && code <= ASCII_f) {\n return code - ASCII_a + 10\n }\n return -1 // Invalid hex character\n}\n","import { BufferError, ParseError } from '../../errors'\nimport { hexValue } from './hex'\n\nconst UUID_BYTE_LENGTH = 16\nconst UUID_STRING_LENGTH = 36\n\n// Mapping from UUID string position to byte index.\n// -1 indicates a dash position that should be skipped.\n// This avoids intermediate string allocations during parsing.\nconst UUID_CHAR_TO_BYTE: number[] = [\n 0,\n 0,\n 1,\n 1,\n 2,\n 2,\n 3,\n 3, // chars 0-7 → bytes 0-3\n -1, // char 8 is '-'\n 4,\n 4,\n 5,\n 5, // chars 9-12 → bytes 4-5\n -1, // char 13 is '-'\n 6,\n 6,\n 7,\n 7, // chars 14-17 → bytes 6-7\n -1, // char 18 is '-'\n 8,\n 8,\n 9,\n 9, // chars 19-22 → bytes 8-9\n -1, // char 23 is '-'\n 10,\n 10,\n 11,\n 11,\n 12,\n 12,\n 13,\n 13,\n 14,\n 14,\n 15,\n 15, // chars 24-35 → bytes 10-15\n]\n\n// Whether each position is the high nibble (true) or low nibble (false)\nconst UUID_CHAR_IS_HIGH: boolean[] = [\n true,\n false,\n true,\n false,\n true,\n false,\n true,\n false, // chars 0-7\n false, // dash (ignored)\n true,\n false,\n true,\n false, // chars 9-12\n false, // dash\n true,\n false,\n true,\n false, // chars 14-17\n false, // dash\n true,\n false,\n true,\n false, // chars 19-22\n false, // dash\n true,\n false,\n true,\n false,\n true,\n false,\n true,\n false,\n true,\n false,\n true,\n false, // chars 24-35\n]\n\n// Pre-computed lookup table for byte-to-hex conversion (0x00 -> \"00\", 0xff -> \"ff\")\n//\n// Note: this table must remain defined in the same module as `formatUuid`, otherwise the v8 optimizer\n// will cause a performance drop of ~36%.\nconst HEX_TABLE: string[] = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'))\n\nexport function formatUuid(bytes: Uint8Array): string {\n if (bytes.length !== UUID_BYTE_LENGTH) {\n throw new BufferError(\n 'UUID_BYTES_INVALID_LENGTH',\n `UUID bytes must be exactly ${UUID_BYTE_LENGTH} bytes, got ${bytes.length}`,\n )\n }\n\n return formatUuidUnchecked(bytes)\n}\n\nexport function formatUuidUnchecked(bytes: Uint8Array): string {\n // Direct string concatenation - optimized for V8's string builder.\n // This approach avoids loop overhead and intermediate allocations.\n // See: https://github.com/uuidjs/uuid/pull/434\n return (\n HEX_TABLE[bytes[0]] +\n HEX_TABLE[bytes[1]] +\n HEX_TABLE[bytes[2]] +\n HEX_TABLE[bytes[3]] +\n '-' +\n HEX_TABLE[bytes[4]] +\n HEX_TABLE[bytes[5]] +\n '-' +\n HEX_TABLE[bytes[6]] +\n HEX_TABLE[bytes[7]] +\n '-' +\n HEX_TABLE[bytes[8]] +\n HEX_TABLE[bytes[9]] +\n '-' +\n HEX_TABLE[bytes[10]] +\n HEX_TABLE[bytes[11]] +\n HEX_TABLE[bytes[12]] +\n HEX_TABLE[bytes[13]] +\n HEX_TABLE[bytes[14]] +\n HEX_TABLE[bytes[15]]\n )\n}\n\nexport function parseUuid(value: string): Uint8Array {\n if (value.length !== UUID_STRING_LENGTH) {\n throw new ParseError('UUID_INVALID_LENGTH', `UUID string must be 36 characters, got ${value.length}`)\n }\n\n // Validate separator positions directly (more efficient than full loop)\n if (value[8] !== '-' || value[13] !== '-' || value[18] !== '-' || value[23] !== '-') {\n throw new ParseError(\n 'UUID_INVALID_SEPARATORS',\n `UUID string has invalid separators at positions 8, 13, 18, 23. Received: \"${value}\"`,\n )\n }\n\n // Parse bytes directly from UUID string without intermediate string allocations.\n // This avoids the 9 allocations (5 slices + 4 concatenations) of the naive approach.\n const bytes = new Uint8Array(UUID_BYTE_LENGTH)\n\n for (let i = 0; i < UUID_STRING_LENGTH; i += 1) {\n const byteIdx = UUID_CHAR_TO_BYTE[i]\n if (byteIdx === -1) continue // Skip dash positions\n\n const nibble = hexValue(value.charCodeAt(i))\n if (nibble === -1) {\n throw new ParseError('UUID_INVALID_HEX_CHAR', `UUID string contains invalid hex character at position ${i}`)\n }\n\n if (UUID_CHAR_IS_HIGH[i]) {\n bytes[byteIdx] = nibble << 4\n } else {\n bytes[byteIdx] |= nibble\n }\n }\n\n return bytes\n}\n"],"mappings":"2DAYA,SAAgB,EAAS,EAAsB,CAU7C,OATI,GAAQ,IAAW,GAAQ,GACtB,EAAO,GAEZ,GAAQ,IAAW,GAAQ,GACtB,EAAO,GAAU,GAEtB,GAAQ,IAAW,GAAQ,IACtB,EAAO,GAAU,GAEnB,EACT,CCpBA,MAMM,EAA8B,CAClC,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,GACA,EACA,EACA,EACA,EACA,GACA,EACA,EACA,EACA,EACA,GACA,EACA,EACA,EACA,EACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,EACF,EAGM,EAA+B,CACnC,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,EACF,EAMM,EAAsB,MAAM,KAAK,CAAE,OAAQ,GAAI,GAAI,EAAG,IAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,CAAC,EAEjG,SAAgB,EAAW,EAA2B,CACpD,GAAI,EAAM,SAAW,GACnB,MAAM,IAAI,EACR,4BACA,4CAA6D,EAAM,QACrE,EAGF,OAAO,EAAoB,CAAK,CAClC,CAEA,SAAgB,EAAoB,EAA2B,CAI7D,OACE,EAAU,EAAM,IAChB,EAAU,EAAM,IAChB,EAAU,EAAM,IAChB,EAAU,EAAM,IAChB,IACA,EAAU,EAAM,IAChB,EAAU,EAAM,IAChB,IACA,EAAU,EAAM,IAChB,EAAU,EAAM,IAChB,IACA,EAAU,EAAM,IAChB,EAAU,EAAM,IAChB,IACA,EAAU,EAAM,KAChB,EAAU,EAAM,KAChB,EAAU,EAAM,KAChB,EAAU,EAAM,KAChB,EAAU,EAAM,KAChB,EAAU,EAAM,IAEpB,CAEA,SAAgB,EAAU,EAA2B,CACnD,GAAI,EAAM,SAAW,GACnB,MAAM,IAAI,EAAW,sBAAuB,0CAA0C,EAAM,QAAQ,EAItG,GAAI,EAAM,KAAO,KAAO,EAAM,MAAQ,KAAO,EAAM,MAAQ,KAAO,EAAM,MAAQ,IAC9E,MAAM,IAAI,EACR,0BACA,6EAA6E,EAAM,EACrF,EAKF,IAAM,EAAQ,IAAI,WAAW,EAAgB,EAE7C,IAAK,IAAI,EAAI,EAAG,EAAI,GAAoB,GAAK,EAAG,CAC9C,IAAM,EAAU,EAAkB,GAClC,GAAI,IAAY,GAAI,SAEpB,IAAM,EAAS,EAAS,EAAM,WAAW,CAAC,CAAC,EAC3C,GAAI,IAAW,GACb,MAAM,IAAI,EAAW,wBAAyB,0DAA0D,GAAG,EAGzG,EAAkB,GACpB,EAAM,GAAW,GAAU,EAE3B,EAAM,IAAY,CAEtB,CAEA,OAAO,CACT"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation-CTNpXm94.mjs","names":[],"sources":["../src/common/validation.ts"],"sourcesContent":["export function isIntegerInRange(value: number, min: number, max: number): boolean {\n return Number.isInteger(value) && value >= min && value <= max\n}\n\nexport function isWritableRange(buffer: Uint8Array, offset: number, byteLength: number): boolean {\n return Number.isInteger(offset) && offset >= 0 && offset + byteLength <= buffer.length\n}\n"],"mappings":"AAAA,SAAgB,EAAiB,EAAe,EAAa,EAAsB,CACjF,OAAO,OAAO,UAAU,CAAK,GAAK,GAAS,GAAO,GAAS,CAC7D,CAEA,SAAgB,EAAgB,EAAoB,EAAgB,EAA6B,CAC/F,OAAO,OAAO,UAAU,CAAM,GAAK,GAAU,GAAK,EAAS,GAAc,EAAO,MAClF"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function isIntegerInRange(value: number, min: number, max: number): boolean {
|
|
2
|
+
return Number.isInteger(value) && value >= min && value <= max
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isWritableRange(buffer: Uint8Array, offset: number, byteLength: number): boolean {
|
|
6
|
+
return Number.isInteger(offset) && offset >= 0 && offset + byteLength <= buffer.length
|
|
7
|
+
}
|
package/src/cuid2/cuid2.ts
CHANGED
|
@@ -135,14 +135,18 @@ function getRandomFn(random?: Uint8Array): () => number {
|
|
|
135
135
|
// --- Main generator ---
|
|
136
136
|
|
|
137
137
|
function cuid2Fn(options?: Cuid2Options): string {
|
|
138
|
-
const
|
|
138
|
+
const requestedLength = options?.length
|
|
139
139
|
|
|
140
|
-
if (
|
|
140
|
+
if (
|
|
141
|
+
requestedLength !== undefined &&
|
|
142
|
+
(!Number.isInteger(requestedLength) || requestedLength < MIN_LENGTH || requestedLength > MAX_LENGTH)
|
|
143
|
+
) {
|
|
141
144
|
throw new InvalidInputError(
|
|
142
145
|
'CUID2_LENGTH_OUT_OF_RANGE',
|
|
143
|
-
`CUID2 length must be between ${MIN_LENGTH} and ${MAX_LENGTH}. Received: ${
|
|
146
|
+
`CUID2 length must be between ${MIN_LENGTH} and ${MAX_LENGTH}. Received: ${requestedLength}`,
|
|
144
147
|
)
|
|
145
148
|
}
|
|
149
|
+
const length = requestedLength ?? DEFAULT_LENGTH
|
|
146
150
|
|
|
147
151
|
const random = getRandomFn(options?.random)
|
|
148
152
|
|
package/src/ksuid/ksuid.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { writeTimestamp32 } from '../common/bytes'
|
|
2
2
|
import { rng } from '../common/random'
|
|
3
|
+
import { isWritableRange } from '../common/validation'
|
|
3
4
|
import { BufferError, InvalidInputError } from '../errors'
|
|
4
5
|
import { decodeBase62, encodeBase62 } from './base62'
|
|
5
6
|
|
|
@@ -58,19 +59,9 @@ export type Ksuid = {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
|
-
* Write KSUID
|
|
62
|
+
* Write already-validated KSUID fields to a buffer.
|
|
62
63
|
*/
|
|
63
|
-
function
|
|
64
|
-
if (!buf) {
|
|
65
|
-
buf = new Uint8Array(KSUID_BYTES)
|
|
66
|
-
offset = 0
|
|
67
|
-
} else if (offset < 0 || offset + KSUID_BYTES > buf.length) {
|
|
68
|
-
throw new BufferError(
|
|
69
|
-
'KSUID_BUFFER_OUT_OF_BOUNDS',
|
|
70
|
-
`KSUID byte range ${offset}:${offset + KSUID_BYTES - 1} is out of buffer bounds`,
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
64
|
+
function writeKsuidBytesUnchecked(timestamp: number, payload: Uint8Array, buf: Uint8Array, offset: number): void {
|
|
74
65
|
// Timestamp (32-bit big-endian seconds since KSUID epoch) -> bytes 0-3
|
|
75
66
|
writeTimestamp32(buf, offset, timestamp)
|
|
76
67
|
|
|
@@ -78,8 +69,6 @@ function ksuidBytes(timestamp: number, payload: Uint8Array, buf?: Uint8Array, of
|
|
|
78
69
|
for (let i = 0; i < PAYLOAD_BYTES; i += 1) {
|
|
79
70
|
buf[offset + TIMESTAMP_BYTES + i] = payload[i]
|
|
80
71
|
}
|
|
81
|
-
|
|
82
|
-
return buf
|
|
83
72
|
}
|
|
84
73
|
|
|
85
74
|
/*
|
|
@@ -103,7 +92,7 @@ function ksuidFn<TBuf extends Uint8Array = Uint8Array>(options?: KsuidOptions, b
|
|
|
103
92
|
let timestamp: number
|
|
104
93
|
const secs = options?.secs
|
|
105
94
|
if (secs !== undefined) {
|
|
106
|
-
if (secs < KSUID_EPOCH) {
|
|
95
|
+
if (!Number.isInteger(secs) || secs < KSUID_EPOCH) {
|
|
107
96
|
throw new InvalidInputError('KSUID_TIMESTAMP_TOO_LOW', 'Timestamp must be >= KSUID epoch')
|
|
108
97
|
}
|
|
109
98
|
|
|
@@ -126,11 +115,18 @@ function ksuidFn<TBuf extends Uint8Array = Uint8Array>(options?: KsuidOptions, b
|
|
|
126
115
|
const payload = random ?? rng()
|
|
127
116
|
|
|
128
117
|
if (buf) {
|
|
129
|
-
|
|
118
|
+
if (!isWritableRange(buf, offset, KSUID_BYTES)) {
|
|
119
|
+
throw new BufferError(
|
|
120
|
+
'KSUID_BUFFER_OUT_OF_BOUNDS',
|
|
121
|
+
`KSUID byte range ${offset}:${offset + KSUID_BYTES - 1} is out of buffer bounds`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
writeKsuidBytesUnchecked(timestamp, payload, buf, offset)
|
|
130
125
|
return buf
|
|
131
126
|
}
|
|
132
127
|
|
|
133
|
-
const bytes =
|
|
128
|
+
const bytes = new Uint8Array(KSUID_BYTES)
|
|
129
|
+
writeKsuidBytesUnchecked(timestamp, payload, bytes, 0)
|
|
134
130
|
|
|
135
131
|
// String mode: create bytes then encode to Base62
|
|
136
132
|
return encodeBase62(bytes)
|
|
@@ -150,6 +146,12 @@ function toBytes(id: string): Uint8Array {
|
|
|
150
146
|
* Convert 20 bytes to a KSUID string.
|
|
151
147
|
*/
|
|
152
148
|
function fromBytes(bytes: Uint8Array): string {
|
|
149
|
+
if (bytes.length !== KSUID_BYTES) {
|
|
150
|
+
throw new BufferError(
|
|
151
|
+
'KSUID_BYTES_INVALID_LENGTH',
|
|
152
|
+
`KSUID bytes must be exactly ${KSUID_BYTES} bytes, got ${bytes.length}`,
|
|
153
|
+
)
|
|
154
|
+
}
|
|
153
155
|
return encodeBase62(bytes)
|
|
154
156
|
}
|
|
155
157
|
|
package/src/objectid/objectid.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { writeTimestamp32 } from '../common/bytes'
|
|
2
2
|
import { randomBytes, randomUint32 } from '../common/random'
|
|
3
|
+
import { isIntegerInRange, isWritableRange } from '../common/validation'
|
|
3
4
|
import { BufferError, InvalidInputError } from '../errors'
|
|
4
5
|
import { decodeObjectIdHex, encodeObjectIdHex } from './hex'
|
|
5
6
|
|
|
@@ -85,17 +86,13 @@ function freshRandom(): Uint8Array {
|
|
|
85
86
|
return bytes
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
`ObjectID byte range ${offset}:${offset + OBJECTID_BYTES - 1} is out of buffer bounds`,
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
|
|
89
|
+
function writeObjectIdBytesUnchecked(
|
|
90
|
+
secs: number,
|
|
91
|
+
random: Uint8Array,
|
|
92
|
+
counter: number,
|
|
93
|
+
buf: Uint8Array,
|
|
94
|
+
offset: number,
|
|
95
|
+
): void {
|
|
99
96
|
// Timestamp (32-bit big-endian seconds since Unix epoch) -> bytes 0-3
|
|
100
97
|
writeTimestamp32(buf, offset, secs)
|
|
101
98
|
|
|
@@ -108,8 +105,6 @@ function objectIdBytes(secs: number, random: Uint8Array, counter: number, buf?:
|
|
|
108
105
|
buf[offset + TIMESTAMP_BYTES + RANDOM_BYTES] = (counter >>> 16) & 0xff
|
|
109
106
|
buf[offset + TIMESTAMP_BYTES + RANDOM_BYTES + 1] = (counter >>> 8) & 0xff
|
|
110
107
|
buf[offset + TIMESTAMP_BYTES + RANDOM_BYTES + COUNTER_BYTES - 1] = counter & 0xff
|
|
111
|
-
|
|
112
|
-
return buf
|
|
113
108
|
}
|
|
114
109
|
|
|
115
110
|
/*
|
|
@@ -143,12 +138,12 @@ function objectIdFn<TBuf extends Uint8Array = Uint8Array>(
|
|
|
143
138
|
}
|
|
144
139
|
|
|
145
140
|
const optSecs = options.secs
|
|
146
|
-
if (optSecs !== undefined && (optSecs
|
|
141
|
+
if (optSecs !== undefined && !isIntegerInRange(optSecs, 0, MAX_SECS)) {
|
|
147
142
|
throw new InvalidInputError('OBJECTID_TIMESTAMP_OUT_OF_RANGE', `Timestamp must be between 0 and ${MAX_SECS}`)
|
|
148
143
|
}
|
|
149
144
|
|
|
150
145
|
const optCounter = options.counter
|
|
151
|
-
if (optCounter !== undefined && (optCounter
|
|
146
|
+
if (optCounter !== undefined && !isIntegerInRange(optCounter, 0, MAX_COUNTER)) {
|
|
152
147
|
throw new InvalidInputError('OBJECTID_COUNTER_OUT_OF_RANGE', `Counter must be between 0 and ${MAX_COUNTER}`)
|
|
153
148
|
}
|
|
154
149
|
|
|
@@ -183,11 +178,18 @@ function objectIdFn<TBuf extends Uint8Array = Uint8Array>(
|
|
|
183
178
|
}
|
|
184
179
|
|
|
185
180
|
if (buf) {
|
|
186
|
-
|
|
181
|
+
if (!isWritableRange(buf, offset, OBJECTID_BYTES)) {
|
|
182
|
+
throw new BufferError(
|
|
183
|
+
'OBJECTID_BUFFER_OUT_OF_BOUNDS',
|
|
184
|
+
`ObjectID byte range ${offset}:${offset + OBJECTID_BYTES - 1} is out of buffer bounds`,
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
writeObjectIdBytesUnchecked(secs, random, counter, buf, offset)
|
|
187
188
|
return buf
|
|
188
189
|
}
|
|
189
190
|
|
|
190
|
-
const bytes =
|
|
191
|
+
const bytes = new Uint8Array(OBJECTID_BYTES)
|
|
192
|
+
writeObjectIdBytesUnchecked(secs, random, counter, bytes, 0)
|
|
191
193
|
return encodeObjectIdHex(bytes)
|
|
192
194
|
}
|
|
193
195
|
|
|
@@ -202,6 +204,12 @@ function toBytes(id: string): Uint8Array {
|
|
|
202
204
|
* Convert 12 bytes to an ObjectID hex string.
|
|
203
205
|
*/
|
|
204
206
|
function fromBytes(bytes: Uint8Array): string {
|
|
207
|
+
if (bytes.length !== OBJECTID_BYTES) {
|
|
208
|
+
throw new BufferError(
|
|
209
|
+
'OBJECTID_BYTES_INVALID_LENGTH',
|
|
210
|
+
`ObjectID bytes must be exactly ${OBJECTID_BYTES} bytes, got ${bytes.length}`,
|
|
211
|
+
)
|
|
212
|
+
}
|
|
205
213
|
return encodeObjectIdHex(bytes)
|
|
206
214
|
}
|
|
207
215
|
|
package/src/tsid/tsid.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUint32 } from '../common/random'
|
|
2
|
+
import { isIntegerInRange, isWritableRange } from '../common/validation'
|
|
2
3
|
import { BufferError, InvalidInputError } from '../errors'
|
|
3
4
|
import { decodeTsidString, encodeTsidString } from './crockford64'
|
|
4
5
|
|
|
@@ -102,22 +103,60 @@ function pack(msecsDiff: bigint, node: number, counterBits: number, counter: num
|
|
|
102
103
|
return (msecsDiff << BigInt(RANDOM_BITS)) | (BigInt(node) << BigInt(counterBits)) | BigInt(counter)
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
function
|
|
106
|
-
if (!buf) {
|
|
107
|
-
buf = new Uint8Array(TSID_BYTES)
|
|
108
|
-
offset = 0
|
|
109
|
-
} else if (offset < 0 || offset + TSID_BYTES > buf.length) {
|
|
110
|
-
throw new BufferError(
|
|
111
|
-
'TSID_BUFFER_OUT_OF_BOUNDS',
|
|
112
|
-
`TSID byte range ${offset}:${offset + TSID_BYTES - 1} is out of buffer bounds`,
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
106
|
+
function writeTsidBytesUnchecked(value: bigint, buf: Uint8Array, offset: number): void {
|
|
116
107
|
for (let i = 0; i < TSID_BYTES; i += 1) {
|
|
117
108
|
buf[offset + i] = Number((value >> BigInt((TSID_BYTES - 1 - i) * 8)) & 0xffn)
|
|
118
109
|
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type ResolvedTsidOptions = {
|
|
113
|
+
msecsDiff: bigint
|
|
114
|
+
node: number
|
|
115
|
+
counterBits: number
|
|
116
|
+
counter: number
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveTsidOptions(options: TsidOptions): ResolvedTsidOptions {
|
|
120
|
+
const nodeBits = options.nodeBits ?? DEFAULT_NODE_BITS
|
|
121
|
+
if (!isIntegerInRange(nodeBits, 0, MAX_NODE_BITS)) {
|
|
122
|
+
throw new InvalidInputError('TSID_NODE_BITS_OUT_OF_RANGE', `nodeBits must be between 0 and ${MAX_NODE_BITS}`)
|
|
123
|
+
}
|
|
119
124
|
|
|
120
|
-
|
|
125
|
+
const counterBits = RANDOM_BITS - nodeBits
|
|
126
|
+
const nodeMask = (1 << nodeBits) - 1
|
|
127
|
+
const counterMask = (1 << counterBits) - 1
|
|
128
|
+
const optNode = options.node
|
|
129
|
+
if (optNode !== undefined && !isIntegerInRange(optNode, 0, nodeMask)) {
|
|
130
|
+
throw new InvalidInputError('TSID_NODE_OUT_OF_RANGE', `node must be between 0 and ${nodeMask}`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const optCounter = options.counter
|
|
134
|
+
if (optCounter !== undefined && !isIntegerInRange(optCounter, 0, counterMask)) {
|
|
135
|
+
throw new InvalidInputError('TSID_COUNTER_OUT_OF_RANGE', `counter must be between 0 and ${counterMask}`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const epoch = options.epoch ?? TSID_EPOCH
|
|
139
|
+
if (!Number.isInteger(epoch)) {
|
|
140
|
+
throw new InvalidInputError('TSID_EPOCH_INVALID', 'epoch must be a finite integer')
|
|
141
|
+
}
|
|
142
|
+
const msecs = options.msecs ?? Date.now()
|
|
143
|
+
if (!Number.isInteger(msecs)) {
|
|
144
|
+
throw new InvalidInputError('TSID_TIMESTAMP_INVALID', 'msecs must be a finite integer')
|
|
145
|
+
}
|
|
146
|
+
const msecsDiff = BigInt(msecs) - BigInt(epoch)
|
|
147
|
+
if (msecsDiff < 0n || msecsDiff > MAX_TIMESTAMP_DIFF) {
|
|
148
|
+
throw new InvalidInputError(
|
|
149
|
+
'TSID_TIMESTAMP_OUT_OF_RANGE',
|
|
150
|
+
`msecs - epoch must be between 0 and ${MAX_TIMESTAMP_DIFF}`,
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
msecsDiff,
|
|
156
|
+
node: optNode ?? randomUint32() & nodeMask,
|
|
157
|
+
counterBits,
|
|
158
|
+
counter: optCounter ?? randomUint32() & counterMask,
|
|
159
|
+
}
|
|
121
160
|
}
|
|
122
161
|
|
|
123
162
|
/*
|
|
@@ -139,44 +178,11 @@ function tsidFn<TBuf extends Uint8Array = Uint8Array>(options?: TsidOptions, buf
|
|
|
139
178
|
let counter: number
|
|
140
179
|
|
|
141
180
|
if (options) {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const nodeMask = (1 << nodeBits) - 1
|
|
148
|
-
const counterMask = (1 << counterBits) - 1
|
|
149
|
-
|
|
150
|
-
const optNode = options.node
|
|
151
|
-
if (optNode !== undefined) {
|
|
152
|
-
if (optNode < 0 || optNode > nodeMask) {
|
|
153
|
-
throw new InvalidInputError('TSID_NODE_OUT_OF_RANGE', `node must be between 0 and ${nodeMask}`)
|
|
154
|
-
}
|
|
155
|
-
node = optNode
|
|
156
|
-
} else {
|
|
157
|
-
node = randomUint32() & nodeMask
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const optCounter = options.counter
|
|
161
|
-
if (optCounter !== undefined) {
|
|
162
|
-
if (optCounter < 0 || optCounter > counterMask) {
|
|
163
|
-
throw new InvalidInputError('TSID_COUNTER_OUT_OF_RANGE', `counter must be between 0 and ${counterMask}`)
|
|
164
|
-
}
|
|
165
|
-
counter = optCounter
|
|
166
|
-
} else {
|
|
167
|
-
counter = randomUint32() & counterMask
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const epoch = options.epoch ?? TSID_EPOCH
|
|
171
|
-
const msecs = options.msecs ?? Date.now()
|
|
172
|
-
const diff = BigInt(msecs) - BigInt(epoch)
|
|
173
|
-
if (diff < 0n || diff > MAX_TIMESTAMP_DIFF) {
|
|
174
|
-
throw new InvalidInputError(
|
|
175
|
-
'TSID_TIMESTAMP_OUT_OF_RANGE',
|
|
176
|
-
`msecs - epoch must be between 0 and ${MAX_TIMESTAMP_DIFF}`,
|
|
177
|
-
)
|
|
178
|
-
}
|
|
179
|
-
msecsDiff = diff
|
|
181
|
+
const resolved = resolveTsidOptions(options)
|
|
182
|
+
msecsDiff = resolved.msecsDiff
|
|
183
|
+
node = resolved.node
|
|
184
|
+
counterBits = resolved.counterBits
|
|
185
|
+
counter = resolved.counter
|
|
180
186
|
} else {
|
|
181
187
|
// Lazily initialize the persistent node ID on first no-option call.
|
|
182
188
|
if (state.node === undefined) {
|
|
@@ -212,7 +218,13 @@ function tsidFn<TBuf extends Uint8Array = Uint8Array>(options?: TsidOptions, buf
|
|
|
212
218
|
const packed = pack(msecsDiff, node, counterBits, counter)
|
|
213
219
|
|
|
214
220
|
if (buf) {
|
|
215
|
-
|
|
221
|
+
if (!isWritableRange(buf, offset, TSID_BYTES)) {
|
|
222
|
+
throw new BufferError(
|
|
223
|
+
'TSID_BUFFER_OUT_OF_BOUNDS',
|
|
224
|
+
`TSID byte range ${offset}:${offset + TSID_BYTES - 1} is out of buffer bounds`,
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
writeTsidBytesUnchecked(packed, buf, offset)
|
|
216
228
|
return buf
|
|
217
229
|
}
|
|
218
230
|
|
|
@@ -223,7 +235,10 @@ function tsidFn<TBuf extends Uint8Array = Uint8Array>(options?: TsidOptions, buf
|
|
|
223
235
|
* Convert a TSID bigint to 8 bytes.
|
|
224
236
|
*/
|
|
225
237
|
function toBytes(id: bigint): Uint8Array {
|
|
226
|
-
|
|
238
|
+
assertValidTsid(id)
|
|
239
|
+
const bytes = new Uint8Array(TSID_BYTES)
|
|
240
|
+
writeTsidBytesUnchecked(id, bytes, 0)
|
|
241
|
+
return bytes
|
|
227
242
|
}
|
|
228
243
|
|
|
229
244
|
/**
|
|
@@ -250,6 +265,10 @@ function fromBytes(bytes: Uint8Array): bigint {
|
|
|
250
265
|
* `epoch` used at generation time if it was overridden from the default.
|
|
251
266
|
*/
|
|
252
267
|
function timestamp(id: bigint, epoch: number = TSID_EPOCH): number {
|
|
268
|
+
assertValidTsid(id)
|
|
269
|
+
if (!Number.isInteger(epoch)) {
|
|
270
|
+
throw new InvalidInputError('TSID_EPOCH_INVALID', 'epoch must be a finite integer')
|
|
271
|
+
}
|
|
253
272
|
return epoch + Number(id >> BigInt(RANDOM_BITS))
|
|
254
273
|
}
|
|
255
274
|
|
|
@@ -263,6 +282,17 @@ function isValid(id: unknown): id is bigint {
|
|
|
263
282
|
const NIL = 0n
|
|
264
283
|
const MAX = (1n << 64n) - 1n
|
|
265
284
|
|
|
285
|
+
function assertValidTsid(id: bigint): void {
|
|
286
|
+
if (id < NIL || id > MAX) {
|
|
287
|
+
throw new InvalidInputError('TSID_VALUE_OUT_OF_RANGE', `TSID value must be between ${NIL} and ${MAX}`)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function toString(id: bigint): string {
|
|
292
|
+
assertValidTsid(id)
|
|
293
|
+
return encodeTsidString(id)
|
|
294
|
+
}
|
|
295
|
+
|
|
266
296
|
/**
|
|
267
297
|
* Generate a TSID bigint or write the bytes into a buffer.
|
|
268
298
|
*
|
|
@@ -299,7 +329,7 @@ const MAX = (1n << 64n) - 1n
|
|
|
299
329
|
export const tsid: Tsid = Object.assign(tsidFn, {
|
|
300
330
|
toBytes,
|
|
301
331
|
fromBytes,
|
|
302
|
-
toString
|
|
332
|
+
toString,
|
|
303
333
|
fromString: decodeTsidString,
|
|
304
334
|
timestamp,
|
|
305
335
|
isValid,
|
package/src/ulid/crockford.ts
CHANGED
|
@@ -231,8 +231,8 @@ export function decodeToBytes(str: string): Uint8Array {
|
|
|
231
231
|
* Encode 16 bytes to a 26-character ULID string.
|
|
232
232
|
*/
|
|
233
233
|
export function bytesToUlid(bytes: Uint8Array): string {
|
|
234
|
-
if (bytes.length
|
|
235
|
-
throw new BufferError('
|
|
234
|
+
if (bytes.length !== 16) {
|
|
235
|
+
throw new BufferError('ULID_BYTES_INVALID_LENGTH', `ULID bytes must be exactly 16 bytes, got ${bytes.length}`)
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
// Timestamp: bytes 0-5 -> 10 characters
|
package/src/ulid/ulid.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { incrementBytesInPlace, writeTimestamp48 } from '../common/bytes'
|
|
2
2
|
import { rng } from '../common/random'
|
|
3
|
+
import { isIntegerInRange, isWritableRange } from '../common/validation'
|
|
3
4
|
import { BufferError, InvalidInputError } from '../errors'
|
|
4
5
|
import { bytesToUlid, decodeToBytes, decodeUlidTime, encodeRandom, encodeTime } from './crockford'
|
|
5
6
|
|
|
@@ -32,6 +33,9 @@ export type Ulid = {
|
|
|
32
33
|
|
|
33
34
|
// Validation regex: first char [0-7] to prevent overflow, rest from Crockford alphabet
|
|
34
35
|
const ULID_REGEX = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/i
|
|
36
|
+
const ULID_BYTES = 16
|
|
37
|
+
const RANDOM_BYTES = 10
|
|
38
|
+
const MAX_MSECS = 0xffffffffffff
|
|
35
39
|
|
|
36
40
|
type UlidState = {
|
|
37
41
|
msecs: number
|
|
@@ -48,29 +52,27 @@ type UlidState = {
|
|
|
48
52
|
*/
|
|
49
53
|
const state: UlidState = {
|
|
50
54
|
msecs: -Infinity,
|
|
51
|
-
lastRandom: new Uint8Array(
|
|
55
|
+
lastRandom: new Uint8Array(RANDOM_BYTES),
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
function
|
|
55
|
-
if (!buf) {
|
|
56
|
-
buf = new Uint8Array(16)
|
|
57
|
-
offset = 0
|
|
58
|
-
} else if (offset < 0 || offset + 16 > buf.length) {
|
|
59
|
-
throw new BufferError(
|
|
60
|
-
'ULID_BUFFER_OUT_OF_BOUNDS',
|
|
61
|
-
`ULID byte range ${offset}:${offset + 15} is out of buffer bounds`,
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
58
|
+
function writeUlidBytesUnchecked(time: number, random: Uint8Array, buf: Uint8Array, offset: number): void {
|
|
65
59
|
// Timestamp (48-bit big-endian milliseconds since Unix epoch) -> bytes 0-5
|
|
66
60
|
writeTimestamp48(buf, offset, time)
|
|
67
61
|
|
|
68
62
|
// Random (80 bits) -> bytes 6-15
|
|
69
|
-
for (let i = 0; i <
|
|
63
|
+
for (let i = 0; i < RANDOM_BYTES; i += 1) {
|
|
70
64
|
buf[offset + 6 + i] = random[i]
|
|
71
65
|
}
|
|
66
|
+
}
|
|
72
67
|
|
|
73
|
-
|
|
68
|
+
function writeUlidBytes(time: number, random: Uint8Array, buf: Uint8Array, offset: number): void {
|
|
69
|
+
if (!isWritableRange(buf, offset, ULID_BYTES)) {
|
|
70
|
+
throw new BufferError(
|
|
71
|
+
'ULID_BUFFER_OUT_OF_BOUNDS',
|
|
72
|
+
`ULID byte range ${offset}:${offset + ULID_BYTES - 1} is out of buffer bounds`,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
writeUlidBytesUnchecked(time, random, buf, offset)
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
/*
|
|
@@ -97,27 +99,36 @@ function ulidFn<TBuf extends Uint8Array = Uint8Array>(options?: UlidOptions, buf
|
|
|
97
99
|
* - all ULIDs generated within a single request will have the same timestamp.
|
|
98
100
|
* - the monotonic ordering will rely entirely on incrementing the random portion.
|
|
99
101
|
*/
|
|
100
|
-
const defaultTime = Date.now()
|
|
101
|
-
|
|
102
102
|
if (options) {
|
|
103
103
|
// Explicit options provided - use them directly without monotonic state
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
const optMsecs = options.msecs
|
|
105
|
+
if (optMsecs !== undefined && !isIntegerInRange(optMsecs, 0, MAX_MSECS)) {
|
|
106
|
+
throw new InvalidInputError(
|
|
107
|
+
'ULID_TIMESTAMP_OUT_OF_RANGE',
|
|
108
|
+
`Timestamp must be an integer between 0 and ${MAX_MSECS}`,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
time = optMsecs ?? Date.now()
|
|
112
|
+
const optRandom = options.random
|
|
113
|
+
if (optRandom) {
|
|
114
|
+
if (optRandom.length < RANDOM_BYTES) {
|
|
115
|
+
throw new InvalidInputError(
|
|
116
|
+
'ULID_RANDOM_BYTES_TOO_SHORT',
|
|
117
|
+
`Random bytes length must be >= ${RANDOM_BYTES} for ULID`,
|
|
118
|
+
)
|
|
108
119
|
}
|
|
109
|
-
random =
|
|
120
|
+
random = optRandom
|
|
110
121
|
} else {
|
|
111
122
|
random = rng()
|
|
112
123
|
}
|
|
113
124
|
} else {
|
|
114
|
-
time =
|
|
125
|
+
time = Date.now()
|
|
115
126
|
|
|
116
127
|
if (time > state.msecs) {
|
|
117
128
|
// New millisecond: generate fresh random
|
|
118
129
|
random = rng()
|
|
119
130
|
state.msecs = time
|
|
120
|
-
state.lastRandom.set(random.subarray(0,
|
|
131
|
+
state.lastRandom.set(random.subarray(0, RANDOM_BYTES))
|
|
121
132
|
} else {
|
|
122
133
|
// Same millisecond or clock rollback: preserve last timestamp and increment random portion.
|
|
123
134
|
time = state.msecs
|
|
@@ -133,7 +144,7 @@ function ulidFn<TBuf extends Uint8Array = Uint8Array>(options?: UlidOptions, buf
|
|
|
133
144
|
}
|
|
134
145
|
|
|
135
146
|
if (buf) {
|
|
136
|
-
|
|
147
|
+
writeUlidBytes(time, random, buf, offset)
|
|
137
148
|
return buf
|
|
138
149
|
}
|
|
139
150
|
|