@zitadel/cli 0.1.0-alpha.17 → 0.1.0-alpha.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -107
- package/SKILLS.md +88 -12
- package/dist/branding-BXj028-W.mjs +79 -0
- package/dist/branding-BXj028-W.mjs.map +1 -0
- package/dist/claim-state-BTGInAaN.mjs +66 -0
- package/dist/claim-state-BTGInAaN.mjs.map +1 -0
- package/dist/commands/apply.mjs +6 -5
- package/dist/commands/apply.mjs.map +1 -1
- package/dist/commands/branding/eject.mjs +125 -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 +477 -37
- package/dist/commands/doctor.mjs.map +1 -1
- package/dist/commands/eject.mjs +3 -3
- package/dist/commands/logs.mjs +2 -2
- package/dist/commands/plan.mjs +6 -5
- package/dist/commands/plan.mjs.map +1 -1
- package/dist/commands/reset.mjs +2 -2
- package/dist/commands/schemas/list.mjs +8 -8
- package/dist/commands/schemas/list.mjs.map +1 -1
- package/dist/commands/setup.mjs +305 -36
- package/dist/commands/setup.mjs.map +1 -1
- package/dist/commands/start.mjs +3 -3
- package/dist/commands/status.mjs +21 -13
- package/dist/commands/status.mjs.map +1 -1
- package/dist/commands/stop.mjs +3 -3
- package/dist/commands/stop.mjs.map +1 -1
- package/dist/{docker-pGcTrDIY.mjs → docker-Ciz9N6uI.mjs} +2 -2
- package/dist/{docker-pGcTrDIY.mjs.map → docker-Ciz9N6uI.mjs.map} +1 -1
- package/dist/{environment-BQF7LeCz.mjs → environment-CuHKxtIO.mjs} +1 -1
- package/dist/{environment-BQF7LeCz.mjs.map → environment-CuHKxtIO.mjs.map} +1 -1
- package/dist/{journey-guidance-CYVpQszz.mjs → journey-guidance-CtQUfWar.mjs} +14 -3
- package/dist/{journey-guidance-CYVpQszz.mjs.map → journey-guidance-CtQUfWar.mjs.map} +1 -1
- package/dist/{oclif-BRdjVNne.mjs → oclif-CL6-M13m.mjs} +201 -43
- package/dist/oclif-CL6-M13m.mjs.map +1 -0
- package/dist/{orca-Dip6DPJ2.mjs → orca-BWB4zOdw.mjs} +749 -192
- package/dist/orca-BWB4zOdw.mjs.map +1 -0
- package/dist/{ports-V8JfiNgT.mjs → ports-Cu2Y7aXQ.mjs} +2 -2
- package/dist/{ports-V8JfiNgT.mjs.map → ports-Cu2Y7aXQ.mjs.map} +1 -1
- package/dist/{processes-BVqYsxT8.mjs → processes-Bn2WngOi.mjs} +1 -1
- package/dist/{processes-BVqYsxT8.mjs.map → processes-Bn2WngOi.mjs.map} +1 -1
- package/dist/{project-BGgv7Bba.mjs → project-yejJSfdJ.mjs} +57 -4
- package/dist/project-yejJSfdJ.mjs.map +1 -0
- package/dist/scaffold-manifest-CKNFLX9T.mjs +81 -0
- package/dist/scaffold-manifest-CKNFLX9T.mjs.map +1 -0
- package/dist/{sync-Cd3z7Q1R.mjs → sync-Cx2_4rXW.mjs} +99 -50
- package/dist/sync-Cx2_4rXW.mjs.map +1 -0
- package/dist/user-schema-W1OsXdpc.mjs +91 -0
- package/dist/user-schema-W1OsXdpc.mjs.map +1 -0
- package/oclif.manifest.json +227 -5
- package/package.json +8 -6
- package/dist/oclif-BRdjVNne.mjs.map +0 -1
- package/dist/orca-Dip6DPJ2.mjs.map +0 -1
- package/dist/project-BGgv7Bba.mjs.map +0 -1
- package/dist/sync-Cd3z7Q1R.mjs.map +0 -1
- package/dist/user-schema-DDz5-lX5.mjs +0 -13
- package/dist/user-schema-DDz5-lX5.mjs.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { k as publicCliCommand } from "./oclif-CL6-M13m.mjs";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
//#region src/lib/local-server/docker-guidance.ts
|
|
4
4
|
function dockerUnavailableMessage(error) {
|
|
@@ -113,4 +113,4 @@ function isLoopback(host) {
|
|
|
113
113
|
//#endregion
|
|
114
114
|
export { dockerRuntimeGuidance as n, dockerUnavailableMessage as r, listenersForPort as t };
|
|
115
115
|
|
|
116
|
-
//# sourceMappingURL=ports-
|
|
116
|
+
//# sourceMappingURL=ports-Cu2Y7aXQ.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ports-
|
|
1
|
+
{"version":3,"file":"ports-Cu2Y7aXQ.mjs","names":[],"sources":["../src/lib/local-server/docker-guidance.ts","../src/lib/prober/ports.ts"],"sourcesContent":["import { publicCliCommand } from \"../public-cli\";\n\nexport type DockerRuntimeGuidance = {\n hint: string;\n nextActions: string[];\n nextCommands: string[];\n};\n\nexport function dockerUnavailableMessage(error: unknown): string {\n const detail = error instanceof Error ? error.message : String(error);\n const suffix = detail.trim() ? ` (${detail.trim()})` : \"\";\n return `Docker is not reachable${suffix}; managed local runtime commands need Docker, but remote or cloud setup can continue with --server <url>.`;\n}\n\nexport function dockerRuntimeGuidance(\n retryCommand: \"doctor\" | \"start\",\n cliVersion: string,\n): DockerRuntimeGuidance {\n return {\n hint:\n \"Docker is required only for the managed local runtime (`zitadel start` and `--server local`). Start Docker, then retry; or target a remote server with `--server <url>`.\",\n nextActions: [\n \"Start Docker Desktop, Docker Engine, or a Docker-compatible runtime such as Colima.\",\n `Run \\`docker version\\`, then rerun \\`${publicCliCommand(retryCommand, cliVersion)}\\`.`,\n \"If you are not using the managed local runtime, run setup with `--server <url>` instead.\",\n ],\n nextCommands: [\"docker version\", publicCliCommand(retryCommand, cliVersion)],\n };\n}\n","import { execFile } from \"node:child_process\";\n\nexport type TcpListener = Readonly<{\n address: string;\n command?: string;\n host: string;\n pid?: number;\n port: number;\n}>;\n\n/**\n * Enumerate TCP LISTEN sockets on loopback/wildcard interfaces (`127.0.0.1`,\n * `::1`, or the wildcard `*`) with enough metadata for CLI diagnostics.\n * Spawns `lsof -iTCP -sTCP:LISTEN -P -n -F pcn` and parses its\n * machine-readable output.\n *\n * Never throws. Returns `[]` whenever lsof is unavailable (e.g. Windows,\n * unusual PATH), the spawn errors, exits non-zero, or the call exceeds\n * `timeoutMs` (default 1000ms). The caller treats an empty list the same as\n * \"no visible listeners.\"\n */\nexport async function listListeningTcpListeners(opts?: {\n readonly timeoutMs?: number;\n}): Promise<ReadonlyArray<TcpListener>> {\n const timeoutMs = opts?.timeoutMs ?? 1000;\n try {\n const stdout = await runLsof(timeoutMs);\n return parseLsofListeners(stdout);\n } catch {\n return [];\n }\n}\n\nexport async function listenersForPort(\n port: number,\n opts?: { readonly timeoutMs?: number },\n): Promise<ReadonlyArray<TcpListener>> {\n return (await listListeningTcpListeners(opts)).filter((listener) => listener.port === port);\n}\n\n/**\n * Spawn `lsof` with the canned argv and resolve to its stdout. Hand-rolled\n * rather than `util.promisify(execFile)` because the latter resolves with a\n * `{stdout, stderr}` object via its custom-promisify symbol — a heavier shape\n * to mock and worse to read at the call site, where we only ever want stdout.\n */\nfunction runLsof(timeoutMs: number): Promise<string> {\n return new Promise<string>((resolve, reject) => {\n execFile(\n \"lsof\",\n [\"-iTCP\", \"-sTCP:LISTEN\", \"-P\", \"-n\", \"-F\", \"pcn\"],\n { timeout: timeoutMs, encoding: \"utf8\" },\n (err, stdout) => {\n if (err) {\n reject(err);\n return;\n }\n resolve(stdout);\n },\n );\n });\n}\n\n/**\n * Parse the pid (`p`), command (`c`), and name/address (`n`) records emitted by\n * `lsof -F pcn`. Address records end in `:<port>` (e.g. `n*:8080`,\n * `n127.0.0.1:3000`, `n[::1]:5050`). Only loopback/wildcard hosts are kept;\n * external interface bindings are ignored.\n */\nfunction parseLsofListeners(stdout: string): ReadonlyArray<TcpListener> {\n const listeners: TcpListener[] = [];\n let pid: number | undefined;\n let command: string | undefined;\n for (const line of stdout.split(/\\r?\\n/)) {\n if (line.startsWith(\"p\")) {\n const parsed = Number.parseInt(line.slice(1), 10);\n pid = Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;\n command = undefined;\n continue;\n }\n if (line.startsWith(\"c\")) {\n command = line.slice(1) || undefined;\n continue;\n }\n if (!line.startsWith(\"n\")) {\n continue;\n }\n const address = line.slice(1);\n const colon = address.lastIndexOf(\":\");\n if (colon < 0) {\n continue;\n }\n const host = address.slice(0, colon);\n const portStr = address.slice(colon + 1);\n if (!isLoopback(host)) {\n continue;\n }\n const port = Number.parseInt(portStr, 10);\n if (Number.isFinite(port) && port > 0 && port < 65536) {\n listeners.push({ address, command, host, pid, port });\n }\n }\n return listeners.sort((a, b) => a.port - b.port || (a.pid ?? 0) - (b.pid ?? 0));\n}\n\n/** Recognised loopback host strings as emitted by `lsof -F n`. */\nfunction isLoopback(host: string): boolean {\n return host === \"*\" || host === \"127.0.0.1\" || host === \"[::1]\" || host === \"::1\";\n}\n"],"mappings":";;;AAQA,SAAgB,yBAAyB,OAAwB;CAC/D,MAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAErE,QAAO,0BADQ,OAAO,MAAM,GAAG,KAAK,OAAO,MAAM,CAAC,KAAK,GACf;;AAG1C,SAAgB,sBACd,cACA,YACuB;AACvB,QAAO;EACL,MACE;EACF,aAAa;GACX;GACA,wCAAwC,iBAAiB,cAAc,WAAW,CAAC;GACnF;GACD;EACD,cAAc,CAAC,kBAAkB,iBAAiB,cAAc,WAAW,CAAC;EAC7E;;;;;;;;;;;;;;;ACNH,eAAsB,0BAA0B,MAER;CACtC,MAAM,YAAY,MAAM,aAAa;AACrC,KAAI;AAEF,SAAO,mBAAmB,MADL,QAAQ,UAAU,CACN;SAC3B;AACN,SAAO,EAAE;;;AAIb,eAAsB,iBACpB,MACA,MACqC;AACrC,SAAQ,MAAM,0BAA0B,KAAK,EAAE,QAAQ,aAAa,SAAS,SAAS,KAAK;;;;;;;;AAS7F,SAAS,QAAQ,WAAoC;AACnD,QAAO,IAAI,SAAiB,SAAS,WAAW;AAC9C,WACE,QACA;GAAC;GAAS;GAAgB;GAAM;GAAM;GAAM;GAAM,EAClD;GAAE,SAAS;GAAW,UAAU;GAAQ,GACvC,KAAK,WAAW;AACf,OAAI,KAAK;AACP,WAAO,IAAI;AACX;;AAEF,WAAQ,OAAO;IAElB;GACD;;;;;;;;AASJ,SAAS,mBAAmB,QAA4C;CACtE,MAAM,YAA2B,EAAE;CACnC,IAAI;CACJ,IAAI;AACJ,MAAK,MAAM,QAAQ,OAAO,MAAM,QAAQ,EAAE;AACxC,MAAI,KAAK,WAAW,IAAI,EAAE;GACxB,MAAM,SAAS,OAAO,SAAS,KAAK,MAAM,EAAE,EAAE,GAAG;AACjD,SAAM,OAAO,SAAS,OAAO,IAAI,SAAS,IAAI,SAAS,KAAA;AACvD,aAAU,KAAA;AACV;;AAEF,MAAI,KAAK,WAAW,IAAI,EAAE;AACxB,aAAU,KAAK,MAAM,EAAE,IAAI,KAAA;AAC3B;;AAEF,MAAI,CAAC,KAAK,WAAW,IAAI,CACvB;EAEF,MAAM,UAAU,KAAK,MAAM,EAAE;EAC7B,MAAM,QAAQ,QAAQ,YAAY,IAAI;AACtC,MAAI,QAAQ,EACV;EAEF,MAAM,OAAO,QAAQ,MAAM,GAAG,MAAM;EACpC,MAAM,UAAU,QAAQ,MAAM,QAAQ,EAAE;AACxC,MAAI,CAAC,WAAW,KAAK,CACnB;EAEF,MAAM,OAAO,OAAO,SAAS,SAAS,GAAG;AACzC,MAAI,OAAO,SAAS,KAAK,IAAI,OAAO,KAAK,OAAO,MAC9C,WAAU,KAAK;GAAE;GAAS;GAAS;GAAM;GAAK;GAAM,CAAC;;AAGzD,QAAO,UAAU,MAAM,GAAG,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,MAAM,EAAE,OAAO,GAAG;;;AAIjF,SAAS,WAAW,MAAuB;AACzC,QAAO,SAAS,OAAO,SAAS,eAAe,SAAS,WAAW,SAAS"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"processes-
|
|
1
|
+
{"version":3,"file":"processes-Bn2WngOi.mjs","names":[],"sources":["../src/lib/local-server/processes.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { basename } from \"node:path\";\n\nexport type ManagedRuntimeProcess = Readonly<{\n command: string;\n data_dir?: string;\n kind: \"server-binary\" | \"server-wrapper\";\n pid: number;\n ppid?: number;\n}>;\n\nexport type ManagedRuntimeDiscovery = Readonly<{\n error?: string;\n processes: ReadonlyArray<ManagedRuntimeProcess>;\n supported: boolean;\n}>;\n\ntype ProcessRow = Readonly<{\n command: string;\n pid: number;\n ppid?: number;\n}>;\n\nexport async function discoverManagedRuntimeProcesses(opts?: {\n readonly timeoutMs?: number;\n}): Promise<ManagedRuntimeDiscovery> {\n if (process.platform === \"win32\") {\n return { supported: false, processes: [], error: \"process sweep is not available on Windows\" };\n }\n\n let rows: ReadonlyArray<ProcessRow>;\n try {\n rows = parsePsRows(await runPs([\"axo\", \"pid=,ppid=,command=\"], opts?.timeoutMs ?? 1000));\n } catch (error) {\n return { supported: false, processes: [], error: errorMessage(error) };\n }\n\n const candidates = rows.filter((row) => isRuntimeCandidate(row.command));\n const hydrated = await Promise.all(\n candidates.map(async (row) => ({\n ...row,\n command: await hydrateCommand(row.pid, row.command, opts?.timeoutMs ?? 1000),\n })),\n );\n const processes = hydrated\n .map(toManagedRuntimeProcess)\n .filter((candidate): candidate is ManagedRuntimeProcess => candidate !== undefined);\n return { supported: true, processes: uniqueByPid(processes) };\n}\n\nfunction runPs(args: string[], timeoutMs: number): Promise<string> {\n return new Promise<string>((resolve, reject) => {\n execFile(\"ps\", args, { timeout: timeoutMs, encoding: \"utf8\" }, (err, stdout) => {\n if (err) {\n reject(err);\n return;\n }\n resolve(stdout);\n });\n });\n}\n\nasync function hydrateCommand(pid: number, fallback: string, timeoutMs: number): Promise<string> {\n try {\n const hydrated = await runPs([\"eww\", \"-p\", String(pid), \"-o\", \"command=\"], timeoutMs);\n return hydrated.trim() || fallback;\n } catch {\n return fallback;\n }\n}\n\nfunction parsePsRows(stdout: string): ReadonlyArray<ProcessRow> {\n const rows: ProcessRow[] = [];\n for (const line of stdout.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (!trimmed) {\n continue;\n }\n const match = trimmed.match(/^(\\d+)\\s+(\\d+)\\s+(.+)$/);\n if (!match) {\n continue;\n }\n rows.push({\n pid: Number(match[1]),\n ppid: Number(match[2]),\n command: match[3] ?? \"\",\n });\n }\n return rows;\n}\n\nfunction toManagedRuntimeProcess(row: ProcessRow): ManagedRuntimeProcess | undefined {\n const dataDir = extractDataDir(row.command);\n const wrapper = isServerWrapper(row.command);\n const binary = isServerBinary(row.command);\n if (!wrapper && !binary) {\n return undefined;\n }\n if (!wrapper && !isCliLocalDataDir(dataDir)) {\n return undefined;\n }\n return {\n pid: row.pid,\n ppid: row.ppid,\n command: stripEnvironmentNoise(row.command),\n kind: wrapper ? \"server-wrapper\" : \"server-binary\",\n ...(dataDir ? { data_dir: dataDir } : {}),\n };\n}\n\nfunction isRuntimeCandidate(command: string): boolean {\n return (\n isServerWrapper(command) ||\n isServerBinary(command) ||\n command.includes(\"NEXTGEN_SERVER_DATA_DIR=\") ||\n basename(command.split(/\\s+/)[0] ?? \"\") === \"nextgen\"\n );\n}\n\nfunction isServerWrapper(command: string): boolean {\n return command.includes(\"zitadel-server.js\");\n}\n\nfunction isServerBinary(command: string): boolean {\n if (/@zitadel\\/server-[^/\\s]+\\/bin\\/nextgen(?:\\.exe)?(?:\\s|$)/.test(command)) {\n return true;\n }\n return (\n isCliLocalDataDir(extractDataDir(command)) &&\n commandTokens(command).some((token) => basename(token) === \"nextgen\")\n );\n}\n\nfunction extractDataDir(command: string): string | undefined {\n const match = command.match(/NEXTGEN_SERVER_DATA_DIR=(\"[^\"]+\"|'[^']+'|[^\\s]+)/);\n const value = match?.[1];\n if (!value) {\n return undefined;\n }\n return value.replace(/^[\"']|[\"']$/g, \"\");\n}\n\nfunction isCliLocalDataDir(path: string | undefined): boolean {\n return Boolean(path && /(?:^|\\/)\\.zitadel\\/local\\/nextgen-data\\/?$/.test(path));\n}\n\nfunction stripEnvironmentNoise(command: string): string {\n return command\n .replace(/(^|\\s)[A-Z_][A-Z0-9_]*=(?:\"[^\"]+\"|'[^']+'|[^\\s]+)/g, \" \")\n .replace(/\\s{2,}/g, \" \")\n .trim();\n}\n\nfunction commandTokens(command: string): string[] {\n return command.split(/\\s+/).filter((token) => !/^[A-Z_][A-Z0-9_]*=/.test(token));\n}\n\nfunction uniqueByPid(\n processes: ReadonlyArray<ManagedRuntimeProcess>,\n): ReadonlyArray<ManagedRuntimeProcess> {\n const byPid = new Map<number, ManagedRuntimeProcess>();\n for (const processInfo of processes) {\n byPid.set(processInfo.pid, processInfo);\n }\n return [...byPid.values()].sort((a, b) => a.pid - b.pid);\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readFile, stat } from "node:fs/promises";
|
|
1
|
+
import { C as parseJsonObject, E as ZitadelError, S as isObject, T as stableStringify, n as DEFAULT_SERVER } from "./oclif-CL6-M13m.mjs";
|
|
2
|
+
import { chmod, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
//#region src/lib/project.ts
|
|
5
5
|
/**
|
|
@@ -62,6 +62,25 @@ async function readZitadelSecret(cwd) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
+
* Writes `.zitadel/secret` back after a command mutated it (today only
|
|
66
|
+
* `zitadel claim`, which records `claimed_at` and `team_id`).
|
|
67
|
+
*
|
|
68
|
+
* Atomic by construction: the bytes land in a sibling temp file that is
|
|
69
|
+
* `chmod`ed and then `rename`d over the target, so a crash mid-write can never
|
|
70
|
+
* leave a truncated or world-readable credentials file — the same
|
|
71
|
+
* temp-then-rename the file-writer uses for managed files. Serialised with
|
|
72
|
+
* {@link stableStringify} at mode `0600` to match exactly how setup's patcher
|
|
73
|
+
* first wrote it, so a claim does not churn the file's formatting or loosen
|
|
74
|
+
* the permissions `doctor` asserts.
|
|
75
|
+
*/
|
|
76
|
+
async function writeZitadelSecret(cwd, secret) {
|
|
77
|
+
const path = join(cwd, ".zitadel/secret");
|
|
78
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
79
|
+
await writeFile(tmp, `${stableStringify(secret)}\n`, { mode: 384 });
|
|
80
|
+
await chmod(tmp, 384).catch(() => void 0);
|
|
81
|
+
await rename(tmp, path);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
65
84
|
* Reads the configured renderer id from a parsed `zitadel.json`, normalising the
|
|
66
85
|
* legacy `default` alias to `react` and falling back to `react` when unset. The
|
|
67
86
|
* value is validated downstream by `getRenderer`, so callers need not re-check.
|
|
@@ -71,6 +90,40 @@ function readRendererId(config) {
|
|
|
71
90
|
const value = branding && typeof branding.renderer === "string" ? branding.renderer : "react";
|
|
72
91
|
return value === "default" ? "react" : value;
|
|
73
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Reads the recorded sign-in preset from a parsed `zitadel.json`, if present.
|
|
95
|
+
* Setup persists it (see the patcher's `projectConfig`); absent means
|
|
96
|
+
* password-first per the setup default.
|
|
97
|
+
*/
|
|
98
|
+
function readPreset(config) {
|
|
99
|
+
return typeof config.preset === "string" ? config.preset : void 0;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Reads the recorded use case from a parsed `zitadel.json`, if present.
|
|
103
|
+
* Guidance-only, like the field itself — behavior lives in the generated
|
|
104
|
+
* schema/flow files.
|
|
105
|
+
*/
|
|
106
|
+
function readUseCase(config) {
|
|
107
|
+
return typeof config.useCase === "string" ? config.useCase : void 0;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Reads the top-level `server` origin from a parsed `zitadel.json`, falling
|
|
111
|
+
* back to {@link DEFAULT_SERVER} when the key is absent.
|
|
112
|
+
*
|
|
113
|
+
* Top-level only, unlike `resolveServer`, which prefers the selected
|
|
114
|
+
* environment's `server` before falling back here. That is the point: an
|
|
115
|
+
* environment override says where one invocation should talk, not where the
|
|
116
|
+
* project lives. No practical difference today, since setup always writes a
|
|
117
|
+
* concrete top-level `server`.
|
|
118
|
+
*
|
|
119
|
+
* This is the server the project actually lives on, which is not always the one
|
|
120
|
+
* a command is talking to: `doctor` pins its own source to the local runtime URL
|
|
121
|
+
* and `status --server local` overrides it. Anything reasoning about the
|
|
122
|
+
* project itself (rather than about this invocation) has to read it from here.
|
|
123
|
+
*/
|
|
124
|
+
function readProjectServer(config) {
|
|
125
|
+
return typeof config.server === "string" ? config.server : DEFAULT_SERVER;
|
|
126
|
+
}
|
|
74
127
|
/** Reads `environments.development.issuer` from a parsed `zitadel.json`, if present. */
|
|
75
128
|
function readDevelopmentIssuer(config) {
|
|
76
129
|
if (isObject(config.environments) && isObject(config.environments.development)) {
|
|
@@ -82,6 +135,6 @@ function isNotFound(error) {
|
|
|
82
135
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
83
136
|
}
|
|
84
137
|
//#endregion
|
|
85
|
-
export {
|
|
138
|
+
export { readProjectServer as a, readZitadelConfig as c, readPreset as i, readZitadelSecret as l, hasZitadelSecret as n, readRendererId as o, readDevelopmentIssuer as r, readUseCase as s, hasZitadelConfig as t, writeZitadelSecret as u };
|
|
86
139
|
|
|
87
|
-
//# sourceMappingURL=project-
|
|
140
|
+
//# sourceMappingURL=project-yejJSfdJ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-yejJSfdJ.mjs","names":[],"sources":["../src/lib/project.ts"],"sourcesContent":["import { chmod, readFile, rename, stat, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { ZitadelError } from \"./errors\";\nimport { isObject, parseJsonObject, stableStringify } from \"./json\";\nimport { DEFAULT_SERVER } from \"./server\";\n\n/**\n * Reports whether `cwd` has already been initialized, i.e. a committed\n * `zitadel.json` exists. Used to decide whether setup should run or skip.\n */\nexport async function hasZitadelConfig(cwd: string): Promise<boolean> {\n return exists(join(cwd, \"zitadel.json\"));\n}\n\n/**\n * Reports whether local secret material (`.zitadel/secret`) is present. Gates\n * commands that need credentials, and signals that secrets were already pulled.\n */\nexport async function hasZitadelSecret(cwd: string): Promise<boolean> {\n return exists(join(cwd, \".zitadel/secret\"));\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch (error) {\n if (isNotFound(error)) {\n return false;\n }\n throw error;\n }\n}\n\n/**\n * Shape of the project secret persisted at `.zitadel/secret`. Holds the\n * project identity plus the credentials used to talk to the platform in\n * preview and production. Validated structurally by {@link readZitadelSecret}.\n */\nexport type ZitadelSecret = {\n project_id: string;\n project_secret: string;\n preview_secret: string;\n preview_origins: string[];\n created_at: string;\n /**\n * When the project was attached to an owning team, recorded by\n * `zitadel claim`. Absent until then. The durable record lives on the\n * platform (the team's project grant); this is a local cache so commands can\n * answer \"is this already attached?\" without a round trip.\n */\n claimed_at?: string;\n /** The team that owns the project, recorded alongside {@link claimed_at}. */\n team_id?: string;\n};\n\n/**\n * Reads and parses `zitadel.json` into a plain object. Translates a missing\n * file into an actionable `E_VALIDATION` error pointing at `zitadel setup`;\n * other errors (e.g. malformed JSON) propagate unchanged.\n */\nexport async function readZitadelConfig(cwd: string): Promise<Record<string, unknown>> {\n try {\n return parseJsonObject(await readFile(join(cwd, \"zitadel.json\"), \"utf8\"), \"zitadel.json\");\n } catch (error) {\n if (isNotFound(error)) {\n throw new ZitadelError(\"E_VALIDATION\", \"zitadel.json was not found\", {\n hint: \"Run `zitadel setup` first.\",\n nextCommands: [\"zitadel setup\"],\n });\n }\n throw error;\n }\n}\n\n/**\n * Reads, parses, and structurally validates `.zitadel/secret`, returning it\n * as a {@link ZitadelSecret}. A missing file becomes an actionable\n * `E_VALIDATION` error pointing at `zitadel setup` / `zitadel doctor --fix`;\n * a present-but-incomplete file throws so callers never proceed with partial\n * credentials.\n */\nexport async function readZitadelSecret(cwd: string): Promise<ZitadelSecret> {\n try {\n const secret = parseJsonObject(\n await readFile(join(cwd, \".zitadel/secret\"), \"utf8\"),\n \".zitadel/secret\",\n );\n if (\n typeof secret.project_id !== \"string\" ||\n typeof secret.project_secret !== \"string\" ||\n typeof secret.preview_secret !== \"string\" ||\n !Array.isArray(secret.preview_origins)\n ) {\n throw new Error(\".zitadel/secret is missing required fields\");\n }\n return secret as ZitadelSecret;\n } catch (error) {\n if (isNotFound(error)) {\n throw new ZitadelError(\"E_VALIDATION\", \".zitadel/secret was not found\", {\n hint: \"Run `zitadel setup` first, or restore the project secret with `zitadel doctor --fix`.\",\n nextCommands: [\"zitadel setup\", \"zitadel doctor --fix\"],\n });\n }\n throw error;\n }\n}\n\n/**\n * Writes `.zitadel/secret` back after a command mutated it (today only\n * `zitadel claim`, which records `claimed_at` and `team_id`).\n *\n * Atomic by construction: the bytes land in a sibling temp file that is\n * `chmod`ed and then `rename`d over the target, so a crash mid-write can never\n * leave a truncated or world-readable credentials file — the same\n * temp-then-rename the file-writer uses for managed files. Serialised with\n * {@link stableStringify} at mode `0600` to match exactly how setup's patcher\n * first wrote it, so a claim does not churn the file's formatting or loosen\n * the permissions `doctor` asserts.\n */\nexport async function writeZitadelSecret(cwd: string, secret: ZitadelSecret): Promise<void> {\n const path = join(cwd, \".zitadel/secret\");\n const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;\n await writeFile(tmp, `${stableStringify(secret)}\\n`, { mode: 0o600 });\n // `writeFile`'s mode is masked by the process umask, so re-assert it.\n await chmod(tmp, 0o600).catch(() => undefined);\n await rename(tmp, path);\n}\n\n/**\n * Reads the configured renderer id from a parsed `zitadel.json`, normalising the\n * legacy `default` alias to `react` and falling back to `react` when unset. The\n * value is validated downstream by `getRenderer`, so callers need not re-check.\n */\nexport function readRendererId(config: Record<string, unknown>): string {\n const branding = isObject(config.branding) ? config.branding : undefined;\n const value = branding && typeof branding.renderer === \"string\" ? branding.renderer : \"react\";\n return value === \"default\" ? \"react\" : value;\n}\n\n/**\n * Reads the recorded sign-in preset from a parsed `zitadel.json`, if present.\n * Setup persists it (see the patcher's `projectConfig`); absent means\n * password-first per the setup default.\n */\nexport function readPreset(config: Record<string, unknown>): string | undefined {\n return typeof config.preset === \"string\" ? config.preset : undefined;\n}\n\n/**\n * Reads the recorded use case from a parsed `zitadel.json`, if present.\n * Guidance-only, like the field itself — behavior lives in the generated\n * schema/flow files.\n */\nexport function readUseCase(config: Record<string, unknown>): string | undefined {\n return typeof config.useCase === \"string\" ? config.useCase : undefined;\n}\n\n/**\n * Reads the top-level `server` origin from a parsed `zitadel.json`, falling\n * back to {@link DEFAULT_SERVER} when the key is absent.\n *\n * Top-level only, unlike `resolveServer`, which prefers the selected\n * environment's `server` before falling back here. That is the point: an\n * environment override says where one invocation should talk, not where the\n * project lives. No practical difference today, since setup always writes a\n * concrete top-level `server`.\n *\n * This is the server the project actually lives on, which is not always the one\n * a command is talking to: `doctor` pins its own source to the local runtime URL\n * and `status --server local` overrides it. Anything reasoning about the\n * project itself (rather than about this invocation) has to read it from here.\n */\nexport function readProjectServer(config: Record<string, unknown>): string {\n return typeof config.server === \"string\" ? config.server : DEFAULT_SERVER;\n}\n\n/** Reads `environments.development.issuer` from a parsed `zitadel.json`, if present. */\nexport function readDevelopmentIssuer(config: Record<string, unknown>): string | undefined {\n if (isObject(config.environments) && isObject(config.environments.development)) {\n const issuer = config.environments.development.issuer;\n return typeof issuer === \"string\" ? issuer : undefined;\n }\n return undefined;\n}\n\nfunction isNotFound(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n (error as { code?: string }).code === \"ENOENT\"\n );\n}\n"],"mappings":";;;;;;;;AAWA,eAAsB,iBAAiB,KAA+B;AACpE,QAAO,OAAO,KAAK,KAAK,eAAe,CAAC;;;;;;AAO1C,eAAsB,iBAAiB,KAA+B;AACpE,QAAO,OAAO,KAAK,KAAK,kBAAkB,CAAC;;AAG7C,eAAe,OAAO,MAAgC;AACpD,KAAI;AACF,QAAM,KAAK,KAAK;AAChB,SAAO;UACA,OAAO;AACd,MAAI,WAAW,MAAM,CACnB,QAAO;AAET,QAAM;;;;;;;;AA+BV,eAAsB,kBAAkB,KAA+C;AACrF,KAAI;AACF,SAAO,gBAAgB,MAAM,SAAS,KAAK,KAAK,eAAe,EAAE,OAAO,EAAE,eAAe;UAClF,OAAO;AACd,MAAI,WAAW,MAAM,CACnB,OAAM,IAAI,aAAa,gBAAgB,8BAA8B;GACnE,MAAM;GACN,cAAc,CAAC,gBAAgB;GAChC,CAAC;AAEJ,QAAM;;;;;;;;;;AAWV,eAAsB,kBAAkB,KAAqC;AAC3E,KAAI;EACF,MAAM,SAAS,gBACb,MAAM,SAAS,KAAK,KAAK,kBAAkB,EAAE,OAAO,EACpD,kBACD;AACD,MACE,OAAO,OAAO,eAAe,YAC7B,OAAO,OAAO,mBAAmB,YACjC,OAAO,OAAO,mBAAmB,YACjC,CAAC,MAAM,QAAQ,OAAO,gBAAgB,CAEtC,OAAM,IAAI,MAAM,6CAA6C;AAE/D,SAAO;UACA,OAAO;AACd,MAAI,WAAW,MAAM,CACnB,OAAM,IAAI,aAAa,gBAAgB,iCAAiC;GACtE,MAAM;GACN,cAAc,CAAC,iBAAiB,uBAAuB;GACxD,CAAC;AAEJ,QAAM;;;;;;;;;;;;;;;AAgBV,eAAsB,mBAAmB,KAAa,QAAsC;CAC1F,MAAM,OAAO,KAAK,KAAK,kBAAkB;CACzC,MAAM,MAAM,GAAG,KAAK,OAAO,QAAQ,IAAI,GAAG,KAAK,KAAK;AACpD,OAAM,UAAU,KAAK,GAAG,gBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,KAAO,CAAC;AAErE,OAAM,MAAM,KAAK,IAAM,CAAC,YAAY,KAAA,EAAU;AAC9C,OAAM,OAAO,KAAK,KAAK;;;;;;;AAQzB,SAAgB,eAAe,QAAyC;CACtE,MAAM,WAAW,SAAS,OAAO,SAAS,GAAG,OAAO,WAAW,KAAA;CAC/D,MAAM,QAAQ,YAAY,OAAO,SAAS,aAAa,WAAW,SAAS,WAAW;AACtF,QAAO,UAAU,YAAY,UAAU;;;;;;;AAQzC,SAAgB,WAAW,QAAqD;AAC9E,QAAO,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS,KAAA;;;;;;;AAQ7D,SAAgB,YAAY,QAAqD;AAC/E,QAAO,OAAO,OAAO,YAAY,WAAW,OAAO,UAAU,KAAA;;;;;;;;;;;;;;;;;AAkB/D,SAAgB,kBAAkB,QAAyC;AACzE,QAAO,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;;;AAI7D,SAAgB,sBAAsB,QAAqD;AACzF,KAAI,SAAS,OAAO,aAAa,IAAI,SAAS,OAAO,aAAa,YAAY,EAAE;EAC9E,MAAM,SAAS,OAAO,aAAa,YAAY;AAC/C,SAAO,OAAO,WAAW,WAAW,SAAS,KAAA;;;AAKjD,SAAS,WAAW,OAAyB;AAC3C,QACE,OAAO,UAAU,YACjB,UAAU,QACV,UAAU,SACT,MAA4B,SAAS"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { S as isObject } from "./oclif-CL6-M13m.mjs";
|
|
2
|
+
import { i as updateScaffold, n as readState } from "./user-schema-W1OsXdpc.mjs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
//#region src/lib/scaffold-manifest.ts
|
|
7
|
+
/**
|
|
8
|
+
* Content hash of one scaffolded file as recorded in the scaffold manifest.
|
|
9
|
+
* Deliberately hashes the raw text (not a parsed structure): the manifest
|
|
10
|
+
* answers "are these still the bytes setup wrote?", nothing more. The sync
|
|
11
|
+
* loop's hashers work on JSON objects and serve a different contract.
|
|
12
|
+
*/
|
|
13
|
+
function hashScaffoldFile(contents) {
|
|
14
|
+
return createHash("sha256").update(contents).digest("hex");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Read the scaffold manifest from `.zitadel/state.json`, or `undefined` when
|
|
18
|
+
* the file, the section, its `files` record, or any file entry is missing or
|
|
19
|
+
* malformed (each entry needs a string `hash` and a known `class`).
|
|
20
|
+
* `undefined` sends the doctor managed-files check into its template-derived
|
|
21
|
+
* fallback mode, so a corrupt or future-shaped manifest degrades rather than
|
|
22
|
+
* breaks.
|
|
23
|
+
*/
|
|
24
|
+
async function readScaffoldManifest(cwd) {
|
|
25
|
+
try {
|
|
26
|
+
const scaffold = (await readState(cwd)).scaffold;
|
|
27
|
+
if (!isObject(scaffold)) return;
|
|
28
|
+
if (!isObject(scaffold.files)) return;
|
|
29
|
+
for (const entry of Object.values(scaffold.files)) {
|
|
30
|
+
if (!isObject(entry)) return;
|
|
31
|
+
if (typeof entry.hash !== "string") return;
|
|
32
|
+
if (entry.class !== "infrastructure" && entry.class !== "presentation") return;
|
|
33
|
+
}
|
|
34
|
+
if (scaffold.posture !== void 0 && scaffold.posture !== "page" && scaffold.posture !== "widget") return;
|
|
35
|
+
return scaffold;
|
|
36
|
+
} catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Record which managed app files `zitadel setup` actually wrote (or found
|
|
42
|
+
* already matching), keyed by project-root-relative posix path with the
|
|
43
|
+
* content hash and ownership class. Only marked files that the scaffold run
|
|
44
|
+
* touched are recorded — a conditional file whose op never ran (e.g. the
|
|
45
|
+
* framework home page on a pre-existing app) is simply absent, which is what
|
|
46
|
+
* lets `doctor` later distinguish "CLI wrote this and it vanished" from "the
|
|
47
|
+
* CLI never owned this".
|
|
48
|
+
*/
|
|
49
|
+
async function writeScaffoldManifest(options) {
|
|
50
|
+
const { cwd, actions, written, scaffoldedFramework, devPort, posture } = options;
|
|
51
|
+
const touched = new Set(written.map((path) => relativePosix(cwd, path)));
|
|
52
|
+
const files = {};
|
|
53
|
+
for (const path of actions.markedFiles) {
|
|
54
|
+
const key = relativePosix(cwd, path);
|
|
55
|
+
if (!touched.has(key)) continue;
|
|
56
|
+
let contents;
|
|
57
|
+
try {
|
|
58
|
+
contents = await readFile(join(cwd, key), "utf8");
|
|
59
|
+
} catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
files[key] = {
|
|
63
|
+
hash: hashScaffoldFile(contents),
|
|
64
|
+
class: actions.fileClasses?.[path] ?? "presentation"
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
await updateScaffold(cwd, {
|
|
68
|
+
files,
|
|
69
|
+
...scaffoldedFramework ? { scaffolded_framework: true } : {},
|
|
70
|
+
...typeof devPort === "number" ? { dev_port: devPort } : {},
|
|
71
|
+
...posture === void 0 ? {} : { posture }
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** Normalizes an absolute or relative path to a cwd-relative posix key. */
|
|
75
|
+
function relativePosix(cwd, path) {
|
|
76
|
+
return (isAbsolute(path) ? relative(cwd, path) : path).split(sep).join("/");
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
export { readScaffoldManifest as n, writeScaffoldManifest as r, hashScaffoldFile as t };
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=scaffold-manifest-CKNFLX9T.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold-manifest-CKNFLX9T.mjs","names":[],"sources":["../src/lib/scaffold-manifest.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport { join, isAbsolute, relative, sep } from \"node:path\";\n\nimport { isObject } from \"./json\";\nimport type { EjectActions } from \"./orca/patchers/types\";\nimport { readState, updateScaffold } from \"./sync/state\";\nimport type { ScaffoldManifest, ScaffoldPosture } from \"./sync/types\";\n\n/**\n * Content hash of one scaffolded file as recorded in the scaffold manifest.\n * Deliberately hashes the raw text (not a parsed structure): the manifest\n * answers \"are these still the bytes setup wrote?\", nothing more. The sync\n * loop's hashers work on JSON objects and serve a different contract.\n */\nexport function hashScaffoldFile(contents: string): string {\n return createHash(\"sha256\").update(contents).digest(\"hex\");\n}\n\n/**\n * Read the scaffold manifest from `.zitadel/state.json`, or `undefined` when\n * the file, the section, its `files` record, or any file entry is missing or\n * malformed (each entry needs a string `hash` and a known `class`).\n * `undefined` sends the doctor managed-files check into its template-derived\n * fallback mode, so a corrupt or future-shaped manifest degrades rather than\n * breaks.\n */\nexport async function readScaffoldManifest(cwd: string): Promise<ScaffoldManifest | undefined> {\n try {\n const state = await readState(cwd);\n const scaffold = state.scaffold;\n // isObject rejects arrays too — `typeof [] === \"object\"`, and an array\n // `files` would otherwise enter manifest mode as \"0 tracked\".\n if (!isObject(scaffold)) {\n return undefined;\n }\n if (!isObject(scaffold.files)) {\n return undefined;\n }\n for (const entry of Object.values(scaffold.files)) {\n if (!isObject(entry)) {\n return undefined;\n }\n if (typeof entry.hash !== \"string\") {\n return undefined;\n }\n if (entry.class !== \"infrastructure\" && entry.class !== \"presentation\") {\n return undefined;\n }\n }\n // A posture value this CLI does not know is a future-shaped manifest —\n // degrade like any other malformed section rather than restoring a\n // posture the templates cannot render.\n if (\n scaffold.posture !== undefined &&\n scaffold.posture !== \"page\" &&\n scaffold.posture !== \"widget\"\n ) {\n return undefined;\n }\n return scaffold as unknown as ScaffoldManifest;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Record which managed app files `zitadel setup` actually wrote (or found\n * already matching), keyed by project-root-relative posix path with the\n * content hash and ownership class. Only marked files that the scaffold run\n * touched are recorded — a conditional file whose op never ran (e.g. the\n * framework home page on a pre-existing app) is simply absent, which is what\n * lets `doctor` later distinguish \"CLI wrote this and it vanished\" from \"the\n * CLI never owned this\".\n */\nexport async function writeScaffoldManifest(options: {\n cwd: string;\n actions: EjectActions;\n /** Paths the scaffold reported written or skipped-as-identical. */\n written: ReadonlyArray<string>;\n scaffoldedFramework?: boolean;\n devPort?: number;\n posture?: ScaffoldPosture;\n}): Promise<void> {\n const { cwd, actions, written, scaffoldedFramework, devPort, posture } = options;\n const touched = new Set(written.map((path) => relativePosix(cwd, path)));\n const files: ScaffoldManifest[\"files\"] = {};\n for (const path of actions.markedFiles) {\n const key = relativePosix(cwd, path);\n if (!touched.has(key)) {\n continue;\n }\n let contents: string;\n try {\n contents = await readFile(join(cwd, key), \"utf8\");\n } catch {\n continue;\n }\n files[key] = {\n hash: hashScaffoldFile(contents),\n class: actions.fileClasses?.[path] ?? \"presentation\",\n };\n }\n await updateScaffold(cwd, {\n files,\n ...(scaffoldedFramework ? { scaffolded_framework: true } : {}),\n ...(typeof devPort === \"number\" ? { dev_port: devPort } : {}),\n ...(posture === undefined ? {} : { posture }),\n });\n}\n\n/** Normalizes an absolute or relative path to a cwd-relative posix key. */\nfunction relativePosix(cwd: string, path: string): string {\n const rel = isAbsolute(path) ? relative(cwd, path) : path;\n return rel.split(sep).join(\"/\");\n}\n"],"mappings":";;;;;;;;;;;;AAeA,SAAgB,iBAAiB,UAA0B;AACzD,QAAO,WAAW,SAAS,CAAC,OAAO,SAAS,CAAC,OAAO,MAAM;;;;;;;;;;AAW5D,eAAsB,qBAAqB,KAAoD;AAC7F,KAAI;EAEF,MAAM,YAAW,MADG,UAAU,IAAI,EACX;AAGvB,MAAI,CAAC,SAAS,SAAS,CACrB;AAEF,MAAI,CAAC,SAAS,SAAS,MAAM,CAC3B;AAEF,OAAK,MAAM,SAAS,OAAO,OAAO,SAAS,MAAM,EAAE;AACjD,OAAI,CAAC,SAAS,MAAM,CAClB;AAEF,OAAI,OAAO,MAAM,SAAS,SACxB;AAEF,OAAI,MAAM,UAAU,oBAAoB,MAAM,UAAU,eACtD;;AAMJ,MACE,SAAS,YAAY,KAAA,KACrB,SAAS,YAAY,UACrB,SAAS,YAAY,SAErB;AAEF,SAAO;SACD;AACN;;;;;;;;;;;;AAaJ,eAAsB,sBAAsB,SAQ1B;CAChB,MAAM,EAAE,KAAK,SAAS,SAAS,qBAAqB,SAAS,YAAY;CACzE,MAAM,UAAU,IAAI,IAAI,QAAQ,KAAK,SAAS,cAAc,KAAK,KAAK,CAAC,CAAC;CACxE,MAAM,QAAmC,EAAE;AAC3C,MAAK,MAAM,QAAQ,QAAQ,aAAa;EACtC,MAAM,MAAM,cAAc,KAAK,KAAK;AACpC,MAAI,CAAC,QAAQ,IAAI,IAAI,CACnB;EAEF,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,SAAS,KAAK,KAAK,IAAI,EAAE,OAAO;UAC3C;AACN;;AAEF,QAAM,OAAO;GACX,MAAM,iBAAiB,SAAS;GAChC,OAAO,QAAQ,cAAc,SAAS;GACvC;;AAEH,OAAM,eAAe,KAAK;EACxB;EACA,GAAI,sBAAsB,EAAE,sBAAsB,MAAM,GAAG,EAAE;EAC7D,GAAI,OAAO,YAAY,WAAW,EAAE,UAAU,SAAS,GAAG,EAAE;EAC5D,GAAI,YAAY,KAAA,IAAY,EAAE,GAAG,EAAE,SAAS;EAC7C,CAAC;;;AAIJ,SAAS,cAAc,KAAa,MAAsB;AAExD,SADY,WAAW,KAAK,GAAG,SAAS,KAAK,KAAK,GAAG,MAC1C,MAAM,IAAI,CAAC,KAAK,IAAI"}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as SCHEMAS_DIR } from "./user-schema-
|
|
1
|
+
import { E as ZitadelError, S as isObject, T as stableStringify } from "./oclif-CL6-M13m.mjs";
|
|
2
|
+
import { a as updateState, n as readState, r as removeFromState, t as SCHEMAS_DIR } from "./user-schema-W1OsXdpc.mjs";
|
|
3
|
+
import { a as toLocalBrandingBody, i as toBrandingWireBody, n as readDescriptorTemplate, r as resolveTemplatePath, t as BRANDING_DIR } from "./branding-BXj028-W.mjs";
|
|
3
4
|
import { readFile, readdir, writeFile } from "node:fs/promises";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
+
import { basename, join } from "node:path";
|
|
5
6
|
import { DEFAULT_FLOW_SCHEMA_URI } from "@zitadel/config/defaults";
|
|
6
7
|
import { consola as consola$1 } from "consola";
|
|
7
8
|
import { createHash } from "node:crypto";
|
|
9
|
+
import { writeFileSync } from "node:fs";
|
|
8
10
|
import { normalizeFlowBody, normalizeSchemaBody } from "@zitadel/config/normalize";
|
|
9
|
-
import { flowConfigSchema, schemaConfigSchema } from "@zitadel/config/schemas";
|
|
11
|
+
import { brandingConfigSchema, flowConfigSchema, schemaConfigSchema } from "@zitadel/config/schemas";
|
|
12
|
+
import { validateLoginTemplate } from "@zitadel/config/template";
|
|
10
13
|
import { validateFlowDefinition } from "@zitadel/config/validate";
|
|
11
14
|
//#region src/lib/flows/env-refs.ts
|
|
12
15
|
/**
|
|
@@ -51,7 +54,11 @@ const FLOWS_DIR = ".zitadel/flows";
|
|
|
51
54
|
* sync loop.
|
|
52
55
|
*/
|
|
53
56
|
function makeSyncers(opts) {
|
|
54
|
-
return [
|
|
57
|
+
return [
|
|
58
|
+
new SchemaSyncer(opts.client, opts.projectId, opts.env),
|
|
59
|
+
new FlowDefinitionSyncer(opts.client, opts.projectId, opts.env),
|
|
60
|
+
new BrandingSyncer(opts.client, opts.projectId, opts.env, opts.cwd)
|
|
61
|
+
];
|
|
55
62
|
}
|
|
56
63
|
/**
|
|
57
64
|
* Assert that every env var a resource references — `${VAR}` placeholders and
|
|
@@ -181,6 +188,87 @@ var FlowDefinitionSyncer = class {
|
|
|
181
188
|
return (await this.client.getFlowDefinition(id, { project_id: this.projectId })).flow_definition;
|
|
182
189
|
}
|
|
183
190
|
};
|
|
191
|
+
/**
|
|
192
|
+
* Branding revisions (ADR 040): schema-style immutable semantics — every
|
|
193
|
+
* edit publishes a new revision via `POST /branding`, no update or delete.
|
|
194
|
+
* Unlike schemas, nothing references branding revisions, so a revise never
|
|
195
|
+
* triggers re-pinning. The descriptor keeps the template in a sibling
|
|
196
|
+
* `.liquid` file (`liquid_template_file`); this syncer inlines it for
|
|
197
|
+
* hashing and upload and splits it back out on write-back.
|
|
198
|
+
*/
|
|
199
|
+
var BrandingSyncer = class {
|
|
200
|
+
kind = "branding";
|
|
201
|
+
directory = BRANDING_DIR;
|
|
202
|
+
mutable = false;
|
|
203
|
+
revisioned = true;
|
|
204
|
+
/** One project, one branding descriptor — extra .json files fail the scan. */
|
|
205
|
+
singletonFile = "branding.json";
|
|
206
|
+
constructor(client, projectId, env, cwd) {
|
|
207
|
+
this.client = client;
|
|
208
|
+
this.projectId = projectId;
|
|
209
|
+
this.env = env;
|
|
210
|
+
this.cwd = cwd;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The comparison form is the wire body with the template inlined, so an
|
|
214
|
+
* edit to the referenced `.liquid` file changes the state hash and plans
|
|
215
|
+
* a `revise` even though the descriptor JSON is untouched.
|
|
216
|
+
*/
|
|
217
|
+
normalize = (data) => toBrandingWireBody(this.cwd, data);
|
|
218
|
+
/**
|
|
219
|
+
* Zod shape + env refs, then the authoritative template validation from
|
|
220
|
+
* `@zitadel/config/template` — the LiquidJS-dialect check the Go server
|
|
221
|
+
* cannot run (its save gate is lexical; see ADR 040).
|
|
222
|
+
*/
|
|
223
|
+
validate(data) {
|
|
224
|
+
const result = brandingConfigSchema.safeParse(data);
|
|
225
|
+
if (!result.success) throw new ZitadelError("E_VALIDATION", "Branding file is not a valid branding descriptor", { details: { issues: result.error.issues } });
|
|
226
|
+
assertEnvRefs(data, this.env);
|
|
227
|
+
const template = readDescriptorTemplate(this.cwd, data);
|
|
228
|
+
if (template === void 0) return;
|
|
229
|
+
const issues = validateLoginTemplate(template).filter((issue) => issue.severity === "error");
|
|
230
|
+
if (issues.length > 0) throw new ZitadelError("E_VALIDATION", "Login template failed validation", {
|
|
231
|
+
details: { issues },
|
|
232
|
+
hint: "Fix the template issues; the rules live in docs/design/flowengine/template-security.md."
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* `POST /branding` publishes a new immutable revision. The canonical body
|
|
237
|
+
* is converted back to descriptor form: the stored template is written to
|
|
238
|
+
* the referenced `.liquid` file (when it differs) and the JSON keeps the
|
|
239
|
+
* file reference, so `writeBackResource` stays pure-JSON.
|
|
240
|
+
*/
|
|
241
|
+
async create(data) {
|
|
242
|
+
const wire = toBrandingWireBody(this.cwd, data);
|
|
243
|
+
const result = await this.client.createBranding(wire, { project_id: this.projectId });
|
|
244
|
+
return {
|
|
245
|
+
id: result.id,
|
|
246
|
+
canonical: this.canonicalToLocal(result.branding, data)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
async update(_id, _data) {
|
|
250
|
+
throw new ZitadelError("E_NOT_IMPLEMENTED", "branding is revisioned — edit publishes a new revision, not an update");
|
|
251
|
+
}
|
|
252
|
+
async delete(id) {
|
|
253
|
+
throw new ZitadelError("E_NOT_IMPLEMENTED", `branding delete is not supported (${id})`);
|
|
254
|
+
}
|
|
255
|
+
/** Wire form (template inlined); diffs compare in the normalized form. */
|
|
256
|
+
async fetch(id) {
|
|
257
|
+
return (await this.client.getBrandingById(id, { project_id: this.projectId })).branding;
|
|
258
|
+
}
|
|
259
|
+
canonicalToLocal(canonicalWire, localData) {
|
|
260
|
+
const local = localData;
|
|
261
|
+
const template = canonicalWire.liquid_template;
|
|
262
|
+
if (typeof local.liquid_template_file === "string" && typeof template === "string") {
|
|
263
|
+
const path = resolveTemplatePath(this.cwd, local.liquid_template_file);
|
|
264
|
+
if (readDescriptorTemplate(this.cwd, localData) !== template) {
|
|
265
|
+
writeFileSync(path, template);
|
|
266
|
+
consola$1.info(`Updated ${local.liquid_template_file} from the server's canonical response`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return toLocalBrandingBody(canonicalWire, localData);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
184
272
|
//#endregion
|
|
185
273
|
//#region src/lib/sync/flow-validation.ts
|
|
186
274
|
/**
|
|
@@ -290,49 +378,6 @@ function resolveSchemaContent(action, scannedContents, stateResources) {
|
|
|
290
378
|
consola$1.debug(`flow validation: no local schema for user_schema ${ref}; flow-local rules only`);
|
|
291
379
|
}
|
|
292
380
|
//#endregion
|
|
293
|
-
//#region src/lib/sync/state.ts
|
|
294
|
-
/**
|
|
295
|
-
* Read and parse `.zitadel/state.json`. Throws if the file is
|
|
296
|
-
* missing or malformed; callers run `zitadel setup` first to bring
|
|
297
|
-
* the file into existence.
|
|
298
|
-
*/
|
|
299
|
-
async function readState(cwd) {
|
|
300
|
-
const raw = await readFile(join(cwd, ".zitadel/state.json"), "utf8");
|
|
301
|
-
return JSON.parse(raw);
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Merge an entry into the state file under `key`, preserving any
|
|
305
|
-
* fields the caller did not override. Reads the file, writes it back
|
|
306
|
-
* with sorted keys disabled (state is engine-managed, not human-
|
|
307
|
-
* authored, so deterministic ordering isn't required here).
|
|
308
|
-
*/
|
|
309
|
-
async function updateState(cwd, key, entry) {
|
|
310
|
-
const current = await readState(cwd);
|
|
311
|
-
const updated = {
|
|
312
|
-
...current,
|
|
313
|
-
resources: {
|
|
314
|
-
...current.resources,
|
|
315
|
-
[key]: {
|
|
316
|
-
...current.resources[key],
|
|
317
|
-
...entry
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
};
|
|
321
|
-
await writeFile(join(cwd, ".zitadel/state.json"), JSON.stringify(updated, null, 2));
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Remove an entry from the state file. No-op if the key is absent.
|
|
325
|
-
*/
|
|
326
|
-
async function removeFromState(cwd, key) {
|
|
327
|
-
const current = await readState(cwd);
|
|
328
|
-
const { [key]: _removed, ...rest } = current.resources;
|
|
329
|
-
const updated = {
|
|
330
|
-
...current,
|
|
331
|
-
resources: rest
|
|
332
|
-
};
|
|
333
|
-
await writeFile(join(cwd, ".zitadel/state.json"), JSON.stringify(updated, null, 2));
|
|
334
|
-
}
|
|
335
|
-
//#endregion
|
|
336
381
|
//#region src/lib/sync/loop.ts
|
|
337
382
|
/**
|
|
338
383
|
* Compute the sync plan for `cwd` against the state file and (when
|
|
@@ -369,6 +414,10 @@ async function buildSyncPlan(cwd, syncers, fetchOld = false) {
|
|
|
369
414
|
const dirPath = join(cwd, syncer.directory);
|
|
370
415
|
consola$1.debug(`scanning ${syncer.directory}`);
|
|
371
416
|
const onDisk = await readJsonDir(dirPath);
|
|
417
|
+
if (syncer.singletonFile) for (const absPath of onDisk.keys()) {
|
|
418
|
+
const name = basename(absPath);
|
|
419
|
+
if (name !== syncer.singletonFile) throw new ZitadelError("E_VALIDATION", `unexpected file in ${syncer.directory}: ${name}`, { hint: `${syncer.kind} is a singleton resource — keep exactly one descriptor at ${syncer.directory}/${syncer.singletonFile} and move other .json files out of the directory (each scanned descriptor would publish as its own revision).` });
|
|
420
|
+
}
|
|
372
421
|
for (const content of onDisk.values()) syncer.validate(content);
|
|
373
422
|
for (const [filePath, entry] of Object.entries(state.resources)) {
|
|
374
423
|
if (!filePath.startsWith(syncer.directory)) continue;
|
|
@@ -1154,6 +1203,6 @@ function renderWarnings(warnings, blkPad, tty, lines) {
|
|
|
1154
1203
|
for (const warning of warnings ?? []) lines.push(paint(`${blkPad}# warning: ${warning.message}`, A.yellow, tty));
|
|
1155
1204
|
}
|
|
1156
1205
|
//#endregion
|
|
1157
|
-
export { buildSyncPlan as a, writeBackResource as c,
|
|
1206
|
+
export { buildSyncPlan as a, writeBackResource as c, summarizePlan as i, makeSyncers as l, enumeratePlanResources as n, hashForState as o, renderPlan as r, runSyncLoop as s, collectPlanWarnings as t, FLOWS_DIR as u };
|
|
1158
1207
|
|
|
1159
|
-
//# sourceMappingURL=sync-
|
|
1208
|
+
//# sourceMappingURL=sync-Cx2_4rXW.mjs.map
|