@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.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Presence and activity hints (§12.3).
3
+ *
4
+ * Both are lossy, TTL-bounded, unsigned, and never authoritative. Expiry means
5
+ * *absence*, not a stale value: a hint past its TTL is invisible to readers, so
6
+ * nothing downstream can mistake an old level for a live one.
7
+ *
8
+ * These hints must never be used to derive coordination state, execution state,
9
+ * authorization, billing, or recovery — which is why this module shares no
10
+ * vocabulary with `board.ts` and no vocabulary with the frozen wire states. The
11
+ * constraint test enforces that separation by scanning this file for those
12
+ * names. A device that looks busy is not evidence that any particular work item
13
+ * moved anywhere.
14
+ *
15
+ * The activity tail is bounded by count and carries an explicit `dropped`
16
+ * counter: lossiness is written into the data instead of being hidden behind a
17
+ * stream that pretends to be complete.
18
+ */
19
+ import type { TenantId } from './tenant';
20
+ /** The five presence levels. */
21
+ export declare const PRESENCE_LEVELS: readonly ['online', 'thinking', 'working', 'error', 'offline'];
22
+ export type PresenceLevel = (typeof PRESENCE_LEVELS)[number];
23
+ /** A device-level hint. `expiresAt` is authoritative: past it, the hint does not exist. */
24
+ export interface PresenceHint {
25
+ readonly tenantId: TenantId;
26
+ readonly deviceId: string;
27
+ readonly level: PresenceLevel;
28
+ /** Free-form host label, bounded by the composition. Never parsed by core. */
29
+ readonly detail?: string;
30
+ readonly observedAt: string;
31
+ readonly expiresAt: string;
32
+ }
33
+ export interface PresenceHintInput {
34
+ readonly deviceId: string;
35
+ readonly level: PresenceLevel;
36
+ readonly detail?: string;
37
+ /** Hint lifetime. §12.7.5 suggests 60-120s for presence. */
38
+ readonly ttlMs: number;
39
+ /** Minimum time between accepted publications for this device. `0` explicitly disables throttling. */
40
+ readonly minimumIntervalMs: number;
41
+ }
42
+ /** One entry of a task's lossy tail. */
43
+ export interface ActivityEntry {
44
+ readonly at: string;
45
+ readonly detail: string;
46
+ }
47
+ /**
48
+ * A task's bounded tail.
49
+ *
50
+ * `dropped` counts entries evicted by `capacity`, so a reader can tell the
51
+ * difference between "nothing happened" and "we lost the middle".
52
+ */
53
+ export interface ActivityTail {
54
+ readonly tenantId: TenantId;
55
+ readonly taskId: string;
56
+ readonly entries: readonly ActivityEntry[];
57
+ readonly dropped: number;
58
+ readonly capacity: number;
59
+ readonly expiresAt: string;
60
+ }
61
+ export interface ActivityAppendInput {
62
+ readonly taskId: string;
63
+ /** One ProgressBatcher-shaped batch. Empty batches are rejected. */
64
+ readonly details: readonly string[];
65
+ /** Events the producer dropped before this batch reached the store. */
66
+ readonly dropped: number;
67
+ /** Tail lifetime. §12.7.5 suggests 5-15 minutes for activity. */
68
+ readonly ttlMs: number;
69
+ /** Maximum retained entries. Must be a positive integer. */
70
+ readonly capacity?: number;
71
+ }
72
+ /**
73
+ * Presence port. Tenant-first, async.
74
+ *
75
+ * Reads filter expired hints out rather than returning them with a flag: an
76
+ * expired hint is indistinguishable from one that was never written.
77
+ */
78
+ export interface PresenceStore {
79
+ publish(tenant: TenantId, input: PresenceHintInput): Promise<PresenceHint>;
80
+ read(tenant: TenantId, deviceId: string): Promise<PresenceHint | undefined>;
81
+ list(tenant: TenantId): Promise<readonly PresenceHint[]>;
82
+ }
83
+ /**
84
+ * Activity port. Tenant-first, async.
85
+ *
86
+ * Raises: `activity_capacity_invalid` (non-positive or non-integer capacity —
87
+ * an unbounded tail is exactly what this contract exists to prevent),
88
+ * `activity_batch_invalid` (empty batch or invalid producer drop count).
89
+ */
90
+ export interface ActivityStore {
91
+ append(tenant: TenantId, input: ActivityAppendInput): Promise<ActivityTail>;
92
+ read(tenant: TenantId, taskId: string): Promise<ActivityTail | undefined>;
93
+ }
94
+ /** Default tail capacity when an append does not specify one. */
95
+ export declare const DEFAULT_ACTIVITY_CAPACITY = 50;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Authenticated principals — layer 2 of the isolation model (§12.6.2).
3
+ *
4
+ * A handler never receives a raw tenant string; it receives a principal that
5
+ * already carries a minted {@link TenantId}. The two principal shapes are
6
+ * deliberately not one type with an optional `deviceId`: a control-plane caller
7
+ * that can write entitlements and a device that can write truth records have
8
+ * different authority, and a discriminated union makes a handler state which
9
+ * one it accepts.
10
+ *
11
+ * `keyId`/`keyEpoch` are **not** here. They are device-proof semantics
12
+ * (`plans/sprints/…sprint.md` §S6.2): the signing key's identity and rotation
13
+ * generation, resolved by looking up the device row during proof verification.
14
+ * Putting them on the principal would create permanently-empty fields on every
15
+ * principal minted by a non-proof path.
16
+ */
17
+ import type { TenantId } from './tenant';
18
+ /** Principal kinds. A composition may not invent a third without a contract change. */
19
+ export declare const PRINCIPAL_KINDS: readonly ['device', 'control-plane'];
20
+ export type PrincipalKind = (typeof PRINCIPAL_KINDS)[number];
21
+ /**
22
+ * A paired device acting inside one tenant/product.
23
+ *
24
+ * Built only from a device row loaded with the tenant as part of the lookup key
25
+ * (§12.6.2 layer 5) — never from claims a device asserted about itself.
26
+ */
27
+ export interface DevicePrincipal {
28
+ readonly kind: 'device';
29
+ readonly tenantId: TenantId;
30
+ readonly productId: string;
31
+ readonly deviceId: string;
32
+ }
33
+ /**
34
+ * The host's control plane acting on a tenant: entitlement writes, retention
35
+ * policy, administrative reads. Carries the operator identity for audit, which
36
+ * is opaque to the SDK.
37
+ */
38
+ export interface ControlPlanePrincipal {
39
+ readonly kind: 'control-plane';
40
+ readonly tenantId: TenantId;
41
+ readonly operatorId: string;
42
+ }
43
+ /** Anything that can address a tenant-scoped store. */
44
+ export type Principal = DevicePrincipal | ControlPlanePrincipal;
45
+ export declare function isDevicePrincipal(principal: Principal): principal is DevicePrincipal;
46
+ export declare function isControlPlanePrincipal(principal: Principal): principal is ControlPlanePrincipal;
47
+ /**
48
+ * The tenant every store call must be scoped to.
49
+ *
50
+ * Exists so a handler cannot accidentally read `principal.tenantId` off one
51
+ * principal and pass a different tenant to a store: the facade that binds
52
+ * stores to a tenant takes this, not a loose string.
53
+ */
54
+ export declare function principalTenant(principal: Principal): TenantId;
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Tenant storage entitlement, usage, reservation and retention (§12.7.6-12.7.7).
3
+ *
4
+ * The SDK does not know what a plan is. It never sees `free`, `pro`, a price, a
5
+ * currency, or a purchase flow — those belong to the host SaaS. What crosses
6
+ * this boundary is a *numeric, versioned* entitlement the host issues, and the
7
+ * constraint test asserts this file contains no plan-name or price vocabulary.
8
+ * The moment the SDK hardcodes a tier, every host is stuck with the SDK's
9
+ * commercial model.
10
+ *
11
+ * Byte counts are `bigint`. Serialization is a composition concern: JSON has no
12
+ * bigint, so a cloud handler renders them as decimal strings on the wire. Using
13
+ * `number` here would put a silent 2^53 ceiling into a storage quota contract.
14
+ *
15
+ * Reservation exists because Postgres and R2 have no shared transaction
16
+ * (§12.7.7). Every durable write reserves first, uploads second, finalizes
17
+ * third; the invariant `committed + reserved + expected <= hardLimit` is
18
+ * checked under the reservation lock, which is what makes concurrent uploads
19
+ * unable to oversell the tenant.
20
+ */
21
+ import type { ContentHash } from './blob';
22
+ import type { TenantId } from './tenant';
23
+ /**
24
+ * The host-issued numeric entitlement (§12.7.6, verbatim).
25
+ *
26
+ * `version` is monotonic and CAS-checked on write: a delayed control-plane
27
+ * update must not resurrect an older plan over a newer one.
28
+ */
29
+ export interface TenantStorageEntitlement {
30
+ tenantId: TenantId;
31
+ version: bigint;
32
+ hardLimitBytes: bigint;
33
+ maxObjectBytes: bigint;
34
+ maxInlineBytes: bigint;
35
+ mailboxLimitBytes: bigint;
36
+ retentionPolicyId: string;
37
+ /** Canonical ISO-8601 UTC instant — see {@link TenantStorageEntitlementInput}. */
38
+ downgradeGraceUntil?: string;
39
+ }
40
+ /**
41
+ * Measured tenant usage (§12.7.6, verbatim).
42
+ *
43
+ * Carries no `tenantId` because it is always read through a tenant-scoped port
44
+ * call — the tenant is the query, not a field of the answer.
45
+ */
46
+ export interface TenantStorageUsage {
47
+ committedObjectBytes: bigint;
48
+ committedInlineBytes: bigint;
49
+ reservedBytes: bigint;
50
+ mailboxBytes: bigint;
51
+ objectCount: bigint;
52
+ updatedAt: string;
53
+ }
54
+ /** Entitlement write payload. `tenantId` comes from the port's first parameter. */
55
+ export interface TenantStorageEntitlementInput {
56
+ readonly version: bigint;
57
+ readonly hardLimitBytes: bigint;
58
+ readonly maxObjectBytes: bigint;
59
+ readonly maxInlineBytes: bigint;
60
+ readonly mailboxLimitBytes: bigint;
61
+ readonly retentionPolicyId: string;
62
+ /**
63
+ * Deadline after which an over-limit tenant is suspended rather than blocked.
64
+ *
65
+ * Must be a **canonical ISO-8601 UTC instant** (`YYYY-MM-DDTHH:mm:ss.sssZ`);
66
+ * anything else is rejected with `timestamp_not_canonical`. The in-memory
67
+ * composition compares this deadline as a string and a SQL composition
68
+ * compares it as a `timestamptz`, and those two agree only on the canonical
69
+ * form — see `time.ts`.
70
+ */
71
+ readonly downgradeGraceUntil?: string;
72
+ }
73
+ /** Durable write classes a reservation can cover. */
74
+ export declare const STORAGE_WRITE_KINDS: readonly ['object', 'inline'];
75
+ export type StorageWriteKind = (typeof STORAGE_WRITE_KINDS)[number];
76
+ export declare const STORAGE_RESERVATION_STATES: readonly ['reserved', 'committed', 'aborted', 'expired'];
77
+ export type StorageReservationState = (typeof STORAGE_RESERVATION_STATES)[number];
78
+ export interface StorageReservation {
79
+ readonly tenantId: TenantId;
80
+ readonly reservationId: string;
81
+ readonly state: StorageReservationState;
82
+ readonly kind: StorageWriteKind;
83
+ readonly expectedBytes: bigint;
84
+ readonly contentHash: ContentHash;
85
+ readonly contentType: string;
86
+ readonly createdAt: string;
87
+ readonly expiresAt: string;
88
+ readonly settledAt?: string;
89
+ }
90
+ export interface StorageReservationInput {
91
+ readonly reservationId: string;
92
+ readonly kind: StorageWriteKind;
93
+ readonly expectedBytes: bigint;
94
+ readonly contentHash: ContentHash;
95
+ readonly contentType: string;
96
+ readonly ttlMs: number;
97
+ }
98
+ /**
99
+ * Finalize payload. Size/type are what the composition observed on the object
100
+ * store, not what the client promised — disagreement is
101
+ * `storage_integrity_mismatch`.
102
+ *
103
+ * Hash identity stays on the reservation as the authenticated daemon's
104
+ * declaration (ADR-024). Object-store HEAD does not independently observe a
105
+ * SHA-256 digest, so accepting one here would turn a copied declaration into a
106
+ * false verification claim.
107
+ */
108
+ export interface StorageFinalizeInput {
109
+ readonly reservationId: string;
110
+ readonly observedByteSize: bigint;
111
+ readonly observedContentType: string;
112
+ }
113
+ export interface StorageFinalizeResult {
114
+ readonly reservation: StorageReservation;
115
+ readonly usage: TenantStorageUsage;
116
+ /**
117
+ * True when this tenant already had the same content hash committed. The
118
+ * reserved bytes are released and nothing is added: same tenant, same hash,
119
+ * counted once (§12.7.6). Cross-tenant sharing never happens — usage is
120
+ * per-tenant even when the bytes are identical.
121
+ */
122
+ readonly deduplicated: boolean;
123
+ }
124
+ /** Effective write posture derived from entitlement + usage + clock (§12.7.8). */
125
+ export declare const STORAGE_WRITE_POSTURES: readonly ['normal', 'warning', 'blocked', 'suspended'];
126
+ export type StorageWritePosture = (typeof STORAGE_WRITE_POSTURES)[number];
127
+ export interface StorageStatus {
128
+ readonly entitlement: TenantStorageEntitlement;
129
+ readonly usage: TenantStorageUsage;
130
+ readonly posture: StorageWritePosture;
131
+ /** `committed + reserved` against `hardLimitBytes`, for host UI. */
132
+ readonly availableBytes: bigint;
133
+ readonly graceActive: boolean;
134
+ }
135
+ /**
136
+ * The five wire-stable storage error codes (§12.7.7). Renaming one is a
137
+ * breaking change for every host that branches on them.
138
+ */
139
+ export declare const STORAGE_ERROR_CODES: readonly ["storage_object_too_large", "storage_quota_exceeded", "storage_reservation_expired", "storage_integrity_mismatch", "storage_write_suspended"];
140
+ export type StorageErrorCode = (typeof STORAGE_ERROR_CODES)[number];
141
+ /** HTTP status mapping from §12.7.7. Compositions render these verbatim. */
142
+ export declare const STORAGE_ERROR_HTTP_STATUS: {
143
+ readonly storage_object_too_large: 413;
144
+ readonly storage_quota_exceeded: 507;
145
+ readonly storage_reservation_expired: 409;
146
+ readonly storage_integrity_mismatch: 422;
147
+ readonly storage_write_suspended: 423;
148
+ };
149
+ export interface MailboxUsageDeltaInput {
150
+ /** Signed. Negative deltas release bytes after retention deletes rows. */
151
+ readonly deltaBytes: bigint;
152
+ }
153
+ /**
154
+ * Quota port. Tenant-first, async.
155
+ *
156
+ * Raises: `storage_entitlement_missing`,
157
+ * `storage_entitlement_version_conflict` (carries the current entitlement),
158
+ * `storage_reservation_not_found`, and the five wire-stable codes above.
159
+ */
160
+ export interface QuotaStore {
161
+ readEntitlement(tenant: TenantId): Promise<TenantStorageEntitlement | undefined>;
162
+ /**
163
+ * Version CAS: a write at or below the stored version is rejected with the
164
+ * current row. Raises `timestamp_not_canonical` when `downgradeGraceUntil` is
165
+ * not a canonical ISO-8601 UTC instant.
166
+ */
167
+ writeEntitlement(tenant: TenantId, input: TenantStorageEntitlementInput): Promise<TenantStorageEntitlement>;
168
+ readUsage(tenant: TenantId): Promise<TenantStorageUsage>;
169
+ readStatus(tenant: TenantId): Promise<StorageStatus>;
170
+ /** Tenant-scoped lookup used to bind a finalize request to its reservation. */
171
+ readReservation(tenant: TenantId, reservationId: string): Promise<StorageReservation | undefined>;
172
+ /**
173
+ * Atomically checks `committed + reserved + expected <= hardLimitBytes` and,
174
+ * on success, adds `expectedBytes` to `reservedBytes`.
175
+ */
176
+ reserve(tenant: TenantId, input: StorageReservationInput): Promise<StorageReservation>;
177
+ /**
178
+ * Moves reserved bytes to committed after verifying the observed object.
179
+ * For object reservations, the composition commits the matching manifest in
180
+ * the same authority step as reservation/accounting settlement.
181
+ */
182
+ finalizeReservation(tenant: TenantId, input: StorageFinalizeInput): Promise<StorageFinalizeResult>;
183
+ /** Idempotent release. Already-settled reservations return their settled row. */
184
+ abortReservation(tenant: TenantId, reservationId: string): Promise<StorageReservation>;
185
+ /** Cleanup hook: expires reservations past their TTL and releases their bytes. */
186
+ expireReservations(tenant: TenantId): Promise<readonly StorageReservation[]>;
187
+ /** Mailbox bytes are platform-protection accounting, bounded by `mailboxLimitBytes`. */
188
+ applyMailboxDelta(tenant: TenantId, input: MailboxUsageDeltaInput): Promise<TenantStorageUsage>;
189
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The composition contract: the full set of store ports, plus the clock seam.
3
+ *
4
+ * Every method on every port in this package obeys two rules, and
5
+ * `src/__tests__/constraints.test.ts` enumerates them method by method to prove
6
+ * it (sprint I7):
7
+ *
8
+ * 1. **Async.** Every method returns a `Promise`. A synchronous port would
9
+ * silently exclude SQL and object-store compositions from the contract.
10
+ * 2. **Tenant-first.** Every method's first parameter is a required
11
+ * {@link TenantId}. There is no bare `deviceId`/`taskId` lookup anywhere —
12
+ * §12.6.2 layer 3 forbids the index such a lookup would need, and layer 5
13
+ * requires the tenant to be part of the lookup key rather than a second
14
+ * comparison step that can be forgotten.
15
+ *
16
+ * No port method can change the tenant of an existing row. That is not an
17
+ * omission: "move this to another tenant" is the one operation that would make
18
+ * every cross-tenant assertion in the conformance suite unfalsifiable.
19
+ */
20
+ import type { ActivityStore, PresenceStore } from './presence';
21
+ import type { BoardStore } from './board';
22
+ import type { MailboxStore } from './mailbox';
23
+ import type { ObjectStore } from './blob';
24
+ import type { QuotaStore } from './quota';
25
+ import type { TruthStore } from './truth';
26
+ /**
27
+ * Injected time.
28
+ *
29
+ * TTL semantics (presence expiry, activity expiry, reservation expiry) are
30
+ * behavior the conformance suite has to assert deterministically, which is
31
+ * impossible against a wall clock. Compositions inject one; nothing in core
32
+ * calls `Date.now()` directly.
33
+ */
34
+ export interface Clock {
35
+ now(): Date;
36
+ }
37
+ /** A clock pinned to a fixed instant, advanced explicitly. Used by tests and the in-memory reference. */
38
+ export interface MutableClock extends Clock {
39
+ advance(ms: number): void;
40
+ set(instant: Date): void;
41
+ }
42
+ /** Every port a composition must supply. */
43
+ export interface CoreStores {
44
+ readonly mailbox: MailboxStore;
45
+ readonly board: BoardStore;
46
+ readonly truth: TruthStore;
47
+ readonly presence: PresenceStore;
48
+ readonly activity: ActivityStore;
49
+ readonly objects: ObjectStore;
50
+ readonly quota: QuotaStore;
51
+ }
52
+ /** Names of the ports in {@link CoreStores}, in contract order. */
53
+ export declare const CORE_STORE_NAMES: readonly ['mailbox', 'board', 'truth', 'presence', 'activity', 'objects', 'quota'];
54
+ export type CoreStoreName = (typeof CORE_STORE_NAMES)[number];
@@ -0,0 +1,48 @@
1
+ /**
2
+ * A validated tenant identifier.
3
+ *
4
+ * Nominal by construction: structurally a `string`, but the phantom brand makes
5
+ * it unassignable from an arbitrary string. Assignment the other way
6
+ * (`TenantId` → `string`) stays legal on purpose, so composition code can pass
7
+ * it as a SQL parameter or key prefix without a cast.
8
+ */
9
+ export type TenantId = string & {
10
+ readonly __byokTenantId: unique symbol;
11
+ };
12
+ /**
13
+ * Upper bound on tenant id length. Not a security boundary — a bound so a
14
+ * pathological id cannot become an unbounded key prefix inside a store.
15
+ */
16
+ export declare const TENANT_ID_MAX_LENGTH = 200;
17
+ /**
18
+ * Separator for flat composite keys. `NUL` cannot appear in a tenant id that a
19
+ * control plane can express in a URL, a header, or a SQL identifier, and
20
+ * {@link tenantId} rejects it outright — so a composite key is never ambiguous
21
+ * between `(a, bc)` and `(ab, c)`.
22
+ */
23
+ export declare const TENANT_KEY_SEPARATOR = "\0";
24
+ /**
25
+ * The single mint point for {@link TenantId}.
26
+ *
27
+ * Fail-closed: empty, whitespace-padded, over-long, non-string, or
28
+ * `NUL`-bearing values are rejected rather than normalized. Normalizing here
29
+ * would create a second source of truth for "which tenant is this", because the
30
+ * control plane that issued the id would then disagree with the SDK about its
31
+ * canonical form.
32
+ *
33
+ * @throws {ByokCoreError} code `tenant_id_invalid`.
34
+ */
35
+ export declare function tenantId(value: string): TenantId;
36
+ /**
37
+ * Non-throwing form of {@link tenantId}, for surfaces that must answer "is this
38
+ * a tenant id?" without building an error (route matching, log redaction).
39
+ * Accepts exactly what {@link tenantId} accepts, never more.
40
+ */
41
+ export declare function isTenantId(value: unknown): value is TenantId;
42
+ /**
43
+ * Composite key helper for compositions that need a flat key space (in-memory
44
+ * maps, KV namespaces). SQL compositions use a tenant-prefixed composite
45
+ * primary key instead — §12.6.2 layer 3 forbids a bare device/task index
46
+ * regardless of which representation a composition picks.
47
+ */
48
+ export declare function tenantKey(tenant: TenantId, ...parts: readonly string[]): string;
package/dist/time.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Exactly what `toISOString()` produces: four-digit year, `T` separator,
3
+ * millisecond precision, literal `Z`. No offsets, no omitted milliseconds, no
4
+ * expanded-year form — each of those breaks the lexicographic-order property.
5
+ */
6
+ export declare const CANONICAL_TIMESTAMP_PATTERN: RegExp;
7
+ /**
8
+ * True when `value` is a canonical ISO-8601 UTC instant.
9
+ *
10
+ * Two checks, in order: the pattern pins the *shape*, then a round trip
11
+ * through `Date` pins *calendar validity* — `2026-02-30T00:00:00.000Z` matches
12
+ * the pattern and is not an instant.
13
+ */
14
+ export declare function isCanonicalTimestamp(value: unknown): value is string;
15
+ /**
16
+ * Fail-closed gate for a caller-supplied instant.
17
+ *
18
+ * @param value The timestamp as received from the caller.
19
+ * @param field Field name, so the failure names the contract that was missed
20
+ * rather than just the bad string.
21
+ * @returns The same string, so call sites can validate and assign in one step.
22
+ * @throws {ByokCoreError} code `timestamp_not_canonical`.
23
+ */
24
+ export declare function assertCanonicalTimestamp(value: string, field: string): string;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Truth records: attested metadata with two different write models (§12.3, §12.6.4).
3
+ *
4
+ * | kind | write model | conflict model |
5
+ * | --------------- | ------------------------------ | --------------------------------------- |
6
+ * | `task.terminal` | first write per task, immutable | different hash → `terminal_conflict` |
7
+ * | `profile` | per-key snapshot | `expectedRev` CAS |
8
+ * | `memory` | per-key snapshot | `expectedRev` CAS |
9
+ *
10
+ * The store is deliberately dumb about content. It can match, sort, and return
11
+ * by tenant/kind/key/rev/hash, and that is the whole of its authority: no
12
+ * summarizing, no merging, no relevance ranking (§12.3). Merge decisions belong
13
+ * to the device that holds the context, which is why a conflict hands back the
14
+ * current snapshot and stops.
15
+ *
16
+ * The manifest listing returns metadata only — never bodies. Selecting which
17
+ * bodies to fetch is a local decision (§S6.4), so shipping bodies in the list
18
+ * response would both defeat that and make the response unbounded.
19
+ */
20
+ import type { ContentHash } from './blob';
21
+ import type { TenantId } from './tenant';
22
+ export declare const TRUTH_RECORD_KINDS: readonly ['task.terminal', 'profile', 'memory'];
23
+ export type TruthRecordKind = (typeof TRUTH_RECORD_KINDS)[number];
24
+ /**
25
+ * Where the record body lives. Small payloads may be inline and still count
26
+ * against tenant usage (§12.7.6); larger ones are an object reference.
27
+ */
28
+ export type TruthBodyRef = {
29
+ readonly kind: 'inline';
30
+ readonly body: string;
31
+ } | {
32
+ readonly kind: 'object';
33
+ readonly hash: ContentHash;
34
+ };
35
+ export interface TruthRecord {
36
+ readonly tenantId: TenantId;
37
+ readonly kind: TruthRecordKind;
38
+ /** For `task.terminal` this is the task id; for snapshots, the host's key. */
39
+ readonly recordKey: string;
40
+ /** `1` for the first write. Terminal records never advance past `1`. */
41
+ readonly rev: number;
42
+ readonly contentHash: ContentHash;
43
+ readonly byteSize: bigint;
44
+ readonly body: TruthBodyRef;
45
+ readonly label?: string;
46
+ /** Idempotency key of the accepted write, for replay detection. */
47
+ readonly requestId?: string;
48
+ readonly writtenAt: string;
49
+ }
50
+ /** Manifest projection: everything needed to decide *whether* to fetch a body. */
51
+ export interface TruthManifestEntry {
52
+ readonly kind: TruthRecordKind;
53
+ readonly recordKey: string;
54
+ readonly rev: number;
55
+ readonly contentHash: ContentHash;
56
+ readonly byteSize: bigint;
57
+ readonly label?: string;
58
+ readonly updatedAt: string;
59
+ }
60
+ export interface TerminalWriteInput {
61
+ readonly taskId: string;
62
+ readonly contentHash: ContentHash;
63
+ readonly byteSize: bigint;
64
+ readonly body: TruthBodyRef;
65
+ readonly label?: string;
66
+ readonly requestId?: string;
67
+ }
68
+ export interface SnapshotWriteInput {
69
+ readonly kind: 'profile' | 'memory';
70
+ readonly recordKey: string;
71
+ /** `0` asserts "no record yet". Any other value must equal the stored `rev`. */
72
+ readonly expectedRev: number;
73
+ readonly contentHash: ContentHash;
74
+ readonly byteSize: bigint;
75
+ readonly body: TruthBodyRef;
76
+ readonly label?: string;
77
+ readonly requestId?: string;
78
+ }
79
+ export interface TruthRecordSelector {
80
+ readonly kind: TruthRecordKind;
81
+ readonly recordKey: string;
82
+ }
83
+ export interface TruthManifestQuery {
84
+ readonly kind?: TruthRecordKind;
85
+ readonly keyPrefix?: string;
86
+ readonly limit?: number;
87
+ }
88
+ /**
89
+ * Truth port. Tenant-first, async.
90
+ *
91
+ * Raises: `terminal_conflict` (same task, different hash — carries the record
92
+ * already committed), `truth_revision_conflict` (`expectedRev` missed — carries
93
+ * the current record), `truth_record_not_found`.
94
+ */
95
+ export interface TruthStore {
96
+ /**
97
+ * Writes the first terminal record for a task.
98
+ *
99
+ * Replaying the identical hash returns the original record unchanged, so a
100
+ * retry after a lost response is safe. A different hash for the same task is
101
+ * a conflict: the first fact is never overwritten.
102
+ */
103
+ writeTerminal(tenant: TenantId, input: TerminalWriteInput): Promise<TruthRecord>;
104
+ /** Per-key snapshot write under `expectedRev` CAS. The store never merges bodies. */
105
+ writeSnapshot(tenant: TenantId, input: SnapshotWriteInput): Promise<TruthRecord>;
106
+ getRecord(tenant: TenantId, selector: TruthRecordSelector): Promise<TruthRecord | undefined>;
107
+ /** Metadata only — key/rev/hash/size/label, no bodies. */
108
+ listManifest(tenant: TenantId, query: TruthManifestQuery): Promise<readonly TruthManifestEntry[]>;
109
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@byok-sdk/core",
3
+ "version": "0.1.0",
4
+ "description": "BYOK SDK platform contracts: branded tenant identity, tenant-first store ports, device proof canonicalization, and the composition conformance reference",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Ancienttwo/byok-sdk.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/Ancienttwo/byok-sdk/issues"
14
+ },
15
+ "homepage": "https://github.com/Ancienttwo/byok-sdk#readme",
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "sideEffects": false,
20
+ "main": "./dist/index.js",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "zod": "^4.4.3"
40
+ },
41
+ "scripts": {
42
+ "build": "tsup && tsc -p tsconfig.build.json",
43
+ "dev": "tsup --watch",
44
+ "test": "vitest run",
45
+ "test:watch": "vitest",
46
+ "typecheck": "tsc --noEmit",
47
+ "clean": "rm -rf dist"
48
+ }
49
+ }