@sponson/sim 0.0.0-stage → 0.4.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 +4 -2
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +21 -0
- package/dist/bin.js.map +1 -0
- package/dist/chaos.d.ts +70 -0
- package/dist/chaos.js +78 -0
- package/dist/chaos.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +99 -0
- package/dist/provider.js +62 -0
- package/dist/provider.js.map +1 -0
- package/dist/routes/clerk.d.ts +33 -0
- package/dist/routes/clerk.js +85 -0
- package/dist/routes/clerk.js.map +1 -0
- package/dist/routes/neon.d.ts +73 -0
- package/dist/routes/neon.js +184 -0
- package/dist/routes/neon.js.map +1 -0
- package/dist/routes/vercel.d.ts +114 -0
- package/dist/routes/vercel.js +370 -0
- package/dist/routes/vercel.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +130 -0
- package/dist/server.js.map +1 -0
- package/dist/state.d.ts +90 -0
- package/dist/state.js +109 -0
- package/dist/state.js.map +1 -0
- package/package.json +47 -3
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulated Clerk: the instance's allowed redirect URLs.
|
|
3
|
+
*
|
|
4
|
+
* Assumptions about the real API that this fake encodes, each marked with how far it is checked. "Verified" means
|
|
5
|
+
* against Clerk's published Backend API OpenAPI document (github.com/clerk/openapi-specs, bapi/2026-05-12.yml,
|
|
6
|
+
* identical for /redirect_urls back to 2021-02-05; fetched 2026-10-10); see docs/api-verification.md:
|
|
7
|
+
* C1. A duplicate redirect URL answers 422 (ClerkErrors body). Unverified: the spec documents 400 and 422 for
|
|
8
|
+
* POST /redirect_urls without saying which a duplicate gets. The adapter checks the list after either.
|
|
9
|
+
* C2. GET /redirect_urls is a bare JSON array (verified); with `paginated=true` it is `{ data, total_count }`
|
|
10
|
+
* paged by `offset`/`limit` (limit 1–500, default 10: verified parameters). The envelope is not in the
|
|
11
|
+
* spec; it is how Clerk's own SDKs (clerk/javascript RedirectUrlApi, clerk-sdk-go redirecturl) call and
|
|
12
|
+
* decode it. Chaos `page_size` caps the page further.
|
|
13
|
+
* C3. A redirect URL is `{ object: "redirect_url", id, url, created_at, updated_at }` and DELETE answers a
|
|
14
|
+
* DeletedObject `{ object, id, deleted }` (verified).
|
|
15
|
+
*/
|
|
16
|
+
import { type CreatedBy, type ProviderSim } from "../provider.js";
|
|
17
|
+
/** One allow-listed redirect URL. */
|
|
18
|
+
export interface ClerkRedirect {
|
|
19
|
+
id: string;
|
|
20
|
+
url: string;
|
|
21
|
+
createdAt: number;
|
|
22
|
+
createdBy: CreatedBy;
|
|
23
|
+
}
|
|
24
|
+
/** Simulated Clerk: the instance's redirect allow-list. */
|
|
25
|
+
export interface ClerkState {
|
|
26
|
+
redirect_urls: ClerkRedirect[];
|
|
27
|
+
}
|
|
28
|
+
/** Initial Clerk state. */
|
|
29
|
+
export interface ClerkSeed {
|
|
30
|
+
redirect_urls: string[];
|
|
31
|
+
}
|
|
32
|
+
/** Simulated Clerk (redirect URL allow-list); see the assumptions at the top of this file. */
|
|
33
|
+
export declare const clerkSim: ProviderSim<ClerkState, ClerkSeed>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulated Clerk: the instance's allowed redirect URLs.
|
|
3
|
+
*
|
|
4
|
+
* Assumptions about the real API that this fake encodes, each marked with how far it is checked. "Verified" means
|
|
5
|
+
* against Clerk's published Backend API OpenAPI document (github.com/clerk/openapi-specs, bapi/2026-05-12.yml,
|
|
6
|
+
* identical for /redirect_urls back to 2021-02-05; fetched 2026-10-10); see docs/api-verification.md:
|
|
7
|
+
* C1. A duplicate redirect URL answers 422 (ClerkErrors body). Unverified: the spec documents 400 and 422 for
|
|
8
|
+
* POST /redirect_urls without saying which a duplicate gets. The adapter checks the list after either.
|
|
9
|
+
* C2. GET /redirect_urls is a bare JSON array (verified); with `paginated=true` it is `{ data, total_count }`
|
|
10
|
+
* paged by `offset`/`limit` (limit 1–500, default 10: verified parameters). The envelope is not in the
|
|
11
|
+
* spec; it is how Clerk's own SDKs (clerk/javascript RedirectUrlApi, clerk-sdk-go redirecturl) call and
|
|
12
|
+
* decode it. Chaos `page_size` caps the page further.
|
|
13
|
+
* C3. A redirect URL is `{ object: "redirect_url", id, url, created_at, updated_at }` and DELETE answers a
|
|
14
|
+
* DeletedObject `{ object, id, deleted }` (verified).
|
|
15
|
+
*/
|
|
16
|
+
import { page, Reply, route, router } from "../provider.js";
|
|
17
|
+
/** Simulated Clerk (redirect URL allow-list); see the assumptions at the top of this file. */
|
|
18
|
+
export const clerkSim = {
|
|
19
|
+
env: { token: "CLERK_SECRET_KEY", url: "CLERK_API_URL", testToken: "tok_clerk" },
|
|
20
|
+
defaultSeed: { redirect_urls: [] },
|
|
21
|
+
reset(core, seed) {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
return { redirect_urls: (seed?.redirect_urls ?? []).map((url) => redirect(core, url, now, "sim")) };
|
|
24
|
+
},
|
|
25
|
+
/** `redirect.<url>`: "delete" | "recreate". Clerk has one instance per key, so there is no `clerk:<project>.` form. */
|
|
26
|
+
drift(core, state, { key, rest, value, only }) {
|
|
27
|
+
if (only !== undefined || !rest.startsWith("redirect."))
|
|
28
|
+
return false;
|
|
29
|
+
const url = rest.slice("redirect.".length);
|
|
30
|
+
if (value !== "delete" && value !== "recreate")
|
|
31
|
+
throw new Error(`drift ${key}: only "delete" and "recreate" are supported`);
|
|
32
|
+
const hit = state.redirect_urls.filter((r) => r.url === url);
|
|
33
|
+
state.redirect_urls = state.redirect_urls.filter((r) => !hit.includes(r));
|
|
34
|
+
if (value === "recreate")
|
|
35
|
+
for (const r of hit)
|
|
36
|
+
state.redirect_urls.push(redirect(core, r.url, Date.now(), "sim"));
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
routes(core, state, req) {
|
|
40
|
+
return routes(core, state, req);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const routes = router([
|
|
44
|
+
route("GET", "/redirect_urls", ({ core, state, url }) => {
|
|
45
|
+
const all = state.redirect_urls.map(publicRedirect);
|
|
46
|
+
if (url.searchParams.get("paginated") !== "true")
|
|
47
|
+
return new Reply(200, all);
|
|
48
|
+
const limit = url.searchParams.get("limit");
|
|
49
|
+
const offset = url.searchParams.get("offset");
|
|
50
|
+
if ((limit !== null && !/^\d+$/.test(limit)) || (offset !== null && !/^\d+$/.test(offset)) || Number(limit ?? 10) < 1 || Number(limit ?? 10) > 500) {
|
|
51
|
+
return clerkError(422, "form_param_format_invalid", "limit must be 1–500 and offset ≥ 0");
|
|
52
|
+
}
|
|
53
|
+
const pg = page(core, all, offset, Number(limit ?? 10));
|
|
54
|
+
return new Reply(200, { data: pg.items, total_count: all.length });
|
|
55
|
+
}),
|
|
56
|
+
route("POST", "/redirect_urls", ({ core, state, body }) => {
|
|
57
|
+
const b = (body ?? {});
|
|
58
|
+
if (!b.url)
|
|
59
|
+
return clerkError(422, "form_param_missing", "url is required");
|
|
60
|
+
if (state.redirect_urls.some((r) => r.url === b.url))
|
|
61
|
+
return clerkError(422, "duplicate_record", "redirect url already exists");
|
|
62
|
+
const r = redirect(core, b.url, Date.now(), "api");
|
|
63
|
+
state.redirect_urls.push(r);
|
|
64
|
+
return new Reply(200, publicRedirect(r));
|
|
65
|
+
}),
|
|
66
|
+
route("DELETE", "/redirect_urls/:id", ({ state, params }) => {
|
|
67
|
+
const r = state.redirect_urls.find((x) => x.id === params.id);
|
|
68
|
+
if (!r)
|
|
69
|
+
return clerkError(404, "resource_not_found", "not found");
|
|
70
|
+
state.redirect_urls = state.redirect_urls.filter((x) => x !== r);
|
|
71
|
+
return new Reply(200, { id: r.id, object: "redirect_url", deleted: true });
|
|
72
|
+
}),
|
|
73
|
+
], () => clerkError(404, "resource_not_found", "not found"));
|
|
74
|
+
/** The spec's RedirectURL: `object`, `id`, `url`, `created_at`, `updated_at` (all required). */
|
|
75
|
+
function publicRedirect(r) {
|
|
76
|
+
return { object: "redirect_url", id: r.id, url: r.url, created_at: r.createdAt, updated_at: r.createdAt };
|
|
77
|
+
}
|
|
78
|
+
/** The spec's ClerkErrors body. */
|
|
79
|
+
function clerkError(status, code, message) {
|
|
80
|
+
return new Reply(status, { errors: [{ code, message, long_message: message }] });
|
|
81
|
+
}
|
|
82
|
+
function redirect(core, url, createdAt, createdBy) {
|
|
83
|
+
return { id: core.nextId("ru_"), url, createdAt, createdBy };
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=clerk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clerk.js","sourceRoot":"","sources":["../../src/routes/clerk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAkD,MAAM,gBAAgB,CAAC;AAoB5G,8FAA8F;AAC9F,MAAM,CAAC,MAAM,QAAQ,GAAuC;IAC1D,GAAG,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,GAAG,EAAE,eAAe,EAAE,SAAS,EAAE,WAAW,EAAE;IAChF,WAAW,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE;IAElC,KAAK,CAAC,IAAI,EAAE,IAAI;QACd,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,OAAO,EAAE,aAAa,EAAE,CAAC,IAAI,EAAE,aAAa,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;IACtG,CAAC;IAED,uHAAuH;IACvH,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;QAC3C,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;YAAE,OAAO,KAAK,CAAC;QACtE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAC3C,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,UAAU;YAAE,MAAM,IAAI,KAAK,CAAC,SAAS,GAAG,8CAA8C,CAAC,CAAC;QAC5H,MAAM,GAAG,GAAG,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;QAC7D,KAAK,CAAC,aAAa,GAAG,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1E,IAAI,KAAK,KAAK,UAAU;YAAE,KAAK,MAAM,CAAC,IAAI,GAAG;gBAAE,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC;QAClH,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG;QACrB,OAAO,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;IAClC,CAAC;CACF,CAAC;AAEF,MAAM,MAAM,GAAG,MAAM,CACnB;IACE,KAAK,CAAC,KAAK,EAAE,gBAAgB,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE;QACtD,MAAM,GAAG,GAAG,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QACpD,IAAI,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,MAAM;YAAE,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC7E,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC9C,IAAI,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC;YACnJ,OAAO,UAAU,CAAC,GAAG,EAAE,2BAA2B,EAAE,oCAAoC,CAAC,CAAC;QAC5F,CAAC;QACD,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC;QACxD,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IACrE,CAAC,CAAC;IAEF,KAAK,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE;QACxD,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAqB,CAAC;QAC3C,IAAI,CAAC,CAAC,CAAC,GAAG;YAAE,OAAO,UAAU,CAAC,GAAG,EAAE,oBAAoB,EAAE,iBAAiB,CAAC,CAAC;QAC5E,IAAI,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC;YAAE,OAAO,UAAU,CAAC,GAAG,EAAE,kBAAkB,EAAE,6BAA6B,CAAC,CAAC;QAChI,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,CAAC,CAAC;QACnD,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC5B,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC,CAAC;IAEF,KAAK,CAAC,QAAQ,EAAE,oBAAoB,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;QAC1D,MAAM,CAAC,GAAG,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,CAAC,CAAC;QAC9D,IAAI,CAAC,CAAC;YAAE,OAAO,UAAU,CAAC,GAAG,EAAE,oBAAoB,EAAE,WAAW,CAAC,CAAC;QAClE,KAAK,CAAC,aAAa,GAAG,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QACjE,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7E,CAAC,CAAC;CACH,EACD,GAAG,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,oBAAoB,EAAE,WAAW,CAAC,CACzD,CAAC;AAEF,gGAAgG;AAChG,SAAS,cAAc,CAAC,CAAgB;IACtC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,UAAU,EAAE,CAAC,CAAC,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC;AAC5G,CAAC;AAED,mCAAmC;AACnC,SAAS,UAAU,CAAC,MAAc,EAAE,IAAY,EAAE,OAAe;IAC/D,OAAO,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;AACnF,CAAC;AAED,SAAS,QAAQ,CAAC,IAAa,EAAE,GAAW,EAAE,SAAiB,EAAE,SAAoB;IACnF,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;AAC/D,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulated Neon: branches, their endpoints, databases and connection strings.
|
|
3
|
+
*
|
|
4
|
+
* Assumptions about the real API that this fake encodes, each marked with how far it is checked. "Verified" means
|
|
5
|
+
* against Neon's published OpenAPI document (https://neon.tech/api_spec/release/v2.json, fetched 2026-10-10); see
|
|
6
|
+
* docs/api-verification.md:
|
|
7
|
+
* N1. Branch deletion is synchronous here: the branch is gone when DELETE returns. Contradicted in part: the
|
|
8
|
+
* spec says "the deletion completes after all operations finish" and DELETE answers with `operations`.
|
|
9
|
+
* Whether the list still shows the branch meanwhile is unverified: needs a live account.
|
|
10
|
+
* N2. Branch creation runs async operations (verified: the create answer carries `operations`): for chaos
|
|
11
|
+
* `neon_op_ms` (default 0) after a create, further creates in the project and writes to the new branch
|
|
12
|
+
* answer 423 Locked, which the spec documents as safe to retry (verified). Which requests are locked, and
|
|
13
|
+
* that the endpoint is usable at once, are unverified: needs a live account.
|
|
14
|
+
* N3. A root branch has one database `neondb` owned by role `neondb_owner` (a new project's defaults); a child
|
|
15
|
+
* copies its parent's. The adapter no longer relies on these names: it reads them from
|
|
16
|
+
* GET /projects/:id/branches/:id/databases (verified), and connection_uri requires both (verified).
|
|
17
|
+
* Branches carry `default: true` on the project's root branch (verified; `primary` is deprecated).
|
|
18
|
+
* N4. Pagination (chaos `page_size`): `{ branches, pagination: { next } }` with `?cursor=`, cursors opaque
|
|
19
|
+
* (verified: CursorPagination). Without `limit` the list is not paged here; the spec says a paged read
|
|
20
|
+
* starts with `limit`, and the adapter follows `next` either way.
|
|
21
|
+
* N5. Creating a branch whose name exists answers 409. Unverified: the spec documents only the generic error.
|
|
22
|
+
*/
|
|
23
|
+
import { type CreatedBy, type ProviderSim, type SimCore } from "../provider.js";
|
|
24
|
+
/** One simulated Neon branch. */
|
|
25
|
+
export interface NeonBranch {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
parent_id: string | null;
|
|
29
|
+
/** The project's root branch (Neon's `default`, formerly `primary`). */
|
|
30
|
+
default: boolean;
|
|
31
|
+
/** Async operations started by the create are running until this time: writes to the branch answer 423. */
|
|
32
|
+
opUntil: number;
|
|
33
|
+
created_at: string;
|
|
34
|
+
endpoint: {
|
|
35
|
+
id: string;
|
|
36
|
+
host: string;
|
|
37
|
+
};
|
|
38
|
+
/** The branch's one database and the role owning it; a child branch copies its parent's. */
|
|
39
|
+
database: {
|
|
40
|
+
name: string;
|
|
41
|
+
owner_name: string;
|
|
42
|
+
};
|
|
43
|
+
createdAt: number;
|
|
44
|
+
createdBy: CreatedBy;
|
|
45
|
+
}
|
|
46
|
+
/** One simulated Neon project and its branches. */
|
|
47
|
+
export interface NeonProject {
|
|
48
|
+
branches: NeonBranch[];
|
|
49
|
+
/** A branch create's operations are running until this time: further creates answer 423. */
|
|
50
|
+
opUntil: number;
|
|
51
|
+
}
|
|
52
|
+
/** Simulated Neon: projects by id. */
|
|
53
|
+
export interface NeonState {
|
|
54
|
+
projects: Record<string, NeonProject>;
|
|
55
|
+
}
|
|
56
|
+
/** Initial Neon state. */
|
|
57
|
+
export interface NeonSeed {
|
|
58
|
+
projects: Record<string, {
|
|
59
|
+
branches: Array<{
|
|
60
|
+
name: string;
|
|
61
|
+
parent?: string;
|
|
62
|
+
}>;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
/** Simulated Neon (projects, branches, operations); see the assumptions at the top of this file. */
|
|
66
|
+
export declare const neonSim: ProviderSim<NeonState, NeonSeed>;
|
|
67
|
+
/**
|
|
68
|
+
* Add a branch to a project as Neon would (ids, timestamps, default flag). Tests use it to seed "someone else's"
|
|
69
|
+
* branch.
|
|
70
|
+
*/
|
|
71
|
+
export declare function createBranch(core: SimCore, project: NeonProject, name: string, parentId: string | null, createdBy: CreatedBy): NeonBranch;
|
|
72
|
+
/** The connection string Neon would hand out for a branch. */
|
|
73
|
+
export declare function connectionUri(branch: NeonBranch): string;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulated Neon: branches, their endpoints, databases and connection strings.
|
|
3
|
+
*
|
|
4
|
+
* Assumptions about the real API that this fake encodes, each marked with how far it is checked. "Verified" means
|
|
5
|
+
* against Neon's published OpenAPI document (https://neon.tech/api_spec/release/v2.json, fetched 2026-10-10); see
|
|
6
|
+
* docs/api-verification.md:
|
|
7
|
+
* N1. Branch deletion is synchronous here: the branch is gone when DELETE returns. Contradicted in part: the
|
|
8
|
+
* spec says "the deletion completes after all operations finish" and DELETE answers with `operations`.
|
|
9
|
+
* Whether the list still shows the branch meanwhile is unverified: needs a live account.
|
|
10
|
+
* N2. Branch creation runs async operations (verified: the create answer carries `operations`): for chaos
|
|
11
|
+
* `neon_op_ms` (default 0) after a create, further creates in the project and writes to the new branch
|
|
12
|
+
* answer 423 Locked, which the spec documents as safe to retry (verified). Which requests are locked, and
|
|
13
|
+
* that the endpoint is usable at once, are unverified: needs a live account.
|
|
14
|
+
* N3. A root branch has one database `neondb` owned by role `neondb_owner` (a new project's defaults); a child
|
|
15
|
+
* copies its parent's. The adapter no longer relies on these names: it reads them from
|
|
16
|
+
* GET /projects/:id/branches/:id/databases (verified), and connection_uri requires both (verified).
|
|
17
|
+
* Branches carry `default: true` on the project's root branch (verified; `primary` is deprecated).
|
|
18
|
+
* N4. Pagination (chaos `page_size`): `{ branches, pagination: { next } }` with `?cursor=`, cursors opaque
|
|
19
|
+
* (verified: CursorPagination). Without `limit` the list is not paged here; the spec says a paged read
|
|
20
|
+
* starts with `limit`, and the adapter follows `next` either way.
|
|
21
|
+
* N5. Creating a branch whose name exists answers 409. Unverified: the spec documents only the generic error.
|
|
22
|
+
*/
|
|
23
|
+
import { page, Reply, route, router } from "../provider.js";
|
|
24
|
+
/** Simulated Neon (projects, branches, operations); see the assumptions at the top of this file. */
|
|
25
|
+
export const neonSim = {
|
|
26
|
+
env: { token: "NEON_API_KEY", url: "NEON_API_URL", testToken: "tok_neon" },
|
|
27
|
+
defaultSeed: { projects: { proj_demo: { branches: [{ name: "main" }] } } },
|
|
28
|
+
reset(core, seed) {
|
|
29
|
+
const state = { projects: {} };
|
|
30
|
+
for (const [id, p] of Object.entries(seed?.projects ?? {})) {
|
|
31
|
+
const project = { branches: [], opUntil: 0 };
|
|
32
|
+
state.projects[id] = project;
|
|
33
|
+
for (const b of p.branches) {
|
|
34
|
+
const parent = b.parent ? project.branches.find((x) => x.name === b.parent) : undefined;
|
|
35
|
+
const branch = createBranch(core, project, b.name, parent?.id ?? null, "sim");
|
|
36
|
+
// The first root branch is the project's default, like the one Neon creates with the project.
|
|
37
|
+
if (!branch.parent_id && !project.branches.some((x) => x.default))
|
|
38
|
+
branch.default = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return state;
|
|
42
|
+
},
|
|
43
|
+
/** `branch.<name>`: "delete" | "recreate". */
|
|
44
|
+
drift(core, state, { key, rest, value, only }) {
|
|
45
|
+
if (!rest.startsWith("branch."))
|
|
46
|
+
return false;
|
|
47
|
+
const name = rest.slice("branch.".length);
|
|
48
|
+
if (value !== "delete" && value !== "recreate")
|
|
49
|
+
throw new Error(`drift ${key}: only "delete" and "recreate" are supported`);
|
|
50
|
+
for (const [id, p] of Object.entries(state.projects)) {
|
|
51
|
+
if (only !== undefined && id !== only)
|
|
52
|
+
continue;
|
|
53
|
+
const hit = p.branches.filter((b) => b.name === name);
|
|
54
|
+
p.branches = p.branches.filter((b) => !hit.includes(b));
|
|
55
|
+
if (value === "recreate")
|
|
56
|
+
for (const b of hit)
|
|
57
|
+
createBranch(core, p, b.name, b.parent_id, "sim");
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
},
|
|
61
|
+
routes(core, state, req) {
|
|
62
|
+
return routes(core, state, req);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const routes = router([
|
|
66
|
+
route("GET", "/projects/:project/branches", ({ core, state, params, url }) => {
|
|
67
|
+
const p = state.projects[params.project];
|
|
68
|
+
if (!p)
|
|
69
|
+
return notFound("project");
|
|
70
|
+
const pg = page(core, p.branches, url.searchParams.get("cursor"));
|
|
71
|
+
return new Reply(200, { branches: pg.items.map(publicBranch), pagination: pg.next === null ? {} : { next: String(pg.next) } });
|
|
72
|
+
}),
|
|
73
|
+
route("POST", "/projects/:project/branches", ({ core, state, params, body }) => {
|
|
74
|
+
const p = state.projects[params.project];
|
|
75
|
+
if (!p)
|
|
76
|
+
return notFound("project");
|
|
77
|
+
if (Date.now() < p.opUntil)
|
|
78
|
+
return locked();
|
|
79
|
+
const b = (body ?? {});
|
|
80
|
+
const name = b.branch?.name;
|
|
81
|
+
if (!name)
|
|
82
|
+
return new Reply(400, { code: "", message: "branch.name required" });
|
|
83
|
+
if (p.branches.some((x) => x.name === name))
|
|
84
|
+
return new Reply(409, { code: "", message: "branch already exists" });
|
|
85
|
+
const parentId = b.branch?.parent_id ?? null;
|
|
86
|
+
if (parentId && !p.branches.some((x) => x.id === parentId))
|
|
87
|
+
return notFound("parent branch");
|
|
88
|
+
const branch = createBranch(core, p, name, parentId, "api");
|
|
89
|
+
if (core.chaos.neon_op_ms > 0)
|
|
90
|
+
branch.opUntil = p.opUntil = Date.now() + core.chaos.neon_op_ms;
|
|
91
|
+
// CreatedBranch: branch, endpoints, operations, roles, databases and (one database and role) connection_uris.
|
|
92
|
+
return new Reply(201, {
|
|
93
|
+
branch: publicBranch(branch),
|
|
94
|
+
endpoints: [publicEndpoint(branch)],
|
|
95
|
+
operations: [{ id: core.nextId("op-"), branch_id: branch.id, action: "create_branch", status: core.chaos.neon_op_ms > 0 ? "running" : "finished" }],
|
|
96
|
+
roles: [{ branch_id: branch.id, name: branch.database.owner_name }],
|
|
97
|
+
databases: [publicDatabase(branch)],
|
|
98
|
+
connection_uris: [{ connection_uri: connectionUri(branch), connection_parameters: { database: branch.database.name, role: branch.database.owner_name, host: branch.endpoint.host } }],
|
|
99
|
+
});
|
|
100
|
+
}),
|
|
101
|
+
route("GET", "/projects/:project/branches/:branch/endpoints", ({ state, params }) => {
|
|
102
|
+
const b = branchOf(state, params.project, params.branch);
|
|
103
|
+
return b ? new Reply(200, { endpoints: [publicEndpoint(b)] }) : notFound("branch");
|
|
104
|
+
}),
|
|
105
|
+
route("GET", "/projects/:project/branches/:branch/databases", ({ state, params }) => {
|
|
106
|
+
const b = branchOf(state, params.project, params.branch);
|
|
107
|
+
return b ? new Reply(200, { databases: [publicDatabase(b)] }) : notFound("branch");
|
|
108
|
+
}),
|
|
109
|
+
route("GET", "/projects/:project/connection_uri", ({ state, params, url }) => {
|
|
110
|
+
const b = branchOf(state, params.project, url.searchParams.get("branch_id"));
|
|
111
|
+
if (!b)
|
|
112
|
+
return notFound("branch");
|
|
113
|
+
// Both are required query parameters in the spec.
|
|
114
|
+
const database = url.searchParams.get("database_name");
|
|
115
|
+
const role = url.searchParams.get("role_name");
|
|
116
|
+
if (!database || !role)
|
|
117
|
+
return new Reply(400, { code: "", message: "database_name and role_name are required" });
|
|
118
|
+
if (database !== b.database.name)
|
|
119
|
+
return notFound(`database ${database}`);
|
|
120
|
+
if (role !== b.database.owner_name)
|
|
121
|
+
return notFound(`role ${role}`);
|
|
122
|
+
return new Reply(200, { uri: connectionUri(b) });
|
|
123
|
+
}),
|
|
124
|
+
route("DELETE", "/projects/:project/branches/:branch", ({ core, state, params }) => {
|
|
125
|
+
const p = state.projects[params.project];
|
|
126
|
+
const b = branchOf(state, params.project, params.branch);
|
|
127
|
+
if (!p || !b)
|
|
128
|
+
return notFound("branch");
|
|
129
|
+
if (Date.now() < b.opUntil)
|
|
130
|
+
return locked();
|
|
131
|
+
// The real deletion "completes after all operations finish" (assumption N1); the sim removes it at once.
|
|
132
|
+
p.branches = p.branches.filter((x) => x !== b);
|
|
133
|
+
return new Reply(200, { branch: publicBranch(b), operations: [{ id: core.nextId("op-"), branch_id: b.id, action: "delete_timeline", status: "finished" }] });
|
|
134
|
+
}),
|
|
135
|
+
], () => notFound("route"));
|
|
136
|
+
/** A branch of a project by id; undefined when either does not exist. */
|
|
137
|
+
function branchOf(state, project, branchId) {
|
|
138
|
+
return state.projects[project]?.branches.find((x) => x.id === branchId);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Add a branch to a project as Neon would (ids, timestamps, default flag). Tests use it to seed "someone else's"
|
|
142
|
+
* branch.
|
|
143
|
+
*/
|
|
144
|
+
export function createBranch(core, project, name, parentId, createdBy) {
|
|
145
|
+
const id = core.nextId("br-");
|
|
146
|
+
const branch = {
|
|
147
|
+
id,
|
|
148
|
+
name,
|
|
149
|
+
parent_id: parentId,
|
|
150
|
+
default: false,
|
|
151
|
+
opUntil: 0,
|
|
152
|
+
created_at: new Date().toISOString(),
|
|
153
|
+
endpoint: { id: core.nextId("ep-"), host: `${id}.sim.neon.tech` },
|
|
154
|
+
database: { ...(project.branches.find((b) => b.id === parentId)?.database ?? { name: DATABASE, owner_name: ROLE }) },
|
|
155
|
+
createdAt: Date.now(),
|
|
156
|
+
createdBy,
|
|
157
|
+
};
|
|
158
|
+
project.branches.push(branch);
|
|
159
|
+
return branch;
|
|
160
|
+
}
|
|
161
|
+
/** A root branch's database and its owner: a new Neon project's defaults. */
|
|
162
|
+
const DATABASE = "neondb";
|
|
163
|
+
const ROLE = "neondb_owner";
|
|
164
|
+
/** The connection string Neon would hand out for a branch. */
|
|
165
|
+
export function connectionUri(branch) {
|
|
166
|
+
return `postgres://${branch.database.owner_name}:pw_${branch.id}@${branch.endpoint.host}/${branch.database.name}`;
|
|
167
|
+
}
|
|
168
|
+
function publicEndpoint(b) {
|
|
169
|
+
return { id: b.endpoint.id, host: b.endpoint.host, branch_id: b.id, type: "read_write" };
|
|
170
|
+
}
|
|
171
|
+
function publicDatabase(b) {
|
|
172
|
+
return { id: 1, branch_id: b.id, name: b.database.name, owner_name: b.database.owner_name, created_at: b.created_at, updated_at: b.created_at };
|
|
173
|
+
}
|
|
174
|
+
/** Neon's GeneralError: `{ code, message }`. */
|
|
175
|
+
function notFound(what) {
|
|
176
|
+
return new Reply(404, { code: "", message: `${what} not found` });
|
|
177
|
+
}
|
|
178
|
+
function publicBranch(b) {
|
|
179
|
+
return { id: b.id, name: b.name, parent_id: b.parent_id, default: b.default, current_state: "ready", created_at: b.created_at };
|
|
180
|
+
}
|
|
181
|
+
function locked() {
|
|
182
|
+
return new Reply(423, { code: "", message: "project already has running conflicting operations, scheduling of new ones is prohibited" });
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=neon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"neon.js","sourceRoot":"","sources":["../../src/routes/neon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAkD,MAAM,gBAAgB,CAAC;AAoC5G,oGAAoG;AACpG,MAAM,CAAC,MAAM,OAAO,GAAqC;IACvD,GAAG,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE;IAC1E,WAAW,EAAE,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE;IAE1E,KAAK,CAAC,IAAI,EAAE,IAAI;QACd,MAAM,KAAK,GAAc,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;QAC1C,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC,EAAE,CAAC;YAC3D,MAAM,OAAO,GAAgB,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;YAC1D,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC;YAC7B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;gBAC3B,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;gBACxF,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,IAAI,EAAE,KAAK,CAAC,CAAC;gBAC9E,8FAA8F;gBAC9F,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;oBAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;YAC3F,CAAC;QACH,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,8CAA8C;IAC9C,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;QAC3C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,KAAK,CAAC;QAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC1C,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,UAAU;YAAE,MAAM,IAAI,KAAK,CAAC,SAAS,GAAG,8CAA8C,CAAC,CAAC;QAC5H,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrD,IAAI,IAAI,KAAK,SAAS,IAAI,EAAE,KAAK,IAAI;gBAAE,SAAS;YAChD,MAAM,GAAG,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;YACtD,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;YACxD,IAAI,KAAK,KAAK,UAAU;gBAAE,KAAK,MAAM,CAAC,IAAI,GAAG;oBAAE,YAAY,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACnG,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG;QACrB,OAAO,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;IAClC,CAAC;CACF,CAAC;AAEF,MAAM,MAAM,GAAG,MAAM,CACnB;IACE,KAAK,CAAC,KAAK,EAAE,6BAA6B,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE;QAC3E,MAAM,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACzC,IAAI,CAAC,CAAC;YAAE,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAC;QACnC,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC;QAClE,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,UAAU,EAAE,EAAE,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;IACjI,CAAC,CAAC;IAEF,KAAK,CAAC,MAAM,EAAE,6BAA6B,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE;QAC7E,MAAM,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACzC,IAAI,CAAC,CAAC;YAAE,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAC;QACnC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,OAAO;YAAE,OAAO,MAAM,EAAE,CAAC;QAC5C,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAuD,CAAC;QAC7E,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC;QAC5B,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,sBAAsB,EAAE,CAAC,CAAC;QAChF,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC;YAAE,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC,CAAC;QACnH,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,EAAE,SAAS,IAAI,IAAI,CAAC;QAC7C,IAAI,QAAQ,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC;YAAE,OAAO,QAAQ,CAAC,eAAe,CAAC,CAAC;QAC7F,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QAC5D,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC;YAAE,MAAM,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC;QAC/F,8GAA8G;QAC9G,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE;YACpB,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC;YAC5B,SAAS,EAAE,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;YACnC,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;YACnJ,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;YACnE,SAAS,EAAE,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;YACnC,eAAe,EAAE,CAAC,EAAE,cAAc,EAAE,aAAa,CAAC,MAAM,CAAC,EAAE,qBAAqB,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;SACtL,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,KAAK,CAAC,KAAK,EAAE,+CAA+C,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;QAClF,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACzD,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACrF,CAAC,CAAC;IAEF,KAAK,CAAC,KAAK,EAAE,+CAA+C,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;QAClF,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACzD,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACrF,CAAC,CAAC;IAEF,KAAK,CAAC,KAAK,EAAE,mCAAmC,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE;QAC3E,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC;QAC7E,IAAI,CAAC,CAAC;YAAE,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAClC,kDAAkD;QAClD,MAAM,QAAQ,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QACvD,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC/C,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,0CAA0C,EAAE,CAAC,CAAC;QACjH,IAAI,QAAQ,KAAK,CAAC,CAAC,QAAQ,CAAC,IAAI;YAAE,OAAO,QAAQ,CAAC,YAAY,QAAQ,EAAE,CAAC,CAAC;QAC1E,IAAI,IAAI,KAAK,CAAC,CAAC,QAAQ,CAAC,UAAU;YAAE,OAAO,QAAQ,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;QACpE,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACnD,CAAC,CAAC;IAEF,KAAK,CAAC,QAAQ,EAAE,qCAAqC,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;QACjF,MAAM,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACzC,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACzD,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;YAAE,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,OAAO;YAAE,OAAO,MAAM,EAAE,CAAC;QAC5C,yGAAyG;QACzG,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QAC/C,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/J,CAAC,CAAC;CACH,EACD,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CACxB,CAAC;AAEF,yEAAyE;AACzE,SAAS,QAAQ,CAAC,KAAgB,EAAE,OAAe,EAAE,QAAuB;IAC1E,OAAO,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC;AAC1E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,IAAa,EAAE,OAAoB,EAAE,IAAY,EAAE,QAAuB,EAAE,SAAoB;IAC3H,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAe;QACzB,EAAE;QACF,IAAI;QACJ,SAAS,EAAE,QAAQ;QACnB,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,CAAC;QACV,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACpC,QAAQ,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,gBAAgB,EAAE;QACjE,QAAQ,EAAE,EAAE,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,EAAE,QAAQ,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,EAAE;QACpH,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,SAAS;KACV,CAAC;IACF,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9B,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,6EAA6E;AAC7E,MAAM,QAAQ,GAAG,QAAQ,CAAC;AAC1B,MAAM,IAAI,GAAG,cAAc,CAAC;AAE5B,8DAA8D;AAC9D,MAAM,UAAU,aAAa,CAAC,MAAkB;IAC9C,OAAO,cAAc,MAAM,CAAC,QAAQ,CAAC,UAAU,OAAO,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;AACpH,CAAC;AAED,SAAS,cAAc,CAAC,CAAa;IACnC,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;AAC3F,CAAC;AAED,SAAS,cAAc,CAAC,CAAa;IACnC,OAAO,EAAE,EAAE,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,UAAU,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC;AAClJ,CAAC;AAED,gDAAgD;AAChD,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,GAAG,IAAI,YAAY,EAAE,CAAC,CAAC;AACpE,CAAC;AAED,SAAS,YAAY,CAAC,CAAa;IACjC,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC;AAClI,CAAC;AAED,SAAS,MAAM;IACb,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,0FAA0F,EAAE,CAAC,CAAC;AAC3I,CAAC"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulated Vercel: project env vars and deployments.
|
|
3
|
+
*
|
|
4
|
+
* Assumptions about the real API that this fake encodes, each marked with how far it is checked. "Verified" means
|
|
5
|
+
* against Vercel's published OpenAPI document (https://openapi.vercel.sh/, fetched 2026-10-10); see
|
|
6
|
+
* docs/api-verification.md for the per-call table. A local fake cannot prove the rest; scenarios/contract.test.ts
|
|
7
|
+
* pins the critical ones against live accounts (`pnpm test:live`):
|
|
8
|
+
* V1. Env writes are visible to the next deployment immediately (no propagation delay). Lists are
|
|
9
|
+
* read-your-writes here; the real list may lag a write, so adapters trust write responses instead.
|
|
10
|
+
* Unverified: needs a live account.
|
|
11
|
+
* V2. GET /v7/deployments?projectId=&sha= filters by commit and returns newest first. The `sha` filter is
|
|
12
|
+
* verified (a v7 query parameter; v6 is not in the spec); items carry `readyState` (required) and `state`
|
|
13
|
+
* (optional), and `url` may be null until the upload completes (verified). The order is unverified: the
|
|
14
|
+
* adapter sorts by createdAt anyway.
|
|
15
|
+
* V3. GET /v10/projects/:id/env?decrypt=true returns plaintext with `decrypted: true`; without `decrypt`,
|
|
16
|
+
* `encrypted` values are opaque. `decrypt` (deprecated), `decrypted` and GET /v1/projects/:id/env/:id
|
|
17
|
+
* ("Retrieve the decrypted value") are verified; whether the deprecated parameter still decrypts is
|
|
18
|
+
* unverified, so the adapter falls back to the per-variable GET. `sensitive` values are never returned
|
|
19
|
+
* (the GET /v1 variant without `value`), so those always diff as `update`.
|
|
20
|
+
* V4. POST /v10/projects/:id/env?upsert=true answers 201 with `{ created, failed }` (verified: `upsert`, the
|
|
21
|
+
* status, `created` as one object or a list, `failed[].error` with `code`, `message`, `key`/`envVarKey`).
|
|
22
|
+
* Unverified: that `created` lists every entry written (new or updated, `createdAt === updatedAt` only for
|
|
23
|
+
* new ones), and that an entry whose key+gitBranch exists with an overlapping but different target set
|
|
24
|
+
* fails the whole request with 400 `ENV_CONFLICT`. Entries named in chaos `env_upsert_fail` come back under
|
|
25
|
+
* `failed` and are not written. Without `upsert` a duplicate answers 403 (verified: documented 403 reason).
|
|
26
|
+
* V5. POST /v13/deployments: `project` overrides `name` and a GitHub `gitSource` needs `ref` plus `repoId` (or
|
|
27
|
+
* `org` + `repo`) — both verified, as is `forceNew`. Deployments go QUEUED → BUILDING → READY over chaos
|
|
28
|
+
* `deploy_ms` (default 0: READY at once; the real API also passes through INITIALIZING, which the adapter
|
|
29
|
+
* handles as in progress). Every deployment has its own URL (verified: "the unique URL of the deployment").
|
|
30
|
+
* `deploy: cancel` cancels a branch's in-progress builds when a new one starts (unverified).
|
|
31
|
+
* V6. Pagination (chaos `page_size`): `{ envs, pagination: { count, next, prev } }` (verified: the spec's
|
|
32
|
+
* Pagination schema). The `?until=` page parameter is unverified for the env list, which documents no page
|
|
33
|
+
* parameter; the adapter drops repeated records, so an API that ignores it costs one extra request.
|
|
34
|
+
* V7. GET /v9/projects/:id carries `link`, the connected repository (`type`, `repoId`, …); verified.
|
|
35
|
+
* V8. GET /v7/deployments accepts `target` (`production` | `preview`) and each item's `target` is `production`,
|
|
36
|
+
* a custom environment name such as `staging`, or null for a preview; verified against the spec's
|
|
37
|
+
* `/v7/deployments` parameters and item schema. The git integration builds previews; production builds
|
|
38
|
+
* of a commit exist separately.
|
|
39
|
+
*/
|
|
40
|
+
import { type CreatedBy, type ProviderSim, type SimCore } from "../provider.js";
|
|
41
|
+
/** One simulated Vercel environment variable. */
|
|
42
|
+
export interface VercelEnv {
|
|
43
|
+
id: string;
|
|
44
|
+
key: string;
|
|
45
|
+
value: string;
|
|
46
|
+
target: string[];
|
|
47
|
+
type: "encrypted" | "plain";
|
|
48
|
+
gitBranch?: string;
|
|
49
|
+
createdAt: number;
|
|
50
|
+
updatedAt: number;
|
|
51
|
+
createdBy: CreatedBy;
|
|
52
|
+
}
|
|
53
|
+
/** Vercel's deployment states, as its API reports them. */
|
|
54
|
+
export type DeploymentState = "QUEUED" | "BUILDING" | "READY" | "ERROR" | "CANCELED";
|
|
55
|
+
/** One simulated deployment. */
|
|
56
|
+
export interface VercelDeployment {
|
|
57
|
+
uid: string;
|
|
58
|
+
url: string;
|
|
59
|
+
state: DeploymentState;
|
|
60
|
+
createdAt: number;
|
|
61
|
+
/** While in progress: QUEUED until `buildingAt`, BUILDING until `readyAt`, then `finalState`. */
|
|
62
|
+
buildingAt?: number;
|
|
63
|
+
readyAt?: number;
|
|
64
|
+
finalState?: DeploymentState;
|
|
65
|
+
meta: {
|
|
66
|
+
githubCommitSha: string;
|
|
67
|
+
githubCommitRef?: string;
|
|
68
|
+
};
|
|
69
|
+
/** `production` for production deployments, null for previews. */
|
|
70
|
+
target: "production" | null;
|
|
71
|
+
createdBy: CreatedBy;
|
|
72
|
+
}
|
|
73
|
+
/** The Git repository a project is connected to (`link` in GET /v9/projects/:id); null when it has none. */
|
|
74
|
+
export interface VercelLink {
|
|
75
|
+
type: "github";
|
|
76
|
+
repoId: number;
|
|
77
|
+
org: string;
|
|
78
|
+
repo: string;
|
|
79
|
+
productionBranch: string;
|
|
80
|
+
}
|
|
81
|
+
/** One simulated Vercel project: its variables, deployments and Git connection. */
|
|
82
|
+
export interface VercelProject {
|
|
83
|
+
envs: VercelEnv[];
|
|
84
|
+
deployments: VercelDeployment[];
|
|
85
|
+
link: VercelLink | null;
|
|
86
|
+
}
|
|
87
|
+
/** Simulated Vercel: projects by id. */
|
|
88
|
+
export interface VercelState {
|
|
89
|
+
projects: Record<string, VercelProject>;
|
|
90
|
+
}
|
|
91
|
+
/** Initial Vercel state. */
|
|
92
|
+
export interface VercelSeed {
|
|
93
|
+
projects: Record<string, {
|
|
94
|
+
envs: Array<{
|
|
95
|
+
key: string;
|
|
96
|
+
value: string;
|
|
97
|
+
target: string;
|
|
98
|
+
gitBranch?: string;
|
|
99
|
+
}>;
|
|
100
|
+
}>;
|
|
101
|
+
}
|
|
102
|
+
/** Simulated Vercel (env vars, deployments); see the assumptions at the top of this file. */
|
|
103
|
+
export declare const vercelSim: ProviderSim<VercelState, VercelSeed>;
|
|
104
|
+
/**
|
|
105
|
+
* Every deployment gets its own URL: the first one of a sha is `<project>-<sha8>.vercel.app`, later ones
|
|
106
|
+
* (redeploys) `-2`, `-3`, … `buildMs` > 0 starts it QUEUED and makes it reach `state` over that time.
|
|
107
|
+
*/
|
|
108
|
+
export declare function createDeployment(core: SimCore, projectId: string, project: VercelProject, sha: string, state: DeploymentState, createdAt: number, opts?: {
|
|
109
|
+
ref?: string;
|
|
110
|
+
buildMs?: number;
|
|
111
|
+
target?: "production";
|
|
112
|
+
}): VercelDeployment;
|
|
113
|
+
/** Advance in-progress deployments to the state their timings say they are in now. */
|
|
114
|
+
export declare function refreshDeployments(list: VercelDeployment[], now?: number): void;
|