@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 +3 -0
- package/dist/lib/api.js +75 -22
- package/dist/package.js +26 -3
- package/dist/secrets.js +7 -1
- package/package.json +1 -1
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
res
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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));
|