@tinycloud/node-sdk 3.8.0-beta.0 → 3.8.0-beta.10

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 (39) hide show
  1. package/dist/{TinyCloudNode-BtKzLDJX.d.ts → TinyCloudNode-DnD-SH6t.d.ts} +27 -4
  2. package/dist/{TinyCloudNode-ggmlnSfI.d.cts → TinyCloudNode-Dy65bxqs.d.cts} +27 -4
  3. package/dist/{authority-sJ6buHMj.d.ts → authority-BQnTX3fI.d.ts} +21 -5
  4. package/dist/{authority-_SYAE-uV.d.cts → authority-Rbye1U4o.d.cts} +21 -5
  5. package/dist/core.cjs +196 -29
  6. package/dist/core.cjs.map +1 -1
  7. package/dist/core.d.cts +5 -4
  8. package/dist/core.d.ts +5 -4
  9. package/dist/core.js +190 -25
  10. package/dist/core.js.map +1 -1
  11. package/dist/credentials-core.d.cts +1 -1
  12. package/dist/credentials-core.d.ts +1 -1
  13. package/dist/credentials.d.cts +1 -1
  14. package/dist/credentials.d.ts +1 -1
  15. package/dist/{delegation-0LKZkmP2.d.cts → delegation-Ckc3UETl.d.cts} +1 -1
  16. package/dist/{delegation-0LKZkmP2.d.ts → delegation-Ckc3UETl.d.ts} +1 -1
  17. package/dist/encryption-keys.d.cts +2 -2
  18. package/dist/encryption-keys.d.ts +2 -2
  19. package/dist/index.cjs +249 -41
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +4 -3
  22. package/dist/index.d.ts +4 -3
  23. package/dist/index.js +236 -28
  24. package/dist/index.js.map +1 -1
  25. package/dist/replication/authority.cjs +43 -1
  26. package/dist/replication/authority.cjs.map +1 -1
  27. package/dist/replication/authority.d.cts +4 -3
  28. package/dist/replication/authority.d.ts +4 -3
  29. package/dist/replication/authority.js +42 -1
  30. package/dist/replication/authority.js.map +1 -1
  31. package/dist/replication/runtime.cjs +1 -0
  32. package/dist/replication/runtime.cjs.map +1 -1
  33. package/dist/replication/runtime.d.cts +1 -1
  34. package/dist/replication/runtime.d.ts +1 -1
  35. package/dist/replication/runtime.js +1 -0
  36. package/dist/replication/runtime.js.map +1 -1
  37. package/dist/{runtime-v62FzQHz.d.ts → runtime-CO4VXnPq.d.ts} +2 -1
  38. package/dist/{runtime-D1o9Eybt.d.cts → runtime-mRhSu-vJ.d.cts} +2 -1
  39. package/package.json +4 -4
package/dist/index.cjs CHANGED
@@ -17053,7 +17053,7 @@ function augmentSignInEntriesWithReplication(input) {
17053
17053
  }
17054
17054
  eligiblePaths.push(path);
17055
17055
  }
17056
- if (eligiblePaths.length === replication.prefixes.length && eligiblePaths.length > 0 && hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, "")) {
17056
+ if (replication.syncScope !== "prefixes" && eligiblePaths.length === replication.prefixes.length && eligiblePaths.length > 0 && hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, "")) {
17057
17057
  return [{
17058
17058
  service: KV_SERVICE,
17059
17059
  space: input.primarySpaceId,
@@ -17086,6 +17086,12 @@ var init_authority_sign_in = __esm({
17086
17086
 
17087
17087
  // src/replication/config.ts
17088
17088
  function assertValidReplicationConfig(replication) {
17089
+ if (replication?.syncScope !== void 0 && replication.syncScope !== "prefixes") {
17090
+ throw new TypeError('replication.syncScope must be "prefixes"');
17091
+ }
17092
+ if (replication?.contentFetch !== void 0 && replication.contentFetch !== "eager" && replication.contentFetch !== "lazy") {
17093
+ throw new TypeError('replication.contentFetch must be "eager" or "lazy"');
17094
+ }
17089
17095
  if (replication === void 0 || !replication.enabled) return;
17090
17096
  if (replication.storage === void 0 || replication.storage === null) {
17091
17097
  throw new TypeError("replication.enabled requires replication.storage");
@@ -17140,6 +17146,7 @@ function resolvedOptions(options) {
17140
17146
  maxStalenessMs: options.maxStalenessMs ?? 12e4,
17141
17147
  staleSyncTimeoutMs: options.staleSyncTimeoutMs ?? 5e3,
17142
17148
  allowSecrets: options.allowSecrets === true,
17149
+ contentFetch: options.contentFetch ?? "eager",
17143
17150
  verify: options.verify === true
17144
17151
  };
17145
17152
  }
@@ -17411,6 +17418,7 @@ __export(authority_exports, {
17411
17418
  assertValidReplicationConfig: () => assertValidReplicationConfig,
17412
17419
  augmentSignInEntriesWithReplication: () => augmentSignInEntriesWithReplication,
17413
17420
  createReplicationAuthority: () => createReplicationAuthority,
17421
+ eraseReplicaLocalCopy: () => eraseReplicaLocalCopy,
17414
17422
  establishReplicaSessionStamp: () => establishReplicaSessionStamp,
17415
17423
  hasUnrestrictedGetCoverage: () => hasUnrestrictedGetCoverage,
17416
17424
  mintNodeReplicationGrant: () => mintNodeReplicationGrant,
@@ -17441,6 +17449,38 @@ function reportReplicaDenial(result, reason, onEvent, context) {
17441
17449
  } catch {
17442
17450
  }
17443
17451
  }
17452
+ async function eraseReplicaLocalCopy(lifecycle, trigger, denyStatus, handlers) {
17453
+ if (lifecycle?.kind !== "indexeddb" || lifecycle.eraseLocalCopy === void 0) return void 0;
17454
+ let outcome = { status: "failed", code: "STORAGE_ERROR", marker: false };
17455
+ for (let attempt = 0; attempt <= ERASE_RETRY_DELAYS_MS.length; attempt++) {
17456
+ if (attempt > 0) await new Promise((resolve2) => setTimeout(resolve2, ERASE_RETRY_DELAYS_MS[attempt - 1]));
17457
+ try {
17458
+ outcome = await lifecycle.eraseLocalCopy({ signal: AbortSignal.timeout(ERASE_ATTEMPT_MS) });
17459
+ } catch {
17460
+ outcome = { status: "failed", code: "STORAGE_ERROR", marker: false };
17461
+ }
17462
+ if (outcome.status !== "failed") break;
17463
+ }
17464
+ const keysKept = denyStatus === "marker" || denyStatus === "unguarded";
17465
+ const purge = outcome.status === "erased" ? { ok: true } : { ok: false, retryNow: outcome.status === "failed" };
17466
+ if (outcome.status === "erased" || outcome.status === "refused" && !keysKept) return { outcome, purge };
17467
+ const logoutPending = outcome.status === "failed" && outcome.marker;
17468
+ const event = {
17469
+ type: "replica.purge.failed",
17470
+ trigger,
17471
+ reason: logoutPending ? "logout_pending" : "gate_unavailable",
17472
+ code: outcome.code,
17473
+ retryNow: outcome.status === "failed"
17474
+ };
17475
+ for (const handler of handlers) {
17476
+ try {
17477
+ handler(event);
17478
+ } catch (error) {
17479
+ console.error('Error in "replica.purge.failed" handler:', error);
17480
+ }
17481
+ }
17482
+ return logoutPending ? { outcome, purge, logoutPending } : { outcome, purge };
17483
+ }
17444
17484
  function verifiedOwnerConsentAnchor(session, wasmBindings, exactVerifiedRecap2) {
17445
17485
  if (typeof session.siwe !== "string" || typeof session.signature !== "string" || typeof session.address !== "string" || typeof session.chainId !== "number") return null;
17446
17486
  try {
@@ -17699,7 +17739,7 @@ function createReplicationAuthority(host) {
17699
17739
  };
17700
17740
  return authority;
17701
17741
  }
17702
- var import_siwe3, import_sdk_core12, import_replica, parseUcanGrant, KV_GET2, KV_SYNC2, KV_SERVICE2, AUTHORITY_EXPIRY_MARGIN_MS;
17742
+ var import_siwe3, import_sdk_core12, import_replica, parseUcanGrant, ERASE_RETRY_DELAYS_MS, ERASE_ATTEMPT_MS, KV_GET2, KV_SYNC2, KV_SERVICE2, AUTHORITY_EXPIRY_MARGIN_MS;
17703
17743
  var init_authority = __esm({
17704
17744
  "src/replication/authority.ts"() {
17705
17745
  "use strict";
@@ -17709,6 +17749,8 @@ var init_authority = __esm({
17709
17749
  init_config();
17710
17750
  init_authority_sign_in();
17711
17751
  parseUcanGrant = import_replica.parseUcanGrant;
17752
+ ERASE_RETRY_DELAYS_MS = [150, 450];
17753
+ ERASE_ATTEMPT_MS = 3e3;
17712
17754
  KV_GET2 = import_sdk_core12.KV.GET;
17713
17755
  KV_SYNC2 = import_sdk_core12.KV.SYNC;
17714
17756
  KV_SERVICE2 = "tinycloud.kv";
@@ -18786,13 +18828,42 @@ function getNodeReplicationLoaders() {
18786
18828
 
18787
18829
  // src/TinyCloudNode.ts
18788
18830
  var import_sdk_core10 = require("@tinycloud/sdk-core");
18789
- var import_sdk_services3 = require("@tinycloud/sdk-services");
18831
+ var import_sdk_services4 = require("@tinycloud/sdk-services");
18790
18832
  var import_requestLog2 = require("@tinycloud/sdk-services/telemetry/requestLog");
18833
+ var import_resource2 = require("@tinycloud/sdk-services/encryption/resource");
18791
18834
 
18792
18835
  // src/authorization/NodeUserAuthorization.ts
18793
18836
  var import_node_crypto = require("crypto");
18794
18837
  var import_siwe = require("siwe");
18795
18838
  var import_sdk_core5 = require("@tinycloud/sdk-core");
18839
+ var import_resource = require("@tinycloud/sdk-services/encryption/resource");
18840
+ var import_sdk_services3 = require("@tinycloud/sdk-services");
18841
+
18842
+ // src/authorization/appKeyHostBinding.ts
18843
+ var DOMAIN_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
18844
+ var IPV4 = /^\d+\.\d+\.\d+\.\d+$/;
18845
+ var APP_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
18846
+ function appKeyHostBinding(domain, appId) {
18847
+ if (!APP_ID.test(appId)) return false;
18848
+ if (domain.endsWith(":")) return false;
18849
+ let url;
18850
+ try {
18851
+ url = new URL(`https://${domain}`);
18852
+ } catch {
18853
+ return false;
18854
+ }
18855
+ if (url.protocol !== "https:" || url.port !== "" || url.username !== "" || url.password !== "") return false;
18856
+ if (url.pathname !== "/" || url.search !== "" || url.hash !== "") return false;
18857
+ const host = url.hostname;
18858
+ if (host.startsWith("[") || host.endsWith(".") || IPV4.test(host)) return false;
18859
+ const labels = host.split(".");
18860
+ if (labels.length < 2 || labels[labels.length - 1] === "localhost") return false;
18861
+ if (!labels.every((label) => DOMAIN_LABEL.test(label))) return false;
18862
+ const reversed = labels.reverse().join(".");
18863
+ return appId === reversed || appId.startsWith(`${reversed}.`);
18864
+ }
18865
+
18866
+ // src/authorization/NodeUserAuthorization.ts
18796
18867
  init_authority_sign_in();
18797
18868
 
18798
18869
  // src/authorization/caveatRefusal.ts
@@ -19304,6 +19375,23 @@ var NodeUserAuthorization = class {
19304
19375
  );
19305
19376
  return this._capabilityRequest;
19306
19377
  }
19378
+ async refreshNamedKeyFeatureForSignIn(address, chainId) {
19379
+ const request = this.getCapabilityRequest();
19380
+ const siweDomain = this.siweConfig?.domain ?? this.domain;
19381
+ const hasNamedKeys = request?.manifests.some((manifest) => manifest.keys?.length && appKeyHostBinding(siweDomain, manifest.keys_app_id ?? manifest.app_id));
19382
+ if (hasNamedKeys) {
19383
+ await this.resolveTinyCloudHostsForSignIn(address, chainId);
19384
+ await this.refreshNodeInfo();
19385
+ }
19386
+ }
19387
+ /** Manifest key paths that this sign-in cannot grant. */
19388
+ get unavailableKeyPaths() {
19389
+ const request = this.getCapabilityRequest();
19390
+ const siweDomain = this.siweConfig?.domain ?? this.domain;
19391
+ return (request?.manifests ?? []).flatMap(
19392
+ (manifest) => this._nodeFeatures.includes("encryption-named-keys-v1") && appKeyHostBinding(siweDomain, manifest.keys_app_id ?? manifest.app_id) ? [] : (manifest.keys ?? []).map((entry) => "path" in entry ? entry.path : entry.prefix)
19393
+ );
19394
+ }
19307
19395
  resolveSpaceName(space, address, chainId) {
19308
19396
  if (space.startsWith("tinycloud:")) {
19309
19397
  return space;
@@ -19495,15 +19583,31 @@ var NodeUserAuthorization = class {
19495
19583
  }
19496
19584
  const rawAbilities = {};
19497
19585
  const currentDid = (0, import_sdk_core5.pkhDid)(address, chainId);
19586
+ if (this._nodeFeatures.includes("encryption-named-keys-v1")) {
19587
+ const siweDomain = this.siweConfig?.domain ?? this.domain;
19588
+ for (const manifest of request.manifests) {
19589
+ if (!appKeyHostBinding(siweDomain, manifest.keys_app_id ?? manifest.app_id)) continue;
19590
+ for (const declaration of manifest.keys ?? []) {
19591
+ const path = "path" in declaration ? declaration.path : declaration.prefix;
19592
+ const resource = `urn:tinycloud:encryption:${currentDid}:default/${path}`;
19593
+ for (const ability of declaration.abilities) {
19594
+ addRawAbility(rawAbilities, resource, `tinycloud.encryption/${ability}`);
19595
+ }
19596
+ }
19597
+ }
19598
+ }
19498
19599
  const spaceResources = request.resources.filter((entry) => {
19499
19600
  if (entry.service !== import_sdk_core5.ENCRYPTION_PERMISSION_SERVICE) {
19500
19601
  return true;
19501
19602
  }
19603
+ const parsed = entry.path.includes("/") ? (0, import_resource.parseEncryptionResource)(entry.path) : { ...(0, import_sdk_services3.parseNetworkId)(entry.path), kind: "network" };
19604
+ if (parsed.kind !== "network") {
19605
+ throw new import_sdk_core5.ManifestValidationError("named keys go in manifest.keys");
19606
+ }
19502
19607
  for (const action of entry.actions) {
19503
19608
  addRawAbility(rawAbilities, entry.path, action);
19504
19609
  }
19505
- if (entry.actions.includes(DECRYPT_ACTION)) {
19506
- const parsed = (0, import_sdk_core5.parseNetworkId)(entry.path);
19610
+ if (entry.actions.includes(DECRYPT_ACTION) && parsed.kind === "network") {
19507
19611
  if (didPrincipalMatches(parsed.ownerDid, currentDid)) {
19508
19612
  addRawAbility(rawAbilities, entry.path, NETWORK_CREATE_ACTION);
19509
19613
  }
@@ -19899,6 +20003,8 @@ var NodeUserAuthorization = class {
19899
20003
  this._chainId = chainId;
19900
20004
  await this.resolveTinyCloudHostsForSignIn(address, chainId);
19901
20005
  this.assertSignInCurrent(generation);
20006
+ await this.refreshNamedKeyFeatureForSignIn(address, chainId);
20007
+ this.assertSignInCurrent(generation);
19902
20008
  const keyId = `session-${Date.now()}`;
19903
20009
  this.sessionManager.renameSessionKeyId(this.activeSessionKeyId, keyId);
19904
20010
  this.activeSessionKeyId = keyId;
@@ -20045,6 +20151,7 @@ var NodeUserAuthorization = class {
20045
20151
  refuseCaveated(this.getCapabilityRequest()?.manifests);
20046
20152
  const address = (0, import_sdk_core5.canonicalizeAddress)(await this.signer.getAddress());
20047
20153
  const chainId = await this.signer.getChainId();
20154
+ await this.refreshNamedKeyFeatureForSignIn(address, chainId);
20048
20155
  const keyId = `session-${Date.now()}`;
20049
20156
  this.sessionManager.renameSessionKeyId(this.activeSessionKeyId, keyId);
20050
20157
  this.activeSessionKeyId = keyId;
@@ -21098,6 +21205,7 @@ var ROOT_DELEGATION_ACTIONS = [
21098
21205
  import_sdk_core10.DUCKDB.ALL
21099
21206
  ];
21100
21207
  var DEFAULT_SESSION_EXPIRATION_MS = import_sdk_core10.EXPIRY.SESSION_MS;
21208
+ var OWNER_REVOCATION_WINDOW_MS = 10 * 6e4;
21101
21209
  function unifiedRootAttenuation(capabilities) {
21102
21210
  const attenuation = {};
21103
21211
  for (const capability of capabilities) {
@@ -21562,6 +21670,7 @@ function replicaRestoreRefusal(error) {
21562
21670
  case "purge_pending":
21563
21671
  case "logout_pending":
21564
21672
  case "gate_unavailable":
21673
+ case "wallet_provider_required":
21565
21674
  return error.gate;
21566
21675
  }
21567
21676
  }
@@ -21596,6 +21705,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21596
21705
  this.auth = null;
21597
21706
  this.tc = null;
21598
21707
  this._chainId = 1;
21708
+ this.replicaPurgeFailedHandlers = /* @__PURE__ */ new Set();
21599
21709
  /** Mints the replica session stamp on each fresh sign-in (TC-858 §4.1). Undefined when replication is off. */
21600
21710
  this.establishReplicaStamp = async (session) => {
21601
21711
  const replication = this.config.replication;
@@ -21627,6 +21737,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21627
21737
  return void 0;
21628
21738
  }
21629
21739
  };
21740
+ this._keysUnavailable = [];
21630
21741
  this._baseVaults = /* @__PURE__ */ new Map();
21631
21742
  this._baseSecrets = /* @__PURE__ */ new Map();
21632
21743
  this._secrets = /* @__PURE__ */ new Map();
@@ -21802,6 +21913,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21802
21913
  return instance ? instance.control.purge(input) : runtime.then((ready) => ready.control.purge(input));
21803
21914
  },
21804
21915
  clearPending: async () => (await runtime).control.clearPending(),
21916
+ purgeLocalCopy: async () => (await this.eraseReplicaLocalCopy("purge_local_copy"))?.purge ?? { ok: false, retryNow: false },
21805
21917
  close: async () => (await runtime).control.close()
21806
21918
  };
21807
21919
  }
@@ -21856,18 +21968,14 @@ var _TinyCloudNode = class _TinyCloudNode {
21856
21968
  get freshEncryptionNetworkCreated() {
21857
21969
  return this._freshEncryptionNetworkCreated;
21858
21970
  }
21859
- /**
21860
- * Subscribe to `storage.full`. It fires on the first write any service had
21861
- * rejected because the owner's TinyCloud storage is full
21862
- * (`STORAGE_QUOTA_EXCEEDED` or `STORAGE_LIMIT_REACHED`), so an app can show
21863
- * one read-only banner. Reads keep working. For the signed-in account's own
21864
- * storage it fires once per episode: the episode ends only when a write to
21865
- * that storage succeeds, or on sign-out. A `storage.status()` read does not
21866
- * end it. A rejection on another owner's delegated space fires once per
21867
- * owner, until that owner's `storage.status()` reports space again. Returns
21868
- * the unsubscribe function.
21869
- */
21870
21971
  on(event, handler) {
21972
+ if (event === "replica.purge.failed") {
21973
+ const listener = handler;
21974
+ this.replicaPurgeFailedHandlers.add(listener);
21975
+ return () => {
21976
+ this.replicaPurgeFailedHandlers.delete(listener);
21977
+ };
21978
+ }
21871
21979
  if (event !== "storage.full") {
21872
21980
  throw new Error(`Unknown TinyCloud event: ${String(event)}`);
21873
21981
  }
@@ -22087,7 +22195,8 @@ var _TinyCloudNode = class _TinyCloudNode {
22087
22195
  replication: config.replication?.enabled === true ? {
22088
22196
  enabled: true,
22089
22197
  prefixes: config.replication.prefixes,
22090
- allowSecrets: config.replication.allowSecrets === true
22198
+ allowSecrets: config.replication.allowSecrets === true,
22199
+ syncScope: config.replication.syncScope
22091
22200
  } : void 0
22092
22201
  });
22093
22202
  this.tc = new import_sdk_core10.TinyCloud(this.auth, {
@@ -22347,6 +22456,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22347
22456
  const leavingAuth = this.auth;
22348
22457
  const leavingContext = this._serviceContext;
22349
22458
  let denied;
22459
+ let erased;
22350
22460
  try {
22351
22461
  this.teardownSession();
22352
22462
  } finally {
@@ -22357,6 +22467,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22357
22467
  try {
22358
22468
  await this.accountRegistryTail;
22359
22469
  await this.quiesceReplicaRuntime();
22470
+ if (denied) erased = await this.eraseReplicaLocalCopy("sign_out", denied.status);
22360
22471
  } finally {
22361
22472
  if (leavingAddress) {
22362
22473
  await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
@@ -22364,8 +22475,37 @@ var _TinyCloudNode = class _TinyCloudNode {
22364
22475
  }
22365
22476
  }
22366
22477
  }
22367
- if (denied?.status === "unguarded") {
22368
- throw new Error(`Replica gate was not denied (${denied.code ?? "unknown"}); the replica may still be restorable.`);
22478
+ if (denied?.status === "unguarded" && erased?.outcome.status !== "erased") {
22479
+ throw Object.assign(
22480
+ new Error(`Replica gate was not denied (${denied.code ?? "unknown"}); the replica may still be restorable.`),
22481
+ {
22482
+ code: "REPLICA_PURGE_FAILED",
22483
+ reason: "gate_unavailable",
22484
+ retryNow: erased?.outcome.status === "failed"
22485
+ }
22486
+ );
22487
+ }
22488
+ }
22489
+ /**
22490
+ * Erases the browser local copy after a deny (retry, event and next-load marker live in the lazy
22491
+ * replication authority). Never rejects; `undefined` when replication is off or the storage keeps
22492
+ * no browser database.
22493
+ */
22494
+ async eraseReplicaLocalCopy(trigger, denyStatus) {
22495
+ const replication = this.config.replication;
22496
+ if (!replication?.enabled) return void 0;
22497
+ try {
22498
+ const { eraseReplicaLocalCopy: eraseReplicaLocalCopy2 } = await getNodeReplicationLoaders().authority();
22499
+ const erased = await eraseReplicaLocalCopy2(
22500
+ replication.storage.lifecycle,
22501
+ trigger,
22502
+ denyStatus,
22503
+ this.replicaPurgeFailedHandlers
22504
+ );
22505
+ if (erased?.logoutPending) this.replicaRestoreGate = "logout_pending";
22506
+ return erased;
22507
+ } catch {
22508
+ return void 0;
22369
22509
  }
22370
22510
  }
22371
22511
  /**
@@ -22425,6 +22565,8 @@ var _TinyCloudNode = class _TinyCloudNode {
22425
22565
  this._hooks = void 0;
22426
22566
  this._vault = void 0;
22427
22567
  this._encryption = void 0;
22568
+ this._namedKeyClient = void 0;
22569
+ this._keysUnavailable = [];
22428
22570
  this._baseVaults.clear();
22429
22571
  this._baseSecrets.clear();
22430
22572
  this._secrets.clear();
@@ -22529,6 +22671,10 @@ var _TinyCloudNode = class _TinyCloudNode {
22529
22671
  const priorPrincipal = this.replicaSessionPrincipal ?? (this.address ? (0, import_sdk_core10.pkhDid)(this.address, this._chainId) : void 0);
22530
22672
  if (this.config.replication?.enabled && priorPrincipal && priorPrincipal !== nextPrincipal) {
22531
22673
  const denial = await this.denyReplicaGate({ reason: "account_switch", principal: priorPrincipal, next: nextPrincipal });
22674
+ if (denial) {
22675
+ await this.quiesceReplicaRuntime();
22676
+ await this.eraseReplicaLocalCopy("account_switch", denial.status);
22677
+ }
22532
22678
  this.replicaRestoreGate = denial?.status === "unguarded" ? "gate_unavailable" : "stamp_stale";
22533
22679
  }
22534
22680
  if (address !== this._address || chainId !== this._chainId) {
@@ -22543,6 +22689,8 @@ var _TinyCloudNode = class _TinyCloudNode {
22543
22689
  this._hooks = void 0;
22544
22690
  this._vault = void 0;
22545
22691
  this._encryption = void 0;
22692
+ this._namedKeyClient = void 0;
22693
+ this._keysUnavailable = [];
22546
22694
  this._baseVaults.clear();
22547
22695
  this._baseSecrets.clear();
22548
22696
  this._secrets.clear();
@@ -22595,6 +22743,26 @@ var _TinyCloudNode = class _TinyCloudNode {
22595
22743
  this.assertSessionEpoch(epoch, "Sign-in");
22596
22744
  await this.ensureRequestedEncryptionNetworks();
22597
22745
  this.assertSessionEpoch(epoch, "Sign-in");
22746
+ this._keysUnavailable = this.auth?.unavailableKeyPaths ?? [];
22747
+ if (this._keysUnavailable.length > 0) {
22748
+ this.notificationHandler.warning(`Named keys unavailable: ${this._keysUnavailable.join(", ")}`);
22749
+ }
22750
+ if (this.nodeFeatures.includes("encryption-named-keys-v1")) {
22751
+ for (const manifest of this.auth?.capabilityRequest?.manifests ?? []) {
22752
+ for (const declaration of manifest.keys ?? []) {
22753
+ if ("path" in declaration && declaration.abilities.includes("key.create") && !this._keysUnavailable.includes(declaration.path)) {
22754
+ try {
22755
+ await this.keys.ensureKey(declaration.path);
22756
+ } catch (error) {
22757
+ this._keysUnavailable.push(declaration.path);
22758
+ const code = error && typeof error === "object" && "code" in error && typeof error.code === "string" && /^[A-Z_]+$/.test(error.code) ? error.code : "UNKNOWN";
22759
+ this.notificationHandler.warning(`Named key unavailable: ${declaration.path} (${code})`);
22760
+ }
22761
+ this.assertSessionEpoch(epoch, "Sign-in");
22762
+ }
22763
+ }
22764
+ }
22765
+ }
22598
22766
  if (!bootstrapped && this.config.manifest === void 0 && this.config.capabilityRequest === void 0) {
22599
22767
  await this.ensureOwnedSpaceHostedById(this.ownedSpaceId(import_sdk_core10.SECRETS_SPACE));
22600
22768
  }
@@ -23171,7 +23339,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23171
23339
  if (!this.currentSessionCoversAccountRegistry()) {
23172
23340
  if (!_TinyCloudNode.accountRegistrySkipLogged) {
23173
23341
  _TinyCloudNode.accountRegistrySkipLogged = true;
23174
- import_sdk_services3.tinyCloudDebugLogger.log("registry.skip");
23342
+ import_sdk_services4.tinyCloudDebugLogger.log("registry.skip");
23175
23343
  }
23176
23344
  return;
23177
23345
  }
@@ -23382,7 +23550,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23382
23550
  const networkIds = /* @__PURE__ */ new Set();
23383
23551
  for (const resource of request.resources) {
23384
23552
  if (resource.service === import_sdk_core10.ENCRYPTION_PERMISSION_SERVICE && resource.path.startsWith("urn:tinycloud:encryption:") && resource.actions.includes(DECRYPT_ACTION2)) {
23385
- networkIds.add(resource.path);
23553
+ const parsed = resource.path.includes("/") ? (0, import_resource2.parseEncryptionResource)(resource.path) : { ...(0, import_sdk_core10.parseNetworkId)(resource.path), kind: "network" };
23554
+ if (parsed.kind === "network") networkIds.add(resource.path);
23386
23555
  }
23387
23556
  }
23388
23557
  return [...networkIds];
@@ -23932,6 +24101,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23932
24101
  this._hooks = stagedGraph.hooks;
23933
24102
  this._vault = stagedGraph.vault;
23934
24103
  this._encryption = stagedGraph.encryption;
24104
+ this._namedKeyClient = void 0;
24105
+ this._keysUnavailable = [];
23935
24106
  this._capabilityRegistry = stagedGraph.capabilityRegistry;
23936
24107
  this._keyProvider = stagedGraph.keyProvider;
23937
24108
  this._sharingService = stagedGraph.sharingService;
@@ -24509,7 +24680,8 @@ var _TinyCloudNode = class _TinyCloudNode {
24509
24680
  replication: this.config.replication?.enabled === true ? {
24510
24681
  enabled: true,
24511
24682
  prefixes: this.config.replication.prefixes,
24512
- allowSecrets: this.config.replication.allowSecrets === true
24683
+ allowSecrets: this.config.replication.allowSecrets === true,
24684
+ syncScope: this.config.replication.syncScope
24513
24685
  } : void 0
24514
24686
  });
24515
24687
  this.tc = new import_sdk_core10.TinyCloud(this.auth, {
@@ -24571,7 +24743,8 @@ var _TinyCloudNode = class _TinyCloudNode {
24571
24743
  replication: this.config.replication?.enabled === true ? {
24572
24744
  enabled: true,
24573
24745
  prefixes: this.config.replication.prefixes,
24574
- allowSecrets: this.config.replication.allowSecrets === true
24746
+ allowSecrets: this.config.replication.allowSecrets === true,
24747
+ syncScope: this.config.replication.syncScope
24575
24748
  } : void 0
24576
24749
  });
24577
24750
  this.tc = new import_sdk_core10.TinyCloud(this.auth, {
@@ -24839,7 +25012,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24839
25012
  );
24840
25013
  graph.assertActive();
24841
25014
  if (!response.ok) {
24842
- const permissionHint = response.status === 401 || response.status === 403 ? (0, import_sdk_services3.parsePermissionHint)({
25015
+ const permissionHint = response.status === 401 || response.status === 403 ? (0, import_sdk_services4.parsePermissionHint)({
24843
25016
  service: "tinycloud.encryption",
24844
25017
  path: networkId,
24845
25018
  actions: ["tinycloud.encryption/decrypt"]
@@ -25283,7 +25456,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25283
25456
  * Sign a wallet (CACAO) revocation of the delegation `cid` for the node's
25284
25457
  * `POST /revoke`, and return its Authorization header value. The node
25285
25458
  * accepts it when the owner delegated `cid` or owns a space its abilities
25286
- * target, whichever key issued it. Valid for 60 seconds. Requires the
25459
+ * target, whichever key issued it. Valid for 10 minutes. Requires the
25287
25460
  * owner wallet signer.
25288
25461
  */
25289
25462
  async signOwnerRevocation(cid) {
@@ -25299,7 +25472,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25299
25472
  chainId: session.chainId,
25300
25473
  nonce: (0, import_siwe2.generateNonce)(),
25301
25474
  issuedAt: issuedAt.toISOString(),
25302
- expirationTime: new Date(issuedAt.getTime() + 6e4).toISOString()
25475
+ expirationTime: new Date(issuedAt.getTime() + OWNER_REVOCATION_WINDOW_MS).toISOString()
25303
25476
  }).prepareMessage();
25304
25477
  const signature2 = await this.signer.signMessage(siwe);
25305
25478
  return this.wasmBindings.siweToDelegationHeaders({ siwe, signature: signature2 }).Authorization;
@@ -25526,6 +25699,40 @@ var _TinyCloudNode = class _TinyCloudNode {
25526
25699
  get encryption() {
25527
25700
  return this.getEncryptionService();
25528
25701
  }
25702
+ /** Named keys bound to the current session. */
25703
+ get keys() {
25704
+ return new Proxy({}, {
25705
+ get: (_target, property) => {
25706
+ if (property === "refreshNodeInfo") {
25707
+ return () => {
25708
+ void this._namedKeyClient?.then((client) => client.refreshNodeInfo());
25709
+ };
25710
+ }
25711
+ if (typeof property !== "string" || ![
25712
+ "getPublicKey",
25713
+ "describeKey",
25714
+ "createKey",
25715
+ "blockKey",
25716
+ "unblockKey",
25717
+ "ensureKey",
25718
+ "encrypt",
25719
+ "decrypt",
25720
+ "decryptMany"
25721
+ ].includes(property)) return void 0;
25722
+ return (...args) => {
25723
+ this._namedKeyClient ?? (this._namedKeyClient = import("./encryption-keys.cjs").then(({ createNamedKeyClient }) => createNamedKeyClient(this)));
25724
+ return this._namedKeyClient.then((client) => {
25725
+ const method = client[property];
25726
+ return method.apply(client, args);
25727
+ });
25728
+ };
25729
+ }
25730
+ });
25731
+ }
25732
+ /** Manifest key paths unavailable during the latest `signIn()` attempt. */
25733
+ get keysUnavailable() {
25734
+ return this._keysUnavailable;
25735
+ }
25529
25736
  async getEncryptionNetwork(nameOrNetworkId = this.getDefaultEncryptionNetworkId()) {
25530
25737
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
25531
25738
  return this.fetchEncryptionNetworkAt(
@@ -25653,7 +25860,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25653
25860
  );
25654
25861
  if (result.status !== "ok") {
25655
25862
  if (result.status === "permission_required") {
25656
- const hint = (0, import_sdk_services3.parsePermissionHint)(result.hint);
25863
+ const hint = (0, import_sdk_services4.parsePermissionHint)(result.hint);
25657
25864
  if (hint === void 0) return { status: "read_failed" };
25658
25865
  return {
25659
25866
  status: "permission_required",
@@ -27998,7 +28205,7 @@ var import_node_fs = require("fs");
27998
28205
  var import_promises = require("fs/promises");
27999
28206
  var import_node_path = require("path");
28000
28207
  var import_sdk_core14 = require("@tinycloud/sdk-core");
28001
- var import_sdk_services4 = require("@tinycloud/sdk-services");
28208
+ var import_sdk_services5 = require("@tinycloud/sdk-services");
28002
28209
  var import_contractConstants = require("@tinycloud/sdk-services/kv/replication/contractConstants");
28003
28210
  init_authority();
28004
28211
  var REPLICA_LOGOUT_PENDING_FILE = "replica-logout-pending.json";
@@ -28560,7 +28767,7 @@ function sha256Base32(material) {
28560
28767
  return out.slice(0, 26);
28561
28768
  }
28562
28769
  function partitionDirOf(root, identity) {
28563
- return (0, import_node_path.join)(root, sha256Base32((0, import_sdk_services4.replicationIdentityKey)(identity)));
28770
+ return (0, import_node_path.join)(root, sha256Base32((0, import_sdk_services5.replicationIdentityKey)(identity)));
28564
28771
  }
28565
28772
  function replicaHashOf(prefix, deviceDid) {
28566
28773
  return sha256Base32(`${prefix}
@@ -28634,8 +28841,8 @@ function deviceRecordOf(value) {
28634
28841
  }
28635
28842
  function sameIdentity(a, b) {
28636
28843
  try {
28637
- const left = (0, import_sdk_services4.canonicalReplicationIdentity)(a);
28638
- const right = (0, import_sdk_services4.canonicalReplicationIdentity)(b);
28844
+ const left = (0, import_sdk_services5.canonicalReplicationIdentity)(a);
28845
+ const right = (0, import_sdk_services5.canonicalReplicationIdentity)(b);
28639
28846
  return left.host === right.host && left.space === right.space && left.principal === right.principal;
28640
28847
  } catch {
28641
28848
  return false;
@@ -28651,7 +28858,7 @@ async function ensurePartitionIdentity(dir, identity, dirSync) {
28651
28858
  const file = (0, import_node_path.join)(dir, "identity.json");
28652
28859
  const existing = await readJsonFile(file);
28653
28860
  if (existing === void 0) {
28654
- await writeJsonAtomic(file, { v: 1, ...(0, import_sdk_services4.canonicalReplicationIdentity)(identity) }, dirSync);
28861
+ await writeJsonAtomic(file, { v: 1, ...(0, import_sdk_services5.canonicalReplicationIdentity)(identity) }, dirSync);
28655
28862
  return;
28656
28863
  }
28657
28864
  const record = identityRecordOf(existing);
@@ -28750,7 +28957,7 @@ withGate_fn = async function(section) {
28750
28957
  if (snapshot === void 0) throw gated("gate_unavailable");
28751
28958
  const [principalHash, identityKeyHash] = await Promise.all([
28752
28959
  gate.principalHashOf(snapshot.salt, this.identity.principal),
28753
- gate.identityHashOf(snapshot.salt, (0, import_sdk_services4.replicationIdentityKey)(this.identity))
28960
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(this.identity))
28754
28961
  ]);
28755
28962
  const access = {
28756
28963
  stamp: __privateGet(this, _stamp),
@@ -29021,7 +29228,8 @@ var SqliteReplicaHandle = class {
29021
29228
  options.signal.addEventListener("abort", onAbort, { once: true });
29022
29229
  const engine = new (__privateGet(this, _replica)).Replica({
29023
29230
  store: __privateGet(this, _store),
29024
- transportFor
29231
+ transportFor,
29232
+ ...this.spec.contentFetch === void 0 ? {} : { contentFetch: this.spec.contentFetch }
29025
29233
  });
29026
29234
  const running = engine.sync({ signal: abort.signal });
29027
29235
  const registration = { promise: running, abort: () => abort.abort() };
@@ -29250,7 +29458,7 @@ function createSqliteReplicaStorage(options) {
29250
29458
  if (snapshot === void 0) throw gated("gate_unavailable");
29251
29459
  const [principalHash, identityKeyHash] = await Promise.all([
29252
29460
  gate.principalHashOf(snapshot.salt, principal),
29253
- identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, (0, import_sdk_services4.replicationIdentityKey)(identity))
29461
+ identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(identity))
29254
29462
  ]);
29255
29463
  return profileSection(async () => {
29256
29464
  assertLogoutClear(options.logoutPendingFile);
@@ -29273,7 +29481,7 @@ function createSqliteReplicaStorage(options) {
29273
29481
  if (snapshot === void 0) throw gated("gate_unavailable");
29274
29482
  const [principalHash, identityKeyHash] = await Promise.all([
29275
29483
  gate.principalHashOf(snapshot.salt, spec.identity.principal),
29276
- gate.identityHashOf(snapshot.salt, (0, import_sdk_services4.replicationIdentityKey)(spec.identity))
29484
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(spec.identity))
29277
29485
  ]);
29278
29486
  const access = {
29279
29487
  stamp: spec.stamp,
@@ -29357,7 +29565,7 @@ function createSqliteReplicaStorage(options) {
29357
29565
  const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(idDir, "identity.json")));
29358
29566
  if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${idDir}.`);
29359
29567
  if (await gate.principalHashOf(salt, identity.principal) !== principalHash) continue;
29360
- const identityKeyHash = await gate.identityHashOf(salt, (0, import_sdk_services4.replicationIdentityKey)(identity));
29568
+ const identityKeyHash = await gate.identityHashOf(salt, (0, import_sdk_services5.replicationIdentityKey)(identity));
29361
29569
  const recordedAt = Date.now();
29362
29570
  if (await pathExists((0, import_node_path.join)(idDir, "pending.json")) || await pathExists((0, import_node_path.join)(idDir, "device.jwk"))) {
29363
29571
  intents.push({ id: `pending:${identityKeyHash}`, kind: "pending", identityKeyHash, principalHash, recordedAt });
@@ -30035,7 +30243,7 @@ function createSqliteReplicaStorage(options) {
30035
30243
  for (const dir of await identityDirs()) {
30036
30244
  const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(dir, "identity.json")));
30037
30245
  if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${dir}.`);
30038
- const hash3 = await gate.identityHashOf(state.salt, (0, import_sdk_services4.replicationIdentityKey)(identity));
30246
+ const hash3 = await gate.identityHashOf(state.salt, (0, import_sdk_services5.replicationIdentityKey)(identity));
30039
30247
  if (intent.kind === "pending" ? hash3 === intent.identityKeyHash : intent.partitionId.startsWith(`${hash3}:`)) {
30040
30248
  idDir = dir;
30041
30249
  break;
@@ -30181,7 +30389,7 @@ function createSqliteReplicaStorage(options) {
30181
30389
  const gate = await fileGate();
30182
30390
  const snapshot = await gate.readGateSnapshot(profileDir);
30183
30391
  if (snapshot === void 0) throw gated("gate_unavailable");
30184
- const identityKeyHash = await gate.identityHashOf(snapshot.salt, (0, import_sdk_services4.replicationIdentityKey)(spec.identity));
30392
+ const identityKeyHash = await gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(spec.identity));
30185
30393
  const resolvePolicy = async (at, recordFirst) => {
30186
30394
  const policy = spec.grantPolicy;
30187
30395
  if (!Number.isSafeInteger(policy.grantTtlMs) || policy.grantTtlMs <= 0 || policy.grantTtlMs > import_contractConstants.MAX_GRANT_TTL_MS) {
@@ -30203,7 +30411,7 @@ function createSqliteReplicaStorage(options) {
30203
30411
  let partitionId;
30204
30412
  const storeGuard = (section) => guarded(spec.identity.principal, spec.stamp, spec.identity, partitionId, section);
30205
30413
  return guarded(spec.identity.principal, spec.stamp, spec.identity, void 0, async () => {
30206
- if ((0, import_sdk_services4.requiresSecretsOptIn)(spec.space, spec.prefix) && spec.allowSecrets !== true) {
30414
+ if ((0, import_sdk_services5.requiresSecretsOptIn)(spec.space, spec.prefix) && spec.allowSecrets !== true) {
30207
30415
  throw new replica.ReplicaError(
30208
30416
  replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
30209
30417
  `Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
@@ -30449,7 +30657,7 @@ function createSqliteReplicaStorage(options) {
30449
30657
  const gate = await fileGate();
30450
30658
  const currentGate = await gate.readGateSnapshot(profileDir);
30451
30659
  if (currentGate !== void 0 && currentGate.principalHash !== null && currentGate.principalHash === await gate.principalHashOf(currentGate.salt, target.identity.principal)) {
30452
- const identityKeyHash = await gate.identityHashOf(currentGate.salt, (0, import_sdk_services4.replicationIdentityKey)(target.identity));
30660
+ const identityKeyHash = await gate.identityHashOf(currentGate.salt, (0, import_sdk_services5.replicationIdentityKey)(target.identity));
30453
30661
  await retainGrantInstalls(gate, currentGate, identityKeyHash, store);
30454
30662
  }
30455
30663
  if (state.revoked === null) {