@smonn/ids 0.9.4 → 0.11.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/README.md +7 -5
- package/dist/{adapter-types-BY-wrYYB.mjs → adapter-types-7wWdELSh.mjs} +2 -2
- package/dist/adapter-types-7wWdELSh.mjs.map +1 -0
- package/dist/{adapter-types-unUcmMXC.d.mts → adapter-types-CdYJM6Sf.d.mts} +2 -2
- package/dist/adapter-types-CdYJM6Sf.d.mts.map +1 -0
- package/dist/cli.mjs +97 -13
- package/dist/cli.mjs.map +1 -1
- package/dist/{codec-shell-CW2sD6BU.mjs → codec-shell-DvrTDa65.mjs} +4 -4
- package/dist/codec-shell-DvrTDa65.mjs.map +1 -0
- package/dist/digest-CknNw2wa.mjs +147 -0
- package/dist/digest-CknNw2wa.mjs.map +1 -0
- package/dist/digest.d.mts +124 -0
- package/dist/digest.d.mts.map +1 -0
- package/dist/digest.mjs +3 -0
- package/dist/drizzle.d.mts +3 -3
- package/dist/drizzle.d.mts.map +1 -1
- package/dist/drizzle.mjs +2 -2
- package/dist/drizzle.mjs.map +1 -1
- package/dist/error-Cp5qYZcv.mjs.map +1 -1
- package/dist/{error-DTr4i6Ic.d.mts → error-JIPylU_E.d.mts} +2 -2
- package/dist/{error-DTr4i6Ic.d.mts.map → error-JIPylU_E.d.mts.map} +1 -1
- package/dist/express.d.mts +2 -2
- package/dist/express.d.mts.map +1 -1
- package/dist/express.mjs +2 -2
- package/dist/express.mjs.map +1 -1
- package/dist/fastify.d.mts +3 -3
- package/dist/fastify.d.mts.map +1 -1
- package/dist/fastify.mjs +3 -3
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.d.mts +5 -4
- package/dist/hono.d.mts.map +1 -1
- package/dist/hono.mjs +4 -3
- package/dist/hono.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{key-material-gOnqTNoV.mjs → key-material-f29JIyrz.mjs} +3 -3
- package/dist/key-material-f29JIyrz.mjs.map +1 -0
- package/dist/kysely.d.mts +3 -3
- package/dist/kysely.d.mts.map +1 -1
- package/dist/kysely.mjs +2 -2
- package/dist/kysely.mjs.map +1 -1
- package/dist/{opaque-BpqxV8oB.mjs → opaque-ayT0KdCt.mjs} +8 -8
- package/dist/opaque-ayT0KdCt.mjs.map +1 -0
- package/dist/opaque.d.mts +4 -4
- package/dist/opaque.d.mts.map +1 -1
- package/dist/opaque.mjs +1 -1
- package/dist/prisma.d.mts +3 -3
- package/dist/prisma.d.mts.map +1 -1
- package/dist/prisma.mjs +2 -2
- package/dist/prisma.mjs.map +1 -1
- package/dist/{reverse-d5uEoIET.mjs → reverse-BRZRc1_U.mjs} +6 -6
- package/dist/reverse-BRZRc1_U.mjs.map +1 -0
- package/dist/reverse.d.mts +2 -2
- package/dist/reverse.d.mts.map +1 -1
- package/dist/reverse.mjs +1 -1
- package/dist/{rng-CPJOx_nE.mjs → rng-DHxioKyI.mjs} +2 -2
- package/dist/rng-DHxioKyI.mjs.map +1 -0
- package/dist/{signed-BnRSC03a.mjs → signed-C8OMt3TJ.mjs} +10 -10
- package/dist/signed-C8OMt3TJ.mjs.map +1 -0
- package/dist/signed.d.mts +5 -5
- package/dist/signed.d.mts.map +1 -1
- package/dist/signed.mjs +1 -1
- package/dist/{timestamp-BbZL8hwg.mjs → timestamp-DBwVjDkg.mjs} +5 -5
- package/dist/timestamp-DBwVjDkg.mjs.map +1 -0
- package/dist/{timestamp-bytes-DoFjLjDp.mjs → timestamp-bytes-DvhWHDa-.mjs} +2 -2
- package/dist/timestamp-bytes-DvhWHDa-.mjs.map +1 -0
- package/dist/{wrapped-BI9UXnAm.mjs → wrapped-CDTiPwNM.mjs} +29 -12
- package/dist/wrapped-CDTiPwNM.mjs.map +1 -0
- package/dist/wrapped.d.mts +4 -4
- package/dist/wrapped.d.mts.map +1 -1
- package/dist/wrapped.mjs +1 -1
- package/package.json +5 -3
- package/dist/adapter-types-BY-wrYYB.mjs.map +0 -1
- package/dist/adapter-types-unUcmMXC.d.mts.map +0 -1
- package/dist/codec-shell-CW2sD6BU.mjs.map +0 -1
- package/dist/key-material-gOnqTNoV.mjs.map +0 -1
- package/dist/opaque-BpqxV8oB.mjs.map +0 -1
- package/dist/reverse-d5uEoIET.mjs.map +0 -1
- package/dist/rng-CPJOx_nE.mjs.map +0 -1
- package/dist/signed-BnRSC03a.mjs.map +0 -1
- package/dist/timestamp-BbZL8hwg.mjs.map +0 -1
- package/dist/timestamp-bytes-DoFjLjDp.mjs.map +0 -1
- package/dist/wrapped-BI9UXnAm.mjs.map +0 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { t as IdsError } from "./error-Cp5qYZcv.mjs";
|
|
2
|
+
import { a as toWireId, n as registerBrand, r as payloadBase32Length, s as validateBrand, t as wireMethods } from "./codec-shell-DvrTDa65.mjs";
|
|
3
|
+
import { i as encodeKeyMaterial, r as decodeKeyMaterial, t as assertValidKeyMaterialByteLength } from "./key-material-f29JIyrz.mjs";
|
|
4
|
+
//#region src/codecs/digest/layout.ts
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
function writeLen32(value, target, offset) {
|
|
7
|
+
target[offset] = value >>> 24 & 255;
|
|
8
|
+
target[offset + 1] = value >>> 16 & 255;
|
|
9
|
+
target[offset + 2] = value >>> 8 & 255;
|
|
10
|
+
target[offset + 3] = value & 255;
|
|
11
|
+
}
|
|
12
|
+
function buildMessage(brandBytes, nsBytes, material) {
|
|
13
|
+
const msgLen = 4 + brandBytes.length + 4 + nsBytes.length + material.length;
|
|
14
|
+
const message = new Uint8Array(msgLen);
|
|
15
|
+
let offset = 0;
|
|
16
|
+
writeLen32(brandBytes.length, message, offset);
|
|
17
|
+
offset += 4;
|
|
18
|
+
message.set(brandBytes, offset);
|
|
19
|
+
offset += brandBytes.length;
|
|
20
|
+
writeLen32(nsBytes.length, message, offset);
|
|
21
|
+
offset += 4;
|
|
22
|
+
message.set(nsBytes, offset);
|
|
23
|
+
offset += nsBytes.length;
|
|
24
|
+
message.set(material, offset);
|
|
25
|
+
return message;
|
|
26
|
+
}
|
|
27
|
+
function createDigestLayoutOps(prefix, brand, ns, hmacKey) {
|
|
28
|
+
const brandBytes = encoder.encode(brand);
|
|
29
|
+
const nsBytes = encoder.encode(ns);
|
|
30
|
+
return {
|
|
31
|
+
digest: async (material) => {
|
|
32
|
+
const message = buildMessage(brandBytes, nsBytes, typeof material === "string" ? encoder.encode(material) : material);
|
|
33
|
+
return toWireId(prefix, new Uint8Array(await crypto.subtle.sign("HMAC", hmacKey, message)).subarray(0, 16));
|
|
34
|
+
},
|
|
35
|
+
exampleWireId: () => prefix + "0".repeat(payloadBase32Length)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/codecs/digest/key.ts
|
|
40
|
+
const hmacInfo = new TextEncoder().encode("@smonn/ids/digest/hmac");
|
|
41
|
+
const internals = /* @__PURE__ */ new WeakMap();
|
|
42
|
+
/**
|
|
43
|
+
* Import raw operator key material into a {@link DigestKey} handle.
|
|
44
|
+
*
|
|
45
|
+
* Derives a single HMAC-SHA-256 key via HKDF under the domain-separation label
|
|
46
|
+
* `@smonn/ids/digest/hmac`. Accepts 16, 24, or 32 bytes. To store or transport key
|
|
47
|
+
* material, use {@link encodeDigestKey} / {@link decodeDigestKey}
|
|
48
|
+
* (`"hex"` or `"base64url"` — not Crockford base32).
|
|
49
|
+
*
|
|
50
|
+
* @param bytes - 16, 24, or 32 raw key bytes.
|
|
51
|
+
* @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
|
|
52
|
+
*/
|
|
53
|
+
async function importDigestKey(bytes) {
|
|
54
|
+
assertValidKeyMaterialByteLength(bytes.length, "digest");
|
|
55
|
+
const hmacKey = await deriveHmacKey(bytes);
|
|
56
|
+
const key = Object.freeze({});
|
|
57
|
+
internals.set(key, { hmacKey });
|
|
58
|
+
return key;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Encode raw digest operator key material for storage in env vars or secret managers.
|
|
62
|
+
*
|
|
63
|
+
* Supports `"hex"` (lowercase) and `"base64url"`. Output round-trips through
|
|
64
|
+
* {@link decodeDigestKey} back to the original bytes.
|
|
65
|
+
*
|
|
66
|
+
* @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
|
|
67
|
+
* @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
|
|
68
|
+
*/
|
|
69
|
+
function encodeDigestKey(bytes, format) {
|
|
70
|
+
return encodeKeyMaterial(bytes, format, "digest", "digest");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Decode key material emitted by {@link encodeDigestKey} back to raw bytes.
|
|
74
|
+
*
|
|
75
|
+
* The result can be passed directly to {@link importDigestKey}.
|
|
76
|
+
*
|
|
77
|
+
* @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
|
|
78
|
+
* @throws {IdsError} `invalid_key_encoding` if the string is malformed for its format.
|
|
79
|
+
* @throws {IdsError} `invalid_key_length` if the decoded bytes are not 16, 24, or 32 bytes.
|
|
80
|
+
*/
|
|
81
|
+
function decodeDigestKey(encoded, format) {
|
|
82
|
+
return decodeKeyMaterial(encoded, format, "digest", "digest");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Returns the derived HMAC webcrypto.CryptoKey held inside the handle.
|
|
86
|
+
*
|
|
87
|
+
* Intentional module-internal escape hatch for codec implementations.
|
|
88
|
+
* Not re-exported from `@smonn/ids/digest`; external callers cannot reach this.
|
|
89
|
+
*/
|
|
90
|
+
function getDigestKeyHmacKey(key) {
|
|
91
|
+
const keyInternals = internals.get(key);
|
|
92
|
+
/* v8 ignore next -- defensive guard; only reachable with a forged DigestKey handle */
|
|
93
|
+
if (keyInternals === void 0) throw new Error("invalid digest key");
|
|
94
|
+
return keyInternals.hmacKey;
|
|
95
|
+
}
|
|
96
|
+
async function deriveHmacKey(bytes) {
|
|
97
|
+
const base = await crypto.subtle.importKey("raw", bytes, "HKDF", false, ["deriveKey"]);
|
|
98
|
+
return crypto.subtle.deriveKey({
|
|
99
|
+
name: "HKDF",
|
|
100
|
+
hash: "SHA-256",
|
|
101
|
+
salt: /* @__PURE__ */ new Uint8Array(),
|
|
102
|
+
info: hmacInfo
|
|
103
|
+
}, base, {
|
|
104
|
+
name: "HMAC",
|
|
105
|
+
hash: "SHA-256",
|
|
106
|
+
length: 256
|
|
107
|
+
}, false, ["sign"]);
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/codecs/digest/index.ts
|
|
111
|
+
/**
|
|
112
|
+
* Construct a {@link DigestCodec} for `brand`.
|
|
113
|
+
*
|
|
114
|
+
* `opts.ns` is the required namespace — the same material under a
|
|
115
|
+
* different `ns` yields a different ID. `opts.key` is the single operator
|
|
116
|
+
* Digest key; there is no keyring.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* const key = await importDigestKey(new Uint8Array(32));
|
|
121
|
+
* const idk = createDigestId("idk", { ns: "checkout", key });
|
|
122
|
+
*
|
|
123
|
+
* const id = await idk.digest("order-123"); // Id<"idk">
|
|
124
|
+
* idk.is(id); // true
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
function createDigestId(brand, opts) {
|
|
128
|
+
validateBrand(brand);
|
|
129
|
+
registerBrand(brand, opts.allowDuplicateBrand);
|
|
130
|
+
if (typeof opts.ns !== "string" || opts.ns.trim() === "") throw new IdsError("invalid_namespace", "invalid namespace: ns must be a non-empty, non-whitespace string");
|
|
131
|
+
const hmacKey = getDigestKeyHmacKey(opts.key);
|
|
132
|
+
const prefix = `${brand}_`;
|
|
133
|
+
const wire = wireMethods(prefix);
|
|
134
|
+
const layout = createDigestLayoutOps(prefix, brand, opts.ns, hmacKey);
|
|
135
|
+
return {
|
|
136
|
+
digest: layout.digest,
|
|
137
|
+
is: wire.is,
|
|
138
|
+
parse: wire.parse,
|
|
139
|
+
safeParse: wire.safeParse,
|
|
140
|
+
toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId()),
|
|
141
|
+
"~standard": wire["~standard"]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
export { importDigestKey as i, decodeDigestKey as n, encodeDigestKey as r, createDigestId as t };
|
|
146
|
+
|
|
147
|
+
//# sourceMappingURL=digest-CknNw2wa.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest-CknNw2wa.mjs","names":[],"sources":["../src/codecs/digest/layout.ts","../src/codecs/digest/key.ts","../src/codecs/digest/index.ts"],"sourcesContent":["import type { webcrypto } from \"node:crypto\";\nimport type { Id, Prefix } from \"../../types.js\";\nimport { toWireId } from \"../../wire/envelope.js\";\nimport { payloadBase32Length, payloadByteLength } from \"../../wire/invariants.js\";\n\nconst encoder = new TextEncoder();\n\nfunction writeLen32(value: number, target: Uint8Array, offset: number): void {\n target[offset] = (value >>> 24) & 0xff;\n target[offset + 1] = (value >>> 16) & 0xff;\n target[offset + 2] = (value >>> 8) & 0xff;\n target[offset + 3] = value & 0xff;\n}\n\nfunction buildMessage(\n brandBytes: Uint8Array,\n nsBytes: Uint8Array,\n material: Uint8Array,\n): Uint8Array {\n const msgLen = 4 + brandBytes.length + 4 + nsBytes.length + material.length;\n const message = new Uint8Array(msgLen);\n let offset = 0;\n writeLen32(brandBytes.length, message, offset);\n offset += 4;\n message.set(brandBytes, offset);\n offset += brandBytes.length;\n writeLen32(nsBytes.length, message, offset);\n offset += 4;\n message.set(nsBytes, offset);\n offset += nsBytes.length;\n message.set(material, offset);\n return message;\n}\n\nexport function createDigestLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n brand: Brand,\n ns: string,\n hmacKey: webcrypto.CryptoKey,\n) {\n const brandBytes = encoder.encode(brand);\n const nsBytes = encoder.encode(ns);\n\n return {\n digest: async (material: string | Uint8Array): Promise<Id<Brand>> => {\n const materialBytes = typeof material === \"string\" ? encoder.encode(material) : material;\n const message = buildMessage(brandBytes, nsBytes, materialBytes);\n const hmacOutput = new Uint8Array(\n await crypto.subtle.sign(\"HMAC\", hmacKey, message as Uint8Array<ArrayBuffer>),\n );\n const payload = hmacOutput.subarray(0, payloadByteLength);\n return toWireId(prefix, payload);\n },\n exampleWireId: (): Id<Brand> => (prefix + \"0\".repeat(payloadBase32Length)) as Id<Brand>,\n };\n}\n","import type { webcrypto } from \"node:crypto\";\nimport {\n assertValidKeyMaterialByteLength,\n decodeKeyMaterial,\n encodeKeyMaterial,\n} from \"../_kernel/key-material.js\";\n\n/** Wire encoding for digest operator key material (not Crockford base32). */\nexport type DigestKeyFormat = \"hex\" | \"base64url\";\n\nconst hmacInfo = new TextEncoder().encode(\"@smonn/ids/digest/hmac\");\n\ndeclare const digestKeyBrand: unique symbol;\n\n/**\n * Opaque imported handle for one operator Digest key.\n *\n * Holds a single HMAC-SHA-256 key derived via HKDF under the domain-separation\n * label `@smonn/ids/digest/hmac`. The underlying `webcrypto.CryptoKey` is held internally and\n * never exposed to callers. Obtain handles via {@link importDigestKey} and pass\n * them to `createDigestId` as the `key` option.\n *\n * Unlike the other keyed codecs, the Digest codec holds exactly one key — there\n * is no keyring. Re-keying is a deliberate, breaking operator action (every ID\n * changes), never an in-band rotation.\n *\n * Distinct from the **Opaque key**, **Wrapping key**, and **Signing key** — the\n * same raw bytes imported as a `DigestKey` are cryptographically independent of\n * any other codec's key.\n */\nexport type DigestKey = {\n readonly [digestKeyBrand]: \"DigestKey\";\n};\n\ntype DigestKeyInternals = {\n hmacKey: webcrypto.CryptoKey;\n};\n\nconst internals = new WeakMap<DigestKey, DigestKeyInternals>();\n\n/**\n * Import raw operator key material into a {@link DigestKey} handle.\n *\n * Derives a single HMAC-SHA-256 key via HKDF under the domain-separation label\n * `@smonn/ids/digest/hmac`. Accepts 16, 24, or 32 bytes. To store or transport key\n * material, use {@link encodeDigestKey} / {@link decodeDigestKey}\n * (`\"hex\"` or `\"base64url\"` — not Crockford base32).\n *\n * @param bytes - 16, 24, or 32 raw key bytes.\n * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.\n */\nexport async function importDigestKey(bytes: Uint8Array): Promise<DigestKey> {\n assertValidKeyMaterialByteLength(bytes.length, \"digest\");\n const hmacKey = await deriveHmacKey(bytes);\n const key = Object.freeze({}) as DigestKey;\n internals.set(key, { hmacKey });\n return key;\n}\n\n/**\n * Encode raw digest operator key material for storage in env vars or secret managers.\n *\n * Supports `\"hex\"` (lowercase) and `\"base64url\"`. Output round-trips through\n * {@link decodeDigestKey} back to the original bytes.\n *\n * @throws {IdsError} `invalid_key_format` if `format` is not `\"hex\"` or `\"base64url\"`.\n * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.\n */\nexport function encodeDigestKey(bytes: Uint8Array, format: DigestKeyFormat): string {\n return encodeKeyMaterial(bytes, format, \"digest\", \"digest\");\n}\n\n/**\n * Decode key material emitted by {@link encodeDigestKey} back to raw bytes.\n *\n * The result can be passed directly to {@link importDigestKey}.\n *\n * @throws {IdsError} `invalid_key_format` if `format` is not `\"hex\"` or `\"base64url\"`.\n * @throws {IdsError} `invalid_key_encoding` if the string is malformed for its format.\n * @throws {IdsError} `invalid_key_length` if the decoded bytes are not 16, 24, or 32 bytes.\n */\nexport function decodeDigestKey(encoded: string, format: DigestKeyFormat): Uint8Array {\n return decodeKeyMaterial(encoded, format, \"digest\", \"digest\");\n}\n\n/**\n * Returns the derived HMAC webcrypto.CryptoKey held inside the handle.\n *\n * Intentional module-internal escape hatch for codec implementations.\n * Not re-exported from `@smonn/ids/digest`; external callers cannot reach this.\n */\nexport function getDigestKeyHmacKey(key: DigestKey): webcrypto.CryptoKey {\n const keyInternals = internals.get(key);\n /* v8 ignore next -- defensive guard; only reachable with a forged DigestKey handle */\n if (keyInternals === undefined) throw new Error(\"invalid digest key\");\n return keyInternals.hmacKey;\n}\n\nasync function deriveHmacKey(bytes: Uint8Array): Promise<webcrypto.CryptoKey> {\n const base = await crypto.subtle.importKey(\n \"raw\",\n bytes as Uint8Array<ArrayBuffer>,\n \"HKDF\",\n false,\n [\"deriveKey\"],\n );\n return crypto.subtle.deriveKey(\n { name: \"HKDF\", hash: \"SHA-256\", salt: new Uint8Array(), info: hmacInfo },\n base,\n { name: \"HMAC\", hash: \"SHA-256\", length: 256 },\n false,\n [\"sign\"],\n );\n}\n","import { validateBrand } from \"../_kernel/brand.js\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"../../error.js\";\nimport { createDigestLayoutOps } from \"./layout.js\";\nimport { registerBrand } from \"../_kernel/registry.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"../../types.js\";\nimport { wireMethods } from \"../../wire/codec-shell.js\";\nimport {\n decodeDigestKey,\n encodeDigestKey,\n getDigestKeyHmacKey,\n importDigestKey,\n type DigestKey,\n type DigestKeyFormat,\n} from \"./key.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode };\nexport { decodeDigestKey, encodeDigestKey, importDigestKey, type DigestKey, type DigestKeyFormat };\n\n/**\n * Configuration options for a Digest codec instance.\n */\nexport type DigestOptions = {\n /**\n * Non-secret, required namespace. The same material under a different\n * `ns` yields a different ID, so one key can serve multiple unlinkable namespaces.\n * Must be non-empty and not whitespace-only.\n */\n ns: string;\n /**\n * Single operator digest key. The Digest codec holds exactly one key — there\n * is no keyring. Re-keying is a deliberate, breaking operator action.\n */\n key: DigestKey;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\n/**\n * Codec returned by {@link createDigestId}.\n *\n * Maps caller **material** to a stable public ID under one **Digest key**:\n * the same material always yields the same ID, and the material cannot be\n * recovered from the ID (**equality leakage** is the intended property).\n *\n * - `digest` is async (WebCrypto HMAC).\n * - `is`, `parse`, `safeParse`, `toJsonSchema`, and `~standard` are synchronous\n * and require no key material — they validate prefix and base32 shape only.\n * - There is no reverse method (`unwrap`, `verify`, `extractTimestamp`) — the\n * codec is one-way by definition.\n */\nexport type DigestCodec<Brand extends string> = {\n /**\n * Digest `material` into a stable canonical {@link Id}.\n *\n * The same `(brand, ns, key, material)` tuple always returns the same ID.\n * Strings are UTF-8 encoded; byte arrays are used as-is.\n */\n digest(material: string | Uint8Array): Promise<Id<Brand>>;\n /** Strict type guard: `true` only for already-canonical `Id<Brand>` strings. */\n is(value: unknown): value is Id<Brand>;\n /** Normalise to canonical form, or throw on parse failure. */\n parse(value: unknown): Id<Brand>;\n /** Normalise to canonical form, or return `{ ok: false, error }`. */\n safeParse(value: unknown): ParseResult<Brand>;\n /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\n/**\n * Construct a {@link DigestCodec} for `brand`.\n *\n * `opts.ns` is the required namespace — the same material under a\n * different `ns` yields a different ID. `opts.key` is the single operator\n * Digest key; there is no keyring.\n *\n * @example\n * ```ts\n * const key = await importDigestKey(new Uint8Array(32));\n * const idk = createDigestId(\"idk\", { ns: \"checkout\", key });\n *\n * const id = await idk.digest(\"order-123\"); // Id<\"idk\">\n * idk.is(id); // true\n * ```\n */\nexport function createDigestId<Brand extends string>(\n brand: Brand,\n opts: DigestOptions,\n): DigestCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n if (typeof opts.ns !== \"string\" || opts.ns.trim() === \"\") {\n throw new IdsError(\n \"invalid_namespace\",\n \"invalid namespace: ns must be a non-empty, non-whitespace string\",\n );\n }\n\n const hmacKey = getDigestKeyHmacKey(opts.key);\n const prefix: Prefix<Brand> = `${brand}_`;\n const wire = wireMethods(prefix);\n const layout = createDigestLayoutOps(prefix, brand, opts.ns, hmacKey);\n\n return {\n digest: layout.digest,\n is: wire.is,\n parse: wire.parse,\n safeParse: wire.safeParse,\n toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId()),\n \"~standard\": wire[\"~standard\"],\n };\n}\n"],"mappings":";;;;AAKA,MAAM,UAAU,IAAI,YAAY;AAEhC,SAAS,WAAW,OAAe,QAAoB,QAAsB;CAC3E,OAAO,UAAW,UAAU,KAAM;CAClC,OAAO,SAAS,KAAM,UAAU,KAAM;CACtC,OAAO,SAAS,KAAM,UAAU,IAAK;CACrC,OAAO,SAAS,KAAK,QAAQ;AAC/B;AAEA,SAAS,aACP,YACA,SACA,UACY;CACZ,MAAM,SAAS,IAAI,WAAW,SAAS,IAAI,QAAQ,SAAS,SAAS;CACrE,MAAM,UAAU,IAAI,WAAW,MAAM;CACrC,IAAI,SAAS;CACb,WAAW,WAAW,QAAQ,SAAS,MAAM;CAC7C,UAAU;CACV,QAAQ,IAAI,YAAY,MAAM;CAC9B,UAAU,WAAW;CACrB,WAAW,QAAQ,QAAQ,SAAS,MAAM;CAC1C,UAAU;CACV,QAAQ,IAAI,SAAS,MAAM;CAC3B,UAAU,QAAQ;CAClB,QAAQ,IAAI,UAAU,MAAM;CAC5B,OAAO;AACT;AAEA,SAAgB,sBACd,QACA,OACA,IACA,SACA;CACA,MAAM,aAAa,QAAQ,OAAO,KAAK;CACvC,MAAM,UAAU,QAAQ,OAAO,EAAE;CAEjC,OAAO;EACL,QAAQ,OAAO,aAAsD;GAEnE,MAAM,UAAU,aAAa,YAAY,SADnB,OAAO,aAAa,WAAW,QAAQ,OAAO,QAAQ,IAAI,QACjB;GAK/D,OAAO,SAAS,QADA,IAHO,WACrB,MAAM,OAAO,OAAO,KAAK,QAAQ,SAAS,OAAkC,CAErD,CAAC,CAAC,SAAS,GAAA,EACN,CAAC;EACjC;EACA,qBAAiC,SAAS,IAAI,OAAO,mBAAmB;CAC1E;AACF;;;AC7CA,MAAM,WAAW,IAAI,YAAY,CAAC,CAAC,OAAO,wBAAwB;AA4BlE,MAAM,4BAAY,IAAI,QAAuC;;;;;;;;;;;;AAa7D,eAAsB,gBAAgB,OAAuC;CAC3E,iCAAiC,MAAM,QAAQ,QAAQ;CACvD,MAAM,UAAU,MAAM,cAAc,KAAK;CACzC,MAAM,MAAM,OAAO,OAAO,CAAC,CAAC;CAC5B,UAAU,IAAI,KAAK,EAAE,QAAQ,CAAC;CAC9B,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,gBAAgB,OAAmB,QAAiC;CAClF,OAAO,kBAAkB,OAAO,QAAQ,UAAU,QAAQ;AAC5D;;;;;;;;;;AAWA,SAAgB,gBAAgB,SAAiB,QAAqC;CACpF,OAAO,kBAAkB,SAAS,QAAQ,UAAU,QAAQ;AAC9D;;;;;;;AAQA,SAAgB,oBAAoB,KAAqC;CACvE,MAAM,eAAe,UAAU,IAAI,GAAG;;CAEtC,IAAI,iBAAiB,KAAA,GAAW,MAAM,IAAI,MAAM,oBAAoB;CACpE,OAAO,aAAa;AACtB;AAEA,eAAe,cAAc,OAAiD;CAC5E,MAAM,OAAO,MAAM,OAAO,OAAO,UAC/B,OACA,OACA,QACA,OACA,CAAC,WAAW,CACd;CACA,OAAO,OAAO,OAAO,UACnB;EAAE,MAAM;EAAQ,MAAM;EAAW,sBAAM,IAAI,WAAW;EAAG,MAAM;CAAS,GACxE,MACA;EAAE,MAAM;EAAQ,MAAM;EAAW,QAAQ;CAAI,GAC7C,OACA,CAAC,MAAM,CACT;AACF;;;;;;;;;;;;;;;;;;;AC1BA,SAAgB,eACd,OACA,MACoB;CACpB,cAAc,KAAK;CACnB,cAAc,OAAO,KAAK,mBAAmB;CAE7C,IAAI,OAAO,KAAK,OAAO,YAAY,KAAK,GAAG,KAAK,MAAM,IACpD,MAAM,IAAI,SACR,qBACA,kEACF;CAGF,MAAM,UAAU,oBAAoB,KAAK,GAAG;CAC5C,MAAM,SAAwB,GAAG,MAAM;CACvC,MAAM,OAAO,YAAY,MAAM;CAC/B,MAAM,SAAS,sBAAsB,QAAQ,OAAO,KAAK,IAAI,OAAO;CAEpE,OAAO;EACL,QAAQ,OAAO;EACf,IAAI,KAAK;EACT,OAAO,KAAK;EACZ,WAAW,KAAK;EAChB,oBAAoB,KAAK,aAAa,OAAO,OAAO,cAAc,CAAC;EACnE,aAAa,KAAK;CACpB;AACF"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
|
|
2
|
+
import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, t as Id } from "./types-g7CiQDyE.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/codecs/digest/key.d.ts
|
|
5
|
+
/** Wire encoding for digest operator key material (not Crockford base32). */
|
|
6
|
+
type DigestKeyFormat = "hex" | "base64url";
|
|
7
|
+
declare const digestKeyBrand: unique symbol;
|
|
8
|
+
/**
|
|
9
|
+
* Opaque imported handle for one operator Digest key.
|
|
10
|
+
*
|
|
11
|
+
* Holds a single HMAC-SHA-256 key derived via HKDF under the domain-separation
|
|
12
|
+
* label `@smonn/ids/digest/hmac`. The underlying `webcrypto.CryptoKey` is held internally and
|
|
13
|
+
* never exposed to callers. Obtain handles via {@link importDigestKey} and pass
|
|
14
|
+
* them to `createDigestId` as the `key` option.
|
|
15
|
+
*
|
|
16
|
+
* Unlike the other keyed codecs, the Digest codec holds exactly one key — there
|
|
17
|
+
* is no keyring. Re-keying is a deliberate, breaking operator action (every ID
|
|
18
|
+
* changes), never an in-band rotation.
|
|
19
|
+
*
|
|
20
|
+
* Distinct from the **Opaque key**, **Wrapping key**, and **Signing key** — the
|
|
21
|
+
* same raw bytes imported as a `DigestKey` are cryptographically independent of
|
|
22
|
+
* any other codec's key.
|
|
23
|
+
*/
|
|
24
|
+
type DigestKey = {
|
|
25
|
+
readonly [digestKeyBrand]: "DigestKey";
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Import raw operator key material into a {@link DigestKey} handle.
|
|
29
|
+
*
|
|
30
|
+
* Derives a single HMAC-SHA-256 key via HKDF under the domain-separation label
|
|
31
|
+
* `@smonn/ids/digest/hmac`. Accepts 16, 24, or 32 bytes. To store or transport key
|
|
32
|
+
* material, use {@link encodeDigestKey} / {@link decodeDigestKey}
|
|
33
|
+
* (`"hex"` or `"base64url"` — not Crockford base32).
|
|
34
|
+
*
|
|
35
|
+
* @param bytes - 16, 24, or 32 raw key bytes.
|
|
36
|
+
* @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
|
|
37
|
+
*/
|
|
38
|
+
declare function importDigestKey(bytes: Uint8Array): Promise<DigestKey>;
|
|
39
|
+
/**
|
|
40
|
+
* Encode raw digest operator key material for storage in env vars or secret managers.
|
|
41
|
+
*
|
|
42
|
+
* Supports `"hex"` (lowercase) and `"base64url"`. Output round-trips through
|
|
43
|
+
* {@link decodeDigestKey} back to the original bytes.
|
|
44
|
+
*
|
|
45
|
+
* @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
|
|
46
|
+
* @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
|
|
47
|
+
*/
|
|
48
|
+
declare function encodeDigestKey(bytes: Uint8Array, format: DigestKeyFormat): string;
|
|
49
|
+
/**
|
|
50
|
+
* Decode key material emitted by {@link encodeDigestKey} back to raw bytes.
|
|
51
|
+
*
|
|
52
|
+
* The result can be passed directly to {@link importDigestKey}.
|
|
53
|
+
*
|
|
54
|
+
* @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
|
|
55
|
+
* @throws {IdsError} `invalid_key_encoding` if the string is malformed for its format.
|
|
56
|
+
* @throws {IdsError} `invalid_key_length` if the decoded bytes are not 16, 24, or 32 bytes.
|
|
57
|
+
*/
|
|
58
|
+
declare function decodeDigestKey(encoded: string, format: DigestKeyFormat): Uint8Array;
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/codecs/digest/index.d.ts
|
|
61
|
+
/**
|
|
62
|
+
* Configuration options for a Digest codec instance.
|
|
63
|
+
*/
|
|
64
|
+
type DigestOptions = {
|
|
65
|
+
/**
|
|
66
|
+
* Non-secret, required namespace. The same material under a different
|
|
67
|
+
* `ns` yields a different ID, so one key can serve multiple unlinkable namespaces.
|
|
68
|
+
* Must be non-empty and not whitespace-only.
|
|
69
|
+
*/
|
|
70
|
+
ns: string;
|
|
71
|
+
/**
|
|
72
|
+
* Single operator digest key. The Digest codec holds exactly one key — there
|
|
73
|
+
* is no keyring. Re-keying is a deliberate, breaking operator action.
|
|
74
|
+
*/
|
|
75
|
+
key: DigestKey; /** If true, silences the duplicate-brand warning in non-production environments. */
|
|
76
|
+
allowDuplicateBrand?: boolean;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Codec returned by {@link createDigestId}.
|
|
80
|
+
*
|
|
81
|
+
* Maps caller **material** to a stable public ID under one **Digest key**:
|
|
82
|
+
* the same material always yields the same ID, and the material cannot be
|
|
83
|
+
* recovered from the ID (**equality leakage** is the intended property).
|
|
84
|
+
*
|
|
85
|
+
* - `digest` is async (WebCrypto HMAC).
|
|
86
|
+
* - `is`, `parse`, `safeParse`, `toJsonSchema`, and `~standard` are synchronous
|
|
87
|
+
* and require no key material — they validate prefix and base32 shape only.
|
|
88
|
+
* - There is no reverse method (`unwrap`, `verify`, `extractTimestamp`) — the
|
|
89
|
+
* codec is one-way by definition.
|
|
90
|
+
*/
|
|
91
|
+
type DigestCodec<Brand extends string> = {
|
|
92
|
+
/**
|
|
93
|
+
* Digest `material` into a stable canonical {@link Id}.
|
|
94
|
+
*
|
|
95
|
+
* The same `(brand, ns, key, material)` tuple always returns the same ID.
|
|
96
|
+
* Strings are UTF-8 encoded; byte arrays are used as-is.
|
|
97
|
+
*/
|
|
98
|
+
digest(material: string | Uint8Array): Promise<Id<Brand>>; /** Strict type guard: `true` only for already-canonical `Id<Brand>` strings. */
|
|
99
|
+
is(value: unknown): value is Id<Brand>; /** Normalise to canonical form, or throw on parse failure. */
|
|
100
|
+
parse(value: unknown): Id<Brand>; /** Normalise to canonical form, or return `{ ok: false, error }`. */
|
|
101
|
+
safeParse(value: unknown): ParseResult<Brand>; /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */
|
|
102
|
+
toJsonSchema(): JsonSchema; /** Standard Schema validate entry point. */
|
|
103
|
+
readonly "~standard": StandardSchemaProps<Brand>;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Construct a {@link DigestCodec} for `brand`.
|
|
107
|
+
*
|
|
108
|
+
* `opts.ns` is the required namespace — the same material under a
|
|
109
|
+
* different `ns` yields a different ID. `opts.key` is the single operator
|
|
110
|
+
* Digest key; there is no keyring.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* const key = await importDigestKey(new Uint8Array(32));
|
|
115
|
+
* const idk = createDigestId("idk", { ns: "checkout", key });
|
|
116
|
+
*
|
|
117
|
+
* const id = await idk.digest("order-123"); // Id<"idk">
|
|
118
|
+
* idk.is(id); // true
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
declare function createDigestId<Brand extends string>(brand: Brand, opts: DigestOptions): DigestCodec<Brand>;
|
|
122
|
+
//#endregion
|
|
123
|
+
export { DigestCodec, type DigestKey, type DigestKeyFormat, DigestOptions, IdsError, type IdsErrorCode, createDigestId, decodeDigestKey, encodeDigestKey, importDigestKey, isIdsError };
|
|
124
|
+
//# sourceMappingURL=digest.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest.d.mts","names":[],"sources":["../src/codecs/digest/key.ts","../src/codecs/digest/index.ts"],"mappings":";;;;;KAQY,eAAA;AAAA,cAIE,cAAA;AAJd;;;;AAAY;AAA0B;;;;AAIxB;AAkBd;;;;AACY;AAoBZ;AA3CA,KAsBY,SAAA;EAAA,UACA,cAAA;AAAA;;;;;;;;;;AAoBsD;AAiBlE;iBAjBsB,eAAA,CAAgB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,SAAA;;;;;;;;AAiBP;AAa3D;iBAbgB,eAAA,CAAgB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,eAAA;;;;;;;;AAagB;;iBAA3D,eAAA,CAAgB,OAAA,UAAiB,MAAA,EAAQ,eAAA,GAAkB,UAAA;;;;;AAzE/D;KCcA,aAAA;EDVE;;;AAAA;AAkBd;ECFE,EAAA;;;ADGU;AAoBZ;EClBE,GAAA,EAAK,SAAA;EAEL,mBAAA;AAAA;;;;;;;;;ADgBgE;AAiBlE;;;;KCjBY,WAAA;;;;;ADiB+C;AAa3D;ECvBE,MAAA,CAAO,QAAA,WAAmB,UAAA,GAAa,OAAA,CAAQ,EAAA,CAAG,KAAA;EAElD,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;EAEhC,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;EAE1B,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA,GDiBkC;ECfzE,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;AA9C5C;;;;;;;;;AAaE;AAgBF;;;;;;AA7BA,iBAiEgB,cAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,EAAM,aAAA,GACL,WAAA,CAAY,KAAA"}
|
package/dist/digest.mjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
|
|
2
|
+
import { i as importDigestKey, n as decodeDigestKey, r as encodeDigestKey, t as createDigestId } from "./digest-CknNw2wa.mjs";
|
|
3
|
+
export { IdsError, createDigestId, decodeDigestKey, encodeDigestKey, importDigestKey, isIdsError };
|
package/dist/drizzle.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-
|
|
1
|
+
import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
|
|
2
2
|
import { t as Id } from "./types-g7CiQDyE.mjs";
|
|
3
|
-
import { n as IdColumnCodec } from "./adapter-types-
|
|
3
|
+
import { n as IdColumnCodec } from "./adapter-types-CdYJM6Sf.mjs";
|
|
4
4
|
import { ConvertCustomConfig, PgCustomColumnBuilder } from "drizzle-orm/pg-core";
|
|
5
5
|
|
|
6
|
-
//#region src/drizzle.d.ts
|
|
6
|
+
//#region src/adapters/drizzle.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value.
|
|
9
9
|
*
|
package/dist/drizzle.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drizzle.d.mts","names":[],"sources":["../src/drizzle.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmCgB,QAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,IACpB,qBAAA,CAAsB,mBAAA;EAA0B,IAAA,EAAM,EAAA,CAAG,KAAA;EAAQ,UAAA;AAAA"}
|
|
1
|
+
{"version":3,"file":"drizzle.d.mts","names":[],"sources":["../src/adapters/drizzle.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmCgB,QAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,IACpB,qBAAA,CAAsB,mBAAA;EAA0B,IAAA,EAAM,EAAA,CAAG,KAAA;EAAQ,UAAA;AAAA"}
|
package/dist/drizzle.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
|
|
2
|
-
import { t as readIdColumn } from "./adapter-types-
|
|
2
|
+
import { t as readIdColumn } from "./adapter-types-7wWdELSh.mjs";
|
|
3
3
|
import { customType } from "drizzle-orm/pg-core";
|
|
4
|
-
//#region src/drizzle.ts
|
|
4
|
+
//#region src/adapters/drizzle.ts
|
|
5
5
|
/**
|
|
6
6
|
* Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value.
|
|
7
7
|
*
|
package/dist/drizzle.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drizzle.mjs","names":[],"sources":["../src/drizzle.ts"],"sourcesContent":["import {\n customType,\n type ConvertCustomConfig,\n type PgCustomColumnBuilder,\n} from \"drizzle-orm/pg-core\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"
|
|
1
|
+
{"version":3,"file":"drizzle.mjs","names":[],"sources":["../src/adapters/drizzle.ts"],"sourcesContent":["import {\n customType,\n type ConvertCustomConfig,\n type PgCustomColumnBuilder,\n} from \"drizzle-orm/pg-core\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\nimport { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode };\n\nexport type { IdColumnCodec };\n\n/**\n * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value.\n *\n * **Write path:** passes the `Id<Brand>` directly to the driver — it is already\n * the canonical string form.\n *\n * **Read path:** normalises the raw DB string via `codec.safeParse()`, not strict\n * `is()`. Data at rest should already be canonical per ADR-0003, but `safeParse`\n * is a safe boundary in case stale non-canonical values exist. Throws if the\n * value from the database does not parse as a valid `Id<Brand>`.\n *\n * @example\n * ```ts\n * import { idColumn } from \"@smonn/ids/drizzle\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * export const users = pgTable(\"users\", { id: idColumn(usr).primaryKey() });\n * // users.id is Id<\"usr\"> end-to-end\n * ```\n */\nexport function idColumn<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): PgCustomColumnBuilder<ConvertCustomConfig<\"\", { data: Id<Brand>; driverData: string }>> {\n return customType<{ data: Id<Brand>; driverData: string }>({\n dataType() {\n return \"text\";\n },\n toDriver(value: Id<Brand>): string {\n return value;\n },\n fromDriver(value: string): Id<Brand> {\n return readIdColumn(codec, value);\n },\n })();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,SACd,OACyF;CACzF,OAAO,WAAoD;EACzD,WAAW;GACT,OAAO;EACT;EACA,SAAS,OAA0B;GACjC,OAAO;EACT;EACA,WAAW,OAA0B;GACnC,OAAO,aAAa,OAAO,KAAK;EAClC;CACF,CAAC,CAAC,CAAC;AACL"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-Cp5qYZcv.mjs","names":[],"sources":["../src/error.ts"],"sourcesContent":["const BRAND = Symbol.for(\"@smonn/ids/IdsError\");\n\n/**\n * The stable machine-readable failure reason carried by `IdsError`.\n * Use `code` — not `message` — for programmatic branching; `message` is non-contractual.\n * Adding a new member is minor-additive; renaming or removing one is breaking.\n */\nexport type IdsErrorCode =\n | \"invalid_brand\"\n | \"invalid_key_format\"\n | \"invalid_key_encoding\"\n | \"invalid_key_length\"\n | \"invalid_kind\"\n | \"empty_keyring\"\n | \"duplicate_keyring_entry\"\n | \"invalid_lookup_key\"\n | \"verification_failed\"\n | \"invalid_id\";\n\n/**\n * The single error class thrown by caller-reachable public failures.\n * Carries a stable `readonly code: IdsErrorCode` for programmatic discrimination.\n * Recognized via `isIdsError()` — a branded guard that survives realm/dual-package duplication\n * where bare `instanceof` would silently fail.\n *\n * @example\n * ```ts\n * try {\n * usr.parse(rawInput);\n * } catch (err) {\n * if (isIdsError(err) && err.code === \"invalid_id\") return; // handle parse failure\n * }\n * ```\n */\nexport class IdsError extends Error {\n readonly code: IdsErrorCode;\n\n constructor(code: IdsErrorCode, message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"IdsError\";\n this.code = code;\n Object.defineProperty(this, BRAND, {\n value: true,\n enumerable: false,\n configurable: false,\n writable: false,\n });\n }\n}\n\n/**\n * Type guard for `IdsError`. Checks a non-enumerable brand rather than bare `instanceof`\n * so it survives realm/dual-package duplication (ESM + CJS dual package hazard).\n *\n * @example\n * ```ts\n * if (isIdsError(err)) {\n * switch (err.code) {\n * case \"verification_failed\": // ...\n * case \"invalid_id\": // ...\n * }\n * }\n * ```\n */\nexport function isIdsError(value: unknown): value is IdsError {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Record<symbol, unknown>)[BRAND] === true\n );\n}\n"],"mappings":";AAAA,MAAM,QAAQ,OAAO,IAAI,qBAAqB;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"error-Cp5qYZcv.mjs","names":[],"sources":["../src/error.ts"],"sourcesContent":["const BRAND = Symbol.for(\"@smonn/ids/IdsError\");\n\n/**\n * The stable machine-readable failure reason carried by `IdsError`.\n * Use `code` — not `message` — for programmatic branching; `message` is non-contractual.\n * Adding a new member is minor-additive; renaming or removing one is breaking.\n */\nexport type IdsErrorCode =\n | \"invalid_brand\"\n | \"invalid_key_format\"\n | \"invalid_key_encoding\"\n | \"invalid_key_length\"\n | \"invalid_kind\"\n | \"empty_keyring\"\n | \"duplicate_keyring_entry\"\n | \"invalid_lookup_key\"\n | \"verification_failed\"\n | \"invalid_id\"\n | \"invalid_namespace\";\n\n/**\n * The single error class thrown by caller-reachable public failures.\n * Carries a stable `readonly code: IdsErrorCode` for programmatic discrimination.\n * Recognized via `isIdsError()` — a branded guard that survives realm/dual-package duplication\n * where bare `instanceof` would silently fail.\n *\n * @example\n * ```ts\n * try {\n * usr.parse(rawInput);\n * } catch (err) {\n * if (isIdsError(err) && err.code === \"invalid_id\") return; // handle parse failure\n * }\n * ```\n */\nexport class IdsError extends Error {\n readonly code: IdsErrorCode;\n\n constructor(code: IdsErrorCode, message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"IdsError\";\n this.code = code;\n Object.defineProperty(this, BRAND, {\n value: true,\n enumerable: false,\n configurable: false,\n writable: false,\n });\n }\n}\n\n/**\n * Type guard for `IdsError`. Checks a non-enumerable brand rather than bare `instanceof`\n * so it survives realm/dual-package duplication (ESM + CJS dual package hazard).\n *\n * @example\n * ```ts\n * if (isIdsError(err)) {\n * switch (err.code) {\n * case \"verification_failed\": // ...\n * case \"invalid_id\": // ...\n * }\n * }\n * ```\n */\nexport function isIdsError(value: unknown): value is IdsError {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Record<symbol, unknown>)[BRAND] === true\n );\n}\n"],"mappings":";AAAA,MAAM,QAAQ,OAAO,IAAI,qBAAqB;;;;;;;;;;;;;;;;AAmC9C,IAAa,WAAb,cAA8B,MAAM;CAClC;CAEA,YAAY,MAAoB,SAAiB,SAAwB;EACvE,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,OAAO,eAAe,MAAM,OAAO;GACjC,OAAO;GACP,YAAY;GACZ,cAAc;GACd,UAAU;EACZ,CAAC;CACH;AACF;;;;;;;;;;;;;;;AAgBA,SAAgB,WAAW,OAAmC;CAC5D,OACE,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,WAAW;AAElD"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Use `code` — not `message` — for programmatic branching; `message` is non-contractual.
|
|
5
5
|
* Adding a new member is minor-additive; renaming or removing one is breaking.
|
|
6
6
|
*/
|
|
7
|
-
type IdsErrorCode = "invalid_brand" | "invalid_key_format" | "invalid_key_encoding" | "invalid_key_length" | "invalid_kind" | "empty_keyring" | "duplicate_keyring_entry" | "invalid_lookup_key" | "verification_failed" | "invalid_id";
|
|
7
|
+
type IdsErrorCode = "invalid_brand" | "invalid_key_format" | "invalid_key_encoding" | "invalid_key_length" | "invalid_kind" | "empty_keyring" | "duplicate_keyring_entry" | "invalid_lookup_key" | "verification_failed" | "invalid_id" | "invalid_namespace";
|
|
8
8
|
/**
|
|
9
9
|
* The single error class thrown by caller-reachable public failures.
|
|
10
10
|
* Carries a stable `readonly code: IdsErrorCode` for programmatic discrimination.
|
|
@@ -41,4 +41,4 @@ declare class IdsError extends Error {
|
|
|
41
41
|
declare function isIdsError(value: unknown): value is IdsError;
|
|
42
42
|
//#endregion
|
|
43
43
|
export { IdsErrorCode as n, isIdsError as r, IdsError as t };
|
|
44
|
-
//# sourceMappingURL=error-
|
|
44
|
+
//# sourceMappingURL=error-JIPylU_E.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-
|
|
1
|
+
{"version":3,"file":"error-JIPylU_E.d.mts","names":[],"sources":["../src/error.ts"],"mappings":";;AAOA;;;;KAAY,YAAA;AA4BZ;;;;;;;;;;;;;;;AAAA,cAAa,QAAA,SAAiB,KAAA;EAAA,SACnB,IAAA,EAAM,YAAA;EAEf,WAAA,CAAY,IAAA,EAAM,YAAA,EAAc,OAAA,UAAiB,OAAA,GAAU,YAAA;AAAA;AAAA;AA2B7D;;;;;;;;AAAqD;;;;;AA3BQ,iBA2B7C,UAAA,CAAW,KAAA,YAAiB,KAAA,IAAS,QAAA"}
|
package/dist/express.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as Id } from "./types-g7CiQDyE.mjs";
|
|
2
|
-
import { r as IdParamFailure, t as IdCodec } from "./adapter-types-
|
|
2
|
+
import { r as IdParamFailure, t as IdCodec } from "./adapter-types-CdYJM6Sf.mjs";
|
|
3
3
|
import { NextFunction, Request, Response } from "express";
|
|
4
4
|
|
|
5
|
-
//#region src/express.d.ts
|
|
5
|
+
//#region src/adapters/express.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Typed error forwarded to Express's error pipeline (`next(err)`) on validation failure.
|
|
8
8
|
* Inspect `err.reason` and `err.status` in error-handling middleware.
|
package/dist/express.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.d.mts","names":[],"sources":["../src/express.ts"],"mappings":";;;;;;AAUA;;;cAAa,YAAA,SAAqB,KAAA;EAAA,SACvB,MAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,MAAA;AAAA;;KAS1C,cAAA;EAT0C;AAAA;AAStD;;EAKE,OAAA,IAAW,OAAA,EAAS,cAAA,EAAgB,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA;;;;;EAKvE,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;;AAAA;AAmDtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAI6E;iBAJ7D,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IACR,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,UAAkB,MAAA,CAAO,QAAA,EAAU,EAAA,CAAG,KAAA,KAAU,IAAA,EAAM,YAAA"}
|
|
1
|
+
{"version":3,"file":"express.d.mts","names":[],"sources":["../src/adapters/express.ts"],"mappings":";;;;;;AAUA;;;cAAa,YAAA,SAAqB,KAAA;EAAA,SACvB,MAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,MAAA;AAAA;;KAS1C,cAAA;EAT0C;AAAA;AAStD;;EAKE,OAAA,IAAW,OAAA,EAAS,cAAA,EAAgB,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA;;;;;EAKvE,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;;AAAA;AAmDtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAI6E;iBAJ7D,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IACR,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,UAAkB,MAAA,CAAO,QAAA,EAAU,EAAA,CAAG,KAAA,KAAU,IAAA,EAAM,YAAA"}
|
package/dist/express.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as resolveIdParamFailure } from "./adapter-types-
|
|
2
|
-
//#region src/express.ts
|
|
1
|
+
import { n as resolveIdParamFailure } from "./adapter-types-7wWdELSh.mjs";
|
|
2
|
+
//#region src/adapters/express.ts
|
|
3
3
|
/**
|
|
4
4
|
* Typed error forwarded to Express's error pipeline (`next(err)`) on validation failure.
|
|
5
5
|
* Inspect `err.reason` and `err.status` in error-handling middleware.
|
package/dist/express.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.mjs","names":[],"sources":["../src/express.ts"],"sourcesContent":["import type { NextFunction, Request, Response } from \"express\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"
|
|
1
|
+
{"version":3,"file":"express.mjs","names":[],"sources":["../src/adapters/express.ts"],"sourcesContent":["import type { NextFunction, Request, Response } from \"express\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdParamFailure };\n\n/**\n * Typed error forwarded to Express's error pipeline (`next(err)`) on validation failure.\n * Inspect `err.reason` and `err.status` in error-handling middleware.\n */\nexport class IdParamError extends Error {\n readonly status: number;\n readonly reason: \"brand_mismatch\" | \"malformed\";\n\n constructor(reason: \"brand_mismatch\" | \"malformed\", status: number) {\n super(`ID validation failed: ${reason}`);\n this.name = \"IdParamError\";\n this.reason = reason;\n this.status = status;\n }\n}\n\n/** Options for `idParam`. All fields are optional. */\nexport type IdParamOptions = {\n /**\n * Called instead of forwarding to `next(err)` when provided. The hook owns the response\n * entirely — the adapter does not call `next(err)` itself.\n */\n onError?: (failure: IdParamFailure, req: Request, res: Response, next: NextFunction) => void;\n /**\n * Remap the default HTTP status for a failure reason without a full handler.\n * e.g. `{ brand_mismatch: 400 }` treats both failure kinds as 400.\n */\n status?: { brand_mismatch?: number; malformed?: number };\n};\n\n/**\n * Express middleware that validates a named route param against a codec via `safeParse`.\n *\n * **Default (no options):** calls `next(err)` with an `IdParamError` carrying `status` and `reason`,\n * so the app's existing error-handling middleware controls rendering. The adapter does not write\n * a response body itself.\n *\n * **`options.onError`:** when provided, the hook owns the response entirely — the adapter does\n * not call `next(err)`.\n *\n * **`options.status`:** remaps the default HTTP status for a reason without a full handler.\n *\n * - **Brand mismatch (`invalid_prefix`) → `reason: \"brand_mismatch\"`, default 404**\n * - **Malformed or missing ID → `reason: \"malformed\"`, default 400**\n *\n * On success, stores the canonical `Id<Brand>` in `res.locals` under `paramName`\n * and calls `next()`.\n *\n * @example\n * ```ts\n * import { idParam, IdParamError } from \"@smonn/ids/express\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: forwards error to app error-handling middleware\n * app.get(\"/users/:id\", idParam(\"id\", usr), (req, res) => {\n * const id = res.locals.id; // Id<\"usr\">, canonical\n * });\n *\n * // Error-handling middleware receives the typed error\n * app.use((err, req, res, next) => {\n * if (err instanceof IdParamError) {\n * res.status(err.status).json({ error: err.reason });\n * return;\n * }\n * next(err);\n * });\n *\n * // Override: consumer fully owns the response\n * app.get(\"/orgs/:id\", idParam(\"id\", org, {\n * onError: (failure, req, res) => res.status(failure.status).json({ error: failure.reason }),\n * }), handler);\n *\n * // Or a lightweight status remap without a full handler\n * app.get(\"/things/:id\", idParam(\"id\", thing, { status: { brand_mismatch: 400 } }), handler);\n * ```\n */\nexport function idParam<ParamKey extends string, Brand extends string>(\n paramName: ParamKey,\n codec: IdCodec<Brand>,\n options?: IdParamOptions,\n): (req: Request, res: Response<unknown, Record<ParamKey, Id<Brand>>>, next: NextFunction) => void {\n return (req, res, next): void => {\n const raw = req.params[paramName];\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n options.onError(failure, req, res, next);\n return;\n }\n next(new IdParamError(failure.reason, failure.status));\n return;\n }\n (res.locals as Record<string, unknown>)[paramName] = result.id;\n next();\n };\n}\n"],"mappings":";;;;;;AAUA,IAAa,eAAb,cAAkC,MAAM;CACtC;CACA;CAEA,YAAY,QAAwC,QAAgB;EAClE,MAAM,yBAAyB,QAAQ;EACvC,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,SAAS;CAChB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA,SAAgB,QACd,WACA,OACA,SACiG;CACjG,QAAQ,KAAK,KAAK,SAAe;EAC/B,MAAM,MAAM,IAAI,OAAO;EACvB,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SAAS;IACpB,QAAQ,QAAQ,SAAS,KAAK,KAAK,IAAI;IACvC;GACF;GACA,KAAK,IAAI,aAAa,QAAQ,QAAQ,QAAQ,MAAM,CAAC;GACrD;EACF;EACA,IAAK,OAAmC,aAAa,OAAO;EAC5D,KAAK;CACP;AACF"}
|
package/dist/fastify.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as Id } from "./types-g7CiQDyE.mjs";
|
|
2
|
-
import { r as IdParamFailure, t as IdCodec } from "./adapter-types-
|
|
2
|
+
import { r as IdParamFailure, t as IdCodec } from "./adapter-types-CdYJM6Sf.mjs";
|
|
3
3
|
import { FastifyReply, FastifyRequest } from "fastify";
|
|
4
4
|
|
|
5
|
-
//#region src/fastify.d.ts
|
|
5
|
+
//#region src/adapters/fastify.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Typed error thrown into Fastify's `setErrorHandler` on validation failure.
|
|
8
8
|
* Inspect `err.reason` and `err.statusCode` in your error handler.
|
|
@@ -60,7 +60,7 @@ type IdParamOptions = {
|
|
|
60
60
|
*
|
|
61
61
|
* // Default: throws IdParamError → setErrorHandler renders it
|
|
62
62
|
* fastify.get("/users/:id", { preHandler: idParam("id", usr) }, (request, reply) => {
|
|
63
|
-
* const id = request.params.id; // Id<"usr"
|
|
63
|
+
* const id = request.params.id; // string (compile-time); Id<"usr"> at runtime after preHandler
|
|
64
64
|
* });
|
|
65
65
|
*
|
|
66
66
|
* // Error handler receives the typed error
|
package/dist/fastify.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fastify.d.mts","names":[],"sources":["../src/fastify.ts"],"mappings":";;;;;;AAUA;;;cAAa,YAAA,SAAqB,KAAA;EAAA,SACvB,UAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,UAAA;AAAA;;KAS1C,cAAA;EAT0C;AAAA;AAStD;;EAKE,OAAA,IACE,OAAA,EAAS,cAAA,EACT,OAAA,EAAS,cAAA,EACT,KAAA,EAAO,YAAA,YACG,OAAA;;;;;EAKZ,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;AAAA;AA6DtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAOK;;;;;;;;;;;;;;iBAPW,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IAEV,OAAA,EAAS,cAAA;EAAiB,MAAA,EAAQ,MAAA,SAAe,EAAA,CAAG,KAAA;AAAA,IACpD,KAAA,EAAO,YAAA,KACJ,OAAA"}
|
|
1
|
+
{"version":3,"file":"fastify.d.mts","names":[],"sources":["../src/adapters/fastify.ts"],"mappings":";;;;;;AAUA;;;cAAa,YAAA,SAAqB,KAAA;EAAA,SACvB,UAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,UAAA;AAAA;;KAS1C,cAAA;EAT0C;AAAA;AAStD;;EAKE,OAAA,IACE,OAAA,EAAS,cAAA,EACT,OAAA,EAAS,cAAA,EACT,KAAA,EAAO,YAAA,YACG,OAAA;;;;;EAKZ,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;AAAA;AA6DtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAOK;;;;;;;;;;;;;;iBAPW,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IAEV,OAAA,EAAS,cAAA;EAAiB,MAAA,EAAQ,MAAA,SAAe,EAAA,CAAG,KAAA;AAAA,IACpD,KAAA,EAAO,YAAA,KACJ,OAAA"}
|
package/dist/fastify.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as resolveIdParamFailure } from "./adapter-types-
|
|
2
|
-
//#region src/fastify.ts
|
|
1
|
+
import { n as resolveIdParamFailure } from "./adapter-types-7wWdELSh.mjs";
|
|
2
|
+
//#region src/adapters/fastify.ts
|
|
3
3
|
/**
|
|
4
4
|
* Typed error thrown into Fastify's `setErrorHandler` on validation failure.
|
|
5
5
|
* Inspect `err.reason` and `err.statusCode` in your error handler.
|
|
@@ -46,7 +46,7 @@ var IdParamError = class extends Error {
|
|
|
46
46
|
*
|
|
47
47
|
* // Default: throws IdParamError → setErrorHandler renders it
|
|
48
48
|
* fastify.get("/users/:id", { preHandler: idParam("id", usr) }, (request, reply) => {
|
|
49
|
-
* const id = request.params.id; // Id<"usr"
|
|
49
|
+
* const id = request.params.id; // string (compile-time); Id<"usr"> at runtime after preHandler
|
|
50
50
|
* });
|
|
51
51
|
*
|
|
52
52
|
* // Error handler receives the typed error
|
package/dist/fastify.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fastify.mjs","names":[],"sources":["../src/fastify.ts"],"sourcesContent":["import type { FastifyReply, FastifyRequest } from \"fastify\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"
|
|
1
|
+
{"version":3,"file":"fastify.mjs","names":[],"sources":["../src/adapters/fastify.ts"],"sourcesContent":["import type { FastifyReply, FastifyRequest } from \"fastify\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdParamFailure };\n\n/**\n * Typed error thrown into Fastify's `setErrorHandler` on validation failure.\n * Inspect `err.reason` and `err.statusCode` in your error handler.\n */\nexport class IdParamError extends Error {\n readonly statusCode: number;\n readonly reason: \"brand_mismatch\" | \"malformed\";\n\n constructor(reason: \"brand_mismatch\" | \"malformed\", statusCode: number) {\n super(`ID validation failed: ${reason}`);\n this.name = \"IdParamError\";\n this.reason = reason;\n this.statusCode = statusCode;\n }\n}\n\n/** Options for `idParam`. All fields are optional. */\nexport type IdParamOptions = {\n /**\n * Called instead of throwing when provided. The hook owns the response entirely —\n * the adapter does not throw.\n */\n onError?: (\n failure: IdParamFailure,\n request: FastifyRequest,\n reply: FastifyReply,\n ) => void | Promise<void>;\n /**\n * Remap the default HTTP status for a failure reason without a full handler.\n * e.g. `{ brand_mismatch: 400 }` treats both failure kinds as 400.\n */\n status?: { brand_mismatch?: number; malformed?: number };\n};\n\n/**\n * Fastify `preHandler` hook factory that validates a named route param against a codec via `safeParse`.\n *\n * **Default (no options):** throws `IdParamError` carrying `statusCode` and `reason` so the app's\n * existing `setErrorHandler` controls rendering. The adapter does not write a response body itself.\n *\n * **`options.onError`:** when provided, the hook calls `onError` and does not throw; the consumer\n * fully owns the response via `reply`.\n *\n * **`options.status`:** remaps the default HTTP status for a reason without a full handler.\n *\n * - **Brand mismatch (`invalid_prefix`) → `reason: \"brand_mismatch\"`, default 404**\n * - **Malformed or missing ID → `reason: \"malformed\"`, default 400**\n *\n * On success, stores the canonical `Id<Brand>` in `request.params` under `paramName`.\n *\n * **Return type note:** the returned hook is typed as\n * `(request: FastifyRequest<{ Params: Record<string, Id<Brand>> }>, reply: FastifyReply) => Promise<void>`.\n * Assigning it to a Fastify `preHandler` slot is backward-compatible (method-signature bivariance applies).\n * However, a locally-annotated variable typed as the bare `(request: FastifyRequest, reply: FastifyReply) => Promise<void>`\n * will produce a TypeScript error under `--strictFunctionTypes` because function parameter types are contravariant.\n * Use `preHandler` assignment or let TypeScript infer the type to avoid this.\n *\n * @example\n * ```ts\n * import { idParam, IdParamError } from \"@smonn/ids/fastify\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: throws IdParamError → setErrorHandler renders it\n * fastify.get(\"/users/:id\", { preHandler: idParam(\"id\", usr) }, (request, reply) => {\n * const id = request.params.id; // string (compile-time); Id<\"usr\"> at runtime after preHandler\n * });\n *\n * // Error handler receives the typed error\n * fastify.setErrorHandler((err, request, reply) => {\n * if (err instanceof IdParamError) {\n * reply.status(err.statusCode).send({ error: err.reason });\n * return;\n * }\n * reply.send(err);\n * });\n *\n * // Override: consumer fully owns the error response\n * fastify.get(\"/orgs/:id\", {\n * preHandler: idParam(\"id\", org, {\n * onError: (failure, request, reply) =>\n * reply.status(failure.status).send({ error: failure.reason }),\n * }),\n * }, handler);\n *\n * // Or a lightweight status remap without a full handler\n * fastify.get(\"/things/:id\", {\n * preHandler: idParam(\"id\", thing, { status: { brand_mismatch: 400 } }),\n * }, handler);\n * ```\n */\nexport function idParam<ParamKey extends string, Brand extends string>(\n paramName: ParamKey,\n codec: IdCodec<Brand>,\n options?: IdParamOptions,\n): (\n request: FastifyRequest<{ Params: Record<string, Id<Brand>> }>,\n reply: FastifyReply,\n) => Promise<void> {\n return async (request, reply): Promise<void> => {\n const raw = request.params[paramName];\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n await options.onError(failure, request, reply);\n return;\n }\n throw new IdParamError(failure.reason, failure.status);\n }\n request.params[paramName] = result.id;\n };\n}\n"],"mappings":";;;;;;AAUA,IAAa,eAAb,cAAkC,MAAM;CACtC;CACA;CAEA,YAAY,QAAwC,YAAoB;EACtE,MAAM,yBAAyB,QAAQ;EACvC,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,aAAa;CACpB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8EA,SAAgB,QACd,WACA,OACA,SAIiB;CACjB,OAAO,OAAO,SAAS,UAAyB;EAC9C,MAAM,MAAM,QAAQ,OAAO;EAC3B,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SAAS;IACpB,MAAM,QAAQ,QAAQ,SAAS,SAAS,KAAK;IAC7C;GACF;GACA,MAAM,IAAI,aAAa,QAAQ,QAAQ,QAAQ,MAAM;EACvD;EACA,QAAQ,OAAO,aAAa,OAAO;CACrC;AACF"}
|
package/dist/hono.d.mts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { t as Id } from "./types-g7CiQDyE.mjs";
|
|
2
|
-
import { r as IdParamFailure, t as IdCodec } from "./adapter-types-
|
|
2
|
+
import { r as IdParamFailure, t as IdCodec } from "./adapter-types-CdYJM6Sf.mjs";
|
|
3
|
+
import { ContentfulStatusCode } from "hono/utils/http-status";
|
|
3
4
|
import { Context, MiddlewareHandler } from "hono";
|
|
4
5
|
|
|
5
|
-
//#region src/hono.d.ts
|
|
6
|
+
//#region src/adapters/hono.d.ts
|
|
6
7
|
/** Options for `idParam`. All fields are optional. */
|
|
7
8
|
type IdParamOptions = {
|
|
8
9
|
/**
|
|
@@ -15,8 +16,8 @@ type IdParamOptions = {
|
|
|
15
16
|
* e.g. `{ brand_mismatch: 400 }` treats both failure kinds as 400.
|
|
16
17
|
*/
|
|
17
18
|
status?: {
|
|
18
|
-
brand_mismatch?:
|
|
19
|
-
malformed?:
|
|
19
|
+
brand_mismatch?: ContentfulStatusCode;
|
|
20
|
+
malformed?: ContentfulStatusCode;
|
|
20
21
|
};
|
|
21
22
|
};
|
|
22
23
|
/**
|
package/dist/hono.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono.d.mts","names":[],"sources":["../src/hono.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"hono.d.mts","names":[],"sources":["../src/adapters/hono.ts"],"mappings":";;;;;;;KASY,cAAA;EAAA;;;;EAKV,OAAA,IAAW,OAAA,EAAS,cAAA,EAAgB,CAAA,EAAG,OAAA,KAAY,QAAA,GAAW,OAAA,CAAQ,QAAA;;;;;EAKtE,MAAA;IAAW,cAAA,GAAiB,oBAAA;IAAsB,SAAA,GAAY,oBAAA;EAAA;AAAA;;;;;;;;;;;;AAAA;AAyChE;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,GACT,iBAAA;EAAoB,SAAA,EAAW,MAAA,CAAO,QAAA,EAAU,EAAA,CAAG,KAAA;AAAA"}
|
package/dist/hono.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as resolveIdParamFailure } from "./adapter-types-
|
|
1
|
+
import { n as resolveIdParamFailure } from "./adapter-types-7wWdELSh.mjs";
|
|
2
2
|
import { HTTPException } from "hono/http-exception";
|
|
3
|
-
//#region src/hono.ts
|
|
3
|
+
//#region src/adapters/hono.ts
|
|
4
4
|
/**
|
|
5
5
|
* Hono middleware that validates a named route param against a codec via `safeParse`.
|
|
6
6
|
*
|
|
@@ -46,7 +46,8 @@ function idParam(paramName, codec, options) {
|
|
|
46
46
|
if (!result.ok) {
|
|
47
47
|
const failure = resolveIdParamFailure(result.error, options);
|
|
48
48
|
if (options?.onError) return options.onError(failure, c);
|
|
49
|
-
|
|
49
|
+
const defaultStatus = failure.reason === "brand_mismatch" ? 404 : 400;
|
|
50
|
+
throw new HTTPException(options?.status?.[failure.reason] ?? defaultStatus);
|
|
50
51
|
}
|
|
51
52
|
c.set(paramName, result.id);
|
|
52
53
|
await next();
|
package/dist/hono.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono.mjs","names":[],"sources":["../src/hono.ts"],"sourcesContent":["import { HTTPException } from \"hono/http-exception\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"
|
|
1
|
+
{"version":3,"file":"hono.mjs","names":[],"sources":["../src/adapters/hono.ts"],"sourcesContent":["import { HTTPException } from \"hono/http-exception\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdParamFailure };\n\n/** Options for `idParam`. All fields are optional. */\nexport type IdParamOptions = {\n /**\n * Called instead of throwing when provided. The hook owns the response entirely —\n * the adapter neither throws nor writes a body.\n */\n onError?: (failure: IdParamFailure, c: Context) => Response | Promise<Response>;\n /**\n * Remap the default HTTP status for a failure reason without a full handler.\n * e.g. `{ brand_mismatch: 400 }` treats both failure kinds as 400.\n */\n status?: { brand_mismatch?: ContentfulStatusCode; malformed?: ContentfulStatusCode };\n};\n\n/**\n * Hono middleware that validates a named route param against a codec via `safeParse`.\n *\n * **Default (no options):** throws `HTTPException(status)` so the app's existing `onError` handler\n * controls rendering and content negotiation. The adapter does not write a response body itself.\n *\n * **`options.onError`:** when provided, the hook owns the response entirely — the adapter neither\n * throws nor writes a response.\n *\n * **`options.status`:** remaps the default HTTP status for a reason without a full handler.\n *\n * - **Brand mismatch (`invalid_prefix`) → `reason: \"brand_mismatch\"`, default 404**\n * - **Malformed or missing ID → `reason: \"malformed\"`, default 400**\n *\n * On success, stores the canonical `Id<Brand>` in the Hono context under `paramName`\n * and calls `next()`.\n *\n * @example\n * ```ts\n * import { idParam } from \"@smonn/ids/hono\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: throws HTTPException → app.onError renders it\n * app.get(\"/users/:id\", idParam(\"id\", usr), (c) => {\n * const id = c.get(\"id\"); // Id<\"usr\">, canonical\n * });\n *\n * // Override: consumer fully owns the response\n * app.get(\"/orgs/:id\", idParam(\"id\", org, {\n * onError: (failure, c) => c.json({ error: failure.reason }, failure.status),\n * }), handler);\n *\n * // Or a lightweight status remap without a full handler\n * app.get(\"/things/:id\", idParam(\"id\", thing, { status: { brand_mismatch: 400 } }), handler);\n * ```\n */\nexport function idParam<ParamKey extends string, Brand extends string>(\n paramName: ParamKey,\n codec: IdCodec<Brand>,\n options?: IdParamOptions,\n): MiddlewareHandler<{ Variables: Record<ParamKey, Id<Brand>> }> {\n return async (c, next) => {\n const raw = c.req.param(paramName);\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n return options.onError(failure, c);\n }\n const defaultStatus: ContentfulStatusCode = failure.reason === \"brand_mismatch\" ? 404 : 400;\n throw new HTTPException(options?.status?.[failure.reason] ?? defaultStatus);\n }\n c.set(paramName, result.id);\n await next();\n return;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,SAAgB,QACd,WACA,OACA,SAC+D;CAC/D,OAAO,OAAO,GAAG,SAAS;EACxB,MAAM,MAAM,EAAE,IAAI,MAAM,SAAS;EACjC,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SACX,OAAO,QAAQ,QAAQ,SAAS,CAAC;GAEnC,MAAM,gBAAsC,QAAQ,WAAW,mBAAmB,MAAM;GACxF,MAAM,IAAI,cAAc,SAAS,SAAS,QAAQ,WAAW,aAAa;EAC5E;EACA,EAAE,IAAI,WAAW,OAAO,EAAE;EAC1B,MAAM,KAAK;CAEb;AACF"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-
|
|
1
|
+
import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
|
|
2
2
|
import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, r as ParseError, t as Id } from "./types-g7CiQDyE.mjs";
|
|
3
3
|
|
|
4
|
-
//#region src/timestamp.d.ts
|
|
4
|
+
//#region src/codecs/timestamp/index.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Configuration options for a codec instance.
|
|
7
7
|
*/
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/timestamp.ts"],"mappings":";;;;;;;KASY,gBAAA;EAAA,6EAEV,GAAA,iBAEe;EAAf,GAAA,IAAO,MAAA,EAAQ,UAAA;EAEf,mBAAA;AAAA;;;AAAA;AAeF;;;;;;KAAY,cAAA;uEAEV,QAAA,IAAY,EAAA,CAAG,KAAA;EAEf,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;;;;;EAK3B,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;;;;EAIhC,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;;;;EAI1B,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;;EAIvC,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,IAAA;EAEjC,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAE7B,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAE7B,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;;;iBAiD5B,iBAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,GAAM,gBAAA,GACL,cAAA,CAAe,KAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/codecs/timestamp/index.ts"],"mappings":";;;;;;;KASY,gBAAA;EAAA,6EAEV,GAAA,iBAEe;EAAf,GAAA,IAAO,MAAA,EAAQ,UAAA;EAEf,mBAAA;AAAA;;;AAAA;AAeF;;;;;;KAAY,cAAA;uEAEV,QAAA,IAAY,EAAA,CAAG,KAAA;EAEf,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;;;;;EAK3B,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;;;;EAIhC,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;;;;EAI1B,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;;EAIvC,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,IAAA;EAEjC,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAE7B,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAE7B,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;;;iBAiD5B,iBAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,GAAM,gBAAA,GACL,cAAA,CAAe,KAAA"}
|