import type { FormField, FormFieldValues } from "@automate.ax/catalog" import type { Encodable } from "@automate.ax/codec" import type { HttpRequestBody, HttpRequestBodyType, } from "@automate.ax/codec/http" import type { StandardSchemaV1 } from "@standard-schema/spec" import { type DashboardRunConfig, dashboardRunTriggerDefinition, } from "@automate.ax/catalog/triggers/core-dashboard" import { getHttpTriggerEndpoint, httpRequestTriggerDefinition, type HttpTriggerScope, } from "@automate.ax/catalog/triggers/core-http" import { automationInvokedTriggerDefinition } from "@automate.ax/catalog/triggers/core-invocation" import { getMailhookAddress, mailhookEmailReceivedTriggerDefinition, type MailhookScope, } from "@automate.ax/catalog/triggers/core-mailhook" import { cronTickTriggerDefinition } from "@automate.ax/catalog/triggers/core-schedule" import { getAutomationInvocationEnvironment, getAutomationPlanningContext, getNextHookLocation, } from "../../automation/runtime" import { subscribe } from "../../automation/subscribe" import { type Signal, transform, withSignalStaticProperties, } from "../../automation/signal-protocol" import { subscriptionEventId } from "../../automation/signal-resolution" import { AUTOMATION_INVOCATION_EVENT_TYPE, userInvocationEntrypointSchema, } from "./invocation" import { decodeHttpRequestEvent, type HttpRequestPayload } from "./http-request" export interface ScheduleConfig { /** Five-field cron expression describing the schedule. */ schedule: string /** IANA time zone used to evaluate the schedule. Defaults to UTC. */ timeZone?: string } export type ScheduleEvent = { /** Exact scheduled time represented by this event. */ scheduledAt: Date } export type HttpRequestMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" type HttpRequestSubscriptionConfig = | { /** Require a signed URL created by an action in this automation. */ protection: "callback" /** Protected callbacks always use their unique trigger endpoint. */ scope?: "trigger" waitForResponse?: boolean } | { protection?: never /** Scope at which HTTP requests are received. Defaults to this trigger. */ scope?: HttpTriggerScope waitForResponse?: boolean } export type HttpRequestConfig< TBodySchema extends StandardSchemaV1 | undefined = undefined, > = HttpRequestSubscriptionConfig & (TBodySchema extends StandardSchemaV1 ? { /** Standard Schema validator applied to the decoded request body. */ body: TBodySchema } : { body?: never }) export type HttpRequestEvent = { /** Request body decoded according to its content type. */ body: TBody /** Representation selected from the request's content type. */ bodyType: HttpRequestBodyType headers: Record method: HttpRequestMethod path: string query: Record /** Exact request bytes, preserved for signature verification. */ rawBody: Uint8Array /** Stable identity passed to and emitted by `respondToHttpRequest`. */ requestId: string url: string } export type HttpRequestTrigger = Signal< HttpRequestEvent > & { /** Public URL that receives requests for this trigger. */ readonly endpoint: string } export type { HttpRequestBody, HttpRequestBodyType, HttpRequestForm, HttpRequestFormValue, } from "@automate.ax/codec/http" export interface MailhookConfig { /** Scope at which the inbound address is shared. Defaults to this trigger. */ scope?: MailhookScope } export interface MailhookEvent { /** Other To, Cc, and Bcc recipients included on the message. */ additionalRecipients: string[] /** Attached files, including inline MIME parts. */ attachments: File[] /** Original message headers normalized to lowercase names. */ headers: Record /** HTML message body, when present. */ html: string | null /** * RFC 5322 Message-ID of this message's parent, from its In-Reply-To header. * Use `messageId`, not this value, when replying to this received message. */ inReplyTo: string | null /** Whether the message has an In-Reply-To header. */ isReply: boolean /** * On-the-wire RFC 5322 Message-ID supplied by the sender. Use this value as * In-Reply-To when replying to this received message. */ messageId: string /** Routing suffix after `+` in the recipient address. */ plusPath: string | null /** When Resend received the message. */ receivedAt: Date /** * On-the-wire RFC 5322 Message-IDs in the conversation ancestry. Append this * message's `messageId` when building a reply's References header. When this * is empty, start with `inReplyTo` if present. */ references: string[] /** Sender address, when supplied. */ senderAddress: string | null /** Sender display name, when supplied. */ senderName: string | null /** Message subject. */ subject: string /** Plain-text message body, when present. */ text: string | null } export type MailhookTrigger = Signal & { /** Unique inbound address for this mailhook scope. */ readonly address: string } export type DashboardRunValues = FormFieldValues export interface DashboardRunUser { /** Authenticated member's email address. */ email: string /** Authenticated member's user ID. */ id: string /** Authenticated member's display name. */ name: string } export interface DashboardRunEvent { /** Values collected from the configured fields. */ data: DashboardRunValues /** Time at which Automate.ax started the run. */ triggeredAt: Date /** Organization member who started the run. */ triggeredBy: DashboardRunUser } export type DashboardRunTrigger = Signal< DashboardRunEvent > export type { DashboardRunConfig } from "@automate.ax/catalog/triggers/core-dashboard" export type { FormField } from "@automate.ax/catalog" /** * Subscribes the current automation to a recurring cron schedule. * * @param config - Cron expression and optional IANA time zone. */ export function onSchedule(config: ScheduleConfig) { return subscribe(cronTickTriggerDefinition, config) } /** * Subscribes the current automation to HTTP requests. * * @param config - HTTP request subscription options. * @throws When declared outside an automation invocation. */ export function onHttpRequest( config: HttpRequestConfig, ): HttpRequestTrigger> /** @inheritdoc */ export function onHttpRequest(config?: HttpRequestConfig): HttpRequestTrigger export function onHttpRequest( config: HttpRequestSubscriptionConfig & { body?: StandardSchemaV1 } = {}, ): HttpRequestTrigger { const environment = getAutomationInvocationEnvironment() const location = getNextHookLocation() if (!environment || !location) { throw new Error( "HTTP triggers must be declared inside an automation invocation.", ) } const { body: bodySchema, ...subscriptionConfig } = config const request = subscribe( httpRequestTriggerDefinition, subscriptionConfig, { endpoint: getHttpTriggerEndpoint({ ...environment, hookSlot: location.slot, scope: subscriptionConfig.scope ?? "trigger", scopePath: location.scopePath, }), }, ) // Keep decoding separate so request addressing remains independent of body decoding. const decoded = transform(request, (event) => decodeHttpRequestEvent(event, bodySchema), ) const requestId = subscriptionEventId(request) return withSignalStaticProperties( transform([decoded, requestId], (event, resolvedRequestId) => ({ ...event, requestId: resolvedRequestId, })), { endpoint: request.endpoint, requestId }, ) } /** * Subscribes the current automation to email received at a unique address. * * Append `+suffix` before the `@` to receive the suffix as `plusPath`. Use the * trigger's address as `sendEmail({ replyTo })` to route replies back here. * * @param config - Mailhook address scope. * @throws When declared outside an automation invocation. */ export function onMailhook(config: MailhookConfig = {}): MailhookTrigger { const environment = getAutomationInvocationEnvironment() const location = getNextHookLocation() if (!environment || !location) { throw new Error( "Mailhook triggers must be declared inside an automation invocation.", ) } return subscribe( mailhookEmailReceivedTriggerDefinition, config, { address: getMailhookAddress({ ...environment, hookSlot: location.slot, scope: config.scope ?? "trigger", scopePath: location.scopePath, }), }, ) } /** * Adds an authenticated dashboard run form to the current automation. * * Only authenticated members of the owning organization can submit the run from * the Automations page. * * @param config - Form content and ordered field definitions. * @throws When declared outside an automation invocation. */ export function onDashboardRun( config: DashboardRunConfig, ): DashboardRunTrigger { if (!getNextHookLocation()) { throw new Error( "Dashboard run triggers must be declared inside an automation invocation.", ) } return subscribe>( dashboardRunTriggerDefinition, config, ) } /** * Subscribes the current automation to typed programmatic invocations. * * The type argument is compile-time only; invocation payloads accept any value * supported by the Automate.ax codec. Omit the entrypoint to use `"default"`. * Entrypoints beginning with `__$` are reserved by Automate.ax. * * @param entrypoint - Static name used to route invocations to this trigger. * @throws When the entrypoint is invalid, reserved, or already defined. */ export function onInvocation(entrypoint?: string) { const normalizedEntrypoint = userInvocationEntrypointSchema.parse(entrypoint) if ( getAutomationPlanningContext()?.subscriptions.some( (subscription) => subscription.eventType === AUTOMATION_INVOCATION_EVENT_TYPE && typeof subscription.config === "object" && subscription.config !== null && "entrypoint" in subscription.config && subscription.config.entrypoint === normalizedEntrypoint, ) ) { throw new Error( `Invocation entrypoint ${JSON.stringify(normalizedEntrypoint)} is already defined by this automation.`, ) } return subscribe(automationInvokedTriggerDefinition, { entrypoint: normalizedEntrypoint, }) }