@zitadel/cli 0.1.0-alpha.18 → 0.1.0-alpha.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -33
- package/SKILLS.md +74 -8
- package/dist/{branding-BXj028-W.mjs → branding-cygSBPkV.mjs} +2 -2
- package/dist/{branding-BXj028-W.mjs.map → branding-cygSBPkV.mjs.map} +1 -1
- package/dist/{claim-state-BTGInAaN.mjs → claim-state-DlfD6Res.mjs} +2 -2
- package/dist/{claim-state-BTGInAaN.mjs.map → claim-state-DlfD6Res.mjs.map} +1 -1
- package/dist/commands/apply.mjs +4 -4
- package/dist/commands/branding/eject.mjs +11 -32
- package/dist/commands/branding/eject.mjs.map +1 -1
- package/dist/commands/claim.mjs +3 -3
- package/dist/commands/doctor.mjs +41 -18
- package/dist/commands/doctor.mjs.map +1 -1
- package/dist/commands/eject.mjs +4 -4
- package/dist/commands/eject.mjs.map +1 -1
- 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 +6 -6
- package/dist/commands/schemas/list.mjs.map +1 -1
- package/dist/commands/setup.mjs +101 -143
- package/dist/commands/setup.mjs.map +1 -1
- package/dist/commands/start.mjs +3 -3
- package/dist/commands/status.mjs +5 -5
- package/dist/commands/stop.mjs +3 -3
- package/dist/designs-Ckz18Dpo.mjs +38 -0
- package/dist/designs-Ckz18Dpo.mjs.map +1 -0
- package/dist/{docker-Ciz9N6uI.mjs → docker-DcRGTOJa.mjs} +4 -3
- package/dist/docker-DcRGTOJa.mjs.map +1 -0
- package/dist/{environment-CuHKxtIO.mjs → environment-rjRVkJjW.mjs} +1 -1
- package/dist/{environment-CuHKxtIO.mjs.map → environment-rjRVkJjW.mjs.map} +1 -1
- package/dist/{journey-guidance-CtQUfWar.mjs → journey-guidance-BGrOX_gT.mjs} +2 -2
- package/dist/{journey-guidance-CtQUfWar.mjs.map → journey-guidance-BGrOX_gT.mjs.map} +1 -1
- package/dist/{oclif-CL6-M13m.mjs → oclif-CanO3zdt.mjs} +48 -2
- package/dist/oclif-CanO3zdt.mjs.map +1 -0
- package/dist/{orca-BWB4zOdw.mjs → orca-Cu0iW1mv.mjs} +167 -26
- package/dist/orca-Cu0iW1mv.mjs.map +1 -0
- package/dist/package-manager-DFdLcDx1.mjs +194 -0
- package/dist/package-manager-DFdLcDx1.mjs.map +1 -0
- package/dist/{ports-Cu2Y7aXQ.mjs → ports-BM20XIZb.mjs} +2 -2
- package/dist/{ports-Cu2Y7aXQ.mjs.map → ports-BM20XIZb.mjs.map} +1 -1
- package/dist/{processes-Bn2WngOi.mjs → processes-DKHPkU8O.mjs} +1 -1
- package/dist/{processes-Bn2WngOi.mjs.map → processes-DKHPkU8O.mjs.map} +1 -1
- package/dist/{project-yejJSfdJ.mjs → project-CHhot85s.mjs} +2 -2
- package/dist/{project-yejJSfdJ.mjs.map → project-CHhot85s.mjs.map} +1 -1
- package/dist/{sync-Cx2_4rXW.mjs → sync-Bd6vASgg.mjs} +456 -31
- package/dist/sync-Bd6vASgg.mjs.map +1 -0
- package/dist/{user-schema-W1OsXdpc.mjs → user-schema-DTuOsdKE.mjs} +1 -1
- package/dist/{user-schema-W1OsXdpc.mjs.map → user-schema-DTuOsdKE.mjs.map} +1 -1
- package/oclif.manifest.json +2 -2
- package/package.json +6 -5
- package/dist/docker-Ciz9N6uI.mjs.map +0 -1
- package/dist/oclif-CL6-M13m.mjs.map +0 -1
- package/dist/orca-BWB4zOdw.mjs.map +0 -1
- package/dist/scaffold-manifest-CKNFLX9T.mjs +0 -81
- package/dist/scaffold-manifest-CKNFLX9T.mjs.map +0 -1
- package/dist/sync-Cx2_4rXW.mjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { E as ZitadelError, S as isObject, T as stableStringify } from "./oclif-
|
|
2
|
-
import { a as updateState, n as readState, r as removeFromState, t as SCHEMAS_DIR } from "./user-schema-
|
|
3
|
-
import { a as toLocalBrandingBody, i as toBrandingWireBody, n as readDescriptorTemplate, r as resolveTemplatePath, t as BRANDING_DIR } from "./branding-
|
|
1
|
+
import { E as ZitadelError, S as isObject, T as stableStringify } from "./oclif-CanO3zdt.mjs";
|
|
2
|
+
import { a as updateState, n as readState, r as removeFromState, t as SCHEMAS_DIR } from "./user-schema-DTuOsdKE.mjs";
|
|
3
|
+
import { a as toLocalBrandingBody, i as toBrandingWireBody, n as readDescriptorTemplate, r as resolveTemplatePath, t as BRANDING_DIR } from "./branding-cygSBPkV.mjs";
|
|
4
4
|
import { readFile, readdir, writeFile } from "node:fs/promises";
|
|
5
5
|
import { basename, join } from "node:path";
|
|
6
6
|
import { DEFAULT_FLOW_SCHEMA_URI } from "@zitadel/config/defaults";
|
|
@@ -10,6 +10,9 @@ import { writeFileSync } from "node:fs";
|
|
|
10
10
|
import { normalizeFlowBody, normalizeSchemaBody } from "@zitadel/config/normalize";
|
|
11
11
|
import { brandingConfigSchema, flowConfigSchema, schemaConfigSchema } from "@zitadel/config/schemas";
|
|
12
12
|
import { validateLoginTemplate } from "@zitadel/config/template";
|
|
13
|
+
import { lookup } from "node:dns/promises";
|
|
14
|
+
import { BlockList, isIP } from "node:net";
|
|
15
|
+
import { Agent } from "undici";
|
|
13
16
|
import { validateFlowDefinition } from "@zitadel/config/validate";
|
|
14
17
|
//#region src/lib/flows/env-refs.ts
|
|
15
18
|
/**
|
|
@@ -124,7 +127,7 @@ var SchemaSyncer = class {
|
|
|
124
127
|
throw new ZitadelError("E_NOT_IMPLEMENTED", `schema delete is not supported (${id})`);
|
|
125
128
|
}
|
|
126
129
|
async fetch(id) {
|
|
127
|
-
return await this.client.getSchemaById(encodeURIComponent(id)
|
|
130
|
+
return await this.client.getSchemaById(encodeURIComponent(id));
|
|
128
131
|
}
|
|
129
132
|
};
|
|
130
133
|
var FlowDefinitionSyncer = class {
|
|
@@ -169,23 +172,24 @@ var FlowDefinitionSyncer = class {
|
|
|
169
172
|
/**
|
|
170
173
|
* PUT completely replaces the flow definition. The wire request wraps the
|
|
171
174
|
* bare on-disk flow in the `{ flow_definition }` update envelope
|
|
172
|
-
* (`api/openapi/components/flows/flow-definition-update-request.yaml`)
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
+
* (`api/openapi/components/flows/flow-definition-update-request.yaml`); the
|
|
176
|
+
* file on disk stays bare so it is human-editable. Flat-by-id: no
|
|
177
|
+
* `project_id` query — authz resolves the project from RSI.
|
|
175
178
|
*/
|
|
176
179
|
async update(id, data) {
|
|
177
|
-
return { canonical: (await this.client.updateFlowDefinition(id, { flow_definition: data }
|
|
180
|
+
return { canonical: (await this.client.updateFlowDefinition(id, { flow_definition: data })).flow_definition };
|
|
178
181
|
}
|
|
179
182
|
async delete(id) {
|
|
180
|
-
await this.client.deleteFlowDefinition(id
|
|
183
|
+
await this.client.deleteFlowDefinition(id);
|
|
181
184
|
}
|
|
182
185
|
/**
|
|
183
186
|
* `GET /flow_definitions/:id` returns a detail envelope with metadata
|
|
184
187
|
* (`id`, `project_id`, `created_at`, `updated_at`) plus `flow_definition`.
|
|
185
188
|
* Return only `flow_definition` so diffs compare with the on-disk bare body.
|
|
189
|
+
* Flat-by-id: no `project_id` query — authz resolves the project from RSI.
|
|
186
190
|
*/
|
|
187
191
|
async fetch(id) {
|
|
188
|
-
return (await this.client.getFlowDefinition(id
|
|
192
|
+
return (await this.client.getFlowDefinition(id)).flow_definition;
|
|
189
193
|
}
|
|
190
194
|
};
|
|
191
195
|
/**
|
|
@@ -252,9 +256,9 @@ var BrandingSyncer = class {
|
|
|
252
256
|
async delete(id) {
|
|
253
257
|
throw new ZitadelError("E_NOT_IMPLEMENTED", `branding delete is not supported (${id})`);
|
|
254
258
|
}
|
|
255
|
-
/** Wire form (template inlined); diffs compare in the normalized form. */
|
|
259
|
+
/** Wire form (template inlined); diffs compare in the normalized form. Flat-by-id: no project_id query. */
|
|
256
260
|
async fetch(id) {
|
|
257
|
-
return (await this.client.getBrandingById(id
|
|
261
|
+
return (await this.client.getBrandingById(id)).branding;
|
|
258
262
|
}
|
|
259
263
|
canonicalToLocal(canonicalWire, localData) {
|
|
260
264
|
const local = localData;
|
|
@@ -270,6 +274,291 @@ var BrandingSyncer = class {
|
|
|
270
274
|
}
|
|
271
275
|
};
|
|
272
276
|
//#endregion
|
|
277
|
+
//#region src/lib/sync/asset-probe.ts
|
|
278
|
+
/**
|
|
279
|
+
* Branding descriptor fields whose value is a URL the login page fetches as
|
|
280
|
+
* an image. Both are optional and both fail the same invisible way when the
|
|
281
|
+
* URL is well-formed but dead.
|
|
282
|
+
*/
|
|
283
|
+
const ASSET_FIELDS = ["logo_url", "hero_url"];
|
|
284
|
+
/**
|
|
285
|
+
* Per-URL probe budget. Short on purpose: this runs on the critical path of
|
|
286
|
+
* every `plan` and `apply`, and a dead host that never answers must cost a
|
|
287
|
+
* couple of seconds, not a hung command.
|
|
288
|
+
*/
|
|
289
|
+
const DEFAULT_TIMEOUT_MS = 2500;
|
|
290
|
+
/** Keep redirect chains bounded and re-validate every destination ourselves. */
|
|
291
|
+
const MAX_REDIRECTS = 5;
|
|
292
|
+
/**
|
|
293
|
+
* Addresses an untrusted descriptor must never make the planning host contact.
|
|
294
|
+
* The browser-side render gate still supports canonical loopback HTTP for local
|
|
295
|
+
* development; the CLI probe deliberately leaves those URLs inconclusive
|
|
296
|
+
* instead of turning `plan` over repo content into a localhost/network scanner.
|
|
297
|
+
*/
|
|
298
|
+
const UNSAFE_IPV4_ADDRESSES = new BlockList();
|
|
299
|
+
for (const [network, prefix] of [
|
|
300
|
+
["0.0.0.0", 8],
|
|
301
|
+
["10.0.0.0", 8],
|
|
302
|
+
["100.64.0.0", 10],
|
|
303
|
+
["127.0.0.0", 8],
|
|
304
|
+
["169.254.0.0", 16],
|
|
305
|
+
["172.16.0.0", 12],
|
|
306
|
+
["192.0.0.0", 24],
|
|
307
|
+
["192.0.2.0", 24],
|
|
308
|
+
["192.88.99.0", 24],
|
|
309
|
+
["192.168.0.0", 16],
|
|
310
|
+
["198.18.0.0", 15],
|
|
311
|
+
["198.51.100.0", 24],
|
|
312
|
+
["203.0.113.0", 24],
|
|
313
|
+
["224.0.0.0", 4],
|
|
314
|
+
["240.0.0.0", 4]
|
|
315
|
+
]) UNSAFE_IPV4_ADDRESSES.addSubnet(network, prefix, "ipv4");
|
|
316
|
+
const UNSAFE_IPV6_ADDRESSES = new BlockList();
|
|
317
|
+
for (const [network, prefix] of [
|
|
318
|
+
["::", 3],
|
|
319
|
+
["4000::", 2],
|
|
320
|
+
["8000::", 1],
|
|
321
|
+
["2001::", 23],
|
|
322
|
+
["2001:db8::", 32],
|
|
323
|
+
["2002::", 16],
|
|
324
|
+
["3fff::", 20]
|
|
325
|
+
]) UNSAFE_IPV6_ADDRESSES.addSubnet(network, prefix, "ipv6");
|
|
326
|
+
/** Content types that are images despite not being `image/*`. */
|
|
327
|
+
const EXTRA_IMAGE_TYPES = new Set(["application/octet-stream"]);
|
|
328
|
+
/**
|
|
329
|
+
* Annotate every branding action in a plan with warnings for asset URLs that
|
|
330
|
+
* are well-formed but will not render: an unreachable host, a non-2xx status,
|
|
331
|
+
* or a response that is not an image.
|
|
332
|
+
*
|
|
333
|
+
* Why the CLI probes at all: a bad `logo_url` clears every gate on the way in
|
|
334
|
+
* (the CLI's Zod shape check, the server's save gate) and then fails silently
|
|
335
|
+
* in the browser as a 0×0 `<img>` — no plan output, no apply output, no
|
|
336
|
+
* console error. The probe is the only place in the pipeline that can see it.
|
|
337
|
+
*
|
|
338
|
+
* Deliberately non-fatal and best-effort. A developer on a plane, behind a
|
|
339
|
+
* proxy, or pointing at a CDN that only resolves from production gets a
|
|
340
|
+
* warning, never a failed plan — the machine running `plan` is not
|
|
341
|
+
* necessarily the machine that renders the login page. Set
|
|
342
|
+
* `ZITADEL_SKIP_ASSET_PROBE` to turn it off entirely, and
|
|
343
|
+
* `ZITADEL_ASSET_PROBE_TIMEOUT_MS` to retune the per-URL budget. Only public
|
|
344
|
+
* HTTPS destinations are contacted: loopback/private/internal targets and
|
|
345
|
+
* redirects to them stay inconclusive so repo config cannot scan the planning
|
|
346
|
+
* host's network.
|
|
347
|
+
*
|
|
348
|
+
* Mutates `actions` in place, like {@link validatePlannedFlows} — the caller
|
|
349
|
+
* (`buildSyncPlan`) owns the array it just built.
|
|
350
|
+
*/
|
|
351
|
+
async function annotateAssetWarnings(actions) {
|
|
352
|
+
if (process.env.ZITADEL_SKIP_ASSET_PROBE) {
|
|
353
|
+
consola$1.debug("Branding asset probe skipped (ZITADEL_SKIP_ASSET_PROBE is set)");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const targets = actions.filter((action) => (action.kind === "create" || action.kind === "update" || action.kind === "revise") && action.syncer.kind === "branding");
|
|
357
|
+
if (targets.length === 0) return;
|
|
358
|
+
const urls = /* @__PURE__ */ new Set();
|
|
359
|
+
for (const action of targets) for (const { url } of assetUrlsOf(action.content)) urls.add(url);
|
|
360
|
+
if (urls.size === 0) return;
|
|
361
|
+
const timeoutMs = probeTimeoutMs();
|
|
362
|
+
const verdicts = new Map(await Promise.all([...urls].map(async (url) => [url, await probeAsset(url, timeoutMs)])));
|
|
363
|
+
for (const action of targets) {
|
|
364
|
+
const warnings = [];
|
|
365
|
+
for (const { field, url } of assetUrlsOf(action.content)) {
|
|
366
|
+
const warning = describeVerdict(field, url, verdicts.get(url) ?? { kind: "inconclusive" });
|
|
367
|
+
if (warning) warnings.push(warning);
|
|
368
|
+
}
|
|
369
|
+
if (warnings.length > 0) action.warnings = [...action.warnings ?? [], ...warnings];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* The probe itself: a HEAD request bounded by an abort timeout. HEAD keeps a
|
|
374
|
+
* multi-megabyte hero image off the wire — we only need the status line and
|
|
375
|
+
* the content type.
|
|
376
|
+
*/
|
|
377
|
+
async function probeAsset(url, timeoutMs) {
|
|
378
|
+
let current;
|
|
379
|
+
try {
|
|
380
|
+
current = new URL(url);
|
|
381
|
+
} catch {
|
|
382
|
+
return { kind: "inconclusive" };
|
|
383
|
+
}
|
|
384
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
385
|
+
try {
|
|
386
|
+
for (let redirects = 0;; redirects += 1) {
|
|
387
|
+
if (!isSafeProbeUrl(current)) {
|
|
388
|
+
consola$1.debug(`asset probe ${url} skipped unsafe/private target: ${current.href}`);
|
|
389
|
+
return { kind: "inconclusive" };
|
|
390
|
+
}
|
|
391
|
+
const response = await requestHead(current, signal);
|
|
392
|
+
if (isRedirect(response.status)) {
|
|
393
|
+
const location = response.location;
|
|
394
|
+
if (!location) return {
|
|
395
|
+
kind: "status",
|
|
396
|
+
status: response.status
|
|
397
|
+
};
|
|
398
|
+
if (redirects >= MAX_REDIRECTS) return {
|
|
399
|
+
kind: "unreachable",
|
|
400
|
+
detail: `more than ${MAX_REDIRECTS} redirects`
|
|
401
|
+
};
|
|
402
|
+
current = new URL(location, current);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (response.status === 405 || response.status === 501) return { kind: "inconclusive" };
|
|
406
|
+
if (response.status < 200 || response.status >= 300) return {
|
|
407
|
+
kind: "status",
|
|
408
|
+
status: response.status
|
|
409
|
+
};
|
|
410
|
+
if (response.status === 204 || response.status === 205) return {
|
|
411
|
+
kind: "empty",
|
|
412
|
+
status: response.status
|
|
413
|
+
};
|
|
414
|
+
const contentType = response.contentType?.split(";")[0]?.trim().toLowerCase();
|
|
415
|
+
if (!contentType) return { kind: "ok" };
|
|
416
|
+
if (contentType.startsWith("image/") || EXTRA_IMAGE_TYPES.has(contentType)) return { kind: "ok" };
|
|
417
|
+
return {
|
|
418
|
+
kind: "content-type",
|
|
419
|
+
contentType
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
} catch (error) {
|
|
423
|
+
if (isUnsafeProbeTargetError(error)) {
|
|
424
|
+
consola$1.debug(`asset probe ${url} skipped unsafe/private target`);
|
|
425
|
+
return { kind: "inconclusive" };
|
|
426
|
+
}
|
|
427
|
+
consola$1.debug(`asset probe ${url} failed:`, error);
|
|
428
|
+
return {
|
|
429
|
+
kind: "unreachable",
|
|
430
|
+
detail: describeProbeError(error, timeoutMs)
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function isRedirect(status) {
|
|
435
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Reject targets whose URL alone proves they are unsafe. Hostname resolution
|
|
439
|
+
* happens inside the connector lookup below so validation and connection use
|
|
440
|
+
* the same DNS answer, without a rebinding window.
|
|
441
|
+
*/
|
|
442
|
+
function isSafeProbeUrl(url) {
|
|
443
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "") return false;
|
|
444
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
|
|
445
|
+
if (hostname === "localhost" || !hostname.includes(".") || hostname.endsWith(".localhost") || hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".lan") || hostname.endsWith(".home.arpa")) return false;
|
|
446
|
+
const literalFamily = isIP(hostname);
|
|
447
|
+
return literalFamily === 0 || !isUnsafeAddress(hostname, literalFamily);
|
|
448
|
+
}
|
|
449
|
+
/** Resolve once and accept the answer only when every address is public. */
|
|
450
|
+
async function resolvePublicAddresses(hostname) {
|
|
451
|
+
const addresses = await lookup(hostname, {
|
|
452
|
+
all: true,
|
|
453
|
+
verbatim: true
|
|
454
|
+
});
|
|
455
|
+
return addresses.length > 0 && addresses.every(({ address, family }) => !isUnsafeAddress(address, family)) ? addresses : void 0;
|
|
456
|
+
}
|
|
457
|
+
function isUnsafeAddress(address, family) {
|
|
458
|
+
return family === 6 ? UNSAFE_IPV6_ADDRESSES.check(address, "ipv6") : UNSAFE_IPV4_ADDRESSES.check(address, "ipv4");
|
|
459
|
+
}
|
|
460
|
+
const UNSAFE_PROBE_TARGET_CODE = "ERR_ZITADEL_UNSAFE_ASSET_TARGET";
|
|
461
|
+
function unsafeProbeTargetError(hostname) {
|
|
462
|
+
return Object.assign(/* @__PURE__ */ new Error(`asset target ${hostname} resolves to a non-public address`), { code: UNSAFE_PROBE_TARGET_CODE });
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Validate DNS in the connector's own lookup and return only approved
|
|
466
|
+
* addresses. The socket cannot perform a second lookup, which closes the DNS
|
|
467
|
+
* rebinding gap while preserving the hostname for TLS SNI/certificate checks.
|
|
468
|
+
*/
|
|
469
|
+
async function requestHead(url, signal) {
|
|
470
|
+
const safeLookup = (hostname, options, callback) => {
|
|
471
|
+
resolvePublicAddresses(hostname).then((addresses) => {
|
|
472
|
+
if (!addresses) {
|
|
473
|
+
callback(unsafeProbeTargetError(hostname), "", 0);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const candidates = options.family === 4 || options.family === 6 ? addresses.filter(({ family }) => family === options.family) : addresses;
|
|
477
|
+
const candidate = candidates[0];
|
|
478
|
+
if (!candidate) {
|
|
479
|
+
callback(unsafeProbeTargetError(hostname), "", 0);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (options.all) {
|
|
483
|
+
callback(null, candidates);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
callback(null, candidate.address, candidate.family);
|
|
487
|
+
}, (error) => callback(error, "", 0));
|
|
488
|
+
};
|
|
489
|
+
const dispatcher = new Agent({ connect: { lookup: safeLookup } });
|
|
490
|
+
try {
|
|
491
|
+
const response = await fetch(url, {
|
|
492
|
+
method: "HEAD",
|
|
493
|
+
redirect: "manual",
|
|
494
|
+
signal,
|
|
495
|
+
dispatcher
|
|
496
|
+
});
|
|
497
|
+
return {
|
|
498
|
+
status: response.status,
|
|
499
|
+
location: response.headers.get("location") ?? void 0,
|
|
500
|
+
contentType: response.headers.get("content-type") ?? void 0
|
|
501
|
+
};
|
|
502
|
+
} finally {
|
|
503
|
+
await dispatcher.destroy();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function isUnsafeProbeTargetError(error) {
|
|
507
|
+
let current = error;
|
|
508
|
+
for (let depth = 0; depth < 4 && current instanceof Error; depth += 1) {
|
|
509
|
+
if (current.code === UNSAFE_PROBE_TARGET_CODE) return true;
|
|
510
|
+
current = current.cause;
|
|
511
|
+
}
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
function describeVerdict(field, url, verdict) {
|
|
515
|
+
switch (verdict.kind) {
|
|
516
|
+
case "ok":
|
|
517
|
+
case "inconclusive": return;
|
|
518
|
+
case "status": return {
|
|
519
|
+
rule: "warn/asset-unreachable",
|
|
520
|
+
message: `${field} ${url} returned HTTP ${verdict.status} — the login page renders a well-formed URL that serves nothing as a 0×0 image, with no error anywhere.`
|
|
521
|
+
};
|
|
522
|
+
case "empty": return {
|
|
523
|
+
rule: "warn/asset-unreachable",
|
|
524
|
+
message: `${field} ${url} returned HTTP ${verdict.status} with no representation — the login page has no image bytes to render.`
|
|
525
|
+
};
|
|
526
|
+
case "unreachable": return {
|
|
527
|
+
rule: "warn/asset-unreachable",
|
|
528
|
+
message: `${field} ${url} could not be reached (${verdict.detail}) — the login page renders an unreachable URL as a 0×0 image. Ignore this if the host is only reachable from where the login page renders; set ZITADEL_SKIP_ASSET_PROBE to stop checking.`
|
|
529
|
+
};
|
|
530
|
+
case "content-type": return {
|
|
531
|
+
rule: "warn/asset-content-type",
|
|
532
|
+
message: `${field} ${url} responded with content-type "${verdict.contentType}", not an image — the login page renders a non-image response as a 0×0 image.`
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/** The `logo_url` / `hero_url` values a descriptor actually sets. */
|
|
537
|
+
function assetUrlsOf(content) {
|
|
538
|
+
const body = content;
|
|
539
|
+
const out = [];
|
|
540
|
+
for (const field of ASSET_FIELDS) {
|
|
541
|
+
const value = body[field];
|
|
542
|
+
if (typeof value === "string" && value !== "") out.push({
|
|
543
|
+
field,
|
|
544
|
+
url: value
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
return out;
|
|
548
|
+
}
|
|
549
|
+
function probeTimeoutMs() {
|
|
550
|
+
const raw = Number(process.env.ZITADEL_ASSET_PROBE_TIMEOUT_MS);
|
|
551
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_TIMEOUT_MS;
|
|
552
|
+
}
|
|
553
|
+
function describeProbeError(error, timeoutMs) {
|
|
554
|
+
if (error instanceof Error && (error.name === "TimeoutError" || error.cause instanceof Error && error.cause.name === "TimeoutError")) return `no response within ${timeoutMs}ms`;
|
|
555
|
+
if (error instanceof Error) {
|
|
556
|
+
const cause = error.cause;
|
|
557
|
+
return (cause instanceof Error ? cause.message : error.message) || error.name;
|
|
558
|
+
}
|
|
559
|
+
return String(error);
|
|
560
|
+
}
|
|
561
|
+
//#endregion
|
|
273
562
|
//#region src/lib/sync/flow-validation.ts
|
|
274
563
|
/**
|
|
275
564
|
* Pre-flight semantic validation for every flow this plan uploads,
|
|
@@ -512,6 +801,7 @@ async function buildSyncPlan(cwd, syncers, fetchOld = false) {
|
|
|
512
801
|
scannedContents,
|
|
513
802
|
stateResources: state.resources
|
|
514
803
|
});
|
|
804
|
+
await annotateAssetWarnings(actions);
|
|
515
805
|
return actions;
|
|
516
806
|
}
|
|
517
807
|
/**
|
|
@@ -531,7 +821,7 @@ async function buildSyncPlan(cwd, syncers, fetchOld = false) {
|
|
|
531
821
|
*/
|
|
532
822
|
async function runSyncLoop(cwd, syncers) {
|
|
533
823
|
const actions = await buildSyncPlan(cwd, syncers);
|
|
534
|
-
for (const action of actions) if (action.kind === "create" || action.kind === "update") for (const warning of action.warnings ?? []) consola$1.warn(`${action.path}: ${warning.message}`);
|
|
824
|
+
for (const action of actions) if (action.kind === "create" || action.kind === "update" || action.kind === "revise") for (const warning of action.warnings ?? []) consola$1.warn(`${action.path}: ${warning.message}`);
|
|
535
825
|
const filesUpdated = [];
|
|
536
826
|
const applied = [];
|
|
537
827
|
const repinned = /* @__PURE__ */ new Map();
|
|
@@ -792,17 +1082,21 @@ function summarizePlan(actions) {
|
|
|
792
1082
|
*/
|
|
793
1083
|
function collectPlanWarnings(actions) {
|
|
794
1084
|
const out = [];
|
|
795
|
-
for (const action of actions) {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
message: warning.message
|
|
801
|
-
});
|
|
802
|
-
}
|
|
1085
|
+
for (const action of actions) for (const warning of warningsOf(action)) out.push({
|
|
1086
|
+
path: action.path,
|
|
1087
|
+
rule: warning.rule,
|
|
1088
|
+
message: warning.message
|
|
1089
|
+
});
|
|
803
1090
|
return out;
|
|
804
1091
|
}
|
|
805
1092
|
/**
|
|
1093
|
+
* The warnings an action carries. Only the three kinds that upload a body can
|
|
1094
|
+
* have any: a `delete` has no content to judge, and a `skip` was never judged.
|
|
1095
|
+
*/
|
|
1096
|
+
function warningsOf(action) {
|
|
1097
|
+
return action.kind === "create" || action.kind === "update" || action.kind === "revise" ? action.warnings ?? [] : [];
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
806
1100
|
* Enumerate the non-`skip` actions of a {@link buildSyncPlan} result as
|
|
807
1101
|
* envelope-ready {@link PlanResourceChange} rows. Pure; the structural
|
|
808
1102
|
* counterpart of {@link summarizePlan} — counts and rows always agree.
|
|
@@ -871,7 +1165,7 @@ function renderPlan(actions, tty) {
|
|
|
871
1165
|
if (revisions > 0) parts.push(`${revisions} new revision${revisions === 1 ? "" : "s"}`);
|
|
872
1166
|
if (deletes > 0) parts.push(`${deletes} to destroy`);
|
|
873
1167
|
out.push(paint(`Plan: ${parts.join(", ")}.`, A.bold, tty));
|
|
874
|
-
const warningCount = active.reduce((count, action) => count + (
|
|
1168
|
+
const warningCount = active.reduce((count, action) => count + warningsOf(action).length, 0);
|
|
875
1169
|
if (warningCount > 0) out.push(paint(`Warnings: ${warningCount} (non-blocking — see the # warning lines above).`, A.yellow, tty));
|
|
876
1170
|
return out.join("\n");
|
|
877
1171
|
}
|
|
@@ -901,6 +1195,37 @@ function fmtPrimitive(v) {
|
|
|
901
1195
|
if (typeof v === "string") return `"${escapeString(v)}"`;
|
|
902
1196
|
return String(v);
|
|
903
1197
|
}
|
|
1198
|
+
/**
|
|
1199
|
+
* A multi-line string is a document, not a scalar: branding inlines a whole
|
|
1200
|
+
* `login.liquid` into `liquid_template`, and escaping 200 lines onto one
|
|
1201
|
+
* `"…\n…\n…"` line buries every real change in the same block. Such values
|
|
1202
|
+
* render as a summary (and, when they changed, as a line diff) instead.
|
|
1203
|
+
*/
|
|
1204
|
+
function isBlockString(v) {
|
|
1205
|
+
return typeof v === "string" && v !== KNOWN_AFTER_APPLY && v.includes("\n");
|
|
1206
|
+
}
|
|
1207
|
+
/** Trailing-newline-insensitive line count: `"a\nb\n"` is two lines, not three. */
|
|
1208
|
+
function lineCount(value) {
|
|
1209
|
+
const lines = value.split("\n");
|
|
1210
|
+
return lines.length > 1 && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
|
|
1211
|
+
}
|
|
1212
|
+
/** Short content fingerprint, so two summarised blocks can be told apart. */
|
|
1213
|
+
function shortHash(value) {
|
|
1214
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 8);
|
|
1215
|
+
}
|
|
1216
|
+
function plural(count, noun) {
|
|
1217
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
1218
|
+
}
|
|
1219
|
+
function blockSummary(value) {
|
|
1220
|
+
return `(${plural(lineCount(value), "line")}, sha256:${shortHash(value)})`;
|
|
1221
|
+
}
|
|
1222
|
+
function unchangedBlockSummary(value) {
|
|
1223
|
+
return `(unchanged, ${plural(lineCount(value), "line")}, sha256:${shortHash(value)})`;
|
|
1224
|
+
}
|
|
1225
|
+
/** {@link fmtPrimitive}, with multi-line strings summarised. */
|
|
1226
|
+
function fmtScalar(v) {
|
|
1227
|
+
return isBlockString(v) ? blockSummary(v) : fmtPrimitive(v);
|
|
1228
|
+
}
|
|
904
1229
|
function prefixAnsi(p) {
|
|
905
1230
|
if (p === "+") return A.green;
|
|
906
1231
|
if (p === "-") return A.red;
|
|
@@ -917,7 +1242,7 @@ function renderFields(obj, prefix, prefixCol, ctx, lines) {
|
|
|
917
1242
|
const val = obj[key];
|
|
918
1243
|
const pk = key.padEnd(maxLen);
|
|
919
1244
|
if (isPrimitive(val)) {
|
|
920
|
-
const formatted =
|
|
1245
|
+
const formatted = fmtScalar(val);
|
|
921
1246
|
const suffix = ctx.deleteMode ? " -> null" : "";
|
|
922
1247
|
lines.push(col(`${pad}${prefix} ${pk} = ${formatted}${suffix}`));
|
|
923
1248
|
} else if (Array.isArray(val)) if (val.length === 0) lines.push(col(`${pad}${prefix} ${pk} = []`));
|
|
@@ -945,7 +1270,7 @@ function renderArrayItems(arr, prefix, prefixCol, ctx, lines) {
|
|
|
945
1270
|
const ansi = prefixAnsi(prefix);
|
|
946
1271
|
const col = (s) => paint(s, ansi, ctx.tty);
|
|
947
1272
|
for (const item of arr) if (isPrimitive(item)) {
|
|
948
|
-
const formatted =
|
|
1273
|
+
const formatted = fmtScalar(item);
|
|
949
1274
|
lines.push(col(`${pad}${prefix} ${formatted},`));
|
|
950
1275
|
} else if (Array.isArray(item)) if (item.length === 0) lines.push(col(`${pad}${prefix} [],`));
|
|
951
1276
|
else {
|
|
@@ -960,6 +1285,102 @@ function renderArrayItems(arr, prefix, prefixCol, ctx, lines) {
|
|
|
960
1285
|
lines.push(col(`${" ".repeat(prefixCol + 2)}},`));
|
|
961
1286
|
}
|
|
962
1287
|
}
|
|
1288
|
+
/** Changed-line budget for one block-string diff, before the "more" trailer. */
|
|
1289
|
+
const MAX_BLOCK_DIFF_LINES = 20;
|
|
1290
|
+
/**
|
|
1291
|
+
* LCS cell budget (500 × 500 changed lines). Above it the middle section
|
|
1292
|
+
* renders as one remove/add block instead — quadratic DP over a pathological
|
|
1293
|
+
* template is not worth the memory, and a whole-block replace is still an
|
|
1294
|
+
* honest rendering. The shipped designs are under 200 lines *before* the
|
|
1295
|
+
* prefix/suffix trim, so real templates never come close.
|
|
1296
|
+
*/
|
|
1297
|
+
const MAX_LCS_CELLS = 25e4;
|
|
1298
|
+
function asLines(v) {
|
|
1299
|
+
return typeof v === "string" ? v.split("\n") : [fmtPrimitive(v)];
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Line-level diff of two block strings. Common prefix and suffix are trimmed
|
|
1303
|
+
* first — a template edit touches one region, so this alone usually reduces
|
|
1304
|
+
* the problem to a handful of lines — and the remaining middle goes through
|
|
1305
|
+
* an LCS pass (or, past {@link MAX_LCS_CELLS}, renders as a wholesale
|
|
1306
|
+
* replacement).
|
|
1307
|
+
*/
|
|
1308
|
+
function diffLines(oldLines, newLines) {
|
|
1309
|
+
let start = 0;
|
|
1310
|
+
while (start < oldLines.length && start < newLines.length && oldLines[start] === newLines[start]) start += 1;
|
|
1311
|
+
let endOld = oldLines.length;
|
|
1312
|
+
let endNew = newLines.length;
|
|
1313
|
+
while (endOld > start && endNew > start && oldLines[endOld - 1] === newLines[endNew - 1]) {
|
|
1314
|
+
endOld -= 1;
|
|
1315
|
+
endNew -= 1;
|
|
1316
|
+
}
|
|
1317
|
+
const midOld = oldLines.slice(start, endOld);
|
|
1318
|
+
const midNew = newLines.slice(start, endNew);
|
|
1319
|
+
if (midOld.length * midNew.length > MAX_LCS_CELLS) return [...midOld.map((line) => ({
|
|
1320
|
+
kind: "del",
|
|
1321
|
+
line
|
|
1322
|
+
})), ...midNew.map((line) => ({
|
|
1323
|
+
kind: "add",
|
|
1324
|
+
line
|
|
1325
|
+
}))];
|
|
1326
|
+
return lcsDiff(midOld, midNew);
|
|
1327
|
+
}
|
|
1328
|
+
/** Classic LCS-length DP, walked back into an op list. */
|
|
1329
|
+
function lcsDiff(a, b) {
|
|
1330
|
+
const table = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
1331
|
+
for (let i = a.length - 1; i >= 0; i -= 1) for (let j = b.length - 1; j >= 0; j -= 1) table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
|
|
1332
|
+
const ops = [];
|
|
1333
|
+
let i = 0;
|
|
1334
|
+
let j = 0;
|
|
1335
|
+
while (i < a.length && j < b.length) if (a[i] === b[j]) {
|
|
1336
|
+
ops.push({
|
|
1337
|
+
kind: "same",
|
|
1338
|
+
line: a[i]
|
|
1339
|
+
});
|
|
1340
|
+
i += 1;
|
|
1341
|
+
j += 1;
|
|
1342
|
+
} else if (table[i + 1][j] >= table[i][j + 1]) {
|
|
1343
|
+
ops.push({
|
|
1344
|
+
kind: "del",
|
|
1345
|
+
line: a[i]
|
|
1346
|
+
});
|
|
1347
|
+
i += 1;
|
|
1348
|
+
} else {
|
|
1349
|
+
ops.push({
|
|
1350
|
+
kind: "add",
|
|
1351
|
+
line: b[j]
|
|
1352
|
+
});
|
|
1353
|
+
j += 1;
|
|
1354
|
+
}
|
|
1355
|
+
for (; i < a.length; i += 1) ops.push({
|
|
1356
|
+
kind: "del",
|
|
1357
|
+
line: a[i]
|
|
1358
|
+
});
|
|
1359
|
+
for (; j < b.length; j += 1) ops.push({
|
|
1360
|
+
kind: "add",
|
|
1361
|
+
line: b[j]
|
|
1362
|
+
});
|
|
1363
|
+
return ops;
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Render a changed block string as a header plus its changed lines. Context
|
|
1367
|
+
* lines are deliberately omitted: the file is on disk, and the plan's job is
|
|
1368
|
+
* to name what moved, not to reproduce the template.
|
|
1369
|
+
*/
|
|
1370
|
+
function renderBlockStringDiff(paddedKey, oldVal, newVal, prefixCol, tty, lines) {
|
|
1371
|
+
const pad = " ".repeat(prefixCol);
|
|
1372
|
+
const bodyPad = " ".repeat(prefixCol + 4);
|
|
1373
|
+
const changed = diffLines(asLines(oldVal), asLines(newVal)).filter((op) => op.kind !== "same");
|
|
1374
|
+
const total = typeof newVal === "string" ? lineCount(newVal) : 1;
|
|
1375
|
+
const fingerprints = typeof oldVal === "string" && typeof newVal === "string" ? `, sha256:${shortHash(oldVal)} -> sha256:${shortHash(newVal)}` : "";
|
|
1376
|
+
lines.push(paint(`${pad}~ ${paddedKey} = (${plural(changed.length, "line")} changed of ${total}${fingerprints})`, A.yellow, tty));
|
|
1377
|
+
for (const op of changed.slice(0, MAX_BLOCK_DIFF_LINES)) {
|
|
1378
|
+
const del = op.kind === "del";
|
|
1379
|
+
lines.push(paint(`${bodyPad}${del ? "-" : "+"} ${op.line}`, del ? A.red : A.green, tty));
|
|
1380
|
+
}
|
|
1381
|
+
const omitted = changed.length - MAX_BLOCK_DIFF_LINES;
|
|
1382
|
+
if (omitted > 0) lines.push(`${bodyPad} # (${plural(omitted, "more changed line")} not shown)`);
|
|
1383
|
+
}
|
|
963
1384
|
/**
|
|
964
1385
|
* Walks both old and new objects, emitting Terraform-style change lines.
|
|
965
1386
|
* Returns true if any actual change line (+ / - / ~) was emitted.
|
|
@@ -985,7 +1406,7 @@ function renderDiff(oldObj, newObj, prefixCol, tty, lines) {
|
|
|
985
1406
|
if (!hasOld) {
|
|
986
1407
|
hasChanges = true;
|
|
987
1408
|
const col = (s) => paint(s, A.green, tty);
|
|
988
|
-
if (isPrimitive(newVal)) lines.push(col(`${pad}+ ${pk} = ${
|
|
1409
|
+
if (isPrimitive(newVal)) lines.push(col(`${pad}+ ${pk} = ${fmtScalar(newVal)}`));
|
|
989
1410
|
else if (Array.isArray(newVal)) {
|
|
990
1411
|
lines.push(col(`${pad}+ ${pk} = [`));
|
|
991
1412
|
renderArrayItems(newVal, "+", prefixCol + 4, {
|
|
@@ -1004,7 +1425,7 @@ function renderDiff(oldObj, newObj, prefixCol, tty, lines) {
|
|
|
1004
1425
|
} else if (!hasNew) {
|
|
1005
1426
|
hasChanges = true;
|
|
1006
1427
|
const col = (s) => paint(s, A.red, tty);
|
|
1007
|
-
if (isPrimitive(oldVal)) lines.push(col(`${pad}- ${pk} = ${
|
|
1428
|
+
if (isPrimitive(oldVal)) lines.push(col(`${pad}- ${pk} = ${fmtScalar(oldVal)} -> null`));
|
|
1008
1429
|
else if (Array.isArray(oldVal)) {
|
|
1009
1430
|
lines.push(col(`${pad}- ${pk} = [`));
|
|
1010
1431
|
renderArrayItems(oldVal, "-", prefixCol + 4, {
|
|
@@ -1020,8 +1441,11 @@ function renderDiff(oldObj, newObj, prefixCol, tty, lines) {
|
|
|
1020
1441
|
}, lines);
|
|
1021
1442
|
lines.push(col(`${" ".repeat(prefixCol + 2)}}`));
|
|
1022
1443
|
}
|
|
1023
|
-
} else if (isPrimitive(oldVal) && isPrimitive(newVal)) if (oldVal === newVal) lines.push(`${pad} ${pk} = ${fmtPrimitive(newVal)}`);
|
|
1024
|
-
else {
|
|
1444
|
+
} else if (isPrimitive(oldVal) && isPrimitive(newVal)) if (oldVal === newVal) lines.push(isBlockString(newVal) ? `${pad} ${pk} = ${unchangedBlockSummary(newVal)}` : `${pad} ${pk} = ${fmtPrimitive(newVal)}`);
|
|
1445
|
+
else if (isBlockString(oldVal) || isBlockString(newVal)) {
|
|
1446
|
+
hasChanges = true;
|
|
1447
|
+
renderBlockStringDiff(pk, oldVal, newVal, prefixCol, tty, lines);
|
|
1448
|
+
} else {
|
|
1025
1449
|
hasChanges = true;
|
|
1026
1450
|
const col = (s) => paint(s, A.yellow, tty);
|
|
1027
1451
|
lines.push(col(`${pad}~ ${pk} = ${fmtPrimitive(oldVal)} -> ${fmtPrimitive(newVal)}`));
|
|
@@ -1075,8 +1499,8 @@ function renderDiff(oldObj, newObj, prefixCol, tty, lines) {
|
|
|
1075
1499
|
hasChanges = true;
|
|
1076
1500
|
const colR = (s) => paint(s, A.red, tty);
|
|
1077
1501
|
const colA = (s) => paint(s, A.green, tty);
|
|
1078
|
-
if (isPrimitive(oldVal)) lines.push(colR(`${pad}- ${pk} = ${
|
|
1079
|
-
if (isPrimitive(newVal)) lines.push(colA(`${pad}+ ${pk} = ${
|
|
1502
|
+
if (isPrimitive(oldVal)) lines.push(colR(`${pad}- ${pk} = ${fmtScalar(oldVal)} -> null`));
|
|
1503
|
+
if (isPrimitive(newVal)) lines.push(colA(`${pad}+ ${pk} = ${fmtScalar(newVal)}`));
|
|
1080
1504
|
}
|
|
1081
1505
|
}
|
|
1082
1506
|
return hasChanges;
|
|
@@ -1184,6 +1608,7 @@ function renderBlock(action, tty) {
|
|
|
1184
1608
|
}, FIELD_COL, tty, lines);
|
|
1185
1609
|
else lines.push(`${" ".repeat(FIELD_COL)} # (field diff unavailable — no read endpoint for ${action.syncer.kind})`);
|
|
1186
1610
|
lines.push(`${closePad}}`);
|
|
1611
|
+
renderWarnings(action.warnings, blkPad, tty, lines);
|
|
1187
1612
|
if (action.affectedPaths.length > 0) {
|
|
1188
1613
|
lines.push(paint(`${blkPad}# user_schema will be re-pinned to the new revision ${KNOWN_AFTER_APPLY} in:`, A.yellow, tty));
|
|
1189
1614
|
for (const path of action.affectedPaths) lines.push(paint(`${blkPad}# - ${path}`, A.yellow, tty));
|
|
@@ -1205,4 +1630,4 @@ function renderWarnings(warnings, blkPad, tty, lines) {
|
|
|
1205
1630
|
//#endregion
|
|
1206
1631
|
export { buildSyncPlan as a, writeBackResource as c, summarizePlan as i, makeSyncers as l, enumeratePlanResources as n, hashForState as o, renderPlan as r, runSyncLoop as s, collectPlanWarnings as t, FLOWS_DIR as u };
|
|
1207
1632
|
|
|
1208
|
-
//# sourceMappingURL=sync-
|
|
1633
|
+
//# sourceMappingURL=sync-Bd6vASgg.mjs.map
|