@tinycloud/node-sdk 3.2.6-beta.1 → 3.2.6-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.
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,115 @@ 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
+ const leavingAuth = this.auth;
4149
+ try {
4150
+ this.teardownSession();
4151
+ } finally {
4152
+ try {
4153
+ await this.accountRegistryTail;
4154
+ if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
4155
+ } finally {
4156
+ if (leavingAddress) {
4157
+ await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
4158
+ }
4159
+ }
4160
+ }
4161
+ }
4162
+ /**
4163
+ * Synchronously drop every piece of in-memory session authority and
4164
+ * invalidate sign-ins and restores still in flight (see `sessionEpoch`).
4165
+ */
4166
+ teardownSession() {
4167
+ this.sessionEpoch++;
4168
+ this.pendingAccountRegistrySync?.controller.abort();
4169
+ const oldGraph = this._serviceGraph;
4170
+ const oldCore = this.tc;
4171
+ const oldAuth = this.auth;
4172
+ this.auth = null;
4173
+ this.tc = null;
4174
+ this._address = void 0;
4175
+ this._restoredTcSession = void 0;
4176
+ this._activeServiceSession = void 0;
4177
+ oldGraph.retire();
4178
+ oldCore?.retireServices();
4179
+ oldAuth?.resetSession(
4180
+ this.sessionManager,
4181
+ this.sessionKeyId,
4182
+ this.explicitHost ? [this.explicitHost] : void 0
4183
+ );
4184
+ this.config.host = this.explicitHost ?? DEFAULT_HOST;
4185
+ this._chainId = 1;
4186
+ this._restoredNodeFeatures = void 0;
4187
+ this._activeServiceSession = void 0;
4188
+ this._serviceContext = void 0;
4189
+ this._kv = void 0;
4190
+ this._sql = void 0;
4191
+ this._duckdb = void 0;
4192
+ this._hooks = void 0;
4193
+ this._vault = void 0;
4194
+ this._encryption = void 0;
4195
+ this._baseVaults.clear();
4196
+ this._baseSecrets.clear();
4197
+ this._secrets.clear();
4198
+ this._account = void 0;
4199
+ this._publicKV = void 0;
4200
+ this._delegationManager = void 0;
4201
+ this._spaceService = void 0;
4202
+ this.runtimePermissionGrants = [];
4203
+ this._recapOperationsCache = void 0;
4204
+ this.confirmedHostedSpaceIds.clear();
4205
+ this.hostedOwnedSpaceIds.clear();
4206
+ this.cancelOwnedSpaceHosts();
4207
+ this.storageFull.clear();
4208
+ this._bootstrapSkipped = false;
4209
+ this._bootstrapStatus = { skipped: false };
4210
+ this.replicationDelegateSession = void 0;
4211
+ this.replicationScopeError = void 0;
4212
+ const sessionKey = createDefaultSessionKey(this.wasmBindings);
4213
+ this.sessionManager = sessionKey.manager;
4214
+ this.sessionKeyId = sessionKey.keyId;
4215
+ this.sessionKeyJwk = sessionKey.jwk;
4216
+ oldAuth?.resetSession(
4217
+ sessionKey.manager,
4218
+ sessionKey.keyId,
4219
+ this.explicitHost ? [this.explicitHost] : void 0
4220
+ );
4221
+ this._serviceGraph = this.createServiceGraphLifetime();
4222
+ this.auth = oldAuth;
4223
+ this.tc = this.auth ? new import_sdk_core10.TinyCloud(this.auth, {
4224
+ invokeAny: this.invokeAnyWithRuntimePermissions,
4225
+ telemetry: this.config.telemetry
4226
+ }) : null;
4227
+ this._capabilityRegistry = new import_sdk_core10.CapabilityKeyRegistry();
4228
+ this._keyProvider = new WasmKeyProvider({ sessionManager: this.sessionManager });
4229
+ this._sharingService = this.createReceiveOnlySharing();
4230
+ }
4231
+ /** Throws when a sign-out started after `epoch` was read. */
4232
+ assertSessionEpoch(epoch, operation) {
4233
+ if (epoch !== this.sessionEpoch) {
4234
+ throw new Error(`${operation} was cancelled by sign-out.`);
4235
+ }
4236
+ }
4054
4237
  /**
4055
4238
  * Sign in and create a new session.
4056
4239
  * This creates the user's space if it doesn't exist.
@@ -4064,11 +4247,28 @@ var _TinyCloudNode = class _TinyCloudNode {
4064
4247
  "Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
4065
4248
  );
4066
4249
  }
4250
+ const epoch = this.sessionEpoch;
4251
+ const attempt = ++this.latestSessionAttempt;
4252
+ await this.signOutInFlight;
4253
+ try {
4254
+ await this.establishSignIn(epoch, options);
4255
+ } catch (error) {
4256
+ if (epoch !== this.sessionEpoch && attempt === this.latestSessionAttempt) {
4257
+ await this.endSession();
4258
+ }
4259
+ throw error;
4260
+ }
4261
+ }
4262
+ async establishSignIn(epoch, options) {
4263
+ if (!this.signer || !this.tc) {
4264
+ throw new Error("Not signed in. Call signIn() first.");
4265
+ }
4067
4266
  this.pendingAccountRegistrySync?.controller.abort();
4068
4267
  await this.accountRegistryTail;
4069
4268
  await this.wasmBindings.ensureInitialized?.();
4070
4269
  const address = (0, import_sdk_core10.canonicalizeAddress)(await this.signer.getAddress());
4071
4270
  const chainId = await this.signer.getChainId();
4271
+ this.assertSessionEpoch(epoch, "Sign-in");
4072
4272
  if (address !== this._address || chainId !== this._chainId) this.storageFull.clear();
4073
4273
  this._address = address;
4074
4274
  this._chainId = chainId;
@@ -4087,6 +4287,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4087
4287
  this.confirmedHostedSpaceIds.clear();
4088
4288
  this.hostedOwnedSpaceIds.clear();
4089
4289
  await this.tc.signIn(options);
4290
+ this.assertSessionEpoch(epoch, "Sign-in");
4090
4291
  this.cancelOwnedSpaceHosts();
4091
4292
  this.syncResolvedHostFromAuth();
4092
4293
  for (const spaceId of this.auth.lastActivationActivatedSpaceIds ?? []) {
@@ -4095,6 +4296,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4095
4296
  this.replicationDelegateSession = void 0;
4096
4297
  this.replicationScopeError = void 0;
4097
4298
  if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
4299
+ this.assertSessionEpoch(epoch, "Sign-in");
4098
4300
  const oldGraph = this._serviceGraph;
4099
4301
  this._serviceGraph = this.createServiceGraphLifetime();
4100
4302
  oldGraph.retire();
@@ -4105,15 +4307,19 @@ var _TinyCloudNode = class _TinyCloudNode {
4105
4307
  this.sessionKeyJwk = signedInSession.jwk;
4106
4308
  }
4107
4309
  await this.initializeServices();
4310
+ this.assertSessionEpoch(epoch, "Sign-in");
4108
4311
  const primarySession = this.currentTinyCloudSession();
4109
4312
  if (primarySession) {
4110
4313
  this.registerPrimarySessionGrant(primarySession);
4111
4314
  }
4112
4315
  const bootstrapped = await this.bootstrapAccountIfNeeded();
4316
+ this.assertSessionEpoch(epoch, "Sign-in");
4113
4317
  await this.ensureRequestedEncryptionNetworks();
4318
+ this.assertSessionEpoch(epoch, "Sign-in");
4114
4319
  if (!bootstrapped && this.config.manifest === void 0 && this.config.capabilityRequest === void 0) {
4115
4320
  await this.ensureOwnedSpaceHostedById(this.ownedSpaceId(import_sdk_core10.SECRETS_SPACE));
4116
4321
  }
4322
+ this.assertSessionEpoch(epoch, "Sign-in");
4117
4323
  if (!bootstrapped) {
4118
4324
  this.scheduleAccountRegistrySync();
4119
4325
  }
@@ -4295,6 +4501,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4295
4501
  throw new Error("Account bootstrap requires a TinyCloud host");
4296
4502
  }
4297
4503
  const auth = this.auth;
4504
+ const graph = this._serviceGraph;
4298
4505
  const sessions = /* @__PURE__ */ new Map();
4299
4506
  const rawAbilitiesBySpace = /* @__PURE__ */ new Map();
4300
4507
  const warnings = [];
@@ -4303,6 +4510,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4303
4510
  const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === import_sdk_core10.BOOTSTRAP_SESSION_REQUESTS.default;
4304
4511
  for (const step of steps) {
4305
4512
  if (step.kind !== "session") continue;
4513
+ graph.assertActive();
4306
4514
  if (step.space === "default" && canReusePrimaryBootstrapSession && primarySession) {
4307
4515
  sessions.set(step.space, primarySession);
4308
4516
  continue;
@@ -4330,6 +4538,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4330
4538
  }
4331
4539
  for (const step of steps) {
4332
4540
  if (step.kind !== "host") continue;
4541
+ graph.assertActive();
4333
4542
  const hosted = await auth.hostOwnedSpaceResult(step.spaceId, "bootstrap-host");
4334
4543
  if (!hosted.success) {
4335
4544
  throw Object.assign(
@@ -4341,11 +4550,16 @@ var _TinyCloudNode = class _TinyCloudNode {
4341
4550
  await new Promise((resolve) => setTimeout(resolve, 100));
4342
4551
  for (const step of steps) {
4343
4552
  if (step.kind !== "activate") continue;
4553
+ graph.assertActive();
4344
4554
  const session = sessions.get(step.space);
4345
4555
  if (!session) {
4346
4556
  throw new Error(`Missing bootstrap session for ${step.space}`);
4347
4557
  }
4348
- const activated = await (0, import_sdk_core10.activateSessionWithHost)(host, session.delegationHeader);
4558
+ const activated = await (0, import_sdk_core10.activateSessionWithHost)(host, session.delegationHeader, graph.signal).catch((error) => {
4559
+ graph.assertActive();
4560
+ throw error;
4561
+ });
4562
+ graph.assertActive();
4349
4563
  if (!activated.success || activated.skipped?.includes(step.spaceId)) {
4350
4564
  throw Object.assign(
4351
4565
  new Error(
@@ -4361,6 +4575,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4361
4575
  );
4362
4576
  }
4363
4577
  for (const step of steps) {
4578
+ graph.assertActive();
4364
4579
  if (step.kind === "account-index-schema") {
4365
4580
  const ensured = await this.account.index.ensure();
4366
4581
  if (!ensured.ok) {
@@ -5136,6 +5351,9 @@ var _TinyCloudNode = class _TinyCloudNode {
5136
5351
  * @param sessionData - The stored delegation data from the browser flow
5137
5352
  */
5138
5353
  async restoreSession(sessionData) {
5354
+ const epoch = this.sessionEpoch;
5355
+ const attempt = ++this.latestSessionAttempt;
5356
+ await this.signOutInFlight;
5139
5357
  await this.wasmBindings.ensureInitialized?.();
5140
5358
  let restoredJwk;
5141
5359
  try {
@@ -5325,6 +5543,7 @@ var _TinyCloudNode = class _TinyCloudNode {
5325
5543
  });
5326
5544
  this.pendingAccountRegistrySync?.controller.abort();
5327
5545
  await this.accountRegistryTail;
5546
+ this.assertSessionEpoch(epoch, "Session restore");
5328
5547
  const oldGraph = this._serviceGraph;
5329
5548
  const oldCore = this.tc;
5330
5549
  this.sessionManager = stagedManager;
@@ -5370,6 +5589,12 @@ var _TinyCloudNode = class _TinyCloudNode {
5370
5589
  this.replicationDelegateSession = compactReplicationSession;
5371
5590
  this.replicationScopeError = replicationScopeError;
5372
5591
  if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
5592
+ if (epoch !== this.sessionEpoch) {
5593
+ oldGraph.retire();
5594
+ oldCore?.retireServices?.();
5595
+ if (attempt === this.latestSessionAttempt) await this.endSession();
5596
+ this.assertSessionEpoch(epoch, "Session restore");
5597
+ }
5373
5598
  oldGraph.retire();
5374
5599
  if (replicationScopeError) {
5375
5600
  stagedGraph.serviceContext.emit("replication.state", {
@@ -5380,6 +5605,10 @@ var _TinyCloudNode = class _TinyCloudNode {
5380
5605
  }
5381
5606
  await this.bindReplicationRuntime(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
5382
5607
  oldCore?.retireServices?.();
5608
+ if (epoch !== this.sessionEpoch) {
5609
+ if (attempt === this.latestSessionAttempt) await this.endSession();
5610
+ this.assertSessionEpoch(epoch, "Session restore");
5611
+ }
5383
5612
  }
5384
5613
  stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
5385
5614
  if (!session?.siwe) return { cache: void 0, grants: [] };
@@ -7238,6 +7467,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7238
7467
  * matching service calls and downstream `delegateTo()` calls can use it.
7239
7468
  */
7240
7469
  async useRuntimeDelegation(delegation) {
7470
+ const graph = this._serviceGraph;
7241
7471
  const session = this.currentTinyCloudSession();
7242
7472
  if (!session) {
7243
7473
  throw new import_sdk_core10.SessionExpiredError(/* @__PURE__ */ new Date(0));
@@ -7256,9 +7486,11 @@ var _TinyCloudNode = class _TinyCloudNode {
7256
7486
  try {
7257
7487
  activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
7258
7488
  targetHost,
7259
- delegation.delegationHeader
7489
+ delegation.delegationHeader,
7490
+ graph.signal
7260
7491
  );
7261
7492
  } catch (cause) {
7493
+ graph.assertActive();
7262
7494
  throw Object.assign(
7263
7495
  new Error("The TinyCloud host could not be reached to activate the delegation."),
7264
7496
  { code: "ACTIVATION_TRANSPORT_FAILED", cause }
@@ -7278,6 +7510,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7278
7510
  { cause: activateResult }
7279
7511
  );
7280
7512
  }
7513
+ graph.assertActive();
7281
7514
  this.runtimePermissionGrants = this.runtimePermissionGrants.filter(
7282
7515
  (grant) => grant.delegation.cid !== delegation.cid
7283
7516
  );
@@ -7294,6 +7527,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7294
7527
  if (!Array.isArray(permissions) || permissions.length === 0) {
7295
7528
  throw new Error("grantRuntimePermissions requires a non-empty permissions array");
7296
7529
  }
7530
+ const graph = this._serviceGraph;
7297
7531
  const session = this.currentTinyCloudSession();
7298
7532
  if (!session) {
7299
7533
  throw new import_sdk_core10.SessionExpiredError(/* @__PURE__ */ new Date(0));
@@ -7374,14 +7608,19 @@ var _TinyCloudNode = class _TinyCloudNode {
7374
7608
  }
7375
7609
  })();
7376
7610
  const signature = await this.signer.signMessage(prepared.siwe);
7611
+ graph.assertActive();
7377
7612
  const delegatedSession = this.wasmBindings.completeSessionSetup({
7378
7613
  ...prepared,
7379
7614
  signature
7380
7615
  });
7381
7616
  const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
7382
7617
  this.config.host,
7383
- delegatedSession.delegationHeader
7384
- );
7618
+ delegatedSession.delegationHeader,
7619
+ graph.signal
7620
+ ).catch((error) => {
7621
+ graph.assertActive();
7622
+ throw error;
7623
+ });
7385
7624
  if (!activateResult.success) {
7386
7625
  if (rawForDelegation.some((entry) => this.isRawRevocationPermissionEntry(entry)) && activateResult.status === 401 && /Cannot find parent delegation/i.test(activateResult.error ?? "")) {
7387
7626
  throw Object.assign(
@@ -7402,6 +7641,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7402
7641
  expiresAt,
7403
7642
  { siwe: prepared.siwe, signature }
7404
7643
  );
7644
+ graph.assertActive();
7405
7645
  this.runtimePermissionGrants.push({
7406
7646
  session: {
7407
7647
  delegationHeader: delegatedSession.delegationHeader,
@@ -8865,6 +9105,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8865
9105
  * @returns A DelegatedAccess instance for performing operations
8866
9106
  */
8867
9107
  async useDelegation(delegation) {
9108
+ const graph = this._serviceGraph;
8868
9109
  const delegationHeader = delegation.delegationHeader;
8869
9110
  const targetHost = delegation.host ?? this.config.host;
8870
9111
  if (this.isSessionOnly) {
@@ -8906,7 +9147,8 @@ var _TinyCloudNode = class _TinyCloudNode {
8906
9147
  targetHost,
8907
9148
  this.wasmBindings.invoke,
8908
9149
  this.wasmBindings.invokeAny,
8909
- this.config.telemetry
9150
+ this.config.telemetry,
9151
+ graph
8910
9152
  );
8911
9153
  }
8912
9154
  const mySession = this.auth?.tinyCloudSession;
@@ -8930,14 +9172,19 @@ var _TinyCloudNode = class _TinyCloudNode {
8930
9172
  ...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
8931
9173
  });
8932
9174
  const signature = await this.signer.signMessage(prepared.siwe);
9175
+ graph.assertActive();
8933
9176
  const invokerSession = this.wasmBindings.completeSessionSetup({
8934
9177
  ...prepared,
8935
9178
  signature
8936
9179
  });
8937
9180
  const activateResult = await (0, import_sdk_core10.activateSessionWithHost)(
8938
9181
  targetHost,
8939
- invokerSession.delegationHeader
8940
- );
9182
+ invokerSession.delegationHeader,
9183
+ graph.signal
9184
+ ).catch((error) => {
9185
+ graph.assertActive();
9186
+ throw error;
9187
+ });
8941
9188
  if (!activateResult.success) {
8942
9189
  throw Object.assign(
8943
9190
  new Error(`Failed to activate delegated session: ${describeHostFailure2(activateResult)}`),
@@ -8956,6 +9203,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8956
9203
  siwe: prepared.siwe,
8957
9204
  signature
8958
9205
  };
9206
+ graph.assertActive();
8959
9207
  this.trackReceivedDelegation(delegation, jwk);
8960
9208
  this.installRuntimeGrantFromServiceSession(
8961
9209
  delegation,
@@ -8974,7 +9222,8 @@ var _TinyCloudNode = class _TinyCloudNode {
8974
9222
  targetHost,
8975
9223
  this.wasmBindings.invoke,
8976
9224
  this.wasmBindings.invokeAny,
8977
- this.config.telemetry
9225
+ this.config.telemetry,
9226
+ graph
8978
9227
  );
8979
9228
  }
8980
9229
  /**