@tinycloud/node-sdk 3.12.0-beta.1 → 3.12.0-beta.3

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 (36) hide show
  1. package/README.md +39 -0
  2. package/dist/{NodeUserAuthorization-BtoDI9in.d.cts → NodeUserAuthorization-B_TFUlua.d.cts} +19 -53
  3. package/dist/{NodeUserAuthorization-BtoDI9in.d.ts → NodeUserAuthorization-B_TFUlua.d.ts} +19 -53
  4. package/dist/{TinyCloudNode-DLrINr98.d.ts → TinyCloudNode-Brcu8QoX.d.cts} +33 -59
  5. package/dist/{TinyCloudNode-C24728N_.d.cts → TinyCloudNode-DBJUnsMN.d.ts} +33 -59
  6. package/dist/{authority-BQN2Z3st.d.ts → authority-BFc5lesl.d.ts} +1 -1
  7. package/dist/{authority-DDdheOL6.d.cts → authority-DQ6R1HEG.d.cts} +1 -1
  8. package/dist/core.cjs +555 -320
  9. package/dist/core.cjs.map +1 -1
  10. package/dist/core.d.cts +5 -5
  11. package/dist/core.d.ts +5 -5
  12. package/dist/core.js +581 -324
  13. package/dist/core.js.map +1 -1
  14. package/dist/credentials-core.d.cts +1 -1
  15. package/dist/credentials-core.d.ts +1 -1
  16. package/dist/credentials.d.cts +1 -1
  17. package/dist/credentials.d.ts +1 -1
  18. package/dist/{delegation-BQbAuKad.d.cts → delegation-CeOWDnBB.d.cts} +7 -0
  19. package/dist/{delegation-BQbAuKad.d.ts → delegation-CeOWDnBB.d.ts} +7 -0
  20. package/dist/encryption-keys.d.cts +3 -3
  21. package/dist/encryption-keys.d.ts +3 -3
  22. package/dist/index.cjs +572 -337
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +5 -4
  25. package/dist/index.d.ts +5 -4
  26. package/dist/index.js +582 -325
  27. package/dist/index.js.map +1 -1
  28. package/dist/replication/authority.d.cts +4 -4
  29. package/dist/replication/authority.d.ts +4 -4
  30. package/dist/space-hosting.cjs +48 -6
  31. package/dist/space-hosting.cjs.map +1 -1
  32. package/dist/space-hosting.d.cts +23 -8
  33. package/dist/space-hosting.d.ts +23 -8
  34. package/dist/space-hosting.js +48 -6
  35. package/dist/space-hosting.js.map +1 -1
  36. package/package.json +4 -4
package/dist/index.cjs CHANGED
@@ -17045,7 +17045,7 @@ function augmentSignInEntriesWithReplication(input) {
17045
17045
  for (const prefix of replication.prefixes) {
17046
17046
  (0, import_prefix.assertFolderPrefix)(prefix);
17047
17047
  const path = prefix === "/" ? "" : prefix;
17048
- if ((0, import_sdk_services2.requiresSecretsOptIn)(input.primarySpaceId, prefix) && replication.allowSecrets !== true) {
17048
+ if ((0, import_sdk_services3.requiresSecretsOptIn)(input.primarySpaceId, prefix) && replication.allowSecrets !== true) {
17049
17049
  continue;
17050
17050
  }
17051
17051
  if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, path)) {
@@ -17071,12 +17071,12 @@ function augmentSignInEntriesWithReplication(input) {
17071
17071
  }
17072
17072
  return out;
17073
17073
  }
17074
- var import_sdk_core2, import_sdk_services2, import_prefix, KV_GET, KV_SYNC, KV_SERVICE;
17074
+ var import_sdk_core2, import_sdk_services3, import_prefix, KV_GET, KV_SYNC, KV_SERVICE;
17075
17075
  var init_authority_sign_in = __esm({
17076
17076
  "src/replication/authority-sign-in.ts"() {
17077
17077
  "use strict";
17078
17078
  import_sdk_core2 = require("@tinycloud/sdk-core");
17079
- import_sdk_services2 = require("@tinycloud/sdk-services");
17079
+ import_sdk_services3 = require("@tinycloud/sdk-services");
17080
17080
  import_prefix = require("@tinycloud/sdk-services/kv/replication/prefix");
17081
17081
  KV_GET = import_sdk_core2.KV.GET;
17082
17082
  KV_SYNC = import_sdk_core2.KV.SYNC;
@@ -17875,8 +17875,11 @@ __export(src_exports, {
17875
17875
  MemorySessionStorage: () => MemorySessionStorage,
17876
17876
  NETWORK_NAME_PATTERN: () => import_sdk_core22.NETWORK_NAME_PATTERN,
17877
17877
  NetworkIdError: () => import_sdk_core22.NetworkIdError,
17878
+ NodeIdentityMismatchError: () => import_sdk_services8.NodeIdentityMismatchError,
17879
+ NodeNotRespondingError: () => import_sdk_services8.NodeNotRespondingError,
17878
17880
  NodeUserAuthorization: () => NodeUserAuthorization,
17879
17881
  NodeWasmBindings: () => NodeWasmBindings,
17882
+ PartialNodeWriteError: () => import_sdk_services8.PartialNodeWriteError,
17880
17883
  PermissionNotInManifestError: () => import_sdk_core17.PermissionNotInManifestError,
17881
17884
  PrefixedKVService: () => import_sdk_core18.PrefixedKVService,
17882
17885
  PrivateKeySigner: () => PrivateKeySigner,
@@ -18275,6 +18278,7 @@ function rememberInvokeAny(invokeAny2, sink, pending) {
18275
18278
 
18276
18279
  // src/delegation.ts
18277
18280
  var import_sdk_core = require("@tinycloud/sdk-core");
18281
+ var import_sdk_services2 = require("@tinycloud/sdk-services");
18278
18282
  var RUNTIME_DELEGATION_REJECTIONS = [
18279
18283
  "host_mismatch",
18280
18284
  "expired",
@@ -18316,7 +18320,8 @@ var SERVICE_BY_SHORT_NAME = {
18316
18320
  kv: "tinycloud.kv",
18317
18321
  sql: "tinycloud.sql",
18318
18322
  duckdb: "tinycloud.duckdb",
18319
- hooks: "tinycloud.hooks"
18323
+ hooks: "tinycloud.hooks",
18324
+ capabilities: "tinycloud.capabilities"
18320
18325
  };
18321
18326
  function authorizationWithoutBearer(authorization) {
18322
18327
  return authorization.replace(/^Bearer /i, "");
@@ -18447,8 +18452,8 @@ function signedAuthorityFromCompactUcan(authorization) {
18447
18452
  ));
18448
18453
  continue;
18449
18454
  }
18450
- const match = /^(?:tinycloud:\/\/([^/]+)|(tinycloud:[^/]+))\/(kv|sql|duckdb|hooks)(?:\/|$)(.*)$/.exec(resource);
18451
- if (!match) {
18455
+ const match = /^(?:tinycloud:\/\/([^/]+)|(tinycloud:[^/]+))\/([^/]+)(?:\/|$)(.*)$/.exec(resource);
18456
+ if (!match || !Object.prototype.hasOwnProperty.call(SERVICE_BY_SHORT_NAME, match[3])) {
18452
18457
  throw new Error(
18453
18458
  `Validated runtime delegation has an unsupported signed resource '${resource}'.`
18454
18459
  );
@@ -18593,13 +18598,8 @@ function resourcesMatch(actual, expected) {
18593
18598
  return JSON.stringify(actual.map(comparable)) === JSON.stringify(expected.map(comparable));
18594
18599
  }
18595
18600
  async function activateValidatedRuntimeDelegation(node, delegation, options) {
18596
- const host = normalizedHost(options.host);
18597
- if (delegation.host !== void 0 && normalizedHost(delegation.host) !== host) {
18598
- reject(
18599
- "host_mismatch",
18600
- `Runtime delegation host '${delegation.host}' does not match expected host '${options.host}'.`
18601
- );
18602
- }
18601
+ let host = normalizedHost(options.host);
18602
+ const differentHost = delegation.host !== void 0 && normalizedHost(delegation.host) !== host;
18603
18603
  if (!(delegation.expiry instanceof Date) || Number.isNaN(delegation.expiry.getTime())) {
18604
18604
  reject("authority_invalid", "Runtime delegation has an invalid expiry.");
18605
18605
  }
@@ -18643,6 +18643,13 @@ async function activateValidatedRuntimeDelegation(node, delegation, options) {
18643
18643
  if (options.authorize !== void 0 && !options.authorize(signed2.permissions)) {
18644
18644
  throw new Error("Runtime delegation is not authorized for this runtime.");
18645
18645
  }
18646
+ if (differentHost) {
18647
+ const registeredHost = await node.resolveRegisteredDelegationHost?.(signed2.permissions, host);
18648
+ if (registeredHost === void 0) {
18649
+ reject("host_mismatch", `Runtime delegation host '${delegation.host}' does not match expected host '${options.host}'.`);
18650
+ }
18651
+ host = normalizedHost(registeredHost);
18652
+ }
18646
18653
  const previouslyInstalled = node.getRuntimePermissionDelegations().find((candidate) => candidate.cid === cid);
18647
18654
  if (previouslyInstalled !== void 0 && previouslyInstalled.delegationHeader.Authorization !== authorization) {
18648
18655
  reject(
@@ -18686,6 +18693,7 @@ async function activateValidatedRuntimeDelegation(node, delegation, options) {
18686
18693
  await node.useRuntimeDelegation(installedCandidate);
18687
18694
  }
18688
18695
  } catch (error) {
18696
+ if ((0, import_sdk_services2.routingFailure)(error)) throw error;
18689
18697
  reject(
18690
18698
  activationRejectionReason(error),
18691
18699
  error instanceof Error ? error.message : String(error)
@@ -18704,6 +18712,7 @@ async function activateValidatedRuntimeDelegation(node, delegation, options) {
18704
18712
  "Runtime delegation activation installed authority that differs from the validated authority."
18705
18713
  );
18706
18714
  }
18715
+ node.refreshRegistryForCapabilities?.(signed2.permissions);
18707
18716
  return {
18708
18717
  cid,
18709
18718
  delegation: installed,
@@ -18813,6 +18822,7 @@ function deserializeDelegation(data) {
18813
18822
  }
18814
18823
 
18815
18824
  // src/TinyCloudNode.ts
18825
+ var import_sdk_services5 = require("@tinycloud/sdk-services");
18816
18826
  var import_siwe2 = require("siwe");
18817
18827
  var import_share_envelope = require("@tinycloud/share-envelope");
18818
18828
 
@@ -18830,7 +18840,7 @@ function getNodeReplicationLoaders() {
18830
18840
 
18831
18841
  // src/TinyCloudNode.ts
18832
18842
  var import_sdk_core10 = require("@tinycloud/sdk-core");
18833
- var import_sdk_services4 = require("@tinycloud/sdk-services");
18843
+ var import_sdk_services6 = require("@tinycloud/sdk-services");
18834
18844
  var import_requestLog2 = require("@tinycloud/sdk-services/telemetry/requestLog");
18835
18845
  var import_resource2 = require("@tinycloud/sdk-services/encryption/resource");
18836
18846
 
@@ -18839,7 +18849,7 @@ var import_node_crypto = require("crypto");
18839
18849
  var import_siwe = require("siwe");
18840
18850
  var import_sdk_core5 = require("@tinycloud/sdk-core");
18841
18851
  var import_resource = require("@tinycloud/sdk-services/encryption/resource");
18842
- var import_sdk_services3 = require("@tinycloud/sdk-services");
18852
+ var import_sdk_services4 = require("@tinycloud/sdk-services");
18843
18853
 
18844
18854
  // src/authorization/appKeyHostBinding.ts
18845
18855
  var DOMAIN_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -19114,11 +19124,12 @@ var NodeUserAuthorization = class {
19114
19124
  this.tinycloudHosts = config.tinycloudHosts;
19115
19125
  this.tinycloudRegistryUrl = config.tinycloudRegistryUrl;
19116
19126
  this.tinycloudFallbackHosts = config.tinycloudFallbackHosts;
19117
- this.registryPolicy = config.registryPolicy;
19118
19127
  this.newUserHost = config.newUserHost;
19119
19128
  this.registryChainId = config.registryChainId;
19120
19129
  this.registrySequenceStore = config.registrySequenceStore ?? (0, import_sdk_core5.createInMemoryRegistrySequenceStore)();
19130
+ this.registryCacheStore = config.registryCacheStore ?? (0, import_sdk_core5.createInMemoryRegistryCacheStore)();
19121
19131
  this.onNodeNotFound = config.onNodeNotFound;
19132
+ this.onRegistryFallback = config.onRegistryFallback;
19122
19133
  this.autoDiscoverLocalNode = config.autoDiscoverLocalNode;
19123
19134
  this.localNodeUrl = config.localNodeUrl;
19124
19135
  this.localLinkName = config.localLinkName;
@@ -19186,13 +19197,8 @@ var NodeUserAuthorization = class {
19186
19197
  * undefined for missing SIWEs and the SDK then treats the session as
19187
19198
  * expired-at-epoch-zero.
19188
19199
  *
19189
- * @param hosts - The TinyCloud hosts this session was created against,
19190
- * as persisted in {@link PersistedSessionData.tinycloudHosts}. When
19191
- * present (and non-empty) they are adopted directly so the restored
19192
- * session resolves to the same node as the original sign-in without
19193
- * re-running registry/fallback resolution. When absent (old session)
19194
- * hosts are resolved lazily on the first host-needing call via
19195
- * {@link ensureTinyCloudHosts}.
19200
+ * @param hosts - Persisted host hints for genuine no-entry onboarding.
19201
+ * Current signed registry membership always takes precedence.
19196
19202
  */
19197
19203
  setRestoredTinyCloudSession(session, hosts) {
19198
19204
  const address = (0, import_sdk_core5.canonicalizeAddress)(session.address);
@@ -19254,89 +19260,84 @@ var NodeUserAuthorization = class {
19254
19260
  signature: session.signature
19255
19261
  };
19256
19262
  }
19257
- /**
19258
- * Ensure `tinycloudHosts` are resolved before a host-needing call.
19259
- *
19260
- * Fresh sign-in resolves hosts up front; a restored session may not have
19261
- * (old persisted sessions predate {@link PersistedSessionData.tinycloudHosts}).
19262
- * This guard makes a restored session resolve the node exactly like a fresh
19263
- * sign-in — persisted hosts are preferred (already set by
19264
- * {@link setRestoredTinyCloudSession}), otherwise the registry/fallback
19265
- * resolution runs lazily here. Idempotent: {@link resolveTinyCloudHostsForSignIn}
19266
- * early-returns when hosts are already set.
19267
- *
19268
- * Throws if hosts are unset and the restored session has no address/chainId
19269
- * to resolve from — a real failure that must surface, not be masked.
19270
- */
19263
+ /** Refresh the primary space's registry destinations, including after restore. */
19271
19264
  async ensureTinyCloudHosts() {
19272
- if (this.tinycloudHosts && this.tinycloudHosts.length > 0) {
19273
- return;
19274
- }
19275
19265
  if (this._address === void 0 || this._chainId === void 0) {
19276
- throw new Error(
19277
- "Cannot resolve TinyCloud hosts: no address/chainId available. Sign in or restore a session first."
19278
- );
19266
+ throw new Error("Cannot resolve TinyCloud hosts: sign in or restore a session first.");
19279
19267
  }
19280
- await this.resolveTinyCloudHostsForSignIn(this._address, this._chainId);
19268
+ await this.resolveTinyCloudHostsForSignIn(this._address, this._chainId, this._tinyCloudSession?.spaceId);
19281
19269
  }
19282
- async resolveTinyCloudHostsForSignIn(address, chainId) {
19283
- if (this.tinycloudHosts && this.tinycloudHosts.length > 0) {
19284
- return;
19285
- }
19286
- if (this.registryPolicy === "fail-closed") {
19287
- const resolved2 = await (0, import_sdk_core5.resolveFailClosedTinyCloudHosts)(address, {
19288
- registryUrl: this.tinycloudRegistryUrl,
19289
- registryChainId: this.registryChainId,
19290
- newUserHost: this.newUserHost,
19291
- sequenceStore: this.registrySequenceStore,
19292
- onNodeNotFound: this.onNodeNotFound,
19293
- autoDiscoverLocalNode: this.autoDiscoverLocalNode,
19294
- localNodeUrl: this.localNodeUrl,
19295
- localLinkName: this.localLinkName,
19296
- expectedNodeDid: this.expectedNodeDid,
19297
- localNodeIdentityStore: this.localNodeIdentityStore
19298
- });
19299
- this.tinycloudHosts = resolved2.hosts;
19300
- this.registryVerdict = resolved2.verdict;
19301
- return;
19302
- }
19303
- const subject = (0, import_sdk_core5.pkhDid)(address, chainId);
19304
- const resolved = await (0, import_sdk_core5.resolveTinyCloudHosts)(subject, {
19305
- registryUrl: this.tinycloudRegistryUrl,
19306
- fallbackHosts: this.tinycloudFallbackHosts,
19307
- autoDiscoverLocalNode: this.autoDiscoverLocalNode,
19308
- localNodeUrl: this.localNodeUrl,
19309
- localLinkName: this.localLinkName,
19310
- expectedNodeDid: this.expectedNodeDid,
19311
- localNodeIdentityStore: this.localNodeIdentityStore
19312
- });
19313
- this.tinycloudHosts = resolved.hosts;
19270
+ async resolveTinyCloudHostsForSignIn(address, chainId, restoredSpaceId) {
19271
+ const spaceId = restoredSpaceId ?? this.resolveSignInCapabilities(address, chainId).spaceId;
19272
+ const destinations = await this.resolveResourceDestinations(spaceId, "read");
19273
+ for (const destination of destinations) await (0, import_sdk_services4.verifyResourceDestination)(destination, globalThis.fetch.bind(globalThis));
19274
+ this.tinycloudHosts = destinations.map(({ url }) => url);
19314
19275
  }
19315
- /**
19316
- * Space-creation guard (TC-1443). Under `registryPolicy: "fail-closed"`,
19317
- * throws unless the owner's registry verdict allows a space on the primary
19318
- * host: `NodeRegistryEntryExistsError` when the owner's record does not
19319
- * list it, `RegistryUnavailableError` when the registry gives no
19320
- * verdict. Reuses this session's sign-in verdict unless it was
19321
- * `unavailable`, else looks it up now (explicit-host apps). Under the
19322
- * default policy it returns at once and performs no lookup.
19323
- */
19324
- async assertMayCreateSpace() {
19325
- if (this.registryPolicy !== "fail-closed") return;
19326
- if (this._address === void 0) {
19327
- throw new Error("Must be signed in to create a space");
19276
+ /** Current full-resource registry routing; saved hosts are only no-entry hints. */
19277
+ async resolveResourceDestinations(resource, access, noEntryHosts = this.tinycloudHosts ?? []) {
19278
+ const owner = resource.startsWith("urn:tinycloud:encryption:") ? (0, import_sdk_services4.parseNetworkId)(resource.split("/")[0]).ownerDid : (0, import_sdk_core5.parseSpaceUri)(resource)?.owner ?? (() => {
19279
+ const key2 = /^tinycloud:key:(z[^:]+):[^/]+$/.exec(resource);
19280
+ return key2 ? `did:key:${key2[1]}` : void 0;
19281
+ })();
19282
+ if (!owner) throw new Error(`Cannot determine registry owner for ${resource}`);
19283
+ const subject = owner.startsWith("did:pkh:") ? (0, import_sdk_core5.registryOwnerSubjectV1)(owner.slice(owner.lastIndexOf(":") + 1), this.registryChainId) : owner;
19284
+ for (let attempt = 1; ; attempt++) {
19285
+ try {
19286
+ const resolved = await (0, import_sdk_core5.resolveRegistryResource)(subject, resource, access, {
19287
+ registryUrl: this.tinycloudRegistryUrl,
19288
+ registryChainId: this.registryChainId,
19289
+ sequenceStore: this.registrySequenceStore,
19290
+ cacheStore: this.registryCacheStore,
19291
+ onRegistryFallback: this.onRegistryFallback
19292
+ });
19293
+ this.registryVerdict = resolved.verdict;
19294
+ if (resolved.verdict.status === "unregistered") {
19295
+ if (noEntryHosts.length === 0 && (this.autoDiscoverLocalNode ?? true)) {
19296
+ const local = await (0, import_sdk_core5.discoverLocalTinyCloudNode)({
19297
+ localNodeUrl: this.localNodeUrl,
19298
+ localLinkName: this.localLinkName,
19299
+ expectedNodeDid: this.expectedNodeDid,
19300
+ identityStore: this.localNodeIdentityStore
19301
+ });
19302
+ if (local) {
19303
+ (0, import_sdk_core5.warnRegistryDefaultNode)(resolved.verdict, this.registryCacheStore, this.onRegistryFallback);
19304
+ return [{ url: local.url }];
19305
+ }
19306
+ }
19307
+ const host = noEntryHosts[0] ?? this.newUserHost ?? (this.tinycloudFallbackHosts === void 0 ? "https://node.tinycloud.xyz" : this.tinycloudFallbackHosts?.[0]);
19308
+ if (!host || this.newUserHost === null && noEntryHosts.length === 0) throw new import_sdk_core5.NodeNotFoundError(subject, resolved.verdict);
19309
+ const url = (0, import_sdk_core5.exactHttpOrigin)(host, "new-user host");
19310
+ (0, import_sdk_core5.warnRegistryDefaultNode)(resolved.verdict, this.registryCacheStore, this.onRegistryFallback);
19311
+ return [{ url }];
19312
+ }
19313
+ const destinations = resolved.hosts.flatMap((host) => host.urls.map((url) => ({ url, nodeDid: host.nodeDid })));
19314
+ return destinations;
19315
+ } catch (error) {
19316
+ if (!(error instanceof import_sdk_core5.NodeNotFoundError) && !(error instanceof import_sdk_core5.RegistryUnavailableError)) throw error;
19317
+ if (!this.onNodeNotFound) throw error;
19318
+ const decision = await this.onNodeNotFound({ error, attempt });
19319
+ if (decision.action === "retry") continue;
19320
+ if (decision.action === "use-host" && error.verdict?.status === "unregistered") {
19321
+ const url = (0, import_sdk_core5.exactHttpOrigin)(decision.host, "new-user host");
19322
+ (0, import_sdk_core5.warnRegistryDefaultNode)(error.verdict, this.registryCacheStore, this.onRegistryFallback);
19323
+ return [{ url }];
19324
+ }
19325
+ throw error;
19326
+ }
19328
19327
  }
19329
- await this.ensureTinyCloudHosts();
19330
- const subject = (0, import_sdk_core5.registryOwnerSubjectV1)(this._address, this.registryChainId);
19331
- let verdict = this.registryVerdict;
19332
- if (verdict?.subject !== subject || verdict.status === "unavailable") {
19333
- verdict = await (0, import_sdk_core5.lookupOwnerRegistryVerdict)(subject, {
19334
- registryUrl: this.tinycloudRegistryUrl,
19335
- sequenceStore: this.registrySequenceStore
19336
- });
19337
- this.registryVerdict = verdict;
19328
+ }
19329
+ get registryRecordPresent() {
19330
+ return this.registryVerdict?.status === "registered";
19331
+ }
19332
+ /** Refresh membership before hosting; a saved no-entry verdict never authorizes a later creation. */
19333
+ async assertMayCreateSpace(spaceId = this._tinyCloudSession?.spaceId, host = this.primaryTinyCloudHost) {
19334
+ if (!spaceId) throw new Error("Must be signed in to create a space");
19335
+ const destinations = await this.resolveResourceDestinations(spaceId, "host");
19336
+ if (!destinations.some((destination) => destination.url === host)) {
19337
+ const subject = (0, import_sdk_core5.parseSpaceUri)(spaceId)?.owner;
19338
+ if (!subject || !this.registryVerdict) throw new Error("Cannot resolve space owner");
19339
+ (0, import_sdk_core5.assertSpaceCreationAllowed)(this.registryVerdict, host, spaceId);
19338
19340
  }
19339
- (0, import_sdk_core5.assertSpaceCreationAllowed)(verdict, this.primaryTinyCloudHost);
19340
19341
  }
19341
19342
  requireTinyCloudHosts() {
19342
19343
  if (!this.tinycloudHosts || this.tinycloudHosts.length === 0) {
@@ -19653,7 +19654,7 @@ var NodeUserAuthorization = class {
19653
19654
  if (entry.service !== import_sdk_core5.ENCRYPTION_PERMISSION_SERVICE) {
19654
19655
  return true;
19655
19656
  }
19656
- const parsed = entry.path.includes("/") ? (0, import_resource.parseEncryptionResource)(entry.path) : { ...(0, import_sdk_services3.parseNetworkId)(entry.path), kind: "network" };
19657
+ const parsed = entry.path.includes("/") ? (0, import_resource.parseEncryptionResource)(entry.path) : { ...(0, import_sdk_services4.parseNetworkId)(entry.path), kind: "network" };
19657
19658
  const sharePrefix = parsed.kind === "prefix" && parsed.network === "default" && parsed.prefix === "share" && entry.actions.length > 0 && entry.actions.every((action) => action === DECRYPT_ACTION || action === KEY_CREATE_ACTION) && didPrincipalMatches(parsed.ownerDid, currentDid);
19658
19659
  if (parsed.kind !== "network" && !sharePrefix) {
19659
19660
  throw new import_sdk_core5.ManifestValidationError("named keys go in manifest.keys");
@@ -19766,23 +19767,60 @@ var NodeUserAuthorization = class {
19766
19767
  * Create the space on the TinyCloud server (host delegation).
19767
19768
  * This registers the user as the owner of the space.
19768
19769
  */
19769
- async hostSpace(targetSpaceId, purpose, signal) {
19770
- if (!this._tinyCloudSession || !this._address || !this._chainId) {
19770
+ async hostSpace(targetSpaceId, purpose, signal, assertCurrent, siweParameters) {
19771
+ const context = typeof targetSpaceId === "object" ? targetSpaceId : void 0;
19772
+ const address = context?.address ?? this._address;
19773
+ const chainId = context?.chainId ?? this._chainId;
19774
+ if (!address || chainId === void 0 || !this._tinyCloudSession) {
19771
19775
  throw new Error("Must be signed in to host space");
19772
19776
  }
19777
+ const checkCurrent = () => {
19778
+ signal?.throwIfAborted();
19779
+ assertCurrent?.();
19780
+ };
19781
+ checkCurrent();
19773
19782
  await this.ensureTinyCloudHosts();
19774
- const host = this.primaryTinyCloudHost;
19775
- const spaceId = targetSpaceId ?? this._tinyCloudSession.spaceId;
19783
+ checkCurrent();
19784
+ const host = context?.host ?? this.primaryTinyCloudHost;
19785
+ const spaceId = context?.spaceId ?? (typeof targetSpaceId === "string" ? targetSpaceId : this._tinyCloudSession.spaceId);
19786
+ const destinations = await this.resolveResourceDestinations(spaceId, "host");
19787
+ if (this.registryVerdict?.status === "registered") {
19788
+ const results = [];
19789
+ for (const destination of destinations) {
19790
+ checkCurrent();
19791
+ try {
19792
+ await (0, import_sdk_services4.verifyResourceDestination)(destination, globalThis.fetch.bind(globalThis));
19793
+ const result = await (0, import_sdk_core5.activateSessionWithHost)(destination.url, this._tinyCloudSession.delegationHeader, signal);
19794
+ results.push(result.skipped?.includes(spaceId) ? { ...result, success: false, status: 403, error: "The session does not activate the requested space" } : result);
19795
+ } catch (error) {
19796
+ if ((0, import_sdk_services4.routingFailure)(error) || signal?.aborted) throw error;
19797
+ throw new import_sdk_services4.NodeNotRespondingError([destination.url], error);
19798
+ }
19799
+ }
19800
+ const failed = results.flatMap((result, index) => result.success ? [] : [{ url: destinations[index].url, status: result.status }]);
19801
+ if (failed.length) {
19802
+ if (destinations.length === 1) return results[0];
19803
+ throw new import_sdk_services4.PartialNodeWriteError(destinations.filter((_, index) => results[index].success).map(({ url }) => url), failed);
19804
+ }
19805
+ return {
19806
+ ...results[0],
19807
+ activated: [...new Set(results.flatMap((result) => result.activated ?? []))],
19808
+ skipped: [...new Set(results.flatMap((result) => result.skipped ?? []))]
19809
+ };
19810
+ }
19776
19811
  const peerId = await (0, import_sdk_core5.fetchPeerId)(host, spaceId, signal);
19777
- const siwe = this.wasm.generateHostSIWEMessage({
19778
- address: this._address,
19779
- chainId: this._chainId,
19780
- domain: this.domain,
19781
- issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
19782
- spaceId,
19783
- peerId
19784
- });
19785
- if (signal?.aborted) throw signal.reason;
19812
+ checkCurrent();
19813
+ const siwe = this.wasm.generateHostSIWEMessage(
19814
+ context && siweParameters ? siweParameters(peerId) : {
19815
+ address,
19816
+ chainId,
19817
+ domain: this.domain,
19818
+ issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
19819
+ spaceId,
19820
+ peerId
19821
+ }
19822
+ );
19823
+ checkCurrent();
19786
19824
  const signing = this.signMessage(siwe, purpose);
19787
19825
  let signature2;
19788
19826
  if (!signal) {
@@ -19801,8 +19839,9 @@ var NodeUserAuthorization = class {
19801
19839
  signal.removeEventListener("abort", onAbort);
19802
19840
  }
19803
19841
  }
19804
- if (signal?.aborted) throw signal.reason;
19842
+ checkCurrent();
19805
19843
  const headers = this.wasm.siweToDelegationHeaders({ siwe, signature: signature2 });
19844
+ checkCurrent();
19806
19845
  return (0, import_sdk_core5.submitHostDelegation)(host, headers, signal);
19807
19846
  }
19808
19847
  /**
@@ -19839,13 +19878,45 @@ var NodeUserAuthorization = class {
19839
19878
  if (!this._tinyCloudSession) {
19840
19879
  throw new Error("Must be signed in to ensure space exists");
19841
19880
  }
19842
- await this.ensureTinyCloudHosts();
19843
- const host = this.primaryTinyCloudHost;
19844
19881
  const primarySpaceId = this._tinyCloudSession.spaceId;
19845
- const result = await (0, import_sdk_core5.activateSessionWithHost)(
19846
- host,
19847
- this._tinyCloudSession.delegationHeader
19848
- );
19882
+ const primary = await this.resolveResourceDestinations(primarySpaceId, "read");
19883
+ this.tinycloudHosts = primary.map(({ url }) => url);
19884
+ const targets = new Map(primary.map((destination) => [destination.url, destination]));
19885
+ if (this.registryVerdict?.status === "registered" && this.registryVerdict.record) {
19886
+ const record = this.registryVerdict.record;
19887
+ const resources = Object.keys((0, import_sdk_core5.extractRecapAttenuations)(this._tinyCloudSession.siwe ?? ""));
19888
+ for (const space of record.spaces) {
19889
+ const registeredSpace = (0, import_sdk_core5.parseSpaceUri)(space.space);
19890
+ if (!resources.some((resource) => {
19891
+ const fullSpace = resource.split("/")[0];
19892
+ const requestedSpace = (0, import_sdk_core5.parseSpaceUri)(fullSpace);
19893
+ return fullSpace === space.space || registeredSpace !== null && requestedSpace !== null && registeredSpace.owner === requestedSpace.owner && registeredSpace.name === requestedSpace.name;
19894
+ })) continue;
19895
+ for (const membership of space.nodes) {
19896
+ const node = record.nodes.find((candidate) => candidate.did === membership.did);
19897
+ targets.set(node.url, { url: node.url, nodeDid: node.did });
19898
+ }
19899
+ }
19900
+ if (record.encryptionNode && resources.some((resource) => resource.startsWith("urn:tinycloud:encryption:"))) {
19901
+ const node = record.nodes.find((candidate) => candidate.did === record.encryptionNode);
19902
+ targets.set(node.url, { url: node.url, nodeDid: node.did });
19903
+ }
19904
+ }
19905
+ let result;
19906
+ for (const destination of targets.values()) {
19907
+ await (0, import_sdk_services4.verifyResourceDestination)(destination, globalThis.fetch.bind(globalThis));
19908
+ let activated;
19909
+ try {
19910
+ activated = await (0, import_sdk_core5.activateSessionWithHost)(destination.url, this._tinyCloudSession.delegationHeader);
19911
+ } catch (error) {
19912
+ throw new import_sdk_services4.NodeNotRespondingError([destination.url], error);
19913
+ }
19914
+ if (!activated.success && activated.status !== 404) {
19915
+ throw Object.assign(new Error(`Failed to activate session: ${describeHostFailure(activated)}`), { cause: activated });
19916
+ }
19917
+ if (destination.url === primary[0].url) result = activated;
19918
+ }
19919
+ if (!result) throw new Error("Primary space activation did not return a result");
19849
19920
  this.recordActivationSkippedSpaces(result, primarySpaceId);
19850
19921
  if (!result.success && result.status !== 404) {
19851
19922
  throw Object.assign(
@@ -19864,43 +19935,36 @@ var NodeUserAuthorization = class {
19864
19935
  *
19865
19936
  * @throws Error if the creation handler fails or times out, or hosting fails
19866
19937
  */
19867
- async ensurePrimarySpaceHosted(hosting) {
19868
- if (hosting !== "not-hosted") {
19869
- return;
19870
- }
19938
+ async ensurePrimarySpaceHosted(hosting, signal) {
19871
19939
  const session = this._tinyCloudSession;
19872
- if (!session) {
19873
- throw new Error("Must be signed in to host the primary space");
19874
- }
19940
+ if (hosting !== "not-hosted") return;
19941
+ if (!session) throw new Error("Must be signed in to host the primary space");
19875
19942
  const handler = this.spaceCreationHandler ?? (this.autoCreateSpace ? new import_sdk_core5.AutoApproveSpaceCreationHandler() : void 0);
19876
- if (!handler) {
19877
- return;
19878
- }
19879
- await this.ensureTinyCloudHosts();
19880
- const host = this.primaryTinyCloudHost;
19881
- const primarySpaceId = session.spaceId;
19882
- const creationContext = {
19883
- spaceId: primarySpaceId,
19884
- address: this._address,
19885
- chainId: this._chainId,
19886
- host
19887
- };
19943
+ if (!handler) return;
19888
19944
  const { createPrimarySpace } = await import("./space-hosting.cjs");
19889
19945
  await createPrimarySpace({
19890
19946
  handler,
19891
- context: creationContext,
19892
- guard: () => this.guardSpaceCreation(handler, creationContext),
19893
- host: () => this.hostSpace(primarySpaceId),
19947
+ session,
19948
+ currentSession: () => this._tinyCloudSession,
19949
+ signerAddress: () => this.signer.getAddress(),
19950
+ resolveHost: async () => {
19951
+ await this.ensureTinyCloudHosts();
19952
+ return this.primaryTinyCloudHost;
19953
+ },
19954
+ domain: this.domain,
19955
+ guard: (context) => this.guardSpaceCreation(handler, context),
19956
+ host: (context, assertCurrent, siweParameters) => this.hostSpace(context, void 0, signal, assertCurrent, siweParameters),
19894
19957
  onHosted: () => {
19895
19958
  this._spaceHosting = "hosted";
19896
19959
  },
19897
- activate: () => (0, import_sdk_core5.activateSessionWithHost)(host, session.delegationHeader)
19960
+ activate: (context) => (0, import_sdk_core5.activateSessionWithHost)(context.host, session.delegationHeader),
19961
+ signal
19898
19962
  });
19899
19963
  }
19900
19964
  /** {@link assertMayCreateSpace}, reporting a refusal to the creation handler before rethrowing it. */
19901
19965
  async guardSpaceCreation(handler, context) {
19902
19966
  try {
19903
- await this.assertMayCreateSpace();
19967
+ await this.assertMayCreateSpace(context.spaceId, context.host);
19904
19968
  } catch (error) {
19905
19969
  const failure = error instanceof Error ? error : new Error(String(error));
19906
19970
  handler.onSpaceCreationFailed?.(context, failure);
@@ -21807,6 +21871,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21807
21871
  * ```
21808
21872
  */
21809
21873
  constructor(config = {}) {
21874
+ this.registryRefreshOwners = /* @__PURE__ */ new Set();
21810
21875
  this.signer = null;
21811
21876
  this.auth = null;
21812
21877
  this.consumedOpenKeyPendings = /* @__PURE__ */ new WeakSet();
@@ -21970,8 +22035,10 @@ var _TinyCloudNode = class _TinyCloudNode {
21970
22035
  this.config = {
21971
22036
  ...config,
21972
22037
  host: config.host ?? DEFAULT_HOST,
22038
+ registryPolicy: "fail-closed",
21973
22039
  // One store for every authorization this client creates.
21974
- registrySequenceStore: config.registrySequenceStore ?? (0, import_sdk_core10.createInMemoryRegistrySequenceStore)()
22040
+ registrySequenceStore: config.registrySequenceStore ?? (0, import_sdk_core10.createInMemoryRegistrySequenceStore)(),
22041
+ registryCacheStore: config.registryCacheStore ?? (0, import_sdk_core10.createInMemoryRegistryCacheStore)()
21975
22042
  };
21976
22043
  if (this.config.replication?.enabled) {
21977
22044
  const runtime = getNodeReplicationLoaders().runtime().then(({ ReplicationRuntime: ReplicationRuntime2 }) => {
@@ -22218,6 +22285,18 @@ var _TinyCloudNode = class _TinyCloudNode {
22218
22285
  return "unknown";
22219
22286
  }
22220
22287
  }
22288
+ /** Run the existing primary-space creation flow for the restored session. */
22289
+ async ensurePrimarySpaceHosted(hosting) {
22290
+ const graph = this._serviceGraph;
22291
+ const epoch = this.sessionEpoch;
22292
+ graph.assertActive();
22293
+ this.assertSessionEpoch(epoch, "Space hosting");
22294
+ const auth = this.auth;
22295
+ if (!auth || !this.session) return;
22296
+ await auth.ensurePrimarySpaceHosted(hosting, graph.signal);
22297
+ graph.assertActive();
22298
+ this.assertSessionEpoch(epoch, "Space hosting");
22299
+ }
22221
22300
  /**
22222
22301
  * The node's last answer for the primary space: recorded at sign-in from the
22223
22302
  * activation, or by {@link checkSpaceHosting}. `unknown` after a restore until
@@ -22263,7 +22342,11 @@ var _TinyCloudNode = class _TinyCloudNode {
22263
22342
  // A session saved but never registered on this node is refused: it registers once.
22264
22343
  activate: (signal) => {
22265
22344
  assertCurrent();
22266
- return (0, import_sdk_core10.activateSessionWithHost)(host, session.delegationHeader, AbortSignal.any([signal, graph.signal]));
22345
+ const activationSignal = AbortSignal.any([signal, graph.signal]);
22346
+ if (primary && primary.delegationCid === session.delegationCid) {
22347
+ return this.activateSignedSession(primary, primary.siwe, host, activationSignal);
22348
+ }
22349
+ return this.activateResourceSession(session.delegationHeader, [spaceId], host, activationSignal);
22267
22350
  }
22268
22351
  });
22269
22352
  assertCurrent();
@@ -22399,14 +22482,38 @@ var _TinyCloudNode = class _TinyCloudNode {
22399
22482
  get requests() {
22400
22483
  return this._requestLog === void 0 ? void 0 : (0, import_requestLog2.readOnlyRequestLog)(this._requestLog);
22401
22484
  }
22402
- createServiceGraphLifetime() {
22485
+ throwRegistryRefusal(status, text) {
22486
+ if (status !== 403 && status !== 503 || !text.startsWith("registry_unavailable:") && !text.startsWith("registry_hosting_denied:")) return;
22487
+ const code = status === 503 ? "registry_unavailable" : "node_registry_entry_exists";
22488
+ throw Object.assign(new Error(status === 503 ? "The location registry is unavailable for this resource." : "The registry does not authorize this node for the requested resource."), { code, meta: { status } });
22489
+ }
22490
+ createServiceGraphLifetime(session) {
22491
+ const routedFetch = (0, import_sdk_services5.createResourceRoutingFetch)(
22492
+ async (url, init2) => {
22493
+ try {
22494
+ const response = await globalThis.fetch(url, init2);
22495
+ if (response.status === 403 || response.status === 503) {
22496
+ this.throwRegistryRefusal(response.status, await response.clone().text());
22497
+ }
22498
+ return response;
22499
+ } catch (error) {
22500
+ if ((0, import_sdk_services5.routingFailure)(error)) throw error;
22501
+ if (new URL(url).pathname.startsWith("/encryption/") && !init2?.signal?.aborted) {
22502
+ throw new import_sdk_services5.NodeNotRespondingError([new URL(url).origin], error);
22503
+ }
22504
+ throw error;
22505
+ }
22506
+ },
22507
+ {
22508
+ resolveResource: (resource, access) => this.routingDestinations(resource, access),
22509
+ bindAudience: (authorization, nodeDid) => this.bindRoutedAudience(authorization, nodeDid, session),
22510
+ verifyDestination: (target) => (0, import_sdk_services5.verifyResourceDestination)(target, globalThis.fetch.bind(globalThis))
22511
+ }
22512
+ );
22403
22513
  return new ServiceGraphLifetime(
22404
22514
  this.invokeWithRuntimePermissions,
22405
22515
  this.invokeAnyWithRuntimePermissions,
22406
- // Resolve fetch at request time. This keeps the graph-owned lifetime
22407
- // signal while preserving the SDK's existing injectable global fetch
22408
- // behavior (including adapters that install it after construction).
22409
- (url, init2) => globalThis.fetch(url, init2),
22516
+ routedFetch,
22410
22517
  (context) => {
22411
22518
  this.storageFull.observe(context);
22412
22519
  if (this._requestLog) (0, import_requestLog2.observeReplicaReads)(context, this._requestLog);
@@ -22414,6 +22521,75 @@ var _TinyCloudNode = class _TinyCloudNode {
22414
22521
  this._requestLog
22415
22522
  );
22416
22523
  }
22524
+ async routingDestinations(resource, access, noEntryHosts) {
22525
+ const session = this.currentTinyCloudSession();
22526
+ if (session) this.refreshRegistryOwner((0, import_sdk_core10.pkhDid)(session.address, session.chainId));
22527
+ if (this.auth) return this.auth.resolveResourceDestinations(resource, access, noEntryHosts);
22528
+ const owner = resource.startsWith("urn:tinycloud:encryption:") ? (0, import_sdk_core10.parseNetworkId)(resource.split("/")[0]).ownerDid : (0, import_sdk_core10.parseSpaceUri)(resource)?.owner ?? (() => {
22529
+ const key2 = /^tinycloud:key:(z[^:]+):[^/]+$/.exec(resource);
22530
+ return key2 ? `did:key:${key2[1]}` : void 0;
22531
+ })();
22532
+ if (!owner) throw new Error(`Cannot determine registry owner for ${resource}`);
22533
+ const subject = owner.startsWith("did:pkh:") ? (0, import_sdk_core10.pkhDid)(owner.slice(owner.lastIndexOf(":") + 1), this.config.registryChainId ?? 1) : owner;
22534
+ const resolved = await (0, import_sdk_core10.resolveRegistryResource)(subject, resource, access, {
22535
+ registryUrl: this.config.tinycloudRegistryUrl,
22536
+ registryChainId: this.config.registryChainId,
22537
+ sequenceStore: this.config.registrySequenceStore,
22538
+ cacheStore: this.config.registryCacheStore,
22539
+ onRegistryFallback: this.config.onRegistryFallback
22540
+ });
22541
+ if (resolved.verdict.status === "unregistered") {
22542
+ const hint = noEntryHosts === void 0 ? this.explicitHost ?? this.config.host : noEntryHosts[0];
22543
+ const host = hint ?? this.config.newUserHost ?? (this.config.tinycloudFallbackHosts === void 0 ? DEFAULT_HOST : this.config.tinycloudFallbackHosts?.[0]);
22544
+ if (!host || !hint && this.config.newUserHost === null) throw new import_sdk_core10.NodeNotFoundError(subject, resolved.verdict);
22545
+ const url = (0, import_sdk_core10.exactHttpOrigin)(host, "new-user host");
22546
+ (0, import_sdk_core10.warnRegistryDefaultNode)(resolved.verdict, this.config.registryCacheStore, this.config.onRegistryFallback);
22547
+ return [{ url }];
22548
+ }
22549
+ const destinations = resolved.hosts.flatMap((host) => host.urls.map((url) => ({ url, nodeDid: host.nodeDid })));
22550
+ return destinations;
22551
+ }
22552
+ /** Transport hints cannot override verified full-resource registry routing. */
22553
+ async resolveRegisteredDelegationHost(permissions, noEntryHost) {
22554
+ const resources = /* @__PURE__ */ new Map();
22555
+ let hasData = false;
22556
+ for (const permission of permissions) {
22557
+ const encryption = this.isEncryptionNetworkOperation(this.invocationServiceName(permission.service), permission.path, permission.space);
22558
+ const resource = encryption ? permission.path.split("/")[0] : permission.space;
22559
+ if (!resource) throw new import_sdk_services5.ResourceNotRoutedError("delegation");
22560
+ resources.set(resource, encryption ? "encryption" : "host");
22561
+ hasData || (hasData = !encryption);
22562
+ }
22563
+ let dataHost;
22564
+ let encryptionHost;
22565
+ let unserved;
22566
+ for (const [resource, access] of resources) {
22567
+ try {
22568
+ const destinations = await this.routingDestinations(resource, access, [noEntryHost]);
22569
+ if (destinations.some(({ nodeDid }) => nodeDid === void 0)) return void 0;
22570
+ if (access === "host") dataHost ?? (dataHost = destinations[0]?.url);
22571
+ else encryptionHost ?? (encryptionHost = destinations[0]?.url);
22572
+ } catch (error) {
22573
+ if (!(error instanceof import_sdk_core10.NodeNotFoundError) || error.verdict.status !== "registered") throw error;
22574
+ unserved ?? (unserved = error);
22575
+ }
22576
+ }
22577
+ const host = hasData ? dataHost : encryptionHost;
22578
+ if (!host) throw unserved ?? new import_sdk_services5.ResourceNotRoutedError("delegation");
22579
+ return host;
22580
+ }
22581
+ async bindRoutedAudience(authorization, nodeDid, stagedSession) {
22582
+ const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(authorization.split(".")[1])));
22583
+ const issuer = typeof payload === "object" && payload !== null && "iss" in payload ? payload.iss : void 0;
22584
+ const session = [
22585
+ stagedSession,
22586
+ this._serviceContext?.session,
22587
+ this.auth?.tinyCloudSession,
22588
+ ...this.runtimePermissionGrants.map((grant) => grant.session)
22589
+ ].find((candidate) => candidate?.verificationMethod === issuer);
22590
+ if (!session) throw new Error("Invocation signing session is not active");
22591
+ return rewriteInvocationAudience(authorization, nodeDid, session.jwk);
22592
+ }
22417
22593
  syncResolvedHostFromAuth() {
22418
22594
  const host = this.auth?.hosts[0];
22419
22595
  if (host) {
@@ -22527,6 +22703,29 @@ var _TinyCloudNode = class _TinyCloudNode {
22527
22703
  const session = this.currentTinyCloudSession();
22528
22704
  return session?.siwe ? this.projectSignedRecapCapabilities(session.siwe) : [];
22529
22705
  }
22706
+ /** Start one background refresh for each owner derived from verified authority. */
22707
+ refreshRegistryForCapabilities(permissions) {
22708
+ for (const permission of permissions) {
22709
+ try {
22710
+ const owner = permission.service === "tinycloud.encryption" && permission.path.startsWith("urn:tinycloud:encryption:") ? (0, import_sdk_core10.parseNetworkId)(permission.path.split("/")[0]).ownerDid : permission.space ? this.ownerDidFromSpaceId(permission.space) : void 0;
22711
+ if (owner) this.refreshRegistryOwner(owner);
22712
+ } catch {
22713
+ }
22714
+ }
22715
+ }
22716
+ refreshRegistryOwner(owner) {
22717
+ if (this.config.tinycloudRegistryUrl === null) return;
22718
+ const subject = owner.startsWith("did:pkh:") ? (0, import_sdk_core10.pkhDid)(owner.slice(owner.lastIndexOf(":") + 1), this.config.registryChainId ?? 1) : owner;
22719
+ const key2 = (0, import_sdk_core10.registryOwnerKeyV2)(subject);
22720
+ if (this.registryRefreshOwners.has(key2)) return;
22721
+ this.registryRefreshOwners.add(key2);
22722
+ void (0, import_sdk_core10.refreshOwnerRegistry)(subject, {
22723
+ registryUrl: this.config.tinycloudRegistryUrl,
22724
+ registryChainId: this.config.registryChainId,
22725
+ sequenceStore: this.config.registrySequenceStore,
22726
+ cacheStore: this.config.registryCacheStore
22727
+ }).catch(() => void 0);
22728
+ }
22530
22729
  /** Normalize signed capabilities without collapsing nested encryption into raw authority. */
22531
22730
  projectSignedRecapCapabilities(siwe) {
22532
22731
  const projected = this.parseRecapWithCaveats(siwe).map((entry) => {
@@ -22958,6 +23157,8 @@ var _TinyCloudNode = class _TinyCloudNode {
22958
23157
  const primarySession = this.currentTinyCloudSession();
22959
23158
  if (primarySession) {
22960
23159
  this.registerPrimarySessionGrant(primarySession);
23160
+ this.refreshRegistryOwner((0, import_sdk_core10.pkhDid)(primarySession.address, primarySession.chainId));
23161
+ this.refreshRegistryForCapabilities(this.getVerifiedSessionCapabilities());
22961
23162
  if (this.auth?.spaceHosting === "unknown") {
22962
23163
  await this.checkSpaceHosting();
22963
23164
  this.assertSessionEpoch(epoch, "Sign-in");
@@ -23180,8 +23381,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23180
23381
  const primarySession = auth.tinyCloudSession;
23181
23382
  const defaultSpaceId = this.ownedSpaceId("default");
23182
23383
  const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === import_sdk_core10.BOOTSTRAP_SESSION_REQUESTS.default;
23183
- if (steps.some((step) => step.kind === "host")) {
23184
- await this.assertMayCreateSpace();
23384
+ for (const step of steps) {
23385
+ if (step.kind === "host") await this.assertMayCreateSpace(step.spaceId);
23185
23386
  }
23186
23387
  for (const step of steps) {
23187
23388
  if (step.kind !== "session") continue;
@@ -23230,10 +23431,20 @@ var _TinyCloudNode = class _TinyCloudNode {
23230
23431
  if (!session) {
23231
23432
  throw new Error(`Missing bootstrap session for ${step.space}`);
23232
23433
  }
23233
- const activated = await (0, import_sdk_core10.activateSessionWithHost)(host, session.delegationHeader, graph.signal).catch((error) => {
23234
- graph.assertActive();
23235
- throw error;
23236
- });
23434
+ const destinations = await this.routingDestinations(step.spaceId, "host");
23435
+ let activated;
23436
+ for (const destination of destinations) {
23437
+ try {
23438
+ await (0, import_sdk_services5.verifyResourceDestination)(destination, globalThis.fetch.bind(globalThis));
23439
+ const result = await (0, import_sdk_core10.activateSessionWithHost)(destination.url, session.delegationHeader, graph.signal);
23440
+ if (!result.success) throw Object.assign(new Error("Bootstrap activation failed"), { cause: result });
23441
+ activated = result;
23442
+ } catch (error) {
23443
+ graph.assertActive();
23444
+ throw error;
23445
+ }
23446
+ }
23447
+ if (!activated) throw new Error("Bootstrap space has no destination");
23237
23448
  graph.assertActive();
23238
23449
  if (!activated.success || activated.skipped?.includes(step.spaceId)) {
23239
23450
  throw Object.assign(
@@ -23563,7 +23774,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23563
23774
  if (!this.currentSessionCoversAccountRegistry()) {
23564
23775
  if (!_TinyCloudNode.accountRegistrySkipLogged) {
23565
23776
  _TinyCloudNode.accountRegistrySkipLogged = true;
23566
- import_sdk_services4.tinyCloudDebugLogger.log("registry.skip");
23777
+ import_sdk_services6.tinyCloudDebugLogger.log("registry.skip");
23567
23778
  }
23568
23779
  return;
23569
23780
  }
@@ -23816,6 +24027,13 @@ var _TinyCloudNode = class _TinyCloudNode {
23816
24027
  if (!session) {
23817
24028
  throw new Error("Owned space hosting requires an active session");
23818
24029
  }
24030
+ await this.assertMayCreateSpace(spaceId);
24031
+ if (this.auth.registryRecordPresent) {
24032
+ const hosted = await this.hostOwnedSpaceOnce(spaceId, signal);
24033
+ if (!hosted.success) throw Object.assign(new Error(`Failed to activate listed space ${spaceId}: ${describeHostFailure2(hosted)}`), { cause: hosted });
24034
+ if (this.auth.tinyCloudSession === session) this.confirmedHostedSpaceIds.add(spaceId);
24035
+ return;
24036
+ }
23819
24037
  if (this.confirmedHostedSpaceIds.has(spaceId) || this.hostedOwnedSpaceIds.has(spaceId)) {
23820
24038
  return;
23821
24039
  }
@@ -23834,7 +24052,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23834
24052
  { cause: activation }
23835
24053
  );
23836
24054
  }
23837
- await this.assertMayCreateSpace();
24055
+ await this.assertMayCreateSpace(spaceId);
23838
24056
  if (signal?.aborted) throw signal.reason;
23839
24057
  const created = await this.hostOwnedSpaceOnce(spaceId, signal);
23840
24058
  if (!created.success) {
@@ -23895,17 +24113,19 @@ var _TinyCloudNode = class _TinyCloudNode {
23895
24113
  { cause: hosted }
23896
24114
  );
23897
24115
  }
23898
- const activation = await (0, import_sdk_core10.activateSessionWithHost)(
23899
- host,
23900
- session.delegationHeader
23901
- );
23902
- if (!activation.success || activation.skipped?.includes(spaceId)) {
23903
- throw Object.assign(
23904
- new Error(
23905
- `Failed to activate session for owned space ${spaceId}: ${activation.success ? "space was skipped" : describeHostFailure2(activation)}`
23906
- ),
23907
- { cause: activation }
24116
+ if (!this.auth.registryRecordPresent) {
24117
+ const activation = await (0, import_sdk_core10.activateSessionWithHost)(
24118
+ host,
24119
+ session.delegationHeader
23908
24120
  );
24121
+ if (!activation.success || activation.skipped?.includes(spaceId)) {
24122
+ throw Object.assign(
24123
+ new Error(
24124
+ `Failed to activate session for owned space ${spaceId}: ${activation.success ? "space was skipped" : describeHostFailure2(activation)}`
24125
+ ),
24126
+ { cause: activation }
24127
+ );
24128
+ }
23909
24129
  }
23910
24130
  if (this.auth?.tinyCloudSession === session) this.hostedOwnedSpaceIds.add(spaceId);
23911
24131
  try {
@@ -23949,11 +24169,9 @@ var _TinyCloudNode = class _TinyCloudNode {
23949
24169
  * session's recap doesn't cover it and the registry has no record; the host
23950
24170
  * SIWE is idempotent server-side. Must be called after {@link signIn}.
23951
24171
  *
23952
- * Under `registryPolicy: "fail-closed"`, hosting is refused with
23953
- * `NodeRegistryEntryExistsError` when the owner's registry record does not
23954
- * list the host, or `RegistryUnavailableError` when the registry gives no
23955
- * verdict. {@link hostOwnedSpace} itself is an explicit action and is not
23956
- * guarded.
24172
+ * Current signed per-space membership is checked before both automatic and
24173
+ * explicit hosting. Registered replicas use the session grant; no-entry
24174
+ * onboarding retains the host-SIWE flow.
23957
24175
  *
23958
24176
  * @param name - The owned space name (e.g. `"secrets"`).
23959
24177
  * @returns The hosted owned-space URI.
@@ -23963,27 +24181,20 @@ var _TinyCloudNode = class _TinyCloudNode {
23963
24181
  throw new Error("Not signed in. Call signIn() first.");
23964
24182
  }
23965
24183
  const spaceId = this.ownedSpaceId(name);
24184
+ await this.assertMayCreateSpace(spaceId);
23966
24185
  if (this.hostedOwnedSpaceIds.has(spaceId) || await this.isOwnedSpaceRegistered(spaceId)) {
23967
24186
  return spaceId;
23968
24187
  }
23969
- await this.assertMayCreateSpace();
23970
24188
  return this.hostOwnedSpace(name);
23971
24189
  }
23972
- /**
23973
- * Space-creation guard (TC-1443) for the automatic hosting paths: sign-in's
23974
- * owned spaces, account bootstrap, {@link ensureOwnedSpaceHosted} and
23975
- * {@link ensurePublicSpace}. Only `registryPolicy: "fail-closed"` consults
23976
- * the registry ({@link NodeUserAuthorization.assertMayCreateSpace}); the
23977
- * default policy performs no lookup.
23978
- */
23979
- async assertMayCreateSpace() {
23980
- if (this.config.registryPolicy !== "fail-closed") return;
23981
- await this.auth.assertMayCreateSpace();
24190
+ /** Refresh full-space membership before any automatic or explicit hosting. */
24191
+ async assertMayCreateSpace(spaceId) {
24192
+ await this.routingDestinations(spaceId, "host");
23982
24193
  }
23983
24194
  /** Registry-policy settings shared by every authorization this client creates. */
23984
24195
  registryPolicyConfig() {
23985
- const { registryPolicy, newUserHost, registryChainId, registrySequenceStore, onNodeNotFound } = this.config;
23986
- return { registryPolicy, newUserHost, registryChainId, registrySequenceStore, onNodeNotFound };
24196
+ const { registryPolicy, newUserHost, registryChainId, registrySequenceStore, registryCacheStore, onNodeNotFound, onRegistryFallback } = this.config;
24197
+ return { registryPolicy, newUserHost, registryChainId, registrySequenceStore, registryCacheStore, onNodeNotFound, onRegistryFallback };
23987
24198
  }
23988
24199
  /**
23989
24200
  * Submit the host delegation for an owned space, sharing one in-flight
@@ -24189,12 +24400,9 @@ var _TinyCloudNode = class _TinyCloudNode {
24189
24400
  }
24190
24401
  stagedSessionExpiry = signedExpiry ?? persistedPolicyExpiry ?? persistedExpiry(void 0);
24191
24402
  }
24192
- const resolvedHost = await this.resolveRestoredHost(
24193
- sessionData.tinycloudHosts,
24194
- restoredAddress,
24195
- sessionData.chainId
24196
- );
24197
- const stagedHost = resolvedHost ?? this.config.host;
24403
+ const restoreHints = this.explicitHost ? [this.explicitHost] : sessionData.tinycloudHosts ?? [];
24404
+ const resolvedHost = this.config.replication?.enabled ? (await this.routingDestinations(sessionData.spaceId, "read", restoreHints))[0]?.url : void 0;
24405
+ const stagedHost = resolvedHost ?? (0, import_sdk_core10.exactHttpOrigin)(restoreHints[0] ?? this.config.host, "restored host hint");
24198
24406
  let restoredNodeFeatures;
24199
24407
  let replicaProtocolMismatch = false;
24200
24408
  if (this.config.replication?.enabled) {
@@ -24311,7 +24519,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24311
24519
  this.sessionManager = stagedManager;
24312
24520
  this.sessionKeyId = stagedKeyId;
24313
24521
  this.sessionKeyJwk = stagedJwk;
24314
- if (resolvedHost) this.config.host = resolvedHost;
24522
+ this.config.host = stagedHost;
24315
24523
  if (spaceOwnerKey(serviceSession.spaceId) !== this.currentStorageOwner()) {
24316
24524
  this.storageFull.clear();
24317
24525
  this._requestLog?.clear();
@@ -24378,6 +24586,10 @@ var _TinyCloudNode = class _TinyCloudNode {
24378
24586
  if (attempt === this.latestSessionAttempt) await this.endSession();
24379
24587
  this.assertSessionEpoch(epoch, "Session restore");
24380
24588
  }
24589
+ if (this.config.replication?.enabled) {
24590
+ if (stagedAddress !== void 0) this.refreshRegistryOwner((0, import_sdk_core10.pkhDid)(stagedAddress, stagedChainId));
24591
+ this.refreshRegistryForCapabilities(this.getVerifiedSessionCapabilities());
24592
+ }
24381
24593
  }
24382
24594
  stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
24383
24595
  if (!session?.siwe) return { cache: void 0, grants: [] };
@@ -24426,7 +24638,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24426
24638
  };
24427
24639
  }
24428
24640
  stageRestoredServiceGraph(input) {
24429
- const graph = this.createServiceGraphLifetime();
24641
+ const graph = this.createServiceGraphLifetime(input.serviceSession);
24430
24642
  const core = this.auth ? new import_sdk_core10.TinyCloud(this.auth, {
24431
24643
  invokeAny: graph.invokeAny,
24432
24644
  telemetry: this.config.telemetry,
@@ -24651,56 +24863,6 @@ var _TinyCloudNode = class _TinyCloudNode {
24651
24863
  spaceService.updateConfig({ sharingService });
24652
24864
  return { core, graph, serviceContext, kv, sql, duckdb, hooks, vault, encryption, capabilityRegistry, keyProvider, sharingService, delegationManager, spaceService };
24653
24865
  }
24654
- /**
24655
- * Resolve the host a restored session should target.
24656
- *
24657
- * Mirrors fresh sign-in host resolution but for the restore path:
24658
- * an explicit/pinned host always wins, then the hosts the session was
24659
- * persisted with, then a lazy registry/fallback resolution for sessions
24660
- * that predate the persisted `tinycloudHosts` field. Returns `undefined`
24661
- * only when there's nothing to resolve from (no explicit host, no
24662
- * persisted hosts, and no address/chainId) — in which case the existing
24663
- * `config.host` (default) is left in place.
24664
- *
24665
- * Resolution failures are surfaced, not swallowed: a genuinely broken
24666
- * registry lookup throws rather than silently falling back to a wrong host.
24667
- */
24668
- async resolveRestoredHost(persistedHosts, address, chainId) {
24669
- if (this.explicitHost) {
24670
- return this.explicitHost;
24671
- }
24672
- if (persistedHosts && persistedHosts.length > 0) {
24673
- return persistedHosts[0];
24674
- }
24675
- if (address === void 0 || chainId === void 0) {
24676
- return void 0;
24677
- }
24678
- if (this.config.registryPolicy === "fail-closed") {
24679
- const resolved2 = await (0, import_sdk_core10.resolveFailClosedTinyCloudHosts)(address, {
24680
- registryUrl: this.config.tinycloudRegistryUrl,
24681
- registryChainId: this.config.registryChainId,
24682
- newUserHost: this.config.newUserHost,
24683
- sequenceStore: this.config.registrySequenceStore,
24684
- onNodeNotFound: this.config.onNodeNotFound,
24685
- autoDiscoverLocalNode: this.config.autoDiscoverLocalNode,
24686
- localNodeUrl: this.config.localNodeUrl,
24687
- localLinkName: this.config.localLinkName,
24688
- expectedNodeDid: this.config.expectedNodeDid,
24689
- localNodeIdentityStore: this.config.localNodeIdentityStore
24690
- });
24691
- return resolved2.hosts[0];
24692
- }
24693
- const resolved = await (0, import_sdk_core10.resolveTinyCloudHosts)((0, import_sdk_core10.pkhDid)(address, chainId), {
24694
- registryUrl: this.config.tinycloudRegistryUrl,
24695
- fallbackHosts: this.config.tinycloudFallbackHosts,
24696
- autoDiscoverLocalNode: this.config.autoDiscoverLocalNode,
24697
- localNodeUrl: this.config.localNodeUrl,
24698
- localLinkName: this.config.localLinkName,
24699
- expectedNodeDid: this.config.expectedNodeDid,
24700
- localNodeIdentityStore: this.config.localNodeIdentityStore
24701
- });
24702
- return resolved.hosts[0];
24703
- }
24704
24866
  /**
24705
24867
  * Resolve the currently-active TinyCloudSession, preferring the auth
24706
24868
  * layer's value (wallet mode) and falling back to the node-level
@@ -25175,10 +25337,10 @@ var _TinyCloudNode = class _TinyCloudNode {
25175
25337
  * second time. Only a value recorded for this exact host qualifies; older
25176
25338
  * nodes that omit `nodeId` fall through to the fetch.
25177
25339
  */
25178
- async fetchNodeId() {
25179
- const cached = this.auth?.nodeIdForHost(this.config.host);
25340
+ async fetchNodeId(host = this.config.host) {
25341
+ const cached = this.auth?.nodeIdForHost(host);
25180
25342
  if (cached) return cached;
25181
- const response = await fetch(`${this.config.host}/info`);
25343
+ const response = await fetch(`${host}/info`);
25182
25344
  if (!response.ok) {
25183
25345
  throw await (0, import_sdk_core10.httpResponseError)(response, "Failed to fetch node info");
25184
25346
  }
@@ -25226,10 +25388,24 @@ var _TinyCloudNode = class _TinyCloudNode {
25226
25388
  )
25227
25389
  };
25228
25390
  }
25229
- async fetchEncryptionNetworkAt(host, networkId, fetchFn) {
25230
- const response = await fetchFn(
25231
- `${host}/encryption/networks/${encodeURIComponent(networkId)}`
25232
- );
25391
+ async encryptionDestination(networkId) {
25392
+ const destination = (await this.routingDestinations(networkId, "encryption"))[0];
25393
+ if (!destination) throw new Error("No encryption destination is registered");
25394
+ await (0, import_sdk_services5.verifyResourceDestination)(destination, globalThis.fetch.bind(globalThis));
25395
+ return { ...destination, nodeDid: destination.nodeDid ?? await this.fetchNodeId(destination.url) };
25396
+ }
25397
+ async fetchEncryptionNetworkAt(host, networkId, fetchFn, callerSignal) {
25398
+ let response;
25399
+ const timeout = AbortSignal.timeout(5e3);
25400
+ try {
25401
+ response = await fetchFn(`${host}/encryption/networks/${encodeURIComponent(networkId)}`, {
25402
+ signal: callerSignal ? AbortSignal.any([callerSignal, timeout]) : timeout,
25403
+ redirect: "error"
25404
+ });
25405
+ } catch (error) {
25406
+ if ((0, import_sdk_services5.routingFailure)(error) || callerSignal?.aborted || !timeout.aborted && error instanceof Error && error.name === "AbortError") throw error;
25407
+ throw new import_sdk_services5.NodeNotRespondingError([host], error);
25408
+ }
25233
25409
  if (response.status === 404) return null;
25234
25410
  if (!response.ok) {
25235
25411
  throw await (0, import_sdk_core10.httpResponseError)(response, `Failed to fetch encryption network ${networkId}`);
@@ -25239,18 +25415,18 @@ var _TinyCloudNode = class _TinyCloudNode {
25239
25415
  }
25240
25416
  createEncryptionService(binding) {
25241
25417
  const graph = binding?.graph ?? this._serviceGraph;
25242
- const host = binding?.host ?? this.config.host;
25243
25418
  const session = binding?.session ?? this.requireEncryptionSession();
25244
25419
  const did = binding?.did ?? this.did;
25245
25420
  const address = binding?.address ?? this._address;
25246
25421
  const chainId = binding?.chainId ?? this._chainId;
25247
25422
  const crypto3 = this.createEncryptionCrypto();
25248
- const descriptorCache = /* @__PURE__ */ new Map();
25249
25423
  const transport = {
25250
- postDecrypt: async ({ networkId, authorization, canonicalBody, signal }) => {
25424
+ postDecrypt: async ({ targetNode, networkId, authorization, canonicalBody, signal }) => {
25251
25425
  graph.assertActive();
25426
+ const destination = await this.encryptionDestination(networkId);
25427
+ if (destination.nodeDid !== targetNode) throw new import_sdk_services5.NodeIdentityMismatchError(destination.url, destination.nodeDid, targetNode);
25252
25428
  const response = await graph.fetch(
25253
- `${host}/encryption/networks/${encodeURIComponent(networkId)}/decrypt`,
25429
+ `${destination.url}/encryption/networks/${encodeURIComponent(networkId)}/decrypt`,
25254
25430
  {
25255
25431
  method: "POST",
25256
25432
  headers: {
@@ -25263,7 +25439,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25263
25439
  );
25264
25440
  graph.assertActive();
25265
25441
  if (!response.ok) {
25266
- const permissionHint = response.status === 401 || response.status === 403 ? (0, import_sdk_services4.parsePermissionHint)({
25442
+ const permissionHint = response.status === 401 || response.status === 403 ? (0, import_sdk_services6.parsePermissionHint)({
25267
25443
  service: "tinycloud.encryption",
25268
25444
  path: networkId,
25269
25445
  actions: ["tinycloud.encryption/decrypt"]
@@ -25278,6 +25454,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25278
25454
  }
25279
25455
  };
25280
25456
  return new import_sdk_core10.EncryptionService({
25457
+ resolveTargetNode: async (networkId) => (await this.encryptionDestination(networkId)).nodeDid,
25281
25458
  crypto: crypto3,
25282
25459
  signer: {
25283
25460
  signDecryptInvocation: async (input) => {
@@ -25301,18 +25478,8 @@ var _TinyCloudNode = class _TinyCloudNode {
25301
25478
  node: {
25302
25479
  fetchByNetworkId: async (networkId) => {
25303
25480
  graph.assertActive();
25304
- let pending = descriptorCache.get(networkId);
25305
- if (!pending) {
25306
- pending = this.fetchEncryptionNetworkAt(host, networkId, graph.fetch).then((descriptor2) => {
25307
- if (descriptor2 === null) descriptorCache.delete(networkId);
25308
- return descriptor2;
25309
- }).catch((error) => {
25310
- descriptorCache.delete(networkId);
25311
- throw error;
25312
- });
25313
- descriptorCache.set(networkId, pending);
25314
- }
25315
- const descriptor = await pending;
25481
+ const destination = await this.encryptionDestination(networkId);
25482
+ const descriptor = await this.fetchEncryptionNetworkAt(destination.url, networkId, graph.fetch, graph.signal);
25316
25483
  graph.assertActive();
25317
25484
  return descriptor;
25318
25485
  }
@@ -25320,13 +25487,15 @@ var _TinyCloudNode = class _TinyCloudNode {
25320
25487
  wellKnown: {
25321
25488
  fetchWellKnown: async (principal, discoveryKey) => {
25322
25489
  graph.assertActive();
25490
+ const destination = await this.encryptionDestination(this.getDefaultEncryptionNetworkId());
25491
+ if (this.auth?.registryRecordPresent) return null;
25323
25492
  if (!address || !didPrincipalMatches2(principal, did)) {
25324
25493
  return null;
25325
25494
  }
25326
25495
  const publicSpaceId = (0, import_sdk_core10.makePublicSpaceId)(address, chainId);
25327
25496
  const encodedKey = discoveryKey.split("/").map(encodeURIComponent).join("/");
25328
25497
  const response = await graph.fetch(
25329
- `${host}/public/${encodeURIComponent(publicSpaceId)}/kv/${encodedKey}`,
25498
+ `${destination.url}/public/${encodeURIComponent(publicSpaceId)}/kv/${encodedKey}`,
25330
25499
  { method: "GET" }
25331
25500
  );
25332
25501
  graph.assertActive();
@@ -25696,7 +25865,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25696
25865
  const signature2 = await this.signer.signMessage(prepared.siwe);
25697
25866
  assertOwnerGraphActive();
25698
25867
  const delegationSession = this.wasmBindings.completeSessionSetup({ ...prepared, signature: signature2 });
25699
- const activation = await (0, import_sdk_core10.activateSessionWithHost)(host, delegationSession.delegationHeader);
25868
+ const activation = await this.activateSignedSession(delegationSession, prepared.siwe, host);
25700
25869
  assertOwnerGraphActive();
25701
25870
  if (!activation.success) {
25702
25871
  throw Object.assign(
@@ -26015,15 +26184,16 @@ var _TinyCloudNode = class _TinyCloudNode {
26015
26184
  async getEncryptionNetwork(nameOrNetworkId = this.getDefaultEncryptionNetworkId()) {
26016
26185
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
26017
26186
  return this.fetchEncryptionNetworkAt(
26018
- this.config.host,
26187
+ (await this.encryptionDestination(networkId)).url,
26019
26188
  networkId,
26020
26189
  globalThis.fetch.bind(globalThis)
26021
26190
  );
26022
26191
  }
26023
26192
  async createEncryptionNetwork(name = DEFAULT_ENCRYPTION_NETWORK_NAME) {
26024
- const targetNode = await this.fetchNodeId();
26025
26193
  const ownerDid = this.did;
26026
26194
  const networkId = this.getDefaultEncryptionNetworkId(name);
26195
+ const destination = await this.encryptionDestination(networkId);
26196
+ const targetNode = destination.nodeDid;
26027
26197
  const body = {
26028
26198
  name,
26029
26199
  ownerDid,
@@ -26046,7 +26216,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26046
26216
  action: NETWORK_CREATE_ACTION2,
26047
26217
  facts
26048
26218
  });
26049
- const response = await fetch(`${this.config.host}/encryption/networks`, {
26219
+ const response = await this._serviceGraph.fetch(`${destination.url}/encryption/networks`, {
26050
26220
  method: "POST",
26051
26221
  headers: {
26052
26222
  Authorization: signed2.authorization,
@@ -26139,7 +26309,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26139
26309
  );
26140
26310
  if (result.status !== "ok") {
26141
26311
  if (result.status === "permission_required") {
26142
- const hint = (0, import_sdk_services4.parsePermissionHint)(result.hint);
26312
+ const hint = (0, import_sdk_services6.parsePermissionHint)(result.hint);
26143
26313
  if (hint === void 0) return { status: "read_failed" };
26144
26314
  return {
26145
26315
  status: "permission_required",
@@ -26386,6 +26556,97 @@ var _TinyCloudNode = class _TinyCloudNode {
26386
26556
  }
26387
26557
  return { recap: exactVerifiedRecap(verified), expiresAt };
26388
26558
  }
26559
+ async activateResourceSession(delegationHeader, resources, hostHint, signal) {
26560
+ const targets = /* @__PURE__ */ new Map();
26561
+ const routedSpaces = [];
26562
+ let unserved;
26563
+ let requestedResource;
26564
+ for (const resource of new Set(resources)) {
26565
+ requestedResource ?? (requestedResource = resource);
26566
+ signal?.throwIfAborted();
26567
+ try {
26568
+ const encryption = resource.startsWith("urn:tinycloud:encryption:");
26569
+ const destinations = await this.routingDestinations(
26570
+ resource,
26571
+ encryption ? "encryption" : "host",
26572
+ [hostHint ?? this.config.host]
26573
+ );
26574
+ if (!encryption && destinations.some(({ nodeDid }) => nodeDid !== void 0)) routedSpaces.push(resource);
26575
+ for (const destination of destinations) {
26576
+ let target = targets.get(destination.url);
26577
+ if (!target) {
26578
+ target = { destination, spaces: [] };
26579
+ targets.set(destination.url, target);
26580
+ }
26581
+ if (!encryption && destination.nodeDid !== void 0) target.spaces.push(resource);
26582
+ }
26583
+ } catch (error) {
26584
+ if (!(error instanceof import_sdk_core10.NodeNotFoundError)) throw error;
26585
+ unserved ?? (unserved = error);
26586
+ }
26587
+ }
26588
+ if (targets.size === 0) throw unserved ?? new import_sdk_services5.ResourceNotRoutedError(requestedResource ?? "session grant");
26589
+ const succeeded = [];
26590
+ const accepted = [];
26591
+ const failed = [];
26592
+ let refusal;
26593
+ for (const { destination: target, spaces } of targets.values()) {
26594
+ signal?.throwIfAborted();
26595
+ try {
26596
+ await (0, import_sdk_services5.verifyResourceDestination)(target, globalThis.fetch.bind(globalThis));
26597
+ const timeout = AbortSignal.timeout(5e3);
26598
+ const result = await (0, import_sdk_core10.activateSessionWithHost)(
26599
+ target.url,
26600
+ delegationHeader,
26601
+ signal ? AbortSignal.any([signal, timeout]) : timeout
26602
+ );
26603
+ this.throwRegistryRefusal(result.status, result.error ?? "");
26604
+ if (result.success) {
26605
+ const skipped = result.skipped?.find((space) => spaces.some((expected) => this.spaceIdsEqual(space, expected)));
26606
+ if (skipped) {
26607
+ failed.push({ url: target.url, status: result.status, cause: new import_sdk_services5.ResourceNotRoutedError(skipped) });
26608
+ continue;
26609
+ }
26610
+ succeeded.push(target.url);
26611
+ accepted.push(result);
26612
+ } else {
26613
+ refusal = result;
26614
+ failed.push({ url: target.url, status: result.status, cause: result });
26615
+ }
26616
+ } catch (error) {
26617
+ if (signal?.aborted) throw error;
26618
+ failed.push({ url: target.url, cause: (0, import_sdk_services5.routingFailure)(error) ? error : new import_sdk_services5.NodeNotRespondingError([target.url], error) });
26619
+ }
26620
+ }
26621
+ if (failed.length > 0) {
26622
+ if (targets.size > 1) throw new import_sdk_services5.PartialNodeWriteError(succeeded, failed);
26623
+ if (refusal) return refusal;
26624
+ throw failed[0].cause;
26625
+ }
26626
+ const activated = new Set(accepted.flatMap((result) => result.activated ?? []));
26627
+ return {
26628
+ ...accepted[0],
26629
+ activated: [...activated],
26630
+ skipped: [...new Set(accepted.flatMap((result) => result.skipped ?? []))].filter((space) => !activated.has(space) && !routedSpaces.some((expected) => this.spaceIdsEqual(space, expected)))
26631
+ };
26632
+ }
26633
+ activateRoutedDelegation(delegation, signal) {
26634
+ const resources = this.operationsFromDelegation(delegation).map(
26635
+ (operation) => operation.resource?.startsWith("urn:tinycloud:encryption:") ? operation.resource.split("/")[0] : operation.spaceId ?? delegation.spaceId
26636
+ );
26637
+ return this.activateResourceSession(delegation.delegationHeader, resources, delegation.host, signal);
26638
+ }
26639
+ activateSignedSession(session, siwe, hostHint, signal) {
26640
+ const resources = this.parseRecapWithCaveats(siwe).map(
26641
+ (entry) => this.isEncryptionNetworkOperation(this.invocationServiceName(entry.service), entry.path, entry.space) ? entry.path.split("/")[0] : entry.space.startsWith("tinycloud:") ? entry.space : session.spaceId
26642
+ );
26643
+ return this.activateResourceSession(
26644
+ session.delegationHeader,
26645
+ resources.length > 0 ? resources : [session.spaceId],
26646
+ hostHint,
26647
+ signal
26648
+ );
26649
+ }
26389
26650
  /**
26390
26651
  * Install a portable runtime permission delegation into this SDK instance so
26391
26652
  * matching service calls and downstream `delegateTo()` calls can use it.
@@ -26405,16 +26666,12 @@ var _TinyCloudNode = class _TinyCloudNode {
26405
26666
  `Runtime delegation targets ${delegation.delegateDID} but this session key is ${session.verificationMethod}.`
26406
26667
  );
26407
26668
  }
26408
- const targetHost = delegation.host ?? this.config.host;
26409
26669
  let activateResult;
26410
26670
  try {
26411
- activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
26412
- targetHost,
26413
- delegation.delegationHeader,
26414
- graph.signal
26415
- );
26671
+ activateResult = await this.activateRoutedDelegation(delegation, graph.signal);
26416
26672
  } catch (cause) {
26417
26673
  graph.assertActive();
26674
+ if ((0, import_sdk_services5.routingFailure)(cause)) throw cause;
26418
26675
  throw Object.assign(
26419
26676
  new Error("The TinyCloud host could not be reached to activate the delegation."),
26420
26677
  { code: "ACTIVATION_TRANSPORT_FAILED", cause }
@@ -26570,11 +26827,15 @@ var _TinyCloudNode = class _TinyCloudNode {
26570
26827
  ...prepared,
26571
26828
  signature: signature2
26572
26829
  });
26573
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
26574
- this.config.host,
26575
- delegatedSession.delegationHeader,
26576
- graph.signal
26577
- ).catch((error) => {
26830
+ const delegation = this.runtimeDelegationFromSession(
26831
+ delegatedSession,
26832
+ delegatedEntries,
26833
+ spaceId,
26834
+ captured,
26835
+ expiresAt,
26836
+ { siwe: prepared.siwe, signature: signature2 }
26837
+ );
26838
+ const activateResult = await this.activateRoutedDelegation(delegation, graph.signal).catch((error) => {
26578
26839
  graph.assertActive();
26579
26840
  throw error;
26580
26841
  });
@@ -26590,14 +26851,6 @@ var _TinyCloudNode = class _TinyCloudNode {
26590
26851
  { cause: activateResult }
26591
26852
  );
26592
26853
  }
26593
- const delegation = this.runtimeDelegationFromSession(
26594
- delegatedSession,
26595
- delegatedEntries,
26596
- spaceId,
26597
- captured,
26598
- expiresAt,
26599
- { siwe: prepared.siwe, signature: signature2 }
26600
- );
26601
26854
  graph.assertActive();
26602
26855
  assertCapturedKey?.();
26603
26856
  this.runtimePermissionGrants.push({
@@ -26742,7 +26995,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26742
26995
  if (!publicSpaceId) {
26743
26996
  throw new Error("Public space not enabled. Set enablePublicSpace: true in config.");
26744
26997
  }
26745
- await this.assertMayCreateSpace();
26998
+ await this.assertMayCreateSpace(publicSpaceId);
26746
26999
  await this.auth.hostPublicSpace(publicSpaceId);
26747
27000
  const kvActions = [import_sdk_core10.KV.PUT, import_sdk_core10.KV.GET, import_sdk_core10.KV.DEL, import_sdk_core10.KV.LIST, import_sdk_core10.KV.METADATA];
26748
27001
  const abilities = { kv: { "": kvActions } };
@@ -26765,10 +27018,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26765
27018
  ...prepared,
26766
27019
  signature: signature2
26767
27020
  });
26768
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
26769
- this.config.host,
26770
- delegationSession.delegationHeader
26771
- );
27021
+ const activateResult = await this.activateSignedSession(delegationSession, prepared.siwe);
26772
27022
  if (!activateResult.success) {
26773
27023
  throw Object.assign(
26774
27024
  new Error(`Failed to activate public space delegation: ${describeHostFailure2(activateResult)}`),
@@ -27318,10 +27568,7 @@ var _TinyCloudNode = class _TinyCloudNode {
27318
27568
  if (onPrepared) {
27319
27569
  await onPrepared(delegation);
27320
27570
  }
27321
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
27322
- this.config.host,
27323
- delegationHeader
27324
- );
27571
+ const activateResult = await this.activateRoutedDelegation(delegation);
27325
27572
  assertDelegationActivated(activateResult, delegation, requireActivation);
27326
27573
  return delegation;
27327
27574
  }
@@ -27355,10 +27602,7 @@ var _TinyCloudNode = class _TinyCloudNode {
27355
27602
  if (onPrepared) {
27356
27603
  await onPrepared(delegation);
27357
27604
  }
27358
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
27359
- targetHost,
27360
- delegationHeader
27361
- );
27605
+ const activateResult = await this.activateRoutedDelegation(delegation);
27362
27606
  assertDelegationActivated(activateResult, delegation, requireActivation);
27363
27607
  return delegation;
27364
27608
  }
@@ -28010,10 +28254,7 @@ var _TinyCloudNode = class _TinyCloudNode {
28010
28254
  ...prepared,
28011
28255
  signature: signature2
28012
28256
  });
28013
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
28014
- this.config.host,
28015
- delegationSession.delegationHeader
28016
- );
28257
+ const activateResult = await this.activateSignedSession(delegationSession, prepared.siwe);
28017
28258
  if (!activateResult.success) {
28018
28259
  throw Object.assign(
28019
28260
  new Error(`Failed to activate delegation: ${describeHostFailure2(activateResult)}`),
@@ -28058,10 +28299,7 @@ var _TinyCloudNode = class _TinyCloudNode {
28058
28299
  ...publicPrepared,
28059
28300
  signature: publicSignature
28060
28301
  });
28061
- const publicActivateResult = await (0, import_sdk_core10.activateSessionWithHost)(
28062
- this.config.host,
28063
- publicSession.delegationHeader
28064
- );
28302
+ const publicActivateResult = await this.activateSignedSession(publicSession, publicPrepared.siwe);
28065
28303
  if (publicActivateResult.success) {
28066
28304
  result.publicDelegation = {
28067
28305
  cid: publicSession.delegationCid,
@@ -28167,11 +28405,7 @@ var _TinyCloudNode = class _TinyCloudNode {
28167
28405
  ...prepared,
28168
28406
  signature: signature2
28169
28407
  });
28170
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
28171
- targetHost,
28172
- invokerSession.delegationHeader,
28173
- graph.signal
28174
- ).catch((error) => {
28408
+ const activateResult = await this.activateSignedSession(invokerSession, prepared.siwe, targetHost, graph.signal).catch((error) => {
28175
28409
  graph.assertActive();
28176
28410
  throw error;
28177
28411
  });
@@ -28298,10 +28532,7 @@ var _TinyCloudNode = class _TinyCloudNode {
28298
28532
  ...prepared,
28299
28533
  signature: signature2
28300
28534
  });
28301
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
28302
- targetHost,
28303
- subDelegationSession.delegationHeader
28304
- );
28535
+ const activateResult = await this.activateSignedSession(subDelegationSession, prepared.siwe, targetHost);
28305
28536
  if (!activateResult.success) {
28306
28537
  throw Object.assign(
28307
28538
  new Error(`Failed to activate sub-delegation: ${describeHostFailure2(activateResult)}`),
@@ -28365,6 +28596,7 @@ registerReplicationLoaders({
28365
28596
 
28366
28597
  // src/index.ts
28367
28598
  var import_sdk_core15 = require("@tinycloud/sdk-core");
28599
+ var import_sdk_services8 = require("@tinycloud/sdk-services");
28368
28600
  var import_sdk_core16 = require("@tinycloud/sdk-core");
28369
28601
 
28370
28602
  // src/storage/FileSessionStorage.ts
@@ -28564,7 +28796,7 @@ var import_node_fs = require("fs");
28564
28796
  var import_promises = require("fs/promises");
28565
28797
  var import_node_path = require("path");
28566
28798
  var import_sdk_core14 = require("@tinycloud/sdk-core");
28567
- var import_sdk_services5 = require("@tinycloud/sdk-services");
28799
+ var import_sdk_services7 = require("@tinycloud/sdk-services");
28568
28800
  var import_contractConstants = require("@tinycloud/sdk-services/kv/replication/contractConstants");
28569
28801
  init_authority();
28570
28802
  var REPLICA_LOGOUT_PENDING_FILE = "replica-logout-pending.json";
@@ -29126,7 +29358,7 @@ function sha256Base32(material) {
29126
29358
  return out.slice(0, 26);
29127
29359
  }
29128
29360
  function partitionDirOf(root, identity) {
29129
- return (0, import_node_path.join)(root, sha256Base32((0, import_sdk_services5.replicationIdentityKey)(identity)));
29361
+ return (0, import_node_path.join)(root, sha256Base32((0, import_sdk_services7.replicationIdentityKey)(identity)));
29130
29362
  }
29131
29363
  function replicaHashOf(prefix, deviceDid) {
29132
29364
  return sha256Base32(`${prefix}
@@ -29200,8 +29432,8 @@ function deviceRecordOf(value) {
29200
29432
  }
29201
29433
  function sameIdentity(a, b) {
29202
29434
  try {
29203
- const left = (0, import_sdk_services5.canonicalReplicationIdentity)(a);
29204
- const right = (0, import_sdk_services5.canonicalReplicationIdentity)(b);
29435
+ const left = (0, import_sdk_services7.canonicalReplicationIdentity)(a);
29436
+ const right = (0, import_sdk_services7.canonicalReplicationIdentity)(b);
29205
29437
  return left.host === right.host && left.space === right.space && left.principal === right.principal;
29206
29438
  } catch {
29207
29439
  return false;
@@ -29217,7 +29449,7 @@ async function ensurePartitionIdentity(dir, identity, dirSync) {
29217
29449
  const file = (0, import_node_path.join)(dir, "identity.json");
29218
29450
  const existing = await readJsonFile(file);
29219
29451
  if (existing === void 0) {
29220
- await writeJsonAtomic(file, { v: 1, ...(0, import_sdk_services5.canonicalReplicationIdentity)(identity) }, dirSync);
29452
+ await writeJsonAtomic(file, { v: 1, ...(0, import_sdk_services7.canonicalReplicationIdentity)(identity) }, dirSync);
29221
29453
  return;
29222
29454
  }
29223
29455
  const record = identityRecordOf(existing);
@@ -29316,7 +29548,7 @@ withGate_fn = async function(section) {
29316
29548
  if (snapshot === void 0) throw gated("gate_unavailable");
29317
29549
  const [principalHash, identityKeyHash] = await Promise.all([
29318
29550
  gate.principalHashOf(snapshot.salt, this.identity.principal),
29319
- gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(this.identity))
29551
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services7.replicationIdentityKey)(this.identity))
29320
29552
  ]);
29321
29553
  const access = {
29322
29554
  stamp: __privateGet(this, _stamp),
@@ -29817,7 +30049,7 @@ function createSqliteReplicaStorage(options) {
29817
30049
  if (snapshot === void 0) throw gated("gate_unavailable");
29818
30050
  const [principalHash, identityKeyHash] = await Promise.all([
29819
30051
  gate.principalHashOf(snapshot.salt, principal),
29820
- identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(identity))
30052
+ identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, (0, import_sdk_services7.replicationIdentityKey)(identity))
29821
30053
  ]);
29822
30054
  return profileSection(async () => {
29823
30055
  assertLogoutClear(options.logoutPendingFile);
@@ -29840,7 +30072,7 @@ function createSqliteReplicaStorage(options) {
29840
30072
  if (snapshot === void 0) throw gated("gate_unavailable");
29841
30073
  const [principalHash, identityKeyHash] = await Promise.all([
29842
30074
  gate.principalHashOf(snapshot.salt, spec.identity.principal),
29843
- gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(spec.identity))
30075
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services7.replicationIdentityKey)(spec.identity))
29844
30076
  ]);
29845
30077
  const access = {
29846
30078
  stamp: spec.stamp,
@@ -29924,7 +30156,7 @@ function createSqliteReplicaStorage(options) {
29924
30156
  const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(idDir, "identity.json")));
29925
30157
  if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${idDir}.`);
29926
30158
  if (await gate.principalHashOf(salt, identity.principal) !== principalHash) continue;
29927
- const identityKeyHash = await gate.identityHashOf(salt, (0, import_sdk_services5.replicationIdentityKey)(identity));
30159
+ const identityKeyHash = await gate.identityHashOf(salt, (0, import_sdk_services7.replicationIdentityKey)(identity));
29928
30160
  const recordedAt = Date.now();
29929
30161
  if (await pathExists((0, import_node_path.join)(idDir, "pending.json")) || await pathExists((0, import_node_path.join)(idDir, "device.jwk"))) {
29930
30162
  intents.push({ id: `pending:${identityKeyHash}`, kind: "pending", identityKeyHash, principalHash, recordedAt });
@@ -30602,7 +30834,7 @@ function createSqliteReplicaStorage(options) {
30602
30834
  for (const dir of await identityDirs()) {
30603
30835
  const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(dir, "identity.json")));
30604
30836
  if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${dir}.`);
30605
- const hash3 = await gate.identityHashOf(state.salt, (0, import_sdk_services5.replicationIdentityKey)(identity));
30837
+ const hash3 = await gate.identityHashOf(state.salt, (0, import_sdk_services7.replicationIdentityKey)(identity));
30606
30838
  if (intent.kind === "pending" ? hash3 === intent.identityKeyHash : intent.partitionId.startsWith(`${hash3}:`)) {
30607
30839
  idDir = dir;
30608
30840
  break;
@@ -30748,7 +30980,7 @@ function createSqliteReplicaStorage(options) {
30748
30980
  const gate = await fileGate();
30749
30981
  const snapshot = await gate.readGateSnapshot(profileDir);
30750
30982
  if (snapshot === void 0) throw gated("gate_unavailable");
30751
- const identityKeyHash = await gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(spec.identity));
30983
+ const identityKeyHash = await gate.identityHashOf(snapshot.salt, (0, import_sdk_services7.replicationIdentityKey)(spec.identity));
30752
30984
  const resolvePolicy = async (at, recordFirst) => {
30753
30985
  const policy = spec.grantPolicy;
30754
30986
  if (!Number.isSafeInteger(policy.grantTtlMs) || policy.grantTtlMs <= 0 || policy.grantTtlMs > import_contractConstants.MAX_GRANT_TTL_MS) {
@@ -30770,7 +31002,7 @@ function createSqliteReplicaStorage(options) {
30770
31002
  let partitionId;
30771
31003
  const storeGuard = (section) => guarded(spec.identity.principal, spec.stamp, spec.identity, partitionId, section);
30772
31004
  return guarded(spec.identity.principal, spec.stamp, spec.identity, void 0, async () => {
30773
- if ((0, import_sdk_services5.requiresSecretsOptIn)(spec.space, spec.prefix) && spec.allowSecrets !== true) {
31005
+ if ((0, import_sdk_services7.requiresSecretsOptIn)(spec.space, spec.prefix) && spec.allowSecrets !== true) {
30774
31006
  throw new replica.ReplicaError(
30775
31007
  replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
30776
31008
  `Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
@@ -31016,7 +31248,7 @@ function createSqliteReplicaStorage(options) {
31016
31248
  const gate = await fileGate();
31017
31249
  const currentGate = await gate.readGateSnapshot(profileDir);
31018
31250
  if (currentGate !== void 0 && currentGate.principalHash !== null && currentGate.principalHash === await gate.principalHashOf(currentGate.salt, target.identity.principal)) {
31019
- const identityKeyHash = await gate.identityHashOf(currentGate.salt, (0, import_sdk_services5.replicationIdentityKey)(target.identity));
31251
+ const identityKeyHash = await gate.identityHashOf(currentGate.salt, (0, import_sdk_services7.replicationIdentityKey)(target.identity));
31020
31252
  await retainGrantInstalls(gate, currentGate, identityKeyHash, store);
31021
31253
  }
31022
31254
  if (state.revoked === null) {
@@ -31112,8 +31344,11 @@ function sqliteReplicaStorage(options) {
31112
31344
  MemorySessionStorage,
31113
31345
  NETWORK_NAME_PATTERN,
31114
31346
  NetworkIdError,
31347
+ NodeIdentityMismatchError,
31348
+ NodeNotRespondingError,
31115
31349
  NodeUserAuthorization,
31116
31350
  NodeWasmBindings,
31351
+ PartialNodeWriteError,
31117
31352
  PermissionNotInManifestError,
31118
31353
  PrefixedKVService,
31119
31354
  PrivateKeySigner,