@tangle-network/agent-app 0.45.28 → 0.45.30

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 (58) hide show
  1. package/dist/{DesignCanvas-3RRPVPCP.js → DesignCanvas-RSWQCXM3.js} +2 -2
  2. package/dist/{DesignCanvasEditor-KAMMEX7E.js → DesignCanvasEditor-XZMGIST7.js} +4 -4
  3. package/dist/{use-file-mentions-CZ-Ua_sb.d.ts → agent-session-controls-BwImzYKC.d.ts} +32 -1
  4. package/dist/alerting/index.d.ts +178 -0
  5. package/dist/alerting/index.js +9 -0
  6. package/dist/alerting/index.js.map +1 -0
  7. package/dist/assistant/index.d.ts +65 -42
  8. package/dist/assistant/index.js +31 -12
  9. package/dist/assistant/index.js.map +1 -1
  10. package/dist/chat-react/index.d.ts +20 -140
  11. package/dist/chat-react/index.js +15 -133
  12. package/dist/chat-react/index.js.map +1 -1
  13. package/dist/chat-routes/index.js +7 -7
  14. package/dist/chunk-COP2K4LF.js +190 -0
  15. package/dist/chunk-COP2K4LF.js.map +1 -0
  16. package/dist/{chunk-NC2V63EL.js → chunk-FNM7JYME.js} +88 -21
  17. package/dist/chunk-FNM7JYME.js.map +1 -0
  18. package/dist/{chunk-WC43OQMR.js → chunk-IFYJDX3J.js} +867 -1260
  19. package/dist/chunk-IFYJDX3J.js.map +1 -0
  20. package/dist/chunk-L7FHRORY.js +763 -0
  21. package/dist/chunk-L7FHRORY.js.map +1 -0
  22. package/dist/{chunk-4QVG4WJQ.js → chunk-OYTMILRK.js} +10 -3
  23. package/dist/chunk-OYTMILRK.js.map +1 -0
  24. package/dist/{chunk-OYGM5WLD.js → chunk-PURHUAOR.js} +52 -15
  25. package/dist/chunk-PURHUAOR.js.map +1 -0
  26. package/dist/{chunk-AWPK7ZDS.js → chunk-QGOU3VXH.js} +3 -3
  27. package/dist/{chunk-OTLEYHOG.js → chunk-RRH23CGS.js} +24 -4
  28. package/dist/chunk-RRH23CGS.js.map +1 -0
  29. package/dist/{chunk-5FAKBTCK.js → chunk-XDB7IBYE.js} +22 -1
  30. package/dist/chunk-XDB7IBYE.js.map +1 -0
  31. package/dist/design-canvas-react/engine.d.ts +15 -1
  32. package/dist/design-canvas-react/engine.js +6 -2
  33. package/dist/design-canvas-react/index.d.ts +13 -1
  34. package/dist/design-canvas-react/index.js +13 -8
  35. package/dist/design-canvas-react/index.js.map +1 -1
  36. package/dist/design-canvas-react/lazy.js +1 -1
  37. package/dist/preflight/cli.js +2 -1
  38. package/dist/preflight/cli.js.map +1 -1
  39. package/dist/preflight/index.d.ts +35 -2
  40. package/dist/preflight/index.js +6 -3
  41. package/dist/sandbox/index.d.ts +11 -1
  42. package/dist/sandbox/index.js +1 -1
  43. package/dist/teams/index.js +5 -5
  44. package/dist/teams/invitations-api.js +4 -4
  45. package/dist/web-react/index.d.ts +26 -35
  46. package/dist/web-react/index.js +10 -10
  47. package/package.json +19 -3
  48. package/dist/chunk-4QVG4WJQ.js.map +0 -1
  49. package/dist/chunk-5FAKBTCK.js.map +0 -1
  50. package/dist/chunk-E4DHYENH.js +0 -292
  51. package/dist/chunk-E4DHYENH.js.map +0 -1
  52. package/dist/chunk-NC2V63EL.js.map +0 -1
  53. package/dist/chunk-OTLEYHOG.js.map +0 -1
  54. package/dist/chunk-OYGM5WLD.js.map +0 -1
  55. package/dist/chunk-WC43OQMR.js.map +0 -1
  56. /package/dist/{DesignCanvas-3RRPVPCP.js.map → DesignCanvas-RSWQCXM3.js.map} +0 -0
  57. /package/dist/{DesignCanvasEditor-KAMMEX7E.js.map → DesignCanvasEditor-XZMGIST7.js.map} +0 -0
  58. /package/dist/{chunk-AWPK7ZDS.js.map → chunk-QGOU3VXH.js.map} +0 -0
@@ -0,0 +1,190 @@
1
+ // src/alerting/slack.ts
2
+ var SLACK_API = "https://slack.com/api";
3
+ var DEFAULT_TIMEOUT_MS = 1e4;
4
+ var DEFAULT_ATTEMPTS = 3;
5
+ var CREDENTIAL_ERRORS = /* @__PURE__ */ new Set([
6
+ "invalid_auth",
7
+ "not_authed",
8
+ "account_inactive",
9
+ "token_revoked",
10
+ "token_expired",
11
+ "no_permission",
12
+ "missing_scope",
13
+ "ekm_access_denied"
14
+ ]);
15
+ var CHANNEL_ERRORS = /* @__PURE__ */ new Set([
16
+ "channel_not_found",
17
+ "not_in_channel",
18
+ "is_archived",
19
+ "restricted_action",
20
+ "restricted_action_read_only_channel"
21
+ ]);
22
+ function remedyFor(reason, error, channel) {
23
+ switch (reason) {
24
+ case "credential":
25
+ return `Slack rejected the bot token (${error}) \u2014 no alert can arrive until a human mints a new one. Reinstall the Slack app and update SLACK_BOT_TOKEN wherever it is stored.`;
26
+ case "channel":
27
+ return `Slack accepted the token but refused ${channel} (${error}) \u2014 invite the bot to ${channel}, or correct the channel name.`;
28
+ default:
29
+ return `Slack refused the post (${error}).`;
30
+ }
31
+ }
32
+ function classifySlackError(error, channel) {
33
+ const reason = CREDENTIAL_ERRORS.has(error) ? "credential" : CHANNEL_ERRORS.has(error) ? "channel" : "api";
34
+ return { delivered: false, reason, detail: remedyFor(reason, error, channel) };
35
+ }
36
+ function retryDelayMs(response, attempt) {
37
+ const header = Number(response.headers.get("retry-after"));
38
+ if (Number.isFinite(header) && header > 0) return Math.min(header * 1e3, 3e4);
39
+ return Math.min(500 * 2 ** (attempt - 1), 8e3);
40
+ }
41
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
42
+ async function postSlackAlert(options) {
43
+ const token = options.token?.trim();
44
+ const channel = options.channel?.trim();
45
+ const webhookUrl = options.webhookUrl?.trim();
46
+ if (!token && !webhookUrl) {
47
+ return {
48
+ delivered: false,
49
+ reason: "not-configured",
50
+ detail: "no Slack credential configured \u2014 set SLACK_BOT_TOKEN (preferred, verifiable) or SLACK_WEBHOOK_URL to route alerts to Slack"
51
+ };
52
+ }
53
+ if (token && !channel) {
54
+ return {
55
+ delivered: false,
56
+ reason: "not-configured",
57
+ detail: "a Slack bot token is set but no channel is \u2014 set the alert channel (e.g. #infra-alerts)"
58
+ };
59
+ }
60
+ const fetchImpl = options.fetchImpl ?? fetch;
61
+ const sleep = options.sleepImpl ?? defaultSleep;
62
+ const attempts = Math.max(1, options.attempts ?? DEFAULT_ATTEMPTS);
63
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
64
+ const transport = token ? { kind: "token", token, channel } : { kind: "webhook", url: webhookUrl };
65
+ let lastTransient = {
66
+ delivered: false,
67
+ reason: "transport",
68
+ detail: "Slack was never reached"
69
+ };
70
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
71
+ const controller = new AbortController();
72
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
73
+ let response;
74
+ try {
75
+ response = await fetchImpl(requestUrl(transport), {
76
+ method: "POST",
77
+ headers: requestHeaders(transport),
78
+ body: JSON.stringify(requestBody(transport, options.text)),
79
+ signal: controller.signal
80
+ });
81
+ } catch (error) {
82
+ lastTransient = {
83
+ delivered: false,
84
+ reason: "transport",
85
+ detail: `could not reach Slack: ${error instanceof Error ? error.message : String(error)}`
86
+ };
87
+ if (attempt < attempts) await sleep(Math.min(500 * 2 ** (attempt - 1), 8e3));
88
+ continue;
89
+ } finally {
90
+ clearTimeout(timer);
91
+ }
92
+ if (response.status === 429 || response.status >= 500) {
93
+ lastTransient = {
94
+ delivered: false,
95
+ reason: response.status === 429 ? "rate-limited" : "transport",
96
+ detail: `Slack returned ${response.status}`
97
+ };
98
+ if (attempt < attempts) await sleep(retryDelayMs(response, attempt));
99
+ continue;
100
+ }
101
+ return transport.kind === "token" ? await settleTokenResponse(response, transport.channel) : await settleWebhookResponse(response);
102
+ }
103
+ return lastTransient;
104
+ }
105
+ function requestUrl(transport) {
106
+ return transport.kind === "token" ? `${SLACK_API}/chat.postMessage` : transport.url;
107
+ }
108
+ function requestHeaders(transport) {
109
+ const headers = { "Content-Type": "application/json; charset=utf-8" };
110
+ if (transport.kind === "token") headers.Authorization = `Bearer ${transport.token}`;
111
+ return headers;
112
+ }
113
+ function requestBody(transport, text) {
114
+ return transport.kind === "token" ? { channel: transport.channel, text } : { text };
115
+ }
116
+ async function settleTokenResponse(response, channel) {
117
+ let body;
118
+ try {
119
+ body = await response.json();
120
+ } catch {
121
+ return {
122
+ delivered: false,
123
+ reason: "api",
124
+ detail: `Slack returned ${response.status} with an unreadable body`
125
+ };
126
+ }
127
+ if (body.ok === true) {
128
+ return { delivered: true, channel: body.channel ?? channel, ts: body.ts ?? "" };
129
+ }
130
+ return classifySlackError(body.error ?? `http_${response.status}`, channel);
131
+ }
132
+ async function settleWebhookResponse(response) {
133
+ const body = (await response.text().catch(() => "")).trim();
134
+ if (response.ok && body === "ok") {
135
+ return { delivered: true, channel: "(webhook)", ts: "" };
136
+ }
137
+ const error = body || `http_${response.status}`;
138
+ const reason = WEBHOOK_CREDENTIAL_ERRORS.has(error) ? "credential" : WEBHOOK_CHANNEL_ERRORS.has(error) ? "channel" : "api";
139
+ return {
140
+ delivered: false,
141
+ reason,
142
+ detail: reason === "credential" ? `the Slack webhook is revoked (${error}) \u2014 no alert can arrive until a human mints a new one. Prefer replacing it with a bot token, whose liveness can be checked before an alert needs it.` : reason === "channel" ? `Slack refused the webhook's channel (${error}) \u2014 the channel was archived, or the app lost access.` : `Slack refused the webhook post (${error}).`
143
+ };
144
+ }
145
+ var WEBHOOK_CREDENTIAL_ERRORS = /* @__PURE__ */ new Set(["no_service", "no_team", "invalid_token"]);
146
+ var WEBHOOK_CHANNEL_ERRORS = /* @__PURE__ */ new Set(["channel_not_found", "channel_is_archived", "action_prohibited"]);
147
+ async function checkSlackCredential(options) {
148
+ const token = options.token?.trim();
149
+ if (!token) {
150
+ return {
151
+ live: false,
152
+ reason: "not-configured",
153
+ detail: "no Slack bot token configured \u2014 set SLACK_BOT_TOKEN"
154
+ };
155
+ }
156
+ const fetchImpl = options.fetchImpl ?? fetch;
157
+ const controller = new AbortController();
158
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
159
+ try {
160
+ const response = await fetchImpl(`${SLACK_API}/auth.test`, {
161
+ method: "POST",
162
+ headers: { Authorization: `Bearer ${token}` },
163
+ signal: controller.signal
164
+ });
165
+ const body = await response.json();
166
+ if (body.ok === true) {
167
+ return { live: true, team: body.team ?? "unknown", botId: body.bot_id ?? "unknown" };
168
+ }
169
+ const outcome = classifySlackError(body.error ?? `http_${response.status}`, "(auth.test)");
170
+ return {
171
+ live: false,
172
+ reason: outcome.reason === "channel" ? "credential" : outcome.reason,
173
+ detail: outcome.detail
174
+ };
175
+ } catch (error) {
176
+ return {
177
+ live: false,
178
+ reason: "transport",
179
+ detail: `could not reach Slack: ${error instanceof Error ? error.message : String(error)}`
180
+ };
181
+ } finally {
182
+ clearTimeout(timer);
183
+ }
184
+ }
185
+
186
+ export {
187
+ postSlackAlert,
188
+ checkSlackCredential
189
+ };
190
+ //# sourceMappingURL=chunk-COP2K4LF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/alerting/slack.ts"],"sourcesContent":["/**\n * `/alerting` — post an operational alert to Slack, and say honestly when it\n * did not arrive.\n *\n * WHY THIS EXISTS: on 2026-08-06 an audit of every Slack credential the org\n * held found three of four dead — the fleet incoming webhook 404ing\n * (`no_service`), a second company webhook likewise, and the bot token\n * answering `account_inactive`. One ops webhook was still live. Nobody knew\n * which was which until each was tested by hand, and that is the actual\n * problem: the credentials were indistinguishable from where the code stood.\n *\n * This failure has already been paid for once, with numbers: in\n * `agent-dev-container`, a revoked webhook let the CI healthcheck sit dead for\n * 18 days — 2,567 consecutive failures, 27 successes, zero alerts — because\n * fifteen call sites across six workflows posted with no body inspection and\n * then asserted delivery. That repo fixed the reporting (its `post-slack.sh`\n * confirms 2xx AND Slack's literal `ok` body, and fails closed otherwise) and\n * that half of the lesson is theirs, adopted here.\n *\n * What their fix cannot do, and this module can, is answer the question BEFORE\n * an alert needs to fire. A fail-closed post still only discovers a dead\n * credential at the moment a page is lost. That is the second half.\n *\n * Two design consequences, and they are the whole module:\n *\n * 1. **A bot token and `chat.postMessage` is the preferred transport; an\n * incoming webhook is supported because one is usually what you already\n * have.** A token reaches every channel (a webhook is bolted to one), is\n * revocable and rotatable in place, and — the part that matters — is\n * VERIFIABLE: `auth.test` answers whether the credential is alive without\n * posting anything, which is what lets `/preflight` fail a deploy on a dead\n * alerting channel instead of discovering it during an incident. A webhook\n * can only be tested by posting to it, which is why the three dead\n * credentials above went unnoticed. A token is never fallen back FROM: if\n * one is configured and dead, delivering over a webhook instead would hide\n * the very condition worth reporting.\n *\n * 2. **Slack answers `ok:false` under HTTP 200.** `invalid_auth`,\n * `channel_not_found` and `not_in_channel` all arrive as a successful\n * response with a failure inside it, so a status check reads a dead channel\n * as a delivered page. The body is always parsed, and the outcome\n * distinguishes a MISSING credential (configuration absent — not an\n * incident) from a DEAD one (the alerting channel itself is broken — the\n * loudest thing this module can report). A summary that calls those two the\n * same thing is the defect wearing a different hat.\n *\n * The caller decides what to do with a non-delivery; this module never throws\n * on one, because an alerting path that can take down the thing it reports on\n * is worse than the outage it was watching for. It is also never the only\n * channel: a durable record (an issue, an audit row) is the caller's job, and\n * that is what survives the credential going dead again.\n *\n * Server-only: holds a bot token. This subpath must never reach a browser\n * bundle.\n */\n\nconst SLACK_API = 'https://slack.com/api'\nconst DEFAULT_TIMEOUT_MS = 10_000\nconst DEFAULT_ATTEMPTS = 3\n\n/**\n * Why an alert did not reach Slack. The split is by what a human must DO about\n * it, since that is the only distinction a caller can act on.\n */\nexport type SlackFailureReason =\n /** No token or no channel configured. Configuration is absent, nothing is broken. */\n | 'not-configured'\n /**\n * The token is dead — revoked, or its app removed from the workspace. No\n * alert will EVER arrive until a human mints a new one. This is itself an\n * incident and the caller should escalate it on a channel that does not\n * depend on Slack.\n */\n | 'credential'\n /**\n * The credential is alive but cannot post HERE: the channel is wrong,\n * archived, or the bot was never invited to it. One human action fixes it.\n */\n | 'channel'\n /** Slack asked us to slow down. Transient; the alert is worth retrying. */\n | 'rate-limited'\n /** The request never got an answer — network, DNS, timeout. Transient. */\n | 'transport'\n /** Slack refused for some other reason; `detail` carries its error code. */\n | 'api'\n\n/** An alert that did not arrive, and why. Named because the classifiers only ever produce this half. */\nexport interface SlackAlertFailure {\n delivered: false\n reason: SlackFailureReason\n /** One line naming what is wrong and what fixes it. Safe to log; carries no token. */\n detail: string\n}\n\n/** What one `postSlackAlert` call did. Never a bare boolean — the caller pages differently per reason. */\nexport type SlackAlertOutcome =\n | {\n delivered: true\n /** The channel id Slack resolved (not necessarily the name that was passed). */\n channel: string\n /** Slack's message timestamp — the message's identity, for a later thread reply. */\n ts: string\n }\n | SlackAlertFailure\n\n/** Define configuration options for posting an alert message to a Slack channel */\nexport interface SlackAlertOptions {\n /**\n * Slack bot token (`xoxb-…`) with `chat:write`. The PREFERRED transport,\n * because it is the only one whose liveness can be checked before an alert\n * needs it. An empty or absent value is `not-configured`, never an error — a\n * product that has not adopted Slack yet must not fail its alerting path.\n */\n token?: string | undefined\n /**\n * Channel to post to: a name (`#infra-alerts`) or an id (`C01234567`). The\n * bot must be a member; Slack answers `not_in_channel` otherwise. Required\n * with `token`, meaningless with `webhookUrl` (a webhook carries its own\n * channel, fixed when it was created).\n */\n channel?: string | undefined\n /**\n * Slack incoming-webhook URL, used only when no `token` is configured.\n *\n * It works and it needs no setup, which is why it is supported — but it\n * cannot be verified without posting, cannot be pointed at a second channel,\n * and gives back an error token instead of a code. Two of the three webhooks\n * this org has held were found revoked. Treat it as the transport you have,\n * not the one you want.\n *\n * When a `token` is also configured this is IGNORED rather than used as a\n * fallback: a dead token must surface as the incident it is, and quietly\n * succeeding over a second transport is how the last outage stayed invisible.\n */\n webhookUrl?: string | undefined\n /** Message body as Slack mrkdwn. Newlines are preserved. */\n text: string\n /**\n * Attempts for a TRANSIENT failure (429 / 5xx / transport). Default 3. A\n * dead credential or a wrong channel is never retried — the answer will not\n * change, and retrying an auth failure is how a token gets rate-limited.\n */\n attempts?: number\n /** Per-request deadline. Default 10s. */\n timeoutMs?: number\n /** Injection seam for tests; defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n /** Injection seam for tests; defaults to a real delay between retries. */\n sleepImpl?: (ms: number) => Promise<void>\n}\n\n/** Slack error codes that mean the CREDENTIAL is dead, not the request. */\nconst CREDENTIAL_ERRORS = new Set([\n 'invalid_auth',\n 'not_authed',\n 'account_inactive',\n 'token_revoked',\n 'token_expired',\n 'no_permission',\n 'missing_scope',\n 'ekm_access_denied',\n])\n\n/** Slack error codes that mean the credential is fine but this CHANNEL is not reachable. */\nconst CHANNEL_ERRORS = new Set([\n 'channel_not_found',\n 'not_in_channel',\n 'is_archived',\n 'restricted_action',\n 'restricted_action_read_only_channel',\n])\n\nfunction remedyFor(reason: SlackFailureReason, error: string, channel: string): string {\n switch (reason) {\n case 'credential':\n return (\n `Slack rejected the bot token (${error}) — no alert can arrive until a human mints a new one. ` +\n 'Reinstall the Slack app and update SLACK_BOT_TOKEN wherever it is stored.'\n )\n case 'channel':\n return (\n `Slack accepted the token but refused ${channel} (${error}) — ` +\n `invite the bot to ${channel}, or correct the channel name.`\n )\n default:\n return `Slack refused the post (${error}).`\n }\n}\n\nfunction classifySlackError(error: string, channel: string): SlackAlertFailure {\n const reason: SlackFailureReason = CREDENTIAL_ERRORS.has(error)\n ? 'credential'\n : CHANNEL_ERRORS.has(error)\n ? 'channel'\n : 'api'\n return { delivered: false, reason, detail: remedyFor(reason, error, channel) }\n}\n\n/** Slack's `Retry-After` is in SECONDS. Bounded so a hostile header cannot park the caller. */\nfunction retryDelayMs(response: Response, attempt: number): number {\n const header = Number(response.headers.get('retry-after'))\n if (Number.isFinite(header) && header > 0) return Math.min(header * 1000, 30_000)\n return Math.min(500 * 2 ** (attempt - 1), 8_000)\n}\n\nconst defaultSleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))\n\n/**\n * Post one alert to a Slack channel.\n *\n * Never throws and never retries a failure whose answer cannot change. The\n * returned outcome is the whole result — a caller that ignores it has an\n * alerting path it cannot prove works, which is the failure this module was\n * written for.\n */\nexport async function postSlackAlert(options: SlackAlertOptions): Promise<SlackAlertOutcome> {\n const token = options.token?.trim()\n const channel = options.channel?.trim()\n const webhookUrl = options.webhookUrl?.trim()\n\n if (!token && !webhookUrl) {\n return {\n delivered: false,\n reason: 'not-configured',\n detail:\n 'no Slack credential configured — set SLACK_BOT_TOKEN (preferred, verifiable) ' +\n 'or SLACK_WEBHOOK_URL to route alerts to Slack',\n }\n }\n if (token && !channel) {\n return {\n delivered: false,\n reason: 'not-configured',\n detail: 'a Slack bot token is set but no channel is — set the alert channel (e.g. #infra-alerts)',\n }\n }\n\n const fetchImpl = options.fetchImpl ?? fetch\n const sleep = options.sleepImpl ?? defaultSleep\n const attempts = Math.max(1, options.attempts ?? DEFAULT_ATTEMPTS)\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS\n\n // A token is never fallen back FROM. If one is configured and dead, that is\n // the incident, and delivering over a webhook instead would hide exactly the\n // condition this module exists to surface.\n const transport: SlackTransport = token\n ? { kind: 'token', token, channel: channel as string }\n : { kind: 'webhook', url: webhookUrl as string }\n\n let lastTransient: SlackAlertFailure = {\n delivered: false,\n reason: 'transport',\n detail: 'Slack was never reached',\n }\n\n for (let attempt = 1; attempt <= attempts; attempt += 1) {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n let response: Response\n try {\n response = await fetchImpl(requestUrl(transport), {\n method: 'POST',\n headers: requestHeaders(transport),\n body: JSON.stringify(requestBody(transport, options.text)),\n signal: controller.signal,\n })\n } catch (error) {\n lastTransient = {\n delivered: false,\n reason: 'transport',\n detail: `could not reach Slack: ${error instanceof Error ? error.message : String(error)}`,\n }\n if (attempt < attempts) await sleep(Math.min(500 * 2 ** (attempt - 1), 8_000))\n continue\n } finally {\n clearTimeout(timer)\n }\n\n if (response.status === 429 || response.status >= 500) {\n lastTransient = {\n delivered: false,\n reason: response.status === 429 ? 'rate-limited' : 'transport',\n detail: `Slack returned ${response.status}`,\n }\n if (attempt < attempts) await sleep(retryDelayMs(response, attempt))\n continue\n }\n\n // Everything below is a settled answer. BOTH transports put the verdict in\n // the BODY under a 200 — the API as `{\"ok\":false,\"error\":…}`, a webhook as\n // an error token where the literal `ok` should be — so a status check alone\n // reads a revoked credential as a delivered page. That misreading is why\n // this module exists.\n return transport.kind === 'token'\n ? await settleTokenResponse(response, transport.channel)\n : await settleWebhookResponse(response)\n }\n\n return lastTransient\n}\n\ntype SlackTransport =\n | { kind: 'token'; token: string; channel: string }\n | { kind: 'webhook'; url: string }\n\nfunction requestUrl(transport: SlackTransport): string {\n return transport.kind === 'token' ? `${SLACK_API}/chat.postMessage` : transport.url\n}\n\nfunction requestHeaders(transport: SlackTransport): Record<string, string> {\n const headers: Record<string, string> = { 'Content-Type': 'application/json; charset=utf-8' }\n // The token rides a header, never a query string: a URL is logged by proxies\n // and retained in error messages; a header is not. (A webhook URL IS the\n // credential and has no such option — one more reason to prefer a token.)\n if (transport.kind === 'token') headers.Authorization = `Bearer ${transport.token}`\n return headers\n}\n\nfunction requestBody(transport: SlackTransport, text: string): Record<string, string> {\n // A webhook carries its own channel, fixed when it was created; naming one\n // here would be ignored at best and rejected at worst.\n return transport.kind === 'token' ? { channel: transport.channel, text } : { text }\n}\n\nasync function settleTokenResponse(\n response: Response,\n channel: string,\n): Promise<SlackAlertOutcome> {\n let body: { ok?: boolean; error?: string; channel?: string; ts?: string }\n try {\n body = (await response.json()) as typeof body\n } catch {\n return {\n delivered: false,\n reason: 'api',\n detail: `Slack returned ${response.status} with an unreadable body`,\n }\n }\n if (body.ok === true) {\n return { delivered: true, channel: body.channel ?? channel, ts: body.ts ?? '' }\n }\n return classifySlackError(body.error ?? `http_${response.status}`, channel)\n}\n\n/**\n * A webhook confirms delivery with a 2xx AND the literal three-byte body `ok`.\n * A revoked one answers `no_service` / `no_team`, sometimes under a 200. Both\n * halves are required: this is the shape `agent-dev-container` proved against a\n * stand-in webhook, where a healthy, a revoked and a deleted webhook produced\n * byte-identical output under a status-only check.\n */\nasync function settleWebhookResponse(response: Response): Promise<SlackAlertOutcome> {\n const body = (await response.text().catch(() => '')).trim()\n if (response.ok && body === 'ok') {\n // A webhook reports neither the channel it posted to nor a message id.\n return { delivered: true, channel: '(webhook)', ts: '' }\n }\n const error = body || `http_${response.status}`\n const reason: SlackFailureReason = WEBHOOK_CREDENTIAL_ERRORS.has(error)\n ? 'credential'\n : WEBHOOK_CHANNEL_ERRORS.has(error)\n ? 'channel'\n : 'api'\n return {\n delivered: false,\n reason,\n detail:\n reason === 'credential'\n ? `the Slack webhook is revoked (${error}) — no alert can arrive until a human mints a new one. ` +\n 'Prefer replacing it with a bot token, whose liveness can be checked before an alert needs it.'\n : reason === 'channel'\n ? `Slack refused the webhook's channel (${error}) — the channel was archived, or the app lost access.`\n : `Slack refused the webhook post (${error}).`,\n }\n}\n\n/** Webhook error tokens that mean the webhook itself is gone. */\nconst WEBHOOK_CREDENTIAL_ERRORS = new Set(['no_service', 'no_team', 'invalid_token'])\n\n/** Webhook error tokens that mean the webhook is valid but its channel is not usable. */\nconst WEBHOOK_CHANNEL_ERRORS = new Set(['channel_not_found', 'channel_is_archived', 'action_prohibited'])\n\n/** Define configuration options for verifying that a Slack bot token is live */\nexport interface SlackCredentialCheckOptions {\n token: string | undefined\n /** Per-request deadline. Default 10s. */\n timeoutMs?: number\n /** Injection seam for tests; defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\n/** What `checkSlackCredential` concluded. `team`/`botId` are present only when live. */\nexport type SlackCredentialVerdict =\n | { live: true; team: string; botId: string }\n | { live: false; reason: SlackFailureReason; detail: string }\n\n/**\n * Ask Slack whether the bot token is alive, WITHOUT posting anything.\n *\n * This is the check that was missing. An incoming webhook offers no equivalent\n * — the only way to test one is to post to it — which is how a revoked webhook\n * sat in a repo secret for months looking configured. Wire it into `/preflight`\n * so a dead alerting channel fails a deploy rather than an incident.\n */\nexport async function checkSlackCredential(\n options: SlackCredentialCheckOptions,\n): Promise<SlackCredentialVerdict> {\n const token = options.token?.trim()\n if (!token) {\n return {\n live: false,\n reason: 'not-configured',\n detail: 'no Slack bot token configured — set SLACK_BOT_TOKEN',\n }\n }\n const fetchImpl = options.fetchImpl ?? fetch\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS)\n try {\n const response = await fetchImpl(`${SLACK_API}/auth.test`, {\n method: 'POST',\n headers: { Authorization: `Bearer ${token}` },\n signal: controller.signal,\n })\n const body = (await response.json()) as {\n ok?: boolean\n error?: string\n team?: string\n bot_id?: string\n }\n if (body.ok === true) {\n return { live: true, team: body.team ?? 'unknown', botId: body.bot_id ?? 'unknown' }\n }\n const outcome = classifySlackError(body.error ?? `http_${response.status}`, '(auth.test)')\n // `classifySlackError` is shaped for a post; auth.test only ever answers\n // about the credential, so a non-credential code here still means the\n // token cannot be used.\n return {\n live: false,\n reason: outcome.reason === 'channel' ? 'credential' : outcome.reason,\n detail: outcome.detail,\n }\n } catch (error) {\n return {\n live: false,\n reason: 'transport',\n detail: `could not reach Slack: ${error instanceof Error ? error.message : String(error)}`,\n }\n } finally {\n clearTimeout(timer)\n }\n}\n"],"mappings":"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","names":[]}
@@ -448,8 +448,8 @@ function PagesStrip({
448
448
  },
449
449
  className: [
450
450
  "group relative flex shrink-0 flex-col items-center gap-1 rounded p-1 transition",
451
- isActive ? "ring-2 ring-[var(--brand-primary)]" : "hover:bg-[var(--border-default)]/40",
452
- dragOverIndex === index ? "ring-1 ring-[var(--brand-primary)]/60" : ""
451
+ isActive ? "ring-2 ring-[var(--brand-primary)]" : "hover:bg-[color-mix(in_srgb,var(--border-default)_40%,transparent)]",
452
+ dragOverIndex === index ? "ring-1 ring-[color-mix(in_srgb,var(--brand-primary)_60%,transparent)]" : ""
453
453
  ].join(" "),
454
454
  children: [
455
455
  /* @__PURE__ */ jsxs3(
@@ -568,7 +568,8 @@ function Rulers({ pageWidth, pageHeight, zoom, scrollLeft, scrollTop, showRulers
568
568
  guides,
569
569
  onGuidesChange
570
570
  }
571
- )
571
+ ),
572
+ /* @__PURE__ */ jsx6(GuidesCanvasOverlay, { guides, zoom, scrollLeft, scrollTop })
572
573
  ] });
573
574
  }
574
575
  function HorizontalRuler({ pageWidth, zoom, scrollLeft, guides, onGuidesChange }) {
@@ -657,7 +658,23 @@ function HorizontalRuler({ pageWidth, zoom, scrollLeft, guides, onGuidesChange }
657
658
  tick.position
658
659
  );
659
660
  }),
660
- pointerX !== null ? /* @__PURE__ */ jsx6("div", { className: "pointer-events-none absolute top-0 bottom-0 w-px bg-[var(--brand-primary)]/60", style: { left: pointerX } }) : null,
661
+ guides.vertical.map((position, index) => /* @__PURE__ */ jsx6(
662
+ "div",
663
+ {
664
+ "data-guide-marker": "vertical",
665
+ "aria-hidden": true,
666
+ className: "pointer-events-none absolute top-0 bottom-0 w-px bg-[var(--brand-primary)]",
667
+ style: { left: position * zoom - scrollLeft * zoom }
668
+ },
669
+ `guide-${index}`
670
+ )),
671
+ pointerX !== null ? /* @__PURE__ */ jsx6(
672
+ "div",
673
+ {
674
+ className: "pointer-events-none absolute top-0 bottom-0 w-px bg-[color-mix(in_srgb,var(--brand-primary)_60%,transparent)]",
675
+ style: { left: pointerX }
676
+ }
677
+ ) : null,
661
678
  dragGuideX !== null ? /* @__PURE__ */ jsx6(
662
679
  "div",
663
680
  {
@@ -758,7 +775,23 @@ function VerticalRuler({ pageHeight, zoom, scrollTop, guides, onGuidesChange })
758
775
  tick.position
759
776
  );
760
777
  }),
761
- pointerY !== null ? /* @__PURE__ */ jsx6("div", { className: "pointer-events-none absolute left-0 right-0 h-px bg-[var(--brand-primary)]/60", style: { top: pointerY } }) : null,
778
+ guides.horizontal.map((position, index) => /* @__PURE__ */ jsx6(
779
+ "div",
780
+ {
781
+ "data-guide-marker": "horizontal",
782
+ "aria-hidden": true,
783
+ className: "pointer-events-none absolute left-0 right-0 h-px bg-[var(--brand-primary)]",
784
+ style: { top: position * zoom - scrollTop * zoom }
785
+ },
786
+ `guide-${index}`
787
+ )),
788
+ pointerY !== null ? /* @__PURE__ */ jsx6(
789
+ "div",
790
+ {
791
+ className: "pointer-events-none absolute left-0 right-0 h-px bg-[color-mix(in_srgb,var(--brand-primary)_60%,transparent)]",
792
+ style: { top: pointerY }
793
+ }
794
+ ) : null,
762
795
  dragGuideY !== null ? /* @__PURE__ */ jsx6(
763
796
  "div",
764
797
  {
@@ -770,6 +803,37 @@ function VerticalRuler({ pageHeight, zoom, scrollTop, guides, onGuidesChange })
770
803
  }
771
804
  );
772
805
  }
806
+ function GuidesCanvasOverlay({ guides, zoom, scrollLeft, scrollTop }) {
807
+ if (guides.vertical.length === 0 && guides.horizontal.length === 0) return null;
808
+ return /* @__PURE__ */ jsxs4(
809
+ "div",
810
+ {
811
+ "aria-hidden": true,
812
+ className: "pointer-events-none absolute right-0 bottom-0 z-10 overflow-hidden",
813
+ style: { left: RULER_SIZE_PX, top: RULER_SIZE_PX },
814
+ children: [
815
+ guides.vertical.map((position, index) => /* @__PURE__ */ jsx6(
816
+ "div",
817
+ {
818
+ "data-guide-line": "vertical",
819
+ className: "absolute top-0 bottom-0 w-px bg-[var(--brand-primary)]",
820
+ style: { left: position * zoom - scrollLeft * zoom }
821
+ },
822
+ `v-${index}`
823
+ )),
824
+ guides.horizontal.map((position, index) => /* @__PURE__ */ jsx6(
825
+ "div",
826
+ {
827
+ "data-guide-line": "horizontal",
828
+ className: "absolute left-0 right-0 h-px bg-[var(--brand-primary)]",
829
+ style: { top: position * zoom - scrollTop * zoom }
830
+ },
831
+ `h-${index}`
832
+ ))
833
+ ]
834
+ }
835
+ );
836
+ }
773
837
 
774
838
  // src/design-canvas-react/components/Toolbar.tsx
775
839
  import { useEffect as useEffect2, useRef as useRef3, useState as useState3 } from "react";
@@ -894,7 +958,7 @@ function SelectControl({
894
958
  onChange(opt.value);
895
959
  setOpen(false);
896
960
  },
897
- className: `px-3 py-1 text-left text-[12px] hover:bg-[var(--brand-primary)]/10 ${opt.value === value ? "text-[var(--brand-primary)]" : "text-[var(--text-primary)]"}`,
961
+ className: `px-3 py-1 text-left text-[12px] hover:bg-[color-mix(in_srgb,var(--brand-primary)_10%,transparent)] ${opt.value === value ? "text-[var(--brand-primary)]" : "text-[var(--text-primary)]"}`,
898
962
  children: opt.label
899
963
  },
900
964
  opt.value
@@ -963,7 +1027,7 @@ function FontPicker({
963
1027
  setQuery("");
964
1028
  },
965
1029
  style: { fontFamily: family },
966
- className: `block w-full px-3 py-1 text-left text-[13px] hover:bg-[var(--brand-primary)]/10 ${family === value ? "text-[var(--brand-primary)]" : "text-[var(--text-primary)]"}`,
1030
+ className: `block w-full px-3 py-1 text-left text-[13px] hover:bg-[color-mix(in_srgb,var(--brand-primary)_10%,transparent)] ${family === value ? "text-[var(--brand-primary)]" : "text-[var(--text-primary)]"}`,
967
1031
  children: family
968
1032
  },
969
1033
  family
@@ -973,7 +1037,7 @@ function FontPicker({
973
1037
  )
974
1038
  ] });
975
1039
  }
976
- function ColorSwatch({ label, value, onCommit, disabled }) {
1040
+ function ColorSwatch({ label, value, onCommit, disabled, none = false }) {
977
1041
  const [open, setOpen] = useState3(false);
978
1042
  const normalized = value.startsWith("#") ? value : "#ffffff";
979
1043
  return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col items-center gap-0.5", children: [
@@ -988,10 +1052,12 @@ function ColorSwatch({ label, value, onCommit, disabled }) {
988
1052
  {
989
1053
  type: "button",
990
1054
  disabled,
991
- "aria-label": `${label} color`,
1055
+ "aria-label": none ? `${label} color (none)` : `${label} color`,
1056
+ title: none ? "None" : void 0,
992
1057
  onClick: () => setOpen((v) => !v),
993
- className: "h-6 w-10 rounded border border-[var(--border-default)] disabled:cursor-default disabled:opacity-40",
994
- style: { backgroundColor: normalized }
1058
+ className: "relative h-6 w-10 overflow-hidden rounded border border-[var(--border-default)] disabled:cursor-default disabled:opacity-40",
1059
+ style: { backgroundColor: none ? "#ffffff" : normalized },
1060
+ children: none ? /* @__PURE__ */ jsx7("svg", { className: "absolute inset-0 h-full w-full", viewBox: "0 0 40 24", preserveAspectRatio: "none", "aria-hidden": true, children: /* @__PURE__ */ jsx7("line", { x1: "1", y1: "23", x2: "39", y2: "1", stroke: "var(--text-danger)", strokeWidth: "1.5" }) }) : null
995
1061
  }
996
1062
  ),
997
1063
  children: /* @__PURE__ */ jsxs5("div", { className: `${POPOVER_PANEL} w-40 gap-2 p-2`, children: [
@@ -1228,7 +1294,7 @@ function SelectionControls({
1228
1294
  onBindSlot(slotInput.trim() || null);
1229
1295
  setSlotPopoverOpen(false);
1230
1296
  },
1231
- className: "flex-1 rounded border border-[var(--brand-primary)] px-2 py-0.5 text-[11px] text-[var(--brand-primary)] hover:bg-[var(--brand-primary)]/10",
1297
+ className: "flex-1 rounded border border-[var(--brand-primary)] px-2 py-0.5 text-[11px] text-[var(--brand-primary)] hover:bg-[color-mix(in_srgb,var(--brand-primary)_10%,transparent)]",
1232
1298
  children: slotInput.trim() ? "Bind" : "Unbind"
1233
1299
  }
1234
1300
  ),
@@ -1310,7 +1376,7 @@ function TextControls({ element, canWrite, onPatch }) {
1310
1376
  function ShapeControls({ element, canWrite, onPatch, showCornerRadius }) {
1311
1377
  return /* @__PURE__ */ jsxs5(Fragment4, { children: [
1312
1378
  /* @__PURE__ */ jsx7(ColorSwatch, { label: "Fill", value: element.fill, onCommit: (v) => onPatch({ fill: v }), disabled: !canWrite }),
1313
- /* @__PURE__ */ jsx7(ColorSwatch, { label: "Stroke", value: element.stroke ?? "#000000", onCommit: (v) => onPatch({ stroke: v }), disabled: !canWrite }),
1379
+ /* @__PURE__ */ jsx7(ColorSwatch, { label: "Stroke", value: element.stroke ?? "#000000", onCommit: (v) => onPatch({ stroke: v }), disabled: !canWrite, none: (element.strokeWidth ?? 0) === 0 }),
1314
1380
  /* @__PURE__ */ jsx7(NumberInput, { label: "Stroke W", value: element.strokeWidth ?? 0, min: 0, onCommit: (v) => onPatch({ strokeWidth: v }), className: "w-14" }),
1315
1381
  showCornerRadius && "cornerRadius" in element ? /* @__PURE__ */ jsx7(NumberInput, { label: "Corner R", value: element.cornerRadius ?? 0, min: 0, onCommit: (v) => onPatch({ cornerRadius: v }), className: "w-14" }) : null
1316
1382
  ] });
@@ -1376,7 +1442,7 @@ function ImageControls({ element, canWrite, onPatch }) {
1376
1442
  onPatch({ src: swapUrl.trim() });
1377
1443
  setSwapOpen(false);
1378
1444
  },
1379
- className: "flex-1 rounded border border-[var(--brand-primary)] px-2 py-0.5 text-[11px] text-[var(--brand-primary)] hover:bg-[var(--brand-primary)]/10 disabled:cursor-default disabled:opacity-40",
1445
+ className: "flex-1 rounded border border-[var(--brand-primary)] px-2 py-0.5 text-[11px] text-[var(--brand-primary)] hover:bg-[color-mix(in_srgb,var(--brand-primary)_10%,transparent)] disabled:cursor-default disabled:opacity-40",
1380
1446
  children: "Replace"
1381
1447
  }
1382
1448
  ),
@@ -1432,7 +1498,8 @@ function PagePropsControls({ page, canWrite, onSetPageProps, onSetPageGuides, pa
1432
1498
  const preset = SIZE_PRESETS.find((p) => p.id === id);
1433
1499
  if (preset) onSetPageProps({ width: preset.width, height: preset.height });
1434
1500
  },
1435
- options: presetOptions
1501
+ options: presetOptions,
1502
+ buttonClassName: "w-44"
1436
1503
  }
1437
1504
  ),
1438
1505
  /* @__PURE__ */ jsxs5("label", { className: "flex flex-col items-center gap-0.5", children: [
@@ -1627,7 +1694,7 @@ function ExportControl({ defaults, onExport, className }) {
1627
1694
  setOpen(false);
1628
1695
  }
1629
1696
  },
1630
- className: "flex h-8 items-center gap-1.5 rounded-md border border-[var(--border-default)] px-2.5 text-xs font-medium text-[var(--text-primary)] transition-colors hover:border-[var(--brand-primary)]/40",
1697
+ className: "flex h-8 items-center gap-1.5 rounded-md border border-[var(--border-default)] px-2.5 text-xs font-medium text-[var(--text-primary)] transition-colors hover:border-[color-mix(in_srgb,var(--brand-primary)_40%,transparent)]",
1631
1698
  children: [
1632
1699
  /* @__PURE__ */ jsx9(ExportGlyph, { className: "h-3.5 w-3.5" }),
1633
1700
  "Export",
@@ -1681,7 +1748,7 @@ function ExportControl({ defaults, onExport, className }) {
1681
1748
  type: "button",
1682
1749
  "aria-label": "Export image",
1683
1750
  onClick: confirm,
1684
- className: "rounded border border-[var(--brand-primary)] px-2 py-1 text-[11px] font-medium text-[var(--brand-primary)] transition-colors hover:bg-[var(--brand-primary)]/10",
1751
+ className: "rounded border border-[var(--brand-primary)] px-2 py-1 text-[11px] font-medium text-[var(--brand-primary)] transition-colors hover:bg-[color-mix(in_srgb,var(--brand-primary)_10%,transparent)]",
1685
1752
  children: "Export"
1686
1753
  }
1687
1754
  )
@@ -2062,7 +2129,7 @@ function DesignCanvas({
2062
2129
  /* @__PURE__ */ jsx10("p", { className: "text-xs leading-5 text-[var(--text-secondary)]", children: "The design canvas needs room to work. Open this on a tablet or desktop to edit pages, elements, and export." })
2063
2130
  ] })
2064
2131
  ] }),
2065
- /* @__PURE__ */ jsxs8("div", { className: "relative hidden min-h-0 flex-1 sm:block", children: [
2132
+ /* @__PURE__ */ jsxs8("div", { className: `relative hidden min-h-0 flex-1 sm:block ${editorState.showRulers ? "pl-[20px] pt-[20px]" : ""}`, children: [
2066
2133
  /* @__PURE__ */ jsx10(
2067
2134
  Rulers,
2068
2135
  {
@@ -2086,8 +2153,8 @@ function DesignCanvas({
2086
2153
  {
2087
2154
  style: {
2088
2155
  position: "absolute",
2089
- left: editorState.panX,
2090
- top: editorState.panY
2156
+ left: editorState.panX + (editorState.showRulers ? 20 : 0),
2157
+ top: editorState.panY + (editorState.showRulers ? 20 : 0)
2091
2158
  },
2092
2159
  children: /* @__PURE__ */ jsx10(
2093
2160
  BleedTrimOverlay,
@@ -2192,4 +2259,4 @@ export {
2192
2259
  ExportControl,
2193
2260
  DesignCanvas
2194
2261
  };
2195
- //# sourceMappingURL=chunk-NC2V63EL.js.map
2262
+ //# sourceMappingURL=chunk-FNM7JYME.js.map