@tinycloud/node-sdk 3.8.0-beta.1 → 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-Ckqu_fqH.d.ts → TinyCloudNode-DnD-SH6t.d.ts} +26 -3
  2. package/dist/{TinyCloudNode-DDfx8Fx5.d.cts → TinyCloudNode-Dy65bxqs.d.cts} +26 -3
  3. package/dist/{authority-DcyJpajL.d.ts → authority-BQnTX3fI.d.ts} +20 -4
  4. package/dist/{authority-BwM_s3Ya.d.cts → authority-Rbye1U4o.d.cts} +20 -4
  5. package/dist/core.cjs +189 -25
  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 +183 -21
  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 +239 -37
  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 +226 -24
  24. package/dist/index.js.map +1 -1
  25. package/dist/replication/authority.cjs +39 -0
  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 +38 -0
  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
@@ -17089,6 +17089,9 @@ function assertValidReplicationConfig(replication) {
17089
17089
  if (replication?.syncScope !== void 0 && replication.syncScope !== "prefixes") {
17090
17090
  throw new TypeError('replication.syncScope must be "prefixes"');
17091
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
+ }
17092
17095
  if (replication === void 0 || !replication.enabled) return;
17093
17096
  if (replication.storage === void 0 || replication.storage === null) {
17094
17097
  throw new TypeError("replication.enabled requires replication.storage");
@@ -17143,6 +17146,7 @@ function resolvedOptions(options) {
17143
17146
  maxStalenessMs: options.maxStalenessMs ?? 12e4,
17144
17147
  staleSyncTimeoutMs: options.staleSyncTimeoutMs ?? 5e3,
17145
17148
  allowSecrets: options.allowSecrets === true,
17149
+ contentFetch: options.contentFetch ?? "eager",
17146
17150
  verify: options.verify === true
17147
17151
  };
17148
17152
  }
@@ -17414,6 +17418,7 @@ __export(authority_exports, {
17414
17418
  assertValidReplicationConfig: () => assertValidReplicationConfig,
17415
17419
  augmentSignInEntriesWithReplication: () => augmentSignInEntriesWithReplication,
17416
17420
  createReplicationAuthority: () => createReplicationAuthority,
17421
+ eraseReplicaLocalCopy: () => eraseReplicaLocalCopy,
17417
17422
  establishReplicaSessionStamp: () => establishReplicaSessionStamp,
17418
17423
  hasUnrestrictedGetCoverage: () => hasUnrestrictedGetCoverage,
17419
17424
  mintNodeReplicationGrant: () => mintNodeReplicationGrant,
@@ -17444,6 +17449,38 @@ function reportReplicaDenial(result, reason, onEvent, context) {
17444
17449
  } catch {
17445
17450
  }
17446
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
+ }
17447
17484
  function verifiedOwnerConsentAnchor(session, wasmBindings, exactVerifiedRecap2) {
17448
17485
  if (typeof session.siwe !== "string" || typeof session.signature !== "string" || typeof session.address !== "string" || typeof session.chainId !== "number") return null;
17449
17486
  try {
@@ -17702,7 +17739,7 @@ function createReplicationAuthority(host) {
17702
17739
  };
17703
17740
  return authority;
17704
17741
  }
17705
- 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;
17706
17743
  var init_authority = __esm({
17707
17744
  "src/replication/authority.ts"() {
17708
17745
  "use strict";
@@ -17712,6 +17749,8 @@ var init_authority = __esm({
17712
17749
  init_config();
17713
17750
  init_authority_sign_in();
17714
17751
  parseUcanGrant = import_replica.parseUcanGrant;
17752
+ ERASE_RETRY_DELAYS_MS = [150, 450];
17753
+ ERASE_ATTEMPT_MS = 3e3;
17715
17754
  KV_GET2 = import_sdk_core12.KV.GET;
17716
17755
  KV_SYNC2 = import_sdk_core12.KV.SYNC;
17717
17756
  KV_SERVICE2 = "tinycloud.kv";
@@ -18789,13 +18828,42 @@ function getNodeReplicationLoaders() {
18789
18828
 
18790
18829
  // src/TinyCloudNode.ts
18791
18830
  var import_sdk_core10 = require("@tinycloud/sdk-core");
18792
- var import_sdk_services3 = require("@tinycloud/sdk-services");
18831
+ var import_sdk_services4 = require("@tinycloud/sdk-services");
18793
18832
  var import_requestLog2 = require("@tinycloud/sdk-services/telemetry/requestLog");
18833
+ var import_resource2 = require("@tinycloud/sdk-services/encryption/resource");
18794
18834
 
18795
18835
  // src/authorization/NodeUserAuthorization.ts
18796
18836
  var import_node_crypto = require("crypto");
18797
18837
  var import_siwe = require("siwe");
18798
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
18799
18867
  init_authority_sign_in();
18800
18868
 
18801
18869
  // src/authorization/caveatRefusal.ts
@@ -19307,6 +19375,23 @@ var NodeUserAuthorization = class {
19307
19375
  );
19308
19376
  return this._capabilityRequest;
19309
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
+ }
19310
19395
  resolveSpaceName(space, address, chainId) {
19311
19396
  if (space.startsWith("tinycloud:")) {
19312
19397
  return space;
@@ -19498,15 +19583,31 @@ var NodeUserAuthorization = class {
19498
19583
  }
19499
19584
  const rawAbilities = {};
19500
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
+ }
19501
19599
  const spaceResources = request.resources.filter((entry) => {
19502
19600
  if (entry.service !== import_sdk_core5.ENCRYPTION_PERMISSION_SERVICE) {
19503
19601
  return true;
19504
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
+ }
19505
19607
  for (const action of entry.actions) {
19506
19608
  addRawAbility(rawAbilities, entry.path, action);
19507
19609
  }
19508
- if (entry.actions.includes(DECRYPT_ACTION)) {
19509
- const parsed = (0, import_sdk_core5.parseNetworkId)(entry.path);
19610
+ if (entry.actions.includes(DECRYPT_ACTION) && parsed.kind === "network") {
19510
19611
  if (didPrincipalMatches(parsed.ownerDid, currentDid)) {
19511
19612
  addRawAbility(rawAbilities, entry.path, NETWORK_CREATE_ACTION);
19512
19613
  }
@@ -19902,6 +20003,8 @@ var NodeUserAuthorization = class {
19902
20003
  this._chainId = chainId;
19903
20004
  await this.resolveTinyCloudHostsForSignIn(address, chainId);
19904
20005
  this.assertSignInCurrent(generation);
20006
+ await this.refreshNamedKeyFeatureForSignIn(address, chainId);
20007
+ this.assertSignInCurrent(generation);
19905
20008
  const keyId = `session-${Date.now()}`;
19906
20009
  this.sessionManager.renameSessionKeyId(this.activeSessionKeyId, keyId);
19907
20010
  this.activeSessionKeyId = keyId;
@@ -20048,6 +20151,7 @@ var NodeUserAuthorization = class {
20048
20151
  refuseCaveated(this.getCapabilityRequest()?.manifests);
20049
20152
  const address = (0, import_sdk_core5.canonicalizeAddress)(await this.signer.getAddress());
20050
20153
  const chainId = await this.signer.getChainId();
20154
+ await this.refreshNamedKeyFeatureForSignIn(address, chainId);
20051
20155
  const keyId = `session-${Date.now()}`;
20052
20156
  this.sessionManager.renameSessionKeyId(this.activeSessionKeyId, keyId);
20053
20157
  this.activeSessionKeyId = keyId;
@@ -21101,6 +21205,7 @@ var ROOT_DELEGATION_ACTIONS = [
21101
21205
  import_sdk_core10.DUCKDB.ALL
21102
21206
  ];
21103
21207
  var DEFAULT_SESSION_EXPIRATION_MS = import_sdk_core10.EXPIRY.SESSION_MS;
21208
+ var OWNER_REVOCATION_WINDOW_MS = 10 * 6e4;
21104
21209
  function unifiedRootAttenuation(capabilities) {
21105
21210
  const attenuation = {};
21106
21211
  for (const capability of capabilities) {
@@ -21565,6 +21670,7 @@ function replicaRestoreRefusal(error) {
21565
21670
  case "purge_pending":
21566
21671
  case "logout_pending":
21567
21672
  case "gate_unavailable":
21673
+ case "wallet_provider_required":
21568
21674
  return error.gate;
21569
21675
  }
21570
21676
  }
@@ -21599,6 +21705,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21599
21705
  this.auth = null;
21600
21706
  this.tc = null;
21601
21707
  this._chainId = 1;
21708
+ this.replicaPurgeFailedHandlers = /* @__PURE__ */ new Set();
21602
21709
  /** Mints the replica session stamp on each fresh sign-in (TC-858 §4.1). Undefined when replication is off. */
21603
21710
  this.establishReplicaStamp = async (session) => {
21604
21711
  const replication = this.config.replication;
@@ -21630,6 +21737,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21630
21737
  return void 0;
21631
21738
  }
21632
21739
  };
21740
+ this._keysUnavailable = [];
21633
21741
  this._baseVaults = /* @__PURE__ */ new Map();
21634
21742
  this._baseSecrets = /* @__PURE__ */ new Map();
21635
21743
  this._secrets = /* @__PURE__ */ new Map();
@@ -21805,6 +21913,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21805
21913
  return instance ? instance.control.purge(input) : runtime.then((ready) => ready.control.purge(input));
21806
21914
  },
21807
21915
  clearPending: async () => (await runtime).control.clearPending(),
21916
+ purgeLocalCopy: async () => (await this.eraseReplicaLocalCopy("purge_local_copy"))?.purge ?? { ok: false, retryNow: false },
21808
21917
  close: async () => (await runtime).control.close()
21809
21918
  };
21810
21919
  }
@@ -21859,18 +21968,14 @@ var _TinyCloudNode = class _TinyCloudNode {
21859
21968
  get freshEncryptionNetworkCreated() {
21860
21969
  return this._freshEncryptionNetworkCreated;
21861
21970
  }
21862
- /**
21863
- * Subscribe to `storage.full`. It fires on the first write any service had
21864
- * rejected because the owner's TinyCloud storage is full
21865
- * (`STORAGE_QUOTA_EXCEEDED` or `STORAGE_LIMIT_REACHED`), so an app can show
21866
- * one read-only banner. Reads keep working. For the signed-in account's own
21867
- * storage it fires once per episode: the episode ends only when a write to
21868
- * that storage succeeds, or on sign-out. A `storage.status()` read does not
21869
- * end it. A rejection on another owner's delegated space fires once per
21870
- * owner, until that owner's `storage.status()` reports space again. Returns
21871
- * the unsubscribe function.
21872
- */
21873
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
+ }
21874
21979
  if (event !== "storage.full") {
21875
21980
  throw new Error(`Unknown TinyCloud event: ${String(event)}`);
21876
21981
  }
@@ -22351,6 +22456,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22351
22456
  const leavingAuth = this.auth;
22352
22457
  const leavingContext = this._serviceContext;
22353
22458
  let denied;
22459
+ let erased;
22354
22460
  try {
22355
22461
  this.teardownSession();
22356
22462
  } finally {
@@ -22361,6 +22467,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22361
22467
  try {
22362
22468
  await this.accountRegistryTail;
22363
22469
  await this.quiesceReplicaRuntime();
22470
+ if (denied) erased = await this.eraseReplicaLocalCopy("sign_out", denied.status);
22364
22471
  } finally {
22365
22472
  if (leavingAddress) {
22366
22473
  await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
@@ -22368,8 +22475,37 @@ var _TinyCloudNode = class _TinyCloudNode {
22368
22475
  }
22369
22476
  }
22370
22477
  }
22371
- if (denied?.status === "unguarded") {
22372
- 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;
22373
22509
  }
22374
22510
  }
22375
22511
  /**
@@ -22429,6 +22565,8 @@ var _TinyCloudNode = class _TinyCloudNode {
22429
22565
  this._hooks = void 0;
22430
22566
  this._vault = void 0;
22431
22567
  this._encryption = void 0;
22568
+ this._namedKeyClient = void 0;
22569
+ this._keysUnavailable = [];
22432
22570
  this._baseVaults.clear();
22433
22571
  this._baseSecrets.clear();
22434
22572
  this._secrets.clear();
@@ -22533,6 +22671,10 @@ var _TinyCloudNode = class _TinyCloudNode {
22533
22671
  const priorPrincipal = this.replicaSessionPrincipal ?? (this.address ? (0, import_sdk_core10.pkhDid)(this.address, this._chainId) : void 0);
22534
22672
  if (this.config.replication?.enabled && priorPrincipal && priorPrincipal !== nextPrincipal) {
22535
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
+ }
22536
22678
  this.replicaRestoreGate = denial?.status === "unguarded" ? "gate_unavailable" : "stamp_stale";
22537
22679
  }
22538
22680
  if (address !== this._address || chainId !== this._chainId) {
@@ -22547,6 +22689,8 @@ var _TinyCloudNode = class _TinyCloudNode {
22547
22689
  this._hooks = void 0;
22548
22690
  this._vault = void 0;
22549
22691
  this._encryption = void 0;
22692
+ this._namedKeyClient = void 0;
22693
+ this._keysUnavailable = [];
22550
22694
  this._baseVaults.clear();
22551
22695
  this._baseSecrets.clear();
22552
22696
  this._secrets.clear();
@@ -22599,6 +22743,26 @@ var _TinyCloudNode = class _TinyCloudNode {
22599
22743
  this.assertSessionEpoch(epoch, "Sign-in");
22600
22744
  await this.ensureRequestedEncryptionNetworks();
22601
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
+ }
22602
22766
  if (!bootstrapped && this.config.manifest === void 0 && this.config.capabilityRequest === void 0) {
22603
22767
  await this.ensureOwnedSpaceHostedById(this.ownedSpaceId(import_sdk_core10.SECRETS_SPACE));
22604
22768
  }
@@ -23175,7 +23339,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23175
23339
  if (!this.currentSessionCoversAccountRegistry()) {
23176
23340
  if (!_TinyCloudNode.accountRegistrySkipLogged) {
23177
23341
  _TinyCloudNode.accountRegistrySkipLogged = true;
23178
- import_sdk_services3.tinyCloudDebugLogger.log("registry.skip");
23342
+ import_sdk_services4.tinyCloudDebugLogger.log("registry.skip");
23179
23343
  }
23180
23344
  return;
23181
23345
  }
@@ -23386,7 +23550,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23386
23550
  const networkIds = /* @__PURE__ */ new Set();
23387
23551
  for (const resource of request.resources) {
23388
23552
  if (resource.service === import_sdk_core10.ENCRYPTION_PERMISSION_SERVICE && resource.path.startsWith("urn:tinycloud:encryption:") && resource.actions.includes(DECRYPT_ACTION2)) {
23389
- 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);
23390
23555
  }
23391
23556
  }
23392
23557
  return [...networkIds];
@@ -23936,6 +24101,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23936
24101
  this._hooks = stagedGraph.hooks;
23937
24102
  this._vault = stagedGraph.vault;
23938
24103
  this._encryption = stagedGraph.encryption;
24104
+ this._namedKeyClient = void 0;
24105
+ this._keysUnavailable = [];
23939
24106
  this._capabilityRegistry = stagedGraph.capabilityRegistry;
23940
24107
  this._keyProvider = stagedGraph.keyProvider;
23941
24108
  this._sharingService = stagedGraph.sharingService;
@@ -24845,7 +25012,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24845
25012
  );
24846
25013
  graph.assertActive();
24847
25014
  if (!response.ok) {
24848
- 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)({
24849
25016
  service: "tinycloud.encryption",
24850
25017
  path: networkId,
24851
25018
  actions: ["tinycloud.encryption/decrypt"]
@@ -25289,7 +25456,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25289
25456
  * Sign a wallet (CACAO) revocation of the delegation `cid` for the node's
25290
25457
  * `POST /revoke`, and return its Authorization header value. The node
25291
25458
  * accepts it when the owner delegated `cid` or owns a space its abilities
25292
- * target, whichever key issued it. Valid for 60 seconds. Requires the
25459
+ * target, whichever key issued it. Valid for 10 minutes. Requires the
25293
25460
  * owner wallet signer.
25294
25461
  */
25295
25462
  async signOwnerRevocation(cid) {
@@ -25305,7 +25472,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25305
25472
  chainId: session.chainId,
25306
25473
  nonce: (0, import_siwe2.generateNonce)(),
25307
25474
  issuedAt: issuedAt.toISOString(),
25308
- expirationTime: new Date(issuedAt.getTime() + 6e4).toISOString()
25475
+ expirationTime: new Date(issuedAt.getTime() + OWNER_REVOCATION_WINDOW_MS).toISOString()
25309
25476
  }).prepareMessage();
25310
25477
  const signature2 = await this.signer.signMessage(siwe);
25311
25478
  return this.wasmBindings.siweToDelegationHeaders({ siwe, signature: signature2 }).Authorization;
@@ -25532,6 +25699,40 @@ var _TinyCloudNode = class _TinyCloudNode {
25532
25699
  get encryption() {
25533
25700
  return this.getEncryptionService();
25534
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
+ }
25535
25736
  async getEncryptionNetwork(nameOrNetworkId = this.getDefaultEncryptionNetworkId()) {
25536
25737
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
25537
25738
  return this.fetchEncryptionNetworkAt(
@@ -25659,7 +25860,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25659
25860
  );
25660
25861
  if (result.status !== "ok") {
25661
25862
  if (result.status === "permission_required") {
25662
- const hint = (0, import_sdk_services3.parsePermissionHint)(result.hint);
25863
+ const hint = (0, import_sdk_services4.parsePermissionHint)(result.hint);
25663
25864
  if (hint === void 0) return { status: "read_failed" };
25664
25865
  return {
25665
25866
  status: "permission_required",
@@ -28004,7 +28205,7 @@ var import_node_fs = require("fs");
28004
28205
  var import_promises = require("fs/promises");
28005
28206
  var import_node_path = require("path");
28006
28207
  var import_sdk_core14 = require("@tinycloud/sdk-core");
28007
- var import_sdk_services4 = require("@tinycloud/sdk-services");
28208
+ var import_sdk_services5 = require("@tinycloud/sdk-services");
28008
28209
  var import_contractConstants = require("@tinycloud/sdk-services/kv/replication/contractConstants");
28009
28210
  init_authority();
28010
28211
  var REPLICA_LOGOUT_PENDING_FILE = "replica-logout-pending.json";
@@ -28566,7 +28767,7 @@ function sha256Base32(material) {
28566
28767
  return out.slice(0, 26);
28567
28768
  }
28568
28769
  function partitionDirOf(root, identity) {
28569
- 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)));
28570
28771
  }
28571
28772
  function replicaHashOf(prefix, deviceDid) {
28572
28773
  return sha256Base32(`${prefix}
@@ -28640,8 +28841,8 @@ function deviceRecordOf(value) {
28640
28841
  }
28641
28842
  function sameIdentity(a, b) {
28642
28843
  try {
28643
- const left = (0, import_sdk_services4.canonicalReplicationIdentity)(a);
28644
- 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);
28645
28846
  return left.host === right.host && left.space === right.space && left.principal === right.principal;
28646
28847
  } catch {
28647
28848
  return false;
@@ -28657,7 +28858,7 @@ async function ensurePartitionIdentity(dir, identity, dirSync) {
28657
28858
  const file = (0, import_node_path.join)(dir, "identity.json");
28658
28859
  const existing = await readJsonFile(file);
28659
28860
  if (existing === void 0) {
28660
- 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);
28661
28862
  return;
28662
28863
  }
28663
28864
  const record = identityRecordOf(existing);
@@ -28756,7 +28957,7 @@ withGate_fn = async function(section) {
28756
28957
  if (snapshot === void 0) throw gated("gate_unavailable");
28757
28958
  const [principalHash, identityKeyHash] = await Promise.all([
28758
28959
  gate.principalHashOf(snapshot.salt, this.identity.principal),
28759
- gate.identityHashOf(snapshot.salt, (0, import_sdk_services4.replicationIdentityKey)(this.identity))
28960
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(this.identity))
28760
28961
  ]);
28761
28962
  const access = {
28762
28963
  stamp: __privateGet(this, _stamp),
@@ -29027,7 +29228,8 @@ var SqliteReplicaHandle = class {
29027
29228
  options.signal.addEventListener("abort", onAbort, { once: true });
29028
29229
  const engine = new (__privateGet(this, _replica)).Replica({
29029
29230
  store: __privateGet(this, _store),
29030
- transportFor
29231
+ transportFor,
29232
+ ...this.spec.contentFetch === void 0 ? {} : { contentFetch: this.spec.contentFetch }
29031
29233
  });
29032
29234
  const running = engine.sync({ signal: abort.signal });
29033
29235
  const registration = { promise: running, abort: () => abort.abort() };
@@ -29256,7 +29458,7 @@ function createSqliteReplicaStorage(options) {
29256
29458
  if (snapshot === void 0) throw gated("gate_unavailable");
29257
29459
  const [principalHash, identityKeyHash] = await Promise.all([
29258
29460
  gate.principalHashOf(snapshot.salt, principal),
29259
- 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))
29260
29462
  ]);
29261
29463
  return profileSection(async () => {
29262
29464
  assertLogoutClear(options.logoutPendingFile);
@@ -29279,7 +29481,7 @@ function createSqliteReplicaStorage(options) {
29279
29481
  if (snapshot === void 0) throw gated("gate_unavailable");
29280
29482
  const [principalHash, identityKeyHash] = await Promise.all([
29281
29483
  gate.principalHashOf(snapshot.salt, spec.identity.principal),
29282
- gate.identityHashOf(snapshot.salt, (0, import_sdk_services4.replicationIdentityKey)(spec.identity))
29484
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(spec.identity))
29283
29485
  ]);
29284
29486
  const access = {
29285
29487
  stamp: spec.stamp,
@@ -29363,7 +29565,7 @@ function createSqliteReplicaStorage(options) {
29363
29565
  const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(idDir, "identity.json")));
29364
29566
  if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${idDir}.`);
29365
29567
  if (await gate.principalHashOf(salt, identity.principal) !== principalHash) continue;
29366
- 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));
29367
29569
  const recordedAt = Date.now();
29368
29570
  if (await pathExists((0, import_node_path.join)(idDir, "pending.json")) || await pathExists((0, import_node_path.join)(idDir, "device.jwk"))) {
29369
29571
  intents.push({ id: `pending:${identityKeyHash}`, kind: "pending", identityKeyHash, principalHash, recordedAt });
@@ -30041,7 +30243,7 @@ function createSqliteReplicaStorage(options) {
30041
30243
  for (const dir of await identityDirs()) {
30042
30244
  const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(dir, "identity.json")));
30043
30245
  if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${dir}.`);
30044
- 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));
30045
30247
  if (intent.kind === "pending" ? hash3 === intent.identityKeyHash : intent.partitionId.startsWith(`${hash3}:`)) {
30046
30248
  idDir = dir;
30047
30249
  break;
@@ -30187,7 +30389,7 @@ function createSqliteReplicaStorage(options) {
30187
30389
  const gate = await fileGate();
30188
30390
  const snapshot = await gate.readGateSnapshot(profileDir);
30189
30391
  if (snapshot === void 0) throw gated("gate_unavailable");
30190
- 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));
30191
30393
  const resolvePolicy = async (at, recordFirst) => {
30192
30394
  const policy = spec.grantPolicy;
30193
30395
  if (!Number.isSafeInteger(policy.grantTtlMs) || policy.grantTtlMs <= 0 || policy.grantTtlMs > import_contractConstants.MAX_GRANT_TTL_MS) {
@@ -30209,7 +30411,7 @@ function createSqliteReplicaStorage(options) {
30209
30411
  let partitionId;
30210
30412
  const storeGuard = (section) => guarded(spec.identity.principal, spec.stamp, spec.identity, partitionId, section);
30211
30413
  return guarded(spec.identity.principal, spec.stamp, spec.identity, void 0, async () => {
30212
- 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) {
30213
30415
  throw new replica.ReplicaError(
30214
30416
  replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
30215
30417
  `Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
@@ -30455,7 +30657,7 @@ function createSqliteReplicaStorage(options) {
30455
30657
  const gate = await fileGate();
30456
30658
  const currentGate = await gate.readGateSnapshot(profileDir);
30457
30659
  if (currentGate !== void 0 && currentGate.principalHash !== null && currentGate.principalHash === await gate.principalHashOf(currentGate.salt, target.identity.principal)) {
30458
- 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));
30459
30661
  await retainGrantInstalls(gate, currentGate, identityKeyHash, store);
30460
30662
  }
30461
30663
  if (state.revoked === null) {