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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +39 -0
  2. package/dist/{NodeUserAuthorization-BtoDI9in.d.cts → NodeUserAuthorization-B_TFUlua.d.cts} +19 -53
  3. package/dist/{NodeUserAuthorization-BtoDI9in.d.ts → NodeUserAuthorization-B_TFUlua.d.ts} +19 -53
  4. package/dist/{TinyCloudNode-DLrINr98.d.ts → TinyCloudNode-Brcu8QoX.d.cts} +33 -59
  5. package/dist/{TinyCloudNode-C24728N_.d.cts → TinyCloudNode-DBJUnsMN.d.ts} +33 -59
  6. package/dist/{authority-BQN2Z3st.d.ts → authority-BFc5lesl.d.ts} +1 -1
  7. package/dist/{authority-DDdheOL6.d.cts → authority-DQ6R1HEG.d.cts} +1 -1
  8. package/dist/core.cjs +555 -320
  9. package/dist/core.cjs.map +1 -1
  10. package/dist/core.d.cts +5 -5
  11. package/dist/core.d.ts +5 -5
  12. package/dist/core.js +581 -324
  13. package/dist/core.js.map +1 -1
  14. package/dist/credentials-core.d.cts +1 -1
  15. package/dist/credentials-core.d.ts +1 -1
  16. package/dist/credentials.d.cts +1 -1
  17. package/dist/credentials.d.ts +1 -1
  18. package/dist/{delegation-BQbAuKad.d.cts → delegation-CeOWDnBB.d.cts} +7 -0
  19. package/dist/{delegation-BQbAuKad.d.ts → delegation-CeOWDnBB.d.ts} +7 -0
  20. package/dist/encryption-keys.d.cts +3 -3
  21. package/dist/encryption-keys.d.ts +3 -3
  22. package/dist/index.cjs +572 -337
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +5 -4
  25. package/dist/index.d.ts +5 -4
  26. package/dist/index.js +582 -325
  27. package/dist/index.js.map +1 -1
  28. package/dist/replication/authority.d.cts +4 -4
  29. package/dist/replication/authority.d.ts +4 -4
  30. package/dist/space-hosting.cjs +48 -6
  31. package/dist/space-hosting.cjs.map +1 -1
  32. package/dist/space-hosting.d.cts +23 -8
  33. package/dist/space-hosting.d.ts +23 -8
  34. package/dist/space-hosting.js +48 -6
  35. package/dist/space-hosting.js.map +1 -1
  36. package/package.json +4 -4
package/dist/core.cjs CHANGED
@@ -54,7 +54,10 @@ __export(core_exports, {
54
54
  KVService: () => import_sdk_core15.KVService,
55
55
  ManifestValidationError: () => import_sdk_core14.ManifestValidationError,
56
56
  MemorySessionStorage: () => MemorySessionStorage,
57
+ NodeIdentityMismatchError: () => import_sdk_services7.NodeIdentityMismatchError,
58
+ NodeNotRespondingError: () => import_sdk_services7.NodeNotRespondingError,
57
59
  NodeUserAuthorization: () => NodeUserAuthorization,
60
+ PartialNodeWriteError: () => import_sdk_services7.PartialNodeWriteError,
58
61
  PermissionNotInManifestError: () => import_sdk_core14.PermissionNotInManifestError,
59
62
  PrefixedKVService: () => import_sdk_core15.PrefixedKVService,
60
63
  ProtocolMismatchError: () => import_sdk_core22.ProtocolMismatchError,
@@ -138,6 +141,7 @@ __export(core_exports, {
138
141
  });
139
142
  module.exports = __toCommonJS(core_exports);
140
143
  var import_sdk_core12 = require("@tinycloud/sdk-core");
144
+ var import_sdk_services7 = require("@tinycloud/sdk-services");
141
145
 
142
146
  // src/replication/module-registry.ts
143
147
  var nodeLoaders;
@@ -609,11 +613,12 @@ var NodeUserAuthorization = class {
609
613
  this.tinycloudHosts = config.tinycloudHosts;
610
614
  this.tinycloudRegistryUrl = config.tinycloudRegistryUrl;
611
615
  this.tinycloudFallbackHosts = config.tinycloudFallbackHosts;
612
- this.registryPolicy = config.registryPolicy;
613
616
  this.newUserHost = config.newUserHost;
614
617
  this.registryChainId = config.registryChainId;
615
618
  this.registrySequenceStore = config.registrySequenceStore ?? (0, import_sdk_core5.createInMemoryRegistrySequenceStore)();
619
+ this.registryCacheStore = config.registryCacheStore ?? (0, import_sdk_core5.createInMemoryRegistryCacheStore)();
616
620
  this.onNodeNotFound = config.onNodeNotFound;
621
+ this.onRegistryFallback = config.onRegistryFallback;
617
622
  this.autoDiscoverLocalNode = config.autoDiscoverLocalNode;
618
623
  this.localNodeUrl = config.localNodeUrl;
619
624
  this.localLinkName = config.localLinkName;
@@ -681,13 +686,8 @@ var NodeUserAuthorization = class {
681
686
  * undefined for missing SIWEs and the SDK then treats the session as
682
687
  * expired-at-epoch-zero.
683
688
  *
684
- * @param hosts - The TinyCloud hosts this session was created against,
685
- * as persisted in {@link PersistedSessionData.tinycloudHosts}. When
686
- * present (and non-empty) they are adopted directly so the restored
687
- * session resolves to the same node as the original sign-in without
688
- * re-running registry/fallback resolution. When absent (old session)
689
- * hosts are resolved lazily on the first host-needing call via
690
- * {@link ensureTinyCloudHosts}.
689
+ * @param hosts - Persisted host hints for genuine no-entry onboarding.
690
+ * Current signed registry membership always takes precedence.
691
691
  */
692
692
  setRestoredTinyCloudSession(session, hosts) {
693
693
  const address = (0, import_sdk_core5.canonicalizeAddress)(session.address);
@@ -749,89 +749,84 @@ var NodeUserAuthorization = class {
749
749
  signature: session.signature
750
750
  };
751
751
  }
752
- /**
753
- * Ensure `tinycloudHosts` are resolved before a host-needing call.
754
- *
755
- * Fresh sign-in resolves hosts up front; a restored session may not have
756
- * (old persisted sessions predate {@link PersistedSessionData.tinycloudHosts}).
757
- * This guard makes a restored session resolve the node exactly like a fresh
758
- * sign-in — persisted hosts are preferred (already set by
759
- * {@link setRestoredTinyCloudSession}), otherwise the registry/fallback
760
- * resolution runs lazily here. Idempotent: {@link resolveTinyCloudHostsForSignIn}
761
- * early-returns when hosts are already set.
762
- *
763
- * Throws if hosts are unset and the restored session has no address/chainId
764
- * to resolve from — a real failure that must surface, not be masked.
765
- */
752
+ /** Refresh the primary space's registry destinations, including after restore. */
766
753
  async ensureTinyCloudHosts() {
767
- if (this.tinycloudHosts && this.tinycloudHosts.length > 0) {
768
- return;
769
- }
770
754
  if (this._address === void 0 || this._chainId === void 0) {
771
- throw new Error(
772
- "Cannot resolve TinyCloud hosts: no address/chainId available. Sign in or restore a session first."
773
- );
755
+ throw new Error("Cannot resolve TinyCloud hosts: sign in or restore a session first.");
756
+ }
757
+ await this.resolveTinyCloudHostsForSignIn(this._address, this._chainId, this._tinyCloudSession?.spaceId);
758
+ }
759
+ async resolveTinyCloudHostsForSignIn(address, chainId, restoredSpaceId) {
760
+ const spaceId = restoredSpaceId ?? this.resolveSignInCapabilities(address, chainId).spaceId;
761
+ const destinations = await this.resolveResourceDestinations(spaceId, "read");
762
+ for (const destination of destinations) await (0, import_sdk_services2.verifyResourceDestination)(destination, globalThis.fetch.bind(globalThis));
763
+ this.tinycloudHosts = destinations.map(({ url }) => url);
764
+ }
765
+ /** Current full-resource registry routing; saved hosts are only no-entry hints. */
766
+ async resolveResourceDestinations(resource, access, noEntryHosts = this.tinycloudHosts ?? []) {
767
+ const owner = resource.startsWith("urn:tinycloud:encryption:") ? (0, import_sdk_services2.parseNetworkId)(resource.split("/")[0]).ownerDid : (0, import_sdk_core5.parseSpaceUri)(resource)?.owner ?? (() => {
768
+ const key = /^tinycloud:key:(z[^:]+):[^/]+$/.exec(resource);
769
+ return key ? `did:key:${key[1]}` : void 0;
770
+ })();
771
+ if (!owner) throw new Error(`Cannot determine registry owner for ${resource}`);
772
+ const subject = owner.startsWith("did:pkh:") ? (0, import_sdk_core5.registryOwnerSubjectV1)(owner.slice(owner.lastIndexOf(":") + 1), this.registryChainId) : owner;
773
+ for (let attempt = 1; ; attempt++) {
774
+ try {
775
+ const resolved = await (0, import_sdk_core5.resolveRegistryResource)(subject, resource, access, {
776
+ registryUrl: this.tinycloudRegistryUrl,
777
+ registryChainId: this.registryChainId,
778
+ sequenceStore: this.registrySequenceStore,
779
+ cacheStore: this.registryCacheStore,
780
+ onRegistryFallback: this.onRegistryFallback
781
+ });
782
+ this.registryVerdict = resolved.verdict;
783
+ if (resolved.verdict.status === "unregistered") {
784
+ if (noEntryHosts.length === 0 && (this.autoDiscoverLocalNode ?? true)) {
785
+ const local = await (0, import_sdk_core5.discoverLocalTinyCloudNode)({
786
+ localNodeUrl: this.localNodeUrl,
787
+ localLinkName: this.localLinkName,
788
+ expectedNodeDid: this.expectedNodeDid,
789
+ identityStore: this.localNodeIdentityStore
790
+ });
791
+ if (local) {
792
+ (0, import_sdk_core5.warnRegistryDefaultNode)(resolved.verdict, this.registryCacheStore, this.onRegistryFallback);
793
+ return [{ url: local.url }];
794
+ }
795
+ }
796
+ const host = noEntryHosts[0] ?? this.newUserHost ?? (this.tinycloudFallbackHosts === void 0 ? "https://node.tinycloud.xyz" : this.tinycloudFallbackHosts?.[0]);
797
+ if (!host || this.newUserHost === null && noEntryHosts.length === 0) throw new import_sdk_core5.NodeNotFoundError(subject, resolved.verdict);
798
+ const url = (0, import_sdk_core5.exactHttpOrigin)(host, "new-user host");
799
+ (0, import_sdk_core5.warnRegistryDefaultNode)(resolved.verdict, this.registryCacheStore, this.onRegistryFallback);
800
+ return [{ url }];
801
+ }
802
+ const destinations = resolved.hosts.flatMap((host) => host.urls.map((url) => ({ url, nodeDid: host.nodeDid })));
803
+ return destinations;
804
+ } catch (error) {
805
+ if (!(error instanceof import_sdk_core5.NodeNotFoundError) && !(error instanceof import_sdk_core5.RegistryUnavailableError)) throw error;
806
+ if (!this.onNodeNotFound) throw error;
807
+ const decision = await this.onNodeNotFound({ error, attempt });
808
+ if (decision.action === "retry") continue;
809
+ if (decision.action === "use-host" && error.verdict?.status === "unregistered") {
810
+ const url = (0, import_sdk_core5.exactHttpOrigin)(decision.host, "new-user host");
811
+ (0, import_sdk_core5.warnRegistryDefaultNode)(error.verdict, this.registryCacheStore, this.onRegistryFallback);
812
+ return [{ url }];
813
+ }
814
+ throw error;
815
+ }
774
816
  }
775
- await this.resolveTinyCloudHostsForSignIn(this._address, this._chainId);
776
817
  }
777
- async resolveTinyCloudHostsForSignIn(address, chainId) {
778
- if (this.tinycloudHosts && this.tinycloudHosts.length > 0) {
779
- return;
780
- }
781
- if (this.registryPolicy === "fail-closed") {
782
- const resolved2 = await (0, import_sdk_core5.resolveFailClosedTinyCloudHosts)(address, {
783
- registryUrl: this.tinycloudRegistryUrl,
784
- registryChainId: this.registryChainId,
785
- newUserHost: this.newUserHost,
786
- sequenceStore: this.registrySequenceStore,
787
- onNodeNotFound: this.onNodeNotFound,
788
- autoDiscoverLocalNode: this.autoDiscoverLocalNode,
789
- localNodeUrl: this.localNodeUrl,
790
- localLinkName: this.localLinkName,
791
- expectedNodeDid: this.expectedNodeDid,
792
- localNodeIdentityStore: this.localNodeIdentityStore
793
- });
794
- this.tinycloudHosts = resolved2.hosts;
795
- this.registryVerdict = resolved2.verdict;
796
- return;
797
- }
798
- const subject = (0, import_sdk_core5.pkhDid)(address, chainId);
799
- const resolved = await (0, import_sdk_core5.resolveTinyCloudHosts)(subject, {
800
- registryUrl: this.tinycloudRegistryUrl,
801
- fallbackHosts: this.tinycloudFallbackHosts,
802
- autoDiscoverLocalNode: this.autoDiscoverLocalNode,
803
- localNodeUrl: this.localNodeUrl,
804
- localLinkName: this.localLinkName,
805
- expectedNodeDid: this.expectedNodeDid,
806
- localNodeIdentityStore: this.localNodeIdentityStore
807
- });
808
- this.tinycloudHosts = resolved.hosts;
818
+ get registryRecordPresent() {
819
+ return this.registryVerdict?.status === "registered";
809
820
  }
810
- /**
811
- * Space-creation guard (TC-1443). Under `registryPolicy: "fail-closed"`,
812
- * throws unless the owner's registry verdict allows a space on the primary
813
- * host: `NodeRegistryEntryExistsError` when the owner's record does not
814
- * list it, `RegistryUnavailableError` when the registry gives no
815
- * verdict. Reuses this session's sign-in verdict unless it was
816
- * `unavailable`, else looks it up now (explicit-host apps). Under the
817
- * default policy it returns at once and performs no lookup.
818
- */
819
- async assertMayCreateSpace() {
820
- if (this.registryPolicy !== "fail-closed") return;
821
- if (this._address === void 0) {
822
- throw new Error("Must be signed in to create a space");
823
- }
824
- await this.ensureTinyCloudHosts();
825
- const subject = (0, import_sdk_core5.registryOwnerSubjectV1)(this._address, this.registryChainId);
826
- let verdict = this.registryVerdict;
827
- if (verdict?.subject !== subject || verdict.status === "unavailable") {
828
- verdict = await (0, import_sdk_core5.lookupOwnerRegistryVerdict)(subject, {
829
- registryUrl: this.tinycloudRegistryUrl,
830
- sequenceStore: this.registrySequenceStore
831
- });
832
- this.registryVerdict = verdict;
821
+ /** Refresh membership before hosting; a saved no-entry verdict never authorizes a later creation. */
822
+ async assertMayCreateSpace(spaceId = this._tinyCloudSession?.spaceId, host = this.primaryTinyCloudHost) {
823
+ if (!spaceId) throw new Error("Must be signed in to create a space");
824
+ const destinations = await this.resolveResourceDestinations(spaceId, "host");
825
+ if (!destinations.some((destination) => destination.url === host)) {
826
+ const subject = (0, import_sdk_core5.parseSpaceUri)(spaceId)?.owner;
827
+ if (!subject || !this.registryVerdict) throw new Error("Cannot resolve space owner");
828
+ (0, import_sdk_core5.assertSpaceCreationAllowed)(this.registryVerdict, host, spaceId);
833
829
  }
834
- (0, import_sdk_core5.assertSpaceCreationAllowed)(verdict, this.primaryTinyCloudHost);
835
830
  }
836
831
  requireTinyCloudHosts() {
837
832
  if (!this.tinycloudHosts || this.tinycloudHosts.length === 0) {
@@ -1261,23 +1256,60 @@ var NodeUserAuthorization = class {
1261
1256
  * Create the space on the TinyCloud server (host delegation).
1262
1257
  * This registers the user as the owner of the space.
1263
1258
  */
1264
- async hostSpace(targetSpaceId, purpose, signal) {
1265
- if (!this._tinyCloudSession || !this._address || !this._chainId) {
1259
+ async hostSpace(targetSpaceId, purpose, signal, assertCurrent, siweParameters) {
1260
+ const context = typeof targetSpaceId === "object" ? targetSpaceId : void 0;
1261
+ const address = context?.address ?? this._address;
1262
+ const chainId = context?.chainId ?? this._chainId;
1263
+ if (!address || chainId === void 0 || !this._tinyCloudSession) {
1266
1264
  throw new Error("Must be signed in to host space");
1267
1265
  }
1266
+ const checkCurrent = () => {
1267
+ signal?.throwIfAborted();
1268
+ assertCurrent?.();
1269
+ };
1270
+ checkCurrent();
1268
1271
  await this.ensureTinyCloudHosts();
1269
- const host = this.primaryTinyCloudHost;
1270
- const spaceId = targetSpaceId ?? this._tinyCloudSession.spaceId;
1272
+ checkCurrent();
1273
+ const host = context?.host ?? this.primaryTinyCloudHost;
1274
+ const spaceId = context?.spaceId ?? (typeof targetSpaceId === "string" ? targetSpaceId : this._tinyCloudSession.spaceId);
1275
+ const destinations = await this.resolveResourceDestinations(spaceId, "host");
1276
+ if (this.registryVerdict?.status === "registered") {
1277
+ const results = [];
1278
+ for (const destination of destinations) {
1279
+ checkCurrent();
1280
+ try {
1281
+ await (0, import_sdk_services2.verifyResourceDestination)(destination, globalThis.fetch.bind(globalThis));
1282
+ const result = await (0, import_sdk_core5.activateSessionWithHost)(destination.url, this._tinyCloudSession.delegationHeader, signal);
1283
+ results.push(result.skipped?.includes(spaceId) ? { ...result, success: false, status: 403, error: "The session does not activate the requested space" } : result);
1284
+ } catch (error) {
1285
+ if ((0, import_sdk_services2.routingFailure)(error) || signal?.aborted) throw error;
1286
+ throw new import_sdk_services2.NodeNotRespondingError([destination.url], error);
1287
+ }
1288
+ }
1289
+ const failed = results.flatMap((result, index) => result.success ? [] : [{ url: destinations[index].url, status: result.status }]);
1290
+ if (failed.length) {
1291
+ if (destinations.length === 1) return results[0];
1292
+ throw new import_sdk_services2.PartialNodeWriteError(destinations.filter((_, index) => results[index].success).map(({ url }) => url), failed);
1293
+ }
1294
+ return {
1295
+ ...results[0],
1296
+ activated: [...new Set(results.flatMap((result) => result.activated ?? []))],
1297
+ skipped: [...new Set(results.flatMap((result) => result.skipped ?? []))]
1298
+ };
1299
+ }
1271
1300
  const peerId = await (0, import_sdk_core5.fetchPeerId)(host, spaceId, signal);
1272
- const siwe = this.wasm.generateHostSIWEMessage({
1273
- address: this._address,
1274
- chainId: this._chainId,
1275
- domain: this.domain,
1276
- issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
1277
- spaceId,
1278
- peerId
1279
- });
1280
- if (signal?.aborted) throw signal.reason;
1301
+ checkCurrent();
1302
+ const siwe = this.wasm.generateHostSIWEMessage(
1303
+ context && siweParameters ? siweParameters(peerId) : {
1304
+ address,
1305
+ chainId,
1306
+ domain: this.domain,
1307
+ issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
1308
+ spaceId,
1309
+ peerId
1310
+ }
1311
+ );
1312
+ checkCurrent();
1281
1313
  const signing = this.signMessage(siwe, purpose);
1282
1314
  let signature;
1283
1315
  if (!signal) {
@@ -1296,8 +1328,9 @@ var NodeUserAuthorization = class {
1296
1328
  signal.removeEventListener("abort", onAbort);
1297
1329
  }
1298
1330
  }
1299
- if (signal?.aborted) throw signal.reason;
1331
+ checkCurrent();
1300
1332
  const headers = this.wasm.siweToDelegationHeaders({ siwe, signature });
1333
+ checkCurrent();
1301
1334
  return (0, import_sdk_core5.submitHostDelegation)(host, headers, signal);
1302
1335
  }
1303
1336
  /**
@@ -1334,13 +1367,45 @@ var NodeUserAuthorization = class {
1334
1367
  if (!this._tinyCloudSession) {
1335
1368
  throw new Error("Must be signed in to ensure space exists");
1336
1369
  }
1337
- await this.ensureTinyCloudHosts();
1338
- const host = this.primaryTinyCloudHost;
1339
1370
  const primarySpaceId = this._tinyCloudSession.spaceId;
1340
- const result = await (0, import_sdk_core5.activateSessionWithHost)(
1341
- host,
1342
- this._tinyCloudSession.delegationHeader
1343
- );
1371
+ const primary = await this.resolveResourceDestinations(primarySpaceId, "read");
1372
+ this.tinycloudHosts = primary.map(({ url }) => url);
1373
+ const targets = new Map(primary.map((destination) => [destination.url, destination]));
1374
+ if (this.registryVerdict?.status === "registered" && this.registryVerdict.record) {
1375
+ const record = this.registryVerdict.record;
1376
+ const resources = Object.keys((0, import_sdk_core5.extractRecapAttenuations)(this._tinyCloudSession.siwe ?? ""));
1377
+ for (const space of record.spaces) {
1378
+ const registeredSpace = (0, import_sdk_core5.parseSpaceUri)(space.space);
1379
+ if (!resources.some((resource) => {
1380
+ const fullSpace = resource.split("/")[0];
1381
+ const requestedSpace = (0, import_sdk_core5.parseSpaceUri)(fullSpace);
1382
+ return fullSpace === space.space || registeredSpace !== null && requestedSpace !== null && registeredSpace.owner === requestedSpace.owner && registeredSpace.name === requestedSpace.name;
1383
+ })) continue;
1384
+ for (const membership of space.nodes) {
1385
+ const node = record.nodes.find((candidate) => candidate.did === membership.did);
1386
+ targets.set(node.url, { url: node.url, nodeDid: node.did });
1387
+ }
1388
+ }
1389
+ if (record.encryptionNode && resources.some((resource) => resource.startsWith("urn:tinycloud:encryption:"))) {
1390
+ const node = record.nodes.find((candidate) => candidate.did === record.encryptionNode);
1391
+ targets.set(node.url, { url: node.url, nodeDid: node.did });
1392
+ }
1393
+ }
1394
+ let result;
1395
+ for (const destination of targets.values()) {
1396
+ await (0, import_sdk_services2.verifyResourceDestination)(destination, globalThis.fetch.bind(globalThis));
1397
+ let activated;
1398
+ try {
1399
+ activated = await (0, import_sdk_core5.activateSessionWithHost)(destination.url, this._tinyCloudSession.delegationHeader);
1400
+ } catch (error) {
1401
+ throw new import_sdk_services2.NodeNotRespondingError([destination.url], error);
1402
+ }
1403
+ if (!activated.success && activated.status !== 404) {
1404
+ throw Object.assign(new Error(`Failed to activate session: ${describeHostFailure(activated)}`), { cause: activated });
1405
+ }
1406
+ if (destination.url === primary[0].url) result = activated;
1407
+ }
1408
+ if (!result) throw new Error("Primary space activation did not return a result");
1344
1409
  this.recordActivationSkippedSpaces(result, primarySpaceId);
1345
1410
  if (!result.success && result.status !== 404) {
1346
1411
  throw Object.assign(
@@ -1359,43 +1424,36 @@ var NodeUserAuthorization = class {
1359
1424
  *
1360
1425
  * @throws Error if the creation handler fails or times out, or hosting fails
1361
1426
  */
1362
- async ensurePrimarySpaceHosted(hosting) {
1363
- if (hosting !== "not-hosted") {
1364
- return;
1365
- }
1427
+ async ensurePrimarySpaceHosted(hosting, signal) {
1366
1428
  const session = this._tinyCloudSession;
1367
- if (!session) {
1368
- throw new Error("Must be signed in to host the primary space");
1369
- }
1429
+ if (hosting !== "not-hosted") return;
1430
+ if (!session) throw new Error("Must be signed in to host the primary space");
1370
1431
  const handler = this.spaceCreationHandler ?? (this.autoCreateSpace ? new import_sdk_core5.AutoApproveSpaceCreationHandler() : void 0);
1371
- if (!handler) {
1372
- return;
1373
- }
1374
- await this.ensureTinyCloudHosts();
1375
- const host = this.primaryTinyCloudHost;
1376
- const primarySpaceId = session.spaceId;
1377
- const creationContext = {
1378
- spaceId: primarySpaceId,
1379
- address: this._address,
1380
- chainId: this._chainId,
1381
- host
1382
- };
1432
+ if (!handler) return;
1383
1433
  const { createPrimarySpace } = await import("./space-hosting.cjs");
1384
1434
  await createPrimarySpace({
1385
1435
  handler,
1386
- context: creationContext,
1387
- guard: () => this.guardSpaceCreation(handler, creationContext),
1388
- host: () => this.hostSpace(primarySpaceId),
1436
+ session,
1437
+ currentSession: () => this._tinyCloudSession,
1438
+ signerAddress: () => this.signer.getAddress(),
1439
+ resolveHost: async () => {
1440
+ await this.ensureTinyCloudHosts();
1441
+ return this.primaryTinyCloudHost;
1442
+ },
1443
+ domain: this.domain,
1444
+ guard: (context) => this.guardSpaceCreation(handler, context),
1445
+ host: (context, assertCurrent, siweParameters) => this.hostSpace(context, void 0, signal, assertCurrent, siweParameters),
1389
1446
  onHosted: () => {
1390
1447
  this._spaceHosting = "hosted";
1391
1448
  },
1392
- activate: () => (0, import_sdk_core5.activateSessionWithHost)(host, session.delegationHeader)
1449
+ activate: (context) => (0, import_sdk_core5.activateSessionWithHost)(context.host, session.delegationHeader),
1450
+ signal
1393
1451
  });
1394
1452
  }
1395
1453
  /** {@link assertMayCreateSpace}, reporting a refusal to the creation handler before rethrowing it. */
1396
1454
  async guardSpaceCreation(handler, context) {
1397
1455
  try {
1398
- await this.assertMayCreateSpace();
1456
+ await this.assertMayCreateSpace(context.spaceId, context.host);
1399
1457
  } catch (error) {
1400
1458
  const failure = error instanceof Error ? error : new Error(String(error));
1401
1459
  handler.onSpaceCreationFailed?.(context, failure);
@@ -2539,6 +2597,7 @@ function rememberInvokeAny(invokeAny, sink, pending) {
2539
2597
 
2540
2598
  // src/delegation.ts
2541
2599
  var import_sdk_core6 = require("@tinycloud/sdk-core");
2600
+ var import_sdk_services4 = require("@tinycloud/sdk-services");
2542
2601
  var RuntimeDelegationRejectedError = class extends Error {
2543
2602
  constructor(reason, message) {
2544
2603
  super(message);
@@ -2565,7 +2624,8 @@ var SERVICE_BY_SHORT_NAME = {
2565
2624
  kv: "tinycloud.kv",
2566
2625
  sql: "tinycloud.sql",
2567
2626
  duckdb: "tinycloud.duckdb",
2568
- hooks: "tinycloud.hooks"
2627
+ hooks: "tinycloud.hooks",
2628
+ capabilities: "tinycloud.capabilities"
2569
2629
  };
2570
2630
  function authorizationWithoutBearer(authorization) {
2571
2631
  return authorization.replace(/^Bearer /i, "");
@@ -2696,8 +2756,8 @@ function signedAuthorityFromCompactUcan(authorization) {
2696
2756
  ));
2697
2757
  continue;
2698
2758
  }
2699
- const match = /^(?:tinycloud:\/\/([^/]+)|(tinycloud:[^/]+))\/(kv|sql|duckdb|hooks)(?:\/|$)(.*)$/.exec(resource);
2700
- if (!match) {
2759
+ const match = /^(?:tinycloud:\/\/([^/]+)|(tinycloud:[^/]+))\/([^/]+)(?:\/|$)(.*)$/.exec(resource);
2760
+ if (!match || !Object.prototype.hasOwnProperty.call(SERVICE_BY_SHORT_NAME, match[3])) {
2701
2761
  throw new Error(
2702
2762
  `Validated runtime delegation has an unsupported signed resource '${resource}'.`
2703
2763
  );
@@ -2842,13 +2902,8 @@ function resourcesMatch(actual, expected) {
2842
2902
  return JSON.stringify(actual.map(comparable)) === JSON.stringify(expected.map(comparable));
2843
2903
  }
2844
2904
  async function activateValidatedRuntimeDelegation(node, delegation, options) {
2845
- const host = normalizedHost(options.host);
2846
- if (delegation.host !== void 0 && normalizedHost(delegation.host) !== host) {
2847
- reject(
2848
- "host_mismatch",
2849
- `Runtime delegation host '${delegation.host}' does not match expected host '${options.host}'.`
2850
- );
2851
- }
2905
+ let host = normalizedHost(options.host);
2906
+ const differentHost = delegation.host !== void 0 && normalizedHost(delegation.host) !== host;
2852
2907
  if (!(delegation.expiry instanceof Date) || Number.isNaN(delegation.expiry.getTime())) {
2853
2908
  reject("authority_invalid", "Runtime delegation has an invalid expiry.");
2854
2909
  }
@@ -2892,6 +2947,13 @@ async function activateValidatedRuntimeDelegation(node, delegation, options) {
2892
2947
  if (options.authorize !== void 0 && !options.authorize(signed.permissions)) {
2893
2948
  throw new Error("Runtime delegation is not authorized for this runtime.");
2894
2949
  }
2950
+ if (differentHost) {
2951
+ const registeredHost = await node.resolveRegisteredDelegationHost?.(signed.permissions, host);
2952
+ if (registeredHost === void 0) {
2953
+ reject("host_mismatch", `Runtime delegation host '${delegation.host}' does not match expected host '${options.host}'.`);
2954
+ }
2955
+ host = normalizedHost(registeredHost);
2956
+ }
2895
2957
  const previouslyInstalled = node.getRuntimePermissionDelegations().find((candidate) => candidate.cid === cid);
2896
2958
  if (previouslyInstalled !== void 0 && previouslyInstalled.delegationHeader.Authorization !== authorization) {
2897
2959
  reject(
@@ -2935,6 +2997,7 @@ async function activateValidatedRuntimeDelegation(node, delegation, options) {
2935
2997
  await node.useRuntimeDelegation(installedCandidate);
2936
2998
  }
2937
2999
  } catch (error) {
3000
+ if ((0, import_sdk_services4.routingFailure)(error)) throw error;
2938
3001
  reject(
2939
3002
  activationRejectionReason(error),
2940
3003
  error instanceof Error ? error.message : String(error)
@@ -2953,6 +3016,7 @@ async function activateValidatedRuntimeDelegation(node, delegation, options) {
2953
3016
  "Runtime delegation activation installed authority that differs from the validated authority."
2954
3017
  );
2955
3018
  }
3019
+ node.refreshRegistryForCapabilities?.(signed.permissions);
2956
3020
  return {
2957
3021
  cid,
2958
3022
  delegation: installed,
@@ -3016,10 +3080,11 @@ function deserializeDelegation(data) {
3016
3080
  }
3017
3081
 
3018
3082
  // src/TinyCloudNode.ts
3083
+ var import_sdk_services5 = require("@tinycloud/sdk-services");
3019
3084
  var import_siwe2 = require("siwe");
3020
3085
  var import_share_envelope = require("@tinycloud/share-envelope");
3021
3086
  var import_sdk_core11 = require("@tinycloud/sdk-core");
3022
- var import_sdk_services4 = require("@tinycloud/sdk-services");
3087
+ var import_sdk_services6 = require("@tinycloud/sdk-services");
3023
3088
  var import_requestLog2 = require("@tinycloud/sdk-services/telemetry/requestLog");
3024
3089
  var import_resource2 = require("@tinycloud/sdk-services/encryption/resource");
3025
3090
 
@@ -3995,6 +4060,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3995
4060
  * ```
3996
4061
  */
3997
4062
  constructor(config = {}) {
4063
+ this.registryRefreshOwners = /* @__PURE__ */ new Set();
3998
4064
  this.signer = null;
3999
4065
  this.auth = null;
4000
4066
  this.consumedOpenKeyPendings = /* @__PURE__ */ new WeakSet();
@@ -4158,8 +4224,10 @@ var _TinyCloudNode = class _TinyCloudNode {
4158
4224
  this.config = {
4159
4225
  ...config,
4160
4226
  host: config.host ?? DEFAULT_HOST,
4227
+ registryPolicy: "fail-closed",
4161
4228
  // One store for every authorization this client creates.
4162
- registrySequenceStore: config.registrySequenceStore ?? (0, import_sdk_core11.createInMemoryRegistrySequenceStore)()
4229
+ registrySequenceStore: config.registrySequenceStore ?? (0, import_sdk_core11.createInMemoryRegistrySequenceStore)(),
4230
+ registryCacheStore: config.registryCacheStore ?? (0, import_sdk_core11.createInMemoryRegistryCacheStore)()
4163
4231
  };
4164
4232
  if (this.config.replication?.enabled) {
4165
4233
  const runtime = getNodeReplicationLoaders().runtime().then(({ ReplicationRuntime }) => {
@@ -4406,6 +4474,18 @@ var _TinyCloudNode = class _TinyCloudNode {
4406
4474
  return "unknown";
4407
4475
  }
4408
4476
  }
4477
+ /** Run the existing primary-space creation flow for the restored session. */
4478
+ async ensurePrimarySpaceHosted(hosting) {
4479
+ const graph = this._serviceGraph;
4480
+ const epoch = this.sessionEpoch;
4481
+ graph.assertActive();
4482
+ this.assertSessionEpoch(epoch, "Space hosting");
4483
+ const auth = this.auth;
4484
+ if (!auth || !this.session) return;
4485
+ await auth.ensurePrimarySpaceHosted(hosting, graph.signal);
4486
+ graph.assertActive();
4487
+ this.assertSessionEpoch(epoch, "Space hosting");
4488
+ }
4409
4489
  /**
4410
4490
  * The node's last answer for the primary space: recorded at sign-in from the
4411
4491
  * activation, or by {@link checkSpaceHosting}. `unknown` after a restore until
@@ -4451,7 +4531,11 @@ var _TinyCloudNode = class _TinyCloudNode {
4451
4531
  // A session saved but never registered on this node is refused: it registers once.
4452
4532
  activate: (signal) => {
4453
4533
  assertCurrent();
4454
- return (0, import_sdk_core11.activateSessionWithHost)(host, session.delegationHeader, AbortSignal.any([signal, graph.signal]));
4534
+ const activationSignal = AbortSignal.any([signal, graph.signal]);
4535
+ if (primary && primary.delegationCid === session.delegationCid) {
4536
+ return this.activateSignedSession(primary, primary.siwe, host, activationSignal);
4537
+ }
4538
+ return this.activateResourceSession(session.delegationHeader, [spaceId], host, activationSignal);
4455
4539
  }
4456
4540
  });
4457
4541
  assertCurrent();
@@ -4587,14 +4671,38 @@ var _TinyCloudNode = class _TinyCloudNode {
4587
4671
  get requests() {
4588
4672
  return this._requestLog === void 0 ? void 0 : (0, import_requestLog2.readOnlyRequestLog)(this._requestLog);
4589
4673
  }
4590
- createServiceGraphLifetime() {
4674
+ throwRegistryRefusal(status, text) {
4675
+ if (status !== 403 && status !== 503 || !text.startsWith("registry_unavailable:") && !text.startsWith("registry_hosting_denied:")) return;
4676
+ const code = status === 503 ? "registry_unavailable" : "node_registry_entry_exists";
4677
+ 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 } });
4678
+ }
4679
+ createServiceGraphLifetime(session) {
4680
+ const routedFetch = (0, import_sdk_services5.createResourceRoutingFetch)(
4681
+ async (url, init) => {
4682
+ try {
4683
+ const response = await globalThis.fetch(url, init);
4684
+ if (response.status === 403 || response.status === 503) {
4685
+ this.throwRegistryRefusal(response.status, await response.clone().text());
4686
+ }
4687
+ return response;
4688
+ } catch (error) {
4689
+ if ((0, import_sdk_services5.routingFailure)(error)) throw error;
4690
+ if (new URL(url).pathname.startsWith("/encryption/") && !init?.signal?.aborted) {
4691
+ throw new import_sdk_services5.NodeNotRespondingError([new URL(url).origin], error);
4692
+ }
4693
+ throw error;
4694
+ }
4695
+ },
4696
+ {
4697
+ resolveResource: (resource, access) => this.routingDestinations(resource, access),
4698
+ bindAudience: (authorization, nodeDid) => this.bindRoutedAudience(authorization, nodeDid, session),
4699
+ verifyDestination: (target) => (0, import_sdk_services5.verifyResourceDestination)(target, globalThis.fetch.bind(globalThis))
4700
+ }
4701
+ );
4591
4702
  return new ServiceGraphLifetime(
4592
4703
  this.invokeWithRuntimePermissions,
4593
4704
  this.invokeAnyWithRuntimePermissions,
4594
- // Resolve fetch at request time. This keeps the graph-owned lifetime
4595
- // signal while preserving the SDK's existing injectable global fetch
4596
- // behavior (including adapters that install it after construction).
4597
- (url, init) => globalThis.fetch(url, init),
4705
+ routedFetch,
4598
4706
  (context) => {
4599
4707
  this.storageFull.observe(context);
4600
4708
  if (this._requestLog) (0, import_requestLog2.observeReplicaReads)(context, this._requestLog);
@@ -4602,6 +4710,75 @@ var _TinyCloudNode = class _TinyCloudNode {
4602
4710
  this._requestLog
4603
4711
  );
4604
4712
  }
4713
+ async routingDestinations(resource, access, noEntryHosts) {
4714
+ const session = this.currentTinyCloudSession();
4715
+ if (session) this.refreshRegistryOwner((0, import_sdk_core11.pkhDid)(session.address, session.chainId));
4716
+ if (this.auth) return this.auth.resolveResourceDestinations(resource, access, noEntryHosts);
4717
+ const owner = resource.startsWith("urn:tinycloud:encryption:") ? (0, import_sdk_core11.parseNetworkId)(resource.split("/")[0]).ownerDid : (0, import_sdk_core11.parseSpaceUri)(resource)?.owner ?? (() => {
4718
+ const key = /^tinycloud:key:(z[^:]+):[^/]+$/.exec(resource);
4719
+ return key ? `did:key:${key[1]}` : void 0;
4720
+ })();
4721
+ if (!owner) throw new Error(`Cannot determine registry owner for ${resource}`);
4722
+ const subject = owner.startsWith("did:pkh:") ? (0, import_sdk_core11.pkhDid)(owner.slice(owner.lastIndexOf(":") + 1), this.config.registryChainId ?? 1) : owner;
4723
+ const resolved = await (0, import_sdk_core11.resolveRegistryResource)(subject, resource, access, {
4724
+ registryUrl: this.config.tinycloudRegistryUrl,
4725
+ registryChainId: this.config.registryChainId,
4726
+ sequenceStore: this.config.registrySequenceStore,
4727
+ cacheStore: this.config.registryCacheStore,
4728
+ onRegistryFallback: this.config.onRegistryFallback
4729
+ });
4730
+ if (resolved.verdict.status === "unregistered") {
4731
+ const hint = noEntryHosts === void 0 ? this.explicitHost ?? this.config.host : noEntryHosts[0];
4732
+ const host = hint ?? this.config.newUserHost ?? (this.config.tinycloudFallbackHosts === void 0 ? DEFAULT_HOST : this.config.tinycloudFallbackHosts?.[0]);
4733
+ if (!host || !hint && this.config.newUserHost === null) throw new import_sdk_core11.NodeNotFoundError(subject, resolved.verdict);
4734
+ const url = (0, import_sdk_core11.exactHttpOrigin)(host, "new-user host");
4735
+ (0, import_sdk_core11.warnRegistryDefaultNode)(resolved.verdict, this.config.registryCacheStore, this.config.onRegistryFallback);
4736
+ return [{ url }];
4737
+ }
4738
+ const destinations = resolved.hosts.flatMap((host) => host.urls.map((url) => ({ url, nodeDid: host.nodeDid })));
4739
+ return destinations;
4740
+ }
4741
+ /** Transport hints cannot override verified full-resource registry routing. */
4742
+ async resolveRegisteredDelegationHost(permissions, noEntryHost) {
4743
+ const resources = /* @__PURE__ */ new Map();
4744
+ let hasData = false;
4745
+ for (const permission of permissions) {
4746
+ const encryption = this.isEncryptionNetworkOperation(this.invocationServiceName(permission.service), permission.path, permission.space);
4747
+ const resource = encryption ? permission.path.split("/")[0] : permission.space;
4748
+ if (!resource) throw new import_sdk_services5.ResourceNotRoutedError("delegation");
4749
+ resources.set(resource, encryption ? "encryption" : "host");
4750
+ hasData || (hasData = !encryption);
4751
+ }
4752
+ let dataHost;
4753
+ let encryptionHost;
4754
+ let unserved;
4755
+ for (const [resource, access] of resources) {
4756
+ try {
4757
+ const destinations = await this.routingDestinations(resource, access, [noEntryHost]);
4758
+ if (destinations.some(({ nodeDid }) => nodeDid === void 0)) return void 0;
4759
+ if (access === "host") dataHost ?? (dataHost = destinations[0]?.url);
4760
+ else encryptionHost ?? (encryptionHost = destinations[0]?.url);
4761
+ } catch (error) {
4762
+ if (!(error instanceof import_sdk_core11.NodeNotFoundError) || error.verdict.status !== "registered") throw error;
4763
+ unserved ?? (unserved = error);
4764
+ }
4765
+ }
4766
+ const host = hasData ? dataHost : encryptionHost;
4767
+ if (!host) throw unserved ?? new import_sdk_services5.ResourceNotRoutedError("delegation");
4768
+ return host;
4769
+ }
4770
+ async bindRoutedAudience(authorization, nodeDid, stagedSession) {
4771
+ const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(authorization.split(".")[1])));
4772
+ const issuer = typeof payload === "object" && payload !== null && "iss" in payload ? payload.iss : void 0;
4773
+ const session = [
4774
+ stagedSession,
4775
+ this._serviceContext?.session,
4776
+ this.auth?.tinyCloudSession,
4777
+ ...this.runtimePermissionGrants.map((grant) => grant.session)
4778
+ ].find((candidate) => candidate?.verificationMethod === issuer);
4779
+ if (!session) throw new Error("Invocation signing session is not active");
4780
+ return rewriteInvocationAudience(authorization, nodeDid, session.jwk);
4781
+ }
4605
4782
  syncResolvedHostFromAuth() {
4606
4783
  const host = this.auth?.hosts[0];
4607
4784
  if (host) {
@@ -4715,6 +4892,29 @@ var _TinyCloudNode = class _TinyCloudNode {
4715
4892
  const session = this.currentTinyCloudSession();
4716
4893
  return session?.siwe ? this.projectSignedRecapCapabilities(session.siwe) : [];
4717
4894
  }
4895
+ /** Start one background refresh for each owner derived from verified authority. */
4896
+ refreshRegistryForCapabilities(permissions) {
4897
+ for (const permission of permissions) {
4898
+ try {
4899
+ const owner = permission.service === "tinycloud.encryption" && permission.path.startsWith("urn:tinycloud:encryption:") ? (0, import_sdk_core11.parseNetworkId)(permission.path.split("/")[0]).ownerDid : permission.space ? this.ownerDidFromSpaceId(permission.space) : void 0;
4900
+ if (owner) this.refreshRegistryOwner(owner);
4901
+ } catch {
4902
+ }
4903
+ }
4904
+ }
4905
+ refreshRegistryOwner(owner) {
4906
+ if (this.config.tinycloudRegistryUrl === null) return;
4907
+ const subject = owner.startsWith("did:pkh:") ? (0, import_sdk_core11.pkhDid)(owner.slice(owner.lastIndexOf(":") + 1), this.config.registryChainId ?? 1) : owner;
4908
+ const key = (0, import_sdk_core11.registryOwnerKeyV2)(subject);
4909
+ if (this.registryRefreshOwners.has(key)) return;
4910
+ this.registryRefreshOwners.add(key);
4911
+ void (0, import_sdk_core11.refreshOwnerRegistry)(subject, {
4912
+ registryUrl: this.config.tinycloudRegistryUrl,
4913
+ registryChainId: this.config.registryChainId,
4914
+ sequenceStore: this.config.registrySequenceStore,
4915
+ cacheStore: this.config.registryCacheStore
4916
+ }).catch(() => void 0);
4917
+ }
4718
4918
  /** Normalize signed capabilities without collapsing nested encryption into raw authority. */
4719
4919
  projectSignedRecapCapabilities(siwe) {
4720
4920
  const projected = this.parseRecapWithCaveats(siwe).map((entry) => {
@@ -5146,6 +5346,8 @@ var _TinyCloudNode = class _TinyCloudNode {
5146
5346
  const primarySession = this.currentTinyCloudSession();
5147
5347
  if (primarySession) {
5148
5348
  this.registerPrimarySessionGrant(primarySession);
5349
+ this.refreshRegistryOwner((0, import_sdk_core11.pkhDid)(primarySession.address, primarySession.chainId));
5350
+ this.refreshRegistryForCapabilities(this.getVerifiedSessionCapabilities());
5149
5351
  if (this.auth?.spaceHosting === "unknown") {
5150
5352
  await this.checkSpaceHosting();
5151
5353
  this.assertSessionEpoch(epoch, "Sign-in");
@@ -5368,8 +5570,8 @@ var _TinyCloudNode = class _TinyCloudNode {
5368
5570
  const primarySession = auth.tinyCloudSession;
5369
5571
  const defaultSpaceId = this.ownedSpaceId("default");
5370
5572
  const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === import_sdk_core11.BOOTSTRAP_SESSION_REQUESTS.default;
5371
- if (steps.some((step) => step.kind === "host")) {
5372
- await this.assertMayCreateSpace();
5573
+ for (const step of steps) {
5574
+ if (step.kind === "host") await this.assertMayCreateSpace(step.spaceId);
5373
5575
  }
5374
5576
  for (const step of steps) {
5375
5577
  if (step.kind !== "session") continue;
@@ -5418,10 +5620,20 @@ var _TinyCloudNode = class _TinyCloudNode {
5418
5620
  if (!session) {
5419
5621
  throw new Error(`Missing bootstrap session for ${step.space}`);
5420
5622
  }
5421
- const activated = await (0, import_sdk_core11.activateSessionWithHost)(host, session.delegationHeader, graph.signal).catch((error) => {
5422
- graph.assertActive();
5423
- throw error;
5424
- });
5623
+ const destinations = await this.routingDestinations(step.spaceId, "host");
5624
+ let activated;
5625
+ for (const destination of destinations) {
5626
+ try {
5627
+ await (0, import_sdk_services5.verifyResourceDestination)(destination, globalThis.fetch.bind(globalThis));
5628
+ const result = await (0, import_sdk_core11.activateSessionWithHost)(destination.url, session.delegationHeader, graph.signal);
5629
+ if (!result.success) throw Object.assign(new Error("Bootstrap activation failed"), { cause: result });
5630
+ activated = result;
5631
+ } catch (error) {
5632
+ graph.assertActive();
5633
+ throw error;
5634
+ }
5635
+ }
5636
+ if (!activated) throw new Error("Bootstrap space has no destination");
5425
5637
  graph.assertActive();
5426
5638
  if (!activated.success || activated.skipped?.includes(step.spaceId)) {
5427
5639
  throw Object.assign(
@@ -5751,7 +5963,7 @@ var _TinyCloudNode = class _TinyCloudNode {
5751
5963
  if (!this.currentSessionCoversAccountRegistry()) {
5752
5964
  if (!_TinyCloudNode.accountRegistrySkipLogged) {
5753
5965
  _TinyCloudNode.accountRegistrySkipLogged = true;
5754
- import_sdk_services4.tinyCloudDebugLogger.log("registry.skip");
5966
+ import_sdk_services6.tinyCloudDebugLogger.log("registry.skip");
5755
5967
  }
5756
5968
  return;
5757
5969
  }
@@ -6004,6 +6216,13 @@ var _TinyCloudNode = class _TinyCloudNode {
6004
6216
  if (!session) {
6005
6217
  throw new Error("Owned space hosting requires an active session");
6006
6218
  }
6219
+ await this.assertMayCreateSpace(spaceId);
6220
+ if (this.auth.registryRecordPresent) {
6221
+ const hosted = await this.hostOwnedSpaceOnce(spaceId, signal);
6222
+ if (!hosted.success) throw Object.assign(new Error(`Failed to activate listed space ${spaceId}: ${describeHostFailure2(hosted)}`), { cause: hosted });
6223
+ if (this.auth.tinyCloudSession === session) this.confirmedHostedSpaceIds.add(spaceId);
6224
+ return;
6225
+ }
6007
6226
  if (this.confirmedHostedSpaceIds.has(spaceId) || this.hostedOwnedSpaceIds.has(spaceId)) {
6008
6227
  return;
6009
6228
  }
@@ -6022,7 +6241,7 @@ var _TinyCloudNode = class _TinyCloudNode {
6022
6241
  { cause: activation }
6023
6242
  );
6024
6243
  }
6025
- await this.assertMayCreateSpace();
6244
+ await this.assertMayCreateSpace(spaceId);
6026
6245
  if (signal?.aborted) throw signal.reason;
6027
6246
  const created = await this.hostOwnedSpaceOnce(spaceId, signal);
6028
6247
  if (!created.success) {
@@ -6083,17 +6302,19 @@ var _TinyCloudNode = class _TinyCloudNode {
6083
6302
  { cause: hosted }
6084
6303
  );
6085
6304
  }
6086
- const activation = await (0, import_sdk_core11.activateSessionWithHost)(
6087
- host,
6088
- session.delegationHeader
6089
- );
6090
- if (!activation.success || activation.skipped?.includes(spaceId)) {
6091
- throw Object.assign(
6092
- new Error(
6093
- `Failed to activate session for owned space ${spaceId}: ${activation.success ? "space was skipped" : describeHostFailure2(activation)}`
6094
- ),
6095
- { cause: activation }
6305
+ if (!this.auth.registryRecordPresent) {
6306
+ const activation = await (0, import_sdk_core11.activateSessionWithHost)(
6307
+ host,
6308
+ session.delegationHeader
6096
6309
  );
6310
+ if (!activation.success || activation.skipped?.includes(spaceId)) {
6311
+ throw Object.assign(
6312
+ new Error(
6313
+ `Failed to activate session for owned space ${spaceId}: ${activation.success ? "space was skipped" : describeHostFailure2(activation)}`
6314
+ ),
6315
+ { cause: activation }
6316
+ );
6317
+ }
6097
6318
  }
6098
6319
  if (this.auth?.tinyCloudSession === session) this.hostedOwnedSpaceIds.add(spaceId);
6099
6320
  try {
@@ -6137,11 +6358,9 @@ var _TinyCloudNode = class _TinyCloudNode {
6137
6358
  * session's recap doesn't cover it and the registry has no record; the host
6138
6359
  * SIWE is idempotent server-side. Must be called after {@link signIn}.
6139
6360
  *
6140
- * Under `registryPolicy: "fail-closed"`, hosting is refused with
6141
- * `NodeRegistryEntryExistsError` when the owner's registry record does not
6142
- * list the host, or `RegistryUnavailableError` when the registry gives no
6143
- * verdict. {@link hostOwnedSpace} itself is an explicit action and is not
6144
- * guarded.
6361
+ * Current signed per-space membership is checked before both automatic and
6362
+ * explicit hosting. Registered replicas use the session grant; no-entry
6363
+ * onboarding retains the host-SIWE flow.
6145
6364
  *
6146
6365
  * @param name - The owned space name (e.g. `"secrets"`).
6147
6366
  * @returns The hosted owned-space URI.
@@ -6151,27 +6370,20 @@ var _TinyCloudNode = class _TinyCloudNode {
6151
6370
  throw new Error("Not signed in. Call signIn() first.");
6152
6371
  }
6153
6372
  const spaceId = this.ownedSpaceId(name);
6373
+ await this.assertMayCreateSpace(spaceId);
6154
6374
  if (this.hostedOwnedSpaceIds.has(spaceId) || await this.isOwnedSpaceRegistered(spaceId)) {
6155
6375
  return spaceId;
6156
6376
  }
6157
- await this.assertMayCreateSpace();
6158
6377
  return this.hostOwnedSpace(name);
6159
6378
  }
6160
- /**
6161
- * Space-creation guard (TC-1443) for the automatic hosting paths: sign-in's
6162
- * owned spaces, account bootstrap, {@link ensureOwnedSpaceHosted} and
6163
- * {@link ensurePublicSpace}. Only `registryPolicy: "fail-closed"` consults
6164
- * the registry ({@link NodeUserAuthorization.assertMayCreateSpace}); the
6165
- * default policy performs no lookup.
6166
- */
6167
- async assertMayCreateSpace() {
6168
- if (this.config.registryPolicy !== "fail-closed") return;
6169
- await this.auth.assertMayCreateSpace();
6379
+ /** Refresh full-space membership before any automatic or explicit hosting. */
6380
+ async assertMayCreateSpace(spaceId) {
6381
+ await this.routingDestinations(spaceId, "host");
6170
6382
  }
6171
6383
  /** Registry-policy settings shared by every authorization this client creates. */
6172
6384
  registryPolicyConfig() {
6173
- const { registryPolicy, newUserHost, registryChainId, registrySequenceStore, onNodeNotFound } = this.config;
6174
- return { registryPolicy, newUserHost, registryChainId, registrySequenceStore, onNodeNotFound };
6385
+ const { registryPolicy, newUserHost, registryChainId, registrySequenceStore, registryCacheStore, onNodeNotFound, onRegistryFallback } = this.config;
6386
+ return { registryPolicy, newUserHost, registryChainId, registrySequenceStore, registryCacheStore, onNodeNotFound, onRegistryFallback };
6175
6387
  }
6176
6388
  /**
6177
6389
  * Submit the host delegation for an owned space, sharing one in-flight
@@ -6377,12 +6589,9 @@ var _TinyCloudNode = class _TinyCloudNode {
6377
6589
  }
6378
6590
  stagedSessionExpiry = signedExpiry ?? persistedPolicyExpiry ?? persistedExpiry(void 0);
6379
6591
  }
6380
- const resolvedHost = await this.resolveRestoredHost(
6381
- sessionData.tinycloudHosts,
6382
- restoredAddress,
6383
- sessionData.chainId
6384
- );
6385
- const stagedHost = resolvedHost ?? this.config.host;
6592
+ const restoreHints = this.explicitHost ? [this.explicitHost] : sessionData.tinycloudHosts ?? [];
6593
+ const resolvedHost = this.config.replication?.enabled ? (await this.routingDestinations(sessionData.spaceId, "read", restoreHints))[0]?.url : void 0;
6594
+ const stagedHost = resolvedHost ?? (0, import_sdk_core11.exactHttpOrigin)(restoreHints[0] ?? this.config.host, "restored host hint");
6386
6595
  let restoredNodeFeatures;
6387
6596
  let replicaProtocolMismatch = false;
6388
6597
  if (this.config.replication?.enabled) {
@@ -6499,7 +6708,7 @@ var _TinyCloudNode = class _TinyCloudNode {
6499
6708
  this.sessionManager = stagedManager;
6500
6709
  this.sessionKeyId = stagedKeyId;
6501
6710
  this.sessionKeyJwk = stagedJwk;
6502
- if (resolvedHost) this.config.host = resolvedHost;
6711
+ this.config.host = stagedHost;
6503
6712
  if (spaceOwnerKey(serviceSession.spaceId) !== this.currentStorageOwner()) {
6504
6713
  this.storageFull.clear();
6505
6714
  this._requestLog?.clear();
@@ -6566,6 +6775,10 @@ var _TinyCloudNode = class _TinyCloudNode {
6566
6775
  if (attempt === this.latestSessionAttempt) await this.endSession();
6567
6776
  this.assertSessionEpoch(epoch, "Session restore");
6568
6777
  }
6778
+ if (this.config.replication?.enabled) {
6779
+ if (stagedAddress !== void 0) this.refreshRegistryOwner((0, import_sdk_core11.pkhDid)(stagedAddress, stagedChainId));
6780
+ this.refreshRegistryForCapabilities(this.getVerifiedSessionCapabilities());
6781
+ }
6569
6782
  }
6570
6783
  stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
6571
6784
  if (!session?.siwe) return { cache: void 0, grants: [] };
@@ -6614,7 +6827,7 @@ var _TinyCloudNode = class _TinyCloudNode {
6614
6827
  };
6615
6828
  }
6616
6829
  stageRestoredServiceGraph(input) {
6617
- const graph = this.createServiceGraphLifetime();
6830
+ const graph = this.createServiceGraphLifetime(input.serviceSession);
6618
6831
  const core = this.auth ? new import_sdk_core11.TinyCloud(this.auth, {
6619
6832
  invokeAny: graph.invokeAny,
6620
6833
  telemetry: this.config.telemetry,
@@ -6839,56 +7052,6 @@ var _TinyCloudNode = class _TinyCloudNode {
6839
7052
  spaceService.updateConfig({ sharingService });
6840
7053
  return { core, graph, serviceContext, kv, sql, duckdb, hooks, vault, encryption, capabilityRegistry, keyProvider, sharingService, delegationManager, spaceService };
6841
7054
  }
6842
- /**
6843
- * Resolve the host a restored session should target.
6844
- *
6845
- * Mirrors fresh sign-in host resolution but for the restore path:
6846
- * an explicit/pinned host always wins, then the hosts the session was
6847
- * persisted with, then a lazy registry/fallback resolution for sessions
6848
- * that predate the persisted `tinycloudHosts` field. Returns `undefined`
6849
- * only when there's nothing to resolve from (no explicit host, no
6850
- * persisted hosts, and no address/chainId) — in which case the existing
6851
- * `config.host` (default) is left in place.
6852
- *
6853
- * Resolution failures are surfaced, not swallowed: a genuinely broken
6854
- * registry lookup throws rather than silently falling back to a wrong host.
6855
- */
6856
- async resolveRestoredHost(persistedHosts, address, chainId) {
6857
- if (this.explicitHost) {
6858
- return this.explicitHost;
6859
- }
6860
- if (persistedHosts && persistedHosts.length > 0) {
6861
- return persistedHosts[0];
6862
- }
6863
- if (address === void 0 || chainId === void 0) {
6864
- return void 0;
6865
- }
6866
- if (this.config.registryPolicy === "fail-closed") {
6867
- const resolved2 = await (0, import_sdk_core11.resolveFailClosedTinyCloudHosts)(address, {
6868
- registryUrl: this.config.tinycloudRegistryUrl,
6869
- registryChainId: this.config.registryChainId,
6870
- newUserHost: this.config.newUserHost,
6871
- sequenceStore: this.config.registrySequenceStore,
6872
- onNodeNotFound: this.config.onNodeNotFound,
6873
- autoDiscoverLocalNode: this.config.autoDiscoverLocalNode,
6874
- localNodeUrl: this.config.localNodeUrl,
6875
- localLinkName: this.config.localLinkName,
6876
- expectedNodeDid: this.config.expectedNodeDid,
6877
- localNodeIdentityStore: this.config.localNodeIdentityStore
6878
- });
6879
- return resolved2.hosts[0];
6880
- }
6881
- const resolved = await (0, import_sdk_core11.resolveTinyCloudHosts)((0, import_sdk_core11.pkhDid)(address, chainId), {
6882
- registryUrl: this.config.tinycloudRegistryUrl,
6883
- fallbackHosts: this.config.tinycloudFallbackHosts,
6884
- autoDiscoverLocalNode: this.config.autoDiscoverLocalNode,
6885
- localNodeUrl: this.config.localNodeUrl,
6886
- localLinkName: this.config.localLinkName,
6887
- expectedNodeDid: this.config.expectedNodeDid,
6888
- localNodeIdentityStore: this.config.localNodeIdentityStore
6889
- });
6890
- return resolved.hosts[0];
6891
- }
6892
7055
  /**
6893
7056
  * Resolve the currently-active TinyCloudSession, preferring the auth
6894
7057
  * layer's value (wallet mode) and falling back to the node-level
@@ -7363,10 +7526,10 @@ var _TinyCloudNode = class _TinyCloudNode {
7363
7526
  * second time. Only a value recorded for this exact host qualifies; older
7364
7527
  * nodes that omit `nodeId` fall through to the fetch.
7365
7528
  */
7366
- async fetchNodeId() {
7367
- const cached = this.auth?.nodeIdForHost(this.config.host);
7529
+ async fetchNodeId(host = this.config.host) {
7530
+ const cached = this.auth?.nodeIdForHost(host);
7368
7531
  if (cached) return cached;
7369
- const response = await fetch(`${this.config.host}/info`);
7532
+ const response = await fetch(`${host}/info`);
7370
7533
  if (!response.ok) {
7371
7534
  throw await (0, import_sdk_core11.httpResponseError)(response, "Failed to fetch node info");
7372
7535
  }
@@ -7414,10 +7577,24 @@ var _TinyCloudNode = class _TinyCloudNode {
7414
7577
  )
7415
7578
  };
7416
7579
  }
7417
- async fetchEncryptionNetworkAt(host, networkId, fetchFn) {
7418
- const response = await fetchFn(
7419
- `${host}/encryption/networks/${encodeURIComponent(networkId)}`
7420
- );
7580
+ async encryptionDestination(networkId) {
7581
+ const destination = (await this.routingDestinations(networkId, "encryption"))[0];
7582
+ if (!destination) throw new Error("No encryption destination is registered");
7583
+ await (0, import_sdk_services5.verifyResourceDestination)(destination, globalThis.fetch.bind(globalThis));
7584
+ return { ...destination, nodeDid: destination.nodeDid ?? await this.fetchNodeId(destination.url) };
7585
+ }
7586
+ async fetchEncryptionNetworkAt(host, networkId, fetchFn, callerSignal) {
7587
+ let response;
7588
+ const timeout = AbortSignal.timeout(5e3);
7589
+ try {
7590
+ response = await fetchFn(`${host}/encryption/networks/${encodeURIComponent(networkId)}`, {
7591
+ signal: callerSignal ? AbortSignal.any([callerSignal, timeout]) : timeout,
7592
+ redirect: "error"
7593
+ });
7594
+ } catch (error) {
7595
+ if ((0, import_sdk_services5.routingFailure)(error) || callerSignal?.aborted || !timeout.aborted && error instanceof Error && error.name === "AbortError") throw error;
7596
+ throw new import_sdk_services5.NodeNotRespondingError([host], error);
7597
+ }
7421
7598
  if (response.status === 404) return null;
7422
7599
  if (!response.ok) {
7423
7600
  throw await (0, import_sdk_core11.httpResponseError)(response, `Failed to fetch encryption network ${networkId}`);
@@ -7427,18 +7604,18 @@ var _TinyCloudNode = class _TinyCloudNode {
7427
7604
  }
7428
7605
  createEncryptionService(binding) {
7429
7606
  const graph = binding?.graph ?? this._serviceGraph;
7430
- const host = binding?.host ?? this.config.host;
7431
7607
  const session = binding?.session ?? this.requireEncryptionSession();
7432
7608
  const did = binding?.did ?? this.did;
7433
7609
  const address = binding?.address ?? this._address;
7434
7610
  const chainId = binding?.chainId ?? this._chainId;
7435
7611
  const crypto2 = this.createEncryptionCrypto();
7436
- const descriptorCache = /* @__PURE__ */ new Map();
7437
7612
  const transport = {
7438
- postDecrypt: async ({ networkId, authorization, canonicalBody, signal }) => {
7613
+ postDecrypt: async ({ targetNode, networkId, authorization, canonicalBody, signal }) => {
7439
7614
  graph.assertActive();
7615
+ const destination = await this.encryptionDestination(networkId);
7616
+ if (destination.nodeDid !== targetNode) throw new import_sdk_services5.NodeIdentityMismatchError(destination.url, destination.nodeDid, targetNode);
7440
7617
  const response = await graph.fetch(
7441
- `${host}/encryption/networks/${encodeURIComponent(networkId)}/decrypt`,
7618
+ `${destination.url}/encryption/networks/${encodeURIComponent(networkId)}/decrypt`,
7442
7619
  {
7443
7620
  method: "POST",
7444
7621
  headers: {
@@ -7451,7 +7628,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7451
7628
  );
7452
7629
  graph.assertActive();
7453
7630
  if (!response.ok) {
7454
- const permissionHint = response.status === 401 || response.status === 403 ? (0, import_sdk_services4.parsePermissionHint)({
7631
+ const permissionHint = response.status === 401 || response.status === 403 ? (0, import_sdk_services6.parsePermissionHint)({
7455
7632
  service: "tinycloud.encryption",
7456
7633
  path: networkId,
7457
7634
  actions: ["tinycloud.encryption/decrypt"]
@@ -7466,6 +7643,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7466
7643
  }
7467
7644
  };
7468
7645
  return new import_sdk_core11.EncryptionService({
7646
+ resolveTargetNode: async (networkId) => (await this.encryptionDestination(networkId)).nodeDid,
7469
7647
  crypto: crypto2,
7470
7648
  signer: {
7471
7649
  signDecryptInvocation: async (input) => {
@@ -7489,18 +7667,8 @@ var _TinyCloudNode = class _TinyCloudNode {
7489
7667
  node: {
7490
7668
  fetchByNetworkId: async (networkId) => {
7491
7669
  graph.assertActive();
7492
- let pending = descriptorCache.get(networkId);
7493
- if (!pending) {
7494
- pending = this.fetchEncryptionNetworkAt(host, networkId, graph.fetch).then((descriptor2) => {
7495
- if (descriptor2 === null) descriptorCache.delete(networkId);
7496
- return descriptor2;
7497
- }).catch((error) => {
7498
- descriptorCache.delete(networkId);
7499
- throw error;
7500
- });
7501
- descriptorCache.set(networkId, pending);
7502
- }
7503
- const descriptor = await pending;
7670
+ const destination = await this.encryptionDestination(networkId);
7671
+ const descriptor = await this.fetchEncryptionNetworkAt(destination.url, networkId, graph.fetch, graph.signal);
7504
7672
  graph.assertActive();
7505
7673
  return descriptor;
7506
7674
  }
@@ -7508,13 +7676,15 @@ var _TinyCloudNode = class _TinyCloudNode {
7508
7676
  wellKnown: {
7509
7677
  fetchWellKnown: async (principal, discoveryKey) => {
7510
7678
  graph.assertActive();
7679
+ const destination = await this.encryptionDestination(this.getDefaultEncryptionNetworkId());
7680
+ if (this.auth?.registryRecordPresent) return null;
7511
7681
  if (!address || !didPrincipalMatches2(principal, did)) {
7512
7682
  return null;
7513
7683
  }
7514
7684
  const publicSpaceId = (0, import_sdk_core11.makePublicSpaceId)(address, chainId);
7515
7685
  const encodedKey = discoveryKey.split("/").map(encodeURIComponent).join("/");
7516
7686
  const response = await graph.fetch(
7517
- `${host}/public/${encodeURIComponent(publicSpaceId)}/kv/${encodedKey}`,
7687
+ `${destination.url}/public/${encodeURIComponent(publicSpaceId)}/kv/${encodedKey}`,
7518
7688
  { method: "GET" }
7519
7689
  );
7520
7690
  graph.assertActive();
@@ -7884,7 +8054,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7884
8054
  const signature = await this.signer.signMessage(prepared.siwe);
7885
8055
  assertOwnerGraphActive();
7886
8056
  const delegationSession = this.wasmBindings.completeSessionSetup({ ...prepared, signature });
7887
- const activation = await (0, import_sdk_core11.activateSessionWithHost)(host, delegationSession.delegationHeader);
8057
+ const activation = await this.activateSignedSession(delegationSession, prepared.siwe, host);
7888
8058
  assertOwnerGraphActive();
7889
8059
  if (!activation.success) {
7890
8060
  throw Object.assign(
@@ -8203,15 +8373,16 @@ var _TinyCloudNode = class _TinyCloudNode {
8203
8373
  async getEncryptionNetwork(nameOrNetworkId = this.getDefaultEncryptionNetworkId()) {
8204
8374
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
8205
8375
  return this.fetchEncryptionNetworkAt(
8206
- this.config.host,
8376
+ (await this.encryptionDestination(networkId)).url,
8207
8377
  networkId,
8208
8378
  globalThis.fetch.bind(globalThis)
8209
8379
  );
8210
8380
  }
8211
8381
  async createEncryptionNetwork(name = DEFAULT_ENCRYPTION_NETWORK_NAME) {
8212
- const targetNode = await this.fetchNodeId();
8213
8382
  const ownerDid = this.did;
8214
8383
  const networkId = this.getDefaultEncryptionNetworkId(name);
8384
+ const destination = await this.encryptionDestination(networkId);
8385
+ const targetNode = destination.nodeDid;
8215
8386
  const body = {
8216
8387
  name,
8217
8388
  ownerDid,
@@ -8234,7 +8405,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8234
8405
  action: NETWORK_CREATE_ACTION2,
8235
8406
  facts
8236
8407
  });
8237
- const response = await fetch(`${this.config.host}/encryption/networks`, {
8408
+ const response = await this._serviceGraph.fetch(`${destination.url}/encryption/networks`, {
8238
8409
  method: "POST",
8239
8410
  headers: {
8240
8411
  Authorization: signed.authorization,
@@ -8327,7 +8498,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8327
8498
  );
8328
8499
  if (result.status !== "ok") {
8329
8500
  if (result.status === "permission_required") {
8330
- const hint = (0, import_sdk_services4.parsePermissionHint)(result.hint);
8501
+ const hint = (0, import_sdk_services6.parsePermissionHint)(result.hint);
8331
8502
  if (hint === void 0) return { status: "read_failed" };
8332
8503
  return {
8333
8504
  status: "permission_required",
@@ -8574,6 +8745,97 @@ var _TinyCloudNode = class _TinyCloudNode {
8574
8745
  }
8575
8746
  return { recap: exactVerifiedRecap(verified), expiresAt };
8576
8747
  }
8748
+ async activateResourceSession(delegationHeader, resources, hostHint, signal) {
8749
+ const targets = /* @__PURE__ */ new Map();
8750
+ const routedSpaces = [];
8751
+ let unserved;
8752
+ let requestedResource;
8753
+ for (const resource of new Set(resources)) {
8754
+ requestedResource ?? (requestedResource = resource);
8755
+ signal?.throwIfAborted();
8756
+ try {
8757
+ const encryption = resource.startsWith("urn:tinycloud:encryption:");
8758
+ const destinations = await this.routingDestinations(
8759
+ resource,
8760
+ encryption ? "encryption" : "host",
8761
+ [hostHint ?? this.config.host]
8762
+ );
8763
+ if (!encryption && destinations.some(({ nodeDid }) => nodeDid !== void 0)) routedSpaces.push(resource);
8764
+ for (const destination of destinations) {
8765
+ let target = targets.get(destination.url);
8766
+ if (!target) {
8767
+ target = { destination, spaces: [] };
8768
+ targets.set(destination.url, target);
8769
+ }
8770
+ if (!encryption && destination.nodeDid !== void 0) target.spaces.push(resource);
8771
+ }
8772
+ } catch (error) {
8773
+ if (!(error instanceof import_sdk_core11.NodeNotFoundError)) throw error;
8774
+ unserved ?? (unserved = error);
8775
+ }
8776
+ }
8777
+ if (targets.size === 0) throw unserved ?? new import_sdk_services5.ResourceNotRoutedError(requestedResource ?? "session grant");
8778
+ const succeeded = [];
8779
+ const accepted = [];
8780
+ const failed = [];
8781
+ let refusal;
8782
+ for (const { destination: target, spaces } of targets.values()) {
8783
+ signal?.throwIfAborted();
8784
+ try {
8785
+ await (0, import_sdk_services5.verifyResourceDestination)(target, globalThis.fetch.bind(globalThis));
8786
+ const timeout = AbortSignal.timeout(5e3);
8787
+ const result = await (0, import_sdk_core11.activateSessionWithHost)(
8788
+ target.url,
8789
+ delegationHeader,
8790
+ signal ? AbortSignal.any([signal, timeout]) : timeout
8791
+ );
8792
+ this.throwRegistryRefusal(result.status, result.error ?? "");
8793
+ if (result.success) {
8794
+ const skipped = result.skipped?.find((space) => spaces.some((expected) => this.spaceIdsEqual(space, expected)));
8795
+ if (skipped) {
8796
+ failed.push({ url: target.url, status: result.status, cause: new import_sdk_services5.ResourceNotRoutedError(skipped) });
8797
+ continue;
8798
+ }
8799
+ succeeded.push(target.url);
8800
+ accepted.push(result);
8801
+ } else {
8802
+ refusal = result;
8803
+ failed.push({ url: target.url, status: result.status, cause: result });
8804
+ }
8805
+ } catch (error) {
8806
+ if (signal?.aborted) throw error;
8807
+ failed.push({ url: target.url, cause: (0, import_sdk_services5.routingFailure)(error) ? error : new import_sdk_services5.NodeNotRespondingError([target.url], error) });
8808
+ }
8809
+ }
8810
+ if (failed.length > 0) {
8811
+ if (targets.size > 1) throw new import_sdk_services5.PartialNodeWriteError(succeeded, failed);
8812
+ if (refusal) return refusal;
8813
+ throw failed[0].cause;
8814
+ }
8815
+ const activated = new Set(accepted.flatMap((result) => result.activated ?? []));
8816
+ return {
8817
+ ...accepted[0],
8818
+ activated: [...activated],
8819
+ skipped: [...new Set(accepted.flatMap((result) => result.skipped ?? []))].filter((space) => !activated.has(space) && !routedSpaces.some((expected) => this.spaceIdsEqual(space, expected)))
8820
+ };
8821
+ }
8822
+ activateRoutedDelegation(delegation, signal) {
8823
+ const resources = this.operationsFromDelegation(delegation).map(
8824
+ (operation) => operation.resource?.startsWith("urn:tinycloud:encryption:") ? operation.resource.split("/")[0] : operation.spaceId ?? delegation.spaceId
8825
+ );
8826
+ return this.activateResourceSession(delegation.delegationHeader, resources, delegation.host, signal);
8827
+ }
8828
+ activateSignedSession(session, siwe, hostHint, signal) {
8829
+ const resources = this.parseRecapWithCaveats(siwe).map(
8830
+ (entry) => this.isEncryptionNetworkOperation(this.invocationServiceName(entry.service), entry.path, entry.space) ? entry.path.split("/")[0] : entry.space.startsWith("tinycloud:") ? entry.space : session.spaceId
8831
+ );
8832
+ return this.activateResourceSession(
8833
+ session.delegationHeader,
8834
+ resources.length > 0 ? resources : [session.spaceId],
8835
+ hostHint,
8836
+ signal
8837
+ );
8838
+ }
8577
8839
  /**
8578
8840
  * Install a portable runtime permission delegation into this SDK instance so
8579
8841
  * matching service calls and downstream `delegateTo()` calls can use it.
@@ -8593,16 +8855,12 @@ var _TinyCloudNode = class _TinyCloudNode {
8593
8855
  `Runtime delegation targets ${delegation.delegateDID} but this session key is ${session.verificationMethod}.`
8594
8856
  );
8595
8857
  }
8596
- const targetHost = delegation.host ?? this.config.host;
8597
8858
  let activateResult;
8598
8859
  try {
8599
- activateResult = await (0, import_sdk_core11.activateSessionWithHost)(
8600
- targetHost,
8601
- delegation.delegationHeader,
8602
- graph.signal
8603
- );
8860
+ activateResult = await this.activateRoutedDelegation(delegation, graph.signal);
8604
8861
  } catch (cause) {
8605
8862
  graph.assertActive();
8863
+ if ((0, import_sdk_services5.routingFailure)(cause)) throw cause;
8606
8864
  throw Object.assign(
8607
8865
  new Error("The TinyCloud host could not be reached to activate the delegation."),
8608
8866
  { code: "ACTIVATION_TRANSPORT_FAILED", cause }
@@ -8758,11 +9016,15 @@ var _TinyCloudNode = class _TinyCloudNode {
8758
9016
  ...prepared,
8759
9017
  signature
8760
9018
  });
8761
- const activateResult = await (0, import_sdk_core11.activateSessionWithHost)(
8762
- this.config.host,
8763
- delegatedSession.delegationHeader,
8764
- graph.signal
8765
- ).catch((error) => {
9019
+ const delegation = this.runtimeDelegationFromSession(
9020
+ delegatedSession,
9021
+ delegatedEntries,
9022
+ spaceId,
9023
+ captured,
9024
+ expiresAt,
9025
+ { siwe: prepared.siwe, signature }
9026
+ );
9027
+ const activateResult = await this.activateRoutedDelegation(delegation, graph.signal).catch((error) => {
8766
9028
  graph.assertActive();
8767
9029
  throw error;
8768
9030
  });
@@ -8778,14 +9040,6 @@ var _TinyCloudNode = class _TinyCloudNode {
8778
9040
  { cause: activateResult }
8779
9041
  );
8780
9042
  }
8781
- const delegation = this.runtimeDelegationFromSession(
8782
- delegatedSession,
8783
- delegatedEntries,
8784
- spaceId,
8785
- captured,
8786
- expiresAt,
8787
- { siwe: prepared.siwe, signature }
8788
- );
8789
9043
  graph.assertActive();
8790
9044
  assertCapturedKey?.();
8791
9045
  this.runtimePermissionGrants.push({
@@ -8930,7 +9184,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8930
9184
  if (!publicSpaceId) {
8931
9185
  throw new Error("Public space not enabled. Set enablePublicSpace: true in config.");
8932
9186
  }
8933
- await this.assertMayCreateSpace();
9187
+ await this.assertMayCreateSpace(publicSpaceId);
8934
9188
  await this.auth.hostPublicSpace(publicSpaceId);
8935
9189
  const kvActions = [import_sdk_core11.KV.PUT, import_sdk_core11.KV.GET, import_sdk_core11.KV.DEL, import_sdk_core11.KV.LIST, import_sdk_core11.KV.METADATA];
8936
9190
  const abilities = { kv: { "": kvActions } };
@@ -8953,10 +9207,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8953
9207
  ...prepared,
8954
9208
  signature
8955
9209
  });
8956
- const activateResult = await (0, import_sdk_core11.activateSessionWithHost)(
8957
- this.config.host,
8958
- delegationSession.delegationHeader
8959
- );
9210
+ const activateResult = await this.activateSignedSession(delegationSession, prepared.siwe);
8960
9211
  if (!activateResult.success) {
8961
9212
  throw Object.assign(
8962
9213
  new Error(`Failed to activate public space delegation: ${describeHostFailure2(activateResult)}`),
@@ -9506,10 +9757,7 @@ var _TinyCloudNode = class _TinyCloudNode {
9506
9757
  if (onPrepared) {
9507
9758
  await onPrepared(delegation);
9508
9759
  }
9509
- const activateResult = await (0, import_sdk_core11.activateSessionWithHost)(
9510
- this.config.host,
9511
- delegationHeader
9512
- );
9760
+ const activateResult = await this.activateRoutedDelegation(delegation);
9513
9761
  assertDelegationActivated(activateResult, delegation, requireActivation);
9514
9762
  return delegation;
9515
9763
  }
@@ -9543,10 +9791,7 @@ var _TinyCloudNode = class _TinyCloudNode {
9543
9791
  if (onPrepared) {
9544
9792
  await onPrepared(delegation);
9545
9793
  }
9546
- const activateResult = await (0, import_sdk_core11.activateSessionWithHost)(
9547
- targetHost,
9548
- delegationHeader
9549
- );
9794
+ const activateResult = await this.activateRoutedDelegation(delegation);
9550
9795
  assertDelegationActivated(activateResult, delegation, requireActivation);
9551
9796
  return delegation;
9552
9797
  }
@@ -10198,10 +10443,7 @@ var _TinyCloudNode = class _TinyCloudNode {
10198
10443
  ...prepared,
10199
10444
  signature
10200
10445
  });
10201
- const activateResult = await (0, import_sdk_core11.activateSessionWithHost)(
10202
- this.config.host,
10203
- delegationSession.delegationHeader
10204
- );
10446
+ const activateResult = await this.activateSignedSession(delegationSession, prepared.siwe);
10205
10447
  if (!activateResult.success) {
10206
10448
  throw Object.assign(
10207
10449
  new Error(`Failed to activate delegation: ${describeHostFailure2(activateResult)}`),
@@ -10246,10 +10488,7 @@ var _TinyCloudNode = class _TinyCloudNode {
10246
10488
  ...publicPrepared,
10247
10489
  signature: publicSignature
10248
10490
  });
10249
- const publicActivateResult = await (0, import_sdk_core11.activateSessionWithHost)(
10250
- this.config.host,
10251
- publicSession.delegationHeader
10252
- );
10491
+ const publicActivateResult = await this.activateSignedSession(publicSession, publicPrepared.siwe);
10253
10492
  if (publicActivateResult.success) {
10254
10493
  result.publicDelegation = {
10255
10494
  cid: publicSession.delegationCid,
@@ -10355,11 +10594,7 @@ var _TinyCloudNode = class _TinyCloudNode {
10355
10594
  ...prepared,
10356
10595
  signature
10357
10596
  });
10358
- const activateResult = await (0, import_sdk_core11.activateSessionWithHost)(
10359
- targetHost,
10360
- invokerSession.delegationHeader,
10361
- graph.signal
10362
- ).catch((error) => {
10597
+ const activateResult = await this.activateSignedSession(invokerSession, prepared.siwe, targetHost, graph.signal).catch((error) => {
10363
10598
  graph.assertActive();
10364
10599
  throw error;
10365
10600
  });
@@ -10486,10 +10721,7 @@ var _TinyCloudNode = class _TinyCloudNode {
10486
10721
  ...prepared,
10487
10722
  signature
10488
10723
  });
10489
- const activateResult = await (0, import_sdk_core11.activateSessionWithHost)(
10490
- targetHost,
10491
- subDelegationSession.delegationHeader
10492
- );
10724
+ const activateResult = await this.activateSignedSession(subDelegationSession, prepared.siwe, targetHost);
10493
10725
  if (!activateResult.success) {
10494
10726
  throw Object.assign(
10495
10727
  new Error(`Failed to activate sub-delegation: ${describeHostFailure2(activateResult)}`),
@@ -10569,7 +10801,10 @@ var import_sdk_core23 = require("@tinycloud/sdk-core");
10569
10801
  KVService,
10570
10802
  ManifestValidationError,
10571
10803
  MemorySessionStorage,
10804
+ NodeIdentityMismatchError,
10805
+ NodeNotRespondingError,
10572
10806
  NodeUserAuthorization,
10807
+ PartialNodeWriteError,
10573
10808
  PermissionNotInManifestError,
10574
10809
  PrefixedKVService,
10575
10810
  ProtocolMismatchError,