@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,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Origin webhook verification (Standard Webhooks `v1ed`).
|
|
3
|
+
*
|
|
4
|
+
* Direct Origin HTTP deliveries must verify the Ed25519 signature
|
|
5
|
+
* (`webhook-id`, `webhook-timestamp`, `webhook-signature`) against Origin's
|
|
6
|
+
* JWKS. Cursor-events relay deliveries skip this — the platform already
|
|
7
|
+
* verified the webhook before enqueueing.
|
|
8
|
+
*
|
|
9
|
+
* The signed content matches the Standard Webhooks `v1` scheme
|
|
10
|
+
* (`${webhook-id}.${webhook-timestamp}.${body}`) but is hashed to a lowercase
|
|
11
|
+
* hex SHA-256 before signing. Lifted from
|
|
12
|
+
* `factory/agentic-owners/agent/lib/origin-webhook.ts` /
|
|
13
|
+
* `backend/server/src/origin/webhooks/asymmetricWebhookSignature.ts`.
|
|
14
|
+
*/
|
|
15
|
+
import {
|
|
16
|
+
createHash,
|
|
17
|
+
createPublicKey,
|
|
18
|
+
type JsonWebKeyInput,
|
|
19
|
+
type KeyObject,
|
|
20
|
+
verify as verifyEd25519,
|
|
21
|
+
} from "node:crypto";
|
|
22
|
+
|
|
23
|
+
export const V1ED_SIGNATURE_VERSION = "v1ed";
|
|
24
|
+
export const DEFAULT_ORIGIN_API_BASE_URL = "https://origin.cursor.com";
|
|
25
|
+
|
|
26
|
+
/** ±5 minutes, the Standard Webhooks replay-window convention. */
|
|
27
|
+
const V1ED_TIMESTAMP_TOLERANCE_SECONDS = 5 * 60;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The sender emits exactly one v1ed entry; key rotation is handled by trying
|
|
31
|
+
* multiple public keys, never multiple signatures.
|
|
32
|
+
*/
|
|
33
|
+
const MAX_V1ED_CANDIDATE_SIGNATURES = 1;
|
|
34
|
+
|
|
35
|
+
const JWKS_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
36
|
+
|
|
37
|
+
export type OriginJwk = Record<string, unknown>;
|
|
38
|
+
|
|
39
|
+
export type OriginWebhookHeaders = {
|
|
40
|
+
"webhook-id"?: string;
|
|
41
|
+
"webhook-timestamp"?: string;
|
|
42
|
+
"webhook-signature"?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function buildV1edSignedContentDigest(args: {
|
|
46
|
+
webhookId: string;
|
|
47
|
+
timestampSeconds: number;
|
|
48
|
+
body: string | Buffer;
|
|
49
|
+
}): string {
|
|
50
|
+
const prefix = Buffer.from(
|
|
51
|
+
`${args.webhookId}.${args.timestampSeconds}.`,
|
|
52
|
+
"utf8"
|
|
53
|
+
);
|
|
54
|
+
const body =
|
|
55
|
+
typeof args.body === "string" ? Buffer.from(args.body, "utf8") : args.body;
|
|
56
|
+
return createHash("sha256")
|
|
57
|
+
.update(Buffer.concat([prefix, body]))
|
|
58
|
+
.digest("hex");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Verify a delivery's Standard Webhooks headers against the Origin signing
|
|
63
|
+
* JWKS. Owns the whole check: header presence, timestamp parsing, the replay
|
|
64
|
+
* window, and Ed25519 verification against any provided key.
|
|
65
|
+
*/
|
|
66
|
+
export function verifyV1edWebhookPayload(args: {
|
|
67
|
+
publicJwks: readonly OriginJwk[];
|
|
68
|
+
body: string | Buffer;
|
|
69
|
+
headers: OriginWebhookHeaders;
|
|
70
|
+
nowMs?: number;
|
|
71
|
+
}): boolean {
|
|
72
|
+
const webhookId = args.headers["webhook-id"];
|
|
73
|
+
const webhookTimestamp = args.headers["webhook-timestamp"];
|
|
74
|
+
const signatureHeader = args.headers["webhook-signature"];
|
|
75
|
+
if (
|
|
76
|
+
webhookId === undefined ||
|
|
77
|
+
webhookTimestamp === undefined ||
|
|
78
|
+
signatureHeader === undefined
|
|
79
|
+
) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const timestampSeconds = Number.parseInt(webhookTimestamp, 10);
|
|
83
|
+
if (Number.isNaN(timestampSeconds)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const nowSeconds = Math.floor((args.nowMs ?? Date.now()) / 1000);
|
|
87
|
+
if (
|
|
88
|
+
Math.abs(nowSeconds - timestampSeconds) > V1ED_TIMESTAMP_TOLERANCE_SECONDS
|
|
89
|
+
) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const digestBytes = Buffer.from(
|
|
94
|
+
buildV1edSignedContentDigest({
|
|
95
|
+
webhookId,
|
|
96
|
+
timestampSeconds,
|
|
97
|
+
body: args.body,
|
|
98
|
+
}),
|
|
99
|
+
"utf8"
|
|
100
|
+
);
|
|
101
|
+
const candidateSignatures = signatureHeader
|
|
102
|
+
.split(/\s+/)
|
|
103
|
+
.filter((entry) => entry.startsWith(`${V1ED_SIGNATURE_VERSION},`))
|
|
104
|
+
.map((entry) => entry.slice(V1ED_SIGNATURE_VERSION.length + 1))
|
|
105
|
+
.slice(0, MAX_V1ED_CANDIDATE_SIGNATURES);
|
|
106
|
+
if (candidateSignatures.length === 0) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const jwk of args.publicJwks) {
|
|
111
|
+
let publicKey: KeyObject;
|
|
112
|
+
try {
|
|
113
|
+
publicKey = createPublicKey({
|
|
114
|
+
key: jwk,
|
|
115
|
+
format: "jwk",
|
|
116
|
+
} as JsonWebKeyInput);
|
|
117
|
+
} catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
for (const candidate of candidateSignatures) {
|
|
121
|
+
const signature = Buffer.from(candidate, "base64");
|
|
122
|
+
try {
|
|
123
|
+
if (verifyEd25519(null, digestBytes, publicKey, signature)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Malformed signature bytes or a non-Ed25519 key: try the next pair.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let jwksCache:
|
|
135
|
+
| { apiBaseUrl: string; keys: OriginJwk[]; fetchedAtMs: number }
|
|
136
|
+
| undefined;
|
|
137
|
+
|
|
138
|
+
/** Fetch (and cache for 5 minutes) the Origin signing JWKS. */
|
|
139
|
+
export async function fetchOriginJwks(
|
|
140
|
+
apiBaseUrl: string,
|
|
141
|
+
fetchImpl: typeof fetch = fetch
|
|
142
|
+
): Promise<readonly OriginJwk[]> {
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
if (
|
|
145
|
+
jwksCache !== undefined &&
|
|
146
|
+
jwksCache.apiBaseUrl === apiBaseUrl &&
|
|
147
|
+
now - jwksCache.fetchedAtMs < JWKS_CACHE_TTL_MS
|
|
148
|
+
) {
|
|
149
|
+
return jwksCache.keys;
|
|
150
|
+
}
|
|
151
|
+
const response = await fetchImpl(
|
|
152
|
+
`${apiBaseUrl.replace(/\/+$/, "")}/v1/origin/keys`
|
|
153
|
+
);
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
throw new Error(`Origin JWKS fetch failed: status=${response.status}`);
|
|
156
|
+
}
|
|
157
|
+
const body: unknown = await response.json().catch(() => undefined);
|
|
158
|
+
const keys =
|
|
159
|
+
typeof body === "object" &&
|
|
160
|
+
body !== null &&
|
|
161
|
+
Array.isArray((body as Record<string, unknown>).keys)
|
|
162
|
+
? ((body as Record<string, unknown>).keys as OriginJwk[])
|
|
163
|
+
: [];
|
|
164
|
+
if (keys.length === 0) {
|
|
165
|
+
throw new Error("Origin JWKS fetch returned no keys");
|
|
166
|
+
}
|
|
167
|
+
jwksCache = { apiBaseUrl, keys, fetchedAtMs: now };
|
|
168
|
+
return keys;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface VerifyOriginWebhookOptions {
|
|
172
|
+
apiBaseUrl?: string;
|
|
173
|
+
fetchImpl?: typeof fetch;
|
|
174
|
+
nowMs?: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Verify a Standard-Webhooks v1ed Origin payload. Throws on failure.
|
|
179
|
+
*/
|
|
180
|
+
export async function verifyOriginWebhook(
|
|
181
|
+
body: string | Buffer,
|
|
182
|
+
headers: OriginWebhookHeaders,
|
|
183
|
+
options?: VerifyOriginWebhookOptions
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
const apiBaseUrl = options?.apiBaseUrl ?? DEFAULT_ORIGIN_API_BASE_URL;
|
|
186
|
+
const keys = await fetchOriginJwks(apiBaseUrl, options?.fetchImpl ?? fetch);
|
|
187
|
+
if (
|
|
188
|
+
!verifyV1edWebhookPayload({
|
|
189
|
+
publicJwks: keys,
|
|
190
|
+
body,
|
|
191
|
+
headers,
|
|
192
|
+
nowMs: options?.nowMs,
|
|
193
|
+
})
|
|
194
|
+
) {
|
|
195
|
+
throw new Error("Origin webhook signature verification failed");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Test helper: drop the in-process JWKS cache. */
|
|
200
|
+
export function resetOriginWebhookKeysCache(): void {
|
|
201
|
+
jwksCache = undefined;
|
|
202
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentCloudOptions,
|
|
3
|
+
ArtifactsApi,
|
|
4
|
+
AuthContext,
|
|
5
|
+
AuthPolicy,
|
|
6
|
+
ChannelStartArgs,
|
|
7
|
+
HostContext,
|
|
8
|
+
} from "../../types.js";
|
|
9
|
+
|
|
10
|
+
export interface OriginActor {
|
|
11
|
+
id: string;
|
|
12
|
+
kind?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface OriginRepositoryRef {
|
|
16
|
+
owner: string;
|
|
17
|
+
name: string;
|
|
18
|
+
fullName: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface OriginPullRequestEvent {
|
|
22
|
+
type: string;
|
|
23
|
+
number: number;
|
|
24
|
+
url: string;
|
|
25
|
+
title?: string;
|
|
26
|
+
body?: string | null;
|
|
27
|
+
draft?: boolean;
|
|
28
|
+
merged?: boolean;
|
|
29
|
+
authorId?: string;
|
|
30
|
+
headRef?: string;
|
|
31
|
+
headSha?: string;
|
|
32
|
+
baseRef?: string;
|
|
33
|
+
baseSha?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OriginComment {
|
|
37
|
+
id: string;
|
|
38
|
+
body: string;
|
|
39
|
+
author?: OriginActor;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface OriginReview {
|
|
43
|
+
id?: string;
|
|
44
|
+
verdict?: string;
|
|
45
|
+
author?: OriginActor;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface OriginEventContext {
|
|
49
|
+
deliveryId?: string;
|
|
50
|
+
eventType: string;
|
|
51
|
+
repository: OriginRepositoryRef;
|
|
52
|
+
pullRequestNumber: number;
|
|
53
|
+
pullRequestUrl: string;
|
|
54
|
+
sender?: OriginActor;
|
|
55
|
+
host: HostContext;
|
|
56
|
+
artifacts: ArtifactsApi;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type OriginTurnDispatch = {
|
|
60
|
+
auth: AuthContext;
|
|
61
|
+
message?: string;
|
|
62
|
+
title?: string;
|
|
63
|
+
context?: readonly string[];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type OriginTaskDispatch = { task: () => Promise<void> };
|
|
67
|
+
|
|
68
|
+
export type OriginDispatch = OriginTurnDispatch | OriginTaskDispatch;
|
|
69
|
+
|
|
70
|
+
export type OriginDispatchResult =
|
|
71
|
+
| OriginDispatch
|
|
72
|
+
| null
|
|
73
|
+
| Promise<OriginDispatch | null>;
|
|
74
|
+
|
|
75
|
+
export type OriginPullRequestHook = (
|
|
76
|
+
ctx: OriginEventContext,
|
|
77
|
+
pullRequest: OriginPullRequestEvent
|
|
78
|
+
) => OriginDispatchResult;
|
|
79
|
+
|
|
80
|
+
export type OriginCommentHook = (
|
|
81
|
+
ctx: OriginEventContext,
|
|
82
|
+
comment: OriginComment
|
|
83
|
+
) => OriginDispatchResult;
|
|
84
|
+
|
|
85
|
+
export type OriginReviewHook = (
|
|
86
|
+
ctx: OriginEventContext,
|
|
87
|
+
review: OriginReview
|
|
88
|
+
) => OriginDispatchResult;
|
|
89
|
+
|
|
90
|
+
export type OriginEventHook = (
|
|
91
|
+
ctx: OriginEventContext,
|
|
92
|
+
event: { eventType: string; payload: unknown }
|
|
93
|
+
) => OriginDispatchResult;
|
|
94
|
+
|
|
95
|
+
export type OriginCloudOptionsResolver = (
|
|
96
|
+
ctx: OriginEventContext,
|
|
97
|
+
pullRequest: OriginPullRequestEvent
|
|
98
|
+
) => AgentCloudOptions | undefined | Promise<AgentCloudOptions | undefined>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Origin delivery envelope. Public payloads are proto3 camelCase
|
|
102
|
+
* (`pullRequest`, `repository.owner.slug`).
|
|
103
|
+
*/
|
|
104
|
+
export interface OriginWebhookEnvelope {
|
|
105
|
+
deliveryId?: string;
|
|
106
|
+
appId?: string;
|
|
107
|
+
installationId?: string;
|
|
108
|
+
sender?: OriginActor;
|
|
109
|
+
event: {
|
|
110
|
+
id?: string;
|
|
111
|
+
type: string;
|
|
112
|
+
eventTime?: string;
|
|
113
|
+
payload?: unknown;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface OriginChannelOptions {
|
|
118
|
+
/**
|
|
119
|
+
* Use the signed-in Cursor account for inbound Origin events via
|
|
120
|
+
* `/v0/scm-events` (`provider: "origin"`). `true` takes repository scope
|
|
121
|
+
* from `serve --cursor-events --repo`; `{ repos }` makes the channel
|
|
122
|
+
* self-contained. Origin identity is the Cursor user — there is no
|
|
123
|
+
* separate OAuth principal, and this does not mint a GitHub token.
|
|
124
|
+
*/
|
|
125
|
+
cursorAccount?: boolean | { repos: readonly string[] };
|
|
126
|
+
/**
|
|
127
|
+
* Origin app id this channel accepts (`envelope.appId`). v1ed only proves
|
|
128
|
+
* Origin signed the body with the platform JWKS; it does not bind the
|
|
129
|
+
* delivery to one app (any Origin app can aim signed POSTs at a public
|
|
130
|
+
* URL). Required for non-loopback HTTP, same role as GitHub's per-channel
|
|
131
|
+
* webhook secret. Falls back to `AGENT_SERVE_ORIGIN_APP_ID`.
|
|
132
|
+
*/
|
|
133
|
+
appId?: string | readonly string[];
|
|
134
|
+
/**
|
|
135
|
+
* Optional Origin installation id bind (`envelope.installationId`). When
|
|
136
|
+
* set, HTTP deliveries for any other installation are rejected.
|
|
137
|
+
*/
|
|
138
|
+
installationId?: string | readonly string[];
|
|
139
|
+
/**
|
|
140
|
+
* Optional HTTP/dispatch repo allowlist (`owner/name`). `{ cursorAccount:
|
|
141
|
+
* { repos } }` is also an allowlist. Events outside the set are acked
|
|
142
|
+
* without starting a turn. Omit to accept every repo the bound app sends.
|
|
143
|
+
*/
|
|
144
|
+
repos?: readonly string[];
|
|
145
|
+
/**
|
|
146
|
+
* Route auth chain. Defaults to allow-all when {@link appId} (or
|
|
147
|
+
* `AGENT_SERVE_ORIGIN_APP_ID`) is set, because v1ed plus that app-id bind
|
|
148
|
+
* is the request auth. Without an app id, production stays loopback-strict
|
|
149
|
+
* (GitHub's no-secret posture); `--dev` / skip-verify admits loopback only.
|
|
150
|
+
*/
|
|
151
|
+
auth?: AuthPolicy[];
|
|
152
|
+
/** Route path under `/v1/channels/<name>`. Defaults to `/`. */
|
|
153
|
+
path?: string;
|
|
154
|
+
/** Origin API base used to fetch `/v1/origin/keys` for v1ed verify. */
|
|
155
|
+
apiBaseUrl?: string | (() => string | undefined);
|
|
156
|
+
/**
|
|
157
|
+
* Pin the Origin event types this channel dispatches on (published as
|
|
158
|
+
* `meta.originWebhookEvents`). Defaults to the set derived from hooks.
|
|
159
|
+
*/
|
|
160
|
+
webhookEvents?: readonly string[];
|
|
161
|
+
/**
|
|
162
|
+
* Decide whether an inbound pull-request event starts a turn. Return
|
|
163
|
+
* `{ auth: defaultOriginAuth(ctx) }` to dispatch, `null` to ignore.
|
|
164
|
+
* Without this hook the channel dispatches on created / published /
|
|
165
|
+
* head_ref.pushed / base_ref.updated.
|
|
166
|
+
*/
|
|
167
|
+
onPullRequest?: OriginPullRequestHook;
|
|
168
|
+
/** Opt-in `pull_request.comment.*` handler. No default dispatch. */
|
|
169
|
+
onComment?: OriginCommentHook;
|
|
170
|
+
/** Opt-in `pull_request.review.*` handler. No default dispatch. */
|
|
171
|
+
onReview?: OriginReviewHook;
|
|
172
|
+
/**
|
|
173
|
+
* Catch-all for Origin event types without a typed hook. NOT a fallback:
|
|
174
|
+
* events with a typed hook never reach this one.
|
|
175
|
+
*/
|
|
176
|
+
onEvent?: OriginEventHook;
|
|
177
|
+
/**
|
|
178
|
+
* Per-session cloud options. Pass `true` to attach the PR repo using the
|
|
179
|
+
* pull request URL, or a resolver for custom cloud options.
|
|
180
|
+
*/
|
|
181
|
+
cloud?: boolean | AgentCloudOptions | OriginCloudOptionsResolver;
|
|
182
|
+
/** Called once when the server mounts this channel. */
|
|
183
|
+
onStart?: (args: ChannelStartArgs) => void | Promise<void>;
|
|
184
|
+
/** Called when the server shuts down. */
|
|
185
|
+
onStop?: () => void | Promise<void>;
|
|
186
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Slack channel doctor — verify env, Socket Mode app token, auth.test
|
|
2
|
+
* Slack channel doctor — verify env, Socket Mode app token, auth.test, and
|
|
3
|
+
* (when watched channels are supplied) that the bot token's workspace can
|
|
4
|
+
* actually see, join, and read those channels.
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
import { callSlackApi, resolveAppToken, resolveBotToken } from "./api.js";
|
|
@@ -16,12 +18,163 @@ export interface SlackDoctorResult {
|
|
|
16
18
|
ok: boolean;
|
|
17
19
|
checks: SlackDoctorCheck[];
|
|
18
20
|
envKeys: { bot: string; app: string };
|
|
21
|
+
/**
|
|
22
|
+
* Things a bot token cannot prove. Event subscriptions are app-level
|
|
23
|
+
* config, so a fully green doctor can still be a silent agent.
|
|
24
|
+
*/
|
|
25
|
+
notes?: string[];
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
export interface SlackDoctorOptions {
|
|
22
29
|
credentials?: SlackCredentials;
|
|
23
30
|
/** Per-agent env prefix (e.g. `MY_AGENT`). */
|
|
24
31
|
envPrefix?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Watched channel ids (`engagement.channelPosts.allow`). Each is checked
|
|
34
|
+
* for visibility, bot membership, and history scope.
|
|
35
|
+
*/
|
|
36
|
+
channels?: readonly string[];
|
|
37
|
+
/** Test seams. */
|
|
38
|
+
callImpl?: typeof callSlackApi;
|
|
39
|
+
fetchImpl?: typeof fetch;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Socket Mode only delivers bot events the app subscribes to. Missing
|
|
44
|
+
* subscriptions produce total silence with valid tokens, and cannot be read
|
|
45
|
+
* back from a bot token.
|
|
46
|
+
*/
|
|
47
|
+
const REQUIRED_BOT_EVENTS: Readonly<Record<string, string>> = {
|
|
48
|
+
app_mention: "@mentions of the bot",
|
|
49
|
+
"message.im": "direct messages",
|
|
50
|
+
"message.channels": "public channel posts",
|
|
51
|
+
"message.groups": "private channel posts",
|
|
52
|
+
reaction_added: "reaction handlers (onReactionAdded)",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function eventSubscriptionNote(): string {
|
|
56
|
+
const lines = Object.entries(REQUIRED_BOT_EVENTS).map(
|
|
57
|
+
([event, purpose]) => ` ${event} — ${purpose}`
|
|
58
|
+
);
|
|
59
|
+
return [
|
|
60
|
+
"A bot token cannot read Event Subscriptions. If every check above passes",
|
|
61
|
+
"but the bot is silent, verify these bot events are subscribed on the app",
|
|
62
|
+
"(Slack app config -> Event Subscriptions -> Subscribe to bot events):",
|
|
63
|
+
...lines,
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
68
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return value as Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function errorOf(response: Record<string, unknown>): string {
|
|
75
|
+
return typeof response.error === "string" ? response.error : "unknown";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Visibility, membership, and history-scope checks for one watched channel.
|
|
80
|
+
* `channel_not_found` here is the classic wrong-workspace bot token: the
|
|
81
|
+
* token authenticates fine, it just belongs to a different installation.
|
|
82
|
+
*/
|
|
83
|
+
async function checkChannel(args: {
|
|
84
|
+
call: typeof callSlackApi;
|
|
85
|
+
botToken: string;
|
|
86
|
+
channelId: string;
|
|
87
|
+
}): Promise<SlackDoctorCheck[]> {
|
|
88
|
+
const { call, botToken, channelId } = args;
|
|
89
|
+
const checks: SlackDoctorCheck[] = [];
|
|
90
|
+
|
|
91
|
+
let info: Record<string, unknown>;
|
|
92
|
+
try {
|
|
93
|
+
info = await call({
|
|
94
|
+
botToken,
|
|
95
|
+
operation: "conversations.info",
|
|
96
|
+
body: { channel: channelId },
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
checks.push({
|
|
100
|
+
id: `channel_visible:${channelId}`,
|
|
101
|
+
ok: false,
|
|
102
|
+
message: `conversations.info threw: ${
|
|
103
|
+
error instanceof Error ? error.message : String(error)
|
|
104
|
+
}`,
|
|
105
|
+
});
|
|
106
|
+
return checks;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (info.ok !== true) {
|
|
110
|
+
const error = errorOf(info);
|
|
111
|
+
checks.push({
|
|
112
|
+
id: `channel_visible:${channelId}`,
|
|
113
|
+
ok: false,
|
|
114
|
+
message:
|
|
115
|
+
error === "channel_not_found"
|
|
116
|
+
? `${channelId} not found by this bot token — the token is probably installed on a different workspace, or the channel id is wrong`
|
|
117
|
+
: `conversations.info failed: ${error}`,
|
|
118
|
+
});
|
|
119
|
+
return checks;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const channel = asRecord(info.channel);
|
|
123
|
+
const name = typeof channel?.name === "string" ? channel.name : "?";
|
|
124
|
+
checks.push({
|
|
125
|
+
id: `channel_visible:${channelId}`,
|
|
126
|
+
ok: true,
|
|
127
|
+
message: `${channelId} visible (#${name})`,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const isMember = channel?.is_member === true;
|
|
131
|
+
checks.push({
|
|
132
|
+
id: `channel_member:${channelId}`,
|
|
133
|
+
ok: isMember,
|
|
134
|
+
message: isMember
|
|
135
|
+
? `bot is a member of ${channelId}`
|
|
136
|
+
: `bot is NOT a member of ${channelId} — invite it to the channel`,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
let history: Record<string, unknown>;
|
|
140
|
+
try {
|
|
141
|
+
history = await call({
|
|
142
|
+
botToken,
|
|
143
|
+
operation: "conversations.history",
|
|
144
|
+
body: { channel: channelId, limit: 1 },
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
checks.push({
|
|
148
|
+
id: `channel_history:${channelId}`,
|
|
149
|
+
ok: false,
|
|
150
|
+
message: `conversations.history threw: ${
|
|
151
|
+
error instanceof Error ? error.message : String(error)
|
|
152
|
+
}`,
|
|
153
|
+
});
|
|
154
|
+
return checks;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (history.ok === true) {
|
|
158
|
+
checks.push({
|
|
159
|
+
id: `channel_history:${channelId}`,
|
|
160
|
+
ok: true,
|
|
161
|
+
message: `conversations.history ok for ${channelId}`,
|
|
162
|
+
});
|
|
163
|
+
return checks;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const error = errorOf(history);
|
|
167
|
+
const needed =
|
|
168
|
+
typeof history.needed === "string" ? history.needed : undefined;
|
|
169
|
+
checks.push({
|
|
170
|
+
id: `channel_history:${channelId}`,
|
|
171
|
+
ok: false,
|
|
172
|
+
message:
|
|
173
|
+
error === "missing_scope"
|
|
174
|
+
? `conversations.history missing scope${needed === undefined ? "" : ` ${needed}`} — add it and reinstall the app`
|
|
175
|
+
: `conversations.history failed: ${error}`,
|
|
176
|
+
});
|
|
177
|
+
return checks;
|
|
25
178
|
}
|
|
26
179
|
|
|
27
180
|
export async function runSlackDoctor(
|
|
@@ -31,6 +184,8 @@ export async function runSlackDoctor(
|
|
|
31
184
|
options.credentials ?? credentialsFromEnv(options.envPrefix);
|
|
32
185
|
const envKeys = slackEnvKeys(options.envPrefix);
|
|
33
186
|
const checks: SlackDoctorCheck[] = [];
|
|
187
|
+
const call = options.callImpl ?? callSlackApi;
|
|
188
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
34
189
|
|
|
35
190
|
let appTokenRaw: string | undefined;
|
|
36
191
|
try {
|
|
@@ -48,7 +203,7 @@ export async function runSlackDoctor(
|
|
|
48
203
|
});
|
|
49
204
|
if (hasApp && appTokenRaw !== undefined) {
|
|
50
205
|
try {
|
|
51
|
-
const response = await
|
|
206
|
+
const response = await fetchImpl(
|
|
52
207
|
"https://slack.com/api/apps.connections.open",
|
|
53
208
|
{
|
|
54
209
|
method: "POST",
|
|
@@ -100,18 +255,25 @@ export async function runSlackDoctor(
|
|
|
100
255
|
message: hasToken ? `${envKeys.bot} present` : `${envKeys.bot} is missing`,
|
|
101
256
|
});
|
|
102
257
|
|
|
258
|
+
let authOk = false;
|
|
259
|
+
let botUserId = "";
|
|
103
260
|
if (hasToken) {
|
|
104
261
|
try {
|
|
105
|
-
const auth = await
|
|
262
|
+
const auth = await call({
|
|
106
263
|
botToken,
|
|
107
264
|
operation: "auth.test",
|
|
108
265
|
body: {},
|
|
109
266
|
});
|
|
110
267
|
if (auth.ok === true) {
|
|
268
|
+
authOk = true;
|
|
269
|
+
botUserId = typeof auth.user_id === "string" ? auth.user_id : "";
|
|
111
270
|
checks.push({
|
|
112
271
|
id: "auth_test",
|
|
113
272
|
ok: true,
|
|
114
|
-
message:
|
|
273
|
+
message:
|
|
274
|
+
`auth.test ok (team=${String(auth.team ?? "?")}` +
|
|
275
|
+
`, team_id=${String(auth.team_id ?? "?")}` +
|
|
276
|
+
`, user=${String(auth.user ?? "?")})`,
|
|
115
277
|
});
|
|
116
278
|
} else {
|
|
117
279
|
checks.push({
|
|
@@ -131,5 +293,56 @@ export async function runSlackDoctor(
|
|
|
131
293
|
}
|
|
132
294
|
}
|
|
133
295
|
|
|
134
|
-
|
|
296
|
+
// The external-sender policy (`blockExternals`, on by default) classifies a
|
|
297
|
+
// sender as external whenever `users.info` fails — so a token that cannot
|
|
298
|
+
// look up users silently drops EVERY inbound event while all other checks
|
|
299
|
+
// stay green. Probe with the bot's own user id from auth.test.
|
|
300
|
+
if (authOk && botToken !== undefined && botUserId !== "") {
|
|
301
|
+
let info: Record<string, unknown> | undefined;
|
|
302
|
+
let threw: string | undefined;
|
|
303
|
+
try {
|
|
304
|
+
info = await call({
|
|
305
|
+
botToken,
|
|
306
|
+
operation: "users.info",
|
|
307
|
+
body: { user: botUserId },
|
|
308
|
+
});
|
|
309
|
+
} catch (error) {
|
|
310
|
+
threw = error instanceof Error ? error.message : String(error);
|
|
311
|
+
}
|
|
312
|
+
if (info?.ok === true) {
|
|
313
|
+
checks.push({
|
|
314
|
+
id: "users_read",
|
|
315
|
+
ok: true,
|
|
316
|
+
message:
|
|
317
|
+
"users.info ok (users:read granted; external-sender policy can classify senders)",
|
|
318
|
+
});
|
|
319
|
+
} else {
|
|
320
|
+
const error = threw ?? (info === undefined ? "unknown" : errorOf(info));
|
|
321
|
+
const hint =
|
|
322
|
+
error === "missing_scope"
|
|
323
|
+
? "add the users:read scope and reinstall the app"
|
|
324
|
+
: error === "user_not_found"
|
|
325
|
+
? "the token cannot see its own workspace's users — probably a token from a different installation"
|
|
326
|
+
: "fix the token";
|
|
327
|
+
checks.push({
|
|
328
|
+
id: "users_read",
|
|
329
|
+
ok: false,
|
|
330
|
+
message: `users.info failed: ${error} — ${hint}. The external-sender policy fails open to "external" on users.info errors, so every inbound event is silently dropped.`,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const channels = options.channels ?? [];
|
|
336
|
+
if (authOk && botToken !== undefined && channels.length > 0) {
|
|
337
|
+
for (const channelId of channels) {
|
|
338
|
+
checks.push(...(await checkChannel({ call, botToken, channelId })));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
ok: checks.every((check) => check.ok),
|
|
344
|
+
checks,
|
|
345
|
+
envKeys,
|
|
346
|
+
notes: [eventSubscriptionNote()],
|
|
347
|
+
};
|
|
135
348
|
}
|
package/src/index.ts
CHANGED
|
@@ -65,12 +65,15 @@ export {
|
|
|
65
65
|
export type {
|
|
66
66
|
AgentServeGitHubTriggerEvent,
|
|
67
67
|
AgentServeGitHubTriggerTarget,
|
|
68
|
+
AgentServeOriginTriggerEvent,
|
|
69
|
+
AgentServeOriginTriggerTarget,
|
|
68
70
|
AgentServeTriggerEvent,
|
|
69
71
|
AgentServeTriggerResult,
|
|
70
72
|
AgentServeTriggerTargetResult,
|
|
71
73
|
} from "./internal/handleAgentServeTrigger.js";
|
|
72
74
|
export {
|
|
73
75
|
collectGitHubTriggerTargets,
|
|
76
|
+
collectOriginTriggerTargets,
|
|
74
77
|
handleAgentServeTrigger,
|
|
75
78
|
} from "./internal/handleAgentServeTrigger.js";
|
|
76
79
|
export {
|
|
@@ -94,7 +97,6 @@ export {
|
|
|
94
97
|
} from "./internal/session-engine.js";
|
|
95
98
|
export { createMemoryKv, memoryCasTable } from "./kv/memory-kv.js";
|
|
96
99
|
export {
|
|
97
|
-
createLedgerJanitor,
|
|
98
100
|
LedgerCasExhaustedError,
|
|
99
101
|
LedgerFencedError,
|
|
100
102
|
SessionLedger,
|