@vibesdotdev/infra-cloudflare 0.0.1

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 (128) hide show
  1. package/README.md +107 -0
  2. package/SPEC.md +166 -0
  3. package/dist/cloudflare.plugin.d.ts +73 -0
  4. package/dist/cloudflare.plugin.d.ts.map +1 -0
  5. package/dist/cloudflare.plugin.js +334 -0
  6. package/dist/cloudflare.plugin.js.map +1 -0
  7. package/dist/implementations/alerts.descriptor.d.ts +13 -0
  8. package/dist/implementations/alerts.descriptor.d.ts.map +1 -0
  9. package/dist/implementations/alerts.descriptor.js +30 -0
  10. package/dist/implementations/alerts.descriptor.js.map +1 -0
  11. package/dist/implementations/alerts.impl.d.ts +35 -0
  12. package/dist/implementations/alerts.impl.d.ts.map +1 -0
  13. package/dist/implementations/alerts.impl.js +283 -0
  14. package/dist/implementations/alerts.impl.js.map +1 -0
  15. package/dist/implementations/kv.impl.d.ts +29 -0
  16. package/dist/implementations/kv.impl.d.ts.map +1 -0
  17. package/dist/implementations/kv.impl.js +36 -0
  18. package/dist/implementations/kv.impl.js.map +1 -0
  19. package/dist/implementations/logs.descriptor.d.ts +15 -0
  20. package/dist/implementations/logs.descriptor.d.ts.map +1 -0
  21. package/dist/implementations/logs.descriptor.js +26 -0
  22. package/dist/implementations/logs.descriptor.js.map +1 -0
  23. package/dist/implementations/logs.impl.d.ts +108 -0
  24. package/dist/implementations/logs.impl.d.ts.map +1 -0
  25. package/dist/implementations/logs.impl.js +154 -0
  26. package/dist/implementations/logs.impl.js.map +1 -0
  27. package/dist/implementations/observability.descriptor.d.ts +9 -0
  28. package/dist/implementations/observability.descriptor.d.ts.map +1 -0
  29. package/dist/implementations/observability.descriptor.js +22 -0
  30. package/dist/implementations/observability.descriptor.js.map +1 -0
  31. package/dist/implementations/observability.impl.d.ts +35 -0
  32. package/dist/implementations/observability.impl.d.ts.map +1 -0
  33. package/dist/implementations/observability.impl.js +229 -0
  34. package/dist/implementations/observability.impl.js.map +1 -0
  35. package/dist/implementations/pages.impl.d.ts +98 -0
  36. package/dist/implementations/pages.impl.d.ts.map +1 -0
  37. package/dist/implementations/pages.impl.js +132 -0
  38. package/dist/implementations/pages.impl.js.map +1 -0
  39. package/dist/implementations/queues.impl.d.ts +29 -0
  40. package/dist/implementations/queues.impl.d.ts.map +1 -0
  41. package/dist/implementations/queues.impl.js +34 -0
  42. package/dist/implementations/queues.impl.js.map +1 -0
  43. package/dist/implementations/r2.impl.d.ts +31 -0
  44. package/dist/implementations/r2.impl.d.ts.map +1 -0
  45. package/dist/implementations/r2.impl.js +41 -0
  46. package/dist/implementations/r2.impl.js.map +1 -0
  47. package/dist/implementations/rum.descriptor.d.ts +13 -0
  48. package/dist/implementations/rum.descriptor.d.ts.map +1 -0
  49. package/dist/implementations/rum.descriptor.js +32 -0
  50. package/dist/implementations/rum.descriptor.js.map +1 -0
  51. package/dist/implementations/rum.impl.d.ts +34 -0
  52. package/dist/implementations/rum.impl.d.ts.map +1 -0
  53. package/dist/implementations/rum.impl.js +153 -0
  54. package/dist/implementations/rum.impl.js.map +1 -0
  55. package/dist/implementations/web-app.impl.d.ts +294 -0
  56. package/dist/implementations/web-app.impl.d.ts.map +1 -0
  57. package/dist/implementations/web-app.impl.js +208 -0
  58. package/dist/implementations/web-app.impl.js.map +1 -0
  59. package/dist/implementations/workers.impl.d.ts +157 -0
  60. package/dist/implementations/workers.impl.d.ts.map +1 -0
  61. package/dist/implementations/workers.impl.js +247 -0
  62. package/dist/implementations/workers.impl.js.map +1 -0
  63. package/dist/index.d.ts +17 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +12 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/pages.d.ts +9 -0
  68. package/dist/pages.d.ts.map +1 -0
  69. package/dist/pages.js +9 -0
  70. package/dist/pages.js.map +1 -0
  71. package/dist/regen.d.ts +58 -0
  72. package/dist/regen.d.ts.map +1 -0
  73. package/dist/regen.js +69 -0
  74. package/dist/regen.js.map +1 -0
  75. package/dist/secrets/cloudflare-api.descriptor.d.ts +18 -0
  76. package/dist/secrets/cloudflare-api.descriptor.d.ts.map +1 -0
  77. package/dist/secrets/cloudflare-api.descriptor.js +32 -0
  78. package/dist/secrets/cloudflare-api.descriptor.js.map +1 -0
  79. package/dist/secrets/cloudflare-api.impl.d.ts +30 -0
  80. package/dist/secrets/cloudflare-api.impl.d.ts.map +1 -0
  81. package/dist/secrets/cloudflare-api.impl.js +111 -0
  82. package/dist/secrets/cloudflare-api.impl.js.map +1 -0
  83. package/dist/secrets/cloudflare-secrets-store.descriptor.d.ts +10 -0
  84. package/dist/secrets/cloudflare-secrets-store.descriptor.d.ts.map +1 -0
  85. package/dist/secrets/cloudflare-secrets-store.descriptor.js +24 -0
  86. package/dist/secrets/cloudflare-secrets-store.descriptor.js.map +1 -0
  87. package/dist/secrets/cloudflare-secrets-store.impl.d.ts +27 -0
  88. package/dist/secrets/cloudflare-secrets-store.impl.d.ts.map +1 -0
  89. package/dist/secrets/cloudflare-secrets-store.impl.js +72 -0
  90. package/dist/secrets/cloudflare-secrets-store.impl.js.map +1 -0
  91. package/dist/secrets/index.d.ts +6 -0
  92. package/dist/secrets/index.d.ts.map +1 -0
  93. package/dist/secrets/index.js +6 -0
  94. package/dist/secrets/index.js.map +1 -0
  95. package/dist/secrets/resolve-cf-credentials.d.ts +18 -0
  96. package/dist/secrets/resolve-cf-credentials.d.ts.map +1 -0
  97. package/dist/secrets/resolve-cf-credentials.js +57 -0
  98. package/dist/secrets/resolve-cf-credentials.js.map +1 -0
  99. package/dist/web-app.d.ts +11 -0
  100. package/dist/web-app.d.ts.map +1 -0
  101. package/dist/web-app.js +11 -0
  102. package/dist/web-app.js.map +1 -0
  103. package/package.json +153 -0
  104. package/src/cloudflare.plugin.ts +477 -0
  105. package/src/implementations/alerts.descriptor.ts +33 -0
  106. package/src/implementations/alerts.impl.ts +332 -0
  107. package/src/implementations/kv.impl.ts +51 -0
  108. package/src/implementations/logs.descriptor.ts +29 -0
  109. package/src/implementations/logs.impl.ts +201 -0
  110. package/src/implementations/observability.descriptor.ts +25 -0
  111. package/src/implementations/observability.impl.ts +307 -0
  112. package/src/implementations/pages.impl.ts +189 -0
  113. package/src/implementations/queues.impl.ts +48 -0
  114. package/src/implementations/r2.impl.ts +58 -0
  115. package/src/implementations/rum.descriptor.ts +35 -0
  116. package/src/implementations/rum.impl.ts +192 -0
  117. package/src/implementations/web-app.impl.ts +494 -0
  118. package/src/implementations/workers.impl.ts +336 -0
  119. package/src/index.ts +60 -0
  120. package/src/pages.ts +18 -0
  121. package/src/regen.ts +87 -0
  122. package/src/secrets/cloudflare-api.descriptor.ts +35 -0
  123. package/src/secrets/cloudflare-api.impl.ts +131 -0
  124. package/src/secrets/cloudflare-secrets-store.descriptor.ts +27 -0
  125. package/src/secrets/cloudflare-secrets-store.impl.ts +87 -0
  126. package/src/secrets/index.ts +13 -0
  127. package/src/secrets/resolve-cf-credentials.ts +63 -0
  128. package/src/web-app.ts +32 -0
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Cloudflare Workers Observability Implementation
3
+ *
4
+ * Reads/writes per-Worker observability via the CF Workers Settings API:
5
+ *
6
+ * GET /accounts/:account/workers/scripts/:script/settings
7
+ * PATCH /accounts/:account/workers/scripts/:script/settings
8
+ *
9
+ * The PATCH body shape is the same as the GET response — supply only
10
+ * the keys you want to change. We translate the kind's
11
+ * `ObservabilitySettings` shape onto CF's nested observability.{logs,
12
+ * traces} fields.
13
+ *
14
+ * Credentials resolve through the standard `secrets/store` runtime
15
+ * chain via `resolveAdapterCredential`; no CF-specific helpers. Token
16
+ * candidates default to CLOUDFLARE_API_TOKEN (Workers Scripts: Edit
17
+ * scope required for write).
18
+ */
19
+
20
+ import * as z from 'zod/v4';
21
+ import {
22
+ resolveAdapterCredential,
23
+ type ObservabilityImplementation,
24
+ type ObservabilityScope,
25
+ type ObservabilitySettings,
26
+ type ObservabilitySnapshot,
27
+ type SetObservabilityInput,
28
+ type SetObservabilityResult
29
+ } from '@vibesdotdev/infra-core';
30
+
31
+ export const CloudflareObservabilityDescriptorSchema = z.object({
32
+ kind: z.literal('infra/observability'),
33
+ id: z.string().min(1),
34
+ name: z.string().min(1),
35
+ description: z.string().optional(),
36
+ adapter: z.literal('cloudflare-workers-observability'),
37
+ adapterConfig: z.record(z.string(), z.unknown()).optional(),
38
+ environment: z.string().default('local'),
39
+ defaultScope: z.unknown().optional(),
40
+ settings: z.unknown().optional()
41
+ });
42
+
43
+ export type CloudflareObservabilityDescriptorInput = z.input<typeof CloudflareObservabilityDescriptorSchema>;
44
+ export type CloudflareObservabilityDescriptor = z.infer<typeof CloudflareObservabilityDescriptorSchema>;
45
+
46
+ interface CfObservabilityBody {
47
+ enabled?: boolean;
48
+ head_sampling_rate?: number;
49
+ logs?: {
50
+ enabled?: boolean;
51
+ head_sampling_rate?: number;
52
+ persist?: boolean;
53
+ invocation_logs?: boolean;
54
+ };
55
+ traces?: {
56
+ enabled?: boolean;
57
+ head_sampling_rate?: number;
58
+ persist?: boolean;
59
+ };
60
+ }
61
+
62
+ interface CfScriptSettings {
63
+ observability?: CfObservabilityBody;
64
+ modified_on?: string;
65
+ }
66
+
67
+ interface CfApiBody<T> {
68
+ success: boolean;
69
+ result: T;
70
+ errors?: Array<{ code: number; message: string }>;
71
+ }
72
+
73
+ interface CfScriptListItem {
74
+ id: string;
75
+ modified_on?: string;
76
+ }
77
+
78
+ /** Map CF native observability → provider-agnostic ObservabilitySettings. */
79
+ function fromCfObservability(cf: CfObservabilityBody | undefined): ObservabilitySettings {
80
+ const out: ObservabilitySettings = {};
81
+ if (cf?.logs) {
82
+ out.logs = {
83
+ enabled: cf.logs.enabled ?? cf.enabled ?? false,
84
+ headSamplingRate: cf.logs.head_sampling_rate ?? cf.head_sampling_rate,
85
+ persist: cf.logs.persist,
86
+ invocationLogs: cf.logs.invocation_logs
87
+ };
88
+ }
89
+ if (cf?.traces) {
90
+ out.traces = {
91
+ enabled: cf.traces.enabled ?? false,
92
+ headSamplingRate: cf.traces.head_sampling_rate,
93
+ persist: cf.traces.persist
94
+ };
95
+ }
96
+ return out;
97
+ }
98
+
99
+ /** Map ObservabilitySettings → CF PATCH body (only includes provided keys). */
100
+ function toCfObservability(settings: ObservabilitySettings): CfObservabilityBody {
101
+ const body: CfObservabilityBody = {};
102
+ if (settings.logs) {
103
+ // CF gates everything under `enabled` at the top-level; mirror the
104
+ // logs.enabled value so single-toggle clients work as expected.
105
+ body.enabled = settings.logs.enabled;
106
+ body.head_sampling_rate = settings.logs.headSamplingRate;
107
+ body.logs = {
108
+ enabled: settings.logs.enabled,
109
+ head_sampling_rate: settings.logs.headSamplingRate,
110
+ persist: settings.logs.persist,
111
+ invocation_logs: settings.logs.invocationLogs
112
+ };
113
+ }
114
+ if (settings.traces) {
115
+ body.traces = {
116
+ enabled: settings.traces.enabled,
117
+ head_sampling_rate: settings.traces.headSamplingRate,
118
+ persist: settings.traces.persist
119
+ };
120
+ }
121
+ return body;
122
+ }
123
+
124
+ function settingsEqual(a: ObservabilitySettings, b: ObservabilitySettings): boolean {
125
+ return JSON.stringify(a) === JSON.stringify(b);
126
+ }
127
+
128
+ class CloudflareObservabilityImplementation implements ObservabilityImplementation {
129
+ readonly id = 'cloudflare-workers-observability';
130
+ readonly descriptor: CloudflareObservabilityDescriptor;
131
+ private creds: { accountId: string; apiToken: string } | null = null;
132
+
133
+ constructor(descriptor: CloudflareObservabilityDescriptor) {
134
+ this.descriptor = CloudflareObservabilityDescriptorSchema.parse(descriptor);
135
+ }
136
+
137
+ private async getCreds(): Promise<{ accountId: string; apiToken: string }> {
138
+ if (this.creds) return this.creds;
139
+
140
+ const env = this.descriptor.environment || 'local';
141
+ const ac = (this.descriptor.adapterConfig ?? {}) as Record<string, unknown>;
142
+ const accountKey =
143
+ typeof ac.accountIdEnvVar === 'string' ? ac.accountIdEnvVar : 'CLOUDFLARE_ACCOUNT_ID';
144
+ const primaryToken =
145
+ typeof ac.apiTokenEnvVar === 'string' ? ac.apiTokenEnvVar : 'CLOUDFLARE_API_TOKEN';
146
+ const fallbackToken = typeof ac.apiTokenFallbackEnvVar === 'string' ? ac.apiTokenFallbackEnvVar : null;
147
+
148
+ const tokenCandidates = fallbackToken ? [primaryToken, fallbackToken] : [primaryToken];
149
+
150
+ const accountRes = await resolveAdapterCredential({
151
+ keyCandidates: [accountKey],
152
+ environment: env,
153
+ humanName: 'Cloudflare account id'
154
+ });
155
+ const tokenRes = await resolveAdapterCredential({
156
+ keyCandidates: tokenCandidates,
157
+ environment: env,
158
+ humanName: 'Cloudflare API token (Workers Scripts: Edit scope for write ops)'
159
+ });
160
+
161
+ this.creds = { accountId: accountRes.value, apiToken: tokenRes.value };
162
+ return this.creds;
163
+ }
164
+
165
+ private async call<T>(path: string, init?: RequestInit): Promise<CfApiBody<T>> {
166
+ const { apiToken } = await this.getCreds();
167
+ // Note: callers that need the multipart/form-data shape (the
168
+ // script settings PATCH endpoint) must set their own Content-Type
169
+ // via the init.headers override — otherwise we apply JSON by default.
170
+ const callerHeaders = (init?.headers ?? {}) as Record<string, string>;
171
+ const explicitContentType = Object.keys(callerHeaders).some(
172
+ (k) => k.toLowerCase() === 'content-type'
173
+ );
174
+ const defaultHeaders: Record<string, string> = {
175
+ Authorization: `Bearer ${apiToken}`
176
+ };
177
+ if (!explicitContentType && !(init?.body instanceof FormData)) {
178
+ defaultHeaders['Content-Type'] = 'application/json';
179
+ }
180
+ const res = await fetch(`https://api.cloudflare.com/client/v4${path}`, {
181
+ ...init,
182
+ headers: {
183
+ ...defaultHeaders,
184
+ ...callerHeaders
185
+ }
186
+ });
187
+ const text = await res.text();
188
+ let body: CfApiBody<T>;
189
+ try {
190
+ body = JSON.parse(text) as CfApiBody<T>;
191
+ } catch {
192
+ throw new Error(
193
+ `cloudflare-observability: non-JSON response from ${path}: ${text.slice(0, 200)}`
194
+ );
195
+ }
196
+ if (!res.ok || body.success === false) {
197
+ const errSummary = body.errors?.map((e) => `${e.code}: ${e.message}`).join('; ') ?? text.slice(0, 200);
198
+ throw new Error(`cloudflare-observability: ${path} → ${res.status} ${errSummary}`);
199
+ }
200
+ return body;
201
+ }
202
+
203
+ private async listWorkerNames(filter?: string): Promise<string[]> {
204
+ const { accountId } = await this.getCreds();
205
+ const body = await this.call<CfScriptListItem[]>(
206
+ `/accounts/${accountId}/workers/scripts?per_page=200`
207
+ );
208
+ const all = (body.result ?? []).map((s) => s.id);
209
+ if (!filter) return all;
210
+ // Simple glob: support * as a wildcard.
211
+ const pattern = filter.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
212
+ const re = new RegExp(`^${pattern}$`);
213
+ return all.filter((name) => re.test(name));
214
+ }
215
+
216
+ private async resolveScopeToWorkers(scope: ObservabilityScope): Promise<string[]> {
217
+ if (scope.kind === 'worker') return [scope.workerName];
218
+ return this.listWorkerNames(scope.filter);
219
+ }
220
+
221
+ async getStatus(scope: ObservabilityScope): Promise<ObservabilitySnapshot[]> {
222
+ const { accountId } = await this.getCreds();
223
+ const workers = await this.resolveScopeToWorkers(scope);
224
+ const snapshots: ObservabilitySnapshot[] = [];
225
+ for (const workerName of workers) {
226
+ try {
227
+ const body = await this.call<CfScriptSettings>(
228
+ `/accounts/${accountId}/workers/scripts/${workerName}/settings`
229
+ );
230
+ snapshots.push({
231
+ workerName,
232
+ settings: fromCfObservability(body.result.observability),
233
+ modifiedAt: body.result.modified_on
234
+ });
235
+ } catch (err) {
236
+ // Skip workers that don't exist or aren't readable, but
237
+ // surface as snapshots with empty settings so the CLI can
238
+ // distinguish "not present" from "100% sampling".
239
+ snapshots.push({
240
+ workerName,
241
+ settings: {},
242
+ modifiedAt: undefined
243
+ });
244
+ if (process.env.VIBES_DEBUG_OBSERVABILITY === '1') {
245
+ console.error(`[observability-debug] ${workerName}: ${(err as Error).message}`);
246
+ }
247
+ }
248
+ }
249
+ return snapshots;
250
+ }
251
+
252
+ async set(input: SetObservabilityInput): Promise<SetObservabilityResult> {
253
+ const { accountId } = await this.getCreds();
254
+ const skipUnchanged = input.skipUnchanged ?? true;
255
+ const workers = await this.resolveScopeToWorkers(input.scope);
256
+ const result: SetObservabilityResult = { changed: [], unchanged: [], failed: [] };
257
+
258
+ for (const workerName of workers) {
259
+ try {
260
+ const current = await this.call<CfScriptSettings>(
261
+ `/accounts/${accountId}/workers/scripts/${workerName}/settings`
262
+ );
263
+ const before = fromCfObservability(current.result.observability);
264
+
265
+ // Merge requested settings into existing, then check for change.
266
+ const desired: ObservabilitySettings = {
267
+ logs: { ...(before.logs ?? { enabled: false }), ...(input.settings.logs ?? {}) },
268
+ traces: { ...(before.traces ?? { enabled: false }), ...(input.settings.traces ?? {}) }
269
+ };
270
+
271
+ if (skipUnchanged && settingsEqual(before, desired)) {
272
+ result.unchanged.push(workerName);
273
+ continue;
274
+ }
275
+
276
+ // CF's script-settings PATCH requires multipart/form-data with
277
+ // a `settings` field carrying the JSON metadata. Bare JSON
278
+ // returns 415 ("Content-Type must be one of: multipart/form-data").
279
+ const form = new FormData();
280
+ form.append(
281
+ 'settings',
282
+ JSON.stringify({ observability: toCfObservability(desired) })
283
+ );
284
+ await this.call<unknown>(`/accounts/${accountId}/workers/scripts/${workerName}/settings`, {
285
+ method: 'PATCH',
286
+ body: form
287
+ });
288
+ result.changed.push({ workerName, before, after: desired });
289
+ } catch (err) {
290
+ result.failed.push({
291
+ workerName,
292
+ error: err instanceof Error ? err.message : String(err)
293
+ });
294
+ }
295
+ }
296
+
297
+ return result;
298
+ }
299
+ }
300
+
301
+ export function createCloudflareObservabilityImplementation(
302
+ input: CloudflareObservabilityDescriptorInput
303
+ ): ObservabilityImplementation {
304
+ return new CloudflareObservabilityImplementation(
305
+ CloudflareObservabilityDescriptorSchema.parse(input)
306
+ );
307
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Cloudflare Pages Implementation
3
+ *
4
+ * Config generator and deployment helpers for Cloudflare Pages projects.
5
+ * Wraps an infra-core AppDeployment into a Cloudflare Pages-compatible
6
+ * wrangler.jsonc fragment and provides rendering utilities.
7
+ *
8
+ * Retained for ecosystem/SDK consumers that target Cloudflare Pages.
9
+ * Vibes monorepo apps deploy as Workers + Static Assets — see web-app.impl.ts.
10
+ */
11
+
12
+ import * as z from 'zod/v4';
13
+ import type { AppDeployment } from '@vibesdotdev/infra-core/deployment';
14
+
15
+ export const CloudflarePagesDescriptorSchema = z.object({
16
+ kind: z.literal('infra/web-app'),
17
+ id: z.string().min(1),
18
+ name: z.string().min(1),
19
+ adapter: z.enum(['cloudflare-pages', 'cloudflare-workers']),
20
+ /** Cloudflare Pages project name (e.g., "vibes-platform") */
21
+ cfProjectName: z.string().min(1),
22
+ /** Build output directory */
23
+ outputDir: z.string().min(1),
24
+ /** Compatibility date for wrangler */
25
+ compatibilityDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
26
+ /** Compatibility flags for wrangler */
27
+ compatibilityFlags: z.array(z.string()).default(['nodejs_compat']),
28
+ /** Environment variables to include */
29
+ env: z.record(z.string(), z.string()).default({}),
30
+ /**
31
+ * Account-level Cloudflare Secrets Store ID. When set, secret env vars
32
+ * emit as `secrets_store_secrets[]` bindings against this store. UUID,
33
+ * not a secret value — safe to commit.
34
+ */
35
+ secretsStoreId: z.string().min(1).optional()
36
+ });
37
+
38
+ export type CloudflarePagesDescriptor = z.infer<typeof CloudflarePagesDescriptorSchema>;
39
+ export type CloudflarePagesDescriptorInput = z.input<typeof CloudflarePagesDescriptorSchema>;
40
+
41
+ export interface CloudflarePagesDeployment {
42
+ projectName: string;
43
+ deployment: AppDeployment;
44
+ compatibilityDate: string;
45
+ /** Generate wrangler.jsonc fragment for this Pages deployment */
46
+ generateWranglerConfig(): WranglerPagesConfig;
47
+ }
48
+
49
+ export interface WranglerSecretsStoreBinding {
50
+ binding: string;
51
+ store_id: string;
52
+ secret_name: string;
53
+ }
54
+
55
+ export interface WranglerPagesConfig {
56
+ $schema?: string;
57
+ name: string;
58
+ compatibility_date: string;
59
+ compatibility_flags: string[];
60
+ /** Pages build output directory */
61
+ pages_build_output_dir: string;
62
+ /** Environment variables for production */
63
+ vars?: Record<string, string>;
64
+ /** Account-level Secrets Store bindings (one per secret env var) */
65
+ secrets_store_secrets?: WranglerSecretsStoreBinding[];
66
+ /** Per-Pages-project required secret names (deploy-time validation only) */
67
+ secrets?: { required: string[] };
68
+ }
69
+
70
+ /**
71
+ * Create a Cloudflare Pages deployment wrapper around an AppDeployment.
72
+ *
73
+ * @param options - Deployment options including projectName and the base deployment
74
+ * @returns A CloudflarePagesDeployment with wrangler config generation
75
+ */
76
+ export function createCloudflarePagesDeployment(options: {
77
+ projectName: string;
78
+ deployment: AppDeployment;
79
+ compatibilityDate?: string;
80
+ /**
81
+ * Account-level Secrets Store ID. When set, secret env vars are emitted
82
+ * as `secrets_store_secrets[]` bindings instead of being silently dropped.
83
+ */
84
+ secretsStoreId?: string;
85
+ }): CloudflarePagesDeployment {
86
+ const { projectName, deployment, compatibilityDate, secretsStoreId } = options;
87
+ const compatDate = compatibilityDate ?? new Date().toISOString().slice(0, 10);
88
+
89
+ return {
90
+ projectName,
91
+ deployment,
92
+ compatibilityDate: compatDate,
93
+
94
+ generateWranglerConfig(): WranglerPagesConfig {
95
+ const config: WranglerPagesConfig = {
96
+ $schema: 'node_modules/wrangler/config-schema.json',
97
+ name: projectName,
98
+ compatibility_date: compatDate,
99
+ compatibility_flags: ['nodejs_compat'],
100
+ pages_build_output_dir: deployment.build.outputDir
101
+ };
102
+
103
+ // Add non-secret env vars with explicit values as vars.
104
+ const plainEnvVars: Record<string, string> = {};
105
+ for (const envVar of deployment.env) {
106
+ if (!envVar.secret && envVar.value !== undefined) {
107
+ plainEnvVars[envVar.name] = envVar.value;
108
+ }
109
+ }
110
+
111
+ if (Object.keys(plainEnvVars).length > 0) {
112
+ config.vars = plainEnvVars;
113
+ }
114
+
115
+ const secretEnvVars = deployment.env
116
+ .filter((envVar) => envVar.secret)
117
+ .sort((a, b) => a.name.localeCompare(b.name));
118
+
119
+ if (secretEnvVars.length > 0) {
120
+ if (secretsStoreId) {
121
+ config.secrets_store_secrets = secretEnvVars.map((envVar) => ({
122
+ binding: envVar.name,
123
+ store_id: secretsStoreId,
124
+ secret_name: envVar.storeKey ?? envVar.name
125
+ }));
126
+ } else {
127
+ config.secrets = {
128
+ required: secretEnvVars
129
+ .filter((envVar) => envVar.required !== false)
130
+ .map((envVar) => envVar.name)
131
+ };
132
+ }
133
+ }
134
+
135
+ return config;
136
+ }
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Render a wrangler.jsonc string from a wrangler config object.
142
+ *
143
+ * Canonical output: tab-indented JSON with a leading JSONC header and a
144
+ * trailing newline. The same renderer is used for Pages and Workers
145
+ * (`infra-cloudflare/web-app`) — pass `headerLines` to customize the
146
+ * leading comment block.
147
+ *
148
+ * @param config - The wrangler config object to render
149
+ * @param options - Optional header override
150
+ * @returns JSONC-formatted string (JSON with comments)
151
+ */
152
+ export function renderWranglerJson(
153
+ config: WranglerPagesConfig | Record<string, unknown>,
154
+ options?: { headerLines?: string[] }
155
+ ): string {
156
+ const headerLines = options?.headerLines ?? [
157
+ 'Generated by @vibesdotdev/infra-cloudflare',
158
+ 'https://developers.cloudflare.com/workers/wrangler/configuration/'
159
+ ];
160
+ const header = headerLines.map((line) => `// ${line}`).join('\n');
161
+ const jsonContent = JSON.stringify(config, null, '\t');
162
+ return `${header}\n${jsonContent}\n`;
163
+ }
164
+
165
+ /**
166
+ * Generate a Pages deployment descriptor from an existing AppDeployment.
167
+ * This creates a descriptor that can be used for runtime discovery.
168
+ *
169
+ * @param deployment - The base AppDeployment
170
+ * @param cfProjectName - Explicit Cloudflare project name (required, no hardcoded conventions)
171
+ * @returns A validated Pages descriptor
172
+ */
173
+ export function generateCloudflarePagesDescriptor(
174
+ deployment: AppDeployment,
175
+ cfProjectName: string
176
+ ): CloudflarePagesDescriptor {
177
+ return CloudflarePagesDescriptorSchema.parse({
178
+ kind: 'infra/web-app',
179
+ id: deployment.appId,
180
+ name: deployment.appName,
181
+ adapter: 'cloudflare-workers', // Our apps use Workers Static Assets via adapter
182
+ cfProjectName,
183
+ outputDir: deployment.build.outputDir,
184
+ env: deployment.env.reduce((acc, e) => {
185
+ if (e.value) acc[e.name] = e.value;
186
+ return acc;
187
+ }, {} as Record<string, string>)
188
+ });
189
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Cloudflare Queues Implementation
3
+ *
4
+ * Config generator for `infra/queue` kind when engine is `cloudflare-queues`.
5
+ * Produces queue binding config to be merged into the producer app's wrangler config.
6
+ */
7
+
8
+ import * as z from 'zod/v4';
9
+
10
+ export const CloudflareQueueDescriptorSchema = z.object({
11
+ kind: z.literal('infra/queue'),
12
+ id: z.string().min(1),
13
+ name: z.string().min(1),
14
+ engine: z.literal('cloudflare-queues'),
15
+ /** Queue name in Cloudflare (e.g. "billing-jobs") */
16
+ queueName: z.string().min(1),
17
+ /** Binding name for the producer (e.g. "BILLING_QUEUE") */
18
+ binding: z.string().regex(/^[_A-Z0-9]+$/)
19
+ });
20
+
21
+ export type CloudflareQueueDescriptor = z.infer<typeof CloudflareQueueDescriptorSchema>;
22
+
23
+ export interface WranglerQueueProducerConfig {
24
+ queues: {
25
+ producers: Array<{
26
+ queue: string;
27
+ binding: string;
28
+ }>;
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Generate queue producer binding config to merge into a wrangler config.
34
+ */
35
+ export function generateCloudflareQueueConfig(descriptor: CloudflareQueueDescriptor): WranglerQueueProducerConfig {
36
+ const parsed = CloudflareQueueDescriptorSchema.parse(descriptor);
37
+
38
+ return {
39
+ queues: {
40
+ producers: [
41
+ {
42
+ queue: parsed.queueName,
43
+ binding: parsed.binding
44
+ }
45
+ ]
46
+ }
47
+ };
48
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Cloudflare R2 Implementation
3
+ *
4
+ * Config generator for `infra/object-storage` kind when adapter is `cloudflare-r2`.
5
+ * Produces R2 bucket binding config to merge into a wrangler config.
6
+ */
7
+
8
+ import * as z from 'zod/v4';
9
+
10
+ export const CloudflareR2DescriptorSchema = z.object({
11
+ kind: z.literal('infra/object-storage'),
12
+ id: z.string().min(1),
13
+ name: z.string().min(1),
14
+ adapter: z.literal('cloudflare-r2'),
15
+ /** Binding name (e.g. "ASSETS") */
16
+ binding: z.string().regex(/^[_A-Z0-9]+$/),
17
+ /** R2 bucket name (e.g. "vibes-assets") */
18
+ bucketName: z.string().min(1),
19
+ /** Optional preview bucket for non-production environments */
20
+ previewBucketName: z.string().optional(),
21
+ /** Jurisdiction restriction (e.g. "eu") */
22
+ jurisdiction: z.string().optional()
23
+ });
24
+
25
+ export type CloudflareR2Descriptor = z.infer<typeof CloudflareR2DescriptorSchema>;
26
+
27
+ export interface WranglerR2Config {
28
+ r2_buckets: Array<{
29
+ binding: string;
30
+ bucket_name: string;
31
+ preview_bucket_name?: string;
32
+ jurisdiction?: string;
33
+ }>;
34
+ }
35
+
36
+ /**
37
+ * Generate R2 bucket binding config to merge into a wrangler config.
38
+ */
39
+ export function generateCloudflareR2Config(descriptor: CloudflareR2Descriptor): WranglerR2Config {
40
+ const parsed = CloudflareR2DescriptorSchema.parse(descriptor);
41
+
42
+ const bucket: WranglerR2Config['r2_buckets'][number] = {
43
+ binding: parsed.binding,
44
+ bucket_name: parsed.bucketName
45
+ };
46
+
47
+ if (parsed.previewBucketName) {
48
+ bucket.preview_bucket_name = parsed.previewBucketName;
49
+ }
50
+
51
+ if (parsed.jurisdiction) {
52
+ bucket.jurisdiction = parsed.jurisdiction;
53
+ }
54
+
55
+ return {
56
+ r2_buckets: [bucket]
57
+ };
58
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Default Cloudflare Web Analytics RUM descriptor — registered at
3
+ * plugin load so `runtime.assets('infra/rum').descriptors()` surfaces
4
+ * the production vibes.dev site without each consumer declaring one.
5
+ *
6
+ * Site token (`96bb815cb0bc4afc87a793e6f34652b2`) is the existing
7
+ * vibes.dev site created in the CF dashboard months ago. Public —
8
+ * embedded in client HTML.
9
+ */
10
+
11
+ import type { InfraRumDescriptor } from '@vibesdotdev/infra-core';
12
+
13
+ const descriptor: InfraRumDescriptor = {
14
+ kind: 'infra/rum',
15
+ id: 'cloudflare-web-analytics',
16
+ name: 'Cloudflare Web Analytics',
17
+ description: 'CF Web Analytics RUM for vibes.dev (beacon-based page-load + Core Web Vitals)',
18
+ adapter: 'cloudflare-web-analytics',
19
+ adapterConfig: {
20
+ apiTokenEnvVar: 'CLOUDFLARE_API_TOKEN',
21
+ accountIdEnvVar: 'CLOUDFLARE_ACCOUNT_ID'
22
+ },
23
+ environment: 'local',
24
+ siteToken: '96bb815cb0bc4afc87a793e6f34652b2',
25
+ hostnames: ['*'],
26
+ // SSR Workers (every customer-facing surface in this monorepo) need
27
+ // manual beacon injection — CF auto-injection only fires for origin
28
+ // / cache responses. The cloudflare-web-analytics SvelteKit hook
29
+ // covers the runtime side.
30
+ injection: 'sveltekit-hook',
31
+ privacy: { excludeEU: false, respectDoNotTrack: true, stripQueryParams: false },
32
+ enabled: true
33
+ };
34
+
35
+ export default descriptor;