@byok-sdk/cloud 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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -0
  3. package/dist/auth/bearer.d.ts +25 -0
  4. package/dist/auth/device-proof.d.ts +38 -0
  5. package/dist/auth/plane.d.ts +67 -0
  6. package/dist/auth/tokens.d.ts +37 -0
  7. package/dist/auth/verify.d.ts +22 -0
  8. package/dist/board-projection.d.ts +9 -0
  9. package/dist/capabilities.d.ts +71 -0
  10. package/dist/cloud.d.ts +123 -0
  11. package/dist/composition/in-memory.d.ts +55 -0
  12. package/dist/coordination-client.d.ts +87 -0
  13. package/dist/coordination.d.ts +29 -0
  14. package/dist/crypto/port.d.ts +44 -0
  15. package/dist/crypto/web-crypto.d.ts +28 -0
  16. package/dist/errors.d.ts +45 -0
  17. package/dist/handlers/auth.d.ts +21 -0
  18. package/dist/handlers/blobs.d.ts +39 -0
  19. package/dist/handlers/board.d.ts +21 -0
  20. package/dist/handlers/capabilities.d.ts +14 -0
  21. package/dist/handlers/events.d.ts +33 -0
  22. package/dist/handlers/messages.d.ts +28 -0
  23. package/dist/handlers/presence.d.ts +13 -0
  24. package/dist/handlers/shared.d.ts +30 -0
  25. package/dist/handlers/truth.d.ts +15 -0
  26. package/dist/inbound.d.ts +35 -0
  27. package/dist/index.d.ts +57 -0
  28. package/dist/index.js +2419 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/router/registry.d.ts +56 -0
  31. package/dist/stores/in-memory/blobs.d.ts +89 -0
  32. package/dist/stores/in-memory/dedup.d.ts +17 -0
  33. package/dist/stores/in-memory/device-directory.d.ts +19 -0
  34. package/dist/stores/in-memory/index.d.ts +36 -0
  35. package/dist/stores/in-memory/nonces.d.ts +22 -0
  36. package/dist/stores/in-memory/pairing-codes.d.ts +18 -0
  37. package/dist/stores/in-memory/proof-receipts.d.ts +11 -0
  38. package/dist/stores/in-memory/rate-limiter.d.ts +13 -0
  39. package/dist/stores/in-memory/receipts.d.ts +21 -0
  40. package/dist/stores/in-memory/sequence.d.ts +11 -0
  41. package/dist/stores/in-memory/task-attempts.d.ts +36 -0
  42. package/dist/stores/ports-contract.d.ts +20 -0
  43. package/dist/stores/ports.d.ts +309 -0
  44. package/dist/tenant-stores.d.ts +124 -0
  45. package/dist/truth/contract.d.ts +149 -0
  46. package/dist/truth/errors.d.ts +8 -0
  47. package/package.json +52 -0
@@ -0,0 +1,309 @@
1
+ /**
2
+ * The cloud-local store ports.
3
+ *
4
+ * `@byok-sdk/core` owns the platform ports every composition shares (mailbox,
5
+ * board, truth, objects, quota). These are the ones only a HOSTED device
6
+ * surface needs — the auth plane and the task-attempt bookkeeping the device
7
+ * routes read. S2 deliberately kept them out of core: they are hosted-surface
8
+ * concerns, and S4A's schema work is the right moment to decide their durable
9
+ * home. Cloud owning them keeps this slice additive and revertible.
10
+ *
11
+ * Two rules, same as core's (`stores.ts`):
12
+ *
13
+ * 1. **Async.** Every method returns a `Promise`, so a SQL or KV composition
14
+ * can implement the same contract the in-memory reference does.
15
+ * 2. **Tenant-first.** Every method's first parameter is a required
16
+ * `TenantId` — with exactly two documented exceptions below, each of
17
+ * which is pre-tenant *by construction* because the credential it is
18
+ * handed is itself what resolves the tenant.
19
+ *
20
+ * The two exceptions:
21
+ *
22
+ * - {@link DeviceDirectory.resolveByDeviceId} — `POST /byok/challenge` and
23
+ * `POST /byok/token` carry only a deviceId (the pinned wire contract), and
24
+ * the row is what tells the deployment which tenant to mint for.
25
+ * - {@link PairingCodeStore.redeem} — the code IS the tenant lookup; it was
26
+ * minted against a tenant that only the host's control plane knew.
27
+ *
28
+ * Neither is reachable through {@link TenantStores} (see `tenant-stores.ts`),
29
+ * so a device-facing handler cannot call them.
30
+ *
31
+ * The byte-proxy trio that used to be a third pre-tenant exception is no
32
+ * longer part of this bundle at all: it moved to {@link BlobContentProxy},
33
+ * an OPTIONAL composition input rather than a {@link CloudStores} member,
34
+ * because a composition backed by object storage physically cannot proxy
35
+ * bytes (see the blobs section below).
36
+ */
37
+ import type { StorageReservation, TenantId } from '@byok-sdk/core';
38
+ export interface DeviceRecord {
39
+ readonly tenantId: TenantId;
40
+ readonly productId: string;
41
+ readonly deviceId: string;
42
+ readonly deviceName: string;
43
+ /** Ed25519 public key, base64url-encoded (JWK `x` form). */
44
+ readonly devicePublicKey: string;
45
+ /** Proof key identity stored on the row; verifier claims never supply a default. */
46
+ readonly proofKeyId: string;
47
+ /** Current proof signing-key rotation generation. */
48
+ readonly proofKeyEpoch: number;
49
+ readonly revoked: boolean;
50
+ }
51
+ /** Everything `POST /byok/pair` knows at registration time. `tenantId` is the store's first parameter; `revoked` is the store's own to set. */
52
+ export interface DeviceRegistration {
53
+ readonly productId: string;
54
+ readonly deviceId: string;
55
+ readonly deviceName: string;
56
+ readonly devicePublicKey: string;
57
+ readonly proofKeyId: string;
58
+ readonly proofKeyEpoch: number;
59
+ }
60
+ export interface DeviceDirectory {
61
+ register(tenant: TenantId, input: DeviceRegistration): Promise<DeviceRecord>;
62
+ /** The row `tenant` owns under `deviceId` — `undefined` including when the device exists under a DIFFERENT tenant. */
63
+ get(tenant: TenantId, deviceId: string): Promise<DeviceRecord | undefined>;
64
+ revoke(tenant: TenantId, deviceId: string): Promise<void>;
65
+ list(tenant: TenantId): Promise<readonly DeviceRecord[]>;
66
+ /** Pre-tenant. Two callers only: `POST /byok/challenge` and `POST /byok/token`. Never exposed through the tenant facade. */
67
+ resolveByDeviceId(deviceId: string): Promise<DeviceRecord | undefined>;
68
+ }
69
+ /**
70
+ * The identity a pairing code carries. Minted out-of-band by the host's own
71
+ * auth/device-flow UI — the only party that knows which tenant a human is
72
+ * acting for. Deliberately NOT a wire field: `PairRequest` has no tenant of
73
+ * its own, so a device can never name the tenant it lands in.
74
+ */
75
+ export interface PairingCodeClaims {
76
+ readonly tenantId: TenantId;
77
+ readonly productId: string;
78
+ }
79
+ export interface PairingCodeInfo {
80
+ readonly code: string;
81
+ /** Canonical ISO-8601 UTC instant. */
82
+ readonly expiresAt: string;
83
+ }
84
+ export interface PairingCodeIssueInput {
85
+ readonly code: string;
86
+ readonly productId: string;
87
+ readonly expiresAt: string;
88
+ }
89
+ export interface PairingCodeStore {
90
+ issue(tenant: TenantId, input: PairingCodeIssueInput): Promise<PairingCodeInfo>;
91
+ /**
92
+ * Validate and CONSUME a code, returning the claims it was minted with, or
93
+ * `undefined` when it is unknown, expired, or already used — one answer for
94
+ * all three, so a redeem attempt is never an existence oracle. Single-use is
95
+ * what makes the caller's "redeem, then register the device row" sequence
96
+ * exclusive: a second redeem can never reach the registration step.
97
+ */
98
+ redeem(code: string): Promise<PairingCodeClaims | undefined>;
99
+ }
100
+ export interface NonceStore {
101
+ issue(tenant: TenantId, deviceId: string): Promise<string>;
102
+ /** `true` iff the nonce exists for this (tenant, device), is unexpired, and has not been consumed. Never mutates. */
103
+ validate(tenant: TenantId, deviceId: string, nonce: string): Promise<boolean>;
104
+ /** Consume the nonce. Called only after every other check on the request has passed. */
105
+ markUsed(tenant: TenantId, nonce: string): Promise<void>;
106
+ }
107
+ export interface InboundDedupStore {
108
+ /**
109
+ * `true` when `envelopeId` was already seen for this (tenant, device);
110
+ * otherwise records it and returns `false`. Bounded per device — the wire is
111
+ * at-least-once (§9); this makes processing at-most-once without an
112
+ * unbounded set.
113
+ */
114
+ checkAndRecord(tenant: TenantId, deviceId: string, envelopeId: string): Promise<boolean>;
115
+ }
116
+ export declare const TASK_ATTEMPT_STATUSES: readonly ['offered', 'claimed', 'running', 'complete', 'failed', 'cancelled'];
117
+ export type TaskAttemptStatus = (typeof TASK_ATTEMPT_STATUSES)[number];
118
+ export interface TaskAttempt {
119
+ readonly tenantId: TenantId;
120
+ readonly taskId: string;
121
+ /** The device the offer was addressed to. */
122
+ readonly deviceId: string;
123
+ /** Set by `task.claim`, and only by the first one. Until then the task has no owner and the gate lets any of this tenant's devices through, matching the reference server. */
124
+ readonly ownerDeviceId?: string;
125
+ readonly status: TaskAttemptStatus;
126
+ readonly updatedAt: string;
127
+ }
128
+ export interface TaskAttemptStore {
129
+ /** Called when an offer is enqueued: records the pending attempt with no owner yet. */
130
+ open(tenant: TenantId, input: {
131
+ readonly taskId: string;
132
+ readonly deviceId: string;
133
+ }): Promise<TaskAttempt>;
134
+ get(tenant: TenantId, taskId: string): Promise<TaskAttempt | undefined>;
135
+ /** First claim wins the ownership; a later claim by the same device is idempotent. No-op (returns `undefined`) for a task this tenant never offered. */
136
+ claim(tenant: TenantId, input: {
137
+ readonly taskId: string;
138
+ readonly deviceId: string;
139
+ }): Promise<TaskAttempt | undefined>;
140
+ /** Record a lifecycle transition. No-op (returns `undefined`) for an unknown task — same shape as the reference server's per-type handlers. */
141
+ recordStatus(tenant: TenantId, input: {
142
+ readonly taskId: string;
143
+ readonly status: TaskAttemptStatus;
144
+ }): Promise<TaskAttempt | undefined>;
145
+ }
146
+ export interface RequestReceipt {
147
+ readonly tenantId: TenantId;
148
+ readonly key: string;
149
+ /** Opaque payload the caller recorded — for terminals, the encoded envelope. */
150
+ readonly body: string;
151
+ readonly recordedAt: string;
152
+ }
153
+ export interface RequestReceiptStore {
154
+ /**
155
+ * Record a receipt under `key`, or return the existing one untouched.
156
+ * `created` is `false` for a replay — the FIRST terminal is the fact
157
+ * (§12.6.4: 不覆写第一份事实), and a retry of the same terminal must not
158
+ * overwrite it.
159
+ */
160
+ record(tenant: TenantId, input: {
161
+ readonly key: string;
162
+ readonly body: string;
163
+ }): Promise<{
164
+ readonly receipt: RequestReceipt;
165
+ readonly created: boolean;
166
+ }>;
167
+ get(tenant: TenantId, key: string): Promise<RequestReceipt | undefined>;
168
+ }
169
+ export interface ProofRequestReceipt {
170
+ readonly tenantId: TenantId;
171
+ readonly deviceId: string;
172
+ readonly requestId: string;
173
+ readonly operation: string;
174
+ readonly resource: string;
175
+ readonly bodySha256: string;
176
+ readonly bodySize: bigint;
177
+ readonly responseStatus: number;
178
+ readonly responseBody: string;
179
+ readonly recordedAt: string;
180
+ }
181
+ export type ProofRequestReceiptInput = Omit<ProofRequestReceipt, 'tenantId' | 'recordedAt'>;
182
+ /**
183
+ * First-result-wins replay store. The application layer compares every stored
184
+ * binding before returning an exact replay; a reused request id with any
185
+ * different binding is a conflict, never a second write.
186
+ */
187
+ export interface ProofRequestReceiptStore {
188
+ record(tenant: TenantId, input: ProofRequestReceiptInput): Promise<{
189
+ readonly receipt: ProofRequestReceipt;
190
+ readonly created: boolean;
191
+ }>;
192
+ get(tenant: TenantId, deviceId: string, requestId: string): Promise<ProofRequestReceipt | undefined>;
193
+ }
194
+ /**
195
+ * The daemon's redelivery cursor is the envelope-level `seq`, which has to be
196
+ * baked into the envelope BEFORE it can be handed to `MailboxStore.append` —
197
+ * the mailbox transports opaque bytes and cannot number an envelope it is
198
+ * being given. So the delivery number is allocated here first, and the
199
+ * enqueue path then asserts the mailbox agreed (`mailbox_seq_mismatch`)
200
+ * rather than letting two counters drift apart silently.
201
+ *
202
+ * A port rather than a counter held by the composition, so no handler-adjacent
203
+ * module carries mutable state (S3.5 boxes 14-15).
204
+ */
205
+ export interface DeviceSequenceStore {
206
+ next(tenant: TenantId, deviceId: string): Promise<number>;
207
+ }
208
+ export interface BlobDeclaration {
209
+ readonly size: number;
210
+ readonly contentType: string;
211
+ /** Canonical `sha256:<64 lowercase hex>` — the schema rejected anything else before this store sees it. */
212
+ readonly contentHash: string;
213
+ }
214
+ /** What object-store metadata can actually observe at finalize (ADR-024). */
215
+ export interface BlobObservation {
216
+ readonly observedByteSize: bigint;
217
+ readonly observedContentType: string;
218
+ }
219
+ export type BlobWriteResult = {
220
+ readonly ok: true;
221
+ } | {
222
+ readonly ok: false;
223
+ readonly reason: string;
224
+ };
225
+ export interface BlobContent {
226
+ readonly data: Uint8Array;
227
+ readonly contentType: string;
228
+ }
229
+ /**
230
+ * The capability-minting half of blobs: what EVERY composition can honestly
231
+ * provide, whoever holds the bytes.
232
+ *
233
+ * Both methods are tenant-first, and both are reachable through {@link
234
+ * TenantStores}. There is no pre-tenant method left on this port — see
235
+ * {@link BlobContentProxy} for where the other three went and why.
236
+ */
237
+ export interface CloudBlobStore {
238
+ /** Mint only from an already-admitted object reservation. */
239
+ createUpload(tenant: TenantId, reservation: StorageReservation): Promise<{
240
+ readonly blobId: string;
241
+ readonly uploadUrl: string;
242
+ }>;
243
+ /** Observe existence/size/type while proving this blob belongs to this reservation. */
244
+ observeUpload(tenant: TenantId, blobId: string, reservation: StorageReservation): Promise<BlobObservation | undefined>;
245
+ /** A presigned GET URL for a blob THIS tenant owns that has finished uploading; `undefined` otherwise — including for another tenant's blob. */
246
+ getDownloadUrl(tenant: TenantId, blobId: string): Promise<string | undefined>;
247
+ }
248
+ /**
249
+ * The byte-proxying half: an OPTIONAL composition input, not a port.
250
+ *
251
+ * These three serve the two `/byok/blobs/:id/content` routes, which exist
252
+ * because the in-memory and self-hosted compositions have nowhere else to put
253
+ * bytes — cloud has to carry them. A composition whose bytes live in object
254
+ * storage (S3/R2) hands the device a presigned URL to the object store itself
255
+ * and never sees a byte, so it cannot implement these at all.
256
+ *
257
+ * That is why this is a separate interface supplied at composition time rather
258
+ * than a member of {@link CloudStores}: "this deployment cannot proxy bytes"
259
+ * becomes a type-level fact (the proxy is simply absent) instead of three
260
+ * methods that throw. The alternatives were both anti-patterns — a conformance
261
+ * suite that skips three methods for one composition is a subset waiver, and
262
+ * one that asserts a typed rejection proves nothing
263
+ * (docs/researches/s4a-dataplane-design.md §6).
264
+ *
265
+ * All three are pre-tenant by construction: the `/content` routes are
266
+ * presigned, not bearer-authed, so there is no principal in scope at all and
267
+ * the HMAC signature over the blob id is the whole credential. They stay off
268
+ * {@link TenantStores} for the same reason the two exceptions above do.
269
+ *
270
+ * A deployment supplying a proxy must also declare `blobs.contentproxy`
271
+ * (ADR-010): the routes mount on proxy-presence AND declaration, never on
272
+ * either alone.
273
+ */
274
+ export interface BlobContentProxy {
275
+ verifySignedUrl(blobId: string, action: 'put' | 'get', sig: string, exp: number): Promise<boolean>;
276
+ writeContent(blobId: string, data: Uint8Array): Promise<BlobWriteResult>;
277
+ readContent(blobId: string): Promise<BlobContent | undefined>;
278
+ }
279
+ /**
280
+ * Step 0 of the inbound gate. S3a ships an allow-all reference (a hosted
281
+ * deployment's real limiter is edge/infra work, not handler work) — what
282
+ * matters now is that the SEAM sits at gate position 0, before the type-allow
283
+ * check, so a flood of garbage-typed envelopes costs the same budget as a
284
+ * flood of well-formed ones once a real limiter is plugged in.
285
+ */
286
+ export interface InboundRateLimiter {
287
+ consume(tenant: TenantId, deviceId: string): Promise<boolean>;
288
+ }
289
+ /**
290
+ * All-or-nothing: a composition supplies every port here or it is not a
291
+ * composition. {@link BlobContentProxy} is deliberately absent — it is an
292
+ * optional input to `createByokCloud`, not a port, precisely so that the
293
+ * bundle can stay all-or-nothing while byte proxying stays optional.
294
+ */
295
+ export interface CloudStores {
296
+ readonly devices: DeviceDirectory;
297
+ readonly pairingCodes: PairingCodeStore;
298
+ readonly nonces: NonceStore;
299
+ readonly dedup: InboundDedupStore;
300
+ readonly tasks: TaskAttemptStore;
301
+ readonly receipts: RequestReceiptStore;
302
+ readonly proofReceipts: ProofRequestReceiptStore;
303
+ readonly sequence: DeviceSequenceStore;
304
+ readonly blobs: CloudBlobStore;
305
+ readonly rateLimiter: InboundRateLimiter;
306
+ }
307
+ /** Names of the ports in {@link CloudStores}, in contract order. */
308
+ export declare const CLOUD_STORE_NAMES: readonly ['devices', 'pairingCodes', 'nonces', 'dedup', 'tasks', 'receipts', 'proofReceipts', 'sequence', 'blobs', 'rateLimiter'];
309
+ export type CloudStoreName = (typeof CLOUD_STORE_NAMES)[number];
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Layer 2 of the six-layer isolation model (§12.6.2) — the tenant-closed
3
+ * facade a handler actually receives.
4
+ *
5
+ * `@byok-sdk/core`'s `stores.ts` deliberately left this undefined: it is shaped by
6
+ * how handlers are written, and the first handlers live here. This is that
7
+ * shape, and the two properties it exists to enforce are:
8
+ *
9
+ * 1. **It can only be built from an authenticated principal.** {@link
10
+ * tenantStoresFor} is the only constructor, and its first parameter is a
11
+ * `Principal` — a value that only `authenticateBearer` (`auth/bearer.ts`)
12
+ * mints, and only from a device ROW. There is no path from a device-supplied
13
+ * string to a facade.
14
+ * 2. **No handler ever passes a `TenantId` again.** Every method below has the
15
+ * tenant pre-applied, so a handler cannot read `principal.tenantId` off one
16
+ * principal and hand a different tenant to a store. Grep the handler tree
17
+ * for `TenantId` and you will not find one — asserted by
18
+ * `src/__tests__/constraints.test.ts`.
19
+ *
20
+ * The three pre-tenant store methods (`resolveByDeviceId`, `redeem`, the
21
+ * presigned blob calls) are deliberately absent from this surface: a
22
+ * device-facing handler must not be able to reach them.
23
+ */
24
+ import { type ActivityAppendInput, type ActivityTail, type BoardClaimInput, type BoardItem, type BoardItemInput, type BoardListQuery, type BoardPage, type BoardStatusUpdateInput, type BoardUnclaimInput, type CoreStores, type MailboxAdvanceCursorInput, type MailboxAppendInput, type MailboxCursorState, type MailboxMessage, type MailboxPage, type MailboxReadQuery, type Principal, type PresenceHint, type PresenceHintInput, type StorageFinalizeInput, type StorageFinalizeResult, type StorageReservation, type StorageReservationInput, type TenantId } from '@byok-sdk/core';
25
+ import type { BlobObservation, CloudStores, DeviceRecord, RequestReceipt, TaskAttempt, TaskAttemptStatus } from './stores/ports';
26
+ export interface TenantBoundMailbox {
27
+ append(input: MailboxAppendInput): Promise<MailboxMessage>;
28
+ /** Pure read. Never advances the cursor — the daemon's next poll is the only ack. */
29
+ readAfter(query: MailboxReadQuery): Promise<MailboxPage>;
30
+ advanceCursor(input: MailboxAdvanceCursorInput): Promise<MailboxCursorState>;
31
+ readCursor(deviceId: string): Promise<MailboxCursorState>;
32
+ }
33
+ export interface TenantBoundBoard {
34
+ create(input: BoardItemInput): Promise<BoardItem>;
35
+ get(itemId: string): Promise<BoardItem | undefined>;
36
+ list(query: BoardListQuery): Promise<BoardPage>;
37
+ claim(input: BoardClaimInput): Promise<BoardItem>;
38
+ unclaim(input: BoardUnclaimInput): Promise<BoardItem>;
39
+ updateStatus(input: BoardStatusUpdateInput): Promise<BoardItem>;
40
+ }
41
+ export interface TenantBoundPresence {
42
+ publish(input: PresenceHintInput): Promise<PresenceHint>;
43
+ read(deviceId: string): Promise<PresenceHint | undefined>;
44
+ list(): Promise<readonly PresenceHint[]>;
45
+ }
46
+ export interface TenantBoundActivity {
47
+ append(input: ActivityAppendInput): Promise<ActivityTail>;
48
+ read(taskId: string): Promise<ActivityTail | undefined>;
49
+ }
50
+ export interface TenantBoundDevices {
51
+ get(deviceId: string): Promise<DeviceRecord | undefined>;
52
+ list(): Promise<readonly DeviceRecord[]>;
53
+ revoke(deviceId: string): Promise<void>;
54
+ }
55
+ export interface TenantBoundTaskAttempts {
56
+ open(input: {
57
+ readonly taskId: string;
58
+ readonly deviceId: string;
59
+ }): Promise<TaskAttempt>;
60
+ get(taskId: string): Promise<TaskAttempt | undefined>;
61
+ claim(input: {
62
+ readonly taskId: string;
63
+ readonly deviceId: string;
64
+ }): Promise<TaskAttempt | undefined>;
65
+ recordStatus(input: {
66
+ readonly taskId: string;
67
+ readonly status: TaskAttemptStatus;
68
+ }): Promise<TaskAttempt | undefined>;
69
+ }
70
+ export interface TenantBoundDedup {
71
+ checkAndRecord(deviceId: string, envelopeId: string): Promise<boolean>;
72
+ }
73
+ export interface TenantBoundReceipts {
74
+ record(input: {
75
+ readonly key: string;
76
+ readonly body: string;
77
+ }): Promise<{
78
+ readonly receipt: RequestReceipt;
79
+ readonly created: boolean;
80
+ }>;
81
+ get(key: string): Promise<RequestReceipt | undefined>;
82
+ }
83
+ export interface TenantBoundBlobs {
84
+ createUpload(reservation: StorageReservation): Promise<{
85
+ readonly blobId: string;
86
+ readonly uploadUrl: string;
87
+ }>;
88
+ observeUpload(blobId: string, reservation: StorageReservation): Promise<BlobObservation | undefined>;
89
+ getDownloadUrl(blobId: string): Promise<string | undefined>;
90
+ }
91
+ export interface TenantBoundQuota {
92
+ readReservation(reservationId: string): Promise<StorageReservation | undefined>;
93
+ reserve(input: StorageReservationInput): Promise<StorageReservation>;
94
+ finalizeReservation(input: StorageFinalizeInput): Promise<StorageFinalizeResult>;
95
+ abortReservation(reservationId: string): Promise<StorageReservation>;
96
+ }
97
+ export interface TenantBoundSequence {
98
+ next(deviceId: string): Promise<number>;
99
+ }
100
+ export interface TenantBoundRateLimiter {
101
+ consume(deviceId: string): Promise<boolean>;
102
+ }
103
+ export interface TenantStores {
104
+ readonly tenant: TenantId;
105
+ readonly principal: Principal;
106
+ readonly mailbox: TenantBoundMailbox;
107
+ readonly board: TenantBoundBoard;
108
+ readonly presence: TenantBoundPresence;
109
+ readonly activity: TenantBoundActivity;
110
+ readonly devices: TenantBoundDevices;
111
+ readonly tasks: TenantBoundTaskAttempts;
112
+ readonly dedup: TenantBoundDedup;
113
+ readonly receipts: TenantBoundReceipts;
114
+ readonly blobs: TenantBoundBlobs;
115
+ readonly quota: TenantBoundQuota;
116
+ readonly sequence: TenantBoundSequence;
117
+ readonly rateLimiter: TenantBoundRateLimiter;
118
+ }
119
+ /** Every naked store a composition supplies. Only {@link tenantStoresFor} reads this. */
120
+ export interface CloudRootStores {
121
+ readonly core: CoreStores;
122
+ readonly cloud: CloudStores;
123
+ }
124
+ export declare function tenantStoresFor(principal: Principal, root: CloudRootStores): TenantStores;
@@ -0,0 +1,149 @@
1
+ import type { ContentHash, TenantId, TruthBodyRef, TruthManifestEntry, TruthManifestQuery, TruthRecord, TruthRecordKind, TruthRecordSelector } from '@byok-sdk/core';
2
+ import { z } from 'zod';
3
+ export declare const TRUTH_RECORD_CAPABILITY = "truth.records";
4
+ export declare const TRUTH_INLINE_CONTENT_TYPE = "application/vnd.byok.truth+utf8";
5
+ export declare const TRUTH_REQUEST_ID_MAX_LENGTH = 120;
6
+ export declare const TRUTH_RECORD_KEY_MAX_LENGTH = 200;
7
+ export declare const TRUTH_LABEL_MAX_LENGTH = 200;
8
+ export declare const TRUTH_BATCH_MAX_RECORDS = 32;
9
+ export declare const TRUTH_MANIFEST_MAX_LIMIT = 100;
10
+ export declare const TruthRecordKeySchema: z.ZodString;
11
+ export declare const TruthBodyInputSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
12
+ kind: z.ZodLiteral<"inline">;
13
+ content: z.ZodString;
14
+ contentHash: z.ZodString;
15
+ }, z.core.$strict>, z.ZodObject<{
16
+ kind: z.ZodLiteral<"object">;
17
+ contentHash: z.ZodString;
18
+ byteSize: z.ZodNumber;
19
+ }, z.core.$strict>], "kind">;
20
+ export declare const TruthWriteRequestSchema: z.ZodObject<{
21
+ expectedRev: z.ZodOptional<z.ZodNumber>;
22
+ body: z.ZodDiscriminatedUnion<[z.ZodObject<{
23
+ kind: z.ZodLiteral<"inline">;
24
+ content: z.ZodString;
25
+ contentHash: z.ZodString;
26
+ }, z.core.$strict>, z.ZodObject<{
27
+ kind: z.ZodLiteral<"object">;
28
+ contentHash: z.ZodString;
29
+ byteSize: z.ZodNumber;
30
+ }, z.core.$strict>], "kind">;
31
+ label: z.ZodOptional<z.ZodString>;
32
+ snapshots: z.ZodOptional<z.ZodArray<z.ZodObject<{
33
+ kind: z.ZodEnum<{
34
+ memory: "memory";
35
+ profile: "profile";
36
+ }>;
37
+ recordKey: z.ZodString;
38
+ expectedRev: z.ZodNumber;
39
+ body: z.ZodDiscriminatedUnion<[z.ZodObject<{
40
+ kind: z.ZodLiteral<"inline">;
41
+ content: z.ZodString;
42
+ contentHash: z.ZodString;
43
+ }, z.core.$strict>, z.ZodObject<{
44
+ kind: z.ZodLiteral<"object">;
45
+ contentHash: z.ZodString;
46
+ byteSize: z.ZodNumber;
47
+ }, z.core.$strict>], "kind">;
48
+ label: z.ZodOptional<z.ZodString>;
49
+ }, z.core.$strict>>>;
50
+ }, z.core.$strict>;
51
+ export type TruthBodyInput = z.infer<typeof TruthBodyInputSchema>;
52
+ export type TruthWriteRequest = z.infer<typeof TruthWriteRequestSchema>;
53
+ export type PreparedTruthWrite = {
54
+ readonly kind: 'task.terminal';
55
+ readonly recordKey: string;
56
+ readonly contentHash: ContentHash;
57
+ readonly byteSize: bigint;
58
+ readonly body: TruthBodyRef;
59
+ readonly label?: string;
60
+ } | {
61
+ readonly kind: 'profile' | 'memory';
62
+ readonly recordKey: string;
63
+ readonly expectedRev: number;
64
+ readonly contentHash: ContentHash;
65
+ readonly byteSize: bigint;
66
+ readonly body: TruthBodyRef;
67
+ readonly label?: string;
68
+ };
69
+ export interface TruthCommitInput {
70
+ readonly deviceId: string;
71
+ readonly requestId: string;
72
+ readonly operation: string;
73
+ readonly resource: string;
74
+ readonly proofBodySha256: string;
75
+ readonly proofBodySize: bigint;
76
+ readonly writes: readonly [PreparedTruthWrite, ...PreparedTruthWrite[]];
77
+ }
78
+ export interface TruthRecordMetadata {
79
+ readonly kind: TruthRecordKind;
80
+ readonly recordKey: string;
81
+ readonly rev: number;
82
+ readonly contentHash: string;
83
+ readonly byteSize: number;
84
+ readonly label?: string;
85
+ readonly updatedAt: string;
86
+ }
87
+ export declare const TruthRecordMetadataSchema: z.ZodObject<{
88
+ kind: z.ZodEnum<{
89
+ memory: "memory";
90
+ profile: "profile";
91
+ "task.terminal": "task.terminal";
92
+ }>;
93
+ recordKey: z.ZodString;
94
+ rev: z.ZodNumber;
95
+ contentHash: z.ZodString;
96
+ byteSize: z.ZodNumber;
97
+ label: z.ZodOptional<z.ZodString>;
98
+ updatedAt: z.ZodString;
99
+ }, z.core.$strict>;
100
+ export interface TruthCommitResponse {
101
+ readonly primary: TruthRecordMetadata;
102
+ readonly snapshots: readonly TruthRecordMetadata[];
103
+ }
104
+ export declare const TruthCommitResponseSchema: z.ZodObject<{
105
+ primary: z.ZodObject<{
106
+ kind: z.ZodEnum<{
107
+ memory: "memory";
108
+ profile: "profile";
109
+ "task.terminal": "task.terminal";
110
+ }>;
111
+ recordKey: z.ZodString;
112
+ rev: z.ZodNumber;
113
+ contentHash: z.ZodString;
114
+ byteSize: z.ZodNumber;
115
+ label: z.ZodOptional<z.ZodString>;
116
+ updatedAt: z.ZodString;
117
+ }, z.core.$strict>;
118
+ snapshots: z.ZodArray<z.ZodObject<{
119
+ kind: z.ZodEnum<{
120
+ memory: "memory";
121
+ profile: "profile";
122
+ "task.terminal": "task.terminal";
123
+ }>;
124
+ recordKey: z.ZodString;
125
+ rev: z.ZodNumber;
126
+ contentHash: z.ZodString;
127
+ byteSize: z.ZodNumber;
128
+ label: z.ZodOptional<z.ZodString>;
129
+ updatedAt: z.ZodString;
130
+ }, z.core.$strict>>;
131
+ }, z.core.$strict>;
132
+ export interface TruthCommitResult {
133
+ readonly response: TruthCommitResponse;
134
+ readonly replayed: boolean;
135
+ }
136
+ export interface TruthCommitter {
137
+ commit(tenant: TenantId, input: TruthCommitInput): Promise<TruthCommitResult>;
138
+ getRecord(tenant: TenantId, selector: TruthRecordSelector): Promise<TruthRecord | undefined>;
139
+ listManifest(tenant: TenantId, query: TruthManifestQuery): Promise<readonly TruthManifestEntry[]>;
140
+ }
141
+ /**
142
+ * Object download grant authority for truth bodies. The input is the canonical
143
+ * content hash stored in `TruthBodyRef`, never an opaque upload id.
144
+ */
145
+ export interface TruthObjectDownloads {
146
+ getDownloadUrl(tenant: TenantId, hash: ContentHash): Promise<string | undefined>;
147
+ }
148
+ export declare function truthRecordMetadata(record: TruthRecord): TruthRecordMetadata;
149
+ export declare function truthManifestMetadata(entry: TruthManifestEntry): TruthRecordMetadata;
@@ -0,0 +1,8 @@
1
+ export declare const TRUTH_COMMIT_ERROR_CODES: readonly ['proof_request_conflict', 'truth_object_not_committed'];
2
+ export type TruthCommitErrorCode = (typeof TRUTH_COMMIT_ERROR_CODES)[number];
3
+ export declare class TruthCommitError extends Error {
4
+ readonly code: TruthCommitErrorCode;
5
+ readonly current?: unknown;
6
+ constructor(code: TruthCommitErrorCode, message: string, current?: unknown);
7
+ }
8
+ export declare function isTruthCommitError(value: unknown): value is TruthCommitError;
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@byok-sdk/cloud",
3
+ "version": "0.1.0",
4
+ "description": "BYOK SDK hosted device surface: stateless frozen-v1 HTTP handlers over tenant-first @byok-sdk/core ports",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Ancienttwo/byok-sdk.git",
10
+ "directory": "packages/cloud"
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
+ "hono": "^4.12.30",
40
+ "zod": "^4.4.3",
41
+ "@byok-sdk/core": "0.1.0",
42
+ "@byok-sdk/protocol": "0.1.0"
43
+ },
44
+ "scripts": {
45
+ "build": "tsup && tsc -p tsconfig.build.json",
46
+ "dev": "tsup --watch",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest",
49
+ "typecheck": "tsc --noEmit",
50
+ "clean": "rm -rf dist"
51
+ }
52
+ }