UNPKG

agents

Version:

A home for your AI agents

267 lines (266 loc) • 10.9 kB
import { n as encodeUtf8, r as isRecord, t as emptyIngressResponse } from "../../../internal-kmw70xwR.js"; import { t as matchesPath } from "../../../ingress-EFbrS547.js"; //#region src/experimental/channels/slack/index.ts const DEFAULT_SLACK_WEBHOOK_PATH = "/webhooks/slack"; const DEFAULT_MAX_SKEW_SECONDS = 300; const APPROVE_ACTION_ID = "cloudflare_channels_approve_v1"; const REJECT_ACTION_ID = "cloudflare_channels_reject_v1"; function channelIdentity(teamId, channelId) { return `slack:${teamId}:channel:${channelId}`; } function messageIdentity(teamId, channelId, timestamp) { return `${channelIdentity(teamId, channelId)}:message:${timestamp}`; } function replySurfaceLabel(channelId, threadTimestamp) { return threadTimestamp ? `Slack · ${channelId} · thread ${threadTimestamp}` : `Slack · ${channelId}`; } function threadIdentity(teamId, channelId, timestamp, threadTimestamp, isDirectMessage) { const channel = channelIdentity(teamId, channelId); if (isDirectMessage && threadTimestamp === void 0) return channel; return `${channel}:thread:${threadTimestamp ?? timestamp}`; } function slackTimestamp(timestamp) { const milliseconds = Number(timestamp) * 1e3; if (!Number.isFinite(milliseconds)) return void 0; try { return new Date(milliseconds).toISOString(); } catch { return; } } function selfUserIds(payload, configuredBotUserId) { const ids = /* @__PURE__ */ new Set(); if (configuredBotUserId) ids.add(configuredBotUserId); if (Array.isArray(payload.authorizations)) { for (const authorization of payload.authorizations) if (authorization?.is_bot === true && typeof authorization.user_id === "string") ids.add(authorization.user_id); } return ids; } function normalizedEvent(payload, configuredBotUserId) { const event = payload.event; if (!event || !isRecord(event)) return void 0; if (event.type !== "app_mention" && event.type !== "message") return; const isMention = event.type === "app_mention"; const isDirectMessage = event.type === "message" && event.channel_type === "im"; if (event.subtype !== void 0 || typeof event.bot_id === "string" || typeof event.app_id === "string") return; const teamId = payload.team_id; const eventId = payload.event_id; const channelId = event.channel; const actorId = event.user; const text = event.text; const timestamp = event.ts; if (typeof teamId !== "string" || typeof eventId !== "string" || typeof channelId !== "string" || typeof actorId !== "string" || typeof text !== "string" || typeof timestamp !== "string") return; if (selfUserIds(payload, configuredBotUserId).has(actorId)) return; const threadTimestamp = typeof event.thread_ts === "string" ? event.thread_ts : void 0; const sentAt = slackTimestamp(timestamp); return { type: "message", eventId: `slack:${teamId}:event:${eventId}`, thread: { id: threadIdentity(teamId, channelId, timestamp, threadTimestamp, isDirectMessage), isDirectMessage }, replySurface: { version: 1, address: { teamId, channelId, ...(threadTimestamp || !isDirectMessage) && { threadTs: threadTimestamp ?? timestamp }, recipientUserId: actorId, recipientTeamId: teamId }, label: replySurfaceLabel(channelId, threadTimestamp ?? (isDirectMessage ? void 0 : timestamp)) }, actor: { id: `slack:${teamId}:user:${actorId}`, identity: { scope: teamId, subject: actorId }, isBot: false, isSelf: false }, message: { id: messageIdentity(teamId, channelId, timestamp), text, markdown: text, ...isMention && { isMention: true }, ...threadTimestamp && threadTimestamp !== timestamp && { reply: { id: messageIdentity(teamId, channelId, threadTimestamp) } }, ...sentAt && { metadata: { sentAt } } } }; } function parseApprovalValue(value) { let parsed; try { parsed = JSON.parse(value); } catch { return; } if (!isRecord(parsed) || parsed.v !== 1 || typeof parsed.approvalId !== "string" || !parsed.approvalId || parsed.decision !== "approve" && parsed.decision !== "reject") return; return parsed; } async function sha256Hex(value) { const digest = await crypto.subtle.digest("SHA-256", encodeUtf8(value)); return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); } async function normalizedInteractions(payload, rawBody) { const teamId = payload.team?.id; const actorId = payload.user?.id; const channelId = typeof payload.channel?.id === "string" ? payload.channel.id : payload.container?.channel_id; const timestamp = typeof payload.message?.ts === "string" ? payload.message.ts : payload.container?.message_ts; if (typeof teamId !== "string" || typeof actorId !== "string" || typeof channelId !== "string" || typeof timestamp !== "string" || !Array.isArray(payload.actions)) return []; const threadTimestamp = typeof payload.message?.thread_ts === "string" ? payload.message.thread_ts : typeof payload.container?.thread_ts === "string" ? payload.container.thread_ts : void 0; const isDirectMessage = channelId.startsWith("D"); const bodyHash = await sha256Hex(rawBody); const events = []; for (const [index, action] of payload.actions.entries()) { if (!action || typeof action.value !== "string") continue; const expectedDecision = action.action_id === APPROVE_ACTION_ID ? "approve" : action.action_id === REJECT_ACTION_ID ? "reject" : void 0; if (!expectedDecision) continue; const approval = parseApprovalValue(action.value); if (!approval || approval.decision !== expectedDecision) continue; const stableEventId = `slack:${teamId}:interaction:sha256:${bodyHash}:action:${index}`; const actionTimestamp = typeof action.action_ts === "string" ? action.action_ts : void 0; events.push({ type: "approval-response", eventId: stableEventId, thread: { id: threadIdentity(teamId, channelId, timestamp, threadTimestamp, isDirectMessage), isDirectMessage }, replySurface: { version: 1, address: { teamId, channelId, threadTs: threadTimestamp ?? timestamp, ...!isDirectMessage && { recipientUserId: actorId, recipientTeamId: teamId } }, label: replySurfaceLabel(channelId, threadTimestamp ?? timestamp) }, actor: { id: `slack:${teamId}:user:${actorId}`, identity: { scope: teamId, subject: actorId }, ...typeof payload.user?.username === "string" && { username: payload.user.username }, isBot: false }, approvalId: approval.approvalId, decision: approval.decision, reference: actionTimestamp ? `${channelIdentity(teamId, channelId)}:action:${actionTimestamp}` : stableEventId }); } return events; } function bytesFromHex(hex) { if (!/^[a-f0-9]{64}$/.test(hex)) return void 0; const bytes = new Uint8Array(hex.length / 2); for (let index = 0; index < bytes.length; index += 1) bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16); return bytes; } async function hasValidSignature(request, rawBody, signingSecret, maxSkewSeconds) { const timestamp = request.headers.get("x-slack-request-timestamp"); const signature = request.headers.get("x-slack-signature"); if (!timestamp || !signature || !/^\d+$/.test(timestamp)) return false; const timestampSeconds = Number(timestamp); if (!Number.isSafeInteger(timestampSeconds) || Math.abs(Math.floor(Date.now() / 1e3) - timestampSeconds) > maxSkewSeconds) return false; const match = signature.match(/^v0=([a-f0-9]{64})$/); const signatureBytes = match ? bytesFromHex(match[1]) : void 0; if (!signatureBytes) return false; const key = await crypto.subtle.importKey("raw", encodeUtf8(signingSecret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]); return crypto.subtle.verify("HMAC", key, signatureBytes, encodeUtf8(`v0:${timestamp}:${rawBody}`)); } function parseJsonPayload(rawBody) { let parsed; try { parsed = JSON.parse(rawBody); } catch { return; } if (!isRecord(parsed)) return void 0; if (parsed.type !== "url_verification" && parsed.type !== "event_callback") return; return parsed; } function parseInteractionPayload(rawBody) { const encodedPayload = new URLSearchParams(rawBody).get("payload"); if (!encodedPayload) return void 0; let parsed; try { parsed = JSON.parse(encodedPayload); } catch { return; } return isRecord(parsed) && parsed.type === "block_actions" ? parsed : void 0; } /** Create dependency-free, request-signed Slack HTTP ingress. */ function slackWebhook(options) { if (!options.signingSecret.trim()) throw new Error("signingSecret is required to create Slack webhook ingress"); const maxSkewSeconds = options.maxSkewSeconds ?? DEFAULT_MAX_SKEW_SECONDS; if (!Number.isInteger(maxSkewSeconds) || maxSkewSeconds < 0) throw new Error("maxSkewSeconds must be a non-negative integer"); const path = options.path ?? DEFAULT_SLACK_WEBHOOK_PATH; return { async receive(request) { if (!matchesPath(request, path)) return null; if (request.method !== "POST") return emptyIngressResponse(405); const rawBody = await request.text(); if (!await hasValidSignature(request, rawBody, options.signingSecret, maxSkewSeconds)) return emptyIngressResponse(401); if ((request.headers.get("content-type")?.split(";", 1)[0]?.trim() ?? "") === "application/x-www-form-urlencoded") { const payload = parseInteractionPayload(rawBody); if (!payload) return emptyIngressResponse(400); return { events: (await normalizedInteractions(payload, rawBody)).map((event) => ({ event, raw: payload })), response: new Response(null, { status: 200 }) }; } const payload = parseJsonPayload(rawBody); if (!payload) return emptyIngressResponse(400); if (payload.type === "url_verification") return typeof payload.challenge === "string" ? { events: [], response: Response.json({ challenge: payload.challenge }) } : emptyIngressResponse(400); const event = normalizedEvent(payload, options.botUserId); return { events: event ? [{ event, raw: payload }] : [], response: new Response(null, { status: 200 }) }; } }; } /** Create a configured Slack ingress Channel. */ function slack(options) { if (!options.botToken.trim()) throw new Error("botToken is required to create a Slack channel"); if (options.webhook && !options.participant) throw new Error("participant is required with a Slack webhook"); const ingress = options.webhook ? slackWebhook(options.webhook) : void 0; return { ...options.participant && { participant: options.participant }, ...options.route && { route: options.route }, ...ingress && { ingress }, contactSurface(identity) { if (identity.scope === void 0) return null; return { version: 1, address: { teamId: identity.scope, userId: identity.subject }, label: `Slack · user ${identity.subject}` }; } }; } //#endregion export { slack, slackWebhook }; //# sourceMappingURL=index.js.map