@sponson/sim 0.4.0 → 0.5.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/README.md +5 -0
- package/dist/auth.d.ts +60 -0
- package/dist/auth.js +195 -0
- package/dist/auth.js.map +1 -0
- package/dist/bin.js +9 -14
- package/dist/bin.js.map +1 -1
- package/dist/chaos.d.ts +4 -0
- package/dist/chaos.js +2 -0
- package/dist/chaos.js.map +1 -1
- package/dist/cli.d.ts +29 -0
- package/dist/cli.js +99 -0
- package/dist/cli.js.map +1 -0
- package/dist/demo.d.ts +39 -0
- package/dist/demo.js +168 -0
- package/dist/demo.js.map +1 -0
- package/dist/graphql.d.ts +22 -0
- package/dist/graphql.js +36 -0
- package/dist/graphql.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +43 -4
- package/dist/provider.js +12 -6
- package/dist/provider.js.map +1 -1
- package/dist/routes/algolia.d.ts +37 -0
- package/dist/routes/algolia.js +83 -0
- package/dist/routes/algolia.js.map +1 -0
- package/dist/routes/aws.d.ts +43 -0
- package/dist/routes/aws.js +211 -0
- package/dist/routes/aws.js.map +1 -0
- package/dist/routes/cloudflare.d.ts +80 -0
- package/dist/routes/cloudflare.js +176 -0
- package/dist/routes/cloudflare.js.map +1 -0
- package/dist/routes/digitalocean.d.ts +58 -0
- package/dist/routes/digitalocean.js +158 -0
- package/dist/routes/digitalocean.js.map +1 -0
- package/dist/routes/fly.d.ts +22 -0
- package/dist/routes/fly.js +113 -0
- package/dist/routes/fly.js.map +1 -0
- package/dist/routes/launchdarkly.d.ts +98 -0
- package/dist/routes/launchdarkly.js +213 -0
- package/dist/routes/launchdarkly.js.map +1 -0
- package/dist/routes/netlify.d.ts +126 -0
- package/dist/routes/netlify.js +347 -0
- package/dist/routes/netlify.js.map +1 -0
- package/dist/routes/planetscale.d.ts +95 -0
- package/dist/routes/planetscale.js +234 -0
- package/dist/routes/planetscale.js.map +1 -0
- package/dist/routes/railway.d.ts +24 -0
- package/dist/routes/railway.js +112 -0
- package/dist/routes/railway.js.map +1 -0
- package/dist/routes/rest.d.ts +138 -0
- package/dist/routes/rest.js +462 -0
- package/dist/routes/rest.js.map +1 -0
- package/dist/routes/supabase.d.ts +103 -0
- package/dist/routes/supabase.js +234 -0
- package/dist/routes/supabase.js.map +1 -0
- package/dist/routes/xata.d.ts +57 -0
- package/dist/routes/xata.js +115 -0
- package/dist/routes/xata.js.map +1 -0
- package/dist/server.js +71 -12
- package/dist/server.js.map +1 -1
- package/dist/state.d.ts +12 -0
- package/dist/state.js +13 -1
- package/dist/state.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ProviderSim } from "../provider.js";
|
|
2
|
+
/** One S3 CORS rule as stored. */
|
|
3
|
+
export interface AwsCorsRule {
|
|
4
|
+
ID?: string;
|
|
5
|
+
AllowedOrigin: string[];
|
|
6
|
+
AllowedMethod: string[];
|
|
7
|
+
AllowedHeader: string[];
|
|
8
|
+
ExposeHeader: string[];
|
|
9
|
+
MaxAgeSeconds?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Simulated AWS state: buckets with their CORS configuration, Amplify apps with their branches. */
|
|
12
|
+
export interface AwsState {
|
|
13
|
+
/** Access key id → secret access key. */
|
|
14
|
+
keys: Record<string, string>;
|
|
15
|
+
region: string;
|
|
16
|
+
buckets: Record<string, {
|
|
17
|
+
cors: AwsCorsRule[] | null;
|
|
18
|
+
}>;
|
|
19
|
+
amplify: Record<string, {
|
|
20
|
+
branches: Record<string, {
|
|
21
|
+
environmentVariables: Record<string, string>;
|
|
22
|
+
}>;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
/** Initial AWS state. Buckets are listed by name; Amplify apps by id, with their branches' variables. */
|
|
26
|
+
export interface AwsSeed {
|
|
27
|
+
keys?: Record<string, string>;
|
|
28
|
+
region?: string;
|
|
29
|
+
buckets?: Record<string, {
|
|
30
|
+
cors?: AwsCorsRule[] | null;
|
|
31
|
+
}>;
|
|
32
|
+
amplify?: Record<string, {
|
|
33
|
+
branches: Record<string, {
|
|
34
|
+
environmentVariables?: Record<string, string>;
|
|
35
|
+
}>;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
/** The access key id the sim's default seed accepts (`AWS_SIM_ACCESS_KEY_ID` in `simEnv`). */
|
|
39
|
+
export declare const AWS_SIM_ACCESS_KEY_ID = "AKIASIMEXAMPLE0042";
|
|
40
|
+
/** Its secret (`AWS_SIM_SECRET_ACCESS_KEY` in `simEnv`). */
|
|
41
|
+
export declare const AWS_SIM_SECRET_ACCESS_KEY = "sim/aws-secret-access-key-0042";
|
|
42
|
+
/** Simulated AWS (S3 bucket CORS, Amplify branch variables); see the assumptions at the top of this file. */
|
|
43
|
+
export declare const awsSim: ProviderSim<AwsState, AwsSeed>;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulated AWS APIs signed with SigV4: S3 bucket CORS configuration (XML) and Amplify branch environment
|
|
3
|
+
* variables (JSON). They exist to test the generic `http` adapter's `auth: { aws_sigv4 }` and `encoding: xml`
|
|
4
|
+
* against something that checks both: every request's signature is recomputed (../auth.ts) and refused when wrong.
|
|
5
|
+
*
|
|
6
|
+
* Path-style, under `/aws`: `/aws/s3/<bucket>/?cors` stands for `https://<bucket>.s3.<region>.amazonaws.com/?cors`,
|
|
7
|
+
* `/aws/amplify/apps/<app>/branches/<branch>` for `https://amplify.<region>.amazonaws.com/apps/<app>/branches/<branch>`.
|
|
8
|
+
*
|
|
9
|
+
* Assumptions (docs/api-verification.md, "AWS through the generic adapter"):
|
|
10
|
+
* A1. Requests are signed with SigV4 for the path's service (`s3`, `amplify`) and the seeded region; S3 requests
|
|
11
|
+
* carry `x-amz-content-sha256`. Verified: IAM "Create a signed AWS API request"; S3 "Common request headers".
|
|
12
|
+
* A2. `PUT /?cors` takes a `CORSConfiguration` of 1 to 100 `CORSRule`s (each with at least one `AllowedOrigin` and
|
|
13
|
+
* one `AllowedMethod`) and requires `Content-MD5` (or an x-amz-checksum); it replaces the whole configuration
|
|
14
|
+
* and answers 200 with no body. An empty configuration is `MalformedXML`. Verified: S3 API Reference
|
|
15
|
+
* PutBucketCors; botocore `requestChecksumRequired`.
|
|
16
|
+
* A3. `GET /?cors` answers the configuration as XML; a bucket without one answers 404 `NoSuchCORSConfiguration`.
|
|
17
|
+
* Unverified (the 404 code from memory); `DELETE /?cors` answers 204. Verified: DeleteBucketCors.
|
|
18
|
+
* A4. Amplify `GetBranch` (`GET`) and `UpdateBranch` (`POST`) on `/apps/{appId}/branches/{branchName}` answer
|
|
19
|
+
* `{ branch: { …, environmentVariables } }`; `environmentVariables` in an update replaces the whole map.
|
|
20
|
+
* Verified: Amplify API Reference UpdateBranch, GetBranch. Errors are JSON `{ message }` with
|
|
21
|
+
* `x-amzn-ErrorType`; unknown app or branch is 404 `NotFoundException`.
|
|
22
|
+
*/
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
24
|
+
import { sigV4Problem } from "../auth.js";
|
|
25
|
+
import { Reply, route, router } from "../provider.js";
|
|
26
|
+
/** The access key id the sim's default seed accepts (`AWS_SIM_ACCESS_KEY_ID` in `simEnv`). */
|
|
27
|
+
export const AWS_SIM_ACCESS_KEY_ID = "AKIASIMEXAMPLE0042";
|
|
28
|
+
/** Its secret (`AWS_SIM_SECRET_ACCESS_KEY` in `simEnv`). */
|
|
29
|
+
export const AWS_SIM_SECRET_ACCESS_KEY = "sim/aws-secret-access-key-0042";
|
|
30
|
+
/** Simulated AWS (S3 bucket CORS, Amplify branch variables); see the assumptions at the top of this file. */
|
|
31
|
+
export const awsSim = {
|
|
32
|
+
env: { token: "AWS_SIM_SECRET_ACCESS_KEY", url: "AWS_SIM_URL", testToken: AWS_SIM_SECRET_ACCESS_KEY, more: { AWS_SIM_ACCESS_KEY_ID: AWS_SIM_ACCESS_KEY_ID } },
|
|
33
|
+
defaultSeed: { buckets: { assets: { cors: null } }, amplify: { d1demo: { branches: { "pr-42": {} } } } },
|
|
34
|
+
reset(_core, seed) {
|
|
35
|
+
const buckets = {};
|
|
36
|
+
for (const [name, b] of Object.entries(seed?.buckets ?? {}))
|
|
37
|
+
buckets[name] = { cors: b.cors ? structuredClone(b.cors) : null };
|
|
38
|
+
const amplify = {};
|
|
39
|
+
for (const [app, a] of Object.entries(seed?.amplify ?? {})) {
|
|
40
|
+
amplify[app] = { branches: Object.fromEntries(Object.entries(a.branches).map(([n, b]) => [n, { environmentVariables: { ...(b.environmentVariables ?? {}) } }])) };
|
|
41
|
+
}
|
|
42
|
+
return { keys: seed?.keys ?? { [AWS_SIM_ACCESS_KEY_ID]: AWS_SIM_SECRET_ACCESS_KEY }, region: seed?.region ?? "us-east-1", buckets, amplify };
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* `s3.<bucket>.cors`: "delete" removes the configuration; "remove:<ID>" one rule.
|
|
46
|
+
* `amplify.<app>.<branch>.<NAME>`: a new value, or "delete".
|
|
47
|
+
*/
|
|
48
|
+
drift(_core, state, { key, rest, value, only }) {
|
|
49
|
+
if (only !== undefined)
|
|
50
|
+
return false;
|
|
51
|
+
const [kind, a, b, c] = rest.split(".");
|
|
52
|
+
if (kind === "s3" && a && b === "cors")
|
|
53
|
+
return driftCors(state, a, value, key);
|
|
54
|
+
if (kind === "amplify" && a && b && c) {
|
|
55
|
+
const branch = state.amplify[a]?.branches[b];
|
|
56
|
+
if (!branch)
|
|
57
|
+
throw new Error(`drift ${key}: no branch ${a}/${b}`);
|
|
58
|
+
if (value === "delete")
|
|
59
|
+
delete branch.environmentVariables[c];
|
|
60
|
+
else
|
|
61
|
+
branch.environmentVariables[c] = value;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
},
|
|
66
|
+
authorized(headers, state, req) {
|
|
67
|
+
return authorized(headers, req, state);
|
|
68
|
+
},
|
|
69
|
+
routes(core, state, req) {
|
|
70
|
+
return routes(core, state, req);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
function driftCors(state, bucket, value, key) {
|
|
74
|
+
const b = state.buckets[bucket];
|
|
75
|
+
if (!b)
|
|
76
|
+
throw new Error(`drift ${key}: no bucket ${bucket}`);
|
|
77
|
+
if (value === "delete")
|
|
78
|
+
b.cors = null;
|
|
79
|
+
else if (value.startsWith("remove:"))
|
|
80
|
+
b.cors = (b.cors ?? []).filter((r) => r.ID !== value.slice(7));
|
|
81
|
+
else
|
|
82
|
+
throw new Error(`drift ${key}: "delete" or "remove:<ID>"`);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
/** A1: the signature must be valid for the service the path belongs to. */
|
|
86
|
+
function authorized(headers, req, state) {
|
|
87
|
+
const service = /^\/aws\/(s3|amplify)\//.exec(req.url.pathname)?.[1];
|
|
88
|
+
if (!service)
|
|
89
|
+
return false;
|
|
90
|
+
if (service === "s3" && headers["x-amz-content-sha256"] === undefined)
|
|
91
|
+
return s3Error(400, "InvalidRequest", "Missing required header for this request: x-amz-content-sha256");
|
|
92
|
+
const problem = sigV4Problem(headers, req, { keys: state.keys, region: state.region, service });
|
|
93
|
+
if (problem === null)
|
|
94
|
+
return true;
|
|
95
|
+
return service === "s3" ? s3Error(403, problem === "InvalidAccessKeyId" ? problem : "SignatureDoesNotMatch", problem) : amplifyError(401, "UnauthorizedException", problem);
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// S3 CORS (XML)
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
const XML = { "content-type": "application/xml" };
|
|
101
|
+
function escape(s) {
|
|
102
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
103
|
+
}
|
|
104
|
+
function unescape(s) {
|
|
105
|
+
return s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
106
|
+
}
|
|
107
|
+
function s3Error(status, code, message) {
|
|
108
|
+
return new Reply(status, `<?xml version="1.0" encoding="UTF-8"?><Error><Code>${code}</Code><Message>${escape(message)}</Message></Error>`, XML);
|
|
109
|
+
}
|
|
110
|
+
function corsXml(rules) {
|
|
111
|
+
const el = (name, values) => values.map((v) => `<${name}>${escape(v)}</${name}>`).join("");
|
|
112
|
+
const body = rules
|
|
113
|
+
.map((r) => `<CORSRule>${r.ID !== undefined ? el("ID", [r.ID]) : ""}${el("AllowedHeader", r.AllowedHeader)}${el("AllowedMethod", r.AllowedMethod)}${el("AllowedOrigin", r.AllowedOrigin)}${el("ExposeHeader", r.ExposeHeader)}${r.MaxAgeSeconds !== undefined ? el("MaxAgeSeconds", [r.MaxAgeSeconds]) : ""}</CORSRule>`)
|
|
114
|
+
.join("");
|
|
115
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">${body}</CORSConfiguration>`;
|
|
116
|
+
}
|
|
117
|
+
/** The rules of a `CORSConfiguration` document; a reason when it is not one S3 accepts (A2). */
|
|
118
|
+
function parseCors(xml) {
|
|
119
|
+
if (/<!DOCTYPE|<!ENTITY/i.test(xml))
|
|
120
|
+
return "DTDs are not accepted";
|
|
121
|
+
if (!/<CORSConfiguration(\s[^>]*)?>[\s\S]*<\/CORSConfiguration>\s*$/.test(xml.replace(/^<\?xml[^>]*\?>\s*/, "")))
|
|
122
|
+
return "the root element must be CORSConfiguration";
|
|
123
|
+
const rules = [];
|
|
124
|
+
for (const m of xml.matchAll(/<CORSRule>([\s\S]*?)<\/CORSRule>/g)) {
|
|
125
|
+
const values = (name) => [...m[1].matchAll(new RegExp(`<${name}>([^<]*)</${name}>`, "g"))].map((x) => unescape(x[1]));
|
|
126
|
+
const rule = { AllowedOrigin: values("AllowedOrigin"), AllowedMethod: values("AllowedMethod"), AllowedHeader: values("AllowedHeader"), ExposeHeader: values("ExposeHeader") };
|
|
127
|
+
const id = values("ID")[0];
|
|
128
|
+
const age = values("MaxAgeSeconds")[0];
|
|
129
|
+
if (id !== undefined)
|
|
130
|
+
rule.ID = id;
|
|
131
|
+
if (age !== undefined)
|
|
132
|
+
rule.MaxAgeSeconds = age;
|
|
133
|
+
if (rule.AllowedOrigin.length === 0 || rule.AllowedMethod.length === 0)
|
|
134
|
+
return "every CORSRule needs an AllowedOrigin and an AllowedMethod";
|
|
135
|
+
if (rule.AllowedMethod.some((x) => !["GET", "PUT", "POST", "DELETE", "HEAD"].includes(x)))
|
|
136
|
+
return "AllowedMethod must be GET, PUT, POST, DELETE or HEAD";
|
|
137
|
+
rules.push(rule);
|
|
138
|
+
}
|
|
139
|
+
if (rules.length === 0 || rules.length > 100)
|
|
140
|
+
return "a CORSConfiguration holds 1 to 100 CORSRules";
|
|
141
|
+
return rules;
|
|
142
|
+
}
|
|
143
|
+
function putCors(bucket, headers, body) {
|
|
144
|
+
const xml = typeof body === "string" ? body : "";
|
|
145
|
+
const md5 = headers["content-md5"];
|
|
146
|
+
if (md5 === undefined)
|
|
147
|
+
return s3Error(400, "InvalidRequest", "Missing required header for this request: Content-Md5");
|
|
148
|
+
if (md5 !== createHash("md5").update(xml, "utf8").digest("base64"))
|
|
149
|
+
return s3Error(400, "BadDigest", "The Content-MD5 you specified did not match what we received.");
|
|
150
|
+
const rules = parseCors(xml);
|
|
151
|
+
if (typeof rules === "string")
|
|
152
|
+
return s3Error(400, "MalformedXML", rules);
|
|
153
|
+
bucket.cors = rules;
|
|
154
|
+
return new Reply(200, "", XML);
|
|
155
|
+
}
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Amplify branches (JSON)
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
function amplifyError(status, type, message) {
|
|
160
|
+
return new Reply(status, { message }, { "x-amzn-errortype": type });
|
|
161
|
+
}
|
|
162
|
+
function branchView(app, name, b) {
|
|
163
|
+
return new Reply(200, { branch: { branchArn: `arn:aws:amplify:us-east-1:000000000000:apps/${app}/branches/${name}`, branchName: name, stage: "PULL_REQUEST", environmentVariables: { ...b.environmentVariables } } });
|
|
164
|
+
}
|
|
165
|
+
function updateBranch(app, name, b, body) {
|
|
166
|
+
if (typeof body !== "object" || body === null)
|
|
167
|
+
return amplifyError(400, "BadRequestException", "body must be a JSON object");
|
|
168
|
+
const vars = body.environmentVariables;
|
|
169
|
+
if (vars !== undefined) {
|
|
170
|
+
if (typeof vars !== "object" || vars === null || !Object.values(vars).every((v) => typeof v === "string"))
|
|
171
|
+
return amplifyError(400, "BadRequestException", "environmentVariables must map names to strings");
|
|
172
|
+
b.environmentVariables = { ...vars };
|
|
173
|
+
}
|
|
174
|
+
return branchView(app, name, b);
|
|
175
|
+
}
|
|
176
|
+
const routes = router([
|
|
177
|
+
route("GET", "/s3/:bucket/", ({ state, params, url }) => {
|
|
178
|
+
const b = state.buckets[params.bucket];
|
|
179
|
+
if (!b)
|
|
180
|
+
return s3Error(404, "NoSuchBucket", "The specified bucket does not exist");
|
|
181
|
+
if (!url.searchParams.has("cors"))
|
|
182
|
+
return s3Error(501, "NotImplemented", "only ?cors is simulated");
|
|
183
|
+
return b.cors ? new Reply(200, corsXml(b.cors), XML) : s3Error(404, "NoSuchCORSConfiguration", "The CORS configuration does not exist");
|
|
184
|
+
}),
|
|
185
|
+
route("PUT", "/s3/:bucket/", ({ state, params, url, headers, body }) => {
|
|
186
|
+
const b = state.buckets[params.bucket];
|
|
187
|
+
if (!b)
|
|
188
|
+
return s3Error(404, "NoSuchBucket", "The specified bucket does not exist");
|
|
189
|
+
if (!url.searchParams.has("cors"))
|
|
190
|
+
return s3Error(501, "NotImplemented", "only ?cors is simulated");
|
|
191
|
+
return putCors(b, headers, body);
|
|
192
|
+
}),
|
|
193
|
+
route("DELETE", "/s3/:bucket/", ({ state, params, url }) => {
|
|
194
|
+
const b = state.buckets[params.bucket];
|
|
195
|
+
if (!b)
|
|
196
|
+
return s3Error(404, "NoSuchBucket", "The specified bucket does not exist");
|
|
197
|
+
if (!url.searchParams.has("cors"))
|
|
198
|
+
return s3Error(501, "NotImplemented", "only ?cors is simulated");
|
|
199
|
+
b.cors = null;
|
|
200
|
+
return new Reply(204, null);
|
|
201
|
+
}),
|
|
202
|
+
route("GET", "/amplify/apps/:app/branches/:branch", ({ state, params }) => {
|
|
203
|
+
const b = state.amplify[params.app]?.branches[params.branch];
|
|
204
|
+
return b ? branchView(params.app, params.branch, b) : amplifyError(404, "NotFoundException", "branch not found");
|
|
205
|
+
}),
|
|
206
|
+
route("POST", "/amplify/apps/:app/branches/:branch", ({ state, params, body }) => {
|
|
207
|
+
const b = state.amplify[params.app]?.branches[params.branch];
|
|
208
|
+
return b ? updateBranch(params.app, params.branch, b, body) : amplifyError(404, "NotFoundException", "branch not found");
|
|
209
|
+
}),
|
|
210
|
+
], () => s3Error(404, "NotFound", "not simulated"));
|
|
211
|
+
//# sourceMappingURL=aws.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aws.js","sourceRoot":"","sources":["../../src/routes/aws.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulated Cloudflare: Pages projects and their per-environment variables (`deployment_configs.<env>.env_vars`).
|
|
3
|
+
*
|
|
4
|
+
* Assumptions about the real API that this fake encodes. Sources (fetched 2026-10-11): the official OpenAPI document
|
|
5
|
+
* https://github.com/cloudflare/api-schemas (`openapi.json`), the API reference pages
|
|
6
|
+
* https://developers.cloudflare.com/api/resources/pages/subresources/projects/methods/get/ and …/methods/edit/,
|
|
7
|
+
* and the Pages docs cited per item. The critical ones are pinned in scenarios/contract.test.ts
|
|
8
|
+
* (`assumption CF<n>: …`) so `pnpm test:live` checks them against a real account.
|
|
9
|
+
*
|
|
10
|
+
* CF1. Base URL `https://api.cloudflare.com/client/v4`, `Authorization: Bearer <API token>` — verified
|
|
11
|
+
* (https://developers.cloudflare.com/fundamentals/api/how-to/make-api-calls/). The token needs the
|
|
12
|
+
* account permission "Cloudflare Pages Edit" (fundamentals/api/reference/permissions/; schema token groups
|
|
13
|
+
* "Pages Read"/"Pages Write").
|
|
14
|
+
* CF2. `GET /accounts/{account_id}/pages/projects/{project_name}` answers `200 { success, errors, messages, result }`,
|
|
15
|
+
* `result` a project with `id`, `name`, `subdomain` and `deployment_configs.{preview,production}.env_vars`, a
|
|
16
|
+
* nullable map NAME → `{ type: "plain_text" | "secret_text", value }` — verified (schema `pages_project`,
|
|
17
|
+
* `pages_plain_text_env_var`, `pages_secret_text_env_var`).
|
|
18
|
+
* CF3. A `secret_text` value is never returned: the entry is listed with its type and `value: ""` — partly
|
|
19
|
+
* verified: the docs say secrets cannot be seen after they are set (pages/functions/bindings/), and the
|
|
20
|
+
* schema's example is `{"type":"secret_text","value":""}`; whether the key comes back with `""`, null or
|
|
21
|
+
* no `value` is not stated (the adapter accepts all three). Pinned.
|
|
22
|
+
* CF4. `PATCH` of the same path merges: `deployment_configs.<env>.env_vars` keys not in the body are left alone, a
|
|
23
|
+
* key set to `null` is deleted, and the answer is `200` with the updated project — `null` deletes and `200`
|
|
24
|
+
* verified ("To delete an environment variable, set its key to `null`"); merging is implied by that rule and
|
|
25
|
+
* by a body with no required fields, but not stated. Pinned: the adapter also re-reads and refuses to go on
|
|
26
|
+
* if a variable it did not send disappeared.
|
|
27
|
+
* CF5. No precondition (ETag / If-Match) exists for that PATCH — verified (only path parameters, no 412). Two
|
|
28
|
+
* writers of the same key race; last write wins. Sponson's own writers take turns through the parent lock
|
|
29
|
+
* (`lockOn`, ADR 0019).
|
|
30
|
+
* CF6. Failures answer `4XX { success: false, errors: [{ code, message }], result: null }` — verified (schema
|
|
31
|
+
* `pages_api-response-common-failure`). An unknown project answers `404` with code `8000007` — unverified:
|
|
32
|
+
* the schema declares only `4XX`; the status and code come from Wrangler's error reports.
|
|
33
|
+
* CF7. A write is visible to the next GET at once — unverified. Pinned.
|
|
34
|
+
* CF8. Preview variables are one set shared by every preview deployment; there are no per-branch values —
|
|
35
|
+
* verified ("This will set the configuration for all preview deployments, not just the deployments from a
|
|
36
|
+
* specific branch", pages/functions/wrangler-configuration/).
|
|
37
|
+
* CF9. Variable names: the schema gives no pattern (free-form map keys); the sim accepts any non-empty name.
|
|
38
|
+
* CF10. Changing a variable does not change existing deployments; it applies to the next one — indirectly
|
|
39
|
+
* documented ("Redeploy your project for the binding to take effect", pages/functions/bindings/). The sim
|
|
40
|
+
* has no deployments, so nothing here depends on it.
|
|
41
|
+
*/
|
|
42
|
+
import { type CreatedBy, type ProviderSim } from "../provider.js";
|
|
43
|
+
/** The two variable sets of a Pages project. */
|
|
44
|
+
export type CloudflarePagesEnv = "preview" | "production";
|
|
45
|
+
/** One variable as the simulated Cloudflare stores it (the secret's value included: the sim must compare it). */
|
|
46
|
+
export interface CloudflareEnvVar {
|
|
47
|
+
type: "plain_text" | "secret_text";
|
|
48
|
+
value: string;
|
|
49
|
+
}
|
|
50
|
+
/** A Pages project, with the bookkeeping the sim adds (`createdBy`). */
|
|
51
|
+
export interface CloudflarePagesProject {
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
subdomain: string;
|
|
55
|
+
createdBy: CreatedBy;
|
|
56
|
+
deployment_configs: Record<CloudflarePagesEnv, {
|
|
57
|
+
env_vars: Record<string, CloudflareEnvVar>;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
/** The simulated Cloudflare's state: Pages projects per account id. */
|
|
61
|
+
export interface CloudflareState {
|
|
62
|
+
accounts: Record<string, {
|
|
63
|
+
projects: Record<string, CloudflarePagesProject>;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
/** Variables of one environment in a seed: NAME → a plain value, or `{ type, value }`. */
|
|
67
|
+
export type CloudflareSeedVars = Record<string, string | {
|
|
68
|
+
type?: CloudflareEnvVar["type"];
|
|
69
|
+
value: string;
|
|
70
|
+
}>;
|
|
71
|
+
/** What a scenario or test seeds the simulated Cloudflare with (`seed: { cloudflare: … }`). */
|
|
72
|
+
export interface CloudflareSeed {
|
|
73
|
+
/** Account id → project name → the variables that already exist (set "by a human", so unmanaged). */
|
|
74
|
+
accounts: Record<string, Record<string, {
|
|
75
|
+
preview?: CloudflareSeedVars;
|
|
76
|
+
production?: CloudflareSeedVars;
|
|
77
|
+
}>>;
|
|
78
|
+
}
|
|
79
|
+
/** The simulated Cloudflare API, registered in `PROVIDERS` (packages/sim/src/state.ts) and served under `/cloudflare`. */
|
|
80
|
+
export declare const cloudflareSim: ProviderSim<CloudflareState, CloudflareSeed>;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulated Cloudflare: Pages projects and their per-environment variables (`deployment_configs.<env>.env_vars`).
|
|
3
|
+
*
|
|
4
|
+
* Assumptions about the real API that this fake encodes. Sources (fetched 2026-10-11): the official OpenAPI document
|
|
5
|
+
* https://github.com/cloudflare/api-schemas (`openapi.json`), the API reference pages
|
|
6
|
+
* https://developers.cloudflare.com/api/resources/pages/subresources/projects/methods/get/ and …/methods/edit/,
|
|
7
|
+
* and the Pages docs cited per item. The critical ones are pinned in scenarios/contract.test.ts
|
|
8
|
+
* (`assumption CF<n>: …`) so `pnpm test:live` checks them against a real account.
|
|
9
|
+
*
|
|
10
|
+
* CF1. Base URL `https://api.cloudflare.com/client/v4`, `Authorization: Bearer <API token>` — verified
|
|
11
|
+
* (https://developers.cloudflare.com/fundamentals/api/how-to/make-api-calls/). The token needs the
|
|
12
|
+
* account permission "Cloudflare Pages Edit" (fundamentals/api/reference/permissions/; schema token groups
|
|
13
|
+
* "Pages Read"/"Pages Write").
|
|
14
|
+
* CF2. `GET /accounts/{account_id}/pages/projects/{project_name}` answers `200 { success, errors, messages, result }`,
|
|
15
|
+
* `result` a project with `id`, `name`, `subdomain` and `deployment_configs.{preview,production}.env_vars`, a
|
|
16
|
+
* nullable map NAME → `{ type: "plain_text" | "secret_text", value }` — verified (schema `pages_project`,
|
|
17
|
+
* `pages_plain_text_env_var`, `pages_secret_text_env_var`).
|
|
18
|
+
* CF3. A `secret_text` value is never returned: the entry is listed with its type and `value: ""` — partly
|
|
19
|
+
* verified: the docs say secrets cannot be seen after they are set (pages/functions/bindings/), and the
|
|
20
|
+
* schema's example is `{"type":"secret_text","value":""}`; whether the key comes back with `""`, null or
|
|
21
|
+
* no `value` is not stated (the adapter accepts all three). Pinned.
|
|
22
|
+
* CF4. `PATCH` of the same path merges: `deployment_configs.<env>.env_vars` keys not in the body are left alone, a
|
|
23
|
+
* key set to `null` is deleted, and the answer is `200` with the updated project — `null` deletes and `200`
|
|
24
|
+
* verified ("To delete an environment variable, set its key to `null`"); merging is implied by that rule and
|
|
25
|
+
* by a body with no required fields, but not stated. Pinned: the adapter also re-reads and refuses to go on
|
|
26
|
+
* if a variable it did not send disappeared.
|
|
27
|
+
* CF5. No precondition (ETag / If-Match) exists for that PATCH — verified (only path parameters, no 412). Two
|
|
28
|
+
* writers of the same key race; last write wins. Sponson's own writers take turns through the parent lock
|
|
29
|
+
* (`lockOn`, ADR 0019).
|
|
30
|
+
* CF6. Failures answer `4XX { success: false, errors: [{ code, message }], result: null }` — verified (schema
|
|
31
|
+
* `pages_api-response-common-failure`). An unknown project answers `404` with code `8000007` — unverified:
|
|
32
|
+
* the schema declares only `4XX`; the status and code come from Wrangler's error reports.
|
|
33
|
+
* CF7. A write is visible to the next GET at once — unverified. Pinned.
|
|
34
|
+
* CF8. Preview variables are one set shared by every preview deployment; there are no per-branch values —
|
|
35
|
+
* verified ("This will set the configuration for all preview deployments, not just the deployments from a
|
|
36
|
+
* specific branch", pages/functions/wrangler-configuration/).
|
|
37
|
+
* CF9. Variable names: the schema gives no pattern (free-form map keys); the sim accepts any non-empty name.
|
|
38
|
+
* CF10. Changing a variable does not change existing deployments; it applies to the next one — indirectly
|
|
39
|
+
* documented ("Redeploy your project for the binding to take effect", pages/functions/bindings/). The sim
|
|
40
|
+
* has no deployments, so nothing here depends on it.
|
|
41
|
+
*/
|
|
42
|
+
import { Reply, route, router } from "../provider.js";
|
|
43
|
+
const ENVS = ["preview", "production"];
|
|
44
|
+
/** The simulated Cloudflare API, registered in `PROVIDERS` (packages/sim/src/state.ts) and served under `/cloudflare`. */
|
|
45
|
+
export const cloudflareSim = {
|
|
46
|
+
env: { token: "CLOUDFLARE_API_TOKEN", url: "CLOUDFLARE_API_URL", testToken: "tok_cloudflare" },
|
|
47
|
+
defaultSeed: { accounts: { acc_demo: { demo: {} } } },
|
|
48
|
+
reset(core, seed) {
|
|
49
|
+
const accounts = {};
|
|
50
|
+
for (const [account, projects] of Object.entries(seed?.accounts ?? {})) {
|
|
51
|
+
accounts[account] = { projects: {} };
|
|
52
|
+
for (const [name, vars] of Object.entries(projects))
|
|
53
|
+
accounts[account].projects[name] = project(core, name, vars);
|
|
54
|
+
}
|
|
55
|
+
return { accounts };
|
|
56
|
+
},
|
|
57
|
+
/**
|
|
58
|
+
* `pages_env.<env>.<NAME>` or `cloudflare:<project>.pages_env.<env>.<NAME>`, what a human in the dashboard would do:
|
|
59
|
+
* `delete`; `plain:<value>` or `secret:<value>` (set, choosing the type); any other value replaces the value and
|
|
60
|
+
* keeps the type. Setting a variable that does not exist creates it as plain text.
|
|
61
|
+
*/
|
|
62
|
+
drift(_core, state, { key, rest, value, only }) {
|
|
63
|
+
const m = /^pages_env\.(preview|production)\.(.+)$/.exec(rest);
|
|
64
|
+
if (!m)
|
|
65
|
+
return false;
|
|
66
|
+
const env = m[1];
|
|
67
|
+
const name = m[2];
|
|
68
|
+
const projects = Object.values(state.accounts).flatMap((a) => Object.values(a.projects)).filter((p) => only === undefined || p.name === only);
|
|
69
|
+
if (projects.length === 0)
|
|
70
|
+
throw new Error(`drift ${key}: no Pages project${only ? ` named ${only}` : ""}`);
|
|
71
|
+
for (const p of projects) {
|
|
72
|
+
const vars = p.deployment_configs[env].env_vars;
|
|
73
|
+
if (value === "delete")
|
|
74
|
+
delete vars[name];
|
|
75
|
+
else if (value.startsWith("plain:"))
|
|
76
|
+
vars[name] = { type: "plain_text", value: value.slice("plain:".length) };
|
|
77
|
+
else if (value.startsWith("secret:"))
|
|
78
|
+
vars[name] = { type: "secret_text", value: value.slice("secret:".length) };
|
|
79
|
+
else
|
|
80
|
+
vars[name] = { type: vars[name]?.type ?? "plain_text", value };
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
},
|
|
84
|
+
routes(core, state, req) {
|
|
85
|
+
return routes(core, state, req);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
const PROJECT = "/accounts/:account/pages/projects/:project";
|
|
89
|
+
const routes = router([
|
|
90
|
+
route("GET", PROJECT, ({ state, params }) => {
|
|
91
|
+
const p = state.accounts[params.account]?.projects[params.project];
|
|
92
|
+
return p ? ok(publicProject(p)) : notFound();
|
|
93
|
+
}),
|
|
94
|
+
route("PATCH", PROJECT, ({ state, params, body }) => {
|
|
95
|
+
const p = state.accounts[params.account]?.projects[params.project];
|
|
96
|
+
if (!p)
|
|
97
|
+
return notFound();
|
|
98
|
+
const problem = patchProblem(body);
|
|
99
|
+
if (problem)
|
|
100
|
+
return failure(400, 8000000, problem);
|
|
101
|
+
const configs = body.deployment_configs ?? {};
|
|
102
|
+
for (const env of ENVS) {
|
|
103
|
+
// CF4: a merge. Keys absent from the body stay; `null` deletes.
|
|
104
|
+
for (const [name, v] of Object.entries(configs[env]?.env_vars ?? {})) {
|
|
105
|
+
if (v === null)
|
|
106
|
+
delete p.deployment_configs[env].env_vars[name];
|
|
107
|
+
else
|
|
108
|
+
p.deployment_configs[env].env_vars[name] = { type: v.type, value: v.value };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return ok(publicProject(p));
|
|
112
|
+
}),
|
|
113
|
+
], () => failure(404, 7003, "No route for the URI"));
|
|
114
|
+
/** Why a PATCH body is refused, or undefined. Only what the schema says about `env_vars` entries is checked. */
|
|
115
|
+
function patchProblem(body) {
|
|
116
|
+
if (body === null || typeof body !== "object" || Array.isArray(body))
|
|
117
|
+
return "request body must be a JSON object";
|
|
118
|
+
const configs = body.deployment_configs;
|
|
119
|
+
if (configs === undefined)
|
|
120
|
+
return undefined;
|
|
121
|
+
if (configs === null || typeof configs !== "object")
|
|
122
|
+
return "deployment_configs must be an object";
|
|
123
|
+
for (const env of Object.keys(configs)) {
|
|
124
|
+
if (!ENVS.includes(env))
|
|
125
|
+
return `unknown deployment config ${env}`;
|
|
126
|
+
const problem = envVarsProblem(env, configs[env]?.env_vars);
|
|
127
|
+
if (problem)
|
|
128
|
+
return problem;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
/** Why one `env_vars` map of a PATCH body is refused, or undefined. */
|
|
133
|
+
function envVarsProblem(env, vars) {
|
|
134
|
+
if (vars === undefined || vars === null)
|
|
135
|
+
return undefined;
|
|
136
|
+
if (typeof vars !== "object" || Array.isArray(vars))
|
|
137
|
+
return `deployment_configs.${env}.env_vars must be an object`;
|
|
138
|
+
for (const [name, v] of Object.entries(vars)) {
|
|
139
|
+
if (name === "")
|
|
140
|
+
return "environment variable names must not be empty";
|
|
141
|
+
if (v === null)
|
|
142
|
+
continue;
|
|
143
|
+
const e = v;
|
|
144
|
+
if ((e.type !== "plain_text" && e.type !== "secret_text") || typeof e.value !== "string")
|
|
145
|
+
return `deployment_configs.${env}.env_vars.${name} must be { type: plain_text | secret_text, value: string }`;
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
function ok(result) {
|
|
150
|
+
return new Reply(200, { success: true, errors: [], messages: [], result });
|
|
151
|
+
}
|
|
152
|
+
function failure(status, code, message) {
|
|
153
|
+
return new Reply(status, { success: false, errors: [{ code, message }], messages: [], result: null });
|
|
154
|
+
}
|
|
155
|
+
/** CF6: what Wrangler reports for an unknown project. */
|
|
156
|
+
function notFound() {
|
|
157
|
+
return failure(404, 8000007, "Project not found. The specified project name does not match any of your existing projects.");
|
|
158
|
+
}
|
|
159
|
+
function project(core, name, seed) {
|
|
160
|
+
const vars = (s) => Object.fromEntries(Object.entries(s ?? {}).map(([k, v]) => [k, typeof v === "string" ? { type: "plain_text", value: v } : { type: v.type ?? "plain_text", value: v.value }]));
|
|
161
|
+
return {
|
|
162
|
+
id: core.nextId("cf_"),
|
|
163
|
+
name,
|
|
164
|
+
subdomain: `${name}.pages.dev`,
|
|
165
|
+
createdBy: "sim",
|
|
166
|
+
deployment_configs: { preview: { env_vars: vars(seed.preview) }, production: { env_vars: vars(seed.production) } },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** The project as GET answers it: CF3, a secret's value is never returned. */
|
|
170
|
+
function publicProject(p) {
|
|
171
|
+
const config = (env) => ({
|
|
172
|
+
env_vars: Object.fromEntries(Object.entries(p.deployment_configs[env].env_vars).map(([k, v]) => [k, v.type === "secret_text" ? { type: v.type, value: "" } : { ...v }])),
|
|
173
|
+
});
|
|
174
|
+
return { id: p.id, name: p.name, subdomain: p.subdomain, production_branch: "main", deployment_configs: { preview: config("preview"), production: config("production") } };
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=cloudflare.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudflare.js","sourceRoot":"","sources":["../../src/routes/cloudflare.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulated DigitalOcean App Platform: apps built from an app spec, each create and update starting a deployment.
|
|
3
|
+
*
|
|
4
|
+
* Assumptions about the real API this fake encodes, checked against the official OpenAPI document
|
|
5
|
+
* (https://github.com/digitalocean/openapi, `specification/resources/apps`) and the docs unless marked unverified.
|
|
6
|
+
* The critical ones are pinned in scenarios/contract.test.ts (`assumption DO…`).
|
|
7
|
+
* DO1. The API is `https://api.digitalocean.com`, authenticated as `Authorization: Bearer <token>` (scopes
|
|
8
|
+
* app:create/read/update/delete); doctl reads the token from `DIGITALOCEAN_ACCESS_TOKEN`. (verified:
|
|
9
|
+
* https://docs.digitalocean.com/reference/api/digitalocean/, doctl README)
|
|
10
|
+
* DO2. `POST /v2/apps` with `{ spec }` creates an app and answers `{ app: { id, spec, pending_deployment, … } }`;
|
|
11
|
+
* `GET /v2/apps/{id}` answers `{ app }` (404 when gone); `GET /v2/apps?page=&per_page=` lists
|
|
12
|
+
* `{ apps, links: { pages: { next } }, meta }`, with no name filter. (verified: apps_create.yml, apps_get.yml,
|
|
13
|
+
* apps_list.yml)
|
|
14
|
+
* DO3. An app's `spec.name` is unique in the account (`^[a-z][a-z0-9-]{0,30}[a-z0-9]$`); the status of a
|
|
15
|
+
* duplicate create is not documented (unverified: the sim answers 409). (verified: app_spec.yml)
|
|
16
|
+
* DO4. `PUT /v2/apps/{id}` with `{ spec }` replaces the whole spec and starts a deployment. (verified:
|
|
17
|
+
* apps_update.yml; "starts a deployment" inferred from `pending_deployment` being set for UpdateApp)
|
|
18
|
+
* DO5. `pending_deployment` names the deployment a create or update started; `GET
|
|
19
|
+
* /v2/apps/{app_id}/deployments/{deployment_id}` answers `{ deployment: { id, phase } }`, the phase one of
|
|
20
|
+
* PENDING_BUILD, BUILDING, PENDING_DEPLOY, DEPLOYING, ACTIVE, SUPERSEDED, ERROR, CANCELED. (verified:
|
|
21
|
+
* app.yml, apps_get_deployment.yml, apps_deployment_response.yml, apps_deployment_phase.yml)
|
|
22
|
+
* DO6. The platform adds defaults to the spec it stores (a region, instance counts); it keeps every field the
|
|
23
|
+
* client sent, lists in the order sent. (unverified: the sim adds `region` and `instance_count`)
|
|
24
|
+
* DO7. A `type: SECRET` variable's value is encrypted on first submission and read back as `EV[…]`.
|
|
25
|
+
* (verified: app_variable_definition.yml, "encrypted on first submission"; the read-back form unverified)
|
|
26
|
+
* DO8. `DELETE /v2/apps/{id}` deletes the app and its deployments (200 `{ id }`; 404 when gone). (verified:
|
|
27
|
+
* apps_delete.yml)
|
|
28
|
+
*/
|
|
29
|
+
import { type CreatedBy, type ProviderSim } from "../provider.js";
|
|
30
|
+
/** An app as the simulated App Platform stores it. */
|
|
31
|
+
export interface DigitaloceanApp {
|
|
32
|
+
id: string;
|
|
33
|
+
spec: Record<string, unknown>;
|
|
34
|
+
liveUrl: string | null;
|
|
35
|
+
/** Deployments by id: phase, and polls left before the next phase. */
|
|
36
|
+
deployments: Record<string, {
|
|
37
|
+
phase: string;
|
|
38
|
+
polls: number;
|
|
39
|
+
}>;
|
|
40
|
+
pending: string | null;
|
|
41
|
+
createdBy: CreatedBy;
|
|
42
|
+
}
|
|
43
|
+
/** The simulated App Platform's state. */
|
|
44
|
+
export interface DigitaloceanState {
|
|
45
|
+
apps: DigitaloceanApp[];
|
|
46
|
+
/** Polls a deployment answers BUILDING before it is ACTIVE (default 1); `fail` ends it ERROR instead. */
|
|
47
|
+
deployPolls: number;
|
|
48
|
+
failDeployments: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** What a scenario or test seeds the simulated App Platform with (`seed: { digitalocean: … }`). */
|
|
51
|
+
export interface DigitaloceanSeed {
|
|
52
|
+
/** Apps that exist already (unmanaged), by spec. */
|
|
53
|
+
apps?: Array<Record<string, unknown>>;
|
|
54
|
+
deployPolls?: number;
|
|
55
|
+
failDeployments?: boolean;
|
|
56
|
+
}
|
|
57
|
+
/** The simulated App Platform API, registered in `PROVIDERS` (packages/sim/src/state.ts) and served under `/digitalocean`. */
|
|
58
|
+
export declare const digitaloceanSim: ProviderSim<DigitaloceanState, DigitaloceanSeed>;
|