sigilid 0.1.0
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/LICENSE +7 -0
- package/README.md +37 -0
- package/dist/alphabet.cjs +82 -0
- package/dist/alphabet.cjs.map +1 -0
- package/dist/alphabet.d.cts +34 -0
- package/dist/alphabet.d.ts +34 -0
- package/dist/alphabet.js +79 -0
- package/dist/alphabet.js.map +1 -0
- package/dist/index.cjs +51 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/non-secure.cjs +31 -0
- package/dist/non-secure.cjs.map +1 -0
- package/dist/non-secure.d.cts +19 -0
- package/dist/non-secure.d.ts +19 -0
- package/dist/non-secure.js +29 -0
- package/dist/non-secure.js.map +1 -0
- package/dist/prefix.cjs +72 -0
- package/dist/prefix.cjs.map +1 -0
- package/dist/prefix.d.cts +33 -0
- package/dist/prefix.d.ts +33 -0
- package/dist/prefix.js +69 -0
- package/dist/prefix.js.map +1 -0
- package/dist/typed.cjs +70 -0
- package/dist/typed.cjs.map +1 -0
- package/dist/typed.d.cts +50 -0
- package/dist/typed.d.ts +50 -0
- package/dist/typed.js +67 -0
- package/dist/typed.js.map +1 -0
- package/dist/validate.cjs +45 -0
- package/dist/validate.cjs.map +1 -0
- package/dist/validate.d.cts +46 -0
- package/dist/validate.d.ts +46 -0
- package/dist/validate.js +41 -0
- package/dist/validate.js.map +1 -0
- package/package.json +89 -0
package/dist/prefix.cjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/internal/alphabet.ts
|
|
4
|
+
function generateFromAlphabet(alphabet, length, getBytes) {
|
|
5
|
+
const alphabetSize = alphabet.length;
|
|
6
|
+
let mask = 1;
|
|
7
|
+
while (mask < alphabetSize) mask = mask << 1 | 1;
|
|
8
|
+
const bufferMultiplier = Math.ceil(1.6 * mask * length / alphabetSize);
|
|
9
|
+
let result = "";
|
|
10
|
+
while (result.length < length) {
|
|
11
|
+
const bytes = getBytes(bufferMultiplier);
|
|
12
|
+
for (let i = 0; i < bytes.length && result.length < length; i++) {
|
|
13
|
+
const byte = bytes[i] & mask;
|
|
14
|
+
if (byte < alphabetSize) {
|
|
15
|
+
result += alphabet[byte];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/internal/assert.ts
|
|
23
|
+
var MIN_LENGTH = 1;
|
|
24
|
+
var MAX_LENGTH = 255;
|
|
25
|
+
function assertLength(length) {
|
|
26
|
+
if (!Number.isInteger(length) || length < MIN_LENGTH || length > MAX_LENGTH) {
|
|
27
|
+
throw new RangeError(
|
|
28
|
+
`ID length must be an integer between ${MIN_LENGTH} and ${MAX_LENGTH}, got ${length}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function assertPrefix(prefix) {
|
|
33
|
+
if (typeof prefix !== "string" || prefix.length === 0) {
|
|
34
|
+
throw new TypeError("Prefix must be a non-empty string");
|
|
35
|
+
}
|
|
36
|
+
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(prefix)) {
|
|
37
|
+
throw new TypeError("Prefix must start with a letter and contain only letters and digits");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/internal/random.ts
|
|
42
|
+
function randomBytes(count) {
|
|
43
|
+
const bytes = new Uint8Array(count);
|
|
44
|
+
crypto.getRandomValues(bytes);
|
|
45
|
+
return bytes;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/index.ts
|
|
49
|
+
var DEFAULT_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
|
|
50
|
+
|
|
51
|
+
// src/prefix.ts
|
|
52
|
+
var DEFAULT_LENGTH = 21;
|
|
53
|
+
var SEPARATOR = "_";
|
|
54
|
+
function generatePrefixedId(prefix, length = DEFAULT_LENGTH) {
|
|
55
|
+
assertPrefix(prefix);
|
|
56
|
+
assertLength(length);
|
|
57
|
+
const id = generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);
|
|
58
|
+
return `${prefix}${SEPARATOR}${id}`;
|
|
59
|
+
}
|
|
60
|
+
function createPrefixedGenerator(prefix, length = DEFAULT_LENGTH) {
|
|
61
|
+
assertPrefix(prefix);
|
|
62
|
+
assertLength(length);
|
|
63
|
+
return () => {
|
|
64
|
+
const id = generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);
|
|
65
|
+
return `${prefix}${SEPARATOR}${id}`;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
exports.createPrefixedGenerator = createPrefixedGenerator;
|
|
70
|
+
exports.generatePrefixedId = generatePrefixedId;
|
|
71
|
+
//# sourceMappingURL=prefix.cjs.map
|
|
72
|
+
//# sourceMappingURL=prefix.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/alphabet.ts","../src/internal/assert.ts","../src/internal/random.ts","../src/index.ts","../src/prefix.ts"],"names":[],"mappings":";;;AAiCO,SAAS,oBAAA,CACd,QAAA,EACA,MAAA,EACA,QAAA,EACQ;AACR,EAAA,MAAM,eAAe,QAAA,CAAS,MAAA;AAG9B,EAAA,IAAI,IAAA,GAAO,CAAA;AACX,EAAA,OAAO,IAAA,GAAO,YAAA,EAAc,IAAA,GAAQ,IAAA,IAAQ,CAAA,GAAK,CAAA;AAGjD,EAAA,MAAM,mBAAmB,IAAA,CAAK,IAAA,CAAM,GAAA,GAAM,IAAA,GAAO,SAAU,YAAY,CAAA;AACvE,EAAA,IAAI,MAAA,GAAS,EAAA;AAEb,EAAA,OAAO,MAAA,CAAO,SAAS,MAAA,EAAQ;AAC7B,IAAA,MAAM,KAAA,GAAQ,SAAS,gBAAgB,CAAA;AACvC,IAAA,KAAA,IAAS,CAAA,GAAI,GAAG,CAAA,GAAI,KAAA,CAAM,UAAU,MAAA,CAAO,MAAA,GAAS,QAAQ,CAAA,EAAA,EAAK;AAC/D,MAAA,MAAM,IAAA,GAAQ,KAAA,CAAM,CAAC,CAAA,GAAe,IAAA;AACpC,MAAA,IAAI,OAAO,YAAA,EAAc;AACvB,QAAA,MAAA,IAAU,SAAS,IAAI,CAAA;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;;;AC3DA,IAAM,UAAA,GAAa,CAAA;AACnB,IAAM,UAAA,GAAa,GAAA;AAEZ,SAAS,aAAa,MAAA,EAAsB;AACjD,EAAA,IAAI,CAAC,OAAO,SAAA,CAAU,MAAM,KAAK,MAAA,GAAS,UAAA,IAAc,SAAS,UAAA,EAAY;AAC3E,IAAA,MAAM,IAAI,UAAA;AAAA,MACR,CAAA,qCAAA,EAAwC,UAAU,CAAA,KAAA,EAAQ,UAAU,SAAS,MAAM,CAAA;AAAA,KACrF;AAAA,EACF;AACF;AAEO,SAAS,aAAa,MAAA,EAAsB;AACjD,EAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,CAAO,WAAW,CAAA,EAAG;AACrD,IAAA,MAAM,IAAI,UAAU,mCAAmC,CAAA;AAAA,EACzD;AACA,EAAA,IAAI,CAAC,wBAAA,CAAyB,IAAA,CAAK,MAAM,CAAA,EAAG;AAC1C,IAAA,MAAM,IAAI,UAAU,qEAAqE,CAAA;AAAA,EAC3F;AACF;;;AChBO,SAAS,YAAY,KAAA,EAA2B;AACrD,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,KAAK,CAAA;AAClC,EAAA,MAAA,CAAO,gBAAgB,KAAK,CAAA;AAC5B,EAAA,OAAO,KAAA;AACT;;;ACEO,IAAM,gBAAA,GAAmB,kEAAA;;;ACHhC,IAAM,cAAA,GAAiB,EAAA;AACvB,IAAM,SAAA,GAAY,GAAA;AAeX,SAAS,kBAAA,CAAmB,MAAA,EAAgB,MAAA,GAAiB,cAAA,EAAwB;AAC1F,EAAA,YAAA,CAAa,MAAM,CAAA;AACnB,EAAA,YAAA,CAAa,MAAM,CAAA;AACnB,EAAA,MAAM,EAAA,GAAK,oBAAA,CAAqB,gBAAA,EAAkB,MAAA,EAAQ,WAAW,CAAA;AACrE,EAAA,OAAO,CAAA,EAAG,MAAM,CAAA,EAAG,SAAS,GAAG,EAAE,CAAA,CAAA;AACnC;AAkBO,SAAS,uBAAA,CACd,MAAA,EACA,MAAA,GAAiB,cAAA,EACH;AAEd,EAAA,YAAA,CAAa,MAAM,CAAA;AACnB,EAAA,YAAA,CAAa,MAAM,CAAA;AACnB,EAAA,OAAO,MAAM;AACX,IAAA,MAAM,EAAA,GAAK,oBAAA,CAAqB,gBAAA,EAAkB,MAAA,EAAQ,WAAW,CAAA;AACrE,IAAA,OAAO,CAAA,EAAG,MAAM,CAAA,EAAG,SAAS,GAAG,EAAE,CAAA,CAAA;AAAA,EACnC,CAAA;AACF","file":"prefix.cjs","sourcesContent":["const MIN_ALPHABET_SIZE = 2;\nconst MAX_ALPHABET_SIZE = 256;\n\nexport function validateAlphabetString(alphabet: string): void {\n if (typeof alphabet !== \"string\") {\n throw new TypeError(\"Alphabet must be a string\");\n }\n if (alphabet.length < MIN_ALPHABET_SIZE) {\n throw new RangeError(\n `Alphabet must have at least ${MIN_ALPHABET_SIZE} characters, got ${alphabet.length}`,\n );\n }\n if (alphabet.length > MAX_ALPHABET_SIZE) {\n throw new RangeError(\n `Alphabet must have at most ${MAX_ALPHABET_SIZE} characters, got ${alphabet.length}`,\n );\n }\n const seen = new Set<string>();\n for (const char of alphabet) {\n if (seen.has(char)) {\n throw new TypeError(`Alphabet contains duplicate character: \"${char}\"`);\n }\n seen.add(char);\n }\n}\n\n/**\n * Generates an ID of `length` characters drawn from `alphabet` using\n * rejection sampling to avoid modulo bias.\n *\n * The caller is responsible for providing valid `alphabet` and `length` values.\n * Use `validateAlphabetString` and `assertLength` before calling this.\n */\nexport function generateFromAlphabet(\n alphabet: string,\n length: number,\n getBytes: (count: number) => Uint8Array,\n): string {\n const alphabetSize = alphabet.length;\n\n // Find the smallest bitmask >= alphabetSize to reduce rejection rate.\n let mask = 1;\n while (mask < alphabetSize) mask = (mask << 1) | 1;\n\n // Overshoot buffer size to reduce round-trips. Most cases complete in one pass.\n const bufferMultiplier = Math.ceil((1.6 * mask * length) / alphabetSize);\n let result = \"\";\n\n while (result.length < length) {\n const bytes = getBytes(bufferMultiplier);\n for (let i = 0; i < bytes.length && result.length < length; i++) {\n const byte = (bytes[i] as number) & mask;\n if (byte < alphabetSize) {\n result += alphabet[byte];\n }\n }\n }\n\n return result;\n}\n","const MIN_LENGTH = 1;\nconst MAX_LENGTH = 255;\n\nexport function assertLength(length: number): void {\n if (!Number.isInteger(length) || length < MIN_LENGTH || length > MAX_LENGTH) {\n throw new RangeError(\n `ID length must be an integer between ${MIN_LENGTH} and ${MAX_LENGTH}, got ${length}`,\n );\n }\n}\n\nexport function assertPrefix(prefix: string): void {\n if (typeof prefix !== \"string\" || prefix.length === 0) {\n throw new TypeError(\"Prefix must be a non-empty string\");\n }\n if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(prefix)) {\n throw new TypeError(\"Prefix must start with a letter and contain only letters and digits\");\n }\n}\n","// Returns a Uint8Array of `count` cryptographically secure random bytes.\n// Works in Node 18+, all modern browsers, and edge runtimes that expose `crypto`.\nexport function randomBytes(count: number): Uint8Array {\n const bytes = new Uint8Array(count);\n crypto.getRandomValues(bytes);\n return bytes;\n}\n","import { generateFromAlphabet } from \"./internal/alphabet.js\";\nimport { assertLength } from \"./internal/assert.js\";\nimport { randomBytes } from \"./internal/random.js\";\n\n/**\n * URL-safe alphabet used by default. 64 characters: A-Z, a-z, 0-9, _, -.\n * Chosen to be safe in URLs, filenames, and most log formats without encoding.\n */\nexport const DEFAULT_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-\";\n\nconst DEFAULT_LENGTH = 21;\n\n/**\n * Generates a cryptographically secure random ID.\n *\n * @param length - Number of characters (1–255). Defaults to 21.\n * @returns A random URL-safe string of the requested length.\n *\n * @example\n * import { generateId } from \"sigilid\";\n * const id = generateId(); // \"K7gkJ_q3vR2nL8xH5eM0w\"\n */\nexport function generateId(length: number = DEFAULT_LENGTH): string {\n assertLength(length);\n return generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);\n}\n","import { DEFAULT_ALPHABET } from \"./index.js\";\nimport { generateFromAlphabet } from \"./internal/alphabet.js\";\nimport { assertLength, assertPrefix } from \"./internal/assert.js\";\nimport { randomBytes } from \"./internal/random.js\";\n\nconst DEFAULT_LENGTH = 21;\nconst SEPARATOR = \"_\";\n\n/**\n * Generates a prefixed, cryptographically secure ID.\n *\n * The format is `{prefix}_{id}`, e.g. `usr_K7gkJ_q3vR2nL8xH5eM0w`.\n *\n * @param prefix - Alphanumeric prefix starting with a letter, e.g. `\"usr\"`.\n * @param length - Length of the random portion (1–255). Defaults to 21.\n * @returns A string in the form `{prefix}_{id}`.\n *\n * @example\n * import { generatePrefixedId } from \"sigilid/prefix\";\n * const userId = generatePrefixedId(\"usr\"); // \"usr_K7gkJ_q3vR2nL8xH5eM0w\"\n */\nexport function generatePrefixedId(prefix: string, length: number = DEFAULT_LENGTH): string {\n assertPrefix(prefix);\n assertLength(length);\n const id = generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);\n return `${prefix}${SEPARATOR}${id}`;\n}\n\n/**\n * Returns a factory function that generates prefixed IDs with fixed settings.\n *\n * Useful when you generate IDs for a specific entity type throughout your\n * codebase and want to avoid repeating the prefix at every call site.\n *\n * @param prefix - Alphanumeric prefix starting with a letter.\n * @param length - Length of the random portion. Defaults to 21.\n * @returns A zero-argument function that returns a new prefixed ID each call.\n *\n * @example\n * import { createPrefixedGenerator } from \"sigilid/prefix\";\n * const userId = createPrefixedGenerator(\"usr\");\n * userId(); // \"usr_K7gkJ_q3vR2nL8xH5eM0w\"\n * userId(); // \"usr_Xp9mN2qL5vR8nK3eJ7cHw\"\n */\nexport function createPrefixedGenerator(\n prefix: string,\n length: number = DEFAULT_LENGTH,\n): () => string {\n // Validate eagerly so callers get errors at factory creation, not later.\n assertPrefix(prefix);\n assertLength(length);\n return () => {\n const id = generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);\n return `${prefix}${SEPARATOR}${id}`;\n };\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a prefixed, cryptographically secure ID.
|
|
3
|
+
*
|
|
4
|
+
* The format is `{prefix}_{id}`, e.g. `usr_K7gkJ_q3vR2nL8xH5eM0w`.
|
|
5
|
+
*
|
|
6
|
+
* @param prefix - Alphanumeric prefix starting with a letter, e.g. `"usr"`.
|
|
7
|
+
* @param length - Length of the random portion (1–255). Defaults to 21.
|
|
8
|
+
* @returns A string in the form `{prefix}_{id}`.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { generatePrefixedId } from "sigilid/prefix";
|
|
12
|
+
* const userId = generatePrefixedId("usr"); // "usr_K7gkJ_q3vR2nL8xH5eM0w"
|
|
13
|
+
*/
|
|
14
|
+
declare function generatePrefixedId(prefix: string, length?: number): string;
|
|
15
|
+
/**
|
|
16
|
+
* Returns a factory function that generates prefixed IDs with fixed settings.
|
|
17
|
+
*
|
|
18
|
+
* Useful when you generate IDs for a specific entity type throughout your
|
|
19
|
+
* codebase and want to avoid repeating the prefix at every call site.
|
|
20
|
+
*
|
|
21
|
+
* @param prefix - Alphanumeric prefix starting with a letter.
|
|
22
|
+
* @param length - Length of the random portion. Defaults to 21.
|
|
23
|
+
* @returns A zero-argument function that returns a new prefixed ID each call.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* import { createPrefixedGenerator } from "sigilid/prefix";
|
|
27
|
+
* const userId = createPrefixedGenerator("usr");
|
|
28
|
+
* userId(); // "usr_K7gkJ_q3vR2nL8xH5eM0w"
|
|
29
|
+
* userId(); // "usr_Xp9mN2qL5vR8nK3eJ7cHw"
|
|
30
|
+
*/
|
|
31
|
+
declare function createPrefixedGenerator(prefix: string, length?: number): () => string;
|
|
32
|
+
|
|
33
|
+
export { createPrefixedGenerator, generatePrefixedId };
|
package/dist/prefix.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a prefixed, cryptographically secure ID.
|
|
3
|
+
*
|
|
4
|
+
* The format is `{prefix}_{id}`, e.g. `usr_K7gkJ_q3vR2nL8xH5eM0w`.
|
|
5
|
+
*
|
|
6
|
+
* @param prefix - Alphanumeric prefix starting with a letter, e.g. `"usr"`.
|
|
7
|
+
* @param length - Length of the random portion (1–255). Defaults to 21.
|
|
8
|
+
* @returns A string in the form `{prefix}_{id}`.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { generatePrefixedId } from "sigilid/prefix";
|
|
12
|
+
* const userId = generatePrefixedId("usr"); // "usr_K7gkJ_q3vR2nL8xH5eM0w"
|
|
13
|
+
*/
|
|
14
|
+
declare function generatePrefixedId(prefix: string, length?: number): string;
|
|
15
|
+
/**
|
|
16
|
+
* Returns a factory function that generates prefixed IDs with fixed settings.
|
|
17
|
+
*
|
|
18
|
+
* Useful when you generate IDs for a specific entity type throughout your
|
|
19
|
+
* codebase and want to avoid repeating the prefix at every call site.
|
|
20
|
+
*
|
|
21
|
+
* @param prefix - Alphanumeric prefix starting with a letter.
|
|
22
|
+
* @param length - Length of the random portion. Defaults to 21.
|
|
23
|
+
* @returns A zero-argument function that returns a new prefixed ID each call.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* import { createPrefixedGenerator } from "sigilid/prefix";
|
|
27
|
+
* const userId = createPrefixedGenerator("usr");
|
|
28
|
+
* userId(); // "usr_K7gkJ_q3vR2nL8xH5eM0w"
|
|
29
|
+
* userId(); // "usr_Xp9mN2qL5vR8nK3eJ7cHw"
|
|
30
|
+
*/
|
|
31
|
+
declare function createPrefixedGenerator(prefix: string, length?: number): () => string;
|
|
32
|
+
|
|
33
|
+
export { createPrefixedGenerator, generatePrefixedId };
|
package/dist/prefix.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/internal/alphabet.ts
|
|
2
|
+
function generateFromAlphabet(alphabet, length, getBytes) {
|
|
3
|
+
const alphabetSize = alphabet.length;
|
|
4
|
+
let mask = 1;
|
|
5
|
+
while (mask < alphabetSize) mask = mask << 1 | 1;
|
|
6
|
+
const bufferMultiplier = Math.ceil(1.6 * mask * length / alphabetSize);
|
|
7
|
+
let result = "";
|
|
8
|
+
while (result.length < length) {
|
|
9
|
+
const bytes = getBytes(bufferMultiplier);
|
|
10
|
+
for (let i = 0; i < bytes.length && result.length < length; i++) {
|
|
11
|
+
const byte = bytes[i] & mask;
|
|
12
|
+
if (byte < alphabetSize) {
|
|
13
|
+
result += alphabet[byte];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/internal/assert.ts
|
|
21
|
+
var MIN_LENGTH = 1;
|
|
22
|
+
var MAX_LENGTH = 255;
|
|
23
|
+
function assertLength(length) {
|
|
24
|
+
if (!Number.isInteger(length) || length < MIN_LENGTH || length > MAX_LENGTH) {
|
|
25
|
+
throw new RangeError(
|
|
26
|
+
`ID length must be an integer between ${MIN_LENGTH} and ${MAX_LENGTH}, got ${length}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function assertPrefix(prefix) {
|
|
31
|
+
if (typeof prefix !== "string" || prefix.length === 0) {
|
|
32
|
+
throw new TypeError("Prefix must be a non-empty string");
|
|
33
|
+
}
|
|
34
|
+
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(prefix)) {
|
|
35
|
+
throw new TypeError("Prefix must start with a letter and contain only letters and digits");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/internal/random.ts
|
|
40
|
+
function randomBytes(count) {
|
|
41
|
+
const bytes = new Uint8Array(count);
|
|
42
|
+
crypto.getRandomValues(bytes);
|
|
43
|
+
return bytes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/index.ts
|
|
47
|
+
var DEFAULT_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
|
|
48
|
+
|
|
49
|
+
// src/prefix.ts
|
|
50
|
+
var DEFAULT_LENGTH = 21;
|
|
51
|
+
var SEPARATOR = "_";
|
|
52
|
+
function generatePrefixedId(prefix, length = DEFAULT_LENGTH) {
|
|
53
|
+
assertPrefix(prefix);
|
|
54
|
+
assertLength(length);
|
|
55
|
+
const id = generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);
|
|
56
|
+
return `${prefix}${SEPARATOR}${id}`;
|
|
57
|
+
}
|
|
58
|
+
function createPrefixedGenerator(prefix, length = DEFAULT_LENGTH) {
|
|
59
|
+
assertPrefix(prefix);
|
|
60
|
+
assertLength(length);
|
|
61
|
+
return () => {
|
|
62
|
+
const id = generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);
|
|
63
|
+
return `${prefix}${SEPARATOR}${id}`;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { createPrefixedGenerator, generatePrefixedId };
|
|
68
|
+
//# sourceMappingURL=prefix.js.map
|
|
69
|
+
//# sourceMappingURL=prefix.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/alphabet.ts","../src/internal/assert.ts","../src/internal/random.ts","../src/index.ts","../src/prefix.ts"],"names":[],"mappings":";AAiCO,SAAS,oBAAA,CACd,QAAA,EACA,MAAA,EACA,QAAA,EACQ;AACR,EAAA,MAAM,eAAe,QAAA,CAAS,MAAA;AAG9B,EAAA,IAAI,IAAA,GAAO,CAAA;AACX,EAAA,OAAO,IAAA,GAAO,YAAA,EAAc,IAAA,GAAQ,IAAA,IAAQ,CAAA,GAAK,CAAA;AAGjD,EAAA,MAAM,mBAAmB,IAAA,CAAK,IAAA,CAAM,GAAA,GAAM,IAAA,GAAO,SAAU,YAAY,CAAA;AACvE,EAAA,IAAI,MAAA,GAAS,EAAA;AAEb,EAAA,OAAO,MAAA,CAAO,SAAS,MAAA,EAAQ;AAC7B,IAAA,MAAM,KAAA,GAAQ,SAAS,gBAAgB,CAAA;AACvC,IAAA,KAAA,IAAS,CAAA,GAAI,GAAG,CAAA,GAAI,KAAA,CAAM,UAAU,MAAA,CAAO,MAAA,GAAS,QAAQ,CAAA,EAAA,EAAK;AAC/D,MAAA,MAAM,IAAA,GAAQ,KAAA,CAAM,CAAC,CAAA,GAAe,IAAA;AACpC,MAAA,IAAI,OAAO,YAAA,EAAc;AACvB,QAAA,MAAA,IAAU,SAAS,IAAI,CAAA;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;;;AC3DA,IAAM,UAAA,GAAa,CAAA;AACnB,IAAM,UAAA,GAAa,GAAA;AAEZ,SAAS,aAAa,MAAA,EAAsB;AACjD,EAAA,IAAI,CAAC,OAAO,SAAA,CAAU,MAAM,KAAK,MAAA,GAAS,UAAA,IAAc,SAAS,UAAA,EAAY;AAC3E,IAAA,MAAM,IAAI,UAAA;AAAA,MACR,CAAA,qCAAA,EAAwC,UAAU,CAAA,KAAA,EAAQ,UAAU,SAAS,MAAM,CAAA;AAAA,KACrF;AAAA,EACF;AACF;AAEO,SAAS,aAAa,MAAA,EAAsB;AACjD,EAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,CAAO,WAAW,CAAA,EAAG;AACrD,IAAA,MAAM,IAAI,UAAU,mCAAmC,CAAA;AAAA,EACzD;AACA,EAAA,IAAI,CAAC,wBAAA,CAAyB,IAAA,CAAK,MAAM,CAAA,EAAG;AAC1C,IAAA,MAAM,IAAI,UAAU,qEAAqE,CAAA;AAAA,EAC3F;AACF;;;AChBO,SAAS,YAAY,KAAA,EAA2B;AACrD,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,KAAK,CAAA;AAClC,EAAA,MAAA,CAAO,gBAAgB,KAAK,CAAA;AAC5B,EAAA,OAAO,KAAA;AACT;;;ACEO,IAAM,gBAAA,GAAmB,kEAAA;;;ACHhC,IAAM,cAAA,GAAiB,EAAA;AACvB,IAAM,SAAA,GAAY,GAAA;AAeX,SAAS,kBAAA,CAAmB,MAAA,EAAgB,MAAA,GAAiB,cAAA,EAAwB;AAC1F,EAAA,YAAA,CAAa,MAAM,CAAA;AACnB,EAAA,YAAA,CAAa,MAAM,CAAA;AACnB,EAAA,MAAM,EAAA,GAAK,oBAAA,CAAqB,gBAAA,EAAkB,MAAA,EAAQ,WAAW,CAAA;AACrE,EAAA,OAAO,CAAA,EAAG,MAAM,CAAA,EAAG,SAAS,GAAG,EAAE,CAAA,CAAA;AACnC;AAkBO,SAAS,uBAAA,CACd,MAAA,EACA,MAAA,GAAiB,cAAA,EACH;AAEd,EAAA,YAAA,CAAa,MAAM,CAAA;AACnB,EAAA,YAAA,CAAa,MAAM,CAAA;AACnB,EAAA,OAAO,MAAM;AACX,IAAA,MAAM,EAAA,GAAK,oBAAA,CAAqB,gBAAA,EAAkB,MAAA,EAAQ,WAAW,CAAA;AACrE,IAAA,OAAO,CAAA,EAAG,MAAM,CAAA,EAAG,SAAS,GAAG,EAAE,CAAA,CAAA;AAAA,EACnC,CAAA;AACF","file":"prefix.js","sourcesContent":["const MIN_ALPHABET_SIZE = 2;\nconst MAX_ALPHABET_SIZE = 256;\n\nexport function validateAlphabetString(alphabet: string): void {\n if (typeof alphabet !== \"string\") {\n throw new TypeError(\"Alphabet must be a string\");\n }\n if (alphabet.length < MIN_ALPHABET_SIZE) {\n throw new RangeError(\n `Alphabet must have at least ${MIN_ALPHABET_SIZE} characters, got ${alphabet.length}`,\n );\n }\n if (alphabet.length > MAX_ALPHABET_SIZE) {\n throw new RangeError(\n `Alphabet must have at most ${MAX_ALPHABET_SIZE} characters, got ${alphabet.length}`,\n );\n }\n const seen = new Set<string>();\n for (const char of alphabet) {\n if (seen.has(char)) {\n throw new TypeError(`Alphabet contains duplicate character: \"${char}\"`);\n }\n seen.add(char);\n }\n}\n\n/**\n * Generates an ID of `length` characters drawn from `alphabet` using\n * rejection sampling to avoid modulo bias.\n *\n * The caller is responsible for providing valid `alphabet` and `length` values.\n * Use `validateAlphabetString` and `assertLength` before calling this.\n */\nexport function generateFromAlphabet(\n alphabet: string,\n length: number,\n getBytes: (count: number) => Uint8Array,\n): string {\n const alphabetSize = alphabet.length;\n\n // Find the smallest bitmask >= alphabetSize to reduce rejection rate.\n let mask = 1;\n while (mask < alphabetSize) mask = (mask << 1) | 1;\n\n // Overshoot buffer size to reduce round-trips. Most cases complete in one pass.\n const bufferMultiplier = Math.ceil((1.6 * mask * length) / alphabetSize);\n let result = \"\";\n\n while (result.length < length) {\n const bytes = getBytes(bufferMultiplier);\n for (let i = 0; i < bytes.length && result.length < length; i++) {\n const byte = (bytes[i] as number) & mask;\n if (byte < alphabetSize) {\n result += alphabet[byte];\n }\n }\n }\n\n return result;\n}\n","const MIN_LENGTH = 1;\nconst MAX_LENGTH = 255;\n\nexport function assertLength(length: number): void {\n if (!Number.isInteger(length) || length < MIN_LENGTH || length > MAX_LENGTH) {\n throw new RangeError(\n `ID length must be an integer between ${MIN_LENGTH} and ${MAX_LENGTH}, got ${length}`,\n );\n }\n}\n\nexport function assertPrefix(prefix: string): void {\n if (typeof prefix !== \"string\" || prefix.length === 0) {\n throw new TypeError(\"Prefix must be a non-empty string\");\n }\n if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(prefix)) {\n throw new TypeError(\"Prefix must start with a letter and contain only letters and digits\");\n }\n}\n","// Returns a Uint8Array of `count` cryptographically secure random bytes.\n// Works in Node 18+, all modern browsers, and edge runtimes that expose `crypto`.\nexport function randomBytes(count: number): Uint8Array {\n const bytes = new Uint8Array(count);\n crypto.getRandomValues(bytes);\n return bytes;\n}\n","import { generateFromAlphabet } from \"./internal/alphabet.js\";\nimport { assertLength } from \"./internal/assert.js\";\nimport { randomBytes } from \"./internal/random.js\";\n\n/**\n * URL-safe alphabet used by default. 64 characters: A-Z, a-z, 0-9, _, -.\n * Chosen to be safe in URLs, filenames, and most log formats without encoding.\n */\nexport const DEFAULT_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-\";\n\nconst DEFAULT_LENGTH = 21;\n\n/**\n * Generates a cryptographically secure random ID.\n *\n * @param length - Number of characters (1–255). Defaults to 21.\n * @returns A random URL-safe string of the requested length.\n *\n * @example\n * import { generateId } from \"sigilid\";\n * const id = generateId(); // \"K7gkJ_q3vR2nL8xH5eM0w\"\n */\nexport function generateId(length: number = DEFAULT_LENGTH): string {\n assertLength(length);\n return generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);\n}\n","import { DEFAULT_ALPHABET } from \"./index.js\";\nimport { generateFromAlphabet } from \"./internal/alphabet.js\";\nimport { assertLength, assertPrefix } from \"./internal/assert.js\";\nimport { randomBytes } from \"./internal/random.js\";\n\nconst DEFAULT_LENGTH = 21;\nconst SEPARATOR = \"_\";\n\n/**\n * Generates a prefixed, cryptographically secure ID.\n *\n * The format is `{prefix}_{id}`, e.g. `usr_K7gkJ_q3vR2nL8xH5eM0w`.\n *\n * @param prefix - Alphanumeric prefix starting with a letter, e.g. `\"usr\"`.\n * @param length - Length of the random portion (1–255). Defaults to 21.\n * @returns A string in the form `{prefix}_{id}`.\n *\n * @example\n * import { generatePrefixedId } from \"sigilid/prefix\";\n * const userId = generatePrefixedId(\"usr\"); // \"usr_K7gkJ_q3vR2nL8xH5eM0w\"\n */\nexport function generatePrefixedId(prefix: string, length: number = DEFAULT_LENGTH): string {\n assertPrefix(prefix);\n assertLength(length);\n const id = generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);\n return `${prefix}${SEPARATOR}${id}`;\n}\n\n/**\n * Returns a factory function that generates prefixed IDs with fixed settings.\n *\n * Useful when you generate IDs for a specific entity type throughout your\n * codebase and want to avoid repeating the prefix at every call site.\n *\n * @param prefix - Alphanumeric prefix starting with a letter.\n * @param length - Length of the random portion. Defaults to 21.\n * @returns A zero-argument function that returns a new prefixed ID each call.\n *\n * @example\n * import { createPrefixedGenerator } from \"sigilid/prefix\";\n * const userId = createPrefixedGenerator(\"usr\");\n * userId(); // \"usr_K7gkJ_q3vR2nL8xH5eM0w\"\n * userId(); // \"usr_Xp9mN2qL5vR8nK3eJ7cHw\"\n */\nexport function createPrefixedGenerator(\n prefix: string,\n length: number = DEFAULT_LENGTH,\n): () => string {\n // Validate eagerly so callers get errors at factory creation, not later.\n assertPrefix(prefix);\n assertLength(length);\n return () => {\n const id = generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);\n return `${prefix}${SEPARATOR}${id}`;\n };\n}\n"]}
|
package/dist/typed.cjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/internal/alphabet.ts
|
|
4
|
+
function generateFromAlphabet(alphabet, length, getBytes) {
|
|
5
|
+
const alphabetSize = alphabet.length;
|
|
6
|
+
let mask = 1;
|
|
7
|
+
while (mask < alphabetSize) mask = mask << 1 | 1;
|
|
8
|
+
const bufferMultiplier = Math.ceil(1.6 * mask * length / alphabetSize);
|
|
9
|
+
let result = "";
|
|
10
|
+
while (result.length < length) {
|
|
11
|
+
const bytes = getBytes(bufferMultiplier);
|
|
12
|
+
for (let i = 0; i < bytes.length && result.length < length; i++) {
|
|
13
|
+
const byte = bytes[i] & mask;
|
|
14
|
+
if (byte < alphabetSize) {
|
|
15
|
+
result += alphabet[byte];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/internal/assert.ts
|
|
23
|
+
var MIN_LENGTH = 1;
|
|
24
|
+
var MAX_LENGTH = 255;
|
|
25
|
+
function assertLength(length) {
|
|
26
|
+
if (!Number.isInteger(length) || length < MIN_LENGTH || length > MAX_LENGTH) {
|
|
27
|
+
throw new RangeError(
|
|
28
|
+
`ID length must be an integer between ${MIN_LENGTH} and ${MAX_LENGTH}, got ${length}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function assertPrefix(prefix) {
|
|
33
|
+
if (typeof prefix !== "string" || prefix.length === 0) {
|
|
34
|
+
throw new TypeError("Prefix must be a non-empty string");
|
|
35
|
+
}
|
|
36
|
+
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(prefix)) {
|
|
37
|
+
throw new TypeError("Prefix must start with a letter and contain only letters and digits");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/internal/random.ts
|
|
42
|
+
function randomBytes(count) {
|
|
43
|
+
const bytes = new Uint8Array(count);
|
|
44
|
+
crypto.getRandomValues(bytes);
|
|
45
|
+
return bytes;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/index.ts
|
|
49
|
+
var DEFAULT_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
|
|
50
|
+
|
|
51
|
+
// src/typed.ts
|
|
52
|
+
var DEFAULT_LENGTH = 21;
|
|
53
|
+
var SEPARATOR = "_";
|
|
54
|
+
function castId(value) {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function createTypedGenerator(prefix, length = DEFAULT_LENGTH) {
|
|
58
|
+
if (prefix !== void 0) assertPrefix(prefix);
|
|
59
|
+
assertLength(length);
|
|
60
|
+
return () => {
|
|
61
|
+
const raw = generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);
|
|
62
|
+
const value = prefix !== void 0 ? `${prefix}${SEPARATOR}${raw}` : raw;
|
|
63
|
+
return value;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
exports.castId = castId;
|
|
68
|
+
exports.createTypedGenerator = createTypedGenerator;
|
|
69
|
+
//# sourceMappingURL=typed.cjs.map
|
|
70
|
+
//# sourceMappingURL=typed.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/alphabet.ts","../src/internal/assert.ts","../src/internal/random.ts","../src/index.ts","../src/typed.ts"],"names":[],"mappings":";;;AAiCO,SAAS,oBAAA,CACd,QAAA,EACA,MAAA,EACA,QAAA,EACQ;AACR,EAAA,MAAM,eAAe,QAAA,CAAS,MAAA;AAG9B,EAAA,IAAI,IAAA,GAAO,CAAA;AACX,EAAA,OAAO,IAAA,GAAO,YAAA,EAAc,IAAA,GAAQ,IAAA,IAAQ,CAAA,GAAK,CAAA;AAGjD,EAAA,MAAM,mBAAmB,IAAA,CAAK,IAAA,CAAM,GAAA,GAAM,IAAA,GAAO,SAAU,YAAY,CAAA;AACvE,EAAA,IAAI,MAAA,GAAS,EAAA;AAEb,EAAA,OAAO,MAAA,CAAO,SAAS,MAAA,EAAQ;AAC7B,IAAA,MAAM,KAAA,GAAQ,SAAS,gBAAgB,CAAA;AACvC,IAAA,KAAA,IAAS,CAAA,GAAI,GAAG,CAAA,GAAI,KAAA,CAAM,UAAU,MAAA,CAAO,MAAA,GAAS,QAAQ,CAAA,EAAA,EAAK;AAC/D,MAAA,MAAM,IAAA,GAAQ,KAAA,CAAM,CAAC,CAAA,GAAe,IAAA;AACpC,MAAA,IAAI,OAAO,YAAA,EAAc;AACvB,QAAA,MAAA,IAAU,SAAS,IAAI,CAAA;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;;;AC3DA,IAAM,UAAA,GAAa,CAAA;AACnB,IAAM,UAAA,GAAa,GAAA;AAEZ,SAAS,aAAa,MAAA,EAAsB;AACjD,EAAA,IAAI,CAAC,OAAO,SAAA,CAAU,MAAM,KAAK,MAAA,GAAS,UAAA,IAAc,SAAS,UAAA,EAAY;AAC3E,IAAA,MAAM,IAAI,UAAA;AAAA,MACR,CAAA,qCAAA,EAAwC,UAAU,CAAA,KAAA,EAAQ,UAAU,SAAS,MAAM,CAAA;AAAA,KACrF;AAAA,EACF;AACF;AAEO,SAAS,aAAa,MAAA,EAAsB;AACjD,EAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,CAAO,WAAW,CAAA,EAAG;AACrD,IAAA,MAAM,IAAI,UAAU,mCAAmC,CAAA;AAAA,EACzD;AACA,EAAA,IAAI,CAAC,wBAAA,CAAyB,IAAA,CAAK,MAAM,CAAA,EAAG;AAC1C,IAAA,MAAM,IAAI,UAAU,qEAAqE,CAAA;AAAA,EAC3F;AACF;;;AChBO,SAAS,YAAY,KAAA,EAA2B;AACrD,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,KAAK,CAAA;AAClC,EAAA,MAAA,CAAO,gBAAgB,KAAK,CAAA;AAC5B,EAAA,OAAO,KAAA;AACT;;;ACEO,IAAM,gBAAA,GAAmB,kEAAA;;;ACHhC,IAAM,cAAA,GAAiB,EAAA;AACvB,IAAM,SAAA,GAAY,GAAA;AA+BX,SAAS,OAAyB,KAAA,EAAwB;AAC/D,EAAA,OAAO,KAAA;AACT;AAmBO,SAAS,oBAAA,CACd,MAAA,EACA,MAAA,GAAiB,cAAA,EACF;AACf,EAAA,IAAI,MAAA,KAAW,MAAA,EAAW,YAAA,CAAa,MAAM,CAAA;AAC7C,EAAA,YAAA,CAAa,MAAM,CAAA;AAEnB,EAAA,OAAO,MAAe;AACpB,IAAA,MAAM,GAAA,GAAM,oBAAA,CAAqB,gBAAA,EAAkB,MAAA,EAAQ,WAAW,CAAA;AACtE,IAAA,MAAM,KAAA,GAAQ,WAAW,MAAA,GAAY,CAAA,EAAG,MAAM,CAAA,EAAG,SAAS,CAAA,EAAG,GAAG,CAAA,CAAA,GAAK,GAAA;AACrE,IAAA,OAAO,KAAA;AAAA,EACT,CAAA;AACF","file":"typed.cjs","sourcesContent":["const MIN_ALPHABET_SIZE = 2;\nconst MAX_ALPHABET_SIZE = 256;\n\nexport function validateAlphabetString(alphabet: string): void {\n if (typeof alphabet !== \"string\") {\n throw new TypeError(\"Alphabet must be a string\");\n }\n if (alphabet.length < MIN_ALPHABET_SIZE) {\n throw new RangeError(\n `Alphabet must have at least ${MIN_ALPHABET_SIZE} characters, got ${alphabet.length}`,\n );\n }\n if (alphabet.length > MAX_ALPHABET_SIZE) {\n throw new RangeError(\n `Alphabet must have at most ${MAX_ALPHABET_SIZE} characters, got ${alphabet.length}`,\n );\n }\n const seen = new Set<string>();\n for (const char of alphabet) {\n if (seen.has(char)) {\n throw new TypeError(`Alphabet contains duplicate character: \"${char}\"`);\n }\n seen.add(char);\n }\n}\n\n/**\n * Generates an ID of `length` characters drawn from `alphabet` using\n * rejection sampling to avoid modulo bias.\n *\n * The caller is responsible for providing valid `alphabet` and `length` values.\n * Use `validateAlphabetString` and `assertLength` before calling this.\n */\nexport function generateFromAlphabet(\n alphabet: string,\n length: number,\n getBytes: (count: number) => Uint8Array,\n): string {\n const alphabetSize = alphabet.length;\n\n // Find the smallest bitmask >= alphabetSize to reduce rejection rate.\n let mask = 1;\n while (mask < alphabetSize) mask = (mask << 1) | 1;\n\n // Overshoot buffer size to reduce round-trips. Most cases complete in one pass.\n const bufferMultiplier = Math.ceil((1.6 * mask * length) / alphabetSize);\n let result = \"\";\n\n while (result.length < length) {\n const bytes = getBytes(bufferMultiplier);\n for (let i = 0; i < bytes.length && result.length < length; i++) {\n const byte = (bytes[i] as number) & mask;\n if (byte < alphabetSize) {\n result += alphabet[byte];\n }\n }\n }\n\n return result;\n}\n","const MIN_LENGTH = 1;\nconst MAX_LENGTH = 255;\n\nexport function assertLength(length: number): void {\n if (!Number.isInteger(length) || length < MIN_LENGTH || length > MAX_LENGTH) {\n throw new RangeError(\n `ID length must be an integer between ${MIN_LENGTH} and ${MAX_LENGTH}, got ${length}`,\n );\n }\n}\n\nexport function assertPrefix(prefix: string): void {\n if (typeof prefix !== \"string\" || prefix.length === 0) {\n throw new TypeError(\"Prefix must be a non-empty string\");\n }\n if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(prefix)) {\n throw new TypeError(\"Prefix must start with a letter and contain only letters and digits\");\n }\n}\n","// Returns a Uint8Array of `count` cryptographically secure random bytes.\n// Works in Node 18+, all modern browsers, and edge runtimes that expose `crypto`.\nexport function randomBytes(count: number): Uint8Array {\n const bytes = new Uint8Array(count);\n crypto.getRandomValues(bytes);\n return bytes;\n}\n","import { generateFromAlphabet } from \"./internal/alphabet.js\";\nimport { assertLength } from \"./internal/assert.js\";\nimport { randomBytes } from \"./internal/random.js\";\n\n/**\n * URL-safe alphabet used by default. 64 characters: A-Z, a-z, 0-9, _, -.\n * Chosen to be safe in URLs, filenames, and most log formats without encoding.\n */\nexport const DEFAULT_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-\";\n\nconst DEFAULT_LENGTH = 21;\n\n/**\n * Generates a cryptographically secure random ID.\n *\n * @param length - Number of characters (1–255). Defaults to 21.\n * @returns A random URL-safe string of the requested length.\n *\n * @example\n * import { generateId } from \"sigilid\";\n * const id = generateId(); // \"K7gkJ_q3vR2nL8xH5eM0w\"\n */\nexport function generateId(length: number = DEFAULT_LENGTH): string {\n assertLength(length);\n return generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);\n}\n","import { DEFAULT_ALPHABET } from \"./index.js\";\nimport { generateFromAlphabet } from \"./internal/alphabet.js\";\nimport { assertLength, assertPrefix } from \"./internal/assert.js\";\nimport { randomBytes } from \"./internal/random.js\";\n\nconst DEFAULT_LENGTH = 21;\nconst SEPARATOR = \"_\";\n\n/**\n * A nominal type brand. Attach this to a string type to create a distinct\n * branded string that is not assignable from plain `string`.\n *\n * @example\n * type UserId = Brand<string, \"UserId\">;\n */\nexport type Brand<T, B> = T & { readonly __brand: B };\n\n/**\n * A branded string type for an entity named `T`.\n * Equivalent to `Brand<string, T>`, but more ergonomic for ID types.\n *\n * @example\n * type UserId = IdOf<\"User\">;\n * type PostId = IdOf<\"Post\">;\n */\nexport type IdOf<T extends string> = Brand<string, T>;\n\n/**\n * Casts a plain string to a branded `IdOf<T>`.\n *\n * This is a type-only operation at runtime — no validation is performed.\n * Use `assertValidId` from `\"sigilid/validate\"` first if you need runtime safety.\n *\n * @example\n * import { castId } from \"sigilid/typed\";\n * const id = castId<\"User\">(\"usr_abc123\");\n */\nexport function castId<T extends string>(value: string): IdOf<T> {\n return value as IdOf<T>;\n}\n\n/**\n * Returns a factory function that generates cryptographically secure branded IDs.\n *\n * The type parameter `T` sets the brand. If a prefix is provided, the format\n * is `{prefix}_{id}`, otherwise it is a plain random string.\n *\n * Validation happens at factory creation, not per-call.\n *\n * @param prefix - Optional alphanumeric prefix.\n * @param length - Length of the random portion (1–255). Defaults to 21.\n * @returns A zero-argument function returning `IdOf<T>`.\n *\n * @example\n * import { createTypedGenerator } from \"sigilid/typed\";\n * const userId = createTypedGenerator<\"User\">(\"usr\");\n * const id = userId(); // IdOf<\"User\"> = \"usr_K7gkJ_q3vR2nL8xH5eM0w\"\n */\nexport function createTypedGenerator<T extends string>(\n prefix?: string,\n length: number = DEFAULT_LENGTH,\n): () => IdOf<T> {\n if (prefix !== undefined) assertPrefix(prefix);\n assertLength(length);\n\n return (): IdOf<T> => {\n const raw = generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);\n const value = prefix !== undefined ? `${prefix}${SEPARATOR}${raw}` : raw;\n return value as IdOf<T>;\n };\n}\n"]}
|
package/dist/typed.d.cts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A nominal type brand. Attach this to a string type to create a distinct
|
|
3
|
+
* branded string that is not assignable from plain `string`.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* type UserId = Brand<string, "UserId">;
|
|
7
|
+
*/
|
|
8
|
+
type Brand<T, B> = T & {
|
|
9
|
+
readonly __brand: B;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* A branded string type for an entity named `T`.
|
|
13
|
+
* Equivalent to `Brand<string, T>`, but more ergonomic for ID types.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* type UserId = IdOf<"User">;
|
|
17
|
+
* type PostId = IdOf<"Post">;
|
|
18
|
+
*/
|
|
19
|
+
type IdOf<T extends string> = Brand<string, T>;
|
|
20
|
+
/**
|
|
21
|
+
* Casts a plain string to a branded `IdOf<T>`.
|
|
22
|
+
*
|
|
23
|
+
* This is a type-only operation at runtime — no validation is performed.
|
|
24
|
+
* Use `assertValidId` from `"sigilid/validate"` first if you need runtime safety.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* import { castId } from "sigilid/typed";
|
|
28
|
+
* const id = castId<"User">("usr_abc123");
|
|
29
|
+
*/
|
|
30
|
+
declare function castId<T extends string>(value: string): IdOf<T>;
|
|
31
|
+
/**
|
|
32
|
+
* Returns a factory function that generates cryptographically secure branded IDs.
|
|
33
|
+
*
|
|
34
|
+
* The type parameter `T` sets the brand. If a prefix is provided, the format
|
|
35
|
+
* is `{prefix}_{id}`, otherwise it is a plain random string.
|
|
36
|
+
*
|
|
37
|
+
* Validation happens at factory creation, not per-call.
|
|
38
|
+
*
|
|
39
|
+
* @param prefix - Optional alphanumeric prefix.
|
|
40
|
+
* @param length - Length of the random portion (1–255). Defaults to 21.
|
|
41
|
+
* @returns A zero-argument function returning `IdOf<T>`.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* import { createTypedGenerator } from "sigilid/typed";
|
|
45
|
+
* const userId = createTypedGenerator<"User">("usr");
|
|
46
|
+
* const id = userId(); // IdOf<"User"> = "usr_K7gkJ_q3vR2nL8xH5eM0w"
|
|
47
|
+
*/
|
|
48
|
+
declare function createTypedGenerator<T extends string>(prefix?: string, length?: number): () => IdOf<T>;
|
|
49
|
+
|
|
50
|
+
export { type Brand, type IdOf, castId, createTypedGenerator };
|
package/dist/typed.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A nominal type brand. Attach this to a string type to create a distinct
|
|
3
|
+
* branded string that is not assignable from plain `string`.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* type UserId = Brand<string, "UserId">;
|
|
7
|
+
*/
|
|
8
|
+
type Brand<T, B> = T & {
|
|
9
|
+
readonly __brand: B;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* A branded string type for an entity named `T`.
|
|
13
|
+
* Equivalent to `Brand<string, T>`, but more ergonomic for ID types.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* type UserId = IdOf<"User">;
|
|
17
|
+
* type PostId = IdOf<"Post">;
|
|
18
|
+
*/
|
|
19
|
+
type IdOf<T extends string> = Brand<string, T>;
|
|
20
|
+
/**
|
|
21
|
+
* Casts a plain string to a branded `IdOf<T>`.
|
|
22
|
+
*
|
|
23
|
+
* This is a type-only operation at runtime — no validation is performed.
|
|
24
|
+
* Use `assertValidId` from `"sigilid/validate"` first if you need runtime safety.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* import { castId } from "sigilid/typed";
|
|
28
|
+
* const id = castId<"User">("usr_abc123");
|
|
29
|
+
*/
|
|
30
|
+
declare function castId<T extends string>(value: string): IdOf<T>;
|
|
31
|
+
/**
|
|
32
|
+
* Returns a factory function that generates cryptographically secure branded IDs.
|
|
33
|
+
*
|
|
34
|
+
* The type parameter `T` sets the brand. If a prefix is provided, the format
|
|
35
|
+
* is `{prefix}_{id}`, otherwise it is a plain random string.
|
|
36
|
+
*
|
|
37
|
+
* Validation happens at factory creation, not per-call.
|
|
38
|
+
*
|
|
39
|
+
* @param prefix - Optional alphanumeric prefix.
|
|
40
|
+
* @param length - Length of the random portion (1–255). Defaults to 21.
|
|
41
|
+
* @returns A zero-argument function returning `IdOf<T>`.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* import { createTypedGenerator } from "sigilid/typed";
|
|
45
|
+
* const userId = createTypedGenerator<"User">("usr");
|
|
46
|
+
* const id = userId(); // IdOf<"User"> = "usr_K7gkJ_q3vR2nL8xH5eM0w"
|
|
47
|
+
*/
|
|
48
|
+
declare function createTypedGenerator<T extends string>(prefix?: string, length?: number): () => IdOf<T>;
|
|
49
|
+
|
|
50
|
+
export { type Brand, type IdOf, castId, createTypedGenerator };
|
package/dist/typed.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/internal/alphabet.ts
|
|
2
|
+
function generateFromAlphabet(alphabet, length, getBytes) {
|
|
3
|
+
const alphabetSize = alphabet.length;
|
|
4
|
+
let mask = 1;
|
|
5
|
+
while (mask < alphabetSize) mask = mask << 1 | 1;
|
|
6
|
+
const bufferMultiplier = Math.ceil(1.6 * mask * length / alphabetSize);
|
|
7
|
+
let result = "";
|
|
8
|
+
while (result.length < length) {
|
|
9
|
+
const bytes = getBytes(bufferMultiplier);
|
|
10
|
+
for (let i = 0; i < bytes.length && result.length < length; i++) {
|
|
11
|
+
const byte = bytes[i] & mask;
|
|
12
|
+
if (byte < alphabetSize) {
|
|
13
|
+
result += alphabet[byte];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/internal/assert.ts
|
|
21
|
+
var MIN_LENGTH = 1;
|
|
22
|
+
var MAX_LENGTH = 255;
|
|
23
|
+
function assertLength(length) {
|
|
24
|
+
if (!Number.isInteger(length) || length < MIN_LENGTH || length > MAX_LENGTH) {
|
|
25
|
+
throw new RangeError(
|
|
26
|
+
`ID length must be an integer between ${MIN_LENGTH} and ${MAX_LENGTH}, got ${length}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function assertPrefix(prefix) {
|
|
31
|
+
if (typeof prefix !== "string" || prefix.length === 0) {
|
|
32
|
+
throw new TypeError("Prefix must be a non-empty string");
|
|
33
|
+
}
|
|
34
|
+
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(prefix)) {
|
|
35
|
+
throw new TypeError("Prefix must start with a letter and contain only letters and digits");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/internal/random.ts
|
|
40
|
+
function randomBytes(count) {
|
|
41
|
+
const bytes = new Uint8Array(count);
|
|
42
|
+
crypto.getRandomValues(bytes);
|
|
43
|
+
return bytes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/index.ts
|
|
47
|
+
var DEFAULT_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
|
|
48
|
+
|
|
49
|
+
// src/typed.ts
|
|
50
|
+
var DEFAULT_LENGTH = 21;
|
|
51
|
+
var SEPARATOR = "_";
|
|
52
|
+
function castId(value) {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function createTypedGenerator(prefix, length = DEFAULT_LENGTH) {
|
|
56
|
+
if (prefix !== void 0) assertPrefix(prefix);
|
|
57
|
+
assertLength(length);
|
|
58
|
+
return () => {
|
|
59
|
+
const raw = generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);
|
|
60
|
+
const value = prefix !== void 0 ? `${prefix}${SEPARATOR}${raw}` : raw;
|
|
61
|
+
return value;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { castId, createTypedGenerator };
|
|
66
|
+
//# sourceMappingURL=typed.js.map
|
|
67
|
+
//# sourceMappingURL=typed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/alphabet.ts","../src/internal/assert.ts","../src/internal/random.ts","../src/index.ts","../src/typed.ts"],"names":[],"mappings":";AAiCO,SAAS,oBAAA,CACd,QAAA,EACA,MAAA,EACA,QAAA,EACQ;AACR,EAAA,MAAM,eAAe,QAAA,CAAS,MAAA;AAG9B,EAAA,IAAI,IAAA,GAAO,CAAA;AACX,EAAA,OAAO,IAAA,GAAO,YAAA,EAAc,IAAA,GAAQ,IAAA,IAAQ,CAAA,GAAK,CAAA;AAGjD,EAAA,MAAM,mBAAmB,IAAA,CAAK,IAAA,CAAM,GAAA,GAAM,IAAA,GAAO,SAAU,YAAY,CAAA;AACvE,EAAA,IAAI,MAAA,GAAS,EAAA;AAEb,EAAA,OAAO,MAAA,CAAO,SAAS,MAAA,EAAQ;AAC7B,IAAA,MAAM,KAAA,GAAQ,SAAS,gBAAgB,CAAA;AACvC,IAAA,KAAA,IAAS,CAAA,GAAI,GAAG,CAAA,GAAI,KAAA,CAAM,UAAU,MAAA,CAAO,MAAA,GAAS,QAAQ,CAAA,EAAA,EAAK;AAC/D,MAAA,MAAM,IAAA,GAAQ,KAAA,CAAM,CAAC,CAAA,GAAe,IAAA;AACpC,MAAA,IAAI,OAAO,YAAA,EAAc;AACvB,QAAA,MAAA,IAAU,SAAS,IAAI,CAAA;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;;;AC3DA,IAAM,UAAA,GAAa,CAAA;AACnB,IAAM,UAAA,GAAa,GAAA;AAEZ,SAAS,aAAa,MAAA,EAAsB;AACjD,EAAA,IAAI,CAAC,OAAO,SAAA,CAAU,MAAM,KAAK,MAAA,GAAS,UAAA,IAAc,SAAS,UAAA,EAAY;AAC3E,IAAA,MAAM,IAAI,UAAA;AAAA,MACR,CAAA,qCAAA,EAAwC,UAAU,CAAA,KAAA,EAAQ,UAAU,SAAS,MAAM,CAAA;AAAA,KACrF;AAAA,EACF;AACF;AAEO,SAAS,aAAa,MAAA,EAAsB;AACjD,EAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,CAAO,WAAW,CAAA,EAAG;AACrD,IAAA,MAAM,IAAI,UAAU,mCAAmC,CAAA;AAAA,EACzD;AACA,EAAA,IAAI,CAAC,wBAAA,CAAyB,IAAA,CAAK,MAAM,CAAA,EAAG;AAC1C,IAAA,MAAM,IAAI,UAAU,qEAAqE,CAAA;AAAA,EAC3F;AACF;;;AChBO,SAAS,YAAY,KAAA,EAA2B;AACrD,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,KAAK,CAAA;AAClC,EAAA,MAAA,CAAO,gBAAgB,KAAK,CAAA;AAC5B,EAAA,OAAO,KAAA;AACT;;;ACEO,IAAM,gBAAA,GAAmB,kEAAA;;;ACHhC,IAAM,cAAA,GAAiB,EAAA;AACvB,IAAM,SAAA,GAAY,GAAA;AA+BX,SAAS,OAAyB,KAAA,EAAwB;AAC/D,EAAA,OAAO,KAAA;AACT;AAmBO,SAAS,oBAAA,CACd,MAAA,EACA,MAAA,GAAiB,cAAA,EACF;AACf,EAAA,IAAI,MAAA,KAAW,MAAA,EAAW,YAAA,CAAa,MAAM,CAAA;AAC7C,EAAA,YAAA,CAAa,MAAM,CAAA;AAEnB,EAAA,OAAO,MAAe;AACpB,IAAA,MAAM,GAAA,GAAM,oBAAA,CAAqB,gBAAA,EAAkB,MAAA,EAAQ,WAAW,CAAA;AACtE,IAAA,MAAM,KAAA,GAAQ,WAAW,MAAA,GAAY,CAAA,EAAG,MAAM,CAAA,EAAG,SAAS,CAAA,EAAG,GAAG,CAAA,CAAA,GAAK,GAAA;AACrE,IAAA,OAAO,KAAA;AAAA,EACT,CAAA;AACF","file":"typed.js","sourcesContent":["const MIN_ALPHABET_SIZE = 2;\nconst MAX_ALPHABET_SIZE = 256;\n\nexport function validateAlphabetString(alphabet: string): void {\n if (typeof alphabet !== \"string\") {\n throw new TypeError(\"Alphabet must be a string\");\n }\n if (alphabet.length < MIN_ALPHABET_SIZE) {\n throw new RangeError(\n `Alphabet must have at least ${MIN_ALPHABET_SIZE} characters, got ${alphabet.length}`,\n );\n }\n if (alphabet.length > MAX_ALPHABET_SIZE) {\n throw new RangeError(\n `Alphabet must have at most ${MAX_ALPHABET_SIZE} characters, got ${alphabet.length}`,\n );\n }\n const seen = new Set<string>();\n for (const char of alphabet) {\n if (seen.has(char)) {\n throw new TypeError(`Alphabet contains duplicate character: \"${char}\"`);\n }\n seen.add(char);\n }\n}\n\n/**\n * Generates an ID of `length` characters drawn from `alphabet` using\n * rejection sampling to avoid modulo bias.\n *\n * The caller is responsible for providing valid `alphabet` and `length` values.\n * Use `validateAlphabetString` and `assertLength` before calling this.\n */\nexport function generateFromAlphabet(\n alphabet: string,\n length: number,\n getBytes: (count: number) => Uint8Array,\n): string {\n const alphabetSize = alphabet.length;\n\n // Find the smallest bitmask >= alphabetSize to reduce rejection rate.\n let mask = 1;\n while (mask < alphabetSize) mask = (mask << 1) | 1;\n\n // Overshoot buffer size to reduce round-trips. Most cases complete in one pass.\n const bufferMultiplier = Math.ceil((1.6 * mask * length) / alphabetSize);\n let result = \"\";\n\n while (result.length < length) {\n const bytes = getBytes(bufferMultiplier);\n for (let i = 0; i < bytes.length && result.length < length; i++) {\n const byte = (bytes[i] as number) & mask;\n if (byte < alphabetSize) {\n result += alphabet[byte];\n }\n }\n }\n\n return result;\n}\n","const MIN_LENGTH = 1;\nconst MAX_LENGTH = 255;\n\nexport function assertLength(length: number): void {\n if (!Number.isInteger(length) || length < MIN_LENGTH || length > MAX_LENGTH) {\n throw new RangeError(\n `ID length must be an integer between ${MIN_LENGTH} and ${MAX_LENGTH}, got ${length}`,\n );\n }\n}\n\nexport function assertPrefix(prefix: string): void {\n if (typeof prefix !== \"string\" || prefix.length === 0) {\n throw new TypeError(\"Prefix must be a non-empty string\");\n }\n if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(prefix)) {\n throw new TypeError(\"Prefix must start with a letter and contain only letters and digits\");\n }\n}\n","// Returns a Uint8Array of `count` cryptographically secure random bytes.\n// Works in Node 18+, all modern browsers, and edge runtimes that expose `crypto`.\nexport function randomBytes(count: number): Uint8Array {\n const bytes = new Uint8Array(count);\n crypto.getRandomValues(bytes);\n return bytes;\n}\n","import { generateFromAlphabet } from \"./internal/alphabet.js\";\nimport { assertLength } from \"./internal/assert.js\";\nimport { randomBytes } from \"./internal/random.js\";\n\n/**\n * URL-safe alphabet used by default. 64 characters: A-Z, a-z, 0-9, _, -.\n * Chosen to be safe in URLs, filenames, and most log formats without encoding.\n */\nexport const DEFAULT_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-\";\n\nconst DEFAULT_LENGTH = 21;\n\n/**\n * Generates a cryptographically secure random ID.\n *\n * @param length - Number of characters (1–255). Defaults to 21.\n * @returns A random URL-safe string of the requested length.\n *\n * @example\n * import { generateId } from \"sigilid\";\n * const id = generateId(); // \"K7gkJ_q3vR2nL8xH5eM0w\"\n */\nexport function generateId(length: number = DEFAULT_LENGTH): string {\n assertLength(length);\n return generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);\n}\n","import { DEFAULT_ALPHABET } from \"./index.js\";\nimport { generateFromAlphabet } from \"./internal/alphabet.js\";\nimport { assertLength, assertPrefix } from \"./internal/assert.js\";\nimport { randomBytes } from \"./internal/random.js\";\n\nconst DEFAULT_LENGTH = 21;\nconst SEPARATOR = \"_\";\n\n/**\n * A nominal type brand. Attach this to a string type to create a distinct\n * branded string that is not assignable from plain `string`.\n *\n * @example\n * type UserId = Brand<string, \"UserId\">;\n */\nexport type Brand<T, B> = T & { readonly __brand: B };\n\n/**\n * A branded string type for an entity named `T`.\n * Equivalent to `Brand<string, T>`, but more ergonomic for ID types.\n *\n * @example\n * type UserId = IdOf<\"User\">;\n * type PostId = IdOf<\"Post\">;\n */\nexport type IdOf<T extends string> = Brand<string, T>;\n\n/**\n * Casts a plain string to a branded `IdOf<T>`.\n *\n * This is a type-only operation at runtime — no validation is performed.\n * Use `assertValidId` from `\"sigilid/validate\"` first if you need runtime safety.\n *\n * @example\n * import { castId } from \"sigilid/typed\";\n * const id = castId<\"User\">(\"usr_abc123\");\n */\nexport function castId<T extends string>(value: string): IdOf<T> {\n return value as IdOf<T>;\n}\n\n/**\n * Returns a factory function that generates cryptographically secure branded IDs.\n *\n * The type parameter `T` sets the brand. If a prefix is provided, the format\n * is `{prefix}_{id}`, otherwise it is a plain random string.\n *\n * Validation happens at factory creation, not per-call.\n *\n * @param prefix - Optional alphanumeric prefix.\n * @param length - Length of the random portion (1–255). Defaults to 21.\n * @returns A zero-argument function returning `IdOf<T>`.\n *\n * @example\n * import { createTypedGenerator } from \"sigilid/typed\";\n * const userId = createTypedGenerator<\"User\">(\"usr\");\n * const id = userId(); // IdOf<\"User\"> = \"usr_K7gkJ_q3vR2nL8xH5eM0w\"\n */\nexport function createTypedGenerator<T extends string>(\n prefix?: string,\n length: number = DEFAULT_LENGTH,\n): () => IdOf<T> {\n if (prefix !== undefined) assertPrefix(prefix);\n assertLength(length);\n\n return (): IdOf<T> => {\n const raw = generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);\n const value = prefix !== undefined ? `${prefix}${SEPARATOR}${raw}` : raw;\n return value as IdOf<T>;\n };\n}\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var DEFAULT_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
|
|
5
|
+
|
|
6
|
+
// src/internal/validation.ts
|
|
7
|
+
function isValidIdValue(value, options = {}) {
|
|
8
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
9
|
+
let idPart = value;
|
|
10
|
+
if (options.prefix !== void 0) {
|
|
11
|
+
const expected = `${options.prefix}_`;
|
|
12
|
+
if (!value.startsWith(expected)) return false;
|
|
13
|
+
idPart = value.slice(expected.length);
|
|
14
|
+
}
|
|
15
|
+
if (options.length !== void 0 && idPart.length !== options.length) return false;
|
|
16
|
+
if (idPart.length === 0) return false;
|
|
17
|
+
if (options.alphabet !== void 0) {
|
|
18
|
+
const charSet = new Set(options.alphabet);
|
|
19
|
+
for (const char of idPart) {
|
|
20
|
+
if (!charSet.has(char)) return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/validate.ts
|
|
27
|
+
function isValidId(value, options = {}) {
|
|
28
|
+
const opts = { alphabet: DEFAULT_ALPHABET, ...options };
|
|
29
|
+
return isValidIdValue(value, opts);
|
|
30
|
+
}
|
|
31
|
+
function assertValidId(value, options = {}) {
|
|
32
|
+
if (!isValidId(value, options)) {
|
|
33
|
+
throw new TypeError(`Invalid ID: "${value}"`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function parseId(value, options = {}) {
|
|
37
|
+
assertValidId(value, options);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
exports.assertValidId = assertValidId;
|
|
42
|
+
exports.isValidId = isValidId;
|
|
43
|
+
exports.parseId = parseId;
|
|
44
|
+
//# sourceMappingURL=validate.cjs.map
|
|
45
|
+
//# sourceMappingURL=validate.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/internal/validation.ts","../src/validate.ts"],"names":[],"mappings":";;;AAQO,IAAM,gBAAA,GAAmB,kEAAA;;;ACCzB,SAAS,cAAA,CAAe,KAAA,EAAe,OAAA,GAA6B,EAAC,EAAY;AACtF,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,CAAM,MAAA,KAAW,GAAG,OAAO,KAAA;AAE5D,EAAA,IAAI,MAAA,GAAS,KAAA;AAEb,EAAA,IAAI,OAAA,CAAQ,WAAW,MAAA,EAAW;AAChC,IAAA,MAAM,QAAA,GAAW,CAAA,EAAG,OAAA,CAAQ,MAAM,CAAA,CAAA,CAAA;AAClC,IAAA,IAAI,CAAC,KAAA,CAAM,UAAA,CAAW,QAAQ,GAAG,OAAO,KAAA;AACxC,IAAA,MAAA,GAAS,KAAA,CAAM,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA;AAAA,EACtC;AAEA,EAAA,IAAI,QAAQ,MAAA,KAAW,MAAA,IAAa,OAAO,MAAA,KAAW,OAAA,CAAQ,QAAQ,OAAO,KAAA;AAC7E,EAAA,IAAI,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG,OAAO,KAAA;AAEhC,EAAA,IAAI,OAAA,CAAQ,aAAa,MAAA,EAAW;AAClC,IAAA,MAAM,OAAA,GAAU,IAAI,GAAA,CAAI,OAAA,CAAQ,QAAQ,CAAA;AACxC,IAAA,KAAA,MAAW,QAAQ,MAAA,EAAQ;AACzB,MAAA,IAAI,CAAC,OAAA,CAAQ,GAAA,CAAI,IAAI,GAAG,OAAO,KAAA;AAAA,IACjC;AAAA,EACF;AAEA,EAAA,OAAO,IAAA;AACT;;;ACZO,SAAS,SAAA,CAAU,KAAA,EAAe,OAAA,GAA6B,EAAC,EAAY;AACjF,EAAA,MAAM,IAAA,GAAO,EAAE,QAAA,EAAU,gBAAA,EAAkB,GAAG,OAAA,EAAQ;AACtD,EAAA,OAAO,cAAA,CAAe,OAAO,IAAI,CAAA;AACnC;AAWO,SAAS,aAAA,CAAc,KAAA,EAAe,OAAA,GAA6B,EAAC,EAAS;AAClF,EAAA,IAAI,CAAC,SAAA,CAAU,KAAA,EAAO,OAAO,CAAA,EAAG;AAC9B,IAAA,MAAM,IAAI,SAAA,CAAU,CAAA,aAAA,EAAgB,KAAK,CAAA,CAAA,CAAG,CAAA;AAAA,EAC9C;AACF;AAYO,SAAS,OAAA,CAAQ,KAAA,EAAe,OAAA,GAA6B,EAAC,EAAW;AAC9E,EAAA,aAAA,CAAc,OAAO,OAAO,CAAA;AAC5B,EAAA,OAAO,KAAA;AACT","file":"validate.cjs","sourcesContent":["import { generateFromAlphabet } from \"./internal/alphabet.js\";\nimport { assertLength } from \"./internal/assert.js\";\nimport { randomBytes } from \"./internal/random.js\";\n\n/**\n * URL-safe alphabet used by default. 64 characters: A-Z, a-z, 0-9, _, -.\n * Chosen to be safe in URLs, filenames, and most log formats without encoding.\n */\nexport const DEFAULT_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-\";\n\nconst DEFAULT_LENGTH = 21;\n\n/**\n * Generates a cryptographically secure random ID.\n *\n * @param length - Number of characters (1–255). Defaults to 21.\n * @returns A random URL-safe string of the requested length.\n *\n * @example\n * import { generateId } from \"sigilid\";\n * const id = generateId(); // \"K7gkJ_q3vR2nL8xH5eM0w\"\n */\nexport function generateId(length: number = DEFAULT_LENGTH): string {\n assertLength(length);\n return generateFromAlphabet(DEFAULT_ALPHABET, length, randomBytes);\n}\n","export interface ValidationOptions {\n /** Expected exact length of the ID portion (excluding prefix and separator). */\n length?: number;\n /** Expected prefix. The separator `_` is assumed. */\n prefix?: string;\n /** Alphabet the ID characters must belong to. Defaults to DEFAULT_ALPHABET. */\n alphabet?: string;\n}\n\nexport function isValidIdValue(value: string, options: ValidationOptions = {}): boolean {\n if (typeof value !== \"string\" || value.length === 0) return false;\n\n let idPart = value;\n\n if (options.prefix !== undefined) {\n const expected = `${options.prefix}_`;\n if (!value.startsWith(expected)) return false;\n idPart = value.slice(expected.length);\n }\n\n if (options.length !== undefined && idPart.length !== options.length) return false;\n if (idPart.length === 0) return false;\n\n if (options.alphabet !== undefined) {\n const charSet = new Set(options.alphabet);\n for (const char of idPart) {\n if (!charSet.has(char)) return false;\n }\n }\n\n return true;\n}\n","import { DEFAULT_ALPHABET } from \"./index.js\";\nimport type { ValidationOptions } from \"./internal/validation.js\";\nimport { isValidIdValue } from \"./internal/validation.js\";\n\nexport type { ValidationOptions };\n\n/**\n * Returns `true` if `value` is a valid ID, `false` otherwise.\n *\n * By default, validates that the value is a non-empty string of characters\n * from the default URL-safe alphabet. Options can constrain length, prefix,\n * and alphabet.\n *\n * @example\n * import { isValidId } from \"sigilid/validate\";\n * isValidId(\"K7gkJ_q3vR2nL8xH5eM0w\"); // true\n * isValidId(\"usr_K7gkJ_q3vR2nL8xH5eM0w\", { prefix: \"usr\" }); // true\n * isValidId(\"\", {}); // false\n */\nexport function isValidId(value: string, options: ValidationOptions = {}): boolean {\n const opts = { alphabet: DEFAULT_ALPHABET, ...options };\n return isValidIdValue(value, opts);\n}\n\n/**\n * Throws a `TypeError` if `value` is not a valid ID.\n *\n * Useful for asserting IDs at API boundaries.\n *\n * @example\n * import { assertValidId } from \"sigilid/validate\";\n * assertValidId(req.params.id); // throws if invalid\n */\nexport function assertValidId(value: string, options: ValidationOptions = {}): void {\n if (!isValidId(value, options)) {\n throw new TypeError(`Invalid ID: \"${value}\"`);\n }\n}\n\n/**\n * Parses and returns the ID if valid, otherwise throws.\n *\n * A convenience wrapper over `assertValidId` for use in pipelines where\n * you want the validated value back from the same call.\n *\n * @example\n * import { parseId } from \"sigilid/validate\";\n * const id = parseId(rawInput); // throws if invalid, returns the same string if valid\n */\nexport function parseId(value: string, options: ValidationOptions = {}): string {\n assertValidId(value, options);\n return value;\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
interface ValidationOptions {
|
|
2
|
+
/** Expected exact length of the ID portion (excluding prefix and separator). */
|
|
3
|
+
length?: number;
|
|
4
|
+
/** Expected prefix. The separator `_` is assumed. */
|
|
5
|
+
prefix?: string;
|
|
6
|
+
/** Alphabet the ID characters must belong to. Defaults to DEFAULT_ALPHABET. */
|
|
7
|
+
alphabet?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns `true` if `value` is a valid ID, `false` otherwise.
|
|
12
|
+
*
|
|
13
|
+
* By default, validates that the value is a non-empty string of characters
|
|
14
|
+
* from the default URL-safe alphabet. Options can constrain length, prefix,
|
|
15
|
+
* and alphabet.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* import { isValidId } from "sigilid/validate";
|
|
19
|
+
* isValidId("K7gkJ_q3vR2nL8xH5eM0w"); // true
|
|
20
|
+
* isValidId("usr_K7gkJ_q3vR2nL8xH5eM0w", { prefix: "usr" }); // true
|
|
21
|
+
* isValidId("", {}); // false
|
|
22
|
+
*/
|
|
23
|
+
declare function isValidId(value: string, options?: ValidationOptions): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Throws a `TypeError` if `value` is not a valid ID.
|
|
26
|
+
*
|
|
27
|
+
* Useful for asserting IDs at API boundaries.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* import { assertValidId } from "sigilid/validate";
|
|
31
|
+
* assertValidId(req.params.id); // throws if invalid
|
|
32
|
+
*/
|
|
33
|
+
declare function assertValidId(value: string, options?: ValidationOptions): void;
|
|
34
|
+
/**
|
|
35
|
+
* Parses and returns the ID if valid, otherwise throws.
|
|
36
|
+
*
|
|
37
|
+
* A convenience wrapper over `assertValidId` for use in pipelines where
|
|
38
|
+
* you want the validated value back from the same call.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* import { parseId } from "sigilid/validate";
|
|
42
|
+
* const id = parseId(rawInput); // throws if invalid, returns the same string if valid
|
|
43
|
+
*/
|
|
44
|
+
declare function parseId(value: string, options?: ValidationOptions): string;
|
|
45
|
+
|
|
46
|
+
export { type ValidationOptions, assertValidId, isValidId, parseId };
|