@sponson/sim 0.4.0 → 0.6.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.
Files changed (69) hide show
  1. package/README.md +5 -0
  2. package/dist/auth.d.ts +60 -0
  3. package/dist/auth.js +195 -0
  4. package/dist/auth.js.map +1 -0
  5. package/dist/bin.js +9 -14
  6. package/dist/bin.js.map +1 -1
  7. package/dist/chaos.d.ts +4 -0
  8. package/dist/chaos.js +2 -0
  9. package/dist/chaos.js.map +1 -1
  10. package/dist/cli.d.ts +29 -0
  11. package/dist/cli.js +99 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/demo.d.ts +39 -0
  14. package/dist/demo.js +168 -0
  15. package/dist/demo.js.map +1 -0
  16. package/dist/graphql.d.ts +22 -0
  17. package/dist/graphql.js +36 -0
  18. package/dist/graphql.js.map +1 -0
  19. package/dist/index.d.ts +13 -0
  20. package/dist/index.js +14 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/provider.d.ts +43 -4
  23. package/dist/provider.js +12 -6
  24. package/dist/provider.js.map +1 -1
  25. package/dist/routes/algolia.d.ts +37 -0
  26. package/dist/routes/algolia.js +83 -0
  27. package/dist/routes/algolia.js.map +1 -0
  28. package/dist/routes/aws.d.ts +43 -0
  29. package/dist/routes/aws.js +211 -0
  30. package/dist/routes/aws.js.map +1 -0
  31. package/dist/routes/cloud_run.d.ts +131 -0
  32. package/dist/routes/cloud_run.js +378 -0
  33. package/dist/routes/cloud_run.js.map +1 -0
  34. package/dist/routes/cloudflare.d.ts +80 -0
  35. package/dist/routes/cloudflare.js +176 -0
  36. package/dist/routes/cloudflare.js.map +1 -0
  37. package/dist/routes/digitalocean.d.ts +58 -0
  38. package/dist/routes/digitalocean.js +158 -0
  39. package/dist/routes/digitalocean.js.map +1 -0
  40. package/dist/routes/fly.d.ts +22 -0
  41. package/dist/routes/fly.js +113 -0
  42. package/dist/routes/fly.js.map +1 -0
  43. package/dist/routes/launchdarkly.d.ts +98 -0
  44. package/dist/routes/launchdarkly.js +213 -0
  45. package/dist/routes/launchdarkly.js.map +1 -0
  46. package/dist/routes/netlify.d.ts +126 -0
  47. package/dist/routes/netlify.js +347 -0
  48. package/dist/routes/netlify.js.map +1 -0
  49. package/dist/routes/planetscale.d.ts +95 -0
  50. package/dist/routes/planetscale.js +234 -0
  51. package/dist/routes/planetscale.js.map +1 -0
  52. package/dist/routes/railway.d.ts +24 -0
  53. package/dist/routes/railway.js +112 -0
  54. package/dist/routes/railway.js.map +1 -0
  55. package/dist/routes/rest.d.ts +138 -0
  56. package/dist/routes/rest.js +462 -0
  57. package/dist/routes/rest.js.map +1 -0
  58. package/dist/routes/supabase.d.ts +103 -0
  59. package/dist/routes/supabase.js +234 -0
  60. package/dist/routes/supabase.js.map +1 -0
  61. package/dist/routes/xata.d.ts +57 -0
  62. package/dist/routes/xata.js +115 -0
  63. package/dist/routes/xata.js.map +1 -0
  64. package/dist/server.js +71 -12
  65. package/dist/server.js.map +1 -1
  66. package/dist/state.d.ts +13 -0
  67. package/dist/state.js +14 -1
  68. package/dist/state.js.map +1 -1
  69. package/package.json +5 -1
package/dist/demo.js ADDED
@@ -0,0 +1,168 @@
1
+ /**
2
+ * The zero-account demo behind `sponson-sim --demo <dir>`: a git repository holding a plan wired to the projects
3
+ * the sim seeds by default, an env file that points `sponson` at the running sim, and the commands to run next.
4
+ * The README's "Try it in 60 seconds" block is these commands verbatim; `scenarios/docs/try-it.test.ts` runs that
5
+ * block and checks it against what `demoSteps` prints, so the two cannot drift apart.
6
+ */
7
+ import { execFile } from "node:child_process";
8
+ import { mkdir, readdir, writeFile } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import { promisify } from "node:util";
11
+ import { simEnv } from "./index.js";
12
+ const exec = promisify(execFile);
13
+ /** The branch the demo commit is on; Vercel variables are written for it. */
14
+ export const DEMO_BRANCH = "feat/checkout";
15
+ /** The pull request the demo pretends to be, so the scope reads `pr-42` as in the README. */
16
+ export const DEMO_PR = "42";
17
+ /** The file `source` loads in the second terminal. */
18
+ export const DEMO_ENV_FILE = "sim.env";
19
+ /**
20
+ * Comments carry the explanation, because a newcomer reads this file before running anything. It uses only the
21
+ * projects every provider's default seed creates (`prj_demo`, `proj_demo`; Clerk needs none).
22
+ */
23
+ export const DEMO_PLAN = `# A release plan for a preview of this branch, against the fake cloud started by \`sponson-sim --demo\`.
24
+ version: 1
25
+ # Receipts (what each run did) go to .sponson/receipts in this directory: the demo has no git remote.
26
+ receipts: local
27
+ providers:
28
+ vercel: { project: prj_demo }
29
+ neon: { project: proj_demo }
30
+
31
+ changes:
32
+ # 1. A Neon database branch for this preview, forked from main.
33
+ - id: db
34
+ adapter: neon
35
+ op: branch
36
+ parent: main
37
+ environments: [preview]
38
+
39
+ # 2. The branch's connection string as the preview's DATABASE_URL. A reference, never a value: nobody copies
40
+ # a password between consoles, and it is masked in every line Sponson prints.
41
+ - id: env
42
+ adapter: vercel
43
+ op: env
44
+ target: preview
45
+ values:
46
+ DATABASE_URL: { from: db.connection_string }
47
+ environments: [preview]
48
+
49
+ # 3. The preview's URL as an allowed Clerk redirect. The URL exists only once Vercel has deployed this commit,
50
+ # so this line waits for the deploy.
51
+ - id: callback
52
+ adapter: clerk
53
+ op: redirect_allow
54
+ url: { from: env.preview_url }
55
+ environments: [preview]
56
+ `;
57
+ /** A console edit, as the sim's drift control applies it: someone pastes another database into DATABASE_URL. */
58
+ export const DEMO_DRIFT = { "vercel.env.preview.DATABASE_URL": "postgres://typed-in-the-console" };
59
+ /**
60
+ * What to run in the second terminal, in order. No inline comments: interactive zsh (macOS's default shell)
61
+ * treats `#` as an argument unless `interactivecomments` is set, so a pasted comment would break the command.
62
+ */
63
+ export function demoSteps(dir) {
64
+ return [
65
+ `cd ${shellQuote(dir)} && source ${DEMO_ENV_FILE}`,
66
+ "sponson plan",
67
+ "sponson apply",
68
+ "sponson apply",
69
+ `curl -s -o /dev/null "$SPONSON_SIM_URL/_chaos" -d '${JSON.stringify({ drift: DEMO_DRIFT })}'`,
70
+ "sponson plan",
71
+ "sponson apply --destroy",
72
+ ];
73
+ }
74
+ /** One line per step of `demoSteps`, for the printed walkthrough. */
75
+ const STEP_NOTES = [
76
+ "point sponson at the fake cloud (placeholder tokens, no accounts)",
77
+ "the diff: a branch to create, a variable and a callback waiting on it",
78
+ "creates the branch, injects DATABASE_URL by reference, finds the preview deploy, allows its URL",
79
+ "again: every line unchanged, zero writes",
80
+ "play a human editing DATABASE_URL in the Vercel console",
81
+ "the drift is caught: that line is blocked until you `sponson apply --reconcile`",
82
+ "removes everything this scope created, in reverse order",
83
+ ];
84
+ /**
85
+ * The env file: the sim's URLs and placeholder tokens (`simEnv`), `SPONSON_SIM_URL` for the drift command, and the
86
+ * pull request number. Setting `SPONSON_CTX_PR` also keeps `sponson` from asking `gh` which pull request this is,
87
+ * which in a repository without a remote only costs time. Without a global install, `sponson` falls back to npx.
88
+ */
89
+ export function demoEnvFile(url) {
90
+ const lines = [
91
+ "# Points sponson at the fake cloud started by `sponson-sim --demo`. Load it with: source sim.env",
92
+ ...envExports(url),
93
+ "# The demo pretends this branch is pull request #42 (scope pr-42).",
94
+ `export SPONSON_CTX_PR=${DEMO_PR}`,
95
+ "# Not installed globally? Run the published CLI through npx.",
96
+ 'command -v sponson >/dev/null 2>&1 || sponson() { npx --yes sponson "$@"; }',
97
+ ];
98
+ return `${lines.join("\n")}\n`;
99
+ }
100
+ /** `export NAME=value` for everything a shell needs to point `sponson` at the sim at `url`. */
101
+ export function envExports(url) {
102
+ return [`export SPONSON_SIM_URL=${url}`, ...Object.entries(simEnv(url)).map(([k, v]) => `export ${k}=${v}`)];
103
+ }
104
+ /**
105
+ * Write the demo into `dir`: the plan, a .gitignore, the env file, and one commit on `DEMO_BRANCH`. The commit's
106
+ * author, dates and contents are fixed, so its sha (and with it the preview URL the sim derives from it) is the
107
+ * same on every machine. The env file is ignored: it holds the port, which differs between runs.
108
+ * Refuses a non-empty directory rather than mixing the demo into someone's files.
109
+ */
110
+ export async function writeDemo(dir, url) {
111
+ const existing = await readdir(dir).catch((e) => {
112
+ if (e.code === "ENOENT")
113
+ return [];
114
+ throw e;
115
+ });
116
+ if (existing.length > 0)
117
+ throw new Error(`${dir} is not empty; pass a new directory to --demo (or delete this one)`);
118
+ await mkdir(dir, { recursive: true });
119
+ await writeFile(join(dir, "release.plan.yaml"), DEMO_PLAN);
120
+ await writeFile(join(dir, ".gitignore"), `.sponson/\n${DEMO_ENV_FILE}\n`);
121
+ await writeFile(join(dir, DEMO_ENV_FILE), demoEnvFile(url));
122
+ await git(dir, ["init", "-q"]);
123
+ await git(dir, ["checkout", "-q", "-b", DEMO_BRANCH]);
124
+ await git(dir, ["add", "release.plan.yaml", ".gitignore"]);
125
+ await git(dir, ["commit", "-q", "--no-verify", "-m", "Add the Sponson demo plan"]);
126
+ }
127
+ /** The walkthrough printed once the sim is up and the demo is written. */
128
+ export function demoInstructions(dir, url) {
129
+ const steps = demoSteps(dir);
130
+ return [
131
+ `Fake cloud (Vercel, Neon and Clerk APIs) listening on ${url}. State lives in memory; Ctrl-C stops it.`,
132
+ `Demo repository written to ${dir}: release.plan.yaml creates a Neon branch, puts its connection string`,
133
+ "into Vercel's DATABASE_URL by reference, waits for the preview deploy, then allows its URL in Clerk.",
134
+ "",
135
+ "In a second terminal, run these one at a time:",
136
+ "",
137
+ ...steps.map((s) => ` ${s}`),
138
+ "",
139
+ "What each one does:",
140
+ "",
141
+ ...STEP_NOTES.map((note, i) => ` ${String(i + 1)}. ${note}`),
142
+ "",
143
+ ].join("\n");
144
+ }
145
+ /**
146
+ * Git with the demo's fixed identity. Inherited GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE would point it at
147
+ * another repository (they are set inside git hooks), and a user's signing or hook settings would change the
148
+ * commit, so both are overridden.
149
+ */
150
+ async function git(cwd, args) {
151
+ const { GIT_DIR: _d, GIT_WORK_TREE: _w, GIT_INDEX_FILE: _i, ...env } = process.env;
152
+ const fixed = { GIT_AUTHOR_NAME: "Sponson demo", GIT_AUTHOR_EMAIL: "demo@sponson.invalid", GIT_COMMITTER_NAME: "Sponson demo", GIT_COMMITTER_EMAIL: "demo@sponson.invalid", GIT_AUTHOR_DATE: "2026-01-01T00:00:00Z", GIT_COMMITTER_DATE: "2026-01-01T00:00:00Z" };
153
+ const config = ["-c", "commit.gpgsign=false", "-c", "core.autocrlf=false", "-c", "init.defaultBranch=main"];
154
+ try {
155
+ await exec("git", [...config, ...args], { cwd, env: { ...env, ...fixed } });
156
+ }
157
+ catch (e) {
158
+ const err = e;
159
+ if (err.code === "ENOENT")
160
+ throw new Error("the demo needs git on PATH (Sponson reads the branch and commit from it)");
161
+ throw new Error(`git ${args.join(" ")} failed: ${(err.stderr ?? err.message).trim()}`);
162
+ }
163
+ }
164
+ /** Quote a path for a POSIX shell only when it needs it, so the common case reads naturally. */
165
+ export function shellQuote(s) {
166
+ return /^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replaceAll("'", `'\\''`)}'`;
167
+ }
168
+ //# sourceMappingURL=demo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"demo.js","sourceRoot":"","sources":["../src/demo.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * What the GraphQL sims (routes/railway.ts, routes/fly.ts) share: reading an operation's kind and root field from a
3
+ * request body, and answering in the GraphQL envelope. They do not parse GraphQL: a simulated schema is a table of
4
+ * root fields, each resolved from the request's variables, so a plan line must pass its inputs as variables (as the
5
+ * official clients do), not inline.
6
+ */
7
+ import { Reply } from "./provider.js";
8
+ /** One GraphQL request as the sims see it. */
9
+ export interface GraphqlOperation {
10
+ kind: "query" | "mutation";
11
+ /** The first selected top-level field (after an alias). */
12
+ root: string;
13
+ variables: Record<string, unknown>;
14
+ }
15
+ /** The operation of a request body; a 400 GRAPHQL_PARSE_FAILED answer when the body is not one. */
16
+ export declare function graphqlOperation(body: unknown): GraphqlOperation | Reply;
17
+ /** A successful answer: `{ data }`. */
18
+ export declare function graphqlData(data: unknown): Reply;
19
+ /** A failed answer: `{ errors: [{ message, extensions: { code } }], data: null }`, HTTP 200 unless said otherwise. */
20
+ export declare function graphqlError(message: string, code: string, status?: number): Reply;
21
+ /** True when a request body is a mutation: only those change state (the write log). */
22
+ export declare function isMutation(body: unknown): boolean;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * What the GraphQL sims (routes/railway.ts, routes/fly.ts) share: reading an operation's kind and root field from a
3
+ * request body, and answering in the GraphQL envelope. They do not parse GraphQL: a simulated schema is a table of
4
+ * root fields, each resolved from the request's variables, so a plan line must pass its inputs as variables (as the
5
+ * official clients do), not inline.
6
+ */
7
+ import { Reply } from "./provider.js";
8
+ /** The operation of a request body; a 400 GRAPHQL_PARSE_FAILED answer when the body is not one. */
9
+ export function graphqlOperation(body) {
10
+ if (typeof body !== "object" || body === null)
11
+ return graphqlError("request body must be a JSON object { query, variables }", "GRAPHQL_PARSE_FAILED", 400);
12
+ const { query, variables } = body;
13
+ if (typeof query !== "string")
14
+ return graphqlError("`query` is required", "GRAPHQL_PARSE_FAILED", 400);
15
+ const text = query.replace(/#[^\n]*/g, "").trim();
16
+ const kind = /^mutation\b/.test(text) ? "mutation" : "query";
17
+ const m = /^\s*([_A-Za-z]\w*)(\s*:\s*([_A-Za-z]\w*))?/.exec(text.slice(text.indexOf("{") + 1));
18
+ if (!m)
19
+ return graphqlError("no field selected", "GRAPHQL_PARSE_FAILED", 400);
20
+ const vars = typeof variables === "object" && variables !== null && !Array.isArray(variables) ? variables : {};
21
+ return { kind, root: m[3] ?? m[1], variables: vars };
22
+ }
23
+ /** A successful answer: `{ data }`. */
24
+ export function graphqlData(data) {
25
+ return new Reply(200, { data });
26
+ }
27
+ /** A failed answer: `{ errors: [{ message, extensions: { code } }], data: null }`, HTTP 200 unless said otherwise. */
28
+ export function graphqlError(message, code, status = 200) {
29
+ return new Reply(status, { errors: [{ message, extensions: { code } }], data: null });
30
+ }
31
+ /** True when a request body is a mutation: only those change state (the write log). */
32
+ export function isMutation(body) {
33
+ const op = graphqlOperation(body);
34
+ return !(op instanceof Reply) && op.kind === "mutation";
35
+ }
36
+ //# sourceMappingURL=graphql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphql.js","sourceRoot":"","sources":["../src/graphql.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAUtC,mGAAmG;AACnG,MAAM,UAAU,gBAAgB,CAAC,IAAa;IAC5C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,YAAY,CAAC,yDAAyD,EAAE,sBAAsB,EAAE,GAAG,CAAC,CAAC;IAC3J,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,IAA+B,CAAC;IAC7D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,YAAY,CAAC,qBAAqB,EAAE,sBAAsB,EAAE,GAAG,CAAC,CAAC;IACvG,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAClD,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC;IAC7D,MAAM,CAAC,GAAG,4CAA4C,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC/F,IAAI,CAAC,CAAC;QAAE,OAAO,YAAY,CAAC,mBAAmB,EAAE,sBAAsB,EAAE,GAAG,CAAC,CAAC;IAC9E,MAAM,IAAI,GAAG,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAE,SAAqC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5I,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AACxD,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,WAAW,CAAC,IAAa;IACvC,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;AAClC,CAAC;AAED,sHAAsH;AACtH,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,IAAY,EAAE,MAAM,GAAG,GAAG;IACtE,OAAO,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AACxF,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,UAAU,CAAC,IAAa;IACtC,MAAM,EAAE,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAClC,OAAO,CAAC,CAAC,EAAE,YAAY,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,KAAK,UAAU,CAAC;AAC1D,CAAC"}
package/dist/index.d.ts CHANGED
@@ -5,6 +5,19 @@ export * from "./state.js";
5
5
  export * from "./routes/vercel.js";
6
6
  export * from "./routes/neon.js";
7
7
  export * from "./routes/clerk.js";
8
+ export * from "./routes/netlify.js";
9
+ export * from "./routes/planetscale.js";
10
+ export * from "./routes/launchdarkly.js";
11
+ export * from "./routes/rest.js";
12
+ export * from "./routes/supabase.js";
13
+ export * from "./routes/aws.js";
14
+ export * from "./routes/railway.js";
15
+ export * from "./routes/fly.js";
16
+ export * from "./routes/cloudflare.js";
17
+ export * from "./routes/algolia.js";
18
+ export * from "./routes/xata.js";
19
+ export * from "./routes/digitalocean.js";
20
+ export * from "./routes/cloud_run.js";
8
21
  export { createSimServer } from "./server.js";
9
22
  /**
10
23
  * A running sim: its base URL (`simEnv` turns it into adapter environment), its live state for seeding and
package/dist/index.js CHANGED
@@ -6,6 +6,19 @@ export * from "./state.js";
6
6
  export * from "./routes/vercel.js";
7
7
  export * from "./routes/neon.js";
8
8
  export * from "./routes/clerk.js";
9
+ export * from "./routes/netlify.js";
10
+ export * from "./routes/planetscale.js";
11
+ export * from "./routes/launchdarkly.js";
12
+ export * from "./routes/rest.js";
13
+ export * from "./routes/supabase.js";
14
+ export * from "./routes/aws.js";
15
+ export * from "./routes/railway.js";
16
+ export * from "./routes/fly.js";
17
+ export * from "./routes/cloudflare.js";
18
+ export * from "./routes/algolia.js";
19
+ export * from "./routes/xata.js";
20
+ export * from "./routes/digitalocean.js";
21
+ export * from "./routes/cloud_run.js";
9
22
  export { createSimServer } from "./server.js";
10
23
  /** Start the fake cloud on 127.0.0.1. Port 0 picks a free one, which is what tests want. */
11
24
  export async function startSim(opts = {}) {
@@ -27,7 +40,7 @@ export async function startSim(opts = {}) {
27
40
  };
28
41
  }
29
42
  /** A placeholder credential per provider (`VERCEL_TOKEN: "tok_vercel"`, …). The sim accepts any non-empty token. */
30
- export const SIM_TOKENS = Object.fromEntries(providerEntries().map(([, p]) => [p.env.token, p.env.testToken]));
43
+ export const SIM_TOKENS = Object.fromEntries(providerEntries().flatMap(([, p]) => [[p.env.token, p.env.testToken], ...Object.entries(p.env.more ?? {})]));
31
44
  /**
32
45
  * The env that points every provider's adapter at a sim (or at a proxy in front of one): each `*_API_URL`
33
46
  * and, unless `tokens: false`, the placeholder credentials of SIM_TOKENS.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAgB,MAAM,YAAY,CAAC;AAErE,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAa9C,4FAA4F;AAC5F,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,OAAmD,EAAE;IAClF,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IACtC,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,GAAI,MAAM,CAAC,OAAO,EAAkB,CAAC,IAAI,CAAC;IACpD,OAAO;QACL,GAAG,EAAE,oBAAoB,IAAI,EAAE;QAC/B,IAAI;QACJ,KAAK;QACL,KAAK,EAAE,GAAG,EAAE,CACV,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACpC,MAAM,CAAC,mBAAmB,EAAE,CAAC;YAC7B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QACnD,CAAC,CAAC;KACL,CAAC;AACJ,CAAC;AAED,oHAAoH;AACpH,MAAM,CAAC,MAAM,UAAU,GAAqC,MAAM,CAAC,WAAW,CAAC,eAAe,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;AAEjJ;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,GAA6B,EAAE,OAA6B,EAAE;IACnF,MAAM,IAAI,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IACrD,MAAM,GAAG,GAA2B,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,UAAU,EAAE,CAAC;IACnF,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,eAAe,EAAE;QAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC;IAC9E,OAAO,GAAG,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@
3
3
  * A provider lives in one `routes/<name>.ts` file: its state, its seed, its drift keys and its HTTP routes.
4
4
  * Adding a provider = writing that file and registering it in `PROVIDERS` (state.ts).
5
5
  */
6
+ import type { IncomingHttpHeaders } from "node:http";
6
7
  import type { ChaosConfig } from "./chaos.js";
7
8
  /** Who made a record: the sim itself (seed, drift, a "human in the console") or a client through the API. */
8
9
  export type CreatedBy = "sim" | "api";
@@ -26,6 +27,19 @@ export interface RouteRequest {
26
27
  /** The path with the provider prefix stripped, e.g. `/v10/projects/prj_demo/env`. */
27
28
  path: string;
28
29
  body: unknown;
30
+ /** Request headers, lower-cased names (Node's `IncomingMessage.headers`, multi-valued ones joined). */
31
+ headers?: Record<string, string>;
32
+ }
33
+ /**
34
+ * A request as a provider's credential check sees it: what a signature (SigV4) or a Digest answer covers. `raw` is
35
+ * the body exactly as received.
36
+ */
37
+ export interface AuthRequest {
38
+ method: string;
39
+ url: URL;
40
+ /** The request target as received (path and query, still percent-encoded). */
41
+ target: string;
42
+ raw: string;
29
43
  }
30
44
  /** One `<provider>.<rest>` drift key, as `ProviderSim.drift` receives it: a change "a human made in the console". */
31
45
  export interface DriftRequest {
@@ -45,11 +59,15 @@ export interface DriftRequest {
45
59
  * registry only knows `ProviderSim<unknown, unknown>`.
46
60
  */
47
61
  export interface ProviderSim<S, Seed> {
48
- /** Env var names the adapter reads its credential and base URL from, and the token the sim hands out. */
62
+ /**
63
+ * Env var names the adapter reads its credential and base URL from, and the token the sim hands out; `more`:
64
+ * further credential variables and their placeholder values, for a credential made of two parts.
65
+ */
49
66
  env: {
50
67
  token: string;
51
68
  url: string;
52
69
  testToken: string;
70
+ more?: Record<string, string>;
53
71
  };
54
72
  /** Seed used when a reset does not name this provider. */
55
73
  defaultSeed: Seed;
@@ -57,6 +75,24 @@ export interface ProviderSim<S, Seed> {
57
75
  reset(core: SimCore, seed: Seed | undefined): S;
58
76
  /** Apply one drift key to the state; false when the key is not one this provider knows. */
59
77
  drift(core: SimCore, state: S, d: DriftRequest): boolean;
78
+ /**
79
+ * Whether a request carries a credential this provider accepts. Absent: any non-empty bearer token. Like the
80
+ * default, accept any non-empty value unless the provider's seed configures real credentials: the point is that a
81
+ * missing credential surfaces as 401. `state` is the provider's current state, for a provider whose seed declares
82
+ * how credentials arrive; `req` what a signature (SigV4) or a Digest answer covers. A `Reply` answers the request
83
+ * instead (a 401 with a Digest challenge).
84
+ */
85
+ authorized?(headers: IncomingHttpHeaders, state: S, req: AuthRequest, core: SimCore): boolean | Reply;
86
+ /**
87
+ * Whether a POST, PUT, PATCH or DELETE changes state, for the write log (I4: `plan` writes nothing). Absent: every
88
+ * one does. A token endpoint, a GraphQL query or a read sent as a POST (an ARM list action) is a POST that does
89
+ * not. `state` is the provider's current state, for a provider whose seed declares such reads.
90
+ */
91
+ isWrite?(req: {
92
+ method: string;
93
+ path: string;
94
+ body: unknown;
95
+ }, state: S): boolean;
60
96
  /** Answer one authenticated, chaos-checked request under `/<name>/`. */
61
97
  routes(core: SimCore, state: S, req: RouteRequest): Reply;
62
98
  }
@@ -66,7 +102,7 @@ export declare function page<T>(core: SimCore, list: T[], cursor: string | null,
66
102
  next: number | null;
67
103
  };
68
104
  /** The `:name` segments of a route path, e.g. `"project" | "id"` for `/v9/projects/:project/env/:id`. */
69
- type ParamNames<P extends string> = P extends `${string}:${infer N}/${infer Rest}` ? N | ParamNames<`/${Rest}`> : P extends `${string}:${infer N}` ? N : never;
105
+ type ParamNames<P extends string> = P extends `${string}:${infer N}/${infer Rest}` ? N | ParamNames<`/${Rest}`> : P extends `${string}:${infer N}` ? N : P extends `${string}*${infer N}` ? N : never;
70
106
  /** The decoded `:name` segments of a matched path, typed from the route's path literal. */
71
107
  export type PathParams<P extends string> = {
72
108
  [K in ParamNames<P>]: string;
@@ -78,6 +114,8 @@ export interface RouteContext<S, Params = Record<string, string>> {
78
114
  params: Params;
79
115
  url: URL;
80
116
  body: unknown;
117
+ /** Request headers, lower-cased names; empty when the caller passed none. */
118
+ headers: Record<string, string>;
81
119
  }
82
120
  /** One route of a simulated provider. Build it with `route`; serve a list of them with `router`. */
83
121
  export interface Route<S> {
@@ -86,8 +124,9 @@ export interface Route<S> {
86
124
  handle(ctx: RouteContext<S>): Reply;
87
125
  }
88
126
  /**
89
- * A route: an HTTP method, a path whose `:name` segments match one non-empty path segment each, and the handler
90
- * that answers it. `params` is typed from the path: `route("GET", "/projects/:project", ({ params }) => …)` gets
127
+ * A route: an HTTP method, a path whose `:name` segments match one non-empty path segment each (and whose last
128
+ * segment may be `*name`, matching the rest of the path, slashes included, undecoded), and the handler that
129
+ * answers it. `params` is typed from the path: `route("GET", "/projects/:project", ({ params }) => …)` gets
91
130
  * `params.project: string`.
92
131
  */
93
132
  export declare function route<S, P extends string>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", path: P, handle: (ctx: RouteContext<S, PathParams<P>>) => Reply): Route<S>;
package/dist/provider.js CHANGED
@@ -20,8 +20,9 @@ export function page(core, list, cursor, limit) {
20
20
  return { items, next: start + size < list.length ? start + size : null };
21
21
  }
22
22
  /**
23
- * A route: an HTTP method, a path whose `:name` segments match one non-empty path segment each, and the handler
24
- * that answers it. `params` is typed from the path: `route("GET", "/projects/:project", ({ params }) => …)` gets
23
+ * A route: an HTTP method, a path whose `:name` segments match one non-empty path segment each (and whose last
24
+ * segment may be `*name`, matching the rest of the path, slashes included, undecoded), and the handler that
25
+ * answers it. `params` is typed from the path: `route("GET", "/projects/:project", ({ params }) => …)` gets
25
26
  * `params.project: string`.
26
27
  */
27
28
  export function route(method, path, handle) {
@@ -38,23 +39,28 @@ export function router(routes, fallback) {
38
39
  const source = r.path
39
40
  .split("/")
40
41
  .map((seg) => {
42
+ if (seg.startsWith("*")) {
43
+ names.push(seg.slice(1));
44
+ return "(.+)";
45
+ }
41
46
  if (!seg.startsWith(":"))
42
47
  return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43
48
  names.push(seg.slice(1));
44
49
  return "([^/]+)";
45
50
  })
46
51
  .join("/");
47
- return { route: r, pattern: new RegExp(`^${source}$`), names };
52
+ return { route: r, pattern: new RegExp(`^${source}$`), names, rest: r.path.split("/").some((s) => s.startsWith("*")) };
48
53
  });
49
54
  return (core, state, req) => {
50
- for (const { route: r, pattern, names } of compiled) {
55
+ for (const { route: r, pattern, names, rest } of compiled) {
51
56
  if (r.method !== req.method)
52
57
  continue;
53
58
  const m = pattern.exec(req.path);
54
59
  if (!m)
55
60
  continue;
56
- const params = Object.fromEntries(names.map((n, i) => [n, decodeURIComponent(m[i + 1])]));
57
- return r.handle({ core, state, params, url: req.url, body: req.body });
61
+ // A `*name` segment (always the last) is passed as matched: its own segments decode separately.
62
+ const params = Object.fromEntries(names.map((n, i) => [n, rest && i === names.length - 1 ? m[i + 1] : decodeURIComponent(m[i + 1])]));
63
+ return r.handle({ core, state, params, url: req.url, body: req.body, headers: req.headers ?? {} });
58
64
  }
59
65
  return fallback();
60
66
  };
@@ -1 +1 @@
1
- {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { type CreatedBy, type ProviderSim } from "../provider.js";
2
+ /** An index as the simulated Algolia stores it. */
3
+ export interface AlgoliaIndex {
4
+ /** Number of records. */
5
+ entries: number;
6
+ settings: Record<string, unknown>;
7
+ createdBy: CreatedBy;
8
+ }
9
+ /** A queued task: the copy it performs when published, and how many polls answer `notPublished` first. */
10
+ interface AlgoliaTask {
11
+ index: string;
12
+ copy?: {
13
+ from: string;
14
+ scope?: string[];
15
+ };
16
+ pending: number;
17
+ }
18
+ /** The simulated Algolia's state: one application's indices and its tasks. */
19
+ export interface AlgoliaState {
20
+ indices: Record<string, AlgoliaIndex>;
21
+ tasks: Record<string, AlgoliaTask>;
22
+ /** How many polls of a new task answer `notPublished` before it is published (default 1). */
23
+ taskPolls: number;
24
+ }
25
+ /** What a scenario or test seeds the simulated Algolia with (`seed: { algolia: … }`). */
26
+ export interface AlgoliaSeed {
27
+ /** Indices that exist already (created "by a human", so unmanaged). */
28
+ indices?: Record<string, {
29
+ entries?: number;
30
+ settings?: Record<string, unknown>;
31
+ }>;
32
+ /** Polls of a new task that answer `notPublished` before it is published. */
33
+ taskPolls?: number;
34
+ }
35
+ /** The simulated Algolia API, registered in `PROVIDERS` (packages/sim/src/state.ts) and served under `/algolia`. */
36
+ export declare const algoliaSim: ProviderSim<AlgoliaState, AlgoliaSeed>;
37
+ export {};
@@ -0,0 +1,83 @@
1
+ import { Reply, route, router } from "../provider.js";
2
+ /** The simulated Algolia API, registered in `PROVIDERS` (packages/sim/src/state.ts) and served under `/algolia`. */
3
+ export const algoliaSim = {
4
+ env: { token: "ALGOLIA_API_KEY", url: "ALGOLIA_API_URL", testToken: "tok_algolia" },
5
+ defaultSeed: { indices: { products: { entries: 3, settings: { searchableAttributes: ["name"] } } } },
6
+ reset(_core, seed) {
7
+ const indices = {};
8
+ for (const [name, i] of Object.entries(seed?.indices ?? {}))
9
+ indices[name] = { entries: i.entries ?? 0, settings: structuredClone(i.settings ?? {}), createdBy: "sim" };
10
+ return { indices, tasks: {}, taskPolls: seed?.taskPolls ?? 1 };
11
+ },
12
+ /** `index.<name>`: "delete" — what a human in the Algolia dashboard would do. */
13
+ drift(_core, state, { key, rest, value, only }) {
14
+ if (only !== undefined || !rest.startsWith("index."))
15
+ return false;
16
+ const name = rest.slice("index.".length);
17
+ if (!state.indices[name])
18
+ throw new Error(`drift ${key}: no index ${name}`);
19
+ if (value !== "delete")
20
+ throw new Error(`drift ${key}: only "delete" is supported`);
21
+ delete state.indices[name];
22
+ return true;
23
+ },
24
+ /** AL2: both headers, the key non-empty. */
25
+ authorized(headers) {
26
+ const key = headers["x-algolia-api-key"];
27
+ const app = headers["x-algolia-application-id"];
28
+ return typeof key === "string" && key !== "" && typeof app === "string" && app !== "";
29
+ },
30
+ routes(core, state, req) {
31
+ return routes(core, state, req);
32
+ },
33
+ };
34
+ function task(core, state, t) {
35
+ const id = Number(core.nextId("").replace(/\D/g, "")) + 1000;
36
+ state.tasks[String(id)] = { ...t, pending: state.taskPolls };
37
+ return id;
38
+ }
39
+ /** The copy a task performs when it is published (AL3, AL5). */
40
+ function publish(state, t) {
41
+ if (!t.copy)
42
+ return;
43
+ const from = state.indices[t.copy.from];
44
+ const before = state.indices[t.index];
45
+ const all = t.copy.scope === undefined;
46
+ state.indices[t.index] = {
47
+ entries: all ? (from?.entries ?? 0) : (before?.entries ?? 0),
48
+ settings: all || t.copy.scope.includes("settings") ? structuredClone(from?.settings ?? {}) : (before?.settings ?? {}),
49
+ createdBy: "api",
50
+ };
51
+ delete t.copy;
52
+ }
53
+ const routes = router([
54
+ route("GET", "/1/indexes", ({ state }) => new Reply(200, { items: Object.entries(state.indices).map(([name, i]) => ({ name, entries: i.entries })), nbPages: 1 })),
55
+ route("POST", "/1/indexes/:name/operation", ({ core, state, params, body }) => {
56
+ const b = (body ?? {});
57
+ if (b.operation !== "copy" || typeof b.destination !== "string" || b.destination === "")
58
+ return new Reply(400, { message: "operation must be copy with a destination" });
59
+ const scope = Array.isArray(b.scope) ? b.scope : undefined;
60
+ const id = task(core, state, { index: b.destination, copy: { from: params.name, ...(scope ? { scope } : {}) } });
61
+ return new Reply(200, { taskID: id, updatedAt: new Date().toISOString() });
62
+ }),
63
+ route("GET", "/1/indexes/:name/task/:task", ({ state, params }) => {
64
+ const t = state.tasks[params.task];
65
+ if (!t || t.index !== params.name)
66
+ return new Reply(404, { message: "Task does not exist" });
67
+ if (t.pending > 0) {
68
+ t.pending--;
69
+ return new Reply(200, { status: "notPublished" });
70
+ }
71
+ publish(state, t);
72
+ return new Reply(200, { status: "published" });
73
+ }),
74
+ route("GET", "/1/indexes/:name/settings", ({ state, params }) => {
75
+ const i = state.indices[params.name];
76
+ return i ? new Reply(200, i.settings) : new Reply(404, { message: "Index does not exist", status: 404 });
77
+ }),
78
+ route("DELETE", "/1/indexes/:name", ({ core, state, params }) => {
79
+ delete state.indices[params.name];
80
+ return new Reply(200, { taskID: task(core, state, { index: params.name }), deletedAt: new Date().toISOString() });
81
+ }),
82
+ ], () => new Reply(404, { message: "not found" }));
83
+ //# sourceMappingURL=algolia.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"algolia.js","sourceRoot":"","sources":["../../src/routes/algolia.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAkD,MAAM,gBAAgB,CAAC;AAiCtG,oHAAoH;AACpH,MAAM,CAAC,MAAM,UAAU,GAA2C;IAChE,GAAG,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,iBAAiB,EAAE,SAAS,EAAE,aAAa,EAAE;IACnF,WAAW,EAAE,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,oBAAoB,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE;IAEpG,KAAK,CAAC,KAAK,EAAE,IAAI;QACf,MAAM,OAAO,GAA4B,EAAE,CAAC;QAC5C,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,CAAC,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;QACxK,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,IAAI,CAAC,EAAE,CAAC;IACjE,CAAC;IAED,iFAAiF;IACjF,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;QAC5C,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,KAAK,CAAC;QACnE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,SAAS,GAAG,cAAc,IAAI,EAAE,CAAC,CAAC;QAC5E,IAAI,KAAK,KAAK,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,SAAS,GAAG,8BAA8B,CAAC,CAAC;QACpF,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,4CAA4C;IAC5C,UAAU,CAAC,OAA4B;QACrC,MAAM,GAAG,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;QACzC,MAAM,GAAG,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;QAChD,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,EAAE,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,EAAE,CAAC;IACxF,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,SAAS,IAAI,CAAC,IAAa,EAAE,KAAmB,EAAE,CAA+B;IAC/E,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;IAC7D,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC;IAC7D,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,gEAAgE;AAChE,SAAS,OAAO,CAAC,KAAmB,EAAE,CAAc;IAClD,IAAI,CAAC,CAAC,CAAC,IAAI;QAAE,OAAO;IACpB,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACtC,MAAM,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC;IACvC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG;QACvB,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,OAAO,IAAI,CAAC,CAAC;QAC5D,QAAQ,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,KAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,QAAQ,IAAI,EAAE,CAAC;QACtH,SAAS,EAAE,KAAK;KACjB,CAAC;IACF,OAAO,CAAC,CAAC,IAAI,CAAC;AAChB,CAAC;AAED,MAAM,MAAM,GAAG,MAAM,CACnB;IACE,KAAK,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAElK,KAAK,CAAC,MAAM,EAAE,4BAA4B,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE;QAC5E,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAoE,CAAC;QAC1F,IAAI,CAAC,CAAC,SAAS,KAAK,MAAM,IAAI,OAAO,CAAC,CAAC,WAAW,KAAK,QAAQ,IAAI,CAAC,CAAC,WAAW,KAAK,EAAE;YAAE,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,2CAA2C,EAAE,CAAC,CAAC;QACzK,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,KAAkB,CAAC,CAAC,CAAC,SAAS,CAAC;QACzE,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,WAAW,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QACjH,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;IAC7E,CAAC,CAAC;IAEF,KAAK,CAAC,KAAK,EAAE,6BAA6B,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;QAChE,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,IAAI;YAAE,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAC;QAC7F,IAAI,CAAC,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC;YAClB,CAAC,CAAC,OAAO,EAAE,CAAC;YACZ,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAClB,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IACjD,CAAC,CAAC;IAEF,KAAK,CAAC,KAAK,EAAE,2BAA2B,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;QAC9D,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACrC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IAC3G,CAAC,CAAC;IAEF,KAAK,CAAC,QAAQ,EAAE,kBAAkB,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;QAC9D,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAClC,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;IACpH,CAAC,CAAC;CACH,EACD,GAAG,EAAE,CAAC,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAC/C,CAAC"}