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

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 (58) hide show
  1. package/README.md +145 -107
  2. package/SKILLS.md +88 -12
  3. package/dist/branding-BXj028-W.mjs +79 -0
  4. package/dist/branding-BXj028-W.mjs.map +1 -0
  5. package/dist/claim-state-BTGInAaN.mjs +66 -0
  6. package/dist/claim-state-BTGInAaN.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 +125 -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 +477 -37
  14. package/dist/commands/doctor.mjs.map +1 -1
  15. package/dist/commands/eject.mjs +3 -3
  16. package/dist/commands/logs.mjs +2 -2
  17. package/dist/commands/plan.mjs +6 -5
  18. package/dist/commands/plan.mjs.map +1 -1
  19. package/dist/commands/reset.mjs +2 -2
  20. package/dist/commands/schemas/list.mjs +8 -8
  21. package/dist/commands/schemas/list.mjs.map +1 -1
  22. package/dist/commands/setup.mjs +305 -36
  23. package/dist/commands/setup.mjs.map +1 -1
  24. package/dist/commands/start.mjs +3 -3
  25. package/dist/commands/status.mjs +21 -13
  26. package/dist/commands/status.mjs.map +1 -1
  27. package/dist/commands/stop.mjs +3 -3
  28. package/dist/commands/stop.mjs.map +1 -1
  29. package/dist/{docker-pGcTrDIY.mjs → docker-Ciz9N6uI.mjs} +2 -2
  30. package/dist/{docker-pGcTrDIY.mjs.map → docker-Ciz9N6uI.mjs.map} +1 -1
  31. package/dist/{environment-BQF7LeCz.mjs → environment-CuHKxtIO.mjs} +1 -1
  32. package/dist/{environment-BQF7LeCz.mjs.map → environment-CuHKxtIO.mjs.map} +1 -1
  33. package/dist/{journey-guidance-CYVpQszz.mjs → journey-guidance-CtQUfWar.mjs} +14 -3
  34. package/dist/{journey-guidance-CYVpQszz.mjs.map → journey-guidance-CtQUfWar.mjs.map} +1 -1
  35. package/dist/{oclif-BRdjVNne.mjs → oclif-CL6-M13m.mjs} +201 -43
  36. package/dist/oclif-CL6-M13m.mjs.map +1 -0
  37. package/dist/{orca-Dip6DPJ2.mjs → orca-BWB4zOdw.mjs} +749 -192
  38. package/dist/orca-BWB4zOdw.mjs.map +1 -0
  39. package/dist/{ports-V8JfiNgT.mjs → ports-Cu2Y7aXQ.mjs} +2 -2
  40. package/dist/{ports-V8JfiNgT.mjs.map → ports-Cu2Y7aXQ.mjs.map} +1 -1
  41. package/dist/{processes-BVqYsxT8.mjs → processes-Bn2WngOi.mjs} +1 -1
  42. package/dist/{processes-BVqYsxT8.mjs.map → processes-Bn2WngOi.mjs.map} +1 -1
  43. package/dist/{project-BGgv7Bba.mjs → project-yejJSfdJ.mjs} +57 -4
  44. package/dist/project-yejJSfdJ.mjs.map +1 -0
  45. package/dist/scaffold-manifest-CKNFLX9T.mjs +81 -0
  46. package/dist/scaffold-manifest-CKNFLX9T.mjs.map +1 -0
  47. package/dist/{sync-Cd3z7Q1R.mjs → sync-Cx2_4rXW.mjs} +99 -50
  48. package/dist/sync-Cx2_4rXW.mjs.map +1 -0
  49. package/dist/user-schema-W1OsXdpc.mjs +91 -0
  50. package/dist/user-schema-W1OsXdpc.mjs.map +1 -0
  51. package/oclif.manifest.json +227 -5
  52. package/package.json +8 -6
  53. package/dist/oclif-BRdjVNne.mjs.map +0 -1
  54. package/dist/orca-Dip6DPJ2.mjs.map +0 -1
  55. package/dist/project-BGgv7Bba.mjs.map +0 -1
  56. package/dist/sync-Cd3z7Q1R.mjs.map +0 -1
  57. package/dist/user-schema-DDz5-lX5.mjs +0 -13
  58. 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 { C as parseJsonObject, E as ZitadelError, O as npmDistTagForCliVersion, S as isObject, T as stableStringify, b as MANAGED_MARKER, k as publicCliCommand, n as DEFAULT_SERVER, w as setTopLevelJsonKey } from "./oclif-CL6-M13m.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
  /**
@@ -136,15 +158,18 @@ var NextDetector = class {
136
158
  async detect(cwd) {
137
159
  const pkg = await readPackageJson(cwd).catch(() => void 0);
138
160
  if (!pkg || !hasDependency(pkg, "next")) return null;
161
+ const belowFloor = dependencySpecProvablyBelowMajor(pkg, "next", 15);
162
+ 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
163
  const appDir = await dirExists$1(join(cwd, "app")) ? "app" : await dirExists$1(join(cwd, "src/app")) ? "src/app" : void 0;
140
164
  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
165
  const devPort = await detectDevPort(cwd, pkg);
166
+ const versionMajor = dependencyVersionMajor(pkg, "next");
142
167
  return {
143
168
  id: "next",
144
169
  appDir,
145
170
  devPort,
146
171
  url: issuerFromPort(devPort),
147
- versionMajor: dependencyVersionMajor(pkg, "next")
172
+ ...versionMajor === void 0 ? {} : { versionMajor }
148
173
  };
149
174
  }
150
175
  };
@@ -224,12 +249,16 @@ var ReactDetector = class {
224
249
  async detect(cwd) {
225
250
  const pkg = await readPackageJson(cwd).catch(() => void 0);
226
251
  if (!pkg || hasDependency(pkg, "next") || !hasDependency(pkg, "react") || !hasDependency(pkg, "vite")) return null;
252
+ const belowFloor = dependencySpecProvablyBelowMajor(pkg, "react", 18);
253
+ 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
254
  const devPort = await detectDevPort(cwd, pkg);
255
+ const versionMajor = dependencyVersionMajor(pkg, "react");
228
256
  return {
229
257
  id: "react",
230
258
  appDir: "src",
231
259
  devPort,
232
- url: issuerFromPort(devPort)
260
+ url: issuerFromPort(devPort),
261
+ ...versionMajor === void 0 ? {} : { versionMajor }
233
262
  };
234
263
  }
235
264
  };
@@ -320,6 +349,21 @@ const detectors = [
320
349
  //#endregion
321
350
  //#region src/lib/orca/patchers/rule/file-writer/index.ts
322
351
  /**
352
+ * Records one touched artifact, deduplicating by path: several plan ops can
353
+ * legitimately hit the same file (the base and framework op lists both merge
354
+ * into `.env.local`, for example), but the report should carry it once, with
355
+ * the first action as the net one — a file created and then extended in the
356
+ * same run was created by the run.
357
+ */
358
+ function record(result, path, kind, action) {
359
+ if (result.files.some((file) => file.path === path)) return;
360
+ result.files.push({
361
+ path,
362
+ kind,
363
+ action
364
+ });
365
+ }
366
+ /**
323
367
  * Applies a {@link ScaffoldPlan} to disk, executing its operations in order.
324
368
  *
325
369
  * Operations are idempotent: writes whose target already matches the desired
@@ -332,12 +376,19 @@ const detectors = [
332
376
  async function scaffold(plan, opts) {
333
377
  const result = {
334
378
  dryRun: opts.dryRun,
335
- filesWritten: [],
379
+ files: [],
336
380
  filesSkipped: [],
337
381
  depsAdded: []
338
382
  };
339
383
  for (const op of plan.ops) await applyOp(op, opts, result);
340
- return result;
384
+ const written = new Set(result.files.map((file) => file.path));
385
+ return {
386
+ dryRun: result.dryRun,
387
+ files: result.files,
388
+ filesWritten: result.files.filter((file) => file.kind === "file").map((file) => file.path),
389
+ filesSkipped: [...new Set(result.filesSkipped)].filter((path) => !written.has(path)),
390
+ depsAdded: result.depsAdded
391
+ };
341
392
  }
342
393
  async function applyOp(op, opts, result) {
343
394
  switch (op.kind) {
@@ -398,8 +449,9 @@ async function editFile(cwd, pathOrPaths, edit, dryRun, result) {
398
449
  result.filesSkipped.push(path);
399
450
  return;
400
451
  }
452
+ const action = source === void 0 ? "create" : "update";
401
453
  if (dryRun) {
402
- result.filesWritten.push(path);
454
+ record(result, path, "file", action);
403
455
  return;
404
456
  }
405
457
  await mkdir(dirname(path), { recursive: true });
@@ -407,11 +459,16 @@ async function editFile(cwd, pathOrPaths, edit, dryRun, result) {
407
459
  await writeFile(tmp, next);
408
460
  if (mode !== void 0) await chmod(tmp, mode).catch(() => void 0);
409
461
  await rename(tmp, path);
410
- result.filesWritten.push(path);
462
+ record(result, path, "file", action);
411
463
  }
412
464
  async function ensureDir(path, mode, dryRun, result) {
465
+ const pre = await stat(path).catch(() => void 0);
466
+ const existed = pre?.isDirectory() ?? false;
467
+ const healsMode = existed && mode !== void 0 && process.platform !== "win32" && (pre.mode & 511) !== mode;
413
468
  if (dryRun) {
414
- result.filesWritten.push(path);
469
+ if (!existed) record(result, path, "dir", "create");
470
+ else if (healsMode) record(result, path, "dir", "update");
471
+ else result.filesSkipped.push(path);
415
472
  return;
416
473
  }
417
474
  await mkdir(path, {
@@ -419,7 +476,9 @@ async function ensureDir(path, mode, dryRun, result) {
419
476
  mode
420
477
  });
421
478
  if (mode) await chmod(path, mode).catch(() => void 0);
422
- result.filesWritten.push(path);
479
+ if (!existed) record(result, path, "dir", "create");
480
+ else if (healsMode) record(result, path, "dir", "update");
481
+ else result.filesSkipped.push(path);
423
482
  }
424
483
  async function writeText(path, contents, opts, result) {
425
484
  const existing = await readIfExists(path);
@@ -431,8 +490,9 @@ async function writeText(path, contents, opts, result) {
431
490
  hint: "Re-run with --force if you want the CLI to replace this file.",
432
491
  details: { path }
433
492
  });
493
+ const action = existing === void 0 ? "create" : "update";
434
494
  if (opts.dryRun) {
435
- result.filesWritten.push(path);
495
+ record(result, path, "file", action);
436
496
  return;
437
497
  }
438
498
  await mkdir(dirname(path), { recursive: true });
@@ -440,10 +500,11 @@ async function writeText(path, contents, opts, result) {
440
500
  await writeFile(tmp, contents, { mode: opts.mode });
441
501
  if (opts.mode) await chmod(tmp, opts.mode).catch(() => void 0);
442
502
  await rename(tmp, path);
443
- result.filesWritten.push(path);
503
+ record(result, path, "file", action);
444
504
  }
445
505
  async function appendText(path, contents, ifMissing, dryRun, result) {
446
- const existing = await readIfExists(path) ?? "";
506
+ const raw = await readIfExists(path);
507
+ const existing = raw ?? "";
447
508
  if (ifMissing && existing.includes(ifMissing)) {
448
509
  result.filesSkipped.push(path);
449
510
  return;
@@ -453,16 +514,18 @@ async function appendText(path, contents, ifMissing, dryRun, result) {
453
514
  result.filesSkipped.push(path);
454
515
  return;
455
516
  }
517
+ const action = raw === void 0 ? "create" : "update";
456
518
  if (dryRun) {
457
- result.filesWritten.push(path);
519
+ record(result, path, "file", action);
458
520
  return;
459
521
  }
460
522
  await mkdir(dirname(path), { recursive: true });
461
523
  await writeFile(path, next);
462
- result.filesWritten.push(path);
524
+ record(result, path, "file", action);
463
525
  }
464
526
  async function mergeEnv(path, entries, dryRun, result) {
465
- const existing = await readIfExists(path) ?? "";
527
+ const raw = await readIfExists(path);
528
+ const existing = raw ?? "";
466
529
  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
530
  const additions = Object.entries(entries).filter(([key]) => !present.has(key));
468
531
  if (additions.length === 0) {
@@ -471,13 +534,14 @@ async function mergeEnv(path, entries, dryRun, result) {
471
534
  }
472
535
  const block = additions.map(([key, value]) => `${key}=${value}`).join("\n");
473
536
  const next = `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${block}\n`;
537
+ const action = raw === void 0 ? "create" : "update";
474
538
  if (dryRun) {
475
- result.filesWritten.push(path);
539
+ record(result, path, "file", action);
476
540
  return;
477
541
  }
478
542
  await mkdir(dirname(path), { recursive: true });
479
543
  await writeFile(path, next);
480
- result.filesWritten.push(path);
544
+ record(result, path, "file", action);
481
545
  }
482
546
  async function mergeJson(path, patch, dryRun, result) {
483
547
  const existing = await readIfExists(path);
@@ -486,16 +550,18 @@ async function mergeJson(path, patch, dryRun, result) {
486
550
  result.filesSkipped.push(path);
487
551
  return;
488
552
  }
553
+ const action = existing === void 0 ? "create" : "update";
489
554
  if (dryRun) {
490
- result.filesWritten.push(path);
555
+ record(result, path, "file", action);
491
556
  return;
492
557
  }
493
558
  await mkdir(dirname(path), { recursive: true });
494
559
  await writeFile(path, contents);
495
- result.filesWritten.push(path);
560
+ record(result, path, "file", action);
496
561
  }
497
562
  async function appendGitignore(path, entries, dryRun, result) {
498
- const existing = await readIfExists(path) ?? "";
563
+ const raw = await readIfExists(path);
564
+ const existing = raw ?? "";
499
565
  const lines = new Set(existing.split(/\r?\n/g).map((line) => line.trim()));
500
566
  const missing = entries.filter((entry) => !lines.has(entry));
501
567
  if (missing.length === 0) {
@@ -503,12 +569,13 @@ async function appendGitignore(path, entries, dryRun, result) {
503
569
  return;
504
570
  }
505
571
  const next = `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${missing.join("\n")}\n`;
572
+ const action = raw === void 0 ? "create" : "update";
506
573
  if (dryRun) {
507
- result.filesWritten.push(path);
574
+ record(result, path, "file", action);
508
575
  return;
509
576
  }
510
577
  await writeFile(path, next);
511
- result.filesWritten.push(path);
578
+ record(result, path, "file", action);
512
579
  }
513
580
  async function addDependency(path, op, dryRun, result) {
514
581
  const existing = await readIfExists(path);
@@ -520,20 +587,23 @@ async function addDependency(path, op, dryRun, result) {
520
587
  result.filesSkipped.push(path);
521
588
  return;
522
589
  }
523
- current[key] = {
590
+ const contents = setTopLevelJsonKey(existing, path, key, sortByKey({
524
591
  ...deps,
525
592
  [op.name]: op.version
526
- };
527
- const contents = `${stableStringify(current)}\n`;
593
+ }));
528
594
  if (dryRun) {
529
- result.filesWritten.push(path);
595
+ record(result, path, "file", "update");
530
596
  result.depsAdded.push(op.name);
531
597
  return;
532
598
  }
533
599
  await writeFile(path, contents);
534
- result.filesWritten.push(path);
600
+ record(result, path, "file", "update");
535
601
  result.depsAdded.push(op.name);
536
602
  }
603
+ /** Rebuilds an object with lexicographically sorted keys (dependency maps). */
604
+ function sortByKey(value) {
605
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
606
+ }
537
607
  async function readIfExists(path) {
538
608
  try {
539
609
  return await readFile(path, "utf8");
@@ -594,6 +664,9 @@ function agentsGuidanceSection(ctx) {
594
664
  const plan = publicCliCommand("plan", ctx.cliVersion);
595
665
  const apply = publicCliCommand("apply", ctx.cliVersion);
596
666
  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." : "";
667
+ 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`." : "";
668
+ 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." : "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.";
669
+ 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. 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
670
  return `## Authentication (Zitadel)
598
671
 
599
672
  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 +683,10 @@ The golden path:
610
683
  - \`${apply}\`
611
684
  - 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
685
 
686
+ ${presentationParagraph}${jsxTypesNote}
687
+
688
+ ${sessionStateParagraph}
689
+
613
690
  Machine-readable dialect (read these before authoring flow or schema edits):
614
691
 
615
692
  - 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 +735,70 @@ Guidance for AI agents working in this repository.
658
735
  function reclaimableOps(plan) {
659
736
  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
737
  }
738
+ /**
739
+ * Narrow a reclaimable op list to what a missing-only repair may replay:
740
+ * content ops (`write`, and `edit` marked `overwrites`) survive only when
741
+ * their target file does not exist, so the repair can restore a deleted
742
+ * managed file but can never touch an edited or user-adopted one. Merging
743
+ * `edit` transforms and the additive kinds (`merge-env`, `append-gitignore`)
744
+ * pass through — they are idempotent and only add what is missing. `add-dep`
745
+ * is additive only while the dependency is absent: `addDependency` replaces
746
+ * a differing existing version, so under missing-only semantics the op is
747
+ * dropped whenever the dependency is already declared at *any* version —
748
+ * repairing an unrelated missing file must never rewrite a user-pinned
749
+ * range. Reads the filesystem; paths resolve against `cwd` like the
750
+ * file-writer's.
751
+ */
752
+ async function withoutExistingTargets(ops, cwd) {
753
+ const kept = [];
754
+ for (const op of ops) {
755
+ if (op.kind === "write") {
756
+ if (!await exists(join(cwd, op.path))) kept.push(op);
757
+ continue;
758
+ }
759
+ if (op.kind === "edit" && op.overwrites) {
760
+ const candidates = typeof op.path === "string" ? [op.path] : op.path;
761
+ if (!(await Promise.all(candidates.map((candidate) => exists(join(cwd, candidate))))).includes(true)) kept.push(op);
762
+ continue;
763
+ }
764
+ if (op.kind === "add-dep") {
765
+ if (!await dependencyDeclared(cwd, op.name)) kept.push(op);
766
+ continue;
767
+ }
768
+ kept.push(op);
769
+ }
770
+ return kept;
771
+ }
772
+ /**
773
+ * Whether `package.json` already declares `name` in dependencies or
774
+ * devDependencies, at any version. A missing or unparsable `package.json`
775
+ * counts as "not declared" so the op is kept and the file-writer's own
776
+ * error handling reports the real problem.
777
+ */
778
+ async function dependencyDeclared(cwd, name) {
779
+ let contents;
780
+ try {
781
+ contents = await readFile(join(cwd, "package.json"), "utf8");
782
+ } catch {
783
+ return false;
784
+ }
785
+ try {
786
+ const pkg = JSON.parse(contents);
787
+ if (!isObject(pkg)) return false;
788
+ const declared = (key) => isObject(pkg[key]) && name in pkg[key];
789
+ return declared("dependencies") || declared("devDependencies");
790
+ } catch {
791
+ return false;
792
+ }
793
+ }
794
+ async function exists(path) {
795
+ try {
796
+ await access(path);
797
+ return true;
798
+ } catch {
799
+ return false;
800
+ }
801
+ }
661
802
  //#endregion
662
803
  //#region src/lib/orca/patchers/rule/base.ts
663
804
  /**
@@ -677,28 +818,131 @@ var AbstractRulePatcher = class {
677
818
  /**
678
819
  * Re-apply only the reclaimable subset — env files, gitignore, the SDK
679
820
  * dependency, and marker-bearing routes — leaving the user-editable
680
- * `.zitadel/` resources untouched. Backs `doctor --fix`.
821
+ * `.zitadel/` resources untouched. Backs `doctor --fix`. With
822
+ * `opts.missingOnly` the content ops are further narrowed to files that do
823
+ * not exist (see {@link withoutExistingTargets}) and the writer runs without
824
+ * `force`, so the repair restores deleted managed files and can never
825
+ * overwrite an edited or user-adopted one.
681
826
  */
682
827
  async repair(ctx, opts) {
683
828
  const plan = this.plan(ctx);
829
+ let ops = opts.missingOnly ? await withoutExistingTargets(reclaimableOps(plan), opts.cwd) : reclaimableOps(plan);
830
+ const excluded = new Set(opts.excludePaths ?? []);
831
+ if (excluded.size > 0) ops = ops.filter((op) => {
832
+ if (op.kind === "write") return !excluded.has(op.path);
833
+ if (op.kind === "edit" && op.overwrites) return !(typeof op.path === "string" ? [op.path] : op.path).some((candidate) => excluded.has(candidate));
834
+ return true;
835
+ });
836
+ const execOpts = opts.missingOnly ? {
837
+ ...opts,
838
+ force: false
839
+ } : opts;
684
840
  return scaffold({
685
- ops: reclaimableOps(plan),
841
+ ops,
686
842
  summary: plan.summary
687
- }, opts);
843
+ }, execOpts);
844
+ }
845
+ /**
846
+ * Probe whether the labelled config wirings are still applied, using each
847
+ * merge transform's own idempotency: the transforms only add what is
848
+ * missing and return the source unchanged when everything they manage is
849
+ * present, so a changed output means the wiring is absent from the user's
850
+ * config. Every labelled wiring gets a verdict: a missing host file is
851
+ * `detached` (the wiring cannot be applied in a file that does not exist),
852
+ * and a transform that throws on restructured content yields `unknown` —
853
+ * surfaced rather than silently dropped, so a deleted `angular.json` can
854
+ * never read as healthy. Read-only.
855
+ */
856
+ async verify(ctx, opts) {
857
+ const statuses = [];
858
+ for (const op of this.plan(ctx).ops) {
859
+ if (op.kind !== "edit" || !op.wiring) continue;
860
+ const candidates = typeof op.path === "string" ? [op.path] : op.path;
861
+ let path = candidates[0];
862
+ let source;
863
+ for (const candidate of candidates) {
864
+ const contents = await readTextIfExists(join(opts.cwd, candidate));
865
+ if (contents !== void 0) {
866
+ path = candidate;
867
+ source = contents;
868
+ break;
869
+ }
870
+ }
871
+ if (source === void 0) {
872
+ statuses.push({
873
+ path,
874
+ wiring: op.wiring,
875
+ state: "detached"
876
+ });
877
+ continue;
878
+ }
879
+ try {
880
+ statuses.push({
881
+ path,
882
+ wiring: op.wiring,
883
+ state: op.edit(source) === source ? "applied" : "detached"
884
+ });
885
+ } catch (error) {
886
+ statuses.push({
887
+ path,
888
+ wiring: op.wiring,
889
+ state: "unknown",
890
+ reason: error instanceof Error ? error.message : "verification failed"
891
+ });
892
+ }
893
+ }
894
+ return statuses;
688
895
  }
689
896
  /** Shared base artifacts plus the subclass's marker-bearing route files. */
690
897
  artifacts(view) {
898
+ const infrastructure = new Set(this.infrastructureFiles(view));
899
+ const markedFiles = this.routeFiles(view);
691
900
  return {
692
- markedFiles: this.routeFiles(view),
901
+ markedFiles,
693
902
  rootConfigFiles: ["zitadel.json"],
694
903
  directories: [".zitadel"],
695
904
  envBackups: [".env.local"],
696
905
  dependencies: this.routeDeps(view),
697
906
  configEdits: this.routeConfigEdits(view),
698
- guidanceFiles: ["AGENTS.md", "README.md"]
907
+ guidanceFiles: ["AGENTS.md", "README.md"],
908
+ fileClasses: Object.fromEntries(markedFiles.map((path) => [path, infrastructure.has(path) ? "infrastructure" : "presentation"])),
909
+ conditionalFiles: this.conditionallyScaffoldedFiles(view),
910
+ retiredAlternates: this.retiredAlternateFiles(view)
699
911
  };
700
912
  }
701
913
  /**
914
+ * Current marked files mapped to sibling paths that older templates wrote
915
+ * in their place and the framework rejects alongside them. Drives the
916
+ * managed-files boundary migration. Defaults to none; Next overrides it
917
+ * for the `middleware.ts`/`proxy.ts` pair.
918
+ */
919
+ retiredAlternateFiles(_view) {
920
+ return {};
921
+ }
922
+ /**
923
+ * The subset of {@link routeFiles} that is load-bearing for the integration
924
+ * (request boundary, proxies, plugins, type declarations). The `doctor`
925
+ * managed-files check fails when one is missing; everything else is a
926
+ * presentation starting point and only warns. Every patcher whose marked
927
+ * files include plumbing must override this — Next (boundary/provider/dts),
928
+ * Angular (proxy.conf.cjs), and Nuxt (plugins) do. The Vite SPAs' only
929
+ * marked file is the root component (presentation); their plumbing lives in
930
+ * `vite.config.*`, which is a config-edit merge into a user file and out of
931
+ * the file check's reach. Defaults to none.
932
+ */
933
+ infrastructureFiles(_view) {
934
+ return [];
935
+ }
936
+ /**
937
+ * Marked files only written on some scaffolds (e.g. a framework home page
938
+ * that setup replaces only when it created the app skeleton itself). The
939
+ * managed-files check excludes them when no scaffold manifest recorded what
940
+ * was actually written. Defaults to none.
941
+ */
942
+ conditionallyScaffoldedFiles(_view) {
943
+ return [];
944
+ }
945
+ /**
702
946
  * User config files this patcher merges into via an `edit` op (e.g.
703
947
  * `vite.config.ts`). `eject` can't reverse an in-place merge, so it lists
704
948
  * these as manual cleanup steps. Defaults to none; patchers that edit a config
@@ -755,10 +999,10 @@ var AbstractRulePatcher = class {
755
999
  mode: 384,
756
1000
  contents: `${stableStringify({
757
1001
  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
1002
+ project_secret: ctx.project.project_secret,
1003
+ preview_secret: ctx.project.preview_secret,
1004
+ preview_origins: ctx.project.preview_origins,
1005
+ created_at: ctx.project.created_at
762
1006
  })}\n`
763
1007
  },
764
1008
  {
@@ -782,7 +1026,7 @@ var AbstractRulePatcher = class {
782
1026
  path: ".env.local",
783
1027
  entries: {
784
1028
  ZITADEL_PROJECT_ID: ctx.project.id,
785
- ZITADEL_PROJECT_SECRET: ctx.project.projectSecret,
1029
+ ZITADEL_PROJECT_SECRET: ctx.project.project_secret,
786
1030
  ZITADEL_ENVIRONMENT: "development",
787
1031
  ZITADEL_ISSUER: ctx.issuer,
788
1032
  ZITADEL_URL: ctx.server
@@ -818,10 +1062,18 @@ var AbstractRulePatcher = class {
818
1062
  ];
819
1063
  }
820
1064
  };
1065
+ /** Reads a text file, mapping every failure to `undefined`. */
1066
+ async function readTextIfExists(path) {
1067
+ try {
1068
+ return await readFile(path, "utf8");
1069
+ } catch {
1070
+ return;
1071
+ }
1072
+ }
821
1073
  /** Builds the `zitadel.json` body persisted at the project root. */
822
1074
  function projectConfig(ctx) {
823
1075
  const environments = { development: { issuer: ctx.issuer } };
824
- if (ctx.project.previewOrigins.length > 0) environments.preview = { issuer_pattern: [...ctx.project.previewOrigins] };
1076
+ if (ctx.project.preview_origins.length > 0) environments.preview = { issuer_pattern: [...ctx.project.preview_origins] };
825
1077
  return {
826
1078
  $schema: "https://schemas.zitadel.com/v2/project.schema.json",
827
1079
  project: ctx.project.id,
@@ -832,7 +1084,8 @@ function projectConfig(ctx) {
832
1084
  attribution: "visible"
833
1085
  },
834
1086
  environments,
835
- ...ctx.preset === void 0 ? {} : { preset: ctx.preset }
1087
+ ...ctx.preset === void 0 ? {} : { preset: ctx.preset },
1088
+ ...ctx.useCase === void 0 ? {} : { useCase: ctx.useCase }
836
1089
  };
837
1090
  }
838
1091
  /** Normalizes a server URL to its origin, falling back to {@link DEFAULT_SERVER}. */
@@ -1030,22 +1283,72 @@ function angularRoutesEdit() {
1030
1283
  */
1031
1284
  const PROXY_PATH = "/__nextgen";
1032
1285
  //#endregion
1286
+ //#region src/lib/orca/patchers/rule/proxy-credential-policy.ts
1287
+ /** Marker emitted after the exchange-only credential policy was introduced. */
1288
+ const PROXY_CREDENTIAL_POLICY_MARKER = "zitadel:proxy:v2";
1289
+ /**
1290
+ * Rejects a browser-reachable proxy that still appears to attach the project
1291
+ * secret without the complete exchange-only guard. Known generated legacy
1292
+ * bytes are migrated before this runs; anything else stays user-owned and is
1293
+ * surfaced for manual review instead of being silently counted as healthy by
1294
+ * `zitadel doctor`.
1295
+ */
1296
+ function assertNoUnreviewedProjectSecretProxy(source, label) {
1297
+ if (!source.includes("/__nextgen") || !source.includes("ZITADEL_PROJECT_SECRET") || !/authorization/i.test(source) || hasExchangeOnlyGuard(source)) return;
1298
+ 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." });
1299
+ }
1300
+ function hasExchangeOnlyGuard(source) {
1301
+ const setterOffsets = [...source.matchAll(/proxyReq\s*\.\s*setHeader\s*\(\s*["']authorization["']\s*,/gi)].map((match) => match.index);
1302
+ if (setterOffsets.length === 0) return false;
1303
+ const safeBodyRanges = [];
1304
+ for (const match of source.matchAll(/if\s*\(([\s\S]*?)\)\s*\{([\s\S]*?)\}/g)) {
1305
+ const condition = match[1];
1306
+ const body = match[2];
1307
+ 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;
1308
+ const bodyOffset = match[0].lastIndexOf(body);
1309
+ const start = match.index + bodyOffset;
1310
+ safeBodyRanges.push({
1311
+ start,
1312
+ end: start + body.length
1313
+ });
1314
+ }
1315
+ return setterOffsets.every((offset) => safeBodyRanges.some(({ start, end }) => offset >= start && offset < end));
1316
+ }
1317
+ //#endregion
1033
1318
  //#region src/lib/orca/patchers/rule/angular/templates.ts
1034
1319
  /**
1035
1320
  * The managed root component `src/app/app.ts`: a standalone component that
1036
1321
  * renders the `@zitadel/sdk-angular` widgets based on the current path. The
1037
1322
  * 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 {
1323
+ * attaches the project service-key secret as the bearer server-side only to
1324
+ * `POST /sessions/exchange` (read from `.env.local`), and no secret reaches
1325
+ * the browser.
1326
+ *
1327
+ * Projects set up with the business use case additionally expose the SDK's
1328
+ * `businessLocales` overlay as a class property, which `app.html` binds to the
1329
+ * login widgets via `[locales]` — restoring work-email copy on top of the
1330
+ * widget's neutral built-in dictionaries.
1331
+ */
1332
+ function appComponentTemplate(ctx) {
1333
+ const business = ctx.useCase === "business";
1334
+ const importNames = business ? `
1045
1335
  ZitadelLoginComponent,
1046
1336
  ZitadelSessionComponent,
1337
+ businessLocales,
1047
1338
  configureZitadel,
1048
- } from "@zitadel/sdk-angular";
1339
+ ` : `
1340
+ ZitadelLoginComponent,
1341
+ ZitadelSessionComponent,
1342
+ configureZitadel,
1343
+ `;
1344
+ const localesProperty = business ? `
1345
+ // Set up for a business audience: businessLocales overlays work-email copy
1346
+ // on the login widget's neutral built-in dictionaries. Remove it (and the
1347
+ // [locales] binding in app.html) to fall back to the neutral wording.
1348
+ protected readonly locales = businessLocales;` : "";
1349
+ return `${MANAGED_MARKER}
1350
+ import { Component, OnInit } from "@angular/core";
1351
+ import {${importNames}} from "@zitadel/sdk-angular";
1049
1352
 
1050
1353
  @Component({
1051
1354
  selector: "app-root",
@@ -1055,10 +1358,10 @@ import {
1055
1358
  })
1056
1359
  export class App implements OnInit {
1057
1360
  protected readonly project = configureZitadel({
1058
- projectId: ${JSON.stringify(projectId)},
1361
+ projectId: ${JSON.stringify(ctx.project.id)},
1059
1362
  proxyPath: "${PROXY_PATH}",
1060
1363
  });
1061
- protected readonly path = window.location.pathname;
1364
+ protected readonly path = window.location.pathname;${localesProperty}
1062
1365
 
1063
1366
  ngOnInit(): void {
1064
1367
  if (this.path === "/") {
@@ -1071,8 +1374,11 @@ export class App implements OnInit {
1071
1374
  /**
1072
1375
  * The managed `src/app/app.html`. The marker lives in an HTML comment that still
1073
1376
  * contains the literal managed-marker text, so eject/doctor stay marker-aware.
1377
+ * Business-use-case scaffolds bind the component's `locales` overlay property
1378
+ * onto the login widgets.
1074
1379
  */
1075
- function appTemplateHtml() {
1380
+ function appTemplateHtml(ctx) {
1381
+ const localesBinding = ctx.useCase === "business" ? `\n [locales]="locales"` : "";
1076
1382
  return `<!-- ${MANAGED_MARKER} -->
1077
1383
  @if (path.startsWith('/profile')) {
1078
1384
  <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
@@ -1084,7 +1390,7 @@ function appTemplateHtml() {
1084
1390
  } @else if (path.startsWith('/register')) {
1085
1391
  <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
1086
1392
  <zitadel-auth-login
1087
- [project]="project"
1393
+ [project]="project"${localesBinding}
1088
1394
  purpose="register"
1089
1395
  [postSignInUrl]="'/profile'"
1090
1396
  ></zitadel-auth-login>
@@ -1092,7 +1398,7 @@ function appTemplateHtml() {
1092
1398
  } @else {
1093
1399
  <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
1094
1400
  <zitadel-auth-login
1095
- [project]="project"
1401
+ [project]="project"${localesBinding}
1096
1402
  purpose="login"
1097
1403
  [postSignInUrl]="'/profile'"
1098
1404
  ></zitadel-auth-login>
@@ -1102,8 +1408,9 @@ function appTemplateHtml() {
1102
1408
  }
1103
1409
  /**
1104
1410
  * 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
1411
+ * backend and attaches the project's service-key secret only to the current
1412
+ * app-plane `POST /sessions/exchange` request. Both the backend URL
1413
+ * (`ZITADEL_URL`) and the secret
1107
1414
  * (`ZITADEL_PROJECT_SECRET`) are read from `.env.local`, which `zitadel setup`
1108
1415
  * writes and `.gitignore` excludes — Angular's CLI does not auto-load env files
1109
1416
  * into the dev-server process, so this file does it itself with a small inline
@@ -1141,9 +1448,7 @@ if (!secret) {
1141
1448
  }
1142
1449
  const bearer = \`Bearer \${secret}\`;
1143
1450
 
1144
- function setBearer(proxyReq) {
1145
- proxyReq.setHeader("authorization", bearer);
1146
- }
1451
+ ${EXCHANGE_ONLY_BEARER}
1147
1452
 
1148
1453
  function stripPrefix(path) {
1149
1454
  return path.replace(/^\\${PROXY_PATH}/, "").replace(/^(?!\\/)/, "/");
@@ -1161,6 +1466,34 @@ module.exports = {
1161
1466
  };
1162
1467
  `;
1163
1468
  }
1469
+ const LEGACY_UNCONDITIONAL_BEARER = `function setBearer(proxyReq) {
1470
+ proxyReq.setHeader("authorization", bearer);
1471
+ }`;
1472
+ const EXCHANGE_ONLY_BEARER = `/* ${PROXY_CREDENTIAL_POLICY_MARKER} */
1473
+ function setBearer(proxyReq) {
1474
+ const pathname = new URL(proxyReq.path, "http://zitadel.local").pathname;
1475
+ if (
1476
+ proxyReq.method === "POST" &&
1477
+ pathname === "/sessions/exchange" &&
1478
+ !proxyReq.getHeader("authorization")
1479
+ ) {
1480
+ proxyReq.setHeader("authorization", bearer);
1481
+ }
1482
+ }`;
1483
+ /**
1484
+ * Creates the managed Angular proxy and upgrades the exact insecure hook the
1485
+ * CLI emitted before the exchange-only policy. Other edits are preserved; a
1486
+ * safe custom proxy is left untouched, while an unrecognized proxy that may
1487
+ * still over-forward the project secret is surfaced for manual review.
1488
+ */
1489
+ function proxyConfEdit() {
1490
+ return (source) => {
1491
+ if (source === void 0) return proxyConfTemplate();
1492
+ if (source.includes("// zitadel-cli: managed-file v1") && source.includes(LEGACY_UNCONDITIONAL_BEARER)) return source.replace(LEGACY_UNCONDITIONAL_BEARER, EXCHANGE_ONLY_BEARER);
1493
+ assertNoUnreviewedProjectSecretProxy(source, "proxy.conf.cjs");
1494
+ return source;
1495
+ };
1496
+ }
1164
1497
  //#endregion
1165
1498
  //#region src/lib/orca/patchers/rule/angular/index.ts
1166
1499
  const SDK_DEPENDENCY$6 = "@zitadel/sdk-angular";
@@ -1176,23 +1509,24 @@ function ensureDevScript(source) {
1176
1509
  const pkg = parseJsonObject(source, "package.json");
1177
1510
  const scripts = isObject(pkg.scripts) ? pkg.scripts : void 0;
1178
1511
  if (scripts?.dev !== void 0) return source;
1179
- pkg.scripts = {
1512
+ return setTopLevelJsonKey(source, "package.json", "scripts", {
1180
1513
  ...scripts ?? {},
1181
1514
  dev: "ng serve"
1182
- };
1183
- return `${stableStringify(pkg)}\n`;
1515
+ });
1184
1516
  }
1185
1517
  /**
1186
1518
  * Rule-based patcher for an Angular app. Inherits the shared `.zitadel/` base
1187
1519
  * files from {@link AbstractRulePatcher} and contributes the managed root
1188
1520
  * component (`app.ts`/`app.html`) that renders the `@zitadel/sdk-angular`
1189
1521
  * widgets, a `proxy.conf.cjs` dev proxy (attaching the project secret from
1190
- * `ZITADEL_PROJECT_SECRET` to every proxied request) wired into `angular.json`,
1522
+ * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`) wired into `angular.json`,
1191
1523
  * and the SDK dep.
1192
1524
  *
1193
1525
  * Unlike React/Vue (whose dev proxy lives in `vite.config.ts`), Angular owns its
1194
1526
  * Vite config, so the proxy is a separate `proxy.conf.cjs` referenced from the
1195
- * `serve` target. Production still needs `@zitadel/edge-proxy`.
1527
+ * `serve` target. In production the same-origin path comes from a platform
1528
+ * rewrite or minimal worker (ADR 036); CLI scaffolding for it is tracked in
1529
+ * issue #560.
1196
1530
  */
1197
1531
  var AngularPatcher = class extends AbstractRulePatcher {
1198
1532
  canPatch(framework) {
@@ -1203,22 +1537,24 @@ var AngularPatcher = class extends AbstractRulePatcher {
1203
1537
  {
1204
1538
  kind: "write",
1205
1539
  path: "src/app/app.ts",
1206
- contents: appComponentTemplate(ctx.project.id)
1540
+ contents: appComponentTemplate(ctx)
1207
1541
  },
1208
1542
  {
1209
1543
  kind: "write",
1210
1544
  path: "src/app/app.html",
1211
- contents: appTemplateHtml()
1545
+ contents: appTemplateHtml(ctx)
1212
1546
  },
1213
1547
  {
1214
1548
  kind: "edit",
1215
1549
  path: "src/app/app.routes.ts",
1216
- edit: angularRoutesEdit()
1550
+ edit: angularRoutesEdit(),
1551
+ wiring: "infrastructure"
1217
1552
  },
1218
1553
  {
1219
- kind: "write",
1554
+ kind: "edit",
1220
1555
  path: "proxy.conf.cjs",
1221
- contents: proxyConfTemplate()
1556
+ edit: proxyConfEdit(),
1557
+ wiring: "infrastructure"
1222
1558
  },
1223
1559
  {
1224
1560
  kind: "edit",
@@ -1226,12 +1562,14 @@ var AngularPatcher = class extends AbstractRulePatcher {
1226
1562
  edit: angularProxyEdit({
1227
1563
  proxyConfig: "proxy.conf.cjs",
1228
1564
  port: ctx.framework.devPort
1229
- })
1565
+ }),
1566
+ wiring: "infrastructure"
1230
1567
  },
1231
1568
  {
1232
1569
  kind: "edit",
1233
1570
  path: "package.json",
1234
- edit: ensureDevScript
1571
+ edit: ensureDevScript,
1572
+ wiring: "convenience"
1235
1573
  },
1236
1574
  {
1237
1575
  kind: "add-dep",
@@ -1247,6 +1585,9 @@ var AngularPatcher = class extends AbstractRulePatcher {
1247
1585
  "proxy.conf.cjs"
1248
1586
  ];
1249
1587
  }
1588
+ infrastructureFiles(_view) {
1589
+ return ["proxy.conf.cjs"];
1590
+ }
1250
1591
  routeDeps(_view) {
1251
1592
  return [SDK_DEPENDENCY$6];
1252
1593
  }
@@ -1341,6 +1682,15 @@ export default function ${mode === "login" ? "LoginPage" : "RegisterPage"}() {
1341
1682
  * forwards it to `ZITADEL_URL` server-side. `NEXT_PUBLIC_ZITADEL_PROJECT_ID` is
1342
1683
  * public — the project id is not sensitive and the widget needs it to start a
1343
1684
  * flow.
1685
+ *
1686
+ * Projects set up with the business use case additionally wire the SDK's
1687
+ * `businessLocales` overlay onto the auth pages, restoring work-email copy on
1688
+ * top of the widget's neutral built-in dictionaries. The overlay is assigned
1689
+ * via a ref callback, not a JSX prop: sdk-next supports React >=18, and only
1690
+ * React 19 binds non-primitive custom-element props as properties — on 18 a
1691
+ * `locales={...}` prop would decay to a useless attribute and silently keep
1692
+ * the neutral copy (the `project` prop tolerates this because
1693
+ * `configureZitadel()` registers a global fallback; `locales` has none).
1344
1694
  */
1345
1695
  const reactRenderer = {
1346
1696
  id: "react",
@@ -1352,9 +1702,24 @@ const reactRenderer = {
1352
1702
  version: "latest"
1353
1703
  },
1354
1704
  templates: {
1355
- authPage(mode) {
1705
+ authPage(mode, context) {
1356
1706
  const componentName = mode === "login" ? "LoginPage" : "RegisterPage";
1357
1707
  const elementName = mode === "login" ? "ZitadelLogin" : "ZitadelRegister";
1708
+ const business = context.useCase === "business";
1709
+ const importNames = business ? "businessLocales, configureZitadel" : "configureZitadel";
1710
+ const localesComment = business ? `
1711
+ // Set up for a business audience: businessLocales overlays work-email
1712
+ // copy on the widget's neutral built-in dictionaries. It is assigned
1713
+ // through the ref because React binds non-primitive custom-element
1714
+ // props as properties only from v19 — the ref works on React 18 too.
1715
+ // Remove the assignment to fall back to the neutral wording.` : "";
1716
+ const localesAttr = business ? `
1717
+ ref={(element) => {
1718
+ if (element) {
1719
+ element.locales = businessLocales;
1720
+ }
1721
+ }}` : "";
1722
+ const widget = context.posture === "widget";
1358
1723
  return {
1359
1724
  mode,
1360
1725
  contents: `${MANAGED_MARKER}
@@ -1364,7 +1729,7 @@ import dynamic from "next/dynamic";
1364
1729
 
1365
1730
  const ${elementName} = dynamic(
1366
1731
  async () => {
1367
- const { configureZitadel } = await import("@zitadel/sdk-next/client");
1732
+ const { ${importNames} } = await import("@zitadel/sdk-next/client");
1368
1733
  // Build the SDK project handle and pass it to the component via the
1369
1734
  // \`project\` prop. The component reads config from this prop directly, so
1370
1735
  // it works regardless of how the SDK packages are bundled. The backend URL
@@ -1374,9 +1739,10 @@ const ${elementName} = dynamic(
1374
1739
  projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
1375
1740
  proxyPath: "/__nextgen",
1376
1741
  });
1377
- return function ${elementName}Element() {
1742
+ return function ${elementName}Element() {${localesComment}
1378
1743
  return (
1379
1744
  <zitadel-login
1745
+ ${widget ? `variant="widget"\n theme="auto"` : `variant="page"`}${localesAttr}
1380
1746
  project={project}
1381
1747
  purpose="${mode}"
1382
1748
  post-sign-in-url="/profile"
@@ -1388,16 +1754,24 @@ const ${elementName} = dynamic(
1388
1754
  );
1389
1755
 
1390
1756
  export default function ${componentName}() {
1757
+ ${widget ? ` // variant="widget" embeds the sign-in card in this app's existing layout,
1758
+ // and theme="auto" follows its color scheme; the wrapper below only centers
1759
+ // the card — restyle or replace it freely. Switch to variant="page" for the
1760
+ // widget's own full-page chrome (viewport height, surface background from
1761
+ // design tokens).` : ` // variant="page" makes the widget paint the full-page chrome itself
1762
+ // (viewport height, surface background) from design tokens; switch to
1763
+ // variant="widget" to embed the sign-in card inside a layout you own.`}
1391
1764
  return (
1392
- <main style={{ minHeight: "100vh", colorScheme: "dark", background: "#0f0f11" }}>
1765
+ ${widget ? `<div style={{ display: "flex", justifyContent: "center", padding: "4rem 1rem" }}>` : `<main style={{ colorScheme: "dark" }}>`}
1393
1766
  <${elementName} />
1394
- </main>
1767
+ ${widget ? `</div>` : `</main>`}
1395
1768
  );
1396
1769
  }
1397
1770
  `
1398
1771
  };
1399
1772
  },
1400
- profilePage() {
1773
+ profilePage(context) {
1774
+ const widget = context.posture === "widget";
1401
1775
  return { contents: `${MANAGED_MARKER}
1402
1776
  "use client";
1403
1777
 
@@ -1408,7 +1782,9 @@ const ZitadelSession = dynamic(
1408
1782
  const { configureZitadel } = await import("@zitadel/sdk-next/client");
1409
1783
  // Build the SDK project handle and pass it to the session card via the
1410
1784
  // \`project\` prop. The card reads identity from "/__nextgen/sessions/me"
1411
- // and exposes a Sign out action.
1785
+ // and exposes a Sign out action. Your own components (a header, an
1786
+ // account menu) can make the same read with getSession() from
1787
+ // "@zitadel/sdk-next/session" to swap sign-in CTAs for account chrome.
1412
1788
  const project = configureZitadel({
1413
1789
  projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
1414
1790
  proxyPath: "/__nextgen",
@@ -1416,6 +1792,7 @@ const ZitadelSession = dynamic(
1416
1792
  return function ZitadelSessionElement() {
1417
1793
  return (
1418
1794
  <zitadel-session
1795
+ ${widget ? `variant="widget"\n theme="auto"` : `variant="page"`}
1419
1796
  project={project}
1420
1797
  post-sign-out-url="/login"
1421
1798
  />
@@ -1426,42 +1803,27 @@ const ZitadelSession = dynamic(
1426
1803
  );
1427
1804
 
1428
1805
  export default function ProfilePage() {
1806
+ ${widget ? ` // variant="widget" embeds the session card in this app's existing layout,
1807
+ // and theme="auto" follows its color scheme; the wrapper below only centers
1808
+ // the card — restyle or replace it freely. Switch to variant="page" for the
1809
+ // card's own full-page chrome (viewport height, surface background from
1810
+ // design tokens).` : ` // variant="page" makes the session card paint the full-page chrome itself
1811
+ // (viewport height, surface background) from design tokens; switch to
1812
+ // variant="widget" to embed the card inside a layout you own.`}
1429
1813
  return (
1430
- <main style={{ minHeight: "100vh", colorScheme: "dark", background: "#0f0f11" }}>
1814
+ ${widget ? `<div style={{ display: "flex", justifyContent: "center", padding: "4rem 1rem" }}>` : `<main style={{ colorScheme: "dark" }}>`}
1431
1815
  <ZitadelSession />
1432
- </main>
1816
+ ${widget ? `</div>` : `</main>`}
1433
1817
  );
1434
1818
  }
1435
1819
  ` };
1436
1820
  },
1437
1821
  customElementsDts() {
1438
1822
  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
- }
1823
+ // React JSX declarations for the <zitadel-*> custom elements ship with the
1824
+ // SDK, so the scaffold stays aligned with the real element surface instead
1825
+ // of carrying a hand-maintained copy that drifts.
1826
+ /// <reference types="@zitadel/sdk-next/jsx" />
1465
1827
  ` };
1466
1828
  }
1467
1829
  }
@@ -1491,6 +1853,15 @@ const RENDERERS = {
1491
1853
  "web-component": litRenderer
1492
1854
  };
1493
1855
  /**
1856
+ * The subset of {@link RENDERER_IDS} that {@link getRenderer} resolves rather
1857
+ * than rejects (`status: "available"`). Anything advertising renderer choices
1858
+ * — flag options, error hints — must derive from this list, not
1859
+ * {@link RENDERER_IDS}: a declared-but-unpublished renderer (ADR 006) keeps
1860
+ * its registry entry to reserve the id, but must never be offered as a usable
1861
+ * value.
1862
+ */
1863
+ const AVAILABLE_RENDERER_IDS = RENDERER_IDS.filter((id) => RENDERERS[id].status === "available");
1864
+ /**
1494
1865
  * Resolves a renderer id (an untrusted string from config) to its spec,
1495
1866
  * throwing a typed {@link ZitadelError} rather than returning `undefined`
1496
1867
  * so callers get an actionable message. Rejects ids that are unknown
@@ -1498,7 +1869,7 @@ const RENDERERS = {
1498
1869
  * guaranteeing the returned spec is safe to scaffold from.
1499
1870
  */
1500
1871
  function getRenderer(id) {
1501
- if (!isRendererId(id)) throw new ZitadelError("E_VALIDATION", `Unknown renderer "${id}"`, { hint: `Available renderers: ${Object.keys(RENDERERS).join(", ")}` });
1872
+ if (!isRendererId(id)) throw new ZitadelError("E_VALIDATION", `Unknown renderer "${id}"`, { hint: `Available renderers: ${AVAILABLE_RENDERER_IDS.join(", ")}` });
1502
1873
  const renderer = RENDERERS[id];
1503
1874
  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
1875
  return renderer;
@@ -1546,6 +1917,16 @@ var NextPatcher = class extends AbstractRulePatcher {
1546
1917
  routeFiles(view) {
1547
1918
  return nextCodeFilePaths(view.framework, getRenderer(view.rendererId));
1548
1919
  }
1920
+ infrastructureFiles(view) {
1921
+ return nextInfrastructureFilePaths(view.framework, getRenderer(view.rendererId));
1922
+ }
1923
+ conditionallyScaffoldedFiles(view) {
1924
+ return [join(view.framework.appDir, "page.tsx")];
1925
+ }
1926
+ retiredAlternateFiles(view) {
1927
+ if (requestBoundaryFile(view.framework).filename !== "proxy.ts") return {};
1928
+ return { [join(view.framework.appDir, "../proxy.ts")]: [join(view.framework.appDir, "../middleware.ts")] };
1929
+ }
1549
1930
  routeDeps(view) {
1550
1931
  return [getRenderer(view.rendererId).dependency.name];
1551
1932
  }
@@ -1575,10 +1956,28 @@ function nextCodeFilePaths(framework, renderer) {
1575
1956
  if (renderer.templates.customElementsDts) paths.push(join(appDir, "../custom-elements.d.ts"));
1576
1957
  return paths;
1577
1958
  }
1959
+ /**
1960
+ * The subset of {@link nextCodeFilePaths} that is load-bearing for the auth
1961
+ * integration rather than a presentation starting point: the request boundary
1962
+ * (proxy/middleware), the provider file, and the custom-elements declarations.
1963
+ * Shares path construction with {@link nextCodeFilePaths} so the two cannot
1964
+ * drift; the `doctor` managed-files check fails when one of these is missing.
1965
+ */
1966
+ function nextInfrastructureFilePaths(framework, renderer) {
1967
+ const appDir = framework.appDir;
1968
+ const paths = [join(appDir, `../${requestBoundaryFile(framework).filename}`)];
1969
+ if (renderer.templates.provider) paths.push(join(appDir, renderer.templates.provider.filename));
1970
+ if (renderer.templates.customElementsDts) paths.push(join(appDir, "../custom-elements.d.ts"));
1971
+ return paths;
1972
+ }
1578
1973
  /** The Next route/request-boundary write ops plus the SDK dependency. */
1579
1974
  function nextCodeOps(ctx, renderer) {
1580
1975
  const appDir = ctx.framework.appDir;
1581
- const profile = renderer.templates.profilePage?.();
1976
+ const pageContext = {
1977
+ useCase: ctx.useCase,
1978
+ posture: ctx.posture
1979
+ };
1980
+ const profile = renderer.templates.profilePage?.(pageContext);
1582
1981
  const provider = renderer.templates.provider;
1583
1982
  const dts = renderer.templates.customElementsDts?.();
1584
1983
  const boundary = requestBoundaryFile(ctx.framework);
@@ -1586,17 +1985,18 @@ function nextCodeOps(ctx, renderer) {
1586
1985
  ctx.scaffoldedFramework ? {
1587
1986
  kind: "edit",
1588
1987
  path: join(appDir, "page.tsx"),
1589
- edit: () => homePageTemplate()
1988
+ edit: () => homePageTemplate(),
1989
+ overwrites: true
1590
1990
  } : void 0,
1591
1991
  {
1592
1992
  kind: "write",
1593
1993
  path: join(appDir, "login/page.tsx"),
1594
- contents: renderer.templates.authPage("login").contents
1994
+ contents: renderer.templates.authPage("login", pageContext).contents
1595
1995
  },
1596
1996
  {
1597
1997
  kind: "write",
1598
1998
  path: join(appDir, "register/page.tsx"),
1599
- contents: renderer.templates.authPage("register").contents
1999
+ contents: renderer.templates.authPage("register", pageContext).contents
1600
2000
  },
1601
2001
  profile ? {
1602
2002
  kind: "write",
@@ -1755,7 +2155,6 @@ function nuxtConfigEdit(opts) {
1755
2155
  }
1756
2156
  //#endregion
1757
2157
  //#region src/lib/orca/patchers/rule/nuxt/templates.ts
1758
- const MAIN_STYLE = "min-height: 100vh; background: #0f0f11; color: #f4f4f6; color-scheme: dark";
1759
2158
  /** `app.vue` — renders the page router. Marker in an HTML comment. */
1760
2159
  function appVueTemplate() {
1761
2160
  return `<!-- ${MANAGED_MARKER} -->
@@ -1784,35 +2183,68 @@ await navigateTo("/login", { replace: true });
1784
2183
  <\/script>
1785
2184
  `;
1786
2185
  }
1787
- /** A login/register page rendering `<zitadel-login>` inside `<ClientOnly>`. */
1788
- function authPage(purpose) {
2186
+ /**
2187
+ * A login/register page rendering `<zitadel-login>` inside `<ClientOnly>`.
2188
+ * Business-use-case projects additionally bind the SDK's `businessLocales`
2189
+ * overlay, restoring work-email copy on top of the widget's neutral built-in
2190
+ * dictionaries. A plain `:locales` binding suffices even on the raw custom
2191
+ * element: Vue sets bindings whose key exists on the element as DOM
2192
+ * properties (unlike React 18, which is why the Next template needs a ref).
2193
+ */
2194
+ function authPage(purpose, ctx) {
2195
+ const business = ctx.useCase === "business";
2196
+ const importNames = business ? "businessLocales, useZitadelProject" : "useZitadelProject";
2197
+ const localesComment = business ? `
2198
+ // Set up for a business audience: businessLocales overlays work-email copy on
2199
+ // the login widget's neutral built-in dictionaries. Remove the :locales
2200
+ // binding to fall back to the neutral wording.` : "";
2201
+ const localesAttr = business ? "\n :locales=\"businessLocales\"" : "";
2202
+ const purposeAttr = purpose === "register" ? "\n purpose=\"register\"" : "";
2203
+ const widget = ctx.posture === "widget";
1789
2204
  return `<script setup lang="ts">
1790
2205
  ${MANAGED_MARKER}
1791
- import { useZitadelProject } from "@zitadel/sdk-nuxt";
2206
+ import { ${importNames} } from "@zitadel/sdk-nuxt";${localesComment}
1792
2207
 
1793
2208
  const project = useZitadelProject();
1794
2209
  <\/script>
1795
2210
 
1796
2211
  <template>
1797
- <main style="${MAIN_STYLE}">
2212
+ ${widget ? "<div style=\"display: flex; justify-content: center; padding: 4rem 1rem\">" : "<main style=\"color-scheme: dark\">"}
1798
2213
  <ClientOnly>
2214
+ ${widget ? `<!-- variant="widget" embeds the card in this app's own layout, and
2215
+ theme="auto" follows its color scheme; variant="page" would paint
2216
+ the widget's full-page chrome instead. -->` : `<!-- variant="page" paints the widget's full-page chrome from design
2217
+ tokens; variant="widget" embeds the card inside a layout you own. -->`}
1799
2218
  <zitadel-login
1800
- :project="project"${purpose === "register" ? "\n purpose=\"register\"" : ""}
2219
+ ${widget ? "variant=\"widget\"\n theme=\"auto\"" : "variant=\"page\""}
2220
+ :project="project"${localesAttr}${purposeAttr}
1801
2221
  post-sign-in-url="/profile"
1802
2222
  />
1803
2223
  </ClientOnly>
1804
- </main>
2224
+ ${widget ? "</div>" : "</main>"}
1805
2225
  </template>
1806
2226
  `;
1807
2227
  }
1808
- function loginPageTemplate() {
1809
- return authPage("login");
2228
+ function loginPageTemplate(ctx) {
2229
+ return authPage("login", ctx);
1810
2230
  }
1811
- function registerPageTemplate() {
1812
- return authPage("register");
2231
+ function registerPageTemplate(ctx) {
2232
+ return authPage("register", ctx);
1813
2233
  }
1814
2234
  /** `pages/profile.vue` — the post-sign-in "signed in as" session card. */
1815
- function profilePageTemplate() {
2235
+ function profilePageTemplate(ctx) {
2236
+ const widget = ctx.posture === "widget";
2237
+ const variantAttrs = widget ? "variant=\"widget\"\n theme=\"auto\"" : "variant=\"page\"";
2238
+ const postureComment = widget ? `<!-- variant="widget" embeds the session card in this app's own layout,
2239
+ and theme="auto" follows its color scheme; variant="page" would
2240
+ paint the card's full-page chrome instead. Your own components (a
2241
+ header, an account menu) read the same session state with the
2242
+ auto-imported useAuth() composable. -->` : `<!-- variant="page" paints the session card's full-page chrome from design
2243
+ tokens; variant="widget" embeds the card inside a layout you own.
2244
+ Your own components (a header, an account menu) read the same session
2245
+ state with the auto-imported useAuth() composable. -->`;
2246
+ const wrapperOpen = widget ? "<div style=\"display: flex; justify-content: center; padding: 4rem 1rem\">" : "<main style=\"color-scheme: dark\">";
2247
+ const wrapperClose = widget ? "</div>" : "</main>";
1816
2248
  return `<script setup lang="ts">
1817
2249
  ${MANAGED_MARKER}
1818
2250
  import { useZitadelProject } from "@zitadel/sdk-nuxt";
@@ -1821,11 +2253,16 @@ const project = useZitadelProject();
1821
2253
  <\/script>
1822
2254
 
1823
2255
  <template>
1824
- <main style="${MAIN_STYLE}">
2256
+ ${wrapperOpen}
1825
2257
  <ClientOnly>
1826
- <zitadel-session :project="project" post-sign-out-url="/login" />
2258
+ ${postureComment}
2259
+ ${widget ? `<zitadel-session
2260
+ ${variantAttrs}
2261
+ :project="project"
2262
+ post-sign-out-url="/login"
2263
+ />` : `<zitadel-session variant="page" :project="project" post-sign-out-url="/login" />`}
1827
2264
  </ClientOnly>
1828
- </main>
2265
+ ${wrapperClose}
1829
2266
  </template>
1830
2267
  `;
1831
2268
  }
@@ -1887,30 +2324,29 @@ var NuxtPatcher = class extends AbstractRulePatcher {
1887
2324
  routeOps(ctx) {
1888
2325
  const src = (rel) => join(ctx.framework.appDir, rel);
1889
2326
  return [
1890
- {
2327
+ ...ctx.scaffoldedFramework ? [{
1891
2328
  kind: "write",
1892
2329
  path: src("app.vue"),
1893
2330
  contents: appVueTemplate()
1894
- },
1895
- {
2331
+ }, {
1896
2332
  kind: "write",
1897
2333
  path: src("pages/index.vue"),
1898
2334
  contents: indexPageTemplate()
1899
- },
2335
+ }] : [],
1900
2336
  {
1901
2337
  kind: "write",
1902
2338
  path: src("pages/login.vue"),
1903
- contents: loginPageTemplate()
2339
+ contents: loginPageTemplate(ctx)
1904
2340
  },
1905
2341
  {
1906
2342
  kind: "write",
1907
2343
  path: src("pages/register.vue"),
1908
- contents: registerPageTemplate()
2344
+ contents: registerPageTemplate(ctx)
1909
2345
  },
1910
2346
  {
1911
2347
  kind: "write",
1912
2348
  path: src("pages/profile.vue"),
1913
- contents: profilePageTemplate()
2349
+ contents: profilePageTemplate(ctx)
1914
2350
  },
1915
2351
  {
1916
2352
  kind: "write",
@@ -1928,7 +2364,8 @@ var NuxtPatcher = class extends AbstractRulePatcher {
1928
2364
  edit: nuxtConfigEdit({
1929
2365
  projectId: ctx.project.id,
1930
2366
  server: ctx.server
1931
- })
2367
+ }),
2368
+ wiring: "infrastructure"
1932
2369
  },
1933
2370
  {
1934
2371
  kind: "merge-env",
@@ -1959,6 +2396,14 @@ var NuxtPatcher = class extends AbstractRulePatcher {
1959
2396
  src("plugins/auth.server.ts")
1960
2397
  ];
1961
2398
  }
2399
+ infrastructureFiles(view) {
2400
+ const src = (rel) => join(view.framework.appDir, rel);
2401
+ return [src("plugins/zitadel-components.client.ts"), src("plugins/auth.server.ts")];
2402
+ }
2403
+ conditionallyScaffoldedFiles(view) {
2404
+ const src = (rel) => join(view.framework.appDir, rel);
2405
+ return [src("app.vue"), src("pages/index.vue")];
2406
+ }
1962
2407
  routeDeps(_view) {
1963
2408
  return [SDK_DEPENDENCY$5];
1964
2409
  }
@@ -1979,13 +2424,13 @@ var NuxtPatcher = class extends AbstractRulePatcher {
1979
2424
  * frameworks (React, Vue, Solid, Svelte, Qwik). It forwards same-origin
1980
2425
  * `/__nextgen/*` calls to the
1981
2426
  * 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.
2427
+ * (read from `ZITADEL_PROJECT_SECRET` in `.env.local`) only to the current
2428
+ * app-plane `POST /sessions/exchange` operation. The secret stays server-side:
2429
+ * Vite only exposes vars with the configured `envPrefix` (default `VITE_`) to
2430
+ * client bundles, so this server-only key never leaks into the browser.
1986
2431
  */
1987
2432
  function proxyEntryCode(server) {
1988
- return `{
2433
+ return `/* ${PROXY_CREDENTIAL_POLICY_MARKER} */ {
1989
2434
  target: ${JSON.stringify(server)},
1990
2435
  changeOrigin: false,
1991
2436
  rewrite: (path) => path.replace(/^\\${PROXY_PATH}/, "").replace(/^(?!\\/)/, "/"),
@@ -1996,11 +2441,45 @@ function proxyEntryCode(server) {
1996
2441
  }
1997
2442
  const bearer = \`Bearer \${secret}\`;
1998
2443
  proxy.on("proxyReq", (proxyReq) => {
1999
- proxyReq.setHeader("authorization", bearer);
2444
+ const pathname = new URL(proxyReq.path, "http://zitadel.local").pathname;
2445
+ if (
2446
+ proxyReq.method === "POST" &&
2447
+ pathname === "/sessions/exchange" &&
2448
+ !proxyReq.getHeader("authorization")
2449
+ ) {
2450
+ proxyReq.setHeader("authorization", bearer);
2451
+ }
2000
2452
  });
2001
2453
  },
2002
2454
  }`;
2003
2455
  }
2456
+ const LEGACY_PROXY_REQUEST_LISTENER = /(^[\t ]*)proxy\.on\("proxyReq", \(proxyReq\) => \{\r?\n\1 {2}proxyReq\.setHeader\("authorization", bearer\);\r?\n\1\}\);/m;
2457
+ function exchangeOnlyProxyRequestListener(indent) {
2458
+ return `${indent}/* ${PROXY_CREDENTIAL_POLICY_MARKER} */
2459
+ ${indent}proxy.on("proxyReq", (proxyReq) => {
2460
+ ${indent} const pathname = new URL(proxyReq.path, "http://zitadel.local").pathname;
2461
+ ${indent} if (
2462
+ ${indent} proxyReq.method === "POST" &&
2463
+ ${indent} pathname === "/sessions/exchange" &&
2464
+ ${indent} !proxyReq.getHeader("authorization")
2465
+ ${indent} ) {
2466
+ ${indent} proxyReq.setHeader("authorization", bearer);
2467
+ ${indent} }
2468
+ ${indent}});`;
2469
+ }
2470
+ /**
2471
+ * Identifies the exact proxy shape emitted before the exchange-only credential
2472
+ * policy. Setup may replace that generated listener, but must preserve the
2473
+ * surrounding `/__nextgen` proxy and any user changes elsewhere in the entry.
2474
+ */
2475
+ function isLegacyManagedProxy(source) {
2476
+ if (source.includes("zitadel:proxy:v2")) return false;
2477
+ return [
2478
+ "loadEnv(\"development\", process.cwd(), \"ZITADEL_\").ZITADEL_PROJECT_SECRET",
2479
+ "throw new Error(\"ZITADEL_PROJECT_SECRET is not set; add it to .env.local (zitadel setup writes it).\")",
2480
+ "const bearer = `Bearer ${secret}`;"
2481
+ ].every((fingerprint) => source.includes(fingerprint)) && LEGACY_PROXY_REQUEST_LISTENER.test(source);
2482
+ }
2004
2483
  /** The imports that the injected proxy entry depends on. */
2005
2484
  const PROXY_IMPORTS = [{
2006
2485
  from: "vite",
@@ -2021,9 +2500,12 @@ const PROXY_IMPORTS = [{
2021
2500
  function viteProxyEdit(devPort, server) {
2022
2501
  return (source) => {
2023
2502
  const label = "the Vite config (vite.config.*)";
2024
- const mod = parseConfigModule(source, label);
2503
+ let workingSource = source;
2504
+ if (workingSource !== void 0 && isLegacyManagedProxy(workingSource)) workingSource = workingSource.replace(LEGACY_PROXY_REQUEST_LISTENER, (_listener, indent) => exchangeOnlyProxyRequestListener(indent));
2505
+ else if (workingSource !== void 0) assertNoUnreviewedProjectSecretProxy(workingSource, label);
2506
+ const mod = parseConfigModule(workingSource, label);
2025
2507
  const config = resolveDefaultExportObject(mod, label);
2026
- let changed = false;
2508
+ let changed = workingSource !== source;
2027
2509
  const serverConfig = ensureEditableObject(config, "server");
2028
2510
  if (serverConfig.port === void 0) {
2029
2511
  serverConfig.port = devPort;
@@ -2058,7 +2540,8 @@ function buildViteProxyOp(devPort, server) {
2058
2540
  return {
2059
2541
  kind: "edit",
2060
2542
  path: [...VITE_CONFIG_PATHS],
2061
- edit: viteProxyEdit(devPort, server)
2543
+ edit: viteProxyEdit(devPort, server),
2544
+ wiring: "infrastructure"
2062
2545
  };
2063
2546
  }
2064
2547
  //#endregion
@@ -2071,12 +2554,25 @@ function buildViteProxyOp(devPort, server) {
2071
2554
  * (`main.tsx` imports `{ App }`). The project id comes from
2072
2555
  * `VITE_ZITADEL_PROJECT_ID`. No secret reaches the browser: the dev proxy in
2073
2556
  * `vite.config.*` attaches the project service-key secret (from
2074
- * `ZITADEL_PROJECT_SECRET`) server-side.
2075
- */
2076
- function appTemplate$4() {
2557
+ * `ZITADEL_PROJECT_SECRET`) server-side only to `POST /sessions/exchange`.
2558
+ *
2559
+ * Projects set up with the business use case additionally pass the SDK's
2560
+ * `businessLocales` overlay to the login widgets, restoring work-email copy on
2561
+ * top of the widget's neutral built-in dictionaries. A plain `locales` prop
2562
+ * suffices here: the wrapper component assigns it as a DOM property internally.
2563
+ */
2564
+ function appTemplate$4(ctx) {
2565
+ const business = ctx.useCase === "business";
2566
+ const importNames = business ? "ZitadelLogin, ZitadelSession, businessLocales, configureZitadel" : "ZitadelLogin, ZitadelSession, configureZitadel";
2567
+ const localesComment = business ? `
2568
+
2569
+ // Set up for a business audience: businessLocales overlays work-email copy on
2570
+ // the login widget's neutral built-in dictionaries. Remove the locales prop to
2571
+ // fall back to the neutral wording.` : "";
2572
+ const localesAttr = business ? " locales={businessLocales}" : "";
2077
2573
  return `${MANAGED_MARKER}
2078
2574
  import { component$, useVisibleTask$ } from "@builder.io/qwik";
2079
- import { ZitadelLogin, ZitadelSession, configureZitadel } from "@zitadel/sdk-qwik";
2575
+ import { ${importNames} } from "@zitadel/sdk-qwik";${localesComment}
2080
2576
 
2081
2577
  const project = configureZitadel({
2082
2578
  projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
@@ -2105,13 +2601,13 @@ export const App = component$(() => {
2105
2601
  if (path.startsWith("/register")) {
2106
2602
  return (
2107
2603
  <div style={{ position: "fixed", inset: "0", overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
2108
- <ZitadelLogin project={project} purpose="register" postSignInUrl="/profile" />
2604
+ <ZitadelLogin project={project}${localesAttr} purpose="register" postSignInUrl="/profile" />
2109
2605
  </div>
2110
2606
  );
2111
2607
  }
2112
2608
  return (
2113
2609
  <div style={{ position: "fixed", inset: "0", overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
2114
- <ZitadelLogin project={project} purpose="login" postSignInUrl="/profile" />
2610
+ <ZitadelLogin project={project}${localesAttr} purpose="login" postSignInUrl="/profile" />
2115
2611
  </div>
2116
2612
  );
2117
2613
  });
@@ -2125,14 +2621,15 @@ const SDK_DEPENDENCY$4 = "@zitadel/sdk-qwik";
2125
2621
  * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
2126
2622
  * managed `src/app.tsx` auth entry, a non-destructive `vite.config.*` merge
2127
2623
  * that adds the `/__nextgen` dev proxy (attaching the project secret from
2128
- * `ZITADEL_PROJECT_SECRET` to every proxied request), the `VITE_`-prefixed
2624
+ * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
2129
2625
  * project id, and the SDK dep.
2130
2626
  *
2131
2627
  * The create-vite Qwik template uses a lowercase `src/app.tsx` exporting a named
2132
2628
  * `App` (mounted by `main.tsx`), so this patcher writes that exact entry. Unlike
2133
2629
  * 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.
2630
+ * so the dev proxy provides the same-origin `/__nextgen` path locally. In
2631
+ * production that path comes from a platform rewrite or minimal worker
2632
+ * (ADR 036); CLI scaffolding for it is tracked in issue #560.
2136
2633
  */
2137
2634
  var QwikPatcher = class extends AbstractRulePatcher {
2138
2635
  canPatch(framework) {
@@ -2146,7 +2643,7 @@ var QwikPatcher = class extends AbstractRulePatcher {
2146
2643
  {
2147
2644
  kind: "write",
2148
2645
  path: "src/app.tsx",
2149
- contents: appTemplate$4()
2646
+ contents: appTemplate$4(ctx)
2150
2647
  },
2151
2648
  this.viteProxyOp(ctx.framework.devPort, ctx.server),
2152
2649
  {
@@ -2191,12 +2688,27 @@ var QwikPatcher = class extends AbstractRulePatcher {
2191
2688
  * The project id comes from `VITE_ZITADEL_PROJECT_ID` (Vite only exposes
2192
2689
  * `VITE_`-prefixed env to the client). No secret reaches the browser: the dev
2193
2690
  * proxy in `vite.config.*` attaches the project service-key secret (from
2194
- * `ZITADEL_PROJECT_SECRET`) server-side.
2195
- */
2196
- function appTemplate$3() {
2691
+ * `ZITADEL_PROJECT_SECRET`) server-side only to `POST /sessions/exchange`.
2692
+ *
2693
+ * Projects set up with the business use case additionally pass the SDK's
2694
+ * `businessLocales` overlay to the login widgets, restoring work-email copy on
2695
+ * top of the widget's neutral built-in dictionaries. A plain `locales` prop
2696
+ * suffices here: the wrapper component assigns it as a DOM property internally,
2697
+ * so the React-18 attribute-decay caveat of the raw custom element does not
2698
+ * apply.
2699
+ */
2700
+ function appTemplate$3(ctx) {
2701
+ const business = ctx.useCase === "business";
2702
+ const importNames = business ? "ZitadelLogin, ZitadelSession, businessLocales, configureZitadel" : "ZitadelLogin, ZitadelSession, configureZitadel";
2703
+ const localesComment = business ? `
2704
+
2705
+ // Set up for a business audience: businessLocales overlays work-email copy on
2706
+ // the login widget's neutral built-in dictionaries. Remove the locales prop to
2707
+ // fall back to the neutral wording.` : "";
2708
+ const localesAttr = business ? " locales={businessLocales}" : "";
2197
2709
  return `${MANAGED_MARKER}
2198
2710
  import { useEffect } from "react";
2199
- import { ZitadelLogin, ZitadelSession, configureZitadel } from "@zitadel/sdk-react";
2711
+ import { ${importNames} } from "@zitadel/sdk-react";${localesComment}
2200
2712
 
2201
2713
  const project = configureZitadel({
2202
2714
  projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
@@ -2225,13 +2737,13 @@ export default function App() {
2225
2737
  if (path.startsWith("/register")) {
2226
2738
  return (
2227
2739
  <div style={{ position: "fixed", inset: 0, overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
2228
- <ZitadelLogin project={project} purpose="register" postSignInUrl="/profile" />
2740
+ <ZitadelLogin project={project}${localesAttr} purpose="register" postSignInUrl="/profile" />
2229
2741
  </div>
2230
2742
  );
2231
2743
  }
2232
2744
  return (
2233
2745
  <div style={{ position: "fixed", inset: 0, overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
2234
- <ZitadelLogin project={project} purpose="login" postSignInUrl="/profile" />
2746
+ <ZitadelLogin project={project}${localesAttr} purpose="login" postSignInUrl="/profile" />
2235
2747
  </div>
2236
2748
  );
2237
2749
  }
@@ -2245,12 +2757,14 @@ const SDK_DEPENDENCY$3 = "@zitadel/sdk-react";
2245
2757
  * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
2246
2758
  * managed `src/App.tsx` auth entry, a non-destructive `vite.config.*` merge
2247
2759
  * that adds the `/__nextgen` dev proxy (attaching the project secret from
2248
- * `ZITADEL_PROJECT_SECRET` to every proxied request), the `VITE_`-prefixed
2760
+ * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
2249
2761
  * project id, and the SDK dep.
2250
2762
  *
2251
2763
  * 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.
2764
+ * server-side — a SPA has no server, so the dev proxy provides the same-origin
2765
+ * `/__nextgen` path locally. In production that path comes from a platform
2766
+ * rewrite or minimal worker (ADR 036); CLI scaffolding for it is tracked in
2767
+ * issue #560.
2254
2768
  */
2255
2769
  var ReactPatcher = class extends AbstractRulePatcher {
2256
2770
  canPatch(framework) {
@@ -2264,7 +2778,7 @@ var ReactPatcher = class extends AbstractRulePatcher {
2264
2778
  {
2265
2779
  kind: "write",
2266
2780
  path: "src/App.tsx",
2267
- contents: appTemplate$3()
2781
+ contents: appTemplate$3(ctx)
2268
2782
  },
2269
2783
  this.viteProxyOp(ctx.framework.devPort, ctx.server),
2270
2784
  {
@@ -2309,12 +2823,25 @@ var ReactPatcher = class extends AbstractRulePatcher {
2309
2823
  * The project id comes from `VITE_ZITADEL_PROJECT_ID` (Vite only exposes
2310
2824
  * `VITE_`-prefixed env to the client). No secret reaches the browser: the dev
2311
2825
  * proxy in `vite.config.*` attaches the project service-key secret (from
2312
- * `ZITADEL_PROJECT_SECRET`) server-side.
2313
- */
2314
- function appTemplate$2() {
2826
+ * `ZITADEL_PROJECT_SECRET`) server-side only to `POST /sessions/exchange`.
2827
+ *
2828
+ * Projects set up with the business use case additionally pass the SDK's
2829
+ * `businessLocales` overlay to the login widgets, restoring work-email copy on
2830
+ * top of the widget's neutral built-in dictionaries. A plain `locales` prop
2831
+ * suffices here: the wrapper component assigns it as a DOM property internally.
2832
+ */
2833
+ function appTemplate$2(ctx) {
2834
+ const business = ctx.useCase === "business";
2835
+ const importNames = business ? "ZitadelLogin, ZitadelSession, businessLocales, configureZitadel" : "ZitadelLogin, ZitadelSession, configureZitadel";
2836
+ const localesComment = business ? `
2837
+
2838
+ // Set up for a business audience: businessLocales overlays work-email copy on
2839
+ // the login widget's neutral built-in dictionaries. Remove the locales prop to
2840
+ // fall back to the neutral wording.` : "";
2841
+ const localesAttr = business ? " locales={businessLocales}" : "";
2315
2842
  return `${MANAGED_MARKER}
2316
2843
  import { onMount } from "solid-js";
2317
- import { ZitadelLogin, ZitadelSession, configureZitadel } from "@zitadel/sdk-solid";
2844
+ import { ${importNames} } from "@zitadel/sdk-solid";${localesComment}
2318
2845
 
2319
2846
  const project = configureZitadel({
2320
2847
  projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
@@ -2343,13 +2870,13 @@ export default function App() {
2343
2870
  if (path.startsWith("/register")) {
2344
2871
  return (
2345
2872
  <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
2346
- <ZitadelLogin project={project} purpose="register" postSignInUrl="/profile" />
2873
+ <ZitadelLogin project={project}${localesAttr} purpose="register" postSignInUrl="/profile" />
2347
2874
  </div>
2348
2875
  );
2349
2876
  }
2350
2877
  return (
2351
2878
  <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
2352
- <ZitadelLogin project={project} purpose="login" postSignInUrl="/profile" />
2879
+ <ZitadelLogin project={project}${localesAttr} purpose="login" postSignInUrl="/profile" />
2353
2880
  </div>
2354
2881
  );
2355
2882
  }
@@ -2363,12 +2890,14 @@ const SDK_DEPENDENCY$2 = "@zitadel/sdk-solid";
2363
2890
  * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
2364
2891
  * managed `src/App.tsx` auth entry, a non-destructive `vite.config.*` merge
2365
2892
  * that adds the `/__nextgen` dev proxy (attaching the project secret from
2366
- * `ZITADEL_PROJECT_SECRET` to every proxied request), the `VITE_`-prefixed
2893
+ * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
2367
2894
  * project id, and the SDK dep.
2368
2895
  *
2369
2896
  * 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.
2897
+ * server-side — a SPA has no server, so the dev proxy provides the same-origin
2898
+ * `/__nextgen` path locally. In production that path comes from a platform
2899
+ * rewrite or minimal worker (ADR 036); CLI scaffolding for it is tracked in
2900
+ * issue #560.
2372
2901
  */
2373
2902
  var SolidPatcher = class extends AbstractRulePatcher {
2374
2903
  canPatch(framework) {
@@ -2382,7 +2911,7 @@ var SolidPatcher = class extends AbstractRulePatcher {
2382
2911
  {
2383
2912
  kind: "write",
2384
2913
  path: "src/App.tsx",
2385
- contents: appTemplate$2()
2914
+ contents: appTemplate$2(ctx)
2386
2915
  },
2387
2916
  this.viteProxyOp(ctx.framework.devPort, ctx.server),
2388
2917
  {
@@ -2428,13 +2957,26 @@ var SolidPatcher = class extends AbstractRulePatcher {
2428
2957
  * eject/doctor stay marker-aware. The project id comes from
2429
2958
  * `VITE_ZITADEL_PROJECT_ID`. No secret reaches the browser: the dev proxy in
2430
2959
  * `vite.config.*` attaches the project service-key secret (from
2431
- * `ZITADEL_PROJECT_SECRET`) server-side.
2432
- */
2433
- function appTemplate$1() {
2960
+ * `ZITADEL_PROJECT_SECRET`) server-side only to `POST /sessions/exchange`.
2961
+ *
2962
+ * Projects set up with the business use case additionally pass the SDK's
2963
+ * `businessLocales` overlay to the login widgets, restoring work-email copy on
2964
+ * top of the widget's neutral built-in dictionaries. A plain `locales` prop
2965
+ * suffices here: the wrapper component assigns it as a DOM property internally.
2966
+ */
2967
+ function appTemplate$1(ctx) {
2968
+ const business = ctx.useCase === "business";
2969
+ const importNames = business ? "ZitadelLogin, ZitadelSession, businessLocales, configureZitadel" : "ZitadelLogin, ZitadelSession, configureZitadel";
2970
+ const localesComment = business ? `
2971
+
2972
+ // Set up for a business audience: businessLocales overlays work-email copy on
2973
+ // the login widget's neutral built-in dictionaries. Remove the locales prop to
2974
+ // fall back to the neutral wording.` : "";
2975
+ const localesAttr = business ? " locales={businessLocales}" : "";
2434
2976
  return `<script lang="ts">
2435
2977
  ${MANAGED_MARKER}
2436
2978
  import { onMount } from "svelte";
2437
- import { ZitadelLogin, ZitadelSession, configureZitadel } from "@zitadel/sdk-svelte";
2979
+ import { ${importNames} } from "@zitadel/sdk-svelte";${localesComment}
2438
2980
 
2439
2981
  const project = configureZitadel({
2440
2982
  projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
@@ -2456,11 +2998,11 @@ onMount(() => {
2456
2998
  </div>
2457
2999
  {:else if path.startsWith("/register")}
2458
3000
  <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
2459
- <ZitadelLogin {project} purpose="register" postSignInUrl="/profile" />
3001
+ <ZitadelLogin {project}${localesAttr} purpose="register" postSignInUrl="/profile" />
2460
3002
  </div>
2461
3003
  {:else if path !== "/"}
2462
3004
  <div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
2463
- <ZitadelLogin {project} purpose="login" postSignInUrl="/profile" />
3005
+ <ZitadelLogin {project}${localesAttr} purpose="login" postSignInUrl="/profile" />
2464
3006
  </div>
2465
3007
  {/if}
2466
3008
  `;
@@ -2473,12 +3015,14 @@ const SDK_DEPENDENCY$1 = "@zitadel/sdk-svelte";
2473
3015
  * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
2474
3016
  * managed `src/App.svelte` auth entry, a non-destructive `vite.config.*` merge
2475
3017
  * that adds the `/__nextgen` dev proxy (attaching the project secret from
2476
- * `ZITADEL_PROJECT_SECRET` to every proxied request), the `VITE_`-prefixed
3018
+ * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
2477
3019
  * project id, and the SDK dep.
2478
3020
  *
2479
3021
  * 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.
3022
+ * server-side — a SPA has no server, so the dev proxy provides the same-origin
3023
+ * `/__nextgen` path locally. In production that path comes from a platform
3024
+ * rewrite or minimal worker (ADR 036); CLI scaffolding for it is tracked in
3025
+ * issue #560.
2482
3026
  */
2483
3027
  var SveltePatcher = class extends AbstractRulePatcher {
2484
3028
  canPatch(framework) {
@@ -2492,7 +3036,7 @@ var SveltePatcher = class extends AbstractRulePatcher {
2492
3036
  {
2493
3037
  kind: "write",
2494
3038
  path: "src/App.svelte",
2495
- contents: appTemplate$1()
3039
+ contents: appTemplate$1(ctx)
2496
3040
  },
2497
3041
  this.viteProxyOp(ctx.framework.devPort, ctx.server),
2498
3042
  {
@@ -2538,13 +3082,26 @@ var SveltePatcher = class extends AbstractRulePatcher {
2538
3082
  * eject/doctor stay marker-aware. The project id comes from
2539
3083
  * `VITE_ZITADEL_PROJECT_ID`. No secret reaches the browser: the dev proxy in
2540
3084
  * `vite.config.*` attaches the project service-key secret (from
2541
- * `ZITADEL_PROJECT_SECRET`) server-side.
2542
- */
2543
- function appTemplate() {
3085
+ * `ZITADEL_PROJECT_SECRET`) server-side only to `POST /sessions/exchange`.
3086
+ *
3087
+ * Projects set up with the business use case additionally pass the SDK's
3088
+ * `businessLocales` overlay to the login widgets, restoring work-email copy on
3089
+ * top of the widget's neutral built-in dictionaries. A plain `:locales` prop
3090
+ * suffices here: the wrapper component assigns it as a DOM property internally.
3091
+ */
3092
+ function appTemplate(ctx) {
3093
+ const business = ctx.useCase === "business";
3094
+ const importNames = business ? "ZitadelLogin, ZitadelSession, businessLocales, configureZitadel" : "ZitadelLogin, ZitadelSession, configureZitadel";
3095
+ const localesComment = business ? `
3096
+
3097
+ // Set up for a business audience: businessLocales overlays work-email copy on
3098
+ // the login widget's neutral built-in dictionaries. Remove the :locales prop
3099
+ // to fall back to the neutral wording.` : "";
3100
+ const localesAttr = business ? " :locales=\"businessLocales\"" : "";
2544
3101
  return `<script setup lang="ts">
2545
3102
  ${MANAGED_MARKER}
2546
3103
  import { onMounted } from "vue";
2547
- import { ZitadelLogin, ZitadelSession, configureZitadel } from "@zitadel/sdk-vue";
3104
+ import { ${importNames} } from "@zitadel/sdk-vue";${localesComment}
2548
3105
 
2549
3106
  const project = configureZitadel({
2550
3107
  projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
@@ -2571,10 +3128,10 @@ onMounted(() => {
2571
3128
  v-else-if="path.startsWith('/register')"
2572
3129
  style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark"
2573
3130
  >
2574
- <ZitadelLogin :project="project" purpose="register" postSignInUrl="/profile" />
3131
+ <ZitadelLogin :project="project"${localesAttr} purpose="register" postSignInUrl="/profile" />
2575
3132
  </div>
2576
3133
  <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" />
3134
+ <ZitadelLogin :project="project"${localesAttr} purpose="login" postSignInUrl="/profile" />
2578
3135
  </div>
2579
3136
  </template>
2580
3137
  `;
@@ -2587,7 +3144,7 @@ const SDK_DEPENDENCY = "@zitadel/sdk-vue";
2587
3144
  * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
2588
3145
  * managed `src/App.vue` auth entry, a non-destructive `vite.config.*` merge
2589
3146
  * that adds the `/__nextgen` dev proxy (attaching the project secret from
2590
- * `ZITADEL_PROJECT_SECRET` to every proxied request), the `VITE_`-prefixed
3147
+ * `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
2591
3148
  * project id, and the SDK dep.
2592
3149
  */
2593
3150
  var VuePatcher = class extends AbstractRulePatcher {
@@ -2602,7 +3159,7 @@ var VuePatcher = class extends AbstractRulePatcher {
2602
3159
  {
2603
3160
  kind: "write",
2604
3161
  path: "src/App.vue",
2605
- contents: appTemplate()
3162
+ contents: appTemplate(ctx)
2606
3163
  },
2607
3164
  this.viteProxyOp(ctx.framework.devPort, ctx.server),
2608
3165
  {
@@ -3214,6 +3771,6 @@ function isErrno(error, code) {
3214
3771
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
3215
3772
  }
3216
3773
  //#endregion
3217
- export { removeGuidanceSection as a, AGENTS_HEADER as i, inspectScaffoldTarget as n, issuerFromPort as o, RENDERER_IDS as r, createOrca as t };
3774
+ 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
3775
 
3219
- //# sourceMappingURL=orca-Dip6DPJ2.mjs.map
3776
+ //# sourceMappingURL=orca-BWB4zOdw.mjs.map