@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,262 @@
1
+ // SLACK SHORTCUTS AS A WAKE ORIGIN (OPE-1376).
2
+ //
3
+ // Slack gives a person two ways to hand something to an app without typing at it: a MESSAGE
4
+ // shortcut (the `…` overflow menu on one message) and a GLOBAL shortcut (the ⚔ composer /
5
+ // search-bar entry). Both arrive on the app's INTERACTIVITY Request URL as a urlencoded
6
+ // `payload=…` body — the same URL and the same body family as a button click, never as an
7
+ // Events API `event_callback`.
8
+ //
9
+ // šŸ”“ A SHORTCUT IS A WAKE ORIGIN, NOT A NEW PLANE. This module does exactly one thing: it turns a
10
+ // shortcut payload into the {@link ParsedMessageEvent} the rest of the Slack front door already
11
+ // speaks. Every downstream property — the partition, the seat, the window, the dedupe, the spend
12
+ // gate, the external-participant policy, the wake trust tier — is then derived by the SAME code a
13
+ // mention or a DM goes through. Nothing here decides posture, and nothing here posts.
14
+ //
15
+ // WHY OE OWNS THE PAYLOAD RATHER THAN FORWARDING IT TO EVE. eve 0.48.0 added an `onShortcut` hook
16
+ // to `slackChannel`, and `slack.ts` does not configure one. So a forwarded shortcut is a
17
+ // guaranteed no-op today. The hook could not carry this work either: `SlackShortcutContext`
18
+ // (eve's own type) hands the handler a workspace API handle and NOTHING else — no session, no
19
+ // resolved seat, no `send`. The machinery that turns an inbound Slack event into a wake on the
20
+ // right window lives in `slack-front.ts`, so the shortcut is classified and handled there, beside
21
+ // the approval click, and never forwarded.
22
+ //
23
+ // šŸ”“ THE PRINCIPAL IS THE INVOKER, NEVER THE MESSAGE AUTHOR. A message shortcut names two people:
24
+ // whoever clicked it, and whoever wrote the message it was clicked on. The wake binds to the
25
+ // CLICKER. Binding to the author would let one person invoke a shortcut on another person's
26
+ // message and get a turn that runs with the author's access, memory scope and admin standing —
27
+ // the message body is the CONTENT, and content never selects the principal. See
28
+ // {@link shortcutMessageEvent}, which is the only place the two identities meet.
29
+ import { ingestUntrusted, noteIngestionVerdict, taintMarkLands, } from "@open-everywhere/core/agent/adapter-api.js";
30
+ import { parseSlackFiles } from "./slack-events.js";
31
+ import { slackInteractivityTenantIds } from "./slack-tenant-fence.js";
32
+ function str(v) {
33
+ return typeof v === "string" && v.length > 0 ? v : undefined;
34
+ }
35
+ function obj(v) {
36
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : undefined;
37
+ }
38
+ /**
39
+ * Decode an already-JSON-parsed interactivity payload into a {@link ParsedSlackShortcut}, or
40
+ * `null` for anything that is not a shortcut. TOTAL AND PURE: it never throws and it writes
41
+ * nothing.
42
+ *
43
+ * šŸ”“ PURITY IS A SECURITY PROPERTY HERE, NOT A STYLE CHOICE (OPE-1442). This runs inside
44
+ * `classifyEnvelope`, which decides what a request IS — long before the dedupe, the readability
45
+ * gate and the ambient-off drop decide whether it wakes anything. An earlier revision registered
46
+ * the target message's attachments right here, so every click seeded the process-global registry
47
+ * (a 1000-entry LRU) even when the shortcut went on to wake NOTHING: repeated clicks on a domain
48
+ * with the unified window off evicted the records real uploads depend on, and `describe_image`
49
+ * then failed closed on a genuine image. The registry is now seeded by the front door's shortcut
50
+ * branch, after the gates it must sit behind.
51
+ *
52
+ * A payload is REFUSED (null) when it cannot produce a wake:
53
+ * • the outer `type` is neither `message_action` nor `shortcut`;
54
+ * • `callback_id`, `trigger_id` or `user.id` is missing — the first two are the trigger's name
55
+ * and its idempotency key, and the third is the principal, so a wake without any of them
56
+ * would have to be invented rather than derived;
57
+ * • a `message_action` that carries no `channel.id` or no `message.ts` — a message shortcut
58
+ * whose target cannot be addressed is not a message shortcut.
59
+ * Refusing is FAIL-CLOSED: the front route acks Slack and wakes nothing.
60
+ *
61
+ * `message.text` is allowed to be EMPTY and still parses. Slack sends an empty `text` for a message
62
+ * whose content lives entirely in blocks or files, and dropping a click a person made is the worse
63
+ * answer. The wake then renders an empty body — it says nothing ABOUT the emptiness. Files on that
64
+ * message ride through as attachments only where the front door admits them
65
+ * ({@link shortcutMessageEvent}'s `admitAttachments`), so a shortcut on a screenshot in a room the
66
+ * app cannot read wakes with an empty body and no file.
67
+ */
68
+ export function parseSlackShortcutPayload(raw) {
69
+ const p = obj(raw);
70
+ if (!p)
71
+ return null;
72
+ const type = str(p.type);
73
+ if (type !== "message_action" && type !== "shortcut")
74
+ return null;
75
+ const callbackId = str(p.callback_id);
76
+ const triggerId = str(p.trigger_id);
77
+ const userId = str(obj(p.user)?.id);
78
+ if (!callbackId || !triggerId || !userId)
79
+ return null;
80
+ // THE WORKSPACE AND THE GRID RUNG (OPE-1559), read by the ONE reader every interactivity ingress
81
+ // spends — `slackInteractivityTenantIds` (slack-tenant-fence.ts), which also states which three
82
+ // spellings of the Grid id it unions and why `context_enterprise_id` is excluded. A shortcut is
83
+ // one interactivity payload among six click families (OPE-1680), and a second copy of the reading
84
+ // rule here is the drift that leaf module exists to prevent.
85
+ const { teamId, enterpriseId } = slackInteractivityTenantIds(p);
86
+ if (type === "shortcut") {
87
+ return { kind: "global", callbackId, triggerId, userId, ...(teamId ? { teamId } : {}), ...(enterpriseId ? { enterpriseId } : {}) };
88
+ }
89
+ const channelId = str(obj(p.channel)?.id);
90
+ const message = obj(p.message);
91
+ const ts = str(message?.ts);
92
+ if (!channelId || !message || !ts)
93
+ return null;
94
+ const threadTs = str(message.thread_ts);
95
+ const authorId = str(message.user);
96
+ // File REFERENCES only. Parsing is all this function does with them: registering an id is what
97
+ // makes it fetchable with the bot token, and that decision belongs to the front door, which knows
98
+ // whether this shortcut wakes anything and whether the app may read the room at all. See
99
+ // {@link shortcutMessageEvent}'s `admitAttachments`.
100
+ const attachments = parseSlackFiles(message.files);
101
+ return {
102
+ kind: "message",
103
+ callbackId,
104
+ triggerId,
105
+ userId,
106
+ ...(teamId ? { teamId } : {}),
107
+ ...(enterpriseId ? { enterpriseId } : {}),
108
+ channelId,
109
+ message: {
110
+ text: typeof message.text === "string" ? message.text : "",
111
+ ts,
112
+ ...(threadTs ? { threadTs } : {}),
113
+ ...(authorId ? { userId: authorId } : {}),
114
+ ...(attachments.length > 0 ? { attachments } : {}),
115
+ },
116
+ };
117
+ }
118
+ /**
119
+ * The idempotency key for one shortcut invocation.
120
+ *
121
+ * Keyed on `trigger_id`, NOT on the target message. What that buys is certain: a person stays free
122
+ * to invoke the same shortcut on the same message twice on purpose, which keying on `channel:ts`
123
+ * (the shape an Events API wake falls back to) would have swallowed for ten minutes.
124
+ *
125
+ * What it de-dupes rests on an UNMEASURED assumption — that a Slack redelivery of one click carries
126
+ * the same trigger id (see {@link ParsedSlackShortcut.triggerId}). Wrong ⇒ a double wake, never a
127
+ * missed one, which is the direction the whole dedupe layer already fails in.
128
+ *
129
+ * It carries no bot-identity prefix, unlike `slack-front.ts`'s `wakeKey`. A trigger id is minted by
130
+ * Slack per app, so two apps in one workspace cannot collide on one; the `oe-shortcut:` prefix
131
+ * keeps it out of the Events API key space.
132
+ */
133
+ export function shortcutWakeKey(sc) {
134
+ return `oe-shortcut:${sc.triggerId}`;
135
+ }
136
+ /**
137
+ * Fence the target message's body, and declare the ingestion route this door opens.
138
+ *
139
+ * The declaration is the kind-binding entry for the route (access/ingestion-binding-manifest.ts). A
140
+ * message shortcut carries a Slack message body in through the INTERACTIVITY door, which no Events
141
+ * API route uses, so the route says what it ingests: `chat-message`.
142
+ *
143
+ * šŸ”“ WHY IT FENCES, AND WHAT THE FENCE STANDS IN FOR. The wake envelope attributes every message to
144
+ * ONE author and grades it with ONE trust tier, and on this path both are the INVOKER'S:
145
+ * `triggerMessage` (slack-events.ts) renders `author`/`author-id` from `ev.userId` and `trust` from
146
+ * the tier the front door resolved for the person who pressed the button. So the target message's
147
+ * OWN author is never graded here — this route resolves no author facts for them and makes no
148
+ * `users.info` probe on them, which means the OPE-466 non-member taint join that the channel door
149
+ * applies does not see them either.
150
+ *
151
+ * The consequence, stated plainly: read through the channel door, a guest's or a Connect stranger's
152
+ * message is graded `unauth` and the envelope fences it. Handed over by a `principal` invoker's
153
+ * shortcut, the SAME bytes would render under that invoker's tier — and
154
+ * `authorTrustAdmitsUnfencedBody` (channels/wake-envelope.ts) lets a `peer`/`principal` human body
155
+ * through UNFENCED. This fence is what closes that gap: the body carries its own wrapper because it
156
+ * cannot carry its own tier.
157
+ *
158
+ * That is deliberately MORE conservative than the ruled Slack-forward precedent (slack-events.ts's
159
+ * note on quoted attachment content, which inherits the forwarder's tier unfenced). The difference
160
+ * is who chose: a forward is text a person retyped into their own message, while a shortcut hands
161
+ * over somebody else's message untouched, so there is no editorial act to inherit a tier from.
162
+ *
163
+ * `taintMarkLands()` reports the RUNTIME effect rather than asserting one, for the reason that
164
+ * function's header states: this runs in the INGRESS process, outside any turn, so the taint half
165
+ * of `ingestUntrusted` no-ops here and the declaration says so honestly.
166
+ */
167
+ function fenceAndDeclareShortcutIngestion(text) {
168
+ noteIngestionVerdict({ kind: "chat-message", label: "slack:shortcut" }, taintMarkLands());
169
+ return ingestUntrusted(text, { source: "slack message shortcut" });
170
+ }
171
+ /**
172
+ * The body a GLOBAL shortcut wakes with. It has no message, so the trigger's own name is the whole
173
+ * content — which is the honest thing to hand the model: somebody pressed a named button.
174
+ *
175
+ * The callback id is operator-authored configuration, not somebody's Slack message, so it is NOT
176
+ * fenced as untrusted content — the same reasoning that leaves a slash-command name unfenced.
177
+ */
178
+ export function globalShortcutText(sc) {
179
+ return `Shortcut "${sc.callbackId}" invoked.`;
180
+ }
181
+ /**
182
+ * A Slack-shaped `ts` for the one wake that has no message behind it: a global shortcut.
183
+ *
184
+ * Slack's format is `<seconds>.<microseconds>`, and the wake envelope reads it back with
185
+ * `Number.parseFloat` to render `sent-at`. Minting from the clock therefore makes `sent-at` say
186
+ * exactly what is true — when the person pressed the button. The microsecond field carries this
187
+ * instant's milliseconds; the last three digits are zero, because a millisecond clock cannot
188
+ * honestly claim more precision than it has.
189
+ */
190
+ function slackTsFromMs(nowMs) {
191
+ const seconds = Math.floor(nowMs / 1000);
192
+ const millis = nowMs % 1000;
193
+ return `${seconds}.${String(millis).padStart(3, "0")}000`;
194
+ }
195
+ /**
196
+ * Map a shortcut onto the shared {@link ParsedMessageEvent} shape, so the front door's existing
197
+ * wake spine (partition → seat → window → dedupe → gates → divert) runs over it unchanged.
198
+ *
199
+ * šŸ”“ `userId` IS THE INVOKER on both kinds. See the file header: the target message's author never
200
+ * becomes the wake's principal.
201
+ *
202
+ * THE TWO TEXT FIELDS CARRY THE MESSAGE-SHORTCUT SPLIT EXACTLY AS THEY ALREADY MEAN IT. A message
203
+ * shortcut is a FORWARD with no covering note — the invoker typed nothing and pointed at somebody
204
+ * else's message — and {@link ParsedMessageEvent} already draws that line: `text` is "own text
205
+ * plus any quoted message", `authoredText` is "the author's OWN typed text". So `text` carries the
206
+ * target message and `authoredText` is empty. Any rule that asks "what did this person SAY?" then
207
+ * correctly answers "nothing", which is true, and the model still receives the message as the
208
+ * wake's content.
209
+ *
210
+ * `ts` differs by kind, and deliberately:
211
+ * • a MESSAGE shortcut takes the target message's real ts, so the reply anchors under the
212
+ * message the shortcut was invoked on (`routeSupervisorEvent` reads `threadTs ?? ts`);
213
+ * • a GLOBAL shortcut has no message, so it MINTS one from `nowMs` in Slack's own
214
+ * `seconds.microseconds` shape. šŸ”“ It must be ts-SHAPED, not merely unique: the envelope
215
+ * renders `sent-at` by running `Number.parseFloat` over this field (`slackTsToIso`), so a
216
+ * non-numeric id silently becomes a nonsense date — the trigger id `13345224609.7384…` parses
217
+ * to the year 2392. Minting from the clock makes `sent-at` the truth: the moment the person
218
+ * pressed the button. Uniqueness is not this field's job — the wake's idempotency key is
219
+ * {@link shortcutWakeKey}, which does key on the trigger id.
220
+ *
221
+ * `isAppMention` is false on both. It reports whether the TEXT contains `<@bot>`, and neither body
222
+ * does; the "was the agent addressed?" question is answered by the caller passing `directMention`,
223
+ * which is the same seam the single-window mention divert uses.
224
+ *
225
+ * šŸ”“ `admitAttachments` GATES THE TARGET MESSAGE'S FILES, AND IT DEFAULTS TO FALSE (OPE-1442).
226
+ * Slack delivers a `message_action` for a room the app is not in, and the payload carries that
227
+ * message's `files[].url_private` — bytes the app's own channel reads refuse. So the files ride
228
+ * through only when the caller has POSITIVELY established that the app can read the room
229
+ * (`slackChannelFactsFor`'s `appCanRead`). The default is closed because the cost of the two
230
+ * mistakes is not symmetric: a withheld file is a wake that says less, an admitted one is the bot
231
+ * token downloading somebody's private upload from a room the agent cannot open.
232
+ *
233
+ * WITHHOLDING DROPS THE REFERENCE TOO, rather than passing an id that resolves to nothing. The
234
+ * envelope renders an attachment as an id and `describe_image` fails closed on an id the registry
235
+ * never saw, so an advertised-but-unresolvable file tells the model an image exists and then
236
+ * cannot show it — worse than not mentioning it.
237
+ */
238
+ export function shortcutMessageEvent(sc, channelId, channelType, nowMs = Date.now(), opts = {}) {
239
+ const attachments = opts.admitAttachments === true ? sc.message?.attachments : undefined;
240
+ const text = sc.kind === "message" && sc.message ? fenceAndDeclareShortcutIngestion(sc.message.text) : globalShortcutText(sc);
241
+ return {
242
+ channelId,
243
+ ts: sc.kind === "message" && sc.message ? sc.message.ts : slackTsFromMs(nowMs),
244
+ ...(sc.kind === "message" && sc.message?.threadTs ? { threadTs: sc.message.threadTs } : {}),
245
+ ...(sc.teamId ? { teamId: sc.teamId } : {}),
246
+ // The Grid rung the payload named (OPE-1559), spread conditionally exactly as the wake routes
247
+ // spread it: it reaches `buildSlackAuth` through `authForEvent` and `scheduleDmWake`, so a
248
+ // shortcut in a Grid workspace declared only at the enterprise key now resolves that line, and a
249
+ // non-Grid shortcut stamps nothing.
250
+ ...(sc.enterpriseId ? { enterpriseId: sc.enterpriseId } : {}),
251
+ userId: sc.userId,
252
+ ...(channelType ? { channelType } : {}),
253
+ text,
254
+ ...(attachments && attachments.length > 0 ? { attachments } : {}),
255
+ // A global shortcut's body IS what the invoker did — they pressed that named button and typed
256
+ // nothing else, so the trigger line is their own "text". A message shortcut's body is somebody
257
+ // else's message, so the invoker authored nothing.
258
+ authoredText: sc.kind === "global" ? text : "",
259
+ isAppMention: false,
260
+ };
261
+ }
262
+ //# sourceMappingURL=slack-shortcut.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-shortcut.js","sourceRoot":"","sources":["../../../agent/channels/slack-shortcut.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /** The header carrying the signature. Exported so no caller re-types the string. */
2
+ export declare const SLACK_SIGNATURE_HEADER = "x-slack-signature";
3
+ /** The header carrying the signed unix-seconds timestamp. */
4
+ export declare const SLACK_TIMESTAMP_HEADER = "x-slack-request-timestamp";
5
+ /** The version tag that both prefixes the signature and opens the basestring. */
6
+ export declare const SLACK_SIGNATURE_VERSION = "v0";
7
+ /**
8
+ * The signature Slack would send for this `(secret, timestamp, rawBody)` — `v0=<hex>` over
9
+ * `v0:<timestamp>:<rawBody>`.
10
+ *
11
+ * `timestamp` is taken as the STRING off the header, never a number, because it is signed
12
+ * character-for-character: re-formatting it (leading zeros, a float round-trip) changes the
13
+ * basestring and breaks a signature that is perfectly good.
14
+ */
15
+ export declare function slackSignature(secret: string, timestamp: string, rawBody: string): string;
16
+ /** Length-prechecked constant-time compare of a presented signature against the expected one —
17
+ * the shape every verifier in this tree uses. */
18
+ export declare function slackSignatureMatches(secret: string, timestamp: string, rawBody: string, presented: string): boolean;
19
+ //# sourceMappingURL=slack-signature.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-signature.d.ts","sourceRoot":"","sources":["../../../agent/channels/slack-signature.ts"],"names":[],"mappings":"AA2BA,oFAAoF;AACpF,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAC1D,6DAA6D;AAC7D,eAAO,MAAM,sBAAsB,8BAA8B,CAAC;AAClE,iFAAiF;AACjF,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEzF;AAED;kDACkD;AAClD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAIpH"}
@@ -0,0 +1,50 @@
1
+ // SLACK'S `v0=` SIGNATURE SCHEME, as a leaf primitive — the ONE implementation of Slack's digest in
2
+ // this tree, extracted (OPE-197) so the two callers that need it cannot drift into two schemes:
3
+ //
4
+ // • channels/slack-front.ts `verifySlackSignature` — the SURFACE front door (events, interactivity,
5
+ // `/oe` slash commands), which reads its secret from `SLACK_SIGNING_SECRET` and answers a Slack
6
+ // Request URL.
7
+ // • channels/slack-webhook-verifier.ts — the WEBHOOK VERIFIER REGISTRY entry, which reads its keys
8
+ // from a hook's `secretRef` list and answers the generic ingress.
9
+ //
10
+ // Those two resolve their key differently and discriminate failures differently; what they must
11
+ // share to the byte is the BASESTRING and the DIGEST, because a difference there is a signature that
12
+ // verifies on one route and 401s on the other with nothing saying why. So exactly that much lives
13
+ // here and nothing else — no header reading, no skew window, no key resolution.
14
+ //
15
+ // THE SCHEME (https://api.slack.com/authentication/verifying-requests-from-slack):
16
+ // X-Slack-Request-Timestamp: <unix seconds>
17
+ // X-Slack-Signature: v0=<hex> HMAC-SHA256 of `v0:<timestamp>:<raw body>`, keyed by the
18
+ // app's Signing Secret.
19
+ // The timestamp is INSIDE the signed basestring, which is what makes a skew check meaningful here
20
+ // (contrast GitHub, which signs the body alone) — but enforcing that window is the caller's, since
21
+ // the two callers bound it differently on purpose.
22
+ //
23
+ // Pinned by Slack's OWN PUBLISHED TEST VECTOR in scripts/slack-webhook-verifier-test.ts — the only
24
+ // assertion that proves we implement THEIR scheme rather than a self-consistent idea of it.
25
+ import { createHmac, timingSafeEqual } from "node:crypto";
26
+ /** The header carrying the signature. Exported so no caller re-types the string. */
27
+ export const SLACK_SIGNATURE_HEADER = "x-slack-signature";
28
+ /** The header carrying the signed unix-seconds timestamp. */
29
+ export const SLACK_TIMESTAMP_HEADER = "x-slack-request-timestamp";
30
+ /** The version tag that both prefixes the signature and opens the basestring. */
31
+ export const SLACK_SIGNATURE_VERSION = "v0";
32
+ /**
33
+ * The signature Slack would send for this `(secret, timestamp, rawBody)` — `v0=<hex>` over
34
+ * `v0:<timestamp>:<rawBody>`.
35
+ *
36
+ * `timestamp` is taken as the STRING off the header, never a number, because it is signed
37
+ * character-for-character: re-formatting it (leading zeros, a float round-trip) changes the
38
+ * basestring and breaks a signature that is perfectly good.
39
+ */
40
+ export function slackSignature(secret, timestamp, rawBody) {
41
+ return `${SLACK_SIGNATURE_VERSION}=${createHmac("sha256", secret).update(`${SLACK_SIGNATURE_VERSION}:${timestamp}:${rawBody}`).digest("hex")}`;
42
+ }
43
+ /** Length-prechecked constant-time compare of a presented signature against the expected one —
44
+ * the shape every verifier in this tree uses. */
45
+ export function slackSignatureMatches(secret, timestamp, rawBody, presented) {
46
+ const a = Buffer.from(slackSignature(secret, timestamp, rawBody));
47
+ const b = Buffer.from(presented);
48
+ return a.length === b.length && timingSafeEqual(a, b);
49
+ }
50
+ //# sourceMappingURL=slack-signature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-signature.js","sourceRoot":"","sources":["../../../agent/channels/slack-signature.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,gGAAgG;AAChG,EAAE;AACF,sGAAsG;AACtG,oGAAoG;AACpG,mBAAmB;AACnB,qGAAqG;AACrG,sEAAsE;AACtE,EAAE;AACF,gGAAgG;AAChG,qGAAqG;AACrG,kGAAkG;AAClG,gFAAgF;AAChF,EAAE;AACF,mFAAmF;AACnF,8CAA8C;AAC9C,mGAAmG;AACnG,gEAAgE;AAChE,kGAAkG;AAClG,mGAAmG;AACnG,mDAAmD;AACnD,EAAE;AACF,mGAAmG;AACnG,4FAA4F;AAE5F,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1D,oFAAoF;AACpF,MAAM,CAAC,MAAM,sBAAsB,GAAG,mBAAmB,CAAC;AAC1D,6DAA6D;AAC7D,MAAM,CAAC,MAAM,sBAAsB,GAAG,2BAA2B,CAAC;AAClE,iFAAiF;AACjF,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAE5C;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,SAAiB,EAAE,OAAe;IAC/E,OAAO,GAAG,uBAAuB,IAAI,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,GAAG,uBAAuB,IAAI,SAAS,IAAI,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AACjJ,CAAC;AAED;kDACkD;AAClD,MAAM,UAAU,qBAAqB,CAAC,MAAc,EAAE,SAAiB,EAAE,OAAe,EAAE,SAAiB;IACzG,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;IAClE,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACjC,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACxD,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { type IngressSpendGateInput, type SpendGateDecision, type SpendRefusalCopyInput } from "@open-everywhere/core/agent/adapter-api.js";
2
+ export type { SpendGateDoor, SpendGateDecision, SpendGateAuditRow } from "@open-everywhere/core/agent/adapter-api.js";
3
+ /** Slack's shape of the shared gate input: `teamId` is the Slack team id (the shared gate's generic
4
+ * `workspaceRef`, emitted as `team` on the audit row), and `mode` stays REQUIRED here because both
5
+ * Slack doors resolve dm/mpim/channel themselves before the gate is reached. */
6
+ export interface SlackSpendGateInput extends Omit<IngressSpendGateInput, "workspaceRef" | "badges" | "mode"> {
7
+ readonly mode: NonNullable<IngressSpendGateInput["mode"]>;
8
+ /** Slack team id, for the audit row only (`null` when the event carried none). */
9
+ readonly teamId?: string | null;
10
+ }
11
+ /** The refusal copy with Slack's mrkdwn shortcode badge — same sentences as every other surface
12
+ * (they live in core), only the leading emoji differs. The caller passes core's own input, so a
13
+ * Slack refusal names the breached layer and its key exactly as every other surface does
14
+ * (OPE-1860); nothing about the copy is decided here. */
15
+ export declare function spendRefusalCopy(input: Omit<SpendRefusalCopyInput, "badge">): string;
16
+ /** The admin-bypass warning with Slack's mrkdwn shortcode badge. */
17
+ export declare function spendBypassWarnCopy(reason: string | undefined): string;
18
+ /** Decide whether a Slack wake may spend — the shared decision table, bound to Slack's badges.
19
+ * See the core module for the full posture (check fails OPEN, a definite OVER fails CLOSED, an
20
+ * admin proceeds as audited sudo). */
21
+ export declare function slackSpendGate(input: SlackSpendGateInput): Promise<SpendGateDecision>;
22
+ //# sourceMappingURL=slack-spend-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-spend-gate.d.ts","sourceRoot":"","sources":["../../../agent/channels/slack-spend-gate.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAkI,KAAK,qBAAqB,EAAE,KAAK,iBAAiB,EAAE,KAAK,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AAE5Q,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAEtH;;iFAEiF;AACjF,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,qBAAqB,EAAE,cAAc,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC1G,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC1D,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED;;;0DAG0D;AAC1D,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,IAAI,CAAC,qBAAqB,EAAE,OAAO,CAAC,GAAG,MAAM,CAEpF;AAED,oEAAoE;AACpE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAEtE;AAED;;uCAEuC;AACvC,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAKrF"}
@@ -0,0 +1,48 @@
1
+ // The Slack ingress SPEND GATE — a THIN BINDING over the shared core gate
2
+ // (@open-everywhere/core/agent/spend/ingress-gate.ts), called at BOTH doors a Slack wake can arrive
3
+ // through (OPE-33).
4
+ //
5
+ // WHY THE GATE IS NOT INLINE IN `slack.ts` (live-diagnosed 2026-07-25). It used to be, inside
6
+ // `onInbound` — eve's `onAppMention`/`onDirectMessage` handler. That covers the MENTION door only.
7
+ // Since eve 0.27.5 the compiled `slackChannel` drops any message it considers self-authored BEFORE
8
+ // the mention hooks run:
9
+ //
10
+ // isSelfAuthoredSlackMessage(e,t){ return e.botUserId!==void 0 && t.author?.userId===e.botUserId
11
+ // ? !0 : e.appId!==void 0 && t.raw.app_id===e.appId }
12
+ //
13
+ // …applied to the `app_mention`/`direct_message` leg AND to eve's own `onMessage` leg — but NOT to
14
+ // the `onEvent` leg. Slack stamps `app_id` on ANY message an app posts through the Web API
15
+ // (including with a user token), so every driver-app-authored mention now bypasses `onInbound`
16
+ // entirely and re-enters through our engagement front route (slack-front.ts → slack-events.ts),
17
+ // which had no spend gate at all. Net effect: a capped surface silently kept spending.
18
+ //
19
+ // WHY THE IMPLEMENTATION NOW LIVES IN CORE. The fix was always structural, not version-specific: the
20
+ // gate is a WAKE-ADMISSION decision, so it belongs at every point a wake is about to dispatch a
21
+ // model turn — and the same defect then recurred on five other surfaces AND on the cross-surface
22
+ // window's own divert. The decision logic, the refusal copy, the admin-bypass semantics and the
23
+ // audit row therefore exist exactly once, in core, and every door — Slack's two, each other
24
+ // surface's native leg, and `divertToUnifiedWindow` — calls that one function. This module keeps
25
+ // only what is genuinely Slack-shaped: the mrkdwn shortcode badges (`:no_entry:` / `:warning:`,
26
+ // which no other surface renders) and the Slack-named type aliases its call sites use.
27
+ import { ingressSpendGate, SLACK_SPEND_BADGES, spendRefusalCopy as coreSpendRefusalCopy, spendBypassWarnCopy as coreSpendBypassWarnCopy } from "@open-everywhere/core/agent/adapter-api.js";
28
+ /** The refusal copy with Slack's mrkdwn shortcode badge — same sentences as every other surface
29
+ * (they live in core), only the leading emoji differs. The caller passes core's own input, so a
30
+ * Slack refusal names the breached layer and its key exactly as every other surface does
31
+ * (OPE-1860); nothing about the copy is decided here. */
32
+ export function spendRefusalCopy(input) {
33
+ return coreSpendRefusalCopy({ ...input, badge: SLACK_SPEND_BADGES.refusal });
34
+ }
35
+ /** The admin-bypass warning with Slack's mrkdwn shortcode badge. */
36
+ export function spendBypassWarnCopy(reason) {
37
+ return coreSpendBypassWarnCopy(reason, SLACK_SPEND_BADGES.warning);
38
+ }
39
+ /** Decide whether a Slack wake may spend — the shared decision table, bound to Slack's badges.
40
+ * See the core module for the full posture (check fails OPEN, a definite OVER fails CLOSED, an
41
+ * admin proceeds as audited sudo). */
42
+ export function slackSpendGate(input) {
43
+ const { teamId, ...rest } = input;
44
+ // `?? null` (never omitted): Slack's row has always carried a `team` key, present-but-null when
45
+ // the event carried no team id, and a live probe reads it that way.
46
+ return ingressSpendGate({ ...rest, workspaceRef: teamId ?? null, badges: SLACK_SPEND_BADGES });
47
+ }
48
+ //# sourceMappingURL=slack-spend-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-spend-gate.js","sourceRoot":"","sources":["../../../agent/channels/slack-spend-gate.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,oGAAoG;AACpG,oBAAoB;AACpB,EAAE;AACF,8FAA8F;AAC9F,mGAAmG;AACnG,mGAAmG;AACnG,yBAAyB;AACzB,EAAE;AACF,mGAAmG;AACnG,0DAA0D;AAC1D,EAAE;AACF,mGAAmG;AACnG,2FAA2F;AAC3F,+FAA+F;AAC/F,gGAAgG;AAChG,uFAAuF;AACvF,EAAE;AACF,qGAAqG;AACrG,gGAAgG;AAChG,iGAAiG;AACjG,gGAAgG;AAChG,4FAA4F;AAC5F,iGAAiG;AACjG,gGAAgG;AAChG,uFAAuF;AAEvF,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,gBAAgB,IAAI,oBAAoB,EAAE,mBAAmB,IAAI,uBAAuB,EAAkF,MAAM,4CAA4C,CAAC;AAa5Q;;;0DAG0D;AAC1D,MAAM,UAAU,gBAAgB,CAAC,KAA2C;IAC1E,OAAO,oBAAoB,CAAC,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,kBAAkB,CAAC,OAAO,EAAE,CAAC,CAAC;AAC/E,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,mBAAmB,CAAC,MAA0B;IAC5D,OAAO,uBAAuB,CAAC,MAAM,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAC;AACrE,CAAC;AAED;;uCAEuC;AACvC,MAAM,UAAU,cAAc,CAAC,KAA0B;IACvD,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC;IAClC,gGAAgG;IAChG,oEAAoE;IACpE,OAAO,gBAAgB,CAAC,EAAE,GAAG,IAAI,EAAE,YAAY,EAAE,MAAM,IAAI,IAAI,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC,CAAC;AACjG,CAAC"}
@@ -0,0 +1,61 @@
1
+ import { type TenantAdmissionDecision } from "@open-everywhere/core/agent/adapter-api.js";
2
+ /**
3
+ * Is this delivery's Slack tenant admitted on this deployment? `proceed` also covers every
4
+ * deliberate inert case (nothing declared, operator opted in, no ids on the event at all) — see
5
+ * access/tenant-admission.ts's classifyTenantAdmission for why each one is a choice rather than a
6
+ * gap. `unadmitted` means: drop the wake, write the audit row, run no model turn.
7
+ *
8
+ * šŸ”“ PASS THE ENTERPRISE ID WHERE THE DOOR CAN READ ONE (OPE-1645). An ORG-WIDE Enterprise Grid
9
+ * install delivers an interactivity payload that names the Grid (`E…`) and NO workspace, so a call
10
+ * that hands over the workspace alone asks the fence to judge a delivery on the one field it is
11
+ * missing — and the classifier's "a delivery naming no tenant proceeds" path then ADMITS a foreign
12
+ * Grid. A door with no enterprise id to read omits the argument and gets the workspace-only
13
+ * decision it had before.
14
+ */
15
+ export declare function slackTenantAdmission(teamId: string | undefined | null, enterpriseId?: string | undefined | null): TenantAdmissionDecision;
16
+ /** The workspace and Grid one INTERACTIVITY payload names. Either field is absent where the payload
17
+ * named none — see {@link slackInteractivityTenantIds} for which install shape omits which. */
18
+ export interface SlackInteractivityTenantIds {
19
+ readonly teamId?: string;
20
+ readonly enterpriseId?: string;
21
+ }
22
+ /**
23
+ * Read the tenant ids off an already-JSON-parsed INTERACTIVITY payload — a `block_actions` click, a
24
+ * `view_submission`, a shortcut. TOTAL AND PURE: any shape that names neither id yields `{}`, which
25
+ * {@link slackTenantAdmission} judges as "names no tenant".
26
+ *
27
+ * šŸ”“ THE GRID RUNG, IN ALL THREE SPELLINGS AN INTERACTIVITY PAYLOAD USES (OPE-1559). Which one
28
+ * arrives depends on how the app was installed, so reading fewer than three loses the rung on some
29
+ * installs — silently, and in the direction that ADMITS:
30
+ * • a top-level `enterprise` OBJECT (an org-wide install; `team` may then be null);
31
+ * • a top-level `enterprise_id` STRING;
32
+ * • `team: { id, enterprise_id }` — what a PER-WORKSPACE install on a Grid sends.
33
+ * Order is widest-first; the top-level object wins where more than one is present.
34
+ *
35
+ * šŸ”“ NO SINGLE PLACE IN eve READS ALL THREE, so this union is OURS and cannot be delegated.
36
+ * Re-measured against eve 0.54.3: `parseShortcutPayload` (public/channels/slack/interactions.js) reads
37
+ * NO enterprise at all; `extractInstallationFromInteractive`
38
+ * (compiled/@chat-adapter/slack/index.js) reads `enterprise?.id || enterprise_id`; and the adapter's
39
+ * event path reads the top-level `enterprise_id` alone. Three readers, three different subsets — and
40
+ * none of them reads the `team.enterprise_id` arm, which is why the union stays ours.
41
+ * āš ļø NOT RE-READ AT 0.54.3: the `enterprise.id || team.enterprise_id` shape this block used to
42
+ * attribute to the block_actions / view_submission parsers. It is not findable in the 0.54.3 bytes, so
43
+ * it is not restated as a measurement. The claim above does not rest on it.
44
+ *
45
+ * šŸ”“ `context_enterprise_id` IS DELIBERATELY NOT READ, though eve falls back to it. On a Slack
46
+ * Connect payload that field can name ANOTHER organisation, so trusting it would let a foreign Grid
47
+ * declaration vouch our room. `slackEnterpriseIdFromEnvelope` (slack-events.ts) makes the same
48
+ * exclusion on the event side. Every field below names the delivery's OWN workspace or Grid.
49
+ *
50
+ * šŸ”“ `user.team_id` IS NOT READ EITHER, and that omission is the point. It names the CLICKER's home
51
+ * workspace, which on a Grid is not the workspace the click happened in — so a fence built on it
52
+ * would judge the wrong tenant, in both directions. The question this fence asks is "which tenant
53
+ * DELIVERED this", and `team` / `enterprise` are the only fields that answer it.
54
+ *
55
+ * šŸ”“ IT LIVES HERE RATHER THAN AT A DOOR because the shortcut ingress and the six CLICK branches
56
+ * (OPE-1680) both need it, and a second copy of the reading rule at a second door is exactly the
57
+ * drift this module exists to prevent — the same argument that keeps {@link slackTenantAdmission}
58
+ * out of `slack-events.ts`.
59
+ */
60
+ export declare function slackInteractivityTenantIds(raw: unknown): SlackInteractivityTenantIds;
61
+ //# sourceMappingURL=slack-tenant-fence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-tenant-fence.d.ts","sourceRoot":"","sources":["../../../agent/channels/slack-tenant-fence.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAsD,KAAK,uBAAuB,EAAE,MAAM,4CAA4C,CAAC;AAE9I;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,uBAAuB,CAEzI;AAED;gGACgG;AAChG,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,OAAO,GAAG,2BAA2B,CAOrF"}
@@ -0,0 +1,88 @@
1
+ // SLACK'S TENANT FENCE WIRING (OPE-309 phase 0c) — one helper, every wake door.
2
+ //
3
+ // The decision itself is core's (access/tenant-admission.ts): pure, unit-tested, platform-neutral.
4
+ // This module is only the Slack-shaped call — "read the admitted set from config, classify this
5
+ // delivery's ids" — named ONCE so the mention door, the four engagement routes, the join door, the
6
+ // `/oe` slash door, the shortcut ingress and the DM window cannot drift apart on what "a tenant we
7
+ // admit" means. Exactly the shape `slackWakeSpendGate` uses for the spend question.
8
+ //
9
+ // WHY SLACK IS THE PLATFORM THAT NEEDS IT. Slack's Events API verifies an APP-LEVEL signing
10
+ // secret, which every workspace that installs the app shares — so `slack-front.ts` cannot tell a
11
+ // second workspace from the first, and there is no team allowlist anywhere else in the tree. The
12
+ // only thing bounding the deployment to one workspace is Slack's own app-distribution setting: a
13
+ // boundary we do not own. Slack is also the one platform in TENANT_SCOPED_PLATFORMS, i.e. the one
14
+ // whose person ids are issued per tenant, so a second workspace is precisely where two humans
15
+ // start sharing one `slack/<id>` principal key.
16
+ //
17
+ // This module lives OUTSIDE slack-events.ts on purpose: slack.ts's mention door needs it too, and
18
+ // a leaf module is how both get it without an import cycle.
19
+ import { classifyTenantAdmission, admittedTenantsFromConfig } from "@open-everywhere/core/agent/adapter-api.js";
20
+ /**
21
+ * Is this delivery's Slack tenant admitted on this deployment? `proceed` also covers every
22
+ * deliberate inert case (nothing declared, operator opted in, no ids on the event at all) — see
23
+ * access/tenant-admission.ts's classifyTenantAdmission for why each one is a choice rather than a
24
+ * gap. `unadmitted` means: drop the wake, write the audit row, run no model turn.
25
+ *
26
+ * šŸ”“ PASS THE ENTERPRISE ID WHERE THE DOOR CAN READ ONE (OPE-1645). An ORG-WIDE Enterprise Grid
27
+ * install delivers an interactivity payload that names the Grid (`E…`) and NO workspace, so a call
28
+ * that hands over the workspace alone asks the fence to judge a delivery on the one field it is
29
+ * missing — and the classifier's "a delivery naming no tenant proceeds" path then ADMITS a foreign
30
+ * Grid. A door with no enterprise id to read omits the argument and gets the workspace-only
31
+ * decision it had before.
32
+ */
33
+ export function slackTenantAdmission(teamId, enterpriseId) {
34
+ return classifyTenantAdmission({ platform: "slack", tenant: teamId, enterprise: enterpriseId, admitted: admittedTenantsFromConfig("slack") });
35
+ }
36
+ /**
37
+ * Read the tenant ids off an already-JSON-parsed INTERACTIVITY payload — a `block_actions` click, a
38
+ * `view_submission`, a shortcut. TOTAL AND PURE: any shape that names neither id yields `{}`, which
39
+ * {@link slackTenantAdmission} judges as "names no tenant".
40
+ *
41
+ * šŸ”“ THE GRID RUNG, IN ALL THREE SPELLINGS AN INTERACTIVITY PAYLOAD USES (OPE-1559). Which one
42
+ * arrives depends on how the app was installed, so reading fewer than three loses the rung on some
43
+ * installs — silently, and in the direction that ADMITS:
44
+ * • a top-level `enterprise` OBJECT (an org-wide install; `team` may then be null);
45
+ * • a top-level `enterprise_id` STRING;
46
+ * • `team: { id, enterprise_id }` — what a PER-WORKSPACE install on a Grid sends.
47
+ * Order is widest-first; the top-level object wins where more than one is present.
48
+ *
49
+ * šŸ”“ NO SINGLE PLACE IN eve READS ALL THREE, so this union is OURS and cannot be delegated.
50
+ * Re-measured against eve 0.54.3: `parseShortcutPayload` (public/channels/slack/interactions.js) reads
51
+ * NO enterprise at all; `extractInstallationFromInteractive`
52
+ * (compiled/@chat-adapter/slack/index.js) reads `enterprise?.id || enterprise_id`; and the adapter's
53
+ * event path reads the top-level `enterprise_id` alone. Three readers, three different subsets — and
54
+ * none of them reads the `team.enterprise_id` arm, which is why the union stays ours.
55
+ * āš ļø NOT RE-READ AT 0.54.3: the `enterprise.id || team.enterprise_id` shape this block used to
56
+ * attribute to the block_actions / view_submission parsers. It is not findable in the 0.54.3 bytes, so
57
+ * it is not restated as a measurement. The claim above does not rest on it.
58
+ *
59
+ * šŸ”“ `context_enterprise_id` IS DELIBERATELY NOT READ, though eve falls back to it. On a Slack
60
+ * Connect payload that field can name ANOTHER organisation, so trusting it would let a foreign Grid
61
+ * declaration vouch our room. `slackEnterpriseIdFromEnvelope` (slack-events.ts) makes the same
62
+ * exclusion on the event side. Every field below names the delivery's OWN workspace or Grid.
63
+ *
64
+ * šŸ”“ `user.team_id` IS NOT READ EITHER, and that omission is the point. It names the CLICKER's home
65
+ * workspace, which on a Grid is not the workspace the click happened in — so a fence built on it
66
+ * would judge the wrong tenant, in both directions. The question this fence asks is "which tenant
67
+ * DELIVERED this", and `team` / `enterprise` are the only fields that answer it.
68
+ *
69
+ * šŸ”“ IT LIVES HERE RATHER THAN AT A DOOR because the shortcut ingress and the six CLICK branches
70
+ * (OPE-1680) both need it, and a second copy of the reading rule at a second door is exactly the
71
+ * drift this module exists to prevent — the same argument that keeps {@link slackTenantAdmission}
72
+ * out of `slack-events.ts`.
73
+ */
74
+ export function slackInteractivityTenantIds(raw) {
75
+ const p = asObject(raw);
76
+ if (p === undefined)
77
+ return {};
78
+ const teamId = asNonEmptyString(asObject(p.team)?.id);
79
+ const enterpriseId = asNonEmptyString(asObject(p.enterprise)?.id) ?? asNonEmptyString(p.enterprise_id) ?? asNonEmptyString(asObject(p.team)?.enterprise_id);
80
+ return { ...(teamId === undefined ? {} : { teamId }), ...(enterpriseId === undefined ? {} : { enterpriseId }) };
81
+ }
82
+ function asObject(v) {
83
+ return typeof v === "object" && v !== null ? v : undefined;
84
+ }
85
+ function asNonEmptyString(v) {
86
+ return typeof v === "string" && v.length > 0 ? v : undefined;
87
+ }
88
+ //# sourceMappingURL=slack-tenant-fence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-tenant-fence.js","sourceRoot":"","sources":["../../../agent/channels/slack-tenant-fence.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,EAAE;AACF,mGAAmG;AACnG,gGAAgG;AAChG,mGAAmG;AACnG,mGAAmG;AACnG,oFAAoF;AACpF,EAAE;AACF,4FAA4F;AAC5F,iGAAiG;AACjG,iGAAiG;AACjG,iGAAiG;AACjG,kGAAkG;AAClG,8FAA8F;AAC9F,gDAAgD;AAChD,EAAE;AACF,kGAAkG;AAClG,4DAA4D;AAE5D,OAAO,EAAE,uBAAuB,EAAE,yBAAyB,EAAgC,MAAM,4CAA4C,CAAC;AAE9I;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAiC,EAAE,YAAwC;IAC9G,OAAO,uBAAuB,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,yBAAyB,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;AAChJ,CAAC;AASD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,UAAU,2BAA2B,CAAC,GAAY;IACtD,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IACxB,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAC/B,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;IACtD,MAAM,YAAY,GAChB,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC,IAAI,gBAAgB,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,CAAC;IACzI,OAAO,EAAE,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;AAClH,CAAC;AAED,SAAS,QAAQ,CAAC,CAAU;IAC1B,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAE,CAA6B,CAAC,CAAC,CAAC,SAAS,CAAC;AAC1F,CAAC;AAED,SAAS,gBAAgB,CAAC,CAAU;IAClC,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/D,CAAC"}