flowcollab 0.3.5 → 0.3.6
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/bin/_client.mjs +12 -6
- package/bin/flow.mjs +6 -1
- package/bin/scan.mjs +17 -13
- package/package.json +1 -1
package/bin/_client.mjs
CHANGED
|
@@ -144,12 +144,18 @@ export async function resolveTaskId(input) {
|
|
|
144
144
|
if (!input) return input;
|
|
145
145
|
const stripped = input.replace(/^#/, '');
|
|
146
146
|
if (UUID_RE.test(stripped)) return stripped;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
// Paginate through all tasks (boards > 500 tasks would silently miss with a single page)
|
|
148
|
+
let allTasks = [];
|
|
149
|
+
let cursor = null;
|
|
150
|
+
do {
|
|
151
|
+
const url = '/api/flow/tasks?limit=200' + (cursor ? `&cursor=${encodeURIComponent(cursor)}` : '');
|
|
152
|
+
const page = await flowFetch(url);
|
|
153
|
+
const items = Array.isArray(page) ? page : (page.tasks ?? []);
|
|
154
|
+
allTasks = allTasks.concat(items);
|
|
155
|
+
cursor = page.has_more ? page.next_cursor : null;
|
|
156
|
+
} while (cursor);
|
|
157
|
+
const decRes = await flowFetch('/api/flow/decisions');
|
|
158
|
+
const taskMatches = allTasks.filter(t => typeof t.id === 'string' && t.id.startsWith(stripped));
|
|
153
159
|
if (taskMatches.length === 1) return taskMatches[0].id;
|
|
154
160
|
if (taskMatches.length > 1) throw new Error(`Ambiguous prefix "${stripped}" matches ${taskMatches.length} tasks:\n${taskMatches.map(t => ` #${t.id.slice(0, 6)} ${t.title}`).join('\n')}`);
|
|
155
161
|
const decisions = decRes.decisions || [];
|
package/bin/flow.mjs
CHANGED
|
@@ -105,4 +105,9 @@ if (!target) {
|
|
|
105
105
|
// read [--focus] — identical to what flow-pull sees when called directly.
|
|
106
106
|
process.argv.splice(2, 1);
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
try {
|
|
109
|
+
await import(new URL(target, import.meta.url));
|
|
110
|
+
} catch (e) {
|
|
111
|
+
process.stderr.write(`flow ${sub}: ${e.message || e}\n`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
package/bin/scan.mjs
CHANGED
|
@@ -68,14 +68,21 @@ function scanTodos(files, dir) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// ── GitHub Issues scan ─────────────────────────────────────────────────────────
|
|
71
|
+
async function fetchGitHub(url) {
|
|
72
|
+
const res = await fetch(url, { headers: githubHeaders() });
|
|
73
|
+
if (res.status === 429 || res.status === 403) {
|
|
74
|
+
const reset = res.headers.get('x-ratelimit-reset');
|
|
75
|
+
const wait = reset ? new Date(Number(reset) * 1000).toLocaleTimeString() : 'soon';
|
|
76
|
+
throw new Error(`GitHub rate limit exceeded — resets at ${wait}`);
|
|
77
|
+
}
|
|
78
|
+
if (!res.ok) throw new Error(`GitHub API ${res.status}: ${res.statusText}`);
|
|
79
|
+
return res.json();
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
async function fetchGitHubIssues(repo) {
|
|
72
83
|
const all = [];
|
|
73
84
|
for (let page = 1; page <= 5; page++) {
|
|
74
|
-
const
|
|
75
|
-
headers: githubHeaders(),
|
|
76
|
-
});
|
|
77
|
-
if (!res.ok) throw new Error(`GitHub API ${res.status}`);
|
|
78
|
-
const batch = await res.json();
|
|
85
|
+
const batch = await fetchGitHub(`https://api.github.com/repos/${repo}/issues?state=open&per_page=100&page=${page}`);
|
|
79
86
|
all.push(...batch.filter(i => !i.pull_request));
|
|
80
87
|
if (batch.length < 100) break;
|
|
81
88
|
}
|
|
@@ -105,11 +112,7 @@ async function scanIssues(repo) {
|
|
|
105
112
|
async function fetchGitHubPRs(repo) {
|
|
106
113
|
const all = [];
|
|
107
114
|
for (let page = 1; page <= 3; page++) {
|
|
108
|
-
const
|
|
109
|
-
headers: githubHeaders(),
|
|
110
|
-
});
|
|
111
|
-
if (!res.ok) throw new Error(`GitHub API ${res.status}`);
|
|
112
|
-
const batch = await res.json();
|
|
115
|
+
const batch = await fetchGitHub(`https://api.github.com/repos/${repo}/pulls?state=open&per_page=100&page=${page}`);
|
|
113
116
|
all.push(...batch);
|
|
114
117
|
if (batch.length < 100) break;
|
|
115
118
|
}
|
|
@@ -136,14 +139,15 @@ async function scanPRs(repo) {
|
|
|
136
139
|
|
|
137
140
|
// ── Security scan ──────────────────────────────────────────────────────────────
|
|
138
141
|
const SEC_PATTERNS = [
|
|
139
|
-
{ re: /(['"`])(sk_live_|AKIA[0-9A-Z]{16}|ghp_[0-9A-Za-z]{36})[0-9A-Za-z]
|
|
142
|
+
{ re: /(['"`])(sk_live_|AKIA[0-9A-Z]{16}|ghp_[0-9A-Za-z]{36}|github_pat_[0-9A-Za-z]{36}|glpat-[0-9A-Za-z]{20}|xox[bpas]-[0-9A-Za-z-]{10,})[0-9A-Za-z-]*\1/i, label: 'Hardcoded secret (live key pattern)' },
|
|
140
143
|
{ re: /\beval\s*\(/, label: 'eval() usage (code injection risk)' },
|
|
141
144
|
{ re: /dangerouslySetInnerHTML/, label: 'dangerouslySetInnerHTML (XSS risk)' },
|
|
142
|
-
{ re: /child_process.*\.exec\s*\(/,
|
|
145
|
+
{ re: /(?:child_process.*\.(?:exec|execSync|spawn|spawnSync)\s*\(|execSync\s*\(|require\(['"]child_process['"]\))/, label: 'exec/spawn call (command injection risk)' },
|
|
143
146
|
{ re: /\.query\s*\(`[^`]*\$\{/, label: 'Template literal in SQL query (injection risk)' },
|
|
147
|
+
{ re: /['"`]\s*\+\s*(?:req\.|request\.|body\.|params\.|query\.|input)/, label: 'String concatenation in query (injection risk)' },
|
|
144
148
|
{ re: /new\s+Function\s*\(/, label: 'new Function() (eval-equivalent)' },
|
|
145
149
|
{ re: /require\s*\(\s*(?:req\.|request\.|body\.|params\.|query\.)/, label: 'Dynamic require() from request input' },
|
|
146
|
-
{ re: /(?:password|secret|apikey)\s*(?:=|:)\s*['"`][^'"`]{6,}/i,
|
|
150
|
+
{ re: /(?:password|secret|apikey|api_key|access_token)\s*(?:=|:)\s*['"`][^'"`]{6,}/i, label: 'Possible hardcoded credential' },
|
|
147
151
|
];
|
|
148
152
|
|
|
149
153
|
function scanSecurity(files, dir) {
|