@open-everywhere/provider-slack 0.1.5 → 0.3.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/NOTICE +9 -9
- package/README.md +1 -1
- package/agent/app-manifest.ts +44 -9
- package/agent/channels/slack-commands.ts +9 -5
- package/agent/channels/slack-events.ts +76 -13
- package/agent/channels/slack-front.ts +4 -80
- package/agent/channels/slack-post-arm.ts +1 -42
- package/agent/channels/slack-task-card.ts +182 -0
- package/agent/channels/slack.ts +518 -468
- package/agent/contributions.ts +1 -1
- package/agent/docs-generated.ts +1 -1
- package/agent/ops.ts +20 -2
- package/agent/setup/claim.ts +16 -1
- package/agent/setup/eve-credentials.ts +10 -2
- package/agent/setup/page.ts +79 -14
- package/agent/surface-parity.ts +0 -6
- package/dist/agent/app-manifest.d.ts +20 -4
- package/dist/agent/app-manifest.d.ts.map +1 -1
- package/dist/agent/app-manifest.js +29 -6
- package/dist/agent/app-manifest.js.map +1 -1
- package/dist/agent/channels/slack-commands.d.ts.map +1 -1
- package/dist/agent/channels/slack-commands.js +9 -6
- package/dist/agent/channels/slack-commands.js.map +1 -1
- package/dist/agent/channels/slack-events.d.ts +28 -6
- package/dist/agent/channels/slack-events.d.ts.map +1 -1
- package/dist/agent/channels/slack-events.js +74 -14
- package/dist/agent/channels/slack-events.js.map +1 -1
- package/dist/agent/channels/slack-front.d.ts +1 -15
- package/dist/agent/channels/slack-front.d.ts.map +1 -1
- package/dist/agent/channels/slack-front.js +4 -62
- package/dist/agent/channels/slack-front.js.map +1 -1
- package/dist/agent/channels/slack-post-arm.d.ts +1 -8
- package/dist/agent/channels/slack-post-arm.d.ts.map +1 -1
- package/dist/agent/channels/slack-post-arm.js +1 -40
- package/dist/agent/channels/slack-post-arm.js.map +1 -1
- package/dist/agent/channels/slack-task-card.d.ts +38 -0
- package/dist/agent/channels/slack-task-card.d.ts.map +1 -0
- package/dist/agent/channels/slack-task-card.js +156 -0
- package/dist/agent/channels/slack-task-card.js.map +1 -0
- package/dist/agent/channels/slack.d.ts.map +1 -1
- package/dist/agent/channels/slack.js +516 -466
- package/dist/agent/channels/slack.js.map +1 -1
- package/dist/agent/contributions.d.ts +1 -1
- package/dist/agent/contributions.d.ts.map +1 -1
- package/dist/agent/contributions.js +1 -1
- package/dist/agent/contributions.js.map +1 -1
- package/dist/agent/docs-generated.js +1 -1
- package/dist/agent/docs-generated.js.map +1 -1
- package/dist/agent/ops.d.ts +5 -0
- package/dist/agent/ops.d.ts.map +1 -1
- package/dist/agent/ops.js +13 -2
- package/dist/agent/ops.js.map +1 -1
- package/dist/agent/setup/claim.d.ts.map +1 -1
- package/dist/agent/setup/claim.js +15 -2
- package/dist/agent/setup/claim.js.map +1 -1
- package/dist/agent/setup/eve-credentials.d.ts +9 -2
- package/dist/agent/setup/eve-credentials.d.ts.map +1 -1
- package/dist/agent/setup/eve-credentials.js +9 -1
- package/dist/agent/setup/eve-credentials.js.map +1 -1
- package/dist/agent/setup/page.d.ts +2 -0
- package/dist/agent/setup/page.d.ts.map +1 -1
- package/dist/agent/setup/page.js +71 -15
- package/dist/agent/setup/page.js.map +1 -1
- package/dist/agent/surface-parity.d.ts.map +1 -1
- package/dist/agent/surface-parity.js +0 -6
- package/dist/agent/surface-parity.js.map +1 -1
- package/package.json +10 -8
- package/agent/channels/slack-approval-card.ts +0 -275
|
@@ -1,275 +0,0 @@
|
|
|
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
|
-
|
|
23
|
-
import {
|
|
24
|
-
admitInputResponse,
|
|
25
|
-
APPROVAL_BUTTON_LABELS,
|
|
26
|
-
APPROVAL_CARD_REFUSED_AUDIT_ACTION as CORE_APPROVAL_CARD_REFUSED_AUDIT_ACTION,
|
|
27
|
-
APPROVAL_CARD_SETTLED_AUDIT_ACTION as CORE_APPROVAL_CARD_SETTLED_AUDIT_ACTION,
|
|
28
|
-
auditApprovalCardTap,
|
|
29
|
-
deploymentOrg,
|
|
30
|
-
forgetApprovalCardTicket,
|
|
31
|
-
forgetBridgedQuestion,
|
|
32
|
-
getOperationalStore,
|
|
33
|
-
ingressSpendGate,
|
|
34
|
-
parseApprovalClickId,
|
|
35
|
-
readBridgedQuestion,
|
|
36
|
-
readApprovalCardTicket,
|
|
37
|
-
resolveApprovalCardClick,
|
|
38
|
-
resolvePartition,
|
|
39
|
-
unifiedThreadRef,
|
|
40
|
-
SLACK_SPEND_BADGES,
|
|
41
|
-
type ApprovalCardRespondFn,
|
|
42
|
-
type BindableContext,
|
|
43
|
-
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
44
|
-
import { slackClickerIsGuest, slackForm } from "./slack-events.js";
|
|
45
|
-
|
|
46
|
-
const log = (m: string): void => console.log(`[open-everywhere:slack] approval card: ${m}`);
|
|
47
|
-
|
|
48
|
-
/** The `input_response.routed` sibling for a tap: an approval settled through a BUTTON rather than a
|
|
49
|
-
* typed reply. RE-EXPORTED FROM CORE, never re-spelled here — one action name for every surface, so
|
|
50
|
-
* a scorer that joins on the row cannot under-count one of them (OPE-1856). */
|
|
51
|
-
export const APPROVAL_CARD_SETTLED_AUDIT_ACTION = CORE_APPROVAL_CARD_SETTLED_AUDIT_ACTION;
|
|
52
|
-
/** A verified tap that settled nothing, carrying WHICH refusal it was. Core-owned, as above. */
|
|
53
|
-
export const APPROVAL_CARD_REFUSED_AUDIT_ACTION = CORE_APPROVAL_CARD_REFUSED_AUDIT_ACTION;
|
|
54
|
-
|
|
55
|
-
/** The settle delivery this leg needs. CORE-OWNED and bound by the front route through
|
|
56
|
-
* `approvalCardRespondVia(args.attachSession)` — re-exported here, never re-spelled, so the address
|
|
57
|
-
* the answer takes cannot drift per surface (OPE-1863). */
|
|
58
|
-
export type ApprovalRespondFn = ApprovalCardRespondFn;
|
|
59
|
-
|
|
60
|
-
/** What one verified tap did, for the front route's log line. Never thrown — this endpoint answers a
|
|
61
|
-
* person who is waiting, so every path returns. */
|
|
62
|
-
export interface ApprovalCardClickOutcome {
|
|
63
|
-
readonly handled: boolean;
|
|
64
|
-
readonly reason: string;
|
|
65
|
-
/** A line to show the tapper alone, or undefined when the outcome needs no note. */
|
|
66
|
-
readonly notice?: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** The `block_actions` fields this leg reads, narrowed so nothing here needs Slack's own types. */
|
|
70
|
-
interface ParsedApprovalClick {
|
|
71
|
-
readonly actionId: string;
|
|
72
|
-
readonly channelId: string;
|
|
73
|
-
readonly userId: string;
|
|
74
|
-
readonly teamId?: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Read the one `oe_input:` action out of a verified `block_actions` payload. `undefined` when the
|
|
79
|
-
* payload is not one of ours or is missing a field the decision needs — the front route then leaves
|
|
80
|
-
* it alone rather than half-handling it.
|
|
81
|
-
*
|
|
82
|
-
* THE FIRST `oe_input:` ACTION WINS and the rest are ignored: Slack sends one action per click, and a
|
|
83
|
-
* payload carrying two of ours is not a shape this host mints.
|
|
84
|
-
*/
|
|
85
|
-
export function parseSlackApprovalClick(payload: unknown): ParsedApprovalClick | undefined {
|
|
86
|
-
if (typeof payload !== "object" || payload === null) return undefined;
|
|
87
|
-
const p = payload as Record<string, unknown>;
|
|
88
|
-
if (!Array.isArray(p.actions)) return undefined;
|
|
89
|
-
let actionId: string | undefined;
|
|
90
|
-
for (const a of p.actions as readonly unknown[]) {
|
|
91
|
-
const id = typeof a === "object" && a !== null ? (a as Record<string, unknown>).action_id : undefined;
|
|
92
|
-
if (typeof id === "string" && parseApprovalClickId(id) !== undefined) {
|
|
93
|
-
actionId = id;
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (actionId === undefined) return undefined;
|
|
98
|
-
const channel = typeof p.channel === "object" && p.channel !== null ? (p.channel as Record<string, unknown>).id : undefined;
|
|
99
|
-
const user = typeof p.user === "object" && p.user !== null ? (p.user as Record<string, unknown>).id : undefined;
|
|
100
|
-
const team = typeof p.team === "object" && p.team !== null ? (p.team as Record<string, unknown>).id : undefined;
|
|
101
|
-
if (typeof channel !== "string" || channel.length === 0) return undefined;
|
|
102
|
-
if (typeof user !== "string" || user.length === 0) return undefined;
|
|
103
|
-
return { actionId, channelId: channel, userId: user, ...(typeof team === "string" && team.length > 0 ? { teamId: team } : {}) };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** The bindable a partition resolve needs from a raw interactivity payload. The team id is the ONE
|
|
107
|
-
* attribute that matters here, and it is read off the wire rather than through a placeholder — the
|
|
108
|
-
* same reading, and the same reason, as `slackAuthTeamId` in slack-input-response.ts. */
|
|
109
|
-
function bindableForClick(click: ParsedApprovalClick): BindableContext {
|
|
110
|
-
return {
|
|
111
|
-
channel: { kind: "slack" },
|
|
112
|
-
session: { auth: { current: { attributes: { ...(click.teamId ? { team_id: click.teamId } : {}), channel_id: click.channelId } } } },
|
|
113
|
-
} as unknown as BindableContext;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* The host copy that rides the delivered answer's `text` — the LABEL THIS DEPLOYMENT PUT ON THE
|
|
118
|
-
* BUTTON, read back out of the same allowlist that minted it, so the transcript says what the person
|
|
119
|
-
* actually chose. A tap carries no words, and writing the person's prose for them is not an option.
|
|
120
|
-
* The fallback capitalises the eve-minted option id, which can only be reached by a row written
|
|
121
|
-
* before an id left the allowlist; the DECISION is never this value, because eve settles on
|
|
122
|
-
* `optionId` alone.
|
|
123
|
-
*/
|
|
124
|
-
function approvalAnswerText(optionId: string): string {
|
|
125
|
-
return APPROVAL_BUTTON_LABELS.get(optionId.toLowerCase())?.label ?? optionId.charAt(0).toUpperCase() + optionId.slice(1);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Show the tapper alone why nothing happened. EPHEMERAL, never a message in the room: the card is
|
|
129
|
-
* still live for somebody who may answer it, and a public "you may not answer" note tells the room
|
|
130
|
-
* nothing it needs. Best-effort — a failed notice never changes the verdict. */
|
|
131
|
-
async function notifyTapper(channelId: string, userId: string, text: string): Promise<void> {
|
|
132
|
-
try {
|
|
133
|
-
await slackForm("chat.postEphemeral", new URLSearchParams({ channel: channelId, user: userId, text }).toString());
|
|
134
|
-
} catch (err) {
|
|
135
|
-
log(`refusal notice failed to post: ${String(err)}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Handle one verified `oe_input:` click: resolve it, admit it, deliver the answer to eve, consume the
|
|
141
|
-
* rows. NEVER THROWS — the front route has already 200'd the interaction, and an unhandled throw here
|
|
142
|
-
* would only lose the log line.
|
|
143
|
-
*
|
|
144
|
-
* 🔴 THE ANSWER TEXT IS HOST COPY, and it must be. A tap carries no words, so a transcript that read
|
|
145
|
-
* the button as the person's own prose would be inventing their words. What rides is the host label
|
|
146
|
-
* this deployment put on that button (`Approve` / `Cancel`), which is what the person actually chose,
|
|
147
|
-
* and the `optionId` beside it is what eve settles on — `resolveApprovalOutcome` reads `optionId`
|
|
148
|
-
* ALONE, so the text is transcript only and never the decision.
|
|
149
|
-
*
|
|
150
|
-
* 🔴 CONSUME-ONCE, AND BOTH ROWS. The ticket goes so a second tap on the same card cannot answer a
|
|
151
|
-
* request eve has already settled; the outstanding-question row goes so the room's NEXT message is an
|
|
152
|
-
* ordinary wake again rather than a second answer to a settled request. Both happen only after eve
|
|
153
|
-
* accepted the delivery, exactly as the text leg orders them.
|
|
154
|
-
*
|
|
155
|
-
* THE LIMIT OF THAT, stated rather than implied: the read and the delete are not one atomic step, and
|
|
156
|
-
* the delete runs only AFTER `respond` returns, so anything arriving while the first tap is still in
|
|
157
|
-
* flight resolves against rows that are both still present. Two such deliveries each spend an
|
|
158
|
-
* admission, each call `respond`, and each can write a settled audit row — so one approval can read
|
|
159
|
-
* as decided by two taps. It is not a second DECISION: both had to be admitted and both had to name
|
|
160
|
-
* an option the request offered, and eve's own resolution refuses the second delivery for a request
|
|
161
|
-
* it already settled. A delivery that arrives after the first settle RETURNED is the covered case —
|
|
162
|
-
* the ticket is gone and it resolves `no-ticket`.
|
|
163
|
-
*
|
|
164
|
-
* 🔴 A SECOND DELIVERY IS MOST LIKELY TO BE SLACK'S OWN, NOT A SECOND PERSON, and this handler does
|
|
165
|
-
* not ack until the settle returns. Slack expects an interactivity endpoint to answer within about
|
|
166
|
-
* three seconds; what it does past that — show the tapper a connection error, redeliver, or both —
|
|
167
|
-
* is VENDOR BEHAVIOUR THIS TREE HAS NOT MEASURED, so do not read the bound above as covering it.
|
|
168
|
-
* Acking first and settling in the background would remove the delay and widen exactly the window
|
|
169
|
-
* this paragraph describes; the ordering here takes the narrower window instead. Whoever measures
|
|
170
|
-
* the real behaviour should revisit that trade, with the measurement written down.
|
|
171
|
-
*/
|
|
172
|
-
export async function handleSlackApprovalCardClick(payload: unknown, respond: ApprovalRespondFn): Promise<ApprovalCardClickOutcome> {
|
|
173
|
-
// 🔴 HOISTED OUT OF THE `try` SO THE `catch` CAN STAMP ITS ROW (OPE-1856). A throw anywhere below
|
|
174
|
-
// — including from the partition resolve itself — must still leave a row saying this deployment
|
|
175
|
-
// took a tap and settled nothing; core's auditor accepts an absent partition for that case.
|
|
176
|
-
let tapper = "";
|
|
177
|
-
let auditPartition: ReturnType<typeof resolvePartition> | undefined;
|
|
178
|
-
try {
|
|
179
|
-
const click = parseSlackApprovalClick(payload);
|
|
180
|
-
if (click === undefined) return { handled: false, reason: "not an oe_input: click" };
|
|
181
|
-
const parsed = parseApprovalClickId(click.actionId);
|
|
182
|
-
if (parsed === undefined) return { handled: false, reason: "oe_input: action id did not parse" };
|
|
183
|
-
tapper = click.userId;
|
|
184
|
-
const org = deploymentOrg();
|
|
185
|
-
const store = getOperationalStore();
|
|
186
|
-
const partition = resolvePartition(bindableForClick(click));
|
|
187
|
-
auditPartition = partition;
|
|
188
|
-
// THE SPEND GATE FIRST, ahead of the identity admission — the same order and the same reason
|
|
189
|
-
// `admitSlackInputResponse` takes: over budget outranks identity, and a decline must not spend a
|
|
190
|
-
// roster lookup. Its refusal copy is its own; a decline here leaves the request PENDING.
|
|
191
|
-
const spend = await ingressSpendGate({
|
|
192
|
-
bindable: bindableForClick(click),
|
|
193
|
-
door: "answer",
|
|
194
|
-
principalId: click.userId,
|
|
195
|
-
workspaceRef: click.teamId ?? null,
|
|
196
|
-
badges: SLACK_SPEND_BADGES,
|
|
197
|
-
post: async (text) => {
|
|
198
|
-
await notifyTapper(click.channelId, click.userId, text);
|
|
199
|
-
},
|
|
200
|
-
});
|
|
201
|
-
if (spend === "declined") {
|
|
202
|
-
// AUDITED, not only logged (OPE-1856): a decline is this deployment refusing a person's tap,
|
|
203
|
-
// and a refusal nobody can count is indistinguishable from a tap that never arrived.
|
|
204
|
-
await auditApprovalCardTap({ platform: "slack", principalId: click.userId, partition, outcome: { kind: "refused", reason: "spend-declined" } });
|
|
205
|
-
log(`tap declined by the spend gate — the approval stays parked`);
|
|
206
|
-
return { handled: true, reason: "spend-declined" };
|
|
207
|
-
}
|
|
208
|
-
// The room the tap came from, in the SAME vocabulary the card's ticket stored — derived through
|
|
209
|
-
// core's own codec rather than spelled here, so the two sides cannot drift on the key's shape.
|
|
210
|
-
const containerKey = unifiedThreadRef({ platform: "slack", channelId: click.channelId }).channelKey;
|
|
211
|
-
const routing = await resolveApprovalCardClick(
|
|
212
|
-
{ ticket: parsed.ticket, optionId: parsed.optionId, containerKey },
|
|
213
|
-
{
|
|
214
|
-
read: (ticket) => readApprovalCardTicket(store, org, ticket),
|
|
215
|
-
// The join with the TEXT leg — see the dep's own note. Fail-CLOSED on an unreadable row: a
|
|
216
|
-
// store this leg cannot read cannot say the request is still parked, and settling on that
|
|
217
|
-
// guess is the one direction that spends a governed decision twice.
|
|
218
|
-
stillOutstanding: async (bareToken, requestId) => {
|
|
219
|
-
const outstanding = await readBridgedQuestion(store, org, bareToken).catch(() => undefined);
|
|
220
|
-
return outstanding?.requestId === requestId;
|
|
221
|
-
},
|
|
222
|
-
admit: async () => {
|
|
223
|
-
// Leg B, the same positive guest verdict both other clicker floors use, and inert on an
|
|
224
|
-
// unreadable probe (`undefined` never refuses).
|
|
225
|
-
const answererIsGuest = await slackClickerIsGuest(click.userId, click.teamId).catch(() => undefined);
|
|
226
|
-
const verdict = await admitInputResponse({
|
|
227
|
-
org: partition.org,
|
|
228
|
-
platform: partition.platform,
|
|
229
|
-
workspace: partition.workspace,
|
|
230
|
-
surface: partition.surface,
|
|
231
|
-
surfaceKey: partition.surfaceKey,
|
|
232
|
-
principalId: click.userId,
|
|
233
|
-
responses: 1,
|
|
234
|
-
...(answererIsGuest === undefined ? {} : { answererIsGuest }),
|
|
235
|
-
});
|
|
236
|
-
return { ok: verdict.ok, floor: verdict.ok ? verdict.floor : "", notice: verdict.ok ? "" : verdict.notice };
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
);
|
|
240
|
-
if (routing.kind === "refuse") {
|
|
241
|
-
await auditApprovalCardTap({ platform: "slack", principalId: click.userId, partition, outcome: { kind: "refused", reason: routing.reason } });
|
|
242
|
-
if (routing.notice !== undefined) await notifyTapper(click.channelId, click.userId, routing.notice);
|
|
243
|
-
log(`tap settled nothing (${routing.reason}) — the approval stays parked and answerable in words`);
|
|
244
|
-
return { handled: true, reason: routing.reason, ...(routing.notice ? { notice: routing.notice } : {}) };
|
|
245
|
-
}
|
|
246
|
-
const label = approvalAnswerText(routing.optionId);
|
|
247
|
-
// 🔴 DELIVERED BY SESSION ID, never by the window token (OPE-1863): a continuation token is
|
|
248
|
-
// channel-local, and this route is the SLACK channel's, so `from(bareToken)` here named a
|
|
249
|
-
// session that never existed and the settle threw into the catch below. The token's job is the
|
|
250
|
-
// outstanding-question row on the two lines that follow.
|
|
251
|
-
await respond(routing.sessionId, { requestId: routing.requestId, optionId: routing.optionId, text: label });
|
|
252
|
-
// CONSUME-ONCE, and only now that eve accepted the delivery. A throw above skips both, which
|
|
253
|
-
// leaves the card tappable and the question answerable — the safe direction.
|
|
254
|
-
await forgetApprovalCardTicket(store, org, parsed.ticket);
|
|
255
|
-
await forgetBridgedQuestion(store, org, routing.bareToken);
|
|
256
|
-
await auditApprovalCardTap({
|
|
257
|
-
platform: "slack",
|
|
258
|
-
principalId: click.userId,
|
|
259
|
-
partition,
|
|
260
|
-
outcome: { kind: "settled", requestId: routing.requestId, optionId: routing.optionId, floor: routing.floor },
|
|
261
|
-
});
|
|
262
|
-
log(`tap settled ${routing.requestId} as "${routing.optionId}" (floor ${routing.floor})`);
|
|
263
|
-
return { handled: true, reason: "settled" };
|
|
264
|
-
} catch (err) {
|
|
265
|
-
// The approval is still parked and the card is still tappable, so the honest outcome is to say so
|
|
266
|
-
// and leave both rows in place. Never a throw: this runs after the interaction was acknowledged.
|
|
267
|
-
// AUDITED, not only logged (OPE-1856): ship #212's S-EVE-CARD leg could not say whether its tap
|
|
268
|
-
// had been refused or had thrown, because neither arm left a row. `auditApprovalCardTap` never
|
|
269
|
-
// throws, so this cannot turn a failed tap into a failed response.
|
|
270
|
-
await auditApprovalCardTap({ platform: "slack", principalId: tapper, ...(auditPartition === undefined ? {} : { partition: auditPartition }), outcome: { kind: "refused", reason: "error" } });
|
|
271
|
-
log(`tap failed (${(err as Error)?.message ?? String(err)}) — nothing was settled, the approval stays parked`);
|
|
272
|
-
return { handled: true, reason: "error" };
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|