craftar 0.29.0 → 0.30.1
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 +25 -7
- package/dist/cli.js +63 -6
- package/dist/cli.js.map +1 -1
- package/dist/core/doctor.js +1 -1
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/impact.js +10 -5
- package/dist/core/impact.js.map +1 -1
- package/dist/core/integrations.js +31 -6
- package/dist/core/integrations.js.map +1 -1
- package/dist/core/registry.js +3 -2
- package/dist/core/registry.js.map +1 -1
- package/dist/core/report.js +5 -1
- package/dist/core/report.js.map +1 -1
- package/dist/importers/claude-code.js +11 -5
- package/dist/importers/claude-code.js.map +1 -1
- package/dist/importers/drift-promote.js +24 -11
- package/dist/importers/drift-promote.js.map +1 -1
- package/dist/importers/kiro-report.js +11 -3
- package/dist/importers/kiro-report.js.map +1 -1
- 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,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":"AAAA,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,KAAK,IAAI,YAAY,EAAE,YAAY,EAAuD,MAAM,aAAa,CAAC;AAEvH;;;;;;;GAOG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AACrC,MAAM,CAAC,MAAM,GAAG,GACd,0IAA0I,CAAC;AAsB7I,+HAA+H;AAC/H,MAAM,KAAK,GAAG,CAAC,IAAY,EAAY,EAAE,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,CAAC;AAEhH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAA0B,EAAE,IAAY,EAAW,EAAE,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACvI,0GAA0G;AAC1G,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,MAA0B,EAAE,IAAY,EAAW,EAAE,CACjF,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,KAAK,UAAU,CAAC,EAAE,CAAC,CAAC;AAC5E,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAA0B,EAAW,EAAE,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,aAAa,IAAI,MAAM,KAAK,MAAM,CAAC;AAE1I,MAAM,MAAM,GAA0D;IACpE,GAAG,EAAE,CAAC,0BAA0B,EAAE,SAAS,CAAC;IAC5C,SAAS,EAAE,CAAC,gCAAgC,EAAE,OAAO,CAAC;IACtD,UAAU,EAAE,CAAC,yBAAyB,EAAE,QAAQ,CAAC;CAClD,CAAC;AAEF;;;GAGG;AACH,MAAM,GAAG,GAAG,CAAC,GAAyB,EAAE,IAAY,EAAsB,EAAE;IAC1E,MAAM,MAAM,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACzC,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAChG,CAAC,CAAC;AAEF,sEAAsE;AACtE,MAAM,UAAU,OAAO,CAAC,IAAe;IACrC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAClE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,KAAK,EAAE,EAAE,MAAM,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,IAAI,YAAY,CAAC;IACvC,wEAAwE;IACxE,IAAI,KAAK,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAC7C,IAAI,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACrB,IAAI,MAAM,GAAG,KAAK,CAAC;IAEnB,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAC5C,MAAM,IAAI,GAAG,CAAC,MAAc,EAAE,IAAY,EAAE,IAAY,EAAE,QAAgC,EAAE,EAAQ,EAAE;YACpG,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE;gBACpB,cAAc,EAAE,IAAI;gBACpB,eAAe,EAAE,UAAU;gBAC3B,wBAAwB,EAAE,SAAS;gBACnC,iBAAiB,EAAE,aAAa;gBAChC,GAAG,KAAK;aACT,CAAC,CAAC;YACH,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChB,CAAC,CAAC;QACF,MAAM,IAAI,GAAG,CAAC,MAAc,EAAE,IAAa,EAAE,QAAgC,EAAE,EAAQ,EAAE,CACvF,IAAI,CAAC,MAAM,EAAE,iCAAiC,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;QAC/E,oEAAoE;QACpE,MAAM,SAAS,GAAG,GAAS,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;QAEhE,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QACpF,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC;QAC9B,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,gBAAgB;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC,CAAC;QAC1G,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC;YAAE,OAAO,SAAS,EAAE,CAAC;QAC7D,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;YAAE,OAAO,SAAS,EAAE,CAAC;QAE7G,8EAA8E;QAC9E,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9B,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACpE,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,yBAAyB,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACrG,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;QAE/D,sFAAsF;QACtF,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,eAAe,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC;QACjE,IAAI,GAAG,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC;YAAE,OAAO,SAAS,EAAE,CAAC;QAElF,oGAAoG;QACpG,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YAC5B,gHAAgH;YAChH,IAAI,MAAM;gBAAE,OAAO;YACnB,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;gBAC3H,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;YACnC,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,oFAAoF;gBACpF,IAAI,CAAC,OAAO,CAAC,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;gBAC/D,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;YACzC,CAAC;QACH,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;YACZ,2GAA2G;YAC3G,sGAAsG;YACtG,GAAG,CAAC,OAAO,EAAE,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,MAAM,CAAC,cAAc,GAAG,MAAM,CAAC;IAC/B,MAAM,CAAC,cAAc,GAAG,MAAM,CAAC;IAE/B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7B,+EAA+E;QAC/E,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE;YACzC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAiB,CAAC;YAChD,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;YACpB,OAAO,CAAC;gBACN,GAAG,EAAE,oBAAoB,IAAI,KAAK,KAAK,EAAE;gBACzC,IAAI;gBACJ,OAAO;gBACP,KAAK,EAAE,GAAG,EAAE,CACV,IAAI,OAAO,CAAO,CAAC,IAAI,EAAE,EAAE;oBACzB,MAAM,GAAG,IAAI,CAAC;oBACd,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;oBAC3B,+HAA+H;oBAC/H,MAAM,CAAC,mBAAmB,EAAE,CAAC;gBAC/B,CAAC,CAAC;aACL,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
|