@smonn/ids 0.7.0 → 0.9.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 +129 -20
- package/dist/adapter-types-BY-wrYYB.mjs +27 -0
- package/dist/adapter-types-BY-wrYYB.mjs.map +1 -0
- package/dist/adapter-types-unUcmMXC.d.mts +20 -0
- package/dist/adapter-types-unUcmMXC.d.mts.map +1 -0
- package/dist/cli.mjs +213 -47
- package/dist/cli.mjs.map +1 -1
- package/dist/{codec-shell-DH-UO4UR.mjs → codec-shell-C7_B4oum.mjs} +4 -3
- package/dist/codec-shell-C7_B4oum.mjs.map +1 -0
- package/dist/{drizzle-CeSni5PB.d.mts → drizzle-CHtyDXpv.d.mts} +4 -15
- package/dist/drizzle-CHtyDXpv.d.mts.map +1 -0
- package/dist/drizzle.d.mts +3 -2
- package/dist/drizzle.mjs +2 -3
- package/dist/drizzle.mjs.map +1 -1
- package/dist/express.d.mts +2 -5
- package/dist/express.d.mts.map +1 -1
- package/dist/express.mjs +3 -8
- package/dist/express.mjs.map +1 -1
- package/dist/fastify.d.mts +12 -6
- package/dist/fastify.d.mts.map +1 -1
- package/dist/fastify.mjs +10 -8
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.d.mts +2 -5
- package/dist/hono.d.mts.map +1 -1
- package/dist/hono.mjs +3 -8
- package/dist/hono.mjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/key-material-DUHhmMq-.mjs +137 -0
- package/dist/key-material-DUHhmMq-.mjs.map +1 -0
- package/dist/kysely.d.mts +1 -1
- package/dist/kysely.d.mts.map +1 -1
- package/dist/{opaque-uvjOFY_0.mjs → opaque-BQOlZ2oD.mjs} +8 -44
- package/dist/opaque-BQOlZ2oD.mjs.map +1 -0
- package/dist/opaque.d.mts +8 -0
- package/dist/opaque.d.mts.map +1 -1
- package/dist/opaque.mjs +1 -1
- package/dist/prisma.d.mts +3 -16
- package/dist/prisma.d.mts.map +1 -1
- package/dist/prisma.mjs +2 -3
- package/dist/prisma.mjs.map +1 -1
- package/dist/{reverse-BgFU6JHw.mjs → reverse-C12D1btB.mjs} +4 -6
- package/dist/reverse-C12D1btB.mjs.map +1 -0
- package/dist/reverse.d.mts.map +1 -1
- package/dist/reverse.mjs +1 -1
- package/dist/rng-CPJOx_nE.mjs +9 -0
- package/dist/rng-CPJOx_nE.mjs.map +1 -0
- package/dist/signed-CwqKTFaQ.mjs +207 -0
- package/dist/signed-CwqKTFaQ.mjs.map +1 -0
- package/dist/signed.d.mts +112 -1
- package/dist/signed.d.mts.map +1 -1
- package/dist/signed.mjs +2 -99
- package/dist/{timestamp-B5_UCzc6.mjs → timestamp-BjIMQkJf.mjs} +3 -3
- package/dist/{timestamp-B5_UCzc6.mjs.map → timestamp-BjIMQkJf.mjs.map} +1 -1
- package/dist/{timestamp-bytes-BBY7JI33.mjs → timestamp-bytes-Bbg6Y66Z.mjs} +2 -2
- package/dist/{timestamp-bytes-BBY7JI33.mjs.map → timestamp-bytes-Bbg6Y66Z.mjs.map} +1 -1
- package/dist/{wrapped-0vL72Nje.mjs → wrapped-DKOsN_dq.mjs} +16 -50
- package/dist/wrapped-DKOsN_dq.mjs.map +1 -0
- package/dist/wrapped.d.mts.map +1 -1
- package/dist/wrapped.mjs +1 -1
- package/package.json +1 -1
- package/dist/adapter-types-oHCCSgOO.d.mts +0 -12
- package/dist/adapter-types-oHCCSgOO.d.mts.map +0 -1
- package/dist/bytes-lhzKVaBV.mjs +0 -53
- package/dist/bytes-lhzKVaBV.mjs.map +0 -1
- package/dist/codec-shell-DH-UO4UR.mjs.map +0 -1
- package/dist/drizzle-CeSni5PB.d.mts.map +0 -1
- package/dist/opaque-uvjOFY_0.mjs.map +0 -1
- package/dist/reverse-BgFU6JHw.mjs.map +0 -1
- package/dist/signed.mjs.map +0 -1
- package/dist/wrapped-0vL72Nje.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -391,6 +391,28 @@ Encryption is AES-CBC with a zero IV. That's deliberately safe here because the
|
|
|
391
391
|
|
|
392
392
|
To store or transport key material outside the library, `encodeOpaqueKey` / `decodeOpaqueKey` round-trip raw bytes in `hex` or `base64url` — distinct from the Crockford base32 used in ID payloads. The CLI's `keygen` subcommand emits keys in this format (see [CLI](#cli)).
|
|
393
393
|
|
|
394
|
+
**Rotating the Opaque key.** Rotation is **forward-only and caller-tracked** — the codec deliberately has no key ring. The key feeds only `generate` and `extractTimestamp`; `parse`, `safeParse`, `is`, and `toJsonSchema` work on the wire form and never touch it, so rotating forward is nearly free: point new writes at a new key and keep the old key only to read old IDs' timestamps. Because the payload is unauthenticated ([ADR-0004](./docs/adr/0004-aes-cbc-strip-trick.md)) and carries no key id ([ADR-0007](./docs/adr/0007-wire-indistinguishable-codec-variants.md)), the library **cannot** trial a ring to pick the right key for you — a wrong key yields a plausible but wrong timestamp, never an error. You hold one codec per _key epoch_ and select it from your own record of which epoch minted each ID:
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
// One codec instance per key epoch. You — not the library — track which epoch
|
|
398
|
+
// minted each ID (a key-epoch column, tenant→key map, created-at cutover). The
|
|
399
|
+
// epoch CANNOT be read from the ID itself. `allowDuplicateBrand` silences the
|
|
400
|
+
// per-brand registry warning (ADR-0007): multiple instances of one brand is the
|
|
401
|
+
// legitimate case that flag exists for.
|
|
402
|
+
const codecs = new Map([
|
|
403
|
+
[1, createOpaqueTimestampId("inv", { key: keyV1, allowDuplicateBrand: true })],
|
|
404
|
+
[2, createOpaqueTimestampId("inv", { key: keyV2, allowDuplicateBrand: true })], // current epoch
|
|
405
|
+
]);
|
|
406
|
+
|
|
407
|
+
const id = await codecs.get(2)!.generate(); // new IDs use the current epoch's key
|
|
408
|
+
|
|
409
|
+
// Reading an old ID: look up its epoch from your records, pick that codec.
|
|
410
|
+
const epoch = await db.keyEpochFor(someOldId); // your bookkeeping, not the ID
|
|
411
|
+
await codecs.get(epoch)!.extractTimestamp(someOldId);
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
If you need transparent, correctness-grade rotation where the library trials a key ring and a wrong key is _rejected_, that's the Signed Timestamp codec's job ([ADR-0012](./docs/adr/0012-signed-timestamp-construction.md)) — its HMAC tag gives a verifiable ring. The Opaque codec trades that away for confidentiality. See [ADR-0013](./docs/adr/0013-opaque-key-rotation.md).
|
|
415
|
+
|
|
394
416
|
### "Newest-first IDs for descending range scans"
|
|
395
417
|
|
|
396
418
|
Most KV stores (DynamoDB, Cloud Datastore, range-scan KV) only support forward lexicographic scans natively — reading the most recent entries first would otherwise require a full reverse scan or a separate sort step. The Reverse Timestamp codec solves this by bitwise-inverting the 48-bit timestamp field before encoding, so newer IDs sort lexicographically before older ones.
|
|
@@ -425,12 +447,13 @@ No key material is required. The inversion is a deterministic byte transform; `g
|
|
|
425
447
|
|
|
426
448
|
## Choosing a codec variant
|
|
427
449
|
|
|
428
|
-
| Codec | Import | Sort direction | Key required | Timestamp extractable | Range query support
|
|
429
|
-
| ----------------- | -------------------- | ------------------------- | ------------------ | -------------------------- |
|
|
430
|
-
| Timestamp | `@smonn/ids` | Ascending (oldest-first) | No | Always
|
|
431
|
-
| Reverse Timestamp | `@smonn/ids/reverse` | Descending (newest-first) | No | Always
|
|
432
|
-
|
|
|
433
|
-
|
|
|
450
|
+
| Codec | Import | Sort direction | Key required | Timestamp extractable | Range query support |
|
|
451
|
+
| ----------------- | -------------------- | ------------------------- | ------------------ | -------------------------- | ---------------------------------------------------------------------------- |
|
|
452
|
+
| Timestamp | `@smonn/ids` | Ascending (oldest-first) | No | Always (plaintext) | `minIdForTime(t_old)` → `maxIdForTime(t_new)` |
|
|
453
|
+
| Reverse Timestamp | `@smonn/ids/reverse` | Descending (newest-first) | No | Always (plaintext) | `minIdForTime(t_new)` → `maxIdForTime(t_old)` (bounds flipped) |
|
|
454
|
+
| Signed Timestamp | `@smonn/ids/signed` | Ascending (oldest-first) | Yes (signing key) | Always (plaintext) | `minIdForTime(t_old)` → `maxIdForTime(t_new)` (sentinels carry no valid tag) |
|
|
455
|
+
| Opaque Timestamp | `@smonn/ids/opaque` | None (encrypted) | Yes (AES key) | With key only | None — encrypted payloads do not sort by time |
|
|
456
|
+
| Wrapped key | `@smonn/ids/wrapped` | None | Yes (wrapping key) | N/A — not timestamp-family | None |
|
|
434
457
|
|
|
435
458
|
## What this is **not** for
|
|
436
459
|
|
|
@@ -482,11 +505,15 @@ import {
|
|
|
482
505
|
importSigningKey, // (bytes: Uint8Array) => Promise<SigningKey>
|
|
483
506
|
encodeSigningKey, // (bytes: Uint8Array, format: SigningKeyFormat) => string
|
|
484
507
|
decodeSigningKey, // (encoded: string, format: SigningKeyFormat) => Uint8Array
|
|
508
|
+
createSignedTimestampId, // (brand: string, opts: SignedTimestampOptions) => SignedTimestampCodec<Brand>
|
|
485
509
|
IdsError, // re-exported from @smonn/ids/signed for convenience
|
|
486
510
|
isIdsError, // re-exported from @smonn/ids/signed for convenience
|
|
487
511
|
type SigningKey, // opaque SigningKey handle (HKDF-derived)
|
|
488
512
|
type SigningKeyFormat, // "hex" | "base64url"
|
|
489
513
|
type IdsErrorCode, // re-exported from @smonn/ids/signed for convenience
|
|
514
|
+
type SignedTimestampCodec, // returned by createSignedTimestampId
|
|
515
|
+
type SignedTimestampOptions, // { keys: SigningKey[], now?, rng?, allowDuplicateBrand? } constructor options
|
|
516
|
+
type SafeVerifyResult, // { ok: true, id: Id<Brand> } | { ok: false, error: ParseError | "verification_failed" }
|
|
490
517
|
} from "@smonn/ids/signed";
|
|
491
518
|
|
|
492
519
|
import {
|
|
@@ -549,6 +576,84 @@ import {
|
|
|
549
576
|
} from "@smonn/ids/fastify";
|
|
550
577
|
```
|
|
551
578
|
|
|
579
|
+
`@smonn/ids/signed` ships the Signed Timestamp codec — it keeps the 48-bit timestamp **readable and sortable** like the Timestamp codec, but replaces half of the random tail with a truncated HMAC tag, making IDs **tamper-evident and verifiable without a database lookup**. This adds **integrity, not confidentiality** — the opposite security axis from the Opaque Timestamp codec, which hides the timestamp but has no auth tag.
|
|
580
|
+
|
|
581
|
+
The canonical use case is **share links**: embed a Signed Timestamp ID in a share URL and verify it on receipt without a database roundtrip.
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
import { createSignedTimestampId, importSigningKey } from "@smonn/ids/signed";
|
|
585
|
+
|
|
586
|
+
const key = await importSigningKey(new Uint8Array(32));
|
|
587
|
+
const shares = createSignedTimestampId("shr", { keys: [key] });
|
|
588
|
+
|
|
589
|
+
const id = await shares.generate(); // "shr_…", timestamp readable and sortable
|
|
590
|
+
shares.extractTimestamp(id); // Date — sync, timestamp is plaintext
|
|
591
|
+
|
|
592
|
+
await shares.verify(id); // passes; throws IdsError verification_failed on tamper
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
The non-throwing `safeVerify` path accepts untrusted input, structurally parses first, then verifies — without throwing:
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
const result = await shares.safeVerify(req.params.shareId);
|
|
599
|
+
|
|
600
|
+
if (!result.ok) {
|
|
601
|
+
if (result.error === "verification_failed") return 403; // tampered or wrong key
|
|
602
|
+
return 400; // malformed ID
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const { id } = result; // Id<"shr">, canonical
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
`safeVerify` returns one of:
|
|
609
|
+
|
|
610
|
+
```ts
|
|
611
|
+
// Success
|
|
612
|
+
{
|
|
613
|
+
ok: true;
|
|
614
|
+
id: Id<Brand>;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Structural parse failure (wrong brand, invalid base32, etc.)
|
|
618
|
+
{
|
|
619
|
+
ok: false;
|
|
620
|
+
error: "not_string" | "invalid_prefix" | "invalid_base32";
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Structurally valid but tag mismatch (tampered, wrong keyring, or revoked key)
|
|
624
|
+
{
|
|
625
|
+
ok: false;
|
|
626
|
+
error: "verification_failed";
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
**False-accept bound.** With a signing keyring of `n` entries, an attacker's per-`verify` success probability is approximately `n / 2⁴⁰`. Verification is online-only — the signing key lives server-side, so offline guessing is not possible.
|
|
631
|
+
|
|
632
|
+
**Key handling.** Import signing key material via `importSigningKey(bytes)` from raw bytes (16, 24, or 32 bytes). Signing-key material is a **separate secret domain** from Opaque keys and Wrapping keys — same `hex` / `base64url` encoded-format conventions, but a distinct `SigningKey` handle and HKDF label so one raw secret cannot silently serve multiple codecs.
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
import { encodeSigningKey, decodeSigningKey } from "@smonn/ids/signed";
|
|
636
|
+
|
|
637
|
+
const encoded = encodeSigningKey(rawBytes, "base64url"); // string
|
|
638
|
+
const decoded = decodeSigningKey(encoded, "base64url"); // Uint8Array
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
**Managing the signing keyring.** Pass a non-empty ordered list of signing keys at construction. The first entry is the _current_ key — the only one `generate` / `generateAt` sign with. `verify` / `safeVerify` trial every entry in order until the tag matches, so IDs signed under any listed key remain verifiable. Removing an entry from the list revokes all IDs signed under it.
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
const oldKey = await importSigningKey(rawOldSecret);
|
|
645
|
+
const newKey = await importSigningKey(rawNewSecret);
|
|
646
|
+
|
|
647
|
+
// Before rotation: only oldKey in the ring
|
|
648
|
+
const legacy = createSignedTimestampId("shr", { keys: [oldKey] });
|
|
649
|
+
const id = await legacy.generate();
|
|
650
|
+
|
|
651
|
+
// After rotation: newKey is current; oldKey is still accepted on verify
|
|
652
|
+
const rotated = createSignedTimestampId("shr", { keys: [newKey, oldKey] });
|
|
653
|
+
await rotated.verify(id); // succeeds — tried oldKey and matched
|
|
654
|
+
await rotated.generate(); // signs with newKey
|
|
655
|
+
```
|
|
656
|
+
|
|
552
657
|
`@smonn/ids/wrapped` ships the Wrapped key codec for `u32`, `i32`, `u64`, and `i64` lookup keys. `wrap(lookupKey)` returns a public ID; `unwrap(id)` verifies the payload and returns the lookup key; `safeUnwrap(input)` is the non-throwing path for untrusted input.
|
|
553
658
|
|
|
554
659
|
**Integer kinds and value types.** The 32-bit kinds (`u32`, `i32`) use safe JavaScript `number` values in their fixed-width ranges. The 64-bit kinds (`u64`, `i64`) always use `bigint` — even when the magnitude would fit in a `number` — to prevent silent truncation or sign erasure.
|
|
@@ -638,23 +743,27 @@ const { id, lookupKey } = result; // Id<"inv">, number
|
|
|
638
743
|
|
|
639
744
|
### Codec methods
|
|
640
745
|
|
|
641
|
-
| Method | `TimestampCodec<Brand>` | `ReverseTimestampCodec<Brand>` | `OpaqueTimestampCodec<Brand>` | `WrappedKeyCodec<Brand, Kind>` | Description |
|
|
642
|
-
| ---------------------- | ----------------------- | ------------------------------ | ----------------------------- | ------------------------------ | ----------------------------------------------------------------------------- |
|
|
643
|
-
| `generate()` | sync | sync | async | — | Produce a fresh ID |
|
|
644
|
-
| `generateAt(date)` | sync | sync | async | — | Produce a fresh ID with timestamp bytes from `date` (for backfills) |
|
|
645
|
-
| `
|
|
646
|
-
| `
|
|
647
|
-
| `
|
|
648
|
-
| `
|
|
649
|
-
| `
|
|
650
|
-
| `
|
|
651
|
-
| `
|
|
652
|
-
| `
|
|
653
|
-
| `
|
|
654
|
-
| `
|
|
746
|
+
| Method | `TimestampCodec<Brand>` | `ReverseTimestampCodec<Brand>` | `OpaqueTimestampCodec<Brand>` | `SignedTimestampCodec<Brand>` | `WrappedKeyCodec<Brand, Kind>` | Description |
|
|
747
|
+
| ---------------------- | ----------------------- | ------------------------------ | ----------------------------- | ----------------------------- | ------------------------------ | ----------------------------------------------------------------------------- |
|
|
748
|
+
| `generate()` | sync | sync | async | async | — | Produce a fresh ID |
|
|
749
|
+
| `generateAt(date)` | sync | sync | async | async | — | Produce a fresh ID with timestamp bytes from `date` (for backfills) |
|
|
750
|
+
| `verify(id)` | — | — | — | async | — | Verify the HMAC tag; throws `IdsError` `verification_failed` on mismatch |
|
|
751
|
+
| `safeVerify(input)` | — | — | — | async | — | Non-throwing: structurally parse then verify; returns parse or verify error |
|
|
752
|
+
| `wrap(lookupKey)` | — | — | — | — | async | Wrap a lookup key into a public ID using the current wrapping key |
|
|
753
|
+
| `unwrap(id)` | — | — | — | — | async | Verify and recover the lookup key; throws on verification failure |
|
|
754
|
+
| `safeUnwrap(input)` | — | — | — | — | async | Non-throwing: structurally parse then verify; returns parse or verify error |
|
|
755
|
+
| `is(value)` | sync | sync | sync | sync | sync | Strict type guard: `true` only for already-canonical strings |
|
|
756
|
+
| `parse(value)` | sync | sync | sync | sync | sync | Lenient: normalise to canonical, or throw |
|
|
757
|
+
| `safeParse(value)` | sync | sync | sync | sync | sync | Lenient: normalise to canonical, or return `{ ok: false, error }` |
|
|
758
|
+
| `extractTimestamp(id)` | sync | sync | async | sync | — | Decode the creation `Date` from an `Id<Brand>` (trusts the type) |
|
|
759
|
+
| `minIdForTime(date)` | sync | sync † | — | sync ‡ | — | Tight lower bound for any ID generated at `date` (for range queries) |
|
|
760
|
+
| `maxIdForTime(date)` | sync | sync † | — | sync ‡ | — | Tight upper bound for any ID generated at `date` (for range queries) |
|
|
761
|
+
| `toJsonSchema()` | sync | sync | sync | sync | sync | JSON Schema (`type`/`pattern`/`description`/`example`) for the canonical form |
|
|
655
762
|
|
|
656
763
|
† Under the Reverse Timestamp codec, a newer timestamp maps to a lexicographically smaller ID — pass `minIdForTime(t_new)` as the lower bound and `maxIdForTime(t_old)` as the upper bound for a [t_old, t_new] scan.
|
|
657
764
|
|
|
765
|
+
‡ Signed Timestamp sentinels carry no valid HMAC tag and are not verifiable — they exist only for indexed range scans, not as real IDs.
|
|
766
|
+
|
|
658
767
|
## ORM adapters
|
|
659
768
|
|
|
660
769
|
### Drizzle (`@smonn/ids/drizzle`)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { t as IdsError } from "./error-Cp5qYZcv.mjs";
|
|
2
|
+
//#region src/adapter-types.ts
|
|
3
|
+
/** Parses `value` as `Id<Brand>` via `codec.safeParse`; throws `IdsError("invalid_id")` on failure. Shared read helper for ORM adapters. */
|
|
4
|
+
function readIdColumn(codec, value) {
|
|
5
|
+
const result = codec.safeParse(value);
|
|
6
|
+
if (!result.ok) throw new IdsError("invalid_id", `invalid ID from database: ${result.error}`, { cause: result.error });
|
|
7
|
+
return result.id;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Maps a `ParseError` to `{ reason, status }` for web adapter failure handling.
|
|
11
|
+
*
|
|
12
|
+
* - `invalid_prefix` → `brand_mismatch` / default 404
|
|
13
|
+
* - anything else → `malformed` / default 400
|
|
14
|
+
* - `options.status[reason]` overrides the default for that reason
|
|
15
|
+
*/
|
|
16
|
+
function resolveIdParamFailure(error, options) {
|
|
17
|
+
const reason = error === "invalid_prefix" ? "brand_mismatch" : "malformed";
|
|
18
|
+
const defaultStatus = reason === "brand_mismatch" ? 404 : 400;
|
|
19
|
+
return {
|
|
20
|
+
reason,
|
|
21
|
+
status: options?.status?.[reason] ?? defaultStatus
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { resolveIdParamFailure as n, readIdColumn as t };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=adapter-types-BY-wrYYB.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-types-BY-wrYYB.mjs","names":[],"sources":["../src/adapter-types.ts"],"sourcesContent":["import { IdsError } from \"./error.js\";\nimport type { Id, ParseError, ParseResult } from \"./types.js\";\n\n/** Discriminated failure value passed to `onError` and emitted to the framework's error handler. */\nexport type IdParamFailure =\n | { readonly reason: \"brand_mismatch\"; readonly status: number }\n | { readonly reason: \"malformed\"; readonly status: number };\n\n/** Minimum structural type required by web and ORM adapters. Any codec variant satisfies this — all expose `safeParse`. Adapters only ever call `safeParse` — never key-dependent methods like `extractTimestamp`, `wrap`, or `unwrap`. */\nexport type IdCodec<Brand extends string> = {\n safeParse(value: unknown): ParseResult<Brand>;\n};\n\n/** Re-exported from ORM adapter subpaths (`@smonn/ids/drizzle`, `@smonn/ids/prisma`, `@smonn/ids/kysely`) under the public name; structurally identical to {@link IdCodec}. */\nexport type IdColumnCodec<Brand extends string> = IdCodec<Brand>;\n\n/** Parses `value` as `Id<Brand>` via `codec.safeParse`; throws `IdsError(\"invalid_id\")` on failure. Shared read helper for ORM adapters. */\nexport function readIdColumn<Brand extends string>(\n codec: IdCodec<Brand>,\n value: unknown,\n): 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 * Maps a `ParseError` to `{ reason, status }` for web adapter failure handling.\n *\n * - `invalid_prefix` → `brand_mismatch` / default 404\n * - anything else → `malformed` / default 400\n * - `options.status[reason]` overrides the default for that reason\n */\nexport function resolveIdParamFailure(\n error: ParseError,\n options?: { status?: { brand_mismatch?: number; malformed?: number } },\n): IdParamFailure {\n const reason = 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 return { reason, status };\n}\n"],"mappings":";;;AAiBA,SAAgB,aACd,OACA,OACW;CACX,MAAM,SAAS,MAAM,UAAU,KAAK;CACpC,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,SAAS,cAAc,6BAA6B,OAAO,SAAS,EAC5E,OAAO,OAAO,MAChB,CAAC;CAEH,OAAO,OAAO;AAChB;;;;;;;;AASA,SAAgB,sBACd,OACA,SACgB;CAChB,MAAM,SAAS,UAAU,mBAAoB,mBAA8B;CAC3E,MAAM,gBAAgB,WAAW,mBAAmB,MAAM;CAE1D,OAAO;EAAE;EAAQ,QADF,SAAS,SAAS,WAAW;CACpB;AAC1B"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { i as ParseResult } from "./types-g7CiQDyE.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/adapter-types.d.ts
|
|
4
|
+
/** Discriminated failure value passed to `onError` and emitted to the framework's error handler. */
|
|
5
|
+
type IdParamFailure = {
|
|
6
|
+
readonly reason: "brand_mismatch";
|
|
7
|
+
readonly status: number;
|
|
8
|
+
} | {
|
|
9
|
+
readonly reason: "malformed";
|
|
10
|
+
readonly status: number;
|
|
11
|
+
};
|
|
12
|
+
/** Minimum structural type required by web and ORM adapters. Any codec variant satisfies this — all expose `safeParse`. Adapters only ever call `safeParse` — never key-dependent methods like `extractTimestamp`, `wrap`, or `unwrap`. */
|
|
13
|
+
type IdCodec<Brand extends string> = {
|
|
14
|
+
safeParse(value: unknown): ParseResult<Brand>;
|
|
15
|
+
};
|
|
16
|
+
/** Re-exported from ORM adapter subpaths (`@smonn/ids/drizzle`, `@smonn/ids/prisma`, `@smonn/ids/kysely`) under the public name; structurally identical to {@link IdCodec}. */
|
|
17
|
+
type IdColumnCodec<Brand extends string> = IdCodec<Brand>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { IdColumnCodec as n, IdParamFailure as r, IdCodec as t };
|
|
20
|
+
//# sourceMappingURL=adapter-types-unUcmMXC.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-types-unUcmMXC.d.mts","names":[],"sources":["../src/adapter-types.ts"],"mappings":";;;;KAIY,cAAA;EAAA,SACG,MAAA;EAAA,SAAmC,MAAA;AAAA;EAAA,SACnC,MAAA;EAAA,SAA8B,MAAA;AAAA;;KAGjC,OAAA;EACV,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;AADzC;AAAA,KAKY,aAAA,yBAAsC,OAAA,CAAQ,KAAA"}
|