@byok-sdk/core 0.26.1 → 0.27.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 CHANGED
@@ -2,6 +2,8 @@ import { z } from 'zod';
2
2
 
3
3
  // src/errors.ts
4
4
  var CORE_ERROR_CODES = {
5
+ retention_input_invalid: "retention_input_invalid",
6
+ retention_collect_failed: "retention_collect_failed",
5
7
  // identity + content addressing
6
8
  tenant_id_invalid: "tenant_id_invalid",
7
9
  content_hash_invalid: "content_hash_invalid",
@@ -544,10 +546,18 @@ var CORE_STORE_NAMES = [
544
546
  // src/ports-contract.ts
545
547
  var CORE_NON_COMPOSITION_PORT_NAMES = [];
546
548
  var CORE_PORT_METHODS = {
547
- mailbox: ["append", "readAfter", "recordDelivery", "advanceCursor", "readCursor", "collectRetired"],
549
+ mailbox: [
550
+ "append",
551
+ "readUsage",
552
+ "readAfter",
553
+ "recordDelivery",
554
+ "advanceCursor",
555
+ "readCursor",
556
+ "collectExpired"
557
+ ],
548
558
  board: ["create", "get", "list", "claim", "unclaim", "updateStatus"],
549
559
  truth: ["writeTerminal", "writeSnapshot", "getRecord", "listManifest"],
550
- presence: ["publish", "read", "list"],
560
+ presence: ["publish", "read", "list", "collectExpired"],
551
561
  objects: [
552
562
  "putManifest",
553
563
  "commit",
@@ -556,7 +566,8 @@ var CORE_PORT_METHODS = {
556
566
  "addReference",
557
567
  "removeReference",
558
568
  "markDeletePending",
559
- "markDeleted"
569
+ "markDeleted",
570
+ "collectExpired"
560
571
  ],
561
572
  quota: [
562
573
  "readEntitlement",
@@ -567,8 +578,7 @@ var CORE_PORT_METHODS = {
567
578
  "reserve",
568
579
  "finalizeReservation",
569
580
  "abortReservation",
570
- "expireReservations",
571
- "applyMailboxDelta"
581
+ "collectExpired"
572
582
  ],
573
583
  skillPacks: ["publish", "get", "list", "readFile"]
574
584
  };
@@ -1671,6 +1681,98 @@ function nonceSigningBytes(nonce) {
1671
1681
  return new TextEncoder().encode(NONCE_SIGNING_DOMAIN + nonce);
1672
1682
  }
1673
1683
 
1684
+ // src/in-memory/storage-ledger.ts
1685
+ var InMemoryStorageLedger = class {
1686
+ constructor(clock) {
1687
+ this.clock = clock;
1688
+ }
1689
+ clock;
1690
+ #usage = /* @__PURE__ */ new Map();
1691
+ #reservations = /* @__PURE__ */ new Map();
1692
+ #committed = /* @__PURE__ */ new Map();
1693
+ readReservation(key) {
1694
+ return this.#reservations.get(key);
1695
+ }
1696
+ writeReservation(key, record) {
1697
+ this.#reservations.set(key, record);
1698
+ }
1699
+ deleteReservation(key) {
1700
+ this.#reservations.delete(key);
1701
+ }
1702
+ reservations() {
1703
+ return this.#reservations.entries();
1704
+ }
1705
+ readUsage(tenant) {
1706
+ let usage = this.#usage.get(tenant);
1707
+ if (usage === void 0) {
1708
+ usage = { committedObjectBytes: 0n, committedInlineBytes: 0n, reservedBytes: 0n, objectCount: 0n, updatedAt: this.clock.now().toISOString() };
1709
+ this.#usage.set(tenant, usage);
1710
+ }
1711
+ return usage;
1712
+ }
1713
+ writeUsage(tenant, usage) {
1714
+ this.#usage.set(tenant, usage);
1715
+ }
1716
+ isCounted(tenant, hash) {
1717
+ return this.#committed.get(tenant)?.has(hash) === true;
1718
+ }
1719
+ recordCommitted(tenant, reservation) {
1720
+ let hashes = this.#committed.get(tenant);
1721
+ if (hashes === void 0) {
1722
+ hashes = /* @__PURE__ */ new Map();
1723
+ this.#committed.set(tenant, hashes);
1724
+ }
1725
+ hashes.set(reservation.contentHash, { kind: reservation.kind, bytes: reservation.expectedBytes });
1726
+ }
1727
+ hasReservedObject(tenant, hash) {
1728
+ for (const { reservation } of this.#reservations.values()) {
1729
+ if (reservation.tenantId === tenant && reservation.kind === "object" && reservation.contentHash === hash && reservation.state === "reserved") return true;
1730
+ }
1731
+ return false;
1732
+ }
1733
+ reservedObjectHashes(tenant) {
1734
+ return [...this.#reservations.values()].filter(({ reservation }) => reservation.tenantId === tenant && reservation.kind === "object" && reservation.state === "reserved").map(({ reservation }) => reservation.contentHash);
1735
+ }
1736
+ prepareCollectedObjectRelease(tenant, hash, byteSize, now) {
1737
+ const hashes = this.#committed.get(tenant);
1738
+ const accounted = hashes?.get(hash);
1739
+ if (accounted === void 0 || accounted.kind === "inline") return { releasedBytes: 0n, commit: () => {
1740
+ } };
1741
+ const usage = this.#usage.get(tenant);
1742
+ if (usage === void 0 || accounted.bytes !== byteSize || usage.committedObjectBytes < accounted.bytes || usage.objectCount < 1n) {
1743
+ throw new ByokCoreError("storage_integrity_mismatch", "Collected object accounting does not match the storage ledger.");
1744
+ }
1745
+ return { releasedBytes: accounted.bytes, commit: () => {
1746
+ hashes.delete(hash);
1747
+ this.writeUsage(tenant, { ...usage, committedObjectBytes: usage.committedObjectBytes - accounted.bytes, objectCount: usage.objectCount - 1n, updatedAt: now });
1748
+ } };
1749
+ }
1750
+ releaseCollectedObject(tenant, hash, byteSize, now) {
1751
+ const release = this.prepareCollectedObjectRelease(tenant, hash, byteSize, now);
1752
+ release.commit();
1753
+ return release.releasedBytes;
1754
+ }
1755
+ };
1756
+
1757
+ // src/retention.ts
1758
+ function assertCollectExpiredInput(input) {
1759
+ assertCanonicalTimestamp(input.now, "now");
1760
+ if (!Number.isSafeInteger(input.limit) || input.limit <= 0) {
1761
+ throw new ByokCoreError("retention_input_invalid", "collectExpired limit must be a positive safe integer.");
1762
+ }
1763
+ }
1764
+ var CollectExpiredFailure = class extends ByokCoreError {
1765
+ constructor(committed, failedRows, more, options) {
1766
+ super("retention_collect_failed", "Retention collection did not complete.", options);
1767
+ this.committed = committed;
1768
+ this.failedRows = failedRows;
1769
+ this.more = more;
1770
+ }
1771
+ committed;
1772
+ failedRows;
1773
+ more;
1774
+ };
1775
+
1674
1776
  // src/in-memory/presence.ts
1675
1777
  function assertTtl(ttlMs) {
1676
1778
  if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
@@ -1755,6 +1857,20 @@ var InMemoryPresenceStore = class {
1755
1857
  live.sort((left, right) => left.deviceId.localeCompare(right.deviceId));
1756
1858
  return live;
1757
1859
  }
1860
+ async collectExpired(tenant, input) {
1861
+ assertCollectExpiredInput(input);
1862
+ let removed = 0, more = false;
1863
+ for (const [key, hint] of this.#hints) {
1864
+ if (hint.tenantId !== tenant || hint.expiresAt > input.now) continue;
1865
+ if (removed === input.limit) {
1866
+ more = true;
1867
+ break;
1868
+ }
1869
+ this.#hints.delete(key);
1870
+ removed++;
1871
+ }
1872
+ return { removed, expired: 0, releasedBytes: 0n, more };
1873
+ }
1758
1874
  #isExpired(expiresAt) {
1759
1875
  return this.#clock.now().toISOString() >= expiresAt;
1760
1876
  }
@@ -1954,16 +2070,29 @@ var DEFAULT_READ_LIMIT = 50;
1954
2070
  var InMemoryMailboxStore = class {
1955
2071
  #devices = /* @__PURE__ */ new Map();
1956
2072
  #clock;
1957
- constructor(clock) {
2073
+ #quota;
2074
+ /** The mailbox reads its limit from quota; quota never reads the mailbox. */
2075
+ constructor(clock, quota) {
1958
2076
  this.#clock = clock;
2077
+ this.#quota = quota;
1959
2078
  }
1960
2079
  async append(tenant, input) {
1961
2080
  const device = this.#device(tenant, input.deviceId);
1962
2081
  const appended = device.mutationTail.then(async () => {
1963
2082
  const existing = device.byMessageId.get(input.messageId);
1964
2083
  if (existing !== void 0) return existing;
2084
+ const limit = input.admission === "cancellation" ? void 0 : await this.#mailboxLimit(tenant);
1965
2085
  const seq = device.nextSeq;
1966
2086
  const materialized = await input.materialize(seq);
2087
+ if (limit !== void 0) {
2088
+ const used = this.#usedBytes(tenant);
2089
+ if (used + materialized.byteSize > limit) {
2090
+ throw new ByokCoreError(
2091
+ "storage_quota_exceeded",
2092
+ `Mailbox would reach ${String(used + materialized.byteSize)} bytes, over the limit of ${String(limit)} bytes.`
2093
+ );
2094
+ }
2095
+ }
1967
2096
  const message = {
1968
2097
  tenantId: tenant,
1969
2098
  deviceId: input.deviceId,
@@ -1984,6 +2113,9 @@ var InMemoryMailboxStore = class {
1984
2113
  );
1985
2114
  return appended;
1986
2115
  }
2116
+ async readUsage(tenant) {
2117
+ return { bytes: this.#usedBytes(tenant) };
2118
+ }
1987
2119
  async readAfter(tenant, query) {
1988
2120
  const device = this.#devices.get(tenantKey(tenant, query.deviceId));
1989
2121
  const limit = query.limit ?? DEFAULT_READ_LIMIT;
@@ -2061,33 +2193,69 @@ var InMemoryMailboxStore = class {
2061
2193
  }
2062
2194
  return this.#cursorState(tenant, deviceId, device);
2063
2195
  }
2064
- async collectRetired(tenant, input) {
2196
+ async collectExpired(tenant, input) {
2197
+ assertCollectExpiredInput(input);
2065
2198
  assertCanonicalTimestamp(input.ackedBefore, "ackedBefore");
2066
2199
  assertCanonicalTimestamp(input.expireUnackedBefore, "expireUnackedBefore");
2067
- let deletedCount = 0;
2068
- let expiredCount = 0;
2069
- let releasedBytes = 0n;
2070
- for (const [key, device] of this.#devices.entries()) {
2071
- if (!key.startsWith(tenantKey(tenant, ""))) continue;
2072
- if (input.deviceId !== void 0 && key !== tenantKey(tenant, input.deviceId)) continue;
2073
- for (let index = device.messages.length - 1; index >= 0; index -= 1) {
2074
- const message = device.messages[index];
2075
- if (message.state === "acked" && message.appendedAt < input.ackedBefore) {
2076
- device.messages.splice(index, 1);
2077
- device.byMessageId.delete(message.messageId);
2078
- deletedCount += 1;
2079
- releasedBytes += message.byteSize;
2080
- continue;
2081
- }
2082
- if (message.state === "pending" && message.appendedAt < input.expireUnackedBefore) {
2083
- const expired = { ...message, state: "expired" };
2084
- device.messages[index] = expired;
2085
- device.byMessageId.set(expired.messageId, expired);
2086
- expiredCount += 1;
2200
+ let removed = 0, expired = 0, more = false;
2201
+ try {
2202
+ phases: for (const phase of ["acked", "pending"]) {
2203
+ for (const [key, device] of this.#devices) {
2204
+ if (!key.startsWith(tenantKey(tenant, ""))) continue;
2205
+ const collected = device.mutationTail.then(() => {
2206
+ for (let index = 0; index < device.messages.length; ) {
2207
+ const message = device.messages[index];
2208
+ const cutoff = phase === "acked" ? input.ackedBefore : input.expireUnackedBefore;
2209
+ if (message.state !== phase || message.appendedAt >= cutoff) {
2210
+ index++;
2211
+ continue;
2212
+ }
2213
+ if (removed + expired === input.limit) {
2214
+ more = true;
2215
+ return;
2216
+ }
2217
+ if (phase === "acked") {
2218
+ device.messages.splice(index, 1);
2219
+ device.byMessageId.delete(message.messageId);
2220
+ removed++;
2221
+ } else {
2222
+ const retired = { ...message, state: "expired" };
2223
+ device.messages[index] = retired;
2224
+ device.byMessageId.set(retired.messageId, retired);
2225
+ expired++;
2226
+ index++;
2227
+ }
2228
+ }
2229
+ });
2230
+ device.mutationTail = collected.then(() => void 0, () => void 0);
2231
+ await collected;
2232
+ if (more) break phases;
2087
2233
  }
2088
2234
  }
2235
+ } catch (cause) {
2236
+ throw new CollectExpiredFailure({ removed, expired, releasedBytes: 0n }, 0, void 0, { cause });
2237
+ }
2238
+ return { removed, expired, releasedBytes: 0n, more };
2239
+ }
2240
+ async #mailboxLimit(tenant) {
2241
+ const entitlement = await this.#quota.readEntitlement(tenant);
2242
+ if (entitlement === void 0) {
2243
+ throw new ByokCoreError(
2244
+ "storage_entitlement_missing",
2245
+ "No storage entitlement has been issued for this tenant."
2246
+ );
2089
2247
  }
2090
- return { deletedCount, expiredCount, releasedBytes };
2248
+ return entitlement.mailboxLimitBytes;
2249
+ }
2250
+ /** Every retained row of the tenant, in any state, on any device. */
2251
+ #usedBytes(tenant) {
2252
+ const prefix = tenantKey(tenant, "");
2253
+ let bytes = 0n;
2254
+ for (const [key, device] of this.#devices.entries()) {
2255
+ if (!key.startsWith(prefix)) continue;
2256
+ for (const message of device.messages) bytes += message.byteSize;
2257
+ }
2258
+ return bytes;
2091
2259
  }
2092
2260
  #device(tenant, deviceId) {
2093
2261
  if (deviceId.length === 0) {
@@ -2141,12 +2309,18 @@ var InMemoryMailboxStore = class {
2141
2309
  // src/in-memory/blob.ts
2142
2310
  var DEFAULT_LIST_LIMIT2 = 100;
2143
2311
  var InMemoryObjectStore = class {
2312
+ constructor(clock, maintenance) {
2313
+ this.maintenance = maintenance;
2314
+ this.#clock = clock;
2315
+ if (maintenance === void 0 || maintenance.ledger === void 0 || typeof maintenance.bytes?.deleteObject !== "function") {
2316
+ throw new ByokCoreError("retention_input_invalid", "ObjectStore requires its shared ledger and an explicit byte authority.");
2317
+ }
2318
+ }
2319
+ maintenance;
2144
2320
  #manifest = /* @__PURE__ */ new Map();
2145
2321
  #references = /* @__PURE__ */ new Map();
2146
2322
  #clock;
2147
- constructor(clock) {
2148
- this.#clock = clock;
2149
- }
2323
+ #deleteCursor = /* @__PURE__ */ new Map();
2150
2324
  async putManifest(tenant, input) {
2151
2325
  const key = tenantKey(tenant, input.hash);
2152
2326
  const existing = this.#manifest.get(key);
@@ -2236,6 +2410,46 @@ var InMemoryObjectStore = class {
2236
2410
  const entry = this.#require(tenant, hash);
2237
2411
  return this.#transition(tenant, entry, "deleted");
2238
2412
  }
2413
+ async collectExpired(tenant, input) {
2414
+ assertCollectExpiredInput(input);
2415
+ assertCanonicalTimestamp(input.orphanBefore, "orphanBefore");
2416
+ const eligible = (entry) => entry.tenantId === tenant && entry.refCount === 0 && !this.maintenance.ledger.hasReservedObject(tenant, entry.hash) && (entry.state === "delete_pending" || (entry.state === "pending" || entry.state === "committed") && entry.updatedAt < input.orphanBefore);
2417
+ let removed = 0, releasedBytes = 0n, more = false, visited = 0, failures = 0;
2418
+ let firstFailure;
2419
+ const cursor = this.#deleteCursor.get(tenant) ?? "";
2420
+ const entries = [...this.#manifest.values()].filter(eligible);
2421
+ const pending = entries.filter((entry) => entry.state === "delete_pending").sort((a, b) => a.hash.localeCompare(b.hash));
2422
+ const first = pending.filter((entry) => entry.hash > cursor).slice(0, input.limit);
2423
+ const fresh = entries.filter((entry) => entry.state !== "delete_pending").sort((a, b) => a.updatedAt.localeCompare(b.updatedAt) || a.hash.localeCompare(b.hash)).slice(0, input.limit - first.length);
2424
+ const wrapped = pending.filter((entry) => entry.hash <= cursor).slice(0, input.limit - first.length - fresh.length);
2425
+ const candidates = [...first, ...fresh, ...wrapped];
2426
+ more = entries.length > candidates.length;
2427
+ for (const candidate of candidates) {
2428
+ if (visited++ === input.limit) {
2429
+ more = true;
2430
+ break;
2431
+ }
2432
+ const entry = this.#manifest.get(tenantKey(tenant, candidate.hash));
2433
+ if (!eligible(entry)) continue;
2434
+ const pending2 = entry.state === "delete_pending" ? entry : { ...entry, state: "delete_pending", deletePendingAt: input.now, updatedAt: input.now };
2435
+ this.#manifest.set(tenantKey(tenant, entry.hash), pending2);
2436
+ try {
2437
+ const release = this.maintenance.ledger.prepareCollectedObjectRelease(tenant, entry.hash, entry.byteSize, input.now);
2438
+ this.maintenance.bytes.deleteObject(tenant, entry.hash);
2439
+ release.commit();
2440
+ const released = release.releasedBytes;
2441
+ this.#manifest.set(tenantKey(tenant, entry.hash), { ...pending2, state: "deleted", updatedAt: input.now });
2442
+ removed++;
2443
+ releasedBytes += released;
2444
+ } catch (cause) {
2445
+ failures++;
2446
+ firstFailure ??= cause;
2447
+ }
2448
+ }
2449
+ this.#deleteCursor.set(tenant, wrapped.at(-1)?.hash ?? first.at(-1)?.hash ?? "");
2450
+ if (failures > 0) throw new CollectExpiredFailure({ removed, expired: 0, releasedBytes }, failures, true, { cause: firstFailure });
2451
+ return { removed, expired: 0, releasedBytes, more };
2452
+ }
2239
2453
  #transition(tenant, entry, next) {
2240
2454
  if (!isLegalObjectTransition(entry.state, next)) {
2241
2455
  throw new ByokCoreError(
@@ -2282,16 +2496,15 @@ var InMemoryObjectStore = class {
2282
2496
  var WARNING_NUMERATOR = 80n;
2283
2497
  var WARNING_DENOMINATOR = 100n;
2284
2498
  var InMemoryQuotaStore = class {
2285
- #entitlements = /* @__PURE__ */ new Map();
2286
- #usage = /* @__PURE__ */ new Map();
2287
- #reservations = /* @__PURE__ */ new Map();
2288
- #committedHashes = /* @__PURE__ */ new Map();
2289
- #clock;
2290
- #objects;
2291
- constructor(clock, objects) {
2499
+ constructor(clock, objects, ledger) {
2500
+ this.ledger = ledger;
2292
2501
  this.#clock = clock;
2293
2502
  this.#objects = objects;
2294
2503
  }
2504
+ ledger;
2505
+ #entitlements = /* @__PURE__ */ new Map();
2506
+ #clock;
2507
+ #objects;
2295
2508
  async readEntitlement(tenant) {
2296
2509
  return this.#entitlements.get(tenant);
2297
2510
  }
@@ -2338,13 +2551,13 @@ var InMemoryQuotaStore = class {
2338
2551
  };
2339
2552
  }
2340
2553
  async readReservation(tenant, reservationId) {
2341
- return this.#reservations.get(tenantKey(tenant, reservationId))?.reservation;
2554
+ return this.ledger.readReservation(tenantKey(tenant, reservationId))?.reservation;
2342
2555
  }
2343
2556
  async reserve(tenant, input) {
2344
2557
  const entitlement = this.#requireEntitlement(tenant);
2345
- await this.expireReservations(tenant);
2558
+ await Array.from(this.#expireReservations(tenant));
2346
2559
  const key = tenantKey(tenant, input.reservationId);
2347
- const existing = this.#reservations.get(key);
2560
+ const existing = this.ledger.readReservation(key);
2348
2561
  if (existing !== void 0) {
2349
2562
  if (existing.reservation.state === "reserved") {
2350
2563
  if (existing.reservation.kind !== input.kind || existing.reservation.expectedBytes !== input.expectedBytes || existing.reservation.contentHash !== input.contentHash || existing.reservation.contentType !== input.contentType) {
@@ -2393,7 +2606,7 @@ var InMemoryQuotaStore = class {
2393
2606
  createdAt: now,
2394
2607
  expiresAt: new Date(this.#clock.now().getTime() + input.ttlMs).toISOString()
2395
2608
  };
2396
- this.#reservations.set(key, { reservation, deduplicated: false });
2609
+ this.ledger.writeReservation(key, { reservation, deduplicated: false });
2397
2610
  this.#setUsage(tenant, { ...usage, reservedBytes: usage.reservedBytes + input.expectedBytes });
2398
2611
  return reservation;
2399
2612
  }
@@ -2447,11 +2660,10 @@ var InMemoryQuotaStore = class {
2447
2660
  throw error;
2448
2661
  }
2449
2662
  }
2450
- const hashes = this.#hashesOf(tenant);
2451
- const deduplicated = hashes.has(reservation.contentHash);
2663
+ const deduplicated = this.ledger.isCounted(tenant, reservation.contentHash);
2452
2664
  const settled = this.#settle(tenant, key, record, "committed", deduplicated);
2453
2665
  if (!deduplicated) {
2454
- hashes.add(reservation.contentHash);
2666
+ this.ledger.recordCommitted(tenant, reservation);
2455
2667
  const current = this.#usageOf(tenant);
2456
2668
  this.#setUsage(tenant, {
2457
2669
  ...current,
@@ -2468,41 +2680,59 @@ var InMemoryQuotaStore = class {
2468
2680
  if (record.reservation.state !== "reserved") return record.reservation;
2469
2681
  return this.#settle(tenant, key, record, "aborted");
2470
2682
  }
2471
- async expireReservations(tenant) {
2683
+ *#expireReservations(tenant, now = this.#now(), limit = Number.MAX_SAFE_INTEGER) {
2472
2684
  const prefix = tenantKey(tenant, "");
2473
- const now = this.#now();
2474
- const expired = [];
2475
- for (const [key, record] of [...this.#reservations.entries()]) {
2476
- if (!key.startsWith(prefix)) continue;
2477
- if (record.reservation.state !== "reserved") continue;
2478
- if (now < record.reservation.expiresAt) continue;
2479
- expired.push(this.#settle(tenant, key, record, "expired"));
2685
+ let expired = 0;
2686
+ for (const [key, record] of this.ledger.reservations()) {
2687
+ if (!key.startsWith(prefix) || record.reservation.state !== "reserved" || now < record.reservation.expiresAt) continue;
2688
+ yield this.#settle(tenant, key, record, "expired", false, now);
2689
+ if (++expired === limit) return;
2480
2690
  }
2481
- return expired;
2482
2691
  }
2483
- async applyMailboxDelta(tenant, input) {
2484
- const entitlement = this.#requireEntitlement(tenant);
2485
- const usage = this.#usageOf(tenant);
2486
- const next = usage.mailboxBytes + input.deltaBytes;
2487
- if (input.deltaBytes > 0n && next > entitlement.mailboxLimitBytes) {
2488
- throw new ByokCoreError(
2489
- "storage_quota_exceeded",
2490
- `Mailbox would reach ${String(next)} bytes, over the limit of ${String(entitlement.mailboxLimitBytes)} bytes.`
2491
- );
2692
+ async collectExpired(tenant, input) {
2693
+ assertCollectExpiredInput(input);
2694
+ assertCanonicalTimestamp(input.settledBefore, "settledBefore");
2695
+ let expired = 0, releasedBytes = 0n, removed = 0, more = false;
2696
+ try {
2697
+ for (const reservation of this.#expireReservations(tenant, input.now, input.limit)) {
2698
+ expired++;
2699
+ releasedBytes += reservation.expectedBytes;
2700
+ }
2701
+ } catch (cause) {
2702
+ throw new CollectExpiredFailure({ removed, expired, releasedBytes }, 1, true, { cause });
2492
2703
  }
2493
- this.#setUsage(tenant, { ...usage, mailboxBytes: next < 0n ? 0n : next });
2494
- return this.#usageOf(tenant);
2704
+ const prefix = tenantKey(tenant, "");
2705
+ try {
2706
+ for (const [key, record] of this.ledger.reservations()) {
2707
+ if (!key.startsWith(prefix)) continue;
2708
+ const row = record.reservation;
2709
+ const shouldExpire = row.state === "reserved" && row.expiresAt <= input.now;
2710
+ const shouldRemove = (row.state === "expired" || row.state === "aborted") && row.settledAt !== void 0 && row.settledAt < input.settledBefore;
2711
+ if (!shouldExpire && !shouldRemove) continue;
2712
+ if (shouldExpire || removed + expired === input.limit) {
2713
+ more = true;
2714
+ continue;
2715
+ }
2716
+ this.ledger.deleteReservation(key);
2717
+ removed++;
2718
+ }
2719
+ } catch (cause) {
2720
+ throw new CollectExpiredFailure({ removed, expired, releasedBytes }, 0, void 0, { cause });
2721
+ }
2722
+ return { removed, expired, releasedBytes, more };
2495
2723
  }
2496
- #settle(tenant, key, record, state, deduplicated = false) {
2724
+ #settle(tenant, key, record, state, deduplicated = false, settledAt = this.#now()) {
2497
2725
  const settled = {
2498
2726
  ...record.reservation,
2499
2727
  state,
2500
- settledAt: this.#now()
2728
+ settledAt
2501
2729
  };
2502
- this.#reservations.set(key, { reservation: settled, deduplicated });
2503
2730
  const usage = this.#usageOf(tenant);
2504
- const released = usage.reservedBytes - record.reservation.expectedBytes;
2505
- this.#setUsage(tenant, { ...usage, reservedBytes: released < 0n ? 0n : released });
2731
+ if (usage.reservedBytes < record.reservation.expectedBytes) {
2732
+ throw new ByokCoreError("storage_integrity_mismatch", "Reservation settlement would underflow reserved usage.");
2733
+ }
2734
+ this.ledger.writeReservation(key, { reservation: settled, deduplicated });
2735
+ this.#setUsage(tenant, { ...usage, reservedBytes: usage.reservedBytes - record.reservation.expectedBytes });
2506
2736
  return settled;
2507
2737
  }
2508
2738
  #posture(entitlement, usage, graceActive) {
@@ -2520,28 +2750,10 @@ var InMemoryQuotaStore = class {
2520
2750
  return usage.committedObjectBytes + usage.committedInlineBytes + usage.reservedBytes;
2521
2751
  }
2522
2752
  #usageOf(tenant) {
2523
- const existing = this.#usage.get(tenant);
2524
- if (existing !== void 0) return existing;
2525
- const empty = {
2526
- committedObjectBytes: 0n,
2527
- committedInlineBytes: 0n,
2528
- reservedBytes: 0n,
2529
- mailboxBytes: 0n,
2530
- objectCount: 0n,
2531
- updatedAt: this.#now()
2532
- };
2533
- this.#usage.set(tenant, empty);
2534
- return empty;
2753
+ return this.ledger.readUsage(tenant);
2535
2754
  }
2536
2755
  #setUsage(tenant, usage) {
2537
- this.#usage.set(tenant, { ...usage, updatedAt: this.#now() });
2538
- }
2539
- #hashesOf(tenant) {
2540
- const existing = this.#committedHashes.get(tenant);
2541
- if (existing !== void 0) return existing;
2542
- const created = /* @__PURE__ */ new Set();
2543
- this.#committedHashes.set(tenant, created);
2544
- return created;
2756
+ this.ledger.writeUsage(tenant, { ...usage, updatedAt: this.#now() });
2545
2757
  }
2546
2758
  #requireEntitlement(tenant) {
2547
2759
  const entitlement = this.#entitlements.get(tenant);
@@ -2554,7 +2766,7 @@ var InMemoryQuotaStore = class {
2554
2766
  return entitlement;
2555
2767
  }
2556
2768
  #requireReservation(tenant, reservationId) {
2557
- const record = this.#reservations.get(tenantKey(tenant, reservationId));
2769
+ const record = this.ledger.readReservation(tenantKey(tenant, reservationId));
2558
2770
  if (record === void 0) {
2559
2771
  throw new ByokCoreError(
2560
2772
  "storage_reservation_not_found",
@@ -2786,43 +2998,45 @@ var InMemoryDeviceAssertionReplayAuthority = class {
2786
2998
  this.#expiresAtByKey.set(key, expiresAt);
2787
2999
  return true;
2788
3000
  }
2789
- /** Delete at most `limit` keys whose assertion lifetime ended at or before `before`. */
2790
- async deleteExpired(before, limit) {
2791
- const cutoff = before.getTime();
2792
- if (!Number.isFinite(cutoff) || !Number.isSafeInteger(limit) || limit <= 0) {
2793
- throw new Error("device assertion replay cleanup bounds are invalid");
2794
- }
2795
- let deleted = 0;
3001
+ async collectExpired(tenant, input) {
3002
+ assertCollectExpiredInput(input);
3003
+ const cutoff = Date.parse(input.now);
3004
+ let removed = 0, more = false;
2796
3005
  for (const [key, expiresAt] of this.#expiresAtByKey) {
2797
- if (expiresAt > cutoff) continue;
3006
+ if (JSON.parse(key)[0] !== tenant || expiresAt > cutoff) continue;
3007
+ if (removed === input.limit) {
3008
+ more = true;
3009
+ break;
3010
+ }
2798
3011
  this.#expiresAtByKey.delete(key);
2799
- deleted += 1;
2800
- if (deleted === limit) break;
3012
+ removed++;
2801
3013
  }
2802
- return deleted;
3014
+ return { removed, expired: 0, releasedBytes: 0n, more };
2803
3015
  }
2804
3016
  };
2805
3017
 
2806
3018
  // src/in-memory/index.ts
2807
- function createInMemoryCoreStores(options = {}) {
3019
+ function createInMemoryCoreStores(options) {
2808
3020
  const clock = options.clock ?? createMutableClock();
2809
- const objects = new InMemoryObjectStore(clock);
3021
+ const ledger = new InMemoryStorageLedger(clock);
3022
+ const objects = new InMemoryObjectStore(clock, { ledger, bytes: options.objectBytes });
3023
+ const quota = new InMemoryQuotaStore(clock, objects, ledger);
2810
3024
  const stores = {
2811
- mailbox: new InMemoryMailboxStore(clock),
3025
+ mailbox: new InMemoryMailboxStore(clock, quota),
2812
3026
  board: new InMemoryBoardStore(clock),
2813
3027
  truth: new InMemoryTruthStore(clock),
2814
3028
  presence: new InMemoryPresenceStore(clock),
2815
3029
  objects,
2816
- quota: new InMemoryQuotaStore(clock, objects),
3030
+ quota,
2817
3031
  skillPacks: new InMemorySkillPackStore()
2818
3032
  };
2819
3033
  return { stores, clock };
2820
3034
  }
2821
- function createInMemoryCoreCompositionWithClock() {
3035
+ function createInMemoryCoreCompositionWithClock(objectBytes) {
2822
3036
  const clock = createMutableClock();
2823
- return { stores: createInMemoryCoreStores({ clock }).stores, clock };
3037
+ return { stores: createInMemoryCoreStores({ clock, objectBytes }).stores, clock };
2824
3038
  }
2825
3039
 
2826
- export { AGENT_MEMORY_INTENT_DIGEST_VERSION, BOARD_STATUSES, BOARD_TRANSITIONS, ByokCoreError, CANONICAL_TIMESTAMP_PATTERN, CAPABILITY_DECLARATION_SCHEMA_ID, CAPABILITY_NAME_PATTERN, CONTENT_HASH_PATTERN, CORE_ERROR_CODES, CORE_NON_COMPOSITION_PORT_NAMES, CORE_PORT_INTERFACES, CORE_PORT_METHODS, CORE_STORE_NAMES, CapabilityDeclarationSchema, CoreConflictError, DEVICE_ASSERTION_ALGORITHMS, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_DOMAIN_PREFIX, DEVICE_ASSERTION_MAX_TTL_MS, DEVICE_ASSERTION_SCHEMA_ID, DEVICE_ASSERTION_VERSION, DEVICE_PROOF_ALGORITHMS, DEVICE_PROOF_DOMAIN_PREFIX, DEVICE_PROOF_HEADER, DEVICE_PROOF_SCHEMA_ID, DEVICE_PROOF_VERSION, DeviceAssertionClaimsSchema, DeviceAssertionEnvelopeV1Schema, DeviceProofEnvelopeV1Schema, DeviceProofProtectedClaimsSchema, IN_MEMORY_CLOCK_EPOCH, InMemoryBoardStore, InMemoryDeviceAssertionReplayAuthority, InMemoryMailboxStore, InMemoryObjectStore, InMemoryPresenceStore, InMemoryQuotaStore, InMemorySkillPackStore, InMemoryTruthStore, MAILBOX_MESSAGE_STATES, NONCE_SIGNING_DOMAIN, OBJECT_KEY_PREFIX_PATTERN, OBJECT_STATES, OBJECT_STATE_TRANSITIONS, PRESENCE_LEVELS, PRINCIPAL_KINDS, PROVIDER_PROVISIONING_MAX_TTL_MS, PROVIDER_PROVISIONING_OPERATIONS, PROVIDER_PROVISIONING_REQUEST_VERSION, PROVIDER_PROVISIONING_SECRET_OPERATIONS, PROVIDER_SECRET_HPKE_INFO, PROVIDER_SECRET_HPKE_SUITE, PROVIDER_SECRET_MAX_BYTES, PROVIDER_SECRET_MAX_PADDED_BYTES, PROVIDER_SECRET_PAD_BLOCK_BYTES, ProviderProvisioningConfigV1Schema, ProviderProvisioningExpectedProfileSchema, ProviderProvisioningHeaderV1Schema, ProviderProvisioningIdentityRevisionSchema, ProviderProvisioningRequestV1Schema, SEALING_KEY_ID_LENGTH, SKILL_FRONTMATTER_FIELDS, SKILL_PACK_DESCRIPTION_MAX_LENGTH, SKILL_PACK_ENTRY_PATH, SKILL_PACK_FILE_MAX_BYTES, SKILL_PACK_FILE_PATH_MAX_LENGTH, SKILL_PACK_FILE_PATH_PATTERN, SKILL_PACK_FORBIDDEN_FIELDS, SKILL_PACK_MANIFEST_SCHEMA_ID, SKILL_PACK_MAX_BYTES, SKILL_PACK_MAX_FILES, SKILL_PACK_NAME_MAX_LENGTH, SKILL_PACK_NAME_PATTERN, SKILL_PACK_REJECTIONS, SKILL_PACK_VERSION_PATTERN, STORAGE_ERROR_CODES, STORAGE_ERROR_HTTP_STATUS, STORAGE_RESERVATION_STATES, STORAGE_WRITE_KINDS, STORAGE_WRITE_POSTURES, SealedProviderSecretV1Schema, SealingKeyClaimV1Schema, SealingKeyIdSchema, SealingPublicJwkSchema, SkillPackFileSchema, SkillPackManifestSchema, TASK_ASSERTION_AGENT_REF_MAX_BYTES, TASK_ASSERTION_DOMAIN_PREFIX, TASK_ASSERTION_SCHEMA_ID, TASK_ASSERTION_TOOLSET_ID_MAX_LENGTH, TASK_ASSERTION_VERSION, TENANT_ID_MAX_LENGTH, TENANT_KEY_SEPARATOR, TRUTH_RECORD_KINDS, TaskAssertionAgentRefSchema, TaskAssertionClaimsSchema, TaskAssertionEnvelopeV1Schema, agentMemoryIntentOperationDigest, assertCanonicalTimestamp, assertCapability, assertProviderProvisioningConfigDigest, authenticateDeviceAssertion, authenticateTaskAssertion, canonicalizeJson, canonicalizeJsonBytes, checkSkillPackEntry, checkSkillPackFileContent, checkSkillPackManifest, contentHash, createInMemoryCoreCompositionWithClock, createInMemoryCoreStores, createMutableClock, deriveSealingKeyId, deviceAssertionCanonicalClaims, deviceAssertionCanonicalJson, deviceAssertionSigningInput, deviceProofCanonicalClaims, deviceProofCanonicalJson, deviceProofSigningInput, hasCapability, isCanonicalTimestamp, isContentHash, isControlPlanePrincipal, isCoreConflictError, isCoreError, isDevicePrincipal, isLegalBoardTransition, isLegalObjectTransition, isSkillPackPathSafe, isTenantId, nonceSigningBytes, objectKeyPrefix, openProviderProvisioningSecret, parseCapabilityDeclaration, parseDeviceAssertionEnvelope, parseDeviceProofEnvelope, parseProviderProvisioningRequest, parseSkillFrontmatter, parseSkillPackManifest, parseTaskAssertionEnvelope, principalTenant, providerProvisioningConfigDigest, providerProvisioningRequestDigest, providerSecretAadBytes, sealProviderProvisioningRequest, sealingKeyClaimCanonicalBytes, skillPackContentHashInput, taskAssertionCanonicalClaims, taskAssertionCanonicalJson, taskAssertionSigningInput, tenantId, tenantKey, tenantObjectKey, verifyDeviceAssertion, verifyTaskAssertion };
3040
+ export { AGENT_MEMORY_INTENT_DIGEST_VERSION, BOARD_STATUSES, BOARD_TRANSITIONS, ByokCoreError, CANONICAL_TIMESTAMP_PATTERN, CAPABILITY_DECLARATION_SCHEMA_ID, CAPABILITY_NAME_PATTERN, CONTENT_HASH_PATTERN, CORE_ERROR_CODES, CORE_NON_COMPOSITION_PORT_NAMES, CORE_PORT_INTERFACES, CORE_PORT_METHODS, CORE_STORE_NAMES, CapabilityDeclarationSchema, CollectExpiredFailure, CoreConflictError, DEVICE_ASSERTION_ALGORITHMS, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_DOMAIN_PREFIX, DEVICE_ASSERTION_MAX_TTL_MS, DEVICE_ASSERTION_SCHEMA_ID, DEVICE_ASSERTION_VERSION, DEVICE_PROOF_ALGORITHMS, DEVICE_PROOF_DOMAIN_PREFIX, DEVICE_PROOF_HEADER, DEVICE_PROOF_SCHEMA_ID, DEVICE_PROOF_VERSION, DeviceAssertionClaimsSchema, DeviceAssertionEnvelopeV1Schema, DeviceProofEnvelopeV1Schema, DeviceProofProtectedClaimsSchema, IN_MEMORY_CLOCK_EPOCH, InMemoryBoardStore, InMemoryDeviceAssertionReplayAuthority, InMemoryMailboxStore, InMemoryObjectStore, InMemoryPresenceStore, InMemoryQuotaStore, InMemorySkillPackStore, InMemoryStorageLedger, InMemoryTruthStore, MAILBOX_MESSAGE_STATES, NONCE_SIGNING_DOMAIN, OBJECT_KEY_PREFIX_PATTERN, OBJECT_STATES, OBJECT_STATE_TRANSITIONS, PRESENCE_LEVELS, PRINCIPAL_KINDS, PROVIDER_PROVISIONING_MAX_TTL_MS, PROVIDER_PROVISIONING_OPERATIONS, PROVIDER_PROVISIONING_REQUEST_VERSION, PROVIDER_PROVISIONING_SECRET_OPERATIONS, PROVIDER_SECRET_HPKE_INFO, PROVIDER_SECRET_HPKE_SUITE, PROVIDER_SECRET_MAX_BYTES, PROVIDER_SECRET_MAX_PADDED_BYTES, PROVIDER_SECRET_PAD_BLOCK_BYTES, ProviderProvisioningConfigV1Schema, ProviderProvisioningExpectedProfileSchema, ProviderProvisioningHeaderV1Schema, ProviderProvisioningIdentityRevisionSchema, ProviderProvisioningRequestV1Schema, SEALING_KEY_ID_LENGTH, SKILL_FRONTMATTER_FIELDS, SKILL_PACK_DESCRIPTION_MAX_LENGTH, SKILL_PACK_ENTRY_PATH, SKILL_PACK_FILE_MAX_BYTES, SKILL_PACK_FILE_PATH_MAX_LENGTH, SKILL_PACK_FILE_PATH_PATTERN, SKILL_PACK_FORBIDDEN_FIELDS, SKILL_PACK_MANIFEST_SCHEMA_ID, SKILL_PACK_MAX_BYTES, SKILL_PACK_MAX_FILES, SKILL_PACK_NAME_MAX_LENGTH, SKILL_PACK_NAME_PATTERN, SKILL_PACK_REJECTIONS, SKILL_PACK_VERSION_PATTERN, STORAGE_ERROR_CODES, STORAGE_ERROR_HTTP_STATUS, STORAGE_RESERVATION_STATES, STORAGE_WRITE_KINDS, STORAGE_WRITE_POSTURES, SealedProviderSecretV1Schema, SealingKeyClaimV1Schema, SealingKeyIdSchema, SealingPublicJwkSchema, SkillPackFileSchema, SkillPackManifestSchema, TASK_ASSERTION_AGENT_REF_MAX_BYTES, TASK_ASSERTION_DOMAIN_PREFIX, TASK_ASSERTION_SCHEMA_ID, TASK_ASSERTION_TOOLSET_ID_MAX_LENGTH, TASK_ASSERTION_VERSION, TENANT_ID_MAX_LENGTH, TENANT_KEY_SEPARATOR, TRUTH_RECORD_KINDS, TaskAssertionAgentRefSchema, TaskAssertionClaimsSchema, TaskAssertionEnvelopeV1Schema, agentMemoryIntentOperationDigest, assertCanonicalTimestamp, assertCapability, assertCollectExpiredInput, assertProviderProvisioningConfigDigest, authenticateDeviceAssertion, authenticateTaskAssertion, canonicalizeJson, canonicalizeJsonBytes, checkSkillPackEntry, checkSkillPackFileContent, checkSkillPackManifest, contentHash, createInMemoryCoreCompositionWithClock, createInMemoryCoreStores, createMutableClock, deriveSealingKeyId, deviceAssertionCanonicalClaims, deviceAssertionCanonicalJson, deviceAssertionSigningInput, deviceProofCanonicalClaims, deviceProofCanonicalJson, deviceProofSigningInput, hasCapability, isCanonicalTimestamp, isContentHash, isControlPlanePrincipal, isCoreConflictError, isCoreError, isDevicePrincipal, isLegalBoardTransition, isLegalObjectTransition, isSkillPackPathSafe, isTenantId, nonceSigningBytes, objectKeyPrefix, openProviderProvisioningSecret, parseCapabilityDeclaration, parseDeviceAssertionEnvelope, parseDeviceProofEnvelope, parseProviderProvisioningRequest, parseSkillFrontmatter, parseSkillPackManifest, parseTaskAssertionEnvelope, principalTenant, providerProvisioningConfigDigest, providerProvisioningRequestDigest, providerSecretAadBytes, sealProviderProvisioningRequest, sealingKeyClaimCanonicalBytes, skillPackContentHashInput, taskAssertionCanonicalClaims, taskAssertionCanonicalJson, taskAssertionSigningInput, tenantId, tenantKey, tenantObjectKey, verifyDeviceAssertion, verifyTaskAssertion };
2827
3041
  //# sourceMappingURL=index.js.map
2828
3042
  //# sourceMappingURL=index.js.map