@open-everywhere/provider-discord 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/discord-grant-click.ts +237 -0
- package/agent/approvals/discord-admin.ts +227 -0
- package/agent/approvals/discord-card.ts +248 -0
- package/agent/approvals/discord-flow.ts +469 -0
- package/agent/approvals/discord-scope-label.ts +27 -0
- package/agent/channels/discord-approval-card.ts +373 -0
- package/agent/channels/discord-backscroll.ts +593 -0
- package/agent/channels/discord-bot-name.ts +113 -0
- package/agent/channels/discord-channel-info.ts +83 -0
- package/agent/channels/discord-directory.ts +179 -0
- package/agent/channels/discord-dispatch-target.ts +103 -0
- package/agent/channels/discord-events.ts +175 -0
- package/agent/channels/discord-front.ts +355 -0
- package/agent/channels/discord-gateway.ts +3256 -0
- package/agent/channels/discord-identity.ts +269 -0
- package/agent/channels/discord-interaction-defer.ts +123 -0
- package/agent/channels/discord-message-ops.ts +127 -0
- package/agent/channels/discord-origin-codec.ts +105 -0
- package/agent/channels/discord-post-arm.ts +230 -0
- package/agent/channels/discord-post.ts +463 -0
- package/agent/channels/discord-read.ts +140 -0
- package/agent/channels/discord-rest.ts +85 -0
- package/agent/channels/discord.ts +244 -0
- package/agent/commands/discord-command-sync.ts +285 -0
- package/agent/commands/discord-commands.ts +291 -0
- package/agent/contributions.ts +31 -0
- package/agent/docs-generated.ts +15 -0
- package/agent/identity/discord-link-consent.ts +124 -0
- package/agent/ops.ts +14 -0
- package/agent/provider-events.ts +86 -0
- package/agent/register.ts +147 -0
- package/agent/surface-parity.ts +95 -0
- package/dist/agent/access/discord-grant-click.d.ts +25 -0
- package/dist/agent/access/discord-grant-click.d.ts.map +1 -0
- package/dist/agent/access/discord-grant-click.js +210 -0
- package/dist/agent/access/discord-grant-click.js.map +1 -0
- package/dist/agent/approvals/discord-admin.d.ts +63 -0
- package/dist/agent/approvals/discord-admin.d.ts.map +1 -0
- package/dist/agent/approvals/discord-admin.js +219 -0
- package/dist/agent/approvals/discord-admin.js.map +1 -0
- package/dist/agent/approvals/discord-card.d.ts +102 -0
- package/dist/agent/approvals/discord-card.d.ts.map +1 -0
- package/dist/agent/approvals/discord-card.js +217 -0
- package/dist/agent/approvals/discord-card.js.map +1 -0
- package/dist/agent/approvals/discord-flow.d.ts +109 -0
- package/dist/agent/approvals/discord-flow.d.ts.map +1 -0
- package/dist/agent/approvals/discord-flow.js +375 -0
- package/dist/agent/approvals/discord-flow.js.map +1 -0
- package/dist/agent/approvals/discord-scope-label.d.ts +3 -0
- package/dist/agent/approvals/discord-scope-label.d.ts.map +1 -0
- package/dist/agent/approvals/discord-scope-label.js +27 -0
- package/dist/agent/approvals/discord-scope-label.js.map +1 -0
- package/dist/agent/channels/discord-approval-card.d.ts +81 -0
- package/dist/agent/channels/discord-approval-card.d.ts.map +1 -0
- package/dist/agent/channels/discord-approval-card.js +303 -0
- package/dist/agent/channels/discord-approval-card.js.map +1 -0
- package/dist/agent/channels/discord-backscroll.d.ts +209 -0
- package/dist/agent/channels/discord-backscroll.d.ts.map +1 -0
- package/dist/agent/channels/discord-backscroll.js +505 -0
- package/dist/agent/channels/discord-backscroll.js.map +1 -0
- package/dist/agent/channels/discord-bot-name.d.ts +19 -0
- package/dist/agent/channels/discord-bot-name.d.ts.map +1 -0
- package/dist/agent/channels/discord-bot-name.js +98 -0
- package/dist/agent/channels/discord-bot-name.js.map +1 -0
- package/dist/agent/channels/discord-channel-info.d.ts +22 -0
- package/dist/agent/channels/discord-channel-info.d.ts.map +1 -0
- package/dist/agent/channels/discord-channel-info.js +65 -0
- package/dist/agent/channels/discord-channel-info.js.map +1 -0
- package/dist/agent/channels/discord-directory.d.ts +11 -0
- package/dist/agent/channels/discord-directory.d.ts.map +1 -0
- package/dist/agent/channels/discord-directory.js +170 -0
- package/dist/agent/channels/discord-directory.js.map +1 -0
- package/dist/agent/channels/discord-dispatch-target.d.ts +15 -0
- package/dist/agent/channels/discord-dispatch-target.d.ts.map +1 -0
- package/dist/agent/channels/discord-dispatch-target.js +99 -0
- package/dist/agent/channels/discord-dispatch-target.js.map +1 -0
- package/dist/agent/channels/discord-events.d.ts +58 -0
- package/dist/agent/channels/discord-events.d.ts.map +1 -0
- package/dist/agent/channels/discord-events.js +150 -0
- package/dist/agent/channels/discord-events.js.map +1 -0
- package/dist/agent/channels/discord-front.d.ts +115 -0
- package/dist/agent/channels/discord-front.d.ts.map +1 -0
- package/dist/agent/channels/discord-front.js +279 -0
- package/dist/agent/channels/discord-front.js.map +1 -0
- package/dist/agent/channels/discord-gateway.d.ts +783 -0
- package/dist/agent/channels/discord-gateway.d.ts.map +1 -0
- package/dist/agent/channels/discord-gateway.js +2591 -0
- package/dist/agent/channels/discord-gateway.js.map +1 -0
- package/dist/agent/channels/discord-identity.d.ts +110 -0
- package/dist/agent/channels/discord-identity.d.ts.map +1 -0
- package/dist/agent/channels/discord-identity.js +201 -0
- package/dist/agent/channels/discord-identity.js.map +1 -0
- package/dist/agent/channels/discord-interaction-defer.d.ts +33 -0
- package/dist/agent/channels/discord-interaction-defer.d.ts.map +1 -0
- package/dist/agent/channels/discord-interaction-defer.js +102 -0
- package/dist/agent/channels/discord-interaction-defer.js.map +1 -0
- package/dist/agent/channels/discord-message-ops.d.ts +3 -0
- package/dist/agent/channels/discord-message-ops.d.ts.map +1 -0
- package/dist/agent/channels/discord-message-ops.js +123 -0
- package/dist/agent/channels/discord-message-ops.js.map +1 -0
- package/dist/agent/channels/discord-origin-codec.d.ts +7 -0
- package/dist/agent/channels/discord-origin-codec.d.ts.map +1 -0
- package/dist/agent/channels/discord-origin-codec.js +98 -0
- package/dist/agent/channels/discord-origin-codec.js.map +1 -0
- package/dist/agent/channels/discord-post-arm.d.ts +3 -0
- package/dist/agent/channels/discord-post-arm.d.ts.map +1 -0
- package/dist/agent/channels/discord-post-arm.js +195 -0
- package/dist/agent/channels/discord-post-arm.js.map +1 -0
- package/dist/agent/channels/discord-post.d.ts +120 -0
- package/dist/agent/channels/discord-post.d.ts.map +1 -0
- package/dist/agent/channels/discord-post.js +426 -0
- package/dist/agent/channels/discord-post.js.map +1 -0
- package/dist/agent/channels/discord-read.d.ts +40 -0
- package/dist/agent/channels/discord-read.d.ts.map +1 -0
- package/dist/agent/channels/discord-read.js +128 -0
- package/dist/agent/channels/discord-read.js.map +1 -0
- package/dist/agent/channels/discord-rest.d.ts +33 -0
- package/dist/agent/channels/discord-rest.d.ts.map +1 -0
- package/dist/agent/channels/discord-rest.js +75 -0
- package/dist/agent/channels/discord-rest.js.map +1 -0
- package/dist/agent/channels/discord.d.ts +3 -0
- package/dist/agent/channels/discord.d.ts.map +1 -0
- package/dist/agent/channels/discord.js +230 -0
- package/dist/agent/channels/discord.js.map +1 -0
- package/dist/agent/commands/discord-command-sync.d.ts +66 -0
- package/dist/agent/commands/discord-command-sync.d.ts.map +1 -0
- package/dist/agent/commands/discord-command-sync.js +224 -0
- package/dist/agent/commands/discord-command-sync.js.map +1 -0
- package/dist/agent/commands/discord-commands.d.ts +45 -0
- package/dist/agent/commands/discord-commands.d.ts.map +1 -0
- package/dist/agent/commands/discord-commands.js +266 -0
- package/dist/agent/commands/discord-commands.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 +31 -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/identity/discord-link-consent.d.ts +13 -0
- package/dist/agent/identity/discord-link-consent.d.ts.map +1 -0
- package/dist/agent/identity/discord-link-consent.js +111 -0
- package/dist/agent/identity/discord-link-consent.js.map +1 -0
- package/dist/agent/ops.d.ts +2 -0
- package/dist/agent/ops.d.ts.map +1 -0
- package/dist/agent/ops.js +13 -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 +78 -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 +125 -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 +90 -0
- package/dist/agent/surface-parity.js.map +1 -0
- package/package.json +185 -0
- package/tsconfig.build.json +22 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type ApprovalCardRespondFn } from "@open-everywhere/core/agent/adapter-api.js";
|
|
2
|
+
import { type InteractionFollowUp } from "./discord-interaction-defer.js";
|
|
3
|
+
/** The `input_response.routed` sibling for a tap — the SAME two action strings the Slack leg writes,
|
|
4
|
+
* so "how often is a bridged approval settled by a button" is one query across surfaces rather than
|
|
5
|
+
* one per surface. RE-EXPORTED FROM CORE since OPE-1856: two files spelling one action name is two
|
|
6
|
+
* places it can drift, and the drift is silent. */
|
|
7
|
+
export declare const APPROVAL_CARD_SETTLED_AUDIT_ACTION = "input_response.card_settled";
|
|
8
|
+
export declare const APPROVAL_CARD_REFUSED_AUDIT_ACTION = "input_response.card_refused";
|
|
9
|
+
/** The interaction response body this handler returns. Loosely typed, exactly as its three sibling
|
|
10
|
+
* handlers type theirs, so the front route holds ONE seam type across all four. */
|
|
11
|
+
export type DiscordApprovalCardResponse = Record<string, unknown>;
|
|
12
|
+
/** The settle delivery this leg needs. CORE-OWNED and bound by the front route through
|
|
13
|
+
* `approvalCardRespondVia(args.attachSession)` — re-exported here, never re-spelled, so the address
|
|
14
|
+
* the answer takes cannot drift per surface (OPE-1863). */
|
|
15
|
+
export type ApprovalRespondFn = ApprovalCardRespondFn;
|
|
16
|
+
/** The host edges this door needs to defer. Both injected so a test drives the whole ack-then-edit
|
|
17
|
+
* sequence with no Discord and no host. */
|
|
18
|
+
export interface DiscordApprovalCardDeps {
|
|
19
|
+
/** The route's own `waitUntil`. ABSENT ⇒ no defer (see the header). */
|
|
20
|
+
readonly waitUntil?: (p: Promise<unknown>) => void;
|
|
21
|
+
/** The follow-up edit. Live default is {@link editOriginalInteractionResponse}. */
|
|
22
|
+
readonly edit?: (followUp: InteractionFollowUp, content: string) => Promise<boolean>;
|
|
23
|
+
}
|
|
24
|
+
/** What one verified tap did, for the front route's log line and for the test to assert on. */
|
|
25
|
+
export interface ApprovalCardClickOutcome {
|
|
26
|
+
readonly handled: boolean;
|
|
27
|
+
readonly reason: string;
|
|
28
|
+
/** A line to show the tapper alone, or undefined when the outcome needs no note. */
|
|
29
|
+
readonly notice?: string;
|
|
30
|
+
}
|
|
31
|
+
/** The interaction fields this leg reads, narrowed so nothing here needs Discord's own types. */
|
|
32
|
+
interface ParsedApprovalClick {
|
|
33
|
+
readonly actionId: string;
|
|
34
|
+
readonly channelId: string;
|
|
35
|
+
readonly userId: string;
|
|
36
|
+
readonly guildId?: string;
|
|
37
|
+
/**
|
|
38
|
+
* LEG B — Discord's own analogue of a guest: a guild member who has NOT cleared membership
|
|
39
|
+
* screening (`member.pending`). The same bundle already reads it off an interaction payload for the
|
|
40
|
+
* inbound trust grain (`discord-identity.ts`), and it is a POSITIVE per-person verdict, which is
|
|
41
|
+
* exactly what `admitInputResponse`'s guest leg takes. Absent ⇒ the probe could not be run (a DM tap
|
|
42
|
+
* carries no member record), and the leg is then INERT: "we could not ask" never becomes a refusal.
|
|
43
|
+
*/
|
|
44
|
+
readonly answererIsGuest?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Read the `oe_input:` custom_id out of a VERIFIED MESSAGE_COMPONENT interaction. `undefined` when
|
|
48
|
+
* the payload is not one of ours or is missing a field the decision needs — the front route then
|
|
49
|
+
* forwards it rather than half-handling it.
|
|
50
|
+
*
|
|
51
|
+
* THE CLICKER AND THE CHANNEL COME FROM THE ENVELOPE, never from a payload field the card could
|
|
52
|
+
* carry: the front has already verified Discord's Ed25519 signature over the raw body, so these are
|
|
53
|
+
* Discord's own statements. Discord puts the clicker at `user` in a DM and at `member.user` in a
|
|
54
|
+
* guild; both are read, so a DM tap and a guild tap are equally attributable.
|
|
55
|
+
*/
|
|
56
|
+
export declare function parseDiscordApprovalClick(raw: unknown): ParsedApprovalClick | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Settle one verified `oe_input:` tap: resolve it, admit it, deliver the answer to eve, consume the
|
|
59
|
+
* rows. NEVER THROWS.
|
|
60
|
+
*
|
|
61
|
+
* 🔴 CONSUME-ONCE, AND BOTH ROWS, and only after eve accepted the delivery. The ticket goes so a
|
|
62
|
+
* second tap cannot answer a request eve has already settled; the outstanding-question row goes so
|
|
63
|
+
* the room's NEXT message is an ordinary wake again rather than a second answer. A throw above skips
|
|
64
|
+
* both, which leaves the card tappable and the question answerable in words — the safe direction.
|
|
65
|
+
*
|
|
66
|
+
* THE LIMIT OF THAT, stated rather than implied and identical to the Slack leg's: the read and the
|
|
67
|
+
* delete are not one atomic step, so two deliveries arriving together each resolve against rows that
|
|
68
|
+
* are both still present. Both must be admitted and both must name an option the request offered, and
|
|
69
|
+
* eve's own resolution refuses the second delivery for a request it already settled — so one approval
|
|
70
|
+
* can READ as decided by two taps without being decided twice.
|
|
71
|
+
*/
|
|
72
|
+
export declare function settleDiscordApprovalCardClick(click: ParsedApprovalClick, respond: ApprovalRespondFn): Promise<ApprovalCardClickOutcome>;
|
|
73
|
+
/**
|
|
74
|
+
* Handle one verified `oe_input:` component click.
|
|
75
|
+
*
|
|
76
|
+
* Returns the interaction response body to send, or `null` for anything that is NOT one of ours —
|
|
77
|
+
* the front route then forwards it, so a click is never silently swallowed. NEVER throws.
|
|
78
|
+
*/
|
|
79
|
+
export declare function handleDiscordApprovalCardInteraction(raw: unknown, respond: ApprovalRespondFn, deps?: DiscordApprovalCardDeps): Promise<DiscordApprovalCardResponse | null>;
|
|
80
|
+
export {};
|
|
81
|
+
//# sourceMappingURL=discord-approval-card.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord-approval-card.d.ts","sourceRoot":"","sources":["../../../agent/channels/discord-approval-card.ts"],"names":[],"mappings":"AAyCA,OAAO,EAkBL,KAAK,qBAAqB,EAE3B,MAAM,4CAA4C,CAAC;AAEpD,OAAO,EAAmF,KAAK,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAI3J;;;oDAGoD;AACpD,eAAO,MAAM,kCAAkC,gCAA0C,CAAC;AAC1F,eAAO,MAAM,kCAAkC,gCAA0C,CAAC;AAQ1F;oFACoF;AACpF,MAAM,MAAM,2BAA2B,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAElE;;4DAE4D;AAC5D,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,CAAC;AAEtD;4CAC4C;AAC5C,MAAM,WAAW,uBAAuB;IACtC,uEAAuE;IACvE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;IACnD,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACtF;AAED,+FAA+F;AAC/F,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,oFAAoF;IACpF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,iGAAiG;AACjG,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;CACpC;AAMD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,mBAAmB,GAAG,SAAS,CA2BvF;AA8BD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,8BAA8B,CAAC,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAsG9I;AAkBD;;;;;GAKG;AACH,wBAAsB,oCAAoC,CACxD,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,iBAAiB,EAC1B,IAAI,GAAE,uBAA4B,GACjC,OAAO,CAAC,2BAA2B,GAAG,IAAI,CAAC,CAkC7C"}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// THE DISCORD 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. The sibling of
|
|
3
|
+
// providers/slack/agent/channels/slack-approval-card.ts, funnelling through the SAME core decision
|
|
4
|
+
// (`resolveApprovalCardClick`) exactly as access/discord-grant-click.ts funnels through
|
|
5
|
+
// `resolveGrantCardClick`. Core decides; this file reads Discord's envelope and writes Discord's
|
|
6
|
+
// answer.
|
|
7
|
+
//
|
|
8
|
+
// WHY THIS EXISTS RATHER THAN EVE'S OWN HITL ROUTE — the Slack reasoning verbatim, and it is
|
|
9
|
+
// structural rather than a not-yet. eve's Discord channel resolves its own `eve_input:` component
|
|
10
|
+
// clicks against the DISCORD channel's state and session address. A DM seat's parked request lives on
|
|
11
|
+
// the UNIFIED window — another channel, another session, another state bag — so a click on a card OE
|
|
12
|
+
// posted there has nothing for eve's route to resolve. It is OE's card, so it is OE's click.
|
|
13
|
+
//
|
|
14
|
+
// 🔴 THE SAME ADMISSION, THOUGH, LEG B INCLUDED. `admitInputResponse` is the ONE decision both HITL
|
|
15
|
+
// answer doors take, and `ingressSpendGate` with door `"answer"` is the same spend floor, consulted
|
|
16
|
+
// FIRST for the same reason: a tap RESUMES a parked turn, which is a decision to spend more on a scope
|
|
17
|
+
// nobody asked whether it could afford. Leg B — the positive per-answerer guest verdict Slack supplies
|
|
18
|
+
// with `slackClickerIsGuest` — rides Discord's own envelope instead of a probe: `member.pending` is
|
|
19
|
+
// this platform's analogue of an unverified account, and the same bundle already reads it for the
|
|
20
|
+
// inbound trust grain. A DM tap carries no member record, so the leg is INERT there rather than a
|
|
21
|
+
// refusal: "we could not ask" is never a no.
|
|
22
|
+
//
|
|
23
|
+
// 🔴 A REFUSAL LEAVES THE CARD LIVE. Nothing here consumes a row on any outcome but a delivered
|
|
24
|
+
// answer: the approval stays parked, the card stays tappable for somebody who may answer it, and the
|
|
25
|
+
// question stays answerable IN WORDS through the text leg (`question-bridge.ts`), which is untouched.
|
|
26
|
+
//
|
|
27
|
+
// 🔴 ACK, THEN SETTLE, THEN EDIT — and Discord is the surface where that ordering is forced (the
|
|
28
|
+
// OPE-1282 finding this door inherits). A settle calls `respond` into eve, which resumes a parked
|
|
29
|
+
// turn; Discord does NOT redeliver an interaction it has not seen answered, and an expired token
|
|
30
|
+
// leaves the tapper on a red "This interaction failed" while the approval really did settle. So this
|
|
31
|
+
// door answers with a DEFERRED EPHEMERAL ack (`type 5`), hands the settle to the route's `waitUntil`,
|
|
32
|
+
// and edits that placeholder with the outcome. The ephemeral flag rides the ACK, because Discord
|
|
33
|
+
// fixes a response's visibility at the first response.
|
|
34
|
+
//
|
|
35
|
+
// 🔴 NO `waitUntil` ⇒ NO DEFER. Without somewhere to run the work a deferred ack would leave the
|
|
36
|
+
// tapper on a permanent "thinking…", which is strictly worse than a slow answer. The handler awaits
|
|
37
|
+
// and answers `type 4` instead, and says so in the log.
|
|
38
|
+
//
|
|
39
|
+
// NEVER THROWS. This endpoint answers a person who is waiting within three seconds, and an unhandled
|
|
40
|
+
// throw becomes a red "interaction failed" they cannot act on.
|
|
41
|
+
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, OE_INPUT_CLICK_PREFIX, parseApprovalClickId, readApprovalCardTicket, readBridgedQuestion, resolveApprovalCardClick, resolvePartition, unifiedThreadRef, } from "@open-everywhere/core/agent/adapter-api.js";
|
|
42
|
+
import { buildDiscordAuth } from "./discord-identity.js";
|
|
43
|
+
import { deferredEphemeralResponse, editOriginalInteractionResponse, interactionFollowUp } from "./discord-interaction-defer.js";
|
|
44
|
+
const log = (m) => console.log(`[open-everywhere:discord] approval card: ${m}`);
|
|
45
|
+
/** The `input_response.routed` sibling for a tap — the SAME two action strings the Slack leg writes,
|
|
46
|
+
* so "how often is a bridged approval settled by a button" is one query across surfaces rather than
|
|
47
|
+
* one per surface. RE-EXPORTED FROM CORE since OPE-1856: two files spelling one action name is two
|
|
48
|
+
* places it can drift, and the drift is silent. */
|
|
49
|
+
export const APPROVAL_CARD_SETTLED_AUDIT_ACTION = CORE_APPROVAL_CARD_SETTLED_AUDIT_ACTION;
|
|
50
|
+
export const APPROVAL_CARD_REFUSED_AUDIT_ACTION = CORE_APPROVAL_CARD_REFUSED_AUDIT_ACTION;
|
|
51
|
+
// Discord interaction-response wire constants, defined locally for the same reason every sibling
|
|
52
|
+
// handler in this bundle defines its own: importing eve's channel module for two integers would
|
|
53
|
+
// widen this leaf's graph.
|
|
54
|
+
const RESPONSE_CHANNEL_MESSAGE = 4; // CHANNEL_MESSAGE_WITH_SOURCE
|
|
55
|
+
const EPHEMERAL = 64; // MessageFlags.EPHEMERAL — only the tapper sees it
|
|
56
|
+
function isObject(v) {
|
|
57
|
+
return typeof v === "object" && v !== null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Read the `oe_input:` custom_id out of a VERIFIED MESSAGE_COMPONENT interaction. `undefined` when
|
|
61
|
+
* the payload is not one of ours or is missing a field the decision needs — the front route then
|
|
62
|
+
* forwards it rather than half-handling it.
|
|
63
|
+
*
|
|
64
|
+
* THE CLICKER AND THE CHANNEL COME FROM THE ENVELOPE, never from a payload field the card could
|
|
65
|
+
* carry: the front has already verified Discord's Ed25519 signature over the raw body, so these are
|
|
66
|
+
* Discord's own statements. Discord puts the clicker at `user` in a DM and at `member.user` in a
|
|
67
|
+
* guild; both are read, so a DM tap and a guild tap are equally attributable.
|
|
68
|
+
*/
|
|
69
|
+
export function parseDiscordApprovalClick(raw) {
|
|
70
|
+
if (!isObject(raw))
|
|
71
|
+
return undefined;
|
|
72
|
+
if (raw["type"] !== 3)
|
|
73
|
+
return undefined; // not a MESSAGE_COMPONENT interaction
|
|
74
|
+
const data = isObject(raw["data"]) ? raw["data"] : undefined;
|
|
75
|
+
const actionId = typeof data?.["custom_id"] === "string" ? data["custom_id"] : undefined;
|
|
76
|
+
if (actionId === undefined || !actionId.startsWith(OE_INPUT_CLICK_PREFIX))
|
|
77
|
+
return undefined;
|
|
78
|
+
const direct = isObject(raw["user"]) ? raw["user"]["id"] : undefined;
|
|
79
|
+
const member = isObject(raw["member"]) ? raw["member"]["user"] : undefined;
|
|
80
|
+
const viaMember = isObject(member) ? member["id"] : undefined;
|
|
81
|
+
const userId = typeof direct === "string" && direct.length > 0 ? direct : typeof viaMember === "string" ? viaMember : undefined;
|
|
82
|
+
const channelRaw = raw["channel_id"];
|
|
83
|
+
const channelViaObject = isObject(raw["channel"]) ? raw["channel"]["id"] : undefined;
|
|
84
|
+
const channelId = typeof channelRaw === "string" && channelRaw.length > 0 ? channelRaw : typeof channelViaObject === "string" ? channelViaObject : undefined;
|
|
85
|
+
if (typeof userId !== "string" || userId.length === 0)
|
|
86
|
+
return undefined;
|
|
87
|
+
if (typeof channelId !== "string" || channelId.length === 0)
|
|
88
|
+
return undefined;
|
|
89
|
+
const guildId = typeof raw["guild_id"] === "string" && raw["guild_id"].length > 0 ? raw["guild_id"] : undefined;
|
|
90
|
+
// LEG B, read off the SAME verified envelope. `member.pending` is Discord's own statement that this
|
|
91
|
+
// account has not cleared the guild's membership screening; a payload that carries no member record
|
|
92
|
+
// (every DM tap) leaves the field absent, which the admission reads as "not asked".
|
|
93
|
+
const memberPending = isObject(raw["member"]) ? raw["member"]["pending"] : undefined;
|
|
94
|
+
return {
|
|
95
|
+
actionId,
|
|
96
|
+
channelId,
|
|
97
|
+
userId,
|
|
98
|
+
...(guildId ? { guildId } : {}),
|
|
99
|
+
...(typeof memberPending === "boolean" ? { answererIsGuest: memberPending } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** The bindable a partition resolve and the spend gate need, built through the bundle's OWN auth
|
|
103
|
+
* builder rather than a hand-spelled attribute bag: the partition a tap files under must be the one
|
|
104
|
+
* a MESSAGE in the same conversation files under, and two spellings is how those drift. */
|
|
105
|
+
function bindableForClick(click) {
|
|
106
|
+
return {
|
|
107
|
+
channel: { kind: "discord" },
|
|
108
|
+
session: {
|
|
109
|
+
auth: {
|
|
110
|
+
current: buildDiscordAuth({
|
|
111
|
+
channelId: click.channelId,
|
|
112
|
+
userId: click.userId,
|
|
113
|
+
...(click.guildId ? { guildId: click.guildId } : {}),
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The host copy that rides the delivered answer's `text` — the LABEL THIS DEPLOYMENT PUT ON THE
|
|
121
|
+
* BUTTON, read back out of the same allowlist that minted it, so the transcript says what the person
|
|
122
|
+
* actually chose. A tap carries no words, and writing the person's prose for them is not an option.
|
|
123
|
+
* The DECISION is never this value: eve settles on `optionId` alone.
|
|
124
|
+
*/
|
|
125
|
+
function approvalAnswerText(optionId) {
|
|
126
|
+
return APPROVAL_BUTTON_LABELS.get(optionId.toLowerCase())?.label ?? optionId.charAt(0).toUpperCase() + optionId.slice(1);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Settle one verified `oe_input:` tap: resolve it, admit it, deliver the answer to eve, consume the
|
|
130
|
+
* rows. NEVER THROWS.
|
|
131
|
+
*
|
|
132
|
+
* 🔴 CONSUME-ONCE, AND BOTH ROWS, and only after eve accepted the delivery. The ticket goes so a
|
|
133
|
+
* second tap cannot answer a request eve has already settled; the outstanding-question row goes so
|
|
134
|
+
* the room's NEXT message is an ordinary wake again rather than a second answer. A throw above skips
|
|
135
|
+
* both, which leaves the card tappable and the question answerable in words — the safe direction.
|
|
136
|
+
*
|
|
137
|
+
* THE LIMIT OF THAT, stated rather than implied and identical to the Slack leg's: the read and the
|
|
138
|
+
* delete are not one atomic step, so two deliveries arriving together each resolve against rows that
|
|
139
|
+
* are both still present. Both must be admitted and both must name an option the request offered, and
|
|
140
|
+
* eve's own resolution refuses the second delivery for a request it already settled — so one approval
|
|
141
|
+
* can READ as decided by two taps without being decided twice.
|
|
142
|
+
*/
|
|
143
|
+
export async function settleDiscordApprovalCardClick(click, respond) {
|
|
144
|
+
// 🔴 HOISTED OUT OF THE `try` SO THE `catch` CAN STAMP ITS ROW (OPE-1856) — the Slack sibling's
|
|
145
|
+
// reasoning verbatim. A throw anywhere below, the partition resolve included, must still leave a
|
|
146
|
+
// row saying this deployment took a tap and settled nothing.
|
|
147
|
+
let auditPartition;
|
|
148
|
+
try {
|
|
149
|
+
const parsed = parseApprovalClickId(click.actionId);
|
|
150
|
+
if (parsed === undefined)
|
|
151
|
+
return { handled: false, reason: "oe_input: custom_id did not parse" };
|
|
152
|
+
const org = deploymentOrg();
|
|
153
|
+
const store = getOperationalStore();
|
|
154
|
+
const bindable = bindableForClick(click);
|
|
155
|
+
const partition = resolvePartition(bindable);
|
|
156
|
+
auditPartition = partition;
|
|
157
|
+
// THE SPEND GATE FIRST, ahead of the identity admission — the same order and the same reason the
|
|
158
|
+
// Slack leg takes: over budget outranks identity, and a decline must not spend a roster lookup.
|
|
159
|
+
// A decline leaves the request PENDING.
|
|
160
|
+
// 🔴 THE `post` ARM CAPTURES, IT DOES NOT POST. The gate's refusal copy would otherwise land as
|
|
161
|
+
// a PUBLIC message in the channel the card sits in, telling the room that a person's tap was
|
|
162
|
+
// declined. This door already owns a private answer — the interaction response — so the copy is
|
|
163
|
+
// captured here and shown to the tapper alone, through the outcome's `notice`. The binding is
|
|
164
|
+
// local to this call, so two taps in flight cannot read each other's copy.
|
|
165
|
+
let declineNotice;
|
|
166
|
+
const spend = await ingressSpendGate({
|
|
167
|
+
bindable,
|
|
168
|
+
door: "answer",
|
|
169
|
+
principalId: click.userId,
|
|
170
|
+
...(click.guildId ? { workspaceRef: click.guildId } : {}),
|
|
171
|
+
post: async (text) => {
|
|
172
|
+
declineNotice = text;
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
if (spend === "declined") {
|
|
176
|
+
// AUDITED, not only logged (OPE-1856): a decline is this deployment refusing a person's tap,
|
|
177
|
+
// and a refusal nobody can count is indistinguishable from a tap that never arrived.
|
|
178
|
+
await auditApprovalCardTap({ platform: "discord", principalId: click.userId, partition, outcome: { kind: "refused", reason: "spend-declined" } });
|
|
179
|
+
log("tap declined by the spend gate — the approval stays parked");
|
|
180
|
+
return { handled: true, reason: "spend-declined", ...(declineNotice ? { notice: declineNotice } : {}) };
|
|
181
|
+
}
|
|
182
|
+
// The room the tap came from, in the SAME vocabulary the card's ticket stored — derived through
|
|
183
|
+
// core's own codec rather than spelled here, so the two sides cannot drift on the key's shape. A
|
|
184
|
+
// guild tap and a DM tap key differently, which is exactly what the codec already knows.
|
|
185
|
+
const containerKey = unifiedThreadRef(click.guildId ? { platform: "discord", channelId: click.channelId, guildId: click.guildId } : { platform: "discord", channelId: click.channelId, dm: true }).channelKey;
|
|
186
|
+
const routing = await resolveApprovalCardClick({ ticket: parsed.ticket, optionId: parsed.optionId, containerKey }, {
|
|
187
|
+
read: (ticket) => readApprovalCardTicket(store, org, ticket),
|
|
188
|
+
// The join with the TEXT leg — fail-CLOSED on an unreadable row: a store this leg cannot read
|
|
189
|
+
// cannot say the request is still parked, and settling on that guess is the one direction
|
|
190
|
+
// that spends a governed decision twice.
|
|
191
|
+
stillOutstanding: async (bareToken, requestId) => {
|
|
192
|
+
const outstanding = await readBridgedQuestion(store, org, bareToken).catch(() => undefined);
|
|
193
|
+
return outstanding?.requestId === requestId;
|
|
194
|
+
},
|
|
195
|
+
admit: async () => {
|
|
196
|
+
const verdict = await admitInputResponse({
|
|
197
|
+
org: partition.org,
|
|
198
|
+
platform: partition.platform,
|
|
199
|
+
workspace: partition.workspace,
|
|
200
|
+
surface: partition.surface,
|
|
201
|
+
surfaceKey: partition.surfaceKey,
|
|
202
|
+
principalId: click.userId,
|
|
203
|
+
responses: 1,
|
|
204
|
+
// Leg B — the positive per-answerer verdict, and INERT when the envelope could not answer
|
|
205
|
+
// it (`undefined` never refuses), exactly as the Slack sibling's probe is.
|
|
206
|
+
...(click.answererIsGuest === undefined ? {} : { answererIsGuest: click.answererIsGuest }),
|
|
207
|
+
});
|
|
208
|
+
return { ok: verdict.ok, floor: verdict.ok ? verdict.floor : "", notice: verdict.ok ? "" : verdict.notice };
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
if (routing.kind === "refuse") {
|
|
212
|
+
await auditApprovalCardTap({ platform: "discord", principalId: click.userId, partition, outcome: { kind: "refused", reason: routing.reason } });
|
|
213
|
+
log(`tap settled nothing (${routing.reason}) — the approval stays parked and answerable in words`);
|
|
214
|
+
return { handled: true, reason: routing.reason, ...(routing.notice ? { notice: routing.notice } : {}) };
|
|
215
|
+
}
|
|
216
|
+
const label = approvalAnswerText(routing.optionId);
|
|
217
|
+
// 🔴 DELIVERED BY SESSION ID, never by the window token (OPE-1863) — the Slack sibling's
|
|
218
|
+
// reasoning verbatim: a continuation token is channel-local, this route is the DISCORD channel's,
|
|
219
|
+
// and `from(bareToken)` here named a session that never existed. The token's job is the
|
|
220
|
+
// outstanding-question row on the two lines that follow.
|
|
221
|
+
await respond(routing.sessionId, { requestId: routing.requestId, optionId: routing.optionId, text: label });
|
|
222
|
+
await forgetApprovalCardTicket(store, org, parsed.ticket);
|
|
223
|
+
await forgetBridgedQuestion(store, org, routing.bareToken);
|
|
224
|
+
await auditApprovalCardTap({
|
|
225
|
+
platform: "discord",
|
|
226
|
+
principalId: click.userId,
|
|
227
|
+
partition,
|
|
228
|
+
outcome: { kind: "settled", requestId: routing.requestId, optionId: routing.optionId, floor: routing.floor },
|
|
229
|
+
});
|
|
230
|
+
log(`tap settled ${routing.requestId} as "${routing.optionId}" (floor ${routing.floor})`);
|
|
231
|
+
return { handled: true, reason: "settled", notice: `${label} — recorded. The request is settled.` };
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
// The approval is still parked and the card is still tappable, so the honest outcome is to say so
|
|
235
|
+
// and leave both rows in place.
|
|
236
|
+
// AUDITED, not only logged (OPE-1856): a tap that threw left no trace at all, so it could not be
|
|
237
|
+
// told apart from a tap that never arrived. `auditApprovalCardTap` never throws.
|
|
238
|
+
await auditApprovalCardTap({ platform: "discord", principalId: click.userId, ...(auditPartition === undefined ? {} : { partition: auditPartition }), outcome: { kind: "refused", reason: "error" } });
|
|
239
|
+
log(`tap failed (${err?.message ?? String(err)}) — nothing was settled, the approval stays parked`);
|
|
240
|
+
return { handled: true, reason: "error", notice: "That could not be answered right now. Nothing was settled — the request is still open, and you can answer it in words." };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The line the tapper reads, for an outcome that carried no notice of its own.
|
|
245
|
+
*
|
|
246
|
+
* 🔴 A REFUSAL MUST NEVER READ AS A SETTLEMENT. This door ANSWERS — it edits a deferred placeholder —
|
|
247
|
+
* so a default of "Recorded." would tell somebody whose tap settled nothing that their approval had
|
|
248
|
+
* landed, and they would stop waiting for it. Core attaches a `notice` only to the `refused` arm, so
|
|
249
|
+
* `no-ticket`, `foreign-room`, `unoffered-option` and `already-answered` all arrive here bare. The
|
|
250
|
+
* Slack sibling can be SILENT in these cases, which is not a false statement; this one cannot, so the
|
|
251
|
+
* default is the honest sentence and "Recorded." is reserved for a settle.
|
|
252
|
+
*/
|
|
253
|
+
function tapperLine(outcome) {
|
|
254
|
+
if (outcome.notice !== undefined && outcome.notice.length > 0)
|
|
255
|
+
return outcome.notice;
|
|
256
|
+
if (outcome.reason === "settled")
|
|
257
|
+
return "Recorded.";
|
|
258
|
+
return "Nothing was settled — the request is still open, and you can answer it in words.";
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Handle one verified `oe_input:` component click.
|
|
262
|
+
*
|
|
263
|
+
* Returns the interaction response body to send, or `null` for anything that is NOT one of ours —
|
|
264
|
+
* the front route then forwards it, so a click is never silently swallowed. NEVER throws.
|
|
265
|
+
*/
|
|
266
|
+
export async function handleDiscordApprovalCardInteraction(raw, respond, deps = {}) {
|
|
267
|
+
const click = parseDiscordApprovalClick(raw);
|
|
268
|
+
if (click === undefined)
|
|
269
|
+
return null;
|
|
270
|
+
const followUp = interactionFollowUp(raw);
|
|
271
|
+
if (deps.waitUntil === undefined || followUp === undefined) {
|
|
272
|
+
// NO PLACE TO RUN THE WORK, OR NO ADDRESS TO ANSWER AT ⇒ settle on the response path. Slow, and
|
|
273
|
+
// still correct: a deferred ack we could not follow up would leave a permanent "thinking…".
|
|
274
|
+
log(`tap answering synchronously (${deps.waitUntil === undefined ? "no waitUntil" : "no follow-up address"}) — a slow settle may outlive the interaction token`);
|
|
275
|
+
const outcome = await settleDiscordApprovalCardClick(click, respond);
|
|
276
|
+
if (!outcome.handled)
|
|
277
|
+
return null;
|
|
278
|
+
return { type: RESPONSE_CHANNEL_MESSAGE, data: { content: tapperLine(outcome), flags: EPHEMERAL } };
|
|
279
|
+
}
|
|
280
|
+
const edit = deps.edit ?? editOriginalInteractionResponse;
|
|
281
|
+
deps.waitUntil((async () => {
|
|
282
|
+
// TOTAL. This runs after the response has gone; a throw here reaches the host's unhandled
|
|
283
|
+
// rejection handler and tells the tapper nothing. `settleDiscordApprovalCardClick` is already
|
|
284
|
+
// total, so the catch is for the edit's own transport.
|
|
285
|
+
let text;
|
|
286
|
+
try {
|
|
287
|
+
const outcome = await settleDiscordApprovalCardClick(click, respond);
|
|
288
|
+
text = tapperLine(outcome);
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
log(`deferred tap settle failed: ${err?.message ?? String(err)}`);
|
|
292
|
+
text = "That could not be answered right now. Nothing was settled — the request is still open, and you can answer it in words.";
|
|
293
|
+
}
|
|
294
|
+
// 🔴 A FAILED EDIT IS LOUD BUT NOT FATAL: the settle has already landed and is audited by core.
|
|
295
|
+
// What is lost is the tapper's explanation — which is exactly what an expired token loses — so
|
|
296
|
+
// it must be findable in the log.
|
|
297
|
+
const shown = await edit(followUp, text);
|
|
298
|
+
if (!shown)
|
|
299
|
+
log("tap answer could NOT be shown to the tapper — the settle landed and is audited, the tapper was not told");
|
|
300
|
+
})());
|
|
301
|
+
return deferredEphemeralResponse();
|
|
302
|
+
}
|
|
303
|
+
//# sourceMappingURL=discord-approval-card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord-approval-card.js","sourceRoot":"","sources":["../../../agent/channels/discord-approval-card.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { type ContextLine, type NormalizedMessage, type BackscrollTrustFor, type SeatCatchUpFetch, type SeatCatchUpRequest, type WakeMessageInput, type BindableContext } from "@open-everywhere/core/agent/adapter-api.js";
|
|
2
|
+
/** The triggering wake, narrowed to what the backscroll needs. */
|
|
3
|
+
export interface DiscordBackscrollInput {
|
|
4
|
+
/** The surface to READ — the channel/thread the human's message is IN (msg.channelId), NOT the
|
|
5
|
+
* re-keyed reply thread. A Discord thread is its own channel id, so this is one endpoint. */
|
|
6
|
+
readonly channelId: string;
|
|
7
|
+
/** The triggering message's id — we read strictly OLDER than this (`?before=`). */
|
|
8
|
+
readonly triggerMessageId: string;
|
|
9
|
+
/** True when `channelId` is a thread (types 10/11/12): apply the since-last-agent slice. A base
|
|
10
|
+
* channel keeps the recent tail (last N) instead — mirrors Slack's replies/history split. */
|
|
11
|
+
readonly isThread: boolean;
|
|
12
|
+
/** The bot's OWN Discord author id (relay `botId`) — labels agent lines + anchors the slice.
|
|
13
|
+
* Absent (older relay) → own posts read as a bot and there's no slice boundary (fail-soft). */
|
|
14
|
+
readonly botId?: string;
|
|
15
|
+
}
|
|
16
|
+
/** A Discord REST caller: GETs `/channels/{channelId}/messages?<query>`, returns parsed JSON
|
|
17
|
+
* (an array of message objects) or undefined on ANY failure (fail-soft). Injected so the fetcher
|
|
18
|
+
* runs without a network in tests but defaults to the live bot-token call. */
|
|
19
|
+
export type DiscordRequester = (channelId: string, query: Record<string, string>) => Promise<unknown>;
|
|
20
|
+
export interface DiscordBackscrollDeps {
|
|
21
|
+
readonly request: DiscordRequester;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Map raw Discord message objects (`GET /channels/{id}/messages` — raw REST JSON, probe
|
|
25
|
+
* defensively) into {@link ContextLine}s, dropping system-type + empty-content messages. `botId`
|
|
26
|
+
* is the agent's own author id: a message by it is labelled `agent` (Discord uses one `author.id`
|
|
27
|
+
* for everyone — bots included — so we compare ids, same idea as Slack comparing bot_id/user_id).
|
|
28
|
+
* Order is PRESERVED — the caller reverses to oldest-first before rendering. Pure. Exported for
|
|
29
|
+
* the test oracle. Kept HERE (not shared): Discord's message JSON is shaped differently from
|
|
30
|
+
* Slack's, so the mapping is genuinely platform-specific; only the ContextLine target + renderer
|
|
31
|
+
* are shared (context-render.ts).
|
|
32
|
+
*/
|
|
33
|
+
export declare function linesFromDiscordMessages(raw: ReadonlyArray<Record<string, unknown>>, botId?: string): ContextLine[];
|
|
34
|
+
/** Resolves a sibling author's real guild roles for the register query below — injected so the
|
|
35
|
+
* grading step is unit-testable offline. Live wiring: {@link discordAuthorRoles}. `undefined` =
|
|
36
|
+
* cannot check (never a guess); a real (possibly empty) array = verified role evidence. */
|
|
37
|
+
export type DiscordRoleProbe = (guildId: string, userId: string) => Promise<readonly string[] | undefined>;
|
|
38
|
+
/**
|
|
39
|
+
* PER-LINE trust for a mention/name-drop backscroll's {@link ContextLine}s (OPE-475) — the Discord
|
|
40
|
+
* analogue of Slack's regrade step, converging both backscroll shapes on ONE trust story rather than
|
|
41
|
+
* a second mechanism. Two additive facts, mirroring exactly what the TRIGGER already carries
|
|
42
|
+
* (discord-gateway.ts `resolveWakeTrustFromContext` + `authorFacts`), resolved per SIBLING:
|
|
43
|
+
*
|
|
44
|
+
* `trust` — {@link resolveWakeTrustFromContext} off the SAME binding the trigger reads, with
|
|
45
|
+
* `principalFallback` OFF (a sibling has its own author — see that function's own
|
|
46
|
+
* per-message-resolution note). Read by the RENDER. Resolved for every `user` line,
|
|
47
|
+
* unconditionally — no I/O, no gate.
|
|
48
|
+
* `authorTrust` — the OPERATOR's author-trust register query (identity/author-trust.ts, OPE-516):
|
|
49
|
+
* Discord's ONLY per-author refutation reachable here. `member.pending` (the
|
|
50
|
+
* trigger's own signal) is NOT available: it rides the gateway relay's MESSAGE_CREATE
|
|
51
|
+
* member object, and Discord's documented Message object (the shape
|
|
52
|
+
* `GET /channels/{id}/messages` — what this backscroll fetch reads — returns) has
|
|
53
|
+
* no `member` field at all. Read ONLY by the taint join.
|
|
54
|
+
*
|
|
55
|
+
* 🔴 THE REGISTER QUERY IS BUILT FROM VERIFIED ROLE EVIDENCE, NEVER FROM AN UNCHECKED GUESS (fixed
|
|
56
|
+
* after a caught bug: an earlier revision hardcoded `memberships: []` on EVERY sibling — on a guild
|
|
57
|
+
* armed the documented-usual way, `{ membership: "discord/<g>/role:*" }`, that made EVERY sibling
|
|
58
|
+
* look role-less and therefore `external`, so a mention wake that hydrated ordinary colleagues'
|
|
59
|
+
* messages marked the taint bit on a role-armed guild — the always-on-bit failure this whole plane
|
|
60
|
+
* exists to avoid). So the role fetch is COST-GATED and FAIL-OPEN, in that order:
|
|
61
|
+
*
|
|
62
|
+
* 1. UNARMED tenant (the common case — `armedAuthorTrustTenants()` doesn't list `discord/<guild>`):
|
|
63
|
+
* no register query is even worth building, so NO role fetch runs at all. Zero new I/O on the
|
|
64
|
+
* default posture.
|
|
65
|
+
* 2. ARMED tenant: each DISTINCT candidate author (a `user` line with a `senderId`) gets ONE
|
|
66
|
+
* {@link DiscordRoleProbe} call (TTL-cached — {@link discordAuthorRoles}), mirroring exactly how
|
|
67
|
+
* {@link regradeSlackContextLineTrust}'s probe is one call per distinct author, not per message.
|
|
68
|
+
* 3. The probe's THREE outcomes are kept apart, never collapsed to a boolean: a real (possibly
|
|
69
|
+
* empty, on a verified 404) roles array builds the SAME group-rung `{tenantKey}/role:<id>` keys
|
|
70
|
+
* the trigger's own `authorFacts` builds from `msg.memberRoles`; `undefined` (no token, a
|
|
71
|
+
* network error, an unreadable body) means "cannot check", and that line gets NO `authorTrust`
|
|
72
|
+
* at all — SKIPPED, never an unverified `[]`. "Cannot check" is not "checked and empty".
|
|
73
|
+
*
|
|
74
|
+
* 🔴 THE REGISTER LEG NEVER DEMOTES `trust` — it matches the trigger, and access/taint-signal.ts's own
|
|
75
|
+
* design: the register is a JOIN-ONLY refutation, decoupled from the render (see `nonMemberChatTrust`).
|
|
76
|
+
* So a register-refuted Discord backscroll sibling marks the turn but does not render the reference-only
|
|
77
|
+
* note — an honest limit of what this endpoint can tell the model, not a shortfall against Slack's
|
|
78
|
+
* parity (Slack's refutation is a SURFACE fact and demotes the render; Discord has none here).
|
|
79
|
+
*
|
|
80
|
+
* `agent`/`bot`/`unknown` lines are left untouched — machine or unattributable authors are not this
|
|
81
|
+
* leg's question.
|
|
82
|
+
*/
|
|
83
|
+
export declare function gradeDiscordContextLines(lines: readonly ContextLine[], ctx: BindableContext, guildId: string | undefined, roleProbe?: DiscordRoleProbe): Promise<ContextLine[]>;
|
|
84
|
+
/**
|
|
85
|
+
* Fetch recent conversation history for a Discord wake/mention as RAW, oldest-first
|
|
86
|
+
* {@link ContextLine}s, capped at {@link WAKE_BACKSCROLL_MAX}. `undefined` = nothing to add OR any
|
|
87
|
+
* failure — always non-fatal. Both Discord wake dispatch paths (discord-gateway.ts, native and
|
|
88
|
+
* single-window divert) call this directly, grade the lines with gradeDiscordContextLines
|
|
89
|
+
* (OPE-475), and render them via the shared renderContext THEMSELVES — no pre-rendered string is
|
|
90
|
+
* built inside this module.
|
|
91
|
+
*
|
|
92
|
+
* One endpoint (`GET /channels/{channelId}/messages?limit&before`) for both cases:
|
|
93
|
+
* - IN-THREAD (`isThread`): strictly-older messages, then sliced to "since the agent's last
|
|
94
|
+
* reply" (a resumed thread session already holds everything up to its last reply).
|
|
95
|
+
* - BASE CHANNEL: strictly-older messages, most recent N.
|
|
96
|
+
*/
|
|
97
|
+
export declare function fetchDiscordBackscrollLines(input: DiscordBackscrollInput, deps?: DiscordBackscrollDeps): Promise<{
|
|
98
|
+
readonly lines: ContextLine[];
|
|
99
|
+
readonly source: string;
|
|
100
|
+
} | undefined>;
|
|
101
|
+
/** Reachback (hours) for the raw Discord window: how far back from the trigger we paginate. Shares
|
|
102
|
+
* the SAME env + 7d default as the Slack adapter (slack-wake-backscroll.ts RAW_FETCH_REACHBACK_HOURS)
|
|
103
|
+
* — it is one supervisor-backscroll concept, kept in sync by reading the same knob. Generous enough
|
|
104
|
+
* that the spine (the most-recent cluster, exempt from the 36h enrichment cap) still spans a
|
|
105
|
+
* weekend/overnight gap, yet a hard floor bounding the fetch. */
|
|
106
|
+
export declare const DISCORD_RAW_FETCH_REACHBACK_HOURS: number;
|
|
107
|
+
/**
|
|
108
|
+
* The creation time (epoch ms) encoded in a Discord snowflake id: the top 42 bits are
|
|
109
|
+
* `(unix_ms - DISCORD_EPOCH_MS)`. Robust — every message id carries it, so we can derive a time even
|
|
110
|
+
* when the raw `timestamp` field is absent/garbled. Returns undefined for a non-numeric / non-positive
|
|
111
|
+
* id (fail-soft; the caller drops that message from the window rather than guessing a time).
|
|
112
|
+
*/
|
|
113
|
+
export declare function snowflakeToMs(id: string): number | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* The VISIBILITY / TRUST GATE (the load-bearing SECURITY filter) — the Discord parity of
|
|
116
|
+
* slack-wake-backscroll.ts isBackscrollVisible, kept LOCAL (not imported from a slack-named file, the
|
|
117
|
+
* same DAG discipline as linesFromDiscordMessages). Backscroll must never surface a message it can't
|
|
118
|
+
* ATTRIBUTE. KEEPS: the bot's OWN prior posts (`agent` → `self`); identifiable humans (`user` →
|
|
119
|
+
* `human`); ATTRIBUTABLE foreign bots (`bot` WITH a bot id → `bot`) as MARKED AWARENESS (clearly
|
|
120
|
+
* bot-flagged, CONTEXT-ONLY, never a wake trigger — dropping them would break conversational
|
|
121
|
+
* coherence when a human replies to a bot post). DROPS ONLY unattributable senders (`unknown`, or a
|
|
122
|
+
* `bot` with no id) — no auth'd author ⇒ fail closed. (System-subtype / empty-content noise is already
|
|
123
|
+
* dropped upstream by linesFromDiscordMessages.) The neutral core never sets `trigger` on backscroll,
|
|
124
|
+
* and the gateway's live wake gate still drops bot authors as triggers, so a bot post here is
|
|
125
|
+
* awareness, never a response obligation. */
|
|
126
|
+
export declare function isDiscordBackscrollVisible(line: ContextLine): boolean;
|
|
127
|
+
/** The triggering first wake, narrowed to what the neutral-core window needs (mirrors Slack's
|
|
128
|
+
* {@link import("./slack-wake-backscroll.js").WakeEnvelopeBackscrollInput}). */
|
|
129
|
+
export interface DiscordBackscrollWindowInput {
|
|
130
|
+
/** The surface to READ — the channel/thread the trigger is IN (a Discord thread is its own id). */
|
|
131
|
+
readonly channelId: string;
|
|
132
|
+
/** The triggering message's id — the window's upper (exclusive) bound (`before=`) and, decoded to
|
|
133
|
+
* ms, the reachback origin the core clusters against. */
|
|
134
|
+
readonly triggerMessageId: string;
|
|
135
|
+
/** True when `channelId` is a thread (types 10/11/12) — every message is then tagged `thread`. */
|
|
136
|
+
readonly isThread: boolean;
|
|
137
|
+
/** The bot's OWN Discord author id (relay `botId`) — labels its own posts `self`. Absent (older
|
|
138
|
+
* relay) → own posts read as an attributable bot (still visible), never a crash. */
|
|
139
|
+
readonly botId?: string;
|
|
140
|
+
/** Hours to look back from the trigger for the raw fetch. Defaults to
|
|
141
|
+
* {@link DISCORD_RAW_FETCH_REACHBACK_HOURS}. */
|
|
142
|
+
readonly reachbackHours?: number;
|
|
143
|
+
/**
|
|
144
|
+
* THE CATCH-UP BOUND (OPE-1726, the Slack seam's exact field): epoch milliseconds of the seat's own
|
|
145
|
+
* LAST WAKE, so this read returns what the channel said since the seat last looked. It only ever
|
|
146
|
+
* NARROWS the window — the reachback floor above still applies — so a seat quiet for a month reads
|
|
147
|
+
* the recent tail, and a seat with no recorded wake reads the bootstrap window unchanged.
|
|
148
|
+
*/
|
|
149
|
+
readonly sinceMs?: number;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* DISCORD FETCH SEAM: fetch a bounded, visibility-GATED, NORMALIZED backscroll window for a first
|
|
153
|
+
* wake. This is the platform boundary — the neutral core ({@link fetchedBackscroll}) consumes its
|
|
154
|
+
* output. FAIL-SOFT: any error/failure yields `[]`. Deduped by message id.
|
|
155
|
+
*/
|
|
156
|
+
export declare function fetchDiscordBackscrollWindow(input: DiscordBackscrollWindowInput, deps?: DiscordBackscrollDeps, failureMode?: "soft" | "throw"): Promise<NormalizedMessage[]>;
|
|
157
|
+
/**
|
|
158
|
+
* Fetch + cluster a Discord first-wake backscroll into session-tagged {@link WakeMessageInput}
|
|
159
|
+
* entries (oldest-first, no `trigger`). Thin orchestration over the Discord fetch seam
|
|
160
|
+
* ({@link fetchDiscordBackscrollWindow}) + the neutral clustering/bounds core
|
|
161
|
+
* ({@link fetchedBackscroll}) — the exact parity of slack-wake-backscroll.ts
|
|
162
|
+
* fetchWakeEnvelopeBackscroll. FAIL-SOFT ABSOLUTELY.
|
|
163
|
+
*
|
|
164
|
+
* Per-message trust mirrors the Slack seam: pass a `trustFor` resolver (build it with
|
|
165
|
+
* identity/wake-trust.ts `backscrollTrustResolver`, off the confirmInternal verdict + domain-grain
|
|
166
|
+
* channelKey) so each backscroll author gets its real roster/gate-derived tier (F3). When omitted,
|
|
167
|
+
* fetchedBackscroll's fail-closed per-sender default applies (a human → `unauth`, self/bot →
|
|
168
|
+
* `relay`), never a blanket `peer`.
|
|
169
|
+
*
|
|
170
|
+
* NOTE: this is a proven DROP-IN with no live caller yet — Discord has no wake-envelope render
|
|
171
|
+
* surface to render the `<session>` output on a live wake (see the file header + the
|
|
172
|
+
* TODO(discord-supervisor-backscroll) at discord-gateway.ts's dispatch seam). The `trustFor` seam
|
|
173
|
+
* is wired here ahead of that surface so, when the Discord supervisor lands, it passes the resolver
|
|
174
|
+
* exactly as slack-events.ts's live supervisor deps do for fetchWakeEnvelopeBackscroll.
|
|
175
|
+
*/
|
|
176
|
+
export declare function fetchDiscordWakeEnvelopeBackscroll(input: DiscordBackscrollWindowInput, deps?: DiscordBackscrollDeps, trustFor?: BackscrollTrustFor): Promise<WakeMessageInput[]>;
|
|
177
|
+
/**
|
|
178
|
+
* DISCORD'S REGISTERED CATCH-UP FETCH (OPE-1726) — the one thing this surface contributes to the
|
|
179
|
+
* shared since-last-wake path (core channels/seat-catch-up.ts). It reuses the window fetch above,
|
|
180
|
+
* addressed by the neutral request shape: the seat's container, bounded below by its last wake.
|
|
181
|
+
* 🔴 THE UPPER BOUND IS A MESSAGE ID, not a time, because `GET /channels/:id/messages` pages with
|
|
182
|
+
* `before=<snowflake>` and takes no timestamp. `excludeId` IS that snowflake (the trigger's own id),
|
|
183
|
+
* and with none the read anchors on a snowflake synthesised from `untilMs` — Discord ids embed their
|
|
184
|
+
* own millisecond, so the synthesised anchor orders correctly even though it names no real message.
|
|
185
|
+
* The shared path re-applies both bounds on its own side, so a page that overshoots is trimmed there.
|
|
186
|
+
* `threadRoot` present ⇒ the seat is bound to a thread, and a Discord thread IS its own channel id,
|
|
187
|
+
* so the read addresses that id and tags every message `thread`.
|
|
188
|
+
* A transport/read failure propagates to core's shared boundary, which keeps the wake alive and
|
|
189
|
+
* records `failed` instead of reporting a quiet channel (OPE-1842). Bootstrap callers keep the
|
|
190
|
+
* window fetch's default fail-soft mode.
|
|
191
|
+
*/
|
|
192
|
+
export declare function fetchDiscordSeatCatchUp(req: SeatCatchUpRequest, deps?: DiscordBackscrollDeps): Promise<readonly NormalizedMessage[]>;
|
|
193
|
+
/**
|
|
194
|
+
* A Discord snowflake encodes MILLISECONDS and the ISO `timestamp` field carries them too (OPE-1847,
|
|
195
|
+
* `core/agent/channels/seat-catch-up.ts` {@link SeatCatchUpDeclaration.timestampPrecisionMs}).
|
|
196
|
+
* 🔴 ONE MILLISECOND IS NOT ZERO WIDENING. The road widens the LOWER bound by nothing here and the
|
|
197
|
+
* UPPER bound by exactly one millisecond, so a message stamped at the trigger's own instant is
|
|
198
|
+
* INSIDE the window and `excludeId` is the only thing that removes the trigger. The paging call
|
|
199
|
+
* already anchors on `before=<the trigger snowflake>`, which is exclusive, and a message that did
|
|
200
|
+
* arrive would carry that snowflake as its id.
|
|
201
|
+
* 🔴 IT IS STILL DECLARED, and the parity test requires it. The declaration is what makes "this
|
|
202
|
+
* surface needs the finest unit" a CHECKED claim rather than an omission, and an omission is
|
|
203
|
+
* indistinguishable from the GitHub and Notion defect this whole mechanism exists to close.
|
|
204
|
+
*/
|
|
205
|
+
export declare const DISCORD_TIMESTAMP_PRECISION_MS = 1;
|
|
206
|
+
/** The registered form — the transport seam is fixed to the live one. Tests drive
|
|
207
|
+
* {@link fetchDiscordSeatCatchUp} with a canned requester instead. */
|
|
208
|
+
export declare const discordSeatCatchUpFetch: SeatCatchUpFetch;
|
|
209
|
+
//# sourceMappingURL=discord-backscroll.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord-backscroll.d.ts","sourceRoot":"","sources":["../../../agent/channels/discord-backscroll.ts"],"names":[],"mappings":"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"}
|