@zitadel/cli 0.1.0-alpha.17 → 0.1.0-alpha.19

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 (62) hide show
  1. package/README.md +210 -124
  2. package/SKILLS.md +159 -17
  3. package/dist/branding-cygSBPkV.mjs +79 -0
  4. package/dist/branding-cygSBPkV.mjs.map +1 -0
  5. package/dist/claim-state-DlfD6Res.mjs +66 -0
  6. package/dist/claim-state-DlfD6Res.mjs.map +1 -0
  7. package/dist/commands/apply.mjs +6 -5
  8. package/dist/commands/apply.mjs.map +1 -1
  9. package/dist/commands/branding/eject.mjs +104 -0
  10. package/dist/commands/branding/eject.mjs.map +1 -0
  11. package/dist/commands/claim.mjs +402 -0
  12. package/dist/commands/claim.mjs.map +1 -0
  13. package/dist/commands/doctor.mjs +500 -37
  14. package/dist/commands/doctor.mjs.map +1 -1
  15. package/dist/commands/eject.mjs +4 -4
  16. package/dist/commands/eject.mjs.map +1 -1
  17. package/dist/commands/logs.mjs +2 -2
  18. package/dist/commands/plan.mjs +6 -5
  19. package/dist/commands/plan.mjs.map +1 -1
  20. package/dist/commands/reset.mjs +2 -2
  21. package/dist/commands/schemas/list.mjs +11 -11
  22. package/dist/commands/schemas/list.mjs.map +1 -1
  23. package/dist/commands/setup.mjs +359 -132
  24. package/dist/commands/setup.mjs.map +1 -1
  25. package/dist/commands/start.mjs +3 -3
  26. package/dist/commands/status.mjs +21 -13
  27. package/dist/commands/status.mjs.map +1 -1
  28. package/dist/commands/stop.mjs +3 -3
  29. package/dist/commands/stop.mjs.map +1 -1
  30. package/dist/designs-Ckz18Dpo.mjs +38 -0
  31. package/dist/designs-Ckz18Dpo.mjs.map +1 -0
  32. package/dist/{docker-pGcTrDIY.mjs → docker-DcRGTOJa.mjs} +4 -3
  33. package/dist/docker-DcRGTOJa.mjs.map +1 -0
  34. package/dist/{environment-BQF7LeCz.mjs → environment-rjRVkJjW.mjs} +1 -1
  35. package/dist/{environment-BQF7LeCz.mjs.map → environment-rjRVkJjW.mjs.map} +1 -1
  36. package/dist/{journey-guidance-CYVpQszz.mjs → journey-guidance-BGrOX_gT.mjs} +14 -3
  37. package/dist/{journey-guidance-CYVpQszz.mjs.map → journey-guidance-BGrOX_gT.mjs.map} +1 -1
  38. package/dist/{oclif-BRdjVNne.mjs → oclif-CanO3zdt.mjs} +247 -43
  39. package/dist/oclif-CanO3zdt.mjs.map +1 -0
  40. package/dist/{orca-Dip6DPJ2.mjs → orca-Cu0iW1mv.mjs} +894 -196
  41. package/dist/orca-Cu0iW1mv.mjs.map +1 -0
  42. package/dist/package-manager-DFdLcDx1.mjs +194 -0
  43. package/dist/package-manager-DFdLcDx1.mjs.map +1 -0
  44. package/dist/{ports-V8JfiNgT.mjs → ports-BM20XIZb.mjs} +2 -2
  45. package/dist/{ports-V8JfiNgT.mjs.map → ports-BM20XIZb.mjs.map} +1 -1
  46. package/dist/{processes-BVqYsxT8.mjs → processes-DKHPkU8O.mjs} +1 -1
  47. package/dist/{processes-BVqYsxT8.mjs.map → processes-DKHPkU8O.mjs.map} +1 -1
  48. package/dist/{project-BGgv7Bba.mjs → project-CHhot85s.mjs} +57 -4
  49. package/dist/project-CHhot85s.mjs.map +1 -0
  50. package/dist/{sync-Cd3z7Q1R.mjs → sync-Bd6vASgg.mjs} +549 -75
  51. package/dist/sync-Bd6vASgg.mjs.map +1 -0
  52. package/dist/user-schema-DTuOsdKE.mjs +91 -0
  53. package/dist/user-schema-DTuOsdKE.mjs.map +1 -0
  54. package/oclif.manifest.json +227 -5
  55. package/package.json +9 -6
  56. package/dist/docker-pGcTrDIY.mjs.map +0 -1
  57. package/dist/oclif-BRdjVNne.mjs.map +0 -1
  58. package/dist/orca-Dip6DPJ2.mjs.map +0 -1
  59. package/dist/project-BGgv7Bba.mjs.map +0 -1
  60. package/dist/sync-Cd3z7Q1R.mjs.map +0 -1
  61. package/dist/user-schema-DDz5-lX5.mjs +0 -13
  62. package/dist/user-schema-DDz5-lX5.mjs.map +0 -1
@@ -1,6 +1,7 @@
1
- import { D as ZitadelError, E as stableStringify, S as MANAGED_MARKER, T as parseJsonObject, b as npmDistTagForCliVersion, n as DEFAULT_SERVER, w as isObject, x as publicCliCommand } from "./oclif-BRdjVNne.mjs";
2
- import { chmod, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
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
3
  import { basename, dirname, join } from "node:path";
4
+ import semver from "semver";
4
5
  import { META_SCHEMA_DIR, metaSchemaFiles } from "@zitadel/config/meta-schemas";
5
6
  import { builders, generateCode, parseModule } from "magicast";
6
7
  import { spawnSync } from "node:child_process";
@@ -21,12 +22,33 @@ async function readPackageJson(cwd) {
21
22
  function hasDependency(pkg, name) {
22
23
  return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
23
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
+ */
24
31
  function dependencyVersionMajor(pkg, name) {
25
32
  const spec = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
26
33
  if (!spec) return;
27
34
  const match = spec.match(/\d+/);
28
35
  return match ? Number(match[0]) : void 0;
29
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
+ }
30
52
  //#endregion
31
53
  //#region src/lib/orca/detectors/port.ts
32
54
  /**
@@ -72,12 +94,62 @@ function extractPort(script) {
72
94
  if (rawEnvPort) return Number.parseInt(rawEnvPort, 10);
73
95
  }
74
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
+ /**
75
122
  * Builds the local OIDC issuer URL for a given dev port. Centralized so the
76
123
  * `localhost` origin convention is defined in exactly one place.
77
124
  */
78
125
  function issuerFromPort(port) {
79
126
  return `http://localhost:${port}`;
80
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
+ }
81
153
  //#endregion
82
154
  //#region src/lib/orca/detectors/angular.ts
83
155
  /** The lowest Angular major the generated templates compile on. */
@@ -136,15 +208,18 @@ var NextDetector = class {
136
208
  async detect(cwd) {
137
209
  const pkg = await readPackageJson(cwd).catch(() => void 0);
138
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." });
139
213
  const appDir = await dirExists$1(join(cwd, "app")) ? "app" : await dirExists$1(join(cwd, "src/app")) ? "src/app" : void 0;
140
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." });
141
215
  const devPort = await detectDevPort(cwd, pkg);
216
+ const versionMajor = dependencyVersionMajor(pkg, "next");
142
217
  return {
143
218
  id: "next",
144
219
  appDir,
145
220
  devPort,
146
221
  url: issuerFromPort(devPort),
147
- versionMajor: dependencyVersionMajor(pkg, "next")
222
+ ...versionMajor === void 0 ? {} : { versionMajor }
148
223
  };
149
224
  }
150
225
  };
@@ -224,12 +299,16 @@ var ReactDetector = class {
224
299
  async detect(cwd) {
225
300
  const pkg = await readPackageJson(cwd).catch(() => void 0);
226
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." });
227
304
  const devPort = await detectDevPort(cwd, pkg);
305
+ const versionMajor = dependencyVersionMajor(pkg, "react");
228
306
  return {
229
307
  id: "react",
230
308
  appDir: "src",
231
309
  devPort,
232
- url: issuerFromPort(devPort)
310
+ url: issuerFromPort(devPort),
311
+ ...versionMajor === void 0 ? {} : { versionMajor }
233
312
  };
234
313
  }
235
314
  };
@@ -320,6 +399,21 @@ const detectors = [
320
399
  //#endregion
321
400
  //#region src/lib/orca/patchers/rule/file-writer/index.ts
322
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
+ /**
323
417
  * Applies a {@link ScaffoldPlan} to disk, executing its operations in order.
324
418
  *
325
419
  * Operations are idempotent: writes whose target already matches the desired
@@ -332,12 +426,19 @@ const detectors = [
332
426
  async function scaffold(plan, opts) {
333
427
  const result = {
334
428
  dryRun: opts.dryRun,
335
- filesWritten: [],
429
+ files: [],
336
430
  filesSkipped: [],
337
431
  depsAdded: []
338
432
  };
339
433
  for (const op of plan.ops) await applyOp(op, opts, result);
340
- return 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
+ };
341
442
  }
342
443
  async function applyOp(op, opts, result) {
343
444
  switch (op.kind) {
@@ -398,8 +499,9 @@ async function editFile(cwd, pathOrPaths, edit, dryRun, result) {
398
499
  result.filesSkipped.push(path);
399
500
  return;
400
501
  }
502
+ const action = source === void 0 ? "create" : "update";
401
503
  if (dryRun) {
402
- result.filesWritten.push(path);
504
+ record(result, path, "file", action);
403
505
  return;
404
506
  }
405
507
  await mkdir(dirname(path), { recursive: true });
@@ -407,11 +509,16 @@ async function editFile(cwd, pathOrPaths, edit, dryRun, result) {
407
509
  await writeFile(tmp, next);
408
510
  if (mode !== void 0) await chmod(tmp, mode).catch(() => void 0);
409
511
  await rename(tmp, path);
410
- result.filesWritten.push(path);
512
+ record(result, path, "file", action);
411
513
  }
412
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;
413
518
  if (dryRun) {
414
- result.filesWritten.push(path);
519
+ if (!existed) record(result, path, "dir", "create");
520
+ else if (healsMode) record(result, path, "dir", "update");
521
+ else result.filesSkipped.push(path);
415
522
  return;
416
523
  }
417
524
  await mkdir(path, {
@@ -419,7 +526,9 @@ async function ensureDir(path, mode, dryRun, result) {
419
526
  mode
420
527
  });
421
528
  if (mode) await chmod(path, mode).catch(() => void 0);
422
- result.filesWritten.push(path);
529
+ if (!existed) record(result, path, "dir", "create");
530
+ else if (healsMode) record(result, path, "dir", "update");
531
+ else result.filesSkipped.push(path);
423
532
  }
424
533
  async function writeText(path, contents, opts, result) {
425
534
  const existing = await readIfExists(path);
@@ -431,8 +540,9 @@ async function writeText(path, contents, opts, result) {
431
540
  hint: "Re-run with --force if you want the CLI to replace this file.",
432
541
  details: { path }
433
542
  });
543
+ const action = existing === void 0 ? "create" : "update";
434
544
  if (opts.dryRun) {
435
- result.filesWritten.push(path);
545
+ record(result, path, "file", action);
436
546
  return;
437
547
  }
438
548
  await mkdir(dirname(path), { recursive: true });
@@ -440,10 +550,11 @@ async function writeText(path, contents, opts, result) {
440
550
  await writeFile(tmp, contents, { mode: opts.mode });
441
551
  if (opts.mode) await chmod(tmp, opts.mode).catch(() => void 0);
442
552
  await rename(tmp, path);
443
- result.filesWritten.push(path);
553
+ record(result, path, "file", action);
444
554
  }
445
555
  async function appendText(path, contents, ifMissing, dryRun, result) {
446
- const existing = await readIfExists(path) ?? "";
556
+ const raw = await readIfExists(path);
557
+ const existing = raw ?? "";
447
558
  if (ifMissing && existing.includes(ifMissing)) {
448
559
  result.filesSkipped.push(path);
449
560
  return;
@@ -453,16 +564,18 @@ async function appendText(path, contents, ifMissing, dryRun, result) {
453
564
  result.filesSkipped.push(path);
454
565
  return;
455
566
  }
567
+ const action = raw === void 0 ? "create" : "update";
456
568
  if (dryRun) {
457
- result.filesWritten.push(path);
569
+ record(result, path, "file", action);
458
570
  return;
459
571
  }
460
572
  await mkdir(dirname(path), { recursive: true });
461
573
  await writeFile(path, next);
462
- result.filesWritten.push(path);
574
+ record(result, path, "file", action);
463
575
  }
464
576
  async function mergeEnv(path, entries, dryRun, result) {
465
- const existing = await readIfExists(path) ?? "";
577
+ const raw = await readIfExists(path);
578
+ const existing = raw ?? "";
466
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)));
467
580
  const additions = Object.entries(entries).filter(([key]) => !present.has(key));
468
581
  if (additions.length === 0) {
@@ -471,13 +584,14 @@ async function mergeEnv(path, entries, dryRun, result) {
471
584
  }
472
585
  const block = additions.map(([key, value]) => `${key}=${value}`).join("\n");
473
586
  const next = `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${block}\n`;
587
+ const action = raw === void 0 ? "create" : "update";
474
588
  if (dryRun) {
475
- result.filesWritten.push(path);
589
+ record(result, path, "file", action);
476
590
  return;
477
591
  }
478
592
  await mkdir(dirname(path), { recursive: true });
479
593
  await writeFile(path, next);
480
- result.filesWritten.push(path);
594
+ record(result, path, "file", action);
481
595
  }
482
596
  async function mergeJson(path, patch, dryRun, result) {
483
597
  const existing = await readIfExists(path);
@@ -486,16 +600,18 @@ async function mergeJson(path, patch, dryRun, result) {
486
600
  result.filesSkipped.push(path);
487
601
  return;
488
602
  }
603
+ const action = existing === void 0 ? "create" : "update";
489
604
  if (dryRun) {
490
- result.filesWritten.push(path);
605
+ record(result, path, "file", action);
491
606
  return;
492
607
  }
493
608
  await mkdir(dirname(path), { recursive: true });
494
609
  await writeFile(path, contents);
495
- result.filesWritten.push(path);
610
+ record(result, path, "file", action);
496
611
  }
497
612
  async function appendGitignore(path, entries, dryRun, result) {
498
- const existing = await readIfExists(path) ?? "";
613
+ const raw = await readIfExists(path);
614
+ const existing = raw ?? "";
499
615
  const lines = new Set(existing.split(/\r?\n/g).map((line) => line.trim()));
500
616
  const missing = entries.filter((entry) => !lines.has(entry));
501
617
  if (missing.length === 0) {
@@ -503,12 +619,13 @@ async function appendGitignore(path, entries, dryRun, result) {
503
619
  return;
504
620
  }
505
621
  const next = `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${missing.join("\n")}\n`;
622
+ const action = raw === void 0 ? "create" : "update";
506
623
  if (dryRun) {
507
- result.filesWritten.push(path);
624
+ record(result, path, "file", action);
508
625
  return;
509
626
  }
510
627
  await writeFile(path, next);
511
- result.filesWritten.push(path);
628
+ record(result, path, "file", action);
512
629
  }
513
630
  async function addDependency(path, op, dryRun, result) {
514
631
  const existing = await readIfExists(path);
@@ -520,20 +637,23 @@ async function addDependency(path, op, dryRun, result) {
520
637
  result.filesSkipped.push(path);
521
638
  return;
522
639
  }
523
- current[key] = {
640
+ const contents = setTopLevelJsonKey(existing, path, key, sortByKey({
524
641
  ...deps,
525
642
  [op.name]: op.version
526
- };
527
- const contents = `${stableStringify(current)}\n`;
643
+ }));
528
644
  if (dryRun) {
529
- result.filesWritten.push(path);
645
+ record(result, path, "file", "update");
530
646
  result.depsAdded.push(op.name);
531
647
  return;
532
648
  }
533
649
  await writeFile(path, contents);
534
- result.filesWritten.push(path);
650
+ record(result, path, "file", "update");
535
651
  result.depsAdded.push(op.name);
536
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
+ }
537
657
  async function readIfExists(path) {
538
658
  try {
539
659
  return await readFile(path, "utf8");
@@ -594,6 +714,10 @@ function agentsGuidanceSection(ctx) {
594
714
  const plan = publicCliCommand("plan", ctx.cliVersion);
595
715
  const apply = publicCliCommand("apply", ctx.cliVersion);
596
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-s`/`-m`/`-l`), the primary CTA pair (`--zl-primary` / `--zl-primary-foreground`), and the link color (`--zl-color-text-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.";
597
721
  return `## Authentication (Zitadel)
598
722
 
599
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.
@@ -610,6 +734,12 @@ The golden path:
610
734
  - \`${apply}\`
611
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.
612
736
 
737
+ ${presentationParagraph}${jsxTypesNote}
738
+
739
+ ${themingParagraph}
740
+
741
+ ${sessionStateParagraph}
742
+
613
743
  Machine-readable dialect (read these before authoring flow or schema edits):
614
744
 
615
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.
@@ -658,6 +788,70 @@ Guidance for AI agents working in this repository.
658
788
  function reclaimableOps(plan) {
659
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"));
660
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
+ }
661
855
  //#endregion
662
856
  //#region src/lib/orca/patchers/rule/base.ts
663
857
  /**
@@ -677,28 +871,131 @@ var AbstractRulePatcher = class {
677
871
  /**
678
872
  * Re-apply only the reclaimable subset — env files, gitignore, the SDK
679
873
  * dependency, and marker-bearing routes — leaving the user-editable
680
- * `.zitadel/` resources untouched. Backs `doctor --fix`.
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.
681
879
  */
682
880
  async repair(ctx, opts) {
683
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;
684
893
  return scaffold({
685
- ops: reclaimableOps(plan),
894
+ ops,
686
895
  summary: plan.summary
687
- }, opts);
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;
688
948
  }
689
949
  /** Shared base artifacts plus the subclass's marker-bearing route files. */
690
950
  artifacts(view) {
951
+ const infrastructure = new Set(this.infrastructureFiles(view));
952
+ const markedFiles = this.routeFiles(view);
691
953
  return {
692
- markedFiles: this.routeFiles(view),
954
+ markedFiles,
693
955
  rootConfigFiles: ["zitadel.json"],
694
956
  directories: [".zitadel"],
695
957
  envBackups: [".env.local"],
696
958
  dependencies: this.routeDeps(view),
697
959
  configEdits: this.routeConfigEdits(view),
698
- guidanceFiles: ["AGENTS.md", "README.md"]
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)
699
964
  };
700
965
  }
701
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
+ /**
702
999
  * User config files this patcher merges into via an `edit` op (e.g.
703
1000
  * `vite.config.ts`). `eject` can't reverse an in-place merge, so it lists
704
1001
  * these as manual cleanup steps. Defaults to none; patchers that edit a config
@@ -755,10 +1052,10 @@ var AbstractRulePatcher = class {
755
1052
  mode: 384,
756
1053
  contents: `${stableStringify({
757
1054
  project_id: ctx.project.id,
758
- project_secret: ctx.project.projectSecret,
759
- preview_secret: ctx.project.previewSecret,
760
- preview_origins: ctx.project.previewOrigins,
761
- created_at: ctx.project.createdAt
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
762
1059
  })}\n`
763
1060
  },
764
1061
  {
@@ -782,7 +1079,7 @@ var AbstractRulePatcher = class {
782
1079
  path: ".env.local",
783
1080
  entries: {
784
1081
  ZITADEL_PROJECT_ID: ctx.project.id,
785
- ZITADEL_PROJECT_SECRET: ctx.project.projectSecret,
1082
+ ZITADEL_PROJECT_SECRET: ctx.project.project_secret,
786
1083
  ZITADEL_ENVIRONMENT: "development",
787
1084
  ZITADEL_ISSUER: ctx.issuer,
788
1085
  ZITADEL_URL: ctx.server
@@ -803,7 +1100,7 @@ var AbstractRulePatcher = class {
803
1100
  ...metaSchemaFiles().map((file) => ({
804
1101
  kind: "write",
805
1102
  path: `${META_SCHEMA_DIR}/${file.name}`,
806
- contents: `${JSON.stringify(file.body, null, 2)}\n`
1103
+ contents: `${JSON.stringify(normalizePublicCliJson(file.body, ctx.cliVersion), null, 2)}\n`
807
1104
  })),
808
1105
  {
809
1106
  kind: "edit",
@@ -818,10 +1115,18 @@ var AbstractRulePatcher = class {
818
1115
  ];
819
1116
  }
820
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
+ }
821
1126
  /** Builds the `zitadel.json` body persisted at the project root. */
822
1127
  function projectConfig(ctx) {
823
1128
  const environments = { development: { issuer: ctx.issuer } };
824
- if (ctx.project.previewOrigins.length > 0) environments.preview = { issuer_pattern: [...ctx.project.previewOrigins] };
1129
+ if (ctx.project.preview_origins.length > 0) environments.preview = { issuer_pattern: [...ctx.project.preview_origins] };
825
1130
  return {
826
1131
  $schema: "https://schemas.zitadel.com/v2/project.schema.json",
827
1132
  project: ctx.project.id,
@@ -832,7 +1137,8 @@ function projectConfig(ctx) {
832
1137
  attribution: "visible"
833
1138
  },
834
1139
  environments,
835
- ...ctx.preset === void 0 ? {} : { preset: ctx.preset }
1140
+ ...ctx.preset === void 0 ? {} : { preset: ctx.preset },
1141
+ ...ctx.useCase === void 0 ? {} : { useCase: ctx.useCase }
836
1142
  };
837
1143
  }
838
1144
  /** Normalizes a server URL to its origin, falling back to {@link DEFAULT_SERVER}. */
@@ -1030,22 +1336,72 @@ function angularRoutesEdit() {
1030
1336
  */
1031
1337
  const PROXY_PATH = "/__nextgen";
1032
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
1033
1371
  //#region src/lib/orca/patchers/rule/angular/templates.ts
1034
1372
  /**
1035
1373
  * The managed root component `src/app/app.ts`: a standalone component that
1036
1374
  * renders the `@zitadel/sdk-angular` widgets based on the current path. The
1037
1375
  * project id (public, not secret) is inlined; the dev proxy in `proxy.conf.cjs`
1038
- * attaches the project service-key secret as the bearer server-side (read from
1039
- * `.env.local`), and no secret reaches the browser.
1040
- */
1041
- function appComponentTemplate(projectId) {
1042
- return `${MANAGED_MARKER}
1043
- import { Component, OnInit } from "@angular/core";
1044
- import {
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
+ ` : `
1045
1393
  ZitadelLoginComponent,
1046
1394
  ZitadelSessionComponent,
1047
1395
  configureZitadel,
1048
- } from "@zitadel/sdk-angular";
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";
1049
1405
 
1050
1406
  @Component({
1051
1407
  selector: "app-root",
@@ -1055,10 +1411,10 @@ import {
1055
1411
  })
1056
1412
  export class App implements OnInit {
1057
1413
  protected readonly project = configureZitadel({
1058
- projectId: ${JSON.stringify(projectId)},
1414
+ projectId: ${JSON.stringify(ctx.project.id)},
1059
1415
  proxyPath: "${PROXY_PATH}",
1060
1416
  });
1061
- protected readonly path = window.location.pathname;
1417
+ protected readonly path = window.location.pathname;${localesProperty}
1062
1418
 
1063
1419
  ngOnInit(): void {
1064
1420
  if (this.path === "/") {
@@ -1071,8 +1427,11 @@ export class App implements OnInit {
1071
1427
  /**
1072
1428
  * The managed `src/app/app.html`. The marker lives in an HTML comment that still
1073
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.
1074
1432
  */
1075
- function appTemplateHtml() {
1433
+ function appTemplateHtml(ctx) {
1434
+ const localesBinding = ctx.useCase === "business" ? `\n [locales]="locales"` : "";
1076
1435
  return `<!-- ${MANAGED_MARKER} -->
1077
1436
  @if (path.startsWith('/profile')) {
1078
1437
  <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
@@ -1084,7 +1443,7 @@ function appTemplateHtml() {
1084
1443
  } @else if (path.startsWith('/register')) {
1085
1444
  <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
1086
1445
  <zitadel-auth-login
1087
- [project]="project"
1446
+ [project]="project"${localesBinding}
1088
1447
  purpose="register"
1089
1448
  [postSignInUrl]="'/profile'"
1090
1449
  ></zitadel-auth-login>
@@ -1092,7 +1451,7 @@ function appTemplateHtml() {
1092
1451
  } @else {
1093
1452
  <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
1094
1453
  <zitadel-auth-login
1095
- [project]="project"
1454
+ [project]="project"${localesBinding}
1096
1455
  purpose="login"
1097
1456
  [postSignInUrl]="'/profile'"
1098
1457
  ></zitadel-auth-login>
@@ -1102,8 +1461,9 @@ function appTemplateHtml() {
1102
1461
  }
1103
1462
  /**
1104
1463
  * The managed `proxy.conf.cjs` for `ng serve`: forwards `/__nextgen/*` to the
1105
- * backend and attaches the project's service-key secret as the bearer on every
1106
- * proxied request. Both the backend URL (`ZITADEL_URL`) and the secret
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
1107
1467
  * (`ZITADEL_PROJECT_SECRET`) are read from `.env.local`, which `zitadel setup`
1108
1468
  * writes and `.gitignore` excludes — Angular's CLI does not auto-load env files
1109
1469
  * into the dev-server process, so this file does it itself with a small inline
@@ -1141,9 +1501,7 @@ if (!secret) {
1141
1501
  }
1142
1502
  const bearer = \`Bearer \${secret}\`;
1143
1503
 
1144
- function setBearer(proxyReq) {
1145
- proxyReq.setHeader("authorization", bearer);
1146
- }
1504
+ ${EXCHANGE_ONLY_BEARER}
1147
1505
 
1148
1506
  function stripPrefix(path) {
1149
1507
  return path.replace(/^\\${PROXY_PATH}/, "").replace(/^(?!\\/)/, "/");
@@ -1161,6 +1519,34 @@ module.exports = {
1161
1519
  };
1162
1520
  `;
1163
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
+ }
1164
1550
  //#endregion
1165
1551
  //#region src/lib/orca/patchers/rule/angular/index.ts
1166
1552
  const SDK_DEPENDENCY$6 = "@zitadel/sdk-angular";
@@ -1176,23 +1562,24 @@ function ensureDevScript(source) {
1176
1562
  const pkg = parseJsonObject(source, "package.json");
1177
1563
  const scripts = isObject(pkg.scripts) ? pkg.scripts : void 0;
1178
1564
  if (scripts?.dev !== void 0) return source;
1179
- pkg.scripts = {
1565
+ return setTopLevelJsonKey(source, "package.json", "scripts", {
1180
1566
  ...scripts ?? {},
1181
1567
  dev: "ng serve"
1182
- };
1183
- return `${stableStringify(pkg)}\n`;
1568
+ });
1184
1569
  }
1185
1570
  /**
1186
1571
  * Rule-based patcher for an Angular app. Inherits the shared `.zitadel/` base
1187
1572
  * files from {@link AbstractRulePatcher} and contributes the managed root
1188
1573
  * component (`app.ts`/`app.html`) that renders the `@zitadel/sdk-angular`
1189
1574
  * widgets, a `proxy.conf.cjs` dev proxy (attaching the project secret from
1190
- * `ZITADEL_PROJECT_SECRET` to every proxied request) wired into `angular.json`,
1575
+ * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`) wired into `angular.json`,
1191
1576
  * and the SDK dep.
1192
1577
  *
1193
1578
  * Unlike React/Vue (whose dev proxy lives in `vite.config.ts`), Angular owns its
1194
1579
  * Vite config, so the proxy is a separate `proxy.conf.cjs` referenced from the
1195
- * `serve` target. Production still needs `@zitadel/edge-proxy`.
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.
1196
1583
  */
1197
1584
  var AngularPatcher = class extends AbstractRulePatcher {
1198
1585
  canPatch(framework) {
@@ -1203,22 +1590,24 @@ var AngularPatcher = class extends AbstractRulePatcher {
1203
1590
  {
1204
1591
  kind: "write",
1205
1592
  path: "src/app/app.ts",
1206
- contents: appComponentTemplate(ctx.project.id)
1593
+ contents: appComponentTemplate(ctx)
1207
1594
  },
1208
1595
  {
1209
1596
  kind: "write",
1210
1597
  path: "src/app/app.html",
1211
- contents: appTemplateHtml()
1598
+ contents: appTemplateHtml(ctx)
1212
1599
  },
1213
1600
  {
1214
1601
  kind: "edit",
1215
1602
  path: "src/app/app.routes.ts",
1216
- edit: angularRoutesEdit()
1603
+ edit: angularRoutesEdit(),
1604
+ wiring: "infrastructure"
1217
1605
  },
1218
1606
  {
1219
- kind: "write",
1607
+ kind: "edit",
1220
1608
  path: "proxy.conf.cjs",
1221
- contents: proxyConfTemplate()
1609
+ edit: proxyConfEdit(),
1610
+ wiring: "infrastructure"
1222
1611
  },
1223
1612
  {
1224
1613
  kind: "edit",
@@ -1226,12 +1615,14 @@ var AngularPatcher = class extends AbstractRulePatcher {
1226
1615
  edit: angularProxyEdit({
1227
1616
  proxyConfig: "proxy.conf.cjs",
1228
1617
  port: ctx.framework.devPort
1229
- })
1618
+ }),
1619
+ wiring: "infrastructure"
1230
1620
  },
1231
1621
  {
1232
1622
  kind: "edit",
1233
1623
  path: "package.json",
1234
- edit: ensureDevScript
1624
+ edit: ensureDevScript,
1625
+ wiring: "convenience"
1235
1626
  },
1236
1627
  {
1237
1628
  kind: "add-dep",
@@ -1247,6 +1638,9 @@ var AngularPatcher = class extends AbstractRulePatcher {
1247
1638
  "proxy.conf.cjs"
1248
1639
  ];
1249
1640
  }
1641
+ infrastructureFiles(_view) {
1642
+ return ["proxy.conf.cjs"];
1643
+ }
1250
1644
  routeDeps(_view) {
1251
1645
  return [SDK_DEPENDENCY$6];
1252
1646
  }
@@ -1265,6 +1659,73 @@ var AngularPatcher = class extends AbstractRulePatcher {
1265
1659
  }
1266
1660
  };
1267
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
1268
1729
  //#region src/lib/orca/patchers/rule/next/renderers/lit/index.ts
1269
1730
  /**
1270
1731
  * Placeholder renderer for the `<zitadel-flow>` Lit web component. Declared
@@ -1341,6 +1802,15 @@ export default function ${mode === "login" ? "LoginPage" : "RegisterPage"}() {
1341
1802
  * forwards it to `ZITADEL_URL` server-side. `NEXT_PUBLIC_ZITADEL_PROJECT_ID` is
1342
1803
  * public — the project id is not sensitive and the widget needs it to start a
1343
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).
1344
1814
  */
1345
1815
  const reactRenderer = {
1346
1816
  id: "react",
@@ -1352,9 +1822,24 @@ const reactRenderer = {
1352
1822
  version: "latest"
1353
1823
  },
1354
1824
  templates: {
1355
- authPage(mode) {
1825
+ authPage(mode, context) {
1356
1826
  const componentName = mode === "login" ? "LoginPage" : "RegisterPage";
1357
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";
1358
1843
  return {
1359
1844
  mode,
1360
1845
  contents: `${MANAGED_MARKER}
@@ -1364,7 +1849,7 @@ import dynamic from "next/dynamic";
1364
1849
 
1365
1850
  const ${elementName} = dynamic(
1366
1851
  async () => {
1367
- const { configureZitadel } = await import("@zitadel/sdk-next/client");
1852
+ const { ${importNames} } = await import("@zitadel/sdk-next/client");
1368
1853
  // Build the SDK project handle and pass it to the component via the
1369
1854
  // \`project\` prop. The component reads config from this prop directly, so
1370
1855
  // it works regardless of how the SDK packages are bundled. The backend URL
@@ -1374,9 +1859,10 @@ const ${elementName} = dynamic(
1374
1859
  projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
1375
1860
  proxyPath: "/__nextgen",
1376
1861
  });
1377
- return function ${elementName}Element() {
1862
+ return function ${elementName}Element() {${localesComment}
1378
1863
  return (
1379
1864
  <zitadel-login
1865
+ ${widget ? `variant="widget"\n theme="auto"` : `variant="page"`}${localesAttr}
1380
1866
  project={project}
1381
1867
  purpose="${mode}"
1382
1868
  post-sign-in-url="/profile"
@@ -1388,16 +1874,29 @@ const ${elementName} = dynamic(
1388
1874
  );
1389
1875
 
1390
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.`}
1391
1889
  return (
1392
- <main style={{ minHeight: "100vh", colorScheme: "dark", background: "#0f0f11" }}>
1890
+ ${widget ? `<div style={{ display: "flex", justifyContent: "center", padding: "4rem 1rem" }}>` : `<main style={{ colorScheme: "dark" }}>`}
1393
1891
  <${elementName} />
1394
- </main>
1892
+ ${widget ? `</div>` : `</main>`}
1395
1893
  );
1396
1894
  }
1397
1895
  `
1398
1896
  };
1399
1897
  },
1400
- profilePage() {
1898
+ profilePage(context) {
1899
+ const widget = context.posture === "widget";
1401
1900
  return { contents: `${MANAGED_MARKER}
1402
1901
  "use client";
1403
1902
 
@@ -1408,7 +1907,9 @@ const ZitadelSession = dynamic(
1408
1907
  const { configureZitadel } = await import("@zitadel/sdk-next/client");
1409
1908
  // Build the SDK project handle and pass it to the session card via the
1410
1909
  // \`project\` prop. The card reads identity from "/__nextgen/sessions/me"
1411
- // and exposes a Sign out action.
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.
1412
1913
  const project = configureZitadel({
1413
1914
  projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
1414
1915
  proxyPath: "/__nextgen",
@@ -1416,6 +1917,7 @@ const ZitadelSession = dynamic(
1416
1917
  return function ZitadelSessionElement() {
1417
1918
  return (
1418
1919
  <zitadel-session
1920
+ ${widget ? `variant="widget"\n theme="auto"` : `variant="page"`}
1419
1921
  project={project}
1420
1922
  post-sign-out-url="/login"
1421
1923
  />
@@ -1426,42 +1928,32 @@ const ZitadelSession = dynamic(
1426
1928
  );
1427
1929
 
1428
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.`}
1429
1943
  return (
1430
- <main style={{ minHeight: "100vh", colorScheme: "dark", background: "#0f0f11" }}>
1944
+ ${widget ? `<div style={{ display: "flex", justifyContent: "center", padding: "4rem 1rem" }}>` : `<main style={{ colorScheme: "dark" }}>`}
1431
1945
  <ZitadelSession />
1432
- </main>
1946
+ ${widget ? `</div>` : `</main>`}
1433
1947
  );
1434
1948
  }
1435
1949
  ` };
1436
1950
  },
1437
1951
  customElementsDts() {
1438
1952
  return { contents: `${MANAGED_MARKER}
1439
- import type React from "react";
1440
- import type { ZitadelProject } from "@zitadel/sdk-next/client";
1441
-
1442
- declare module "react" {
1443
- namespace JSX {
1444
- interface IntrinsicElements {
1445
- "zitadel-login": React.HTMLAttributes<HTMLElement> & {
1446
- project?: ZitadelProject;
1447
- "session-exchange-path"?: string;
1448
- "post-sign-in-url"?: string;
1449
- purpose?: string;
1450
- "flow-name"?: string;
1451
- };
1452
- "zitadel-logout": React.HTMLAttributes<HTMLElement> & {
1453
- project?: ZitadelProject;
1454
- "post-sign-out-url"?: string;
1455
- };
1456
- "zitadel-session": React.HTMLAttributes<HTMLElement> & {
1457
- project?: ZitadelProject;
1458
- "post-sign-out-url"?: string;
1459
- heading?: string;
1460
- "logout-label"?: string;
1461
- };
1462
- }
1463
- }
1464
- }
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" />
1465
1957
  ` };
1466
1958
  }
1467
1959
  }
@@ -1491,6 +1983,15 @@ const RENDERERS = {
1491
1983
  "web-component": litRenderer
1492
1984
  };
1493
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
+ /**
1494
1995
  * Resolves a renderer id (an untrusted string from config) to its spec,
1495
1996
  * throwing a typed {@link ZitadelError} rather than returning `undefined`
1496
1997
  * so callers get an actionable message. Rejects ids that are unknown
@@ -1498,7 +1999,7 @@ const RENDERERS = {
1498
1999
  * guaranteeing the returned spec is safe to scaffold from.
1499
2000
  */
1500
2001
  function getRenderer(id) {
1501
- if (!isRendererId(id)) throw new ZitadelError("E_VALIDATION", `Unknown renderer "${id}"`, { hint: `Available renderers: ${Object.keys(RENDERERS).join(", ")}` });
2002
+ if (!isRendererId(id)) throw new ZitadelError("E_VALIDATION", `Unknown renderer "${id}"`, { hint: `Available renderers: ${AVAILABLE_RENDERER_IDS.join(", ")}` });
1502
2003
  const renderer = RENDERERS[id];
1503
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." });
1504
2005
  return renderer;
@@ -1546,9 +2047,22 @@ var NextPatcher = class extends AbstractRulePatcher {
1546
2047
  routeFiles(view) {
1547
2048
  return nextCodeFilePaths(view.framework, getRenderer(view.rendererId));
1548
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
+ }
1549
2060
  routeDeps(view) {
1550
2061
  return [getRenderer(view.rendererId).dependency.name];
1551
2062
  }
2063
+ routeConfigEdits(_view) {
2064
+ return ["package.json"];
2065
+ }
1552
2066
  summary(ctx) {
1553
2067
  return {
1554
2068
  title: "Next.js integration",
@@ -1575,10 +2089,28 @@ function nextCodeFilePaths(framework, renderer) {
1575
2089
  if (renderer.templates.customElementsDts) paths.push(join(appDir, "../custom-elements.d.ts"));
1576
2090
  return paths;
1577
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
+ }
1578
2106
  /** The Next route/request-boundary write ops plus the SDK dependency. */
1579
2107
  function nextCodeOps(ctx, renderer) {
1580
2108
  const appDir = ctx.framework.appDir;
1581
- const profile = renderer.templates.profilePage?.();
2109
+ const pageContext = {
2110
+ useCase: ctx.useCase,
2111
+ posture: ctx.posture
2112
+ };
2113
+ const profile = renderer.templates.profilePage?.(pageContext);
1582
2114
  const provider = renderer.templates.provider;
1583
2115
  const dts = renderer.templates.customElementsDts?.();
1584
2116
  const boundary = requestBoundaryFile(ctx.framework);
@@ -1586,17 +2118,18 @@ function nextCodeOps(ctx, renderer) {
1586
2118
  ctx.scaffoldedFramework ? {
1587
2119
  kind: "edit",
1588
2120
  path: join(appDir, "page.tsx"),
1589
- edit: () => homePageTemplate()
2121
+ edit: () => homePageTemplate(),
2122
+ overwrites: true
1590
2123
  } : void 0,
1591
2124
  {
1592
2125
  kind: "write",
1593
2126
  path: join(appDir, "login/page.tsx"),
1594
- contents: renderer.templates.authPage("login").contents
2127
+ contents: renderer.templates.authPage("login", pageContext).contents
1595
2128
  },
1596
2129
  {
1597
2130
  kind: "write",
1598
2131
  path: join(appDir, "register/page.tsx"),
1599
- contents: renderer.templates.authPage("register").contents
2132
+ contents: renderer.templates.authPage("register", pageContext).contents
1600
2133
  },
1601
2134
  profile ? {
1602
2135
  kind: "write",
@@ -1632,7 +2165,8 @@ function nextCodeOps(ctx, renderer) {
1632
2165
  kind: "add-dep",
1633
2166
  name: renderer.dependency.name,
1634
2167
  version: dependencyVersionForCli(ctx.cliVersion, renderer.dependency.version)
1635
- }
2168
+ },
2169
+ devScriptPortOp(ctx.issuer)
1636
2170
  ].filter((op) => op !== void 0);
1637
2171
  }
1638
2172
  function homePageTemplate() {
@@ -1755,7 +2289,6 @@ function nuxtConfigEdit(opts) {
1755
2289
  }
1756
2290
  //#endregion
1757
2291
  //#region src/lib/orca/patchers/rule/nuxt/templates.ts
1758
- const MAIN_STYLE = "min-height: 100vh; background: #0f0f11; color: #f4f4f6; color-scheme: dark";
1759
2292
  /** `app.vue` — renders the page router. Marker in an HTML comment. */
1760
2293
  function appVueTemplate() {
1761
2294
  return `<!-- ${MANAGED_MARKER} -->
@@ -1784,35 +2317,74 @@ await navigateTo("/login", { replace: true });
1784
2317
  <\/script>
1785
2318
  `;
1786
2319
  }
1787
- /** A login/register page rendering `<zitadel-login>` inside `<ClientOnly>`. */
1788
- function authPage(purpose) {
2320
+ /**
2321
+ * A login/register page rendering `<zitadel-login>` inside `<ClientOnly>`.
2322
+ * Business-use-case projects additionally bind the SDK's `businessLocales`
2323
+ * overlay, restoring work-email copy on top of the widget's neutral built-in
2324
+ * dictionaries. A plain `:locales` binding suffices even on the raw custom
2325
+ * element: Vue sets bindings whose key exists on the element as DOM
2326
+ * properties (unlike React 18, which is why the Next template needs a ref).
2327
+ */
2328
+ function authPage(purpose, ctx) {
2329
+ const business = ctx.useCase === "business";
2330
+ const importNames = business ? "businessLocales, useZitadelProject" : "useZitadelProject";
2331
+ const localesComment = business ? `
2332
+ // Set up for a business audience: businessLocales overlays work-email copy on
2333
+ // the login widget's neutral built-in dictionaries. Remove the :locales
2334
+ // binding to fall back to the neutral wording.` : "";
2335
+ const localesAttr = business ? "\n :locales=\"businessLocales\"" : "";
2336
+ const purposeAttr = purpose === "register" ? "\n purpose=\"register\"" : "";
2337
+ const widget = ctx.posture === "widget";
1789
2338
  return `<script setup lang="ts">
1790
2339
  ${MANAGED_MARKER}
1791
- import { useZitadelProject } from "@zitadel/sdk-nuxt";
2340
+ import { ${importNames} } from "@zitadel/sdk-nuxt";${localesComment}
1792
2341
 
1793
2342
  const project = useZitadelProject();
1794
2343
  <\/script>
1795
2344
 
1796
2345
  <template>
1797
- <main style="${MAIN_STYLE}">
2346
+ ${widget ? "<div style=\"display: flex; justify-content: center; padding: 4rem 1rem\">" : "<main style=\"color-scheme: dark\">"}
1798
2347
  <ClientOnly>
2348
+ ${widget ? `<!-- variant="widget" embeds the card in this app's own layout.
2349
+ theme="auto" follows the OS light/dark preference
2350
+ (prefers-color-scheme), not the app's own theme — set
2351
+ theme="light" or theme="dark" to match an app that pins its
2352
+ scheme. variant="page" would paint the widget's full-page
2353
+ chrome instead. -->` : `<!-- variant="page" paints the widget's full-page chrome from design
2354
+ tokens; variant="widget" embeds the card inside a layout you own. -->`}
1799
2355
  <zitadel-login
1800
- :project="project"${purpose === "register" ? "\n purpose=\"register\"" : ""}
2356
+ ${widget ? "variant=\"widget\"\n theme=\"auto\"" : "variant=\"page\""}
2357
+ :project="project"${localesAttr}${purposeAttr}
1801
2358
  post-sign-in-url="/profile"
1802
2359
  />
1803
2360
  </ClientOnly>
1804
- </main>
2361
+ ${widget ? "</div>" : "</main>"}
1805
2362
  </template>
1806
2363
  `;
1807
2364
  }
1808
- function loginPageTemplate() {
1809
- return authPage("login");
2365
+ function loginPageTemplate(ctx) {
2366
+ return authPage("login", ctx);
1810
2367
  }
1811
- function registerPageTemplate() {
1812
- return authPage("register");
2368
+ function registerPageTemplate(ctx) {
2369
+ return authPage("register", ctx);
1813
2370
  }
1814
2371
  /** `pages/profile.vue` — the post-sign-in "signed in as" session card. */
1815
- function profilePageTemplate() {
2372
+ function profilePageTemplate(ctx) {
2373
+ const widget = ctx.posture === "widget";
2374
+ const variantAttrs = widget ? "variant=\"widget\"\n theme=\"auto\"" : "variant=\"page\"";
2375
+ const postureComment = widget ? `<!-- variant="widget" embeds the session card in this app's own layout.
2376
+ theme="auto" follows the OS light/dark preference
2377
+ (prefers-color-scheme), not the app's own theme — set
2378
+ theme="light" or theme="dark" to match an app that pins its
2379
+ scheme. variant="page" would paint the card's full-page chrome
2380
+ instead. Your own components (a header, an account menu) read
2381
+ the same session state with the auto-imported useAuth()
2382
+ composable. -->` : `<!-- variant="page" paints the session card's full-page chrome from design
2383
+ tokens; variant="widget" embeds the card inside a layout you own.
2384
+ Your own components (a header, an account menu) read the same session
2385
+ state with the auto-imported useAuth() composable. -->`;
2386
+ const wrapperOpen = widget ? "<div style=\"display: flex; justify-content: center; padding: 4rem 1rem\">" : "<main style=\"color-scheme: dark\">";
2387
+ const wrapperClose = widget ? "</div>" : "</main>";
1816
2388
  return `<script setup lang="ts">
1817
2389
  ${MANAGED_MARKER}
1818
2390
  import { useZitadelProject } from "@zitadel/sdk-nuxt";
@@ -1821,11 +2393,16 @@ const project = useZitadelProject();
1821
2393
  <\/script>
1822
2394
 
1823
2395
  <template>
1824
- <main style="${MAIN_STYLE}">
2396
+ ${wrapperOpen}
1825
2397
  <ClientOnly>
1826
- <zitadel-session :project="project" post-sign-out-url="/login" />
2398
+ ${postureComment}
2399
+ ${widget ? `<zitadel-session
2400
+ ${variantAttrs}
2401
+ :project="project"
2402
+ post-sign-out-url="/login"
2403
+ />` : `<zitadel-session variant="page" :project="project" post-sign-out-url="/login" />`}
1827
2404
  </ClientOnly>
1828
- </main>
2405
+ ${wrapperClose}
1829
2406
  </template>
1830
2407
  `;
1831
2408
  }
@@ -1887,30 +2464,29 @@ var NuxtPatcher = class extends AbstractRulePatcher {
1887
2464
  routeOps(ctx) {
1888
2465
  const src = (rel) => join(ctx.framework.appDir, rel);
1889
2466
  return [
1890
- {
2467
+ ...ctx.scaffoldedFramework ? [{
1891
2468
  kind: "write",
1892
2469
  path: src("app.vue"),
1893
2470
  contents: appVueTemplate()
1894
- },
1895
- {
2471
+ }, {
1896
2472
  kind: "write",
1897
2473
  path: src("pages/index.vue"),
1898
2474
  contents: indexPageTemplate()
1899
- },
2475
+ }] : [],
1900
2476
  {
1901
2477
  kind: "write",
1902
2478
  path: src("pages/login.vue"),
1903
- contents: loginPageTemplate()
2479
+ contents: loginPageTemplate(ctx)
1904
2480
  },
1905
2481
  {
1906
2482
  kind: "write",
1907
2483
  path: src("pages/register.vue"),
1908
- contents: registerPageTemplate()
2484
+ contents: registerPageTemplate(ctx)
1909
2485
  },
1910
2486
  {
1911
2487
  kind: "write",
1912
2488
  path: src("pages/profile.vue"),
1913
- contents: profilePageTemplate()
2489
+ contents: profilePageTemplate(ctx)
1914
2490
  },
1915
2491
  {
1916
2492
  kind: "write",
@@ -1928,7 +2504,8 @@ var NuxtPatcher = class extends AbstractRulePatcher {
1928
2504
  edit: nuxtConfigEdit({
1929
2505
  projectId: ctx.project.id,
1930
2506
  server: ctx.server
1931
- })
2507
+ }),
2508
+ wiring: "infrastructure"
1932
2509
  },
1933
2510
  {
1934
2511
  kind: "merge-env",
@@ -1944,7 +2521,8 @@ var NuxtPatcher = class extends AbstractRulePatcher {
1944
2521
  kind: "add-dep",
1945
2522
  name: SDK_DEPENDENCY$5,
1946
2523
  version: npmDistTagForCliVersion(ctx.cliVersion)
1947
- }
2524
+ },
2525
+ devScriptPortOp(ctx.issuer)
1948
2526
  ];
1949
2527
  }
1950
2528
  routeFiles(view) {
@@ -1959,11 +2537,19 @@ var NuxtPatcher = class extends AbstractRulePatcher {
1959
2537
  src("plugins/auth.server.ts")
1960
2538
  ];
1961
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
+ }
1962
2548
  routeDeps(_view) {
1963
2549
  return [SDK_DEPENDENCY$5];
1964
2550
  }
1965
2551
  routeConfigEdits(_view) {
1966
- return ["nuxt.config.*"];
2552
+ return ["nuxt.config.*", "package.json"];
1967
2553
  }
1968
2554
  summary(_ctx) {
1969
2555
  return {
@@ -1979,13 +2565,13 @@ var NuxtPatcher = class extends AbstractRulePatcher {
1979
2565
  * frameworks (React, Vue, Solid, Svelte, Qwik). It forwards same-origin
1980
2566
  * `/__nextgen/*` calls to the
1981
2567
  * backend, strips the prefix, and attaches the project's service-key secret
1982
- * (read from `ZITADEL_PROJECT_SECRET` in `.env.local`) as the bearer on every
1983
- * proxied request. The secret stays server-side: Vite only exposes vars with the
1984
- * configured `envPrefix` (default `VITE_`) to client bundles, so this server-only
1985
- * key never leaks into the browser.
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.
1986
2572
  */
1987
2573
  function proxyEntryCode(server) {
1988
- return `{
2574
+ return `/* ${PROXY_CREDENTIAL_POLICY_MARKER} */ {
1989
2575
  target: ${JSON.stringify(server)},
1990
2576
  changeOrigin: false,
1991
2577
  rewrite: (path) => path.replace(/^\\${PROXY_PATH}/, "").replace(/^(?!\\/)/, "/"),
@@ -1996,11 +2582,45 @@ function proxyEntryCode(server) {
1996
2582
  }
1997
2583
  const bearer = \`Bearer \${secret}\`;
1998
2584
  proxy.on("proxyReq", (proxyReq) => {
1999
- proxyReq.setHeader("authorization", bearer);
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
+ }
2000
2593
  });
2001
2594
  },
2002
2595
  }`;
2003
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
+ }
2004
2624
  /** The imports that the injected proxy entry depends on. */
2005
2625
  const PROXY_IMPORTS = [{
2006
2626
  from: "vite",
@@ -2021,9 +2641,12 @@ const PROXY_IMPORTS = [{
2021
2641
  function viteProxyEdit(devPort, server) {
2022
2642
  return (source) => {
2023
2643
  const label = "the Vite config (vite.config.*)";
2024
- const mod = parseConfigModule(source, label);
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);
2025
2648
  const config = resolveDefaultExportObject(mod, label);
2026
- let changed = false;
2649
+ let changed = workingSource !== source;
2027
2650
  const serverConfig = ensureEditableObject(config, "server");
2028
2651
  if (serverConfig.port === void 0) {
2029
2652
  serverConfig.port = devPort;
@@ -2058,7 +2681,8 @@ function buildViteProxyOp(devPort, server) {
2058
2681
  return {
2059
2682
  kind: "edit",
2060
2683
  path: [...VITE_CONFIG_PATHS],
2061
- edit: viteProxyEdit(devPort, server)
2684
+ edit: viteProxyEdit(devPort, server),
2685
+ wiring: "infrastructure"
2062
2686
  };
2063
2687
  }
2064
2688
  //#endregion
@@ -2071,12 +2695,25 @@ function buildViteProxyOp(devPort, server) {
2071
2695
  * (`main.tsx` imports `{ App }`). The project id comes from
2072
2696
  * `VITE_ZITADEL_PROJECT_ID`. No secret reaches the browser: the dev proxy in
2073
2697
  * `vite.config.*` attaches the project service-key secret (from
2074
- * `ZITADEL_PROJECT_SECRET`) server-side.
2075
- */
2076
- function appTemplate$4() {
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}" : "";
2077
2714
  return `${MANAGED_MARKER}
2078
2715
  import { component$, useVisibleTask$ } from "@builder.io/qwik";
2079
- import { ZitadelLogin, ZitadelSession, configureZitadel } from "@zitadel/sdk-qwik";
2716
+ import { ${importNames} } from "@zitadel/sdk-qwik";${localesComment}
2080
2717
 
2081
2718
  const project = configureZitadel({
2082
2719
  projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
@@ -2105,13 +2742,13 @@ export const App = component$(() => {
2105
2742
  if (path.startsWith("/register")) {
2106
2743
  return (
2107
2744
  <div style={{ position: "fixed", inset: "0", overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
2108
- <ZitadelLogin project={project} purpose="register" postSignInUrl="/profile" />
2745
+ <ZitadelLogin project={project}${localesAttr} purpose="register" postSignInUrl="/profile" />
2109
2746
  </div>
2110
2747
  );
2111
2748
  }
2112
2749
  return (
2113
2750
  <div style={{ position: "fixed", inset: "0", overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
2114
- <ZitadelLogin project={project} purpose="login" postSignInUrl="/profile" />
2751
+ <ZitadelLogin project={project}${localesAttr} purpose="login" postSignInUrl="/profile" />
2115
2752
  </div>
2116
2753
  );
2117
2754
  });
@@ -2125,14 +2762,15 @@ const SDK_DEPENDENCY$4 = "@zitadel/sdk-qwik";
2125
2762
  * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
2126
2763
  * managed `src/app.tsx` auth entry, a non-destructive `vite.config.*` merge
2127
2764
  * that adds the `/__nextgen` dev proxy (attaching the project secret from
2128
- * `ZITADEL_PROJECT_SECRET` to every proxied request), the `VITE_`-prefixed
2765
+ * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
2129
2766
  * project id, and the SDK dep.
2130
2767
  *
2131
2768
  * The create-vite Qwik template uses a lowercase `src/app.tsx` exporting a named
2132
2769
  * `App` (mounted by `main.tsx`), so this patcher writes that exact entry. Unlike
2133
2770
  * Next.js — whose middleware runs the proxy server-side — a SPA has no server,
2134
- * so the dev proxy stands in for `@zitadel/edge-proxy` locally. Production
2135
- * deployments still need that proxy.
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.
2136
2774
  */
2137
2775
  var QwikPatcher = class extends AbstractRulePatcher {
2138
2776
  canPatch(framework) {
@@ -2146,7 +2784,7 @@ var QwikPatcher = class extends AbstractRulePatcher {
2146
2784
  {
2147
2785
  kind: "write",
2148
2786
  path: "src/app.tsx",
2149
- contents: appTemplate$4()
2787
+ contents: appTemplate$4(ctx)
2150
2788
  },
2151
2789
  this.viteProxyOp(ctx.framework.devPort, ctx.server),
2152
2790
  {
@@ -2191,12 +2829,27 @@ var QwikPatcher = class extends AbstractRulePatcher {
2191
2829
  * The project id comes from `VITE_ZITADEL_PROJECT_ID` (Vite only exposes
2192
2830
  * `VITE_`-prefixed env to the client). No secret reaches the browser: the dev
2193
2831
  * proxy in `vite.config.*` attaches the project service-key secret (from
2194
- * `ZITADEL_PROJECT_SECRET`) server-side.
2195
- */
2196
- function appTemplate$3() {
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}" : "";
2197
2850
  return `${MANAGED_MARKER}
2198
2851
  import { useEffect } from "react";
2199
- import { ZitadelLogin, ZitadelSession, configureZitadel } from "@zitadel/sdk-react";
2852
+ import { ${importNames} } from "@zitadel/sdk-react";${localesComment}
2200
2853
 
2201
2854
  const project = configureZitadel({
2202
2855
  projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
@@ -2225,13 +2878,13 @@ export default function App() {
2225
2878
  if (path.startsWith("/register")) {
2226
2879
  return (
2227
2880
  <div style={{ position: "fixed", inset: 0, overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
2228
- <ZitadelLogin project={project} purpose="register" postSignInUrl="/profile" />
2881
+ <ZitadelLogin project={project}${localesAttr} purpose="register" postSignInUrl="/profile" />
2229
2882
  </div>
2230
2883
  );
2231
2884
  }
2232
2885
  return (
2233
2886
  <div style={{ position: "fixed", inset: 0, overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
2234
- <ZitadelLogin project={project} purpose="login" postSignInUrl="/profile" />
2887
+ <ZitadelLogin project={project}${localesAttr} purpose="login" postSignInUrl="/profile" />
2235
2888
  </div>
2236
2889
  );
2237
2890
  }
@@ -2245,12 +2898,14 @@ const SDK_DEPENDENCY$3 = "@zitadel/sdk-react";
2245
2898
  * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
2246
2899
  * managed `src/App.tsx` auth entry, a non-destructive `vite.config.*` merge
2247
2900
  * that adds the `/__nextgen` dev proxy (attaching the project secret from
2248
- * `ZITADEL_PROJECT_SECRET` to every proxied request), the `VITE_`-prefixed
2901
+ * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
2249
2902
  * project id, and the SDK dep.
2250
2903
  *
2251
2904
  * Unlike Next.js — whose middleware runs the proxy and token exchange
2252
- * server-side — a SPA has no server, so the dev proxy stands in for
2253
- * `@zitadel/edge-proxy` locally. Production deployments still need that proxy.
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.
2254
2909
  */
2255
2910
  var ReactPatcher = class extends AbstractRulePatcher {
2256
2911
  canPatch(framework) {
@@ -2264,7 +2919,7 @@ var ReactPatcher = class extends AbstractRulePatcher {
2264
2919
  {
2265
2920
  kind: "write",
2266
2921
  path: "src/App.tsx",
2267
- contents: appTemplate$3()
2922
+ contents: appTemplate$3(ctx)
2268
2923
  },
2269
2924
  this.viteProxyOp(ctx.framework.devPort, ctx.server),
2270
2925
  {
@@ -2309,12 +2964,25 @@ var ReactPatcher = class extends AbstractRulePatcher {
2309
2964
  * The project id comes from `VITE_ZITADEL_PROJECT_ID` (Vite only exposes
2310
2965
  * `VITE_`-prefixed env to the client). No secret reaches the browser: the dev
2311
2966
  * proxy in `vite.config.*` attaches the project service-key secret (from
2312
- * `ZITADEL_PROJECT_SECRET`) server-side.
2313
- */
2314
- function appTemplate$2() {
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}" : "";
2315
2983
  return `${MANAGED_MARKER}
2316
2984
  import { onMount } from "solid-js";
2317
- import { ZitadelLogin, ZitadelSession, configureZitadel } from "@zitadel/sdk-solid";
2985
+ import { ${importNames} } from "@zitadel/sdk-solid";${localesComment}
2318
2986
 
2319
2987
  const project = configureZitadel({
2320
2988
  projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
@@ -2343,13 +3011,13 @@ export default function App() {
2343
3011
  if (path.startsWith("/register")) {
2344
3012
  return (
2345
3013
  <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
2346
- <ZitadelLogin project={project} purpose="register" postSignInUrl="/profile" />
3014
+ <ZitadelLogin project={project}${localesAttr} purpose="register" postSignInUrl="/profile" />
2347
3015
  </div>
2348
3016
  );
2349
3017
  }
2350
3018
  return (
2351
3019
  <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
2352
- <ZitadelLogin project={project} purpose="login" postSignInUrl="/profile" />
3020
+ <ZitadelLogin project={project}${localesAttr} purpose="login" postSignInUrl="/profile" />
2353
3021
  </div>
2354
3022
  );
2355
3023
  }
@@ -2363,12 +3031,14 @@ const SDK_DEPENDENCY$2 = "@zitadel/sdk-solid";
2363
3031
  * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
2364
3032
  * managed `src/App.tsx` auth entry, a non-destructive `vite.config.*` merge
2365
3033
  * that adds the `/__nextgen` dev proxy (attaching the project secret from
2366
- * `ZITADEL_PROJECT_SECRET` to every proxied request), the `VITE_`-prefixed
3034
+ * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
2367
3035
  * project id, and the SDK dep.
2368
3036
  *
2369
3037
  * Unlike Next.js — whose middleware runs the proxy and token exchange
2370
- * server-side — a SPA has no server, so the dev proxy stands in for
2371
- * `@zitadel/edge-proxy` locally. Production deployments still need that proxy.
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.
2372
3042
  */
2373
3043
  var SolidPatcher = class extends AbstractRulePatcher {
2374
3044
  canPatch(framework) {
@@ -2382,7 +3052,7 @@ var SolidPatcher = class extends AbstractRulePatcher {
2382
3052
  {
2383
3053
  kind: "write",
2384
3054
  path: "src/App.tsx",
2385
- contents: appTemplate$2()
3055
+ contents: appTemplate$2(ctx)
2386
3056
  },
2387
3057
  this.viteProxyOp(ctx.framework.devPort, ctx.server),
2388
3058
  {
@@ -2428,13 +3098,26 @@ var SolidPatcher = class extends AbstractRulePatcher {
2428
3098
  * eject/doctor stay marker-aware. The project id comes from
2429
3099
  * `VITE_ZITADEL_PROJECT_ID`. No secret reaches the browser: the dev proxy in
2430
3100
  * `vite.config.*` attaches the project service-key secret (from
2431
- * `ZITADEL_PROJECT_SECRET`) server-side.
2432
- */
2433
- function appTemplate$1() {
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}" : "";
2434
3117
  return `<script lang="ts">
2435
3118
  ${MANAGED_MARKER}
2436
3119
  import { onMount } from "svelte";
2437
- import { ZitadelLogin, ZitadelSession, configureZitadel } from "@zitadel/sdk-svelte";
3120
+ import { ${importNames} } from "@zitadel/sdk-svelte";${localesComment}
2438
3121
 
2439
3122
  const project = configureZitadel({
2440
3123
  projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
@@ -2456,11 +3139,11 @@ onMount(() => {
2456
3139
  </div>
2457
3140
  {:else if path.startsWith("/register")}
2458
3141
  <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
2459
- <ZitadelLogin {project} purpose="register" postSignInUrl="/profile" />
3142
+ <ZitadelLogin {project}${localesAttr} purpose="register" postSignInUrl="/profile" />
2460
3143
  </div>
2461
3144
  {:else if path !== "/"}
2462
3145
  <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
2463
- <ZitadelLogin {project} purpose="login" postSignInUrl="/profile" />
3146
+ <ZitadelLogin {project}${localesAttr} purpose="login" postSignInUrl="/profile" />
2464
3147
  </div>
2465
3148
  {/if}
2466
3149
  `;
@@ -2473,12 +3156,14 @@ const SDK_DEPENDENCY$1 = "@zitadel/sdk-svelte";
2473
3156
  * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
2474
3157
  * managed `src/App.svelte` auth entry, a non-destructive `vite.config.*` merge
2475
3158
  * that adds the `/__nextgen` dev proxy (attaching the project secret from
2476
- * `ZITADEL_PROJECT_SECRET` to every proxied request), the `VITE_`-prefixed
3159
+ * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
2477
3160
  * project id, and the SDK dep.
2478
3161
  *
2479
3162
  * Unlike Next.js — whose middleware runs the proxy and token exchange
2480
- * server-side — a SPA has no server, so the dev proxy stands in for
2481
- * `@zitadel/edge-proxy` locally. Production deployments still need that proxy.
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.
2482
3167
  */
2483
3168
  var SveltePatcher = class extends AbstractRulePatcher {
2484
3169
  canPatch(framework) {
@@ -2492,7 +3177,7 @@ var SveltePatcher = class extends AbstractRulePatcher {
2492
3177
  {
2493
3178
  kind: "write",
2494
3179
  path: "src/App.svelte",
2495
- contents: appTemplate$1()
3180
+ contents: appTemplate$1(ctx)
2496
3181
  },
2497
3182
  this.viteProxyOp(ctx.framework.devPort, ctx.server),
2498
3183
  {
@@ -2538,13 +3223,26 @@ var SveltePatcher = class extends AbstractRulePatcher {
2538
3223
  * eject/doctor stay marker-aware. The project id comes from
2539
3224
  * `VITE_ZITADEL_PROJECT_ID`. No secret reaches the browser: the dev proxy in
2540
3225
  * `vite.config.*` attaches the project service-key secret (from
2541
- * `ZITADEL_PROJECT_SECRET`) server-side.
2542
- */
2543
- function appTemplate() {
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\"" : "";
2544
3242
  return `<script setup lang="ts">
2545
3243
  ${MANAGED_MARKER}
2546
3244
  import { onMounted } from "vue";
2547
- import { ZitadelLogin, ZitadelSession, configureZitadel } from "@zitadel/sdk-vue";
3245
+ import { ${importNames} } from "@zitadel/sdk-vue";${localesComment}
2548
3246
 
2549
3247
  const project = configureZitadel({
2550
3248
  projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
@@ -2571,10 +3269,10 @@ onMounted(() => {
2571
3269
  v-else-if="path.startsWith('/register')"
2572
3270
  style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark"
2573
3271
  >
2574
- <ZitadelLogin :project="project" purpose="register" postSignInUrl="/profile" />
3272
+ <ZitadelLogin :project="project"${localesAttr} purpose="register" postSignInUrl="/profile" />
2575
3273
  </div>
2576
3274
  <div v-else-if="path !== '/'" style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
2577
- <ZitadelLogin :project="project" purpose="login" postSignInUrl="/profile" />
3275
+ <ZitadelLogin :project="project"${localesAttr} purpose="login" postSignInUrl="/profile" />
2578
3276
  </div>
2579
3277
  </template>
2580
3278
  `;
@@ -2587,7 +3285,7 @@ const SDK_DEPENDENCY = "@zitadel/sdk-vue";
2587
3285
  * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
2588
3286
  * managed `src/App.vue` auth entry, a non-destructive `vite.config.*` merge
2589
3287
  * that adds the `/__nextgen` dev proxy (attaching the project secret from
2590
- * `ZITADEL_PROJECT_SECRET` to every proxied request), the `VITE_`-prefixed
3288
+ * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
2591
3289
  * project id, and the SDK dep.
2592
3290
  */
2593
3291
  var VuePatcher = class extends AbstractRulePatcher {
@@ -2602,7 +3300,7 @@ var VuePatcher = class extends AbstractRulePatcher {
2602
3300
  {
2603
3301
  kind: "write",
2604
3302
  path: "src/App.vue",
2605
- contents: appTemplate()
3303
+ contents: appTemplate(ctx)
2606
3304
  },
2607
3305
  this.viteProxyOp(ctx.framework.devPort, ctx.server),
2608
3306
  {
@@ -3214,6 +3912,6 @@ function isErrno(error, code) {
3214
3912
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
3215
3913
  }
3216
3914
  //#endregion
3217
- export { removeGuidanceSection as a, AGENTS_HEADER as i, inspectScaffoldTarget as n, issuerFromPort as o, RENDERER_IDS as r, createOrca as t };
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 };
3218
3916
 
3219
- //# sourceMappingURL=orca-Dip6DPJ2.mjs.map
3917
+ //# sourceMappingURL=orca-Cu0iW1mv.mjs.map