@open-everywhere/provider-slack 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +47 -0
  4. package/agent/access/slack-attachment-fetch.ts +168 -0
  5. package/agent/app-manifest.ts +149 -0
  6. package/agent/channels/slack-approval-card.ts +275 -0
  7. package/agent/channels/slack-attachments.ts +195 -0
  8. package/agent/channels/slack-commands.ts +227 -0
  9. package/agent/channels/slack-context.ts +185 -0
  10. package/agent/channels/slack-events.ts +5406 -0
  11. package/agent/channels/slack-front.ts +2349 -0
  12. package/agent/channels/slack-identity.ts +520 -0
  13. package/agent/channels/slack-input-response.ts +162 -0
  14. package/agent/channels/slack-mrkdwn.ts +61 -0
  15. package/agent/channels/slack-origin-codec.ts +62 -0
  16. package/agent/channels/slack-post-arm.ts +314 -0
  17. package/agent/channels/slack-read.ts +248 -0
  18. package/agent/channels/slack-route-hold.ts +107 -0
  19. package/agent/channels/slack-shortcut.ts +336 -0
  20. package/agent/channels/slack-signature.ts +53 -0
  21. package/agent/channels/slack-spend-gate.ts +62 -0
  22. package/agent/channels/slack-tenant-fence.ts +99 -0
  23. package/agent/channels/slack-wake-backscroll.ts +604 -0
  24. package/agent/channels/slack-web-api.ts +178 -0
  25. package/agent/channels/slack-webhook-verifier.ts +116 -0
  26. package/agent/channels/slack.ts +1266 -0
  27. package/agent/contributions.ts +32 -0
  28. package/agent/docs-generated.ts +15 -0
  29. package/agent/ops.ts +50 -0
  30. package/agent/provider-events.ts +91 -0
  31. package/agent/register.ts +144 -0
  32. package/agent/surface-parity.ts +99 -0
  33. package/dist/agent/access/slack-attachment-fetch.d.ts +15 -0
  34. package/dist/agent/access/slack-attachment-fetch.d.ts.map +1 -0
  35. package/dist/agent/access/slack-attachment-fetch.js +151 -0
  36. package/dist/agent/access/slack-attachment-fetch.js.map +1 -0
  37. package/dist/agent/app-manifest.d.ts +15 -0
  38. package/dist/agent/app-manifest.d.ts.map +1 -0
  39. package/dist/agent/app-manifest.js +141 -0
  40. package/dist/agent/app-manifest.js.map +1 -0
  41. package/dist/agent/channels/slack-approval-card.d.ts +71 -0
  42. package/dist/agent/channels/slack-approval-card.d.ts.map +1 -0
  43. package/dist/agent/channels/slack-approval-card.js +232 -0
  44. package/dist/agent/channels/slack-approval-card.js.map +1 -0
  45. package/dist/agent/channels/slack-attachments.d.ts +66 -0
  46. package/dist/agent/channels/slack-attachments.d.ts.map +1 -0
  47. package/dist/agent/channels/slack-attachments.js +174 -0
  48. package/dist/agent/channels/slack-attachments.js.map +1 -0
  49. package/dist/agent/channels/slack-commands.d.ts +27 -0
  50. package/dist/agent/channels/slack-commands.d.ts.map +1 -0
  51. package/dist/agent/channels/slack-commands.js +208 -0
  52. package/dist/agent/channels/slack-commands.js.map +1 -0
  53. package/dist/agent/channels/slack-context.d.ts +26 -0
  54. package/dist/agent/channels/slack-context.d.ts.map +1 -0
  55. package/dist/agent/channels/slack-context.js +178 -0
  56. package/dist/agent/channels/slack-context.js.map +1 -0
  57. package/dist/agent/channels/slack-events.d.ts +1654 -0
  58. package/dist/agent/channels/slack-events.d.ts.map +1 -0
  59. package/dist/agent/channels/slack-events.js +4240 -0
  60. package/dist/agent/channels/slack-events.js.map +1 -0
  61. package/dist/agent/channels/slack-front.d.ts +387 -0
  62. package/dist/agent/channels/slack-front.d.ts.map +1 -0
  63. package/dist/agent/channels/slack-front.js +1958 -0
  64. package/dist/agent/channels/slack-front.js.map +1 -0
  65. package/dist/agent/channels/slack-identity.d.ts +245 -0
  66. package/dist/agent/channels/slack-identity.d.ts.map +1 -0
  67. package/dist/agent/channels/slack-identity.js +405 -0
  68. package/dist/agent/channels/slack-identity.js.map +1 -0
  69. package/dist/agent/channels/slack-input-response.d.ts +26 -0
  70. package/dist/agent/channels/slack-input-response.d.ts.map +1 -0
  71. package/dist/agent/channels/slack-input-response.js +127 -0
  72. package/dist/agent/channels/slack-input-response.js.map +1 -0
  73. package/dist/agent/channels/slack-mrkdwn.d.ts +3 -0
  74. package/dist/agent/channels/slack-mrkdwn.d.ts.map +1 -0
  75. package/dist/agent/channels/slack-mrkdwn.js +53 -0
  76. package/dist/agent/channels/slack-mrkdwn.js.map +1 -0
  77. package/dist/agent/channels/slack-origin-codec.d.ts +7 -0
  78. package/dist/agent/channels/slack-origin-codec.d.ts.map +1 -0
  79. package/dist/agent/channels/slack-origin-codec.js +55 -0
  80. package/dist/agent/channels/slack-origin-codec.js.map +1 -0
  81. package/dist/agent/channels/slack-post-arm.d.ts +54 -0
  82. package/dist/agent/channels/slack-post-arm.d.ts.map +1 -0
  83. package/dist/agent/channels/slack-post-arm.js +268 -0
  84. package/dist/agent/channels/slack-post-arm.js.map +1 -0
  85. package/dist/agent/channels/slack-read.d.ts +84 -0
  86. package/dist/agent/channels/slack-read.d.ts.map +1 -0
  87. package/dist/agent/channels/slack-read.js +205 -0
  88. package/dist/agent/channels/slack-read.js.map +1 -0
  89. package/dist/agent/channels/slack-route-hold.d.ts +34 -0
  90. package/dist/agent/channels/slack-route-hold.d.ts.map +1 -0
  91. package/dist/agent/channels/slack-route-hold.js +97 -0
  92. package/dist/agent/channels/slack-route-hold.js.map +1 -0
  93. package/dist/agent/channels/slack-shortcut.d.ts +162 -0
  94. package/dist/agent/channels/slack-shortcut.d.ts.map +1 -0
  95. package/dist/agent/channels/slack-shortcut.js +262 -0
  96. package/dist/agent/channels/slack-shortcut.js.map +1 -0
  97. package/dist/agent/channels/slack-signature.d.ts +19 -0
  98. package/dist/agent/channels/slack-signature.d.ts.map +1 -0
  99. package/dist/agent/channels/slack-signature.js +50 -0
  100. package/dist/agent/channels/slack-signature.js.map +1 -0
  101. package/dist/agent/channels/slack-spend-gate.d.ts +22 -0
  102. package/dist/agent/channels/slack-spend-gate.d.ts.map +1 -0
  103. package/dist/agent/channels/slack-spend-gate.js +48 -0
  104. package/dist/agent/channels/slack-spend-gate.js.map +1 -0
  105. package/dist/agent/channels/slack-tenant-fence.d.ts +61 -0
  106. package/dist/agent/channels/slack-tenant-fence.d.ts.map +1 -0
  107. package/dist/agent/channels/slack-tenant-fence.js +88 -0
  108. package/dist/agent/channels/slack-tenant-fence.js.map +1 -0
  109. package/dist/agent/channels/slack-wake-backscroll.d.ts +188 -0
  110. package/dist/agent/channels/slack-wake-backscroll.d.ts.map +1 -0
  111. package/dist/agent/channels/slack-wake-backscroll.js +473 -0
  112. package/dist/agent/channels/slack-wake-backscroll.js.map +1 -0
  113. package/dist/agent/channels/slack-web-api.d.ts +46 -0
  114. package/dist/agent/channels/slack-web-api.d.ts.map +1 -0
  115. package/dist/agent/channels/slack-web-api.js +147 -0
  116. package/dist/agent/channels/slack-web-api.js.map +1 -0
  117. package/dist/agent/channels/slack-webhook-verifier.d.ts +5 -0
  118. package/dist/agent/channels/slack-webhook-verifier.d.ts.map +1 -0
  119. package/dist/agent/channels/slack-webhook-verifier.js +112 -0
  120. package/dist/agent/channels/slack-webhook-verifier.js.map +1 -0
  121. package/dist/agent/channels/slack.d.ts +37 -0
  122. package/dist/agent/channels/slack.d.ts.map +1 -0
  123. package/dist/agent/channels/slack.js +1169 -0
  124. package/dist/agent/channels/slack.js.map +1 -0
  125. package/dist/agent/contributions.d.ts +17 -0
  126. package/dist/agent/contributions.d.ts.map +1 -0
  127. package/dist/agent/contributions.js +32 -0
  128. package/dist/agent/contributions.js.map +1 -0
  129. package/dist/agent/docs-generated.d.ts +3 -0
  130. package/dist/agent/docs-generated.d.ts.map +1 -0
  131. package/dist/agent/docs-generated.js +13 -0
  132. package/dist/agent/docs-generated.js.map +1 -0
  133. package/dist/agent/ops.d.ts +3 -0
  134. package/dist/agent/ops.d.ts.map +1 -0
  135. package/dist/agent/ops.js +38 -0
  136. package/dist/agent/ops.js.map +1 -0
  137. package/dist/agent/provider-events.d.ts +3 -0
  138. package/dist/agent/provider-events.d.ts.map +1 -0
  139. package/dist/agent/provider-events.js +84 -0
  140. package/dist/agent/provider-events.js.map +1 -0
  141. package/dist/agent/register.d.ts +5 -0
  142. package/dist/agent/register.d.ts.map +1 -0
  143. package/dist/agent/register.js +118 -0
  144. package/dist/agent/register.js.map +1 -0
  145. package/dist/agent/surface-parity.d.ts +3 -0
  146. package/dist/agent/surface-parity.d.ts.map +1 -0
  147. package/dist/agent/surface-parity.js +95 -0
  148. package/dist/agent/surface-parity.js.map +1 -0
  149. package/package.json +209 -0
  150. package/tsconfig.build.json +22 -0
  151. package/tsconfig.json +15 -0
@@ -0,0 +1,205 @@
1
+ // Component 3 of the Slack engagement model (slack-engagement-spec.md): domain-scoped
2
+ // channel reads. "If a domain has 3 channels, the agent knows all 3 and can read them —
3
+ // and NOTHING else." The Slack bot token is workspace-wide (it can read any channel the
4
+ // bot is in), so the boundary is OURS to enforce at the tool layer — the same shape as the
5
+ // connector hard-gate (access/connector-scope.ts): broad credential underneath, narrow
6
+ // allowlist on top, FAIL CLOSED. The domain's channel list IS the allowlist.
7
+ //
8
+ // This module is the shared spine for the Slack read path (channels/channel-read.ts's SLACK
9
+ // adapter, behind the generic read verbs): resolve the allowed channel ids for the session, assert a
10
+ // requested channel is in them (or throw), and make the actual Slack Web API call. The call
11
+ // goes through OE's `callSlackWebApi` (channels/slack-web-api.ts) — the ONE Slack Web API seam,
12
+ // which carries the 429/`Retry-After` backoff — form-encoded with the same `SLACK_BOT_TOKEN` env
13
+ // fallback the Slack channel itself uses.
14
+ import { callSlackWebApi } from "./slack-web-api.js";
15
+ import { auditChannelReadRefusal, bindFrom, getConfig, domainForChannel, resolvePartition, refutedReadAuthorIds, ChannelScopeError, markNotApplied, domainRoster, } from "@open-everywhere/core/agent/adapter-api.js";
16
+ // The SAME per-author membership probe the wake leg uses (OPE-474) — see the adapter's own note on
17
+ // why this is reused rather than re-derived. The direction is safe: slack-events.ts does not import
18
+ // this module, so nothing here closes a cycle.
19
+ import { slackAuthorIsMember } from "./slack-events.js";
20
+ // ChannelScopeError now LIVES in core (channels/channel-read.ts) — it predates the generic read
21
+ // verbs and the platform-parameterised assertSurfaceAllowed throws the same class, so the ONE
22
+ // class identity must sit on the core side of the boundary. Re-exported here so the bundle's own
23
+ // callers/tests keep their import site.
24
+ export { ChannelScopeError };
25
+ // A fully-qualified surface key is `platform/workspace/surface`; for Slack the surface
26
+ // segment IS the channel id. Returns null for non-Slack keys (defensive — domains can span
27
+ // platforms in principle, and we only gate Slack here).
28
+ function channelIdFromKey(key) {
29
+ const parts = key.split("/");
30
+ if (parts.length !== 3 || parts[0] !== "slack")
31
+ return null;
32
+ return parts[2] || null;
33
+ }
34
+ /**
35
+ * The Slack channel ids this session may read: every Slack channel in its explicit domain,
36
+ * PLUS its own current channel (always readable — it's where the agent was engaged). A DM,
37
+ * an unassigned channel, or a criteria pseudo-domain (org/public/dm — no channel list)
38
+ * yields just the current channel. Empty only when the surface isn't a Slack channel.
39
+ */
40
+ export function allowedChannelIds(ctx) {
41
+ const p = resolvePartition(ctx);
42
+ const ids = new Set();
43
+ const own = channelIdFromKey(p.surfaceKey);
44
+ if (own)
45
+ ids.add(own); // the agent can always read its own channel
46
+ const domain = domainForChannel(p.surfaceKey, getConfig().loadDomains());
47
+ if (domain) {
48
+ for (const key of domainRoster(domain)) {
49
+ const id = channelIdFromKey(key);
50
+ if (id)
51
+ ids.add(id);
52
+ }
53
+ }
54
+ return { ids, domain: domain?.name };
55
+ }
56
+ /**
57
+ * Assert a requested channel id is readable by this session — else AUDIT the refusal and throw
58
+ * (fail closed). The error names the domain so the model gets an actionable message, not a bare
59
+ * denial. The decision is unchanged from the silent version; only the row is new
60
+ * (core channels/read-scope-audit.ts).
61
+ */
62
+ export async function assertChannelAllowed(ctx, channelId, deps) {
63
+ const { ids, domain } = allowedChannelIds(ctx);
64
+ if (ids.has(channelId))
65
+ return;
66
+ const p = resolvePartition(ctx);
67
+ await auditChannelReadRefusal(ctx, { platform: "slack", surfaceId: channelId, callerKey: p.surfaceKey, callerDomain: domain, domains: getConfig().loadDomains() }, deps);
68
+ const scope = domain ? `domain "${domain}"` : "this channel's (no multi-channel domain)";
69
+ throw new ChannelScopeError(`channel "${channelId}" is not in ${scope} read scope. The agent may only read channels in its ` +
70
+ `memory domain (memory/config/domains.ts). Add the channel to the domain's \`channels\` to grant it.`);
71
+ }
72
+ /**
73
+ * Low-level POST to a (read-only) Slack Web API method with the bot token, via OE's
74
+ * `callSlackWebApi` (form-encoded — Slack's JSON support is partial: `conversations.replies`/
75
+ * `history` want form). Throws on a missing token or a non-ok Slack response so the tool
76
+ * surfaces a real error. The explicit env pre-check keeps the actionable "not set" message
77
+ * (the seam itself throws a terser "SLACK_BOT_TOKEN is required.").
78
+ */
79
+ export async function callSlackRead(operation, params) {
80
+ if (!process.env.SLACK_BOT_TOKEN) {
81
+ throw new Error("SLACK_BOT_TOKEN is not set — the Slack read tools need the bot token to call the Web API.");
82
+ }
83
+ // botToken omitted → the seam resolves it from SLACK_BOT_TOKEN (same fallback as before).
84
+ const json = (await callSlackWebApi({ botToken: undefined, operation, body: params }));
85
+ // DEFINITIVE (OPE-821): Slack answers HTTP 200 with `ok:false` when it READ the request and
86
+ // refused it, so the call had no effect. `react` is the caller that acts on this — a refused
87
+ // `reactions.add` proves no reaction landed. A throw from the seam (transport, or a non-2xx)
88
+ // never reaches this line and stays AMBIGUOUS, which is the honest answer for a call that got no
89
+ // reply. Marking it here rather than in `react` keeps ONE statement of what Slack's `ok:false`
90
+ // means, for every method that goes through this transport.
91
+ if (!json.ok)
92
+ throw markNotApplied(new Error(`Slack ${operation} failed: ${json.error ?? "unknown error"}`));
93
+ return json;
94
+ }
95
+ /**
96
+ * Normalize a raw Slack messages array (history/replies) to rendered lines, oldest-first.
97
+ *
98
+ * `author` IS UNCHANGED and stays the display string the model reads (`user`, else `bot_id`, else
99
+ * `(unknown)`) — every render golden depends on it. OPE-474 adds the two GRADING fields beside it
100
+ * rather than reinterpreting it: `author` is chosen for legibility and cannot be parsed back into an
101
+ * identity, which is exactly why the read leg had nothing to probe before.
102
+ *
103
+ * `bot_id` WINS over `user` for the machine classification, even on the messages that carry both (an
104
+ * app posting as a bot user). That direction is fail-OPEN — a machine is dropped from the probe and
105
+ * marks nothing — which is the posture the whole author leg takes.
106
+ */
107
+ export function renderMessages(raw, opts = {}) {
108
+ if (!Array.isArray(raw))
109
+ return [];
110
+ const out = [];
111
+ for (const m of raw) {
112
+ if (typeof m.subtype === "string" && m.subtype.length > 0)
113
+ continue; // skip joins/edits/system
114
+ const text = typeof m.text === "string" ? m.text : "";
115
+ if (!text)
116
+ continue;
117
+ const user = typeof m.user === "string" && m.user.length > 0 ? m.user : undefined;
118
+ const botId = typeof m.bot_id === "string" && m.bot_id.length > 0 ? m.bot_id : undefined;
119
+ out.push({
120
+ author: m.user ?? m.bot_id ?? "(unknown)",
121
+ ts: m.ts ?? "",
122
+ text,
123
+ ...(botId !== undefined ? { bot: true } : user !== undefined ? { authorId: user } : {}),
124
+ });
125
+ }
126
+ // conversations.history returns newest-first; replies returns oldest-first.
127
+ if (opts.oldestFirst === false)
128
+ out.reverse();
129
+ return out;
130
+ }
131
+ // --- the SLACK read adapter (moved from core channels/channel-read.ts in OPE-114 stage 4) ---
132
+ // Registered by registerSlackProvider() via registerReadAdapter — the generic read verbs
133
+ // (read_channel / read_thread) resolve it by platform string; behaviour is byte-identical to the
134
+ // in-core adapter (the EXACT calls the retired per-surface Slack read tools made).
135
+ const SLACK_CHANNEL_DEFAULT_LIMIT = 50;
136
+ const SLACK_THREAD_DEFAULT_LIMIT = 100;
137
+ // --- the OPE-474 non-member author leg ---------------------------------------------------------
138
+ //
139
+ // Slack is the one chat surface that can refute an author BY ITSELF: `users.info` reports
140
+ // `is_restricted` / `is_ultra_restricted` / `is_stranger`, which slack-events.ts's
141
+ // {@link slackAuthorIsMember} already folds into one TTL-cached boolean for the wake leg. The read
142
+ // leg reuses THAT probe verbatim — a second implementation of "is this person a member?" is how a
143
+ // wake and a read come to disagree about the same author.
144
+ //
145
+ // 🔴 THE COST IS BOUNDED BY PEOPLE, NOT BY MESSAGES (Tommy, 2026-08-14: no up-front member list).
146
+ // `refutedReadAuthorIds` probes the page's DISTINCT authors, and the probe is TTL-cached across
147
+ // calls, so a 200-message read of a busy channel is a handful of authors and mostly cache hits. An
148
+ // up-front roster would instead size with the WORKSPACE and go stale between reads.
149
+ //
150
+ // THE THREE-WAY VERDICT IS KEPT THREE-WAY. `slackAuthorIsMember` answers `true` (member), `false`
151
+ // (positively refuted) or `undefined` (lookup unavailable), and only `false` refutes. Collapsing
152
+ // `undefined` into a refusal would mark every read on a deployment whose bot token cannot call
153
+ // `users.info` — an always-on bit, arriving through this door.
154
+ /** The Slack team id + the register's arming key for this session's own workspace. Both come from
155
+ * the BINDING, never from inspecting an author id: the platform and the tenant are surface facts. */
156
+ function slackReadTenant(ctx) {
157
+ const p = resolvePartition(ctx);
158
+ return { teamId: p.workspace, tenantKey: p.workspaceKey };
159
+ }
160
+ /** Slack read via OE's callSlackWebApi — domain-allowlist-gated, rendered oldest-first. */
161
+ export const slackReadAdapter = {
162
+ platform: "slack",
163
+ /**
164
+ * OPE-929 — Slack's own spelling of "which room and which thread is this turn in".
165
+ *
166
+ * The CONTAINER is the channel the post-back target names. The THREAD ROOT is `threadTs`, which
167
+ * eve's Slack adapter falls back to the triggering message's own ts — so a bare channel mention
168
+ * carries one too, and reading "this thread" on such a turn reads the one-message thread the
169
+ * mention started, which is what a person means by it.
170
+ */
171
+ wakeAddress(ctx) {
172
+ const b = bindFrom(ctx);
173
+ const container = typeof b.target?.["channelId"] === "string" ? b.target["channelId"] : undefined;
174
+ if (!container)
175
+ return undefined;
176
+ return { container, ...(b.threadTs ? { threadRoot: b.threadTs } : {}) };
177
+ },
178
+ async readChannel(ctx, channel, limit, call = callSlackRead) {
179
+ await assertChannelAllowed(ctx, channel);
180
+ const res = await call("conversations.history", { channel, limit: limit ?? SLACK_CHANNEL_DEFAULT_LIMIT, inclusive: true });
181
+ return renderMessages(res.messages, { oldestFirst: false });
182
+ },
183
+ async readThread(ctx, channel, threadTs, limit, call = callSlackRead) {
184
+ await assertChannelAllowed(ctx, channel);
185
+ const res = await call("conversations.replies", { channel, ts: threadTs, limit: limit ?? SLACK_THREAD_DEFAULT_LIMIT });
186
+ return renderMessages(res.messages, { oldestFirst: true });
187
+ },
188
+ /** OPE-474 / OPE-534. Both legs run: Slack's own `users.info` refutation, and the operator's
189
+ * author-trust register (which most Slack deployments leave unarmed, so it costs nothing there).
190
+ * It answers with the refuted AUTHOR IDS, because core reads the one answer twice — the turn mark
191
+ * and the `withhold` body replacement. `probe` is injected ONLY so a test can drive the grading
192
+ * without a live Slack — every live call defaults. */
193
+ async refutedAuthorIds(ctx, lines, probe = slackAuthorIsMember) {
194
+ const { teamId, tenantKey } = slackReadTenant(ctx);
195
+ return await refutedReadAuthorIds(lines, tenantKey, {
196
+ surfaceRefuted: async (authorId) => {
197
+ const member = await probe(authorId, teamId);
198
+ return member === undefined ? undefined : !member; // undefined stays "cannot check"
199
+ },
200
+ // No `memberships` leg: Slack has no group-rung key a message carries, so an armed register
201
+ // matches this surface on principal keys alone (`slack/<team>/<user>`).
202
+ });
203
+ },
204
+ };
205
+ //# sourceMappingURL=slack-read.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-read.js","sourceRoot":"","sources":["../../../agent/channels/slack-read.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Run `fn` as ONE delivery, so the writes below it can be tracked.
3
+ *
4
+ * ONE caller: `slack-front.ts`'s public route handler, wrapping its whole body. Do not wrap anything
5
+ * else; the value means "the delivery in flight right now", and a second writer would make that
6
+ * false.
7
+ */
8
+ export declare function withSlackRouteHold<T>(fn: () => T): T;
9
+ /**
10
+ * Track a best-effort write on the delivery in flight, and hand it back so a caller that can also
11
+ * await it may.
12
+ *
13
+ * `work` MUST already swallow its own failures — every caller here is an audit write whose contract
14
+ * is "never throws, never changes the decision". Nothing is caught for it: swallowing a rejection
15
+ * here would let a genuinely broken writer look held.
16
+ *
17
+ * Outside a delivery (a unit test calling a door directly, a future non-route caller) there is
18
+ * nothing to track and the work comes straight back — so `await holdRouteWork(...)` is correct in
19
+ * both worlds and a caller never has to ask which one it is in.
20
+ */
21
+ export declare function holdRouteWork<T>(work: Promise<T>): Promise<T>;
22
+ /**
23
+ * The promise to hand the platform in place of `work`: it settles when `work` has settled AND every
24
+ * write {@link holdRouteWork} tracked has settled too.
25
+ *
26
+ * 🔴 CALL IT SYNCHRONOUSLY, INSIDE THE DELIVERY — it reads the delivery's tracked set at call time
27
+ * and closes over it, so a call from outside one silently degrades to "just `work`". `slack-front.ts`
28
+ * calls it from the `waitUntil` it hands down, which is exactly that position.
29
+ *
30
+ * It never rejects: a rejected `work` is the caller's own business (eve already logs its background
31
+ * task failures), and a hold that rejected would turn an observability write into a route error.
32
+ */
33
+ export declare function routeHoldQuiesced(work: Promise<unknown>): Promise<void>;
34
+ //# sourceMappingURL=slack-route-hold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-route-hold.d.ts","sourceRoot":"","sources":["../../../agent/channels/slack-route-hold.ts"],"names":[],"mappings":"AAwDA;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEpD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAG7D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAWvE"}
@@ -0,0 +1,97 @@
1
+ // THE ROUTE HOLD (OPE-1648) — "keep this best-effort write alive until it lands".
2
+ //
3
+ // WHAT IT CLOSES. Every Slack door answers its 200 IMMEDIATELY: Slack retries a delivery it does
4
+ // not see acked inside three seconds, and eve's own dispatcher returns the response and then
5
+ // finishes the turn in the route's `waitUntil`. On Vercel the serverless invocation can freeze the
6
+ // moment nothing is holding it, so a `void`-ed audit write races that freeze and the row never
7
+ // lands. Those rows are not decoration: `slack.ts`'s `auditWakeDropped` header states that the
8
+ // webhook function's console output never reaches `vercel logs`, so the row is the ONLY durable
9
+ // signal that a wake was refused.
10
+ //
11
+ // 🔴 A LATE `waitUntil` CALL IS NOT A HOLD, WHICH IS WHY THIS FILE EXISTS AT ALL. eve collects the
12
+ // route's background tasks in an array and, the moment the handler resolves, hands the platform
13
+ // `Promise.allSettled(tasks)` (`internal/nitro/routes/channel-dispatch.js`, `flushBackgroundTasks`).
14
+ // `allSettled` reads that array ONCE, synchronously — so a promise pushed after the handler returned
15
+ // is in a list nobody is awaiting, and calling `waitUntil` from a wake route that runs after the 200
16
+ // buys exactly nothing. Almost every write this module holds is in that position.
17
+ //
18
+ // SO THE HOLD RIDES THE WORK THE ROUTE ALREADY REGISTERED. Every one of these writes happens inside
19
+ // a promise the front route handed to `waitUntil` while it was still running — eve's own turn
20
+ // dispatch on the native forward, a FIFO wake, a name-drop. `slack-front.ts` wraps each of those in
21
+ // {@link routeHoldQuiesced} before passing it on, so the promise the platform waits on settles only
22
+ // after the delivery's tracked writes have. Nothing extra is registered: a delivery that queues no
23
+ // background work (a 401, a dropped sibling event) hands the platform nothing, exactly as before.
24
+ //
25
+ // WHY AsyncLocalStorage AND NOT A PARAMETER. The writers do not hold the route args: the mention
26
+ // door's row is written inside eve's `onAppMention` hook, several frames below a hand-off that
27
+ // discards them, and the engagement doors' rows are written from route functions that take deps.
28
+ // Threading a serverless-runtime concern through every wake route's signature to reach them would
29
+ // put it in the type of each one. The front route stows it once instead, for the extent of ONE
30
+ // delivery. One process serves many Slack deliveries at once, and a module global would mix
31
+ // delivery A's writes into delivery B's hold. Same primitive and same reasoning as
32
+ // slack-identity.ts's forwarded-envelope carrier, which is proven to survive the hand-off into eve's
33
+ // handler (slack-enterprise-rung-test.ts §2).
34
+ //
35
+ // 🔴 IT NEVER CHANGES A DECISION, AND IT NEVER THROWS. A hold is observability plumbing: every
36
+ // tracked write already swallows its own failures, and the quiesced promise settles rather than
37
+ // rejects, so a broken sink can never turn a drop into a turn.
38
+ import { AsyncLocalStorage } from "node:async_hooks";
39
+ /**
40
+ * How many drain rounds one quiesce gets. Each round awaits everything registered so far, so a round
41
+ * only follows work that another round's work started. Ten is far past any real chain (a wake queues
42
+ * one audit write, which queues nothing) and it bounds a writer that re-registers itself forever —
43
+ * which would otherwise hold the invocation open until the platform killed it.
44
+ */
45
+ const MAX_DRAIN_ROUNDS = 10;
46
+ const routeHold = new AsyncLocalStorage();
47
+ /**
48
+ * Run `fn` as ONE delivery, so the writes below it can be tracked.
49
+ *
50
+ * ONE caller: `slack-front.ts`'s public route handler, wrapping its whole body. Do not wrap anything
51
+ * else; the value means "the delivery in flight right now", and a second writer would make that
52
+ * false.
53
+ */
54
+ export function withSlackRouteHold(fn) {
55
+ return routeHold.run({ pending: [] }, fn);
56
+ }
57
+ /**
58
+ * Track a best-effort write on the delivery in flight, and hand it back so a caller that can also
59
+ * await it may.
60
+ *
61
+ * `work` MUST already swallow its own failures — every caller here is an audit write whose contract
62
+ * is "never throws, never changes the decision". Nothing is caught for it: swallowing a rejection
63
+ * here would let a genuinely broken writer look held.
64
+ *
65
+ * Outside a delivery (a unit test calling a door directly, a future non-route caller) there is
66
+ * nothing to track and the work comes straight back — so `await holdRouteWork(...)` is correct in
67
+ * both worlds and a caller never has to ask which one it is in.
68
+ */
69
+ export function holdRouteWork(work) {
70
+ routeHold.getStore()?.pending.push(work);
71
+ return work;
72
+ }
73
+ /**
74
+ * The promise to hand the platform in place of `work`: it settles when `work` has settled AND every
75
+ * write {@link holdRouteWork} tracked has settled too.
76
+ *
77
+ * 🔴 CALL IT SYNCHRONOUSLY, INSIDE THE DELIVERY — it reads the delivery's tracked set at call time
78
+ * and closes over it, so a call from outside one silently degrades to "just `work`". `slack-front.ts`
79
+ * calls it from the `waitUntil` it hands down, which is exactly that position.
80
+ *
81
+ * It never rejects: a rejected `work` is the caller's own business (eve already logs its background
82
+ * task failures), and a hold that rejected would turn an observability write into a route error.
83
+ */
84
+ export function routeHoldQuiesced(work) {
85
+ const hold = routeHold.getStore();
86
+ return (async () => {
87
+ await Promise.allSettled([work]);
88
+ if (!hold)
89
+ return;
90
+ for (let round = 0; round < MAX_DRAIN_ROUNDS && hold.pending.length > 0; round++) {
91
+ const batch = hold.pending;
92
+ hold.pending = [];
93
+ await Promise.allSettled(batch);
94
+ }
95
+ })();
96
+ }
97
+ //# sourceMappingURL=slack-route-hold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-route-hold.js","sourceRoot":"","sources":["../../../agent/channels/slack-route-hold.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,EAAE;AACF,iGAAiG;AACjG,6FAA6F;AAC7F,mGAAmG;AACnG,+FAA+F;AAC/F,+FAA+F;AAC/F,gGAAgG;AAChG,kCAAkC;AAClC,EAAE;AACF,mGAAmG;AACnG,gGAAgG;AAChG,qGAAqG;AACrG,qGAAqG;AACrG,qGAAqG;AACrG,kFAAkF;AAClF,EAAE;AACF,oGAAoG;AACpG,8FAA8F;AAC9F,oGAAoG;AACpG,oGAAoG;AACpG,mGAAmG;AACnG,kGAAkG;AAClG,EAAE;AACF,iGAAiG;AACjG,+FAA+F;AAC/F,iGAAiG;AACjG,kGAAkG;AAClG,+FAA+F;AAC/F,4FAA4F;AAC5F,mFAAmF;AACnF,qGAAqG;AACrG,8CAA8C;AAC9C,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,+DAA+D;AAE/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAQrD;;;;;GAKG;AACH,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAE5B,MAAM,SAAS,GAAG,IAAI,iBAAiB,EAAa,CAAC;AAErD;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAI,EAAW;IAC/C,OAAO,SAAS,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAI,IAAgB;IAC/C,SAAS,CAAC,QAAQ,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzC,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAsB;IACtD,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,EAAE,CAAC;IAClC,OAAO,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACjC,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,gBAAgB,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;YACjF,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC;YAC3B,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;YAClB,MAAM,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAClC,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;AACP,CAAC"}
@@ -0,0 +1,162 @@
1
+ import { type WakeAttachment } from "@open-everywhere/core/agent/adapter-api.js";
2
+ import { type ParsedMessageEvent } from "./slack-events.js";
3
+ /**
4
+ * A decoded Slack shortcut invocation, narrowed to the fields a wake needs.
5
+ *
6
+ * The two shortcut families share every field but the target: a `message` shortcut carries the
7
+ * channel it was invoked in and the message it was invoked on; a `global` shortcut carries
8
+ * neither, because Slack fires it from the composer with no conversation in scope.
9
+ *
10
+ * Field-compatible with eve's own `SlackShortcut` union (`eve/channels/slack`), which is the
11
+ * public contract for the same two payloads. We parse the raw payload ourselves because eve's
12
+ * `parseShortcutPayload` is internal to its interactivity handler and is not exported.
13
+ */
14
+ export interface ParsedSlackShortcut {
15
+ /** `message` for a `message_action` payload, `global` for a `shortcut` payload. */
16
+ readonly kind: "message" | "global";
17
+ /** The shortcut's registered callback id — the operator-chosen name of this trigger. */
18
+ readonly callbackId: string;
19
+ /** Slack's per-invocation trigger id — unique per click. Used as the wake's idempotency key on
20
+ * the ASSUMPTION that a Slack redelivery of one click carries the same trigger id. UNMEASURED:
21
+ * Slack documents stable-`event_id` retry semantics for the Events API, not for interactivity
22
+ * payloads. If the assumption is wrong the cost is a double wake, which is the same fail-open
23
+ * posture the rest of the dedupe layer already takes. See {@link shortcutWakeKey}. */
24
+ readonly triggerId: string;
25
+ /** The Slack user who invoked the shortcut. The wake's principal. */
26
+ readonly userId: string;
27
+ readonly teamId?: string;
28
+ /**
29
+ * The Enterprise Grid organisation the invocation's workspace sits in (`E…`), when the payload
30
+ * named one (OPE-1559). A shortcut never arrives as an event envelope, so there is no
31
+ * `enterprise_id` envelope field to read; the interactivity route names the Grid on the payload
32
+ * itself, in THREE spellings, and all three are read ({@link parseSlackShortcutPayload}).
33
+ *
34
+ * 🔴 IT IS THE ROOM'S GRID, NEVER THE INVOKER'S. Both spellings sit on the payload's own
35
+ * `enterprise` / `team` object, which describes WHERE the invocation happened. The invoker's
36
+ * `user.enterprise_user.enterprise_id` describes where THEY belong, and reading it would let a
37
+ * Slack Connect stranger's own Grid declaration vouch our room. So the author leg keeps its own
38
+ * source (`users.info`, slack-identity.ts) and this one never touches it.
39
+ */
40
+ readonly enterpriseId?: string;
41
+ /** Present on a `message` shortcut only — the conversation the message lives in. */
42
+ readonly channelId?: string;
43
+ /** Present on a `message` shortcut only. */
44
+ readonly message?: {
45
+ /** The target message's text, verbatim. UNTRUSTED — it is somebody's Slack message. */
46
+ readonly text: string;
47
+ readonly ts: string;
48
+ /** The thread the target message sits in, when it sits in one. */
49
+ readonly threadTs?: string;
50
+ /** The target message's AUTHOR — never the wake's principal (see the file header). */
51
+ readonly userId?: string;
52
+ /** Image/file REFERENCES off the target message's `files[]`, parsed by the SAME
53
+ * {@link parseSlackFiles} the Events API path uses. A shortcut on a message carrying a
54
+ * screenshot must surface that screenshot, exactly as reading the message in the channel
55
+ * would — otherwise the same message means less through this door than through that one.
56
+ * A REFERENCE ONLY: it reaches a wake, and the bot token, solely through
57
+ * {@link shortcutMessageEvent}'s `admitAttachments` gate. */
58
+ readonly attachments?: readonly WakeAttachment[];
59
+ };
60
+ }
61
+ /**
62
+ * Decode an already-JSON-parsed interactivity payload into a {@link ParsedSlackShortcut}, or
63
+ * `null` for anything that is not a shortcut. TOTAL AND PURE: it never throws and it writes
64
+ * nothing.
65
+ *
66
+ * 🔴 PURITY IS A SECURITY PROPERTY HERE, NOT A STYLE CHOICE (OPE-1442). This runs inside
67
+ * `classifyEnvelope`, which decides what a request IS — long before the dedupe, the readability
68
+ * gate and the ambient-off drop decide whether it wakes anything. An earlier revision registered
69
+ * the target message's attachments right here, so every click seeded the process-global registry
70
+ * (a 1000-entry LRU) even when the shortcut went on to wake NOTHING: repeated clicks on a domain
71
+ * with the unified window off evicted the records real uploads depend on, and `describe_image`
72
+ * then failed closed on a genuine image. The registry is now seeded by the front door's shortcut
73
+ * branch, after the gates it must sit behind.
74
+ *
75
+ * A payload is REFUSED (null) when it cannot produce a wake:
76
+ * • the outer `type` is neither `message_action` nor `shortcut`;
77
+ * • `callback_id`, `trigger_id` or `user.id` is missing — the first two are the trigger's name
78
+ * and its idempotency key, and the third is the principal, so a wake without any of them
79
+ * would have to be invented rather than derived;
80
+ * • a `message_action` that carries no `channel.id` or no `message.ts` — a message shortcut
81
+ * whose target cannot be addressed is not a message shortcut.
82
+ * Refusing is FAIL-CLOSED: the front route acks Slack and wakes nothing.
83
+ *
84
+ * `message.text` is allowed to be EMPTY and still parses. Slack sends an empty `text` for a message
85
+ * whose content lives entirely in blocks or files, and dropping a click a person made is the worse
86
+ * answer. The wake then renders an empty body — it says nothing ABOUT the emptiness. Files on that
87
+ * message ride through as attachments only where the front door admits them
88
+ * ({@link shortcutMessageEvent}'s `admitAttachments`), so a shortcut on a screenshot in a room the
89
+ * app cannot read wakes with an empty body and no file.
90
+ */
91
+ export declare function parseSlackShortcutPayload(raw: unknown): ParsedSlackShortcut | null;
92
+ /**
93
+ * The idempotency key for one shortcut invocation.
94
+ *
95
+ * Keyed on `trigger_id`, NOT on the target message. What that buys is certain: a person stays free
96
+ * to invoke the same shortcut on the same message twice on purpose, which keying on `channel:ts`
97
+ * (the shape an Events API wake falls back to) would have swallowed for ten minutes.
98
+ *
99
+ * What it de-dupes rests on an UNMEASURED assumption — that a Slack redelivery of one click carries
100
+ * the same trigger id (see {@link ParsedSlackShortcut.triggerId}). Wrong ⇒ a double wake, never a
101
+ * missed one, which is the direction the whole dedupe layer already fails in.
102
+ *
103
+ * It carries no bot-identity prefix, unlike `slack-front.ts`'s `wakeKey`. A trigger id is minted by
104
+ * Slack per app, so two apps in one workspace cannot collide on one; the `oe-shortcut:` prefix
105
+ * keeps it out of the Events API key space.
106
+ */
107
+ export declare function shortcutWakeKey(sc: ParsedSlackShortcut): string;
108
+ /**
109
+ * The body a GLOBAL shortcut wakes with. It has no message, so the trigger's own name is the whole
110
+ * content — which is the honest thing to hand the model: somebody pressed a named button.
111
+ *
112
+ * The callback id is operator-authored configuration, not somebody's Slack message, so it is NOT
113
+ * fenced as untrusted content — the same reasoning that leaves a slash-command name unfenced.
114
+ */
115
+ export declare function globalShortcutText(sc: ParsedSlackShortcut): string;
116
+ /**
117
+ * Map a shortcut onto the shared {@link ParsedMessageEvent} shape, so the front door's existing
118
+ * wake spine (partition → seat → window → dedupe → gates → divert) runs over it unchanged.
119
+ *
120
+ * 🔴 `userId` IS THE INVOKER on both kinds. See the file header: the target message's author never
121
+ * becomes the wake's principal.
122
+ *
123
+ * THE TWO TEXT FIELDS CARRY THE MESSAGE-SHORTCUT SPLIT EXACTLY AS THEY ALREADY MEAN IT. A message
124
+ * shortcut is a FORWARD with no covering note — the invoker typed nothing and pointed at somebody
125
+ * else's message — and {@link ParsedMessageEvent} already draws that line: `text` is "own text
126
+ * plus any quoted message", `authoredText` is "the author's OWN typed text". So `text` carries the
127
+ * target message and `authoredText` is empty. Any rule that asks "what did this person SAY?" then
128
+ * correctly answers "nothing", which is true, and the model still receives the message as the
129
+ * wake's content.
130
+ *
131
+ * `ts` differs by kind, and deliberately:
132
+ * • a MESSAGE shortcut takes the target message's real ts, so the reply anchors under the
133
+ * message the shortcut was invoked on (`routeSupervisorEvent` reads `threadTs ?? ts`);
134
+ * • a GLOBAL shortcut has no message, so it MINTS one from `nowMs` in Slack's own
135
+ * `seconds.microseconds` shape. 🔴 It must be ts-SHAPED, not merely unique: the envelope
136
+ * renders `sent-at` by running `Number.parseFloat` over this field (`slackTsToIso`), so a
137
+ * non-numeric id silently becomes a nonsense date — the trigger id `13345224609.7384…` parses
138
+ * to the year 2392. Minting from the clock makes `sent-at` the truth: the moment the person
139
+ * pressed the button. Uniqueness is not this field's job — the wake's idempotency key is
140
+ * {@link shortcutWakeKey}, which does key on the trigger id.
141
+ *
142
+ * `isAppMention` is false on both. It reports whether the TEXT contains `<@bot>`, and neither body
143
+ * does; the "was the agent addressed?" question is answered by the caller passing `directMention`,
144
+ * which is the same seam the single-window mention divert uses.
145
+ *
146
+ * 🔴 `admitAttachments` GATES THE TARGET MESSAGE'S FILES, AND IT DEFAULTS TO FALSE (OPE-1442).
147
+ * Slack delivers a `message_action` for a room the app is not in, and the payload carries that
148
+ * message's `files[].url_private` — bytes the app's own channel reads refuse. So the files ride
149
+ * through only when the caller has POSITIVELY established that the app can read the room
150
+ * (`slackChannelFactsFor`'s `appCanRead`). The default is closed because the cost of the two
151
+ * mistakes is not symmetric: a withheld file is a wake that says less, an admitted one is the bot
152
+ * token downloading somebody's private upload from a room the agent cannot open.
153
+ *
154
+ * WITHHOLDING DROPS THE REFERENCE TOO, rather than passing an id that resolves to nothing. The
155
+ * envelope renders an attachment as an id and `describe_image` fails closed on an id the registry
156
+ * never saw, so an advertised-but-unresolvable file tells the model an image exists and then
157
+ * cannot show it — worse than not mentioning it.
158
+ */
159
+ export declare function shortcutMessageEvent(sc: ParsedSlackShortcut, channelId: string, channelType?: string, nowMs?: number, opts?: {
160
+ readonly admitAttachments?: boolean;
161
+ }): ParsedMessageEvent;
162
+ //# sourceMappingURL=slack-shortcut.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-shortcut.d.ts","sourceRoot":"","sources":["../../../agent/channels/slack-shortcut.ts"],"names":[],"mappings":"AA6BA,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,4CAA4C,CAAC;AACpD,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAG7E;;;;;;;;;;GAUG;AACH,MAAM,WAAW,mBAAmB;IAClC,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,QAAQ,CAAC;IACpC,wFAAwF;IACxF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;2FAIuF;IACvF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,oFAAoF;IACpF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,CAAC,EAAE;QACjB,uFAAuF;QACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QACpB,kEAAkE;QAClE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,sFAAsF;QACtF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB;;;;;sEAK8D;QAC9D,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;KAClD,CAAC;CACH;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CA6ClF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,mBAAmB,GAAG,MAAM,CAE/D;AAsCD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,mBAAmB,GAAG,MAAM,CAElE;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,mBAAmB,EACvB,SAAS,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,MAAM,EACpB,KAAK,GAAE,MAAmB,EAC1B,IAAI,GAAE;IAAE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAO,GACjD,kBAAkB,CAuBpB"}