@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.
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +47 -0
- package/agent/access/slack-attachment-fetch.ts +168 -0
- package/agent/app-manifest.ts +149 -0
- package/agent/channels/slack-approval-card.ts +275 -0
- package/agent/channels/slack-attachments.ts +195 -0
- package/agent/channels/slack-commands.ts +227 -0
- package/agent/channels/slack-context.ts +185 -0
- package/agent/channels/slack-events.ts +5406 -0
- package/agent/channels/slack-front.ts +2349 -0
- package/agent/channels/slack-identity.ts +520 -0
- package/agent/channels/slack-input-response.ts +162 -0
- package/agent/channels/slack-mrkdwn.ts +61 -0
- package/agent/channels/slack-origin-codec.ts +62 -0
- package/agent/channels/slack-post-arm.ts +314 -0
- package/agent/channels/slack-read.ts +248 -0
- package/agent/channels/slack-route-hold.ts +107 -0
- package/agent/channels/slack-shortcut.ts +336 -0
- package/agent/channels/slack-signature.ts +53 -0
- package/agent/channels/slack-spend-gate.ts +62 -0
- package/agent/channels/slack-tenant-fence.ts +99 -0
- package/agent/channels/slack-wake-backscroll.ts +604 -0
- package/agent/channels/slack-web-api.ts +178 -0
- package/agent/channels/slack-webhook-verifier.ts +116 -0
- package/agent/channels/slack.ts +1266 -0
- package/agent/contributions.ts +32 -0
- package/agent/docs-generated.ts +15 -0
- package/agent/ops.ts +50 -0
- package/agent/provider-events.ts +91 -0
- package/agent/register.ts +144 -0
- package/agent/surface-parity.ts +99 -0
- package/dist/agent/access/slack-attachment-fetch.d.ts +15 -0
- package/dist/agent/access/slack-attachment-fetch.d.ts.map +1 -0
- package/dist/agent/access/slack-attachment-fetch.js +151 -0
- package/dist/agent/access/slack-attachment-fetch.js.map +1 -0
- package/dist/agent/app-manifest.d.ts +15 -0
- package/dist/agent/app-manifest.d.ts.map +1 -0
- package/dist/agent/app-manifest.js +141 -0
- package/dist/agent/app-manifest.js.map +1 -0
- package/dist/agent/channels/slack-approval-card.d.ts +71 -0
- package/dist/agent/channels/slack-approval-card.d.ts.map +1 -0
- package/dist/agent/channels/slack-approval-card.js +232 -0
- package/dist/agent/channels/slack-approval-card.js.map +1 -0
- package/dist/agent/channels/slack-attachments.d.ts +66 -0
- package/dist/agent/channels/slack-attachments.d.ts.map +1 -0
- package/dist/agent/channels/slack-attachments.js +174 -0
- package/dist/agent/channels/slack-attachments.js.map +1 -0
- package/dist/agent/channels/slack-commands.d.ts +27 -0
- package/dist/agent/channels/slack-commands.d.ts.map +1 -0
- package/dist/agent/channels/slack-commands.js +208 -0
- package/dist/agent/channels/slack-commands.js.map +1 -0
- package/dist/agent/channels/slack-context.d.ts +26 -0
- package/dist/agent/channels/slack-context.d.ts.map +1 -0
- package/dist/agent/channels/slack-context.js +178 -0
- package/dist/agent/channels/slack-context.js.map +1 -0
- package/dist/agent/channels/slack-events.d.ts +1654 -0
- package/dist/agent/channels/slack-events.d.ts.map +1 -0
- package/dist/agent/channels/slack-events.js +4240 -0
- package/dist/agent/channels/slack-events.js.map +1 -0
- package/dist/agent/channels/slack-front.d.ts +387 -0
- package/dist/agent/channels/slack-front.d.ts.map +1 -0
- package/dist/agent/channels/slack-front.js +1958 -0
- package/dist/agent/channels/slack-front.js.map +1 -0
- package/dist/agent/channels/slack-identity.d.ts +245 -0
- package/dist/agent/channels/slack-identity.d.ts.map +1 -0
- package/dist/agent/channels/slack-identity.js +405 -0
- package/dist/agent/channels/slack-identity.js.map +1 -0
- package/dist/agent/channels/slack-input-response.d.ts +26 -0
- package/dist/agent/channels/slack-input-response.d.ts.map +1 -0
- package/dist/agent/channels/slack-input-response.js +127 -0
- package/dist/agent/channels/slack-input-response.js.map +1 -0
- package/dist/agent/channels/slack-mrkdwn.d.ts +3 -0
- package/dist/agent/channels/slack-mrkdwn.d.ts.map +1 -0
- package/dist/agent/channels/slack-mrkdwn.js +53 -0
- package/dist/agent/channels/slack-mrkdwn.js.map +1 -0
- package/dist/agent/channels/slack-origin-codec.d.ts +7 -0
- package/dist/agent/channels/slack-origin-codec.d.ts.map +1 -0
- package/dist/agent/channels/slack-origin-codec.js +55 -0
- package/dist/agent/channels/slack-origin-codec.js.map +1 -0
- package/dist/agent/channels/slack-post-arm.d.ts +54 -0
- package/dist/agent/channels/slack-post-arm.d.ts.map +1 -0
- package/dist/agent/channels/slack-post-arm.js +268 -0
- package/dist/agent/channels/slack-post-arm.js.map +1 -0
- package/dist/agent/channels/slack-read.d.ts +84 -0
- package/dist/agent/channels/slack-read.d.ts.map +1 -0
- package/dist/agent/channels/slack-read.js +205 -0
- package/dist/agent/channels/slack-read.js.map +1 -0
- package/dist/agent/channels/slack-route-hold.d.ts +34 -0
- package/dist/agent/channels/slack-route-hold.d.ts.map +1 -0
- package/dist/agent/channels/slack-route-hold.js +97 -0
- package/dist/agent/channels/slack-route-hold.js.map +1 -0
- package/dist/agent/channels/slack-shortcut.d.ts +162 -0
- package/dist/agent/channels/slack-shortcut.d.ts.map +1 -0
- package/dist/agent/channels/slack-shortcut.js +262 -0
- package/dist/agent/channels/slack-shortcut.js.map +1 -0
- package/dist/agent/channels/slack-signature.d.ts +19 -0
- package/dist/agent/channels/slack-signature.d.ts.map +1 -0
- package/dist/agent/channels/slack-signature.js +50 -0
- package/dist/agent/channels/slack-signature.js.map +1 -0
- package/dist/agent/channels/slack-spend-gate.d.ts +22 -0
- package/dist/agent/channels/slack-spend-gate.d.ts.map +1 -0
- package/dist/agent/channels/slack-spend-gate.js +48 -0
- package/dist/agent/channels/slack-spend-gate.js.map +1 -0
- package/dist/agent/channels/slack-tenant-fence.d.ts +61 -0
- package/dist/agent/channels/slack-tenant-fence.d.ts.map +1 -0
- package/dist/agent/channels/slack-tenant-fence.js +88 -0
- package/dist/agent/channels/slack-tenant-fence.js.map +1 -0
- package/dist/agent/channels/slack-wake-backscroll.d.ts +188 -0
- package/dist/agent/channels/slack-wake-backscroll.d.ts.map +1 -0
- package/dist/agent/channels/slack-wake-backscroll.js +473 -0
- package/dist/agent/channels/slack-wake-backscroll.js.map +1 -0
- package/dist/agent/channels/slack-web-api.d.ts +46 -0
- package/dist/agent/channels/slack-web-api.d.ts.map +1 -0
- package/dist/agent/channels/slack-web-api.js +147 -0
- package/dist/agent/channels/slack-web-api.js.map +1 -0
- package/dist/agent/channels/slack-webhook-verifier.d.ts +5 -0
- package/dist/agent/channels/slack-webhook-verifier.d.ts.map +1 -0
- package/dist/agent/channels/slack-webhook-verifier.js +112 -0
- package/dist/agent/channels/slack-webhook-verifier.js.map +1 -0
- package/dist/agent/channels/slack.d.ts +37 -0
- package/dist/agent/channels/slack.d.ts.map +1 -0
- package/dist/agent/channels/slack.js +1169 -0
- package/dist/agent/channels/slack.js.map +1 -0
- package/dist/agent/contributions.d.ts +17 -0
- package/dist/agent/contributions.d.ts.map +1 -0
- package/dist/agent/contributions.js +32 -0
- package/dist/agent/contributions.js.map +1 -0
- package/dist/agent/docs-generated.d.ts +3 -0
- package/dist/agent/docs-generated.d.ts.map +1 -0
- package/dist/agent/docs-generated.js +13 -0
- package/dist/agent/docs-generated.js.map +1 -0
- package/dist/agent/ops.d.ts +3 -0
- package/dist/agent/ops.d.ts.map +1 -0
- package/dist/agent/ops.js +38 -0
- package/dist/agent/ops.js.map +1 -0
- package/dist/agent/provider-events.d.ts +3 -0
- package/dist/agent/provider-events.d.ts.map +1 -0
- package/dist/agent/provider-events.js +84 -0
- package/dist/agent/provider-events.js.map +1 -0
- package/dist/agent/register.d.ts +5 -0
- package/dist/agent/register.d.ts.map +1 -0
- package/dist/agent/register.js +118 -0
- package/dist/agent/register.js.map +1 -0
- package/dist/agent/surface-parity.d.ts +3 -0
- package/dist/agent/surface-parity.d.ts.map +1 -0
- package/dist/agent/surface-parity.js +95 -0
- package/dist/agent/surface-parity.js.map +1 -0
- package/package.json +209 -0
- package/tsconfig.build.json +22 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// THE SLACK RETURN LEG FOR A BRIDGED APPROVAL CARD (OPE-1720) — what makes the button the unified
|
|
2
|
+
// post arm now renders actually settle the approval it names.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS RATHER THAN EVE'S OWN HITL ROUTE. eve's Slack channel has an interactivity seam for
|
|
5
|
+
// its own `eve_input:` cards, and `slack-input-response.ts` is OE's admission on it. That seam cannot
|
|
6
|
+
// serve this card, and the reason is structural rather than a not-yet: eve's own handler stores the
|
|
7
|
+
// card it minted in the SLACK channel's state (`defaults.js` writes `state.pendingApprovalCards`) and
|
|
8
|
+
// resolves the click against that channel's session address. A DM seat's parked request lives on the
|
|
9
|
+
// UNIFIED window — another channel, another session, another state bag — so a click on a card OE
|
|
10
|
+
// posted there has nothing for eve's route to resolve. It is OE's card, so it is OE's click.
|
|
11
|
+
//
|
|
12
|
+
// 🔴 THE SAME ADMISSION, THOUGH. `admitInputResponse` (core approvals/input-response-floor.ts) is the
|
|
13
|
+
// ONE decision both HITL answer doors take, and `ingressSpendGate` with door `"answer"` is the same
|
|
14
|
+
// spend floor `admitSlackInputResponse` consults FIRST for the same reason: a tap RESUMES a parked
|
|
15
|
+
// turn, which is a decision to spend more on a scope nobody asked whether it could afford. This
|
|
16
|
+
// module adds no floor of its own — it resolves the click, asks those two, and delivers.
|
|
17
|
+
//
|
|
18
|
+
// 🔴 A REFUSAL LEAVES THE CARD LIVE. Nothing here consumes a row on any outcome but a delivered
|
|
19
|
+
// answer: the approval stays parked, the card stays tappable for somebody who may answer it, and the
|
|
20
|
+
// question stays answerable IN WORDS through the text leg (`question-bridge.ts`), which is untouched.
|
|
21
|
+
// That is the same "a refusal is not a drop" posture the text leg holds.
|
|
22
|
+
import { admitInputResponse, APPROVAL_BUTTON_LABELS, APPROVAL_CARD_REFUSED_AUDIT_ACTION as CORE_APPROVAL_CARD_REFUSED_AUDIT_ACTION, APPROVAL_CARD_SETTLED_AUDIT_ACTION as CORE_APPROVAL_CARD_SETTLED_AUDIT_ACTION, auditApprovalCardTap, deploymentOrg, forgetApprovalCardTicket, forgetBridgedQuestion, getOperationalStore, ingressSpendGate, parseApprovalClickId, readBridgedQuestion, readApprovalCardTicket, resolveApprovalCardClick, resolvePartition, unifiedThreadRef, SLACK_SPEND_BADGES, } from "@open-everywhere/core/agent/adapter-api.js";
|
|
23
|
+
import { slackClickerIsGuest, slackForm } from "./slack-events.js";
|
|
24
|
+
const log = (m) => console.log(`[open-everywhere:slack] approval card: ${m}`);
|
|
25
|
+
/** The `input_response.routed` sibling for a tap: an approval settled through a BUTTON rather than a
|
|
26
|
+
* typed reply. RE-EXPORTED FROM CORE, never re-spelled here — one action name for every surface, so
|
|
27
|
+
* a scorer that joins on the row cannot under-count one of them (OPE-1856). */
|
|
28
|
+
export const APPROVAL_CARD_SETTLED_AUDIT_ACTION = CORE_APPROVAL_CARD_SETTLED_AUDIT_ACTION;
|
|
29
|
+
/** A verified tap that settled nothing, carrying WHICH refusal it was. Core-owned, as above. */
|
|
30
|
+
export const APPROVAL_CARD_REFUSED_AUDIT_ACTION = CORE_APPROVAL_CARD_REFUSED_AUDIT_ACTION;
|
|
31
|
+
/**
|
|
32
|
+
* Read the one `oe_input:` action out of a verified `block_actions` payload. `undefined` when the
|
|
33
|
+
* payload is not one of ours or is missing a field the decision needs — the front route then leaves
|
|
34
|
+
* it alone rather than half-handling it.
|
|
35
|
+
*
|
|
36
|
+
* THE FIRST `oe_input:` ACTION WINS and the rest are ignored: Slack sends one action per click, and a
|
|
37
|
+
* payload carrying two of ours is not a shape this host mints.
|
|
38
|
+
*/
|
|
39
|
+
export function parseSlackApprovalClick(payload) {
|
|
40
|
+
if (typeof payload !== "object" || payload === null)
|
|
41
|
+
return undefined;
|
|
42
|
+
const p = payload;
|
|
43
|
+
if (!Array.isArray(p.actions))
|
|
44
|
+
return undefined;
|
|
45
|
+
let actionId;
|
|
46
|
+
for (const a of p.actions) {
|
|
47
|
+
const id = typeof a === "object" && a !== null ? a.action_id : undefined;
|
|
48
|
+
if (typeof id === "string" && parseApprovalClickId(id) !== undefined) {
|
|
49
|
+
actionId = id;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (actionId === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
const channel = typeof p.channel === "object" && p.channel !== null ? p.channel.id : undefined;
|
|
56
|
+
const user = typeof p.user === "object" && p.user !== null ? p.user.id : undefined;
|
|
57
|
+
const team = typeof p.team === "object" && p.team !== null ? p.team.id : undefined;
|
|
58
|
+
if (typeof channel !== "string" || channel.length === 0)
|
|
59
|
+
return undefined;
|
|
60
|
+
if (typeof user !== "string" || user.length === 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
return { actionId, channelId: channel, userId: user, ...(typeof team === "string" && team.length > 0 ? { teamId: team } : {}) };
|
|
63
|
+
}
|
|
64
|
+
/** The bindable a partition resolve needs from a raw interactivity payload. The team id is the ONE
|
|
65
|
+
* attribute that matters here, and it is read off the wire rather than through a placeholder — the
|
|
66
|
+
* same reading, and the same reason, as `slackAuthTeamId` in slack-input-response.ts. */
|
|
67
|
+
function bindableForClick(click) {
|
|
68
|
+
return {
|
|
69
|
+
channel: { kind: "slack" },
|
|
70
|
+
session: { auth: { current: { attributes: { ...(click.teamId ? { team_id: click.teamId } : {}), channel_id: click.channelId } } } },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The host copy that rides the delivered answer's `text` — the LABEL THIS DEPLOYMENT PUT ON THE
|
|
75
|
+
* BUTTON, read back out of the same allowlist that minted it, so the transcript says what the person
|
|
76
|
+
* actually chose. A tap carries no words, and writing the person's prose for them is not an option.
|
|
77
|
+
* The fallback capitalises the eve-minted option id, which can only be reached by a row written
|
|
78
|
+
* before an id left the allowlist; the DECISION is never this value, because eve settles on
|
|
79
|
+
* `optionId` alone.
|
|
80
|
+
*/
|
|
81
|
+
function approvalAnswerText(optionId) {
|
|
82
|
+
return APPROVAL_BUTTON_LABELS.get(optionId.toLowerCase())?.label ?? optionId.charAt(0).toUpperCase() + optionId.slice(1);
|
|
83
|
+
}
|
|
84
|
+
/** Show the tapper alone why nothing happened. EPHEMERAL, never a message in the room: the card is
|
|
85
|
+
* still live for somebody who may answer it, and a public "you may not answer" note tells the room
|
|
86
|
+
* nothing it needs. Best-effort — a failed notice never changes the verdict. */
|
|
87
|
+
async function notifyTapper(channelId, userId, text) {
|
|
88
|
+
try {
|
|
89
|
+
await slackForm("chat.postEphemeral", new URLSearchParams({ channel: channelId, user: userId, text }).toString());
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
log(`refusal notice failed to post: ${String(err)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Handle one verified `oe_input:` click: resolve it, admit it, deliver the answer to eve, consume the
|
|
97
|
+
* rows. NEVER THROWS — the front route has already 200'd the interaction, and an unhandled throw here
|
|
98
|
+
* would only lose the log line.
|
|
99
|
+
*
|
|
100
|
+
* 🔴 THE ANSWER TEXT IS HOST COPY, and it must be. A tap carries no words, so a transcript that read
|
|
101
|
+
* the button as the person's own prose would be inventing their words. What rides is the host label
|
|
102
|
+
* this deployment put on that button (`Approve` / `Cancel`), which is what the person actually chose,
|
|
103
|
+
* and the `optionId` beside it is what eve settles on — `resolveApprovalOutcome` reads `optionId`
|
|
104
|
+
* ALONE, so the text is transcript only and never the decision.
|
|
105
|
+
*
|
|
106
|
+
* 🔴 CONSUME-ONCE, AND BOTH ROWS. The ticket goes so a second tap on the same card cannot answer a
|
|
107
|
+
* request eve has already settled; the outstanding-question row goes so the room's NEXT message is an
|
|
108
|
+
* ordinary wake again rather than a second answer to a settled request. Both happen only after eve
|
|
109
|
+
* accepted the delivery, exactly as the text leg orders them.
|
|
110
|
+
*
|
|
111
|
+
* THE LIMIT OF THAT, stated rather than implied: the read and the delete are not one atomic step, and
|
|
112
|
+
* the delete runs only AFTER `respond` returns, so anything arriving while the first tap is still in
|
|
113
|
+
* flight resolves against rows that are both still present. Two such deliveries each spend an
|
|
114
|
+
* admission, each call `respond`, and each can write a settled audit row — so one approval can read
|
|
115
|
+
* as decided by two taps. It is not a second DECISION: both had to be admitted and both had to name
|
|
116
|
+
* an option the request offered, and eve's own resolution refuses the second delivery for a request
|
|
117
|
+
* it already settled. A delivery that arrives after the first settle RETURNED is the covered case —
|
|
118
|
+
* the ticket is gone and it resolves `no-ticket`.
|
|
119
|
+
*
|
|
120
|
+
* 🔴 A SECOND DELIVERY IS MOST LIKELY TO BE SLACK'S OWN, NOT A SECOND PERSON, and this handler does
|
|
121
|
+
* not ack until the settle returns. Slack expects an interactivity endpoint to answer within about
|
|
122
|
+
* three seconds; what it does past that — show the tapper a connection error, redeliver, or both —
|
|
123
|
+
* is VENDOR BEHAVIOUR THIS TREE HAS NOT MEASURED, so do not read the bound above as covering it.
|
|
124
|
+
* Acking first and settling in the background would remove the delay and widen exactly the window
|
|
125
|
+
* this paragraph describes; the ordering here takes the narrower window instead. Whoever measures
|
|
126
|
+
* the real behaviour should revisit that trade, with the measurement written down.
|
|
127
|
+
*/
|
|
128
|
+
export async function handleSlackApprovalCardClick(payload, respond) {
|
|
129
|
+
// 🔴 HOISTED OUT OF THE `try` SO THE `catch` CAN STAMP ITS ROW (OPE-1856). A throw anywhere below
|
|
130
|
+
// — including from the partition resolve itself — must still leave a row saying this deployment
|
|
131
|
+
// took a tap and settled nothing; core's auditor accepts an absent partition for that case.
|
|
132
|
+
let tapper = "";
|
|
133
|
+
let auditPartition;
|
|
134
|
+
try {
|
|
135
|
+
const click = parseSlackApprovalClick(payload);
|
|
136
|
+
if (click === undefined)
|
|
137
|
+
return { handled: false, reason: "not an oe_input: click" };
|
|
138
|
+
const parsed = parseApprovalClickId(click.actionId);
|
|
139
|
+
if (parsed === undefined)
|
|
140
|
+
return { handled: false, reason: "oe_input: action id did not parse" };
|
|
141
|
+
tapper = click.userId;
|
|
142
|
+
const org = deploymentOrg();
|
|
143
|
+
const store = getOperationalStore();
|
|
144
|
+
const partition = resolvePartition(bindableForClick(click));
|
|
145
|
+
auditPartition = partition;
|
|
146
|
+
// THE SPEND GATE FIRST, ahead of the identity admission — the same order and the same reason
|
|
147
|
+
// `admitSlackInputResponse` takes: over budget outranks identity, and a decline must not spend a
|
|
148
|
+
// roster lookup. Its refusal copy is its own; a decline here leaves the request PENDING.
|
|
149
|
+
const spend = await ingressSpendGate({
|
|
150
|
+
bindable: bindableForClick(click),
|
|
151
|
+
door: "answer",
|
|
152
|
+
principalId: click.userId,
|
|
153
|
+
workspaceRef: click.teamId ?? null,
|
|
154
|
+
badges: SLACK_SPEND_BADGES,
|
|
155
|
+
post: async (text) => {
|
|
156
|
+
await notifyTapper(click.channelId, click.userId, text);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
if (spend === "declined") {
|
|
160
|
+
// AUDITED, not only logged (OPE-1856): a decline is this deployment refusing a person's tap,
|
|
161
|
+
// and a refusal nobody can count is indistinguishable from a tap that never arrived.
|
|
162
|
+
await auditApprovalCardTap({ platform: "slack", principalId: click.userId, partition, outcome: { kind: "refused", reason: "spend-declined" } });
|
|
163
|
+
log(`tap declined by the spend gate — the approval stays parked`);
|
|
164
|
+
return { handled: true, reason: "spend-declined" };
|
|
165
|
+
}
|
|
166
|
+
// The room the tap came from, in the SAME vocabulary the card's ticket stored — derived through
|
|
167
|
+
// core's own codec rather than spelled here, so the two sides cannot drift on the key's shape.
|
|
168
|
+
const containerKey = unifiedThreadRef({ platform: "slack", channelId: click.channelId }).channelKey;
|
|
169
|
+
const routing = await resolveApprovalCardClick({ ticket: parsed.ticket, optionId: parsed.optionId, containerKey }, {
|
|
170
|
+
read: (ticket) => readApprovalCardTicket(store, org, ticket),
|
|
171
|
+
// The join with the TEXT leg — see the dep's own note. Fail-CLOSED on an unreadable row: a
|
|
172
|
+
// store this leg cannot read cannot say the request is still parked, and settling on that
|
|
173
|
+
// guess is the one direction that spends a governed decision twice.
|
|
174
|
+
stillOutstanding: async (bareToken, requestId) => {
|
|
175
|
+
const outstanding = await readBridgedQuestion(store, org, bareToken).catch(() => undefined);
|
|
176
|
+
return outstanding?.requestId === requestId;
|
|
177
|
+
},
|
|
178
|
+
admit: async () => {
|
|
179
|
+
// Leg B, the same positive guest verdict both other clicker floors use, and inert on an
|
|
180
|
+
// unreadable probe (`undefined` never refuses).
|
|
181
|
+
const answererIsGuest = await slackClickerIsGuest(click.userId, click.teamId).catch(() => undefined);
|
|
182
|
+
const verdict = await admitInputResponse({
|
|
183
|
+
org: partition.org,
|
|
184
|
+
platform: partition.platform,
|
|
185
|
+
workspace: partition.workspace,
|
|
186
|
+
surface: partition.surface,
|
|
187
|
+
surfaceKey: partition.surfaceKey,
|
|
188
|
+
principalId: click.userId,
|
|
189
|
+
responses: 1,
|
|
190
|
+
...(answererIsGuest === undefined ? {} : { answererIsGuest }),
|
|
191
|
+
});
|
|
192
|
+
return { ok: verdict.ok, floor: verdict.ok ? verdict.floor : "", notice: verdict.ok ? "" : verdict.notice };
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
if (routing.kind === "refuse") {
|
|
196
|
+
await auditApprovalCardTap({ platform: "slack", principalId: click.userId, partition, outcome: { kind: "refused", reason: routing.reason } });
|
|
197
|
+
if (routing.notice !== undefined)
|
|
198
|
+
await notifyTapper(click.channelId, click.userId, routing.notice);
|
|
199
|
+
log(`tap settled nothing (${routing.reason}) — the approval stays parked and answerable in words`);
|
|
200
|
+
return { handled: true, reason: routing.reason, ...(routing.notice ? { notice: routing.notice } : {}) };
|
|
201
|
+
}
|
|
202
|
+
const label = approvalAnswerText(routing.optionId);
|
|
203
|
+
// 🔴 DELIVERED BY SESSION ID, never by the window token (OPE-1863): a continuation token is
|
|
204
|
+
// channel-local, and this route is the SLACK channel's, so `from(bareToken)` here named a
|
|
205
|
+
// session that never existed and the settle threw into the catch below. The token's job is the
|
|
206
|
+
// outstanding-question row on the two lines that follow.
|
|
207
|
+
await respond(routing.sessionId, { requestId: routing.requestId, optionId: routing.optionId, text: label });
|
|
208
|
+
// CONSUME-ONCE, and only now that eve accepted the delivery. A throw above skips both, which
|
|
209
|
+
// leaves the card tappable and the question answerable — the safe direction.
|
|
210
|
+
await forgetApprovalCardTicket(store, org, parsed.ticket);
|
|
211
|
+
await forgetBridgedQuestion(store, org, routing.bareToken);
|
|
212
|
+
await auditApprovalCardTap({
|
|
213
|
+
platform: "slack",
|
|
214
|
+
principalId: click.userId,
|
|
215
|
+
partition,
|
|
216
|
+
outcome: { kind: "settled", requestId: routing.requestId, optionId: routing.optionId, floor: routing.floor },
|
|
217
|
+
});
|
|
218
|
+
log(`tap settled ${routing.requestId} as "${routing.optionId}" (floor ${routing.floor})`);
|
|
219
|
+
return { handled: true, reason: "settled" };
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
// The approval is still parked and the card is still tappable, so the honest outcome is to say so
|
|
223
|
+
// and leave both rows in place. Never a throw: this runs after the interaction was acknowledged.
|
|
224
|
+
// AUDITED, not only logged (OPE-1856): ship #212's S-EVE-CARD leg could not say whether its tap
|
|
225
|
+
// had been refused or had thrown, because neither arm left a row. `auditApprovalCardTap` never
|
|
226
|
+
// throws, so this cannot turn a failed tap into a failed response.
|
|
227
|
+
await auditApprovalCardTap({ platform: "slack", principalId: tapper, ...(auditPartition === undefined ? {} : { partition: auditPartition }), outcome: { kind: "refused", reason: "error" } });
|
|
228
|
+
log(`tap failed (${err?.message ?? String(err)}) — nothing was settled, the approval stays parked`);
|
|
229
|
+
return { handled: true, reason: "error" };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=slack-approval-card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-approval-card.js","sourceRoot":"","sources":["../../../agent/channels/slack-approval-card.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** A resolved attachment record — its `urlPrivate` came from a Slack event's `files[].url_private`
|
|
2
|
+
* (trusted provenance), never a model-supplied string. */
|
|
3
|
+
export interface SlackAttachmentRecord {
|
|
4
|
+
/** The Slack `url_private` download URL from the event's `files[]`. */
|
|
5
|
+
readonly urlPrivate: string;
|
|
6
|
+
/** IANA media type from the event (`files[].mimetype`), when known — describe_image uses it
|
|
7
|
+
* because the id-based ref carries no filename extension to infer from. */
|
|
8
|
+
readonly mimeType?: string;
|
|
9
|
+
/** Original filename from the event, when known — the raw-bytes read path (read_file_upload)
|
|
10
|
+
* infers a type from its extension when the mimetype is absent. */
|
|
11
|
+
readonly name?: string;
|
|
12
|
+
/** The Slack team id the event belonged to (for future multi-bot token routing). */
|
|
13
|
+
readonly teamId?: string;
|
|
14
|
+
/** The platform that minted this attachment id — always "slack" for records written here. Stamped
|
|
15
|
+
* at ingest so the attachment plane's provenance is explicit end to end (channels/attachments.ts). */
|
|
16
|
+
readonly platform?: "slack";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Record an image attachment's trusted `url_private` under its Slack file id. THIS LIST IS THE
|
|
20
|
+
* AUDIT LIST for "who may write a fetchable url_private into the registry", so keep it exact.
|
|
21
|
+
* Two writers, both of trusted provenance:
|
|
22
|
+
* • `parseMessageEvent` (slack-events.ts) — an Events API message, which only arrives for a room
|
|
23
|
+
* the app is subscribed to and in;
|
|
24
|
+
* • `registerAttachments` in the front route's SHORTCUT branch (slack-front.ts) — a message
|
|
25
|
+
* shortcut, which Slack delivers even for a room the app is NOT in. It therefore registers
|
|
26
|
+
* only what that branch's readability and wake gates admitted (OPE-1442). The shortcut PARSER
|
|
27
|
+
* registers nothing: it runs at classification time, in front of those gates.
|
|
28
|
+
* A falsy id or an empty url is ignored (nothing to resolve later). Re-registering an id refreshes
|
|
29
|
+
* its recency.
|
|
30
|
+
*/
|
|
31
|
+
export declare function registerSlackAttachment(fileId: string | undefined, rec: SlackAttachmentRecord): void;
|
|
32
|
+
/** The trusted record for a Slack file id, or undefined when this session never saw an event
|
|
33
|
+
* carrying it (→ describe_image fails closed rather than fetching an arbitrary URL). PROCESS-LOCAL:
|
|
34
|
+
* a cross-process worker must go through {@link resolveSlackAttachmentDurable}. */
|
|
35
|
+
export declare function resolveSlackAttachment(fileId: string): SlackAttachmentRecord | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Mirror one event attachment into the DURABLE store (task #107) so a delegated worker in a
|
|
38
|
+
* SEPARATE process can resolve it. Called ONLY from the trusted parse-consumption path (slack-front),
|
|
39
|
+
* with `org` resolved from the same event — never a model-supplied string. Best-effort: a store
|
|
40
|
+
* failure must never break the wake (the in-memory path still serves the same-process seat), so it
|
|
41
|
+
* swallows and logs. `org` defaults to the deployment org (single-tenant) when omitted.
|
|
42
|
+
*/
|
|
43
|
+
export declare function persistSlackAttachment(fileId: string | undefined, rec: SlackAttachmentRecord, org?: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a file id ACROSS processes: the in-memory registry first (same-process fast path), then
|
|
46
|
+
* the durable store (the cross-process bridge a delegated worker needs). Returns undefined when
|
|
47
|
+
* neither knows the id (→ the caller fails closed rather than fetching an arbitrary URL). `org`
|
|
48
|
+
* defaults to the deployment org (single-tenant) when omitted.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveSlackAttachmentDurable(fileId: string, org?: string): Promise<SlackAttachmentRecord | undefined>;
|
|
51
|
+
/**
|
|
52
|
+
* Register Slack as an attachment-plane resolver (channels/attachments.ts) — the neutral seam
|
|
53
|
+
* describe_image consults so its contract carries no per-platform union arm. The resolver wraps the
|
|
54
|
+
* SAME two guarantees the in-memory/durable registry + host-pinned fetch already provide:
|
|
55
|
+
* • it resolves a model-named id ONLY through the trusted registry (in-memory then durable,
|
|
56
|
+
* org-scoped) — an id no real Slack event recorded returns `undefined`, so the plane fails
|
|
57
|
+
* closed rather than fetching an arbitrary URL;
|
|
58
|
+
* • the download rides `fetchSlackAttachment` (bot token, HOST-PINNED to Slack, SSRF-guarded,
|
|
59
|
+
* redirect:error) against the event-supplied `url_private` — never a model string.
|
|
60
|
+
* A recognised id whose fetch fails THROWS (the plane surfaces it as a fetch failure), distinct
|
|
61
|
+
* from an unknown id (undefined). Idempotent per platform key.
|
|
62
|
+
*/
|
|
63
|
+
export declare function registerSlackAttachmentResolver(): void;
|
|
64
|
+
/** Test seam: clear the in-memory registry so a test starts from empty. */
|
|
65
|
+
export declare function _resetSlackAttachmentsForTest(): void;
|
|
66
|
+
//# sourceMappingURL=slack-attachments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-attachments.d.ts","sourceRoot":"","sources":["../../../agent/channels/slack-attachments.ts"],"names":[],"mappings":"AAsCA;2DAC2D;AAC3D,MAAM,WAAW,qBAAqB;IACpC,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;gFAC4E;IAC5E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;wEACoE;IACpE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,oFAAoF;IACpF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;2GACuG;IACvG,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAUD;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,qBAAqB,GAAG,IAAI,CASpG;AAED;;oFAEoF;AACpF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAExF;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,qBAAqB,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAahI;AAED;;;;;GAKG;AACH,wBAAsB,6BAA6B,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAY5H;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,+BAA+B,IAAI,IAAI,CAoCtD;AAMD,2EAA2E;AAC3E,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Trusted Slack attachment registry — the provenance bridge between a Slack event's
|
|
2
|
+
// `files[].url_private` and the `describe_image` tool.
|
|
3
|
+
//
|
|
4
|
+
// THE PROBLEM IT SOLVES. A Slack image sits behind `url_private`, which needs the bot token to
|
|
5
|
+
// download. That token must NEVER ride a model-chosen URL (an exfil channel). So describe_image
|
|
6
|
+
// can't let the model hand it a Slack URL string — but the model still needs to point at *which*
|
|
7
|
+
// image it wants to see. This registry is the indirection: the wake pipeline (parseMessageEvent)
|
|
8
|
+
// registers each image attachment's `url_private` under its Slack FILE ID (trusted provenance —
|
|
9
|
+
// the event, not the model). describe_image's slack-attachment path takes a model-named file id,
|
|
10
|
+
// resolves it HERE to the event-supplied `url_private`, and hands THAT to the authenticated fetch
|
|
11
|
+
// helper (access/slack-attachment-fetch.ts). A model-supplied URL string can therefore never reach
|
|
12
|
+
// the token-bearing fetch — the model can only *name* an id that a real event already recorded.
|
|
13
|
+
//
|
|
14
|
+
// BOUNDED: a long-lived process must not accumulate url_private entries without limit, so the map
|
|
15
|
+
// evicts oldest-first past a cap. An evicted id simply fails closed in describe_image ("no such
|
|
16
|
+
// attachment") — a re-post (or a fresh wake) re-registers it. url_private links are themselves
|
|
17
|
+
// short-lived Slack-side, so unbounded retention would be pointless as well as leaky.
|
|
18
|
+
//
|
|
19
|
+
// THE CROSS-PROCESS HALF (task #107). The map above is PROCESS-LOCAL, so a DELEGATED worker — a
|
|
20
|
+
// one-shot task in a SEPARATE session/process from the seat that saw the upload (delegation-reshape-
|
|
21
|
+
// plan-2026-07-16.md) — can't resolve a file id it never received, and "read this uploaded file"
|
|
22
|
+
// couldn't be delegated. `persistSlackAttachment` mirrors each event attachment into the DURABLE
|
|
23
|
+
// operational store (org-scoped, TTL-pruned), and `resolveSlackAttachmentDurable` reads in-memory
|
|
24
|
+
// first (same-process fast path) then the store (cross-process). Written ONLY from the trusted parse
|
|
25
|
+
// path — never a model string — the same provenance contract the in-memory map holds. Non-image
|
|
26
|
+
// uploads are persisted too (the in-memory map is image-only for describe_image), so the raw-bytes
|
|
27
|
+
// read path (read_file_upload) can resolve a CSV/text/log an image reader never registered.
|
|
28
|
+
import { getOperationalStore, deploymentOrg, resolvePartition, registerAttachmentResolver, AttachmentOversizeError, } from "@open-everywhere/core/agent/adapter-api.js";
|
|
29
|
+
import { fetchSlackAttachment } from "../access/slack-attachment-fetch.js";
|
|
30
|
+
/** How long a durable attachment row is retained. Slack `url_private` links are short-lived
|
|
31
|
+
* server-side, and a delegated worker resolves its brief within hours — a day bounds the table
|
|
32
|
+
* without stranding a legitimately slow task. */
|
|
33
|
+
const SLACK_ATTACHMENT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
34
|
+
const MAX_ENTRIES = 1000;
|
|
35
|
+
const registry = new Map();
|
|
36
|
+
/**
|
|
37
|
+
* Record an image attachment's trusted `url_private` under its Slack file id. THIS LIST IS THE
|
|
38
|
+
* AUDIT LIST for "who may write a fetchable url_private into the registry", so keep it exact.
|
|
39
|
+
* Two writers, both of trusted provenance:
|
|
40
|
+
* • `parseMessageEvent` (slack-events.ts) — an Events API message, which only arrives for a room
|
|
41
|
+
* the app is subscribed to and in;
|
|
42
|
+
* • `registerAttachments` in the front route's SHORTCUT branch (slack-front.ts) — a message
|
|
43
|
+
* shortcut, which Slack delivers even for a room the app is NOT in. It therefore registers
|
|
44
|
+
* only what that branch's readability and wake gates admitted (OPE-1442). The shortcut PARSER
|
|
45
|
+
* registers nothing: it runs at classification time, in front of those gates.
|
|
46
|
+
* A falsy id or an empty url is ignored (nothing to resolve later). Re-registering an id refreshes
|
|
47
|
+
* its recency.
|
|
48
|
+
*/
|
|
49
|
+
export function registerSlackAttachment(fileId, rec) {
|
|
50
|
+
if (!fileId || !rec.urlPrivate)
|
|
51
|
+
return;
|
|
52
|
+
if (registry.has(fileId))
|
|
53
|
+
registry.delete(fileId); // refresh insertion order (LRU-ish)
|
|
54
|
+
registry.set(fileId, { ...rec, platform: "slack" }); // stamp provenance for the attachment plane
|
|
55
|
+
while (registry.size > MAX_ENTRIES) {
|
|
56
|
+
const oldest = registry.keys().next().value;
|
|
57
|
+
if (oldest === undefined)
|
|
58
|
+
break;
|
|
59
|
+
registry.delete(oldest);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** The trusted record for a Slack file id, or undefined when this session never saw an event
|
|
63
|
+
* carrying it (→ describe_image fails closed rather than fetching an arbitrary URL). PROCESS-LOCAL:
|
|
64
|
+
* a cross-process worker must go through {@link resolveSlackAttachmentDurable}. */
|
|
65
|
+
export function resolveSlackAttachment(fileId) {
|
|
66
|
+
return registry.get(fileId);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Mirror one event attachment into the DURABLE store (task #107) so a delegated worker in a
|
|
70
|
+
* SEPARATE process can resolve it. Called ONLY from the trusted parse-consumption path (slack-front),
|
|
71
|
+
* with `org` resolved from the same event — never a model-supplied string. Best-effort: a store
|
|
72
|
+
* failure must never break the wake (the in-memory path still serves the same-process seat), so it
|
|
73
|
+
* swallows and logs. `org` defaults to the deployment org (single-tenant) when omitted.
|
|
74
|
+
*/
|
|
75
|
+
export async function persistSlackAttachment(fileId, rec, org) {
|
|
76
|
+
if (!fileId || !rec.urlPrivate)
|
|
77
|
+
return;
|
|
78
|
+
const row = {
|
|
79
|
+
urlPrivate: rec.urlPrivate,
|
|
80
|
+
...(rec.mimeType ? { mimeType: rec.mimeType } : {}),
|
|
81
|
+
...(rec.name ? { name: rec.name } : {}),
|
|
82
|
+
...(rec.teamId ? { teamId: rec.teamId } : {}),
|
|
83
|
+
};
|
|
84
|
+
try {
|
|
85
|
+
await getOperationalStore().putSlackAttachment(org ?? deploymentOrg(), fileId, row, new Date().toISOString(), SLACK_ATTACHMENT_TTL_MS);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
console.warn(`[open-everywhere:slack-attachments] dropped a durable attachment row for "${fileId}": ${String(err)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a file id ACROSS processes: the in-memory registry first (same-process fast path), then
|
|
93
|
+
* the durable store (the cross-process bridge a delegated worker needs). Returns undefined when
|
|
94
|
+
* neither knows the id (→ the caller fails closed rather than fetching an arbitrary URL). `org`
|
|
95
|
+
* defaults to the deployment org (single-tenant) when omitted.
|
|
96
|
+
*/
|
|
97
|
+
export async function resolveSlackAttachmentDurable(fileId, org) {
|
|
98
|
+
const local = registry.get(fileId);
|
|
99
|
+
if (local)
|
|
100
|
+
return local;
|
|
101
|
+
try {
|
|
102
|
+
const row = await getOperationalStore().getSlackAttachment(org ?? deploymentOrg(), fileId);
|
|
103
|
+
// Stamp provenance on the durable read too (the store is Slack-specific) so every resolved
|
|
104
|
+
// SlackAttachmentRecord carries `platform`, cross-process included.
|
|
105
|
+
return row ? { ...row, platform: "slack" } : undefined;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.warn(`[open-everywhere:slack-attachments] durable attachment lookup for "${fileId}" failed: ${String(err)}`);
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Register Slack as an attachment-plane resolver (channels/attachments.ts) — the neutral seam
|
|
114
|
+
* describe_image consults so its contract carries no per-platform union arm. The resolver wraps the
|
|
115
|
+
* SAME two guarantees the in-memory/durable registry + host-pinned fetch already provide:
|
|
116
|
+
* • it resolves a model-named id ONLY through the trusted registry (in-memory then durable,
|
|
117
|
+
* org-scoped) — an id no real Slack event recorded returns `undefined`, so the plane fails
|
|
118
|
+
* closed rather than fetching an arbitrary URL;
|
|
119
|
+
* • the download rides `fetchSlackAttachment` (bot token, HOST-PINNED to Slack, SSRF-guarded,
|
|
120
|
+
* redirect:error) against the event-supplied `url_private` — never a model string.
|
|
121
|
+
* A recognised id whose fetch fails THROWS (the plane surfaces it as a fetch failure), distinct
|
|
122
|
+
* from an unknown id (undefined). Idempotent per platform key.
|
|
123
|
+
*/
|
|
124
|
+
export function registerSlackAttachmentResolver() {
|
|
125
|
+
registerAttachmentResolver({
|
|
126
|
+
platform: "slack",
|
|
127
|
+
async resolve(attachmentId, ctx) {
|
|
128
|
+
const rec = await resolveSlackAttachmentDurable(attachmentId, resolvePartition(ctx).org);
|
|
129
|
+
if (!rec)
|
|
130
|
+
return undefined; // not a Slack id we saw → let the plane fail closed
|
|
131
|
+
const res = await fetchSlackAttachment(ctx, { urlPrivate: rec.urlPrivate, id: attachmentId });
|
|
132
|
+
if (!res.ok)
|
|
133
|
+
throw new Error(`fetching Slack attachment "${attachmentId}" failed (HTTP ${res.status})`);
|
|
134
|
+
return { bytes: new Uint8Array(await res.arrayBuffer()), ref: `slack-file:${attachmentId}`, mimeType: rec.mimeType };
|
|
135
|
+
},
|
|
136
|
+
// Size-guarded variant (task #107) for read_file_upload's raw-bytes read path. It resolves the
|
|
137
|
+
// TRUSTED record up front (no fetch) so the caller can refuse a type before any download, then
|
|
138
|
+
// defers the host-pinned, bot-token fetch to `pull` — which preserves the Content-Length
|
|
139
|
+
// pre-check EXACTLY: it rejects (AttachmentOversizeError, phase "declared") off the declared
|
|
140
|
+
// length BEFORE reading the body, and re-checks the actual byte count after (phase "actual").
|
|
141
|
+
async resolveGuarded(attachmentId, ctx) {
|
|
142
|
+
const rec = await resolveSlackAttachmentDurable(attachmentId, resolvePartition(ctx).org);
|
|
143
|
+
if (!rec)
|
|
144
|
+
return undefined; // not a Slack id we saw → let the plane fail closed
|
|
145
|
+
return {
|
|
146
|
+
ref: `slack-file:${attachmentId}`,
|
|
147
|
+
mimeType: rec.mimeType,
|
|
148
|
+
name: rec.name,
|
|
149
|
+
async pull({ maxBytes }) {
|
|
150
|
+
const res = await fetchSlackAttachment(ctx, { urlPrivate: rec.urlPrivate, id: attachmentId });
|
|
151
|
+
if (!res.ok)
|
|
152
|
+
throw new Error(`fetching the upload failed (HTTP ${res.status})`);
|
|
153
|
+
// Content-Length pre-check off the declared length — reject before reading the body.
|
|
154
|
+
const declared = Number.parseInt(res.headers.get("content-length") ?? "", 10);
|
|
155
|
+
if (Number.isFinite(declared) && declared > maxBytes)
|
|
156
|
+
throw new AttachmentOversizeError(declared, "declared", maxBytes);
|
|
157
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
158
|
+
// Post-read cap — a chunked response can exceed the (possibly-absent) Content-Length.
|
|
159
|
+
if (bytes.byteLength > maxBytes)
|
|
160
|
+
throw new AttachmentOversizeError(bytes.byteLength, "actual", maxBytes);
|
|
161
|
+
return bytes;
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// NOTE (OPE-114 stage 4): the resolver is no longer wired at module load — registerSlackProvider()
|
|
168
|
+
// (agent/register.ts) calls registerSlackAttachmentResolver() at boot, the same "register in every
|
|
169
|
+
// process" posture (register-adapters.ts runs in every process that can execute describe_image).
|
|
170
|
+
/** Test seam: clear the in-memory registry so a test starts from empty. */
|
|
171
|
+
export function _resetSlackAttachmentsForTest() {
|
|
172
|
+
registry.clear();
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=slack-attachments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-attachments.js","sourceRoot":"","sources":["../../../agent/channels/slack-attachments.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type BindableContext } from "@open-everywhere/core/agent/adapter-api.js";
|
|
2
|
+
/** The public URL Slack POSTs the "/oe" slash command to (the app's Slash Command Request URL). */
|
|
3
|
+
export declare const SLACK_COMMANDS_ROUTE = "/eve/v1/slack/commands";
|
|
4
|
+
/** Build the BindableContext the identity seam (bindFrom/bindSlack) expects from Slack's
|
|
5
|
+
* slash-command form fields. Slack's slash-command payload carries no `channel_type` /
|
|
6
|
+
* `is_private` bit (unlike an app_mention/message event) — `bindSlack`'s existing fallbacks
|
|
7
|
+
* already handle that (DM detection via the `channel_id`'s "D" prefix; visibility fails
|
|
8
|
+
* closed to private), same posture as every other under-specified Slack event.
|
|
9
|
+
*
|
|
10
|
+
* 🔴 IT DOES CARRY THE ENTERPRISE GRID RUNG (OPE-1559). Slack sends `enterprise_id` as a form field
|
|
11
|
+
* on a Grid, beside `team_id`, and it reaches `RawIdentity.trustEnterprise` → the declaration walk →
|
|
12
|
+
* `binding.declaredTrust`. That is the OPERATOR-DECLARATION half — the half `roomVouchedInternalFrom`
|
|
13
|
+
* reads — so a Grid declared once now answers here the same way it answers at every other door,
|
|
14
|
+
* instead of resolving as though the line were absent.
|
|
15
|
+
*
|
|
16
|
+
* 🔴 STATE ITS REACH EXACTLY, because this door is narrower than the wake doors. For a CHANNEL
|
|
17
|
+
* command it does NOT change `binding.trust`: this payload carries no `conversations.info` verdict,
|
|
18
|
+
* so `slackTrust(undefined, undefined, "channel")` fails closed to `external`, and `bindFrom` takes
|
|
19
|
+
* that raw verdict ahead of any vouch. So a channel `/oe` stays `trust: "external"` with the rung
|
|
20
|
+
* and without it, and the budget scope `domainNameForScope` picks is unchanged. The rung reaches
|
|
21
|
+
* `binding.trust` only on a DM-shaped `/oe` (a `D…` channel id), where the mapper leaves trust
|
|
22
|
+
* undefined and the vouch is what answers. Spread conditionally, like every other door: a
|
|
23
|
+
* single-workspace install stamps nothing. */
|
|
24
|
+
export declare function bindableForSlashCommand(params: URLSearchParams): BindableContext;
|
|
25
|
+
declare const _default: import("eve/channels").Channel<undefined, Record<string, unknown>, Record<string, unknown>>;
|
|
26
|
+
export default _default;
|
|
27
|
+
//# sourceMappingURL=slack-commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-commands.d.ts","sourceRoot":"","sources":["../../../agent/channels/slack-commands.ts"],"names":[],"mappings":"AAoCA,OAAO,EASL,KAAK,eAAe,EACrB,MAAM,4CAA4C,CAAC;AAOpD,mGAAmG;AACnG,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAS7D;;;;;;;;;;;;;;;;;;;+CAmB+C;AAC/C,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CAkBhF"}
|