The Coline App SDK: apps, tools, file types and surfaces that run inside Coline workspaces, and public websites with Coline Sites. TypeScript, with an in-memory test workspace. MIT.
| 5 | * Wire payloads for the execution kinds added after Phase 6, carried in |
| 6 | * the protocol-2 envelope. Each one is dispatched by the platform to the |
| 7 | * app's logic — the hosted Dynamic Worker or the external backend's |
| 8 | * `/coline/app` handler — with a per-run invocation token. |
| 9 | */ |
| 10 |
| 11 | const actorSchema = z.discriminatedUnion("kind", [ |
| 12 | z.strictObject({ kind: z.literal("user"), userId: z.string().min(1).max(200) }), |
| 13 | z.strictObject({ kind: z.literal("app") }), |
| 14 | ]); |
| 15 |
| 16 | export const TRIGGER_EVENT_KIND = "trigger.event" as const; |
| 17 |
| 18 | export const triggerEventPayloadSchema = z.strictObject({ |
| 19 | event: z.strictObject({ |
| 20 | id: z.string().min(1).max(200), |
| 21 | type: z.string().min(1).max(160), |
| 22 | occurredAt: z.iso.datetime(), |
| 23 | actor: actorSchema.nullable(), |
| 24 | source: z.discriminatedUnion("kind", [ |
| 25 | z.strictObject({ kind: z.literal("platform") }), |
| 26 | z.strictObject({ kind: z.literal("app"), appKey: z.string().min(1).max(64) }), |
| 27 | ]), |
| 28 | data: z.record(z.string(), z.unknown()), |
| 29 | }), |
| 30 | attempt: z.number().int().min(1), |
| 31 | capabilities: toolInvokeCapabilitiesSchema, |
| 32 | }); |
| 33 | export type TriggerEventPayload = z.infer<typeof triggerEventPayloadSchema>; |
| 34 |
| 35 | export const ENDPOINT_REQUEST_KIND = "endpoint.request" as const; |
| 36 |
| 37 | export const endpointRequestPayloadSchema = z.strictObject({ |
| 38 | request: z.strictObject({ |
| 39 | endpointId: z.string().min(1).max(80), |
| 40 | method: z.string().min(1).max(10), |
| 41 | path: z.string().min(1).max(2000), |
| 42 | query: z.record(z.string(), z.string()), |
| 43 | headers: z.record(z.string(), z.string()), |
| 44 | body: z.string(), |
| 45 | bodyBase64: z.string().nullable(), |
| 46 | receivedAt: z.iso.datetime(), |
| 47 | verifiedBy: z.string().min(1).max(40), |
| 48 | }), |
| 49 | capabilities: toolInvokeCapabilitiesSchema, |
| 50 | }); |
| 51 | export type EndpointRequestPayload = z.infer<typeof endpointRequestPayloadSchema>; |
| 52 |
| 53 | export const endpointResponseSchema = z.strictObject({ |
| 54 | status: z.number().int().min(100).max(599).default(200), |
| 55 | headers: z.record(z.string().max(120), z.string().max(4000)).default({}), |
| 56 | body: z.unknown().optional(), |
| 57 | }); |
| 58 | export type EndpointResponse = z.infer<typeof endpointResponseSchema>; |
| 59 |
| 60 | export const JOB_RUN_KIND = "job.run" as const; |
| 61 |
| 62 | export const jobRunPayloadSchema = z.strictObject({ |
| 63 | job: z.strictObject({ |
| 64 | id: z.string().min(1).max(200), |
| 65 | name: z.string().min(1).max(80), |
| 66 | attempt: z.number().int().min(1), |
| 67 | maxAttempts: z.number().int().min(1), |
| 68 | enqueuedAt: z.iso.datetime(), |
| 69 | checkpoint: z.unknown(), |
| 70 | payload: z.record(z.string(), z.unknown()), |
| 71 | }), |
| 72 | actor: actorSchema, |
| 73 | capabilities: toolInvokeCapabilitiesSchema, |
| 74 | }); |
| 75 | export type JobRunPayload = z.infer<typeof jobRunPayloadSchema>; |
| 76 |
| 77 | export const jobRunResultSchema = z.union([ |
| 78 | z.strictObject({ kind: z.literal("done"), result: z.unknown().optional() }), |
| 79 | z.strictObject({ |
| 80 | kind: z.literal("continue"), |
| 81 | payload: z.record(z.string(), z.unknown()), |
| 82 | delaySeconds: z.number().int().min(0).max(86_400).optional(), |
| 83 | }), |
| 84 | ]); |
| 85 | export type JobRunResult = z.infer<typeof jobRunResultSchema>; |
| 86 |
| 87 | /** Convert a job's return value into the wire result. */ |
| 88 | export function toJobRunResult(outcome: unknown): JobRunResult { |
| 89 | if (outcome && typeof outcome === "object" && "continueWith" in outcome) { |
| 90 | const value = outcome as { continueWith: Record<string, unknown>; delaySeconds?: number }; |
| 91 | return { |
| 92 | kind: "continue", |
| 93 | payload: value.continueWith, |
| 94 | ...(value.delaySeconds !== undefined ? { delaySeconds: value.delaySeconds } : {}), |
| 95 | }; |
| 96 | } |
| 97 | if (outcome && typeof outcome === "object" && "result" in outcome) { |
| 98 | return { kind: "done", result: (outcome as { result?: unknown }).result }; |
| 99 | } |
| 100 | return { kind: "done" }; |
| 101 | } |
| 102 |
| 103 | /** Normalize a handler's endpoint response for the wire. */ |
| 104 | export function toEndpointResponse(value: unknown): EndpointResponse { |
| 105 | if (!value || typeof value !== "object") return { status: 200, headers: {} }; |
| 106 | const parsed = endpointResponseSchema.safeParse(value); |
| 107 | if (!parsed.success) { |
| 108 | throw Object.assign(new Error("Endpoint handler returned an invalid response."), { |
| 109 | code: "invalid_response", |
| 110 | }); |
| 111 | } |
| 112 | return parsed.data; |
| 113 | } |
| 114 |