@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.
Files changed (58) hide show
  1. package/README.md +145 -107
  2. package/SKILLS.md +88 -12
  3. package/dist/branding-BXj028-W.mjs +79 -0
  4. package/dist/branding-BXj028-W.mjs.map +1 -0
  5. package/dist/claim-state-BTGInAaN.mjs +66 -0
  6. package/dist/claim-state-BTGInAaN.mjs.map +1 -0
  7. package/dist/commands/apply.mjs +6 -5
  8. package/dist/commands/apply.mjs.map +1 -1
  9. package/dist/commands/branding/eject.mjs +125 -0
  10. package/dist/commands/branding/eject.mjs.map +1 -0
  11. package/dist/commands/claim.mjs +402 -0
  12. package/dist/commands/claim.mjs.map +1 -0
  13. package/dist/commands/doctor.mjs +477 -37
  14. package/dist/commands/doctor.mjs.map +1 -1
  15. package/dist/commands/eject.mjs +3 -3
  16. package/dist/commands/logs.mjs +2 -2
  17. package/dist/commands/plan.mjs +6 -5
  18. package/dist/commands/plan.mjs.map +1 -1
  19. package/dist/commands/reset.mjs +2 -2
  20. package/dist/commands/schemas/list.mjs +8 -8
  21. package/dist/commands/schemas/list.mjs.map +1 -1
  22. package/dist/commands/setup.mjs +305 -36
  23. package/dist/commands/setup.mjs.map +1 -1
  24. package/dist/commands/start.mjs +3 -3
  25. package/dist/commands/status.mjs +21 -13
  26. package/dist/commands/status.mjs.map +1 -1
  27. package/dist/commands/stop.mjs +3 -3
  28. package/dist/commands/stop.mjs.map +1 -1
  29. package/dist/{docker-pGcTrDIY.mjs → docker-Ciz9N6uI.mjs} +2 -2
  30. package/dist/{docker-pGcTrDIY.mjs.map → docker-Ciz9N6uI.mjs.map} +1 -1
  31. package/dist/{environment-BQF7LeCz.mjs → environment-CuHKxtIO.mjs} +1 -1
  32. package/dist/{environment-BQF7LeCz.mjs.map → environment-CuHKxtIO.mjs.map} +1 -1
  33. package/dist/{journey-guidance-CYVpQszz.mjs → journey-guidance-CtQUfWar.mjs} +14 -3
  34. package/dist/{journey-guidance-CYVpQszz.mjs.map → journey-guidance-CtQUfWar.mjs.map} +1 -1
  35. package/dist/{oclif-BRdjVNne.mjs → oclif-CL6-M13m.mjs} +201 -43
  36. package/dist/oclif-CL6-M13m.mjs.map +1 -0
  37. package/dist/{orca-Dip6DPJ2.mjs → orca-BWB4zOdw.mjs} +749 -192
  38. package/dist/orca-BWB4zOdw.mjs.map +1 -0
  39. package/dist/{ports-V8JfiNgT.mjs → ports-Cu2Y7aXQ.mjs} +2 -2
  40. package/dist/{ports-V8JfiNgT.mjs.map → ports-Cu2Y7aXQ.mjs.map} +1 -1
  41. package/dist/{processes-BVqYsxT8.mjs → processes-Bn2WngOi.mjs} +1 -1
  42. package/dist/{processes-BVqYsxT8.mjs.map → processes-Bn2WngOi.mjs.map} +1 -1
  43. package/dist/{project-BGgv7Bba.mjs → project-yejJSfdJ.mjs} +57 -4
  44. package/dist/project-yejJSfdJ.mjs.map +1 -0
  45. package/dist/scaffold-manifest-CKNFLX9T.mjs +81 -0
  46. package/dist/scaffold-manifest-CKNFLX9T.mjs.map +1 -0
  47. package/dist/{sync-Cd3z7Q1R.mjs → sync-Cx2_4rXW.mjs} +99 -50
  48. package/dist/sync-Cx2_4rXW.mjs.map +1 -0
  49. package/dist/user-schema-W1OsXdpc.mjs +91 -0
  50. package/dist/user-schema-W1OsXdpc.mjs.map +1 -0
  51. package/oclif.manifest.json +227 -5
  52. package/package.json +8 -6
  53. package/dist/oclif-BRdjVNne.mjs.map +0 -1
  54. package/dist/orca-Dip6DPJ2.mjs.map +0 -1
  55. package/dist/project-BGgv7Bba.mjs.map +0 -1
  56. package/dist/sync-Cd3z7Q1R.mjs.map +0 -1
  57. package/dist/user-schema-DDz5-lX5.mjs +0 -13
  58. package/dist/user-schema-DDz5-lX5.mjs.map +0 -1
@@ -0,0 +1,402 @@
1
+ import { E as ZitadelError, S as isObject, t as BaseCommand } from "../oclif-CL6-M13m.mjs";
2
+ import { i as isAttached } from "../claim-state-BTGInAaN.mjs";
3
+ import { l as readZitadelSecret, u as writeZitadelSecret } from "../project-yejJSfdJ.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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"}