@open-everywhere/provider-slack 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/access/slack-attachment-fetch.ts +3 -1
- package/agent/app-manifest.ts +48 -10
- package/agent/channels/slack-commands.ts +5 -1
- package/agent/channels/slack-events.ts +13 -4
- package/agent/channels/slack-front.ts +21 -6
- package/agent/channels/slack-read.ts +9 -3
- package/agent/channels/slack-setup-page.ts +20 -0
- package/agent/channels/slack-web-api.ts +34 -7
- package/agent/channels/slack.ts +11 -3
- package/agent/contributions.ts +1 -0
- package/agent/docs-generated.ts +1 -1
- package/agent/ops.ts +29 -61
- package/agent/register.ts +27 -1
- package/agent/setup/claim.ts +343 -0
- package/agent/setup/credentials.ts +265 -0
- package/agent/setup/eve-credentials.ts +77 -0
- package/agent/setup/install-state.ts +248 -0
- package/agent/setup/page.ts +579 -0
- package/agent/setup/slack-api-setup.ts +176 -0
- package/agent/status.ts +81 -0
- package/dist/agent/access/slack-attachment-fetch.d.ts.map +1 -1
- package/dist/agent/access/slack-attachment-fetch.js +3 -1
- package/dist/agent/access/slack-attachment-fetch.js.map +1 -1
- package/dist/agent/app-manifest.d.ts +4 -4
- package/dist/agent/app-manifest.d.ts.map +1 -1
- package/dist/agent/app-manifest.js +48 -10
- package/dist/agent/app-manifest.js.map +1 -1
- package/dist/agent/channels/slack-commands.d.ts.map +1 -1
- package/dist/agent/channels/slack-commands.js +5 -1
- package/dist/agent/channels/slack-commands.js.map +1 -1
- package/dist/agent/channels/slack-events.d.ts +1 -1
- package/dist/agent/channels/slack-events.d.ts.map +1 -1
- package/dist/agent/channels/slack-events.js +13 -4
- package/dist/agent/channels/slack-events.js.map +1 -1
- package/dist/agent/channels/slack-front.d.ts +1 -1
- package/dist/agent/channels/slack-front.d.ts.map +1 -1
- package/dist/agent/channels/slack-front.js +20 -5
- package/dist/agent/channels/slack-front.js.map +1 -1
- package/dist/agent/channels/slack-read.d.ts +6 -1
- package/dist/agent/channels/slack-read.d.ts.map +1 -1
- package/dist/agent/channels/slack-read.js +9 -3
- package/dist/agent/channels/slack-read.js.map +1 -1
- package/dist/agent/channels/slack-setup-page.d.ts +3 -0
- package/dist/agent/channels/slack-setup-page.d.ts.map +1 -0
- package/dist/agent/channels/slack-setup-page.js +13 -0
- package/dist/agent/channels/slack-setup-page.js.map +1 -0
- package/dist/agent/channels/slack-web-api.d.ts +3 -0
- package/dist/agent/channels/slack-web-api.d.ts.map +1 -1
- package/dist/agent/channels/slack-web-api.js +26 -6
- package/dist/agent/channels/slack-web-api.js.map +1 -1
- package/dist/agent/channels/slack.d.ts.map +1 -1
- package/dist/agent/channels/slack.js +11 -3
- package/dist/agent/channels/slack.js.map +1 -1
- package/dist/agent/contributions.d.ts +5 -0
- package/dist/agent/contributions.d.ts.map +1 -1
- package/dist/agent/contributions.js +1 -0
- package/dist/agent/contributions.js.map +1 -1
- package/dist/agent/docs-generated.js +1 -1
- package/dist/agent/docs-generated.js.map +1 -1
- package/dist/agent/ops.d.ts +1 -1
- package/dist/agent/ops.d.ts.map +1 -1
- package/dist/agent/ops.js +29 -47
- package/dist/agent/ops.js.map +1 -1
- package/dist/agent/register.d.ts.map +1 -1
- package/dist/agent/register.js +26 -2
- package/dist/agent/register.js.map +1 -1
- package/dist/agent/setup/claim.d.ts +73 -0
- package/dist/agent/setup/claim.d.ts.map +1 -0
- package/dist/agent/setup/claim.js +298 -0
- package/dist/agent/setup/claim.js.map +1 -0
- package/dist/agent/setup/credentials.d.ts +56 -0
- package/dist/agent/setup/credentials.d.ts.map +1 -0
- package/dist/agent/setup/credentials.js +214 -0
- package/dist/agent/setup/credentials.js.map +1 -0
- package/dist/agent/setup/eve-credentials.d.ts +22 -0
- package/dist/agent/setup/eve-credentials.d.ts.map +1 -0
- package/dist/agent/setup/eve-credentials.js +73 -0
- package/dist/agent/setup/eve-credentials.js.map +1 -0
- package/dist/agent/setup/install-state.d.ts +139 -0
- package/dist/agent/setup/install-state.d.ts.map +1 -0
- package/dist/agent/setup/install-state.js +196 -0
- package/dist/agent/setup/install-state.js.map +1 -0
- package/dist/agent/setup/page.d.ts +29 -0
- package/dist/agent/setup/page.d.ts.map +1 -0
- package/dist/agent/setup/page.js +460 -0
- package/dist/agent/setup/page.js.map +1 -0
- package/dist/agent/setup/slack-api-setup.d.ts +62 -0
- package/dist/agent/setup/slack-api-setup.d.ts.map +1 -0
- package/dist/agent/setup/slack-api-setup.js +138 -0
- package/dist/agent/setup/slack-api-setup.js.map +1 -0
- package/dist/agent/status.d.ts +4 -0
- package/dist/agent/status.d.ts.map +1 -0
- package/dist/agent/status.js +79 -0
- package/dist/agent/status.js.map +1 -0
- package/package.json +36 -3
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// WHERE THE SLACK BUNDLE'S TWO SECRETS COME FROM (OPE-2231) — the signing secret that verifies
|
|
2
|
+
// Slack's requests, and the bot token that signs ours.
|
|
3
|
+
//
|
|
4
|
+
// Before this module there was one answer: `process.env.SLACK_SIGNING_SECRET` and
|
|
5
|
+
// `process.env.SLACK_BOT_TOKEN`, read inline at each call site. A `/setup` run now captures both
|
|
6
|
+
// into the credential store, so there are two homes and an order is needed.
|
|
7
|
+
//
|
|
8
|
+
// 🔴 THE ORDER IS ENV-FIRST, AND IT IS THE OPPOSITE OF `getOperatorSecret`'s. Core's operator-secret
|
|
9
|
+
// resolver puts the db store first and treats env as a fallback on its way out (OPE-408: "db-over-env
|
|
10
|
+
// is the DEFAULT home for secrets"). This module inverts that, deliberately, on the OPE-2231 ruling
|
|
11
|
+
// ("env as fallback (env wins if set, for existing installs)"). The reason is the migration, not a
|
|
12
|
+
// disagreement about where secrets belong:
|
|
13
|
+
//
|
|
14
|
+
// • EVERY deployment that exists today holds these two in env and works. This change must not be
|
|
15
|
+
// able to alter which Slack app a working deployment speaks as. Store-first could: a `/setup` run
|
|
16
|
+
// somebody started and abandoned, or a second workspace's install, would silently take over a
|
|
17
|
+
// live deployment's Slack identity at the next cold start, with a 401 loop as the symptom and
|
|
18
|
+
// nothing in the logs saying a different secret had been picked.
|
|
19
|
+
// • Env-first cannot do that. A deployment with the env vars set resolves to exactly the value it
|
|
20
|
+
// resolved to before this commit, every time. It reads the store AT MOST ONCE PER NAME PER
|
|
21
|
+
// PROCESS — only to decide whether to warn that the env var is shadowing something — and never
|
|
22
|
+
// again, so the per-request hot path for an existing install costs the same property read it
|
|
23
|
+
// always did.
|
|
24
|
+
// • A fresh hosted install sets neither env var (the deploy button asks for one value, the
|
|
25
|
+
// credential encryption key), so it reads the store, which is where `/setup` put them.
|
|
26
|
+
//
|
|
27
|
+
// The cost, stated plainly: an operator who runs `/setup` on a deployment that still has the env vars
|
|
28
|
+
// set will find the stored credentials ignored. {@link slackCredentialSourceNotice} is what makes that
|
|
29
|
+
// visible instead of mysterious — the page says it before the run, and the resolver logs it once.
|
|
30
|
+
//
|
|
31
|
+
// MEMOISED PER PROCESS WITH A SHORT TTL, store reads only. The signature check runs on every inbound
|
|
32
|
+
// Slack request and the bot token on every outbound call, so a vault round trip per request is not
|
|
33
|
+
// acceptable.
|
|
34
|
+
//
|
|
35
|
+
// 🔴 THE TTL IS NOT A TIDINESS CHOICE — A LIFETIME MEMO WAS A CORRECTNESS BUG. An earlier draft cached
|
|
36
|
+
// a store hit for the process's lifetime, on the same reasoning core's `self-secrets.ts` gives for its
|
|
37
|
+
// own memo. That reasoning does not transfer, because `self-secrets` holds values this deployment
|
|
38
|
+
// minted for ITSELF and only rotation changes: nothing re-mints them behind a warm process. These two
|
|
39
|
+
// DO change behind one — `reopen_slack_setup` followed by a second `/setup` run replaces both, by
|
|
40
|
+
// design. With a lifetime memo every warm instance kept serving the OLD signing secret and the OLD
|
|
41
|
+
// bot token after that second install, so the deployment verified the previous app's signature and
|
|
42
|
+
// posted with the previous app's token until every instance happened to recycle: a 401 loop that
|
|
43
|
+
// healed at different times on different instances, with nothing in the logs naming the cause.
|
|
44
|
+
//
|
|
45
|
+
// A TTL bounds that to {@link HIT_TTL_MS} without giving up the per-request cost, which is what the
|
|
46
|
+
// memo was for. It is deliberately not a cache invalidated by the reopen path: a reopen runs in ONE
|
|
47
|
+
// process and these memos live in all of them, so nothing a reopen does can reach them. Only time can. An env hit is not memoised because it is already a
|
|
48
|
+
// property read. A MISS is remembered only BRIEFLY ({@link MISS_TTL_MS}) — long enough that an
|
|
49
|
+
// unauthenticated POST to the public Slack route cannot drive one database query per request, short
|
|
50
|
+
// enough that a process which booted before `/setup` finished still picks the credentials up without a
|
|
51
|
+
// restart.
|
|
52
|
+
import { encryptedKvVault } from "@open-everywhere/core/agent/adapter-api.js";
|
|
53
|
+
import { SLACK_BOT_TOKEN_REF, SLACK_SIGNING_SECRET_REF } from "./install-state.js";
|
|
54
|
+
/** The env var holding the app's request-signing secret — the pre-OPE-2231 home, still first. */
|
|
55
|
+
const SLACK_SIGNING_SECRET_ENV = "SLACK_SIGNING_SECRET";
|
|
56
|
+
/** The env var holding the workspace bot token — the pre-OPE-2231 home, still first. */
|
|
57
|
+
const SLACK_BOT_TOKEN_ENV = "SLACK_BOT_TOKEN";
|
|
58
|
+
/**
|
|
59
|
+
* How long a resolved STORE value is reused. Bounds how long a warm instance can keep serving the
|
|
60
|
+
* credentials a `reopen_slack_setup` + second `/setup` run replaced — see the header. Thirty seconds
|
|
61
|
+
* keeps the per-request cost at zero in the steady state while making a reinstall take effect on its
|
|
62
|
+
* own, which a lifetime memo never did.
|
|
63
|
+
*/
|
|
64
|
+
const HIT_TTL_MS = 30_000;
|
|
65
|
+
/** A resolved store value and when it stops being trusted. */
|
|
66
|
+
const memo = new Map();
|
|
67
|
+
/**
|
|
68
|
+
* How long a store MISS is remembered. A miss cannot be cached for the process's life — a process
|
|
69
|
+
* that boots while `/setup` is still running has to pick the credentials up without a restart — but it
|
|
70
|
+
* must not be re-asked on every request either.
|
|
71
|
+
*
|
|
72
|
+
* 🔴 WHY A MISS NEEDS CACHING AT ALL. The signature check runs through this resolver on every inbound
|
|
73
|
+
* request to the PUBLIC Slack webhook route. On a deployment with neither an env var nor a stored value
|
|
74
|
+
* — which is every deployment for the whole window `/setup` is open — an uncached miss meant one
|
|
75
|
+
* `stored_credential` SELECT per request, before the 401. Anyone who knew the host could drive one
|
|
76
|
+
* database query per request by POSTing garbage. Thirty seconds bounds that at two queries a minute
|
|
77
|
+
* while keeping "a mid-`/setup` process picks them up without a restart" true, one TTL later.
|
|
78
|
+
*/
|
|
79
|
+
const MISS_TTL_MS = 30_000;
|
|
80
|
+
/** When each ref's miss stops being trusted, epoch ms. Absent ⇒ no remembered miss. */
|
|
81
|
+
const missUntil = new Map();
|
|
82
|
+
/** Test seam: forget memoised store resolutions, hits and misses alike. */
|
|
83
|
+
export function resetSlackCredentialMemo() {
|
|
84
|
+
memo.clear();
|
|
85
|
+
missUntil.clear();
|
|
86
|
+
}
|
|
87
|
+
const noticed = new Set();
|
|
88
|
+
/** The names whose env-shadows-store CHECK has run in this process, whether or not it warned. See the
|
|
89
|
+
* resolver: keying the one-shot off `noticed` alone made the no-stored-value case re-read the vault
|
|
90
|
+
* on every single request. */
|
|
91
|
+
const shadowChecked = new Set();
|
|
92
|
+
/** Test seam: forget which names have already logged the shadowing notice, and which have been
|
|
93
|
+
* checked at all. */
|
|
94
|
+
export function resetSlackCredentialNotices() {
|
|
95
|
+
noticed.clear();
|
|
96
|
+
shadowChecked.clear();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The sentence an operator needs when the env var is set AND the store holds a value for the same
|
|
100
|
+
* secret: the env var is winning and the stored value is inert. `undefined` when there is nothing to
|
|
101
|
+
* say. The `/setup` page renders this before a run so the operator learns it BEFORE capturing
|
|
102
|
+
* credentials the deployment will ignore, and {@link resolveSlackSecret} logs it once per name.
|
|
103
|
+
*/
|
|
104
|
+
function slackCredentialSourceNotice(envName) {
|
|
105
|
+
return (`[open-everywhere:slack-setup] ${envName} is set in the environment, so this deployment uses the environment value and ` +
|
|
106
|
+
`IGNORES the one in the credential store. That is the documented order (OPE-2231): env wins, so landing this change cannot ` +
|
|
107
|
+
`alter which Slack app an existing deployment speaks as. To use the stored value instead, unset ${envName} and redeploy.`);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve one Slack secret: env if set, else the credential store, else `undefined`.
|
|
111
|
+
*
|
|
112
|
+
* `undefined` is a real answer, not an error — it means this deployment has not been through `/setup`
|
|
113
|
+
* and holds no env var either. Each caller fails in its own way (the front route 401s; an outbound
|
|
114
|
+
* call throws the message eve's primitive always threw), which is what keeps this function free of
|
|
115
|
+
* any opinion about what a missing credential should do.
|
|
116
|
+
*/
|
|
117
|
+
export async function resolveSlackSecret(envName, fromEnv, ref, deps = {}) {
|
|
118
|
+
// 🔴 THE ENV VALUE IS PASSED IN, NOT READ HERE, and that is a gate's requirement rather than a
|
|
119
|
+
// style choice. `lint:env-docs` resolves `env[SOME_CONST]` through the const so every variable the
|
|
120
|
+
// runtime reads can be named in `scripts/env-read-allowlist.ts`; a read through a PARAMETER
|
|
121
|
+
// (`env[envName]`) resolves to nothing, so the gate cannot tell which variable it reaches and
|
|
122
|
+
// refuses the site. The two public helpers below each do their own read with a module-level const
|
|
123
|
+
// as the key, which keeps both names visible to the allowlist — and leaves this function with no
|
|
124
|
+
// environment access at all.
|
|
125
|
+
// An injected vault means a test driving a specific store state — bypass the memo entirely, so a
|
|
126
|
+
// test can rewrite a row and see the effect without reaching for a reset.
|
|
127
|
+
const vault = deps.vault;
|
|
128
|
+
const now = (deps.now ?? Date.now)();
|
|
129
|
+
// The memo applies on the live path, and on an injected vault only when a test opts in — see
|
|
130
|
+
// `deps.memo`.
|
|
131
|
+
const memoApplies = vault === undefined || deps.memo === true;
|
|
132
|
+
const cached = memoApplies ? memo.get(ref) : undefined;
|
|
133
|
+
const memoised = cached !== undefined && cached.until > now ? cached.value : undefined;
|
|
134
|
+
if (fromEnv.length > 0) {
|
|
135
|
+
// Only say the env is shadowing a stored value when one is ACTUALLY stored — the notice is for
|
|
136
|
+
// the operator who ran `/setup` and is about to wonder why nothing changed, not for every
|
|
137
|
+
// deployment that has always used env.
|
|
138
|
+
//
|
|
139
|
+
// 🔴 THE CHECK IS MARKED DONE WHETHER OR NOT IT WARNED, and that is the whole reason this is a
|
|
140
|
+
// separate set from `noticed`. Keying the guard off "have I warned?" meant a deployment with the
|
|
141
|
+
// env var set and NOTHING stored — the common case, every pre-OPE-2231 install — never recorded
|
|
142
|
+
// that it had looked, so it re-read the vault on EVERY resolution: one store round trip per
|
|
143
|
+
// inbound Slack request, for an answer that was always going to be the env value. One read per
|
|
144
|
+
// name per process, then never again.
|
|
145
|
+
if (!shadowChecked.has(envName)) {
|
|
146
|
+
shadowChecked.add(envName);
|
|
147
|
+
const stored = memoised ?? (await readStored(ref, vault)); // best-effort; a store failure says nothing
|
|
148
|
+
if (stored !== undefined) {
|
|
149
|
+
noticed.add(envName);
|
|
150
|
+
console.warn(slackCredentialSourceNotice(envName));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { value: fromEnv, source: "env" };
|
|
154
|
+
}
|
|
155
|
+
if (memoised !== undefined)
|
|
156
|
+
return { value: memoised, source: "store" };
|
|
157
|
+
// A remembered MISS, still inside its TTL — answer without touching the store. Skipped entirely for
|
|
158
|
+
// an injected vault (a test driving a specific store state).
|
|
159
|
+
if (memoApplies) {
|
|
160
|
+
const until = missUntil.get(ref);
|
|
161
|
+
if (until !== undefined && until > now)
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const stored = await readStored(ref, vault);
|
|
165
|
+
if (stored === undefined) {
|
|
166
|
+
if (memoApplies)
|
|
167
|
+
missUntil.set(ref, now + MISS_TTL_MS);
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
if (memoApplies) {
|
|
171
|
+
memo.set(ref, { value: stored, until: now + HIT_TTL_MS });
|
|
172
|
+
missUntil.delete(ref);
|
|
173
|
+
}
|
|
174
|
+
return { value: stored, source: "store" };
|
|
175
|
+
}
|
|
176
|
+
/** Read one ref out of the vault, fail-soft to `undefined`. A disarmed credential key, an
|
|
177
|
+
* unreachable store and an absent row are one answer here on purpose: all three mean "the store has
|
|
178
|
+
* nothing for me", and the caller's next step is the same for each. */
|
|
179
|
+
async function readStored(ref, injected) {
|
|
180
|
+
try {
|
|
181
|
+
const value = await (injected ?? encryptedKvVault()).read(ref);
|
|
182
|
+
return value === null || value.length === 0 ? undefined : value;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** The app's request-signing secret, or `undefined` when this deployment holds none. */
|
|
189
|
+
export async function slackSigningSecret(deps = {}) {
|
|
190
|
+
const env = deps.env ?? process.env;
|
|
191
|
+
const fromEnv = (env[SLACK_SIGNING_SECRET_ENV] ?? "").trim();
|
|
192
|
+
return (await resolveSlackSecret(SLACK_SIGNING_SECRET_ENV, fromEnv, SLACK_SIGNING_SECRET_REF, deps))?.value;
|
|
193
|
+
}
|
|
194
|
+
/** The workspace bot token, or `undefined` when this deployment holds none. */
|
|
195
|
+
export async function slackBotToken(deps = {}) {
|
|
196
|
+
const env = deps.env ?? process.env;
|
|
197
|
+
const fromEnv = (env[SLACK_BOT_TOKEN_ENV] ?? "").trim();
|
|
198
|
+
return (await resolveSlackSecret(SLACK_BOT_TOKEN_ENV, fromEnv, SLACK_BOT_TOKEN_REF, deps))?.value;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Is `name`'s env var set, for the ONE caller that needs to ask without resolving — the `/setup`
|
|
202
|
+
* page's "this env var will shadow what you are about to store" notice.
|
|
203
|
+
*
|
|
204
|
+
* Unrolled rather than looped over the two names for the same reason {@link resolveSlackSecret} takes
|
|
205
|
+
* its env value as a parameter: the gate resolves a const key and nothing else.
|
|
206
|
+
*/
|
|
207
|
+
export function slackSecretEnvPresent(deps = {}) {
|
|
208
|
+
const env = deps.env ?? process.env;
|
|
209
|
+
return {
|
|
210
|
+
signingSecret: (env[SLACK_SIGNING_SECRET_ENV] ?? "").trim().length > 0,
|
|
211
|
+
botToken: (env[SLACK_BOT_TOKEN_ENV] ?? "").trim().length > 0,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../../agent/setup/credentials.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eve's inbound verifier. THROWS to reject — eve catches, logs, and answers 401, so a thrown error
|
|
3
|
+
* here is the refusal. It must never return a verdict as a value.
|
|
4
|
+
*
|
|
5
|
+
* The message names no secret and no header value: a verification failure is one of three things to
|
|
6
|
+
* an attacker (no secret configured, bad signature, stale timestamp) and telling them which is free
|
|
7
|
+
* information. The three are distinguished in the thrown text only to the extent an OPERATOR reading
|
|
8
|
+
* their own logs needs, which is why it names the CONDITION and never the input.
|
|
9
|
+
*/
|
|
10
|
+
export declare function verifySlackInboundForEve(request: Request, body: string): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Eve's outbound bot-token provider. Throws eve's OWN message when none resolves, byte for byte, so a
|
|
13
|
+
* misconfigured deployment produces the error every runbook and every existing test already names.
|
|
14
|
+
*/
|
|
15
|
+
export declare function slackBotTokenForEve(): Promise<string>;
|
|
16
|
+
/** The `credentials` block for `slackChannel(...)`. One object, so the two halves cannot be wired
|
|
17
|
+
* independently and leave eve resolving one of them from the environment. */
|
|
18
|
+
export declare const SLACK_EVE_CREDENTIALS: {
|
|
19
|
+
readonly webhookVerifier: typeof verifySlackInboundForEve;
|
|
20
|
+
readonly botToken: typeof slackBotTokenForEve;
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=eve-credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eve-credentials.d.ts","sourceRoot":"","sources":["../../../agent/setup/eve-credentials.ts"],"names":[],"mappings":"AAiCA;;;;;;;;GAQG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiB5F;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC,CAI3D;AAED;8EAC8E;AAC9E,eAAO,MAAM,qBAAqB;aAChC,eAAe;aACf,QAAQ;CACA,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// WHAT EVE IS TOLD ABOUT THIS DEPLOYMENT'S SLACK CREDENTIALS (OPE-2231).
|
|
2
|
+
//
|
|
3
|
+
// 🔴 WHY THIS FILE HAD TO EXIST, AND THE DEFECT IT CLOSES. OE's front route is not the only thing
|
|
4
|
+
// that verifies a Slack signature. It verifies, then FORWARDS to eve's own `slackChannel` POST
|
|
5
|
+
// handler, and eve verifies AGAIN — `verifyInbound` in eve's `slackChannel.js`:
|
|
6
|
+
//
|
|
7
|
+
// signingSecret: credentials?.signingSecret ?? (credentials?.webhookVerifier ? undefined
|
|
8
|
+
// : process.env.SLACK_SIGNING_SECRET)
|
|
9
|
+
//
|
|
10
|
+
// `channels/slack.ts` passed NO `credentials`, so eve resolved the secret from `process.env` and
|
|
11
|
+
// nowhere else. Moving OE's own read to the credential store therefore produced a deployment where
|
|
12
|
+
// OE's half of the verification passed and EVE's half 401'd: a `/setup`-configured deployment with no
|
|
13
|
+
// env vars — the exact case the page exists to produce — would have acked nothing, woken nothing, and
|
|
14
|
+
// said only `unauthorized`. Eve's outbound path had the same shape: `buildSlackBinding({ botToken:
|
|
15
|
+
// credentials?.botToken })` falls back to `resolveSlackBotToken`, which throws
|
|
16
|
+
// `SLACK_BOT_TOKEN is required.`
|
|
17
|
+
//
|
|
18
|
+
// So the migration is only complete when EVE resolves through the same seam OE does. That is what
|
|
19
|
+
// these two arms are. Both are FUNCTIONS, not values, because the resolution is async and because a
|
|
20
|
+
// value read at module load would pin whatever was true at boot — before `/setup` ran.
|
|
21
|
+
//
|
|
22
|
+
// `webhookVerifier` rather than `signingSecret`: eve takes `signingSecret` as a plain string, which
|
|
23
|
+
// means reading it once at module load. Supplying the verifier instead also makes eve SKIP its
|
|
24
|
+
// `process.env.SLACK_SIGNING_SECRET` fallback entirely (see the expression above), so there is no path
|
|
25
|
+
// left on which eve and OE can disagree about which secret this request is held to.
|
|
26
|
+
import { slackSignatureMatches, SLACK_SIGNATURE_HEADER, SLACK_TIMESTAMP_HEADER } from "../channels/slack-signature.js";
|
|
27
|
+
import { slackBotToken, slackSigningSecret } from "./credentials.js";
|
|
28
|
+
/** How far a request's signed timestamp may be from now. Slack's own recommendation, and the same
|
|
29
|
+
* bound OE's front route applies — the two verifications must not differ on skew either. */
|
|
30
|
+
const MAX_SKEW_SECONDS = 60 * 5;
|
|
31
|
+
/**
|
|
32
|
+
* Eve's inbound verifier. THROWS to reject — eve catches, logs, and answers 401, so a thrown error
|
|
33
|
+
* here is the refusal. It must never return a verdict as a value.
|
|
34
|
+
*
|
|
35
|
+
* The message names no secret and no header value: a verification failure is one of three things to
|
|
36
|
+
* an attacker (no secret configured, bad signature, stale timestamp) and telling them which is free
|
|
37
|
+
* information. The three are distinguished in the thrown text only to the extent an OPERATOR reading
|
|
38
|
+
* their own logs needs, which is why it names the CONDITION and never the input.
|
|
39
|
+
*/
|
|
40
|
+
export async function verifySlackInboundForEve(request, body) {
|
|
41
|
+
const secret = await slackSigningSecret();
|
|
42
|
+
if (!secret) {
|
|
43
|
+
throw new Error("SLACK_SIGNING_SECRET is not set and no signing secret is stored — this deployment cannot verify a Slack request. " +
|
|
44
|
+
"Run /setup, or set SLACK_SIGNING_SECRET.");
|
|
45
|
+
}
|
|
46
|
+
const timestamp = request.headers.get(SLACK_TIMESTAMP_HEADER);
|
|
47
|
+
const signature = request.headers.get(SLACK_SIGNATURE_HEADER);
|
|
48
|
+
if (!timestamp || !signature)
|
|
49
|
+
throw new Error("slack inbound: the request carries no signature headers.");
|
|
50
|
+
// The timestamp is INSIDE the signed basestring, which is what makes a skew check meaningful here.
|
|
51
|
+
const skew = Math.abs(Date.now() / 1000 - Number(timestamp));
|
|
52
|
+
if (!Number.isFinite(skew) || skew > MAX_SKEW_SECONDS)
|
|
53
|
+
throw new Error("slack inbound: the signed timestamp is outside the accepted window.");
|
|
54
|
+
if (!slackSignatureMatches(secret, timestamp, body, signature))
|
|
55
|
+
throw new Error("slack inbound: the signature does not match.");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Eve's outbound bot-token provider. Throws eve's OWN message when none resolves, byte for byte, so a
|
|
59
|
+
* misconfigured deployment produces the error every runbook and every existing test already names.
|
|
60
|
+
*/
|
|
61
|
+
export async function slackBotTokenForEve() {
|
|
62
|
+
const token = await slackBotToken();
|
|
63
|
+
if (!token)
|
|
64
|
+
throw new Error("SLACK_BOT_TOKEN is required.");
|
|
65
|
+
return token;
|
|
66
|
+
}
|
|
67
|
+
/** The `credentials` block for `slackChannel(...)`. One object, so the two halves cannot be wired
|
|
68
|
+
* independently and leave eve resolving one of them from the environment. */
|
|
69
|
+
export const SLACK_EVE_CREDENTIALS = {
|
|
70
|
+
webhookVerifier: verifySlackInboundForEve,
|
|
71
|
+
botToken: slackBotTokenForEve,
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=eve-credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eve-credentials.js","sourceRoot":"","sources":["../../../agent/setup/eve-credentials.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,EAAE;AACF,kGAAkG;AAClG,+FAA+F;AAC/F,gFAAgF;AAChF,EAAE;AACF,6FAA6F;AAC7F,oHAAoH;AACpH,EAAE;AACF,iGAAiG;AACjG,mGAAmG;AACnG,sGAAsG;AACtG,sGAAsG;AACtG,mGAAmG;AACnG,+EAA+E;AAC/E,iCAAiC;AACjC,EAAE;AACF,kGAAkG;AAClG,oGAAoG;AACpG,uFAAuF;AACvF,EAAE;AACF,oGAAoG;AACpG,+FAA+F;AAC/F,uGAAuG;AACvG,oFAAoF;AAEpF,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACvH,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAErE;6FAC6F;AAC7F,MAAM,gBAAgB,GAAG,EAAE,GAAG,CAAC,CAAC;AAEhC;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,OAAgB,EAAE,IAAY;IAC3E,MAAM,MAAM,GAAG,MAAM,kBAAkB,EAAE,CAAC;IAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,mHAAmH;YACjH,0CAA0C,CAC7C,CAAC;IACJ,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;IAC9D,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;IAC9D,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAE1G,mGAAmG;IACnG,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;IAC7D,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,gBAAgB;QAAE,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAC;IAE9I,IAAI,CAAC,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;AAClI,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,MAAM,KAAK,GAAG,MAAM,aAAa,EAAE,CAAC;IACpC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAC5D,OAAO,KAAK,CAAC;AACf,CAAC;AAED;8EAC8E;AAC9E,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,eAAe,EAAE,wBAAwB;IACzC,QAAQ,EAAE,mBAAmB;CACrB,CAAC"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type EncryptedKvVault } from "@open-everywhere/core/agent/adapter-api.js";
|
|
2
|
+
/** The ref holding the app's OAuth client secret. SECRET — never rendered, never logged. */
|
|
3
|
+
export declare const SLACK_CLIENT_SECRET_REF = "slack-app:client_secret";
|
|
4
|
+
/** The ref holding the app's request-signing secret. SECRET. */
|
|
5
|
+
export declare const SLACK_SIGNING_SECRET_REF = "slack-app:signing_secret";
|
|
6
|
+
/** The ref holding the workspace bot token (`xoxb-…`). SECRET. */
|
|
7
|
+
export declare const SLACK_BOT_TOKEN_REF = "slack-app:bot_token";
|
|
8
|
+
/**
|
|
9
|
+
* The ref holding a PENDING app's signing secret, between the create and the OAuth callback.
|
|
10
|
+
*
|
|
11
|
+
* 🔴 WHY A SECOND REF AND NOT JUST {@link SLACK_SIGNING_SECRET_REF}. That ref is LIVE — it is what
|
|
12
|
+
* verifies every inbound Slack request. Writing the new app's secret straight into it at the create
|
|
13
|
+
* step would swap the running deployment's signing key before the install existed, so an ABANDONED
|
|
14
|
+
* run (wrong workspace, expired token, closed tab) left an installed deployment unable to verify its
|
|
15
|
+
* own workspace's requests: every cold start from then on 401s, staggered by the per-process memo, with
|
|
16
|
+
* nothing in the logs saying a different secret had been picked. The callback promotes this to the live
|
|
17
|
+
* ref immediately before it records the install, so the live secret changes only when a run COMPLETES.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SLACK_PENDING_SIGNING_SECRET_REF = "slack-app:pending_signing_secret";
|
|
20
|
+
/** The ref holding the HMAC key for the claim cookie. SECRET, and minted here — see claim.ts. */
|
|
21
|
+
export declare const SLACK_COOKIE_KEY_REF = "slack-app:cookie_key";
|
|
22
|
+
/** The ref holding the one-time setup code a human reads out of the deployment's logs. SECRET. */
|
|
23
|
+
export declare const SLACK_SETUP_CODE_REF = "slack-app:setup_code";
|
|
24
|
+
/** The ref holding {@link SlackInstallState} as JSON. The ONLY ref here with no secret in it. */
|
|
25
|
+
export declare const SLACK_INSTALL_REF = "slack-app:install";
|
|
26
|
+
/** The ref holding the PENDING app record (`agent/setup/page.ts` writes and spends it). Declared here
|
|
27
|
+
* so one module owns the ref vocabulary — `reopenSlackSetup` has to clear it, and a ref it could not
|
|
28
|
+
* name is a ref it would leave behind. */
|
|
29
|
+
export declare const SLACK_PENDING_REF = "slack-app:pending";
|
|
30
|
+
/**
|
|
31
|
+
* What a completed `/setup` run recorded. IDS AND TIMESTAMPS ONLY — see the header on why.
|
|
32
|
+
*
|
|
33
|
+
* `v` guards a future shape change: an unreadable or wrong-version record reads as NO install, which
|
|
34
|
+
* fails in the safe direction (the page reopens; it does not grant anything).
|
|
35
|
+
*/
|
|
36
|
+
export interface SlackInstallState {
|
|
37
|
+
readonly v: 1;
|
|
38
|
+
/** The Slack app id (`A…`) `apps.manifest.create` returned. */
|
|
39
|
+
readonly appId: string;
|
|
40
|
+
/** The app's OAuth client id. Public by Slack's own design (it rides the authorize URL). */
|
|
41
|
+
readonly clientId: string;
|
|
42
|
+
/** The workspace (team) id (`T…`) the OAuth install landed in. */
|
|
43
|
+
readonly teamId: string;
|
|
44
|
+
/** The workspace's display name, when `oauth.v2.access` reported one. */
|
|
45
|
+
readonly teamName?: string;
|
|
46
|
+
/** The installing member's id (`U…`) — `authed_user.id` on the OAuth response. */
|
|
47
|
+
readonly installerUserId: string;
|
|
48
|
+
/** ISO-8601 instant the install completed. */
|
|
49
|
+
readonly installedAt: string;
|
|
50
|
+
}
|
|
51
|
+
/** Injectable seams for tests — the live default is the real encrypted KV vault. */
|
|
52
|
+
export interface SlackInstallStateDeps {
|
|
53
|
+
readonly vault?: EncryptedKvVault;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The recorded Slack install, or `undefined` when this deployment has none.
|
|
57
|
+
*
|
|
58
|
+
* 🔴 THIS IS THE SEAM OPE-2230's STATUS ENDPOINT READS, and the only one it should: it returns the
|
|
59
|
+
* whole non-secret truth about whether Slack is installed and where, and it can return nothing
|
|
60
|
+
* else. Call it; do not read `slack-app:install` directly, or the status endpoint and the claim lock
|
|
61
|
+
* end up with two readings of one record.
|
|
62
|
+
*
|
|
63
|
+
* FAIL-SOFT TO "NOT INSTALLED". An unreachable vault, a disarmed credential key, a malformed row or
|
|
64
|
+
* a record of an unknown version all answer `undefined`. For a STATUS READER that is the safe
|
|
65
|
+
* direction and the honest one: it reports Slack as not installed, which is true as far as this
|
|
66
|
+
* process can tell, and it cannot report an install that is not there.
|
|
67
|
+
*
|
|
68
|
+
* 🔴 THE CLAIM LOCK MUST NOT USE THIS FUNCTION. For the lock, the same collapse is a hole — a store
|
|
69
|
+
* fault would reopen `/setup`. The lock reads {@link readInstallRecord} instead, which tells the
|
|
70
|
+
* three cases apart. See its doc comment.
|
|
71
|
+
*/
|
|
72
|
+
export declare function slackInstallState(deps?: SlackInstallStateDeps): Promise<SlackInstallState | undefined>;
|
|
73
|
+
/**
|
|
74
|
+
* What a read of the install record actually found — THREE outcomes, not two.
|
|
75
|
+
*
|
|
76
|
+
* 🔴 WHY THE LOCK NEEDS THIS AND THE STATUS SEAM DOES NOT. {@link slackInstallState} collapses every
|
|
77
|
+
* unreadable case into `undefined`, which is right for a status endpoint: "no install I can see" is
|
|
78
|
+
* the honest answer whether the store is unreachable or the row is absent, and it cannot report an
|
|
79
|
+
* install that is not there. For the LOCK that collapse is a hole. The lock asks the opposite
|
|
80
|
+
* question — "may this page serve?" — and `undefined` meaning both "nothing stored" and "I could not
|
|
81
|
+
* tell" makes a store fault, a malformed row or a row from a future version REOPEN `/setup`. The
|
|
82
|
+
* setup code is in the deployment's old log lines from before the install, so a reopen is a window
|
|
83
|
+
* for whoever read them.
|
|
84
|
+
*
|
|
85
|
+
* So the lock distinguishes: `absent` is the ONLY outcome that opens the page. `unreadable` covers a
|
|
86
|
+
* throw, a non-JSON row, and a row whose `v` this build does not know — all of which mean "something
|
|
87
|
+
* IS stored and I cannot read it", which is never a reason to grant. The cost of that is a setup page
|
|
88
|
+
* that is briefly unavailable during a store outage, recoverable by a retry or the control call; the
|
|
89
|
+
* cost of the other direction is not recoverable at all.
|
|
90
|
+
*/
|
|
91
|
+
export type InstallRecordRead = {
|
|
92
|
+
readonly kind: "present";
|
|
93
|
+
readonly state: SlackInstallState;
|
|
94
|
+
} | {
|
|
95
|
+
readonly kind: "absent";
|
|
96
|
+
readonly state: undefined;
|
|
97
|
+
} | {
|
|
98
|
+
readonly kind: "unreadable";
|
|
99
|
+
readonly state: undefined;
|
|
100
|
+
};
|
|
101
|
+
/** Read the install record, reporting WHICH of the three outcomes happened. The lock reads this. */
|
|
102
|
+
export declare function readInstallRecord(deps?: SlackInstallStateDeps): Promise<InstallRecordRead>;
|
|
103
|
+
/** Parse + validate a stored record. Exported for the suite; `undefined` for anything unreadable. */
|
|
104
|
+
export declare function parseInstallState(raw: string): SlackInstallState | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Record a completed install. THE LAST WRITE OF A SUCCESSFUL `/setup` RUN — it is what closes the
|
|
107
|
+
* page, so every secret the install captured must already be stored when this is called.
|
|
108
|
+
*
|
|
109
|
+
* Throws whatever the vault throws (a disarmed credential key, an unreachable store). The caller
|
|
110
|
+
* must NOT swallow it: a run that stored the credentials but failed to record the install has to
|
|
111
|
+
* say so, because the page is still open and a second run would mint a second Slack app.
|
|
112
|
+
*/
|
|
113
|
+
export declare function recordSlackInstall(state: SlackInstallState, deps?: SlackInstallStateDeps): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* REOPEN the setup page: destroy the install record, keeping every stored credential.
|
|
116
|
+
*
|
|
117
|
+
* WHY THE LIVE CREDENTIALS STAY. A reopen is "let me run setup again", not "forget this workspace".
|
|
118
|
+
* The running deployment is still serving Slack with the stored bot token and signing secret while the
|
|
119
|
+
* admin walks the page, and a reopen that deleted them would take the agent off Slack the moment it
|
|
120
|
+
* was called. A COMPLETED second run replaces them, at its callback; an abandoned one leaves the
|
|
121
|
+
* working install untouched, which is true because the create step stages the new signing secret under
|
|
122
|
+
* {@link SLACK_PENDING_SIGNING_SECRET_REF} rather than writing the live ref.
|
|
123
|
+
*
|
|
124
|
+
* What DOES go, besides the record: the setup code (so the reopened page needs a fresh one — see
|
|
125
|
+
* below) and any stale pending-app material.
|
|
126
|
+
*
|
|
127
|
+
* THROWS on a failure to rotate the setup code or the claim cookie key, and leaves the install record
|
|
128
|
+
* in place when it does — see the body. A reopen that could not rotate must not open the page.
|
|
129
|
+
*
|
|
130
|
+
* NO REOPEN HISTORY SURVIVES THIS, deliberately. The record IS the lock, so reopening means
|
|
131
|
+
* destroying it, and a field recording "reopened at" would have to live on a record that no longer
|
|
132
|
+
* exists. The audit row is where the history lives (`slack.setup_reopened`, audit/action-registry.ts)
|
|
133
|
+
* — a hash-chained row naming the admin who called it, which is a better home for that fact than a
|
|
134
|
+
* field on a row the next install overwrites.
|
|
135
|
+
*
|
|
136
|
+
* Returns true iff a record went (false when the page was already open) — idempotent either way.
|
|
137
|
+
*/
|
|
138
|
+
export declare function reopenSlackSetup(deps?: SlackInstallStateDeps): Promise<boolean>;
|
|
139
|
+
//# sourceMappingURL=install-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-state.d.ts","sourceRoot":"","sources":["../../../agent/setup/install-state.ts"],"names":[],"mappings":"AA2BA,OAAO,EAA6C,KAAK,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AAE9H,4FAA4F;AAC5F,eAAO,MAAM,uBAAuB,4BAA4C,CAAC;AACjF,gEAAgE;AAChE,eAAO,MAAM,wBAAwB,6BAA6C,CAAC;AACnF,kEAAkE;AAClE,eAAO,MAAM,mBAAmB,wBAAwC,CAAC;AACzE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,qCAAqD,CAAC;AACnG,iGAAiG;AACjG,eAAO,MAAM,oBAAoB,yBAAyC,CAAC;AAC3E,kGAAkG;AAClG,eAAO,MAAM,oBAAoB,yBAAyC,CAAC;AAC3E,iGAAiG;AACjG,eAAO,MAAM,iBAAiB,sBAAsC,CAAC;AACrE;;2CAE2C;AAC3C,eAAO,MAAM,iBAAiB,sBAAsC,CAAC;AAErE;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC;IACd,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4FAA4F;IAC5F,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,kFAAkF;IAClF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,8CAA8C;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,oFAAoF;AACpF,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;CACnC;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAEhH;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAA;CAAE,GAC/D;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;CAAE,CAAC;AAE/D,oGAAoG;AACpG,wBAAsB,iBAAiB,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAYpG;AAED,qGAAqG;AACrG,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CA2B5E;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,iBAAiB,EAAE,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAElH;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,OAAO,CAAC,CAiCzF"}
|