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

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