@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.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/core.ts
2
2
  import { TinyCloud as TinyCloud2 } from "@tinycloud/sdk-core";
3
+ import { NodeNotRespondingError as NodeNotRespondingError3, NodeIdentityMismatchError as NodeIdentityMismatchError2, PartialNodeWriteError as PartialNodeWriteError3 } from "@tinycloud/sdk-services";
3
4
 
4
5
  // src/replication/module-registry.ts
5
6
  var nodeLoaders;
@@ -259,11 +260,16 @@ import {
259
260
  principalDidEquals,
260
261
  resourceCapabilitiesToAbilitiesMap,
261
262
  resourceCapabilitiesToSpaceAbilitiesMap,
262
- resolveTinyCloudHosts,
263
- resolveFailClosedTinyCloudHosts,
264
- lookupOwnerRegistryVerdict,
263
+ resolveRegistryResource,
264
+ warnRegistryDefaultNode,
265
+ parseSpaceUri,
266
+ NodeNotFoundError,
267
+ RegistryUnavailableError,
268
+ discoverLocalTinyCloudNode,
269
+ exactHttpOrigin,
265
270
  assertSpaceCreationAllowed,
266
271
  createInMemoryRegistrySequenceStore,
272
+ createInMemoryRegistryCacheStore,
267
273
  registryOwnerSubjectV1,
268
274
  EXPIRY,
269
275
  assertExpiryWithinTechnicalLimit,
@@ -286,7 +292,13 @@ import {
286
292
  ENCRYPTION
287
293
  } from "@tinycloud/sdk-core";
288
294
  import { parseEncryptionResource } from "@tinycloud/sdk-services/encryption/resource";
289
- import { parseNetworkId } from "@tinycloud/sdk-services";
295
+ import {
296
+ parseNetworkId,
297
+ verifyResourceDestination,
298
+ NodeNotRespondingError,
299
+ PartialNodeWriteError,
300
+ routingFailure
301
+ } from "@tinycloud/sdk-services";
290
302
 
291
303
  // src/authorization/appKeyHostBinding.ts
292
304
  var DOMAIN_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -541,11 +553,12 @@ var NodeUserAuthorization = class {
541
553
  this.tinycloudHosts = config.tinycloudHosts;
542
554
  this.tinycloudRegistryUrl = config.tinycloudRegistryUrl;
543
555
  this.tinycloudFallbackHosts = config.tinycloudFallbackHosts;
544
- this.registryPolicy = config.registryPolicy;
545
556
  this.newUserHost = config.newUserHost;
546
557
  this.registryChainId = config.registryChainId;
547
558
  this.registrySequenceStore = config.registrySequenceStore ?? createInMemoryRegistrySequenceStore();
559
+ this.registryCacheStore = config.registryCacheStore ?? createInMemoryRegistryCacheStore();
548
560
  this.onNodeNotFound = config.onNodeNotFound;
561
+ this.onRegistryFallback = config.onRegistryFallback;
549
562
  this.autoDiscoverLocalNode = config.autoDiscoverLocalNode;
550
563
  this.localNodeUrl = config.localNodeUrl;
551
564
  this.localLinkName = config.localLinkName;
@@ -613,13 +626,8 @@ var NodeUserAuthorization = class {
613
626
  * undefined for missing SIWEs and the SDK then treats the session as
614
627
  * expired-at-epoch-zero.
615
628
  *
616
- * @param hosts - The TinyCloud hosts this session was created against,
617
- * as persisted in {@link PersistedSessionData.tinycloudHosts}. When
618
- * present (and non-empty) they are adopted directly so the restored
619
- * session resolves to the same node as the original sign-in without
620
- * re-running registry/fallback resolution. When absent (old session)
621
- * hosts are resolved lazily on the first host-needing call via
622
- * {@link ensureTinyCloudHosts}.
629
+ * @param hosts - Persisted host hints for genuine no-entry onboarding.
630
+ * Current signed registry membership always takes precedence.
623
631
  */
624
632
  setRestoredTinyCloudSession(session, hosts) {
625
633
  const address = canonicalizeAddress(session.address);
@@ -681,89 +689,84 @@ var NodeUserAuthorization = class {
681
689
  signature: session.signature
682
690
  };
683
691
  }
684
- /**
685
- * Ensure `tinycloudHosts` are resolved before a host-needing call.
686
- *
687
- * Fresh sign-in resolves hosts up front; a restored session may not have
688
- * (old persisted sessions predate {@link PersistedSessionData.tinycloudHosts}).
689
- * This guard makes a restored session resolve the node exactly like a fresh
690
- * sign-in — persisted hosts are preferred (already set by
691
- * {@link setRestoredTinyCloudSession}), otherwise the registry/fallback
692
- * resolution runs lazily here. Idempotent: {@link resolveTinyCloudHostsForSignIn}
693
- * early-returns when hosts are already set.
694
- *
695
- * Throws if hosts are unset and the restored session has no address/chainId
696
- * to resolve from — a real failure that must surface, not be masked.
697
- */
692
+ /** Refresh the primary space's registry destinations, including after restore. */
698
693
  async ensureTinyCloudHosts() {
699
- if (this.tinycloudHosts && this.tinycloudHosts.length > 0) {
700
- return;
701
- }
702
694
  if (this._address === void 0 || this._chainId === void 0) {
703
- throw new Error(
704
- "Cannot resolve TinyCloud hosts: no address/chainId available. Sign in or restore a session first."
705
- );
695
+ throw new Error("Cannot resolve TinyCloud hosts: sign in or restore a session first.");
696
+ }
697
+ await this.resolveTinyCloudHostsForSignIn(this._address, this._chainId, this._tinyCloudSession?.spaceId);
698
+ }
699
+ async resolveTinyCloudHostsForSignIn(address, chainId, restoredSpaceId) {
700
+ const spaceId = restoredSpaceId ?? this.resolveSignInCapabilities(address, chainId).spaceId;
701
+ const destinations = await this.resolveResourceDestinations(spaceId, "read");
702
+ for (const destination of destinations) await verifyResourceDestination(destination, globalThis.fetch.bind(globalThis));
703
+ this.tinycloudHosts = destinations.map(({ url }) => url);
704
+ }
705
+ /** Current full-resource registry routing; saved hosts are only no-entry hints. */
706
+ async resolveResourceDestinations(resource, access, noEntryHosts = this.tinycloudHosts ?? []) {
707
+ const owner = resource.startsWith("urn:tinycloud:encryption:") ? parseNetworkId(resource.split("/")[0]).ownerDid : parseSpaceUri(resource)?.owner ?? (() => {
708
+ const key = /^tinycloud:key:(z[^:]+):[^/]+$/.exec(resource);
709
+ return key ? `did:key:${key[1]}` : void 0;
710
+ })();
711
+ if (!owner) throw new Error(`Cannot determine registry owner for ${resource}`);
712
+ const subject = owner.startsWith("did:pkh:") ? registryOwnerSubjectV1(owner.slice(owner.lastIndexOf(":") + 1), this.registryChainId) : owner;
713
+ for (let attempt = 1; ; attempt++) {
714
+ try {
715
+ const resolved = await resolveRegistryResource(subject, resource, access, {
716
+ registryUrl: this.tinycloudRegistryUrl,
717
+ registryChainId: this.registryChainId,
718
+ sequenceStore: this.registrySequenceStore,
719
+ cacheStore: this.registryCacheStore,
720
+ onRegistryFallback: this.onRegistryFallback
721
+ });
722
+ this.registryVerdict = resolved.verdict;
723
+ if (resolved.verdict.status === "unregistered") {
724
+ if (noEntryHosts.length === 0 && (this.autoDiscoverLocalNode ?? true)) {
725
+ const local = await discoverLocalTinyCloudNode({
726
+ localNodeUrl: this.localNodeUrl,
727
+ localLinkName: this.localLinkName,
728
+ expectedNodeDid: this.expectedNodeDid,
729
+ identityStore: this.localNodeIdentityStore
730
+ });
731
+ if (local) {
732
+ warnRegistryDefaultNode(resolved.verdict, this.registryCacheStore, this.onRegistryFallback);
733
+ return [{ url: local.url }];
734
+ }
735
+ }
736
+ const host = noEntryHosts[0] ?? this.newUserHost ?? (this.tinycloudFallbackHosts === void 0 ? "https://node.tinycloud.xyz" : this.tinycloudFallbackHosts?.[0]);
737
+ if (!host || this.newUserHost === null && noEntryHosts.length === 0) throw new NodeNotFoundError(subject, resolved.verdict);
738
+ const url = exactHttpOrigin(host, "new-user host");
739
+ warnRegistryDefaultNode(resolved.verdict, this.registryCacheStore, this.onRegistryFallback);
740
+ return [{ url }];
741
+ }
742
+ const destinations = resolved.hosts.flatMap((host) => host.urls.map((url) => ({ url, nodeDid: host.nodeDid })));
743
+ return destinations;
744
+ } catch (error) {
745
+ if (!(error instanceof NodeNotFoundError) && !(error instanceof RegistryUnavailableError)) throw error;
746
+ if (!this.onNodeNotFound) throw error;
747
+ const decision = await this.onNodeNotFound({ error, attempt });
748
+ if (decision.action === "retry") continue;
749
+ if (decision.action === "use-host" && error.verdict?.status === "unregistered") {
750
+ const url = exactHttpOrigin(decision.host, "new-user host");
751
+ warnRegistryDefaultNode(error.verdict, this.registryCacheStore, this.onRegistryFallback);
752
+ return [{ url }];
753
+ }
754
+ throw error;
755
+ }
706
756
  }
707
- await this.resolveTinyCloudHostsForSignIn(this._address, this._chainId);
708
757
  }
709
- async resolveTinyCloudHostsForSignIn(address, chainId) {
710
- if (this.tinycloudHosts && this.tinycloudHosts.length > 0) {
711
- return;
712
- }
713
- if (this.registryPolicy === "fail-closed") {
714
- const resolved2 = await resolveFailClosedTinyCloudHosts(address, {
715
- registryUrl: this.tinycloudRegistryUrl,
716
- registryChainId: this.registryChainId,
717
- newUserHost: this.newUserHost,
718
- sequenceStore: this.registrySequenceStore,
719
- onNodeNotFound: this.onNodeNotFound,
720
- autoDiscoverLocalNode: this.autoDiscoverLocalNode,
721
- localNodeUrl: this.localNodeUrl,
722
- localLinkName: this.localLinkName,
723
- expectedNodeDid: this.expectedNodeDid,
724
- localNodeIdentityStore: this.localNodeIdentityStore
725
- });
726
- this.tinycloudHosts = resolved2.hosts;
727
- this.registryVerdict = resolved2.verdict;
728
- return;
729
- }
730
- const subject = pkhDid(address, chainId);
731
- const resolved = await resolveTinyCloudHosts(subject, {
732
- registryUrl: this.tinycloudRegistryUrl,
733
- fallbackHosts: this.tinycloudFallbackHosts,
734
- autoDiscoverLocalNode: this.autoDiscoverLocalNode,
735
- localNodeUrl: this.localNodeUrl,
736
- localLinkName: this.localLinkName,
737
- expectedNodeDid: this.expectedNodeDid,
738
- localNodeIdentityStore: this.localNodeIdentityStore
739
- });
740
- this.tinycloudHosts = resolved.hosts;
758
+ get registryRecordPresent() {
759
+ return this.registryVerdict?.status === "registered";
741
760
  }
742
- /**
743
- * Space-creation guard (TC-1443). Under `registryPolicy: "fail-closed"`,
744
- * throws unless the owner's registry verdict allows a space on the primary
745
- * host: `NodeRegistryEntryExistsError` when the owner's record does not
746
- * list it, `RegistryUnavailableError` when the registry gives no
747
- * verdict. Reuses this session's sign-in verdict unless it was
748
- * `unavailable`, else looks it up now (explicit-host apps). Under the
749
- * default policy it returns at once and performs no lookup.
750
- */
751
- async assertMayCreateSpace() {
752
- if (this.registryPolicy !== "fail-closed") return;
753
- if (this._address === void 0) {
754
- throw new Error("Must be signed in to create a space");
755
- }
756
- await this.ensureTinyCloudHosts();
757
- const subject = registryOwnerSubjectV1(this._address, this.registryChainId);
758
- let verdict = this.registryVerdict;
759
- if (verdict?.subject !== subject || verdict.status === "unavailable") {
760
- verdict = await lookupOwnerRegistryVerdict(subject, {
761
- registryUrl: this.tinycloudRegistryUrl,
762
- sequenceStore: this.registrySequenceStore
763
- });
764
- this.registryVerdict = verdict;
761
+ /** Refresh membership before hosting; a saved no-entry verdict never authorizes a later creation. */
762
+ async assertMayCreateSpace(spaceId = this._tinyCloudSession?.spaceId, host = this.primaryTinyCloudHost) {
763
+ if (!spaceId) throw new Error("Must be signed in to create a space");
764
+ const destinations = await this.resolveResourceDestinations(spaceId, "host");
765
+ if (!destinations.some((destination) => destination.url === host)) {
766
+ const subject = parseSpaceUri(spaceId)?.owner;
767
+ if (!subject || !this.registryVerdict) throw new Error("Cannot resolve space owner");
768
+ assertSpaceCreationAllowed(this.registryVerdict, host, spaceId);
765
769
  }
766
- assertSpaceCreationAllowed(verdict, this.primaryTinyCloudHost);
767
770
  }
768
771
  requireTinyCloudHosts() {
769
772
  if (!this.tinycloudHosts || this.tinycloudHosts.length === 0) {
@@ -1193,23 +1196,60 @@ var NodeUserAuthorization = class {
1193
1196
  * Create the space on the TinyCloud server (host delegation).
1194
1197
  * This registers the user as the owner of the space.
1195
1198
  */
1196
- async hostSpace(targetSpaceId, purpose, signal) {
1197
- if (!this._tinyCloudSession || !this._address || !this._chainId) {
1199
+ async hostSpace(targetSpaceId, purpose, signal, assertCurrent, siweParameters) {
1200
+ const context = typeof targetSpaceId === "object" ? targetSpaceId : void 0;
1201
+ const address = context?.address ?? this._address;
1202
+ const chainId = context?.chainId ?? this._chainId;
1203
+ if (!address || chainId === void 0 || !this._tinyCloudSession) {
1198
1204
  throw new Error("Must be signed in to host space");
1199
1205
  }
1206
+ const checkCurrent = () => {
1207
+ signal?.throwIfAborted();
1208
+ assertCurrent?.();
1209
+ };
1210
+ checkCurrent();
1200
1211
  await this.ensureTinyCloudHosts();
1201
- const host = this.primaryTinyCloudHost;
1202
- const spaceId = targetSpaceId ?? this._tinyCloudSession.spaceId;
1212
+ checkCurrent();
1213
+ const host = context?.host ?? this.primaryTinyCloudHost;
1214
+ const spaceId = context?.spaceId ?? (typeof targetSpaceId === "string" ? targetSpaceId : this._tinyCloudSession.spaceId);
1215
+ const destinations = await this.resolveResourceDestinations(spaceId, "host");
1216
+ if (this.registryVerdict?.status === "registered") {
1217
+ const results = [];
1218
+ for (const destination of destinations) {
1219
+ checkCurrent();
1220
+ try {
1221
+ await verifyResourceDestination(destination, globalThis.fetch.bind(globalThis));
1222
+ const result = await activateSessionWithHost(destination.url, this._tinyCloudSession.delegationHeader, signal);
1223
+ results.push(result.skipped?.includes(spaceId) ? { ...result, success: false, status: 403, error: "The session does not activate the requested space" } : result);
1224
+ } catch (error) {
1225
+ if (routingFailure(error) || signal?.aborted) throw error;
1226
+ throw new NodeNotRespondingError([destination.url], error);
1227
+ }
1228
+ }
1229
+ const failed = results.flatMap((result, index) => result.success ? [] : [{ url: destinations[index].url, status: result.status }]);
1230
+ if (failed.length) {
1231
+ if (destinations.length === 1) return results[0];
1232
+ throw new PartialNodeWriteError(destinations.filter((_, index) => results[index].success).map(({ url }) => url), failed);
1233
+ }
1234
+ return {
1235
+ ...results[0],
1236
+ activated: [...new Set(results.flatMap((result) => result.activated ?? []))],
1237
+ skipped: [...new Set(results.flatMap((result) => result.skipped ?? []))]
1238
+ };
1239
+ }
1203
1240
  const peerId = await fetchPeerId(host, spaceId, signal);
1204
- const siwe = this.wasm.generateHostSIWEMessage({
1205
- address: this._address,
1206
- chainId: this._chainId,
1207
- domain: this.domain,
1208
- issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
1209
- spaceId,
1210
- peerId
1211
- });
1212
- if (signal?.aborted) throw signal.reason;
1241
+ checkCurrent();
1242
+ const siwe = this.wasm.generateHostSIWEMessage(
1243
+ context && siweParameters ? siweParameters(peerId) : {
1244
+ address,
1245
+ chainId,
1246
+ domain: this.domain,
1247
+ issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
1248
+ spaceId,
1249
+ peerId
1250
+ }
1251
+ );
1252
+ checkCurrent();
1213
1253
  const signing = this.signMessage(siwe, purpose);
1214
1254
  let signature;
1215
1255
  if (!signal) {
@@ -1228,8 +1268,9 @@ var NodeUserAuthorization = class {
1228
1268
  signal.removeEventListener("abort", onAbort);
1229
1269
  }
1230
1270
  }
1231
- if (signal?.aborted) throw signal.reason;
1271
+ checkCurrent();
1232
1272
  const headers = this.wasm.siweToDelegationHeaders({ siwe, signature });
1273
+ checkCurrent();
1233
1274
  return submitHostDelegation(host, headers, signal);
1234
1275
  }
1235
1276
  /**
@@ -1266,13 +1307,45 @@ var NodeUserAuthorization = class {
1266
1307
  if (!this._tinyCloudSession) {
1267
1308
  throw new Error("Must be signed in to ensure space exists");
1268
1309
  }
1269
- await this.ensureTinyCloudHosts();
1270
- const host = this.primaryTinyCloudHost;
1271
1310
  const primarySpaceId = this._tinyCloudSession.spaceId;
1272
- const result = await activateSessionWithHost(
1273
- host,
1274
- this._tinyCloudSession.delegationHeader
1275
- );
1311
+ const primary = await this.resolveResourceDestinations(primarySpaceId, "read");
1312
+ this.tinycloudHosts = primary.map(({ url }) => url);
1313
+ const targets = new Map(primary.map((destination) => [destination.url, destination]));
1314
+ if (this.registryVerdict?.status === "registered" && this.registryVerdict.record) {
1315
+ const record = this.registryVerdict.record;
1316
+ const resources = Object.keys(extractRecapAttenuations(this._tinyCloudSession.siwe ?? ""));
1317
+ for (const space of record.spaces) {
1318
+ const registeredSpace = parseSpaceUri(space.space);
1319
+ if (!resources.some((resource) => {
1320
+ const fullSpace = resource.split("/")[0];
1321
+ const requestedSpace = parseSpaceUri(fullSpace);
1322
+ return fullSpace === space.space || registeredSpace !== null && requestedSpace !== null && registeredSpace.owner === requestedSpace.owner && registeredSpace.name === requestedSpace.name;
1323
+ })) continue;
1324
+ for (const membership of space.nodes) {
1325
+ const node = record.nodes.find((candidate) => candidate.did === membership.did);
1326
+ targets.set(node.url, { url: node.url, nodeDid: node.did });
1327
+ }
1328
+ }
1329
+ if (record.encryptionNode && resources.some((resource) => resource.startsWith("urn:tinycloud:encryption:"))) {
1330
+ const node = record.nodes.find((candidate) => candidate.did === record.encryptionNode);
1331
+ targets.set(node.url, { url: node.url, nodeDid: node.did });
1332
+ }
1333
+ }
1334
+ let result;
1335
+ for (const destination of targets.values()) {
1336
+ await verifyResourceDestination(destination, globalThis.fetch.bind(globalThis));
1337
+ let activated;
1338
+ try {
1339
+ activated = await activateSessionWithHost(destination.url, this._tinyCloudSession.delegationHeader);
1340
+ } catch (error) {
1341
+ throw new NodeNotRespondingError([destination.url], error);
1342
+ }
1343
+ if (!activated.success && activated.status !== 404) {
1344
+ throw Object.assign(new Error(`Failed to activate session: ${describeHostFailure(activated)}`), { cause: activated });
1345
+ }
1346
+ if (destination.url === primary[0].url) result = activated;
1347
+ }
1348
+ if (!result) throw new Error("Primary space activation did not return a result");
1276
1349
  this.recordActivationSkippedSpaces(result, primarySpaceId);
1277
1350
  if (!result.success && result.status !== 404) {
1278
1351
  throw Object.assign(
@@ -1291,43 +1364,36 @@ var NodeUserAuthorization = class {
1291
1364
  *
1292
1365
  * @throws Error if the creation handler fails or times out, or hosting fails
1293
1366
  */
1294
- async ensurePrimarySpaceHosted(hosting) {
1295
- if (hosting !== "not-hosted") {
1296
- return;
1297
- }
1367
+ async ensurePrimarySpaceHosted(hosting, signal) {
1298
1368
  const session = this._tinyCloudSession;
1299
- if (!session) {
1300
- throw new Error("Must be signed in to host the primary space");
1301
- }
1369
+ if (hosting !== "not-hosted") return;
1370
+ if (!session) throw new Error("Must be signed in to host the primary space");
1302
1371
  const handler = this.spaceCreationHandler ?? (this.autoCreateSpace ? new AutoApproveSpaceCreationHandler() : void 0);
1303
- if (!handler) {
1304
- return;
1305
- }
1306
- await this.ensureTinyCloudHosts();
1307
- const host = this.primaryTinyCloudHost;
1308
- const primarySpaceId = session.spaceId;
1309
- const creationContext = {
1310
- spaceId: primarySpaceId,
1311
- address: this._address,
1312
- chainId: this._chainId,
1313
- host
1314
- };
1372
+ if (!handler) return;
1315
1373
  const { createPrimarySpace } = await import("./space-hosting.js");
1316
1374
  await createPrimarySpace({
1317
1375
  handler,
1318
- context: creationContext,
1319
- guard: () => this.guardSpaceCreation(handler, creationContext),
1320
- host: () => this.hostSpace(primarySpaceId),
1376
+ session,
1377
+ currentSession: () => this._tinyCloudSession,
1378
+ signerAddress: () => this.signer.getAddress(),
1379
+ resolveHost: async () => {
1380
+ await this.ensureTinyCloudHosts();
1381
+ return this.primaryTinyCloudHost;
1382
+ },
1383
+ domain: this.domain,
1384
+ guard: (context) => this.guardSpaceCreation(handler, context),
1385
+ host: (context, assertCurrent, siweParameters) => this.hostSpace(context, void 0, signal, assertCurrent, siweParameters),
1321
1386
  onHosted: () => {
1322
1387
  this._spaceHosting = "hosted";
1323
1388
  },
1324
- activate: () => activateSessionWithHost(host, session.delegationHeader)
1389
+ activate: (context) => activateSessionWithHost(context.host, session.delegationHeader),
1390
+ signal
1325
1391
  });
1326
1392
  }
1327
1393
  /** {@link assertMayCreateSpace}, reporting a refusal to the creation handler before rethrowing it. */
1328
1394
  async guardSpaceCreation(handler, context) {
1329
1395
  try {
1330
- await this.assertMayCreateSpace();
1396
+ await this.assertMayCreateSpace(context.spaceId, context.host);
1331
1397
  } catch (error) {
1332
1398
  const failure = error instanceof Error ? error : new Error(String(error));
1333
1399
  handler.onSpaceCreationFailed?.(context, failure);
@@ -2481,6 +2547,7 @@ import {
2481
2547
  decodeBase64Url,
2482
2548
  principalDidEquals as principalDidEquals2
2483
2549
  } from "@tinycloud/sdk-core";
2550
+ import { routingFailure as routingFailure2 } from "@tinycloud/sdk-services";
2484
2551
  var RuntimeDelegationRejectedError = class extends Error {
2485
2552
  constructor(reason, message) {
2486
2553
  super(message);
@@ -2507,7 +2574,8 @@ var SERVICE_BY_SHORT_NAME = {
2507
2574
  kv: "tinycloud.kv",
2508
2575
  sql: "tinycloud.sql",
2509
2576
  duckdb: "tinycloud.duckdb",
2510
- hooks: "tinycloud.hooks"
2577
+ hooks: "tinycloud.hooks",
2578
+ capabilities: "tinycloud.capabilities"
2511
2579
  };
2512
2580
  function authorizationWithoutBearer(authorization) {
2513
2581
  return authorization.replace(/^Bearer /i, "");
@@ -2638,8 +2706,8 @@ function signedAuthorityFromCompactUcan(authorization) {
2638
2706
  ));
2639
2707
  continue;
2640
2708
  }
2641
- const match = /^(?:tinycloud:\/\/([^/]+)|(tinycloud:[^/]+))\/(kv|sql|duckdb|hooks)(?:\/|$)(.*)$/.exec(resource);
2642
- if (!match) {
2709
+ const match = /^(?:tinycloud:\/\/([^/]+)|(tinycloud:[^/]+))\/([^/]+)(?:\/|$)(.*)$/.exec(resource);
2710
+ if (!match || !Object.prototype.hasOwnProperty.call(SERVICE_BY_SHORT_NAME, match[3])) {
2643
2711
  throw new Error(
2644
2712
  `Validated runtime delegation has an unsupported signed resource '${resource}'.`
2645
2713
  );
@@ -2784,13 +2852,8 @@ function resourcesMatch(actual, expected) {
2784
2852
  return JSON.stringify(actual.map(comparable)) === JSON.stringify(expected.map(comparable));
2785
2853
  }
2786
2854
  async function activateValidatedRuntimeDelegation(node, delegation, options) {
2787
- const host = normalizedHost(options.host);
2788
- if (delegation.host !== void 0 && normalizedHost(delegation.host) !== host) {
2789
- reject(
2790
- "host_mismatch",
2791
- `Runtime delegation host '${delegation.host}' does not match expected host '${options.host}'.`
2792
- );
2793
- }
2855
+ let host = normalizedHost(options.host);
2856
+ const differentHost = delegation.host !== void 0 && normalizedHost(delegation.host) !== host;
2794
2857
  if (!(delegation.expiry instanceof Date) || Number.isNaN(delegation.expiry.getTime())) {
2795
2858
  reject("authority_invalid", "Runtime delegation has an invalid expiry.");
2796
2859
  }
@@ -2834,6 +2897,13 @@ async function activateValidatedRuntimeDelegation(node, delegation, options) {
2834
2897
  if (options.authorize !== void 0 && !options.authorize(signed.permissions)) {
2835
2898
  throw new Error("Runtime delegation is not authorized for this runtime.");
2836
2899
  }
2900
+ if (differentHost) {
2901
+ const registeredHost = await node.resolveRegisteredDelegationHost?.(signed.permissions, host);
2902
+ if (registeredHost === void 0) {
2903
+ reject("host_mismatch", `Runtime delegation host '${delegation.host}' does not match expected host '${options.host}'.`);
2904
+ }
2905
+ host = normalizedHost(registeredHost);
2906
+ }
2837
2907
  const previouslyInstalled = node.getRuntimePermissionDelegations().find((candidate) => candidate.cid === cid);
2838
2908
  if (previouslyInstalled !== void 0 && previouslyInstalled.delegationHeader.Authorization !== authorization) {
2839
2909
  reject(
@@ -2877,6 +2947,7 @@ async function activateValidatedRuntimeDelegation(node, delegation, options) {
2877
2947
  await node.useRuntimeDelegation(installedCandidate);
2878
2948
  }
2879
2949
  } catch (error) {
2950
+ if (routingFailure2(error)) throw error;
2880
2951
  reject(
2881
2952
  activationRejectionReason(error),
2882
2953
  error instanceof Error ? error.message : String(error)
@@ -2895,6 +2966,7 @@ async function activateValidatedRuntimeDelegation(node, delegation, options) {
2895
2966
  "Runtime delegation activation installed authority that differs from the validated authority."
2896
2967
  );
2897
2968
  }
2969
+ node.refreshRegistryForCapabilities?.(signed.permissions);
2898
2970
  return {
2899
2971
  cid,
2900
2972
  delegation: installed,
@@ -2958,6 +3030,15 @@ function deserializeDelegation(data) {
2958
3030
  }
2959
3031
 
2960
3032
  // src/TinyCloudNode.ts
3033
+ import {
3034
+ createResourceRoutingFetch,
3035
+ verifyResourceDestination as verifyResourceDestination2,
3036
+ NodeNotRespondingError as NodeNotRespondingError2,
3037
+ NodeIdentityMismatchError,
3038
+ PartialNodeWriteError as PartialNodeWriteError2,
3039
+ ResourceNotRoutedError,
3040
+ routingFailure as routingFailure3
3041
+ } from "@tinycloud/sdk-services";
2961
3042
  import { generateNonce, SiweMessage as SiweMessage3 } from "siwe";
2962
3043
  import { didKeyFromEd25519PublicKey } from "@tinycloud/share-envelope";
2963
3044
  import {
@@ -3017,9 +3098,15 @@ import {
3017
3098
  verifyDidKeyEd25519Signature,
3018
3099
  canonicalizeAddress as canonicalizeAddress2,
3019
3100
  pkhDid as pkhDid2,
3020
- resolveTinyCloudHosts as resolveTinyCloudHosts2,
3021
- resolveFailClosedTinyCloudHosts as resolveFailClosedTinyCloudHosts2,
3101
+ parseSpaceUri as parseSpaceUri2,
3102
+ resolveRegistryResource as resolveRegistryResource2,
3103
+ warnRegistryDefaultNode as warnRegistryDefaultNode2,
3104
+ NodeNotFoundError as NodeNotFoundError2,
3105
+ exactHttpOrigin as exactHttpOrigin2,
3022
3106
  createInMemoryRegistrySequenceStore as createInMemoryRegistrySequenceStore2,
3107
+ createInMemoryRegistryCacheStore as createInMemoryRegistryCacheStore2,
3108
+ refreshOwnerRegistry,
3109
+ registryOwnerKeyV2,
3023
3110
  publishLocationRecord,
3024
3111
  principalDidEquals as principalDidEquals3,
3025
3112
  parseNetworkId as parseNetworkId2,
@@ -4037,6 +4124,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4037
4124
  * ```
4038
4125
  */
4039
4126
  constructor(config = {}) {
4127
+ this.registryRefreshOwners = /* @__PURE__ */ new Set();
4040
4128
  this.signer = null;
4041
4129
  this.auth = null;
4042
4130
  this.consumedOpenKeyPendings = /* @__PURE__ */ new WeakSet();
@@ -4200,8 +4288,10 @@ var _TinyCloudNode = class _TinyCloudNode {
4200
4288
  this.config = {
4201
4289
  ...config,
4202
4290
  host: config.host ?? DEFAULT_HOST,
4291
+ registryPolicy: "fail-closed",
4203
4292
  // One store for every authorization this client creates.
4204
- registrySequenceStore: config.registrySequenceStore ?? createInMemoryRegistrySequenceStore2()
4293
+ registrySequenceStore: config.registrySequenceStore ?? createInMemoryRegistrySequenceStore2(),
4294
+ registryCacheStore: config.registryCacheStore ?? createInMemoryRegistryCacheStore2()
4205
4295
  };
4206
4296
  if (this.config.replication?.enabled) {
4207
4297
  const runtime = getNodeReplicationLoaders().runtime().then(({ ReplicationRuntime }) => {
@@ -4448,6 +4538,18 @@ var _TinyCloudNode = class _TinyCloudNode {
4448
4538
  return "unknown";
4449
4539
  }
4450
4540
  }
4541
+ /** Run the existing primary-space creation flow for the restored session. */
4542
+ async ensurePrimarySpaceHosted(hosting) {
4543
+ const graph = this._serviceGraph;
4544
+ const epoch = this.sessionEpoch;
4545
+ graph.assertActive();
4546
+ this.assertSessionEpoch(epoch, "Space hosting");
4547
+ const auth = this.auth;
4548
+ if (!auth || !this.session) return;
4549
+ await auth.ensurePrimarySpaceHosted(hosting, graph.signal);
4550
+ graph.assertActive();
4551
+ this.assertSessionEpoch(epoch, "Space hosting");
4552
+ }
4451
4553
  /**
4452
4554
  * The node's last answer for the primary space: recorded at sign-in from the
4453
4555
  * activation, or by {@link checkSpaceHosting}. `unknown` after a restore until
@@ -4493,7 +4595,11 @@ var _TinyCloudNode = class _TinyCloudNode {
4493
4595
  // A session saved but never registered on this node is refused: it registers once.
4494
4596
  activate: (signal) => {
4495
4597
  assertCurrent();
4496
- return activateSessionWithHost2(host, session.delegationHeader, AbortSignal.any([signal, graph.signal]));
4598
+ const activationSignal = AbortSignal.any([signal, graph.signal]);
4599
+ if (primary && primary.delegationCid === session.delegationCid) {
4600
+ return this.activateSignedSession(primary, primary.siwe, host, activationSignal);
4601
+ }
4602
+ return this.activateResourceSession(session.delegationHeader, [spaceId], host, activationSignal);
4497
4603
  }
4498
4604
  });
4499
4605
  assertCurrent();
@@ -4629,14 +4735,38 @@ var _TinyCloudNode = class _TinyCloudNode {
4629
4735
  get requests() {
4630
4736
  return this._requestLog === void 0 ? void 0 : readOnlyRequestLog(this._requestLog);
4631
4737
  }
4632
- createServiceGraphLifetime() {
4738
+ throwRegistryRefusal(status, text) {
4739
+ if (status !== 403 && status !== 503 || !text.startsWith("registry_unavailable:") && !text.startsWith("registry_hosting_denied:")) return;
4740
+ const code = status === 503 ? "registry_unavailable" : "node_registry_entry_exists";
4741
+ 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 } });
4742
+ }
4743
+ createServiceGraphLifetime(session) {
4744
+ const routedFetch = createResourceRoutingFetch(
4745
+ async (url, init) => {
4746
+ try {
4747
+ const response = await globalThis.fetch(url, init);
4748
+ if (response.status === 403 || response.status === 503) {
4749
+ this.throwRegistryRefusal(response.status, await response.clone().text());
4750
+ }
4751
+ return response;
4752
+ } catch (error) {
4753
+ if (routingFailure3(error)) throw error;
4754
+ if (new URL(url).pathname.startsWith("/encryption/") && !init?.signal?.aborted) {
4755
+ throw new NodeNotRespondingError2([new URL(url).origin], error);
4756
+ }
4757
+ throw error;
4758
+ }
4759
+ },
4760
+ {
4761
+ resolveResource: (resource, access) => this.routingDestinations(resource, access),
4762
+ bindAudience: (authorization, nodeDid) => this.bindRoutedAudience(authorization, nodeDid, session),
4763
+ verifyDestination: (target) => verifyResourceDestination2(target, globalThis.fetch.bind(globalThis))
4764
+ }
4765
+ );
4633
4766
  return new ServiceGraphLifetime(
4634
4767
  this.invokeWithRuntimePermissions,
4635
4768
  this.invokeAnyWithRuntimePermissions,
4636
- // Resolve fetch at request time. This keeps the graph-owned lifetime
4637
- // signal while preserving the SDK's existing injectable global fetch
4638
- // behavior (including adapters that install it after construction).
4639
- (url, init) => globalThis.fetch(url, init),
4769
+ routedFetch,
4640
4770
  (context) => {
4641
4771
  this.storageFull.observe(context);
4642
4772
  if (this._requestLog) observeReplicaReads(context, this._requestLog);
@@ -4644,6 +4774,75 @@ var _TinyCloudNode = class _TinyCloudNode {
4644
4774
  this._requestLog
4645
4775
  );
4646
4776
  }
4777
+ async routingDestinations(resource, access, noEntryHosts) {
4778
+ const session = this.currentTinyCloudSession();
4779
+ if (session) this.refreshRegistryOwner(pkhDid2(session.address, session.chainId));
4780
+ if (this.auth) return this.auth.resolveResourceDestinations(resource, access, noEntryHosts);
4781
+ const owner = resource.startsWith("urn:tinycloud:encryption:") ? parseNetworkId2(resource.split("/")[0]).ownerDid : parseSpaceUri2(resource)?.owner ?? (() => {
4782
+ const key = /^tinycloud:key:(z[^:]+):[^/]+$/.exec(resource);
4783
+ return key ? `did:key:${key[1]}` : void 0;
4784
+ })();
4785
+ if (!owner) throw new Error(`Cannot determine registry owner for ${resource}`);
4786
+ const subject = owner.startsWith("did:pkh:") ? pkhDid2(owner.slice(owner.lastIndexOf(":") + 1), this.config.registryChainId ?? 1) : owner;
4787
+ const resolved = await resolveRegistryResource2(subject, resource, access, {
4788
+ registryUrl: this.config.tinycloudRegistryUrl,
4789
+ registryChainId: this.config.registryChainId,
4790
+ sequenceStore: this.config.registrySequenceStore,
4791
+ cacheStore: this.config.registryCacheStore,
4792
+ onRegistryFallback: this.config.onRegistryFallback
4793
+ });
4794
+ if (resolved.verdict.status === "unregistered") {
4795
+ const hint = noEntryHosts === void 0 ? this.explicitHost ?? this.config.host : noEntryHosts[0];
4796
+ const host = hint ?? this.config.newUserHost ?? (this.config.tinycloudFallbackHosts === void 0 ? DEFAULT_HOST : this.config.tinycloudFallbackHosts?.[0]);
4797
+ if (!host || !hint && this.config.newUserHost === null) throw new NodeNotFoundError2(subject, resolved.verdict);
4798
+ const url = exactHttpOrigin2(host, "new-user host");
4799
+ warnRegistryDefaultNode2(resolved.verdict, this.config.registryCacheStore, this.config.onRegistryFallback);
4800
+ return [{ url }];
4801
+ }
4802
+ const destinations = resolved.hosts.flatMap((host) => host.urls.map((url) => ({ url, nodeDid: host.nodeDid })));
4803
+ return destinations;
4804
+ }
4805
+ /** Transport hints cannot override verified full-resource registry routing. */
4806
+ async resolveRegisteredDelegationHost(permissions, noEntryHost) {
4807
+ const resources = /* @__PURE__ */ new Map();
4808
+ let hasData = false;
4809
+ for (const permission of permissions) {
4810
+ const encryption = this.isEncryptionNetworkOperation(this.invocationServiceName(permission.service), permission.path, permission.space);
4811
+ const resource = encryption ? permission.path.split("/")[0] : permission.space;
4812
+ if (!resource) throw new ResourceNotRoutedError("delegation");
4813
+ resources.set(resource, encryption ? "encryption" : "host");
4814
+ hasData || (hasData = !encryption);
4815
+ }
4816
+ let dataHost;
4817
+ let encryptionHost;
4818
+ let unserved;
4819
+ for (const [resource, access] of resources) {
4820
+ try {
4821
+ const destinations = await this.routingDestinations(resource, access, [noEntryHost]);
4822
+ if (destinations.some(({ nodeDid }) => nodeDid === void 0)) return void 0;
4823
+ if (access === "host") dataHost ?? (dataHost = destinations[0]?.url);
4824
+ else encryptionHost ?? (encryptionHost = destinations[0]?.url);
4825
+ } catch (error) {
4826
+ if (!(error instanceof NodeNotFoundError2) || error.verdict.status !== "registered") throw error;
4827
+ unserved ?? (unserved = error);
4828
+ }
4829
+ }
4830
+ const host = hasData ? dataHost : encryptionHost;
4831
+ if (!host) throw unserved ?? new ResourceNotRoutedError("delegation");
4832
+ return host;
4833
+ }
4834
+ async bindRoutedAudience(authorization, nodeDid, stagedSession) {
4835
+ const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(authorization.split(".")[1])));
4836
+ const issuer = typeof payload === "object" && payload !== null && "iss" in payload ? payload.iss : void 0;
4837
+ const session = [
4838
+ stagedSession,
4839
+ this._serviceContext?.session,
4840
+ this.auth?.tinyCloudSession,
4841
+ ...this.runtimePermissionGrants.map((grant) => grant.session)
4842
+ ].find((candidate) => candidate?.verificationMethod === issuer);
4843
+ if (!session) throw new Error("Invocation signing session is not active");
4844
+ return rewriteInvocationAudience(authorization, nodeDid, session.jwk);
4845
+ }
4647
4846
  syncResolvedHostFromAuth() {
4648
4847
  const host = this.auth?.hosts[0];
4649
4848
  if (host) {
@@ -4757,6 +4956,29 @@ var _TinyCloudNode = class _TinyCloudNode {
4757
4956
  const session = this.currentTinyCloudSession();
4758
4957
  return session?.siwe ? this.projectSignedRecapCapabilities(session.siwe) : [];
4759
4958
  }
4959
+ /** Start one background refresh for each owner derived from verified authority. */
4960
+ refreshRegistryForCapabilities(permissions) {
4961
+ for (const permission of permissions) {
4962
+ try {
4963
+ const owner = permission.service === "tinycloud.encryption" && permission.path.startsWith("urn:tinycloud:encryption:") ? parseNetworkId2(permission.path.split("/")[0]).ownerDid : permission.space ? this.ownerDidFromSpaceId(permission.space) : void 0;
4964
+ if (owner) this.refreshRegistryOwner(owner);
4965
+ } catch {
4966
+ }
4967
+ }
4968
+ }
4969
+ refreshRegistryOwner(owner) {
4970
+ if (this.config.tinycloudRegistryUrl === null) return;
4971
+ const subject = owner.startsWith("did:pkh:") ? pkhDid2(owner.slice(owner.lastIndexOf(":") + 1), this.config.registryChainId ?? 1) : owner;
4972
+ const key = registryOwnerKeyV2(subject);
4973
+ if (this.registryRefreshOwners.has(key)) return;
4974
+ this.registryRefreshOwners.add(key);
4975
+ void refreshOwnerRegistry(subject, {
4976
+ registryUrl: this.config.tinycloudRegistryUrl,
4977
+ registryChainId: this.config.registryChainId,
4978
+ sequenceStore: this.config.registrySequenceStore,
4979
+ cacheStore: this.config.registryCacheStore
4980
+ }).catch(() => void 0);
4981
+ }
4760
4982
  /** Normalize signed capabilities without collapsing nested encryption into raw authority. */
4761
4983
  projectSignedRecapCapabilities(siwe) {
4762
4984
  const projected = this.parseRecapWithCaveats(siwe).map((entry) => {
@@ -5188,6 +5410,8 @@ var _TinyCloudNode = class _TinyCloudNode {
5188
5410
  const primarySession = this.currentTinyCloudSession();
5189
5411
  if (primarySession) {
5190
5412
  this.registerPrimarySessionGrant(primarySession);
5413
+ this.refreshRegistryOwner(pkhDid2(primarySession.address, primarySession.chainId));
5414
+ this.refreshRegistryForCapabilities(this.getVerifiedSessionCapabilities());
5191
5415
  if (this.auth?.spaceHosting === "unknown") {
5192
5416
  await this.checkSpaceHosting();
5193
5417
  this.assertSessionEpoch(epoch, "Sign-in");
@@ -5410,8 +5634,8 @@ var _TinyCloudNode = class _TinyCloudNode {
5410
5634
  const primarySession = auth.tinyCloudSession;
5411
5635
  const defaultSpaceId = this.ownedSpaceId("default");
5412
5636
  const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === BOOTSTRAP_SESSION_REQUESTS.default;
5413
- if (steps.some((step) => step.kind === "host")) {
5414
- await this.assertMayCreateSpace();
5637
+ for (const step of steps) {
5638
+ if (step.kind === "host") await this.assertMayCreateSpace(step.spaceId);
5415
5639
  }
5416
5640
  for (const step of steps) {
5417
5641
  if (step.kind !== "session") continue;
@@ -5460,10 +5684,20 @@ var _TinyCloudNode = class _TinyCloudNode {
5460
5684
  if (!session) {
5461
5685
  throw new Error(`Missing bootstrap session for ${step.space}`);
5462
5686
  }
5463
- const activated = await activateSessionWithHost2(host, session.delegationHeader, graph.signal).catch((error) => {
5464
- graph.assertActive();
5465
- throw error;
5466
- });
5687
+ const destinations = await this.routingDestinations(step.spaceId, "host");
5688
+ let activated;
5689
+ for (const destination of destinations) {
5690
+ try {
5691
+ await verifyResourceDestination2(destination, globalThis.fetch.bind(globalThis));
5692
+ const result = await activateSessionWithHost2(destination.url, session.delegationHeader, graph.signal);
5693
+ if (!result.success) throw Object.assign(new Error("Bootstrap activation failed"), { cause: result });
5694
+ activated = result;
5695
+ } catch (error) {
5696
+ graph.assertActive();
5697
+ throw error;
5698
+ }
5699
+ }
5700
+ if (!activated) throw new Error("Bootstrap space has no destination");
5467
5701
  graph.assertActive();
5468
5702
  if (!activated.success || activated.skipped?.includes(step.spaceId)) {
5469
5703
  throw Object.assign(
@@ -6046,6 +6280,13 @@ var _TinyCloudNode = class _TinyCloudNode {
6046
6280
  if (!session) {
6047
6281
  throw new Error("Owned space hosting requires an active session");
6048
6282
  }
6283
+ await this.assertMayCreateSpace(spaceId);
6284
+ if (this.auth.registryRecordPresent) {
6285
+ const hosted = await this.hostOwnedSpaceOnce(spaceId, signal);
6286
+ if (!hosted.success) throw Object.assign(new Error(`Failed to activate listed space ${spaceId}: ${describeHostFailure2(hosted)}`), { cause: hosted });
6287
+ if (this.auth.tinyCloudSession === session) this.confirmedHostedSpaceIds.add(spaceId);
6288
+ return;
6289
+ }
6049
6290
  if (this.confirmedHostedSpaceIds.has(spaceId) || this.hostedOwnedSpaceIds.has(spaceId)) {
6050
6291
  return;
6051
6292
  }
@@ -6064,7 +6305,7 @@ var _TinyCloudNode = class _TinyCloudNode {
6064
6305
  { cause: activation }
6065
6306
  );
6066
6307
  }
6067
- await this.assertMayCreateSpace();
6308
+ await this.assertMayCreateSpace(spaceId);
6068
6309
  if (signal?.aborted) throw signal.reason;
6069
6310
  const created = await this.hostOwnedSpaceOnce(spaceId, signal);
6070
6311
  if (!created.success) {
@@ -6125,17 +6366,19 @@ var _TinyCloudNode = class _TinyCloudNode {
6125
6366
  { cause: hosted }
6126
6367
  );
6127
6368
  }
6128
- const activation = await activateSessionWithHost2(
6129
- host,
6130
- session.delegationHeader
6131
- );
6132
- if (!activation.success || activation.skipped?.includes(spaceId)) {
6133
- throw Object.assign(
6134
- new Error(
6135
- `Failed to activate session for owned space ${spaceId}: ${activation.success ? "space was skipped" : describeHostFailure2(activation)}`
6136
- ),
6137
- { cause: activation }
6369
+ if (!this.auth.registryRecordPresent) {
6370
+ const activation = await activateSessionWithHost2(
6371
+ host,
6372
+ session.delegationHeader
6138
6373
  );
6374
+ if (!activation.success || activation.skipped?.includes(spaceId)) {
6375
+ throw Object.assign(
6376
+ new Error(
6377
+ `Failed to activate session for owned space ${spaceId}: ${activation.success ? "space was skipped" : describeHostFailure2(activation)}`
6378
+ ),
6379
+ { cause: activation }
6380
+ );
6381
+ }
6139
6382
  }
6140
6383
  if (this.auth?.tinyCloudSession === session) this.hostedOwnedSpaceIds.add(spaceId);
6141
6384
  try {
@@ -6179,11 +6422,9 @@ var _TinyCloudNode = class _TinyCloudNode {
6179
6422
  * session's recap doesn't cover it and the registry has no record; the host
6180
6423
  * SIWE is idempotent server-side. Must be called after {@link signIn}.
6181
6424
  *
6182
- * Under `registryPolicy: "fail-closed"`, hosting is refused with
6183
- * `NodeRegistryEntryExistsError` when the owner's registry record does not
6184
- * list the host, or `RegistryUnavailableError` when the registry gives no
6185
- * verdict. {@link hostOwnedSpace} itself is an explicit action and is not
6186
- * guarded.
6425
+ * Current signed per-space membership is checked before both automatic and
6426
+ * explicit hosting. Registered replicas use the session grant; no-entry
6427
+ * onboarding retains the host-SIWE flow.
6187
6428
  *
6188
6429
  * @param name - The owned space name (e.g. `"secrets"`).
6189
6430
  * @returns The hosted owned-space URI.
@@ -6193,27 +6434,20 @@ var _TinyCloudNode = class _TinyCloudNode {
6193
6434
  throw new Error("Not signed in. Call signIn() first.");
6194
6435
  }
6195
6436
  const spaceId = this.ownedSpaceId(name);
6437
+ await this.assertMayCreateSpace(spaceId);
6196
6438
  if (this.hostedOwnedSpaceIds.has(spaceId) || await this.isOwnedSpaceRegistered(spaceId)) {
6197
6439
  return spaceId;
6198
6440
  }
6199
- await this.assertMayCreateSpace();
6200
6441
  return this.hostOwnedSpace(name);
6201
6442
  }
6202
- /**
6203
- * Space-creation guard (TC-1443) for the automatic hosting paths: sign-in's
6204
- * owned spaces, account bootstrap, {@link ensureOwnedSpaceHosted} and
6205
- * {@link ensurePublicSpace}. Only `registryPolicy: "fail-closed"` consults
6206
- * the registry ({@link NodeUserAuthorization.assertMayCreateSpace}); the
6207
- * default policy performs no lookup.
6208
- */
6209
- async assertMayCreateSpace() {
6210
- if (this.config.registryPolicy !== "fail-closed") return;
6211
- await this.auth.assertMayCreateSpace();
6443
+ /** Refresh full-space membership before any automatic or explicit hosting. */
6444
+ async assertMayCreateSpace(spaceId) {
6445
+ await this.routingDestinations(spaceId, "host");
6212
6446
  }
6213
6447
  /** Registry-policy settings shared by every authorization this client creates. */
6214
6448
  registryPolicyConfig() {
6215
- const { registryPolicy, newUserHost, registryChainId, registrySequenceStore, onNodeNotFound } = this.config;
6216
- return { registryPolicy, newUserHost, registryChainId, registrySequenceStore, onNodeNotFound };
6449
+ const { registryPolicy, newUserHost, registryChainId, registrySequenceStore, registryCacheStore, onNodeNotFound, onRegistryFallback } = this.config;
6450
+ return { registryPolicy, newUserHost, registryChainId, registrySequenceStore, registryCacheStore, onNodeNotFound, onRegistryFallback };
6217
6451
  }
6218
6452
  /**
6219
6453
  * Submit the host delegation for an owned space, sharing one in-flight
@@ -6419,12 +6653,9 @@ var _TinyCloudNode = class _TinyCloudNode {
6419
6653
  }
6420
6654
  stagedSessionExpiry = signedExpiry ?? persistedPolicyExpiry ?? persistedExpiry(void 0);
6421
6655
  }
6422
- const resolvedHost = await this.resolveRestoredHost(
6423
- sessionData.tinycloudHosts,
6424
- restoredAddress,
6425
- sessionData.chainId
6426
- );
6427
- const stagedHost = resolvedHost ?? this.config.host;
6656
+ const restoreHints = this.explicitHost ? [this.explicitHost] : sessionData.tinycloudHosts ?? [];
6657
+ const resolvedHost = this.config.replication?.enabled ? (await this.routingDestinations(sessionData.spaceId, "read", restoreHints))[0]?.url : void 0;
6658
+ const stagedHost = resolvedHost ?? exactHttpOrigin2(restoreHints[0] ?? this.config.host, "restored host hint");
6428
6659
  let restoredNodeFeatures;
6429
6660
  let replicaProtocolMismatch = false;
6430
6661
  if (this.config.replication?.enabled) {
@@ -6541,7 +6772,7 @@ var _TinyCloudNode = class _TinyCloudNode {
6541
6772
  this.sessionManager = stagedManager;
6542
6773
  this.sessionKeyId = stagedKeyId;
6543
6774
  this.sessionKeyJwk = stagedJwk;
6544
- if (resolvedHost) this.config.host = resolvedHost;
6775
+ this.config.host = stagedHost;
6545
6776
  if (spaceOwnerKey(serviceSession.spaceId) !== this.currentStorageOwner()) {
6546
6777
  this.storageFull.clear();
6547
6778
  this._requestLog?.clear();
@@ -6608,6 +6839,10 @@ var _TinyCloudNode = class _TinyCloudNode {
6608
6839
  if (attempt === this.latestSessionAttempt) await this.endSession();
6609
6840
  this.assertSessionEpoch(epoch, "Session restore");
6610
6841
  }
6842
+ if (this.config.replication?.enabled) {
6843
+ if (stagedAddress !== void 0) this.refreshRegistryOwner(pkhDid2(stagedAddress, stagedChainId));
6844
+ this.refreshRegistryForCapabilities(this.getVerifiedSessionCapabilities());
6845
+ }
6611
6846
  }
6612
6847
  stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
6613
6848
  if (!session?.siwe) return { cache: void 0, grants: [] };
@@ -6656,7 +6891,7 @@ var _TinyCloudNode = class _TinyCloudNode {
6656
6891
  };
6657
6892
  }
6658
6893
  stageRestoredServiceGraph(input) {
6659
- const graph = this.createServiceGraphLifetime();
6894
+ const graph = this.createServiceGraphLifetime(input.serviceSession);
6660
6895
  const core = this.auth ? new TinyCloud(this.auth, {
6661
6896
  invokeAny: graph.invokeAny,
6662
6897
  telemetry: this.config.telemetry,
@@ -6881,56 +7116,6 @@ var _TinyCloudNode = class _TinyCloudNode {
6881
7116
  spaceService.updateConfig({ sharingService });
6882
7117
  return { core, graph, serviceContext, kv, sql, duckdb, hooks, vault, encryption, capabilityRegistry, keyProvider, sharingService, delegationManager, spaceService };
6883
7118
  }
6884
- /**
6885
- * Resolve the host a restored session should target.
6886
- *
6887
- * Mirrors fresh sign-in host resolution but for the restore path:
6888
- * an explicit/pinned host always wins, then the hosts the session was
6889
- * persisted with, then a lazy registry/fallback resolution for sessions
6890
- * that predate the persisted `tinycloudHosts` field. Returns `undefined`
6891
- * only when there's nothing to resolve from (no explicit host, no
6892
- * persisted hosts, and no address/chainId) — in which case the existing
6893
- * `config.host` (default) is left in place.
6894
- *
6895
- * Resolution failures are surfaced, not swallowed: a genuinely broken
6896
- * registry lookup throws rather than silently falling back to a wrong host.
6897
- */
6898
- async resolveRestoredHost(persistedHosts, address, chainId) {
6899
- if (this.explicitHost) {
6900
- return this.explicitHost;
6901
- }
6902
- if (persistedHosts && persistedHosts.length > 0) {
6903
- return persistedHosts[0];
6904
- }
6905
- if (address === void 0 || chainId === void 0) {
6906
- return void 0;
6907
- }
6908
- if (this.config.registryPolicy === "fail-closed") {
6909
- const resolved2 = await resolveFailClosedTinyCloudHosts2(address, {
6910
- registryUrl: this.config.tinycloudRegistryUrl,
6911
- registryChainId: this.config.registryChainId,
6912
- newUserHost: this.config.newUserHost,
6913
- sequenceStore: this.config.registrySequenceStore,
6914
- onNodeNotFound: this.config.onNodeNotFound,
6915
- autoDiscoverLocalNode: this.config.autoDiscoverLocalNode,
6916
- localNodeUrl: this.config.localNodeUrl,
6917
- localLinkName: this.config.localLinkName,
6918
- expectedNodeDid: this.config.expectedNodeDid,
6919
- localNodeIdentityStore: this.config.localNodeIdentityStore
6920
- });
6921
- return resolved2.hosts[0];
6922
- }
6923
- const resolved = await resolveTinyCloudHosts2(pkhDid2(address, chainId), {
6924
- registryUrl: this.config.tinycloudRegistryUrl,
6925
- fallbackHosts: this.config.tinycloudFallbackHosts,
6926
- autoDiscoverLocalNode: this.config.autoDiscoverLocalNode,
6927
- localNodeUrl: this.config.localNodeUrl,
6928
- localLinkName: this.config.localLinkName,
6929
- expectedNodeDid: this.config.expectedNodeDid,
6930
- localNodeIdentityStore: this.config.localNodeIdentityStore
6931
- });
6932
- return resolved.hosts[0];
6933
- }
6934
7119
  /**
6935
7120
  * Resolve the currently-active TinyCloudSession, preferring the auth
6936
7121
  * layer's value (wallet mode) and falling back to the node-level
@@ -7405,10 +7590,10 @@ var _TinyCloudNode = class _TinyCloudNode {
7405
7590
  * second time. Only a value recorded for this exact host qualifies; older
7406
7591
  * nodes that omit `nodeId` fall through to the fetch.
7407
7592
  */
7408
- async fetchNodeId() {
7409
- const cached = this.auth?.nodeIdForHost(this.config.host);
7593
+ async fetchNodeId(host = this.config.host) {
7594
+ const cached = this.auth?.nodeIdForHost(host);
7410
7595
  if (cached) return cached;
7411
- const response = await fetch(`${this.config.host}/info`);
7596
+ const response = await fetch(`${host}/info`);
7412
7597
  if (!response.ok) {
7413
7598
  throw await httpResponseError(response, "Failed to fetch node info");
7414
7599
  }
@@ -7456,10 +7641,24 @@ var _TinyCloudNode = class _TinyCloudNode {
7456
7641
  )
7457
7642
  };
7458
7643
  }
7459
- async fetchEncryptionNetworkAt(host, networkId, fetchFn) {
7460
- const response = await fetchFn(
7461
- `${host}/encryption/networks/${encodeURIComponent(networkId)}`
7462
- );
7644
+ async encryptionDestination(networkId) {
7645
+ const destination = (await this.routingDestinations(networkId, "encryption"))[0];
7646
+ if (!destination) throw new Error("No encryption destination is registered");
7647
+ await verifyResourceDestination2(destination, globalThis.fetch.bind(globalThis));
7648
+ return { ...destination, nodeDid: destination.nodeDid ?? await this.fetchNodeId(destination.url) };
7649
+ }
7650
+ async fetchEncryptionNetworkAt(host, networkId, fetchFn, callerSignal) {
7651
+ let response;
7652
+ const timeout = AbortSignal.timeout(5e3);
7653
+ try {
7654
+ response = await fetchFn(`${host}/encryption/networks/${encodeURIComponent(networkId)}`, {
7655
+ signal: callerSignal ? AbortSignal.any([callerSignal, timeout]) : timeout,
7656
+ redirect: "error"
7657
+ });
7658
+ } catch (error) {
7659
+ if (routingFailure3(error) || callerSignal?.aborted || !timeout.aborted && error instanceof Error && error.name === "AbortError") throw error;
7660
+ throw new NodeNotRespondingError2([host], error);
7661
+ }
7463
7662
  if (response.status === 404) return null;
7464
7663
  if (!response.ok) {
7465
7664
  throw await httpResponseError(response, `Failed to fetch encryption network ${networkId}`);
@@ -7469,18 +7668,18 @@ var _TinyCloudNode = class _TinyCloudNode {
7469
7668
  }
7470
7669
  createEncryptionService(binding) {
7471
7670
  const graph = binding?.graph ?? this._serviceGraph;
7472
- const host = binding?.host ?? this.config.host;
7473
7671
  const session = binding?.session ?? this.requireEncryptionSession();
7474
7672
  const did = binding?.did ?? this.did;
7475
7673
  const address = binding?.address ?? this._address;
7476
7674
  const chainId = binding?.chainId ?? this._chainId;
7477
7675
  const crypto2 = this.createEncryptionCrypto();
7478
- const descriptorCache = /* @__PURE__ */ new Map();
7479
7676
  const transport = {
7480
- postDecrypt: async ({ networkId, authorization, canonicalBody, signal }) => {
7677
+ postDecrypt: async ({ targetNode, networkId, authorization, canonicalBody, signal }) => {
7481
7678
  graph.assertActive();
7679
+ const destination = await this.encryptionDestination(networkId);
7680
+ if (destination.nodeDid !== targetNode) throw new NodeIdentityMismatchError(destination.url, destination.nodeDid, targetNode);
7482
7681
  const response = await graph.fetch(
7483
- `${host}/encryption/networks/${encodeURIComponent(networkId)}/decrypt`,
7682
+ `${destination.url}/encryption/networks/${encodeURIComponent(networkId)}/decrypt`,
7484
7683
  {
7485
7684
  method: "POST",
7486
7685
  headers: {
@@ -7508,6 +7707,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7508
7707
  }
7509
7708
  };
7510
7709
  return new EncryptionService({
7710
+ resolveTargetNode: async (networkId) => (await this.encryptionDestination(networkId)).nodeDid,
7511
7711
  crypto: crypto2,
7512
7712
  signer: {
7513
7713
  signDecryptInvocation: async (input) => {
@@ -7531,18 +7731,8 @@ var _TinyCloudNode = class _TinyCloudNode {
7531
7731
  node: {
7532
7732
  fetchByNetworkId: async (networkId) => {
7533
7733
  graph.assertActive();
7534
- let pending = descriptorCache.get(networkId);
7535
- if (!pending) {
7536
- pending = this.fetchEncryptionNetworkAt(host, networkId, graph.fetch).then((descriptor2) => {
7537
- if (descriptor2 === null) descriptorCache.delete(networkId);
7538
- return descriptor2;
7539
- }).catch((error) => {
7540
- descriptorCache.delete(networkId);
7541
- throw error;
7542
- });
7543
- descriptorCache.set(networkId, pending);
7544
- }
7545
- const descriptor = await pending;
7734
+ const destination = await this.encryptionDestination(networkId);
7735
+ const descriptor = await this.fetchEncryptionNetworkAt(destination.url, networkId, graph.fetch, graph.signal);
7546
7736
  graph.assertActive();
7547
7737
  return descriptor;
7548
7738
  }
@@ -7550,13 +7740,15 @@ var _TinyCloudNode = class _TinyCloudNode {
7550
7740
  wellKnown: {
7551
7741
  fetchWellKnown: async (principal, discoveryKey) => {
7552
7742
  graph.assertActive();
7743
+ const destination = await this.encryptionDestination(this.getDefaultEncryptionNetworkId());
7744
+ if (this.auth?.registryRecordPresent) return null;
7553
7745
  if (!address || !didPrincipalMatches2(principal, did)) {
7554
7746
  return null;
7555
7747
  }
7556
7748
  const publicSpaceId = makePublicSpaceId(address, chainId);
7557
7749
  const encodedKey = discoveryKey.split("/").map(encodeURIComponent).join("/");
7558
7750
  const response = await graph.fetch(
7559
- `${host}/public/${encodeURIComponent(publicSpaceId)}/kv/${encodedKey}`,
7751
+ `${destination.url}/public/${encodeURIComponent(publicSpaceId)}/kv/${encodedKey}`,
7560
7752
  { method: "GET" }
7561
7753
  );
7562
7754
  graph.assertActive();
@@ -7926,7 +8118,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7926
8118
  const signature = await this.signer.signMessage(prepared.siwe);
7927
8119
  assertOwnerGraphActive();
7928
8120
  const delegationSession = this.wasmBindings.completeSessionSetup({ ...prepared, signature });
7929
- const activation = await activateSessionWithHost2(host, delegationSession.delegationHeader);
8121
+ const activation = await this.activateSignedSession(delegationSession, prepared.siwe, host);
7930
8122
  assertOwnerGraphActive();
7931
8123
  if (!activation.success) {
7932
8124
  throw Object.assign(
@@ -8245,15 +8437,16 @@ var _TinyCloudNode = class _TinyCloudNode {
8245
8437
  async getEncryptionNetwork(nameOrNetworkId = this.getDefaultEncryptionNetworkId()) {
8246
8438
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
8247
8439
  return this.fetchEncryptionNetworkAt(
8248
- this.config.host,
8440
+ (await this.encryptionDestination(networkId)).url,
8249
8441
  networkId,
8250
8442
  globalThis.fetch.bind(globalThis)
8251
8443
  );
8252
8444
  }
8253
8445
  async createEncryptionNetwork(name = DEFAULT_ENCRYPTION_NETWORK_NAME) {
8254
- const targetNode = await this.fetchNodeId();
8255
8446
  const ownerDid = this.did;
8256
8447
  const networkId = this.getDefaultEncryptionNetworkId(name);
8448
+ const destination = await this.encryptionDestination(networkId);
8449
+ const targetNode = destination.nodeDid;
8257
8450
  const body = {
8258
8451
  name,
8259
8452
  ownerDid,
@@ -8276,7 +8469,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8276
8469
  action: NETWORK_CREATE_ACTION2,
8277
8470
  facts
8278
8471
  });
8279
- const response = await fetch(`${this.config.host}/encryption/networks`, {
8472
+ const response = await this._serviceGraph.fetch(`${destination.url}/encryption/networks`, {
8280
8473
  method: "POST",
8281
8474
  headers: {
8282
8475
  Authorization: signed.authorization,
@@ -8616,6 +8809,97 @@ var _TinyCloudNode = class _TinyCloudNode {
8616
8809
  }
8617
8810
  return { recap: exactVerifiedRecap(verified), expiresAt };
8618
8811
  }
8812
+ async activateResourceSession(delegationHeader, resources, hostHint, signal) {
8813
+ const targets = /* @__PURE__ */ new Map();
8814
+ const routedSpaces = [];
8815
+ let unserved;
8816
+ let requestedResource;
8817
+ for (const resource of new Set(resources)) {
8818
+ requestedResource ?? (requestedResource = resource);
8819
+ signal?.throwIfAborted();
8820
+ try {
8821
+ const encryption = resource.startsWith("urn:tinycloud:encryption:");
8822
+ const destinations = await this.routingDestinations(
8823
+ resource,
8824
+ encryption ? "encryption" : "host",
8825
+ [hostHint ?? this.config.host]
8826
+ );
8827
+ if (!encryption && destinations.some(({ nodeDid }) => nodeDid !== void 0)) routedSpaces.push(resource);
8828
+ for (const destination of destinations) {
8829
+ let target = targets.get(destination.url);
8830
+ if (!target) {
8831
+ target = { destination, spaces: [] };
8832
+ targets.set(destination.url, target);
8833
+ }
8834
+ if (!encryption && destination.nodeDid !== void 0) target.spaces.push(resource);
8835
+ }
8836
+ } catch (error) {
8837
+ if (!(error instanceof NodeNotFoundError2)) throw error;
8838
+ unserved ?? (unserved = error);
8839
+ }
8840
+ }
8841
+ if (targets.size === 0) throw unserved ?? new ResourceNotRoutedError(requestedResource ?? "session grant");
8842
+ const succeeded = [];
8843
+ const accepted = [];
8844
+ const failed = [];
8845
+ let refusal;
8846
+ for (const { destination: target, spaces } of targets.values()) {
8847
+ signal?.throwIfAborted();
8848
+ try {
8849
+ await verifyResourceDestination2(target, globalThis.fetch.bind(globalThis));
8850
+ const timeout = AbortSignal.timeout(5e3);
8851
+ const result = await activateSessionWithHost2(
8852
+ target.url,
8853
+ delegationHeader,
8854
+ signal ? AbortSignal.any([signal, timeout]) : timeout
8855
+ );
8856
+ this.throwRegistryRefusal(result.status, result.error ?? "");
8857
+ if (result.success) {
8858
+ const skipped = result.skipped?.find((space) => spaces.some((expected) => this.spaceIdsEqual(space, expected)));
8859
+ if (skipped) {
8860
+ failed.push({ url: target.url, status: result.status, cause: new ResourceNotRoutedError(skipped) });
8861
+ continue;
8862
+ }
8863
+ succeeded.push(target.url);
8864
+ accepted.push(result);
8865
+ } else {
8866
+ refusal = result;
8867
+ failed.push({ url: target.url, status: result.status, cause: result });
8868
+ }
8869
+ } catch (error) {
8870
+ if (signal?.aborted) throw error;
8871
+ failed.push({ url: target.url, cause: routingFailure3(error) ? error : new NodeNotRespondingError2([target.url], error) });
8872
+ }
8873
+ }
8874
+ if (failed.length > 0) {
8875
+ if (targets.size > 1) throw new PartialNodeWriteError2(succeeded, failed);
8876
+ if (refusal) return refusal;
8877
+ throw failed[0].cause;
8878
+ }
8879
+ const activated = new Set(accepted.flatMap((result) => result.activated ?? []));
8880
+ return {
8881
+ ...accepted[0],
8882
+ activated: [...activated],
8883
+ skipped: [...new Set(accepted.flatMap((result) => result.skipped ?? []))].filter((space) => !activated.has(space) && !routedSpaces.some((expected) => this.spaceIdsEqual(space, expected)))
8884
+ };
8885
+ }
8886
+ activateRoutedDelegation(delegation, signal) {
8887
+ const resources = this.operationsFromDelegation(delegation).map(
8888
+ (operation) => operation.resource?.startsWith("urn:tinycloud:encryption:") ? operation.resource.split("/")[0] : operation.spaceId ?? delegation.spaceId
8889
+ );
8890
+ return this.activateResourceSession(delegation.delegationHeader, resources, delegation.host, signal);
8891
+ }
8892
+ activateSignedSession(session, siwe, hostHint, signal) {
8893
+ const resources = this.parseRecapWithCaveats(siwe).map(
8894
+ (entry) => this.isEncryptionNetworkOperation(this.invocationServiceName(entry.service), entry.path, entry.space) ? entry.path.split("/")[0] : entry.space.startsWith("tinycloud:") ? entry.space : session.spaceId
8895
+ );
8896
+ return this.activateResourceSession(
8897
+ session.delegationHeader,
8898
+ resources.length > 0 ? resources : [session.spaceId],
8899
+ hostHint,
8900
+ signal
8901
+ );
8902
+ }
8619
8903
  /**
8620
8904
  * Install a portable runtime permission delegation into this SDK instance so
8621
8905
  * matching service calls and downstream `delegateTo()` calls can use it.
@@ -8635,16 +8919,12 @@ var _TinyCloudNode = class _TinyCloudNode {
8635
8919
  `Runtime delegation targets ${delegation.delegateDID} but this session key is ${session.verificationMethod}.`
8636
8920
  );
8637
8921
  }
8638
- const targetHost = delegation.host ?? this.config.host;
8639
8922
  let activateResult;
8640
8923
  try {
8641
- activateResult = await activateSessionWithHost2(
8642
- targetHost,
8643
- delegation.delegationHeader,
8644
- graph.signal
8645
- );
8924
+ activateResult = await this.activateRoutedDelegation(delegation, graph.signal);
8646
8925
  } catch (cause) {
8647
8926
  graph.assertActive();
8927
+ if (routingFailure3(cause)) throw cause;
8648
8928
  throw Object.assign(
8649
8929
  new Error("The TinyCloud host could not be reached to activate the delegation."),
8650
8930
  { code: "ACTIVATION_TRANSPORT_FAILED", cause }
@@ -8800,11 +9080,15 @@ var _TinyCloudNode = class _TinyCloudNode {
8800
9080
  ...prepared,
8801
9081
  signature
8802
9082
  });
8803
- const activateResult = await activateSessionWithHost2(
8804
- this.config.host,
8805
- delegatedSession.delegationHeader,
8806
- graph.signal
8807
- ).catch((error) => {
9083
+ const delegation = this.runtimeDelegationFromSession(
9084
+ delegatedSession,
9085
+ delegatedEntries,
9086
+ spaceId,
9087
+ captured,
9088
+ expiresAt,
9089
+ { siwe: prepared.siwe, signature }
9090
+ );
9091
+ const activateResult = await this.activateRoutedDelegation(delegation, graph.signal).catch((error) => {
8808
9092
  graph.assertActive();
8809
9093
  throw error;
8810
9094
  });
@@ -8820,14 +9104,6 @@ var _TinyCloudNode = class _TinyCloudNode {
8820
9104
  { cause: activateResult }
8821
9105
  );
8822
9106
  }
8823
- const delegation = this.runtimeDelegationFromSession(
8824
- delegatedSession,
8825
- delegatedEntries,
8826
- spaceId,
8827
- captured,
8828
- expiresAt,
8829
- { siwe: prepared.siwe, signature }
8830
- );
8831
9107
  graph.assertActive();
8832
9108
  assertCapturedKey?.();
8833
9109
  this.runtimePermissionGrants.push({
@@ -8972,7 +9248,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8972
9248
  if (!publicSpaceId) {
8973
9249
  throw new Error("Public space not enabled. Set enablePublicSpace: true in config.");
8974
9250
  }
8975
- await this.assertMayCreateSpace();
9251
+ await this.assertMayCreateSpace(publicSpaceId);
8976
9252
  await this.auth.hostPublicSpace(publicSpaceId);
8977
9253
  const kvActions = [KV3.PUT, KV3.GET, KV3.DEL, KV3.LIST, KV3.METADATA];
8978
9254
  const abilities = { kv: { "": kvActions } };
@@ -8995,10 +9271,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8995
9271
  ...prepared,
8996
9272
  signature
8997
9273
  });
8998
- const activateResult = await activateSessionWithHost2(
8999
- this.config.host,
9000
- delegationSession.delegationHeader
9001
- );
9274
+ const activateResult = await this.activateSignedSession(delegationSession, prepared.siwe);
9002
9275
  if (!activateResult.success) {
9003
9276
  throw Object.assign(
9004
9277
  new Error(`Failed to activate public space delegation: ${describeHostFailure2(activateResult)}`),
@@ -9548,10 +9821,7 @@ var _TinyCloudNode = class _TinyCloudNode {
9548
9821
  if (onPrepared) {
9549
9822
  await onPrepared(delegation);
9550
9823
  }
9551
- const activateResult = await activateSessionWithHost2(
9552
- this.config.host,
9553
- delegationHeader
9554
- );
9824
+ const activateResult = await this.activateRoutedDelegation(delegation);
9555
9825
  assertDelegationActivated(activateResult, delegation, requireActivation);
9556
9826
  return delegation;
9557
9827
  }
@@ -9585,10 +9855,7 @@ var _TinyCloudNode = class _TinyCloudNode {
9585
9855
  if (onPrepared) {
9586
9856
  await onPrepared(delegation);
9587
9857
  }
9588
- const activateResult = await activateSessionWithHost2(
9589
- targetHost,
9590
- delegationHeader
9591
- );
9858
+ const activateResult = await this.activateRoutedDelegation(delegation);
9592
9859
  assertDelegationActivated(activateResult, delegation, requireActivation);
9593
9860
  return delegation;
9594
9861
  }
@@ -10240,10 +10507,7 @@ var _TinyCloudNode = class _TinyCloudNode {
10240
10507
  ...prepared,
10241
10508
  signature
10242
10509
  });
10243
- const activateResult = await activateSessionWithHost2(
10244
- this.config.host,
10245
- delegationSession.delegationHeader
10246
- );
10510
+ const activateResult = await this.activateSignedSession(delegationSession, prepared.siwe);
10247
10511
  if (!activateResult.success) {
10248
10512
  throw Object.assign(
10249
10513
  new Error(`Failed to activate delegation: ${describeHostFailure2(activateResult)}`),
@@ -10288,10 +10552,7 @@ var _TinyCloudNode = class _TinyCloudNode {
10288
10552
  ...publicPrepared,
10289
10553
  signature: publicSignature
10290
10554
  });
10291
- const publicActivateResult = await activateSessionWithHost2(
10292
- this.config.host,
10293
- publicSession.delegationHeader
10294
- );
10555
+ const publicActivateResult = await this.activateSignedSession(publicSession, publicPrepared.siwe);
10295
10556
  if (publicActivateResult.success) {
10296
10557
  result.publicDelegation = {
10297
10558
  cid: publicSession.delegationCid,
@@ -10397,11 +10658,7 @@ var _TinyCloudNode = class _TinyCloudNode {
10397
10658
  ...prepared,
10398
10659
  signature
10399
10660
  });
10400
- const activateResult = await activateSessionWithHost2(
10401
- targetHost,
10402
- invokerSession.delegationHeader,
10403
- graph.signal
10404
- ).catch((error) => {
10661
+ const activateResult = await this.activateSignedSession(invokerSession, prepared.siwe, targetHost, graph.signal).catch((error) => {
10405
10662
  graph.assertActive();
10406
10663
  throw error;
10407
10664
  });
@@ -10528,10 +10785,7 @@ var _TinyCloudNode = class _TinyCloudNode {
10528
10785
  ...prepared,
10529
10786
  signature
10530
10787
  });
10531
- const activateResult = await activateSessionWithHost2(
10532
- targetHost,
10533
- subDelegationSession.delegationHeader
10534
- );
10788
+ const activateResult = await this.activateSignedSession(subDelegationSession, prepared.siwe, targetHost);
10535
10789
  if (!activateResult.success) {
10536
10790
  throw Object.assign(
10537
10791
  new Error(`Failed to activate sub-delegation: ${describeHostFailure2(activateResult)}`),
@@ -10634,7 +10888,7 @@ import {
10634
10888
  SpaceService as SpaceService2,
10635
10889
  SpaceErrorCodes,
10636
10890
  createSpaceService,
10637
- parseSpaceUri as parseSpaceUri2,
10891
+ parseSpaceUri as parseSpaceUri3,
10638
10892
  buildSpaceUri,
10639
10893
  makePublicSpaceId as makePublicSpaceId2,
10640
10894
  Space
@@ -10671,7 +10925,10 @@ export {
10671
10925
  KVService3 as KVService,
10672
10926
  ManifestValidationError3 as ManifestValidationError,
10673
10927
  MemorySessionStorage,
10928
+ NodeIdentityMismatchError2 as NodeIdentityMismatchError,
10929
+ NodeNotRespondingError3 as NodeNotRespondingError,
10674
10930
  NodeUserAuthorization,
10931
+ PartialNodeWriteError3 as PartialNodeWriteError,
10675
10932
  PermissionNotInManifestError2 as PermissionNotInManifestError,
10676
10933
  PrefixedKVService,
10677
10934
  ProtocolMismatchError2 as ProtocolMismatchError,
@@ -10738,7 +10995,7 @@ export {
10738
10995
  parseExpiry2 as parseExpiry,
10739
10996
  parsePkhDid,
10740
10997
  parseSecretCatalogKey,
10741
- parseSpaceUri2 as parseSpaceUri,
10998
+ parseSpaceUri3 as parseSpaceUri,
10742
10999
  pkhDid3 as pkhDid,
10743
11000
  principalDid,
10744
11001
  principalDidEquals4 as principalDidEquals,