@zitadel/cli 1.0.0-alpha.21 → 1.0.0-alpha.22
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/SKILLS.md +20 -4
- package/dist/box-B_mwKole.mjs +35 -0
- package/dist/box-B_mwKole.mjs.map +1 -0
- package/dist/{branding-cygSBPkV.mjs → branding-DvoH8e0D.mjs} +2 -2
- package/dist/{branding-cygSBPkV.mjs.map → branding-DvoH8e0D.mjs.map} +1 -1
- package/dist/claim-state-B6ZSKmhW.mjs +125 -0
- package/dist/claim-state-B6ZSKmhW.mjs.map +1 -0
- package/dist/commands/apply.mjs +4 -4
- package/dist/commands/branding/eject.mjs +3 -3
- package/dist/commands/claim.mjs +34 -4
- package/dist/commands/claim.mjs.map +1 -1
- package/dist/commands/doctor.mjs +33 -17
- package/dist/commands/doctor.mjs.map +1 -1
- package/dist/commands/eject.mjs +3 -3
- package/dist/commands/logs.mjs +2 -2
- package/dist/commands/plan.mjs +4 -4
- package/dist/commands/reset.mjs +2 -2
- package/dist/commands/schemas/list.mjs +3 -3
- package/dist/commands/setup.mjs +56 -18
- package/dist/commands/setup.mjs.map +1 -1
- package/dist/commands/start.mjs +3 -3
- package/dist/commands/status.mjs +6 -6
- package/dist/commands/status.mjs.map +1 -1
- package/dist/commands/stop.mjs +3 -3
- package/dist/{docker-Ev1Ggt9l.mjs → docker-B1MjOMFo.mjs} +2 -2
- package/dist/{docker-Ev1Ggt9l.mjs.map → docker-B1MjOMFo.mjs.map} +1 -1
- package/dist/{environment-rjRVkJjW.mjs → environment-fibXmzM8.mjs} +1 -1
- package/dist/{environment-rjRVkJjW.mjs.map → environment-fibXmzM8.mjs.map} +1 -1
- package/dist/{journey-guidance-BGrOX_gT.mjs → journey-guidance-C9MxJNTg.mjs} +2 -2
- package/dist/{journey-guidance-BGrOX_gT.mjs.map → journey-guidance-C9MxJNTg.mjs.map} +1 -1
- package/dist/{oclif-CanO3zdt.mjs → oclif-JlsN1lfO.mjs} +1 -1
- package/dist/{oclif-CanO3zdt.mjs.map → oclif-JlsN1lfO.mjs.map} +1 -1
- package/dist/{orca-66vAxeIe.mjs → orca-CPMHzmg3.mjs} +2 -2
- package/dist/{orca-66vAxeIe.mjs.map → orca-CPMHzmg3.mjs.map} +1 -1
- package/dist/{package-manager-DFdLcDx1.mjs → package-manager-CDUsoGBy.mjs} +3 -3
- package/dist/{package-manager-DFdLcDx1.mjs.map → package-manager-CDUsoGBy.mjs.map} +1 -1
- package/dist/{ports-BM20XIZb.mjs → ports-hidBnSW-.mjs} +2 -2
- package/dist/{ports-BM20XIZb.mjs.map → ports-hidBnSW-.mjs.map} +1 -1
- package/dist/{processes-DKHPkU8O.mjs → processes-Cv19mJ2I.mjs} +1 -1
- package/dist/{processes-DKHPkU8O.mjs.map → processes-Cv19mJ2I.mjs.map} +1 -1
- package/dist/{project-CHhot85s.mjs → project-Ba0nriNm.mjs} +2 -2
- package/dist/{project-CHhot85s.mjs.map → project-Ba0nriNm.mjs.map} +1 -1
- package/dist/{sync-BBlACKzT.mjs → sync-ChlLQ4vZ.mjs} +4 -4
- package/dist/{sync-BBlACKzT.mjs.map → sync-ChlLQ4vZ.mjs.map} +1 -1
- package/dist/{user-schema-DTuOsdKE.mjs → user-schema-C2mC-JTN.mjs} +1 -1
- package/dist/{user-schema-DTuOsdKE.mjs.map → user-schema-C2mC-JTN.mjs.map} +1 -1
- package/oclif.manifest.json +1 -1
- package/package.json +7 -5
- package/dist/claim-state-DlfD6Res.mjs +0 -66
- package/dist/claim-state-DlfD6Res.mjs.map +0 -1
package/SKILLS.md
CHANGED
|
@@ -210,21 +210,37 @@ the CLI's help layer, not the envelope.
|
|
|
210
210
|
safely. The link is always printed before any browser opens, so a headless
|
|
211
211
|
machine, an SSH session, or `--no-open` needs no special handling — copy it
|
|
212
212
|
and open it anywhere. Links last 10 minutes; once one lapses the command
|
|
213
|
-
exits `E_VALIDATION` and points at a fresh run.
|
|
213
|
+
exits `E_VALIDATION` and points at a fresh run. Claiming itself is only
|
|
214
|
+
possible within 14 days of project creation: past that the platform answers
|
|
215
|
+
`410 proj.claim_window_expired`, the command exits `E_VALIDATION`, and a
|
|
216
|
+
fresh link does **not** help — only a fresh `setup` yields a claimable
|
|
217
|
+
project (the old one keeps working, it just can't be attached anymore).
|
|
218
|
+
`--dry-run` stops before
|
|
214
219
|
anything is minted and reports `status: "skipped"`, `reason: "dry-run"` —
|
|
215
220
|
there is nothing to preview, because a claim is decided in a browser.
|
|
216
221
|
Flags: `--no-open` (print the link instead of launching a browser),
|
|
217
222
|
`--timeout <seconds>` (stop waiting sooner than the link's own expiry).
|
|
218
223
|
`setup`, `status`, and `doctor` report whether a team is attached, reading
|
|
219
224
|
`claimed_at`/`team_id` from `.zitadel/secret` (no platform call). `status`
|
|
220
|
-
carries `data.project.claim` as
|
|
225
|
+
carries `data.project.claim` as
|
|
226
|
+
`{"kind": "detached", "claimable": true, "deadline": "2026-09-18T09:00:00.000Z"}`
|
|
227
|
+
(`claimable` flips to `false` once the locally recorded creation time says
|
|
228
|
+
the 14-day window has passed; the guidance then switches to reconciliation
|
|
229
|
+
wording but the claim command stays in `next_commands`, because the local
|
|
230
|
+
record can be stale and running `claim` answers authoritatively — an
|
|
231
|
+
attached project skips cleanly. `deadline` is omitted when the creation
|
|
232
|
+
time is unknown) or
|
|
221
233
|
`{"kind": "attached", "team_id": "team_01H…", "claimed_at": "2026-08-01T09:00:00.000Z"}`,
|
|
222
234
|
and `doctor` reports a
|
|
223
235
|
`claim` check. A project with no team is only ever a **warning**, never a
|
|
224
236
|
failure — it works exactly like one with a team, so `doctor` still exits 0
|
|
225
237
|
and `--fix` deliberately does nothing (a claim needs a human in a browser).
|
|
226
|
-
|
|
227
|
-
|
|
238
|
+
`status` and `doctor` stay silent about teams off the cloud: they answer
|
|
239
|
+
offline and cannot know whether a local or self-hosted server hosts a
|
|
240
|
+
platform to claim into. `setup` is online anyway, so against a local server
|
|
241
|
+
it probes the runtime document and nudges only when the server hosts the
|
|
242
|
+
platform plane (`platform.bootstrap_project`), where the claim can actually
|
|
243
|
+
complete.
|
|
228
244
|
- `status` — summarize the local runtime and project state.
|
|
229
245
|
- `eject` (alias `uninstall`) — remove managed files and local Zitadel state;
|
|
230
246
|
requires `--force` when non-interactive.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import wrapAnsi from "wrap-ansi";
|
|
3
|
+
//#region src/lib/box.ts
|
|
4
|
+
/**
|
|
5
|
+
* Frame overhead of a `consola.box` with `padding: 1`: two border columns,
|
|
6
|
+
* the padding columns, and consola's left margin. Content wrapped to the
|
|
7
|
+
* terminal width minus this stays inside the frame instead of soft-wrapping
|
|
8
|
+
* under the right border.
|
|
9
|
+
*/
|
|
10
|
+
const BOX_OVERHEAD = 8;
|
|
11
|
+
const MIN_CONTENT_WIDTH = 20;
|
|
12
|
+
const MAX_TERMINAL_WIDTH = 80;
|
|
13
|
+
/** Bold cyan, for commands the user should run (URLs/paths stay plain cyan). */
|
|
14
|
+
const command = (s) => pc.bold(pc.cyan(s));
|
|
15
|
+
/**
|
|
16
|
+
* Wraps box content to the terminal (capped at 80 columns) so consola's
|
|
17
|
+
* frame, which sizes itself to the longest content line, never exceeds the
|
|
18
|
+
* window. ANSI-aware, preserves hard line breaks and indentation, and never
|
|
19
|
+
* splits a token: URLs and commands stay whole for clicking, copying, and the
|
|
20
|
+
* journey e2e scrape (claim.spec.ts), even when such a token alone is wider
|
|
21
|
+
* than a very narrow window.
|
|
22
|
+
*/
|
|
23
|
+
function wrapForBox(message, columns = process.stdout.columns) {
|
|
24
|
+
return wrapAnsi(message, Math.max(MIN_CONTENT_WIDTH, Math.min(columns ?? MAX_TERMINAL_WIDTH, MAX_TERMINAL_WIDTH) - BOX_OVERHEAD), {
|
|
25
|
+
hard: false,
|
|
26
|
+
trim: false
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function renderBoxActions(actions) {
|
|
30
|
+
return actions.map((action) => action.command ? `${action.text}\n\n ${command(action.command)}` : action.text).join("\n\n");
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { wrapForBox as n, renderBoxActions as t };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=box-B_mwKole.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"box-B_mwKole.mjs","names":[],"sources":["../src/lib/box.ts"],"sourcesContent":["import pc from \"picocolors\";\nimport wrapAnsi from \"wrap-ansi\";\n\n/**\n * Frame overhead of a `consola.box` with `padding: 1`: two border columns,\n * the padding columns, and consola's left margin. Content wrapped to the\n * terminal width minus this stays inside the frame instead of soft-wrapping\n * under the right border.\n */\nconst BOX_OVERHEAD = 8;\nconst MIN_CONTENT_WIDTH = 20;\nconst MAX_TERMINAL_WIDTH = 80;\n\n/** Bold cyan, for commands the user should run (URLs/paths stay plain cyan). */\nexport const command = (s: string): string => pc.bold(pc.cyan(s));\n\n/**\n * Wraps box content to the terminal (capped at 80 columns) so consola's\n * frame, which sizes itself to the longest content line, never exceeds the\n * window. ANSI-aware, preserves hard line breaks and indentation, and never\n * splits a token: URLs and commands stay whole for clicking, copying, and the\n * journey e2e scrape (claim.spec.ts), even when such a token alone is wider\n * than a very narrow window.\n */\nexport function wrapForBox(message: string, columns = process.stdout.columns): string {\n const width = Math.max(\n MIN_CONTENT_WIDTH,\n Math.min(columns ?? MAX_TERMINAL_WIDTH, MAX_TERMINAL_WIDTH) - BOX_OVERHEAD,\n );\n return wrapAnsi(message, width, { hard: false, trim: false });\n}\n\n/**\n * A next-step line for the human box: prose plus, when the step is literally\n * a command to run, that command pulled out of the sentence. The JSON\n * envelope keeps its own plain `next_actions`/`next_commands` strings; this\n * shape exists only so {@link renderBoxActions} can put the command alone on\n * an indented styled line, where it stands out and a drag-select copies it\n * without surrounding prose. (Line selection such as triple-click still\n * includes the box border and padding — commands that must copy as a whole\n * physical line belong outside a frame, like the claim URL.)\n */\nexport type BoxAction = { text: string; command?: string };\n\nexport function renderBoxActions(actions: BoxAction[]): string {\n return actions\n .map((action) =>\n action.command ? `${action.text}\\n\\n ${command(action.command)}` : action.text,\n )\n .join(\"\\n\\n\");\n}\n"],"mappings":";;;;;;;;;AASA,MAAM,eAAe;AACrB,MAAM,oBAAoB;AAC1B,MAAM,qBAAqB;;AAG3B,MAAa,WAAW,MAAsB,GAAG,KAAK,GAAG,KAAK,EAAE,CAAC;;;;;;;;;AAUjE,SAAgB,WAAW,SAAiB,UAAU,QAAQ,OAAO,SAAiB;AAKpF,QAAO,SAAS,SAJF,KAAK,IACjB,mBACA,KAAK,IAAI,WAAW,oBAAoB,mBAAmB,GAAG,aAElC,EAAE;EAAE,MAAM;EAAO,MAAM;EAAO,CAAC;;AAe/D,SAAgB,iBAAiB,SAA8B;AAC7D,QAAO,QACJ,KAAK,WACJ,OAAO,UAAU,GAAG,OAAO,KAAK,QAAQ,QAAQ,OAAO,QAAQ,KAAK,OAAO,KAC5E,CACA,KAAK,OAAO"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { E as ZitadelError } from "./oclif-
|
|
1
|
+
import { E as ZitadelError } from "./oclif-JlsN1lfO.mjs";
|
|
2
2
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
//#region src/lib/branding/index.ts
|
|
@@ -76,4 +76,4 @@ function toLocalBrandingBody(canonicalWire, localData) {
|
|
|
76
76
|
//#endregion
|
|
77
77
|
export { toLocalBrandingBody as a, toBrandingWireBody as i, readDescriptorTemplate as n, resolveTemplatePath as r, BRANDING_DIR as t };
|
|
78
78
|
|
|
79
|
-
//# sourceMappingURL=branding-
|
|
79
|
+
//# sourceMappingURL=branding-DvoH8e0D.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"branding-
|
|
1
|
+
{"version":3,"file":"branding-DvoH8e0D.mjs","names":[],"sources":["../src/lib/branding/index.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { isAbsolute, join, relative, resolve } from \"node:path\";\n\nimport { ZitadelError } from \"../errors\";\n\n/**\n * Relative directory (from the project root) where local branding files live:\n * the `branding.json` descriptor plus the sibling `.liquid` template it\n * references. Owned here so commands, syncers, and tests share one source of\n * truth; only `*.json` files are sync-discovered, the template rides along\n * via `liquid_template_file`.\n */\nexport const BRANDING_DIR = \".zitadel/branding\";\n\ntype BrandingDescriptor = {\n liquid_template?: string;\n liquid_template_file?: string;\n [key: string]: unknown;\n};\n\n/**\n * Resolves a descriptor's `liquid_template_file` reference to an absolute\n * path. References are relative to the descriptor's directory (all\n * descriptors live flat in {@link BRANDING_DIR}) and must stay inside the\n * project — a reference escaping `cwd` is an error because apply would later\n * write server state back to that path.\n */\nexport function resolveTemplatePath(cwd: string, ref: string): string {\n const absolute = isAbsolute(ref) ? ref : resolve(join(cwd, BRANDING_DIR), ref);\n const rel = relative(cwd, absolute);\n if (rel.startsWith(\"..\") || isAbsolute(rel)) {\n throw new ZitadelError(\n \"E_VALIDATION\",\n `liquid_template_file ${JSON.stringify(ref)} points outside the project`,\n { hint: `Keep the template next to its descriptor in ${BRANDING_DIR}/.` },\n );\n }\n return absolute;\n}\n\n/**\n * Returns the template string a descriptor carries — the inline\n * `liquid_template`, or the content of `liquid_template_file`. Returns\n * undefined when the descriptor has neither (a legal descriptor that only\n * sets layout/asset URLs).\n */\nexport function readDescriptorTemplate(cwd: string, data: object): string | undefined {\n const descriptor = data as BrandingDescriptor;\n if (typeof descriptor.liquid_template === \"string\") {\n return descriptor.liquid_template;\n }\n if (typeof descriptor.liquid_template_file !== \"string\") {\n return undefined;\n }\n const path = resolveTemplatePath(cwd, descriptor.liquid_template_file);\n try {\n return readFileSync(path, \"utf8\");\n } catch (error) {\n throw new ZitadelError(\n \"E_VALIDATION\",\n `liquid_template_file ${JSON.stringify(descriptor.liquid_template_file)} cannot be read`,\n {\n hint: \"Create the template file or fix the reference; the `branding eject` command scaffolds a starting point.\",\n details: { cause: error instanceof Error ? error.message : String(error) },\n },\n );\n }\n}\n\n/**\n * Converts a local descriptor to the wire body of `POST /branding`: strips\n * the editor `$schema` affordance and replaces `liquid_template_file` with\n * the inlined template content. Non-throwing on a missing template file (the\n * field is simply left out) — `validate` reports that case with a hint\n * before any planning happens.\n */\nexport function toBrandingWireBody(cwd: string, data: object): object {\n const { $schema, liquid_template_file, ...rest } = data as BrandingDescriptor & {\n $schema?: string;\n };\n void $schema;\n const out: Record<string, unknown> = { ...rest };\n if (typeof liquid_template_file === \"string\" && out.liquid_template === undefined) {\n try {\n out.liquid_template = readFileSync(resolveTemplatePath(cwd, liquid_template_file), \"utf8\");\n } catch {\n // Reported by validate(); keep normalize/hashing total.\n }\n }\n return out;\n}\n\n/**\n * Converts the server's canonical wire body back to the local descriptor\n * form: when the local descriptor references a template file, the template\n * string moves back out of the JSON into that reference. The caller writes\n * the template content to the referenced file separately.\n */\nexport function toLocalBrandingBody(canonicalWire: object, localData: object): object {\n const local = localData as BrandingDescriptor;\n if (typeof local.liquid_template_file !== \"string\") {\n return canonicalWire;\n }\n const { liquid_template, ...rest } = canonicalWire as BrandingDescriptor;\n void liquid_template;\n return { ...rest, liquid_template_file: local.liquid_template_file };\n}\n"],"mappings":";;;;;;;;;;;AAYA,MAAa,eAAe;;;;;;;;AAe5B,SAAgB,oBAAoB,KAAa,KAAqB;CACpE,MAAM,WAAW,WAAW,IAAI,GAAG,MAAM,QAAQ,KAAK,KAAK,aAAa,EAAE,IAAI;CAC9E,MAAM,MAAM,SAAS,KAAK,SAAS;AACnC,KAAI,IAAI,WAAW,KAAK,IAAI,WAAW,IAAI,CACzC,OAAM,IAAI,aACR,gBACA,wBAAwB,KAAK,UAAU,IAAI,CAAC,8BAC5C,EAAE,MAAM,+CAA+C,aAAa,KAAK,CAC1E;AAEH,QAAO;;;;;;;;AAST,SAAgB,uBAAuB,KAAa,MAAkC;CACpF,MAAM,aAAa;AACnB,KAAI,OAAO,WAAW,oBAAoB,SACxC,QAAO,WAAW;AAEpB,KAAI,OAAO,WAAW,yBAAyB,SAC7C;CAEF,MAAM,OAAO,oBAAoB,KAAK,WAAW,qBAAqB;AACtE,KAAI;AACF,SAAO,aAAa,MAAM,OAAO;UAC1B,OAAO;AACd,QAAM,IAAI,aACR,gBACA,wBAAwB,KAAK,UAAU,WAAW,qBAAqB,CAAC,kBACxE;GACE,MAAM;GACN,SAAS,EAAE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,EAAE;GAC3E,CACF;;;;;;;;;;AAWL,SAAgB,mBAAmB,KAAa,MAAsB;CACpE,MAAM,EAAE,SAAS,sBAAsB,GAAG,SAAS;CAInD,MAAM,MAA+B,EAAE,GAAG,MAAM;AAChD,KAAI,OAAO,yBAAyB,YAAY,IAAI,oBAAoB,KAAA,EACtE,KAAI;AACF,MAAI,kBAAkB,aAAa,oBAAoB,KAAK,qBAAqB,EAAE,OAAO;SACpF;AAIV,QAAO;;;;;;;;AAST,SAAgB,oBAAoB,eAAuB,WAA2B;CACpF,MAAM,QAAQ;AACd,KAAI,OAAO,MAAM,yBAAyB,SACxC,QAAO;CAET,MAAM,EAAE,iBAAiB,GAAG,SAAS;AAErC,QAAO;EAAE,GAAG;EAAM,sBAAsB,MAAM;EAAsB"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { M as serverKind, j as publicCliCommand } from "./oclif-JlsN1lfO.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* The date after which the platform refuses to claim, from the project's
|
|
4
|
+
* creation time. Falls back to `now` when creation time is unknown, which is
|
|
5
|
+
* exact at setup time (the project was just created) and the only caller
|
|
6
|
+
* without a recorded creation time.
|
|
7
|
+
*/
|
|
8
|
+
function claimWindowDeadline(createdAt, now = Date.now()) {
|
|
9
|
+
const base = createdAt === void 0 ? now : Date.parse(createdAt);
|
|
10
|
+
return new Date((Number.isNaN(base) ? now : base) + 336 * 60 * 60 * 1e3);
|
|
11
|
+
}
|
|
12
|
+
function deadlinePhrase(deadline) {
|
|
13
|
+
return `attach it ${deadline === void 0 ? `within 14 days of creation` : `before ${deadline.toLocaleString(void 0, {
|
|
14
|
+
year: "numeric",
|
|
15
|
+
month: "short",
|
|
16
|
+
day: "numeric",
|
|
17
|
+
hour: "2-digit",
|
|
18
|
+
minute: "2-digit",
|
|
19
|
+
timeZoneName: "short"
|
|
20
|
+
})}`}, after that it can no longer be claimed`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether the local record says a team owns this project.
|
|
24
|
+
*
|
|
25
|
+
* Both fields or neither: a half-written record is not an attachment, and
|
|
26
|
+
* treating it as one would hand callers a `team_id` with no idea when it
|
|
27
|
+
* landed. Shared with `zitadel claim`, which asks the same question before it
|
|
28
|
+
* decides to skip, so the two cannot drift as the record grows fields.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately separate from {@link claimState}: the command must answer this
|
|
31
|
+
* on any server, while the nudges only ask it on the cloud.
|
|
32
|
+
*/
|
|
33
|
+
function isAttached(secret) {
|
|
34
|
+
return Boolean(secret.claimed_at && secret.team_id);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Decides the attachment state from local state alone.
|
|
38
|
+
*
|
|
39
|
+
* **Why no network call.** The contract exposes no ambient claim-state read:
|
|
40
|
+
* `getClaimStatus` answers for one live challenge and rejects a secret that did
|
|
41
|
+
* not mint it, and claim attributes were deliberately kept off
|
|
42
|
+
* `GET /projects/{id}` because they belong to the grant, not the project. So
|
|
43
|
+
* `.zitadel/secret` is the only source, which is also why `status` and `doctor`
|
|
44
|
+
* stay fast and work offline. `zitadel claim` reaches the same conclusion the
|
|
45
|
+
* same way before it decides to skip.
|
|
46
|
+
*
|
|
47
|
+
* **Why cloud only.** Claiming attaches a project to a team on the platform
|
|
48
|
+
* that hosts it. The cloud always has one. A local or self-hosted server has
|
|
49
|
+
* one only when it opted into `platform.bootstrap_project`, and that flag
|
|
50
|
+
* also pins the deployment's console and hosted-login default project to
|
|
51
|
+
* proj_platform, so it is deliberately NOT a `zitadel start` default — which
|
|
52
|
+
* means this offline classifier cannot know whether a local claim could
|
|
53
|
+
* complete. `setup` is online anyway and probes the local server's runtime
|
|
54
|
+
* document to decide the nudge there; the offline surfaces (`status`,
|
|
55
|
+
* `doctor`) stay cloud-gated rather than advertise a possibly impossible
|
|
56
|
+
* action.
|
|
57
|
+
*
|
|
58
|
+
* The local record can be stale in one direction: a project claimed from
|
|
59
|
+
* another machine, or a `.zitadel/secret` restored from a backup, reads as
|
|
60
|
+
* `detached` here. That is tolerable because everything downstream is
|
|
61
|
+
* advisory, and `zitadel claim` itself turns the resulting `409` into a clean
|
|
62
|
+
* skip rather than an error.
|
|
63
|
+
*/
|
|
64
|
+
function claimState(input) {
|
|
65
|
+
if (serverKind.value(input.server) !== "cloud") return { kind: "not-applicable" };
|
|
66
|
+
if (isAttached(input.secret)) return {
|
|
67
|
+
kind: "attached",
|
|
68
|
+
team_id: input.secret.team_id,
|
|
69
|
+
claimed_at: input.secret.claimed_at
|
|
70
|
+
};
|
|
71
|
+
const createdAt = Date.parse(input.secret.created_at ?? "");
|
|
72
|
+
if (Number.isNaN(createdAt)) return {
|
|
73
|
+
kind: "detached",
|
|
74
|
+
claimable: true
|
|
75
|
+
};
|
|
76
|
+
const deadline = claimWindowDeadline(input.secret.created_at);
|
|
77
|
+
return {
|
|
78
|
+
kind: "detached",
|
|
79
|
+
claimable: deadline.getTime() > Date.now(),
|
|
80
|
+
deadline: deadline.toISOString()
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The nudge for `next_actions`, quoting the command the way `journey-guidance`
|
|
85
|
+
* does.
|
|
86
|
+
*
|
|
87
|
+
* Names the claim window because the server now enforces it at claim time
|
|
88
|
+
* (`proj.claim_window_expired`); before that enforcement existed this copy
|
|
89
|
+
* deliberately promised nothing. It still says nothing about deletion, because
|
|
90
|
+
* nothing deletes the project when the window closes — it only stops being
|
|
91
|
+
* claimable (ADR 046 §Non-goals).
|
|
92
|
+
*/
|
|
93
|
+
function claimAction(cliVersion, deadline) {
|
|
94
|
+
return `This project is temporary until you attach it to a team: ${deadlinePhrase(deadline)}. Run ${publicCliCommand("claim", cliVersion)} to make it permanent. Nothing about the project changes, so users, passkeys, and the issuer keep working.`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The same nudge for the human box, with the command on its own styled line.
|
|
98
|
+
*/
|
|
99
|
+
function claimBoxAction(cliVersion, deadline) {
|
|
100
|
+
return {
|
|
101
|
+
text: `This project is temporary until you attach it to a team: ${deadlinePhrase(deadline)}. Claiming is independent of the steps above, so you can do it right away; nothing about the project changes, and users, passkeys, and the issuer keep working:`,
|
|
102
|
+
command: publicCliCommand("claim", cliVersion)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** The command for `next_commands`. */
|
|
106
|
+
function claimCommand(cliVersion) {
|
|
107
|
+
return publicCliCommand("claim", cliVersion);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The nudge's counterpart for a closed claim window. Deliberately framed as
|
|
111
|
+
* reconciliation, not a verdict: the local record can be stale in one
|
|
112
|
+
* direction (a project claimed from another machine still reads detached
|
|
113
|
+
* here), so "window closed" from local data alone must not send the user off
|
|
114
|
+
* to duplicate a project that is already attached and working. Running
|
|
115
|
+
* `zitadel claim` is safe either way — the server checks the grant before
|
|
116
|
+
* the window, so an attached project answers with a clean already-claimed
|
|
117
|
+
* skip, and only a genuinely unattached one gets the final refusal.
|
|
118
|
+
*/
|
|
119
|
+
function claimWindowClosedAction(cliVersion) {
|
|
120
|
+
return `The local record says this project's 14-day claim window has closed. Run ${publicCliCommand("claim", cliVersion)} to reconcile: if the project was attached to a team from another machine it confirms that, and if it was never attached it can no longer be claimed. Either way the project keeps working; ${publicCliCommand("setup", cliVersion)} in a fresh directory gets you a new project you can attach.`;
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { claimWindowClosedAction as a, claimState as i, claimBoxAction as n, claimWindowDeadline as o, claimCommand as r, isAttached as s, claimAction as t };
|
|
124
|
+
|
|
125
|
+
//# sourceMappingURL=claim-state-B6ZSKmhW.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claim-state-B6ZSKmhW.mjs","names":[],"sources":["../src/lib/claim-state.ts"],"sourcesContent":["import type { BoxAction } from \"./box\";\nimport { serverKind } from \"./oclif/server-kind\";\nimport type { ZitadelSecret } from \"./project\";\nimport { publicCliCommand } from \"./public-cli\";\n\n/**\n * Mirrors domain.ClaimWindow (internal/domain/claim.go): the server refuses\n * `claim/init` and `claim/complete` with `proj.claim_window_expired` once the\n * project is more than this many days old. Keep the two in sync — drift here\n * shows as a wrong printed deadline, the enforcement stays server-side.\n */\nexport const CLAIM_WINDOW_DAYS = 14;\n\n/**\n * The date after which the platform refuses to claim, from the project's\n * creation time. Falls back to `now` when creation time is unknown, which is\n * exact at setup time (the project was just created) and the only caller\n * without a recorded creation time.\n */\nexport function claimWindowDeadline(createdAt: string | undefined, now = Date.now()): Date {\n const base = createdAt === undefined ? now : Date.parse(createdAt);\n return new Date((Number.isNaN(base) ? now : base) + CLAIM_WINDOW_DAYS * 24 * 60 * 60 * 1000);\n}\n\nfunction deadlinePhrase(deadline?: Date): string {\n // Time and zone included, not just the date: the server closes the window\n // at an exact timestamp (created_at + 14 x 24h), and a bare \"before Sep 18\"\n // misleads in both directions at the boundary day.\n const when =\n deadline === undefined\n ? `within ${CLAIM_WINDOW_DAYS} days of creation`\n : `before ${deadline.toLocaleString(undefined, {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n timeZoneName: \"short\",\n })}`;\n return `attach it ${when}, after that it can no longer be claimed`;\n}\n\n/**\n * Whether this project is attached to a team, as the CLI can tell locally.\n *\n * `not-applicable` is its own variant rather than a boolean flag on the caller\n * because \"there is nothing to attach here\" and \"nothing is attached yet\" want\n * opposite output: the first prints nothing at all, the second nudges.\n */\nexport type ClaimState =\n | { kind: \"attached\"; team_id: string; claimed_at: string }\n /**\n * `claimable` classifies the claim window from the locally recorded\n * creation time: once it has passed, nudges switch to reconciliation\n * wording (claimWindowClosedAction). Never proof of anything: the local\n * record can be stale (a project claimed from another machine still reads\n * detached), so the claim command stays advertised as the safe check —\n * the server resolves the grant before the window. Lenient on an unknown\n * or unparsable creation time (claimable, no deadline) because the server\n * is the enforcer and the CLI only advises. `deadline` is the ISO date\n * the window closes, when the creation time is known.\n */\n | { kind: \"detached\"; claimable: boolean; deadline?: string }\n | { kind: \"not-applicable\" };\n\n/**\n * Whether the local record says a team owns this project.\n *\n * Both fields or neither: a half-written record is not an attachment, and\n * treating it as one would hand callers a `team_id` with no idea when it\n * landed. Shared with `zitadel claim`, which asks the same question before it\n * decides to skip, so the two cannot drift as the record grows fields.\n *\n * Deliberately separate from {@link claimState}: the command must answer this\n * on any server, while the nudges only ask it on the cloud.\n */\nexport function isAttached(\n secret: Pick<ZitadelSecret, \"claimed_at\" | \"team_id\">,\n): secret is { claimed_at: string; team_id: string } {\n return Boolean(secret.claimed_at && secret.team_id);\n}\n\n/**\n * Decides the attachment state from local state alone.\n *\n * **Why no network call.** The contract exposes no ambient claim-state read:\n * `getClaimStatus` answers for one live challenge and rejects a secret that did\n * not mint it, and claim attributes were deliberately kept off\n * `GET /projects/{id}` because they belong to the grant, not the project. So\n * `.zitadel/secret` is the only source, which is also why `status` and `doctor`\n * stay fast and work offline. `zitadel claim` reaches the same conclusion the\n * same way before it decides to skip.\n *\n * **Why cloud only.** Claiming attaches a project to a team on the platform\n * that hosts it. The cloud always has one. A local or self-hosted server has\n * one only when it opted into `platform.bootstrap_project`, and that flag\n * also pins the deployment's console and hosted-login default project to\n * proj_platform, so it is deliberately NOT a `zitadel start` default — which\n * means this offline classifier cannot know whether a local claim could\n * complete. `setup` is online anyway and probes the local server's runtime\n * document to decide the nudge there; the offline surfaces (`status`,\n * `doctor`) stay cloud-gated rather than advertise a possibly impossible\n * action.\n *\n * The local record can be stale in one direction: a project claimed from\n * another machine, or a `.zitadel/secret` restored from a backup, reads as\n * `detached` here. That is tolerable because everything downstream is\n * advisory, and `zitadel claim` itself turns the resulting `409` into a clean\n * skip rather than an error.\n */\nexport function claimState(input: {\n secret: Pick<ZitadelSecret, \"claimed_at\" | \"team_id\"> & { created_at?: string };\n server: string;\n}): ClaimState {\n if (serverKind.value(input.server) !== \"cloud\") {\n return { kind: \"not-applicable\" };\n }\n if (isAttached(input.secret)) {\n return {\n kind: \"attached\",\n team_id: input.secret.team_id,\n claimed_at: input.secret.claimed_at,\n };\n }\n const createdAt = Date.parse(input.secret.created_at ?? \"\");\n if (Number.isNaN(createdAt)) {\n return { kind: \"detached\", claimable: true };\n }\n const deadline = claimWindowDeadline(input.secret.created_at);\n return {\n kind: \"detached\",\n claimable: deadline.getTime() > Date.now(),\n deadline: deadline.toISOString(),\n };\n}\n\n/**\n * The nudge for `next_actions`, quoting the command the way `journey-guidance`\n * does.\n *\n * Names the claim window because the server now enforces it at claim time\n * (`proj.claim_window_expired`); before that enforcement existed this copy\n * deliberately promised nothing. It still says nothing about deletion, because\n * nothing deletes the project when the window closes — it only stops being\n * claimable (ADR 046 §Non-goals).\n */\nexport function claimAction(cliVersion: string, deadline?: Date): string {\n return (\n `This project is temporary until you attach it to a team: ${deadlinePhrase(deadline)}. ` +\n `Run ${publicCliCommand(\"claim\", cliVersion)} to make it permanent. ` +\n \"Nothing about the project changes, so users, passkeys, and the issuer keep working.\"\n );\n}\n\n/**\n * The same nudge for the human box, with the command on its own styled line.\n */\nexport function claimBoxAction(cliVersion: string, deadline?: Date): BoxAction {\n return {\n text:\n `This project is temporary until you attach it to a team: ${deadlinePhrase(deadline)}. ` +\n \"Claiming is independent of the steps above, so you can do it right away; \" +\n \"nothing about the project changes, and users, passkeys, and the issuer keep working:\",\n command: publicCliCommand(\"claim\", cliVersion),\n };\n}\n\n/** The command for `next_commands`. */\nexport function claimCommand(cliVersion: string): string {\n return publicCliCommand(\"claim\", cliVersion);\n}\n\n/**\n * The nudge's counterpart for a closed claim window. Deliberately framed as\n * reconciliation, not a verdict: the local record can be stale in one\n * direction (a project claimed from another machine still reads detached\n * here), so \"window closed\" from local data alone must not send the user off\n * to duplicate a project that is already attached and working. Running\n * `zitadel claim` is safe either way — the server checks the grant before\n * the window, so an attached project answers with a clean already-claimed\n * skip, and only a genuinely unattached one gets the final refusal.\n */\nexport function claimWindowClosedAction(cliVersion: string): string {\n return (\n `The local record says this project's ${CLAIM_WINDOW_DAYS}-day claim window has ` +\n `closed. Run ${publicCliCommand(\"claim\", cliVersion)} to reconcile: if the project ` +\n \"was attached to a team from another machine it confirms that, and if it was never \" +\n \"attached it can no longer be claimed. Either way the project keeps working; \" +\n `${publicCliCommand(\"setup\", cliVersion)} in a fresh directory gets you a new ` +\n \"project you can attach.\"\n );\n}\n"],"mappings":";;;;;;;AAmBA,SAAgB,oBAAoB,WAA+B,MAAM,KAAK,KAAK,EAAQ;CACzF,MAAM,OAAO,cAAc,KAAA,IAAY,MAAM,KAAK,MAAM,UAAU;AAClE,QAAO,IAAI,MAAM,OAAO,MAAM,KAAK,GAAG,MAAM,QAAA,MAAiC,KAAK,KAAK,IAAK;;AAG9F,SAAS,eAAe,UAAyB;AAe/C,QAAO,aAVL,aAAa,KAAA,IACT,+BACA,UAAU,SAAS,eAAe,KAAA,GAAW;EAC3C,MAAM;EACN,OAAO;EACP,KAAK;EACL,MAAM;EACN,QAAQ;EACR,cAAc;EACf,CAAC,GACiB;;;;;;;;;;;;;AAqC3B,SAAgB,WACd,QACmD;AACnD,QAAO,QAAQ,OAAO,cAAc,OAAO,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BrD,SAAgB,WAAW,OAGZ;AACb,KAAI,WAAW,MAAM,MAAM,OAAO,KAAK,QACrC,QAAO,EAAE,MAAM,kBAAkB;AAEnC,KAAI,WAAW,MAAM,OAAO,CAC1B,QAAO;EACL,MAAM;EACN,SAAS,MAAM,OAAO;EACtB,YAAY,MAAM,OAAO;EAC1B;CAEH,MAAM,YAAY,KAAK,MAAM,MAAM,OAAO,cAAc,GAAG;AAC3D,KAAI,OAAO,MAAM,UAAU,CACzB,QAAO;EAAE,MAAM;EAAY,WAAW;EAAM;CAE9C,MAAM,WAAW,oBAAoB,MAAM,OAAO,WAAW;AAC7D,QAAO;EACL,MAAM;EACN,WAAW,SAAS,SAAS,GAAG,KAAK,KAAK;EAC1C,UAAU,SAAS,aAAa;EACjC;;;;;;;;;;;;AAaH,SAAgB,YAAY,YAAoB,UAAyB;AACvE,QACE,4DAA4D,eAAe,SAAS,CAAC,QAC9E,iBAAiB,SAAS,WAAW,CAAC;;;;;AAQjD,SAAgB,eAAe,YAAoB,UAA4B;AAC7E,QAAO;EACL,MACE,4DAA4D,eAAe,SAAS,CAAC;EAGvF,SAAS,iBAAiB,SAAS,WAAW;EAC/C;;;AAIH,SAAgB,aAAa,YAA4B;AACvD,QAAO,iBAAiB,SAAS,WAAW;;;;;;;;;;;;AAa9C,SAAgB,wBAAwB,YAA4B;AAClE,QACE,4EACe,iBAAiB,SAAS,WAAW,CAAC,8LAGlD,iBAAiB,SAAS,WAAW,CAAC"}
|
package/dist/commands/apply.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { j as publicCliCommand, t as BaseCommand } from "../oclif-
|
|
2
|
-
import { l as readZitadelSecret } from "../project-
|
|
3
|
-
import { a as buildSyncPlan, i as summarizePlan, l as makeSyncers, n as enumeratePlanResources, r as renderPlan, s as runSyncLoop, t as collectPlanWarnings } from "../sync-
|
|
4
|
-
import { t as environmentSchema } from "../environment-
|
|
1
|
+
import { j as publicCliCommand, t as BaseCommand } from "../oclif-JlsN1lfO.mjs";
|
|
2
|
+
import { l as readZitadelSecret } from "../project-Ba0nriNm.mjs";
|
|
3
|
+
import { a as buildSyncPlan, i as summarizePlan, l as makeSyncers, n as enumeratePlanResources, r as renderPlan, s as runSyncLoop, t as collectPlanWarnings } from "../sync-ChlLQ4vZ.mjs";
|
|
4
|
+
import { t as environmentSchema } from "../environment-fibXmzM8.mjs";
|
|
5
5
|
import { Flags } from "@oclif/core";
|
|
6
6
|
import { createZitadelClient } from "@zitadel/api/client";
|
|
7
7
|
import { consola as consola$1 } from "consola";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
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-
|
|
3
|
-
import { t as hasZitadelConfig } from "../../project-
|
|
4
|
-
import { t as BRANDING_DIR } from "../../branding-
|
|
2
|
+
import { E as ZitadelError, O as normalizePublicCliJson, T as stableStringify, j as publicCliCommand, k as normalizePublicCliProse, t as BaseCommand } from "../../oclif-JlsN1lfO.mjs";
|
|
3
|
+
import { t as hasZitadelConfig } from "../../project-Ba0nriNm.mjs";
|
|
4
|
+
import { t as BRANDING_DIR } from "../../branding-DvoH8e0D.mjs";
|
|
5
5
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { cancel, isCancel, select } from "@clack/prompts";
|
package/dist/commands/claim.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { n as wrapForBox } from "../box-B_mwKole.mjs";
|
|
2
|
+
import { E as ZitadelError, S as isObject, t as BaseCommand } from "../oclif-JlsN1lfO.mjs";
|
|
3
|
+
import { s as isAttached } from "../claim-state-B6ZSKmhW.mjs";
|
|
4
|
+
import { l as readZitadelSecret, u as writeZitadelSecret } from "../project-Ba0nriNm.mjs";
|
|
4
5
|
import { access, readFile } from "node:fs/promises";
|
|
5
6
|
import { delimiter, join } from "node:path";
|
|
6
7
|
import { Flags } from "@oclif/core";
|
|
@@ -271,6 +272,10 @@ var Claim = class Claim extends BaseCommand {
|
|
|
271
272
|
project_id: secret.project_id,
|
|
272
273
|
...claimed
|
|
273
274
|
});
|
|
275
|
+
if (isClaimWindowExpired(error)) {
|
|
276
|
+
this.recordTelemetry({ claim_outcome: "window_expired" });
|
|
277
|
+
throw claimWindowExpiredError();
|
|
278
|
+
}
|
|
274
279
|
throw error;
|
|
275
280
|
}
|
|
276
281
|
const deadline = claimDeadline({
|
|
@@ -281,13 +286,14 @@ var Claim = class Claim extends BaseCommand {
|
|
|
281
286
|
if (isLoopbackUrl(this.meta.source) && !isLoopbackUrl(challenge.claim_url)) consola.warn(`The server at ${this.meta.source} advertised a claim page on ${new URL(challenge.claim_url).origin}. If you started that server yourself, set NEXTGEN_SERVER_PUBLIC_BASE to its reachable origin (e.g. http://localhost:8080).`);
|
|
282
287
|
consola.box({
|
|
283
288
|
title: "Finish in your browser",
|
|
284
|
-
message:
|
|
289
|
+
message: wrapForBox("Sign in at the link below to attach this project to your team."),
|
|
285
290
|
style: {
|
|
286
291
|
padding: 1,
|
|
287
292
|
borderStyle: "rounded",
|
|
288
293
|
borderColor: "cyan"
|
|
289
294
|
}
|
|
290
295
|
});
|
|
296
|
+
consola.log(challenge.claim_url);
|
|
291
297
|
const opened = flags["no-open"] || nonInteractive ? false : (await openInBrowser(challenge.claim_url)).opened;
|
|
292
298
|
if (!opened) consola.info("Open the link above to continue.");
|
|
293
299
|
consola.start("Waiting for the browser step to finish");
|
|
@@ -334,6 +340,13 @@ var Claim = class Claim extends BaseCommand {
|
|
|
334
340
|
return status;
|
|
335
341
|
}
|
|
336
342
|
} catch (error) {
|
|
343
|
+
if (isClaimWindowExpired(error)) {
|
|
344
|
+
this.recordTelemetry({
|
|
345
|
+
claim_outcome: "window_expired",
|
|
346
|
+
poll_count: polls
|
|
347
|
+
});
|
|
348
|
+
throw claimWindowExpiredError();
|
|
349
|
+
}
|
|
337
350
|
if (error instanceof ApiError && error.status === 410) {
|
|
338
351
|
this.recordTelemetry({
|
|
339
352
|
claim_outcome: "expired",
|
|
@@ -391,6 +404,15 @@ function alreadyClaimedDetails(error) {
|
|
|
391
404
|
dashboard_url: typeof details.dashboard_url === "string" ? details.dashboard_url : void 0
|
|
392
405
|
};
|
|
393
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* A `410 proj.claim_window_expired` from `claim/init`: the project outlived
|
|
409
|
+
* domain.ClaimWindow before anyone claimed it. Distinct from the plain 410 the
|
|
410
|
+
* poll maps to "the link expired", which a new link fixes; this one nothing
|
|
411
|
+
* fixes.
|
|
412
|
+
*/
|
|
413
|
+
function isClaimWindowExpired(error) {
|
|
414
|
+
return error instanceof ApiError && error.status === 410 && isObject(error.body) && error.body.code === "proj.claim_window_expired";
|
|
415
|
+
}
|
|
394
416
|
function expiredError(message) {
|
|
395
417
|
return new ZitadelError("E_VALIDATION", message, {
|
|
396
418
|
hint: "Links are valid for 10 minutes. Start a new one.",
|
|
@@ -398,6 +420,14 @@ function expiredError(message) {
|
|
|
398
420
|
});
|
|
399
421
|
}
|
|
400
422
|
/**
|
|
423
|
+
* Deliberately not the retryable "start a new link" shape: a new challenge
|
|
424
|
+
* cannot fix a closed window, so suggesting `claim` again would send the
|
|
425
|
+
* user in a circle.
|
|
426
|
+
*/
|
|
427
|
+
function claimWindowExpiredError() {
|
|
428
|
+
return new ZitadelError("E_VALIDATION", `This project was not attached to a team within 14 days of creation, so it can no longer be claimed.`, { hint: "The project itself keeps working. To get a claimable project, run `zitadel setup` in a fresh directory (here it would just skip as already initialized) and claim the new one within the window." });
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
401
431
|
* Loopback check on the URL's hostname: `localhost`, the whole `127.0.0.0/8`
|
|
402
432
|
* block, or `[::1]` (how WHATWG URLs spell IPv6 loopback).
|
|
403
433
|
*/
|
|
@@ -1 +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 // A local server left on the default public base advertises a claim page\n // on a remote origin — the exact confusion this warning names. Cloud\n // servers legitimately use a console origin different from the API one,\n // so only a loopback API paired with a non-loopback claim page warns.\n if (isLoopbackUrl(this.meta.source) && !isLoopbackUrl(challenge.claim_url)) {\n consola.warn(\n `The server at ${this.meta.source} advertised a claim page on ${new URL(challenge.claim_url).origin}. ` +\n \"If you started that server yourself, set NEXTGEN_SERVER_PUBLIC_BASE to its reachable origin (e.g. http://localhost:8080).\",\n );\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\n/**\n * Loopback check on the URL's hostname: `localhost`, the whole `127.0.0.0/8`\n * block, or `[::1]` (how WHATWG URLs spell IPv6 loopback).\n */\nfunction isLoopbackUrl(value: string): boolean {\n try {\n const hostname = new URL(value).hostname;\n return hostname === \"localhost\" || hostname === \"[::1]\" || /^127(\\.\\d{1,3}){3}$/.test(hostname);\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
|
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 { wrapForBox } from \"../lib/box\";\nimport { openInBrowser } from \"../lib/browser\";\nimport { CLAIM_WINDOW_DAYS, 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 if (isClaimWindowExpired(error)) {\n this.recordTelemetry({ claim_outcome: \"window_expired\" });\n throw claimWindowExpiredError();\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 // A local server left on the default public base advertises a claim page\n // on a remote origin — the exact confusion this warning names. Cloud\n // servers legitimately use a console origin different from the API one,\n // so only a loopback API paired with a non-loopback claim page warns.\n if (isLoopbackUrl(this.meta.source) && !isLoopbackUrl(challenge.claim_url)) {\n consola.warn(\n `The server at ${this.meta.source} advertised a claim page on ${new URL(challenge.claim_url).origin}. ` +\n \"If you started that server yourself, set NEXTGEN_SERVER_PUBLIC_BASE to its reachable origin (e.g. http://localhost:8080).\",\n );\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 //\n // The URL prints as a bare line under the frame rather than inside it:\n // it must never be split (clickability, and the journey e2e scrape reads\n // it out of the narration), and consola pads every box line to the\n // longest one, so a ~110-character URL inside the box would re-break the\n // frame on any terminal narrower than the URL itself. `wrapForBox` keeps\n // the frame's own content inside the terminal width.\n consola.box({\n title: \"Finish in your browser\",\n message: wrapForBox(\"Sign in at the link below to attach this project to your team.\"),\n style: { padding: 1, borderStyle: \"rounded\", borderColor: \"cyan\" },\n });\n consola.log(challenge.claim_url);\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 // The window can close between init and poll (a challenge minted in\n // the window's last minutes); the status route reports that as its\n // own 410 code, and it must not read as the retryable link expiry.\n if (isClaimWindowExpired(error)) {\n this.recordTelemetry({ claim_outcome: \"window_expired\", poll_count: polls });\n throw claimWindowExpiredError();\n }\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\n/**\n * A `410 proj.claim_window_expired` from `claim/init`: the project outlived\n * domain.ClaimWindow before anyone claimed it. Distinct from the plain 410 the\n * poll maps to \"the link expired\", which a new link fixes; this one nothing\n * fixes.\n */\nfunction isClaimWindowExpired(error: unknown): boolean {\n return (\n error instanceof ApiError &&\n error.status === 410 &&\n isObject(error.body) &&\n error.body.code === \"proj.claim_window_expired\"\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\n/**\n * Deliberately not the retryable \"start a new link\" shape: a new challenge\n * cannot fix a closed window, so suggesting `claim` again would send the\n * user in a circle.\n */\nfunction claimWindowExpiredError(): ZitadelError {\n return new ZitadelError(\n \"E_VALIDATION\",\n `This project was not attached to a team within ${CLAIM_WINDOW_DAYS} days of creation, so it can no longer be claimed.`,\n {\n hint: \"The project itself keeps working. To get a claimable project, run `zitadel setup` in a fresh directory (here it would just skip as already initialized) and claim the new one within the window.\",\n },\n );\n}\n\n/**\n * Loopback check on the URL's hostname: `localhost`, the whole `127.0.0.0/8`\n * block, or `[::1]` (how WHATWG URLs spell IPv6 loopback).\n */\nfunction isLoopbackUrl(value: string): boolean {\n try {\n const hostname = new URL(value).hostname;\n return hostname === \"localhost\" || hostname === \"[::1]\" || /^127(\\.\\d{1,3}){3}$/.test(hostname);\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
package/dist/commands/doctor.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { E as ZitadelError, S as isObject, a as DEFAULT_LOCAL_SERVER_PORT, c as checkLocalServerHealth, h as readRuntimeMetadata, j as publicCliCommand, l as defaultLocalServerImageForCliVersion, m as localServerUrl, s as assertLocalStateWritable, t as BaseCommand } from "../oclif-
|
|
2
|
-
import {
|
|
3
|
-
import { s as issuerFromPort, t as createOrca } from "../orca-
|
|
4
|
-
import { a as readProjectServer, c as readZitadelConfig, i as readPreset, l as readZitadelSecret, o as readRendererId, r as readDevelopmentIssuer, s as readUseCase, t as hasZitadelConfig } from "../project-
|
|
5
|
-
import { i as updateScaffold, t as SCHEMAS_DIR } from "../user-schema-
|
|
6
|
-
import { c as writeScaffoldManifest, n as detectPackageManager, o as hashScaffoldFile, s as readScaffoldManifest, t as addExactCommandFor } from "../package-manager-
|
|
7
|
-
import { d as assertServerPackageAvailable, o as imageAvailable, r as dockerAvailable } from "../docker-
|
|
8
|
-
import { n as dockerRuntimeGuidance, r as dockerUnavailableMessage, t as listenersForPort } from "../ports-
|
|
9
|
-
import { t as discoverManagedRuntimeProcesses } from "../processes-
|
|
1
|
+
import { E as ZitadelError, S as isObject, a as DEFAULT_LOCAL_SERVER_PORT, c as checkLocalServerHealth, h as readRuntimeMetadata, j as publicCliCommand, l as defaultLocalServerImageForCliVersion, m as localServerUrl, s as assertLocalStateWritable, t as BaseCommand } from "../oclif-JlsN1lfO.mjs";
|
|
2
|
+
import { a as claimWindowClosedAction, i as claimState, r as claimCommand, t as claimAction } from "../claim-state-B6ZSKmhW.mjs";
|
|
3
|
+
import { s as issuerFromPort, t as createOrca } from "../orca-CPMHzmg3.mjs";
|
|
4
|
+
import { a as readProjectServer, c as readZitadelConfig, i as readPreset, l as readZitadelSecret, o as readRendererId, r as readDevelopmentIssuer, s as readUseCase, t as hasZitadelConfig } from "../project-Ba0nriNm.mjs";
|
|
5
|
+
import { i as updateScaffold, t as SCHEMAS_DIR } from "../user-schema-C2mC-JTN.mjs";
|
|
6
|
+
import { c as writeScaffoldManifest, n as detectPackageManager, o as hashScaffoldFile, s as readScaffoldManifest, t as addExactCommandFor } from "../package-manager-CDUsoGBy.mjs";
|
|
7
|
+
import { d as assertServerPackageAvailable, o as imageAvailable, r as dockerAvailable } from "../docker-B1MjOMFo.mjs";
|
|
8
|
+
import { n as dockerRuntimeGuidance, r as dockerUnavailableMessage, t as listenersForPort } from "../ports-hidBnSW-.mjs";
|
|
9
|
+
import { t as discoverManagedRuntimeProcesses } from "../processes-Cv19mJ2I.mjs";
|
|
10
10
|
import { chmod, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { Flags } from "@oclif/core";
|
|
@@ -721,12 +721,16 @@ var ClaimCheck = class {
|
|
|
721
721
|
path: this.path
|
|
722
722
|
};
|
|
723
723
|
}
|
|
724
|
-
if (state.kind === "detached")
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
724
|
+
if (state.kind === "detached") {
|
|
725
|
+
const message = state.claimable ? `This project is temporary until you attach it to a team, and claiming is only possible within 14 days of creation. Run \`zitadel claim\` to make it permanent; nothing about the project changes.` : `The local record says this project's 14-day claim window has closed. Run \`zitadel claim\` to reconcile: a project attached from another machine reports its team, one never attached can no longer be claimed. Either way it keeps working; \`zitadel setup\` in a fresh directory gets you a new project you can attach.`;
|
|
726
|
+
return {
|
|
727
|
+
name: this.name,
|
|
728
|
+
status: "warn",
|
|
729
|
+
message,
|
|
730
|
+
details: { claimable: state.claimable },
|
|
731
|
+
path: this.path
|
|
732
|
+
};
|
|
733
|
+
}
|
|
730
734
|
return {
|
|
731
735
|
name: this.name,
|
|
732
736
|
status: "pass",
|
|
@@ -932,8 +936,9 @@ function advisoryForWarnings(warnings, cliVersion) {
|
|
|
932
936
|
nextActions.push(...advice.nextActions);
|
|
933
937
|
nextCommands.push(...advice.nextCommands);
|
|
934
938
|
}
|
|
935
|
-
|
|
936
|
-
|
|
939
|
+
const claimWarning = warnings.find((check) => check.name === "claim");
|
|
940
|
+
if (claimWarning) {
|
|
941
|
+
nextActions.push(claimWindowClosed(claimWarning) ? claimWindowClosedAction(cliVersion) : claimAction(cliVersion));
|
|
937
942
|
nextCommands.push(claimCommand(cliVersion));
|
|
938
943
|
}
|
|
939
944
|
if (hasManagedRuntimeProcesses(warnings.find((check) => check.name === "managed-runtime-processes"))) {
|
|
@@ -1073,6 +1078,17 @@ function remedyCommandOf(check) {
|
|
|
1073
1078
|
const remedy = details.remedy_command;
|
|
1074
1079
|
return typeof remedy === "string" && remedy.length > 0 ? remedy : void 0;
|
|
1075
1080
|
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Reads the claim check's window classification out of its details. Absent
|
|
1083
|
+
* or malformed details mean "not closed". The classification only selects
|
|
1084
|
+
* the advisory wording (deadline nudge vs reconciliation); the claim
|
|
1085
|
+
* command stays suggested either way, because the local record may be stale
|
|
1086
|
+
* and running claim lets the server answer authoritatively.
|
|
1087
|
+
*/
|
|
1088
|
+
function claimWindowClosed(check) {
|
|
1089
|
+
const details = check.details;
|
|
1090
|
+
return typeof details === "object" && details !== null && details.claimable === false;
|
|
1091
|
+
}
|
|
1076
1092
|
function hasManagedRuntimeProcesses(check) {
|
|
1077
1093
|
if (!check || check.status !== "warn") return false;
|
|
1078
1094
|
const details = check.details;
|