craftar 0.28.2 → 0.30.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/README.md +52 -8
- package/dist/cli.js +138 -96
- package/dist/cli.js.map +1 -1
- package/dist/core/catalogue.js +5 -0
- package/dist/core/catalogue.js.map +1 -1
- package/dist/core/doctor.js +12 -3
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/drift.js +3 -1
- package/dist/core/drift.js.map +1 -1
- package/dist/core/mcp-protocol.js +228 -0
- package/dist/core/mcp-protocol.js.map +1 -0
- package/dist/core/registry.js +3 -2
- package/dist/core/registry.js.map +1 -1
- package/dist/core/report.js +164 -0
- package/dist/core/report.js.map +1 -0
- package/dist/mcp.js +87 -0
- package/dist/mcp.js.map +1 -0
- package/dist/ui/page.js +393 -0
- package/dist/ui/page.js.map +1 -0
- package/dist/ui/routes.js +132 -0
- package/dist/ui/routes.js.map +1 -0
- package/dist/ui/server.js +122 -0
- package/dist/ui/server.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/ui/page.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9C,MAAM,CAAC,MAAM,SAAS,GAAG;;;;;;;;;;;;;CAaxB,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCvB,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAA2E;IACjG,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE;IACzD,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,EAAE;IAC/D,KAAK,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE;IAC7D,KAAK,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE;IAC9D,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,CAAC,WAAW,CAAC,EAAE;IACtE,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,EAAE;IAC/D,cAAc,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,CAAC,cAAc,CAAC,EAAE;IAC9E,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,CAAC,WAAW,CAAC,EAAE;CACvE,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;cAKnB,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC;;;oBAGrB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;mBACtH,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmT9G,CAAC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { catalogueContext, listIngredients, listRecipes, withLoadWarnings } from "../core/catalogue.js";
|
|
2
|
+
import { renderDiff } from "../core/diff.js";
|
|
3
|
+
import { oneLine } from "../core/doctor.js";
|
|
4
|
+
import { isMissing, listWorkspaces, readRegistry } from "../core/registry.js";
|
|
5
|
+
import { credentialFault, CREDENTIAL_HIDDEN, refHoldsCredentials } from "../core/remote.js";
|
|
6
|
+
import { EXAMPLE_CONTENT_HIDDEN, diffReport, explainFile, statusReport } from "../core/report.js";
|
|
7
|
+
import { loadWorkspace, plan, readLock, status, unsetSummary } from "../core/sync.js";
|
|
8
|
+
import { hashNormalized } from "../core/text.js";
|
|
9
|
+
export const NOT_FOUND = { error: "not found" };
|
|
10
|
+
export const PATH_REQUIRED = { error: "path is required, once" };
|
|
11
|
+
export const MISSING = "the directory or its craftar.yaml is gone — craftar workspaces prune";
|
|
12
|
+
/** A workspace's id in a URL: 16 hex characters of the hash of its registered path — the path itself never travels. */
|
|
13
|
+
export const workspaceId = (registeredPath) => hashNormalized(registeredPath).slice("sha256:".length, "sha256:".length + 16);
|
|
14
|
+
/** Why a workspace cannot be shown, in one line — the wording `doctor` uses, so the table and a 409 agree. */
|
|
15
|
+
export function routeReason(e) {
|
|
16
|
+
return e instanceof Error ? oneLine(e) : String(e).split("\n")[0];
|
|
17
|
+
}
|
|
18
|
+
/** The route a path names, as its pattern — what the one stderr line of a 500 prints, never the request's text. */
|
|
19
|
+
export function routePattern(path) {
|
|
20
|
+
if (path === "/api/workspaces")
|
|
21
|
+
return path;
|
|
22
|
+
const m = /^\/api\/workspaces\/[0-9a-f]{16}(\/(diff|explain|recipes|ingredients))?$/.exec(path);
|
|
23
|
+
return m ? `/api/workspaces/<id>${m[1] ?? ""}` : null;
|
|
24
|
+
}
|
|
25
|
+
/** The query's `path`, given exactly once and decoded once (`+` is not a space) — or null. */
|
|
26
|
+
function onePath(query) {
|
|
27
|
+
const given = query.split("&").filter((kv) => kv === "path" || kv.startsWith("path="));
|
|
28
|
+
if (given.length !== 1)
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
return decodeURIComponent(given[0].slice("path=".length));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A row of the table as the page is given it: its id, and — whatever the core answered — never a Forge URL that
|
|
39
|
+
* `credentialFault` flags. Since 0.28.1 the core gives the label itself for a `missing` or an `error` row; this
|
|
40
|
+
* is the page's own check, a second line: the label is the core's (`CREDENTIAL_HIDDEN`), and the key goes with it.
|
|
41
|
+
* A recorded `ref` that `refHoldsCredentials` flags is given the same label (`CREDENTIAL_HIDDEN`).
|
|
42
|
+
*/
|
|
43
|
+
export function servedRow(r) {
|
|
44
|
+
const row = { id: workspaceId(r.path), ...r };
|
|
45
|
+
if (credentialFault(r.forge.source))
|
|
46
|
+
row.forge = { ...r.forge, source: CREDENTIAL_HIDDEN, key: null };
|
|
47
|
+
if (r.forge.ref !== null && refHoldsCredentials(r.forge.ref))
|
|
48
|
+
row.forge = { ...row.forge, ref: CREDENTIAL_HIDDEN };
|
|
49
|
+
return row;
|
|
50
|
+
}
|
|
51
|
+
export async function route(req, deps) {
|
|
52
|
+
const pattern = routePattern(req.path);
|
|
53
|
+
if (pattern === null)
|
|
54
|
+
return { status: 404, body: NOT_FOUND };
|
|
55
|
+
if (pattern === "/api/workspaces") {
|
|
56
|
+
const t = await listWorkspaces(deps.home, { fetch: false, reason: routeReason });
|
|
57
|
+
const rows = t.rows.map(servedRow);
|
|
58
|
+
return {
|
|
59
|
+
status: 200,
|
|
60
|
+
body: { version: deps.version, registry: t.registry, fetch: false, workspaces: rows, warnings: t.warnings },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const [, , , id, what] = req.path.split("/");
|
|
64
|
+
// Decided from the request alone, before the registry is read.
|
|
65
|
+
let wanted = "";
|
|
66
|
+
if (what === "diff" || what === "explain") {
|
|
67
|
+
const given = onePath(req.query);
|
|
68
|
+
if (given === null)
|
|
69
|
+
return { status: 400, body: PATH_REQUIRED };
|
|
70
|
+
wanted = given;
|
|
71
|
+
}
|
|
72
|
+
// A registry that does not read is not this workspace's fault: it propagates, and the server answers 500.
|
|
73
|
+
const entry = (await readRegistry(deps.home)).workspaces.find((e) => workspaceId(e.path) === id);
|
|
74
|
+
if (!entry)
|
|
75
|
+
return { status: 404, body: NOT_FOUND };
|
|
76
|
+
if (await isMissing(entry))
|
|
77
|
+
return { status: 409, body: { error: MISSING, warnings: [] } };
|
|
78
|
+
// What cannot be loaded, planned or read is this workspace's state: 409, in one line. A failure past that point is ours: 500.
|
|
79
|
+
let ws;
|
|
80
|
+
let p;
|
|
81
|
+
let lock;
|
|
82
|
+
let st;
|
|
83
|
+
try {
|
|
84
|
+
ws = await loadWorkspace(entry.path, { mode: "no-fetch", home: deps.home });
|
|
85
|
+
if (what === "recipes" || what === "ingredients") {
|
|
86
|
+
// The catalogue resolves and never plans; it does not read the lock either.
|
|
87
|
+
const context = catalogueContext(ws.forge, { kind: "workspace", config: ws.config });
|
|
88
|
+
const listing = what === "recipes" ? listRecipes(ws.forge, context) : listIngredients(ws.forge, context);
|
|
89
|
+
return { status: 200, body: withLoadWarnings(listing, ws.warnings) };
|
|
90
|
+
}
|
|
91
|
+
p = await plan(ws);
|
|
92
|
+
lock = await readLock(ws.root);
|
|
93
|
+
st = await status(ws, p, lock);
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
return { status: 409, body: { error: routeReason(e), warnings: ws?.warnings ?? [] } };
|
|
97
|
+
}
|
|
98
|
+
if (what === undefined) {
|
|
99
|
+
const report = statusReport(ws, p, lock, st);
|
|
100
|
+
return {
|
|
101
|
+
status: 200,
|
|
102
|
+
body: { id, path: entry.path, profile: ws.config.profile, recipes: p.resolution.recipes, targets: p.resolution.targets, ...report, refused: report.unsetParams.length ? unsetSummary(report.unsetParams) : null },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (what === "explain") {
|
|
106
|
+
// Never `notProduced`'s message: it quotes the path asked.
|
|
107
|
+
const explained = explainFile(ws, p, wanted);
|
|
108
|
+
return explained ? { status: 200, body: explained } : { status: 404, body: NOT_FOUND };
|
|
109
|
+
}
|
|
110
|
+
// The row whose path is the string asked, character for character: nothing is opened that status() did not name.
|
|
111
|
+
const row = st.find((s) => s.path === wanted);
|
|
112
|
+
if (!row)
|
|
113
|
+
return { status: 404, body: NOT_FOUND };
|
|
114
|
+
const [d] = await diffReport(ws, [row], { withholdMcp: false });
|
|
115
|
+
if (!d)
|
|
116
|
+
return { status: 200, body: { path: row.path, state: row.state, from: null, to: null, hidden: false, note: null, diff: null } };
|
|
117
|
+
const from = `${row.path} (disk, ${row.state})`;
|
|
118
|
+
const to = row.state === "orphan-drift" ? null : row.state === "orphan" ? `${row.path} (forge: no longer produced — sync removes it)` : `${row.path} (forge)`;
|
|
119
|
+
return {
|
|
120
|
+
status: 200,
|
|
121
|
+
body: {
|
|
122
|
+
path: row.path,
|
|
123
|
+
state: row.state,
|
|
124
|
+
from,
|
|
125
|
+
to,
|
|
126
|
+
hidden: d.note === EXAMPLE_CONTENT_HIDDEN,
|
|
127
|
+
note: d.note,
|
|
128
|
+
diff: d.note === null ? renderDiff(d.disk ?? "", d.forge ?? "") : null,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/ui/routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import { errorText } from "../core/schema-fault.js";
|
|
4
|
+
import { PAGE_CSS, PAGE_HTML, PAGE_JS } from "./page.js";
|
|
5
|
+
import { route as defaultRoute, routePattern } from "./routes.js";
|
|
6
|
+
/**
|
|
7
|
+
* The loopback server of `craftar ui` (spec 36 §4.5, §7) — the one file under `src/` that imports `node:http`.
|
|
8
|
+
* It never prints and never exits: what the terminal must see goes through `opts.onError`.
|
|
9
|
+
*
|
|
10
|
+
* Every request passes the same checks, in this order, and the order is the contract (§7.2):
|
|
11
|
+
* method → length of the target → `Host` → `Origin` and `Sec-Fetch-Site` → the route (a static one answers
|
|
12
|
+
* here; an unknown path is 404) → the run token → the queue of one → `route()`.
|
|
13
|
+
*/
|
|
14
|
+
export const MAX_TARGET_BYTES = 2048;
|
|
15
|
+
export const CSP = "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
|
|
16
|
+
/** The names this server answers to. `localhost` is not one: it may resolve elsewhere, and a rebound name could read as it. */
|
|
17
|
+
const names = (port) => (port === 80 ? ["127.0.0.1", "127.0.0.1:80"] : [`127.0.0.1:${port}`]);
|
|
18
|
+
export const hostAllowed = (header, port) => header !== undefined && names(port).includes(header);
|
|
19
|
+
/** Absent is allowed: a browser sends no `Origin` on a navigation or a same-origin GET. `null` is not. */
|
|
20
|
+
export const originAllowed = (header, port) => header === undefined || names(port).some((n) => header === `http://${n}`);
|
|
21
|
+
export const siteAllowed = (header) => header === undefined || header === "same-origin" || header === "none";
|
|
22
|
+
const STATIC = {
|
|
23
|
+
"/": ["text/html; charset=utf-8", PAGE_HTML],
|
|
24
|
+
"/app.js": ["text/javascript; charset=utf-8", PAGE_JS],
|
|
25
|
+
"/app.css": ["text/css; charset=utf-8", PAGE_CSS],
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* A header as sent, once — read from `headersDistinct`, because Node keeps only the first `Host` or `Authorization`
|
|
29
|
+
* of a request that gives two. Given twice, it reads as one value no allowed spelling equals, whichever came first.
|
|
30
|
+
*/
|
|
31
|
+
const one = (req, name) => {
|
|
32
|
+
const values = req.headersDistinct[name];
|
|
33
|
+
return values === undefined ? undefined : values.length === 1 ? values[0] : values.join(", ");
|
|
34
|
+
};
|
|
35
|
+
/** Starts listening; rejects with the bind error, its `code` kept. */
|
|
36
|
+
export function startUi(opts) {
|
|
37
|
+
const token = opts.token ?? randomBytes(32).toString("base64url");
|
|
38
|
+
const want = Buffer.from(`Bearer ${token}`, "utf8");
|
|
39
|
+
const run = opts.route ?? defaultRoute;
|
|
40
|
+
// One data request at a time: each loads a Forge and plans a workspace.
|
|
41
|
+
let queue = Promise.resolve();
|
|
42
|
+
let port = opts.port;
|
|
43
|
+
let closed = false;
|
|
44
|
+
const server = http.createServer((req, res) => {
|
|
45
|
+
const send = (status, type, body, extra = {}) => {
|
|
46
|
+
res.writeHead(status, {
|
|
47
|
+
"Content-Type": type,
|
|
48
|
+
"Cache-Control": "no-store",
|
|
49
|
+
"X-Content-Type-Options": "nosniff",
|
|
50
|
+
"Referrer-Policy": "no-referrer",
|
|
51
|
+
...extra,
|
|
52
|
+
});
|
|
53
|
+
res.end(body);
|
|
54
|
+
};
|
|
55
|
+
const json = (status, body, extra = {}) => send(status, "application/json; charset=utf-8", JSON.stringify(body), extra);
|
|
56
|
+
// Every refusal is the same bytes: which check refused is not said.
|
|
57
|
+
const forbidden = () => json(403, { error: "forbidden" });
|
|
58
|
+
if (req.method !== "GET")
|
|
59
|
+
return json(405, { error: "GET only" }, { Allow: "GET" });
|
|
60
|
+
const target = req.url ?? "/";
|
|
61
|
+
if (Buffer.byteLength(target, "utf8") > MAX_TARGET_BYTES)
|
|
62
|
+
return json(414, { error: "request too long" });
|
|
63
|
+
if (!hostAllowed(one(req, "host"), port))
|
|
64
|
+
return forbidden();
|
|
65
|
+
if (!originAllowed(one(req, "origin"), port) || !siteAllowed(one(req, "sec-fetch-site")))
|
|
66
|
+
return forbidden();
|
|
67
|
+
// The path is split from the query by hand: no URL is built from the request.
|
|
68
|
+
const q = target.indexOf("?");
|
|
69
|
+
const path = q === -1 ? target : target.slice(0, q);
|
|
70
|
+
const page = Object.hasOwn(STATIC, path) ? STATIC[path] : undefined;
|
|
71
|
+
if (page)
|
|
72
|
+
return send(200, page[0], page[1], path === "/" ? { "Content-Security-Policy": CSP } : {});
|
|
73
|
+
const pattern = routePattern(path);
|
|
74
|
+
if (pattern === null)
|
|
75
|
+
return json(404, { error: "not found" });
|
|
76
|
+
// The token: in the Authorization header and nowhere else, compared in constant time.
|
|
77
|
+
const got = Buffer.from(one(req, "authorization") ?? "", "utf8");
|
|
78
|
+
if (got.length !== want.length || !timingSafeEqual(got, want))
|
|
79
|
+
return forbidden();
|
|
80
|
+
// One request's failure must neither end the process nor stop the queue (see the catch at the end).
|
|
81
|
+
queue = queue.then(async () => {
|
|
82
|
+
// Work still queued when the server closed is dropped: its connection is gone, and nothing should keep loading.
|
|
83
|
+
if (closed)
|
|
84
|
+
return;
|
|
85
|
+
try {
|
|
86
|
+
const answer = await run({ path, query: q === -1 ? "" : target.slice(q + 1) }, { home: opts.home, version: opts.version });
|
|
87
|
+
json(answer.status, answer.body);
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
// The pattern, never the request's text; the browser learns nothing of the message.
|
|
91
|
+
opts.onError(`ui: ${pattern}: ${errorText(e).split("\n")[0]}`);
|
|
92
|
+
json(500, { error: "internal error" });
|
|
93
|
+
}
|
|
94
|
+
}).catch(() => {
|
|
95
|
+
// Only the catch above can throw: a failing `onError`, or an answer that can no longer be written. Nothing
|
|
96
|
+
// more can be said then; the connection is closed rather than left unanswered, and the queue goes on.
|
|
97
|
+
res.destroy();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
server.headersTimeout = 10_000;
|
|
101
|
+
server.requestTimeout = 30_000;
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
server.once("error", reject);
|
|
104
|
+
// The literal address: no option, flag or variable changes where this listens.
|
|
105
|
+
server.listen(opts.port, "127.0.0.1", () => {
|
|
106
|
+
const address = server.address();
|
|
107
|
+
port = address.port;
|
|
108
|
+
resolve({
|
|
109
|
+
url: `http://127.0.0.1:${port}/#${token}`,
|
|
110
|
+
port,
|
|
111
|
+
address,
|
|
112
|
+
close: () => new Promise((done) => {
|
|
113
|
+
closed = true;
|
|
114
|
+
server.close(() => done());
|
|
115
|
+
// A browser's keep-alive connection, or a route still running, would hold the listener open; that route's answer is abandoned.
|
|
116
|
+
server.closeAllConnections();
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/ui/server.ts"],"names":[],"mappings":"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"}
|