@cavuno/board 1.39.0 → 1.40.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.
Files changed (37) hide show
  1. package/dist/{_spec-DxC1ze93.d.mts → _spec-DRbgPIlN.d.mts} +351 -0
  2. package/dist/{_spec-DxC1ze93.d.ts → _spec-DRbgPIlN.d.ts} +351 -0
  3. package/dist/bin.mjs +242 -18
  4. package/dist/{board-RfZEAJse.d.ts → board-BxPUtrOl.d.ts} +1 -1
  5. package/dist/{board-CqYibYUA.d.mts → board-DCiNpgFf.d.mts} +1 -1
  6. package/dist/doctor.js +238 -14
  7. package/dist/doctor.mjs +238 -14
  8. package/dist/filters.d.mts +2 -2
  9. package/dist/filters.d.ts +2 -2
  10. package/dist/format.d.mts +3 -3
  11. package/dist/format.d.ts +3 -3
  12. package/dist/index.d.mts +55 -10
  13. package/dist/index.d.ts +55 -10
  14. package/dist/index.js +50 -1
  15. package/dist/index.mjs +50 -1
  16. package/dist/{jobs-CLLIvtMc.d.ts → jobs-DAGAVHQL.d.ts} +1 -1
  17. package/dist/{jobs-DPPA1Nev.d.mts → jobs-DhePKSRe.d.mts} +1 -1
  18. package/dist/{salaries-Rb5h_eVZ.d.mts → salaries-D6SUVMmt.d.mts} +2 -2
  19. package/dist/{salaries-DK4RnJnw.d.ts → salaries-cqb78kg0.d.ts} +2 -2
  20. package/dist/{search-DBoMM-gE.d.ts → search-BalPAS0P.d.ts} +1 -1
  21. package/dist/{search-CqBa1Qc4.d.mts → search-DYUQzCq_.d.mts} +1 -1
  22. package/dist/seo.d.mts +4 -4
  23. package/dist/seo.d.ts +4 -4
  24. package/dist/server.d.mts +56 -6
  25. package/dist/server.d.ts +56 -6
  26. package/dist/server.js +41 -0
  27. package/dist/server.mjs +41 -0
  28. package/dist/sitemap.d.mts +5 -5
  29. package/dist/sitemap.d.ts +5 -5
  30. package/dist/suggest.d.mts +2 -2
  31. package/dist/suggest.d.ts +2 -2
  32. package/dist/theme.d.mts +49 -1
  33. package/dist/theme.d.ts +49 -1
  34. package/dist/theme.js +60 -26
  35. package/dist/theme.mjs +60 -26
  36. package/package.json +1 -1
  37. 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 readFileSync2 } from "fs";
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 = join(projectRoot, "src/tokens.css");
233
- if (!existsSync(tokensPath)) {
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(readFileSync2(tokensPath, "utf8"), "utf8").digest("hex");
242
- const resolvedPath = join(projectRoot, "src/theme/resolved.ts");
243
- const resolvedHash = existsSync(resolvedPath) ? readFileSync2(resolvedPath, "utf8").match(
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 existsSync2, readFileSync as readFileSync3 } from "fs";
484
- import { join as join2 } from "path";
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) => join2(projectRoot, root)).filter(
551
- (root) => existsSync2(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 = join2(root, skill.name, "SKILL.md");
565
- if (!existsSync2(copied)) continue;
782
+ const copied = join3(root, skill.name, "SKILL.md");
783
+ if (!existsSync3(copied)) continue;
566
784
  seen.add(skill.name);
567
- if (readFileSync3(copied, "utf8") !== skill.content) {
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 existsSync3, mkdirSync, readFileSync as readFileSync4 } from "fs";
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 (!existsSync3(pkgPath)) return null;
648
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
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) => existsSync3(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) {
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-DxC1ze93.js';
1
+ import { S as Schemas } from './_spec-DRbgPIlN.js';
2
2
 
3
3
  type PublicBoard = Schemas['PublicBoardContext'];
4
4
  type PublicBoardFeatures = PublicBoard['features'];
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-DxC1ze93.mjs';
1
+ import { S as Schemas } from './_spec-DRbgPIlN.mjs';
2
2
 
3
3
  type PublicBoard = Schemas['PublicBoardContext'];
4
4
  type PublicBoardFeatures = PublicBoard['features'];
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 import_node_fs2 = require("fs");
257
- var import_node_path2 = require("path");
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, import_node_path2.join)(projectRoot, "src/tokens.css");
261
- if (!(0, import_node_fs2.existsSync)(tokensPath)) {
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, import_node_fs2.readFileSync)(tokensPath, "utf8"), "utf8").digest("hex");
270
- const resolvedPath = (0, import_node_path2.join)(projectRoot, "src/theme/resolved.ts");
271
- const resolvedHash = (0, import_node_fs2.existsSync)(resolvedPath) ? (0, import_node_fs2.readFileSync)(resolvedPath, "utf8").match(
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 import_node_fs3 = require("fs");
512
- var import_node_path3 = require("path");
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, import_node_path3.join)(projectRoot, root)).filter(
579
- (root) => (0, import_node_fs3.existsSync)(root)
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, import_node_path3.join)(root, skill.name, "SKILL.md");
593
- if (!(0, import_node_fs3.existsSync)(copied)) continue;
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, import_node_fs3.readFileSync)(copied, "utf8") !== skill.content) {
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
  );