@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,102 @@
1
+ // THE DEFERRED INTERACTION RESPONSE (OPE-1282) — how a Discord door answers work that outlasts the
2
+ // three-second interaction token.
3
+ //
4
+ // WHAT WAS WRONG. Discord gives an interactions endpoint THREE SECONDS to answer, and it does NOT
5
+ // redeliver an expired token. A door that decides first and answers second — `await the work, then
6
+ // return the reply` — therefore has a window in which the work really lands and the person sees a
7
+ // red "This interaction failed" over it. `access/discord-grant-click.ts` is that shape: an APPROVE
8
+ // spends a 2000 ms borrow-read budget, then a CAS, a grant insert, audit rows and an awaited post
9
+ // into the requesting room. When it overruns, the owner believes their approval failed while a
10
+ // conversation can already spend their credential, and a second tap answers "that request is no
11
+ // longer open" — true, and reading like a second failure. Slack does not have this: it redelivers an
12
+ // interactivity payload it has not seen answered. Discord does not.
13
+ //
14
+ // THE SHAPE THAT CLOSES IT — ack, then decide, then edit:
15
+ // 1. Answer the interaction IMMEDIATELY with {@link deferredEphemeralResponse} (`type 5`,
16
+ // DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, `flags 64`). Discord renders "thinking…", privately,
17
+ // and the token is now good for fifteen minutes instead of three seconds.
18
+ // 2. Do the work off the response path, under the host's `waitUntil`.
19
+ // 3. Replace the placeholder with the real answer through {@link editOriginalInteractionResponse}.
20
+ //
21
+ // 🔴 THE EPHEMERAL FLAG RIDES THE DEFER, NOT THE EDIT, and that is not a style choice. Discord fixes
22
+ // a response's visibility at the FIRST response; the follow-up edit carries no flags and cannot
23
+ // change it. A defer sent without `flags 64` makes the eventual answer PUBLIC — on the grant door
24
+ // that would publish one person's credential decision into the channel. The flag is therefore part
25
+ // of the ack, and a test pins it.
26
+ //
27
+ // 🔴 THE FOLLOW-UP NEEDS NO BOT TOKEN AND NO NEW CONFIGURATION. The interaction webhook route is
28
+ // authenticated by the interaction TOKEN itself, and both halves of the address — `application_id`
29
+ // and `token` — ride the interaction payload Discord already signed. {@link interactionFollowUp}
30
+ // reads them from that verified envelope and returns `undefined` when either is missing, which is
31
+ // what lets a caller fall back to answering synchronously rather than deferring into silence.
32
+ //
33
+ // 🔴 THE INTERACTION TOKEN IS A CREDENTIAL. It authorises anyone holding it to write as the app for
34
+ // fifteen minutes. It never reaches a log line, an error string or an audit row from this module —
35
+ // `editOriginalInteractionResponse` builds the URL and reports only the HTTP status.
36
+ //
37
+ // EVERY WRITE GOES THROUGH `discordFetch` (the OPE-1236 seam), so the follow-up honours a 429's
38
+ // `retry_after` like every other Discord write. `discord-rest-seam-test.ts` §3 reds on a bare
39
+ // `fetch(` in this package, and this module holding its own would be exactly that.
40
+ import { discordFetch } from "./discord-rest.js";
41
+ const API = "https://discord.com/api/v10";
42
+ /** Discord interaction-response wire constants, defined locally for the same reason each door
43
+ * defines its own: this package holds no shared wire-constant module, and a door that cannot read
44
+ * its own numbers is harder to review than one that repeats them. */
45
+ const RESPONSE_DEFERRED_CHANNEL_MESSAGE = 5; // DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE
46
+ const EPHEMERAL = 64; // MessageFlags.EPHEMERAL — only the clicker sees it
47
+ function isObject(v) {
48
+ return typeof v === "object" && v !== null;
49
+ }
50
+ function str(v) {
51
+ return typeof v === "string" && v.length > 0 ? v : undefined;
52
+ }
53
+ /**
54
+ * The follow-up address carried by one verified interaction envelope, or `undefined` when either
55
+ * half is absent.
56
+ *
57
+ * 🔴 `undefined` MUST MEAN "DO NOT DEFER". A door that deferred without an address would ack with
58
+ * "thinking…" and then have no way to say anything else — a permanent placeholder, which is worse
59
+ * than the slow answer this whole module exists to replace. Callers fall back to the synchronous
60
+ * shape instead. Unreachable on a real Discord payload (both fields are always present); written as
61
+ * a real branch because the alternative failure is silent and permanent.
62
+ */
63
+ export function interactionFollowUp(raw) {
64
+ if (!isObject(raw))
65
+ return undefined;
66
+ const applicationId = str(raw["application_id"]);
67
+ const token = str(raw["token"]);
68
+ return applicationId === undefined || token === undefined ? undefined : { applicationId, token };
69
+ }
70
+ /** The immediate ack: "thinking…", visible only to the clicker. See the header for why the flag
71
+ * cannot be moved to the follow-up. */
72
+ export function deferredEphemeralResponse() {
73
+ return { type: RESPONSE_DEFERRED_CHANNEL_MESSAGE, data: { flags: EPHEMERAL } };
74
+ }
75
+ /**
76
+ * Replace the deferred placeholder with the real answer.
77
+ *
78
+ * NEVER THROWS. This runs off the response path, under `waitUntil`, on a decision that has ALREADY
79
+ * been made and written — an unsent follow-up costs the person their explanation, and throwing here
80
+ * would additionally poison whatever else that background promise carries. Returns whether the edit
81
+ * landed, so a caller can log the difference.
82
+ *
83
+ * The URL's `@original` is the deferred message itself; Discord has no other handle on it.
84
+ */
85
+ export async function editOriginalInteractionResponse(followUp, content, deps = {}) {
86
+ try {
87
+ const res = await discordFetch("PATCH /webhooks/{application.id}/{interaction.token}/messages/@original", `${API}/webhooks/${encodeURIComponent(followUp.applicationId)}/${encodeURIComponent(followUp.token)}/messages/@original`, {
88
+ method: "PATCH",
89
+ // NO `authorization` HEADER, deliberately: the interaction token in the path IS the
90
+ // authorisation, and Discord rejects this route when a bot token is sent alongside it.
91
+ headers: { "content-type": "application/json" },
92
+ body: JSON.stringify({ content }),
93
+ }, deps);
94
+ return res.ok;
95
+ }
96
+ catch {
97
+ // A 429 that outlasts the seam's attempts arrives as `RateLimitExhaustedError`; a transport
98
+ // failure arrives as itself. Neither may escape — see the paragraph above.
99
+ return false;
100
+ }
101
+ }
102
+ //# sourceMappingURL=discord-interaction-defer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-interaction-defer.js","sourceRoot":"","sources":["../../../agent/channels/discord-interaction-defer.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,kCAAkC;AAClC,EAAE;AACF,kGAAkG;AAClG,mGAAmG;AACnG,kGAAkG;AAClG,mGAAmG;AACnG,kGAAkG;AAClG,+FAA+F;AAC/F,gGAAgG;AAChG,qGAAqG;AACrG,oEAAoE;AACpE,EAAE;AACF,0DAA0D;AAC1D,4FAA4F;AAC5F,kGAAkG;AAClG,+EAA+E;AAC/E,wEAAwE;AACxE,qGAAqG;AACrG,EAAE;AACF,qGAAqG;AACrG,gGAAgG;AAChG,kGAAkG;AAClG,mGAAmG;AACnG,kCAAkC;AAClC,EAAE;AACF,iGAAiG;AACjG,mGAAmG;AACnG,iGAAiG;AACjG,kGAAkG;AAClG,8FAA8F;AAC9F,EAAE;AACF,oGAAoG;AACpG,mGAAmG;AACnG,qFAAqF;AACrF,EAAE;AACF,gGAAgG;AAChG,8FAA8F;AAC9F,mFAAmF;AAEnF,OAAO,EAAE,YAAY,EAAyB,MAAM,mBAAmB,CAAC;AAExE,MAAM,GAAG,GAAG,6BAA6B,CAAC;AAE1C;;sEAEsE;AACtE,MAAM,iCAAiC,GAAG,CAAC,CAAC,CAAC,uCAAuC;AACpF,MAAM,SAAS,GAAG,EAAE,CAAC,CAAC,oDAAoD;AAS1E,SAAS,QAAQ,CAAC,CAAU;IAC1B,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,CAAC;AAC7C,CAAC;AAED,SAAS,GAAG,CAAC,CAAU;IACrB,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/D,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC9C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IACrC,MAAM,aAAa,GAAG,GAAG,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;IAChC,OAAO,aAAa,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC;AACnG,CAAC;AAED;wCACwC;AACxC,MAAM,UAAU,yBAAyB;IACvC,OAAO,EAAE,IAAI,EAAE,iCAAiC,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,CAAC;AACjF,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,+BAA+B,CACnD,QAA6B,EAC7B,OAAe,EACf,IAAI,GAAqB,EAAE;IAE3B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,YAAY,CAC5B,yEAAyE,EACzE,GAAG,GAAG,aAAa,kBAAkB,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,kBAAkB,CAAC,QAAQ,CAAC,KAAK,CAAC,qBAAqB,EACxH;YACE,MAAM,EAAE,OAAO;YACf,oFAAoF;YACpF,uFAAuF;YACvF,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC;SAClC,EACD,IAAI,CACL,CAAC;QACF,OAAO,GAAG,CAAC,EAAE,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,4FAA4F;QAC5F,2EAA2E;QAC3E,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { SurfaceMessageOps } from "@open-everywhere/core/agent/adapter-api.js";
2
+ export declare const discordMessageOps: SurfaceMessageOps;
3
+ //# sourceMappingURL=discord-message-ops.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-message-ops.d.ts","sourceRoot":"","sources":["../../../agent/channels/discord-message-ops.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,iBAAiB,EAAgB,MAAM,4CAA4C,CAAC;AAYlG,eAAO,MAAM,iBAAiB,EAAE,iBAoG/B,CAAC"}
@@ -0,0 +1,123 @@
1
+ // The Discord SurfaceMessageOps — the bot-token REST transport for the ANCILLARY chat message ops
2
+ // generic core features dispatch through channels/surface-message-ops.ts (OPE-114 stage 4):
3
+ // - postText — a plain create-message (the hosted-artifact URL, the float notice, the approval
4
+ // decision notice). `replyToMessageId` anchors it via message_reference. Rides the
5
+ // reply feature's own write seam (discord-post.ts discordPostMessage — the 2000-char
6
+ // split + reply anchor already live there).
7
+ // - editMessage — PATCH /channels/{id}/messages/{mid} (an approval notice's decided-state rewrite).
8
+ // - postFiles — a hand-rolled multipart/form-data create-message (post_file's uploads). eve's
9
+ // Discord API is JSON-only, so payload_json + one files[i] part per file.
10
+ // - permalink — the message's own URL (request_access's PR body, OPE-303). NO network: Discord's
11
+ // message URL is a pure composition of ids we already hold.
12
+ // Registered by registerDiscordProvider(). The bot token is a SECRET — header only, never logged; on
13
+ // failure we surface the STATUS only.
14
+ import { discordPostMessage } from "./discord-post.js";
15
+ import { discordFetch } from "./discord-rest.js";
16
+ const API = "https://discord.com/api/v10";
17
+ function botToken() {
18
+ const token = process.env.DISCORD_BOT_TOKEN;
19
+ if (!token)
20
+ throw new Error("DISCORD_BOT_TOKEN is not set — the bot token is required to post to Discord.");
21
+ return token;
22
+ }
23
+ export const discordMessageOps = {
24
+ platform: "discord",
25
+ postText(channelId, text, opts) {
26
+ return discordPostMessage(channelId, text, opts?.replyToMessageId ? { replyToMessageId: opts.replyToMessageId } : undefined);
27
+ },
28
+ async editMessage(channelId, messageId, text) {
29
+ const res = await discordFetch("PATCH /channels/{id}/messages/{id}", `${API}/channels/${channelId}/messages/${messageId}`, {
30
+ method: "PATCH",
31
+ headers: { authorization: `Bot ${botToken()}`, "content-type": "application/json" },
32
+ // 🔴 `components: []` STRIPS THE BUTTONS, AND IT IS LOAD-BEARING (OPE-1260). Discord leaves a
33
+ // message's components untouched when a PATCH omits them, so without this line a card rewritten
34
+ // to a closed state kept four live buttons over copy saying the request was already over.
35
+ // Slack's own rewrite behaves this way already: `chat.update` sends a replacement `blocks`
36
+ // array and the buttons go with it.
37
+ //
38
+ // THE CALLER INVENTORY, because the strip is only safe while it holds. This arm is reached from
39
+ // ONE place — core's `channels/surface-message-ops.ts` `editSurfaceText` — whose only production
40
+ // caller is `access/credential-grant-card.ts` `rewriteGrantCard`. That helper now has FOUR
41
+ // callers: the sweep's expiry, the approve-time catalogue refusal, and (OPE-1278) the owner's
42
+ // own approve and decline. ALL FOUR RUN ONLY AFTER THE ASK ROW IS TERMINAL OR DECIDED — each
43
+ // one follows the CAS that closed the ask — so the strip still deletes no live control. An
44
+ // owner on Discord reaches this arm; an owner on Slack reaches `chat.update` instead.
45
+ // 🔴 A CALLER THAT MEANS "EDIT THE BODY, KEEP THE BUTTONS" MUST NOT USE THIS ARM — it has no way
46
+ // to say so, and this line would delete a live control. The core interface says the same.
47
+ body: JSON.stringify({ components: [], content: text }),
48
+ });
49
+ if (!res.ok)
50
+ throw new Error(`Discord edit-message failed (${res.status}) for channel ${channelId}`);
51
+ },
52
+ /**
53
+ * `https://discord.com/channels/<guild|@me>/<channel>/<message>` — the canonical Discord message
54
+ * link, composed rather than fetched (there is no getPermalink API and none is needed).
55
+ *
56
+ * 🔴 THE DM CASE IS WHY `ref.dm` EXISTS AND A HEURISTIC WOULD NOT DO. A Discord DM binds
57
+ * `workspace` to the USER id (core identity/binding.ts), and a user id and a guild id are both
58
+ * snowflakes — indistinguishable by shape. Putting a user id in the guild slot yields a URL that
59
+ * looks right and resolves to nothing, which is worse than no link at all in a PR body an admin
60
+ * reads to decide a grant. So a DM composes the `@me` form, and a channel with no workspace
61
+ * resolves `undefined` rather than guessing either way.
62
+ *
63
+ * ⚠️ NOT YET REACHED BY THE ONE CORE CONSUMER, and the reason is in the BINDING, not here.
64
+ * `request_access` asks for a permalink to `binding.threadTs`, and this bundle's identity mapper
65
+ * (channels/discord-identity.ts) does not set that field at all — Discord threads are their own
66
+ * channel ids, so the binding carries a `channelId` and no per-message anchor. So a Discord
67
+ * access request still freezes no permalink, exactly as before, and its PR body falls back to
68
+ * naming the channel + scope. Giving the mapper a `threadTs` is not a free fix: that field also
69
+ * drives approval-card threading and the float notice, so it is its own change. Written down
70
+ * rather than left to be rediscovered — this method is correct and currently idle.
71
+ */
72
+ async permalink(ref) {
73
+ const guild = ref.dm ? "@me" : ref.workspace;
74
+ return guild ? `https://discord.com/channels/${guild}/${ref.channelId}/${ref.messageId}` : undefined;
75
+ },
76
+ /**
77
+ * `{PUT|DELETE} /channels/{id}/messages/{mid}/reactions/{emoji}/@me` — the bot's own reaction,
78
+ * URL-encoded. This is the transport behind core's ✋ stop affordance (OPE-418).
79
+ *
80
+ * NEVER THROWS, deliberately, unlike the three ops above: a reaction is decoration beside work
81
+ * that is already running, so a missing permission must cost the turn its stop button, not the
82
+ * turn. A DELETE of a reaction that was never added 404s — that is the idempotent no-op, not an
83
+ * error.
84
+ */
85
+ async setReaction(channelId, messageId, emoji, on) {
86
+ const encoded = encodeURIComponent(emoji);
87
+ try {
88
+ const res = await discordFetch(on ? "PUT /channels/{id}/messages/{id}/reactions/{emoji}/@me" : "DELETE /channels/{id}/messages/{id}/reactions/{emoji}/@me", `${API}/channels/${channelId}/messages/${messageId}/reactions/${encoded}/@me`, { method: on ? "PUT" : "DELETE", headers: { authorization: `Bot ${botToken()}` } });
89
+ if (!res.ok && !(!on && res.status === 404)) {
90
+ console.warn(`[open-everywhere:discord] reaction ${on ? "add" : "remove"} failed (${res.status}) for message ${messageId}`);
91
+ }
92
+ }
93
+ catch (err) {
94
+ console.warn(`[open-everywhere:discord] reaction ${on ? "add" : "remove"} error: ${err?.message ?? String(err)}`);
95
+ }
96
+ },
97
+ async postFiles(channelId, files, comment) {
98
+ const form = new FormData();
99
+ const attachments = files.map((file, i) => ({ id: i, filename: file.filename, ...(file.caption ? { description: file.caption } : {}) }));
100
+ form.append("payload_json", JSON.stringify({ ...(comment ? { content: comment } : {}), attachments }));
101
+ files.forEach((file, i) => {
102
+ // A fresh ArrayBuffer copy so the Blob owns contiguous bytes regardless of the source view.
103
+ const ab = file.bytes.buffer.slice(file.bytes.byteOffset, file.bytes.byteOffset + file.bytes.byteLength);
104
+ form.append(`files[${i}]`, new Blob([ab], { type: file.contentType }), file.filename);
105
+ });
106
+ // No explicit content-type header — fetch derives the multipart boundary from the FormData.
107
+ const res = await discordFetch("POST /channels/{id}/messages", `${API}/channels/${channelId}/messages`, {
108
+ method: "POST",
109
+ headers: { authorization: `Bot ${botToken()}` },
110
+ body: form,
111
+ });
112
+ if (!res.ok)
113
+ throw new Error(`Discord create-message (files) failed (HTTP ${res.status}) for channel ${channelId}`);
114
+ // Discord's create-message response carries the new message's own `id` — the same field
115
+ // discordPostOne reads for a plain text post (discord-post.ts). Hand it back so post_file's
116
+ // audit row can stamp `detail.id`, matching the reply path's shape (OPE-932). DELIBERATELY
117
+ // UNMARKED when absent, like discordPostOne: a 2xx means the message IS in the channel — this
118
+ // host just cannot name it, never an error worth failing an otherwise-successful upload over.
119
+ const body = (await res.json().catch(() => undefined));
120
+ return typeof body?.id === "string" ? body.id : undefined;
121
+ },
122
+ };
123
+ //# sourceMappingURL=discord-message-ops.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-message-ops.js","sourceRoot":"","sources":["../../../agent/channels/discord-message-ops.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { UnifiedWakeOrigin, WakeOriginCodec } from "@open-everywhere/core/agent/adapter-api.js";
2
+ type DiscordOrigin = Extract<UnifiedWakeOrigin, {
3
+ platform: "discord";
4
+ }>;
5
+ export declare const discordWakeOriginCodec: WakeOriginCodec<DiscordOrigin>;
6
+ export {};
7
+ //# sourceMappingURL=discord-origin-codec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-origin-codec.d.ts","sourceRoot":"","sources":["../../../agent/channels/discord-origin-codec.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,4CAA4C,CAAC;AAErG,KAAK,aAAa,GAAG,OAAO,CAAC,iBAAiB,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,CAAC,CAAC;AASzE,eAAO,MAAM,sBAAsB,EAAE,eAAe,CAAC,aAAa,CA8EjE,CAAC"}
@@ -0,0 +1,98 @@
1
+ // The Discord UnifiedWakeOrigin codec — MOVED VERBATIM out of core's channels/wake-origin-codecs.ts
2
+ // (OPE-114 stage 4). STORED-DATA COMPATIBILITY IS THE CONTRACT: a Discord origin is JSON-serialised
3
+ // into the durable operational store and fail-closed re-validated on read, so every behaviour here
4
+ // is frozen by core's scripts/wake-origin-fixtures-test.ts (run from the app root with this bundle's
5
+ // boot wiring). Validator tolerances (typeof-only optional checks, extra unknown fields) are
6
+ // LOAD-BEARING for rows already on disk — never tighten them here.
7
+ //
8
+ // The `UnifiedWakeOrigin` TYPE union (including BOTH discord arms) stays in core — it is the wire
9
+ // schema for stored rows; only the codec BEHAVIOUR lives in the bundle, registered by
10
+ // registerDiscordProvider() (../register.ts).
11
+ //
12
+ // OPE-315 added a SECOND discord arm — a `dm: true`-discriminated DM/group-DM variant — so the
13
+ // validator became an XOR. Every existing GUILD row keeps validating byte for byte: the guild branch
14
+ // is the old predicate verbatim, and the additions are all on the new positive discriminant.
15
+ /** COLON-FREE by construction (a task windowId is `[^:]+` — core channels/window-key.ts): any colon
16
+ * in a raw surface id is folded to `_`, deterministically, so spawn + ingress agree on identity. */
17
+ const noColon = (s) => s.replace(/:/g, "_");
18
+ /** A DM-variant origin — narrowed on the positive discriminant, never on the ABSENCE of guildId. */
19
+ const isDmOrigin = (o) => "dm" in o && o.dm === true;
20
+ export const discordWakeOriginCodec = {
21
+ validate: (o) => {
22
+ // TWO valid shapes, enforced as an XOR (never both, never neither) — the Linear arm's model.
23
+ // • GUILD: a non-empty guildId + NO `dm`. Unchanged from the original single shape, byte for
24
+ // byte: guildId is REQUIRED because the Discord privacy floor
25
+ // (discord-gateway.ts discordSingleWindowPrivacyOk) can never have admitted a guildless
26
+ // surface into a shared window, and this validator enforces the same invariant on the reply
27
+ // path. `messageId` and `autoThreadName` stay typeof-only optionals.
28
+ // • DM (OPE-315): `dm === true` + NO guildId + NO autoThreadName. A DM/group-DM reply needs
29
+ // only the channel id (POST /channels/{id}/messages under the bot token).
30
+ //
31
+ // 🔴 THE XOR IS DELIBERATELY BIDIRECTIONAL. "dm:true AND a guildId" is INVALID — not "the guild
32
+ // wins", not "the dm wins". That is the IMPERSONATION direction: a DM-shaped row that acquired
33
+ // guild authority would reach the guild-keyed threadRef, markThreadEngaged, and `discord/<guild>`
34
+ // vouch keys. And the guild arm still refuses guildless BY OMISSION (D1): nothing here relaxes
35
+ // it, so `divertDiscordWakeToUnifiedWindow`'s `guildId: msg.guildId ?? ""` fall-through stays
36
+ // invalid exactly as before — pinned by the two unmodified guildless-rejection fixture rows.
37
+ //
38
+ // autoThreadName is forbidden on the DM shape (D4) because the reply leg's auto-thread branch is
39
+ // guild-keyed (discord-post-arm.ts writes `discord/${origin.guildId}` engagement rows); a DM
40
+ // carrying it would mint `discord/undefined/...`. messageId is allowed — cosmetic
41
+ // message_reference anchoring with no guild-shaped consequence. That is the whole line: forbid
42
+ // exactly the fields with guild-keyed effects.
43
+ //
44
+ // OPE-1719 adds `thread` to the GUILD shape only, and it is a LITERAL-`true` optional rather
45
+ // than a typeof-boolean optional: the field's whole job is to be a POSITIVE thread signal, so
46
+ // `thread: false` must not validate and then read as "a thread that says it is not one". It is
47
+ // forbidden on the DM shape because a Discord DM has no threads at all — the same "forbid
48
+ // exactly the fields whose effects the shape cannot carry" line `autoThreadName` sits on.
49
+ if (typeof o.channelId !== "string" || o.channelId.length === 0)
50
+ return false;
51
+ if (o.messageId !== undefined && typeof o.messageId !== "string")
52
+ return false;
53
+ const isDm = o.dm === true && o.guildId === undefined && o.autoThreadName === undefined && o.thread === undefined;
54
+ const isGuild = o.dm === undefined &&
55
+ typeof o.guildId === "string" &&
56
+ o.guildId.length > 0 &&
57
+ (o.autoThreadName === undefined || typeof o.autoThreadName === "string") &&
58
+ (o.thread === undefined || o.thread === true);
59
+ return isDm || isGuild;
60
+ },
61
+ // The DM namespace is `discord/dm/<channelId>` — a literal `dm` segment where a guild id sits on
62
+ // the guild shape. Collision-free because a Discord guild id is a decimal snowflake and can never
63
+ // be the string "dm". Relied on rather than enforced at the type level, so it is asserted in the
64
+ // fixtures oracle; if it were ever violated the blast radius is ROUTING (a shared thread-ownership
65
+ // row + taskWindowId prefix), not authority — a reply still goes to the stored origin.
66
+ threadRef: (origin) => isDmOrigin(origin)
67
+ ? { channelKey: noColon(`discord/dm/${origin.channelId}`), threadKey: noColon(origin.channelId) }
68
+ : { channelKey: noColon(`discord/${origin.guildId}/${origin.channelId}`), threadKey: noColon(origin.channelId) },
69
+ // THE POSITIVE SIGNAL, and nothing else (OPE-1719). A Discord thread is its own channelId, so the
70
+ // id proves nothing; the gateway stamps `thread: true` at mint from the relay payload's channel
71
+ // type, and this reads exactly that stamp. Every other shape — a plain guild channel, a DM, and
72
+ // every stored row minted before the field existed — reads FALSE and routes to the standing seat,
73
+ // which is the fail-closed direction and byte-identical to the behaviour this replaced.
74
+ isThreadScoped: (origin) => !isDmOrigin(origin) && origin.thread === true,
75
+ containerId: (origin) => origin.channelId,
76
+ // THE PLACEMENT AXIS (OPE-1826). Discord is the SECOND surface whose post placement sits OUTSIDE
77
+ // its conversation identity, and until this member existed nothing said so: `threadRef` keys on the
78
+ // channel id alone (a Discord thread IS its own channel id), while the post arm hangs the answer on
79
+ // `messageId` as a `message_reference`. So two wakes in one channel are ONE conversation naming two
80
+ // different messages to reply under — Slack's `replyAnchorTs` shape exactly.
81
+ //
82
+ // 🔴 `messageId` ONLY, NEVER `autoThreadName`. The post arm reads both and only one is a PLACEMENT:
83
+ // `autoThreadName` decides whether the answer opens a NEW thread off the message, which MOVES the
84
+ // post out of the conversation it was asked in. The carry's own guarantee is that it can place a
85
+ // post and never move one, so the name is deliberately outside this axis and outside the carry
86
+ // below. A DM origin carries no message anchor at all — there is nothing to hang under in a 1:1 —
87
+ // and answers `undefined` through the same emptiness-is-absence read every other field takes.
88
+ placementRef: (origin) => (typeof origin.messageId === "string" && origin.messageId.length > 0 ? origin.messageId : undefined),
89
+ // THE CARRY (OPE-1826), narrowing-only and byte-for-byte the contract Slack's arm holds: fill a
90
+ // placement the target LACKS, never overwrite one, change no other field. A target that already
91
+ // names a message keeps it, because the model named THAT wake and answering it where it was asked
92
+ // is the honest reading. Core re-validates the result and re-tests the conversation, so this arm
93
+ // can fill a gap and cannot move a post even if this line were wrong.
94
+ carryPlacement: (target, source) => !(target.messageId && target.messageId.length > 0) && typeof source.messageId === "string" && source.messageId.length > 0
95
+ ? { ...target, messageId: source.messageId }
96
+ : target,
97
+ };
98
+ //# sourceMappingURL=discord-origin-codec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-origin-codec.js","sourceRoot":"","sources":["../../../agent/channels/discord-origin-codec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { type PostArm } from "@open-everywhere/core/agent/adapter-api.js";
2
+ export declare const discordPostArm: PostArm;
3
+ //# sourceMappingURL=discord-post-arm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-post-arm.d.ts","sourceRoot":"","sources":["../../../agent/channels/discord-post-arm.ts"],"names":[],"mappings":"AAkBA,OAAO,EAKL,KAAK,OAAO,EAUb,MAAM,4CAA4C,CAAC;AA+JpD,eAAO,MAAM,cAAc,EAAE,OAqC5B,CAAC"}
@@ -0,0 +1,195 @@
1
+ // discord-post-arm — Discord's POST arm for the post-arms registry (OPE-114 stage 1c). Absorbs,
2
+ // VERBATIM, Discord's arms out of the three reply/delivery spines: reply-post.ts (resolveReplyTarget's
3
+ // discord case + dispatchReply's POST-messages branch + the discord splitForDiscord chunking used by
4
+ // postTerminalProse), unified-reply.ts (liveDiscordPost, incl. the auto-thread restore + engage), and
5
+ // unified-redrive.ts (the discord describe/container cases). The write seam is discord-post.ts (the
6
+ // same one the reply tools use — deliberately NOT the gateway module).
7
+ import { DISCORD_MESSAGE_MAX, discordAddReaction, discordEditMessage, discordExistingThreadForMessage, discordOpenDmChannel, discordPostMessage, discordPostMessageWithButtons, discordThreadForMessage, splitForDiscord, } from "./discord-post.js";
8
+ import { bindFrom, markThreadEngaged, deploymentOrg, recordOwnPost, } from "@open-everywhere/core/agent/adapter-api.js";
9
+ /** UNIFIED: POST /channels/{id}/messages under the bot token. The target channelId is the frozen wake
10
+ * origin's (host-side, never model-authored); a thread IS its own channel id so a threaded trigger
11
+ * replies into its thread. The FIRST chunk anchors to the triggering message via message_reference
12
+ * (fail-soft when the trigger was deleted); Discord caps a message at 2000 chars, so a long reply
13
+ * splits and posts in order. Errors re-wrapped token-free (defensive parity with the other arms —
14
+ * discordPostMessage's own errors are already clean, but never risk echoing a transport body).
15
+ * Auto-thread restore (origin.autoThreadName ⇔ an addressed mention in a base channel): open the
16
+ * thread off the mention and answer there — the native path's UX the divert skips.
17
+ * discordThreadForMessage is idempotent (finds an existing thread) and fail-open (null ⇒ today's
18
+ * in-channel reply, anchored to the mention via message_reference). */
19
+ async function liveDiscordPost(origin, text) {
20
+ let targetChannelId = origin.channelId;
21
+ let anchorToMessage = origin.messageId;
22
+ // The auto-thread branch is GUILD-ONLY, and now says so structurally. `!("dm" in origin)` narrows
23
+ // to the guild variant so `origin.guildId` type-checks below (OPE-315) — no behavioural change:
24
+ // the DM variant cannot carry `autoThreadName` in the first place (the type forbids it and the
25
+ // validator refuses it, D4), precisely because the engagement row this branch writes is keyed on
26
+ // the guild. The narrowing makes that unrepresentable rather than merely unreached.
27
+ if (!("dm" in origin) && origin.autoThreadName && origin.messageId) {
28
+ const threadId = await discordThreadForMessage(origin.channelId, origin.messageId, origin.autoThreadName);
29
+ if (threadId) {
30
+ targetChannelId = threadId;
31
+ anchorToMessage = undefined; // the thread already hangs off the mention — no reference needed
32
+ // ENGAGE the thread we just opened — the native path engages at dispatch, but this thread
33
+ // only exists NOW, so the divert path must enroll it here or non-mention follow-ups inside
34
+ // it are dropped at the wake gate (live-found 2026-07-19: J-DISC's continuity leg — the
35
+ // reply landed, then "expand that..." produced zero audit rows). The partition mirrors what
36
+ // the gate resolves for a message ARRIVING in the thread (workspace=guild, surface=thread id,
37
+ // key=thread channel id — engagementKey). markThreadEngaged is best-effort/never throws, and
38
+ // a later "stop" disengages normally: nothing re-engages until OE next replies here.
39
+ const workspaceKey = `discord/${origin.guildId}`;
40
+ const surfaceKey = `${workspaceKey}/${threadId}`;
41
+ await markThreadEngaged({ org: deploymentOrg(), platform: "discord", workspace: origin.guildId, surface: threadId, workspaceKey, surfaceKey, key: surfaceKey }, threadId, new Date().toISOString(), { participating: true });
42
+ }
43
+ }
44
+ let first;
45
+ try {
46
+ for (const chunk of splitForDiscord(text)) {
47
+ if (!chunk.trim())
48
+ continue; // never post an invisible empty message
49
+ const id = await discordPostMessage(targetChannelId, chunk, first === undefined && anchorToMessage ? { replyToMessageId: anchorToMessage } : undefined);
50
+ first ??= id;
51
+ }
52
+ }
53
+ catch {
54
+ throw new Error(`unified reply couldn't post to Discord — create-message failed on channel ${targetChannelId}`);
55
+ }
56
+ if (first === undefined) {
57
+ throw new Error(`unified reply couldn't post to Discord — nothing to post (empty reply) for channel ${origin.channelId}`);
58
+ }
59
+ // 🔴 OPE-793 — THE ONE PLACE AN ARM POSTS SOMEWHERE ITS SPINE CANNOT NAME. When the auto-thread
60
+ // branch above fires, this reply lands in a thread that did not exist when the wake origin was
61
+ // frozen, so the spine's own ledger write records `origin.channelId` — the BASE channel — and an
62
+ // `edit_reply` addressed at the thread would be refused as not-ours. Recording the real channel
63
+ // here closes that, and only that: the id is one this call just posted, at the locus it posted to.
64
+ // Best-effort by construction (recordOwnPost never throws); a lost row costs an edit, never a post.
65
+ if (targetChannelId !== origin.channelId) {
66
+ await recordOwnPost(deploymentOrg(), { platform: "discord", channelId: targetChannelId }, first);
67
+ }
68
+ return first;
69
+ }
70
+ /**
71
+ * UNIFIED, WITH A COMPONENT PLANE (OPE-1720): the same single-window post as {@link liveDiscordPost},
72
+ * carrying a row of tappable actions under it. Discord is the SECOND surface to implement this
73
+ * member, and it may only do so because the tap lands: `discord-front.ts` classifies the `oe_input:`
74
+ * custom_id and `discord-approval-card.ts` settles it.
75
+ *
76
+ * 🔴 NO AUTO-THREAD BRANCH, DELIBERATELY. `liveDiscordPost` may open a thread off the mention and
77
+ * answer there. A governed approval card must not move: the people who can answer it are watching the
78
+ * conversation it was asked in, and burying the buttons in a freshly-created thread is how a card
79
+ * goes unanswered. The anchor still rides — the card hangs under the triggering message through the
80
+ * same `message_reference` a plain reply uses — so it is placed and never moved.
81
+ *
82
+ * 🔴 IT FALLS BACK TO TEXT RATHER THAN SPLITTING. Discord caps a message at 2000 characters and a
83
+ * button row cannot be split across messages: a row attached to the wrong part would sit under a
84
+ * fragment. So an over-cap card posts through the ordinary splitting path and loses the SHORTCUT,
85
+ * never the meaning — `DirectMessageAction`'s own rule, which the question bridge's text already
86
+ * satisfies (it prints the prompt, the options, and the line saying a reply may name one).
87
+ */
88
+ async function liveDiscordPostWithActions(origin, text, actions) {
89
+ const buttons = actions.map((a) => ({ customId: a.id, label: a.label, intent: a.intent }));
90
+ if (buttons.length === 0 || text.length > DISCORD_MESSAGE_MAX)
91
+ return liveDiscordPost(origin, text);
92
+ try {
93
+ return await discordPostMessageWithButtons(origin.channelId, text, buttons, origin.messageId ? { replyToMessageId: origin.messageId } : undefined);
94
+ }
95
+ catch {
96
+ throw new Error(`unified reply couldn't post to Discord — create-message with buttons failed on channel ${origin.channelId}`);
97
+ }
98
+ }
99
+ /** EDIT (OPE-793): `PATCH /channels/{id}/messages/{mid}` through the same write seam the post path
100
+ * uses. Ownership was already decided upstream against the own-post ledger; this is transport only. */
101
+ async function editDiscordMessage(locus, messageId, text) {
102
+ await discordEditMessage(locus.channelId, messageId, text);
103
+ }
104
+ /** EDIT ADDRESS (OPE-798): the other half of the auto-thread branch in {@link liveDiscordPost}.
105
+ *
106
+ * When that branch fires the reply lands in a thread — and a Discord thread IS its own channel —
107
+ * while the frozen wake origin still names the BASE channel. The arm already records an own-post
108
+ * ledger row on the thread so the fence passes; without this the PATCH still went to the base
109
+ * channel and Discord answered 404 (measured live, deploy #89). Here the edit path resolves the
110
+ * SAME thread the post path used, so the ledger read and the PATCH address one conversation.
111
+ *
112
+ * 🔴 READ-ONLY: `discordExistingThreadForMessage`, never `discordThreadForMessage`. The post path
113
+ * may CREATE the thread; the edit path may not. A create at edit time would open a new empty thread
114
+ * — a visible message in the room, and still a 404 for a message that is not in it.
115
+ *
116
+ * Fail-soft to the base locus on every unknown: a DM origin, a wake with no auto-thread name, a
117
+ * message with no thread, an API failure. That degrades to today's behaviour, which is correct for
118
+ * a reply that genuinely went to the base channel. */
119
+ async function retargetDiscordEditLocus(origin, locus) {
120
+ const o = origin;
121
+ if ("dm" in o || !o.autoThreadName || !o.messageId)
122
+ return locus;
123
+ const threadId = await discordExistingThreadForMessage(o.channelId, o.messageId);
124
+ return threadId ? { ...locus, channelId: threadId } : locus;
125
+ }
126
+ /**
127
+ * DM (OPE-623): open the 1:1 conversation with a Discord user and post there. The SECOND provider to
128
+ * implement this member — Slack was the only one, and several core behaviours read the registry
129
+ * rather than a platform list precisely so that they tighten the day a second arm appears:
130
+ *
131
+ * • `link_identity` can now offer the DIRECTORY transport on Discord (it needs a directory arm AND
132
+ * a DM arm; channels/discord-directory.ts is the other half).
133
+ * • `claimVenueRefusal` (core identity/link-flow.ts) now REFUSES a `link <code>` claim typed in a
134
+ * Discord channel and points the person at the DM. A code is a bearer secret for its whole TTL,
135
+ * so it must not be posted where a room can read it. That is a deliberate tightening, not a
136
+ * regression: it only arms where a private door exists.
137
+ * • link-set change notices (`notifyLinkSetMembers`) and the governance DM-delivery venue
138
+ * (core access/pane-venue.ts) reach Discord accounts instead of skipping them.
139
+ *
140
+ * The caller names WHO, never WHERE — opening the conversation is this arm's business. `text` goes
141
+ * through the same 2000-char splitter every other Discord post uses, so a long notice arrives whole
142
+ * rather than 400-ing; the FIRST part's id is the returned anchor.
143
+ */
144
+ async function deliverDiscordDirectMessage(input) {
145
+ const surfaceId = await discordOpenDmChannel(input.principalId);
146
+ // BUTTONS (OPE-622): Discord has a component plane, so a caller's `actions` render as a row under
147
+ // the text and a tap comes back to the interactions endpoint (channels/discord-front.ts). The
148
+ // button path cannot split an over-cap message, so a long text falls back to the plain splitting
149
+ // post rather than failing — the caller's copy always stands on its own without a button.
150
+ const buttons = (input.actions ?? []).map((a) => ({ customId: a.id, label: a.label, intent: a.intent }));
151
+ if (buttons.length > 0 && input.text.length <= DISCORD_MESSAGE_MAX) {
152
+ return { surfaceId, messageId: await discordPostMessageWithButtons(surfaceId, input.text, buttons) };
153
+ }
154
+ const messageId = await discordPostMessage(surfaceId, input.text);
155
+ return { surfaceId, messageId };
156
+ }
157
+ export const discordPostArm = {
158
+ platform: "discord",
159
+ resolveTarget: (bag) => {
160
+ // On Discord the surface channel already IS the thread — no separate thread ts.
161
+ const channelId = bag.str("channelId");
162
+ return channelId ? { platform: "discord", channelId } : null;
163
+ },
164
+ dispatchReply: (target, text) => discordPostMessage(target.channelId, text),
165
+ dispatchUnified: (origin, text, deps) => (deps.postDiscord ?? liveDiscordPost)(origin, text),
166
+ // OPE-1720: Discord declares the unified component plane because the tap route exists on it —
167
+ // `discord-front.ts` classifies the `oe_input:` custom_id and `discord-approval-card.ts` settles
168
+ // it. The injected `postDiscord` override is honoured VERBATIM and drops the buttons: a test that
169
+ // scripted a text-only post must not start receiving a card it never asked for.
170
+ dispatchUnifiedActions: (origin, text, actions, deps) => deps.postDiscord ? deps.postDiscord(origin, text) : liveDiscordPostWithActions(origin, text, actions),
171
+ describeRedriveTarget: (origin) => `discord channel ${origin.channelId}`,
172
+ redriveContainerId: (origin) => origin.channelId,
173
+ editMessage: editDiscordMessage,
174
+ retargetEditLocus: retargetDiscordEditLocus,
175
+ deliverDirectMessage: deliverDiscordDirectMessage,
176
+ // REACT (OPE-987): the message-level (channelId, messageId) pair `setReaction` already takes for
177
+ // the host-placed ✋ glyph — closer to Slack's per-message granularity than to GitHub/Linear's
178
+ // conversation-anchor one (post-arms.ts `ReactTarget`'s header explains why the union carries both
179
+ // shapes). `channelId` comes off the binding's post-back target, same as `resolveTarget` above;
180
+ // `messageId` is NOT on that target (Discord threads reuse `channelId` for their own identity, so
181
+ // the triggering message rides its own `anchorMessageId` field — identity/binding.ts, OPE-671) —
182
+ // read via `bindFrom(bag.ctx)` rather than widening the bag's target shape for one extra field.
183
+ resolveReactTarget: (bag) => {
184
+ const channelId = bag.str("channelId");
185
+ if (!channelId)
186
+ return null;
187
+ const messageId = bindFrom(bag.ctx).anchorMessageId;
188
+ return messageId ? { platform: "discord", channelId, messageId } : null;
189
+ },
190
+ react: (target, emoji) => {
191
+ const t = target;
192
+ return discordAddReaction(t.channelId, t.messageId, emoji);
193
+ },
194
+ };
195
+ //# sourceMappingURL=discord-post-arm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-post-arm.js","sourceRoot":"","sources":["../../../agent/channels/discord-post-arm.ts"],"names":[],"mappings":"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"}