@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.
- package/README.md +210 -124
- package/SKILLS.md +159 -17
- package/dist/branding-cygSBPkV.mjs +79 -0
- package/dist/branding-cygSBPkV.mjs.map +1 -0
- package/dist/claim-state-DlfD6Res.mjs +66 -0
- package/dist/claim-state-DlfD6Res.mjs.map +1 -0
- package/dist/commands/apply.mjs +6 -5
- package/dist/commands/apply.mjs.map +1 -1
- package/dist/commands/branding/eject.mjs +104 -0
- package/dist/commands/branding/eject.mjs.map +1 -0
- package/dist/commands/claim.mjs +402 -0
- package/dist/commands/claim.mjs.map +1 -0
- package/dist/commands/doctor.mjs +500 -37
- package/dist/commands/doctor.mjs.map +1 -1
- package/dist/commands/eject.mjs +4 -4
- package/dist/commands/eject.mjs.map +1 -1
- package/dist/commands/logs.mjs +2 -2
- package/dist/commands/plan.mjs +6 -5
- package/dist/commands/plan.mjs.map +1 -1
- package/dist/commands/reset.mjs +2 -2
- package/dist/commands/schemas/list.mjs +11 -11
- package/dist/commands/schemas/list.mjs.map +1 -1
- package/dist/commands/setup.mjs +359 -132
- package/dist/commands/setup.mjs.map +1 -1
- package/dist/commands/start.mjs +3 -3
- package/dist/commands/status.mjs +21 -13
- package/dist/commands/status.mjs.map +1 -1
- package/dist/commands/stop.mjs +3 -3
- package/dist/commands/stop.mjs.map +1 -1
- package/dist/designs-Ckz18Dpo.mjs +38 -0
- package/dist/designs-Ckz18Dpo.mjs.map +1 -0
- package/dist/{docker-pGcTrDIY.mjs → docker-DcRGTOJa.mjs} +4 -3
- package/dist/docker-DcRGTOJa.mjs.map +1 -0
- package/dist/{environment-BQF7LeCz.mjs → environment-rjRVkJjW.mjs} +1 -1
- package/dist/{environment-BQF7LeCz.mjs.map → environment-rjRVkJjW.mjs.map} +1 -1
- package/dist/{journey-guidance-CYVpQszz.mjs → journey-guidance-BGrOX_gT.mjs} +14 -3
- package/dist/{journey-guidance-CYVpQszz.mjs.map → journey-guidance-BGrOX_gT.mjs.map} +1 -1
- package/dist/{oclif-BRdjVNne.mjs → oclif-CanO3zdt.mjs} +247 -43
- package/dist/oclif-CanO3zdt.mjs.map +1 -0
- package/dist/{orca-Dip6DPJ2.mjs → orca-Cu0iW1mv.mjs} +894 -196
- package/dist/orca-Cu0iW1mv.mjs.map +1 -0
- package/dist/package-manager-DFdLcDx1.mjs +194 -0
- package/dist/package-manager-DFdLcDx1.mjs.map +1 -0
- package/dist/{ports-V8JfiNgT.mjs → ports-BM20XIZb.mjs} +2 -2
- package/dist/{ports-V8JfiNgT.mjs.map → ports-BM20XIZb.mjs.map} +1 -1
- package/dist/{processes-BVqYsxT8.mjs → processes-DKHPkU8O.mjs} +1 -1
- package/dist/{processes-BVqYsxT8.mjs.map → processes-DKHPkU8O.mjs.map} +1 -1
- package/dist/{project-BGgv7Bba.mjs → project-CHhot85s.mjs} +57 -4
- package/dist/project-CHhot85s.mjs.map +1 -0
- package/dist/{sync-Cd3z7Q1R.mjs → sync-Bd6vASgg.mjs} +549 -75
- package/dist/sync-Bd6vASgg.mjs.map +1 -0
- package/dist/user-schema-DTuOsdKE.mjs +91 -0
- package/dist/user-schema-DTuOsdKE.mjs.map +1 -0
- package/oclif.manifest.json +227 -5
- package/package.json +9 -6
- package/dist/docker-pGcTrDIY.mjs.map +0 -1
- package/dist/oclif-BRdjVNne.mjs.map +0 -1
- package/dist/orca-Dip6DPJ2.mjs.map +0 -1
- package/dist/project-BGgv7Bba.mjs.map +0 -1
- package/dist/sync-Cd3z7Q1R.mjs.map +0 -1
- package/dist/user-schema-DDz5-lX5.mjs +0 -13
- package/dist/user-schema-DDz5-lX5.mjs.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
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:
|
|
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
|
-
|
|
429
|
+
files: [],
|
|
336
430
|
filesSkipped: [],
|
|
337
431
|
depsAdded: []
|
|
338
432
|
};
|
|
339
433
|
for (const op of plan.ops) await applyOp(op, opts, result);
|
|
340
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
553
|
+
record(result, path, "file", action);
|
|
444
554
|
}
|
|
445
555
|
async function appendText(path, contents, ifMissing, dryRun, result) {
|
|
446
|
-
const
|
|
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
|
|
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
|
|
574
|
+
record(result, path, "file", action);
|
|
463
575
|
}
|
|
464
576
|
async function mergeEnv(path, entries, dryRun, result) {
|
|
465
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
610
|
+
record(result, path, "file", action);
|
|
496
611
|
}
|
|
497
612
|
async function appendGitignore(path, entries, dryRun, result) {
|
|
498
|
-
const
|
|
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
|
|
624
|
+
record(result, path, "file", action);
|
|
508
625
|
return;
|
|
509
626
|
}
|
|
510
627
|
await writeFile(path, next);
|
|
511
|
-
result
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
894
|
+
ops,
|
|
686
895
|
summary: plan.summary
|
|
687
|
-
},
|
|
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
|
|
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.
|
|
759
|
-
preview_secret: ctx.project.
|
|
760
|
-
preview_origins: ctx.project.
|
|
761
|
-
created_at: ctx.project.
|
|
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.
|
|
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.
|
|
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
|
|
1039
|
-
* `.env.local`), and no secret reaches
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
1106
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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: "
|
|
1607
|
+
kind: "edit",
|
|
1220
1608
|
path: "proxy.conf.cjs",
|
|
1221
|
-
|
|
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 {
|
|
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
|
-
|
|
1890
|
+
${widget ? `<div style={{ display: "flex", justifyContent: "center", padding: "4rem 1rem" }}>` : `<main style={{ colorScheme: "dark" }}>`}
|
|
1393
1891
|
<${elementName} />
|
|
1394
|
-
|
|
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
|
-
|
|
1944
|
+
${widget ? `<div style={{ display: "flex", justifyContent: "center", padding: "4rem 1rem" }}>` : `<main style={{ colorScheme: "dark" }}>`}
|
|
1431
1945
|
<ZitadelSession />
|
|
1432
|
-
|
|
1946
|
+
${widget ? `</div>` : `</main>`}
|
|
1433
1947
|
);
|
|
1434
1948
|
}
|
|
1435
1949
|
` };
|
|
1436
1950
|
},
|
|
1437
1951
|
customElementsDts() {
|
|
1438
1952
|
return { contents: `${MANAGED_MARKER}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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: ${
|
|
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
|
|
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
|
-
/**
|
|
1788
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2396
|
+
${wrapperOpen}
|
|
1825
2397
|
<ClientOnly>
|
|
1826
|
-
|
|
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
|
-
|
|
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`)
|
|
1983
|
-
*
|
|
1984
|
-
* configured `envPrefix` (default `VITE_`) to
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
2135
|
-
*
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
2253
|
-
*
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
2371
|
-
*
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
2481
|
-
*
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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 {
|
|
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-
|
|
3917
|
+
//# sourceMappingURL=orca-Cu0iW1mv.mjs.map
|