@floomhq/floom 1.0.32 → 1.0.34

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/dist/errors.js CHANGED
@@ -28,6 +28,9 @@ export function friendlyHttp(status, action) {
28
28
  if (status === 413) {
29
29
  return new FloomError("That file is too large to publish.");
30
30
  }
31
+ if (status === 429) {
32
+ return new FloomError("Floom is rate limiting requests.", "The CLI retries rate-limited requests automatically. Try again in a moment if this persists.");
33
+ }
31
34
  if (status >= 500) {
32
35
  return new FloomError("Floom is having trouble right now.", "Try again in a moment.");
33
36
  }
package/dist/lib/api.js CHANGED
@@ -11,30 +11,39 @@ export async function floomFetch(url, action, opts = {}) {
11
11
  headers.authorization = `Bearer ${opts.token}`;
12
12
  if (opts.body !== undefined)
13
13
  headers["content-type"] = "application/json";
14
- const controller = new AbortController();
15
- const timer = setTimeout(() => controller.abort(), requestTimeoutMs(opts.timeoutMs));
16
- let res;
17
- try {
18
- res = await fetch(url, {
19
- method: opts.method ?? "GET",
20
- headers,
21
- signal: controller.signal,
22
- ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
23
- });
24
- }
25
- catch (err) {
26
- if (err.name === "AbortError") {
27
- throw new FloomError(`Request timed out while trying to ${action}.`, "Try again in a moment.");
14
+ const attempts = Math.max(1, 1 + rateLimitRetries(opts.rateLimitRetries));
15
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
16
+ const controller = new AbortController();
17
+ const timer = setTimeout(() => controller.abort(), requestTimeoutMs(opts.timeoutMs));
18
+ let res;
19
+ try {
20
+ res = await fetch(url, {
21
+ method: opts.method ?? "GET",
22
+ headers,
23
+ signal: controller.signal,
24
+ ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
25
+ });
28
26
  }
29
- throw friendlyNetwork(err);
30
- }
31
- finally {
32
- clearTimeout(timer);
33
- }
34
- if (opts.checkOk !== false && !res.ok) {
35
- throw friendlyHttp(res.status, action);
27
+ catch (err) {
28
+ if (err.name === "AbortError") {
29
+ throw new FloomError(`Request timed out while trying to ${action}.`, "Try again in a moment.");
30
+ }
31
+ throw friendlyNetwork(err);
32
+ }
33
+ finally {
34
+ clearTimeout(timer);
35
+ }
36
+ if (res.status === 429 && attempt < attempts - 1) {
37
+ await drainResponse(res);
38
+ await sleep(rateLimitDelayMs(res, attempt));
39
+ continue;
40
+ }
41
+ if (opts.checkOk !== false && !res.ok) {
42
+ throw friendlyHttp(res.status, action);
43
+ }
44
+ return res;
36
45
  }
37
- return res;
46
+ throw new FloomError(`Request failed (HTTP 429) while trying to ${action}.`);
38
47
  }
39
48
  function requestTimeoutMs(override) {
40
49
  if (override !== undefined)
@@ -44,6 +53,50 @@ function requestTimeoutMs(override) {
44
53
  return fromEnv;
45
54
  return DEFAULT_TIMEOUT_MS;
46
55
  }
56
+ function rateLimitRetries(override) {
57
+ if (override !== undefined)
58
+ return Math.max(0, Math.floor(override));
59
+ const fromEnv = Number(process.env.FLOOM_RATE_LIMIT_RETRIES);
60
+ if (Number.isFinite(fromEnv) && fromEnv >= 0)
61
+ return Math.floor(fromEnv);
62
+ return 8;
63
+ }
64
+ function rateLimitDelayMs(res, attempt) {
65
+ const retryAfter = res.headers.get("retry-after");
66
+ if (retryAfter) {
67
+ const seconds = Number(retryAfter);
68
+ if (Number.isFinite(seconds) && seconds >= 0) {
69
+ return capRateLimitDelay(seconds * 1000);
70
+ }
71
+ const dateMs = Date.parse(retryAfter);
72
+ if (Number.isFinite(dateMs)) {
73
+ return capRateLimitDelay(Math.max(0, dateMs - Date.now()));
74
+ }
75
+ }
76
+ const resetSeconds = Number(res.headers.get("x-ratelimit-reset"));
77
+ if (Number.isFinite(resetSeconds) && resetSeconds > 0) {
78
+ return capRateLimitDelay(Math.max(0, (resetSeconds * 1000) - Date.now()));
79
+ }
80
+ const base = Number(process.env.FLOOM_RATE_LIMIT_RETRY_MS);
81
+ const fallback = Number.isFinite(base) && base >= 0 ? base : 1_000;
82
+ return capRateLimitDelay(fallback * (attempt + 1));
83
+ }
84
+ function capRateLimitDelay(ms) {
85
+ const fromEnv = Number(process.env.FLOOM_RATE_LIMIT_MAX_WAIT_MS);
86
+ const max = Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : 65_000;
87
+ return Math.min(Math.max(0, ms), max);
88
+ }
89
+ async function drainResponse(res) {
90
+ try {
91
+ await res.arrayBuffer();
92
+ }
93
+ catch {
94
+ // Ignore bodies from rate-limit responses; the retry decision is header-based.
95
+ }
96
+ }
97
+ function sleep(ms) {
98
+ return new Promise((resolve) => setTimeout(resolve, ms));
99
+ }
47
100
  export async function getJson(url, action, token) {
48
101
  const res = await floomFetch(url, action, token ? { token } : {});
49
102
  return (await res.json());
package/dist/package.js CHANGED
@@ -216,7 +216,7 @@ async function collectPackageFiles(root) {
216
216
  continue;
217
217
  if (await isIgnored(entry.name))
218
218
  continue;
219
- if (entry.isDirectory() && await isNestedSkillDir(join(root, entry.name)))
219
+ if (entry.isDirectory() && await hasNestedSkillDir(join(root, entry.name)))
220
220
  continue;
221
221
  if (entry.isSymbolicLink())
222
222
  continue;
@@ -287,16 +287,39 @@ async function collectDir(root, dir, files, isIgnored, addBytes) {
287
287
  await collectFile(fullPath, rel, files, addBytes);
288
288
  }
289
289
  }
290
- async function isNestedSkillDir(path) {
290
+ async function hasNestedSkillDir(path) {
291
+ return hasNestedSkillDirInner(path, 0);
292
+ }
293
+ async function hasNestedSkillDirInner(path, depth) {
294
+ if (depth > 8)
295
+ return false;
291
296
  try {
292
297
  const stat = await lstat(join(path, "SKILL.md"));
293
- return stat.isFile();
298
+ if (stat.isFile())
299
+ return true;
300
+ }
301
+ catch (err) {
302
+ if (err.code !== "ENOENT")
303
+ throw err;
304
+ }
305
+ let entries;
306
+ try {
307
+ entries = await readdir(path, { withFileTypes: true });
294
308
  }
295
309
  catch (err) {
296
310
  if (err.code === "ENOENT")
297
311
  return false;
298
312
  throw err;
299
313
  }
314
+ for (const entry of entries) {
315
+ if (!entry.isDirectory() || entry.isSymbolicLink())
316
+ continue;
317
+ if (isGeneratedPackageEntry(entry.name, true))
318
+ continue;
319
+ if (await hasNestedSkillDirInner(join(path, entry.name), depth + 1))
320
+ return true;
321
+ }
322
+ return false;
300
323
  }
301
324
  async function collectFile(fullPath, rel, files, addBytes) {
302
325
  const normalized = rel.split(sep).join("/");
package/dist/secrets.js CHANGED
@@ -23,7 +23,6 @@ const PROMPT_INJECTION_PATTERNS = [
23
23
  ];
24
24
  const DATA_EXFILTRATION_PATTERNS = [
25
25
  { label: "Data exfiltration instruction", regex: /\b(?:send|post|upload|exfiltrate|copy) (?:[^.\n]{0,80})\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b(?:[^.\n]{0,120})\b(?:to|into) https?:\/\//gi },
26
- { label: "Data exfiltration instruction", regex: /\b(?:curl|wget|fetch)\b[^\n]{0,160}\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b/gi },
27
26
  { label: "Credential harvesting instruction", regex: /\b(?:collect|harvest|steal|extract) (?:[^.\n]{0,80})\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b/gi },
28
27
  ];
29
28
  function redact(value) {
@@ -46,6 +45,9 @@ function pushFinding(findings, seen, label, line, value) {
46
45
  seen.add(key);
47
46
  findings.push({ label, line, preview: redact(value) });
48
47
  }
48
+ function isDocumentedEnvReference(value) {
49
+ return /\b(?:process\.env|import\.meta\.env|os\.environ)\b/.test(value);
50
+ }
49
51
  export function detectSecrets(input) {
50
52
  const findings = [];
51
53
  const seen = new Set();
@@ -61,6 +63,8 @@ export function detectSecrets(input) {
61
63
  const value = match[1] ?? "";
62
64
  if (!value || PLACEHOLDER_RE.test(value))
63
65
  continue;
66
+ if (isDocumentedEnvReference(value))
67
+ continue;
64
68
  pushFinding(findings, seen, "Possible secret assignment", lineNumberAt(input, match.index ?? 0), value);
65
69
  }
66
70
  PROVIDER_LIKE_ASSIGNMENT_RE.lastIndex = 0;
@@ -68,6 +72,8 @@ export function detectSecrets(input) {
68
72
  const value = match[1] ?? "";
69
73
  if (!value)
70
74
  continue;
75
+ if (isDocumentedEnvReference(value))
76
+ continue;
71
77
  pushFinding(findings, seen, "Provider-like secret assignment", lineNumberAt(input, match.index ?? 0), value);
72
78
  }
73
79
  return findings.sort((a, b) => a.line - b.line || a.label.localeCompare(b.label));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
4
4
  "description": "Sync AI skills across agents and machines.",
5
5
  "license": "MIT",
6
6
  "type": "module",