@zitadel/cli 0.1.0-alpha.9 → 1.0.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +250 -62
- package/SKILLS.md +221 -37
- package/dist/branding-cygSBPkV.mjs +79 -0
- package/dist/branding-cygSBPkV.mjs.map +1 -0
- package/dist/claim-state-DlfD6Res.mjs +66 -0
- package/dist/claim-state-DlfD6Res.mjs.map +1 -0
- package/dist/commands/apply.mjs +27 -8
- package/dist/commands/apply.mjs.map +1 -1
- package/dist/commands/branding/eject.mjs +104 -0
- package/dist/commands/branding/eject.mjs.map +1 -0
- package/dist/commands/claim.mjs +402 -0
- package/dist/commands/claim.mjs.map +1 -0
- package/dist/commands/doctor.mjs +594 -41
- package/dist/commands/doctor.mjs.map +1 -1
- package/dist/commands/eject.mjs +18 -6
- package/dist/commands/eject.mjs.map +1 -1
- package/dist/commands/logs.mjs +2 -2
- package/dist/commands/plan.mjs +19 -6
- package/dist/commands/plan.mjs.map +1 -1
- package/dist/commands/reset.mjs +2 -2
- package/dist/commands/schemas/list.mjs +146 -0
- package/dist/commands/schemas/list.mjs.map +1 -0
- package/dist/commands/setup.mjs +629 -214
- package/dist/commands/setup.mjs.map +1 -1
- package/dist/commands/start.mjs +4 -4
- package/dist/commands/start.mjs.map +1 -1
- package/dist/commands/status.mjs +52 -13
- package/dist/commands/status.mjs.map +1 -1
- package/dist/commands/stop.mjs +3 -3
- package/dist/commands/stop.mjs.map +1 -1
- package/dist/designs-Ckz18Dpo.mjs +38 -0
- package/dist/designs-Ckz18Dpo.mjs.map +1 -0
- package/dist/{docker-CnGQK3ZK.mjs → docker-DcRGTOJa.mjs} +5 -4
- package/dist/docker-DcRGTOJa.mjs.map +1 -0
- package/dist/environment-rjRVkJjW.mjs +17 -0
- package/dist/environment-rjRVkJjW.mjs.map +1 -0
- package/dist/journey-guidance-BGrOX_gT.mjs +40 -0
- package/dist/journey-guidance-BGrOX_gT.mjs.map +1 -0
- package/dist/oclif-CanO3zdt.mjs +1769 -0
- package/dist/oclif-CanO3zdt.mjs.map +1 -0
- package/dist/{orca-U142Wrau.mjs → orca-BzQIQqzt.mjs} +1663 -303
- package/dist/orca-BzQIQqzt.mjs.map +1 -0
- package/dist/package-manager-DFdLcDx1.mjs +194 -0
- package/dist/package-manager-DFdLcDx1.mjs.map +1 -0
- package/dist/{ports-B09RjuHx.mjs → ports-BM20XIZb.mjs} +25 -20
- package/dist/ports-BM20XIZb.mjs.map +1 -0
- package/dist/{processes-Cw8TO1SY.mjs → processes-DKHPkU8O.mjs} +1 -1
- package/dist/{processes-Cw8TO1SY.mjs.map → processes-DKHPkU8O.mjs.map} +1 -1
- package/dist/{project-Cd0L3PtM.mjs → project-CHhot85s.mjs} +57 -4
- package/dist/project-CHhot85s.mjs.map +1 -0
- package/dist/sync-BBlACKzT.mjs +1633 -0
- package/dist/sync-BBlACKzT.mjs.map +1 -0
- package/dist/user-schema-DTuOsdKE.mjs +91 -0
- package/dist/user-schema-DTuOsdKE.mjs.map +1 -0
- package/oclif.manifest.json +406 -5
- package/package.json +10 -5
- package/dist/docker-CnGQK3ZK.mjs.map +0 -1
- package/dist/docker-guidance-ypN3IM3o.mjs +0 -21
- package/dist/docker-guidance-ypN3IM3o.mjs.map +0 -1
- package/dist/oclif-B7lBzh3R.mjs +0 -808
- package/dist/oclif-B7lBzh3R.mjs.map +0 -1
- package/dist/orca-U142Wrau.mjs.map +0 -1
- package/dist/ports-B09RjuHx.mjs.map +0 -1
- package/dist/project-Cd0L3PtM.mjs.map +0 -1
- package/dist/sync-BojoQm2P.mjs +0 -733
- package/dist/sync-BojoQm2P.mjs.map +0 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { t as BRANDING_DESIGN_INFO } from "../../designs-Ckz18Dpo.mjs";
|
|
2
|
+
import { E as ZitadelError, O as normalizePublicCliJson, T as stableStringify, j as publicCliCommand, k as normalizePublicCliProse, t as BaseCommand } from "../../oclif-CanO3zdt.mjs";
|
|
3
|
+
import { t as hasZitadelConfig } from "../../project-CHhot85s.mjs";
|
|
4
|
+
import { t as BRANDING_DIR } from "../../branding-cygSBPkV.mjs";
|
|
5
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { cancel, isCancel, select } from "@clack/prompts";
|
|
8
|
+
import { Flags } from "@oclif/core";
|
|
9
|
+
import { BRANDING_DESIGNS, DEFAULT_BRANDING_CONFIG_PATH, DEFAULT_BRANDING_DESIGN, DEFAULT_BRANDING_TEMPLATE_PATH, brandingReadmeContent, getDefaultBrandingConfig } from "@zitadel/config/defaults";
|
|
10
|
+
import { consola as consola$1 } from "consola";
|
|
11
|
+
import { BRANDING_FILE_SCHEMA_REF, META_SCHEMA_DIR, metaSchemaFiles } from "@zitadel/config/meta-schemas";
|
|
12
|
+
//#region src/commands/branding/eject.ts
|
|
13
|
+
/**
|
|
14
|
+
* The `branding eject` command — take ownership of the login template.
|
|
15
|
+
*
|
|
16
|
+
* Scaffolds `.zitadel/branding/` from a shipped design: the `branding.json`
|
|
17
|
+
* descriptor, the `login.liquid` template it references, a README, and the
|
|
18
|
+
* branding dialect meta-schema (for projects set up before it existed).
|
|
19
|
+
* Purely local — nothing is uploaded; the first `zitadel apply` publishes
|
|
20
|
+
* revision 1. Ejecting the template is the last rung of the branding
|
|
21
|
+
* override ladder (docs/design/branding/override-ladder.md); unrelated to
|
|
22
|
+
* `zitadel eject`, which removes the Zitadel integration.
|
|
23
|
+
*/
|
|
24
|
+
var BrandingEject = class BrandingEject extends BaseCommand {
|
|
25
|
+
static description = "Take ownership of the login template: scaffold .zitadel/branding/ from a shipped design.";
|
|
26
|
+
static flags = { design: Flags.string({
|
|
27
|
+
description: `Design to start from (default: ${DEFAULT_BRANDING_DESIGN}).`,
|
|
28
|
+
options: [...BRANDING_DESIGNS]
|
|
29
|
+
}) };
|
|
30
|
+
async run() {
|
|
31
|
+
const { flags } = await this.parse(BrandingEject);
|
|
32
|
+
await this.toMeta(flags);
|
|
33
|
+
const { cwd, force, nonInteractive } = this.meta;
|
|
34
|
+
if (!await hasZitadelConfig(cwd)) throw new ZitadelError("E_VALIDATION", "This directory is not a Zitadel project", {
|
|
35
|
+
hint: "Run setup first; branding files live under .zitadel/.",
|
|
36
|
+
nextCommands: [publicCliCommand("setup", this.meta.cliVersion)]
|
|
37
|
+
});
|
|
38
|
+
const design = flags.design ?? await this.promptDesign(nonInteractive);
|
|
39
|
+
this.recordTelemetry({
|
|
40
|
+
design,
|
|
41
|
+
force
|
|
42
|
+
});
|
|
43
|
+
const { branding, template } = getDefaultBrandingConfig(design);
|
|
44
|
+
const filesWritten = [];
|
|
45
|
+
const write = async (relPath, content, overwrite) => {
|
|
46
|
+
const dest = join(cwd, relPath);
|
|
47
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
48
|
+
try {
|
|
49
|
+
await writeFile(dest, content, overwrite ? void 0 : { flag: "wx" });
|
|
50
|
+
filesWritten.push(relPath);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (isErrno(error, "EEXIST")) throw new ZitadelError("E_CONFLICT", `${relPath} already exists`, { hint: "Rerun with --force to replace it, or edit the existing file directly." });
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
await write(DEFAULT_BRANDING_CONFIG_PATH, `${stableStringify({
|
|
57
|
+
$schema: BRANDING_FILE_SCHEMA_REF,
|
|
58
|
+
...branding
|
|
59
|
+
})}\n`, force);
|
|
60
|
+
await write(DEFAULT_BRANDING_TEMPLATE_PATH, template, force);
|
|
61
|
+
await write(join(BRANDING_DIR, "README.md"), normalizePublicCliProse(brandingReadmeContent(), this.meta.cliVersion), false).catch(swallowConflict);
|
|
62
|
+
const brandingMeta = metaSchemaFiles().find((f) => f.name === "branding.json");
|
|
63
|
+
if (brandingMeta) await write(join(META_SCHEMA_DIR, brandingMeta.name), `${stableStringify(normalizePublicCliJson(brandingMeta.body, this.meta.cliVersion))}\n`, false).catch(swallowConflict);
|
|
64
|
+
for (const file of filesWritten) consola$1.info(`Wrote ${file}`);
|
|
65
|
+
consola$1.success(`Ejected the ${design} design into ${BRANDING_DIR}/`);
|
|
66
|
+
return this.emit({
|
|
67
|
+
status: "ok",
|
|
68
|
+
data: {
|
|
69
|
+
design,
|
|
70
|
+
files_written: filesWritten,
|
|
71
|
+
next_actions: [`Edit ${DEFAULT_BRANDING_TEMPLATE_PATH} — the login UI is yours now.`, "Plan validates the template and shows the pending revision; apply publishes it."],
|
|
72
|
+
next_commands: [publicCliCommand("plan", this.meta.cliVersion), publicCliCommand("apply", this.meta.cliVersion)]
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async promptDesign(nonInteractive) {
|
|
77
|
+
if (nonInteractive) return DEFAULT_BRANDING_DESIGN;
|
|
78
|
+
const choice = await select({
|
|
79
|
+
message: "Which design do you want to start from?",
|
|
80
|
+
initialValue: DEFAULT_BRANDING_DESIGN,
|
|
81
|
+
options: BRANDING_DESIGNS.map((design) => ({
|
|
82
|
+
value: design,
|
|
83
|
+
label: BRANDING_DESIGN_INFO[design].label,
|
|
84
|
+
hint: BRANDING_DESIGN_INFO[design].hint
|
|
85
|
+
}))
|
|
86
|
+
});
|
|
87
|
+
if (isCancel(choice)) {
|
|
88
|
+
cancel("Eject cancelled.");
|
|
89
|
+
throw new ZitadelError("E_VALIDATION", "Eject cancelled by user");
|
|
90
|
+
}
|
|
91
|
+
return choice;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
function swallowConflict(error) {
|
|
95
|
+
if (error instanceof ZitadelError && error.code === "E_CONFLICT") return;
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
function isErrno(error, code) {
|
|
99
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export { BrandingEject as default };
|
|
103
|
+
|
|
104
|
+
//# sourceMappingURL=eject.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eject.mjs","names":[],"sources":["../../../src/commands/branding/eject.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\nimport { cancel, isCancel, select } from \"@clack/prompts\";\nimport { Flags } from \"@oclif/core\";\nimport { consola } from \"consola\";\n\nimport {\n BRANDING_DESIGNS,\n DEFAULT_BRANDING_CONFIG_PATH,\n DEFAULT_BRANDING_DESIGN,\n DEFAULT_BRANDING_TEMPLATE_PATH,\n brandingReadmeContent,\n getDefaultBrandingConfig,\n} from \"@zitadel/config/defaults\";\nimport { BRANDING_FILE_SCHEMA_REF, META_SCHEMA_DIR, metaSchemaFiles } from \"@zitadel/config/meta-schemas\";\n\nimport { BRANDING_DIR } from \"../../lib/branding\";\nimport { BRANDING_DESIGN_INFO } from \"../../lib/branding/designs\";\nimport { ZitadelError } from \"../../lib/errors\";\nimport { stableStringify } from \"../../lib/json\";\nimport { BaseCommand, type JsonEnvelope } from \"../../lib/oclif\";\nimport { hasZitadelConfig } from \"../../lib/project\";\nimport {\n normalizePublicCliJson,\n normalizePublicCliProse,\n publicCliCommand,\n} from \"../../lib/public-cli\";\n\n/**\n * The `branding eject` command — take ownership of the login template.\n *\n * Scaffolds `.zitadel/branding/` from a shipped design: the `branding.json`\n * descriptor, the `login.liquid` template it references, a README, and the\n * branding dialect meta-schema (for projects set up before it existed).\n * Purely local — nothing is uploaded; the first `zitadel apply` publishes\n * revision 1. Ejecting the template is the last rung of the branding\n * override ladder (docs/design/branding/override-ladder.md); unrelated to\n * `zitadel eject`, which removes the Zitadel integration.\n */\nexport default class BrandingEject extends BaseCommand {\n static override description =\n \"Take ownership of the login template: scaffold .zitadel/branding/ from a shipped design.\";\n static override flags = {\n design: Flags.string({\n description: `Design to start from (default: ${DEFAULT_BRANDING_DESIGN}).`,\n options: [...BRANDING_DESIGNS],\n }),\n };\n\n async run(): Promise<JsonEnvelope> {\n const { flags } = await this.parse(BrandingEject);\n await this.toMeta(flags);\n const { cwd, force, nonInteractive } = this.meta;\n\n if (!(await hasZitadelConfig(cwd))) {\n throw new ZitadelError(\"E_VALIDATION\", \"This directory is not a Zitadel project\", {\n hint: \"Run setup first; branding files live under .zitadel/.\",\n nextCommands: [publicCliCommand(\"setup\", this.meta.cliVersion)],\n });\n }\n\n const design = flags.design ?? (await this.promptDesign(nonInteractive));\n this.recordTelemetry({ design, force });\n const { branding, template } = getDefaultBrandingConfig(design);\n\n const filesWritten: string[] = [];\n const write = async (relPath: string, content: string, overwrite: boolean): Promise<void> => {\n const dest = join(cwd, relPath);\n await mkdir(dirname(dest), { recursive: true });\n try {\n await writeFile(dest, content, overwrite ? undefined : { flag: \"wx\" });\n filesWritten.push(relPath);\n } catch (error) {\n if (isErrno(error, \"EEXIST\")) {\n throw new ZitadelError(\"E_CONFLICT\", `${relPath} already exists`, {\n hint: \"Rerun with --force to replace it, or edit the existing file directly.\",\n });\n }\n throw error;\n }\n };\n\n await write(\n DEFAULT_BRANDING_CONFIG_PATH,\n `${stableStringify({ $schema: BRANDING_FILE_SCHEMA_REF, ...branding })}\\n`,\n force,\n );\n await write(DEFAULT_BRANDING_TEMPLATE_PATH, template, force);\n\n // Never overwrite a hand-edited README; setups from before the branding\n // dialect also miss the meta-schema, so materialize it when absent.\n await write(\n join(BRANDING_DIR, \"README.md\"),\n normalizePublicCliProse(brandingReadmeContent(), this.meta.cliVersion),\n false,\n ).catch(swallowConflict);\n const brandingMeta = metaSchemaFiles().find((f) => f.name === \"branding.json\");\n if (brandingMeta) {\n await write(\n join(META_SCHEMA_DIR, brandingMeta.name),\n `${stableStringify(normalizePublicCliJson(brandingMeta.body, this.meta.cliVersion))}\\n`,\n false,\n ).catch(swallowConflict);\n }\n\n for (const file of filesWritten) {\n consola.info(`Wrote ${file}`);\n }\n consola.success(`Ejected the ${design} design into ${BRANDING_DIR}/`);\n\n return this.emit({\n status: \"ok\",\n data: {\n design,\n files_written: filesWritten,\n next_actions: [\n `Edit ${DEFAULT_BRANDING_TEMPLATE_PATH} — the login UI is yours now.`,\n \"Plan validates the template and shows the pending revision; apply publishes it.\",\n ],\n next_commands: [\n publicCliCommand(\"plan\", this.meta.cliVersion),\n publicCliCommand(\"apply\", this.meta.cliVersion),\n ],\n },\n });\n }\n\n private async promptDesign(nonInteractive: boolean): Promise<string> {\n if (nonInteractive) {\n return DEFAULT_BRANDING_DESIGN;\n }\n const choice = await select({\n message: \"Which design do you want to start from?\",\n initialValue: DEFAULT_BRANDING_DESIGN as string,\n options: BRANDING_DESIGNS.map((design) => ({\n value: design as string,\n label: BRANDING_DESIGN_INFO[design].label,\n hint: BRANDING_DESIGN_INFO[design].hint,\n })),\n });\n if (isCancel(choice)) {\n cancel(\"Eject cancelled.\");\n throw new ZitadelError(\"E_VALIDATION\", \"Eject cancelled by user\");\n }\n return choice;\n }\n}\n\nfunction swallowConflict(error: unknown): void {\n if (error instanceof ZitadelError && error.code === \"E_CONFLICT\") {\n return;\n }\n throw error;\n}\n\nfunction isErrno(error: unknown, code: string): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n (error as NodeJS.ErrnoException).code === code\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwCA,IAAqB,gBAArB,MAAqB,sBAAsB,YAAY;CACrD,OAAgB,cACd;CACF,OAAgB,QAAQ,EACtB,QAAQ,MAAM,OAAO;EACnB,aAAa,kCAAkC,wBAAwB;EACvE,SAAS,CAAC,GAAG,iBAAiB;EAC/B,CAAC,EACH;CAED,MAAM,MAA6B;EACjC,MAAM,EAAE,UAAU,MAAM,KAAK,MAAM,cAAc;AACjD,QAAM,KAAK,OAAO,MAAM;EACxB,MAAM,EAAE,KAAK,OAAO,mBAAmB,KAAK;AAE5C,MAAI,CAAE,MAAM,iBAAiB,IAAI,CAC/B,OAAM,IAAI,aAAa,gBAAgB,2CAA2C;GAChF,MAAM;GACN,cAAc,CAAC,iBAAiB,SAAS,KAAK,KAAK,WAAW,CAAC;GAChE,CAAC;EAGJ,MAAM,SAAS,MAAM,UAAW,MAAM,KAAK,aAAa,eAAe;AACvE,OAAK,gBAAgB;GAAE;GAAQ;GAAO,CAAC;EACvC,MAAM,EAAE,UAAU,aAAa,yBAAyB,OAAO;EAE/D,MAAM,eAAyB,EAAE;EACjC,MAAM,QAAQ,OAAO,SAAiB,SAAiB,cAAsC;GAC3F,MAAM,OAAO,KAAK,KAAK,QAAQ;AAC/B,SAAM,MAAM,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AAC/C,OAAI;AACF,UAAM,UAAU,MAAM,SAAS,YAAY,KAAA,IAAY,EAAE,MAAM,MAAM,CAAC;AACtE,iBAAa,KAAK,QAAQ;YACnB,OAAO;AACd,QAAI,QAAQ,OAAO,SAAS,CAC1B,OAAM,IAAI,aAAa,cAAc,GAAG,QAAQ,kBAAkB,EAChE,MAAM,yEACP,CAAC;AAEJ,UAAM;;;AAIV,QAAM,MACJ,8BACA,GAAG,gBAAgB;GAAE,SAAS;GAA0B,GAAG;GAAU,CAAC,CAAC,KACvE,MACD;AACD,QAAM,MAAM,gCAAgC,UAAU,MAAM;AAI5D,QAAM,MACJ,KAAK,cAAc,YAAY,EAC/B,wBAAwB,uBAAuB,EAAE,KAAK,KAAK,WAAW,EACtE,MACD,CAAC,MAAM,gBAAgB;EACxB,MAAM,eAAe,iBAAiB,CAAC,MAAM,MAAM,EAAE,SAAS,gBAAgB;AAC9E,MAAI,aACF,OAAM,MACJ,KAAK,iBAAiB,aAAa,KAAK,EACxC,GAAG,gBAAgB,uBAAuB,aAAa,MAAM,KAAK,KAAK,WAAW,CAAC,CAAC,KACpF,MACD,CAAC,MAAM,gBAAgB;AAG1B,OAAK,MAAM,QAAQ,aACjB,WAAQ,KAAK,SAAS,OAAO;AAE/B,YAAQ,QAAQ,eAAe,OAAO,eAAe,aAAa,GAAG;AAErE,SAAO,KAAK,KAAK;GACf,QAAQ;GACR,MAAM;IACJ;IACA,eAAe;IACf,cAAc,CACZ,QAAQ,+BAA+B,gCACvC,kFACD;IACD,eAAe,CACb,iBAAiB,QAAQ,KAAK,KAAK,WAAW,EAC9C,iBAAiB,SAAS,KAAK,KAAK,WAAW,CAChD;IACF;GACF,CAAC;;CAGJ,MAAc,aAAa,gBAA0C;AACnE,MAAI,eACF,QAAO;EAET,MAAM,SAAS,MAAM,OAAO;GAC1B,SAAS;GACT,cAAc;GACd,SAAS,iBAAiB,KAAK,YAAY;IACzC,OAAO;IACP,OAAO,qBAAqB,QAAQ;IACpC,MAAM,qBAAqB,QAAQ;IACpC,EAAE;GACJ,CAAC;AACF,MAAI,SAAS,OAAO,EAAE;AACpB,UAAO,mBAAmB;AAC1B,SAAM,IAAI,aAAa,gBAAgB,0BAA0B;;AAEnE,SAAO;;;AAIX,SAAS,gBAAgB,OAAsB;AAC7C,KAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD;AAEF,OAAM;;AAGR,SAAS,QAAQ,OAAgB,MAAuB;AACtD,QACE,OAAO,UAAU,YACjB,UAAU,QACV,UAAU,SACT,MAAgC,SAAS"}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { E as ZitadelError, S as isObject, t as BaseCommand } from "../oclif-CanO3zdt.mjs";
|
|
2
|
+
import { i as isAttached } from "../claim-state-DlfD6Res.mjs";
|
|
3
|
+
import { l as readZitadelSecret, u as writeZitadelSecret } from "../project-CHhot85s.mjs";
|
|
4
|
+
import { access, readFile } from "node:fs/promises";
|
|
5
|
+
import { delimiter, join } from "node:path";
|
|
6
|
+
import { Flags } from "@oclif/core";
|
|
7
|
+
import { createZitadelClient } from "@zitadel/api/client";
|
|
8
|
+
import consola from "consola";
|
|
9
|
+
import { ApiError } from "@zitadel/api/runtime/fetch";
|
|
10
|
+
import { constants } from "node:fs";
|
|
11
|
+
import { setTimeout } from "node:timers/promises";
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
//#region src/lib/browser.ts
|
|
14
|
+
/**
|
|
15
|
+
* Openers tried in order on Linux and the BSDs. `xdg-open` is the standard and
|
|
16
|
+
* covers any desktop that ships `xdg-utils`; the rest are the fallbacks that
|
|
17
|
+
* exist on systems that don't (GNOME-only, Debian's alternatives system, KDE).
|
|
18
|
+
* `gio` takes a subcommand, so it carries its own argv shape.
|
|
19
|
+
*/
|
|
20
|
+
const UNIX_OPENERS = [
|
|
21
|
+
{
|
|
22
|
+
command: "xdg-open",
|
|
23
|
+
args: (url) => [url]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
command: "gio",
|
|
27
|
+
args: (url) => ["open", url]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
command: "x-www-browser",
|
|
31
|
+
args: (url) => [url]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
command: "sensible-browser",
|
|
35
|
+
args: (url) => [url]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
command: "gnome-open",
|
|
39
|
+
args: (url) => [url]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
command: "kde-open",
|
|
43
|
+
args: (url) => [url]
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* Opens `url` in the user's default browser, best effort.
|
|
48
|
+
*
|
|
49
|
+
* Best effort is the contract, not a caveat: this never throws and never
|
|
50
|
+
* reports a failure the caller must handle. There is no portable way to know a
|
|
51
|
+
* browser actually rendered the page — the opener returns as soon as it has
|
|
52
|
+
* handed the URL off — so `opened: true` means only "an opener was started".
|
|
53
|
+
* Callers must print the URL regardless and treat the launch as a convenience
|
|
54
|
+
* on top of it, which is what makes headless machines, bare containers, and
|
|
55
|
+
* SSH sessions work without a special case.
|
|
56
|
+
*/
|
|
57
|
+
async function openInBrowser(url, overrides = {}) {
|
|
58
|
+
const deps = {
|
|
59
|
+
...defaultDeps(overrides.env ?? process.env),
|
|
60
|
+
...overrides
|
|
61
|
+
};
|
|
62
|
+
const browser = deps.env.BROWSER?.trim();
|
|
63
|
+
if (browser === "none") return {
|
|
64
|
+
opened: false,
|
|
65
|
+
reason: "disabled"
|
|
66
|
+
};
|
|
67
|
+
if (browser) return launch(deps, browser, [url]);
|
|
68
|
+
if (deps.platform === "darwin") return launch(deps, "open", [url]);
|
|
69
|
+
if (deps.platform === "win32") return launch(deps, "cmd", [
|
|
70
|
+
"/c",
|
|
71
|
+
"start",
|
|
72
|
+
"",
|
|
73
|
+
url
|
|
74
|
+
]);
|
|
75
|
+
if (await isWsl(deps)) {
|
|
76
|
+
if (await deps.lookupPath("wslview")) return launch(deps, "wslview", [url]);
|
|
77
|
+
return launch(deps, "powershell.exe", [
|
|
78
|
+
"-NoProfile",
|
|
79
|
+
"-Command",
|
|
80
|
+
"Start-Process",
|
|
81
|
+
url
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
if (!hasGraphicalSession(deps.env)) return {
|
|
85
|
+
opened: false,
|
|
86
|
+
reason: "headless"
|
|
87
|
+
};
|
|
88
|
+
for (const opener of UNIX_OPENERS) if (await deps.lookupPath(opener.command)) return launch(deps, opener.command, opener.args(url));
|
|
89
|
+
return {
|
|
90
|
+
opened: false,
|
|
91
|
+
reason: "no-opener"
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Whether a graphical session is plausibly present and worth opening into.
|
|
96
|
+
*
|
|
97
|
+
* X11 and Wayland both advertise themselves through an env var. SSH is
|
|
98
|
+
* excluded even when `DISPLAY` is set, because X11 forwarding sets it: the
|
|
99
|
+
* browser would launch on the remote host and, at best, tunnel a window back
|
|
100
|
+
* over a link nobody asked to render one on.
|
|
101
|
+
*/
|
|
102
|
+
function hasGraphicalSession(env) {
|
|
103
|
+
if (env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT) return false;
|
|
104
|
+
return Boolean(env.DISPLAY || env.WAYLAND_DISPLAY);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* WSL advertises itself through `WSL_DISTRO_NAME`/`WSLENV`, but neither is set
|
|
108
|
+
* under every shell, so fall back to the kernel release string, which carries
|
|
109
|
+
* `microsoft` on all WSL kernels.
|
|
110
|
+
*/
|
|
111
|
+
async function isWsl(deps) {
|
|
112
|
+
if (deps.platform !== "linux") return false;
|
|
113
|
+
if (deps.env.WSL_DISTRO_NAME || deps.env.WSLENV) return true;
|
|
114
|
+
const version = await deps.readProcVersion();
|
|
115
|
+
return version !== void 0 && version.toLowerCase().includes("microsoft");
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Starts the opener fully detached: the CLI keeps polling for the claim while
|
|
119
|
+
* the browser lives on, and must not hold the event loop open waiting for a
|
|
120
|
+
* process it does not own. Output is discarded because openers write
|
|
121
|
+
* diagnostics to stderr that would interleave with the CLI's own narration.
|
|
122
|
+
*/
|
|
123
|
+
function launch(deps, command, args) {
|
|
124
|
+
try {
|
|
125
|
+
deps.spawn(command, args);
|
|
126
|
+
return { opened: true };
|
|
127
|
+
} catch {
|
|
128
|
+
return {
|
|
129
|
+
opened: false,
|
|
130
|
+
reason: "spawn-failed"
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function defaultDeps(env) {
|
|
135
|
+
return {
|
|
136
|
+
platform: process.platform,
|
|
137
|
+
env,
|
|
138
|
+
lookupPath: (command) => lookupPath(command, env.PATH ?? ""),
|
|
139
|
+
readProcVersion: async () => {
|
|
140
|
+
try {
|
|
141
|
+
return await readFile("/proc/version", "utf8");
|
|
142
|
+
} catch {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
spawn: (command, args) => {
|
|
147
|
+
const child = spawn(command, args, {
|
|
148
|
+
stdio: "ignore",
|
|
149
|
+
detached: true
|
|
150
|
+
});
|
|
151
|
+
child.on("error", () => void 0);
|
|
152
|
+
child.unref();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Minimal `which`: the first entry of `pathEnv` holding an executable
|
|
158
|
+
* `command`. Probing the filesystem directly avoids shelling out (and avoids
|
|
159
|
+
* depending on a `which`/`where` binary being present on the very systems that
|
|
160
|
+
* are missing `xdg-open`). `pathEnv` is passed in rather than read from
|
|
161
|
+
* `process.env` so it follows whatever environment the caller injected.
|
|
162
|
+
*/
|
|
163
|
+
async function lookupPath(command, pathEnv) {
|
|
164
|
+
for (const dir of pathEnv.split(delimiter).filter(Boolean)) {
|
|
165
|
+
const candidate = join(dir, command);
|
|
166
|
+
try {
|
|
167
|
+
await access(candidate, constants.X_OK);
|
|
168
|
+
return candidate;
|
|
169
|
+
} catch {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/commands/claim.ts
|
|
176
|
+
/**
|
|
177
|
+
* Poll cadence while a human completes the browser step. Starts responsive so
|
|
178
|
+
* a claim finished in a few seconds is reported almost immediately, then eases
|
|
179
|
+
* off so a link left open for the full ten minutes costs a few hundred
|
|
180
|
+
* requests rather than a few thousand.
|
|
181
|
+
*/
|
|
182
|
+
const INITIAL_POLL_MS = 1e3;
|
|
183
|
+
const MAX_POLL_MS = 5e3;
|
|
184
|
+
const POLL_BACKOFF_FACTOR = 1.5;
|
|
185
|
+
/**
|
|
186
|
+
* Backstop for the poll deadline, used only when the server's own `expires_at`
|
|
187
|
+
* cannot be parsed. The challenge TTL is the platform's to decide (ADR 046) and
|
|
188
|
+
* the response carries it, so this is never the normal path — but without it a
|
|
189
|
+
* malformed or misrouted response would leave the loop with no deadline at all
|
|
190
|
+
* and the CLI polling forever. Matches the TTL the ADR documents.
|
|
191
|
+
*/
|
|
192
|
+
const FALLBACK_TTL_MS = 600 * 1e3;
|
|
193
|
+
/**
|
|
194
|
+
* When to stop polling: the earlier of the server's own expiry and any
|
|
195
|
+
* `--timeout` the caller set.
|
|
196
|
+
*
|
|
197
|
+
* Trusts `expires_at` rather than assuming the TTL, because the contract
|
|
198
|
+
* documents it as the authority and the CLI should not go stale if the platform
|
|
199
|
+
* ever retunes it. An unparseable value falls back to {@link FALLBACK_TTL_MS}
|
|
200
|
+
* rather than to no deadline: a malformed or misrouted response must not leave
|
|
201
|
+
* the loop running forever. Exported so that fallback is testable without
|
|
202
|
+
* waiting out a real TTL.
|
|
203
|
+
*/
|
|
204
|
+
function claimDeadline(input) {
|
|
205
|
+
const parsed = Date.parse(input.expiresAt);
|
|
206
|
+
const serverDeadline = Number.isNaN(parsed) ? input.now + FALLBACK_TTL_MS : parsed;
|
|
207
|
+
if (input.timeoutSeconds === void 0) return serverDeadline;
|
|
208
|
+
return Math.min(serverDeadline, input.now + input.timeoutSeconds * 1e3);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* `zitadel claim` — attach this project to a team.
|
|
212
|
+
*
|
|
213
|
+
* Shaped like the device-authorization grant (ADR 046): the CLI mints a
|
|
214
|
+
* challenge with the project secret, hands a URL to a browser, and polls until
|
|
215
|
+
* the human finishes. The project secret never leaves the machine and the
|
|
216
|
+
* browser only ever sees the challenge id.
|
|
217
|
+
*
|
|
218
|
+
* The command blocks for as long as the link is valid. That is the point: the
|
|
219
|
+
* developer is looking at the terminal, finishes in the browser, and comes
|
|
220
|
+
* back to a terminal that already knows. Nothing about the project changes —
|
|
221
|
+
* the issuer, users, and applications keep working exactly as before, so this
|
|
222
|
+
* is purely additive.
|
|
223
|
+
*/
|
|
224
|
+
var Claim = class Claim extends BaseCommand {
|
|
225
|
+
static description = "Attach this project to a team so it becomes permanent. Opens a browser to finish signing in.";
|
|
226
|
+
static examples = [
|
|
227
|
+
"<%= config.bin %> <%= command.id %>",
|
|
228
|
+
"<%= config.bin %> <%= command.id %> --no-open",
|
|
229
|
+
"<%= config.bin %> <%= command.id %> --timeout 120"
|
|
230
|
+
];
|
|
231
|
+
static flags = {
|
|
232
|
+
"no-open": Flags.boolean({ description: "Print the link instead of opening a browser." }),
|
|
233
|
+
timeout: Flags.integer({
|
|
234
|
+
min: 1,
|
|
235
|
+
description: "Seconds to wait for the browser step. Defaults to the link's own expiry (10 minutes)."
|
|
236
|
+
})
|
|
237
|
+
};
|
|
238
|
+
async run() {
|
|
239
|
+
const { flags } = await this.parse(Claim);
|
|
240
|
+
await this.toMeta(flags);
|
|
241
|
+
const { cwd, dryRun, nonInteractive } = this.meta;
|
|
242
|
+
const secret = await readZitadelSecret(cwd);
|
|
243
|
+
if (isAttached(secret)) return this.alreadyClaimed({
|
|
244
|
+
project_id: secret.project_id,
|
|
245
|
+
team_id: secret.team_id,
|
|
246
|
+
claimed_at: secret.claimed_at
|
|
247
|
+
});
|
|
248
|
+
if (dryRun) {
|
|
249
|
+
this.recordTelemetry({ claim_outcome: "dry_run" });
|
|
250
|
+
return this.emit({
|
|
251
|
+
status: "skipped",
|
|
252
|
+
reason: "dry-run",
|
|
253
|
+
data: {
|
|
254
|
+
title: "Zitadel claim was not started.",
|
|
255
|
+
project_id: secret.project_id,
|
|
256
|
+
would: "Open a browser to attach this project to a team, then record the team in .zitadel/secret."
|
|
257
|
+
},
|
|
258
|
+
nextCommands: ["zitadel claim"]
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const client = createZitadelClient({
|
|
262
|
+
baseUrl: this.meta.source,
|
|
263
|
+
token: secret.project_secret
|
|
264
|
+
});
|
|
265
|
+
let challenge;
|
|
266
|
+
try {
|
|
267
|
+
challenge = await client.initClaim(secret.project_id);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
const claimed = alreadyClaimedDetails(error);
|
|
270
|
+
if (claimed) return this.alreadyClaimed({
|
|
271
|
+
project_id: secret.project_id,
|
|
272
|
+
...claimed
|
|
273
|
+
});
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
const deadline = claimDeadline({
|
|
277
|
+
expiresAt: challenge.expires_at,
|
|
278
|
+
timeoutSeconds: flags.timeout,
|
|
279
|
+
now: Date.now()
|
|
280
|
+
});
|
|
281
|
+
consola.box({
|
|
282
|
+
title: "Finish in your browser",
|
|
283
|
+
message: `${challenge.claim_url}\n\nSign in there to attach this project to your team.`,
|
|
284
|
+
style: {
|
|
285
|
+
padding: 1,
|
|
286
|
+
borderStyle: "rounded",
|
|
287
|
+
borderColor: "cyan"
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
const opened = flags["no-open"] || nonInteractive ? false : (await openInBrowser(challenge.claim_url)).opened;
|
|
291
|
+
if (!opened) consola.info("Open the link above to continue.");
|
|
292
|
+
consola.start("Waiting for the browser step to finish");
|
|
293
|
+
const completed = await this.poll(client, secret.project_id, challenge.challenge_id, deadline);
|
|
294
|
+
await writeZitadelSecret(cwd, {
|
|
295
|
+
...secret,
|
|
296
|
+
claimed_at: completed.claimed_at,
|
|
297
|
+
team_id: completed.team_id
|
|
298
|
+
});
|
|
299
|
+
consola.success(`Project attached to team ${completed.team_id}`);
|
|
300
|
+
this.recordTelemetry({
|
|
301
|
+
claim_outcome: "completed",
|
|
302
|
+
browser_opened: opened
|
|
303
|
+
});
|
|
304
|
+
return this.emit({
|
|
305
|
+
status: "ok",
|
|
306
|
+
data: {
|
|
307
|
+
title: "Zitadel project attached to a team.",
|
|
308
|
+
project_id: secret.project_id,
|
|
309
|
+
team_id: completed.team_id,
|
|
310
|
+
claimed_at: completed.claimed_at,
|
|
311
|
+
dashboard_url: completed.dashboard_url,
|
|
312
|
+
next_actions: [`Manage the project at ${completed.dashboard_url}.`]
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Polls `claim/status` until the browser leg lands or the link dies.
|
|
318
|
+
*
|
|
319
|
+
* The two failure modes read the same to a user (the link no longer works,
|
|
320
|
+
* start another one), so they share an error shape and differ only in
|
|
321
|
+
* wording — a `410` means the platform expired it, a passed deadline means
|
|
322
|
+
* we stopped waiting first.
|
|
323
|
+
*/
|
|
324
|
+
async poll(client, projectId, challengeId, deadline) {
|
|
325
|
+
let interval = INITIAL_POLL_MS;
|
|
326
|
+
let polls = 0;
|
|
327
|
+
while (Date.now() < deadline) {
|
|
328
|
+
polls += 1;
|
|
329
|
+
try {
|
|
330
|
+
const status = await client.getClaimStatus(projectId, { challenge_id: challengeId });
|
|
331
|
+
if (status.status === "completed") {
|
|
332
|
+
this.recordTelemetry({ poll_count: polls });
|
|
333
|
+
return status;
|
|
334
|
+
}
|
|
335
|
+
} catch (error) {
|
|
336
|
+
if (error instanceof ApiError && error.status === 410) {
|
|
337
|
+
this.recordTelemetry({
|
|
338
|
+
claim_outcome: "expired",
|
|
339
|
+
poll_count: polls
|
|
340
|
+
});
|
|
341
|
+
throw expiredError("The link expired before the browser step finished.");
|
|
342
|
+
}
|
|
343
|
+
if (error instanceof ApiError && error.status === 429) interval = MAX_POLL_MS;
|
|
344
|
+
else {
|
|
345
|
+
this.recordTelemetry({ poll_count: polls });
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const remaining = deadline - Date.now();
|
|
350
|
+
if (remaining <= 0) break;
|
|
351
|
+
await setTimeout(Math.min(interval, remaining));
|
|
352
|
+
interval = Math.min(interval * POLL_BACKOFF_FACTOR, MAX_POLL_MS);
|
|
353
|
+
}
|
|
354
|
+
this.recordTelemetry({
|
|
355
|
+
claim_outcome: "timeout",
|
|
356
|
+
poll_count: polls
|
|
357
|
+
});
|
|
358
|
+
throw expiredError("Stopped waiting for the browser step to finish.");
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* The idempotent outcome: the project already belongs to a team, whether we
|
|
362
|
+
* learned that locally or from a `409`. Reported as a skip, not an error, so
|
|
363
|
+
* re-running the command (or an agent retrying it) is a clean no-op.
|
|
364
|
+
*/
|
|
365
|
+
alreadyClaimed(data) {
|
|
366
|
+
this.recordTelemetry({ claim_outcome: "already_claimed" });
|
|
367
|
+
consola.info(`This project already belongs to team ${data.team_id}.`);
|
|
368
|
+
return this.emit({
|
|
369
|
+
status: "skipped",
|
|
370
|
+
reason: "already-claimed",
|
|
371
|
+
data: {
|
|
372
|
+
title: "Zitadel project already belongs to a team.",
|
|
373
|
+
...data
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
/**
|
|
379
|
+
* Extracts the owning team from a `409 proj.already_claimed` envelope. Reads
|
|
380
|
+
* the body defensively rather than casting to the generated type: this is the
|
|
381
|
+
* one place a malformed error body would otherwise turn a clean skip into a
|
|
382
|
+
* crash.
|
|
383
|
+
*/
|
|
384
|
+
function alreadyClaimedDetails(error) {
|
|
385
|
+
if (!(error instanceof ApiError) || error.status !== 409 || !isObject(error.body)) return;
|
|
386
|
+
const details = error.body.details;
|
|
387
|
+
if (!isObject(details) || typeof details.team_id !== "string") return;
|
|
388
|
+
return {
|
|
389
|
+
team_id: details.team_id,
|
|
390
|
+
dashboard_url: typeof details.dashboard_url === "string" ? details.dashboard_url : void 0
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function expiredError(message) {
|
|
394
|
+
return new ZitadelError("E_VALIDATION", message, {
|
|
395
|
+
hint: "Links are valid for 10 minutes. Start a new one.",
|
|
396
|
+
nextCommands: ["zitadel claim"]
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
//#endregion
|
|
400
|
+
export { claimDeadline, Claim as default };
|
|
401
|
+
|
|
402
|
+
//# sourceMappingURL=claim.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claim.mjs","names":["nodeSpawn","sleep"],"sources":["../../src/lib/browser.ts","../../src/commands/claim.ts"],"sourcesContent":["import { spawn as nodeSpawn } from \"node:child_process\";\nimport { access, readFile } from \"node:fs/promises\";\nimport { constants } from \"node:fs\";\nimport { delimiter, join } from \"node:path\";\n\n/**\n * Why a browser launch did not happen. Never an error condition: every reason\n * resolves to the same caller behaviour (show the link and let the user open\n * it themselves), so this exists for narration and telemetry, not control flow.\n *\n * - `headless` — a graphical session was not detected (no `DISPLAY`, no\n * `WAYLAND_DISPLAY`, or a plain SSH session). Spawning would either fail\n * noisily or open a browser on a machine nobody is looking at.\n * - `no-opener` — no known opener binary is on `PATH`. Common on minimal\n * containers and BSDs without `xdg-utils`.\n * - `disabled` — `BROWSER=none`, the convention honoured by webbrowser(3) and\n * most dev servers for \"never open anything\".\n * - `spawn-failed` — the opener exists but the process could not be started.\n */\nexport type OpenFailure = \"headless\" | \"no-opener\" | \"disabled\" | \"spawn-failed\";\n\n/** Outcome of {@link openInBrowser}. `opened` is best effort: see the note there. */\nexport type OpenResult = { opened: boolean; reason?: OpenFailure };\n\n/**\n * Injectable seams so the platform matrix is unit-testable without touching\n * the real machine: every environment probe this module makes goes through\n * one of these.\n */\nexport type BrowserDeps = {\n platform: NodeJS.Platform;\n env: NodeJS.ProcessEnv;\n /**\n * Resolves an executable name against `PATH`, or `undefined` if absent.\n * The default implementation reads `PATH` from {@link BrowserDeps.env}, not\n * from `process.env`, so overriding `env` alone is enough to redirect\n * executable resolution.\n */\n lookupPath: (command: string) => Promise<string | undefined>;\n /** Reads `/proc/version` for WSL detection; `undefined` when unreadable. */\n readProcVersion: () => Promise<string | undefined>;\n spawn: (command: string, args: string[]) => void;\n};\n\n/**\n * Openers tried in order on Linux and the BSDs. `xdg-open` is the standard and\n * covers any desktop that ships `xdg-utils`; the rest are the fallbacks that\n * exist on systems that don't (GNOME-only, Debian's alternatives system, KDE).\n * `gio` takes a subcommand, so it carries its own argv shape.\n */\nconst UNIX_OPENERS: ReadonlyArray<{ command: string; args: (url: string) => string[] }> = [\n { command: \"xdg-open\", args: (url) => [url] },\n { command: \"gio\", args: (url) => [\"open\", url] },\n { command: \"x-www-browser\", args: (url) => [url] },\n { command: \"sensible-browser\", args: (url) => [url] },\n { command: \"gnome-open\", args: (url) => [url] },\n { command: \"kde-open\", args: (url) => [url] },\n];\n\n/**\n * Opens `url` in the user's default browser, best effort.\n *\n * Best effort is the contract, not a caveat: this never throws and never\n * reports a failure the caller must handle. There is no portable way to know a\n * browser actually rendered the page — the opener returns as soon as it has\n * handed the URL off — so `opened: true` means only \"an opener was started\".\n * Callers must print the URL regardless and treat the launch as a convenience\n * on top of it, which is what makes headless machines, bare containers, and\n * SSH sessions work without a special case.\n */\nexport async function openInBrowser(\n url: string,\n overrides: Partial<BrowserDeps> = {},\n): Promise<OpenResult> {\n // Resolve `env` first so the default `lookupPath` closes over the *effective*\n // environment: overriding `env.PATH` alone must redirect executable\n // resolution, without the caller also having to replace `lookupPath`.\n const env = overrides.env ?? process.env;\n const deps: BrowserDeps = { ...defaultDeps(env), ...overrides };\n\n const browser = deps.env.BROWSER?.trim();\n if (browser === \"none\") {\n return { opened: false, reason: \"disabled\" };\n }\n if (browser) {\n return launch(deps, browser, [url]);\n }\n\n if (deps.platform === \"darwin\") {\n return launch(deps, \"open\", [url]);\n }\n if (deps.platform === \"win32\") {\n // `start` is a cmd builtin, not an executable, and its first quoted\n // argument is taken as the window title — hence the empty \"\" before the\n // URL, without which a quoted URL would be swallowed as the title.\n return launch(deps, \"cmd\", [\"/c\", \"start\", \"\", url]);\n }\n\n if (await isWsl(deps)) {\n // Inside WSL the browser lives on the Windows side. `wslview` (wslu) is\n // the purpose-built bridge; PowerShell is the fallback every WSL install\n // has.\n const wslview = await deps.lookupPath(\"wslview\");\n if (wslview) {\n return launch(deps, \"wslview\", [url]);\n }\n return launch(deps, \"powershell.exe\", [\"-NoProfile\", \"-Command\", \"Start-Process\", url]);\n }\n\n if (!hasGraphicalSession(deps.env)) {\n return { opened: false, reason: \"headless\" };\n }\n\n for (const opener of UNIX_OPENERS) {\n if (await deps.lookupPath(opener.command)) {\n return launch(deps, opener.command, opener.args(url));\n }\n }\n return { opened: false, reason: \"no-opener\" };\n}\n\n/**\n * Whether a graphical session is plausibly present and worth opening into.\n *\n * X11 and Wayland both advertise themselves through an env var. SSH is\n * excluded even when `DISPLAY` is set, because X11 forwarding sets it: the\n * browser would launch on the remote host and, at best, tunnel a window back\n * over a link nobody asked to render one on.\n */\nfunction hasGraphicalSession(env: NodeJS.ProcessEnv): boolean {\n if (env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT) {\n return false;\n }\n return Boolean(env.DISPLAY || env.WAYLAND_DISPLAY);\n}\n\n/**\n * WSL advertises itself through `WSL_DISTRO_NAME`/`WSLENV`, but neither is set\n * under every shell, so fall back to the kernel release string, which carries\n * `microsoft` on all WSL kernels.\n */\nasync function isWsl(deps: BrowserDeps): Promise<boolean> {\n if (deps.platform !== \"linux\") {\n return false;\n }\n if (deps.env.WSL_DISTRO_NAME || deps.env.WSLENV) {\n return true;\n }\n const version = await deps.readProcVersion();\n return version !== undefined && version.toLowerCase().includes(\"microsoft\");\n}\n\n/**\n * Starts the opener fully detached: the CLI keeps polling for the claim while\n * the browser lives on, and must not hold the event loop open waiting for a\n * process it does not own. Output is discarded because openers write\n * diagnostics to stderr that would interleave with the CLI's own narration.\n */\nfunction launch(deps: BrowserDeps, command: string, args: string[]): OpenResult {\n try {\n deps.spawn(command, args);\n return { opened: true };\n } catch {\n return { opened: false, reason: \"spawn-failed\" };\n }\n}\n\nfunction defaultDeps(env: NodeJS.ProcessEnv): BrowserDeps {\n return {\n platform: process.platform,\n env,\n lookupPath: (command) => lookupPath(command, env.PATH ?? \"\"),\n readProcVersion: async () => {\n try {\n return await readFile(\"/proc/version\", \"utf8\");\n } catch {\n return undefined;\n }\n },\n spawn: (command, args) => {\n const child = nodeSpawn(command, args, { stdio: \"ignore\", detached: true });\n // A spawn error surfaces asynchronously; swallow it so an opener that\n // vanishes between the PATH probe and the spawn cannot crash the CLI\n // with an unhandled 'error' event.\n child.on(\"error\", () => undefined);\n child.unref();\n },\n };\n}\n\n/**\n * Minimal `which`: the first entry of `pathEnv` holding an executable\n * `command`. Probing the filesystem directly avoids shelling out (and avoids\n * depending on a `which`/`where` binary being present on the very systems that\n * are missing `xdg-open`). `pathEnv` is passed in rather than read from\n * `process.env` so it follows whatever environment the caller injected.\n */\nasync function lookupPath(command: string, pathEnv: string): Promise<string | undefined> {\n for (const dir of pathEnv.split(delimiter).filter(Boolean)) {\n const candidate = join(dir, command);\n try {\n await access(candidate, constants.X_OK);\n return candidate;\n } catch {\n continue;\n }\n }\n return undefined;\n}\n","import { setTimeout as sleep } from \"node:timers/promises\";\n\nimport { Flags } from \"@oclif/core\";\nimport { createZitadelClient } from \"@zitadel/api/client\";\nimport { ApiError } from \"@zitadel/api/runtime/fetch\";\nimport consola from \"consola\";\n\nimport { openInBrowser } from \"../lib/browser\";\nimport { isAttached } from \"../lib/claim-state\";\nimport { ZitadelError } from \"../lib/errors\";\nimport { isObject } from \"../lib/json\";\nimport { BaseCommand, type JsonEnvelope } from \"../lib/oclif\";\nimport { readZitadelSecret, writeZitadelSecret, type ZitadelSecret } from \"../lib/project\";\n\n/**\n * Poll cadence while a human completes the browser step. Starts responsive so\n * a claim finished in a few seconds is reported almost immediately, then eases\n * off so a link left open for the full ten minutes costs a few hundred\n * requests rather than a few thousand.\n */\nconst INITIAL_POLL_MS = 1000;\nconst MAX_POLL_MS = 5000;\nconst POLL_BACKOFF_FACTOR = 1.5;\n\n/**\n * Backstop for the poll deadline, used only when the server's own `expires_at`\n * cannot be parsed. The challenge TTL is the platform's to decide (ADR 046) and\n * the response carries it, so this is never the normal path — but without it a\n * malformed or misrouted response would leave the loop with no deadline at all\n * and the CLI polling forever. Matches the TTL the ADR documents.\n */\nconst FALLBACK_TTL_MS = 10 * 60 * 1000;\n\n/**\n * When to stop polling: the earlier of the server's own expiry and any\n * `--timeout` the caller set.\n *\n * Trusts `expires_at` rather than assuming the TTL, because the contract\n * documents it as the authority and the CLI should not go stale if the platform\n * ever retunes it. An unparseable value falls back to {@link FALLBACK_TTL_MS}\n * rather than to no deadline: a malformed or misrouted response must not leave\n * the loop running forever. Exported so that fallback is testable without\n * waiting out a real TTL.\n */\nexport function claimDeadline(input: {\n expiresAt: string;\n timeoutSeconds?: number;\n now: number;\n}): number {\n const parsed = Date.parse(input.expiresAt);\n const serverDeadline = Number.isNaN(parsed) ? input.now + FALLBACK_TTL_MS : parsed;\n if (input.timeoutSeconds === undefined) {\n return serverDeadline;\n }\n return Math.min(serverDeadline, input.now + input.timeoutSeconds * 1000);\n}\n\n/**\n * `zitadel claim` — attach this project to a team.\n *\n * Shaped like the device-authorization grant (ADR 046): the CLI mints a\n * challenge with the project secret, hands a URL to a browser, and polls until\n * the human finishes. The project secret never leaves the machine and the\n * browser only ever sees the challenge id.\n *\n * The command blocks for as long as the link is valid. That is the point: the\n * developer is looking at the terminal, finishes in the browser, and comes\n * back to a terminal that already knows. Nothing about the project changes —\n * the issuer, users, and applications keep working exactly as before, so this\n * is purely additive.\n */\nexport default class Claim extends BaseCommand {\n static override description =\n \"Attach this project to a team so it becomes permanent. Opens a browser to finish signing in.\";\n\n static override examples = [\n \"<%= config.bin %> <%= command.id %>\",\n \"<%= config.bin %> <%= command.id %> --no-open\",\n \"<%= config.bin %> <%= command.id %> --timeout 120\",\n ];\n\n static override flags = {\n \"no-open\": Flags.boolean({\n description: \"Print the link instead of opening a browser.\",\n }),\n timeout: Flags.integer({\n min: 1,\n description:\n \"Seconds to wait for the browser step. Defaults to the link's own expiry (10 minutes).\",\n }),\n };\n\n async run(): Promise<JsonEnvelope> {\n const { flags } = await this.parse(Claim);\n await this.toMeta(flags);\n const { cwd, dryRun, nonInteractive } = this.meta;\n\n const secret = await readZitadelSecret(cwd);\n // The local record is authoritative enough to skip the round trip: the\n // platform enforces first-claim-wins anyway, so re-asking could only ever\n // return the same answer at the cost of a request.\n if (isAttached(secret)) {\n return this.alreadyClaimed({\n project_id: secret.project_id,\n team_id: secret.team_id,\n claimed_at: secret.claimed_at,\n });\n }\n\n // `--dry-run` promises to mutate neither files nor the platform, so it has\n // to stop before `initClaim`: minting a challenge is a platform write, and\n // a developer who then finished the browser step would really claim the\n // project while this run deliberately skipped recording it — the local file\n // and the platform would disagree, which is worse than not previewing at\n // all. There is nothing further to preview here anyway: the outcome of a\n // claim is decided in a browser, not by anything the CLI could compute.\n if (dryRun) {\n this.recordTelemetry({ claim_outcome: \"dry_run\" });\n return this.emit({\n status: \"skipped\",\n reason: \"dry-run\",\n data: {\n title: \"Zitadel claim was not started.\",\n project_id: secret.project_id,\n would: \"Open a browser to attach this project to a team, then record the team in .zitadel/secret.\",\n },\n nextCommands: [\"zitadel claim\"],\n });\n }\n\n const client = createZitadelClient({\n baseUrl: this.meta.source,\n token: secret.project_secret,\n });\n\n let challenge;\n try {\n challenge = await client.initClaim(secret.project_id);\n } catch (error) {\n const claimed = alreadyClaimedDetails(error);\n if (claimed) {\n return this.alreadyClaimed({ project_id: secret.project_id, ...claimed });\n }\n throw error;\n }\n\n const deadline = claimDeadline({\n expiresAt: challenge.expires_at,\n timeoutSeconds: flags.timeout,\n now: Date.now(),\n });\n\n // Always show the link first, before attempting anything: it is the whole\n // instruction on its own, so a launch that never happens (headless box,\n // no `xdg-open`, `--no-open`, an agent) needs no separate path.\n consola.box({\n title: \"Finish in your browser\",\n message: `${challenge.claim_url}\\n\\nSign in there to attach this project to your team.`,\n style: { padding: 1, borderStyle: \"rounded\", borderColor: \"cyan\" },\n });\n\n const skipLaunch = flags[\"no-open\"] || nonInteractive;\n const opened = skipLaunch ? false : (await openInBrowser(challenge.claim_url)).opened;\n if (!opened) {\n consola.info(\"Open the link above to continue.\");\n }\n consola.start(\"Waiting for the browser step to finish\");\n\n const completed = await this.poll(client, secret.project_id, challenge.challenge_id, deadline);\n\n const next: ZitadelSecret = {\n ...secret,\n claimed_at: completed.claimed_at,\n team_id: completed.team_id,\n };\n // Unconditional: `--dry-run` never reaches here (it returns above), so a\n // claim that got this far really happened on the platform and the local\n // record must follow it.\n await writeZitadelSecret(cwd, next);\n consola.success(`Project attached to team ${completed.team_id}`);\n\n this.recordTelemetry({ claim_outcome: \"completed\", browser_opened: opened });\n return this.emit({\n status: \"ok\",\n data: {\n title: \"Zitadel project attached to a team.\",\n project_id: secret.project_id,\n team_id: completed.team_id,\n claimed_at: completed.claimed_at,\n dashboard_url: completed.dashboard_url,\n next_actions: [`Manage the project at ${completed.dashboard_url}.`],\n },\n });\n }\n\n /**\n * Polls `claim/status` until the browser leg lands or the link dies.\n *\n * The two failure modes read the same to a user (the link no longer works,\n * start another one), so they share an error shape and differ only in\n * wording — a `410` means the platform expired it, a passed deadline means\n * we stopped waiting first.\n */\n private async poll(\n client: ReturnType<typeof createZitadelClient>,\n projectId: string,\n challengeId: string,\n deadline: number,\n ): Promise<{ team_id: string; claimed_at: string; dashboard_url: string }> {\n let interval = INITIAL_POLL_MS;\n let polls = 0;\n\n while (Date.now() < deadline) {\n polls += 1;\n try {\n const status = await client.getClaimStatus(projectId, { challenge_id: challengeId });\n if (status.status === \"completed\") {\n this.recordTelemetry({ poll_count: polls });\n return status;\n }\n } catch (error) {\n if (error instanceof ApiError && error.status === 410) {\n this.recordTelemetry({ claim_outcome: \"expired\", poll_count: polls });\n throw expiredError(\"The link expired before the browser step finished.\");\n }\n if (error instanceof ApiError && error.status === 429) {\n // Polled too eagerly. Back off to the ceiling immediately rather\n // than easing towards it, so a rate limit is never compounded.\n interval = MAX_POLL_MS;\n } else {\n this.recordTelemetry({ poll_count: polls });\n throw error;\n }\n }\n\n // Never sleep past the deadline: the last wait should end the loop, not\n // overshoot it by up to a full interval.\n const remaining = deadline - Date.now();\n if (remaining <= 0) {\n break;\n }\n await sleep(Math.min(interval, remaining));\n interval = Math.min(interval * POLL_BACKOFF_FACTOR, MAX_POLL_MS);\n }\n\n this.recordTelemetry({ claim_outcome: \"timeout\", poll_count: polls });\n throw expiredError(\"Stopped waiting for the browser step to finish.\");\n }\n\n /**\n * The idempotent outcome: the project already belongs to a team, whether we\n * learned that locally or from a `409`. Reported as a skip, not an error, so\n * re-running the command (or an agent retrying it) is a clean no-op.\n */\n private alreadyClaimed(data: {\n project_id: string;\n team_id: string;\n claimed_at?: string;\n dashboard_url?: string;\n }): JsonEnvelope {\n this.recordTelemetry({ claim_outcome: \"already_claimed\" });\n consola.info(`This project already belongs to team ${data.team_id}.`);\n return this.emit({\n status: \"skipped\",\n reason: \"already-claimed\",\n data: { title: \"Zitadel project already belongs to a team.\", ...data },\n });\n }\n}\n\n/**\n * Extracts the owning team from a `409 proj.already_claimed` envelope. Reads\n * the body defensively rather than casting to the generated type: this is the\n * one place a malformed error body would otherwise turn a clean skip into a\n * crash.\n */\nfunction alreadyClaimedDetails(\n error: unknown,\n): { team_id: string; dashboard_url?: string } | undefined {\n if (!(error instanceof ApiError) || error.status !== 409 || !isObject(error.body)) {\n return undefined;\n }\n const details = error.body.details;\n if (!isObject(details) || typeof details.team_id !== \"string\") {\n return undefined;\n }\n return {\n team_id: details.team_id,\n dashboard_url: typeof details.dashboard_url === \"string\" ? details.dashboard_url : undefined,\n };\n}\n\nfunction expiredError(message: string): ZitadelError {\n return new ZitadelError(\"E_VALIDATION\", message, {\n hint: \"Links are valid for 10 minutes. Start a new one.\",\n nextCommands: [\"zitadel claim\"],\n });\n}\n"],"mappings":"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"}
|