run402-mcp
Version:
MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.
629 lines • 29.3 kB
JavaScript
/**
* `email` namespace — project mailboxes (defaults, send / list / get / raw /
* delete) and mailbox webhooks.
*/
import { ApiError, LocalError } from "../errors.js";
import { requireProjectCredentials } from "../project-credentials.js";
import { assertEmailAddress, assertHttpUrl, assertNonEmptyString, assertPositiveSafeInteger, assertStringInSet, } from "../validation.js";
const SLUG_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
const WEBHOOK_EVENTS = ["delivery", "bounced", "complained", "reply_received", "mailbox_suspended"];
const MESSAGE_DIRECTIONS = ["inbound", "outbound"];
const DELIVERY_STATUSES = ["pending", "in_flight", "delivered", "failed_permanent"];
const MAILBOX_FOOTER_POLICIES = ["run402_transparency", "none"];
export class Webhooks {
client;
resolveMailbox;
constructor(client, resolveMailbox) {
this.client = client;
this.resolveMailbox = resolveMailbox;
}
/**
* List durable webhook delivery rows for the mailbox, optionally filtered by
* status. `failed_permanent` is the dead-letter queue. Delivery is
* at-least-once — consumers MUST dedupe on the envelope `idempotency_key`.
*/
async listDeliveries(projectId, opts = {}) {
if (opts.status !== undefined) {
assertStringInSet(opts.status, DELIVERY_STATUSES, "status", "listing webhook deliveries");
}
if (opts.limit !== undefined) {
assertPositiveSafeInteger(opts.limit, "limit", "listing webhook deliveries");
}
const { id, serviceKey } = await this.resolveMailbox(projectId, opts.mailbox);
const qs = new URLSearchParams();
if (opts.status !== undefined)
qs.set("status", opts.status);
if (opts.limit !== undefined)
qs.set("limit", String(opts.limit));
if (opts.after)
qs.set("after", opts.after);
const path = `/mailboxes/v1/${id}/webhooks/deliveries${qs.toString() ? "?" + qs.toString() : ""}`;
return this.client.request(path, {
headers: { Authorization: `Bearer ${serviceKey}` },
context: "listing webhook deliveries",
});
}
/** Re-queue a dead-lettered (`failed_permanent`) delivery for another attempt. */
async redriveDelivery(projectId, deliveryId, opts = {}) {
const { id, serviceKey } = await this.resolveMailbox(projectId, opts.mailbox);
const encodedDeliveryId = encodePathSegment(deliveryId, "deliveryId", "redriving webhook delivery");
return this.client.request(`/mailboxes/v1/${id}/webhooks/deliveries/${encodedDeliveryId}/redrive`, {
method: "POST",
headers: { Authorization: `Bearer ${serviceKey}` },
context: "redriving webhook delivery",
});
}
async register(projectId, opts) {
validateRegisterWebhookOptions(opts);
const { id, serviceKey } = await this.resolveMailbox(projectId, opts.mailbox);
return this.client.request(`/mailboxes/v1/${id}/webhooks`, {
method: "POST",
headers: { Authorization: `Bearer ${serviceKey}` },
body: { url: opts.url, events: opts.events },
context: "registering webhook",
});
}
async list(projectId, opts = {}) {
const { id, serviceKey } = await this.resolveMailbox(projectId, opts.mailbox);
return this.client.request(`/mailboxes/v1/${id}/webhooks`, {
headers: { Authorization: `Bearer ${serviceKey}` },
context: "listing webhooks",
});
}
async get(projectId, webhookId, opts = {}) {
const { id, serviceKey } = await this.resolveMailbox(projectId, opts.mailbox);
const encodedWebhookId = encodePathSegment(webhookId, "webhookId", "getting webhook");
return this.client.request(`/mailboxes/v1/${id}/webhooks/${encodedWebhookId}`, {
headers: { Authorization: `Bearer ${serviceKey}` },
context: "getting webhook",
});
}
async update(projectId, webhookId, opts) {
const patch = validateUpdateWebhookOptions(opts);
if (!patch.hasUrl && !patch.hasEvents) {
throw new LocalError("Provide at least `url` or `events` to update a webhook.", "updating webhook");
}
const { id, serviceKey } = await this.resolveMailbox(projectId, opts.mailbox);
const encodedWebhookId = encodePathSegment(webhookId, "webhookId", "updating webhook");
const body = {};
if (patch.hasUrl)
body.url = opts.url;
if (patch.hasEvents)
body.events = opts.events;
return this.client.request(`/mailboxes/v1/${id}/webhooks/${encodedWebhookId}`, {
method: "PATCH",
headers: { Authorization: `Bearer ${serviceKey}` },
body,
context: "updating webhook",
});
}
async delete(projectId, webhookId, opts = {}) {
const { id, serviceKey } = await this.resolveMailbox(projectId, opts.mailbox);
const encodedWebhookId = encodePathSegment(webhookId, "webhookId", "deleting webhook");
await this.client.request(`/mailboxes/v1/${id}/webhooks/${encodedWebhookId}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${serviceKey}` },
context: "deleting webhook",
});
}
}
export class Email {
client;
webhooks;
create;
status;
info;
delete;
update;
constructor(client) {
this.client = client;
this.webhooks = new Webhooks(client, (projectId, selector) => this.resolveMailbox(projectId, selector));
this.create = this.createMailbox.bind(this);
this.status = this.getMailbox.bind(this);
this.info = this.getMailbox.bind(this);
this.delete = this.deleteMailbox.bind(this);
this.update = this.updateMailbox.bind(this);
}
/**
* Resolve a project mailbox to `{ id, serviceKey }` for an operation.
*
* - `selector` is a mailbox id (`mbx_…`) → used directly; the gateway 403s
* if the id belongs to a different project, so no list call is needed.
* - `selector` is a slug → the project's mailboxes are listed and matched.
* - `selector` omitted for sends → use the configured
* `default_outbound_mailbox_id`. When the gateway returns
* `mailbox_settings` and no default is set, throw
* `DEFAULT_MAILBOX_REQUIRED`; do not silently pick the first/single row.
* - `selector` omitted for reads/webhooks → require a unique mailbox, as
* before. This keeps non-send flows from guessing that the outbound
* default is also the intended read/webhook target.
*
* The cached `mailbox_id` is intentionally NOT used to resolve when a
* selector is omitted — trusting it would silently target an arbitrary
* mailbox on a multi-mailbox project.
*/
async resolveMailbox(projectId, selector, mode = "unique", context = "resolving mailbox") {
const project = await requireProjectCredentials(this.client, projectId, context);
if (selector && /^mbx_/.test(selector)) {
return { id: selector, serviceKey: project.service_key };
}
const list = await this.listMailboxEnvelope(project.service_key);
const mb = mode === "default_outbound" && !selector
? this.pickDefaultOutboundMailbox(projectId, list, context)
: this.pickMailbox(list.mailboxes, selector, context);
if (!selector && list.mailboxes.length === 1)
await this.cacheMailbox(projectId, mb);
return { id: mb.mailbox_id, serviceKey: project.service_key };
}
pickDefaultOutboundMailbox(projectId, envelope, context) {
const defaultId = envelope.mailbox_settings?.default_outbound_mailbox_id ??
envelope.mailboxes.find((m) => m.is_default_outbound)?.mailbox_id ??
null;
if (defaultId) {
const mb = envelope.mailboxes.find((m) => m.mailbox_id === defaultId);
if (!mb) {
throw mailboxConfigError({
status: 409,
code: "DEFAULT_MAILBOX_INVALID",
message: `Configured default outbound mailbox ${defaultId} is not available. Set a valid default before sending email.`,
context,
projectId,
mailboxes: envelope.mailboxes,
settings: envelope.mailbox_settings,
nextActions: envelope.next_actions,
details: { default_outbound_mailbox_id: defaultId, reason: "missing_from_mailbox_list" },
});
}
if (mb.can_send === false) {
throw mailboxConfigError({
status: 409,
code: "DEFAULT_MAILBOX_INVALID",
message: `Configured default outbound mailbox ${defaultId} cannot send email: ${mb.send_blocked_reason ?? "blocked"}.`,
context,
projectId,
mailboxes: envelope.mailboxes,
settings: envelope.mailbox_settings,
nextActions: envelope.next_actions,
details: {
default_outbound_mailbox_id: defaultId,
reason: mb.send_blocked_reason ?? "send_blocked",
},
});
}
return mb;
}
// Old gateways did not return mailbox_settings. Keep the legacy
// single-mailbox convenience only while that field is absent, so tolerant
// public clients can ship before the stricter gateway rollout.
if (envelope.mailbox_settings === undefined) {
return this.pickMailbox(envelope.mailboxes, undefined, context);
}
throw mailboxConfigError({
status: envelope.mailboxes.length > 1 ? 409 : 400,
code: "DEFAULT_MAILBOX_REQUIRED",
message: "Set default_outbound_mailbox_id before sending email without an explicit mailbox.",
context,
projectId,
mailboxes: envelope.mailboxes,
settings: envelope.mailbox_settings,
nextActions: envelope.next_actions,
});
}
/**
* Choose a mailbox from a project's list given an optional selector.
* `selector` matches by exact mailbox id or slug. With no selector: 0 →
* "create one first" (404), 1 → that one, 2+ → ambiguity error (409) naming
* the available slugs.
*/
pickMailbox(list, selector, context) {
if (selector) {
const hit = list.find((m) => m.mailbox_id === selector || m.slug === selector);
if (!hit) {
throw new ApiError(`No mailbox matching "${selector}" (slug or id) in this project.`, 404, null, context);
}
return hit;
}
if (list.length === 0) {
throw new ApiError("No mailbox found for this project. Use `create_mailbox` to create one first.", 404, null, context);
}
if (list.length > 1) {
const slugs = list.map((m) => m.slug).join(", ");
throw new ApiError(`Project has ${list.length} mailboxes (${slugs}). Specify which one via the "mailbox" parameter (slug or id).`, 409, null, context);
}
return list[0];
}
/** Best-effort refresh of the single-mailbox convenience cache. */
async cacheMailbox(projectId, mb) {
const updater = this.client.credentials.updateProject;
if (!updater)
return;
try {
await updater.call(this.client.credentials, projectId, {
mailbox_id: mb.mailbox_id,
mailbox_address: mb.address,
});
}
catch {
// best-effort cache — ignore failures
}
}
async listMailboxEnvelope(serviceKey) {
const raw = await this.client.request(`/mailboxes/v1`, {
headers: { Authorization: `Bearer ${serviceKey}` },
context: "listing mailboxes",
});
return {
mailboxes: Array.isArray(raw.mailboxes) ? raw.mailboxes : [],
...(raw.mailbox_settings !== undefined ? { mailbox_settings: raw.mailbox_settings } : {}),
...(raw.provider_readiness !== undefined ? { provider_readiness: raw.provider_readiness } : {}),
...(Array.isArray(raw.next_actions) ? { next_actions: raw.next_actions } : {}),
};
}
/** List project mailboxes plus default-role settings and gateway repair hints. */
async listMailboxes(projectId) {
const project = await requireProjectCredentials(this.client, projectId, "listing mailboxes");
return this.listMailboxEnvelope(project.service_key);
}
/**
* Configure the project's mailbox defaults. Values are mailbox ids
* (`mbx_…`) or `null` to clear when the gateway allows clearing.
*/
async setMailboxDefaults(projectId, opts) {
if (!opts || typeof opts !== "object" || Array.isArray(opts)) {
throw new LocalError("r.email.setMailboxDefaults(projectId, opts) requires an opts object.", "setting mailbox defaults");
}
const hasOutbound = Object.prototype.hasOwnProperty.call(opts, "default_outbound_mailbox_id");
const hasAuthSender = Object.prototype.hasOwnProperty.call(opts, "auth_sender_mailbox_id");
if (!hasOutbound && !hasAuthSender) {
throw new LocalError("Provide default_outbound_mailbox_id and/or auth_sender_mailbox_id.", "setting mailbox defaults");
}
validateOptionalMailboxId(opts.default_outbound_mailbox_id, "default_outbound_mailbox_id");
validateOptionalMailboxId(opts.auth_sender_mailbox_id, "auth_sender_mailbox_id");
const project = await requireProjectCredentials(this.client, projectId, "setting mailbox defaults");
return this.client.request("/mailboxes/v1/settings", {
method: "PATCH",
headers: { Authorization: `Bearer ${project.service_key}` },
body: {
...(hasOutbound ? { default_outbound_mailbox_id: opts.default_outbound_mailbox_id } : {}),
...(hasAuthSender ? { auth_sender_mailbox_id: opts.auth_sender_mailbox_id } : {}),
},
context: "setting mailbox defaults",
});
}
/**
* Update per-mailbox settings. Currently supports the outbound
* `footer_policy` field accepted by PATCH /mailboxes/v1/:mailbox_id.
*/
async updateMailbox(projectId, opts) {
if (!opts || typeof opts !== "object" || Array.isArray(opts)) {
throw new LocalError("r.email.updateMailbox(projectId, opts) requires an opts object.", "updating mailbox");
}
const hasFooterPolicy = Object.prototype.hasOwnProperty.call(opts, "footer_policy");
if (!hasFooterPolicy) {
throw new LocalError("Provide footer_policy.", "updating mailbox");
}
assertStringInSet(opts.footer_policy, MAILBOX_FOOTER_POLICIES, "footer_policy", "updating mailbox");
const { id, serviceKey } = await this.resolveMailbox(projectId, opts.mailbox, "unique", "updating mailbox");
return this.client.request(`/mailboxes/v1/${id}`, {
method: "PATCH",
headers: { Authorization: `Bearer ${serviceKey}` },
body: { footer_policy: opts.footer_policy },
context: "updating mailbox",
});
}
/**
* Create a mailbox for a project.
*
* NOT idempotent. A 409 from the gateway now means `Slug already in use`,
* `Address is in cooldown period`, or `Project mailbox limit reached (5)` —
* none of which mean "you already own this slug" — so the 409 is surfaced
* verbatim rather than silently returning some other existing mailbox.
* Callers that want create-or-get should `list`/`getMailbox` explicitly.
*/
async createMailbox(projectId, slug) {
const project = await requireProjectCredentials(this.client, projectId, "creating mailbox");
if (slug.length < 3 || slug.length > 63) {
throw new LocalError("Slug must be 3-63 characters.", "creating mailbox");
}
if (!SLUG_RE.test(slug)) {
throw new LocalError("Slug must be lowercase alphanumeric + hyphens, start/end with alphanumeric.", "creating mailbox");
}
if (slug.includes("--")) {
throw new LocalError("Slug must not contain consecutive hyphens.", "creating mailbox");
}
const raw = await this.client.request("/mailboxes/v1", {
method: "POST",
headers: { Authorization: `Bearer ${project.service_key}` },
body: { slug, project_id: projectId },
context: "creating mailbox",
});
const result = normalizeCreateMailboxResult(raw);
await this.cacheMailbox(projectId, result);
return result;
}
/** Send an email via template or raw (subject + html) mode. */
async send(projectId, opts) {
if (!opts || typeof opts !== "object" || Array.isArray(opts)) {
throw new LocalError("r.email.send(projectId, opts) requires an opts object as the 2nd argument.", "sending email");
}
assertEmailAddress(opts.to, "to", "sending email");
const hasSubject = !!opts.subject;
const hasHtml = !!opts.html;
const isRaw = hasSubject || hasHtml;
const isTemplate = !!opts.template;
if (!isRaw && !isTemplate) {
throw new LocalError("Provide either `template` + `variables` or both `subject` + `html`.", "sending email");
}
if (isRaw && isTemplate) {
throw new LocalError("Provide `template` OR raw mode (`subject` + `html`), not both.", "sending email");
}
if (isRaw && !(hasSubject && hasHtml)) {
const missing = hasSubject ? "html" : "subject";
throw new LocalError(`Raw mode requires both \`subject\` and \`html\` (missing \`${missing}\`).`, "sending email");
}
const hasAttachments = Array.isArray(opts.attachments) && opts.attachments.length > 0;
if (hasAttachments && isTemplate) {
throw new LocalError("Attachments are only supported in raw mode (`subject` + `html`), not with `template`.", "sending email");
}
const { id, serviceKey } = await this.resolveMailbox(projectId, opts.mailbox, "default_outbound", "sending email");
const body = { to: opts.to };
if (isTemplate) {
body.template = opts.template;
body.variables = opts.variables;
}
else {
body.subject = opts.subject;
body.html = opts.html;
if (opts.text !== undefined)
body.text = opts.text;
if (hasAttachments)
body.attachments = opts.attachments;
}
if (opts.from_name !== undefined)
body.from_name = opts.from_name;
if (opts.in_reply_to !== undefined)
body.in_reply_to = opts.in_reply_to;
return this.client.request(`/mailboxes/v1/${id}/messages`, {
method: "POST",
headers: { Authorization: `Bearer ${serviceKey}` },
body,
context: "sending email",
});
}
/** List messages in the project's mailbox. */
async list(projectId, opts = {}) {
if (opts.limit !== undefined) {
assertPositiveSafeInteger(opts.limit, "limit", "listing emails");
}
if (opts.direction !== undefined) {
assertStringInSet(opts.direction, MESSAGE_DIRECTIONS, "direction", "listing emails");
}
const { id, serviceKey } = await this.resolveMailbox(projectId, opts.mailbox);
const qs = new URLSearchParams();
if (opts.limit !== undefined)
qs.set("limit", String(opts.limit));
if (opts.after)
qs.set("after", opts.after);
if (opts.direction !== undefined)
qs.set("direction", opts.direction);
const path = `/mailboxes/v1/${id}/messages${qs.toString() ? "?" + qs.toString() : ""}`;
const raw = await this.client.request(path, {
headers: { Authorization: `Bearer ${serviceKey}` },
context: "listing emails",
});
const rows = Array.isArray(raw) ? raw : Array.isArray(raw.messages) ? raw.messages : [];
return rows.map(normalizeEmailSummary);
}
/** Get a single message by id, including any replies. */
async get(projectId, messageId, opts = {}) {
const { id, serviceKey } = await this.resolveMailbox(projectId, opts.mailbox);
const encodedMessageId = encodePathSegment(messageId, "messageId", "getting email");
const raw = await this.client.request(`/mailboxes/v1/${id}/messages/${encodedMessageId}`, {
headers: { Authorization: `Bearer ${serviceKey}` },
context: "getting email",
});
return normalizeEmailDetail(raw);
}
/**
* Fetch the raw RFC-822 bytes of an inbound message. Returns `Uint8Array`
* so the consumer can decode / store / forward without re-encoding.
*/
async getRaw(projectId, messageId, opts = {}) {
const { id, serviceKey } = await this.resolveMailbox(projectId, opts.mailbox);
const encodedMessageId = encodePathSegment(messageId, "messageId", "fetching raw MIME");
const url = `${this.client.apiBase}/mailboxes/v1/${id}/messages/${encodedMessageId}/raw`;
const res = await this.client.fetch(url, {
headers: { Authorization: `Bearer ${serviceKey}` },
});
if (!res.ok) {
let body;
try {
body = await res.json();
}
catch {
body = await res.text().catch(() => "");
}
throw new ApiError(`Fetching raw MIME failed (HTTP ${res.status})`, res.status, body, "fetching raw MIME");
}
const buf = await res.arrayBuffer();
return {
content_type: res.headers.get("content-type") ?? "message/rfc822",
bytes: new Uint8Array(buf),
};
}
/**
* Get a project mailbox's info. With a `selector` (slug or `mbx_…` id),
* returns that mailbox; without one, returns the project's only mailbox or
* throws an ambiguity error when it has more than one.
*/
async getMailbox(projectId, selector) {
const project = await requireProjectCredentials(this.client, projectId, "getting mailbox");
const list = await this.listMailboxEnvelope(project.service_key);
const mb = this.pickMailbox(list.mailboxes, selector, "getting mailbox");
if (!selector && list.mailboxes.length === 1)
await this.cacheMailbox(projectId, mb);
return mb;
}
/**
* Delete the project's mailbox. Destructive — drops all messages and
* webhook subscriptions. Pass `mailboxId` explicitly to delete a specific
* mailbox; otherwise the project's current mailbox is resolved. Returns
* the deleted record echoed by the gateway.
*/
async deleteMailbox(projectId, selector) {
const project = await requireProjectCredentials(this.client, projectId, "deleting mailbox");
let id;
if (selector && /^mbx_/.test(selector)) {
id = selector;
}
else {
const list = await this.listMailboxEnvelope(project.service_key);
if (!selector && list.mailboxes.length === 0) {
throw new ApiError("No mailbox found for this project — nothing to delete.", 404, null, "deleting mailbox");
}
id = this.pickMailbox(list.mailboxes, selector, "deleting mailbox").mailbox_id;
}
const result = await this.client.request(`/mailboxes/v1/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${project.service_key}` },
context: "deleting mailbox",
});
const updater = this.client.credentials.updateProject;
if (updater) {
try {
await updater.call(this.client.credentials, projectId, {
mailbox_id: undefined,
mailbox_address: undefined,
});
}
catch {
// best-effort
}
}
return result;
}
}
function normalizeCreateMailboxResult(raw) {
const maybeEnvelope = raw;
if (maybeEnvelope.mailbox && typeof maybeEnvelope.mailbox === "object") {
return {
...maybeEnvelope.mailbox,
...(maybeEnvelope.mailbox_settings !== undefined
? { mailbox_settings: maybeEnvelope.mailbox_settings }
: {}),
...(Array.isArray(maybeEnvelope.next_actions)
? { next_actions: maybeEnvelope.next_actions }
: {}),
};
}
const maybeLegacy = raw;
if (!maybeLegacy.mailbox_id && typeof maybeLegacy.id === "string") {
return { ...maybeLegacy, mailbox_id: maybeLegacy.id };
}
return raw;
}
function normalizeEmailSummary(raw) {
const id = raw.id ?? raw.message_id ?? "";
return {
...raw,
id,
...(raw.message_id === undefined && id ? { message_id: id } : {}),
direction: raw.direction ?? "outbound",
template: raw.template ?? null,
};
}
function normalizeEmailDetail(raw) {
const id = raw.id ?? raw.message_id ?? "";
return {
...raw,
id,
...(raw.message_id === undefined && id ? { message_id: id } : {}),
template: raw.template ?? null,
variables: raw.variables ?? {},
};
}
function validateOptionalMailboxId(value, field) {
if (value === undefined || value === null)
return;
assertNonEmptyString(value, field, "setting mailbox defaults");
if (!/^mbx_/.test(value)) {
throw new LocalError(`${field} must be a mailbox id starting with "mbx_". Use listMailboxes() to resolve a slug first.`, "setting mailbox defaults");
}
}
function mailboxConfigError(opts) {
const next_actions = opts.nextActions && opts.nextActions.length > 0
? opts.nextActions
: [{
type: "set_mailbox_defaults",
method: "PATCH",
path: "/mailboxes/v1/settings",
auth: "service_key",
why: "Choose the mailbox that should send outbound project email, then set default_outbound_mailbox_id.",
command: `run402 email defaults --outbound <slug|mbx_id> --project ${opts.projectId}`,
}];
return new ApiError(opts.message, opts.status, {
error: opts.message,
message: opts.message,
code: opts.code,
category: "email",
retryable: false,
safe_to_retry: true,
mutation_state: "none",
details: {
project_id: opts.projectId,
mailbox_settings: opts.settings ?? null,
candidates: summarizeMailboxCandidates(opts.mailboxes),
...(opts.details ?? {}),
},
next_actions,
}, opts.context);
}
function summarizeMailboxCandidates(mailboxes) {
return mailboxes.map((m) => ({
mailbox_id: m.mailbox_id,
slug: m.slug,
address: m.address,
status: m.status,
is_default_outbound: m.is_default_outbound ?? false,
is_auth_sender: m.is_auth_sender ?? false,
can_send: m.can_send,
send_blocked_reason: m.send_blocked_reason ?? null,
domain_kind: m.domain_kind,
managed_address: m.managed_address,
address_domain: m.address_domain,
custom_domain_ready: m.custom_domain_ready,
can_receive: m.can_receive,
}));
}
function encodePathSegment(value, name, context) {
assertNonEmptyString(value, name, context);
return encodeURIComponent(value);
}
function validateRegisterWebhookOptions(opts) {
if (!opts || typeof opts !== "object" || Array.isArray(opts)) {
throw new LocalError("r.email.webhooks.register(projectId, opts) requires an opts object with url and events.", "registering webhook");
}
assertHttpUrl(opts.url, "url", "registering webhook");
validateWebhookEvents(opts.events, "events", "registering webhook");
}
function validateUpdateWebhookOptions(opts) {
if (!opts || typeof opts !== "object" || Array.isArray(opts)) {
throw new LocalError("r.email.webhooks.update(projectId, webhookId, opts) requires an opts object.", "updating webhook");
}
const hasUrl = opts.url !== undefined;
const hasEvents = opts.events !== undefined;
if (hasUrl)
assertHttpUrl(opts.url, "url", "updating webhook");
if (hasEvents)
validateWebhookEvents(opts.events, "events", "updating webhook");
return { hasUrl, hasEvents };
}
function validateWebhookEvents(value, name, context) {
if (!Array.isArray(value) || value.length === 0) {
throw new LocalError(`${name} must be a non-empty array of email webhook events.`, context);
}
for (const event of value) {
assertStringInSet(event, WEBHOOK_EVENTS, name, context);
}
}
//# sourceMappingURL=email.js.map