@fml-inc/fml 0.7.0 → 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.
Files changed (45) hide show
  1. package/.claude-plugin/plugin.json +2 -4
  2. package/README.md +323 -79
  3. package/agent-assets/fml/SKILL.md +180 -0
  4. package/bin/fml +4 -18
  5. package/bin/hook-handler +7 -0
  6. package/bin/mcp-server +4 -18
  7. package/bin/observer +6 -0
  8. package/dist/chunk-3KISRT57.js +140 -0
  9. package/dist/chunk-56TPYMNK.js +4657 -0
  10. package/dist/chunk-7V7G7NVF.js +142 -0
  11. package/dist/chunk-A6HAEVJX.js +1606 -0
  12. package/dist/chunk-DZKYW327.js +1037 -0
  13. package/dist/chunk-ENW2GJNO.js +109 -0
  14. package/dist/{chunk-PZ5AY32C.js → chunk-MLKGABMK.js} +0 -1
  15. package/dist/{chunk-PTUZMKSN.js → chunk-R45CWWZO.js} +34 -23
  16. package/dist/chunk-WZLLO563.js +953 -0
  17. package/dist/chunk-YSU5CS2V.js +3331 -0
  18. package/dist/cli.js +7742 -1072
  19. package/dist/{esm-2EUDFZWP.js → esm-4YKLKFRX.js} +546 -741
  20. package/dist/hook-handler.js +440 -0
  21. package/dist/mcp/server.js +112 -98
  22. package/dist/observer-server.js +1566 -0
  23. package/dist/sync-cycle.js +31 -0
  24. package/dist/{token-store-3IQSLOTQ.js → token-store-BT5THIAJ.js} +10 -3
  25. package/hooks/hooks.json +290 -0
  26. package/package.json +24 -27
  27. package/dist/chunk-PTUZMKSN.js.map +0 -1
  28. package/dist/chunk-PZ5AY32C.js.map +0 -1
  29. package/dist/chunk-S4TJNLEN.js +0 -61
  30. package/dist/chunk-S4TJNLEN.js.map +0 -1
  31. package/dist/chunk-VV4JHH4H.js +0 -437
  32. package/dist/chunk-VV4JHH4H.js.map +0 -1
  33. package/dist/chunk-YH5ROR7G.js +0 -69
  34. package/dist/chunk-YH5ROR7G.js.map +0 -1
  35. package/dist/cli.js.map +0 -1
  36. package/dist/daemon-MSLDHLMH.js +0 -21
  37. package/dist/daemon-MSLDHLMH.js.map +0 -1
  38. package/dist/daemon-utils-OODBIYB7.js +0 -10
  39. package/dist/daemon-utils-OODBIYB7.js.map +0 -1
  40. package/dist/esm-2EUDFZWP.js.map +0 -1
  41. package/dist/mcp/server.js.map +0 -1
  42. package/dist/postinstall.js +0 -16
  43. package/dist/postinstall.js.map +0 -1
  44. package/dist/token-store-3IQSLOTQ.js.map +0 -1
  45. package/scripts/postinstall.cjs +0 -25
@@ -0,0 +1,1606 @@
1
+ // src/auth/token-store.ts
2
+ import { createHash, randomUUID } from "crypto";
3
+ import { execFileSync } from "child_process";
4
+ import fs3 from "fs";
5
+ import path3 from "path";
6
+
7
+ // src/config.ts
8
+ import fs2 from "fs";
9
+ import path2 from "path";
10
+
11
+ // src/dirs.ts
12
+ import fs from "fs";
13
+ import os from "os";
14
+ import path from "path";
15
+ function isPresent(value) {
16
+ return value !== void 0 && value.trim().length > 0;
17
+ }
18
+ function normalizeAbsolutePath(value, variableName, pathApi) {
19
+ if (!pathApi.isAbsolute(value)) {
20
+ throw new Error(`${variableName} must be an absolute path: ${value}`);
21
+ }
22
+ return pathApi.normalize(value);
23
+ }
24
+ function explicitDirectory(value, variableName, pathApi) {
25
+ if (!isPresent(value)) return void 0;
26
+ return normalizeAbsolutePath(value, variableName, pathApi);
27
+ }
28
+ function platformBaseDirectory(value, pathApi) {
29
+ if (!isPresent(value) || !pathApi.isAbsolute(value)) return void 0;
30
+ return pathApi.normalize(value);
31
+ }
32
+ function resolveFmlDirectories(options = {}) {
33
+ const platform = options.platform ?? process.platform;
34
+ const env = options.env ?? process.env;
35
+ const pathApi = platform === "win32" ? path.win32 : path.posix;
36
+ const homeDir = normalizeAbsolutePath(
37
+ options.homeDir ?? os.homedir(),
38
+ "home directory",
39
+ pathApi
40
+ );
41
+ const explicitDataDir = explicitDirectory(
42
+ env.FML_DATA_DIR,
43
+ "FML_DATA_DIR",
44
+ pathApi
45
+ );
46
+ const explicitStateDir = explicitDirectory(
47
+ env.FML_STATE_DIR,
48
+ "FML_STATE_DIR",
49
+ pathApi
50
+ );
51
+ const explicitLogDir = explicitDirectory(
52
+ env.FML_LOG_DIR,
53
+ "FML_LOG_DIR",
54
+ pathApi
55
+ );
56
+ let defaultDataDir;
57
+ let defaultStateDir;
58
+ let nativeLogDir;
59
+ switch (platform) {
60
+ case "darwin": {
61
+ defaultDataDir = pathApi.join(
62
+ homeDir,
63
+ "Library",
64
+ "Application Support",
65
+ "fml"
66
+ );
67
+ defaultStateDir = defaultDataDir;
68
+ nativeLogDir = pathApi.join(homeDir, "Library", "Logs", "fml");
69
+ break;
70
+ }
71
+ case "win32": {
72
+ const roamingAppData = platformBaseDirectory(env.APPDATA, pathApi) ?? pathApi.join(homeDir, "AppData", "Roaming");
73
+ const localAppData = platformBaseDirectory(env.LOCALAPPDATA, pathApi) ?? pathApi.join(homeDir, "AppData", "Local");
74
+ defaultDataDir = pathApi.join(roamingAppData, "fml");
75
+ defaultStateDir = pathApi.join(localAppData, "fml");
76
+ break;
77
+ }
78
+ default: {
79
+ const xdgDataHome = platformBaseDirectory(env.XDG_DATA_HOME, pathApi) ?? pathApi.join(homeDir, ".local", "share");
80
+ const xdgStateHome = platformBaseDirectory(env.XDG_STATE_HOME, pathApi) ?? pathApi.join(homeDir, ".local", "state");
81
+ defaultDataDir = pathApi.join(xdgDataHome, "fml");
82
+ defaultStateDir = pathApi.join(xdgStateHome, "fml");
83
+ break;
84
+ }
85
+ }
86
+ const dataDir = explicitDataDir ?? defaultDataDir;
87
+ const stateDir = explicitStateDir ?? explicitDataDir ?? defaultStateDir;
88
+ const defaultLogDir = nativeLogDir ?? (platform === "win32" ? pathApi.join(defaultDataDir, "logs") : pathApi.join(defaultStateDir, "log"));
89
+ const explicitObserverDir = explicitDirectory(
90
+ env.FML_OBSERVER_DIR,
91
+ "FML_OBSERVER_DIR",
92
+ pathApi
93
+ );
94
+ const legacyPanopticonHome = explicitDirectory(
95
+ env.PANOPTICON_HOME,
96
+ "PANOPTICON_HOME",
97
+ pathApi
98
+ );
99
+ const fmlHome = explicitDirectory(env.FML_HOME, "FML_HOME", pathApi);
100
+ return {
101
+ dataDir,
102
+ stateDir,
103
+ logDir: explicitLogDir ?? defaultLogDir,
104
+ observerDir: explicitObserverDir ?? legacyPanopticonHome ?? (fmlHome ? pathApi.join(fmlHome, "panopticon") : pathApi.join(homeDir, ".fml", "panopticon"))
105
+ };
106
+ }
107
+ function resolveFmlDefaultDirectories(options = {}) {
108
+ const env = options.env ?? process.env;
109
+ return resolveFmlDirectories({
110
+ ...options,
111
+ env: {
112
+ APPDATA: env.APPDATA,
113
+ LOCALAPPDATA: env.LOCALAPPDATA,
114
+ XDG_DATA_HOME: env.XDG_DATA_HOME,
115
+ XDG_STATE_HOME: env.XDG_STATE_HOME
116
+ }
117
+ });
118
+ }
119
+ function observerPort() {
120
+ const configured = process.env.FML_OBSERVER_PORT ?? process.env.PANOPTICON_PORT;
121
+ if (configured) {
122
+ const parsed = Number.parseInt(configured, 10);
123
+ if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
124
+ return parsed;
125
+ }
126
+ }
127
+ return 4318 + (process.getuid?.() ?? 0) % 100;
128
+ }
129
+ var DIRECTORIES = resolveFmlDirectories();
130
+ var FML_DATA_DIR = DIRECTORIES.dataDir;
131
+ var FML_STATE_DIR = DIRECTORIES.stateDir;
132
+ var FML_LOG_DIR = DIRECTORIES.logDir;
133
+ var FML_OBSERVER_DIR = DIRECTORIES.observerDir;
134
+ var FML_OBSERVER_HOST = "127.0.0.1";
135
+ var FML_OBSERVER_PORT = observerPort();
136
+ var FML_OBSERVER_STATE_FILE = path.join(
137
+ FML_OBSERVER_DIR,
138
+ "observer.pid.json"
139
+ );
140
+ var FML_OBSERVER_LOCK_FILE = path.join(
141
+ FML_OBSERVER_DIR,
142
+ "observer.lock"
143
+ );
144
+ var FML_OBSERVER_AUTH_TOKEN_FILE = path.join(
145
+ FML_OBSERVER_DIR,
146
+ "auth-token"
147
+ );
148
+ var FML_OBSERVER_ENDPOINT_FILE = path.join(
149
+ FML_OBSERVER_DIR,
150
+ "endpoint.json"
151
+ );
152
+ var FML_OBSERVER_LOG_FILE = path.join(FML_LOG_DIR, "observer.log");
153
+ var FML_INSTALLATION_ID_FILE = path.join(
154
+ FML_STATE_DIR,
155
+ "installation_id"
156
+ );
157
+ var FML_PANOPTICON_DB_FILE = path.join(FML_STATE_DIR, "panopticon.db");
158
+ var FML_PANOPTICON_DB_OBSERVER_STATE_FILE = `${FML_PANOPTICON_DB_FILE}.observer.pid.json`;
159
+ var FML_PANOPTICON_DB_OBSERVER_LOCK_FILE = `${FML_PANOPTICON_DB_FILE}.observer.lock`;
160
+ var FML_LEGACY_OBSERVER_DB_FILE = path.join(
161
+ FML_OBSERVER_DIR,
162
+ "observer.db"
163
+ );
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
+ );
169
+ var FML_HOOK_CAPTURE_DISABLED_FILE = path.join(
170
+ FML_OBSERVER_DIR,
171
+ "hook-capture.disabled"
172
+ );
173
+ function observerOnlyOverrideWarning(env = process.env, databaseFile = FML_PANOPTICON_DB_FILE) {
174
+ if (isPresent(env.FML_STATE_DIR) || isPresent(env.FML_DATA_DIR)) {
175
+ return null;
176
+ }
177
+ const observerAlias = ["FML_OBSERVER_DIR", "PANOPTICON_HOME", "FML_HOME"].find((variableName) => isPresent(env[variableName]));
178
+ if (!observerAlias) return null;
179
+ return `${observerAlias} relocates only observer coordination state; the FML database stays at ${databaseFile}. Set FML_STATE_DIR (or FML_DATA_DIR, including its platform default) to choose the database root explicitly.`;
180
+ }
181
+ function ensureDirs() {
182
+ fs.mkdirSync(FML_DATA_DIR, { recursive: true, mode: 448 });
183
+ fs.mkdirSync(FML_STATE_DIR, { recursive: true, mode: 448 });
184
+ fs.mkdirSync(FML_LOG_DIR, { recursive: true, mode: 448 });
185
+ fs.mkdirSync(FML_OBSERVER_DIR, { recursive: true, mode: 448 });
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
+ });
191
+ }
192
+
193
+ // src/config.ts
194
+ var DEFAULT_PROD_URL = "https://trustworthy-chihuahua-382.convex.cloud";
195
+ var DEFAULT_SYNC_URL = DEFAULT_PROD_URL.replace(
196
+ ".convex.cloud",
197
+ ".convex.site"
198
+ );
199
+ var DEFAULT_TARGET_NAME = "fml";
200
+ var ENV_CONFIG_PATH = path2.join(FML_DATA_DIR, "env.json");
201
+ var ENV_NAME_RE = /^[A-Za-z0-9_-]+$/;
202
+ function isValidEnvName(envName) {
203
+ return ENV_NAME_RE.test(envName);
204
+ }
205
+ function normalizeConvexUrl(value) {
206
+ const url = new URL(value);
207
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
208
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.search || url.hash || url.pathname !== "" && url.pathname !== "/") {
209
+ throw new Error(
210
+ "FML environment URL must be HTTPS (HTTP is allowed only for loopback) with no path, credentials, query, or fragment"
211
+ );
212
+ }
213
+ return value.replace(/\/$/, "").replace(".convex.site", ".convex.cloud");
214
+ }
215
+ function readEnvConfig() {
216
+ try {
217
+ const parsed = JSON.parse(
218
+ fs2.readFileSync(ENV_CONFIG_PATH, "utf8")
219
+ );
220
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
221
+ throw new Error("invalid environment config");
222
+ }
223
+ const config = parsed;
224
+ if (typeof config.active !== "string" || !isValidEnvName(config.active)) {
225
+ throw new Error("invalid active environment");
226
+ }
227
+ const environments = {};
228
+ if (config.environments && typeof config.environments === "object" && !Array.isArray(config.environments)) {
229
+ for (const [name, environment] of Object.entries(
230
+ config.environments
231
+ )) {
232
+ if (!isValidEnvName(name) || !environment || typeof environment !== "object" || Array.isArray(environment)) {
233
+ continue;
234
+ }
235
+ const convexUrl = environment.convexUrl;
236
+ if (typeof convexUrl !== "string") continue;
237
+ try {
238
+ environments[name] = { convexUrl: normalizeConvexUrl(convexUrl) };
239
+ } catch {
240
+ }
241
+ }
242
+ }
243
+ return {
244
+ schemaVersion: 1,
245
+ active: config.active,
246
+ environments
247
+ };
248
+ } catch {
249
+ return {
250
+ schemaVersion: 1,
251
+ active: DEFAULT_TARGET_NAME,
252
+ environments: {
253
+ [DEFAULT_TARGET_NAME]: { convexUrl: DEFAULT_PROD_URL }
254
+ }
255
+ };
256
+ }
257
+ }
258
+ function writeEnvConfig(config) {
259
+ const dir = path2.dirname(ENV_CONFIG_PATH);
260
+ fs2.mkdirSync(dir, { recursive: true, mode: 448 });
261
+ fs2.writeFileSync(
262
+ ENV_CONFIG_PATH,
263
+ `${JSON.stringify({ ...config, schemaVersion: 1 }, null, 2)}
264
+ `,
265
+ { mode: 384 }
266
+ );
267
+ }
268
+ function envConfigExists() {
269
+ return fs2.existsSync(ENV_CONFIG_PATH);
270
+ }
271
+ function resolveEnvConvexUrl(envName) {
272
+ const configured = readEnvConfig().environments?.[envName]?.convexUrl;
273
+ if (configured) return configured;
274
+ if (envName === DEFAULT_TARGET_NAME) return DEFAULT_PROD_URL;
275
+ return null;
276
+ }
277
+ function setActiveEnvironment(name, convexUrl) {
278
+ if (!isValidEnvName(name)) {
279
+ throw new Error(`Invalid FML environment name: ${name}`);
280
+ }
281
+ const current = readEnvConfig();
282
+ const next = {
283
+ schemaVersion: 1,
284
+ active: name,
285
+ environments: {
286
+ ...current.environments ?? {},
287
+ [name]: { convexUrl: normalizeConvexUrl(convexUrl) }
288
+ }
289
+ };
290
+ writeEnvConfig(next);
291
+ return next;
292
+ }
293
+ function getActiveEnv() {
294
+ const name = readEnvConfig().active;
295
+ const override = process.env.FML_CONVEX_URL;
296
+ return {
297
+ name,
298
+ convexUrl: override ? normalizeConvexUrl(override) : resolveEnvConvexUrl(name)
299
+ };
300
+ }
301
+ function authStorePathFor(envName) {
302
+ return path2.join(FML_DATA_DIR, `auth.${envName}.json`);
303
+ }
304
+ var CONVEX_URL = (process.env.FML_CONVEX_URL ?? getActiveEnv().convexUrl ?? DEFAULT_PROD_URL).replace(/\/$/, "");
305
+ var WORKOS_API_URL = "https://api.workos.com";
306
+ var WORKOS_AUTH_URL = "https://auth.fml.inc";
307
+ function getSiteUrl() {
308
+ return getSiteUrlForConvexUrl(getActiveEnv().convexUrl);
309
+ }
310
+ function getSiteUrlForConvexUrl(convexUrl) {
311
+ const explicit = process.env.CONVEX_SITE_URL;
312
+ if (explicit) return explicit.replace(/\/$/, "");
313
+ return (convexUrl ?? DEFAULT_PROD_URL).replace(".convex.cloud", ".convex.site").replace(/\/$/, "");
314
+ }
315
+
316
+ // src/sentry.ts
317
+ var initialized = false;
318
+ var sentry = null;
319
+ var client = null;
320
+ async function loadSentry() {
321
+ if (sentry) return sentry;
322
+ try {
323
+ sentry = await import("./esm-4YKLKFRX.js");
324
+ return sentry;
325
+ } catch {
326
+ return null;
327
+ }
328
+ }
329
+ async function initSentry() {
330
+ if (initialized) return;
331
+ initialized = true;
332
+ const mod = await loadSentry();
333
+ if (!mod) return;
334
+ client = new mod.ServerRuntimeClient({
335
+ dsn: "https://4c243521c03917c886d0bf33bc0038c8@o4510167429873664.ingest.us.sentry.io/4511078213156864",
336
+ tracesSampleRate: 0,
337
+ integrations: [],
338
+ stackParser: mod.createStackParser(mod.nodeStackLineParser()),
339
+ transport: (options) => mod.createTransport(options, async (request) => {
340
+ const res = await fetch(options.url, {
341
+ method: "POST",
342
+ headers: { "Content-Type": "application/x-sentry-envelope" },
343
+ body: request.body
344
+ });
345
+ return { statusCode: res.status };
346
+ })
347
+ });
348
+ mod.setCurrentClient(client);
349
+ client.init();
350
+ }
351
+ var Sentry = {
352
+ captureException: (err) => sentry?.captureException(err),
353
+ captureMessage: (msg) => sentry?.captureMessage(msg),
354
+ flush: async (timeoutMs) => {
355
+ try {
356
+ await client?.flush(timeoutMs);
357
+ } catch {
358
+ }
359
+ }
360
+ };
361
+
362
+ // src/auth/token-store.ts
363
+ var TOKEN_REFRESH_WINDOW_MS = 6e4;
364
+ var OAUTH_REFRESH_LOCK_POLL_MS = 50;
365
+ var OAUTH_REFRESH_LOCK_STALE_MS = 45e3;
366
+ var OAUTH_REFRESH_RECOVERY_STALE_MS = 45e3;
367
+ var OAUTH_REFRESH_LOCK_WAIT_MS = 6e4;
368
+ var OAUTH_REFRESH_REQUEST_TIMEOUT_MS = 3e4;
369
+ var PROCESS_IDENTITY_STALE_MS = 45e3;
370
+ var PROCESS_IDENTITY_RECHECK_MS = 5e3;
371
+ var PROCESS_INSTANCE_KEY = /* @__PURE__ */ Symbol.for("@fml-inc/fml:auth-process-instance");
372
+ var PROCESS_IDENTITY_KEY = /* @__PURE__ */ Symbol.for("@fml-inc/fml:auth-process-identity");
373
+ var processGlobals = globalThis;
374
+ var existingProcessInstanceId = processGlobals[PROCESS_INSTANCE_KEY];
375
+ var CURRENT_PROCESS_INSTANCE_ID = typeof existingProcessInstanceId === "string" ? existingProcessInstanceId : randomUUID();
376
+ processGlobals[PROCESS_INSTANCE_KEY] = CURRENT_PROCESS_INSTANCE_ID;
377
+ function getCurrentProcessIdentity() {
378
+ let cache = processGlobals[PROCESS_IDENTITY_KEY];
379
+ if (!cache) {
380
+ cache = { resolved: false };
381
+ processGlobals[PROCESS_IDENTITY_KEY] = cache;
382
+ }
383
+ if (!cache.resolved) {
384
+ cache.value = getProcessIdentity(process.pid) ?? void 0;
385
+ cache.resolved = true;
386
+ }
387
+ return cache.value;
388
+ }
389
+ function storePathFor(envName) {
390
+ return authStorePathFor(envName ?? getActiveEnv().name);
391
+ }
392
+ function resolveEnvName(envName) {
393
+ return envName ?? getActiveEnv().name;
394
+ }
395
+ function ensureDir(envName) {
396
+ const dir = path3.dirname(storePathFor(envName));
397
+ fs3.mkdirSync(dir, { recursive: true, mode: 448 });
398
+ try {
399
+ fs3.chmodSync(dir, 448);
400
+ } catch {
401
+ }
402
+ }
403
+ function writeSecretFile(filePath, data) {
404
+ const temporaryFile = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
405
+ let descriptor = null;
406
+ try {
407
+ descriptor = fs3.openSync(temporaryFile, "wx", 384);
408
+ fs3.writeFileSync(descriptor, data, { encoding: "utf8" });
409
+ fs3.fsyncSync(descriptor);
410
+ fs3.closeSync(descriptor);
411
+ descriptor = null;
412
+ fs3.renameSync(temporaryFile, filePath);
413
+ try {
414
+ fs3.chmodSync(filePath, 384);
415
+ } catch {
416
+ }
417
+ } finally {
418
+ if (descriptor !== null) {
419
+ try {
420
+ fs3.closeSync(descriptor);
421
+ } catch {
422
+ }
423
+ }
424
+ try {
425
+ fs3.unlinkSync(temporaryFile);
426
+ } catch {
427
+ }
428
+ }
429
+ }
430
+ function readTokens(envName) {
431
+ let raw;
432
+ try {
433
+ raw = fs3.readFileSync(storePathFor(envName), "utf-8");
434
+ } catch {
435
+ return null;
436
+ }
437
+ try {
438
+ return parseStoredAuthShape(JSON.parse(raw));
439
+ } catch {
440
+ return null;
441
+ }
442
+ }
443
+ var CorruptStoredCredentialsError = class extends Error {
444
+ };
445
+ function parseStoredAuthShape(parsed) {
446
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
447
+ return null;
448
+ }
449
+ const auth = parsed;
450
+ const user = auth.user;
451
+ if (typeof auth.accessToken !== "string" || typeof auth.refreshToken !== "string" || typeof auth.expiresAt !== "number" || !Number.isFinite(auth.expiresAt) || !user || typeof user.id !== "string" || typeof user.email !== "string" || typeof user.name !== "string" || auth.orgId !== void 0 && typeof auth.orgId !== "string" || auth.orgSlug !== void 0 && typeof auth.orgSlug !== "string" || auth.workosClientId !== void 0 && typeof auth.workosClientId !== "string" || auth.tokenType !== void 0 && auth.tokenType !== "oauth" && auth.tokenType !== "service" || auth.refreshInvalidAt !== void 0 && (typeof auth.refreshInvalidAt !== "number" || !Number.isFinite(auth.refreshInvalidAt))) {
452
+ return null;
453
+ }
454
+ return auth;
455
+ }
456
+ function readTokensForReconciliation(envName) {
457
+ let raw;
458
+ try {
459
+ raw = fs3.readFileSync(storePathFor(envName), "utf8");
460
+ } catch (error) {
461
+ if (error.code === "ENOENT") return null;
462
+ throw error;
463
+ }
464
+ let parsed;
465
+ try {
466
+ parsed = JSON.parse(raw);
467
+ } catch (error) {
468
+ throw new CorruptStoredCredentialsError(
469
+ "Stored credentials are not valid JSON",
470
+ { cause: error }
471
+ );
472
+ }
473
+ const auth = parseStoredAuthShape(parsed);
474
+ if (!auth) {
475
+ throw new CorruptStoredCredentialsError(
476
+ "Stored credentials have an invalid shape"
477
+ );
478
+ }
479
+ return auth;
480
+ }
481
+ function writeTokensUnlocked(auth, envName) {
482
+ ensureDir(envName);
483
+ writeSecretFile(storePathFor(envName), JSON.stringify(auth, null, 2));
484
+ }
485
+ async function writeTokens(auth, envName) {
486
+ const resolvedEnvName = resolveEnvName(envName);
487
+ await withRefreshLock(
488
+ resolvedEnvName,
489
+ () => writeTokensUnlocked(auth, resolvedEnvName),
490
+ { unreadableStorePolicy: "discard-staged" }
491
+ );
492
+ }
493
+ function getSelectedOrg(envName) {
494
+ return readTokens(envName)?.orgSlug ?? null;
495
+ }
496
+ function getSelectedOrgId(envName) {
497
+ return readTokens(envName)?.orgId ?? null;
498
+ }
499
+ async function setSelectedOrg(orgSlug, orgId, envName) {
500
+ const resolvedEnvName = resolveEnvName(envName);
501
+ await withRefreshLock(resolvedEnvName, () => {
502
+ const stored = readTokens(resolvedEnvName);
503
+ if (!stored) return;
504
+ writeTokensUnlocked({ ...stored, orgId, orgSlug }, resolvedEnvName);
505
+ });
506
+ }
507
+ function credentialHash(value) {
508
+ return createHash("sha256").update(value).digest("hex");
509
+ }
510
+ function describeError(error) {
511
+ return error instanceof Error ? error.message : String(error);
512
+ }
513
+ function getSyncDestinationAuthBinding(envName) {
514
+ const resolvedEnvName = resolveEnvName(envName);
515
+ const envToken = process.env.FML_TOKEN;
516
+ if (envToken?.startsWith("fml_srt_")) {
517
+ return {
518
+ destinationIdentity: `service-refresh:${credentialHash(envToken)}`
519
+ };
520
+ }
521
+ if (envToken?.startsWith("fml_st_")) {
522
+ return {
523
+ destinationIdentity: `service-access:${credentialHash(envToken)}`
524
+ };
525
+ }
526
+ const stored = readTokens(resolvedEnvName);
527
+ if (!stored) return null;
528
+ if (stored.tokenType === "service") {
529
+ return {
530
+ destinationIdentity: `service-refresh:${credentialHash(stored.refreshToken)}`
531
+ };
532
+ }
533
+ const destinationIdentity = `oauth-user:${stored.user.id}`;
534
+ if (stored.orgId && stored.orgSlug) {
535
+ return {
536
+ destinationIdentity,
537
+ organizationId: stored.orgId,
538
+ organizationSlug: stored.orgSlug
539
+ };
540
+ }
541
+ return { destinationIdentity, organizationSelectionRequired: true };
542
+ }
543
+ function getSyncDestinationIdentity(envName) {
544
+ return getSyncDestinationAuthBinding(envName)?.destinationIdentity ?? null;
545
+ }
546
+ var SERVICE_TOKEN_LOGIN_USER_ID = "service-token";
547
+ var serviceTokenCaches = /* @__PURE__ */ new Map();
548
+ var serviceRefreshPromises = /* @__PURE__ */ new Map();
549
+ var oauthRefreshPromises = /* @__PURE__ */ new Map();
550
+ var cacheKey = (envName) => envName;
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) {
574
+ const existing = flights.get(key);
575
+ if (existing && (!forceRefresh || existing.forceRefresh)) {
576
+ return waitForTokenRefresh(existing.promise, signal);
577
+ }
578
+ const operation = existing ? existing.promise.then(refresh, refresh) : refresh();
579
+ let flight;
580
+ const promise = operation.finally(() => {
581
+ if (flights.get(key) === flight) flights.delete(key);
582
+ });
583
+ flight = { forceRefresh, promise };
584
+ flights.set(key, flight);
585
+ return waitForTokenRefresh(promise, signal);
586
+ }
587
+ function refreshLockPath(envName) {
588
+ return `${storePathFor(envName)}.refresh.lock`;
589
+ }
590
+ function pendingOAuthRefreshPrefix(envName) {
591
+ return `${path3.basename(storePathFor(envName))}.refresh-result.`;
592
+ }
593
+ function parsePendingOAuthRefresh(raw) {
594
+ try {
595
+ const parsed = JSON.parse(raw);
596
+ const refreshed = parsed.refreshed;
597
+ if (parsed.version !== 1 || typeof parsed.previousRefreshTokenHash !== "string" || !/^[a-f0-9]{64}$/.test(parsed.previousRefreshTokenHash) || typeof parsed.createdAtMs !== "number" || !Number.isFinite(parsed.createdAtMs) || !refreshed || typeof refreshed.accessToken !== "string" || typeof refreshed.refreshToken !== "string" || typeof refreshed.expiresAt !== "number" || !Number.isFinite(refreshed.expiresAt) || !refreshed.user || typeof refreshed.user.id !== "string" || typeof refreshed.user.email !== "string" || typeof refreshed.user.name !== "string") {
598
+ return null;
599
+ }
600
+ return parsed;
601
+ } catch {
602
+ return null;
603
+ }
604
+ }
605
+ function stagePendingOAuthRefresh(previousRefreshToken, refreshed, envName) {
606
+ ensureDir(envName);
607
+ const resultPath = `${storePathFor(envName)}.refresh-result.${Date.now()}.${randomUUID()}.json`;
608
+ const pending = {
609
+ version: 1,
610
+ previousRefreshTokenHash: credentialHash(previousRefreshToken),
611
+ refreshed,
612
+ createdAtMs: Date.now()
613
+ };
614
+ writeSecretFile(resultPath, `${JSON.stringify(pending)}
615
+ `);
616
+ return resultPath;
617
+ }
618
+ function removePendingOAuthRefresh(resultPath) {
619
+ try {
620
+ fs3.unlinkSync(resultPath);
621
+ } catch (error) {
622
+ if (error.code !== "ENOENT") {
623
+ console.error(
624
+ `[fml] Auth: unable to remove a committed refresh result: ${error instanceof Error ? error.message : String(error)}`
625
+ );
626
+ }
627
+ }
628
+ }
629
+ function reconcilePendingOAuthRefreshes(envName, unreadableStorePolicy = "preserve-staged") {
630
+ const storePath = storePathFor(envName);
631
+ const directory = path3.dirname(storePath);
632
+ const prefix = pendingOAuthRefreshPrefix(envName);
633
+ let resultPaths;
634
+ try {
635
+ resultPaths = fs3.readdirSync(directory).filter((entry) => entry.startsWith(prefix) && entry.endsWith(".json")).map((entry) => path3.join(directory, entry));
636
+ } catch (error) {
637
+ if (error.code === "ENOENT") return;
638
+ throw error;
639
+ }
640
+ const pendingResults = resultPaths.map((resultPath) => {
641
+ let pending = null;
642
+ try {
643
+ pending = parsePendingOAuthRefresh(fs3.readFileSync(resultPath, "utf8"));
644
+ } catch (error) {
645
+ if (error.code !== "ENOENT") throw error;
646
+ }
647
+ return { resultPath, pending };
648
+ }).sort(
649
+ (left, right) => (left.pending?.createdAtMs ?? 0) - (right.pending?.createdAtMs ?? 0)
650
+ );
651
+ for (const { resultPath, pending } of pendingResults) {
652
+ if (!pending) {
653
+ removePendingOAuthRefresh(resultPath);
654
+ continue;
655
+ }
656
+ let current;
657
+ try {
658
+ current = readTokensForReconciliation(envName);
659
+ } catch (error) {
660
+ if (unreadableStorePolicy === "discard-staged" && error instanceof CorruptStoredCredentialsError) {
661
+ console.error(
662
+ `[fml] Auth: discarding a staged refresh result because the credential store is corrupt and is being replaced: ${error.message}`
663
+ );
664
+ removePendingOAuthRefresh(resultPath);
665
+ continue;
666
+ }
667
+ throw error;
668
+ }
669
+ if (current?.refreshToken === pending.refreshed.refreshToken || current && credentialHash(current.refreshToken) === pending.previousRefreshTokenHash) {
670
+ if (current.refreshToken !== pending.refreshed.refreshToken) {
671
+ writeTokensUnlocked(
672
+ {
673
+ ...pending.refreshed,
674
+ orgId: current.orgId,
675
+ orgSlug: current.orgSlug
676
+ },
677
+ envName
678
+ );
679
+ }
680
+ }
681
+ removePendingOAuthRefresh(resultPath);
682
+ }
683
+ }
684
+ function parseRefreshLockOwner(raw) {
685
+ try {
686
+ const parsed = JSON.parse(raw);
687
+ if (parsed.version !== 1 || typeof parsed.ownerId !== "string" || parsed.ownerId.length === 0 || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0 || parsed.processIdentity !== void 0 && typeof parsed.processIdentity !== "string" || parsed.processInstanceId !== void 0 && typeof parsed.processInstanceId !== "string" || typeof parsed.createdAtMs !== "number" || !Number.isFinite(parsed.createdAtMs) || parsed.releasedAtMs !== void 0 && (typeof parsed.releasedAtMs !== "number" || !Number.isFinite(parsed.releasedAtMs))) {
688
+ return null;
689
+ }
690
+ return parsed;
691
+ } catch {
692
+ return null;
693
+ }
694
+ }
695
+ function getProcessIdentity(pid) {
696
+ try {
697
+ if (process.platform === "linux") {
698
+ const stat = fs3.readFileSync(`/proc/${pid}/stat`, "utf8");
699
+ const fieldsAfterCommand = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
700
+ const startTimeTicks = fieldsAfterCommand[19];
701
+ return startTimeTicks ? `linux:${startTimeTicks}` : null;
702
+ }
703
+ if (process.platform === "darwin") {
704
+ const startedAt = execFileSync(
705
+ "ps",
706
+ ["-p", String(pid), "-o", "lstart="],
707
+ {
708
+ encoding: "utf8",
709
+ stdio: ["ignore", "pipe", "ignore"],
710
+ timeout: 2e3
711
+ }
712
+ ).trim();
713
+ return startedAt ? `darwin:${startedAt}` : null;
714
+ }
715
+ if (process.platform === "win32") {
716
+ const startedAt = execFileSync(
717
+ "powershell.exe",
718
+ [
719
+ "-NoProfile",
720
+ "-NonInteractive",
721
+ "-Command",
722
+ `(Get-Process -Id ${pid}).StartTime.ToUniversalTime().Ticks`
723
+ ],
724
+ {
725
+ encoding: "utf8",
726
+ stdio: ["ignore", "pipe", "ignore"],
727
+ timeout: 2e3
728
+ }
729
+ ).trim();
730
+ return startedAt ? `win32:${startedAt}` : null;
731
+ }
732
+ } catch {
733
+ }
734
+ return null;
735
+ }
736
+ function parseRecoveryMarkerOwner(raw) {
737
+ try {
738
+ const parsed = JSON.parse(raw);
739
+ if (parsed.version !== 1 || typeof parsed.ownerId !== "string" || parsed.ownerId.length === 0 || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0 || parsed.processIdentity !== void 0 && typeof parsed.processIdentity !== "string" || parsed.processInstanceId !== void 0 && typeof parsed.processInstanceId !== "string" || typeof parsed.createdAtMs !== "number" || !Number.isFinite(parsed.createdAtMs) || parsed.releasedAtMs !== void 0 && (typeof parsed.releasedAtMs !== "number" || !Number.isFinite(parsed.releasedAtMs))) {
740
+ return null;
741
+ }
742
+ return parsed;
743
+ } catch {
744
+ return null;
745
+ }
746
+ }
747
+ function snapshotLeaseFile(filePath) {
748
+ let stat;
749
+ try {
750
+ stat = fs3.lstatSync(filePath);
751
+ } catch (error) {
752
+ if (error.code === "ENOENT") return null;
753
+ throw error;
754
+ }
755
+ if (stat.isDirectory()) {
756
+ try {
757
+ return {
758
+ stat,
759
+ raw: fs3.readFileSync(path3.join(filePath, "owner.json"), "utf8")
760
+ };
761
+ } catch (error) {
762
+ if (error.code === "ENOENT") {
763
+ return { stat, raw: "" };
764
+ }
765
+ throw error;
766
+ }
767
+ }
768
+ try {
769
+ return { stat, raw: fs3.readFileSync(filePath, "utf8") };
770
+ } catch (error) {
771
+ if (error.code === "ENOENT") return null;
772
+ throw error;
773
+ }
774
+ }
775
+ function snapshotsMatch(observed, current) {
776
+ return current.stat.dev === observed.stat.dev && current.stat.ino === observed.stat.ino && current.stat.mtimeMs === observed.stat.mtimeMs && current.raw === observed.raw;
777
+ }
778
+ function restoreMovedLease(movedPath, originalPath) {
779
+ let movedStat;
780
+ try {
781
+ movedStat = fs3.lstatSync(movedPath);
782
+ } catch (error) {
783
+ if (error.code === "ENOENT") return;
784
+ throw error;
785
+ }
786
+ if (!movedStat.isFile()) return;
787
+ try {
788
+ fs3.linkSync(movedPath, originalPath);
789
+ } catch (error) {
790
+ const code = error.code;
791
+ if (code === "EEXIST" || code === "ENOENT") return;
792
+ throw error;
793
+ }
794
+ try {
795
+ fs3.unlinkSync(movedPath);
796
+ } catch (error) {
797
+ if (error.code !== "ENOENT") throw error;
798
+ }
799
+ }
800
+ function pathMatchesClaim(filePath, claimedStat, ownerId, parseOwner) {
801
+ const current = snapshotLeaseFile(filePath);
802
+ return Boolean(
803
+ current && current.stat.dev === claimedStat.dev && current.stat.ino === claimedStat.ino && parseOwner(current.raw)?.ownerId === ownerId
804
+ );
805
+ }
806
+ function markClaimReleased(filePath, claimedStat, owner, parseOwner) {
807
+ let descriptor;
808
+ try {
809
+ descriptor = fs3.openSync(filePath, "r+");
810
+ } catch (error) {
811
+ if (error.code === "ENOENT") return false;
812
+ throw error;
813
+ }
814
+ try {
815
+ const currentStat = fs3.fstatSync(descriptor);
816
+ if (currentStat.dev !== claimedStat.dev || currentStat.ino !== claimedStat.ino) {
817
+ return false;
818
+ }
819
+ const currentOwner = parseOwner(fs3.readFileSync(descriptor, "utf8"));
820
+ if (currentOwner?.ownerId !== owner.ownerId) return false;
821
+ const serialized = Buffer.from(
822
+ `${JSON.stringify({ ...owner, releasedAtMs: Date.now() })}
823
+ `,
824
+ "utf8"
825
+ );
826
+ fs3.ftruncateSync(descriptor, 0);
827
+ let offset = 0;
828
+ while (offset < serialized.length) {
829
+ offset += fs3.writeSync(
830
+ descriptor,
831
+ serialized,
832
+ offset,
833
+ serialized.length - offset,
834
+ offset
835
+ );
836
+ }
837
+ fs3.fsyncSync(descriptor);
838
+ return true;
839
+ } finally {
840
+ fs3.closeSync(descriptor);
841
+ }
842
+ }
843
+ function releaseOwnedLease(filePath, claimedStat, owner, parseOwner, description) {
844
+ const releasedPath = `${filePath}.released.${randomUUID()}`;
845
+ let moved = false;
846
+ try {
847
+ if (!markClaimReleased(filePath, claimedStat, owner, parseOwner)) return;
848
+ try {
849
+ fs3.renameSync(filePath, releasedPath);
850
+ moved = true;
851
+ } catch (error) {
852
+ if (error.code === "ENOENT") return;
853
+ throw error;
854
+ }
855
+ const released = snapshotLeaseFile(releasedPath);
856
+ const releasedOwner = released ? parseOwner(released.raw) : null;
857
+ if (!released || released.stat.dev !== claimedStat.dev || released.stat.ino !== claimedStat.ino || releasedOwner?.ownerId !== owner.ownerId || releasedOwner.releasedAtMs === void 0) {
858
+ restoreMovedLease(releasedPath, filePath);
859
+ moved = false;
860
+ return;
861
+ }
862
+ fs3.rmSync(releasedPath, {
863
+ recursive: released.stat.isDirectory(),
864
+ force: true
865
+ });
866
+ moved = false;
867
+ } catch (error) {
868
+ let restorationError;
869
+ if (moved) {
870
+ try {
871
+ restoreMovedLease(releasedPath, filePath);
872
+ } catch (restoreError) {
873
+ restorationError = restoreError;
874
+ }
875
+ }
876
+ console.error(
877
+ `[fml] Auth: unable to remove released ${description}: ${error instanceof Error ? error.message : String(error)}${restorationError ? `; unable to restore it: ${restorationError instanceof Error ? restorationError.message : String(restorationError)}` : ""}`
878
+ );
879
+ }
880
+ }
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
+ });
905
+ }
906
+ function isProcessAlive(pid) {
907
+ try {
908
+ process.kill(pid, 0);
909
+ return true;
910
+ } catch (error) {
911
+ return error.code === "EPERM";
912
+ }
913
+ }
914
+ var processIdentityChecks = /* @__PURE__ */ new Map();
915
+ function ownerPidWasReused(owner, lockMtimeMs) {
916
+ const now = Date.now();
917
+ if (now - Math.max(owner.createdAtMs, lockMtimeMs) < PROCESS_IDENTITY_STALE_MS)
918
+ return false;
919
+ if (owner.pid === process.pid && owner.processInstanceId !== void 0) {
920
+ return owner.processInstanceId !== CURRENT_PROCESS_INSTANCE_ID;
921
+ }
922
+ if (!owner.processIdentity) return false;
923
+ const cached = processIdentityChecks.get(owner.ownerId);
924
+ let currentIdentity;
925
+ if (cached && now - cached.checkedAtMs < PROCESS_IDENTITY_RECHECK_MS) {
926
+ currentIdentity = cached.identity;
927
+ } else {
928
+ currentIdentity = getProcessIdentity(owner.pid);
929
+ processIdentityChecks.set(owner.ownerId, {
930
+ checkedAtMs: now,
931
+ identity: currentIdentity
932
+ });
933
+ }
934
+ return currentIdentity !== null && currentIdentity !== owner.processIdentity;
935
+ }
936
+ function writeClaimToDescriptor(descriptor, serializedOwner) {
937
+ const claimedStat = fs3.fstatSync(descriptor);
938
+ fs3.writeFileSync(descriptor, serializedOwner, { encoding: "utf8" });
939
+ fs3.fsyncSync(descriptor);
940
+ return claimedStat;
941
+ }
942
+ function recoverAbandonedRecoveryMarker(recoveryPath) {
943
+ const observed = snapshotLeaseFile(recoveryPath);
944
+ if (!observed) return;
945
+ const owner = parseRecoveryMarkerOwner(observed.raw);
946
+ const ownerAlive = owner ? isProcessAlive(owner.pid) : false;
947
+ const reclaimable = owner ? owner.releasedAtMs !== void 0 || !ownerAlive || ownerAlive && ownerPidWasReused(owner, observed.stat.mtimeMs) : Date.now() - observed.stat.mtimeMs > OAUTH_REFRESH_RECOVERY_STALE_MS;
948
+ if (!reclaimable) return;
949
+ const abandonedPath = `${recoveryPath}.stale.${randomUUID()}`;
950
+ try {
951
+ fs3.renameSync(recoveryPath, abandonedPath);
952
+ const moved = snapshotLeaseFile(abandonedPath);
953
+ if (!moved || !snapshotsMatch(observed, moved)) {
954
+ restoreMovedLease(abandonedPath, recoveryPath);
955
+ return;
956
+ }
957
+ fs3.rmSync(abandonedPath, {
958
+ recursive: moved.stat.isDirectory(),
959
+ force: true
960
+ });
961
+ } catch (error) {
962
+ if (error.code !== "ENOENT") throw error;
963
+ }
964
+ }
965
+ function acquireRecoveryMarker(recoveryPath) {
966
+ const ownerId = randomUUID();
967
+ const owner = {
968
+ version: 1,
969
+ ownerId,
970
+ pid: process.pid,
971
+ processIdentity: getCurrentProcessIdentity(),
972
+ processInstanceId: CURRENT_PROCESS_INSTANCE_ID,
973
+ createdAtMs: Date.now()
974
+ };
975
+ let descriptor;
976
+ try {
977
+ descriptor = fs3.openSync(recoveryPath, "wx", 384);
978
+ } catch (error) {
979
+ if (error.code === "EEXIST") return null;
980
+ throw error;
981
+ }
982
+ let claimedStat;
983
+ try {
984
+ claimedStat = writeClaimToDescriptor(
985
+ descriptor,
986
+ `${JSON.stringify(owner)}
987
+ `
988
+ );
989
+ } finally {
990
+ fs3.closeSync(descriptor);
991
+ }
992
+ const isOwned = () => pathMatchesClaim(
993
+ recoveryPath,
994
+ claimedStat,
995
+ ownerId,
996
+ parseRecoveryMarkerOwner
997
+ );
998
+ if (!isOwned()) return null;
999
+ let released = false;
1000
+ return {
1001
+ isOwned,
1002
+ release: () => {
1003
+ if (released) return;
1004
+ released = true;
1005
+ if (isOwned()) {
1006
+ releaseOwnedLease(
1007
+ recoveryPath,
1008
+ claimedStat,
1009
+ owner,
1010
+ parseRecoveryMarkerOwner,
1011
+ "credential-lock recovery marker"
1012
+ );
1013
+ }
1014
+ }
1015
+ };
1016
+ }
1017
+ function recoverStaleRefreshLock(lockPath) {
1018
+ const recoveryPath = `${lockPath}.recovery`;
1019
+ const recovery = acquireRecoveryMarker(recoveryPath);
1020
+ if (!recovery) return;
1021
+ try {
1022
+ const observed = snapshotLeaseFile(lockPath);
1023
+ if (!observed) return;
1024
+ let reclaimable;
1025
+ const owner = parseRefreshLockOwner(observed.raw);
1026
+ if (owner) {
1027
+ const alive = isProcessAlive(owner.pid);
1028
+ reclaimable = owner.releasedAtMs !== void 0 || !alive || alive && ownerPidWasReused(owner, observed.stat.mtimeMs);
1029
+ } else {
1030
+ reclaimable = Date.now() - observed.stat.mtimeMs > OAUTH_REFRESH_LOCK_STALE_MS;
1031
+ }
1032
+ if (!reclaimable) return;
1033
+ if (!recovery.isOwned()) return;
1034
+ const abandonedPath = `${lockPath}.stale.${randomUUID()}`;
1035
+ try {
1036
+ fs3.renameSync(lockPath, abandonedPath);
1037
+ const moved = snapshotLeaseFile(abandonedPath);
1038
+ if (!moved || !snapshotsMatch(observed, moved)) {
1039
+ restoreMovedLease(abandonedPath, lockPath);
1040
+ return;
1041
+ }
1042
+ fs3.rmSync(abandonedPath, {
1043
+ recursive: moved.stat.isDirectory(),
1044
+ force: true
1045
+ });
1046
+ } catch (error) {
1047
+ if (error.code !== "ENOENT") throw error;
1048
+ }
1049
+ } finally {
1050
+ recovery.release();
1051
+ }
1052
+ }
1053
+ async function acquireRefreshLock(envName, signal) {
1054
+ try {
1055
+ ensureDir(envName);
1056
+ } catch (error) {
1057
+ console.error(
1058
+ `[fml] Auth: unable to prepare refresh lock: ${error instanceof Error ? error.message : String(error)}`
1059
+ );
1060
+ return null;
1061
+ }
1062
+ const lockPath = refreshLockPath(envName);
1063
+ const recoveryPath = `${lockPath}.recovery`;
1064
+ const deadline = Date.now() + OAUTH_REFRESH_LOCK_WAIT_MS;
1065
+ while (Date.now() < deadline) {
1066
+ if (signal?.aborted) {
1067
+ throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
1068
+ }
1069
+ if (fs3.existsSync(recoveryPath)) {
1070
+ try {
1071
+ recoverAbandonedRecoveryMarker(recoveryPath);
1072
+ } catch (recoveryError) {
1073
+ console.error(
1074
+ `[fml] Auth: unable to recover refresh recovery marker: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`
1075
+ );
1076
+ return null;
1077
+ }
1078
+ await sleep(OAUTH_REFRESH_LOCK_POLL_MS, signal);
1079
+ continue;
1080
+ }
1081
+ let descriptor;
1082
+ try {
1083
+ descriptor = fs3.openSync(lockPath, "wx", 384);
1084
+ } catch (error) {
1085
+ if (error.code !== "EEXIST") {
1086
+ console.error(
1087
+ `[fml] Auth: unable to acquire refresh lock: ${error instanceof Error ? error.message : String(error)}`
1088
+ );
1089
+ return null;
1090
+ }
1091
+ try {
1092
+ recoverStaleRefreshLock(lockPath);
1093
+ } catch (recoveryError) {
1094
+ console.error(
1095
+ `[fml] Auth: unable to recover refresh lock: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`
1096
+ );
1097
+ return null;
1098
+ }
1099
+ await sleep(OAUTH_REFRESH_LOCK_POLL_MS, signal);
1100
+ continue;
1101
+ }
1102
+ const ownerId = randomUUID();
1103
+ const owner = {
1104
+ version: 1,
1105
+ ownerId,
1106
+ pid: process.pid,
1107
+ processIdentity: getCurrentProcessIdentity(),
1108
+ processInstanceId: CURRENT_PROCESS_INSTANCE_ID,
1109
+ createdAtMs: Date.now()
1110
+ };
1111
+ let claimedStat;
1112
+ try {
1113
+ claimedStat = writeClaimToDescriptor(
1114
+ descriptor,
1115
+ `${JSON.stringify(owner)}
1116
+ `
1117
+ );
1118
+ } catch (error) {
1119
+ fs3.closeSync(descriptor);
1120
+ console.error(
1121
+ `[fml] Auth: unable to initialize refresh lock: ${error instanceof Error ? error.message : String(error)}`
1122
+ );
1123
+ return null;
1124
+ }
1125
+ fs3.closeSync(descriptor);
1126
+ const isOwned = () => pathMatchesClaim(lockPath, claimedStat, ownerId, parseRefreshLockOwner);
1127
+ const releaseClaim = () => releaseOwnedLease(
1128
+ lockPath,
1129
+ claimedStat,
1130
+ owner,
1131
+ parseRefreshLockOwner,
1132
+ "credential lock"
1133
+ );
1134
+ if (signal?.aborted) {
1135
+ releaseClaim();
1136
+ throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
1137
+ }
1138
+ let entryBlocked;
1139
+ try {
1140
+ entryBlocked = fs3.existsSync(recoveryPath) || !isOwned();
1141
+ } catch (error) {
1142
+ console.error(
1143
+ `[fml] Auth: unable to verify refresh lock ownership: ${describeError(error)}`
1144
+ );
1145
+ try {
1146
+ if (isOwned()) releaseClaim();
1147
+ } catch {
1148
+ }
1149
+ return null;
1150
+ }
1151
+ if (entryBlocked) {
1152
+ try {
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
+ }
1162
+ }
1163
+ continue;
1164
+ }
1165
+ let released = false;
1166
+ return {
1167
+ isOwned,
1168
+ release: () => {
1169
+ if (released) return;
1170
+ released = true;
1171
+ let owned = false;
1172
+ try {
1173
+ owned = isOwned();
1174
+ } catch (error) {
1175
+ console.error(
1176
+ `[fml] Auth: unable to verify credential lock ownership during release: ${describeError(error)}`
1177
+ );
1178
+ }
1179
+ if (owned) releaseClaim();
1180
+ }
1181
+ };
1182
+ }
1183
+ console.error(
1184
+ (() => {
1185
+ try {
1186
+ const owner = snapshotLeaseFile(lockPath);
1187
+ const parsedOwner = owner ? parseRefreshLockOwner(owner.raw) : null;
1188
+ return parsedOwner && isProcessAlive(parsedOwner.pid) ? `[fml] Auth: credential lock is still owned by live process ${parsedOwner.pid}; refusing an unsafe takeover` : "[fml] Auth: timed out waiting for another process to refresh credentials";
1189
+ } catch {
1190
+ return "[fml] Auth: timed out waiting for another process to refresh credentials";
1191
+ }
1192
+ })()
1193
+ );
1194
+ return null;
1195
+ }
1196
+ function isLockOwnedSafely(lock) {
1197
+ try {
1198
+ return lock.isOwned();
1199
+ } catch (error) {
1200
+ console.error(
1201
+ `[fml] Auth: unable to verify credential lock ownership: ${describeError(error)}`
1202
+ );
1203
+ return false;
1204
+ }
1205
+ }
1206
+ async function withRefreshLock(envName, mutation, options) {
1207
+ const lock = await acquireRefreshLock(envName, options?.signal);
1208
+ if (!lock) {
1209
+ throw new Error("Unable to acquire the credential mutation lock");
1210
+ }
1211
+ try {
1212
+ if (!isLockOwnedSafely(lock)) {
1213
+ throw new Error("Credential mutation lock ownership was lost");
1214
+ }
1215
+ reconcilePendingOAuthRefreshes(envName, options?.unreadableStorePolicy);
1216
+ if (!isLockOwnedSafely(lock)) {
1217
+ throw new Error("Credential mutation lock ownership was lost");
1218
+ }
1219
+ const result = await mutation(lock);
1220
+ if (!isLockOwnedSafely(lock)) {
1221
+ throw new Error("Credential mutation lock ownership was lost");
1222
+ }
1223
+ return result;
1224
+ } finally {
1225
+ lock.release();
1226
+ }
1227
+ }
1228
+ async function clearStoredCredentials(envName) {
1229
+ const resolvedEnvName = resolveEnvName(envName);
1230
+ await withRefreshLock(
1231
+ resolvedEnvName,
1232
+ () => {
1233
+ try {
1234
+ fs3.unlinkSync(storePathFor(resolvedEnvName));
1235
+ } catch (error) {
1236
+ if (error.code !== "ENOENT") throw error;
1237
+ }
1238
+ },
1239
+ { unreadableStorePolicy: "discard-staged" }
1240
+ );
1241
+ const key = cacheKey(resolvedEnvName);
1242
+ serviceTokenCaches.delete(key);
1243
+ serviceRefreshPromises.delete(key);
1244
+ oauthRefreshPromises.delete(key);
1245
+ }
1246
+ async function refreshServiceToken(refreshToken2, envName, siteUrl, signal) {
1247
+ try {
1248
+ if (signal?.aborted) return null;
1249
+ const convexUrl = (envName ? resolveEnvConvexUrl(envName) : null) ?? CONVEX_URL;
1250
+ const requestTimeout = AbortSignal.timeout(
1251
+ OAUTH_REFRESH_REQUEST_TIMEOUT_MS
1252
+ );
1253
+ const response = await fetch(
1254
+ `${siteUrl ?? getSiteUrlForConvexUrl(convexUrl)}/api/tokens/refresh`,
1255
+ {
1256
+ method: "POST",
1257
+ headers: {
1258
+ "Content-Type": "application/json",
1259
+ Authorization: `Bearer ${refreshToken2}`
1260
+ },
1261
+ signal: signal ? AbortSignal.any([signal, requestTimeout]) : requestTimeout
1262
+ }
1263
+ );
1264
+ if (!response.ok) {
1265
+ console.error(
1266
+ `[fml] Service token refresh failed: HTTP ${response.status}`
1267
+ );
1268
+ return null;
1269
+ }
1270
+ const data = await response.json();
1271
+ if (!data.ok || !data.accessToken) {
1272
+ console.error(`[fml] Service token refresh error: ${data.error}`);
1273
+ return null;
1274
+ }
1275
+ serviceTokenCaches.set(cacheKey(envName), {
1276
+ accessToken: data.accessToken,
1277
+ expiresAt: data.expiresAt
1278
+ });
1279
+ return data.accessToken;
1280
+ } catch (err) {
1281
+ if (signal?.aborted) return null;
1282
+ Sentry.captureException(err);
1283
+ console.error(`[fml] Service token refresh error: ${err}`);
1284
+ return null;
1285
+ }
1286
+ }
1287
+ async function storeServiceRefreshToken(refreshToken2, opts) {
1288
+ const envName = resolveEnvName(opts?.env);
1289
+ const token = refreshToken2.trim();
1290
+ if (!token.startsWith("fml_srt_")) {
1291
+ console.error(
1292
+ "[fml] Auth: service-token login expects a refresh token that starts with fml_srt_."
1293
+ );
1294
+ return false;
1295
+ }
1296
+ const accessToken = await refreshServiceToken(token, envName, opts?.siteUrl);
1297
+ const cached = serviceTokenCaches.get(cacheKey(envName));
1298
+ if (!accessToken || !cached) {
1299
+ return false;
1300
+ }
1301
+ await writeTokens(
1302
+ {
1303
+ accessToken,
1304
+ refreshToken: token,
1305
+ expiresAt: cached.expiresAt,
1306
+ user: {
1307
+ id: SERVICE_TOKEN_LOGIN_USER_ID,
1308
+ email: "service-token",
1309
+ name: "FML service token"
1310
+ },
1311
+ tokenType: "service"
1312
+ },
1313
+ envName
1314
+ );
1315
+ return true;
1316
+ }
1317
+ async function getValidToken(opts) {
1318
+ if (opts?.signal?.aborted) return null;
1319
+ const envName = resolveEnvName(opts?.env);
1320
+ const forceRefresh = opts?.forceRefresh === true;
1321
+ const key = cacheKey(envName);
1322
+ const envToken = process.env.FML_TOKEN;
1323
+ if (envToken?.startsWith("fml_srt_")) {
1324
+ const cached = serviceTokenCaches.get(key);
1325
+ if (!forceRefresh && cached && cached.expiresAt > Date.now() + TOKEN_REFRESH_WINDOW_MS) {
1326
+ return cached.accessToken;
1327
+ }
1328
+ return await getOrStartTokenRefresh(
1329
+ serviceRefreshPromises,
1330
+ key,
1331
+ forceRefresh,
1332
+ () => refreshServiceToken(envToken, envName, void 0, opts?.signal),
1333
+ opts?.signal
1334
+ );
1335
+ }
1336
+ if (envToken?.startsWith("fml_st_")) return envToken;
1337
+ if (envToken) {
1338
+ console.error(
1339
+ "[fml] Auth: unsupported FML_TOKEN format. Expected fml_srt_* or fml_st_*."
1340
+ );
1341
+ return null;
1342
+ }
1343
+ const stored = readTokens(envName);
1344
+ if (!stored) {
1345
+ console.error(
1346
+ envName ? `[fml] Auth: no stored tokens for env "${envName}"` : "[fml] Auth: no stored tokens"
1347
+ );
1348
+ return null;
1349
+ }
1350
+ if (!forceRefresh && stored.expiresAt > Date.now() + TOKEN_REFRESH_WINDOW_MS) {
1351
+ return stored.accessToken;
1352
+ }
1353
+ if (stored.tokenType === "service") {
1354
+ return await getOrStartTokenRefresh(
1355
+ serviceRefreshPromises,
1356
+ key,
1357
+ forceRefresh,
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
1375
+ );
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;
1393
+ console.error(
1394
+ `[fml] Auth: unable to refresh stored service credentials: ${error instanceof Error ? error.message : String(error)}`
1395
+ );
1396
+ return null;
1397
+ }),
1398
+ opts?.signal
1399
+ );
1400
+ }
1401
+ if (stored.refreshInvalidAt) {
1402
+ console.error(
1403
+ "[fml] Auth: refresh token is no longer valid \u2014 run `fml login`"
1404
+ );
1405
+ return null;
1406
+ }
1407
+ return await getOrStartTokenRefresh(
1408
+ oauthRefreshPromises,
1409
+ key,
1410
+ forceRefresh,
1411
+ () => refreshTokenWithLock(envName, forceRefresh, opts?.signal),
1412
+ opts?.signal
1413
+ );
1414
+ }
1415
+ async function refreshTokenWithLock(envName, forceRefresh = false, signal) {
1416
+ const lock = await acquireRefreshLock(envName, signal);
1417
+ if (!lock) return null;
1418
+ try {
1419
+ if (!isLockOwnedSafely(lock)) return null;
1420
+ try {
1421
+ reconcilePendingOAuthRefreshes(envName);
1422
+ } catch (error) {
1423
+ console.error(
1424
+ `[fml] Auth: unable to reconcile staged refresh results: ${describeError(error)}`
1425
+ );
1426
+ return null;
1427
+ }
1428
+ if (!isLockOwnedSafely(lock)) return null;
1429
+ const current = readTokens(envName);
1430
+ if (!current) {
1431
+ console.error("[fml] Auth: credentials disappeared during refresh");
1432
+ return null;
1433
+ }
1434
+ if (!forceRefresh && current.expiresAt > Date.now() + TOKEN_REFRESH_WINDOW_MS) {
1435
+ return current.accessToken;
1436
+ }
1437
+ if (current.refreshInvalidAt) {
1438
+ console.error(
1439
+ "[fml] Auth: refresh token is no longer valid \u2014 run `fml login`"
1440
+ );
1441
+ return null;
1442
+ }
1443
+ if (current.tokenType === "service") {
1444
+ return null;
1445
+ }
1446
+ if (!isLockOwnedSafely(lock)) return null;
1447
+ return await refreshToken(current, lock, envName, signal);
1448
+ } finally {
1449
+ lock.release();
1450
+ }
1451
+ }
1452
+ async function refreshToken(stored, lock, envName, signal) {
1453
+ if (!stored.workosClientId) {
1454
+ console.error(
1455
+ "[fml] Auth: token expired, missing workosClientId \u2014 run `fml login`"
1456
+ );
1457
+ return null;
1458
+ }
1459
+ try {
1460
+ if (signal?.aborted) return null;
1461
+ if (!lock.isOwned()) return null;
1462
+ const requestTimeout = AbortSignal.timeout(
1463
+ OAUTH_REFRESH_REQUEST_TIMEOUT_MS
1464
+ );
1465
+ const response = await fetch(
1466
+ `${WORKOS_API_URL}/user_management/authenticate`,
1467
+ {
1468
+ method: "POST",
1469
+ headers: { "Content-Type": "application/json" },
1470
+ body: JSON.stringify({
1471
+ grant_type: "refresh_token",
1472
+ client_id: stored.workosClientId,
1473
+ refresh_token: stored.refreshToken
1474
+ }),
1475
+ signal: signal ? AbortSignal.any([signal, requestTimeout]) : requestTimeout
1476
+ }
1477
+ );
1478
+ if (!response.ok) {
1479
+ if (!lock.isOwned()) return null;
1480
+ let oauthError = null;
1481
+ try {
1482
+ const errorBody = await response.json();
1483
+ if (errorBody && typeof errorBody === "object" && "error" in errorBody && typeof errorBody.error === "string") {
1484
+ oauthError = errorBody.error;
1485
+ }
1486
+ } catch {
1487
+ }
1488
+ if (oauthError === "invalid_grant") {
1489
+ const current2 = readTokens(envName);
1490
+ if (lock.isOwned() && current2?.refreshToken === stored.refreshToken) {
1491
+ writeTokensUnlocked(
1492
+ { ...current2, refreshInvalidAt: Date.now() },
1493
+ envName
1494
+ );
1495
+ }
1496
+ console.error(
1497
+ "[fml] Auth: refresh token is no longer valid \u2014 run `fml login`"
1498
+ );
1499
+ return null;
1500
+ }
1501
+ console.error(
1502
+ `[fml] Auth: refresh failed (HTTP ${response.status}) \u2014 token preserved`
1503
+ );
1504
+ return null;
1505
+ }
1506
+ const data = await response.json();
1507
+ const userName = [data.user.first_name, data.user.last_name].filter(Boolean).join(" ");
1508
+ const refreshed = {
1509
+ accessToken: data.access_token,
1510
+ refreshToken: data.refresh_token,
1511
+ expiresAt: Date.now() + (data.expires_in ?? 300) * 1e3,
1512
+ user: {
1513
+ id: data.user.id,
1514
+ email: data.user.email,
1515
+ name: userName || stored.user.name
1516
+ },
1517
+ orgId: stored.orgId,
1518
+ orgSlug: stored.orgSlug,
1519
+ workosClientId: stored.workosClientId
1520
+ };
1521
+ const pendingPath = stagePendingOAuthRefresh(
1522
+ stored.refreshToken,
1523
+ refreshed,
1524
+ envName
1525
+ );
1526
+ const current = readTokens(envName);
1527
+ if (current?.refreshToken === stored.refreshToken && lock.isOwned()) {
1528
+ writeTokensUnlocked(
1529
+ {
1530
+ ...refreshed,
1531
+ orgId: current.orgId,
1532
+ orgSlug: current.orgSlug
1533
+ },
1534
+ envName
1535
+ );
1536
+ removePendingOAuthRefresh(pendingPath);
1537
+ return refreshed.accessToken;
1538
+ }
1539
+ if (!current) return null;
1540
+ if (current.refreshToken !== stored.refreshToken) {
1541
+ return current.accessToken;
1542
+ }
1543
+ return refreshed.accessToken;
1544
+ } catch (err) {
1545
+ if (signal?.aborted) return null;
1546
+ const msg = err instanceof Error ? err.message : String(err);
1547
+ console.error(
1548
+ `[fml] Auth: refresh failed (network error: ${msg}) \u2014 token preserved`
1549
+ );
1550
+ Sentry.captureException(err);
1551
+ return null;
1552
+ }
1553
+ }
1554
+
1555
+ export {
1556
+ initSentry,
1557
+ Sentry,
1558
+ resolveFmlDefaultDirectories,
1559
+ FML_DATA_DIR,
1560
+ FML_STATE_DIR,
1561
+ FML_LOG_DIR,
1562
+ FML_OBSERVER_DIR,
1563
+ FML_OBSERVER_HOST,
1564
+ FML_OBSERVER_PORT,
1565
+ FML_OBSERVER_STATE_FILE,
1566
+ FML_OBSERVER_LOCK_FILE,
1567
+ FML_OBSERVER_AUTH_TOKEN_FILE,
1568
+ FML_OBSERVER_ENDPOINT_FILE,
1569
+ FML_OBSERVER_LOG_FILE,
1570
+ FML_INSTALLATION_ID_FILE,
1571
+ FML_PANOPTICON_DB_FILE,
1572
+ FML_PANOPTICON_DB_OBSERVER_STATE_FILE,
1573
+ FML_PANOPTICON_DB_OBSERVER_LOCK_FILE,
1574
+ FML_LEGACY_OBSERVER_DB_FILE,
1575
+ FML_HOOK_INBOX_DIR,
1576
+ FML_HOOK_CONTEXT_STATE_DIR,
1577
+ FML_HOOK_CAPTURE_DISABLED_FILE,
1578
+ observerOnlyOverrideWarning,
1579
+ ensureDirs,
1580
+ DEFAULT_PROD_URL,
1581
+ DEFAULT_TARGET_NAME,
1582
+ isValidEnvName,
1583
+ normalizeConvexUrl,
1584
+ writeEnvConfig,
1585
+ envConfigExists,
1586
+ resolveEnvConvexUrl,
1587
+ setActiveEnvironment,
1588
+ getActiveEnv,
1589
+ authStorePathFor,
1590
+ CONVEX_URL,
1591
+ WORKOS_API_URL,
1592
+ WORKOS_AUTH_URL,
1593
+ getSiteUrl,
1594
+ getSiteUrlForConvexUrl,
1595
+ readTokens,
1596
+ writeTokens,
1597
+ getSelectedOrg,
1598
+ getSelectedOrgId,
1599
+ setSelectedOrg,
1600
+ getSyncDestinationAuthBinding,
1601
+ getSyncDestinationIdentity,
1602
+ SERVICE_TOKEN_LOGIN_USER_ID,
1603
+ clearStoredCredentials,
1604
+ storeServiceRefreshToken,
1605
+ getValidToken
1606
+ };