import { z } from "zod/v4"; /** * Inbound HTTP endpoints: how external services (Stripe, GitHub, a form * provider, another company's backend) call INTO an app that has no server. * * Every install gets a private, unguessable URL per endpoint. Coline * receives the request, verifies it server-side with the declared scheme * (the verification secret is an install secret the app never sees), and * only then dispatches it to the app's `onRequest` handler — hosted or * external. `async` endpoints acknowledge with 202 immediately and run the * handler as a durable, retried job; `sync` endpoints wait for the handler * and return its response. */ export const secretKeySchema = z .string() .trim() .regex(/^[A-Z][A-Z0-9_]*$/, "Secret names are UPPER_SNAKE_CASE.") .max(120); export const ENDPOINT_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const; export const endpointMethodSchema = z.enum(ENDPOINT_METHODS); export type EndpointMethod = z.infer; const headerNameSchema = z .string() .trim() .toLowerCase() .regex(/^[a-z0-9-]+$/, "Header names are lowercase letters, digits, and dashes.") .max(120); export const endpointVerificationSchema = z.discriminatedUnion("kind", [ /** No verification beyond the unguessable URL. Review flags this. */ z.strictObject({ kind: z.literal("none") }), /** * A shared token, compared in constant time. Read from `header` * (default `authorization`, with an optional `Bearer ` prefix) or from * the `query` parameter when one is named. */ z.strictObject({ kind: z.literal("token"), secret: secretKeySchema, header: headerNameSchema.optional(), query: z.string().trim().min(1).max(120).optional(), }), /** Generic HMAC over the raw body (optionally prefixed by a timestamp). */ z.strictObject({ kind: z.literal("hmac"), secret: secretKeySchema, header: headerNameSchema, algorithm: z.enum(["sha256", "sha1", "sha512"]).default("sha256"), encoding: z.enum(["hex", "base64"]).default("hex"), /** Literal prefix on the header value, e.g. `sha256=`. */ prefix: z.string().max(40).optional(), /** When set, the signed payload is `${timestamp}.${body}` and stale timestamps are rejected. */ timestampHeader: headerNameSchema.optional(), }), /** Provider presets with their exact signature formats. */ z.strictObject({ kind: z.literal("stripe"), secret: secretKeySchema }), z.strictObject({ kind: z.literal("github"), secret: secretKeySchema }), z.strictObject({ kind: z.literal("slack"), secret: secretKeySchema }), z.strictObject({ kind: z.literal("shopify"), secret: secretKeySchema }), z.strictObject({ kind: z.literal("linear"), secret: secretKeySchema }), ]); export type EndpointVerification = z.infer; export const endpointIdSchema = z .string() .trim() .regex(/^[a-z][a-z0-9-]*$/, "Endpoint ids are lowercase kebab-case.") .max(80); export const appEndpointSchema = z.strictObject({ id: endpointIdSchema, description: z.string().trim().max(500).optional(), methods: z.array(endpointMethodSchema).min(1).max(5).default(["POST"]), verify: endpointVerificationSchema, /** `async`: 202 now, durable retried handler run. `sync`: wait (≤25s) and relay the response. */ mode: z.enum(["async", "sync"]).default("async"), /** Largest accepted request body. */ maxBodyBytes: z .number() .int() .min(1_024) .max(5_000_000) .default(1_000_000), }); export type AppEndpoint = z.infer; export type AppEndpointInput = z.input; export const appEndpointsSchema = z .array(appEndpointSchema) .max(20) .default([]) .refine( (endpoints) => new Set(endpoints.map((endpoint) => endpoint.id)).size === endpoints.length, "Endpoint ids must be unique.", );