Skip to content

Commit 6ef7494

Browse files
committed
feat(dashboard): paginate accounts and harden release scans
1 parent da95565 commit 6ef7494

15 files changed

Lines changed: 817 additions & 107 deletions

.github/workflows/release.yml

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,30 @@ on:
55
tags: ['v*']
66

77
jobs:
8-
# ──────────────────────────────────────────────
9-
# Job 1: Build & Push Docker Image (GHCR)
10-
# ──────────────────────────────────────────────
8+
test:
9+
name: Test
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Setup Node.js
19+
uses: actions/setup-node@v4
20+
with:
21+
node-version: '20'
22+
23+
- name: Secret scan
24+
run: npm run secret-scan
25+
26+
- name: Run tests
27+
run: node --test test/*.test.js
28+
1129
docker:
1230
name: Docker Build & Push
31+
needs: test
1332
runs-on: ubuntu-latest
1433
permissions:
1534
contents: read
@@ -19,6 +38,17 @@ jobs:
1938
- name: Checkout
2039
uses: actions/checkout@v4
2140

41+
- name: Build metadata
42+
run: |
43+
echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
44+
echo "COMMIT_DATE=$(git log -1 --pretty=%cI)" >> "$GITHUB_ENV"
45+
echo "BUILD_BRANCH=${GITHUB_REF_NAME}" >> "$GITHUB_ENV"
46+
{
47+
echo "COMMIT_MESSAGE<<EOF"
48+
git log -1 --pretty=%s
49+
echo "EOF"
50+
} >> "$GITHUB_ENV"
51+
2252
- name: Set up Docker Buildx
2353
uses: docker/setup-buildx-action@v3
2454

@@ -55,21 +85,25 @@ jobs:
5585
labels: ${{ steps.meta.outputs.labels }}
5686
cache-from: type=gha
5787
cache-to: type=gha,mode=max
88+
build-args: |
89+
BUILD_VERSION=${{ env.VERSION }}
90+
BUILD_COMMIT=${{ github.sha }}
91+
BUILD_COMMIT_MESSAGE=${{ env.COMMIT_MESSAGE }}
92+
BUILD_COMMIT_DATE=${{ env.COMMIT_DATE }}
93+
BUILD_BRANCH=${{ env.BUILD_BRANCH }}
5894
5995
- name: Summary
6096
run: |
61-
echo "## 🐳 Docker Image Published" >> "$GITHUB_STEP_SUMMARY"
97+
echo "## Docker Image Published" >> "$GITHUB_STEP_SUMMARY"
6298
echo "" >> "$GITHUB_STEP_SUMMARY"
6399
echo "**Tags:**" >> "$GITHUB_STEP_SUMMARY"
64100
echo '${{ steps.meta.outputs.tags }}' | tr ',' '\n' | sed 's/^/- `/' | sed 's/$/`/' >> "$GITHUB_STEP_SUMMARY"
65101
echo "" >> "$GITHUB_STEP_SUMMARY"
66102
echo "**Platform:** \`linux/amd64\`" >> "$GITHUB_STEP_SUMMARY"
67103
68-
# ──────────────────────────────────────────────
69-
# Job 2: Create GitHub Release with source code
70-
# ──────────────────────────────────────────────
71104
release:
72105
name: GitHub Release
106+
needs: docker
73107
runs-on: ubuntu-latest
74108
permissions:
75109
contents: write

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ scripts/*
1010
!scripts/special-agent-smoke.mjs
1111
!scripts/lsp-capacity-matrix.mjs
1212
!scripts/web-search-direct-probe.mjs
13+
!scripts/secret-scan.mjs
1314
src/get-token.js
1415
src/test-cascade.js
1516
src/runtime-config.json

docs/native-bridge-protocol-notes.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,19 @@ hashes, and safe classifications such as `looksPathLike` and
149149
`looksPromptLike`. Do not use the global raw-string trace switch for production
150150
traffic; it can capture prompts.
151151

152+
v2.0.137 adds a parser-aligned evidence block at
153+
`semantic.steps[].readWrapperField19.candidateSummary`:
154+
155+
- `acceptedField` = the field current production parsing would accept
156+
(`1`/`2` only, and only when path-like), otherwise `null`
157+
- `pathLikeFields` = fields whose string payload looks like a path/file URI
158+
- `rejectedPromptFields` = prompt-like string fields not accepted by the parser
159+
- `ambiguous` = more than one accepted field, which is a trace investigation
160+
signal rather than a license to widen production parsing
161+
162+
This lets a gated canary answer "which field would the current Read parser
163+
use?" without printing the actual file path or prompt text.
164+
152165
Error trajectory steps also have a dedicated redacted summary. For `type=17`
153166
or any step carrying `error_message` field `24` / `error` field `31`, traces
154167
now expose `semantic.steps[].errorStep` with source field numbers, byte
@@ -338,10 +351,27 @@ User settings that influence auto-approval:
338351

339352
`WINDSURFAPI_PROTO_TRACE` now summarizes `requested_interaction=56` and its
340353
read-url body with byte lengths and hashes only. It also summarizes
341-
`HandleCascadeUserInteraction` requests, including cascade/trajectory ID hashes,
342-
step index, action enum, and URL/origin hashes. The next valid WebFetch canary
343-
decision point is: after approval, did the LS emit a completed `field=40` step
344-
with `web_document`, an error step, or another requested interaction?
354+
`HandleCascadeUserInteraction` requests, including cascade/trajectory ID
355+
hashes, step index, action enum, and URL/origin hashes.
356+
357+
v2.0.137 adds
358+
`semantic.steps[].webFetchTrace` so a canary can classify the post-approval
359+
trajectory without raw URL text:
360+
361+
- `pending_permission`
362+
- `completed_web_document`
363+
- `auto_run_decision_only`
364+
- `legacy_summary_only`
365+
- `native_oneof_no_document`
366+
- `error` with redacted `permissionDenied` / `failedPrecondition` style flags
367+
368+
Use this field after `HandleCascadeUserInteraction` to decide whether the LS
369+
advanced to a real `read_url_content.web_document`, repeated the permission
370+
prompt, or failed a precondition.
371+
372+
The next valid WebFetch canary decision point is: after approval, did the LS
373+
emit a completed `field=40` step with `web_document`, an error step, or another
374+
requested interaction?
345375

346376
v2.0.135 adds a lab-only auto-approval hook for this canary:
347377

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# v2.0.137 - dashboard pagination and protocol trace evidence
2+
3+
## What changed
4+
5+
- Dashboard accounts now load a lightweight paged summary list instead of the full heavy account payload.
6+
- Account row expansion and model block editing now lazy-load one full account detail via `GET /dashboard/api/accounts/:id`.
7+
- The abnormal accounts panel now loads only flagged summary rows and uses server-side account stats.
8+
- Added safe Read wrapper trace evidence for `type=14 / field=19`: accepted field, path-like fields, prompt-like rejected fields, and ambiguity, without raw path or prompt text by default.
9+
- Added WebFetch trajectory branch evidence for pending permission, completed `web_document`, auto-run-only, legacy-summary-only, and permission/precondition error states.
10+
- Replaced real-looking email/password examples in `docs/releases/RELEASE_NOTES_2.0.39.md` with non-login placeholders.
11+
- Added `scripts/secret-scan.mjs` and `npm run secret-scan` for tracked-file secret scanning.
12+
- Secret scan findings print only `path:line rule`; matched secret values are never printed.
13+
- Added regression tests for secret-scan output redaction and release workflow ordering.
14+
- Release workflow now runs tests first, makes Docker depend on tests, and makes GitHub Release depend on Docker.
15+
- Docker builds now receive `BUILD_VERSION`, `BUILD_COMMIT`, `BUILD_COMMIT_MESSAGE`, `BUILD_COMMIT_DATE`, and `BUILD_BRANCH`.
16+
17+
## Validation
18+
19+
- `npm.cmd run secret-scan`
20+
- `node --test test/dashboard-api.test.js`
21+
- `node --test test/proto-trace.test.js`
22+
- `node --test test/secret-scan.test.js test/release-workflow.test.js`
23+
- `node --test test/*.test.js`

docs/releases/RELEASE_NOTES_2.0.39.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ VPS 实测 `/auth/login` 邮箱密码模式提交三个真实账号:
77
```
88
POST /auth/login
99
{ "accounts":[
10-
{"email":"qxl.n257570.722@gmail.com","password":"..."},
11-
{"email":"tc.vmb.7687617@gmail.com","password":"..."},
12-
{"email":"ge.jgpmnelfiyo9.15@gmail.com","password":"..."}
10+
{"email":"user1@example.com","password":"<password>"},
11+
{"email":"user2@example.com","password":"<password>"},
12+
{"email":"user3@example.com","password":"<password>"}
1313
]}
1414
```
1515

@@ -55,7 +55,7 @@ throw new Error('Direct email/password login is not supported. Use token-based a
5555
```bash
5656
$ curl -X POST https://windsurf.com/_backend/exa.seat_management_pb.SeatManagementService/CheckUserLoginMethod \
5757
-H 'Content-Type: application/json' -H 'Connect-Protocol-Version: 1' \
58-
-d '{"email":"qxl.n257570.722@gmail.com"}'
58+
-d '{"email":"user1@example.com"}'
5959
{}
6060
```
6161

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "windsurf-api",
3-
"version": "2.0.136",
3+
"version": "2.0.137",
44
"description": "Windsurf to OpenAI + Anthropic compatible API proxy. Turns Windsurf's 107 AI models (Claude, GPT, Gemini, DeepSeek, Grok, Qwen, Kimi, GLM, SWE) into dual-protocol API endpoints. Zero npm deps.",
55
"type": "module",
66
"main": "src/index.js",
@@ -11,7 +11,8 @@
1111
"smoke:native-bridge": "node scripts/native-bridge-smoke.mjs",
1212
"smoke:special-agent": "node scripts/special-agent-smoke.mjs",
1313
"smoke:lsp-matrix": "node scripts/lsp-capacity-matrix.mjs",
14-
"probe:web-search": "node scripts/web-search-direct-probe.mjs"
14+
"probe:web-search": "node scripts/web-search-direct-probe.mjs",
15+
"secret-scan": "node scripts/secret-scan.mjs"
1516
},
1617
"engines": {
1718
"node": ">=20.0.0"

scripts/secret-scan.mjs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env node
2+
import { execFileSync } from 'node:child_process';
3+
import { existsSync, readFileSync, statSync } from 'node:fs';
4+
import { relative, resolve, sep } from 'node:path';
5+
import { fileURLToPath } from 'node:url';
6+
7+
const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
8+
const args = process.argv.slice(2);
9+
10+
const RULES = [
11+
{
12+
id: 'openai-api-key',
13+
regex: /sk-[A-Za-z0-9_-]{20,}/g,
14+
},
15+
{
16+
id: 'literal-credential-assignment',
17+
regex: /\b(?:secret|token|password)\b\s*[:=]\s*["'][A-Za-z0-9_./+=-]{16,}["']/gi,
18+
},
19+
{
20+
id: 'private-key-block',
21+
regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g,
22+
},
23+
{
24+
id: 'credentialed-email-example',
25+
regex: /[A-Za-z0-9._%+-]+@(?!example\.(?:com|org|net)\b)[A-Za-z0-9.-]+\.[A-Za-z]{2,}["']?\s*,\s*["']?password["']?\s*:/gi,
26+
},
27+
];
28+
29+
const IGNORED_PATHS = new Set([
30+
'scripts/secret-scan.mjs',
31+
'test/secret-scan.test.js',
32+
]);
33+
34+
const IGNORED_PREFIXES = [
35+
'test/',
36+
'test/_research/',
37+
];
38+
39+
const IGNORED_EXTENSIONS = new Set([
40+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.zip', '.db',
41+
]);
42+
43+
function toRepoPath(file) {
44+
return relative(root, resolve(root, file)).split(sep).join('/');
45+
}
46+
47+
function isIgnored(file) {
48+
const repoPath = toRepoPath(file);
49+
if (!repoPath || repoPath.startsWith('..') || repoPath.includes('\0')) return true;
50+
if (IGNORED_PATHS.has(repoPath)) return true;
51+
if (IGNORED_PREFIXES.some(prefix => repoPath.startsWith(prefix))) return true;
52+
const lower = repoPath.toLowerCase();
53+
return [...IGNORED_EXTENSIONS].some(ext => lower.endsWith(ext));
54+
}
55+
56+
function trackedFiles() {
57+
const output = execFileSync('git', ['ls-files', '-z'], {
58+
cwd: root,
59+
encoding: 'utf8',
60+
maxBuffer: 16 * 1024 * 1024,
61+
});
62+
return output.split('\0').filter(Boolean);
63+
}
64+
65+
function inputFiles() {
66+
if (args.length) return args;
67+
return trackedFiles();
68+
}
69+
70+
function lineForOffset(text, offset) {
71+
let line = 1;
72+
for (let i = 0; i < offset; i += 1) {
73+
if (text.charCodeAt(i) === 10) line += 1;
74+
}
75+
return line;
76+
}
77+
78+
function scanFile(file) {
79+
if (isIgnored(file)) return [];
80+
const abs = resolve(root, file);
81+
if (!existsSync(abs) || !statSync(abs).isFile()) return [];
82+
const text = readFileSync(abs, 'utf8');
83+
const findings = [];
84+
for (const rule of RULES) {
85+
rule.regex.lastIndex = 0;
86+
for (const match of text.matchAll(rule.regex)) {
87+
findings.push({
88+
path: toRepoPath(file),
89+
line: lineForOffset(text, match.index || 0),
90+
rule: rule.id,
91+
});
92+
}
93+
}
94+
return findings;
95+
}
96+
97+
const findings = inputFiles().flatMap(scanFile)
98+
.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.rule.localeCompare(b.rule));
99+
100+
for (const finding of findings) {
101+
console.log(`${finding.path}:${finding.line} ${finding.rule}`);
102+
}
103+
104+
if (findings.length) process.exitCode = 1;

0 commit comments

Comments
 (0)