@pithy-sh/cloudflare 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -0
  3. package/package.json +48 -0
  4. package/src/ai/aiManager.ts +227 -0
  5. package/src/ai/vectorizeManager.ts +161 -0
  6. package/src/ai/vectorizeProvisioner.ts +266 -0
  7. package/src/client/accounts.ts +80 -0
  8. package/src/client/clients.ts +244 -0
  9. package/src/client/errors.ts +143 -0
  10. package/src/client/manager.ts +85 -0
  11. package/src/d1/d1Manager.ts +171 -0
  12. package/src/d1/d1PreparedStatement.ts +114 -0
  13. package/src/d1/d1Provisioner.ts +75 -0
  14. package/src/email/emailRoutingManager.ts +143 -0
  15. package/src/email/emailSendManager.ts +81 -0
  16. package/src/env/devVars.ts +90 -0
  17. package/src/hostnames/customHostnamesManager.ts +134 -0
  18. package/src/kv/kvManager.ts +202 -0
  19. package/src/kv/kvProvisioner.ts +80 -0
  20. package/src/media/assetSeeder.ts +87 -0
  21. package/src/media/imageManager.ts +125 -0
  22. package/src/media/ownership.ts +59 -0
  23. package/src/media/streamManager.ts +198 -0
  24. package/src/queue/queueManager.ts +185 -0
  25. package/src/r2/r2Credentials.ts +17 -0
  26. package/src/r2/r2Manager.ts +548 -0
  27. package/src/r2/r2Provisioner.ts +99 -0
  28. package/src/secrets/secretsStoreManager.ts +177 -0
  29. package/src/secrets/secretsStores.ts +75 -0
  30. package/src/test-utils/emailRoutingRules.ts +122 -0
  31. package/src/test-utils/fixtureReportSetup.ts +31 -0
  32. package/src/test-utils/fixtures.ts +372 -0
  33. package/src/test-utils/harness.ts +413 -0
  34. package/src/test-utils/inboundRecorder.ts +189 -0
  35. package/src/test-utils/integrationSetup.ts +46 -0
  36. package/src/test-utils/reap.ts +297 -0
  37. package/src/tokens/accountTokensManager.ts +334 -0
  38. package/src/tokens/permissions.ts +67 -0
  39. package/src/tokens/profiles.ts +238 -0
  40. package/src/turnstile/turnstileManager.ts +177 -0
  41. package/src/user/userManager.ts +73 -0
  42. package/src/workers/buildsManager.ts +348 -0
  43. package/src/workers/buildsTypes.ts +122 -0
  44. package/src/workers/workersBuildEvent.ts +48 -0
  45. package/src/workers/workersManager.ts +423 -0
  46. package/src/workers/workersProvisioner.ts +167 -0
  47. package/src/workflows/stepFailure.ts +280 -0
  48. package/src/workflows/workflowsClient.ts +213 -0
  49. package/src/zones/zonesManager.ts +92 -0
@@ -0,0 +1,348 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { z } from "zod";
5
+ import {
6
+ CloudflareInvalidResponseError,
7
+ CloudflareRequestError,
8
+ cloudflareRequest,
9
+ decodeResponse,
10
+ messageOf,
11
+ } from "../client/errors";
12
+ import { CloudflareManager } from "../client/manager";
13
+ import {
14
+ CfBuild,
15
+ CfBuildLog,
16
+ CfBuildToken,
17
+ CfBuildTrigger,
18
+ CfRepoConnection,
19
+ type CfTriggerEnvVar,
20
+ } from "./buildsTypes";
21
+
22
+ /** The Cloudflare REST API v4 base URL. The Builds endpoints live under `/accounts/{id}/builds`. */
23
+ const CF_API_BASE = "https://api.cloudflare.com/client/v4";
24
+
25
+ /** CF 409 code 12042: "A trigger already exists for this configuration". */
26
+ const CONFLICT_TRIGGER_EXISTS = 12042;
27
+
28
+ /** CF 404 code 12000: "Not found" — returned when a script has no triggers. */
29
+ const NOT_FOUND = 12000;
30
+
31
+ /** The standard `{ result, success, errors, messages }` envelope every CF REST response carries. */
32
+ const CfEnvelope = z.object({
33
+ result: z.unknown(),
34
+ success: z.boolean().optional(),
35
+ errors: z.array(z.object({ code: z.number().optional(), message: z.string().optional() }).loose()).optional(),
36
+ });
37
+
38
+ /** Arguments for `createRepoConnection`. */
39
+ export interface CreateRepoConnectionArgs {
40
+ /** The git provider hosting the repo. */
41
+ providerType: "gitlab" | "github";
42
+ /** The repository's id in the git provider. */
43
+ repoId: string;
44
+ /** The repo path relative to the provider account. */
45
+ repoName: string;
46
+ /** The provider account identifier (e.g. 'group:15366484'). */
47
+ providerAccountId: string;
48
+ /** The provider account display name. */
49
+ providerAccountName: string;
50
+ }
51
+
52
+ /** Arguments for `upsertTrigger`. */
53
+ export interface UpsertTriggerArgs {
54
+ /** The immutable Worker id (hex UUID) the trigger builds — not the worker name. */
55
+ scriptName: string;
56
+ /** The repo connection UUID to build from. */
57
+ repoConnectionId: string;
58
+ /** The build token UUID the trigger authenticates with. */
59
+ buildTokenUuid: string;
60
+ /** The trigger's human-readable name. */
61
+ triggerName: string;
62
+ /** Branch patterns whose pushes auto-fire the trigger (must be non-empty). */
63
+ branchIncludes: string[];
64
+ /** Branch patterns excluded from auto-firing. */
65
+ branchExcludes?: string[];
66
+ /** Path patterns whose changes auto-fire the trigger. */
67
+ pathIncludes?: string[];
68
+ /** The shell command that builds the Worker. */
69
+ buildCommand: string;
70
+ /** The shell command CF Builds runs to deploy after a build. */
71
+ deployCommand: string;
72
+ /** The repo subdirectory builds run from. Defaults to '/'. */
73
+ rootDirectory?: string;
74
+ /** Whether build caching is enabled. Defaults to true. */
75
+ buildCachingEnabled?: boolean;
76
+ }
77
+
78
+ /** A subset of trigger fields that can be patched after creation. */
79
+ export interface UpdateTriggerArgs {
80
+ /** A branch filter to set on the trigger. */
81
+ branchFilter?: string;
82
+ /** The build command to set. */
83
+ buildCommand?: string;
84
+ /** The deploy command to set. */
85
+ deployCommand?: string;
86
+ /** The root directory to set. */
87
+ rootDirectory?: string;
88
+ }
89
+
90
+ /**
91
+ * Out-of-Worker access to the Cloudflare Builds API: repo connections, build triggers, builds, and
92
+ * trigger env vars. CF Builds is the CI/CD layer for Workers — it clones a git repo, runs a build
93
+ * command, and reports outcomes.
94
+ *
95
+ * As of the SDK version this package targets, the Builds endpoints are NOT exposed as typed SDK
96
+ * resources, so this manager uses the documented escape hatch (`getApiToken()` + raw `fetch`) against
97
+ * explicit `/accounts/{id}/builds/...` paths. Every request goes through `cloudflareRequest` so
98
+ * failures wrap as `cloudflare/request_failed`, and every response is validated with Zod
99
+ * (`cloudflare/invalid_response` on mismatch).
100
+ */
101
+ export class CloudflareBuildsManager extends CloudflareManager {
102
+ /**
103
+ * Issue a raw CF REST call and unwrap the `{ result, success, errors }` envelope. A failure —
104
+ * either a non-2xx status **or** a 200 carrying `success: false` — throws `cloudflare/request_failed`
105
+ * with the structured CF error codes pinned to the front of `detail` (so idempotency checks read a
106
+ * stable marker, never a substring a verbose body could push past truncation). An unparseable body
107
+ * throws `cloudflare/invalid_response`.
108
+ */
109
+ private async call(
110
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
111
+ path: string,
112
+ body?: unknown,
113
+ ): Promise<unknown> {
114
+ const response = await fetch(`${CF_API_BASE}${path}`, {
115
+ method,
116
+ headers: {
117
+ authorization: `Bearer ${this.getApiToken()}`,
118
+ ...(body !== undefined ? { "content-type": "application/json" } : {}),
119
+ },
120
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
121
+ });
122
+
123
+ const text = await response.text();
124
+
125
+ let parsed: unknown = {};
126
+ if (text.length > 0) {
127
+ try {
128
+ parsed = JSON.parse(text);
129
+ } catch (error) {
130
+ throw new CloudflareInvalidResponseError({
131
+ message: "A Cloudflare Builds response was not valid JSON.",
132
+ detail: `${method} ${path}: ${messageOf(error)}`,
133
+ });
134
+ }
135
+ }
136
+
137
+ const envelope = CfEnvelope.safeParse(parsed);
138
+ // CF signals failure two ways: a non-2xx status, or a 200 body with `success: false`.
139
+ const apiReportedFailure = envelope.success && envelope.data.success === false;
140
+ if (!response.ok || apiReportedFailure) {
141
+ const codes = envelope.success
142
+ ? (envelope.data.errors ?? []).map((e) => e.code).filter((c): c is number => c !== undefined)
143
+ : [];
144
+ const marker = codes.length > 0 ? `[cf-codes:${codes.join(",")}] ` : "";
145
+ throw new CloudflareRequestError({
146
+ message: `Cloudflare Builds returned ${response.status}.`,
147
+ detail: `${marker}${method} ${path} → ${response.status}: ${text}`.slice(0, 2000),
148
+ });
149
+ }
150
+
151
+ return envelope.success ? envelope.data.result : parsed;
152
+ }
153
+
154
+ /**
155
+ * Register a git repository with CF Builds. `PUT /builds/repos/connections` is CF's documented
156
+ * upsert — calling with the same repo id returns the existing connection.
157
+ */
158
+ async createRepoConnection(args: CreateRepoConnectionArgs): Promise<CfRepoConnection> {
159
+ return cloudflareRequest("create repo connection", async () => {
160
+ const result = await this.call("PUT", `/accounts/${this.getAccountId()}/builds/repos/connections`, {
161
+ provider_type: args.providerType,
162
+ repo_id: args.repoId,
163
+ repo_name: args.repoName,
164
+ provider_account_id: args.providerAccountId,
165
+ provider_account_name: args.providerAccountName,
166
+ });
167
+ return this.validate(CfRepoConnection, result, "repo connection");
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Upsert a build trigger linking a Worker script to a repo. Tries `POST /builds/triggers`; on the
173
+ * 409 "trigger already exists" conflict, falls back to listing the script's triggers and returning
174
+ * the matching one. Idempotent.
175
+ */
176
+ async upsertTrigger(args: UpsertTriggerArgs): Promise<CfBuildTrigger> {
177
+ return cloudflareRequest("upsert trigger", async () => {
178
+ try {
179
+ const result = await this.call("POST", `/accounts/${this.getAccountId()}/builds/triggers`, {
180
+ external_script_id: args.scriptName,
181
+ repo_connection_uuid: args.repoConnectionId,
182
+ build_token_uuid: args.buildTokenUuid,
183
+ trigger_name: args.triggerName,
184
+ branch_includes: args.branchIncludes,
185
+ branch_excludes: args.branchExcludes ?? [],
186
+ path_includes: args.pathIncludes ?? ["*"],
187
+ build_command: args.buildCommand,
188
+ deploy_command: args.deployCommand,
189
+ root_directory: args.rootDirectory ?? "/",
190
+ build_caching_enabled: args.buildCachingEnabled ?? true,
191
+ });
192
+ return this.validate(CfBuildTrigger, result, "build trigger");
193
+ } catch (error) {
194
+ if (hasErrorCode(error, CONFLICT_TRIGGER_EXISTS)) {
195
+ const existing = await this.listTriggersByScript(args.scriptName);
196
+ const match =
197
+ existing.find((t) => t.trigger_name === args.triggerName) ??
198
+ existing.find((t) => JSON.stringify(t.branch_includes) === JSON.stringify(args.branchIncludes));
199
+ if (match) return match;
200
+ }
201
+ throw error;
202
+ }
203
+ });
204
+ }
205
+
206
+ /** List the build tokens available on the account. */
207
+ async listBuildTokens(): Promise<CfBuildToken[]> {
208
+ return cloudflareRequest("list build tokens", async () => {
209
+ const result = await this.call("GET", `/accounts/${this.getAccountId()}/builds/tokens`);
210
+ return this.validate(z.array(CfBuildToken), Array.isArray(result) ? result : [], "build tokens");
211
+ });
212
+ }
213
+
214
+ /** Patch an existing build trigger's configuration. */
215
+ async updateTrigger(triggerId: string, args: UpdateTriggerArgs): Promise<CfBuildTrigger> {
216
+ return cloudflareRequest("update trigger", async () => {
217
+ const body: Record<string, string> = {};
218
+ if (args.branchFilter !== undefined) body.branch_filter = args.branchFilter;
219
+ if (args.buildCommand !== undefined) body.build_command = args.buildCommand;
220
+ if (args.deployCommand !== undefined) body.deploy_command = args.deployCommand;
221
+ if (args.rootDirectory !== undefined) body.root_directory = args.rootDirectory;
222
+ const result = await this.call("PUT", `/accounts/${this.getAccountId()}/builds/triggers/${triggerId}`, body);
223
+ return this.validate(CfBuildTrigger, result, "build trigger");
224
+ });
225
+ }
226
+
227
+ /** Delete a build trigger. */
228
+ async deleteTrigger(triggerId: string): Promise<void> {
229
+ await cloudflareRequest("delete trigger", () =>
230
+ this.call("DELETE", `/accounts/${this.getAccountId()}/builds/triggers/${triggerId}`),
231
+ );
232
+ }
233
+
234
+ /** Manually start a build for a trigger at a specific branch + commit. */
235
+ async triggerManualBuild(triggerId: string, branch: string, commitHash: string): Promise<CfBuild> {
236
+ return cloudflareRequest("trigger manual build", async () => {
237
+ const result = await this.call("POST", `/accounts/${this.getAccountId()}/builds/triggers/${triggerId}/builds`, {
238
+ branch,
239
+ commit_hash: commitHash,
240
+ });
241
+ return this.validate(CfBuild, result, "build");
242
+ });
243
+ }
244
+
245
+ /** Get a build's current status and metadata by id. */
246
+ async getBuild(buildId: string): Promise<CfBuild> {
247
+ return cloudflareRequest("get build", async () => {
248
+ const result = await this.call("GET", `/accounts/${this.getAccountId()}/builds/${buildId}`);
249
+ return this.validate(CfBuild, result, "build");
250
+ });
251
+ }
252
+
253
+ /** Cancel a running build. */
254
+ async cancelBuild(buildId: string): Promise<void> {
255
+ await cloudflareRequest("cancel build", () =>
256
+ this.call("POST", `/accounts/${this.getAccountId()}/builds/${buildId}/cancel`),
257
+ );
258
+ }
259
+
260
+ /** Get the log output for a build. */
261
+ async getBuildLogs(buildId: string): Promise<CfBuildLog[]> {
262
+ return cloudflareRequest("get build logs", async () => {
263
+ const result = await this.call("GET", `/accounts/${this.getAccountId()}/builds/${buildId}/logs`);
264
+ return this.validate(z.array(CfBuildLog), result, "build logs");
265
+ });
266
+ }
267
+
268
+ /** List all builds for a Worker script. */
269
+ async listBuildsByScript(scriptName: string): Promise<CfBuild[]> {
270
+ return cloudflareRequest("list builds by script", async () => {
271
+ const result = await this.call(
272
+ "GET",
273
+ `/accounts/${this.getAccountId()}/builds?script_name=${encodeURIComponent(scriptName)}`,
274
+ );
275
+ return this.validate(z.array(CfBuild), result, "builds");
276
+ });
277
+ }
278
+
279
+ /**
280
+ * List the build triggers for a Worker script. `externalScriptId` must be the immutable Worker id
281
+ * (hex UUID), resolved via `CloudflareWorkersManager.getWorkerInternalId`. Returns an empty array
282
+ * when the script has no triggers (CF 404 code 12000).
283
+ */
284
+ async listTriggersByScript(externalScriptId: string): Promise<CfBuildTrigger[]> {
285
+ return cloudflareRequest("list triggers by script", async () => {
286
+ try {
287
+ const result = await this.call(
288
+ "GET",
289
+ `/accounts/${this.getAccountId()}/builds/workers/${encodeURIComponent(externalScriptId)}/triggers`,
290
+ );
291
+ return this.validate(z.array(CfBuildTrigger), result, "build triggers");
292
+ } catch (error) {
293
+ if (hasErrorCode(error, NOT_FOUND)) return [];
294
+ throw error;
295
+ }
296
+ });
297
+ }
298
+
299
+ /**
300
+ * Bulk-upsert environment variables on a build trigger. The wire body is a map keyed by variable
301
+ * name (`{ NAME: { is_secret, value }, ... }`), not an array.
302
+ */
303
+ async upsertTriggerEnvVars(triggerId: string, envVars: CfTriggerEnvVar[]): Promise<void> {
304
+ await cloudflareRequest("upsert trigger env vars", () => {
305
+ const body: Record<string, { is_secret: boolean; value: string }> = {};
306
+ for (const variable of envVars) {
307
+ body[variable.name] = { is_secret: variable.type === "secret_text", value: variable.value };
308
+ }
309
+ return this.call(
310
+ "PATCH",
311
+ `/accounts/${this.getAccountId()}/builds/triggers/${triggerId}/environment_variables`,
312
+ body,
313
+ );
314
+ });
315
+ }
316
+
317
+ getServiceType(): string {
318
+ return "Cloudflare Builds";
319
+ }
320
+
321
+ /** Prove access by listing build triggers. Never throws. */
322
+ async validateServiceAccess(): Promise<boolean> {
323
+ try {
324
+ await this.call("GET", `/accounts/${this.getAccountId()}/builds/triggers`);
325
+ return true;
326
+ } catch {
327
+ return false;
328
+ }
329
+ }
330
+
331
+ /** Validate a raw CF result against a Zod schema, throwing `cloudflare/invalid_response` on mismatch. */
332
+ private validate<T extends z.ZodType>(schema: T, raw: unknown, label: string): z.output<T> {
333
+ return decodeResponse(schema, raw, `Builds ${label}`);
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Whether a thrown `CloudflareRequestError` carries the given CF error code. Reads the `[cf-codes:…]`
339
+ * marker `call()` pins to the front of `detail` (stable across the 2000-char truncation); falls back
340
+ * to a substring scan for errors raised without the structured marker.
341
+ */
342
+ function hasErrorCode(error: unknown, code: number): boolean {
343
+ if (!(error instanceof CloudflareRequestError)) return false;
344
+ const detail = error.payload.detail ?? "";
345
+ const marker = detail.match(/^\[cf-codes:([\d,]+)\]/);
346
+ if (marker?.[1]) return marker[1].split(",").includes(String(code));
347
+ return detail.includes(`"code":${code}`) || detail.includes(`"code": ${code}`);
348
+ }
@@ -0,0 +1,122 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { JsonDate } from "@pithy-sh/core/src/data/codecs";
5
+ import { z } from "zod";
6
+
7
+ /**
8
+ * Zod schemas for the Cloudflare Builds REST API.
9
+ *
10
+ * CF Builds provides CI/CD for Workers — it clones a git repo, runs a build command, and optionally
11
+ * runs a deploy command. As of the SDK version this package targets, the Builds endpoints are not
12
+ * exposed as typed SDK resources, so `buildsManager` reaches them over raw `fetch` (the documented
13
+ * escape hatch) and validates every response through the schemas here. ISO-string dates on the wire
14
+ * are decoded to `Date` via the `JsonDate` codec, so each dated schema round-trips through
15
+ * `.parse()`/`.encode()`.
16
+ */
17
+
18
+ /** A git-provider repository linked to the Cloudflare account for builds. */
19
+ export const CfRepoConnection = z
20
+ .object({
21
+ repo_connection_uuid: z.string().describe("The connection's UUID, used to attach build triggers."),
22
+ repo_id: z.string().describe("The repository's id in the git provider."),
23
+ repo_name: z.string().describe("The repo path relative to the provider account."),
24
+ provider_type: z.string().describe("The git provider (e.g. 'gitlab', 'github')."),
25
+ provider_account_id: z.string().describe("The provider account identifier (e.g. 'group:15366484')."),
26
+ provider_account_name: z.string().describe("The provider account display name."),
27
+ created_on: JsonDate.describe("When the connection was created (ISO-8601 on the wire, Date in JS)."),
28
+ modified_on: JsonDate.describe("When the connection was last modified (ISO-8601 on the wire, Date in JS)."),
29
+ deleted_on: JsonDate.nullish().describe("When the connection was deleted, if it has been."),
30
+ })
31
+ .describe("A git-provider repository connected to Cloudflare Builds.");
32
+ export type CfRepoConnection = z.output<typeof CfRepoConnection>;
33
+
34
+ /** A build trigger linking a Worker script to a repo connection and its build/deploy commands. */
35
+ export const CfBuildTrigger = z
36
+ .object({
37
+ trigger_uuid: z.string().describe("The trigger's UUID."),
38
+ external_script_id: z.string().describe("The immutable Worker id (hex UUID) this trigger builds."),
39
+ build_token_uuid: z.string().describe("The build token this trigger authenticates with."),
40
+ build_token_name: z.string().nullish().describe("The build token's display name, if returned."),
41
+ trigger_name: z.string().nullish().describe("The trigger's human-readable name."),
42
+ build_command: z.string().describe("The shell command that builds the Worker."),
43
+ deploy_command: z.string().describe("The shell command CF Builds runs to deploy after a build."),
44
+ root_directory: z.string().describe("The repo subdirectory builds run from."),
45
+ branch_includes: z.array(z.string()).describe("Branch patterns whose pushes auto-fire the trigger."),
46
+ branch_excludes: z.array(z.string()).nullish().describe("Branch patterns excluded from auto-firing."),
47
+ path_includes: z.array(z.string()).nullish().describe("Path patterns whose changes auto-fire the trigger."),
48
+ path_excludes: z.array(z.string()).nullish().describe("Path patterns excluded from auto-firing."),
49
+ build_caching_enabled: z.boolean().nullish().describe("Whether build caching is enabled for the trigger."),
50
+ created_on: JsonDate.describe("When the trigger was created (ISO-8601 on the wire, Date in JS)."),
51
+ modified_on: JsonDate.describe("When the trigger was last modified (ISO-8601 on the wire, Date in JS)."),
52
+ deleted_on: JsonDate.nullish().describe("When the trigger was deleted, if it has been."),
53
+ })
54
+ .describe("A Cloudflare Builds trigger binding a Worker script to a repo and build commands.");
55
+ export type CfBuildTrigger = z.output<typeof CfBuildTrigger>;
56
+
57
+ /** Metadata about the commit a build ran against. */
58
+ export const CfBuildTriggerMetadata = z
59
+ .object({
60
+ branch: z.string().describe("The git branch the build ran against."),
61
+ commit_hash: z.string().describe("The git commit SHA the build ran against."),
62
+ build_trigger_source: z.string().nullish().describe("How the build was initiated (push, manual, api, …)."),
63
+ })
64
+ .loose()
65
+ .describe("Commit/branch metadata attached to a build.");
66
+ export type CfBuildTriggerMetadata = z.output<typeof CfBuildTriggerMetadata>;
67
+
68
+ /** A back-reference from a build to the trigger that produced it. */
69
+ export const CfBuildTriggerRef = z
70
+ .object({
71
+ trigger_uuid: z.string().describe("The producing trigger's UUID."),
72
+ external_script_id: z.string().describe("The immutable Worker id the trigger targets."),
73
+ })
74
+ .loose()
75
+ .describe("A reference to the trigger that produced a build.");
76
+ export type CfBuildTriggerRef = z.output<typeof CfBuildTriggerRef>;
77
+
78
+ /** A single execution of a build trigger and its lifecycle state. */
79
+ export const CfBuild = z
80
+ .object({
81
+ build_uuid: z.string().describe("The build's identifier."),
82
+ status: z.string().describe("The build's lifecycle state (queued, initializing, running, stopped)."),
83
+ build_outcome: z.string().nullish().describe("The final result, present once the build completes."),
84
+ initializing_on: JsonDate.nullish().describe("When the build began initializing (ISO-8601 on the wire)."),
85
+ running_on: JsonDate.nullish().describe("When execution began (ISO-8601 on the wire, Date in JS)."),
86
+ stopped_on: JsonDate.nullish().describe("When the build finished (ISO-8601 on the wire, Date in JS)."),
87
+ created_on: JsonDate.describe("When the build was queued (ISO-8601 on the wire, Date in JS)."),
88
+ modified_on: JsonDate.describe("When the build's status last changed (ISO-8601 on the wire, Date in JS)."),
89
+ build_trigger_metadata: CfBuildTriggerMetadata.nullish().describe("Commit/branch metadata for the build."),
90
+ trigger: CfBuildTriggerRef.nullish().describe("The trigger that produced this build."),
91
+ })
92
+ .describe("A single Cloudflare Builds execution and its lifecycle state.");
93
+ export type CfBuild = z.output<typeof CfBuild>;
94
+
95
+ /** One line of a build's log output. */
96
+ export const CfBuildLog = z
97
+ .object({
98
+ line: z.number().describe("The log line number."),
99
+ timestamp: JsonDate.describe("When the line was emitted (ISO-8601 on the wire, Date in JS)."),
100
+ message: z.string().describe("The log message content."),
101
+ })
102
+ .describe("A single line of Cloudflare Builds log output.");
103
+ export type CfBuildLog = z.output<typeof CfBuildLog>;
104
+
105
+ /** An environment variable set on a build trigger. */
106
+ export const CfTriggerEnvVar = z
107
+ .object({
108
+ name: z.string().describe("The environment variable's name."),
109
+ value: z.string().describe("The environment variable's value."),
110
+ type: z.enum(["plain_text", "secret_text"]).describe("Whether the value is stored encrypted at rest."),
111
+ })
112
+ .describe("An environment variable set on a Cloudflare Builds trigger.");
113
+ export type CfTriggerEnvVar = z.output<typeof CfTriggerEnvVar>;
114
+
115
+ /** A build token usable by triggers on the account. */
116
+ export const CfBuildToken = z
117
+ .object({
118
+ build_token_uuid: z.string().describe("The build token's UUID."),
119
+ build_token_name: z.string().describe("The build token's display name."),
120
+ })
121
+ .describe("A Cloudflare Builds token available on the account.");
122
+ export type CfBuildToken = z.output<typeof CfBuildToken>;
@@ -0,0 +1,48 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { JsonDate } from "@pithy-sh/core/src/data/codecs";
5
+ import { z } from "zod";
6
+
7
+ /**
8
+ * The shape of a Cloudflare Workers Builds lifecycle event, as delivered to a subscribed queue.
9
+ * A subscription (created via `workersProvisioner.setupBuildEventSubscription`) pushes one of these
10
+ * each time a build changes state. Consumers validate the raw message through `WorkersBuildEvent`
11
+ * before trusting it; ISO-string dates decode to `Date` via `JsonDate`, so the schema round-trips.
12
+ */
13
+
14
+ /** Where a build's lifecycle currently sits. */
15
+ export const WorkersBuildStatus = z
16
+ .enum(["queued", "initializing", "running", "stopped"])
17
+ .describe("A Cloudflare Builds lifecycle state.");
18
+ export type WorkersBuildStatus = z.infer<typeof WorkersBuildStatus>;
19
+
20
+ /** The terminal result of a build, set once it reaches `stopped`. */
21
+ export const WorkersBuildOutcome = z
22
+ .enum(["success", "fail", "skipped", "cancelled", "terminated"])
23
+ .describe("The terminal outcome of a Cloudflare build.");
24
+ export type WorkersBuildOutcome = z.infer<typeof WorkersBuildOutcome>;
25
+
26
+ /** What kicked off a build. */
27
+ export const WorkersTriggerSource = z
28
+ .enum(["push", "pull_request", "manual", "api"])
29
+ .describe("What initiated a Cloudflare build.");
30
+ export type WorkersTriggerSource = z.infer<typeof WorkersTriggerSource>;
31
+
32
+ /** A build lifecycle event delivered to a subscribed queue. */
33
+ export const WorkersBuildEvent = z
34
+ .object({
35
+ build_id: z.string().describe("The build's unique identifier from CF Builds."),
36
+ script_name: z.string().describe("The Worker script this build targets."),
37
+ trigger_id: z.string().describe("The build trigger that initiated this build."),
38
+ trigger_source: WorkersTriggerSource.describe("How the build was initiated."),
39
+ status: WorkersBuildStatus.describe("The build's current lifecycle state."),
40
+ outcome: WorkersBuildOutcome.nullish().describe("The final result, set once status reaches 'stopped'."),
41
+ branch: z.string().describe("The git branch that was built."),
42
+ commit_hash: z.string().describe("The git commit SHA that was built."),
43
+ started_at: JsonDate.nullish().describe("When execution began (ISO-8601 on the wire, Date in JS)."),
44
+ completed_at: JsonDate.nullish().describe("When the build finished (ISO-8601 on the wire, Date in JS)."),
45
+ created_at: JsonDate.describe("When the build was queued (ISO-8601 on the wire, Date in JS)."),
46
+ })
47
+ .describe("A Cloudflare Workers Builds lifecycle event delivered to a subscribed queue.");
48
+ export type WorkersBuildEvent = z.output<typeof WorkersBuildEvent>;