@byok-sdk/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ancienttwo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @byok-sdk/core
2
+
3
+ Tenant-first platform contracts, branded identity types, store ports, device
4
+ proof canonicalization, object/quota contracts, and in-memory reference stores.
5
+ This package is runtime-neutral: it has no Node built-in or protocol dependency.
6
+
7
+ ```ts
8
+ import { tenantId, InMemoryCoreStores } from '@byok-sdk/core';
9
+ ```
10
+
11
+ MIT licensed. Node.js 20 or newer.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Device proof envelope and canonical signing bytes (§12.6.3, sprint §S6.2).
3
+ *
4
+ * Three things live here and nothing else:
5
+ *
6
+ * 1. **The protected-claims schema.** Every field a signature must cover so the
7
+ * proof binds a *specific request*: tenant, product, device, key + epoch,
8
+ * request id, operation, resource, the request line (method+path or a stable
9
+ * operation id), and the body's hash *and* size. Signing only the body is
10
+ * forbidden — it would let a valid proof be replayed against a different
11
+ * operation or resource.
12
+ * 2. **A dependency-free deterministic canonicalizer.** RFC 8785 (JCS) in the
13
+ * narrow subset the envelope allows. Node and Workers must produce
14
+ * byte-identical output, so the accepted value space is deliberately small:
15
+ * strings, booleans, null, safe integers, plain objects, arrays. Floats,
16
+ * non-safe integers, `NaN`, `Infinity`, `undefined`, `bigint`, dates and
17
+ * class instances throw instead of being coerced — a signature over a value
18
+ * whose serialization is implementation-defined is not a signature.
19
+ * 3. **The verify port.** An interface, never an implementation: core is
20
+ * Node-free and Workers-safe, and `node:crypto` and WebCrypto disagree about
21
+ * key handling. The composition brings its own verifier.
22
+ *
23
+ * Claims are **untrusted input**. `tenantId` here is a plain string, not a
24
+ * branded `TenantId`, because a device asserting a tenant proves nothing: the
25
+ * claim is a lookup key used to load the device row, and the row is the
26
+ * authority (§12.6.2 layer 5). Branding happens after that lookup, not here.
27
+ */
28
+ import { z } from 'zod';
29
+ /** Envelope schema id, self-consistent with the domain prefix below. */
30
+ export declare const DEVICE_PROOF_SCHEMA_ID = "byok-device-proof-v1";
31
+ /**
32
+ * Domain separation prefix (§12.6.3). Prepended to the canonical claim bytes
33
+ * before signing so a device-proof signature can never be replayed as a nonce
34
+ * signature (`byok-nonce-v1\n`) or a record attestation.
35
+ */
36
+ export declare const DEVICE_PROOF_DOMAIN_PREFIX = "byok-device-proof-v1\n";
37
+ export declare const DEVICE_PROOF_VERSION = 1;
38
+ /** Signature algorithms this envelope version admits. */
39
+ export declare const DEVICE_PROOF_ALGORITHMS: readonly ['ed25519'];
40
+ export type DeviceProofAlgorithm = (typeof DEVICE_PROOF_ALGORITHMS)[number];
41
+ export type JsonPrimitive = string | number | boolean | null;
42
+ export type JsonValue = JsonPrimitive | readonly JsonValue[] | {
43
+ readonly [key: string]: JsonValue;
44
+ };
45
+ export type JsonObject = {
46
+ readonly [key: string]: JsonValue;
47
+ };
48
+ /**
49
+ * Canonical JSON text for `value`, per RFC 8785 restricted to the value space
50
+ * above. Key insertion order never affects the output; anything outside the
51
+ * accepted space throws `proof_canonicalization_failed`.
52
+ */
53
+ export declare function canonicalizeJson(value: JsonValue): string;
54
+ /** UTF-8 bytes of {@link canonicalizeJson}. `TextEncoder` is a platform global on Node and Workers. */
55
+ export declare function canonicalizeJsonBytes(value: JsonValue): Uint8Array;
56
+ /**
57
+ * The signed claim set (§S6.2).
58
+ *
59
+ * The request line is expressible two ways — `method` + `path`, or a stable
60
+ * `operationId` — and exactly one form must be present. Allowing both at once
61
+ * would create two different canonical byte strings for one request; allowing
62
+ * neither would drop the operation binding the whole design rests on.
63
+ */
64
+ export declare const DeviceProofProtectedClaimsSchema: z.ZodObject<{
65
+ version: z.ZodLiteral<1>;
66
+ tenantId: z.ZodString;
67
+ productId: z.ZodString;
68
+ deviceId: z.ZodString;
69
+ keyId: z.ZodString;
70
+ keyEpoch: z.ZodNumber;
71
+ requestId: z.ZodString;
72
+ operation: z.ZodString;
73
+ resource: z.ZodString;
74
+ method: z.ZodOptional<z.ZodString>;
75
+ path: z.ZodOptional<z.ZodString>;
76
+ operationId: z.ZodOptional<z.ZodString>;
77
+ bodySha256: z.ZodString;
78
+ bodySize: z.ZodNumber;
79
+ issuedAt: z.ZodISODateTime;
80
+ expiresAt: z.ZodOptional<z.ZodISODateTime>;
81
+ nonce: z.ZodOptional<z.ZodString>;
82
+ }, z.core.$strict>;
83
+ export type DeviceProofProtectedClaims = z.infer<typeof DeviceProofProtectedClaimsSchema>;
84
+ export declare const DeviceProofEnvelopeV1Schema: z.ZodObject<{
85
+ schema: z.ZodLiteral<"byok-device-proof-v1">;
86
+ algorithm: z.ZodEnum<{
87
+ ed25519: "ed25519";
88
+ }>;
89
+ protected: z.ZodObject<{
90
+ version: z.ZodLiteral<1>;
91
+ tenantId: z.ZodString;
92
+ productId: z.ZodString;
93
+ deviceId: z.ZodString;
94
+ keyId: z.ZodString;
95
+ keyEpoch: z.ZodNumber;
96
+ requestId: z.ZodString;
97
+ operation: z.ZodString;
98
+ resource: z.ZodString;
99
+ method: z.ZodOptional<z.ZodString>;
100
+ path: z.ZodOptional<z.ZodString>;
101
+ operationId: z.ZodOptional<z.ZodString>;
102
+ bodySha256: z.ZodString;
103
+ bodySize: z.ZodNumber;
104
+ issuedAt: z.ZodISODateTime;
105
+ expiresAt: z.ZodOptional<z.ZodISODateTime>;
106
+ nonce: z.ZodOptional<z.ZodString>;
107
+ }, z.core.$strict>;
108
+ signature: z.ZodString;
109
+ }, z.core.$strict>;
110
+ export type DeviceProofEnvelopeV1 = z.infer<typeof DeviceProofEnvelopeV1Schema>;
111
+ /**
112
+ * Parses an envelope fail-closed.
113
+ *
114
+ * @throws {ByokCoreError} code `proof_envelope_invalid`.
115
+ */
116
+ export declare function parseDeviceProofEnvelope(input: unknown): DeviceProofEnvelopeV1;
117
+ /**
118
+ * Projects claims into the exact JSON object that gets canonicalized.
119
+ *
120
+ * Built field by field rather than by spreading the parsed object: an absent
121
+ * optional must be an *absent key*, never a key holding `undefined`, or
122
+ * `{nonce: undefined}` and `{}` would be the same request with two different
123
+ * signatures. The canonicalizer refuses `undefined` outright, so this
124
+ * projection is where absence is decided.
125
+ */
126
+ export declare function deviceProofCanonicalClaims(claims: DeviceProofProtectedClaims): JsonObject;
127
+ /** Canonical JSON text of the protected claim set, without the domain prefix. */
128
+ export declare function deviceProofCanonicalJson(claims: DeviceProofProtectedClaims): string;
129
+ /**
130
+ * The exact bytes a device signs and a verifier reconstructs:
131
+ * `byok-device-proof-v1\n` followed by the canonical claim JSON, UTF-8 encoded.
132
+ *
133
+ * Frozen by `src/__tests__/golden/device-proof-v1.canonical.json`.
134
+ */
135
+ export declare function deviceProofSigningInput(claims: DeviceProofProtectedClaims): Uint8Array;
136
+ export interface DeviceProofVerifyInput {
137
+ readonly algorithm: DeviceProofAlgorithm;
138
+ /** Raw public key, base64url — the JWK `x` encoding the device registry stores. */
139
+ readonly publicKey: string;
140
+ readonly signature: string;
141
+ readonly signingInput: Uint8Array;
142
+ }
143
+ /**
144
+ * Injected signature verification.
145
+ *
146
+ * Not a store port, so it has no tenant parameter: it answers a pure
147
+ * cryptographic question about bytes. Deciding *which* key to check against is
148
+ * the caller's tenant-scoped device-row lookup, and keeping that out of here is
149
+ * what stops a verifier from becoming a second authority on device identity.
150
+ */
151
+ export interface DeviceProofVerifier {
152
+ verify(input: DeviceProofVerifyInput): Promise<boolean>;
153
+ }
package/dist/blob.d.ts ADDED
@@ -0,0 +1,116 @@
1
+ import type { TenantId } from './tenant';
2
+ /** `sha256:<64 lowercase hex>`. The only content-address form core accepts. */
3
+ export declare const CONTENT_HASH_PATTERN: RegExp;
4
+ /** A validated content address. Branded so an unvalidated digest cannot stand in for one. */
5
+ export type ContentHash = string & {
6
+ readonly __byokContentHash: unique symbol;
7
+ };
8
+ /**
9
+ * The single mint point for {@link ContentHash}.
10
+ *
11
+ * Fail-closed on uppercase hex: accepting both cases would make the same bytes
12
+ * addressable under two keys, which breaks per-tenant deduplication and the
13
+ * "count each hash once" billing rule (§12.7.6).
14
+ *
15
+ * @throws {ByokCoreError} code `content_hash_invalid`.
16
+ */
17
+ export declare function contentHash(value: string): ContentHash;
18
+ /** Non-throwing form of {@link contentHash}. */
19
+ export declare function isContentHash(value: unknown): value is ContentHash;
20
+ /**
21
+ * Tenant-scoped object key, e.g.
22
+ * `tenants/<tenantId>/objects/sha256/<hex>` (§12.7.4).
23
+ */
24
+ export declare function tenantObjectKey(tenant: TenantId, hash: ContentHash): string;
25
+ /**
26
+ * Object manifest lifecycle (§12.7.8).
27
+ *
28
+ * `pending` exists because Postgres and R2 have no shared transaction: the row
29
+ * is written before the bytes land, and only `committed` rows may be referenced
30
+ * by a truth record. `delete_pending` is the tombstone the GC worker drives, so
31
+ * a failed R2 delete is retryable instead of leaving usage silently wrong.
32
+ */
33
+ export declare const OBJECT_STATES: readonly ['pending', 'committed', 'delete_pending', 'deleted'];
34
+ export type ObjectState = (typeof OBJECT_STATES)[number];
35
+ /** Legal manifest transitions. Anything else raises `object_state_invalid`. */
36
+ export declare const OBJECT_STATE_TRANSITIONS: Readonly<Record<ObjectState, readonly ObjectState[]>>;
37
+ export declare function isLegalObjectTransition(from: ObjectState, to: ObjectState): boolean;
38
+ /** One row of `object_manifest` (§12.7.6). */
39
+ export interface ObjectManifestEntry {
40
+ readonly tenantId: TenantId;
41
+ readonly hash: ContentHash;
42
+ /** Declared at reservation time, re-verified at commit against the store's `HEAD`. */
43
+ readonly byteSize: bigint;
44
+ readonly contentType: string;
45
+ readonly state: ObjectState;
46
+ /** Number of live {@link ObjectReference} rows. `0` makes the object GC-eligible after grace. */
47
+ readonly refCount: number;
48
+ readonly createdAt: string;
49
+ readonly updatedAt: string;
50
+ /** Set when the row entered `delete_pending`; the grace window is measured from here. */
51
+ readonly deletePendingAt?: string;
52
+ }
53
+ /** What references an object. `refKind`/`refId` are opaque to core. */
54
+ export interface ObjectReference {
55
+ readonly tenantId: TenantId;
56
+ readonly hash: ContentHash;
57
+ readonly refKind: string;
58
+ readonly refId: string;
59
+ readonly createdAt: string;
60
+ }
61
+ export interface ObjectManifestInput {
62
+ readonly hash: ContentHash;
63
+ readonly byteSize: bigint;
64
+ readonly contentType: string;
65
+ }
66
+ /**
67
+ * Finalize input. `byteSize`/`contentType` are what the composition actually
68
+ * observed on the object store, not what the client declared — the whole point
69
+ * of the check is that the two can differ (§12.7.7 step 4).
70
+ */
71
+ export interface ObjectCommitInput {
72
+ readonly hash: ContentHash;
73
+ readonly observedByteSize: bigint;
74
+ readonly observedContentType: string;
75
+ }
76
+ export interface ObjectReferenceInput {
77
+ readonly hash: ContentHash;
78
+ readonly refKind: string;
79
+ readonly refId: string;
80
+ }
81
+ export interface ObjectListQuery {
82
+ readonly state?: ObjectState;
83
+ /**
84
+ * Only rows whose `deletePendingAt` is at or before this instant.
85
+ *
86
+ * Must be a **canonical ISO-8601 UTC instant** (`YYYY-MM-DDTHH:mm:ss.sssZ`);
87
+ * anything else is rejected with `timestamp_not_canonical`, because the
88
+ * in-memory composition compares it as a string and a SQL composition
89
+ * compares it as a `timestamptz` — see `time.ts`.
90
+ */
91
+ readonly deletePendingBefore?: string;
92
+ readonly limit?: number;
93
+ }
94
+ /**
95
+ * Object manifest port. Tenant-first, async, metadata only.
96
+ *
97
+ * Raises: `object_not_found`, `object_state_invalid`,
98
+ * `storage_integrity_mismatch` (commit observed size/type disagreeing with the
99
+ * declared manifest row), `timestamp_not_canonical` (a `list` query whose
100
+ * `deletePendingBefore` is not a canonical ISO-8601 UTC instant).
101
+ */
102
+ export interface ObjectStore {
103
+ /** Creates or returns the `pending` row for `hash`. Idempotent per (tenant, hash). */
104
+ putManifest(tenant: TenantId, input: ObjectManifestInput): Promise<ObjectManifestEntry>;
105
+ /** `pending` → `committed` after verifying observed size/type. */
106
+ commit(tenant: TenantId, input: ObjectCommitInput): Promise<ObjectManifestEntry>;
107
+ get(tenant: TenantId, hash: ContentHash): Promise<ObjectManifestEntry | undefined>;
108
+ list(tenant: TenantId, query: ObjectListQuery): Promise<readonly ObjectManifestEntry[]>;
109
+ /** Idempotent per (tenant, hash, refKind, refId) — re-adding does not double-count. */
110
+ addReference(tenant: TenantId, input: ObjectReferenceInput): Promise<ObjectManifestEntry>;
111
+ removeReference(tenant: TenantId, input: ObjectReferenceInput): Promise<ObjectManifestEntry>;
112
+ /** Tombstone step 1: only legal at `refCount === 0`. */
113
+ markDeletePending(tenant: TenantId, hash: ContentHash): Promise<ObjectManifestEntry>;
114
+ /** Tombstone step 3: the object store delete succeeded. */
115
+ markDeleted(tenant: TenantId, hash: ContentHash): Promise<ObjectManifestEntry>;
116
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Board coordination state (§12.3).
3
+ *
4
+ * This is the *human and multi-device* collaboration lifecycle, and it is a
5
+ * separate vocabulary from the frozen wire execution vocabulary on purpose. One
6
+ * run attempt is not one work item: an item can outlive several attempts, and a
7
+ * finished attempt does not mean a human accepted the result. The constraint
8
+ * test asserts that no wire execution state name appears in this file — if the
9
+ * two vocabularies ever merge, status sniffing comes back and the board becomes
10
+ * a second, unreliable execution authority.
11
+ *
12
+ * Three invariants the ports below encode:
13
+ *
14
+ * - **assignee and status are two fields**, not one enum. "Who holds it" and
15
+ * "where it is" change independently.
16
+ * - **every mutation is a CAS.** `claim` compares against "unheld"; every status
17
+ * move carries `expectedStatus`. There is no last-write-wins path.
18
+ * - **conflicts return the snapshot they lost to**, so the caller re-decides
19
+ * against real state instead of retrying blind.
20
+ */
21
+ import type { TenantId } from './tenant';
22
+ /** The five board statuses. `closed` means "terminated, unaccepted" (§12.3). */
23
+ export declare const BOARD_STATUSES: readonly ['todo', 'in_progress', 'in_review', 'done', 'closed'];
24
+ export type BoardStatus = (typeof BOARD_STATUSES)[number];
25
+ /**
26
+ * Legal transitions, transcribed from the §12.3 state diagram.
27
+ *
28
+ * `in_review → done` is the human acceptance step: a device reporting a
29
+ * terminal record can push an item to `in_review`, never straight to `done`.
30
+ * `done` and `closed` are sinks — if archival semantics are ever needed, §12.3
31
+ * pins the answer to an `archivedAt` field, not a sixth status.
32
+ */
33
+ export declare const BOARD_TRANSITIONS: Readonly<Record<BoardStatus, readonly BoardStatus[]>>;
34
+ export declare function isLegalBoardTransition(from: BoardStatus, to: BoardStatus): boolean;
35
+ /** Who currently holds the item. Separate from {@link BoardItem.status}. */
36
+ export interface BoardAssignee {
37
+ readonly holderId: string;
38
+ readonly heldSince: string;
39
+ }
40
+ /**
41
+ * One work item.
42
+ *
43
+ * `boardSeq` is monotonic **per tenant** and bumps on every mutation, which is
44
+ * what makes incremental list polling possible without a cross-tenant sequence
45
+ * (a global sequence would leak other tenants' write rate).
46
+ */
47
+ export interface BoardItem {
48
+ readonly tenantId: TenantId;
49
+ readonly itemId: string;
50
+ readonly channel: string;
51
+ readonly title: string;
52
+ readonly status: BoardStatus;
53
+ readonly assignee?: BoardAssignee;
54
+ readonly boardSeq: number;
55
+ readonly createdAt: string;
56
+ readonly updatedAt: string;
57
+ }
58
+ export interface BoardItemInput {
59
+ readonly itemId: string;
60
+ readonly channel: string;
61
+ readonly title: string;
62
+ /** Defaults to `todo`. Creating directly into a sink status is legal but explicit. */
63
+ readonly status?: BoardStatus;
64
+ }
65
+ export interface BoardListQuery {
66
+ /** Exclusive lower bound on `boardSeq`, for incremental polling. */
67
+ readonly afterSeq?: number;
68
+ readonly channel?: string;
69
+ readonly status?: BoardStatus;
70
+ readonly limit?: number;
71
+ }
72
+ export interface BoardPage {
73
+ readonly items: readonly BoardItem[];
74
+ /** Highest `boardSeq` in this page, or the requested `afterSeq` when empty. */
75
+ readonly nextSeq: number;
76
+ readonly hasMore: boolean;
77
+ }
78
+ export interface BoardClaimInput {
79
+ readonly itemId: string;
80
+ readonly holderId: string;
81
+ /**
82
+ * Status the claimer believes the item is in. Defaults to `todo`; supplying
83
+ * it explicitly makes the claim a full CAS rather than an assignee-only one.
84
+ *
85
+ * The CAS holds on the idempotent path too: a re-claim by the current holder
86
+ * that supplies a stale `expectedStatus` fails with `board_status_conflict`
87
+ * and the current item. The default is *not* applied there — a retry of a
88
+ * successful claim legitimately observes `in_progress`.
89
+ */
90
+ readonly expectedStatus?: BoardStatus;
91
+ }
92
+ export interface BoardUnclaimInput {
93
+ readonly itemId: string;
94
+ /** Must match the current holder — releasing someone else's item is not a legal move. */
95
+ readonly holderId: string;
96
+ }
97
+ export interface BoardStatusUpdateInput {
98
+ readonly itemId: string;
99
+ readonly expectedStatus: BoardStatus;
100
+ readonly status: BoardStatus;
101
+ /** Optional holder assertion, for transitions only the holder may make. */
102
+ readonly holderId?: string;
103
+ }
104
+ /**
105
+ * Board port. Tenant-first, async.
106
+ *
107
+ * Raises: `board_item_not_found`, `board_claim_conflict` (loser gets the
108
+ * winner's holder snapshot), `board_status_conflict` (`expectedStatus` missed —
109
+ * carries the current item), `board_transition_invalid` (move not in
110
+ * {@link BOARD_TRANSITIONS} — also carries the current item), `board_not_held`.
111
+ */
112
+ export interface BoardStore {
113
+ create(tenant: TenantId, input: BoardItemInput): Promise<BoardItem>;
114
+ get(tenant: TenantId, itemId: string): Promise<BoardItem | undefined>;
115
+ list(tenant: TenantId, query: BoardListQuery): Promise<BoardPage>;
116
+ /** CAS on "unheld". Exactly one concurrent caller wins; losers get the holder snapshot. */
117
+ claim(tenant: TenantId, input: BoardClaimInput): Promise<BoardItem>;
118
+ unclaim(tenant: TenantId, input: BoardUnclaimInput): Promise<BoardItem>;
119
+ updateStatus(tenant: TenantId, input: BoardStatusUpdateInput): Promise<BoardItem>;
120
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Capability declaration (ADR-010).
3
+ *
4
+ * A client learns what a deployment supports by reading a declaration, never by
5
+ * probing endpoints and interpreting 404/405/501. Status-code sniffing conflates
6
+ * "this build does not have that feature" with "that request was wrong" and
7
+ * with "a proxy ate it", and every consumer ends up with its own guess.
8
+ *
9
+ * The schema is intentionally the minimum that supports that rule: an opaque
10
+ * string set plus a monotonic declaration version. Capability names are host and
11
+ * deployment vocabulary — core validates their shape, never their meaning, so a
12
+ * new capability never requires a core release.
13
+ */
14
+ import { z } from 'zod';
15
+ export declare const CAPABILITY_DECLARATION_SCHEMA_ID = "byok-capabilities-v1";
16
+ /** Capability names: lowercase dotted segments, e.g. `board.sse`, `storage.reservations`. */
17
+ export declare const CAPABILITY_NAME_PATTERN: RegExp;
18
+ export declare const CapabilityDeclarationSchema: z.ZodObject<{
19
+ schema: z.ZodLiteral<"byok-capabilities-v1">;
20
+ version: z.ZodNumber;
21
+ capabilities: z.ZodArray<z.ZodString>;
22
+ }, z.core.$strip>;
23
+ export type CapabilityDeclaration = z.infer<typeof CapabilityDeclarationSchema>;
24
+ /**
25
+ * Parses a declaration fail-closed.
26
+ *
27
+ * @throws {ByokCoreError} code `capability_declaration_invalid`.
28
+ */
29
+ export declare function parseCapabilityDeclaration(input: unknown): CapabilityDeclaration;
30
+ export declare function hasCapability(declaration: CapabilityDeclaration, capability: string): boolean;
31
+ /**
32
+ * The enforcement point ADR-010 exists for: a caller asserts the capability up
33
+ * front and gets a named failure, instead of issuing the request and guessing
34
+ * what the status code meant.
35
+ *
36
+ * @throws {ByokCoreError} code `capability_unavailable`.
37
+ */
38
+ export declare function assertCapability(declaration: CapabilityDeclaration, capability: string): void;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The one error taxonomy for `@byok-sdk/core`.
3
+ *
4
+ * Two rules the rest of the package is built around:
5
+ *
6
+ * 1. **One class, code-based branching.** Consumers switch on `error.code`, not
7
+ * on class identity — the same idiom `@byok-sdk/keys` uses. A composition that
8
+ * maps core errors onto HTTP does it with a code table, so adding a code is
9
+ * an additive change instead of a new `instanceof` chain everywhere.
10
+ * 2. **Every conflict carries the current snapshot.** A CAS failure that
11
+ * reports only "conflict" forces the caller into a second round trip and
12
+ * invites last-write-wins retries. {@link CoreConflictError} therefore
13
+ * carries `current` — the authoritative state the caller lost to — plus the
14
+ * `observedAt` instant it was read at.
15
+ *
16
+ * This module imports nothing. It is the graph sink: `quota.ts` narrows
17
+ * {@link StorageErrorCode} out of this union and maps it to HTTP status, and
18
+ * every port module documents which codes it can raise. That keeps the code
19
+ * list in one place while letting each domain own its own status mapping.
20
+ */
21
+ /**
22
+ * Every error code this package can raise.
23
+ *
24
+ * Codes are stable strings. Two groups are additionally **wire-stable** — a
25
+ * composition is expected to surface them verbatim to clients, so renaming one
26
+ * is a breaking change:
27
+ *
28
+ * - `terminal_conflict` (§12.6.4, HTTP 409)
29
+ * - the five `storage_*` codes (§12.7.7, see `quota.ts`)
30
+ */
31
+ export declare const CORE_ERROR_CODES: {
32
+ readonly tenant_id_invalid: 'tenant_id_invalid';
33
+ readonly content_hash_invalid: 'content_hash_invalid';
34
+ readonly timestamp_not_canonical: 'timestamp_not_canonical';
35
+ readonly capability_declaration_invalid: 'capability_declaration_invalid';
36
+ readonly capability_unavailable: 'capability_unavailable';
37
+ readonly proof_envelope_invalid: 'proof_envelope_invalid';
38
+ readonly proof_canonicalization_failed: 'proof_canonicalization_failed';
39
+ readonly mailbox_message_not_found: 'mailbox_message_not_found';
40
+ readonly mailbox_cursor_regression: 'mailbox_cursor_regression';
41
+ readonly board_item_not_found: 'board_item_not_found';
42
+ readonly board_item_exists: 'board_item_exists';
43
+ readonly board_transition_invalid: 'board_transition_invalid';
44
+ readonly board_status_conflict: 'board_status_conflict';
45
+ readonly board_claim_conflict: 'board_claim_conflict';
46
+ readonly board_not_held: 'board_not_held';
47
+ readonly truth_record_not_found: 'truth_record_not_found';
48
+ readonly terminal_conflict: 'terminal_conflict';
49
+ readonly truth_revision_conflict: 'truth_revision_conflict';
50
+ readonly activity_capacity_invalid: 'activity_capacity_invalid';
51
+ readonly activity_batch_invalid: 'activity_batch_invalid';
52
+ readonly hint_ttl_invalid: 'hint_ttl_invalid';
53
+ readonly hint_rate_limited: 'hint_rate_limited';
54
+ readonly object_not_found: 'object_not_found';
55
+ readonly object_state_invalid: 'object_state_invalid';
56
+ readonly storage_entitlement_missing: 'storage_entitlement_missing';
57
+ readonly storage_entitlement_version_conflict: 'storage_entitlement_version_conflict';
58
+ readonly storage_reservation_not_found: 'storage_reservation_not_found';
59
+ readonly storage_object_too_large: 'storage_object_too_large';
60
+ readonly storage_quota_exceeded: 'storage_quota_exceeded';
61
+ readonly storage_reservation_expired: 'storage_reservation_expired';
62
+ readonly storage_integrity_mismatch: 'storage_integrity_mismatch';
63
+ readonly storage_write_suspended: 'storage_write_suspended';
64
+ };
65
+ export type CoreErrorCode = (typeof CORE_ERROR_CODES)[keyof typeof CORE_ERROR_CODES];
66
+ /** Base error for every failure this package raises. */
67
+ export declare class ByokCoreError extends Error {
68
+ readonly code: CoreErrorCode;
69
+ constructor(code: CoreErrorCode, message: string, options?: ErrorOptions);
70
+ }
71
+ /**
72
+ * A compare-and-set failure.
73
+ *
74
+ * `current` is the authoritative state at the moment the write was rejected —
75
+ * the board item whose status moved, the terminal record already committed, the
76
+ * entitlement row at a newer version. The caller re-decides against it; the
77
+ * store never merges (§12.3: "不做 silent last-write-wins", §12.6.4: 不覆写第一份事实).
78
+ */
79
+ export declare class CoreConflictError<TCurrent> extends ByokCoreError {
80
+ readonly current: TCurrent;
81
+ readonly observedAt: string;
82
+ constructor(code: CoreErrorCode, message: string, current: TCurrent, observedAt: string, options?: ErrorOptions);
83
+ }
84
+ /** Narrows an unknown thrown value to a core error, optionally to one code. */
85
+ export declare function isCoreError(value: unknown, code?: CoreErrorCode): value is ByokCoreError;
86
+ /** Narrows an unknown thrown value to a conflict error carrying a snapshot. */
87
+ export declare function isCoreConflictError<TCurrent>(value: unknown, code?: CoreErrorCode): value is CoreConflictError<TCurrent>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * In-memory {@link ObjectStore} reference (§12.7.4, §12.7.8).
3
+ *
4
+ * Metadata only — there are no bytes here, and there are none in the Postgres
5
+ * composition either: the manifest is the transaction authority and the object
6
+ * store holds the payload. `refCount` is derived from the reference rows rather
7
+ * than incremented in place, so a double `addReference` for the same
8
+ * `(refKind, refId)` cannot inflate it and strand an object forever.
9
+ */
10
+ import { type ContentHash, type ObjectCommitInput, type ObjectListQuery, type ObjectManifestEntry, type ObjectManifestInput, type ObjectReferenceInput, type ObjectStore } from '../blob';
11
+ import type { Clock } from '../stores';
12
+ import { type TenantId } from '../tenant';
13
+ export declare class InMemoryObjectStore implements ObjectStore {
14
+ #private;
15
+ constructor(clock: Clock);
16
+ putManifest(tenant: TenantId, input: ObjectManifestInput): Promise<ObjectManifestEntry>;
17
+ commit(tenant: TenantId, input: ObjectCommitInput): Promise<ObjectManifestEntry>;
18
+ get(tenant: TenantId, hash: ContentHash): Promise<ObjectManifestEntry | undefined>;
19
+ list(tenant: TenantId, query: ObjectListQuery): Promise<readonly ObjectManifestEntry[]>;
20
+ addReference(tenant: TenantId, input: ObjectReferenceInput): Promise<ObjectManifestEntry>;
21
+ removeReference(tenant: TenantId, input: ObjectReferenceInput): Promise<ObjectManifestEntry>;
22
+ markDeletePending(tenant: TenantId, hash: ContentHash): Promise<ObjectManifestEntry>;
23
+ markDeleted(tenant: TenantId, hash: ContentHash): Promise<ObjectManifestEntry>;
24
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * In-memory {@link BoardStore} reference (§12.3).
3
+ *
4
+ * The claim path is the interesting one: because JavaScript resolves each
5
+ * `await` boundary atomically here, N concurrent `claim` calls serialize and
6
+ * exactly one finds `assignee === undefined`. A SQL composition gets the same
7
+ * outcome from a conditional `UPDATE ... WHERE assignee IS NULL`; the
8
+ * conformance suite asserts the outcome, not the mechanism.
9
+ *
10
+ * `boardSeq` is per tenant and bumps on every mutation, which is what makes
11
+ * `list({ afterSeq })` an incremental feed that cannot leak another tenant's
12
+ * write rate.
13
+ */
14
+ import { type BoardClaimInput, type BoardItem, type BoardItemInput, type BoardListQuery, type BoardPage, type BoardStatusUpdateInput, type BoardStore, type BoardUnclaimInput } from '../board';
15
+ import type { Clock } from '../stores';
16
+ import { type TenantId } from '../tenant';
17
+ export declare class InMemoryBoardStore implements BoardStore {
18
+ #private;
19
+ constructor(clock: Clock);
20
+ create(tenant: TenantId, input: BoardItemInput): Promise<BoardItem>;
21
+ get(tenant: TenantId, itemId: string): Promise<BoardItem | undefined>;
22
+ list(tenant: TenantId, query: BoardListQuery): Promise<BoardPage>;
23
+ claim(tenant: TenantId, input: BoardClaimInput): Promise<BoardItem>;
24
+ unclaim(tenant: TenantId, input: BoardUnclaimInput): Promise<BoardItem>;
25
+ updateStatus(tenant: TenantId, input: BoardStatusUpdateInput): Promise<BoardItem>;
26
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A deterministic clock for the in-memory reference and the conformance suite.
3
+ *
4
+ * TTL behavior (presence expiry, activity expiry, reservation expiry) is part
5
+ * of the contract, and asserting it against a wall clock means either sleeping
6
+ * or accepting flakes. A composition under test injects one of these and moves
7
+ * time explicitly.
8
+ */
9
+ import type { MutableClock } from '../stores';
10
+ /** Fixed start instant, so golden-ish assertions in the suite read the same on every run. */
11
+ export declare const IN_MEMORY_CLOCK_EPOCH = "2026-01-01T00:00:00.000Z";
12
+ export declare function createMutableClock(start?: Date): MutableClock;
@@ -0,0 +1,23 @@
1
+ import type { Clock, CoreStores, MutableClock } from '../stores';
2
+ export { createMutableClock, IN_MEMORY_CLOCK_EPOCH } from './clock';
3
+ export { InMemoryMailboxStore } from './mailbox';
4
+ export { InMemoryBoardStore } from './board';
5
+ export { InMemoryTruthStore } from './truth';
6
+ export { InMemoryPresenceStore, InMemoryActivityStore } from './presence';
7
+ export { InMemoryObjectStore } from './blob';
8
+ export { InMemoryQuotaStore } from './quota';
9
+ export interface InMemoryCoreOptions {
10
+ /** Defaults to a fresh {@link createMutableClock}, so TTL behavior is deterministic. */
11
+ readonly clock?: Clock;
12
+ }
13
+ export interface InMemoryCoreComposition {
14
+ readonly stores: CoreStores;
15
+ /** The clock the stores read. Mutable only when the caller did not inject its own. */
16
+ readonly clock: Clock;
17
+ }
18
+ export declare function createInMemoryCoreStores(options?: InMemoryCoreOptions): InMemoryCoreComposition;
19
+ /** Convenience for tests that need to move time: returns the composition and its mutable clock. */
20
+ export declare function createInMemoryCoreCompositionWithClock(): {
21
+ readonly stores: CoreStores;
22
+ readonly clock: MutableClock;
23
+ };
@@ -0,0 +1,12 @@
1
+ import type { MailboxAdvanceCursorInput, MailboxAppendInput, MailboxCursorState, MailboxMessage, MailboxPage, MailboxReadQuery, MailboxRetentionInput, MailboxRetentionResult, MailboxStore } from '../mailbox';
2
+ import type { Clock } from '../stores';
3
+ import { type TenantId } from '../tenant';
4
+ export declare class InMemoryMailboxStore implements MailboxStore {
5
+ #private;
6
+ constructor(clock: Clock);
7
+ append(tenant: TenantId, input: MailboxAppendInput): Promise<MailboxMessage>;
8
+ readAfter(tenant: TenantId, query: MailboxReadQuery): Promise<MailboxPage>;
9
+ advanceCursor(tenant: TenantId, input: MailboxAdvanceCursorInput): Promise<MailboxCursorState>;
10
+ readCursor(tenant: TenantId, deviceId: string): Promise<MailboxCursorState>;
11
+ collectRetired(tenant: TenantId, input: MailboxRetentionInput): Promise<MailboxRetentionResult>;
12
+ }