@smonn/ids 0.5.0 → 0.7.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 +555 -18
- package/dist/adapter-types-oHCCSgOO.d.mts +12 -0
- package/dist/adapter-types-oHCCSgOO.d.mts.map +1 -0
- package/dist/cli.mjs +246 -63
- package/dist/cli.mjs.map +1 -1
- package/dist/{codec-shell-dWpxoFmy.mjs → codec-shell-DH-UO4UR.mjs} +8 -8
- package/dist/codec-shell-DH-UO4UR.mjs.map +1 -0
- package/dist/drizzle-CeSni5PB.d.mts +44 -0
- package/dist/drizzle-CeSni5PB.d.mts.map +1 -0
- package/dist/drizzle.d.mts +3 -0
- package/dist/drizzle.mjs +43 -0
- package/dist/drizzle.mjs.map +1 -0
- package/dist/error-Cp5qYZcv.mjs +52 -0
- package/dist/error-Cp5qYZcv.mjs.map +1 -0
- package/dist/error-DTr4i6Ic.d.mts +44 -0
- package/dist/error-DTr4i6Ic.d.mts.map +1 -0
- package/dist/express.d.mts +85 -0
- package/dist/express.d.mts.map +1 -0
- package/dist/express.mjs +90 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +88 -0
- package/dist/fastify.d.mts.map +1 -0
- package/dist/fastify.mjs +91 -0
- package/dist/fastify.mjs.map +1 -0
- package/dist/hono.d.mts +68 -0
- package/dist/hono.d.mts.map +1 -0
- package/dist/hono.mjs +63 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -2
- package/dist/kysely.d.mts +56 -0
- package/dist/kysely.d.mts.map +1 -0
- package/dist/kysely.mjs +43 -0
- package/dist/kysely.mjs.map +1 -0
- package/dist/{opaque-B4ps7Pqk.mjs → opaque-uvjOFY_0.mjs} +37 -20
- package/dist/opaque-uvjOFY_0.mjs.map +1 -0
- package/dist/opaque.d.mts +34 -9
- package/dist/opaque.d.mts.map +1 -1
- package/dist/opaque.mjs +3 -2
- package/dist/prisma.d.mts +85 -0
- package/dist/prisma.d.mts.map +1 -0
- package/dist/prisma.mjs +54 -0
- package/dist/prisma.mjs.map +1 -0
- package/dist/reverse-BgFU6JHw.mjs +87 -0
- package/dist/reverse-BgFU6JHw.mjs.map +1 -0
- package/dist/reverse.d.mts +77 -0
- package/dist/reverse.d.mts.map +1 -0
- package/dist/reverse.mjs +3 -0
- package/dist/signed.d.mts +56 -0
- package/dist/signed.d.mts.map +1 -0
- package/dist/signed.mjs +100 -0
- package/dist/signed.mjs.map +1 -0
- package/dist/{timestamp-Bgzxx8bE.mjs → timestamp-B5_UCzc6.mjs} +3 -3
- package/dist/{timestamp-Bgzxx8bE.mjs.map → timestamp-B5_UCzc6.mjs.map} +1 -1
- package/dist/{timestamp-bytes-B57RM7Ho.mjs → timestamp-bytes-BBY7JI33.mjs} +2 -2
- package/dist/{timestamp-bytes-B57RM7Ho.mjs.map → timestamp-bytes-BBY7JI33.mjs.map} +1 -1
- package/dist/wrapped-0vL72Nje.mjs +361 -0
- package/dist/wrapped-0vL72Nje.mjs.map +1 -0
- package/dist/wrapped.d.mts +89 -9
- package/dist/wrapped.d.mts.map +1 -1
- package/dist/wrapped.mjs +3 -336
- package/package.json +45 -3
- package/dist/codec-shell-dWpxoFmy.mjs.map +0 -1
- package/dist/opaque-B4ps7Pqk.mjs.map +0 -1
- package/dist/wrapped.mjs.map +0 -1
package/dist/fastify.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
//#region src/fastify.ts
|
|
2
|
+
/**
|
|
3
|
+
* Typed error thrown into Fastify's `setErrorHandler` on validation failure.
|
|
4
|
+
* Inspect `err.reason` and `err.statusCode` in your error handler.
|
|
5
|
+
*/
|
|
6
|
+
var IdParamError = class extends Error {
|
|
7
|
+
statusCode;
|
|
8
|
+
reason;
|
|
9
|
+
constructor(reason, statusCode) {
|
|
10
|
+
super(`ID validation failed: ${reason}`);
|
|
11
|
+
this.name = "IdParamError";
|
|
12
|
+
this.reason = reason;
|
|
13
|
+
this.statusCode = statusCode;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Fastify `preHandler` hook factory that validates a named route param against a codec via `safeParse`.
|
|
18
|
+
*
|
|
19
|
+
* **Default (no options):** throws `IdParamError` carrying `statusCode` and `reason` so the app's
|
|
20
|
+
* existing `setErrorHandler` controls rendering. The adapter does not write a response body itself.
|
|
21
|
+
*
|
|
22
|
+
* **`options.onError`:** when provided, the hook calls `onError` and does not throw; the consumer
|
|
23
|
+
* fully owns the response via `reply`.
|
|
24
|
+
*
|
|
25
|
+
* **`options.status`:** remaps the default HTTP status for a reason without a full handler.
|
|
26
|
+
*
|
|
27
|
+
* - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
|
|
28
|
+
* - **Malformed or missing ID → `reason: "malformed"`, default 400**
|
|
29
|
+
*
|
|
30
|
+
* On success, stores the canonical `Id<Brand>` in `request.params` under `paramName`.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { idParam, IdParamError } from "@smonn/ids/fastify";
|
|
35
|
+
* import { createTimestampId } from "@smonn/ids";
|
|
36
|
+
*
|
|
37
|
+
* const usr = createTimestampId("usr");
|
|
38
|
+
*
|
|
39
|
+
* // Default: throws IdParamError → setErrorHandler renders it
|
|
40
|
+
* fastify.get("/users/:id", { preHandler: idParam("id", usr) }, (request, reply) => {
|
|
41
|
+
* const id = request.params.id; // Id<"usr">, canonical
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Error handler receives the typed error
|
|
45
|
+
* fastify.setErrorHandler((err, request, reply) => {
|
|
46
|
+
* if (err instanceof IdParamError) {
|
|
47
|
+
* reply.status(err.statusCode).send({ error: err.reason });
|
|
48
|
+
* return;
|
|
49
|
+
* }
|
|
50
|
+
* reply.send(err);
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* // Override: consumer fully owns the error response
|
|
54
|
+
* fastify.get("/orgs/:id", {
|
|
55
|
+
* preHandler: idParam("id", org, {
|
|
56
|
+
* onError: (failure, request, reply) =>
|
|
57
|
+
* reply.status(failure.status).send({ error: failure.reason }),
|
|
58
|
+
* }),
|
|
59
|
+
* }, handler);
|
|
60
|
+
*
|
|
61
|
+
* // Or a lightweight status remap without a full handler
|
|
62
|
+
* fastify.get("/things/:id", {
|
|
63
|
+
* preHandler: idParam("id", thing, { status: { brand_mismatch: 400 } }),
|
|
64
|
+
* }, handler);
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
function idParam(paramName, codec, options) {
|
|
68
|
+
return async (request, reply) => {
|
|
69
|
+
const raw = request.params[paramName];
|
|
70
|
+
const result = codec.safeParse(raw);
|
|
71
|
+
if (!result.ok) {
|
|
72
|
+
const reason = result.error === "invalid_prefix" ? "brand_mismatch" : "malformed";
|
|
73
|
+
const defaultStatus = reason === "brand_mismatch" ? 404 : 400;
|
|
74
|
+
const status = options?.status?.[reason] ?? defaultStatus;
|
|
75
|
+
const failure = {
|
|
76
|
+
reason,
|
|
77
|
+
status
|
|
78
|
+
};
|
|
79
|
+
if (options?.onError) {
|
|
80
|
+
await options.onError(failure, request, reply);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
throw new IdParamError(reason, status);
|
|
84
|
+
}
|
|
85
|
+
request.params[paramName] = result.id;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { IdParamError, idParam };
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=fastify.mjs.map
|
|
@@ -0,0 +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 = (request.params as Record<ParamKey, unknown>)[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 as Record<ParamKey, Id<Brand>>)[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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuEA,SAAgB,QACd,WACA,OACA,SACiE;CACjE,OAAO,OAAO,SAAS,UAAyB;EAC9C,MAAM,MAAO,QAAQ,OAAqC;EAC1D,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,QAAS,OAAuC,aAAa,OAAO;CACtE;AACF"}
|
package/dist/hono.d.mts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { i as ParseResult, t as Id } from "./types-g7CiQDyE.mjs";
|
|
2
|
+
import { t as IdParamFailure } from "./adapter-types-oHCCSgOO.mjs";
|
|
3
|
+
import { Context, MiddlewareHandler } from "hono";
|
|
4
|
+
|
|
5
|
+
//#region src/hono.d.ts
|
|
6
|
+
type IdCodec<Brand extends string> = {
|
|
7
|
+
safeParse(value: unknown): ParseResult<Brand>;
|
|
8
|
+
};
|
|
9
|
+
/** Options for `idParam`. All fields are optional. */
|
|
10
|
+
type IdParamOptions = {
|
|
11
|
+
/**
|
|
12
|
+
* Called instead of throwing when provided. The hook owns the response entirely —
|
|
13
|
+
* the adapter neither throws nor writes a body.
|
|
14
|
+
*/
|
|
15
|
+
onError?: (failure: IdParamFailure, c: Context) => Response | Promise<Response>;
|
|
16
|
+
/**
|
|
17
|
+
* Remap the default HTTP status for a failure reason without a full handler.
|
|
18
|
+
* e.g. `{ brand_mismatch: 400 }` treats both failure kinds as 400.
|
|
19
|
+
*/
|
|
20
|
+
status?: {
|
|
21
|
+
brand_mismatch?: number;
|
|
22
|
+
malformed?: number;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Hono middleware that validates a named route param against a codec via `safeParse`.
|
|
27
|
+
*
|
|
28
|
+
* **Default (no options):** throws `HTTPException(status)` so the app's existing `onError` handler
|
|
29
|
+
* controls rendering and content negotiation. The adapter does not write a response body itself.
|
|
30
|
+
*
|
|
31
|
+
* **`options.onError`:** when provided, the hook owns the response entirely — the adapter neither
|
|
32
|
+
* throws nor writes a response.
|
|
33
|
+
*
|
|
34
|
+
* **`options.status`:** remaps the default HTTP status for a reason without a full handler.
|
|
35
|
+
*
|
|
36
|
+
* - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
|
|
37
|
+
* - **Malformed or missing ID → `reason: "malformed"`, default 400**
|
|
38
|
+
*
|
|
39
|
+
* On success, stores the canonical `Id<Brand>` in the Hono context under `paramName`
|
|
40
|
+
* and calls `next()`.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { idParam } from "@smonn/ids/hono";
|
|
45
|
+
* import { createTimestampId } from "@smonn/ids";
|
|
46
|
+
*
|
|
47
|
+
* const usr = createTimestampId("usr");
|
|
48
|
+
*
|
|
49
|
+
* // Default: throws HTTPException → app.onError renders it
|
|
50
|
+
* app.get("/users/:id", idParam("id", usr), (c) => {
|
|
51
|
+
* const id = c.get("id"); // Id<"usr">, canonical
|
|
52
|
+
* });
|
|
53
|
+
*
|
|
54
|
+
* // Override: consumer fully owns the response
|
|
55
|
+
* app.get("/orgs/:id", idParam("id", org, {
|
|
56
|
+
* onError: (failure, c) => c.json({ error: failure.reason }, failure.status),
|
|
57
|
+
* }), handler);
|
|
58
|
+
*
|
|
59
|
+
* // Or a lightweight status remap without a full handler
|
|
60
|
+
* app.get("/things/:id", idParam("id", thing, { status: { brand_mismatch: 400 } }), handler);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
declare function idParam<ParamKey extends string, Brand extends string>(paramName: ParamKey, codec: IdCodec<Brand>, options?: IdParamOptions): MiddlewareHandler<{
|
|
64
|
+
Variables: Record<ParamKey, Id<Brand>>;
|
|
65
|
+
}>;
|
|
66
|
+
//#endregion
|
|
67
|
+
export { type IdParamFailure, IdParamOptions, idParam };
|
|
68
|
+
//# sourceMappingURL=hono.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hono.d.mts","names":[],"sources":["../src/hono.ts"],"mappings":";;;;;KAOK,OAAA;EACH,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;;KAI7B,cAAA;;;;;EAKV,OAAA,IAAW,OAAA,EAAS,cAAA,EAAgB,CAAA,EAAG,OAAA,KAAY,QAAA,GAAW,OAAA,CAAQ,QAAA;EAT/B;AAAA;AAIzC;;EAUE,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;;;;;;;AAAA;AAyCtC;;;;;;;;;;;;;;;;;;;;;;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
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { HTTPException } from "hono/http-exception";
|
|
2
|
+
//#region src/hono.ts
|
|
3
|
+
/**
|
|
4
|
+
* Hono middleware that validates a named route param against a codec via `safeParse`.
|
|
5
|
+
*
|
|
6
|
+
* **Default (no options):** throws `HTTPException(status)` so the app's existing `onError` handler
|
|
7
|
+
* controls rendering and content negotiation. The adapter does not write a response body itself.
|
|
8
|
+
*
|
|
9
|
+
* **`options.onError`:** when provided, the hook owns the response entirely — the adapter neither
|
|
10
|
+
* throws nor writes a response.
|
|
11
|
+
*
|
|
12
|
+
* **`options.status`:** remaps the default HTTP status for a reason without a full handler.
|
|
13
|
+
*
|
|
14
|
+
* - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
|
|
15
|
+
* - **Malformed or missing ID → `reason: "malformed"`, default 400**
|
|
16
|
+
*
|
|
17
|
+
* On success, stores the canonical `Id<Brand>` in the Hono context under `paramName`
|
|
18
|
+
* and calls `next()`.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { idParam } from "@smonn/ids/hono";
|
|
23
|
+
* import { createTimestampId } from "@smonn/ids";
|
|
24
|
+
*
|
|
25
|
+
* const usr = createTimestampId("usr");
|
|
26
|
+
*
|
|
27
|
+
* // Default: throws HTTPException → app.onError renders it
|
|
28
|
+
* app.get("/users/:id", idParam("id", usr), (c) => {
|
|
29
|
+
* const id = c.get("id"); // Id<"usr">, canonical
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // Override: consumer fully owns the response
|
|
33
|
+
* app.get("/orgs/:id", idParam("id", org, {
|
|
34
|
+
* onError: (failure, c) => c.json({ error: failure.reason }, failure.status),
|
|
35
|
+
* }), handler);
|
|
36
|
+
*
|
|
37
|
+
* // Or a lightweight status remap without a full handler
|
|
38
|
+
* app.get("/things/:id", idParam("id", thing, { status: { brand_mismatch: 400 } }), handler);
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
function idParam(paramName, codec, options) {
|
|
42
|
+
return async (c, next) => {
|
|
43
|
+
const raw = c.req.param(paramName);
|
|
44
|
+
const result = codec.safeParse(raw);
|
|
45
|
+
if (!result.ok) {
|
|
46
|
+
const reason = result.error === "invalid_prefix" ? "brand_mismatch" : "malformed";
|
|
47
|
+
const defaultStatus = reason === "brand_mismatch" ? 404 : 400;
|
|
48
|
+
const status = options?.status?.[reason] ?? defaultStatus;
|
|
49
|
+
const failure = {
|
|
50
|
+
reason,
|
|
51
|
+
status
|
|
52
|
+
};
|
|
53
|
+
if (options?.onError) return options.onError(failure, c);
|
|
54
|
+
throw new HTTPException(status);
|
|
55
|
+
}
|
|
56
|
+
c.set(paramName, result.id);
|
|
57
|
+
await next();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { idParam };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=hono.mjs.map
|
|
@@ -0,0 +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 { 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/** 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?: number; malformed?: number };\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 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 return options.onError(failure, c);\n }\n throw new HTTPException(status as ConstructorParameters<typeof HTTPException>[0]);\n }\n c.set(paramName, result.id);\n await next();\n return;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DA,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,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,SACX,OAAO,QAAQ,QAAQ,SAAS,CAAC;GAEnC,MAAM,IAAI,cAAc,MAAwD;EAClF;EACA,EAAE,IAAI,WAAW,OAAO,EAAE;EAC1B,MAAM,KAAK;CAEb;AACF"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-DTr4i6Ic.mjs";
|
|
1
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/timestamp.d.ts
|
|
@@ -51,5 +52,5 @@ type TimestampCodec<Brand extends string> = {
|
|
|
51
52
|
*/
|
|
52
53
|
declare function createTimestampId<Brand extends string>(brand: Brand, opts?: TimestampOptions): TimestampCodec<Brand>;
|
|
53
54
|
//#endregion
|
|
54
|
-
export { type Id, type JsonSchema, type ParseError, type ParseResult, type TimestampCodec, type TimestampOptions, createTimestampId };
|
|
55
|
+
export { type Id, IdsError, type IdsErrorCode, type JsonSchema, type ParseError, type ParseResult, type TimestampCodec, type TimestampOptions, createTimestampId, isIdsError };
|
|
55
56
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/timestamp.ts"],"mappings":"
|
|
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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
|
|
1
|
+
import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
|
|
2
|
+
import { t as createTimestampId } from "./timestamp-B5_UCzc6.mjs";
|
|
3
|
+
export { IdsError, createTimestampId, isIdsError };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-DTr4i6Ic.mjs";
|
|
2
|
+
import { t as Id } from "./types-g7CiQDyE.mjs";
|
|
3
|
+
import { t as IdColumnCodec } from "./drizzle-CeSni5PB.mjs";
|
|
4
|
+
import { ColumnType } from "kysely";
|
|
5
|
+
|
|
6
|
+
//#region src/kysely.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Kysely column type mapping for `Id<Brand>`.
|
|
9
|
+
*
|
|
10
|
+
* Use this in your Kysely `Database` interface to type a column as `Id<Brand>` at
|
|
11
|
+
* the TypeScript level. Pair it with `idColumn(codec)` for runtime read/write
|
|
12
|
+
* transformation.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import type { IdColumnType } from "@smonn/ids/kysely";
|
|
17
|
+
* import type { Id } from "@smonn/ids";
|
|
18
|
+
*
|
|
19
|
+
* interface Database {
|
|
20
|
+
* users: { id: IdColumnType<"usr"> };
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
type IdColumnType<Brand extends string> = ColumnType<Id<Brand>, Id<Brand>, Id<Brand>>;
|
|
25
|
+
/**
|
|
26
|
+
* Kysely column adapter bound to a codec.
|
|
27
|
+
*
|
|
28
|
+
* Returns an object with `fromDriver` / `toDriver` helpers that mirror the read/write
|
|
29
|
+
* contract of the Drizzle adapter — same error message, same strictness (safeParse on
|
|
30
|
+
* read, identity on write).
|
|
31
|
+
*
|
|
32
|
+
* **Write path:** passes the `Id<Brand>` directly to the driver — it is already
|
|
33
|
+
* the canonical string form.
|
|
34
|
+
*
|
|
35
|
+
* **Read path:** normalises the raw DB string via `codec.safeParse()`. Throws if
|
|
36
|
+
* the value does not parse as a valid `Id<Brand>`.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { idColumn } from "@smonn/ids/kysely";
|
|
41
|
+
* import { createTimestampId } from "@smonn/ids";
|
|
42
|
+
*
|
|
43
|
+
* const usr = createTimestampId("usr");
|
|
44
|
+
* const usrCol = idColumn(usr);
|
|
45
|
+
*
|
|
46
|
+
* // In a query result handler:
|
|
47
|
+
* const id = usrCol.fromDriver(row.id);
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
declare function idColumn<Brand extends string>(codec: IdColumnCodec<Brand>): {
|
|
51
|
+
toDriver(value: Id<Brand>): string;
|
|
52
|
+
fromDriver(value: string): Id<Brand>;
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
export { type IdColumnCodec, IdColumnType, IdsError, type IdsErrorCode, idColumn, isIdsError };
|
|
56
|
+
//# sourceMappingURL=kysely.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kysely.d.mts","names":[],"sources":["../src/kysely.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;KA0BY,YAAA,yBAAqC,UAAA,CAAW,EAAA,CAAG,KAAA,GAAQ,EAAA,CAAG,KAAA,GAAQ,EAAA,CAAG,KAAA;;;AAAA;AA2BrF;;;;;;;;;;;;;;;;;;;;;;iBAAgB,QAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA;EAErB,QAAA,CAAS,KAAA,EAAO,EAAA,CAAG,KAAA;EACnB,UAAA,CAAW,KAAA,WAAgB,EAAA,CAAG,KAAA;AAAA"}
|
package/dist/kysely.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
|
|
2
|
+
//#region src/kysely.ts
|
|
3
|
+
/**
|
|
4
|
+
* Kysely column adapter bound to a codec.
|
|
5
|
+
*
|
|
6
|
+
* Returns an object with `fromDriver` / `toDriver` helpers that mirror the read/write
|
|
7
|
+
* contract of the Drizzle adapter — same error message, same strictness (safeParse on
|
|
8
|
+
* read, identity on write).
|
|
9
|
+
*
|
|
10
|
+
* **Write path:** passes the `Id<Brand>` directly to the driver — it is already
|
|
11
|
+
* the canonical string form.
|
|
12
|
+
*
|
|
13
|
+
* **Read path:** normalises the raw DB string via `codec.safeParse()`. Throws if
|
|
14
|
+
* the value does not parse as a valid `Id<Brand>`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { idColumn } from "@smonn/ids/kysely";
|
|
19
|
+
* import { createTimestampId } from "@smonn/ids";
|
|
20
|
+
*
|
|
21
|
+
* const usr = createTimestampId("usr");
|
|
22
|
+
* const usrCol = idColumn(usr);
|
|
23
|
+
*
|
|
24
|
+
* // In a query result handler:
|
|
25
|
+
* const id = usrCol.fromDriver(row.id);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
function idColumn(codec) {
|
|
29
|
+
return {
|
|
30
|
+
toDriver(value) {
|
|
31
|
+
return value;
|
|
32
|
+
},
|
|
33
|
+
fromDriver(value) {
|
|
34
|
+
const result = codec.safeParse(value);
|
|
35
|
+
if (!result.ok) throw new IdsError("invalid_id", `invalid ID from database: ${result.error}`, { cause: result.error });
|
|
36
|
+
return result.id;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { IdsError, idColumn, isIdsError };
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=kysely.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kysely.mjs","names":[],"sources":["../src/kysely.ts"],"sourcesContent":["import type { ColumnType } from \"kysely\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"./error.js\";\nimport type { IdColumnCodec } from \"./drizzle.js\";\nimport type { Id } from \"./types.js\";\n\nexport type { IdColumnCodec } from \"./drizzle.js\";\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\n/**\n * Kysely column type mapping for `Id<Brand>`.\n *\n * Use this in your Kysely `Database` interface to type a column as `Id<Brand>` at\n * the TypeScript level. Pair it with `idColumn(codec)` for runtime read/write\n * transformation.\n *\n * @example\n * ```ts\n * import type { IdColumnType } from \"@smonn/ids/kysely\";\n * import type { Id } from \"@smonn/ids\";\n *\n * interface Database {\n * users: { id: IdColumnType<\"usr\"> };\n * }\n * ```\n */\nexport type IdColumnType<Brand extends string> = ColumnType<Id<Brand>, Id<Brand>, Id<Brand>>;\n\n/**\n * Kysely column adapter bound to a codec.\n *\n * Returns an object with `fromDriver` / `toDriver` helpers that mirror the read/write\n * contract of the Drizzle adapter — same error message, same strictness (safeParse on\n * read, identity on write).\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()`. Throws if\n * the value does not parse as a valid `Id<Brand>`.\n *\n * @example\n * ```ts\n * import { idColumn } from \"@smonn/ids/kysely\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const usrCol = idColumn(usr);\n *\n * // In a query result handler:\n * const id = usrCol.fromDriver(row.id);\n * ```\n */\nexport function idColumn<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): {\n toDriver(value: Id<Brand>): string;\n fromDriver(value: string): Id<Brand>;\n} {\n return {\n toDriver(value: Id<Brand>): string {\n return value;\n },\n fromDriver(value: string): Id<Brand> {\n const result = codec.safeParse(value);\n if (!result.ok) {\n throw new IdsError(\"invalid_id\", `invalid ID from database: ${result.error}`, {\n cause: result.error,\n });\n }\n return result.id;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAgB,SACd,OAIA;CACA,OAAO;EACL,SAAS,OAA0B;GACjC,OAAO;EACT;EACA,WAAW,OAA0B;GACnC,MAAM,SAAS,MAAM,UAAU,KAAK;GACpC,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,SAAS,cAAc,6BAA6B,OAAO,SAAS,EAC5E,OAAO,OAAO,MAChB,CAAC;GAEH,OAAO,OAAO;EAChB;CACF;AACF"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { r as
|
|
1
|
+
import { 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 { r as writeTimestamp, t as readTimestampMs } from "./timestamp-bytes-BBY7JI33.mjs";
|
|
3
4
|
import { i as encodeHex, n as decodeHex, r as encodeBase64Url, t as decodeBase64Url } from "./bytes-lhzKVaBV.mjs";
|
|
4
5
|
//#region src/layouts/opaque.ts
|
|
5
6
|
const zeroIv = new Uint8Array(16);
|
|
@@ -59,6 +60,29 @@ const validAesKeyByteLengths = new Set([
|
|
|
59
60
|
24,
|
|
60
61
|
32
|
|
61
62
|
]);
|
|
63
|
+
const opaqueKeyInternals = /* @__PURE__ */ new WeakMap();
|
|
64
|
+
/**
|
|
65
|
+
* Imports raw AES key bytes into an {@link OpaqueKey} handle for the Opaque
|
|
66
|
+
* Timestamp codec.
|
|
67
|
+
*
|
|
68
|
+
* Accepts 16, 24, or 32 bytes (AES-128 / AES-192 / AES-256 strength).
|
|
69
|
+
* To store or transport key material, use {@link encodeOpaqueKey} /
|
|
70
|
+
* {@link decodeOpaqueKey} (`"hex"` or `"base64url"` — not Crockford base32).
|
|
71
|
+
*
|
|
72
|
+
* @param bytes - 16, 24, or 32 raw key bytes.
|
|
73
|
+
*/
|
|
74
|
+
async function importOpaqueKey(bytes) {
|
|
75
|
+
assertValidAesKeyByteLength(bytes.length);
|
|
76
|
+
const cryptoKey = await crypto.subtle.importKey("raw", bytes, "AES-CBC", false, ["encrypt", "decrypt"]);
|
|
77
|
+
const key = Object.freeze({});
|
|
78
|
+
opaqueKeyInternals.set(key, cryptoKey);
|
|
79
|
+
return key;
|
|
80
|
+
}
|
|
81
|
+
function getOpaqueKeyCryptoKey(key) {
|
|
82
|
+
const cryptoKey = opaqueKeyInternals.get(key);
|
|
83
|
+
if (cryptoKey === void 0) throw new Error("invalid opaque key");
|
|
84
|
+
return cryptoKey;
|
|
85
|
+
}
|
|
62
86
|
/**
|
|
63
87
|
* Encodes raw AES key bytes for storage in env vars or secret managers.
|
|
64
88
|
*
|
|
@@ -81,22 +105,22 @@ function decodeOpaqueKey(encoded, format) {
|
|
|
81
105
|
assertOpaqueKeyFormat(format);
|
|
82
106
|
let bytes;
|
|
83
107
|
if (format === "hex") {
|
|
84
|
-
if (encoded.length === 0 || encoded.length % 2 !== 0) throw new
|
|
85
|
-
if (!/^[0-9a-fA-F]+$/.test(encoded)) throw new
|
|
108
|
+
if (encoded.length === 0 || encoded.length % 2 !== 0) throw new IdsError("invalid_key_encoding", "invalid hex key: length must be a positive even number of characters");
|
|
109
|
+
if (!/^[0-9a-fA-F]+$/.test(encoded)) throw new IdsError("invalid_key_encoding", "invalid hex key: expected [0-9a-fA-F] only");
|
|
86
110
|
bytes = decodeHex(encoded);
|
|
87
111
|
} else try {
|
|
88
112
|
bytes = decodeBase64Url(encoded);
|
|
89
113
|
} catch {
|
|
90
|
-
throw new
|
|
114
|
+
throw new IdsError("invalid_key_encoding", "invalid base64url key");
|
|
91
115
|
}
|
|
92
116
|
assertValidAesKeyByteLength(bytes.length);
|
|
93
117
|
return bytes;
|
|
94
118
|
}
|
|
95
119
|
function assertValidAesKeyByteLength(byteLength) {
|
|
96
|
-
if (!validAesKeyByteLengths.has(byteLength)) throw new
|
|
120
|
+
if (!validAesKeyByteLengths.has(byteLength)) throw new IdsError("invalid_key_length", `invalid AES key length: expected 16, 24, or 32 bytes, got ${byteLength}`);
|
|
97
121
|
}
|
|
98
122
|
function assertOpaqueKeyFormat(format) {
|
|
99
|
-
if (format !== "hex" && format !== "base64url") throw new
|
|
123
|
+
if (format !== "hex" && format !== "base64url") throw new IdsError("invalid_key_format", `invalid opaque key format: expected hex or base64url, got '${formatForError(format)}'`);
|
|
100
124
|
}
|
|
101
125
|
function formatForError(value) {
|
|
102
126
|
try {
|
|
@@ -111,28 +135,21 @@ function defaultRng(target) {
|
|
|
111
135
|
crypto.getRandomValues(target);
|
|
112
136
|
}
|
|
113
137
|
/**
|
|
114
|
-
* Imports a raw AES key for use with the Opaque Timestamp codec.
|
|
115
|
-
*
|
|
116
|
-
* @param bytes - Raw key bytes (16, 24, or 32 bytes for AES-128/192/256).
|
|
117
|
-
*/
|
|
118
|
-
function importOpaqueKey(bytes) {
|
|
119
|
-
return crypto.subtle.importKey("raw", bytes, "AES-CBC", false, ["encrypt", "decrypt"]);
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
138
|
* Creates an Opaque Timestamp codec for `brand` (three lowercase a–z characters).
|
|
123
139
|
*
|
|
124
140
|
* @param brand - Entity type brand validated once at construction.
|
|
125
|
-
* @param opts - Required `key`
|
|
141
|
+
* @param opts - Required `key` (an {@link OpaqueKey} from {@link importOpaqueKey}) plus
|
|
142
|
+
* optional `now`, `rng`, and `allowDuplicateBrand` overrides.
|
|
126
143
|
*/
|
|
127
144
|
function createOpaqueTimestampId(brand, opts) {
|
|
128
145
|
validateBrand(brand);
|
|
129
146
|
registerBrand(brand, opts.allowDuplicateBrand);
|
|
130
|
-
const
|
|
147
|
+
const cryptoKey = getOpaqueKeyCryptoKey(opts.key);
|
|
131
148
|
const now = opts.now ?? Date.now;
|
|
132
149
|
const rng = opts.rng ?? defaultRng;
|
|
133
150
|
const prefix = `${brand}_`;
|
|
134
151
|
const wire = wireMethods(prefix);
|
|
135
|
-
const layout = createOpaqueLayoutOps(prefix,
|
|
152
|
+
const layout = createOpaqueLayoutOps(prefix, cryptoKey, rng);
|
|
136
153
|
return {
|
|
137
154
|
generate: () => layout.generateAt(now()),
|
|
138
155
|
generateAt: (date) => layout.generateAt(date.getTime()),
|
|
@@ -145,6 +162,6 @@ function createOpaqueTimestampId(brand, opts) {
|
|
|
145
162
|
};
|
|
146
163
|
}
|
|
147
164
|
//#endregion
|
|
148
|
-
export {
|
|
165
|
+
export { importOpaqueKey as i, decodeOpaqueKey as n, encodeOpaqueKey as r, createOpaqueTimestampId as t };
|
|
149
166
|
|
|
150
|
-
//# sourceMappingURL=opaque-
|
|
167
|
+
//# sourceMappingURL=opaque-uvjOFY_0.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opaque-uvjOFY_0.mjs","names":[],"sources":["../src/layouts/opaque.ts","../src/opaque-key.ts","../src/opaque.ts"],"sourcesContent":["import type { Id, Prefix } from \"../types.js\";\nimport { payloadBytesFromId, toWireId } from \"../wire/envelope.js\";\nimport { payloadBase32Length, payloadByteLength } from \"../wire/invariants.js\";\nimport { readTimestampMs, timestampByteLength, writeTimestamp } from \"../wire/timestamp-bytes.js\";\n\nconst zeroIv = new Uint8Array(payloadByteLength);\nconst pkcsPad = 0x10;\n\nfunction buildPlaintext(ms: number, rng: (target: Uint8Array) => void): Uint8Array {\n const plaintext = new Uint8Array(payloadByteLength);\n writeTimestamp(ms, plaintext);\n rng(plaintext.subarray(timestampByteLength, payloadByteLength));\n return plaintext;\n}\n\nasync function encryptPayload(key: CryptoKey, plaintext: Uint8Array): Promise<Uint8Array> {\n const encrypted = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: \"AES-CBC\", iv: zeroIv },\n key,\n plaintext as Uint8Array<ArrayBuffer>,\n ),\n );\n return encrypted.subarray(0, payloadByteLength);\n}\n\n// AES-CBC strip-and-reconstruct decrypt (ADR-0004). The wire carries only C1\n// (16 bytes); C2 = AES_K(P2 XOR C1) where P2 is the PKCS#7 pad block (0x10×16).\n// Recompute C2 via CBC encrypt of (P2 XOR C1) with IV=0, then decrypt C1‖C2.\nasync function decryptPayload(key: CryptoKey, c1: Uint8Array): Promise<Uint8Array> {\n const c2Input = new Uint8Array(payloadByteLength);\n for (let i = 0; i < payloadByteLength; i++) c2Input[i] = pkcsPad ^ c1[i]!;\n const c2Encrypted = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: \"AES-CBC\", iv: zeroIv },\n key,\n c2Input as Uint8Array<ArrayBuffer>,\n ),\n );\n const ciphertext = new Uint8Array(payloadByteLength * 2);\n ciphertext.set(c1, 0);\n ciphertext.set(c2Encrypted.subarray(0, payloadByteLength), payloadByteLength);\n return new Uint8Array(\n await crypto.subtle.decrypt(\n { name: \"AES-CBC\", iv: zeroIv },\n key,\n ciphertext as Uint8Array<ArrayBuffer>,\n ),\n );\n}\n\nasync function extractTimestampFromId<Brand extends string>(\n prefix: Prefix<Brand>,\n key: CryptoKey,\n id: Id<Brand>,\n): Promise<Date> {\n const plaintext = await decryptPayload(key, payloadBytesFromId(prefix, id));\n return new Date(readTimestampMs(plaintext));\n}\n\n/** Produces a canonical encrypted wire ID. Per-call plaintext/ciphertext buffers —\n * subtle dominates this path; reuse would be safe but not worth pinning to spec detail. */\nasync function generateWireId<Brand extends string>(\n prefix: Prefix<Brand>,\n key: CryptoKey,\n rng: (target: Uint8Array) => void,\n ms: number,\n): Promise<Id<Brand>> {\n const plaintext = buildPlaintext(ms, rng);\n const encrypted = await encryptPayload(key, plaintext);\n return toWireId(prefix, encrypted);\n}\n\n/** Structural placeholder for JSON Schema (encrypt is async). */\nfunction schemaExample<Brand extends string>(prefix: Prefix<Brand>): string {\n return prefix + \"0\".repeat(payloadBase32Length);\n}\n\n/** Layout ops binder for the Opaque Timestamp variant. `extractTimestampFromId` is module-private; the binder exposes `extractTimestamp` for the codec constructor. */\nexport function createOpaqueLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n key: CryptoKey,\n rng: (target: Uint8Array) => void,\n) {\n return {\n generateAt: (ms: number): Promise<Id<Brand>> => generateWireId(prefix, key, rng, ms),\n extractTimestamp: (id: Id<Brand>): Promise<Date> => extractTimestampFromId(prefix, key, id),\n exampleWireId: (): Id<Brand> => schemaExample(prefix) as Id<Brand>,\n };\n}\n","import { decodeBase64Url, decodeHex, encodeBase64Url, encodeHex } from \"./bytes.js\";\nimport { IdsError } from \"./error.js\";\n\n/** Wire encoding for opaque AES key material (not Crockford base32). */\nexport type OpaqueKeyFormat = \"hex\" | \"base64url\";\n\nconst validAesKeyByteLengths = new Set([16, 24, 32]);\n\ndeclare const opaqueKeyBrand: unique symbol;\n\n/**\n * Opaque imported handle for one AES key used by the Opaque Timestamp codec.\n *\n * Holds the underlying `CryptoKey` internally; callers never access it directly.\n * Obtain handles via {@link importOpaqueKey} and pass them to\n * `createOpaqueTimestampId` as the `key` option.\n *\n * Distinct from the `WrappingKey` used by `@smonn/ids/wrapped` — one raw\n * secret must not silently serve both codecs without an explicit import.\n */\nexport type OpaqueKey = {\n readonly [opaqueKeyBrand]: \"OpaqueKey\";\n};\n\nconst opaqueKeyInternals = new WeakMap<OpaqueKey, CryptoKey>();\n\n/**\n * Imports raw AES key bytes into an {@link OpaqueKey} handle for the Opaque\n * Timestamp codec.\n *\n * Accepts 16, 24, or 32 bytes (AES-128 / AES-192 / AES-256 strength).\n * To store or transport key material, use {@link encodeOpaqueKey} /\n * {@link decodeOpaqueKey} (`\"hex\"` or `\"base64url\"` — not Crockford base32).\n *\n * @param bytes - 16, 24, or 32 raw key bytes.\n */\nexport async function importOpaqueKey(bytes: Uint8Array): Promise<OpaqueKey> {\n assertValidAesKeyByteLength(bytes.length);\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n bytes as Uint8Array<ArrayBuffer>,\n \"AES-CBC\",\n false,\n [\"encrypt\", \"decrypt\"],\n );\n const key = Object.freeze({}) as OpaqueKey;\n opaqueKeyInternals.set(key, cryptoKey);\n return key;\n}\n\nexport function getOpaqueKeyCryptoKey(key: OpaqueKey): CryptoKey {\n const cryptoKey = opaqueKeyInternals.get(key);\n if (cryptoKey === undefined) {\n throw new Error(\"invalid opaque key\");\n }\n return cryptoKey;\n}\n\n/**\n * Encodes raw AES key bytes for storage in env vars or secret managers.\n *\n * @param bytes - 16, 24, or 32 raw key bytes (AES-128/192/256).\n * @param format - `hex` (lowercase) or `base64url`.\n */\nexport function encodeOpaqueKey(bytes: Uint8Array, format: OpaqueKeyFormat): string {\n assertOpaqueKeyFormat(format);\n assertValidAesKeyByteLength(bytes.length);\n if (format === \"hex\") return encodeHex(bytes);\n return encodeBase64Url(bytes);\n}\n\n/**\n * Decodes key material emitted by `encodeOpaqueKey` (or `ids keygen`) back to raw bytes.\n *\n * @param encoded - Hex or base64url string.\n * @param format - Must match how the string was encoded.\n */\nexport function decodeOpaqueKey(encoded: string, format: OpaqueKeyFormat): Uint8Array {\n assertOpaqueKeyFormat(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 assertValidAesKeyByteLength(bytes.length);\n return bytes;\n}\n\nfunction assertValidAesKeyByteLength(byteLength: number): void {\n if (!validAesKeyByteLengths.has(byteLength)) {\n throw new IdsError(\n \"invalid_key_length\",\n `invalid AES key length: expected 16, 24, or 32 bytes, got ${byteLength}`,\n );\n }\n}\n\nfunction assertOpaqueKeyFormat(format: unknown): asserts format is OpaqueKeyFormat {\n if (format !== \"hex\" && format !== \"base64url\") {\n throw new IdsError(\n \"invalid_key_format\",\n `invalid opaque 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 { createOpaqueLayoutOps } from \"./layouts/opaque.js\";\nimport { getOpaqueKeyCryptoKey, type OpaqueKey } from \"./opaque-key.js\";\nimport { registerBrand } from \"./registry.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"./types.js\";\nimport { wireMethods } from \"./wire/codec-shell.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 } from \"./error.js\";\nexport {\n decodeOpaqueKey,\n encodeOpaqueKey,\n importOpaqueKey,\n type OpaqueKey,\n type OpaqueKeyFormat,\n} from \"./opaque-key.js\";\n\n/**\n * Configuration options for an Opaque Timestamp codec instance.\n */\nexport type OpaqueTimestampOptions = {\n /**\n * {@link OpaqueKey} handle for AES-CBC encryption and decryption.\n * Obtain via {@link importOpaqueKey}.\n */\n key: OpaqueKey;\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now?: () => number;\n /** Writes random bytes into `target` for ID generation. 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 * A brand-scoped codec for generating and validating Opaque Timestamp IDs.\n *\n * Same wire shape as the Timestamp codec (`{brand}_` + 26 base32 chars) but the\n * payload is AES-CBC encrypted. `generate`, `generateAt`, and `extractTimestamp`\n * are async; parsing methods are sync. No `minIdForTime` / `maxIdForTime` —\n * encrypted payloads do not sort by creation time.\n */\nexport type OpaqueTimestampCodec<Brand extends string> = {\n /** Produces a new canonical encrypted ID using the codec's `now` and `rng`. */\n generate(): Promise<Id<Brand>>;\n /** Produces a new canonical encrypted ID with timestamp bytes from `date`. Throws on invalid dates. */\n generateAt(date: Date): Promise<Id<Brand>>;\n /**\n * Strict type guard: `true` only for already-canonical strings for this brand.\n * For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.\n */\n is(value: unknown): value is Id<Brand>;\n /**\n * Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.\n */\n parse(value: unknown): Id<Brand>;\n /**\n * Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.\n */\n safeParse(value: unknown): ParseResult<Brand>;\n /**\n * Decrypts and decodes the creation `Date` from an `Id<Brand>`. Trusts the type — use `safeParse()` at boundaries first. See ADR-0002.\n */\n extractTimestamp(id: Id<Brand>): Promise<Date>;\n /** JSON Schema for the canonical wire form (`example` is a structural placeholder). */\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 * Creates an Opaque Timestamp codec for `brand` (three lowercase a–z characters).\n *\n * @param brand - Entity type brand validated once at construction.\n * @param opts - Required `key` (an {@link OpaqueKey} from {@link importOpaqueKey}) plus\n * optional `now`, `rng`, and `allowDuplicateBrand` overrides.\n */\nexport function createOpaqueTimestampId<Brand extends string>(\n brand: Brand,\n opts: OpaqueTimestampOptions,\n): OpaqueTimestampCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n const cryptoKey = getOpaqueKeyCryptoKey(opts.key);\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 = createOpaqueLayoutOps(prefix, cryptoKey, rng);\n\n return {\n generate: () => layout.generateAt(now()),\n generateAt: (date: Date) => layout.generateAt(date.getTime()),\n is: wire.is,\n parse: wire.parse,\n safeParse: wire.safeParse,\n extractTimestamp: layout.extractTimestamp,\n toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId()),\n \"~standard\": wire[\"~standard\"],\n };\n}\n"],"mappings":"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"}
|
package/dist/opaque.d.mts
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
+
import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-DTr4i6Ic.mjs";
|
|
1
2
|
import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, t as Id } from "./types-g7CiQDyE.mjs";
|
|
2
3
|
|
|
3
4
|
//#region src/opaque-key.d.ts
|
|
4
5
|
/** Wire encoding for opaque AES key material (not Crockford base32). */
|
|
5
6
|
type OpaqueKeyFormat = "hex" | "base64url";
|
|
7
|
+
declare const opaqueKeyBrand: unique symbol;
|
|
8
|
+
/**
|
|
9
|
+
* Opaque imported handle for one AES key used by the Opaque Timestamp codec.
|
|
10
|
+
*
|
|
11
|
+
* Holds the underlying `CryptoKey` internally; callers never access it directly.
|
|
12
|
+
* Obtain handles via {@link importOpaqueKey} and pass them to
|
|
13
|
+
* `createOpaqueTimestampId` as the `key` option.
|
|
14
|
+
*
|
|
15
|
+
* Distinct from the `WrappingKey` used by `@smonn/ids/wrapped` — one raw
|
|
16
|
+
* secret must not silently serve both codecs without an explicit import.
|
|
17
|
+
*/
|
|
18
|
+
type OpaqueKey = {
|
|
19
|
+
readonly [opaqueKeyBrand]: "OpaqueKey";
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Imports raw AES key bytes into an {@link OpaqueKey} handle for the Opaque
|
|
23
|
+
* Timestamp codec.
|
|
24
|
+
*
|
|
25
|
+
* Accepts 16, 24, or 32 bytes (AES-128 / AES-192 / AES-256 strength).
|
|
26
|
+
* To store or transport key material, use {@link encodeOpaqueKey} /
|
|
27
|
+
* {@link decodeOpaqueKey} (`"hex"` or `"base64url"` — not Crockford base32).
|
|
28
|
+
*
|
|
29
|
+
* @param bytes - 16, 24, or 32 raw key bytes.
|
|
30
|
+
*/
|
|
31
|
+
declare function importOpaqueKey(bytes: Uint8Array): Promise<OpaqueKey>;
|
|
6
32
|
/**
|
|
7
33
|
* Encodes raw AES key bytes for storage in env vars or secret managers.
|
|
8
34
|
*
|
|
@@ -23,7 +49,11 @@ declare function decodeOpaqueKey(encoded: string, format: OpaqueKeyFormat): Uint
|
|
|
23
49
|
* Configuration options for an Opaque Timestamp codec instance.
|
|
24
50
|
*/
|
|
25
51
|
type OpaqueTimestampOptions = {
|
|
26
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* {@link OpaqueKey} handle for AES-CBC encryption and decryption.
|
|
54
|
+
* Obtain via {@link importOpaqueKey}.
|
|
55
|
+
*/
|
|
56
|
+
key: OpaqueKey; /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */
|
|
27
57
|
now?: () => number; /** Writes random bytes into `target` for ID generation. Defaults to `crypto.getRandomValues`. */
|
|
28
58
|
rng?: (target: Uint8Array) => void; /** If true, silences the duplicate-brand warning in non-production environments. */
|
|
29
59
|
allowDuplicateBrand?: boolean;
|
|
@@ -60,18 +90,13 @@ type OpaqueTimestampCodec<Brand extends string> = {
|
|
|
60
90
|
readonly "~standard": StandardSchemaProps<Brand>;
|
|
61
91
|
};
|
|
62
92
|
/**
|
|
63
|
-
* Imports a raw AES key for use with the Opaque Timestamp codec.
|
|
64
|
-
*
|
|
65
|
-
* @param bytes - Raw key bytes (16, 24, or 32 bytes for AES-128/192/256).
|
|
66
|
-
*/
|
|
67
|
-
declare function importOpaqueKey(bytes: Uint8Array): Promise<CryptoKey>;
|
|
68
|
-
/**
|
|
69
93
|
* Creates an Opaque Timestamp codec for `brand` (three lowercase a–z characters).
|
|
70
94
|
*
|
|
71
95
|
* @param brand - Entity type brand validated once at construction.
|
|
72
|
-
* @param opts - Required `key`
|
|
96
|
+
* @param opts - Required `key` (an {@link OpaqueKey} from {@link importOpaqueKey}) plus
|
|
97
|
+
* optional `now`, `rng`, and `allowDuplicateBrand` overrides.
|
|
73
98
|
*/
|
|
74
99
|
declare function createOpaqueTimestampId<Brand extends string>(brand: Brand, opts: OpaqueTimestampOptions): OpaqueTimestampCodec<Brand>;
|
|
75
100
|
//#endregion
|
|
76
|
-
export { type OpaqueKeyFormat, OpaqueTimestampCodec, OpaqueTimestampOptions, createOpaqueTimestampId, decodeOpaqueKey, encodeOpaqueKey, importOpaqueKey };
|
|
101
|
+
export { IdsError, type IdsErrorCode, type OpaqueKey, type OpaqueKeyFormat, OpaqueTimestampCodec, OpaqueTimestampOptions, createOpaqueTimestampId, decodeOpaqueKey, encodeOpaqueKey, importOpaqueKey, isIdsError };
|
|
77
102
|
//# sourceMappingURL=opaque.d.mts.map
|
package/dist/opaque.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opaque.d.mts","names":[],"sources":["../src/opaque-key.ts","../src/opaque.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"opaque.d.mts","names":[],"sources":["../src/opaque-key.ts","../src/opaque.ts"],"mappings":";;;;;KAIY,eAAA;AAAA,cAIE,cAAA;;AAJd;;;;AAAY;AAA0B;;;;KAgB1B,SAAA;EAAA,UACA,cAAA;AAAA;;;AAAA;AAeZ;;;;;;;iBAAsB,eAAA,CAAgB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,SAAA;;;;;AAAA;AA4BlE;iBAAgB,eAAA,CAAgB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,eAAA;;;;;;;iBAa3C,eAAA,CAAgB,OAAA,UAAiB,MAAA,EAAQ,eAAA,GAAkB,UAAA;;;;;;KCzD/D,sBAAA;EDhB0B;;;;ECqBpC,GAAA,EAAK,SAAA,EDLP;ECOE,GAAA;EAEA,GAAA,IAAO,MAAA,EAAQ,UAAA,WDRL;ECUV,mBAAA;AAAA;;;;;;;;;KAWU,oBAAA;iFAEV,QAAA,IAAY,OAAA,CAAQ,EAAA,CAAG,KAAA,IDRyC;ECUhE,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,EAAA,CAAG,KAAA;EDkBrB;;;;ECbd,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;;;;EAIhC,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;EDsB5B;;;EClBE,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;;EAIvC,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,OAAA,CAAQ,IAAA,GDcgC;ECZzE,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;AA/C5C;;;;;;;AAAA,iBA6DgB,uBAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,EAAM,sBAAA,GACL,oBAAA,CAAqB,KAAA"}
|
package/dist/opaque.mjs
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
|
|
2
|
+
import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-uvjOFY_0.mjs";
|
|
3
|
+
export { IdsError, createOpaqueTimestampId, decodeOpaqueKey, encodeOpaqueKey, importOpaqueKey, isIdsError };
|