@nanonets/graft 0.12.1 → 0.15.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 +20 -7
- package/dist/ai/crux.d.ts.map +1 -1
- package/dist/ai/crux.js +22 -1
- package/dist/ai/crux.js.map +1 -1
- package/dist/ai/llm/factory.d.ts +5 -1
- package/dist/ai/llm/factory.d.ts.map +1 -1
- package/dist/ai/llm/factory.js +8 -0
- package/dist/ai/llm/factory.js.map +1 -1
- package/dist/ai/llm/litellm.d.ts +34 -0
- package/dist/ai/llm/litellm.d.ts.map +1 -0
- package/dist/ai/llm/litellm.js +45 -0
- package/dist/ai/llm/litellm.js.map +1 -0
- package/dist/ai/llm/openai.d.ts.map +1 -1
- package/dist/ai/llm/openai.js +66 -7
- package/dist/ai/llm/openai.js.map +1 -1
- package/dist/ai/llm/recover-tool.d.ts +19 -0
- package/dist/ai/llm/recover-tool.d.ts.map +1 -0
- package/dist/ai/llm/recover-tool.js +109 -0
- package/dist/ai/llm/recover-tool.js.map +1 -0
- package/dist/ai/providers.d.ts.map +1 -1
- package/dist/ai/providers.js +2 -0
- package/dist/ai/providers.js.map +1 -1
- package/dist/ai/synthesize.d.ts.map +1 -1
- package/dist/ai/synthesize.js +19 -5
- package/dist/ai/synthesize.js.map +1 -1
- package/dist/app/checkout.d.ts +28 -0
- package/dist/app/checkout.d.ts.map +1 -0
- package/dist/app/checkout.js +100 -0
- package/dist/app/checkout.js.map +1 -0
- package/dist/app/events.d.ts +37 -0
- package/dist/app/events.d.ts.map +1 -0
- package/dist/app/events.js +59 -0
- package/dist/app/events.js.map +1 -0
- package/dist/app/identity.d.ts +54 -0
- package/dist/app/identity.d.ts.map +1 -0
- package/dist/app/identity.js +96 -0
- package/dist/app/identity.js.map +1 -0
- package/dist/app/main.d.ts +2 -0
- package/dist/app/main.d.ts.map +1 -0
- package/dist/app/main.js +56 -0
- package/dist/app/main.js.map +1 -0
- package/dist/app/pages.d.ts +39 -0
- package/dist/app/pages.d.ts.map +1 -0
- package/dist/app/pages.js +86 -0
- package/dist/app/pages.js.map +1 -0
- package/dist/app/queue.d.ts +40 -0
- package/dist/app/queue.d.ts.map +1 -0
- package/dist/app/queue.js +68 -0
- package/dist/app/queue.js.map +1 -0
- package/dist/app/review.d.ts +23 -0
- package/dist/app/review.d.ts.map +1 -0
- package/dist/app/review.js +129 -0
- package/dist/app/review.js.map +1 -0
- package/dist/app/server.d.ts +40 -0
- package/dist/app/server.d.ts.map +1 -0
- package/dist/app/server.js +117 -0
- package/dist/app/server.js.map +1 -0
- package/dist/ask/ask.d.ts +42 -0
- package/dist/ask/ask.d.ts.map +1 -1
- package/dist/ask/ask.js +442 -57
- package/dist/ask/ask.js.map +1 -1
- package/dist/ask/file-rank.d.ts +72 -0
- package/dist/ask/file-rank.d.ts.map +1 -0
- package/dist/ask/file-rank.js +160 -0
- package/dist/ask/file-rank.js.map +1 -0
- package/dist/ask/file-selection.d.ts +22 -0
- package/dist/ask/file-selection.d.ts.map +1 -0
- package/dist/ask/file-selection.js +66 -0
- package/dist/ask/file-selection.js.map +1 -0
- package/dist/ask/fuse.d.ts +16 -0
- package/dist/ask/fuse.d.ts.map +1 -1
- package/dist/ask/fuse.js +21 -4
- package/dist/ask/fuse.js.map +1 -1
- package/dist/ask/graphrank.d.ts +18 -0
- package/dist/ask/graphrank.d.ts.map +1 -1
- package/dist/ask/graphrank.js +65 -19
- package/dist/ask/graphrank.js.map +1 -1
- package/dist/blast/blast-cli.d.ts +5 -0
- package/dist/blast/blast-cli.d.ts.map +1 -1
- package/dist/blast/blast-cli.js +16 -1
- package/dist/blast/blast-cli.js.map +1 -1
- package/dist/blast/blast.d.ts +9 -0
- package/dist/blast/blast.d.ts.map +1 -1
- package/dist/blast/blast.js.map +1 -1
- package/dist/blast/evidence.d.ts +51 -0
- package/dist/blast/evidence.d.ts.map +1 -0
- package/dist/blast/evidence.js +162 -0
- package/dist/blast/evidence.js.map +1 -0
- package/dist/blast/owners.d.ts +85 -0
- package/dist/blast/owners.d.ts.map +1 -0
- package/dist/blast/owners.js +272 -0
- package/dist/blast/owners.js.map +1 -0
- package/dist/blast/render.d.ts +11 -2
- package/dist/blast/render.d.ts.map +1 -1
- package/dist/blast/render.js +125 -4
- package/dist/blast/render.js.map +1 -1
- package/dist/blast/viz.d.ts +20 -0
- package/dist/blast/viz.d.ts.map +1 -1
- package/dist/blast/viz.js +13 -157
- package/dist/blast/viz.js.map +1 -1
- package/dist/claude/format.js +1 -1
- package/dist/claude/format.js.map +1 -1
- package/dist/claude/hooks.d.ts.map +1 -1
- package/dist/claude/hooks.js +99 -11
- package/dist/claude/hooks.js.map +1 -1
- package/dist/claude/settings-merge.d.ts +11 -0
- package/dist/claude/settings-merge.d.ts.map +1 -1
- package/dist/claude/settings-merge.js +24 -10
- package/dist/claude/settings-merge.js.map +1 -1
- package/dist/claude/state.d.ts +14 -1
- package/dist/claude/state.d.ts.map +1 -1
- package/dist/claude/state.js +1 -1
- package/dist/claude/state.js.map +1 -1
- package/dist/claude/stats.d.ts.map +1 -1
- package/dist/claude/stats.js +2 -1
- package/dist/claude/stats.js.map +1 -1
- package/dist/claude/statusline.d.ts +5 -3
- package/dist/claude/statusline.d.ts.map +1 -1
- package/dist/claude/statusline.js +6 -4
- package/dist/claude/statusline.js.map +1 -1
- package/dist/claude/sync-run.d.ts.map +1 -1
- package/dist/claude/sync-run.js +10 -2
- package/dist/claude/sync-run.js.map +1 -1
- package/dist/claude/tally.d.ts +23 -0
- package/dist/claude/tally.d.ts.map +1 -0
- package/dist/claude/tally.js +138 -0
- package/dist/claude/tally.js.map +1 -0
- package/dist/cli.js +138 -1
- package/dist/cli.js.map +1 -1
- package/dist/context/build.d.ts.map +1 -1
- package/dist/context/build.js +23 -4
- package/dist/context/build.js.map +1 -1
- package/dist/context/check.d.ts.map +1 -1
- package/dist/context/check.js +2 -1
- package/dist/context/check.js.map +1 -1
- package/dist/context/node-file.d.ts +2 -1
- package/dist/context/node-file.d.ts.map +1 -1
- package/dist/context/node-file.js +37 -2
- package/dist/context/node-file.js.map +1 -1
- package/dist/engine.d.ts +2 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -0
- package/dist/engine.js.map +1 -1
- package/dist/graph/bindings.d.ts +25 -3
- package/dist/graph/bindings.d.ts.map +1 -1
- package/dist/graph/bindings.js +168 -2
- package/dist/graph/bindings.js.map +1 -1
- package/dist/graph/build.d.ts +4 -0
- package/dist/graph/build.d.ts.map +1 -1
- package/dist/graph/build.js +7 -4
- package/dist/graph/build.js.map +1 -1
- package/dist/graph/check.d.ts +3 -0
- package/dist/graph/check.d.ts.map +1 -1
- package/dist/graph/check.js +58 -11
- package/dist/graph/check.js.map +1 -1
- package/dist/graph/enrich.d.ts.map +1 -1
- package/dist/graph/enrich.js +29 -9
- package/dist/graph/enrich.js.map +1 -1
- package/dist/graph/extract.d.ts +22 -2
- package/dist/graph/extract.d.ts.map +1 -1
- package/dist/graph/extract.js +1444 -44
- package/dist/graph/extract.js.map +1 -1
- package/dist/graph/fingerprint.d.ts +6 -1
- package/dist/graph/fingerprint.d.ts.map +1 -1
- package/dist/graph/fingerprint.js +7 -3
- package/dist/graph/fingerprint.js.map +1 -1
- package/dist/graph/generic.d.ts.map +1 -1
- package/dist/graph/generic.js +39 -7
- package/dist/graph/generic.js.map +1 -1
- package/dist/graph/invariants.d.ts +3 -2
- package/dist/graph/invariants.d.ts.map +1 -1
- package/dist/graph/invariants.js +6 -3
- package/dist/graph/invariants.js.map +1 -1
- package/dist/graph/queries/dart.scm +42 -0
- package/dist/graph/queries/lua.scm +36 -0
- package/dist/graph/queries/nix.scm +32 -0
- package/dist/graph/refresh.d.ts.map +1 -1
- package/dist/graph/refresh.js +6 -2
- package/dist/graph/refresh.js.map +1 -1
- package/dist/graph/resolve.d.ts +1 -1
- package/dist/graph/resolve.d.ts.map +1 -1
- package/dist/graph/resolve.js +211 -26
- package/dist/graph/resolve.js.map +1 -1
- package/dist/graph/scopes.d.ts.map +1 -1
- package/dist/graph/scopes.js +2 -1
- package/dist/graph/scopes.js.map +1 -1
- package/dist/graph/source-files.d.ts +7 -2
- package/dist/graph/source-files.d.ts.map +1 -1
- package/dist/graph/source-files.js +19 -6
- package/dist/graph/source-files.js.map +1 -1
- package/dist/graph/traverse-cli.d.ts +13 -2
- package/dist/graph/traverse-cli.d.ts.map +1 -1
- package/dist/graph/traverse-cli.js +25 -4
- package/dist/graph/traverse-cli.js.map +1 -1
- package/dist/graph/workspace-cli.d.ts +2 -0
- package/dist/graph/workspace-cli.d.ts.map +1 -1
- package/dist/graph/workspace-cli.js +3 -0
- package/dist/graph/workspace-cli.js.map +1 -1
- package/dist/graph/workspace.d.ts.map +1 -1
- package/dist/graph/workspace.js +26 -2
- package/dist/graph/workspace.js.map +1 -1
- package/dist/hosts/mcp-config.d.ts +12 -0
- package/dist/hosts/mcp-config.d.ts.map +1 -1
- package/dist/hosts/mcp-config.js +54 -5
- package/dist/hosts/mcp-config.js.map +1 -1
- package/dist/hosts/registry.d.ts.map +1 -1
- package/dist/hosts/registry.js +20 -0
- package/dist/hosts/registry.js.map +1 -1
- package/dist/hosts/retract.d.ts +47 -0
- package/dist/hosts/retract.d.ts.map +1 -0
- package/dist/hosts/retract.js +473 -0
- package/dist/hosts/retract.js.map +1 -0
- package/dist/ingest/fs.d.ts +19 -5
- package/dist/ingest/fs.d.ts.map +1 -1
- package/dist/ingest/fs.js +35 -9
- package/dist/ingest/fs.js.map +1 -1
- package/dist/telemetry/contract.d.ts.map +1 -1
- package/dist/telemetry/contract.js +7 -2
- package/dist/telemetry/contract.js.map +1 -1
- package/dist/telemetry/sessions.d.ts.map +1 -1
- package/dist/telemetry/sessions.js +6 -0
- package/dist/telemetry/sessions.js.map +1 -1
- package/dist/util/state.d.ts +25 -2
- package/dist/util/state.d.ts.map +1 -1
- package/dist/util/state.js +24 -2
- package/dist/util/state.js.map +1 -1
- package/dist/viewer/app.js +2 -1
- package/dist/viewer/style.css +14 -0
- package/dist/viz/assemble.d.ts +17 -0
- package/dist/viz/assemble.d.ts.map +1 -1
- package/dist/viz/assemble.js.map +1 -1
- package/package.json +19 -4
- package/scripts/graph-quality.mjs +3 -3
- package/scripts/tally-audit.mjs +140 -0
- package/dist/ai/llm/claude-cli.d.ts +0 -60
- package/dist/ai/llm/claude-cli.d.ts.map +0 -1
- package/dist/ai/llm/claude-cli.js +0 -417
- package/dist/ai/llm/claude-cli.js.map +0 -1
- package/dist/util/num.d.ts +0 -3
- package/dist/util/num.d.ts.map +0 -1
- package/dist/util/num.js +0 -20
- package/dist/util/num.js.map +0 -1
package/dist/ai/synthesize.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recoverToolArgsFromContent, warnToolChoiceIgnored } from "./llm/recover-tool.js";
|
|
1
2
|
const SYSTEM_PROMPT = `You build an ARCHITECTURE graph of a codebase from per-file summaries. The reader is an AI agent that will read this graph before working on the code, so it must describe the system at the level a senior engineer would explain it — not file by file.
|
|
2
3
|
|
|
3
4
|
Produce a CURATED set of nodes of mixed granularity:
|
|
@@ -107,11 +108,24 @@ export class ChatSynthesizer {
|
|
|
107
108
|
{ role: "user", content: userContent(files) },
|
|
108
109
|
],
|
|
109
110
|
});
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
return clean(nodesFromResponse(res)?.nodes);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Prefer a real tool call; if the gateway ignored `tool_choice` (#129), recover
|
|
116
|
+
* the same payload from `content`. Recovered args still go through {@link clean}.
|
|
117
|
+
*/
|
|
118
|
+
function nodesFromResponse(res) {
|
|
119
|
+
const call = res.toolCalls.find((c) => c.name === RECORD_TOOL) ?? res.toolCalls[0];
|
|
120
|
+
if (call?.args && typeof call.args === "object" && !Array.isArray(call.args)) {
|
|
121
|
+
return call.args;
|
|
115
122
|
}
|
|
123
|
+
const recovered = recoverToolArgsFromContent(res.text, {
|
|
124
|
+
toolNames: [RECORD_TOOL, "emit_json"],
|
|
125
|
+
payloadKey: "nodes",
|
|
126
|
+
});
|
|
127
|
+
if (!recovered)
|
|
128
|
+
warnToolChoiceIgnored("synthesize", res.text?.trim() ? "unparsed" : "empty");
|
|
129
|
+
return recovered;
|
|
116
130
|
}
|
|
117
131
|
//# sourceMappingURL=synthesize.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"synthesize.js","sourceRoot":"","sources":["../../src/ai/synthesize.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"synthesize.js","sourceRoot":"","sources":["../../src/ai/synthesize.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface CheckoutRequest {
|
|
2
|
+
owner: string;
|
|
3
|
+
repo: string;
|
|
4
|
+
number: number;
|
|
5
|
+
baseRef: string;
|
|
6
|
+
token: string;
|
|
7
|
+
api?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface Checkout {
|
|
10
|
+
/** Working tree at the pull request's merge commit. */
|
|
11
|
+
dir: string;
|
|
12
|
+
/** What `blast --base` should diff against. */
|
|
13
|
+
base: string;
|
|
14
|
+
/** Removes the tree. Always call it — the token's clone is not something to
|
|
15
|
+
* leave in /tmp. */
|
|
16
|
+
cleanup: () => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Fetch the PR's merge ref and its base, shallow.
|
|
20
|
+
*
|
|
21
|
+
* `refs/pull/N/merge` is GitHub's own merge of the PR into its base — the same
|
|
22
|
+
* thing the checks see, and the right thing to review: it is what will land, not
|
|
23
|
+
* what the contributor's branch says in isolation.
|
|
24
|
+
*/
|
|
25
|
+
export declare function checkoutPullRequest(req: CheckoutRequest): Checkout;
|
|
26
|
+
/** Never let a token reach a log line, even inside git's own error text. */
|
|
27
|
+
export declare function redact(text: string, token: string): string;
|
|
28
|
+
//# sourceMappingURL=checkout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../../src/app/checkout.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb;wBACoB;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAkCD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,eAAe,GAAG,QAAQ,CAoClE;AAED,4EAA4E;AAC5E,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAI1D"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Getting a pull request's code onto disk without running any of it.
|
|
3
|
+
*
|
|
4
|
+
* On a fork PR every byte here was written by a stranger, and this service holds
|
|
5
|
+
* an installation token for the base repository. So the rules are absolute:
|
|
6
|
+
*
|
|
7
|
+
* - **Nothing is executed.** No `npm install`, no build, no hooks. The graph is
|
|
8
|
+
* produced by tree-sitter reading source text, which runs none of it.
|
|
9
|
+
* - **Git is told not to run things either.** A repository can carry hooks, and
|
|
10
|
+
* `core.hooksPath` is a per-repo config a clone would otherwise honour; both
|
|
11
|
+
* are disabled explicitly rather than assumed absent.
|
|
12
|
+
* - **The token never lands in the repo.** It is passed per-invocation through
|
|
13
|
+
* an http header config, not baked into a remote URL that `git remote -v`,
|
|
14
|
+
* the reflog and any submodule would carry.
|
|
15
|
+
* - **Bounded.** A hostile or merely enormous repository must not hold a worker
|
|
16
|
+
* forever: every git call has a timeout, and the fetch is shallow.
|
|
17
|
+
*/
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
/** Enough history for a merge base against the pull request's base branch. */
|
|
23
|
+
const FETCH_DEPTH = 50;
|
|
24
|
+
const GIT_TIMEOUT_MS = 120_000;
|
|
25
|
+
/** A git invocation with the dangerous parts of the environment removed. */
|
|
26
|
+
function git(dir, args, token) {
|
|
27
|
+
const auth = token
|
|
28
|
+
? [
|
|
29
|
+
"-c",
|
|
30
|
+
// Basic auth with the token as the password, per GitHub's own guidance for
|
|
31
|
+
// installation tokens. Passed as an argument to THIS call so it is never
|
|
32
|
+
// written into .git/config.
|
|
33
|
+
`http.extraheader=Authorization: Basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`,
|
|
34
|
+
]
|
|
35
|
+
: [];
|
|
36
|
+
const res = spawnSync("git", ["-c", "core.hooksPath=/dev/null", "-c", "protocol.version=2", ...auth, ...args], {
|
|
37
|
+
cwd: dir,
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
timeout: GIT_TIMEOUT_MS,
|
|
40
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
41
|
+
env: {
|
|
42
|
+
...process.env,
|
|
43
|
+
// No credential prompts, no system config, no repo-supplied helpers.
|
|
44
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
45
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
46
|
+
GIT_ASKPASS: "",
|
|
47
|
+
HOME: dir,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return { ok: res.status === 0, err: `${res.stderr ?? ""}${res.error ? ` ${res.error.message}` : ""}`.trim() };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Fetch the PR's merge ref and its base, shallow.
|
|
54
|
+
*
|
|
55
|
+
* `refs/pull/N/merge` is GitHub's own merge of the PR into its base — the same
|
|
56
|
+
* thing the checks see, and the right thing to review: it is what will land, not
|
|
57
|
+
* what the contributor's branch says in isolation.
|
|
58
|
+
*/
|
|
59
|
+
export function checkoutPullRequest(req) {
|
|
60
|
+
const host = (req.api ?? "https://github.com").replace(/\/$/, "");
|
|
61
|
+
const dir = mkdtempSync(join(tmpdir(), `graft-app-${req.owner}-${req.repo}-`));
|
|
62
|
+
const cleanup = () => rmSync(dir, { recursive: true, force: true });
|
|
63
|
+
const steps = [
|
|
64
|
+
["init", ["init", "--quiet", "-b", "__graft_base"]],
|
|
65
|
+
["remote", ["remote", "add", "origin", `${host}/${req.owner}/${req.repo}.git`]],
|
|
66
|
+
// Both refs in one fetch: the merge ref to review, the base to diff against.
|
|
67
|
+
[
|
|
68
|
+
"fetch",
|
|
69
|
+
[
|
|
70
|
+
"fetch",
|
|
71
|
+
"--quiet",
|
|
72
|
+
`--depth=${FETCH_DEPTH}`,
|
|
73
|
+
"--no-recurse-submodules",
|
|
74
|
+
"origin",
|
|
75
|
+
`+refs/pull/${req.number}/merge:refs/graft/merge`,
|
|
76
|
+
`+refs/heads/${req.baseRef}:refs/graft/base`,
|
|
77
|
+
],
|
|
78
|
+
],
|
|
79
|
+
["checkout", ["checkout", "--quiet", "--detach", "refs/graft/merge"]],
|
|
80
|
+
];
|
|
81
|
+
for (const [name, args] of steps) {
|
|
82
|
+
const { ok, err } = git(dir, args, name === "fetch" ? req.token : undefined);
|
|
83
|
+
if (!ok) {
|
|
84
|
+
cleanup();
|
|
85
|
+
// A merge ref is absent while GitHub is still computing mergeability, and
|
|
86
|
+
// present-but-stale right after a push — worth saying which step failed so
|
|
87
|
+
// that case is distinguishable from a permissions problem.
|
|
88
|
+
throw new Error(`checkout ${req.owner}/${req.repo}#${req.number} failed at ${name}: ${redact(err, req.token)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { dir, base: "refs/graft/base", cleanup };
|
|
92
|
+
}
|
|
93
|
+
/** Never let a token reach a log line, even inside git's own error text. */
|
|
94
|
+
export function redact(text, token) {
|
|
95
|
+
if (!token)
|
|
96
|
+
return text;
|
|
97
|
+
const encoded = Buffer.from(`x-access-token:${token}`).toString("base64");
|
|
98
|
+
return text.split(token).join("[token]").split(encoded).join("[token]");
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=checkout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkout.js","sourceRoot":"","sources":["../../src/app/checkout.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What arrived, and whether it is worth work.
|
|
3
|
+
*
|
|
4
|
+
* A busy installation delivers a lot of noise — labels, assignments, reviews —
|
|
5
|
+
* and every accepted delivery costs a clone and a parse. Narrowing happens here,
|
|
6
|
+
* once, so the queue only ever holds jobs that will produce a comment.
|
|
7
|
+
*/
|
|
8
|
+
/** The subset of a `pull_request` delivery this App reads. */
|
|
9
|
+
export interface ReviewJob {
|
|
10
|
+
installationId: number;
|
|
11
|
+
owner: string;
|
|
12
|
+
repo: string;
|
|
13
|
+
/** Base repository's full name, which is where the comment goes — a fork PR
|
|
14
|
+
* carries a different head repo, and posting there would be posting on the
|
|
15
|
+
* contributor's copy. */
|
|
16
|
+
number: number;
|
|
17
|
+
baseRef: string;
|
|
18
|
+
headSha: string;
|
|
19
|
+
/** True when the head branch lives in a fork: the PR's code is not ours. */
|
|
20
|
+
fromFork: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A delivery to act on, or null with the reason it was skipped.
|
|
24
|
+
*
|
|
25
|
+
* Draft pull requests are skipped deliberately: a draft is a work in progress and
|
|
26
|
+
* a bot commenting on every push to one is the fastest way to be uninstalled.
|
|
27
|
+
* `ready_for_review` is in the accepted set so the comment appears the moment the
|
|
28
|
+
* author asks for eyes.
|
|
29
|
+
*/
|
|
30
|
+
export declare function reviewJobFor(event: string, payload: unknown): {
|
|
31
|
+
job: ReviewJob;
|
|
32
|
+
} | {
|
|
33
|
+
skip: string;
|
|
34
|
+
};
|
|
35
|
+
/** Collapse queued work per pull request: only the newest push is worth reviewing. */
|
|
36
|
+
export declare const jobKey: (j: ReviewJob) => string;
|
|
37
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/app/events.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,8DAA8D;AAC9D,MAAM,WAAW,SAAS;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb;;6BAEyB;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAmBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG;IAAE,GAAG,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAqCnG;AAED,sFAAsF;AACtF,eAAO,MAAM,MAAM,GAAI,GAAG,SAAS,KAAG,MAA4C,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What arrived, and whether it is worth work.
|
|
3
|
+
*
|
|
4
|
+
* A busy installation delivers a lot of noise — labels, assignments, reviews —
|
|
5
|
+
* and every accepted delivery costs a clone and a parse. Narrowing happens here,
|
|
6
|
+
* once, so the queue only ever holds jobs that will produce a comment.
|
|
7
|
+
*/
|
|
8
|
+
/** Actions that change the diff. `edited` (a retitled PR) and `labeled` do not. */
|
|
9
|
+
const ACTIONS = new Set(["opened", "synchronize", "reopened", "ready_for_review"]);
|
|
10
|
+
/**
|
|
11
|
+
* A delivery to act on, or null with the reason it was skipped.
|
|
12
|
+
*
|
|
13
|
+
* Draft pull requests are skipped deliberately: a draft is a work in progress and
|
|
14
|
+
* a bot commenting on every push to one is the fastest way to be uninstalled.
|
|
15
|
+
* `ready_for_review` is in the accepted set so the comment appears the moment the
|
|
16
|
+
* author asks for eyes.
|
|
17
|
+
*/
|
|
18
|
+
export function reviewJobFor(event, payload) {
|
|
19
|
+
if (event === "ping")
|
|
20
|
+
return { skip: "ping" };
|
|
21
|
+
if (event !== "pull_request")
|
|
22
|
+
return { skip: `event ${event}` };
|
|
23
|
+
const p = payload;
|
|
24
|
+
const action = p.action ?? "";
|
|
25
|
+
if (!ACTIONS.has(action))
|
|
26
|
+
return { skip: `action ${action}` };
|
|
27
|
+
const pr = p.pull_request;
|
|
28
|
+
const installationId = p.installation?.id;
|
|
29
|
+
const owner = p.repository?.owner?.login;
|
|
30
|
+
const repo = p.repository?.name;
|
|
31
|
+
const number = pr?.number ?? p.number;
|
|
32
|
+
const baseRef = pr?.base?.ref;
|
|
33
|
+
const headSha = pr?.head?.sha;
|
|
34
|
+
if (!installationId || !owner || !repo || !number || !baseRef || !headSha) {
|
|
35
|
+
return { skip: "payload missing installation, repository or pull request fields" };
|
|
36
|
+
}
|
|
37
|
+
if (pr?.draft === true && action !== "ready_for_review")
|
|
38
|
+
return { skip: "draft" };
|
|
39
|
+
if (pr?.state === "closed")
|
|
40
|
+
return { skip: "closed" };
|
|
41
|
+
const base = pr?.base?.repo?.full_name;
|
|
42
|
+
const head = pr?.head?.repo?.full_name;
|
|
43
|
+
return {
|
|
44
|
+
job: {
|
|
45
|
+
installationId,
|
|
46
|
+
owner,
|
|
47
|
+
repo,
|
|
48
|
+
number,
|
|
49
|
+
baseRef,
|
|
50
|
+
headSha,
|
|
51
|
+
// A missing head repo means a deleted fork — treat it as one, since it is
|
|
52
|
+
// certainly not ours.
|
|
53
|
+
fromFork: head !== base,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Collapse queued work per pull request: only the newest push is worth reviewing. */
|
|
58
|
+
export const jobKey = (j) => `${j.owner}/${j.repo}#${j.number}`;
|
|
59
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/app/events.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA+BH,mFAAmF;AACnF,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,aAAa,EAAE,UAAU,EAAE,kBAAkB,CAAC,CAAC,CAAC;AAEnF;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa,EAAE,OAAgB;IAC1D,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC9C,IAAI,KAAK,KAAK,cAAc;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,KAAK,EAAE,EAAE,CAAC;IAEhE,MAAM,CAAC,GAAG,OAAyB,CAAC;IACpC,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC;IAC9B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,MAAM,EAAE,EAAE,CAAC;IAE9D,MAAM,EAAE,GAAG,CAAC,CAAC,YAAY,CAAC;IAC1B,MAAM,cAAc,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC;IAC1C,MAAM,KAAK,GAAG,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC;IACzC,MAAM,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,IAAI,CAAC;IAChC,MAAM,MAAM,GAAG,EAAE,EAAE,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC;IACtC,MAAM,OAAO,GAAG,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC;IAC9B,MAAM,OAAO,GAAG,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC;IAE9B,IAAI,CAAC,cAAc,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;QAC1E,OAAO,EAAE,IAAI,EAAE,iEAAiE,EAAE,CAAC;IACrF,CAAC;IACD,IAAI,EAAE,EAAE,KAAK,KAAK,IAAI,IAAI,MAAM,KAAK,kBAAkB;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAClF,IAAI,EAAE,EAAE,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAEtD,MAAM,IAAI,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC;IACvC,MAAM,IAAI,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC;IACvC,OAAO;QACL,GAAG,EAAE;YACH,cAAc;YACd,KAAK;YACL,IAAI;YACJ,MAAM;YACN,OAAO;YACP,OAAO;YACP,0EAA0E;YAC1E,sBAAsB;YACtB,QAAQ,EAAE,IAAI,KAAK,IAAI;SACxB;KACF,CAAC;AACJ,CAAC;AAED,sFAAsF;AACtF,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,CAAY,EAAU,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface AppCredentials {
|
|
2
|
+
appId: string;
|
|
3
|
+
/** PEM, as GitHub hands it over. */
|
|
4
|
+
privateKey: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A signed App JWT.
|
|
8
|
+
*
|
|
9
|
+
* `iat` is backdated a minute: GitHub compares against ITS clock, and a server
|
|
10
|
+
* running even slightly fast has its tokens rejected as "issued in the future" —
|
|
11
|
+
* a failure that looks like a bad key and is not.
|
|
12
|
+
*/
|
|
13
|
+
export declare function appJwt(creds: AppCredentials, nowMs: number): string;
|
|
14
|
+
export interface TokenResponse {
|
|
15
|
+
token: string;
|
|
16
|
+
expires_at: string;
|
|
17
|
+
}
|
|
18
|
+
/** Minimal shape of `fetch`, so tests need no network and no mocking library. */
|
|
19
|
+
export type Fetch = (url: string, init?: {
|
|
20
|
+
method?: string;
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
body?: string;
|
|
23
|
+
}) => Promise<{
|
|
24
|
+
ok: boolean;
|
|
25
|
+
status: number;
|
|
26
|
+
text: () => Promise<string>;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Installation tokens, fetched once and reused until they are nearly expired.
|
|
30
|
+
*
|
|
31
|
+
* Tokens last an hour and a busy repository can fire a dozen webhooks a minute;
|
|
32
|
+
* without the cache every one of them spends a round trip and a signature.
|
|
33
|
+
*/
|
|
34
|
+
export declare class InstallationTokens {
|
|
35
|
+
private readonly creds;
|
|
36
|
+
private readonly fetchImpl;
|
|
37
|
+
private readonly now;
|
|
38
|
+
private readonly api;
|
|
39
|
+
private readonly cache;
|
|
40
|
+
constructor(creds: AppCredentials, fetchImpl: Fetch, now?: () => number, api?: string);
|
|
41
|
+
get(installationId: number): Promise<string>;
|
|
42
|
+
/** Drop a token GitHub has rejected, so the next call fetches a fresh one. */
|
|
43
|
+
invalidate(installationId: number): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Is this delivery really from GitHub?
|
|
47
|
+
*
|
|
48
|
+
* The webhook endpoint is public, and everything downstream — cloning a repo,
|
|
49
|
+
* posting as the App — happens on its say-so. Compared in constant time because
|
|
50
|
+
* a byte-at-a-time comparison leaks the expected digest to anyone willing to
|
|
51
|
+
* measure, and forging a signature is a total compromise of the endpoint.
|
|
52
|
+
*/
|
|
53
|
+
export declare function verifySignature(secret: string, body: string, header: string | undefined): boolean;
|
|
54
|
+
//# sourceMappingURL=identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/app/identity.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAOnE;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,iFAAiF;AACjF,MAAM,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,KAAK,OAAO,CAAC;IACxH,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7B,CAAC,CAAC;AAOH;;;;;GAKG;AACH,qBAAa,kBAAkB;IAI3B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,GAAG;IANtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;gBAGpC,KAAK,EAAE,cAAc,EACrB,SAAS,EAAE,KAAK,EAChB,GAAG,GAAE,MAAM,MAAiB,EAC5B,GAAG,SAA2B;IAG3C,GAAG,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAuBlD,8EAA8E;IAC9E,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;CAGzC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAMjG"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who the App is, to GitHub.
|
|
3
|
+
*
|
|
4
|
+
* A GitHub App authenticates twice over: it signs a short JWT with its private
|
|
5
|
+
* key to prove it is the App, then trades that for an *installation* token scoped
|
|
6
|
+
* to one repository owner. The installation token is what matters here — it is
|
|
7
|
+
* the reason this exists at all. A workflow on a fork's pull request gets a
|
|
8
|
+
* read-only token and cannot comment; an App's installation token belongs to the
|
|
9
|
+
* base repository, so a fork PR is no different from any other.
|
|
10
|
+
*
|
|
11
|
+
* `node:crypto` signs the JWT, so there is no dependency to audit for something
|
|
12
|
+
* that handles a private key.
|
|
13
|
+
*/
|
|
14
|
+
import { createHmac, createSign, timingSafeEqual } from "node:crypto";
|
|
15
|
+
/** GitHub rejects a JWT older than 60s or more than 10 minutes in the future. */
|
|
16
|
+
const JWT_TTL_S = 540; // 9 minutes, inside the limit with room for clock skew
|
|
17
|
+
/** Renew this long before expiry rather than racing it. */
|
|
18
|
+
const RENEW_MARGIN_MS = 60_000;
|
|
19
|
+
const b64url = (b) => Buffer.from(b).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
20
|
+
/**
|
|
21
|
+
* A signed App JWT.
|
|
22
|
+
*
|
|
23
|
+
* `iat` is backdated a minute: GitHub compares against ITS clock, and a server
|
|
24
|
+
* running even slightly fast has its tokens rejected as "issued in the future" —
|
|
25
|
+
* a failure that looks like a bad key and is not.
|
|
26
|
+
*/
|
|
27
|
+
export function appJwt(creds, nowMs) {
|
|
28
|
+
const iat = Math.floor(nowMs / 1000) - 60;
|
|
29
|
+
const header = b64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
30
|
+
const payload = b64url(JSON.stringify({ iat, exp: iat + JWT_TTL_S, iss: creds.appId }));
|
|
31
|
+
const signer = createSign("RSA-SHA256");
|
|
32
|
+
signer.update(`${header}.${payload}`);
|
|
33
|
+
return `${header}.${payload}.${b64url(signer.sign(creds.privateKey))}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Installation tokens, fetched once and reused until they are nearly expired.
|
|
37
|
+
*
|
|
38
|
+
* Tokens last an hour and a busy repository can fire a dozen webhooks a minute;
|
|
39
|
+
* without the cache every one of them spends a round trip and a signature.
|
|
40
|
+
*/
|
|
41
|
+
export class InstallationTokens {
|
|
42
|
+
creds;
|
|
43
|
+
fetchImpl;
|
|
44
|
+
now;
|
|
45
|
+
api;
|
|
46
|
+
cache = new Map();
|
|
47
|
+
constructor(creds, fetchImpl, now = Date.now, api = "https://api.github.com") {
|
|
48
|
+
this.creds = creds;
|
|
49
|
+
this.fetchImpl = fetchImpl;
|
|
50
|
+
this.now = now;
|
|
51
|
+
this.api = api;
|
|
52
|
+
}
|
|
53
|
+
async get(installationId) {
|
|
54
|
+
const hit = this.cache.get(installationId);
|
|
55
|
+
if (hit && hit.expiresMs - RENEW_MARGIN_MS > this.now())
|
|
56
|
+
return hit.token;
|
|
57
|
+
const res = await this.fetchImpl(`${this.api}/app/installations/${installationId}/access_tokens`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
authorization: `Bearer ${appJwt(this.creds, this.now())}`,
|
|
61
|
+
accept: "application/vnd.github+json",
|
|
62
|
+
"user-agent": "graft-app",
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const body = await res.text();
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
// The body carries GitHub's reason ("integration not found", a key that does
|
|
68
|
+
// not match the app id); losing it turns every setup mistake into "401".
|
|
69
|
+
throw new Error(`installation token for ${installationId} failed: ${res.status} ${body.slice(0, 200)}`);
|
|
70
|
+
}
|
|
71
|
+
const parsed = JSON.parse(body);
|
|
72
|
+
this.cache.set(installationId, { token: parsed.token, expiresMs: Date.parse(parsed.expires_at) });
|
|
73
|
+
return parsed.token;
|
|
74
|
+
}
|
|
75
|
+
/** Drop a token GitHub has rejected, so the next call fetches a fresh one. */
|
|
76
|
+
invalidate(installationId) {
|
|
77
|
+
this.cache.delete(installationId);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Is this delivery really from GitHub?
|
|
82
|
+
*
|
|
83
|
+
* The webhook endpoint is public, and everything downstream — cloning a repo,
|
|
84
|
+
* posting as the App — happens on its say-so. Compared in constant time because
|
|
85
|
+
* a byte-at-a-time comparison leaks the expected digest to anyone willing to
|
|
86
|
+
* measure, and forging a signature is a total compromise of the endpoint.
|
|
87
|
+
*/
|
|
88
|
+
export function verifySignature(secret, body, header) {
|
|
89
|
+
if (!header)
|
|
90
|
+
return false;
|
|
91
|
+
const expected = `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;
|
|
92
|
+
const a = Buffer.from(expected);
|
|
93
|
+
const b = Buffer.from(header);
|
|
94
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/app/identity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/app/main.ts"],"names":[],"mappings":""}
|
package/dist/app/main.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry point: read the environment, refuse to start half-configured, listen.
|
|
3
|
+
*
|
|
4
|
+
* Every misconfiguration is fatal at boot rather than at the first webhook. A
|
|
5
|
+
* server that starts without a webhook secret looks healthy and silently rejects
|
|
6
|
+
* every delivery; one that starts without a private key looks healthy and fails
|
|
7
|
+
* every review. Both are worth a loud death on line one.
|
|
8
|
+
*/
|
|
9
|
+
import { createApp } from "./server.js";
|
|
10
|
+
function required(name) {
|
|
11
|
+
const value = process.env[name];
|
|
12
|
+
if (!value) {
|
|
13
|
+
console.error(`✗ ${name} is required`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* GitHub hands the private key over as a PEM file. Env vars cannot hold newlines
|
|
20
|
+
* comfortably, so both spellings are accepted: the raw PEM, or one with `\n`
|
|
21
|
+
* escaped — the shape you get from pasting a key into a hosting provider's UI.
|
|
22
|
+
*/
|
|
23
|
+
function privateKey() {
|
|
24
|
+
const raw = required("GRAFT_APP_PRIVATE_KEY");
|
|
25
|
+
const pem = raw.includes("\\n") ? raw.replace(/\\n/g, "\n") : raw;
|
|
26
|
+
if (!pem.includes("BEGIN") || !pem.includes("PRIVATE KEY")) {
|
|
27
|
+
console.error("✗ GRAFT_APP_PRIVATE_KEY does not look like a PEM private key");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
return pem;
|
|
31
|
+
}
|
|
32
|
+
const port = Number(process.env.PORT ?? 3000);
|
|
33
|
+
const { server, queue } = createApp({
|
|
34
|
+
appId: required("GRAFT_APP_ID"),
|
|
35
|
+
privateKey: privateKey(),
|
|
36
|
+
webhookSecret: required("GRAFT_WEBHOOK_SECRET"),
|
|
37
|
+
publicUrl: required("GRAFT_PUBLIC_URL"),
|
|
38
|
+
port,
|
|
39
|
+
concurrency: Number(process.env.GRAFT_CONCURRENCY ?? 2),
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* Finish what is in flight before dying.
|
|
43
|
+
*
|
|
44
|
+
* A container gets a TERM and a grace period on every deploy. Dropping a review
|
|
45
|
+
* mid-clone leaves a stale comment on a pull request and no retry, because the
|
|
46
|
+
* delivery was acknowledged long ago.
|
|
47
|
+
*/
|
|
48
|
+
for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
49
|
+
process.on(signal, () => {
|
|
50
|
+
console.log(`${signal}: draining ${queue.size} job(s)`);
|
|
51
|
+
server.close();
|
|
52
|
+
void queue.drain().then(() => process.exit(0));
|
|
53
|
+
setTimeout(() => process.exit(1), 25_000).unref();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/app/main.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,SAAS,QAAQ,CAAC,IAAY;IAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,cAAc,CAAC,CAAC;QACvC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,SAAS,UAAU;IACjB,MAAM,GAAG,GAAG,QAAQ,CAAC,uBAAuB,CAAC,CAAC;IAC9C,MAAM,GAAG,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAClE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;QAC3D,OAAO,CAAC,KAAK,CAAC,8DAA8D,CAAC,CAAC;QAC9E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;AAC9C,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC;IAClC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC;IAC/B,UAAU,EAAE,UAAU,EAAE;IACxB,aAAa,EAAE,QAAQ,CAAC,sBAAsB,CAAC;IAC/C,SAAS,EAAE,QAAQ,CAAC,kBAAkB,CAAC;IACvC,IAAI;IACJ,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,CAAC,CAAC;CACxD,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,KAAK,MAAM,MAAM,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAU,EAAE,CAAC;IACpD,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;QACtB,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,cAAc,KAAK,CAAC,IAAI,SAAS,CAAC,CAAC;QACxD,MAAM,CAAC,KAAK,EAAE,CAAC;QACf,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/C,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC;IACpD,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface StoredPage {
|
|
2
|
+
html: string;
|
|
3
|
+
storedMs: number;
|
|
4
|
+
}
|
|
5
|
+
export interface PageStoreOptions {
|
|
6
|
+
secret: string;
|
|
7
|
+
ttlMs?: number;
|
|
8
|
+
maxPages?: number;
|
|
9
|
+
now?: () => number;
|
|
10
|
+
}
|
|
11
|
+
export declare class PageStore {
|
|
12
|
+
private readonly pages;
|
|
13
|
+
private readonly secret;
|
|
14
|
+
private readonly ttlMs;
|
|
15
|
+
private readonly maxPages;
|
|
16
|
+
private readonly now;
|
|
17
|
+
constructor(opts: PageStoreOptions);
|
|
18
|
+
/** `owner/repo#number` → a path segment safe to put in a URL. */
|
|
19
|
+
static idFor(owner: string, repo: string, number: number): string;
|
|
20
|
+
/** Store (replacing any previous page for the same PR) and return `id` + `token`. */
|
|
21
|
+
put(owner: string, repo: string, number: number, html: string): {
|
|
22
|
+
id: string;
|
|
23
|
+
token: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The page, if the token is valid and it has not expired.
|
|
27
|
+
*
|
|
28
|
+
* Expiry is checked against the STORED time rather than encoded in the token,
|
|
29
|
+
* so a link cannot outlive the data it points at — and re-reviewing a PR
|
|
30
|
+
* refreshes both together.
|
|
31
|
+
*/
|
|
32
|
+
get(id: string, token: string | undefined): string | null;
|
|
33
|
+
get size(): number;
|
|
34
|
+
private sign;
|
|
35
|
+
private valid;
|
|
36
|
+
/** Oldest out first, plus anything past its TTL. */
|
|
37
|
+
private evict;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=pages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/app/pages.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IACvD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,IAAI,EAAE,gBAAgB;IAOlC,iEAAiE;IACjE,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAIjE,qFAAqF;IACrF,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;IAO7F;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI;IAUzD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,KAAK;IAQb,oDAAoD;IACpD,OAAO,CAAC,KAAK;CASd"}
|