@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.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,115 @@ 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
+ const leavingAuth = this.auth;
4172
+ try {
4173
+ this.teardownSession();
4174
+ } finally {
4175
+ try {
4176
+ await this.accountRegistryTail;
4177
+ if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
4178
+ } finally {
4179
+ if (leavingAddress) {
4180
+ await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
4181
+ }
4182
+ }
4183
+ }
4184
+ }
4185
+ /**
4186
+ * Synchronously drop every piece of in-memory session authority and
4187
+ * invalidate sign-ins and restores still in flight (see `sessionEpoch`).
4188
+ */
4189
+ teardownSession() {
4190
+ this.sessionEpoch++;
4191
+ this.pendingAccountRegistrySync?.controller.abort();
4192
+ const oldGraph = this._serviceGraph;
4193
+ const oldCore = this.tc;
4194
+ const oldAuth = this.auth;
4195
+ this.auth = null;
4196
+ this.tc = null;
4197
+ this._address = void 0;
4198
+ this._restoredTcSession = void 0;
4199
+ this._activeServiceSession = void 0;
4200
+ oldGraph.retire();
4201
+ oldCore?.retireServices();
4202
+ oldAuth?.resetSession(
4203
+ this.sessionManager,
4204
+ this.sessionKeyId,
4205
+ this.explicitHost ? [this.explicitHost] : void 0
4206
+ );
4207
+ this.config.host = this.explicitHost ?? DEFAULT_HOST;
4208
+ this._chainId = 1;
4209
+ this._restoredNodeFeatures = void 0;
4210
+ this._activeServiceSession = void 0;
4211
+ this._serviceContext = void 0;
4212
+ this._kv = void 0;
4213
+ this._sql = void 0;
4214
+ this._duckdb = void 0;
4215
+ this._hooks = void 0;
4216
+ this._vault = void 0;
4217
+ this._encryption = void 0;
4218
+ this._baseVaults.clear();
4219
+ this._baseSecrets.clear();
4220
+ this._secrets.clear();
4221
+ this._account = void 0;
4222
+ this._publicKV = void 0;
4223
+ this._delegationManager = void 0;
4224
+ this._spaceService = void 0;
4225
+ this.runtimePermissionGrants = [];
4226
+ this._recapOperationsCache = void 0;
4227
+ this.confirmedHostedSpaceIds.clear();
4228
+ this.hostedOwnedSpaceIds.clear();
4229
+ this.cancelOwnedSpaceHosts();
4230
+ this.storageFull.clear();
4231
+ this._bootstrapSkipped = false;
4232
+ this._bootstrapStatus = { skipped: false };
4233
+ this.replicationDelegateSession = void 0;
4234
+ this.replicationScopeError = void 0;
4235
+ const sessionKey = createDefaultSessionKey(this.wasmBindings);
4236
+ this.sessionManager = sessionKey.manager;
4237
+ this.sessionKeyId = sessionKey.keyId;
4238
+ this.sessionKeyJwk = sessionKey.jwk;
4239
+ oldAuth?.resetSession(
4240
+ sessionKey.manager,
4241
+ sessionKey.keyId,
4242
+ this.explicitHost ? [this.explicitHost] : void 0
4243
+ );
4244
+ this._serviceGraph = this.createServiceGraphLifetime();
4245
+ this.auth = oldAuth;
4246
+ this.tc = this.auth ? new TinyCloud(this.auth, {
4247
+ invokeAny: this.invokeAnyWithRuntimePermissions,
4248
+ telemetry: this.config.telemetry
4249
+ }) : null;
4250
+ this._capabilityRegistry = new CapabilityKeyRegistry();
4251
+ this._keyProvider = new WasmKeyProvider({ sessionManager: this.sessionManager });
4252
+ this._sharingService = this.createReceiveOnlySharing();
4253
+ }
4254
+ /** Throws when a sign-out started after `epoch` was read. */
4255
+ assertSessionEpoch(epoch, operation) {
4256
+ if (epoch !== this.sessionEpoch) {
4257
+ throw new Error(`${operation} was cancelled by sign-out.`);
4258
+ }
4259
+ }
4077
4260
  /**
4078
4261
  * Sign in and create a new session.
4079
4262
  * This creates the user's space if it doesn't exist.
@@ -4087,11 +4270,28 @@ var _TinyCloudNode = class _TinyCloudNode {
4087
4270
  "Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
4088
4271
  );
4089
4272
  }
4273
+ const epoch = this.sessionEpoch;
4274
+ const attempt = ++this.latestSessionAttempt;
4275
+ await this.signOutInFlight;
4276
+ try {
4277
+ await this.establishSignIn(epoch, options);
4278
+ } catch (error) {
4279
+ if (epoch !== this.sessionEpoch && attempt === this.latestSessionAttempt) {
4280
+ await this.endSession();
4281
+ }
4282
+ throw error;
4283
+ }
4284
+ }
4285
+ async establishSignIn(epoch, options) {
4286
+ if (!this.signer || !this.tc) {
4287
+ throw new Error("Not signed in. Call signIn() first.");
4288
+ }
4090
4289
  this.pendingAccountRegistrySync?.controller.abort();
4091
4290
  await this.accountRegistryTail;
4092
4291
  await this.wasmBindings.ensureInitialized?.();
4093
4292
  const address = canonicalizeAddress2(await this.signer.getAddress());
4094
4293
  const chainId = await this.signer.getChainId();
4294
+ this.assertSessionEpoch(epoch, "Sign-in");
4095
4295
  if (address !== this._address || chainId !== this._chainId) this.storageFull.clear();
4096
4296
  this._address = address;
4097
4297
  this._chainId = chainId;
@@ -4110,6 +4310,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4110
4310
  this.confirmedHostedSpaceIds.clear();
4111
4311
  this.hostedOwnedSpaceIds.clear();
4112
4312
  await this.tc.signIn(options);
4313
+ this.assertSessionEpoch(epoch, "Sign-in");
4113
4314
  this.cancelOwnedSpaceHosts();
4114
4315
  this.syncResolvedHostFromAuth();
4115
4316
  for (const spaceId of this.auth.lastActivationActivatedSpaceIds ?? []) {
@@ -4118,6 +4319,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4118
4319
  this.replicationDelegateSession = void 0;
4119
4320
  this.replicationScopeError = void 0;
4120
4321
  if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
4322
+ this.assertSessionEpoch(epoch, "Sign-in");
4121
4323
  const oldGraph = this._serviceGraph;
4122
4324
  this._serviceGraph = this.createServiceGraphLifetime();
4123
4325
  oldGraph.retire();
@@ -4128,15 +4330,19 @@ var _TinyCloudNode = class _TinyCloudNode {
4128
4330
  this.sessionKeyJwk = signedInSession.jwk;
4129
4331
  }
4130
4332
  await this.initializeServices();
4333
+ this.assertSessionEpoch(epoch, "Sign-in");
4131
4334
  const primarySession = this.currentTinyCloudSession();
4132
4335
  if (primarySession) {
4133
4336
  this.registerPrimarySessionGrant(primarySession);
4134
4337
  }
4135
4338
  const bootstrapped = await this.bootstrapAccountIfNeeded();
4339
+ this.assertSessionEpoch(epoch, "Sign-in");
4136
4340
  await this.ensureRequestedEncryptionNetworks();
4341
+ this.assertSessionEpoch(epoch, "Sign-in");
4137
4342
  if (!bootstrapped && this.config.manifest === void 0 && this.config.capabilityRequest === void 0) {
4138
4343
  await this.ensureOwnedSpaceHostedById(this.ownedSpaceId(SECRETS_SPACE2));
4139
4344
  }
4345
+ this.assertSessionEpoch(epoch, "Sign-in");
4140
4346
  if (!bootstrapped) {
4141
4347
  this.scheduleAccountRegistrySync();
4142
4348
  }
@@ -4318,6 +4524,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4318
4524
  throw new Error("Account bootstrap requires a TinyCloud host");
4319
4525
  }
4320
4526
  const auth = this.auth;
4527
+ const graph = this._serviceGraph;
4321
4528
  const sessions = /* @__PURE__ */ new Map();
4322
4529
  const rawAbilitiesBySpace = /* @__PURE__ */ new Map();
4323
4530
  const warnings = [];
@@ -4326,6 +4533,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4326
4533
  const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === BOOTSTRAP_SESSION_REQUESTS.default;
4327
4534
  for (const step of steps) {
4328
4535
  if (step.kind !== "session") continue;
4536
+ graph.assertActive();
4329
4537
  if (step.space === "default" && canReusePrimaryBootstrapSession && primarySession) {
4330
4538
  sessions.set(step.space, primarySession);
4331
4539
  continue;
@@ -4353,6 +4561,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4353
4561
  }
4354
4562
  for (const step of steps) {
4355
4563
  if (step.kind !== "host") continue;
4564
+ graph.assertActive();
4356
4565
  const hosted = await auth.hostOwnedSpaceResult(step.spaceId, "bootstrap-host");
4357
4566
  if (!hosted.success) {
4358
4567
  throw Object.assign(
@@ -4364,11 +4573,16 @@ var _TinyCloudNode = class _TinyCloudNode {
4364
4573
  await new Promise((resolve) => setTimeout(resolve, 100));
4365
4574
  for (const step of steps) {
4366
4575
  if (step.kind !== "activate") continue;
4576
+ graph.assertActive();
4367
4577
  const session = sessions.get(step.space);
4368
4578
  if (!session) {
4369
4579
  throw new Error(`Missing bootstrap session for ${step.space}`);
4370
4580
  }
4371
- const activated = await activateSessionWithHost2(host, session.delegationHeader);
4581
+ const activated = await activateSessionWithHost2(host, session.delegationHeader, graph.signal).catch((error) => {
4582
+ graph.assertActive();
4583
+ throw error;
4584
+ });
4585
+ graph.assertActive();
4372
4586
  if (!activated.success || activated.skipped?.includes(step.spaceId)) {
4373
4587
  throw Object.assign(
4374
4588
  new Error(
@@ -4384,6 +4598,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4384
4598
  );
4385
4599
  }
4386
4600
  for (const step of steps) {
4601
+ graph.assertActive();
4387
4602
  if (step.kind === "account-index-schema") {
4388
4603
  const ensured = await this.account.index.ensure();
4389
4604
  if (!ensured.ok) {
@@ -5159,6 +5374,9 @@ var _TinyCloudNode = class _TinyCloudNode {
5159
5374
  * @param sessionData - The stored delegation data from the browser flow
5160
5375
  */
5161
5376
  async restoreSession(sessionData) {
5377
+ const epoch = this.sessionEpoch;
5378
+ const attempt = ++this.latestSessionAttempt;
5379
+ await this.signOutInFlight;
5162
5380
  await this.wasmBindings.ensureInitialized?.();
5163
5381
  let restoredJwk;
5164
5382
  try {
@@ -5348,6 +5566,7 @@ var _TinyCloudNode = class _TinyCloudNode {
5348
5566
  });
5349
5567
  this.pendingAccountRegistrySync?.controller.abort();
5350
5568
  await this.accountRegistryTail;
5569
+ this.assertSessionEpoch(epoch, "Session restore");
5351
5570
  const oldGraph = this._serviceGraph;
5352
5571
  const oldCore = this.tc;
5353
5572
  this.sessionManager = stagedManager;
@@ -5393,6 +5612,12 @@ var _TinyCloudNode = class _TinyCloudNode {
5393
5612
  this.replicationDelegateSession = compactReplicationSession;
5394
5613
  this.replicationScopeError = replicationScopeError;
5395
5614
  if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
5615
+ if (epoch !== this.sessionEpoch) {
5616
+ oldGraph.retire();
5617
+ oldCore?.retireServices?.();
5618
+ if (attempt === this.latestSessionAttempt) await this.endSession();
5619
+ this.assertSessionEpoch(epoch, "Session restore");
5620
+ }
5396
5621
  oldGraph.retire();
5397
5622
  if (replicationScopeError) {
5398
5623
  stagedGraph.serviceContext.emit("replication.state", {
@@ -5403,6 +5628,10 @@ var _TinyCloudNode = class _TinyCloudNode {
5403
5628
  }
5404
5629
  await this.bindReplicationRuntime(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
5405
5630
  oldCore?.retireServices?.();
5631
+ if (epoch !== this.sessionEpoch) {
5632
+ if (attempt === this.latestSessionAttempt) await this.endSession();
5633
+ this.assertSessionEpoch(epoch, "Session restore");
5634
+ }
5406
5635
  }
5407
5636
  stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
5408
5637
  if (!session?.siwe) return { cache: void 0, grants: [] };
@@ -7261,6 +7490,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7261
7490
  * matching service calls and downstream `delegateTo()` calls can use it.
7262
7491
  */
7263
7492
  async useRuntimeDelegation(delegation) {
7493
+ const graph = this._serviceGraph;
7264
7494
  const session = this.currentTinyCloudSession();
7265
7495
  if (!session) {
7266
7496
  throw new SessionExpiredError(/* @__PURE__ */ new Date(0));
@@ -7279,9 +7509,11 @@ var _TinyCloudNode = class _TinyCloudNode {
7279
7509
  try {
7280
7510
  activateResult = await activateSessionWithHost2(
7281
7511
  targetHost,
7282
- delegation.delegationHeader
7512
+ delegation.delegationHeader,
7513
+ graph.signal
7283
7514
  );
7284
7515
  } catch (cause) {
7516
+ graph.assertActive();
7285
7517
  throw Object.assign(
7286
7518
  new Error("The TinyCloud host could not be reached to activate the delegation."),
7287
7519
  { code: "ACTIVATION_TRANSPORT_FAILED", cause }
@@ -7301,6 +7533,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7301
7533
  { cause: activateResult }
7302
7534
  );
7303
7535
  }
7536
+ graph.assertActive();
7304
7537
  this.runtimePermissionGrants = this.runtimePermissionGrants.filter(
7305
7538
  (grant) => grant.delegation.cid !== delegation.cid
7306
7539
  );
@@ -7317,6 +7550,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7317
7550
  if (!Array.isArray(permissions) || permissions.length === 0) {
7318
7551
  throw new Error("grantRuntimePermissions requires a non-empty permissions array");
7319
7552
  }
7553
+ const graph = this._serviceGraph;
7320
7554
  const session = this.currentTinyCloudSession();
7321
7555
  if (!session) {
7322
7556
  throw new SessionExpiredError(/* @__PURE__ */ new Date(0));
@@ -7397,14 +7631,19 @@ var _TinyCloudNode = class _TinyCloudNode {
7397
7631
  }
7398
7632
  })();
7399
7633
  const signature = await this.signer.signMessage(prepared.siwe);
7634
+ graph.assertActive();
7400
7635
  const delegatedSession = this.wasmBindings.completeSessionSetup({
7401
7636
  ...prepared,
7402
7637
  signature
7403
7638
  });
7404
7639
  const activateResult = await activateSessionWithHost2(
7405
7640
  this.config.host,
7406
- delegatedSession.delegationHeader
7407
- );
7641
+ delegatedSession.delegationHeader,
7642
+ graph.signal
7643
+ ).catch((error) => {
7644
+ graph.assertActive();
7645
+ throw error;
7646
+ });
7408
7647
  if (!activateResult.success) {
7409
7648
  if (rawForDelegation.some((entry) => this.isRawRevocationPermissionEntry(entry)) && activateResult.status === 401 && /Cannot find parent delegation/i.test(activateResult.error ?? "")) {
7410
7649
  throw Object.assign(
@@ -7425,6 +7664,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7425
7664
  expiresAt,
7426
7665
  { siwe: prepared.siwe, signature }
7427
7666
  );
7667
+ graph.assertActive();
7428
7668
  this.runtimePermissionGrants.push({
7429
7669
  session: {
7430
7670
  delegationHeader: delegatedSession.delegationHeader,
@@ -8888,6 +9128,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8888
9128
  * @returns A DelegatedAccess instance for performing operations
8889
9129
  */
8890
9130
  async useDelegation(delegation) {
9131
+ const graph = this._serviceGraph;
8891
9132
  const delegationHeader = delegation.delegationHeader;
8892
9133
  const targetHost = delegation.host ?? this.config.host;
8893
9134
  if (this.isSessionOnly) {
@@ -8929,7 +9170,8 @@ var _TinyCloudNode = class _TinyCloudNode {
8929
9170
  targetHost,
8930
9171
  this.wasmBindings.invoke,
8931
9172
  this.wasmBindings.invokeAny,
8932
- this.config.telemetry
9173
+ this.config.telemetry,
9174
+ graph
8933
9175
  );
8934
9176
  }
8935
9177
  const mySession = this.auth?.tinyCloudSession;
@@ -8953,14 +9195,19 @@ var _TinyCloudNode = class _TinyCloudNode {
8953
9195
  ...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
8954
9196
  });
8955
9197
  const signature = await this.signer.signMessage(prepared.siwe);
9198
+ graph.assertActive();
8956
9199
  const invokerSession = this.wasmBindings.completeSessionSetup({
8957
9200
  ...prepared,
8958
9201
  signature
8959
9202
  });
8960
9203
  const activateResult = await activateSessionWithHost2(
8961
9204
  targetHost,
8962
- invokerSession.delegationHeader
8963
- );
9205
+ invokerSession.delegationHeader,
9206
+ graph.signal
9207
+ ).catch((error) => {
9208
+ graph.assertActive();
9209
+ throw error;
9210
+ });
8964
9211
  if (!activateResult.success) {
8965
9212
  throw Object.assign(
8966
9213
  new Error(`Failed to activate delegated session: ${describeHostFailure2(activateResult)}`),
@@ -8979,6 +9226,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8979
9226
  siwe: prepared.siwe,
8980
9227
  signature
8981
9228
  };
9229
+ graph.assertActive();
8982
9230
  this.trackReceivedDelegation(delegation, jwk);
8983
9231
  this.installRuntimeGrantFromServiceSession(
8984
9232
  delegation,
@@ -8997,7 +9245,8 @@ var _TinyCloudNode = class _TinyCloudNode {
8997
9245
  targetHost,
8998
9246
  this.wasmBindings.invoke,
8999
9247
  this.wasmBindings.invokeAny,
9000
- this.config.telemetry
9248
+ this.config.telemetry,
9249
+ graph
9001
9250
  );
9002
9251
  }
9003
9252
  /**