@open-everywhere/provider-slack 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +47 -0
  4. package/agent/access/slack-attachment-fetch.ts +168 -0
  5. package/agent/app-manifest.ts +149 -0
  6. package/agent/channels/slack-approval-card.ts +275 -0
  7. package/agent/channels/slack-attachments.ts +195 -0
  8. package/agent/channels/slack-commands.ts +227 -0
  9. package/agent/channels/slack-context.ts +185 -0
  10. package/agent/channels/slack-events.ts +5406 -0
  11. package/agent/channels/slack-front.ts +2349 -0
  12. package/agent/channels/slack-identity.ts +520 -0
  13. package/agent/channels/slack-input-response.ts +162 -0
  14. package/agent/channels/slack-mrkdwn.ts +61 -0
  15. package/agent/channels/slack-origin-codec.ts +62 -0
  16. package/agent/channels/slack-post-arm.ts +314 -0
  17. package/agent/channels/slack-read.ts +248 -0
  18. package/agent/channels/slack-route-hold.ts +107 -0
  19. package/agent/channels/slack-shortcut.ts +336 -0
  20. package/agent/channels/slack-signature.ts +53 -0
  21. package/agent/channels/slack-spend-gate.ts +62 -0
  22. package/agent/channels/slack-tenant-fence.ts +99 -0
  23. package/agent/channels/slack-wake-backscroll.ts +604 -0
  24. package/agent/channels/slack-web-api.ts +178 -0
  25. package/agent/channels/slack-webhook-verifier.ts +116 -0
  26. package/agent/channels/slack.ts +1266 -0
  27. package/agent/contributions.ts +32 -0
  28. package/agent/docs-generated.ts +15 -0
  29. package/agent/ops.ts +50 -0
  30. package/agent/provider-events.ts +91 -0
  31. package/agent/register.ts +144 -0
  32. package/agent/surface-parity.ts +99 -0
  33. package/dist/agent/access/slack-attachment-fetch.d.ts +15 -0
  34. package/dist/agent/access/slack-attachment-fetch.d.ts.map +1 -0
  35. package/dist/agent/access/slack-attachment-fetch.js +151 -0
  36. package/dist/agent/access/slack-attachment-fetch.js.map +1 -0
  37. package/dist/agent/app-manifest.d.ts +15 -0
  38. package/dist/agent/app-manifest.d.ts.map +1 -0
  39. package/dist/agent/app-manifest.js +141 -0
  40. package/dist/agent/app-manifest.js.map +1 -0
  41. package/dist/agent/channels/slack-approval-card.d.ts +71 -0
  42. package/dist/agent/channels/slack-approval-card.d.ts.map +1 -0
  43. package/dist/agent/channels/slack-approval-card.js +232 -0
  44. package/dist/agent/channels/slack-approval-card.js.map +1 -0
  45. package/dist/agent/channels/slack-attachments.d.ts +66 -0
  46. package/dist/agent/channels/slack-attachments.d.ts.map +1 -0
  47. package/dist/agent/channels/slack-attachments.js +174 -0
  48. package/dist/agent/channels/slack-attachments.js.map +1 -0
  49. package/dist/agent/channels/slack-commands.d.ts +27 -0
  50. package/dist/agent/channels/slack-commands.d.ts.map +1 -0
  51. package/dist/agent/channels/slack-commands.js +208 -0
  52. package/dist/agent/channels/slack-commands.js.map +1 -0
  53. package/dist/agent/channels/slack-context.d.ts +26 -0
  54. package/dist/agent/channels/slack-context.d.ts.map +1 -0
  55. package/dist/agent/channels/slack-context.js +178 -0
  56. package/dist/agent/channels/slack-context.js.map +1 -0
  57. package/dist/agent/channels/slack-events.d.ts +1654 -0
  58. package/dist/agent/channels/slack-events.d.ts.map +1 -0
  59. package/dist/agent/channels/slack-events.js +4240 -0
  60. package/dist/agent/channels/slack-events.js.map +1 -0
  61. package/dist/agent/channels/slack-front.d.ts +387 -0
  62. package/dist/agent/channels/slack-front.d.ts.map +1 -0
  63. package/dist/agent/channels/slack-front.js +1958 -0
  64. package/dist/agent/channels/slack-front.js.map +1 -0
  65. package/dist/agent/channels/slack-identity.d.ts +245 -0
  66. package/dist/agent/channels/slack-identity.d.ts.map +1 -0
  67. package/dist/agent/channels/slack-identity.js +405 -0
  68. package/dist/agent/channels/slack-identity.js.map +1 -0
  69. package/dist/agent/channels/slack-input-response.d.ts +26 -0
  70. package/dist/agent/channels/slack-input-response.d.ts.map +1 -0
  71. package/dist/agent/channels/slack-input-response.js +127 -0
  72. package/dist/agent/channels/slack-input-response.js.map +1 -0
  73. package/dist/agent/channels/slack-mrkdwn.d.ts +3 -0
  74. package/dist/agent/channels/slack-mrkdwn.d.ts.map +1 -0
  75. package/dist/agent/channels/slack-mrkdwn.js +53 -0
  76. package/dist/agent/channels/slack-mrkdwn.js.map +1 -0
  77. package/dist/agent/channels/slack-origin-codec.d.ts +7 -0
  78. package/dist/agent/channels/slack-origin-codec.d.ts.map +1 -0
  79. package/dist/agent/channels/slack-origin-codec.js +55 -0
  80. package/dist/agent/channels/slack-origin-codec.js.map +1 -0
  81. package/dist/agent/channels/slack-post-arm.d.ts +54 -0
  82. package/dist/agent/channels/slack-post-arm.d.ts.map +1 -0
  83. package/dist/agent/channels/slack-post-arm.js +268 -0
  84. package/dist/agent/channels/slack-post-arm.js.map +1 -0
  85. package/dist/agent/channels/slack-read.d.ts +84 -0
  86. package/dist/agent/channels/slack-read.d.ts.map +1 -0
  87. package/dist/agent/channels/slack-read.js +205 -0
  88. package/dist/agent/channels/slack-read.js.map +1 -0
  89. package/dist/agent/channels/slack-route-hold.d.ts +34 -0
  90. package/dist/agent/channels/slack-route-hold.d.ts.map +1 -0
  91. package/dist/agent/channels/slack-route-hold.js +97 -0
  92. package/dist/agent/channels/slack-route-hold.js.map +1 -0
  93. package/dist/agent/channels/slack-shortcut.d.ts +162 -0
  94. package/dist/agent/channels/slack-shortcut.d.ts.map +1 -0
  95. package/dist/agent/channels/slack-shortcut.js +262 -0
  96. package/dist/agent/channels/slack-shortcut.js.map +1 -0
  97. package/dist/agent/channels/slack-signature.d.ts +19 -0
  98. package/dist/agent/channels/slack-signature.d.ts.map +1 -0
  99. package/dist/agent/channels/slack-signature.js +50 -0
  100. package/dist/agent/channels/slack-signature.js.map +1 -0
  101. package/dist/agent/channels/slack-spend-gate.d.ts +22 -0
  102. package/dist/agent/channels/slack-spend-gate.d.ts.map +1 -0
  103. package/dist/agent/channels/slack-spend-gate.js +48 -0
  104. package/dist/agent/channels/slack-spend-gate.js.map +1 -0
  105. package/dist/agent/channels/slack-tenant-fence.d.ts +61 -0
  106. package/dist/agent/channels/slack-tenant-fence.d.ts.map +1 -0
  107. package/dist/agent/channels/slack-tenant-fence.js +88 -0
  108. package/dist/agent/channels/slack-tenant-fence.js.map +1 -0
  109. package/dist/agent/channels/slack-wake-backscroll.d.ts +188 -0
  110. package/dist/agent/channels/slack-wake-backscroll.d.ts.map +1 -0
  111. package/dist/agent/channels/slack-wake-backscroll.js +473 -0
  112. package/dist/agent/channels/slack-wake-backscroll.js.map +1 -0
  113. package/dist/agent/channels/slack-web-api.d.ts +46 -0
  114. package/dist/agent/channels/slack-web-api.d.ts.map +1 -0
  115. package/dist/agent/channels/slack-web-api.js +147 -0
  116. package/dist/agent/channels/slack-web-api.js.map +1 -0
  117. package/dist/agent/channels/slack-webhook-verifier.d.ts +5 -0
  118. package/dist/agent/channels/slack-webhook-verifier.d.ts.map +1 -0
  119. package/dist/agent/channels/slack-webhook-verifier.js +112 -0
  120. package/dist/agent/channels/slack-webhook-verifier.js.map +1 -0
  121. package/dist/agent/channels/slack.d.ts +37 -0
  122. package/dist/agent/channels/slack.d.ts.map +1 -0
  123. package/dist/agent/channels/slack.js +1169 -0
  124. package/dist/agent/channels/slack.js.map +1 -0
  125. package/dist/agent/contributions.d.ts +17 -0
  126. package/dist/agent/contributions.d.ts.map +1 -0
  127. package/dist/agent/contributions.js +32 -0
  128. package/dist/agent/contributions.js.map +1 -0
  129. package/dist/agent/docs-generated.d.ts +3 -0
  130. package/dist/agent/docs-generated.d.ts.map +1 -0
  131. package/dist/agent/docs-generated.js +13 -0
  132. package/dist/agent/docs-generated.js.map +1 -0
  133. package/dist/agent/ops.d.ts +3 -0
  134. package/dist/agent/ops.d.ts.map +1 -0
  135. package/dist/agent/ops.js +38 -0
  136. package/dist/agent/ops.js.map +1 -0
  137. package/dist/agent/provider-events.d.ts +3 -0
  138. package/dist/agent/provider-events.d.ts.map +1 -0
  139. package/dist/agent/provider-events.js +84 -0
  140. package/dist/agent/provider-events.js.map +1 -0
  141. package/dist/agent/register.d.ts +5 -0
  142. package/dist/agent/register.d.ts.map +1 -0
  143. package/dist/agent/register.js +118 -0
  144. package/dist/agent/register.js.map +1 -0
  145. package/dist/agent/surface-parity.d.ts +3 -0
  146. package/dist/agent/surface-parity.d.ts.map +1 -0
  147. package/dist/agent/surface-parity.js +95 -0
  148. package/dist/agent/surface-parity.js.map +1 -0
  149. package/package.json +209 -0
  150. package/tsconfig.build.json +22 -0
  151. package/tsconfig.json +15 -0
@@ -0,0 +1,147 @@
1
+ // slack-web-api — THE ONE place a Slack Web API call leaves this host (OPE-1211).
2
+ //
3
+ // WHY THIS MODULE REPLACED eve's `callSlackApi`. Every Slack write used to funnel into
4
+ // `callSlackApi` from `eve/channels/slack`. That primitive is a bare `fetch` POST: it reads no
5
+ // status but the 2xx/non-2xx split, and on a non-2xx it throws a `SlackApiError` carrying
6
+ // `{ method, response, status }` — and NOT the response headers. Slack states a rate-limit delay in
7
+ // the `Retry-After` HEADER and nowhere else, so that delay was unreadable from OE code. Honouring it
8
+ // therefore needs OE to own the request. This module is a faithful port of eve's encoding, token
9
+ // resolution and error posture, with the 429 backoff added around the call.
10
+ //
11
+ // 🔴 WHAT THIS SEAM COVERS, EXACTLY. Every Slack Web API call this repo issues DIRECTLY goes through
12
+ // here: `slackForm`, `callSlackRead`, the `auth.test` identity probes and the wake backscroll. It
13
+ // does NOT cover the calls that ride eve's session BINDING, because those never reach OE code:
14
+ // • `ctx.slack.request(...)` — slack-context.ts (`auth.test`, `conversations.history`);
15
+ // • `ctx.thread.postEphemeral(...)` — slack-input-response.ts;
16
+ // • eve's own machinery — `startTyping`, thread `refresh`, `uploadSlackFiles`.
17
+ // Those resolve through eve's `createSlackRequester`/`postSlackEphemeral` to the same bare
18
+ // `callSlackApi`, so they still fail on the first 429. `slack-web-api-seam-test.ts` holds that list
19
+ // as a KNOWN-UNCOVERED inventory and fails when a NEW bypass appears, which is the honest guarantee:
20
+ // the seam is not universal, but the set of things outside it cannot grow unnoticed.
21
+ //
22
+ // SEMANTICS PORTED VERBATIM from eve's primitive, because callers depend on each one:
23
+ // • Form encoding. Slack's JSON support is partial (`conversations.replies`/`history` want form),
24
+ // so form is the only safe default. A null or undefined value is dropped; a string, number or
25
+ // boolean is stringified; anything else is JSON-encoded.
26
+ // • Token resolution. An explicit token wins, otherwise `SLACK_BOT_TOKEN`. Neither ⇒ throw
27
+ // `SLACK_BOT_TOKEN is required.` — the same message and the same fail-loud posture. ONE
28
+ // DELIBERATE NARROWING: eve's `SlackBotToken` also accepts a function (the secret-manager and
29
+ // rotation form). No caller in this repo passes one, so this seam takes a string. Widening it
30
+ // back is a one-line `await` on the resolver if the multi-bot seam ever needs it.
31
+ // • `ok:false` PASSES THROUGH. Slack answers HTTP 200 with `ok:false` when it read the request and
32
+ // refused it. That is a successful transport, and the callers classify it themselves
33
+ // (slack-read.ts marks it not-applied; slack-post-arm.ts reads it as definitive). This module
34
+ // must never turn it into a throw.
35
+ // • A non-2xx THROWS {@link SlackWebApiError}. A request that got no usable reply stays ambiguous,
36
+ // which is what the post spine needs to avoid double-posting.
37
+ //
38
+ // WHAT IS NEW: a 429 is retried after the delay `Retry-After` names, bounded by
39
+ // `channels/rate-limit-retry.js`. When the attempts run out the call throws `RateLimitExhaustedError`
40
+ // — a NAMED failure. Non-429 responses are untouched.
41
+ import { withRetryAfter } from "@open-everywhere/core/agent/adapter-api.js";
42
+ /** Slack's Web API base. A trailing slash is required — `new URL(operation, base)` resolves the
43
+ * operation against the LAST path segment otherwise. */
44
+ const SLACK_API_BASE = "https://slack.com/api/";
45
+ /** The failure a non-2xx Slack response produces. Mirrors eve's `SlackApiError` field-for-field, so
46
+ * a caller that reads `status` or `response` off a caught error keeps reading the same thing. */
47
+ export class SlackWebApiError extends Error {
48
+ method;
49
+ status;
50
+ response;
51
+ constructor(init) {
52
+ super(`Slack ${init.method} returned HTTP ${init.status}`);
53
+ this.name = "SlackWebApiError";
54
+ this.method = init.method;
55
+ this.status = init.status;
56
+ this.response = init.response;
57
+ }
58
+ }
59
+ /** Form-encode a Slack request body exactly as eve's `encodeSlackApiBody` does. */
60
+ export function encodeSlackForm(body) {
61
+ const params = new URLSearchParams();
62
+ if (body !== null && typeof body === "object" && !Array.isArray(body)) {
63
+ for (const [key, value] of Object.entries(body)) {
64
+ if (value === null || value === undefined)
65
+ continue;
66
+ const scalar = typeof value === "string" || typeof value === "number" || typeof value === "boolean";
67
+ params.set(key, scalar ? String(value) : JSON.stringify(value));
68
+ }
69
+ }
70
+ return params.toString();
71
+ }
72
+ /**
73
+ * Operations that are BEST-EFFORT PROBES on a latency-critical path, and their tightened policy.
74
+ *
75
+ * 🔴 WHY THIS EXISTS (OPE-1211 review finding 2). `auth.test` resolves the bot's own identity, and
76
+ * `slack-front.ts` awaits it BEFORE returning `OK()` on the route Slack posts events to. Slack times
77
+ * that ack out at 3 seconds and re-delivers; an app that keeps missing the ack gets its event
78
+ * subscription disabled. Under the shared defaults a workspace-wide 429 — the exact case this whole
79
+ * change exists for — would block that ack for up to 60 s and turn a rate-limit episode into a
80
+ * delivery outage. So the probe waits at most about 2 s in total, and its callers already degrade
81
+ * gracefully when it answers nothing.
82
+ *
83
+ * `conversations.info` and `users.info` are here for the same reason (deploy #142 batch review):
84
+ * the Slack front route awaits both before `OK()` — `channelFactsFor` resolves through
85
+ * `channelInternalAndMeta` (fails closed to `{internal:false}`) and `authorFactsFor` through
86
+ * `slackAuthorIsMember` (fails closed to `undefined`) — so the shared 60 s budget would hold the
87
+ * 3-second ack exactly like `auth.test`.
88
+ *
89
+ * `conversations.open` joined them for the SAME two reasons (OPE-1376): a GLOBAL shortcut has no
90
+ * conversation, so the front route awaits this call to resolve the invoker's DM before it acks, and
91
+ * `liveOpenDirectMessageChannel` fails closed to `undefined` (the shortcut then wakes nothing and
92
+ * says so). Slack's interactivity budget is 3 seconds, so the shared policy would hold the ack past
93
+ * it on a rate-limited workspace. It is NOT here for the governance DM-delivery arm, which makes
94
+ * the same call off any ack's critical path and is content with the shared budget.
95
+ *
96
+ * Only add an operation here if BOTH hold: its caller treats a failure as non-fatal, AND something
97
+ * time-critical waits on it.
98
+ *
99
+ * Every member is pinned by name in `scripts/slack-rate-limit-test.ts` § 4c (OPE-1242): drop one
100
+ * and it falls back to the shared 4-attempt / 30 s policy, which that suite asserts against.
101
+ */
102
+ const PROBE_OPERATIONS = new Set(["auth.test", "conversations.info", "users.info", "conversations.open"]);
103
+ const PROBE_MAX_ATTEMPTS = 2;
104
+ const PROBE_CAP_MS = 2_000;
105
+ /** An explicit token, else `SLACK_BOT_TOKEN`. Throws eve's exact message when neither is set. */
106
+ function resolveBotToken(botToken) {
107
+ const token = botToken ?? process.env.SLACK_BOT_TOKEN;
108
+ if (!token)
109
+ throw new Error("SLACK_BOT_TOKEN is required.");
110
+ return token;
111
+ }
112
+ /**
113
+ * POST one Slack Web API method, with the 429 backoff applied.
114
+ *
115
+ * Returns Slack's parsed JSON envelope on any 2xx, `ok:false` included. Throws
116
+ * {@link SlackWebApiError} on a non-2xx that is not a 429, and `RateLimitExhaustedError` when a 429
117
+ * outlasts the bounded attempts.
118
+ */
119
+ export async function callSlackWebApi(input, deps = {}) {
120
+ const token = resolveBotToken(input.botToken);
121
+ const encoded = encodeSlackForm(input.body);
122
+ const url = new URL(input.operation, deps.apiBase ?? SLACK_API_BASE).toString();
123
+ const send = () => (deps.fetch ?? globalThis.fetch)(url, {
124
+ body: encoded,
125
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/x-www-form-urlencoded" },
126
+ method: "POST",
127
+ });
128
+ const probe = PROBE_OPERATIONS.has(input.operation);
129
+ const res = await withRetryAfter(send, {
130
+ surface: "slack",
131
+ operation: input.operation,
132
+ // An explicit dep always wins, so a test still drives the policy it wants.
133
+ maxAttempts: deps.maxAttempts ?? (probe ? PROBE_MAX_ATTEMPTS : undefined),
134
+ capMs: deps.capMs ?? (probe ? PROBE_CAP_MS : undefined),
135
+ totalBudgetMs: deps.totalBudgetMs,
136
+ sleep: deps.sleep,
137
+ now: deps.now,
138
+ });
139
+ // eve parses the body before the status check, so a non-2xx carries Slack's error envelope. Keep
140
+ // that, and keep the parse failure that a non-JSON body produces — callers already treat any throw
141
+ // from this seam as an ambiguous transport failure.
142
+ const json = (await res.json());
143
+ if (!res.ok)
144
+ throw new SlackWebApiError({ method: input.operation, status: res.status, response: json });
145
+ return json;
146
+ }
147
+ //# sourceMappingURL=slack-web-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-web-api.js","sourceRoot":"","sources":["../../../agent/channels/slack-web-api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import { type WebhookVerifier } from "@open-everywhere/core/agent/adapter-api.js";
2
+ /** The registry key — what a generic hook declares as its `source` to be verified as Slack. */
3
+ export declare const SLACK_WEBHOOK_SOURCE = "slack";
4
+ export declare const slackWebhookVerifier: WebhookVerifier;
5
+ //# sourceMappingURL=slack-webhook-verifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-webhook-verifier.d.ts","sourceRoot":"","sources":["../../../agent/channels/slack-webhook-verifier.ts"],"names":[],"mappings":"AAuDA,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,4CAA4C,CAAC;AAEzG,+FAA+F;AAC/F,eAAO,MAAM,oBAAoB,UAAU,CAAC;AA0B5C,eAAO,MAAM,oBAAoB,EAAE,eA+BlC,CAAC"}
@@ -0,0 +1,112 @@
1
+ // Slack's webhook signature scheme, as a registry entry (OPE-197) — what lets a GENERIC hook
2
+ // (`POST /webhooks/<id>`) declare `source: "slack"` and be verified exactly the way Slack signs,
3
+ // instead of the deployment being able to check that signature in the surface front door and
4
+ // nowhere else.
5
+ //
6
+ // WHY THIS EXISTS AT ALL, in the two terms that survive. (1) `registeredWebhookVerifierSources()` is
7
+ // printed VERBATIM in the gate's 401 (webhooks/gate.ts), so it is a live statement to a caller about
8
+ // what this deployment can authenticate — and it was under-reporting a scheme the tree has
9
+ // implemented and tested for as long as the Slack surface has existed. (2) Registration is the only
10
+ // place `keyOrigin` and `signedTimestamp` get DECLARED, and those two fields are what drive
11
+ // `acceptsMintedSecret` (webhooks/source-vocabulary.ts) and the durable single-use replay dedupe
12
+ // (webhooks/replay-dedupe.ts). Inline in a front door, both facts are true of the code and invisible
13
+ // to every registry that reasons about them.
14
+ //
15
+ // ZERO NEW CRYPTO. The basestring and digest are `channels/slack-signature.ts`'s — the SAME
16
+ // primitive the surface front door (`verifySlackSignature`) uses, extracted for this entry rather
17
+ // than copied, so there is one implementation of Slack's scheme in the tree and not two that can
18
+ // drift. What this file adds is the part a registry entry needs and a boolean-returning front door
19
+ // has no use for: the closed FAILURE vocabulary, and iteration over a rotation's key list.
20
+ //
21
+ // THE SCHEME (https://api.slack.com/authentication/verifying-requests-from-slack):
22
+ // X-Slack-Request-Timestamp: <unix seconds>
23
+ // X-Slack-Signature: v0=<hex> HMAC-SHA256 of `v0:<timestamp>:<raw body>`.
24
+ //
25
+ // 🔴 `keyOrigin: "vendor-issued"` (OPE-186) — the key is the app's **Signing Secret**, which SLACK
26
+ // generates and shows on the app's Basic Information page (docs/deploy/surfaces/slack.md step 1.5:
27
+ // *"copy the Signing Secret"*). It is read, never chosen, so a secret WE minted can never verify a
28
+ // Slack delivery. That is what keeps `slack` OUT of the chat-mintable source vocabulary
29
+ // (`acceptsMintedSecret` is false for it) — only an OPERATOR-DECLARED hook, whose `secretRef` the
30
+ // operator points at the stored signing secret, can use this verifier. Registering it does NOT make
31
+ // it chat-creatable, and that is the intended outcome, not a limitation to be fixed later.
32
+ //
33
+ // ⭐ `signedTimestamp: true` — and this is the substantive gain over leaving it inline. Slack's
34
+ // basestring COVERS the timestamp, so the freshness claim is the sender's own signed one, and this
35
+ // verifier enforces a window against it. Under `webhooks/gate.ts` that promotes the source from
36
+ // "replay bounded by nothing" to the durable single-use 409 (webhooks/replay-dedupe.ts) — the layer
37
+ // `github` and `notion` structurally cannot have, because they sign the body alone.
38
+ //
39
+ // ⚠️ THE SKEW WINDOW HERE IS THE DEPLOYMENT'S, NOT SLACK'S. The front door hard-codes Slack's own
40
+ // documented 5 minutes because it answers a Slack Request URL; this entry reads
41
+ // `webhookMaxSkewSeconds()` (`OE_WEBHOOK_MAX_SKEW_SECONDS`, default 300) because it answers the
42
+ // GENERIC ingress, where an operator's one freshness bound governs every scheme. The two coincide at
43
+ // the default on purpose and are allowed to diverge on purpose.
44
+ //
45
+ // NOTE this entry changes NO existing route. `/eve/v1/slack` and `/eve/v1/slack/commands` keep their
46
+ // own inline gate, unchanged; registering here only makes the scheme available to the generic
47
+ // ingress.
48
+ import { createHash } from "node:crypto";
49
+ import { SLACK_SIGNATURE_HEADER, SLACK_SIGNATURE_VERSION, SLACK_TIMESTAMP_HEADER, slackSignatureMatches, } from "./slack-signature.js";
50
+ import { webhookMaxSkewSeconds } from "@open-everywhere/core/agent/adapter-api.js";
51
+ /** The registry key — what a generic hook declares as its `source` to be verified as Slack. */
52
+ export const SLACK_WEBHOOK_SOURCE = "slack";
53
+ /** Unix seconds, as Slack sends them. Checked as TEXT before `Number()` so `"1e9"`, `" 12"` and
54
+ * `"12.0"` are `malformed-timestamp` rather than silently coerced into a passing window. */
55
+ const TIMESTAMP_RE = /^[0-9]{1,15}$/;
56
+ /**
57
+ * The opaque single-use nonce for an accepted Slack delivery.
58
+ *
59
+ * 🔴 IT IS THE SIGNED CONTENT, HASHED — not a delivery id, because Slack's scheme has none: the
60
+ * basestring is `v0:<timestamp>:<rawBody>` and nothing else, so `(timestamp, rawBody)` IS the
61
+ * complete identity the sender signed. Two presentations agreeing on both are, to this scheme,
62
+ * indistinguishable — which is exactly what "the same delivery, twice" means here.
63
+ *
64
+ * WHAT THAT COSTS, stated rather than assumed: a byte-identical re-send from Slack inside the window
65
+ * is graded a replay. That is deliberate and it is nearly free, because a byte-identical re-send
66
+ * OUTSIDE the window already fails the skew check at 401 — so this dedupe only ever changes the
67
+ * verdict in the interval where the scheme itself cannot tell a legitimate re-send from a captured
68
+ * one. Deriving it from the SIGNATURE instead would have been wrong for a different reason: the
69
+ * signature is key-dependent, so it would change identity across a rotation overlap for a delivery
70
+ * that never changed.
71
+ */
72
+ function slackReplayKey(timestamp, rawBody) {
73
+ return createHash("sha256").update(`${SLACK_WEBHOOK_SOURCE}:${timestamp}:${rawBody}`).digest("hex");
74
+ }
75
+ export const slackWebhookVerifier = {
76
+ source: SLACK_WEBHOOK_SOURCE,
77
+ // Provider-owned ⇒ routed through admitProviderRegistration: de-select the slack bundle and this
78
+ // row never lands, so a `source: "slack"` hook 401s instead of silently using another scheme.
79
+ provider: "slack",
80
+ keyKind: "shared-secret",
81
+ keyOrigin: "vendor-issued",
82
+ signedTimestamp: true,
83
+ verify({ rawBody, headers, keys, nowMs }) {
84
+ const signature = headers.get(SLACK_SIGNATURE_HEADER);
85
+ if (!signature)
86
+ return { ok: false, failure: "missing-signature" };
87
+ const timestamp = headers.get(SLACK_TIMESTAMP_HEADER);
88
+ // A missing timestamp is its own failure, not `missing-signature`: it is COVERED BY the
89
+ // signature, so without it there is no basestring to compute at all.
90
+ if (timestamp === null)
91
+ return { ok: false, failure: "missing-timestamp" };
92
+ if (!TIMESTAMP_RE.test(timestamp))
93
+ return { ok: false, failure: "malformed-timestamp" };
94
+ // SKEW BEFORE ANY HMAC — a stale replay must not cost a digest per configured key.
95
+ if (Math.abs(nowMs / 1000 - Number(timestamp)) > webhookMaxSkewSeconds())
96
+ return { ok: false, failure: "stale-timestamp" };
97
+ // A signature tagged with a version we do not implement is `unsupported-scheme`, never an
98
+ // attempted match: if Slack ever ships a `v1`, guessing at it is the shape of a downgrade.
99
+ if (!signature.startsWith(`${SLACK_SIGNATURE_VERSION}=`))
100
+ return { ok: false, failure: "unsupported-scheme" };
101
+ for (const [i, key] of keys.entries()) {
102
+ // Ordered, current-first: a match at i>0 is a rotation overlap and the gate audits it. Slack
103
+ // documents no overlap window of its own (rotating the signing secret is immediate), so
104
+ // `keys[]` is how an operator gets one on our side.
105
+ if (slackSignatureMatches(key, timestamp, rawBody, signature)) {
106
+ return { ok: true, keyIndex: i, replayKey: slackReplayKey(timestamp, rawBody) };
107
+ }
108
+ }
109
+ return { ok: false, failure: "signature-mismatch" };
110
+ },
111
+ };
112
+ //# sourceMappingURL=slack-webhook-verifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-webhook-verifier.js","sourceRoot":"","sources":["../../../agent/channels/slack-webhook-verifier.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { type SlackContext, type SlackMessage } from "eve/channels/slack";
2
+ import { type OpenEverywhereSlackAuth } from "./slack-identity.js";
3
+ /**
4
+ * The `chat.postMessage` form body for a channel-supervisor's host-side reply post (OPE-878).
5
+ *
6
+ * `deliverSupervisorReply`'s `post` callback bypasses the post-arm registry (slack-post-arm.ts) —
7
+ * it builds its own `slackForm` call rather than going through `dispatchSlackReply` — so it needs
8
+ * its OWN `markdownToSlackMrkdwn` conversion at the wire; that seam does not cover it. This is that
9
+ * ONE conversion point, extracted so a test can drive it without a live Slack call (the closure
10
+ * above cannot be invoked directly — it captures live session state).
11
+ */
12
+ export declare function supervisorPostFormBody(channelId: string, threadTs: string | undefined, text: string): string;
13
+ /** What eve's mention/DM hook may return. `null` (={@link DROP}) is the ONLY value that
14
+ * dispatches no model turn — see {@link DROP}. */
15
+ type InboundOutcome = {
16
+ readonly auth: OpenEverywhereSlackAuth | null;
17
+ readonly context?: readonly string[];
18
+ } | null;
19
+ /**
20
+ * THE WAKE-ADMISSION CHOKEPOINT for eve's native mention/DM door (OPE-33) — the only thing wired
21
+ * into `onAppMention`/`onDirectMessage`.
22
+ *
23
+ * A returned turn is a spend commitment, so this wrapper enforces the invariant that inspection
24
+ * kept failing to hold: **nothing is admitted unless the spend gate was consulted for THIS
25
+ * message**. Fail-CLOSED — an admission with no consult (a future early-return added above the
26
+ * gate, exactly the shape that broke it) is downgraded to a real DROP and said out loud, rather
27
+ * than quietly spending. A DROP passes straight through (nothing to gate: no turn, no spend).
28
+ *
29
+ * The refusal itself is posted by the gate, so a declined wake is still visible in-thread; this
30
+ * wrapper only decides whether a MODEL TURN follows.
31
+ */
32
+ export declare function admitInbound(ctx: SlackContext, message: SlackMessage, isDm: boolean, run?: (consult: {
33
+ gated: boolean;
34
+ }) => Promise<InboundOutcome>): Promise<InboundOutcome>;
35
+ declare const _default: import("eve/channels/slack").SlackChannel;
36
+ export default _default;
37
+ //# sourceMappingURL=slack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../../agent/channels/slack.ts"],"names":[],"mappings":"AAkBA,OAAO,EAEL,KAAK,YAAY,EAGjB,KAAK,YAAY,EAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAQL,KAAK,uBAAuB,EAC7B,MAAM,qBAAqB,CAAC;AA4K7B;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI5G;AA+QD;mDACmD;AACnD,KAAK,cAAc,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,uBAAuB,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CAAC;AA4OrH;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,YAAY,EACrB,IAAI,EAAE,OAAO,EACb,GAAG,GAAE,CAAC,OAAO,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,KAAK,OAAO,CAAC,cAAc,CAAuD,GAClH,OAAO,CAAC,cAAc,CAAC,CAYzB"}