@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.
Files changed (140) hide show
  1. package/dist/channels/checks.d.ts +111 -0
  2. package/dist/channels/checks.d.ts.map +1 -0
  3. package/dist/channels/checks.js +86 -0
  4. package/dist/channels/deployments/deployments-channel.d.ts +44 -0
  5. package/dist/channels/deployments/deployments-channel.d.ts.map +1 -0
  6. package/dist/channels/deployments/deployments-channel.js +92 -0
  7. package/dist/channels/deployments/index.d.ts +26 -0
  8. package/dist/channels/deployments/index.d.ts.map +1 -0
  9. package/dist/channels/deployments/index.js +25 -0
  10. package/dist/channels/deployments/types.d.ts +69 -0
  11. package/dist/channels/deployments/types.d.ts.map +1 -0
  12. package/dist/channels/deployments/types.js +6 -0
  13. package/dist/channels/github/checks.d.ts +13 -48
  14. package/dist/channels/github/checks.d.ts.map +1 -1
  15. package/dist/channels/github/checks.js +56 -64
  16. package/dist/channels/origin/api.d.ts +100 -0
  17. package/dist/channels/origin/api.d.ts.map +1 -0
  18. package/dist/channels/origin/api.js +225 -0
  19. package/dist/channels/origin/binding.d.ts +44 -0
  20. package/dist/channels/origin/binding.d.ts.map +1 -0
  21. package/dist/channels/origin/binding.js +45 -0
  22. package/dist/channels/origin/checks.d.ts +107 -0
  23. package/dist/channels/origin/checks.d.ts.map +1 -0
  24. package/dist/channels/origin/checks.js +145 -0
  25. package/dist/channels/origin/index.d.ts +3 -0
  26. package/dist/channels/origin/index.d.ts.map +1 -1
  27. package/dist/channels/origin/index.js +3 -0
  28. package/dist/channels/origin/origin-channel.d.ts +4 -4
  29. package/dist/channels/origin/origin-channel.d.ts.map +1 -1
  30. package/dist/channels/origin/origin-channel.js +12 -2
  31. package/dist/channels/origin/types.d.ts +36 -0
  32. package/dist/channels/origin/types.d.ts.map +1 -1
  33. package/dist/channels.d.ts +1 -0
  34. package/dist/channels.d.ts.map +1 -1
  35. package/dist/channels.js +1 -0
  36. package/dist/docs/404.html +2 -2
  37. package/dist/docs/ab.html +3 -3
  38. package/dist/docs/assets/{app.BETd-WqU.js → app.b8bhB2SV.js} +1 -1
  39. package/dist/docs/assets/chunks/@localSearchIndexroot.DGZeXVqn.js +1 -0
  40. package/dist/docs/assets/chunks/{VPLocalSearchBox.wEaGEmV9.js → VPLocalSearchBox.lLSeIyWw.js} +1 -1
  41. package/dist/docs/assets/chunks/{theme.DNDicFOP.js → theme.C_owesa-.js} +2 -2
  42. package/dist/docs/assets/{reference_channels.md.MN8iDy8V.js → reference_channels.md.G5GysZ7e.js} +2 -2
  43. package/dist/docs/assets/{reference_channels.md.MN8iDy8V.lean.js → reference_channels.md.G5GysZ7e.lean.js} +1 -1
  44. package/dist/docs/building-with-agents.html +3 -3
  45. package/dist/docs/concepts.html +3 -3
  46. package/dist/docs/deployment.html +3 -3
  47. package/dist/docs/evals.html +3 -3
  48. package/dist/docs/example-agents/approval-buddy.html +3 -3
  49. package/dist/docs/example-agents/benny.html +3 -3
  50. package/dist/docs/example-agents/bugbot.html +3 -3
  51. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  52. package/dist/docs/example-agents/codeowners-review.html +3 -3
  53. package/dist/docs/example-agents/concierge.html +3 -3
  54. package/dist/docs/example-agents/fsd.html +3 -3
  55. package/dist/docs/example-agents/index.html +3 -3
  56. package/dist/docs/example-agents/knowledge-base.html +3 -3
  57. package/dist/docs/example-agents/oncall.html +3 -3
  58. package/dist/docs/example-agents/security-reviewer.html +3 -3
  59. package/dist/docs/example-agents/slack-agent.html +3 -3
  60. package/dist/docs/example-agents/weather-agent.html +3 -3
  61. package/dist/docs/guides/agent-to-agent.html +3 -3
  62. package/dist/docs/guides/cloud-runtime.html +3 -3
  63. package/dist/docs/guides/convert-automation.html +3 -3
  64. package/dist/docs/guides/github.html +3 -3
  65. package/dist/docs/guides/human-in-the-loop.html +3 -3
  66. package/dist/docs/guides/mcp-oauth.html +3 -3
  67. package/dist/docs/guides/opentelemetry.html +3 -3
  68. package/dist/docs/guides/slack.html +3 -3
  69. package/dist/docs/guides/webhooks.html +3 -3
  70. package/dist/docs/hashmap.json +1 -1
  71. package/dist/docs/hillclimbing.html +3 -3
  72. package/dist/docs/index.html +3 -3
  73. package/dist/docs/quickstart.html +3 -3
  74. package/dist/docs/reference/agent-config.html +3 -3
  75. package/dist/docs/reference/artifacts.html +3 -3
  76. package/dist/docs/reference/channels.html +5 -5
  77. package/dist/docs/reference/cli.html +3 -3
  78. package/dist/docs/reference/connections.html +3 -3
  79. package/dist/docs/reference/hooks.html +3 -3
  80. package/dist/docs/reference/http-api.html +3 -3
  81. package/dist/docs/reference/instructions.html +3 -3
  82. package/dist/docs/reference/playground.html +3 -3
  83. package/dist/docs/reference/project-layout.html +3 -3
  84. package/dist/docs/reference/prompt.html +3 -3
  85. package/dist/docs/reference/schedules.html +3 -3
  86. package/dist/docs/reference/sessions.html +3 -3
  87. package/dist/docs/reference/skills.html +3 -3
  88. package/dist/docs/reference/subagents.html +3 -3
  89. package/dist/docs/reference/tools.html +3 -3
  90. package/dist/docs/scaffolding-agents.html +3 -3
  91. package/dist/docs/storage.html +3 -3
  92. package/dist/docs/templates/agentic-owners.html +3 -3
  93. package/dist/docs/templates/demo.html +3 -3
  94. package/dist/docs/templates/pr-autofixer.html +3 -3
  95. package/dist/docs/templates/security-reviewer.html +3 -3
  96. package/dist/docs/templates/triage.html +3 -3
  97. package/dist/docs/troubleshooting.html +3 -3
  98. package/dist/internal/authored-alias-hooks.d.ts +12 -0
  99. package/dist/internal/authored-alias-hooks.d.ts.map +1 -1
  100. package/dist/internal/authored-alias-hooks.js +60 -8
  101. package/dist/internal/authored-loaders.d.ts +5 -3
  102. package/dist/internal/authored-loaders.d.ts.map +1 -1
  103. package/dist/internal/authored-loaders.js +13 -4
  104. package/dist/internal/cursor-deployment-relay.d.ts +77 -0
  105. package/dist/internal/cursor-deployment-relay.d.ts.map +1 -0
  106. package/dist/internal/cursor-deployment-relay.js +201 -0
  107. package/dist/internal/deployment-scm-event-stream.js +4 -4
  108. package/dist/internal/distribution.d.ts.map +1 -1
  109. package/dist/internal/distribution.js +1 -0
  110. package/dist/internal/review-comments.d.ts +62 -0
  111. package/dist/internal/review-comments.d.ts.map +1 -1
  112. package/dist/internal/review-comments.js +199 -0
  113. package/dist/internal/server.d.ts.map +1 -1
  114. package/dist/internal/server.js +114 -5
  115. package/dist/playground/assets/{index-D_8I9TM0.js → index-BWqjJcRo.js} +1 -1
  116. package/dist/playground/assets/index-Bfwz2yLN.css +1 -0
  117. package/dist/playground/index.html +2 -2
  118. package/docs/reference/channels.md +15 -0
  119. package/package.json +7 -1
  120. package/src/channels/checks.ts +185 -0
  121. package/src/channels/deployments/deployments-channel.ts +109 -0
  122. package/src/channels/deployments/index.ts +26 -0
  123. package/src/channels/deployments/types.ts +80 -0
  124. package/src/channels/github/checks.ts +90 -115
  125. package/src/channels/origin/api.ts +318 -0
  126. package/src/channels/origin/binding.ts +96 -0
  127. package/src/channels/origin/checks.ts +250 -0
  128. package/src/channels/origin/index.ts +3 -0
  129. package/src/channels/origin/origin-channel.ts +18 -6
  130. package/src/channels/origin/types.ts +37 -0
  131. package/src/channels.ts +16 -0
  132. package/src/internal/authored-alias-hooks.ts +75 -7
  133. package/src/internal/authored-loaders.ts +15 -4
  134. package/src/internal/cursor-deployment-relay.ts +274 -0
  135. package/src/internal/deployment-scm-event-stream.ts +4 -4
  136. package/src/internal/distribution.ts +1 -0
  137. package/src/internal/review-comments.ts +286 -0
  138. package/src/internal/server.ts +149 -0
  139. package/dist/docs/assets/chunks/@localSearchIndexroot.DM5INS71.js +0 -1
  140. package/dist/playground/assets/index-M3nSMc6_.css +0 -1
@@ -1,81 +1,115 @@
1
1
  /**
2
- * Generic CI checks (GitHub Checks API) for agent authors.
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 a
5
- * handle for updating and concluding it, so an agent can report its own CI
6
- * result (name, summary, and a "Details" link e.g. the agent session run).
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 GitHubCheckRunConclusion,
19
- type GitHubCheckRunStatus,
20
- wrapOctokitError,
21
- } from "./api.js";
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
- /** Output shown on the check page (both fields required by GitHub). */
29
- export interface GitHubCheckOutput {
30
- /** Heading on the check page. */
31
- title: string;
32
- /** Markdown body (supports links, e.g. `[View session](…)`). */
33
- summary: string;
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
- export interface GitHubCheckConcludeInput {
63
- /** Final result shown in the merge box. */
64
- conclusion: GitHubCheckRunConclusion;
65
- detailsUrl?: string;
66
- output?: GitHubCheckOutput;
67
- }
68
-
69
- /** A created check run. All methods throw `GitHubApiError` on API failure. */
70
- export interface GitHubCheck {
71
- /** Check run id (for reuse across process restarts). */
72
- readonly id: number;
73
- /** GitHub web URL of the check run page, when returned by the API. */
74
- readonly url: string | undefined;
75
- /** Patch status / output / details link while the check is running. */
76
- update(input: GitHubCheckUpdateInput): Promise<void>;
77
- /** Complete the check with a conclusion (sets `completed_at`). */
78
- conclude(input: GitHubCheckConcludeInput): Promise<void>;
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 async function createGitHubCheck(
132
+ export function createGitHubCheck(
99
133
  input: GitHubCheckCreateInput
100
134
  ): Promise<GitHubCheck> {
101
- const octokit = await input.github.getOctokit();
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
- const patch = async (update: GitHubCheckUpdateInput): Promise<void> => {
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
- update: patch,
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
+ }