@tinycloud/node-sdk 3.12.0-beta.2 → 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 (32) hide show
  1. package/README.md +39 -0
  2. package/dist/{NodeUserAuthorization-B05Wjjpb.d.cts → NodeUserAuthorization-B_TFUlua.d.cts} +18 -52
  3. package/dist/{NodeUserAuthorization-B05Wjjpb.d.ts → NodeUserAuthorization-B_TFUlua.d.ts} +18 -52
  4. package/dist/{TinyCloudNode-CEFGtSWn.d.ts → TinyCloudNode-Brcu8QoX.d.cts} +31 -59
  5. package/dist/{TinyCloudNode-6_IumWu0.d.cts → TinyCloudNode-DBJUnsMN.d.ts} +31 -59
  6. package/dist/{authority-DXpnuId1.d.ts → authority-BFc5lesl.d.ts} +1 -1
  7. package/dist/{authority-DODDXxoP.d.cts → authority-DQ6R1HEG.d.cts} +1 -1
  8. package/dist/core.cjs +500 -283
  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 +526 -287
  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 +517 -300
  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 +527 -288
  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.d.cts +1 -1
  31. package/dist/space-hosting.d.ts +1 -1
  32. 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");
@@ -19782,6 +19783,31 @@ var NodeUserAuthorization = class {
19782
19783
  checkCurrent();
19783
19784
  const host = context?.host ?? this.primaryTinyCloudHost;
19784
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
+ }
19785
19811
  const peerId = await (0, import_sdk_core5.fetchPeerId)(host, spaceId, signal);
19786
19812
  checkCurrent();
19787
19813
  const siwe = this.wasm.generateHostSIWEMessage(
@@ -19852,13 +19878,45 @@ var NodeUserAuthorization = class {
19852
19878
  if (!this._tinyCloudSession) {
19853
19879
  throw new Error("Must be signed in to ensure space exists");
19854
19880
  }
19855
- await this.ensureTinyCloudHosts();
19856
- const host = this.primaryTinyCloudHost;
19857
19881
  const primarySpaceId = this._tinyCloudSession.spaceId;
19858
- const result = await (0, import_sdk_core5.activateSessionWithHost)(
19859
- host,
19860
- this._tinyCloudSession.delegationHeader
19861
- );
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");
19862
19920
  this.recordActivationSkippedSpaces(result, primarySpaceId);
19863
19921
  if (!result.success && result.status !== 404) {
19864
19922
  throw Object.assign(
@@ -19906,7 +19964,7 @@ var NodeUserAuthorization = class {
19906
19964
  /** {@link assertMayCreateSpace}, reporting a refusal to the creation handler before rethrowing it. */
19907
19965
  async guardSpaceCreation(handler, context) {
19908
19966
  try {
19909
- await this.assertMayCreateSpace();
19967
+ await this.assertMayCreateSpace(context.spaceId, context.host);
19910
19968
  } catch (error) {
19911
19969
  const failure = error instanceof Error ? error : new Error(String(error));
19912
19970
  handler.onSpaceCreationFailed?.(context, failure);
@@ -21813,6 +21871,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21813
21871
  * ```
21814
21872
  */
21815
21873
  constructor(config = {}) {
21874
+ this.registryRefreshOwners = /* @__PURE__ */ new Set();
21816
21875
  this.signer = null;
21817
21876
  this.auth = null;
21818
21877
  this.consumedOpenKeyPendings = /* @__PURE__ */ new WeakSet();
@@ -21976,8 +22035,10 @@ var _TinyCloudNode = class _TinyCloudNode {
21976
22035
  this.config = {
21977
22036
  ...config,
21978
22037
  host: config.host ?? DEFAULT_HOST,
22038
+ registryPolicy: "fail-closed",
21979
22039
  // One store for every authorization this client creates.
21980
- 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)()
21981
22042
  };
21982
22043
  if (this.config.replication?.enabled) {
21983
22044
  const runtime = getNodeReplicationLoaders().runtime().then(({ ReplicationRuntime: ReplicationRuntime2 }) => {
@@ -22281,7 +22342,11 @@ var _TinyCloudNode = class _TinyCloudNode {
22281
22342
  // A session saved but never registered on this node is refused: it registers once.
22282
22343
  activate: (signal) => {
22283
22344
  assertCurrent();
22284
- 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);
22285
22350
  }
22286
22351
  });
22287
22352
  assertCurrent();
@@ -22417,14 +22482,38 @@ var _TinyCloudNode = class _TinyCloudNode {
22417
22482
  get requests() {
22418
22483
  return this._requestLog === void 0 ? void 0 : (0, import_requestLog2.readOnlyRequestLog)(this._requestLog);
22419
22484
  }
22420
- 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
+ );
22421
22513
  return new ServiceGraphLifetime(
22422
22514
  this.invokeWithRuntimePermissions,
22423
22515
  this.invokeAnyWithRuntimePermissions,
22424
- // Resolve fetch at request time. This keeps the graph-owned lifetime
22425
- // signal while preserving the SDK's existing injectable global fetch
22426
- // behavior (including adapters that install it after construction).
22427
- (url, init2) => globalThis.fetch(url, init2),
22516
+ routedFetch,
22428
22517
  (context) => {
22429
22518
  this.storageFull.observe(context);
22430
22519
  if (this._requestLog) (0, import_requestLog2.observeReplicaReads)(context, this._requestLog);
@@ -22432,6 +22521,75 @@ var _TinyCloudNode = class _TinyCloudNode {
22432
22521
  this._requestLog
22433
22522
  );
22434
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
+ }
22435
22593
  syncResolvedHostFromAuth() {
22436
22594
  const host = this.auth?.hosts[0];
22437
22595
  if (host) {
@@ -22545,6 +22703,29 @@ var _TinyCloudNode = class _TinyCloudNode {
22545
22703
  const session = this.currentTinyCloudSession();
22546
22704
  return session?.siwe ? this.projectSignedRecapCapabilities(session.siwe) : [];
22547
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
+ }
22548
22729
  /** Normalize signed capabilities without collapsing nested encryption into raw authority. */
22549
22730
  projectSignedRecapCapabilities(siwe) {
22550
22731
  const projected = this.parseRecapWithCaveats(siwe).map((entry) => {
@@ -22976,6 +23157,8 @@ var _TinyCloudNode = class _TinyCloudNode {
22976
23157
  const primarySession = this.currentTinyCloudSession();
22977
23158
  if (primarySession) {
22978
23159
  this.registerPrimarySessionGrant(primarySession);
23160
+ this.refreshRegistryOwner((0, import_sdk_core10.pkhDid)(primarySession.address, primarySession.chainId));
23161
+ this.refreshRegistryForCapabilities(this.getVerifiedSessionCapabilities());
22979
23162
  if (this.auth?.spaceHosting === "unknown") {
22980
23163
  await this.checkSpaceHosting();
22981
23164
  this.assertSessionEpoch(epoch, "Sign-in");
@@ -23198,8 +23381,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23198
23381
  const primarySession = auth.tinyCloudSession;
23199
23382
  const defaultSpaceId = this.ownedSpaceId("default");
23200
23383
  const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === import_sdk_core10.BOOTSTRAP_SESSION_REQUESTS.default;
23201
- if (steps.some((step) => step.kind === "host")) {
23202
- await this.assertMayCreateSpace();
23384
+ for (const step of steps) {
23385
+ if (step.kind === "host") await this.assertMayCreateSpace(step.spaceId);
23203
23386
  }
23204
23387
  for (const step of steps) {
23205
23388
  if (step.kind !== "session") continue;
@@ -23248,10 +23431,20 @@ var _TinyCloudNode = class _TinyCloudNode {
23248
23431
  if (!session) {
23249
23432
  throw new Error(`Missing bootstrap session for ${step.space}`);
23250
23433
  }
23251
- const activated = await (0, import_sdk_core10.activateSessionWithHost)(host, session.delegationHeader, graph.signal).catch((error) => {
23252
- graph.assertActive();
23253
- throw error;
23254
- });
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");
23255
23448
  graph.assertActive();
23256
23449
  if (!activated.success || activated.skipped?.includes(step.spaceId)) {
23257
23450
  throw Object.assign(
@@ -23581,7 +23774,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23581
23774
  if (!this.currentSessionCoversAccountRegistry()) {
23582
23775
  if (!_TinyCloudNode.accountRegistrySkipLogged) {
23583
23776
  _TinyCloudNode.accountRegistrySkipLogged = true;
23584
- import_sdk_services4.tinyCloudDebugLogger.log("registry.skip");
23777
+ import_sdk_services6.tinyCloudDebugLogger.log("registry.skip");
23585
23778
  }
23586
23779
  return;
23587
23780
  }
@@ -23834,6 +24027,13 @@ var _TinyCloudNode = class _TinyCloudNode {
23834
24027
  if (!session) {
23835
24028
  throw new Error("Owned space hosting requires an active session");
23836
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
+ }
23837
24037
  if (this.confirmedHostedSpaceIds.has(spaceId) || this.hostedOwnedSpaceIds.has(spaceId)) {
23838
24038
  return;
23839
24039
  }
@@ -23852,7 +24052,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23852
24052
  { cause: activation }
23853
24053
  );
23854
24054
  }
23855
- await this.assertMayCreateSpace();
24055
+ await this.assertMayCreateSpace(spaceId);
23856
24056
  if (signal?.aborted) throw signal.reason;
23857
24057
  const created = await this.hostOwnedSpaceOnce(spaceId, signal);
23858
24058
  if (!created.success) {
@@ -23913,17 +24113,19 @@ var _TinyCloudNode = class _TinyCloudNode {
23913
24113
  { cause: hosted }
23914
24114
  );
23915
24115
  }
23916
- const activation = await (0, import_sdk_core10.activateSessionWithHost)(
23917
- host,
23918
- session.delegationHeader
23919
- );
23920
- if (!activation.success || activation.skipped?.includes(spaceId)) {
23921
- throw Object.assign(
23922
- new Error(
23923
- `Failed to activate session for owned space ${spaceId}: ${activation.success ? "space was skipped" : describeHostFailure2(activation)}`
23924
- ),
23925
- { cause: activation }
24116
+ if (!this.auth.registryRecordPresent) {
24117
+ const activation = await (0, import_sdk_core10.activateSessionWithHost)(
24118
+ host,
24119
+ session.delegationHeader
23926
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
+ }
23927
24129
  }
23928
24130
  if (this.auth?.tinyCloudSession === session) this.hostedOwnedSpaceIds.add(spaceId);
23929
24131
  try {
@@ -23967,11 +24169,9 @@ var _TinyCloudNode = class _TinyCloudNode {
23967
24169
  * session's recap doesn't cover it and the registry has no record; the host
23968
24170
  * SIWE is idempotent server-side. Must be called after {@link signIn}.
23969
24171
  *
23970
- * Under `registryPolicy: "fail-closed"`, hosting is refused with
23971
- * `NodeRegistryEntryExistsError` when the owner's registry record does not
23972
- * list the host, or `RegistryUnavailableError` when the registry gives no
23973
- * verdict. {@link hostOwnedSpace} itself is an explicit action and is not
23974
- * 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.
23975
24175
  *
23976
24176
  * @param name - The owned space name (e.g. `"secrets"`).
23977
24177
  * @returns The hosted owned-space URI.
@@ -23981,27 +24181,20 @@ var _TinyCloudNode = class _TinyCloudNode {
23981
24181
  throw new Error("Not signed in. Call signIn() first.");
23982
24182
  }
23983
24183
  const spaceId = this.ownedSpaceId(name);
24184
+ await this.assertMayCreateSpace(spaceId);
23984
24185
  if (this.hostedOwnedSpaceIds.has(spaceId) || await this.isOwnedSpaceRegistered(spaceId)) {
23985
24186
  return spaceId;
23986
24187
  }
23987
- await this.assertMayCreateSpace();
23988
24188
  return this.hostOwnedSpace(name);
23989
24189
  }
23990
- /**
23991
- * Space-creation guard (TC-1443) for the automatic hosting paths: sign-in's
23992
- * owned spaces, account bootstrap, {@link ensureOwnedSpaceHosted} and
23993
- * {@link ensurePublicSpace}. Only `registryPolicy: "fail-closed"` consults
23994
- * the registry ({@link NodeUserAuthorization.assertMayCreateSpace}); the
23995
- * default policy performs no lookup.
23996
- */
23997
- async assertMayCreateSpace() {
23998
- if (this.config.registryPolicy !== "fail-closed") return;
23999
- 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");
24000
24193
  }
24001
24194
  /** Registry-policy settings shared by every authorization this client creates. */
24002
24195
  registryPolicyConfig() {
24003
- const { registryPolicy, newUserHost, registryChainId, registrySequenceStore, onNodeNotFound } = this.config;
24004
- 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 };
24005
24198
  }
24006
24199
  /**
24007
24200
  * Submit the host delegation for an owned space, sharing one in-flight
@@ -24207,12 +24400,9 @@ var _TinyCloudNode = class _TinyCloudNode {
24207
24400
  }
24208
24401
  stagedSessionExpiry = signedExpiry ?? persistedPolicyExpiry ?? persistedExpiry(void 0);
24209
24402
  }
24210
- const resolvedHost = await this.resolveRestoredHost(
24211
- sessionData.tinycloudHosts,
24212
- restoredAddress,
24213
- sessionData.chainId
24214
- );
24215
- 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");
24216
24406
  let restoredNodeFeatures;
24217
24407
  let replicaProtocolMismatch = false;
24218
24408
  if (this.config.replication?.enabled) {
@@ -24329,7 +24519,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24329
24519
  this.sessionManager = stagedManager;
24330
24520
  this.sessionKeyId = stagedKeyId;
24331
24521
  this.sessionKeyJwk = stagedJwk;
24332
- if (resolvedHost) this.config.host = resolvedHost;
24522
+ this.config.host = stagedHost;
24333
24523
  if (spaceOwnerKey(serviceSession.spaceId) !== this.currentStorageOwner()) {
24334
24524
  this.storageFull.clear();
24335
24525
  this._requestLog?.clear();
@@ -24396,6 +24586,10 @@ var _TinyCloudNode = class _TinyCloudNode {
24396
24586
  if (attempt === this.latestSessionAttempt) await this.endSession();
24397
24587
  this.assertSessionEpoch(epoch, "Session restore");
24398
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
+ }
24399
24593
  }
24400
24594
  stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
24401
24595
  if (!session?.siwe) return { cache: void 0, grants: [] };
@@ -24444,7 +24638,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24444
24638
  };
24445
24639
  }
24446
24640
  stageRestoredServiceGraph(input) {
24447
- const graph = this.createServiceGraphLifetime();
24641
+ const graph = this.createServiceGraphLifetime(input.serviceSession);
24448
24642
  const core = this.auth ? new import_sdk_core10.TinyCloud(this.auth, {
24449
24643
  invokeAny: graph.invokeAny,
24450
24644
  telemetry: this.config.telemetry,
@@ -24669,56 +24863,6 @@ var _TinyCloudNode = class _TinyCloudNode {
24669
24863
  spaceService.updateConfig({ sharingService });
24670
24864
  return { core, graph, serviceContext, kv, sql, duckdb, hooks, vault, encryption, capabilityRegistry, keyProvider, sharingService, delegationManager, spaceService };
24671
24865
  }
24672
- /**
24673
- * Resolve the host a restored session should target.
24674
- *
24675
- * Mirrors fresh sign-in host resolution but for the restore path:
24676
- * an explicit/pinned host always wins, then the hosts the session was
24677
- * persisted with, then a lazy registry/fallback resolution for sessions
24678
- * that predate the persisted `tinycloudHosts` field. Returns `undefined`
24679
- * only when there's nothing to resolve from (no explicit host, no
24680
- * persisted hosts, and no address/chainId) — in which case the existing
24681
- * `config.host` (default) is left in place.
24682
- *
24683
- * Resolution failures are surfaced, not swallowed: a genuinely broken
24684
- * registry lookup throws rather than silently falling back to a wrong host.
24685
- */
24686
- async resolveRestoredHost(persistedHosts, address, chainId) {
24687
- if (this.explicitHost) {
24688
- return this.explicitHost;
24689
- }
24690
- if (persistedHosts && persistedHosts.length > 0) {
24691
- return persistedHosts[0];
24692
- }
24693
- if (address === void 0 || chainId === void 0) {
24694
- return void 0;
24695
- }
24696
- if (this.config.registryPolicy === "fail-closed") {
24697
- const resolved2 = await (0, import_sdk_core10.resolveFailClosedTinyCloudHosts)(address, {
24698
- registryUrl: this.config.tinycloudRegistryUrl,
24699
- registryChainId: this.config.registryChainId,
24700
- newUserHost: this.config.newUserHost,
24701
- sequenceStore: this.config.registrySequenceStore,
24702
- onNodeNotFound: this.config.onNodeNotFound,
24703
- autoDiscoverLocalNode: this.config.autoDiscoverLocalNode,
24704
- localNodeUrl: this.config.localNodeUrl,
24705
- localLinkName: this.config.localLinkName,
24706
- expectedNodeDid: this.config.expectedNodeDid,
24707
- localNodeIdentityStore: this.config.localNodeIdentityStore
24708
- });
24709
- return resolved2.hosts[0];
24710
- }
24711
- const resolved = await (0, import_sdk_core10.resolveTinyCloudHosts)((0, import_sdk_core10.pkhDid)(address, chainId), {
24712
- registryUrl: this.config.tinycloudRegistryUrl,
24713
- fallbackHosts: this.config.tinycloudFallbackHosts,
24714
- autoDiscoverLocalNode: this.config.autoDiscoverLocalNode,
24715
- localNodeUrl: this.config.localNodeUrl,
24716
- localLinkName: this.config.localLinkName,
24717
- expectedNodeDid: this.config.expectedNodeDid,
24718
- localNodeIdentityStore: this.config.localNodeIdentityStore
24719
- });
24720
- return resolved.hosts[0];
24721
- }
24722
24866
  /**
24723
24867
  * Resolve the currently-active TinyCloudSession, preferring the auth
24724
24868
  * layer's value (wallet mode) and falling back to the node-level
@@ -25193,10 +25337,10 @@ var _TinyCloudNode = class _TinyCloudNode {
25193
25337
  * second time. Only a value recorded for this exact host qualifies; older
25194
25338
  * nodes that omit `nodeId` fall through to the fetch.
25195
25339
  */
25196
- async fetchNodeId() {
25197
- const cached = this.auth?.nodeIdForHost(this.config.host);
25340
+ async fetchNodeId(host = this.config.host) {
25341
+ const cached = this.auth?.nodeIdForHost(host);
25198
25342
  if (cached) return cached;
25199
- const response = await fetch(`${this.config.host}/info`);
25343
+ const response = await fetch(`${host}/info`);
25200
25344
  if (!response.ok) {
25201
25345
  throw await (0, import_sdk_core10.httpResponseError)(response, "Failed to fetch node info");
25202
25346
  }
@@ -25244,10 +25388,24 @@ var _TinyCloudNode = class _TinyCloudNode {
25244
25388
  )
25245
25389
  };
25246
25390
  }
25247
- async fetchEncryptionNetworkAt(host, networkId, fetchFn) {
25248
- const response = await fetchFn(
25249
- `${host}/encryption/networks/${encodeURIComponent(networkId)}`
25250
- );
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
+ }
25251
25409
  if (response.status === 404) return null;
25252
25410
  if (!response.ok) {
25253
25411
  throw await (0, import_sdk_core10.httpResponseError)(response, `Failed to fetch encryption network ${networkId}`);
@@ -25257,18 +25415,18 @@ var _TinyCloudNode = class _TinyCloudNode {
25257
25415
  }
25258
25416
  createEncryptionService(binding) {
25259
25417
  const graph = binding?.graph ?? this._serviceGraph;
25260
- const host = binding?.host ?? this.config.host;
25261
25418
  const session = binding?.session ?? this.requireEncryptionSession();
25262
25419
  const did = binding?.did ?? this.did;
25263
25420
  const address = binding?.address ?? this._address;
25264
25421
  const chainId = binding?.chainId ?? this._chainId;
25265
25422
  const crypto3 = this.createEncryptionCrypto();
25266
- const descriptorCache = /* @__PURE__ */ new Map();
25267
25423
  const transport = {
25268
- postDecrypt: async ({ networkId, authorization, canonicalBody, signal }) => {
25424
+ postDecrypt: async ({ targetNode, networkId, authorization, canonicalBody, signal }) => {
25269
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);
25270
25428
  const response = await graph.fetch(
25271
- `${host}/encryption/networks/${encodeURIComponent(networkId)}/decrypt`,
25429
+ `${destination.url}/encryption/networks/${encodeURIComponent(networkId)}/decrypt`,
25272
25430
  {
25273
25431
  method: "POST",
25274
25432
  headers: {
@@ -25281,7 +25439,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25281
25439
  );
25282
25440
  graph.assertActive();
25283
25441
  if (!response.ok) {
25284
- 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)({
25285
25443
  service: "tinycloud.encryption",
25286
25444
  path: networkId,
25287
25445
  actions: ["tinycloud.encryption/decrypt"]
@@ -25296,6 +25454,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25296
25454
  }
25297
25455
  };
25298
25456
  return new import_sdk_core10.EncryptionService({
25457
+ resolveTargetNode: async (networkId) => (await this.encryptionDestination(networkId)).nodeDid,
25299
25458
  crypto: crypto3,
25300
25459
  signer: {
25301
25460
  signDecryptInvocation: async (input) => {
@@ -25319,18 +25478,8 @@ var _TinyCloudNode = class _TinyCloudNode {
25319
25478
  node: {
25320
25479
  fetchByNetworkId: async (networkId) => {
25321
25480
  graph.assertActive();
25322
- let pending = descriptorCache.get(networkId);
25323
- if (!pending) {
25324
- pending = this.fetchEncryptionNetworkAt(host, networkId, graph.fetch).then((descriptor2) => {
25325
- if (descriptor2 === null) descriptorCache.delete(networkId);
25326
- return descriptor2;
25327
- }).catch((error) => {
25328
- descriptorCache.delete(networkId);
25329
- throw error;
25330
- });
25331
- descriptorCache.set(networkId, pending);
25332
- }
25333
- const descriptor = await pending;
25481
+ const destination = await this.encryptionDestination(networkId);
25482
+ const descriptor = await this.fetchEncryptionNetworkAt(destination.url, networkId, graph.fetch, graph.signal);
25334
25483
  graph.assertActive();
25335
25484
  return descriptor;
25336
25485
  }
@@ -25338,13 +25487,15 @@ var _TinyCloudNode = class _TinyCloudNode {
25338
25487
  wellKnown: {
25339
25488
  fetchWellKnown: async (principal, discoveryKey) => {
25340
25489
  graph.assertActive();
25490
+ const destination = await this.encryptionDestination(this.getDefaultEncryptionNetworkId());
25491
+ if (this.auth?.registryRecordPresent) return null;
25341
25492
  if (!address || !didPrincipalMatches2(principal, did)) {
25342
25493
  return null;
25343
25494
  }
25344
25495
  const publicSpaceId = (0, import_sdk_core10.makePublicSpaceId)(address, chainId);
25345
25496
  const encodedKey = discoveryKey.split("/").map(encodeURIComponent).join("/");
25346
25497
  const response = await graph.fetch(
25347
- `${host}/public/${encodeURIComponent(publicSpaceId)}/kv/${encodedKey}`,
25498
+ `${destination.url}/public/${encodeURIComponent(publicSpaceId)}/kv/${encodedKey}`,
25348
25499
  { method: "GET" }
25349
25500
  );
25350
25501
  graph.assertActive();
@@ -25714,7 +25865,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25714
25865
  const signature2 = await this.signer.signMessage(prepared.siwe);
25715
25866
  assertOwnerGraphActive();
25716
25867
  const delegationSession = this.wasmBindings.completeSessionSetup({ ...prepared, signature: signature2 });
25717
- const activation = await (0, import_sdk_core10.activateSessionWithHost)(host, delegationSession.delegationHeader);
25868
+ const activation = await this.activateSignedSession(delegationSession, prepared.siwe, host);
25718
25869
  assertOwnerGraphActive();
25719
25870
  if (!activation.success) {
25720
25871
  throw Object.assign(
@@ -26033,15 +26184,16 @@ var _TinyCloudNode = class _TinyCloudNode {
26033
26184
  async getEncryptionNetwork(nameOrNetworkId = this.getDefaultEncryptionNetworkId()) {
26034
26185
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
26035
26186
  return this.fetchEncryptionNetworkAt(
26036
- this.config.host,
26187
+ (await this.encryptionDestination(networkId)).url,
26037
26188
  networkId,
26038
26189
  globalThis.fetch.bind(globalThis)
26039
26190
  );
26040
26191
  }
26041
26192
  async createEncryptionNetwork(name = DEFAULT_ENCRYPTION_NETWORK_NAME) {
26042
- const targetNode = await this.fetchNodeId();
26043
26193
  const ownerDid = this.did;
26044
26194
  const networkId = this.getDefaultEncryptionNetworkId(name);
26195
+ const destination = await this.encryptionDestination(networkId);
26196
+ const targetNode = destination.nodeDid;
26045
26197
  const body = {
26046
26198
  name,
26047
26199
  ownerDid,
@@ -26064,7 +26216,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26064
26216
  action: NETWORK_CREATE_ACTION2,
26065
26217
  facts
26066
26218
  });
26067
- const response = await fetch(`${this.config.host}/encryption/networks`, {
26219
+ const response = await this._serviceGraph.fetch(`${destination.url}/encryption/networks`, {
26068
26220
  method: "POST",
26069
26221
  headers: {
26070
26222
  Authorization: signed2.authorization,
@@ -26157,7 +26309,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26157
26309
  );
26158
26310
  if (result.status !== "ok") {
26159
26311
  if (result.status === "permission_required") {
26160
- const hint = (0, import_sdk_services4.parsePermissionHint)(result.hint);
26312
+ const hint = (0, import_sdk_services6.parsePermissionHint)(result.hint);
26161
26313
  if (hint === void 0) return { status: "read_failed" };
26162
26314
  return {
26163
26315
  status: "permission_required",
@@ -26404,6 +26556,97 @@ var _TinyCloudNode = class _TinyCloudNode {
26404
26556
  }
26405
26557
  return { recap: exactVerifiedRecap(verified), expiresAt };
26406
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
+ }
26407
26650
  /**
26408
26651
  * Install a portable runtime permission delegation into this SDK instance so
26409
26652
  * matching service calls and downstream `delegateTo()` calls can use it.
@@ -26423,16 +26666,12 @@ var _TinyCloudNode = class _TinyCloudNode {
26423
26666
  `Runtime delegation targets ${delegation.delegateDID} but this session key is ${session.verificationMethod}.`
26424
26667
  );
26425
26668
  }
26426
- const targetHost = delegation.host ?? this.config.host;
26427
26669
  let activateResult;
26428
26670
  try {
26429
- activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
26430
- targetHost,
26431
- delegation.delegationHeader,
26432
- graph.signal
26433
- );
26671
+ activateResult = await this.activateRoutedDelegation(delegation, graph.signal);
26434
26672
  } catch (cause) {
26435
26673
  graph.assertActive();
26674
+ if ((0, import_sdk_services5.routingFailure)(cause)) throw cause;
26436
26675
  throw Object.assign(
26437
26676
  new Error("The TinyCloud host could not be reached to activate the delegation."),
26438
26677
  { code: "ACTIVATION_TRANSPORT_FAILED", cause }
@@ -26588,11 +26827,15 @@ var _TinyCloudNode = class _TinyCloudNode {
26588
26827
  ...prepared,
26589
26828
  signature: signature2
26590
26829
  });
26591
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
26592
- this.config.host,
26593
- delegatedSession.delegationHeader,
26594
- graph.signal
26595
- ).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) => {
26596
26839
  graph.assertActive();
26597
26840
  throw error;
26598
26841
  });
@@ -26608,14 +26851,6 @@ var _TinyCloudNode = class _TinyCloudNode {
26608
26851
  { cause: activateResult }
26609
26852
  );
26610
26853
  }
26611
- const delegation = this.runtimeDelegationFromSession(
26612
- delegatedSession,
26613
- delegatedEntries,
26614
- spaceId,
26615
- captured,
26616
- expiresAt,
26617
- { siwe: prepared.siwe, signature: signature2 }
26618
- );
26619
26854
  graph.assertActive();
26620
26855
  assertCapturedKey?.();
26621
26856
  this.runtimePermissionGrants.push({
@@ -26760,7 +26995,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26760
26995
  if (!publicSpaceId) {
26761
26996
  throw new Error("Public space not enabled. Set enablePublicSpace: true in config.");
26762
26997
  }
26763
- await this.assertMayCreateSpace();
26998
+ await this.assertMayCreateSpace(publicSpaceId);
26764
26999
  await this.auth.hostPublicSpace(publicSpaceId);
26765
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];
26766
27001
  const abilities = { kv: { "": kvActions } };
@@ -26783,10 +27018,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26783
27018
  ...prepared,
26784
27019
  signature: signature2
26785
27020
  });
26786
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
26787
- this.config.host,
26788
- delegationSession.delegationHeader
26789
- );
27021
+ const activateResult = await this.activateSignedSession(delegationSession, prepared.siwe);
26790
27022
  if (!activateResult.success) {
26791
27023
  throw Object.assign(
26792
27024
  new Error(`Failed to activate public space delegation: ${describeHostFailure2(activateResult)}`),
@@ -27336,10 +27568,7 @@ var _TinyCloudNode = class _TinyCloudNode {
27336
27568
  if (onPrepared) {
27337
27569
  await onPrepared(delegation);
27338
27570
  }
27339
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
27340
- this.config.host,
27341
- delegationHeader
27342
- );
27571
+ const activateResult = await this.activateRoutedDelegation(delegation);
27343
27572
  assertDelegationActivated(activateResult, delegation, requireActivation);
27344
27573
  return delegation;
27345
27574
  }
@@ -27373,10 +27602,7 @@ var _TinyCloudNode = class _TinyCloudNode {
27373
27602
  if (onPrepared) {
27374
27603
  await onPrepared(delegation);
27375
27604
  }
27376
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
27377
- targetHost,
27378
- delegationHeader
27379
- );
27605
+ const activateResult = await this.activateRoutedDelegation(delegation);
27380
27606
  assertDelegationActivated(activateResult, delegation, requireActivation);
27381
27607
  return delegation;
27382
27608
  }
@@ -28028,10 +28254,7 @@ var _TinyCloudNode = class _TinyCloudNode {
28028
28254
  ...prepared,
28029
28255
  signature: signature2
28030
28256
  });
28031
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
28032
- this.config.host,
28033
- delegationSession.delegationHeader
28034
- );
28257
+ const activateResult = await this.activateSignedSession(delegationSession, prepared.siwe);
28035
28258
  if (!activateResult.success) {
28036
28259
  throw Object.assign(
28037
28260
  new Error(`Failed to activate delegation: ${describeHostFailure2(activateResult)}`),
@@ -28076,10 +28299,7 @@ var _TinyCloudNode = class _TinyCloudNode {
28076
28299
  ...publicPrepared,
28077
28300
  signature: publicSignature
28078
28301
  });
28079
- const publicActivateResult = await (0, import_sdk_core10.activateSessionWithHost)(
28080
- this.config.host,
28081
- publicSession.delegationHeader
28082
- );
28302
+ const publicActivateResult = await this.activateSignedSession(publicSession, publicPrepared.siwe);
28083
28303
  if (publicActivateResult.success) {
28084
28304
  result.publicDelegation = {
28085
28305
  cid: publicSession.delegationCid,
@@ -28185,11 +28405,7 @@ var _TinyCloudNode = class _TinyCloudNode {
28185
28405
  ...prepared,
28186
28406
  signature: signature2
28187
28407
  });
28188
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
28189
- targetHost,
28190
- invokerSession.delegationHeader,
28191
- graph.signal
28192
- ).catch((error) => {
28408
+ const activateResult = await this.activateSignedSession(invokerSession, prepared.siwe, targetHost, graph.signal).catch((error) => {
28193
28409
  graph.assertActive();
28194
28410
  throw error;
28195
28411
  });
@@ -28316,10 +28532,7 @@ var _TinyCloudNode = class _TinyCloudNode {
28316
28532
  ...prepared,
28317
28533
  signature: signature2
28318
28534
  });
28319
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
28320
- targetHost,
28321
- subDelegationSession.delegationHeader
28322
- );
28535
+ const activateResult = await this.activateSignedSession(subDelegationSession, prepared.siwe, targetHost);
28323
28536
  if (!activateResult.success) {
28324
28537
  throw Object.assign(
28325
28538
  new Error(`Failed to activate sub-delegation: ${describeHostFailure2(activateResult)}`),
@@ -28383,6 +28596,7 @@ registerReplicationLoaders({
28383
28596
 
28384
28597
  // src/index.ts
28385
28598
  var import_sdk_core15 = require("@tinycloud/sdk-core");
28599
+ var import_sdk_services8 = require("@tinycloud/sdk-services");
28386
28600
  var import_sdk_core16 = require("@tinycloud/sdk-core");
28387
28601
 
28388
28602
  // src/storage/FileSessionStorage.ts
@@ -28582,7 +28796,7 @@ var import_node_fs = require("fs");
28582
28796
  var import_promises = require("fs/promises");
28583
28797
  var import_node_path = require("path");
28584
28798
  var import_sdk_core14 = require("@tinycloud/sdk-core");
28585
- var import_sdk_services5 = require("@tinycloud/sdk-services");
28799
+ var import_sdk_services7 = require("@tinycloud/sdk-services");
28586
28800
  var import_contractConstants = require("@tinycloud/sdk-services/kv/replication/contractConstants");
28587
28801
  init_authority();
28588
28802
  var REPLICA_LOGOUT_PENDING_FILE = "replica-logout-pending.json";
@@ -29144,7 +29358,7 @@ function sha256Base32(material) {
29144
29358
  return out.slice(0, 26);
29145
29359
  }
29146
29360
  function partitionDirOf(root, identity) {
29147
- 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)));
29148
29362
  }
29149
29363
  function replicaHashOf(prefix, deviceDid) {
29150
29364
  return sha256Base32(`${prefix}
@@ -29218,8 +29432,8 @@ function deviceRecordOf(value) {
29218
29432
  }
29219
29433
  function sameIdentity(a, b) {
29220
29434
  try {
29221
- const left = (0, import_sdk_services5.canonicalReplicationIdentity)(a);
29222
- 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);
29223
29437
  return left.host === right.host && left.space === right.space && left.principal === right.principal;
29224
29438
  } catch {
29225
29439
  return false;
@@ -29235,7 +29449,7 @@ async function ensurePartitionIdentity(dir, identity, dirSync) {
29235
29449
  const file = (0, import_node_path.join)(dir, "identity.json");
29236
29450
  const existing = await readJsonFile(file);
29237
29451
  if (existing === void 0) {
29238
- 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);
29239
29453
  return;
29240
29454
  }
29241
29455
  const record = identityRecordOf(existing);
@@ -29334,7 +29548,7 @@ withGate_fn = async function(section) {
29334
29548
  if (snapshot === void 0) throw gated("gate_unavailable");
29335
29549
  const [principalHash, identityKeyHash] = await Promise.all([
29336
29550
  gate.principalHashOf(snapshot.salt, this.identity.principal),
29337
- gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(this.identity))
29551
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services7.replicationIdentityKey)(this.identity))
29338
29552
  ]);
29339
29553
  const access = {
29340
29554
  stamp: __privateGet(this, _stamp),
@@ -29835,7 +30049,7 @@ function createSqliteReplicaStorage(options) {
29835
30049
  if (snapshot === void 0) throw gated("gate_unavailable");
29836
30050
  const [principalHash, identityKeyHash] = await Promise.all([
29837
30051
  gate.principalHashOf(snapshot.salt, principal),
29838
- 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))
29839
30053
  ]);
29840
30054
  return profileSection(async () => {
29841
30055
  assertLogoutClear(options.logoutPendingFile);
@@ -29858,7 +30072,7 @@ function createSqliteReplicaStorage(options) {
29858
30072
  if (snapshot === void 0) throw gated("gate_unavailable");
29859
30073
  const [principalHash, identityKeyHash] = await Promise.all([
29860
30074
  gate.principalHashOf(snapshot.salt, spec.identity.principal),
29861
- gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(spec.identity))
30075
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services7.replicationIdentityKey)(spec.identity))
29862
30076
  ]);
29863
30077
  const access = {
29864
30078
  stamp: spec.stamp,
@@ -29942,7 +30156,7 @@ function createSqliteReplicaStorage(options) {
29942
30156
  const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(idDir, "identity.json")));
29943
30157
  if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${idDir}.`);
29944
30158
  if (await gate.principalHashOf(salt, identity.principal) !== principalHash) continue;
29945
- 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));
29946
30160
  const recordedAt = Date.now();
29947
30161
  if (await pathExists((0, import_node_path.join)(idDir, "pending.json")) || await pathExists((0, import_node_path.join)(idDir, "device.jwk"))) {
29948
30162
  intents.push({ id: `pending:${identityKeyHash}`, kind: "pending", identityKeyHash, principalHash, recordedAt });
@@ -30620,7 +30834,7 @@ function createSqliteReplicaStorage(options) {
30620
30834
  for (const dir of await identityDirs()) {
30621
30835
  const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(dir, "identity.json")));
30622
30836
  if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${dir}.`);
30623
- 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));
30624
30838
  if (intent.kind === "pending" ? hash3 === intent.identityKeyHash : intent.partitionId.startsWith(`${hash3}:`)) {
30625
30839
  idDir = dir;
30626
30840
  break;
@@ -30766,7 +30980,7 @@ function createSqliteReplicaStorage(options) {
30766
30980
  const gate = await fileGate();
30767
30981
  const snapshot = await gate.readGateSnapshot(profileDir);
30768
30982
  if (snapshot === void 0) throw gated("gate_unavailable");
30769
- 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));
30770
30984
  const resolvePolicy = async (at, recordFirst) => {
30771
30985
  const policy = spec.grantPolicy;
30772
30986
  if (!Number.isSafeInteger(policy.grantTtlMs) || policy.grantTtlMs <= 0 || policy.grantTtlMs > import_contractConstants.MAX_GRANT_TTL_MS) {
@@ -30788,7 +31002,7 @@ function createSqliteReplicaStorage(options) {
30788
31002
  let partitionId;
30789
31003
  const storeGuard = (section) => guarded(spec.identity.principal, spec.stamp, spec.identity, partitionId, section);
30790
31004
  return guarded(spec.identity.principal, spec.stamp, spec.identity, void 0, async () => {
30791
- 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) {
30792
31006
  throw new replica.ReplicaError(
30793
31007
  replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
30794
31008
  `Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
@@ -31034,7 +31248,7 @@ function createSqliteReplicaStorage(options) {
31034
31248
  const gate = await fileGate();
31035
31249
  const currentGate = await gate.readGateSnapshot(profileDir);
31036
31250
  if (currentGate !== void 0 && currentGate.principalHash !== null && currentGate.principalHash === await gate.principalHashOf(currentGate.salt, target.identity.principal)) {
31037
- 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));
31038
31252
  await retainGrantInstalls(gate, currentGate, identityKeyHash, store);
31039
31253
  }
31040
31254
  if (state.revoked === null) {
@@ -31130,8 +31344,11 @@ function sqliteReplicaStorage(options) {
31130
31344
  MemorySessionStorage,
31131
31345
  NETWORK_NAME_PATTERN,
31132
31346
  NetworkIdError,
31347
+ NodeIdentityMismatchError,
31348
+ NodeNotRespondingError,
31133
31349
  NodeUserAuthorization,
31134
31350
  NodeWasmBindings,
31351
+ PartialNodeWriteError,
31135
31352
  PermissionNotInManifestError,
31136
31353
  PrefixedKVService,
31137
31354
  PrivateKeySigner,