@smonn/ids 0.7.0 → 0.8.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 +4 -0
- package/dist/fastify.d.mts +11 -2
- package/dist/fastify.d.mts.map +1 -1
- package/dist/fastify.mjs +7 -0
- package/dist/fastify.mjs.map +1 -1
- package/dist/signed.d.mts +112 -1
- package/dist/signed.d.mts.map +1 -1
- package/dist/signed.mjs +158 -1
- package/dist/signed.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -482,11 +482,15 @@ import {
|
|
|
482
482
|
importSigningKey, // (bytes: Uint8Array) => Promise<SigningKey>
|
|
483
483
|
encodeSigningKey, // (bytes: Uint8Array, format: SigningKeyFormat) => string
|
|
484
484
|
decodeSigningKey, // (encoded: string, format: SigningKeyFormat) => Uint8Array
|
|
485
|
+
createSignedTimestampId, // (brand: string, opts: SignedTimestampOptions) => SignedTimestampCodec<Brand>
|
|
485
486
|
IdsError, // re-exported from @smonn/ids/signed for convenience
|
|
486
487
|
isIdsError, // re-exported from @smonn/ids/signed for convenience
|
|
487
488
|
type SigningKey, // opaque SigningKey handle (HKDF-derived)
|
|
488
489
|
type SigningKeyFormat, // "hex" | "base64url"
|
|
489
490
|
type IdsErrorCode, // re-exported from @smonn/ids/signed for convenience
|
|
491
|
+
type SignedTimestampCodec, // returned by createSignedTimestampId
|
|
492
|
+
type SignedTimestampOptions, // { keys: SigningKey[], now?, rng?, allowDuplicateBrand? } constructor options
|
|
493
|
+
type SafeVerifyResult, // { ok: true, id: Id<Brand> } | { ok: false, error: ParseError | "verification_failed" }
|
|
490
494
|
} from "@smonn/ids/signed";
|
|
491
495
|
|
|
492
496
|
import {
|
package/dist/fastify.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as ParseResult } from "./types-g7CiQDyE.mjs";
|
|
1
|
+
import { i as ParseResult, t as Id } from "./types-g7CiQDyE.mjs";
|
|
2
2
|
import { t as IdParamFailure } from "./adapter-types-oHCCSgOO.mjs";
|
|
3
3
|
import { FastifyReply, FastifyRequest } from "fastify";
|
|
4
4
|
|
|
@@ -47,6 +47,13 @@ type IdParamOptions = {
|
|
|
47
47
|
*
|
|
48
48
|
* On success, stores the canonical `Id<Brand>` in `request.params` under `paramName`.
|
|
49
49
|
*
|
|
50
|
+
* **Return type note:** the returned hook is typed as
|
|
51
|
+
* `(request: FastifyRequest<{ Params: Record<string, Id<Brand>> }>, reply: FastifyReply) => Promise<void>`.
|
|
52
|
+
* Assigning it to a Fastify `preHandler` slot is backward-compatible (method-signature bivariance applies).
|
|
53
|
+
* However, a locally-annotated variable typed as the bare `(request: FastifyRequest, reply: FastifyReply) => Promise<void>`
|
|
54
|
+
* will produce a TypeScript error under `--strictFunctionTypes` because function parameter types are contravariant.
|
|
55
|
+
* Use `preHandler` assignment or let TypeScript infer the type to avoid this.
|
|
56
|
+
*
|
|
50
57
|
* @example
|
|
51
58
|
* ```ts
|
|
52
59
|
* import { idParam, IdParamError } from "@smonn/ids/fastify";
|
|
@@ -82,7 +89,9 @@ type IdParamOptions = {
|
|
|
82
89
|
* }, handler);
|
|
83
90
|
* ```
|
|
84
91
|
*/
|
|
85
|
-
declare function idParam<ParamKey extends string, Brand extends string>(paramName: ParamKey, codec: IdCodec<Brand>, options?: IdParamOptions): (request: FastifyRequest
|
|
92
|
+
declare function idParam<ParamKey extends string, Brand extends string>(paramName: ParamKey, codec: IdCodec<Brand>, options?: IdParamOptions): (request: FastifyRequest<{
|
|
93
|
+
Params: Record<string, Id<Brand>>;
|
|
94
|
+
}>, reply: FastifyReply) => Promise<void>;
|
|
86
95
|
//#endregion
|
|
87
96
|
export { IdParamError, type IdParamFailure, IdParamOptions, idParam };
|
|
88
97
|
//# sourceMappingURL=fastify.d.mts.map
|
package/dist/fastify.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fastify.d.mts","names":[],"sources":["../src/fastify.ts"],"mappings":";;;;;KAMK,OAAA;EACH,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;;;;;cAO5B,YAAA,SAAqB,KAAA;EAAA,SACvB,UAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,UAAA;AAAA;AAJtD;AAAA,KAaY,cAAA;;;;;EAKV,OAAA,IACE,OAAA,EAAS,cAAA,EACT,OAAA,EAAS,cAAA,EACT,KAAA,EAAO,YAAA,YACG,OAAA;;;;;EAKZ,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;;;;;;;;;AAAA;
|
|
1
|
+
{"version":3,"file":"fastify.d.mts","names":[],"sources":["../src/fastify.ts"],"mappings":";;;;;KAMK,OAAA;EACH,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;;;;;cAO5B,YAAA,SAAqB,KAAA;EAAA,SACvB,UAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,UAAA;AAAA;AAJtD;AAAA,KAaY,cAAA;;;;;EAKV,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
|
@@ -29,6 +29,13 @@ var IdParamError = class extends Error {
|
|
|
29
29
|
*
|
|
30
30
|
* On success, stores the canonical `Id<Brand>` in `request.params` under `paramName`.
|
|
31
31
|
*
|
|
32
|
+
* **Return type note:** the returned hook is typed as
|
|
33
|
+
* `(request: FastifyRequest<{ Params: Record<string, Id<Brand>> }>, reply: FastifyReply) => Promise<void>`.
|
|
34
|
+
* Assigning it to a Fastify `preHandler` slot is backward-compatible (method-signature bivariance applies).
|
|
35
|
+
* However, a locally-annotated variable typed as the bare `(request: FastifyRequest, reply: FastifyReply) => Promise<void>`
|
|
36
|
+
* will produce a TypeScript error under `--strictFunctionTypes` because function parameter types are contravariant.
|
|
37
|
+
* Use `preHandler` assignment or let TypeScript infer the type to avoid this.
|
|
38
|
+
*
|
|
32
39
|
* @example
|
|
33
40
|
* ```ts
|
|
34
41
|
* import { idParam, IdParamError } from "@smonn/ids/fastify";
|
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 { IdParamFailure } from \"./adapter-types.js\";\nimport type { Id, ParseResult } from \"./types.js\";\n\nexport type { IdParamFailure };\n\ntype IdCodec<Brand extends string> = {\n safeParse(value: unknown): ParseResult<Brand>;\n};\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 * @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; // Id<\"usr\">, canonical\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): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {\n return async (request, reply): Promise<void> => {\n const raw =
|
|
1
|
+
{"version":3,"file":"fastify.mjs","names":[],"sources":["../src/fastify.ts"],"sourcesContent":["import type { FastifyReply, FastifyRequest } from \"fastify\";\nimport type { IdParamFailure } from \"./adapter-types.js\";\nimport type { Id, ParseResult } from \"./types.js\";\n\nexport type { IdParamFailure };\n\ntype IdCodec<Brand extends string> = {\n safeParse(value: unknown): ParseResult<Brand>;\n};\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; // Id<\"usr\">, canonical\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 reason =\n result.error === \"invalid_prefix\" ? (\"brand_mismatch\" as const) : (\"malformed\" as const);\n const defaultStatus = reason === \"brand_mismatch\" ? 404 : 400;\n const status = options?.status?.[reason] ?? defaultStatus;\n const failure: IdParamFailure = { reason, status };\n if (options?.onError) {\n await options.onError(failure, request, reply);\n return;\n }\n throw new IdParamError(reason, status);\n }\n request.params[paramName] = result.id;\n };\n}\n"],"mappings":";;;;;AAcA,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,SACJ,OAAO,UAAU,mBAAoB,mBAA8B;GACrE,MAAM,gBAAgB,WAAW,mBAAmB,MAAM;GAC1D,MAAM,SAAS,SAAS,SAAS,WAAW;GAC5C,MAAM,UAA0B;IAAE;IAAQ;GAAO;GACjD,IAAI,SAAS,SAAS;IACpB,MAAM,QAAQ,QAAQ,SAAS,SAAS,KAAK;IAC7C;GACF;GACA,MAAM,IAAI,aAAa,QAAQ,MAAM;EACvC;EACA,QAAQ,OAAO,aAAa,OAAO;CACrC;AACF"}
|
package/dist/signed.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-DTr4i6Ic.mjs";
|
|
2
|
+
import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, r as ParseError, t as Id } from "./types-g7CiQDyE.mjs";
|
|
2
3
|
|
|
3
4
|
//#region src/signing-key.d.ts
|
|
4
5
|
/** Wire encoding for signing key raw key bytes (not Crockford base32). */
|
|
@@ -52,5 +53,115 @@ declare function encodeSigningKey(bytes: Uint8Array, format: SigningKeyFormat):
|
|
|
52
53
|
*/
|
|
53
54
|
declare function decodeSigningKey(encoded: string, format: SigningKeyFormat): Uint8Array;
|
|
54
55
|
//#endregion
|
|
55
|
-
|
|
56
|
+
//#region src/signed.d.ts
|
|
57
|
+
/**
|
|
58
|
+
* Configuration options for a Signed Timestamp codec instance.
|
|
59
|
+
*/
|
|
60
|
+
type SignedTimestampOptions = {
|
|
61
|
+
/**
|
|
62
|
+
* Non-empty ordered signing keyring. The first entry is current — the only one
|
|
63
|
+
* `generate` / `generateAt` sign with. `verify` / `safeVerify` trial every entry
|
|
64
|
+
* until the tag matches. Duplicate raw secrets are rejected at construction.
|
|
65
|
+
*/
|
|
66
|
+
keys: [SigningKey, ...SigningKey[]]; /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */
|
|
67
|
+
now?: () => number; /** Writes 5 random bytes into `target` for the random tail. Defaults to `crypto.getRandomValues`. */
|
|
68
|
+
rng?: (target: Uint8Array) => void; /** If true, silences the duplicate-brand warning in non-production environments. */
|
|
69
|
+
allowDuplicateBrand?: boolean;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Result returned by {@link SignedTimestampCodec.safeVerify}.
|
|
73
|
+
*
|
|
74
|
+
* On success, `id` is the canonical {@link Id}.
|
|
75
|
+
* On failure, `error` is a {@link ParseError} for structural problems or
|
|
76
|
+
* `"verification_failed"` when the HMAC tag does not match any entry in the
|
|
77
|
+
* signing keyring.
|
|
78
|
+
*/
|
|
79
|
+
type SafeVerifyResult<Brand extends string> = {
|
|
80
|
+
ok: true;
|
|
81
|
+
id: Id<Brand>;
|
|
82
|
+
} | {
|
|
83
|
+
ok: false;
|
|
84
|
+
error: ParseError | "verification_failed";
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Codec returned by {@link createSignedTimestampId}.
|
|
88
|
+
*
|
|
89
|
+
* Keeps the 6-byte millisecond timestamp **readable and sortable** like the
|
|
90
|
+
* Timestamp codec, but replaces half of the 10-byte random tail with a truncated
|
|
91
|
+
* HMAC tag, making IDs **tamper-evident and verifiable without a database lookup**.
|
|
92
|
+
*
|
|
93
|
+
* Byte layout: `ts6 ‖ rand5 ‖ tag5` where the 40-bit tag =
|
|
94
|
+
* `trunc(HMAC-SHA256(hmacKey, brand ‖ ts6 ‖ rand5), 40)`.
|
|
95
|
+
*
|
|
96
|
+
* - Async (HMAC): `generate`, `generateAt`, `verify`, `safeVerify`.
|
|
97
|
+
* - Sync (no key / plaintext timestamp): all other methods.
|
|
98
|
+
*/
|
|
99
|
+
type SignedTimestampCodec<Brand extends string> = {
|
|
100
|
+
/** Produces a canonical ID signed with the current (first) key. */generate(): Promise<Id<Brand>>;
|
|
101
|
+
/**
|
|
102
|
+
* Produces a canonical ID with timestamp from `date`, signed with the current key.
|
|
103
|
+
* Throws on invalid dates.
|
|
104
|
+
*/
|
|
105
|
+
generateAt(date: Date): Promise<Id<Brand>>;
|
|
106
|
+
/**
|
|
107
|
+
* Recomputes the HMAC tag across every keyring entry.
|
|
108
|
+
*
|
|
109
|
+
* Throws `IdsError` with `code: "verification_failed"` if no entry matches.
|
|
110
|
+
* Tamper of the brand, timestamp bytes, or random bytes all fail here.
|
|
111
|
+
*/
|
|
112
|
+
verify(id: Id<Brand>): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Non-throwing path for untrusted input.
|
|
115
|
+
*
|
|
116
|
+
* Structurally parses `input` first (same rules as {@link safeParse}), then
|
|
117
|
+
* verifies the HMAC tag. Returns `{ ok: false, error }` on any failure —
|
|
118
|
+
* {@link ParseError} for structural problems or `"verification_failed"` for tag
|
|
119
|
+
* mismatch — without throwing.
|
|
120
|
+
*/
|
|
121
|
+
safeVerify(input: unknown): Promise<SafeVerifyResult<Brand>>;
|
|
122
|
+
/**
|
|
123
|
+
* Decodes the creation `Date` from an `Id<Brand>`.
|
|
124
|
+
* Sync — the 6-byte timestamp is plaintext. Trusts the type; use `safeParse()` at boundaries first.
|
|
125
|
+
*/
|
|
126
|
+
extractTimestamp(id: Id<Brand>): Date;
|
|
127
|
+
/**
|
|
128
|
+
* Tight lower bound sentinel for range scans (`ts(t) ‖ 0x00×10`).
|
|
129
|
+
* **Not verifiable** — carries no valid tag.
|
|
130
|
+
*/
|
|
131
|
+
minIdForTime(date: Date): Id<Brand>;
|
|
132
|
+
/**
|
|
133
|
+
* Tight upper bound sentinel for range scans (`ts(t) ‖ 0xff×10`).
|
|
134
|
+
* **Not verifiable** — carries no valid tag.
|
|
135
|
+
*/
|
|
136
|
+
maxIdForTime(date: Date): Id<Brand>;
|
|
137
|
+
/**
|
|
138
|
+
* Strict type guard: `true` only for already-canonical `Id<Brand>` strings.
|
|
139
|
+
* For untrusted input, use `safeParse()` or `safeVerify()` instead.
|
|
140
|
+
*/
|
|
141
|
+
is(value: unknown): value is Id<Brand>; /** Normalise to canonical form, or throw on parse failure. */
|
|
142
|
+
parse(value: unknown): Id<Brand>; /** Normalise to canonical form, or return `{ ok: false, error }`. */
|
|
143
|
+
safeParse(value: unknown): ParseResult<Brand>; /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */
|
|
144
|
+
toJsonSchema(): JsonSchema; /** Standard Schema validate entry point. */
|
|
145
|
+
readonly "~standard": StandardSchemaProps<Brand>;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Construct a {@link SignedTimestampCodec} for `brand`.
|
|
149
|
+
*
|
|
150
|
+
* `opts.keys` is a non-empty ordered signing keyring — the first entry is current
|
|
151
|
+
* (used by `generate` / `generateAt`); all entries are tried on `verify` /
|
|
152
|
+
* `safeVerify`; duplicate operator secrets are rejected at construction.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```ts
|
|
156
|
+
* const key = await importSigningKey(new Uint8Array(32));
|
|
157
|
+
* const usr = createSignedTimestampId("usr", { keys: [key] });
|
|
158
|
+
*
|
|
159
|
+
* const id = await usr.generate(); // Id<"usr">
|
|
160
|
+
* await usr.verify(id); // passes
|
|
161
|
+
* usr.extractTimestamp(id); // Date — sync, timestamp is plaintext
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
declare function createSignedTimestampId<Brand extends string>(brand: Brand, opts: SignedTimestampOptions): SignedTimestampCodec<Brand>;
|
|
165
|
+
//#endregion
|
|
166
|
+
export { IdsError, type IdsErrorCode, SafeVerifyResult, SignedTimestampCodec, SignedTimestampOptions, type SigningKey, type SigningKeyFormat, createSignedTimestampId, decodeSigningKey, encodeSigningKey, importSigningKey, isIdsError };
|
|
56
167
|
//# sourceMappingURL=signed.d.mts.map
|
package/dist/signed.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signed.d.mts","names":[],"sources":["../src/signing-key.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"signed.d.mts","names":[],"sources":["../src/signing-key.ts","../src/signed.ts"],"mappings":";;;;;KAIY,gBAAA;AAAA,cAME,eAAA;;AANd;;;;AAAY;AAA2B;;;;AAMzB;AAcd;KAAY,UAAA;EAAA,UACA,eAAA;AAAA;AAAA;AAqBZ;;;;;;;;;;AArBY,iBAqBU,gBAAA,CAAiB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,UAAA;;;AAAA;AAiBnE;;;;;;iBAAgB,gBAAA,CAAiB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,gBAAA;;;AAAA;AAgB5D;;;;;;iBAAgB,gBAAA,CAAiB,OAAA,UAAiB,MAAA,EAAQ,gBAAA,GAAmB,UAAA;;;;;AA3EjE;KCiCA,sBAAA;ED3BE;;;AAAA;AAcd;ECmBE,IAAA,GAAO,UAAA,KAAe,UAAA;EAEtB,GAAA,iBDpBU;ECsBV,GAAA,IAAO,MAAA,EAAQ,UAAA,WDDK;ECGpB,mBAAA;AAAA;;;;;;;;;KAWU,gBAAA;EACN,EAAA;EAAU,EAAA,EAAI,EAAA,CAAG,KAAA;AAAA;EACjB,EAAA;EAAW,KAAA,EAAO,UAAA;AAAA;;;;;ADCoC;AAgB5D;;;;;;;;KCFY,oBAAA;EDEiE,mECA3E,QAAA,IAAY,OAAA,CAAQ,EAAA,CAAG,KAAA;;;AA1CzB;;EA+CE,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,EAAA,CAAG,KAAA;;;;;;;EAOnC,MAAA,CAAO,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,OAAA;;;;;;;;AA1CvB;EAmDA,UAAA,CAAW,KAAA,YAAiB,OAAA,CAAQ,gBAAA,CAAiB,KAAA;EAxC3C;;;;EA6CV,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,IAAA;EA3CX;;;;EAgDtB,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;;;;;EAK7B,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EArDP;AAAA;AAexB;;EA2CE,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;EAEvC,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;;;;;;;;;;;;;;iBAwB5B,uBAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,EAAM,sBAAA,GACL,oBAAA,CAAqB,KAAA"}
|
package/dist/signed.mjs
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
|
|
2
|
+
import { a as toWireId, i as payloadBytesFromId, n as registerBrand, r as payloadBase32Length, s as validateBrand, t as wireMethods } from "./codec-shell-DH-UO4UR.mjs";
|
|
3
|
+
import { n as readTimestampMsFromBase32Suffix, r as writeTimestamp } from "./timestamp-bytes-BBY7JI33.mjs";
|
|
2
4
|
import { i as encodeHex, n as decodeHex, r as encodeBase64Url, t as decodeBase64Url } from "./bytes-lhzKVaBV.mjs";
|
|
5
|
+
const tagByteLength = 5;
|
|
6
|
+
const randomOffset = 6;
|
|
7
|
+
const tagOffset = 11;
|
|
8
|
+
const signedContentByteLength = 11;
|
|
9
|
+
async function computeTag(hmacKey, brandBytes, signedContent) {
|
|
10
|
+
const message = new Uint8Array(brandBytes.length + signedContent.length);
|
|
11
|
+
message.set(brandBytes, 0);
|
|
12
|
+
message.set(signedContent, brandBytes.length);
|
|
13
|
+
return new Uint8Array(await crypto.subtle.sign("HMAC", hmacKey, message)).subarray(0, tagByteLength);
|
|
14
|
+
}
|
|
15
|
+
function tagsEqual(a, b) {
|
|
16
|
+
/* v8 ignore next -- defensive guard; both call sites always pass tagByteLength-byte arrays */
|
|
17
|
+
if (a.length !== b.length) return false;
|
|
18
|
+
let diff = 0;
|
|
19
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
20
|
+
return diff === 0;
|
|
21
|
+
}
|
|
22
|
+
function createSignedTimestampLayoutOps(prefix, brand, rng, hmacKeys) {
|
|
23
|
+
const signKey = hmacKeys[0];
|
|
24
|
+
const brandBytes = new TextEncoder().encode(brand);
|
|
25
|
+
const syncBuffer = new Uint8Array(16);
|
|
26
|
+
return {
|
|
27
|
+
generateAt: async (ms) => {
|
|
28
|
+
const buffer = new Uint8Array(16);
|
|
29
|
+
writeTimestamp(ms, buffer);
|
|
30
|
+
rng(buffer.subarray(randomOffset, tagOffset));
|
|
31
|
+
const tag = await computeTag(signKey, brandBytes, buffer.subarray(0, signedContentByteLength));
|
|
32
|
+
buffer.set(tag, tagOffset);
|
|
33
|
+
return toWireId(prefix, buffer);
|
|
34
|
+
},
|
|
35
|
+
tryVerify: async (id) => {
|
|
36
|
+
const payload = payloadBytesFromId(prefix, id);
|
|
37
|
+
const storedTag = payload.subarray(tagOffset, 16);
|
|
38
|
+
const signedContent = payload.subarray(0, signedContentByteLength);
|
|
39
|
+
for (const hmacKey of hmacKeys) if (tagsEqual(storedTag, await computeTag(hmacKey, brandBytes, signedContent))) return true;
|
|
40
|
+
return false;
|
|
41
|
+
},
|
|
42
|
+
extractTimestamp: (id) => new Date(readTimestampMsFromBase32Suffix(id.slice(prefix.length))),
|
|
43
|
+
minIdForTime: (ms) => {
|
|
44
|
+
writeTimestamp(ms, syncBuffer);
|
|
45
|
+
syncBuffer.fill(0, randomOffset, 16);
|
|
46
|
+
return toWireId(prefix, syncBuffer);
|
|
47
|
+
},
|
|
48
|
+
maxIdForTime: (ms) => {
|
|
49
|
+
writeTimestamp(ms, syncBuffer);
|
|
50
|
+
syncBuffer.fill(255, randomOffset, 16);
|
|
51
|
+
return toWireId(prefix, syncBuffer);
|
|
52
|
+
},
|
|
53
|
+
exampleWireId: () => prefix + "0".repeat(payloadBase32Length)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
3
57
|
//#region src/signing-key.ts
|
|
4
58
|
const validKeyByteLengths = new Set([
|
|
5
59
|
16,
|
|
@@ -68,6 +122,48 @@ function decodeSigningKey(encoded, format) {
|
|
|
68
122
|
assertValidKeyByteLength(bytes.length);
|
|
69
123
|
return bytes;
|
|
70
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Returns true when two handles were imported from the same raw key material.
|
|
127
|
+
*
|
|
128
|
+
* Uses a constant-time comparison so duplicate detection over key material does
|
|
129
|
+
* not leak the position of the first differing byte through a timing side channel.
|
|
130
|
+
*/
|
|
131
|
+
function signingKeysEqual(a, b) {
|
|
132
|
+
const aBytes = getSigningKeyInternals(a).rawBytes;
|
|
133
|
+
const bBytes = getSigningKeyInternals(b).rawBytes;
|
|
134
|
+
if (aBytes.length !== bBytes.length) return false;
|
|
135
|
+
let diff = 0;
|
|
136
|
+
for (let i = 0; i < aBytes.length; i++) diff |= aBytes[i] ^ bBytes[i];
|
|
137
|
+
return diff === 0;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Returns the derived HMAC CryptoKey held inside the handle.
|
|
141
|
+
*
|
|
142
|
+
* Intentional module-internal escape hatch for codec implementations (e.g. `createSignedTimestampId`).
|
|
143
|
+
* Not re-exported from `@smonn/ids/signed`; external callers cannot reach this.
|
|
144
|
+
*/
|
|
145
|
+
function getSigningKeyHmacKey(key) {
|
|
146
|
+
return getSigningKeyInternals(key).hmacKey;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Asserts that a signing keyring is non-empty.
|
|
150
|
+
* @throws {IdsError} `empty_keyring` if the array is empty.
|
|
151
|
+
*/
|
|
152
|
+
function assertNonEmptySigningKeyring(keys) {
|
|
153
|
+
if (keys.length === 0) throw new IdsError("empty_keyring", "signing keyring must contain at least one key");
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Asserts that no two entries in the signing keyring share the same raw bytes.
|
|
157
|
+
* @throws {IdsError} `duplicate_keyring_entry` if a duplicate is found.
|
|
158
|
+
*/
|
|
159
|
+
function assertNonDuplicateSigningKeys(keys) {
|
|
160
|
+
for (let i = 0; i < keys.length; i++) for (let j = i + 1; j < keys.length; j++) if (signingKeysEqual(keys[i], keys[j])) throw new IdsError("duplicate_keyring_entry", "duplicate signing key in keyring");
|
|
161
|
+
}
|
|
162
|
+
function getSigningKeyInternals(key) {
|
|
163
|
+
const keyInternals = internals.get(key);
|
|
164
|
+
if (keyInternals === void 0) throw new Error("invalid signing key");
|
|
165
|
+
return keyInternals;
|
|
166
|
+
}
|
|
71
167
|
async function deriveHmacKey(bytes) {
|
|
72
168
|
const base = await crypto.subtle.importKey("raw", bytes, "HKDF", false, ["deriveKey"]);
|
|
73
169
|
return crypto.subtle.deriveKey({
|
|
@@ -95,6 +191,67 @@ function formatForError(value) {
|
|
|
95
191
|
}
|
|
96
192
|
}
|
|
97
193
|
//#endregion
|
|
98
|
-
|
|
194
|
+
//#region src/signed.ts
|
|
195
|
+
function defaultRng(target) {
|
|
196
|
+
crypto.getRandomValues(target);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Construct a {@link SignedTimestampCodec} for `brand`.
|
|
200
|
+
*
|
|
201
|
+
* `opts.keys` is a non-empty ordered signing keyring — the first entry is current
|
|
202
|
+
* (used by `generate` / `generateAt`); all entries are tried on `verify` /
|
|
203
|
+
* `safeVerify`; duplicate operator secrets are rejected at construction.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```ts
|
|
207
|
+
* const key = await importSigningKey(new Uint8Array(32));
|
|
208
|
+
* const usr = createSignedTimestampId("usr", { keys: [key] });
|
|
209
|
+
*
|
|
210
|
+
* const id = await usr.generate(); // Id<"usr">
|
|
211
|
+
* await usr.verify(id); // passes
|
|
212
|
+
* usr.extractTimestamp(id); // Date — sync, timestamp is plaintext
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
function createSignedTimestampId(brand, opts) {
|
|
216
|
+
validateBrand(brand);
|
|
217
|
+
registerBrand(brand, opts.allowDuplicateBrand);
|
|
218
|
+
assertNonEmptySigningKeyring(opts.keys);
|
|
219
|
+
assertNonDuplicateSigningKeys(opts.keys);
|
|
220
|
+
const hmacKeys = opts.keys.map(getSigningKeyHmacKey);
|
|
221
|
+
const now = opts.now ?? Date.now;
|
|
222
|
+
const rng = opts.rng ?? defaultRng;
|
|
223
|
+
const prefix = `${brand}_`;
|
|
224
|
+
const wire = wireMethods(prefix);
|
|
225
|
+
const layout = createSignedTimestampLayoutOps(prefix, brand, rng, hmacKeys);
|
|
226
|
+
return {
|
|
227
|
+
generate: () => layout.generateAt(now()),
|
|
228
|
+
generateAt: (date) => layout.generateAt(date.getTime()),
|
|
229
|
+
verify: async (id) => {
|
|
230
|
+
if (!await layout.tryVerify(id)) throw new IdsError("verification_failed", "verification failed");
|
|
231
|
+
},
|
|
232
|
+
safeVerify: async (input) => {
|
|
233
|
+
const parsed = wire.safeParse(input);
|
|
234
|
+
if (!parsed.ok) return parsed;
|
|
235
|
+
if (!await layout.tryVerify(parsed.id)) return {
|
|
236
|
+
ok: false,
|
|
237
|
+
error: "verification_failed"
|
|
238
|
+
};
|
|
239
|
+
return {
|
|
240
|
+
ok: true,
|
|
241
|
+
id: parsed.id
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
extractTimestamp: layout.extractTimestamp,
|
|
245
|
+
minIdForTime: (date) => layout.minIdForTime(date.getTime()),
|
|
246
|
+
maxIdForTime: (date) => layout.maxIdForTime(date.getTime()),
|
|
247
|
+
is: wire.is,
|
|
248
|
+
parse: wire.parse,
|
|
249
|
+
safeParse: wire.safeParse,
|
|
250
|
+
toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId()),
|
|
251
|
+
"~standard": wire["~standard"]
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
255
|
+
export { IdsError, createSignedTimestampId, decodeSigningKey, encodeSigningKey, importSigningKey, isIdsError };
|
|
99
256
|
|
|
100
257
|
//# sourceMappingURL=signed.mjs.map
|
package/dist/signed.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signed.mjs","names":[],"sources":["../src/signing-key.ts"],"sourcesContent":["import { decodeBase64Url, decodeHex, encodeBase64Url, encodeHex } from \"./bytes.js\";\nimport { IdsError } from \"./error.js\";\n\n/** Wire encoding for signing key raw key bytes (not Crockford base32). */\nexport type SigningKeyFormat = \"hex\" | \"base64url\";\n\nconst validKeyByteLengths = new Set([16, 24, 32]);\n\nconst hmacInfo = new TextEncoder().encode(\"ids/signed-timestamp/hmac\");\n\ndeclare const signingKeyBrand: unique symbol;\n\n/**\n * Opaque imported handle for one operator signing key.\n *\n * Holds a single HMAC-SHA-256 key derived via HKDF under the domain-separation\n * label `ids/signed-timestamp/hmac`. The underlying `CryptoKey` is held\n * internally and never exposed to callers. Obtain handles via\n * {@link importSigningKey} and pass them to `createSignedTimestampId` as the\n * `keys` signing keyring.\n *\n * Distinct from both the **Opaque key** and the **Wrapping key** — the same\n * raw key material must not silently serve multiple codecs without an explicit import.\n */\nexport type SigningKey = {\n readonly [signingKeyBrand]: \"SigningKey\";\n};\n\ntype SigningKeyInternals = {\n rawBytes: Uint8Array;\n hmacKey: CryptoKey;\n};\n\nconst internals = new WeakMap<SigningKey, SigningKeyInternals>();\n\n/**\n * Import raw operator key material into a {@link SigningKey} handle.\n *\n * Derives a single HMAC-SHA-256 key via HKDF under the domain-separation label\n * `ids/signed-timestamp/hmac`. Accepts 16, 24, or 32 bytes. To store or\n * transport key material, use {@link encodeSigningKey} / {@link decodeSigningKey}\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 importSigningKey(bytes: Uint8Array): Promise<SigningKey> {\n assertValidKeyByteLength(bytes.length);\n const hmacKey = await deriveHmacKey(bytes);\n const key = Object.freeze({}) as SigningKey;\n internals.set(key, { rawBytes: bytes.slice(), hmacKey });\n return key;\n}\n\n/**\n * Encode raw signing operator key material for storage in env vars or secret managers.\n *\n * Supports `\"hex\"` (lowercase) and `\"base64url\"`. Output round-trips through\n * {@link decodeSigningKey} 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 encodeSigningKey(bytes: Uint8Array, format: SigningKeyFormat): string {\n assertSigningKeyFormat(format);\n assertValidKeyByteLength(bytes.length);\n if (format === \"hex\") return encodeHex(bytes);\n return encodeBase64Url(bytes);\n}\n\n/**\n * Decode key material emitted by {@link encodeSigningKey} back to raw bytes.\n *\n * The result can be passed directly to {@link importSigningKey}.\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 decodeSigningKey(encoded: string, format: SigningKeyFormat): Uint8Array {\n assertSigningKeyFormat(format);\n let bytes: Uint8Array;\n if (format === \"hex\") {\n if (encoded.length === 0 || encoded.length % 2 !== 0) {\n throw new IdsError(\n \"invalid_key_encoding\",\n \"invalid hex key: length must be a positive even number of characters\",\n );\n }\n if (!/^[0-9a-fA-F]+$/.test(encoded)) {\n throw new IdsError(\"invalid_key_encoding\", \"invalid hex key: expected [0-9a-fA-F] only\");\n }\n bytes = decodeHex(encoded);\n } else {\n try {\n bytes = decodeBase64Url(encoded);\n } catch {\n throw new IdsError(\"invalid_key_encoding\", \"invalid base64url key\");\n }\n }\n assertValidKeyByteLength(bytes.length);\n return bytes;\n}\n\n/**\n * Returns true when two handles were imported from the same raw key material.\n *\n * Uses a constant-time comparison so duplicate detection over key material does\n * not leak the position of the first differing byte through a timing side channel.\n */\nexport function signingKeysEqual(a: SigningKey, b: SigningKey): boolean {\n const aBytes = getSigningKeyInternals(a).rawBytes;\n const bBytes = getSigningKeyInternals(b).rawBytes;\n if (aBytes.length !== bBytes.length) return false;\n let diff = 0;\n for (let i = 0; i < aBytes.length; i++) {\n diff |= aBytes[i]! ^ bBytes[i]!;\n }\n return diff === 0;\n}\n\n/**\n * Returns the derived HMAC CryptoKey held inside the handle.\n *\n * Intentional module-internal escape hatch for codec implementations (e.g. `createSignedTimestampId`).\n * Not re-exported from `@smonn/ids/signed`; external callers cannot reach this.\n */\nexport function getSigningKeyHmacKey(key: SigningKey): CryptoKey {\n return getSigningKeyInternals(key).hmacKey;\n}\n\n/**\n * Asserts that a signing keyring is non-empty.\n * @throws {IdsError} `empty_keyring` if the array is empty.\n */\nexport function assertNonEmptySigningKeyring(keys: readonly SigningKey[]): void {\n if (keys.length === 0) {\n throw new IdsError(\"empty_keyring\", \"signing keyring must contain at least one key\");\n }\n}\n\n/**\n * Asserts that no two entries in the signing keyring share the same raw bytes.\n * @throws {IdsError} `duplicate_keyring_entry` if a duplicate is found.\n */\nexport function assertNonDuplicateSigningKeys(keys: readonly SigningKey[]): void {\n for (let i = 0; i < keys.length; i++) {\n for (let j = i + 1; j < keys.length; j++) {\n if (signingKeysEqual(keys[i]!, keys[j]!)) {\n throw new IdsError(\"duplicate_keyring_entry\", \"duplicate signing key in keyring\");\n }\n }\n }\n}\n\nfunction getSigningKeyInternals(key: SigningKey): SigningKeyInternals {\n const keyInternals = internals.get(key);\n if (keyInternals === undefined) {\n throw new Error(\"invalid signing key\");\n }\n return keyInternals;\n}\n\nasync function deriveHmacKey(bytes: Uint8Array): Promise<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\", \"verify\"],\n );\n}\n\nfunction assertValidKeyByteLength(byteLength: number): void {\n if (!validKeyByteLengths.has(byteLength)) {\n throw new IdsError(\n \"invalid_key_length\",\n `invalid signing key length: expected 16, 24, or 32 bytes, got ${byteLength}`,\n );\n }\n}\n\nfunction assertSigningKeyFormat(format: unknown): asserts format is SigningKeyFormat {\n if (format !== \"hex\" && format !== \"base64url\") {\n throw new IdsError(\n \"invalid_key_format\",\n `invalid signing key format: expected hex or base64url, got '${formatForError(format)}'`,\n );\n }\n}\n\nfunction formatForError(value: unknown): string {\n try {\n return String(value);\n } catch {\n return \"[unprintable]\";\n }\n}\n"],"mappings":";;;AAMA,MAAM,sBAAsB,IAAI,IAAI;CAAC;CAAI;CAAI;AAAE,CAAC;AAEhD,MAAM,WAAW,IAAI,YAAY,CAAC,CAAC,OAAO,2BAA2B;AAyBrE,MAAM,4BAAY,IAAI,QAAyC;;;;;;;;;;;;AAa/D,eAAsB,iBAAiB,OAAwC;CAC7E,yBAAyB,MAAM,MAAM;CACrC,MAAM,UAAU,MAAM,cAAc,KAAK;CACzC,MAAM,MAAM,OAAO,OAAO,CAAC,CAAC;CAC5B,UAAU,IAAI,KAAK;EAAE,UAAU,MAAM,MAAM;EAAG;CAAQ,CAAC;CACvD,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,iBAAiB,OAAmB,QAAkC;CACpF,uBAAuB,MAAM;CAC7B,yBAAyB,MAAM,MAAM;CACrC,IAAI,WAAW,OAAO,OAAO,UAAU,KAAK;CAC5C,OAAO,gBAAgB,KAAK;AAC9B;;;;;;;;;;AAWA,SAAgB,iBAAiB,SAAiB,QAAsC;CACtF,uBAAuB,MAAM;CAC7B,IAAI;CACJ,IAAI,WAAW,OAAO;EACpB,IAAI,QAAQ,WAAW,KAAK,QAAQ,SAAS,MAAM,GACjD,MAAM,IAAI,SACR,wBACA,sEACF;EAEF,IAAI,CAAC,iBAAiB,KAAK,OAAO,GAChC,MAAM,IAAI,SAAS,wBAAwB,4CAA4C;EAEzF,QAAQ,UAAU,OAAO;CAC3B,OACE,IAAI;EACF,QAAQ,gBAAgB,OAAO;CACjC,QAAQ;EACN,MAAM,IAAI,SAAS,wBAAwB,uBAAuB;CACpE;CAEF,yBAAyB,MAAM,MAAM;CACrC,OAAO;AACT;AA6DA,eAAe,cAAc,OAAuC;CAClE,MAAM,OAAO,MAAM,OAAO,OAAO,UAC/B,OACA,OACA,QACA,OACA,CAAC,WAAW,CACd;CACA,OAAO,OAAO,OAAO,UACnB;EAAE,MAAM;EAAQ,MAAM;EAAW,MAAM,IAAI,WAAW;EAAG,MAAM;CAAS,GACxE,MACA;EAAE,MAAM;EAAQ,MAAM;EAAW,QAAQ;CAAI,GAC7C,OACA,CAAC,QAAQ,QAAQ,CACnB;AACF;AAEA,SAAS,yBAAyB,YAA0B;CAC1D,IAAI,CAAC,oBAAoB,IAAI,UAAU,GACrC,MAAM,IAAI,SACR,sBACA,iEAAiE,YACnE;AAEJ;AAEA,SAAS,uBAAuB,QAAqD;CACnF,IAAI,WAAW,SAAS,WAAW,aACjC,MAAM,IAAI,SACR,sBACA,+DAA+D,eAAe,MAAM,EAAE,EACxF;AAEJ;AAEA,SAAS,eAAe,OAAwB;CAC9C,IAAI;EACF,OAAO,OAAO,KAAK;CACrB,QAAQ;EACN,OAAO;CACT;AACF"}
|
|
1
|
+
{"version":3,"file":"signed.mjs","names":[],"sources":["../src/layouts/signed-timestamp.ts","../src/signing-key.ts","../src/signed.ts"],"sourcesContent":["import type { Id, Prefix } from \"../types.js\";\nimport { payloadBytesFromId, toWireId } from \"../wire/envelope.js\";\nimport { payloadBase32Length, payloadByteLength } from \"../wire/invariants.js\";\nimport {\n readTimestampMsFromBase32Suffix,\n timestampByteLength,\n writeTimestamp,\n} from \"../wire/timestamp-bytes.js\";\n\nconst randomByteLength = 5;\nconst tagByteLength = 5;\nconst randomOffset = timestampByteLength; // 6\nconst tagOffset = randomOffset + randomByteLength; // 11\nconst signedContentByteLength = randomOffset + randomByteLength; // 11 (ts6 ‖ rand5)\n\nasync function computeTag(\n hmacKey: CryptoKey,\n brandBytes: Uint8Array,\n signedContent: Uint8Array,\n): Promise<Uint8Array> {\n const message = new Uint8Array(brandBytes.length + signedContent.length);\n message.set(brandBytes, 0);\n message.set(signedContent, brandBytes.length);\n const signature = new Uint8Array(\n await crypto.subtle.sign(\"HMAC\", hmacKey, message as Uint8Array<ArrayBuffer>),\n );\n return signature.subarray(0, tagByteLength);\n}\n\nfunction tagsEqual(a: Uint8Array, b: Uint8Array): boolean {\n /* v8 ignore next -- defensive guard; both call sites always pass tagByteLength-byte arrays */\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a[i]! ^ b[i]!;\n return diff === 0;\n}\n\nexport function createSignedTimestampLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n brand: Brand,\n rng: (target: Uint8Array) => void,\n hmacKeys: readonly CryptoKey[],\n) {\n const signKey = hmacKeys[0]!;\n const brandBytes = new TextEncoder().encode(brand);\n const syncBuffer = new Uint8Array(payloadByteLength);\n\n return {\n generateAt: async (ms: number): Promise<Id<Brand>> => {\n const buffer = new Uint8Array(payloadByteLength);\n writeTimestamp(ms, buffer);\n rng(buffer.subarray(randomOffset, tagOffset));\n const tag = await computeTag(\n signKey,\n brandBytes,\n buffer.subarray(0, signedContentByteLength),\n );\n buffer.set(tag, tagOffset);\n return toWireId(prefix, buffer);\n },\n tryVerify: async (id: Id<Brand>): Promise<boolean> => {\n const payload = payloadBytesFromId(prefix, id);\n const storedTag = payload.subarray(tagOffset, payloadByteLength);\n const signedContent = payload.subarray(0, signedContentByteLength);\n for (const hmacKey of hmacKeys) {\n const expected = await computeTag(hmacKey, brandBytes, signedContent);\n if (tagsEqual(storedTag, expected)) return true;\n }\n return false;\n },\n extractTimestamp: (id: Id<Brand>): Date =>\n new Date(readTimestampMsFromBase32Suffix(id.slice(prefix.length))),\n minIdForTime: (ms: number): Id<Brand> => {\n writeTimestamp(ms, syncBuffer);\n syncBuffer.fill(0x00, randomOffset, payloadByteLength);\n return toWireId(prefix, syncBuffer);\n },\n maxIdForTime: (ms: number): Id<Brand> => {\n writeTimestamp(ms, syncBuffer);\n syncBuffer.fill(0xff, randomOffset, payloadByteLength);\n return toWireId(prefix, syncBuffer);\n },\n exampleWireId: (): Id<Brand> => (prefix + \"0\".repeat(payloadBase32Length)) as Id<Brand>,\n };\n}\n","import { decodeBase64Url, decodeHex, encodeBase64Url, encodeHex } from \"./bytes.js\";\nimport { IdsError } from \"./error.js\";\n\n/** Wire encoding for signing key raw key bytes (not Crockford base32). */\nexport type SigningKeyFormat = \"hex\" | \"base64url\";\n\nconst validKeyByteLengths = new Set([16, 24, 32]);\n\nconst hmacInfo = new TextEncoder().encode(\"ids/signed-timestamp/hmac\");\n\ndeclare const signingKeyBrand: unique symbol;\n\n/**\n * Opaque imported handle for one operator signing key.\n *\n * Holds a single HMAC-SHA-256 key derived via HKDF under the domain-separation\n * label `ids/signed-timestamp/hmac`. The underlying `CryptoKey` is held\n * internally and never exposed to callers. Obtain handles via\n * {@link importSigningKey} and pass them to `createSignedTimestampId` as the\n * `keys` signing keyring.\n *\n * Distinct from both the **Opaque key** and the **Wrapping key** — the same\n * raw key material must not silently serve multiple codecs without an explicit import.\n */\nexport type SigningKey = {\n readonly [signingKeyBrand]: \"SigningKey\";\n};\n\ntype SigningKeyInternals = {\n rawBytes: Uint8Array;\n hmacKey: CryptoKey;\n};\n\nconst internals = new WeakMap<SigningKey, SigningKeyInternals>();\n\n/**\n * Import raw operator key material into a {@link SigningKey} handle.\n *\n * Derives a single HMAC-SHA-256 key via HKDF under the domain-separation label\n * `ids/signed-timestamp/hmac`. Accepts 16, 24, or 32 bytes. To store or\n * transport key material, use {@link encodeSigningKey} / {@link decodeSigningKey}\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 importSigningKey(bytes: Uint8Array): Promise<SigningKey> {\n assertValidKeyByteLength(bytes.length);\n const hmacKey = await deriveHmacKey(bytes);\n const key = Object.freeze({}) as SigningKey;\n internals.set(key, { rawBytes: bytes.slice(), hmacKey });\n return key;\n}\n\n/**\n * Encode raw signing operator key material for storage in env vars or secret managers.\n *\n * Supports `\"hex\"` (lowercase) and `\"base64url\"`. Output round-trips through\n * {@link decodeSigningKey} 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 encodeSigningKey(bytes: Uint8Array, format: SigningKeyFormat): string {\n assertSigningKeyFormat(format);\n assertValidKeyByteLength(bytes.length);\n if (format === \"hex\") return encodeHex(bytes);\n return encodeBase64Url(bytes);\n}\n\n/**\n * Decode key material emitted by {@link encodeSigningKey} back to raw bytes.\n *\n * The result can be passed directly to {@link importSigningKey}.\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 decodeSigningKey(encoded: string, format: SigningKeyFormat): Uint8Array {\n assertSigningKeyFormat(format);\n let bytes: Uint8Array;\n if (format === \"hex\") {\n if (encoded.length === 0 || encoded.length % 2 !== 0) {\n throw new IdsError(\n \"invalid_key_encoding\",\n \"invalid hex key: length must be a positive even number of characters\",\n );\n }\n if (!/^[0-9a-fA-F]+$/.test(encoded)) {\n throw new IdsError(\"invalid_key_encoding\", \"invalid hex key: expected [0-9a-fA-F] only\");\n }\n bytes = decodeHex(encoded);\n } else {\n try {\n bytes = decodeBase64Url(encoded);\n } catch {\n throw new IdsError(\"invalid_key_encoding\", \"invalid base64url key\");\n }\n }\n assertValidKeyByteLength(bytes.length);\n return bytes;\n}\n\n/**\n * Returns true when two handles were imported from the same raw key material.\n *\n * Uses a constant-time comparison so duplicate detection over key material does\n * not leak the position of the first differing byte through a timing side channel.\n */\nexport function signingKeysEqual(a: SigningKey, b: SigningKey): boolean {\n const aBytes = getSigningKeyInternals(a).rawBytes;\n const bBytes = getSigningKeyInternals(b).rawBytes;\n if (aBytes.length !== bBytes.length) return false;\n let diff = 0;\n for (let i = 0; i < aBytes.length; i++) {\n diff |= aBytes[i]! ^ bBytes[i]!;\n }\n return diff === 0;\n}\n\n/**\n * Returns the derived HMAC CryptoKey held inside the handle.\n *\n * Intentional module-internal escape hatch for codec implementations (e.g. `createSignedTimestampId`).\n * Not re-exported from `@smonn/ids/signed`; external callers cannot reach this.\n */\nexport function getSigningKeyHmacKey(key: SigningKey): CryptoKey {\n return getSigningKeyInternals(key).hmacKey;\n}\n\n/**\n * Asserts that a signing keyring is non-empty.\n * @throws {IdsError} `empty_keyring` if the array is empty.\n */\nexport function assertNonEmptySigningKeyring(keys: readonly SigningKey[]): void {\n if (keys.length === 0) {\n throw new IdsError(\"empty_keyring\", \"signing keyring must contain at least one key\");\n }\n}\n\n/**\n * Asserts that no two entries in the signing keyring share the same raw bytes.\n * @throws {IdsError} `duplicate_keyring_entry` if a duplicate is found.\n */\nexport function assertNonDuplicateSigningKeys(keys: readonly SigningKey[]): void {\n for (let i = 0; i < keys.length; i++) {\n for (let j = i + 1; j < keys.length; j++) {\n if (signingKeysEqual(keys[i]!, keys[j]!)) {\n throw new IdsError(\"duplicate_keyring_entry\", \"duplicate signing key in keyring\");\n }\n }\n }\n}\n\nfunction getSigningKeyInternals(key: SigningKey): SigningKeyInternals {\n const keyInternals = internals.get(key);\n if (keyInternals === undefined) {\n throw new Error(\"invalid signing key\");\n }\n return keyInternals;\n}\n\nasync function deriveHmacKey(bytes: Uint8Array): Promise<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\", \"verify\"],\n );\n}\n\nfunction assertValidKeyByteLength(byteLength: number): void {\n if (!validKeyByteLengths.has(byteLength)) {\n throw new IdsError(\n \"invalid_key_length\",\n `invalid signing key length: expected 16, 24, or 32 bytes, got ${byteLength}`,\n );\n }\n}\n\nfunction assertSigningKeyFormat(format: unknown): asserts format is SigningKeyFormat {\n if (format !== \"hex\" && format !== \"base64url\") {\n throw new IdsError(\n \"invalid_key_format\",\n `invalid signing key format: expected hex or base64url, got '${formatForError(format)}'`,\n );\n }\n}\n\nfunction formatForError(value: unknown): string {\n try {\n return String(value);\n } catch {\n return \"[unprintable]\";\n }\n}\n","import { validateBrand } from \"./brand.js\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"./error.js\";\nimport { createSignedTimestampLayoutOps } from \"./layouts/signed-timestamp.js\";\nimport { registerBrand } from \"./registry.js\";\nimport type {\n Id,\n JsonSchema,\n ParseError,\n ParseResult,\n Prefix,\n StandardSchemaProps,\n} from \"./types.js\";\nimport { wireMethods } from \"./wire/codec-shell.js\";\nimport {\n assertNonDuplicateSigningKeys,\n assertNonEmptySigningKeyring,\n decodeSigningKey,\n encodeSigningKey,\n getSigningKeyHmacKey,\n importSigningKey,\n type SigningKey,\n type SigningKeyFormat,\n} from \"./signing-key.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode };\nexport {\n decodeSigningKey,\n encodeSigningKey,\n importSigningKey,\n type SigningKey,\n type SigningKeyFormat,\n};\n\n/**\n * Configuration options for a Signed Timestamp codec instance.\n */\nexport type SignedTimestampOptions = {\n /**\n * Non-empty ordered signing keyring. The first entry is current — the only one\n * `generate` / `generateAt` sign with. `verify` / `safeVerify` trial every entry\n * until the tag matches. Duplicate raw secrets are rejected at construction.\n */\n keys: [SigningKey, ...SigningKey[]];\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now?: () => number;\n /** Writes 5 random bytes into `target` for the random tail. Defaults to `crypto.getRandomValues`. */\n rng?: (target: Uint8Array) => void;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\n/**\n * Result returned by {@link SignedTimestampCodec.safeVerify}.\n *\n * On success, `id` is the canonical {@link Id}.\n * On failure, `error` is a {@link ParseError} for structural problems or\n * `\"verification_failed\"` when the HMAC tag does not match any entry in the\n * signing keyring.\n */\nexport type SafeVerifyResult<Brand extends string> =\n | { ok: true; id: Id<Brand> }\n | { ok: false; error: ParseError | \"verification_failed\" };\n\n/**\n * Codec returned by {@link createSignedTimestampId}.\n *\n * Keeps the 6-byte millisecond timestamp **readable and sortable** like the\n * Timestamp codec, but replaces half of the 10-byte random tail with a truncated\n * HMAC tag, making IDs **tamper-evident and verifiable without a database lookup**.\n *\n * Byte layout: `ts6 ‖ rand5 ‖ tag5` where the 40-bit tag =\n * `trunc(HMAC-SHA256(hmacKey, brand ‖ ts6 ‖ rand5), 40)`.\n *\n * - Async (HMAC): `generate`, `generateAt`, `verify`, `safeVerify`.\n * - Sync (no key / plaintext timestamp): all other methods.\n */\nexport type SignedTimestampCodec<Brand extends string> = {\n /** Produces a canonical ID signed with the current (first) key. */\n generate(): Promise<Id<Brand>>;\n /**\n * Produces a canonical ID with timestamp from `date`, signed with the current key.\n * Throws on invalid dates.\n */\n generateAt(date: Date): Promise<Id<Brand>>;\n /**\n * Recomputes the HMAC tag across every keyring entry.\n *\n * Throws `IdsError` with `code: \"verification_failed\"` if no entry matches.\n * Tamper of the brand, timestamp bytes, or random bytes all fail here.\n */\n verify(id: Id<Brand>): Promise<void>;\n /**\n * Non-throwing path for untrusted input.\n *\n * Structurally parses `input` first (same rules as {@link safeParse}), then\n * verifies the HMAC tag. Returns `{ ok: false, error }` on any failure —\n * {@link ParseError} for structural problems or `\"verification_failed\"` for tag\n * mismatch — without throwing.\n */\n safeVerify(input: unknown): Promise<SafeVerifyResult<Brand>>;\n /**\n * Decodes the creation `Date` from an `Id<Brand>`.\n * Sync — the 6-byte timestamp is plaintext. Trusts the type; use `safeParse()` at boundaries first.\n */\n extractTimestamp(id: Id<Brand>): Date;\n /**\n * Tight lower bound sentinel for range scans (`ts(t) ‖ 0x00×10`).\n * **Not verifiable** — carries no valid tag.\n */\n minIdForTime(date: Date): Id<Brand>;\n /**\n * Tight upper bound sentinel for range scans (`ts(t) ‖ 0xff×10`).\n * **Not verifiable** — carries no valid tag.\n */\n maxIdForTime(date: Date): Id<Brand>;\n /**\n * Strict type guard: `true` only for already-canonical `Id<Brand>` strings.\n * For untrusted input, use `safeParse()` or `safeVerify()` instead.\n */\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\nfunction defaultRng(target: Uint8Array): void {\n crypto.getRandomValues(target as Uint8Array<ArrayBuffer>);\n}\n\n/**\n * Construct a {@link SignedTimestampCodec} for `brand`.\n *\n * `opts.keys` is a non-empty ordered signing keyring — the first entry is current\n * (used by `generate` / `generateAt`); all entries are tried on `verify` /\n * `safeVerify`; duplicate operator secrets are rejected at construction.\n *\n * @example\n * ```ts\n * const key = await importSigningKey(new Uint8Array(32));\n * const usr = createSignedTimestampId(\"usr\", { keys: [key] });\n *\n * const id = await usr.generate(); // Id<\"usr\">\n * await usr.verify(id); // passes\n * usr.extractTimestamp(id); // Date — sync, timestamp is plaintext\n * ```\n */\nexport function createSignedTimestampId<Brand extends string>(\n brand: Brand,\n opts: SignedTimestampOptions,\n): SignedTimestampCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n assertNonEmptySigningKeyring(opts.keys);\n assertNonDuplicateSigningKeys(opts.keys);\n\n const hmacKeys = opts.keys.map(getSigningKeyHmacKey);\n const now = opts.now ?? Date.now;\n const rng = opts.rng ?? defaultRng;\n const prefix: Prefix<Brand> = `${brand}_`;\n const wire = wireMethods(prefix);\n const layout = createSignedTimestampLayoutOps(prefix, brand, rng, hmacKeys);\n\n return {\n generate: () => layout.generateAt(now()),\n generateAt: (date: Date) => layout.generateAt(date.getTime()),\n verify: async (id) => {\n const ok = await layout.tryVerify(id);\n if (!ok) throw new IdsError(\"verification_failed\", \"verification failed\");\n },\n safeVerify: async (input) => {\n const parsed = wire.safeParse(input);\n if (!parsed.ok) return parsed;\n const ok = await layout.tryVerify(parsed.id);\n if (!ok) return { ok: false, error: \"verification_failed\" };\n return { ok: true, id: parsed.id };\n },\n extractTimestamp: layout.extractTimestamp,\n minIdForTime: (date: Date) => layout.minIdForTime(date.getTime()),\n maxIdForTime: (date: Date) => layout.maxIdForTime(date.getTime()),\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":"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"}
|