@tinycloud/sdk-services 3.2.6-beta.4 → 3.3.0-beta.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/{BaseService-1Ug6y7DC.d.cts → BaseService-BhfKzKke.d.cts} +5 -1
  2. package/dist/{BaseService-Bn07Vudy.d.ts → BaseService-x4AFZiEa.d.ts} +5 -1
  3. package/dist/encryption/index.cjs +25 -4
  4. package/dist/encryption/index.cjs.map +1 -1
  5. package/dist/encryption/index.d.cts +2 -2
  6. package/dist/encryption/index.d.ts +2 -2
  7. package/dist/encryption/index.js +25 -4
  8. package/dist/encryption/index.js.map +1 -1
  9. package/dist/index.cjs +959 -808
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +114 -7
  12. package/dist/index.d.ts +114 -7
  13. package/dist/index.js +958 -808
  14. package/dist/index.js.map +1 -1
  15. package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
  16. package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
  17. package/dist/kv/index.cjs +136 -99
  18. package/dist/kv/index.cjs.map +1 -1
  19. package/dist/kv/index.d.cts +4 -5
  20. package/dist/kv/index.d.ts +4 -5
  21. package/dist/kv/index.js +136 -99
  22. package/dist/kv/index.js.map +1 -1
  23. package/dist/kv/replication/grantPolicy.cjs +82 -0
  24. package/dist/kv/replication/grantPolicy.cjs.map +1 -0
  25. package/dist/kv/replication/grantPolicy.d.cts +19 -0
  26. package/dist/kv/replication/grantPolicy.d.ts +19 -0
  27. package/dist/kv/replication/grantPolicy.js +50 -0
  28. package/dist/kv/replication/grantPolicy.js.map +1 -0
  29. package/dist/kv/replication/index.cjs +576 -132
  30. package/dist/kv/replication/index.cjs.map +1 -1
  31. package/dist/kv/replication/index.d.cts +3 -3
  32. package/dist/kv/replication/index.d.ts +3 -3
  33. package/dist/kv/replication/index.js +576 -132
  34. package/dist/kv/replication/index.js.map +1 -1
  35. package/dist/{scope-BnyVPZ5E.d.ts → scope-9U4gVNFN.d.ts} +312 -7
  36. package/dist/{scope-CQ5kmHlt.d.cts → scope-BzdcrOZG.d.cts} +312 -7
  37. package/dist/sql/index.cjs +98 -50
  38. package/dist/sql/index.cjs.map +1 -1
  39. package/dist/sql/index.d.cts +2 -2
  40. package/dist/sql/index.d.ts +2 -2
  41. package/dist/sql/index.js +98 -50
  42. package/dist/sql/index.js.map +1 -1
  43. package/dist/{types-gmdUiorz.d.cts → types-eVS1Whuf.d.cts} +29 -3
  44. package/dist/{types-gmdUiorz.d.ts → types-eVS1Whuf.d.ts} +29 -3
  45. package/package.json +16 -2
@@ -79,6 +79,14 @@ var ErrorCodes = {
79
79
  ABORTED: "ABORTED",
80
80
  INVALID_INPUT: "INVALID_INPUT",
81
81
  PERMISSION_DENIED: "PERMISSION_DENIED",
82
+ // Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
83
+ // retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
84
+ /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */
85
+ NODE_BUSY: "NODE_BUSY",
86
+ /** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
87
+ REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
88
+ /** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */
89
+ OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN",
82
90
  // KV-specific errors
83
91
  KV_NOT_FOUND: "KV_NOT_FOUND",
84
92
  KV_WRITE_FAILED: "KV_WRITE_FAILED",
@@ -114,11 +122,12 @@ var ErrorCodes = {
114
122
  DUCKDB_READONLY_VIOLATION: "DUCKDB_READONLY_VIOLATION"
115
123
  };
116
124
  var defaultRetryPolicy = {
117
- maxAttempts: 3,
125
+ maxAttempts: 4,
118
126
  backoff: "exponential",
119
- baseDelayMs: 1e3,
120
- maxDelayMs: 1e4,
121
- retryableErrors: [ErrorCodes.NETWORK_ERROR, ErrorCodes.TIMEOUT]
127
+ baseDelayMs: 250,
128
+ maxDelayMs: 4e3,
129
+ retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],
130
+ maxTotalMs: 12e3
122
131
  };
123
132
  function ok(data) {
124
133
  return { ok: true, data };
@@ -372,6 +381,35 @@ function emitEvent(emit, scheduler, event) {
372
381
  }
373
382
  }
374
383
 
384
+ // src/kv/replication/grantPolicyConstants.ts
385
+ var HOUR_MS = 60 * 60 * 1e3;
386
+ var DAY_MS = 24 * HOUR_MS;
387
+ var DEFAULT_GRANT_TTL_MS = DAY_MS;
388
+ var MAX_GRANT_TTL_MS = 7 * DAY_MS;
389
+ var CONSENT_WINDOW_MS = 7 * DAY_MS;
390
+ var RENEW_BELOW_FRACTION = 0.25;
391
+
392
+ // src/kv/replication/grantPolicy.ts
393
+ var EXPIRED = Number.NEGATIVE_INFINITY;
394
+ var consentDeadlineMs = (anchor) => Number.isFinite(anchor.at) ? anchor.at + CONSENT_WINDOW_MS : EXPIRED;
395
+ var clampConsentAnchor = (anchor, now) => {
396
+ if (anchor === null || !Number.isFinite(anchor.at) || !Number.isFinite(now)) return null;
397
+ return anchor.at > now ? { at: now, source: anchor.source } : anchor;
398
+ };
399
+ var mintRemainingMs = ({ now, grantTtlMs, consentDeadline }) => {
400
+ if (!Number.isFinite(now) || !Number.isFinite(grantTtlMs) || !Number.isFinite(consentDeadline)) return 0;
401
+ return Math.min(Math.min(grantTtlMs, MAX_GRANT_TTL_MS), consentDeadline - now);
402
+ };
403
+ var needsSilentRenewal = ({ now, installedAt, effectiveExpiresAt, consentDeadline }) => {
404
+ if (!Number.isFinite(now) || !Number.isFinite(installedAt) || !Number.isFinite(effectiveExpiresAt) || !Number.isFinite(consentDeadline)) {
405
+ return false;
406
+ }
407
+ if (!(now < consentDeadline)) return false;
408
+ const remaining = effectiveExpiresAt - now;
409
+ const lifetime = effectiveExpiresAt - installedAt;
410
+ return remaining < lifetime * RENEW_BELOW_FRACTION;
411
+ };
412
+
375
413
  // src/kv/replication/controller.ts
376
414
  var CLOSE_TIMEOUT_MS = 3e3;
377
415
  var STATUS_REFRESH_TIMEOUT_MS = CLOSE_TIMEOUT_MS;
@@ -399,6 +437,128 @@ function createKVReplication(deps) {
399
437
  let previous = deps.previous;
400
438
  let pinnedStateReported = false;
401
439
  const inProcessProof = /* @__PURE__ */ new Set();
440
+ let anchorUnavailable = false;
441
+ let consentAnchor = null;
442
+ try {
443
+ consentAnchor = clampConsentAnchor(authority.consentAnchor?.() ?? null, scheduler.now());
444
+ if (consentAnchor?.source === "first_install") consentAnchor = null;
445
+ } catch {
446
+ anchorUnavailable = true;
447
+ }
448
+ const grantPolicy = { grantTtlMs: options.grantTtlMs ?? DEFAULT_GRANT_TTL_MS, consentAnchor };
449
+ const quota = options.maxBytes === void 0 && options.minFreeBytes === void 0 ? void 0 : { maxBytes: options.maxBytes, minFreeBytes: options.minFreeBytes };
450
+ const policyDeadline = consentAnchor === null ? null : consentDeadlineMs(consentAnchor);
451
+ let scheduledConsentDeadline = null;
452
+ let cancelConsentTimer;
453
+ function durableDeadline() {
454
+ if (storage.kind !== "indexeddb" || !deps.stamp) return null;
455
+ try {
456
+ const deadline = storage.lifecycle?.consentDeadline(deps.stamp);
457
+ return typeof deadline === "number" && Number.isFinite(deadline) ? deadline : null;
458
+ } catch {
459
+ return null;
460
+ }
461
+ }
462
+ function mintDeadline() {
463
+ if (policyDeadline === null) return null;
464
+ if (storage.kind !== "indexeddb") return policyDeadline;
465
+ const durable = durableDeadline();
466
+ return durable === null ? null : Math.min(policyDeadline, durable);
467
+ }
468
+ function scheduleConsentDeadline() {
469
+ const deadline = durableDeadline();
470
+ if (deadline === null || consentDenyStarted || isClosed) return;
471
+ const now = scheduler.now();
472
+ if (!Number.isFinite(now)) return;
473
+ if (scheduledConsentDeadline === deadline && cancelConsentTimer) return;
474
+ scheduledConsentDeadline = deadline;
475
+ cancelConsentTimer?.();
476
+ cancelConsentTimer = scheduler.setTimeout(() => {
477
+ cancelConsentTimer = void 0;
478
+ checkDelivery();
479
+ }, Math.max(0, deadline - now));
480
+ }
481
+ const reportedDeadline = /* @__PURE__ */ new Set();
482
+ let consentDenyStarted = false;
483
+ function checkDelivery() {
484
+ const gate = storage.lifecycle;
485
+ if (!gate) return storage.kind === "indexeddb" || anchorUnavailable || deps.stamp !== void 0 ? { status: "gated", gate: "gate_unavailable" } : { status: "allow" };
486
+ if (!deps.stamp) return { status: "gated", gate: "not_established" };
487
+ if (consentDenyStarted) return { status: "gated", gate: "stamp_stale" };
488
+ try {
489
+ const decision = gate.checkDelivery(deps.stamp);
490
+ if (decision.status === "consent_expired") startConsentDenial();
491
+ if (decision.status !== "allow") return decision;
492
+ if (storage.kind === "indexeddb") {
493
+ const deadline = durableDeadline();
494
+ if (deadline === null) return { status: "gated", gate: "gate_unavailable" };
495
+ const now = scheduler.now();
496
+ if (!Number.isFinite(now)) return { status: "gated", gate: "gate_unavailable" };
497
+ if (now >= deadline) {
498
+ startConsentDenial();
499
+ return { status: "consent_expired", code: "CONSENT_EXPIRED" };
500
+ }
501
+ }
502
+ return anchorUnavailable ? { status: "gated", gate: "gate_unavailable" } : decision;
503
+ } catch {
504
+ return { status: "gated", gate: "gate_unavailable" };
505
+ }
506
+ }
507
+ function markConsentExpired() {
508
+ if (consentDenyStarted || isClosed || purgeStarted) return false;
509
+ cancelConsentTimer?.();
510
+ cancelConsentTimer = void 0;
511
+ consentDenyStarted = true;
512
+ for (const state of replicas.values()) {
513
+ state.renewalTimer?.();
514
+ state.renewalTimer = void 0;
515
+ state.mintAbort?.abort();
516
+ const id = state.handle?.replicaId;
517
+ if (id && !reportedDeadline.has(id)) {
518
+ reportedDeadline.add(id);
519
+ emit({ type: "replication.grant", replicaId: id, action: "consent_deadline_reached", code: "CONSENT_EXPIRED" });
520
+ }
521
+ }
522
+ return true;
523
+ }
524
+ function observeConsentExpired(stamp) {
525
+ if (!deps.stamp || stamp.incarnation !== deps.stamp.incarnation || stamp.lifecycleEpoch !== deps.stamp.lifecycleEpoch) return;
526
+ if (markConsentExpired()) emit({ type: "replication.lifecycle", action: "denied", reason: "consent_expired" });
527
+ }
528
+ function startConsentDenial() {
529
+ const gate = storage.lifecycle;
530
+ if (!gate || !markConsentExpired()) return;
531
+ void gate.deny({ reason: "consent_expired", principal: identity.principal }).then(
532
+ (outcome) => emit({ type: "replication.lifecycle", action: outcome.status === "denied" ? "denied" : outcome.status === "stale" ? "deny_stale" : outcome.status === "deleted" ? "database_deleted" : outcome.status === "marker" ? "marker_set" : "unguarded", reason: "consent_expired" }),
533
+ () => emit({ type: "replication.lifecycle", action: "deny_failed", reason: "consent_expired" })
534
+ );
535
+ }
536
+ function deliveryError(decision) {
537
+ return Object.assign(new Error(decision.status === "consent_expired" ? "Replica consent expired" : "Replica session is gated"), {
538
+ code: decision.status === "consent_expired" ? decision.code : "REPLICA_GATED",
539
+ ...decision.status === "gated" ? { gate: decision.gate } : {}
540
+ });
541
+ }
542
+ function deliveryReason(code) {
543
+ return code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? "grant_expired" : code === "REPLICA_GATED" ? "replica_gated" : "replica_error";
544
+ }
545
+ function reportGrantExpiry(prefix) {
546
+ const state = replicas.get(prefix);
547
+ if (!state) return;
548
+ state.reason = "grant_expired";
549
+ state.lastError = "GRANT_EXPIRED";
550
+ const replicaId = state.handle?.replicaId;
551
+ if (!replicaId || state.reportedExpired) return;
552
+ state.reportedExpired = true;
553
+ emit({ type: "replication.grant", replicaId, action: "expired", code: "GRANT_EXPIRED" });
554
+ }
555
+ function reportGrantCap(state, handle, grant) {
556
+ if (!grant?.unconstrained || !handle.replicaId || typeof grant.effectiveExpiresAt !== "number" || !Number.isFinite(grant.effectiveExpiresAt) || !(grant.expiresAt === null || grant.effectiveExpiresAt < grant.expiresAt)) return;
557
+ const capKey = `${grant.cid}:${grant.effectiveExpiresAt}`;
558
+ if (state.reportedCappedCid === capKey) return;
559
+ state.reportedCappedCid = capKey;
560
+ emit({ type: "replication.grant", replicaId: handle.replicaId, action: "capped", effectiveExpiresAt: nowDate(grant.effectiveExpiresAt) });
561
+ }
402
562
  const configuredPrefix = (path) => options.prefixes.find((prefix) => kvPrefixCovers(prefix, path));
403
563
  function recordRevocation(prefix, error) {
404
564
  if (errorCode(error) !== "GRANT_REVOKED") return;
@@ -422,7 +582,26 @@ function createKVReplication(deps) {
422
582
  markRootUnsupported(prefix);
423
583
  return Object.assign(new Error("This node doesn't support whole-space sync yet."), { code: "REPLICATION_ROOT_UNSUPPORTED" });
424
584
  }
585
+ function canReuseInstalled(grant, sessionOnly) {
586
+ if (!grant?.unconstrained) return false;
587
+ if (!deps.stamp) return true;
588
+ if (!sessionOnly && consentAnchor === null) return false;
589
+ const now = scheduler.now();
590
+ const deadline = storage.kind === "indexeddb" ? durableDeadline() : policyDeadline ?? grant.consentDeadline;
591
+ return grant.state === "active" && typeof grant.installedAt === "number" && Number.isFinite(grant.installedAt) && typeof grant.effectiveExpiresAt === "number" && Number.isFinite(grant.effectiveExpiresAt) && typeof grant.consentDeadline === "number" && Number.isFinite(grant.consentDeadline) && typeof deadline === "number" && Number.isFinite(deadline) && Number.isFinite(now) && now >= grant.installedAt && (grant.expiresAt === null || typeof grant.expiresAt === "number" && Number.isFinite(grant.expiresAt) && now < grant.expiresAt) && now < Math.min(grant.effectiveExpiresAt, grant.consentDeadline, deadline);
592
+ }
593
+ function sessionRefusal(prefix) {
594
+ try {
595
+ if (authority.sessionOnly !== true) return void 0;
596
+ const grant = authority.sessionGrant(prefix);
597
+ return "refused" in grant ? grant.refused : void 0;
598
+ } catch {
599
+ return "NOT_COVERED";
600
+ }
601
+ }
425
602
  async function open(prefix) {
603
+ const delivery = checkDelivery();
604
+ if (delivery.status !== "allow") throw deliveryError(delivery);
426
605
  if (isClosed || purgeStarted) throw Object.assign(new Error("Replication controller is closed"), { code: "REPLICA_CLOSED" });
427
606
  let state = replicas.get(prefix);
428
607
  if (state?.handle) return state.handle;
@@ -443,46 +622,99 @@ function createKVReplication(deps) {
443
622
  await previous;
444
623
  previous = void 0;
445
624
  }
625
+ const delivery2 = checkDelivery();
626
+ if (delivery2.status !== "allow") throw deliveryError(delivery2);
627
+ const sessionOnly = authority.sessionOnly === true;
446
628
  const sessionGrant = authority.sessionGrant(prefix);
447
629
  const delegated = "refused" in sessionGrant ? void 0 : sessionGrant;
448
- const plan = authority.sessionOnly ? void 0 : authority.plan(prefix);
630
+ const plan = sessionOnly ? void 0 : authority.plan(prefix);
449
631
  let refusalCode;
450
632
  if ("refused" in sessionGrant) refusalCode = sessionGrant.refused;
451
633
  if (plan && "refused" in plan) refusalCode ?? (refusalCode = plan.refused);
634
+ if (deps.stamp && consentAnchor === null && !sessionOnly) refusalCode = "REPLICA_GATED";
452
635
  let device;
453
636
  if (delegated) {
454
637
  device = delegated.device;
455
638
  state.strategy = "session";
456
639
  }
457
- const spec = { identity, space: session.space, prefix, allowSecrets: options.allowSecrets, ...device ? { device } : {}, ...deps.fetch ? { fetch: deps.fetch } : {} };
640
+ const spec = {
641
+ identity,
642
+ space: session.space,
643
+ prefix,
644
+ allowSecrets: options.allowSecrets,
645
+ grantPolicy,
646
+ ...quota ? { quota } : {},
647
+ ...deps.stamp ? { stamp: deps.stamp } : {},
648
+ ...device ? { device } : {},
649
+ ...deps.fetch ? { fetch: deps.fetch } : {},
650
+ onPeerCommit: (info) => {
651
+ if (isClosed || purgeStarted || state.handle === void 0) return;
652
+ emit({
653
+ type: "replication.sync",
654
+ space: session.space,
655
+ replica: prefix,
656
+ trigger: "peer",
657
+ origin: "peer",
658
+ outcome: "ok",
659
+ durationMs: 0,
660
+ lagMs: null,
661
+ ...info.changes === void 0 ? {} : { changes: info.changes },
662
+ ...info.coverage === void 0 ? {} : { coverage: info.coverage }
663
+ });
664
+ }
665
+ };
458
666
  if (prefix === "/" && deps.nodeFeatures !== void 0 && !deps.nodeFeatures.includes("kv-sync-root")) {
459
667
  throw unsupportedRoot(prefix);
460
668
  }
461
669
  const handle = await storage.open(spec);
462
670
  state.handle = handle;
671
+ const afterOpen = checkDelivery();
672
+ if (afterOpen.status !== "allow") throw deliveryError(afterOpen);
463
673
  emit({ type: "replication.state", space: session.space, replica: prefix, state: "opened" });
464
- const runtimePlan = !authority.sessionOnly && plan && !("refused" in plan) ? plan : void 0;
465
- let installed = runtimePlan ? await handle.grant() : void 0;
466
- if (delegated) installed = await handle.installGrant(delegated.ucan);
467
- else if (runtimePlan) {
674
+ const runtimePlan = !sessionOnly && plan && !("refused" in plan) ? plan : void 0;
675
+ let installed = runtimePlan || delegated ? await handle.grant() : void 0;
676
+ if (installed && !canReuseInstalled(installed, sessionOnly)) {
677
+ if (installed.unconstrained) refusalCode = consentAnchor === null && !sessionOnly ? "REPLICA_GATED" : "GRANT_EXPIRED";
678
+ installed = void 0;
679
+ }
680
+ let mintedExpiryMs;
681
+ if (delegated && consentAnchor !== null) {
682
+ const beforeInstall = checkDelivery();
683
+ if (beforeInstall.status !== "allow") throw deliveryError(beforeInstall);
684
+ installed = await handle.installGrant(delegated.ucan);
685
+ } else if (runtimePlan) {
468
686
  if (!installed || !installed.unconstrained || installed.parentCid !== runtimePlan.parentCid || installed.expiresAt !== null && runtimePlan.expiresAt > installed.expiresAt + 3e5) {
469
- try {
470
- const mintAbort = new AbortController();
471
- state.mintAbort = mintAbort;
472
- const minted = await authority.mint(handle.deviceDid, prefix, mintAbort.signal);
473
- installed = await handle.installGrant(minted.ucan);
474
- state.strategy = "minted";
475
- } catch (error) {
476
- if (!installed?.unconstrained) throw error;
477
- } finally {
478
- state.mintAbort = void 0;
687
+ const deadline = mintDeadline();
688
+ const expiryMs = mintRemainingMs({ now: scheduler.now(), grantTtlMs: grantPolicy.grantTtlMs, consentDeadline: deadline ?? Number.NaN });
689
+ if (expiryMs > 0) {
690
+ try {
691
+ const mintAbort = new AbortController();
692
+ state.mintAbort = mintAbort;
693
+ const minted = await authority.mint(handle.deviceDid, prefix, mintAbort.signal, { expiryMs });
694
+ const beforeInstall = checkDelivery();
695
+ if (beforeInstall.status !== "allow") throw deliveryError(beforeInstall);
696
+ installed = await handle.installGrant(minted.ucan);
697
+ state.strategy = "minted";
698
+ mintedExpiryMs = expiryMs;
699
+ } catch (error) {
700
+ if (errorCode(error) === "CONSENT_EXPIRED" || errorCode(error) === "REPLICA_GATED" || !installed?.unconstrained) throw error;
701
+ } finally {
702
+ state.mintAbort = void 0;
703
+ }
479
704
  }
480
705
  }
481
706
  if (installed?.unconstrained) {
482
707
  state.strategy ?? (state.strategy = "installed");
483
708
  state.reason = void 0;
484
709
  }
710
+ } else if ((delegated || sessionOnly) && installed?.unconstrained) {
711
+ state.strategy = "installed";
485
712
  }
713
+ const beforeGrantEvent = checkDelivery();
714
+ if (beforeGrantEvent.status !== "allow") throw deliveryError(beforeGrantEvent);
715
+ reportGrantCap(state, handle, installed);
716
+ if (mintedExpiryMs !== void 0) state.reportedExpired = false;
717
+ if (mintedExpiryMs !== void 0 && handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "minted", expiryMs: mintedExpiryMs });
486
718
  if (!installed?.unconstrained) {
487
719
  state.reason = "grant_missing";
488
720
  state.lastError = refusalCode ?? "SESSION_LACKS_SYNC";
@@ -492,6 +724,7 @@ function createKVReplication(deps) {
492
724
  await handle.close();
493
725
  throw Object.assign(new Error("Replication controller is closed"), { code: "REPLICA_CLOSED" });
494
726
  }
727
+ if (installed?.unconstrained) await maybeRenew(prefix, handle, installed, runtimePlan);
495
728
  if (mode === "background" && state.reason !== "grant_missing") {
496
729
  void syncPrefix(prefix, "start").catch(() => void 0);
497
730
  schedule(prefix);
@@ -502,12 +735,14 @@ function createKVReplication(deps) {
502
735
  return await state.opening;
503
736
  } catch (error) {
504
737
  const code = errorCode(error);
738
+ if (code === "CONSENT_EXPIRED") startConsentDenial();
505
739
  recordRevocation(prefix, error);
506
- if (code !== "GRANT_REVOKED") state.reason = "replica_unavailable";
740
+ if (code === "REPLICA_GATED" || code === "CONSENT_EXPIRED") state.reason = deliveryReason(code);
741
+ else if (code !== "GRANT_REVOKED") state.reason = "replica_unavailable";
507
742
  state.lastError = code;
508
743
  state.failures = (state.failures ?? 0) + 1;
509
744
  if (code === "RUNTIME_UNSUPPORTED") state.unsupported = true;
510
- else if (code !== "REPLICA_CLOSED" && code !== "REPLICATION_ROOT_UNSUPPORTED") state.retryAt = scheduler.now() + Math.min(3e5, 5e3 * 2 ** Math.min(state.failures - 1, 6));
745
+ else if (code !== "REPLICA_CLOSED" && code !== "REPLICATION_ROOT_UNSUPPORTED" && code !== "REPLICA_GATED" && code !== "CONSENT_EXPIRED") state.retryAt = scheduler.now() + Math.min(3e5, 5e3 * 2 ** Math.min(state.failures - 1, 6));
511
746
  state.handle?.close().catch(() => void 0);
512
747
  state.handle = void 0;
513
748
  throw error;
@@ -515,6 +750,96 @@ function createKVReplication(deps) {
515
750
  state.opening = void 0;
516
751
  }
517
752
  }
753
+ function scheduleRenewal(prefix, handle, grant, plan) {
754
+ const state = replicas.get(prefix);
755
+ state?.renewalTimer?.();
756
+ if (state) state.renewalTimer = void 0;
757
+ if (!state || isClosed || purgeStarted || plan.path !== "session" || !authority.canRenew) return;
758
+ const { installedAt, effectiveExpiresAt } = grant;
759
+ const deadline = mintDeadline();
760
+ if (typeof installedAt !== "number" || !Number.isFinite(installedAt) || typeof effectiveExpiresAt !== "number" || !Number.isFinite(effectiveExpiresAt) || !(effectiveExpiresAt > installedAt) || deadline === null) return;
761
+ const threshold = installedAt + (effectiveExpiresAt - installedAt) * (1 - RENEW_BELOW_FRACTION);
762
+ const now = scheduler.now();
763
+ if (!Number.isFinite(now) || !(threshold < deadline) || now > threshold) return;
764
+ state.renewalTimer = scheduler.setTimeout(() => {
765
+ state.renewalTimer = void 0;
766
+ void maybeRenew(prefix, handle).catch(() => void 0);
767
+ }, Math.max(0, Math.floor(threshold) + 1 - now));
768
+ }
769
+ async function maybeRenew(prefix, handle, knownGrant, knownPlan) {
770
+ const state = replicas.get(prefix);
771
+ if (!state || state.handle !== handle || isClosed || purgeStarted || !authority.canRenew) return;
772
+ if (state.renewing) return state.renewing;
773
+ const decision = checkDelivery();
774
+ if (decision.status !== "allow") return;
775
+ const grant = knownGrant ?? await handle.grant();
776
+ if (!grant?.unconstrained) return;
777
+ const { installedAt, effectiveExpiresAt } = grant;
778
+ const deadline = mintDeadline();
779
+ if (typeof installedAt !== "number" || !Number.isFinite(installedAt) || typeof effectiveExpiresAt !== "number" || !Number.isFinite(effectiveExpiresAt) || deadline === null) return;
780
+ const now = scheduler.now();
781
+ if (!needsSilentRenewal({ now, installedAt, effectiveExpiresAt, consentDeadline: deadline })) {
782
+ if (knownPlan) scheduleRenewal(prefix, handle, grant, knownPlan);
783
+ return;
784
+ }
785
+ let plan = knownPlan;
786
+ try {
787
+ if (!plan) {
788
+ const selected = authority.plan(prefix);
789
+ if ("refused" in selected) {
790
+ if (handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewal_refused", code: selected.refused });
791
+ return;
792
+ }
793
+ plan = selected;
794
+ }
795
+ } catch {
796
+ if (handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewal_deferred" });
797
+ return;
798
+ }
799
+ if (plan?.path !== "session") {
800
+ if (handle.replicaId && state.reportedRenewalUnqualifiedCid !== grant.cid) {
801
+ state.reportedRenewalUnqualifiedCid = grant.cid;
802
+ emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewal_deferred" });
803
+ }
804
+ return;
805
+ }
806
+ const expiryMs = mintRemainingMs({ now, grantTtlMs: grantPolicy.grantTtlMs, consentDeadline: deadline });
807
+ if (!(expiryMs > 0)) return;
808
+ state.renewalTimer?.();
809
+ state.renewalTimer = void 0;
810
+ const mintAbort = new AbortController();
811
+ state.mintAbort = mintAbort;
812
+ const job = Promise.resolve().then(async () => {
813
+ const minted = await authority.mint(handle.deviceDid, prefix, mintAbort.signal, { expiryMs, renewalOnly: true });
814
+ const beforeInstall = checkDelivery();
815
+ if (beforeInstall.status !== "allow") throw deliveryError(beforeInstall);
816
+ const installed = await handle.installGrant(minted.ucan);
817
+ const beforeEvent = checkDelivery();
818
+ if (beforeEvent.status !== "allow") throw deliveryError(beforeEvent);
819
+ reportGrantCap(state, handle, installed);
820
+ state.reportedExpired = false;
821
+ if (state.reason === "grant_expired") state.reason = void 0;
822
+ if (state.lastError === "GRANT_EXPIRED") state.lastError = void 0;
823
+ if (handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewed", expiryMs });
824
+ if (typeof installed.effectiveExpiresAt === "number" && installed.effectiveExpiresAt > effectiveExpiresAt) {
825
+ scheduleRenewal(prefix, handle, installed, plan);
826
+ }
827
+ });
828
+ state.renewing = job;
829
+ try {
830
+ await job;
831
+ } catch (error) {
832
+ const code = errorCode(error);
833
+ if (code === "CONSENT_EXPIRED" || code === "REPLICA_GATED") throw error;
834
+ if (!isClosed && handle.replicaId) {
835
+ const refused = typeof error === "object" && error !== null && "refused" in error && typeof error.refused === "string" ? error.refused : null;
836
+ emit({ type: "replication.grant", replicaId: handle.replicaId, action: refused ? "renewal_refused" : "renewal_deferred", code: refused ?? code });
837
+ }
838
+ } finally {
839
+ state.renewing = void 0;
840
+ if (state.mintAbort === mintAbort) state.mintAbort = void 0;
841
+ }
842
+ }
518
843
  function schedule(prefix) {
519
844
  const state = replicas.get(prefix);
520
845
  if (!state || mode !== "background" || isClosed || purgeStarted) return;
@@ -527,6 +852,8 @@ function createKVReplication(deps) {
527
852
  if (prefix === "/" && deps.nodeFeatures !== void 0 && !deps.nodeFeatures.includes("kv-sync-root")) {
528
853
  throw unsupportedRoot(prefix);
529
854
  }
855
+ const initialRefusal = sessionRefusal(prefix);
856
+ if (initialRefusal) throw Object.assign(new Error("Current session does not cover replica sync"), { code: initialRefusal });
530
857
  const handle = await open(prefix);
531
858
  const state = replicas.get(prefix);
532
859
  if (state.sync) return state.sync;
@@ -544,22 +871,36 @@ function createKVReplication(deps) {
544
871
  job = (async () => {
545
872
  try {
546
873
  const syncStartEpoch = (await pending.read()).committedEpoch;
874
+ const beforeSync = checkDelivery();
875
+ if (beforeSync.status !== "allow") throw deliveryError(beforeSync);
876
+ await maybeRenew(prefix, handle);
877
+ const afterRenewal = checkDelivery();
878
+ if (afterRenewal.status !== "allow") throw deliveryError(afterRenewal);
879
+ const refusal = sessionRefusal(prefix);
880
+ if (refusal) throw Object.assign(new Error("Current session does not cover replica sync"), { code: refusal });
547
881
  const result = await handle.sync({ signal: abort.signal, syncStartEpoch });
882
+ const afterSyncDelivery = checkDelivery();
883
+ if (afterSyncDelivery.status !== "allow") throw deliveryError(afterSyncDelivery);
548
884
  if (result.status === "busy") {
549
885
  emit({ type: "replication.sync", space: session.space, replica: prefix, trigger, outcome: "busy", durationMs: scheduler.now() - started, lagMs: null });
550
886
  return result;
551
887
  }
552
888
  inProcessProof.add(prefix);
553
889
  const snapshot = await pending.read();
890
+ const beforePending = checkDelivery();
891
+ if (beforePending.status !== "allow") throw deliveryError(beforePending);
554
892
  const clearedCandidates = snapshot.records.some((r) => r.state === "committed" && r.epoch !== null && r.epoch <= syncStartEpoch && kvPrefixCovers(prefix, r.key));
555
893
  const cleared = clearedCandidates ? await pending.update((s) => afterSync(s, prefix, syncStartEpoch)) : 0;
894
+ const beforeResult = checkDelivery();
895
+ if (beforeResult.status !== "allow") throw deliveryError(beforeResult);
556
896
  emit({ type: "replication.sync", space: session.space, replica: prefix, trigger, outcome: "ok", durationMs: scheduler.now() - started, lagMs: null, pendingCleared: cleared, pages: result.pages, changes: result.changes, deleted: result.deleted, fetched: result.fetched, contentMissing: result.contentMissing, coverage: result.coverage });
557
897
  return result;
558
898
  } catch (error) {
559
899
  const code = errorCode(error);
560
900
  state.lastError = code;
561
901
  recordRevocation(prefix, error);
562
- emit({ type: "replication.sync", space: session.space, replica: prefix, trigger, outcome: abort.signal.aborted ? "aborted" : "error", class: code === "NETWORK_ERROR" || code === "TIMEOUT" ? "offline" : "node", code, durationMs: scheduler.now() - started, lagMs: null });
902
+ if (code === "CONSENT_EXPIRED") startConsentDenial();
903
+ emit({ type: "replication.sync", space: session.space, replica: prefix, trigger, outcome: abort.signal.aborted ? "aborted" : "error", class: code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" ? "lifecycle" : code === "NETWORK_ERROR" || code === "TIMEOUT" ? "offline" : "node", code, durationMs: scheduler.now() - started, lagMs: null });
563
904
  throw error;
564
905
  } finally {
565
906
  signal?.removeEventListener("abort", abortFromCaller);
@@ -715,8 +1056,19 @@ function createKVReplication(deps) {
715
1056
  return { status: currentStatus, syncedBeforeRead: false };
716
1057
  }
717
1058
  function readEvent(op, path, prefix, source, reason, outcome, started, extra = {}) {
1059
+ if (prefix !== null && reason === "grant_expired" && extra.code !== "CONSENT_EXPIRED") reportGrantExpiry(prefix);
718
1060
  emit({ type: "replication.read", op, space: session.space, key: path, replica: prefix, source, reason, outcome, latencyMs: scheduler.now() - started, stalenessMs: null, coverage: null, authority: null, ...extra });
719
1061
  }
1062
+ async function networkRead(network, op, path, prefix, reason, started, extra = {}) {
1063
+ let outcome = "error";
1064
+ try {
1065
+ const result = await network();
1066
+ outcome = result.ok ? "found" : op === "get" && (reason === "pending_write" || reason === "not_covered") && result.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error";
1067
+ return result;
1068
+ } finally {
1069
+ readEvent(op, path, prefix, "network", reason, outcome, started, extra);
1070
+ }
1071
+ }
720
1072
  async function verifyGet(r, local, reason, started, extra) {
721
1073
  const prefix = configuredPrefix(r.path) ?? null;
722
1074
  const localFound = local.ok;
@@ -728,6 +1080,16 @@ function createKVReplication(deps) {
728
1080
  readEvent("get", r.path, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
729
1081
  return abortedResult(r.signal);
730
1082
  }
1083
+ const delivery = checkDelivery();
1084
+ if (delivery.status !== "allow") {
1085
+ readEvent("get", r.path, prefix, "network", delivery.status === "consent_expired" ? "grant_expired" : "replica_gated", network.ok ? "found" : "error", started, { code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" });
1086
+ return network;
1087
+ }
1088
+ const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
1089
+ if (refusal) {
1090
+ readEvent("get", r.path, prefix, "network", "grant_missing", network.ok ? "found" : "error", started, { code: refusal });
1091
+ return network;
1092
+ }
731
1093
  if (!network.ok && network.error.code !== ErrorCodes.KV_NOT_FOUND) {
732
1094
  readEvent("get", r.path, prefix, "replica", reason, outcome, started, { ...extra, code: network.error.code, verify: "error" });
733
1095
  return local;
@@ -758,6 +1120,16 @@ function createKVReplication(deps) {
758
1120
  readEvent("get", r.path, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
759
1121
  return abortedResult(r.signal);
760
1122
  }
1123
+ const delivery = checkDelivery();
1124
+ if (delivery.status !== "allow") {
1125
+ readEvent("get", r.path, prefix, "network", delivery.status === "consent_expired" ? "grant_expired" : "replica_gated", "error", started, { code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" });
1126
+ return err(wrapError("kv", error));
1127
+ }
1128
+ const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
1129
+ if (refusal) {
1130
+ readEvent("get", r.path, prefix, "network", "grant_missing", "error", started, { code: refusal });
1131
+ return err(wrapError("kv", error));
1132
+ }
761
1133
  readEvent("get", r.path, prefix, "replica", reason, outcome, started, { ...extra, code: errorCode(error), verify: "error" });
762
1134
  return local;
763
1135
  }
@@ -770,6 +1142,16 @@ function createKVReplication(deps) {
770
1142
  readEvent("list", r.listPath, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
771
1143
  return abortedResult(r.signal);
772
1144
  }
1145
+ const delivery = checkDelivery();
1146
+ if (delivery.status !== "allow") {
1147
+ readEvent("list", r.listPath, prefix, "network", delivery.status === "consent_expired" ? "grant_expired" : "replica_gated", network.ok ? "found" : "error", started, { code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" });
1148
+ return network;
1149
+ }
1150
+ const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
1151
+ if (refusal) {
1152
+ readEvent("list", r.listPath, prefix, "network", "grant_missing", network.ok ? "found" : "error", started, { code: refusal });
1153
+ return network;
1154
+ }
773
1155
  if (!network.ok) {
774
1156
  readEvent("list", r.listPath, prefix, "replica", "hit", "found", started, { ...extra, code: network.error.code, verify: "error" });
775
1157
  return ok(local);
@@ -800,6 +1182,16 @@ function createKVReplication(deps) {
800
1182
  readEvent("list", r.listPath, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
801
1183
  return abortedResult(r.signal);
802
1184
  }
1185
+ const delivery = checkDelivery();
1186
+ if (delivery.status !== "allow") {
1187
+ readEvent("list", r.listPath, prefix, "network", delivery.status === "consent_expired" ? "grant_expired" : "replica_gated", "error", started, { code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" });
1188
+ return err(wrapError("kv", error));
1189
+ }
1190
+ const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
1191
+ if (refusal) {
1192
+ readEvent("list", r.listPath, prefix, "network", "grant_missing", "error", started, { code: refusal });
1193
+ return err(wrapError("kv", error));
1194
+ }
803
1195
  readEvent("list", r.listPath, prefix, "replica", "hit", "found", started, { ...extra, code: errorCode(error), verify: "error" });
804
1196
  return ok(local);
805
1197
  }
@@ -828,11 +1220,26 @@ function createKVReplication(deps) {
828
1220
  observeNetworkRequested({ op: "get", space: r.space, path: r.path, reason: "NETWORK_REQUESTED", outcome, latencyMs: scheduler.now() - started });
829
1221
  }
830
1222
  }
831
- if (prefix === void 0) {
832
- const value = await r.network();
833
- readEvent("get", r.path, null, "network", "not_covered", value.ok ? "found" : value.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error", started);
834
- return value;
1223
+ let networkRejected = false;
1224
+ const fallback = async (replica, reason, extra = {}) => {
1225
+ try {
1226
+ return await networkRead(r.network, "get", r.path, replica, reason, started, extra);
1227
+ } catch (error) {
1228
+ networkRejected = true;
1229
+ throw error;
1230
+ }
1231
+ };
1232
+ if (prefix === void 0) return networkRead(r.network, "get", r.path, null, "not_covered", started);
1233
+ const delivery = checkDelivery();
1234
+ if (delivery.status !== "allow") {
1235
+ return fallback(
1236
+ prefix,
1237
+ delivery.status === "consent_expired" ? "grant_expired" : "replica_gated",
1238
+ { code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" }
1239
+ );
835
1240
  }
1241
+ const refusal = sessionRefusal(prefix);
1242
+ if (refusal) return fallback(prefix, "grant_missing", { code: refusal });
836
1243
  try {
837
1244
  await raceSignal(reportExistingPins(), r.signal);
838
1245
  } catch {
@@ -843,86 +1250,63 @@ function createKVReplication(deps) {
843
1250
  handle = await raceSignal(open(prefix), r.signal);
844
1251
  } catch (error) {
845
1252
  if (r.signal.aborted) return abortedResult(r.signal);
846
- const value = await r.network();
847
- readEvent("get", r.path, prefix, "network", errorCode(error) === "RUNTIME_UNSUPPORTED" ? "runtime_unsupported" : "replica_unavailable", value.ok ? "found" : "error", started, { code: errorCode(error) });
848
- return value;
1253
+ if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
1254
+ const code = errorCode(error);
1255
+ return fallback(prefix, code === "RUNTIME_UNSUPPORTED" ? "runtime_unsupported" : code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
849
1256
  }
850
1257
  if (replicas.get(prefix)?.reason === "grant_missing") {
851
- const value = await r.network();
852
- readEvent("get", r.path, prefix, "network", "grant_missing", value.ok ? "found" : "error", started, { code: replicas.get(prefix)?.lastError });
853
- return value;
1258
+ const code = replicas.get(prefix)?.lastError;
1259
+ return fallback(prefix, code === "GRANT_EXPIRED" ? "grant_expired" : code === "REPLICA_GATED" ? "replica_gated" : "grant_missing", { code });
854
1260
  }
855
1261
  let fresh;
856
1262
  try {
857
1263
  fresh = await freshness(prefix, handle, r.signal);
858
1264
  } catch (error) {
859
1265
  recordRevocation(prefix, error);
1266
+ if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
860
1267
  if (r.signal.aborted) return abortedResult(r.signal);
861
- const value = await r.network();
862
- readEvent("get", r.path, prefix, "network", "replica_error", value.ok ? "found" : "error", started, { code: errorCode(error) });
863
- return value;
1268
+ const code = errorCode(error);
1269
+ return fallback(prefix, code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_error", { code });
864
1270
  }
865
1271
  if (r.signal.aborted) return abortedResult(r.signal);
866
1272
  if (fresh.failure) {
867
- const value = await r.network();
868
- readEvent("get", r.path, prefix, "network", fresh.failure === "busy" ? "stale" : "stale", value.ok ? "found" : "error", started, { code: fresh.failure === "busy" ? "REPLICA_BUSY" : fresh.syncError, syncedBeforeRead: fresh.syncedBeforeRead });
869
- return value;
1273
+ const code = fresh.failure === "busy" ? "REPLICA_BUSY" : fresh.syncError;
1274
+ return fallback(
1275
+ prefix,
1276
+ code === "GRANT_REVOKED" ? "grant_revoked" : code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "stale",
1277
+ { code, syncedBeforeRead: fresh.syncedBeforeRead }
1278
+ );
870
1279
  }
871
1280
  let pendingState;
872
1281
  try {
873
1282
  pendingState = await raceSignal(pending.read(), r.signal);
874
1283
  } catch (error) {
875
1284
  if (r.signal.aborted) return abortedResult(r.signal);
876
- const value = await r.network();
877
- readEvent("get", r.path, prefix, "network", "replica_unavailable", value.ok ? "found" : "error", started, { code: errorCode(error) });
878
- return value;
1285
+ const code = errorCode(error);
1286
+ return fallback(prefix, code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
879
1287
  }
880
1288
  const record = pendingState.records.find((x) => x.key === r.path);
881
- if (record) {
882
- const value = await r.network();
883
- readEvent("get", r.path, prefix, "network", "pending_write", value.ok ? "found" : value.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error", started, { pendingState: record.state, syncedBeforeRead: fresh.syncedBeforeRead });
884
- return value;
885
- }
886
- if (!pending.durable && !inProcessProof.has(prefix)) {
887
- const value = await r.network();
888
- readEvent("get", r.path, prefix, "network", "REPLICA_UNPROVEN_SINCE_START", value.ok ? "found" : "error", started);
889
- return value;
890
- }
891
- if (fresh.status.syncedThroughEpoch < pendingState.committedEpoch) {
892
- const value = await r.network();
893
- readEvent("get", r.path, prefix, "network", "REPLICA_BEHIND_OWN_WRITES", value.ok ? "found" : "error", started);
894
- return value;
895
- }
896
- if (fresh.status.authority.state !== "valid") {
897
- const value = await r.network();
898
- readEvent("get", r.path, prefix, "network", fresh.status.authority.state === "expired" ? "grant_expired" : fresh.status.authority.state === "revoked" ? "grant_revoked" : "grant_not_yet_valid", value.ok ? "found" : "error", started);
899
- return value;
900
- }
901
- if (fresh.status.coverage !== "complete") {
902
- const value = await r.network();
903
- readEvent("get", r.path, prefix, "network", "coverage_incomplete", value.ok ? "found" : "error", started);
904
- return value;
905
- }
1289
+ if (record) return fallback(prefix, "pending_write", { pendingState: record.state, syncedBeforeRead: fresh.syncedBeforeRead });
1290
+ if (!pending.durable && !inProcessProof.has(prefix)) return fallback(prefix, "REPLICA_UNPROVEN_SINCE_START");
1291
+ if (fresh.status.syncedThroughEpoch < pendingState.committedEpoch) return fallback(prefix, "REPLICA_BEHIND_OWN_WRITES");
1292
+ if (fresh.status.authority.state !== "valid") return fallback(prefix, fresh.status.authority.state === "expired" ? "grant_expired" : fresh.status.authority.state === "revoked" ? "grant_revoked" : "grant_not_yet_valid");
1293
+ if (fresh.status.coverage !== "complete") return fallback(prefix, "coverage_incomplete");
906
1294
  try {
907
1295
  const local = await raceSignal(handle.get(r.path), r.signal);
1296
+ const delivery2 = checkDelivery();
1297
+ if (delivery2.status !== "allow") throw deliveryError(delivery2);
1298
+ const refusal2 = sessionRefusal(prefix);
1299
+ if (refusal2) return await fallback(prefix, "grant_missing", { code: refusal2 });
908
1300
  try {
909
1301
  validateReadMeta(local.meta);
910
1302
  } catch (error) {
911
1303
  const code = errorCode(error);
912
1304
  recordRevocation(prefix, error);
913
- const value = await r.network();
914
- readEvent("get", r.path, prefix, "network", reasonForReadMetaCode(code), value.ok ? "found" : "error", started, { code });
915
- return value;
916
- }
917
- if (local.status === "coverage_incomplete" || local.status === "not_covered" || local.status === "content_missing") {
918
- const value = await r.network();
919
- readEvent("get", r.path, prefix, "network", local.status === "content_missing" ? "content_missing" : local.status === "coverage_incomplete" ? "coverage_incomplete" : "not_covered", value.ok ? "found" : "error", started);
920
- return value;
1305
+ return await fallback(prefix, reasonForReadMetaCode(code), { code });
921
1306
  }
1307
+ if (local.status === "coverage_incomplete" || local.status === "not_covered" || local.status === "content_missing") return await fallback(prefix, local.status === "content_missing" ? "content_missing" : local.status === "coverage_incomplete" ? "coverage_incomplete" : "not_covered");
922
1308
  if (local.status === "present" && r.options?.maxResponseBytes !== void 0 && local.value.byteLength > r.options.maxResponseBytes) {
923
- const value = await r.network();
924
- readEvent("get", r.path, prefix, "network", "unsupported_option", value.ok ? "found" : "error", started);
925
- return value;
1309
+ return await fallback(prefix, "unsupported_option");
926
1310
  }
927
1311
  const result = localGet(local, r.path, r.options ?? {});
928
1312
  const reason = local.status === "present" ? "hit" : local.status;
@@ -938,11 +1322,11 @@ function createKVReplication(deps) {
938
1322
  readEvent("get", r.path, prefix, "replica", reason, result.ok ? "found" : result.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error", started, event);
939
1323
  return result;
940
1324
  } catch (error) {
1325
+ if (networkRejected) throw error;
941
1326
  recordRevocation(prefix, error);
1327
+ if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
942
1328
  if (r.signal.aborted) return abortedResult(r.signal);
943
- const value = await r.network();
944
- readEvent("get", r.path, prefix, "network", "replica_error", value.ok ? "found" : "error", started, { code: errorCode(error) });
945
- return value;
1329
+ return fallback(prefix, deliveryReason(errorCode(error)), { code: errorCode(error) });
946
1330
  }
947
1331
  }
948
1332
  async function list(r) {
@@ -963,17 +1347,46 @@ function createKVReplication(deps) {
963
1347
  readEvent("list", r.listPath, null, "none", "cursor_restart", "error", started);
964
1348
  return cursorRestart();
965
1349
  }
966
- if (cursor && !cursor.startsWith("tcr1.")) {
967
- const result = await r.network();
968
- readEvent("list", r.listPath, null, "network", "network_cursor", result.ok ? "found" : "error", started);
969
- return result;
1350
+ if (cursor && !cursor.startsWith("tcr1.")) return networkRead(r.network, "list", r.listPath, null, "network_cursor", started);
1351
+ const prefix = listPathCovered(options.prefixes, r.listPath);
1352
+ let networkRejected = false;
1353
+ const fallback = async (replica, reason, extra = {}) => {
1354
+ try {
1355
+ return await networkRead(r.network, "list", r.listPath, replica, reason, started, extra);
1356
+ } catch (error) {
1357
+ networkRejected = true;
1358
+ throw error;
1359
+ }
1360
+ };
1361
+ if (prefix !== void 0 && !r.options?.raw) {
1362
+ const delivery = checkDelivery();
1363
+ if (delivery.status !== "allow") {
1364
+ if (decoded) {
1365
+ readEvent("list", r.listPath, prefix, "none", "cursor_restart", "error", started);
1366
+ return cursorRestart();
1367
+ }
1368
+ return fallback(
1369
+ prefix,
1370
+ delivery.status === "consent_expired" ? "grant_expired" : "replica_gated",
1371
+ { code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" }
1372
+ );
1373
+ }
1374
+ }
1375
+ if (prefix !== void 0 && !r.options?.raw) {
1376
+ const refusal = sessionRefusal(prefix);
1377
+ if (refusal) {
1378
+ if (decoded) {
1379
+ readEvent("list", r.listPath, prefix, "none", "cursor_restart", "error", started);
1380
+ return cursorRestart();
1381
+ }
1382
+ return fallback(prefix, "grant_missing", { code: refusal });
1383
+ }
970
1384
  }
971
1385
  try {
972
1386
  await raceSignal(reportExistingPins(), r.signal);
973
1387
  } catch {
974
1388
  if (r.signal.aborted) return abortedResult(r.signal);
975
1389
  }
976
- const prefix = listPathCovered(options.prefixes, r.listPath);
977
1390
  const restart = () => {
978
1391
  readEvent("list", r.listPath, prefix ?? null, "none", "cursor_restart", "error", started);
979
1392
  return cursorRestart();
@@ -981,36 +1394,33 @@ function createKVReplication(deps) {
981
1394
  if (cursor && decoded && (decoded.space !== r.space || decoded.path !== r.listPath)) return restart();
982
1395
  if (prefix === void 0 || r.options?.raw) {
983
1396
  if (cursor && decoded) return restart();
984
- const result = await r.network();
985
- readEvent("list", r.listPath, prefix ?? null, "network", prefix !== void 0 ? "unsupported_option" : "not_covered", result.ok ? "found" : "error", started);
986
- return result;
1397
+ return networkRead(r.network, "list", r.listPath, prefix ?? null, prefix !== void 0 ? "unsupported_option" : "not_covered", started);
987
1398
  }
988
1399
  let handle;
989
1400
  try {
990
1401
  handle = await raceSignal(open(prefix), r.signal);
991
1402
  } catch (error) {
992
1403
  if (r.signal.aborted) return abortedResult(r.signal);
1404
+ if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
993
1405
  if (cursor && decoded) return restart();
994
- const result = await r.network();
995
- readEvent("list", r.listPath, prefix, "network", errorCode(error) === "RUNTIME_UNSUPPORTED" ? "runtime_unsupported" : "replica_unavailable", result.ok ? "found" : "error", started, { code: errorCode(error) });
996
- return result;
1406
+ const code = errorCode(error);
1407
+ return fallback(prefix, code === "RUNTIME_UNSUPPORTED" ? "runtime_unsupported" : code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
997
1408
  }
998
1409
  if (replicas.get(prefix)?.reason === "grant_missing") {
999
1410
  if (cursor && decoded) return restart();
1000
- const result = await r.network();
1001
- readEvent("list", r.listPath, prefix, "network", "grant_missing", result.ok ? "found" : "error", started);
1002
- return result;
1411
+ const code = replicas.get(prefix)?.lastError;
1412
+ return fallback(prefix, code === "GRANT_EXPIRED" ? "grant_expired" : code === "REPLICA_GATED" ? "replica_gated" : "grant_missing", { code });
1003
1413
  }
1004
1414
  let fresh;
1005
1415
  try {
1006
1416
  fresh = await freshness(prefix, handle, r.signal);
1007
1417
  } catch (error) {
1008
1418
  recordRevocation(prefix, error);
1419
+ if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
1009
1420
  if (r.signal.aborted) return abortedResult(r.signal);
1010
1421
  if (cursor && decoded) return restart();
1011
- const result = await r.network();
1012
- readEvent("list", r.listPath, prefix, "network", "replica_error", result.ok ? "found" : "error", started, { code: errorCode(error) });
1013
- return result;
1422
+ const code = errorCode(error);
1423
+ return fallback(prefix, code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_error", { code });
1014
1424
  }
1015
1425
  if (r.signal.aborted) return abortedResult(r.signal);
1016
1426
  let state;
@@ -1019,41 +1429,35 @@ function createKVReplication(deps) {
1019
1429
  } catch (error) {
1020
1430
  if (r.signal.aborted) return abortedResult(r.signal);
1021
1431
  if (cursor && decoded) return restart();
1022
- const result = await r.network();
1023
- readEvent("list", r.listPath, prefix, "network", "replica_unavailable", result.ok ? "found" : "error", started, { code: errorCode(error) });
1024
- return result;
1432
+ const code = errorCode(error);
1433
+ return fallback(prefix, code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
1025
1434
  }
1026
- const pendingRange = state.records.find((record) => listRangeCovers(r.listPath, record.key) && (!decoded || utf8Compare(record.key, decoded.last) > 0));
1435
+ const pendingRange = state.records.find((record) => listRangeCovers(r.listPath, record.key));
1027
1436
  const behind = fresh.status.syncedThroughEpoch < state.committedEpoch;
1028
1437
  if (cursor && decoded && (fresh.failure || pendingRange || behind || fresh.status.coverage !== "complete" || fresh.status.authority.state !== "valid" || !pending.durable && !inProcessProof.has(prefix))) return restart();
1029
- if (behind) {
1030
- const result = await r.network();
1031
- readEvent("list", r.listPath, prefix, "network", "REPLICA_BEHIND_OWN_WRITES", result.ok ? "found" : "error", started);
1032
- return result;
1033
- }
1438
+ if (behind) return fallback(prefix, "REPLICA_BEHIND_OWN_WRITES");
1034
1439
  if (fresh.failure) {
1035
- const result = await r.network();
1036
- readEvent("list", r.listPath, prefix, "network", "stale", result.ok ? "found" : "error", started, { code: fresh.failure === "busy" ? "REPLICA_BUSY" : fresh.syncError });
1037
- return result;
1038
- }
1039
- if (pendingRange) {
1040
- const result = await r.network();
1041
- readEvent("list", r.listPath, prefix, "network", "pending_write", result.ok ? "found" : "error", started, { pendingState: pendingRange.state });
1042
- return result;
1043
- }
1044
- if (!pending.durable && !inProcessProof.has(prefix)) {
1045
- const result = await r.network();
1046
- readEvent("list", r.listPath, prefix, "network", "REPLICA_UNPROVEN_SINCE_START", result.ok ? "found" : "error", started);
1047
- return result;
1048
- }
1049
- if (fresh.status.coverage !== "complete" || fresh.status.authority.state !== "valid") {
1050
- const result = await r.network();
1051
- readEvent("list", r.listPath, prefix, "network", fresh.status.coverage !== "complete" ? "coverage_incomplete" : fresh.status.authority.state === "expired" ? "grant_expired" : fresh.status.authority.state === "revoked" ? "grant_revoked" : "grant_not_yet_valid", result.ok ? "found" : "error", started);
1052
- return result;
1440
+ const code = fresh.failure === "busy" ? "REPLICA_BUSY" : fresh.syncError;
1441
+ return fallback(prefix, code === "GRANT_REVOKED" ? "grant_revoked" : code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "stale", { code });
1053
1442
  }
1443
+ if (pendingRange) return fallback(prefix, "pending_write", { pendingState: pendingRange.state });
1444
+ if (!pending.durable && !inProcessProof.has(prefix)) return fallback(prefix, "REPLICA_UNPROVEN_SINCE_START");
1445
+ if (fresh.status.coverage !== "complete" || fresh.status.authority.state !== "valid") return fallback(prefix, fresh.status.coverage !== "complete" ? "coverage_incomplete" : fresh.status.authority.state === "expired" ? "grant_expired" : fresh.status.authority.state === "revoked" ? "grant_revoked" : "grant_not_yet_valid");
1054
1446
  try {
1055
1447
  const signalHandle = { list: (o) => raceSignal(handle.list(o), r.signal) };
1056
1448
  const page = await raceSignal(localList(signalHandle, r.space, r.listPath, r.options?.limit, cursor, validateReadMeta), r.signal);
1449
+ const latest = await raceSignal(pending.read(), r.signal);
1450
+ const laterPending = latest.records.find((record) => listRangeCovers(r.listPath, record.key));
1451
+ if (cursor && decoded && (laterPending || fresh.status.syncedThroughEpoch < latest.committedEpoch)) return restart();
1452
+ if (fresh.status.syncedThroughEpoch < latest.committedEpoch) return await fallback(prefix, "REPLICA_BEHIND_OWN_WRITES");
1453
+ if (laterPending) return await fallback(prefix, "pending_write", { pendingState: laterPending.state });
1454
+ const delivery = checkDelivery();
1455
+ if (delivery.status !== "allow") throw deliveryError(delivery);
1456
+ const refusal = sessionRefusal(prefix);
1457
+ if (refusal) {
1458
+ if (decoded) return restart();
1459
+ return await fallback(prefix, "grant_missing", { code: refusal });
1460
+ }
1057
1461
  const event = {
1058
1462
  count: page.keys.length,
1059
1463
  syncedBeforeRead: fresh.syncedBeforeRead,
@@ -1064,14 +1468,14 @@ function createKVReplication(deps) {
1064
1468
  readEvent("list", r.listPath, prefix, "replica", "hit", "found", started, event);
1065
1469
  return ok(page);
1066
1470
  } catch (error) {
1471
+ if (networkRejected) throw error;
1067
1472
  if (r.signal.aborted) return abortedResult(r.signal);
1068
1473
  const code = errorCode(error);
1474
+ if (code === "CONSENT_EXPIRED") startConsentDenial();
1069
1475
  recordRevocation(prefix, error);
1070
1476
  if (cursor && decoded) return restart();
1071
- const reason = reasonForReadMetaCode(code);
1072
- const result = await r.network();
1073
- readEvent("list", r.listPath, prefix, "network", reason, result.ok ? "found" : "error", started, { code });
1074
- return result;
1477
+ const reason = code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : reasonForReadMetaCode(code);
1478
+ return fallback(prefix, reason, { code });
1075
1479
  }
1076
1480
  }
1077
1481
  async function write(r) {
@@ -1080,11 +1484,14 @@ function createKVReplication(deps) {
1080
1484
  return prefix !== void 0 ? [{ key: entry.path, op: r.op === "delete" ? "delete" : "put" }] : [];
1081
1485
  });
1082
1486
  if (covered.length === 0) return r.network();
1487
+ if (checkDelivery().status !== "allow") return r.network();
1083
1488
  await reportExistingPins();
1489
+ if (checkDelivery().status !== "allow") return r.network();
1084
1490
  const opId = globalThis.crypto?.randomUUID?.() ?? `${scheduler.now()}-${Math.random()}`;
1085
1491
  try {
1086
1492
  await pending.update((s) => begin(s, opId, covered, nowDate(scheduler.now())));
1087
1493
  } catch (error) {
1494
+ if (errorCode(error) === "REPLICA_GATED" || errorCode(error) === "CONSENT_EXPIRED" || checkDelivery().status !== "allow") return r.network();
1088
1495
  return err(serviceError(ErrorCodes.KV_WRITE_FAILED, "Unable to persist pending KV write; request was not dispatched", "kv", { cause: error instanceof Error ? error : void 0, meta: { replication: "pending_store_unavailable", requestDispatched: false } }));
1089
1496
  }
1090
1497
  const started = scheduler.now();
@@ -1149,8 +1556,32 @@ function createKVReplication(deps) {
1149
1556
  for (const inspection of active) inspection.abort.abort(Object.assign(new Error("Replica status inspection cancelled"), { code: "ABORTED" }));
1150
1557
  return Promise.all(active.map((inspection) => inspection.settled)).then(() => void 0);
1151
1558
  }
1559
+ function gatedStatus(decision) {
1560
+ return options.prefixes.map((prefix) => ({
1561
+ prefix,
1562
+ state: "gated",
1563
+ reason: "replica_gated",
1564
+ gate: decision.status === "gated" ? decision.gate : "stamp_stale",
1565
+ errorCode: decision.status === "consent_expired" ? decision.code : "REPLICA_GATED",
1566
+ pending: { inFlight: 0, committed: 0, ambiguous: 0 },
1567
+ pinned: [],
1568
+ lagMs: null,
1569
+ redacted: true
1570
+ }));
1571
+ }
1152
1572
  async function status() {
1153
- const persistedSnapshot = pending.readExisting ? await pending.readExisting() : await pending.read();
1573
+ const beforeStatus = checkDelivery();
1574
+ if (beforeStatus.status !== "allow") return gatedStatus(beforeStatus);
1575
+ let persistedSnapshot;
1576
+ try {
1577
+ persistedSnapshot = pending.readExisting ? await pending.readExisting() : await pending.read();
1578
+ } catch (error) {
1579
+ const delivery = checkDelivery();
1580
+ if (delivery.status !== "allow") return gatedStatus(delivery);
1581
+ throw error;
1582
+ }
1583
+ const afterPending = checkDelivery();
1584
+ if (afterPending.status !== "allow") return gatedStatus(afterPending);
1154
1585
  const snapshot = persistedSnapshot ?? emptyPendingState(identity);
1155
1586
  const entries = await Promise.all(options.prefixes.map(async (prefix) => {
1156
1587
  if (prefix === "/" && deps.nodeFeatures !== void 0 && !deps.nodeFeatures.includes("kv-sync-root")) {
@@ -1172,7 +1603,7 @@ function createKVReplication(deps) {
1172
1603
  const full = local && !("redacted" in local) ? local : void 0;
1173
1604
  const revoked = opened?.authorityRevoked === true || full?.authority.state === "revoked";
1174
1605
  const rootUnsupported = opened?.lastError === "REPLICATION_ROOT_UNSUPPORTED";
1175
- const state = isClosed ? "closed" : revoked ? "revoked" : rootUnsupported ? "unavailable" : opened?.reason === "grant_missing" ? "grant_missing" : full?.coverage === "complete" && full.authority.state === "valid" ? "ready" : opened?.reason || inspected.errorCode ? "unavailable" : "idle";
1606
+ const state = isClosed ? "closed" : revoked ? "revoked" : rootUnsupported || opened?.reason === "grant_expired" ? "unavailable" : opened?.reason === "grant_missing" ? "grant_missing" : full?.coverage === "complete" && full.authority.state === "valid" ? "ready" : opened?.reason || inspected.errorCode ? "unavailable" : "idle";
1176
1607
  return {
1177
1608
  prefix,
1178
1609
  state,
@@ -1189,6 +1620,8 @@ function createKVReplication(deps) {
1189
1620
  lagMs: full?.lastSyncAt ? scheduler.now() - Date.parse(full.lastSyncAt) : null
1190
1621
  };
1191
1622
  }));
1623
+ const beforeReturn = checkDelivery();
1624
+ if (beforeReturn.status !== "allow") return gatedStatus(beforeReturn);
1192
1625
  return entries.filter((entry) => entry !== void 0);
1193
1626
  }
1194
1627
  async function sync(o) {
@@ -1204,11 +1637,15 @@ function createKVReplication(deps) {
1204
1637
  var _a;
1205
1638
  purgeStarted = true;
1206
1639
  purgeInProgress = true;
1640
+ cancelConsentTimer?.();
1641
+ cancelConsentTimer = void 0;
1207
1642
  const hasStatusInspections = statusInspections.size > 0;
1208
1643
  const statusDrain = cancelStatusInspections();
1209
1644
  for (const state of replicas.values()) {
1210
1645
  state.timer?.();
1211
1646
  state.timer = void 0;
1647
+ state.renewalTimer?.();
1648
+ state.renewalTimer = void 0;
1212
1649
  state.syncContext && ((_a = state.syncContext).abortCause ?? (_a.abortCause = "purge"));
1213
1650
  state.abort?.abort();
1214
1651
  state.mintAbort?.abort();
@@ -1232,6 +1669,7 @@ function createKVReplication(deps) {
1232
1669
  ...[...replicas.values()].flatMap((state) => [
1233
1670
  state.opening?.catch(() => void 0),
1234
1671
  state.sync?.catch(() => void 0),
1672
+ state.renewing?.catch(() => void 0),
1235
1673
  state.handle?.close().catch(() => void 0)
1236
1674
  ]),
1237
1675
  ...hasStatusInspections ? [statusDrain] : []
@@ -1269,11 +1707,15 @@ function createKVReplication(deps) {
1269
1707
  var _a;
1270
1708
  if (isClosed) return;
1271
1709
  isClosed = true;
1710
+ cancelConsentTimer?.();
1711
+ cancelConsentTimer = void 0;
1272
1712
  const hasStatusInspections = statusInspections.size > 0;
1273
1713
  const statusDrain = cancelStatusInspections();
1274
1714
  for (const [prefix, state] of replicas) {
1275
1715
  state.timer?.();
1276
1716
  state.timer = void 0;
1717
+ state.renewalTimer?.();
1718
+ state.renewalTimer = void 0;
1277
1719
  state.mintAbort?.abort();
1278
1720
  state.syncContext && ((_a = state.syncContext).abortCause ?? (_a.abortCause = "close"));
1279
1721
  state.abort?.abort();
@@ -1282,6 +1724,7 @@ function createKVReplication(deps) {
1282
1724
  const drain = Promise.all([
1283
1725
  ...[...replicas.values()].map(async (state) => {
1284
1726
  await state.opening?.catch(() => void 0);
1727
+ await state.renewing?.catch(() => void 0);
1285
1728
  await state.sync?.catch(() => void 0);
1286
1729
  await state.handle?.close().catch(() => void 0);
1287
1730
  }),
@@ -1311,7 +1754,8 @@ function createKVReplication(deps) {
1311
1754
  cancel();
1312
1755
  }
1313
1756
  }
1314
- return { get, list, write, observeNetworkRequested, status, sync, purge, clearPending: clearPending2, close };
1757
+ scheduleConsentDeadline();
1758
+ return { get, list, write, observeNetworkRequested, observeConsentExpired, status, sync, purge, clearPending: clearPending2, close };
1315
1759
  }
1316
1760
  function cancellationCode(signal) {
1317
1761
  return wrapError("kv", signal.reason).code === ErrorCodes.TIMEOUT || typeof signal.reason === "object" && signal.reason !== null && "code" in signal.reason && signal.reason.code === ErrorCodes.TIMEOUT ? ErrorCodes.TIMEOUT : ErrorCodes.ABORTED;