@volter/twin-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.
- package/LICENSE +202 -0
- package/README.md +223 -0
- package/dist/r2/src/endpoint.d.ts +14 -0
- package/dist/r2/src/endpoint.js +30 -0
- package/dist/r2/src/fetch.d.ts +7 -0
- package/dist/r2/src/fetch.js +230 -0
- package/dist/r2/src/generated/surface.gen.json +1 -0
- package/dist/r2/src/manifest.d.ts +22 -0
- package/dist/r2/src/manifest.js +69 -0
- package/dist/r2/src/semantics/buckets.d.ts +18 -0
- package/dist/r2/src/semantics/buckets.js +145 -0
- package/dist/r2/src/semantics/objects.d.ts +8 -0
- package/dist/r2/src/semantics/objects.js +252 -0
- package/dist/r2/src/wire.d.ts +54 -0
- package/dist/r2/src/wire.js +120 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +26 -0
- package/dist/src/cloudflare-budget.d.ts +22 -0
- package/dist/src/cloudflare-budget.js +40 -0
- package/dist/src/cloudflare-capabilities.d.ts +6 -0
- package/dist/src/cloudflare-capabilities.js +1161 -0
- package/dist/src/cloudflare-common.d.ts +59 -0
- package/dist/src/cloudflare-common.js +142 -0
- package/dist/src/cloudflare-conformance.d.ts +6 -0
- package/dist/src/cloudflare-conformance.js +251 -0
- package/dist/src/cloudflare-connector.d.ts +53 -0
- package/dist/src/cloudflare-connector.js +418 -0
- package/dist/src/cloudflare-openapi-operations.d.ts +10 -0
- package/dist/src/cloudflare-openapi-operations.js +3336 -0
- package/dist/src/cloudflare-provisioning.d.ts +5 -0
- package/dist/src/cloudflare-provisioning.js +2097 -0
- package/dist/src/cloudflare-r2.d.ts +121 -0
- package/dist/src/cloudflare-r2.js +138 -0
- package/dist/src/cloudflare-server.d.ts +17 -0
- package/dist/src/cloudflare-server.js +101 -0
- package/dist/src/cloudflare-twin.d.ts +14 -0
- package/dist/src/cloudflare-twin.js +762 -0
- package/dist/src/cloudflare-worker-assets.d.ts +25 -0
- package/dist/src/cloudflare-worker-assets.js +192 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.js +69 -0
- package/dist/src/screens/dashboard.d.ts +3 -0
- package/dist/src/screens/dashboard.js +265 -0
- package/package.json +56 -0
- package/src/cli.ts +25 -0
- package/src/cloudflare-budget.ts +61 -0
- package/src/cloudflare-capabilities.ts +1107 -0
- package/src/cloudflare-common.ts +183 -0
- package/src/cloudflare-conformance.ts +204 -0
- package/src/cloudflare-connector.ts +443 -0
- package/src/cloudflare-openapi-operations.ts +3344 -0
- package/src/cloudflare-provisioning.ts +1969 -0
- package/src/cloudflare-r2.ts +173 -0
- package/src/cloudflare-server.ts +109 -0
- package/src/cloudflare-twin.ts +714 -0
- package/src/cloudflare-worker-assets.ts +207 -0
- package/src/index.ts +112 -0
- package/src/screens/dashboard.tsx +297 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Shared Cloudflare twin plumbing: the API v4 envelope, kernel row projection/minting, and
|
|
2
|
+
// request parsing helpers used by both the core slice (accounts/zones/tunnels/DNS in
|
|
3
|
+
// cloudflare-twin.ts) and the provisioning slice (Workers/R2/Hyperdrive/Queues in
|
|
4
|
+
// cloudflare-provisioning.ts). Pure helpers over the @volter/world-core kernel — no routes here.
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { applyTwinWrite, projectResources } from '@volter/world-core';
|
|
7
|
+
|
|
8
|
+
export const SERVICE = 'cloudflare';
|
|
9
|
+
|
|
10
|
+
export const CLOUDFLARE_RESOURCE_TYPES = [
|
|
11
|
+
'account',
|
|
12
|
+
'zone',
|
|
13
|
+
'cloudflare_tunnel',
|
|
14
|
+
'tunnel_configuration',
|
|
15
|
+
'dns_record',
|
|
16
|
+
'worker_script',
|
|
17
|
+
'worker_route',
|
|
18
|
+
'worker_domain',
|
|
19
|
+
'workers_subdomain',
|
|
20
|
+
'durable_object_namespace',
|
|
21
|
+
'r2_bucket',
|
|
22
|
+
'r2_object',
|
|
23
|
+
'r2_custom_domain',
|
|
24
|
+
'hyperdrive_config',
|
|
25
|
+
'queue',
|
|
26
|
+
'queue_consumer',
|
|
27
|
+
'worker_asset_upload_session',
|
|
28
|
+
'worker_asset',
|
|
29
|
+
'worker_version',
|
|
30
|
+
'account_member',
|
|
31
|
+
'zone_setting',
|
|
32
|
+
'user_token',
|
|
33
|
+
'audit_log',
|
|
34
|
+
] as const;
|
|
35
|
+
export type CloudflareResourceType = (typeof CLOUDFLARE_RESOURCE_TYPES)[number];
|
|
36
|
+
|
|
37
|
+
export type CloudflareRequest = {
|
|
38
|
+
method: string;
|
|
39
|
+
path: string;
|
|
40
|
+
body?: string;
|
|
41
|
+
headers?: Record<string, string>;
|
|
42
|
+
occurredAt?: string;
|
|
43
|
+
root?: string;
|
|
44
|
+
readOnly?: boolean;
|
|
45
|
+
};
|
|
46
|
+
/** `raw: true` responses send `body` verbatim (or an empty body when null) with the given
|
|
47
|
+
* content-type headers — used by Worker script download, which is not JSON-enveloped. */
|
|
48
|
+
export type CloudflareResponse = { status: number; body: unknown; headers?: Record<string, string>; raw?: boolean };
|
|
49
|
+
|
|
50
|
+
export type Row = Record<string, unknown> & { id: string; type: string };
|
|
51
|
+
|
|
52
|
+
export function headers(input?: Record<string, string>): Record<string, string> {
|
|
53
|
+
return Object.fromEntries(Object.entries(input ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function success(result: unknown, resultInfo?: Record<string, unknown>): CloudflareResponse {
|
|
57
|
+
return {
|
|
58
|
+
status: 200,
|
|
59
|
+
body: {
|
|
60
|
+
success: true,
|
|
61
|
+
errors: [],
|
|
62
|
+
messages: [],
|
|
63
|
+
result,
|
|
64
|
+
...(resultInfo ? { result_info: resultInfo } : {}),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function failure(status: number, code: number, message: string): CloudflareResponse {
|
|
70
|
+
return {
|
|
71
|
+
status,
|
|
72
|
+
body: { success: false, errors: [{ code, message }], messages: [], result: null },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function parseBody(req: CloudflareRequest): Record<string, unknown> | CloudflareResponse {
|
|
77
|
+
try {
|
|
78
|
+
const value = req.body ? JSON.parse(req.body) : {};
|
|
79
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return failure(400, 1004, 'Invalid request body');
|
|
80
|
+
return value as Record<string, unknown>;
|
|
81
|
+
} catch {
|
|
82
|
+
return failure(400, 1004, 'Invalid request body');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isResponse(value: unknown): value is CloudflareResponse {
|
|
87
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
88
|
+
&& typeof (value as CloudflareResponse).status === 'number'
|
|
89
|
+
&& 'body' in (value as Record<string, unknown>);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The rows of a type (protocol 2: a subject's id is the vendor's, with no type prefix, and a delete is the
|
|
93
|
+
* kernel's tombstone `deleted: true` — which the subject's next write clears). */
|
|
94
|
+
export function rows(type: CloudflareResourceType, root?: string, includeDeleted = false): Row[] {
|
|
95
|
+
return projectResources(SERVICE, root)
|
|
96
|
+
.filter((item) => item.type === type && (includeDeleted || (item as Row).deleted !== true))
|
|
97
|
+
.map((item) => ({ ...(item as Row) }));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function row(type: CloudflareResourceType, id: string, root?: string, includeDeleted = false): Row | undefined {
|
|
101
|
+
return rows(type, root, includeDeleted).find((item) => item.id === id);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function deterministicHex(label: string, length: number): string {
|
|
105
|
+
let out = '';
|
|
106
|
+
for (let i = 0; out.length < length; i++) out += createHash('sha256').update(`${label}:${i}`).digest('hex');
|
|
107
|
+
return out.slice(0, length);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function uuidFrom(label: string): string {
|
|
111
|
+
const hex = deterministicHex(label, 32);
|
|
112
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Mint a fresh id for `type`, scanning the id-SET already in state (tombstones included) so
|
|
116
|
+
* delete/recreate and pulled-vendor ids can never collide with a local mint. */
|
|
117
|
+
export function mint(type: CloudflareResourceType, label: string, root?: string): string {
|
|
118
|
+
const taken = new Set(rows(type, root, true).map((item) => item.id));
|
|
119
|
+
for (let salt = 0; ; salt++) {
|
|
120
|
+
const id = type === 'cloudflare_tunnel' || type === 'worker_version' ? uuidFrom(`${label}:${salt}`) : deterministicHex(`${label}:${salt}`, 32);
|
|
121
|
+
if (!taken.has(id)) return id;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function write(
|
|
126
|
+
type: CloudflareResourceType,
|
|
127
|
+
id: string,
|
|
128
|
+
operation: string,
|
|
129
|
+
fields: Record<string, unknown>,
|
|
130
|
+
req: CloudflareRequest,
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
await applyTwinWrite(
|
|
133
|
+
SERVICE,
|
|
134
|
+
{
|
|
135
|
+
operation,
|
|
136
|
+
subjectType: type,
|
|
137
|
+
subjectId: id, // protocol 2: the subject's id is the vendor's, the type is the subject's own
|
|
138
|
+
fields,
|
|
139
|
+
...(req.occurredAt ? { occurredAt: req.occurredAt } : {}),
|
|
140
|
+
actor: { kind: 'agent' },
|
|
141
|
+
},
|
|
142
|
+
req.root,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function paginate<T>(items: T[], url: URL, defaultPerPage = 20, maxPerPage = 50): { result: T[]; info: Record<string, number> } {
|
|
147
|
+
const page = Math.max(1, Number(url.searchParams.get('page') ?? 1) || 1);
|
|
148
|
+
const perPage = Math.min(maxPerPage, Math.max(1, Number(url.searchParams.get('per_page') ?? defaultPerPage) || defaultPerPage));
|
|
149
|
+
const start = (page - 1) * perPage;
|
|
150
|
+
const result = items.slice(start, start + perPage);
|
|
151
|
+
return {
|
|
152
|
+
result,
|
|
153
|
+
info: { page, per_page: perPage, count: result.length, total_count: items.length, total_pages: Math.max(1, Math.ceil(items.length / perPage)) },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function readOnly(req: CloudflareRequest): CloudflareResponse | null {
|
|
158
|
+
return req.readOnly ? failure(405, 1000, 'Method not allowed in read-only mode') : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function scopeMissing(kind: 'account' | 'zone' | 'tunnel', id: string): CloudflareResponse {
|
|
162
|
+
return failure(404, 7003, `Could not route to /${kind}/${id}, perhaps your object identifier is invalid?`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function noRoute(): CloudflareResponse {
|
|
166
|
+
return failure(404, 7000, 'No route for that URI');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The user a request's API token belongs to (a token minted at POST /twin/users/{email}/tokens); empty for any other. */
|
|
170
|
+
export function callerEmail(req: CloudflareRequest): string {
|
|
171
|
+
const bearer = /^Bearer\s+(\S+)$/i.exec(headers(req.headers).authorization ?? '')?.[1] ?? '';
|
|
172
|
+
return bearer ? String(rows('user_token', req.root).find((t) => t.token === bearer)?.email ?? '') : '';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** One entry in an account's audit log, attributed to the calling token's user. */
|
|
176
|
+
export async function audit(req: CloudflareRequest, accountID: string, entry: { type: string; resource: { type: string; id: string }; oldValue?: unknown; newValue?: unknown; metadata?: Record<string, unknown> }): Promise<void> {
|
|
177
|
+
const when = req.occurredAt ?? new Date().toISOString();
|
|
178
|
+
const id = deterministicHex(`audit:${accountID}:${when}:${entry.resource.type}:${entry.resource.id}:${rows('audit_log', req.root).length}`, 32);
|
|
179
|
+
const email = callerEmail(req);
|
|
180
|
+
await write('audit_log', id, 'audit_log.record', { account_id: accountID, when, action: { type: entry.type, result: true, info: '' },
|
|
181
|
+
actor: { email, id: email ? deterministicHex(`user:${email}`, 32) : '', ip: '', type: email ? 'user' : 'token' },
|
|
182
|
+
interface: 'API', resource: entry.resource, oldValue: entry.oldValue ?? null, newValue: entry.newValue ?? null, metadata: entry.metadata ?? {}, owner: { id: accountID } }, req);
|
|
183
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Dev-only conformance probe. Expected paths are literal (not imported from the router),
|
|
2
|
+
// and every claimed modeled endpoint is exercised against a fresh kernel root with a real
|
|
3
|
+
// request asserting the outcome a live handler produces (status + body predicate).
|
|
4
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { CLOUDFLARE_ROUTER_SURFACE, handleCloudflareTwinRequest } from './cloudflare-twin.ts';
|
|
8
|
+
|
|
9
|
+
export type CloudflareConformanceReport = { ok: boolean; endpointsChecked: number; violations: string[] };
|
|
10
|
+
|
|
11
|
+
const EXPECTED = [
|
|
12
|
+
'POST /twin/bootstrap', 'GET /accounts', 'GET /accounts/{account_id}', 'GET /zones', 'GET /zones/{zone_id}',
|
|
13
|
+
'GET /accounts/{account_id}/cfd_tunnel', 'POST /accounts/{account_id}/cfd_tunnel',
|
|
14
|
+
'GET /accounts/{account_id}/cfd_tunnel/{tunnel_id}', 'PATCH /accounts/{account_id}/cfd_tunnel/{tunnel_id}',
|
|
15
|
+
'DELETE /accounts/{account_id}/cfd_tunnel/{tunnel_id}', 'GET /accounts/{account_id}/cfd_tunnel/{tunnel_id}/token',
|
|
16
|
+
'GET /accounts/{account_id}/cfd_tunnel/{tunnel_id}/configurations', 'PUT /accounts/{account_id}/cfd_tunnel/{tunnel_id}/configurations',
|
|
17
|
+
'GET /zones/{zone_id}/dns_records', 'POST /zones/{zone_id}/dns_records', 'GET /zones/{zone_id}/dns_records/{dns_record_id}',
|
|
18
|
+
'PUT /zones/{zone_id}/dns_records/{dns_record_id}', 'PATCH /zones/{zone_id}/dns_records/{dns_record_id}',
|
|
19
|
+
'DELETE /zones/{zone_id}/dns_records/{dns_record_id}',
|
|
20
|
+
// Workers / R2 / Hyperdrive / Queues provisioning control plane.
|
|
21
|
+
'GET /accounts/{account_id}/workers/scripts',
|
|
22
|
+
'PUT /accounts/{account_id}/workers/scripts/{script_name}',
|
|
23
|
+
'GET /accounts/{account_id}/workers/scripts/{script_name}',
|
|
24
|
+
'DELETE /accounts/{account_id}/workers/scripts/{script_name}',
|
|
25
|
+
'GET /accounts/{account_id}/workers/scripts/{script_name}/schedules',
|
|
26
|
+
'PUT /accounts/{account_id}/workers/scripts/{script_name}/schedules',
|
|
27
|
+
'GET /accounts/{account_id}/workers/scripts/{script_name}/subdomain',
|
|
28
|
+
'POST /accounts/{account_id}/workers/scripts/{script_name}/subdomain',
|
|
29
|
+
'DELETE /accounts/{account_id}/workers/scripts/{script_name}/subdomain',
|
|
30
|
+
'GET /accounts/{account_id}/workers/subdomain',
|
|
31
|
+
'PUT /accounts/{account_id}/workers/subdomain',
|
|
32
|
+
'DELETE /accounts/{account_id}/workers/subdomain',
|
|
33
|
+
'GET /accounts/{account_id}/workers/domains',
|
|
34
|
+
'PUT /accounts/{account_id}/workers/domains',
|
|
35
|
+
'GET /accounts/{account_id}/workers/domains/{domain_id}',
|
|
36
|
+
'DELETE /accounts/{account_id}/workers/domains/{domain_id}',
|
|
37
|
+
'GET /accounts/{account_id}/workers/durable_objects/namespaces',
|
|
38
|
+
'GET /zones/{zone_id}/workers/routes',
|
|
39
|
+
'POST /zones/{zone_id}/workers/routes',
|
|
40
|
+
'GET /zones/{zone_id}/workers/routes/{route_id}',
|
|
41
|
+
'PUT /zones/{zone_id}/workers/routes/{route_id}',
|
|
42
|
+
'DELETE /zones/{zone_id}/workers/routes/{route_id}',
|
|
43
|
+
'GET /accounts/{account_id}/r2/buckets',
|
|
44
|
+
'POST /accounts/{account_id}/r2/buckets',
|
|
45
|
+
'GET /accounts/{account_id}/r2/buckets/{bucket_name}',
|
|
46
|
+
'DELETE /accounts/{account_id}/r2/buckets/{bucket_name}',
|
|
47
|
+
'GET /accounts/{account_id}/hyperdrive/configs',
|
|
48
|
+
'POST /accounts/{account_id}/hyperdrive/configs',
|
|
49
|
+
'GET /accounts/{account_id}/hyperdrive/configs/{hyperdrive_id}',
|
|
50
|
+
'PUT /accounts/{account_id}/hyperdrive/configs/{hyperdrive_id}',
|
|
51
|
+
'PATCH /accounts/{account_id}/hyperdrive/configs/{hyperdrive_id}',
|
|
52
|
+
'DELETE /accounts/{account_id}/hyperdrive/configs/{hyperdrive_id}',
|
|
53
|
+
'GET /accounts/{account_id}/queues',
|
|
54
|
+
'POST /accounts/{account_id}/queues',
|
|
55
|
+
'GET /accounts/{account_id}/queues/{queue_id}',
|
|
56
|
+
'DELETE /accounts/{account_id}/queues/{queue_id}',
|
|
57
|
+
'GET /accounts/{account_id}/queues/{queue_id}/consumers',
|
|
58
|
+
'POST /accounts/{account_id}/queues/{queue_id}/consumers',
|
|
59
|
+
'GET /accounts/{account_id}/queues/{queue_id}/consumers/{consumer_id}',
|
|
60
|
+
'PUT /accounts/{account_id}/queues/{queue_id}/consumers/{consumer_id}',
|
|
61
|
+
'DELETE /accounts/{account_id}/queues/{queue_id}/consumers/{consumer_id}',
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
export async function checkCloudflareConformance(): Promise<CloudflareConformanceReport> {
|
|
65
|
+
const violations: string[] = [];
|
|
66
|
+
const actual = new Set(CLOUDFLARE_ROUTER_SURFACE);
|
|
67
|
+
for (const endpoint of EXPECTED) if (!actual.has(endpoint)) violations.push(`router missing literal claim ${endpoint}`);
|
|
68
|
+
for (const endpoint of actual) if (!(EXPECTED as readonly string[]).includes(endpoint)) violations.push(`router serves unclaimed surface ${endpoint}`);
|
|
69
|
+
|
|
70
|
+
const root = mkdtempSync(join(tmpdir(), 'cloudflare-conformance-'));
|
|
71
|
+
const account = 'a'.repeat(32);
|
|
72
|
+
const zone = 'b'.repeat(32);
|
|
73
|
+
let tick = 0;
|
|
74
|
+
const call = (method: string, path: string, body?: unknown, headers?: Record<string, string>) => handleCloudflareTwinRequest({
|
|
75
|
+
root, method, path,
|
|
76
|
+
headers: { authorization: 'Bearer conformance', ...(headers ?? {}) },
|
|
77
|
+
occurredAt: `2026-08-23T00:00:${String(tick++ % 60).padStart(2, '0')}.${String(Math.floor(tick / 60)).padStart(3, '0')}Z`,
|
|
78
|
+
...(body !== undefined ? { body: typeof body === 'string' ? body : JSON.stringify(body) } : {}),
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
const seeded = await call('POST', '/twin/bootstrap', { accounts: [{ id: account, name: 'A' }], zones: [{ id: zone, account_id: account, name: 'example.test' }] });
|
|
82
|
+
if (seeded.status !== 200 || (seeded.body as any).result.accounts !== 1) violations.push('bootstrap probe failed');
|
|
83
|
+
const accounts = await call('GET', '/accounts');
|
|
84
|
+
if (accounts.status !== 200 || (accounts.body as any).result[0]?.id !== account) violations.push('accounts list probe failed');
|
|
85
|
+
const accountGet = await call('GET', `/accounts/${account}`);
|
|
86
|
+
if ((accountGet.body as any).result?.name !== 'A') violations.push('account get probe failed');
|
|
87
|
+
const zones = await call('GET', '/zones');
|
|
88
|
+
if ((zones.body as any).result[0]?.id !== zone) violations.push('zones list probe failed');
|
|
89
|
+
const zoneGet = await call('GET', `/zones/${zone}`);
|
|
90
|
+
if ((zoneGet.body as any).result?.name !== 'example.test') violations.push('zone get probe failed');
|
|
91
|
+
|
|
92
|
+
const created = await call('POST', `/accounts/${account}/cfd_tunnel`, { name: 'conformance', config_src: 'cloudflare' });
|
|
93
|
+
const tunnelID = (created.body as any).result?.id as string;
|
|
94
|
+
if (created.status !== 200 || !tunnelID) violations.push('tunnel create probe failed');
|
|
95
|
+
const tunnelList = await call('GET', `/accounts/${account}/cfd_tunnel`);
|
|
96
|
+
if ((tunnelList.body as any).result[0]?.name !== 'conformance') violations.push('tunnel list probe failed');
|
|
97
|
+
if ((await call('GET', `/accounts/${account}/cfd_tunnel/${tunnelID}`)).status !== 200) violations.push('tunnel get probe failed');
|
|
98
|
+
if ((await call('PATCH', `/accounts/${account}/cfd_tunnel/${tunnelID}`, { name: 'renamed' })).body && (await call('GET', `/accounts/${account}/cfd_tunnel/${tunnelID}`)).body && ((await call('GET', `/accounts/${account}/cfd_tunnel/${tunnelID}`)).body as any).result?.name !== 'renamed') violations.push('tunnel edit probe failed');
|
|
99
|
+
if (typeof ((await call('GET', `/accounts/${account}/cfd_tunnel/${tunnelID}/token`)).body as any).result !== 'string') violations.push('tunnel token probe failed');
|
|
100
|
+
const config = await call('PUT', `/accounts/${account}/cfd_tunnel/${tunnelID}/configurations`, { config: { ingress: [{ hostname: 'app.example.test', service: 'http://127.0.0.1:8787' }] } });
|
|
101
|
+
if ((config.body as any).result?.version !== 1) violations.push('configuration put probe failed');
|
|
102
|
+
if ((await call('GET', `/accounts/${account}/cfd_tunnel/${tunnelID}/configurations`)).status !== 200) violations.push('configuration get probe failed');
|
|
103
|
+
|
|
104
|
+
const record = await call('POST', `/zones/${zone}/dns_records`, { type: 'CNAME', name: 'app.example.test', content: `${tunnelID}.cfargotunnel.com`, ttl: 1 });
|
|
105
|
+
const recordID = (record.body as any).result?.id as string;
|
|
106
|
+
if (!recordID) violations.push('DNS create probe failed');
|
|
107
|
+
if ((await call('GET', `/zones/${zone}/dns_records`)).body && ((await call('GET', `/zones/${zone}/dns_records`)).body as any).result[0]?.id !== recordID) violations.push('DNS list probe failed');
|
|
108
|
+
if ((await call('GET', `/zones/${zone}/dns_records/${recordID}`)).status !== 200) violations.push('DNS get probe failed');
|
|
109
|
+
if ((await call('PUT', `/zones/${zone}/dns_records/${recordID}`, { type: 'CNAME', name: 'app.example.test', content: 'put.cfargotunnel.com', ttl: 1 })).status !== 200) violations.push('DNS put probe failed');
|
|
110
|
+
if ((await call('PATCH', `/zones/${zone}/dns_records/${recordID}`, { content: 'patch.cfargotunnel.com' })).status !== 200) violations.push('DNS patch probe failed');
|
|
111
|
+
if ((await call('DELETE', `/zones/${zone}/dns_records/${recordID}`)).status !== 200) violations.push('DNS delete probe failed');
|
|
112
|
+
if ((await call('DELETE', `/accounts/${account}/cfd_tunnel/${tunnelID}`)).status !== 200) violations.push('tunnel delete probe failed');
|
|
113
|
+
|
|
114
|
+
// ── provisioning slice ──
|
|
115
|
+
const bucket = await call('POST', `/accounts/${account}/r2/buckets`, { name: 'conformance-bucket' });
|
|
116
|
+
if (bucket.status !== 200 || (bucket.body as any).result?.name !== 'conformance-bucket') violations.push('R2 create probe failed');
|
|
117
|
+
if (((await call('GET', `/accounts/${account}/r2/buckets`)).body as any).result?.buckets?.[0]?.name !== 'conformance-bucket') violations.push('R2 list probe failed');
|
|
118
|
+
if (((await call('GET', `/accounts/${account}/r2/buckets/conformance-bucket`)).body as any).result?.name !== 'conformance-bucket') violations.push('R2 get probe failed');
|
|
119
|
+
|
|
120
|
+
const hyperdrive = await call('POST', `/accounts/${account}/hyperdrive/configs`, {
|
|
121
|
+
name: 'conformance-hd',
|
|
122
|
+
origin: { scheme: 'postgres', database: 'app', user: 'u', password: 'p', host: 'db.example.test', port: 5432 },
|
|
123
|
+
});
|
|
124
|
+
const hyperdriveID = (hyperdrive.body as any).result?.id as string;
|
|
125
|
+
if (hyperdrive.status !== 200 || !hyperdriveID) violations.push('hyperdrive create probe failed');
|
|
126
|
+
if (((await call('GET', `/accounts/${account}/hyperdrive/configs`)).body as any).result?.[0]?.id !== hyperdriveID) violations.push('hyperdrive list probe failed');
|
|
127
|
+
if (((await call('GET', `/accounts/${account}/hyperdrive/configs/${hyperdriveID}`)).body as any).result?.name !== 'conformance-hd') violations.push('hyperdrive get probe failed');
|
|
128
|
+
if (((await call('PUT', `/accounts/${account}/hyperdrive/configs/${hyperdriveID}`, {
|
|
129
|
+
name: 'conformance-hd-2',
|
|
130
|
+
origin: { scheme: 'postgres', database: 'app', user: 'u', password: 'p', host: 'db2.example.test', port: 5432 },
|
|
131
|
+
})).body as any).result?.name !== 'conformance-hd-2') violations.push('hyperdrive put probe failed');
|
|
132
|
+
if (((await call('PATCH', `/accounts/${account}/hyperdrive/configs/${hyperdriveID}`, { name: 'conformance-hd-3' })).body as any).result?.name !== 'conformance-hd-3') violations.push('hyperdrive patch probe failed');
|
|
133
|
+
|
|
134
|
+
const queue = await call('POST', `/accounts/${account}/queues`, { queue_name: 'conformance-queue' });
|
|
135
|
+
const queueID = (queue.body as any).result?.queue_id as string;
|
|
136
|
+
if (queue.status !== 200 || !queueID) violations.push('queue create probe failed');
|
|
137
|
+
if (((await call('GET', `/accounts/${account}/queues`)).body as any).result?.[0]?.queue_id !== queueID) violations.push('queue list probe failed');
|
|
138
|
+
if (((await call('GET', `/accounts/${account}/queues/${queueID}`)).body as any).result?.queue_name !== 'conformance-queue') violations.push('queue get probe failed');
|
|
139
|
+
|
|
140
|
+
const metadata = {
|
|
141
|
+
main_module: 'worker.js',
|
|
142
|
+
compatibility_date: '2026-08-01',
|
|
143
|
+
bindings: [
|
|
144
|
+
{ type: 'r2_bucket', name: 'BUCKET', bucket_name: 'conformance-bucket' },
|
|
145
|
+
{ type: 'queue', name: 'QUEUE', queue_name: 'conformance-queue' },
|
|
146
|
+
{ type: 'hyperdrive', name: 'HYPERDRIVE', id: hyperdriveID },
|
|
147
|
+
{ type: 'durable_object_namespace', name: 'DO', class_name: 'Conformance' },
|
|
148
|
+
],
|
|
149
|
+
migrations: { new_tag: 'v1', new_sqlite_classes: ['Conformance'] },
|
|
150
|
+
};
|
|
151
|
+
const boundary = 'conformance-boundary';
|
|
152
|
+
const upload = await call('PUT', `/accounts/${account}/workers/scripts/conformance-worker`,
|
|
153
|
+
`--${boundary}\r\nContent-Disposition: form-data; name="metadata"\r\nContent-Type: application/json\r\n\r\n${JSON.stringify(metadata)}\r\n--${boundary}\r\nContent-Disposition: form-data; name="files[]"; filename="worker.js"\r\nContent-Type: application/javascript+module\r\n\r\nexport default { fetch() { return new Response("ok"); } };\r\n--${boundary}--\r\n`,
|
|
154
|
+
{ 'content-type': `multipart/form-data; boundary=${boundary}` });
|
|
155
|
+
if (upload.status !== 200 || (upload.body as any).result?.id !== 'conformance-worker') violations.push('worker upload probe failed');
|
|
156
|
+
if (((await call('GET', `/accounts/${account}/workers/scripts`)).body as any).result?.[0]?.id !== 'conformance-worker') violations.push('worker list probe failed');
|
|
157
|
+
const download = await call('GET', `/accounts/${account}/workers/scripts/conformance-worker`);
|
|
158
|
+
if (download.status !== 200 || typeof download.body !== 'string' || !download.body.includes('fetch()')) violations.push('worker download probe failed');
|
|
159
|
+
if (((await call('GET', `/accounts/${account}/workers/durable_objects/namespaces`)).body as any).result?.[0]?.class !== 'Conformance') violations.push('DO namespace list probe failed');
|
|
160
|
+
|
|
161
|
+
if (((await call('PUT', `/accounts/${account}/workers/scripts/conformance-worker/schedules`, [{ cron: '*/5 * * * *' }])).body as any).result?.schedules?.[0]?.cron !== '*/5 * * * *') violations.push('schedules put probe failed');
|
|
162
|
+
if (((await call('GET', `/accounts/${account}/workers/scripts/conformance-worker/schedules`)).body as any).result?.schedules?.length !== 1) violations.push('schedules get probe failed');
|
|
163
|
+
|
|
164
|
+
if (((await call('PUT', `/accounts/${account}/workers/subdomain`, { subdomain: 'conformance-team' })).body as any).result?.subdomain !== 'conformance-team') violations.push('account subdomain put probe failed');
|
|
165
|
+
if (((await call('GET', `/accounts/${account}/workers/subdomain`)).body as any).result?.subdomain !== 'conformance-team') violations.push('account subdomain get probe failed');
|
|
166
|
+
if (((await call('POST', `/accounts/${account}/workers/scripts/conformance-worker/subdomain`, { enabled: true, previews_enabled: true })).body as any).result?.enabled !== true) violations.push('script subdomain post probe failed');
|
|
167
|
+
if (((await call('GET', `/accounts/${account}/workers/scripts/conformance-worker/subdomain`)).body as any).result?.enabled !== true) violations.push('script subdomain get probe failed');
|
|
168
|
+
if (((await call('DELETE', `/accounts/${account}/workers/scripts/conformance-worker/subdomain`)).body as any).result?.enabled !== false) violations.push('script subdomain delete probe failed');
|
|
169
|
+
|
|
170
|
+
const route = await call('POST', `/zones/${zone}/workers/routes`, { pattern: 'example.test/api/*', script: 'conformance-worker' });
|
|
171
|
+
const routeID = (route.body as any).result?.id as string;
|
|
172
|
+
if (route.status !== 200 || !routeID) violations.push('route create probe failed');
|
|
173
|
+
if (((await call('GET', `/zones/${zone}/workers/routes`)).body as any).result?.[0]?.id !== routeID) violations.push('route list probe failed');
|
|
174
|
+
if (((await call('GET', `/zones/${zone}/workers/routes/${routeID}`)).body as any).result?.pattern !== 'example.test/api/*') violations.push('route get probe failed');
|
|
175
|
+
if (((await call('PUT', `/zones/${zone}/workers/routes/${routeID}`, { pattern: 'example.test/v2/*', script: 'conformance-worker' })).body as any).result?.pattern !== 'example.test/v2/*') violations.push('route put probe failed');
|
|
176
|
+
|
|
177
|
+
const domain = await call('PUT', `/accounts/${account}/workers/domains`, { hostname: 'api.example.test', service: 'conformance-worker' });
|
|
178
|
+
const domainID = (domain.body as any).result?.id as string;
|
|
179
|
+
if (domain.status !== 200 || (domain.body as any).result?.zone_id !== zone) violations.push('domain attach probe failed');
|
|
180
|
+
if (((await call('GET', `/accounts/${account}/workers/domains`)).body as any).result?.[0]?.id !== domainID) violations.push('domain list probe failed');
|
|
181
|
+
if (((await call('GET', `/accounts/${account}/workers/domains/${domainID}`)).body as any).result?.hostname !== 'api.example.test') violations.push('domain get probe failed');
|
|
182
|
+
|
|
183
|
+
const consumer = await call('POST', `/accounts/${account}/queues/${queueID}/consumers`, { type: 'worker', script_name: 'conformance-worker' });
|
|
184
|
+
const consumerID = (consumer.body as any).result?.consumer_id as string;
|
|
185
|
+
if (consumer.status !== 200 || !consumerID) violations.push('consumer create probe failed');
|
|
186
|
+
if (((await call('GET', `/accounts/${account}/queues/${queueID}/consumers`)).body as any).result?.[0]?.consumer_id !== consumerID) violations.push('consumer list probe failed');
|
|
187
|
+
if (((await call('GET', `/accounts/${account}/queues/${queueID}/consumers/${consumerID}`)).body as any).result?.type !== 'worker') violations.push('consumer get probe failed');
|
|
188
|
+
if (((await call('PUT', `/accounts/${account}/queues/${queueID}/consumers/${consumerID}`, { type: 'worker', script_name: 'conformance-worker', settings: { batch_size: 5 } })).body as any).result?.settings?.batch_size !== 5) violations.push('consumer put probe failed');
|
|
189
|
+
if ((await call('DELETE', `/accounts/${account}/queues/${queueID}/consumers/${consumerID}`)).status !== 200) violations.push('consumer delete probe failed');
|
|
190
|
+
|
|
191
|
+
// teardown probes (delete endpoints)
|
|
192
|
+
if ((await call('DELETE', `/accounts/${account}/workers/domains/${domainID}`)).status !== 200) violations.push('domain delete probe failed');
|
|
193
|
+
if ((await call('DELETE', `/zones/${zone}/workers/routes/${routeID}`)).status !== 200) violations.push('route delete probe failed');
|
|
194
|
+
if ((await call('DELETE', `/accounts/${account}/workers/subdomain`)).status !== 204) violations.push('account subdomain delete probe failed');
|
|
195
|
+
if ((await call('DELETE', `/accounts/${account}/workers/scripts/conformance-worker`)).status !== 400) violations.push('worker delete without force should be blocked by its Durable Object namespace');
|
|
196
|
+
if ((await call('DELETE', `/accounts/${account}/workers/scripts/conformance-worker?force=true`)).status !== 200) violations.push('worker force delete probe failed');
|
|
197
|
+
if ((await call('DELETE', `/accounts/${account}/queues/${queueID}`)).status !== 200) violations.push('queue delete probe failed');
|
|
198
|
+
if ((await call('DELETE', `/accounts/${account}/hyperdrive/configs/${hyperdriveID}`)).status !== 200) violations.push('hyperdrive delete probe failed');
|
|
199
|
+
if ((await call('DELETE', `/accounts/${account}/r2/buckets/conformance-bucket`)).status !== 200) violations.push('R2 delete probe failed');
|
|
200
|
+
} finally {
|
|
201
|
+
rmSync(root, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
return { ok: violations.length === 0, endpointsChecked: EXPECTED.length, violations };
|
|
204
|
+
}
|