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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -18494,6 +18494,13 @@ function cloneAbilitiesMap(actions) {
18494
18494
  var NodeUserAuthorization = class {
18495
18495
  constructor(config) {
18496
18496
  this.activeSessionKeyId = "default";
18497
+ /** Incremented by resetSession; a sign-in started earlier publishes nothing. */
18498
+ this.sessionGeneration = 0;
18499
+ /**
18500
+ * Sign-in saves run one at a time, in order, each together with its own
18501
+ * cancellation cleanup, so no later write can land between the two.
18502
+ */
18503
+ this.persistenceQueue = Promise.resolve();
18497
18504
  this.extensions = [];
18498
18505
  this._nodeFeatures = [];
18499
18506
  this._nodeFeaturesKnown = false;
@@ -18624,6 +18631,27 @@ var NodeUserAuthorization = class {
18624
18631
  this.tinycloudHosts = [...hosts];
18625
18632
  }
18626
18633
  }
18634
+ /**
18635
+ * @internal End the session: adopt a fresh session manager whose active key
18636
+ * is `sessionKeyId`, and return to the constructed state.
18637
+ */
18638
+ resetSession(sessionManager, sessionKeyId, hosts) {
18639
+ this.sessionGeneration++;
18640
+ this.installRestoredSession(sessionManager, void 0, hosts);
18641
+ this.activeSessionKeyId = sessionKeyId;
18642
+ }
18643
+ /** Throws when the session was reset (signed out) after `generation` was read. */
18644
+ assertSignInCurrent(generation) {
18645
+ if (generation !== this.sessionGeneration) {
18646
+ throw new Error("Sign-in was cancelled by sign-out.");
18647
+ }
18648
+ }
18649
+ /** Runs `task` after every earlier queued persistence task has settled. */
18650
+ enqueuePersistence(task) {
18651
+ const run = this.persistenceQueue.then(task);
18652
+ this.persistenceQueue = run.catch(() => void 0);
18653
+ return run;
18654
+ }
18627
18655
  /** @internal Atomically adopt the state staged by TinyCloudNode.restoreSession. */
18628
18656
  installRestoredSession(sessionManager, session, hosts, nodeFeatures) {
18629
18657
  this.sessionManager = sessionManager;
@@ -19312,11 +19340,14 @@ var NodeUserAuthorization = class {
19312
19340
  * @param options - Optional per-call SIWE overrides for this sign-in only
19313
19341
  */
19314
19342
  async signIn(options) {
19315
- this._address = (0, import_sdk_core4.canonicalizeAddress)(await this.signer.getAddress());
19316
- this._chainId = await this.signer.getChainId();
19317
- const address = this._address;
19318
- const chainId = this._chainId;
19343
+ const generation = this.sessionGeneration;
19344
+ const address = (0, import_sdk_core4.canonicalizeAddress)(await this.signer.getAddress());
19345
+ const chainId = await this.signer.getChainId();
19346
+ this.assertSignInCurrent(generation);
19347
+ this._address = address;
19348
+ this._chainId = chainId;
19319
19349
  await this.resolveTinyCloudHostsForSignIn(address, chainId);
19350
+ this.assertSignInCurrent(generation);
19320
19351
  const keyId = `session-${Date.now()}`;
19321
19352
  this.sessionManager.renameSessionKeyId(this.activeSessionKeyId, keyId);
19322
19353
  this.activeSessionKeyId = keyId;
@@ -19349,6 +19380,7 @@ var NodeUserAuthorization = class {
19349
19380
  type: "siwe",
19350
19381
  purpose: "sign-in"
19351
19382
  });
19383
+ this.assertSignInCurrent(generation);
19352
19384
  const session = this.wasm.completeSessionSetup({
19353
19385
  ...prepared,
19354
19386
  signature: signature2
@@ -19393,18 +19425,27 @@ var NodeUserAuthorization = class {
19393
19425
  version: "1.0",
19394
19426
  tinycloudHosts: this.tinycloudHosts
19395
19427
  };
19396
- await this.sessionStorage.save(address, persistedData);
19428
+ await this.enqueuePersistence(async () => {
19429
+ await this.sessionStorage.save(address, persistedData);
19430
+ if (generation !== this.sessionGeneration) {
19431
+ await this.sessionStorage.clear(address);
19432
+ }
19433
+ });
19434
+ this.assertSignInCurrent(generation);
19397
19435
  this._session = clientSession;
19398
19436
  this._tinyCloudSession = tinyCloudSession;
19399
19437
  this._address = address;
19400
19438
  this._chainId = chainId;
19401
19439
  await this.refreshNodeInfo();
19440
+ this.assertSignInCurrent(generation);
19402
19441
  for (const ext of this.extensions) {
19403
19442
  if (ext.afterSignIn) {
19404
19443
  await ext.afterSignIn(clientSession);
19444
+ this.assertSignInCurrent(generation);
19405
19445
  }
19406
19446
  }
19407
19447
  await this.ensureSpaceExists();
19448
+ this.assertSignInCurrent(generation);
19408
19449
  return clientSession;
19409
19450
  }
19410
19451
  /**
@@ -20041,17 +20082,24 @@ var import_sdk_core5 = require("@tinycloud/sdk-core");
20041
20082
  // src/DelegatedAccess.ts
20042
20083
  var import_sdk_core6 = require("@tinycloud/sdk-core");
20043
20084
  var DelegatedAccess = class {
20044
- constructor(session, delegation, host, invoke2, invokeAny2, telemetry) {
20085
+ /**
20086
+ * @param lifetime - Optional owner of this access: its `fetch` is used for
20087
+ * requests and the service context is registered with `track`, so the
20088
+ * owner can cancel and retire the access (TinyCloudNode does this when its
20089
+ * session is replaced or signed out).
20090
+ */
20091
+ constructor(session, delegation, host, invoke2, invokeAny2, telemetry, lifetime) {
20045
20092
  this.session = session;
20046
20093
  this._delegation = delegation;
20047
20094
  this.host = host;
20048
- this._serviceContext = new import_sdk_core6.ServiceContext({
20095
+ const context = new import_sdk_core6.ServiceContext({
20049
20096
  invoke: invoke2,
20050
20097
  invokeAny: invokeAny2,
20051
- fetch: globalThis.fetch.bind(globalThis),
20098
+ fetch: lifetime?.fetch ?? globalThis.fetch.bind(globalThis),
20052
20099
  hosts: [host],
20053
20100
  telemetry
20054
20101
  });
20102
+ this._serviceContext = lifetime ? lifetime.track(context) : context;
20055
20103
  const prefix = this._delegation.path.replace(/\/$/, "");
20056
20104
  this._kv = new import_sdk_core6.KVService({ prefix });
20057
20105
  this._kv.initialize(this._serviceContext);
@@ -20625,6 +20673,19 @@ function exactVerifiedRecap(verified) {
20625
20673
  caveats: cloneRecapCaveats(entry.caveats)
20626
20674
  }));
20627
20675
  }
20676
+ function createDefaultSessionKey(wasm) {
20677
+ const manager = wasm.createSessionManager();
20678
+ let keyId = "default";
20679
+ let jwkStr = manager.jwk(keyId);
20680
+ if (!jwkStr) {
20681
+ keyId = manager.createSessionKey(keyId);
20682
+ jwkStr = manager.jwk(keyId);
20683
+ }
20684
+ if (!jwkStr) {
20685
+ throw new Error("Failed to get session key JWK");
20686
+ }
20687
+ return { manager, keyId, jwk: JSON.parse(jwkStr) };
20688
+ }
20628
20689
  var ServiceGraphLifetime = class {
20629
20690
  constructor(invokeFn, invokeAnyFn, fetchFn, observeContext) {
20630
20691
  this.invokeFn = invokeFn;
@@ -20674,9 +20735,13 @@ var ServiceGraphLifetime = class {
20674
20735
  this.contexts.clear();
20675
20736
  }
20676
20737
  }
20738
+ /** Aborted when the graph retires: cancels requests made for its session. */
20739
+ get signal() {
20740
+ return this.abortController.signal;
20741
+ }
20677
20742
  assertActive() {
20678
20743
  if (this.retired) {
20679
- throw new Error("Service graph has been retired by session replacement.");
20744
+ throw new Error("Service graph has been retired: its session was replaced or signed out.");
20680
20745
  }
20681
20746
  }
20682
20747
  combineSignals(signal) {
@@ -20978,6 +21043,16 @@ var _TinyCloudNode = class _TinyCloudNode {
20978
21043
  * so it must outlast a slow but healthy sync (9–18 s on prod 1.20.2, TC-998).
20979
21044
  */
20980
21045
  this.accountRegistryDeadlineMs = 3e4;
21046
+ /**
21047
+ * Incremented by every sign-out. A sign-in or restore records it when it
21048
+ * starts and does not establish its session once it has changed.
21049
+ */
21050
+ this.sessionEpoch = 0;
21051
+ /**
21052
+ * Numbers sign-in and restore attempts. A cancelled attempt cleans up only
21053
+ * while it is still the latest, so it never ends a newer session.
21054
+ */
21055
+ this.latestSessionAttempt = 0;
20981
21056
  /**
20982
21057
  * True when the last signIn() detected an interactive signer and skipped
20983
21058
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -21111,54 +21186,17 @@ var _TinyCloudNode = class _TinyCloudNode {
21111
21186
  "wasmBindings must be provided in config. Import from '@tinycloud/node-sdk' (not '/core') for automatic Node.js defaults."
21112
21187
  );
21113
21188
  }
21114
- this.sessionManager = this.wasmBindings.createSessionManager();
21115
- const defaultKeyId = "default";
21116
- let jwkStr = this.sessionManager.jwk(defaultKeyId);
21117
- if (jwkStr) {
21118
- this.sessionKeyId = defaultKeyId;
21119
- } else {
21120
- this.sessionKeyId = this.sessionManager.createSessionKey(defaultKeyId);
21121
- jwkStr = this.sessionManager.jwk(this.sessionKeyId);
21122
- }
21123
- if (!jwkStr) {
21124
- throw new Error("Failed to get session key JWK");
21125
- }
21126
- this.sessionKeyJwk = JSON.parse(jwkStr);
21189
+ const sessionKey = createDefaultSessionKey(this.wasmBindings);
21190
+ this.sessionManager = sessionKey.manager;
21191
+ this.sessionKeyId = sessionKey.keyId;
21192
+ this.sessionKeyJwk = sessionKey.jwk;
21127
21193
  this._serviceGraph = this.createServiceGraphLifetime();
21128
21194
  this._capabilityRegistry = new import_sdk_core9.CapabilityKeyRegistry();
21129
21195
  this._keyProvider = new WasmKeyProvider({
21130
21196
  sessionManager: this.sessionManager
21131
21197
  });
21132
21198
  this.notificationHandler = config.notificationHandler ?? new import_sdk_core9.SilentNotificationHandler();
21133
- const receiveOnlyGraph = this._serviceGraph;
21134
- this._sharingService = new import_sdk_core9.SharingService({
21135
- hosts: [this.config.host],
21136
- // session: undefined - not needed for receive()
21137
- invoke: receiveOnlyGraph.invoke,
21138
- fetch: receiveOnlyGraph.fetch,
21139
- assertActive: () => receiveOnlyGraph.assertActive(),
21140
- keyProvider: this._keyProvider,
21141
- registry: this._capabilityRegistry,
21142
- createDelegationWasm: (params) => this.createDelegationWrapper(params),
21143
- computeCid: (data, codec) => {
21144
- if (!this.wasmBindings.computeCid) throw new Error("computeCid is unavailable");
21145
- return this.wasmBindings.computeCid(data, codec);
21146
- },
21147
- // delegationManager: undefined - not needed for receive()
21148
- createKVService: (config2) => {
21149
- const prefix = config2.pathPrefix?.replace(/\/$/, "");
21150
- const kvService = new import_sdk_core9.KVService({ prefix });
21151
- const kvContext = receiveOnlyGraph.track(new import_sdk_core9.ServiceContext({
21152
- invoke: config2.invoke,
21153
- fetch: config2.fetch ?? globalThis.fetch.bind(globalThis),
21154
- hosts: config2.hosts,
21155
- telemetry: this.config.telemetry
21156
- }));
21157
- kvContext.setSession(config2.session);
21158
- kvService.initialize(kvContext);
21159
- return kvService;
21160
- }
21161
- });
21199
+ this._sharingService = this.createReceiveOnlySharing();
21162
21200
  if (config.signer) {
21163
21201
  this.signer = config.signer;
21164
21202
  this.setupAuth(config);
@@ -21300,6 +21338,42 @@ var _TinyCloudNode = class _TinyCloudNode {
21300
21338
  get siweDomain() {
21301
21339
  return this.config.domain ?? "app.tinycloud.xyz";
21302
21340
  }
21341
+ /**
21342
+ * SharingService for receive-only access (no session required), bound to the
21343
+ * current service graph. This lets session-only users receive sharing links
21344
+ * without signIn(); full capabilities (generate) are added after signIn().
21345
+ */
21346
+ createReceiveOnlySharing() {
21347
+ const receiveOnlyGraph = this._serviceGraph;
21348
+ return new import_sdk_core9.SharingService({
21349
+ hosts: [this.config.host],
21350
+ // session: undefined - not needed for receive()
21351
+ invoke: receiveOnlyGraph.invoke,
21352
+ fetch: receiveOnlyGraph.fetch,
21353
+ assertActive: () => receiveOnlyGraph.assertActive(),
21354
+ keyProvider: this._keyProvider,
21355
+ registry: this._capabilityRegistry,
21356
+ createDelegationWasm: (params) => this.createDelegationWrapper(params),
21357
+ computeCid: (data, codec) => {
21358
+ if (!this.wasmBindings.computeCid) throw new Error("computeCid is unavailable");
21359
+ return this.wasmBindings.computeCid(data, codec);
21360
+ },
21361
+ // delegationManager: undefined - not needed for receive()
21362
+ createKVService: (config) => {
21363
+ const prefix = config.pathPrefix?.replace(/\/$/, "");
21364
+ const kvService = new import_sdk_core9.KVService({ prefix });
21365
+ const kvContext = receiveOnlyGraph.track(new import_sdk_core9.ServiceContext({
21366
+ invoke: config.invoke,
21367
+ fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
21368
+ hosts: config.hosts,
21369
+ telemetry: this.config.telemetry
21370
+ }));
21371
+ kvContext.setSession(config.session);
21372
+ kvService.initialize(kvContext);
21373
+ return kvService;
21374
+ }
21375
+ });
21376
+ }
21303
21377
  /**
21304
21378
  * Set up authorization handler and TinyCloud instance.
21305
21379
  * @internal
@@ -21559,6 +21633,102 @@ var _TinyCloudNode = class _TinyCloudNode {
21559
21633
  get restorableSession() {
21560
21634
  return this.currentTinyCloudSession();
21561
21635
  }
21636
+ /**
21637
+ * Sign out and end the session on this instance.
21638
+ *
21639
+ * Every service built for the session (and every `useDelegation()` result)
21640
+ * is retired and the session key is replaced before anything is awaited, so
21641
+ * nothing captured from the session can sign once sign-out has started.
21642
+ * Pending work is aborted and the persisted session is cleared from the
21643
+ * configured session storage. Afterwards session accessors return
21644
+ * `undefined` and service calls fail with "Not signed in" until
21645
+ * {@link signIn} or {@link restoreSession}, which wait for a sign-out in
21646
+ * progress.
21647
+ */
21648
+ signOut() {
21649
+ this.signOutInFlight ?? (this.signOutInFlight = this.endSession().finally(() => {
21650
+ this.signOutInFlight = void 0;
21651
+ }));
21652
+ return this.signOutInFlight;
21653
+ }
21654
+ async endSession() {
21655
+ const leavingAddress = this.address ?? this._restoredTcSession?.address;
21656
+ this.teardownSession();
21657
+ try {
21658
+ await this.accountRegistryTail;
21659
+ if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
21660
+ } finally {
21661
+ if (leavingAddress) await this.clearPersistedSessionFor(leavingAddress);
21662
+ }
21663
+ }
21664
+ /**
21665
+ * Synchronously drop every piece of in-memory session authority and
21666
+ * invalidate sign-ins and restores still in flight (see `sessionEpoch`).
21667
+ */
21668
+ teardownSession() {
21669
+ this.sessionEpoch++;
21670
+ this.pendingAccountRegistrySync?.controller.abort();
21671
+ const oldGraph = this._serviceGraph;
21672
+ const oldCore = this.tc;
21673
+ const sessionKey = createDefaultSessionKey(this.wasmBindings);
21674
+ this.sessionManager = sessionKey.manager;
21675
+ this.sessionKeyId = sessionKey.keyId;
21676
+ this.sessionKeyJwk = sessionKey.jwk;
21677
+ this.auth?.resetSession(
21678
+ sessionKey.manager,
21679
+ sessionKey.keyId,
21680
+ this.explicitHost ? [this.explicitHost] : void 0
21681
+ );
21682
+ this.config.host = this.explicitHost ?? DEFAULT_HOST;
21683
+ this._address = void 0;
21684
+ this._chainId = 1;
21685
+ this._restoredTcSession = void 0;
21686
+ this._restoredNodeFeatures = void 0;
21687
+ this._activeServiceSession = void 0;
21688
+ this._serviceContext = void 0;
21689
+ this._kv = void 0;
21690
+ this._sql = void 0;
21691
+ this._duckdb = void 0;
21692
+ this._hooks = void 0;
21693
+ this._vault = void 0;
21694
+ this._encryption = void 0;
21695
+ this._baseVaults.clear();
21696
+ this._baseSecrets.clear();
21697
+ this._secrets.clear();
21698
+ this._account = void 0;
21699
+ this._publicKV = void 0;
21700
+ this._delegationManager = void 0;
21701
+ this._spaceService = void 0;
21702
+ this.runtimePermissionGrants = [];
21703
+ this._recapOperationsCache = void 0;
21704
+ this.confirmedHostedSpaceIds.clear();
21705
+ this.hostedOwnedSpaceIds.clear();
21706
+ this.cancelOwnedSpaceHosts();
21707
+ this.storageFull.clear();
21708
+ this._bootstrapSkipped = false;
21709
+ this._bootstrapStatus = { skipped: false };
21710
+ this.replicationDelegateSession = void 0;
21711
+ this.replicationScopeError = void 0;
21712
+ this._serviceGraph = this.createServiceGraphLifetime();
21713
+ oldGraph.retire();
21714
+ this.tc = this.auth ? new import_sdk_core9.TinyCloud(this.auth, {
21715
+ invokeAny: this.invokeAnyWithRuntimePermissions,
21716
+ telemetry: this.config.telemetry
21717
+ }) : null;
21718
+ oldCore?.retireServices();
21719
+ this._capabilityRegistry = new import_sdk_core9.CapabilityKeyRegistry();
21720
+ this._keyProvider = new WasmKeyProvider({ sessionManager: this.sessionManager });
21721
+ this._sharingService = this.createReceiveOnlySharing();
21722
+ }
21723
+ async clearPersistedSessionFor(address) {
21724
+ await (this.auth ? this.auth.clearPersistedSession(address) : this.config.sessionStorage?.clear(address));
21725
+ }
21726
+ /** Throws when a sign-out started after `epoch` was read. */
21727
+ assertSessionEpoch(epoch, operation) {
21728
+ if (epoch !== this.sessionEpoch) {
21729
+ throw new Error(`${operation} was cancelled by sign-out.`);
21730
+ }
21731
+ }
21562
21732
  /**
21563
21733
  * Sign in and create a new session.
21564
21734
  * This creates the user's space if it doesn't exist.
@@ -21572,11 +21742,28 @@ var _TinyCloudNode = class _TinyCloudNode {
21572
21742
  "Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
21573
21743
  );
21574
21744
  }
21745
+ const epoch = this.sessionEpoch;
21746
+ const attempt = ++this.latestSessionAttempt;
21747
+ await this.signOutInFlight;
21748
+ try {
21749
+ await this.establishSignIn(epoch, options);
21750
+ } catch (error) {
21751
+ if (epoch !== this.sessionEpoch && attempt === this.latestSessionAttempt) {
21752
+ await this.endSession();
21753
+ }
21754
+ throw error;
21755
+ }
21756
+ }
21757
+ async establishSignIn(epoch, options) {
21758
+ if (!this.signer || !this.tc) {
21759
+ throw new Error("Not signed in. Call signIn() first.");
21760
+ }
21575
21761
  this.pendingAccountRegistrySync?.controller.abort();
21576
21762
  await this.accountRegistryTail;
21577
21763
  await this.wasmBindings.ensureInitialized?.();
21578
21764
  const address = (0, import_sdk_core9.canonicalizeAddress)(await this.signer.getAddress());
21579
21765
  const chainId = await this.signer.getChainId();
21766
+ this.assertSessionEpoch(epoch, "Sign-in");
21580
21767
  if (address !== this._address || chainId !== this._chainId) this.storageFull.clear();
21581
21768
  this._address = address;
21582
21769
  this._chainId = chainId;
@@ -21595,6 +21782,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21595
21782
  this.confirmedHostedSpaceIds.clear();
21596
21783
  this.hostedOwnedSpaceIds.clear();
21597
21784
  await this.tc.signIn(options);
21785
+ this.assertSessionEpoch(epoch, "Sign-in");
21598
21786
  this.cancelOwnedSpaceHosts();
21599
21787
  this.syncResolvedHostFromAuth();
21600
21788
  for (const spaceId of this.auth.lastActivationActivatedSpaceIds ?? []) {
@@ -21603,6 +21791,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21603
21791
  this.replicationDelegateSession = void 0;
21604
21792
  this.replicationScopeError = void 0;
21605
21793
  if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
21794
+ this.assertSessionEpoch(epoch, "Sign-in");
21606
21795
  const oldGraph = this._serviceGraph;
21607
21796
  this._serviceGraph = this.createServiceGraphLifetime();
21608
21797
  oldGraph.retire();
@@ -21613,15 +21802,19 @@ var _TinyCloudNode = class _TinyCloudNode {
21613
21802
  this.sessionKeyJwk = signedInSession.jwk;
21614
21803
  }
21615
21804
  await this.initializeServices();
21805
+ this.assertSessionEpoch(epoch, "Sign-in");
21616
21806
  const primarySession = this.currentTinyCloudSession();
21617
21807
  if (primarySession) {
21618
21808
  this.registerPrimarySessionGrant(primarySession);
21619
21809
  }
21620
21810
  const bootstrapped = await this.bootstrapAccountIfNeeded();
21811
+ this.assertSessionEpoch(epoch, "Sign-in");
21621
21812
  await this.ensureRequestedEncryptionNetworks();
21813
+ this.assertSessionEpoch(epoch, "Sign-in");
21622
21814
  if (!bootstrapped && this.config.manifest === void 0 && this.config.capabilityRequest === void 0) {
21623
21815
  await this.ensureOwnedSpaceHostedById(this.ownedSpaceId(import_sdk_core9.SECRETS_SPACE));
21624
21816
  }
21817
+ this.assertSessionEpoch(epoch, "Sign-in");
21625
21818
  if (!bootstrapped) {
21626
21819
  this.scheduleAccountRegistrySync();
21627
21820
  }
@@ -21803,6 +21996,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21803
21996
  throw new Error("Account bootstrap requires a TinyCloud host");
21804
21997
  }
21805
21998
  const auth = this.auth;
21999
+ const graph = this._serviceGraph;
21806
22000
  const sessions = /* @__PURE__ */ new Map();
21807
22001
  const rawAbilitiesBySpace = /* @__PURE__ */ new Map();
21808
22002
  const warnings = [];
@@ -21811,6 +22005,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21811
22005
  const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === import_sdk_core9.BOOTSTRAP_SESSION_REQUESTS.default;
21812
22006
  for (const step of steps) {
21813
22007
  if (step.kind !== "session") continue;
22008
+ graph.assertActive();
21814
22009
  if (step.space === "default" && canReusePrimaryBootstrapSession && primarySession) {
21815
22010
  sessions.set(step.space, primarySession);
21816
22011
  continue;
@@ -21838,6 +22033,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21838
22033
  }
21839
22034
  for (const step of steps) {
21840
22035
  if (step.kind !== "host") continue;
22036
+ graph.assertActive();
21841
22037
  const hosted = await auth.hostOwnedSpaceResult(step.spaceId, "bootstrap-host");
21842
22038
  if (!hosted.success) {
21843
22039
  throw Object.assign(
@@ -21849,11 +22045,16 @@ var _TinyCloudNode = class _TinyCloudNode {
21849
22045
  await new Promise((resolve) => setTimeout(resolve, 100));
21850
22046
  for (const step of steps) {
21851
22047
  if (step.kind !== "activate") continue;
22048
+ graph.assertActive();
21852
22049
  const session = sessions.get(step.space);
21853
22050
  if (!session) {
21854
22051
  throw new Error(`Missing bootstrap session for ${step.space}`);
21855
22052
  }
21856
- const activated = await (0, import_sdk_core9.activateSessionWithHost)(host, session.delegationHeader);
22053
+ const activated = await (0, import_sdk_core9.activateSessionWithHost)(host, session.delegationHeader, graph.signal).catch((error) => {
22054
+ graph.assertActive();
22055
+ throw error;
22056
+ });
22057
+ graph.assertActive();
21857
22058
  if (!activated.success || activated.skipped?.includes(step.spaceId)) {
21858
22059
  throw Object.assign(
21859
22060
  new Error(
@@ -21869,6 +22070,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21869
22070
  );
21870
22071
  }
21871
22072
  for (const step of steps) {
22073
+ graph.assertActive();
21872
22074
  if (step.kind === "account-index-schema") {
21873
22075
  const ensured = await this.account.index.ensure();
21874
22076
  if (!ensured.ok) {
@@ -22644,6 +22846,9 @@ var _TinyCloudNode = class _TinyCloudNode {
22644
22846
  * @param sessionData - The stored delegation data from the browser flow
22645
22847
  */
22646
22848
  async restoreSession(sessionData) {
22849
+ const epoch = this.sessionEpoch;
22850
+ const attempt = ++this.latestSessionAttempt;
22851
+ await this.signOutInFlight;
22647
22852
  await this.wasmBindings.ensureInitialized?.();
22648
22853
  let restoredJwk;
22649
22854
  try {
@@ -22833,6 +23038,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22833
23038
  });
22834
23039
  this.pendingAccountRegistrySync?.controller.abort();
22835
23040
  await this.accountRegistryTail;
23041
+ this.assertSessionEpoch(epoch, "Session restore");
22836
23042
  const oldGraph = this._serviceGraph;
22837
23043
  const oldCore = this.tc;
22838
23044
  this.sessionManager = stagedManager;
@@ -22878,6 +23084,12 @@ var _TinyCloudNode = class _TinyCloudNode {
22878
23084
  this.replicationDelegateSession = compactReplicationSession;
22879
23085
  this.replicationScopeError = replicationScopeError;
22880
23086
  if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
23087
+ if (epoch !== this.sessionEpoch) {
23088
+ oldGraph.retire();
23089
+ oldCore?.retireServices?.();
23090
+ if (attempt === this.latestSessionAttempt) await this.endSession();
23091
+ this.assertSessionEpoch(epoch, "Session restore");
23092
+ }
22881
23093
  oldGraph.retire();
22882
23094
  if (replicationScopeError) {
22883
23095
  stagedGraph.serviceContext.emit("replication.state", {
@@ -22888,6 +23100,10 @@ var _TinyCloudNode = class _TinyCloudNode {
22888
23100
  }
22889
23101
  await this.bindReplicationRuntime(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
22890
23102
  oldCore?.retireServices?.();
23103
+ if (epoch !== this.sessionEpoch) {
23104
+ if (attempt === this.latestSessionAttempt) await this.endSession();
23105
+ this.assertSessionEpoch(epoch, "Session restore");
23106
+ }
22891
23107
  }
22892
23108
  stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
22893
23109
  if (!session?.siwe) return { cache: void 0, grants: [] };
@@ -24746,6 +24962,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24746
24962
  * matching service calls and downstream `delegateTo()` calls can use it.
24747
24963
  */
24748
24964
  async useRuntimeDelegation(delegation) {
24965
+ const graph = this._serviceGraph;
24749
24966
  const session = this.currentTinyCloudSession();
24750
24967
  if (!session) {
24751
24968
  throw new import_sdk_core9.SessionExpiredError(/* @__PURE__ */ new Date(0));
@@ -24764,9 +24981,11 @@ var _TinyCloudNode = class _TinyCloudNode {
24764
24981
  try {
24765
24982
  activateResult = await (0, import_sdk_core9.activateSessionWithHost)(
24766
24983
  targetHost,
24767
- delegation.delegationHeader
24984
+ delegation.delegationHeader,
24985
+ graph.signal
24768
24986
  );
24769
24987
  } catch (cause) {
24988
+ graph.assertActive();
24770
24989
  throw Object.assign(
24771
24990
  new Error("The TinyCloud host could not be reached to activate the delegation."),
24772
24991
  { code: "ACTIVATION_TRANSPORT_FAILED", cause }
@@ -24786,6 +25005,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24786
25005
  { cause: activateResult }
24787
25006
  );
24788
25007
  }
25008
+ graph.assertActive();
24789
25009
  this.runtimePermissionGrants = this.runtimePermissionGrants.filter(
24790
25010
  (grant) => grant.delegation.cid !== delegation.cid
24791
25011
  );
@@ -24802,6 +25022,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24802
25022
  if (!Array.isArray(permissions) || permissions.length === 0) {
24803
25023
  throw new Error("grantRuntimePermissions requires a non-empty permissions array");
24804
25024
  }
25025
+ const graph = this._serviceGraph;
24805
25026
  const session = this.currentTinyCloudSession();
24806
25027
  if (!session) {
24807
25028
  throw new import_sdk_core9.SessionExpiredError(/* @__PURE__ */ new Date(0));
@@ -24882,14 +25103,19 @@ var _TinyCloudNode = class _TinyCloudNode {
24882
25103
  }
24883
25104
  })();
24884
25105
  const signature2 = await this.signer.signMessage(prepared.siwe);
25106
+ graph.assertActive();
24885
25107
  const delegatedSession = this.wasmBindings.completeSessionSetup({
24886
25108
  ...prepared,
24887
25109
  signature: signature2
24888
25110
  });
24889
25111
  const activateResult = await (0, import_sdk_core9.activateSessionWithHost)(
24890
25112
  this.config.host,
24891
- delegatedSession.delegationHeader
24892
- );
25113
+ delegatedSession.delegationHeader,
25114
+ graph.signal
25115
+ ).catch((error) => {
25116
+ graph.assertActive();
25117
+ throw error;
25118
+ });
24893
25119
  if (!activateResult.success) {
24894
25120
  if (rawForDelegation.some((entry) => this.isRawRevocationPermissionEntry(entry)) && activateResult.status === 401 && /Cannot find parent delegation/i.test(activateResult.error ?? "")) {
24895
25121
  throw Object.assign(
@@ -24910,6 +25136,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24910
25136
  expiresAt,
24911
25137
  { siwe: prepared.siwe, signature: signature2 }
24912
25138
  );
25139
+ graph.assertActive();
24913
25140
  this.runtimePermissionGrants.push({
24914
25141
  session: {
24915
25142
  delegationHeader: delegatedSession.delegationHeader,
@@ -26373,6 +26600,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26373
26600
  * @returns A DelegatedAccess instance for performing operations
26374
26601
  */
26375
26602
  async useDelegation(delegation) {
26603
+ const graph = this._serviceGraph;
26376
26604
  const delegationHeader = delegation.delegationHeader;
26377
26605
  const targetHost = delegation.host ?? this.config.host;
26378
26606
  if (this.isSessionOnly) {
@@ -26414,7 +26642,8 @@ var _TinyCloudNode = class _TinyCloudNode {
26414
26642
  targetHost,
26415
26643
  this.wasmBindings.invoke,
26416
26644
  this.wasmBindings.invokeAny,
26417
- this.config.telemetry
26645
+ this.config.telemetry,
26646
+ graph
26418
26647
  );
26419
26648
  }
26420
26649
  const mySession = this.auth?.tinyCloudSession;
@@ -26438,14 +26667,19 @@ var _TinyCloudNode = class _TinyCloudNode {
26438
26667
  ...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
26439
26668
  });
26440
26669
  const signature2 = await this.signer.signMessage(prepared.siwe);
26670
+ graph.assertActive();
26441
26671
  const invokerSession = this.wasmBindings.completeSessionSetup({
26442
26672
  ...prepared,
26443
26673
  signature: signature2
26444
26674
  });
26445
26675
  const activateResult = await (0, import_sdk_core9.activateSessionWithHost)(
26446
26676
  targetHost,
26447
- invokerSession.delegationHeader
26448
- );
26677
+ invokerSession.delegationHeader,
26678
+ graph.signal
26679
+ ).catch((error) => {
26680
+ graph.assertActive();
26681
+ throw error;
26682
+ });
26449
26683
  if (!activateResult.success) {
26450
26684
  throw Object.assign(
26451
26685
  new Error(`Failed to activate delegated session: ${describeHostFailure2(activateResult)}`),
@@ -26464,6 +26698,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26464
26698
  siwe: prepared.siwe,
26465
26699
  signature: signature2
26466
26700
  };
26701
+ graph.assertActive();
26467
26702
  this.trackReceivedDelegation(delegation, jwk);
26468
26703
  this.installRuntimeGrantFromServiceSession(
26469
26704
  delegation,
@@ -26482,7 +26717,8 @@ var _TinyCloudNode = class _TinyCloudNode {
26482
26717
  targetHost,
26483
26718
  this.wasmBindings.invoke,
26484
26719
  this.wasmBindings.invokeAny,
26485
- this.config.telemetry
26720
+ this.config.telemetry,
26721
+ graph
26486
26722
  );
26487
26723
  }
26488
26724
  /**