agents
Version:
A home for your AI agents
267 lines (266 loc) • 10.9 kB
JavaScript
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