@zitadel/cli 1.0.0-alpha.21 → 1.0.0-alpha.23

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 (64) hide show
  1. package/README.md +2248 -227
  2. package/SKILLS.md +246 -58
  3. package/dist/groups-CMB0fMzE.mjs +22 -0
  4. package/dist/groups-CMB0fMzE.mjs.map +1 -0
  5. package/dist/index.mjs +16027 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/dist/lib/oclif/help.mjs +67 -0
  8. package/dist/lib/oclif/help.mjs.map +1 -0
  9. package/oclif.manifest.json +4577 -185
  10. package/package.json +42 -8
  11. package/dist/branding-cygSBPkV.mjs +0 -79
  12. package/dist/branding-cygSBPkV.mjs.map +0 -1
  13. package/dist/claim-state-DlfD6Res.mjs +0 -66
  14. package/dist/claim-state-DlfD6Res.mjs.map +0 -1
  15. package/dist/commands/apply.mjs +0 -81
  16. package/dist/commands/apply.mjs.map +0 -1
  17. package/dist/commands/branding/eject.mjs +0 -104
  18. package/dist/commands/branding/eject.mjs.map +0 -1
  19. package/dist/commands/claim.mjs +0 -415
  20. package/dist/commands/claim.mjs.map +0 -1
  21. package/dist/commands/doctor.mjs +0 -1122
  22. package/dist/commands/doctor.mjs.map +0 -1
  23. package/dist/commands/eject.mjs +0 -157
  24. package/dist/commands/eject.mjs.map +0 -1
  25. package/dist/commands/logs.mjs +0 -58
  26. package/dist/commands/logs.mjs.map +0 -1
  27. package/dist/commands/plan.mjs +0 -65
  28. package/dist/commands/plan.mjs.map +0 -1
  29. package/dist/commands/reset.mjs +0 -79
  30. package/dist/commands/reset.mjs.map +0 -1
  31. package/dist/commands/schemas/list.mjs +0 -146
  32. package/dist/commands/schemas/list.mjs.map +0 -1
  33. package/dist/commands/setup.mjs +0 -1271
  34. package/dist/commands/setup.mjs.map +0 -1
  35. package/dist/commands/start.mjs +0 -288
  36. package/dist/commands/start.mjs.map +0 -1
  37. package/dist/commands/status.mjs +0 -151
  38. package/dist/commands/status.mjs.map +0 -1
  39. package/dist/commands/stop.mjs +0 -105
  40. package/dist/commands/stop.mjs.map +0 -1
  41. package/dist/designs-Ckz18Dpo.mjs +0 -38
  42. package/dist/designs-Ckz18Dpo.mjs.map +0 -1
  43. package/dist/docker-Ev1Ggt9l.mjs +0 -436
  44. package/dist/docker-Ev1Ggt9l.mjs.map +0 -1
  45. package/dist/environment-rjRVkJjW.mjs +0 -17
  46. package/dist/environment-rjRVkJjW.mjs.map +0 -1
  47. package/dist/journey-guidance-BGrOX_gT.mjs +0 -40
  48. package/dist/journey-guidance-BGrOX_gT.mjs.map +0 -1
  49. package/dist/oclif-CanO3zdt.mjs +0 -1769
  50. package/dist/oclif-CanO3zdt.mjs.map +0 -1
  51. package/dist/orca-66vAxeIe.mjs +0 -3917
  52. package/dist/orca-66vAxeIe.mjs.map +0 -1
  53. package/dist/package-manager-DFdLcDx1.mjs +0 -194
  54. package/dist/package-manager-DFdLcDx1.mjs.map +0 -1
  55. package/dist/ports-BM20XIZb.mjs +0 -116
  56. package/dist/ports-BM20XIZb.mjs.map +0 -1
  57. package/dist/processes-DKHPkU8O.mjs +0 -120
  58. package/dist/processes-DKHPkU8O.mjs.map +0 -1
  59. package/dist/project-CHhot85s.mjs +0 -140
  60. package/dist/project-CHhot85s.mjs.map +0 -1
  61. package/dist/sync-BBlACKzT.mjs +0 -1633
  62. package/dist/sync-BBlACKzT.mjs.map +0 -1
  63. package/dist/user-schema-DTuOsdKE.mjs +0 -91
  64. package/dist/user-schema-DTuOsdKE.mjs.map +0 -1
@@ -1,3917 +0,0 @@
1
- import { A as npmDistTagForCliVersion, C as parseJsonObject, E as ZitadelError, O as normalizePublicCliJson, S as isObject, T as stableStringify, b as MANAGED_MARKER, j as publicCliCommand, n as DEFAULT_SERVER, w as setTopLevelJsonKey } from "./oclif-CanO3zdt.mjs";
2
- import { access, chmod, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
- import { basename, dirname, join } from "node:path";
4
- import semver from "semver";
5
- import { META_SCHEMA_DIR, metaSchemaFiles } from "@zitadel/config/meta-schemas";
6
- import { builders, generateCode, parseModule } from "magicast";
7
- import { spawnSync } from "node:child_process";
8
- //#region src/lib/orca/detectors/package-json.ts
9
- /**
10
- * Reads and parses the `package.json` at `cwd`. Rejects if the file is absent
11
- * or malformed; callers that treat those as "not a project" are expected to
12
- * catch and fall back rather than have detection swallow the error here.
13
- */
14
- async function readPackageJson(cwd) {
15
- const contents = await readFile(join(cwd, "package.json"), "utf8");
16
- return JSON.parse(contents);
17
- }
18
- /**
19
- * Reports whether `name` appears in either `dependencies` or `devDependencies`.
20
- * Both are checked because framework packages may legitimately live in either.
21
- */
22
- function hasDependency(pkg, name) {
23
- return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
24
- }
25
- /**
26
- * Best-effort major version of a declared dependency: the first number in the
27
- * spec. A heuristic for scaffold decisions (e.g. which Next request-boundary
28
- * convention to emit), NOT for support gating — range semantics like `<16`
29
- * are beyond it. Floors use {@link dependencySpecProvablyBelowMajor}.
30
- */
31
- function dependencyVersionMajor(pkg, name) {
32
- const spec = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
33
- if (!spec) return;
34
- const match = spec.match(/\d+/);
35
- return match ? Number(match[0]) : void 0;
36
- }
37
- /**
38
- * Returns the declared spec for `name` when that spec provably cannot resolve
39
- * to `floorMajor` or newer: a valid semver range with no overlap with
40
- * `>=floorMajor.0.0-0`. Everything unprovable returns `undefined` — protocol
41
- * specs (`file:`, `link:`, `workspace:`, git URLs), dist-tags (`latest`,
42
- * `canary`), absent deps, and ranges that also admit a compliant version
43
- * (`>=14`, `14 || 16`). The floor comparator includes prereleases so a
44
- * `15.0.0-rc` user is not rejected by the 15 floor.
45
- */
46
- function dependencySpecProvablyBelowMajor(pkg, name, floorMajor) {
47
- const spec = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
48
- if (!spec || semver.validRange(spec) === null) return;
49
- const atOrAboveFloor = `>=${String(floorMajor)}.0.0-0`;
50
- return semver.intersects(spec, atOrAboveFloor, { includePrerelease: true }) ? void 0 : spec;
51
- }
52
- //#endregion
53
- //#region src/lib/orca/detectors/port.ts
54
- /**
55
- * Port assumed when no explicit dev port can be discovered. Matches Next.js's
56
- * own default so the inferred issuer URL lines up with `next dev`.
57
- */
58
- const DEFAULT_DEV_PORT = 3e3;
59
- /**
60
- * Determines the local dev-server port for `cwd`. The `dev` script is the most
61
- * authoritative source, then a `PORT` declaration in an env file, falling back
62
- * to {@link DEFAULT_DEV_PORT}. Used to derive the local issuer URL.
63
- */
64
- async function detectDevPort(cwd, pkg) {
65
- const dev = pkg.scripts?.dev;
66
- const fromScript = typeof dev === "string" ? extractPort(dev) : void 0;
67
- if (fromScript) return fromScript;
68
- const fromEnvFile = await portFromEnvFile(cwd);
69
- if (fromEnvFile) return fromEnvFile;
70
- return DEFAULT_DEV_PORT;
71
- }
72
- async function portFromEnvFile(cwd) {
73
- for (const candidate of [".env.local", ".env"]) try {
74
- const rawPort = (await readFile(join(cwd, candidate), "utf8")).match(/^\s*PORT\s*=\s*(\d+)/m)?.[1];
75
- if (rawPort) return Number.parseInt(rawPort, 10);
76
- } catch {
77
- continue;
78
- }
79
- }
80
- /**
81
- * Parses a dev port out of an npm `dev` script string, recognizing both flag
82
- * forms (`-p`/`--port`) and a leading `PORT=` env assignment. Returns
83
- * `undefined` when no valid positive port is present so callers can fall back.
84
- */
85
- function extractPort(script) {
86
- const inline = script.match(/-p\s+(\d+)|--port[=\s]+(\d+)/);
87
- if (inline) {
88
- const raw = inline[1] ?? inline[2];
89
- if (!raw) return;
90
- const value = Number.parseInt(raw, 10);
91
- if (Number.isFinite(value) && value > 0) return value;
92
- }
93
- const rawEnvPort = script.match(/(?:^|\s)PORT=(\d+)/)?.[1];
94
- if (rawEnvPort) return Number.parseInt(rawEnvPort, 10);
95
- }
96
- /**
97
- * Rewrites a `dev` script so it explicitly runs on `port`.
98
- *
99
- * Setup registers `http://localhost:<port>` as the project's only allowed
100
- * origin, so the dev server has to land on exactly that port — a bare
101
- * `next dev` does not: it defaults to 3000 and, when 3000 is taken, silently
102
- * falls back to 3001. Either way the flow API then rejects the app's origin
103
- * mid-login. An explicit `--port` removes both failure modes: the port
104
- * matches what setup registered, and a busy port fails loudly with
105
- * `EADDRINUSE` instead of drifting to one the project does not allow.
106
- *
107
- * `--port <n>` is the one spelling every framework this CLI scaffolds
108
- * accepts (`next dev`, `nuxt dev`, `vite`, `vinxi dev`, `ng serve`), so an
109
- * appended declaration stays framework-agnostic. An existing declaration is
110
- * rewritten in its own form instead — a `PORT=` env assignment keeps the
111
- * `PORT=` form — so the script keeps whichever mechanism its author chose and
112
- * only the number changes.
113
- */
114
- function withDevPort(script, port) {
115
- const flagPattern = /(-p\s+|--port[=\s]+)\d+/;
116
- if (flagPattern.test(script)) return script.replace(flagPattern, (_match, prefix) => `${prefix}${String(port)}`);
117
- const envPattern = /((?:^|\s)PORT=)\d+/;
118
- if (envPattern.test(script)) return script.replace(envPattern, (_match, prefix) => `${prefix}${String(port)}`);
119
- return `${script.trimEnd()} --port ${String(port)}`;
120
- }
121
- /**
122
- * Builds the local OIDC issuer URL for a given dev port. Centralized so the
123
- * `localhost` origin convention is defined in exactly one place.
124
- */
125
- function issuerFromPort(port) {
126
- return `http://localhost:${port}`;
127
- }
128
- /**
129
- * The port an issuer URL names, or `undefined` when it names none.
130
- *
131
- * The inverse of {@link issuerFromPort}, and the authoritative way to learn
132
- * which port a project *registered* — as opposed to {@link detectDevPort},
133
- * which reports the port the app would start on right now. The two agree
134
- * during setup and diverge afterwards, which is exactly when it matters: a
135
- * dev script edited to another port must be read as a mismatch against the
136
- * registered origin, not as the new truth.
137
- */
138
- function portFromIssuer(issuer) {
139
- try {
140
- new URL(issuer);
141
- } catch {
142
- return;
143
- }
144
- let probe;
145
- try {
146
- probe = new URL(issuer.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:/, "zl-issuer-probe:"));
147
- } catch {
148
- return;
149
- }
150
- const port = Number.parseInt(probe.port, 10);
151
- return Number.isFinite(port) && port > 0 ? port : void 0;
152
- }
153
- //#endregion
154
- //#region src/lib/orca/detectors/angular.ts
155
- /** The lowest Angular major the generated templates compile on. */
156
- const MIN_ANGULAR_MAJOR = 17;
157
- /** Best-effort major version from a dependency range (`^17.3.0` → 17). */
158
- function angularMajor(spec) {
159
- const match = spec.match(/\d+/);
160
- return match ? Number.parseInt(match[0], 10) : void 0;
161
- }
162
- /**
163
- * Detects an Angular project by its `@angular/core` dependency. The app dir is
164
- * `src/app` (where the patcher writes `app.ts`/`app.html`), the dev port comes
165
- * from the project (else the default), and the issuer is derived from it.
166
- * Angular's dev server (`@angular/build:dev-server`)
167
- * is Vite-based but configured via `angular.json` + a `proxy.conf.cjs`, not a
168
- * `vite.config.ts` — handled by the Angular patcher.
169
- *
170
- * The managed templates use Angular 17+ control flow (`@if`), so a clearly
171
- * older project fails fast with `E_VALIDATION` instead of scaffolding files
172
- * that won't compile.
173
- */
174
- var AngularDetector = class {
175
- framework = "angular";
176
- async detect(cwd) {
177
- const pkg = await readPackageJson(cwd).catch(() => void 0);
178
- if (!pkg || !hasDependency(pkg, "@angular/core")) return null;
179
- const spec = pkg.dependencies?.["@angular/core"] ?? pkg.devDependencies?.["@angular/core"];
180
- const major = spec ? angularMajor(spec) : void 0;
181
- if (major !== void 0 && major < MIN_ANGULAR_MAJOR) throw new ZitadelError("E_VALIDATION", `Angular ${major} is unsupported — the generated auth templates use Angular ${MIN_ANGULAR_MAJOR}+ control flow (@if).`, { hint: `Upgrade to Angular ${MIN_ANGULAR_MAJOR} or newer before running setup, or add the Zitadel components to your templates manually.` });
182
- const devPort = await detectDevPort(cwd, pkg);
183
- return {
184
- id: "angular",
185
- appDir: "src/app",
186
- devPort,
187
- url: issuerFromPort(devPort)
188
- };
189
- }
190
- };
191
- //#endregion
192
- //#region src/lib/orca/detectors/next.ts
193
- /**
194
- * Detects a Next.js App Router project and extracts its facts: the App Router
195
- * directory (`app` vs `src/app`), the dev-server port (parsed from the `dev`
196
- * script / env file, else 3000), and the derived local issuer URL. Owns every
197
- * Next-specific assumption so the orchestrator and commands stay generic.
198
- */
199
- var NextDetector = class {
200
- framework = "next";
201
- /**
202
- * Returns `null` when `cwd` is not a Next.js project (no `next` dependency),
203
- * so the orchestrator can try other detectors. Throws
204
- * `E_UNSUPPORTED_PROJECT_SHAPE` when it is Next.js but lacks an App Router
205
- * directory (e.g. a Pages Router project), which is a hard error rather than
206
- * an empty directory to scaffold.
207
- */
208
- async detect(cwd) {
209
- const pkg = await readPackageJson(cwd).catch(() => void 0);
210
- if (!pkg || !hasDependency(pkg, "next")) return null;
211
- const belowFloor = dependencySpecProvablyBelowMajor(pkg, "next", 15);
212
- if (belowFloor !== void 0) throw new ZitadelError("E_UNSUPPORTED_PROJECT_SHAPE", `Next.js "${belowFloor}" is below the supported floor — the CLI integrates Next.js 15 and newer`, { hint: "Upgrade the app to Next 15+ (e.g. `npx @next/codemod@latest upgrade`) and rerun." });
213
- const appDir = await dirExists$1(join(cwd, "app")) ? "app" : await dirExists$1(join(cwd, "src/app")) ? "src/app" : void 0;
214
- if (!appDir) throw new ZitadelError("E_UNSUPPORTED_PROJECT_SHAPE", "Next.js Pages Router projects are not supported in v1", { hint: "Create an App Router project with an app/ or src/app/ directory." });
215
- const devPort = await detectDevPort(cwd, pkg);
216
- const versionMajor = dependencyVersionMajor(pkg, "next");
217
- return {
218
- id: "next",
219
- appDir,
220
- devPort,
221
- url: issuerFromPort(devPort),
222
- ...versionMajor === void 0 ? {} : { versionMajor }
223
- };
224
- }
225
- };
226
- async function dirExists$1(path) {
227
- try {
228
- return (await stat(path)).isDirectory();
229
- } catch (error) {
230
- if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return false;
231
- throw error;
232
- }
233
- }
234
- //#endregion
235
- //#region src/lib/orca/detectors/nuxt.ts
236
- /**
237
- * Detects a Nuxt project by its `nuxt` dependency. Like Next.js, Nuxt proxies
238
- * the auth backend through server middleware (`@zitadel/sdk-nuxt`), not a Vite
239
- * dev-server proxy — so the patcher wires the module + a `nuxt.config.ts` edit.
240
- * Runs before the Vue detector (which excludes Nuxt) since Nuxt ships Vue.
241
- *
242
- * `appDir` tracks the Nuxt srcDir: Nuxt 4 (what `nuxi init` now scaffolds) keeps
243
- * `app.vue`/`pages/`/`plugins/` under `app/`, while Nuxt 3 keeps them at the
244
- * root — so the patcher writes its files relative to whichever this project uses.
245
- */
246
- var NuxtDetector = class {
247
- framework = "nuxt";
248
- async detect(cwd) {
249
- const pkg = await readPackageJson(cwd).catch(() => void 0);
250
- if (!pkg || !hasDependency(pkg, "nuxt")) return null;
251
- const appDir = await dirExists(join(cwd, "app")) ? "app" : ".";
252
- const devPort = await detectDevPort(cwd, pkg);
253
- return {
254
- id: "nuxt",
255
- appDir,
256
- devPort,
257
- url: issuerFromPort(devPort)
258
- };
259
- }
260
- };
261
- async function dirExists(path) {
262
- return stat(path).then((s) => s.isDirectory(), () => false);
263
- }
264
- //#endregion
265
- //#region src/lib/orca/detectors/qwik.ts
266
- /**
267
- * Detects a Vite + Qwik single-page app: depends on `@builder.io/qwik` and
268
- * `vite` but NOT `@builder.io/qwik-city` (Qwik City is its own meta-framework
269
- * and ships Qwik). The source dir is `src`, the dev port comes from the
270
- * project, and the issuer is derived from it.
271
- */
272
- var QwikDetector = class {
273
- framework = "qwik";
274
- async detect(cwd) {
275
- const pkg = await readPackageJson(cwd).catch(() => void 0);
276
- if (!pkg || hasDependency(pkg, "@builder.io/qwik-city") || !hasDependency(pkg, "@builder.io/qwik") || !hasDependency(pkg, "vite")) return null;
277
- const devPort = await detectDevPort(cwd, pkg);
278
- return {
279
- id: "qwik",
280
- appDir: "src",
281
- devPort,
282
- url: issuerFromPort(devPort)
283
- };
284
- }
285
- };
286
- //#endregion
287
- //#region src/lib/orca/detectors/react.ts
288
- /**
289
- * Detects a Vite + React single-page app and extracts its facts: the source
290
- * directory (`src`), the dev-server port (parsed from the `dev` script / env
291
- * file, else the framework default), and the derived local issuer URL.
292
- *
293
- * Recognises a project that depends on both `react` and `vite` but NOT `next`
294
- * — Next.js ships React too, so the {@link import("./next").NextDetector} must
295
- * run first (and does, by registry order) and this detector excludes it.
296
- */
297
- var ReactDetector = class {
298
- framework = "react";
299
- async detect(cwd) {
300
- const pkg = await readPackageJson(cwd).catch(() => void 0);
301
- if (!pkg || hasDependency(pkg, "next") || !hasDependency(pkg, "react") || !hasDependency(pkg, "vite")) return null;
302
- const belowFloor = dependencySpecProvablyBelowMajor(pkg, "react", 18);
303
- if (belowFloor !== void 0) throw new ZitadelError("E_UNSUPPORTED_PROJECT_SHAPE", `React "${belowFloor}" is below the supported floor — the CLI integrates React 18 and newer`, { hint: "Upgrade the app to React 18+ and rerun." });
304
- const devPort = await detectDevPort(cwd, pkg);
305
- const versionMajor = dependencyVersionMajor(pkg, "react");
306
- return {
307
- id: "react",
308
- appDir: "src",
309
- devPort,
310
- url: issuerFromPort(devPort),
311
- ...versionMajor === void 0 ? {} : { versionMajor }
312
- };
313
- }
314
- };
315
- //#endregion
316
- //#region src/lib/orca/detectors/solid.ts
317
- /**
318
- * Detects a Vite + Solid single-page app: depends on `solid-js` and `vite` but
319
- * NOT `@solidjs/start` (SolidStart is its own meta-framework and ships Solid).
320
- * The source dir is `src`, the dev port comes from the project, and the issuer
321
- * is derived from it.
322
- */
323
- var SolidDetector = class {
324
- framework = "solid";
325
- async detect(cwd) {
326
- const pkg = await readPackageJson(cwd).catch(() => void 0);
327
- if (!pkg || hasDependency(pkg, "@solidjs/start") || !hasDependency(pkg, "solid-js") || !hasDependency(pkg, "vite")) return null;
328
- const devPort = await detectDevPort(cwd, pkg);
329
- return {
330
- id: "solid",
331
- appDir: "src",
332
- devPort,
333
- url: issuerFromPort(devPort)
334
- };
335
- }
336
- };
337
- //#endregion
338
- //#region src/lib/orca/detectors/svelte.ts
339
- /**
340
- * Detects a Vite + Svelte single-page app: depends on `svelte` and `vite` but
341
- * NOT `@sveltejs/kit` (SvelteKit is its own meta-framework and ships Svelte).
342
- * The source dir is `src`, the dev port comes from the project, and the issuer
343
- * is derived from it.
344
- */
345
- var SvelteDetector = class {
346
- framework = "svelte";
347
- async detect(cwd) {
348
- const pkg = await readPackageJson(cwd).catch(() => void 0);
349
- if (!pkg || hasDependency(pkg, "@sveltejs/kit") || !hasDependency(pkg, "svelte") || !hasDependency(pkg, "vite")) return null;
350
- const devPort = await detectDevPort(cwd, pkg);
351
- return {
352
- id: "svelte",
353
- appDir: "src",
354
- devPort,
355
- url: issuerFromPort(devPort)
356
- };
357
- }
358
- };
359
- //#endregion
360
- //#region src/lib/orca/detectors/vue.ts
361
- /**
362
- * Detects a Vite + Vue single-page app: depends on `vue` and `vite` but NOT
363
- * `nuxt` (Nuxt is its own meta-framework and ships Vue) — so the source dir is
364
- * `src`, the dev port comes from the project, and the issuer is derived from it.
365
- */
366
- var VueDetector = class {
367
- framework = "vue";
368
- async detect(cwd) {
369
- const pkg = await readPackageJson(cwd).catch(() => void 0);
370
- if (!pkg || hasDependency(pkg, "nuxt") || !hasDependency(pkg, "vue") || !hasDependency(pkg, "vite")) return null;
371
- const devPort = await detectDevPort(cwd, pkg);
372
- return {
373
- id: "vue",
374
- appDir: "src",
375
- devPort,
376
- url: issuerFromPort(devPort)
377
- };
378
- }
379
- };
380
- //#endregion
381
- //#region src/lib/orca/detectors/index.ts
382
- /**
383
- * Active detectors, in probe order. The orchestrator tries each until one
384
- * recognises the project. Add a framework by appending its detector here — no
385
- * orchestrator changes needed. Meta-frameworks run before their base: Next
386
- * before React (Next ships React), and the Vue detector excludes Nuxt — so a
387
- * Next/Nuxt project is never mistaken for a bare React/Vue SPA.
388
- */
389
- const detectors = [
390
- new NextDetector(),
391
- new NuxtDetector(),
392
- new ReactDetector(),
393
- new VueDetector(),
394
- new SolidDetector(),
395
- new SvelteDetector(),
396
- new QwikDetector(),
397
- new AngularDetector()
398
- ];
399
- //#endregion
400
- //#region src/lib/orca/patchers/rule/file-writer/index.ts
401
- /**
402
- * Records one touched artifact, deduplicating by path: several plan ops can
403
- * legitimately hit the same file (the base and framework op lists both merge
404
- * into `.env.local`, for example), but the report should carry it once, with
405
- * the first action as the net one — a file created and then extended in the
406
- * same run was created by the run.
407
- */
408
- function record(result, path, kind, action) {
409
- if (result.files.some((file) => file.path === path)) return;
410
- result.files.push({
411
- path,
412
- kind,
413
- action
414
- });
415
- }
416
- /**
417
- * Applies a {@link ScaffoldPlan} to disk, executing its operations in order.
418
- *
419
- * Operations are idempotent: writes whose target already matches the desired
420
- * contents are recorded as skipped rather than rewritten, so re-running setup
421
- * is safe. With `dryRun` no filesystem changes are made but the result still
422
- * reflects what would have been written. Existing files are only overwritten
423
- * when `force` is set; otherwise an `E_CONFLICT` is thrown to protect
424
- * user-authored content. Paths in the plan are resolved relative to `cwd`.
425
- */
426
- async function scaffold(plan, opts) {
427
- const result = {
428
- dryRun: opts.dryRun,
429
- files: [],
430
- filesSkipped: [],
431
- depsAdded: []
432
- };
433
- for (const op of plan.ops) await applyOp(op, opts, result);
434
- const written = new Set(result.files.map((file) => file.path));
435
- return {
436
- dryRun: result.dryRun,
437
- files: result.files,
438
- filesWritten: result.files.filter((file) => file.kind === "file").map((file) => file.path),
439
- filesSkipped: [...new Set(result.filesSkipped)].filter((path) => !written.has(path)),
440
- depsAdded: result.depsAdded
441
- };
442
- }
443
- async function applyOp(op, opts, result) {
444
- switch (op.kind) {
445
- case "mkdir":
446
- await ensureDir(abs(opts.cwd, op.path), op.mode, opts.dryRun, result);
447
- break;
448
- case "write":
449
- await writeText(abs(opts.cwd, op.path), op.contents, {
450
- mode: op.mode,
451
- force: opts.force,
452
- dryRun: opts.dryRun
453
- }, result);
454
- break;
455
- case "append":
456
- await appendText(abs(opts.cwd, op.path), op.contents, op.ifMissing, opts.dryRun, result);
457
- break;
458
- case "merge-env":
459
- await mergeEnv(abs(opts.cwd, op.path), op.entries, opts.dryRun, result);
460
- break;
461
- case "merge-json":
462
- await mergeJson(abs(opts.cwd, op.path), op.patch, opts.dryRun, result);
463
- break;
464
- case "append-gitignore":
465
- await appendGitignore(abs(opts.cwd, ".gitignore"), op.entries, opts.dryRun, result);
466
- break;
467
- case "add-dep":
468
- await addDependency(abs(opts.cwd, "package.json"), op, opts.dryRun, result);
469
- break;
470
- case "edit":
471
- await editFile(opts.cwd, op.path, op.edit, opts.dryRun, result);
472
- break;
473
- }
474
- }
475
- /**
476
- * Generic content edit: read the file, run the patcher-supplied transform, write
477
- * the result. Framework knowledge lives entirely in `edit` (next to its
478
- * patcher); this executor only owns candidate resolution, idempotency, dry-run,
479
- * and the atomic write. `pathOrPaths` may be a single path or a priority list of
480
- * candidates — the first that exists wins, else the first candidate.
481
- */
482
- async function editFile(cwd, pathOrPaths, edit, dryRun, result) {
483
- const candidates = (typeof pathOrPaths === "string" ? [pathOrPaths] : pathOrPaths).map((p) => abs(cwd, p));
484
- if (candidates.length === 0) throw new ZitadelError("E_VALIDATION", "An edit op needs at least one candidate path", { hint: "This is an internal patcher error — please report it if you hit it." });
485
- let path = candidates[0];
486
- let source;
487
- let mode;
488
- for (const candidate of candidates) {
489
- const contents = await readIfExists(candidate);
490
- if (contents !== void 0) {
491
- path = candidate;
492
- source = contents;
493
- mode = (await stat(candidate)).mode & 511;
494
- break;
495
- }
496
- }
497
- const next = edit(source);
498
- if (next === source) {
499
- result.filesSkipped.push(path);
500
- return;
501
- }
502
- const action = source === void 0 ? "create" : "update";
503
- if (dryRun) {
504
- record(result, path, "file", action);
505
- return;
506
- }
507
- await mkdir(dirname(path), { recursive: true });
508
- const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
509
- await writeFile(tmp, next);
510
- if (mode !== void 0) await chmod(tmp, mode).catch(() => void 0);
511
- await rename(tmp, path);
512
- record(result, path, "file", action);
513
- }
514
- async function ensureDir(path, mode, dryRun, result) {
515
- const pre = await stat(path).catch(() => void 0);
516
- const existed = pre?.isDirectory() ?? false;
517
- const healsMode = existed && mode !== void 0 && process.platform !== "win32" && (pre.mode & 511) !== mode;
518
- if (dryRun) {
519
- if (!existed) record(result, path, "dir", "create");
520
- else if (healsMode) record(result, path, "dir", "update");
521
- else result.filesSkipped.push(path);
522
- return;
523
- }
524
- await mkdir(path, {
525
- recursive: true,
526
- mode
527
- });
528
- if (mode) await chmod(path, mode).catch(() => void 0);
529
- if (!existed) record(result, path, "dir", "create");
530
- else if (healsMode) record(result, path, "dir", "update");
531
- else result.filesSkipped.push(path);
532
- }
533
- async function writeText(path, contents, opts, result) {
534
- const existing = await readIfExists(path);
535
- if (existing === contents) {
536
- result.filesSkipped.push(path);
537
- return;
538
- }
539
- if (existing !== void 0 && !opts.force) throw new ZitadelError("E_CONFLICT", `Refusing to overwrite ${path}`, {
540
- hint: "Re-run with --force if you want the CLI to replace this file.",
541
- details: { path }
542
- });
543
- const action = existing === void 0 ? "create" : "update";
544
- if (opts.dryRun) {
545
- record(result, path, "file", action);
546
- return;
547
- }
548
- await mkdir(dirname(path), { recursive: true });
549
- const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
550
- await writeFile(tmp, contents, { mode: opts.mode });
551
- if (opts.mode) await chmod(tmp, opts.mode).catch(() => void 0);
552
- await rename(tmp, path);
553
- record(result, path, "file", action);
554
- }
555
- async function appendText(path, contents, ifMissing, dryRun, result) {
556
- const raw = await readIfExists(path);
557
- const existing = raw ?? "";
558
- if (ifMissing && existing.includes(ifMissing)) {
559
- result.filesSkipped.push(path);
560
- return;
561
- }
562
- const next = `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${contents}`;
563
- if (next === existing) {
564
- result.filesSkipped.push(path);
565
- return;
566
- }
567
- const action = raw === void 0 ? "create" : "update";
568
- if (dryRun) {
569
- record(result, path, "file", action);
570
- return;
571
- }
572
- await mkdir(dirname(path), { recursive: true });
573
- await writeFile(path, next);
574
- record(result, path, "file", action);
575
- }
576
- async function mergeEnv(path, entries, dryRun, result) {
577
- const raw = await readIfExists(path);
578
- const existing = raw ?? "";
579
- const present = new Set(existing.split(/\r?\n/g).map((line) => line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=/)?.[1]).filter((value) => Boolean(value)));
580
- const additions = Object.entries(entries).filter(([key]) => !present.has(key));
581
- if (additions.length === 0) {
582
- result.filesSkipped.push(path);
583
- return;
584
- }
585
- const block = additions.map(([key, value]) => `${key}=${value}`).join("\n");
586
- const next = `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${block}\n`;
587
- const action = raw === void 0 ? "create" : "update";
588
- if (dryRun) {
589
- record(result, path, "file", action);
590
- return;
591
- }
592
- await mkdir(dirname(path), { recursive: true });
593
- await writeFile(path, next);
594
- record(result, path, "file", action);
595
- }
596
- async function mergeJson(path, patch, dryRun, result) {
597
- const existing = await readIfExists(path);
598
- const contents = `${stableStringify(deepMerge(existing ? parseJsonObject(existing, path) : {}, patch))}\n`;
599
- if (existing === contents) {
600
- result.filesSkipped.push(path);
601
- return;
602
- }
603
- const action = existing === void 0 ? "create" : "update";
604
- if (dryRun) {
605
- record(result, path, "file", action);
606
- return;
607
- }
608
- await mkdir(dirname(path), { recursive: true });
609
- await writeFile(path, contents);
610
- record(result, path, "file", action);
611
- }
612
- async function appendGitignore(path, entries, dryRun, result) {
613
- const raw = await readIfExists(path);
614
- const existing = raw ?? "";
615
- const lines = new Set(existing.split(/\r?\n/g).map((line) => line.trim()));
616
- const missing = entries.filter((entry) => !lines.has(entry));
617
- if (missing.length === 0) {
618
- result.filesSkipped.push(path);
619
- return;
620
- }
621
- const next = `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${missing.join("\n")}\n`;
622
- const action = raw === void 0 ? "create" : "update";
623
- if (dryRun) {
624
- record(result, path, "file", action);
625
- return;
626
- }
627
- await writeFile(path, next);
628
- record(result, path, "file", action);
629
- }
630
- async function addDependency(path, op, dryRun, result) {
631
- const existing = await readIfExists(path);
632
- if (!existing) throw new ZitadelError("E_VALIDATION", "package.json is required to add Zitadel dependencies");
633
- const current = parseJsonObject(existing, path);
634
- const key = op.dev ? "devDependencies" : "dependencies";
635
- const deps = isObject(current[key]) ? current[key] : {};
636
- if (deps[op.name] === op.version) {
637
- result.filesSkipped.push(path);
638
- return;
639
- }
640
- const contents = setTopLevelJsonKey(existing, path, key, sortByKey({
641
- ...deps,
642
- [op.name]: op.version
643
- }));
644
- if (dryRun) {
645
- record(result, path, "file", "update");
646
- result.depsAdded.push(op.name);
647
- return;
648
- }
649
- await writeFile(path, contents);
650
- record(result, path, "file", "update");
651
- result.depsAdded.push(op.name);
652
- }
653
- /** Rebuilds an object with lexicographically sorted keys (dependency maps). */
654
- function sortByKey(value) {
655
- return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
656
- }
657
- async function readIfExists(path) {
658
- try {
659
- return await readFile(path, "utf8");
660
- } catch (error) {
661
- if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return;
662
- throw error;
663
- }
664
- }
665
- function abs(cwd, path) {
666
- return join(cwd, path);
667
- }
668
- function deepMerge(target, patch) {
669
- const out = { ...target };
670
- for (const [key, value] of Object.entries(patch)) if (isObject(value) && isObject(out[key])) out[key] = deepMerge(out[key], value);
671
- else out[key] = value;
672
- return out;
673
- }
674
- //#endregion
675
- //#region src/lib/orca/patchers/rule/guidance.ts
676
- /**
677
- * Scaffolded guidance for humans (`README.md`) and agents (`AGENTS.md`):
678
- * the golden journey from a fresh scaffold to a customized, published
679
- * login — so the project explains its own next step without the docs
680
- * site. Both files are edited via a marker-fenced managed section, so
681
- * setup never clobbers what a developer (or another tool) wrote and a
682
- * rerun replaces only its own section.
683
- */
684
- const MARKER_BEGIN = "<!-- zitadel:guidance:begin -->";
685
- const MARKER_END = "<!-- zitadel:guidance:end -->";
686
- /**
687
- * Insert or replace the managed guidance section in `source`. A missing
688
- * file becomes `header` + section; an existing section (marker pair) is
689
- * replaced in place; anything else gets the section appended. Pure and
690
- * idempotent — the file-writer skips the write when output equals input.
691
- */
692
- function upsertGuidanceSection(source, section, header) {
693
- const block = `${MARKER_BEGIN}\n${section}\n${MARKER_END}\n`;
694
- if (source === void 0 || source.trim() === "") return `${header}${block}`;
695
- const begin = source.indexOf(MARKER_BEGIN);
696
- const end = source.indexOf(MARKER_END);
697
- if (begin !== -1 && end > begin) return `${source.slice(0, begin)}${block}${source.slice(end + 29).replace(/^\n/, "")}`;
698
- return `${source.replace(/\n*$/, "\n\n")}${block}`;
699
- }
700
- /**
701
- * Remove the managed guidance section (markers inclusive) from `source` —
702
- * the inverse of {@link upsertGuidanceSection}, for `eject`. Content outside
703
- * the markers is preserved byte-for-byte; a missing or malformed marker pair
704
- * returns `source` unchanged.
705
- */
706
- function removeGuidanceSection(source) {
707
- const begin = source.indexOf(MARKER_BEGIN);
708
- const end = source.indexOf(MARKER_END);
709
- if (begin === -1 || end <= begin) return source;
710
- return `${source.slice(0, begin)}${source.slice(end + 29).replace(/^\n/, "")}`;
711
- }
712
- /** The agent-facing golden path, written into `AGENTS.md`. */
713
- function agentsGuidanceSection(ctx) {
714
- const plan = publicCliCommand("plan", ctx.cliVersion);
715
- const apply = publicCliCommand("apply", ctx.cliVersion);
716
- const passkeyVerifyNote = ctx.preset === "passkey-first" ? " Agents: automated browsers can't complete passkey ceremonies — verify the loop via the email/password fallback actions, or attach a CDP WebAuthn virtual authenticator." : "";
717
- const jsxTypesNote = ctx.framework.id === "next" ? " React JSX types for the `<zitadel-*>` elements ship with the SDK — `custom-elements.d.ts` references `@zitadel/sdk-next/jsx`." : "";
718
- const presentationParagraph = ctx.posture === "widget" ? "Presentation, by contrast, is edited in the generated pages: this app pre-dates setup, so they embed the sign-in widgets as `variant=\"widget\"` cards (with `theme=\"auto\"`) inside your existing layout; switch to `variant=\"page\"` for the widget's own full-page chrome, and set `theme` (`light` | `dark` | `auto`) to pick the color scheme — `auto` follows the OS `prefers-color-scheme`, not this app's own theme, so pin `light` or `dark` when the app fixes its scheme." : "Presentation, by contrast, is edited in the generated pages: they pin the sign-in widgets to `variant=\"page\"` (full-page chrome); switch to `variant=\"widget\"` to embed a card inside your own layout, and set `theme` (`light` | `dark` | `auto`) to pick the color scheme — `auto` follows the OS `prefers-color-scheme`, not this app's own theme, so pin `light` or `dark` when the app fixes its scheme.";
719
- const themingParagraph = "Theming the widgets from your app: `--zl-*` design-token custom properties set on the element in your own stylesheet pierce the widgets' shadow DOM. The bridge covers your fonts (`--zl-font-family-heading`, `--zl-font-family-sans`), radii (`--zl-radius-md`/`-lg`/`-xl`), the primary CTA pair (`--zl-primary` / `--zl-primary-foreground`), and the link color (`--zl-link`). When your page already carries the heading, set the `suppress-header` attribute on `<zitadel-login>` / `<zitadel-session>` (SDK wrappers: `suppressHeader`) to visually hide the widget's own heading block — it stays in the accessibility tree. Starter designs from `.zitadel/branding/` collapse their brand pane by container width, not viewport: an embedded card usually renders the compact brand mark instead (`logo_url`, else `hero_url`, from `branding.json`; the `hero` design falls back to editable text).";
720
- const sessionStateParagraph = ctx.framework.id === "next" ? "Your app's own chrome (header navigation, account menus) reads session state with `getSession()` from `@zitadel/sdk-next/session` — a no-store client read of the same-origin `/__nextgen/sessions/me` that the session card itself uses. It returns identity only for 200 with a non-empty `user_id`, and signed-out only for the backend's canonical `401/auth.unauthorized` or `404/sess.not_found`; any other response is unknown/error, never signed-out. In Server Components on routes covered by the request-boundary `matcher`, `auth()` from `@zitadel/sdk-next/server` works too — extend the `matcher` in the request boundary to cover your public routes when you want server-rendered, flash-free session state in shared chrome (the trade: the middleware then runs on those routes). Sign-in and sign-out navigate (`post-sign-in-url` / `post-sign-out-url`), so chrome re-reads on the next page load without extra wiring." : ctx.framework.id === "nuxt" ? "Your app's own chrome (header navigation, account menus) reads session state with the auto-imported `useAuth()` composable — seeded from the server on every render by the scaffolded auth plugin. Sign-in and sign-out navigate (`post-sign-in-url` / `post-sign-out-url`), so the state is fresh on every page load." : "Your app's own chrome (header navigation, account menus) can read session state from the same-origin `/__nextgen/sessions/me` — the same server-answered read the session card performs. Fetch with `credentials: \"include\"`, `cache: \"no-store\"`, and `Accept: application/json`; treat 200 as signed-in only with a non-empty `user_id`, and signed-out only for the canonical error envelope `401/auth.unauthorized` or `404/sess.not_found`. Any other response is unknown/error, never signed-out. Sign-in and sign-out navigate (`post-sign-in-url` / `post-sign-out-url`), so chrome re-reads on the next page load.";
721
- return `## Authentication (Zitadel)
722
-
723
- This app's login is managed by Zitadel. Local config is the source of truth; never change auth behavior by editing generated route files.
724
-
725
- The golden path:
726
-
727
- 1. Start the dev server and open ${ctx.issuer}/login — **exactly this origin, not 127.0.0.1**. Passkeys and the origin allowlist are bound to it. (Freshly scaffolded apps also redirect / there; pre-existing apps keep their homepage.)
728
- 2. Prove the loop in a real browser: register a user → sign out → sign in → /profile shows signed in.${passkeyVerifyNote}
729
- 3. Customize by editing local config:
730
- - \`.zitadel/schemas/*.json\` — what a user is (fields, required, auth methods). See \`.zitadel/schemas/README.md\`.
731
- - \`.zitadel/flows/*.json\` — which screen collects which field/credential and how steps transition. See \`.zitadel/flows/README.md\`.
732
- 4. Preview, then publish:
733
- - \`${plan}\`
734
- - \`${apply}\`
735
- - Agents: append \`--non-interactive --json\` to both. \`plan\` validates flow invariants with the server's own rules **before** anything uploads — fix what it reports and re-run.
736
-
737
- ${presentationParagraph}${jsxTypesNote}
738
-
739
- ${themingParagraph}
740
-
741
- ${sessionStateParagraph}
742
-
743
- Machine-readable dialect (read these before authoring flow or schema edits):
744
-
745
- - Flow files carry \`"$schema": "../meta/flow-definition.json"\` — the flow dialect spec (steps, actions and their kinds, transitions, reserved outcomes like \`user_not_found\`). Editors validate against it.
746
- - \`.zitadel/meta/user-schema.json\` (with its companions \`user-property.json\`, \`auth-methods.json\`, \`auth-method.json\`) specifies the user-schema dialect (\`x-auth-methods\`, \`x-unique\`, property constraints).
747
- - Worked flow examples: https://github.com/zitadel/nextgen/tree/main/api/openapi/endpoints/flow_definitions/examples
748
-
749
- Never edit \`.zitadel/state.json\` (sync bookkeeping) or \`.zitadel/secret\` (credentials, git-ignored). Keep \`.zitadel/local/\` out of source control.`;
750
- }
751
- /** The human-facing summary appended to the app `README.md`. */
752
- function readmeGuidanceSection(ctx) {
753
- const plan = publicCliCommand("plan", ctx.cliVersion);
754
- const apply = publicCliCommand("apply", ctx.cliVersion);
755
- return `## Authentication (Zitadel)
756
-
757
- Login for this app is managed by [Zitadel](https://zitadel.com). Try it: start the dev server, open ${ctx.issuer}/login (use this exact origin — passkeys are bound to it), register a user, sign out, and sign in again.
758
-
759
- To change what the login collects or how sign-in works, edit the files under \`.zitadel/schemas/\` and \`.zitadel/flows/\` (each folder has a README), then:
760
-
761
- \`\`\`sh
762
- ${plan}
763
- ${apply}
764
- \`\`\``;
765
- }
766
- /** Full-file header used when `AGENTS.md` does not exist yet. */
767
- const AGENTS_HEADER = `# AGENTS.md
768
-
769
- Guidance for AI agents working in this repository.
770
-
771
- `;
772
- //#endregion
773
- //#region src/lib/orca/patchers/rule/reclaim.ts
774
- /**
775
- * The subset of a patcher plan's operations that `doctor --fix` re-applies:
776
- * env merges, gitignore entries, dependency additions, marker-bearing managed
777
- * files (framework routes/middleware), and the `edit` transforms — the
778
- * `/__nextgen` dev proxy merged into `vite.config`/`nuxt.config`/`angular.json`
779
- * and the Angular `dev` script added to `package.json`. Every `edit` transform
780
- * is idempotent and only adds what is missing (an existing value is left as-is,
781
- * the transform returning the source unchanged), so replaying one restores a
782
- * removed managed block without clobbering the user's own edits. Deliberately
783
- * excludes the unmarked `.zitadel/` resource writes and `zitadel.json` — those
784
- * are user-editable and synced by `apply`, so `--fix` must not clobber them.
785
- *
786
- * Pure: filters a freshly-allocated list; the input plan is not mutated.
787
- */
788
- function reclaimableOps(plan) {
789
- return plan.ops.filter((op) => op.kind === "merge-env" || op.kind === "append-gitignore" || op.kind === "add-dep" || op.kind === "edit" || op.kind === "write" && op.contents.includes("// zitadel-cli: managed-file v1"));
790
- }
791
- /**
792
- * Narrow a reclaimable op list to what a missing-only repair may replay:
793
- * content ops (`write`, and `edit` marked `overwrites`) survive only when
794
- * their target file does not exist, so the repair can restore a deleted
795
- * managed file but can never touch an edited or user-adopted one. Merging
796
- * `edit` transforms and the additive kinds (`merge-env`, `append-gitignore`)
797
- * pass through — they are idempotent and only add what is missing. `add-dep`
798
- * is additive only while the dependency is absent: `addDependency` replaces
799
- * a differing existing version, so under missing-only semantics the op is
800
- * dropped whenever the dependency is already declared at *any* version —
801
- * repairing an unrelated missing file must never rewrite a user-pinned
802
- * range. Reads the filesystem; paths resolve against `cwd` like the
803
- * file-writer's.
804
- */
805
- async function withoutExistingTargets(ops, cwd) {
806
- const kept = [];
807
- for (const op of ops) {
808
- if (op.kind === "write") {
809
- if (!await exists(join(cwd, op.path))) kept.push(op);
810
- continue;
811
- }
812
- if (op.kind === "edit" && op.overwrites) {
813
- const candidates = typeof op.path === "string" ? [op.path] : op.path;
814
- if (!(await Promise.all(candidates.map((candidate) => exists(join(cwd, candidate))))).includes(true)) kept.push(op);
815
- continue;
816
- }
817
- if (op.kind === "add-dep") {
818
- if (!await dependencyDeclared(cwd, op.name)) kept.push(op);
819
- continue;
820
- }
821
- kept.push(op);
822
- }
823
- return kept;
824
- }
825
- /**
826
- * Whether `package.json` already declares `name` in dependencies or
827
- * devDependencies, at any version. A missing or unparsable `package.json`
828
- * counts as "not declared" so the op is kept and the file-writer's own
829
- * error handling reports the real problem.
830
- */
831
- async function dependencyDeclared(cwd, name) {
832
- let contents;
833
- try {
834
- contents = await readFile(join(cwd, "package.json"), "utf8");
835
- } catch {
836
- return false;
837
- }
838
- try {
839
- const pkg = JSON.parse(contents);
840
- if (!isObject(pkg)) return false;
841
- const declared = (key) => isObject(pkg[key]) && name in pkg[key];
842
- return declared("dependencies") || declared("devDependencies");
843
- } catch {
844
- return false;
845
- }
846
- }
847
- async function exists(path) {
848
- try {
849
- await access(path);
850
- return true;
851
- } catch {
852
- return false;
853
- }
854
- }
855
- //#endregion
856
- //#region src/lib/orca/patchers/rule/base.ts
857
- /**
858
- * Base for rule-based (deterministic, template-driven) patchers, as opposed to
859
- * a future LLM-driven family. It applies the integration by building a
860
- * file-operation plan and running the file-writer — that strategy stays
861
- * entirely inside this family, so callers only ever see the family-neutral
862
- * {@link Patcher} surface. Owns the framework-agnostic `.zitadel/` base files
863
- * and the shared eject classification; subclasses contribute only their
864
- * framework-specific routes/middleware.
865
- */
866
- var AbstractRulePatcher = class {
867
- /** Apply the full plan (base `.zitadel/` files + framework routes). */
868
- async patch(ctx, opts) {
869
- return scaffold(this.plan(ctx), opts);
870
- }
871
- /**
872
- * Re-apply only the reclaimable subset — env files, gitignore, the SDK
873
- * dependency, and marker-bearing routes — leaving the user-editable
874
- * `.zitadel/` resources untouched. Backs `doctor --fix`. With
875
- * `opts.missingOnly` the content ops are further narrowed to files that do
876
- * not exist (see {@link withoutExistingTargets}) and the writer runs without
877
- * `force`, so the repair restores deleted managed files and can never
878
- * overwrite an edited or user-adopted one.
879
- */
880
- async repair(ctx, opts) {
881
- const plan = this.plan(ctx);
882
- let ops = opts.missingOnly ? await withoutExistingTargets(reclaimableOps(plan), opts.cwd) : reclaimableOps(plan);
883
- const excluded = new Set(opts.excludePaths ?? []);
884
- if (excluded.size > 0) ops = ops.filter((op) => {
885
- if (op.kind === "write") return !excluded.has(op.path);
886
- if (op.kind === "edit" && op.overwrites) return !(typeof op.path === "string" ? [op.path] : op.path).some((candidate) => excluded.has(candidate));
887
- return true;
888
- });
889
- const execOpts = opts.missingOnly ? {
890
- ...opts,
891
- force: false
892
- } : opts;
893
- return scaffold({
894
- ops,
895
- summary: plan.summary
896
- }, execOpts);
897
- }
898
- /**
899
- * Probe whether the labelled config wirings are still applied, using each
900
- * merge transform's own idempotency: the transforms only add what is
901
- * missing and return the source unchanged when everything they manage is
902
- * present, so a changed output means the wiring is absent from the user's
903
- * config. Every labelled wiring gets a verdict: a missing host file is
904
- * `detached` (the wiring cannot be applied in a file that does not exist),
905
- * and a transform that throws on restructured content yields `unknown` —
906
- * surfaced rather than silently dropped, so a deleted `angular.json` can
907
- * never read as healthy. Read-only.
908
- */
909
- async verify(ctx, opts) {
910
- const statuses = [];
911
- for (const op of this.plan(ctx).ops) {
912
- if (op.kind !== "edit" || !op.wiring) continue;
913
- const candidates = typeof op.path === "string" ? [op.path] : op.path;
914
- let path = candidates[0];
915
- let source;
916
- for (const candidate of candidates) {
917
- const contents = await readTextIfExists(join(opts.cwd, candidate));
918
- if (contents !== void 0) {
919
- path = candidate;
920
- source = contents;
921
- break;
922
- }
923
- }
924
- if (source === void 0) {
925
- statuses.push({
926
- path,
927
- wiring: op.wiring,
928
- state: "detached"
929
- });
930
- continue;
931
- }
932
- try {
933
- statuses.push({
934
- path,
935
- wiring: op.wiring,
936
- state: op.edit(source) === source ? "applied" : "detached"
937
- });
938
- } catch (error) {
939
- statuses.push({
940
- path,
941
- wiring: op.wiring,
942
- state: "unknown",
943
- reason: error instanceof Error ? error.message : "verification failed"
944
- });
945
- }
946
- }
947
- return statuses;
948
- }
949
- /** Shared base artifacts plus the subclass's marker-bearing route files. */
950
- artifacts(view) {
951
- const infrastructure = new Set(this.infrastructureFiles(view));
952
- const markedFiles = this.routeFiles(view);
953
- return {
954
- markedFiles,
955
- rootConfigFiles: ["zitadel.json"],
956
- directories: [".zitadel"],
957
- envBackups: [".env.local"],
958
- dependencies: this.routeDeps(view),
959
- configEdits: this.routeConfigEdits(view),
960
- guidanceFiles: ["AGENTS.md", "README.md"],
961
- fileClasses: Object.fromEntries(markedFiles.map((path) => [path, infrastructure.has(path) ? "infrastructure" : "presentation"])),
962
- conditionalFiles: this.conditionallyScaffoldedFiles(view),
963
- retiredAlternates: this.retiredAlternateFiles(view)
964
- };
965
- }
966
- /**
967
- * Current marked files mapped to sibling paths that older templates wrote
968
- * in their place and the framework rejects alongside them. Drives the
969
- * managed-files boundary migration. Defaults to none; Next overrides it
970
- * for the `middleware.ts`/`proxy.ts` pair.
971
- */
972
- retiredAlternateFiles(_view) {
973
- return {};
974
- }
975
- /**
976
- * The subset of {@link routeFiles} that is load-bearing for the integration
977
- * (request boundary, proxies, plugins, type declarations). The `doctor`
978
- * managed-files check fails when one is missing; everything else is a
979
- * presentation starting point and only warns. Every patcher whose marked
980
- * files include plumbing must override this — Next (boundary/provider/dts),
981
- * Angular (proxy.conf.cjs), and Nuxt (plugins) do. The Vite SPAs' only
982
- * marked file is the root component (presentation); their plumbing lives in
983
- * `vite.config.*`, which is a config-edit merge into a user file and out of
984
- * the file check's reach. Defaults to none.
985
- */
986
- infrastructureFiles(_view) {
987
- return [];
988
- }
989
- /**
990
- * Marked files only written on some scaffolds (e.g. a framework home page
991
- * that setup replaces only when it created the app skeleton itself). The
992
- * managed-files check excludes them when no scaffold manifest recorded what
993
- * was actually written. Defaults to none.
994
- */
995
- conditionallyScaffoldedFiles(_view) {
996
- return [];
997
- }
998
- /**
999
- * User config files this patcher merges into via an `edit` op (e.g.
1000
- * `vite.config.ts`). `eject` can't reverse an in-place merge, so it lists
1001
- * these as manual cleanup steps. Defaults to none; patchers that edit a config
1002
- * (React/Vue/Angular/Nuxt) override it. Next writes whole marker-bearing files
1003
- * instead, so it has none.
1004
- */
1005
- routeConfigEdits(_view) {
1006
- return [];
1007
- }
1008
- /**
1009
- * The full file-operation plan this patcher would apply. Public so rule-family
1010
- * unit tests can assert the planned ops directly; the generic {@link Patcher}
1011
- * interface deliberately does not expose it (an LLM patcher has no such plan).
1012
- */
1013
- plan(ctx) {
1014
- return {
1015
- ops: [...this.baseOps(ctx), ...this.routeOps(ctx)],
1016
- summary: [this.summary(ctx)]
1017
- };
1018
- }
1019
- /**
1020
- * The framework-agnostic `.zitadel/` base files every rule patcher writes:
1021
- * the project secret, `zitadel.json`, env templates, and an empty sync
1022
- * state. The `schemas/` and `flows/` directories are created empty here;
1023
- * setup fills them from versioned local defaults after patching.
1024
- * Pure: no filesystem or network.
1025
- */
1026
- baseOps(ctx) {
1027
- return [
1028
- {
1029
- kind: "mkdir",
1030
- path: ".zitadel",
1031
- mode: 448
1032
- },
1033
- {
1034
- kind: "mkdir",
1035
- path: ".zitadel/flows"
1036
- },
1037
- {
1038
- kind: "mkdir",
1039
- path: ".zitadel/schemas"
1040
- },
1041
- {
1042
- kind: "append-gitignore",
1043
- entries: [
1044
- ".zitadel/secret",
1045
- ".env*",
1046
- "!.env.example"
1047
- ]
1048
- },
1049
- {
1050
- kind: "write",
1051
- path: ".zitadel/secret",
1052
- mode: 384,
1053
- contents: `${stableStringify({
1054
- project_id: ctx.project.id,
1055
- project_secret: ctx.project.project_secret,
1056
- preview_secret: ctx.project.preview_secret,
1057
- preview_origins: ctx.project.preview_origins,
1058
- created_at: ctx.project.created_at
1059
- })}\n`
1060
- },
1061
- {
1062
- kind: "write",
1063
- path: "zitadel.json",
1064
- contents: `${stableStringify(projectConfig(ctx))}\n`
1065
- },
1066
- {
1067
- kind: "merge-env",
1068
- path: ".env.example",
1069
- entries: {
1070
- ZITADEL_PROJECT_ID: "",
1071
- ZITADEL_PROJECT_SECRET: "",
1072
- ZITADEL_ENVIRONMENT: "",
1073
- ZITADEL_ISSUER: "",
1074
- ZITADEL_URL: ""
1075
- }
1076
- },
1077
- {
1078
- kind: "merge-env",
1079
- path: ".env.local",
1080
- entries: {
1081
- ZITADEL_PROJECT_ID: ctx.project.id,
1082
- ZITADEL_PROJECT_SECRET: ctx.project.project_secret,
1083
- ZITADEL_ENVIRONMENT: "development",
1084
- ZITADEL_ISSUER: ctx.issuer,
1085
- ZITADEL_URL: ctx.server
1086
- }
1087
- },
1088
- {
1089
- kind: "write",
1090
- path: ".zitadel/state.json",
1091
- contents: `${stableStringify({
1092
- framework: ctx.framework.id,
1093
- resources: {}
1094
- })}\n`
1095
- },
1096
- {
1097
- kind: "mkdir",
1098
- path: META_SCHEMA_DIR
1099
- },
1100
- ...metaSchemaFiles().map((file) => ({
1101
- kind: "write",
1102
- path: `${META_SCHEMA_DIR}/${file.name}`,
1103
- contents: `${JSON.stringify(normalizePublicCliJson(file.body, ctx.cliVersion), null, 2)}\n`
1104
- })),
1105
- {
1106
- kind: "edit",
1107
- path: "AGENTS.md",
1108
- edit: (source) => upsertGuidanceSection(source, agentsGuidanceSection(ctx), AGENTS_HEADER)
1109
- },
1110
- {
1111
- kind: "edit",
1112
- path: "README.md",
1113
- edit: (source) => upsertGuidanceSection(source, readmeGuidanceSection(ctx), "")
1114
- }
1115
- ];
1116
- }
1117
- };
1118
- /** Reads a text file, mapping every failure to `undefined`. */
1119
- async function readTextIfExists(path) {
1120
- try {
1121
- return await readFile(path, "utf8");
1122
- } catch {
1123
- return;
1124
- }
1125
- }
1126
- /** Builds the `zitadel.json` body persisted at the project root. */
1127
- function projectConfig(ctx) {
1128
- const environments = { development: { issuer: ctx.issuer } };
1129
- if (ctx.project.preview_origins.length > 0) environments.preview = { issuer_pattern: [...ctx.project.preview_origins] };
1130
- return {
1131
- $schema: "https://schemas.zitadel.com/v2/project.schema.json",
1132
- project: ctx.project.id,
1133
- server: resolveServerOrigin(ctx.server),
1134
- framework: { id: ctx.framework.id },
1135
- branding: {
1136
- renderer: ctx.rendererId,
1137
- attribution: "visible"
1138
- },
1139
- environments,
1140
- ...ctx.preset === void 0 ? {} : { preset: ctx.preset },
1141
- ...ctx.useCase === void 0 ? {} : { useCase: ctx.useCase }
1142
- };
1143
- }
1144
- /** Normalizes a server URL to its origin, falling back to {@link DEFAULT_SERVER}. */
1145
- function resolveServerOrigin(source) {
1146
- try {
1147
- return new URL(source).origin;
1148
- } catch {
1149
- return DEFAULT_SERVER;
1150
- }
1151
- }
1152
- //#endregion
1153
- //#region src/lib/orca/patchers/rule/angular/angular-json.ts
1154
- /**
1155
- * Builds the pure `edit` transform the file-writer applies to `angular.json`:
1156
- * wires a dev-server `proxyConfig` (and optional `port`) into the project's
1157
- * `serve` target. The project name is discovered from the file (`defaultProject`,
1158
- * else the sole project) rather than hardcoded, since it varies per app.
1159
- * Idempotent — already-set values are left as-is. Throws `E_VALIDATION` when the
1160
- * file is absent, the project/serve target cannot be located, or the workspace
1161
- * has several projects with no `defaultProject` to disambiguate (rather than
1162
- * guessing and wiring the proxy into an arbitrary one).
1163
- */
1164
- function angularProxyEdit(opts) {
1165
- return (source) => {
1166
- if (source === void 0) throw new ZitadelError("E_VALIDATION", "Cannot wire Angular proxy: angular.json not found", { hint: "Run setup from an Angular project." });
1167
- const root = parseJsonObject(source, "angular.json");
1168
- const projects = isObject(root.projects) ? root.projects : void 0;
1169
- const projectNames = Object.keys(projects ?? {});
1170
- let projectName;
1171
- if (typeof root.defaultProject === "string") projectName = root.defaultProject;
1172
- else if (projectNames.length === 1) projectName = projectNames[0];
1173
- else if (projectNames.length > 1) throw new ZitadelError("E_VALIDATION", "angular.json has multiple projects and no defaultProject to choose from", { hint: `Set "defaultProject" in angular.json, or add "proxyConfig" to the right project's serve target manually. Projects: ${projectNames.join(", ")}.` });
1174
- const project = projects && projectName ? projects[projectName] : void 0;
1175
- if (!isObject(project)) throw new ZitadelError("E_VALIDATION", "No project found in angular.json", { hint: "Add \"proxyConfig\" to your serve target manually." });
1176
- const targets = isObject(project.architect) ? project.architect : isObject(project.targets) ? project.targets : void 0;
1177
- const serve = targets && isObject(targets.serve) ? targets.serve : void 0;
1178
- if (!serve) throw new ZitadelError("E_VALIDATION", "No serve target in angular.json", { hint: "Add \"proxyConfig\" to your serve options manually." });
1179
- const options = isObject(serve.options) ? serve.options : {};
1180
- let changed = false;
1181
- if (options.proxyConfig === void 0) {
1182
- options.proxyConfig = opts.proxyConfig;
1183
- changed = true;
1184
- }
1185
- if (opts.port !== void 0 && options.port === void 0) {
1186
- options.port = opts.port;
1187
- changed = true;
1188
- }
1189
- if (!changed) return source;
1190
- serve.options = options;
1191
- return `${JSON.stringify(root, null, 2)}\n`;
1192
- };
1193
- }
1194
- //#endregion
1195
- //#region src/lib/orca/patchers/rule/utils/magicast.ts
1196
- /**
1197
- * Generic magicast helpers shared by the config-editing patchers (Vite, Nuxt).
1198
- * They navigate a module's default export — they carry no framework knowledge
1199
- * beyond "find the config object literal" and "is this import present".
1200
- */
1201
- /**
1202
- * Parses a config file with magicast, throwing a clean `E_VALIDATION` (instead
1203
- * of a raw parse error) when the source is missing or unparseable. `filename` is
1204
- * only used in the error message, so each patcher can name its own config file.
1205
- */
1206
- function parseConfigModule(source, filename) {
1207
- if (source === void 0) throw new ZitadelError("E_VALIDATION", `Cannot edit ${filename}: file not found`, { hint: `Run setup from a project that has ${filename}.` });
1208
- let mod;
1209
- try {
1210
- mod = parseModule(source);
1211
- } catch (error) {
1212
- throw new ZitadelError("E_VALIDATION", `Could not parse ${filename}`, {
1213
- hint: `Ensure ${filename} is valid, or apply the Zitadel changes manually.`,
1214
- details: { cause: error instanceof Error ? error.message : String(error) }
1215
- });
1216
- }
1217
- if (hasCommonJsExport(mod)) throw new ZitadelError("E_VALIDATION", `${filename} is a CommonJS module, which can't be edited`, { hint: `The Zitadel edits use ESM imports. Convert the config to ESM (a .ts/.mts file, or set "type": "module"), or add the Zitadel block manually.` });
1218
- return mod;
1219
- }
1220
- /**
1221
- * Whether the module has a top-level CommonJS export assignment —
1222
- * `module.exports = …`, `module.exports.x = …`, or `exports.x = …` — read from
1223
- * the parsed AST so comments and string literals can't trigger a false match.
1224
- */
1225
- function hasCommonJsExport(mod) {
1226
- return ((mod?.$ast?.program ?? mod?.$ast)?.body ?? []).some((node) => {
1227
- if (node?.type !== "ExpressionStatement" || node.expression?.type !== "AssignmentExpression") return false;
1228
- const left = node.expression.left;
1229
- if (left?.type !== "MemberExpression") return false;
1230
- const object = left.object;
1231
- if (object?.type === "Identifier" && object.name === "exports") return true;
1232
- if (object?.type === "Identifier" && object.name === "module" && left.property?.name === "exports") return true;
1233
- return object?.type === "MemberExpression" && object.object?.name === "module" && object.property?.name === "exports";
1234
- });
1235
- }
1236
- /**
1237
- * Reaches the object literal of a module's default export — the argument of
1238
- * `export default <call>({...})` (e.g. `defineConfig`/`defineNuxtConfig`) or a
1239
- * bare `export default {...}`. Throws `E_VALIDATION` for shapes magicast cannot
1240
- * safely edit (function-form, configs built elsewhere) so the caller can fall
1241
- * back to manual steps.
1242
- */
1243
- function resolveDefaultExportObject(mod, filename) {
1244
- const def = mod.exports?.default;
1245
- const unreachable = () => new ZitadelError("E_VALIDATION", `Could not locate the config object in ${filename}`, { hint: `Add the Zitadel configuration to ${filename} manually (see the SDK README).` });
1246
- if (!def) throw unreachable();
1247
- if (def.$type === "function-call") {
1248
- const arg = def.$args?.[0];
1249
- if (!arg || arg.$type !== "object") throw unreachable();
1250
- return arg;
1251
- }
1252
- if (def.$type === "object") return def;
1253
- throw unreachable();
1254
- }
1255
- function importIsPresent(mod, local, from) {
1256
- try {
1257
- return (mod.imports?.$items ?? []).some((item) => item.local === local && (from === void 0 || item.from === from));
1258
- } catch {
1259
- return false;
1260
- }
1261
- }
1262
- /**
1263
- * Appends `item` to a string array at `parent[key]`, creating the array when
1264
- * absent and skipping it when already present. Reads the proxified array by
1265
- * index so primitive elements compare as plain values. Returns `true` when it
1266
- * actually added the item, so callers can tell whether the edit changed
1267
- * anything (and skip rewriting an already-complete config).
1268
- */
1269
- function ensureArrayItem(parent, key, item) {
1270
- if (parent[key] === void 0) {
1271
- parent[key] = [item];
1272
- return true;
1273
- }
1274
- const arr = parent[key];
1275
- if (typeof arr?.push !== "function" || typeof arr?.length !== "number") throw new ZitadelError("E_VALIDATION", `Could not add "${item}" to "${key}"`, { hint: `Add "${item}" to "${key}" in your config manually.` });
1276
- if (!Array.from({ length: arr.length }, (_unused, i) => arr[i]).includes(item)) {
1277
- arr.push(item);
1278
- return true;
1279
- }
1280
- return false;
1281
- }
1282
- /**
1283
- * Returns the object literal at `parent[key]`, creating an empty one when
1284
- * absent, so callers can safely descend into it. Throws `E_VALIDATION` when the
1285
- * key already holds something that is not an inline object literal (an
1286
- * identifier, spread, or function call) — magicast cannot edit those, and
1287
- * assigning into them otherwise throws a raw proxy `TypeError`. The object
1288
- * sibling of {@link ensureArrayItem}.
1289
- */
1290
- function ensureEditableObject(parent, key) {
1291
- if (parent[key] === void 0) parent[key] = {};
1292
- const value = parent[key];
1293
- if (value?.$type !== "object") throw new ZitadelError("E_VALIDATION", `Could not edit "${key}" in the config`, { hint: `Set "${key}" to an inline object literal, or add the Zitadel settings manually.` });
1294
- return value;
1295
- }
1296
- //#endregion
1297
- //#region src/lib/orca/patchers/rule/angular/angular-routes.ts
1298
- const AUTH_ROUTE_PATHS = [
1299
- "login",
1300
- "register",
1301
- "profile"
1302
- ];
1303
- /**
1304
- * Angular's default `ng new` app enables the router with an empty route table.
1305
- * That router rejects direct `/login` and `/profile` navigations, then rewrites
1306
- * the URL back to `/`. Add componentless routes for the auth paths so the root
1307
- * component can keep rendering based on `window.location.pathname` without
1308
- * requiring a router outlet.
1309
- */
1310
- function angularRoutesEdit() {
1311
- return (source) => {
1312
- const label = "src/app/app.routes.ts";
1313
- const mod = parseConfigModule(source, label);
1314
- const routes = mod.exports?.routes;
1315
- if (routes?.$type !== "array" || typeof routes.push !== "function") throw new ZitadelError("E_VALIDATION", "Cannot wire Angular auth routes", { hint: `Set "routes" in ${label} to an inline Routes array, or add /login, /register, and /profile manually.` });
1316
- const present = new Set(Array.from({ length: routes.length }, (_unused, index) => routes[index]?.path).filter((path) => typeof path === "string"));
1317
- let changed = false;
1318
- for (const path of AUTH_ROUTE_PATHS) {
1319
- if (present.has(path)) continue;
1320
- routes.push(builders.raw(`{ path: ${JSON.stringify(path)}, children: [] }`));
1321
- changed = true;
1322
- }
1323
- if (!changed && source !== void 0) return source;
1324
- const code = generateCode(mod).code;
1325
- return code.endsWith("\n") ? code : `${code}\n`;
1326
- };
1327
- }
1328
- //#endregion
1329
- //#region src/lib/orca/patchers/rule/proxy.ts
1330
- /**
1331
- * The same-origin path the SDK widgets call (`configureZitadel({ proxyPath })`),
1332
- * which every framework's dev proxy forwards to the backend. Framework-agnostic:
1333
- * Vite (React/Vue), Angular's dev-server proxy, and Nuxt's server middleware all
1334
- * key off the same prefix, so it lives here rather than in any one framework's
1335
- * patcher.
1336
- */
1337
- const PROXY_PATH = "/__nextgen";
1338
- //#endregion
1339
- //#region src/lib/orca/patchers/rule/proxy-credential-policy.ts
1340
- /** Marker emitted after the exchange-only credential policy was introduced. */
1341
- const PROXY_CREDENTIAL_POLICY_MARKER = "zitadel:proxy:v2";
1342
- /**
1343
- * Rejects a browser-reachable proxy that still appears to attach the project
1344
- * secret without the complete exchange-only guard. Known generated legacy
1345
- * bytes are migrated before this runs; anything else stays user-owned and is
1346
- * surfaced for manual review instead of being silently counted as healthy by
1347
- * `zitadel doctor`.
1348
- */
1349
- function assertNoUnreviewedProjectSecretProxy(source, label) {
1350
- if (!source.includes("/__nextgen") || !source.includes("ZITADEL_PROJECT_SECRET") || !/authorization/i.test(source) || hasExchangeOnlyGuard(source)) return;
1351
- throw new ZitadelError("E_VALIDATION", `${label} may forward ZITADEL_PROJECT_SECRET outside POST /sessions/exchange`, { hint: "Review the proxy manually: restrict the bearer to the exact POST /sessions/exchange pathname and preserve caller-provided authorization." });
1352
- }
1353
- function hasExchangeOnlyGuard(source) {
1354
- const setterOffsets = [...source.matchAll(/proxyReq\s*\.\s*setHeader\s*\(\s*["']authorization["']\s*,/gi)].map((match) => match.index);
1355
- if (setterOffsets.length === 0) return false;
1356
- const safeBodyRanges = [];
1357
- for (const match of source.matchAll(/if\s*\(([\s\S]*?)\)\s*\{([\s\S]*?)\}/g)) {
1358
- const condition = match[1];
1359
- const body = match[2];
1360
- if (!/proxyReq\s*\.\s*method\s*===\s*["']POST["']/i.test(condition) || !/pathname\s*===\s*["']\/sessions\/exchange["']/i.test(condition) || !/!\s*proxyReq\s*\.\s*getHeader\s*\(\s*["']authorization["']\s*\)/i.test(condition) || !/proxyReq\s*\.\s*setHeader\s*\(\s*["']authorization["']\s*,/i.test(body)) continue;
1361
- const bodyOffset = match[0].lastIndexOf(body);
1362
- const start = match.index + bodyOffset;
1363
- safeBodyRanges.push({
1364
- start,
1365
- end: start + body.length
1366
- });
1367
- }
1368
- return setterOffsets.every((offset) => safeBodyRanges.some(({ start, end }) => offset >= start && offset < end));
1369
- }
1370
- //#endregion
1371
- //#region src/lib/orca/patchers/rule/angular/templates.ts
1372
- /**
1373
- * The managed root component `src/app/app.ts`: a standalone component that
1374
- * renders the `@zitadel/sdk-angular` widgets based on the current path. The
1375
- * project id (public, not secret) is inlined; the dev proxy in `proxy.conf.cjs`
1376
- * attaches the project service-key secret as the bearer server-side only to
1377
- * `POST /sessions/exchange` (read from `.env.local`), and no secret reaches
1378
- * the browser.
1379
- *
1380
- * Projects set up with the business use case additionally expose the SDK's
1381
- * `businessLocales` overlay as a class property, which `app.html` binds to the
1382
- * login widgets via `[locales]` — restoring work-email copy on top of the
1383
- * widget's neutral built-in dictionaries.
1384
- */
1385
- function appComponentTemplate(ctx) {
1386
- const business = ctx.useCase === "business";
1387
- const importNames = business ? `
1388
- ZitadelLoginComponent,
1389
- ZitadelSessionComponent,
1390
- businessLocales,
1391
- configureZitadel,
1392
- ` : `
1393
- ZitadelLoginComponent,
1394
- ZitadelSessionComponent,
1395
- configureZitadel,
1396
- `;
1397
- const localesProperty = business ? `
1398
- // Set up for a business audience: businessLocales overlays work-email copy
1399
- // on the login widget's neutral built-in dictionaries. Remove it (and the
1400
- // [locales] binding in app.html) to fall back to the neutral wording.
1401
- protected readonly locales = businessLocales;` : "";
1402
- return `${MANAGED_MARKER}
1403
- import { Component, OnInit } from "@angular/core";
1404
- import {${importNames}} from "@zitadel/sdk-angular";
1405
-
1406
- @Component({
1407
- selector: "app-root",
1408
- standalone: true,
1409
- imports: [ZitadelLoginComponent, ZitadelSessionComponent],
1410
- templateUrl: "./app.html",
1411
- })
1412
- export class App implements OnInit {
1413
- protected readonly project = configureZitadel({
1414
- projectId: ${JSON.stringify(ctx.project.id)},
1415
- proxyPath: "${PROXY_PATH}",
1416
- });
1417
- protected readonly path = window.location.pathname;${localesProperty}
1418
-
1419
- ngOnInit(): void {
1420
- if (this.path === "/") {
1421
- window.location.replace("/login");
1422
- }
1423
- }
1424
- }
1425
- `;
1426
- }
1427
- /**
1428
- * The managed `src/app/app.html`. The marker lives in an HTML comment that still
1429
- * contains the literal managed-marker text, so eject/doctor stay marker-aware.
1430
- * Business-use-case scaffolds bind the component's `locales` overlay property
1431
- * onto the login widgets.
1432
- */
1433
- function appTemplateHtml(ctx) {
1434
- const localesBinding = ctx.useCase === "business" ? `\n [locales]="locales"` : "";
1435
- return `<!-- ${MANAGED_MARKER} -->
1436
- @if (path.startsWith('/profile')) {
1437
- <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
1438
- <zitadel-auth-session
1439
- [project]="project"
1440
- [postSignOutUrl]="'/login'"
1441
- ></zitadel-auth-session>
1442
- </div>
1443
- } @else if (path.startsWith('/register')) {
1444
- <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
1445
- <zitadel-auth-login
1446
- [project]="project"${localesBinding}
1447
- purpose="register"
1448
- [postSignInUrl]="'/profile'"
1449
- ></zitadel-auth-login>
1450
- </div>
1451
- } @else {
1452
- <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
1453
- <zitadel-auth-login
1454
- [project]="project"${localesBinding}
1455
- purpose="login"
1456
- [postSignInUrl]="'/profile'"
1457
- ></zitadel-auth-login>
1458
- </div>
1459
- }
1460
- `;
1461
- }
1462
- /**
1463
- * The managed `proxy.conf.cjs` for `ng serve`: forwards `/__nextgen/*` to the
1464
- * backend and attaches the project's service-key secret only to the current
1465
- * app-plane `POST /sessions/exchange` request. Both the backend URL
1466
- * (`ZITADEL_URL`) and the secret
1467
- * (`ZITADEL_PROJECT_SECRET`) are read from `.env.local`, which `zitadel setup`
1468
- * writes and `.gitignore` excludes — Angular's CLI does not auto-load env files
1469
- * into the dev-server process, so this file does it itself with a small inline
1470
- * parser. The prefix strip and the bearer are each provided in both the
1471
- * http-proxy-middleware form (`pathRewrite`/`onProxyReq`) and the Vite form
1472
- * (`rewrite`/`configure`), so both fire whichever proxy layer Angular's
1473
- * dev server uses.
1474
- */
1475
- function proxyConfTemplate() {
1476
- return `${MANAGED_MARKER}
1477
- const { readFileSync, existsSync } = require("node:fs");
1478
-
1479
- function loadEnvLocal() {
1480
- if (!existsSync(".env.local")) return {};
1481
- const out = {};
1482
- for (const line of readFileSync(".env.local", "utf8").split(/\\r?\\n/)) {
1483
- const m = line.match(/^\\s*(?:export\\s+)?([A-Z_][A-Z0-9_]*)\\s*=\\s*(.*)$/);
1484
- if (m) {
1485
- const raw = m[2].trim();
1486
- const quoted = raw.match(/^(['"])(.*)\\1\\s*(?:#.*)?$/);
1487
- out[m[1]] = quoted ? quoted[2] : raw.replace(/\\s+#.*$/, "").trim();
1488
- }
1489
- }
1490
- return out;
1491
- }
1492
-
1493
- const env = loadEnvLocal();
1494
- const server = process.env.ZITADEL_URL ?? env.ZITADEL_URL;
1495
- const secret = process.env.ZITADEL_PROJECT_SECRET ?? env.ZITADEL_PROJECT_SECRET;
1496
- if (!server) {
1497
- throw new Error("ZITADEL_URL is not set; add it to .env.local (zitadel setup writes it).");
1498
- }
1499
- if (!secret) {
1500
- throw new Error("ZITADEL_PROJECT_SECRET is not set; add it to .env.local (zitadel setup writes it).");
1501
- }
1502
- const bearer = \`Bearer \${secret}\`;
1503
-
1504
- ${EXCHANGE_ONLY_BEARER}
1505
-
1506
- function stripPrefix(path) {
1507
- return path.replace(/^\\${PROXY_PATH}/, "").replace(/^(?!\\/)/, "/");
1508
- }
1509
-
1510
- module.exports = {
1511
- "${PROXY_PATH}": {
1512
- target: server,
1513
- changeOrigin: false,
1514
- pathRewrite: stripPrefix,
1515
- rewrite: stripPrefix,
1516
- onProxyReq: setBearer,
1517
- configure: (proxy) => proxy.on("proxyReq", setBearer),
1518
- },
1519
- };
1520
- `;
1521
- }
1522
- const LEGACY_UNCONDITIONAL_BEARER = `function setBearer(proxyReq) {
1523
- proxyReq.setHeader("authorization", bearer);
1524
- }`;
1525
- const EXCHANGE_ONLY_BEARER = `/* ${PROXY_CREDENTIAL_POLICY_MARKER} */
1526
- function setBearer(proxyReq) {
1527
- const pathname = new URL(proxyReq.path, "http://zitadel.local").pathname;
1528
- if (
1529
- proxyReq.method === "POST" &&
1530
- pathname === "/sessions/exchange" &&
1531
- !proxyReq.getHeader("authorization")
1532
- ) {
1533
- proxyReq.setHeader("authorization", bearer);
1534
- }
1535
- }`;
1536
- /**
1537
- * Creates the managed Angular proxy and upgrades the exact insecure hook the
1538
- * CLI emitted before the exchange-only policy. Other edits are preserved; a
1539
- * safe custom proxy is left untouched, while an unrecognized proxy that may
1540
- * still over-forward the project secret is surfaced for manual review.
1541
- */
1542
- function proxyConfEdit() {
1543
- return (source) => {
1544
- if (source === void 0) return proxyConfTemplate();
1545
- if (source.includes("// zitadel-cli: managed-file v1") && source.includes(LEGACY_UNCONDITIONAL_BEARER)) return source.replace(LEGACY_UNCONDITIONAL_BEARER, EXCHANGE_ONLY_BEARER);
1546
- assertNoUnreviewedProjectSecretProxy(source, "proxy.conf.cjs");
1547
- return source;
1548
- };
1549
- }
1550
- //#endregion
1551
- //#region src/lib/orca/patchers/rule/angular/index.ts
1552
- const SDK_DEPENDENCY$6 = "@zitadel/sdk-angular";
1553
- /**
1554
- * Adds a `dev: "ng serve"` script only when the project does not already define
1555
- * one. `ng new` ships only a `start` script, but the CLI tells every framework
1556
- * to run `npm run dev` (and `ng serve` reads the proxy + port from
1557
- * `angular.json`). Non-destructive: an existing `dev` script is preserved, so
1558
- * patching a project that already wires its own `dev` leaves it untouched.
1559
- */
1560
- function ensureDevScript(source) {
1561
- if (source === void 0) throw new ZitadelError("E_VALIDATION", "package.json is required to add the dev script", { hint: "Run setup from a project that has a package.json." });
1562
- const pkg = parseJsonObject(source, "package.json");
1563
- const scripts = isObject(pkg.scripts) ? pkg.scripts : void 0;
1564
- if (scripts?.dev !== void 0) return source;
1565
- return setTopLevelJsonKey(source, "package.json", "scripts", {
1566
- ...scripts ?? {},
1567
- dev: "ng serve"
1568
- });
1569
- }
1570
- /**
1571
- * Rule-based patcher for an Angular app. Inherits the shared `.zitadel/` base
1572
- * files from {@link AbstractRulePatcher} and contributes the managed root
1573
- * component (`app.ts`/`app.html`) that renders the `@zitadel/sdk-angular`
1574
- * widgets, a `proxy.conf.cjs` dev proxy (attaching the project secret from
1575
- * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`) wired into `angular.json`,
1576
- * and the SDK dep.
1577
- *
1578
- * Unlike React/Vue (whose dev proxy lives in `vite.config.ts`), Angular owns its
1579
- * Vite config, so the proxy is a separate `proxy.conf.cjs` referenced from the
1580
- * `serve` target. In production the same-origin path comes from a platform
1581
- * rewrite or minimal worker (ADR 036); CLI scaffolding for it is tracked in
1582
- * issue #560.
1583
- */
1584
- var AngularPatcher = class extends AbstractRulePatcher {
1585
- canPatch(framework) {
1586
- return framework === "angular";
1587
- }
1588
- routeOps(ctx) {
1589
- return [
1590
- {
1591
- kind: "write",
1592
- path: "src/app/app.ts",
1593
- contents: appComponentTemplate(ctx)
1594
- },
1595
- {
1596
- kind: "write",
1597
- path: "src/app/app.html",
1598
- contents: appTemplateHtml(ctx)
1599
- },
1600
- {
1601
- kind: "edit",
1602
- path: "src/app/app.routes.ts",
1603
- edit: angularRoutesEdit(),
1604
- wiring: "infrastructure"
1605
- },
1606
- {
1607
- kind: "edit",
1608
- path: "proxy.conf.cjs",
1609
- edit: proxyConfEdit(),
1610
- wiring: "infrastructure"
1611
- },
1612
- {
1613
- kind: "edit",
1614
- path: "angular.json",
1615
- edit: angularProxyEdit({
1616
- proxyConfig: "proxy.conf.cjs",
1617
- port: ctx.framework.devPort
1618
- }),
1619
- wiring: "infrastructure"
1620
- },
1621
- {
1622
- kind: "edit",
1623
- path: "package.json",
1624
- edit: ensureDevScript,
1625
- wiring: "convenience"
1626
- },
1627
- {
1628
- kind: "add-dep",
1629
- name: SDK_DEPENDENCY$6,
1630
- version: npmDistTagForCliVersion(ctx.cliVersion)
1631
- }
1632
- ];
1633
- }
1634
- routeFiles(_view) {
1635
- return [
1636
- "src/app/app.ts",
1637
- "src/app/app.html",
1638
- "proxy.conf.cjs"
1639
- ];
1640
- }
1641
- infrastructureFiles(_view) {
1642
- return ["proxy.conf.cjs"];
1643
- }
1644
- routeDeps(_view) {
1645
- return [SDK_DEPENDENCY$6];
1646
- }
1647
- routeConfigEdits(_view) {
1648
- return [
1649
- "angular.json",
1650
- "src/app/app.routes.ts",
1651
- "package.json"
1652
- ];
1653
- }
1654
- summary(_ctx) {
1655
- return {
1656
- title: "Angular integration",
1657
- detail: "Wrote the app root component + proxy.conf.cjs, added auth routes, and wired the /__nextgen dev proxy into angular.json."
1658
- };
1659
- }
1660
- };
1661
- //#endregion
1662
- //#region src/lib/orca/patchers/rule/dev-script-port.ts
1663
- /**
1664
- * Pins the `dev` script to the port setup registered as the project's origin.
1665
- *
1666
- * Setup allows exactly one origin, `http://localhost:<devPort>`, so the dev
1667
- * server has to land on that port or the flow API rejects the app mid-login
1668
- * (the first step renders, the first submit 400s with `origin ... is not
1669
- * allowed for this project`). Every other framework this CLI patches already
1670
- * guarantees that in its own dev-server config — Vite gets `server.port` plus
1671
- * `strictPort`, Angular gets `serve.options.port` — but `next dev` and
1672
- * `nuxt dev` take their port from the command line, so nothing held them to
1673
- * it: they default to 3000 regardless of the port setup registered.
1674
- *
1675
- * For Next the flag also restores the guarantee `strictPort` gives Vite: bare
1676
- * `next dev` silently serves 3001 when 3000 is taken (measured), while
1677
- * `next dev --port N` exits with `EADDRINUSE` instead of drifting onto a port
1678
- * the project does not allow.
1679
- *
1680
- * Non-destructive: a script already on the right port is returned unchanged
1681
- * (so the op skips the file rather than reformatting a user's package.json),
1682
- * and a project without a `dev` script is left alone because there is no
1683
- * command to pin.
1684
- *
1685
- * Takes the project's registered `issuer` rather than a port, because the
1686
- * expected value has to survive the app drifting away from it. `doctor`
1687
- * rebuilds this plan from `loadPatchContext`, whose `framework.devPort` is
1688
- * freshly *detected* from the current script — deriving the target from that
1689
- * would make any edited script verify against itself (always "applied") and
1690
- * would make `doctor --fix` write back the detected fallback instead of the
1691
- * port the project actually allows. The issuer comes from `zitadel.json`
1692
- * (falling back to the scaffold manifest), so it still says 3456 after
1693
- * someone edits the script to 4000. An issuer that names no port yields no
1694
- * expectation and the op becomes a no-op.
1695
- */
1696
- function devScriptPortEdit(issuer) {
1697
- return (source) => {
1698
- if (source === void 0) throw new ZitadelError("E_VALIDATION", "package.json is required to pin the dev port", { hint: "Run setup from a project that has a package.json." });
1699
- const devPort = portFromIssuer(issuer);
1700
- if (devPort === void 0) return source;
1701
- const pkg = parseJsonObject(source, "package.json");
1702
- const scripts = isObject(pkg.scripts) ? pkg.scripts : void 0;
1703
- const dev = scripts?.dev;
1704
- if (typeof dev !== "string" || dev.trim() === "") return source;
1705
- if (extractPort(dev) === devPort) return source;
1706
- return setTopLevelJsonKey(source, "package.json", "scripts", {
1707
- ...scripts,
1708
- dev: withDevPort(dev, devPort)
1709
- });
1710
- };
1711
- }
1712
- /**
1713
- * The `package.json` op pinning the dev script's port. Marked as managed
1714
- * wiring so `doctor` reports a dev script that drifted off the registered
1715
- * origin — `convenience` (warn, not fail) because the script is only one of
1716
- * the ways a dev server picks its port: `npm run dev -- --port 4000` still
1717
- * moves it without touching the file, so a hard failure here would promise a
1718
- * guarantee the check cannot make.
1719
- */
1720
- function devScriptPortOp(issuer) {
1721
- return {
1722
- kind: "edit",
1723
- path: "package.json",
1724
- edit: devScriptPortEdit(issuer),
1725
- wiring: "convenience"
1726
- };
1727
- }
1728
- //#endregion
1729
- //#region src/lib/orca/patchers/rule/next/renderers/lit/index.ts
1730
- /**
1731
- * Placeholder renderer for the `<zitadel-flow>` Lit web component. Declared
1732
- * so the `web-component` renderer id resolves and surfaces a clear
1733
- * "not yet published" error, while reserving the integration shape for when
1734
- * `@zitadel/ui-lit` ships. The `authPage` template emits an illustrative
1735
- * page only; this renderer is never selected for real scaffolding because
1736
- * `getRenderer` rejects any `status: "not-implemented"` spec.
1737
- */
1738
- const litRenderer = {
1739
- id: "web-component",
1740
- displayName: "Web component (<zitadel-flow>)",
1741
- status: "not-implemented",
1742
- frameworks: [
1743
- "next",
1744
- "astro",
1745
- "remix",
1746
- "sveltekit",
1747
- "nuxt",
1748
- "vanilla"
1749
- ],
1750
- dependency: {
1751
- name: "@zitadel/ui-lit",
1752
- version: "workspace:*"
1753
- },
1754
- templates: { authPage(mode) {
1755
- return {
1756
- mode,
1757
- contents: `${MANAGED_MARKER}
1758
- // The web component renderer ships a <zitadel-flow> element. Until
1759
- // @zitadel/ui-lit is published, this template only declares the
1760
- // intended integration point. See docs/design/cli/bdui-renderer.md.
1761
- import "@zitadel/ui-lit";
1762
-
1763
- const environment =
1764
- process.env.ZITADEL_ENVIRONMENT ??
1765
- (process.env.NODE_ENV === "production" ? "production" : "development");
1766
-
1767
- export default function ${mode === "login" ? "LoginPage" : "RegisterPage"}() {
1768
- return (
1769
- <zitadel-flow
1770
- purpose="${mode === "login" ? "login" : "register"}"
1771
- project-id={process.env.ZITADEL_PROJECT_ID}
1772
- issuer={process.env.ZITADEL_ISSUER}
1773
- environment={environment}
1774
- />
1775
- );
1776
- }
1777
- `
1778
- };
1779
- } }
1780
- };
1781
- //#endregion
1782
- //#region src/lib/orca/patchers/rule/next/renderers/react/index.ts
1783
- /**
1784
- * The Next.js App Router renderer scaffolds `/login`, `/register`, and
1785
- * `/profile` pages that drive the `<zitadel-login>` and `<zitadel-session>`
1786
- * Lit web components. `/profile` is the post-sign-in "signed in as" card.
1787
- *
1788
- * Each page is a single client component (`"use client"`) that, inside a
1789
- * `next/dynamic({ ssr: false })` loader, builds the SDK project handle with
1790
- * `configureZitadel({ projectId, proxyPath: "/__nextgen" })` and passes it to
1791
- * the widget via `project={...}`. It also imports
1792
- * `@zitadel/sdk-next/client` for its `customElements.define`
1793
- * side-effect — importing `@zitadel/components` directly would fail on
1794
- * strict-resolution package managers (pnpm, yarn PnP) because the app only
1795
- * declares `sdk-next` as a direct dep. SSR is disabled because Lit's element
1796
- * registration needs a browser.
1797
- *
1798
- * The handle is passed as the `project` DOM property, which relies on React
1799
- * 19's custom-element property binding (the scaffold targets the latest Next /
1800
- * React). The backend URL never reaches the browser: the client talks to the
1801
- * same-origin `/__nextgen` proxy path, and the scaffolded Next request boundary
1802
- * forwards it to `ZITADEL_URL` server-side. `NEXT_PUBLIC_ZITADEL_PROJECT_ID` is
1803
- * public — the project id is not sensitive and the widget needs it to start a
1804
- * flow.
1805
- *
1806
- * Projects set up with the business use case additionally wire the SDK's
1807
- * `businessLocales` overlay onto the auth pages, restoring work-email copy on
1808
- * top of the widget's neutral built-in dictionaries. The overlay is assigned
1809
- * via a ref callback, not a JSX prop: sdk-next supports React >=18, and only
1810
- * React 19 binds non-primitive custom-element props as properties — on 18 a
1811
- * `locales={...}` prop would decay to a useless attribute and silently keep
1812
- * the neutral copy (the `project` prop tolerates this because
1813
- * `configureZitadel()` registers a global fallback; `locales` has none).
1814
- */
1815
- const reactRenderer = {
1816
- id: "react",
1817
- displayName: "React (Next.js App Router)",
1818
- status: "available",
1819
- frameworks: ["next"],
1820
- dependency: {
1821
- name: "@zitadel/sdk-next",
1822
- version: "latest"
1823
- },
1824
- templates: {
1825
- authPage(mode, context) {
1826
- const componentName = mode === "login" ? "LoginPage" : "RegisterPage";
1827
- const elementName = mode === "login" ? "ZitadelLogin" : "ZitadelRegister";
1828
- const business = context.useCase === "business";
1829
- const importNames = business ? "businessLocales, configureZitadel" : "configureZitadel";
1830
- const localesComment = business ? `
1831
- // Set up for a business audience: businessLocales overlays work-email
1832
- // copy on the widget's neutral built-in dictionaries. It is assigned
1833
- // through the ref because React binds non-primitive custom-element
1834
- // props as properties only from v19 — the ref works on React 18 too.
1835
- // Remove the assignment to fall back to the neutral wording.` : "";
1836
- const localesAttr = business ? `
1837
- ref={(element) => {
1838
- if (element) {
1839
- element.locales = businessLocales;
1840
- }
1841
- }}` : "";
1842
- const widget = context.posture === "widget";
1843
- return {
1844
- mode,
1845
- contents: `${MANAGED_MARKER}
1846
- "use client";
1847
-
1848
- import dynamic from "next/dynamic";
1849
-
1850
- const ${elementName} = dynamic(
1851
- async () => {
1852
- const { ${importNames} } = await import("@zitadel/sdk-next/client");
1853
- // Build the SDK project handle and pass it to the component via the
1854
- // \`project\` prop. The component reads config from this prop directly, so
1855
- // it works regardless of how the SDK packages are bundled. The backend URL
1856
- // stays server-side: requests go through the proxy path "/__nextgen",
1857
- // which the scaffolded request boundary forwards to the Zitadel server.
1858
- const project = configureZitadel({
1859
- projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
1860
- proxyPath: "/__nextgen",
1861
- });
1862
- return function ${elementName}Element() {${localesComment}
1863
- return (
1864
- <zitadel-login
1865
- ${widget ? `variant="widget"\n theme="auto"` : `variant="page"`}${localesAttr}
1866
- project={project}
1867
- purpose="${mode}"
1868
- post-sign-in-url="/profile"
1869
- />
1870
- );
1871
- };
1872
- },
1873
- { ssr: false },
1874
- );
1875
-
1876
- export default function ${componentName}() {
1877
- ${widget ? ` // variant="widget" embeds the sign-in card in this app's existing layout.
1878
- // theme="auto" follows the OS light/dark preference (prefers-color-scheme),
1879
- // not this app's own theme — set theme="light" or theme="dark" to match an
1880
- // app that pins its scheme. The wrapper below only centers the card —
1881
- // restyle or replace it freely. Switch to variant="page" for the widget's
1882
- // own full-page chrome (viewport height, surface background from design
1883
- // tokens).
1884
- // If this page already carries its own heading, add suppress-header to the
1885
- // element to visually hide the widget's heading block — it stays in the
1886
- // accessibility tree, and the card keeps no blank header band.` : ` // variant="page" makes the widget paint the full-page chrome itself
1887
- // (viewport height, surface background) from design tokens; switch to
1888
- // variant="widget" to embed the sign-in card inside a layout you own.`}
1889
- return (
1890
- ${widget ? `<div style={{ display: "flex", justifyContent: "center", padding: "4rem 1rem" }}>` : `<main style={{ colorScheme: "dark" }}>`}
1891
- <${elementName} />
1892
- ${widget ? `</div>` : `</main>`}
1893
- );
1894
- }
1895
- `
1896
- };
1897
- },
1898
- profilePage(context) {
1899
- const widget = context.posture === "widget";
1900
- return { contents: `${MANAGED_MARKER}
1901
- "use client";
1902
-
1903
- import dynamic from "next/dynamic";
1904
-
1905
- const ZitadelSession = dynamic(
1906
- async () => {
1907
- const { configureZitadel } = await import("@zitadel/sdk-next/client");
1908
- // Build the SDK project handle and pass it to the session card via the
1909
- // \`project\` prop. The card reads identity from "/__nextgen/sessions/me"
1910
- // and exposes a Sign out action. Your own components (a header, an
1911
- // account menu) can make the same read with getSession() from
1912
- // "@zitadel/sdk-next/session" to swap sign-in CTAs for account chrome.
1913
- const project = configureZitadel({
1914
- projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
1915
- proxyPath: "/__nextgen",
1916
- });
1917
- return function ZitadelSessionElement() {
1918
- return (
1919
- <zitadel-session
1920
- ${widget ? `variant="widget"\n theme="auto"` : `variant="page"`}
1921
- project={project}
1922
- post-sign-out-url="/login"
1923
- />
1924
- );
1925
- };
1926
- },
1927
- { ssr: false },
1928
- );
1929
-
1930
- export default function ProfilePage() {
1931
- ${widget ? ` // variant="widget" embeds the session card in this app's existing layout.
1932
- // theme="auto" follows the OS light/dark preference (prefers-color-scheme),
1933
- // not this app's own theme — set theme="light" or theme="dark" to match an
1934
- // app that pins its scheme. The wrapper below only centers the card —
1935
- // restyle or replace it freely. Switch to variant="page" for the card's
1936
- // own full-page chrome (viewport height, surface background from design
1937
- // tokens).
1938
- // If this page already carries its own heading, add suppress-header to the
1939
- // element to visually hide the widget's heading block — it stays in the
1940
- // accessibility tree, and the card keeps no blank header band.` : ` // variant="page" makes the session card paint the full-page chrome itself
1941
- // (viewport height, surface background) from design tokens; switch to
1942
- // variant="widget" to embed the card inside a layout you own.`}
1943
- return (
1944
- ${widget ? `<div style={{ display: "flex", justifyContent: "center", padding: "4rem 1rem" }}>` : `<main style={{ colorScheme: "dark" }}>`}
1945
- <ZitadelSession />
1946
- ${widget ? `</div>` : `</main>`}
1947
- );
1948
- }
1949
- ` };
1950
- },
1951
- customElementsDts() {
1952
- return { contents: `${MANAGED_MARKER}
1953
- // React JSX declarations for the <zitadel-*> custom elements ship with the
1954
- // SDK, so the scaffold stays aligned with the real element surface instead
1955
- // of carrying a hand-maintained copy that drifts.
1956
- /// <reference types="@zitadel/sdk-next/jsx" />
1957
- ` };
1958
- }
1959
- }
1960
- };
1961
- //#endregion
1962
- //#region src/lib/orca/patchers/rule/next/renderers/registry.ts
1963
- /**
1964
- * Runtime mirror of the {@link RendererId} union, used by {@link isRendererId}
1965
- * to validate untrusted strings (a TS union has no runtime presence). Must stay
1966
- * in sync with the {@link RendererId} type.
1967
- */
1968
- const RENDERER_IDS = ["react", "web-component"];
1969
- /**
1970
- * Type guard narrowing an arbitrary value to a {@link RendererId}, used to
1971
- * validate renderer ids read from config before indexing {@link RENDERERS}.
1972
- */
1973
- function isRendererId(value) {
1974
- return typeof value === "string" && RENDERER_IDS.includes(value);
1975
- }
1976
- /**
1977
- * The single source of truth mapping each {@link RendererId} to its spec.
1978
- * Keyed by id so {@link getRenderer} can look up and validate a renderer
1979
- * chosen from persisted config (an arbitrary string) at runtime.
1980
- */
1981
- const RENDERERS = {
1982
- react: reactRenderer,
1983
- "web-component": litRenderer
1984
- };
1985
- /**
1986
- * The subset of {@link RENDERER_IDS} that {@link getRenderer} resolves rather
1987
- * than rejects (`status: "available"`). Anything advertising renderer choices
1988
- * — flag options, error hints — must derive from this list, not
1989
- * {@link RENDERER_IDS}: a declared-but-unpublished renderer (ADR 006) keeps
1990
- * its registry entry to reserve the id, but must never be offered as a usable
1991
- * value.
1992
- */
1993
- const AVAILABLE_RENDERER_IDS = RENDERER_IDS.filter((id) => RENDERERS[id].status === "available");
1994
- /**
1995
- * Resolves a renderer id (an untrusted string from config) to its spec,
1996
- * throwing a typed {@link ZitadelError} rather than returning `undefined`
1997
- * so callers get an actionable message. Rejects ids that are unknown
1998
- * (`E_VALIDATION`) or declared-but-unpublished (`E_NOT_IMPLEMENTED`),
1999
- * guaranteeing the returned spec is safe to scaffold from.
2000
- */
2001
- function getRenderer(id) {
2002
- if (!isRendererId(id)) throw new ZitadelError("E_VALIDATION", `Unknown renderer "${id}"`, { hint: `Available renderers: ${AVAILABLE_RENDERER_IDS.join(", ")}` });
2003
- const renderer = RENDERERS[id];
2004
- if (renderer.status === "not-implemented") throw new ZitadelError("E_NOT_IMPLEMENTED", `Renderer "${id}" is declared but not yet published`, { hint: "Use --renderer react for now; the <zitadel-flow> web component ships in a later package." });
2005
- return renderer;
2006
- }
2007
- //#endregion
2008
- //#region src/lib/orca/patchers/rule/next/index.ts
2009
- /**
2010
- * Next.js request-boundary file at the project root. Wires `nextgenMiddleware` so the
2011
- * generated project config's `/__nextgen` proxy path is same-origin proxied
2012
- * to `ZITADEL_URL` and `/profile` is gated. Next 16 renamed this convention to
2013
- * `proxy.ts`; older projects keep `middleware.ts`.
2014
- * Carries the managed marker so `doctor --fix` reclaims it and `eject` removes it.
2015
- */
2016
- function requestBoundaryTemplate(functionName) {
2017
- return `${MANAGED_MARKER}
2018
- import { nextgenMiddleware } from "@zitadel/sdk-next/middleware";
2019
- import type { NextRequest } from "next/server";
2020
-
2021
- export function ${functionName}(req: NextRequest) {
2022
- return nextgenMiddleware(req, {
2023
- url: process.env.ZITADEL_URL,
2024
- protectedRoutes: ["/profile"],
2025
- loginPath: "/login",
2026
- });
2027
- }
2028
-
2029
- export const config = {
2030
- matcher: ["/__nextgen/:path*", "/profile/:path*"],
2031
- };
2032
- `;
2033
- }
2034
- /**
2035
- * Rule-based patcher for the Next.js App Router. Inherits the shared
2036
- * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
2037
- * Next routes and request boundary whose templates come from the chosen renderer.
2038
- */
2039
- var NextPatcher = class extends AbstractRulePatcher {
2040
- /** Returns true for Next.js projects. */
2041
- canPatch(framework) {
2042
- return framework === "next";
2043
- }
2044
- routeOps(ctx) {
2045
- return nextCodeOps(ctx, getRenderer(ctx.rendererId));
2046
- }
2047
- routeFiles(view) {
2048
- return nextCodeFilePaths(view.framework, getRenderer(view.rendererId));
2049
- }
2050
- infrastructureFiles(view) {
2051
- return nextInfrastructureFilePaths(view.framework, getRenderer(view.rendererId));
2052
- }
2053
- conditionallyScaffoldedFiles(view) {
2054
- return [join(view.framework.appDir, "page.tsx")];
2055
- }
2056
- retiredAlternateFiles(view) {
2057
- if (requestBoundaryFile(view.framework).filename !== "proxy.ts") return {};
2058
- return { [join(view.framework.appDir, "../proxy.ts")]: [join(view.framework.appDir, "../middleware.ts")] };
2059
- }
2060
- routeDeps(view) {
2061
- return [getRenderer(view.rendererId).dependency.name];
2062
- }
2063
- routeConfigEdits(_view) {
2064
- return ["package.json"];
2065
- }
2066
- summary(ctx) {
2067
- return {
2068
- title: "Next.js integration",
2069
- detail: `Scaffolded login/register/profile routes with renderer "${ctx.rendererId}".`
2070
- };
2071
- }
2072
- };
2073
- /**
2074
- * Ordered paths of the framework code files the patcher writes. All carry the
2075
- * managed marker. Shared by {@link NextPatcher.routeOps} (which adds contents)
2076
- * and {@link NextPatcher.routeFiles} (which only needs the paths) so the two
2077
- * cannot drift.
2078
- */
2079
- function nextCodeFilePaths(framework, renderer) {
2080
- const appDir = framework.appDir;
2081
- const paths = [
2082
- join(appDir, "page.tsx"),
2083
- join(appDir, "login/page.tsx"),
2084
- join(appDir, "register/page.tsx")
2085
- ];
2086
- if (renderer.templates.profilePage) paths.push(join(appDir, "profile/page.tsx"));
2087
- paths.push(join(appDir, `../${requestBoundaryFile(framework).filename}`));
2088
- if (renderer.templates.provider) paths.push(join(appDir, renderer.templates.provider.filename));
2089
- if (renderer.templates.customElementsDts) paths.push(join(appDir, "../custom-elements.d.ts"));
2090
- return paths;
2091
- }
2092
- /**
2093
- * The subset of {@link nextCodeFilePaths} that is load-bearing for the auth
2094
- * integration rather than a presentation starting point: the request boundary
2095
- * (proxy/middleware), the provider file, and the custom-elements declarations.
2096
- * Shares path construction with {@link nextCodeFilePaths} so the two cannot
2097
- * drift; the `doctor` managed-files check fails when one of these is missing.
2098
- */
2099
- function nextInfrastructureFilePaths(framework, renderer) {
2100
- const appDir = framework.appDir;
2101
- const paths = [join(appDir, `../${requestBoundaryFile(framework).filename}`)];
2102
- if (renderer.templates.provider) paths.push(join(appDir, renderer.templates.provider.filename));
2103
- if (renderer.templates.customElementsDts) paths.push(join(appDir, "../custom-elements.d.ts"));
2104
- return paths;
2105
- }
2106
- /** The Next route/request-boundary write ops plus the SDK dependency. */
2107
- function nextCodeOps(ctx, renderer) {
2108
- const appDir = ctx.framework.appDir;
2109
- const pageContext = {
2110
- useCase: ctx.useCase,
2111
- posture: ctx.posture
2112
- };
2113
- const profile = renderer.templates.profilePage?.(pageContext);
2114
- const provider = renderer.templates.provider;
2115
- const dts = renderer.templates.customElementsDts?.();
2116
- const boundary = requestBoundaryFile(ctx.framework);
2117
- return [
2118
- ctx.scaffoldedFramework ? {
2119
- kind: "edit",
2120
- path: join(appDir, "page.tsx"),
2121
- edit: () => homePageTemplate(),
2122
- overwrites: true
2123
- } : void 0,
2124
- {
2125
- kind: "write",
2126
- path: join(appDir, "login/page.tsx"),
2127
- contents: renderer.templates.authPage("login", pageContext).contents
2128
- },
2129
- {
2130
- kind: "write",
2131
- path: join(appDir, "register/page.tsx"),
2132
- contents: renderer.templates.authPage("register", pageContext).contents
2133
- },
2134
- profile ? {
2135
- kind: "write",
2136
- path: join(appDir, "profile/page.tsx"),
2137
- contents: profile.contents
2138
- } : void 0,
2139
- {
2140
- kind: "write",
2141
- path: join(appDir, `../${boundary.filename}`),
2142
- contents: requestBoundaryTemplate(boundary.functionName)
2143
- },
2144
- provider ? {
2145
- kind: "write",
2146
- path: join(appDir, provider.filename),
2147
- contents: provider.contents
2148
- } : void 0,
2149
- dts ? {
2150
- kind: "write",
2151
- path: join(appDir, "../custom-elements.d.ts"),
2152
- contents: dts.contents
2153
- } : void 0,
2154
- {
2155
- kind: "merge-env",
2156
- path: ".env.example",
2157
- entries: { NEXT_PUBLIC_ZITADEL_PROJECT_ID: "" }
2158
- },
2159
- {
2160
- kind: "merge-env",
2161
- path: ".env.local",
2162
- entries: { NEXT_PUBLIC_ZITADEL_PROJECT_ID: ctx.project.id }
2163
- },
2164
- {
2165
- kind: "add-dep",
2166
- name: renderer.dependency.name,
2167
- version: dependencyVersionForCli(ctx.cliVersion, renderer.dependency.version)
2168
- },
2169
- devScriptPortOp(ctx.issuer)
2170
- ].filter((op) => op !== void 0);
2171
- }
2172
- function homePageTemplate() {
2173
- return `${MANAGED_MARKER}
2174
- import { redirect } from "next/navigation";
2175
-
2176
- export default function Home() {
2177
- redirect("/login");
2178
- }
2179
- `;
2180
- }
2181
- function requestBoundaryFile(framework) {
2182
- if ((framework.versionMajor ?? 0) >= 16) return {
2183
- filename: "proxy.ts",
2184
- functionName: "proxy"
2185
- };
2186
- return {
2187
- filename: "middleware.ts",
2188
- functionName: "middleware"
2189
- };
2190
- }
2191
- function dependencyVersionForCli(cliVersion, fallback) {
2192
- const normalized = cliVersion.trim().replace(/^v/, "");
2193
- if (/^\d+\.\d+\.\d+-alpha\.\d+$/.test(normalized)) return normalized;
2194
- return normalized.match(/^\d+\.\d+\.\d+-([0-9A-Za-z]+)(?:[.-]|$)/)?.[1] ?? fallback;
2195
- }
2196
- //#endregion
2197
- //#region src/lib/orca/patchers/rule/config-paths.ts
2198
- /**
2199
- * The module extensions the config edits can actually write, in resolution
2200
- * priority. magicast injects ESM `import`/`import.meta.url`, so only ESM-capable
2201
- * extensions are editable; CommonJS (`cts`/`cjs`) is not.
2202
- */
2203
- const CONFIG_EXTENSIONS = [
2204
- "ts",
2205
- "mts",
2206
- "js",
2207
- "mjs"
2208
- ];
2209
- /**
2210
- * The CommonJS extensions we still *probe* (after the ESM ones) so a project
2211
- * whose only config is `*.cjs`/`*.cts` is found and rejected with a targeted
2212
- * "CommonJS is unsupported" error, rather than a misleading "file not found".
2213
- * {@link parseConfigModule} surfaces that error when it sees CommonJS source.
2214
- */
2215
- const COMMONJS_EXTENSIONS = ["cts", "cjs"];
2216
- /**
2217
- * Candidate config filenames for `basename`, ESM extensions first then the
2218
- * CommonJS ones, e.g. `configCandidates("vite.config")` → `["vite.config.ts",
2219
- * "vite.config.mts", "vite.config.js", "vite.config.mjs", "vite.config.cts",
2220
- * "vite.config.cjs"]`. Handed to the
2221
- * generic `edit` file-op, which patches the first one that exists — an ESM
2222
- * config wins, and a CommonJS-only project is still read so the edit can emit a
2223
- * clear unsupported-format error.
2224
- */
2225
- function configCandidates(basename) {
2226
- return [...CONFIG_EXTENSIONS, ...COMMONJS_EXTENSIONS].map((ext) => `${basename}.${ext}`);
2227
- }
2228
- //#endregion
2229
- //#region src/lib/orca/patchers/rule/nuxt/nuxt-config.ts
2230
- const NUXT_MODULE = "@zitadel/sdk-nuxt/module";
2231
- /**
2232
- * Builds the pure `edit` transform the file-writer applies to the project's Nuxt
2233
- * config (`nuxt.config.*`): registers the `@zitadel/sdk-nuxt` module (which wires
2234
- * the server-side proxy + session middleware), sets the login path, seeds
2235
- * `runtimeConfig` with the backend URL, the proxy path, and the project id, and
2236
- * marks the `zitadel-*` Lit elements as custom elements for the Vue compiler —
2237
- * preserving the user's existing config via magicast. Idempotent. Throws
2238
- * `E_VALIDATION` when the file is absent or `defineNuxtConfig` cannot be reached.
2239
- */
2240
- function nuxtConfigEdit(opts) {
2241
- return (source) => {
2242
- const label = "the Nuxt config (nuxt.config.*)";
2243
- const mod = parseConfigModule(source, label);
2244
- const config = resolveDefaultExportObject(mod, label);
2245
- let changed = ensureArrayItem(config, "modules", NUXT_MODULE);
2246
- const nextgen = ensureEditableObject(config, "nextgen");
2247
- if (nextgen.url === void 0) {
2248
- nextgen.url = builders.raw(`process.env.ZITADEL_URL ?? ${JSON.stringify(opts.server)}`);
2249
- changed = true;
2250
- }
2251
- if (nextgen.loginPath === void 0) {
2252
- nextgen.loginPath = "/login";
2253
- changed = true;
2254
- }
2255
- if (nextgen.protectedRoutes === void 0) {
2256
- nextgen.protectedRoutes = ["/profile"];
2257
- changed = true;
2258
- }
2259
- const runtimeConfig = ensureEditableObject(config, "runtimeConfig");
2260
- if (runtimeConfig.zitadelUrl === void 0) {
2261
- runtimeConfig.zitadelUrl = builders.raw(`process.env.ZITADEL_URL ?? ${JSON.stringify(opts.server)}`);
2262
- changed = true;
2263
- }
2264
- const publicConfig = ensureEditableObject(runtimeConfig, "public");
2265
- if (publicConfig.nextgenProxyPath === void 0) {
2266
- publicConfig.nextgenProxyPath = PROXY_PATH;
2267
- changed = true;
2268
- }
2269
- if (publicConfig.zitadelProjectId === void 0) {
2270
- publicConfig.zitadelProjectId = builders.raw(`process.env.NUXT_PUBLIC_ZITADEL_PROJECT_ID ?? ${JSON.stringify(opts.projectId)}`);
2271
- changed = true;
2272
- }
2273
- const build = ensureEditableObject(config, "build");
2274
- for (const dep of [
2275
- "@zitadel/api",
2276
- "@zitadel/components",
2277
- "@zitadel/design-tokens"
2278
- ]) if (ensureArrayItem(build, "transpile", dep)) changed = true;
2279
- const compilerOptions = ensureEditableObject(ensureEditableObject(config, "vue"), "compilerOptions");
2280
- if (compilerOptions.isCustomElement === void 0) {
2281
- compilerOptions.isCustomElement = builders.raw(`(tag) => tag.startsWith("zitadel-")`);
2282
- changed = true;
2283
- }
2284
- if (!changed && source !== void 0) return source;
2285
- const code = generateCode(mod).code;
2286
- return code.endsWith("\n") ? code : `${code}\n`;
2287
- };
2288
- }
2289
- //#endregion
2290
- //#region src/lib/orca/patchers/rule/nuxt/templates.ts
2291
- /** `app.vue` — renders the page router. Marker in an HTML comment. */
2292
- function appVueTemplate() {
2293
- return `<!-- ${MANAGED_MARKER} -->
2294
- <template>
2295
- <NuxtPage />
2296
- </template>
2297
-
2298
- <style>
2299
- :root {
2300
- color-scheme: dark;
2301
- }
2302
- body {
2303
- margin: 0;
2304
- background: #0f0f11;
2305
- color: #f4f4f6;
2306
- font-family: sans-serif;
2307
- }
2308
- </style>
2309
- `;
2310
- }
2311
- /** `pages/index.vue` — redirects the app root to `/login`. */
2312
- function indexPageTemplate() {
2313
- return `<script setup lang="ts">
2314
- ${MANAGED_MARKER}
2315
- await navigateTo("/login", { replace: true });
2316
- <\/script>
2317
- `;
2318
- }
2319
- /**
2320
- * A login/register page rendering `<zitadel-login>` inside `<ClientOnly>`.
2321
- * Business-use-case projects additionally bind the SDK's `businessLocales`
2322
- * overlay, restoring work-email copy on top of the widget's neutral built-in
2323
- * dictionaries. A plain `:locales` binding suffices even on the raw custom
2324
- * element: Vue sets bindings whose key exists on the element as DOM
2325
- * properties (unlike React 18, which is why the Next template needs a ref).
2326
- */
2327
- function authPage(purpose, ctx) {
2328
- const business = ctx.useCase === "business";
2329
- const importNames = business ? "businessLocales, useZitadelProject" : "useZitadelProject";
2330
- const localesComment = business ? `
2331
- // Set up for a business audience: businessLocales overlays work-email copy on
2332
- // the login widget's neutral built-in dictionaries. Remove the :locales
2333
- // binding to fall back to the neutral wording.` : "";
2334
- const localesAttr = business ? "\n :locales=\"businessLocales\"" : "";
2335
- const purposeAttr = purpose === "register" ? "\n purpose=\"register\"" : "";
2336
- const widget = ctx.posture === "widget";
2337
- return `<script setup lang="ts">
2338
- ${MANAGED_MARKER}
2339
- import { ${importNames} } from "@zitadel/sdk-nuxt";${localesComment}
2340
-
2341
- const project = useZitadelProject();
2342
- <\/script>
2343
-
2344
- <template>
2345
- ${widget ? "<div style=\"display: flex; justify-content: center; padding: 4rem 1rem\">" : "<main style=\"color-scheme: dark\">"}
2346
- <ClientOnly>
2347
- ${widget ? `<!-- variant="widget" embeds the card in this app's own layout.
2348
- theme="auto" follows the OS light/dark preference
2349
- (prefers-color-scheme), not the app's own theme — set
2350
- theme="light" or theme="dark" to match an app that pins its
2351
- scheme. variant="page" would paint the widget's full-page
2352
- chrome instead. -->` : `<!-- variant="page" paints the widget's full-page chrome from design
2353
- tokens; variant="widget" embeds the card inside a layout you own. -->`}
2354
- <zitadel-login
2355
- ${widget ? "variant=\"widget\"\n theme=\"auto\"" : "variant=\"page\""}
2356
- :project="project"${localesAttr}${purposeAttr}
2357
- post-sign-in-url="/profile"
2358
- />
2359
- </ClientOnly>
2360
- ${widget ? "</div>" : "</main>"}
2361
- </template>
2362
- `;
2363
- }
2364
- function loginPageTemplate(ctx) {
2365
- return authPage("login", ctx);
2366
- }
2367
- function registerPageTemplate(ctx) {
2368
- return authPage("register", ctx);
2369
- }
2370
- /** `pages/profile.vue` — the post-sign-in "signed in as" session card. */
2371
- function profilePageTemplate(ctx) {
2372
- const widget = ctx.posture === "widget";
2373
- const variantAttrs = widget ? "variant=\"widget\"\n theme=\"auto\"" : "variant=\"page\"";
2374
- const postureComment = widget ? `<!-- variant="widget" embeds the session card in this app's own layout.
2375
- theme="auto" follows the OS light/dark preference
2376
- (prefers-color-scheme), not the app's own theme — set
2377
- theme="light" or theme="dark" to match an app that pins its
2378
- scheme. variant="page" would paint the card's full-page chrome
2379
- instead. Your own components (a header, an account menu) read
2380
- the same session state with the auto-imported useAuth()
2381
- composable. -->` : `<!-- variant="page" paints the session card's full-page chrome from design
2382
- tokens; variant="widget" embeds the card inside a layout you own.
2383
- Your own components (a header, an account menu) read the same session
2384
- state with the auto-imported useAuth() composable. -->`;
2385
- const wrapperOpen = widget ? "<div style=\"display: flex; justify-content: center; padding: 4rem 1rem\">" : "<main style=\"color-scheme: dark\">";
2386
- const wrapperClose = widget ? "</div>" : "</main>";
2387
- return `<script setup lang="ts">
2388
- ${MANAGED_MARKER}
2389
- import { useZitadelProject } from "@zitadel/sdk-nuxt";
2390
-
2391
- const project = useZitadelProject();
2392
- <\/script>
2393
-
2394
- <template>
2395
- ${wrapperOpen}
2396
- <ClientOnly>
2397
- ${postureComment}
2398
- ${widget ? `<zitadel-session
2399
- ${variantAttrs}
2400
- :project="project"
2401
- post-sign-out-url="/login"
2402
- />` : `<zitadel-session variant="page" :project="project" post-sign-out-url="/login" />`}
2403
- </ClientOnly>
2404
- ${wrapperClose}
2405
- </template>
2406
- `;
2407
- }
2408
- /** `plugins/zitadel-components.client.ts` — register the Lit elements client-side. */
2409
- function componentsPluginTemplate() {
2410
- return `${MANAGED_MARKER}
2411
- // Register Lit custom elements on the client only. Importing @zitadel/components
2412
- // from a page <script setup> would run during SSR and break the widgets.
2413
- import "@zitadel/components";
2414
-
2415
- export default defineNuxtPlugin(() => {});
2416
- `;
2417
- }
2418
- /** `plugins/auth.server.ts` — seed the client auth state from the server context. */
2419
- function authPluginTemplate() {
2420
- return `${MANAGED_MARKER}
2421
- import { defineNuxtPlugin, useRequestEvent, useState } from "#imports";
2422
- import type { ClientAuthResult } from "@zitadel/sdk-nuxt";
2423
-
2424
- export default defineNuxtPlugin(() => {
2425
- const event = useRequestEvent();
2426
- const auth = event?.context.nextgenAuth ?? {
2427
- isAuthenticated: false as const,
2428
- session: null,
2429
- };
2430
-
2431
- // Strip the raw JWT before seeding useState — it must not appear in the SSR
2432
- // payload where client-side scripts could read it.
2433
- const clientAuth: ClientAuthResult = auth.isAuthenticated
2434
- ? {
2435
- isAuthenticated: true,
2436
- session: {
2437
- userId: auth.session.userId,
2438
- identifier: auth.session.identifier,
2439
- identifierProperty: auth.session.identifierProperty,
2440
- display: auth.session.display,
2441
- },
2442
- }
2443
- : { isAuthenticated: false, session: null };
2444
-
2445
- useState<ClientAuthResult>("nextgen-auth", () => clientAuth);
2446
- });
2447
- `;
2448
- }
2449
- //#endregion
2450
- //#region src/lib/orca/patchers/rule/nuxt/index.ts
2451
- const SDK_DEPENDENCY$5 = "@zitadel/sdk-nuxt";
2452
- const NUXT_CONFIG_PATHS = configCandidates("nuxt.config");
2453
- /**
2454
- * Rule-based patcher for a Nuxt app. Like Next.js, Nuxt proxies the backend and
2455
- * verifies the session through server middleware — here the `@zitadel/sdk-nuxt`
2456
- * module, registered via a non-destructive `nuxt.config.*` edit. Contributes
2457
- * the login/register/profile pages (the raw `<zitadel-login>`/`<zitadel-logout>`
2458
- * elements), the client/server plugins, the `app.vue` router, and the SDK dep.
2459
- */
2460
- var NuxtPatcher = class extends AbstractRulePatcher {
2461
- canPatch(framework) {
2462
- return framework === "nuxt";
2463
- }
2464
- routeOps(ctx) {
2465
- const src = (rel) => join(ctx.framework.appDir, rel);
2466
- return [
2467
- ...ctx.scaffoldedFramework ? [{
2468
- kind: "write",
2469
- path: src("app.vue"),
2470
- contents: appVueTemplate()
2471
- }, {
2472
- kind: "write",
2473
- path: src("pages/index.vue"),
2474
- contents: indexPageTemplate()
2475
- }] : [],
2476
- {
2477
- kind: "write",
2478
- path: src("pages/login.vue"),
2479
- contents: loginPageTemplate(ctx)
2480
- },
2481
- {
2482
- kind: "write",
2483
- path: src("pages/register.vue"),
2484
- contents: registerPageTemplate(ctx)
2485
- },
2486
- {
2487
- kind: "write",
2488
- path: src("pages/profile.vue"),
2489
- contents: profilePageTemplate(ctx)
2490
- },
2491
- {
2492
- kind: "write",
2493
- path: src("plugins/zitadel-components.client.ts"),
2494
- contents: componentsPluginTemplate()
2495
- },
2496
- {
2497
- kind: "write",
2498
- path: src("plugins/auth.server.ts"),
2499
- contents: authPluginTemplate()
2500
- },
2501
- {
2502
- kind: "edit",
2503
- path: [...NUXT_CONFIG_PATHS],
2504
- edit: nuxtConfigEdit({
2505
- projectId: ctx.project.id,
2506
- server: ctx.server
2507
- }),
2508
- wiring: "infrastructure"
2509
- },
2510
- {
2511
- kind: "merge-env",
2512
- path: ".env.example",
2513
- entries: { NUXT_PUBLIC_ZITADEL_PROJECT_ID: "" }
2514
- },
2515
- {
2516
- kind: "merge-env",
2517
- path: ".env.local",
2518
- entries: { NUXT_PUBLIC_ZITADEL_PROJECT_ID: ctx.project.id }
2519
- },
2520
- {
2521
- kind: "add-dep",
2522
- name: SDK_DEPENDENCY$5,
2523
- version: npmDistTagForCliVersion(ctx.cliVersion)
2524
- },
2525
- devScriptPortOp(ctx.issuer)
2526
- ];
2527
- }
2528
- routeFiles(view) {
2529
- const src = (rel) => join(view.framework.appDir, rel);
2530
- return [
2531
- src("app.vue"),
2532
- src("pages/index.vue"),
2533
- src("pages/login.vue"),
2534
- src("pages/register.vue"),
2535
- src("pages/profile.vue"),
2536
- src("plugins/zitadel-components.client.ts"),
2537
- src("plugins/auth.server.ts")
2538
- ];
2539
- }
2540
- infrastructureFiles(view) {
2541
- const src = (rel) => join(view.framework.appDir, rel);
2542
- return [src("plugins/zitadel-components.client.ts"), src("plugins/auth.server.ts")];
2543
- }
2544
- conditionallyScaffoldedFiles(view) {
2545
- const src = (rel) => join(view.framework.appDir, rel);
2546
- return [src("app.vue"), src("pages/index.vue")];
2547
- }
2548
- routeDeps(_view) {
2549
- return [SDK_DEPENDENCY$5];
2550
- }
2551
- routeConfigEdits(_view) {
2552
- return ["nuxt.config.*", "package.json"];
2553
- }
2554
- summary(_ctx) {
2555
- return {
2556
- title: "Nuxt integration",
2557
- detail: "Wrote login/register/profile pages + plugins and registered @zitadel/sdk-nuxt in nuxt.config.*."
2558
- };
2559
- }
2560
- };
2561
- //#endregion
2562
- //#region src/lib/orca/patchers/rule/vite-support.ts
2563
- /**
2564
- * Shared Vite dev-server proxy merged into the project's Vite config for the SPA
2565
- * frameworks (React, Vue, Solid, Svelte, Qwik). It forwards same-origin
2566
- * `/__nextgen/*` calls to the
2567
- * backend, strips the prefix, and attaches the project's service-key secret
2568
- * (read from `ZITADEL_PROJECT_SECRET` in `.env.local`) only to the current
2569
- * app-plane `POST /sessions/exchange` operation. The secret stays server-side:
2570
- * Vite only exposes vars with the configured `envPrefix` (default `VITE_`) to
2571
- * client bundles, so this server-only key never leaks into the browser.
2572
- */
2573
- function proxyEntryCode(server) {
2574
- return `/* ${PROXY_CREDENTIAL_POLICY_MARKER} */ {
2575
- target: ${JSON.stringify(server)},
2576
- changeOrigin: false,
2577
- rewrite: (path) => path.replace(/^\\${PROXY_PATH}/, "").replace(/^(?!\\/)/, "/"),
2578
- configure: (proxy) => {
2579
- const secret = loadEnv("development", process.cwd(), "ZITADEL_").ZITADEL_PROJECT_SECRET;
2580
- if (!secret) {
2581
- throw new Error("ZITADEL_PROJECT_SECRET is not set; add it to .env.local (zitadel setup writes it).");
2582
- }
2583
- const bearer = \`Bearer \${secret}\`;
2584
- proxy.on("proxyReq", (proxyReq) => {
2585
- const pathname = new URL(proxyReq.path, "http://zitadel.local").pathname;
2586
- if (
2587
- proxyReq.method === "POST" &&
2588
- pathname === "/sessions/exchange" &&
2589
- !proxyReq.getHeader("authorization")
2590
- ) {
2591
- proxyReq.setHeader("authorization", bearer);
2592
- }
2593
- });
2594
- },
2595
- }`;
2596
- }
2597
- const LEGACY_PROXY_REQUEST_LISTENER = /(^[\t ]*)proxy\.on\("proxyReq", \(proxyReq\) => \{\r?\n\1 {2}proxyReq\.setHeader\("authorization", bearer\);\r?\n\1\}\);/m;
2598
- function exchangeOnlyProxyRequestListener(indent) {
2599
- return `${indent}/* ${PROXY_CREDENTIAL_POLICY_MARKER} */
2600
- ${indent}proxy.on("proxyReq", (proxyReq) => {
2601
- ${indent} const pathname = new URL(proxyReq.path, "http://zitadel.local").pathname;
2602
- ${indent} if (
2603
- ${indent} proxyReq.method === "POST" &&
2604
- ${indent} pathname === "/sessions/exchange" &&
2605
- ${indent} !proxyReq.getHeader("authorization")
2606
- ${indent} ) {
2607
- ${indent} proxyReq.setHeader("authorization", bearer);
2608
- ${indent} }
2609
- ${indent}});`;
2610
- }
2611
- /**
2612
- * Identifies the exact proxy shape emitted before the exchange-only credential
2613
- * policy. Setup may replace that generated listener, but must preserve the
2614
- * surrounding `/__nextgen` proxy and any user changes elsewhere in the entry.
2615
- */
2616
- function isLegacyManagedProxy(source) {
2617
- if (source.includes("zitadel:proxy:v2")) return false;
2618
- return [
2619
- "loadEnv(\"development\", process.cwd(), \"ZITADEL_\").ZITADEL_PROJECT_SECRET",
2620
- "throw new Error(\"ZITADEL_PROJECT_SECRET is not set; add it to .env.local (zitadel setup writes it).\")",
2621
- "const bearer = `Bearer ${secret}`;"
2622
- ].every((fingerprint) => source.includes(fingerprint)) && LEGACY_PROXY_REQUEST_LISTENER.test(source);
2623
- }
2624
- /** The imports that the injected proxy entry depends on. */
2625
- const PROXY_IMPORTS = [{
2626
- from: "vite",
2627
- imported: "loadEnv",
2628
- local: "loadEnv"
2629
- }];
2630
- /**
2631
- * Builds the pure `edit` transform the file-writer applies to the project's Vite
2632
- * config (`vite.config.*`): a non-destructive magicast merge that adds the
2633
- * `/__nextgen` proxy and sets `server.port`/`strictPort` when they are unset,
2634
- * preserving the user's plugins, options, and formatting. Leaves `server.host`
2635
- * alone so the user can still opt into network binding (`--host`/`host: true`);
2636
- * the issuer/origin requirement is about the port, not the bind host. Idempotent
2637
- * — entries already present are left as-is. Throws `E_VALIDATION` when the file
2638
- * is absent or the config object cannot be reached (function-built/exotic
2639
- * configs), with a hint to add the block manually.
2640
- */
2641
- function viteProxyEdit(devPort, server) {
2642
- return (source) => {
2643
- const label = "the Vite config (vite.config.*)";
2644
- let workingSource = source;
2645
- if (workingSource !== void 0 && isLegacyManagedProxy(workingSource)) workingSource = workingSource.replace(LEGACY_PROXY_REQUEST_LISTENER, (_listener, indent) => exchangeOnlyProxyRequestListener(indent));
2646
- else if (workingSource !== void 0) assertNoUnreviewedProjectSecretProxy(workingSource, label);
2647
- const mod = parseConfigModule(workingSource, label);
2648
- const config = resolveDefaultExportObject(mod, label);
2649
- let changed = workingSource !== source;
2650
- const serverConfig = ensureEditableObject(config, "server");
2651
- if (serverConfig.port === void 0) {
2652
- serverConfig.port = devPort;
2653
- changed = true;
2654
- }
2655
- if (serverConfig.strictPort === void 0) {
2656
- serverConfig.strictPort = true;
2657
- changed = true;
2658
- }
2659
- const proxyConfig = ensureEditableObject(serverConfig, "proxy");
2660
- if (proxyConfig["/__nextgen"] === void 0) {
2661
- proxyConfig[PROXY_PATH] = builders.raw(proxyEntryCode(server));
2662
- changed = true;
2663
- }
2664
- for (const imp of PROXY_IMPORTS) if (!importIsPresent(mod, imp.local, imp.from)) {
2665
- mod.imports.$add({ ...imp });
2666
- changed = true;
2667
- }
2668
- if (!changed && source !== void 0) return source;
2669
- const code = generateCode(mod).code;
2670
- return code.endsWith("\n") ? code : `${code}\n`;
2671
- };
2672
- }
2673
- /**
2674
- * Candidate Vite config filenames, in resolution priority. The patcher hands
2675
- * this list to the generic `edit` file-op, which patches the first one that
2676
- * exists — so any project layout (`vite.config.ts`, `.mts`, `.js`, …) is covered.
2677
- */
2678
- const VITE_CONFIG_PATHS = configCandidates("vite.config");
2679
- /** Builds the shared Vite-config proxy {@link FileOp} for a {@link ViteSupport} patcher. */
2680
- function buildViteProxyOp(devPort, server) {
2681
- return {
2682
- kind: "edit",
2683
- path: [...VITE_CONFIG_PATHS],
2684
- edit: viteProxyEdit(devPort, server),
2685
- wiring: "infrastructure"
2686
- };
2687
- }
2688
- //#endregion
2689
- //#region src/lib/orca/patchers/rule/qwik/templates.ts
2690
- /**
2691
- * The managed `src/app.tsx`: a minimal path-based router that renders the
2692
- * `@zitadel/sdk-qwik` widgets — login at `/login`, register at `/register`, and
2693
- * the signed-in session card at `/profile`. The root path redirects to `/login`.
2694
- * Exports a named `App` (`component$`) to match the create-vite Qwik entry
2695
- * (`main.tsx` imports `{ App }`). The project id comes from
2696
- * `VITE_ZITADEL_PROJECT_ID`. No secret reaches the browser: the dev proxy in
2697
- * `vite.config.*` attaches the project service-key secret (from
2698
- * `ZITADEL_PROJECT_SECRET`) server-side only to `POST /sessions/exchange`.
2699
- *
2700
- * Projects set up with the business use case additionally pass the SDK's
2701
- * `businessLocales` overlay to the login widgets, restoring work-email copy on
2702
- * top of the widget's neutral built-in dictionaries. A plain `locales` prop
2703
- * suffices here: the wrapper component assigns it as a DOM property internally.
2704
- */
2705
- function appTemplate$4(ctx) {
2706
- const business = ctx.useCase === "business";
2707
- const importNames = business ? "ZitadelLogin, ZitadelSession, businessLocales, configureZitadel" : "ZitadelLogin, ZitadelSession, configureZitadel";
2708
- const localesComment = business ? `
2709
-
2710
- // Set up for a business audience: businessLocales overlays work-email copy on
2711
- // the login widget's neutral built-in dictionaries. Remove the locales prop to
2712
- // fall back to the neutral wording.` : "";
2713
- const localesAttr = business ? " locales={businessLocales}" : "";
2714
- return `${MANAGED_MARKER}
2715
- import { component$, useVisibleTask$ } from "@builder.io/qwik";
2716
- import { ${importNames} } from "@zitadel/sdk-qwik";${localesComment}
2717
-
2718
- const project = configureZitadel({
2719
- projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
2720
- proxyPath: "${PROXY_PATH}",
2721
- });
2722
-
2723
- export const App = component$(() => {
2724
- const path = window.location.pathname;
2725
-
2726
- useVisibleTask$(() => {
2727
- if (path === "/") {
2728
- window.location.replace("/login");
2729
- }
2730
- });
2731
-
2732
- if (path === "/") {
2733
- return null;
2734
- }
2735
- if (path.startsWith("/profile")) {
2736
- return (
2737
- <div style={{ position: "fixed", inset: "0", overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
2738
- <ZitadelSession project={project} postSignOutUrl="/login" />
2739
- </div>
2740
- );
2741
- }
2742
- if (path.startsWith("/register")) {
2743
- return (
2744
- <div style={{ position: "fixed", inset: "0", overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
2745
- <ZitadelLogin project={project}${localesAttr} purpose="register" postSignInUrl="/profile" />
2746
- </div>
2747
- );
2748
- }
2749
- return (
2750
- <div style={{ position: "fixed", inset: "0", overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
2751
- <ZitadelLogin project={project}${localesAttr} purpose="login" postSignInUrl="/profile" />
2752
- </div>
2753
- );
2754
- });
2755
- `;
2756
- }
2757
- //#endregion
2758
- //#region src/lib/orca/patchers/rule/qwik/index.ts
2759
- const SDK_DEPENDENCY$4 = "@zitadel/sdk-qwik";
2760
- /**
2761
- * Rule-based patcher for a Vite + Qwik single-page app. Inherits the shared
2762
- * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
2763
- * managed `src/app.tsx` auth entry, a non-destructive `vite.config.*` merge
2764
- * that adds the `/__nextgen` dev proxy (attaching the project secret from
2765
- * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
2766
- * project id, and the SDK dep.
2767
- *
2768
- * The create-vite Qwik template uses a lowercase `src/app.tsx` exporting a named
2769
- * `App` (mounted by `main.tsx`), so this patcher writes that exact entry. Unlike
2770
- * Next.js — whose middleware runs the proxy server-side — a SPA has no server,
2771
- * so the dev proxy provides the same-origin `/__nextgen` path locally. In
2772
- * production that path comes from a platform rewrite or minimal worker
2773
- * (ADR 036); CLI scaffolding for it is tracked in issue #560.
2774
- */
2775
- var QwikPatcher = class extends AbstractRulePatcher {
2776
- canPatch(framework) {
2777
- return framework === "qwik";
2778
- }
2779
- viteProxyOp(devPort, server) {
2780
- return buildViteProxyOp(devPort, server);
2781
- }
2782
- routeOps(ctx) {
2783
- return [
2784
- {
2785
- kind: "write",
2786
- path: "src/app.tsx",
2787
- contents: appTemplate$4(ctx)
2788
- },
2789
- this.viteProxyOp(ctx.framework.devPort, ctx.server),
2790
- {
2791
- kind: "merge-env",
2792
- path: ".env.example",
2793
- entries: { VITE_ZITADEL_PROJECT_ID: "" }
2794
- },
2795
- {
2796
- kind: "merge-env",
2797
- path: ".env.local",
2798
- entries: { VITE_ZITADEL_PROJECT_ID: ctx.project.id }
2799
- },
2800
- {
2801
- kind: "add-dep",
2802
- name: SDK_DEPENDENCY$4,
2803
- version: npmDistTagForCliVersion(ctx.cliVersion)
2804
- }
2805
- ];
2806
- }
2807
- routeFiles(_view) {
2808
- return ["src/app.tsx"];
2809
- }
2810
- routeDeps(_view) {
2811
- return [SDK_DEPENDENCY$4];
2812
- }
2813
- routeConfigEdits(_view) {
2814
- return ["vite.config.*"];
2815
- }
2816
- summary(_ctx) {
2817
- return {
2818
- title: "Qwik (Vite) integration",
2819
- detail: "Wrote src/app.tsx auth entry and merged the /__nextgen dev proxy into vite.config.*."
2820
- };
2821
- }
2822
- };
2823
- //#endregion
2824
- //#region src/lib/orca/patchers/rule/react/templates.ts
2825
- /**
2826
- * The managed `src/App.tsx`: a minimal path-based router that renders the
2827
- * `@zitadel/sdk-react` widgets — login at `/login`, register at `/register`, and
2828
- * the signed-in session card at `/profile`. The root path redirects to `/login`.
2829
- * The project id comes from `VITE_ZITADEL_PROJECT_ID` (Vite only exposes
2830
- * `VITE_`-prefixed env to the client). No secret reaches the browser: the dev
2831
- * proxy in `vite.config.*` attaches the project service-key secret (from
2832
- * `ZITADEL_PROJECT_SECRET`) server-side only to `POST /sessions/exchange`.
2833
- *
2834
- * Projects set up with the business use case additionally pass the SDK's
2835
- * `businessLocales` overlay to the login widgets, restoring work-email copy on
2836
- * top of the widget's neutral built-in dictionaries. A plain `locales` prop
2837
- * suffices here: the wrapper component assigns it as a DOM property internally,
2838
- * so the React-18 attribute-decay caveat of the raw custom element does not
2839
- * apply.
2840
- */
2841
- function appTemplate$3(ctx) {
2842
- const business = ctx.useCase === "business";
2843
- const importNames = business ? "ZitadelLogin, ZitadelSession, businessLocales, configureZitadel" : "ZitadelLogin, ZitadelSession, configureZitadel";
2844
- const localesComment = business ? `
2845
-
2846
- // Set up for a business audience: businessLocales overlays work-email copy on
2847
- // the login widget's neutral built-in dictionaries. Remove the locales prop to
2848
- // fall back to the neutral wording.` : "";
2849
- const localesAttr = business ? " locales={businessLocales}" : "";
2850
- return `${MANAGED_MARKER}
2851
- import { useEffect } from "react";
2852
- import { ${importNames} } from "@zitadel/sdk-react";${localesComment}
2853
-
2854
- const project = configureZitadel({
2855
- projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
2856
- proxyPath: "${PROXY_PATH}",
2857
- });
2858
-
2859
- export default function App() {
2860
- const path = window.location.pathname;
2861
-
2862
- useEffect(() => {
2863
- if (path === "/") {
2864
- window.location.replace("/login");
2865
- }
2866
- }, [path]);
2867
-
2868
- if (path === "/") {
2869
- return null;
2870
- }
2871
- if (path.startsWith("/profile")) {
2872
- return (
2873
- <div style={{ position: "fixed", inset: 0, overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
2874
- <ZitadelSession project={project} postSignOutUrl="/login" />
2875
- </div>
2876
- );
2877
- }
2878
- if (path.startsWith("/register")) {
2879
- return (
2880
- <div style={{ position: "fixed", inset: 0, overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
2881
- <ZitadelLogin project={project}${localesAttr} purpose="register" postSignInUrl="/profile" />
2882
- </div>
2883
- );
2884
- }
2885
- return (
2886
- <div style={{ position: "fixed", inset: 0, overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
2887
- <ZitadelLogin project={project}${localesAttr} purpose="login" postSignInUrl="/profile" />
2888
- </div>
2889
- );
2890
- }
2891
- `;
2892
- }
2893
- //#endregion
2894
- //#region src/lib/orca/patchers/rule/react/index.ts
2895
- const SDK_DEPENDENCY$3 = "@zitadel/sdk-react";
2896
- /**
2897
- * Rule-based patcher for a Vite + React single-page app. Inherits the shared
2898
- * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
2899
- * managed `src/App.tsx` auth entry, a non-destructive `vite.config.*` merge
2900
- * that adds the `/__nextgen` dev proxy (attaching the project secret from
2901
- * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
2902
- * project id, and the SDK dep.
2903
- *
2904
- * Unlike Next.js — whose middleware runs the proxy and token exchange
2905
- * server-side — a SPA has no server, so the dev proxy provides the same-origin
2906
- * `/__nextgen` path locally. In production that path comes from a platform
2907
- * rewrite or minimal worker (ADR 036); CLI scaffolding for it is tracked in
2908
- * issue #560.
2909
- */
2910
- var ReactPatcher = class extends AbstractRulePatcher {
2911
- canPatch(framework) {
2912
- return framework === "react";
2913
- }
2914
- viteProxyOp(devPort, server) {
2915
- return buildViteProxyOp(devPort, server);
2916
- }
2917
- routeOps(ctx) {
2918
- return [
2919
- {
2920
- kind: "write",
2921
- path: "src/App.tsx",
2922
- contents: appTemplate$3(ctx)
2923
- },
2924
- this.viteProxyOp(ctx.framework.devPort, ctx.server),
2925
- {
2926
- kind: "merge-env",
2927
- path: ".env.example",
2928
- entries: { VITE_ZITADEL_PROJECT_ID: "" }
2929
- },
2930
- {
2931
- kind: "merge-env",
2932
- path: ".env.local",
2933
- entries: { VITE_ZITADEL_PROJECT_ID: ctx.project.id }
2934
- },
2935
- {
2936
- kind: "add-dep",
2937
- name: SDK_DEPENDENCY$3,
2938
- version: npmDistTagForCliVersion(ctx.cliVersion)
2939
- }
2940
- ];
2941
- }
2942
- routeFiles(_view) {
2943
- return ["src/App.tsx"];
2944
- }
2945
- routeDeps(_view) {
2946
- return [SDK_DEPENDENCY$3];
2947
- }
2948
- routeConfigEdits(_view) {
2949
- return ["vite.config.*"];
2950
- }
2951
- summary(_ctx) {
2952
- return {
2953
- title: "React (Vite) integration",
2954
- detail: "Wrote src/App.tsx auth entry and merged the /__nextgen dev proxy into vite.config.*."
2955
- };
2956
- }
2957
- };
2958
- //#endregion
2959
- //#region src/lib/orca/patchers/rule/solid/templates.ts
2960
- /**
2961
- * The managed `src/App.tsx`: a minimal path-based router that renders the
2962
- * `@zitadel/sdk-solid` widgets — login at `/login`, register at `/register`, and
2963
- * the signed-in session card at `/profile`. The root path redirects to `/login`.
2964
- * The project id comes from `VITE_ZITADEL_PROJECT_ID` (Vite only exposes
2965
- * `VITE_`-prefixed env to the client). No secret reaches the browser: the dev
2966
- * proxy in `vite.config.*` attaches the project service-key secret (from
2967
- * `ZITADEL_PROJECT_SECRET`) server-side only to `POST /sessions/exchange`.
2968
- *
2969
- * Projects set up with the business use case additionally pass the SDK's
2970
- * `businessLocales` overlay to the login widgets, restoring work-email copy on
2971
- * top of the widget's neutral built-in dictionaries. A plain `locales` prop
2972
- * suffices here: the wrapper component assigns it as a DOM property internally.
2973
- */
2974
- function appTemplate$2(ctx) {
2975
- const business = ctx.useCase === "business";
2976
- const importNames = business ? "ZitadelLogin, ZitadelSession, businessLocales, configureZitadel" : "ZitadelLogin, ZitadelSession, configureZitadel";
2977
- const localesComment = business ? `
2978
-
2979
- // Set up for a business audience: businessLocales overlays work-email copy on
2980
- // the login widget's neutral built-in dictionaries. Remove the locales prop to
2981
- // fall back to the neutral wording.` : "";
2982
- const localesAttr = business ? " locales={businessLocales}" : "";
2983
- return `${MANAGED_MARKER}
2984
- import { onMount } from "solid-js";
2985
- import { ${importNames} } from "@zitadel/sdk-solid";${localesComment}
2986
-
2987
- const project = configureZitadel({
2988
- projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
2989
- proxyPath: "${PROXY_PATH}",
2990
- });
2991
-
2992
- export default function App() {
2993
- const path = window.location.pathname;
2994
-
2995
- onMount(() => {
2996
- if (path === "/") {
2997
- window.location.replace("/login");
2998
- }
2999
- });
3000
-
3001
- if (path === "/") {
3002
- return null;
3003
- }
3004
- if (path.startsWith("/profile")) {
3005
- return (
3006
- <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
3007
- <ZitadelSession project={project} postSignOutUrl="/login" />
3008
- </div>
3009
- );
3010
- }
3011
- if (path.startsWith("/register")) {
3012
- return (
3013
- <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
3014
- <ZitadelLogin project={project}${localesAttr} purpose="register" postSignInUrl="/profile" />
3015
- </div>
3016
- );
3017
- }
3018
- return (
3019
- <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
3020
- <ZitadelLogin project={project}${localesAttr} purpose="login" postSignInUrl="/profile" />
3021
- </div>
3022
- );
3023
- }
3024
- `;
3025
- }
3026
- //#endregion
3027
- //#region src/lib/orca/patchers/rule/solid/index.ts
3028
- const SDK_DEPENDENCY$2 = "@zitadel/sdk-solid";
3029
- /**
3030
- * Rule-based patcher for a Vite + Solid single-page app. Inherits the shared
3031
- * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
3032
- * managed `src/App.tsx` auth entry, a non-destructive `vite.config.*` merge
3033
- * that adds the `/__nextgen` dev proxy (attaching the project secret from
3034
- * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
3035
- * project id, and the SDK dep.
3036
- *
3037
- * Unlike Next.js — whose middleware runs the proxy and token exchange
3038
- * server-side — a SPA has no server, so the dev proxy provides the same-origin
3039
- * `/__nextgen` path locally. In production that path comes from a platform
3040
- * rewrite or minimal worker (ADR 036); CLI scaffolding for it is tracked in
3041
- * issue #560.
3042
- */
3043
- var SolidPatcher = class extends AbstractRulePatcher {
3044
- canPatch(framework) {
3045
- return framework === "solid";
3046
- }
3047
- viteProxyOp(devPort, server) {
3048
- return buildViteProxyOp(devPort, server);
3049
- }
3050
- routeOps(ctx) {
3051
- return [
3052
- {
3053
- kind: "write",
3054
- path: "src/App.tsx",
3055
- contents: appTemplate$2(ctx)
3056
- },
3057
- this.viteProxyOp(ctx.framework.devPort, ctx.server),
3058
- {
3059
- kind: "merge-env",
3060
- path: ".env.example",
3061
- entries: { VITE_ZITADEL_PROJECT_ID: "" }
3062
- },
3063
- {
3064
- kind: "merge-env",
3065
- path: ".env.local",
3066
- entries: { VITE_ZITADEL_PROJECT_ID: ctx.project.id }
3067
- },
3068
- {
3069
- kind: "add-dep",
3070
- name: SDK_DEPENDENCY$2,
3071
- version: npmDistTagForCliVersion(ctx.cliVersion)
3072
- }
3073
- ];
3074
- }
3075
- routeFiles(_view) {
3076
- return ["src/App.tsx"];
3077
- }
3078
- routeDeps(_view) {
3079
- return [SDK_DEPENDENCY$2];
3080
- }
3081
- routeConfigEdits(_view) {
3082
- return ["vite.config.*"];
3083
- }
3084
- summary(_ctx) {
3085
- return {
3086
- title: "Solid (Vite) integration",
3087
- detail: "Wrote src/App.tsx auth entry and merged the /__nextgen dev proxy into vite.config.*."
3088
- };
3089
- }
3090
- };
3091
- //#endregion
3092
- //#region src/lib/orca/patchers/rule/svelte/templates.ts
3093
- /**
3094
- * The managed `src/App.svelte`: a minimal path-based router that renders the
3095
- * `@zitadel/sdk-svelte` widgets — login at `/login`, register at `/register`, and
3096
- * the signed-in session card at `/profile`. The root path redirects to `/login`.
3097
- * The managed marker lives in the `<script lang="ts">` block (a JS comment) so
3098
- * eject/doctor stay marker-aware. The project id comes from
3099
- * `VITE_ZITADEL_PROJECT_ID`. No secret reaches the browser: the dev proxy in
3100
- * `vite.config.*` attaches the project service-key secret (from
3101
- * `ZITADEL_PROJECT_SECRET`) server-side only to `POST /sessions/exchange`.
3102
- *
3103
- * Projects set up with the business use case additionally pass the SDK's
3104
- * `businessLocales` overlay to the login widgets, restoring work-email copy on
3105
- * top of the widget's neutral built-in dictionaries. A plain `locales` prop
3106
- * suffices here: the wrapper component assigns it as a DOM property internally.
3107
- */
3108
- function appTemplate$1(ctx) {
3109
- const business = ctx.useCase === "business";
3110
- const importNames = business ? "ZitadelLogin, ZitadelSession, businessLocales, configureZitadel" : "ZitadelLogin, ZitadelSession, configureZitadel";
3111
- const localesComment = business ? `
3112
-
3113
- // Set up for a business audience: businessLocales overlays work-email copy on
3114
- // the login widget's neutral built-in dictionaries. Remove the locales prop to
3115
- // fall back to the neutral wording.` : "";
3116
- const localesAttr = business ? " locales={businessLocales}" : "";
3117
- return `<script lang="ts">
3118
- ${MANAGED_MARKER}
3119
- import { onMount } from "svelte";
3120
- import { ${importNames} } from "@zitadel/sdk-svelte";${localesComment}
3121
-
3122
- const project = configureZitadel({
3123
- projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
3124
- proxyPath: "${PROXY_PATH}",
3125
- });
3126
-
3127
- const path = window.location.pathname;
3128
-
3129
- onMount(() => {
3130
- if (path === "/") {
3131
- window.location.replace("/login");
3132
- }
3133
- });
3134
- <\/script>
3135
-
3136
- {#if path.startsWith("/profile")}
3137
- <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
3138
- <ZitadelSession {project} postSignOutUrl="/login" />
3139
- </div>
3140
- {:else if path.startsWith("/register")}
3141
- <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
3142
- <ZitadelLogin {project}${localesAttr} purpose="register" postSignInUrl="/profile" />
3143
- </div>
3144
- {:else if path !== "/"}
3145
- <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
3146
- <ZitadelLogin {project}${localesAttr} purpose="login" postSignInUrl="/profile" />
3147
- </div>
3148
- {/if}
3149
- `;
3150
- }
3151
- //#endregion
3152
- //#region src/lib/orca/patchers/rule/svelte/index.ts
3153
- const SDK_DEPENDENCY$1 = "@zitadel/sdk-svelte";
3154
- /**
3155
- * Rule-based patcher for a Vite + Svelte single-page app. Inherits the shared
3156
- * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
3157
- * managed `src/App.svelte` auth entry, a non-destructive `vite.config.*` merge
3158
- * that adds the `/__nextgen` dev proxy (attaching the project secret from
3159
- * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
3160
- * project id, and the SDK dep.
3161
- *
3162
- * Unlike Next.js — whose middleware runs the proxy and token exchange
3163
- * server-side — a SPA has no server, so the dev proxy provides the same-origin
3164
- * `/__nextgen` path locally. In production that path comes from a platform
3165
- * rewrite or minimal worker (ADR 036); CLI scaffolding for it is tracked in
3166
- * issue #560.
3167
- */
3168
- var SveltePatcher = class extends AbstractRulePatcher {
3169
- canPatch(framework) {
3170
- return framework === "svelte";
3171
- }
3172
- viteProxyOp(devPort, server) {
3173
- return buildViteProxyOp(devPort, server);
3174
- }
3175
- routeOps(ctx) {
3176
- return [
3177
- {
3178
- kind: "write",
3179
- path: "src/App.svelte",
3180
- contents: appTemplate$1(ctx)
3181
- },
3182
- this.viteProxyOp(ctx.framework.devPort, ctx.server),
3183
- {
3184
- kind: "merge-env",
3185
- path: ".env.example",
3186
- entries: { VITE_ZITADEL_PROJECT_ID: "" }
3187
- },
3188
- {
3189
- kind: "merge-env",
3190
- path: ".env.local",
3191
- entries: { VITE_ZITADEL_PROJECT_ID: ctx.project.id }
3192
- },
3193
- {
3194
- kind: "add-dep",
3195
- name: SDK_DEPENDENCY$1,
3196
- version: npmDistTagForCliVersion(ctx.cliVersion)
3197
- }
3198
- ];
3199
- }
3200
- routeFiles(_view) {
3201
- return ["src/App.svelte"];
3202
- }
3203
- routeDeps(_view) {
3204
- return [SDK_DEPENDENCY$1];
3205
- }
3206
- routeConfigEdits(_view) {
3207
- return ["vite.config.*"];
3208
- }
3209
- summary(_ctx) {
3210
- return {
3211
- title: "Svelte (Vite) integration",
3212
- detail: "Wrote src/App.svelte auth entry and merged the /__nextgen dev proxy into vite.config.*."
3213
- };
3214
- }
3215
- };
3216
- //#endregion
3217
- //#region src/lib/orca/patchers/rule/vue/templates.ts
3218
- /**
3219
- * The managed `src/App.vue`: a minimal path-based router that renders the
3220
- * `@zitadel/sdk-vue` widgets — login at `/login`, register at `/register`, and
3221
- * the signed-in session card at `/profile`. The root path redirects to `/login`.
3222
- * The managed marker lives in the `<script setup>` block (a JS comment) so
3223
- * eject/doctor stay marker-aware. The project id comes from
3224
- * `VITE_ZITADEL_PROJECT_ID`. No secret reaches the browser: the dev proxy in
3225
- * `vite.config.*` attaches the project service-key secret (from
3226
- * `ZITADEL_PROJECT_SECRET`) server-side only to `POST /sessions/exchange`.
3227
- *
3228
- * Projects set up with the business use case additionally pass the SDK's
3229
- * `businessLocales` overlay to the login widgets, restoring work-email copy on
3230
- * top of the widget's neutral built-in dictionaries. A plain `:locales` prop
3231
- * suffices here: the wrapper component assigns it as a DOM property internally.
3232
- */
3233
- function appTemplate(ctx) {
3234
- const business = ctx.useCase === "business";
3235
- const importNames = business ? "ZitadelLogin, ZitadelSession, businessLocales, configureZitadel" : "ZitadelLogin, ZitadelSession, configureZitadel";
3236
- const localesComment = business ? `
3237
-
3238
- // Set up for a business audience: businessLocales overlays work-email copy on
3239
- // the login widget's neutral built-in dictionaries. Remove the :locales prop
3240
- // to fall back to the neutral wording.` : "";
3241
- const localesAttr = business ? " :locales=\"businessLocales\"" : "";
3242
- return `<script setup lang="ts">
3243
- ${MANAGED_MARKER}
3244
- import { onMounted } from "vue";
3245
- import { ${importNames} } from "@zitadel/sdk-vue";${localesComment}
3246
-
3247
- const project = configureZitadel({
3248
- projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
3249
- proxyPath: "${PROXY_PATH}",
3250
- });
3251
-
3252
- const path = window.location.pathname;
3253
-
3254
- onMounted(() => {
3255
- if (path === "/") {
3256
- window.location.replace("/login");
3257
- }
3258
- });
3259
- <\/script>
3260
-
3261
- <template>
3262
- <div
3263
- v-if="path.startsWith('/profile')"
3264
- style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark"
3265
- >
3266
- <ZitadelSession :project="project" postSignOutUrl="/login" />
3267
- </div>
3268
- <div
3269
- v-else-if="path.startsWith('/register')"
3270
- style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark"
3271
- >
3272
- <ZitadelLogin :project="project"${localesAttr} purpose="register" postSignInUrl="/profile" />
3273
- </div>
3274
- <div v-else-if="path !== '/'" style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
3275
- <ZitadelLogin :project="project"${localesAttr} purpose="login" postSignInUrl="/profile" />
3276
- </div>
3277
- </template>
3278
- `;
3279
- }
3280
- //#endregion
3281
- //#region src/lib/orca/patchers/rule/vue/index.ts
3282
- const SDK_DEPENDENCY = "@zitadel/sdk-vue";
3283
- /**
3284
- * Rule-based patcher for a Vite + Vue single-page app. Inherits the shared
3285
- * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
3286
- * managed `src/App.vue` auth entry, a non-destructive `vite.config.*` merge
3287
- * that adds the `/__nextgen` dev proxy (attaching the project secret from
3288
- * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
3289
- * project id, and the SDK dep.
3290
- */
3291
- var VuePatcher = class extends AbstractRulePatcher {
3292
- canPatch(framework) {
3293
- return framework === "vue";
3294
- }
3295
- viteProxyOp(devPort, server) {
3296
- return buildViteProxyOp(devPort, server);
3297
- }
3298
- routeOps(ctx) {
3299
- return [
3300
- {
3301
- kind: "write",
3302
- path: "src/App.vue",
3303
- contents: appTemplate(ctx)
3304
- },
3305
- this.viteProxyOp(ctx.framework.devPort, ctx.server),
3306
- {
3307
- kind: "merge-env",
3308
- path: ".env.example",
3309
- entries: { VITE_ZITADEL_PROJECT_ID: "" }
3310
- },
3311
- {
3312
- kind: "merge-env",
3313
- path: ".env.local",
3314
- entries: { VITE_ZITADEL_PROJECT_ID: ctx.project.id }
3315
- },
3316
- {
3317
- kind: "add-dep",
3318
- name: SDK_DEPENDENCY,
3319
- version: npmDistTagForCliVersion(ctx.cliVersion)
3320
- }
3321
- ];
3322
- }
3323
- routeFiles(_view) {
3324
- return ["src/App.vue"];
3325
- }
3326
- routeDeps(_view) {
3327
- return [SDK_DEPENDENCY];
3328
- }
3329
- routeConfigEdits(_view) {
3330
- return ["vite.config.*"];
3331
- }
3332
- summary(_ctx) {
3333
- return {
3334
- title: "Vue (Vite) integration",
3335
- detail: "Wrote src/App.vue auth entry and merged the /__nextgen dev proxy into vite.config.*."
3336
- };
3337
- }
3338
- };
3339
- //#endregion
3340
- //#region src/lib/orca/patchers/index.ts
3341
- /**
3342
- * Active patchers, in priority order; the first whose `canPatch` matches wins.
3343
- *
3344
- * Patchers are grouped by family under subdirectories: `rule/` holds the
3345
- * deterministic, template-driven patchers (extending
3346
- * {@link import("./rule/base").AbstractRulePatcher}). A future LLM-driven
3347
- * family lives under `llm/` and registers its concrete patchers here — no
3348
- * orchestrator or command changes needed. Only Next.js is supported today.
3349
- */
3350
- const patchers = [
3351
- new NextPatcher(),
3352
- new NuxtPatcher(),
3353
- new ReactPatcher(),
3354
- new VuePatcher(),
3355
- new SolidPatcher(),
3356
- new SveltePatcher(),
3357
- new QwikPatcher(),
3358
- new AngularPatcher()
3359
- ];
3360
- //#endregion
3361
- //#region src/lib/orca/scaffolders/cli.ts
3362
- /**
3363
- * Base for scaffolders that delegate to an external CLI (e.g. create-next-app).
3364
- * Subclasses implement {@link scaffold} and call {@link runCommand}.
3365
- */
3366
- var AbstractCLIScaffolder = class {
3367
- /** True when the requested framework is in {@link supportedFrameworks}. */
3368
- canScaffold(framework) {
3369
- return this.supportedFrameworks.includes(framework);
3370
- }
3371
- /**
3372
- * Runs an external command in `cwd`, throwing a typed {@link ZitadelError} on
3373
- * failure so the cause surfaces as a categorized CLI error. Distinguishes
3374
- * "binary not on PATH" (`ENOENT` from the spawn itself) from "binary ran but
3375
- * exited non-zero" — the former previously got masked as a generic
3376
- * `exited with status 1`, leaving users to guess. Tests stub
3377
- * `node:child_process` to assert the command without spawning.
3378
- */
3379
- runCommand(command, args, cwd) {
3380
- const result = spawnSync(command, [...args], {
3381
- cwd,
3382
- encoding: "utf8"
3383
- });
3384
- if (result.error) {
3385
- const err = result.error;
3386
- const notFound = err.code === "ENOENT";
3387
- throw new ZitadelError("E_VALIDATION", notFound ? `Command not found: ${command}` : `Failed to spawn "${command}": ${err.message}`, {
3388
- hint: notFound ? `Ensure '${command}' is installed and on PATH.` : void 0,
3389
- details: {
3390
- command,
3391
- args: [...args],
3392
- code: err.code
3393
- }
3394
- });
3395
- }
3396
- const status = result.status ?? 1;
3397
- if (status !== 0) {
3398
- const stdout = String(result.stdout ?? "");
3399
- const stderr = String(result.stderr ?? "");
3400
- const output = truncateCommandOutput([stderr, stdout].filter(Boolean).join("\n").trim());
3401
- throw new ZitadelError("E_VALIDATION", `Command "${command} ${args.join(" ")}" exited with status ${String(status)}`, {
3402
- hint: output ? `Command output:\n${output}` : "Run the command directly for more detail.",
3403
- details: {
3404
- command,
3405
- args: [...args],
3406
- cwd,
3407
- stdout,
3408
- stderr
3409
- }
3410
- });
3411
- }
3412
- }
3413
- };
3414
- function truncateCommandOutput(output) {
3415
- const limit = 4e3;
3416
- if (output.length <= limit) return output;
3417
- return `${output.slice(0, limit)}\n... output truncated ...`;
3418
- }
3419
- //#endregion
3420
- //#region src/lib/orca/scaffolders/angular.ts
3421
- /**
3422
- * Derives a valid Angular project name from the target directory. `ng new`
3423
- * validates the name against `^[a-zA-Z0-9-~][a-zA-Z0-9-._~]*$` and rejects `.`,
3424
- * so we slugify the directory's basename (lowercase, non-alphanumerics → `-`)
3425
- * and guarantee a leading letter by prefixing `app-` when the slug does not
3426
- * start with one (`app-zitadel` when the basename slugifies to nothing).
3427
- */
3428
- function angularProjectName(cwd) {
3429
- const slug = basename(cwd).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
3430
- return /^[a-z]/.test(slug) ? slug : `app-${slug || "zitadel"}`;
3431
- }
3432
- /**
3433
- * Scaffolds a new Angular app with the Angular CLI, then removes the starter
3434
- * `app.ts`/`app.html` root component (and its now-unreferenced `app.css`) so the
3435
- * patcher can write the managed ones without colliding with boilerplate. The
3436
- * managed component uses only `templateUrl`, so `app.css` would otherwise be a
3437
- * dangling file eject never cleans up. Unlike `create-vite`/`nuxi`, `ng new`
3438
- * needs a real project name plus `--directory .` to populate the current dir.
3439
- * Requires a Node version Angular supports (^22.22.3 || ^24.15.0 || >=26).
3440
- */
3441
- var AngularScaffolder = class extends AbstractCLIScaffolder {
3442
- displayName = "Angular";
3443
- supportedFrameworks = ["angular"];
3444
- async scaffold(cwd, _framework) {
3445
- this.runCommand("npx", [
3446
- "-y",
3447
- "@angular/cli@latest",
3448
- "new",
3449
- angularProjectName(cwd),
3450
- "--directory",
3451
- ".",
3452
- "--defaults",
3453
- "--style=css",
3454
- "--ssr=false",
3455
- "--skip-git"
3456
- ], cwd);
3457
- await rm(join(cwd, "src/app/app.ts"), { force: true });
3458
- await rm(join(cwd, "src/app/app.html"), { force: true });
3459
- await rm(join(cwd, "src/app/app.css"), { force: true });
3460
- }
3461
- };
3462
- //#endregion
3463
- //#region src/lib/orca/scaffolders/next.ts
3464
- const CREATE_NEXT_APP_VERSION = "16.2.4";
3465
- /** Scaffolds a new Next.js App Router project with `create-next-app`. */
3466
- var NextScaffolder = class extends AbstractCLIScaffolder {
3467
- displayName = "Next.js";
3468
- supportedFrameworks = ["next"];
3469
- /**
3470
- * Runs the pinned `create-next-app` version in `cwd`, creating a TypeScript
3471
- * App Router project in place. `--yes` accepts all defaults so the command
3472
- * runs unattended, and `--skip-install` leaves dependency installation to the
3473
- * setup command's explicit next step after Zitadel patches package.json.
3474
- */
3475
- async scaffold(cwd, _framework) {
3476
- this.runCommand("npx", [
3477
- "--yes",
3478
- `create-next-app@${CREATE_NEXT_APP_VERSION}`,
3479
- ".",
3480
- "--ts",
3481
- "--app",
3482
- "--use-npm",
3483
- "--disable-git",
3484
- "--yes",
3485
- "--skip-install"
3486
- ], cwd);
3487
- }
3488
- };
3489
- //#endregion
3490
- //#region src/lib/orca/scaffolders/nuxt.ts
3491
- /**
3492
- * Scaffolds a new Nuxt app with `nuxi init`, then removes the starter `app.vue`
3493
- * so the patcher can write the managed one without colliding with boilerplate.
3494
- * Nuxt 4 (what `nuxi init` scaffolds today) puts it under `app/`; older Nuxt put
3495
- * it at the root, so both are removed. `nuxt.config.ts` is left in place — the
3496
- * patcher merges into it via an `edit`, which preserves whatever `nuxi` generated.
3497
- */
3498
- var NuxtScaffolder = class extends AbstractCLIScaffolder {
3499
- displayName = "Nuxt";
3500
- supportedFrameworks = ["nuxt"];
3501
- async scaffold(cwd, _framework) {
3502
- this.runCommand("npx", [
3503
- "-y",
3504
- "nuxi@latest",
3505
- "init",
3506
- ".",
3507
- "--template",
3508
- "minimal",
3509
- "--packageManager",
3510
- "npm",
3511
- "--no-gitInit",
3512
- "--force"
3513
- ], cwd);
3514
- await rm(join(cwd, "app/app.vue"), { force: true });
3515
- await rm(join(cwd, "app.vue"), { force: true });
3516
- }
3517
- };
3518
- //#endregion
3519
- //#region src/lib/orca/scaffolders/qwik.ts
3520
- /**
3521
- * Scaffolds a new Vite + Qwik (TypeScript) single-page app with `create-vite`,
3522
- * then removes the starter `app.tsx`/`app.css` demo so the patcher can write the
3523
- * managed `src/app.tsx` without colliding with boilerplate. The create-vite Qwik
3524
- * template uses a lowercase `app.tsx` (named `App` export, mounted by `main.tsx`)
3525
- * — the patched file keeps that same entry.
3526
- */
3527
- var QwikScaffolder = class extends AbstractCLIScaffolder {
3528
- displayName = "Qwik (Vite)";
3529
- supportedFrameworks = ["qwik"];
3530
- async scaffold(cwd, _framework) {
3531
- this.runCommand("npm", [
3532
- "create",
3533
- "vite@latest",
3534
- ".",
3535
- "--",
3536
- "--template",
3537
- "qwik-ts"
3538
- ], cwd);
3539
- await rm(join(cwd, "src/app.tsx"), { force: true });
3540
- await rm(join(cwd, "src/app.css"), { force: true });
3541
- this.runCommand("npm", [
3542
- "pkg",
3543
- "set",
3544
- "devDependencies.vite=^7.3.5"
3545
- ], cwd);
3546
- }
3547
- };
3548
- //#endregion
3549
- //#region src/lib/orca/scaffolders/react.ts
3550
- /**
3551
- * Scaffolds a new Vite + React (TypeScript) single-page app with `create-vite`,
3552
- * then removes the starter `App.tsx`/`App.css` demo so the patcher can write the
3553
- * managed `src/App.tsx` without colliding with boilerplate. `index.css` and
3554
- * `main.tsx` are left in place — the patched `App.tsx` keeps the same entry.
3555
- */
3556
- var ReactScaffolder = class extends AbstractCLIScaffolder {
3557
- displayName = "React (Vite)";
3558
- supportedFrameworks = ["react"];
3559
- async scaffold(cwd, _framework) {
3560
- this.runCommand("npm", [
3561
- "create",
3562
- "vite@latest",
3563
- ".",
3564
- "--",
3565
- "--template",
3566
- "react-ts"
3567
- ], cwd);
3568
- await rm(join(cwd, "src/App.tsx"), { force: true });
3569
- await rm(join(cwd, "src/App.css"), { force: true });
3570
- }
3571
- };
3572
- //#endregion
3573
- //#region src/lib/orca/scaffolders/solid.ts
3574
- /**
3575
- * Scaffolds a new Vite + Solid (TypeScript) single-page app with `create-vite`,
3576
- * then removes the starter `App.tsx`/`App.css` demo so the patcher can write the
3577
- * managed `src/App.tsx` without colliding with boilerplate. `index.css` and
3578
- * `index.tsx` are left in place — the patched `App.tsx` keeps the same entry.
3579
- */
3580
- var SolidScaffolder = class extends AbstractCLIScaffolder {
3581
- displayName = "Solid (Vite)";
3582
- supportedFrameworks = ["solid"];
3583
- async scaffold(cwd, _framework) {
3584
- this.runCommand("npm", [
3585
- "create",
3586
- "vite@latest",
3587
- ".",
3588
- "--",
3589
- "--template",
3590
- "solid-ts"
3591
- ], cwd);
3592
- await rm(join(cwd, "src/App.tsx"), { force: true });
3593
- await rm(join(cwd, "src/App.css"), { force: true });
3594
- }
3595
- };
3596
- //#endregion
3597
- //#region src/lib/orca/scaffolders/svelte.ts
3598
- /**
3599
- * Scaffolds a new Vite + Svelte (TypeScript) single-page app with `create-vite`,
3600
- * then removes the starter `App.svelte`/`lib/Counter.svelte` demo so the patcher
3601
- * can write the managed `src/App.svelte` without colliding with boilerplate.
3602
- * `app.css` and `main.ts` are left in place — the patched `App.svelte` keeps the
3603
- * same entry.
3604
- */
3605
- var SvelteScaffolder = class extends AbstractCLIScaffolder {
3606
- displayName = "Svelte (Vite)";
3607
- supportedFrameworks = ["svelte"];
3608
- async scaffold(cwd, _framework) {
3609
- this.runCommand("npm", [
3610
- "create",
3611
- "vite@latest",
3612
- ".",
3613
- "--",
3614
- "--template",
3615
- "svelte-ts"
3616
- ], cwd);
3617
- await rm(join(cwd, "src/App.svelte"), { force: true });
3618
- await rm(join(cwd, "src/lib/Counter.svelte"), { force: true });
3619
- }
3620
- };
3621
- //#endregion
3622
- //#region src/lib/orca/scaffolders/vue.ts
3623
- /**
3624
- * Scaffolds a new Vite + Vue (TypeScript) single-page app with `create-vite`,
3625
- * then removes the starter `App.vue`/`components/HelloWorld.vue` demo so the
3626
- * patcher can write the managed `src/App.vue` without colliding with boilerplate.
3627
- */
3628
- var VueScaffolder = class extends AbstractCLIScaffolder {
3629
- displayName = "Vue (Vite)";
3630
- supportedFrameworks = ["vue"];
3631
- async scaffold(cwd, _framework) {
3632
- this.runCommand("npm", [
3633
- "create",
3634
- "vite@latest",
3635
- ".",
3636
- "--",
3637
- "--template",
3638
- "vue-ts"
3639
- ], cwd);
3640
- await rm(join(cwd, "src/App.vue"), { force: true });
3641
- await rm(join(cwd, "src/components/HelloWorld.vue"), { force: true });
3642
- }
3643
- };
3644
- //#endregion
3645
- //#region src/lib/orca/scaffolders/index.ts
3646
- /**
3647
- * Active scaffolders, in priority order. The framework picker derives its
3648
- * choices from this list. Add a new framework by appending its scaffolder
3649
- * here — no orchestrator changes needed.
3650
- */
3651
- const scaffolders = [
3652
- new NextScaffolder(),
3653
- new NuxtScaffolder(),
3654
- new ReactScaffolder(),
3655
- new VueScaffolder(),
3656
- new SolidScaffolder(),
3657
- new SvelteScaffolder(),
3658
- new QwikScaffolder(),
3659
- new AngularScaffolder()
3660
- ];
3661
- //#endregion
3662
- //#region src/lib/orca/index.ts
3663
- /**
3664
- * Orchestrates the three per-framework strategies — detectors (recognise an
3665
- * existing project and extract its facts), scaffolders (create a project), and
3666
- * patchers (integrate Zitadel) — over their respective registries. It resolves
3667
- * the right strategy for a framework and drives the detect/scaffold lifecycle;
3668
- * how a patcher applies its work (file operations vs an LLM agent) stays
3669
- * internal to that patcher. Registries are injected so tests can supply fakes.
3670
- */
3671
- var Orca = class {
3672
- constructor(detectors, scaffolders, patchers) {
3673
- this.detectors = detectors;
3674
- this.scaffolders = scaffolders;
3675
- this.patchers = patchers;
3676
- }
3677
- /**
3678
- * Detects the framework in `cwd` and extracts its {@link FrameworkFacts},
3679
- * honouring an explicit `requested` framework. Throws
3680
- * `E_FRAMEWORK_NOT_DETECTED` when nothing matches; a detector's
3681
- * `E_UNSUPPORTED_PROJECT_SHAPE` (recognised but unsupported) propagates.
3682
- */
3683
- async detect(cwd, requested) {
3684
- const candidates = requested ? this.detectors.filter((detector) => detector.framework === requested) : this.detectors;
3685
- if (requested && candidates.length === 0) throw new ZitadelError("E_FRAMEWORK_NOT_DETECTED", `Unsupported framework "${requested}"`, { hint: `Supported frameworks: ${this.frameworkIds().join(", ")}.` });
3686
- for (const detector of candidates) {
3687
- const facts = await detector.detect(cwd);
3688
- if (facts) return facts;
3689
- }
3690
- throw new ZitadelError("E_FRAMEWORK_NOT_DETECTED", "Could not detect a supported app framework", { hint: "Run setup from your app project directory, pass --cwd <path-to-app>, or run setup from an empty directory to scaffold a new app." });
3691
- }
3692
- /**
3693
- * Non-throwing detection: returns `undefined` instead of raising for a
3694
- * project that is absent, unrecognised, or recognised-but-unsupported, so
3695
- * callers (e.g. `eject`) can probe and degrade gracefully.
3696
- */
3697
- async tryDetect(cwd) {
3698
- try {
3699
- return await this.detect(cwd);
3700
- } catch (error) {
3701
- if (error instanceof ZitadelError && (error.code === "E_FRAMEWORK_NOT_DETECTED" || error.code === "E_UNSUPPORTED_PROJECT_SHAPE")) return;
3702
- throw error;
3703
- }
3704
- }
3705
- /** Whether `cwd` is safe for an in-place framework scaffold. */
3706
- async isFreshScaffoldTarget(cwd) {
3707
- return (await inspectScaffoldTarget(cwd)).scaffoldable;
3708
- }
3709
- /**
3710
- * Creates a new `framework` project in `cwd`, then re-detects it to return
3711
- * the resulting {@link FrameworkFacts}. Throws `E_CONFLICT` when the directory
3712
- * already contains a project ("already scaffolded") and `E_VALIDATION` when no
3713
- * scaffolder supports the framework.
3714
- */
3715
- async scaffold(cwd, framework) {
3716
- const target = await inspectScaffoldTarget(cwd);
3717
- if (!target.scaffoldable) throw new ZitadelError("E_CONFLICT", `Cannot scaffold: ${cwd} is not empty`, {
3718
- hint: target.reason ?? "Run setup in an empty directory, or run setup from an existing supported app project.",
3719
- details: { entries: target.entries }
3720
- });
3721
- assertNpmSafeScaffoldDirectoryName(cwd);
3722
- const stash = await stashFreshScaffoldArtifacts(cwd, target);
3723
- try {
3724
- await this.scaffolderFor(framework).scaffold(cwd, framework);
3725
- } finally {
3726
- await restoreFreshScaffoldArtifacts(cwd, stash);
3727
- }
3728
- return this.detect(cwd, framework);
3729
- }
3730
- /**
3731
- * Resolves the scaffolder for a framework, throwing `E_VALIDATION` (with the
3732
- * available list) when none matches.
3733
- */
3734
- scaffolderFor(framework) {
3735
- const scaffolder = this.scaffolders.find((candidate) => candidate.canScaffold(framework));
3736
- if (!scaffolder) throw new ZitadelError("E_VALIDATION", `No scaffolder supports "${framework}"`, { hint: `Available frameworks: ${this.availableFrameworks().map((f) => f.id).join(", ")}.` });
3737
- return scaffolder;
3738
- }
3739
- /**
3740
- * Resolves the patcher for a framework, throwing `E_VALIDATION` when none
3741
- * matches (e.g. a framework that can be scaffolded but not yet integrated).
3742
- */
3743
- patcherFor(framework) {
3744
- const patcher = this.patchers.find((candidate) => candidate.canPatch(framework));
3745
- if (!patcher) throw new ZitadelError("E_VALIDATION", `No patcher supports "${framework}"`, { hint: "Zitadel integration currently supports Next.js." });
3746
- return patcher;
3747
- }
3748
- /** The frameworks that can be scaffolded, derived from the scaffolder registry. */
3749
- availableFrameworks() {
3750
- return this.scaffolders.map((scaffolder) => ({
3751
- id: scaffolder.supportedFrameworks[0] ?? scaffolder.displayName,
3752
- displayName: scaffolder.displayName
3753
- }));
3754
- }
3755
- frameworkIds() {
3756
- return this.detectors.map((detector) => detector.framework);
3757
- }
3758
- };
3759
- function assertNpmSafeScaffoldDirectoryName(cwd) {
3760
- const name = basename(cwd);
3761
- const errors = npmPackageNameErrors(name);
3762
- if (errors.length === 0) return;
3763
- throw new ZitadelError("E_VALIDATION", `Fresh app directory name "${name}" is not npm-package-safe`, {
3764
- hint: "Rename the directory to a lowercase npm-package-safe name, for example `my-zitadel-app`, then rerun setup.",
3765
- details: {
3766
- cwd,
3767
- name,
3768
- validation_errors: errors
3769
- }
3770
- });
3771
- }
3772
- function npmPackageNameErrors(name) {
3773
- const errors = [];
3774
- if (name.length === 0) errors.push("name is empty");
3775
- if (name.length > 214) errors.push("name is longer than 214 characters");
3776
- if (name !== name.trim()) errors.push("name contains leading or trailing whitespace");
3777
- if (/[A-Z]/.test(name)) errors.push("name can no longer contain capital letters");
3778
- if (name.startsWith(".") || name.startsWith("_")) errors.push("name cannot start with a period or underscore");
3779
- if (!/^[a-z0-9][a-z0-9._~-]*$/.test(name)) errors.push("name may only contain lowercase letters, numbers, dots, underscores, tildes, and hyphens");
3780
- if (name === "node_modules" || name === "favicon.ico") errors.push(`name "${name}" is reserved`);
3781
- return [...new Set(errors)];
3782
- }
3783
- /** {@link Orca} wired with the default detector, scaffolder, and patcher registries. */
3784
- function createOrca() {
3785
- return new Orca(detectors, scaffolders, patchers);
3786
- }
3787
- async function inspectScaffoldTarget(cwd) {
3788
- const entries = await readdir(cwd, { withFileTypes: true });
3789
- const names = entries.map((entry) => entry.name).sort();
3790
- let hasGitignore = false;
3791
- let hasRuntimeOnlyZitadel = false;
3792
- for (const entry of entries) {
3793
- if (entry.name === ".gitignore") {
3794
- if (!entry.isFile()) return {
3795
- scaffoldable: false,
3796
- hasGitignore: false,
3797
- hasRuntimeOnlyZitadel: false,
3798
- reason: ".gitignore exists but is not a file.",
3799
- entries: names
3800
- };
3801
- hasGitignore = true;
3802
- continue;
3803
- }
3804
- if (entry.name === ".zitadel") {
3805
- if (!entry.isDirectory() || !await isRuntimeOnlyZitadelDir(join(cwd, ".zitadel"))) return {
3806
- scaffoldable: false,
3807
- hasGitignore,
3808
- hasRuntimeOnlyZitadel: false,
3809
- reason: ".zitadel contains project state. Move it aside or run setup from an empty app directory.",
3810
- entries: names
3811
- };
3812
- hasRuntimeOnlyZitadel = true;
3813
- continue;
3814
- }
3815
- return {
3816
- scaffoldable: false,
3817
- hasGitignore,
3818
- hasRuntimeOnlyZitadel: false,
3819
- reason: `Directory contains ${entry.name}. Run setup from an empty directory to scaffold a new app.`,
3820
- entries: names
3821
- };
3822
- }
3823
- return {
3824
- scaffoldable: true,
3825
- hasGitignore,
3826
- hasRuntimeOnlyZitadel,
3827
- entries: names
3828
- };
3829
- }
3830
- async function isRuntimeOnlyZitadelDir(path) {
3831
- const entries = await readdir(path, { withFileTypes: true });
3832
- if (entries.length !== 1 || entries[0]?.name !== "local" || !entries[0].isDirectory()) return false;
3833
- return true;
3834
- }
3835
- async function stashFreshScaffoldArtifacts(cwd, target) {
3836
- if (!target.hasGitignore && !target.hasRuntimeOnlyZitadel) return;
3837
- const root = join(dirname(cwd), `.${basename(cwd)}.fresh-scaffold-stash-${String(process.pid)}-${String(Date.now())}`);
3838
- await mkdir(root, { mode: 448 });
3839
- const stash = { root };
3840
- if (target.hasRuntimeOnlyZitadel) {
3841
- stash.zitadel = join(root, ".zitadel");
3842
- await rename(join(cwd, ".zitadel"), stash.zitadel);
3843
- }
3844
- if (target.hasGitignore) {
3845
- stash.gitignore = join(root, ".gitignore");
3846
- await rename(join(cwd, ".gitignore"), stash.gitignore);
3847
- }
3848
- return stash;
3849
- }
3850
- async function restoreFreshScaffoldArtifacts(cwd, stash) {
3851
- if (!stash) return;
3852
- try {
3853
- await restoreRuntimeOnlyZitadel(cwd, stash.zitadel);
3854
- await restoreGitignore(cwd, stash.gitignore);
3855
- } finally {
3856
- await rm(stash.root, {
3857
- recursive: true,
3858
- force: true
3859
- });
3860
- }
3861
- }
3862
- async function restoreRuntimeOnlyZitadel(cwd, stash) {
3863
- if (!stash) return;
3864
- const target = join(cwd, ".zitadel");
3865
- try {
3866
- await rename(stash, target);
3867
- await appendGitignoreEntry(cwd, ".zitadel/local/");
3868
- return;
3869
- } catch (error) {
3870
- if (!isErrno(error, "EEXIST")) throw error;
3871
- }
3872
- await mkdir(target, {
3873
- recursive: true,
3874
- mode: 448
3875
- });
3876
- await rename(join(stash, "local"), join(target, "local"));
3877
- await rm(stash, {
3878
- recursive: true,
3879
- force: true
3880
- });
3881
- await appendGitignoreEntry(cwd, ".zitadel/local/");
3882
- }
3883
- async function restoreGitignore(cwd, stash) {
3884
- if (!stash) return;
3885
- const path = join(cwd, ".gitignore");
3886
- const stashed = await readFile(stash, "utf8");
3887
- let current = "";
3888
- try {
3889
- current = await readFile(path, "utf8");
3890
- } catch (error) {
3891
- if (!isErrno(error, "ENOENT")) throw error;
3892
- }
3893
- const existingLines = new Set(current.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean));
3894
- const missingLines = stashed.split(/\r?\n/g).map((line) => line.trim()).filter((line) => line.length > 0 && !existingLines.has(line));
3895
- if (missingLines.length === 0) return;
3896
- const prefix = current.length === 0 || current.endsWith("\n") ? "" : "\n";
3897
- await writeFile(path, `${current}${prefix}${missingLines.join("\n")}\n`);
3898
- }
3899
- async function appendGitignoreEntry(cwd, entry) {
3900
- const path = join(cwd, ".gitignore");
3901
- let existing = "";
3902
- try {
3903
- existing = await readFile(path, "utf8");
3904
- } catch (error) {
3905
- if (!isErrno(error, "ENOENT")) throw error;
3906
- }
3907
- if (existing.split(/\r?\n/g).map((line) => line.trim()).includes(entry)) return;
3908
- const prefix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
3909
- await writeFile(path, `${existing}${prefix}${entry}\n`);
3910
- }
3911
- function isErrno(error, code) {
3912
- return typeof error === "object" && error !== null && "code" in error && error.code === code;
3913
- }
3914
- //#endregion
3915
- export { AGENTS_HEADER as a, RENDERER_IDS as i, inspectScaffoldTarget as n, removeGuidanceSection as o, AVAILABLE_RENDERER_IDS as r, issuerFromPort as s, createOrca as t };
3916
-
3917
- //# sourceMappingURL=orca-66vAxeIe.mjs.map