@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
@@ -111,6 +111,14 @@ var ErrorCodes = {
111
111
  ABORTED: "ABORTED",
112
112
  INVALID_INPUT: "INVALID_INPUT",
113
113
  PERMISSION_DENIED: "PERMISSION_DENIED",
114
+ // Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
115
+ // retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
116
+ /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */
117
+ NODE_BUSY: "NODE_BUSY",
118
+ /** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
119
+ REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
120
+ /** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */
121
+ OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN",
114
122
  // KV-specific errors
115
123
  KV_NOT_FOUND: "KV_NOT_FOUND",
116
124
  KV_WRITE_FAILED: "KV_WRITE_FAILED",
@@ -146,11 +154,12 @@ var ErrorCodes = {
146
154
  DUCKDB_READONLY_VIOLATION: "DUCKDB_READONLY_VIOLATION"
147
155
  };
148
156
  var defaultRetryPolicy = {
149
- maxAttempts: 3,
157
+ maxAttempts: 4,
150
158
  backoff: "exponential",
151
- baseDelayMs: 1e3,
152
- maxDelayMs: 1e4,
153
- retryableErrors: [ErrorCodes.NETWORK_ERROR, ErrorCodes.TIMEOUT]
159
+ baseDelayMs: 250,
160
+ maxDelayMs: 4e3,
161
+ retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],
162
+ maxTotalMs: 12e3
154
163
  };
155
164
  function ok(data) {
156
165
  return { ok: true, data };
@@ -404,6 +413,35 @@ function emitEvent(emit, scheduler, event) {
404
413
  }
405
414
  }
406
415
 
416
+ // src/kv/replication/grantPolicyConstants.ts
417
+ var HOUR_MS = 60 * 60 * 1e3;
418
+ var DAY_MS = 24 * HOUR_MS;
419
+ var DEFAULT_GRANT_TTL_MS = DAY_MS;
420
+ var MAX_GRANT_TTL_MS = 7 * DAY_MS;
421
+ var CONSENT_WINDOW_MS = 7 * DAY_MS;
422
+ var RENEW_BELOW_FRACTION = 0.25;
423
+
424
+ // src/kv/replication/grantPolicy.ts
425
+ var EXPIRED = Number.NEGATIVE_INFINITY;
426
+ var consentDeadlineMs = (anchor) => Number.isFinite(anchor.at) ? anchor.at + CONSENT_WINDOW_MS : EXPIRED;
427
+ var clampConsentAnchor = (anchor, now) => {
428
+ if (anchor === null || !Number.isFinite(anchor.at) || !Number.isFinite(now)) return null;
429
+ return anchor.at > now ? { at: now, source: anchor.source } : anchor;
430
+ };
431
+ var mintRemainingMs = ({ now, grantTtlMs, consentDeadline }) => {
432
+ if (!Number.isFinite(now) || !Number.isFinite(grantTtlMs) || !Number.isFinite(consentDeadline)) return 0;
433
+ return Math.min(Math.min(grantTtlMs, MAX_GRANT_TTL_MS), consentDeadline - now);
434
+ };
435
+ var needsSilentRenewal = ({ now, installedAt, effectiveExpiresAt, consentDeadline }) => {
436
+ if (!Number.isFinite(now) || !Number.isFinite(installedAt) || !Number.isFinite(effectiveExpiresAt) || !Number.isFinite(consentDeadline)) {
437
+ return false;
438
+ }
439
+ if (!(now < consentDeadline)) return false;
440
+ const remaining = effectiveExpiresAt - now;
441
+ const lifetime = effectiveExpiresAt - installedAt;
442
+ return remaining < lifetime * RENEW_BELOW_FRACTION;
443
+ };
444
+
407
445
  // src/kv/replication/controller.ts
408
446
  var CLOSE_TIMEOUT_MS = 3e3;
409
447
  var STATUS_REFRESH_TIMEOUT_MS = CLOSE_TIMEOUT_MS;
@@ -431,6 +469,128 @@ function createKVReplication(deps) {
431
469
  let previous = deps.previous;
432
470
  let pinnedStateReported = false;
433
471
  const inProcessProof = /* @__PURE__ */ new Set();
472
+ let anchorUnavailable = false;
473
+ let consentAnchor = null;
474
+ try {
475
+ consentAnchor = clampConsentAnchor(authority.consentAnchor?.() ?? null, scheduler.now());
476
+ if (consentAnchor?.source === "first_install") consentAnchor = null;
477
+ } catch {
478
+ anchorUnavailable = true;
479
+ }
480
+ const grantPolicy = { grantTtlMs: options.grantTtlMs ?? DEFAULT_GRANT_TTL_MS, consentAnchor };
481
+ const quota = options.maxBytes === void 0 && options.minFreeBytes === void 0 ? void 0 : { maxBytes: options.maxBytes, minFreeBytes: options.minFreeBytes };
482
+ const policyDeadline = consentAnchor === null ? null : consentDeadlineMs(consentAnchor);
483
+ let scheduledConsentDeadline = null;
484
+ let cancelConsentTimer;
485
+ function durableDeadline() {
486
+ if (storage.kind !== "indexeddb" || !deps.stamp) return null;
487
+ try {
488
+ const deadline = storage.lifecycle?.consentDeadline(deps.stamp);
489
+ return typeof deadline === "number" && Number.isFinite(deadline) ? deadline : null;
490
+ } catch {
491
+ return null;
492
+ }
493
+ }
494
+ function mintDeadline() {
495
+ if (policyDeadline === null) return null;
496
+ if (storage.kind !== "indexeddb") return policyDeadline;
497
+ const durable = durableDeadline();
498
+ return durable === null ? null : Math.min(policyDeadline, durable);
499
+ }
500
+ function scheduleConsentDeadline() {
501
+ const deadline = durableDeadline();
502
+ if (deadline === null || consentDenyStarted || isClosed) return;
503
+ const now = scheduler.now();
504
+ if (!Number.isFinite(now)) return;
505
+ if (scheduledConsentDeadline === deadline && cancelConsentTimer) return;
506
+ scheduledConsentDeadline = deadline;
507
+ cancelConsentTimer?.();
508
+ cancelConsentTimer = scheduler.setTimeout(() => {
509
+ cancelConsentTimer = void 0;
510
+ checkDelivery();
511
+ }, Math.max(0, deadline - now));
512
+ }
513
+ const reportedDeadline = /* @__PURE__ */ new Set();
514
+ let consentDenyStarted = false;
515
+ function checkDelivery() {
516
+ const gate = storage.lifecycle;
517
+ if (!gate) return storage.kind === "indexeddb" || anchorUnavailable || deps.stamp !== void 0 ? { status: "gated", gate: "gate_unavailable" } : { status: "allow" };
518
+ if (!deps.stamp) return { status: "gated", gate: "not_established" };
519
+ if (consentDenyStarted) return { status: "gated", gate: "stamp_stale" };
520
+ try {
521
+ const decision = gate.checkDelivery(deps.stamp);
522
+ if (decision.status === "consent_expired") startConsentDenial();
523
+ if (decision.status !== "allow") return decision;
524
+ if (storage.kind === "indexeddb") {
525
+ const deadline = durableDeadline();
526
+ if (deadline === null) return { status: "gated", gate: "gate_unavailable" };
527
+ const now = scheduler.now();
528
+ if (!Number.isFinite(now)) return { status: "gated", gate: "gate_unavailable" };
529
+ if (now >= deadline) {
530
+ startConsentDenial();
531
+ return { status: "consent_expired", code: "CONSENT_EXPIRED" };
532
+ }
533
+ }
534
+ return anchorUnavailable ? { status: "gated", gate: "gate_unavailable" } : decision;
535
+ } catch {
536
+ return { status: "gated", gate: "gate_unavailable" };
537
+ }
538
+ }
539
+ function markConsentExpired() {
540
+ if (consentDenyStarted || isClosed || purgeStarted) return false;
541
+ cancelConsentTimer?.();
542
+ cancelConsentTimer = void 0;
543
+ consentDenyStarted = true;
544
+ for (const state of replicas.values()) {
545
+ state.renewalTimer?.();
546
+ state.renewalTimer = void 0;
547
+ state.mintAbort?.abort();
548
+ const id = state.handle?.replicaId;
549
+ if (id && !reportedDeadline.has(id)) {
550
+ reportedDeadline.add(id);
551
+ emit({ type: "replication.grant", replicaId: id, action: "consent_deadline_reached", code: "CONSENT_EXPIRED" });
552
+ }
553
+ }
554
+ return true;
555
+ }
556
+ function observeConsentExpired(stamp) {
557
+ if (!deps.stamp || stamp.incarnation !== deps.stamp.incarnation || stamp.lifecycleEpoch !== deps.stamp.lifecycleEpoch) return;
558
+ if (markConsentExpired()) emit({ type: "replication.lifecycle", action: "denied", reason: "consent_expired" });
559
+ }
560
+ function startConsentDenial() {
561
+ const gate = storage.lifecycle;
562
+ if (!gate || !markConsentExpired()) return;
563
+ void gate.deny({ reason: "consent_expired", principal: identity.principal }).then(
564
+ (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" }),
565
+ () => emit({ type: "replication.lifecycle", action: "deny_failed", reason: "consent_expired" })
566
+ );
567
+ }
568
+ function deliveryError(decision) {
569
+ return Object.assign(new Error(decision.status === "consent_expired" ? "Replica consent expired" : "Replica session is gated"), {
570
+ code: decision.status === "consent_expired" ? decision.code : "REPLICA_GATED",
571
+ ...decision.status === "gated" ? { gate: decision.gate } : {}
572
+ });
573
+ }
574
+ function deliveryReason(code) {
575
+ return code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? "grant_expired" : code === "REPLICA_GATED" ? "replica_gated" : "replica_error";
576
+ }
577
+ function reportGrantExpiry(prefix) {
578
+ const state = replicas.get(prefix);
579
+ if (!state) return;
580
+ state.reason = "grant_expired";
581
+ state.lastError = "GRANT_EXPIRED";
582
+ const replicaId = state.handle?.replicaId;
583
+ if (!replicaId || state.reportedExpired) return;
584
+ state.reportedExpired = true;
585
+ emit({ type: "replication.grant", replicaId, action: "expired", code: "GRANT_EXPIRED" });
586
+ }
587
+ function reportGrantCap(state, handle, grant) {
588
+ if (!grant?.unconstrained || !handle.replicaId || typeof grant.effectiveExpiresAt !== "number" || !Number.isFinite(grant.effectiveExpiresAt) || !(grant.expiresAt === null || grant.effectiveExpiresAt < grant.expiresAt)) return;
589
+ const capKey = `${grant.cid}:${grant.effectiveExpiresAt}`;
590
+ if (state.reportedCappedCid === capKey) return;
591
+ state.reportedCappedCid = capKey;
592
+ emit({ type: "replication.grant", replicaId: handle.replicaId, action: "capped", effectiveExpiresAt: nowDate(grant.effectiveExpiresAt) });
593
+ }
434
594
  const configuredPrefix = (path) => options.prefixes.find((prefix) => kvPrefixCovers(prefix, path));
435
595
  function recordRevocation(prefix, error) {
436
596
  if (errorCode(error) !== "GRANT_REVOKED") return;
@@ -454,7 +614,26 @@ function createKVReplication(deps) {
454
614
  markRootUnsupported(prefix);
455
615
  return Object.assign(new Error("This node doesn't support whole-space sync yet."), { code: "REPLICATION_ROOT_UNSUPPORTED" });
456
616
  }
617
+ function canReuseInstalled(grant, sessionOnly) {
618
+ if (!grant?.unconstrained) return false;
619
+ if (!deps.stamp) return true;
620
+ if (!sessionOnly && consentAnchor === null) return false;
621
+ const now = scheduler.now();
622
+ const deadline = storage.kind === "indexeddb" ? durableDeadline() : policyDeadline ?? grant.consentDeadline;
623
+ 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);
624
+ }
625
+ function sessionRefusal(prefix) {
626
+ try {
627
+ if (authority.sessionOnly !== true) return void 0;
628
+ const grant = authority.sessionGrant(prefix);
629
+ return "refused" in grant ? grant.refused : void 0;
630
+ } catch {
631
+ return "NOT_COVERED";
632
+ }
633
+ }
457
634
  async function open(prefix) {
635
+ const delivery = checkDelivery();
636
+ if (delivery.status !== "allow") throw deliveryError(delivery);
458
637
  if (isClosed || purgeStarted) throw Object.assign(new Error("Replication controller is closed"), { code: "REPLICA_CLOSED" });
459
638
  let state = replicas.get(prefix);
460
639
  if (state?.handle) return state.handle;
@@ -475,46 +654,99 @@ function createKVReplication(deps) {
475
654
  await previous;
476
655
  previous = void 0;
477
656
  }
657
+ const delivery2 = checkDelivery();
658
+ if (delivery2.status !== "allow") throw deliveryError(delivery2);
659
+ const sessionOnly = authority.sessionOnly === true;
478
660
  const sessionGrant = authority.sessionGrant(prefix);
479
661
  const delegated = "refused" in sessionGrant ? void 0 : sessionGrant;
480
- const plan = authority.sessionOnly ? void 0 : authority.plan(prefix);
662
+ const plan = sessionOnly ? void 0 : authority.plan(prefix);
481
663
  let refusalCode;
482
664
  if ("refused" in sessionGrant) refusalCode = sessionGrant.refused;
483
665
  if (plan && "refused" in plan) refusalCode ?? (refusalCode = plan.refused);
666
+ if (deps.stamp && consentAnchor === null && !sessionOnly) refusalCode = "REPLICA_GATED";
484
667
  let device;
485
668
  if (delegated) {
486
669
  device = delegated.device;
487
670
  state.strategy = "session";
488
671
  }
489
- const spec = { identity, space: session.space, prefix, allowSecrets: options.allowSecrets, ...device ? { device } : {}, ...deps.fetch ? { fetch: deps.fetch } : {} };
672
+ const spec = {
673
+ identity,
674
+ space: session.space,
675
+ prefix,
676
+ allowSecrets: options.allowSecrets,
677
+ grantPolicy,
678
+ ...quota ? { quota } : {},
679
+ ...deps.stamp ? { stamp: deps.stamp } : {},
680
+ ...device ? { device } : {},
681
+ ...deps.fetch ? { fetch: deps.fetch } : {},
682
+ onPeerCommit: (info) => {
683
+ if (isClosed || purgeStarted || state.handle === void 0) return;
684
+ emit({
685
+ type: "replication.sync",
686
+ space: session.space,
687
+ replica: prefix,
688
+ trigger: "peer",
689
+ origin: "peer",
690
+ outcome: "ok",
691
+ durationMs: 0,
692
+ lagMs: null,
693
+ ...info.changes === void 0 ? {} : { changes: info.changes },
694
+ ...info.coverage === void 0 ? {} : { coverage: info.coverage }
695
+ });
696
+ }
697
+ };
490
698
  if (prefix === "/" && deps.nodeFeatures !== void 0 && !deps.nodeFeatures.includes("kv-sync-root")) {
491
699
  throw unsupportedRoot(prefix);
492
700
  }
493
701
  const handle = await storage.open(spec);
494
702
  state.handle = handle;
703
+ const afterOpen = checkDelivery();
704
+ if (afterOpen.status !== "allow") throw deliveryError(afterOpen);
495
705
  emit({ type: "replication.state", space: session.space, replica: prefix, state: "opened" });
496
- const runtimePlan = !authority.sessionOnly && plan && !("refused" in plan) ? plan : void 0;
497
- let installed = runtimePlan ? await handle.grant() : void 0;
498
- if (delegated) installed = await handle.installGrant(delegated.ucan);
499
- else if (runtimePlan) {
706
+ const runtimePlan = !sessionOnly && plan && !("refused" in plan) ? plan : void 0;
707
+ let installed = runtimePlan || delegated ? await handle.grant() : void 0;
708
+ if (installed && !canReuseInstalled(installed, sessionOnly)) {
709
+ if (installed.unconstrained) refusalCode = consentAnchor === null && !sessionOnly ? "REPLICA_GATED" : "GRANT_EXPIRED";
710
+ installed = void 0;
711
+ }
712
+ let mintedExpiryMs;
713
+ if (delegated && consentAnchor !== null) {
714
+ const beforeInstall = checkDelivery();
715
+ if (beforeInstall.status !== "allow") throw deliveryError(beforeInstall);
716
+ installed = await handle.installGrant(delegated.ucan);
717
+ } else if (runtimePlan) {
500
718
  if (!installed || !installed.unconstrained || installed.parentCid !== runtimePlan.parentCid || installed.expiresAt !== null && runtimePlan.expiresAt > installed.expiresAt + 3e5) {
501
- try {
502
- const mintAbort = new AbortController();
503
- state.mintAbort = mintAbort;
504
- const minted = await authority.mint(handle.deviceDid, prefix, mintAbort.signal);
505
- installed = await handle.installGrant(minted.ucan);
506
- state.strategy = "minted";
507
- } catch (error) {
508
- if (!installed?.unconstrained) throw error;
509
- } finally {
510
- state.mintAbort = void 0;
719
+ const deadline = mintDeadline();
720
+ const expiryMs = mintRemainingMs({ now: scheduler.now(), grantTtlMs: grantPolicy.grantTtlMs, consentDeadline: deadline ?? Number.NaN });
721
+ if (expiryMs > 0) {
722
+ try {
723
+ const mintAbort = new AbortController();
724
+ state.mintAbort = mintAbort;
725
+ const minted = await authority.mint(handle.deviceDid, prefix, mintAbort.signal, { expiryMs });
726
+ const beforeInstall = checkDelivery();
727
+ if (beforeInstall.status !== "allow") throw deliveryError(beforeInstall);
728
+ installed = await handle.installGrant(minted.ucan);
729
+ state.strategy = "minted";
730
+ mintedExpiryMs = expiryMs;
731
+ } catch (error) {
732
+ if (errorCode(error) === "CONSENT_EXPIRED" || errorCode(error) === "REPLICA_GATED" || !installed?.unconstrained) throw error;
733
+ } finally {
734
+ state.mintAbort = void 0;
735
+ }
511
736
  }
512
737
  }
513
738
  if (installed?.unconstrained) {
514
739
  state.strategy ?? (state.strategy = "installed");
515
740
  state.reason = void 0;
516
741
  }
742
+ } else if ((delegated || sessionOnly) && installed?.unconstrained) {
743
+ state.strategy = "installed";
517
744
  }
745
+ const beforeGrantEvent = checkDelivery();
746
+ if (beforeGrantEvent.status !== "allow") throw deliveryError(beforeGrantEvent);
747
+ reportGrantCap(state, handle, installed);
748
+ if (mintedExpiryMs !== void 0) state.reportedExpired = false;
749
+ if (mintedExpiryMs !== void 0 && handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "minted", expiryMs: mintedExpiryMs });
518
750
  if (!installed?.unconstrained) {
519
751
  state.reason = "grant_missing";
520
752
  state.lastError = refusalCode ?? "SESSION_LACKS_SYNC";
@@ -524,6 +756,7 @@ function createKVReplication(deps) {
524
756
  await handle.close();
525
757
  throw Object.assign(new Error("Replication controller is closed"), { code: "REPLICA_CLOSED" });
526
758
  }
759
+ if (installed?.unconstrained) await maybeRenew(prefix, handle, installed, runtimePlan);
527
760
  if (mode === "background" && state.reason !== "grant_missing") {
528
761
  void syncPrefix(prefix, "start").catch(() => void 0);
529
762
  schedule(prefix);
@@ -534,12 +767,14 @@ function createKVReplication(deps) {
534
767
  return await state.opening;
535
768
  } catch (error) {
536
769
  const code = errorCode(error);
770
+ if (code === "CONSENT_EXPIRED") startConsentDenial();
537
771
  recordRevocation(prefix, error);
538
- if (code !== "GRANT_REVOKED") state.reason = "replica_unavailable";
772
+ if (code === "REPLICA_GATED" || code === "CONSENT_EXPIRED") state.reason = deliveryReason(code);
773
+ else if (code !== "GRANT_REVOKED") state.reason = "replica_unavailable";
539
774
  state.lastError = code;
540
775
  state.failures = (state.failures ?? 0) + 1;
541
776
  if (code === "RUNTIME_UNSUPPORTED") state.unsupported = true;
542
- else if (code !== "REPLICA_CLOSED" && code !== "REPLICATION_ROOT_UNSUPPORTED") state.retryAt = scheduler.now() + Math.min(3e5, 5e3 * 2 ** Math.min(state.failures - 1, 6));
777
+ 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));
543
778
  state.handle?.close().catch(() => void 0);
544
779
  state.handle = void 0;
545
780
  throw error;
@@ -547,6 +782,96 @@ function createKVReplication(deps) {
547
782
  state.opening = void 0;
548
783
  }
549
784
  }
785
+ function scheduleRenewal(prefix, handle, grant, plan) {
786
+ const state = replicas.get(prefix);
787
+ state?.renewalTimer?.();
788
+ if (state) state.renewalTimer = void 0;
789
+ if (!state || isClosed || purgeStarted || plan.path !== "session" || !authority.canRenew) return;
790
+ const { installedAt, effectiveExpiresAt } = grant;
791
+ const deadline = mintDeadline();
792
+ if (typeof installedAt !== "number" || !Number.isFinite(installedAt) || typeof effectiveExpiresAt !== "number" || !Number.isFinite(effectiveExpiresAt) || !(effectiveExpiresAt > installedAt) || deadline === null) return;
793
+ const threshold = installedAt + (effectiveExpiresAt - installedAt) * (1 - RENEW_BELOW_FRACTION);
794
+ const now = scheduler.now();
795
+ if (!Number.isFinite(now) || !(threshold < deadline) || now > threshold) return;
796
+ state.renewalTimer = scheduler.setTimeout(() => {
797
+ state.renewalTimer = void 0;
798
+ void maybeRenew(prefix, handle).catch(() => void 0);
799
+ }, Math.max(0, Math.floor(threshold) + 1 - now));
800
+ }
801
+ async function maybeRenew(prefix, handle, knownGrant, knownPlan) {
802
+ const state = replicas.get(prefix);
803
+ if (!state || state.handle !== handle || isClosed || purgeStarted || !authority.canRenew) return;
804
+ if (state.renewing) return state.renewing;
805
+ const decision = checkDelivery();
806
+ if (decision.status !== "allow") return;
807
+ const grant = knownGrant ?? await handle.grant();
808
+ if (!grant?.unconstrained) return;
809
+ const { installedAt, effectiveExpiresAt } = grant;
810
+ const deadline = mintDeadline();
811
+ if (typeof installedAt !== "number" || !Number.isFinite(installedAt) || typeof effectiveExpiresAt !== "number" || !Number.isFinite(effectiveExpiresAt) || deadline === null) return;
812
+ const now = scheduler.now();
813
+ if (!needsSilentRenewal({ now, installedAt, effectiveExpiresAt, consentDeadline: deadline })) {
814
+ if (knownPlan) scheduleRenewal(prefix, handle, grant, knownPlan);
815
+ return;
816
+ }
817
+ let plan = knownPlan;
818
+ try {
819
+ if (!plan) {
820
+ const selected = authority.plan(prefix);
821
+ if ("refused" in selected) {
822
+ if (handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewal_refused", code: selected.refused });
823
+ return;
824
+ }
825
+ plan = selected;
826
+ }
827
+ } catch {
828
+ if (handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewal_deferred" });
829
+ return;
830
+ }
831
+ if (plan?.path !== "session") {
832
+ if (handle.replicaId && state.reportedRenewalUnqualifiedCid !== grant.cid) {
833
+ state.reportedRenewalUnqualifiedCid = grant.cid;
834
+ emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewal_deferred" });
835
+ }
836
+ return;
837
+ }
838
+ const expiryMs = mintRemainingMs({ now, grantTtlMs: grantPolicy.grantTtlMs, consentDeadline: deadline });
839
+ if (!(expiryMs > 0)) return;
840
+ state.renewalTimer?.();
841
+ state.renewalTimer = void 0;
842
+ const mintAbort = new AbortController();
843
+ state.mintAbort = mintAbort;
844
+ const job = Promise.resolve().then(async () => {
845
+ const minted = await authority.mint(handle.deviceDid, prefix, mintAbort.signal, { expiryMs, renewalOnly: true });
846
+ const beforeInstall = checkDelivery();
847
+ if (beforeInstall.status !== "allow") throw deliveryError(beforeInstall);
848
+ const installed = await handle.installGrant(minted.ucan);
849
+ const beforeEvent = checkDelivery();
850
+ if (beforeEvent.status !== "allow") throw deliveryError(beforeEvent);
851
+ reportGrantCap(state, handle, installed);
852
+ state.reportedExpired = false;
853
+ if (state.reason === "grant_expired") state.reason = void 0;
854
+ if (state.lastError === "GRANT_EXPIRED") state.lastError = void 0;
855
+ if (handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewed", expiryMs });
856
+ if (typeof installed.effectiveExpiresAt === "number" && installed.effectiveExpiresAt > effectiveExpiresAt) {
857
+ scheduleRenewal(prefix, handle, installed, plan);
858
+ }
859
+ });
860
+ state.renewing = job;
861
+ try {
862
+ await job;
863
+ } catch (error) {
864
+ const code = errorCode(error);
865
+ if (code === "CONSENT_EXPIRED" || code === "REPLICA_GATED") throw error;
866
+ if (!isClosed && handle.replicaId) {
867
+ const refused = typeof error === "object" && error !== null && "refused" in error && typeof error.refused === "string" ? error.refused : null;
868
+ emit({ type: "replication.grant", replicaId: handle.replicaId, action: refused ? "renewal_refused" : "renewal_deferred", code: refused ?? code });
869
+ }
870
+ } finally {
871
+ state.renewing = void 0;
872
+ if (state.mintAbort === mintAbort) state.mintAbort = void 0;
873
+ }
874
+ }
550
875
  function schedule(prefix) {
551
876
  const state = replicas.get(prefix);
552
877
  if (!state || mode !== "background" || isClosed || purgeStarted) return;
@@ -559,6 +884,8 @@ function createKVReplication(deps) {
559
884
  if (prefix === "/" && deps.nodeFeatures !== void 0 && !deps.nodeFeatures.includes("kv-sync-root")) {
560
885
  throw unsupportedRoot(prefix);
561
886
  }
887
+ const initialRefusal = sessionRefusal(prefix);
888
+ if (initialRefusal) throw Object.assign(new Error("Current session does not cover replica sync"), { code: initialRefusal });
562
889
  const handle = await open(prefix);
563
890
  const state = replicas.get(prefix);
564
891
  if (state.sync) return state.sync;
@@ -576,22 +903,36 @@ function createKVReplication(deps) {
576
903
  job = (async () => {
577
904
  try {
578
905
  const syncStartEpoch = (await pending.read()).committedEpoch;
906
+ const beforeSync = checkDelivery();
907
+ if (beforeSync.status !== "allow") throw deliveryError(beforeSync);
908
+ await maybeRenew(prefix, handle);
909
+ const afterRenewal = checkDelivery();
910
+ if (afterRenewal.status !== "allow") throw deliveryError(afterRenewal);
911
+ const refusal = sessionRefusal(prefix);
912
+ if (refusal) throw Object.assign(new Error("Current session does not cover replica sync"), { code: refusal });
579
913
  const result = await handle.sync({ signal: abort.signal, syncStartEpoch });
914
+ const afterSyncDelivery = checkDelivery();
915
+ if (afterSyncDelivery.status !== "allow") throw deliveryError(afterSyncDelivery);
580
916
  if (result.status === "busy") {
581
917
  emit({ type: "replication.sync", space: session.space, replica: prefix, trigger, outcome: "busy", durationMs: scheduler.now() - started, lagMs: null });
582
918
  return result;
583
919
  }
584
920
  inProcessProof.add(prefix);
585
921
  const snapshot = await pending.read();
922
+ const beforePending = checkDelivery();
923
+ if (beforePending.status !== "allow") throw deliveryError(beforePending);
586
924
  const clearedCandidates = snapshot.records.some((r) => r.state === "committed" && r.epoch !== null && r.epoch <= syncStartEpoch && kvPrefixCovers(prefix, r.key));
587
925
  const cleared = clearedCandidates ? await pending.update((s) => afterSync(s, prefix, syncStartEpoch)) : 0;
926
+ const beforeResult = checkDelivery();
927
+ if (beforeResult.status !== "allow") throw deliveryError(beforeResult);
588
928
  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 });
589
929
  return result;
590
930
  } catch (error) {
591
931
  const code = errorCode(error);
592
932
  state.lastError = code;
593
933
  recordRevocation(prefix, error);
594
- 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 });
934
+ if (code === "CONSENT_EXPIRED") startConsentDenial();
935
+ 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 });
595
936
  throw error;
596
937
  } finally {
597
938
  signal?.removeEventListener("abort", abortFromCaller);
@@ -747,8 +1088,19 @@ function createKVReplication(deps) {
747
1088
  return { status: currentStatus, syncedBeforeRead: false };
748
1089
  }
749
1090
  function readEvent(op, path, prefix, source, reason, outcome, started, extra = {}) {
1091
+ if (prefix !== null && reason === "grant_expired" && extra.code !== "CONSENT_EXPIRED") reportGrantExpiry(prefix);
750
1092
  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 });
751
1093
  }
1094
+ async function networkRead(network, op, path, prefix, reason, started, extra = {}) {
1095
+ let outcome = "error";
1096
+ try {
1097
+ const result = await network();
1098
+ outcome = result.ok ? "found" : op === "get" && (reason === "pending_write" || reason === "not_covered") && result.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error";
1099
+ return result;
1100
+ } finally {
1101
+ readEvent(op, path, prefix, "network", reason, outcome, started, extra);
1102
+ }
1103
+ }
752
1104
  async function verifyGet(r, local, reason, started, extra) {
753
1105
  const prefix = configuredPrefix(r.path) ?? null;
754
1106
  const localFound = local.ok;
@@ -760,6 +1112,16 @@ function createKVReplication(deps) {
760
1112
  readEvent("get", r.path, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
761
1113
  return abortedResult(r.signal);
762
1114
  }
1115
+ const delivery = checkDelivery();
1116
+ if (delivery.status !== "allow") {
1117
+ 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" });
1118
+ return network;
1119
+ }
1120
+ const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
1121
+ if (refusal) {
1122
+ readEvent("get", r.path, prefix, "network", "grant_missing", network.ok ? "found" : "error", started, { code: refusal });
1123
+ return network;
1124
+ }
763
1125
  if (!network.ok && network.error.code !== ErrorCodes.KV_NOT_FOUND) {
764
1126
  readEvent("get", r.path, prefix, "replica", reason, outcome, started, { ...extra, code: network.error.code, verify: "error" });
765
1127
  return local;
@@ -790,6 +1152,16 @@ function createKVReplication(deps) {
790
1152
  readEvent("get", r.path, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
791
1153
  return abortedResult(r.signal);
792
1154
  }
1155
+ const delivery = checkDelivery();
1156
+ if (delivery.status !== "allow") {
1157
+ 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" });
1158
+ return err(wrapError("kv", error));
1159
+ }
1160
+ const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
1161
+ if (refusal) {
1162
+ readEvent("get", r.path, prefix, "network", "grant_missing", "error", started, { code: refusal });
1163
+ return err(wrapError("kv", error));
1164
+ }
793
1165
  readEvent("get", r.path, prefix, "replica", reason, outcome, started, { ...extra, code: errorCode(error), verify: "error" });
794
1166
  return local;
795
1167
  }
@@ -802,6 +1174,16 @@ function createKVReplication(deps) {
802
1174
  readEvent("list", r.listPath, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
803
1175
  return abortedResult(r.signal);
804
1176
  }
1177
+ const delivery = checkDelivery();
1178
+ if (delivery.status !== "allow") {
1179
+ 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" });
1180
+ return network;
1181
+ }
1182
+ const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
1183
+ if (refusal) {
1184
+ readEvent("list", r.listPath, prefix, "network", "grant_missing", network.ok ? "found" : "error", started, { code: refusal });
1185
+ return network;
1186
+ }
805
1187
  if (!network.ok) {
806
1188
  readEvent("list", r.listPath, prefix, "replica", "hit", "found", started, { ...extra, code: network.error.code, verify: "error" });
807
1189
  return ok(local);
@@ -832,6 +1214,16 @@ function createKVReplication(deps) {
832
1214
  readEvent("list", r.listPath, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
833
1215
  return abortedResult(r.signal);
834
1216
  }
1217
+ const delivery = checkDelivery();
1218
+ if (delivery.status !== "allow") {
1219
+ 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" });
1220
+ return err(wrapError("kv", error));
1221
+ }
1222
+ const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
1223
+ if (refusal) {
1224
+ readEvent("list", r.listPath, prefix, "network", "grant_missing", "error", started, { code: refusal });
1225
+ return err(wrapError("kv", error));
1226
+ }
835
1227
  readEvent("list", r.listPath, prefix, "replica", "hit", "found", started, { ...extra, code: errorCode(error), verify: "error" });
836
1228
  return ok(local);
837
1229
  }
@@ -860,11 +1252,26 @@ function createKVReplication(deps) {
860
1252
  observeNetworkRequested({ op: "get", space: r.space, path: r.path, reason: "NETWORK_REQUESTED", outcome, latencyMs: scheduler.now() - started });
861
1253
  }
862
1254
  }
863
- if (prefix === void 0) {
864
- const value = await r.network();
865
- readEvent("get", r.path, null, "network", "not_covered", value.ok ? "found" : value.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error", started);
866
- return value;
1255
+ let networkRejected = false;
1256
+ const fallback = async (replica, reason, extra = {}) => {
1257
+ try {
1258
+ return await networkRead(r.network, "get", r.path, replica, reason, started, extra);
1259
+ } catch (error) {
1260
+ networkRejected = true;
1261
+ throw error;
1262
+ }
1263
+ };
1264
+ if (prefix === void 0) return networkRead(r.network, "get", r.path, null, "not_covered", started);
1265
+ const delivery = checkDelivery();
1266
+ if (delivery.status !== "allow") {
1267
+ return fallback(
1268
+ prefix,
1269
+ delivery.status === "consent_expired" ? "grant_expired" : "replica_gated",
1270
+ { code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" }
1271
+ );
867
1272
  }
1273
+ const refusal = sessionRefusal(prefix);
1274
+ if (refusal) return fallback(prefix, "grant_missing", { code: refusal });
868
1275
  try {
869
1276
  await raceSignal(reportExistingPins(), r.signal);
870
1277
  } catch {
@@ -875,86 +1282,63 @@ function createKVReplication(deps) {
875
1282
  handle = await raceSignal(open(prefix), r.signal);
876
1283
  } catch (error) {
877
1284
  if (r.signal.aborted) return abortedResult(r.signal);
878
- const value = await r.network();
879
- readEvent("get", r.path, prefix, "network", errorCode(error) === "RUNTIME_UNSUPPORTED" ? "runtime_unsupported" : "replica_unavailable", value.ok ? "found" : "error", started, { code: errorCode(error) });
880
- return value;
1285
+ if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
1286
+ const code = errorCode(error);
1287
+ return fallback(prefix, code === "RUNTIME_UNSUPPORTED" ? "runtime_unsupported" : code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
881
1288
  }
882
1289
  if (replicas.get(prefix)?.reason === "grant_missing") {
883
- const value = await r.network();
884
- readEvent("get", r.path, prefix, "network", "grant_missing", value.ok ? "found" : "error", started, { code: replicas.get(prefix)?.lastError });
885
- return value;
1290
+ const code = replicas.get(prefix)?.lastError;
1291
+ return fallback(prefix, code === "GRANT_EXPIRED" ? "grant_expired" : code === "REPLICA_GATED" ? "replica_gated" : "grant_missing", { code });
886
1292
  }
887
1293
  let fresh;
888
1294
  try {
889
1295
  fresh = await freshness(prefix, handle, r.signal);
890
1296
  } catch (error) {
891
1297
  recordRevocation(prefix, error);
1298
+ if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
892
1299
  if (r.signal.aborted) return abortedResult(r.signal);
893
- const value = await r.network();
894
- readEvent("get", r.path, prefix, "network", "replica_error", value.ok ? "found" : "error", started, { code: errorCode(error) });
895
- return value;
1300
+ const code = errorCode(error);
1301
+ return fallback(prefix, code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_error", { code });
896
1302
  }
897
1303
  if (r.signal.aborted) return abortedResult(r.signal);
898
1304
  if (fresh.failure) {
899
- const value = await r.network();
900
- 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 });
901
- return value;
1305
+ const code = fresh.failure === "busy" ? "REPLICA_BUSY" : fresh.syncError;
1306
+ return fallback(
1307
+ prefix,
1308
+ code === "GRANT_REVOKED" ? "grant_revoked" : code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "stale",
1309
+ { code, syncedBeforeRead: fresh.syncedBeforeRead }
1310
+ );
902
1311
  }
903
1312
  let pendingState;
904
1313
  try {
905
1314
  pendingState = await raceSignal(pending.read(), r.signal);
906
1315
  } catch (error) {
907
1316
  if (r.signal.aborted) return abortedResult(r.signal);
908
- const value = await r.network();
909
- readEvent("get", r.path, prefix, "network", "replica_unavailable", value.ok ? "found" : "error", started, { code: errorCode(error) });
910
- return value;
1317
+ const code = errorCode(error);
1318
+ return fallback(prefix, code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
911
1319
  }
912
1320
  const record = pendingState.records.find((x) => x.key === r.path);
913
- if (record) {
914
- const value = await r.network();
915
- 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 });
916
- return value;
917
- }
918
- if (!pending.durable && !inProcessProof.has(prefix)) {
919
- const value = await r.network();
920
- readEvent("get", r.path, prefix, "network", "REPLICA_UNPROVEN_SINCE_START", value.ok ? "found" : "error", started);
921
- return value;
922
- }
923
- if (fresh.status.syncedThroughEpoch < pendingState.committedEpoch) {
924
- const value = await r.network();
925
- readEvent("get", r.path, prefix, "network", "REPLICA_BEHIND_OWN_WRITES", value.ok ? "found" : "error", started);
926
- return value;
927
- }
928
- if (fresh.status.authority.state !== "valid") {
929
- const value = await r.network();
930
- 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);
931
- return value;
932
- }
933
- if (fresh.status.coverage !== "complete") {
934
- const value = await r.network();
935
- readEvent("get", r.path, prefix, "network", "coverage_incomplete", value.ok ? "found" : "error", started);
936
- return value;
937
- }
1321
+ if (record) return fallback(prefix, "pending_write", { pendingState: record.state, syncedBeforeRead: fresh.syncedBeforeRead });
1322
+ if (!pending.durable && !inProcessProof.has(prefix)) return fallback(prefix, "REPLICA_UNPROVEN_SINCE_START");
1323
+ if (fresh.status.syncedThroughEpoch < pendingState.committedEpoch) return fallback(prefix, "REPLICA_BEHIND_OWN_WRITES");
1324
+ 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");
1325
+ if (fresh.status.coverage !== "complete") return fallback(prefix, "coverage_incomplete");
938
1326
  try {
939
1327
  const local = await raceSignal(handle.get(r.path), r.signal);
1328
+ const delivery2 = checkDelivery();
1329
+ if (delivery2.status !== "allow") throw deliveryError(delivery2);
1330
+ const refusal2 = sessionRefusal(prefix);
1331
+ if (refusal2) return await fallback(prefix, "grant_missing", { code: refusal2 });
940
1332
  try {
941
1333
  validateReadMeta(local.meta);
942
1334
  } catch (error) {
943
1335
  const code = errorCode(error);
944
1336
  recordRevocation(prefix, error);
945
- const value = await r.network();
946
- readEvent("get", r.path, prefix, "network", reasonForReadMetaCode(code), value.ok ? "found" : "error", started, { code });
947
- return value;
948
- }
949
- if (local.status === "coverage_incomplete" || local.status === "not_covered" || local.status === "content_missing") {
950
- const value = await r.network();
951
- 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);
952
- return value;
1337
+ return await fallback(prefix, reasonForReadMetaCode(code), { code });
953
1338
  }
1339
+ 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");
954
1340
  if (local.status === "present" && r.options?.maxResponseBytes !== void 0 && local.value.byteLength > r.options.maxResponseBytes) {
955
- const value = await r.network();
956
- readEvent("get", r.path, prefix, "network", "unsupported_option", value.ok ? "found" : "error", started);
957
- return value;
1341
+ return await fallback(prefix, "unsupported_option");
958
1342
  }
959
1343
  const result = localGet(local, r.path, r.options ?? {});
960
1344
  const reason = local.status === "present" ? "hit" : local.status;
@@ -970,11 +1354,11 @@ function createKVReplication(deps) {
970
1354
  readEvent("get", r.path, prefix, "replica", reason, result.ok ? "found" : result.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error", started, event);
971
1355
  return result;
972
1356
  } catch (error) {
1357
+ if (networkRejected) throw error;
973
1358
  recordRevocation(prefix, error);
1359
+ if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
974
1360
  if (r.signal.aborted) return abortedResult(r.signal);
975
- const value = await r.network();
976
- readEvent("get", r.path, prefix, "network", "replica_error", value.ok ? "found" : "error", started, { code: errorCode(error) });
977
- return value;
1361
+ return fallback(prefix, deliveryReason(errorCode(error)), { code: errorCode(error) });
978
1362
  }
979
1363
  }
980
1364
  async function list(r) {
@@ -995,17 +1379,46 @@ function createKVReplication(deps) {
995
1379
  readEvent("list", r.listPath, null, "none", "cursor_restart", "error", started);
996
1380
  return cursorRestart();
997
1381
  }
998
- if (cursor && !cursor.startsWith("tcr1.")) {
999
- const result = await r.network();
1000
- readEvent("list", r.listPath, null, "network", "network_cursor", result.ok ? "found" : "error", started);
1001
- return result;
1382
+ if (cursor && !cursor.startsWith("tcr1.")) return networkRead(r.network, "list", r.listPath, null, "network_cursor", started);
1383
+ const prefix = listPathCovered(options.prefixes, r.listPath);
1384
+ let networkRejected = false;
1385
+ const fallback = async (replica, reason, extra = {}) => {
1386
+ try {
1387
+ return await networkRead(r.network, "list", r.listPath, replica, reason, started, extra);
1388
+ } catch (error) {
1389
+ networkRejected = true;
1390
+ throw error;
1391
+ }
1392
+ };
1393
+ if (prefix !== void 0 && !r.options?.raw) {
1394
+ const delivery = checkDelivery();
1395
+ if (delivery.status !== "allow") {
1396
+ if (decoded) {
1397
+ readEvent("list", r.listPath, prefix, "none", "cursor_restart", "error", started);
1398
+ return cursorRestart();
1399
+ }
1400
+ return fallback(
1401
+ prefix,
1402
+ delivery.status === "consent_expired" ? "grant_expired" : "replica_gated",
1403
+ { code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" }
1404
+ );
1405
+ }
1406
+ }
1407
+ if (prefix !== void 0 && !r.options?.raw) {
1408
+ const refusal = sessionRefusal(prefix);
1409
+ if (refusal) {
1410
+ if (decoded) {
1411
+ readEvent("list", r.listPath, prefix, "none", "cursor_restart", "error", started);
1412
+ return cursorRestart();
1413
+ }
1414
+ return fallback(prefix, "grant_missing", { code: refusal });
1415
+ }
1002
1416
  }
1003
1417
  try {
1004
1418
  await raceSignal(reportExistingPins(), r.signal);
1005
1419
  } catch {
1006
1420
  if (r.signal.aborted) return abortedResult(r.signal);
1007
1421
  }
1008
- const prefix = listPathCovered(options.prefixes, r.listPath);
1009
1422
  const restart = () => {
1010
1423
  readEvent("list", r.listPath, prefix ?? null, "none", "cursor_restart", "error", started);
1011
1424
  return cursorRestart();
@@ -1013,36 +1426,33 @@ function createKVReplication(deps) {
1013
1426
  if (cursor && decoded && (decoded.space !== r.space || decoded.path !== r.listPath)) return restart();
1014
1427
  if (prefix === void 0 || r.options?.raw) {
1015
1428
  if (cursor && decoded) return restart();
1016
- const result = await r.network();
1017
- readEvent("list", r.listPath, prefix ?? null, "network", prefix !== void 0 ? "unsupported_option" : "not_covered", result.ok ? "found" : "error", started);
1018
- return result;
1429
+ return networkRead(r.network, "list", r.listPath, prefix ?? null, prefix !== void 0 ? "unsupported_option" : "not_covered", started);
1019
1430
  }
1020
1431
  let handle;
1021
1432
  try {
1022
1433
  handle = await raceSignal(open(prefix), r.signal);
1023
1434
  } catch (error) {
1024
1435
  if (r.signal.aborted) return abortedResult(r.signal);
1436
+ if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
1025
1437
  if (cursor && decoded) return restart();
1026
- const result = await r.network();
1027
- readEvent("list", r.listPath, prefix, "network", errorCode(error) === "RUNTIME_UNSUPPORTED" ? "runtime_unsupported" : "replica_unavailable", result.ok ? "found" : "error", started, { code: errorCode(error) });
1028
- return result;
1438
+ const code = errorCode(error);
1439
+ return fallback(prefix, code === "RUNTIME_UNSUPPORTED" ? "runtime_unsupported" : code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
1029
1440
  }
1030
1441
  if (replicas.get(prefix)?.reason === "grant_missing") {
1031
1442
  if (cursor && decoded) return restart();
1032
- const result = await r.network();
1033
- readEvent("list", r.listPath, prefix, "network", "grant_missing", result.ok ? "found" : "error", started);
1034
- return result;
1443
+ const code = replicas.get(prefix)?.lastError;
1444
+ return fallback(prefix, code === "GRANT_EXPIRED" ? "grant_expired" : code === "REPLICA_GATED" ? "replica_gated" : "grant_missing", { code });
1035
1445
  }
1036
1446
  let fresh;
1037
1447
  try {
1038
1448
  fresh = await freshness(prefix, handle, r.signal);
1039
1449
  } catch (error) {
1040
1450
  recordRevocation(prefix, error);
1451
+ if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
1041
1452
  if (r.signal.aborted) return abortedResult(r.signal);
1042
1453
  if (cursor && decoded) return restart();
1043
- const result = await r.network();
1044
- readEvent("list", r.listPath, prefix, "network", "replica_error", result.ok ? "found" : "error", started, { code: errorCode(error) });
1045
- return result;
1454
+ const code = errorCode(error);
1455
+ return fallback(prefix, code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_error", { code });
1046
1456
  }
1047
1457
  if (r.signal.aborted) return abortedResult(r.signal);
1048
1458
  let state;
@@ -1051,41 +1461,35 @@ function createKVReplication(deps) {
1051
1461
  } catch (error) {
1052
1462
  if (r.signal.aborted) return abortedResult(r.signal);
1053
1463
  if (cursor && decoded) return restart();
1054
- const result = await r.network();
1055
- readEvent("list", r.listPath, prefix, "network", "replica_unavailable", result.ok ? "found" : "error", started, { code: errorCode(error) });
1056
- return result;
1464
+ const code = errorCode(error);
1465
+ return fallback(prefix, code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
1057
1466
  }
1058
- const pendingRange = state.records.find((record) => listRangeCovers(r.listPath, record.key) && (!decoded || utf8Compare(record.key, decoded.last) > 0));
1467
+ const pendingRange = state.records.find((record) => listRangeCovers(r.listPath, record.key));
1059
1468
  const behind = fresh.status.syncedThroughEpoch < state.committedEpoch;
1060
1469
  if (cursor && decoded && (fresh.failure || pendingRange || behind || fresh.status.coverage !== "complete" || fresh.status.authority.state !== "valid" || !pending.durable && !inProcessProof.has(prefix))) return restart();
1061
- if (behind) {
1062
- const result = await r.network();
1063
- readEvent("list", r.listPath, prefix, "network", "REPLICA_BEHIND_OWN_WRITES", result.ok ? "found" : "error", started);
1064
- return result;
1065
- }
1470
+ if (behind) return fallback(prefix, "REPLICA_BEHIND_OWN_WRITES");
1066
1471
  if (fresh.failure) {
1067
- const result = await r.network();
1068
- readEvent("list", r.listPath, prefix, "network", "stale", result.ok ? "found" : "error", started, { code: fresh.failure === "busy" ? "REPLICA_BUSY" : fresh.syncError });
1069
- return result;
1070
- }
1071
- if (pendingRange) {
1072
- const result = await r.network();
1073
- readEvent("list", r.listPath, prefix, "network", "pending_write", result.ok ? "found" : "error", started, { pendingState: pendingRange.state });
1074
- return result;
1075
- }
1076
- if (!pending.durable && !inProcessProof.has(prefix)) {
1077
- const result = await r.network();
1078
- readEvent("list", r.listPath, prefix, "network", "REPLICA_UNPROVEN_SINCE_START", result.ok ? "found" : "error", started);
1079
- return result;
1080
- }
1081
- if (fresh.status.coverage !== "complete" || fresh.status.authority.state !== "valid") {
1082
- const result = await r.network();
1083
- 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);
1084
- return result;
1472
+ const code = fresh.failure === "busy" ? "REPLICA_BUSY" : fresh.syncError;
1473
+ return fallback(prefix, code === "GRANT_REVOKED" ? "grant_revoked" : code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "stale", { code });
1085
1474
  }
1475
+ if (pendingRange) return fallback(prefix, "pending_write", { pendingState: pendingRange.state });
1476
+ if (!pending.durable && !inProcessProof.has(prefix)) return fallback(prefix, "REPLICA_UNPROVEN_SINCE_START");
1477
+ 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");
1086
1478
  try {
1087
1479
  const signalHandle = { list: (o) => raceSignal(handle.list(o), r.signal) };
1088
1480
  const page = await raceSignal(localList(signalHandle, r.space, r.listPath, r.options?.limit, cursor, validateReadMeta), r.signal);
1481
+ const latest = await raceSignal(pending.read(), r.signal);
1482
+ const laterPending = latest.records.find((record) => listRangeCovers(r.listPath, record.key));
1483
+ if (cursor && decoded && (laterPending || fresh.status.syncedThroughEpoch < latest.committedEpoch)) return restart();
1484
+ if (fresh.status.syncedThroughEpoch < latest.committedEpoch) return await fallback(prefix, "REPLICA_BEHIND_OWN_WRITES");
1485
+ if (laterPending) return await fallback(prefix, "pending_write", { pendingState: laterPending.state });
1486
+ const delivery = checkDelivery();
1487
+ if (delivery.status !== "allow") throw deliveryError(delivery);
1488
+ const refusal = sessionRefusal(prefix);
1489
+ if (refusal) {
1490
+ if (decoded) return restart();
1491
+ return await fallback(prefix, "grant_missing", { code: refusal });
1492
+ }
1089
1493
  const event = {
1090
1494
  count: page.keys.length,
1091
1495
  syncedBeforeRead: fresh.syncedBeforeRead,
@@ -1096,14 +1500,14 @@ function createKVReplication(deps) {
1096
1500
  readEvent("list", r.listPath, prefix, "replica", "hit", "found", started, event);
1097
1501
  return ok(page);
1098
1502
  } catch (error) {
1503
+ if (networkRejected) throw error;
1099
1504
  if (r.signal.aborted) return abortedResult(r.signal);
1100
1505
  const code = errorCode(error);
1506
+ if (code === "CONSENT_EXPIRED") startConsentDenial();
1101
1507
  recordRevocation(prefix, error);
1102
1508
  if (cursor && decoded) return restart();
1103
- const reason = reasonForReadMetaCode(code);
1104
- const result = await r.network();
1105
- readEvent("list", r.listPath, prefix, "network", reason, result.ok ? "found" : "error", started, { code });
1106
- return result;
1509
+ const reason = code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : reasonForReadMetaCode(code);
1510
+ return fallback(prefix, reason, { code });
1107
1511
  }
1108
1512
  }
1109
1513
  async function write(r) {
@@ -1112,11 +1516,14 @@ function createKVReplication(deps) {
1112
1516
  return prefix !== void 0 ? [{ key: entry.path, op: r.op === "delete" ? "delete" : "put" }] : [];
1113
1517
  });
1114
1518
  if (covered.length === 0) return r.network();
1519
+ if (checkDelivery().status !== "allow") return r.network();
1115
1520
  await reportExistingPins();
1521
+ if (checkDelivery().status !== "allow") return r.network();
1116
1522
  const opId = globalThis.crypto?.randomUUID?.() ?? `${scheduler.now()}-${Math.random()}`;
1117
1523
  try {
1118
1524
  await pending.update((s) => begin(s, opId, covered, nowDate(scheduler.now())));
1119
1525
  } catch (error) {
1526
+ if (errorCode(error) === "REPLICA_GATED" || errorCode(error) === "CONSENT_EXPIRED" || checkDelivery().status !== "allow") return r.network();
1120
1527
  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 } }));
1121
1528
  }
1122
1529
  const started = scheduler.now();
@@ -1181,8 +1588,32 @@ function createKVReplication(deps) {
1181
1588
  for (const inspection of active) inspection.abort.abort(Object.assign(new Error("Replica status inspection cancelled"), { code: "ABORTED" }));
1182
1589
  return Promise.all(active.map((inspection) => inspection.settled)).then(() => void 0);
1183
1590
  }
1591
+ function gatedStatus(decision) {
1592
+ return options.prefixes.map((prefix) => ({
1593
+ prefix,
1594
+ state: "gated",
1595
+ reason: "replica_gated",
1596
+ gate: decision.status === "gated" ? decision.gate : "stamp_stale",
1597
+ errorCode: decision.status === "consent_expired" ? decision.code : "REPLICA_GATED",
1598
+ pending: { inFlight: 0, committed: 0, ambiguous: 0 },
1599
+ pinned: [],
1600
+ lagMs: null,
1601
+ redacted: true
1602
+ }));
1603
+ }
1184
1604
  async function status() {
1185
- const persistedSnapshot = pending.readExisting ? await pending.readExisting() : await pending.read();
1605
+ const beforeStatus = checkDelivery();
1606
+ if (beforeStatus.status !== "allow") return gatedStatus(beforeStatus);
1607
+ let persistedSnapshot;
1608
+ try {
1609
+ persistedSnapshot = pending.readExisting ? await pending.readExisting() : await pending.read();
1610
+ } catch (error) {
1611
+ const delivery = checkDelivery();
1612
+ if (delivery.status !== "allow") return gatedStatus(delivery);
1613
+ throw error;
1614
+ }
1615
+ const afterPending = checkDelivery();
1616
+ if (afterPending.status !== "allow") return gatedStatus(afterPending);
1186
1617
  const snapshot = persistedSnapshot ?? emptyPendingState(identity);
1187
1618
  const entries = await Promise.all(options.prefixes.map(async (prefix) => {
1188
1619
  if (prefix === "/" && deps.nodeFeatures !== void 0 && !deps.nodeFeatures.includes("kv-sync-root")) {
@@ -1204,7 +1635,7 @@ function createKVReplication(deps) {
1204
1635
  const full = local && !("redacted" in local) ? local : void 0;
1205
1636
  const revoked = opened?.authorityRevoked === true || full?.authority.state === "revoked";
1206
1637
  const rootUnsupported = opened?.lastError === "REPLICATION_ROOT_UNSUPPORTED";
1207
- 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";
1638
+ 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";
1208
1639
  return {
1209
1640
  prefix,
1210
1641
  state,
@@ -1221,6 +1652,8 @@ function createKVReplication(deps) {
1221
1652
  lagMs: full?.lastSyncAt ? scheduler.now() - Date.parse(full.lastSyncAt) : null
1222
1653
  };
1223
1654
  }));
1655
+ const beforeReturn = checkDelivery();
1656
+ if (beforeReturn.status !== "allow") return gatedStatus(beforeReturn);
1224
1657
  return entries.filter((entry) => entry !== void 0);
1225
1658
  }
1226
1659
  async function sync(o) {
@@ -1236,11 +1669,15 @@ function createKVReplication(deps) {
1236
1669
  var _a;
1237
1670
  purgeStarted = true;
1238
1671
  purgeInProgress = true;
1672
+ cancelConsentTimer?.();
1673
+ cancelConsentTimer = void 0;
1239
1674
  const hasStatusInspections = statusInspections.size > 0;
1240
1675
  const statusDrain = cancelStatusInspections();
1241
1676
  for (const state of replicas.values()) {
1242
1677
  state.timer?.();
1243
1678
  state.timer = void 0;
1679
+ state.renewalTimer?.();
1680
+ state.renewalTimer = void 0;
1244
1681
  state.syncContext && ((_a = state.syncContext).abortCause ?? (_a.abortCause = "purge"));
1245
1682
  state.abort?.abort();
1246
1683
  state.mintAbort?.abort();
@@ -1264,6 +1701,7 @@ function createKVReplication(deps) {
1264
1701
  ...[...replicas.values()].flatMap((state) => [
1265
1702
  state.opening?.catch(() => void 0),
1266
1703
  state.sync?.catch(() => void 0),
1704
+ state.renewing?.catch(() => void 0),
1267
1705
  state.handle?.close().catch(() => void 0)
1268
1706
  ]),
1269
1707
  ...hasStatusInspections ? [statusDrain] : []
@@ -1301,11 +1739,15 @@ function createKVReplication(deps) {
1301
1739
  var _a;
1302
1740
  if (isClosed) return;
1303
1741
  isClosed = true;
1742
+ cancelConsentTimer?.();
1743
+ cancelConsentTimer = void 0;
1304
1744
  const hasStatusInspections = statusInspections.size > 0;
1305
1745
  const statusDrain = cancelStatusInspections();
1306
1746
  for (const [prefix, state] of replicas) {
1307
1747
  state.timer?.();
1308
1748
  state.timer = void 0;
1749
+ state.renewalTimer?.();
1750
+ state.renewalTimer = void 0;
1309
1751
  state.mintAbort?.abort();
1310
1752
  state.syncContext && ((_a = state.syncContext).abortCause ?? (_a.abortCause = "close"));
1311
1753
  state.abort?.abort();
@@ -1314,6 +1756,7 @@ function createKVReplication(deps) {
1314
1756
  const drain = Promise.all([
1315
1757
  ...[...replicas.values()].map(async (state) => {
1316
1758
  await state.opening?.catch(() => void 0);
1759
+ await state.renewing?.catch(() => void 0);
1317
1760
  await state.sync?.catch(() => void 0);
1318
1761
  await state.handle?.close().catch(() => void 0);
1319
1762
  }),
@@ -1343,7 +1786,8 @@ function createKVReplication(deps) {
1343
1786
  cancel();
1344
1787
  }
1345
1788
  }
1346
- return { get, list, write, observeNetworkRequested, status, sync, purge, clearPending: clearPending2, close };
1789
+ scheduleConsentDeadline();
1790
+ return { get, list, write, observeNetworkRequested, observeConsentExpired, status, sync, purge, clearPending: clearPending2, close };
1347
1791
  }
1348
1792
  function cancellationCode(signal) {
1349
1793
  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;