@tinycloud/node-sdk 2.11.0-beta.1 → 2.11.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.
@@ -319,6 +319,12 @@ declare class NodeUserAuthorization implements IUserAuthorization {
319
319
  private _address?;
320
320
  private _chainId?;
321
321
  private _nodeFeatures;
322
+ /**
323
+ * `nodeId` from the `GET /info` performed during sign-in/restore, tagged with
324
+ * the host it came from. Callers that need the node DID (encryption-network
325
+ * admin invocations) reuse this instead of re-fetching `/info`.
326
+ */
327
+ private _nodeInfoIdentity?;
322
328
  private _lastActivationSkippedSpaceIds;
323
329
  constructor(config: NodeUserAuthorizationConfig);
324
330
  /**
@@ -387,6 +393,13 @@ declare class NodeUserAuthorization implements IUserAuthorization {
387
393
  private requireTinyCloudHosts;
388
394
  private get primaryTinyCloudHost();
389
395
  get nodeFeatures(): string[];
396
+ /**
397
+ * The node DID reported by `GET /info` for `host`, if the sign-in (or
398
+ * restore) that populated it targeted that same host. Returns `undefined`
399
+ * for any other host, and for nodes whose `/info` omits `nodeId`, so callers
400
+ * fall back to fetching it themselves rather than using a foreign node's DID.
401
+ */
402
+ nodeIdForHost(host: string): string | undefined;
390
403
  get lastActivationSkippedSpaceIds(): string[];
391
404
  /**
392
405
  * Compute the `abilities` map the WASM `prepareSession` call should
@@ -987,6 +1000,21 @@ interface RuntimePermissionGrantOptions {
987
1000
  /** Override expiry. ms-format string ("7d", "1h") or raw milliseconds. */
988
1001
  expiry?: string | number;
989
1002
  }
1003
+ interface EnsureEncryptionNetworkOptions {
1004
+ /**
1005
+ * Skip the `GET /encryption/networks/{id}` existence probe and go straight
1006
+ * to the create.
1007
+ *
1008
+ * The probe pays for itself only when the network already exists. On a
1009
+ * freshly bootstrapped account it is a guaranteed 404, so it costs a round
1010
+ * trip on the cold sign-in path and saves nothing. Creating without it is
1011
+ * safe: the server answers `409 Conflict` if the network is already there,
1012
+ * which {@link TinyCloudNode.createEncryptionNetwork} resolves by reading the
1013
+ * existing descriptor. Set this only when the caller knows the account was
1014
+ * just provisioned.
1015
+ */
1016
+ assumeMissing?: boolean;
1017
+ }
990
1018
  /**
991
1019
  * High-level TinyCloud API for Node.js environments.
992
1020
  *
@@ -1044,6 +1072,12 @@ declare class TinyCloudNode {
1044
1072
  * so this avoids re-parsing the recap on every registration.
1045
1073
  */
1046
1074
  private _recapOperationsCache?;
1075
+ /**
1076
+ * Owned space ids this sign-in has already confirmed are hosted. Consulted by
1077
+ * {@link ensureOwnedSpaceHostedById}; cleared on every {@link signIn} because
1078
+ * hosting is confirmed against the session that was active at the time.
1079
+ */
1080
+ private readonly confirmedHostedSpaceIds;
1047
1081
  /**
1048
1082
  * TinyCloudSession captured by {@link restoreSession} when there's no
1049
1083
  * auth-layer signer available (session-only mode used by OpenKey-backed
@@ -1252,6 +1286,22 @@ declare class TinyCloudNode {
1252
1286
  private withAccountRegistryRetry;
1253
1287
  private requestedEncryptionNetworkIds;
1254
1288
  private ensureRequestedEncryptionNetworks;
1289
+ /**
1290
+ * Ensure one of this user's owned spaces is hosted, at most once per space
1291
+ * per sign-in.
1292
+ *
1293
+ * Account bootstrap funnels six calls through here (via
1294
+ * `AccountService.ensureAccountSpaceHosted`), and each one re-submits
1295
+ * `activateSessionWithHost` with the *primary* session. That session's recap
1296
+ * covers only `default` and `secrets`, so the account space appears in
1297
+ * neither `activated` nor `skipped` and the guard below returns immediately
1298
+ * — the repeat calls cannot change the outcome, they only cost round trips.
1299
+ *
1300
+ * The memo is per sign-in ({@link confirmedHostedSpaceIds} is cleared in
1301
+ * `signIn`) and only records confirmed successes. A wrong skip is
1302
+ * self-revealing: the next write to the space fails immediately and loudly
1303
+ * with `404 Space not found`.
1304
+ */
1255
1305
  private ensureOwnedSpaceHostedById;
1256
1306
  /**
1257
1307
  * Host one of this user's owned spaces by name (e.g. `"applications"`).
@@ -1462,6 +1512,14 @@ declare class TinyCloudNode {
1462
1512
  */
1463
1513
  private requireEncryptionSession;
1464
1514
  private createEncryptionCrypto;
1515
+ /**
1516
+ * The node DID for {@link config.host}.
1517
+ *
1518
+ * Sign-in already performs `GET /info` for the protocol-version check, and
1519
+ * that response carries `nodeId`, so reuse it rather than fetching `/info` a
1520
+ * second time. Only a value recorded for this exact host qualifies; older
1521
+ * nodes that omit `nodeId` fall through to the fetch.
1522
+ */
1465
1523
  private fetchNodeId;
1466
1524
  private signRawNetworkAuthorization;
1467
1525
  private fetchEncryptionNetworkAt;
@@ -1586,7 +1644,13 @@ declare class TinyCloudNode {
1586
1644
  get encryption(): IEncryptionService;
1587
1645
  getEncryptionNetwork(nameOrNetworkId?: string): Promise<NetworkDescriptor | null>;
1588
1646
  createEncryptionNetwork(name?: string): Promise<NetworkDescriptor>;
1589
- ensureEncryptionNetwork(nameOrNetworkId?: string): Promise<NetworkDescriptor>;
1647
+ /**
1648
+ * Ensure an encryption network exists, creating it if necessary.
1649
+ *
1650
+ * @param nameOrNetworkId - Network name or full `urn:tinycloud:encryption:` id.
1651
+ * @param options - See {@link EnsureEncryptionNetworkOptions}.
1652
+ */
1653
+ ensureEncryptionNetwork(nameOrNetworkId?: string, options?: EnsureEncryptionNetworkOptions): Promise<NetworkDescriptor>;
1590
1654
  /**
1591
1655
  * App-facing secrets API backed by the `secrets` space vault.
1592
1656
  */
@@ -2168,4 +2232,4 @@ declare class WasmKeyProvider implements KeyProvider {
2168
2232
  */
2169
2233
  declare function createWasmKeyProvider(sessionManager: SessionManagerWithListing): WasmKeyProvider;
2170
2234
 
2171
- export { type AuthDelegationArtifact as A, type CreateOwnerDelegationParams as C, type DelegateToOptions as D, FileSessionStorage as F, MemorySessionStorage as M, type NodeEventEmitterStrategy as N, type OwnerDelegationReceipt as O, type PortableDelegation as P, type RestorableSession as R, type SecretPermissionHint as S, TinyCloudNode as T, UnsupportedSessionRestoreError as U, type ValidatedRuntimeDelegation as V, WasmKeyProvider as W, type AuthRequestArtifact as a, type DelegateToResult as b, DelegatedAccess as c, type DelegationAuthority as d, NodeUserAuthorization as e, type NodeUserAuthorizationConfig as f, type RuntimeDelegationActivator as g, type RuntimePermissionGrantOptions as h, type SecretReadInput as i, type SecretReadResult as j, type SignStrategy as k, type TinyCloudNodeConfig as l, type WasmKeyProviderConfig as m, activateValidatedRuntimeDelegation as n, createWasmKeyProvider as o, defaultSignStrategy as p, deserializeDelegation as q, grantAuthRequest as r, serializeDelegation as s };
2235
+ export { type AuthDelegationArtifact as A, type CreateOwnerDelegationParams as C, type DelegateToOptions as D, type EnsureEncryptionNetworkOptions as E, FileSessionStorage as F, MemorySessionStorage as M, type NodeEventEmitterStrategy as N, type OwnerDelegationReceipt as O, type PortableDelegation as P, type RestorableSession as R, type SecretPermissionHint as S, TinyCloudNode as T, UnsupportedSessionRestoreError as U, type ValidatedRuntimeDelegation as V, WasmKeyProvider as W, type AuthRequestArtifact as a, type DelegateToResult as b, DelegatedAccess as c, type DelegationAuthority as d, NodeUserAuthorization as e, type NodeUserAuthorizationConfig as f, type RuntimeDelegationActivator as g, type RuntimePermissionGrantOptions as h, type SecretReadInput as i, type SecretReadResult as j, type SignStrategy as k, type TinyCloudNodeConfig as l, type WasmKeyProviderConfig as m, activateValidatedRuntimeDelegation as n, createWasmKeyProvider as o, defaultSignStrategy as p, deserializeDelegation as q, grantAuthRequest as r, serializeDelegation as s };
@@ -319,6 +319,12 @@ declare class NodeUserAuthorization implements IUserAuthorization {
319
319
  private _address?;
320
320
  private _chainId?;
321
321
  private _nodeFeatures;
322
+ /**
323
+ * `nodeId` from the `GET /info` performed during sign-in/restore, tagged with
324
+ * the host it came from. Callers that need the node DID (encryption-network
325
+ * admin invocations) reuse this instead of re-fetching `/info`.
326
+ */
327
+ private _nodeInfoIdentity?;
322
328
  private _lastActivationSkippedSpaceIds;
323
329
  constructor(config: NodeUserAuthorizationConfig);
324
330
  /**
@@ -387,6 +393,13 @@ declare class NodeUserAuthorization implements IUserAuthorization {
387
393
  private requireTinyCloudHosts;
388
394
  private get primaryTinyCloudHost();
389
395
  get nodeFeatures(): string[];
396
+ /**
397
+ * The node DID reported by `GET /info` for `host`, if the sign-in (or
398
+ * restore) that populated it targeted that same host. Returns `undefined`
399
+ * for any other host, and for nodes whose `/info` omits `nodeId`, so callers
400
+ * fall back to fetching it themselves rather than using a foreign node's DID.
401
+ */
402
+ nodeIdForHost(host: string): string | undefined;
390
403
  get lastActivationSkippedSpaceIds(): string[];
391
404
  /**
392
405
  * Compute the `abilities` map the WASM `prepareSession` call should
@@ -987,6 +1000,21 @@ interface RuntimePermissionGrantOptions {
987
1000
  /** Override expiry. ms-format string ("7d", "1h") or raw milliseconds. */
988
1001
  expiry?: string | number;
989
1002
  }
1003
+ interface EnsureEncryptionNetworkOptions {
1004
+ /**
1005
+ * Skip the `GET /encryption/networks/{id}` existence probe and go straight
1006
+ * to the create.
1007
+ *
1008
+ * The probe pays for itself only when the network already exists. On a
1009
+ * freshly bootstrapped account it is a guaranteed 404, so it costs a round
1010
+ * trip on the cold sign-in path and saves nothing. Creating without it is
1011
+ * safe: the server answers `409 Conflict` if the network is already there,
1012
+ * which {@link TinyCloudNode.createEncryptionNetwork} resolves by reading the
1013
+ * existing descriptor. Set this only when the caller knows the account was
1014
+ * just provisioned.
1015
+ */
1016
+ assumeMissing?: boolean;
1017
+ }
990
1018
  /**
991
1019
  * High-level TinyCloud API for Node.js environments.
992
1020
  *
@@ -1044,6 +1072,12 @@ declare class TinyCloudNode {
1044
1072
  * so this avoids re-parsing the recap on every registration.
1045
1073
  */
1046
1074
  private _recapOperationsCache?;
1075
+ /**
1076
+ * Owned space ids this sign-in has already confirmed are hosted. Consulted by
1077
+ * {@link ensureOwnedSpaceHostedById}; cleared on every {@link signIn} because
1078
+ * hosting is confirmed against the session that was active at the time.
1079
+ */
1080
+ private readonly confirmedHostedSpaceIds;
1047
1081
  /**
1048
1082
  * TinyCloudSession captured by {@link restoreSession} when there's no
1049
1083
  * auth-layer signer available (session-only mode used by OpenKey-backed
@@ -1252,6 +1286,22 @@ declare class TinyCloudNode {
1252
1286
  private withAccountRegistryRetry;
1253
1287
  private requestedEncryptionNetworkIds;
1254
1288
  private ensureRequestedEncryptionNetworks;
1289
+ /**
1290
+ * Ensure one of this user's owned spaces is hosted, at most once per space
1291
+ * per sign-in.
1292
+ *
1293
+ * Account bootstrap funnels six calls through here (via
1294
+ * `AccountService.ensureAccountSpaceHosted`), and each one re-submits
1295
+ * `activateSessionWithHost` with the *primary* session. That session's recap
1296
+ * covers only `default` and `secrets`, so the account space appears in
1297
+ * neither `activated` nor `skipped` and the guard below returns immediately
1298
+ * — the repeat calls cannot change the outcome, they only cost round trips.
1299
+ *
1300
+ * The memo is per sign-in ({@link confirmedHostedSpaceIds} is cleared in
1301
+ * `signIn`) and only records confirmed successes. A wrong skip is
1302
+ * self-revealing: the next write to the space fails immediately and loudly
1303
+ * with `404 Space not found`.
1304
+ */
1255
1305
  private ensureOwnedSpaceHostedById;
1256
1306
  /**
1257
1307
  * Host one of this user's owned spaces by name (e.g. `"applications"`).
@@ -1462,6 +1512,14 @@ declare class TinyCloudNode {
1462
1512
  */
1463
1513
  private requireEncryptionSession;
1464
1514
  private createEncryptionCrypto;
1515
+ /**
1516
+ * The node DID for {@link config.host}.
1517
+ *
1518
+ * Sign-in already performs `GET /info` for the protocol-version check, and
1519
+ * that response carries `nodeId`, so reuse it rather than fetching `/info` a
1520
+ * second time. Only a value recorded for this exact host qualifies; older
1521
+ * nodes that omit `nodeId` fall through to the fetch.
1522
+ */
1465
1523
  private fetchNodeId;
1466
1524
  private signRawNetworkAuthorization;
1467
1525
  private fetchEncryptionNetworkAt;
@@ -1586,7 +1644,13 @@ declare class TinyCloudNode {
1586
1644
  get encryption(): IEncryptionService;
1587
1645
  getEncryptionNetwork(nameOrNetworkId?: string): Promise<NetworkDescriptor | null>;
1588
1646
  createEncryptionNetwork(name?: string): Promise<NetworkDescriptor>;
1589
- ensureEncryptionNetwork(nameOrNetworkId?: string): Promise<NetworkDescriptor>;
1647
+ /**
1648
+ * Ensure an encryption network exists, creating it if necessary.
1649
+ *
1650
+ * @param nameOrNetworkId - Network name or full `urn:tinycloud:encryption:` id.
1651
+ * @param options - See {@link EnsureEncryptionNetworkOptions}.
1652
+ */
1653
+ ensureEncryptionNetwork(nameOrNetworkId?: string, options?: EnsureEncryptionNetworkOptions): Promise<NetworkDescriptor>;
1590
1654
  /**
1591
1655
  * App-facing secrets API backed by the `secrets` space vault.
1592
1656
  */
@@ -2168,4 +2232,4 @@ declare class WasmKeyProvider implements KeyProvider {
2168
2232
  */
2169
2233
  declare function createWasmKeyProvider(sessionManager: SessionManagerWithListing): WasmKeyProvider;
2170
2234
 
2171
- export { type AuthDelegationArtifact as A, type CreateOwnerDelegationParams as C, type DelegateToOptions as D, FileSessionStorage as F, MemorySessionStorage as M, type NodeEventEmitterStrategy as N, type OwnerDelegationReceipt as O, type PortableDelegation as P, type RestorableSession as R, type SecretPermissionHint as S, TinyCloudNode as T, UnsupportedSessionRestoreError as U, type ValidatedRuntimeDelegation as V, WasmKeyProvider as W, type AuthRequestArtifact as a, type DelegateToResult as b, DelegatedAccess as c, type DelegationAuthority as d, NodeUserAuthorization as e, type NodeUserAuthorizationConfig as f, type RuntimeDelegationActivator as g, type RuntimePermissionGrantOptions as h, type SecretReadInput as i, type SecretReadResult as j, type SignStrategy as k, type TinyCloudNodeConfig as l, type WasmKeyProviderConfig as m, activateValidatedRuntimeDelegation as n, createWasmKeyProvider as o, defaultSignStrategy as p, deserializeDelegation as q, grantAuthRequest as r, serializeDelegation as s };
2235
+ export { type AuthDelegationArtifact as A, type CreateOwnerDelegationParams as C, type DelegateToOptions as D, type EnsureEncryptionNetworkOptions as E, FileSessionStorage as F, MemorySessionStorage as M, type NodeEventEmitterStrategy as N, type OwnerDelegationReceipt as O, type PortableDelegation as P, type RestorableSession as R, type SecretPermissionHint as S, TinyCloudNode as T, UnsupportedSessionRestoreError as U, type ValidatedRuntimeDelegation as V, WasmKeyProvider as W, type AuthRequestArtifact as a, type DelegateToResult as b, DelegatedAccess as c, type DelegationAuthority as d, NodeUserAuthorization as e, type NodeUserAuthorizationConfig as f, type RuntimeDelegationActivator as g, type RuntimePermissionGrantOptions as h, type SecretReadInput as i, type SecretReadResult as j, type SignStrategy as k, type TinyCloudNodeConfig as l, type WasmKeyProviderConfig as m, activateValidatedRuntimeDelegation as n, createWasmKeyProvider as o, defaultSignStrategy as p, deserializeDelegation as q, grantAuthRequest as r, serializeDelegation as s };
package/dist/core.cjs CHANGED
@@ -551,6 +551,15 @@ var NodeUserAuthorization = class {
551
551
  get nodeFeatures() {
552
552
  return this._nodeFeatures;
553
553
  }
554
+ /**
555
+ * The node DID reported by `GET /info` for `host`, if the sign-in (or
556
+ * restore) that populated it targeted that same host. Returns `undefined`
557
+ * for any other host, and for nodes whose `/info` omits `nodeId`, so callers
558
+ * fall back to fetching it themselves rather than using a foreign node's DID.
559
+ */
560
+ nodeIdForHost(host) {
561
+ return this._nodeInfoIdentity?.host === host ? this._nodeInfoIdentity.nodeId : void 0;
562
+ }
554
563
  get lastActivationSkippedSpaceIds() {
555
564
  return [...this._lastActivationSkippedSpaceIds];
556
565
  }
@@ -1020,11 +1029,13 @@ var NodeUserAuthorization = class {
1020
1029
  this._tinyCloudSession = tinyCloudSession;
1021
1030
  this._address = address;
1022
1031
  this._chainId = chainId;
1032
+ const infoHost = this.primaryTinyCloudHost;
1023
1033
  const nodeInfo = await (0, import_sdk_core3.checkNodeInfo)(
1024
- this.primaryTinyCloudHost,
1034
+ infoHost,
1025
1035
  this.wasm.protocolVersion()
1026
1036
  );
1027
1037
  this._nodeFeatures = nodeInfo.features;
1038
+ this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
1028
1039
  for (const ext of this.extensions) {
1029
1040
  if (ext.afterSignIn) {
1030
1041
  await ext.afterSignIn(clientSession);
@@ -1190,11 +1201,13 @@ var NodeUserAuthorization = class {
1190
1201
  this._tinyCloudSession = tinyCloudSession;
1191
1202
  this._address = address;
1192
1203
  this._chainId = chainId;
1204
+ const infoHost = this.primaryTinyCloudHost;
1193
1205
  const nodeInfo = await (0, import_sdk_core3.checkNodeInfo)(
1194
- this.primaryTinyCloudHost,
1206
+ infoHost,
1195
1207
  this.wasm.protocolVersion()
1196
1208
  );
1197
1209
  this._nodeFeatures = nodeInfo.features;
1210
+ this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
1198
1211
  for (const ext of this.extensions) {
1199
1212
  if (ext.afterSignIn) {
1200
1213
  await ext.afterSignIn(clientSession);
@@ -2088,6 +2101,12 @@ var _TinyCloudNode = class _TinyCloudNode {
2088
2101
  this._baseSecrets = /* @__PURE__ */ new Map();
2089
2102
  this._secrets = /* @__PURE__ */ new Map();
2090
2103
  this.runtimePermissionGrants = [];
2104
+ /**
2105
+ * Owned space ids this sign-in has already confirmed are hosted. Consulted by
2106
+ * {@link ensureOwnedSpaceHostedById}; cleared on every {@link signIn} because
2107
+ * hosting is confirmed against the session that was active at the time.
2108
+ */
2109
+ this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
2091
2110
  /**
2092
2111
  * True when the last signIn() detected an interactive signer and skipped
2093
2112
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -2484,6 +2503,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2484
2503
  this._spaceService = void 0;
2485
2504
  this._serviceContext = void 0;
2486
2505
  this.runtimePermissionGrants = [];
2506
+ this.confirmedHostedSpaceIds.clear();
2487
2507
  await this.tc.signIn(options);
2488
2508
  this.syncResolvedHostFromAuth();
2489
2509
  const oldGraph = this._serviceGraph;
@@ -2666,14 +2686,18 @@ var _TinyCloudNode = class _TinyCloudNode {
2666
2686
  }
2667
2687
  if (step.kind === "seed-applications") {
2668
2688
  const registered = await this.account.applications.register(
2669
- step.manifests.length > 0 ? [...step.manifests] : import_sdk_core8.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST
2689
+ step.manifests.length > 0 ? [...step.manifests] : import_sdk_core8.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
2690
+ // Bootstrap only runs on an account with no registry records, so the
2691
+ // manifest-hash pre-read is definitionally a miss. Skip it; the write
2692
+ // it guards is an INSERT OR REPLACE and is safe to repeat.
2693
+ { assumeUnregistered: true }
2670
2694
  );
2671
2695
  if (!registered.ok) {
2672
2696
  throw new Error(`Failed to seed bootstrap applications: ${registered.error.message}`);
2673
2697
  }
2674
2698
  }
2675
2699
  if (step.kind === "encryption-network-create") {
2676
- await this.ensureEncryptionNetwork(step.networkId);
2700
+ await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: true });
2677
2701
  }
2678
2702
  if (step.kind === "secret-records-schema") {
2679
2703
  const db = this.sqlForSpace(step.spaceId).db(step.database);
@@ -2952,6 +2976,22 @@ var _TinyCloudNode = class _TinyCloudNode {
2952
2976
  await this.ensureEncryptionNetwork(networkId);
2953
2977
  }
2954
2978
  }
2979
+ /**
2980
+ * Ensure one of this user's owned spaces is hosted, at most once per space
2981
+ * per sign-in.
2982
+ *
2983
+ * Account bootstrap funnels six calls through here (via
2984
+ * `AccountService.ensureAccountSpaceHosted`), and each one re-submits
2985
+ * `activateSessionWithHost` with the *primary* session. That session's recap
2986
+ * covers only `default` and `secrets`, so the account space appears in
2987
+ * neither `activated` nor `skipped` and the guard below returns immediately
2988
+ * — the repeat calls cannot change the outcome, they only cost round trips.
2989
+ *
2990
+ * The memo is per sign-in ({@link confirmedHostedSpaceIds} is cleared in
2991
+ * `signIn`) and only records confirmed successes. A wrong skip is
2992
+ * self-revealing: the next write to the space fails immediately and loudly
2993
+ * with `404 Space not found`.
2994
+ */
2955
2995
  async ensureOwnedSpaceHostedById(spaceId) {
2956
2996
  if (!this.auth) {
2957
2997
  throw new Error("Owned space hosting requires wallet mode");
@@ -2960,12 +3000,16 @@ var _TinyCloudNode = class _TinyCloudNode {
2960
3000
  if (!session) {
2961
3001
  throw new Error("Owned space hosting requires an active session");
2962
3002
  }
3003
+ if (this.confirmedHostedSpaceIds.has(spaceId)) {
3004
+ return;
3005
+ }
2963
3006
  const host = this.hosts[0] ?? this.config.host;
2964
3007
  if (!host) {
2965
3008
  throw new Error("Owned space hosting requires a TinyCloud host");
2966
3009
  }
2967
3010
  const activation = await (0, import_sdk_core8.activateSessionWithHost)(host, session.delegationHeader);
2968
3011
  if (activation.success && !activation.skipped?.includes(spaceId)) {
3012
+ this.confirmedHostedSpaceIds.add(spaceId);
2969
3013
  return;
2970
3014
  }
2971
3015
  if (!activation.success && activation.status !== 404) {
@@ -2984,6 +3028,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2984
3028
  `Failed to activate session after creating owned space ${spaceId}: ${retry.error ?? "space was skipped"}`
2985
3029
  );
2986
3030
  }
3031
+ this.confirmedHostedSpaceIds.add(spaceId);
2987
3032
  }
2988
3033
  /**
2989
3034
  * Host one of this user's owned spaces by name (e.g. `"applications"`).
@@ -3306,6 +3351,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3306
3351
  this.tc = stagedGraph.core;
3307
3352
  this._recapOperationsCache = stagedPrimary.cache;
3308
3353
  this.runtimePermissionGrants = stagedPrimary.grants;
3354
+ this.confirmedHostedSpaceIds.clear();
3309
3355
  if (this.auth) {
3310
3356
  this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost]);
3311
3357
  this._restoredTcSession = void 0;
@@ -3888,7 +3934,17 @@ var _TinyCloudNode = class _TinyCloudNode {
3888
3934
  verifyNodeSignature: (nodeId, message, signature) => (0, import_sdk_core8.verifyDidKeyEd25519Signature)(nodeId, message, signature)
3889
3935
  };
3890
3936
  }
3937
+ /**
3938
+ * The node DID for {@link config.host}.
3939
+ *
3940
+ * Sign-in already performs `GET /info` for the protocol-version check, and
3941
+ * that response carries `nodeId`, so reuse it rather than fetching `/info` a
3942
+ * second time. Only a value recorded for this exact host qualifies; older
3943
+ * nodes that omit `nodeId` fall through to the fetch.
3944
+ */
3891
3945
  async fetchNodeId() {
3946
+ const cached = this.auth?.nodeIdForHost(this.config.host);
3947
+ if (cached) return cached;
3892
3948
  const response = await fetch(`${this.config.host}/info`);
3893
3949
  if (!response.ok) {
3894
3950
  throw new Error(`Failed to fetch node info: HTTP ${response.status}`);
@@ -4660,6 +4716,13 @@ var _TinyCloudNode = class _TinyCloudNode {
4660
4716
  body
4661
4717
  )
4662
4718
  });
4719
+ if (response.status === 409) {
4720
+ const existing = await this.getEncryptionNetwork(networkId);
4721
+ if (existing) return existing;
4722
+ throw new Error(
4723
+ `Encryption network ${networkId} already exists but could not be read back`
4724
+ );
4725
+ }
4663
4726
  if (!response.ok) {
4664
4727
  throw new Error(
4665
4728
  `Failed to create encryption network ${networkId}: HTTP ${response.status} ${await response.text()}`
@@ -4668,11 +4731,19 @@ var _TinyCloudNode = class _TinyCloudNode {
4668
4731
  const created = await response.json();
4669
4732
  return created.descriptor;
4670
4733
  }
4671
- async ensureEncryptionNetwork(nameOrNetworkId = DEFAULT_ENCRYPTION_NETWORK_NAME) {
4734
+ /**
4735
+ * Ensure an encryption network exists, creating it if necessary.
4736
+ *
4737
+ * @param nameOrNetworkId - Network name or full `urn:tinycloud:encryption:` id.
4738
+ * @param options - See {@link EnsureEncryptionNetworkOptions}.
4739
+ */
4740
+ async ensureEncryptionNetwork(nameOrNetworkId = DEFAULT_ENCRYPTION_NETWORK_NAME, options = {}) {
4672
4741
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
4673
- const existing = await this.getEncryptionNetwork(networkId);
4674
- if (existing) {
4675
- return existing;
4742
+ if (!options.assumeMissing) {
4743
+ const existing = await this.getEncryptionNetwork(networkId);
4744
+ if (existing) {
4745
+ return existing;
4746
+ }
4676
4747
  }
4677
4748
  const parsed = (0, import_sdk_core8.parseNetworkId)(networkId);
4678
4749
  if (!didPrincipalMatches2(parsed.ownerDid, this.did)) {