@cursor/july 0.1.74 → 0.1.76
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/dist/channels/checks.d.ts +111 -0
- package/dist/channels/checks.d.ts.map +1 -0
- package/dist/channels/checks.js +86 -0
- package/dist/channels/deployments/deployments-channel.d.ts +44 -0
- package/dist/channels/deployments/deployments-channel.d.ts.map +1 -0
- package/dist/channels/deployments/deployments-channel.js +92 -0
- package/dist/channels/deployments/index.d.ts +26 -0
- package/dist/channels/deployments/index.d.ts.map +1 -0
- package/dist/channels/deployments/index.js +25 -0
- package/dist/channels/deployments/types.d.ts +69 -0
- package/dist/channels/deployments/types.d.ts.map +1 -0
- package/dist/channels/deployments/types.js +6 -0
- package/dist/channels/github/checks.d.ts +13 -48
- package/dist/channels/github/checks.d.ts.map +1 -1
- package/dist/channels/github/checks.js +56 -64
- package/dist/channels/origin/api.d.ts +100 -0
- package/dist/channels/origin/api.d.ts.map +1 -0
- package/dist/channels/origin/api.js +225 -0
- package/dist/channels/origin/binding.d.ts +44 -0
- package/dist/channels/origin/binding.d.ts.map +1 -0
- package/dist/channels/origin/binding.js +45 -0
- package/dist/channels/origin/checks.d.ts +107 -0
- package/dist/channels/origin/checks.d.ts.map +1 -0
- package/dist/channels/origin/checks.js +145 -0
- package/dist/channels/origin/index.d.ts +3 -0
- package/dist/channels/origin/index.d.ts.map +1 -1
- package/dist/channels/origin/index.js +3 -0
- package/dist/channels/origin/origin-channel.d.ts +4 -4
- package/dist/channels/origin/origin-channel.d.ts.map +1 -1
- package/dist/channels/origin/origin-channel.js +12 -2
- package/dist/channels/origin/types.d.ts +36 -0
- package/dist/channels/origin/types.d.ts.map +1 -1
- package/dist/channels.d.ts +1 -0
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +1 -0
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.BETd-WqU.js → app.b8bhB2SV.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DGZeXVqn.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.wEaGEmV9.js → VPLocalSearchBox.lLSeIyWw.js} +1 -1
- package/dist/docs/assets/chunks/{theme.DNDicFOP.js → theme.C_owesa-.js} +2 -2
- package/dist/docs/assets/{reference_channels.md.MN8iDy8V.js → reference_channels.md.G5GysZ7e.js} +2 -2
- package/dist/docs/assets/{reference_channels.md.MN8iDy8V.lean.js → reference_channels.md.G5GysZ7e.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/convert-automation.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/opentelemetry.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/artifacts.html +3 -3
- package/dist/docs/reference/channels.html +5 -5
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/templates/agentic-owners.html +3 -3
- package/dist/docs/templates/demo.html +3 -3
- package/dist/docs/templates/pr-autofixer.html +3 -3
- package/dist/docs/templates/security-reviewer.html +3 -3
- package/dist/docs/templates/triage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/internal/authored-alias-hooks.d.ts +12 -0
- package/dist/internal/authored-alias-hooks.d.ts.map +1 -1
- package/dist/internal/authored-alias-hooks.js +60 -8
- package/dist/internal/authored-loaders.d.ts +5 -3
- package/dist/internal/authored-loaders.d.ts.map +1 -1
- package/dist/internal/authored-loaders.js +13 -4
- package/dist/internal/cursor-deployment-relay.d.ts +77 -0
- package/dist/internal/cursor-deployment-relay.d.ts.map +1 -0
- package/dist/internal/cursor-deployment-relay.js +201 -0
- package/dist/internal/deployment-scm-event-stream.js +4 -4
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +1 -0
- package/dist/internal/review-comments.d.ts +62 -0
- package/dist/internal/review-comments.d.ts.map +1 -1
- package/dist/internal/review-comments.js +199 -0
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +114 -5
- package/dist/playground/assets/{index-D_8I9TM0.js → index-BWqjJcRo.js} +1 -1
- package/dist/playground/assets/index-Bfwz2yLN.css +1 -0
- package/dist/playground/index.html +2 -2
- package/docs/reference/channels.md +15 -0
- package/package.json +7 -1
- package/src/channels/checks.ts +185 -0
- package/src/channels/deployments/deployments-channel.ts +109 -0
- package/src/channels/deployments/index.ts +26 -0
- package/src/channels/deployments/types.ts +80 -0
- package/src/channels/github/checks.ts +90 -115
- package/src/channels/origin/api.ts +318 -0
- package/src/channels/origin/binding.ts +96 -0
- package/src/channels/origin/checks.ts +250 -0
- package/src/channels/origin/index.ts +3 -0
- package/src/channels/origin/origin-channel.ts +18 -6
- package/src/channels/origin/types.ts +37 -0
- package/src/channels.ts +16 -0
- package/src/internal/authored-alias-hooks.ts +75 -7
- package/src/internal/authored-loaders.ts +15 -4
- package/src/internal/cursor-deployment-relay.ts +274 -0
- package/src/internal/deployment-scm-event-stream.ts +4 -4
- package/src/internal/distribution.ts +1 -0
- package/src/internal/review-comments.ts +286 -0
- package/src/internal/server.ts +149 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.DM5INS71.js +0 -1
- package/dist/playground/assets/index-M3nSMc6_.css +0 -1
|
@@ -1,81 +1,115 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* GitHub CI checks (Checks API) — the GitHub {@link CiCheckWriter} behind the
|
|
3
|
+
* shared check lifecycle in `../checks.js`.
|
|
3
4
|
*
|
|
4
|
-
* `createGitHubCheck` opens a merge-box check run on a commit and returns
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Works with any client exposing `getOctokit()` (`ctx.github`,
|
|
5
|
+
* `createGitHubCheck` opens a merge-box check run on a commit and returns the
|
|
6
|
+
* shared `CiCheck` handle; `resumeGitHubCheck` rebuilds one from a stored
|
|
7
|
+
* check-run id. Works with any client exposing `getOctokit()` (`ctx.github`,
|
|
8
8
|
* `ctx.host.github`), and is also reachable as `ctx.github.createCheck(...)`.
|
|
9
9
|
*
|
|
10
10
|
* Posting checks needs `checks:write`. With `cursorAccount`, declare
|
|
11
11
|
* `githubChannel({ checks: true })` so the hosted lease is minted on the
|
|
12
12
|
* `contents-write` tier; GitHub App / PAT credentials need the equivalent
|
|
13
|
-
* permission.
|
|
13
|
+
* permission. API failures are normalized into `GitHubApiError`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { Octokit } from "@octokit/rest";
|
|
17
17
|
import {
|
|
18
|
-
type
|
|
19
|
-
type
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
type CiCheck,
|
|
19
|
+
type CiCheckConcludeInput,
|
|
20
|
+
type CiCheckCreateInput,
|
|
21
|
+
type CiCheckOutput,
|
|
22
|
+
type CiCheckRef,
|
|
23
|
+
type CiCheckUpdateInput,
|
|
24
|
+
type CiCheckWriter,
|
|
25
|
+
openCiCheck,
|
|
26
|
+
resumeCiCheck,
|
|
27
|
+
} from "../checks.js";
|
|
28
|
+
import { wrapOctokitError } from "./api.js";
|
|
22
29
|
|
|
23
30
|
/** Any client that can hand out an authenticated Octokit. */
|
|
24
31
|
export interface GitHubCheckClient {
|
|
25
32
|
getOctokit(): Promise<Octokit>;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
+
export type GitHubCheckOutput = CiCheckOutput;
|
|
36
|
+
export type GitHubCheckUpdateInput = CiCheckUpdateInput;
|
|
37
|
+
export type GitHubCheckConcludeInput = CiCheckConcludeInput;
|
|
38
|
+
|
|
39
|
+
/** A created check run. All methods throw `GitHubApiError` on API failure. */
|
|
40
|
+
export type GitHubCheck = CiCheck & CiCheckRef;
|
|
35
41
|
|
|
36
|
-
export interface GitHubCheckCreateInput {
|
|
42
|
+
export interface GitHubCheckCreateInput extends CiCheckCreateInput {
|
|
37
43
|
/** Client used for every API call on the returned handle. */
|
|
38
44
|
github: GitHubCheckClient;
|
|
39
45
|
/** Repository owner login, e.g. `"acme"`. */
|
|
40
46
|
owner: string;
|
|
41
47
|
/** Repository name, e.g. `"widgets"`. */
|
|
42
48
|
repo: string;
|
|
43
|
-
/** Check name shown in the PR merge box, e.g. `"Cursor Security Review"`. */
|
|
44
|
-
name: string;
|
|
45
|
-
/** Commit the check attaches to (usually the PR head SHA). */
|
|
46
|
-
sha: string;
|
|
47
|
-
/** Initial status. Defaults to `"in_progress"`. */
|
|
48
|
-
status?: "queued" | "in_progress";
|
|
49
|
-
/** "Details" link on the check, e.g. the agent session run URL. */
|
|
50
|
-
detailsUrl?: string;
|
|
51
|
-
/** Title + markdown summary shown on the check page. */
|
|
52
|
-
output?: GitHubCheckOutput;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface GitHubCheckUpdateInput {
|
|
56
|
-
status?: GitHubCheckRunStatus;
|
|
57
|
-
conclusion?: GitHubCheckRunConclusion;
|
|
58
|
-
detailsUrl?: string;
|
|
59
|
-
output?: GitHubCheckOutput;
|
|
60
49
|
}
|
|
61
50
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
51
|
+
/**
|
|
52
|
+
* GitHub writer: first write creates the check run (capturing its id), later
|
|
53
|
+
* writes patch it.
|
|
54
|
+
*/
|
|
55
|
+
function githubCheckWriter(input: {
|
|
56
|
+
github: GitHubCheckClient;
|
|
57
|
+
owner: string;
|
|
58
|
+
repo: string;
|
|
59
|
+
name: string;
|
|
60
|
+
sha?: string;
|
|
61
|
+
checkRunId?: number;
|
|
62
|
+
}): CiCheckWriter {
|
|
63
|
+
let checkRunId = input.checkRunId;
|
|
64
|
+
return async (write) => {
|
|
65
|
+
const octokit = await input.github.getOctokit();
|
|
66
|
+
const base = {
|
|
67
|
+
owner: input.owner,
|
|
68
|
+
repo: input.repo,
|
|
69
|
+
name: input.name,
|
|
70
|
+
status: write.status,
|
|
71
|
+
conclusion: write.conclusion,
|
|
72
|
+
completed_at: write.completedAt,
|
|
73
|
+
details_url: write.detailsUrl,
|
|
74
|
+
output: write.output,
|
|
75
|
+
};
|
|
76
|
+
if (checkRunId === undefined) {
|
|
77
|
+
if (input.sha === undefined) {
|
|
78
|
+
throw new Error("githubChannel: cannot create a check without a sha.");
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const response = await octokit.rest.checks.create({
|
|
82
|
+
...base,
|
|
83
|
+
head_sha: input.sha,
|
|
84
|
+
started_at: write.startedAt,
|
|
85
|
+
});
|
|
86
|
+
checkRunId = response.data.id;
|
|
87
|
+
return {
|
|
88
|
+
id: response.data.id,
|
|
89
|
+
url:
|
|
90
|
+
typeof response.data.html_url === "string"
|
|
91
|
+
? response.data.html_url
|
|
92
|
+
: undefined,
|
|
93
|
+
};
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw wrapOctokitError(
|
|
96
|
+
error,
|
|
97
|
+
"POST",
|
|
98
|
+
`/repos/${input.owner}/${input.repo}/check-runs`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
await octokit.rest.checks.update({ ...base, check_run_id: checkRunId });
|
|
104
|
+
return undefined;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw wrapOctokitError(
|
|
107
|
+
error,
|
|
108
|
+
"PATCH",
|
|
109
|
+
`/repos/${input.owner}/${input.repo}/check-runs/${checkRunId}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
79
113
|
}
|
|
80
114
|
|
|
81
115
|
/**
|
|
@@ -95,38 +129,10 @@ export interface GitHubCheck {
|
|
|
95
129
|
* await check.conclude({ conclusion: "success", output: { … } });
|
|
96
130
|
* ```
|
|
97
131
|
*/
|
|
98
|
-
export
|
|
132
|
+
export function createGitHubCheck(
|
|
99
133
|
input: GitHubCheckCreateInput
|
|
100
134
|
): Promise<GitHubCheck> {
|
|
101
|
-
|
|
102
|
-
let created: { id: number; html_url?: string | null };
|
|
103
|
-
try {
|
|
104
|
-
const response = await octokit.rest.checks.create({
|
|
105
|
-
owner: input.owner,
|
|
106
|
-
repo: input.repo,
|
|
107
|
-
name: input.name,
|
|
108
|
-
head_sha: input.sha,
|
|
109
|
-
status: input.status ?? "in_progress",
|
|
110
|
-
started_at: new Date().toISOString(),
|
|
111
|
-
details_url: input.detailsUrl,
|
|
112
|
-
output: input.output,
|
|
113
|
-
});
|
|
114
|
-
created = response.data;
|
|
115
|
-
} catch (error) {
|
|
116
|
-
throw wrapOctokitError(
|
|
117
|
-
error,
|
|
118
|
-
"POST",
|
|
119
|
-
`/repos/${input.owner}/${input.repo}/check-runs`
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
return resumeGitHubCheck({
|
|
123
|
-
github: input.github,
|
|
124
|
-
owner: input.owner,
|
|
125
|
-
repo: input.repo,
|
|
126
|
-
name: input.name,
|
|
127
|
-
checkRunId: created.id,
|
|
128
|
-
url: typeof created.html_url === "string" ? created.html_url : undefined,
|
|
129
|
-
});
|
|
135
|
+
return openCiCheck(githubCheckWriter(input), input);
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
/**
|
|
@@ -141,39 +147,8 @@ export function resumeGitHubCheck(input: {
|
|
|
141
147
|
checkRunId: number;
|
|
142
148
|
url?: string;
|
|
143
149
|
}): GitHubCheck {
|
|
144
|
-
|
|
145
|
-
const octokit = await input.github.getOctokit();
|
|
146
|
-
try {
|
|
147
|
-
await octokit.rest.checks.update({
|
|
148
|
-
owner: input.owner,
|
|
149
|
-
repo: input.repo,
|
|
150
|
-
check_run_id: input.checkRunId,
|
|
151
|
-
name: input.name,
|
|
152
|
-
status: update.status,
|
|
153
|
-
conclusion: update.conclusion,
|
|
154
|
-
completed_at:
|
|
155
|
-
update.status === "completed" ? new Date().toISOString() : undefined,
|
|
156
|
-
details_url: update.detailsUrl,
|
|
157
|
-
output: update.output,
|
|
158
|
-
});
|
|
159
|
-
} catch (error) {
|
|
160
|
-
throw wrapOctokitError(
|
|
161
|
-
error,
|
|
162
|
-
"PATCH",
|
|
163
|
-
`/repos/${input.owner}/${input.repo}/check-runs/${input.checkRunId}`
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
return {
|
|
150
|
+
return resumeCiCheck(githubCheckWriter(input), {
|
|
168
151
|
id: input.checkRunId,
|
|
169
152
|
url: input.url,
|
|
170
|
-
|
|
171
|
-
conclude: (conclude) =>
|
|
172
|
-
patch({
|
|
173
|
-
status: "completed",
|
|
174
|
-
conclusion: conclude.conclusion,
|
|
175
|
-
detailsUrl: conclude.detailsUrl,
|
|
176
|
-
output: conclude.output,
|
|
177
|
-
}),
|
|
178
|
-
};
|
|
153
|
+
});
|
|
179
154
|
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Origin public REST helpers (`https://api.cursor.com/v1/origin/...`).
|
|
3
|
+
*
|
|
4
|
+
* Origin writes such as CI check runs are installation-only: they need an
|
|
5
|
+
* `oit_…` installation token minted from Origin app credentials (EdDSA app
|
|
6
|
+
* JWT → `/v1/origin/app/installations/{id}/access_tokens`). This module
|
|
7
|
+
* consolidates that flow so agents configure credentials once and call
|
|
8
|
+
* {@link createOriginClient} — the GitHub-channel analogue of
|
|
9
|
+
* `createGitHubOctokit`.
|
|
10
|
+
*
|
|
11
|
+
* Credentials resolve from options first, then the environment:
|
|
12
|
+
* `ORIGIN_APP_ID` (or `AGENT_SERVE_ORIGIN_APP_ID`),
|
|
13
|
+
* `ORIGIN_APP_INSTALLATION_ID`, `ORIGIN_APP_PRIVATE_KEY` (PKCS8 PEM for the
|
|
14
|
+
* app's Ed25519 key; literal `\n` sequences are normalized), and
|
|
15
|
+
* `ORIGIN_API_BASE_URL` for the public REST base.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Buffer } from "node:buffer";
|
|
19
|
+
import { createPrivateKey, sign } from "node:crypto";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Public Origin REST base (check runs, installation tokens). Distinct from
|
|
23
|
+
* the webhook JWKS base (`DEFAULT_ORIGIN_API_BASE_URL` in
|
|
24
|
+
* `origin-webhook.ts`).
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_ORIGIN_PUBLIC_API_BASE_URL = "https://api.cursor.com";
|
|
27
|
+
|
|
28
|
+
/** Fixed audience for Origin app JWTs (`APP_TOKEN_AUDIENCE` backend-side). */
|
|
29
|
+
const ORIGIN_APP_TOKEN_AUDIENCE = "origin-apps";
|
|
30
|
+
const APP_JWT_TTL_SECONDS = 5 * 60;
|
|
31
|
+
/** Re-mint the `oit_` token after this long; tokens outlive it comfortably. */
|
|
32
|
+
const TOKEN_REUSE_MS = 4 * 60 * 1000;
|
|
33
|
+
const TOKEN_EXCHANGE_TIMEOUT_MS = 30_000;
|
|
34
|
+
|
|
35
|
+
export type OriginApiMethod = "DELETE" | "GET" | "PATCH" | "POST" | "PUT";
|
|
36
|
+
|
|
37
|
+
/** Normalized Origin REST failure — the `GitHubApiError` analogue. */
|
|
38
|
+
export class OriginApiError extends Error {
|
|
39
|
+
readonly body: unknown;
|
|
40
|
+
readonly method: string;
|
|
41
|
+
readonly path: string;
|
|
42
|
+
readonly status: number;
|
|
43
|
+
|
|
44
|
+
constructor(input: {
|
|
45
|
+
body: unknown;
|
|
46
|
+
method: string;
|
|
47
|
+
path: string;
|
|
48
|
+
status: number;
|
|
49
|
+
}) {
|
|
50
|
+
super(
|
|
51
|
+
`Origin ${input.method} ${input.path} failed with HTTP ${input.status}.`
|
|
52
|
+
);
|
|
53
|
+
this.name = "OriginApiError";
|
|
54
|
+
this.body = input.body;
|
|
55
|
+
this.method = input.method;
|
|
56
|
+
this.path = input.path;
|
|
57
|
+
this.status = input.status;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Yields a bearer for Origin REST calls (usually a short-lived `oit_…`). */
|
|
62
|
+
export type OriginTokenProvider = () => Promise<string>;
|
|
63
|
+
|
|
64
|
+
export interface OriginAppCredentials {
|
|
65
|
+
/** Origin app id (`app_<typeid>`). */
|
|
66
|
+
appId: string;
|
|
67
|
+
/** Installation the token is minted for. */
|
|
68
|
+
installationId: string;
|
|
69
|
+
/** PKCS8 PEM for the app's EdDSA (Ed25519) private key. */
|
|
70
|
+
privateKey: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Vault/env round-trip: literal `\n` sequences appear in practice. */
|
|
74
|
+
function normalizePrivateKeyPem(raw: string | undefined): string | undefined {
|
|
75
|
+
const pem = raw?.trim().replace(/\\n/g, "\n");
|
|
76
|
+
return pem === undefined || pem === "" ? undefined : pem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function envValue(value: string | undefined): string | undefined {
|
|
80
|
+
const trimmed = value?.trim();
|
|
81
|
+
return trimmed === undefined || trimmed === "" ? undefined : trimmed;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Each Origin app credential field found in the environment, when set. */
|
|
85
|
+
export function resolveOriginAppCredentialParts(
|
|
86
|
+
env: NodeJS.ProcessEnv = process.env
|
|
87
|
+
): Partial<OriginAppCredentials> {
|
|
88
|
+
return {
|
|
89
|
+
appId:
|
|
90
|
+
envValue(env.ORIGIN_APP_ID) ?? envValue(env.AGENT_SERVE_ORIGIN_APP_ID),
|
|
91
|
+
installationId: envValue(env.ORIGIN_APP_INSTALLATION_ID),
|
|
92
|
+
privateKey: normalizePrivateKeyPem(env.ORIGIN_APP_PRIVATE_KEY),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Read Origin app credentials from the environment. Returns `undefined` when
|
|
98
|
+
* incomplete so callers can fail with a clear message at use time instead of
|
|
99
|
+
* at startup.
|
|
100
|
+
*/
|
|
101
|
+
export function resolveOriginAppCredentials(
|
|
102
|
+
env: NodeJS.ProcessEnv = process.env
|
|
103
|
+
): OriginAppCredentials | undefined {
|
|
104
|
+
const { appId, installationId, privateKey } =
|
|
105
|
+
resolveOriginAppCredentialParts(env);
|
|
106
|
+
if (
|
|
107
|
+
appId === undefined ||
|
|
108
|
+
installationId === undefined ||
|
|
109
|
+
privateKey === undefined
|
|
110
|
+
) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
return { appId, installationId, privateKey };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Public Origin REST base. Trailing slashes are stripped for path joins. */
|
|
117
|
+
export function originPublicApiBaseUrl(
|
|
118
|
+
env: NodeJS.ProcessEnv = process.env
|
|
119
|
+
): string {
|
|
120
|
+
const fromEnv = envValue(env.ORIGIN_API_BASE_URL)?.replace(/\/+$/, "");
|
|
121
|
+
return fromEnv ?? DEFAULT_ORIGIN_PUBLIC_API_BASE_URL;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function base64UrlJson(value: unknown): string {
|
|
125
|
+
return Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function mintOriginAppJwt(
|
|
129
|
+
credentials: OriginAppCredentials,
|
|
130
|
+
nowMs: number
|
|
131
|
+
): string {
|
|
132
|
+
const now = Math.floor(nowMs / 1000);
|
|
133
|
+
const header = base64UrlJson({
|
|
134
|
+
alg: "EdDSA",
|
|
135
|
+
kid: credentials.appId,
|
|
136
|
+
typ: "JWT",
|
|
137
|
+
});
|
|
138
|
+
const payload = base64UrlJson({
|
|
139
|
+
iss: credentials.appId,
|
|
140
|
+
aud: ORIGIN_APP_TOKEN_AUDIENCE,
|
|
141
|
+
iat: now,
|
|
142
|
+
exp: now + APP_JWT_TTL_SECONDS,
|
|
143
|
+
});
|
|
144
|
+
const signingInput = `${header}.${payload}`;
|
|
145
|
+
const key = createPrivateKey(credentials.privateKey);
|
|
146
|
+
const signature = sign(null, Buffer.from(signingInput), key).toString(
|
|
147
|
+
"base64url"
|
|
148
|
+
);
|
|
149
|
+
return `${signingInput}.${signature}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function mintOriginInstallationToken(input: {
|
|
153
|
+
credentials: OriginAppCredentials;
|
|
154
|
+
apiBaseUrl: string;
|
|
155
|
+
fetchImpl: typeof fetch;
|
|
156
|
+
nowMs: number;
|
|
157
|
+
}): Promise<string> {
|
|
158
|
+
const jwt = mintOriginAppJwt(input.credentials, input.nowMs);
|
|
159
|
+
const path = `/v1/origin/app/installations/${encodeURIComponent(
|
|
160
|
+
input.credentials.installationId
|
|
161
|
+
)}/access_tokens`;
|
|
162
|
+
const response = await input.fetchImpl(`${input.apiBaseUrl}${path}`, {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: { authorization: `Bearer ${jwt}` },
|
|
165
|
+
signal: AbortSignal.timeout(TOKEN_EXCHANGE_TIMEOUT_MS),
|
|
166
|
+
});
|
|
167
|
+
const body: unknown = await response.json().catch(() => undefined);
|
|
168
|
+
const token =
|
|
169
|
+
typeof body === "object" && body !== null && !Array.isArray(body)
|
|
170
|
+
? (body as Record<string, unknown>).token
|
|
171
|
+
: undefined;
|
|
172
|
+
if (!response.ok || typeof token !== "string" || token === "") {
|
|
173
|
+
// Never echo the body: on success it contains the installation token.
|
|
174
|
+
throw new OriginApiError({
|
|
175
|
+
body: undefined,
|
|
176
|
+
method: "POST",
|
|
177
|
+
path,
|
|
178
|
+
status: response.status,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return token;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* A cached per-use token provider for one set of app credentials. Every call
|
|
186
|
+
* inside the reuse window returns the same `oit_…`; the first call after it
|
|
187
|
+
* re-mints.
|
|
188
|
+
*/
|
|
189
|
+
export function originAppTokenProvider(input: {
|
|
190
|
+
credentials: OriginAppCredentials;
|
|
191
|
+
apiBaseUrl?: string;
|
|
192
|
+
fetch?: typeof fetch;
|
|
193
|
+
now?: () => number;
|
|
194
|
+
}): OriginTokenProvider {
|
|
195
|
+
const apiBaseUrl = input.apiBaseUrl ?? originPublicApiBaseUrl();
|
|
196
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
197
|
+
const now = input.now ?? Date.now;
|
|
198
|
+
let cached: { token: string; mintedAtMs: number } | undefined;
|
|
199
|
+
return async () => {
|
|
200
|
+
const nowMs = now();
|
|
201
|
+
if (cached !== undefined && nowMs - cached.mintedAtMs < TOKEN_REUSE_MS) {
|
|
202
|
+
return cached.token;
|
|
203
|
+
}
|
|
204
|
+
const token = await mintOriginInstallationToken({
|
|
205
|
+
credentials: input.credentials,
|
|
206
|
+
apiBaseUrl,
|
|
207
|
+
fetchImpl,
|
|
208
|
+
nowMs,
|
|
209
|
+
});
|
|
210
|
+
cached = { token, mintedAtMs: nowMs };
|
|
211
|
+
return token;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface OriginClientOptions {
|
|
216
|
+
/** Static bearer (`oit_…`) or refreshing provider used for every call. */
|
|
217
|
+
token?: string | OriginTokenProvider;
|
|
218
|
+
/**
|
|
219
|
+
* App credentials to mint installation tokens from. Defaults to the
|
|
220
|
+
* environment ({@link resolveOriginAppCredentials}).
|
|
221
|
+
*/
|
|
222
|
+
credentials?: OriginAppCredentials;
|
|
223
|
+
/** Public Origin REST base. Defaults to `ORIGIN_API_BASE_URL` / prod. */
|
|
224
|
+
apiBaseUrl?: string;
|
|
225
|
+
/** Injectable fetch for tests. */
|
|
226
|
+
fetch?: typeof fetch;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Minimal authenticated Origin REST client (the `Octokit` analogue). */
|
|
230
|
+
export interface OriginClient {
|
|
231
|
+
readonly apiBaseUrl: string;
|
|
232
|
+
/**
|
|
233
|
+
* Call an Origin public REST path (e.g. `/v1/origin/repos/o/r/check-runs`).
|
|
234
|
+
* Throws {@link OriginApiError} on non-2xx responses.
|
|
235
|
+
*/
|
|
236
|
+
request<T = unknown>(input: {
|
|
237
|
+
method: OriginApiMethod;
|
|
238
|
+
path: string;
|
|
239
|
+
body?: unknown;
|
|
240
|
+
}): Promise<T>;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Build an authenticated Origin REST client. Auth resolution: explicit
|
|
245
|
+
* token/provider, then app credentials (options or environment). Missing
|
|
246
|
+
* auth fails at the first request with a clear message.
|
|
247
|
+
*/
|
|
248
|
+
export function createOriginClient(
|
|
249
|
+
options: OriginClientOptions = {}
|
|
250
|
+
): OriginClient {
|
|
251
|
+
const apiBaseUrl = (options.apiBaseUrl ?? originPublicApiBaseUrl()).replace(
|
|
252
|
+
/\/+$/,
|
|
253
|
+
""
|
|
254
|
+
);
|
|
255
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
256
|
+
|
|
257
|
+
let tokenProvider: OriginTokenProvider | undefined;
|
|
258
|
+
const getToken = async (): Promise<string> => {
|
|
259
|
+
if (typeof options.token === "string") {
|
|
260
|
+
return options.token;
|
|
261
|
+
}
|
|
262
|
+
if (typeof options.token === "function") {
|
|
263
|
+
return await options.token();
|
|
264
|
+
}
|
|
265
|
+
if (tokenProvider === undefined) {
|
|
266
|
+
const credentials = options.credentials ?? resolveOriginAppCredentials();
|
|
267
|
+
if (credentials === undefined) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
"originClient: no Origin credential available (pass token / credentials, or set ORIGIN_APP_ID + ORIGIN_APP_INSTALLATION_ID + ORIGIN_APP_PRIVATE_KEY)."
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
tokenProvider = originAppTokenProvider({
|
|
273
|
+
credentials,
|
|
274
|
+
apiBaseUrl,
|
|
275
|
+
fetch: fetchImpl,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return await tokenProvider();
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
apiBaseUrl,
|
|
283
|
+
async request<T = unknown>(input: {
|
|
284
|
+
method: OriginApiMethod;
|
|
285
|
+
path: string;
|
|
286
|
+
body?: unknown;
|
|
287
|
+
}): Promise<T> {
|
|
288
|
+
const path = input.path.startsWith("/") ? input.path : `/${input.path}`;
|
|
289
|
+
const token = await getToken();
|
|
290
|
+
const response = await fetchImpl(`${apiBaseUrl}${path}`, {
|
|
291
|
+
method: input.method,
|
|
292
|
+
headers: {
|
|
293
|
+
authorization: `Bearer ${token}`,
|
|
294
|
+
...(input.body === undefined
|
|
295
|
+
? {}
|
|
296
|
+
: { "content-type": "application/json" }),
|
|
297
|
+
},
|
|
298
|
+
body: input.body === undefined ? undefined : JSON.stringify(input.body),
|
|
299
|
+
});
|
|
300
|
+
const text = await response.text();
|
|
301
|
+
let parsed: unknown;
|
|
302
|
+
try {
|
|
303
|
+
parsed = text === "" ? undefined : JSON.parse(text);
|
|
304
|
+
} catch {
|
|
305
|
+
parsed = text;
|
|
306
|
+
}
|
|
307
|
+
if (!response.ok) {
|
|
308
|
+
throw new OriginApiError({
|
|
309
|
+
body: parsed,
|
|
310
|
+
method: input.method,
|
|
311
|
+
path,
|
|
312
|
+
status: response.status,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return parsed as T;
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rebuild the Origin API handle (`ctx.origin`) for inbound hooks.
|
|
3
|
+
*
|
|
4
|
+
* The handle is repo-bound and lazy: no credential is resolved until the
|
|
5
|
+
* first call, so channels without outbound Origin credentials still dispatch
|
|
6
|
+
* normally. Credential resolution combines the channel's `app` option with
|
|
7
|
+
* the environment (`ORIGIN_APP_ID` / `ORIGIN_APP_INSTALLATION_ID` /
|
|
8
|
+
* `ORIGIN_APP_PRIVATE_KEY`); the delivery's installation id fills in when
|
|
9
|
+
* neither pins one — the GitHub webhook `installation.id` analogue.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
createOriginClient,
|
|
14
|
+
type OriginApiMethod,
|
|
15
|
+
type OriginClient,
|
|
16
|
+
resolveOriginAppCredentialParts,
|
|
17
|
+
} from "./api.js";
|
|
18
|
+
import {
|
|
19
|
+
createOriginCheck,
|
|
20
|
+
type OriginCheck,
|
|
21
|
+
type OriginCheckCreateInput,
|
|
22
|
+
} from "./checks.js";
|
|
23
|
+
import type { OriginAppOptions, OriginRepositoryRef } from "./types.js";
|
|
24
|
+
|
|
25
|
+
/** Origin operations exposed to hooks (`ctx.origin`). */
|
|
26
|
+
export interface OriginHandle {
|
|
27
|
+
/** Installation id the outbound credential is minted for, when known. */
|
|
28
|
+
installationId: string | undefined;
|
|
29
|
+
repository: OriginRepositoryRef;
|
|
30
|
+
/**
|
|
31
|
+
* Call an Origin public REST path with the channel's app credential.
|
|
32
|
+
* Throws `OriginApiError` on API failure.
|
|
33
|
+
*/
|
|
34
|
+
request<T = unknown>(input: {
|
|
35
|
+
method: OriginApiMethod;
|
|
36
|
+
path: string;
|
|
37
|
+
body?: unknown;
|
|
38
|
+
}): Promise<T>;
|
|
39
|
+
/**
|
|
40
|
+
* Open a CI check run on `sha` and get a handle for updating and
|
|
41
|
+
* concluding it. Needs an app installation with
|
|
42
|
+
* `repository:checks:write` — Origin check writes are installation-only.
|
|
43
|
+
*/
|
|
44
|
+
createCheck(
|
|
45
|
+
input: Omit<OriginCheckCreateInput, "origin" | "owner" | "repo">
|
|
46
|
+
): Promise<OriginCheck>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildOriginBinding(input: {
|
|
50
|
+
repository: OriginRepositoryRef;
|
|
51
|
+
/** Installation id from the delivery envelope, when present. */
|
|
52
|
+
installationId?: string;
|
|
53
|
+
/** Channel-authored outbound app credentials / API base. */
|
|
54
|
+
app?: OriginAppOptions;
|
|
55
|
+
/** Injectable fetch for tests. */
|
|
56
|
+
fetch?: typeof fetch;
|
|
57
|
+
}): OriginHandle {
|
|
58
|
+
let client: OriginClient | undefined;
|
|
59
|
+
const getClient = (): OriginClient => {
|
|
60
|
+
if (client === undefined) {
|
|
61
|
+
const fromEnv = resolveOriginAppCredentialParts();
|
|
62
|
+
const appId = input.app?.appId ?? fromEnv.appId;
|
|
63
|
+
const installationId =
|
|
64
|
+
input.app?.installationId ??
|
|
65
|
+
input.installationId ??
|
|
66
|
+
fromEnv.installationId;
|
|
67
|
+
const privateKey = input.app?.privateKey ?? fromEnv.privateKey;
|
|
68
|
+
const credentials =
|
|
69
|
+
appId !== undefined &&
|
|
70
|
+
installationId !== undefined &&
|
|
71
|
+
privateKey !== undefined
|
|
72
|
+
? { appId, installationId, privateKey }
|
|
73
|
+
: undefined;
|
|
74
|
+
client = createOriginClient({
|
|
75
|
+
token: input.app?.token,
|
|
76
|
+
credentials,
|
|
77
|
+
apiBaseUrl: input.app?.apiBaseUrl,
|
|
78
|
+
fetch: input.fetch,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return client;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
installationId: input.app?.installationId ?? input.installationId,
|
|
86
|
+
repository: input.repository,
|
|
87
|
+
request: (request) => getClient().request(request),
|
|
88
|
+
createCheck: (check) =>
|
|
89
|
+
createOriginCheck({
|
|
90
|
+
...check,
|
|
91
|
+
origin: getClient(),
|
|
92
|
+
owner: input.repository.owner,
|
|
93
|
+
repo: input.repository.name,
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
}
|