@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.js CHANGED
@@ -389,6 +389,13 @@ function cloneAbilitiesMap(actions) {
389
389
  var NodeUserAuthorization = class {
390
390
  constructor(config) {
391
391
  this.activeSessionKeyId = "default";
392
+ /** Incremented by resetSession; a sign-in started earlier publishes nothing. */
393
+ this.sessionGeneration = 0;
394
+ /**
395
+ * Sign-in saves run one at a time, in order, each together with its own
396
+ * cancellation cleanup, so no later write can land between the two.
397
+ */
398
+ this.persistenceQueue = Promise.resolve();
392
399
  this.extensions = [];
393
400
  this._nodeFeatures = [];
394
401
  this._nodeFeaturesKnown = false;
@@ -519,6 +526,27 @@ var NodeUserAuthorization = class {
519
526
  this.tinycloudHosts = [...hosts];
520
527
  }
521
528
  }
529
+ /**
530
+ * @internal End the session: adopt a fresh session manager whose active key
531
+ * is `sessionKeyId`, and return to the constructed state.
532
+ */
533
+ resetSession(sessionManager, sessionKeyId, hosts) {
534
+ this.sessionGeneration++;
535
+ this.installRestoredSession(sessionManager, void 0, hosts);
536
+ this.activeSessionKeyId = sessionKeyId;
537
+ }
538
+ /** Throws when the session was reset (signed out) after `generation` was read. */
539
+ assertSignInCurrent(generation) {
540
+ if (generation !== this.sessionGeneration) {
541
+ throw new Error("Sign-in was cancelled by sign-out.");
542
+ }
543
+ }
544
+ /** Runs `task` after every earlier queued persistence task has settled. */
545
+ enqueuePersistence(task) {
546
+ const run = this.persistenceQueue.then(task);
547
+ this.persistenceQueue = run.catch(() => void 0);
548
+ return run;
549
+ }
522
550
  /** @internal Atomically adopt the state staged by TinyCloudNode.restoreSession. */
523
551
  installRestoredSession(sessionManager, session, hosts, nodeFeatures) {
524
552
  this.sessionManager = sessionManager;
@@ -1207,11 +1235,14 @@ var NodeUserAuthorization = class {
1207
1235
  * @param options - Optional per-call SIWE overrides for this sign-in only
1208
1236
  */
1209
1237
  async signIn(options) {
1210
- this._address = canonicalizeAddress(await this.signer.getAddress());
1211
- this._chainId = await this.signer.getChainId();
1212
- const address = this._address;
1213
- const chainId = this._chainId;
1238
+ const generation = this.sessionGeneration;
1239
+ const address = canonicalizeAddress(await this.signer.getAddress());
1240
+ const chainId = await this.signer.getChainId();
1241
+ this.assertSignInCurrent(generation);
1242
+ this._address = address;
1243
+ this._chainId = chainId;
1214
1244
  await this.resolveTinyCloudHostsForSignIn(address, chainId);
1245
+ this.assertSignInCurrent(generation);
1215
1246
  const keyId = `session-${Date.now()}`;
1216
1247
  this.sessionManager.renameSessionKeyId(this.activeSessionKeyId, keyId);
1217
1248
  this.activeSessionKeyId = keyId;
@@ -1244,6 +1275,7 @@ var NodeUserAuthorization = class {
1244
1275
  type: "siwe",
1245
1276
  purpose: "sign-in"
1246
1277
  });
1278
+ this.assertSignInCurrent(generation);
1247
1279
  const session = this.wasm.completeSessionSetup({
1248
1280
  ...prepared,
1249
1281
  signature
@@ -1288,18 +1320,27 @@ var NodeUserAuthorization = class {
1288
1320
  version: "1.0",
1289
1321
  tinycloudHosts: this.tinycloudHosts
1290
1322
  };
1291
- await this.sessionStorage.save(address, persistedData);
1323
+ await this.enqueuePersistence(async () => {
1324
+ await this.sessionStorage.save(address, persistedData);
1325
+ if (generation !== this.sessionGeneration) {
1326
+ await this.sessionStorage.clear(address);
1327
+ }
1328
+ });
1329
+ this.assertSignInCurrent(generation);
1292
1330
  this._session = clientSession;
1293
1331
  this._tinyCloudSession = tinyCloudSession;
1294
1332
  this._address = address;
1295
1333
  this._chainId = chainId;
1296
1334
  await this.refreshNodeInfo();
1335
+ this.assertSignInCurrent(generation);
1297
1336
  for (const ext of this.extensions) {
1298
1337
  if (ext.afterSignIn) {
1299
1338
  await ext.afterSignIn(clientSession);
1339
+ this.assertSignInCurrent(generation);
1300
1340
  }
1301
1341
  }
1302
1342
  await this.ensureSpaceExists();
1343
+ this.assertSignInCurrent(generation);
1303
1344
  return clientSession;
1304
1345
  }
1305
1346
  /**
@@ -2546,17 +2587,24 @@ import {
2546
2587
  ServiceContext
2547
2588
  } from "@tinycloud/sdk-core";
2548
2589
  var DelegatedAccess = class {
2549
- constructor(session, delegation, host, invoke, invokeAny, telemetry) {
2590
+ /**
2591
+ * @param lifetime - Optional owner of this access: its `fetch` is used for
2592
+ * requests and the service context is registered with `track`, so the
2593
+ * owner can cancel and retire the access (TinyCloudNode does this when its
2594
+ * session is replaced or signed out).
2595
+ */
2596
+ constructor(session, delegation, host, invoke, invokeAny, telemetry, lifetime) {
2550
2597
  this.session = session;
2551
2598
  this._delegation = delegation;
2552
2599
  this.host = host;
2553
- this._serviceContext = new ServiceContext({
2600
+ const context = new ServiceContext({
2554
2601
  invoke,
2555
2602
  invokeAny,
2556
- fetch: globalThis.fetch.bind(globalThis),
2603
+ fetch: lifetime?.fetch ?? globalThis.fetch.bind(globalThis),
2557
2604
  hosts: [host],
2558
2605
  telemetry
2559
2606
  });
2607
+ this._serviceContext = lifetime ? lifetime.track(context) : context;
2560
2608
  const prefix = this._delegation.path.replace(/\/$/, "");
2561
2609
  this._kv = new KVService({ prefix });
2562
2610
  this._kv.initialize(this._serviceContext);
@@ -3140,6 +3188,19 @@ function exactVerifiedRecap(verified) {
3140
3188
  caveats: cloneRecapCaveats(entry.caveats)
3141
3189
  }));
3142
3190
  }
3191
+ function createDefaultSessionKey(wasm) {
3192
+ const manager = wasm.createSessionManager();
3193
+ let keyId = "default";
3194
+ let jwkStr = manager.jwk(keyId);
3195
+ if (!jwkStr) {
3196
+ keyId = manager.createSessionKey(keyId);
3197
+ jwkStr = manager.jwk(keyId);
3198
+ }
3199
+ if (!jwkStr) {
3200
+ throw new Error("Failed to get session key JWK");
3201
+ }
3202
+ return { manager, keyId, jwk: JSON.parse(jwkStr) };
3203
+ }
3143
3204
  var ServiceGraphLifetime = class {
3144
3205
  constructor(invokeFn, invokeAnyFn, fetchFn, observeContext) {
3145
3206
  this.invokeFn = invokeFn;
@@ -3189,9 +3250,13 @@ var ServiceGraphLifetime = class {
3189
3250
  this.contexts.clear();
3190
3251
  }
3191
3252
  }
3253
+ /** Aborted when the graph retires: cancels requests made for its session. */
3254
+ get signal() {
3255
+ return this.abortController.signal;
3256
+ }
3192
3257
  assertActive() {
3193
3258
  if (this.retired) {
3194
- throw new Error("Service graph has been retired by session replacement.");
3259
+ throw new Error("Service graph has been retired: its session was replaced or signed out.");
3195
3260
  }
3196
3261
  }
3197
3262
  combineSignals(signal) {
@@ -3493,6 +3558,16 @@ var _TinyCloudNode = class _TinyCloudNode {
3493
3558
  * so it must outlast a slow but healthy sync (9–18 s on prod 1.20.2, TC-998).
3494
3559
  */
3495
3560
  this.accountRegistryDeadlineMs = 3e4;
3561
+ /**
3562
+ * Incremented by every sign-out. A sign-in or restore records it when it
3563
+ * starts and does not establish its session once it has changed.
3564
+ */
3565
+ this.sessionEpoch = 0;
3566
+ /**
3567
+ * Numbers sign-in and restore attempts. A cancelled attempt cleans up only
3568
+ * while it is still the latest, so it never ends a newer session.
3569
+ */
3570
+ this.latestSessionAttempt = 0;
3496
3571
  /**
3497
3572
  * True when the last signIn() detected an interactive signer and skipped
3498
3573
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -3626,54 +3701,17 @@ var _TinyCloudNode = class _TinyCloudNode {
3626
3701
  "wasmBindings must be provided in config. Import from '@tinycloud/node-sdk' (not '/core') for automatic Node.js defaults."
3627
3702
  );
3628
3703
  }
3629
- this.sessionManager = this.wasmBindings.createSessionManager();
3630
- const defaultKeyId = "default";
3631
- let jwkStr = this.sessionManager.jwk(defaultKeyId);
3632
- if (jwkStr) {
3633
- this.sessionKeyId = defaultKeyId;
3634
- } else {
3635
- this.sessionKeyId = this.sessionManager.createSessionKey(defaultKeyId);
3636
- jwkStr = this.sessionManager.jwk(this.sessionKeyId);
3637
- }
3638
- if (!jwkStr) {
3639
- throw new Error("Failed to get session key JWK");
3640
- }
3641
- this.sessionKeyJwk = JSON.parse(jwkStr);
3704
+ const sessionKey = createDefaultSessionKey(this.wasmBindings);
3705
+ this.sessionManager = sessionKey.manager;
3706
+ this.sessionKeyId = sessionKey.keyId;
3707
+ this.sessionKeyJwk = sessionKey.jwk;
3642
3708
  this._serviceGraph = this.createServiceGraphLifetime();
3643
3709
  this._capabilityRegistry = new CapabilityKeyRegistry();
3644
3710
  this._keyProvider = new WasmKeyProvider({
3645
3711
  sessionManager: this.sessionManager
3646
3712
  });
3647
3713
  this.notificationHandler = config.notificationHandler ?? new SilentNotificationHandler();
3648
- const receiveOnlyGraph = this._serviceGraph;
3649
- this._sharingService = new SharingService({
3650
- hosts: [this.config.host],
3651
- // session: undefined - not needed for receive()
3652
- invoke: receiveOnlyGraph.invoke,
3653
- fetch: receiveOnlyGraph.fetch,
3654
- assertActive: () => receiveOnlyGraph.assertActive(),
3655
- keyProvider: this._keyProvider,
3656
- registry: this._capabilityRegistry,
3657
- createDelegationWasm: (params) => this.createDelegationWrapper(params),
3658
- computeCid: (data, codec) => {
3659
- if (!this.wasmBindings.computeCid) throw new Error("computeCid is unavailable");
3660
- return this.wasmBindings.computeCid(data, codec);
3661
- },
3662
- // delegationManager: undefined - not needed for receive()
3663
- createKVService: (config2) => {
3664
- const prefix = config2.pathPrefix?.replace(/\/$/, "");
3665
- const kvService = new KVService2({ prefix });
3666
- const kvContext = receiveOnlyGraph.track(new ServiceContext2({
3667
- invoke: config2.invoke,
3668
- fetch: config2.fetch ?? globalThis.fetch.bind(globalThis),
3669
- hosts: config2.hosts,
3670
- telemetry: this.config.telemetry
3671
- }));
3672
- kvContext.setSession(config2.session);
3673
- kvService.initialize(kvContext);
3674
- return kvService;
3675
- }
3676
- });
3714
+ this._sharingService = this.createReceiveOnlySharing();
3677
3715
  if (config.signer) {
3678
3716
  this.signer = config.signer;
3679
3717
  this.setupAuth(config);
@@ -3815,6 +3853,42 @@ var _TinyCloudNode = class _TinyCloudNode {
3815
3853
  get siweDomain() {
3816
3854
  return this.config.domain ?? "app.tinycloud.xyz";
3817
3855
  }
3856
+ /**
3857
+ * SharingService for receive-only access (no session required), bound to the
3858
+ * current service graph. This lets session-only users receive sharing links
3859
+ * without signIn(); full capabilities (generate) are added after signIn().
3860
+ */
3861
+ createReceiveOnlySharing() {
3862
+ const receiveOnlyGraph = this._serviceGraph;
3863
+ return new SharingService({
3864
+ hosts: [this.config.host],
3865
+ // session: undefined - not needed for receive()
3866
+ invoke: receiveOnlyGraph.invoke,
3867
+ fetch: receiveOnlyGraph.fetch,
3868
+ assertActive: () => receiveOnlyGraph.assertActive(),
3869
+ keyProvider: this._keyProvider,
3870
+ registry: this._capabilityRegistry,
3871
+ createDelegationWasm: (params) => this.createDelegationWrapper(params),
3872
+ computeCid: (data, codec) => {
3873
+ if (!this.wasmBindings.computeCid) throw new Error("computeCid is unavailable");
3874
+ return this.wasmBindings.computeCid(data, codec);
3875
+ },
3876
+ // delegationManager: undefined - not needed for receive()
3877
+ createKVService: (config) => {
3878
+ const prefix = config.pathPrefix?.replace(/\/$/, "");
3879
+ const kvService = new KVService2({ prefix });
3880
+ const kvContext = receiveOnlyGraph.track(new ServiceContext2({
3881
+ invoke: config.invoke,
3882
+ fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
3883
+ hosts: config.hosts,
3884
+ telemetry: this.config.telemetry
3885
+ }));
3886
+ kvContext.setSession(config.session);
3887
+ kvService.initialize(kvContext);
3888
+ return kvService;
3889
+ }
3890
+ });
3891
+ }
3818
3892
  /**
3819
3893
  * Set up authorization handler and TinyCloud instance.
3820
3894
  * @internal
@@ -4074,6 +4148,102 @@ var _TinyCloudNode = class _TinyCloudNode {
4074
4148
  get restorableSession() {
4075
4149
  return this.currentTinyCloudSession();
4076
4150
  }
4151
+ /**
4152
+ * Sign out and end the session on this instance.
4153
+ *
4154
+ * Every service built for the session (and every `useDelegation()` result)
4155
+ * is retired and the session key is replaced before anything is awaited, so
4156
+ * nothing captured from the session can sign once sign-out has started.
4157
+ * Pending work is aborted and the persisted session is cleared from the
4158
+ * configured session storage. Afterwards session accessors return
4159
+ * `undefined` and service calls fail with "Not signed in" until
4160
+ * {@link signIn} or {@link restoreSession}, which wait for a sign-out in
4161
+ * progress.
4162
+ */
4163
+ signOut() {
4164
+ this.signOutInFlight ?? (this.signOutInFlight = this.endSession().finally(() => {
4165
+ this.signOutInFlight = void 0;
4166
+ }));
4167
+ return this.signOutInFlight;
4168
+ }
4169
+ async endSession() {
4170
+ const leavingAddress = this.address ?? this._restoredTcSession?.address;
4171
+ this.teardownSession();
4172
+ try {
4173
+ await this.accountRegistryTail;
4174
+ if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
4175
+ } finally {
4176
+ if (leavingAddress) await this.clearPersistedSessionFor(leavingAddress);
4177
+ }
4178
+ }
4179
+ /**
4180
+ * Synchronously drop every piece of in-memory session authority and
4181
+ * invalidate sign-ins and restores still in flight (see `sessionEpoch`).
4182
+ */
4183
+ teardownSession() {
4184
+ this.sessionEpoch++;
4185
+ this.pendingAccountRegistrySync?.controller.abort();
4186
+ const oldGraph = this._serviceGraph;
4187
+ const oldCore = this.tc;
4188
+ const sessionKey = createDefaultSessionKey(this.wasmBindings);
4189
+ this.sessionManager = sessionKey.manager;
4190
+ this.sessionKeyId = sessionKey.keyId;
4191
+ this.sessionKeyJwk = sessionKey.jwk;
4192
+ this.auth?.resetSession(
4193
+ sessionKey.manager,
4194
+ sessionKey.keyId,
4195
+ this.explicitHost ? [this.explicitHost] : void 0
4196
+ );
4197
+ this.config.host = this.explicitHost ?? DEFAULT_HOST;
4198
+ this._address = void 0;
4199
+ this._chainId = 1;
4200
+ this._restoredTcSession = void 0;
4201
+ this._restoredNodeFeatures = void 0;
4202
+ this._activeServiceSession = void 0;
4203
+ this._serviceContext = void 0;
4204
+ this._kv = void 0;
4205
+ this._sql = void 0;
4206
+ this._duckdb = void 0;
4207
+ this._hooks = void 0;
4208
+ this._vault = void 0;
4209
+ this._encryption = void 0;
4210
+ this._baseVaults.clear();
4211
+ this._baseSecrets.clear();
4212
+ this._secrets.clear();
4213
+ this._account = void 0;
4214
+ this._publicKV = void 0;
4215
+ this._delegationManager = void 0;
4216
+ this._spaceService = void 0;
4217
+ this.runtimePermissionGrants = [];
4218
+ this._recapOperationsCache = void 0;
4219
+ this.confirmedHostedSpaceIds.clear();
4220
+ this.hostedOwnedSpaceIds.clear();
4221
+ this.cancelOwnedSpaceHosts();
4222
+ this.storageFull.clear();
4223
+ this._bootstrapSkipped = false;
4224
+ this._bootstrapStatus = { skipped: false };
4225
+ this.replicationDelegateSession = void 0;
4226
+ this.replicationScopeError = void 0;
4227
+ this._serviceGraph = this.createServiceGraphLifetime();
4228
+ oldGraph.retire();
4229
+ this.tc = this.auth ? new TinyCloud(this.auth, {
4230
+ invokeAny: this.invokeAnyWithRuntimePermissions,
4231
+ telemetry: this.config.telemetry
4232
+ }) : null;
4233
+ oldCore?.retireServices();
4234
+ this._capabilityRegistry = new CapabilityKeyRegistry();
4235
+ this._keyProvider = new WasmKeyProvider({ sessionManager: this.sessionManager });
4236
+ this._sharingService = this.createReceiveOnlySharing();
4237
+ }
4238
+ async clearPersistedSessionFor(address) {
4239
+ await (this.auth ? this.auth.clearPersistedSession(address) : this.config.sessionStorage?.clear(address));
4240
+ }
4241
+ /** Throws when a sign-out started after `epoch` was read. */
4242
+ assertSessionEpoch(epoch, operation) {
4243
+ if (epoch !== this.sessionEpoch) {
4244
+ throw new Error(`${operation} was cancelled by sign-out.`);
4245
+ }
4246
+ }
4077
4247
  /**
4078
4248
  * Sign in and create a new session.
4079
4249
  * This creates the user's space if it doesn't exist.
@@ -4087,11 +4257,28 @@ var _TinyCloudNode = class _TinyCloudNode {
4087
4257
  "Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
4088
4258
  );
4089
4259
  }
4260
+ const epoch = this.sessionEpoch;
4261
+ const attempt = ++this.latestSessionAttempt;
4262
+ await this.signOutInFlight;
4263
+ try {
4264
+ await this.establishSignIn(epoch, options);
4265
+ } catch (error) {
4266
+ if (epoch !== this.sessionEpoch && attempt === this.latestSessionAttempt) {
4267
+ await this.endSession();
4268
+ }
4269
+ throw error;
4270
+ }
4271
+ }
4272
+ async establishSignIn(epoch, options) {
4273
+ if (!this.signer || !this.tc) {
4274
+ throw new Error("Not signed in. Call signIn() first.");
4275
+ }
4090
4276
  this.pendingAccountRegistrySync?.controller.abort();
4091
4277
  await this.accountRegistryTail;
4092
4278
  await this.wasmBindings.ensureInitialized?.();
4093
4279
  const address = canonicalizeAddress2(await this.signer.getAddress());
4094
4280
  const chainId = await this.signer.getChainId();
4281
+ this.assertSessionEpoch(epoch, "Sign-in");
4095
4282
  if (address !== this._address || chainId !== this._chainId) this.storageFull.clear();
4096
4283
  this._address = address;
4097
4284
  this._chainId = chainId;
@@ -4110,6 +4297,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4110
4297
  this.confirmedHostedSpaceIds.clear();
4111
4298
  this.hostedOwnedSpaceIds.clear();
4112
4299
  await this.tc.signIn(options);
4300
+ this.assertSessionEpoch(epoch, "Sign-in");
4113
4301
  this.cancelOwnedSpaceHosts();
4114
4302
  this.syncResolvedHostFromAuth();
4115
4303
  for (const spaceId of this.auth.lastActivationActivatedSpaceIds ?? []) {
@@ -4118,6 +4306,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4118
4306
  this.replicationDelegateSession = void 0;
4119
4307
  this.replicationScopeError = void 0;
4120
4308
  if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
4309
+ this.assertSessionEpoch(epoch, "Sign-in");
4121
4310
  const oldGraph = this._serviceGraph;
4122
4311
  this._serviceGraph = this.createServiceGraphLifetime();
4123
4312
  oldGraph.retire();
@@ -4128,15 +4317,19 @@ var _TinyCloudNode = class _TinyCloudNode {
4128
4317
  this.sessionKeyJwk = signedInSession.jwk;
4129
4318
  }
4130
4319
  await this.initializeServices();
4320
+ this.assertSessionEpoch(epoch, "Sign-in");
4131
4321
  const primarySession = this.currentTinyCloudSession();
4132
4322
  if (primarySession) {
4133
4323
  this.registerPrimarySessionGrant(primarySession);
4134
4324
  }
4135
4325
  const bootstrapped = await this.bootstrapAccountIfNeeded();
4326
+ this.assertSessionEpoch(epoch, "Sign-in");
4136
4327
  await this.ensureRequestedEncryptionNetworks();
4328
+ this.assertSessionEpoch(epoch, "Sign-in");
4137
4329
  if (!bootstrapped && this.config.manifest === void 0 && this.config.capabilityRequest === void 0) {
4138
4330
  await this.ensureOwnedSpaceHostedById(this.ownedSpaceId(SECRETS_SPACE2));
4139
4331
  }
4332
+ this.assertSessionEpoch(epoch, "Sign-in");
4140
4333
  if (!bootstrapped) {
4141
4334
  this.scheduleAccountRegistrySync();
4142
4335
  }
@@ -4318,6 +4511,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4318
4511
  throw new Error("Account bootstrap requires a TinyCloud host");
4319
4512
  }
4320
4513
  const auth = this.auth;
4514
+ const graph = this._serviceGraph;
4321
4515
  const sessions = /* @__PURE__ */ new Map();
4322
4516
  const rawAbilitiesBySpace = /* @__PURE__ */ new Map();
4323
4517
  const warnings = [];
@@ -4326,6 +4520,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4326
4520
  const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === BOOTSTRAP_SESSION_REQUESTS.default;
4327
4521
  for (const step of steps) {
4328
4522
  if (step.kind !== "session") continue;
4523
+ graph.assertActive();
4329
4524
  if (step.space === "default" && canReusePrimaryBootstrapSession && primarySession) {
4330
4525
  sessions.set(step.space, primarySession);
4331
4526
  continue;
@@ -4353,6 +4548,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4353
4548
  }
4354
4549
  for (const step of steps) {
4355
4550
  if (step.kind !== "host") continue;
4551
+ graph.assertActive();
4356
4552
  const hosted = await auth.hostOwnedSpaceResult(step.spaceId, "bootstrap-host");
4357
4553
  if (!hosted.success) {
4358
4554
  throw Object.assign(
@@ -4364,11 +4560,16 @@ var _TinyCloudNode = class _TinyCloudNode {
4364
4560
  await new Promise((resolve) => setTimeout(resolve, 100));
4365
4561
  for (const step of steps) {
4366
4562
  if (step.kind !== "activate") continue;
4563
+ graph.assertActive();
4367
4564
  const session = sessions.get(step.space);
4368
4565
  if (!session) {
4369
4566
  throw new Error(`Missing bootstrap session for ${step.space}`);
4370
4567
  }
4371
- const activated = await activateSessionWithHost2(host, session.delegationHeader);
4568
+ const activated = await activateSessionWithHost2(host, session.delegationHeader, graph.signal).catch((error) => {
4569
+ graph.assertActive();
4570
+ throw error;
4571
+ });
4572
+ graph.assertActive();
4372
4573
  if (!activated.success || activated.skipped?.includes(step.spaceId)) {
4373
4574
  throw Object.assign(
4374
4575
  new Error(
@@ -4384,6 +4585,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4384
4585
  );
4385
4586
  }
4386
4587
  for (const step of steps) {
4588
+ graph.assertActive();
4387
4589
  if (step.kind === "account-index-schema") {
4388
4590
  const ensured = await this.account.index.ensure();
4389
4591
  if (!ensured.ok) {
@@ -5159,6 +5361,9 @@ var _TinyCloudNode = class _TinyCloudNode {
5159
5361
  * @param sessionData - The stored delegation data from the browser flow
5160
5362
  */
5161
5363
  async restoreSession(sessionData) {
5364
+ const epoch = this.sessionEpoch;
5365
+ const attempt = ++this.latestSessionAttempt;
5366
+ await this.signOutInFlight;
5162
5367
  await this.wasmBindings.ensureInitialized?.();
5163
5368
  let restoredJwk;
5164
5369
  try {
@@ -5348,6 +5553,7 @@ var _TinyCloudNode = class _TinyCloudNode {
5348
5553
  });
5349
5554
  this.pendingAccountRegistrySync?.controller.abort();
5350
5555
  await this.accountRegistryTail;
5556
+ this.assertSessionEpoch(epoch, "Session restore");
5351
5557
  const oldGraph = this._serviceGraph;
5352
5558
  const oldCore = this.tc;
5353
5559
  this.sessionManager = stagedManager;
@@ -5393,6 +5599,12 @@ var _TinyCloudNode = class _TinyCloudNode {
5393
5599
  this.replicationDelegateSession = compactReplicationSession;
5394
5600
  this.replicationScopeError = replicationScopeError;
5395
5601
  if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
5602
+ if (epoch !== this.sessionEpoch) {
5603
+ oldGraph.retire();
5604
+ oldCore?.retireServices?.();
5605
+ if (attempt === this.latestSessionAttempt) await this.endSession();
5606
+ this.assertSessionEpoch(epoch, "Session restore");
5607
+ }
5396
5608
  oldGraph.retire();
5397
5609
  if (replicationScopeError) {
5398
5610
  stagedGraph.serviceContext.emit("replication.state", {
@@ -5403,6 +5615,10 @@ var _TinyCloudNode = class _TinyCloudNode {
5403
5615
  }
5404
5616
  await this.bindReplicationRuntime(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
5405
5617
  oldCore?.retireServices?.();
5618
+ if (epoch !== this.sessionEpoch) {
5619
+ if (attempt === this.latestSessionAttempt) await this.endSession();
5620
+ this.assertSessionEpoch(epoch, "Session restore");
5621
+ }
5406
5622
  }
5407
5623
  stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
5408
5624
  if (!session?.siwe) return { cache: void 0, grants: [] };
@@ -7261,6 +7477,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7261
7477
  * matching service calls and downstream `delegateTo()` calls can use it.
7262
7478
  */
7263
7479
  async useRuntimeDelegation(delegation) {
7480
+ const graph = this._serviceGraph;
7264
7481
  const session = this.currentTinyCloudSession();
7265
7482
  if (!session) {
7266
7483
  throw new SessionExpiredError(/* @__PURE__ */ new Date(0));
@@ -7279,9 +7496,11 @@ var _TinyCloudNode = class _TinyCloudNode {
7279
7496
  try {
7280
7497
  activateResult = await activateSessionWithHost2(
7281
7498
  targetHost,
7282
- delegation.delegationHeader
7499
+ delegation.delegationHeader,
7500
+ graph.signal
7283
7501
  );
7284
7502
  } catch (cause) {
7503
+ graph.assertActive();
7285
7504
  throw Object.assign(
7286
7505
  new Error("The TinyCloud host could not be reached to activate the delegation."),
7287
7506
  { code: "ACTIVATION_TRANSPORT_FAILED", cause }
@@ -7301,6 +7520,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7301
7520
  { cause: activateResult }
7302
7521
  );
7303
7522
  }
7523
+ graph.assertActive();
7304
7524
  this.runtimePermissionGrants = this.runtimePermissionGrants.filter(
7305
7525
  (grant) => grant.delegation.cid !== delegation.cid
7306
7526
  );
@@ -7317,6 +7537,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7317
7537
  if (!Array.isArray(permissions) || permissions.length === 0) {
7318
7538
  throw new Error("grantRuntimePermissions requires a non-empty permissions array");
7319
7539
  }
7540
+ const graph = this._serviceGraph;
7320
7541
  const session = this.currentTinyCloudSession();
7321
7542
  if (!session) {
7322
7543
  throw new SessionExpiredError(/* @__PURE__ */ new Date(0));
@@ -7397,14 +7618,19 @@ var _TinyCloudNode = class _TinyCloudNode {
7397
7618
  }
7398
7619
  })();
7399
7620
  const signature = await this.signer.signMessage(prepared.siwe);
7621
+ graph.assertActive();
7400
7622
  const delegatedSession = this.wasmBindings.completeSessionSetup({
7401
7623
  ...prepared,
7402
7624
  signature
7403
7625
  });
7404
7626
  const activateResult = await activateSessionWithHost2(
7405
7627
  this.config.host,
7406
- delegatedSession.delegationHeader
7407
- );
7628
+ delegatedSession.delegationHeader,
7629
+ graph.signal
7630
+ ).catch((error) => {
7631
+ graph.assertActive();
7632
+ throw error;
7633
+ });
7408
7634
  if (!activateResult.success) {
7409
7635
  if (rawForDelegation.some((entry) => this.isRawRevocationPermissionEntry(entry)) && activateResult.status === 401 && /Cannot find parent delegation/i.test(activateResult.error ?? "")) {
7410
7636
  throw Object.assign(
@@ -7425,6 +7651,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7425
7651
  expiresAt,
7426
7652
  { siwe: prepared.siwe, signature }
7427
7653
  );
7654
+ graph.assertActive();
7428
7655
  this.runtimePermissionGrants.push({
7429
7656
  session: {
7430
7657
  delegationHeader: delegatedSession.delegationHeader,
@@ -8888,6 +9115,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8888
9115
  * @returns A DelegatedAccess instance for performing operations
8889
9116
  */
8890
9117
  async useDelegation(delegation) {
9118
+ const graph = this._serviceGraph;
8891
9119
  const delegationHeader = delegation.delegationHeader;
8892
9120
  const targetHost = delegation.host ?? this.config.host;
8893
9121
  if (this.isSessionOnly) {
@@ -8929,7 +9157,8 @@ var _TinyCloudNode = class _TinyCloudNode {
8929
9157
  targetHost,
8930
9158
  this.wasmBindings.invoke,
8931
9159
  this.wasmBindings.invokeAny,
8932
- this.config.telemetry
9160
+ this.config.telemetry,
9161
+ graph
8933
9162
  );
8934
9163
  }
8935
9164
  const mySession = this.auth?.tinyCloudSession;
@@ -8953,14 +9182,19 @@ var _TinyCloudNode = class _TinyCloudNode {
8953
9182
  ...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
8954
9183
  });
8955
9184
  const signature = await this.signer.signMessage(prepared.siwe);
9185
+ graph.assertActive();
8956
9186
  const invokerSession = this.wasmBindings.completeSessionSetup({
8957
9187
  ...prepared,
8958
9188
  signature
8959
9189
  });
8960
9190
  const activateResult = await activateSessionWithHost2(
8961
9191
  targetHost,
8962
- invokerSession.delegationHeader
8963
- );
9192
+ invokerSession.delegationHeader,
9193
+ graph.signal
9194
+ ).catch((error) => {
9195
+ graph.assertActive();
9196
+ throw error;
9197
+ });
8964
9198
  if (!activateResult.success) {
8965
9199
  throw Object.assign(
8966
9200
  new Error(`Failed to activate delegated session: ${describeHostFailure2(activateResult)}`),
@@ -8979,6 +9213,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8979
9213
  siwe: prepared.siwe,
8980
9214
  signature
8981
9215
  };
9216
+ graph.assertActive();
8982
9217
  this.trackReceivedDelegation(delegation, jwk);
8983
9218
  this.installRuntimeGrantFromServiceSession(
8984
9219
  delegation,
@@ -8997,7 +9232,8 @@ var _TinyCloudNode = class _TinyCloudNode {
8997
9232
  targetHost,
8998
9233
  this.wasmBindings.invoke,
8999
9234
  this.wasmBindings.invokeAny,
9000
- this.config.telemetry
9235
+ this.config.telemetry,
9236
+ graph
9001
9237
  );
9002
9238
  }
9003
9239
  /**