@fml-inc/fml 0.8.7 → 0.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runObserverSyncCycle
4
- } from "./chunk-6BTC3S7C.js";
5
- import "./chunk-5JJQWV4G.js";
4
+ } from "./chunk-2DGHAVVR.js";
5
+ import "./chunk-JPHN4VKM.js";
6
6
  import "./chunk-F57IFY3K.js";
7
- import "./chunk-B3H2YWPJ.js";
7
+ import "./chunk-HDAFEHUY.js";
8
8
  import "./chunk-MLKGABMK.js";
9
9
 
10
10
  // src/sync-cycle.ts
@@ -0,0 +1,148 @@
1
+ import {
2
+ DEFAULT_SYNC_URL,
3
+ FML_DATA_DIR,
4
+ getActiveEnv,
5
+ getSiteUrlForConvexUrl,
6
+ isHostedBackendUrl,
7
+ readTokens,
8
+ resolveEnvConvexUrl
9
+ } from "./chunk-HDAFEHUY.js";
10
+ import "./chunk-MLKGABMK.js";
11
+
12
+ // src/telemetry-config.ts
13
+ import { createHash, randomUUID } from "crypto";
14
+ import fs from "fs";
15
+ import path from "path";
16
+ var CONFIG_TTL_MS = 60 * 60 * 1e3;
17
+ var CONFIG_TIMEOUT_MS = 1500;
18
+ var refreshesInFlight = /* @__PURE__ */ new Map();
19
+ function parseSentryConfig(value) {
20
+ if (value === null) return null;
21
+ if (!value || typeof value !== "object")
22
+ throw new Error("Invalid Sentry config");
23
+ const config = value;
24
+ if (typeof config.dsn !== "string") throw new Error("Invalid Sentry DSN");
25
+ const url = new URL(config.dsn);
26
+ if (url.protocol !== "https:" || !url.username || url.password || url.search || url.hash || !/^\/(?:.*\/)?\d+$/.test(url.pathname)) {
27
+ throw new Error("Invalid Sentry DSN");
28
+ }
29
+ if (config.environment !== void 0 && (typeof config.environment !== "string" || !config.environment.trim())) {
30
+ throw new Error("Invalid Sentry environment");
31
+ }
32
+ return {
33
+ dsn: config.dsn,
34
+ ...typeof config.environment === "string" ? { environment: config.environment } : {}
35
+ };
36
+ }
37
+ function cachePathFor(siteUrl) {
38
+ const key = isHostedBackendUrl(siteUrl) ? DEFAULT_SYNC_URL : siteUrl;
39
+ return path.join(
40
+ FML_DATA_DIR,
41
+ "telemetry-config",
42
+ `${createHash("sha256").update(key).digest("hex")}.json`
43
+ );
44
+ }
45
+ function readCache(siteUrl) {
46
+ try {
47
+ const value = JSON.parse(
48
+ fs.readFileSync(cachePathFor(siteUrl), "utf8")
49
+ );
50
+ if (!value || typeof value !== "object") return void 0;
51
+ const entry = value;
52
+ if (typeof entry.siteUrl !== "string" || entry.siteUrl !== siteUrl && !(isHostedBackendUrl(entry.siteUrl) && isHostedBackendUrl(siteUrl)) || typeof entry.fetchedAt !== "number" || !Number.isFinite(entry.fetchedAt))
53
+ return void 0;
54
+ return {
55
+ siteUrl: entry.siteUrl,
56
+ fetchedAt: entry.fetchedAt,
57
+ ...entry.sentry === void 0 ? {} : { sentry: parseSentryConfig(entry.sentry) }
58
+ };
59
+ } catch {
60
+ return void 0;
61
+ }
62
+ }
63
+ function writeCache(entry) {
64
+ const cachePath = cachePathFor(entry.siteUrl);
65
+ const temporaryPath = `${cachePath}.${randomUUID()}.tmp`;
66
+ try {
67
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true, mode: 448 });
68
+ fs.writeFileSync(temporaryPath, JSON.stringify(entry), { mode: 384 });
69
+ fs.renameSync(temporaryPath, cachePath);
70
+ } catch {
71
+ } finally {
72
+ try {
73
+ fs.rmSync(temporaryPath, { force: true });
74
+ } catch {
75
+ }
76
+ }
77
+ }
78
+ async function refreshCliTelemetryConfig(opts) {
79
+ try {
80
+ if (opts.signal?.aborted) return;
81
+ const siteUrl = opts.siteUrl ?? getSiteUrlForConvexUrl(resolveEnvConvexUrl(opts.envName));
82
+ if (!opts.siteUrl && !isHostedBackendUrl(siteUrl)) return;
83
+ if (!opts.force) {
84
+ const age = Date.now() - (readCache(siteUrl)?.fetchedAt ?? Number.NaN);
85
+ if (age >= 0 && age < CONFIG_TTL_MS) return;
86
+ }
87
+ const key = cachePathFor(siteUrl);
88
+ let refresh = refreshesInFlight.get(key);
89
+ if (!refresh) {
90
+ refresh = fetchAndCache(siteUrl, opts.accessToken, opts.signal).finally(
91
+ () => refreshesInFlight.delete(key)
92
+ );
93
+ refreshesInFlight.set(key, refresh);
94
+ }
95
+ await refresh;
96
+ } catch {
97
+ }
98
+ }
99
+ async function fetchAndCache(siteUrl, accessToken, signal) {
100
+ const timeout = AbortSignal.timeout(CONFIG_TIMEOUT_MS);
101
+ const response = await fetch(`${siteUrl}/api/telemetry/config`, {
102
+ headers: { Authorization: `Bearer ${accessToken}` },
103
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
104
+ redirect: "error"
105
+ });
106
+ if (!response.ok) throw new Error("Config fetch failed");
107
+ const data = await response.json();
108
+ if (!data || typeof data !== "object" || data.ok !== true)
109
+ throw new Error("Invalid telemetry config");
110
+ const sentry = data.sentry;
111
+ writeCache({
112
+ siteUrl,
113
+ fetchedAt: Date.now(),
114
+ ...sentry === void 0 ? {} : { sentry: parseSentryConfig(sentry) }
115
+ });
116
+ }
117
+ async function resolveCliSentryConfig(defaultDsn, forceRefresh = false) {
118
+ if (process.env.FML_SENTRY_DSN !== void 0) {
119
+ return process.env.FML_SENTRY_DSN ? {
120
+ dsn: process.env.FML_SENTRY_DSN,
121
+ environment: process.env.FML_SENTRY_ENVIRONMENT
122
+ } : null;
123
+ }
124
+ const active = getActiveEnv();
125
+ const siteUrl = getSiteUrlForConvexUrl(active.convexUrl);
126
+ const hosted = active.convexUrl !== null && isHostedBackendUrl(active.convexUrl) && isHostedBackendUrl(siteUrl);
127
+ const fallback = hosted && defaultDsn ? {
128
+ dsn: defaultDsn,
129
+ environment: process.env.DEPLOYMENT_ENV ?? process.env.NODE_ENV ?? "production"
130
+ } : null;
131
+ const auth = readTokens(active.name);
132
+ if (!auth || auth.refreshInvalidAt || (auth.siteUrl ? auth.siteUrl !== siteUrl && !(hosted && isHostedBackendUrl(auth.siteUrl)) : !hosted))
133
+ return fallback;
134
+ if (forceRefresh && auth.expiresAt > Date.now()) {
135
+ await refreshCliTelemetryConfig({
136
+ envName: active.name,
137
+ accessToken: auth.accessToken,
138
+ siteUrl: auth.siteUrl,
139
+ force: true
140
+ });
141
+ }
142
+ const cached = readCache(siteUrl);
143
+ return cached?.sentry === void 0 ? fallback : cached.sentry;
144
+ }
145
+ export {
146
+ refreshCliTelemetryConfig,
147
+ resolveCliSentryConfig
148
+ };
@@ -10,7 +10,7 @@ import {
10
10
  setSelectedOrg,
11
11
  storeServiceRefreshToken,
12
12
  writeTokens
13
- } from "./chunk-B3H2YWPJ.js";
13
+ } from "./chunk-HDAFEHUY.js";
14
14
  import "./chunk-MLKGABMK.js";
15
15
  export {
16
16
  SERVICE_TOKEN_LOGIN_USER_ID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fml-inc/fml",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "description": "FML CLI and agent tools",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "open": "^10.1.0",
34
34
  "ora": "^8.2.0",
35
35
  "picocolors": "^1.1.1",
36
- "smol-toml": "^1.7.0",
36
+ "smol-toml": "1.7.0",
37
37
  "zod": "4.1.12"
38
38
  },
39
39
  "devDependencies": {