@cavuno/board 1.39.0 → 1.41.0
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/{_spec-DxC1ze93.d.mts → _spec-l_596ZwW.d.mts} +373 -0
- package/dist/{_spec-DxC1ze93.d.ts → _spec-l_596ZwW.d.ts} +373 -0
- package/dist/bin.mjs +242 -18
- package/dist/{board-RfZEAJse.d.ts → board-BzK5j73O.d.ts} +1 -1
- package/dist/{board-CqYibYUA.d.mts → board-CyHA6rRP.d.mts} +1 -1
- package/dist/doctor.js +238 -14
- package/dist/doctor.mjs +238 -14
- package/dist/filters.d.mts +2 -2
- package/dist/filters.d.ts +2 -2
- package/dist/format.d.mts +3 -3
- package/dist/format.d.ts +3 -3
- package/dist/index.d.mts +89 -10
- package/dist/index.d.ts +89 -10
- package/dist/index.js +91 -1
- package/dist/index.mjs +91 -1
- package/dist/{jobs-DPPA1Nev.d.mts → jobs-BhthvMkA.d.mts} +1 -1
- package/dist/{jobs-CLLIvtMc.d.ts → jobs-CXudz1Y4.d.ts} +1 -1
- package/dist/{salaries-DK4RnJnw.d.ts → salaries-ByBuHCLH.d.ts} +2 -2
- package/dist/{salaries-Rb5h_eVZ.d.mts → salaries-Dr_Zugal.d.mts} +2 -2
- package/dist/{search-CqBa1Qc4.d.mts → search-CrIlXSJP.d.mts} +1 -1
- package/dist/{search-DBoMM-gE.d.ts → search-DZrq-k76.d.ts} +1 -1
- package/dist/seo.d.mts +4 -4
- package/dist/seo.d.ts +4 -4
- package/dist/server.d.mts +56 -6
- package/dist/server.d.ts +56 -6
- package/dist/server.js +41 -0
- package/dist/server.mjs +41 -0
- package/dist/sitemap.d.mts +5 -5
- package/dist/sitemap.d.ts +5 -5
- package/dist/suggest.d.mts +2 -2
- package/dist/suggest.d.ts +2 -2
- package/dist/theme.d.mts +49 -1
- package/dist/theme.d.ts +49 -1
- package/dist/theme.js +60 -26
- package/dist/theme.mjs +60 -26
- package/package.json +1 -1
- package/skills/manifest.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -104,6 +104,156 @@ function summarize(results) {
|
|
|
104
104
|
return { exitCode: failed.length > 0 ? 1 : 0, passed, failed, skipped };
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// src/doctor/cookie-conformance.ts
|
|
108
|
+
import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
109
|
+
import { join, relative } from "path";
|
|
110
|
+
var COOKIE = record("static.cookie-codec", 1);
|
|
111
|
+
var SOURCE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|astro|svelte|vue)$/;
|
|
112
|
+
var REMEDIATION = "set cookies only through the SDK server cookie codec (buildCookie/buildClearCookie from @cavuno/board/server)";
|
|
113
|
+
var SET_COOKIE_SINK = /set-cookie/i;
|
|
114
|
+
var DOCUMENT_COOKIE_ASSIGN = /document\.cookie\s*\+?=(?!=)/i;
|
|
115
|
+
var DOMAIN_SCOPE = /domain\s*=/i;
|
|
116
|
+
var LOOKAHEAD_LINES = 3;
|
|
117
|
+
function stripComments(line) {
|
|
118
|
+
let out = "";
|
|
119
|
+
let i = 0;
|
|
120
|
+
let quote = null;
|
|
121
|
+
while (i < line.length) {
|
|
122
|
+
const c = line[i];
|
|
123
|
+
if (quote !== null) {
|
|
124
|
+
out += c;
|
|
125
|
+
if (c === "\\" && i + 1 < line.length) {
|
|
126
|
+
out += line[i + 1];
|
|
127
|
+
i += 2;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (c === quote) quote = null;
|
|
131
|
+
i += 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
135
|
+
quote = c;
|
|
136
|
+
out += c;
|
|
137
|
+
i += 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (c === "/" && i + 1 < line.length && line[i - 1] !== "\\") {
|
|
141
|
+
const next = line[i + 1];
|
|
142
|
+
if (next === "/") {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
if (next === "*") {
|
|
146
|
+
i += 2;
|
|
147
|
+
while (i < line.length) {
|
|
148
|
+
if (line[i] === "*" && i + 1 < line.length && line[i + 1] === "/") {
|
|
149
|
+
i += 2;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
i += 1;
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
out += c;
|
|
158
|
+
i += 1;
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
function isCommentLine(line) {
|
|
163
|
+
const trimmed = line.trim();
|
|
164
|
+
if (trimmed.startsWith("*")) return true;
|
|
165
|
+
return stripComments(line).trim() === "";
|
|
166
|
+
}
|
|
167
|
+
function hasCookieWriteSink(line) {
|
|
168
|
+
return SET_COOKIE_SINK.test(line) || DOCUMENT_COOKIE_ASSIGN.test(line);
|
|
169
|
+
}
|
|
170
|
+
function statementWindow(lines, sinkIdx) {
|
|
171
|
+
const sinkStripped = stripComments(lines[sinkIdx]);
|
|
172
|
+
const parts = [sinkStripped];
|
|
173
|
+
if (sinkStripped.includes(";")) return sinkStripped;
|
|
174
|
+
let taken = 0;
|
|
175
|
+
for (let j = sinkIdx + 1; j < lines.length && taken < LOOKAHEAD_LINES; j += 1) {
|
|
176
|
+
const next = lines[j];
|
|
177
|
+
if (isCommentLine(next)) continue;
|
|
178
|
+
const stripped = stripComments(next);
|
|
179
|
+
parts.push(stripped);
|
|
180
|
+
taken += 1;
|
|
181
|
+
if (stripped.includes(";")) break;
|
|
182
|
+
}
|
|
183
|
+
return parts.join("\n");
|
|
184
|
+
}
|
|
185
|
+
function isOffendingSink(lines, index) {
|
|
186
|
+
const line = lines[index];
|
|
187
|
+
if (isCommentLine(line)) return false;
|
|
188
|
+
const stripped = stripComments(line);
|
|
189
|
+
if (!hasCookieWriteSink(stripped)) return false;
|
|
190
|
+
return DOMAIN_SCOPE.test(statementWindow(lines, index));
|
|
191
|
+
}
|
|
192
|
+
function walkSourceFiles(dir, out) {
|
|
193
|
+
let entries;
|
|
194
|
+
try {
|
|
195
|
+
entries = readdirSync(dir);
|
|
196
|
+
} catch {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
for (const name of entries) {
|
|
200
|
+
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
201
|
+
const full = join(dir, name);
|
|
202
|
+
let st;
|
|
203
|
+
try {
|
|
204
|
+
st = statSync(full);
|
|
205
|
+
} catch {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (st.isDirectory()) {
|
|
209
|
+
walkSourceFiles(full, out);
|
|
210
|
+
} else if (st.isFile() && SOURCE_EXT.test(name)) {
|
|
211
|
+
out.push(full);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function truncate(line, max = 120) {
|
|
216
|
+
const t = line.trim();
|
|
217
|
+
return t.length > max ? `${t.slice(0, max)}\u2026` : t;
|
|
218
|
+
}
|
|
219
|
+
function checkCookieCodecConformance(projectRoot) {
|
|
220
|
+
const srcDir = join(projectRoot, "src");
|
|
221
|
+
if (!existsSync(srcDir)) {
|
|
222
|
+
return [
|
|
223
|
+
COOKIE(
|
|
224
|
+
"skip",
|
|
225
|
+
"no src/ directory \u2014 cookie-codec conformance scan skipped"
|
|
226
|
+
)
|
|
227
|
+
];
|
|
228
|
+
}
|
|
229
|
+
const files = [];
|
|
230
|
+
walkSourceFiles(srcDir, files);
|
|
231
|
+
const findings = [];
|
|
232
|
+
for (const file of files) {
|
|
233
|
+
const rel = relative(projectRoot, file).split("\\").join("/");
|
|
234
|
+
let text;
|
|
235
|
+
try {
|
|
236
|
+
text = readFileSync2(file, "utf8");
|
|
237
|
+
} catch {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const lines = text.split(/\r?\n/);
|
|
241
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
242
|
+
if (!isOffendingSink(lines, i)) continue;
|
|
243
|
+
findings.push(`${rel}:${i + 1} \u2014 ${truncate(lines[i])}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (findings.length === 0) {
|
|
247
|
+
return [
|
|
248
|
+
COOKIE(
|
|
249
|
+
"pass",
|
|
250
|
+
"no domain-scoped Set-Cookie or document.cookie writes in src/"
|
|
251
|
+
)
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
return [COOKIE("fail", `${findings.join("; ")}; ${REMEDIATION}`)];
|
|
255
|
+
}
|
|
256
|
+
|
|
107
257
|
// src/doctor/probe.ts
|
|
108
258
|
var FETCH_TIMEOUT_MS = 1e4;
|
|
109
259
|
async function probe(fetchImpl, url) {
|
|
@@ -223,14 +373,82 @@ async function runReadProbes(fetchImpl, frontendUrl) {
|
|
|
223
373
|
];
|
|
224
374
|
}
|
|
225
375
|
|
|
376
|
+
// src/doctor/sandbox.ts
|
|
377
|
+
var SANDBOX_GATE = record("sandbox.persona-gate", 1);
|
|
378
|
+
var EXPECTED_PERSONA_COUNT = 8;
|
|
379
|
+
function skipSandboxPersonaGate(reason) {
|
|
380
|
+
return SANDBOX_GATE("skip", reason);
|
|
381
|
+
}
|
|
382
|
+
async function checkSandboxPersonaGate(fetchImpl, env, boardContext) {
|
|
383
|
+
if (boardContext === null) {
|
|
384
|
+
return SANDBOX_GATE(
|
|
385
|
+
"skip",
|
|
386
|
+
"board did not resolve \u2014 fix static.board first"
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
const url = `${apiBase(env.apiUrl)}/v1/boards/${encodeURIComponent(env.boardKey)}/sandbox/personas`;
|
|
390
|
+
const result = await probe(fetchImpl, url);
|
|
391
|
+
if (boardContext.sandbox !== true) {
|
|
392
|
+
return result.status === 404 ? SANDBOX_GATE(
|
|
393
|
+
"pass",
|
|
394
|
+
"sandbox persona endpoint refuses (404) on a non-sandbox board"
|
|
395
|
+
) : SANDBOX_GATE(
|
|
396
|
+
"fail",
|
|
397
|
+
`sandbox persona endpoint is reachable on a NON-sandbox board (HTTP ${result.status}) \u2014 the capability gate is not holding`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
if (!result.ok) {
|
|
401
|
+
return SANDBOX_GATE(
|
|
402
|
+
"fail",
|
|
403
|
+
`sandbox persona roster did not resolve (HTTP ${result.status}) \u2014 the persona seed may have failed`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
let personas;
|
|
407
|
+
let password;
|
|
408
|
+
try {
|
|
409
|
+
const parsed = JSON.parse(result.body);
|
|
410
|
+
personas = parsed.personas;
|
|
411
|
+
password = parsed.password;
|
|
412
|
+
} catch {
|
|
413
|
+
return SANDBOX_GATE(
|
|
414
|
+
"fail",
|
|
415
|
+
"sandbox persona endpoint returned 200 but not JSON \u2014 proxy or captive portal in the way?"
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (!Array.isArray(personas) || personas.length !== EXPECTED_PERSONA_COUNT) {
|
|
419
|
+
return SANDBOX_GATE(
|
|
420
|
+
"fail",
|
|
421
|
+
`expected ${EXPECTED_PERSONA_COUNT} personas, got ${Array.isArray(personas) ? personas.length : "a non-array"} \u2014 the persona seed is incomplete`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
const unseeded = personas.filter((persona) => persona.seeded !== true);
|
|
425
|
+
if (unseeded.length > 0) {
|
|
426
|
+
const ids = unseeded.map((persona) => typeof persona.id === "string" ? persona.id : "?").join(", ");
|
|
427
|
+
return SANDBOX_GATE(
|
|
428
|
+
"fail",
|
|
429
|
+
`${unseeded.length} of ${EXPECTED_PERSONA_COUNT} personas are not seeded on the board (${ids}) \u2014 the persona seed failed or was purged; run POST /v1/boards/{key}/sandbox/reseed to restore the roster`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
if (typeof password !== "string" || password.length === 0) {
|
|
433
|
+
return SANDBOX_GATE(
|
|
434
|
+
"fail",
|
|
435
|
+
"roster present but the published password is missing"
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
return SANDBOX_GATE(
|
|
439
|
+
"pass",
|
|
440
|
+
`sandbox roster complete and seeded (${EXPECTED_PERSONA_COUNT} personas)`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
226
444
|
// src/doctor/theme.ts
|
|
227
445
|
import { createHash } from "crypto";
|
|
228
|
-
import { existsSync, readFileSync as
|
|
229
|
-
import { join } from "path";
|
|
446
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
447
|
+
import { join as join2 } from "path";
|
|
230
448
|
var THEME = record("static.theme", 1);
|
|
231
449
|
function checkThemeFreshness(projectRoot, context) {
|
|
232
|
-
const tokensPath =
|
|
233
|
-
if (!
|
|
450
|
+
const tokensPath = join2(projectRoot, "src/tokens.css");
|
|
451
|
+
if (!existsSync2(tokensPath)) {
|
|
234
452
|
return [
|
|
235
453
|
THEME(
|
|
236
454
|
"skip",
|
|
@@ -238,9 +456,9 @@ function checkThemeFreshness(projectRoot, context) {
|
|
|
238
456
|
)
|
|
239
457
|
];
|
|
240
458
|
}
|
|
241
|
-
const tokensHash = createHash("sha256").update(
|
|
242
|
-
const resolvedPath =
|
|
243
|
-
const resolvedHash =
|
|
459
|
+
const tokensHash = createHash("sha256").update(readFileSync3(tokensPath, "utf8"), "utf8").digest("hex");
|
|
460
|
+
const resolvedPath = join2(projectRoot, "src/theme/resolved.ts");
|
|
461
|
+
const resolvedHash = existsSync2(resolvedPath) ? readFileSync3(resolvedPath, "utf8").match(
|
|
244
462
|
/tokensHash = '([0-9a-f]{64})'/
|
|
245
463
|
)?.[1] ?? null : null;
|
|
246
464
|
if (resolvedHash !== tokensHash) {
|
|
@@ -480,8 +698,8 @@ async function runWriteProbes(options) {
|
|
|
480
698
|
}
|
|
481
699
|
|
|
482
700
|
// src/doctor/run.ts
|
|
483
|
-
import { existsSync as
|
|
484
|
-
import { join as
|
|
701
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
702
|
+
import { join as join3 } from "path";
|
|
485
703
|
var STATIC_API = record("static.api", 1);
|
|
486
704
|
var STATIC_BOARD = record("static.board", 1);
|
|
487
705
|
var STATIC_SKILLS = record("static.skills", 1);
|
|
@@ -547,8 +765,8 @@ var SKILL_ROOTS = [
|
|
|
547
765
|
".cursor/skills"
|
|
548
766
|
];
|
|
549
767
|
function checkSkillsFreshness(projectRoot) {
|
|
550
|
-
const roots = SKILL_ROOTS.map((root) =>
|
|
551
|
-
(root) =>
|
|
768
|
+
const roots = SKILL_ROOTS.map((root) => join3(projectRoot, root)).filter(
|
|
769
|
+
(root) => existsSync3(root)
|
|
552
770
|
);
|
|
553
771
|
if (roots.length === 0) {
|
|
554
772
|
return STATIC_SKILLS(
|
|
@@ -561,10 +779,10 @@ function checkSkillsFreshness(projectRoot) {
|
|
|
561
779
|
const seen = /* @__PURE__ */ new Set();
|
|
562
780
|
for (const root of roots) {
|
|
563
781
|
for (const skill of corpus.skills) {
|
|
564
|
-
const copied =
|
|
565
|
-
if (!
|
|
782
|
+
const copied = join3(root, skill.name, "SKILL.md");
|
|
783
|
+
if (!existsSync3(copied)) continue;
|
|
566
784
|
seen.add(skill.name);
|
|
567
|
-
if (
|
|
785
|
+
if (readFileSync4(copied, "utf8") !== skill.content) {
|
|
568
786
|
stale.add(skill.name);
|
|
569
787
|
}
|
|
570
788
|
}
|
|
@@ -614,6 +832,12 @@ async function runDoctor(options) {
|
|
|
614
832
|
results.push(
|
|
615
833
|
...checkThemeFreshness(options.projectRoot ?? process.cwd(), boardContext)
|
|
616
834
|
);
|
|
835
|
+
results.push(
|
|
836
|
+
...checkCookieCodecConformance(options.projectRoot ?? process.cwd())
|
|
837
|
+
);
|
|
838
|
+
results.push(
|
|
839
|
+
envOk ? await checkSandboxPersonaGate(fetchImpl, env, boardContext) : skipSandboxPersonaGate("env checks failed \u2014 fix them first")
|
|
840
|
+
);
|
|
617
841
|
results.push(
|
|
618
842
|
...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl) : skipReadProbes("no --frontend <url> given")
|
|
619
843
|
);
|
|
@@ -640,12 +864,12 @@ async function runDoctor(options) {
|
|
|
640
864
|
}
|
|
641
865
|
|
|
642
866
|
// src/setup/run.ts
|
|
643
|
-
import { cpSync, existsSync as
|
|
867
|
+
import { cpSync, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync5 } from "fs";
|
|
644
868
|
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
645
869
|
function detectFramework(cwd) {
|
|
646
870
|
const pkgPath = resolve2(cwd, "package.json");
|
|
647
|
-
if (!
|
|
648
|
-
const pkg = JSON.parse(
|
|
871
|
+
if (!existsSync4(pkgPath)) return null;
|
|
872
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
649
873
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
650
874
|
if (deps["@tanstack/react-start"]) return "tanstack-start";
|
|
651
875
|
return null;
|
|
@@ -662,7 +886,7 @@ function runSetup(cwd = process.cwd()) {
|
|
|
662
886
|
resolve2(cwd, ".codex", "skills"),
|
|
663
887
|
resolve2(cwd, ".cursor", "skills")
|
|
664
888
|
];
|
|
665
|
-
const existing = roots.filter((root) =>
|
|
889
|
+
const existing = roots.filter((root) => existsSync4(root));
|
|
666
890
|
const targetDirs = existing.length > 0 ? existing : [roots[0]];
|
|
667
891
|
const copied = [];
|
|
668
892
|
for (const targetDir of targetDirs) {
|
package/dist/doctor.js
CHANGED
|
@@ -132,6 +132,156 @@ function summarize(results) {
|
|
|
132
132
|
return { exitCode: failed.length > 0 ? 1 : 0, passed, failed, skipped };
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// src/doctor/cookie-conformance.ts
|
|
136
|
+
var import_node_fs2 = require("fs");
|
|
137
|
+
var import_node_path2 = require("path");
|
|
138
|
+
var COOKIE = record("static.cookie-codec", 1);
|
|
139
|
+
var SOURCE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|astro|svelte|vue)$/;
|
|
140
|
+
var REMEDIATION = "set cookies only through the SDK server cookie codec (buildCookie/buildClearCookie from @cavuno/board/server)";
|
|
141
|
+
var SET_COOKIE_SINK = /set-cookie/i;
|
|
142
|
+
var DOCUMENT_COOKIE_ASSIGN = /document\.cookie\s*\+?=(?!=)/i;
|
|
143
|
+
var DOMAIN_SCOPE = /domain\s*=/i;
|
|
144
|
+
var LOOKAHEAD_LINES = 3;
|
|
145
|
+
function stripComments(line) {
|
|
146
|
+
let out = "";
|
|
147
|
+
let i = 0;
|
|
148
|
+
let quote = null;
|
|
149
|
+
while (i < line.length) {
|
|
150
|
+
const c = line[i];
|
|
151
|
+
if (quote !== null) {
|
|
152
|
+
out += c;
|
|
153
|
+
if (c === "\\" && i + 1 < line.length) {
|
|
154
|
+
out += line[i + 1];
|
|
155
|
+
i += 2;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (c === quote) quote = null;
|
|
159
|
+
i += 1;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
163
|
+
quote = c;
|
|
164
|
+
out += c;
|
|
165
|
+
i += 1;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (c === "/" && i + 1 < line.length && line[i - 1] !== "\\") {
|
|
169
|
+
const next = line[i + 1];
|
|
170
|
+
if (next === "/") {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
if (next === "*") {
|
|
174
|
+
i += 2;
|
|
175
|
+
while (i < line.length) {
|
|
176
|
+
if (line[i] === "*" && i + 1 < line.length && line[i + 1] === "/") {
|
|
177
|
+
i += 2;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
i += 1;
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
out += c;
|
|
186
|
+
i += 1;
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
function isCommentLine(line) {
|
|
191
|
+
const trimmed = line.trim();
|
|
192
|
+
if (trimmed.startsWith("*")) return true;
|
|
193
|
+
return stripComments(line).trim() === "";
|
|
194
|
+
}
|
|
195
|
+
function hasCookieWriteSink(line) {
|
|
196
|
+
return SET_COOKIE_SINK.test(line) || DOCUMENT_COOKIE_ASSIGN.test(line);
|
|
197
|
+
}
|
|
198
|
+
function statementWindow(lines, sinkIdx) {
|
|
199
|
+
const sinkStripped = stripComments(lines[sinkIdx]);
|
|
200
|
+
const parts = [sinkStripped];
|
|
201
|
+
if (sinkStripped.includes(";")) return sinkStripped;
|
|
202
|
+
let taken = 0;
|
|
203
|
+
for (let j = sinkIdx + 1; j < lines.length && taken < LOOKAHEAD_LINES; j += 1) {
|
|
204
|
+
const next = lines[j];
|
|
205
|
+
if (isCommentLine(next)) continue;
|
|
206
|
+
const stripped = stripComments(next);
|
|
207
|
+
parts.push(stripped);
|
|
208
|
+
taken += 1;
|
|
209
|
+
if (stripped.includes(";")) break;
|
|
210
|
+
}
|
|
211
|
+
return parts.join("\n");
|
|
212
|
+
}
|
|
213
|
+
function isOffendingSink(lines, index) {
|
|
214
|
+
const line = lines[index];
|
|
215
|
+
if (isCommentLine(line)) return false;
|
|
216
|
+
const stripped = stripComments(line);
|
|
217
|
+
if (!hasCookieWriteSink(stripped)) return false;
|
|
218
|
+
return DOMAIN_SCOPE.test(statementWindow(lines, index));
|
|
219
|
+
}
|
|
220
|
+
function walkSourceFiles(dir, out) {
|
|
221
|
+
let entries;
|
|
222
|
+
try {
|
|
223
|
+
entries = (0, import_node_fs2.readdirSync)(dir);
|
|
224
|
+
} catch {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
for (const name of entries) {
|
|
228
|
+
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
229
|
+
const full = (0, import_node_path2.join)(dir, name);
|
|
230
|
+
let st;
|
|
231
|
+
try {
|
|
232
|
+
st = (0, import_node_fs2.statSync)(full);
|
|
233
|
+
} catch {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (st.isDirectory()) {
|
|
237
|
+
walkSourceFiles(full, out);
|
|
238
|
+
} else if (st.isFile() && SOURCE_EXT.test(name)) {
|
|
239
|
+
out.push(full);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function truncate(line, max = 120) {
|
|
244
|
+
const t = line.trim();
|
|
245
|
+
return t.length > max ? `${t.slice(0, max)}\u2026` : t;
|
|
246
|
+
}
|
|
247
|
+
function checkCookieCodecConformance(projectRoot) {
|
|
248
|
+
const srcDir = (0, import_node_path2.join)(projectRoot, "src");
|
|
249
|
+
if (!(0, import_node_fs2.existsSync)(srcDir)) {
|
|
250
|
+
return [
|
|
251
|
+
COOKIE(
|
|
252
|
+
"skip",
|
|
253
|
+
"no src/ directory \u2014 cookie-codec conformance scan skipped"
|
|
254
|
+
)
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
const files = [];
|
|
258
|
+
walkSourceFiles(srcDir, files);
|
|
259
|
+
const findings = [];
|
|
260
|
+
for (const file of files) {
|
|
261
|
+
const rel = (0, import_node_path2.relative)(projectRoot, file).split("\\").join("/");
|
|
262
|
+
let text;
|
|
263
|
+
try {
|
|
264
|
+
text = (0, import_node_fs2.readFileSync)(file, "utf8");
|
|
265
|
+
} catch {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const lines = text.split(/\r?\n/);
|
|
269
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
270
|
+
if (!isOffendingSink(lines, i)) continue;
|
|
271
|
+
findings.push(`${rel}:${i + 1} \u2014 ${truncate(lines[i])}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (findings.length === 0) {
|
|
275
|
+
return [
|
|
276
|
+
COOKIE(
|
|
277
|
+
"pass",
|
|
278
|
+
"no domain-scoped Set-Cookie or document.cookie writes in src/"
|
|
279
|
+
)
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
return [COOKIE("fail", `${findings.join("; ")}; ${REMEDIATION}`)];
|
|
283
|
+
}
|
|
284
|
+
|
|
135
285
|
// src/doctor/probe.ts
|
|
136
286
|
var FETCH_TIMEOUT_MS = 1e4;
|
|
137
287
|
async function probe(fetchImpl, url) {
|
|
@@ -251,14 +401,82 @@ async function runReadProbes(fetchImpl, frontendUrl) {
|
|
|
251
401
|
];
|
|
252
402
|
}
|
|
253
403
|
|
|
404
|
+
// src/doctor/sandbox.ts
|
|
405
|
+
var SANDBOX_GATE = record("sandbox.persona-gate", 1);
|
|
406
|
+
var EXPECTED_PERSONA_COUNT = 8;
|
|
407
|
+
function skipSandboxPersonaGate(reason) {
|
|
408
|
+
return SANDBOX_GATE("skip", reason);
|
|
409
|
+
}
|
|
410
|
+
async function checkSandboxPersonaGate(fetchImpl, env, boardContext) {
|
|
411
|
+
if (boardContext === null) {
|
|
412
|
+
return SANDBOX_GATE(
|
|
413
|
+
"skip",
|
|
414
|
+
"board did not resolve \u2014 fix static.board first"
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
const url = `${apiBase(env.apiUrl)}/v1/boards/${encodeURIComponent(env.boardKey)}/sandbox/personas`;
|
|
418
|
+
const result = await probe(fetchImpl, url);
|
|
419
|
+
if (boardContext.sandbox !== true) {
|
|
420
|
+
return result.status === 404 ? SANDBOX_GATE(
|
|
421
|
+
"pass",
|
|
422
|
+
"sandbox persona endpoint refuses (404) on a non-sandbox board"
|
|
423
|
+
) : SANDBOX_GATE(
|
|
424
|
+
"fail",
|
|
425
|
+
`sandbox persona endpoint is reachable on a NON-sandbox board (HTTP ${result.status}) \u2014 the capability gate is not holding`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
if (!result.ok) {
|
|
429
|
+
return SANDBOX_GATE(
|
|
430
|
+
"fail",
|
|
431
|
+
`sandbox persona roster did not resolve (HTTP ${result.status}) \u2014 the persona seed may have failed`
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
let personas;
|
|
435
|
+
let password;
|
|
436
|
+
try {
|
|
437
|
+
const parsed = JSON.parse(result.body);
|
|
438
|
+
personas = parsed.personas;
|
|
439
|
+
password = parsed.password;
|
|
440
|
+
} catch {
|
|
441
|
+
return SANDBOX_GATE(
|
|
442
|
+
"fail",
|
|
443
|
+
"sandbox persona endpoint returned 200 but not JSON \u2014 proxy or captive portal in the way?"
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
if (!Array.isArray(personas) || personas.length !== EXPECTED_PERSONA_COUNT) {
|
|
447
|
+
return SANDBOX_GATE(
|
|
448
|
+
"fail",
|
|
449
|
+
`expected ${EXPECTED_PERSONA_COUNT} personas, got ${Array.isArray(personas) ? personas.length : "a non-array"} \u2014 the persona seed is incomplete`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
const unseeded = personas.filter((persona) => persona.seeded !== true);
|
|
453
|
+
if (unseeded.length > 0) {
|
|
454
|
+
const ids = unseeded.map((persona) => typeof persona.id === "string" ? persona.id : "?").join(", ");
|
|
455
|
+
return SANDBOX_GATE(
|
|
456
|
+
"fail",
|
|
457
|
+
`${unseeded.length} of ${EXPECTED_PERSONA_COUNT} personas are not seeded on the board (${ids}) \u2014 the persona seed failed or was purged; run POST /v1/boards/{key}/sandbox/reseed to restore the roster`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
if (typeof password !== "string" || password.length === 0) {
|
|
461
|
+
return SANDBOX_GATE(
|
|
462
|
+
"fail",
|
|
463
|
+
"roster present but the published password is missing"
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
return SANDBOX_GATE(
|
|
467
|
+
"pass",
|
|
468
|
+
`sandbox roster complete and seeded (${EXPECTED_PERSONA_COUNT} personas)`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
254
472
|
// src/doctor/theme.ts
|
|
255
473
|
var import_node_crypto = require("crypto");
|
|
256
|
-
var
|
|
257
|
-
var
|
|
474
|
+
var import_node_fs3 = require("fs");
|
|
475
|
+
var import_node_path3 = require("path");
|
|
258
476
|
var THEME = record("static.theme", 1);
|
|
259
477
|
function checkThemeFreshness(projectRoot, context) {
|
|
260
|
-
const tokensPath = (0,
|
|
261
|
-
if (!(0,
|
|
478
|
+
const tokensPath = (0, import_node_path3.join)(projectRoot, "src/tokens.css");
|
|
479
|
+
if (!(0, import_node_fs3.existsSync)(tokensPath)) {
|
|
262
480
|
return [
|
|
263
481
|
THEME(
|
|
264
482
|
"skip",
|
|
@@ -266,9 +484,9 @@ function checkThemeFreshness(projectRoot, context) {
|
|
|
266
484
|
)
|
|
267
485
|
];
|
|
268
486
|
}
|
|
269
|
-
const tokensHash = (0, import_node_crypto.createHash)("sha256").update((0,
|
|
270
|
-
const resolvedPath = (0,
|
|
271
|
-
const resolvedHash = (0,
|
|
487
|
+
const tokensHash = (0, import_node_crypto.createHash)("sha256").update((0, import_node_fs3.readFileSync)(tokensPath, "utf8"), "utf8").digest("hex");
|
|
488
|
+
const resolvedPath = (0, import_node_path3.join)(projectRoot, "src/theme/resolved.ts");
|
|
489
|
+
const resolvedHash = (0, import_node_fs3.existsSync)(resolvedPath) ? (0, import_node_fs3.readFileSync)(resolvedPath, "utf8").match(
|
|
272
490
|
/tokensHash = '([0-9a-f]{64})'/
|
|
273
491
|
)?.[1] ?? null : null;
|
|
274
492
|
if (resolvedHash !== tokensHash) {
|
|
@@ -508,8 +726,8 @@ async function runWriteProbes(options) {
|
|
|
508
726
|
}
|
|
509
727
|
|
|
510
728
|
// src/doctor/run.ts
|
|
511
|
-
var
|
|
512
|
-
var
|
|
729
|
+
var import_node_fs4 = require("fs");
|
|
730
|
+
var import_node_path4 = require("path");
|
|
513
731
|
var STATIC_API = record("static.api", 1);
|
|
514
732
|
var STATIC_BOARD = record("static.board", 1);
|
|
515
733
|
var STATIC_SKILLS = record("static.skills", 1);
|
|
@@ -575,8 +793,8 @@ var SKILL_ROOTS = [
|
|
|
575
793
|
".cursor/skills"
|
|
576
794
|
];
|
|
577
795
|
function checkSkillsFreshness(projectRoot) {
|
|
578
|
-
const roots = SKILL_ROOTS.map((root) => (0,
|
|
579
|
-
(root) => (0,
|
|
796
|
+
const roots = SKILL_ROOTS.map((root) => (0, import_node_path4.join)(projectRoot, root)).filter(
|
|
797
|
+
(root) => (0, import_node_fs4.existsSync)(root)
|
|
580
798
|
);
|
|
581
799
|
if (roots.length === 0) {
|
|
582
800
|
return STATIC_SKILLS(
|
|
@@ -589,10 +807,10 @@ function checkSkillsFreshness(projectRoot) {
|
|
|
589
807
|
const seen = /* @__PURE__ */ new Set();
|
|
590
808
|
for (const root of roots) {
|
|
591
809
|
for (const skill of corpus.skills) {
|
|
592
|
-
const copied = (0,
|
|
593
|
-
if (!(0,
|
|
810
|
+
const copied = (0, import_node_path4.join)(root, skill.name, "SKILL.md");
|
|
811
|
+
if (!(0, import_node_fs4.existsSync)(copied)) continue;
|
|
594
812
|
seen.add(skill.name);
|
|
595
|
-
if ((0,
|
|
813
|
+
if ((0, import_node_fs4.readFileSync)(copied, "utf8") !== skill.content) {
|
|
596
814
|
stale.add(skill.name);
|
|
597
815
|
}
|
|
598
816
|
}
|
|
@@ -642,6 +860,12 @@ async function runDoctor(options) {
|
|
|
642
860
|
results.push(
|
|
643
861
|
...checkThemeFreshness(options.projectRoot ?? process.cwd(), boardContext)
|
|
644
862
|
);
|
|
863
|
+
results.push(
|
|
864
|
+
...checkCookieCodecConformance(options.projectRoot ?? process.cwd())
|
|
865
|
+
);
|
|
866
|
+
results.push(
|
|
867
|
+
envOk ? await checkSandboxPersonaGate(fetchImpl, env, boardContext) : skipSandboxPersonaGate("env checks failed \u2014 fix them first")
|
|
868
|
+
);
|
|
645
869
|
results.push(
|
|
646
870
|
...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl) : skipReadProbes("no --frontend <url> given")
|
|
647
871
|
);
|