@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.
- package/README.md +107 -0
- package/SPEC.md +166 -0
- package/dist/cloudflare.plugin.d.ts +73 -0
- package/dist/cloudflare.plugin.d.ts.map +1 -0
- package/dist/cloudflare.plugin.js +334 -0
- package/dist/cloudflare.plugin.js.map +1 -0
- package/dist/implementations/alerts.descriptor.d.ts +13 -0
- package/dist/implementations/alerts.descriptor.d.ts.map +1 -0
- package/dist/implementations/alerts.descriptor.js +30 -0
- package/dist/implementations/alerts.descriptor.js.map +1 -0
- package/dist/implementations/alerts.impl.d.ts +35 -0
- package/dist/implementations/alerts.impl.d.ts.map +1 -0
- package/dist/implementations/alerts.impl.js +283 -0
- package/dist/implementations/alerts.impl.js.map +1 -0
- package/dist/implementations/kv.impl.d.ts +29 -0
- package/dist/implementations/kv.impl.d.ts.map +1 -0
- package/dist/implementations/kv.impl.js +36 -0
- package/dist/implementations/kv.impl.js.map +1 -0
- package/dist/implementations/logs.descriptor.d.ts +15 -0
- package/dist/implementations/logs.descriptor.d.ts.map +1 -0
- package/dist/implementations/logs.descriptor.js +26 -0
- package/dist/implementations/logs.descriptor.js.map +1 -0
- package/dist/implementations/logs.impl.d.ts +108 -0
- package/dist/implementations/logs.impl.d.ts.map +1 -0
- package/dist/implementations/logs.impl.js +154 -0
- package/dist/implementations/logs.impl.js.map +1 -0
- package/dist/implementations/observability.descriptor.d.ts +9 -0
- package/dist/implementations/observability.descriptor.d.ts.map +1 -0
- package/dist/implementations/observability.descriptor.js +22 -0
- package/dist/implementations/observability.descriptor.js.map +1 -0
- package/dist/implementations/observability.impl.d.ts +35 -0
- package/dist/implementations/observability.impl.d.ts.map +1 -0
- package/dist/implementations/observability.impl.js +229 -0
- package/dist/implementations/observability.impl.js.map +1 -0
- package/dist/implementations/pages.impl.d.ts +98 -0
- package/dist/implementations/pages.impl.d.ts.map +1 -0
- package/dist/implementations/pages.impl.js +132 -0
- package/dist/implementations/pages.impl.js.map +1 -0
- package/dist/implementations/queues.impl.d.ts +29 -0
- package/dist/implementations/queues.impl.d.ts.map +1 -0
- package/dist/implementations/queues.impl.js +34 -0
- package/dist/implementations/queues.impl.js.map +1 -0
- package/dist/implementations/r2.impl.d.ts +31 -0
- package/dist/implementations/r2.impl.d.ts.map +1 -0
- package/dist/implementations/r2.impl.js +41 -0
- package/dist/implementations/r2.impl.js.map +1 -0
- package/dist/implementations/rum.descriptor.d.ts +13 -0
- package/dist/implementations/rum.descriptor.d.ts.map +1 -0
- package/dist/implementations/rum.descriptor.js +32 -0
- package/dist/implementations/rum.descriptor.js.map +1 -0
- package/dist/implementations/rum.impl.d.ts +34 -0
- package/dist/implementations/rum.impl.d.ts.map +1 -0
- package/dist/implementations/rum.impl.js +153 -0
- package/dist/implementations/rum.impl.js.map +1 -0
- package/dist/implementations/web-app.impl.d.ts +294 -0
- package/dist/implementations/web-app.impl.d.ts.map +1 -0
- package/dist/implementations/web-app.impl.js +208 -0
- package/dist/implementations/web-app.impl.js.map +1 -0
- package/dist/implementations/workers.impl.d.ts +157 -0
- package/dist/implementations/workers.impl.d.ts.map +1 -0
- package/dist/implementations/workers.impl.js +247 -0
- package/dist/implementations/workers.impl.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/pages.d.ts +9 -0
- package/dist/pages.d.ts.map +1 -0
- package/dist/pages.js +9 -0
- package/dist/pages.js.map +1 -0
- package/dist/regen.d.ts +58 -0
- package/dist/regen.d.ts.map +1 -0
- package/dist/regen.js +69 -0
- package/dist/regen.js.map +1 -0
- package/dist/secrets/cloudflare-api.descriptor.d.ts +18 -0
- package/dist/secrets/cloudflare-api.descriptor.d.ts.map +1 -0
- package/dist/secrets/cloudflare-api.descriptor.js +32 -0
- package/dist/secrets/cloudflare-api.descriptor.js.map +1 -0
- package/dist/secrets/cloudflare-api.impl.d.ts +30 -0
- package/dist/secrets/cloudflare-api.impl.d.ts.map +1 -0
- package/dist/secrets/cloudflare-api.impl.js +111 -0
- package/dist/secrets/cloudflare-api.impl.js.map +1 -0
- package/dist/secrets/cloudflare-secrets-store.descriptor.d.ts +10 -0
- package/dist/secrets/cloudflare-secrets-store.descriptor.d.ts.map +1 -0
- package/dist/secrets/cloudflare-secrets-store.descriptor.js +24 -0
- package/dist/secrets/cloudflare-secrets-store.descriptor.js.map +1 -0
- package/dist/secrets/cloudflare-secrets-store.impl.d.ts +27 -0
- package/dist/secrets/cloudflare-secrets-store.impl.d.ts.map +1 -0
- package/dist/secrets/cloudflare-secrets-store.impl.js +72 -0
- package/dist/secrets/cloudflare-secrets-store.impl.js.map +1 -0
- package/dist/secrets/index.d.ts +6 -0
- package/dist/secrets/index.d.ts.map +1 -0
- package/dist/secrets/index.js +6 -0
- package/dist/secrets/index.js.map +1 -0
- package/dist/secrets/resolve-cf-credentials.d.ts +18 -0
- package/dist/secrets/resolve-cf-credentials.d.ts.map +1 -0
- package/dist/secrets/resolve-cf-credentials.js +57 -0
- package/dist/secrets/resolve-cf-credentials.js.map +1 -0
- package/dist/web-app.d.ts +11 -0
- package/dist/web-app.d.ts.map +1 -0
- package/dist/web-app.js +11 -0
- package/dist/web-app.js.map +1 -0
- package/package.json +153 -0
- package/src/cloudflare.plugin.ts +477 -0
- package/src/implementations/alerts.descriptor.ts +33 -0
- package/src/implementations/alerts.impl.ts +332 -0
- package/src/implementations/kv.impl.ts +51 -0
- package/src/implementations/logs.descriptor.ts +29 -0
- package/src/implementations/logs.impl.ts +201 -0
- package/src/implementations/observability.descriptor.ts +25 -0
- package/src/implementations/observability.impl.ts +307 -0
- package/src/implementations/pages.impl.ts +189 -0
- package/src/implementations/queues.impl.ts +48 -0
- package/src/implementations/r2.impl.ts +58 -0
- package/src/implementations/rum.descriptor.ts +35 -0
- package/src/implementations/rum.impl.ts +192 -0
- package/src/implementations/web-app.impl.ts +494 -0
- package/src/implementations/workers.impl.ts +336 -0
- package/src/index.ts +60 -0
- package/src/pages.ts +18 -0
- package/src/regen.ts +87 -0
- package/src/secrets/cloudflare-api.descriptor.ts +35 -0
- package/src/secrets/cloudflare-api.impl.ts +131 -0
- package/src/secrets/cloudflare-secrets-store.descriptor.ts +27 -0
- package/src/secrets/cloudflare-secrets-store.impl.ts +87 -0
- package/src/secrets/index.ts +13 -0
- package/src/secrets/resolve-cf-credentials.ts +63 -0
- package/src/web-app.ts +32 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Web Analytics RUM Implementation
|
|
3
|
+
*
|
|
4
|
+
* Maps the provider-agnostic `infra/rum` kind onto CF Web Analytics:
|
|
5
|
+
*
|
|
6
|
+
* GET /accounts/:account/rum/site_info/list list sites
|
|
7
|
+
* POST /accounts/:account/rum/site_info create site
|
|
8
|
+
* PATCH /accounts/:account/rum/site_info/:siteTag update site
|
|
9
|
+
* DELETE /accounts/:account/rum/site_info/:siteTag delete
|
|
10
|
+
*
|
|
11
|
+
* Credentials route through `resolveAdapterCredential` (vibes secrets
|
|
12
|
+
* → `secrets/store`); no CF-specific helpers. Token candidates default
|
|
13
|
+
* to CLOUDFLARE_API_TOKEN (Account → RUM: Edit scope required for write).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as z from 'zod/v4';
|
|
17
|
+
import {
|
|
18
|
+
resolveAdapterCredential,
|
|
19
|
+
type InfraRumDescriptor,
|
|
20
|
+
type RumImplementation,
|
|
21
|
+
type RumSiteSnapshot
|
|
22
|
+
} from '@vibesdotdev/infra-core';
|
|
23
|
+
|
|
24
|
+
export const CloudflareRumDescriptorSchema = z.object({
|
|
25
|
+
kind: z.literal('infra/rum'),
|
|
26
|
+
id: z.string().min(1),
|
|
27
|
+
name: z.string().min(1),
|
|
28
|
+
description: z.string().optional(),
|
|
29
|
+
adapter: z.literal('cloudflare-web-analytics'),
|
|
30
|
+
adapterConfig: z.record(z.string(), z.unknown()).optional(),
|
|
31
|
+
environment: z.string().default('local'),
|
|
32
|
+
siteToken: z.unknown().optional(),
|
|
33
|
+
hostnames: z.array(z.string()).default([]),
|
|
34
|
+
injection: z.string().default('sveltekit-hook'),
|
|
35
|
+
privacy: z.unknown().optional(),
|
|
36
|
+
enabled: z.boolean().default(true)
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export type CloudflareRumDescriptorInput = z.input<typeof CloudflareRumDescriptorSchema>;
|
|
40
|
+
export type CloudflareRumDescriptor = z.infer<typeof CloudflareRumDescriptorSchema>;
|
|
41
|
+
|
|
42
|
+
interface CfApiBody<T> {
|
|
43
|
+
success: boolean;
|
|
44
|
+
result: T;
|
|
45
|
+
errors?: Array<{ code: number; message: string }>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface CfRumSite {
|
|
49
|
+
site_tag: string;
|
|
50
|
+
created: string;
|
|
51
|
+
auto_install?: boolean;
|
|
52
|
+
rules?: Array<{ host: string; paths?: string[]; is_paused?: boolean; inclusive?: boolean }>;
|
|
53
|
+
ruleset?: { id?: string; zone_tag?: string };
|
|
54
|
+
snippet?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fromCfSite(cf: CfRumSite): RumSiteSnapshot {
|
|
58
|
+
const hosts = (cf.rules ?? []).map((r) => r.host);
|
|
59
|
+
const enabled = !(cf.rules ?? []).every((r) => r.is_paused === true);
|
|
60
|
+
return {
|
|
61
|
+
siteTag: cf.site_tag,
|
|
62
|
+
hostnames: hosts,
|
|
63
|
+
enabled,
|
|
64
|
+
injection: cf.auto_install ? 'provider-auto' : 'sveltekit-hook',
|
|
65
|
+
createdAt: cf.created
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class CloudflareRumImplementation implements RumImplementation {
|
|
70
|
+
readonly id = 'cloudflare-web-analytics';
|
|
71
|
+
readonly descriptor: CloudflareRumDescriptor;
|
|
72
|
+
private creds: { accountId: string; apiToken: string } | null = null;
|
|
73
|
+
|
|
74
|
+
constructor(descriptor: CloudflareRumDescriptor) {
|
|
75
|
+
this.descriptor = CloudflareRumDescriptorSchema.parse(descriptor);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async getCreds(): Promise<{ accountId: string; apiToken: string }> {
|
|
79
|
+
if (this.creds) return this.creds;
|
|
80
|
+
const env = this.descriptor.environment || 'local';
|
|
81
|
+
const ac = (this.descriptor.adapterConfig ?? {}) as Record<string, unknown>;
|
|
82
|
+
const accountKey =
|
|
83
|
+
typeof ac.accountIdEnvVar === 'string' ? ac.accountIdEnvVar : 'CLOUDFLARE_ACCOUNT_ID';
|
|
84
|
+
const primaryToken =
|
|
85
|
+
typeof ac.apiTokenEnvVar === 'string' ? ac.apiTokenEnvVar : 'CLOUDFLARE_API_TOKEN';
|
|
86
|
+
const accountRes = await resolveAdapterCredential({
|
|
87
|
+
keyCandidates: [accountKey],
|
|
88
|
+
environment: env,
|
|
89
|
+
humanName: 'Cloudflare account id'
|
|
90
|
+
});
|
|
91
|
+
const tokenRes = await resolveAdapterCredential({
|
|
92
|
+
keyCandidates: [primaryToken],
|
|
93
|
+
environment: env,
|
|
94
|
+
humanName: 'Cloudflare API token (Account RUM: Edit scope for write ops)'
|
|
95
|
+
});
|
|
96
|
+
this.creds = { accountId: accountRes.value, apiToken: tokenRes.value };
|
|
97
|
+
return this.creds;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async call<T>(path: string, init?: RequestInit): Promise<CfApiBody<T>> {
|
|
101
|
+
const { apiToken } = await this.getCreds();
|
|
102
|
+
const res = await fetch(`https://api.cloudflare.com/client/v4${path}`, {
|
|
103
|
+
...init,
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: `Bearer ${apiToken}`,
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
...(init?.headers ?? {})
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
const text = await res.text();
|
|
111
|
+
let body: CfApiBody<T>;
|
|
112
|
+
try {
|
|
113
|
+
body = JSON.parse(text) as CfApiBody<T>;
|
|
114
|
+
} catch {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`cloudflare-web-analytics: non-JSON response from ${path}: ${text.slice(0, 200)}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (!res.ok || body.success === false) {
|
|
120
|
+
const errSummary = body.errors?.map((e) => `${e.code}: ${e.message}`).join('; ') ?? text.slice(0, 200);
|
|
121
|
+
throw new Error(`cloudflare-web-analytics: ${path} → ${res.status} ${errSummary}`);
|
|
122
|
+
}
|
|
123
|
+
return body;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async listSites(): Promise<RumSiteSnapshot[]> {
|
|
127
|
+
const { accountId } = await this.getCreds();
|
|
128
|
+
const body = await this.call<CfRumSite[]>(
|
|
129
|
+
`/accounts/${accountId}/rum/site_info/list?per_page=50`
|
|
130
|
+
);
|
|
131
|
+
return (body.result ?? []).map(fromCfSite);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async getSite(siteTag: string): Promise<RumSiteSnapshot | null> {
|
|
135
|
+
const { accountId } = await this.getCreds();
|
|
136
|
+
try {
|
|
137
|
+
const body = await this.call<CfRumSite>(`/accounts/${accountId}/rum/site_info/${siteTag}`);
|
|
138
|
+
return fromCfSite(body.result);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
141
|
+
if (msg.includes('404') || msg.toLowerCase().includes('not found')) return null;
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async ensureSite(descriptor: InfraRumDescriptor): Promise<{ siteTag: string; created: boolean }> {
|
|
147
|
+
const { accountId } = await this.getCreds();
|
|
148
|
+
// If the descriptor already names an explicit siteToken (string),
|
|
149
|
+
// treat that as the existing siteTag and just return it. CF Web
|
|
150
|
+
// Analytics rotates tags only on explicit re-creation.
|
|
151
|
+
const declaredToken =
|
|
152
|
+
typeof descriptor.siteToken === 'string' ? descriptor.siteToken : undefined;
|
|
153
|
+
if (declaredToken) {
|
|
154
|
+
const live = await this.getSite(declaredToken);
|
|
155
|
+
if (live) return { siteTag: live.siteTag, created: false };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Otherwise look for an existing site matching one of the declared hostnames.
|
|
159
|
+
const all = await this.listSites();
|
|
160
|
+
const match = all.find((s) => descriptor.hostnames.some((h) => s.hostnames.includes(h)));
|
|
161
|
+
if (match) return { siteTag: match.siteTag, created: false };
|
|
162
|
+
|
|
163
|
+
// Create a new site.
|
|
164
|
+
const payload = {
|
|
165
|
+
host: descriptor.hostnames[0] ?? '*',
|
|
166
|
+
auto_install: descriptor.injection === 'provider-auto'
|
|
167
|
+
};
|
|
168
|
+
const body = await this.call<CfRumSite>(`/accounts/${accountId}/rum/site_info`, {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
body: JSON.stringify(payload)
|
|
171
|
+
});
|
|
172
|
+
return { siteTag: body.result.site_tag, created: true };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async setEnabled(siteTag: string, enabled: boolean): Promise<void> {
|
|
176
|
+
const { accountId } = await this.getCreds();
|
|
177
|
+
// CF doesn't expose a single "enabled" toggle — it pauses rules
|
|
178
|
+
// instead. We touch the first rule per the dashboard pattern.
|
|
179
|
+
const current = await this.call<CfRumSite>(`/accounts/${accountId}/rum/site_info/${siteTag}`);
|
|
180
|
+
const rules = (current.result.rules ?? []).map((r) => ({ ...r, is_paused: !enabled }));
|
|
181
|
+
await this.call<unknown>(`/accounts/${accountId}/rum/site_info/${siteTag}`, {
|
|
182
|
+
method: 'PATCH',
|
|
183
|
+
body: JSON.stringify({ rules })
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function createCloudflareRumImplementation(
|
|
189
|
+
input: CloudflareRumDescriptorInput
|
|
190
|
+
): RumImplementation {
|
|
191
|
+
return new CloudflareRumImplementation(CloudflareRumDescriptorSchema.parse(input));
|
|
192
|
+
}
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Web App (Workers + Static Assets) Implementation
|
|
3
|
+
*
|
|
4
|
+
* Generates the COMPLETE wrangler.jsonc shape for SvelteKit (and other SSR-asset)
|
|
5
|
+
* apps that deploy as a Cloudflare Worker with a Static Assets binding —
|
|
6
|
+
* the production target for every web app in this monorepo.
|
|
7
|
+
*
|
|
8
|
+
* `createCloudflareWebAppDeployment({ ... })` takes the provider-agnostic
|
|
9
|
+
* `AppDeployment` plus every Cloudflare-specific config group, and emits a
|
|
10
|
+
* `WranglerWebAppConfig` covering every wrangler key currently in use across
|
|
11
|
+
* the repo: `name`, `main`, `compatibility_date`, `compatibility_flags`,
|
|
12
|
+
* `assets`, `routes`, `placement`, `vars`, `secrets_store_secrets`, `secrets.required`,
|
|
13
|
+
* `d1_databases`, `r2_buckets`, `services`, `observability`, `triggers`,
|
|
14
|
+
* `queues`, `durable_objects`, `migrations`, `send_email`, `workers_dev`,
|
|
15
|
+
* `preview_urls`.
|
|
16
|
+
*
|
|
17
|
+
* The output of `generateWranglerConfig()` is the authoritative shape;
|
|
18
|
+
* `apps/<name>/wrangler.jsonc` is a build artifact written by
|
|
19
|
+
* `vibes infra deploy regenerate` (the sole supported path) from each app's
|
|
20
|
+
* `deployment.config.ts`. Drift is caught by the CLI with `--check`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as z from 'zod/v4';
|
|
24
|
+
import type { AppDeployment } from '@vibesdotdev/infra-core/deployment';
|
|
25
|
+
|
|
26
|
+
export const CloudflareWebAppDescriptorSchema = z.object({
|
|
27
|
+
kind: z.literal('infra/web-app'),
|
|
28
|
+
id: z.string().min(1),
|
|
29
|
+
name: z.string().min(1),
|
|
30
|
+
adapter: z.literal('cloudflare-workers'),
|
|
31
|
+
/** Cloudflare Workers script name (e.g., "vibes-ai") */
|
|
32
|
+
cfWorkerName: z.string().min(1),
|
|
33
|
+
/** Build output directory consumed by both `main` and `assets.directory` */
|
|
34
|
+
outputDir: z.string().min(1),
|
|
35
|
+
/** Path to the `_worker.js` entry point relative to `outputDir` */
|
|
36
|
+
workerEntry: z.string().min(1).default('_worker.js'),
|
|
37
|
+
/** Compatibility date for wrangler */
|
|
38
|
+
compatibilityDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
39
|
+
/** Compatibility flags for wrangler */
|
|
40
|
+
compatibilityFlags: z.array(z.string()).default(['nodejs_compat']),
|
|
41
|
+
/** Environment variables to include */
|
|
42
|
+
env: z.record(z.string(), z.string()).default({}),
|
|
43
|
+
/**
|
|
44
|
+
* Account-level Cloudflare Secrets Store ID. When set, secret env vars
|
|
45
|
+
* emit as `secrets_store_secrets[]` bindings; otherwise the generator
|
|
46
|
+
* falls back to `secrets.required[]` for deploy-time validation.
|
|
47
|
+
*/
|
|
48
|
+
secretsStoreId: z.string().min(1).optional()
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type CloudflareWebAppDescriptor = z.infer<typeof CloudflareWebAppDescriptorSchema>;
|
|
52
|
+
export type CloudflareWebAppDescriptorInput = z.input<typeof CloudflareWebAppDescriptorSchema>;
|
|
53
|
+
|
|
54
|
+
export interface WranglerSecretsStoreBinding {
|
|
55
|
+
binding: string;
|
|
56
|
+
store_id: string;
|
|
57
|
+
secret_name: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface WranglerWebAppRoute {
|
|
61
|
+
pattern: string;
|
|
62
|
+
custom_domain?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface WranglerWebAppAssets {
|
|
66
|
+
directory: string;
|
|
67
|
+
binding?: string;
|
|
68
|
+
not_found_handling?: 'none' | 'single-page-application' | '404-page';
|
|
69
|
+
run_worker_first?: boolean | string[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface WranglerD1Database {
|
|
73
|
+
binding: string;
|
|
74
|
+
database_name: string;
|
|
75
|
+
database_id?: string;
|
|
76
|
+
migrations_dir?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface WranglerR2Bucket {
|
|
80
|
+
binding: string;
|
|
81
|
+
bucket_name: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface WranglerServiceBinding {
|
|
85
|
+
binding: string;
|
|
86
|
+
service: string;
|
|
87
|
+
entrypoint?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface WranglerObservability {
|
|
91
|
+
enabled: boolean;
|
|
92
|
+
head_sampling_rate?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface WranglerAiBinding {
|
|
96
|
+
binding: string;
|
|
97
|
+
staging?: boolean;
|
|
98
|
+
remote?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface WranglerPlacement {
|
|
102
|
+
mode: 'smart';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface WranglerTriggers {
|
|
106
|
+
crons: string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface WranglerQueueProducer {
|
|
110
|
+
queue: string;
|
|
111
|
+
binding: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface WranglerQueueConsumer {
|
|
115
|
+
queue: string;
|
|
116
|
+
max_batch_size?: number;
|
|
117
|
+
max_batch_timeout?: number;
|
|
118
|
+
max_retries?: number;
|
|
119
|
+
dead_letter_queue?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface WranglerQueues {
|
|
123
|
+
producers?: WranglerQueueProducer[];
|
|
124
|
+
consumers?: WranglerQueueConsumer[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface WranglerDurableObjectBinding {
|
|
128
|
+
name: string;
|
|
129
|
+
class_name: string;
|
|
130
|
+
script_name?: string;
|
|
131
|
+
environment?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface WranglerDurableObjects {
|
|
135
|
+
bindings: WranglerDurableObjectBinding[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface WranglerMigration {
|
|
139
|
+
tag: string;
|
|
140
|
+
new_classes?: string[];
|
|
141
|
+
new_sqlite_classes?: string[];
|
|
142
|
+
deleted_classes?: string[];
|
|
143
|
+
renamed_classes?: Array<{ from: string; to: string }>;
|
|
144
|
+
transferred_classes?: Array<{ from: string; from_script: string; to: string }>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface WranglerSendEmailBinding {
|
|
148
|
+
name: string;
|
|
149
|
+
destination_address?: string;
|
|
150
|
+
allowed_destination_addresses?: string[];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface WranglerAnalyticsEngineBinding {
|
|
154
|
+
binding: string;
|
|
155
|
+
dataset: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface WranglerWebAppConfig {
|
|
159
|
+
$schema?: string;
|
|
160
|
+
name: string;
|
|
161
|
+
/** Worker entry. Omitted only for `staticAssetsOnly` configs (e.g. apps/docs). */
|
|
162
|
+
main?: string;
|
|
163
|
+
compatibility_date: string;
|
|
164
|
+
compatibility_flags: string[];
|
|
165
|
+
workers_dev?: boolean;
|
|
166
|
+
preview_urls?: boolean;
|
|
167
|
+
upload_source_maps?: boolean;
|
|
168
|
+
observability?: WranglerObservability;
|
|
169
|
+
placement?: WranglerPlacement;
|
|
170
|
+
/** Always present: every CF Worker in this repo binds static assets. */
|
|
171
|
+
assets: WranglerWebAppAssets;
|
|
172
|
+
routes?: WranglerWebAppRoute[];
|
|
173
|
+
vars?: Record<string, string>;
|
|
174
|
+
secrets_store_secrets?: WranglerSecretsStoreBinding[];
|
|
175
|
+
secrets?: { required: string[] };
|
|
176
|
+
d1_databases?: WranglerD1Database[];
|
|
177
|
+
kv_namespaces?: Array<{
|
|
178
|
+
binding: string;
|
|
179
|
+
id: string;
|
|
180
|
+
preview_id?: string;
|
|
181
|
+
}>;
|
|
182
|
+
r2_buckets?: WranglerR2Bucket[];
|
|
183
|
+
services?: WranglerServiceBinding[];
|
|
184
|
+
ai?: WranglerAiBinding;
|
|
185
|
+
triggers?: WranglerTriggers;
|
|
186
|
+
queues?: WranglerQueues;
|
|
187
|
+
durable_objects?: WranglerDurableObjects;
|
|
188
|
+
migrations?: WranglerMigration[];
|
|
189
|
+
send_email?: WranglerSendEmailBinding[];
|
|
190
|
+
analytics_engine_datasets?: WranglerAnalyticsEngineBinding[];
|
|
191
|
+
/** esbuild/wrangler bundler rules (e.g. to treat leaked .svelte icon files from @lucide/svelte as text). */
|
|
192
|
+
rules?: Array<{
|
|
193
|
+
type: string;
|
|
194
|
+
globs: string[];
|
|
195
|
+
fallthrough?: boolean;
|
|
196
|
+
}>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Shared Workers Analytics Engine dataset used for per-org bandwidth
|
|
201
|
+
* attribution. Bound automatically into every customer-facing Worker so
|
|
202
|
+
* the `billing.bandwidth-sampler` job has a uniform write target. Apps
|
|
203
|
+
* that need to opt out can pass `analyticsEngineDatasets: []`.
|
|
204
|
+
*/
|
|
205
|
+
export const SHARED_BANDWIDTH_TELEMETRY_DATASET: WranglerAnalyticsEngineBinding = {
|
|
206
|
+
binding: 'BANDWIDTH_TELEMETRY',
|
|
207
|
+
dataset: 'bandwidth_egress'
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Provider-agnostic cloudflare-side configuration for `createCloudflareWebAppDeployment`.
|
|
212
|
+
* Every field is optional; the generator emits only what the app declares so
|
|
213
|
+
* existing apps that don't (e.g.) bind R2 don't get an empty `r2_buckets: []`
|
|
214
|
+
* key in their wrangler.jsonc.
|
|
215
|
+
*/
|
|
216
|
+
export interface CloudflareWebAppOptions {
|
|
217
|
+
workerName: string;
|
|
218
|
+
deployment: AppDeployment;
|
|
219
|
+
compatibilityDate?: string;
|
|
220
|
+
compatibilityFlags?: string[];
|
|
221
|
+
/** When set, secret env vars are emitted as `secrets_store_secrets[]`. */
|
|
222
|
+
secretsStoreId?: string;
|
|
223
|
+
/** `_worker.js`-relative path inside outputDir; defaults to `_worker.js`. */
|
|
224
|
+
workerEntry?: string;
|
|
225
|
+
/** Override `assets.binding`; defaults to `ASSETS`. */
|
|
226
|
+
assetsBinding?: string;
|
|
227
|
+
/** Override `assets.not_found_handling`; defaults to `'none'`. */
|
|
228
|
+
assetsNotFoundHandling?: 'none' | 'single-page-application' | '404-page';
|
|
229
|
+
/** Override `assets.run_worker_first`; defaults to `true` for SSR adapters. */
|
|
230
|
+
assetsRunWorkerFirst?: boolean | string[];
|
|
231
|
+
/** Omit `main` and emit a static-assets-only Worker (used for `apps/docs`). */
|
|
232
|
+
staticAssetsOnly?: boolean;
|
|
233
|
+
/** `workers_dev` flag — defaults to omitted (CF default behavior). */
|
|
234
|
+
workersDev?: boolean;
|
|
235
|
+
/** `preview_urls` flag — defaults to omitted (CF default behavior). */
|
|
236
|
+
previewUrls?: boolean;
|
|
237
|
+
/** Observability config; pass `{ enabled: true }` to emit `observability: { enabled: true }`. */
|
|
238
|
+
observability?: WranglerObservability;
|
|
239
|
+
/** Placement config; use `{ mode: 'smart' }` for Cloudflare Smart Placement. */
|
|
240
|
+
placement?: WranglerPlacement;
|
|
241
|
+
/** Convenience flag for `placement: { mode: 'smart' }`. */
|
|
242
|
+
smartPlacement?: boolean;
|
|
243
|
+
/** D1 database bindings. */
|
|
244
|
+
d1Databases?: WranglerD1Database[];
|
|
245
|
+
/** KV namespace bindings. */
|
|
246
|
+
kvNamespaces?: Array<{
|
|
247
|
+
binding: string;
|
|
248
|
+
id: string;
|
|
249
|
+
preview_id?: string;
|
|
250
|
+
}>;
|
|
251
|
+
/** R2 bucket bindings. */
|
|
252
|
+
r2Buckets?: WranglerR2Bucket[];
|
|
253
|
+
/** Worker-to-Worker Service Bindings (e.g. `AUTH_SERVICE` → `vibes-auth`). */
|
|
254
|
+
services?: WranglerServiceBinding[];
|
|
255
|
+
/** Cloudflare Workers AI binding. */
|
|
256
|
+
ai?: WranglerAiBinding;
|
|
257
|
+
/** `triggers.crons` cron expressions. */
|
|
258
|
+
crons?: string[];
|
|
259
|
+
/** CF Queue producer bindings. */
|
|
260
|
+
queueProducers?: WranglerQueueProducer[];
|
|
261
|
+
/** CF Queue consumer configs (declared on the consuming Worker). */
|
|
262
|
+
queueConsumers?: WranglerQueueConsumer[];
|
|
263
|
+
/** Durable Object class bindings. */
|
|
264
|
+
durableObjects?: WranglerDurableObjectBinding[];
|
|
265
|
+
/** DO migration tags (required when declaring `durableObjects`). */
|
|
266
|
+
migrations?: WranglerMigration[];
|
|
267
|
+
/** `send_email[]` bindings for the Cloudflare Email Workers API. */
|
|
268
|
+
sendEmail?: WranglerSendEmailBinding[];
|
|
269
|
+
/**
|
|
270
|
+
* Workers Analytics Engine dataset bindings. Used for per-org bandwidth
|
|
271
|
+
* attribution by `@vibesdotdev/cloudflare-bandwidth-telemetry` (see
|
|
272
|
+
* `billing.bandwidth-sampler`). All customer-facing Workers in this
|
|
273
|
+
* monorepo share the same `bandwidth_egress` dataset.
|
|
274
|
+
*/
|
|
275
|
+
analyticsEngineDatasets?: WranglerAnalyticsEngineBinding[];
|
|
276
|
+
/**
|
|
277
|
+
* When `true` (default), this deployment is managed by the Vibes infra system
|
|
278
|
+
* and wrangler.jsonc should be regenerated from the deployment config.
|
|
279
|
+
* Set to `false` to opt out of automatic wrangler.jsonc regeneration.
|
|
280
|
+
*/
|
|
281
|
+
managed?: boolean;
|
|
282
|
+
/**
|
|
283
|
+
* When `true`, run `wrangler d1 migrations apply` for each declared D1 database
|
|
284
|
+
* with a `migrations_dir` before deploying the Worker. Defaults to `false`.
|
|
285
|
+
*/
|
|
286
|
+
autoMigrateD1?: boolean;
|
|
287
|
+
/**
|
|
288
|
+
* When `true`, upload source maps alongside the Worker for better production
|
|
289
|
+
* stack traces. Defaults to `true` for all managed deployments.
|
|
290
|
+
*/
|
|
291
|
+
uploadSourceMaps?: boolean;
|
|
292
|
+
/** Additional wrangler/esbuild rules (e.g. to treat third-party .svelte icon files as text so wrangler bundling doesn't fail). */
|
|
293
|
+
rules?: Array<{ type: string; globs: string[]; fallthrough?: boolean }>;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface CloudflareWebAppDeployment {
|
|
297
|
+
workerName: string;
|
|
298
|
+
deployment: AppDeployment;
|
|
299
|
+
compatibilityDate: string;
|
|
300
|
+
/**
|
|
301
|
+
* When `true`, this deployment is managed by the Vibes infra system
|
|
302
|
+
* and wrangler.jsonc should be regenerated from the deployment config.
|
|
303
|
+
* Defaults to `true` for all Cloudflare web app deployments.
|
|
304
|
+
*/
|
|
305
|
+
managed: boolean;
|
|
306
|
+
/**
|
|
307
|
+
* When `true`, run D1 migrations before deploying the Worker.
|
|
308
|
+
*/
|
|
309
|
+
autoMigrateD1: boolean;
|
|
310
|
+
generateWranglerConfig(): WranglerWebAppConfig;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Create a Cloudflare Workers + Static Assets deployment wrapper.
|
|
315
|
+
*/
|
|
316
|
+
export function createCloudflareWebAppDeployment(
|
|
317
|
+
options: CloudflareWebAppOptions
|
|
318
|
+
): CloudflareWebAppDeployment {
|
|
319
|
+
const {
|
|
320
|
+
workerName,
|
|
321
|
+
deployment,
|
|
322
|
+
compatibilityDate,
|
|
323
|
+
compatibilityFlags = ['nodejs_compat'],
|
|
324
|
+
secretsStoreId,
|
|
325
|
+
workerEntry = '_worker.js',
|
|
326
|
+
assetsBinding = 'ASSETS',
|
|
327
|
+
assetsNotFoundHandling = 'none',
|
|
328
|
+
assetsRunWorkerFirst = true,
|
|
329
|
+
staticAssetsOnly = false,
|
|
330
|
+
workersDev,
|
|
331
|
+
previewUrls,
|
|
332
|
+
observability,
|
|
333
|
+
placement,
|
|
334
|
+
smartPlacement,
|
|
335
|
+
d1Databases,
|
|
336
|
+
kvNamespaces,
|
|
337
|
+
r2Buckets,
|
|
338
|
+
services,
|
|
339
|
+
ai,
|
|
340
|
+
crons,
|
|
341
|
+
queueProducers,
|
|
342
|
+
queueConsumers,
|
|
343
|
+
durableObjects,
|
|
344
|
+
migrations,
|
|
345
|
+
sendEmail,
|
|
346
|
+
analyticsEngineDatasets,
|
|
347
|
+
managed = true,
|
|
348
|
+
autoMigrateD1 = false,
|
|
349
|
+
uploadSourceMaps = true,
|
|
350
|
+
rules
|
|
351
|
+
} = options;
|
|
352
|
+
|
|
353
|
+
if (kvNamespaces?.length) (deployment as Record<string, unknown>).kvNamespaces = kvNamespaces;
|
|
354
|
+
const compatDate = compatibilityDate ?? new Date().toISOString().slice(0, 10);
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
workerName,
|
|
358
|
+
deployment,
|
|
359
|
+
compatibilityDate: compatDate,
|
|
360
|
+
managed,
|
|
361
|
+
autoMigrateD1,
|
|
362
|
+
|
|
363
|
+
generateWranglerConfig(): WranglerWebAppConfig {
|
|
364
|
+
// wrangler.jsonc paths are resolved relative to the directory wrangler
|
|
365
|
+
// runs from. CI / infra-deploy both invoke `wrangler deploy --cwd <appDir>`,
|
|
366
|
+
// so the assets directory and `main` must be APP-relative even though
|
|
367
|
+
// `deployment.build.outputDir` is workspace-relative (that form is used
|
|
368
|
+
// by the build command and other consumers).
|
|
369
|
+
const rawOutputDir = deployment.build.outputDir.replace(/\/$/, '');
|
|
370
|
+
const appDir = deployment.build.appDir.replace(/\/$/, '');
|
|
371
|
+
const outputDir = rawOutputDir.startsWith(`${appDir}/`)
|
|
372
|
+
? rawOutputDir.slice(appDir.length + 1)
|
|
373
|
+
: rawOutputDir;
|
|
374
|
+
const assets: WranglerWebAppAssets = staticAssetsOnly
|
|
375
|
+
? {
|
|
376
|
+
directory: outputDir,
|
|
377
|
+
not_found_handling: assetsNotFoundHandling
|
|
378
|
+
}
|
|
379
|
+
: {
|
|
380
|
+
directory: outputDir,
|
|
381
|
+
binding: assetsBinding,
|
|
382
|
+
not_found_handling: assetsNotFoundHandling,
|
|
383
|
+
run_worker_first: assetsRunWorkerFirst
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const config: WranglerWebAppConfig = {
|
|
387
|
+
$schema: 'node_modules/wrangler/config-schema.json',
|
|
388
|
+
name: workerName,
|
|
389
|
+
compatibility_date: compatDate,
|
|
390
|
+
compatibility_flags: compatibilityFlags,
|
|
391
|
+
assets
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
if (!staticAssetsOnly) {
|
|
395
|
+
config.main = `${outputDir}/${workerEntry}`;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (workersDev !== undefined) config.workers_dev = workersDev;
|
|
399
|
+
if (previewUrls !== undefined) config.preview_urls = previewUrls;
|
|
400
|
+
if (uploadSourceMaps) config.upload_source_maps = true;
|
|
401
|
+
if (observability) config.observability = observability;
|
|
402
|
+
if (placement) config.placement = placement;
|
|
403
|
+
else if (smartPlacement) config.placement = { mode: 'smart' };
|
|
404
|
+
|
|
405
|
+
const primaryRoutes = deployment.origins
|
|
406
|
+
.filter((o) => o.kind === 'primary')
|
|
407
|
+
.map((o) => ({ pattern: o.hostname, custom_domain: true }));
|
|
408
|
+
if (primaryRoutes.length > 0) {
|
|
409
|
+
config.routes = primaryRoutes;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const plainEnvVars: Record<string, string> = {};
|
|
413
|
+
for (const envVar of deployment.env) {
|
|
414
|
+
if (!envVar.secret && envVar.value !== undefined) {
|
|
415
|
+
plainEnvVars[envVar.name] = envVar.value;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (Object.keys(plainEnvVars).length > 0) {
|
|
419
|
+
config.vars = plainEnvVars;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const secretEnvVars = deployment.env
|
|
423
|
+
.filter((envVar) => envVar.secret)
|
|
424
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
425
|
+
|
|
426
|
+
if (secretEnvVars.length > 0) {
|
|
427
|
+
if (secretsStoreId) {
|
|
428
|
+
config.secrets_store_secrets = secretEnvVars
|
|
429
|
+
.map((envVar) => ({
|
|
430
|
+
binding: envVar.name,
|
|
431
|
+
store_id: secretsStoreId,
|
|
432
|
+
secret_name: envVar.storeKey ?? envVar.name
|
|
433
|
+
}));
|
|
434
|
+
} else {
|
|
435
|
+
config.secrets = {
|
|
436
|
+
required: secretEnvVars
|
|
437
|
+
.filter((envVar) => envVar.required !== false)
|
|
438
|
+
.map((envVar) => envVar.name)
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (d1Databases?.length) config.d1_databases = d1Databases;
|
|
444
|
+
if (kvNamespaces?.length) config.kv_namespaces = kvNamespaces;
|
|
445
|
+
if (r2Buckets?.length) config.r2_buckets = r2Buckets;
|
|
446
|
+
if (services?.length) config.services = services;
|
|
447
|
+
if (ai) config.ai = ai;
|
|
448
|
+
if (crons?.length) config.triggers = { crons };
|
|
449
|
+
if (queueProducers?.length || queueConsumers?.length) {
|
|
450
|
+
config.queues = {};
|
|
451
|
+
if (queueProducers?.length) config.queues.producers = queueProducers;
|
|
452
|
+
if (queueConsumers?.length) config.queues.consumers = queueConsumers;
|
|
453
|
+
}
|
|
454
|
+
if (durableObjects?.length) {
|
|
455
|
+
config.durable_objects = { bindings: durableObjects };
|
|
456
|
+
}
|
|
457
|
+
if (migrations?.length) config.migrations = migrations;
|
|
458
|
+
if (sendEmail?.length) config.send_email = sendEmail;
|
|
459
|
+
// WAE binding default: every customer-facing Worker gets
|
|
460
|
+
// BANDWIDTH_TELEMETRY → bandwidth_egress so the
|
|
461
|
+
// billing.bandwidth-sampler aggregator can attribute bytes per
|
|
462
|
+
// org. Opt out by passing `analyticsEngineDatasets: []`.
|
|
463
|
+
const resolvedAnalyticsBindings =
|
|
464
|
+
analyticsEngineDatasets ?? [SHARED_BANDWIDTH_TELEMETRY_DATASET];
|
|
465
|
+
if (resolvedAnalyticsBindings.length > 0) {
|
|
466
|
+
config.analytics_engine_datasets = resolvedAnalyticsBindings;
|
|
467
|
+
}
|
|
468
|
+
if (rules?.length) (config as any).rules = rules;
|
|
469
|
+
|
|
470
|
+
return config;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Generate a Workers Static Assets descriptor from an existing AppDeployment.
|
|
477
|
+
*/
|
|
478
|
+
export function generateCloudflareWebAppDescriptor(
|
|
479
|
+
deployment: AppDeployment,
|
|
480
|
+
cfWorkerName: string
|
|
481
|
+
): CloudflareWebAppDescriptor {
|
|
482
|
+
return CloudflareWebAppDescriptorSchema.parse({
|
|
483
|
+
kind: 'infra/web-app',
|
|
484
|
+
id: deployment.appId,
|
|
485
|
+
name: deployment.appName,
|
|
486
|
+
adapter: 'cloudflare-workers',
|
|
487
|
+
cfWorkerName,
|
|
488
|
+
outputDir: deployment.build.outputDir,
|
|
489
|
+
env: deployment.env.reduce((acc, e) => {
|
|
490
|
+
if (e.value) acc[e.name] = e.value;
|
|
491
|
+
return acc;
|
|
492
|
+
}, {} as Record<string, string>)
|
|
493
|
+
});
|
|
494
|
+
}
|