openmeld 0.3.134 → 0.3.136

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { $ as runWithHeldLock, K as isOpenMeldLockError, Lt as openMeldRootDir, W as acquireLock, _t as CURRENT_PROCESS_START_IDENTITY, vt as isProcessIdentityAlive, xn as __exportAll } from "./base-url-BtqW4aLS.js";
4
- import { t as CURRENT_DAEMON_EXECUTION_CONTRACT_EPOCH } from "./runtime-transport-D4_-d5Yb.js";
4
+ import { t as CURRENT_DAEMON_EXECUTION_CONTRACT_EPOCH } from "./runtime-transport-BiQfJGnw.js";
5
5
  import { createRequire } from "node:module";
6
6
  import * as z$1 from "zod/v4";
7
7
  import { join, resolve } from "node:path";
@@ -110,25 +110,40 @@ function resolveAuthSessionExpiryEligibility(expiresAt, nowMs = Date.now()) {
110
110
  //#endregion
111
111
  //#region src/config/auth-session-transition-lock.ts
112
112
  const AUTH_SESSION_LOCK_KEY = "session-transition-mutation";
113
+ const AUTH_SESSION_CANDIDATE_REFRESH_LOCK_KEY = "candidate-session-refresh";
113
114
  const AUTH_SESSION_LOCK_PROFILE = "auth-session";
114
115
  const AUTH_SESSION_LOCK_RETRY_DELAY_MS = 20;
115
116
  const AUTH_SESSION_LOCK_WAIT_MS = 1e4;
116
117
  async function withAuthSessionTransitionMutationLock(operation) {
118
+ return await withAuthSessionLock({
119
+ component: "auth_session_transition",
120
+ key: AUTH_SESSION_LOCK_KEY,
121
+ operation
122
+ });
123
+ }
124
+ async function withAuthSessionCandidateRefreshLock(operation) {
125
+ return await withAuthSessionLock({
126
+ component: "auth_session_candidate_refresh",
127
+ key: AUTH_SESSION_CANDIDATE_REFRESH_LOCK_KEY,
128
+ operation
129
+ });
130
+ }
131
+ async function withAuthSessionLock(input) {
117
132
  return await runWithHeldLock({
118
- lock: await acquireAuthSessionTransitionMutationLock(),
119
- run: operation,
133
+ lock: await acquireAuthSessionLock(input),
134
+ run: input.operation,
120
135
  releaseContext: {
121
- component: "auth_session_transition",
122
- lockKey: AUTH_SESSION_LOCK_KEY
136
+ component: input.component,
137
+ lockKey: input.key
123
138
  }
124
139
  });
125
140
  }
126
- async function acquireAuthSessionTransitionMutationLock() {
141
+ async function acquireAuthSessionLock(input) {
127
142
  const deadline = Date.now() + AUTH_SESSION_LOCK_WAIT_MS;
128
143
  while (true) try {
129
144
  return await acquireLock({
130
- key: AUTH_SESSION_LOCK_KEY,
131
- meta: { component: "auth_session_transition" },
145
+ key: input.key,
146
+ meta: { component: input.component },
132
147
  openMeldProfileId: AUTH_SESSION_LOCK_PROFILE
133
148
  });
134
149
  } catch (error) {
@@ -731,6 +746,8 @@ var auth_session_exports = /* @__PURE__ */ __exportAll({
731
746
  setAuthSession: () => setAuthSession,
732
747
  updateAuthSessionActiveOrganization: () => updateAuthSessionActiveOrganization
733
748
  });
749
+ const MAX_CANDIDATE_SERVICE_PROCESSES = 16;
750
+ const MAX_CANDIDATE_TOKEN_REFS = 64;
734
751
  var AuthSessionStorageError = class extends Error {
735
752
  reason;
736
753
  constructor(input) {
@@ -772,6 +789,9 @@ const TRAILING_SLASHES_RE = /\/+$/u;
772
789
  let cachedAuthSession = null;
773
790
  const activeAuthSessionTransitionIds = /* @__PURE__ */ new Set();
774
791
  async function getAuthSessionState(options = {}) {
792
+ return await getAuthSessionStateWithCandidateRefreshFence(options, false);
793
+ }
794
+ async function getAuthSessionStateWithCandidateRefreshFence(options, candidateRefreshFenceHeld) {
775
795
  const localState = await getLocalAuthSessionState();
776
796
  const localPayloadKey = cachedAuthSession?.payloadKey ?? null;
777
797
  const shouldValidateRemote = options.validateRemote === true || options.refreshExpired === true && localState.state === "expired";
@@ -785,6 +805,7 @@ async function getAuthSessionState(options = {}) {
785
805
  default: return localState;
786
806
  }
787
807
  if (!shouldValidateRemote) return localState;
808
+ if (!candidateRefreshFenceHeld && await shouldFencePendingCandidateRemoteValidation()) return await withAuthSessionCandidateRefreshLock(async () => await getAuthSessionStateWithCandidateRefreshFence(options, true));
788
809
  const remoteValidation = await getRemoteAuthSessionState(localState.session);
789
810
  if (remoteValidation.state === "valid") return await resolveRemotelyValidatedAuthSession({
790
811
  localPayloadKey,
@@ -804,6 +825,10 @@ async function getAuthSessionState(options = {}) {
804
825
  session: null
805
826
  };
806
827
  }
828
+ async function shouldFencePendingCandidateRemoteValidation() {
829
+ const journal = await readAuthSessionTransitionJournal();
830
+ return Boolean(journal?.state === "pending" && journal.candidateCredentialRefreshVersion === 1 && isAuthSessionTransitionCandidateVisibleToCurrentProcess(journal));
831
+ }
807
832
  async function resolveRemotelyValidatedAuthSession(input) {
808
833
  if (!input.remoteValidation.expiresAt || isExpired(input.remoteValidation.expiresAt)) return input.localState;
809
834
  let refreshedSession;
@@ -816,7 +841,14 @@ async function resolveRemotelyValidatedAuthSession(input) {
816
841
  });
817
842
  } catch (error) {
818
843
  if (!(error instanceof AuthSessionRecoveryInProgressError)) throw error;
819
- return await getLocalAuthSessionState({ recover: false });
844
+ return {
845
+ state: "valid",
846
+ session: {
847
+ ...input.localState.session,
848
+ accessToken: input.remoteValidation.accessToken ?? input.localState.session.accessToken,
849
+ expiresAt: input.remoteValidation.expiresAt
850
+ }
851
+ };
820
852
  }
821
853
  if (!refreshedSession) return await getLocalAuthSessionState();
822
854
  return {
@@ -922,6 +954,8 @@ async function beginAuthSessionTransition(input, context = {}) {
922
954
  ownerProcessStartedAt: CURRENT_PROCESS_START_IDENTITY,
923
955
  previousPayload: previousPayload?.v === 1 ? previousPayload : null,
924
956
  candidatePayload: nextPayload,
957
+ candidateCredentialRefreshVersion: 1,
958
+ candidateTokenRefs: [tokenRef],
925
959
  ...context.computerHandoff ? { computerHandoff: { gatewayUrl: context.computerHandoff.gatewayUrl } } : {}
926
960
  };
927
961
  let journalClaimed = false;
@@ -957,14 +991,14 @@ async function beginAuthSessionTransition(input, context = {}) {
957
991
  if (!journal.previousPayload) return currentPayload === null;
958
992
  return currentPayload?.v === 1 && buildAuthSessionPayloadKey(currentPayload) === buildAuthSessionPayloadKey(journal.previousPayload);
959
993
  };
960
- const isCandidateSessionCurrent = (currentPayload) => currentPayload?.v === 1 && buildAuthSessionPayloadKey(currentPayload) === buildAuthSessionPayloadKey(nextPayload);
994
+ const isCandidateSessionCurrent = (currentPayload, candidatePayload = nextPayload) => currentPayload?.v === 1 && buildAuthSessionPayloadKey(currentPayload) === buildAuthSessionPayloadKey(candidatePayload);
961
995
  const assertPreviousSessionCurrent = async () => {
962
996
  const currentPayload = await readSessionPayloadFile();
963
997
  if (!isPreviousSessionCurrent(currentPayload)) throw new Error("auth session changed before the setup transition completed");
964
998
  };
965
- const assertTransitionSessionCurrent = async () => {
999
+ const assertTransitionSessionCurrent = async (candidatePayload = nextPayload) => {
966
1000
  const currentPayload = await readSessionPayloadFile();
967
- if (!(isPreviousSessionCurrent(currentPayload) || isCandidateSessionCurrent(currentPayload))) throw new Error("auth session changed before the setup transition completed");
1001
+ if (!(isPreviousSessionCurrent(currentPayload) || isCandidateSessionCurrent(currentPayload, candidatePayload))) throw new Error("auth session changed before the setup transition completed");
968
1002
  };
969
1003
  const restoreMissingJournalTransition = async () => {
970
1004
  const currentPayload = await readSessionPayloadFile();
@@ -1016,14 +1050,13 @@ async function beginAuthSessionTransition(input, context = {}) {
1016
1050
  reason: currentJournal.status
1017
1051
  });
1018
1052
  }
1053
+ const committedPayload = currentJournal.journal.candidatePayload;
1054
+ const committedSession = await readStoredAuthSessionFromPayload(committedPayload);
1019
1055
  await assertPreviousSessionCurrent();
1020
- await writeSessionPayloadAtomically(authSessionPath(), nextPayload);
1056
+ await writeSessionPayloadAtomically(authSessionPath(), committedPayload);
1021
1057
  cacheAuthSession({
1022
- payload: nextPayload,
1023
- session: buildStoredAuthSession({
1024
- accessToken,
1025
- payload: nextPayload
1026
- })
1058
+ payload: committedPayload,
1059
+ session: committedSession
1027
1060
  });
1028
1061
  const committedJournal = {
1029
1062
  ...currentJournal.journal,
@@ -1054,7 +1087,7 @@ async function beginAuthSessionTransition(input, context = {}) {
1054
1087
  reason: currentJournal.status
1055
1088
  });
1056
1089
  }
1057
- await assertTransitionSessionCurrent();
1090
+ await assertTransitionSessionCurrent(currentJournal.journal.candidatePayload);
1058
1091
  await restoreTransitionPreviousPayload(currentJournal.journal, previousSession);
1059
1092
  settlement = "rolled_back";
1060
1093
  activeAuthSessionTransitionIds.delete(journal.id);
@@ -1066,7 +1099,7 @@ async function beginAuthSessionTransition(input, context = {}) {
1066
1099
  });
1067
1100
  return;
1068
1101
  }
1069
- await deleteAccessTokenOrRetainForCleanup(tokenRef);
1102
+ await cleanupCandidateTokenRefs(currentJournal.journal);
1070
1103
  await removeAuthSessionTransitionJournalIfOwned(currentJournal.journal);
1071
1104
  })
1072
1105
  };
@@ -1164,7 +1197,7 @@ async function completeInterruptedComputerAuthSessionRecovery(id) {
1164
1197
  const journal = await readRequiredComputerAuthRecoveryJournal(id);
1165
1198
  if (!(journal.computerHandoff.serviceReconciledAt && journal.computerHandoff.remoteSessionRevokedAt)) throw new Error("computer auth recovery cannot complete before service and remote-session cleanup");
1166
1199
  try {
1167
- await deleteAccessTokenOrRetainForCleanup(journal.candidatePayload.tokenRef);
1200
+ await cleanupCandidateTokenRefs(journal);
1168
1201
  await removeAuthSessionTransitionJournalIfOwned(journal);
1169
1202
  return true;
1170
1203
  } catch (error) {
@@ -1436,7 +1469,7 @@ function buildStoredAuthSessionIdentity(session) {
1436
1469
  async function cleanupTransitionTokensNotReferencedByCurrentSession(input) {
1437
1470
  const currentTokenRef = normalizeTokenRef(input.currentPayload?.tokenRef);
1438
1471
  const previousTokenRef = normalizeTokenRef(input.journal.previousPayload?.tokenRef);
1439
- const transitionTokenRefs = [...previousTokenRef ? [previousTokenRef] : [], input.journal.candidatePayload.tokenRef];
1472
+ const transitionTokenRefs = [...previousTokenRef ? [previousTokenRef] : [], ...getCandidateTokenRefs(input.journal)];
1440
1473
  for (const [index, tokenRef] of transitionTokenRefs.entries()) {
1441
1474
  if (transitionTokenRefs.slice(0, index).some((previousRef) => areStoredTokenRefsEqual(previousRef, tokenRef)) || currentTokenRef && areStoredTokenRefsEqual(currentTokenRef, tokenRef)) continue;
1442
1475
  await deleteAccessTokenOrRetainForCleanup(tokenRef);
@@ -1458,57 +1491,134 @@ function buildAuthSessionPayloadKey(payload) {
1458
1491
  function areStoredTokenRefsEqual(left, right) {
1459
1492
  return JSON.stringify(left) === JSON.stringify(right);
1460
1493
  }
1494
+ function getUniqueTokenRefs(tokenRefs) {
1495
+ const uniqueTokenRefs = [];
1496
+ for (const tokenRef of tokenRefs) if (!uniqueTokenRefs.some((existing) => areStoredTokenRefsEqual(existing, tokenRef))) uniqueTokenRefs.push(tokenRef);
1497
+ return uniqueTokenRefs;
1498
+ }
1499
+ function getCandidateTokenRefs(journal) {
1500
+ return getUniqueTokenRefs([...journal.candidateTokenRefs ?? [], journal.candidatePayload.tokenRef]);
1501
+ }
1502
+ async function cleanupCandidateTokenRefs(journal, keepTokenRef) {
1503
+ for (const tokenRef of getCandidateTokenRefs(journal)) {
1504
+ if (keepTokenRef && areStoredTokenRefsEqual(keepTokenRef, tokenRef)) continue;
1505
+ await deleteAccessTokenOrRetainForCleanup(tokenRef);
1506
+ }
1507
+ }
1461
1508
  async function persistRemoteSessionRefresh(input) {
1462
1509
  return await withAuthSessionTransitionMutationLock(async () => {
1463
- await assertAuthSessionMutationAvailableWithOwnership();
1464
- const payload = await readSessionPayloadFile();
1465
- if (payload?.v !== 1 || input.expectedPayloadKey === null || buildAuthSessionPayloadKey(payload) !== input.expectedPayloadKey) return null;
1466
- const tokenRef = normalizeTokenRef(payload.tokenRef);
1467
- const tokenType = normalizeOptionalString(payload.tokenType);
1468
- const authBaseUrl = normalizeOptionalString(payload.authBaseUrl);
1469
- const clientId = normalizeOptionalString(payload.clientId);
1470
- const createdAt = normalizeOptionalString(payload.createdAt);
1471
- if (!(tokenRef && tokenType && authBaseUrl && clientId && createdAt)) return null;
1472
- const nextAccessToken = normalizeOptionalString(input.accessToken);
1473
- if (!nextAccessToken) return null;
1474
- const nextTokenRef = nextAccessToken === input.previousAccessToken ? tokenRef : await storeAccessToken({
1475
- accessToken: nextAccessToken,
1476
- authBaseUrl,
1477
- clientId
1478
- });
1479
- const nextPayload = {
1480
- v: 1,
1481
- tokenRef: nextTokenRef,
1482
- tokenType,
1483
- authBaseUrl,
1484
- clientId,
1485
- createdAt,
1486
- expiresAt: input.expiresAt,
1487
- ...typeof payload.scope === "string" && payload.scope.trim() ? { scope: payload.scope.trim() } : {},
1488
- ...buildAuthUserPayload(payload.user),
1489
- ...buildActiveOrganizationPayload(payload.activeOrganization)
1490
- };
1491
- const nextSession = buildStoredAuthSession({
1492
- accessToken: nextAccessToken,
1493
- payload: nextPayload
1494
- });
1495
- try {
1496
- await assertStoredAccessTokenReloads({
1497
- accessToken: nextAccessToken,
1498
- tokenRef: nextTokenRef
1510
+ await recoverInterruptedAuthSessionTransitionWithOwnership();
1511
+ const transitionJournal = await readAuthSessionTransitionJournal();
1512
+ if (transitionJournal) {
1513
+ if (transitionJournal.state === "pending" && transitionJournal.candidateCredentialRefreshVersion === 1 && isAuthSessionTransitionCandidateVisibleToCurrentProcess(transitionJournal) && input.expectedPayloadKey !== null && buildAuthSessionPayloadKey(transitionJournal.candidatePayload) === input.expectedPayloadKey) return await persistPendingCandidateRemoteSessionRefresh({
1514
+ ...input,
1515
+ journal: transitionJournal
1499
1516
  });
1500
- await writeSessionPayloadAtomically(authSessionPath(), nextPayload);
1501
- } catch (error) {
1502
- if (!areStoredTokenRefsEqual(tokenRef, nextTokenRef)) await deleteAccessTokenOrRetainForCleanup(nextTokenRef);
1503
- throw error;
1517
+ throw new AuthSessionRecoveryInProgressError();
1504
1518
  }
1505
- cacheAuthSession({
1506
- payload: nextPayload,
1507
- session: nextSession
1519
+ return await persistCommittedRemoteSessionRefresh(input);
1520
+ });
1521
+ }
1522
+ async function persistCommittedRemoteSessionRefresh(input) {
1523
+ const payload = await readSessionPayloadFile();
1524
+ if (payload?.v !== 1 || input.expectedPayloadKey === null || buildAuthSessionPayloadKey(payload) !== input.expectedPayloadKey) return null;
1525
+ const refresh = await prepareRemoteSessionRefresh({
1526
+ ...input,
1527
+ payload,
1528
+ uniqueTokenRef: false
1529
+ });
1530
+ if (!refresh) return null;
1531
+ try {
1532
+ await assertStoredAccessTokenReloads({
1533
+ accessToken: refresh.nextSession.accessToken,
1534
+ tokenRef: refresh.nextTokenRef
1508
1535
  });
1509
- if (!areStoredTokenRefsEqual(tokenRef, nextTokenRef)) await deleteAccessTokenOrRetainForCleanup(tokenRef);
1510
- return nextSession;
1536
+ await writeSessionPayloadAtomically(authSessionPath(), refresh.nextPayload);
1537
+ } catch (error) {
1538
+ await cleanupFailedRemoteSessionRefresh(refresh);
1539
+ throw error;
1540
+ }
1541
+ cacheAuthSession({
1542
+ payload: refresh.nextPayload,
1543
+ session: refresh.nextSession
1511
1544
  });
1545
+ if (!areStoredTokenRefsEqual(refresh.currentTokenRef, refresh.nextTokenRef)) await deleteAccessTokenOrRetainForCleanup(refresh.currentTokenRef);
1546
+ return refresh.nextSession;
1547
+ }
1548
+ async function persistPendingCandidateRemoteSessionRefresh(input) {
1549
+ const refresh = await prepareRemoteSessionRefresh({
1550
+ ...input,
1551
+ payload: input.journal.candidatePayload,
1552
+ uniqueTokenRef: true
1553
+ });
1554
+ if (!refresh) return null;
1555
+ const candidateTokenRefs = getUniqueTokenRefs([...getCandidateTokenRefs(input.journal), refresh.nextTokenRef]);
1556
+ if (candidateTokenRefs.length > MAX_CANDIDATE_TOKEN_REFS) {
1557
+ await cleanupFailedRemoteSessionRefresh(refresh);
1558
+ throw new AuthSessionStorageError({
1559
+ message: `auth session transition has more than ${String(MAX_CANDIDATE_TOKEN_REFS)} candidate token rotations`,
1560
+ reason: "unreadable"
1561
+ });
1562
+ }
1563
+ const nextJournal = {
1564
+ ...input.journal,
1565
+ candidatePayload: refresh.nextPayload,
1566
+ candidateTokenRefs
1567
+ };
1568
+ try {
1569
+ await assertStoredAccessTokenReloads({
1570
+ accessToken: refresh.nextSession.accessToken,
1571
+ tokenRef: refresh.nextTokenRef
1572
+ });
1573
+ await writeAuthSessionTransitionJournalIfOwned(input.journal, nextJournal);
1574
+ } catch (error) {
1575
+ await cleanupFailedRemoteSessionRefresh(refresh);
1576
+ throw error;
1577
+ }
1578
+ cacheAuthSession({
1579
+ payload: refresh.nextPayload,
1580
+ session: refresh.nextSession
1581
+ });
1582
+ return refresh.nextSession;
1583
+ }
1584
+ async function prepareRemoteSessionRefresh(input) {
1585
+ const currentTokenRef = normalizeTokenRef(input.payload.tokenRef);
1586
+ const tokenType = normalizeOptionalString(input.payload.tokenType);
1587
+ const authBaseUrl = normalizeOptionalString(input.payload.authBaseUrl);
1588
+ const clientId = normalizeOptionalString(input.payload.clientId);
1589
+ const createdAt = normalizeOptionalString(input.payload.createdAt);
1590
+ const nextAccessToken = normalizeOptionalString(input.accessToken);
1591
+ if (!(currentTokenRef && tokenType && authBaseUrl && clientId && createdAt && nextAccessToken)) return null;
1592
+ const nextTokenRef = nextAccessToken === input.previousAccessToken ? currentTokenRef : await storeAccessToken({
1593
+ accessToken: nextAccessToken,
1594
+ authBaseUrl,
1595
+ clientId,
1596
+ unique: input.uniqueTokenRef
1597
+ });
1598
+ const nextPayload = {
1599
+ v: 1,
1600
+ tokenRef: nextTokenRef,
1601
+ tokenType,
1602
+ authBaseUrl,
1603
+ clientId,
1604
+ createdAt,
1605
+ expiresAt: input.expiresAt,
1606
+ ...typeof input.payload.scope === "string" && input.payload.scope.trim() ? { scope: input.payload.scope.trim() } : {},
1607
+ ...buildAuthUserPayload(input.payload.user),
1608
+ ...buildActiveOrganizationPayload(input.payload.activeOrganization)
1609
+ };
1610
+ return {
1611
+ currentTokenRef,
1612
+ nextPayload,
1613
+ nextSession: buildStoredAuthSession({
1614
+ accessToken: nextAccessToken,
1615
+ payload: nextPayload
1616
+ }),
1617
+ nextTokenRef
1618
+ };
1619
+ }
1620
+ async function cleanupFailedRemoteSessionRefresh(refresh) {
1621
+ if (!areStoredTokenRefsEqual(refresh.currentTokenRef, refresh.nextTokenRef)) await deleteAccessTokenOrRetainForCleanup(refresh.nextTokenRef);
1512
1622
  }
1513
1623
  function normalizeTokenRef(input) {
1514
1624
  if (!isStoredTokenRef(input)) return null;
@@ -1587,12 +1697,17 @@ function resolveRemoteAuthSessionReasonCode(state) {
1587
1697
  async function readSessionPayload() {
1588
1698
  await recoverInterruptedAuthSessionTransition();
1589
1699
  const journal = await readAuthSessionTransitionJournal();
1590
- if (journal?.state === "pending" && isAuthSessionTransitionCandidateVisibleToCurrentProcess(journal)) return journal.candidatePayload;
1700
+ if (journal?.state === "pending") {
1701
+ if (isAuthSessionTransitionCandidateVisibleToCurrentProcess(journal)) return journal.candidatePayload;
1702
+ }
1591
1703
  return await readSessionPayloadFile();
1592
1704
  }
1593
1705
  function isAuthSessionTransitionCandidateVisibleToCurrentProcess(journal) {
1594
1706
  if (activeAuthSessionTransitionIds.has(journal.id)) return true;
1595
- return journal.candidateServiceProcess?.ownerPid === process.pid && journal.candidateServiceProcess.ownerProcessStartedAt === CURRENT_PROCESS_START_IDENTITY;
1707
+ return getRegisteredCandidateServiceProcesses(journal).some((candidateServiceProcess) => isSameProcessIdentity(candidateServiceProcess, {
1708
+ ownerPid: process.pid,
1709
+ ownerProcessStartedAt: CURRENT_PROCESS_START_IDENTITY
1710
+ }));
1596
1711
  }
1597
1712
  async function readSessionPayloadFile() {
1598
1713
  const path = authSessionPath();
@@ -1639,7 +1754,7 @@ async function recoverInterruptedAuthSessionTransitionWithOwnership() {
1639
1754
  });
1640
1755
  return;
1641
1756
  }
1642
- await deleteAccessTokenOrRetainForCleanup(journal.candidatePayload.tokenRef);
1757
+ await cleanupCandidateTokenRefs(journal);
1643
1758
  await removeAuthSessionTransitionJournalIfOwned(journal);
1644
1759
  return;
1645
1760
  }
@@ -1664,6 +1779,7 @@ async function recoverInterruptedAuthSessionTransitionWithOwnership() {
1664
1779
  async function finalizeCommittedAuthSessionTransition(journal) {
1665
1780
  const previousTokenRef = normalizeTokenRef(journal.previousPayload?.tokenRef);
1666
1781
  if (previousTokenRef && !areStoredTokenRefsEqual(previousTokenRef, journal.candidatePayload.tokenRef)) await deleteAccessTokenOrRetainForCleanup(previousTokenRef);
1782
+ await cleanupCandidateTokenRefs(journal, journal.candidatePayload.tokenRef);
1667
1783
  await removeAuthSessionTransitionJournalIfOwned(journal);
1668
1784
  }
1669
1785
  async function registerCurrentCandidateServiceProcess(journal) {
@@ -1672,29 +1788,43 @@ async function registerCurrentCandidateServiceProcess(journal) {
1672
1788
  ownerPid: process.pid,
1673
1789
  ownerProcessStartedAt: CURRENT_PROCESS_START_IDENTITY
1674
1790
  };
1675
- if (journal.candidateServiceProcess?.ownerPid === currentProcess.ownerPid && journal.candidateServiceProcess.ownerProcessStartedAt === currentProcess.ownerProcessStartedAt) return journal;
1791
+ if (getRegisteredCandidateServiceProcesses(journal).some((candidateServiceProcess) => isSameProcessIdentity(candidateServiceProcess, currentProcess))) return journal;
1676
1792
  const currentJournal = await readAuthSessionTransitionJournal();
1677
1793
  if (currentJournal?.id !== journal.id || currentJournal.state !== "pending") return currentJournal ?? journal;
1794
+ const candidateServiceProcesses = getRegisteredCandidateServiceProcesses(currentJournal).filter(isProcessIdentityAlive);
1795
+ candidateServiceProcesses.push(currentProcess);
1796
+ if (candidateServiceProcesses.length > MAX_CANDIDATE_SERVICE_PROCESSES) throw new Error(`auth session transition has more than ${String(MAX_CANDIDATE_SERVICE_PROCESSES)} live candidate OpenMeld Service processes`);
1678
1797
  const updatedJournal = {
1679
1798
  ...currentJournal,
1799
+ candidateServiceProcesses,
1680
1800
  candidateServiceProcess: currentProcess
1681
1801
  };
1682
1802
  await writeAuthSessionTransitionJournalIfOwned(currentJournal, updatedJournal);
1683
1803
  return updatedJournal;
1684
1804
  }
1685
1805
  function stopRegisteredCandidateService(journal) {
1686
- const candidateServiceProcess = journal.candidateServiceProcess;
1687
- if (!(candidateServiceProcess && isProcessIdentityAlive(candidateServiceProcess))) return;
1688
- try {
1689
- process.kill(candidateServiceProcess.ownerPid, "SIGTERM");
1690
- } catch (error) {
1691
- if (!isFileNotFoundErrorCode(error, "ESRCH")) throw new AuthSessionStorageError({
1692
- message: `candidate OpenMeld Service process ${String(candidateServiceProcess.ownerPid)} could not be stopped: ${toErrorMessage(error)}`,
1693
- reason: "unreadable",
1694
- cause: error
1695
- });
1806
+ for (const candidateServiceProcess of getRegisteredCandidateServiceProcesses(journal)) {
1807
+ if (!isProcessIdentityAlive(candidateServiceProcess)) continue;
1808
+ try {
1809
+ process.kill(candidateServiceProcess.ownerPid, "SIGTERM");
1810
+ } catch (error) {
1811
+ if (!isFileNotFoundErrorCode(error, "ESRCH")) throw new AuthSessionStorageError({
1812
+ message: `candidate OpenMeld Service process ${String(candidateServiceProcess.ownerPid)} could not be stopped: ${toErrorMessage(error)}`,
1813
+ reason: "unreadable",
1814
+ cause: error
1815
+ });
1816
+ }
1696
1817
  }
1697
1818
  }
1819
+ function getRegisteredCandidateServiceProcesses(journal) {
1820
+ const candidates = [...journal.candidateServiceProcesses ?? [], ...journal.candidateServiceProcess ? [journal.candidateServiceProcess] : []];
1821
+ const uniqueCandidates = [];
1822
+ for (const candidate of candidates) if (!uniqueCandidates.some((existing) => isSameProcessIdentity(existing, candidate))) uniqueCandidates.push(candidate);
1823
+ return uniqueCandidates;
1824
+ }
1825
+ function isSameProcessIdentity(left, right) {
1826
+ return left.ownerPid === right.ownerPid && left.ownerProcessStartedAt === right.ownerProcessStartedAt;
1827
+ }
1698
1828
  function isCurrentProcessCandidateService() {
1699
1829
  const runtimeMode = String(process.env.OPENMELD_DAEMON_RUNTIME_MODE ?? "").trim();
1700
1830
  return runtimeMode === "background" || runtimeMode === "foreground";
@@ -1771,10 +1901,19 @@ async function readAuthSessionTransitionJournal() {
1771
1901
  }
1772
1902
  function isAuthSessionTransitionJournal(value) {
1773
1903
  if (!isRecord(value)) return false;
1774
- return value.v === 1 && typeof value.id === "string" && value.id.length > 0 && (value.state === "pending" || value.state === "recovery_required" || value.state === "committed") && Number.isSafeInteger(value.ownerPid) && Number(value.ownerPid) > 0 && typeof value.ownerProcessStartedAt === "string" && value.ownerProcessStartedAt.length > 0 && (value.previousPayload === null || isAuthSessionPayload(value.previousPayload)) && isAuthSessionPayload(value.candidatePayload) && isOptionalProcessIdentity(value.candidateServiceProcess) && isOptionalComputerHandoffRecovery(value.computerHandoff);
1904
+ return value.v === 1 && typeof value.id === "string" && value.id.length > 0 && (value.state === "pending" || value.state === "recovery_required" || value.state === "committed") && Number.isSafeInteger(value.ownerPid) && Number(value.ownerPid) > 0 && typeof value.ownerProcessStartedAt === "string" && value.ownerProcessStartedAt.length > 0 && (value.previousPayload === null || isAuthSessionPayload(value.previousPayload)) && isAuthSessionPayload(value.candidatePayload) && (value.candidateCredentialRefreshVersion === void 0 || value.candidateCredentialRefreshVersion === 1) && isOptionalStoredTokenRefList(value.candidateTokenRefs) && isOptionalProcessIdentityList(value.candidateServiceProcesses) && isOptionalProcessIdentity(value.candidateServiceProcess) && isOptionalComputerHandoffRecovery(value.computerHandoff);
1905
+ }
1906
+ function isOptionalStoredTokenRefList(value) {
1907
+ return value === void 0 || Array.isArray(value) && value.length <= MAX_CANDIDATE_TOKEN_REFS && value.every((tokenRef) => isStoredTokenRef(tokenRef));
1908
+ }
1909
+ function isOptionalProcessIdentityList(value) {
1910
+ return value === void 0 || Array.isArray(value) && value.length <= MAX_CANDIDATE_SERVICE_PROCESSES && value.every((candidate) => isProcessIdentity(candidate));
1775
1911
  }
1776
1912
  function isOptionalProcessIdentity(value) {
1777
- return value === void 0 || isRecord(value) && Number.isSafeInteger(value.ownerPid) && Number(value.ownerPid) > 0 && typeof value.ownerProcessStartedAt === "string" && value.ownerProcessStartedAt.length > 0;
1913
+ return value === void 0 || isProcessIdentity(value);
1914
+ }
1915
+ function isProcessIdentity(value) {
1916
+ return isRecord(value) && Number.isSafeInteger(value.ownerPid) && Number(value.ownerPid) > 0 && typeof value.ownerProcessStartedAt === "string" && value.ownerProcessStartedAt.length > 0;
1778
1917
  }
1779
1918
  function isOptionalComputerHandoffRecovery(value) {
1780
1919
  if (value === void 0) return true;
@@ -1958,4 +2097,4 @@ async function removeSessionFile() {
1958
2097
  //#endregion
1959
2098
  export { HEARTBEAT_RESPONSE_TIMEOUT_MS as A, RUNTIME_AGENT_CONTROLLER_REPORT_SYNC_INTERVAL_MS as B, DAEMON_RUNTIME_CONTRACT_EPOCH as C, DEFAULT_GATEWAY_CHAIN_TIMEOUT_MS as D, DAEMON_STREAM_KEEPALIVE_PONG_TIMEOUT_MS as E, OPENCLAW_CLI_TIMEOUT_MS as F, RUNTIME_STATE_PERSIST_MIN_INTERVAL_MS as H, PROVIDER_CONVERSATION_PROOF_SYNC_INTERVAL_MS as I, PROVIDER_CONVERSATION_PROOF_SYNC_RETRY_INTERVAL_MS as L, MAX_SPACE_REQUEST_TIMEOUT_MS as M, MIN_HEARTBEAT_INTERVAL_MS as N, DEFAULT_HEARTBEAT_INTERVAL_MS as O, MIN_SPACE_REQUEST_TIMEOUT_MS as P, REGISTER_RESPONSE_TIMEOUT_MS as R, CATALOG_SYNC_SIGNAL_POLL_INTERVAL_MS as S, DAEMON_STREAM_KEEPALIVE_INTERVAL_MS as T, RUNTIME_AGENT_CONTROLLER_REPORT_SYNC_RETRY_INTERVAL_MS as V, runMacOsNativeKeychainHelperIfRequested as _, beginAuthSessionTransition as a, resolveAuthSessionExpiryEligibility as b, getAuthSessionMetadata as c, getInterruptedComputerAuthSessionRecovery as d, markInterruptedComputerAuthSessionRecovery as f, updateAuthSessionActiveOrganization as g, setAuthSession as h, auth_session_exports as i, MAX_HEARTBEAT_INTERVAL_MS as j, DEFAULT_SPACE_REQUEST_TIMEOUT_MS as k, getAuthSessionState as l, recordInterruptedComputerAuthSessionRecoveryFailure as m, AuthSessionRecoveryInProgressError as n, clearAuthSession as o, normalizeStoredActiveOrganization as p, assertAuthSessionMutationAvailable as r, completeInterruptedComputerAuthSessionRecovery as s, AuthSessionChangedBeforeTransitionError as t, getCurrentAuthOwnerUserId as u, isBinaryDistribution as v, DAEMON_STREAM_CONNECT_TIMEOUT_MS as w, CATALOG_SYNC_RETRY_INTERVAL_MS as x, resolveOpenMeldDistribution as y, ROUTE_CATALOG_LOAD_TIMEOUT_MS as z };
1960
2099
 
1961
- //# sourceMappingURL=auth-session-BbrUVaZd.js.map
2100
+ //# sourceMappingURL=auth-session-Bj8n8YwV.js.map