@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/dist/index.js ADDED
@@ -0,0 +1,1498 @@
1
+ import { z } from 'zod';
2
+
3
+ // src/errors.ts
4
+ var CORE_ERROR_CODES = {
5
+ // identity + content addressing
6
+ tenant_id_invalid: "tenant_id_invalid",
7
+ content_hash_invalid: "content_hash_invalid",
8
+ // caller-supplied instants (see `time.ts`)
9
+ timestamp_not_canonical: "timestamp_not_canonical",
10
+ // capability declaration (ADR-010)
11
+ capability_declaration_invalid: "capability_declaration_invalid",
12
+ capability_unavailable: "capability_unavailable",
13
+ // device proof (§12.6.3)
14
+ proof_envelope_invalid: "proof_envelope_invalid",
15
+ proof_canonicalization_failed: "proof_canonicalization_failed",
16
+ // mailbox (§12.7.3)
17
+ mailbox_message_not_found: "mailbox_message_not_found",
18
+ mailbox_cursor_regression: "mailbox_cursor_regression",
19
+ // board coordination (§12.3)
20
+ board_item_not_found: "board_item_not_found",
21
+ board_item_exists: "board_item_exists",
22
+ board_transition_invalid: "board_transition_invalid",
23
+ board_status_conflict: "board_status_conflict",
24
+ board_claim_conflict: "board_claim_conflict",
25
+ board_not_held: "board_not_held",
26
+ // truth records (§12.3, §12.6.4)
27
+ truth_record_not_found: "truth_record_not_found",
28
+ terminal_conflict: "terminal_conflict",
29
+ truth_revision_conflict: "truth_revision_conflict",
30
+ // presence + activity hints (§12.3)
31
+ activity_capacity_invalid: "activity_capacity_invalid",
32
+ activity_batch_invalid: "activity_batch_invalid",
33
+ hint_ttl_invalid: "hint_ttl_invalid",
34
+ hint_rate_limited: "hint_rate_limited",
35
+ // object manifest (§12.7.4, §12.7.8)
36
+ object_not_found: "object_not_found",
37
+ object_state_invalid: "object_state_invalid",
38
+ // storage entitlement / usage / reservation (§12.7.6-12.7.7)
39
+ storage_entitlement_missing: "storage_entitlement_missing",
40
+ storage_entitlement_version_conflict: "storage_entitlement_version_conflict",
41
+ storage_reservation_not_found: "storage_reservation_not_found",
42
+ storage_object_too_large: "storage_object_too_large",
43
+ storage_quota_exceeded: "storage_quota_exceeded",
44
+ storage_reservation_expired: "storage_reservation_expired",
45
+ storage_integrity_mismatch: "storage_integrity_mismatch",
46
+ storage_write_suspended: "storage_write_suspended"
47
+ };
48
+ var ByokCoreError = class extends Error {
49
+ code;
50
+ constructor(code, message, options) {
51
+ super(message, options);
52
+ this.name = "ByokCoreError";
53
+ this.code = code;
54
+ }
55
+ };
56
+ var CoreConflictError = class extends ByokCoreError {
57
+ current;
58
+ observedAt;
59
+ constructor(code, message, current, observedAt, options) {
60
+ super(code, message, options);
61
+ this.name = "CoreConflictError";
62
+ this.current = current;
63
+ this.observedAt = observedAt;
64
+ }
65
+ };
66
+ function isCoreError(value, code) {
67
+ if (!(value instanceof ByokCoreError)) return false;
68
+ return code === void 0 || value.code === code;
69
+ }
70
+ function isCoreConflictError(value, code) {
71
+ if (!(value instanceof CoreConflictError)) return false;
72
+ return code === void 0 || value.code === code;
73
+ }
74
+
75
+ // src/tenant.ts
76
+ var TENANT_ID_MAX_LENGTH = 200;
77
+ var TENANT_KEY_SEPARATOR = "\0";
78
+ function tenantId(value) {
79
+ const candidate = value;
80
+ if (typeof candidate !== "string") {
81
+ throw new ByokCoreError(
82
+ "tenant_id_invalid",
83
+ `Tenant id must be a string, received ${typeof candidate}.`
84
+ );
85
+ }
86
+ if (candidate.length === 0) {
87
+ throw new ByokCoreError("tenant_id_invalid", "Tenant id must not be empty.");
88
+ }
89
+ if (candidate.trim() !== candidate) {
90
+ throw new ByokCoreError(
91
+ "tenant_id_invalid",
92
+ "Tenant id must not have leading or trailing whitespace."
93
+ );
94
+ }
95
+ if (candidate.length > TENANT_ID_MAX_LENGTH) {
96
+ throw new ByokCoreError(
97
+ "tenant_id_invalid",
98
+ `Tenant id must be at most ${TENANT_ID_MAX_LENGTH} characters.`
99
+ );
100
+ }
101
+ if (candidate.includes(TENANT_KEY_SEPARATOR)) {
102
+ throw new ByokCoreError("tenant_id_invalid", "Tenant id must not contain a NUL character.");
103
+ }
104
+ return candidate;
105
+ }
106
+ function isTenantId(value) {
107
+ if (typeof value !== "string") return false;
108
+ return value.length > 0 && value.trim() === value && value.length <= TENANT_ID_MAX_LENGTH && !value.includes(TENANT_KEY_SEPARATOR);
109
+ }
110
+ function tenantKey(tenant, ...parts) {
111
+ return [tenant, ...parts].join(TENANT_KEY_SEPARATOR);
112
+ }
113
+
114
+ // src/principals.ts
115
+ var PRINCIPAL_KINDS = ["device", "control-plane"];
116
+ function isDevicePrincipal(principal) {
117
+ return principal.kind === "device";
118
+ }
119
+ function isControlPlanePrincipal(principal) {
120
+ return principal.kind === "control-plane";
121
+ }
122
+ function principalTenant(principal) {
123
+ return principal.tenantId;
124
+ }
125
+
126
+ // src/time.ts
127
+ var CANONICAL_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
128
+ function isCanonicalTimestamp(value) {
129
+ if (typeof value !== "string") return false;
130
+ if (!CANONICAL_TIMESTAMP_PATTERN.test(value)) return false;
131
+ const parsed = Date.parse(value);
132
+ if (!Number.isFinite(parsed)) return false;
133
+ return new Date(parsed).toISOString() === value;
134
+ }
135
+ function assertCanonicalTimestamp(value, field) {
136
+ if (!isCanonicalTimestamp(value)) {
137
+ throw new ByokCoreError(
138
+ "timestamp_not_canonical",
139
+ `${field} must be a canonical ISO-8601 UTC instant (YYYY-MM-DDTHH:mm:ss.sssZ), received ${JSON.stringify(value)}.`
140
+ );
141
+ }
142
+ return value;
143
+ }
144
+
145
+ // src/mailbox.ts
146
+ var MAILBOX_MESSAGE_STATES = ["pending", "acked", "expired"];
147
+
148
+ // src/board.ts
149
+ var BOARD_STATUSES = ["todo", "in_progress", "in_review", "done", "closed"];
150
+ var BOARD_TRANSITIONS = {
151
+ todo: ["in_progress", "closed"],
152
+ in_progress: ["todo", "in_review", "closed"],
153
+ in_review: ["in_progress", "done", "closed"],
154
+ done: [],
155
+ closed: []
156
+ };
157
+ function isLegalBoardTransition(from, to) {
158
+ return BOARD_TRANSITIONS[from].includes(to);
159
+ }
160
+
161
+ // src/truth.ts
162
+ var TRUTH_RECORD_KINDS = ["task.terminal", "profile", "memory"];
163
+
164
+ // src/presence.ts
165
+ var PRESENCE_LEVELS = ["online", "thinking", "working", "error", "offline"];
166
+ var DEFAULT_ACTIVITY_CAPACITY = 50;
167
+
168
+ // src/blob.ts
169
+ var CONTENT_HASH_PATTERN = /^sha256:[0-9a-f]{64}$/;
170
+ function contentHash(value) {
171
+ if (!CONTENT_HASH_PATTERN.test(value)) {
172
+ throw new ByokCoreError(
173
+ "content_hash_invalid",
174
+ `Content hash must match ${CONTENT_HASH_PATTERN.source}, received ${JSON.stringify(value)}.`
175
+ );
176
+ }
177
+ return value;
178
+ }
179
+ function isContentHash(value) {
180
+ return typeof value === "string" && CONTENT_HASH_PATTERN.test(value);
181
+ }
182
+ function tenantObjectKey(tenant, hash) {
183
+ const hex = hash.slice("sha256:".length);
184
+ return `tenants/${tenant}/objects/sha256/${hex}`;
185
+ }
186
+ var OBJECT_STATES = ["pending", "committed", "delete_pending", "deleted"];
187
+ var OBJECT_STATE_TRANSITIONS = {
188
+ pending: ["committed", "delete_pending"],
189
+ committed: ["delete_pending"],
190
+ delete_pending: ["deleted", "committed"],
191
+ deleted: []
192
+ };
193
+ function isLegalObjectTransition(from, to) {
194
+ return OBJECT_STATE_TRANSITIONS[from].includes(to);
195
+ }
196
+
197
+ // src/quota.ts
198
+ var STORAGE_WRITE_KINDS = ["object", "inline"];
199
+ var STORAGE_RESERVATION_STATES = [
200
+ "reserved",
201
+ "committed",
202
+ "aborted",
203
+ "expired"
204
+ ];
205
+ var STORAGE_WRITE_POSTURES = ["normal", "warning", "blocked", "suspended"];
206
+ var STORAGE_ERROR_CODES = [
207
+ "storage_object_too_large",
208
+ "storage_quota_exceeded",
209
+ "storage_reservation_expired",
210
+ "storage_integrity_mismatch",
211
+ "storage_write_suspended"
212
+ ];
213
+ var STORAGE_ERROR_HTTP_STATUS = {
214
+ storage_object_too_large: 413,
215
+ storage_quota_exceeded: 507,
216
+ storage_reservation_expired: 409,
217
+ storage_integrity_mismatch: 422,
218
+ storage_write_suspended: 423
219
+ };
220
+ var CAPABILITY_DECLARATION_SCHEMA_ID = "byok-capabilities-v1";
221
+ var CAPABILITY_NAME_PATTERN = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/;
222
+ var CapabilityDeclarationSchema = z.object({
223
+ schema: z.literal(CAPABILITY_DECLARATION_SCHEMA_ID),
224
+ /** Monotonic per deployment. Lets a client cache and detect a rollout. */
225
+ version: z.number().int().nonnegative(),
226
+ capabilities: z.array(z.string().regex(CAPABILITY_NAME_PATTERN))
227
+ }).superRefine((declaration, ctx) => {
228
+ const seen = /* @__PURE__ */ new Set();
229
+ for (const [index, name] of declaration.capabilities.entries()) {
230
+ if (seen.has(name)) {
231
+ ctx.addIssue({
232
+ code: "custom",
233
+ path: ["capabilities", index],
234
+ message: `Duplicate capability ${JSON.stringify(name)}.`
235
+ });
236
+ }
237
+ seen.add(name);
238
+ }
239
+ });
240
+ function parseCapabilityDeclaration(input) {
241
+ const result = CapabilityDeclarationSchema.safeParse(input);
242
+ if (!result.success) {
243
+ throw new ByokCoreError(
244
+ "capability_declaration_invalid",
245
+ `Invalid capability declaration: ${result.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ")}`,
246
+ { cause: result.error }
247
+ );
248
+ }
249
+ return result.data;
250
+ }
251
+ function hasCapability(declaration, capability) {
252
+ return declaration.capabilities.includes(capability);
253
+ }
254
+ function assertCapability(declaration, capability) {
255
+ if (!hasCapability(declaration, capability)) {
256
+ throw new ByokCoreError(
257
+ "capability_unavailable",
258
+ `Capability ${JSON.stringify(capability)} is not declared by this deployment.`
259
+ );
260
+ }
261
+ }
262
+
263
+ // src/stores.ts
264
+ var CORE_STORE_NAMES = [
265
+ "mailbox",
266
+ "board",
267
+ "truth",
268
+ "presence",
269
+ "activity",
270
+ "objects",
271
+ "quota"
272
+ ];
273
+
274
+ // src/ports-contract.ts
275
+ var CORE_PORT_METHODS = {
276
+ mailbox: ["append", "readAfter", "advanceCursor", "readCursor", "collectRetired"],
277
+ board: ["create", "get", "list", "claim", "unclaim", "updateStatus"],
278
+ truth: ["writeTerminal", "writeSnapshot", "getRecord", "listManifest"],
279
+ presence: ["publish", "read", "list"],
280
+ activity: ["append", "read"],
281
+ objects: [
282
+ "putManifest",
283
+ "commit",
284
+ "get",
285
+ "list",
286
+ "addReference",
287
+ "removeReference",
288
+ "markDeletePending",
289
+ "markDeleted"
290
+ ],
291
+ quota: [
292
+ "readEntitlement",
293
+ "writeEntitlement",
294
+ "readUsage",
295
+ "readStatus",
296
+ "readReservation",
297
+ "reserve",
298
+ "finalizeReservation",
299
+ "abortReservation",
300
+ "expireReservations",
301
+ "applyMailboxDelta"
302
+ ]
303
+ };
304
+ var CORE_PORT_INTERFACES = {
305
+ mailbox: "MailboxStore",
306
+ board: "BoardStore",
307
+ truth: "TruthStore",
308
+ presence: "PresenceStore",
309
+ activity: "ActivityStore",
310
+ objects: "ObjectStore",
311
+ quota: "QuotaStore"
312
+ };
313
+ var DEVICE_PROOF_SCHEMA_ID = "byok-device-proof-v1";
314
+ var DEVICE_PROOF_DOMAIN_PREFIX = "byok-device-proof-v1\n";
315
+ var DEVICE_PROOF_VERSION = 1;
316
+ var DEVICE_PROOF_ALGORITHMS = ["ed25519"];
317
+ var BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
318
+ var HTTP_METHOD_PATTERN = /^[A-Z]+$/;
319
+ function canonicalizationFailure(path, reason) {
320
+ return new ByokCoreError(
321
+ "proof_canonicalization_failed",
322
+ `Cannot canonicalize ${path}: ${reason}`
323
+ );
324
+ }
325
+ function isPlainObject(value) {
326
+ const prototype = Object.getPrototypeOf(value);
327
+ return prototype === Object.prototype || prototype === null;
328
+ }
329
+ function canonicalizeValue(value, path, seen) {
330
+ if (value === null) return "null";
331
+ switch (typeof value) {
332
+ case "boolean":
333
+ return value ? "true" : "false";
334
+ case "string":
335
+ return JSON.stringify(value);
336
+ case "number": {
337
+ if (!Number.isSafeInteger(value)) {
338
+ throw canonicalizationFailure(
339
+ path,
340
+ `only safe integers are canonicalizable, received ${String(value)}`
341
+ );
342
+ }
343
+ return String(value === 0 ? 0 : value);
344
+ }
345
+ case "undefined":
346
+ throw canonicalizationFailure(path, "undefined has no canonical form");
347
+ case "bigint":
348
+ throw canonicalizationFailure(path, "bigint is not a JSON type");
349
+ case "function":
350
+ case "symbol":
351
+ throw canonicalizationFailure(path, `${typeof value} is not a JSON type`);
352
+ case "object":
353
+ break;
354
+ default:
355
+ throw canonicalizationFailure(path, `unsupported type ${typeof value}`);
356
+ }
357
+ const objectValue = value;
358
+ if (seen.has(objectValue)) {
359
+ throw canonicalizationFailure(path, "circular reference");
360
+ }
361
+ seen.add(objectValue);
362
+ try {
363
+ if (Array.isArray(objectValue)) {
364
+ const parts2 = objectValue.map(
365
+ (entry, index) => canonicalizeValue(entry, `${path}[${index}]`, seen)
366
+ );
367
+ return `[${parts2.join(",")}]`;
368
+ }
369
+ if (!isPlainObject(objectValue)) {
370
+ throw canonicalizationFailure(
371
+ path,
372
+ "only plain objects and arrays are canonicalizable"
373
+ );
374
+ }
375
+ const keys = Object.keys(objectValue).sort();
376
+ const record = objectValue;
377
+ const parts = keys.map((key) => {
378
+ const encodedKey = JSON.stringify(key);
379
+ const encodedValue = canonicalizeValue(record[key], `${path}.${key}`, seen);
380
+ return `${encodedKey}:${encodedValue}`;
381
+ });
382
+ return `{${parts.join(",")}}`;
383
+ } finally {
384
+ seen.delete(objectValue);
385
+ }
386
+ }
387
+ function canonicalizeJson(value) {
388
+ return canonicalizeValue(value, "<root>", /* @__PURE__ */ new Set());
389
+ }
390
+ function canonicalizeJsonBytes(value) {
391
+ return new TextEncoder().encode(canonicalizeJson(value));
392
+ }
393
+ var DeviceProofProtectedClaimsSchema = z.strictObject({
394
+ version: z.literal(DEVICE_PROOF_VERSION),
395
+ tenantId: z.string().min(1),
396
+ productId: z.string().min(1),
397
+ deviceId: z.string().min(1),
398
+ /** Identity of the signing key, resolved against the device row at verify time. */
399
+ keyId: z.string().min(1),
400
+ /** Rotation generation. An older epoch is rejected by the verifier, not here. */
401
+ keyEpoch: z.number().int().nonnegative(),
402
+ /** Idempotency key: exact replay returns the original result, reuse with a different body is a conflict. */
403
+ requestId: z.string().min(1),
404
+ /** Logical operation, e.g. `truth.write`. */
405
+ operation: z.string().min(1),
406
+ /** Tenant-scoped resource the operation targets. */
407
+ resource: z.string().min(1),
408
+ method: z.string().regex(HTTP_METHOD_PATTERN).optional(),
409
+ path: z.string().startsWith("/").optional(),
410
+ operationId: z.string().min(1).optional(),
411
+ bodySha256: z.string().regex(CONTENT_HASH_PATTERN),
412
+ /** Bound alongside the hash so a truncation attack cannot hide behind a hash collision claim. */
413
+ bodySize: z.number().int().nonnegative(),
414
+ issuedAt: z.iso.datetime(),
415
+ expiresAt: z.iso.datetime().optional(),
416
+ nonce: z.string().min(1).optional()
417
+ }).superRefine((claims, ctx) => {
418
+ const hasRequestLine = claims.method !== void 0 && claims.path !== void 0;
419
+ const hasOperationId = claims.operationId !== void 0;
420
+ if (hasRequestLine === hasOperationId) {
421
+ ctx.addIssue({
422
+ code: "custom",
423
+ message: "Claims must carry exactly one request binding: both `method` and `path`, or `operationId`."
424
+ });
425
+ }
426
+ if (!hasRequestLine && (claims.method !== void 0 || claims.path !== void 0)) {
427
+ ctx.addIssue({
428
+ code: "custom",
429
+ message: "`method` and `path` must be supplied together."
430
+ });
431
+ }
432
+ });
433
+ var DeviceProofEnvelopeV1Schema = z.strictObject({
434
+ schema: z.literal(DEVICE_PROOF_SCHEMA_ID),
435
+ algorithm: z.enum(DEVICE_PROOF_ALGORITHMS),
436
+ protected: DeviceProofProtectedClaimsSchema,
437
+ /** base64url, unpadded. */
438
+ signature: z.string().regex(BASE64URL_PATTERN)
439
+ });
440
+ function parseDeviceProofEnvelope(input) {
441
+ const result = DeviceProofEnvelopeV1Schema.safeParse(input);
442
+ if (!result.success) {
443
+ throw new ByokCoreError(
444
+ "proof_envelope_invalid",
445
+ `Invalid device proof envelope: ${result.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ")}`,
446
+ { cause: result.error }
447
+ );
448
+ }
449
+ return result.data;
450
+ }
451
+ function deviceProofCanonicalClaims(claims) {
452
+ const canonical = {
453
+ version: claims.version,
454
+ tenantId: claims.tenantId,
455
+ productId: claims.productId,
456
+ deviceId: claims.deviceId,
457
+ keyId: claims.keyId,
458
+ keyEpoch: claims.keyEpoch,
459
+ requestId: claims.requestId,
460
+ operation: claims.operation,
461
+ resource: claims.resource,
462
+ bodySha256: claims.bodySha256,
463
+ bodySize: claims.bodySize,
464
+ issuedAt: claims.issuedAt
465
+ };
466
+ if (claims.method !== void 0) canonical["method"] = claims.method;
467
+ if (claims.path !== void 0) canonical["path"] = claims.path;
468
+ if (claims.operationId !== void 0) canonical["operationId"] = claims.operationId;
469
+ if (claims.expiresAt !== void 0) canonical["expiresAt"] = claims.expiresAt;
470
+ if (claims.nonce !== void 0) canonical["nonce"] = claims.nonce;
471
+ return canonical;
472
+ }
473
+ function deviceProofCanonicalJson(claims) {
474
+ return canonicalizeJson(deviceProofCanonicalClaims(claims));
475
+ }
476
+ function deviceProofSigningInput(claims) {
477
+ return new TextEncoder().encode(
478
+ DEVICE_PROOF_DOMAIN_PREFIX + deviceProofCanonicalJson(claims)
479
+ );
480
+ }
481
+
482
+ // src/in-memory/presence.ts
483
+ function assertTtl(ttlMs) {
484
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
485
+ throw new ByokCoreError(
486
+ "hint_ttl_invalid",
487
+ `Hint ttl must be a positive number of milliseconds, received ${String(ttlMs)}.`
488
+ );
489
+ }
490
+ }
491
+ function assertMinimumInterval(minimumIntervalMs) {
492
+ if (!Number.isFinite(minimumIntervalMs) || minimumIntervalMs < 0) {
493
+ throw new ByokCoreError(
494
+ "hint_ttl_invalid",
495
+ `Hint minimum interval must be a non-negative number of milliseconds, received ${String(minimumIntervalMs)}.`
496
+ );
497
+ }
498
+ }
499
+ var InMemoryPresenceStore = class {
500
+ #hints = /* @__PURE__ */ new Map();
501
+ #clock;
502
+ constructor(clock) {
503
+ this.#clock = clock;
504
+ }
505
+ async publish(tenant, input) {
506
+ assertTtl(input.ttlMs);
507
+ assertMinimumInterval(input.minimumIntervalMs);
508
+ const now = this.#clock.now();
509
+ const key = tenantKey(tenant, input.deviceId);
510
+ const existing = this.#hints.get(key);
511
+ if (existing !== void 0 && now.toISOString() < existing.expiresAt && now.getTime() - Date.parse(existing.observedAt) < input.minimumIntervalMs) {
512
+ throw new ByokCoreError(
513
+ "hint_rate_limited",
514
+ `Presence for ${input.deviceId} was published more recently than the configured minimum interval.`
515
+ );
516
+ }
517
+ const hint = {
518
+ tenantId: tenant,
519
+ deviceId: input.deviceId,
520
+ level: input.level,
521
+ ...input.detail === void 0 ? {} : { detail: input.detail },
522
+ observedAt: now.toISOString(),
523
+ expiresAt: new Date(now.getTime() + input.ttlMs).toISOString()
524
+ };
525
+ this.#hints.set(key, hint);
526
+ return hint;
527
+ }
528
+ async read(tenant, deviceId) {
529
+ const key = tenantKey(tenant, deviceId);
530
+ const hint = this.#hints.get(key);
531
+ if (hint === void 0) return void 0;
532
+ if (this.#isExpired(hint.expiresAt)) {
533
+ this.#hints.delete(key);
534
+ return void 0;
535
+ }
536
+ return hint;
537
+ }
538
+ async list(tenant) {
539
+ const prefix = tenantKey(tenant, "");
540
+ const live = [];
541
+ for (const [key, hint] of [...this.#hints.entries()]) {
542
+ if (!key.startsWith(prefix)) continue;
543
+ if (this.#isExpired(hint.expiresAt)) {
544
+ this.#hints.delete(key);
545
+ continue;
546
+ }
547
+ live.push(hint);
548
+ }
549
+ live.sort((left, right) => left.deviceId.localeCompare(right.deviceId));
550
+ return live;
551
+ }
552
+ #isExpired(expiresAt) {
553
+ return this.#clock.now().toISOString() >= expiresAt;
554
+ }
555
+ };
556
+ var InMemoryActivityStore = class {
557
+ #tails = /* @__PURE__ */ new Map();
558
+ #clock;
559
+ constructor(clock) {
560
+ this.#clock = clock;
561
+ }
562
+ async append(tenant, input) {
563
+ assertTtl(input.ttlMs);
564
+ const capacity = input.capacity ?? DEFAULT_ACTIVITY_CAPACITY;
565
+ if (!Number.isSafeInteger(capacity) || capacity <= 0) {
566
+ throw new ByokCoreError(
567
+ "activity_capacity_invalid",
568
+ `Activity capacity must be a positive integer, received ${String(capacity)}.`
569
+ );
570
+ }
571
+ if (input.details.length === 0 || !Number.isSafeInteger(input.dropped) || input.dropped < 0) {
572
+ throw new ByokCoreError(
573
+ "activity_batch_invalid",
574
+ "Activity batches require at least one detail and a non-negative integer dropped count."
575
+ );
576
+ }
577
+ const now = this.#clock.now();
578
+ const key = tenantKey(tenant, input.taskId);
579
+ const existing = this.#tails.get(key);
580
+ const live = existing !== void 0 && now.toISOString() < existing.expiresAt ? existing : void 0;
581
+ const appended = input.details.map((detail) => ({
582
+ at: now.toISOString(),
583
+ detail
584
+ }));
585
+ const entries = [...live?.entries ?? [], ...appended];
586
+ let dropped = (live?.dropped ?? 0) + input.dropped;
587
+ while (entries.length > capacity) {
588
+ entries.shift();
589
+ dropped += 1;
590
+ }
591
+ const tail = {
592
+ tenantId: tenant,
593
+ taskId: input.taskId,
594
+ entries,
595
+ dropped,
596
+ capacity,
597
+ expiresAt: new Date(now.getTime() + input.ttlMs).toISOString()
598
+ };
599
+ this.#tails.set(key, tail);
600
+ return tail;
601
+ }
602
+ async read(tenant, taskId) {
603
+ const key = tenantKey(tenant, taskId);
604
+ const tail = this.#tails.get(key);
605
+ if (tail === void 0) return void 0;
606
+ if (this.#clock.now().toISOString() >= tail.expiresAt) {
607
+ this.#tails.delete(key);
608
+ return void 0;
609
+ }
610
+ return tail;
611
+ }
612
+ };
613
+
614
+ // src/in-memory/board.ts
615
+ var DEFAULT_LIST_LIMIT = 50;
616
+ var InMemoryBoardStore = class {
617
+ #items = /* @__PURE__ */ new Map();
618
+ #seqByTenant = /* @__PURE__ */ new Map();
619
+ #clock;
620
+ constructor(clock) {
621
+ this.#clock = clock;
622
+ }
623
+ async create(tenant, input) {
624
+ const key = tenantKey(tenant, input.itemId);
625
+ if (this.#items.has(key)) {
626
+ throw new ByokCoreError(
627
+ "board_item_exists",
628
+ `Board item ${input.itemId} already exists in this tenant.`
629
+ );
630
+ }
631
+ const now = this.#now();
632
+ const item = {
633
+ tenantId: tenant,
634
+ itemId: input.itemId,
635
+ channel: input.channel,
636
+ title: input.title,
637
+ status: input.status ?? "todo",
638
+ boardSeq: this.#nextSeq(tenant),
639
+ createdAt: now,
640
+ updatedAt: now
641
+ };
642
+ this.#items.set(key, item);
643
+ return item;
644
+ }
645
+ async get(tenant, itemId) {
646
+ return this.#items.get(tenantKey(tenant, itemId));
647
+ }
648
+ async list(tenant, query) {
649
+ const afterSeq = query.afterSeq ?? 0;
650
+ const limit = query.limit ?? DEFAULT_LIST_LIMIT;
651
+ const prefix = tenantKey(tenant, "");
652
+ const matches = [];
653
+ for (const [key, item] of this.#items.entries()) {
654
+ if (!key.startsWith(prefix)) continue;
655
+ if (item.boardSeq <= afterSeq) continue;
656
+ if (query.channel !== void 0 && item.channel !== query.channel) continue;
657
+ if (query.status !== void 0 && item.status !== query.status) continue;
658
+ matches.push(item);
659
+ }
660
+ matches.sort((left, right) => left.boardSeq - right.boardSeq);
661
+ const page = matches.slice(0, limit);
662
+ return {
663
+ items: page,
664
+ nextSeq: page.at(-1)?.boardSeq ?? afterSeq,
665
+ hasMore: matches.length > page.length
666
+ };
667
+ }
668
+ async claim(tenant, input) {
669
+ const item = this.#require(tenant, input.itemId);
670
+ const expectedStatus = input.expectedStatus ?? "todo";
671
+ if (item.assignee !== void 0) {
672
+ if (item.assignee.holderId === input.holderId) {
673
+ if (input.expectedStatus !== void 0 && item.status !== input.expectedStatus) {
674
+ throw new CoreConflictError(
675
+ "board_status_conflict",
676
+ `Board item ${input.itemId} is ${item.status}, not ${input.expectedStatus}.`,
677
+ item,
678
+ this.#now()
679
+ );
680
+ }
681
+ return item;
682
+ }
683
+ throw new CoreConflictError(
684
+ "board_claim_conflict",
685
+ `Board item ${input.itemId} is held by ${item.assignee.holderId}.`,
686
+ item,
687
+ this.#now()
688
+ );
689
+ }
690
+ if (item.status !== expectedStatus) {
691
+ throw new CoreConflictError(
692
+ "board_status_conflict",
693
+ `Board item ${input.itemId} is ${item.status}, not ${expectedStatus}.`,
694
+ item,
695
+ this.#now()
696
+ );
697
+ }
698
+ if (item.status !== "todo" && item.status !== "in_progress") {
699
+ throw new CoreConflictError(
700
+ "board_transition_invalid",
701
+ `Board item ${input.itemId} cannot be claimed from ${item.status}.`,
702
+ item,
703
+ this.#now()
704
+ );
705
+ }
706
+ const now = this.#now();
707
+ const claimed = {
708
+ ...item,
709
+ status: item.status === "todo" ? "in_progress" : item.status,
710
+ assignee: { holderId: input.holderId, heldSince: now },
711
+ boardSeq: this.#nextSeq(tenant),
712
+ updatedAt: now
713
+ };
714
+ this.#items.set(tenantKey(tenant, input.itemId), claimed);
715
+ return claimed;
716
+ }
717
+ async unclaim(tenant, input) {
718
+ const item = this.#require(tenant, input.itemId);
719
+ if (item.assignee === void 0) {
720
+ throw new ByokCoreError(
721
+ "board_not_held",
722
+ `Board item ${input.itemId} is not held by anyone.`
723
+ );
724
+ }
725
+ if (item.assignee.holderId !== input.holderId) {
726
+ throw new CoreConflictError(
727
+ "board_claim_conflict",
728
+ `Board item ${input.itemId} is held by ${item.assignee.holderId}, not ${input.holderId}.`,
729
+ item,
730
+ this.#now()
731
+ );
732
+ }
733
+ const now = this.#now();
734
+ const released = {
735
+ tenantId: item.tenantId,
736
+ itemId: item.itemId,
737
+ channel: item.channel,
738
+ title: item.title,
739
+ status: item.status === "in_progress" ? "todo" : item.status,
740
+ boardSeq: this.#nextSeq(tenant),
741
+ createdAt: item.createdAt,
742
+ updatedAt: now
743
+ };
744
+ this.#items.set(tenantKey(tenant, input.itemId), released);
745
+ return released;
746
+ }
747
+ async updateStatus(tenant, input) {
748
+ const item = this.#require(tenant, input.itemId);
749
+ if (item.status !== input.expectedStatus) {
750
+ throw new CoreConflictError(
751
+ "board_status_conflict",
752
+ `Board item ${input.itemId} is ${item.status}, not ${input.expectedStatus}.`,
753
+ item,
754
+ this.#now()
755
+ );
756
+ }
757
+ if (input.holderId !== void 0 && item.assignee?.holderId !== input.holderId) {
758
+ throw new CoreConflictError(
759
+ "board_claim_conflict",
760
+ `Board item ${input.itemId} is not held by ${input.holderId}.`,
761
+ item,
762
+ this.#now()
763
+ );
764
+ }
765
+ if (!isLegalBoardTransition(input.expectedStatus, input.status)) {
766
+ throw new CoreConflictError(
767
+ "board_transition_invalid",
768
+ `${input.expectedStatus} to ${input.status} is not a legal board transition.`,
769
+ item,
770
+ this.#now()
771
+ );
772
+ }
773
+ const now = this.#now();
774
+ const updated = {
775
+ ...item,
776
+ status: input.status,
777
+ boardSeq: this.#nextSeq(tenant),
778
+ updatedAt: now
779
+ };
780
+ this.#items.set(tenantKey(tenant, input.itemId), updated);
781
+ return updated;
782
+ }
783
+ #require(tenant, itemId) {
784
+ const item = this.#items.get(tenantKey(tenant, itemId));
785
+ if (item === void 0) {
786
+ throw new ByokCoreError(
787
+ "board_item_not_found",
788
+ `Board item ${itemId} does not exist in this tenant.`
789
+ );
790
+ }
791
+ return item;
792
+ }
793
+ #nextSeq(tenant) {
794
+ const next = (this.#seqByTenant.get(tenant) ?? 0) + 1;
795
+ this.#seqByTenant.set(tenant, next);
796
+ return next;
797
+ }
798
+ #now() {
799
+ return this.#clock.now().toISOString();
800
+ }
801
+ };
802
+
803
+ // src/in-memory/mailbox.ts
804
+ var DEFAULT_READ_LIMIT = 50;
805
+ var InMemoryMailboxStore = class {
806
+ #devices = /* @__PURE__ */ new Map();
807
+ #clock;
808
+ constructor(clock) {
809
+ this.#clock = clock;
810
+ }
811
+ async append(tenant, input) {
812
+ const device = this.#device(tenant, input.deviceId);
813
+ const existing = device.byMessageId.get(input.messageId);
814
+ if (existing !== void 0) return existing;
815
+ const message = {
816
+ tenantId: tenant,
817
+ deviceId: input.deviceId,
818
+ seq: device.nextSeq,
819
+ messageId: input.messageId,
820
+ body: input.body,
821
+ bodyHash: input.bodyHash,
822
+ byteSize: input.byteSize,
823
+ state: "pending",
824
+ appendedAt: this.#now()
825
+ };
826
+ device.nextSeq += 1;
827
+ device.messages.push(message);
828
+ device.byMessageId.set(message.messageId, message);
829
+ return message;
830
+ }
831
+ async readAfter(tenant, query) {
832
+ const device = this.#devices.get(tenantKey(tenant, query.deviceId));
833
+ const limit = query.limit ?? DEFAULT_READ_LIMIT;
834
+ if (device === void 0) {
835
+ return { messages: [], nextSeq: query.afterSeq, hasMore: false };
836
+ }
837
+ const pending = device.messages.filter((message) => message.state === "pending" && message.seq > query.afterSeq).sort((left, right) => left.seq - right.seq);
838
+ const page = pending.slice(0, limit);
839
+ const last = page.at(-1);
840
+ return {
841
+ messages: page,
842
+ // Reading is not acknowledging: the returned position is a *read* cursor.
843
+ // Nothing above was mutated, so an identical call replays the same page.
844
+ nextSeq: last?.seq ?? query.afterSeq,
845
+ hasMore: pending.length > page.length
846
+ };
847
+ }
848
+ async advanceCursor(tenant, input) {
849
+ const device = this.#device(tenant, input.deviceId);
850
+ if (input.ackedSeq < device.ackedSeq) {
851
+ throw new CoreConflictError(
852
+ "mailbox_cursor_regression",
853
+ `Cursor for device ${input.deviceId} is at ${device.ackedSeq}; refusing to move it back to ${input.ackedSeq}.`,
854
+ this.#cursorState(tenant, input.deviceId, device),
855
+ this.#now()
856
+ );
857
+ }
858
+ device.ackedSeq = input.ackedSeq;
859
+ device.cursorUpdatedAt = this.#now();
860
+ for (const [index, message] of device.messages.entries()) {
861
+ if (message.state === "pending" && message.seq <= input.ackedSeq) {
862
+ device.messages[index] = { ...message, state: "acked" };
863
+ device.byMessageId.set(message.messageId, device.messages[index]);
864
+ }
865
+ }
866
+ return this.#cursorState(tenant, input.deviceId, device);
867
+ }
868
+ async readCursor(tenant, deviceId) {
869
+ const device = this.#devices.get(tenantKey(tenant, deviceId));
870
+ if (device === void 0) {
871
+ return { tenantId: tenant, deviceId, ackedSeq: 0, updatedAt: this.#now() };
872
+ }
873
+ return this.#cursorState(tenant, deviceId, device);
874
+ }
875
+ async collectRetired(tenant, input) {
876
+ assertCanonicalTimestamp(input.ackedBefore, "ackedBefore");
877
+ assertCanonicalTimestamp(input.expireUnackedBefore, "expireUnackedBefore");
878
+ let deletedCount = 0;
879
+ let expiredCount = 0;
880
+ let releasedBytes = 0n;
881
+ for (const [key, device] of this.#devices.entries()) {
882
+ if (!key.startsWith(tenantKey(tenant, ""))) continue;
883
+ if (input.deviceId !== void 0 && key !== tenantKey(tenant, input.deviceId)) continue;
884
+ for (let index = device.messages.length - 1; index >= 0; index -= 1) {
885
+ const message = device.messages[index];
886
+ if (message.state === "acked" && message.appendedAt < input.ackedBefore) {
887
+ device.messages.splice(index, 1);
888
+ device.byMessageId.delete(message.messageId);
889
+ deletedCount += 1;
890
+ releasedBytes += message.byteSize;
891
+ continue;
892
+ }
893
+ if (message.state === "pending" && message.appendedAt < input.expireUnackedBefore) {
894
+ const expired = { ...message, state: "expired" };
895
+ device.messages[index] = expired;
896
+ device.byMessageId.set(expired.messageId, expired);
897
+ expiredCount += 1;
898
+ }
899
+ }
900
+ }
901
+ return { deletedCount, expiredCount, releasedBytes };
902
+ }
903
+ #device(tenant, deviceId) {
904
+ if (deviceId.length === 0) {
905
+ throw new ByokCoreError("mailbox_message_not_found", "Device id must not be empty.");
906
+ }
907
+ const key = tenantKey(tenant, deviceId);
908
+ const existing = this.#devices.get(key);
909
+ if (existing !== void 0) return existing;
910
+ const created = {
911
+ nextSeq: 1,
912
+ ackedSeq: 0,
913
+ cursorUpdatedAt: this.#now(),
914
+ messages: [],
915
+ byMessageId: /* @__PURE__ */ new Map()
916
+ };
917
+ this.#devices.set(key, created);
918
+ return created;
919
+ }
920
+ #cursorState(tenant, deviceId, device) {
921
+ return {
922
+ tenantId: tenant,
923
+ deviceId,
924
+ ackedSeq: device.ackedSeq,
925
+ updatedAt: device.cursorUpdatedAt
926
+ };
927
+ }
928
+ #now() {
929
+ return this.#clock.now().toISOString();
930
+ }
931
+ };
932
+
933
+ // src/in-memory/blob.ts
934
+ var DEFAULT_LIST_LIMIT2 = 100;
935
+ var InMemoryObjectStore = class {
936
+ #manifest = /* @__PURE__ */ new Map();
937
+ #references = /* @__PURE__ */ new Map();
938
+ #clock;
939
+ constructor(clock) {
940
+ this.#clock = clock;
941
+ }
942
+ async putManifest(tenant, input) {
943
+ const key = tenantKey(tenant, input.hash);
944
+ const existing = this.#manifest.get(key);
945
+ if (existing !== void 0 && existing.state !== "deleted") return existing;
946
+ const now = this.#now();
947
+ const entry = {
948
+ tenantId: tenant,
949
+ hash: input.hash,
950
+ byteSize: input.byteSize,
951
+ contentType: input.contentType,
952
+ state: "pending",
953
+ refCount: 0,
954
+ createdAt: now,
955
+ updatedAt: now
956
+ };
957
+ this.#manifest.set(key, entry);
958
+ return entry;
959
+ }
960
+ async commit(tenant, input) {
961
+ const entry = this.#require(tenant, input.hash);
962
+ if (entry.byteSize !== input.observedByteSize || entry.contentType !== input.observedContentType) {
963
+ throw new ByokCoreError(
964
+ "storage_integrity_mismatch",
965
+ `Observed object ${input.hash} (${String(input.observedByteSize)} bytes, ${input.observedContentType}) does not match the declared manifest.`
966
+ );
967
+ }
968
+ if (entry.state === "committed") return entry;
969
+ return this.#transition(tenant, entry, "committed");
970
+ }
971
+ async get(tenant, hash) {
972
+ return this.#manifest.get(tenantKey(tenant, hash));
973
+ }
974
+ async list(tenant, query) {
975
+ if (query.deletePendingBefore !== void 0) {
976
+ assertCanonicalTimestamp(query.deletePendingBefore, "deletePendingBefore");
977
+ }
978
+ const prefix = tenantKey(tenant, "");
979
+ const matches = [];
980
+ for (const [key, entry] of this.#manifest.entries()) {
981
+ if (!key.startsWith(prefix)) continue;
982
+ if (query.state !== void 0 && entry.state !== query.state) continue;
983
+ if (query.deletePendingBefore !== void 0) {
984
+ if (entry.deletePendingAt === void 0) continue;
985
+ if (entry.deletePendingAt >= query.deletePendingBefore) continue;
986
+ }
987
+ matches.push(entry);
988
+ }
989
+ matches.sort((left, right) => left.hash.localeCompare(right.hash));
990
+ return matches.slice(0, query.limit ?? DEFAULT_LIST_LIMIT2);
991
+ }
992
+ async addReference(tenant, input) {
993
+ const entry = this.#require(tenant, input.hash);
994
+ if (entry.state !== "committed") {
995
+ throw new ByokCoreError(
996
+ "object_state_invalid",
997
+ `Only committed objects can be referenced; ${input.hash} is ${entry.state}.`
998
+ );
999
+ }
1000
+ const refKey = tenantKey(tenant, input.hash, input.refKind, input.refId);
1001
+ if (!this.#references.has(refKey)) {
1002
+ this.#references.set(refKey, {
1003
+ tenantId: tenant,
1004
+ hash: input.hash,
1005
+ refKind: input.refKind,
1006
+ refId: input.refId,
1007
+ createdAt: this.#now()
1008
+ });
1009
+ }
1010
+ return this.#recount(tenant, entry);
1011
+ }
1012
+ async removeReference(tenant, input) {
1013
+ const entry = this.#require(tenant, input.hash);
1014
+ this.#references.delete(tenantKey(tenant, input.hash, input.refKind, input.refId));
1015
+ return this.#recount(tenant, entry);
1016
+ }
1017
+ async markDeletePending(tenant, hash) {
1018
+ const entry = this.#require(tenant, hash);
1019
+ if (entry.refCount !== 0) {
1020
+ throw new ByokCoreError(
1021
+ "object_state_invalid",
1022
+ `Object ${hash} still has ${entry.refCount} reference(s).`
1023
+ );
1024
+ }
1025
+ return this.#transition(tenant, entry, "delete_pending");
1026
+ }
1027
+ async markDeleted(tenant, hash) {
1028
+ const entry = this.#require(tenant, hash);
1029
+ return this.#transition(tenant, entry, "deleted");
1030
+ }
1031
+ #transition(tenant, entry, next) {
1032
+ if (!isLegalObjectTransition(entry.state, next)) {
1033
+ throw new ByokCoreError(
1034
+ "object_state_invalid",
1035
+ `${entry.state} to ${next} is not a legal object manifest transition.`
1036
+ );
1037
+ }
1038
+ const now = this.#now();
1039
+ const updated = {
1040
+ ...entry,
1041
+ state: next,
1042
+ updatedAt: now,
1043
+ ...next === "delete_pending" ? { deletePendingAt: now } : {}
1044
+ };
1045
+ this.#manifest.set(tenantKey(tenant, entry.hash), updated);
1046
+ return updated;
1047
+ }
1048
+ #recount(tenant, entry) {
1049
+ const refPrefix = tenantKey(tenant, entry.hash, "");
1050
+ let refCount = 0;
1051
+ for (const key of this.#references.keys()) {
1052
+ if (key.startsWith(refPrefix)) refCount += 1;
1053
+ }
1054
+ const updated = { ...entry, refCount, updatedAt: this.#now() };
1055
+ this.#manifest.set(tenantKey(tenant, entry.hash), updated);
1056
+ return updated;
1057
+ }
1058
+ #require(tenant, hash) {
1059
+ const entry = this.#manifest.get(tenantKey(tenant, hash));
1060
+ if (entry === void 0) {
1061
+ throw new ByokCoreError(
1062
+ "object_not_found",
1063
+ `Object ${hash} has no manifest row in this tenant.`
1064
+ );
1065
+ }
1066
+ return entry;
1067
+ }
1068
+ #now() {
1069
+ return this.#clock.now().toISOString();
1070
+ }
1071
+ };
1072
+
1073
+ // src/in-memory/quota.ts
1074
+ var WARNING_NUMERATOR = 80n;
1075
+ var WARNING_DENOMINATOR = 100n;
1076
+ var InMemoryQuotaStore = class {
1077
+ #entitlements = /* @__PURE__ */ new Map();
1078
+ #usage = /* @__PURE__ */ new Map();
1079
+ #reservations = /* @__PURE__ */ new Map();
1080
+ #committedHashes = /* @__PURE__ */ new Map();
1081
+ #clock;
1082
+ #objects;
1083
+ constructor(clock, objects) {
1084
+ this.#clock = clock;
1085
+ this.#objects = objects;
1086
+ }
1087
+ async readEntitlement(tenant) {
1088
+ return this.#entitlements.get(tenant);
1089
+ }
1090
+ async writeEntitlement(tenant, input) {
1091
+ if (input.downgradeGraceUntil !== void 0) {
1092
+ assertCanonicalTimestamp(input.downgradeGraceUntil, "downgradeGraceUntil");
1093
+ }
1094
+ const existing = this.#entitlements.get(tenant);
1095
+ if (existing !== void 0 && input.version <= existing.version) {
1096
+ throw new CoreConflictError(
1097
+ "storage_entitlement_version_conflict",
1098
+ `Entitlement is at version ${String(existing.version)}; refusing to apply version ${String(input.version)}.`,
1099
+ existing,
1100
+ this.#now()
1101
+ );
1102
+ }
1103
+ const entitlement = {
1104
+ tenantId: tenant,
1105
+ version: input.version,
1106
+ hardLimitBytes: input.hardLimitBytes,
1107
+ maxObjectBytes: input.maxObjectBytes,
1108
+ maxInlineBytes: input.maxInlineBytes,
1109
+ mailboxLimitBytes: input.mailboxLimitBytes,
1110
+ retentionPolicyId: input.retentionPolicyId,
1111
+ ...input.downgradeGraceUntil === void 0 ? {} : { downgradeGraceUntil: input.downgradeGraceUntil }
1112
+ };
1113
+ this.#entitlements.set(tenant, entitlement);
1114
+ return entitlement;
1115
+ }
1116
+ async readUsage(tenant) {
1117
+ return this.#usageOf(tenant);
1118
+ }
1119
+ async readStatus(tenant) {
1120
+ const entitlement = this.#requireEntitlement(tenant);
1121
+ const usage = this.#usageOf(tenant);
1122
+ const used = this.#usedBytes(usage);
1123
+ const graceActive = entitlement.downgradeGraceUntil !== void 0 && this.#now() < entitlement.downgradeGraceUntil;
1124
+ return {
1125
+ entitlement,
1126
+ usage,
1127
+ posture: this.#posture(entitlement, usage, graceActive),
1128
+ availableBytes: used >= entitlement.hardLimitBytes ? 0n : entitlement.hardLimitBytes - used,
1129
+ graceActive
1130
+ };
1131
+ }
1132
+ async readReservation(tenant, reservationId) {
1133
+ return this.#reservations.get(tenantKey(tenant, reservationId))?.reservation;
1134
+ }
1135
+ async reserve(tenant, input) {
1136
+ const entitlement = this.#requireEntitlement(tenant);
1137
+ await this.expireReservations(tenant);
1138
+ const key = tenantKey(tenant, input.reservationId);
1139
+ const existing = this.#reservations.get(key);
1140
+ if (existing !== void 0) {
1141
+ if (existing.reservation.state === "reserved") {
1142
+ if (existing.reservation.kind !== input.kind || existing.reservation.expectedBytes !== input.expectedBytes || existing.reservation.contentHash !== input.contentHash || existing.reservation.contentType !== input.contentType) {
1143
+ throw new ByokCoreError(
1144
+ "storage_integrity_mismatch",
1145
+ `Reservation ${input.reservationId} already binds a different storage declaration.`
1146
+ );
1147
+ }
1148
+ return existing.reservation;
1149
+ }
1150
+ throw new ByokCoreError(
1151
+ "storage_reservation_expired",
1152
+ `Reservation ${input.reservationId} is already ${existing.reservation.state}.`
1153
+ );
1154
+ }
1155
+ const usage = this.#usageOf(tenant);
1156
+ const graceActive = entitlement.downgradeGraceUntil !== void 0 && this.#now() < entitlement.downgradeGraceUntil;
1157
+ if (this.#posture(entitlement, usage, graceActive) === "suspended") {
1158
+ throw new ByokCoreError(
1159
+ "storage_write_suspended",
1160
+ "Tenant is over its hard limit and its downgrade grace has ended; durable writes are suspended."
1161
+ );
1162
+ }
1163
+ const perObjectLimit = input.kind === "object" ? entitlement.maxObjectBytes : entitlement.maxInlineBytes;
1164
+ if (input.expectedBytes > perObjectLimit) {
1165
+ throw new ByokCoreError(
1166
+ "storage_object_too_large",
1167
+ `${String(input.expectedBytes)} bytes exceeds the ${input.kind} limit of ${String(perObjectLimit)} bytes.`
1168
+ );
1169
+ }
1170
+ if (this.#usedBytes(usage) + input.expectedBytes > entitlement.hardLimitBytes) {
1171
+ throw new ByokCoreError(
1172
+ "storage_quota_exceeded",
1173
+ `Reserving ${String(input.expectedBytes)} bytes would exceed the hard limit of ${String(entitlement.hardLimitBytes)} bytes.`
1174
+ );
1175
+ }
1176
+ const now = this.#now();
1177
+ const reservation = {
1178
+ tenantId: tenant,
1179
+ reservationId: input.reservationId,
1180
+ state: "reserved",
1181
+ kind: input.kind,
1182
+ expectedBytes: input.expectedBytes,
1183
+ contentHash: input.contentHash,
1184
+ contentType: input.contentType,
1185
+ createdAt: now,
1186
+ expiresAt: new Date(this.#clock.now().getTime() + input.ttlMs).toISOString()
1187
+ };
1188
+ this.#reservations.set(key, { reservation, deduplicated: false });
1189
+ this.#setUsage(tenant, { ...usage, reservedBytes: usage.reservedBytes + input.expectedBytes });
1190
+ return reservation;
1191
+ }
1192
+ async finalizeReservation(tenant, input) {
1193
+ const key = tenantKey(tenant, input.reservationId);
1194
+ const record = this.#requireReservation(tenant, input.reservationId);
1195
+ const reservation = record.reservation;
1196
+ if (reservation.state === "committed") {
1197
+ return {
1198
+ reservation,
1199
+ usage: this.#usageOf(tenant),
1200
+ deduplicated: record.deduplicated
1201
+ };
1202
+ }
1203
+ if (reservation.state !== "reserved") {
1204
+ throw new ByokCoreError(
1205
+ "storage_reservation_expired",
1206
+ `Reservation ${input.reservationId} is ${reservation.state}.`
1207
+ );
1208
+ }
1209
+ if (this.#now() >= reservation.expiresAt) {
1210
+ this.#settle(tenant, key, record, "expired");
1211
+ throw new ByokCoreError(
1212
+ "storage_reservation_expired",
1213
+ `Reservation ${input.reservationId} expired at ${reservation.expiresAt}.`
1214
+ );
1215
+ }
1216
+ if (input.observedByteSize !== reservation.expectedBytes || input.observedContentType !== reservation.contentType) {
1217
+ this.#settle(tenant, key, record, "aborted");
1218
+ throw new ByokCoreError(
1219
+ "storage_integrity_mismatch",
1220
+ `Observed object does not match reservation ${input.reservationId}.`
1221
+ );
1222
+ }
1223
+ if (reservation.kind === "object") {
1224
+ try {
1225
+ await this.#objects.commit(tenant, {
1226
+ hash: reservation.contentHash,
1227
+ observedByteSize: input.observedByteSize,
1228
+ observedContentType: input.observedContentType
1229
+ });
1230
+ } catch (error) {
1231
+ this.#settle(tenant, key, record, "aborted");
1232
+ if (error instanceof ByokCoreError && (error.code === "object_not_found" || error.code === "object_state_invalid")) {
1233
+ throw new ByokCoreError(
1234
+ "storage_integrity_mismatch",
1235
+ `Reservation ${input.reservationId} has no committable object manifest.`,
1236
+ { cause: error }
1237
+ );
1238
+ }
1239
+ throw error;
1240
+ }
1241
+ }
1242
+ const hashes = this.#hashesOf(tenant);
1243
+ const deduplicated = hashes.has(reservation.contentHash);
1244
+ const settled = this.#settle(tenant, key, record, "committed", deduplicated);
1245
+ if (!deduplicated) {
1246
+ hashes.add(reservation.contentHash);
1247
+ const current = this.#usageOf(tenant);
1248
+ this.#setUsage(tenant, {
1249
+ ...current,
1250
+ committedObjectBytes: reservation.kind === "object" ? current.committedObjectBytes + reservation.expectedBytes : current.committedObjectBytes,
1251
+ committedInlineBytes: reservation.kind === "inline" ? current.committedInlineBytes + reservation.expectedBytes : current.committedInlineBytes,
1252
+ objectCount: reservation.kind === "object" ? current.objectCount + 1n : current.objectCount
1253
+ });
1254
+ }
1255
+ return { reservation: settled, usage: this.#usageOf(tenant), deduplicated };
1256
+ }
1257
+ async abortReservation(tenant, reservationId) {
1258
+ const key = tenantKey(tenant, reservationId);
1259
+ const record = this.#requireReservation(tenant, reservationId);
1260
+ if (record.reservation.state !== "reserved") return record.reservation;
1261
+ return this.#settle(tenant, key, record, "aborted");
1262
+ }
1263
+ async expireReservations(tenant) {
1264
+ const prefix = tenantKey(tenant, "");
1265
+ const now = this.#now();
1266
+ const expired = [];
1267
+ for (const [key, record] of [...this.#reservations.entries()]) {
1268
+ if (!key.startsWith(prefix)) continue;
1269
+ if (record.reservation.state !== "reserved") continue;
1270
+ if (now < record.reservation.expiresAt) continue;
1271
+ expired.push(this.#settle(tenant, key, record, "expired"));
1272
+ }
1273
+ return expired;
1274
+ }
1275
+ async applyMailboxDelta(tenant, input) {
1276
+ const entitlement = this.#requireEntitlement(tenant);
1277
+ const usage = this.#usageOf(tenant);
1278
+ const next = usage.mailboxBytes + input.deltaBytes;
1279
+ if (input.deltaBytes > 0n && next > entitlement.mailboxLimitBytes) {
1280
+ throw new ByokCoreError(
1281
+ "storage_quota_exceeded",
1282
+ `Mailbox would reach ${String(next)} bytes, over the limit of ${String(entitlement.mailboxLimitBytes)} bytes.`
1283
+ );
1284
+ }
1285
+ this.#setUsage(tenant, { ...usage, mailboxBytes: next < 0n ? 0n : next });
1286
+ return this.#usageOf(tenant);
1287
+ }
1288
+ #settle(tenant, key, record, state, deduplicated = false) {
1289
+ const settled = {
1290
+ ...record.reservation,
1291
+ state,
1292
+ settledAt: this.#now()
1293
+ };
1294
+ this.#reservations.set(key, { reservation: settled, deduplicated });
1295
+ const usage = this.#usageOf(tenant);
1296
+ const released = usage.reservedBytes - record.reservation.expectedBytes;
1297
+ this.#setUsage(tenant, { ...usage, reservedBytes: released < 0n ? 0n : released });
1298
+ return settled;
1299
+ }
1300
+ #posture(entitlement, usage, graceActive) {
1301
+ const used = this.#usedBytes(usage);
1302
+ if (used >= entitlement.hardLimitBytes) {
1303
+ const graceConfigured = entitlement.downgradeGraceUntil !== void 0;
1304
+ return graceConfigured && !graceActive ? "suspended" : "blocked";
1305
+ }
1306
+ if (entitlement.hardLimitBytes > 0n && used * WARNING_DENOMINATOR >= entitlement.hardLimitBytes * WARNING_NUMERATOR) {
1307
+ return "warning";
1308
+ }
1309
+ return "normal";
1310
+ }
1311
+ #usedBytes(usage) {
1312
+ return usage.committedObjectBytes + usage.committedInlineBytes + usage.reservedBytes;
1313
+ }
1314
+ #usageOf(tenant) {
1315
+ const existing = this.#usage.get(tenant);
1316
+ if (existing !== void 0) return existing;
1317
+ const empty = {
1318
+ committedObjectBytes: 0n,
1319
+ committedInlineBytes: 0n,
1320
+ reservedBytes: 0n,
1321
+ mailboxBytes: 0n,
1322
+ objectCount: 0n,
1323
+ updatedAt: this.#now()
1324
+ };
1325
+ this.#usage.set(tenant, empty);
1326
+ return empty;
1327
+ }
1328
+ #setUsage(tenant, usage) {
1329
+ this.#usage.set(tenant, { ...usage, updatedAt: this.#now() });
1330
+ }
1331
+ #hashesOf(tenant) {
1332
+ const existing = this.#committedHashes.get(tenant);
1333
+ if (existing !== void 0) return existing;
1334
+ const created = /* @__PURE__ */ new Set();
1335
+ this.#committedHashes.set(tenant, created);
1336
+ return created;
1337
+ }
1338
+ #requireEntitlement(tenant) {
1339
+ const entitlement = this.#entitlements.get(tenant);
1340
+ if (entitlement === void 0) {
1341
+ throw new ByokCoreError(
1342
+ "storage_entitlement_missing",
1343
+ "No storage entitlement has been issued for this tenant."
1344
+ );
1345
+ }
1346
+ return entitlement;
1347
+ }
1348
+ #requireReservation(tenant, reservationId) {
1349
+ const record = this.#reservations.get(tenantKey(tenant, reservationId));
1350
+ if (record === void 0) {
1351
+ throw new ByokCoreError(
1352
+ "storage_reservation_not_found",
1353
+ `Reservation ${reservationId} does not exist in this tenant.`
1354
+ );
1355
+ }
1356
+ return record;
1357
+ }
1358
+ #now() {
1359
+ return this.#clock.now().toISOString();
1360
+ }
1361
+ };
1362
+
1363
+ // src/in-memory/truth.ts
1364
+ var DEFAULT_MANIFEST_LIMIT = 100;
1365
+ var InMemoryTruthStore = class {
1366
+ #records = /* @__PURE__ */ new Map();
1367
+ #clock;
1368
+ constructor(clock) {
1369
+ this.#clock = clock;
1370
+ }
1371
+ async writeTerminal(tenant, input) {
1372
+ const key = tenantKey(tenant, "task.terminal", input.taskId);
1373
+ const existing = this.#records.get(key);
1374
+ if (existing !== void 0) {
1375
+ if (existing.contentHash === input.contentHash) {
1376
+ return existing;
1377
+ }
1378
+ throw new CoreConflictError(
1379
+ "terminal_conflict",
1380
+ `Task ${input.taskId} already has an immutable terminal record with a different hash.`,
1381
+ existing,
1382
+ this.#now()
1383
+ );
1384
+ }
1385
+ const record = {
1386
+ tenantId: tenant,
1387
+ kind: "task.terminal",
1388
+ recordKey: input.taskId,
1389
+ rev: 1,
1390
+ contentHash: input.contentHash,
1391
+ byteSize: input.byteSize,
1392
+ body: input.body,
1393
+ ...input.label === void 0 ? {} : { label: input.label },
1394
+ ...input.requestId === void 0 ? {} : { requestId: input.requestId },
1395
+ writtenAt: this.#now()
1396
+ };
1397
+ this.#records.set(key, record);
1398
+ return record;
1399
+ }
1400
+ async writeSnapshot(tenant, input) {
1401
+ const key = tenantKey(tenant, input.kind, input.recordKey);
1402
+ const existing = this.#records.get(key);
1403
+ const currentRev = existing?.rev ?? 0;
1404
+ if (input.expectedRev !== currentRev) {
1405
+ throw new CoreConflictError(
1406
+ "truth_revision_conflict",
1407
+ `Record ${input.kind}/${input.recordKey} is at rev ${currentRev}, not ${input.expectedRev}.`,
1408
+ existing,
1409
+ this.#now()
1410
+ );
1411
+ }
1412
+ const record = {
1413
+ tenantId: tenant,
1414
+ kind: input.kind,
1415
+ recordKey: input.recordKey,
1416
+ rev: currentRev + 1,
1417
+ contentHash: input.contentHash,
1418
+ byteSize: input.byteSize,
1419
+ body: input.body,
1420
+ ...input.label === void 0 ? {} : { label: input.label },
1421
+ ...input.requestId === void 0 ? {} : { requestId: input.requestId },
1422
+ writtenAt: this.#now()
1423
+ };
1424
+ this.#records.set(key, record);
1425
+ return record;
1426
+ }
1427
+ async getRecord(tenant, selector) {
1428
+ return this.#records.get(tenantKey(tenant, selector.kind, selector.recordKey));
1429
+ }
1430
+ async listManifest(tenant, query) {
1431
+ const prefix = tenantKey(tenant, "");
1432
+ const entries = [];
1433
+ for (const [key, record] of this.#records.entries()) {
1434
+ if (!key.startsWith(prefix)) continue;
1435
+ if (query.kind !== void 0 && record.kind !== query.kind) continue;
1436
+ if (query.keyPrefix !== void 0 && !record.recordKey.startsWith(query.keyPrefix)) {
1437
+ continue;
1438
+ }
1439
+ entries.push({
1440
+ kind: record.kind,
1441
+ recordKey: record.recordKey,
1442
+ rev: record.rev,
1443
+ contentHash: record.contentHash,
1444
+ byteSize: record.byteSize,
1445
+ ...record.label === void 0 ? {} : { label: record.label },
1446
+ updatedAt: record.writtenAt
1447
+ });
1448
+ }
1449
+ entries.sort(
1450
+ (left, right) => left.kind.localeCompare(right.kind) || left.recordKey.localeCompare(right.recordKey)
1451
+ );
1452
+ return entries.slice(0, query.limit ?? DEFAULT_MANIFEST_LIMIT);
1453
+ }
1454
+ #now() {
1455
+ return this.#clock.now().toISOString();
1456
+ }
1457
+ };
1458
+
1459
+ // src/in-memory/clock.ts
1460
+ var IN_MEMORY_CLOCK_EPOCH = "2026-01-01T00:00:00.000Z";
1461
+ function createMutableClock(start = new Date(IN_MEMORY_CLOCK_EPOCH)) {
1462
+ let current = start.getTime();
1463
+ return {
1464
+ now() {
1465
+ return new Date(current);
1466
+ },
1467
+ advance(ms) {
1468
+ current += ms;
1469
+ },
1470
+ set(instant) {
1471
+ current = instant.getTime();
1472
+ }
1473
+ };
1474
+ }
1475
+
1476
+ // src/in-memory/index.ts
1477
+ function createInMemoryCoreStores(options = {}) {
1478
+ const clock = options.clock ?? createMutableClock();
1479
+ const objects = new InMemoryObjectStore(clock);
1480
+ const stores = {
1481
+ mailbox: new InMemoryMailboxStore(clock),
1482
+ board: new InMemoryBoardStore(clock),
1483
+ truth: new InMemoryTruthStore(clock),
1484
+ presence: new InMemoryPresenceStore(clock),
1485
+ activity: new InMemoryActivityStore(clock),
1486
+ objects,
1487
+ quota: new InMemoryQuotaStore(clock, objects)
1488
+ };
1489
+ return { stores, clock };
1490
+ }
1491
+ function createInMemoryCoreCompositionWithClock() {
1492
+ const clock = createMutableClock();
1493
+ return { stores: createInMemoryCoreStores({ clock }).stores, clock };
1494
+ }
1495
+
1496
+ export { BOARD_STATUSES, BOARD_TRANSITIONS, ByokCoreError, CANONICAL_TIMESTAMP_PATTERN, CAPABILITY_DECLARATION_SCHEMA_ID, CAPABILITY_NAME_PATTERN, CONTENT_HASH_PATTERN, CORE_ERROR_CODES, CORE_PORT_INTERFACES, CORE_PORT_METHODS, CORE_STORE_NAMES, CapabilityDeclarationSchema, CoreConflictError, DEFAULT_ACTIVITY_CAPACITY, DEVICE_PROOF_ALGORITHMS, DEVICE_PROOF_DOMAIN_PREFIX, DEVICE_PROOF_SCHEMA_ID, DEVICE_PROOF_VERSION, DeviceProofEnvelopeV1Schema, DeviceProofProtectedClaimsSchema, IN_MEMORY_CLOCK_EPOCH, InMemoryActivityStore, InMemoryBoardStore, InMemoryMailboxStore, InMemoryObjectStore, InMemoryPresenceStore, InMemoryQuotaStore, InMemoryTruthStore, MAILBOX_MESSAGE_STATES, OBJECT_STATES, OBJECT_STATE_TRANSITIONS, PRESENCE_LEVELS, PRINCIPAL_KINDS, STORAGE_ERROR_CODES, STORAGE_ERROR_HTTP_STATUS, STORAGE_RESERVATION_STATES, STORAGE_WRITE_KINDS, STORAGE_WRITE_POSTURES, TENANT_ID_MAX_LENGTH, TENANT_KEY_SEPARATOR, TRUTH_RECORD_KINDS, assertCanonicalTimestamp, assertCapability, canonicalizeJson, canonicalizeJsonBytes, contentHash, createInMemoryCoreCompositionWithClock, createInMemoryCoreStores, createMutableClock, deviceProofCanonicalClaims, deviceProofCanonicalJson, deviceProofSigningInput, hasCapability, isCanonicalTimestamp, isContentHash, isControlPlanePrincipal, isCoreConflictError, isCoreError, isDevicePrincipal, isLegalBoardTransition, isLegalObjectTransition, isTenantId, parseCapabilityDeclaration, parseDeviceProofEnvelope, principalTenant, tenantId, tenantKey, tenantObjectKey };
1497
+ //# sourceMappingURL=index.js.map
1498
+ //# sourceMappingURL=index.js.map