@fml-inc/fml 0.8.0-rc.1 → 0.8.0-rc.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.
@@ -162,6 +162,10 @@ var FML_LEGACY_OBSERVER_DB_FILE = path.join(
162
162
  "observer.db"
163
163
  );
164
164
  var FML_HOOK_INBOX_DIR = path.join(FML_OBSERVER_DIR, "hook-inbox");
165
+ var FML_HOOK_CONTEXT_STATE_DIR = path.join(
166
+ FML_OBSERVER_DIR,
167
+ "hook-context-state"
168
+ );
165
169
  var FML_HOOK_CAPTURE_DISABLED_FILE = path.join(
166
170
  FML_OBSERVER_DIR,
167
171
  "hook-capture.disabled"
@@ -180,6 +184,10 @@ function ensureDirs() {
180
184
  fs.mkdirSync(FML_LOG_DIR, { recursive: true, mode: 448 });
181
185
  fs.mkdirSync(FML_OBSERVER_DIR, { recursive: true, mode: 448 });
182
186
  fs.mkdirSync(FML_HOOK_INBOX_DIR, { recursive: true, mode: 448 });
187
+ fs.mkdirSync(FML_HOOK_CONTEXT_STATE_DIR, {
188
+ recursive: true,
189
+ mode: 448
190
+ });
183
191
  }
184
192
 
185
193
  // src/config.ts
@@ -540,10 +548,32 @@ var serviceTokenCaches = /* @__PURE__ */ new Map();
540
548
  var serviceRefreshPromises = /* @__PURE__ */ new Map();
541
549
  var oauthRefreshPromises = /* @__PURE__ */ new Map();
542
550
  var cacheKey = (envName) => envName;
543
- function getOrStartTokenRefresh(flights, key, forceRefresh, refresh) {
551
+ function waitForTokenRefresh(promise, signal) {
552
+ if (!signal) return promise;
553
+ if (signal.aborted) return Promise.resolve(null);
554
+ return new Promise((resolve, reject) => {
555
+ const onAbort = () => {
556
+ cleanup();
557
+ resolve(null);
558
+ };
559
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
560
+ signal.addEventListener("abort", onAbort, { once: true });
561
+ void promise.then(
562
+ (value) => {
563
+ cleanup();
564
+ resolve(value);
565
+ },
566
+ (error) => {
567
+ cleanup();
568
+ reject(error);
569
+ }
570
+ );
571
+ });
572
+ }
573
+ function getOrStartTokenRefresh(flights, key, forceRefresh, refresh, signal) {
544
574
  const existing = flights.get(key);
545
575
  if (existing && (!forceRefresh || existing.forceRefresh)) {
546
- return existing.promise;
576
+ return waitForTokenRefresh(existing.promise, signal);
547
577
  }
548
578
  const operation = existing ? existing.promise.then(refresh, refresh) : refresh();
549
579
  let flight;
@@ -552,7 +582,7 @@ function getOrStartTokenRefresh(flights, key, forceRefresh, refresh) {
552
582
  });
553
583
  flight = { forceRefresh, promise };
554
584
  flights.set(key, flight);
555
- return promise;
585
+ return waitForTokenRefresh(promise, signal);
556
586
  }
557
587
  function refreshLockPath(envName) {
558
588
  return `${storePathFor(envName)}.refresh.lock`;
@@ -848,8 +878,30 @@ function releaseOwnedLease(filePath, claimedStat, owner, parseOwner, description
848
878
  );
849
879
  }
850
880
  }
851
- function sleep(milliseconds) {
852
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
881
+ function sleep(milliseconds, signal) {
882
+ if (!signal) {
883
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
884
+ }
885
+ if (signal.aborted) {
886
+ return Promise.reject(
887
+ signal.reason ?? new DOMException("The operation was aborted", "AbortError")
888
+ );
889
+ }
890
+ return new Promise((resolve, reject) => {
891
+ const timeout = setTimeout(() => {
892
+ cleanup();
893
+ resolve();
894
+ }, milliseconds);
895
+ const onAbort = () => {
896
+ clearTimeout(timeout);
897
+ cleanup();
898
+ reject(
899
+ signal.reason ?? new DOMException("The operation was aborted", "AbortError")
900
+ );
901
+ };
902
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
903
+ signal.addEventListener("abort", onAbort, { once: true });
904
+ });
853
905
  }
854
906
  function isProcessAlive(pid) {
855
907
  try {
@@ -998,7 +1050,7 @@ function recoverStaleRefreshLock(lockPath) {
998
1050
  recovery.release();
999
1051
  }
1000
1052
  }
1001
- async function acquireRefreshLock(envName) {
1053
+ async function acquireRefreshLock(envName, signal) {
1002
1054
  try {
1003
1055
  ensureDir(envName);
1004
1056
  } catch (error) {
@@ -1011,6 +1063,9 @@ async function acquireRefreshLock(envName) {
1011
1063
  const recoveryPath = `${lockPath}.recovery`;
1012
1064
  const deadline = Date.now() + OAUTH_REFRESH_LOCK_WAIT_MS;
1013
1065
  while (Date.now() < deadline) {
1066
+ if (signal?.aborted) {
1067
+ throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
1068
+ }
1014
1069
  if (fs3.existsSync(recoveryPath)) {
1015
1070
  try {
1016
1071
  recoverAbandonedRecoveryMarker(recoveryPath);
@@ -1020,7 +1075,7 @@ async function acquireRefreshLock(envName) {
1020
1075
  );
1021
1076
  return null;
1022
1077
  }
1023
- await sleep(OAUTH_REFRESH_LOCK_POLL_MS);
1078
+ await sleep(OAUTH_REFRESH_LOCK_POLL_MS, signal);
1024
1079
  continue;
1025
1080
  }
1026
1081
  let descriptor;
@@ -1041,7 +1096,7 @@ async function acquireRefreshLock(envName) {
1041
1096
  );
1042
1097
  return null;
1043
1098
  }
1044
- await sleep(OAUTH_REFRESH_LOCK_POLL_MS);
1099
+ await sleep(OAUTH_REFRESH_LOCK_POLL_MS, signal);
1045
1100
  continue;
1046
1101
  }
1047
1102
  const ownerId = randomUUID();
@@ -1076,6 +1131,10 @@ async function acquireRefreshLock(envName) {
1076
1131
  parseRefreshLockOwner,
1077
1132
  "credential lock"
1078
1133
  );
1134
+ if (signal?.aborted) {
1135
+ releaseClaim();
1136
+ throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
1137
+ }
1079
1138
  let entryBlocked;
1080
1139
  try {
1081
1140
  entryBlocked = fs3.existsSync(recoveryPath) || !isOwned();
@@ -1090,13 +1149,16 @@ async function acquireRefreshLock(envName) {
1090
1149
  return null;
1091
1150
  }
1092
1151
  if (entryBlocked) {
1093
- await sleep(OAUTH_REFRESH_LOCK_POLL_MS);
1094
1152
  try {
1095
- if (isOwned()) releaseClaim();
1096
- } catch (error) {
1097
- console.error(
1098
- `[fml] Auth: unable to release a blocked refresh lock claim: ${describeError(error)}`
1099
- );
1153
+ await sleep(OAUTH_REFRESH_LOCK_POLL_MS, signal);
1154
+ } finally {
1155
+ try {
1156
+ if (isOwned()) releaseClaim();
1157
+ } catch (error) {
1158
+ console.error(
1159
+ `[fml] Auth: unable to release a blocked refresh lock claim: ${describeError(error)}`
1160
+ );
1161
+ }
1100
1162
  }
1101
1163
  continue;
1102
1164
  }
@@ -1142,7 +1204,7 @@ function isLockOwnedSafely(lock) {
1142
1204
  }
1143
1205
  }
1144
1206
  async function withRefreshLock(envName, mutation, options) {
1145
- const lock = await acquireRefreshLock(envName);
1207
+ const lock = await acquireRefreshLock(envName, options?.signal);
1146
1208
  if (!lock) {
1147
1209
  throw new Error("Unable to acquire the credential mutation lock");
1148
1210
  }
@@ -1181,9 +1243,13 @@ async function clearStoredCredentials(envName) {
1181
1243
  serviceRefreshPromises.delete(key);
1182
1244
  oauthRefreshPromises.delete(key);
1183
1245
  }
1184
- async function refreshServiceToken(refreshToken2, envName, siteUrl) {
1246
+ async function refreshServiceToken(refreshToken2, envName, siteUrl, signal) {
1185
1247
  try {
1248
+ if (signal?.aborted) return null;
1186
1249
  const convexUrl = (envName ? resolveEnvConvexUrl(envName) : null) ?? CONVEX_URL;
1250
+ const requestTimeout = AbortSignal.timeout(
1251
+ OAUTH_REFRESH_REQUEST_TIMEOUT_MS
1252
+ );
1187
1253
  const response = await fetch(
1188
1254
  `${siteUrl ?? getSiteUrlForConvexUrl(convexUrl)}/api/tokens/refresh`,
1189
1255
  {
@@ -1192,7 +1258,7 @@ async function refreshServiceToken(refreshToken2, envName, siteUrl) {
1192
1258
  "Content-Type": "application/json",
1193
1259
  Authorization: `Bearer ${refreshToken2}`
1194
1260
  },
1195
- signal: AbortSignal.timeout(OAUTH_REFRESH_REQUEST_TIMEOUT_MS)
1261
+ signal: signal ? AbortSignal.any([signal, requestTimeout]) : requestTimeout
1196
1262
  }
1197
1263
  );
1198
1264
  if (!response.ok) {
@@ -1212,6 +1278,7 @@ async function refreshServiceToken(refreshToken2, envName, siteUrl) {
1212
1278
  });
1213
1279
  return data.accessToken;
1214
1280
  } catch (err) {
1281
+ if (signal?.aborted) return null;
1215
1282
  Sentry.captureException(err);
1216
1283
  console.error(`[fml] Service token refresh error: ${err}`);
1217
1284
  return null;
@@ -1248,6 +1315,7 @@ async function storeServiceRefreshToken(refreshToken2, opts) {
1248
1315
  return true;
1249
1316
  }
1250
1317
  async function getValidToken(opts) {
1318
+ if (opts?.signal?.aborted) return null;
1251
1319
  const envName = resolveEnvName(opts?.env);
1252
1320
  const forceRefresh = opts?.forceRefresh === true;
1253
1321
  const key = cacheKey(envName);
@@ -1261,7 +1329,8 @@ async function getValidToken(opts) {
1261
1329
  serviceRefreshPromises,
1262
1330
  key,
1263
1331
  forceRefresh,
1264
- () => refreshServiceToken(envToken, envName)
1332
+ () => refreshServiceToken(envToken, envName, void 0, opts?.signal),
1333
+ opts?.signal
1265
1334
  );
1266
1335
  }
1267
1336
  if (envToken?.startsWith("fml_st_")) return envToken;
@@ -1286,39 +1355,47 @@ async function getValidToken(opts) {
1286
1355
  serviceRefreshPromises,
1287
1356
  key,
1288
1357
  forceRefresh,
1289
- () => withRefreshLock(envName, async (lock) => {
1290
- const current = readTokens(envName);
1291
- if (!current) return null;
1292
- if (!forceRefresh && current.expiresAt > Date.now() + TOKEN_REFRESH_WINDOW_MS) {
1293
- return current.accessToken;
1294
- }
1295
- if (current.tokenType !== "service") {
1296
- return current.accessToken;
1297
- }
1298
- if (!lock.isOwned()) return null;
1299
- const newAccessToken = await refreshServiceToken(
1300
- current.refreshToken,
1301
- envName
1302
- );
1303
- if (!lock.isOwned()) return null;
1304
- const cached = serviceTokenCaches.get(key);
1305
- if (newAccessToken && cached) {
1306
- writeTokensUnlocked(
1307
- {
1308
- ...current,
1309
- accessToken: newAccessToken,
1310
- expiresAt: cached.expiresAt
1311
- },
1312
- envName
1358
+ () => withRefreshLock(
1359
+ envName,
1360
+ async (lock) => {
1361
+ const current = readTokens(envName);
1362
+ if (!current) return null;
1363
+ if (!forceRefresh && current.expiresAt > Date.now() + TOKEN_REFRESH_WINDOW_MS) {
1364
+ return current.accessToken;
1365
+ }
1366
+ if (current.tokenType !== "service") {
1367
+ return current.accessToken;
1368
+ }
1369
+ if (!lock.isOwned()) return null;
1370
+ const newAccessToken = await refreshServiceToken(
1371
+ current.refreshToken,
1372
+ envName,
1373
+ void 0,
1374
+ opts?.signal
1313
1375
  );
1314
- }
1315
- return newAccessToken;
1316
- }).catch((error) => {
1376
+ if (!lock.isOwned()) return null;
1377
+ const cached = serviceTokenCaches.get(key);
1378
+ if (newAccessToken && cached) {
1379
+ writeTokensUnlocked(
1380
+ {
1381
+ ...current,
1382
+ accessToken: newAccessToken,
1383
+ expiresAt: cached.expiresAt
1384
+ },
1385
+ envName
1386
+ );
1387
+ }
1388
+ return newAccessToken;
1389
+ },
1390
+ { signal: opts?.signal }
1391
+ ).catch((error) => {
1392
+ if (opts?.signal?.aborted) return null;
1317
1393
  console.error(
1318
1394
  `[fml] Auth: unable to refresh stored service credentials: ${error instanceof Error ? error.message : String(error)}`
1319
1395
  );
1320
1396
  return null;
1321
- })
1397
+ }),
1398
+ opts?.signal
1322
1399
  );
1323
1400
  }
1324
1401
  if (stored.refreshInvalidAt) {
@@ -1331,11 +1408,12 @@ async function getValidToken(opts) {
1331
1408
  oauthRefreshPromises,
1332
1409
  key,
1333
1410
  forceRefresh,
1334
- () => refreshTokenWithLock(envName, forceRefresh)
1411
+ () => refreshTokenWithLock(envName, forceRefresh, opts?.signal),
1412
+ opts?.signal
1335
1413
  );
1336
1414
  }
1337
- async function refreshTokenWithLock(envName, forceRefresh = false) {
1338
- const lock = await acquireRefreshLock(envName);
1415
+ async function refreshTokenWithLock(envName, forceRefresh = false, signal) {
1416
+ const lock = await acquireRefreshLock(envName, signal);
1339
1417
  if (!lock) return null;
1340
1418
  try {
1341
1419
  if (!isLockOwnedSafely(lock)) return null;
@@ -1366,12 +1444,12 @@ async function refreshTokenWithLock(envName, forceRefresh = false) {
1366
1444
  return null;
1367
1445
  }
1368
1446
  if (!isLockOwnedSafely(lock)) return null;
1369
- return await refreshToken(current, lock, envName);
1447
+ return await refreshToken(current, lock, envName, signal);
1370
1448
  } finally {
1371
1449
  lock.release();
1372
1450
  }
1373
1451
  }
1374
- async function refreshToken(stored, lock, envName) {
1452
+ async function refreshToken(stored, lock, envName, signal) {
1375
1453
  if (!stored.workosClientId) {
1376
1454
  console.error(
1377
1455
  "[fml] Auth: token expired, missing workosClientId \u2014 run `fml login`"
@@ -1379,7 +1457,11 @@ async function refreshToken(stored, lock, envName) {
1379
1457
  return null;
1380
1458
  }
1381
1459
  try {
1460
+ if (signal?.aborted) return null;
1382
1461
  if (!lock.isOwned()) return null;
1462
+ const requestTimeout = AbortSignal.timeout(
1463
+ OAUTH_REFRESH_REQUEST_TIMEOUT_MS
1464
+ );
1383
1465
  const response = await fetch(
1384
1466
  `${WORKOS_API_URL}/user_management/authenticate`,
1385
1467
  {
@@ -1390,7 +1472,7 @@ async function refreshToken(stored, lock, envName) {
1390
1472
  client_id: stored.workosClientId,
1391
1473
  refresh_token: stored.refreshToken
1392
1474
  }),
1393
- signal: AbortSignal.timeout(OAUTH_REFRESH_REQUEST_TIMEOUT_MS)
1475
+ signal: signal ? AbortSignal.any([signal, requestTimeout]) : requestTimeout
1394
1476
  }
1395
1477
  );
1396
1478
  if (!response.ok) {
@@ -1460,6 +1542,7 @@ async function refreshToken(stored, lock, envName) {
1460
1542
  }
1461
1543
  return refreshed.accessToken;
1462
1544
  } catch (err) {
1545
+ if (signal?.aborted) return null;
1463
1546
  const msg = err instanceof Error ? err.message : String(err);
1464
1547
  console.error(
1465
1548
  `[fml] Auth: refresh failed (network error: ${msg}) \u2014 token preserved`
@@ -1490,6 +1573,7 @@ export {
1490
1573
  FML_PANOPTICON_DB_OBSERVER_LOCK_FILE,
1491
1574
  FML_LEGACY_OBSERVER_DB_FILE,
1492
1575
  FML_HOOK_INBOX_DIR,
1576
+ FML_HOOK_CONTEXT_STATE_DIR,
1493
1577
  FML_HOOK_CAPTURE_DISABLED_FILE,
1494
1578
  observerOnlyOverrideWarning,
1495
1579
  ensureDirs,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  resolveRepoFromCwdAsync
3
- } from "./chunk-T5YVLSUC.js";
3
+ } from "./chunk-7V7G7NVF.js";
4
4
  import {
5
5
  getActiveEnv,
6
6
  getSiteUrlForConvexUrl,
7
7
  getSyncDestinationIdentity,
8
8
  getValidToken
9
- } from "./chunk-7DR5X7XI.js";
9
+ } from "./chunk-A6HAEVJX.js";
10
10
 
11
11
  // src/hooks/sync-loop.ts
12
12
  import { createHash } from "crypto";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  FML_HOOK_CAPTURE_DISABLED_FILE
3
- } from "./chunk-7DR5X7XI.js";
3
+ } from "./chunk-A6HAEVJX.js";
4
4
 
5
5
  // src/json-file.ts
6
6
  import { randomUUID } from "crypto";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  resolveRepoFromCwd
3
- } from "./chunk-T5YVLSUC.js";
3
+ } from "./chunk-7V7G7NVF.js";
4
4
  import {
5
5
  CONVEX_URL,
6
6
  getActiveEnv,
7
7
  getSelectedOrg,
8
8
  getSiteUrlForConvexUrl,
9
9
  getValidToken
10
- } from "./chunk-7DR5X7XI.js";
10
+ } from "./chunk-A6HAEVJX.js";
11
11
 
12
12
  // src/fml-client.ts
13
13
  var LOGIN_EXPIRED_MESSAGE = "Authentication expired. Run `fml login` to sign in again, then restart Claude Code.";
@@ -79,7 +79,7 @@ function createFmlClient(token, options = {}) {
79
79
  const repo = resolveRepoFromCwd(process.cwd());
80
80
  if (repo) body.repo = repo.repo;
81
81
  if (isServiceToken) {
82
- const { readTokens, SERVICE_TOKEN_LOGIN_USER_ID } = await import("./token-store-R7WXEPLI.js");
82
+ const { readTokens, SERVICE_TOKEN_LOGIN_USER_ID } = await import("./token-store-BT5THIAJ.js");
83
83
  const stored = readTokens(options.envName);
84
84
  const storedUserExternalId = stored?.tokenType === "service" && stored.user.id !== SERVICE_TOKEN_LOGIN_USER_ID ? stored.user.id : void 0;
85
85
  const userExternalId = process.env.FML_USER_EXTERNAL_ID ?? storedUserExternalId;
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  OBSERVER_SERVICE_NAME
3
- } from "./chunk-DEMVOVVY.js";
3
+ } from "./chunk-56TPYMNK.js";
4
4
  import {
5
5
  FML_OBSERVER_LOCK_FILE,
6
6
  FML_OBSERVER_PORT,
7
7
  FML_OBSERVER_STATE_FILE
8
- } from "./chunk-7DR5X7XI.js";
8
+ } from "./chunk-A6HAEVJX.js";
9
9
 
10
10
  // src/observer/state.ts
11
11
  import { randomUUID } from "crypto";
@@ -6,7 +6,7 @@ import {
6
6
  getSiteUrlForConvexUrl,
7
7
  getSyncDestinationIdentity,
8
8
  getValidToken
9
- } from "./chunk-7DR5X7XI.js";
9
+ } from "./chunk-A6HAEVJX.js";
10
10
 
11
11
  // ../../packages/panopticon/src/artifact.ts
12
12
  import {