@cursor/july 0.1.55 → 0.1.61
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/bin/agent-serve.js +30 -23
- package/dist/channels/github/github-channel.d.ts +12 -0
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +67 -15
- package/dist/channels/github/types.d.ts +27 -1
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels/origin/cursor-account.d.ts +22 -0
- package/dist/channels/origin/cursor-account.d.ts.map +1 -0
- package/dist/channels/origin/cursor-account.js +35 -0
- package/dist/channels/origin/events.d.ts +10 -0
- package/dist/channels/origin/events.d.ts.map +1 -0
- package/dist/channels/origin/events.js +27 -0
- package/dist/channels/origin/index.d.ts +29 -0
- package/dist/channels/origin/index.d.ts.map +1 -0
- package/dist/channels/origin/index.js +28 -0
- package/dist/channels/origin/origin-channel.d.ts +68 -0
- package/dist/channels/origin/origin-channel.d.ts.map +1 -0
- package/dist/channels/origin/origin-channel.js +567 -0
- package/dist/channels/origin/origin-webhook.d.ts +38 -0
- package/dist/channels/origin/origin-webhook.d.ts.map +1 -0
- package/dist/channels/origin/origin-webhook.js +151 -0
- package/dist/channels/origin/types.d.ts +152 -0
- package/dist/channels/origin/types.d.ts.map +1 -0
- package/dist/channels/origin/types.js +1 -0
- package/dist/channels/slack/doctor.d.ts +17 -1
- package/dist/channels/slack/doctor.d.ts.map +1 -1
- package/dist/channels/slack/doctor.js +186 -10
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.CVQxhV85.js → app.CW3KJh72.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.BuymfBAo.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.QAPzJjql.js → VPLocalSearchBox.Bk6EWpPj.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CSxTCOFu.js → theme.Bz9AodBA.js} +2 -2
- package/dist/docs/assets/{example-agents_fsd.md.DPz9ezO4.js → example-agents_fsd.md.ZeGEpAw_.js} +2 -2
- package/dist/docs/assets/{example-agents_fsd.md.DPz9ezO4.lean.js → example-agents_fsd.md.ZeGEpAw_.lean.js} +1 -1
- package/dist/docs/assets/{example-agents_security-reviewer.md.Dkf1gyo6.js → example-agents_security-reviewer.md.DBL1TwtP.js} +2 -2
- package/dist/docs/assets/{example-agents_security-reviewer.md.Dkf1gyo6.lean.js → example-agents_security-reviewer.md.DBL1TwtP.lean.js} +1 -1
- package/dist/docs/assets/guides_convert-automation.md.BhMzTkE5.js +12 -0
- package/dist/docs/assets/guides_convert-automation.md.BhMzTkE5.lean.js +1 -0
- package/dist/docs/assets/{guides_github.md.B8QKtIpw.js → guides_github.md.CLLQJKRB.js} +4 -4
- package/dist/docs/assets/{guides_github.md.B8QKtIpw.lean.js → guides_github.md.CLLQJKRB.lean.js} +1 -1
- package/dist/docs/assets/index.md.DhRHS_-L.js +5 -0
- package/dist/docs/assets/{index.md.COeoSkcF.lean.js → index.md.DhRHS_-L.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.D_CCfG8R.js → reference_cli.md.Cl14Jfo3.js} +8 -2
- package/dist/docs/assets/{reference_cli.md.D_CCfG8R.lean.js → reference_cli.md.Cl14Jfo3.lean.js} +1 -1
- package/dist/docs/assets/scaffolding-agents.md.CrWSA0Ko.js +1 -0
- package/dist/docs/assets/{scaffolding-agents.md.mkc3B_ZW.lean.js → scaffolding-agents.md.CrWSA0Ko.lean.js} +1 -1
- package/dist/docs/assets/templates_agentic-owners.md.BkTLORaU.js +14 -0
- package/dist/docs/assets/templates_agentic-owners.md.BkTLORaU.lean.js +1 -0
- package/dist/docs/assets/templates_demo.md.Bgd6MBaZ.js +4 -0
- package/dist/docs/assets/templates_demo.md.Bgd6MBaZ.lean.js +1 -0
- package/dist/docs/assets/templates_pr-autofixer.md.DcmoeUNZ.js +8 -0
- package/dist/docs/assets/templates_pr-autofixer.md.DcmoeUNZ.lean.js +1 -0
- package/dist/docs/assets/templates_security-reviewer.md.C0yIUaYs.js +2 -0
- package/dist/docs/assets/templates_security-reviewer.md.C0yIUaYs.lean.js +1 -0
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +5 -5
- package/dist/docs/deployment.html +4 -4
- package/dist/docs/evals.html +4 -4
- package/dist/docs/example-agents/approval-buddy.html +4 -4
- package/dist/docs/example-agents/benny.html +4 -4
- package/dist/docs/example-agents/bugbot.html +4 -4
- package/dist/docs/example-agents/codebase-wiki.html +4 -4
- package/dist/docs/example-agents/codeowners-review.html +4 -4
- package/dist/docs/example-agents/concierge.html +4 -4
- package/dist/docs/example-agents/fsd.html +5 -5
- package/dist/docs/example-agents/index.html +4 -4
- package/dist/docs/example-agents/knowledge-base.html +4 -4
- package/dist/docs/example-agents/oncall.html +4 -4
- package/dist/docs/example-agents/security-reviewer.html +5 -5
- package/dist/docs/example-agents/slack-agent.html +4 -4
- package/dist/docs/example-agents/weather-agent.html +4 -4
- package/dist/docs/guides/agent-to-agent.html +4 -4
- package/dist/docs/guides/cloud-runtime.html +4 -4
- package/dist/docs/guides/convert-automation.html +36 -0
- package/dist/docs/guides/github.html +7 -7
- package/dist/docs/guides/human-in-the-loop.html +4 -4
- package/dist/docs/guides/mcp-oauth.html +4 -4
- package/dist/docs/guides/opentelemetry.html +4 -4
- package/dist/docs/guides/slack.html +4 -4
- package/dist/docs/guides/webhooks.html +4 -4
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +4 -4
- package/dist/docs/index.html +6 -6
- package/dist/docs/quickstart.html +4 -4
- package/dist/docs/reference/agent-config.html +4 -4
- package/dist/docs/reference/artifacts.html +4 -4
- package/dist/docs/reference/channels.html +4 -4
- package/dist/docs/reference/cli.html +12 -6
- package/dist/docs/reference/connections.html +4 -4
- package/dist/docs/reference/hooks.html +4 -4
- package/dist/docs/reference/http-api.html +4 -4
- package/dist/docs/reference/instructions.html +4 -4
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/prompt.html +4 -4
- package/dist/docs/reference/schedules.html +4 -4
- package/dist/docs/reference/sessions.html +4 -4
- package/dist/docs/reference/skills.html +4 -4
- package/dist/docs/reference/subagents.html +4 -4
- package/dist/docs/reference/tools.html +4 -4
- package/dist/docs/scaffolding-agents.html +5 -5
- package/dist/docs/storage.html +4 -4
- package/dist/docs/templates/agentic-owners.html +38 -0
- package/dist/docs/templates/demo.html +28 -0
- package/dist/docs/templates/pr-autofixer.html +32 -0
- package/dist/docs/templates/security-reviewer.html +26 -0
- package/dist/docs/templates/triage.html +5 -5
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/internal/cli-ax.d.ts +2 -0
- package/dist/internal/cli-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +20 -0
- package/dist/internal/cli-slack.d.ts +2 -0
- package/dist/internal/cli-slack.d.ts.map +1 -1
- package/dist/internal/cli-slack.js +11 -2
- package/dist/internal/cursor-event-relay.d.ts +59 -11
- package/dist/internal/cursor-event-relay.d.ts.map +1 -1
- package/dist/internal/cursor-event-relay.js +133 -26
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +11 -2
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +1 -0
- package/dist/internal/handleAgentServeTrigger.d.ts +33 -6
- package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
- package/dist/internal/handleAgentServeTrigger.js +97 -19
- package/dist/internal/init-interview.d.ts +67 -0
- package/dist/internal/init-interview.d.ts.map +1 -0
- package/dist/internal/init-interview.js +300 -0
- package/dist/internal/init-project.d.ts +4 -0
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +40 -16
- package/dist/internal/mcp-endpoint.d.ts +7 -0
- package/dist/internal/mcp-endpoint.d.ts.map +1 -1
- package/dist/internal/mcp-endpoint.js +7 -0
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +159 -57
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +20 -21
- package/dist/internal/session-links.d.ts +14 -0
- package/dist/internal/session-links.d.ts.map +1 -0
- package/dist/internal/session-links.js +69 -0
- package/dist/internal/sessions-client.d.ts.map +1 -1
- package/dist/internal/sessions-client.js +1 -0
- package/dist/playground/assets/index-ClbuaLpm.css +1 -0
- package/dist/playground/assets/{index-0oqXUkrR.js → index-SqkR13k_.js} +39 -39
- package/dist/playground/index.html +2 -2
- package/dist/types.d.ts +19 -12
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +15 -0
- package/docs/example-agents/fsd.md +3 -0
- package/docs/example-agents/security-reviewer.md +3 -0
- package/docs/guides/convert-automation.md +176 -0
- package/docs/guides/github.md +6 -2
- package/docs/reference/cli.md +34 -10
- package/docs/scaffolding-agents.md +3 -0
- package/docs/templates/agentic-owners.md +97 -0
- package/docs/templates/demo.md +84 -0
- package/docs/templates/pr-autofixer.md +136 -0
- package/docs/templates/security-reviewer.md +89 -0
- package/package.json +9 -1
- package/skills/github/SKILL.md +4 -5
- package/src/bin/agent-serve.ts +13 -6
- package/src/channels/github/github-channel.ts +95 -18
- package/src/channels/github/types.ts +31 -0
- package/src/channels/origin/cursor-account.ts +64 -0
- package/src/channels/origin/events.ts +41 -0
- package/src/channels/origin/index.ts +29 -0
- package/src/channels/origin/origin-channel.ts +784 -0
- package/src/channels/origin/origin-webhook.ts +202 -0
- package/src/channels/origin/types.ts +186 -0
- package/src/channels/slack/doctor.ts +218 -5
- package/src/index.ts +3 -1
- package/src/internal/cli-ax.ts +23 -0
- package/src/internal/cli-slack.ts +11 -1
- package/src/internal/cursor-event-relay.ts +193 -31
- package/src/internal/discovery.ts +15 -5
- package/src/internal/distribution.ts +1 -0
- package/src/internal/handleAgentServeTrigger.ts +129 -19
- package/src/internal/init-interview.ts +384 -0
- package/src/internal/init-project.ts +70 -15
- package/src/internal/mcp-endpoint.ts +7 -0
- package/src/internal/server.ts +188 -58
- package/src/internal/session-engine.ts +5 -21
- package/src/internal/session-links.ts +88 -0
- package/src/internal/sessions-client.ts +1 -0
- package/src/types.ts +19 -12
- package/templates/agentic-owners/README.md +73 -0
- package/templates/agentic-owners/agent/agent.ts +15 -0
- package/templates/agentic-owners/agent/channels/github.ts +85 -0
- package/templates/agentic-owners/agent/instructions.md +34 -0
- package/templates/agentic-owners/agent/lib/config.ts +13 -0
- package/templates/agentic-owners/agent/lib/github.ts +241 -0
- package/templates/agentic-owners/agent/lib/owners/README.md +50 -0
- package/templates/agentic-owners/agent/lib/owners/constants.ts +22 -0
- package/templates/agentic-owners/agent/lib/owners/decision.ts +485 -0
- package/templates/agentic-owners/agent/lib/owners/handles.ts +54 -0
- package/templates/agentic-owners/agent/lib/owners/match.ts +177 -0
- package/templates/agentic-owners/agent/lib/owners/matching.ts +95 -0
- package/templates/agentic-owners/agent/lib/owners/parse.ts +589 -0
- package/templates/agentic-owners/agent/lib/owners/policyMode.ts +65 -0
- package/templates/agentic-owners/agent/lib/owners/prompt.ts +66 -0
- package/templates/agentic-owners/agent/lib/owners/prompts/base-review.md +105 -0
- package/templates/agentic-owners/agent/lib/owners/types.ts +45 -0
- package/templates/agentic-owners/agent/lib/review.ts +777 -0
- package/templates/agentic-owners/agent/tools/record_review.ts +69 -0
- package/templates/agentic-owners/evals/evals.config.ts +5 -0
- package/templates/agentic-owners/evals/owners.eval.ts +116 -0
- package/templates/agentic-owners/package.json +20 -0
- package/templates/agentic-owners/tsconfig.json +12 -0
- package/templates/demo/README.md +33 -0
- package/templates/demo/agent/agent.ts +15 -0
- package/templates/demo/agent/channels/github.ts +50 -0
- package/templates/demo/agent/channels/queue.ts +30 -0
- package/templates/demo/agent/instructions.md +8 -0
- package/templates/demo/agent/lib/collect.ts +78 -0
- package/templates/demo/agent/tools/record_demo.ts +40 -0
- package/templates/demo/evals/demo.eval.ts +33 -0
- package/templates/demo/evals/evals.config.ts +5 -0
- package/templates/demo/package.json +18 -0
- package/templates/demo/tsconfig.json +12 -0
- package/templates/pr-autofixer/README.md +44 -0
- package/templates/pr-autofixer/agent/agent.ts +22 -0
- package/templates/pr-autofixer/agent/channels/drive.ts +40 -0
- package/templates/pr-autofixer/agent/channels/github.ts +167 -0
- package/templates/pr-autofixer/agent/channels/slack.ts +17 -0
- package/templates/pr-autofixer/agent/hooks/bind-agent.ts +19 -0
- package/templates/pr-autofixer/agent/instructions.md +83 -0
- package/templates/pr-autofixer/agent/lib/affinity.ts +33 -0
- package/templates/pr-autofixer/agent/lib/drive-schemas.ts +15 -0
- package/templates/pr-autofixer/agent/lib/drive.ts +79 -0
- package/templates/pr-autofixer/agent/lib/merge-watch.ts +123 -0
- package/templates/pr-autofixer/agent/lib/pr-state.ts +54 -0
- package/templates/pr-autofixer/agent/lib/repos.ts +19 -0
- package/templates/pr-autofixer/agent/lib/start-drive.ts +122 -0
- package/templates/pr-autofixer/agent/lib/webhook-buffer.ts +225 -0
- package/templates/pr-autofixer/agent/lib/webhook-routing.ts +231 -0
- package/templates/pr-autofixer/agent/storage.ts +7 -0
- package/templates/pr-autofixer/agent/tools/drive_pr.ts +22 -0
- package/templates/pr-autofixer/evals/coordinator.eval.ts +16 -0
- package/templates/pr-autofixer/evals/evals.config.ts +5 -0
- package/templates/pr-autofixer/init.json +25 -0
- package/templates/pr-autofixer/package.json +18 -0
- package/templates/pr-autofixer/tsconfig.json +12 -0
- package/templates/security-reviewer/README.md +31 -0
- package/templates/security-reviewer/agent/agent.ts +15 -0
- package/templates/security-reviewer/agent/channels/github.ts +46 -0
- package/templates/security-reviewer/agent/instructions.md +22 -0
- package/templates/security-reviewer/agent/lib/repos.ts +5 -0
- package/templates/security-reviewer/agent/tools/post_findings.ts +72 -0
- package/templates/security-reviewer/evals/evals.config.ts +5 -0
- package/templates/security-reviewer/evals/review.eval.ts +53 -0
- package/templates/security-reviewer/init.json +25 -0
- package/templates/security-reviewer/package.json +18 -0
- package/templates/security-reviewer/tsconfig.json +12 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.BFpWSo6D.js +0 -1
- package/dist/docs/assets/index.md.COeoSkcF.js +0 -5
- package/dist/docs/assets/scaffolding-agents.md.mkc3B_ZW.js +0 -1
- package/dist/playground/assets/index-tHCjJxv4.css +0 -1
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
allowAll,
|
|
4
|
+
defineChannel,
|
|
5
|
+
localDevStrict,
|
|
6
|
+
loopbackOnly,
|
|
7
|
+
POST,
|
|
8
|
+
} from "../../channels.js";
|
|
9
|
+
import { isDevMode } from "../../internal/dev-mode.js";
|
|
10
|
+
import type {
|
|
11
|
+
AgentCloudOptions,
|
|
12
|
+
AuthContext,
|
|
13
|
+
ChannelDefinition,
|
|
14
|
+
ChannelHandlerArgs,
|
|
15
|
+
} from "../../types.js";
|
|
16
|
+
import {
|
|
17
|
+
parseCursorOriginRepoScope,
|
|
18
|
+
registerOriginCursorAccountRuntime,
|
|
19
|
+
} from "./cursor-account.js";
|
|
20
|
+
import { deriveOriginWebhookEvents, ORIGIN_CHANNEL_KIND } from "./events.js";
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_ORIGIN_API_BASE_URL,
|
|
23
|
+
type OriginWebhookHeaders,
|
|
24
|
+
verifyOriginWebhook,
|
|
25
|
+
} from "./origin-webhook.js";
|
|
26
|
+
import type {
|
|
27
|
+
OriginActor,
|
|
28
|
+
OriginChannelOptions,
|
|
29
|
+
OriginComment,
|
|
30
|
+
OriginDispatch,
|
|
31
|
+
OriginEventContext,
|
|
32
|
+
OriginPullRequestEvent,
|
|
33
|
+
OriginRepositoryRef,
|
|
34
|
+
OriginReview,
|
|
35
|
+
OriginTaskDispatch,
|
|
36
|
+
OriginTurnDispatch,
|
|
37
|
+
OriginWebhookEnvelope,
|
|
38
|
+
} from "./types.js";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Default dispatch gate when no `onPullRequest` hook is declared: the common
|
|
42
|
+
* review triggers start a turn as the delivery's actor.
|
|
43
|
+
*/
|
|
44
|
+
const DEFAULT_PULL_REQUEST_TYPES: ReadonlySet<string> = new Set([
|
|
45
|
+
"pull_request.created",
|
|
46
|
+
"pull_request.published",
|
|
47
|
+
"pull_request.head_ref.pushed",
|
|
48
|
+
"pull_request.base_ref.updated",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
52
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
53
|
+
? (value as Record<string, unknown>)
|
|
54
|
+
: undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function asNonEmptyString(value: unknown): string | undefined {
|
|
58
|
+
return typeof value === "string" && value.trim() !== ""
|
|
59
|
+
? value.trim()
|
|
60
|
+
: undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function asPositiveInt(value: unknown): number | undefined {
|
|
64
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
if (typeof value === "string" && /^[1-9]\d*$/.test(value)) {
|
|
68
|
+
return Number(value);
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function asBoolean(value: unknown): boolean | undefined {
|
|
74
|
+
return typeof value === "boolean" ? value : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function originActor(value: unknown): OriginActor | undefined {
|
|
78
|
+
const record = asRecord(value);
|
|
79
|
+
if (record === undefined) return undefined;
|
|
80
|
+
const nestedUser = asRecord(record.user);
|
|
81
|
+
const nestedApp = asRecord(record.app);
|
|
82
|
+
const id =
|
|
83
|
+
asNonEmptyString(nestedUser?.id) ??
|
|
84
|
+
asNonEmptyString(nestedApp?.id) ??
|
|
85
|
+
asNonEmptyString(asRecord(record.serviceAccount)?.id) ??
|
|
86
|
+
asNonEmptyString(record.id);
|
|
87
|
+
if (id === undefined) return undefined;
|
|
88
|
+
const kind = asNonEmptyString(record.kind);
|
|
89
|
+
return kind === undefined ? { id } : { id, kind };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function originHeaders(request: Request): OriginWebhookHeaders {
|
|
93
|
+
return {
|
|
94
|
+
"webhook-id": request.headers.get("webhook-id") ?? undefined,
|
|
95
|
+
"webhook-timestamp": request.headers.get("webhook-timestamp") ?? undefined,
|
|
96
|
+
"webhook-signature": request.headers.get("webhook-signature") ?? undefined,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function skipOriginVerify(): boolean {
|
|
101
|
+
return process.env.AGENT_SERVE_ORIGIN_SKIP_VERIFY?.trim() === "1";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Direct Origin HTTP deliveries verify v1ed. `--dev` and
|
|
106
|
+
* `AGENT_SERVE_ORIGIN_SKIP_VERIFY=1` skip that so local fixtures work, matching
|
|
107
|
+
* GitHub's unsigned loopback posture when no webhook secret is set.
|
|
108
|
+
*/
|
|
109
|
+
function shouldSkipOriginVerify(): boolean {
|
|
110
|
+
return skipOriginVerify() || isDevMode();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function asIdList(value: string | readonly string[] | undefined): string[] {
|
|
114
|
+
if (value === undefined) return [];
|
|
115
|
+
const items = typeof value === "string" ? [value] : [...value];
|
|
116
|
+
return [...new Set(items.map((id) => id.trim()).filter(Boolean))];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** `AGENT_SERVE_ORIGIN_APP_ID` fallback for {@link OriginChannelOptions.appId}. */
|
|
120
|
+
export function envOriginAppId(): string | undefined {
|
|
121
|
+
const value = process.env.AGENT_SERVE_ORIGIN_APP_ID?.trim();
|
|
122
|
+
return value === undefined || value === "" ? undefined : value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function configuredOriginAppIds(
|
|
126
|
+
options: OriginChannelOptions
|
|
127
|
+
): readonly string[] {
|
|
128
|
+
const fromOptions = asIdList(options.appId);
|
|
129
|
+
if (fromOptions.length > 0) return fromOptions;
|
|
130
|
+
const fromEnv = envOriginAppId();
|
|
131
|
+
return fromEnv === undefined ? [] : [fromEnv];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function configuredOriginInstallationIds(
|
|
135
|
+
options: OriginChannelOptions
|
|
136
|
+
): readonly string[] {
|
|
137
|
+
return asIdList(options.installationId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Repo allowlist for HTTP and trigger fan-out. `undefined` means no extra
|
|
142
|
+
* repo gate (the bound Origin app's installations still bound what Origin
|
|
143
|
+
* will send).
|
|
144
|
+
*/
|
|
145
|
+
function configuredOriginRepoAllowlist(
|
|
146
|
+
options: OriginChannelOptions
|
|
147
|
+
): ReadonlySet<string> | undefined {
|
|
148
|
+
const repos: string[] = [];
|
|
149
|
+
if (options.repos !== undefined) {
|
|
150
|
+
repos.push(...parseCursorOriginRepoScope(options.repos).repos);
|
|
151
|
+
}
|
|
152
|
+
if (typeof options.cursorAccount === "object") {
|
|
153
|
+
repos.push(
|
|
154
|
+
...parseCursorOriginRepoScope(options.cursorAccount.repos).repos
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return repos.length === 0 ? undefined : new Set(repos);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* v1ed proves Origin signed the body. Channel auth is the signed
|
|
162
|
+
* `envelope.appId` matching this channel's configured Origin app (the GitHub
|
|
163
|
+
* webhook-secret analogue). Cursor-internal documents the same gap.
|
|
164
|
+
*/
|
|
165
|
+
function originHttpBindingError(
|
|
166
|
+
options: OriginChannelOptions,
|
|
167
|
+
envelope: OriginWebhookEnvelope
|
|
168
|
+
): string | undefined {
|
|
169
|
+
const allowedApps = configuredOriginAppIds(options);
|
|
170
|
+
if (allowedApps.length === 0) {
|
|
171
|
+
return "unbound_origin_app";
|
|
172
|
+
}
|
|
173
|
+
const deliveredAppId = envelope.appId?.trim();
|
|
174
|
+
if (deliveredAppId === undefined || deliveredAppId === "") {
|
|
175
|
+
return "invalid_app";
|
|
176
|
+
}
|
|
177
|
+
if (!allowedApps.includes(deliveredAppId)) {
|
|
178
|
+
return "invalid_app";
|
|
179
|
+
}
|
|
180
|
+
const allowedInstalls = configuredOriginInstallationIds(options);
|
|
181
|
+
if (allowedInstalls.length === 0) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const deliveredInstallationId = envelope.installationId?.trim();
|
|
185
|
+
if (
|
|
186
|
+
deliveredInstallationId === undefined ||
|
|
187
|
+
deliveredInstallationId === "" ||
|
|
188
|
+
!allowedInstalls.includes(deliveredInstallationId)
|
|
189
|
+
) {
|
|
190
|
+
return "invalid_installation";
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function resolveApiBaseUrl(options: OriginChannelOptions): string {
|
|
196
|
+
const authored =
|
|
197
|
+
typeof options.apiBaseUrl === "function"
|
|
198
|
+
? options.apiBaseUrl()
|
|
199
|
+
: options.apiBaseUrl;
|
|
200
|
+
const trimmed = authored?.trim();
|
|
201
|
+
return trimmed === undefined || trimmed === ""
|
|
202
|
+
? DEFAULT_ORIGIN_API_BASE_URL
|
|
203
|
+
: trimmed;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const originEnvelopeSchema: z.ZodType<OriginWebhookEnvelope> = z.object({
|
|
207
|
+
deliveryId: z.string().optional(),
|
|
208
|
+
appId: z.string().optional(),
|
|
209
|
+
installationId: z.string().optional(),
|
|
210
|
+
sender: z
|
|
211
|
+
.object({
|
|
212
|
+
id: z.string().min(1),
|
|
213
|
+
kind: z.string().optional(),
|
|
214
|
+
})
|
|
215
|
+
.optional(),
|
|
216
|
+
event: z.object({
|
|
217
|
+
id: z.string().optional(),
|
|
218
|
+
type: z.string().min(1),
|
|
219
|
+
eventTime: z.string().optional(),
|
|
220
|
+
payload: z.unknown().optional(),
|
|
221
|
+
}),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
export function parseOriginWebhookEnvelope(
|
|
225
|
+
payload: unknown
|
|
226
|
+
): OriginWebhookEnvelope | null {
|
|
227
|
+
const parsed = originEnvelopeSchema.safeParse(payload);
|
|
228
|
+
return parsed.success ? parsed.data : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function originRepoFromPayload(
|
|
232
|
+
payload: Record<string, unknown>
|
|
233
|
+
): OriginRepositoryRef | undefined {
|
|
234
|
+
const repository =
|
|
235
|
+
asRecord(payload.repository) ??
|
|
236
|
+
asRecord(asRecord(payload.pullRequest)?.repository);
|
|
237
|
+
const owner = asNonEmptyString(asRecord(repository?.owner)?.slug);
|
|
238
|
+
const name = asNonEmptyString(repository?.name);
|
|
239
|
+
if (owner === undefined || name === undefined) return undefined;
|
|
240
|
+
return { owner, name, fullName: `${owner}/${name}` };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function originPrUrl(
|
|
244
|
+
repository: OriginRepositoryRef,
|
|
245
|
+
number: number,
|
|
246
|
+
explicit?: string
|
|
247
|
+
): string {
|
|
248
|
+
return (
|
|
249
|
+
explicit ??
|
|
250
|
+
`https://cursor.com/codebase/${repository.owner}/${repository.name}/pull/${number}`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function originSender(
|
|
255
|
+
envelope: OriginWebhookEnvelope,
|
|
256
|
+
payload: Record<string, unknown>
|
|
257
|
+
): OriginActor | undefined {
|
|
258
|
+
return (
|
|
259
|
+
envelope.sender ??
|
|
260
|
+
originActor(payload.actor) ??
|
|
261
|
+
originActor(payload.pusher) ??
|
|
262
|
+
originActor(asRecord(payload.review)?.author) ??
|
|
263
|
+
originActor(asRecord(payload.comment)?.author) ??
|
|
264
|
+
originActor(asRecord(payload.pullRequest)?.author)
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function parseOriginPullRequestEvent(input: {
|
|
269
|
+
envelope: OriginWebhookEnvelope;
|
|
270
|
+
}): {
|
|
271
|
+
ctx: Omit<OriginEventContext, "host" | "artifacts">;
|
|
272
|
+
pullRequest: OriginPullRequestEvent;
|
|
273
|
+
payload: Record<string, unknown>;
|
|
274
|
+
} | null {
|
|
275
|
+
const payload = asRecord(input.envelope.event.payload) ?? {};
|
|
276
|
+
const repository = originRepoFromPayload(payload);
|
|
277
|
+
const pullRequest = asRecord(payload.pullRequest);
|
|
278
|
+
const number = asPositiveInt(pullRequest?.number);
|
|
279
|
+
if (repository === undefined || number === undefined) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const eventType = input.envelope.event.type;
|
|
283
|
+
const url = originPrUrl(
|
|
284
|
+
repository,
|
|
285
|
+
number,
|
|
286
|
+
asNonEmptyString(pullRequest?.url)
|
|
287
|
+
);
|
|
288
|
+
const head = asRecord(pullRequest?.head);
|
|
289
|
+
const base = asRecord(pullRequest?.base);
|
|
290
|
+
const version = asRecord(pullRequest?.version);
|
|
291
|
+
const author = originActor(pullRequest?.author);
|
|
292
|
+
return {
|
|
293
|
+
payload,
|
|
294
|
+
ctx: {
|
|
295
|
+
deliveryId: input.envelope.deliveryId,
|
|
296
|
+
eventType,
|
|
297
|
+
repository,
|
|
298
|
+
pullRequestNumber: number,
|
|
299
|
+
pullRequestUrl: url,
|
|
300
|
+
sender: originSender(input.envelope, payload),
|
|
301
|
+
},
|
|
302
|
+
pullRequest: {
|
|
303
|
+
type: eventType,
|
|
304
|
+
number,
|
|
305
|
+
url,
|
|
306
|
+
title: asNonEmptyString(pullRequest?.title),
|
|
307
|
+
body:
|
|
308
|
+
pullRequest?.body === null
|
|
309
|
+
? null
|
|
310
|
+
: (asNonEmptyString(pullRequest?.body) ??
|
|
311
|
+
(typeof pullRequest?.body === "string"
|
|
312
|
+
? pullRequest.body
|
|
313
|
+
: undefined)),
|
|
314
|
+
draft: asBoolean(pullRequest?.draft),
|
|
315
|
+
merged:
|
|
316
|
+
eventType === "pull_request.merged" || asBoolean(pullRequest?.merged),
|
|
317
|
+
authorId: author?.id,
|
|
318
|
+
headRef: asNonEmptyString(head?.ref),
|
|
319
|
+
headSha:
|
|
320
|
+
asNonEmptyString(head?.sha) ?? asNonEmptyString(version?.headSha),
|
|
321
|
+
baseRef: asNonEmptyString(base?.ref),
|
|
322
|
+
baseSha:
|
|
323
|
+
asNonEmptyString(base?.sha) ?? asNonEmptyString(version?.baseSha),
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Derive the turn's auth from the actor that triggered the delivery — the
|
|
330
|
+
* standard `{ auth: defaultOriginAuth(ctx) }` dispatch from an inbound hook.
|
|
331
|
+
* The subscriber's Cursor identity is not the turn principal (same pattern
|
|
332
|
+
* as {@link defaultGitHubAuth}).
|
|
333
|
+
*/
|
|
334
|
+
export function defaultOriginAuth(
|
|
335
|
+
ctx: Omit<OriginEventContext, "host" | "artifacts">
|
|
336
|
+
): AuthContext {
|
|
337
|
+
const senderId = ctx.sender?.id;
|
|
338
|
+
return {
|
|
339
|
+
authenticator: "origin-webhook",
|
|
340
|
+
principalId: senderId ?? "origin",
|
|
341
|
+
principalType: ctx.sender?.kind === "app" ? "service" : "user",
|
|
342
|
+
attributes: {
|
|
343
|
+
conversation_kind: "pull_request",
|
|
344
|
+
repository: ctx.repository.fullName,
|
|
345
|
+
...(ctx.deliveryId === undefined ? {} : { delivery_id: ctx.deliveryId }),
|
|
346
|
+
...(senderId === undefined ? {} : { subject: senderId }),
|
|
347
|
+
...(ctx.sender?.kind === undefined ? {} : { user_type: ctx.sender.kind }),
|
|
348
|
+
issuer: `origin:${ctx.repository.owner}`,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function originContinuationKey(
|
|
354
|
+
ctx: Pick<OriginEventContext, "repository" | "pullRequestNumber">
|
|
355
|
+
): string {
|
|
356
|
+
return `origin-pr:${ctx.repository.fullName}#${ctx.pullRequestNumber}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function defaultOriginTitle(
|
|
360
|
+
ctx: Pick<OriginEventContext, "repository" | "pullRequestNumber">
|
|
361
|
+
): string {
|
|
362
|
+
return `Origin ${ctx.repository.fullName}#${ctx.pullRequestNumber}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function defaultOriginPullRequestMessage(
|
|
366
|
+
ctx: Omit<OriginEventContext, "host" | "artifacts">,
|
|
367
|
+
pullRequest: OriginPullRequestEvent
|
|
368
|
+
): string {
|
|
369
|
+
const head =
|
|
370
|
+
pullRequest.headRef !== undefined && pullRequest.headSha !== undefined
|
|
371
|
+
? `Head: ${pullRequest.headRef} (${pullRequest.headSha})`
|
|
372
|
+
: undefined;
|
|
373
|
+
return [
|
|
374
|
+
`Origin pull request trigger: ${ctx.repository.fullName}#${pullRequest.number}`,
|
|
375
|
+
`Event: ${pullRequest.type}`,
|
|
376
|
+
`URL: ${pullRequest.url}`,
|
|
377
|
+
...(pullRequest.title === undefined ? [] : [`Title: ${pullRequest.title}`]),
|
|
378
|
+
...(head === undefined ? [] : [head]),
|
|
379
|
+
"Handle this pull request event.",
|
|
380
|
+
].join("\n");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function defaultOriginPullRequestDispatch(
|
|
384
|
+
ctx: Omit<OriginEventContext, "host" | "artifacts">,
|
|
385
|
+
pullRequest: OriginPullRequestEvent
|
|
386
|
+
): OriginTurnDispatch | null {
|
|
387
|
+
if (!DEFAULT_PULL_REQUEST_TYPES.has(pullRequest.type)) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
return { auth: defaultOriginAuth(ctx) };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function isTaskDispatch(
|
|
394
|
+
dispatch: OriginDispatch
|
|
395
|
+
): dispatch is OriginTaskDispatch {
|
|
396
|
+
return "task" in dispatch;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function enrichEventContext(input: {
|
|
400
|
+
ctx: Omit<OriginEventContext, "host" | "artifacts">;
|
|
401
|
+
args: ChannelHandlerArgs;
|
|
402
|
+
}): OriginEventContext {
|
|
403
|
+
return {
|
|
404
|
+
...input.ctx,
|
|
405
|
+
host: input.args.host,
|
|
406
|
+
artifacts: input.args.artifacts,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function skipped(
|
|
411
|
+
reason: string,
|
|
412
|
+
extra: Record<string, unknown> = {}
|
|
413
|
+
): Response {
|
|
414
|
+
return Response.json({ ok: true, skipped: true, reason, ...extra });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function acknowledgeTask(
|
|
418
|
+
args: ChannelHandlerArgs,
|
|
419
|
+
dispatch: OriginTaskDispatch,
|
|
420
|
+
extra: Record<string, unknown>
|
|
421
|
+
): Response {
|
|
422
|
+
args.waitUntil(
|
|
423
|
+
new Promise<void>((resolve, reject) => {
|
|
424
|
+
setImmediate(() => {
|
|
425
|
+
Promise.resolve()
|
|
426
|
+
.then(dispatch.task)
|
|
427
|
+
.then(() => resolve(), reject);
|
|
428
|
+
});
|
|
429
|
+
})
|
|
430
|
+
);
|
|
431
|
+
return Response.json(
|
|
432
|
+
{ ok: true, dispatched: true, mode: "task", ...extra },
|
|
433
|
+
{ status: 202 }
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function startTurn(input: {
|
|
438
|
+
args: ChannelHandlerArgs;
|
|
439
|
+
ctx: OriginEventContext;
|
|
440
|
+
message: string;
|
|
441
|
+
dispatch: OriginTurnDispatch;
|
|
442
|
+
cloud?: AgentCloudOptions;
|
|
443
|
+
}): Promise<Response> {
|
|
444
|
+
const session = await input.args.send(input.message, {
|
|
445
|
+
auth: input.dispatch.auth,
|
|
446
|
+
continuationToken: originContinuationKey(input.ctx),
|
|
447
|
+
title: input.dispatch.title ?? defaultOriginTitle(input.ctx),
|
|
448
|
+
...(input.cloud === undefined ? {} : { cloud: input.cloud }),
|
|
449
|
+
});
|
|
450
|
+
return Response.json({
|
|
451
|
+
ok: true,
|
|
452
|
+
sessionId: session.id,
|
|
453
|
+
isNew: session.isNew,
|
|
454
|
+
continuationToken: session.continuationToken,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function resolveCloudOptions(
|
|
459
|
+
cloud: OriginChannelOptions["cloud"],
|
|
460
|
+
ctx: OriginEventContext,
|
|
461
|
+
pullRequest: OriginPullRequestEvent
|
|
462
|
+
): Promise<AgentCloudOptions | undefined> {
|
|
463
|
+
if (cloud === undefined || cloud === false) {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
if (cloud === true) {
|
|
467
|
+
return {
|
|
468
|
+
repos: [
|
|
469
|
+
{
|
|
470
|
+
url: `https://cursor.com/codebase/${ctx.repository.fullName}`,
|
|
471
|
+
prUrl: pullRequest.url,
|
|
472
|
+
startingRef: pullRequest.headSha,
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (typeof cloud === "function") {
|
|
478
|
+
return await cloud(ctx, pullRequest);
|
|
479
|
+
}
|
|
480
|
+
return cloud;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function applyDispatch(input: {
|
|
484
|
+
args: ChannelHandlerArgs;
|
|
485
|
+
ctx: OriginEventContext;
|
|
486
|
+
dispatch: OriginDispatch | null;
|
|
487
|
+
extra: Record<string, unknown>;
|
|
488
|
+
message: string;
|
|
489
|
+
cloud?: AgentCloudOptions;
|
|
490
|
+
}): Promise<Response> {
|
|
491
|
+
if (input.dispatch === null) {
|
|
492
|
+
return skipped("not_dispatched", input.extra);
|
|
493
|
+
}
|
|
494
|
+
if (isTaskDispatch(input.dispatch)) {
|
|
495
|
+
return acknowledgeTask(input.args, input.dispatch, input.extra);
|
|
496
|
+
}
|
|
497
|
+
const message = [
|
|
498
|
+
input.dispatch.message ?? input.message,
|
|
499
|
+
...(input.dispatch.context ?? []),
|
|
500
|
+
].join("\n");
|
|
501
|
+
return startTurn({
|
|
502
|
+
args: input.args,
|
|
503
|
+
ctx: input.ctx,
|
|
504
|
+
message,
|
|
505
|
+
dispatch: input.dispatch,
|
|
506
|
+
cloud: input.cloud,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Core Origin webhook dispatch used by {@link originChannel} routes and by
|
|
512
|
+
* {@link handleAgentServeTrigger} fan-out. Signature verification is the
|
|
513
|
+
* caller's responsibility (HTTP route verifies; internal fan-out skips).
|
|
514
|
+
*/
|
|
515
|
+
export async function dispatchOriginWebhook(input: {
|
|
516
|
+
options: OriginChannelOptions;
|
|
517
|
+
envelope: OriginWebhookEnvelope;
|
|
518
|
+
args: ChannelHandlerArgs;
|
|
519
|
+
}): Promise<Response> {
|
|
520
|
+
const eventType = input.envelope.event.type;
|
|
521
|
+
const parsed = parseOriginPullRequestEvent({ envelope: input.envelope });
|
|
522
|
+
if (parsed === null) {
|
|
523
|
+
return Response.json(
|
|
524
|
+
{ ok: false, error: "invalid_origin_payload" },
|
|
525
|
+
{ status: 400 }
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
const ctx = enrichEventContext({ ctx: parsed.ctx, args: input.args });
|
|
529
|
+
const { pullRequest, payload } = parsed;
|
|
530
|
+
const repoAllowlist = configuredOriginRepoAllowlist(input.options);
|
|
531
|
+
if (
|
|
532
|
+
repoAllowlist !== undefined &&
|
|
533
|
+
!repoAllowlist.has(ctx.repository.fullName.toLowerCase())
|
|
534
|
+
) {
|
|
535
|
+
return skipped("repo_not_allowed", { repo: ctx.repository.fullName });
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (eventType.startsWith("pull_request.comment.")) {
|
|
539
|
+
if (input.options.onComment === undefined) {
|
|
540
|
+
return skipped("no_handler", { event: eventType });
|
|
541
|
+
}
|
|
542
|
+
const commentRecord = asRecord(payload.comment);
|
|
543
|
+
const commentId = asNonEmptyString(commentRecord?.id);
|
|
544
|
+
const commentBody =
|
|
545
|
+
typeof commentRecord?.body === "string" ? commentRecord.body : undefined;
|
|
546
|
+
if (commentId === undefined || commentBody === undefined) {
|
|
547
|
+
return Response.json(
|
|
548
|
+
{ ok: false, error: "invalid_origin_comment_payload" },
|
|
549
|
+
{ status: 400 }
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
const comment: OriginComment = {
|
|
553
|
+
id: commentId,
|
|
554
|
+
body: commentBody,
|
|
555
|
+
author: originActor(commentRecord?.author),
|
|
556
|
+
};
|
|
557
|
+
const dispatch = await input.options.onComment(ctx, comment);
|
|
558
|
+
return applyDispatch({
|
|
559
|
+
args: input.args,
|
|
560
|
+
ctx,
|
|
561
|
+
dispatch,
|
|
562
|
+
extra: { event: eventType },
|
|
563
|
+
message: `Origin comment on ${ctx.repository.fullName}#${ctx.pullRequestNumber}`,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (eventType.startsWith("pull_request.review.")) {
|
|
568
|
+
if (input.options.onReview === undefined) {
|
|
569
|
+
return skipped("no_handler", { event: eventType });
|
|
570
|
+
}
|
|
571
|
+
const reviewRecord = asRecord(payload.review) ?? {};
|
|
572
|
+
const review: OriginReview = {
|
|
573
|
+
id: asNonEmptyString(reviewRecord.id),
|
|
574
|
+
verdict: asNonEmptyString(reviewRecord.verdict),
|
|
575
|
+
author: originActor(reviewRecord.author),
|
|
576
|
+
};
|
|
577
|
+
const dispatch = await input.options.onReview(ctx, review);
|
|
578
|
+
return applyDispatch({
|
|
579
|
+
args: input.args,
|
|
580
|
+
ctx,
|
|
581
|
+
dispatch,
|
|
582
|
+
extra: { event: eventType },
|
|
583
|
+
message: `Origin review on ${ctx.repository.fullName}#${ctx.pullRequestNumber}`,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (eventType.startsWith("pull_request.")) {
|
|
588
|
+
const dispatch =
|
|
589
|
+
input.options.onPullRequest === undefined
|
|
590
|
+
? defaultOriginPullRequestDispatch(ctx, pullRequest)
|
|
591
|
+
: await input.options.onPullRequest(ctx, pullRequest);
|
|
592
|
+
const cloud = await resolveCloudOptions(
|
|
593
|
+
input.options.cloud,
|
|
594
|
+
ctx,
|
|
595
|
+
pullRequest
|
|
596
|
+
);
|
|
597
|
+
return applyDispatch({
|
|
598
|
+
args: input.args,
|
|
599
|
+
ctx,
|
|
600
|
+
dispatch,
|
|
601
|
+
extra: { event: eventType },
|
|
602
|
+
message: defaultOriginPullRequestMessage(ctx, pullRequest),
|
|
603
|
+
cloud,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (input.options.onEvent !== undefined) {
|
|
608
|
+
const dispatch = await input.options.onEvent(ctx, {
|
|
609
|
+
eventType,
|
|
610
|
+
payload,
|
|
611
|
+
});
|
|
612
|
+
return applyDispatch({
|
|
613
|
+
args: input.args,
|
|
614
|
+
ctx,
|
|
615
|
+
dispatch,
|
|
616
|
+
extra: { event: eventType },
|
|
617
|
+
message: `Origin ${eventType} event`,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return skipped("unsupported_event", { event: eventType });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* HTTP entry for a single agent's Origin channel: optional v1ed verify, then
|
|
626
|
+
* app-id bind, then {@link dispatchOriginWebhook}.
|
|
627
|
+
*/
|
|
628
|
+
export async function handleOriginWebhookRequest(input: {
|
|
629
|
+
options: OriginChannelOptions;
|
|
630
|
+
request: Request;
|
|
631
|
+
args: ChannelHandlerArgs;
|
|
632
|
+
/** When true, skip v1ed even though Origin HTTP deliveries are signed. */
|
|
633
|
+
skipSignatureVerification?: boolean;
|
|
634
|
+
}): Promise<Response> {
|
|
635
|
+
const bodyText = await input.request.text();
|
|
636
|
+
const requireHttpBinding =
|
|
637
|
+
input.skipSignatureVerification !== true &&
|
|
638
|
+
shouldSkipOriginVerify() !== true;
|
|
639
|
+
if (requireHttpBinding) {
|
|
640
|
+
try {
|
|
641
|
+
await verifyOriginWebhook(bodyText, originHeaders(input.request), {
|
|
642
|
+
apiBaseUrl: resolveApiBaseUrl(input.options),
|
|
643
|
+
});
|
|
644
|
+
} catch {
|
|
645
|
+
return Response.json(
|
|
646
|
+
{ ok: false, error: "invalid_signature" },
|
|
647
|
+
{ status: 401 }
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
let payload: unknown;
|
|
653
|
+
try {
|
|
654
|
+
payload = JSON.parse(bodyText) as unknown;
|
|
655
|
+
} catch {
|
|
656
|
+
return Response.json({ ok: false, error: "invalid_json" }, { status: 400 });
|
|
657
|
+
}
|
|
658
|
+
const envelope = parseOriginWebhookEnvelope(payload);
|
|
659
|
+
if (envelope === null) {
|
|
660
|
+
return Response.json(
|
|
661
|
+
{ ok: false, error: "invalid_origin_payload" },
|
|
662
|
+
{ status: 400 }
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
if (requireHttpBinding) {
|
|
666
|
+
const bindingError = originHttpBindingError(input.options, envelope);
|
|
667
|
+
if (bindingError !== undefined) {
|
|
668
|
+
return Response.json({ ok: false, error: bindingError }, { status: 401 });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return dispatchOriginWebhook({
|
|
672
|
+
options: input.options,
|
|
673
|
+
envelope,
|
|
674
|
+
args: input.args,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const originChannelOptionsByDefinition = new WeakMap<
|
|
679
|
+
object,
|
|
680
|
+
OriginChannelOptions
|
|
681
|
+
>();
|
|
682
|
+
|
|
683
|
+
export function getOriginChannelOptions(
|
|
684
|
+
definition: object
|
|
685
|
+
): OriginChannelOptions | undefined {
|
|
686
|
+
return originChannelOptionsByDefinition.get(definition);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export function originChannel(
|
|
690
|
+
options: OriginChannelOptions = {}
|
|
691
|
+
): ChannelDefinition {
|
|
692
|
+
const usesCursorAccount =
|
|
693
|
+
options.cursorAccount === true || typeof options.cursorAccount === "object";
|
|
694
|
+
const cursorScope =
|
|
695
|
+
typeof options.cursorAccount === "object"
|
|
696
|
+
? parseCursorOriginRepoScope(options.cursorAccount.repos)
|
|
697
|
+
: undefined;
|
|
698
|
+
if (options.repos !== undefined) {
|
|
699
|
+
parseCursorOriginRepoScope(options.repos);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const routePath = options.path ?? "/";
|
|
703
|
+
// v1ed + configured app id is the request auth (allowAll), matching GitHub
|
|
704
|
+
// HMAC + webhookSecret. Without an app id, prod stays loopback-strict so a
|
|
705
|
+
// reachable URL cannot be driven by any Origin-signed delivery. `--dev`
|
|
706
|
+
// / skip-verify admits loopback only.
|
|
707
|
+
const auth = options.auth ?? [
|
|
708
|
+
(request, info) =>
|
|
709
|
+
(shouldSkipOriginVerify()
|
|
710
|
+
? loopbackOnly()
|
|
711
|
+
: configuredOriginAppIds(options).length > 0
|
|
712
|
+
? allowAll()
|
|
713
|
+
: localDevStrict())(request, info),
|
|
714
|
+
];
|
|
715
|
+
|
|
716
|
+
const definition = defineChannel({
|
|
717
|
+
auth,
|
|
718
|
+
onStart: options.onStart,
|
|
719
|
+
onStop: options.onStop,
|
|
720
|
+
meta: {
|
|
721
|
+
kind: ORIGIN_CHANNEL_KIND,
|
|
722
|
+
originWebhookEvents: deriveOriginWebhookEvents(options),
|
|
723
|
+
},
|
|
724
|
+
routes: [
|
|
725
|
+
POST(routePath, {
|
|
726
|
+
bodySchema: z.unknown(),
|
|
727
|
+
description: "Origin webhook ingress",
|
|
728
|
+
handler: async (request, args) =>
|
|
729
|
+
handleOriginWebhookRequest({
|
|
730
|
+
options,
|
|
731
|
+
request,
|
|
732
|
+
args,
|
|
733
|
+
}),
|
|
734
|
+
}),
|
|
735
|
+
],
|
|
736
|
+
});
|
|
737
|
+
originChannelOptionsByDefinition.set(definition, options);
|
|
738
|
+
if (usesCursorAccount) {
|
|
739
|
+
registerOriginCursorAccountRuntime(definition, {
|
|
740
|
+
options,
|
|
741
|
+
scope: cursorScope,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
return definition;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** Minimal mount shape shared by `serve` / trigger fan-out. */
|
|
748
|
+
export interface OriginAgentMountLike {
|
|
749
|
+
slug: string;
|
|
750
|
+
project: {
|
|
751
|
+
channels: Array<{
|
|
752
|
+
id: string;
|
|
753
|
+
definition: {
|
|
754
|
+
meta?: { kind?: string; originWebhookEvents?: readonly string[] };
|
|
755
|
+
};
|
|
756
|
+
}>;
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export interface OriginChannelTarget {
|
|
761
|
+
slug: string;
|
|
762
|
+
channelId: string;
|
|
763
|
+
events: string[];
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export function findOriginChannelTargets(
|
|
767
|
+
mounts: ReadonlyArray<OriginAgentMountLike>
|
|
768
|
+
): OriginChannelTarget[] {
|
|
769
|
+
const targets: OriginChannelTarget[] = [];
|
|
770
|
+
for (const { slug, project } of mounts) {
|
|
771
|
+
for (const channel of project.channels) {
|
|
772
|
+
const meta = channel.definition.meta;
|
|
773
|
+
if (meta?.kind !== ORIGIN_CHANNEL_KIND) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
targets.push({
|
|
777
|
+
slug,
|
|
778
|
+
channelId: channel.id,
|
|
779
|
+
events: [...(meta.originWebhookEvents ?? [])],
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return targets;
|
|
784
|
+
}
|