import { z } from "zod/v4"; import { secretKeySchema } from "./endpoints"; /** * Account connections: OAuth 2.0 accounts on external services, held by * Coline and used by the app without the app ever seeing a token. * * `scope: "user"` gives every member their own connection (each person's * GitHub); `scope: "workspace"` is one shared connection an admin makes. * Coline runs the authorization-code flow (PKCE by default) in a host * window, stores tokens encrypted, refreshes them, and injects the access * token into `net.fetch` headers through `{{connection:KEY}}` placeholders * — only for requests to the connection's declared hosts. * * The OAuth client secret is a developer secret (set once in the developer * console), never an install secret, so store apps work in every * workspace without each admin registering an OAuth client. */ export const connectionKeySchema = z .string() .trim() .regex(/^[a-z][a-z0-9_-]*$/, "Connection keys are lowercase identifiers.") .max(60); const httpsUrlSchema = z .url() .max(2000) .refine((value) => value.startsWith("https://"), "OAuth URLs must use https."); const hostSchema = z .string() .trim() .toLowerCase() .regex( /^(\*\.)?([a-z0-9-]+\.)+[a-z]{2,}$/, "Connection hosts are domains, optionally with a leading `*.` wildcard.", ) .max(253); export const appConnectionSchema = z.strictObject({ key: connectionKeySchema, name: z.string().trim().min(1).max(120), description: z.string().trim().max(500).optional(), scope: z.enum(["user", "workspace"]).default("user"), /** Hosts this connection's token may be sent to. Must be in `network.allowedHosts`. */ hosts: z.array(hostSchema).min(1).max(10), oauth2: z.strictObject({ authorizeUrl: httpsUrlSchema, tokenUrl: httpsUrlSchema, clientId: z.string().trim().min(1).max(500), /** Developer secret holding the OAuth client secret. Omit for public PKCE clients. */ clientSecret: secretKeySchema.optional(), scopes: z.array(z.string().trim().min(1).max(200)).max(50).default([]), scopeSeparator: z.string().max(4).default(" "), pkce: z.boolean().default(true), /** Extra query parameters for the authorize URL (e.g. `access_type: offline`). */ authorizeParams: z.record(z.string().max(80), z.string().max(500)).default({}), /** How the client authenticates at the token endpoint. */ tokenAuth: z.enum(["body", "basic"]).default("body"), /** Optional endpoint returning the connected account, for display. */ accountUrl: httpsUrlSchema.optional(), /** Dotted path into the account response used as the account label. */ accountLabelPath: z.string().trim().max(200).optional(), }), }); export type AppConnection = z.infer; export type AppConnectionInput = z.input; export const appConnectionsSchema = z .array(appConnectionSchema) .max(10) .default([]) .refine( (connections) => new Set(connections.map((connection) => connection.key)).size === connections.length, "Connection keys must be unique.", );