@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,185 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { NotFoundError } from "@pithy-sh/core/src/error/pithyError";
5
+ import type { MessageBulkPushResponse } from "cloudflare/resources/queues/messages";
6
+ import { CloudflareNotConfiguredError, cloudflareRequest, reasonOf } from "../client/errors";
7
+ import { CloudflareManager, type CloudflareManagerConfig } from "../client/manager";
8
+
9
+ /** Config for the Queue manager: the shared client config plus the queue it targets by name. */
10
+ export interface QueueManagerConfig extends CloudflareManagerConfig {
11
+ /**
12
+ * The queue name. The REST API addresses queues by id, so the manager resolves this name to an
13
+ * id once (and caches it). Required for every operation.
14
+ */
15
+ queueName: string;
16
+ }
17
+
18
+ /** The per-batch outcome of a `sendBatches` run: a partial-failure report, never a thrown batch. */
19
+ export interface QueueBatchResult {
20
+ /** The caller-supplied number identifying this batch. */
21
+ batchNumber: number;
22
+ /** Whether the batch was sent. */
23
+ success: boolean;
24
+ /** The failure reason, present only when `success` is false. */
25
+ error?: string;
26
+ /** How many messages the batch carried. */
27
+ messageCount: number;
28
+ }
29
+
30
+ /** The aggregate result of a `sendBatches` run. */
31
+ export interface SendBatchesResult {
32
+ /** How many batches sent successfully. */
33
+ successCount: number;
34
+ /** How many batches failed. */
35
+ failureCount: number;
36
+ /** The per-batch outcomes, in input order (truncated if a non-continuing run stopped early). */
37
+ results: QueueBatchResult[];
38
+ }
39
+
40
+ /** One batch of messages to send, with bookkeeping for progress reporting. */
41
+ export interface QueueBatch<T> {
42
+ /** A number identifying this batch in callbacks and results. */
43
+ batchNumber: number;
44
+ /** How many messages the batch carries (for progress reporting; need not equal `messages.length`). */
45
+ messageCount: number;
46
+ /** The message bodies to enqueue. */
47
+ messages: T[];
48
+ }
49
+
50
+ /** Options governing a `sendBatches` run. Callbacks are pure reporting hooks — no I/O is built in. */
51
+ export interface SendBatchesOptions {
52
+ /** Called before each batch is sent. */
53
+ onProgress?: (batchNumber: number, totalBatches: number, messageCount: number) => void;
54
+ /** Called after a batch sends successfully. */
55
+ onSuccess?: (batchNumber: number, messageCount: number) => void;
56
+ /** Called when a batch fails. */
57
+ onError?: (batchNumber: number, error: string) => void;
58
+ /** Milliseconds to wait between batches. Defaults to 0 (no delay). */
59
+ delayBetweenBatches?: number;
60
+ /** Keep going after a batch fails (collecting results). When false, the first failure throws. */
61
+ continueOnError?: boolean;
62
+ }
63
+
64
+ /**
65
+ * Out-of-Worker Cloudflare Queues access over the REST API: resolve a queue by name and bulk-push
66
+ * messages from a CLI/CI/provisioning context. Inside a Worker you use the `Queue` binding directly
67
+ * — this manager is the REST counterpart, addressed by queue id (resolved from the name).
68
+ */
69
+ export class CloudflareQueueManager extends CloudflareManager {
70
+ private readonly queueName: string;
71
+
72
+ private queueId?: string;
73
+
74
+ constructor(config: QueueManagerConfig) {
75
+ super(config);
76
+ if (!config.queueName) {
77
+ throw new CloudflareNotConfiguredError({ detail: "Missing queueName for Queue REST access." });
78
+ }
79
+ this.queueName = config.queueName;
80
+ }
81
+
82
+ /** Resolve (and cache) the queue id for the configured name by listing the account's queues. */
83
+ async getQueueId(): Promise<string> {
84
+ if (this.queueId) return this.queueId;
85
+
86
+ const queues = await cloudflareRequest("list queues", async () => {
87
+ const response = await this.getClient().queues.list({ account_id: this.accountId });
88
+ return response.result;
89
+ });
90
+
91
+ const match = queues.find((queue) => queue.queue_name === this.queueName);
92
+ if (!match?.queue_id) {
93
+ const available = queues.map((queue) => queue.queue_name).join(", ");
94
+ throw new NotFoundError({
95
+ message: `Queue '${this.queueName}' not found.`,
96
+ detail: `Available queues: ${available || "(none)"}.`,
97
+ action: "Verify the queue name and that the API token has Queues access.",
98
+ });
99
+ }
100
+
101
+ this.queueId = match.queue_id;
102
+ return this.queueId;
103
+ }
104
+
105
+ /** Bulk-push a batch of message bodies to the queue. Each item becomes one queue message. */
106
+ async sendBatch<T>(messages: T[]): Promise<MessageBulkPushResponse> {
107
+ const queueId = await this.getQueueId();
108
+ return cloudflareRequest(`send batch of ${messages.length} message(s)`, async () => {
109
+ // The SDK unwraps the API envelope and throws an `APIError` when `success` is false, so a
110
+ // resolved response is already a success — `cloudflareRequest` converts any throw for us.
111
+ return await this.getClient().queues.messages.bulkPush(queueId, {
112
+ account_id: this.accountId,
113
+ messages: messages.map((body) => ({ body: body as unknown })),
114
+ });
115
+ });
116
+ }
117
+
118
+ /** Send a single message. Convenience wrapper over `sendBatch`. */
119
+ async send<T>(message: T): Promise<void> {
120
+ await this.sendBatch([message]);
121
+ }
122
+
123
+ /**
124
+ * Send many batches in sequence, collecting a per-batch result. By default it continues past a
125
+ * failed batch (reporting it) rather than aborting; set `continueOnError: false` to throw on the
126
+ * first failure. Optional callbacks are pure reporting hooks; nothing is logged or prompted.
127
+ */
128
+ async sendBatches<T>(batches: QueueBatch<T>[], options: SendBatchesOptions = {}): Promise<SendBatchesResult> {
129
+ const { onProgress, onSuccess, onError, delayBetweenBatches = 0, continueOnError = true } = options;
130
+
131
+ // Resolve the queue id once so a misconfigured queue fails before any batch is attempted.
132
+ await this.getQueueId();
133
+
134
+ let successCount = 0;
135
+ let failureCount = 0;
136
+ const results: QueueBatchResult[] = [];
137
+
138
+ for (const batch of batches) {
139
+ onProgress?.(batch.batchNumber, batches.length, batch.messageCount);
140
+ try {
141
+ await this.sendBatch(batch.messages);
142
+ onSuccess?.(batch.batchNumber, batch.messageCount);
143
+ successCount++;
144
+ results.push({ batchNumber: batch.batchNumber, success: true, messageCount: batch.messageCount });
145
+ } catch (error) {
146
+ const reason = reasonOf(error);
147
+ onError?.(batch.batchNumber, reason);
148
+ failureCount++;
149
+ results.push({
150
+ batchNumber: batch.batchNumber,
151
+ success: false,
152
+ error: reason,
153
+ messageCount: batch.messageCount,
154
+ });
155
+ if (!continueOnError) throw error;
156
+ }
157
+
158
+ const isLast = batch === batches[batches.length - 1];
159
+ if (!isLast && delayBetweenBatches > 0) {
160
+ await new Promise((resolve) => setTimeout(resolve, delayBetweenBatches));
161
+ }
162
+ }
163
+
164
+ return { successCount, failureCount, results };
165
+ }
166
+
167
+ /** The queue this manager targets, the account it lives in, and the resolved id (once known). */
168
+ getQueueInfo(): { queueName: string; accountId: string; queueId?: string } {
169
+ return { queueName: this.queueName, accountId: this.accountId, queueId: this.queueId };
170
+ }
171
+
172
+ getServiceType(): string {
173
+ return "Queues";
174
+ }
175
+
176
+ /** Prove access by listing the account's queues. Never throws. */
177
+ async validateServiceAccess(): Promise<boolean> {
178
+ try {
179
+ await this.getClient().queues.list({ account_id: this.accountId });
180
+ return true;
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+ }
@@ -0,0 +1,17 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { z } from "zod";
5
+
6
+ /**
7
+ * The R2 S3-compatible credential pair. R2 speaks the S3 protocol, so presigned-URL signing needs
8
+ * an access-key/secret-key pair (distinct from the CF API token). This is the canonical shape —
9
+ * `@pithy-sh/secrets` validates the persisted credential envelope against it.
10
+ */
11
+ export const R2Credentials = z
12
+ .object({
13
+ accessKeyId: z.string().min(1).describe("The R2 S3 access key id."),
14
+ secretAccessKey: z.string().min(1).describe("The R2 S3 secret access key. Never logged or serialized to clients."),
15
+ })
16
+ .describe("The S3-compatible access-key/secret-key pair R2 uses to sign presigned URLs.");
17
+ export type R2Credentials = z.infer<typeof R2Credentials>;