@zitadel/cli 0.1.0-alpha.3 → 0.1.0-alpha.5

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.
@@ -1,1120 +0,0 @@
1
- import { C as isObject, E as ZitadelError, T as stableStringify, n as DEFAULT_SERVER, w as parseJsonObject, x as MANAGED_MARKER } from "./oclif-D-R4dfQr.mjs";
2
- import { basename, dirname, join } from "node:path";
3
- import { chmod, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
4
- import { spawnSync } from "node:child_process";
5
- //#region src/lib/orca/detectors/package-json.ts
6
- /**
7
- * Reads and parses the `package.json` at `cwd`. Rejects if the file is absent
8
- * or malformed; callers that treat those as "not a project" are expected to
9
- * catch and fall back rather than have detection swallow the error here.
10
- */
11
- async function readPackageJson(cwd) {
12
- const contents = await readFile(join(cwd, "package.json"), "utf8");
13
- return JSON.parse(contents);
14
- }
15
- /**
16
- * Reports whether `name` appears in either `dependencies` or `devDependencies`.
17
- * Both are checked because framework packages may legitimately live in either.
18
- */
19
- function hasDependency(pkg, name) {
20
- return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
21
- }
22
- //#endregion
23
- //#region src/lib/orca/detectors/port.ts
24
- /**
25
- * Port assumed when no explicit dev port can be discovered. Matches Next.js's
26
- * own default so the inferred issuer URL lines up with `next dev`.
27
- */
28
- const DEFAULT_DEV_PORT = 3e3;
29
- /**
30
- * Determines the local dev-server port for `cwd`. The `dev` script is the most
31
- * authoritative source, then a `PORT` declaration in an env file, falling back
32
- * to {@link DEFAULT_DEV_PORT}. Used to derive the local issuer URL.
33
- */
34
- async function detectDevPort(cwd, pkg) {
35
- const dev = pkg.scripts?.dev;
36
- const fromScript = typeof dev === "string" ? extractPort(dev) : void 0;
37
- if (fromScript) return fromScript;
38
- const fromEnvFile = await portFromEnvFile(cwd);
39
- if (fromEnvFile) return fromEnvFile;
40
- return DEFAULT_DEV_PORT;
41
- }
42
- async function portFromEnvFile(cwd) {
43
- for (const candidate of [".env.local", ".env"]) try {
44
- const rawPort = (await readFile(join(cwd, candidate), "utf8")).match(/^\s*PORT\s*=\s*(\d+)/m)?.[1];
45
- if (rawPort) return Number.parseInt(rawPort, 10);
46
- } catch {
47
- continue;
48
- }
49
- }
50
- /**
51
- * Parses a dev port out of an npm `dev` script string, recognizing both flag
52
- * forms (`-p`/`--port`) and a leading `PORT=` env assignment. Returns
53
- * `undefined` when no valid positive port is present so callers can fall back.
54
- */
55
- function extractPort(script) {
56
- const inline = script.match(/-p\s+(\d+)|--port[=\s]+(\d+)/);
57
- if (inline) {
58
- const raw = inline[1] ?? inline[2];
59
- if (!raw) return;
60
- const value = Number.parseInt(raw, 10);
61
- if (Number.isFinite(value) && value > 0) return value;
62
- }
63
- const rawEnvPort = script.match(/(?:^|\s)PORT=(\d+)/)?.[1];
64
- if (rawEnvPort) return Number.parseInt(rawEnvPort, 10);
65
- }
66
- /**
67
- * Builds the local OIDC issuer URL for a given dev port. Centralized so the
68
- * `localhost` origin convention is defined in exactly one place.
69
- */
70
- function issuerFromPort(port) {
71
- return `http://localhost:${port}`;
72
- }
73
- //#endregion
74
- //#region src/lib/orca/detectors/next.ts
75
- /**
76
- * Detects a Next.js App Router project and extracts its facts: the App Router
77
- * directory (`app` vs `src/app`), the dev-server port (parsed from the `dev`
78
- * script / env file, else 3000), and the derived local issuer URL. Owns every
79
- * Next-specific assumption so the orchestrator and commands stay generic.
80
- */
81
- var NextDetector = class {
82
- framework = "next";
83
- /**
84
- * Returns `null` when `cwd` is not a Next.js project (no `next` dependency),
85
- * so the orchestrator can try other detectors. Throws
86
- * `E_UNSUPPORTED_PROJECT_SHAPE` when it is Next.js but lacks an App Router
87
- * directory (e.g. a Pages Router project), which is a hard error rather than
88
- * an empty directory to scaffold.
89
- */
90
- async detect(cwd) {
91
- const pkg = await readPackageJson(cwd).catch(() => void 0);
92
- if (!pkg || !hasDependency(pkg, "next")) return null;
93
- const appDir = await dirExists(join(cwd, "app")) ? "app" : await dirExists(join(cwd, "src/app")) ? "src/app" : void 0;
94
- 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." });
95
- const devPort = await detectDevPort(cwd, pkg);
96
- return {
97
- id: "next",
98
- appDir,
99
- devPort,
100
- url: issuerFromPort(devPort)
101
- };
102
- }
103
- };
104
- async function dirExists(path) {
105
- try {
106
- return (await stat(path)).isDirectory();
107
- } catch (error) {
108
- if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return false;
109
- throw error;
110
- }
111
- }
112
- //#endregion
113
- //#region src/lib/orca/detectors/index.ts
114
- /**
115
- * Active detectors, in probe order. The orchestrator tries each until one
116
- * recognises the project. Add a framework by appending its detector here — no
117
- * orchestrator changes needed.
118
- */
119
- const detectors = [new NextDetector()];
120
- //#endregion
121
- //#region src/lib/orca/patchers/rule/file-writer/index.ts
122
- /**
123
- * Applies a {@link ScaffoldPlan} to disk, executing its operations in order.
124
- *
125
- * Operations are idempotent: writes whose target already matches the desired
126
- * contents are recorded as skipped rather than rewritten, so re-running setup
127
- * is safe. With `dryRun` no filesystem changes are made but the result still
128
- * reflects what would have been written. Existing files are only overwritten
129
- * when `force` is set; otherwise an `E_CONFLICT` is thrown to protect
130
- * user-authored content. Paths in the plan are resolved relative to `cwd`.
131
- */
132
- async function scaffold(plan, opts) {
133
- const result = {
134
- dryRun: opts.dryRun,
135
- filesWritten: [],
136
- filesSkipped: [],
137
- depsAdded: []
138
- };
139
- for (const op of plan.ops) await applyOp(op, opts, result);
140
- return result;
141
- }
142
- async function applyOp(op, opts, result) {
143
- switch (op.kind) {
144
- case "mkdir":
145
- await ensureDir(abs(opts.cwd, op.path), op.mode, opts.dryRun, result);
146
- break;
147
- case "write":
148
- await writeText(abs(opts.cwd, op.path), op.contents, {
149
- mode: op.mode,
150
- force: opts.force,
151
- dryRun: opts.dryRun
152
- }, result);
153
- break;
154
- case "append":
155
- await appendText(abs(opts.cwd, op.path), op.contents, op.ifMissing, opts.dryRun, result);
156
- break;
157
- case "merge-env":
158
- await mergeEnv(abs(opts.cwd, op.path), op.entries, opts.dryRun, result);
159
- break;
160
- case "merge-json":
161
- await mergeJson(abs(opts.cwd, op.path), op.patch, opts.dryRun, result);
162
- break;
163
- case "append-gitignore":
164
- await appendGitignore(abs(opts.cwd, ".gitignore"), op.entries, opts.dryRun, result);
165
- break;
166
- case "add-dep":
167
- await addDependency(abs(opts.cwd, "package.json"), op, opts.dryRun, result);
168
- break;
169
- }
170
- }
171
- async function ensureDir(path, mode, dryRun, result) {
172
- if (dryRun) {
173
- result.filesWritten.push(path);
174
- return;
175
- }
176
- await mkdir(path, {
177
- recursive: true,
178
- mode
179
- });
180
- if (mode) await chmod(path, mode).catch(() => void 0);
181
- result.filesWritten.push(path);
182
- }
183
- async function writeText(path, contents, opts, result) {
184
- const existing = await readIfExists(path);
185
- if (existing === contents) {
186
- result.filesSkipped.push(path);
187
- return;
188
- }
189
- if (existing !== void 0 && !opts.force) throw new ZitadelError("E_CONFLICT", `Refusing to overwrite ${path}`, {
190
- hint: "Re-run with --force if you want the CLI to replace this file.",
191
- details: { path }
192
- });
193
- if (opts.dryRun) {
194
- result.filesWritten.push(path);
195
- return;
196
- }
197
- await mkdir(dirname(path), { recursive: true });
198
- const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
199
- await writeFile(tmp, contents, { mode: opts.mode });
200
- if (opts.mode) await chmod(tmp, opts.mode).catch(() => void 0);
201
- await rename(tmp, path);
202
- result.filesWritten.push(path);
203
- }
204
- async function appendText(path, contents, ifMissing, dryRun, result) {
205
- const existing = await readIfExists(path) ?? "";
206
- if (ifMissing && existing.includes(ifMissing)) {
207
- result.filesSkipped.push(path);
208
- return;
209
- }
210
- const next = `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${contents}`;
211
- if (next === existing) {
212
- result.filesSkipped.push(path);
213
- return;
214
- }
215
- if (dryRun) {
216
- result.filesWritten.push(path);
217
- return;
218
- }
219
- await mkdir(dirname(path), { recursive: true });
220
- await writeFile(path, next);
221
- result.filesWritten.push(path);
222
- }
223
- async function mergeEnv(path, entries, dryRun, result) {
224
- const existing = await readIfExists(path) ?? "";
225
- 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)));
226
- const additions = Object.entries(entries).filter(([key]) => !present.has(key));
227
- if (additions.length === 0) {
228
- result.filesSkipped.push(path);
229
- return;
230
- }
231
- const block = additions.map(([key, value]) => `${key}=${value}`).join("\n");
232
- const next = `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${block}\n`;
233
- if (dryRun) {
234
- result.filesWritten.push(path);
235
- return;
236
- }
237
- await mkdir(dirname(path), { recursive: true });
238
- await writeFile(path, next);
239
- result.filesWritten.push(path);
240
- }
241
- async function mergeJson(path, patch, dryRun, result) {
242
- const existing = await readIfExists(path);
243
- const contents = `${stableStringify(deepMerge(existing ? parseJsonObject(existing, path) : {}, patch))}\n`;
244
- if (existing === contents) {
245
- result.filesSkipped.push(path);
246
- return;
247
- }
248
- if (dryRun) {
249
- result.filesWritten.push(path);
250
- return;
251
- }
252
- await mkdir(dirname(path), { recursive: true });
253
- await writeFile(path, contents);
254
- result.filesWritten.push(path);
255
- }
256
- async function appendGitignore(path, entries, dryRun, result) {
257
- const existing = await readIfExists(path) ?? "";
258
- const lines = new Set(existing.split(/\r?\n/g).map((line) => line.trim()));
259
- const missing = entries.filter((entry) => !lines.has(entry));
260
- if (missing.length === 0) {
261
- result.filesSkipped.push(path);
262
- return;
263
- }
264
- const next = `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${missing.join("\n")}\n`;
265
- if (dryRun) {
266
- result.filesWritten.push(path);
267
- return;
268
- }
269
- await writeFile(path, next);
270
- result.filesWritten.push(path);
271
- }
272
- async function addDependency(path, op, dryRun, result) {
273
- const existing = await readIfExists(path);
274
- if (!existing) throw new ZitadelError("E_VALIDATION", "package.json is required to add Zitadel dependencies");
275
- const current = parseJsonObject(existing, path);
276
- const key = op.dev ? "devDependencies" : "dependencies";
277
- const deps = isObject(current[key]) ? current[key] : {};
278
- if (deps[op.name] === op.version) {
279
- result.filesSkipped.push(path);
280
- return;
281
- }
282
- current[key] = {
283
- ...deps,
284
- [op.name]: op.version
285
- };
286
- const contents = `${stableStringify(current)}\n`;
287
- if (dryRun) {
288
- result.filesWritten.push(path);
289
- result.depsAdded.push(op.name);
290
- return;
291
- }
292
- await writeFile(path, contents);
293
- result.filesWritten.push(path);
294
- result.depsAdded.push(op.name);
295
- }
296
- async function readIfExists(path) {
297
- try {
298
- return await readFile(path, "utf8");
299
- } catch (error) {
300
- if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return;
301
- throw error;
302
- }
303
- }
304
- function abs(cwd, path) {
305
- return join(cwd, path);
306
- }
307
- function deepMerge(target, patch) {
308
- const out = { ...target };
309
- for (const [key, value] of Object.entries(patch)) if (isObject(value) && isObject(out[key])) out[key] = deepMerge(out[key], value);
310
- else out[key] = value;
311
- return out;
312
- }
313
- //#endregion
314
- //#region src/lib/orca/patchers/rule/reclaim.ts
315
- /**
316
- * The subset of a patcher plan's operations that `doctor --fix` re-applies:
317
- * env merges, gitignore entries, dependency additions, and marker-bearing
318
- * managed files (framework routes/middleware). Deliberately excludes the
319
- * unmarked `.zitadel/` resource writes and `zitadel.json` — those are
320
- * user-editable and synced by `apply`, so `--fix` must not clobber them.
321
- *
322
- * Pure: filters a freshly-allocated list; the input plan is not mutated.
323
- */
324
- function reclaimableOps(plan) {
325
- return plan.ops.filter((op) => op.kind === "merge-env" || op.kind === "append-gitignore" || op.kind === "add-dep" || op.kind === "write" && op.contents.includes("// zitadel-cli: managed-file v1"));
326
- }
327
- //#endregion
328
- //#region src/lib/orca/patchers/rule/base.ts
329
- /**
330
- * Base for rule-based (deterministic, template-driven) patchers, as opposed to
331
- * a future LLM-driven family. It applies the integration by building a
332
- * file-operation plan and running the file-writer — that strategy stays
333
- * entirely inside this family, so callers only ever see the family-neutral
334
- * {@link Patcher} surface. Owns the framework-agnostic `.zitadel/` base files
335
- * and the shared eject classification; subclasses contribute only their
336
- * framework-specific routes/middleware.
337
- */
338
- var AbstractRulePatcher = class {
339
- /** Apply the full plan (base `.zitadel/` files + framework routes). */
340
- async patch(ctx, opts) {
341
- return scaffold(this.plan(ctx), opts);
342
- }
343
- /**
344
- * Re-apply only the reclaimable subset — env files, gitignore, the SDK
345
- * dependency, and marker-bearing routes — leaving the user-editable
346
- * `.zitadel/` resources untouched. Backs `doctor --fix`.
347
- */
348
- async repair(ctx, opts) {
349
- const plan = this.plan(ctx);
350
- return scaffold({
351
- ops: reclaimableOps(plan),
352
- summary: plan.summary
353
- }, opts);
354
- }
355
- /** Shared base artifacts plus the subclass's marker-bearing route files. */
356
- artifacts(view) {
357
- return {
358
- markedFiles: this.routeFiles(view),
359
- rootConfigFiles: ["zitadel.json"],
360
- directories: [".zitadel"],
361
- envBackups: [".env.local"],
362
- dependencies: this.routeDeps(view)
363
- };
364
- }
365
- /**
366
- * The full file-operation plan this patcher would apply. Public so rule-family
367
- * unit tests can assert the planned ops directly; the generic {@link Patcher}
368
- * interface deliberately does not expose it (an LLM patcher has no such plan).
369
- */
370
- plan(ctx) {
371
- return {
372
- ops: [...this.baseOps(ctx), ...this.routeOps(ctx)],
373
- summary: [this.summary(ctx)]
374
- };
375
- }
376
- /**
377
- * The framework-agnostic `.zitadel/` base files every rule patcher writes:
378
- * the project secret, `zitadel.json`, env templates, and an empty sync
379
- * state. The `schemas/` and `flows/` directories are created empty — the
380
- * server provisions the default user schema and flow definition when the
381
- * project is created, so nothing is scaffolded into them here. Pure: no
382
- * filesystem or network.
383
- */
384
- baseOps(ctx) {
385
- return [
386
- {
387
- kind: "mkdir",
388
- path: ".zitadel",
389
- mode: 448
390
- },
391
- {
392
- kind: "mkdir",
393
- path: ".zitadel/flows"
394
- },
395
- {
396
- kind: "mkdir",
397
- path: ".zitadel/schemas"
398
- },
399
- {
400
- kind: "append-gitignore",
401
- entries: [
402
- ".zitadel/secret",
403
- ".env*",
404
- "!.env.example"
405
- ]
406
- },
407
- {
408
- kind: "write",
409
- path: ".zitadel/secret",
410
- mode: 384,
411
- contents: `${stableStringify({
412
- project_id: ctx.project.id,
413
- project_secret: ctx.project.projectSecret,
414
- preview_secret: ctx.project.previewSecret,
415
- preview_origins: ctx.project.previewOrigins,
416
- created_at: ctx.project.createdAt
417
- })}\n`
418
- },
419
- {
420
- kind: "write",
421
- path: "zitadel.json",
422
- contents: `${stableStringify(projectConfig(ctx))}\n`
423
- },
424
- {
425
- kind: "merge-env",
426
- path: ".env.example",
427
- entries: {
428
- ZITADEL_PROJECT_ID: "",
429
- ZITADEL_ENVIRONMENT: "",
430
- ZITADEL_ISSUER: "",
431
- ZITADEL_URL: "",
432
- NEXT_PUBLIC_ZITADEL_PROJECT_ID: ""
433
- }
434
- },
435
- {
436
- kind: "merge-env",
437
- path: ".env.local",
438
- entries: {
439
- ZITADEL_PROJECT_ID: ctx.project.id,
440
- ZITADEL_ENVIRONMENT: "development",
441
- ZITADEL_ISSUER: ctx.issuer,
442
- ZITADEL_URL: ctx.server,
443
- NEXT_PUBLIC_ZITADEL_PROJECT_ID: ctx.project.id
444
- }
445
- },
446
- {
447
- kind: "write",
448
- path: ".zitadel/state.json",
449
- contents: `${stableStringify({
450
- framework: ctx.framework.id,
451
- resources: {}
452
- })}\n`
453
- }
454
- ];
455
- }
456
- };
457
- /** Builds the `zitadel.json` body persisted at the project root. */
458
- function projectConfig(ctx) {
459
- const environments = { development: { issuer: ctx.issuer } };
460
- if (ctx.project.previewOrigins.length > 0) environments.preview = { issuer_pattern: ctx.project.previewOrigins.map((origin) => `https://${origin}`) };
461
- return {
462
- $schema: "https://schemas.zitadel.com/v2/project.schema.json",
463
- project: ctx.project.id,
464
- server: resolveServerOrigin(ctx.server),
465
- framework: { id: ctx.framework.id },
466
- branding: {
467
- renderer: ctx.rendererId,
468
- attribution: "visible"
469
- },
470
- environments
471
- };
472
- }
473
- /** Normalizes a server URL to its origin, falling back to {@link DEFAULT_SERVER}. */
474
- function resolveServerOrigin(source) {
475
- try {
476
- return new URL(source).origin;
477
- } catch {
478
- return DEFAULT_SERVER;
479
- }
480
- }
481
- //#endregion
482
- //#region src/lib/orca/patchers/rule/next/renderers/lit/index.ts
483
- /**
484
- * Placeholder renderer for the `<zitadel-flow>` Lit web component. Declared
485
- * so the `web-component` renderer id resolves and surfaces a clear
486
- * "not yet published" error, while reserving the integration shape for when
487
- * `@zitadel/ui-lit` ships. The `authPage` template emits an illustrative
488
- * page only; this renderer is never selected for real scaffolding because
489
- * `getRenderer` rejects any `status: "not-implemented"` spec.
490
- */
491
- const litRenderer = {
492
- id: "web-component",
493
- displayName: "Web component (<zitadel-flow>)",
494
- status: "not-implemented",
495
- frameworks: [
496
- "next",
497
- "astro",
498
- "remix",
499
- "sveltekit",
500
- "nuxt",
501
- "vanilla"
502
- ],
503
- dependency: {
504
- name: "@zitadel/ui-lit",
505
- version: "workspace:*"
506
- },
507
- templates: { authPage(mode) {
508
- return {
509
- mode,
510
- contents: `${MANAGED_MARKER}
511
- // The web component renderer ships a <zitadel-flow> element. Until
512
- // @zitadel/ui-lit is published, this template only declares the
513
- // intended integration point. See docs/design/cli/bdui-renderer.md.
514
- import "@zitadel/ui-lit";
515
-
516
- const environment =
517
- process.env.ZITADEL_ENVIRONMENT ??
518
- (process.env.NODE_ENV === "production" ? "production" : "development");
519
-
520
- export default function ${mode === "login" ? "LoginPage" : "RegisterPage"}() {
521
- return (
522
- <zitadel-flow
523
- purpose="${mode === "login" ? "login" : "register"}"
524
- project-id={process.env.ZITADEL_PROJECT_ID}
525
- issuer={process.env.ZITADEL_ISSUER}
526
- environment={environment}
527
- />
528
- );
529
- }
530
- `
531
- };
532
- } }
533
- };
534
- //#endregion
535
- //#region src/lib/orca/patchers/rule/next/renderers/react/index.ts
536
- /**
537
- * The Next.js App Router renderer scaffolds `/login`, `/register`, and
538
- * `/profile` pages that drive the `<zitadel-login>` and `<zitadel-logout>`
539
- * Lit web components.
540
- *
541
- * Each page is a single client component (`"use client"`) that, inside a
542
- * `next/dynamic({ ssr: false })` loader, builds the SDK project handle with
543
- * `configureZitadel({ projectId, proxyPath: "/__nextgen" })` and passes it to
544
- * the widget via `project={...}`. It also imports
545
- * `@zitadel/sdk-next/client` for its `customElements.define`
546
- * side-effect — importing `@zitadel/components` directly would fail on
547
- * strict-resolution package managers (pnpm, yarn PnP) because the app only
548
- * declares `sdk-next` as a direct dep. SSR is disabled because Lit's element
549
- * registration needs a browser.
550
- *
551
- * The handle is passed as the `project` DOM property, which relies on React
552
- * 19's custom-element property binding (the scaffold targets the latest Next /
553
- * React). The backend URL never reaches the browser: the client talks to the
554
- * same-origin `/__nextgen` proxy path, and the scaffolded `middleware.ts`
555
- * forwards it to `ZITADEL_URL` server-side. `NEXT_PUBLIC_ZITADEL_PROJECT_ID` is
556
- * public — the project id is not sensitive and the widget needs it to start a
557
- * flow.
558
- */
559
- const reactRenderer = {
560
- id: "react",
561
- displayName: "React (Next.js App Router)",
562
- status: "available",
563
- frameworks: ["next"],
564
- dependency: {
565
- name: "@zitadel/sdk-next",
566
- version: "latest"
567
- },
568
- templates: {
569
- authPage(mode) {
570
- const componentName = mode === "login" ? "LoginPage" : "RegisterPage";
571
- const elementName = mode === "login" ? "ZitadelLogin" : "ZitadelRegister";
572
- return {
573
- mode,
574
- contents: `${MANAGED_MARKER}
575
- "use client";
576
-
577
- import dynamic from "next/dynamic";
578
-
579
- const ${elementName} = dynamic(
580
- async () => {
581
- const { configureZitadel } = await import("@zitadel/sdk-next/client");
582
- // Build the SDK project handle and pass it to the component via the
583
- // \`project\` prop. The component reads config from this prop directly, so
584
- // it works regardless of how the SDK packages are bundled. The backend URL
585
- // stays server-side — requests go through the proxy path "/__nextgen",
586
- // which the scaffolded middleware forwards to the Zitadel server.
587
- const project = configureZitadel({
588
- projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
589
- proxyPath: "/__nextgen",
590
- });
591
- return function ${elementName}Element() {
592
- return (
593
- <zitadel-login
594
- project={project}
595
- purpose="${mode}"
596
- post-sign-in-url="/profile"
597
- />
598
- );
599
- };
600
- },
601
- { ssr: false },
602
- );
603
-
604
- export default function ${componentName}() {
605
- return (
606
- <main style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center" }}>
607
- <${elementName} />
608
- </main>
609
- );
610
- }
611
- `
612
- };
613
- },
614
- profilePage() {
615
- return { contents: `${MANAGED_MARKER}
616
- "use client";
617
-
618
- import dynamic from "next/dynamic";
619
-
620
- const ZitadelLogout = dynamic(
621
- async () => {
622
- const { configureZitadel } = await import("@zitadel/sdk-next/client");
623
- const project = configureZitadel({
624
- projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
625
- proxyPath: "/__nextgen",
626
- });
627
- return function ZitadelLogoutElement() {
628
- return (
629
- <zitadel-logout
630
- project={project}
631
- post-sign-out-url="/login"
632
- />
633
- );
634
- };
635
- },
636
- { ssr: false },
637
- );
638
-
639
- export default function ProfilePage() {
640
- return (
641
- <main style={{ padding: "48px", maxWidth: "600px", margin: "0 auto" }}>
642
- <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "24px" }}>
643
- <h1 style={{ fontSize: "24px", fontWeight: 700, margin: 0 }}>Signed in</h1>
644
- <ZitadelLogout />
645
- </div>
646
- <p style={{ color: "#6b7280" }}>You are signed in. Use the button above to log out.</p>
647
- </main>
648
- );
649
- }
650
- ` };
651
- },
652
- customElementsDts() {
653
- return { contents: `${MANAGED_MARKER}
654
- import type React from "react";
655
- import type { ZitadelProject } from "@zitadel/sdk-next/client";
656
-
657
- declare module "react" {
658
- namespace JSX {
659
- interface IntrinsicElements {
660
- "zitadel-login": React.HTMLAttributes<HTMLElement> & {
661
- project?: ZitadelProject;
662
- "session-exchange-path"?: string;
663
- "post-sign-in-url"?: string;
664
- purpose?: string;
665
- };
666
- "zitadel-logout": React.HTMLAttributes<HTMLElement> & {
667
- project?: ZitadelProject;
668
- "post-sign-out-url"?: string;
669
- };
670
- }
671
- }
672
- }
673
- ` };
674
- }
675
- }
676
- };
677
- //#endregion
678
- //#region src/lib/orca/patchers/rule/next/renderers/registry.ts
679
- /**
680
- * Runtime mirror of the {@link RendererId} union, used by {@link isRendererId}
681
- * to validate untrusted strings (a TS union has no runtime presence). Must stay
682
- * in sync with the {@link RendererId} type.
683
- */
684
- const RENDERER_IDS = ["react", "web-component"];
685
- /**
686
- * Type guard narrowing an arbitrary value to a {@link RendererId}, used to
687
- * validate renderer ids read from config before indexing {@link RENDERERS}.
688
- */
689
- function isRendererId(value) {
690
- return typeof value === "string" && RENDERER_IDS.includes(value);
691
- }
692
- /**
693
- * The single source of truth mapping each {@link RendererId} to its spec.
694
- * Keyed by id so {@link getRenderer} can look up and validate a renderer
695
- * chosen from persisted config (an arbitrary string) at runtime.
696
- */
697
- const RENDERERS = {
698
- react: reactRenderer,
699
- "web-component": litRenderer
700
- };
701
- /**
702
- * Resolves a renderer id (an untrusted string from config) to its spec,
703
- * throwing a typed {@link ZitadelError} rather than returning `undefined`
704
- * so callers get an actionable message. Rejects ids that are unknown
705
- * (`E_VALIDATION`) or declared-but-unpublished (`E_NOT_IMPLEMENTED`),
706
- * guaranteeing the returned spec is safe to scaffold from.
707
- */
708
- function getRenderer(id) {
709
- if (!isRendererId(id)) throw new ZitadelError("E_VALIDATION", `Unknown renderer "${id}"`, { hint: `Available renderers: ${Object.keys(RENDERERS).join(", ")}` });
710
- const renderer = RENDERERS[id];
711
- 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." });
712
- return renderer;
713
- }
714
- //#endregion
715
- //#region src/lib/orca/patchers/rule/next/index.ts
716
- /**
717
- * Next.js `middleware.ts` at the project root. Wires `nextgenMiddleware` so the
718
- * scaffolded `<zitadel-login api-base="/__nextgen">` requests are same-origin
719
- * proxied to `ZITADEL_URL` and `/profile` is gated. The `middleware`
720
- * form (not the Next 16 `proxy` rename) works on every supported Next major.
721
- * Carries the managed marker so `doctor --fix` reclaims it and `eject` removes it.
722
- */
723
- const middlewareTemplate = `${MANAGED_MARKER}
724
- import { nextgenMiddleware } from "@zitadel/sdk-next/middleware";
725
- import type { NextRequest } from "next/server";
726
-
727
- export function middleware(req: NextRequest) {
728
- return nextgenMiddleware(req, {
729
- url: process.env.ZITADEL_URL,
730
- protectedRoutes: ["/profile"],
731
- loginPath: "/login",
732
- });
733
- }
734
-
735
- export const config = {
736
- matcher: ["/__nextgen/:path*", "/profile/:path*"],
737
- };
738
- `;
739
- /**
740
- * Rule-based patcher for the Next.js App Router. Inherits the shared
741
- * `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
742
- * Next routes/middleware whose templates come from the chosen renderer.
743
- */
744
- var NextPatcher = class extends AbstractRulePatcher {
745
- /** Returns true for Next.js projects. */
746
- canPatch(framework) {
747
- return framework === "next";
748
- }
749
- routeOps(ctx) {
750
- return nextCodeOps(ctx, getRenderer(ctx.rendererId));
751
- }
752
- routeFiles(view) {
753
- return nextCodeFilePaths(view.framework.appDir, getRenderer(view.rendererId));
754
- }
755
- routeDeps(view) {
756
- return [getRenderer(view.rendererId).dependency.name];
757
- }
758
- summary(ctx) {
759
- return {
760
- title: "Next.js integration",
761
- detail: `Scaffolded login/register/profile routes with renderer "${ctx.rendererId}".`
762
- };
763
- }
764
- };
765
- /**
766
- * Ordered paths of the framework code files the patcher writes. All carry the
767
- * managed marker. Shared by {@link NextPatcher.routeOps} (which adds contents)
768
- * and {@link NextPatcher.routeFiles} (which only needs the paths) so the two
769
- * cannot drift.
770
- */
771
- function nextCodeFilePaths(appDir, renderer) {
772
- const paths = [join(appDir, "login/page.tsx"), join(appDir, "register/page.tsx")];
773
- if (renderer.templates.profilePage) paths.push(join(appDir, "profile/page.tsx"));
774
- paths.push(join(appDir, "../middleware.ts"));
775
- if (renderer.templates.provider) paths.push(join(appDir, renderer.templates.provider.filename));
776
- if (renderer.templates.customElementsDts) paths.push(join(appDir, "../custom-elements.d.ts"));
777
- return paths;
778
- }
779
- /** The Next route/middleware write ops plus the SDK dependency. */
780
- function nextCodeOps(ctx, renderer) {
781
- const appDir = ctx.framework.appDir;
782
- const ops = [{
783
- kind: "write",
784
- path: join(appDir, "login/page.tsx"),
785
- contents: renderer.templates.authPage("login").contents
786
- }, {
787
- kind: "write",
788
- path: join(appDir, "register/page.tsx"),
789
- contents: renderer.templates.authPage("register").contents
790
- }];
791
- const profile = renderer.templates.profilePage?.();
792
- if (profile) ops.push({
793
- kind: "write",
794
- path: join(appDir, "profile/page.tsx"),
795
- contents: profile.contents
796
- });
797
- ops.push({
798
- kind: "write",
799
- path: join(appDir, "../middleware.ts"),
800
- contents: middlewareTemplate
801
- });
802
- const provider = renderer.templates.provider;
803
- if (provider) ops.push({
804
- kind: "write",
805
- path: join(appDir, provider.filename),
806
- contents: provider.contents
807
- });
808
- const dts = renderer.templates.customElementsDts?.();
809
- if (dts) ops.push({
810
- kind: "write",
811
- path: join(appDir, "../custom-elements.d.ts"),
812
- contents: dts.contents
813
- });
814
- ops.push({
815
- kind: "add-dep",
816
- name: renderer.dependency.name,
817
- version: dependencyVersionForCli(ctx.cliVersion, renderer.dependency.version)
818
- });
819
- return ops;
820
- }
821
- function dependencyVersionForCli(cliVersion, fallback) {
822
- const normalized = cliVersion.trim().replace(/^v/, "");
823
- if (/^\d+\.\d+\.\d+-alpha\.\d+$/.test(normalized)) return normalized;
824
- return normalized.match(/^\d+\.\d+\.\d+-([0-9A-Za-z]+)(?:[.-]|$)/)?.[1] ?? fallback;
825
- }
826
- //#endregion
827
- //#region src/lib/orca/patchers/index.ts
828
- /**
829
- * Active patchers, in priority order; the first whose `canPatch` matches wins.
830
- *
831
- * Patchers are grouped by family under subdirectories: `rule/` holds the
832
- * deterministic, template-driven patchers (extending
833
- * {@link import("./rule/base").AbstractRulePatcher}). A future LLM-driven
834
- * family lives under `llm/` and registers its concrete patchers here — no
835
- * orchestrator or command changes needed. Only Next.js is supported today.
836
- */
837
- const patchers = [new NextPatcher()];
838
- //#endregion
839
- //#region src/lib/orca/scaffolders/cli.ts
840
- /**
841
- * Base for scaffolders that delegate to an external CLI (e.g. create-next-app).
842
- * Subclasses implement {@link scaffold} and call {@link runCommand}.
843
- */
844
- var AbstractCLIScaffolder = class {
845
- /** True when the requested framework is in {@link supportedFrameworks}. */
846
- canScaffold(framework) {
847
- return this.supportedFrameworks.includes(framework);
848
- }
849
- /**
850
- * Runs an external command in `cwd`, throwing a typed {@link ZitadelError} on
851
- * failure so the cause surfaces as a categorized CLI error. Distinguishes
852
- * "binary not on PATH" (`ENOENT` from the spawn itself) from "binary ran but
853
- * exited non-zero" — the former previously got masked as a generic
854
- * `exited with status 1`, leaving users to guess. Tests stub
855
- * `node:child_process` to assert the command without spawning.
856
- */
857
- runCommand(command, args, cwd) {
858
- const result = spawnSync(command, [...args], {
859
- cwd,
860
- encoding: "utf8"
861
- });
862
- if (result.error) {
863
- const err = result.error;
864
- const notFound = err.code === "ENOENT";
865
- throw new ZitadelError("E_VALIDATION", notFound ? `Command not found: ${command}` : `Failed to spawn "${command}": ${err.message}`, {
866
- hint: notFound ? `Ensure '${command}' is installed and on PATH.` : void 0,
867
- details: {
868
- command,
869
- args: [...args],
870
- code: err.code
871
- }
872
- });
873
- }
874
- const status = result.status ?? 1;
875
- if (status !== 0) {
876
- const stdout = String(result.stdout ?? "");
877
- const stderr = String(result.stderr ?? "");
878
- const output = truncateCommandOutput([stderr, stdout].filter(Boolean).join("\n").trim());
879
- throw new ZitadelError("E_VALIDATION", `Command "${command} ${args.join(" ")}" exited with status ${String(status)}`, {
880
- hint: output ? `Command output:\n${output}` : "Run the command directly for more detail.",
881
- details: {
882
- command,
883
- args: [...args],
884
- cwd,
885
- stdout,
886
- stderr
887
- }
888
- });
889
- }
890
- }
891
- };
892
- function truncateCommandOutput(output) {
893
- const limit = 4e3;
894
- if (output.length <= limit) return output;
895
- return `${output.slice(0, limit)}\n... output truncated ...`;
896
- }
897
- //#endregion
898
- //#region src/lib/orca/scaffolders/next.ts
899
- const CREATE_NEXT_APP_VERSION = "16.2.4";
900
- /** Scaffolds a new Next.js App Router project with `create-next-app`. */
901
- var NextScaffolder = class extends AbstractCLIScaffolder {
902
- displayName = "Next.js";
903
- supportedFrameworks = ["next"];
904
- /**
905
- * Runs the pinned `create-next-app` version in `cwd`, creating a TypeScript
906
- * App Router project in place. `--yes` accepts all defaults so the command
907
- * runs unattended, and `--skip-install` leaves dependency installation to the
908
- * setup command's explicit next step after Zitadel patches package.json.
909
- */
910
- async scaffold(cwd, _framework) {
911
- this.runCommand("npx", [
912
- "--yes",
913
- `create-next-app@${CREATE_NEXT_APP_VERSION}`,
914
- ".",
915
- "--ts",
916
- "--app",
917
- "--use-npm",
918
- "--disable-git",
919
- "--yes",
920
- "--skip-install"
921
- ], cwd);
922
- }
923
- };
924
- //#endregion
925
- //#region src/lib/orca/scaffolders/index.ts
926
- /**
927
- * Active scaffolders, in priority order. The framework picker derives its
928
- * choices from this list. Add a new framework by appending its scaffolder
929
- * here — no orchestrator changes needed.
930
- */
931
- const scaffolders = [new NextScaffolder()];
932
- //#endregion
933
- //#region src/lib/orca/index.ts
934
- /**
935
- * Orchestrates the three per-framework strategies — detectors (recognise an
936
- * existing project and extract its facts), scaffolders (create a project), and
937
- * patchers (integrate Zitadel) — over their respective registries. It resolves
938
- * the right strategy for a framework and drives the detect/scaffold lifecycle;
939
- * how a patcher applies its work (file operations vs an LLM agent) stays
940
- * internal to that patcher. Registries are injected so tests can supply fakes.
941
- */
942
- var Orca = class {
943
- constructor(detectors, scaffolders, patchers) {
944
- this.detectors = detectors;
945
- this.scaffolders = scaffolders;
946
- this.patchers = patchers;
947
- }
948
- /**
949
- * Detects the framework in `cwd` and extracts its {@link FrameworkFacts},
950
- * honouring an explicit `requested` framework. Throws
951
- * `E_FRAMEWORK_NOT_DETECTED` when nothing matches; a detector's
952
- * `E_UNSUPPORTED_PROJECT_SHAPE` (recognised but unsupported) propagates.
953
- */
954
- async detect(cwd, requested) {
955
- const candidates = requested ? this.detectors.filter((detector) => detector.framework === requested) : this.detectors;
956
- if (requested && candidates.length === 0) throw new ZitadelError("E_FRAMEWORK_NOT_DETECTED", `Unsupported framework "${requested}"`, { hint: `Supported frameworks: ${this.frameworkIds().join(", ")}.` });
957
- for (const detector of candidates) {
958
- const facts = await detector.detect(cwd);
959
- if (facts) return facts;
960
- }
961
- throw new ZitadelError("E_FRAMEWORK_NOT_DETECTED", "Could not detect a supported app framework", { hint: "Run setup from your app project directory, pass --cwd <path-to-app>, or run setup from an empty directory to scaffold a new app." });
962
- }
963
- /**
964
- * Non-throwing detection: returns `undefined` instead of raising for a
965
- * project that is absent, unrecognised, or recognised-but-unsupported, so
966
- * callers (e.g. `eject`) can probe and degrade gracefully.
967
- */
968
- async tryDetect(cwd) {
969
- try {
970
- return await this.detect(cwd);
971
- } catch (error) {
972
- if (error instanceof ZitadelError && (error.code === "E_FRAMEWORK_NOT_DETECTED" || error.code === "E_UNSUPPORTED_PROJECT_SHAPE")) return;
973
- throw error;
974
- }
975
- }
976
- /** Whether `cwd` is safe for an in-place framework scaffold. */
977
- async isFreshScaffoldTarget(cwd) {
978
- return (await inspectScaffoldTarget(cwd)).scaffoldable;
979
- }
980
- /**
981
- * Creates a new `framework` project in `cwd`, then re-detects it to return
982
- * the resulting {@link FrameworkFacts}. Throws `E_CONFLICT` when the directory
983
- * already contains a project ("already scaffolded") and `E_VALIDATION` when no
984
- * scaffolder supports the framework.
985
- */
986
- async scaffold(cwd, framework) {
987
- const target = await inspectScaffoldTarget(cwd);
988
- if (!target.scaffoldable) throw new ZitadelError("E_CONFLICT", `Cannot scaffold: ${cwd} is not empty`, {
989
- hint: target.reason ?? "Run setup in an empty directory, or run setup from an existing supported app project.",
990
- details: { entries: target.entries }
991
- });
992
- const stash = target.hasRuntimeOnlyZitadel ? await stashRuntimeOnlyZitadel(cwd) : void 0;
993
- try {
994
- await this.scaffolderFor(framework).scaffold(cwd, framework);
995
- } finally {
996
- await restoreRuntimeOnlyZitadel(cwd, stash);
997
- }
998
- return this.detect(cwd, framework);
999
- }
1000
- /**
1001
- * Resolves the scaffolder for a framework, throwing `E_VALIDATION` (with the
1002
- * available list) when none matches.
1003
- */
1004
- scaffolderFor(framework) {
1005
- const scaffolder = this.scaffolders.find((candidate) => candidate.canScaffold(framework));
1006
- if (!scaffolder) throw new ZitadelError("E_VALIDATION", `No scaffolder supports "${framework}"`, { hint: `Available frameworks: ${this.availableFrameworks().map((f) => f.id).join(", ")}.` });
1007
- return scaffolder;
1008
- }
1009
- /**
1010
- * Resolves the patcher for a framework, throwing `E_VALIDATION` when none
1011
- * matches (e.g. a framework that can be scaffolded but not yet integrated).
1012
- */
1013
- patcherFor(framework) {
1014
- const patcher = this.patchers.find((candidate) => candidate.canPatch(framework));
1015
- if (!patcher) throw new ZitadelError("E_VALIDATION", `No patcher supports "${framework}"`, { hint: "Zitadel integration currently supports Next.js." });
1016
- return patcher;
1017
- }
1018
- /** The frameworks that can be scaffolded, derived from the scaffolder registry. */
1019
- availableFrameworks() {
1020
- return this.scaffolders.map((scaffolder) => ({
1021
- id: scaffolder.supportedFrameworks[0] ?? scaffolder.displayName,
1022
- displayName: scaffolder.displayName
1023
- }));
1024
- }
1025
- frameworkIds() {
1026
- return this.detectors.map((detector) => detector.framework);
1027
- }
1028
- };
1029
- /** {@link Orca} wired with the default detector, scaffolder, and patcher registries. */
1030
- function createOrca() {
1031
- return new Orca(detectors, scaffolders, patchers);
1032
- }
1033
- async function inspectScaffoldTarget(cwd) {
1034
- const entries = await readdir(cwd, { withFileTypes: true });
1035
- const names = entries.map((entry) => entry.name).sort();
1036
- let hasRuntimeOnlyZitadel = false;
1037
- for (const entry of entries) {
1038
- if (entry.name === ".gitignore") {
1039
- if (!entry.isFile()) return {
1040
- scaffoldable: false,
1041
- hasRuntimeOnlyZitadel: false,
1042
- reason: ".gitignore exists but is not a file.",
1043
- entries: names
1044
- };
1045
- continue;
1046
- }
1047
- if (entry.name === ".zitadel") {
1048
- if (!entry.isDirectory() || !await isRuntimeOnlyZitadelDir(join(cwd, ".zitadel"))) return {
1049
- scaffoldable: false,
1050
- hasRuntimeOnlyZitadel: false,
1051
- reason: ".zitadel contains project state. Move it aside or run setup from an empty app directory.",
1052
- entries: names
1053
- };
1054
- hasRuntimeOnlyZitadel = true;
1055
- continue;
1056
- }
1057
- return {
1058
- scaffoldable: false,
1059
- hasRuntimeOnlyZitadel: false,
1060
- reason: `Directory contains ${entry.name}. Run setup from an empty directory to scaffold a new app.`,
1061
- entries: names
1062
- };
1063
- }
1064
- return {
1065
- scaffoldable: true,
1066
- hasRuntimeOnlyZitadel,
1067
- entries: names
1068
- };
1069
- }
1070
- async function isRuntimeOnlyZitadelDir(path) {
1071
- const entries = await readdir(path, { withFileTypes: true });
1072
- if (entries.length !== 1 || entries[0]?.name !== "local" || !entries[0].isDirectory()) return false;
1073
- return true;
1074
- }
1075
- async function stashRuntimeOnlyZitadel(cwd) {
1076
- const source = join(cwd, ".zitadel");
1077
- const stash = join(dirname(cwd), `${`.${basename(cwd)}.zitadel-local-stash`}-${String(process.pid)}-${String(Date.now())}`);
1078
- await rename(source, stash);
1079
- return stash;
1080
- }
1081
- async function restoreRuntimeOnlyZitadel(cwd, stash) {
1082
- if (!stash) return;
1083
- const target = join(cwd, ".zitadel");
1084
- try {
1085
- await rename(stash, target);
1086
- await appendGitignoreEntry(cwd, ".zitadel/local/");
1087
- return;
1088
- } catch (error) {
1089
- if (!isErrno(error, "EEXIST")) throw error;
1090
- }
1091
- await mkdir(target, {
1092
- recursive: true,
1093
- mode: 448
1094
- });
1095
- await rename(join(stash, "local"), join(target, "local"));
1096
- await rm(stash, {
1097
- recursive: true,
1098
- force: true
1099
- });
1100
- await appendGitignoreEntry(cwd, ".zitadel/local/");
1101
- }
1102
- async function appendGitignoreEntry(cwd, entry) {
1103
- const path = join(cwd, ".gitignore");
1104
- let existing = "";
1105
- try {
1106
- existing = await readFile(path, "utf8");
1107
- } catch (error) {
1108
- if (!isErrno(error, "ENOENT")) throw error;
1109
- }
1110
- if (existing.split(/\r?\n/g).map((line) => line.trim()).includes(entry)) return;
1111
- const prefix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
1112
- await writeFile(path, `${existing}${prefix}${entry}\n`);
1113
- }
1114
- function isErrno(error, code) {
1115
- return typeof error === "object" && error !== null && "code" in error && error.code === code;
1116
- }
1117
- //#endregion
1118
- export { RENDERER_IDS as n, issuerFromPort as r, createOrca as t };
1119
-
1120
- //# sourceMappingURL=orca-BX4AhKLD.mjs.map