@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,59 @@
|
|
|
1
|
+
export declare const SERVICE = "cloudflare";
|
|
2
|
+
export declare const CLOUDFLARE_RESOURCE_TYPES: readonly ["account", "zone", "cloudflare_tunnel", "tunnel_configuration", "dns_record", "worker_script", "worker_route", "worker_domain", "workers_subdomain", "durable_object_namespace", "r2_bucket", "r2_object", "r2_custom_domain", "hyperdrive_config", "queue", "queue_consumer", "worker_asset_upload_session", "worker_asset", "worker_version", "account_member", "zone_setting", "user_token", "audit_log"];
|
|
3
|
+
export type CloudflareResourceType = (typeof CLOUDFLARE_RESOURCE_TYPES)[number];
|
|
4
|
+
export type CloudflareRequest = {
|
|
5
|
+
method: string;
|
|
6
|
+
path: string;
|
|
7
|
+
body?: string;
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
occurredAt?: string;
|
|
10
|
+
root?: string;
|
|
11
|
+
readOnly?: boolean;
|
|
12
|
+
};
|
|
13
|
+
/** `raw: true` responses send `body` verbatim (or an empty body when null) with the given
|
|
14
|
+
* content-type headers — used by Worker script download, which is not JSON-enveloped. */
|
|
15
|
+
export type CloudflareResponse = {
|
|
16
|
+
status: number;
|
|
17
|
+
body: unknown;
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
raw?: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type Row = Record<string, unknown> & {
|
|
22
|
+
id: string;
|
|
23
|
+
type: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function headers(input?: Record<string, string>): Record<string, string>;
|
|
26
|
+
export declare function success(result: unknown, resultInfo?: Record<string, unknown>): CloudflareResponse;
|
|
27
|
+
export declare function failure(status: number, code: number, message: string): CloudflareResponse;
|
|
28
|
+
export declare function parseBody(req: CloudflareRequest): Record<string, unknown> | CloudflareResponse;
|
|
29
|
+
export declare function isResponse(value: unknown): value is CloudflareResponse;
|
|
30
|
+
/** The rows of a type (protocol 2: a subject's id is the vendor's, with no type prefix, and a delete is the
|
|
31
|
+
* kernel's tombstone `deleted: true` — which the subject's next write clears). */
|
|
32
|
+
export declare function rows(type: CloudflareResourceType, root?: string, includeDeleted?: boolean): Row[];
|
|
33
|
+
export declare function row(type: CloudflareResourceType, id: string, root?: string, includeDeleted?: boolean): Row | undefined;
|
|
34
|
+
export declare function deterministicHex(label: string, length: number): string;
|
|
35
|
+
export declare function uuidFrom(label: string): string;
|
|
36
|
+
/** Mint a fresh id for `type`, scanning the id-SET already in state (tombstones included) so
|
|
37
|
+
* delete/recreate and pulled-vendor ids can never collide with a local mint. */
|
|
38
|
+
export declare function mint(type: CloudflareResourceType, label: string, root?: string): string;
|
|
39
|
+
export declare function write(type: CloudflareResourceType, id: string, operation: string, fields: Record<string, unknown>, req: CloudflareRequest): Promise<void>;
|
|
40
|
+
export declare function paginate<T>(items: T[], url: URL, defaultPerPage?: number, maxPerPage?: number): {
|
|
41
|
+
result: T[];
|
|
42
|
+
info: Record<string, number>;
|
|
43
|
+
};
|
|
44
|
+
export declare function readOnly(req: CloudflareRequest): CloudflareResponse | null;
|
|
45
|
+
export declare function scopeMissing(kind: 'account' | 'zone' | 'tunnel', id: string): CloudflareResponse;
|
|
46
|
+
export declare function noRoute(): CloudflareResponse;
|
|
47
|
+
/** The user a request's API token belongs to (a token minted at POST /twin/users/{email}/tokens); empty for any other. */
|
|
48
|
+
export declare function callerEmail(req: CloudflareRequest): string;
|
|
49
|
+
/** One entry in an account's audit log, attributed to the calling token's user. */
|
|
50
|
+
export declare function audit(req: CloudflareRequest, accountID: string, entry: {
|
|
51
|
+
type: string;
|
|
52
|
+
resource: {
|
|
53
|
+
type: string;
|
|
54
|
+
id: string;
|
|
55
|
+
};
|
|
56
|
+
oldValue?: unknown;
|
|
57
|
+
newValue?: unknown;
|
|
58
|
+
metadata?: Record<string, unknown>;
|
|
59
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
export const SERVICE = 'cloudflare';
|
|
8
|
+
export const CLOUDFLARE_RESOURCE_TYPES = [
|
|
9
|
+
'account',
|
|
10
|
+
'zone',
|
|
11
|
+
'cloudflare_tunnel',
|
|
12
|
+
'tunnel_configuration',
|
|
13
|
+
'dns_record',
|
|
14
|
+
'worker_script',
|
|
15
|
+
'worker_route',
|
|
16
|
+
'worker_domain',
|
|
17
|
+
'workers_subdomain',
|
|
18
|
+
'durable_object_namespace',
|
|
19
|
+
'r2_bucket',
|
|
20
|
+
'r2_object',
|
|
21
|
+
'r2_custom_domain',
|
|
22
|
+
'hyperdrive_config',
|
|
23
|
+
'queue',
|
|
24
|
+
'queue_consumer',
|
|
25
|
+
'worker_asset_upload_session',
|
|
26
|
+
'worker_asset',
|
|
27
|
+
'worker_version',
|
|
28
|
+
'account_member',
|
|
29
|
+
'zone_setting',
|
|
30
|
+
'user_token',
|
|
31
|
+
'audit_log',
|
|
32
|
+
];
|
|
33
|
+
export function headers(input) {
|
|
34
|
+
return Object.fromEntries(Object.entries(input ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
|
|
35
|
+
}
|
|
36
|
+
export function success(result, resultInfo) {
|
|
37
|
+
return {
|
|
38
|
+
status: 200,
|
|
39
|
+
body: {
|
|
40
|
+
success: true,
|
|
41
|
+
errors: [],
|
|
42
|
+
messages: [],
|
|
43
|
+
result,
|
|
44
|
+
...(resultInfo ? { result_info: resultInfo } : {}),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function failure(status, code, message) {
|
|
49
|
+
return {
|
|
50
|
+
status,
|
|
51
|
+
body: { success: false, errors: [{ code, message }], messages: [], result: null },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function parseBody(req) {
|
|
55
|
+
try {
|
|
56
|
+
const value = req.body ? JSON.parse(req.body) : {};
|
|
57
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
58
|
+
return failure(400, 1004, 'Invalid request body');
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return failure(400, 1004, 'Invalid request body');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function isResponse(value) {
|
|
66
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
67
|
+
&& typeof value.status === 'number'
|
|
68
|
+
&& 'body' in value;
|
|
69
|
+
}
|
|
70
|
+
/** The rows of a type (protocol 2: a subject's id is the vendor's, with no type prefix, and a delete is the
|
|
71
|
+
* kernel's tombstone `deleted: true` — which the subject's next write clears). */
|
|
72
|
+
export function rows(type, root, includeDeleted = false) {
|
|
73
|
+
return projectResources(SERVICE, root)
|
|
74
|
+
.filter((item) => item.type === type && (includeDeleted || item.deleted !== true))
|
|
75
|
+
.map((item) => ({ ...item }));
|
|
76
|
+
}
|
|
77
|
+
export function row(type, id, root, includeDeleted = false) {
|
|
78
|
+
return rows(type, root, includeDeleted).find((item) => item.id === id);
|
|
79
|
+
}
|
|
80
|
+
export function deterministicHex(label, length) {
|
|
81
|
+
let out = '';
|
|
82
|
+
for (let i = 0; out.length < length; i++)
|
|
83
|
+
out += createHash('sha256').update(`${label}:${i}`).digest('hex');
|
|
84
|
+
return out.slice(0, length);
|
|
85
|
+
}
|
|
86
|
+
export function uuidFrom(label) {
|
|
87
|
+
const hex = deterministicHex(label, 32);
|
|
88
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
89
|
+
}
|
|
90
|
+
/** Mint a fresh id for `type`, scanning the id-SET already in state (tombstones included) so
|
|
91
|
+
* delete/recreate and pulled-vendor ids can never collide with a local mint. */
|
|
92
|
+
export function mint(type, label, root) {
|
|
93
|
+
const taken = new Set(rows(type, root, true).map((item) => item.id));
|
|
94
|
+
for (let salt = 0;; salt++) {
|
|
95
|
+
const id = type === 'cloudflare_tunnel' || type === 'worker_version' ? uuidFrom(`${label}:${salt}`) : deterministicHex(`${label}:${salt}`, 32);
|
|
96
|
+
if (!taken.has(id))
|
|
97
|
+
return id;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export async function write(type, id, operation, fields, req) {
|
|
101
|
+
await applyTwinWrite(SERVICE, {
|
|
102
|
+
operation,
|
|
103
|
+
subjectType: type,
|
|
104
|
+
subjectId: id, // protocol 2: the subject's id is the vendor's, the type is the subject's own
|
|
105
|
+
fields,
|
|
106
|
+
...(req.occurredAt ? { occurredAt: req.occurredAt } : {}),
|
|
107
|
+
actor: { kind: 'agent' },
|
|
108
|
+
}, req.root);
|
|
109
|
+
}
|
|
110
|
+
export function paginate(items, url, defaultPerPage = 20, maxPerPage = 50) {
|
|
111
|
+
const page = Math.max(1, Number(url.searchParams.get('page') ?? 1) || 1);
|
|
112
|
+
const perPage = Math.min(maxPerPage, Math.max(1, Number(url.searchParams.get('per_page') ?? defaultPerPage) || defaultPerPage));
|
|
113
|
+
const start = (page - 1) * perPage;
|
|
114
|
+
const result = items.slice(start, start + perPage);
|
|
115
|
+
return {
|
|
116
|
+
result,
|
|
117
|
+
info: { page, per_page: perPage, count: result.length, total_count: items.length, total_pages: Math.max(1, Math.ceil(items.length / perPage)) },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function readOnly(req) {
|
|
121
|
+
return req.readOnly ? failure(405, 1000, 'Method not allowed in read-only mode') : null;
|
|
122
|
+
}
|
|
123
|
+
export function scopeMissing(kind, id) {
|
|
124
|
+
return failure(404, 7003, `Could not route to /${kind}/${id}, perhaps your object identifier is invalid?`);
|
|
125
|
+
}
|
|
126
|
+
export function noRoute() {
|
|
127
|
+
return failure(404, 7000, 'No route for that URI');
|
|
128
|
+
}
|
|
129
|
+
/** The user a request's API token belongs to (a token minted at POST /twin/users/{email}/tokens); empty for any other. */
|
|
130
|
+
export function callerEmail(req) {
|
|
131
|
+
const bearer = /^Bearer\s+(\S+)$/i.exec(headers(req.headers).authorization ?? '')?.[1] ?? '';
|
|
132
|
+
return bearer ? String(rows('user_token', req.root).find((t) => t.token === bearer)?.email ?? '') : '';
|
|
133
|
+
}
|
|
134
|
+
/** One entry in an account's audit log, attributed to the calling token's user. */
|
|
135
|
+
export async function audit(req, accountID, entry) {
|
|
136
|
+
const when = req.occurredAt ?? new Date().toISOString();
|
|
137
|
+
const id = deterministicHex(`audit:${accountID}:${when}:${entry.resource.type}:${entry.resource.id}:${rows('audit_log', req.root).length}`, 32);
|
|
138
|
+
const email = callerEmail(req);
|
|
139
|
+
await write('audit_log', id, 'audit_log.record', { account_id: accountID, when, action: { type: entry.type, result: true, info: '' },
|
|
140
|
+
actor: { email, id: email ? deterministicHex(`user:${email}`, 32) : '', ip: '', type: email ? 'user' : 'token' },
|
|
141
|
+
interface: 'API', resource: entry.resource, oldValue: entry.oldValue ?? null, newValue: entry.newValue ?? null, metadata: entry.metadata ?? {}, owner: { id: accountID } }, req);
|
|
142
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
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.js";
|
|
8
|
+
const EXPECTED = [
|
|
9
|
+
'POST /twin/bootstrap', 'GET /accounts', 'GET /accounts/{account_id}', 'GET /zones', 'GET /zones/{zone_id}',
|
|
10
|
+
'GET /accounts/{account_id}/cfd_tunnel', 'POST /accounts/{account_id}/cfd_tunnel',
|
|
11
|
+
'GET /accounts/{account_id}/cfd_tunnel/{tunnel_id}', 'PATCH /accounts/{account_id}/cfd_tunnel/{tunnel_id}',
|
|
12
|
+
'DELETE /accounts/{account_id}/cfd_tunnel/{tunnel_id}', 'GET /accounts/{account_id}/cfd_tunnel/{tunnel_id}/token',
|
|
13
|
+
'GET /accounts/{account_id}/cfd_tunnel/{tunnel_id}/configurations', 'PUT /accounts/{account_id}/cfd_tunnel/{tunnel_id}/configurations',
|
|
14
|
+
'GET /zones/{zone_id}/dns_records', 'POST /zones/{zone_id}/dns_records', 'GET /zones/{zone_id}/dns_records/{dns_record_id}',
|
|
15
|
+
'PUT /zones/{zone_id}/dns_records/{dns_record_id}', 'PATCH /zones/{zone_id}/dns_records/{dns_record_id}',
|
|
16
|
+
'DELETE /zones/{zone_id}/dns_records/{dns_record_id}',
|
|
17
|
+
// Workers / R2 / Hyperdrive / Queues provisioning control plane.
|
|
18
|
+
'GET /accounts/{account_id}/workers/scripts',
|
|
19
|
+
'PUT /accounts/{account_id}/workers/scripts/{script_name}',
|
|
20
|
+
'GET /accounts/{account_id}/workers/scripts/{script_name}',
|
|
21
|
+
'DELETE /accounts/{account_id}/workers/scripts/{script_name}',
|
|
22
|
+
'GET /accounts/{account_id}/workers/scripts/{script_name}/schedules',
|
|
23
|
+
'PUT /accounts/{account_id}/workers/scripts/{script_name}/schedules',
|
|
24
|
+
'GET /accounts/{account_id}/workers/scripts/{script_name}/subdomain',
|
|
25
|
+
'POST /accounts/{account_id}/workers/scripts/{script_name}/subdomain',
|
|
26
|
+
'DELETE /accounts/{account_id}/workers/scripts/{script_name}/subdomain',
|
|
27
|
+
'GET /accounts/{account_id}/workers/subdomain',
|
|
28
|
+
'PUT /accounts/{account_id}/workers/subdomain',
|
|
29
|
+
'DELETE /accounts/{account_id}/workers/subdomain',
|
|
30
|
+
'GET /accounts/{account_id}/workers/domains',
|
|
31
|
+
'PUT /accounts/{account_id}/workers/domains',
|
|
32
|
+
'GET /accounts/{account_id}/workers/domains/{domain_id}',
|
|
33
|
+
'DELETE /accounts/{account_id}/workers/domains/{domain_id}',
|
|
34
|
+
'GET /accounts/{account_id}/workers/durable_objects/namespaces',
|
|
35
|
+
'GET /zones/{zone_id}/workers/routes',
|
|
36
|
+
'POST /zones/{zone_id}/workers/routes',
|
|
37
|
+
'GET /zones/{zone_id}/workers/routes/{route_id}',
|
|
38
|
+
'PUT /zones/{zone_id}/workers/routes/{route_id}',
|
|
39
|
+
'DELETE /zones/{zone_id}/workers/routes/{route_id}',
|
|
40
|
+
'GET /accounts/{account_id}/r2/buckets',
|
|
41
|
+
'POST /accounts/{account_id}/r2/buckets',
|
|
42
|
+
'GET /accounts/{account_id}/r2/buckets/{bucket_name}',
|
|
43
|
+
'DELETE /accounts/{account_id}/r2/buckets/{bucket_name}',
|
|
44
|
+
'GET /accounts/{account_id}/hyperdrive/configs',
|
|
45
|
+
'POST /accounts/{account_id}/hyperdrive/configs',
|
|
46
|
+
'GET /accounts/{account_id}/hyperdrive/configs/{hyperdrive_id}',
|
|
47
|
+
'PUT /accounts/{account_id}/hyperdrive/configs/{hyperdrive_id}',
|
|
48
|
+
'PATCH /accounts/{account_id}/hyperdrive/configs/{hyperdrive_id}',
|
|
49
|
+
'DELETE /accounts/{account_id}/hyperdrive/configs/{hyperdrive_id}',
|
|
50
|
+
'GET /accounts/{account_id}/queues',
|
|
51
|
+
'POST /accounts/{account_id}/queues',
|
|
52
|
+
'GET /accounts/{account_id}/queues/{queue_id}',
|
|
53
|
+
'DELETE /accounts/{account_id}/queues/{queue_id}',
|
|
54
|
+
'GET /accounts/{account_id}/queues/{queue_id}/consumers',
|
|
55
|
+
'POST /accounts/{account_id}/queues/{queue_id}/consumers',
|
|
56
|
+
'GET /accounts/{account_id}/queues/{queue_id}/consumers/{consumer_id}',
|
|
57
|
+
'PUT /accounts/{account_id}/queues/{queue_id}/consumers/{consumer_id}',
|
|
58
|
+
'DELETE /accounts/{account_id}/queues/{queue_id}/consumers/{consumer_id}',
|
|
59
|
+
];
|
|
60
|
+
export async function checkCloudflareConformance() {
|
|
61
|
+
const violations = [];
|
|
62
|
+
const actual = new Set(CLOUDFLARE_ROUTER_SURFACE);
|
|
63
|
+
for (const endpoint of EXPECTED)
|
|
64
|
+
if (!actual.has(endpoint))
|
|
65
|
+
violations.push(`router missing literal claim ${endpoint}`);
|
|
66
|
+
for (const endpoint of actual)
|
|
67
|
+
if (!EXPECTED.includes(endpoint))
|
|
68
|
+
violations.push(`router serves unclaimed surface ${endpoint}`);
|
|
69
|
+
const root = mkdtempSync(join(tmpdir(), 'cloudflare-conformance-'));
|
|
70
|
+
const account = 'a'.repeat(32);
|
|
71
|
+
const zone = 'b'.repeat(32);
|
|
72
|
+
let tick = 0;
|
|
73
|
+
const call = (method, path, body, headers) => handleCloudflareTwinRequest({
|
|
74
|
+
root, method, path,
|
|
75
|
+
headers: { authorization: 'Bearer conformance', ...(headers ?? {}) },
|
|
76
|
+
occurredAt: `2026-08-23T00:00:${String(tick++ % 60).padStart(2, '0')}.${String(Math.floor(tick / 60)).padStart(3, '0')}Z`,
|
|
77
|
+
...(body !== undefined ? { body: typeof body === 'string' ? body : JSON.stringify(body) } : {}),
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
const seeded = await call('POST', '/twin/bootstrap', { accounts: [{ id: account, name: 'A' }], zones: [{ id: zone, account_id: account, name: 'example.test' }] });
|
|
81
|
+
if (seeded.status !== 200 || seeded.body.result.accounts !== 1)
|
|
82
|
+
violations.push('bootstrap probe failed');
|
|
83
|
+
const accounts = await call('GET', '/accounts');
|
|
84
|
+
if (accounts.status !== 200 || accounts.body.result[0]?.id !== account)
|
|
85
|
+
violations.push('accounts list probe failed');
|
|
86
|
+
const accountGet = await call('GET', `/accounts/${account}`);
|
|
87
|
+
if (accountGet.body.result?.name !== 'A')
|
|
88
|
+
violations.push('account get probe failed');
|
|
89
|
+
const zones = await call('GET', '/zones');
|
|
90
|
+
if (zones.body.result[0]?.id !== zone)
|
|
91
|
+
violations.push('zones list probe failed');
|
|
92
|
+
const zoneGet = await call('GET', `/zones/${zone}`);
|
|
93
|
+
if (zoneGet.body.result?.name !== 'example.test')
|
|
94
|
+
violations.push('zone get probe failed');
|
|
95
|
+
const created = await call('POST', `/accounts/${account}/cfd_tunnel`, { name: 'conformance', config_src: 'cloudflare' });
|
|
96
|
+
const tunnelID = created.body.result?.id;
|
|
97
|
+
if (created.status !== 200 || !tunnelID)
|
|
98
|
+
violations.push('tunnel create probe failed');
|
|
99
|
+
const tunnelList = await call('GET', `/accounts/${account}/cfd_tunnel`);
|
|
100
|
+
if (tunnelList.body.result[0]?.name !== 'conformance')
|
|
101
|
+
violations.push('tunnel list probe failed');
|
|
102
|
+
if ((await call('GET', `/accounts/${account}/cfd_tunnel/${tunnelID}`)).status !== 200)
|
|
103
|
+
violations.push('tunnel get probe failed');
|
|
104
|
+
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.result?.name !== 'renamed')
|
|
105
|
+
violations.push('tunnel edit probe failed');
|
|
106
|
+
if (typeof (await call('GET', `/accounts/${account}/cfd_tunnel/${tunnelID}/token`)).body.result !== 'string')
|
|
107
|
+
violations.push('tunnel token probe failed');
|
|
108
|
+
const config = await call('PUT', `/accounts/${account}/cfd_tunnel/${tunnelID}/configurations`, { config: { ingress: [{ hostname: 'app.example.test', service: 'http://127.0.0.1:8787' }] } });
|
|
109
|
+
if (config.body.result?.version !== 1)
|
|
110
|
+
violations.push('configuration put probe failed');
|
|
111
|
+
if ((await call('GET', `/accounts/${account}/cfd_tunnel/${tunnelID}/configurations`)).status !== 200)
|
|
112
|
+
violations.push('configuration get probe failed');
|
|
113
|
+
const record = await call('POST', `/zones/${zone}/dns_records`, { type: 'CNAME', name: 'app.example.test', content: `${tunnelID}.cfargotunnel.com`, ttl: 1 });
|
|
114
|
+
const recordID = record.body.result?.id;
|
|
115
|
+
if (!recordID)
|
|
116
|
+
violations.push('DNS create probe failed');
|
|
117
|
+
if ((await call('GET', `/zones/${zone}/dns_records`)).body && (await call('GET', `/zones/${zone}/dns_records`)).body.result[0]?.id !== recordID)
|
|
118
|
+
violations.push('DNS list probe failed');
|
|
119
|
+
if ((await call('GET', `/zones/${zone}/dns_records/${recordID}`)).status !== 200)
|
|
120
|
+
violations.push('DNS get probe failed');
|
|
121
|
+
if ((await call('PUT', `/zones/${zone}/dns_records/${recordID}`, { type: 'CNAME', name: 'app.example.test', content: 'put.cfargotunnel.com', ttl: 1 })).status !== 200)
|
|
122
|
+
violations.push('DNS put probe failed');
|
|
123
|
+
if ((await call('PATCH', `/zones/${zone}/dns_records/${recordID}`, { content: 'patch.cfargotunnel.com' })).status !== 200)
|
|
124
|
+
violations.push('DNS patch probe failed');
|
|
125
|
+
if ((await call('DELETE', `/zones/${zone}/dns_records/${recordID}`)).status !== 200)
|
|
126
|
+
violations.push('DNS delete probe failed');
|
|
127
|
+
if ((await call('DELETE', `/accounts/${account}/cfd_tunnel/${tunnelID}`)).status !== 200)
|
|
128
|
+
violations.push('tunnel delete probe failed');
|
|
129
|
+
// ── provisioning slice ──
|
|
130
|
+
const bucket = await call('POST', `/accounts/${account}/r2/buckets`, { name: 'conformance-bucket' });
|
|
131
|
+
if (bucket.status !== 200 || bucket.body.result?.name !== 'conformance-bucket')
|
|
132
|
+
violations.push('R2 create probe failed');
|
|
133
|
+
if ((await call('GET', `/accounts/${account}/r2/buckets`)).body.result?.buckets?.[0]?.name !== 'conformance-bucket')
|
|
134
|
+
violations.push('R2 list probe failed');
|
|
135
|
+
if ((await call('GET', `/accounts/${account}/r2/buckets/conformance-bucket`)).body.result?.name !== 'conformance-bucket')
|
|
136
|
+
violations.push('R2 get probe failed');
|
|
137
|
+
const hyperdrive = await call('POST', `/accounts/${account}/hyperdrive/configs`, {
|
|
138
|
+
name: 'conformance-hd',
|
|
139
|
+
origin: { scheme: 'postgres', database: 'app', user: 'u', password: 'p', host: 'db.example.test', port: 5432 },
|
|
140
|
+
});
|
|
141
|
+
const hyperdriveID = hyperdrive.body.result?.id;
|
|
142
|
+
if (hyperdrive.status !== 200 || !hyperdriveID)
|
|
143
|
+
violations.push('hyperdrive create probe failed');
|
|
144
|
+
if ((await call('GET', `/accounts/${account}/hyperdrive/configs`)).body.result?.[0]?.id !== hyperdriveID)
|
|
145
|
+
violations.push('hyperdrive list probe failed');
|
|
146
|
+
if ((await call('GET', `/accounts/${account}/hyperdrive/configs/${hyperdriveID}`)).body.result?.name !== 'conformance-hd')
|
|
147
|
+
violations.push('hyperdrive get probe failed');
|
|
148
|
+
if ((await call('PUT', `/accounts/${account}/hyperdrive/configs/${hyperdriveID}`, {
|
|
149
|
+
name: 'conformance-hd-2',
|
|
150
|
+
origin: { scheme: 'postgres', database: 'app', user: 'u', password: 'p', host: 'db2.example.test', port: 5432 },
|
|
151
|
+
})).body.result?.name !== 'conformance-hd-2')
|
|
152
|
+
violations.push('hyperdrive put probe failed');
|
|
153
|
+
if ((await call('PATCH', `/accounts/${account}/hyperdrive/configs/${hyperdriveID}`, { name: 'conformance-hd-3' })).body.result?.name !== 'conformance-hd-3')
|
|
154
|
+
violations.push('hyperdrive patch probe failed');
|
|
155
|
+
const queue = await call('POST', `/accounts/${account}/queues`, { queue_name: 'conformance-queue' });
|
|
156
|
+
const queueID = queue.body.result?.queue_id;
|
|
157
|
+
if (queue.status !== 200 || !queueID)
|
|
158
|
+
violations.push('queue create probe failed');
|
|
159
|
+
if ((await call('GET', `/accounts/${account}/queues`)).body.result?.[0]?.queue_id !== queueID)
|
|
160
|
+
violations.push('queue list probe failed');
|
|
161
|
+
if ((await call('GET', `/accounts/${account}/queues/${queueID}`)).body.result?.queue_name !== 'conformance-queue')
|
|
162
|
+
violations.push('queue get probe failed');
|
|
163
|
+
const metadata = {
|
|
164
|
+
main_module: 'worker.js',
|
|
165
|
+
compatibility_date: '2026-08-01',
|
|
166
|
+
bindings: [
|
|
167
|
+
{ type: 'r2_bucket', name: 'BUCKET', bucket_name: 'conformance-bucket' },
|
|
168
|
+
{ type: 'queue', name: 'QUEUE', queue_name: 'conformance-queue' },
|
|
169
|
+
{ type: 'hyperdrive', name: 'HYPERDRIVE', id: hyperdriveID },
|
|
170
|
+
{ type: 'durable_object_namespace', name: 'DO', class_name: 'Conformance' },
|
|
171
|
+
],
|
|
172
|
+
migrations: { new_tag: 'v1', new_sqlite_classes: ['Conformance'] },
|
|
173
|
+
};
|
|
174
|
+
const boundary = 'conformance-boundary';
|
|
175
|
+
const upload = await call('PUT', `/accounts/${account}/workers/scripts/conformance-worker`, `--${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`, { 'content-type': `multipart/form-data; boundary=${boundary}` });
|
|
176
|
+
if (upload.status !== 200 || upload.body.result?.id !== 'conformance-worker')
|
|
177
|
+
violations.push('worker upload probe failed');
|
|
178
|
+
if ((await call('GET', `/accounts/${account}/workers/scripts`)).body.result?.[0]?.id !== 'conformance-worker')
|
|
179
|
+
violations.push('worker list probe failed');
|
|
180
|
+
const download = await call('GET', `/accounts/${account}/workers/scripts/conformance-worker`);
|
|
181
|
+
if (download.status !== 200 || typeof download.body !== 'string' || !download.body.includes('fetch()'))
|
|
182
|
+
violations.push('worker download probe failed');
|
|
183
|
+
if ((await call('GET', `/accounts/${account}/workers/durable_objects/namespaces`)).body.result?.[0]?.class !== 'Conformance')
|
|
184
|
+
violations.push('DO namespace list probe failed');
|
|
185
|
+
if ((await call('PUT', `/accounts/${account}/workers/scripts/conformance-worker/schedules`, [{ cron: '*/5 * * * *' }])).body.result?.schedules?.[0]?.cron !== '*/5 * * * *')
|
|
186
|
+
violations.push('schedules put probe failed');
|
|
187
|
+
if ((await call('GET', `/accounts/${account}/workers/scripts/conformance-worker/schedules`)).body.result?.schedules?.length !== 1)
|
|
188
|
+
violations.push('schedules get probe failed');
|
|
189
|
+
if ((await call('PUT', `/accounts/${account}/workers/subdomain`, { subdomain: 'conformance-team' })).body.result?.subdomain !== 'conformance-team')
|
|
190
|
+
violations.push('account subdomain put probe failed');
|
|
191
|
+
if ((await call('GET', `/accounts/${account}/workers/subdomain`)).body.result?.subdomain !== 'conformance-team')
|
|
192
|
+
violations.push('account subdomain get probe failed');
|
|
193
|
+
if ((await call('POST', `/accounts/${account}/workers/scripts/conformance-worker/subdomain`, { enabled: true, previews_enabled: true })).body.result?.enabled !== true)
|
|
194
|
+
violations.push('script subdomain post probe failed');
|
|
195
|
+
if ((await call('GET', `/accounts/${account}/workers/scripts/conformance-worker/subdomain`)).body.result?.enabled !== true)
|
|
196
|
+
violations.push('script subdomain get probe failed');
|
|
197
|
+
if ((await call('DELETE', `/accounts/${account}/workers/scripts/conformance-worker/subdomain`)).body.result?.enabled !== false)
|
|
198
|
+
violations.push('script subdomain delete probe failed');
|
|
199
|
+
const route = await call('POST', `/zones/${zone}/workers/routes`, { pattern: 'example.test/api/*', script: 'conformance-worker' });
|
|
200
|
+
const routeID = route.body.result?.id;
|
|
201
|
+
if (route.status !== 200 || !routeID)
|
|
202
|
+
violations.push('route create probe failed');
|
|
203
|
+
if ((await call('GET', `/zones/${zone}/workers/routes`)).body.result?.[0]?.id !== routeID)
|
|
204
|
+
violations.push('route list probe failed');
|
|
205
|
+
if ((await call('GET', `/zones/${zone}/workers/routes/${routeID}`)).body.result?.pattern !== 'example.test/api/*')
|
|
206
|
+
violations.push('route get probe failed');
|
|
207
|
+
if ((await call('PUT', `/zones/${zone}/workers/routes/${routeID}`, { pattern: 'example.test/v2/*', script: 'conformance-worker' })).body.result?.pattern !== 'example.test/v2/*')
|
|
208
|
+
violations.push('route put probe failed');
|
|
209
|
+
const domain = await call('PUT', `/accounts/${account}/workers/domains`, { hostname: 'api.example.test', service: 'conformance-worker' });
|
|
210
|
+
const domainID = domain.body.result?.id;
|
|
211
|
+
if (domain.status !== 200 || domain.body.result?.zone_id !== zone)
|
|
212
|
+
violations.push('domain attach probe failed');
|
|
213
|
+
if ((await call('GET', `/accounts/${account}/workers/domains`)).body.result?.[0]?.id !== domainID)
|
|
214
|
+
violations.push('domain list probe failed');
|
|
215
|
+
if ((await call('GET', `/accounts/${account}/workers/domains/${domainID}`)).body.result?.hostname !== 'api.example.test')
|
|
216
|
+
violations.push('domain get probe failed');
|
|
217
|
+
const consumer = await call('POST', `/accounts/${account}/queues/${queueID}/consumers`, { type: 'worker', script_name: 'conformance-worker' });
|
|
218
|
+
const consumerID = consumer.body.result?.consumer_id;
|
|
219
|
+
if (consumer.status !== 200 || !consumerID)
|
|
220
|
+
violations.push('consumer create probe failed');
|
|
221
|
+
if ((await call('GET', `/accounts/${account}/queues/${queueID}/consumers`)).body.result?.[0]?.consumer_id !== consumerID)
|
|
222
|
+
violations.push('consumer list probe failed');
|
|
223
|
+
if ((await call('GET', `/accounts/${account}/queues/${queueID}/consumers/${consumerID}`)).body.result?.type !== 'worker')
|
|
224
|
+
violations.push('consumer get probe failed');
|
|
225
|
+
if ((await call('PUT', `/accounts/${account}/queues/${queueID}/consumers/${consumerID}`, { type: 'worker', script_name: 'conformance-worker', settings: { batch_size: 5 } })).body.result?.settings?.batch_size !== 5)
|
|
226
|
+
violations.push('consumer put probe failed');
|
|
227
|
+
if ((await call('DELETE', `/accounts/${account}/queues/${queueID}/consumers/${consumerID}`)).status !== 200)
|
|
228
|
+
violations.push('consumer delete probe failed');
|
|
229
|
+
// teardown probes (delete endpoints)
|
|
230
|
+
if ((await call('DELETE', `/accounts/${account}/workers/domains/${domainID}`)).status !== 200)
|
|
231
|
+
violations.push('domain delete probe failed');
|
|
232
|
+
if ((await call('DELETE', `/zones/${zone}/workers/routes/${routeID}`)).status !== 200)
|
|
233
|
+
violations.push('route delete probe failed');
|
|
234
|
+
if ((await call('DELETE', `/accounts/${account}/workers/subdomain`)).status !== 204)
|
|
235
|
+
violations.push('account subdomain delete probe failed');
|
|
236
|
+
if ((await call('DELETE', `/accounts/${account}/workers/scripts/conformance-worker`)).status !== 400)
|
|
237
|
+
violations.push('worker delete without force should be blocked by its Durable Object namespace');
|
|
238
|
+
if ((await call('DELETE', `/accounts/${account}/workers/scripts/conformance-worker?force=true`)).status !== 200)
|
|
239
|
+
violations.push('worker force delete probe failed');
|
|
240
|
+
if ((await call('DELETE', `/accounts/${account}/queues/${queueID}`)).status !== 200)
|
|
241
|
+
violations.push('queue delete probe failed');
|
|
242
|
+
if ((await call('DELETE', `/accounts/${account}/hyperdrive/configs/${hyperdriveID}`)).status !== 200)
|
|
243
|
+
violations.push('hyperdrive delete probe failed');
|
|
244
|
+
if ((await call('DELETE', `/accounts/${account}/r2/buckets/conformance-bucket`)).status !== 200)
|
|
245
|
+
violations.push('R2 delete probe failed');
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
rmSync(root, { recursive: true, force: true });
|
|
249
|
+
}
|
|
250
|
+
return { ok: violations.length === 0, endpointsChecked: EXPECTED.length, violations };
|
|
251
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PerformContext, PushOutcome, RemoteExecute, SyncResource, TwinAction } from '@volter/world-core';
|
|
2
|
+
import { CloudflareBudget, type CloudflareBudgetOptions } from './cloudflare-budget.js';
|
|
3
|
+
export type CloudflareExecute = (method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', path: string, body?: Record<string, unknown>) => Promise<{
|
|
4
|
+
status: number;
|
|
5
|
+
body: any;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
}>;
|
|
8
|
+
export type LiveCloudflareOptions = {
|
|
9
|
+
fetchImpl?: typeof fetch;
|
|
10
|
+
budget?: CloudflareBudget;
|
|
11
|
+
budgetOptions?: CloudflareBudgetOptions;
|
|
12
|
+
};
|
|
13
|
+
/** The single live Cloudflare call site. It is budgeted before fetch and records 429/Retry-After. */
|
|
14
|
+
export declare function liveCloudflareExecute(token: string, base?: string, opts?: LiveCloudflareOptions): CloudflareExecute;
|
|
15
|
+
export declare function mapCloudflareAccount(value: Record<string, unknown>): SyncResource;
|
|
16
|
+
export declare function mapCloudflareZone(value: Record<string, unknown>): SyncResource;
|
|
17
|
+
export declare function mapCloudflareTunnel(value: Record<string, unknown>): SyncResource;
|
|
18
|
+
export declare function mapCloudflareConfiguration(value: Record<string, unknown>): SyncResource;
|
|
19
|
+
export declare function mapCloudflareDnsRecord(value: Record<string, unknown>): SyncResource;
|
|
20
|
+
export declare function pullCloudflareState(execute: CloudflareExecute): Promise<SyncResource[]>;
|
|
21
|
+
/** D7 pull entry point. Defaults to a moving timestamp so A→B→A observations cannot dedupe. */
|
|
22
|
+
export declare function syncCloudflareFromReal(execute: CloudflareExecute, opts?: {
|
|
23
|
+
root?: string;
|
|
24
|
+
occurredAt?: string;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
observed: number;
|
|
27
|
+
deltasAppended: number;
|
|
28
|
+
}>;
|
|
29
|
+
/** The pack's executor over the kernel's: the same API v4 call, carried by the head. */
|
|
30
|
+
export declare function cloudflareExecuteOver(execute: RemoteExecute): CloudflareExecute;
|
|
31
|
+
/** The refresh adapter: pull the account's control plane through the executor and fold it into the root. */
|
|
32
|
+
export declare function syncCloudflareFromRemote(execute: RemoteExecute, opts?: {
|
|
33
|
+
root?: string;
|
|
34
|
+
origin?: string;
|
|
35
|
+
occurredAt?: string;
|
|
36
|
+
}): Promise<{
|
|
37
|
+
observed: number;
|
|
38
|
+
deltasAppended: number;
|
|
39
|
+
}>;
|
|
40
|
+
type PushRequest = {
|
|
41
|
+
method: 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
42
|
+
path: string;
|
|
43
|
+
body?: Record<string, unknown>;
|
|
44
|
+
};
|
|
45
|
+
/** The one wire request an entry becomes, or null when the entry is the twin's own (an observation, a
|
|
46
|
+
* locally-authored provisioning record) and crosses nothing. `resolve` is the kernel's: a local id that a
|
|
47
|
+
* landing already adopted answers as the vendor's, so Cloudflare is asked about its own id, never ours. */
|
|
48
|
+
export declare function cloudflareRequestForAction(action: TwinAction, resolve?: (type: string, localId: string) => string): PushRequest | null;
|
|
49
|
+
/** The perform adapter: one entry crosses to Cloudflare through the executor, and the vendor's own id for what
|
|
50
|
+
* it made comes back as the externalId (the kernel adopts it and every declared reference follows). An entry the
|
|
51
|
+
* vendor has nothing to write for — an observation, a provisioning record — crosses nothing and keeps its id. */
|
|
52
|
+
export declare function performCloudflareAction(execute: RemoteExecute, action: TwinAction, ctx: PerformContext): Promise<PushOutcome>;
|
|
53
|
+
export {};
|