@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/index.js CHANGED
@@ -18476,6 +18476,13 @@ function cloneAbilitiesMap(actions) {
18476
18476
  var NodeUserAuthorization = class {
18477
18477
  constructor(config) {
18478
18478
  this.activeSessionKeyId = "default";
18479
+ /** Incremented by resetSession; a sign-in started earlier publishes nothing. */
18480
+ this.sessionGeneration = 0;
18481
+ /**
18482
+ * Sign-in saves run one at a time, in order, each together with its own
18483
+ * cancellation cleanup, so no later write can land between the two.
18484
+ */
18485
+ this.persistenceQueue = Promise.resolve();
18479
18486
  this.extensions = [];
18480
18487
  this._nodeFeatures = [];
18481
18488
  this._nodeFeaturesKnown = false;
@@ -18606,6 +18613,27 @@ var NodeUserAuthorization = class {
18606
18613
  this.tinycloudHosts = [...hosts];
18607
18614
  }
18608
18615
  }
18616
+ /**
18617
+ * @internal End the session: adopt a fresh session manager whose active key
18618
+ * is `sessionKeyId`, and return to the constructed state.
18619
+ */
18620
+ resetSession(sessionManager, sessionKeyId, hosts) {
18621
+ this.sessionGeneration++;
18622
+ this.installRestoredSession(sessionManager, void 0, hosts);
18623
+ this.activeSessionKeyId = sessionKeyId;
18624
+ }
18625
+ /** Throws when the session was reset (signed out) after `generation` was read. */
18626
+ assertSignInCurrent(generation) {
18627
+ if (generation !== this.sessionGeneration) {
18628
+ throw new Error("Sign-in was cancelled by sign-out.");
18629
+ }
18630
+ }
18631
+ /** Runs `task` after every earlier queued persistence task has settled. */
18632
+ enqueuePersistence(task) {
18633
+ const run = this.persistenceQueue.then(task);
18634
+ this.persistenceQueue = run.catch(() => void 0);
18635
+ return run;
18636
+ }
18609
18637
  /** @internal Atomically adopt the state staged by TinyCloudNode.restoreSession. */
18610
18638
  installRestoredSession(sessionManager, session, hosts, nodeFeatures) {
18611
18639
  this.sessionManager = sessionManager;
@@ -19294,11 +19322,14 @@ var NodeUserAuthorization = class {
19294
19322
  * @param options - Optional per-call SIWE overrides for this sign-in only
19295
19323
  */
19296
19324
  async signIn(options) {
19297
- this._address = canonicalizeAddress(await this.signer.getAddress());
19298
- this._chainId = await this.signer.getChainId();
19299
- const address = this._address;
19300
- const chainId = this._chainId;
19325
+ const generation = this.sessionGeneration;
19326
+ const address = canonicalizeAddress(await this.signer.getAddress());
19327
+ const chainId = await this.signer.getChainId();
19328
+ this.assertSignInCurrent(generation);
19329
+ this._address = address;
19330
+ this._chainId = chainId;
19301
19331
  await this.resolveTinyCloudHostsForSignIn(address, chainId);
19332
+ this.assertSignInCurrent(generation);
19302
19333
  const keyId = `session-${Date.now()}`;
19303
19334
  this.sessionManager.renameSessionKeyId(this.activeSessionKeyId, keyId);
19304
19335
  this.activeSessionKeyId = keyId;
@@ -19331,6 +19362,7 @@ var NodeUserAuthorization = class {
19331
19362
  type: "siwe",
19332
19363
  purpose: "sign-in"
19333
19364
  });
19365
+ this.assertSignInCurrent(generation);
19334
19366
  const session = this.wasm.completeSessionSetup({
19335
19367
  ...prepared,
19336
19368
  signature: signature2
@@ -19375,18 +19407,27 @@ var NodeUserAuthorization = class {
19375
19407
  version: "1.0",
19376
19408
  tinycloudHosts: this.tinycloudHosts
19377
19409
  };
19378
- await this.sessionStorage.save(address, persistedData);
19410
+ await this.enqueuePersistence(async () => {
19411
+ await this.sessionStorage.save(address, persistedData);
19412
+ if (generation !== this.sessionGeneration) {
19413
+ await this.sessionStorage.clear(address);
19414
+ }
19415
+ });
19416
+ this.assertSignInCurrent(generation);
19379
19417
  this._session = clientSession;
19380
19418
  this._tinyCloudSession = tinyCloudSession;
19381
19419
  this._address = address;
19382
19420
  this._chainId = chainId;
19383
19421
  await this.refreshNodeInfo();
19422
+ this.assertSignInCurrent(generation);
19384
19423
  for (const ext of this.extensions) {
19385
19424
  if (ext.afterSignIn) {
19386
19425
  await ext.afterSignIn(clientSession);
19426
+ this.assertSignInCurrent(generation);
19387
19427
  }
19388
19428
  }
19389
19429
  await this.ensureSpaceExists();
19430
+ this.assertSignInCurrent(generation);
19390
19431
  return clientSession;
19391
19432
  }
19392
19433
  /**
@@ -20029,17 +20070,24 @@ import {
20029
20070
  ServiceContext
20030
20071
  } from "@tinycloud/sdk-core";
20031
20072
  var DelegatedAccess = class {
20032
- constructor(session, delegation, host, invoke2, invokeAny2, telemetry) {
20073
+ /**
20074
+ * @param lifetime - Optional owner of this access: its `fetch` is used for
20075
+ * requests and the service context is registered with `track`, so the
20076
+ * owner can cancel and retire the access (TinyCloudNode does this when its
20077
+ * session is replaced or signed out).
20078
+ */
20079
+ constructor(session, delegation, host, invoke2, invokeAny2, telemetry, lifetime) {
20033
20080
  this.session = session;
20034
20081
  this._delegation = delegation;
20035
20082
  this.host = host;
20036
- this._serviceContext = new ServiceContext({
20083
+ const context = new ServiceContext({
20037
20084
  invoke: invoke2,
20038
20085
  invokeAny: invokeAny2,
20039
- fetch: globalThis.fetch.bind(globalThis),
20086
+ fetch: lifetime?.fetch ?? globalThis.fetch.bind(globalThis),
20040
20087
  hosts: [host],
20041
20088
  telemetry
20042
20089
  });
20090
+ this._serviceContext = lifetime ? lifetime.track(context) : context;
20043
20091
  const prefix = this._delegation.path.replace(/\/$/, "");
20044
20092
  this._kv = new KVService({ prefix });
20045
20093
  this._kv.initialize(this._serviceContext);
@@ -20623,6 +20671,19 @@ function exactVerifiedRecap(verified) {
20623
20671
  caveats: cloneRecapCaveats(entry.caveats)
20624
20672
  }));
20625
20673
  }
20674
+ function createDefaultSessionKey(wasm) {
20675
+ const manager = wasm.createSessionManager();
20676
+ let keyId = "default";
20677
+ let jwkStr = manager.jwk(keyId);
20678
+ if (!jwkStr) {
20679
+ keyId = manager.createSessionKey(keyId);
20680
+ jwkStr = manager.jwk(keyId);
20681
+ }
20682
+ if (!jwkStr) {
20683
+ throw new Error("Failed to get session key JWK");
20684
+ }
20685
+ return { manager, keyId, jwk: JSON.parse(jwkStr) };
20686
+ }
20626
20687
  var ServiceGraphLifetime = class {
20627
20688
  constructor(invokeFn, invokeAnyFn, fetchFn, observeContext) {
20628
20689
  this.invokeFn = invokeFn;
@@ -20672,9 +20733,13 @@ var ServiceGraphLifetime = class {
20672
20733
  this.contexts.clear();
20673
20734
  }
20674
20735
  }
20736
+ /** Aborted when the graph retires: cancels requests made for its session. */
20737
+ get signal() {
20738
+ return this.abortController.signal;
20739
+ }
20675
20740
  assertActive() {
20676
20741
  if (this.retired) {
20677
- throw new Error("Service graph has been retired by session replacement.");
20742
+ throw new Error("Service graph has been retired: its session was replaced or signed out.");
20678
20743
  }
20679
20744
  }
20680
20745
  combineSignals(signal) {
@@ -20976,6 +21041,16 @@ var _TinyCloudNode = class _TinyCloudNode {
20976
21041
  * so it must outlast a slow but healthy sync (9–18 s on prod 1.20.2, TC-998).
20977
21042
  */
20978
21043
  this.accountRegistryDeadlineMs = 3e4;
21044
+ /**
21045
+ * Incremented by every sign-out. A sign-in or restore records it when it
21046
+ * starts and does not establish its session once it has changed.
21047
+ */
21048
+ this.sessionEpoch = 0;
21049
+ /**
21050
+ * Numbers sign-in and restore attempts. A cancelled attempt cleans up only
21051
+ * while it is still the latest, so it never ends a newer session.
21052
+ */
21053
+ this.latestSessionAttempt = 0;
20979
21054
  /**
20980
21055
  * True when the last signIn() detected an interactive signer and skipped
20981
21056
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -21109,54 +21184,17 @@ var _TinyCloudNode = class _TinyCloudNode {
21109
21184
  "wasmBindings must be provided in config. Import from '@tinycloud/node-sdk' (not '/core') for automatic Node.js defaults."
21110
21185
  );
21111
21186
  }
21112
- this.sessionManager = this.wasmBindings.createSessionManager();
21113
- const defaultKeyId = "default";
21114
- let jwkStr = this.sessionManager.jwk(defaultKeyId);
21115
- if (jwkStr) {
21116
- this.sessionKeyId = defaultKeyId;
21117
- } else {
21118
- this.sessionKeyId = this.sessionManager.createSessionKey(defaultKeyId);
21119
- jwkStr = this.sessionManager.jwk(this.sessionKeyId);
21120
- }
21121
- if (!jwkStr) {
21122
- throw new Error("Failed to get session key JWK");
21123
- }
21124
- this.sessionKeyJwk = JSON.parse(jwkStr);
21187
+ const sessionKey = createDefaultSessionKey(this.wasmBindings);
21188
+ this.sessionManager = sessionKey.manager;
21189
+ this.sessionKeyId = sessionKey.keyId;
21190
+ this.sessionKeyJwk = sessionKey.jwk;
21125
21191
  this._serviceGraph = this.createServiceGraphLifetime();
21126
21192
  this._capabilityRegistry = new CapabilityKeyRegistry();
21127
21193
  this._keyProvider = new WasmKeyProvider({
21128
21194
  sessionManager: this.sessionManager
21129
21195
  });
21130
21196
  this.notificationHandler = config.notificationHandler ?? new SilentNotificationHandler();
21131
- const receiveOnlyGraph = this._serviceGraph;
21132
- this._sharingService = new SharingService({
21133
- hosts: [this.config.host],
21134
- // session: undefined - not needed for receive()
21135
- invoke: receiveOnlyGraph.invoke,
21136
- fetch: receiveOnlyGraph.fetch,
21137
- assertActive: () => receiveOnlyGraph.assertActive(),
21138
- keyProvider: this._keyProvider,
21139
- registry: this._capabilityRegistry,
21140
- createDelegationWasm: (params) => this.createDelegationWrapper(params),
21141
- computeCid: (data, codec) => {
21142
- if (!this.wasmBindings.computeCid) throw new Error("computeCid is unavailable");
21143
- return this.wasmBindings.computeCid(data, codec);
21144
- },
21145
- // delegationManager: undefined - not needed for receive()
21146
- createKVService: (config2) => {
21147
- const prefix = config2.pathPrefix?.replace(/\/$/, "");
21148
- const kvService = new KVService2({ prefix });
21149
- const kvContext = receiveOnlyGraph.track(new ServiceContext2({
21150
- invoke: config2.invoke,
21151
- fetch: config2.fetch ?? globalThis.fetch.bind(globalThis),
21152
- hosts: config2.hosts,
21153
- telemetry: this.config.telemetry
21154
- }));
21155
- kvContext.setSession(config2.session);
21156
- kvService.initialize(kvContext);
21157
- return kvService;
21158
- }
21159
- });
21197
+ this._sharingService = this.createReceiveOnlySharing();
21160
21198
  if (config.signer) {
21161
21199
  this.signer = config.signer;
21162
21200
  this.setupAuth(config);
@@ -21298,6 +21336,42 @@ var _TinyCloudNode = class _TinyCloudNode {
21298
21336
  get siweDomain() {
21299
21337
  return this.config.domain ?? "app.tinycloud.xyz";
21300
21338
  }
21339
+ /**
21340
+ * SharingService for receive-only access (no session required), bound to the
21341
+ * current service graph. This lets session-only users receive sharing links
21342
+ * without signIn(); full capabilities (generate) are added after signIn().
21343
+ */
21344
+ createReceiveOnlySharing() {
21345
+ const receiveOnlyGraph = this._serviceGraph;
21346
+ return new SharingService({
21347
+ hosts: [this.config.host],
21348
+ // session: undefined - not needed for receive()
21349
+ invoke: receiveOnlyGraph.invoke,
21350
+ fetch: receiveOnlyGraph.fetch,
21351
+ assertActive: () => receiveOnlyGraph.assertActive(),
21352
+ keyProvider: this._keyProvider,
21353
+ registry: this._capabilityRegistry,
21354
+ createDelegationWasm: (params) => this.createDelegationWrapper(params),
21355
+ computeCid: (data, codec) => {
21356
+ if (!this.wasmBindings.computeCid) throw new Error("computeCid is unavailable");
21357
+ return this.wasmBindings.computeCid(data, codec);
21358
+ },
21359
+ // delegationManager: undefined - not needed for receive()
21360
+ createKVService: (config) => {
21361
+ const prefix = config.pathPrefix?.replace(/\/$/, "");
21362
+ const kvService = new KVService2({ prefix });
21363
+ const kvContext = receiveOnlyGraph.track(new ServiceContext2({
21364
+ invoke: config.invoke,
21365
+ fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
21366
+ hosts: config.hosts,
21367
+ telemetry: this.config.telemetry
21368
+ }));
21369
+ kvContext.setSession(config.session);
21370
+ kvService.initialize(kvContext);
21371
+ return kvService;
21372
+ }
21373
+ });
21374
+ }
21301
21375
  /**
21302
21376
  * Set up authorization handler and TinyCloud instance.
21303
21377
  * @internal
@@ -21557,6 +21631,115 @@ var _TinyCloudNode = class _TinyCloudNode {
21557
21631
  get restorableSession() {
21558
21632
  return this.currentTinyCloudSession();
21559
21633
  }
21634
+ /**
21635
+ * Sign out and end the session on this instance.
21636
+ *
21637
+ * Every service built for the session (and every `useDelegation()` result)
21638
+ * is retired and the session key is replaced before anything is awaited, so
21639
+ * nothing captured from the session can sign once sign-out has started.
21640
+ * Pending work is aborted and the persisted session is cleared from the
21641
+ * configured session storage. Afterwards session accessors return
21642
+ * `undefined` and service calls fail with "Not signed in" until
21643
+ * {@link signIn} or {@link restoreSession}, which wait for a sign-out in
21644
+ * progress.
21645
+ */
21646
+ signOut() {
21647
+ this.signOutInFlight ?? (this.signOutInFlight = this.endSession().finally(() => {
21648
+ this.signOutInFlight = void 0;
21649
+ }));
21650
+ return this.signOutInFlight;
21651
+ }
21652
+ async endSession() {
21653
+ const leavingAddress = this.address ?? this._restoredTcSession?.address;
21654
+ const leavingAuth = this.auth;
21655
+ try {
21656
+ this.teardownSession();
21657
+ } finally {
21658
+ try {
21659
+ await this.accountRegistryTail;
21660
+ if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
21661
+ } finally {
21662
+ if (leavingAddress) {
21663
+ await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
21664
+ }
21665
+ }
21666
+ }
21667
+ }
21668
+ /**
21669
+ * Synchronously drop every piece of in-memory session authority and
21670
+ * invalidate sign-ins and restores still in flight (see `sessionEpoch`).
21671
+ */
21672
+ teardownSession() {
21673
+ this.sessionEpoch++;
21674
+ this.pendingAccountRegistrySync?.controller.abort();
21675
+ const oldGraph = this._serviceGraph;
21676
+ const oldCore = this.tc;
21677
+ const oldAuth = this.auth;
21678
+ this.auth = null;
21679
+ this.tc = null;
21680
+ this._address = void 0;
21681
+ this._restoredTcSession = void 0;
21682
+ this._activeServiceSession = void 0;
21683
+ oldGraph.retire();
21684
+ oldCore?.retireServices();
21685
+ oldAuth?.resetSession(
21686
+ this.sessionManager,
21687
+ this.sessionKeyId,
21688
+ this.explicitHost ? [this.explicitHost] : void 0
21689
+ );
21690
+ this.config.host = this.explicitHost ?? DEFAULT_HOST;
21691
+ this._chainId = 1;
21692
+ this._restoredNodeFeatures = void 0;
21693
+ this._activeServiceSession = void 0;
21694
+ this._serviceContext = void 0;
21695
+ this._kv = void 0;
21696
+ this._sql = void 0;
21697
+ this._duckdb = void 0;
21698
+ this._hooks = void 0;
21699
+ this._vault = void 0;
21700
+ this._encryption = void 0;
21701
+ this._baseVaults.clear();
21702
+ this._baseSecrets.clear();
21703
+ this._secrets.clear();
21704
+ this._account = void 0;
21705
+ this._publicKV = void 0;
21706
+ this._delegationManager = void 0;
21707
+ this._spaceService = void 0;
21708
+ this.runtimePermissionGrants = [];
21709
+ this._recapOperationsCache = void 0;
21710
+ this.confirmedHostedSpaceIds.clear();
21711
+ this.hostedOwnedSpaceIds.clear();
21712
+ this.cancelOwnedSpaceHosts();
21713
+ this.storageFull.clear();
21714
+ this._bootstrapSkipped = false;
21715
+ this._bootstrapStatus = { skipped: false };
21716
+ this.replicationDelegateSession = void 0;
21717
+ this.replicationScopeError = void 0;
21718
+ const sessionKey = createDefaultSessionKey(this.wasmBindings);
21719
+ this.sessionManager = sessionKey.manager;
21720
+ this.sessionKeyId = sessionKey.keyId;
21721
+ this.sessionKeyJwk = sessionKey.jwk;
21722
+ oldAuth?.resetSession(
21723
+ sessionKey.manager,
21724
+ sessionKey.keyId,
21725
+ this.explicitHost ? [this.explicitHost] : void 0
21726
+ );
21727
+ this._serviceGraph = this.createServiceGraphLifetime();
21728
+ this.auth = oldAuth;
21729
+ this.tc = this.auth ? new TinyCloud(this.auth, {
21730
+ invokeAny: this.invokeAnyWithRuntimePermissions,
21731
+ telemetry: this.config.telemetry
21732
+ }) : null;
21733
+ this._capabilityRegistry = new CapabilityKeyRegistry();
21734
+ this._keyProvider = new WasmKeyProvider({ sessionManager: this.sessionManager });
21735
+ this._sharingService = this.createReceiveOnlySharing();
21736
+ }
21737
+ /** Throws when a sign-out started after `epoch` was read. */
21738
+ assertSessionEpoch(epoch, operation) {
21739
+ if (epoch !== this.sessionEpoch) {
21740
+ throw new Error(`${operation} was cancelled by sign-out.`);
21741
+ }
21742
+ }
21560
21743
  /**
21561
21744
  * Sign in and create a new session.
21562
21745
  * This creates the user's space if it doesn't exist.
@@ -21570,11 +21753,28 @@ var _TinyCloudNode = class _TinyCloudNode {
21570
21753
  "Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
21571
21754
  );
21572
21755
  }
21756
+ const epoch = this.sessionEpoch;
21757
+ const attempt = ++this.latestSessionAttempt;
21758
+ await this.signOutInFlight;
21759
+ try {
21760
+ await this.establishSignIn(epoch, options);
21761
+ } catch (error) {
21762
+ if (epoch !== this.sessionEpoch && attempt === this.latestSessionAttempt) {
21763
+ await this.endSession();
21764
+ }
21765
+ throw error;
21766
+ }
21767
+ }
21768
+ async establishSignIn(epoch, options) {
21769
+ if (!this.signer || !this.tc) {
21770
+ throw new Error("Not signed in. Call signIn() first.");
21771
+ }
21573
21772
  this.pendingAccountRegistrySync?.controller.abort();
21574
21773
  await this.accountRegistryTail;
21575
21774
  await this.wasmBindings.ensureInitialized?.();
21576
21775
  const address = canonicalizeAddress2(await this.signer.getAddress());
21577
21776
  const chainId = await this.signer.getChainId();
21777
+ this.assertSessionEpoch(epoch, "Sign-in");
21578
21778
  if (address !== this._address || chainId !== this._chainId) this.storageFull.clear();
21579
21779
  this._address = address;
21580
21780
  this._chainId = chainId;
@@ -21593,6 +21793,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21593
21793
  this.confirmedHostedSpaceIds.clear();
21594
21794
  this.hostedOwnedSpaceIds.clear();
21595
21795
  await this.tc.signIn(options);
21796
+ this.assertSessionEpoch(epoch, "Sign-in");
21596
21797
  this.cancelOwnedSpaceHosts();
21597
21798
  this.syncResolvedHostFromAuth();
21598
21799
  for (const spaceId of this.auth.lastActivationActivatedSpaceIds ?? []) {
@@ -21601,6 +21802,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21601
21802
  this.replicationDelegateSession = void 0;
21602
21803
  this.replicationScopeError = void 0;
21603
21804
  if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
21805
+ this.assertSessionEpoch(epoch, "Sign-in");
21604
21806
  const oldGraph = this._serviceGraph;
21605
21807
  this._serviceGraph = this.createServiceGraphLifetime();
21606
21808
  oldGraph.retire();
@@ -21611,15 +21813,19 @@ var _TinyCloudNode = class _TinyCloudNode {
21611
21813
  this.sessionKeyJwk = signedInSession.jwk;
21612
21814
  }
21613
21815
  await this.initializeServices();
21816
+ this.assertSessionEpoch(epoch, "Sign-in");
21614
21817
  const primarySession = this.currentTinyCloudSession();
21615
21818
  if (primarySession) {
21616
21819
  this.registerPrimarySessionGrant(primarySession);
21617
21820
  }
21618
21821
  const bootstrapped = await this.bootstrapAccountIfNeeded();
21822
+ this.assertSessionEpoch(epoch, "Sign-in");
21619
21823
  await this.ensureRequestedEncryptionNetworks();
21824
+ this.assertSessionEpoch(epoch, "Sign-in");
21620
21825
  if (!bootstrapped && this.config.manifest === void 0 && this.config.capabilityRequest === void 0) {
21621
21826
  await this.ensureOwnedSpaceHostedById(this.ownedSpaceId(SECRETS_SPACE2));
21622
21827
  }
21828
+ this.assertSessionEpoch(epoch, "Sign-in");
21623
21829
  if (!bootstrapped) {
21624
21830
  this.scheduleAccountRegistrySync();
21625
21831
  }
@@ -21801,6 +22007,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21801
22007
  throw new Error("Account bootstrap requires a TinyCloud host");
21802
22008
  }
21803
22009
  const auth = this.auth;
22010
+ const graph = this._serviceGraph;
21804
22011
  const sessions = /* @__PURE__ */ new Map();
21805
22012
  const rawAbilitiesBySpace = /* @__PURE__ */ new Map();
21806
22013
  const warnings = [];
@@ -21809,6 +22016,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21809
22016
  const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === BOOTSTRAP_SESSION_REQUESTS.default;
21810
22017
  for (const step of steps) {
21811
22018
  if (step.kind !== "session") continue;
22019
+ graph.assertActive();
21812
22020
  if (step.space === "default" && canReusePrimaryBootstrapSession && primarySession) {
21813
22021
  sessions.set(step.space, primarySession);
21814
22022
  continue;
@@ -21836,6 +22044,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21836
22044
  }
21837
22045
  for (const step of steps) {
21838
22046
  if (step.kind !== "host") continue;
22047
+ graph.assertActive();
21839
22048
  const hosted = await auth.hostOwnedSpaceResult(step.spaceId, "bootstrap-host");
21840
22049
  if (!hosted.success) {
21841
22050
  throw Object.assign(
@@ -21847,11 +22056,16 @@ var _TinyCloudNode = class _TinyCloudNode {
21847
22056
  await new Promise((resolve) => setTimeout(resolve, 100));
21848
22057
  for (const step of steps) {
21849
22058
  if (step.kind !== "activate") continue;
22059
+ graph.assertActive();
21850
22060
  const session = sessions.get(step.space);
21851
22061
  if (!session) {
21852
22062
  throw new Error(`Missing bootstrap session for ${step.space}`);
21853
22063
  }
21854
- const activated = await activateSessionWithHost2(host, session.delegationHeader);
22064
+ const activated = await activateSessionWithHost2(host, session.delegationHeader, graph.signal).catch((error) => {
22065
+ graph.assertActive();
22066
+ throw error;
22067
+ });
22068
+ graph.assertActive();
21855
22069
  if (!activated.success || activated.skipped?.includes(step.spaceId)) {
21856
22070
  throw Object.assign(
21857
22071
  new Error(
@@ -21867,6 +22081,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21867
22081
  );
21868
22082
  }
21869
22083
  for (const step of steps) {
22084
+ graph.assertActive();
21870
22085
  if (step.kind === "account-index-schema") {
21871
22086
  const ensured = await this.account.index.ensure();
21872
22087
  if (!ensured.ok) {
@@ -22642,6 +22857,9 @@ var _TinyCloudNode = class _TinyCloudNode {
22642
22857
  * @param sessionData - The stored delegation data from the browser flow
22643
22858
  */
22644
22859
  async restoreSession(sessionData) {
22860
+ const epoch = this.sessionEpoch;
22861
+ const attempt = ++this.latestSessionAttempt;
22862
+ await this.signOutInFlight;
22645
22863
  await this.wasmBindings.ensureInitialized?.();
22646
22864
  let restoredJwk;
22647
22865
  try {
@@ -22831,6 +23049,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22831
23049
  });
22832
23050
  this.pendingAccountRegistrySync?.controller.abort();
22833
23051
  await this.accountRegistryTail;
23052
+ this.assertSessionEpoch(epoch, "Session restore");
22834
23053
  const oldGraph = this._serviceGraph;
22835
23054
  const oldCore = this.tc;
22836
23055
  this.sessionManager = stagedManager;
@@ -22876,6 +23095,12 @@ var _TinyCloudNode = class _TinyCloudNode {
22876
23095
  this.replicationDelegateSession = compactReplicationSession;
22877
23096
  this.replicationScopeError = replicationScopeError;
22878
23097
  if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
23098
+ if (epoch !== this.sessionEpoch) {
23099
+ oldGraph.retire();
23100
+ oldCore?.retireServices?.();
23101
+ if (attempt === this.latestSessionAttempt) await this.endSession();
23102
+ this.assertSessionEpoch(epoch, "Session restore");
23103
+ }
22879
23104
  oldGraph.retire();
22880
23105
  if (replicationScopeError) {
22881
23106
  stagedGraph.serviceContext.emit("replication.state", {
@@ -22886,6 +23111,10 @@ var _TinyCloudNode = class _TinyCloudNode {
22886
23111
  }
22887
23112
  await this.bindReplicationRuntime(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
22888
23113
  oldCore?.retireServices?.();
23114
+ if (epoch !== this.sessionEpoch) {
23115
+ if (attempt === this.latestSessionAttempt) await this.endSession();
23116
+ this.assertSessionEpoch(epoch, "Session restore");
23117
+ }
22889
23118
  }
22890
23119
  stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
22891
23120
  if (!session?.siwe) return { cache: void 0, grants: [] };
@@ -24744,6 +24973,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24744
24973
  * matching service calls and downstream `delegateTo()` calls can use it.
24745
24974
  */
24746
24975
  async useRuntimeDelegation(delegation) {
24976
+ const graph = this._serviceGraph;
24747
24977
  const session = this.currentTinyCloudSession();
24748
24978
  if (!session) {
24749
24979
  throw new SessionExpiredError(/* @__PURE__ */ new Date(0));
@@ -24762,9 +24992,11 @@ var _TinyCloudNode = class _TinyCloudNode {
24762
24992
  try {
24763
24993
  activateResult = await activateSessionWithHost2(
24764
24994
  targetHost,
24765
- delegation.delegationHeader
24995
+ delegation.delegationHeader,
24996
+ graph.signal
24766
24997
  );
24767
24998
  } catch (cause) {
24999
+ graph.assertActive();
24768
25000
  throw Object.assign(
24769
25001
  new Error("The TinyCloud host could not be reached to activate the delegation."),
24770
25002
  { code: "ACTIVATION_TRANSPORT_FAILED", cause }
@@ -24784,6 +25016,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24784
25016
  { cause: activateResult }
24785
25017
  );
24786
25018
  }
25019
+ graph.assertActive();
24787
25020
  this.runtimePermissionGrants = this.runtimePermissionGrants.filter(
24788
25021
  (grant) => grant.delegation.cid !== delegation.cid
24789
25022
  );
@@ -24800,6 +25033,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24800
25033
  if (!Array.isArray(permissions) || permissions.length === 0) {
24801
25034
  throw new Error("grantRuntimePermissions requires a non-empty permissions array");
24802
25035
  }
25036
+ const graph = this._serviceGraph;
24803
25037
  const session = this.currentTinyCloudSession();
24804
25038
  if (!session) {
24805
25039
  throw new SessionExpiredError(/* @__PURE__ */ new Date(0));
@@ -24880,14 +25114,19 @@ var _TinyCloudNode = class _TinyCloudNode {
24880
25114
  }
24881
25115
  })();
24882
25116
  const signature2 = await this.signer.signMessage(prepared.siwe);
25117
+ graph.assertActive();
24883
25118
  const delegatedSession = this.wasmBindings.completeSessionSetup({
24884
25119
  ...prepared,
24885
25120
  signature: signature2
24886
25121
  });
24887
25122
  const activateResult = await activateSessionWithHost2(
24888
25123
  this.config.host,
24889
- delegatedSession.delegationHeader
24890
- );
25124
+ delegatedSession.delegationHeader,
25125
+ graph.signal
25126
+ ).catch((error) => {
25127
+ graph.assertActive();
25128
+ throw error;
25129
+ });
24891
25130
  if (!activateResult.success) {
24892
25131
  if (rawForDelegation.some((entry) => this.isRawRevocationPermissionEntry(entry)) && activateResult.status === 401 && /Cannot find parent delegation/i.test(activateResult.error ?? "")) {
24893
25132
  throw Object.assign(
@@ -24908,6 +25147,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24908
25147
  expiresAt,
24909
25148
  { siwe: prepared.siwe, signature: signature2 }
24910
25149
  );
25150
+ graph.assertActive();
24911
25151
  this.runtimePermissionGrants.push({
24912
25152
  session: {
24913
25153
  delegationHeader: delegatedSession.delegationHeader,
@@ -26371,6 +26611,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26371
26611
  * @returns A DelegatedAccess instance for performing operations
26372
26612
  */
26373
26613
  async useDelegation(delegation) {
26614
+ const graph = this._serviceGraph;
26374
26615
  const delegationHeader = delegation.delegationHeader;
26375
26616
  const targetHost = delegation.host ?? this.config.host;
26376
26617
  if (this.isSessionOnly) {
@@ -26412,7 +26653,8 @@ var _TinyCloudNode = class _TinyCloudNode {
26412
26653
  targetHost,
26413
26654
  this.wasmBindings.invoke,
26414
26655
  this.wasmBindings.invokeAny,
26415
- this.config.telemetry
26656
+ this.config.telemetry,
26657
+ graph
26416
26658
  );
26417
26659
  }
26418
26660
  const mySession = this.auth?.tinyCloudSession;
@@ -26436,14 +26678,19 @@ var _TinyCloudNode = class _TinyCloudNode {
26436
26678
  ...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
26437
26679
  });
26438
26680
  const signature2 = await this.signer.signMessage(prepared.siwe);
26681
+ graph.assertActive();
26439
26682
  const invokerSession = this.wasmBindings.completeSessionSetup({
26440
26683
  ...prepared,
26441
26684
  signature: signature2
26442
26685
  });
26443
26686
  const activateResult = await activateSessionWithHost2(
26444
26687
  targetHost,
26445
- invokerSession.delegationHeader
26446
- );
26688
+ invokerSession.delegationHeader,
26689
+ graph.signal
26690
+ ).catch((error) => {
26691
+ graph.assertActive();
26692
+ throw error;
26693
+ });
26447
26694
  if (!activateResult.success) {
26448
26695
  throw Object.assign(
26449
26696
  new Error(`Failed to activate delegated session: ${describeHostFailure2(activateResult)}`),
@@ -26462,6 +26709,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26462
26709
  siwe: prepared.siwe,
26463
26710
  signature: signature2
26464
26711
  };
26712
+ graph.assertActive();
26465
26713
  this.trackReceivedDelegation(delegation, jwk);
26466
26714
  this.installRuntimeGrantFromServiceSession(
26467
26715
  delegation,
@@ -26480,7 +26728,8 @@ var _TinyCloudNode = class _TinyCloudNode {
26480
26728
  targetHost,
26481
26729
  this.wasmBindings.invoke,
26482
26730
  this.wasmBindings.invokeAny,
26483
- this.config.telemetry
26731
+ this.config.telemetry,
26732
+ graph
26484
26733
  );
26485
26734
  }
26486
26735
  /**