@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.
Files changed (166) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +47 -0
  4. package/agent/access/discord-grant-click.ts +237 -0
  5. package/agent/approvals/discord-admin.ts +227 -0
  6. package/agent/approvals/discord-card.ts +248 -0
  7. package/agent/approvals/discord-flow.ts +469 -0
  8. package/agent/approvals/discord-scope-label.ts +27 -0
  9. package/agent/channels/discord-approval-card.ts +373 -0
  10. package/agent/channels/discord-backscroll.ts +593 -0
  11. package/agent/channels/discord-bot-name.ts +113 -0
  12. package/agent/channels/discord-channel-info.ts +83 -0
  13. package/agent/channels/discord-directory.ts +179 -0
  14. package/agent/channels/discord-dispatch-target.ts +103 -0
  15. package/agent/channels/discord-events.ts +175 -0
  16. package/agent/channels/discord-front.ts +355 -0
  17. package/agent/channels/discord-gateway.ts +3256 -0
  18. package/agent/channels/discord-identity.ts +269 -0
  19. package/agent/channels/discord-interaction-defer.ts +123 -0
  20. package/agent/channels/discord-message-ops.ts +127 -0
  21. package/agent/channels/discord-origin-codec.ts +105 -0
  22. package/agent/channels/discord-post-arm.ts +230 -0
  23. package/agent/channels/discord-post.ts +463 -0
  24. package/agent/channels/discord-read.ts +140 -0
  25. package/agent/channels/discord-rest.ts +85 -0
  26. package/agent/channels/discord.ts +244 -0
  27. package/agent/commands/discord-command-sync.ts +285 -0
  28. package/agent/commands/discord-commands.ts +291 -0
  29. package/agent/contributions.ts +31 -0
  30. package/agent/docs-generated.ts +15 -0
  31. package/agent/identity/discord-link-consent.ts +124 -0
  32. package/agent/ops.ts +14 -0
  33. package/agent/provider-events.ts +86 -0
  34. package/agent/register.ts +147 -0
  35. package/agent/surface-parity.ts +95 -0
  36. package/dist/agent/access/discord-grant-click.d.ts +25 -0
  37. package/dist/agent/access/discord-grant-click.d.ts.map +1 -0
  38. package/dist/agent/access/discord-grant-click.js +210 -0
  39. package/dist/agent/access/discord-grant-click.js.map +1 -0
  40. package/dist/agent/approvals/discord-admin.d.ts +63 -0
  41. package/dist/agent/approvals/discord-admin.d.ts.map +1 -0
  42. package/dist/agent/approvals/discord-admin.js +219 -0
  43. package/dist/agent/approvals/discord-admin.js.map +1 -0
  44. package/dist/agent/approvals/discord-card.d.ts +102 -0
  45. package/dist/agent/approvals/discord-card.d.ts.map +1 -0
  46. package/dist/agent/approvals/discord-card.js +217 -0
  47. package/dist/agent/approvals/discord-card.js.map +1 -0
  48. package/dist/agent/approvals/discord-flow.d.ts +109 -0
  49. package/dist/agent/approvals/discord-flow.d.ts.map +1 -0
  50. package/dist/agent/approvals/discord-flow.js +375 -0
  51. package/dist/agent/approvals/discord-flow.js.map +1 -0
  52. package/dist/agent/approvals/discord-scope-label.d.ts +3 -0
  53. package/dist/agent/approvals/discord-scope-label.d.ts.map +1 -0
  54. package/dist/agent/approvals/discord-scope-label.js +27 -0
  55. package/dist/agent/approvals/discord-scope-label.js.map +1 -0
  56. package/dist/agent/channels/discord-approval-card.d.ts +81 -0
  57. package/dist/agent/channels/discord-approval-card.d.ts.map +1 -0
  58. package/dist/agent/channels/discord-approval-card.js +303 -0
  59. package/dist/agent/channels/discord-approval-card.js.map +1 -0
  60. package/dist/agent/channels/discord-backscroll.d.ts +209 -0
  61. package/dist/agent/channels/discord-backscroll.d.ts.map +1 -0
  62. package/dist/agent/channels/discord-backscroll.js +505 -0
  63. package/dist/agent/channels/discord-backscroll.js.map +1 -0
  64. package/dist/agent/channels/discord-bot-name.d.ts +19 -0
  65. package/dist/agent/channels/discord-bot-name.d.ts.map +1 -0
  66. package/dist/agent/channels/discord-bot-name.js +98 -0
  67. package/dist/agent/channels/discord-bot-name.js.map +1 -0
  68. package/dist/agent/channels/discord-channel-info.d.ts +22 -0
  69. package/dist/agent/channels/discord-channel-info.d.ts.map +1 -0
  70. package/dist/agent/channels/discord-channel-info.js +65 -0
  71. package/dist/agent/channels/discord-channel-info.js.map +1 -0
  72. package/dist/agent/channels/discord-directory.d.ts +11 -0
  73. package/dist/agent/channels/discord-directory.d.ts.map +1 -0
  74. package/dist/agent/channels/discord-directory.js +170 -0
  75. package/dist/agent/channels/discord-directory.js.map +1 -0
  76. package/dist/agent/channels/discord-dispatch-target.d.ts +15 -0
  77. package/dist/agent/channels/discord-dispatch-target.d.ts.map +1 -0
  78. package/dist/agent/channels/discord-dispatch-target.js +99 -0
  79. package/dist/agent/channels/discord-dispatch-target.js.map +1 -0
  80. package/dist/agent/channels/discord-events.d.ts +58 -0
  81. package/dist/agent/channels/discord-events.d.ts.map +1 -0
  82. package/dist/agent/channels/discord-events.js +150 -0
  83. package/dist/agent/channels/discord-events.js.map +1 -0
  84. package/dist/agent/channels/discord-front.d.ts +115 -0
  85. package/dist/agent/channels/discord-front.d.ts.map +1 -0
  86. package/dist/agent/channels/discord-front.js +279 -0
  87. package/dist/agent/channels/discord-front.js.map +1 -0
  88. package/dist/agent/channels/discord-gateway.d.ts +783 -0
  89. package/dist/agent/channels/discord-gateway.d.ts.map +1 -0
  90. package/dist/agent/channels/discord-gateway.js +2591 -0
  91. package/dist/agent/channels/discord-gateway.js.map +1 -0
  92. package/dist/agent/channels/discord-identity.d.ts +110 -0
  93. package/dist/agent/channels/discord-identity.d.ts.map +1 -0
  94. package/dist/agent/channels/discord-identity.js +201 -0
  95. package/dist/agent/channels/discord-identity.js.map +1 -0
  96. package/dist/agent/channels/discord-interaction-defer.d.ts +33 -0
  97. package/dist/agent/channels/discord-interaction-defer.d.ts.map +1 -0
  98. package/dist/agent/channels/discord-interaction-defer.js +102 -0
  99. package/dist/agent/channels/discord-interaction-defer.js.map +1 -0
  100. package/dist/agent/channels/discord-message-ops.d.ts +3 -0
  101. package/dist/agent/channels/discord-message-ops.d.ts.map +1 -0
  102. package/dist/agent/channels/discord-message-ops.js +123 -0
  103. package/dist/agent/channels/discord-message-ops.js.map +1 -0
  104. package/dist/agent/channels/discord-origin-codec.d.ts +7 -0
  105. package/dist/agent/channels/discord-origin-codec.d.ts.map +1 -0
  106. package/dist/agent/channels/discord-origin-codec.js +98 -0
  107. package/dist/agent/channels/discord-origin-codec.js.map +1 -0
  108. package/dist/agent/channels/discord-post-arm.d.ts +3 -0
  109. package/dist/agent/channels/discord-post-arm.d.ts.map +1 -0
  110. package/dist/agent/channels/discord-post-arm.js +195 -0
  111. package/dist/agent/channels/discord-post-arm.js.map +1 -0
  112. package/dist/agent/channels/discord-post.d.ts +120 -0
  113. package/dist/agent/channels/discord-post.d.ts.map +1 -0
  114. package/dist/agent/channels/discord-post.js +426 -0
  115. package/dist/agent/channels/discord-post.js.map +1 -0
  116. package/dist/agent/channels/discord-read.d.ts +40 -0
  117. package/dist/agent/channels/discord-read.d.ts.map +1 -0
  118. package/dist/agent/channels/discord-read.js +128 -0
  119. package/dist/agent/channels/discord-read.js.map +1 -0
  120. package/dist/agent/channels/discord-rest.d.ts +33 -0
  121. package/dist/agent/channels/discord-rest.d.ts.map +1 -0
  122. package/dist/agent/channels/discord-rest.js +75 -0
  123. package/dist/agent/channels/discord-rest.js.map +1 -0
  124. package/dist/agent/channels/discord.d.ts +3 -0
  125. package/dist/agent/channels/discord.d.ts.map +1 -0
  126. package/dist/agent/channels/discord.js +230 -0
  127. package/dist/agent/channels/discord.js.map +1 -0
  128. package/dist/agent/commands/discord-command-sync.d.ts +66 -0
  129. package/dist/agent/commands/discord-command-sync.d.ts.map +1 -0
  130. package/dist/agent/commands/discord-command-sync.js +224 -0
  131. package/dist/agent/commands/discord-command-sync.js.map +1 -0
  132. package/dist/agent/commands/discord-commands.d.ts +45 -0
  133. package/dist/agent/commands/discord-commands.d.ts.map +1 -0
  134. package/dist/agent/commands/discord-commands.js +266 -0
  135. package/dist/agent/commands/discord-commands.js.map +1 -0
  136. package/dist/agent/contributions.d.ts +17 -0
  137. package/dist/agent/contributions.d.ts.map +1 -0
  138. package/dist/agent/contributions.js +31 -0
  139. package/dist/agent/contributions.js.map +1 -0
  140. package/dist/agent/docs-generated.d.ts +3 -0
  141. package/dist/agent/docs-generated.d.ts.map +1 -0
  142. package/dist/agent/docs-generated.js +13 -0
  143. package/dist/agent/docs-generated.js.map +1 -0
  144. package/dist/agent/identity/discord-link-consent.d.ts +13 -0
  145. package/dist/agent/identity/discord-link-consent.d.ts.map +1 -0
  146. package/dist/agent/identity/discord-link-consent.js +111 -0
  147. package/dist/agent/identity/discord-link-consent.js.map +1 -0
  148. package/dist/agent/ops.d.ts +2 -0
  149. package/dist/agent/ops.d.ts.map +1 -0
  150. package/dist/agent/ops.js +13 -0
  151. package/dist/agent/ops.js.map +1 -0
  152. package/dist/agent/provider-events.d.ts +3 -0
  153. package/dist/agent/provider-events.d.ts.map +1 -0
  154. package/dist/agent/provider-events.js +78 -0
  155. package/dist/agent/provider-events.js.map +1 -0
  156. package/dist/agent/register.d.ts +5 -0
  157. package/dist/agent/register.d.ts.map +1 -0
  158. package/dist/agent/register.js +125 -0
  159. package/dist/agent/register.js.map +1 -0
  160. package/dist/agent/surface-parity.d.ts +3 -0
  161. package/dist/agent/surface-parity.d.ts.map +1 -0
  162. package/dist/agent/surface-parity.js +90 -0
  163. package/dist/agent/surface-parity.js.map +1 -0
  164. package/package.json +185 -0
  165. package/tsconfig.build.json +22 -0
  166. 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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"}