@fml-inc/fml 0.8.0-rc.1 → 0.8.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +29 -16
- package/dist/{chunk-CK5MEL5J.js → chunk-3KISRT57.js} +1 -1
- package/dist/{chunk-HUR2HCUG.js → chunk-7DWFJENY.js} +2 -2
- package/dist/{chunk-T5YVLSUC.js → chunk-7V7G7NVF.js} +4 -0
- package/dist/{chunk-7DR5X7XI.js → chunk-A6HAEVJX.js} +136 -52
- package/dist/{chunk-F66IBM3Z.js → chunk-DZKYW327.js} +2 -2
- package/dist/{chunk-I4GZPTBM.js → chunk-ENW2GJNO.js} +1 -1
- package/dist/{chunk-DEMVOVVY.js → chunk-KBT5RMAU.js} +263 -32
- package/dist/{chunk-P3QLARXE.js → chunk-R45CWWZO.js} +3 -3
- package/dist/{chunk-JXSSPQ5B.js → chunk-YSU5CS2V.js} +1 -1
- package/dist/cli.js +1708 -1351
- package/dist/hook-handler.js +299 -42
- package/dist/mcp/server.js +7 -7
- package/dist/observer-server.js +8 -8
- package/dist/pi/extension.js +350 -0
- package/dist/sync-cycle.js +3 -3
- package/dist/{token-store-R7WXEPLI.js → token-store-BT5THIAJ.js} +1 -1
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -10,25 +10,34 @@ not install or run a second Panopticon package.
|
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
|
-
Node.js 22.15 or newer and
|
|
13
|
+
Node.js 22.15 or newer and npm are required.
|
|
14
|
+
|
|
15
|
+
On macOS, Linux, and compatible Bash environments, install with:
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
|
-
|
|
17
|
-
fml install
|
|
18
|
-
fml login
|
|
19
|
-
fml org
|
|
20
|
-
fml start
|
|
18
|
+
curl -fsSL https://fml.inc/install.sh | bash
|
|
21
19
|
```
|
|
22
20
|
|
|
21
|
+
For Windows or manual installation, run:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install --global @fml-inc/fml
|
|
25
|
+
fml setup
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Run `fml update` to upgrade the global package with npm. Stable releases stay
|
|
29
|
+
on npm's `latest` channel; prerelease versions stay on `next`.
|
|
30
|
+
|
|
23
31
|
Installing the package does not change agent configuration. Run `fml install`
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
to register the FML Claude Code plugin, install the agent-native FML skill,
|
|
33
|
+
configure the FML MCP server, register capture-only lifecycle hooks for
|
|
34
|
+
supported agents, and install the FML extension for Pi. `fml setup` performs
|
|
35
|
+
this installation, signs in when needed, and restarts the observer. Invoke the
|
|
36
|
+
skill as `/fml review` in Claude, Gemini, or Grok, and as `$fml review` in
|
|
37
|
+
Codex. Review runs inside the agent from the packaged skill; it is not an
|
|
38
|
+
`fml review` CLI command. Start a new agent session after installing so it
|
|
39
|
+
picks up the integration. FML pre-approves only the isolated command hooks it
|
|
40
|
+
installs for Codex; use `/hooks` to review or disable them.
|
|
32
41
|
|
|
33
42
|
The Claude plugin is MCP-only. FML installs the workflow through each host's
|
|
34
43
|
native skill directory so it keeps the bare `fml` name without a second,
|
|
@@ -59,7 +68,8 @@ Windows: `%APPDATA%\panopticon`), and remove any leftover `panopticon` entries
|
|
|
59
68
|
from the Claude plugin registration (`~/.claude/settings.json` and
|
|
60
69
|
`~/.claude/plugins/`), agent hook configs (`~/.codex/config.toml`,
|
|
61
70
|
`~/.codex/hooks.json`, `~/.gemini/settings.json`, `~/.grok/hooks/`), and MCP
|
|
62
|
-
server registrations in those same agent config files.
|
|
71
|
+
server registrations in those same agent config files. Also remove the legacy
|
|
72
|
+
Pi extension at `~/.pi/agent/extensions/panopticon.js`.
|
|
63
73
|
|
|
64
74
|
## Raw artifact sync
|
|
65
75
|
|
|
@@ -84,6 +94,7 @@ fml sync disable # Stop discovery and upload
|
|
|
84
94
|
fml sync enable # Allow discovery and upload again
|
|
85
95
|
fml sync reset # Clear watermarks and replay every local artifact from byte zero
|
|
86
96
|
fml start # Start the observer and continuously run enabled raw sync
|
|
97
|
+
fml restart # Stop and start the observer
|
|
87
98
|
fml stop # Stop the observer and disable raw sync
|
|
88
99
|
```
|
|
89
100
|
|
|
@@ -262,11 +273,13 @@ fml tools call <tool-name> --args '{"key":"value"}'
|
|
|
262
273
|
## CLI overview
|
|
263
274
|
|
|
264
275
|
```text
|
|
276
|
+
fml setup Install integrations, sign in if needed, and start FML
|
|
265
277
|
fml install Register FML agent integrations and the MCP server
|
|
266
278
|
fml uninstall Remove agent integration; add --purge to remove local state
|
|
267
|
-
fml update Update @fml-inc/fml with
|
|
279
|
+
fml update Update @fml-inc/fml with npm (stable: latest; prerelease: next)
|
|
268
280
|
|
|
269
281
|
fml start Start the local observer and continuous raw sync
|
|
282
|
+
fml restart Stop and start the local observer
|
|
270
283
|
fml stop Stop the local observer and disable raw sync
|
|
271
284
|
|
|
272
285
|
fml login Sign in to FML
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
OBSERVER_SERVICE_NAME
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KBT5RMAU.js";
|
|
4
4
|
import {
|
|
5
5
|
FML_OBSERVER_LOCK_FILE,
|
|
6
6
|
FML_OBSERVER_PORT,
|
|
7
7
|
FML_OBSERVER_STATE_FILE
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-A6HAEVJX.js";
|
|
9
9
|
|
|
10
10
|
// src/observer/state.ts
|
|
11
11
|
import { randomUUID } from "crypto";
|
|
@@ -99,6 +99,9 @@ function resolveRepositorySlugFromCwd(cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
|
99
99
|
const remote = git(cwd, ["remote", "get-url", "origin"], timeoutMs);
|
|
100
100
|
return remote ? repositorySlug(remote) : null;
|
|
101
101
|
}
|
|
102
|
+
function resolveRepositoryRootFromCwd(cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
103
|
+
return git(cwd, ["rev-parse", "--show-toplevel"], timeoutMs);
|
|
104
|
+
}
|
|
102
105
|
async function resolveRepoFromCwdAsync(cwd, signal) {
|
|
103
106
|
if (signal?.aborted) {
|
|
104
107
|
throw signal.reason ?? new Error("Repository lookup aborted");
|
|
@@ -134,5 +137,6 @@ async function resolveRepoFromCwdAsync(cwd, signal) {
|
|
|
134
137
|
export {
|
|
135
138
|
resolveRepoFromCwd,
|
|
136
139
|
resolveRepositorySlugFromCwd,
|
|
140
|
+
resolveRepositoryRootFromCwd,
|
|
137
141
|
resolveRepoFromCwdAsync
|
|
138
142
|
};
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
|
|
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.
|
|
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(
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
return
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
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
|
-
|
|
1316
|
-
|
|
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.
|
|
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-
|
|
3
|
+
} from "./chunk-7V7G7NVF.js";
|
|
4
4
|
import {
|
|
5
5
|
getActiveEnv,
|
|
6
6
|
getSiteUrlForConvexUrl,
|
|
7
7
|
getSyncDestinationIdentity,
|
|
8
8
|
getValidToken
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-A6HAEVJX.js";
|
|
10
10
|
|
|
11
11
|
// src/hooks/sync-loop.ts
|
|
12
12
|
import { createHash } from "crypto";
|