@byok-sdk/cloud 0.10.2 → 0.12.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.
@@ -77,6 +77,7 @@ export declare class InMemoryBlobContentProxy implements BlobContentProxy {
77
77
  #private;
78
78
  constructor(registry: InMemoryBlobRegistry);
79
79
  verifySignedUrl(blobId: string, action: 'put' | 'get', sig: string, exp: number): Promise<boolean>;
80
+ expectedUploadBytes(blobId: string): Promise<bigint | undefined>;
80
81
  writeContent(blobId: string, data: Uint8Array): Promise<BlobWriteResult>;
81
82
  /**
82
83
  * Never returns `{ok:false}`: this composition holds the bytes in the same
@@ -17,6 +17,5 @@ export declare class InMemoryNonceStore implements NonceStore {
17
17
  /** Number of records currently held (post-sweep). Test-facing only. */
18
18
  get size(): number;
19
19
  issue(tenant: TenantId, deviceId: string): Promise<string>;
20
- validate(tenant: TenantId, deviceId: string, nonce: string): Promise<boolean>;
21
- markUsed(tenant: TenantId, nonce: string): Promise<void>;
20
+ consumeIfValid(tenant: TenantId, deviceId: string, nonce: string): Promise<boolean>;
22
21
  }
@@ -2,17 +2,21 @@
2
2
  * In-memory {@link PairingCodeStore}: single-use codes bound to the tenant and
3
3
  * product they were minted for.
4
4
  *
5
- * `redeem` answers `undefined` for unknown, expired, and already-used alike.
5
+ * Enrollment answers `undefined` for unknown, expired, and already-used alike.
6
6
  * The reference server distinguishes those three in its 401 text; a hosted,
7
7
  * multi-tenant surface deliberately does not — the code is a bearer credential
8
8
  * addressable across every tenant, and "already used" versus "never existed"
9
9
  * is exactly the difference an attacker enumerating codes would pay for.
10
+ *
11
+ * Code issuance and enrollment project this one authority into separate ports.
12
+ * The enrollment path serializes each code through registration and flips
13
+ * `used` only after its shared device directory has accepted the row.
10
14
  */
11
15
  import type { Clock, TenantId } from '@byok-sdk/core';
12
- import type { PairingCodeClaims, PairingCodeInfo, PairingCodeIssueInput, PairingCodeStore } from '../ports';
13
- export declare class InMemoryPairingCodeStore implements PairingCodeStore {
16
+ import type { DeviceDirectory, DeviceRecord, PairingEnrollment, PairingEnrollmentInput, PairingCodeInfo, PairingCodeIssueInput, PairingCodeStore } from '../ports';
17
+ export declare class InMemoryPairingCodeStore implements PairingCodeStore, PairingEnrollment {
14
18
  #private;
15
- constructor(clock: Clock);
19
+ constructor(clock: Clock, devices: DeviceDirectory);
16
20
  issue(tenant: TenantId, input: PairingCodeIssueInput): Promise<PairingCodeInfo>;
17
- redeem(code: string): Promise<PairingCodeClaims | undefined>;
21
+ redeemAndRegister(input: PairingEnrollmentInput): Promise<DeviceRecord | undefined>;
18
22
  }
@@ -16,7 +16,7 @@
16
16
  * unfalsifiable.
17
17
  */
18
18
  import { type Clock, type TenantId } from '@byok-sdk/core';
19
- import type { AgentRef, TaskAttempt, TaskAttemptStatus, TaskAttemptStore } from '../ports';
19
+ import { type AgentMessageAdmission, type AgentRef, type TaskAttempt, type TaskAttemptStatus, type TaskAttemptStore } from '../ports';
20
20
  export declare class InMemoryTaskAttemptStore implements TaskAttemptStore {
21
21
  #private;
22
22
  constructor(clock: Clock, state?: InMemoryTaskAttemptState);
@@ -33,6 +33,25 @@ export declare class InMemoryTaskAttemptStore implements TaskAttemptStore {
33
33
  readonly attempt: TaskAttempt;
34
34
  readonly created: boolean;
35
35
  }>;
36
+ reserveAgentMessage(tenant: TenantId, input: {
37
+ readonly taskId: string;
38
+ readonly deviceId: string;
39
+ readonly messageId: string;
40
+ readonly payloadBody: string;
41
+ }): Promise<'reserved' | 'pending' | 'rejected'>;
42
+ readAgentMessage(tenant: TenantId, input: {
43
+ readonly taskId: string;
44
+ readonly deviceId: string;
45
+ readonly messageId: string;
46
+ readonly payloadBody: string;
47
+ }): Promise<AgentMessageAdmission | undefined>;
48
+ finalizeAgentMessage(tenant: TenantId, input: {
49
+ readonly taskId: string;
50
+ readonly deviceId: string;
51
+ readonly messageId: string;
52
+ readonly payloadBody: string;
53
+ readonly terminalBody: string;
54
+ }): Promise<AgentMessageAdmission | undefined>;
36
55
  get(tenant: TenantId, taskId: string): Promise<TaskAttempt | undefined>;
37
56
  getMany(tenant: TenantId, taskIds: readonly string[]): Promise<readonly TaskAttempt[]>;
38
57
  claim(tenant: TenantId, input: {
@@ -50,6 +69,7 @@ export declare class InMemoryTaskAttemptStore implements TaskAttemptStore {
50
69
  export declare class InMemoryTaskAttemptState {
51
70
  #private;
52
71
  readonly attempts: Map<string, TaskAttempt>;
72
+ readonly messageAdmissions: Map<string, AgentMessageAdmission>;
53
73
  constructor(clock: Clock);
54
74
  now(): string;
55
75
  /** Serialize every state-changing operation for one tenant/task key. */
@@ -13,20 +13,18 @@
13
13
  * 1. **Async.** Every method returns a `Promise`, so a SQL or KV composition
14
14
  * can implement the same contract the in-memory reference does.
15
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.
16
+ * `TenantId` — with one documented lookup exception below. Pairing code
17
+ * consumption is a separate composition-owned enrollment operation: its
18
+ * bearer code resolves the tenant internally and it returns only the
19
+ * resulting device row.
19
20
  *
20
- * The two exceptions:
21
+ * The lookup exception:
21
22
  *
22
23
  * - {@link DeviceDirectory.resolveByDeviceId} — `POST /byok/challenge` and
23
24
  * `POST /byok/token` carry only a deviceId (the pinned wire contract), and
24
25
  * 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.
26
+ * It is not reachable through {@link TenantStores} (see `tenant-stores.ts`),
27
+ * so a device-facing handler cannot call it.
30
28
  *
31
29
  * The byte-proxy trio that used to be a third pre-tenant exception is no
32
30
  * longer part of this bundle at all: it moved to {@link BlobContentProxy},
@@ -151,21 +149,38 @@ export interface PairingCodeIssueInput {
151
149
  }
152
150
  export interface PairingCodeStore {
153
151
  issue(tenant: TenantId, input: PairingCodeIssueInput): Promise<PairingCodeInfo>;
154
- /**
155
- * Validate and CONSUME a code, returning the claims it was minted with, or
156
- * `undefined` when it is unknown, expired, or already used — one answer for
157
- * all three, so a redeem attempt is never an existence oracle. Single-use is
158
- * what makes the caller's "redeem, then register the device row" sequence
159
- * exclusive: a second redeem can never reach the registration step.
160
- */
161
- redeem(code: string): Promise<PairingCodeClaims | undefined>;
152
+ }
153
+ /**
154
+ * The pre-tenant facts a pairing request carries into enrollment. Tenant and
155
+ * product are deliberately absent: the guarded pairing-code row is their sole
156
+ * authority, and no caller can choose the registration scope.
157
+ */
158
+ export interface PairingEnrollmentInput {
159
+ readonly pairingCode: string;
160
+ readonly deviceId: string;
161
+ readonly deviceName: string;
162
+ readonly devicePublicKey: string;
163
+ readonly proofKeyId: string;
164
+ readonly proofKeyEpoch: number;
165
+ readonly machineId?: string;
166
+ }
167
+ /**
168
+ * The only pairing-code consumption operation. Implementations atomically
169
+ * consume a valid code, apply machine supersession/state cleanup, and register
170
+ * the device; failures leave the code retryable. Unknown, expired, and already
171
+ * consumed codes all return `undefined`.
172
+ */
173
+ export interface PairingEnrollment {
174
+ redeemAndRegister(input: PairingEnrollmentInput): Promise<DeviceRecord | undefined>;
162
175
  }
163
176
  export interface NonceStore {
164
177
  issue(tenant: TenantId, deviceId: string): Promise<string>;
165
- /** `true` iff the nonce exists for this (tenant, device), is unexpired, and has not been consumed. Never mutates. */
166
- validate(tenant: TenantId, deviceId: string, nonce: string): Promise<boolean>;
167
- /** Consume the nonce. Called only after every other check on the request has passed. */
168
- markUsed(tenant: TenantId, nonce: string): Promise<void>;
178
+ /**
179
+ * Atomically consume a nonce only when it belongs to this exact (tenant,
180
+ * device), is unexpired, and has not already been consumed. Returns `true`
181
+ * for the sole winner; every other case returns `false`.
182
+ */
183
+ consumeIfValid(tenant: TenantId, deviceId: string, nonce: string): Promise<boolean>;
169
184
  }
170
185
  export interface InboundDedupStore {
171
186
  /**
@@ -178,6 +193,12 @@ export interface InboundDedupStore {
178
193
  }
179
194
  export declare const TASK_ATTEMPT_STATUSES: readonly ['offered', 'claimed', 'running', 'cancel_requested', 'complete', 'failed', 'cancelled'];
180
195
  export type TaskAttemptStatus = (typeof TASK_ATTEMPT_STATUSES)[number];
196
+ /** The durable first-message reservation and, once present, its terminal disposition. */
197
+ export interface AgentMessageAdmission {
198
+ readonly messageId: string;
199
+ readonly payloadBody: string;
200
+ readonly terminalBody?: string;
201
+ }
181
202
  export interface TaskAttempt {
182
203
  readonly tenantId: TenantId;
183
204
  readonly taskId: string;
@@ -216,6 +237,33 @@ export interface TaskAttemptStore {
216
237
  readonly attempt: TaskAttempt;
217
238
  readonly created: boolean;
218
239
  }>;
240
+ /**
241
+ * Atomically binds one message payload to a live task before an external
242
+ * consumer can run. `pending` is an existing exact reservation whose terminal
243
+ * receipt has not been recorded yet; callers must fail closed rather than
244
+ * invoke the consumer again.
245
+ */
246
+ reserveAgentMessage(tenant: TenantId, input: {
247
+ readonly taskId: string;
248
+ readonly deviceId: string;
249
+ readonly messageId: string;
250
+ readonly payloadBody: string;
251
+ }): Promise<'reserved' | 'pending' | 'rejected'>;
252
+ /** Read only an exact reservation; conflicting task/message bindings are not observable. */
253
+ readAgentMessage(tenant: TenantId, input: {
254
+ readonly taskId: string;
255
+ readonly deviceId: string;
256
+ readonly messageId: string;
257
+ readonly payloadBody: string;
258
+ }): Promise<AgentMessageAdmission | undefined>;
259
+ /** CAS the exact reservation to one immutable terminal disposition body. */
260
+ finalizeAgentMessage(tenant: TenantId, input: {
261
+ readonly taskId: string;
262
+ readonly deviceId: string;
263
+ readonly messageId: string;
264
+ readonly payloadBody: string;
265
+ readonly terminalBody: string;
266
+ }): Promise<AgentMessageAdmission | undefined>;
219
267
  get(tenant: TenantId, taskId: string): Promise<TaskAttempt | undefined>;
220
268
  /** Batch lookup used by mailbox projection; implementations must not turn one poll into N queries. */
221
269
  getMany(tenant: TenantId, taskIds: readonly string[]): Promise<readonly TaskAttempt[]>;
@@ -411,6 +459,8 @@ export interface CloudBlobStore {
411
459
  */
412
460
  export interface BlobContentProxy {
413
461
  verifySignedUrl(blobId: string, action: 'put' | 'get', sig: string, exp: number): Promise<boolean>;
462
+ /** Authoritative declared size for a signed PUT; no byte buffering precedes this lookup. */
463
+ expectedUploadBytes(blobId: string): Promise<bigint | undefined>;
414
464
  writeContent(blobId: string, data: Uint8Array): Promise<BlobWriteResult>;
415
465
  /** `undefined` = no such blob (404); a `{ok:false}` result = the blob exists but its bytes could not be proxied (502, distinguished by {@link BlobReadErrorCode}). */
416
466
  readContent(blobId: string): Promise<BlobReadResult | undefined>;
@@ -436,6 +486,7 @@ export interface CloudStores {
436
486
  readonly approvals: ApprovalTimelineStore;
437
487
  readonly devices: DeviceDirectory;
438
488
  readonly pairingCodes: PairingCodeStore;
489
+ readonly pairing: PairingEnrollment;
439
490
  readonly nonces: NonceStore;
440
491
  readonly dedup: InboundDedupStore;
441
492
  readonly tasks: TaskAttemptStore;
@@ -447,5 +498,5 @@ export interface CloudStores {
447
498
  readonly rateLimiter: InboundRateLimiter;
448
499
  }
449
500
  /** Names of every port in {@link CloudStores}, in contract order. */
450
- export declare const CLOUD_STORE_NAMES: readonly ['activity', 'approvals', 'devices', 'pairingCodes', 'nonces', 'dedup', 'tasks', 'cancellations', 'receipts', 'egress', 'proofReceipts', 'blobs', 'rateLimiter'];
501
+ export declare const CLOUD_STORE_NAMES: readonly ['activity', 'approvals', 'devices', 'pairingCodes', 'pairing', 'nonces', 'dedup', 'tasks', 'cancellations', 'receipts', 'egress', 'proofReceipts', 'blobs', 'rateLimiter'];
451
502
  export type CloudStoreName = (typeof CLOUD_STORE_NAMES)[number];
@@ -21,7 +21,7 @@
21
21
  * presigned blob calls) are deliberately absent from this surface: a
22
22
  * device-facing handler must not be able to reach them.
23
23
  */
24
- import { 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 TenantReadiness, type StorageFinalizeInput, type StorageFinalizeResult, type StorageReservation, type StorageReservationInput, type TenantId } from '@byok-sdk/core';
24
+ import { 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 MailboxRecordDeliveryInput, type Principal, type PresenceHint, type PresenceHintInput, type TenantReadiness, type StorageFinalizeInput, type StorageFinalizeResult, type StorageReservation, type StorageReservationInput, type TenantId } from '@byok-sdk/core';
25
25
  import type { ActivityAppendInput, ActivityTail } from './activity';
26
26
  import type { ApprovalTimelineAppendInput, ApprovalTimelineTail } from './approval-timeline';
27
27
  import type { BlobObservation, CloudStores, DeviceRecord, AgentEgressRecord, RequestReceipt, TaskCancellationMutation, TaskCancellationRequest, TaskAttempt, TaskAttemptStatus } from './stores/ports';
@@ -29,6 +29,7 @@ export interface TenantBoundMailbox {
29
29
  append(input: MailboxAppendInput): Promise<MailboxMessage>;
30
30
  /** Pure read. Never advances the cursor — the daemon's next poll is the only ack. */
31
31
  readAfter(query: MailboxReadQuery): Promise<MailboxPage>;
32
+ recordDelivery(input: MailboxRecordDeliveryInput): Promise<MailboxCursorState>;
32
33
  advanceCursor(input: MailboxAdvanceCursorInput): Promise<MailboxCursorState>;
33
34
  readCursor(deviceId: string): Promise<MailboxCursorState>;
34
35
  }
@@ -77,6 +78,25 @@ export interface TenantBoundTaskAttempts {
77
78
  readonly attempt: TaskAttempt;
78
79
  readonly created: boolean;
79
80
  }>;
81
+ reserveAgentMessage(input: {
82
+ readonly taskId: string;
83
+ readonly deviceId: string;
84
+ readonly messageId: string;
85
+ readonly payloadBody: string;
86
+ }): Promise<'reserved' | 'pending' | 'rejected'>;
87
+ readAgentMessage(input: {
88
+ readonly taskId: string;
89
+ readonly deviceId: string;
90
+ readonly messageId: string;
91
+ readonly payloadBody: string;
92
+ }): Promise<import('./stores/ports').AgentMessageAdmission | undefined>;
93
+ finalizeAgentMessage(input: {
94
+ readonly taskId: string;
95
+ readonly deviceId: string;
96
+ readonly messageId: string;
97
+ readonly payloadBody: string;
98
+ readonly terminalBody: string;
99
+ }): Promise<import('./stores/ports').AgentMessageAdmission | undefined>;
80
100
  get(taskId: string): Promise<TaskAttempt | undefined>;
81
101
  getMany(taskIds: readonly string[]): Promise<readonly TaskAttempt[]>;
82
102
  claim(input: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byok-sdk/cloud",
3
- "version": "0.10.2",
3
+ "version": "0.12.0",
4
4
  "description": "BYOK SDK hosted device surface: stateless frozen-v1 HTTP handlers over tenant-first @byok-sdk/core ports",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -44,8 +44,8 @@
44
44
  "clean": "rm -rf dist"
45
45
  },
46
46
  "dependencies": {
47
- "@byok-sdk/core": "0.10.2",
48
- "@byok-sdk/protocol": "0.10.2",
47
+ "@byok-sdk/core": "0.12.0",
48
+ "@byok-sdk/protocol": "0.12.0",
49
49
  "hono": "^4.12.30",
50
50
  "zod": "^4.4.3"
51
51
  }