@zitadel/cli 0.1.0-alpha.8 → 1.0.0-alpha.20
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 +250 -62
- package/SKILLS.md +227 -38
- 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 +27 -8
- 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 +687 -55
- package/dist/commands/doctor.mjs.map +1 -1
- package/dist/commands/eject.mjs +18 -6
- package/dist/commands/eject.mjs.map +1 -1
- package/dist/commands/logs.mjs +2 -2
- package/dist/commands/plan.mjs +19 -6
- package/dist/commands/plan.mjs.map +1 -1
- package/dist/commands/reset.mjs +12 -4
- package/dist/commands/reset.mjs.map +1 -1
- package/dist/commands/schemas/list.mjs +146 -0
- package/dist/commands/schemas/list.mjs.map +1 -0
- package/dist/commands/setup.mjs +642 -287
- package/dist/commands/setup.mjs.map +1 -1
- package/dist/commands/start.mjs +73 -6
- package/dist/commands/start.mjs.map +1 -1
- package/dist/commands/status.mjs +52 -13
- package/dist/commands/status.mjs.map +1 -1
- package/dist/commands/stop.mjs +59 -6
- 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-BA78SdC2.mjs → docker-DcRGTOJa.mjs} +61 -11
- package/dist/docker-DcRGTOJa.mjs.map +1 -0
- package/dist/environment-rjRVkJjW.mjs +17 -0
- package/dist/environment-rjRVkJjW.mjs.map +1 -0
- package/dist/journey-guidance-BGrOX_gT.mjs +40 -0
- package/dist/journey-guidance-BGrOX_gT.mjs.map +1 -0
- package/dist/oclif-CanO3zdt.mjs +1769 -0
- package/dist/oclif-CanO3zdt.mjs.map +1 -0
- package/dist/{orca-CfKDQRop.mjs → orca-BzQIQqzt.mjs} +1663 -303
- package/dist/orca-BzQIQqzt.mjs.map +1 -0
- package/dist/package-manager-DFdLcDx1.mjs +194 -0
- package/dist/package-manager-DFdLcDx1.mjs.map +1 -0
- package/dist/ports-BM20XIZb.mjs +116 -0
- package/dist/ports-BM20XIZb.mjs.map +1 -0
- package/dist/processes-DKHPkU8O.mjs +120 -0
- package/dist/processes-DKHPkU8O.mjs.map +1 -0
- package/dist/{project-CKAHtHML.mjs → project-CHhot85s.mjs} +57 -4
- package/dist/project-CHhot85s.mjs.map +1 -0
- package/dist/sync-BBlACKzT.mjs +1633 -0
- package/dist/sync-BBlACKzT.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 +412 -5
- package/package.json +10 -5
- package/dist/docker-BA78SdC2.mjs.map +0 -1
- package/dist/docker-guidance-BvfpmsDj.mjs +0 -21
- package/dist/docker-guidance-BvfpmsDj.mjs.map +0 -1
- package/dist/oclif-VkCTGIEk.mjs +0 -818
- package/dist/oclif-VkCTGIEk.mjs.map +0 -1
- package/dist/orca-CfKDQRop.mjs.map +0 -1
- package/dist/project-CKAHtHML.mjs.map +0 -1
- package/dist/sync-B5lqgQO3.mjs +0 -733
- package/dist/sync-B5lqgQO3.mjs.map +0 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
2
3
|
import { basename, dirname, join } from "node:path";
|
|
3
|
-
import
|
|
4
|
+
import semver from "semver";
|
|
5
|
+
import { META_SCHEMA_DIR, metaSchemaFiles } from "@zitadel/config/meta-schemas";
|
|
4
6
|
import { builders, generateCode, parseModule } from "magicast";
|
|
5
7
|
import { spawnSync } from "node:child_process";
|
|
6
8
|
//#region src/lib/orca/detectors/package-json.ts
|
|
@@ -20,12 +22,33 @@ async function readPackageJson(cwd) {
|
|
|
20
22
|
function hasDependency(pkg, name) {
|
|
21
23
|
return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
|
|
22
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
|
+
*/
|
|
23
31
|
function dependencyVersionMajor(pkg, name) {
|
|
24
32
|
const spec = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
|
|
25
33
|
if (!spec) return;
|
|
26
34
|
const match = spec.match(/\d+/);
|
|
27
35
|
return match ? Number(match[0]) : void 0;
|
|
28
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
|
+
}
|
|
29
52
|
//#endregion
|
|
30
53
|
//#region src/lib/orca/detectors/port.ts
|
|
31
54
|
/**
|
|
@@ -71,12 +94,62 @@ function extractPort(script) {
|
|
|
71
94
|
if (rawEnvPort) return Number.parseInt(rawEnvPort, 10);
|
|
72
95
|
}
|
|
73
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
|
+
/**
|
|
74
122
|
* Builds the local OIDC issuer URL for a given dev port. Centralized so the
|
|
75
123
|
* `localhost` origin convention is defined in exactly one place.
|
|
76
124
|
*/
|
|
77
125
|
function issuerFromPort(port) {
|
|
78
126
|
return `http://localhost:${port}`;
|
|
79
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
|
+
}
|
|
80
153
|
//#endregion
|
|
81
154
|
//#region src/lib/orca/detectors/angular.ts
|
|
82
155
|
/** The lowest Angular major the generated templates compile on. */
|
|
@@ -135,15 +208,18 @@ var NextDetector = class {
|
|
|
135
208
|
async detect(cwd) {
|
|
136
209
|
const pkg = await readPackageJson(cwd).catch(() => void 0);
|
|
137
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." });
|
|
138
213
|
const appDir = await dirExists$1(join(cwd, "app")) ? "app" : await dirExists$1(join(cwd, "src/app")) ? "src/app" : void 0;
|
|
139
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." });
|
|
140
215
|
const devPort = await detectDevPort(cwd, pkg);
|
|
216
|
+
const versionMajor = dependencyVersionMajor(pkg, "next");
|
|
141
217
|
return {
|
|
142
218
|
id: "next",
|
|
143
219
|
appDir,
|
|
144
220
|
devPort,
|
|
145
221
|
url: issuerFromPort(devPort),
|
|
146
|
-
versionMajor:
|
|
222
|
+
...versionMajor === void 0 ? {} : { versionMajor }
|
|
147
223
|
};
|
|
148
224
|
}
|
|
149
225
|
};
|
|
@@ -186,6 +262,28 @@ async function dirExists(path) {
|
|
|
186
262
|
return stat(path).then((s) => s.isDirectory(), () => false);
|
|
187
263
|
}
|
|
188
264
|
//#endregion
|
|
265
|
+
//#region src/lib/orca/detectors/qwik.ts
|
|
266
|
+
/**
|
|
267
|
+
* Detects a Vite + Qwik single-page app: depends on `@builder.io/qwik` and
|
|
268
|
+
* `vite` but NOT `@builder.io/qwik-city` (Qwik City is its own meta-framework
|
|
269
|
+
* and ships Qwik). The source dir is `src`, the dev port comes from the
|
|
270
|
+
* project, and the issuer is derived from it.
|
|
271
|
+
*/
|
|
272
|
+
var QwikDetector = class {
|
|
273
|
+
framework = "qwik";
|
|
274
|
+
async detect(cwd) {
|
|
275
|
+
const pkg = await readPackageJson(cwd).catch(() => void 0);
|
|
276
|
+
if (!pkg || hasDependency(pkg, "@builder.io/qwik-city") || !hasDependency(pkg, "@builder.io/qwik") || !hasDependency(pkg, "vite")) return null;
|
|
277
|
+
const devPort = await detectDevPort(cwd, pkg);
|
|
278
|
+
return {
|
|
279
|
+
id: "qwik",
|
|
280
|
+
appDir: "src",
|
|
281
|
+
devPort,
|
|
282
|
+
url: issuerFromPort(devPort)
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
//#endregion
|
|
189
287
|
//#region src/lib/orca/detectors/react.ts
|
|
190
288
|
/**
|
|
191
289
|
* Detects a Vite + React single-page app and extracts its facts: the source
|
|
@@ -201,11 +299,59 @@ var ReactDetector = class {
|
|
|
201
299
|
async detect(cwd) {
|
|
202
300
|
const pkg = await readPackageJson(cwd).catch(() => void 0);
|
|
203
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." });
|
|
204
304
|
const devPort = await detectDevPort(cwd, pkg);
|
|
305
|
+
const versionMajor = dependencyVersionMajor(pkg, "react");
|
|
205
306
|
return {
|
|
206
307
|
id: "react",
|
|
207
308
|
appDir: "src",
|
|
208
309
|
devPort,
|
|
310
|
+
url: issuerFromPort(devPort),
|
|
311
|
+
...versionMajor === void 0 ? {} : { versionMajor }
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region src/lib/orca/detectors/solid.ts
|
|
317
|
+
/**
|
|
318
|
+
* Detects a Vite + Solid single-page app: depends on `solid-js` and `vite` but
|
|
319
|
+
* NOT `@solidjs/start` (SolidStart is its own meta-framework and ships Solid).
|
|
320
|
+
* The source dir is `src`, the dev port comes from the project, and the issuer
|
|
321
|
+
* is derived from it.
|
|
322
|
+
*/
|
|
323
|
+
var SolidDetector = class {
|
|
324
|
+
framework = "solid";
|
|
325
|
+
async detect(cwd) {
|
|
326
|
+
const pkg = await readPackageJson(cwd).catch(() => void 0);
|
|
327
|
+
if (!pkg || hasDependency(pkg, "@solidjs/start") || !hasDependency(pkg, "solid-js") || !hasDependency(pkg, "vite")) return null;
|
|
328
|
+
const devPort = await detectDevPort(cwd, pkg);
|
|
329
|
+
return {
|
|
330
|
+
id: "solid",
|
|
331
|
+
appDir: "src",
|
|
332
|
+
devPort,
|
|
333
|
+
url: issuerFromPort(devPort)
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/lib/orca/detectors/svelte.ts
|
|
339
|
+
/**
|
|
340
|
+
* Detects a Vite + Svelte single-page app: depends on `svelte` and `vite` but
|
|
341
|
+
* NOT `@sveltejs/kit` (SvelteKit is its own meta-framework and ships Svelte).
|
|
342
|
+
* The source dir is `src`, the dev port comes from the project, and the issuer
|
|
343
|
+
* is derived from it.
|
|
344
|
+
*/
|
|
345
|
+
var SvelteDetector = class {
|
|
346
|
+
framework = "svelte";
|
|
347
|
+
async detect(cwd) {
|
|
348
|
+
const pkg = await readPackageJson(cwd).catch(() => void 0);
|
|
349
|
+
if (!pkg || hasDependency(pkg, "@sveltejs/kit") || !hasDependency(pkg, "svelte") || !hasDependency(pkg, "vite")) return null;
|
|
350
|
+
const devPort = await detectDevPort(cwd, pkg);
|
|
351
|
+
return {
|
|
352
|
+
id: "svelte",
|
|
353
|
+
appDir: "src",
|
|
354
|
+
devPort,
|
|
209
355
|
url: issuerFromPort(devPort)
|
|
210
356
|
};
|
|
211
357
|
}
|
|
@@ -245,11 +391,29 @@ const detectors = [
|
|
|
245
391
|
new NuxtDetector(),
|
|
246
392
|
new ReactDetector(),
|
|
247
393
|
new VueDetector(),
|
|
394
|
+
new SolidDetector(),
|
|
395
|
+
new SvelteDetector(),
|
|
396
|
+
new QwikDetector(),
|
|
248
397
|
new AngularDetector()
|
|
249
398
|
];
|
|
250
399
|
//#endregion
|
|
251
400
|
//#region src/lib/orca/patchers/rule/file-writer/index.ts
|
|
252
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
|
+
/**
|
|
253
417
|
* Applies a {@link ScaffoldPlan} to disk, executing its operations in order.
|
|
254
418
|
*
|
|
255
419
|
* Operations are idempotent: writes whose target already matches the desired
|
|
@@ -262,12 +426,19 @@ const detectors = [
|
|
|
262
426
|
async function scaffold(plan, opts) {
|
|
263
427
|
const result = {
|
|
264
428
|
dryRun: opts.dryRun,
|
|
265
|
-
|
|
429
|
+
files: [],
|
|
266
430
|
filesSkipped: [],
|
|
267
431
|
depsAdded: []
|
|
268
432
|
};
|
|
269
433
|
for (const op of plan.ops) await applyOp(op, opts, result);
|
|
270
|
-
|
|
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
|
+
};
|
|
271
442
|
}
|
|
272
443
|
async function applyOp(op, opts, result) {
|
|
273
444
|
switch (op.kind) {
|
|
@@ -328,8 +499,9 @@ async function editFile(cwd, pathOrPaths, edit, dryRun, result) {
|
|
|
328
499
|
result.filesSkipped.push(path);
|
|
329
500
|
return;
|
|
330
501
|
}
|
|
502
|
+
const action = source === void 0 ? "create" : "update";
|
|
331
503
|
if (dryRun) {
|
|
332
|
-
result
|
|
504
|
+
record(result, path, "file", action);
|
|
333
505
|
return;
|
|
334
506
|
}
|
|
335
507
|
await mkdir(dirname(path), { recursive: true });
|
|
@@ -337,11 +509,16 @@ async function editFile(cwd, pathOrPaths, edit, dryRun, result) {
|
|
|
337
509
|
await writeFile(tmp, next);
|
|
338
510
|
if (mode !== void 0) await chmod(tmp, mode).catch(() => void 0);
|
|
339
511
|
await rename(tmp, path);
|
|
340
|
-
result
|
|
512
|
+
record(result, path, "file", action);
|
|
341
513
|
}
|
|
342
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;
|
|
343
518
|
if (dryRun) {
|
|
344
|
-
result
|
|
519
|
+
if (!existed) record(result, path, "dir", "create");
|
|
520
|
+
else if (healsMode) record(result, path, "dir", "update");
|
|
521
|
+
else result.filesSkipped.push(path);
|
|
345
522
|
return;
|
|
346
523
|
}
|
|
347
524
|
await mkdir(path, {
|
|
@@ -349,7 +526,9 @@ async function ensureDir(path, mode, dryRun, result) {
|
|
|
349
526
|
mode
|
|
350
527
|
});
|
|
351
528
|
if (mode) await chmod(path, mode).catch(() => void 0);
|
|
352
|
-
result
|
|
529
|
+
if (!existed) record(result, path, "dir", "create");
|
|
530
|
+
else if (healsMode) record(result, path, "dir", "update");
|
|
531
|
+
else result.filesSkipped.push(path);
|
|
353
532
|
}
|
|
354
533
|
async function writeText(path, contents, opts, result) {
|
|
355
534
|
const existing = await readIfExists(path);
|
|
@@ -361,8 +540,9 @@ async function writeText(path, contents, opts, result) {
|
|
|
361
540
|
hint: "Re-run with --force if you want the CLI to replace this file.",
|
|
362
541
|
details: { path }
|
|
363
542
|
});
|
|
543
|
+
const action = existing === void 0 ? "create" : "update";
|
|
364
544
|
if (opts.dryRun) {
|
|
365
|
-
result
|
|
545
|
+
record(result, path, "file", action);
|
|
366
546
|
return;
|
|
367
547
|
}
|
|
368
548
|
await mkdir(dirname(path), { recursive: true });
|
|
@@ -370,10 +550,11 @@ async function writeText(path, contents, opts, result) {
|
|
|
370
550
|
await writeFile(tmp, contents, { mode: opts.mode });
|
|
371
551
|
if (opts.mode) await chmod(tmp, opts.mode).catch(() => void 0);
|
|
372
552
|
await rename(tmp, path);
|
|
373
|
-
result
|
|
553
|
+
record(result, path, "file", action);
|
|
374
554
|
}
|
|
375
555
|
async function appendText(path, contents, ifMissing, dryRun, result) {
|
|
376
|
-
const
|
|
556
|
+
const raw = await readIfExists(path);
|
|
557
|
+
const existing = raw ?? "";
|
|
377
558
|
if (ifMissing && existing.includes(ifMissing)) {
|
|
378
559
|
result.filesSkipped.push(path);
|
|
379
560
|
return;
|
|
@@ -383,16 +564,18 @@ async function appendText(path, contents, ifMissing, dryRun, result) {
|
|
|
383
564
|
result.filesSkipped.push(path);
|
|
384
565
|
return;
|
|
385
566
|
}
|
|
567
|
+
const action = raw === void 0 ? "create" : "update";
|
|
386
568
|
if (dryRun) {
|
|
387
|
-
result
|
|
569
|
+
record(result, path, "file", action);
|
|
388
570
|
return;
|
|
389
571
|
}
|
|
390
572
|
await mkdir(dirname(path), { recursive: true });
|
|
391
573
|
await writeFile(path, next);
|
|
392
|
-
result
|
|
574
|
+
record(result, path, "file", action);
|
|
393
575
|
}
|
|
394
576
|
async function mergeEnv(path, entries, dryRun, result) {
|
|
395
|
-
const
|
|
577
|
+
const raw = await readIfExists(path);
|
|
578
|
+
const existing = raw ?? "";
|
|
396
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)));
|
|
397
580
|
const additions = Object.entries(entries).filter(([key]) => !present.has(key));
|
|
398
581
|
if (additions.length === 0) {
|
|
@@ -401,13 +584,14 @@ async function mergeEnv(path, entries, dryRun, result) {
|
|
|
401
584
|
}
|
|
402
585
|
const block = additions.map(([key, value]) => `${key}=${value}`).join("\n");
|
|
403
586
|
const next = `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${block}\n`;
|
|
587
|
+
const action = raw === void 0 ? "create" : "update";
|
|
404
588
|
if (dryRun) {
|
|
405
|
-
result
|
|
589
|
+
record(result, path, "file", action);
|
|
406
590
|
return;
|
|
407
591
|
}
|
|
408
592
|
await mkdir(dirname(path), { recursive: true });
|
|
409
593
|
await writeFile(path, next);
|
|
410
|
-
result
|
|
594
|
+
record(result, path, "file", action);
|
|
411
595
|
}
|
|
412
596
|
async function mergeJson(path, patch, dryRun, result) {
|
|
413
597
|
const existing = await readIfExists(path);
|
|
@@ -416,16 +600,18 @@ async function mergeJson(path, patch, dryRun, result) {
|
|
|
416
600
|
result.filesSkipped.push(path);
|
|
417
601
|
return;
|
|
418
602
|
}
|
|
603
|
+
const action = existing === void 0 ? "create" : "update";
|
|
419
604
|
if (dryRun) {
|
|
420
|
-
result
|
|
605
|
+
record(result, path, "file", action);
|
|
421
606
|
return;
|
|
422
607
|
}
|
|
423
608
|
await mkdir(dirname(path), { recursive: true });
|
|
424
609
|
await writeFile(path, contents);
|
|
425
|
-
result
|
|
610
|
+
record(result, path, "file", action);
|
|
426
611
|
}
|
|
427
612
|
async function appendGitignore(path, entries, dryRun, result) {
|
|
428
|
-
const
|
|
613
|
+
const raw = await readIfExists(path);
|
|
614
|
+
const existing = raw ?? "";
|
|
429
615
|
const lines = new Set(existing.split(/\r?\n/g).map((line) => line.trim()));
|
|
430
616
|
const missing = entries.filter((entry) => !lines.has(entry));
|
|
431
617
|
if (missing.length === 0) {
|
|
@@ -433,12 +619,13 @@ async function appendGitignore(path, entries, dryRun, result) {
|
|
|
433
619
|
return;
|
|
434
620
|
}
|
|
435
621
|
const next = `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${missing.join("\n")}\n`;
|
|
622
|
+
const action = raw === void 0 ? "create" : "update";
|
|
436
623
|
if (dryRun) {
|
|
437
|
-
result
|
|
624
|
+
record(result, path, "file", action);
|
|
438
625
|
return;
|
|
439
626
|
}
|
|
440
627
|
await writeFile(path, next);
|
|
441
|
-
result
|
|
628
|
+
record(result, path, "file", action);
|
|
442
629
|
}
|
|
443
630
|
async function addDependency(path, op, dryRun, result) {
|
|
444
631
|
const existing = await readIfExists(path);
|
|
@@ -450,20 +637,23 @@ async function addDependency(path, op, dryRun, result) {
|
|
|
450
637
|
result.filesSkipped.push(path);
|
|
451
638
|
return;
|
|
452
639
|
}
|
|
453
|
-
|
|
640
|
+
const contents = setTopLevelJsonKey(existing, path, key, sortByKey({
|
|
454
641
|
...deps,
|
|
455
642
|
[op.name]: op.version
|
|
456
|
-
};
|
|
457
|
-
const contents = `${stableStringify(current)}\n`;
|
|
643
|
+
}));
|
|
458
644
|
if (dryRun) {
|
|
459
|
-
result
|
|
645
|
+
record(result, path, "file", "update");
|
|
460
646
|
result.depsAdded.push(op.name);
|
|
461
647
|
return;
|
|
462
648
|
}
|
|
463
649
|
await writeFile(path, contents);
|
|
464
|
-
result
|
|
650
|
+
record(result, path, "file", "update");
|
|
465
651
|
result.depsAdded.push(op.name);
|
|
466
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
|
+
}
|
|
467
657
|
async function readIfExists(path) {
|
|
468
658
|
try {
|
|
469
659
|
return await readFile(path, "utf8");
|
|
@@ -482,6 +672,104 @@ function deepMerge(target, patch) {
|
|
|
482
672
|
return out;
|
|
483
673
|
}
|
|
484
674
|
//#endregion
|
|
675
|
+
//#region src/lib/orca/patchers/rule/guidance.ts
|
|
676
|
+
/**
|
|
677
|
+
* Scaffolded guidance for humans (`README.md`) and agents (`AGENTS.md`):
|
|
678
|
+
* the golden journey from a fresh scaffold to a customized, published
|
|
679
|
+
* login — so the project explains its own next step without the docs
|
|
680
|
+
* site. Both files are edited via a marker-fenced managed section, so
|
|
681
|
+
* setup never clobbers what a developer (or another tool) wrote and a
|
|
682
|
+
* rerun replaces only its own section.
|
|
683
|
+
*/
|
|
684
|
+
const MARKER_BEGIN = "<!-- zitadel:guidance:begin -->";
|
|
685
|
+
const MARKER_END = "<!-- zitadel:guidance:end -->";
|
|
686
|
+
/**
|
|
687
|
+
* Insert or replace the managed guidance section in `source`. A missing
|
|
688
|
+
* file becomes `header` + section; an existing section (marker pair) is
|
|
689
|
+
* replaced in place; anything else gets the section appended. Pure and
|
|
690
|
+
* idempotent — the file-writer skips the write when output equals input.
|
|
691
|
+
*/
|
|
692
|
+
function upsertGuidanceSection(source, section, header) {
|
|
693
|
+
const block = `${MARKER_BEGIN}\n${section}\n${MARKER_END}\n`;
|
|
694
|
+
if (source === void 0 || source.trim() === "") return `${header}${block}`;
|
|
695
|
+
const begin = source.indexOf(MARKER_BEGIN);
|
|
696
|
+
const end = source.indexOf(MARKER_END);
|
|
697
|
+
if (begin !== -1 && end > begin) return `${source.slice(0, begin)}${block}${source.slice(end + 29).replace(/^\n/, "")}`;
|
|
698
|
+
return `${source.replace(/\n*$/, "\n\n")}${block}`;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Remove the managed guidance section (markers inclusive) from `source` —
|
|
702
|
+
* the inverse of {@link upsertGuidanceSection}, for `eject`. Content outside
|
|
703
|
+
* the markers is preserved byte-for-byte; a missing or malformed marker pair
|
|
704
|
+
* returns `source` unchanged.
|
|
705
|
+
*/
|
|
706
|
+
function removeGuidanceSection(source) {
|
|
707
|
+
const begin = source.indexOf(MARKER_BEGIN);
|
|
708
|
+
const end = source.indexOf(MARKER_END);
|
|
709
|
+
if (begin === -1 || end <= begin) return source;
|
|
710
|
+
return `${source.slice(0, begin)}${source.slice(end + 29).replace(/^\n/, "")}`;
|
|
711
|
+
}
|
|
712
|
+
/** The agent-facing golden path, written into `AGENTS.md`. */
|
|
713
|
+
function agentsGuidanceSection(ctx) {
|
|
714
|
+
const plan = publicCliCommand("plan", ctx.cliVersion);
|
|
715
|
+
const apply = publicCliCommand("apply", ctx.cliVersion);
|
|
716
|
+
const passkeyVerifyNote = ctx.preset === "passkey-first" ? " Agents: automated browsers can't complete passkey ceremonies — verify the loop via the email/password fallback actions, or attach a CDP WebAuthn virtual authenticator." : "";
|
|
717
|
+
const jsxTypesNote = ctx.framework.id === "next" ? " React JSX types for the `<zitadel-*>` elements ship with the SDK — `custom-elements.d.ts` references `@zitadel/sdk-next/jsx`." : "";
|
|
718
|
+
const presentationParagraph = ctx.posture === "widget" ? "Presentation, by contrast, is edited in the generated pages: this app pre-dates setup, so they embed the sign-in widgets as `variant=\"widget\"` cards (with `theme=\"auto\"`) inside your existing layout; switch to `variant=\"page\"` for the widget's own full-page chrome, and set `theme` (`light` | `dark` | `auto`) to pick the color scheme — `auto` follows the OS `prefers-color-scheme`, not this app's own theme, so pin `light` or `dark` when the app fixes its scheme." : "Presentation, by contrast, is edited in the generated pages: they pin the sign-in widgets to `variant=\"page\"` (full-page chrome); switch to `variant=\"widget\"` to embed a card inside your own layout, and set `theme` (`light` | `dark` | `auto`) to pick the color scheme — `auto` follows the OS `prefers-color-scheme`, not this app's own theme, so pin `light` or `dark` when the app fixes its scheme.";
|
|
719
|
+
const themingParagraph = "Theming the widgets from your app: `--zl-*` design-token custom properties set on the element in your own stylesheet pierce the widgets' shadow DOM. The bridge covers your fonts (`--zl-font-family-heading`, `--zl-font-family-sans`), radii (`--zl-radius-md`/`-lg`/`-xl`), the primary CTA pair (`--zl-primary` / `--zl-primary-foreground`), and the link color (`--zl-link`). When your page already carries the heading, set the `suppress-header` attribute on `<zitadel-login>` / `<zitadel-session>` (SDK wrappers: `suppressHeader`) to visually hide the widget's own heading block — it stays in the accessibility tree. Starter designs from `.zitadel/branding/` collapse their brand pane by container width, not viewport: an embedded card usually renders the compact brand mark instead (`logo_url`, else `hero_url`, from `branding.json`; the `hero` design falls back to editable text).";
|
|
720
|
+
const sessionStateParagraph = ctx.framework.id === "next" ? "Your app's own chrome (header navigation, account menus) reads session state with `getSession()` from `@zitadel/sdk-next/session` — a no-store client read of the same-origin `/__nextgen/sessions/me` that the session card itself uses. It returns identity only for 200 with a non-empty `user_id`, and signed-out only for the backend's canonical `401/auth.unauthorized` or `404/sess.not_found`; any other response is unknown/error, never signed-out. In Server Components on routes covered by the request-boundary `matcher`, `auth()` from `@zitadel/sdk-next/server` works too — extend the `matcher` in the request boundary to cover your public routes when you want server-rendered, flash-free session state in shared chrome (the trade: the middleware then runs on those routes). Sign-in and sign-out navigate (`post-sign-in-url` / `post-sign-out-url`), so chrome re-reads on the next page load without extra wiring." : ctx.framework.id === "nuxt" ? "Your app's own chrome (header navigation, account menus) reads session state with the auto-imported `useAuth()` composable — seeded from the server on every render by the scaffolded auth plugin. Sign-in and sign-out navigate (`post-sign-in-url` / `post-sign-out-url`), so the state is fresh on every page load." : "Your app's own chrome (header navigation, account menus) can read session state from the same-origin `/__nextgen/sessions/me` — the same server-answered read the session card performs. Fetch with `credentials: \"include\"`, `cache: \"no-store\"`, and `Accept: application/json`; treat 200 as signed-in only with a non-empty `user_id`, and signed-out only for the canonical error envelope `401/auth.unauthorized` or `404/sess.not_found`. Any other response is unknown/error, never signed-out. Sign-in and sign-out navigate (`post-sign-in-url` / `post-sign-out-url`), so chrome re-reads on the next page load.";
|
|
721
|
+
return `## Authentication (Zitadel)
|
|
722
|
+
|
|
723
|
+
This app's login is managed by Zitadel. Local config is the source of truth; never change auth behavior by editing generated route files.
|
|
724
|
+
|
|
725
|
+
The golden path:
|
|
726
|
+
|
|
727
|
+
1. Start the dev server and open ${ctx.issuer}/login — **exactly this origin, not 127.0.0.1**. Passkeys and the origin allowlist are bound to it. (Freshly scaffolded apps also redirect / there; pre-existing apps keep their homepage.)
|
|
728
|
+
2. Prove the loop in a real browser: register a user → sign out → sign in → /profile shows signed in.${passkeyVerifyNote}
|
|
729
|
+
3. Customize by editing local config:
|
|
730
|
+
- \`.zitadel/schemas/*.json\` — what a user is (fields, required, auth methods). See \`.zitadel/schemas/README.md\`.
|
|
731
|
+
- \`.zitadel/flows/*.json\` — which screen collects which field/credential and how steps transition. See \`.zitadel/flows/README.md\`.
|
|
732
|
+
4. Preview, then publish:
|
|
733
|
+
- \`${plan}\`
|
|
734
|
+
- \`${apply}\`
|
|
735
|
+
- Agents: append \`--non-interactive --json\` to both. \`plan\` validates flow invariants with the server's own rules **before** anything uploads — fix what it reports and re-run.
|
|
736
|
+
|
|
737
|
+
${presentationParagraph}${jsxTypesNote}
|
|
738
|
+
|
|
739
|
+
${themingParagraph}
|
|
740
|
+
|
|
741
|
+
${sessionStateParagraph}
|
|
742
|
+
|
|
743
|
+
Machine-readable dialect (read these before authoring flow or schema edits):
|
|
744
|
+
|
|
745
|
+
- Flow files carry \`"$schema": "../meta/flow-definition.json"\` — the flow dialect spec (steps, actions and their kinds, transitions, reserved outcomes like \`user_not_found\`). Editors validate against it.
|
|
746
|
+
- \`.zitadel/meta/user-schema.json\` (with its companions \`user-property.json\`, \`auth-methods.json\`, \`auth-method.json\`) specifies the user-schema dialect (\`x-auth-methods\`, \`x-unique\`, property constraints).
|
|
747
|
+
- Worked flow examples: https://github.com/zitadel/nextgen/tree/main/api/openapi/endpoints/flow_definitions/examples
|
|
748
|
+
|
|
749
|
+
Never edit \`.zitadel/state.json\` (sync bookkeeping) or \`.zitadel/secret\` (credentials, git-ignored). Keep \`.zitadel/local/\` out of source control.`;
|
|
750
|
+
}
|
|
751
|
+
/** The human-facing summary appended to the app `README.md`. */
|
|
752
|
+
function readmeGuidanceSection(ctx) {
|
|
753
|
+
const plan = publicCliCommand("plan", ctx.cliVersion);
|
|
754
|
+
const apply = publicCliCommand("apply", ctx.cliVersion);
|
|
755
|
+
return `## Authentication (Zitadel)
|
|
756
|
+
|
|
757
|
+
Login for this app is managed by [Zitadel](https://zitadel.com). Try it: start the dev server, open ${ctx.issuer}/login (use this exact origin — passkeys are bound to it), register a user, sign out, and sign in again.
|
|
758
|
+
|
|
759
|
+
To change what the login collects or how sign-in works, edit the files under \`.zitadel/schemas/\` and \`.zitadel/flows/\` (each folder has a README), then:
|
|
760
|
+
|
|
761
|
+
\`\`\`sh
|
|
762
|
+
${plan}
|
|
763
|
+
${apply}
|
|
764
|
+
\`\`\``;
|
|
765
|
+
}
|
|
766
|
+
/** Full-file header used when `AGENTS.md` does not exist yet. */
|
|
767
|
+
const AGENTS_HEADER = `# AGENTS.md
|
|
768
|
+
|
|
769
|
+
Guidance for AI agents working in this repository.
|
|
770
|
+
|
|
771
|
+
`;
|
|
772
|
+
//#endregion
|
|
485
773
|
//#region src/lib/orca/patchers/rule/reclaim.ts
|
|
486
774
|
/**
|
|
487
775
|
* The subset of a patcher plan's operations that `doctor --fix` re-applies:
|
|
@@ -500,6 +788,70 @@ function deepMerge(target, patch) {
|
|
|
500
788
|
function reclaimableOps(plan) {
|
|
501
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"));
|
|
502
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
|
+
}
|
|
503
855
|
//#endregion
|
|
504
856
|
//#region src/lib/orca/patchers/rule/base.ts
|
|
505
857
|
/**
|
|
@@ -519,27 +871,131 @@ var AbstractRulePatcher = class {
|
|
|
519
871
|
/**
|
|
520
872
|
* Re-apply only the reclaimable subset — env files, gitignore, the SDK
|
|
521
873
|
* dependency, and marker-bearing routes — leaving the user-editable
|
|
522
|
-
* `.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.
|
|
523
879
|
*/
|
|
524
880
|
async repair(ctx, opts) {
|
|
525
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;
|
|
526
893
|
return scaffold({
|
|
527
|
-
ops
|
|
894
|
+
ops,
|
|
528
895
|
summary: plan.summary
|
|
529
|
-
},
|
|
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;
|
|
530
948
|
}
|
|
531
949
|
/** Shared base artifacts plus the subclass's marker-bearing route files. */
|
|
532
950
|
artifacts(view) {
|
|
951
|
+
const infrastructure = new Set(this.infrastructureFiles(view));
|
|
952
|
+
const markedFiles = this.routeFiles(view);
|
|
533
953
|
return {
|
|
534
|
-
markedFiles
|
|
954
|
+
markedFiles,
|
|
535
955
|
rootConfigFiles: ["zitadel.json"],
|
|
536
956
|
directories: [".zitadel"],
|
|
537
957
|
envBackups: [".env.local"],
|
|
538
958
|
dependencies: this.routeDeps(view),
|
|
539
|
-
configEdits: this.routeConfigEdits(view)
|
|
959
|
+
configEdits: this.routeConfigEdits(view),
|
|
960
|
+
guidanceFiles: ["AGENTS.md", "README.md"],
|
|
961
|
+
fileClasses: Object.fromEntries(markedFiles.map((path) => [path, infrastructure.has(path) ? "infrastructure" : "presentation"])),
|
|
962
|
+
conditionalFiles: this.conditionallyScaffoldedFiles(view),
|
|
963
|
+
retiredAlternates: this.retiredAlternateFiles(view)
|
|
540
964
|
};
|
|
541
965
|
}
|
|
542
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
|
+
/**
|
|
543
999
|
* User config files this patcher merges into via an `edit` op (e.g.
|
|
544
1000
|
* `vite.config.ts`). `eject` can't reverse an in-place merge, so it lists
|
|
545
1001
|
* these as manual cleanup steps. Defaults to none; patchers that edit a config
|
|
@@ -563,10 +1019,9 @@ var AbstractRulePatcher = class {
|
|
|
563
1019
|
/**
|
|
564
1020
|
* The framework-agnostic `.zitadel/` base files every rule patcher writes:
|
|
565
1021
|
* the project secret, `zitadel.json`, env templates, and an empty sync
|
|
566
|
-
* state. The `schemas/` and `flows/` directories are created empty
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
* filesystem or network.
|
|
1022
|
+
* state. The `schemas/` and `flows/` directories are created empty here;
|
|
1023
|
+
* setup fills them from versioned local defaults after patching.
|
|
1024
|
+
* Pure: no filesystem or network.
|
|
570
1025
|
*/
|
|
571
1026
|
baseOps(ctx) {
|
|
572
1027
|
return [
|
|
@@ -597,10 +1052,10 @@ var AbstractRulePatcher = class {
|
|
|
597
1052
|
mode: 384,
|
|
598
1053
|
contents: `${stableStringify({
|
|
599
1054
|
project_id: ctx.project.id,
|
|
600
|
-
project_secret: ctx.project.
|
|
601
|
-
preview_secret: ctx.project.
|
|
602
|
-
preview_origins: ctx.project.
|
|
603
|
-
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
|
|
604
1059
|
})}\n`
|
|
605
1060
|
},
|
|
606
1061
|
{
|
|
@@ -613,6 +1068,7 @@ var AbstractRulePatcher = class {
|
|
|
613
1068
|
path: ".env.example",
|
|
614
1069
|
entries: {
|
|
615
1070
|
ZITADEL_PROJECT_ID: "",
|
|
1071
|
+
ZITADEL_PROJECT_SECRET: "",
|
|
616
1072
|
ZITADEL_ENVIRONMENT: "",
|
|
617
1073
|
ZITADEL_ISSUER: "",
|
|
618
1074
|
ZITADEL_URL: ""
|
|
@@ -623,6 +1079,7 @@ var AbstractRulePatcher = class {
|
|
|
623
1079
|
path: ".env.local",
|
|
624
1080
|
entries: {
|
|
625
1081
|
ZITADEL_PROJECT_ID: ctx.project.id,
|
|
1082
|
+
ZITADEL_PROJECT_SECRET: ctx.project.project_secret,
|
|
626
1083
|
ZITADEL_ENVIRONMENT: "development",
|
|
627
1084
|
ZITADEL_ISSUER: ctx.issuer,
|
|
628
1085
|
ZITADEL_URL: ctx.server
|
|
@@ -635,14 +1092,41 @@ var AbstractRulePatcher = class {
|
|
|
635
1092
|
framework: ctx.framework.id,
|
|
636
1093
|
resources: {}
|
|
637
1094
|
})}\n`
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
kind: "mkdir",
|
|
1098
|
+
path: META_SCHEMA_DIR
|
|
1099
|
+
},
|
|
1100
|
+
...metaSchemaFiles().map((file) => ({
|
|
1101
|
+
kind: "write",
|
|
1102
|
+
path: `${META_SCHEMA_DIR}/${file.name}`,
|
|
1103
|
+
contents: `${JSON.stringify(normalizePublicCliJson(file.body, ctx.cliVersion), null, 2)}\n`
|
|
1104
|
+
})),
|
|
1105
|
+
{
|
|
1106
|
+
kind: "edit",
|
|
1107
|
+
path: "AGENTS.md",
|
|
1108
|
+
edit: (source) => upsertGuidanceSection(source, agentsGuidanceSection(ctx), AGENTS_HEADER)
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
kind: "edit",
|
|
1112
|
+
path: "README.md",
|
|
1113
|
+
edit: (source) => upsertGuidanceSection(source, readmeGuidanceSection(ctx), "")
|
|
638
1114
|
}
|
|
639
1115
|
];
|
|
640
1116
|
}
|
|
641
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
|
+
}
|
|
642
1126
|
/** Builds the `zitadel.json` body persisted at the project root. */
|
|
643
1127
|
function projectConfig(ctx) {
|
|
644
1128
|
const environments = { development: { issuer: ctx.issuer } };
|
|
645
|
-
if (ctx.project.
|
|
1129
|
+
if (ctx.project.preview_origins.length > 0) environments.preview = { issuer_pattern: [...ctx.project.preview_origins] };
|
|
646
1130
|
return {
|
|
647
1131
|
$schema: "https://schemas.zitadel.com/v2/project.schema.json",
|
|
648
1132
|
project: ctx.project.id,
|
|
@@ -652,7 +1136,9 @@ function projectConfig(ctx) {
|
|
|
652
1136
|
renderer: ctx.rendererId,
|
|
653
1137
|
attribution: "visible"
|
|
654
1138
|
},
|
|
655
|
-
environments
|
|
1139
|
+
environments,
|
|
1140
|
+
...ctx.preset === void 0 ? {} : { preset: ctx.preset },
|
|
1141
|
+
...ctx.useCase === void 0 ? {} : { useCase: ctx.useCase }
|
|
656
1142
|
};
|
|
657
1143
|
}
|
|
658
1144
|
/** Normalizes a server URL to its origin, falling back to {@link DEFAULT_SERVER}. */
|
|
@@ -850,82 +1336,172 @@ function angularRoutesEdit() {
|
|
|
850
1336
|
*/
|
|
851
1337
|
const PROXY_PATH = "/__nextgen";
|
|
852
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
|
|
853
1371
|
//#region src/lib/orca/patchers/rule/angular/templates.ts
|
|
854
1372
|
/**
|
|
855
1373
|
* The managed root component `src/app/app.ts`: a standalone component that
|
|
856
1374
|
* renders the `@zitadel/sdk-angular` widgets based on the current path. The
|
|
857
1375
|
* project id (public, not secret) is inlined; the dev proxy in `proxy.conf.cjs`
|
|
858
|
-
* attaches the
|
|
859
|
-
*
|
|
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.
|
|
860
1384
|
*/
|
|
861
|
-
function appComponentTemplate(
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1385
|
+
function appComponentTemplate(ctx) {
|
|
1386
|
+
const business = ctx.useCase === "business";
|
|
1387
|
+
const importNames = business ? `
|
|
1388
|
+
ZitadelLoginComponent,
|
|
1389
|
+
ZitadelSessionComponent,
|
|
1390
|
+
businessLocales,
|
|
1391
|
+
configureZitadel,
|
|
1392
|
+
` : `
|
|
865
1393
|
ZitadelLoginComponent,
|
|
866
|
-
|
|
1394
|
+
ZitadelSessionComponent,
|
|
867
1395
|
configureZitadel,
|
|
868
|
-
|
|
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";
|
|
869
1405
|
|
|
870
1406
|
@Component({
|
|
871
1407
|
selector: "app-root",
|
|
872
1408
|
standalone: true,
|
|
873
|
-
imports: [ZitadelLoginComponent,
|
|
1409
|
+
imports: [ZitadelLoginComponent, ZitadelSessionComponent],
|
|
874
1410
|
templateUrl: "./app.html",
|
|
875
1411
|
})
|
|
876
|
-
export class App {
|
|
1412
|
+
export class App implements OnInit {
|
|
877
1413
|
protected readonly project = configureZitadel({
|
|
878
|
-
projectId: ${JSON.stringify(
|
|
1414
|
+
projectId: ${JSON.stringify(ctx.project.id)},
|
|
879
1415
|
proxyPath: "${PROXY_PATH}",
|
|
880
1416
|
});
|
|
881
|
-
protected readonly path = window.location.pathname
|
|
1417
|
+
protected readonly path = window.location.pathname;${localesProperty}
|
|
1418
|
+
|
|
1419
|
+
ngOnInit(): void {
|
|
1420
|
+
if (this.path === "/") {
|
|
1421
|
+
window.location.replace("/login");
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
882
1424
|
}
|
|
883
1425
|
`;
|
|
884
1426
|
}
|
|
885
1427
|
/**
|
|
886
1428
|
* The managed `src/app/app.html`. The marker lives in an HTML comment that still
|
|
887
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.
|
|
888
1432
|
*/
|
|
889
|
-
function appTemplateHtml() {
|
|
1433
|
+
function appTemplateHtml(ctx) {
|
|
1434
|
+
const localesBinding = ctx.useCase === "business" ? `\n [locales]="locales"` : "";
|
|
890
1435
|
return `<!-- ${MANAGED_MARKER} -->
|
|
891
1436
|
@if (path.startsWith('/profile')) {
|
|
892
|
-
<
|
|
1437
|
+
<div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
|
|
1438
|
+
<zitadel-auth-session
|
|
1439
|
+
[project]="project"
|
|
1440
|
+
[postSignOutUrl]="'/login'"
|
|
1441
|
+
></zitadel-auth-session>
|
|
1442
|
+
</div>
|
|
893
1443
|
} @else if (path.startsWith('/register')) {
|
|
894
|
-
<
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1444
|
+
<div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
|
|
1445
|
+
<zitadel-auth-login
|
|
1446
|
+
[project]="project"${localesBinding}
|
|
1447
|
+
purpose="register"
|
|
1448
|
+
[postSignInUrl]="'/profile'"
|
|
1449
|
+
></zitadel-auth-login>
|
|
1450
|
+
</div>
|
|
899
1451
|
} @else {
|
|
900
|
-
<
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1452
|
+
<div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
|
|
1453
|
+
<zitadel-auth-login
|
|
1454
|
+
[project]="project"${localesBinding}
|
|
1455
|
+
purpose="login"
|
|
1456
|
+
[postSignInUrl]="'/profile'"
|
|
1457
|
+
></zitadel-auth-login>
|
|
1458
|
+
</div>
|
|
905
1459
|
}
|
|
906
1460
|
`;
|
|
907
1461
|
}
|
|
908
1462
|
/**
|
|
909
1463
|
* The managed `proxy.conf.cjs` for `ng serve`: forwards `/__nextgen/*` to the
|
|
910
|
-
* backend
|
|
911
|
-
* `
|
|
912
|
-
*
|
|
913
|
-
* (`
|
|
914
|
-
*
|
|
1464
|
+
* backend and attaches the project's service-key secret only to the current
|
|
1465
|
+
* app-plane `POST /sessions/exchange` request. Both the backend URL
|
|
1466
|
+
* (`ZITADEL_URL`) and the secret
|
|
1467
|
+
* (`ZITADEL_PROJECT_SECRET`) are read from `.env.local`, which `zitadel setup`
|
|
1468
|
+
* writes and `.gitignore` excludes — Angular's CLI does not auto-load env files
|
|
1469
|
+
* into the dev-server process, so this file does it itself with a small inline
|
|
1470
|
+
* parser. The prefix strip and the bearer are each provided in both the
|
|
1471
|
+
* http-proxy-middleware form (`pathRewrite`/`onProxyReq`) and the Vite form
|
|
1472
|
+
* (`rewrite`/`configure`), so both fire whichever proxy layer Angular's
|
|
1473
|
+
* dev server uses.
|
|
915
1474
|
*/
|
|
916
1475
|
function proxyConfTemplate() {
|
|
917
1476
|
return `${MANAGED_MARKER}
|
|
918
|
-
const { readFileSync } = require("node:fs");
|
|
1477
|
+
const { readFileSync, existsSync } = require("node:fs");
|
|
919
1478
|
|
|
920
|
-
|
|
921
|
-
if (!
|
|
922
|
-
|
|
1479
|
+
function loadEnvLocal() {
|
|
1480
|
+
if (!existsSync(".env.local")) return {};
|
|
1481
|
+
const out = {};
|
|
1482
|
+
for (const line of readFileSync(".env.local", "utf8").split(/\\r?\\n/)) {
|
|
1483
|
+
const m = line.match(/^\\s*(?:export\\s+)?([A-Z_][A-Z0-9_]*)\\s*=\\s*(.*)$/);
|
|
1484
|
+
if (m) {
|
|
1485
|
+
const raw = m[2].trim();
|
|
1486
|
+
const quoted = raw.match(/^(['"])(.*)\\1\\s*(?:#.*)?$/);
|
|
1487
|
+
out[m[1]] = quoted ? quoted[2] : raw.replace(/\\s+#.*$/, "").trim();
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return out;
|
|
923
1491
|
}
|
|
924
|
-
const bearer = \`Bearer sk_\${config.project}\`;
|
|
925
1492
|
|
|
926
|
-
|
|
927
|
-
|
|
1493
|
+
const env = loadEnvLocal();
|
|
1494
|
+
const server = process.env.ZITADEL_URL ?? env.ZITADEL_URL;
|
|
1495
|
+
const secret = process.env.ZITADEL_PROJECT_SECRET ?? env.ZITADEL_PROJECT_SECRET;
|
|
1496
|
+
if (!server) {
|
|
1497
|
+
throw new Error("ZITADEL_URL is not set; add it to .env.local (zitadel setup writes it).");
|
|
1498
|
+
}
|
|
1499
|
+
if (!secret) {
|
|
1500
|
+
throw new Error("ZITADEL_PROJECT_SECRET is not set; add it to .env.local (zitadel setup writes it).");
|
|
928
1501
|
}
|
|
1502
|
+
const bearer = \`Bearer \${secret}\`;
|
|
1503
|
+
|
|
1504
|
+
${EXCHANGE_ONLY_BEARER}
|
|
929
1505
|
|
|
930
1506
|
function stripPrefix(path) {
|
|
931
1507
|
return path.replace(/^\\${PROXY_PATH}/, "").replace(/^(?!\\/)/, "/");
|
|
@@ -933,7 +1509,7 @@ function stripPrefix(path) {
|
|
|
933
1509
|
|
|
934
1510
|
module.exports = {
|
|
935
1511
|
"${PROXY_PATH}": {
|
|
936
|
-
target:
|
|
1512
|
+
target: server,
|
|
937
1513
|
changeOrigin: false,
|
|
938
1514
|
pathRewrite: stripPrefix,
|
|
939
1515
|
rewrite: stripPrefix,
|
|
@@ -943,9 +1519,37 @@ module.exports = {
|
|
|
943
1519
|
};
|
|
944
1520
|
`;
|
|
945
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
|
+
}
|
|
946
1550
|
//#endregion
|
|
947
1551
|
//#region src/lib/orca/patchers/rule/angular/index.ts
|
|
948
|
-
const SDK_DEPENDENCY$
|
|
1552
|
+
const SDK_DEPENDENCY$6 = "@zitadel/sdk-angular";
|
|
949
1553
|
/**
|
|
950
1554
|
* Adds a `dev: "ng serve"` script only when the project does not already define
|
|
951
1555
|
* one. `ng new` ships only a `start` script, but the CLI tells every framework
|
|
@@ -958,22 +1562,24 @@ function ensureDevScript(source) {
|
|
|
958
1562
|
const pkg = parseJsonObject(source, "package.json");
|
|
959
1563
|
const scripts = isObject(pkg.scripts) ? pkg.scripts : void 0;
|
|
960
1564
|
if (scripts?.dev !== void 0) return source;
|
|
961
|
-
|
|
1565
|
+
return setTopLevelJsonKey(source, "package.json", "scripts", {
|
|
962
1566
|
...scripts ?? {},
|
|
963
1567
|
dev: "ng serve"
|
|
964
|
-
};
|
|
965
|
-
return `${stableStringify(pkg)}\n`;
|
|
1568
|
+
});
|
|
966
1569
|
}
|
|
967
1570
|
/**
|
|
968
1571
|
* Rule-based patcher for an Angular app. Inherits the shared `.zitadel/` base
|
|
969
1572
|
* files from {@link AbstractRulePatcher} and contributes the managed root
|
|
970
1573
|
* component (`app.ts`/`app.html`) that renders the `@zitadel/sdk-angular`
|
|
971
|
-
* widgets, a `proxy.conf.cjs` dev proxy (attaching the
|
|
972
|
-
* to
|
|
1574
|
+
* widgets, a `proxy.conf.cjs` dev proxy (attaching the project secret from
|
|
1575
|
+
* `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`) wired into `angular.json`,
|
|
1576
|
+
* and the SDK dep.
|
|
973
1577
|
*
|
|
974
1578
|
* Unlike React/Vue (whose dev proxy lives in `vite.config.ts`), Angular owns its
|
|
975
1579
|
* Vite config, so the proxy is a separate `proxy.conf.cjs` referenced from the
|
|
976
|
-
* `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.
|
|
977
1583
|
*/
|
|
978
1584
|
var AngularPatcher = class extends AbstractRulePatcher {
|
|
979
1585
|
canPatch(framework) {
|
|
@@ -984,22 +1590,24 @@ var AngularPatcher = class extends AbstractRulePatcher {
|
|
|
984
1590
|
{
|
|
985
1591
|
kind: "write",
|
|
986
1592
|
path: "src/app/app.ts",
|
|
987
|
-
contents: appComponentTemplate(ctx
|
|
1593
|
+
contents: appComponentTemplate(ctx)
|
|
988
1594
|
},
|
|
989
1595
|
{
|
|
990
1596
|
kind: "write",
|
|
991
1597
|
path: "src/app/app.html",
|
|
992
|
-
contents: appTemplateHtml()
|
|
1598
|
+
contents: appTemplateHtml(ctx)
|
|
993
1599
|
},
|
|
994
1600
|
{
|
|
995
1601
|
kind: "edit",
|
|
996
1602
|
path: "src/app/app.routes.ts",
|
|
997
|
-
edit: angularRoutesEdit()
|
|
1603
|
+
edit: angularRoutesEdit(),
|
|
1604
|
+
wiring: "infrastructure"
|
|
998
1605
|
},
|
|
999
1606
|
{
|
|
1000
|
-
kind: "
|
|
1607
|
+
kind: "edit",
|
|
1001
1608
|
path: "proxy.conf.cjs",
|
|
1002
|
-
|
|
1609
|
+
edit: proxyConfEdit(),
|
|
1610
|
+
wiring: "infrastructure"
|
|
1003
1611
|
},
|
|
1004
1612
|
{
|
|
1005
1613
|
kind: "edit",
|
|
@@ -1007,16 +1615,18 @@ var AngularPatcher = class extends AbstractRulePatcher {
|
|
|
1007
1615
|
edit: angularProxyEdit({
|
|
1008
1616
|
proxyConfig: "proxy.conf.cjs",
|
|
1009
1617
|
port: ctx.framework.devPort
|
|
1010
|
-
})
|
|
1618
|
+
}),
|
|
1619
|
+
wiring: "infrastructure"
|
|
1011
1620
|
},
|
|
1012
1621
|
{
|
|
1013
1622
|
kind: "edit",
|
|
1014
1623
|
path: "package.json",
|
|
1015
|
-
edit: ensureDevScript
|
|
1624
|
+
edit: ensureDevScript,
|
|
1625
|
+
wiring: "convenience"
|
|
1016
1626
|
},
|
|
1017
1627
|
{
|
|
1018
1628
|
kind: "add-dep",
|
|
1019
|
-
name: SDK_DEPENDENCY$
|
|
1629
|
+
name: SDK_DEPENDENCY$6,
|
|
1020
1630
|
version: npmDistTagForCliVersion(ctx.cliVersion)
|
|
1021
1631
|
}
|
|
1022
1632
|
];
|
|
@@ -1028,8 +1638,11 @@ var AngularPatcher = class extends AbstractRulePatcher {
|
|
|
1028
1638
|
"proxy.conf.cjs"
|
|
1029
1639
|
];
|
|
1030
1640
|
}
|
|
1641
|
+
infrastructureFiles(_view) {
|
|
1642
|
+
return ["proxy.conf.cjs"];
|
|
1643
|
+
}
|
|
1031
1644
|
routeDeps(_view) {
|
|
1032
|
-
return [SDK_DEPENDENCY$
|
|
1645
|
+
return [SDK_DEPENDENCY$6];
|
|
1033
1646
|
}
|
|
1034
1647
|
routeConfigEdits(_view) {
|
|
1035
1648
|
return [
|
|
@@ -1046,6 +1659,73 @@ var AngularPatcher = class extends AbstractRulePatcher {
|
|
|
1046
1659
|
}
|
|
1047
1660
|
};
|
|
1048
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
|
|
1049
1729
|
//#region src/lib/orca/patchers/rule/next/renderers/lit/index.ts
|
|
1050
1730
|
/**
|
|
1051
1731
|
* Placeholder renderer for the `<zitadel-flow>` Lit web component. Declared
|
|
@@ -1102,8 +1782,8 @@ export default function ${mode === "login" ? "LoginPage" : "RegisterPage"}() {
|
|
|
1102
1782
|
//#region src/lib/orca/patchers/rule/next/renderers/react/index.ts
|
|
1103
1783
|
/**
|
|
1104
1784
|
* The Next.js App Router renderer scaffolds `/login`, `/register`, and
|
|
1105
|
-
* `/profile` pages that drive the `<zitadel-login>` and `<zitadel-
|
|
1106
|
-
* Lit web components.
|
|
1785
|
+
* `/profile` pages that drive the `<zitadel-login>` and `<zitadel-session>`
|
|
1786
|
+
* Lit web components. `/profile` is the post-sign-in "signed in as" card.
|
|
1107
1787
|
*
|
|
1108
1788
|
* Each page is a single client component (`"use client"`) that, inside a
|
|
1109
1789
|
* `next/dynamic({ ssr: false })` loader, builds the SDK project handle with
|
|
@@ -1122,6 +1802,15 @@ export default function ${mode === "login" ? "LoginPage" : "RegisterPage"}() {
|
|
|
1122
1802
|
* forwards it to `ZITADEL_URL` server-side. `NEXT_PUBLIC_ZITADEL_PROJECT_ID` is
|
|
1123
1803
|
* public — the project id is not sensitive and the widget needs it to start a
|
|
1124
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).
|
|
1125
1814
|
*/
|
|
1126
1815
|
const reactRenderer = {
|
|
1127
1816
|
id: "react",
|
|
@@ -1133,20 +1822,34 @@ const reactRenderer = {
|
|
|
1133
1822
|
version: "latest"
|
|
1134
1823
|
},
|
|
1135
1824
|
templates: {
|
|
1136
|
-
authPage(mode) {
|
|
1825
|
+
authPage(mode, context) {
|
|
1137
1826
|
const componentName = mode === "login" ? "LoginPage" : "RegisterPage";
|
|
1138
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";
|
|
1139
1843
|
return {
|
|
1140
1844
|
mode,
|
|
1141
1845
|
contents: `${MANAGED_MARKER}
|
|
1142
1846
|
"use client";
|
|
1143
1847
|
|
|
1144
1848
|
import dynamic from "next/dynamic";
|
|
1145
|
-
import Link from "next/link";
|
|
1146
1849
|
|
|
1147
1850
|
const ${elementName} = dynamic(
|
|
1148
1851
|
async () => {
|
|
1149
|
-
const {
|
|
1852
|
+
const { ${importNames} } = await import("@zitadel/sdk-next/client");
|
|
1150
1853
|
// Build the SDK project handle and pass it to the component via the
|
|
1151
1854
|
// \`project\` prop. The component reads config from this prop directly, so
|
|
1152
1855
|
// it works regardless of how the SDK packages are bundled. The backend URL
|
|
@@ -1156,9 +1859,10 @@ const ${elementName} = dynamic(
|
|
|
1156
1859
|
projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
|
|
1157
1860
|
proxyPath: "/__nextgen",
|
|
1158
1861
|
});
|
|
1159
|
-
return function ${elementName}Element() {
|
|
1862
|
+
return function ${elementName}Element() {${localesComment}
|
|
1160
1863
|
return (
|
|
1161
1864
|
<zitadel-login
|
|
1865
|
+
${widget ? `variant="widget"\n theme="auto"` : `variant="page"`}${localesAttr}
|
|
1162
1866
|
project={project}
|
|
1163
1867
|
purpose="${mode}"
|
|
1164
1868
|
post-sign-in-url="/profile"
|
|
@@ -1170,43 +1874,50 @@ const ${elementName} = dynamic(
|
|
|
1170
1874
|
);
|
|
1171
1875
|
|
|
1172
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.`}
|
|
1173
1889
|
return (
|
|
1174
|
-
|
|
1175
|
-
<nav aria-label="Authentication" style={{ position: "absolute", top: "24px", right: "24px", zIndex: 1, display: "flex", gap: "12px" }}>
|
|
1176
|
-
<Link href="${mode === "login" ? "/register" : "/login"}" style={{ color: "#f4f4f6", fontWeight: 700, textDecoration: "none" }}>
|
|
1177
|
-
${mode === "login" ? "Create account" : "Sign in"}
|
|
1178
|
-
</Link>
|
|
1179
|
-
</nav>
|
|
1890
|
+
${widget ? `<div style={{ display: "flex", justifyContent: "center", padding: "4rem 1rem" }}>` : `<main style={{ colorScheme: "dark" }}>`}
|
|
1180
1891
|
<${elementName} />
|
|
1181
|
-
|
|
1892
|
+
${widget ? `</div>` : `</main>`}
|
|
1182
1893
|
);
|
|
1183
1894
|
}
|
|
1184
1895
|
`
|
|
1185
1896
|
};
|
|
1186
1897
|
},
|
|
1187
|
-
profilePage() {
|
|
1898
|
+
profilePage(context) {
|
|
1899
|
+
const widget = context.posture === "widget";
|
|
1188
1900
|
return { contents: `${MANAGED_MARKER}
|
|
1189
1901
|
"use client";
|
|
1190
1902
|
|
|
1191
1903
|
import dynamic from "next/dynamic";
|
|
1192
|
-
import { useEffect, useState } from "react";
|
|
1193
|
-
|
|
1194
|
-
type SessionProof = {
|
|
1195
|
-
session_id?: string;
|
|
1196
|
-
state?: string;
|
|
1197
|
-
user_id?: string;
|
|
1198
|
-
};
|
|
1199
1904
|
|
|
1200
|
-
const
|
|
1905
|
+
const ZitadelSession = dynamic(
|
|
1201
1906
|
async () => {
|
|
1202
1907
|
const { configureZitadel } = await import("@zitadel/sdk-next/client");
|
|
1908
|
+
// Build the SDK project handle and pass it to the session card via the
|
|
1909
|
+
// \`project\` prop. The card reads identity from "/__nextgen/sessions/me"
|
|
1910
|
+
// and exposes a Sign out action. Your own components (a header, an
|
|
1911
|
+
// account menu) can make the same read with getSession() from
|
|
1912
|
+
// "@zitadel/sdk-next/session" to swap sign-in CTAs for account chrome.
|
|
1203
1913
|
const project = configureZitadel({
|
|
1204
1914
|
projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
|
|
1205
1915
|
proxyPath: "/__nextgen",
|
|
1206
1916
|
});
|
|
1207
|
-
return function
|
|
1917
|
+
return function ZitadelSessionElement() {
|
|
1208
1918
|
return (
|
|
1209
|
-
<zitadel-
|
|
1919
|
+
<zitadel-session
|
|
1920
|
+
${widget ? `variant="widget"\n theme="auto"` : `variant="page"`}
|
|
1210
1921
|
project={project}
|
|
1211
1922
|
post-sign-out-url="/login"
|
|
1212
1923
|
/>
|
|
@@ -1217,84 +1928,32 @@ const ZitadelLogout = dynamic(
|
|
|
1217
1928
|
);
|
|
1218
1929
|
|
|
1219
1930
|
export default function ProfilePage() {
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
})
|
|
1233
|
-
.then((nextSession) => {
|
|
1234
|
-
if (!cancelled) {
|
|
1235
|
-
setSession(nextSession);
|
|
1236
|
-
}
|
|
1237
|
-
})
|
|
1238
|
-
.catch((error: unknown) => {
|
|
1239
|
-
if (!cancelled) {
|
|
1240
|
-
setSessionError(error instanceof Error ? error.message : "Session check failed");
|
|
1241
|
-
}
|
|
1242
|
-
});
|
|
1243
|
-
|
|
1244
|
-
return () => {
|
|
1245
|
-
cancelled = true;
|
|
1246
|
-
};
|
|
1247
|
-
}, []);
|
|
1248
|
-
|
|
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.`}
|
|
1249
1943
|
return (
|
|
1250
|
-
|
|
1251
|
-
<
|
|
1252
|
-
|
|
1253
|
-
<ZitadelLogout />
|
|
1254
|
-
</div>
|
|
1255
|
-
<p style={{ color: "#166534", fontWeight: 600 }}>Signed in profile loaded.</p>
|
|
1256
|
-
{session ? (
|
|
1257
|
-
<dl style={{ display: "grid", gap: "12px", marginTop: "24px" }}>
|
|
1258
|
-
<div>
|
|
1259
|
-
<dt style={{ color: "#6b7280", fontSize: "14px" }}>Session state</dt>
|
|
1260
|
-
<dd style={{ margin: 0, fontWeight: 600 }}>{session.state ?? "active"}</dd>
|
|
1261
|
-
</div>
|
|
1262
|
-
<div>
|
|
1263
|
-
<dt style={{ color: "#6b7280", fontSize: "14px" }}>User id</dt>
|
|
1264
|
-
<dd style={{ margin: 0, fontFamily: "monospace" }}>{session.user_id ?? "available"}</dd>
|
|
1265
|
-
</div>
|
|
1266
|
-
</dl>
|
|
1267
|
-
) : (
|
|
1268
|
-
<p style={{ color: sessionError ? "#b91c1c" : "#6b7280" }}>
|
|
1269
|
-
{sessionError || "Checking session..."}
|
|
1270
|
-
</p>
|
|
1271
|
-
)}
|
|
1272
|
-
</main>
|
|
1944
|
+
${widget ? `<div style={{ display: "flex", justifyContent: "center", padding: "4rem 1rem" }}>` : `<main style={{ colorScheme: "dark" }}>`}
|
|
1945
|
+
<ZitadelSession />
|
|
1946
|
+
${widget ? `</div>` : `</main>`}
|
|
1273
1947
|
);
|
|
1274
1948
|
}
|
|
1275
1949
|
` };
|
|
1276
1950
|
},
|
|
1277
1951
|
customElementsDts() {
|
|
1278
1952
|
return { contents: `${MANAGED_MARKER}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
namespace JSX {
|
|
1284
|
-
interface IntrinsicElements {
|
|
1285
|
-
"zitadel-login": React.HTMLAttributes<HTMLElement> & {
|
|
1286
|
-
project?: ZitadelProject;
|
|
1287
|
-
"session-exchange-path"?: string;
|
|
1288
|
-
"post-sign-in-url"?: string;
|
|
1289
|
-
purpose?: string;
|
|
1290
|
-
};
|
|
1291
|
-
"zitadel-logout": React.HTMLAttributes<HTMLElement> & {
|
|
1292
|
-
project?: ZitadelProject;
|
|
1293
|
-
"post-sign-out-url"?: string;
|
|
1294
|
-
};
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
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" />
|
|
1298
1957
|
` };
|
|
1299
1958
|
}
|
|
1300
1959
|
}
|
|
@@ -1324,6 +1983,15 @@ const RENDERERS = {
|
|
|
1324
1983
|
"web-component": litRenderer
|
|
1325
1984
|
};
|
|
1326
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
|
+
/**
|
|
1327
1995
|
* Resolves a renderer id (an untrusted string from config) to its spec,
|
|
1328
1996
|
* throwing a typed {@link ZitadelError} rather than returning `undefined`
|
|
1329
1997
|
* so callers get an actionable message. Rejects ids that are unknown
|
|
@@ -1331,7 +1999,7 @@ const RENDERERS = {
|
|
|
1331
1999
|
* guaranteeing the returned spec is safe to scaffold from.
|
|
1332
2000
|
*/
|
|
1333
2001
|
function getRenderer(id) {
|
|
1334
|
-
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(", ")}` });
|
|
1335
2003
|
const renderer = RENDERERS[id];
|
|
1336
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." });
|
|
1337
2005
|
return renderer;
|
|
@@ -1379,9 +2047,22 @@ var NextPatcher = class extends AbstractRulePatcher {
|
|
|
1379
2047
|
routeFiles(view) {
|
|
1380
2048
|
return nextCodeFilePaths(view.framework, getRenderer(view.rendererId));
|
|
1381
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
|
+
}
|
|
1382
2060
|
routeDeps(view) {
|
|
1383
2061
|
return [getRenderer(view.rendererId).dependency.name];
|
|
1384
2062
|
}
|
|
2063
|
+
routeConfigEdits(_view) {
|
|
2064
|
+
return ["package.json"];
|
|
2065
|
+
}
|
|
1385
2066
|
summary(ctx) {
|
|
1386
2067
|
return {
|
|
1387
2068
|
title: "Next.js integration",
|
|
@@ -1408,10 +2089,28 @@ function nextCodeFilePaths(framework, renderer) {
|
|
|
1408
2089
|
if (renderer.templates.customElementsDts) paths.push(join(appDir, "../custom-elements.d.ts"));
|
|
1409
2090
|
return paths;
|
|
1410
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
|
+
}
|
|
1411
2106
|
/** The Next route/request-boundary write ops plus the SDK dependency. */
|
|
1412
2107
|
function nextCodeOps(ctx, renderer) {
|
|
1413
2108
|
const appDir = ctx.framework.appDir;
|
|
1414
|
-
const
|
|
2109
|
+
const pageContext = {
|
|
2110
|
+
useCase: ctx.useCase,
|
|
2111
|
+
posture: ctx.posture
|
|
2112
|
+
};
|
|
2113
|
+
const profile = renderer.templates.profilePage?.(pageContext);
|
|
1415
2114
|
const provider = renderer.templates.provider;
|
|
1416
2115
|
const dts = renderer.templates.customElementsDts?.();
|
|
1417
2116
|
const boundary = requestBoundaryFile(ctx.framework);
|
|
@@ -1419,17 +2118,18 @@ function nextCodeOps(ctx, renderer) {
|
|
|
1419
2118
|
ctx.scaffoldedFramework ? {
|
|
1420
2119
|
kind: "edit",
|
|
1421
2120
|
path: join(appDir, "page.tsx"),
|
|
1422
|
-
edit: () => homePageTemplate()
|
|
2121
|
+
edit: () => homePageTemplate(),
|
|
2122
|
+
overwrites: true
|
|
1423
2123
|
} : void 0,
|
|
1424
2124
|
{
|
|
1425
2125
|
kind: "write",
|
|
1426
2126
|
path: join(appDir, "login/page.tsx"),
|
|
1427
|
-
contents: renderer.templates.authPage("login").contents
|
|
2127
|
+
contents: renderer.templates.authPage("login", pageContext).contents
|
|
1428
2128
|
},
|
|
1429
2129
|
{
|
|
1430
2130
|
kind: "write",
|
|
1431
2131
|
path: join(appDir, "register/page.tsx"),
|
|
1432
|
-
contents: renderer.templates.authPage("register").contents
|
|
2132
|
+
contents: renderer.templates.authPage("register", pageContext).contents
|
|
1433
2133
|
},
|
|
1434
2134
|
profile ? {
|
|
1435
2135
|
kind: "write",
|
|
@@ -1465,33 +2165,16 @@ function nextCodeOps(ctx, renderer) {
|
|
|
1465
2165
|
kind: "add-dep",
|
|
1466
2166
|
name: renderer.dependency.name,
|
|
1467
2167
|
version: dependencyVersionForCli(ctx.cliVersion, renderer.dependency.version)
|
|
1468
|
-
}
|
|
2168
|
+
},
|
|
2169
|
+
devScriptPortOp(ctx.issuer)
|
|
1469
2170
|
].filter((op) => op !== void 0);
|
|
1470
2171
|
}
|
|
1471
2172
|
function homePageTemplate() {
|
|
1472
2173
|
return `${MANAGED_MARKER}
|
|
1473
|
-
import
|
|
2174
|
+
import { redirect } from "next/navigation";
|
|
1474
2175
|
|
|
1475
2176
|
export default function Home() {
|
|
1476
|
-
|
|
1477
|
-
<main style={{ minHeight: "100vh", padding: "48px", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
|
1478
|
-
<section style={{ width: "100%", maxWidth: "560px" }}>
|
|
1479
|
-
<p style={{ margin: "0 0 12px", color: "#4b5563", fontSize: "14px" }}>Zitadel auth</p>
|
|
1480
|
-
<h1 style={{ margin: "0 0 24px", fontSize: "32px", lineHeight: 1.15 }}>Sign in, create an account, or open your profile.</h1>
|
|
1481
|
-
<div style={{ display: "flex", flexWrap: "wrap", gap: "12px" }}>
|
|
1482
|
-
<Link href="/login" style={{ padding: "10px 16px", borderRadius: "8px", background: "#111827", color: "#ffffff", textDecoration: "none", fontWeight: 600 }}>
|
|
1483
|
-
Sign in
|
|
1484
|
-
</Link>
|
|
1485
|
-
<Link href="/register" style={{ padding: "10px 16px", borderRadius: "8px", border: "1px solid #d1d5db", color: "#111827", textDecoration: "none", fontWeight: 600 }}>
|
|
1486
|
-
Create account
|
|
1487
|
-
</Link>
|
|
1488
|
-
<Link href="/profile" style={{ padding: "10px 16px", borderRadius: "8px", border: "1px solid #d1d5db", color: "#111827", textDecoration: "none", fontWeight: 600 }}>
|
|
1489
|
-
Profile
|
|
1490
|
-
</Link>
|
|
1491
|
-
</div>
|
|
1492
|
-
</section>
|
|
1493
|
-
</main>
|
|
1494
|
-
);
|
|
2177
|
+
redirect("/login");
|
|
1495
2178
|
}
|
|
1496
2179
|
`;
|
|
1497
2180
|
}
|
|
@@ -1591,7 +2274,6 @@ function nuxtConfigEdit(opts) {
|
|
|
1591
2274
|
for (const dep of [
|
|
1592
2275
|
"@zitadel/api",
|
|
1593
2276
|
"@zitadel/components",
|
|
1594
|
-
"@zitadel/shared-component-styles",
|
|
1595
2277
|
"@zitadel/design-tokens"
|
|
1596
2278
|
]) if (ensureArrayItem(build, "transpile", dep)) changed = true;
|
|
1597
2279
|
const compilerOptions = ensureEditableObject(ensureEditableObject(config, "vue"), "compilerOptions");
|
|
@@ -1606,7 +2288,6 @@ function nuxtConfigEdit(opts) {
|
|
|
1606
2288
|
}
|
|
1607
2289
|
//#endregion
|
|
1608
2290
|
//#region src/lib/orca/patchers/rule/nuxt/templates.ts
|
|
1609
|
-
const MAIN_STYLE = "min-height: 100vh; background: #0f0f11";
|
|
1610
2291
|
/** `app.vue` — renders the page router. Marker in an HTML comment. */
|
|
1611
2292
|
function appVueTemplate() {
|
|
1612
2293
|
return `<!-- ${MANAGED_MARKER} -->
|
|
@@ -1615,42 +2296,94 @@ function appVueTemplate() {
|
|
|
1615
2296
|
</template>
|
|
1616
2297
|
|
|
1617
2298
|
<style>
|
|
2299
|
+
:root {
|
|
2300
|
+
color-scheme: dark;
|
|
2301
|
+
}
|
|
1618
2302
|
body {
|
|
1619
2303
|
margin: 0;
|
|
2304
|
+
background: #0f0f11;
|
|
2305
|
+
color: #f4f4f6;
|
|
1620
2306
|
font-family: sans-serif;
|
|
1621
2307
|
}
|
|
1622
2308
|
</style>
|
|
1623
2309
|
`;
|
|
1624
2310
|
}
|
|
1625
|
-
/**
|
|
1626
|
-
function
|
|
2311
|
+
/** `pages/index.vue` — redirects the app root to `/login`. */
|
|
2312
|
+
function indexPageTemplate() {
|
|
1627
2313
|
return `<script setup lang="ts">
|
|
1628
2314
|
${MANAGED_MARKER}
|
|
1629
|
-
|
|
2315
|
+
await navigateTo("/login", { replace: true });
|
|
2316
|
+
<\/script>
|
|
2317
|
+
`;
|
|
2318
|
+
}
|
|
2319
|
+
/**
|
|
2320
|
+
* A login/register page rendering `<zitadel-login>` inside `<ClientOnly>`.
|
|
2321
|
+
* Business-use-case projects additionally bind the SDK's `businessLocales`
|
|
2322
|
+
* overlay, restoring work-email copy on top of the widget's neutral built-in
|
|
2323
|
+
* dictionaries. A plain `:locales` binding suffices even on the raw custom
|
|
2324
|
+
* element: Vue sets bindings whose key exists on the element as DOM
|
|
2325
|
+
* properties (unlike React 18, which is why the Next template needs a ref).
|
|
2326
|
+
*/
|
|
2327
|
+
function authPage(purpose, ctx) {
|
|
2328
|
+
const business = ctx.useCase === "business";
|
|
2329
|
+
const importNames = business ? "businessLocales, useZitadelProject" : "useZitadelProject";
|
|
2330
|
+
const localesComment = business ? `
|
|
2331
|
+
// Set up for a business audience: businessLocales overlays work-email copy on
|
|
2332
|
+
// the login widget's neutral built-in dictionaries. Remove the :locales
|
|
2333
|
+
// binding to fall back to the neutral wording.` : "";
|
|
2334
|
+
const localesAttr = business ? "\n :locales=\"businessLocales\"" : "";
|
|
2335
|
+
const purposeAttr = purpose === "register" ? "\n purpose=\"register\"" : "";
|
|
2336
|
+
const widget = ctx.posture === "widget";
|
|
2337
|
+
return `<script setup lang="ts">
|
|
2338
|
+
${MANAGED_MARKER}
|
|
2339
|
+
import { ${importNames} } from "@zitadel/sdk-nuxt";${localesComment}
|
|
1630
2340
|
|
|
1631
2341
|
const project = useZitadelProject();
|
|
1632
2342
|
<\/script>
|
|
1633
2343
|
|
|
1634
2344
|
<template>
|
|
1635
|
-
<main style
|
|
2345
|
+
${widget ? "<div style=\"display: flex; justify-content: center; padding: 4rem 1rem\">" : "<main style=\"color-scheme: dark\">"}
|
|
1636
2346
|
<ClientOnly>
|
|
2347
|
+
${widget ? `<!-- variant="widget" embeds the card in this app's own layout.
|
|
2348
|
+
theme="auto" follows the OS light/dark preference
|
|
2349
|
+
(prefers-color-scheme), not the app's own theme — set
|
|
2350
|
+
theme="light" or theme="dark" to match an app that pins its
|
|
2351
|
+
scheme. variant="page" would paint the widget's full-page
|
|
2352
|
+
chrome instead. -->` : `<!-- variant="page" paints the widget's full-page chrome from design
|
|
2353
|
+
tokens; variant="widget" embeds the card inside a layout you own. -->`}
|
|
1637
2354
|
<zitadel-login
|
|
1638
|
-
|
|
2355
|
+
${widget ? "variant=\"widget\"\n theme=\"auto\"" : "variant=\"page\""}
|
|
2356
|
+
:project="project"${localesAttr}${purposeAttr}
|
|
1639
2357
|
post-sign-in-url="/profile"
|
|
1640
2358
|
/>
|
|
1641
2359
|
</ClientOnly>
|
|
1642
|
-
</main>
|
|
2360
|
+
${widget ? "</div>" : "</main>"}
|
|
1643
2361
|
</template>
|
|
1644
2362
|
`;
|
|
1645
2363
|
}
|
|
1646
|
-
function loginPageTemplate() {
|
|
1647
|
-
return authPage("login");
|
|
1648
|
-
}
|
|
1649
|
-
function registerPageTemplate() {
|
|
1650
|
-
return authPage("register");
|
|
1651
|
-
}
|
|
1652
|
-
/** `pages/profile.vue` — the
|
|
1653
|
-
function profilePageTemplate() {
|
|
2364
|
+
function loginPageTemplate(ctx) {
|
|
2365
|
+
return authPage("login", ctx);
|
|
2366
|
+
}
|
|
2367
|
+
function registerPageTemplate(ctx) {
|
|
2368
|
+
return authPage("register", ctx);
|
|
2369
|
+
}
|
|
2370
|
+
/** `pages/profile.vue` — the post-sign-in "signed in as" session card. */
|
|
2371
|
+
function profilePageTemplate(ctx) {
|
|
2372
|
+
const widget = ctx.posture === "widget";
|
|
2373
|
+
const variantAttrs = widget ? "variant=\"widget\"\n theme=\"auto\"" : "variant=\"page\"";
|
|
2374
|
+
const postureComment = widget ? `<!-- variant="widget" embeds the session card in this app's own layout.
|
|
2375
|
+
theme="auto" follows the OS light/dark preference
|
|
2376
|
+
(prefers-color-scheme), not the app's own theme — set
|
|
2377
|
+
theme="light" or theme="dark" to match an app that pins its
|
|
2378
|
+
scheme. variant="page" would paint the card's full-page chrome
|
|
2379
|
+
instead. Your own components (a header, an account menu) read
|
|
2380
|
+
the same session state with the auto-imported useAuth()
|
|
2381
|
+
composable. -->` : `<!-- variant="page" paints the session card's full-page chrome from design
|
|
2382
|
+
tokens; variant="widget" embeds the card inside a layout you own.
|
|
2383
|
+
Your own components (a header, an account menu) read the same session
|
|
2384
|
+
state with the auto-imported useAuth() composable. -->`;
|
|
2385
|
+
const wrapperOpen = widget ? "<div style=\"display: flex; justify-content: center; padding: 4rem 1rem\">" : "<main style=\"color-scheme: dark\">";
|
|
2386
|
+
const wrapperClose = widget ? "</div>" : "</main>";
|
|
1654
2387
|
return `<script setup lang="ts">
|
|
1655
2388
|
${MANAGED_MARKER}
|
|
1656
2389
|
import { useZitadelProject } from "@zitadel/sdk-nuxt";
|
|
@@ -1659,12 +2392,16 @@ const project = useZitadelProject();
|
|
|
1659
2392
|
<\/script>
|
|
1660
2393
|
|
|
1661
2394
|
<template>
|
|
1662
|
-
|
|
1663
|
-
<h1>Signed in (Nuxt)</h1>
|
|
2395
|
+
${wrapperOpen}
|
|
1664
2396
|
<ClientOnly>
|
|
1665
|
-
|
|
2397
|
+
${postureComment}
|
|
2398
|
+
${widget ? `<zitadel-session
|
|
2399
|
+
${variantAttrs}
|
|
2400
|
+
:project="project"
|
|
2401
|
+
post-sign-out-url="/login"
|
|
2402
|
+
/>` : `<zitadel-session variant="page" :project="project" post-sign-out-url="/login" />`}
|
|
1666
2403
|
</ClientOnly>
|
|
1667
|
-
|
|
2404
|
+
${wrapperClose}
|
|
1668
2405
|
</template>
|
|
1669
2406
|
`;
|
|
1670
2407
|
}
|
|
@@ -1710,7 +2447,7 @@ export default defineNuxtPlugin(() => {
|
|
|
1710
2447
|
}
|
|
1711
2448
|
//#endregion
|
|
1712
2449
|
//#region src/lib/orca/patchers/rule/nuxt/index.ts
|
|
1713
|
-
const SDK_DEPENDENCY$
|
|
2450
|
+
const SDK_DEPENDENCY$5 = "@zitadel/sdk-nuxt";
|
|
1714
2451
|
const NUXT_CONFIG_PATHS = configCandidates("nuxt.config");
|
|
1715
2452
|
/**
|
|
1716
2453
|
* Rule-based patcher for a Nuxt app. Like Next.js, Nuxt proxies the backend and
|
|
@@ -1726,25 +2463,29 @@ var NuxtPatcher = class extends AbstractRulePatcher {
|
|
|
1726
2463
|
routeOps(ctx) {
|
|
1727
2464
|
const src = (rel) => join(ctx.framework.appDir, rel);
|
|
1728
2465
|
return [
|
|
1729
|
-
{
|
|
2466
|
+
...ctx.scaffoldedFramework ? [{
|
|
1730
2467
|
kind: "write",
|
|
1731
2468
|
path: src("app.vue"),
|
|
1732
2469
|
contents: appVueTemplate()
|
|
1733
|
-
},
|
|
2470
|
+
}, {
|
|
2471
|
+
kind: "write",
|
|
2472
|
+
path: src("pages/index.vue"),
|
|
2473
|
+
contents: indexPageTemplate()
|
|
2474
|
+
}] : [],
|
|
1734
2475
|
{
|
|
1735
2476
|
kind: "write",
|
|
1736
2477
|
path: src("pages/login.vue"),
|
|
1737
|
-
contents: loginPageTemplate()
|
|
2478
|
+
contents: loginPageTemplate(ctx)
|
|
1738
2479
|
},
|
|
1739
2480
|
{
|
|
1740
2481
|
kind: "write",
|
|
1741
2482
|
path: src("pages/register.vue"),
|
|
1742
|
-
contents: registerPageTemplate()
|
|
2483
|
+
contents: registerPageTemplate(ctx)
|
|
1743
2484
|
},
|
|
1744
2485
|
{
|
|
1745
2486
|
kind: "write",
|
|
1746
2487
|
path: src("pages/profile.vue"),
|
|
1747
|
-
contents: profilePageTemplate()
|
|
2488
|
+
contents: profilePageTemplate(ctx)
|
|
1748
2489
|
},
|
|
1749
2490
|
{
|
|
1750
2491
|
kind: "write",
|
|
@@ -1762,7 +2503,8 @@ var NuxtPatcher = class extends AbstractRulePatcher {
|
|
|
1762
2503
|
edit: nuxtConfigEdit({
|
|
1763
2504
|
projectId: ctx.project.id,
|
|
1764
2505
|
server: ctx.server
|
|
1765
|
-
})
|
|
2506
|
+
}),
|
|
2507
|
+
wiring: "infrastructure"
|
|
1766
2508
|
},
|
|
1767
2509
|
{
|
|
1768
2510
|
kind: "merge-env",
|
|
@@ -1776,15 +2518,17 @@ var NuxtPatcher = class extends AbstractRulePatcher {
|
|
|
1776
2518
|
},
|
|
1777
2519
|
{
|
|
1778
2520
|
kind: "add-dep",
|
|
1779
|
-
name: SDK_DEPENDENCY$
|
|
2521
|
+
name: SDK_DEPENDENCY$5,
|
|
1780
2522
|
version: npmDistTagForCliVersion(ctx.cliVersion)
|
|
1781
|
-
}
|
|
2523
|
+
},
|
|
2524
|
+
devScriptPortOp(ctx.issuer)
|
|
1782
2525
|
];
|
|
1783
2526
|
}
|
|
1784
2527
|
routeFiles(view) {
|
|
1785
2528
|
const src = (rel) => join(view.framework.appDir, rel);
|
|
1786
2529
|
return [
|
|
1787
2530
|
src("app.vue"),
|
|
2531
|
+
src("pages/index.vue"),
|
|
1788
2532
|
src("pages/login.vue"),
|
|
1789
2533
|
src("pages/register.vue"),
|
|
1790
2534
|
src("pages/profile.vue"),
|
|
@@ -1792,11 +2536,19 @@ var NuxtPatcher = class extends AbstractRulePatcher {
|
|
|
1792
2536
|
src("plugins/auth.server.ts")
|
|
1793
2537
|
];
|
|
1794
2538
|
}
|
|
2539
|
+
infrastructureFiles(view) {
|
|
2540
|
+
const src = (rel) => join(view.framework.appDir, rel);
|
|
2541
|
+
return [src("plugins/zitadel-components.client.ts"), src("plugins/auth.server.ts")];
|
|
2542
|
+
}
|
|
2543
|
+
conditionallyScaffoldedFiles(view) {
|
|
2544
|
+
const src = (rel) => join(view.framework.appDir, rel);
|
|
2545
|
+
return [src("app.vue"), src("pages/index.vue")];
|
|
2546
|
+
}
|
|
1795
2547
|
routeDeps(_view) {
|
|
1796
|
-
return [SDK_DEPENDENCY$
|
|
2548
|
+
return [SDK_DEPENDENCY$5];
|
|
1797
2549
|
}
|
|
1798
2550
|
routeConfigEdits(_view) {
|
|
1799
|
-
return ["nuxt.config.*"];
|
|
2551
|
+
return ["nuxt.config.*", "package.json"];
|
|
1800
2552
|
}
|
|
1801
2553
|
summary(_ctx) {
|
|
1802
2554
|
return {
|
|
@@ -1809,27 +2561,65 @@ var NuxtPatcher = class extends AbstractRulePatcher {
|
|
|
1809
2561
|
//#region src/lib/orca/patchers/rule/vite-support.ts
|
|
1810
2562
|
/**
|
|
1811
2563
|
* Shared Vite dev-server proxy merged into the project's Vite config for the SPA
|
|
1812
|
-
* frameworks (React, Vue). It forwards same-origin
|
|
1813
|
-
*
|
|
1814
|
-
*
|
|
2564
|
+
* frameworks (React, Vue, Solid, Svelte, Qwik). It forwards same-origin
|
|
2565
|
+
* `/__nextgen/*` calls to the
|
|
2566
|
+
* backend, strips the prefix, and attaches the project's service-key secret
|
|
2567
|
+
* (read from `ZITADEL_PROJECT_SECRET` in `.env.local`) only to the current
|
|
2568
|
+
* app-plane `POST /sessions/exchange` operation. The secret stays server-side:
|
|
2569
|
+
* Vite only exposes vars with the configured `envPrefix` (default `VITE_`) to
|
|
2570
|
+
* client bundles, so this server-only key never leaks into the browser.
|
|
1815
2571
|
*/
|
|
1816
2572
|
function proxyEntryCode(server) {
|
|
1817
|
-
return
|
|
2573
|
+
return `/* ${PROXY_CREDENTIAL_POLICY_MARKER} */ {
|
|
1818
2574
|
target: ${JSON.stringify(server)},
|
|
1819
2575
|
changeOrigin: false,
|
|
1820
2576
|
rewrite: (path) => path.replace(/^\\${PROXY_PATH}/, "").replace(/^(?!\\/)/, "/"),
|
|
1821
2577
|
configure: (proxy) => {
|
|
1822
|
-
const
|
|
1823
|
-
if (!
|
|
1824
|
-
throw new Error("
|
|
2578
|
+
const secret = loadEnv("development", process.cwd(), "ZITADEL_").ZITADEL_PROJECT_SECRET;
|
|
2579
|
+
if (!secret) {
|
|
2580
|
+
throw new Error("ZITADEL_PROJECT_SECRET is not set; add it to .env.local (zitadel setup writes it).");
|
|
1825
2581
|
}
|
|
1826
|
-
const bearer = \`Bearer
|
|
2582
|
+
const bearer = \`Bearer \${secret}\`;
|
|
1827
2583
|
proxy.on("proxyReq", (proxyReq) => {
|
|
1828
|
-
proxyReq.
|
|
2584
|
+
const pathname = new URL(proxyReq.path, "http://zitadel.local").pathname;
|
|
2585
|
+
if (
|
|
2586
|
+
proxyReq.method === "POST" &&
|
|
2587
|
+
pathname === "/sessions/exchange" &&
|
|
2588
|
+
!proxyReq.getHeader("authorization")
|
|
2589
|
+
) {
|
|
2590
|
+
proxyReq.setHeader("authorization", bearer);
|
|
2591
|
+
}
|
|
1829
2592
|
});
|
|
1830
2593
|
},
|
|
1831
2594
|
}`;
|
|
1832
2595
|
}
|
|
2596
|
+
const LEGACY_PROXY_REQUEST_LISTENER = /(^[\t ]*)proxy\.on\("proxyReq", \(proxyReq\) => \{\r?\n\1 {2}proxyReq\.setHeader\("authorization", bearer\);\r?\n\1\}\);/m;
|
|
2597
|
+
function exchangeOnlyProxyRequestListener(indent) {
|
|
2598
|
+
return `${indent}/* ${PROXY_CREDENTIAL_POLICY_MARKER} */
|
|
2599
|
+
${indent}proxy.on("proxyReq", (proxyReq) => {
|
|
2600
|
+
${indent} const pathname = new URL(proxyReq.path, "http://zitadel.local").pathname;
|
|
2601
|
+
${indent} if (
|
|
2602
|
+
${indent} proxyReq.method === "POST" &&
|
|
2603
|
+
${indent} pathname === "/sessions/exchange" &&
|
|
2604
|
+
${indent} !proxyReq.getHeader("authorization")
|
|
2605
|
+
${indent} ) {
|
|
2606
|
+
${indent} proxyReq.setHeader("authorization", bearer);
|
|
2607
|
+
${indent} }
|
|
2608
|
+
${indent}});`;
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Identifies the exact proxy shape emitted before the exchange-only credential
|
|
2612
|
+
* policy. Setup may replace that generated listener, but must preserve the
|
|
2613
|
+
* surrounding `/__nextgen` proxy and any user changes elsewhere in the entry.
|
|
2614
|
+
*/
|
|
2615
|
+
function isLegacyManagedProxy(source) {
|
|
2616
|
+
if (source.includes("zitadel:proxy:v2")) return false;
|
|
2617
|
+
return [
|
|
2618
|
+
"loadEnv(\"development\", process.cwd(), \"ZITADEL_\").ZITADEL_PROJECT_SECRET",
|
|
2619
|
+
"throw new Error(\"ZITADEL_PROJECT_SECRET is not set; add it to .env.local (zitadel setup writes it).\")",
|
|
2620
|
+
"const bearer = `Bearer ${secret}`;"
|
|
2621
|
+
].every((fingerprint) => source.includes(fingerprint)) && LEGACY_PROXY_REQUEST_LISTENER.test(source);
|
|
2622
|
+
}
|
|
1833
2623
|
/** The imports that the injected proxy entry depends on. */
|
|
1834
2624
|
const PROXY_IMPORTS = [{
|
|
1835
2625
|
from: "vite",
|
|
@@ -1850,9 +2640,12 @@ const PROXY_IMPORTS = [{
|
|
|
1850
2640
|
function viteProxyEdit(devPort, server) {
|
|
1851
2641
|
return (source) => {
|
|
1852
2642
|
const label = "the Vite config (vite.config.*)";
|
|
1853
|
-
|
|
2643
|
+
let workingSource = source;
|
|
2644
|
+
if (workingSource !== void 0 && isLegacyManagedProxy(workingSource)) workingSource = workingSource.replace(LEGACY_PROXY_REQUEST_LISTENER, (_listener, indent) => exchangeOnlyProxyRequestListener(indent));
|
|
2645
|
+
else if (workingSource !== void 0) assertNoUnreviewedProjectSecretProxy(workingSource, label);
|
|
2646
|
+
const mod = parseConfigModule(workingSource, label);
|
|
1854
2647
|
const config = resolveDefaultExportObject(mod, label);
|
|
1855
|
-
let changed =
|
|
2648
|
+
let changed = workingSource !== source;
|
|
1856
2649
|
const serverConfig = ensureEditableObject(config, "server");
|
|
1857
2650
|
if (serverConfig.port === void 0) {
|
|
1858
2651
|
serverConfig.port = devPort;
|
|
@@ -1887,23 +2680,175 @@ function buildViteProxyOp(devPort, server) {
|
|
|
1887
2680
|
return {
|
|
1888
2681
|
kind: "edit",
|
|
1889
2682
|
path: [...VITE_CONFIG_PATHS],
|
|
1890
|
-
edit: viteProxyEdit(devPort, server)
|
|
2683
|
+
edit: viteProxyEdit(devPort, server),
|
|
2684
|
+
wiring: "infrastructure"
|
|
1891
2685
|
};
|
|
1892
2686
|
}
|
|
1893
2687
|
//#endregion
|
|
2688
|
+
//#region src/lib/orca/patchers/rule/qwik/templates.ts
|
|
2689
|
+
/**
|
|
2690
|
+
* The managed `src/app.tsx`: a minimal path-based router that renders the
|
|
2691
|
+
* `@zitadel/sdk-qwik` widgets — login at `/login`, register at `/register`, and
|
|
2692
|
+
* the signed-in session card at `/profile`. The root path redirects to `/login`.
|
|
2693
|
+
* Exports a named `App` (`component$`) to match the create-vite Qwik entry
|
|
2694
|
+
* (`main.tsx` imports `{ App }`). The project id comes from
|
|
2695
|
+
* `VITE_ZITADEL_PROJECT_ID`. No secret reaches the browser: the dev proxy in
|
|
2696
|
+
* `vite.config.*` attaches the project service-key secret (from
|
|
2697
|
+
* `ZITADEL_PROJECT_SECRET`) server-side only to `POST /sessions/exchange`.
|
|
2698
|
+
*
|
|
2699
|
+
* Projects set up with the business use case additionally pass the SDK's
|
|
2700
|
+
* `businessLocales` overlay to the login widgets, restoring work-email copy on
|
|
2701
|
+
* top of the widget's neutral built-in dictionaries. A plain `locales` prop
|
|
2702
|
+
* suffices here: the wrapper component assigns it as a DOM property internally.
|
|
2703
|
+
*/
|
|
2704
|
+
function appTemplate$4(ctx) {
|
|
2705
|
+
const business = ctx.useCase === "business";
|
|
2706
|
+
const importNames = business ? "ZitadelLogin, ZitadelSession, businessLocales, configureZitadel" : "ZitadelLogin, ZitadelSession, configureZitadel";
|
|
2707
|
+
const localesComment = business ? `
|
|
2708
|
+
|
|
2709
|
+
// Set up for a business audience: businessLocales overlays work-email copy on
|
|
2710
|
+
// the login widget's neutral built-in dictionaries. Remove the locales prop to
|
|
2711
|
+
// fall back to the neutral wording.` : "";
|
|
2712
|
+
const localesAttr = business ? " locales={businessLocales}" : "";
|
|
2713
|
+
return `${MANAGED_MARKER}
|
|
2714
|
+
import { component$, useVisibleTask$ } from "@builder.io/qwik";
|
|
2715
|
+
import { ${importNames} } from "@zitadel/sdk-qwik";${localesComment}
|
|
2716
|
+
|
|
2717
|
+
const project = configureZitadel({
|
|
2718
|
+
projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
|
|
2719
|
+
proxyPath: "${PROXY_PATH}",
|
|
2720
|
+
});
|
|
2721
|
+
|
|
2722
|
+
export const App = component$(() => {
|
|
2723
|
+
const path = window.location.pathname;
|
|
2724
|
+
|
|
2725
|
+
useVisibleTask$(() => {
|
|
2726
|
+
if (path === "/") {
|
|
2727
|
+
window.location.replace("/login");
|
|
2728
|
+
}
|
|
2729
|
+
});
|
|
2730
|
+
|
|
2731
|
+
if (path === "/") {
|
|
2732
|
+
return null;
|
|
2733
|
+
}
|
|
2734
|
+
if (path.startsWith("/profile")) {
|
|
2735
|
+
return (
|
|
2736
|
+
<div style={{ position: "fixed", inset: "0", overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
|
|
2737
|
+
<ZitadelSession project={project} postSignOutUrl="/login" />
|
|
2738
|
+
</div>
|
|
2739
|
+
);
|
|
2740
|
+
}
|
|
2741
|
+
if (path.startsWith("/register")) {
|
|
2742
|
+
return (
|
|
2743
|
+
<div style={{ position: "fixed", inset: "0", overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
|
|
2744
|
+
<ZitadelLogin project={project}${localesAttr} purpose="register" postSignInUrl="/profile" />
|
|
2745
|
+
</div>
|
|
2746
|
+
);
|
|
2747
|
+
}
|
|
2748
|
+
return (
|
|
2749
|
+
<div style={{ position: "fixed", inset: "0", overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
|
|
2750
|
+
<ZitadelLogin project={project}${localesAttr} purpose="login" postSignInUrl="/profile" />
|
|
2751
|
+
</div>
|
|
2752
|
+
);
|
|
2753
|
+
});
|
|
2754
|
+
`;
|
|
2755
|
+
}
|
|
2756
|
+
//#endregion
|
|
2757
|
+
//#region src/lib/orca/patchers/rule/qwik/index.ts
|
|
2758
|
+
const SDK_DEPENDENCY$4 = "@zitadel/sdk-qwik";
|
|
2759
|
+
/**
|
|
2760
|
+
* Rule-based patcher for a Vite + Qwik single-page app. Inherits the shared
|
|
2761
|
+
* `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
|
|
2762
|
+
* managed `src/app.tsx` auth entry, a non-destructive `vite.config.*` merge
|
|
2763
|
+
* that adds the `/__nextgen` dev proxy (attaching the project secret from
|
|
2764
|
+
* `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
|
|
2765
|
+
* project id, and the SDK dep.
|
|
2766
|
+
*
|
|
2767
|
+
* The create-vite Qwik template uses a lowercase `src/app.tsx` exporting a named
|
|
2768
|
+
* `App` (mounted by `main.tsx`), so this patcher writes that exact entry. Unlike
|
|
2769
|
+
* Next.js — whose middleware runs the proxy server-side — a SPA has no server,
|
|
2770
|
+
* so the dev proxy provides the same-origin `/__nextgen` path locally. In
|
|
2771
|
+
* production that path comes from a platform rewrite or minimal worker
|
|
2772
|
+
* (ADR 036); CLI scaffolding for it is tracked in issue #560.
|
|
2773
|
+
*/
|
|
2774
|
+
var QwikPatcher = class extends AbstractRulePatcher {
|
|
2775
|
+
canPatch(framework) {
|
|
2776
|
+
return framework === "qwik";
|
|
2777
|
+
}
|
|
2778
|
+
viteProxyOp(devPort, server) {
|
|
2779
|
+
return buildViteProxyOp(devPort, server);
|
|
2780
|
+
}
|
|
2781
|
+
routeOps(ctx) {
|
|
2782
|
+
return [
|
|
2783
|
+
{
|
|
2784
|
+
kind: "write",
|
|
2785
|
+
path: "src/app.tsx",
|
|
2786
|
+
contents: appTemplate$4(ctx)
|
|
2787
|
+
},
|
|
2788
|
+
this.viteProxyOp(ctx.framework.devPort, ctx.server),
|
|
2789
|
+
{
|
|
2790
|
+
kind: "merge-env",
|
|
2791
|
+
path: ".env.example",
|
|
2792
|
+
entries: { VITE_ZITADEL_PROJECT_ID: "" }
|
|
2793
|
+
},
|
|
2794
|
+
{
|
|
2795
|
+
kind: "merge-env",
|
|
2796
|
+
path: ".env.local",
|
|
2797
|
+
entries: { VITE_ZITADEL_PROJECT_ID: ctx.project.id }
|
|
2798
|
+
},
|
|
2799
|
+
{
|
|
2800
|
+
kind: "add-dep",
|
|
2801
|
+
name: SDK_DEPENDENCY$4,
|
|
2802
|
+
version: npmDistTagForCliVersion(ctx.cliVersion)
|
|
2803
|
+
}
|
|
2804
|
+
];
|
|
2805
|
+
}
|
|
2806
|
+
routeFiles(_view) {
|
|
2807
|
+
return ["src/app.tsx"];
|
|
2808
|
+
}
|
|
2809
|
+
routeDeps(_view) {
|
|
2810
|
+
return [SDK_DEPENDENCY$4];
|
|
2811
|
+
}
|
|
2812
|
+
routeConfigEdits(_view) {
|
|
2813
|
+
return ["vite.config.*"];
|
|
2814
|
+
}
|
|
2815
|
+
summary(_ctx) {
|
|
2816
|
+
return {
|
|
2817
|
+
title: "Qwik (Vite) integration",
|
|
2818
|
+
detail: "Wrote src/app.tsx auth entry and merged the /__nextgen dev proxy into vite.config.*."
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
};
|
|
2822
|
+
//#endregion
|
|
1894
2823
|
//#region src/lib/orca/patchers/rule/react/templates.ts
|
|
1895
2824
|
/**
|
|
1896
2825
|
* The managed `src/App.tsx`: a minimal path-based router that renders the
|
|
1897
|
-
* `@zitadel/sdk-react` widgets — login at `/login
|
|
1898
|
-
*
|
|
1899
|
-
* `VITE_ZITADEL_PROJECT_ID` (Vite only exposes
|
|
1900
|
-
* client). No secret reaches the browser: the dev
|
|
1901
|
-
*
|
|
1902
|
-
* server-side
|
|
1903
|
-
|
|
1904
|
-
|
|
2826
|
+
* `@zitadel/sdk-react` widgets — login at `/login`, register at `/register`, and
|
|
2827
|
+
* the signed-in session card at `/profile`. The root path redirects to `/login`.
|
|
2828
|
+
* The project id comes from `VITE_ZITADEL_PROJECT_ID` (Vite only exposes
|
|
2829
|
+
* `VITE_`-prefixed env to the client). No secret reaches the browser: the dev
|
|
2830
|
+
* proxy in `vite.config.*` attaches the project service-key secret (from
|
|
2831
|
+
* `ZITADEL_PROJECT_SECRET`) server-side only to `POST /sessions/exchange`.
|
|
2832
|
+
*
|
|
2833
|
+
* Projects set up with the business use case additionally pass the SDK's
|
|
2834
|
+
* `businessLocales` overlay to the login widgets, restoring work-email copy on
|
|
2835
|
+
* top of the widget's neutral built-in dictionaries. A plain `locales` prop
|
|
2836
|
+
* suffices here: the wrapper component assigns it as a DOM property internally,
|
|
2837
|
+
* so the React-18 attribute-decay caveat of the raw custom element does not
|
|
2838
|
+
* apply.
|
|
2839
|
+
*/
|
|
2840
|
+
function appTemplate$3(ctx) {
|
|
2841
|
+
const business = ctx.useCase === "business";
|
|
2842
|
+
const importNames = business ? "ZitadelLogin, ZitadelSession, businessLocales, configureZitadel" : "ZitadelLogin, ZitadelSession, configureZitadel";
|
|
2843
|
+
const localesComment = business ? `
|
|
2844
|
+
|
|
2845
|
+
// Set up for a business audience: businessLocales overlays work-email copy on
|
|
2846
|
+
// the login widget's neutral built-in dictionaries. Remove the locales prop to
|
|
2847
|
+
// fall back to the neutral wording.` : "";
|
|
2848
|
+
const localesAttr = business ? " locales={businessLocales}" : "";
|
|
1905
2849
|
return `${MANAGED_MARKER}
|
|
1906
|
-
import {
|
|
2850
|
+
import { useEffect } from "react";
|
|
2851
|
+
import { ${importNames} } from "@zitadel/sdk-react";${localesComment}
|
|
1907
2852
|
|
|
1908
2853
|
const project = configureZitadel({
|
|
1909
2854
|
projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
|
|
@@ -1913,30 +2858,53 @@ const project = configureZitadel({
|
|
|
1913
2858
|
export default function App() {
|
|
1914
2859
|
const path = window.location.pathname;
|
|
1915
2860
|
|
|
2861
|
+
useEffect(() => {
|
|
2862
|
+
if (path === "/") {
|
|
2863
|
+
window.location.replace("/login");
|
|
2864
|
+
}
|
|
2865
|
+
}, [path]);
|
|
2866
|
+
|
|
2867
|
+
if (path === "/") {
|
|
2868
|
+
return null;
|
|
2869
|
+
}
|
|
1916
2870
|
if (path.startsWith("/profile")) {
|
|
1917
|
-
return
|
|
2871
|
+
return (
|
|
2872
|
+
<div style={{ position: "fixed", inset: 0, overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
|
|
2873
|
+
<ZitadelSession project={project} postSignOutUrl="/login" />
|
|
2874
|
+
</div>
|
|
2875
|
+
);
|
|
1918
2876
|
}
|
|
1919
2877
|
if (path.startsWith("/register")) {
|
|
1920
|
-
return
|
|
2878
|
+
return (
|
|
2879
|
+
<div style={{ position: "fixed", inset: 0, overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
|
|
2880
|
+
<ZitadelLogin project={project}${localesAttr} purpose="register" postSignInUrl="/profile" />
|
|
2881
|
+
</div>
|
|
2882
|
+
);
|
|
1921
2883
|
}
|
|
1922
|
-
return
|
|
2884
|
+
return (
|
|
2885
|
+
<div style={{ position: "fixed", inset: 0, overflow: "auto", background: "#0f0f11", colorScheme: "dark" }}>
|
|
2886
|
+
<ZitadelLogin project={project}${localesAttr} purpose="login" postSignInUrl="/profile" />
|
|
2887
|
+
</div>
|
|
2888
|
+
);
|
|
1923
2889
|
}
|
|
1924
2890
|
`;
|
|
1925
2891
|
}
|
|
1926
2892
|
//#endregion
|
|
1927
2893
|
//#region src/lib/orca/patchers/rule/react/index.ts
|
|
1928
|
-
const SDK_DEPENDENCY$
|
|
2894
|
+
const SDK_DEPENDENCY$3 = "@zitadel/sdk-react";
|
|
1929
2895
|
/**
|
|
1930
2896
|
* Rule-based patcher for a Vite + React single-page app. Inherits the shared
|
|
1931
2897
|
* `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
|
|
1932
2898
|
* managed `src/App.tsx` auth entry, a non-destructive `vite.config.*` merge
|
|
1933
|
-
* that adds the `/__nextgen` dev proxy (attaching the
|
|
1934
|
-
*
|
|
2899
|
+
* that adds the `/__nextgen` dev proxy (attaching the project secret from
|
|
2900
|
+
* `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
|
|
1935
2901
|
* project id, and the SDK dep.
|
|
1936
2902
|
*
|
|
1937
2903
|
* Unlike Next.js — whose middleware runs the proxy and token exchange
|
|
1938
|
-
* server-side — a SPA has no server, so the dev proxy
|
|
1939
|
-
*
|
|
2904
|
+
* server-side — a SPA has no server, so the dev proxy provides the same-origin
|
|
2905
|
+
* `/__nextgen` path locally. In production that path comes from a platform
|
|
2906
|
+
* rewrite or minimal worker (ADR 036); CLI scaffolding for it is tracked in
|
|
2907
|
+
* issue #560.
|
|
1940
2908
|
*/
|
|
1941
2909
|
var ReactPatcher = class extends AbstractRulePatcher {
|
|
1942
2910
|
canPatch(framework) {
|
|
@@ -1950,7 +2918,7 @@ var ReactPatcher = class extends AbstractRulePatcher {
|
|
|
1950
2918
|
{
|
|
1951
2919
|
kind: "write",
|
|
1952
2920
|
path: "src/App.tsx",
|
|
1953
|
-
contents: appTemplate$
|
|
2921
|
+
contents: appTemplate$3(ctx)
|
|
1954
2922
|
},
|
|
1955
2923
|
this.viteProxyOp(ctx.framework.devPort, ctx.server),
|
|
1956
2924
|
{
|
|
@@ -1965,7 +2933,7 @@ var ReactPatcher = class extends AbstractRulePatcher {
|
|
|
1965
2933
|
},
|
|
1966
2934
|
{
|
|
1967
2935
|
kind: "add-dep",
|
|
1968
|
-
name: SDK_DEPENDENCY$
|
|
2936
|
+
name: SDK_DEPENDENCY$3,
|
|
1969
2937
|
version: npmDistTagForCliVersion(ctx.cliVersion)
|
|
1970
2938
|
}
|
|
1971
2939
|
];
|
|
@@ -1974,7 +2942,7 @@ var ReactPatcher = class extends AbstractRulePatcher {
|
|
|
1974
2942
|
return ["src/App.tsx"];
|
|
1975
2943
|
}
|
|
1976
2944
|
routeDeps(_view) {
|
|
1977
|
-
return [SDK_DEPENDENCY$
|
|
2945
|
+
return [SDK_DEPENDENCY$3];
|
|
1978
2946
|
}
|
|
1979
2947
|
routeConfigEdits(_view) {
|
|
1980
2948
|
return ["vite.config.*"];
|
|
@@ -1987,20 +2955,293 @@ var ReactPatcher = class extends AbstractRulePatcher {
|
|
|
1987
2955
|
}
|
|
1988
2956
|
};
|
|
1989
2957
|
//#endregion
|
|
2958
|
+
//#region src/lib/orca/patchers/rule/solid/templates.ts
|
|
2959
|
+
/**
|
|
2960
|
+
* The managed `src/App.tsx`: a minimal path-based router that renders the
|
|
2961
|
+
* `@zitadel/sdk-solid` widgets — login at `/login`, register at `/register`, and
|
|
2962
|
+
* the signed-in session card at `/profile`. The root path redirects to `/login`.
|
|
2963
|
+
* The project id comes from `VITE_ZITADEL_PROJECT_ID` (Vite only exposes
|
|
2964
|
+
* `VITE_`-prefixed env to the client). No secret reaches the browser: the dev
|
|
2965
|
+
* proxy in `vite.config.*` attaches the project service-key secret (from
|
|
2966
|
+
* `ZITADEL_PROJECT_SECRET`) server-side only to `POST /sessions/exchange`.
|
|
2967
|
+
*
|
|
2968
|
+
* Projects set up with the business use case additionally pass the SDK's
|
|
2969
|
+
* `businessLocales` overlay to the login widgets, restoring work-email copy on
|
|
2970
|
+
* top of the widget's neutral built-in dictionaries. A plain `locales` prop
|
|
2971
|
+
* suffices here: the wrapper component assigns it as a DOM property internally.
|
|
2972
|
+
*/
|
|
2973
|
+
function appTemplate$2(ctx) {
|
|
2974
|
+
const business = ctx.useCase === "business";
|
|
2975
|
+
const importNames = business ? "ZitadelLogin, ZitadelSession, businessLocales, configureZitadel" : "ZitadelLogin, ZitadelSession, configureZitadel";
|
|
2976
|
+
const localesComment = business ? `
|
|
2977
|
+
|
|
2978
|
+
// Set up for a business audience: businessLocales overlays work-email copy on
|
|
2979
|
+
// the login widget's neutral built-in dictionaries. Remove the locales prop to
|
|
2980
|
+
// fall back to the neutral wording.` : "";
|
|
2981
|
+
const localesAttr = business ? " locales={businessLocales}" : "";
|
|
2982
|
+
return `${MANAGED_MARKER}
|
|
2983
|
+
import { onMount } from "solid-js";
|
|
2984
|
+
import { ${importNames} } from "@zitadel/sdk-solid";${localesComment}
|
|
2985
|
+
|
|
2986
|
+
const project = configureZitadel({
|
|
2987
|
+
projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
|
|
2988
|
+
proxyPath: "${PROXY_PATH}",
|
|
2989
|
+
});
|
|
2990
|
+
|
|
2991
|
+
export default function App() {
|
|
2992
|
+
const path = window.location.pathname;
|
|
2993
|
+
|
|
2994
|
+
onMount(() => {
|
|
2995
|
+
if (path === "/") {
|
|
2996
|
+
window.location.replace("/login");
|
|
2997
|
+
}
|
|
2998
|
+
});
|
|
2999
|
+
|
|
3000
|
+
if (path === "/") {
|
|
3001
|
+
return null;
|
|
3002
|
+
}
|
|
3003
|
+
if (path.startsWith("/profile")) {
|
|
3004
|
+
return (
|
|
3005
|
+
<div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
|
|
3006
|
+
<ZitadelSession project={project} postSignOutUrl="/login" />
|
|
3007
|
+
</div>
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
if (path.startsWith("/register")) {
|
|
3011
|
+
return (
|
|
3012
|
+
<div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
|
|
3013
|
+
<ZitadelLogin project={project}${localesAttr} purpose="register" postSignInUrl="/profile" />
|
|
3014
|
+
</div>
|
|
3015
|
+
);
|
|
3016
|
+
}
|
|
3017
|
+
return (
|
|
3018
|
+
<div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
|
|
3019
|
+
<ZitadelLogin project={project}${localesAttr} purpose="login" postSignInUrl="/profile" />
|
|
3020
|
+
</div>
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
`;
|
|
3024
|
+
}
|
|
3025
|
+
//#endregion
|
|
3026
|
+
//#region src/lib/orca/patchers/rule/solid/index.ts
|
|
3027
|
+
const SDK_DEPENDENCY$2 = "@zitadel/sdk-solid";
|
|
3028
|
+
/**
|
|
3029
|
+
* Rule-based patcher for a Vite + Solid single-page app. Inherits the shared
|
|
3030
|
+
* `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
|
|
3031
|
+
* managed `src/App.tsx` auth entry, a non-destructive `vite.config.*` merge
|
|
3032
|
+
* that adds the `/__nextgen` dev proxy (attaching the project secret from
|
|
3033
|
+
* `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
|
|
3034
|
+
* project id, and the SDK dep.
|
|
3035
|
+
*
|
|
3036
|
+
* Unlike Next.js — whose middleware runs the proxy and token exchange
|
|
3037
|
+
* server-side — a SPA has no server, so the dev proxy provides the same-origin
|
|
3038
|
+
* `/__nextgen` path locally. In production that path comes from a platform
|
|
3039
|
+
* rewrite or minimal worker (ADR 036); CLI scaffolding for it is tracked in
|
|
3040
|
+
* issue #560.
|
|
3041
|
+
*/
|
|
3042
|
+
var SolidPatcher = class extends AbstractRulePatcher {
|
|
3043
|
+
canPatch(framework) {
|
|
3044
|
+
return framework === "solid";
|
|
3045
|
+
}
|
|
3046
|
+
viteProxyOp(devPort, server) {
|
|
3047
|
+
return buildViteProxyOp(devPort, server);
|
|
3048
|
+
}
|
|
3049
|
+
routeOps(ctx) {
|
|
3050
|
+
return [
|
|
3051
|
+
{
|
|
3052
|
+
kind: "write",
|
|
3053
|
+
path: "src/App.tsx",
|
|
3054
|
+
contents: appTemplate$2(ctx)
|
|
3055
|
+
},
|
|
3056
|
+
this.viteProxyOp(ctx.framework.devPort, ctx.server),
|
|
3057
|
+
{
|
|
3058
|
+
kind: "merge-env",
|
|
3059
|
+
path: ".env.example",
|
|
3060
|
+
entries: { VITE_ZITADEL_PROJECT_ID: "" }
|
|
3061
|
+
},
|
|
3062
|
+
{
|
|
3063
|
+
kind: "merge-env",
|
|
3064
|
+
path: ".env.local",
|
|
3065
|
+
entries: { VITE_ZITADEL_PROJECT_ID: ctx.project.id }
|
|
3066
|
+
},
|
|
3067
|
+
{
|
|
3068
|
+
kind: "add-dep",
|
|
3069
|
+
name: SDK_DEPENDENCY$2,
|
|
3070
|
+
version: npmDistTagForCliVersion(ctx.cliVersion)
|
|
3071
|
+
}
|
|
3072
|
+
];
|
|
3073
|
+
}
|
|
3074
|
+
routeFiles(_view) {
|
|
3075
|
+
return ["src/App.tsx"];
|
|
3076
|
+
}
|
|
3077
|
+
routeDeps(_view) {
|
|
3078
|
+
return [SDK_DEPENDENCY$2];
|
|
3079
|
+
}
|
|
3080
|
+
routeConfigEdits(_view) {
|
|
3081
|
+
return ["vite.config.*"];
|
|
3082
|
+
}
|
|
3083
|
+
summary(_ctx) {
|
|
3084
|
+
return {
|
|
3085
|
+
title: "Solid (Vite) integration",
|
|
3086
|
+
detail: "Wrote src/App.tsx auth entry and merged the /__nextgen dev proxy into vite.config.*."
|
|
3087
|
+
};
|
|
3088
|
+
}
|
|
3089
|
+
};
|
|
3090
|
+
//#endregion
|
|
3091
|
+
//#region src/lib/orca/patchers/rule/svelte/templates.ts
|
|
3092
|
+
/**
|
|
3093
|
+
* The managed `src/App.svelte`: a minimal path-based router that renders the
|
|
3094
|
+
* `@zitadel/sdk-svelte` widgets — login at `/login`, register at `/register`, and
|
|
3095
|
+
* the signed-in session card at `/profile`. The root path redirects to `/login`.
|
|
3096
|
+
* The managed marker lives in the `<script lang="ts">` block (a JS comment) so
|
|
3097
|
+
* eject/doctor stay marker-aware. The project id comes from
|
|
3098
|
+
* `VITE_ZITADEL_PROJECT_ID`. No secret reaches the browser: the dev proxy in
|
|
3099
|
+
* `vite.config.*` attaches the project service-key secret (from
|
|
3100
|
+
* `ZITADEL_PROJECT_SECRET`) server-side only to `POST /sessions/exchange`.
|
|
3101
|
+
*
|
|
3102
|
+
* Projects set up with the business use case additionally pass the SDK's
|
|
3103
|
+
* `businessLocales` overlay to the login widgets, restoring work-email copy on
|
|
3104
|
+
* top of the widget's neutral built-in dictionaries. A plain `locales` prop
|
|
3105
|
+
* suffices here: the wrapper component assigns it as a DOM property internally.
|
|
3106
|
+
*/
|
|
3107
|
+
function appTemplate$1(ctx) {
|
|
3108
|
+
const business = ctx.useCase === "business";
|
|
3109
|
+
const importNames = business ? "ZitadelLogin, ZitadelSession, businessLocales, configureZitadel" : "ZitadelLogin, ZitadelSession, configureZitadel";
|
|
3110
|
+
const localesComment = business ? `
|
|
3111
|
+
|
|
3112
|
+
// Set up for a business audience: businessLocales overlays work-email copy on
|
|
3113
|
+
// the login widget's neutral built-in dictionaries. Remove the locales prop to
|
|
3114
|
+
// fall back to the neutral wording.` : "";
|
|
3115
|
+
const localesAttr = business ? " locales={businessLocales}" : "";
|
|
3116
|
+
return `<script lang="ts">
|
|
3117
|
+
${MANAGED_MARKER}
|
|
3118
|
+
import { onMount } from "svelte";
|
|
3119
|
+
import { ${importNames} } from "@zitadel/sdk-svelte";${localesComment}
|
|
3120
|
+
|
|
3121
|
+
const project = configureZitadel({
|
|
3122
|
+
projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
|
|
3123
|
+
proxyPath: "${PROXY_PATH}",
|
|
3124
|
+
});
|
|
3125
|
+
|
|
3126
|
+
const path = window.location.pathname;
|
|
3127
|
+
|
|
3128
|
+
onMount(() => {
|
|
3129
|
+
if (path === "/") {
|
|
3130
|
+
window.location.replace("/login");
|
|
3131
|
+
}
|
|
3132
|
+
});
|
|
3133
|
+
<\/script>
|
|
3134
|
+
|
|
3135
|
+
{#if path.startsWith("/profile")}
|
|
3136
|
+
<div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
|
|
3137
|
+
<ZitadelSession {project} postSignOutUrl="/login" />
|
|
3138
|
+
</div>
|
|
3139
|
+
{:else if path.startsWith("/register")}
|
|
3140
|
+
<div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
|
|
3141
|
+
<ZitadelLogin {project}${localesAttr} purpose="register" postSignInUrl="/profile" />
|
|
3142
|
+
</div>
|
|
3143
|
+
{:else if path !== "/"}
|
|
3144
|
+
<div style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
|
|
3145
|
+
<ZitadelLogin {project}${localesAttr} purpose="login" postSignInUrl="/profile" />
|
|
3146
|
+
</div>
|
|
3147
|
+
{/if}
|
|
3148
|
+
`;
|
|
3149
|
+
}
|
|
3150
|
+
//#endregion
|
|
3151
|
+
//#region src/lib/orca/patchers/rule/svelte/index.ts
|
|
3152
|
+
const SDK_DEPENDENCY$1 = "@zitadel/sdk-svelte";
|
|
3153
|
+
/**
|
|
3154
|
+
* Rule-based patcher for a Vite + Svelte single-page app. Inherits the shared
|
|
3155
|
+
* `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
|
|
3156
|
+
* managed `src/App.svelte` auth entry, a non-destructive `vite.config.*` merge
|
|
3157
|
+
* that adds the `/__nextgen` dev proxy (attaching the project secret from
|
|
3158
|
+
* `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
|
|
3159
|
+
* project id, and the SDK dep.
|
|
3160
|
+
*
|
|
3161
|
+
* Unlike Next.js — whose middleware runs the proxy and token exchange
|
|
3162
|
+
* server-side — a SPA has no server, so the dev proxy provides the same-origin
|
|
3163
|
+
* `/__nextgen` path locally. In production that path comes from a platform
|
|
3164
|
+
* rewrite or minimal worker (ADR 036); CLI scaffolding for it is tracked in
|
|
3165
|
+
* issue #560.
|
|
3166
|
+
*/
|
|
3167
|
+
var SveltePatcher = class extends AbstractRulePatcher {
|
|
3168
|
+
canPatch(framework) {
|
|
3169
|
+
return framework === "svelte";
|
|
3170
|
+
}
|
|
3171
|
+
viteProxyOp(devPort, server) {
|
|
3172
|
+
return buildViteProxyOp(devPort, server);
|
|
3173
|
+
}
|
|
3174
|
+
routeOps(ctx) {
|
|
3175
|
+
return [
|
|
3176
|
+
{
|
|
3177
|
+
kind: "write",
|
|
3178
|
+
path: "src/App.svelte",
|
|
3179
|
+
contents: appTemplate$1(ctx)
|
|
3180
|
+
},
|
|
3181
|
+
this.viteProxyOp(ctx.framework.devPort, ctx.server),
|
|
3182
|
+
{
|
|
3183
|
+
kind: "merge-env",
|
|
3184
|
+
path: ".env.example",
|
|
3185
|
+
entries: { VITE_ZITADEL_PROJECT_ID: "" }
|
|
3186
|
+
},
|
|
3187
|
+
{
|
|
3188
|
+
kind: "merge-env",
|
|
3189
|
+
path: ".env.local",
|
|
3190
|
+
entries: { VITE_ZITADEL_PROJECT_ID: ctx.project.id }
|
|
3191
|
+
},
|
|
3192
|
+
{
|
|
3193
|
+
kind: "add-dep",
|
|
3194
|
+
name: SDK_DEPENDENCY$1,
|
|
3195
|
+
version: npmDistTagForCliVersion(ctx.cliVersion)
|
|
3196
|
+
}
|
|
3197
|
+
];
|
|
3198
|
+
}
|
|
3199
|
+
routeFiles(_view) {
|
|
3200
|
+
return ["src/App.svelte"];
|
|
3201
|
+
}
|
|
3202
|
+
routeDeps(_view) {
|
|
3203
|
+
return [SDK_DEPENDENCY$1];
|
|
3204
|
+
}
|
|
3205
|
+
routeConfigEdits(_view) {
|
|
3206
|
+
return ["vite.config.*"];
|
|
3207
|
+
}
|
|
3208
|
+
summary(_ctx) {
|
|
3209
|
+
return {
|
|
3210
|
+
title: "Svelte (Vite) integration",
|
|
3211
|
+
detail: "Wrote src/App.svelte auth entry and merged the /__nextgen dev proxy into vite.config.*."
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
};
|
|
3215
|
+
//#endregion
|
|
1990
3216
|
//#region src/lib/orca/patchers/rule/vue/templates.ts
|
|
1991
3217
|
/**
|
|
1992
3218
|
* The managed `src/App.vue`: a minimal path-based router that renders the
|
|
1993
|
-
* `@zitadel/sdk-vue` widgets — login at `/login
|
|
1994
|
-
*
|
|
1995
|
-
* the `<script setup>` block (a JS comment) so
|
|
1996
|
-
* The project id comes from
|
|
1997
|
-
* browser: the dev proxy in
|
|
1998
|
-
*
|
|
1999
|
-
|
|
2000
|
-
|
|
3219
|
+
* `@zitadel/sdk-vue` widgets — login at `/login`, register at `/register`, and
|
|
3220
|
+
* the signed-in session card at `/profile`. The root path redirects to `/login`.
|
|
3221
|
+
* The managed marker lives in the `<script setup>` block (a JS comment) so
|
|
3222
|
+
* eject/doctor stay marker-aware. The project id comes from
|
|
3223
|
+
* `VITE_ZITADEL_PROJECT_ID`. No secret reaches the browser: the dev proxy in
|
|
3224
|
+
* `vite.config.*` attaches the project service-key secret (from
|
|
3225
|
+
* `ZITADEL_PROJECT_SECRET`) server-side only to `POST /sessions/exchange`.
|
|
3226
|
+
*
|
|
3227
|
+
* Projects set up with the business use case additionally pass the SDK's
|
|
3228
|
+
* `businessLocales` overlay to the login widgets, restoring work-email copy on
|
|
3229
|
+
* top of the widget's neutral built-in dictionaries. A plain `:locales` prop
|
|
3230
|
+
* suffices here: the wrapper component assigns it as a DOM property internally.
|
|
3231
|
+
*/
|
|
3232
|
+
function appTemplate(ctx) {
|
|
3233
|
+
const business = ctx.useCase === "business";
|
|
3234
|
+
const importNames = business ? "ZitadelLogin, ZitadelSession, businessLocales, configureZitadel" : "ZitadelLogin, ZitadelSession, configureZitadel";
|
|
3235
|
+
const localesComment = business ? `
|
|
3236
|
+
|
|
3237
|
+
// Set up for a business audience: businessLocales overlays work-email copy on
|
|
3238
|
+
// the login widget's neutral built-in dictionaries. Remove the :locales prop
|
|
3239
|
+
// to fall back to the neutral wording.` : "";
|
|
3240
|
+
const localesAttr = business ? " :locales=\"businessLocales\"" : "";
|
|
2001
3241
|
return `<script setup lang="ts">
|
|
2002
3242
|
${MANAGED_MARKER}
|
|
2003
|
-
import {
|
|
3243
|
+
import { onMounted } from "vue";
|
|
3244
|
+
import { ${importNames} } from "@zitadel/sdk-vue";${localesComment}
|
|
2004
3245
|
|
|
2005
3246
|
const project = configureZitadel({
|
|
2006
3247
|
projectId: import.meta.env.VITE_ZITADEL_PROJECT_ID,
|
|
@@ -2008,21 +3249,30 @@ const project = configureZitadel({
|
|
|
2008
3249
|
});
|
|
2009
3250
|
|
|
2010
3251
|
const path = window.location.pathname;
|
|
3252
|
+
|
|
3253
|
+
onMounted(() => {
|
|
3254
|
+
if (path === "/") {
|
|
3255
|
+
window.location.replace("/login");
|
|
3256
|
+
}
|
|
3257
|
+
});
|
|
2011
3258
|
<\/script>
|
|
2012
3259
|
|
|
2013
3260
|
<template>
|
|
2014
|
-
<
|
|
3261
|
+
<div
|
|
2015
3262
|
v-if="path.startsWith('/profile')"
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
3263
|
+
style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark"
|
|
3264
|
+
>
|
|
3265
|
+
<ZitadelSession :project="project" postSignOutUrl="/login" />
|
|
3266
|
+
</div>
|
|
3267
|
+
<div
|
|
2020
3268
|
v-else-if="path.startsWith('/register')"
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
postSignInUrl="/profile"
|
|
2024
|
-
|
|
2025
|
-
<
|
|
3269
|
+
style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark"
|
|
3270
|
+
>
|
|
3271
|
+
<ZitadelLogin :project="project"${localesAttr} purpose="register" postSignInUrl="/profile" />
|
|
3272
|
+
</div>
|
|
3273
|
+
<div v-else-if="path !== '/'" style="position:fixed;inset:0;overflow:auto;background:#0f0f11;color-scheme:dark">
|
|
3274
|
+
<ZitadelLogin :project="project"${localesAttr} purpose="login" postSignInUrl="/profile" />
|
|
3275
|
+
</div>
|
|
2026
3276
|
</template>
|
|
2027
3277
|
`;
|
|
2028
3278
|
}
|
|
@@ -2033,8 +3283,8 @@ const SDK_DEPENDENCY = "@zitadel/sdk-vue";
|
|
|
2033
3283
|
* Rule-based patcher for a Vite + Vue single-page app. Inherits the shared
|
|
2034
3284
|
* `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
|
|
2035
3285
|
* managed `src/App.vue` auth entry, a non-destructive `vite.config.*` merge
|
|
2036
|
-
* that adds the `/__nextgen` dev proxy (attaching the
|
|
2037
|
-
*
|
|
3286
|
+
* that adds the `/__nextgen` dev proxy (attaching the project secret from
|
|
3287
|
+
* `ZITADEL_PROJECT_SECRET` only to `POST /sessions/exchange`), the `VITE_`-prefixed
|
|
2038
3288
|
* project id, and the SDK dep.
|
|
2039
3289
|
*/
|
|
2040
3290
|
var VuePatcher = class extends AbstractRulePatcher {
|
|
@@ -2049,7 +3299,7 @@ var VuePatcher = class extends AbstractRulePatcher {
|
|
|
2049
3299
|
{
|
|
2050
3300
|
kind: "write",
|
|
2051
3301
|
path: "src/App.vue",
|
|
2052
|
-
contents: appTemplate()
|
|
3302
|
+
contents: appTemplate(ctx)
|
|
2053
3303
|
},
|
|
2054
3304
|
this.viteProxyOp(ctx.framework.devPort, ctx.server),
|
|
2055
3305
|
{
|
|
@@ -2101,6 +3351,9 @@ const patchers = [
|
|
|
2101
3351
|
new NuxtPatcher(),
|
|
2102
3352
|
new ReactPatcher(),
|
|
2103
3353
|
new VuePatcher(),
|
|
3354
|
+
new SolidPatcher(),
|
|
3355
|
+
new SveltePatcher(),
|
|
3356
|
+
new QwikPatcher(),
|
|
2104
3357
|
new AngularPatcher()
|
|
2105
3358
|
];
|
|
2106
3359
|
//#endregion
|
|
@@ -2262,6 +3515,36 @@ var NuxtScaffolder = class extends AbstractCLIScaffolder {
|
|
|
2262
3515
|
}
|
|
2263
3516
|
};
|
|
2264
3517
|
//#endregion
|
|
3518
|
+
//#region src/lib/orca/scaffolders/qwik.ts
|
|
3519
|
+
/**
|
|
3520
|
+
* Scaffolds a new Vite + Qwik (TypeScript) single-page app with `create-vite`,
|
|
3521
|
+
* then removes the starter `app.tsx`/`app.css` demo so the patcher can write the
|
|
3522
|
+
* managed `src/app.tsx` without colliding with boilerplate. The create-vite Qwik
|
|
3523
|
+
* template uses a lowercase `app.tsx` (named `App` export, mounted by `main.tsx`)
|
|
3524
|
+
* — the patched file keeps that same entry.
|
|
3525
|
+
*/
|
|
3526
|
+
var QwikScaffolder = class extends AbstractCLIScaffolder {
|
|
3527
|
+
displayName = "Qwik (Vite)";
|
|
3528
|
+
supportedFrameworks = ["qwik"];
|
|
3529
|
+
async scaffold(cwd, _framework) {
|
|
3530
|
+
this.runCommand("npm", [
|
|
3531
|
+
"create",
|
|
3532
|
+
"vite@latest",
|
|
3533
|
+
".",
|
|
3534
|
+
"--",
|
|
3535
|
+
"--template",
|
|
3536
|
+
"qwik-ts"
|
|
3537
|
+
], cwd);
|
|
3538
|
+
await rm(join(cwd, "src/app.tsx"), { force: true });
|
|
3539
|
+
await rm(join(cwd, "src/app.css"), { force: true });
|
|
3540
|
+
this.runCommand("npm", [
|
|
3541
|
+
"pkg",
|
|
3542
|
+
"set",
|
|
3543
|
+
"devDependencies.vite=^7.3.5"
|
|
3544
|
+
], cwd);
|
|
3545
|
+
}
|
|
3546
|
+
};
|
|
3547
|
+
//#endregion
|
|
2265
3548
|
//#region src/lib/orca/scaffolders/react.ts
|
|
2266
3549
|
/**
|
|
2267
3550
|
* Scaffolds a new Vite + React (TypeScript) single-page app with `create-vite`,
|
|
@@ -2286,6 +3569,55 @@ var ReactScaffolder = class extends AbstractCLIScaffolder {
|
|
|
2286
3569
|
}
|
|
2287
3570
|
};
|
|
2288
3571
|
//#endregion
|
|
3572
|
+
//#region src/lib/orca/scaffolders/solid.ts
|
|
3573
|
+
/**
|
|
3574
|
+
* Scaffolds a new Vite + Solid (TypeScript) single-page app with `create-vite`,
|
|
3575
|
+
* then removes the starter `App.tsx`/`App.css` demo so the patcher can write the
|
|
3576
|
+
* managed `src/App.tsx` without colliding with boilerplate. `index.css` and
|
|
3577
|
+
* `index.tsx` are left in place — the patched `App.tsx` keeps the same entry.
|
|
3578
|
+
*/
|
|
3579
|
+
var SolidScaffolder = class extends AbstractCLIScaffolder {
|
|
3580
|
+
displayName = "Solid (Vite)";
|
|
3581
|
+
supportedFrameworks = ["solid"];
|
|
3582
|
+
async scaffold(cwd, _framework) {
|
|
3583
|
+
this.runCommand("npm", [
|
|
3584
|
+
"create",
|
|
3585
|
+
"vite@latest",
|
|
3586
|
+
".",
|
|
3587
|
+
"--",
|
|
3588
|
+
"--template",
|
|
3589
|
+
"solid-ts"
|
|
3590
|
+
], cwd);
|
|
3591
|
+
await rm(join(cwd, "src/App.tsx"), { force: true });
|
|
3592
|
+
await rm(join(cwd, "src/App.css"), { force: true });
|
|
3593
|
+
}
|
|
3594
|
+
};
|
|
3595
|
+
//#endregion
|
|
3596
|
+
//#region src/lib/orca/scaffolders/svelte.ts
|
|
3597
|
+
/**
|
|
3598
|
+
* Scaffolds a new Vite + Svelte (TypeScript) single-page app with `create-vite`,
|
|
3599
|
+
* then removes the starter `App.svelte`/`lib/Counter.svelte` demo so the patcher
|
|
3600
|
+
* can write the managed `src/App.svelte` without colliding with boilerplate.
|
|
3601
|
+
* `app.css` and `main.ts` are left in place — the patched `App.svelte` keeps the
|
|
3602
|
+
* same entry.
|
|
3603
|
+
*/
|
|
3604
|
+
var SvelteScaffolder = class extends AbstractCLIScaffolder {
|
|
3605
|
+
displayName = "Svelte (Vite)";
|
|
3606
|
+
supportedFrameworks = ["svelte"];
|
|
3607
|
+
async scaffold(cwd, _framework) {
|
|
3608
|
+
this.runCommand("npm", [
|
|
3609
|
+
"create",
|
|
3610
|
+
"vite@latest",
|
|
3611
|
+
".",
|
|
3612
|
+
"--",
|
|
3613
|
+
"--template",
|
|
3614
|
+
"svelte-ts"
|
|
3615
|
+
], cwd);
|
|
3616
|
+
await rm(join(cwd, "src/App.svelte"), { force: true });
|
|
3617
|
+
await rm(join(cwd, "src/lib/Counter.svelte"), { force: true });
|
|
3618
|
+
}
|
|
3619
|
+
};
|
|
3620
|
+
//#endregion
|
|
2289
3621
|
//#region src/lib/orca/scaffolders/vue.ts
|
|
2290
3622
|
/**
|
|
2291
3623
|
* Scaffolds a new Vite + Vue (TypeScript) single-page app with `create-vite`,
|
|
@@ -2320,6 +3652,9 @@ const scaffolders = [
|
|
|
2320
3652
|
new NuxtScaffolder(),
|
|
2321
3653
|
new ReactScaffolder(),
|
|
2322
3654
|
new VueScaffolder(),
|
|
3655
|
+
new SolidScaffolder(),
|
|
3656
|
+
new SvelteScaffolder(),
|
|
3657
|
+
new QwikScaffolder(),
|
|
2323
3658
|
new AngularScaffolder()
|
|
2324
3659
|
];
|
|
2325
3660
|
//#endregion
|
|
@@ -2382,6 +3717,7 @@ var Orca = class {
|
|
|
2382
3717
|
hint: target.reason ?? "Run setup in an empty directory, or run setup from an existing supported app project.",
|
|
2383
3718
|
details: { entries: target.entries }
|
|
2384
3719
|
});
|
|
3720
|
+
assertNpmSafeScaffoldDirectoryName(cwd);
|
|
2385
3721
|
const stash = await stashFreshScaffoldArtifacts(cwd, target);
|
|
2386
3722
|
try {
|
|
2387
3723
|
await this.scaffolderFor(framework).scaffold(cwd, framework);
|
|
@@ -2419,6 +3755,30 @@ var Orca = class {
|
|
|
2419
3755
|
return this.detectors.map((detector) => detector.framework);
|
|
2420
3756
|
}
|
|
2421
3757
|
};
|
|
3758
|
+
function assertNpmSafeScaffoldDirectoryName(cwd) {
|
|
3759
|
+
const name = basename(cwd);
|
|
3760
|
+
const errors = npmPackageNameErrors(name);
|
|
3761
|
+
if (errors.length === 0) return;
|
|
3762
|
+
throw new ZitadelError("E_VALIDATION", `Fresh app directory name "${name}" is not npm-package-safe`, {
|
|
3763
|
+
hint: "Rename the directory to a lowercase npm-package-safe name, for example `my-zitadel-app`, then rerun setup.",
|
|
3764
|
+
details: {
|
|
3765
|
+
cwd,
|
|
3766
|
+
name,
|
|
3767
|
+
validation_errors: errors
|
|
3768
|
+
}
|
|
3769
|
+
});
|
|
3770
|
+
}
|
|
3771
|
+
function npmPackageNameErrors(name) {
|
|
3772
|
+
const errors = [];
|
|
3773
|
+
if (name.length === 0) errors.push("name is empty");
|
|
3774
|
+
if (name.length > 214) errors.push("name is longer than 214 characters");
|
|
3775
|
+
if (name !== name.trim()) errors.push("name contains leading or trailing whitespace");
|
|
3776
|
+
if (/[A-Z]/.test(name)) errors.push("name can no longer contain capital letters");
|
|
3777
|
+
if (name.startsWith(".") || name.startsWith("_")) errors.push("name cannot start with a period or underscore");
|
|
3778
|
+
if (!/^[a-z0-9][a-z0-9._~-]*$/.test(name)) errors.push("name may only contain lowercase letters, numbers, dots, underscores, tildes, and hyphens");
|
|
3779
|
+
if (name === "node_modules" || name === "favicon.ico") errors.push(`name "${name}" is reserved`);
|
|
3780
|
+
return [...new Set(errors)];
|
|
3781
|
+
}
|
|
2422
3782
|
/** {@link Orca} wired with the default detector, scaffolder, and patcher registries. */
|
|
2423
3783
|
function createOrca() {
|
|
2424
3784
|
return new Orca(detectors, scaffolders, patchers);
|
|
@@ -2551,6 +3911,6 @@ function isErrno(error, code) {
|
|
|
2551
3911
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
2552
3912
|
}
|
|
2553
3913
|
//#endregion
|
|
2554
|
-
export { RENDERER_IDS as n,
|
|
3914
|
+
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 };
|
|
2555
3915
|
|
|
2556
|
-
//# sourceMappingURL=orca-
|
|
3916
|
+
//# sourceMappingURL=orca-BzQIQqzt.mjs.map
|