@cortexkit/anthropic-auth-core 2.1.0 → 2.1.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.js CHANGED
@@ -13922,18 +13922,43 @@ function nativeLocalCredentialValidationMatches(validation, binding, material) {
13922
13922
  }
13923
13923
 
13924
13924
  // src/native-lock-observer.ts
13925
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
13926
+ var detachedLocks = new AsyncLocalStorage2;
13927
+ function withDetachedNativeLocks(work) {
13928
+ return detachedLocks.run(true, work);
13929
+ }
13925
13930
  function nativeLockObserver(observer) {
13926
13931
  if (!observer)
13927
13932
  return;
13928
13933
  return (event) => {
13929
13934
  try {
13930
- const result = observer(event);
13935
+ const result = observer(detachedLocks.getStore() ? { ...event, detached: true } : event);
13931
13936
  if (result !== null && (typeof result === "object" || typeof result === "function") && "then" in result && typeof result.then === "function") {
13932
13937
  Promise.resolve(result).catch(() => {});
13933
13938
  }
13934
13939
  } catch {}
13935
13940
  };
13936
13941
  }
13942
+ async function withNativeLock(path2, options, run, acquire = withLock) {
13943
+ const observer = nativeLockObserver(options.onLockEvent);
13944
+ if (!observer)
13945
+ return acquire(path2, options, run);
13946
+ observer({ type: "acquiring", name: options.name, path: path2 });
13947
+ let acquired = false;
13948
+ try {
13949
+ return await acquire(path2, options, async (lease) => {
13950
+ acquired = true;
13951
+ observer({ type: "acquired", name: options.name, path: path2 });
13952
+ return run(lease);
13953
+ });
13954
+ } finally {
13955
+ if (acquired)
13956
+ observer({ type: "released", name: options.name, path: path2 });
13957
+ }
13958
+ }
13959
+ function observeNativeLocks(acquire, observer) {
13960
+ return (path2, options, run) => withNativeLock(path2, { ...options, onLockEvent: options.onLockEvent ?? observer }, run, acquire());
13961
+ }
13937
13962
 
13938
13963
  // src/native-vault-seed.ts
13939
13964
  import { lstat as lstat4, readFile as readFile8 } from "node:fs/promises";
@@ -14932,18 +14957,27 @@ async function renameWithWindowsSharingRetry(from, to, options = {}, beforeRetry
14932
14957
  }
14933
14958
 
14934
14959
  // src/native-runtime.ts
14960
+ var withLock2 = observeNativeLocks(() => withLock);
14961
+
14935
14962
  class NativeRuntimeError extends Error {
14936
14963
  code;
14964
+ status;
14965
+ retryable;
14937
14966
  constructor(code) {
14938
14967
  super({
14939
14968
  "invalid-runtime": "Anthropic runtime state is invalid",
14940
14969
  "unsafe-runtime": "Anthropic runtime file is not owner-only regular storage",
14941
14970
  "runtime-io": "Anthropic runtime storage is unavailable",
14942
14971
  "runtime-conflict": "Anthropic runtime observation is older than persisted state",
14943
- "publication-refused": "Anthropic runtime publication was refused"
14972
+ "publication-refused": "Anthropic runtime publication was refused",
14973
+ "relay-inconsistent": "request refused: relay configuration is inconsistent; no request was sent"
14944
14974
  }[code]);
14945
14975
  this.code = code;
14946
14976
  this.name = "NativeRuntimeError";
14977
+ if (code === "relay-inconsistent") {
14978
+ this.status = 400;
14979
+ this.retryable = false;
14980
+ }
14947
14981
  }
14948
14982
  }
14949
14983
  function record6(value) {
@@ -15090,7 +15124,7 @@ function entry(value, id2, storageId) {
15090
15124
  return true;
15091
15125
  }
15092
15126
  function decodeNativeRuntime(value, storageId) {
15093
- if (!record6(value) || !keys3(value, ["version", "storageId", "accounts"], ["relay"]) || value.version !== 1 || !digest(storageId) || value.storageId !== storageId || !record6(value.accounts) || !optional3(value, "relay", (entry2) => record6(entry2) && keys3(entry2, ["token"]) && typeof entry2.token === "string"))
15127
+ if (!record6(value) || !keys3(value, ["version", "storageId", "accounts"], ["relay", "relayGeneration"]) || value.version !== 1 || !digest(storageId) || value.storageId !== storageId || !record6(value.accounts) || !optional3(value, "relayGeneration", text3) || !optional3(value, "relay", (entry2) => record6(entry2) && keys3(entry2, ["token"]) && typeof entry2.token === "string"))
15094
15128
  throw new NativeRuntimeError("invalid-runtime");
15095
15129
  const accounts = Object.create(null);
15096
15130
  for (const [id2, item] of Object.entries(value.accounts)) {
@@ -15102,6 +15136,7 @@ function decodeNativeRuntime(value, storageId) {
15102
15136
  version: 1,
15103
15137
  storageId,
15104
15138
  accounts,
15139
+ ...typeof value.relayGeneration === "string" ? { relayGeneration: value.relayGeneration } : {},
15105
15140
  ...record6(value.relay) && typeof value.relay.token === "string" ? { relay: { token: value.relay.token } } : {}
15106
15141
  };
15107
15142
  }
@@ -15218,7 +15253,8 @@ async function updateNativeRuntime(path2, storageId, change, hooks = {}) {
15218
15253
  async function updateNativeRuntimeOptional(path2, storageId, change, hooks) {
15219
15254
  return writeNativeRuntimeOutcome(path2, storageId, change, {
15220
15255
  beforeRename: hooks.beforeRename,
15221
- publication: hooks.publication
15256
+ publication: hooks.publication,
15257
+ onLockEvent: hooks.onLockEvent
15222
15258
  });
15223
15259
  }
15224
15260
  async function writeNativeRuntime(path2, storageId, change, hooks, firstIdentity) {
@@ -15229,7 +15265,8 @@ async function writeNativeRuntime(path2, storageId, change, hooks, firstIdentity
15229
15265
  },
15230
15266
  beforePublish: hooks.beforePublish,
15231
15267
  afterRename: hooks.afterRename,
15232
- publication: hooks.publication
15268
+ publication: hooks.publication,
15269
+ onLockEvent: hooks.onLockEvent
15233
15270
  }, firstIdentity);
15234
15271
  if (outcome.status !== "published")
15235
15272
  throw new NativeRuntimeError("publication-refused");
@@ -15240,7 +15277,13 @@ async function writeNativeRuntimeOutcome(path2, storageId, change, hooks, firstI
15240
15277
  const publication2 = capturePublicationOptions(hooks.publication);
15241
15278
  try {
15242
15279
  await mkdir9(dirname9(path2), { recursive: true, mode: 448 });
15243
- return await withLock(path2, { name: "native-runtime", ttlMs: 1e4, timeoutMs: 15000, renew: true }, async (lock) => {
15280
+ return await withLock2(path2, {
15281
+ name: "native-runtime",
15282
+ ttlMs: 1e4,
15283
+ timeoutMs: 15000,
15284
+ renew: true,
15285
+ onLockEvent: hooks.onLockEvent
15286
+ }, async (lock) => {
15244
15287
  const read = await readNativeRuntime(path2, storageId);
15245
15288
  const current = read.status === "ready" ? read.state : { version: 1, storageId, accounts: {} };
15246
15289
  const next = decodeNativeRuntime(await change(structuredClone(current)), storageId);
@@ -15638,7 +15681,11 @@ async function publishNativeLocalRefreshFailure(paths, observation, policy, hook
15638
15681
  }
15639
15682
  async function publishLocalEntry(paths, subject, matches, replacement, firstIdentity, superseded, hooks = {}) {
15640
15683
  try {
15641
- const store = createNativePoolStore({ paths, quota: nativeQuotaCodec });
15684
+ const store = createNativePoolStore({
15685
+ paths,
15686
+ quota: nativeQuotaCodec,
15687
+ onLockEvent: hooks.onLockEvent
15688
+ });
15642
15689
  const [config, state] = nativePoolStoreLocks(paths);
15643
15690
  const checkPool = async () => {
15644
15691
  const read = await store.read();
@@ -15654,7 +15701,15 @@ async function publishLocalEntry(paths, subject, matches, replacement, firstIden
15654
15701
  throw new NativeRuntimeError("publication-refused");
15655
15702
  }
15656
15703
  };
15657
- return await withLock(config.path, { ...POOL_LOCK_DEFAULTS, name: config.name }, async (configLease) => withLock(state.path, { ...POOL_LOCK_DEFAULTS, name: state.name }, async (stateLease) => {
15704
+ return await withLock2(config.path, {
15705
+ ...POOL_LOCK_DEFAULTS,
15706
+ name: config.name,
15707
+ onLockEvent: hooks.onLockEvent
15708
+ }, async (configLease) => withLock2(state.path, {
15709
+ ...POOL_LOCK_DEFAULTS,
15710
+ name: state.name,
15711
+ onLockEvent: hooks.onLockEvent
15712
+ }, async (stateLease) => {
15658
15713
  await checkPool();
15659
15714
  return writeNativeRuntime(paths.runtime, paths.storageId, (current) => {
15660
15715
  const id2 = subject.binding.rowId;
@@ -15666,7 +15721,8 @@ async function publishLocalEntry(paths, subject, matches, replacement, firstIden
15666
15721
  await configLease.assertOwned();
15667
15722
  await stateLease.assertOwned();
15668
15723
  },
15669
- beforePublish: hooks.beforePublish
15724
+ beforePublish: hooks.beforePublish,
15725
+ onLockEvent: hooks.onLockEvent
15670
15726
  }, firstIdentity);
15671
15727
  }));
15672
15728
  } catch (error) {
@@ -20196,7 +20252,8 @@ var optionKeys = [
20196
20252
  "failurePolicy",
20197
20253
  "now",
20198
20254
  "refreshToken",
20199
- "resolveIdentity"
20255
+ "resolveIdentity",
20256
+ "onLockEvent"
20200
20257
  ];
20201
20258
  var pathKeys = [
20202
20259
  "legacyConfig",
@@ -20283,6 +20340,7 @@ function createNativeLocalCredentialService(options) {
20283
20340
  const publicationGuard = requiredFunction(descriptors, "publicationGuard");
20284
20341
  const externalPolicy = requiredFunction(descriptors, "externalPolicy");
20285
20342
  const failurePolicy = requiredFunction(descriptors, "failurePolicy");
20343
+ const onLockEvent = optionalFunction(descriptors, "onLockEvent");
20286
20344
  const now = optionalFunction(descriptors, "now");
20287
20345
  const refreshToken = optionalFunction(descriptors, "refreshToken");
20288
20346
  const resolveIdentity = optionalFunction(descriptors, "resolveIdentity");
@@ -20290,6 +20348,7 @@ function createNativeLocalCredentialService(options) {
20290
20348
  if (await publicationGuard(event) !== true)
20291
20349
  return;
20292
20350
  const hooks = {
20351
+ onLockEvent,
20293
20352
  beforePublish: async () => {
20294
20353
  if (await publicationGuard(event) !== true)
20295
20354
  throw new NativeRuntimeError("publication-refused");
@@ -20324,6 +20383,7 @@ function createNativeLocalCredentialService(options) {
20324
20383
  const coordinator = createNativeRefreshCoordinator({
20325
20384
  paths,
20326
20385
  quota: nativeQuotaCodec,
20386
+ onLockEvent,
20327
20387
  ...now ? { now } : {},
20328
20388
  ...refreshToken ? { refreshToken } : {},
20329
20389
  ...resolveIdentity ? { resolveIdentity } : {},
@@ -20636,7 +20696,8 @@ function isNativeTransientAccountError(value) {
20636
20696
  return typeof value === "object" && value !== null && transientAccountErrors.has(value);
20637
20697
  }
20638
20698
  var NATIVE_POOL_WRITE_JOIN_BUDGET_MS = 2000;
20639
- async function joinNativePoolWrite(paths, signals, check) {
20699
+ async function joinNativePoolWrite(paths, signals, check, onLockEvent) {
20700
+ const observer = nativeLockObserver(onLockEvent);
20640
20701
  const started = performance.now();
20641
20702
  const remaining = () => NATIVE_POOL_WRITE_JOIN_BUDGET_MS - (performance.now() - started);
20642
20703
  const held = [];
@@ -20650,14 +20711,17 @@ async function joinNativePoolWrite(paths, signals, check) {
20650
20711
  outcome = "deadline";
20651
20712
  break;
20652
20713
  }
20714
+ observer?.({ type: "acquiring", path: lock.path, name: lock.name });
20653
20715
  const lease = await acquireRefreshFileLock({
20654
20716
  path: lock.path,
20655
20717
  name: lock.name,
20656
20718
  ttlMs: POOL_LOCK_DEFAULTS.ttlMs,
20657
20719
  renew: false
20658
20720
  });
20659
- if (lease)
20721
+ if (lease) {
20660
20722
  held.push(lease);
20723
+ observer?.({ type: "acquired", path: lock.path, name: lock.name });
20724
+ }
20661
20725
  if (remaining() <= 0) {
20662
20726
  outcome = "deadline";
20663
20727
  break;
@@ -21261,7 +21325,17 @@ function createNativeVaultRuntime(options) {
21261
21325
  const connectionFile = nativeRuntimeConnectionFile(options.host, env);
21262
21326
  const identity2 = options.identity ? { ...options.identity } : undefined;
21263
21327
  const shutdown = new AbortController;
21264
- const store = createNativePoolStore({ paths, quota: nativeQuotaCodec });
21328
+ const store = createNativePoolStore({
21329
+ paths,
21330
+ quota: nativeQuotaCodec,
21331
+ onLockEvent: options.onLockEvent
21332
+ });
21333
+ const observeLock = nativeLockObserver(options.onLockEvent);
21334
+ const observeRosterLock = () => observeLock?.({
21335
+ type: "acquiring",
21336
+ name: "claustrum-roster-write",
21337
+ path: paths.roster
21338
+ });
21265
21339
  let rosterWatch;
21266
21340
  let rosterWatching = false;
21267
21341
  let rosterView;
@@ -21420,7 +21494,7 @@ function createNativeVaultRuntime(options) {
21420
21494
  function poll() {
21421
21495
  if (polling)
21422
21496
  return polling;
21423
- polling = (async () => {
21497
+ polling = withDetachedNativeLocks(async () => {
21424
21498
  const allowed = await Promise.all([...watchers].map(async (watch) => !watch.shouldRefresh || await watch.shouldRefresh()));
21425
21499
  if (shutdown.signal.aborted || !allowed.some(Boolean))
21426
21500
  return;
@@ -21440,7 +21514,7 @@ function createNativeVaultRuntime(options) {
21440
21514
  return watch.onRefresh?.();
21441
21515
  }).catch(() => {});
21442
21516
  }
21443
- })().finally(() => {
21517
+ }).finally(() => {
21444
21518
  polling = undefined;
21445
21519
  });
21446
21520
  return polling;
@@ -21474,6 +21548,7 @@ function createNativeVaultRuntime(options) {
21474
21548
  await requireActive(signal);
21475
21549
  if (absentOnly && !await nativeVaultFileAbsent(paths.config))
21476
21550
  return;
21551
+ observeRosterLock();
21477
21552
  await mutateVaultRoster(paths.roster, async (roster, lease) => {
21478
21553
  if (absentOnly && !await nativeVaultFileAbsent(paths.config))
21479
21554
  return { result: undefined };
@@ -21487,6 +21562,7 @@ function createNativeVaultRuntime(options) {
21487
21562
  const writer = createNativePoolStore({
21488
21563
  paths,
21489
21564
  quota: nativeQuotaCodec,
21565
+ onLockEvent: options.onLockEvent,
21490
21566
  onStep: async (point) => {
21491
21567
  if (point === "before-config-write" || point === "before-state-write") {
21492
21568
  await requireActive(signal);
@@ -21583,6 +21659,12 @@ function createNativeVaultRuntime(options) {
21583
21659
  if (!await active())
21584
21660
  return;
21585
21661
  check(signal);
21662
+ if (!refreshing)
21663
+ observeLock?.({
21664
+ type: "acquiring",
21665
+ name: "claustrum-roster",
21666
+ path: paths.roster
21667
+ });
21586
21668
  refreshing ??= refreshVaultRoster({
21587
21669
  path: paths.roster,
21588
21670
  custody: options.discover ? { discover: options.discover } : custody,
@@ -21873,6 +21955,7 @@ function createNativeVaultRuntime(options) {
21873
21955
  ]);
21874
21956
  if ((versions.get(key) ?? 0) > receipt.recordVersion)
21875
21957
  return false;
21958
+ observeRosterLock();
21876
21959
  return mutateVaultRoster(paths.roster, async (roster, lease) => {
21877
21960
  const row = await rowFor(roster, route.logicalId);
21878
21961
  if (!row?.accountIdentity || row.routeId !== route.routeId || row.accountIdentity !== receipt.assertedAccountIdentity || row.credentialId !== receipt.credentialId && !row.aliases?.includes(receipt.credentialId))
@@ -21900,6 +21983,7 @@ function createNativeVaultRuntime(options) {
21900
21983
  kept = true;
21901
21984
  return current;
21902
21985
  }, {
21986
+ onLockEvent: options.onLockEvent,
21903
21987
  beforeRename: async () => {
21904
21988
  await requireActive();
21905
21989
  const pinned = await rowFor(roster, route.logicalId);
@@ -22032,6 +22116,7 @@ function nativeVaultRuntimeKey(options) {
22032
22116
  options.discover,
22033
22117
  options.readToken,
22034
22118
  options.now,
22119
+ options.onLockEvent,
22035
22120
  options.setTimeout,
22036
22121
  options.clearTimeout,
22037
22122
  options.onRosterNoticeHandled
@@ -22354,7 +22439,9 @@ class StickySessionRouter {
22354
22439
  loaded = false;
22355
22440
  state;
22356
22441
  stateMtimeNs;
22357
- touchChain = Promise.resolve();
22442
+ publicationChain = Promise.resolve();
22443
+ seedPublication;
22444
+ changes = new Map;
22358
22445
  seedFinalized = false;
22359
22446
  constructor(options) {
22360
22447
  this.options = options;
@@ -22400,17 +22487,22 @@ class StickySessionRouter {
22400
22487
  if (before !== after)
22401
22488
  continue;
22402
22489
  this.state = state;
22490
+ this.overlayChanges();
22403
22491
  this.stateMtimeNs = after;
22404
22492
  this.loaded = true;
22405
22493
  return;
22406
22494
  }
22407
22495
  this.state = await this.readState();
22496
+ this.overlayChanges();
22408
22497
  this.stateMtimeNs = await this.readStateMtimeNs();
22409
22498
  this.loaded = true;
22410
22499
  }
22411
- async acquireLock(wait = true) {
22500
+ async acquireLock(wait = true, detached = false) {
22501
+ const observer = nativeLockObserver(this.options.onLockEvent);
22502
+ const event = { name: "write", path: this.options.path, detached };
22503
+ observer?.({ ...event, type: "acquiring" });
22412
22504
  await mkdir10(dirname12(this.options.path), { recursive: true, mode: 448 });
22413
- const deadline = this.now() + STICKY_ROUTING_LOCK_WAIT_MS;
22505
+ const deadline = performance.now() + STICKY_ROUTING_LOCK_WAIT_MS;
22414
22506
  while (true) {
22415
22507
  const lock = await acquireRefreshFileLock2({
22416
22508
  name: "write",
@@ -22418,32 +22510,42 @@ class StickySessionRouter {
22418
22510
  ttlMs: STICKY_ROUTING_LOCK_TTL_MS,
22419
22511
  renew: true
22420
22512
  });
22421
- if (lock)
22422
- return lock;
22423
- if (!wait || this.now() >= deadline) {
22513
+ if (lock) {
22514
+ observer?.({ ...event, type: "acquired" });
22515
+ return {
22516
+ assertOwned: () => lock.assertOwned(),
22517
+ release: async () => {
22518
+ try {
22519
+ await lock.release();
22520
+ } finally {
22521
+ observer?.({ ...event, type: "released" });
22522
+ }
22523
+ }
22524
+ };
22525
+ }
22526
+ observer?.({ ...event, type: "contended" });
22527
+ if (!wait || performance.now() >= deadline) {
22424
22528
  throw new Error("Timed out acquiring sticky routing state lock");
22425
22529
  }
22426
22530
  await new Promise((resolve9) => setTimeout(resolve9, STICKY_ROUTING_LOCK_POLL_MS));
22427
22531
  }
22428
22532
  }
22429
- async recoverLocalPins() {
22533
+ recoverLocalPins() {
22430
22534
  const source2 = this.options.localRecoveryPath;
22431
22535
  if (!source2 || source2 === this.options.path)
22432
22536
  return;
22433
22537
  const key = JSON.stringify([this.options.path, source2]);
22434
22538
  let recovery = localRecoveries.get(key);
22435
22539
  if (!recovery) {
22436
- recovery = this.fillLocalPins(source2).catch(() => {
22437
- logger.warn("sticky-routing", "local pin recovery unavailable", {
22438
- reason: "recovery-failed"
22439
- });
22440
- });
22540
+ recovery = this.enqueuePublication("recovery", () => this.fillLocalPins(source2));
22441
22541
  localRecoveries.set(key, recovery);
22542
+ } else {
22543
+ const shared2 = recovery;
22544
+ this.publicationChain = this.publicationChain.then(() => shared2);
22442
22545
  }
22443
- await recovery;
22444
22546
  }
22445
22547
  async fillLocalPins(source2) {
22446
- const lock = await this.acquireLock(false);
22548
+ const lock = await this.acquireLock(true, true);
22447
22549
  try {
22448
22550
  let legacy;
22449
22551
  try {
@@ -22498,9 +22600,7 @@ class StickySessionRouter {
22498
22600
  } finally {
22499
22601
  await rm7(tempPath, { force: true }).catch(() => {});
22500
22602
  }
22501
- this.state = state;
22502
- this.stateMtimeNs = await this.readStateMtimeNs();
22503
- this.loaded = true;
22603
+ this.loaded = false;
22504
22604
  }
22505
22605
  async writeResolutionState(state, assertOwned, operation) {
22506
22606
  let ownershipLost = false;
@@ -22512,63 +22612,59 @@ class StickySessionRouter {
22512
22612
  ownershipLost = true;
22513
22613
  throw error;
22514
22614
  }
22515
- });
22516
- } catch (error) {
22517
- if (!ownershipLost)
22518
- throw error;
22615
+ }, false);
22616
+ return true;
22617
+ } catch {
22519
22618
  logger.warn("sticky-routing", "assignment write refused", {
22520
22619
  operation,
22521
- reason: "ownership-lost"
22620
+ reason: ownershipLost ? "ownership-lost" : "storage-unavailable"
22522
22621
  });
22622
+ return false;
22523
22623
  }
22524
22624
  }
22525
22625
  async prepareLegacySeed(knownRoster) {
22526
22626
  const seed = this.options.vaultSeed;
22527
- if (!seed || this.seedFinalized)
22627
+ if (!seed || this.seedFinalized || this.seedPublication)
22528
22628
  return;
22529
- try {
22530
- await this.refreshStateIfChanged();
22531
- } catch {
22532
- this.seedFinalized = true;
22629
+ if (knownRoster !== undefined && !knownRoster?.complete)
22533
22630
  return;
22534
- }
22535
- if (this.state.vaultLegacySeed !== "pending") {
22536
- this.seedFinalized = true;
22537
- return;
22538
- }
22539
- const roster = knownRoster === undefined ? await readVaultRoster(seed.roster) : knownRoster;
22540
- if (!roster?.complete)
22541
- return;
22542
- const lock = await this.acquireLock();
22543
- try {
22544
- const state = await this.readState();
22545
- if (state.vaultLegacySeed !== "pending") {
22546
- this.seedFinalized = true;
22631
+ this.seedPublication = this.enqueuePublication("seed", async () => {
22632
+ const roster = knownRoster === undefined ? await readVaultRoster(seed.roster) : knownRoster;
22633
+ if (!roster?.complete)
22547
22634
  return;
22548
- }
22549
- const source2 = await readNativeVaultLegacyJson(seed.legacyConfig);
22550
- const routes = nativeVaultLegacyRoutes(source2, roster);
22551
- const legacy = await readNativeVaultLegacyJson(getStickyRoutingStatePath(seed.legacyConfig));
22552
- if (isRecord12(legacy) && legacy.version === 1 && isRecord12(legacy.assignments)) {
22553
- for (const [key, raw] of Object.entries(legacy.assignments)) {
22554
- if (!/^[0-9a-f]{64}$/.test(key) || Object.hasOwn(state.assignments, key))
22555
- continue;
22556
- const assignment = normalizeAssignment(raw);
22557
- const target2 = assignment && routes.get(assignment.accountId);
22558
- if (assignment && target2)
22559
- state.assignments[key] = {
22560
- ...assignment,
22561
- accountId: target2.routeId,
22562
- accountIdentity: target2.accountIdentity
22563
- };
22635
+ const lock = await this.acquireLock(true, true);
22636
+ try {
22637
+ const state = await this.readState();
22638
+ if (state.vaultLegacySeed !== "pending") {
22639
+ this.seedFinalized = true;
22640
+ return;
22564
22641
  }
22642
+ const source2 = await readNativeVaultLegacyJson(seed.legacyConfig);
22643
+ const routes = nativeVaultLegacyRoutes(source2, roster);
22644
+ const legacy = await readNativeVaultLegacyJson(getStickyRoutingStatePath(seed.legacyConfig));
22645
+ if (isRecord12(legacy) && legacy.version === 1 && isRecord12(legacy.assignments)) {
22646
+ for (const [key, raw] of Object.entries(legacy.assignments)) {
22647
+ if (!/^[0-9a-f]{64}$/.test(key) || Object.hasOwn(state.assignments, key))
22648
+ continue;
22649
+ const assignment = normalizeAssignment(raw);
22650
+ const target2 = assignment && routes.get(assignment.accountId);
22651
+ if (assignment && target2)
22652
+ state.assignments[key] = {
22653
+ ...assignment,
22654
+ accountId: target2.routeId,
22655
+ accountIdentity: target2.accountIdentity
22656
+ };
22657
+ }
22658
+ }
22659
+ state.vaultLegacySeed = "complete";
22660
+ await this.writeState(state, () => lock.assertOwned(), false);
22661
+ this.seedFinalized = true;
22662
+ } finally {
22663
+ await lock.release();
22565
22664
  }
22566
- state.vaultLegacySeed = "complete";
22567
- await this.writeState(state, () => lock.assertOwned(), false);
22568
- this.seedFinalized = true;
22569
- } finally {
22570
- await lock.release();
22571
- }
22665
+ }).finally(() => {
22666
+ this.seedPublication = undefined;
22667
+ });
22572
22668
  }
22573
22669
  logicalAccountId(assignment) {
22574
22670
  return assignment.accountId === this.options.vaultSeed?.mainRouteId ? STICKY_ROUTING_MAIN_ACCOUNT_ID : assignment.accountId;
@@ -22577,19 +22673,86 @@ class StickySessionRouter {
22577
22673
  this.options.vaultSeed = seed;
22578
22674
  }
22579
22675
  scheduleTouch(key) {
22580
- this.touchChain = this.touchChain.catch(() => {}).then(async () => {
22581
- const lock = await this.acquireLock();
22676
+ const assignment = this.state.assignments[key];
22677
+ if (assignment)
22678
+ this.scheduleChange(key, assignment, "touch");
22679
+ }
22680
+ overlayChanges() {
22681
+ for (const [key, change] of this.changes) {
22682
+ if (change.assignment)
22683
+ this.state.assignments[key] = { ...change.assignment };
22684
+ else
22685
+ delete this.state.assignments[key];
22686
+ }
22687
+ }
22688
+ enqueuePublication(operation, work) {
22689
+ const pending = this.publicationChain.then(work).catch(() => {
22690
+ if (operation === "recovery") {
22691
+ logger.warn("sticky-routing", "local pin recovery unavailable", {
22692
+ reason: "recovery-failed"
22693
+ });
22694
+ return;
22695
+ }
22696
+ logger.warn("sticky-routing", "assignment write refused", {
22697
+ operation,
22698
+ reason: "storage-unavailable"
22699
+ });
22700
+ });
22701
+ this.publicationChain = pending;
22702
+ return pending;
22703
+ }
22704
+ scheduleChange(key, assignment, operation) {
22705
+ const change = {
22706
+ assignment: assignment ? { ...assignment } : undefined,
22707
+ operation,
22708
+ queued: false
22709
+ };
22710
+ this.changes.set(key, change);
22711
+ this.overlayChanges();
22712
+ this.retryChange(key);
22713
+ }
22714
+ retryChange(key) {
22715
+ const change = this.changes.get(key);
22716
+ if (!change || change.queued)
22717
+ return;
22718
+ change.queued = true;
22719
+ this.enqueuePublication(change.operation, async () => {
22720
+ if (this.changes.get(key) !== change)
22721
+ return;
22722
+ const lock = await this.acquireLock(true, true);
22582
22723
  try {
22583
22724
  const state = await this.readState();
22584
- const assignment = state.assignments[key];
22585
- if (!assignment)
22725
+ if (this.changes.get(key) !== change)
22586
22726
  return;
22587
- assignment.lastSeenAt = this.now();
22588
- await this.writeState(state, () => lock.assertOwned());
22727
+ if (change.assignment) {
22728
+ if (change.operation === "touch") {
22729
+ const current = state.assignments[key];
22730
+ if (current?.accountId !== change.assignment.accountId || current.accountIdentity !== change.assignment.accountIdentity || current.affinityModelId !== change.assignment.affinityModelId || current.family !== change.assignment.family) {
22731
+ if (this.changes.get(key) === change)
22732
+ this.changes.delete(key);
22733
+ return;
22734
+ }
22735
+ current.lastSeenAt = Math.max(current.lastSeenAt, change.assignment.lastSeenAt);
22736
+ } else
22737
+ state.assignments[key] = { ...change.assignment };
22738
+ } else
22739
+ delete state.assignments[key];
22740
+ const saved = await this.writeResolutionState(state, () => lock.assertOwned(), change.operation);
22741
+ if (saved && this.changes.get(key) === change)
22742
+ this.changes.delete(key);
22589
22743
  } finally {
22590
- await lock.release();
22744
+ await lock.release().catch(() => {});
22591
22745
  }
22592
- }).catch(() => {});
22746
+ }).finally(() => {
22747
+ change.queued = false;
22748
+ });
22749
+ }
22750
+ async drain() {
22751
+ let pending;
22752
+ do {
22753
+ pending = this.publicationChain;
22754
+ await pending;
22755
+ } while (pending !== this.publicationChain);
22593
22756
  }
22594
22757
  selectCandidate(input) {
22595
22758
  let candidates = [...input.candidates];
@@ -22629,21 +22792,21 @@ class StickySessionRouter {
22629
22792
  async resolve(input) {
22630
22793
  if (!input.sessionId)
22631
22794
  return null;
22632
- await this.recoverLocalPins();
22795
+ this.recoverLocalPins();
22633
22796
  await this.prepareLegacySeed();
22634
- await this.refreshStateIfChanged();
22797
+ await this.refreshStateIfChanged().catch(() => {});
22635
22798
  const key = sessionKey(input.sessionId);
22636
22799
  const affinityModelId = input.affinityModelId ?? input.modelId;
22637
- const matchesAffinity = (assignment) => affinityModelId !== undefined ? assignment.affinityModelId === affinityModelId : assignment.family === input.family;
22800
+ const matchesAffinity = (assignment2) => affinityModelId !== undefined ? assignment2.affinityModelId === affinityModelId : assignment2.family === input.family;
22638
22801
  const identityForAccount = (accountId) => {
22639
22802
  const identity2 = input.accountIdentities?.get(accountId);
22640
22803
  return typeof identity2 === "string" && identity2.length > 0 && identity2.trim() === identity2 ? identity2 : undefined;
22641
22804
  };
22642
- const matchesAccountIdentity = (assignment) => {
22805
+ const matchesAccountIdentity = (assignment2) => {
22643
22806
  if (!input.accountIdentities)
22644
22807
  return true;
22645
- const identity2 = identityForAccount(this.logicalAccountId(assignment));
22646
- return identity2 !== undefined && assignment.accountIdentity === identity2;
22808
+ const identity2 = identityForAccount(this.logicalAccountId(assignment2));
22809
+ return identity2 !== undefined && assignment2.accountIdentity === identity2;
22647
22810
  };
22648
22811
  const cached = this.state.assignments[key];
22649
22812
  if (cached && this.assignmentIsActive(cached) && matchesAffinity(cached) && matchesAccountIdentity(cached) && input.retainAccountIds.has(this.logicalAccountId(cached)) && !input.excludeAccountIds?.has(this.logicalAccountId(cached))) {
@@ -22651,6 +22814,7 @@ class StickySessionRouter {
22651
22814
  cached.lastSeenAt = this.now();
22652
22815
  this.scheduleTouch(key);
22653
22816
  }
22817
+ this.retryChange(key);
22654
22818
  return {
22655
22819
  accountId: this.logicalAccountId(cached),
22656
22820
  assignment: { ...cached },
@@ -22658,81 +22822,69 @@ class StickySessionRouter {
22658
22822
  migrated: false
22659
22823
  };
22660
22824
  }
22661
- const lock = await this.acquireLock();
22662
- try {
22663
- const state = await this.readState();
22664
- this.prune(state);
22665
- const current = state.assignments[key];
22666
- if (current && matchesAffinity(current) && matchesAccountIdentity(current) && input.retainAccountIds.has(this.logicalAccountId(current)) && !input.excludeAccountIds?.has(this.logicalAccountId(current))) {
22667
- current.lastSeenAt = this.now();
22668
- await this.writeResolutionState(state, () => lock.assertOwned(), "touch");
22669
- return {
22670
- accountId: this.logicalAccountId(current),
22671
- assignment: { ...current },
22672
- created: false,
22673
- migrated: false
22674
- };
22675
- }
22676
- const affinityChanged = Boolean(current && !matchesAffinity(current));
22677
- const identityChanged = Boolean(current && !matchesAccountIdentity(current));
22678
- if (affinityChanged || identityChanged) {
22679
- delete state.assignments[key];
22680
- }
22681
- const candidates = input.candidates.filter((candidate) => !input.excludeAccountIds?.has(candidate.accountId) && (!input.accountIdentities || identityForAccount(candidate.accountId) !== undefined));
22682
- const preferred = !affinityChanged && !identityChanged && input.preferredAccountId ? candidates.find((candidate) => candidate.accountId === input.preferredAccountId && stickyRouteCandidateWeight({
22683
- candidate,
22684
- family: input.family,
22685
- modelId: input.modelId,
22686
- storage: input.storage,
22687
- now: this.now()
22688
- }) > 0) : undefined;
22689
- const selected = preferred ?? this.selectCandidate({ ...input, candidates, state });
22690
- if (!selected) {
22691
- if (current) {
22692
- delete state.assignments[key];
22693
- await this.writeResolutionState(state, () => lock.assertOwned(), "delete");
22694
- }
22695
- return null;
22696
- }
22697
- const now = this.now();
22698
- const selectedQuota = knownQuota(selected.quota);
22699
- const assignment = {
22700
- accountId: selected.accountId,
22701
- ...input.accountIdentities ? { accountIdentity: identityForAccount(selected.accountId) } : {},
22702
- family: input.family,
22703
- affinityModelId,
22704
- assignedAt: now,
22705
- lastSeenAt: now,
22706
- initialInputBytes: Math.max(1, input.inputBytes),
22707
- quotaCheckedAt: selectedQuota ? snapshotCheckedAt(selectedQuota) : 0
22708
- };
22709
- state.assignments[key] = assignment;
22710
- await this.writeResolutionState(state, () => lock.assertOwned(), "assign");
22825
+ const state = structuredClone(this.state);
22826
+ const save = (operation) => {
22827
+ this.scheduleChange(key, state.assignments[key], operation);
22828
+ };
22829
+ this.prune(state);
22830
+ const current = state.assignments[key];
22831
+ if (current && matchesAffinity(current) && matchesAccountIdentity(current) && input.retainAccountIds.has(this.logicalAccountId(current)) && !input.excludeAccountIds?.has(this.logicalAccountId(current))) {
22832
+ current.lastSeenAt = this.now();
22833
+ save("touch");
22711
22834
  return {
22712
- accountId: selected.accountId,
22713
- assignment: { ...assignment },
22714
- created: !current,
22715
- migrated: Boolean(current && (identityChanged || current.accountId !== selected.accountId))
22835
+ accountId: this.logicalAccountId(current),
22836
+ assignment: { ...current },
22837
+ created: false,
22838
+ migrated: false
22716
22839
  };
22717
- } finally {
22718
- await lock.release();
22719
22840
  }
22841
+ const affinityChanged = Boolean(current && !matchesAffinity(current));
22842
+ const identityChanged = Boolean(current && !matchesAccountIdentity(current));
22843
+ if (affinityChanged || identityChanged) {
22844
+ delete state.assignments[key];
22845
+ }
22846
+ const candidates = input.candidates.filter((candidate) => !input.excludeAccountIds?.has(candidate.accountId) && (!input.accountIdentities || identityForAccount(candidate.accountId) !== undefined));
22847
+ const preferred = !affinityChanged && !identityChanged && input.preferredAccountId ? candidates.find((candidate) => candidate.accountId === input.preferredAccountId && stickyRouteCandidateWeight({
22848
+ candidate,
22849
+ family: input.family,
22850
+ modelId: input.modelId,
22851
+ storage: input.storage,
22852
+ now: this.now()
22853
+ }) > 0) : undefined;
22854
+ const selected = preferred ?? this.selectCandidate({ ...input, candidates, state });
22855
+ if (!selected) {
22856
+ if (current) {
22857
+ delete state.assignments[key];
22858
+ save("delete");
22859
+ }
22860
+ return null;
22861
+ }
22862
+ const now = this.now();
22863
+ const selectedQuota = knownQuota(selected.quota);
22864
+ const assignment = {
22865
+ accountId: selected.accountId,
22866
+ ...input.accountIdentities ? { accountIdentity: identityForAccount(selected.accountId) } : {},
22867
+ family: input.family,
22868
+ affinityModelId,
22869
+ assignedAt: now,
22870
+ lastSeenAt: now,
22871
+ initialInputBytes: Math.max(1, input.inputBytes),
22872
+ quotaCheckedAt: selectedQuota ? snapshotCheckedAt(selectedQuota) : 0
22873
+ };
22874
+ state.assignments[key] = assignment;
22875
+ save("assign");
22876
+ return {
22877
+ accountId: selected.accountId,
22878
+ assignment: { ...assignment },
22879
+ created: !current,
22880
+ migrated: Boolean(current && (identityChanged || current.accountId !== selected.accountId))
22881
+ };
22720
22882
  }
22721
22883
  async clear(sessionId) {
22722
22884
  if (!sessionId)
22723
22885
  return;
22724
- await this.recoverLocalPins();
22725
- const key = sessionKey(sessionId);
22726
- const lock = await this.acquireLock();
22727
- try {
22728
- const state = await this.readState();
22729
- if (!(key in state.assignments))
22730
- return;
22731
- delete state.assignments[key];
22732
- await this.writeState(state, () => lock.assertOwned());
22733
- } finally {
22734
- await lock.release();
22735
- }
22886
+ this.recoverLocalPins();
22887
+ this.scheduleChange(sessionKey(sessionId), undefined, "delete");
22736
22888
  }
22737
22889
  }
22738
22890
 
@@ -24394,16 +24546,21 @@ function acquireNativeAccountRuntime(options) {
24394
24546
  options.local?.refreshToken,
24395
24547
  options.local?.resolveIdentity,
24396
24548
  options.local?.failurePolicy,
24397
- options.local?.now
24549
+ options.local?.now,
24550
+ options.local?.onLockEvent
24398
24551
  ].map(nativeRuntimeOptionIdentity)
24399
24552
  ]);
24400
24553
  const { use, close } = sharedAccounts.acquire(key, () => createNativeAccountRuntime(options));
24401
24554
  const vault = leaseNativeVaultRuntime(() => use().vault, close);
24402
24555
  const quotaObservations = use().quotaObservations;
24556
+ const onLockEvent = use().onLockEvent;
24557
+ const drainRoutingPublications = use().drainRoutingPublications;
24403
24558
  return {
24559
+ onLockEvent,
24404
24560
  vault,
24405
24561
  quotaObservations,
24406
24562
  drainQuotaPublications: () => quotaObservations.drain(),
24563
+ drainRoutingPublications,
24407
24564
  getRelayConfig: () => use().getRelayConfig(),
24408
24565
  getSettingsPath: () => use().getSettingsPath(),
24409
24566
  updateRelay: (patch) => use().updateRelay(patch),
@@ -24428,7 +24585,10 @@ function acquireNativeAccountRuntime(options) {
24428
24585
  fetchQuota: (id2, transport, signal, dispatch, purpose) => use().fetchQuota(id2, transport, signal, dispatch, purpose),
24429
24586
  fetchProfile: (id2, transport, signal) => use().fetchProfile(id2, transport, signal),
24430
24587
  fetchProfileForDisplay: (id2, transport, signal) => use().fetchProfileForDisplay(id2, transport, signal),
24431
- close: () => vault.close()
24588
+ close: () => {
24589
+ vault.close();
24590
+ return drainRoutingPublications();
24591
+ }
24432
24592
  };
24433
24593
  }
24434
24594
  function nativeAccountPolicy(account, snapshot, modelId) {
@@ -24440,6 +24600,8 @@ function nativeAccountPolicy(account, snapshot, modelId) {
24440
24600
  }
24441
24601
  function createNativeAccountRuntime(options) {
24442
24602
  const paths = Object.freeze({ ...options.paths });
24603
+ const onLockEvent = nativeLockObserver(options.local?.onLockEvent ?? options.vault?.onLockEvent);
24604
+ const withLock3 = observeNativeLocks(() => withLock, onLockEvent);
24443
24605
  const now = options.local?.now ?? options.vault?.now ?? Date.now;
24444
24606
  const env = { ...options.vault?.env ?? process.env };
24445
24607
  const vaultPaths = nativeVaultPaths(paths);
@@ -24450,6 +24612,7 @@ function createNativeAccountRuntime(options) {
24450
24612
  const vaultRouter = new StickySessionRouter({
24451
24613
  path: vaultPaths.routing,
24452
24614
  vaultSeed: routingSeed,
24615
+ onLockEvent,
24453
24616
  now
24454
24617
  });
24455
24618
  let settingsBootstrap;
@@ -24457,6 +24620,7 @@ function createNativeAccountRuntime(options) {
24457
24620
  paths: vaultPaths,
24458
24621
  quota: nativeQuotaCodec,
24459
24622
  now,
24623
+ onLockEvent: options.local?.onLockEvent,
24460
24624
  onStep: async (point) => {
24461
24625
  if (point === "before-config-write" || point === "before-state-write")
24462
24626
  await requireMode("claustrum");
@@ -24467,6 +24631,7 @@ function createNativeAccountRuntime(options) {
24467
24631
  paths,
24468
24632
  quota: nativeQuotaCodec,
24469
24633
  now,
24634
+ onLockEvent: options.local?.onLockEvent,
24470
24635
  onStep: async (point) => {
24471
24636
  if (point === "before-config-write" || point === "before-state-write") {
24472
24637
  if (options.beforePoolWrite) {
@@ -24480,7 +24645,8 @@ function createNativeAccountRuntime(options) {
24480
24645
  const vault = acquireNativeVaultRuntime({
24481
24646
  ...options.vault,
24482
24647
  paths,
24483
- host: options.host
24648
+ host: options.host,
24649
+ onLockEvent
24484
24650
  });
24485
24651
  function check(signal) {
24486
24652
  if (shutdown.signal.aborted)
@@ -24547,7 +24713,15 @@ function createNativeAccountRuntime(options) {
24547
24713
  };
24548
24714
  const [config, state] = nativePoolStoreLocks(paths);
24549
24715
  try {
24550
- return await withLock(config.path, { ...POOL_LOCK_DEFAULTS, name: config.name }, async (configLease) => withLock(state.path, { ...POOL_LOCK_DEFAULTS, name: state.name }, async (stateLease) => {
24716
+ return await withLock3(config.path, {
24717
+ ...POOL_LOCK_DEFAULTS,
24718
+ name: config.name,
24719
+ onLockEvent: options.local?.onLockEvent
24720
+ }, async (configLease) => withLock3(state.path, {
24721
+ ...POOL_LOCK_DEFAULTS,
24722
+ name: state.name,
24723
+ onLockEvent: options.local?.onLockEvent
24724
+ }, async (stateLease) => {
24551
24725
  if (!await matches2())
24552
24726
  return false;
24553
24727
  await updateNativeRuntime(paths.runtime, paths.storageId, (current) => {
@@ -24557,6 +24731,7 @@ function createNativeAccountRuntime(options) {
24557
24731
  current.accounts[binding2.rowId] = change(same && old ? old : { binding: binding2 }, old);
24558
24732
  return current;
24559
24733
  }, {
24734
+ onLockEvent: options.local?.onLockEvent,
24560
24735
  beforeRename: async () => {
24561
24736
  if (!await matches2())
24562
24737
  throw new NativeRuntimeError("publication-refused");
@@ -24706,24 +24881,32 @@ function createNativeAccountRuntime(options) {
24706
24881
  return source2 !== undefined && nativeLocalRouteAwaitsTornWrite(paths, source2.rows, source2.settings, routeId, source2.runtime);
24707
24882
  }
24708
24883
  const runtime2 = {
24884
+ onLockEvent,
24709
24885
  vault,
24710
24886
  quotaObservations: vault.quotaObservations,
24711
24887
  drainQuotaPublications: () => vault.quotaObservations.drain(),
24888
+ drainRoutingPublications: () => vaultRouter.drain(),
24712
24889
  async getRelayConfig() {
24713
24890
  const { mode, paths: paths2, store: store2 } = await selectedStorage();
24714
- return withLock(paths2.config, { ...POOL_LOCK_DEFAULTS, name: "native-relay" }, async (lease) => {
24891
+ for (let attempt = 0;attempt < 4; attempt++) {
24892
+ if (attempt)
24893
+ await new Promise((resolve10) => setTimeout(resolve10, 10));
24715
24894
  const settings = await store2.readSettings();
24716
24895
  if (settings.status !== "ready")
24717
24896
  throw new NativeRuntimeError("publication-refused");
24718
24897
  const flags2 = relayFlags(settings.settings.relay);
24898
+ const generation = relayGeneration(settings.settings.relay);
24719
24899
  const read = await readNativeRuntime(paths2.runtime, paths2.storageId);
24720
- const token2 = read.status === "ready" ? read.state.relay?.token : undefined;
24721
24900
  await requireMode(mode);
24722
- await lease.assertOwned();
24901
+ const state = read.status === "ready" ? read.state : undefined;
24902
+ if (generation !== state?.relayGeneration)
24903
+ continue;
24904
+ const token2 = state?.relay?.token;
24723
24905
  if (!flags2.enabled || !flags2.url || !token2)
24724
24906
  return null;
24725
24907
  return { ...flags2, token: token2 };
24726
- });
24908
+ }
24909
+ throw new NativeRuntimeError("relay-inconsistent");
24727
24910
  },
24728
24911
  async updateRelay(input) {
24729
24912
  const patch = captureNativePlainData(input);
@@ -24745,11 +24928,13 @@ function createNativeAccountRuntime(options) {
24745
24928
  if (patch.token !== undefined && patch.token !== null && (typeof patch.token !== "string" || !patch.token.trim() || /\p{Cc}/u.test(patch.token)))
24746
24929
  throw new NativeRuntimeError("publication-refused");
24747
24930
  const { mode, paths: paths2, store: store2 } = await selectedStorage();
24748
- await withLock(paths2.config, { ...POOL_LOCK_DEFAULTS, name: "native-relay" }, async (lease) => {
24931
+ await withLock3(paths2.config, { ...POOL_LOCK_DEFAULTS, name: "native-relay" }, async (lease) => {
24749
24932
  const read = await store2.readSettings();
24750
24933
  if (read.status !== "ready")
24751
24934
  throw new NativeRuntimeError("publication-refused");
24752
24935
  const previous = relayFlags(read.settings.relay);
24936
+ const previousGeneration = relayGeneration(read.settings.relay);
24937
+ const generation = randomUUID15();
24753
24938
  const metadata2 = await readNativeRuntime(paths2.runtime, paths2.storageId);
24754
24939
  const token2 = patch.token === undefined ? metadata2.status === "ready" ? metadata2.state.relay?.token : undefined : patch.token?.trim();
24755
24940
  const next = relayFlags({
@@ -24759,28 +24944,31 @@ function createNativeAccountRuntime(options) {
24759
24944
  });
24760
24945
  if (next.enabled && (!next.url || !token2))
24761
24946
  throw new NativeRuntimeError("publication-refused");
24762
- async function writeFlags(flags2) {
24947
+ async function writeFlags(flags2, stamp) {
24763
24948
  await store2.updateSettings(async (settings) => {
24764
24949
  await requireMode(mode);
24765
24950
  await lease.assertOwned();
24766
- settings.relay = flags2;
24951
+ settings.relay = {
24952
+ ...flags2,
24953
+ ...stamp === undefined ? {} : { generation: stamp }
24954
+ };
24767
24955
  });
24768
24956
  }
24769
- await writeFlags({ ...previous, enabled: false });
24770
- if (patch.token !== undefined)
24771
- await updateNativeRuntime(paths2.runtime, paths2.storageId, (current) => {
24772
- if (token2)
24773
- current.relay = { token: token2 };
24774
- else
24775
- delete current.relay;
24776
- return current;
24777
- }, {
24778
- beforeRename: async () => {
24779
- await requireMode(mode);
24780
- await lease.assertOwned();
24781
- }
24782
- });
24783
- await writeFlags(next);
24957
+ await writeFlags({ ...previous, enabled: false }, previousGeneration);
24958
+ await updateNativeRuntime(paths2.runtime, paths2.storageId, (current) => {
24959
+ if (token2)
24960
+ current.relay = { token: token2 };
24961
+ else
24962
+ delete current.relay;
24963
+ current.relayGeneration = generation;
24964
+ return current;
24965
+ }, {
24966
+ beforeRename: async () => {
24967
+ await requireMode(mode);
24968
+ await lease.assertOwned();
24969
+ }
24970
+ });
24971
+ await writeFlags(next, generation);
24784
24972
  });
24785
24973
  },
24786
24974
  async getSettingsPath() {
@@ -24801,7 +24989,7 @@ function createNativeAccountRuntime(options) {
24801
24989
  logger.debug("native-auth", "waiting for an account write to finish", {
24802
24990
  phase: "waiting"
24803
24991
  });
24804
- const joined = await joinNativePoolWrite(paths, request.signal ? [shutdown.signal, request.signal] : [shutdown.signal], () => check(request.signal));
24992
+ const joined = await joinNativePoolWrite(paths, request.signal ? [shutdown.signal, request.signal] : [shutdown.signal], () => check(request.signal), onLockEvent);
24805
24993
  if (joined === "deadline") {
24806
24994
  logger.warn("native-auth", "account write did not finish in time", {
24807
24995
  outcome: "deadline"
@@ -25302,6 +25490,7 @@ function createNativeAccountRuntime(options) {
25302
25490
  shutdown.abort(new NativeCustodyError("closed"));
25303
25491
  vault.close();
25304
25492
  }
25493
+ return vaultRouter.drain();
25305
25494
  }
25306
25495
  };
25307
25496
  function readLocalProfile(id2, fetchImpl, signal) {
@@ -25398,6 +25587,16 @@ function relayFlags(input) {
25398
25587
  fallbackToDirect: value.fallbackToDirect !== false
25399
25588
  };
25400
25589
  }
25590
+ function relayGeneration(input) {
25591
+ if (!input || typeof input !== "object" || Array.isArray(input))
25592
+ return;
25593
+ const generation = input.generation;
25594
+ if (generation === undefined)
25595
+ return;
25596
+ if (typeof generation !== "string" || !generation.trim())
25597
+ throw new NativeRuntimeError("relay-inconsistent");
25598
+ return generation;
25599
+ }
25401
25600
  // src/native-host-auth-write.ts
25402
25601
  import { randomUUID as randomUUID16 } from "node:crypto";
25403
25602
  import { lstat as lstat7, open as open9, rename as rename13, unlink as unlink6 } from "node:fs/promises";
@@ -32065,6 +32264,7 @@ export {
32065
32264
  OAUTH_SCOPES,
32066
32265
  NativeVaultNoAccountError,
32067
32266
  NativeTransientAccountError,
32267
+ NativeRuntimeError,
32068
32268
  NativeQuotaObservations,
32069
32269
  PiSlotError as NativePiAuthSlotError,
32070
32270
  NativeMigrationError,