@tinycloud/node-sdk 3.2.6-beta.1 → 3.2.6-beta.2

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.
package/dist/core.cjs CHANGED
@@ -461,6 +461,13 @@ function cloneAbilitiesMap(actions) {
461
461
  var NodeUserAuthorization = class {
462
462
  constructor(config) {
463
463
  this.activeSessionKeyId = "default";
464
+ /** Incremented by resetSession; a sign-in started earlier publishes nothing. */
465
+ this.sessionGeneration = 0;
466
+ /**
467
+ * Sign-in saves run one at a time, in order, each together with its own
468
+ * cancellation cleanup, so no later write can land between the two.
469
+ */
470
+ this.persistenceQueue = Promise.resolve();
464
471
  this.extensions = [];
465
472
  this._nodeFeatures = [];
466
473
  this._nodeFeaturesKnown = false;
@@ -591,6 +598,27 @@ var NodeUserAuthorization = class {
591
598
  this.tinycloudHosts = [...hosts];
592
599
  }
593
600
  }
601
+ /**
602
+ * @internal End the session: adopt a fresh session manager whose active key
603
+ * is `sessionKeyId`, and return to the constructed state.
604
+ */
605
+ resetSession(sessionManager, sessionKeyId, hosts) {
606
+ this.sessionGeneration++;
607
+ this.installRestoredSession(sessionManager, void 0, hosts);
608
+ this.activeSessionKeyId = sessionKeyId;
609
+ }
610
+ /** Throws when the session was reset (signed out) after `generation` was read. */
611
+ assertSignInCurrent(generation) {
612
+ if (generation !== this.sessionGeneration) {
613
+ throw new Error("Sign-in was cancelled by sign-out.");
614
+ }
615
+ }
616
+ /** Runs `task` after every earlier queued persistence task has settled. */
617
+ enqueuePersistence(task) {
618
+ const run = this.persistenceQueue.then(task);
619
+ this.persistenceQueue = run.catch(() => void 0);
620
+ return run;
621
+ }
594
622
  /** @internal Atomically adopt the state staged by TinyCloudNode.restoreSession. */
595
623
  installRestoredSession(sessionManager, session, hosts, nodeFeatures) {
596
624
  this.sessionManager = sessionManager;
@@ -1279,11 +1307,14 @@ var NodeUserAuthorization = class {
1279
1307
  * @param options - Optional per-call SIWE overrides for this sign-in only
1280
1308
  */
1281
1309
  async signIn(options) {
1282
- this._address = (0, import_sdk_core4.canonicalizeAddress)(await this.signer.getAddress());
1283
- this._chainId = await this.signer.getChainId();
1284
- const address = this._address;
1285
- const chainId = this._chainId;
1310
+ const generation = this.sessionGeneration;
1311
+ const address = (0, import_sdk_core4.canonicalizeAddress)(await this.signer.getAddress());
1312
+ const chainId = await this.signer.getChainId();
1313
+ this.assertSignInCurrent(generation);
1314
+ this._address = address;
1315
+ this._chainId = chainId;
1286
1316
  await this.resolveTinyCloudHostsForSignIn(address, chainId);
1317
+ this.assertSignInCurrent(generation);
1287
1318
  const keyId = `session-${Date.now()}`;
1288
1319
  this.sessionManager.renameSessionKeyId(this.activeSessionKeyId, keyId);
1289
1320
  this.activeSessionKeyId = keyId;
@@ -1316,6 +1347,7 @@ var NodeUserAuthorization = class {
1316
1347
  type: "siwe",
1317
1348
  purpose: "sign-in"
1318
1349
  });
1350
+ this.assertSignInCurrent(generation);
1319
1351
  const session = this.wasm.completeSessionSetup({
1320
1352
  ...prepared,
1321
1353
  signature
@@ -1360,18 +1392,27 @@ var NodeUserAuthorization = class {
1360
1392
  version: "1.0",
1361
1393
  tinycloudHosts: this.tinycloudHosts
1362
1394
  };
1363
- await this.sessionStorage.save(address, persistedData);
1395
+ await this.enqueuePersistence(async () => {
1396
+ await this.sessionStorage.save(address, persistedData);
1397
+ if (generation !== this.sessionGeneration) {
1398
+ await this.sessionStorage.clear(address);
1399
+ }
1400
+ });
1401
+ this.assertSignInCurrent(generation);
1364
1402
  this._session = clientSession;
1365
1403
  this._tinyCloudSession = tinyCloudSession;
1366
1404
  this._address = address;
1367
1405
  this._chainId = chainId;
1368
1406
  await this.refreshNodeInfo();
1407
+ this.assertSignInCurrent(generation);
1369
1408
  for (const ext of this.extensions) {
1370
1409
  if (ext.afterSignIn) {
1371
1410
  await ext.afterSignIn(clientSession);
1411
+ this.assertSignInCurrent(generation);
1372
1412
  }
1373
1413
  }
1374
1414
  await this.ensureSpaceExists();
1415
+ this.assertSignInCurrent(generation);
1375
1416
  return clientSession;
1376
1417
  }
1377
1418
  /**
@@ -2533,17 +2574,24 @@ var import_sdk_core6 = require("@tinycloud/sdk-core");
2533
2574
  // src/DelegatedAccess.ts
2534
2575
  var import_sdk_core7 = require("@tinycloud/sdk-core");
2535
2576
  var DelegatedAccess = class {
2536
- constructor(session, delegation, host, invoke, invokeAny, telemetry) {
2577
+ /**
2578
+ * @param lifetime - Optional owner of this access: its `fetch` is used for
2579
+ * requests and the service context is registered with `track`, so the
2580
+ * owner can cancel and retire the access (TinyCloudNode does this when its
2581
+ * session is replaced or signed out).
2582
+ */
2583
+ constructor(session, delegation, host, invoke, invokeAny, telemetry, lifetime) {
2537
2584
  this.session = session;
2538
2585
  this._delegation = delegation;
2539
2586
  this.host = host;
2540
- this._serviceContext = new import_sdk_core7.ServiceContext({
2587
+ const context = new import_sdk_core7.ServiceContext({
2541
2588
  invoke,
2542
2589
  invokeAny,
2543
- fetch: globalThis.fetch.bind(globalThis),
2590
+ fetch: lifetime?.fetch ?? globalThis.fetch.bind(globalThis),
2544
2591
  hosts: [host],
2545
2592
  telemetry
2546
2593
  });
2594
+ this._serviceContext = lifetime ? lifetime.track(context) : context;
2547
2595
  const prefix = this._delegation.path.replace(/\/$/, "");
2548
2596
  this._kv = new import_sdk_core7.KVService({ prefix });
2549
2597
  this._kv.initialize(this._serviceContext);
@@ -3117,6 +3165,19 @@ function exactVerifiedRecap(verified) {
3117
3165
  caveats: cloneRecapCaveats(entry.caveats)
3118
3166
  }));
3119
3167
  }
3168
+ function createDefaultSessionKey(wasm) {
3169
+ const manager = wasm.createSessionManager();
3170
+ let keyId = "default";
3171
+ let jwkStr = manager.jwk(keyId);
3172
+ if (!jwkStr) {
3173
+ keyId = manager.createSessionKey(keyId);
3174
+ jwkStr = manager.jwk(keyId);
3175
+ }
3176
+ if (!jwkStr) {
3177
+ throw new Error("Failed to get session key JWK");
3178
+ }
3179
+ return { manager, keyId, jwk: JSON.parse(jwkStr) };
3180
+ }
3120
3181
  var ServiceGraphLifetime = class {
3121
3182
  constructor(invokeFn, invokeAnyFn, fetchFn, observeContext) {
3122
3183
  this.invokeFn = invokeFn;
@@ -3166,9 +3227,13 @@ var ServiceGraphLifetime = class {
3166
3227
  this.contexts.clear();
3167
3228
  }
3168
3229
  }
3230
+ /** Aborted when the graph retires: cancels requests made for its session. */
3231
+ get signal() {
3232
+ return this.abortController.signal;
3233
+ }
3169
3234
  assertActive() {
3170
3235
  if (this.retired) {
3171
- throw new Error("Service graph has been retired by session replacement.");
3236
+ throw new Error("Service graph has been retired: its session was replaced or signed out.");
3172
3237
  }
3173
3238
  }
3174
3239
  combineSignals(signal) {
@@ -3470,6 +3535,16 @@ var _TinyCloudNode = class _TinyCloudNode {
3470
3535
  * so it must outlast a slow but healthy sync (9–18 s on prod 1.20.2, TC-998).
3471
3536
  */
3472
3537
  this.accountRegistryDeadlineMs = 3e4;
3538
+ /**
3539
+ * Incremented by every sign-out. A sign-in or restore records it when it
3540
+ * starts and does not establish its session once it has changed.
3541
+ */
3542
+ this.sessionEpoch = 0;
3543
+ /**
3544
+ * Numbers sign-in and restore attempts. A cancelled attempt cleans up only
3545
+ * while it is still the latest, so it never ends a newer session.
3546
+ */
3547
+ this.latestSessionAttempt = 0;
3473
3548
  /**
3474
3549
  * True when the last signIn() detected an interactive signer and skipped
3475
3550
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -3603,54 +3678,17 @@ var _TinyCloudNode = class _TinyCloudNode {
3603
3678
  "wasmBindings must be provided in config. Import from '@tinycloud/node-sdk' (not '/core') for automatic Node.js defaults."
3604
3679
  );
3605
3680
  }
3606
- this.sessionManager = this.wasmBindings.createSessionManager();
3607
- const defaultKeyId = "default";
3608
- let jwkStr = this.sessionManager.jwk(defaultKeyId);
3609
- if (jwkStr) {
3610
- this.sessionKeyId = defaultKeyId;
3611
- } else {
3612
- this.sessionKeyId = this.sessionManager.createSessionKey(defaultKeyId);
3613
- jwkStr = this.sessionManager.jwk(this.sessionKeyId);
3614
- }
3615
- if (!jwkStr) {
3616
- throw new Error("Failed to get session key JWK");
3617
- }
3618
- this.sessionKeyJwk = JSON.parse(jwkStr);
3681
+ const sessionKey = createDefaultSessionKey(this.wasmBindings);
3682
+ this.sessionManager = sessionKey.manager;
3683
+ this.sessionKeyId = sessionKey.keyId;
3684
+ this.sessionKeyJwk = sessionKey.jwk;
3619
3685
  this._serviceGraph = this.createServiceGraphLifetime();
3620
3686
  this._capabilityRegistry = new import_sdk_core10.CapabilityKeyRegistry();
3621
3687
  this._keyProvider = new WasmKeyProvider({
3622
3688
  sessionManager: this.sessionManager
3623
3689
  });
3624
3690
  this.notificationHandler = config.notificationHandler ?? new import_sdk_core10.SilentNotificationHandler();
3625
- const receiveOnlyGraph = this._serviceGraph;
3626
- this._sharingService = new import_sdk_core10.SharingService({
3627
- hosts: [this.config.host],
3628
- // session: undefined - not needed for receive()
3629
- invoke: receiveOnlyGraph.invoke,
3630
- fetch: receiveOnlyGraph.fetch,
3631
- assertActive: () => receiveOnlyGraph.assertActive(),
3632
- keyProvider: this._keyProvider,
3633
- registry: this._capabilityRegistry,
3634
- createDelegationWasm: (params) => this.createDelegationWrapper(params),
3635
- computeCid: (data, codec) => {
3636
- if (!this.wasmBindings.computeCid) throw new Error("computeCid is unavailable");
3637
- return this.wasmBindings.computeCid(data, codec);
3638
- },
3639
- // delegationManager: undefined - not needed for receive()
3640
- createKVService: (config2) => {
3641
- const prefix = config2.pathPrefix?.replace(/\/$/, "");
3642
- const kvService = new import_sdk_core10.KVService({ prefix });
3643
- const kvContext = receiveOnlyGraph.track(new import_sdk_core10.ServiceContext({
3644
- invoke: config2.invoke,
3645
- fetch: config2.fetch ?? globalThis.fetch.bind(globalThis),
3646
- hosts: config2.hosts,
3647
- telemetry: this.config.telemetry
3648
- }));
3649
- kvContext.setSession(config2.session);
3650
- kvService.initialize(kvContext);
3651
- return kvService;
3652
- }
3653
- });
3691
+ this._sharingService = this.createReceiveOnlySharing();
3654
3692
  if (config.signer) {
3655
3693
  this.signer = config.signer;
3656
3694
  this.setupAuth(config);
@@ -3792,6 +3830,42 @@ var _TinyCloudNode = class _TinyCloudNode {
3792
3830
  get siweDomain() {
3793
3831
  return this.config.domain ?? "app.tinycloud.xyz";
3794
3832
  }
3833
+ /**
3834
+ * SharingService for receive-only access (no session required), bound to the
3835
+ * current service graph. This lets session-only users receive sharing links
3836
+ * without signIn(); full capabilities (generate) are added after signIn().
3837
+ */
3838
+ createReceiveOnlySharing() {
3839
+ const receiveOnlyGraph = this._serviceGraph;
3840
+ return new import_sdk_core10.SharingService({
3841
+ hosts: [this.config.host],
3842
+ // session: undefined - not needed for receive()
3843
+ invoke: receiveOnlyGraph.invoke,
3844
+ fetch: receiveOnlyGraph.fetch,
3845
+ assertActive: () => receiveOnlyGraph.assertActive(),
3846
+ keyProvider: this._keyProvider,
3847
+ registry: this._capabilityRegistry,
3848
+ createDelegationWasm: (params) => this.createDelegationWrapper(params),
3849
+ computeCid: (data, codec) => {
3850
+ if (!this.wasmBindings.computeCid) throw new Error("computeCid is unavailable");
3851
+ return this.wasmBindings.computeCid(data, codec);
3852
+ },
3853
+ // delegationManager: undefined - not needed for receive()
3854
+ createKVService: (config) => {
3855
+ const prefix = config.pathPrefix?.replace(/\/$/, "");
3856
+ const kvService = new import_sdk_core10.KVService({ prefix });
3857
+ const kvContext = receiveOnlyGraph.track(new import_sdk_core10.ServiceContext({
3858
+ invoke: config.invoke,
3859
+ fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
3860
+ hosts: config.hosts,
3861
+ telemetry: this.config.telemetry
3862
+ }));
3863
+ kvContext.setSession(config.session);
3864
+ kvService.initialize(kvContext);
3865
+ return kvService;
3866
+ }
3867
+ });
3868
+ }
3795
3869
  /**
3796
3870
  * Set up authorization handler and TinyCloud instance.
3797
3871
  * @internal
@@ -4051,6 +4125,102 @@ var _TinyCloudNode = class _TinyCloudNode {
4051
4125
  get restorableSession() {
4052
4126
  return this.currentTinyCloudSession();
4053
4127
  }
4128
+ /**
4129
+ * Sign out and end the session on this instance.
4130
+ *
4131
+ * Every service built for the session (and every `useDelegation()` result)
4132
+ * is retired and the session key is replaced before anything is awaited, so
4133
+ * nothing captured from the session can sign once sign-out has started.
4134
+ * Pending work is aborted and the persisted session is cleared from the
4135
+ * configured session storage. Afterwards session accessors return
4136
+ * `undefined` and service calls fail with "Not signed in" until
4137
+ * {@link signIn} or {@link restoreSession}, which wait for a sign-out in
4138
+ * progress.
4139
+ */
4140
+ signOut() {
4141
+ this.signOutInFlight ?? (this.signOutInFlight = this.endSession().finally(() => {
4142
+ this.signOutInFlight = void 0;
4143
+ }));
4144
+ return this.signOutInFlight;
4145
+ }
4146
+ async endSession() {
4147
+ const leavingAddress = this.address ?? this._restoredTcSession?.address;
4148
+ this.teardownSession();
4149
+ try {
4150
+ await this.accountRegistryTail;
4151
+ if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
4152
+ } finally {
4153
+ if (leavingAddress) await this.clearPersistedSessionFor(leavingAddress);
4154
+ }
4155
+ }
4156
+ /**
4157
+ * Synchronously drop every piece of in-memory session authority and
4158
+ * invalidate sign-ins and restores still in flight (see `sessionEpoch`).
4159
+ */
4160
+ teardownSession() {
4161
+ this.sessionEpoch++;
4162
+ this.pendingAccountRegistrySync?.controller.abort();
4163
+ const oldGraph = this._serviceGraph;
4164
+ const oldCore = this.tc;
4165
+ const sessionKey = createDefaultSessionKey(this.wasmBindings);
4166
+ this.sessionManager = sessionKey.manager;
4167
+ this.sessionKeyId = sessionKey.keyId;
4168
+ this.sessionKeyJwk = sessionKey.jwk;
4169
+ this.auth?.resetSession(
4170
+ sessionKey.manager,
4171
+ sessionKey.keyId,
4172
+ this.explicitHost ? [this.explicitHost] : void 0
4173
+ );
4174
+ this.config.host = this.explicitHost ?? DEFAULT_HOST;
4175
+ this._address = void 0;
4176
+ this._chainId = 1;
4177
+ this._restoredTcSession = void 0;
4178
+ this._restoredNodeFeatures = void 0;
4179
+ this._activeServiceSession = void 0;
4180
+ this._serviceContext = void 0;
4181
+ this._kv = void 0;
4182
+ this._sql = void 0;
4183
+ this._duckdb = void 0;
4184
+ this._hooks = void 0;
4185
+ this._vault = void 0;
4186
+ this._encryption = void 0;
4187
+ this._baseVaults.clear();
4188
+ this._baseSecrets.clear();
4189
+ this._secrets.clear();
4190
+ this._account = void 0;
4191
+ this._publicKV = void 0;
4192
+ this._delegationManager = void 0;
4193
+ this._spaceService = void 0;
4194
+ this.runtimePermissionGrants = [];
4195
+ this._recapOperationsCache = void 0;
4196
+ this.confirmedHostedSpaceIds.clear();
4197
+ this.hostedOwnedSpaceIds.clear();
4198
+ this.cancelOwnedSpaceHosts();
4199
+ this.storageFull.clear();
4200
+ this._bootstrapSkipped = false;
4201
+ this._bootstrapStatus = { skipped: false };
4202
+ this.replicationDelegateSession = void 0;
4203
+ this.replicationScopeError = void 0;
4204
+ this._serviceGraph = this.createServiceGraphLifetime();
4205
+ oldGraph.retire();
4206
+ this.tc = this.auth ? new import_sdk_core10.TinyCloud(this.auth, {
4207
+ invokeAny: this.invokeAnyWithRuntimePermissions,
4208
+ telemetry: this.config.telemetry
4209
+ }) : null;
4210
+ oldCore?.retireServices();
4211
+ this._capabilityRegistry = new import_sdk_core10.CapabilityKeyRegistry();
4212
+ this._keyProvider = new WasmKeyProvider({ sessionManager: this.sessionManager });
4213
+ this._sharingService = this.createReceiveOnlySharing();
4214
+ }
4215
+ async clearPersistedSessionFor(address) {
4216
+ await (this.auth ? this.auth.clearPersistedSession(address) : this.config.sessionStorage?.clear(address));
4217
+ }
4218
+ /** Throws when a sign-out started after `epoch` was read. */
4219
+ assertSessionEpoch(epoch, operation) {
4220
+ if (epoch !== this.sessionEpoch) {
4221
+ throw new Error(`${operation} was cancelled by sign-out.`);
4222
+ }
4223
+ }
4054
4224
  /**
4055
4225
  * Sign in and create a new session.
4056
4226
  * This creates the user's space if it doesn't exist.
@@ -4064,11 +4234,28 @@ var _TinyCloudNode = class _TinyCloudNode {
4064
4234
  "Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
4065
4235
  );
4066
4236
  }
4237
+ const epoch = this.sessionEpoch;
4238
+ const attempt = ++this.latestSessionAttempt;
4239
+ await this.signOutInFlight;
4240
+ try {
4241
+ await this.establishSignIn(epoch, options);
4242
+ } catch (error) {
4243
+ if (epoch !== this.sessionEpoch && attempt === this.latestSessionAttempt) {
4244
+ await this.endSession();
4245
+ }
4246
+ throw error;
4247
+ }
4248
+ }
4249
+ async establishSignIn(epoch, options) {
4250
+ if (!this.signer || !this.tc) {
4251
+ throw new Error("Not signed in. Call signIn() first.");
4252
+ }
4067
4253
  this.pendingAccountRegistrySync?.controller.abort();
4068
4254
  await this.accountRegistryTail;
4069
4255
  await this.wasmBindings.ensureInitialized?.();
4070
4256
  const address = (0, import_sdk_core10.canonicalizeAddress)(await this.signer.getAddress());
4071
4257
  const chainId = await this.signer.getChainId();
4258
+ this.assertSessionEpoch(epoch, "Sign-in");
4072
4259
  if (address !== this._address || chainId !== this._chainId) this.storageFull.clear();
4073
4260
  this._address = address;
4074
4261
  this._chainId = chainId;
@@ -4087,6 +4274,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4087
4274
  this.confirmedHostedSpaceIds.clear();
4088
4275
  this.hostedOwnedSpaceIds.clear();
4089
4276
  await this.tc.signIn(options);
4277
+ this.assertSessionEpoch(epoch, "Sign-in");
4090
4278
  this.cancelOwnedSpaceHosts();
4091
4279
  this.syncResolvedHostFromAuth();
4092
4280
  for (const spaceId of this.auth.lastActivationActivatedSpaceIds ?? []) {
@@ -4095,6 +4283,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4095
4283
  this.replicationDelegateSession = void 0;
4096
4284
  this.replicationScopeError = void 0;
4097
4285
  if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
4286
+ this.assertSessionEpoch(epoch, "Sign-in");
4098
4287
  const oldGraph = this._serviceGraph;
4099
4288
  this._serviceGraph = this.createServiceGraphLifetime();
4100
4289
  oldGraph.retire();
@@ -4105,15 +4294,19 @@ var _TinyCloudNode = class _TinyCloudNode {
4105
4294
  this.sessionKeyJwk = signedInSession.jwk;
4106
4295
  }
4107
4296
  await this.initializeServices();
4297
+ this.assertSessionEpoch(epoch, "Sign-in");
4108
4298
  const primarySession = this.currentTinyCloudSession();
4109
4299
  if (primarySession) {
4110
4300
  this.registerPrimarySessionGrant(primarySession);
4111
4301
  }
4112
4302
  const bootstrapped = await this.bootstrapAccountIfNeeded();
4303
+ this.assertSessionEpoch(epoch, "Sign-in");
4113
4304
  await this.ensureRequestedEncryptionNetworks();
4305
+ this.assertSessionEpoch(epoch, "Sign-in");
4114
4306
  if (!bootstrapped && this.config.manifest === void 0 && this.config.capabilityRequest === void 0) {
4115
4307
  await this.ensureOwnedSpaceHostedById(this.ownedSpaceId(import_sdk_core10.SECRETS_SPACE));
4116
4308
  }
4309
+ this.assertSessionEpoch(epoch, "Sign-in");
4117
4310
  if (!bootstrapped) {
4118
4311
  this.scheduleAccountRegistrySync();
4119
4312
  }
@@ -4295,6 +4488,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4295
4488
  throw new Error("Account bootstrap requires a TinyCloud host");
4296
4489
  }
4297
4490
  const auth = this.auth;
4491
+ const graph = this._serviceGraph;
4298
4492
  const sessions = /* @__PURE__ */ new Map();
4299
4493
  const rawAbilitiesBySpace = /* @__PURE__ */ new Map();
4300
4494
  const warnings = [];
@@ -4303,6 +4497,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4303
4497
  const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === import_sdk_core10.BOOTSTRAP_SESSION_REQUESTS.default;
4304
4498
  for (const step of steps) {
4305
4499
  if (step.kind !== "session") continue;
4500
+ graph.assertActive();
4306
4501
  if (step.space === "default" && canReusePrimaryBootstrapSession && primarySession) {
4307
4502
  sessions.set(step.space, primarySession);
4308
4503
  continue;
@@ -4330,6 +4525,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4330
4525
  }
4331
4526
  for (const step of steps) {
4332
4527
  if (step.kind !== "host") continue;
4528
+ graph.assertActive();
4333
4529
  const hosted = await auth.hostOwnedSpaceResult(step.spaceId, "bootstrap-host");
4334
4530
  if (!hosted.success) {
4335
4531
  throw Object.assign(
@@ -4341,11 +4537,16 @@ var _TinyCloudNode = class _TinyCloudNode {
4341
4537
  await new Promise((resolve) => setTimeout(resolve, 100));
4342
4538
  for (const step of steps) {
4343
4539
  if (step.kind !== "activate") continue;
4540
+ graph.assertActive();
4344
4541
  const session = sessions.get(step.space);
4345
4542
  if (!session) {
4346
4543
  throw new Error(`Missing bootstrap session for ${step.space}`);
4347
4544
  }
4348
- const activated = await (0, import_sdk_core10.activateSessionWithHost)(host, session.delegationHeader);
4545
+ const activated = await (0, import_sdk_core10.activateSessionWithHost)(host, session.delegationHeader, graph.signal).catch((error) => {
4546
+ graph.assertActive();
4547
+ throw error;
4548
+ });
4549
+ graph.assertActive();
4349
4550
  if (!activated.success || activated.skipped?.includes(step.spaceId)) {
4350
4551
  throw Object.assign(
4351
4552
  new Error(
@@ -4361,6 +4562,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4361
4562
  );
4362
4563
  }
4363
4564
  for (const step of steps) {
4565
+ graph.assertActive();
4364
4566
  if (step.kind === "account-index-schema") {
4365
4567
  const ensured = await this.account.index.ensure();
4366
4568
  if (!ensured.ok) {
@@ -5136,6 +5338,9 @@ var _TinyCloudNode = class _TinyCloudNode {
5136
5338
  * @param sessionData - The stored delegation data from the browser flow
5137
5339
  */
5138
5340
  async restoreSession(sessionData) {
5341
+ const epoch = this.sessionEpoch;
5342
+ const attempt = ++this.latestSessionAttempt;
5343
+ await this.signOutInFlight;
5139
5344
  await this.wasmBindings.ensureInitialized?.();
5140
5345
  let restoredJwk;
5141
5346
  try {
@@ -5325,6 +5530,7 @@ var _TinyCloudNode = class _TinyCloudNode {
5325
5530
  });
5326
5531
  this.pendingAccountRegistrySync?.controller.abort();
5327
5532
  await this.accountRegistryTail;
5533
+ this.assertSessionEpoch(epoch, "Session restore");
5328
5534
  const oldGraph = this._serviceGraph;
5329
5535
  const oldCore = this.tc;
5330
5536
  this.sessionManager = stagedManager;
@@ -5370,6 +5576,12 @@ var _TinyCloudNode = class _TinyCloudNode {
5370
5576
  this.replicationDelegateSession = compactReplicationSession;
5371
5577
  this.replicationScopeError = replicationScopeError;
5372
5578
  if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
5579
+ if (epoch !== this.sessionEpoch) {
5580
+ oldGraph.retire();
5581
+ oldCore?.retireServices?.();
5582
+ if (attempt === this.latestSessionAttempt) await this.endSession();
5583
+ this.assertSessionEpoch(epoch, "Session restore");
5584
+ }
5373
5585
  oldGraph.retire();
5374
5586
  if (replicationScopeError) {
5375
5587
  stagedGraph.serviceContext.emit("replication.state", {
@@ -5380,6 +5592,10 @@ var _TinyCloudNode = class _TinyCloudNode {
5380
5592
  }
5381
5593
  await this.bindReplicationRuntime(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
5382
5594
  oldCore?.retireServices?.();
5595
+ if (epoch !== this.sessionEpoch) {
5596
+ if (attempt === this.latestSessionAttempt) await this.endSession();
5597
+ this.assertSessionEpoch(epoch, "Session restore");
5598
+ }
5383
5599
  }
5384
5600
  stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
5385
5601
  if (!session?.siwe) return { cache: void 0, grants: [] };
@@ -7238,6 +7454,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7238
7454
  * matching service calls and downstream `delegateTo()` calls can use it.
7239
7455
  */
7240
7456
  async useRuntimeDelegation(delegation) {
7457
+ const graph = this._serviceGraph;
7241
7458
  const session = this.currentTinyCloudSession();
7242
7459
  if (!session) {
7243
7460
  throw new import_sdk_core10.SessionExpiredError(/* @__PURE__ */ new Date(0));
@@ -7256,9 +7473,11 @@ var _TinyCloudNode = class _TinyCloudNode {
7256
7473
  try {
7257
7474
  activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
7258
7475
  targetHost,
7259
- delegation.delegationHeader
7476
+ delegation.delegationHeader,
7477
+ graph.signal
7260
7478
  );
7261
7479
  } catch (cause) {
7480
+ graph.assertActive();
7262
7481
  throw Object.assign(
7263
7482
  new Error("The TinyCloud host could not be reached to activate the delegation."),
7264
7483
  { code: "ACTIVATION_TRANSPORT_FAILED", cause }
@@ -7278,6 +7497,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7278
7497
  { cause: activateResult }
7279
7498
  );
7280
7499
  }
7500
+ graph.assertActive();
7281
7501
  this.runtimePermissionGrants = this.runtimePermissionGrants.filter(
7282
7502
  (grant) => grant.delegation.cid !== delegation.cid
7283
7503
  );
@@ -7294,6 +7514,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7294
7514
  if (!Array.isArray(permissions) || permissions.length === 0) {
7295
7515
  throw new Error("grantRuntimePermissions requires a non-empty permissions array");
7296
7516
  }
7517
+ const graph = this._serviceGraph;
7297
7518
  const session = this.currentTinyCloudSession();
7298
7519
  if (!session) {
7299
7520
  throw new import_sdk_core10.SessionExpiredError(/* @__PURE__ */ new Date(0));
@@ -7374,14 +7595,19 @@ var _TinyCloudNode = class _TinyCloudNode {
7374
7595
  }
7375
7596
  })();
7376
7597
  const signature = await this.signer.signMessage(prepared.siwe);
7598
+ graph.assertActive();
7377
7599
  const delegatedSession = this.wasmBindings.completeSessionSetup({
7378
7600
  ...prepared,
7379
7601
  signature
7380
7602
  });
7381
7603
  const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
7382
7604
  this.config.host,
7383
- delegatedSession.delegationHeader
7384
- );
7605
+ delegatedSession.delegationHeader,
7606
+ graph.signal
7607
+ ).catch((error) => {
7608
+ graph.assertActive();
7609
+ throw error;
7610
+ });
7385
7611
  if (!activateResult.success) {
7386
7612
  if (rawForDelegation.some((entry) => this.isRawRevocationPermissionEntry(entry)) && activateResult.status === 401 && /Cannot find parent delegation/i.test(activateResult.error ?? "")) {
7387
7613
  throw Object.assign(
@@ -7402,6 +7628,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7402
7628
  expiresAt,
7403
7629
  { siwe: prepared.siwe, signature }
7404
7630
  );
7631
+ graph.assertActive();
7405
7632
  this.runtimePermissionGrants.push({
7406
7633
  session: {
7407
7634
  delegationHeader: delegatedSession.delegationHeader,
@@ -8865,6 +9092,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8865
9092
  * @returns A DelegatedAccess instance for performing operations
8866
9093
  */
8867
9094
  async useDelegation(delegation) {
9095
+ const graph = this._serviceGraph;
8868
9096
  const delegationHeader = delegation.delegationHeader;
8869
9097
  const targetHost = delegation.host ?? this.config.host;
8870
9098
  if (this.isSessionOnly) {
@@ -8906,7 +9134,8 @@ var _TinyCloudNode = class _TinyCloudNode {
8906
9134
  targetHost,
8907
9135
  this.wasmBindings.invoke,
8908
9136
  this.wasmBindings.invokeAny,
8909
- this.config.telemetry
9137
+ this.config.telemetry,
9138
+ graph
8910
9139
  );
8911
9140
  }
8912
9141
  const mySession = this.auth?.tinyCloudSession;
@@ -8930,14 +9159,19 @@ var _TinyCloudNode = class _TinyCloudNode {
8930
9159
  ...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
8931
9160
  });
8932
9161
  const signature = await this.signer.signMessage(prepared.siwe);
9162
+ graph.assertActive();
8933
9163
  const invokerSession = this.wasmBindings.completeSessionSetup({
8934
9164
  ...prepared,
8935
9165
  signature
8936
9166
  });
8937
9167
  const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
8938
9168
  targetHost,
8939
- invokerSession.delegationHeader
8940
- );
9169
+ invokerSession.delegationHeader,
9170
+ graph.signal
9171
+ ).catch((error) => {
9172
+ graph.assertActive();
9173
+ throw error;
9174
+ });
8941
9175
  if (!activateResult.success) {
8942
9176
  throw Object.assign(
8943
9177
  new Error(`Failed to activate delegated session: ${describeHostFailure2(activateResult)}`),
@@ -8956,6 +9190,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8956
9190
  siwe: prepared.siwe,
8957
9191
  signature
8958
9192
  };
9193
+ graph.assertActive();
8959
9194
  this.trackReceivedDelegation(delegation, jwk);
8960
9195
  this.installRuntimeGrantFromServiceSession(
8961
9196
  delegation,
@@ -8974,7 +9209,8 @@ var _TinyCloudNode = class _TinyCloudNode {
8974
9209
  targetHost,
8975
9210
  this.wasmBindings.invoke,
8976
9211
  this.wasmBindings.invokeAny,
8977
- this.config.telemetry
9212
+ this.config.telemetry,
9213
+ graph
8978
9214
  );
8979
9215
  }
8980
9216
  /**