@nanonets/graft 0.16.0 → 0.19.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 +10 -3
- package/dist/app/brain-build-process.d.ts +22 -0
- package/dist/app/brain-build-process.d.ts.map +1 -0
- package/dist/app/brain-build-process.js +129 -0
- package/dist/app/brain-build-process.js.map +1 -0
- package/dist/app/brain-build-worker.d.ts +35 -0
- package/dist/app/brain-build-worker.d.ts.map +1 -0
- package/dist/app/brain-build-worker.js +56 -0
- package/dist/app/brain-build-worker.js.map +1 -0
- package/dist/app/brain-build.d.ts +160 -0
- package/dist/app/brain-build.d.ts.map +1 -0
- package/dist/app/brain-build.js +278 -0
- package/dist/app/brain-build.js.map +1 -0
- package/dist/app/checkout.d.ts +53 -3
- package/dist/app/checkout.d.ts.map +1 -1
- package/dist/app/checkout.js +195 -27
- package/dist/app/checkout.js.map +1 -1
- package/dist/app/events.d.ts +7 -0
- package/dist/app/events.d.ts.map +1 -1
- package/dist/app/events.js +7 -2
- package/dist/app/events.js.map +1 -1
- package/dist/app/history.d.ts +128 -0
- package/dist/app/history.d.ts.map +1 -0
- package/dist/app/history.js +266 -0
- package/dist/app/history.js.map +1 -0
- package/dist/app/identity.d.ts +56 -0
- package/dist/app/identity.d.ts.map +1 -1
- package/dist/app/identity.js +119 -0
- package/dist/app/identity.js.map +1 -1
- package/dist/app/main.js +14 -0
- package/dist/app/main.js.map +1 -1
- package/dist/app/pages.d.ts +17 -0
- package/dist/app/pages.d.ts.map +1 -1
- package/dist/app/pages.js +112 -5
- package/dist/app/pages.js.map +1 -1
- package/dist/app/review-process.d.ts +66 -0
- package/dist/app/review-process.d.ts.map +1 -0
- package/dist/app/review-process.js +199 -0
- package/dist/app/review-process.js.map +1 -0
- package/dist/app/review-worker.d.ts +2 -0
- package/dist/app/review-worker.d.ts.map +1 -0
- package/dist/app/review-worker.js +108 -0
- package/dist/app/review-worker.js.map +1 -0
- package/dist/app/review.d.ts.map +1 -1
- package/dist/app/review.js +4 -1
- package/dist/app/review.js.map +1 -1
- package/dist/app/server.d.ts +31 -2
- package/dist/app/server.d.ts.map +1 -1
- package/dist/app/server.js +130 -3
- package/dist/app/server.js.map +1 -1
- package/dist/app/sources.d.ts +68 -0
- package/dist/app/sources.d.ts.map +1 -0
- package/dist/app/sources.js +342 -0
- package/dist/app/sources.js.map +1 -0
- package/dist/ask/ask.d.ts +6 -0
- package/dist/ask/ask.d.ts.map +1 -1
- package/dist/ask/ask.js +30 -2
- package/dist/ask/ask.js.map +1 -1
- package/dist/brain/attach.d.ts +44 -0
- package/dist/brain/attach.d.ts.map +1 -0
- package/dist/brain/attach.js +63 -0
- package/dist/brain/attach.js.map +1 -0
- package/dist/brain/connect.d.ts +59 -0
- package/dist/brain/connect.d.ts.map +1 -0
- package/dist/brain/connect.js +80 -0
- package/dist/brain/connect.js.map +1 -0
- package/dist/brain/link.d.ts +89 -0
- package/dist/brain/link.d.ts.map +1 -0
- package/dist/brain/link.js +146 -0
- package/dist/brain/link.js.map +1 -0
- package/dist/brain/push.d.ts +72 -0
- package/dist/brain/push.d.ts.map +1 -0
- package/dist/brain/push.js +191 -0
- package/dist/brain/push.js.map +1 -0
- package/dist/brain/signup.d.ts +73 -0
- package/dist/brain/signup.d.ts.map +1 -0
- package/dist/brain/signup.js +186 -0
- package/dist/brain/signup.js.map +1 -0
- package/dist/brain/watch.d.ts +136 -0
- package/dist/brain/watch.d.ts.map +1 -0
- package/dist/brain/watch.js +172 -0
- package/dist/brain/watch.js.map +1 -0
- package/dist/brain/wire.d.ts +38 -0
- package/dist/brain/wire.d.ts.map +1 -0
- package/dist/brain/wire.js +133 -0
- package/dist/brain/wire.js.map +1 -0
- package/dist/claude/format.d.ts.map +1 -1
- package/dist/claude/format.js +10 -3
- package/dist/claude/format.js.map +1 -1
- package/dist/claude/hooks.d.ts.map +1 -1
- package/dist/claude/hooks.js +32 -1
- package/dist/claude/hooks.js.map +1 -1
- package/dist/claude/init.d.ts +5 -0
- package/dist/claude/init.d.ts.map +1 -1
- package/dist/claude/init.js +9 -1
- package/dist/claude/init.js.map +1 -1
- package/dist/claude/session-metrics.d.ts +10 -0
- package/dist/claude/session-metrics.d.ts.map +1 -1
- package/dist/claude/session-metrics.js +25 -0
- package/dist/claude/session-metrics.js.map +1 -1
- package/dist/claude/settings-merge.d.ts +18 -0
- package/dist/claude/settings-merge.d.ts.map +1 -1
- package/dist/claude/settings-merge.js +42 -8
- package/dist/claude/settings-merge.js.map +1 -1
- package/dist/claude/skill-template.d.ts.map +1 -1
- package/dist/claude/skill-template.js +6 -0
- package/dist/claude/skill-template.js.map +1 -1
- package/dist/claude/state.d.ts +14 -0
- package/dist/claude/state.d.ts.map +1 -1
- package/dist/claude/state.js.map +1 -1
- package/dist/claude/tally.d.ts +27 -0
- package/dist/claude/tally.d.ts.map +1 -1
- package/dist/claude/tally.js +74 -1
- package/dist/claude/tally.js.map +1 -1
- package/dist/cli.js +310 -3
- package/dist/cli.js.map +1 -1
- package/dist/context/node-file.d.ts +4 -10
- package/dist/context/node-file.d.ts.map +1 -1
- package/dist/context/node-file.js +6 -2
- package/dist/context/node-file.js.map +1 -1
- package/dist/context/price.d.ts +49 -0
- package/dist/context/price.d.ts.map +1 -0
- package/dist/context/price.js +83 -0
- package/dist/context/price.js.map +1 -0
- package/dist/context/savings.d.ts +13 -3
- package/dist/context/savings.d.ts.map +1 -1
- package/dist/context/savings.js +44 -5
- package/dist/context/savings.js.map +1 -1
- package/dist/hosts/claude-global.d.ts +33 -0
- package/dist/hosts/claude-global.d.ts.map +1 -0
- package/dist/hosts/claude-global.js +118 -0
- package/dist/hosts/claude-global.js.map +1 -0
- package/dist/hosts/plan.d.ts.map +1 -1
- package/dist/hosts/plan.js +5 -1
- package/dist/hosts/plan.js.map +1 -1
- package/dist/hosts/retract.d.ts.map +1 -1
- package/dist/hosts/retract.js +33 -13
- package/dist/hosts/retract.js.map +1 -1
- package/dist/hosts/sections.d.ts +23 -2
- package/dist/hosts/sections.d.ts.map +1 -1
- package/dist/hosts/sections.js +25 -9
- package/dist/hosts/sections.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +33 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +5 -1
- package/dist/mcp/tools.js.map +1 -1
- package/dist/telemetry/contract.d.ts +12 -0
- package/dist/telemetry/contract.d.ts.map +1 -1
- package/dist/telemetry/contract.js +19 -0
- package/dist/telemetry/contract.js.map +1 -1
- package/dist/telemetry/gate.d.ts +14 -0
- package/dist/telemetry/gate.d.ts.map +1 -1
- package/dist/telemetry/gate.js +18 -2
- package/dist/telemetry/gate.js.map +1 -1
- package/dist/upkeep-run.d.ts.map +1 -1
- package/dist/upkeep-run.js +12 -2
- package/dist/upkeep-run.js.map +1 -1
- package/dist/upkeep.d.ts +18 -0
- package/dist/upkeep.d.ts.map +1 -1
- package/dist/upkeep.js +38 -0
- package/dist/upkeep.js.map +1 -1
- package/dist/util/state.d.ts +9 -0
- package/dist/util/state.d.ts.map +1 -1
- package/dist/util/state.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running a review somewhere it cannot stop the App answering.
|
|
3
|
+
*
|
|
4
|
+
* A review is synchronous from end to end, and not in one place we could fix: the
|
|
5
|
+
* pull request is fetched with `spawnSync` (`./checkout.ts`), the whole repository
|
|
6
|
+
* is parsed in one `files.forEach` loop of native tree-sitter calls
|
|
7
|
+
* (`graph/build.ts`), the graph is written with a blocking `JSON.stringify`, and
|
|
8
|
+
* the viewer page is exported synchronously too. In-process that holds the event
|
|
9
|
+
* loop for the entire review.
|
|
10
|
+
*
|
|
11
|
+
* That is not a theory. On the deployed App, probing `GET /p/<id>` from outside
|
|
12
|
+
* every ten seconds while one review ran: three consecutive timeouts (no response
|
|
13
|
+
* at all, not even with `--max-time 45`), then 404 in 0.9s the moment the queue
|
|
14
|
+
* emptied. Meanwhile the box's load average was 0.96 on eight vCPUs — one thread
|
|
15
|
+
* pegged, seven cores idle — and the App's own logs put a single review at 92s to
|
|
16
|
+
* 274s ("3 changed → 0 affected in 180134ms"). So a *single* in-flight review took
|
|
17
|
+
* the whole HTTP surface down for minutes: the container's 5s `HEALTHCHECK` failed,
|
|
18
|
+
* Docker marked the container unhealthy, and pages the App had already produced
|
|
19
|
+
* were unreachable. On a busy repository that is most of the working day.
|
|
20
|
+
*
|
|
21
|
+
* Neither bounding the queue nor lowering `GRAFT_CONCURRENCY` touches this —
|
|
22
|
+
* concurrency was already 2 and one job is enough. Cooperative yielding inside the
|
|
23
|
+
* parse loop does not either: a `git fetch` of a large repository is one
|
|
24
|
+
* uninterruptible `spawnSync` that can legitimately take a minute, and it is not
|
|
25
|
+
* even using a core while it blocks. The work has to leave the process.
|
|
26
|
+
*
|
|
27
|
+
* A child process rather than a `worker_threads` worker, deliberately:
|
|
28
|
+
*
|
|
29
|
+
* - **The native grammars keep loading the way they already do.** `graph/extract.ts`
|
|
30
|
+
* imports nine tree-sitter addons at module scope and constructs a `Parser` there
|
|
31
|
+
* too, and `graph/generic.ts` loads more through `createRequire`. In a worker all
|
|
32
|
+
* of that is re-instantiated per thread, which puts the N-API-in-worker behaviour
|
|
33
|
+
* of nine third-party native modules between the App and every review. A fork runs
|
|
34
|
+
* byte-identical code to what production runs today.
|
|
35
|
+
* - **A review can be killed.** A wedged parse or a 10-minute clone is a process
|
|
36
|
+
* to SIGKILL, not a thread with no safe way to stop it.
|
|
37
|
+
* - **A crash stops being an outage.** An OOM kill or a grammar segfault on a
|
|
38
|
+
* stranger's source now costs one review and is reported as a failure, instead
|
|
39
|
+
* of taking the HTTP server with it. This process clones code written by
|
|
40
|
+
* people who opened a pull request; that isolation is worth having on its own.
|
|
41
|
+
* - **The heap goes away.** A repository's graph is freed by the child exiting
|
|
42
|
+
* rather than left for the server process to fragment around.
|
|
43
|
+
*
|
|
44
|
+
* The parent keeps exactly the two things a review cannot mint for itself, and
|
|
45
|
+
* hands each across the IPC channel rather than argv or the environment (`ps` and
|
|
46
|
+
* `/proc/<pid>/environ` are readable; an installation token belongs in neither):
|
|
47
|
+
* the token, so the App's private key never crosses the boundary, and page
|
|
48
|
+
* publishing, because the store and the secret it signs URLs with live with the
|
|
49
|
+
* server.
|
|
50
|
+
*/
|
|
51
|
+
import { fork } from "node:child_process";
|
|
52
|
+
import { existsSync } from "node:fs";
|
|
53
|
+
import { fileURLToPath } from "node:url";
|
|
54
|
+
import { jobKey } from "./events.js";
|
|
55
|
+
/**
|
|
56
|
+
* How long one review may take before the process running it is killed.
|
|
57
|
+
*
|
|
58
|
+
* Generously above the worst honest review observed in production (274s) and above
|
|
59
|
+
* what `checkout.ts`'s own 120s-per-git-call budget can add up to, because the cost
|
|
60
|
+
* of being wrong here is a review that never happens. It is a ceiling on a *lost*
|
|
61
|
+
* queue slot, not on responsiveness — the server no longer cares how long a review
|
|
62
|
+
* runs, only that the slot comes back.
|
|
63
|
+
*/
|
|
64
|
+
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
65
|
+
/**
|
|
66
|
+
* The module a review runs in.
|
|
67
|
+
*
|
|
68
|
+
* `dist/app/review-worker.js` in an installed or containerised App. Run from a
|
|
69
|
+
* checkout there is no `dist`, so fall back to the TypeScript source next door:
|
|
70
|
+
* `fork` inherits `process.execArgv`, so a parent started through tsx hands the
|
|
71
|
+
* child the same loader and a `.ts` entry runs. Resolved once, at import.
|
|
72
|
+
*
|
|
73
|
+
* Exported so a test can assert the path a deployed App will fork actually exists —
|
|
74
|
+
* getting this wrong is the one failure here that no unit test of the protocol
|
|
75
|
+
* would catch and every review would hit.
|
|
76
|
+
*/
|
|
77
|
+
export function reviewWorkerEntry() {
|
|
78
|
+
const js = fileURLToPath(new URL("./review-worker.js", import.meta.url));
|
|
79
|
+
if (existsSync(js))
|
|
80
|
+
return js;
|
|
81
|
+
const ts = js.replace(/\.js$/, ".ts");
|
|
82
|
+
return existsSync(ts) ? ts : js;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Live review processes.
|
|
86
|
+
*
|
|
87
|
+
* `main.ts` force-exits 25 seconds after SIGTERM if a review has not finished
|
|
88
|
+
* draining. That used to kill the review along with the process; now the review is
|
|
89
|
+
* a separate process that would outlive us, still holding a clone made with an
|
|
90
|
+
* installation token. So the last thing this process does is take them with it.
|
|
91
|
+
*
|
|
92
|
+
* A container stop is unaffected either way: Docker signals PID 1, not the process
|
|
93
|
+
* group, so a review keeps going through the grace period and the drain in `main.ts`
|
|
94
|
+
* means what it says. Ctrl-C in a terminal signals the whole group, so the children
|
|
95
|
+
* die with the default disposition and their reviews are reported as killed — which
|
|
96
|
+
* is what an interrupted review was before this too.
|
|
97
|
+
*/
|
|
98
|
+
const live = new Set();
|
|
99
|
+
let hooked = false;
|
|
100
|
+
function track(child) {
|
|
101
|
+
live.add(child);
|
|
102
|
+
if (hooked)
|
|
103
|
+
return;
|
|
104
|
+
hooked = true;
|
|
105
|
+
process.on("exit", () => {
|
|
106
|
+
for (const c of live)
|
|
107
|
+
c.kill("SIGKILL");
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** A dead channel is not worth an exception: the exit handler already covers it. */
|
|
111
|
+
function post(child, msg) {
|
|
112
|
+
if (!child.connected)
|
|
113
|
+
return;
|
|
114
|
+
try {
|
|
115
|
+
child.send(msg);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* channel closed under us — `exit` will settle the review */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A reviewer with `reviewPullRequest`'s signature that runs it in a child process.
|
|
123
|
+
*
|
|
124
|
+
* Same shape on purpose: `server.ts` swaps one for the other in a single line, and
|
|
125
|
+
* every existing test that injects its own reviewer through `AppSeams.review` keeps
|
|
126
|
+
* running in-process, which is what a test wants.
|
|
127
|
+
*/
|
|
128
|
+
export function childReviewer(opts = {}) {
|
|
129
|
+
const entry = opts.entry ?? reviewWorkerEntry();
|
|
130
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
131
|
+
return (job, deps) => runInChild(job, deps, entry, timeoutMs);
|
|
132
|
+
}
|
|
133
|
+
async function runInChild(job, deps, entry, timeoutMs) {
|
|
134
|
+
const log = deps.log ?? (() => { });
|
|
135
|
+
// Minted here rather than in the child: this is a network round trip, never a
|
|
136
|
+
// blocking one, and the cache that owns installation tokens — including the
|
|
137
|
+
// forget-a-rejected-token behaviour — belongs to the one process that sees every
|
|
138
|
+
// delivery.
|
|
139
|
+
const token = await deps.token(job.installationId);
|
|
140
|
+
// The job key as argv makes `ps` in a container say which pull request each
|
|
141
|
+
// review is; the token stays out of it.
|
|
142
|
+
const child = fork(entry, [jobKey(job)], { stdio: ["ignore", "inherit", "inherit", "ipc"] });
|
|
143
|
+
track(child);
|
|
144
|
+
try {
|
|
145
|
+
return await new Promise((resolve, reject) => {
|
|
146
|
+
let settled = false;
|
|
147
|
+
const settle = (fn) => {
|
|
148
|
+
if (settled)
|
|
149
|
+
return;
|
|
150
|
+
settled = true;
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
fn();
|
|
153
|
+
};
|
|
154
|
+
const timer = setTimeout(() => {
|
|
155
|
+
settle(() => reject(new Error(`review exceeded ${timeoutMs}ms and was killed`)));
|
|
156
|
+
}, timeoutMs);
|
|
157
|
+
timer.unref();
|
|
158
|
+
child.on("message", (raw) => {
|
|
159
|
+
const msg = raw;
|
|
160
|
+
if (msg.t === "log") {
|
|
161
|
+
log(msg.msg);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (msg.t === "publish") {
|
|
165
|
+
// Publishing happens here because the store and its signing secret do.
|
|
166
|
+
// A page that cannot be stored costs the link, never the review — the
|
|
167
|
+
// same contract `review.ts` already has with a null `publish`.
|
|
168
|
+
const stored = deps.publish ? deps.publish(job, msg.html) : Promise.resolve(null);
|
|
169
|
+
void stored
|
|
170
|
+
.catch(() => null)
|
|
171
|
+
.then((url) => post(child, { t: "published", seq: msg.seq, url }));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (msg.t === "done") {
|
|
175
|
+
settle(() => resolve(msg.result));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
settle(() => reject(new Error(msg.message)));
|
|
179
|
+
});
|
|
180
|
+
child.on("error", (err) => settle(() => reject(err)));
|
|
181
|
+
// The case that used to be an App-wide outage: a child that dies without
|
|
182
|
+
// reporting. An OOM kill, a segfault in a grammar on hostile source. Now it
|
|
183
|
+
// is one failed review, named, with the signal that did it.
|
|
184
|
+
child.on("exit", (code, signal) => settle(() => reject(new Error(`review process exited ${signal ? `on ${signal}` : `with code ${code}`} before reporting a result`))));
|
|
185
|
+
post(child, { t: "start", job, token, api: deps.api });
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
live.delete(child);
|
|
190
|
+
// Whatever happened — done, failed, timed out, or a delivery superseded by the
|
|
191
|
+
// queue — nothing forked here is allowed to outlive the review. `review.ts`
|
|
192
|
+
// deletes the checkout before it reports, so this leaves nothing behind.
|
|
193
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
194
|
+
child.kill("SIGKILL");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** The App's default reviewer. */
|
|
198
|
+
export const reviewInChildProcess = childReviewer();
|
|
199
|
+
//# sourceMappingURL=review-process.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-process.js","sourceRoot":"","sources":["../../src/app/review-process.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-worker.d.ts","sourceRoot":"","sources":["../../src/app/review-worker.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One review, in its own process. Forked by `./review-process.ts`, never imported.
|
|
3
|
+
*
|
|
4
|
+
* The whole file is glue: it waits for a `start`, calls the same
|
|
5
|
+
* `reviewPullRequest` the server used to call directly, and reports back. Nothing
|
|
6
|
+
* about a review changes by running here — that is the point. The two dependencies
|
|
7
|
+
* it cannot satisfy locally are wired to the parent instead:
|
|
8
|
+
*
|
|
9
|
+
* - `token` is handed over in the start message. This process never holds the
|
|
10
|
+
* App's private key, so a hostile repository that somehow got code running here
|
|
11
|
+
* finds one repository's short-lived token and not the App's identity.
|
|
12
|
+
* - `publish` is a round trip. The page store and the webhook secret its signed
|
|
13
|
+
* URLs are keyed on live with the HTTP server, and a page is only useful to the
|
|
14
|
+
* process that will serve it.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately no `graft` state of its own: no lock, no cache directory, no
|
|
17
|
+
* telemetry. A review works on a throwaway clone in /tmp and takes its findings
|
|
18
|
+
* with it when it exits.
|
|
19
|
+
*/
|
|
20
|
+
import { reviewPullRequest } from "./review.js";
|
|
21
|
+
/** Publish requests waiting on the parent, by sequence number. */
|
|
22
|
+
const awaiting = new Map();
|
|
23
|
+
let seq = 0;
|
|
24
|
+
/** Fire and forget. `process.send` throws on a closed channel, and a log line is
|
|
25
|
+
* not worth crashing a review over — the `disconnect` handler below has already
|
|
26
|
+
* decided what a closed channel means. */
|
|
27
|
+
const send = (msg) => {
|
|
28
|
+
if (!process.connected)
|
|
29
|
+
return;
|
|
30
|
+
try {
|
|
31
|
+
process.send?.(msg);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* parent went away mid-review */
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
/** Close the channel, or leave if it is already gone. Either way this is the end of
|
|
38
|
+
* the process — see the `disconnect` handler below. */
|
|
39
|
+
function bye() {
|
|
40
|
+
try {
|
|
41
|
+
if (process.connected)
|
|
42
|
+
process.disconnect?.();
|
|
43
|
+
else
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The last word, then close the channel.
|
|
52
|
+
*
|
|
53
|
+
* Sent with a callback because that callback is the flush: `done` has to be on the
|
|
54
|
+
* wire before the channel closes, or the parent sees a process that exited without
|
|
55
|
+
* reporting and turns a finished review into a failure.
|
|
56
|
+
*/
|
|
57
|
+
function finish(msg) {
|
|
58
|
+
if (!process.connected)
|
|
59
|
+
return bye();
|
|
60
|
+
try {
|
|
61
|
+
process.send?.(msg, () => bye());
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
bye();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function run(start) {
|
|
68
|
+
try {
|
|
69
|
+
const result = await reviewPullRequest(start.job, {
|
|
70
|
+
token: async () => start.token,
|
|
71
|
+
fetch: globalThis.fetch,
|
|
72
|
+
api: start.api,
|
|
73
|
+
publish: (_job, html) => new Promise((resolve) => {
|
|
74
|
+
const id = (seq += 1);
|
|
75
|
+
awaiting.set(id, resolve);
|
|
76
|
+
send({ t: "publish", seq: id, html });
|
|
77
|
+
}),
|
|
78
|
+
log: (msg) => send({ t: "log", msg }),
|
|
79
|
+
});
|
|
80
|
+
finish({ t: "done", result });
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
// `reviewPullRequest` has already run the token through `redact`, so this
|
|
84
|
+
// message is safe to put in the parent's log — which is where it is going.
|
|
85
|
+
finish({ t: "failed", message: err instanceof Error ? err.message : String(err) });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* No parent, no point.
|
|
90
|
+
*
|
|
91
|
+
* A review whose channel has closed has nowhere to publish a page and nobody to
|
|
92
|
+
* report to. If the server process died, nothing will SIGKILL this one either, and
|
|
93
|
+
* without this it would sit in a clone for the rest of the parent's 15-minute
|
|
94
|
+
* ceiling. Going now leaves the same throwaway tree behind that a SIGKILL would —
|
|
95
|
+
* what it does not do is spend a quarter of an hour producing a review no one can
|
|
96
|
+
* receive.
|
|
97
|
+
*/
|
|
98
|
+
process.on("disconnect", () => process.exit(0));
|
|
99
|
+
process.on("message", (raw) => {
|
|
100
|
+
const msg = raw;
|
|
101
|
+
if (msg.t === "start") {
|
|
102
|
+
void run(msg);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
awaiting.get(msg.seq)?.(msg.url);
|
|
106
|
+
awaiting.delete(msg.seq);
|
|
107
|
+
});
|
|
108
|
+
//# sourceMappingURL=review-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-worker.js","sourceRoot":"","sources":["../../src/app/review-worker.ts"],"names":[],"mappings":"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"}
|
package/dist/app/review.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../../src/app/review.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE3C,iFAAiF;AACjF,eAAO,MAAM,MAAM,gCAAgC,CAAC;AAIpD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,KAAK,EAAE,KAAK,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;qBAEiB;IACjB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnE,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../../src/app/review.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE3C,iFAAiF;AACjF,eAAO,MAAM,MAAM,gCAAgC,CAAC;AAIpD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,KAAK,EAAE,KAAK,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;qBAEiB;IACjB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnE,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAsD/F"}
|
package/dist/app/review.js
CHANGED
|
@@ -21,7 +21,10 @@ const DEPTH = 2;
|
|
|
21
21
|
export async function reviewPullRequest(job, deps) {
|
|
22
22
|
const log = deps.log ?? (() => { });
|
|
23
23
|
const token = await deps.token(job.installationId);
|
|
24
|
-
|
|
24
|
+
// `log` goes into the checkout because a closed pull request is reviewed from
|
|
25
|
+
// `refs/pull/N/head` against a different basis than an open one, and the only
|
|
26
|
+
// place that difference is visible afterwards is this line.
|
|
27
|
+
const checkout = checkoutPullRequest({ owner: job.owner, repo: job.repo, number: job.number, baseRef: job.baseRef, token, log });
|
|
25
28
|
try {
|
|
26
29
|
log(`${job.owner}/${job.repo}#${job.number}: building`);
|
|
27
30
|
await buildGraph(checkout.dir);
|
package/dist/app/review.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review.js","sourceRoot":"","sources":["../../src/app/review.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAI5D,iFAAiF;AACjF,MAAM,CAAC,MAAM,MAAM,GAAG,6BAA6B,CAAC;AAEpD,MAAM,KAAK,GAAG,CAAC,CAAC;AAqBhB,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,GAAc,EAAE,IAAgB;IACtE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,mBAAmB,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"review.js","sourceRoot":"","sources":["../../src/app/review.ts"],"names":[],"mappings":"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"}
|
package/dist/app/server.d.ts
CHANGED
|
@@ -9,24 +9,53 @@
|
|
|
9
9
|
* GitHub retries a delivery it considers failed and gives up after ten seconds,
|
|
10
10
|
* while a review takes tens of them — so the handler validates, queues, and
|
|
11
11
|
* answers 202. Everything real happens on the queue.
|
|
12
|
+
*
|
|
13
|
+
* "On the queue" used to mean "on this event loop", and that was the whole of a
|
|
14
|
+
* multi-minute outage: a review is synchronous end to end, so one in flight left
|
|
15
|
+
* this server unable to answer /healthz or serve a page it had already produced.
|
|
16
|
+
* The queue now runs each review in its own process (`./review-process.ts`) and
|
|
17
|
+
* everything in here is I/O again.
|
|
12
18
|
*/
|
|
13
19
|
import { type Server } from "node:http";
|
|
14
20
|
import { type ReviewJob } from "./events.js";
|
|
15
21
|
import { type AppCredentials, type Fetch } from "./identity.js";
|
|
22
|
+
import { buildRepoIntoBrain, checkRepoAccess } from "./brain-build.js";
|
|
16
23
|
import { PageStore } from "./pages.js";
|
|
17
24
|
import { WorkQueue } from "./queue.js";
|
|
18
|
-
import { reviewPullRequest } from "./review.js";
|
|
25
|
+
import type { reviewPullRequest } from "./review.js";
|
|
19
26
|
/** Seams for tests: nothing here has a default that touches the network. */
|
|
20
27
|
export interface AppSeams {
|
|
21
28
|
fetch?: Fetch;
|
|
22
|
-
/** Swapped out
|
|
29
|
+
/** Swapped out so a test can assert what the route handed the builder without
|
|
30
|
+
* cloning a repository or reaching GitHub. */
|
|
31
|
+
brainBuild?: typeof buildRepoIntoBrain;
|
|
32
|
+
/** Swapped out so a test can assert the access route's answers without
|
|
33
|
+
* reaching GitHub. */
|
|
34
|
+
brainAccess?: typeof checkRepoAccess;
|
|
35
|
+
/** Swapped out to assert what the queue was handed, without a clone — and, being
|
|
36
|
+
* called in-process, without the fork the default reviewer does. */
|
|
23
37
|
review?: typeof reviewPullRequest;
|
|
24
38
|
now?: () => number;
|
|
25
39
|
}
|
|
26
40
|
export interface AppConfig extends AppCredentials {
|
|
27
41
|
webhookSecret: string;
|
|
42
|
+
/** Shared secret for `POST /brain/build`. That route is called by the
|
|
43
|
+
* platform, not by GitHub, so it cannot use the webhook's payload signature —
|
|
44
|
+
* it takes a bearer token instead. Unset disables the route entirely, which
|
|
45
|
+
* is the right default for a deployment that only reviews pull requests. */
|
|
46
|
+
brainBuildSecret?: string;
|
|
47
|
+
/** Platform base URL the digest is posted to. Defaults to production. */
|
|
48
|
+
brainBaseUrl?: string;
|
|
49
|
+
/** Token for reading public repositories the App is not installed on.
|
|
50
|
+
* Optional — without it the read borrows one of our own installation's
|
|
51
|
+
* tokens, and only falls back to anonymous if the App has none. */
|
|
52
|
+
publicToken?: string;
|
|
53
|
+
/** Whose installation to borrow for that. Defaults to ours. */
|
|
54
|
+
publicOwner?: string;
|
|
28
55
|
/** Public origin, for the links put in comments, e.g. https://graft.example.com */
|
|
29
56
|
publicUrl: string;
|
|
57
|
+
/** Where pages are kept, so a restart does not strand the links already posted. */
|
|
58
|
+
pageDir?: string;
|
|
30
59
|
port?: number;
|
|
31
60
|
concurrency?: number;
|
|
32
61
|
api?: string;
|
package/dist/app/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/app/server.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/app/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAEjG,OAAO,EAAwB,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAuC,KAAK,cAAc,EAAE,KAAK,KAAK,EAAE,MAAM,eAAe,CAAC;AACrG,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAA8C,MAAM,kBAAkB,CAAC;AAEnH,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAKrD,4EAA4E;AAC5E,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd;kDAC8C;IAC9C,UAAU,CAAC,EAAE,OAAO,kBAAkB,CAAC;IACvC;0BACsB;IACtB,WAAW,CAAC,EAAE,OAAO,eAAe,CAAC;IACrC;wEACoE;IACpE,MAAM,CAAC,EAAE,OAAO,iBAAiB,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAU,SAAQ,cAAc;IAC/C,aAAa,EAAE,MAAM,CAAC;IACtB;;;gFAG4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;uEAEmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,wBAAgB,SAAS,CACvB,MAAM,EAAE,SAAS,EACjB,KAAK,GAAE,QAAa,GACnB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,CAqLnE"}
|
package/dist/app/server.js
CHANGED
|
@@ -9,21 +9,30 @@
|
|
|
9
9
|
* GitHub retries a delivery it considers failed and gives up after ten seconds,
|
|
10
10
|
* while a review takes tens of them — so the handler validates, queues, and
|
|
11
11
|
* answers 202. Everything real happens on the queue.
|
|
12
|
+
*
|
|
13
|
+
* "On the queue" used to mean "on this event loop", and that was the whole of a
|
|
14
|
+
* multi-minute outage: a review is synchronous end to end, so one in flight left
|
|
15
|
+
* this server unable to answer /healthz or serve a page it had already produced.
|
|
16
|
+
* The queue now runs each review in its own process (`./review-process.ts`) and
|
|
17
|
+
* everything in here is I/O again.
|
|
12
18
|
*/
|
|
13
19
|
import { createServer } from "node:http";
|
|
20
|
+
import { timingSafeEqual } from "node:crypto";
|
|
14
21
|
import { jobKey, reviewJobFor } from "./events.js";
|
|
15
22
|
import { InstallationTokens, verifySignature } from "./identity.js";
|
|
23
|
+
import { checkRepoAccess, RepoNotAccessibleError } from "./brain-build.js";
|
|
24
|
+
import { buildRepoInChildProcess } from "./brain-build-process.js";
|
|
16
25
|
import { PageStore } from "./pages.js";
|
|
17
26
|
import { WorkQueue } from "./queue.js";
|
|
18
|
-
import {
|
|
27
|
+
import { reviewInChildProcess } from "./review-process.js";
|
|
19
28
|
/** A webhook body larger than this is not a pull_request event we can use. */
|
|
20
29
|
const MAX_BODY_BYTES = 2 * 1024 * 1024;
|
|
21
30
|
export function createApp(config, seams = {}) {
|
|
22
31
|
const log = config.log ?? ((msg) => console.log(msg));
|
|
23
32
|
const fetchImpl = seams.fetch ?? globalThis.fetch;
|
|
24
|
-
const review = seams.review ??
|
|
33
|
+
const review = seams.review ?? reviewInChildProcess;
|
|
25
34
|
const tokens = new InstallationTokens(config, fetchImpl, seams.now ?? Date.now, config.api);
|
|
26
|
-
const pages = new PageStore({ secret: config.webhookSecret });
|
|
35
|
+
const pages = new PageStore({ secret: config.webhookSecret, dir: config.pageDir });
|
|
27
36
|
const origin = config.publicUrl.replace(/\/$/, "");
|
|
28
37
|
const queue = new WorkQueue(async (job) => {
|
|
29
38
|
const started = Date.now();
|
|
@@ -88,6 +97,112 @@ export function createApp(config, seams = {}) {
|
|
|
88
97
|
log(`${jobKey(decided.job)}: queued${decided.job.fromFork ? " (fork)" : ""}`);
|
|
89
98
|
return send(res, 202, "application/json", JSON.stringify({ queued: true }));
|
|
90
99
|
}
|
|
100
|
+
// Can we read this repository? The cheap question, answered on its own.
|
|
101
|
+
//
|
|
102
|
+
// Same secret as /brain/build, because it is the same caller and the same
|
|
103
|
+
// trust. Kept separate from it because it costs a second where the build
|
|
104
|
+
// costs minutes, and the platform needs the answer before it can decide
|
|
105
|
+
// which screen to show.
|
|
106
|
+
if (req.method === "POST" && url.pathname === "/brain/access") {
|
|
107
|
+
if (!config.brainBuildSecret)
|
|
108
|
+
return send(res, 404, "text/plain", "not found");
|
|
109
|
+
if (!bearerMatches(header(req, "authorization"), config.brainBuildSecret)) {
|
|
110
|
+
return send(res, 401, "text/plain", "unauthorized");
|
|
111
|
+
}
|
|
112
|
+
const body = await readBody(req);
|
|
113
|
+
if (body === null)
|
|
114
|
+
return send(res, 413, "text/plain", "payload too large");
|
|
115
|
+
let ask;
|
|
116
|
+
try {
|
|
117
|
+
ask = JSON.parse(body);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return send(res, 400, "text/plain", "bad json");
|
|
121
|
+
}
|
|
122
|
+
if (!ask.owner || !ask.repo) {
|
|
123
|
+
return send(res, 400, "application/json", JSON.stringify({ error: "owner and repo are required" }));
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const access = await (seams.brainAccess ?? checkRepoAccess)({ owner: ask.owner, repo: ask.repo }, {
|
|
127
|
+
creds: config,
|
|
128
|
+
fetch: fetchImpl,
|
|
129
|
+
api: config.api,
|
|
130
|
+
publicToken: config.publicToken,
|
|
131
|
+
publicOwner: config.publicOwner,
|
|
132
|
+
log,
|
|
133
|
+
now: seams.now,
|
|
134
|
+
});
|
|
135
|
+
// 200 either way: "we cannot see it" is an answer, not a failure, and
|
|
136
|
+
// the platform turns it into a choice rather than an error.
|
|
137
|
+
return send(res, 200, "application/json", JSON.stringify(access));
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
141
|
+
log(`brain access ${ask.owner}/${ask.repo} failed: ${msg}`);
|
|
142
|
+
return send(res, 502, "application/json", JSON.stringify({ error: msg }));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Build one repository into a brain, on demand. Called by the platform when
|
|
146
|
+
// someone pastes a repository URL during onboarding — there is no webhook
|
|
147
|
+
// behind it, so it authenticates with a bearer token rather than a payload
|
|
148
|
+
// signature.
|
|
149
|
+
if (req.method === "POST" && url.pathname === "/brain/build") {
|
|
150
|
+
if (!config.brainBuildSecret)
|
|
151
|
+
return send(res, 404, "text/plain", "not found");
|
|
152
|
+
// Compared in constant time for the same reason verifySignature is: this
|
|
153
|
+
// is a long-lived shared secret, and a timing oracle on it is worth more
|
|
154
|
+
// to an attacker than one on a per-payload signature.
|
|
155
|
+
if (!bearerMatches(header(req, "authorization"), config.brainBuildSecret)) {
|
|
156
|
+
return send(res, 401, "text/plain", "unauthorized");
|
|
157
|
+
}
|
|
158
|
+
const body = await readBody(req);
|
|
159
|
+
if (body === null)
|
|
160
|
+
return send(res, 413, "text/plain", "payload too large");
|
|
161
|
+
let job;
|
|
162
|
+
try {
|
|
163
|
+
job = JSON.parse(body);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return send(res, 400, "text/plain", "bad json");
|
|
167
|
+
}
|
|
168
|
+
// brainId/brainToken are optional: without them the digest comes back in
|
|
169
|
+
// the response for the caller to ingest itself. See BrainBuildJob.
|
|
170
|
+
if (!job.owner || !job.repo) {
|
|
171
|
+
return send(res, 400, "application/json", JSON.stringify({ error: "owner and repo are required" }));
|
|
172
|
+
}
|
|
173
|
+
// Synchronous on purpose: the caller is a person waiting on a screen, and
|
|
174
|
+
// the platform's own import job is what makes the SLOW half (extraction and
|
|
175
|
+
// placement) asynchronous. This half is a shallow clone and a graph build.
|
|
176
|
+
try {
|
|
177
|
+
// The deployment's platform URL wins over anything a caller sends, so a
|
|
178
|
+
// request cannot redirect a repository's history to another host.
|
|
179
|
+
if (config.brainBaseUrl)
|
|
180
|
+
job.brainBaseUrl = config.brainBaseUrl;
|
|
181
|
+
// Out of this process by default. A read is minutes of blocking work
|
|
182
|
+
// and the App has to keep answering — including the one-second access
|
|
183
|
+
// question the next person's onboarding is waiting on. A test that
|
|
184
|
+
// injects its own builder still runs in-process, which is what a test
|
|
185
|
+
// wants.
|
|
186
|
+
const built = await (seams.brainBuild ?? buildRepoInChildProcess)(job, {
|
|
187
|
+
creds: config,
|
|
188
|
+
fetch: fetchImpl,
|
|
189
|
+
api: config.api,
|
|
190
|
+
publicToken: config.publicToken,
|
|
191
|
+
publicOwner: config.publicOwner,
|
|
192
|
+
log,
|
|
193
|
+
now: seams.now,
|
|
194
|
+
});
|
|
195
|
+
return send(res, 202, "application/json", JSON.stringify(built));
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
if (e instanceof RepoNotAccessibleError) {
|
|
199
|
+
return send(res, 404, "application/json", JSON.stringify({ error: e.message, ...e.gap }));
|
|
200
|
+
}
|
|
201
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
202
|
+
log(`brain build ${job.owner}/${job.repo} failed: ${msg}`);
|
|
203
|
+
return send(res, 502, "application/json", JSON.stringify({ error: msg }));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
91
206
|
return send(res, 404, "text/plain", "not found");
|
|
92
207
|
}
|
|
93
208
|
if (config.port !== undefined)
|
|
@@ -98,6 +213,18 @@ const header = (req, name) => {
|
|
|
98
213
|
const v = req.headers[name];
|
|
99
214
|
return Array.isArray(v) ? v[0] : v;
|
|
100
215
|
};
|
|
216
|
+
/** Constant-time `Authorization: Bearer <secret>` check. */
|
|
217
|
+
function bearerMatches(headerValue, secret) {
|
|
218
|
+
const prefix = "Bearer ";
|
|
219
|
+
if (!headerValue || !headerValue.startsWith(prefix))
|
|
220
|
+
return false;
|
|
221
|
+
const given = Buffer.from(headerValue.slice(prefix.length));
|
|
222
|
+
const want = Buffer.from(secret);
|
|
223
|
+
// timingSafeEqual throws on a length mismatch, which is itself a leak of the
|
|
224
|
+
// secret's length — but one byte of length is not worth a branch that skips
|
|
225
|
+
// the comparison, and the lengths here are fixed by deployment config.
|
|
226
|
+
return given.length === want.length && timingSafeEqual(given, want);
|
|
227
|
+
}
|
|
101
228
|
function send(res, status, type, body, extra = {}) {
|
|
102
229
|
res.writeHead(status, { "content-type": type, ...extra });
|
|
103
230
|
res.end(body);
|