@tinycloud/node-sdk 3.12.0-beta.2 → 3.12.0-beta.4

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 +499 -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 +525 -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 +516 -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 +526 -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,8 @@ 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 stagedHost = (0, import_sdk_core10.exactHttpOrigin)(restoreHints[0] ?? this.config.host, "restored host hint");
24216
24405
  let restoredNodeFeatures;
24217
24406
  let replicaProtocolMismatch = false;
24218
24407
  if (this.config.replication?.enabled) {
@@ -24329,7 +24518,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24329
24518
  this.sessionManager = stagedManager;
24330
24519
  this.sessionKeyId = stagedKeyId;
24331
24520
  this.sessionKeyJwk = stagedJwk;
24332
- if (resolvedHost) this.config.host = resolvedHost;
24521
+ this.config.host = stagedHost;
24333
24522
  if (spaceOwnerKey(serviceSession.spaceId) !== this.currentStorageOwner()) {
24334
24523
  this.storageFull.clear();
24335
24524
  this._requestLog?.clear();
@@ -24396,6 +24585,10 @@ var _TinyCloudNode = class _TinyCloudNode {
24396
24585
  if (attempt === this.latestSessionAttempt) await this.endSession();
24397
24586
  this.assertSessionEpoch(epoch, "Session restore");
24398
24587
  }
24588
+ if (this.config.replication?.enabled) {
24589
+ if (stagedAddress !== void 0) this.refreshRegistryOwner((0, import_sdk_core10.pkhDid)(stagedAddress, stagedChainId));
24590
+ this.refreshRegistryForCapabilities(this.getVerifiedSessionCapabilities());
24591
+ }
24399
24592
  }
24400
24593
  stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
24401
24594
  if (!session?.siwe) return { cache: void 0, grants: [] };
@@ -24444,7 +24637,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24444
24637
  };
24445
24638
  }
24446
24639
  stageRestoredServiceGraph(input) {
24447
- const graph = this.createServiceGraphLifetime();
24640
+ const graph = this.createServiceGraphLifetime(input.serviceSession);
24448
24641
  const core = this.auth ? new import_sdk_core10.TinyCloud(this.auth, {
24449
24642
  invokeAny: graph.invokeAny,
24450
24643
  telemetry: this.config.telemetry,
@@ -24669,56 +24862,6 @@ var _TinyCloudNode = class _TinyCloudNode {
24669
24862
  spaceService.updateConfig({ sharingService });
24670
24863
  return { core, graph, serviceContext, kv, sql, duckdb, hooks, vault, encryption, capabilityRegistry, keyProvider, sharingService, delegationManager, spaceService };
24671
24864
  }
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
24865
  /**
24723
24866
  * Resolve the currently-active TinyCloudSession, preferring the auth
24724
24867
  * layer's value (wallet mode) and falling back to the node-level
@@ -25193,10 +25336,10 @@ var _TinyCloudNode = class _TinyCloudNode {
25193
25336
  * second time. Only a value recorded for this exact host qualifies; older
25194
25337
  * nodes that omit `nodeId` fall through to the fetch.
25195
25338
  */
25196
- async fetchNodeId() {
25197
- const cached = this.auth?.nodeIdForHost(this.config.host);
25339
+ async fetchNodeId(host = this.config.host) {
25340
+ const cached = this.auth?.nodeIdForHost(host);
25198
25341
  if (cached) return cached;
25199
- const response = await fetch(`${this.config.host}/info`);
25342
+ const response = await fetch(`${host}/info`);
25200
25343
  if (!response.ok) {
25201
25344
  throw await (0, import_sdk_core10.httpResponseError)(response, "Failed to fetch node info");
25202
25345
  }
@@ -25244,10 +25387,24 @@ var _TinyCloudNode = class _TinyCloudNode {
25244
25387
  )
25245
25388
  };
25246
25389
  }
25247
- async fetchEncryptionNetworkAt(host, networkId, fetchFn) {
25248
- const response = await fetchFn(
25249
- `${host}/encryption/networks/${encodeURIComponent(networkId)}`
25250
- );
25390
+ async encryptionDestination(networkId) {
25391
+ const destination = (await this.routingDestinations(networkId, "encryption"))[0];
25392
+ if (!destination) throw new Error("No encryption destination is registered");
25393
+ await (0, import_sdk_services5.verifyResourceDestination)(destination, globalThis.fetch.bind(globalThis));
25394
+ return { ...destination, nodeDid: destination.nodeDid ?? await this.fetchNodeId(destination.url) };
25395
+ }
25396
+ async fetchEncryptionNetworkAt(host, networkId, fetchFn, callerSignal) {
25397
+ let response;
25398
+ const timeout = AbortSignal.timeout(5e3);
25399
+ try {
25400
+ response = await fetchFn(`${host}/encryption/networks/${encodeURIComponent(networkId)}`, {
25401
+ signal: callerSignal ? AbortSignal.any([callerSignal, timeout]) : timeout,
25402
+ redirect: "error"
25403
+ });
25404
+ } catch (error) {
25405
+ if ((0, import_sdk_services5.routingFailure)(error) || callerSignal?.aborted || !timeout.aborted && error instanceof Error && error.name === "AbortError") throw error;
25406
+ throw new import_sdk_services5.NodeNotRespondingError([host], error);
25407
+ }
25251
25408
  if (response.status === 404) return null;
25252
25409
  if (!response.ok) {
25253
25410
  throw await (0, import_sdk_core10.httpResponseError)(response, `Failed to fetch encryption network ${networkId}`);
@@ -25257,18 +25414,18 @@ var _TinyCloudNode = class _TinyCloudNode {
25257
25414
  }
25258
25415
  createEncryptionService(binding) {
25259
25416
  const graph = binding?.graph ?? this._serviceGraph;
25260
- const host = binding?.host ?? this.config.host;
25261
25417
  const session = binding?.session ?? this.requireEncryptionSession();
25262
25418
  const did = binding?.did ?? this.did;
25263
25419
  const address = binding?.address ?? this._address;
25264
25420
  const chainId = binding?.chainId ?? this._chainId;
25265
25421
  const crypto3 = this.createEncryptionCrypto();
25266
- const descriptorCache = /* @__PURE__ */ new Map();
25267
25422
  const transport = {
25268
- postDecrypt: async ({ networkId, authorization, canonicalBody, signal }) => {
25423
+ postDecrypt: async ({ targetNode, networkId, authorization, canonicalBody, signal }) => {
25269
25424
  graph.assertActive();
25425
+ const destination = await this.encryptionDestination(networkId);
25426
+ if (destination.nodeDid !== targetNode) throw new import_sdk_services5.NodeIdentityMismatchError(destination.url, destination.nodeDid, targetNode);
25270
25427
  const response = await graph.fetch(
25271
- `${host}/encryption/networks/${encodeURIComponent(networkId)}/decrypt`,
25428
+ `${destination.url}/encryption/networks/${encodeURIComponent(networkId)}/decrypt`,
25272
25429
  {
25273
25430
  method: "POST",
25274
25431
  headers: {
@@ -25281,7 +25438,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25281
25438
  );
25282
25439
  graph.assertActive();
25283
25440
  if (!response.ok) {
25284
- const permissionHint = response.status === 401 || response.status === 403 ? (0, import_sdk_services4.parsePermissionHint)({
25441
+ const permissionHint = response.status === 401 || response.status === 403 ? (0, import_sdk_services6.parsePermissionHint)({
25285
25442
  service: "tinycloud.encryption",
25286
25443
  path: networkId,
25287
25444
  actions: ["tinycloud.encryption/decrypt"]
@@ -25296,6 +25453,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25296
25453
  }
25297
25454
  };
25298
25455
  return new import_sdk_core10.EncryptionService({
25456
+ resolveTargetNode: async (networkId) => (await this.encryptionDestination(networkId)).nodeDid,
25299
25457
  crypto: crypto3,
25300
25458
  signer: {
25301
25459
  signDecryptInvocation: async (input) => {
@@ -25319,18 +25477,8 @@ var _TinyCloudNode = class _TinyCloudNode {
25319
25477
  node: {
25320
25478
  fetchByNetworkId: async (networkId) => {
25321
25479
  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;
25480
+ const destination = await this.encryptionDestination(networkId);
25481
+ const descriptor = await this.fetchEncryptionNetworkAt(destination.url, networkId, graph.fetch, graph.signal);
25334
25482
  graph.assertActive();
25335
25483
  return descriptor;
25336
25484
  }
@@ -25338,13 +25486,15 @@ var _TinyCloudNode = class _TinyCloudNode {
25338
25486
  wellKnown: {
25339
25487
  fetchWellKnown: async (principal, discoveryKey) => {
25340
25488
  graph.assertActive();
25489
+ const destination = await this.encryptionDestination(this.getDefaultEncryptionNetworkId());
25490
+ if (this.auth?.registryRecordPresent) return null;
25341
25491
  if (!address || !didPrincipalMatches2(principal, did)) {
25342
25492
  return null;
25343
25493
  }
25344
25494
  const publicSpaceId = (0, import_sdk_core10.makePublicSpaceId)(address, chainId);
25345
25495
  const encodedKey = discoveryKey.split("/").map(encodeURIComponent).join("/");
25346
25496
  const response = await graph.fetch(
25347
- `${host}/public/${encodeURIComponent(publicSpaceId)}/kv/${encodedKey}`,
25497
+ `${destination.url}/public/${encodeURIComponent(publicSpaceId)}/kv/${encodedKey}`,
25348
25498
  { method: "GET" }
25349
25499
  );
25350
25500
  graph.assertActive();
@@ -25714,7 +25864,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25714
25864
  const signature2 = await this.signer.signMessage(prepared.siwe);
25715
25865
  assertOwnerGraphActive();
25716
25866
  const delegationSession = this.wasmBindings.completeSessionSetup({ ...prepared, signature: signature2 });
25717
- const activation = await (0, import_sdk_core10.activateSessionWithHost)(host, delegationSession.delegationHeader);
25867
+ const activation = await this.activateSignedSession(delegationSession, prepared.siwe, host);
25718
25868
  assertOwnerGraphActive();
25719
25869
  if (!activation.success) {
25720
25870
  throw Object.assign(
@@ -26033,15 +26183,16 @@ var _TinyCloudNode = class _TinyCloudNode {
26033
26183
  async getEncryptionNetwork(nameOrNetworkId = this.getDefaultEncryptionNetworkId()) {
26034
26184
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
26035
26185
  return this.fetchEncryptionNetworkAt(
26036
- this.config.host,
26186
+ (await this.encryptionDestination(networkId)).url,
26037
26187
  networkId,
26038
26188
  globalThis.fetch.bind(globalThis)
26039
26189
  );
26040
26190
  }
26041
26191
  async createEncryptionNetwork(name = DEFAULT_ENCRYPTION_NETWORK_NAME) {
26042
- const targetNode = await this.fetchNodeId();
26043
26192
  const ownerDid = this.did;
26044
26193
  const networkId = this.getDefaultEncryptionNetworkId(name);
26194
+ const destination = await this.encryptionDestination(networkId);
26195
+ const targetNode = destination.nodeDid;
26045
26196
  const body = {
26046
26197
  name,
26047
26198
  ownerDid,
@@ -26064,7 +26215,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26064
26215
  action: NETWORK_CREATE_ACTION2,
26065
26216
  facts
26066
26217
  });
26067
- const response = await fetch(`${this.config.host}/encryption/networks`, {
26218
+ const response = await this._serviceGraph.fetch(`${destination.url}/encryption/networks`, {
26068
26219
  method: "POST",
26069
26220
  headers: {
26070
26221
  Authorization: signed2.authorization,
@@ -26157,7 +26308,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26157
26308
  );
26158
26309
  if (result.status !== "ok") {
26159
26310
  if (result.status === "permission_required") {
26160
- const hint = (0, import_sdk_services4.parsePermissionHint)(result.hint);
26311
+ const hint = (0, import_sdk_services6.parsePermissionHint)(result.hint);
26161
26312
  if (hint === void 0) return { status: "read_failed" };
26162
26313
  return {
26163
26314
  status: "permission_required",
@@ -26404,6 +26555,97 @@ var _TinyCloudNode = class _TinyCloudNode {
26404
26555
  }
26405
26556
  return { recap: exactVerifiedRecap(verified), expiresAt };
26406
26557
  }
26558
+ async activateResourceSession(delegationHeader, resources, hostHint, signal) {
26559
+ const targets = /* @__PURE__ */ new Map();
26560
+ const routedSpaces = [];
26561
+ let unserved;
26562
+ let requestedResource;
26563
+ for (const resource of new Set(resources)) {
26564
+ requestedResource ?? (requestedResource = resource);
26565
+ signal?.throwIfAborted();
26566
+ try {
26567
+ const encryption = resource.startsWith("urn:tinycloud:encryption:");
26568
+ const destinations = await this.routingDestinations(
26569
+ resource,
26570
+ encryption ? "encryption" : "host",
26571
+ [hostHint ?? this.config.host]
26572
+ );
26573
+ if (!encryption && destinations.some(({ nodeDid }) => nodeDid !== void 0)) routedSpaces.push(resource);
26574
+ for (const destination of destinations) {
26575
+ let target = targets.get(destination.url);
26576
+ if (!target) {
26577
+ target = { destination, spaces: [] };
26578
+ targets.set(destination.url, target);
26579
+ }
26580
+ if (!encryption && destination.nodeDid !== void 0) target.spaces.push(resource);
26581
+ }
26582
+ } catch (error) {
26583
+ if (!(error instanceof import_sdk_core10.NodeNotFoundError)) throw error;
26584
+ unserved ?? (unserved = error);
26585
+ }
26586
+ }
26587
+ if (targets.size === 0) throw unserved ?? new import_sdk_services5.ResourceNotRoutedError(requestedResource ?? "session grant");
26588
+ const succeeded = [];
26589
+ const accepted = [];
26590
+ const failed = [];
26591
+ let refusal;
26592
+ for (const { destination: target, spaces } of targets.values()) {
26593
+ signal?.throwIfAborted();
26594
+ try {
26595
+ await (0, import_sdk_services5.verifyResourceDestination)(target, globalThis.fetch.bind(globalThis));
26596
+ const timeout = AbortSignal.timeout(5e3);
26597
+ const result = await (0, import_sdk_core10.activateSessionWithHost)(
26598
+ target.url,
26599
+ delegationHeader,
26600
+ signal ? AbortSignal.any([signal, timeout]) : timeout
26601
+ );
26602
+ this.throwRegistryRefusal(result.status, result.error ?? "");
26603
+ if (result.success) {
26604
+ const skipped = result.skipped?.find((space) => spaces.some((expected) => this.spaceIdsEqual(space, expected)));
26605
+ if (skipped) {
26606
+ failed.push({ url: target.url, status: result.status, cause: new import_sdk_services5.ResourceNotRoutedError(skipped) });
26607
+ continue;
26608
+ }
26609
+ succeeded.push(target.url);
26610
+ accepted.push(result);
26611
+ } else {
26612
+ refusal = result;
26613
+ failed.push({ url: target.url, status: result.status, cause: result });
26614
+ }
26615
+ } catch (error) {
26616
+ if (signal?.aborted) throw error;
26617
+ failed.push({ url: target.url, cause: (0, import_sdk_services5.routingFailure)(error) ? error : new import_sdk_services5.NodeNotRespondingError([target.url], error) });
26618
+ }
26619
+ }
26620
+ if (failed.length > 0) {
26621
+ if (targets.size > 1) throw new import_sdk_services5.PartialNodeWriteError(succeeded, failed);
26622
+ if (refusal) return refusal;
26623
+ throw failed[0].cause;
26624
+ }
26625
+ const activated = new Set(accepted.flatMap((result) => result.activated ?? []));
26626
+ return {
26627
+ ...accepted[0],
26628
+ activated: [...activated],
26629
+ skipped: [...new Set(accepted.flatMap((result) => result.skipped ?? []))].filter((space) => !activated.has(space) && !routedSpaces.some((expected) => this.spaceIdsEqual(space, expected)))
26630
+ };
26631
+ }
26632
+ activateRoutedDelegation(delegation, signal) {
26633
+ const resources = this.operationsFromDelegation(delegation).map(
26634
+ (operation) => operation.resource?.startsWith("urn:tinycloud:encryption:") ? operation.resource.split("/")[0] : operation.spaceId ?? delegation.spaceId
26635
+ );
26636
+ return this.activateResourceSession(delegation.delegationHeader, resources, delegation.host, signal);
26637
+ }
26638
+ activateSignedSession(session, siwe, hostHint, signal) {
26639
+ const resources = this.parseRecapWithCaveats(siwe).map(
26640
+ (entry) => this.isEncryptionNetworkOperation(this.invocationServiceName(entry.service), entry.path, entry.space) ? entry.path.split("/")[0] : entry.space.startsWith("tinycloud:") ? entry.space : session.spaceId
26641
+ );
26642
+ return this.activateResourceSession(
26643
+ session.delegationHeader,
26644
+ resources.length > 0 ? resources : [session.spaceId],
26645
+ hostHint,
26646
+ signal
26647
+ );
26648
+ }
26407
26649
  /**
26408
26650
  * Install a portable runtime permission delegation into this SDK instance so
26409
26651
  * matching service calls and downstream `delegateTo()` calls can use it.
@@ -26423,16 +26665,12 @@ var _TinyCloudNode = class _TinyCloudNode {
26423
26665
  `Runtime delegation targets ${delegation.delegateDID} but this session key is ${session.verificationMethod}.`
26424
26666
  );
26425
26667
  }
26426
- const targetHost = delegation.host ?? this.config.host;
26427
26668
  let activateResult;
26428
26669
  try {
26429
- activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
26430
- targetHost,
26431
- delegation.delegationHeader,
26432
- graph.signal
26433
- );
26670
+ activateResult = await this.activateRoutedDelegation(delegation, graph.signal);
26434
26671
  } catch (cause) {
26435
26672
  graph.assertActive();
26673
+ if ((0, import_sdk_services5.routingFailure)(cause)) throw cause;
26436
26674
  throw Object.assign(
26437
26675
  new Error("The TinyCloud host could not be reached to activate the delegation."),
26438
26676
  { code: "ACTIVATION_TRANSPORT_FAILED", cause }
@@ -26588,11 +26826,15 @@ var _TinyCloudNode = class _TinyCloudNode {
26588
26826
  ...prepared,
26589
26827
  signature: signature2
26590
26828
  });
26591
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
26592
- this.config.host,
26593
- delegatedSession.delegationHeader,
26594
- graph.signal
26595
- ).catch((error) => {
26829
+ const delegation = this.runtimeDelegationFromSession(
26830
+ delegatedSession,
26831
+ delegatedEntries,
26832
+ spaceId,
26833
+ captured,
26834
+ expiresAt,
26835
+ { siwe: prepared.siwe, signature: signature2 }
26836
+ );
26837
+ const activateResult = await this.activateRoutedDelegation(delegation, graph.signal).catch((error) => {
26596
26838
  graph.assertActive();
26597
26839
  throw error;
26598
26840
  });
@@ -26608,14 +26850,6 @@ var _TinyCloudNode = class _TinyCloudNode {
26608
26850
  { cause: activateResult }
26609
26851
  );
26610
26852
  }
26611
- const delegation = this.runtimeDelegationFromSession(
26612
- delegatedSession,
26613
- delegatedEntries,
26614
- spaceId,
26615
- captured,
26616
- expiresAt,
26617
- { siwe: prepared.siwe, signature: signature2 }
26618
- );
26619
26853
  graph.assertActive();
26620
26854
  assertCapturedKey?.();
26621
26855
  this.runtimePermissionGrants.push({
@@ -26760,7 +26994,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26760
26994
  if (!publicSpaceId) {
26761
26995
  throw new Error("Public space not enabled. Set enablePublicSpace: true in config.");
26762
26996
  }
26763
- await this.assertMayCreateSpace();
26997
+ await this.assertMayCreateSpace(publicSpaceId);
26764
26998
  await this.auth.hostPublicSpace(publicSpaceId);
26765
26999
  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
27000
  const abilities = { kv: { "": kvActions } };
@@ -26783,10 +27017,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26783
27017
  ...prepared,
26784
27018
  signature: signature2
26785
27019
  });
26786
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
26787
- this.config.host,
26788
- delegationSession.delegationHeader
26789
- );
27020
+ const activateResult = await this.activateSignedSession(delegationSession, prepared.siwe);
26790
27021
  if (!activateResult.success) {
26791
27022
  throw Object.assign(
26792
27023
  new Error(`Failed to activate public space delegation: ${describeHostFailure2(activateResult)}`),
@@ -27336,10 +27567,7 @@ var _TinyCloudNode = class _TinyCloudNode {
27336
27567
  if (onPrepared) {
27337
27568
  await onPrepared(delegation);
27338
27569
  }
27339
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
27340
- this.config.host,
27341
- delegationHeader
27342
- );
27570
+ const activateResult = await this.activateRoutedDelegation(delegation);
27343
27571
  assertDelegationActivated(activateResult, delegation, requireActivation);
27344
27572
  return delegation;
27345
27573
  }
@@ -27373,10 +27601,7 @@ var _TinyCloudNode = class _TinyCloudNode {
27373
27601
  if (onPrepared) {
27374
27602
  await onPrepared(delegation);
27375
27603
  }
27376
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
27377
- targetHost,
27378
- delegationHeader
27379
- );
27604
+ const activateResult = await this.activateRoutedDelegation(delegation);
27380
27605
  assertDelegationActivated(activateResult, delegation, requireActivation);
27381
27606
  return delegation;
27382
27607
  }
@@ -28028,10 +28253,7 @@ var _TinyCloudNode = class _TinyCloudNode {
28028
28253
  ...prepared,
28029
28254
  signature: signature2
28030
28255
  });
28031
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
28032
- this.config.host,
28033
- delegationSession.delegationHeader
28034
- );
28256
+ const activateResult = await this.activateSignedSession(delegationSession, prepared.siwe);
28035
28257
  if (!activateResult.success) {
28036
28258
  throw Object.assign(
28037
28259
  new Error(`Failed to activate delegation: ${describeHostFailure2(activateResult)}`),
@@ -28076,10 +28298,7 @@ var _TinyCloudNode = class _TinyCloudNode {
28076
28298
  ...publicPrepared,
28077
28299
  signature: publicSignature
28078
28300
  });
28079
- const publicActivateResult = await (0, import_sdk_core10.activateSessionWithHost)(
28080
- this.config.host,
28081
- publicSession.delegationHeader
28082
- );
28301
+ const publicActivateResult = await this.activateSignedSession(publicSession, publicPrepared.siwe);
28083
28302
  if (publicActivateResult.success) {
28084
28303
  result.publicDelegation = {
28085
28304
  cid: publicSession.delegationCid,
@@ -28185,11 +28404,7 @@ var _TinyCloudNode = class _TinyCloudNode {
28185
28404
  ...prepared,
28186
28405
  signature: signature2
28187
28406
  });
28188
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
28189
- targetHost,
28190
- invokerSession.delegationHeader,
28191
- graph.signal
28192
- ).catch((error) => {
28407
+ const activateResult = await this.activateSignedSession(invokerSession, prepared.siwe, targetHost, graph.signal).catch((error) => {
28193
28408
  graph.assertActive();
28194
28409
  throw error;
28195
28410
  });
@@ -28316,10 +28531,7 @@ var _TinyCloudNode = class _TinyCloudNode {
28316
28531
  ...prepared,
28317
28532
  signature: signature2
28318
28533
  });
28319
- const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
28320
- targetHost,
28321
- subDelegationSession.delegationHeader
28322
- );
28534
+ const activateResult = await this.activateSignedSession(subDelegationSession, prepared.siwe, targetHost);
28323
28535
  if (!activateResult.success) {
28324
28536
  throw Object.assign(
28325
28537
  new Error(`Failed to activate sub-delegation: ${describeHostFailure2(activateResult)}`),
@@ -28383,6 +28595,7 @@ registerReplicationLoaders({
28383
28595
 
28384
28596
  // src/index.ts
28385
28597
  var import_sdk_core15 = require("@tinycloud/sdk-core");
28598
+ var import_sdk_services8 = require("@tinycloud/sdk-services");
28386
28599
  var import_sdk_core16 = require("@tinycloud/sdk-core");
28387
28600
 
28388
28601
  // src/storage/FileSessionStorage.ts
@@ -28582,7 +28795,7 @@ var import_node_fs = require("fs");
28582
28795
  var import_promises = require("fs/promises");
28583
28796
  var import_node_path = require("path");
28584
28797
  var import_sdk_core14 = require("@tinycloud/sdk-core");
28585
- var import_sdk_services5 = require("@tinycloud/sdk-services");
28798
+ var import_sdk_services7 = require("@tinycloud/sdk-services");
28586
28799
  var import_contractConstants = require("@tinycloud/sdk-services/kv/replication/contractConstants");
28587
28800
  init_authority();
28588
28801
  var REPLICA_LOGOUT_PENDING_FILE = "replica-logout-pending.json";
@@ -29144,7 +29357,7 @@ function sha256Base32(material) {
29144
29357
  return out.slice(0, 26);
29145
29358
  }
29146
29359
  function partitionDirOf(root, identity) {
29147
- return (0, import_node_path.join)(root, sha256Base32((0, import_sdk_services5.replicationIdentityKey)(identity)));
29360
+ return (0, import_node_path.join)(root, sha256Base32((0, import_sdk_services7.replicationIdentityKey)(identity)));
29148
29361
  }
29149
29362
  function replicaHashOf(prefix, deviceDid) {
29150
29363
  return sha256Base32(`${prefix}
@@ -29218,8 +29431,8 @@ function deviceRecordOf(value) {
29218
29431
  }
29219
29432
  function sameIdentity(a, b) {
29220
29433
  try {
29221
- const left = (0, import_sdk_services5.canonicalReplicationIdentity)(a);
29222
- const right = (0, import_sdk_services5.canonicalReplicationIdentity)(b);
29434
+ const left = (0, import_sdk_services7.canonicalReplicationIdentity)(a);
29435
+ const right = (0, import_sdk_services7.canonicalReplicationIdentity)(b);
29223
29436
  return left.host === right.host && left.space === right.space && left.principal === right.principal;
29224
29437
  } catch {
29225
29438
  return false;
@@ -29235,7 +29448,7 @@ async function ensurePartitionIdentity(dir, identity, dirSync) {
29235
29448
  const file = (0, import_node_path.join)(dir, "identity.json");
29236
29449
  const existing = await readJsonFile(file);
29237
29450
  if (existing === void 0) {
29238
- await writeJsonAtomic(file, { v: 1, ...(0, import_sdk_services5.canonicalReplicationIdentity)(identity) }, dirSync);
29451
+ await writeJsonAtomic(file, { v: 1, ...(0, import_sdk_services7.canonicalReplicationIdentity)(identity) }, dirSync);
29239
29452
  return;
29240
29453
  }
29241
29454
  const record = identityRecordOf(existing);
@@ -29334,7 +29547,7 @@ withGate_fn = async function(section) {
29334
29547
  if (snapshot === void 0) throw gated("gate_unavailable");
29335
29548
  const [principalHash, identityKeyHash] = await Promise.all([
29336
29549
  gate.principalHashOf(snapshot.salt, this.identity.principal),
29337
- gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(this.identity))
29550
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services7.replicationIdentityKey)(this.identity))
29338
29551
  ]);
29339
29552
  const access = {
29340
29553
  stamp: __privateGet(this, _stamp),
@@ -29835,7 +30048,7 @@ function createSqliteReplicaStorage(options) {
29835
30048
  if (snapshot === void 0) throw gated("gate_unavailable");
29836
30049
  const [principalHash, identityKeyHash] = await Promise.all([
29837
30050
  gate.principalHashOf(snapshot.salt, principal),
29838
- identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(identity))
30051
+ identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, (0, import_sdk_services7.replicationIdentityKey)(identity))
29839
30052
  ]);
29840
30053
  return profileSection(async () => {
29841
30054
  assertLogoutClear(options.logoutPendingFile);
@@ -29858,7 +30071,7 @@ function createSqliteReplicaStorage(options) {
29858
30071
  if (snapshot === void 0) throw gated("gate_unavailable");
29859
30072
  const [principalHash, identityKeyHash] = await Promise.all([
29860
30073
  gate.principalHashOf(snapshot.salt, spec.identity.principal),
29861
- gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(spec.identity))
30074
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services7.replicationIdentityKey)(spec.identity))
29862
30075
  ]);
29863
30076
  const access = {
29864
30077
  stamp: spec.stamp,
@@ -29942,7 +30155,7 @@ function createSqliteReplicaStorage(options) {
29942
30155
  const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(idDir, "identity.json")));
29943
30156
  if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${idDir}.`);
29944
30157
  if (await gate.principalHashOf(salt, identity.principal) !== principalHash) continue;
29945
- const identityKeyHash = await gate.identityHashOf(salt, (0, import_sdk_services5.replicationIdentityKey)(identity));
30158
+ const identityKeyHash = await gate.identityHashOf(salt, (0, import_sdk_services7.replicationIdentityKey)(identity));
29946
30159
  const recordedAt = Date.now();
29947
30160
  if (await pathExists((0, import_node_path.join)(idDir, "pending.json")) || await pathExists((0, import_node_path.join)(idDir, "device.jwk"))) {
29948
30161
  intents.push({ id: `pending:${identityKeyHash}`, kind: "pending", identityKeyHash, principalHash, recordedAt });
@@ -30620,7 +30833,7 @@ function createSqliteReplicaStorage(options) {
30620
30833
  for (const dir of await identityDirs()) {
30621
30834
  const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(dir, "identity.json")));
30622
30835
  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));
30836
+ const hash3 = await gate.identityHashOf(state.salt, (0, import_sdk_services7.replicationIdentityKey)(identity));
30624
30837
  if (intent.kind === "pending" ? hash3 === intent.identityKeyHash : intent.partitionId.startsWith(`${hash3}:`)) {
30625
30838
  idDir = dir;
30626
30839
  break;
@@ -30766,7 +30979,7 @@ function createSqliteReplicaStorage(options) {
30766
30979
  const gate = await fileGate();
30767
30980
  const snapshot = await gate.readGateSnapshot(profileDir);
30768
30981
  if (snapshot === void 0) throw gated("gate_unavailable");
30769
- const identityKeyHash = await gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(spec.identity));
30982
+ const identityKeyHash = await gate.identityHashOf(snapshot.salt, (0, import_sdk_services7.replicationIdentityKey)(spec.identity));
30770
30983
  const resolvePolicy = async (at, recordFirst) => {
30771
30984
  const policy = spec.grantPolicy;
30772
30985
  if (!Number.isSafeInteger(policy.grantTtlMs) || policy.grantTtlMs <= 0 || policy.grantTtlMs > import_contractConstants.MAX_GRANT_TTL_MS) {
@@ -30788,7 +31001,7 @@ function createSqliteReplicaStorage(options) {
30788
31001
  let partitionId;
30789
31002
  const storeGuard = (section) => guarded(spec.identity.principal, spec.stamp, spec.identity, partitionId, section);
30790
31003
  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) {
31004
+ if ((0, import_sdk_services7.requiresSecretsOptIn)(spec.space, spec.prefix) && spec.allowSecrets !== true) {
30792
31005
  throw new replica.ReplicaError(
30793
31006
  replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
30794
31007
  `Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
@@ -31034,7 +31247,7 @@ function createSqliteReplicaStorage(options) {
31034
31247
  const gate = await fileGate();
31035
31248
  const currentGate = await gate.readGateSnapshot(profileDir);
31036
31249
  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));
31250
+ const identityKeyHash = await gate.identityHashOf(currentGate.salt, (0, import_sdk_services7.replicationIdentityKey)(target.identity));
31038
31251
  await retainGrantInstalls(gate, currentGate, identityKeyHash, store);
31039
31252
  }
31040
31253
  if (state.revoked === null) {
@@ -31130,8 +31343,11 @@ function sqliteReplicaStorage(options) {
31130
31343
  MemorySessionStorage,
31131
31344
  NETWORK_NAME_PATTERN,
31132
31345
  NetworkIdError,
31346
+ NodeIdentityMismatchError,
31347
+ NodeNotRespondingError,
31133
31348
  NodeUserAuthorization,
31134
31349
  NodeWasmBindings,
31350
+ PartialNodeWriteError,
31135
31351
  PermissionNotInManifestError,
31136
31352
  PrefixedKVService,
31137
31353
  PrivateKeySigner,