chamba 0.7.0 → 0.8.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 (120) hide show
  1. package/LICENSE +53 -14
  2. package/README.md +28 -8
  3. package/dist/commands/advanced.js +7 -278
  4. package/dist/commands/dev.js +11 -612
  5. package/dist/commands/doctor.js +1 -29
  6. package/dist/commands/menu.js +1 -80
  7. package/dist/commands/onboard.js +6 -230
  8. package/dist/commands/settings.js +15 -349
  9. package/dist/lib/agent-commands.js +2 -0
  10. package/dist/lib/agent-context.js +6 -210
  11. package/dist/lib/browser.js +1 -40
  12. package/dist/lib/chamba-yaml.js +10 -191
  13. package/dist/lib/constants.js +1 -125
  14. package/dist/lib/dockerfile-builder.js +41 -261
  15. package/dist/lib/env.js +2 -78
  16. package/dist/lib/git-env.js +1 -21
  17. package/dist/lib/global-config.js +4 -66
  18. package/dist/lib/pnpm-store.js +1 -19
  19. package/dist/lib/ports.js +1 -210
  20. package/dist/lib/safe-rm.js +1 -36
  21. package/dist/lib/sessions.js +2 -34
  22. package/dist/lib/shadows.js +1 -176
  23. package/dist/lib/skills.js +2 -0
  24. package/dist/lib/templates.js +1 -0
  25. package/dist/lib/webterm.js +1 -305
  26. package/dist/lib/workspace-identity.js +4 -260
  27. package/package.json +4 -4
  28. package/templates/Dockerfile +12 -19
  29. package/templates/claude-statusline.sh +5 -7
  30. package/templates/context/baseline.md +2 -0
  31. package/templates/pane-apps/client/assets/c-BNa-DeUl.json +3552 -0
  32. package/templates/pane-apps/client/assets/cpp-B-T4NRoH.json +20239 -0
  33. package/templates/pane-apps/client/assets/csharp-BbxHChhA.json +6010 -0
  34. package/templates/pane-apps/client/assets/css-CWCxuwmz.json +1860 -0
  35. package/templates/pane-apps/client/assets/diff-B-2N89g8.json +154 -0
  36. package/templates/pane-apps/client/assets/docker-DWTvYW26.json +103 -0
  37. package/templates/pane-apps/client/assets/files-D0nJgFAA.js +12 -0
  38. package/templates/pane-apps/client/assets/files-h0K1vZyQ.css +1 -0
  39. package/templates/pane-apps/client/assets/go-B2hyYeNg.json +3156 -0
  40. package/templates/pane-apps/client/assets/graphql-Z6Id2aNO.json +1292 -0
  41. package/templates/pane-apps/client/assets/html-BtzvlIfu.json +2627 -0
  42. package/templates/pane-apps/client/assets/ini-Bn3Twap1.json +108 -0
  43. package/templates/pane-apps/client/assets/java-C7t2dwLO.json +1885 -0
  44. package/templates/pane-apps/client/assets/javascript-DuC2v0C0.json +5996 -0
  45. package/templates/pane-apps/client/assets/json-CwdkWeV5.json +206 -0
  46. package/templates/pane-apps/client/assets/jsonc-MYWDbTU3.json +206 -0
  47. package/templates/pane-apps/client/assets/jsx-Cn8EZuIC.json +5996 -0
  48. package/templates/pane-apps/client/assets/kotlin-CFNOz1gw.json +580 -0
  49. package/templates/pane-apps/client/assets/make-BVnlFCcl.json +629 -0
  50. package/templates/pane-apps/client/assets/markdown-B4Xk2WM_.json +3263 -0
  51. package/templates/pane-apps/client/assets/php-CAQzQmmI.json +4188 -0
  52. package/templates/pane-apps/client/assets/python-CYQee8m4.json +4174 -0
  53. package/templates/pane-apps/client/assets/reviews-BsoGZ35r.css +1 -0
  54. package/templates/pane-apps/client/assets/reviews-iW0Mbn4e.js +153 -0
  55. package/templates/pane-apps/client/assets/ruby-D7R58tus.json +3114 -0
  56. package/templates/pane-apps/client/assets/rust-BFBbFiei.json +1103 -0
  57. package/templates/pane-apps/client/assets/scss-TQRhBkPG.json +1874 -0
  58. package/templates/pane-apps/client/assets/shellscript-DieAkC4W.json +2375 -0
  59. package/templates/pane-apps/client/assets/specs-D9z3neR-.js +13 -0
  60. package/templates/pane-apps/client/assets/sql-Bxd030LV.json +634 -0
  61. package/templates/pane-apps/client/assets/toml-ChrKGzsV.json +430 -0
  62. package/templates/pane-apps/client/assets/tsx-D2e90MFw.json +5996 -0
  63. package/templates/pane-apps/client/assets/typescript-DonXQ_n1.json +5747 -0
  64. package/templates/pane-apps/client/assets/xml-B17r2RXF.json +382 -0
  65. package/templates/pane-apps/client/assets/yaml-CKn4h1zy.json +627 -0
  66. package/templates/pane-apps/client/reviews/index.html +15 -0
  67. package/templates/pane-apps/client/specs/index.html +3 -2
  68. package/templates/pane-apps/server/reviews.mjs +20 -0
  69. package/templates/pane-apps/server/specs.mjs +5 -1588
  70. package/templates/skills/chamba-statusline/SKILL.md +7 -19
  71. package/templates/skills/dx-review/SKILL.md +123 -0
  72. package/templates/skills/dx-review/references/acts.md +162 -0
  73. package/templates/skills/dx-spec/SKILL.md +223 -27
  74. package/templates/skills/dx-spec/references/principles-template.md +2 -0
  75. package/templates/skills/dx-spec/references/review-guide.md +4 -50
  76. package/templates/skills/dx-spec/references/spec-guide.md +24 -4
  77. package/templates/skills/dx-spec/references/stages.md +143 -58
  78. package/templates/skills/dx-spec-execute/SKILL.md +148 -35
  79. package/templates/startup.mjs +10 -4
  80. package/templates/tool-helper.sh +166 -0
  81. package/templates/webterm/README.md +60 -26
  82. package/templates/webterm/color.js +61 -0
  83. package/templates/webterm/config.js +35 -4
  84. package/templates/webterm/context/claude.md +0 -1
  85. package/templates/webterm/public/app/alerts.js +4 -4
  86. package/templates/webterm/public/app/anchored.js +81 -0
  87. package/templates/webterm/public/app/connection.js +9 -0
  88. package/templates/webterm/public/app/dom.js +2 -0
  89. package/templates/webterm/public/app/frames.js +51 -17
  90. package/templates/webterm/public/app/main.js +22 -8
  91. package/templates/webterm/public/app/new-session.js +13 -41
  92. package/templates/webterm/public/app/palette.js +58 -0
  93. package/templates/webterm/public/app/pane-shape.js +167 -0
  94. package/templates/webterm/public/app/pane-shell.js +199 -134
  95. package/templates/webterm/public/app/pane.js +33 -24
  96. package/templates/webterm/public/app/reviews-host.js +15 -0
  97. package/templates/webterm/public/app/specs-host.js +12 -218
  98. package/templates/webterm/public/app/state.js +5 -0
  99. package/templates/webterm/public/app/tabs.js +6 -2
  100. package/templates/webterm/public/app/theme.js +75 -27
  101. package/templates/webterm/public/app/tool-host.js +302 -0
  102. package/templates/webterm/public/app/workspace-color.js +192 -0
  103. package/templates/webterm/public/index.html +17 -3
  104. package/templates/webterm/public/styles.css +143 -21
  105. package/templates/webterm/server.js +198 -150
  106. package/templates/webterm/tools/commands.js +61 -0
  107. package/templates/webterm/tools/index.js +25 -0
  108. package/templates/webterm/tools/mount.js +337 -0
  109. package/templates/webterm/tools/paths.js +34 -0
  110. package/templates/webterm/tools/reviews.js +133 -0
  111. package/templates/webterm/tools/specs.js +154 -0
  112. package/templates/webterm/typed-line.js +70 -7
  113. package/templates/context/context-usage.md +0 -1
  114. package/templates/context-usage.sh +0 -266
  115. package/templates/pane-apps/client/assets/specs-B1970L17.css +0 -1
  116. package/templates/pane-apps/client/assets/specs-cEee_SPn.js +0 -23
  117. package/templates/skills/dx-spec-config/SKILL.md +0 -313
  118. package/templates/skills/dx-spec-config/references/principles-template.md +0 -12
  119. package/templates/specs.sh +0 -106
  120. package/templates/webterm/specs.js +0 -358
@@ -1,358 +0,0 @@
1
- // =========================================================================================================================================
2
- // specs.js - The Specs tool, as webterm mounts it.
3
- //
4
- // The tool's own knowledge - what a root of specs holds, what one spec holds, what it writes - is a module
5
- // built elsewhere and bundled into one file (see ../pane-apps/). This is the mount around it: the credentials
6
- // that open it, the rules about which request may reach which part of it, and the shape of every answer.
7
- //
8
- // One function decides all of that. `handle()` takes a request as plain data and gives back a status, headers
9
- // and a body, and never throws: a module that fails answers with a status, so a bug in spec reading costs the
10
- // tab its view and nothing else. The express side is one delegation in server.js, and holds no decision.
11
- //
12
- // Three kinds of request reach the tool, each with its own credential, and neither credential opens anything
13
- // but its own kind:
14
- // - The tool's client, a document and its assets, served open by server.js like every other static file.
15
- // It holds nothing secret and drives nothing, and its responses are CORS-readable because the client runs
16
- // with an opaque origin and fetches its own module scripts cross-origin.
17
- // - The data routes, gated by a Specs-scoped key the shell hands into the frame, presented as a header and
18
- // never in a URL. These alone answer the cross-origin preflight that header needs.
19
- // - Workspace-file loads, gated by a raw-route token that rides as a path segment, because a frame and an
20
- // image cannot send a header.
21
- // The master key opens none of this, and none of this opens the master key: a rendering bug inside the frame
22
- // leaks spec reading at worst, never the key that types into terminals.
23
- // =========================================================================================================================================
24
-
25
- import { timingSafeEqual } from "node:crypto";
26
- import { BadRequest, createSpecsModule, statusFor } from "../pane-apps/server/specs.mjs";
27
-
28
- // The module maker, re-exported so the path to the built dist is written once. webterm resolves the pane apps
29
- // as a sibling directory, which is where the image bakes them and where a checkout stages them.
30
- export { createSpecsModule };
31
-
32
- // The header the Specs key travels in. A header rather than a URL for the reason the pane's key is one: a
33
- // document can read the URL it was loaded from, and workspace content renders inside this tool.
34
- export const SPECS_KEY_HEADER = "x-specs-key";
35
-
36
- // Everything the tool answers sits under one prefix, and the two kinds are told apart by the next segment.
37
- export const SPECS_PREFIX = "/specs";
38
- export const SPECS_KEYS_PATH = "/specs/keys";
39
- // The `specs` helper's door. Behind the master key like every other helper route, and behind being a process
40
- // inside a session as well.
41
- export const SPECS_AGENT_PATH = "/specs/agent";
42
- export const API_PREFIX = "/specs/api/";
43
- export const RAW_PREFIX = "/specs/raw/";
44
-
45
- // Where the built tool clients are served from. Open statics, one directory per tool.
46
- export const CLIENT_PREFIX = "/pane-apps";
47
-
48
- // Read by the frame, which has an opaque origin, so every answer it may read says so. It carries no
49
- // credential of its own - the browser sends none without `Access-Control-Allow-Credentials`, which nothing
50
- // here sends - so this opens a route to any origin that already holds the key the route asks for.
51
- //
52
- // The data routes alone. Workspace files are not readable cross-origin by anybody: a jailed document can read
53
- // the token out of its own URL, and making the files it opens readable as well would turn one rendering bug
54
- // into every spec in the repository, posted anywhere. A frame and an image need no permission to render.
55
- const CORS_OPEN = { "Access-Control-Allow-Origin": "*" };
56
-
57
- // The media types the data routes hand back as text, which is what this client reads rather than renders.
58
- // Everything else - a page, a diagram, a stylesheet, an image - is something a browser renders for itself,
59
- // and is loaded from the workspace-file path, where a frame and an image reach it without sending a header.
60
- // An allowlist rather than a family, because `text/html` is in the family and is exactly the wrong side of
61
- // this line.
62
- const TEXTUAL = new Set(["text/markdown", "text/plain", "application/json"]);
63
-
64
- // The jail on workspace bytes, sent whatever the media type. Unconditional on purpose: a per-type list would
65
- // put the one control between workspace content and this origin at the mercy of an enumeration, and the next
66
- // scriptable type would walk past it. The sandbox token set is the one /pane/page already sends.
67
- const JAIL = {
68
- "Content-Security-Policy": "sandbox allow-scripts allow-forms",
69
- "X-Content-Type-Options": "nosniff",
70
- "Cache-Control": "no-store",
71
- };
72
-
73
- /** One query or header value as a string. A repeated parameter parses to an array, which is not a value. */
74
- function field(value) {
75
- return typeof value === "string" ? value : "";
76
- }
77
-
78
- /** Whether a presented credential is the live one, compared in constant time. */
79
- function holds(presented, secret) {
80
- if (typeof presented !== "string" || presented === "") return false;
81
- // Byte lengths, not string lengths: timingSafeEqual throws on a length mismatch, and a multi-byte
82
- // character makes those two differ.
83
- const shown = Buffer.from(presented);
84
- const live = Buffer.from(secret);
85
- if (shown.length !== live.length) return false;
86
- return timingSafeEqual(shown, live);
87
- }
88
-
89
- /**
90
- * What a caller is told a failure was.
91
- *
92
- * The module's own words name the path it was asked for, which is the caller's own text. Anything else came
93
- * from below it, and those messages carry the container path the tool resolved to - which no caller needs and
94
- * which the log is the place for.
95
- */
96
- function reason(error) {
97
- const message = error instanceof Error ? error.message : String(error);
98
- if (message.startsWith("specs:")) return message;
99
- console.warn(`[webterm] specs: ${message}`);
100
- return "that could not be read";
101
- }
102
-
103
- /** A workspace path as the URL spells it, one segment at a time. Throws for a segment that is not a path. */
104
- function decodePath(rest) {
105
- return rest
106
- .split("/")
107
- .map((segment) => decodeURIComponent(segment))
108
- .join("/");
109
- }
110
-
111
- /**
112
- * Mount the Specs module behind its credentials.
113
- *
114
- * `module` is what the tool knows, made by createSpecsModule and handed in the way paneStore is handed to the
115
- * pane: this file decides who may ask, never what the answer is. `key` opens the data routes and `rawToken`
116
- * opens the workspace files, and the two are separate so that neither one is the other's spare.
117
- */
118
- export function mountSpecs({ module, key, rawToken }) {
119
- // The data set, as one table. The gate, the preflight answer and the router all read it, so they cannot
120
- // disagree about which routes exist or which methods each one takes.
121
- const DATA = {
122
- dashboard: { GET: () => module.dashboard() },
123
- entry: { GET: (query) => module.entry(field(query.path)) },
124
- state: {
125
- GET: (query) => module.state(field(query.entry)),
126
- PUT: (query, body) => {
127
- module.writeState(field(query.entry), body);
128
- return { ok: true };
129
- },
130
- },
131
- feedback: {
132
- GET: (query) => ({ rounds: module.filed(field(query.entry)) }),
133
- POST: (query, body) => module.feedback(field(query.entry), body),
134
- },
135
- file: { GET: (query) => textOf(field(query.path)) },
136
- // The protocol, as the reader sees and answers it. What an agent posts is not here: it arrives at the
137
- // helper's door below, which is gated by being inside a session rather than by holding this key.
138
- stages: { GET: () => ({ stages: module.stages() }) },
139
- protocol: { GET: (query) => module.specState(field(query.entry)) },
140
- // A declared round or gate as it was written. Read through the tool rather than as a workspace file:
141
- // it lives in the dot-directory, which the file route refuses on purpose.
142
- round: { GET: (query) => module.round(field(query.entry), field(query.id)) },
143
- intake: { POST: (_query, body) => module.intake(body) },
144
- confirm: { POST: (query, body) => module.confirm(field(query.entry), body) },
145
- // The user answering a gate, a round or a decision card. The agent's side of the same file is a verb
146
- // at the helper's door, so neither caller can do the other's half.
147
- answer: { POST: (query, body) => module.answer(field(query.entry), field(query.id), body) },
148
- };
149
-
150
- // The helper's verbs, which an agent reaches from inside a session. A verb that is not here is not one.
151
- const VERBS = {
152
- // The first call any agent makes: it answers whether there is a pane at all, and where the specs
153
- // this pane serves live. An agent writing into a spec needs a path, and the root is a mount
154
- // parameter, so nothing it could work out for itself.
155
- stages: () => ({ root: module.root, stages: module.stages() }),
156
- state: (entry) => module.specState(entry),
157
- intake: (_entry, payload) => module.intake(payload),
158
- propose: (entry, payload) => module.propose(entry, payload),
159
- await: (entry, payload) => module.declare(entry, payload),
160
- post: (entry, payload) => module.post(entry, payload),
161
- review: (entry, payload) => module.review(entry, payload),
162
- answers: (entry, payload) => ({ answer: module.answers(entry, field(payload?.id)) }),
163
- };
164
-
165
- /** One workspace file as text. Anything a page renders rather than reads is refused to this route. */
166
- function textOf(path) {
167
- const read = module.file(path);
168
- const type = read.mediaType.split(";")[0].trim();
169
- if (!TEXTUAL.has(type)) throw new BadRequest(`${path} is not read as text - load it as a workspace file`);
170
- return { path, mediaType: read.mediaType, size: read.size, modified: read.modified, text: read.bytes.toString("utf8") };
171
- }
172
-
173
- /**
174
- * The route a path names, or undefined.
175
- *
176
- * Through `Object.hasOwn`, because everything inherits `constructor`, `toString` and their neighbours: a
177
- * plain lookup would answer for `/specs/api/constructor` and make a name that is not a route act like one.
178
- */
179
- function routeFor(path) {
180
- const name = path.slice(API_PREFIX.length);
181
- return Object.hasOwn(DATA, name) ? DATA[name] : undefined;
182
- }
183
-
184
- /** The path a workspace file is loaded from. The token is a segment because a frame sends no headers. */
185
- const rawBase = `${RAW_PREFIX}${rawToken}`;
186
-
187
- function refused(what) {
188
- return { status: 403, headers: CORS_OPEN, body: { error: what } };
189
- }
190
-
191
- // The preflight the Specs key header needs, answered for the data routes and for nothing else. A route
192
- // that is not in the table gets no `Access-Control-Allow-` header at all, which is what a browser reads as
193
- // "not allowed" rather than as an answer it may act on.
194
- function preflight(path) {
195
- const route = routeFor(path);
196
- if (!route) return { status: 403, headers: {}, body: { error: "not a Specs data route" } };
197
- return {
198
- status: 204,
199
- headers: {
200
- ...CORS_OPEN,
201
- "Access-Control-Allow-Methods": Object.keys(route).join(", "),
202
- "Access-Control-Allow-Headers": `${SPECS_KEY_HEADER}, content-type`,
203
- "Access-Control-Max-Age": "600",
204
- },
205
- body: null,
206
- };
207
- }
208
-
209
- function data(method, path, query, body, headers) {
210
- const route = routeFor(path);
211
- if (!route) return { status: 404, headers: CORS_OPEN, body: { error: "no such Specs route" } };
212
- // The gate before the body: a caller without the key learns nothing about what a body would have done.
213
- if (!holds(field(headers[SPECS_KEY_HEADER]), key)) {
214
- return refused(`the Specs routes take their key in the ${SPECS_KEY_HEADER} header`);
215
- }
216
- const handler = route[method];
217
- if (!handler) {
218
- const allowed = Object.keys(route).join(", ");
219
- return { status: 405, headers: { ...CORS_OPEN, Allow: allowed }, body: { error: `this route takes ${allowed}` } };
220
- }
221
- let payload = null;
222
- if (body !== null && body !== undefined && body !== "") {
223
- try {
224
- payload = typeof body === "string" ? JSON.parse(body) : body;
225
- } catch {
226
- return { status: 400, headers: CORS_OPEN, body: { error: "the body is not JSON" } };
227
- }
228
- }
229
- try {
230
- return { status: 200, headers: CORS_OPEN, body: handler(query, payload) };
231
- } catch (error) {
232
- return { status: statusFor(error), headers: CORS_OPEN, body: { error: reason(error) } };
233
- }
234
- }
235
-
236
- function raw(method, path, headers) {
237
- // The jail rides on every answer this route gives, a refusal included: the rule is about the route,
238
- // not about the happy path, and a body that says why is still workspace-shaped text.
239
- const base = { ...JAIL };
240
- if (method !== "GET" && method !== "HEAD") {
241
- return { status: 405, headers: { ...base, Allow: "GET, HEAD" }, body: { error: "this route takes GET, HEAD" } };
242
- }
243
- // Frame and subresource only. The jail's second mechanism is the sandbox of whatever embeds the file,
244
- // and a top-level document has no embedder - it would rest on the header alone, which is the
245
- // one-mechanism state the content boundary forbids, and a top-level document can do more on an origin
246
- // than a framed one can.
247
- //
248
- // A request that says nothing about where it is going is refused with it. Reading an absent header as
249
- // "not a document" would hand that refusal to whoever leaves the header off, and the cost of the
250
- // other way round is visible and small: in a browser that sends no Fetch Metadata, workspace files do
251
- // not render.
252
- const dest = field(headers["sec-fetch-dest"]);
253
- if (dest === "" || dest === "document") {
254
- return { status: 403, headers: base, body: { error: "a workspace file renders inside the tool, not as a page of its own" } };
255
- }
256
- const rest = path.slice(RAW_PREFIX.length);
257
- const cut = rest.indexOf("/");
258
- if (!holds(cut === -1 ? rest : rest.slice(0, cut), rawToken)) {
259
- return { status: 403, headers: base, body: { error: "the workspace files take their token in the path" } };
260
- }
261
- let wanted = "";
262
- try {
263
- wanted = decodePath(cut === -1 ? "" : rest.slice(cut + 1));
264
- } catch {
265
- return { status: 400, headers: base, body: { error: "that path cannot be read" } };
266
- }
267
- try {
268
- const file = module.file(wanted);
269
- return { status: 200, headers: { ...base, "Content-Type": file.mediaType }, body: file.bytes };
270
- } catch (error) {
271
- return { status: statusFor(error), headers: base, body: { error: reason(error) } };
272
- }
273
- }
274
-
275
- return {
276
- /** The root the module resolved, so the mount can say what it is serving. */
277
- root: module.root,
278
- /** The path prefix a workspace file is loaded from, token included. Handed to the frame, never logged. */
279
- rawBase,
280
-
281
- /**
282
- * Where a file the tool wrote sits on disk, or null.
283
- *
284
- * What a typed line is allowed to name. The module refuses any path that is not inside a `.specs/`,
285
- * so a delivery can point an agent at something this tool wrote and at nothing else - not at a file
286
- * of the workspace, and not at anything outside the served root.
287
- */
288
- fileFor(path) {
289
- try {
290
- return module.toolPath(field(path));
291
- } catch {
292
- return null;
293
- }
294
- },
295
-
296
- /**
297
- * Answer one verb from the `specs` helper.
298
- *
299
- * The other half of the coordination channel: the pane writes a file and types a line, and an agent
300
- * moves the work through these. There is no key here on purpose - the caller is gated by being a
301
- * process inside a session this server started, which server.js works out from the caller's pid, the
302
- * way it does for `webpane`. A key on disk would add nothing, since everything in the container can
303
- * read the one that is there.
304
- */
305
- agent({ verb, entry, payload }) {
306
- const name = field(verb);
307
- if (!Object.hasOwn(VERBS, name)) {
308
- return { status: 400, body: { error: `specs: no verb named ${name || "(none)"}` } };
309
- }
310
- try {
311
- return { status: 200, body: VERBS[name](field(entry), payload ?? null) };
312
- } catch (error) {
313
- return { status: statusFor(error), body: { error: reason(error) } };
314
- }
315
- },
316
-
317
- /**
318
- * Answer one request under the Specs prefix.
319
- *
320
- * Takes the request as plain data - method, path, query, headers, body - and gives back `{ status,
321
- * headers, body }`, where the body is a Buffer for a workspace file, an object for everything else,
322
- * and null for an answer that carries none. It never throws.
323
- */
324
- handle({ method, path, query, headers, body }) {
325
- const verb = String(method || "GET").toUpperCase();
326
- const where = String(path || "");
327
- // The path decides first and the method second, so the workspace-file route answers everything
328
- // under it - a preflight included, which it refuses as a method it does not take, with the jail on
329
- // the refusal like every other answer it gives.
330
- if (where.startsWith(RAW_PREFIX)) return raw(verb, where, headers ?? {});
331
- if (where.startsWith(API_PREFIX)) {
332
- if (verb === "OPTIONS") return preflight(where);
333
- return data(verb, where, query ?? {}, body, headers ?? {});
334
- }
335
- return { status: 404, headers: {}, body: { error: "no such Specs route" } };
336
- },
337
-
338
- /**
339
- * Whether this request may carry a body at all.
340
- *
341
- * Asked before a body is read, so nothing without the key reaches a parser - not the JSON one, and not
342
- * the buffering underneath it, which would otherwise hold whatever an anonymous caller sent up to the
343
- * body limit. The rule is the same one the data routes are gated by; it is answered earlier.
344
- */
345
- takesBody(method, path, headers) {
346
- const verb = String(method || "").toUpperCase();
347
- const where = String(path || "");
348
- if ((verb !== "POST" && verb !== "PUT") || !where.startsWith(API_PREFIX)) return false;
349
- if (!routeFor(where)?.[verb]) return false;
350
- return holds(field(headers?.[SPECS_KEY_HEADER]), key);
351
- },
352
-
353
- /** Stop watching the root. The mount holds nothing else that outlives it. */
354
- close() {
355
- module.close();
356
- },
357
- };
358
- }