@m-kopa/launchpad-cli 0.57.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +10 -0
  3. package/dist/bundle/upload.d.ts +2 -0
  4. package/dist/bundle/upload.d.ts.map +1 -1
  5. package/dist/cli.js +845 -326
  6. package/dist/commands/deploy-flags.d.ts.map +1 -1
  7. package/dist/commands/deploy.d.ts +15 -0
  8. package/dist/commands/deploy.d.ts.map +1 -1
  9. package/dist/commands/init.d.ts +5 -1
  10. package/dist/commands/init.d.ts.map +1 -1
  11. package/dist/commands/status.d.ts +14 -1
  12. package/dist/commands/status.d.ts.map +1 -1
  13. package/dist/commands/validate.d.ts.map +1 -1
  14. package/dist/deploy/apply.d.ts +6 -0
  15. package/dist/deploy/apply.d.ts.map +1 -1
  16. package/dist/deploy/commit-match-settle.d.ts +14 -1
  17. package/dist/deploy/commit-match-settle.d.ts.map +1 -1
  18. package/dist/deploy/deployment-status.d.ts +19 -0
  19. package/dist/deploy/deployment-status.d.ts.map +1 -1
  20. package/dist/deploy/manifest-diff.d.ts +2 -0
  21. package/dist/deploy/manifest-diff.d.ts.map +1 -1
  22. package/dist/deploy/verify-deploy.d.ts +2 -0
  23. package/dist/deploy/verify-deploy.d.ts.map +1 -1
  24. package/dist/manifest/load.d.ts +27 -0
  25. package/dist/manifest/load.d.ts.map +1 -1
  26. package/dist/secrets/push.d.ts +8 -0
  27. package/dist/secrets/push.d.ts.map +1 -1
  28. package/dist/secrets/status.d.ts.map +1 -1
  29. package/dist/version.d.ts +1 -1
  30. package/package.json +1 -1
  31. package/skills/launchpad-content-pr/SKILL.md +13 -1
  32. package/skills/launchpad-deploy/SKILL.md +50 -23
  33. package/skills/launchpad-deploy-status/SKILL.md +10 -1
  34. package/skills/launchpad-destroy/SKILL.md +1 -1
  35. package/skills/launchpad-identity/SKILL.md +1 -1
  36. package/skills/launchpad-onboard/SKILL.md +13 -1
  37. package/skills/launchpad-report/SKILL.md +1 -1
  38. package/skills/launchpad-status/SKILL.md +10 -1
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => {
19
19
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  // src/version.ts
22
- var CLI_VERSION = "0.57.0";
22
+ var CLI_VERSION = "0.61.0";
23
23
 
24
24
  // src/config.ts
25
25
  import { readFileSync } from "node:fs";
@@ -2138,6 +2138,9 @@ function parse202(body, fallbackSlug) {
2138
2138
  slug: typeof body.slug === "string" ? body.slug : fallbackSlug,
2139
2139
  message: typeof body.message === "string" ? body.message : "bundle committed; deployment is pending"
2140
2140
  };
2141
+ if (body.v2Delivery !== undefined && body.v2Delivery !== "pending" && body.v2Delivery !== "deferred") {
2142
+ return null;
2143
+ }
2141
2144
  return reconcile === undefined ? accepted : { ...accepted, reconcile };
2142
2145
  }
2143
2146
  if (body.status === "provisioning_started") {
@@ -2567,11 +2570,23 @@ import { join as join6 } from "node:path";
2567
2570
  import { parse as parseYaml2 } from "yaml";
2568
2571
 
2569
2572
  // ../launchpad-engine/dist/schema.js
2573
+ import { z as z2 } from "zod";
2574
+
2575
+ // ../launchpad-engine/dist/audience.js
2570
2576
  import { z } from "zod";
2577
+ var PrivateOwnerSchema = z.object({ oid: z.uuid(), email: z.email() }).strict();
2578
+ function validateAudience(access, ctx) {
2579
+ const groups = access.allowed_entra_groups ?? (access.allowed_entra_group ? [access.allowed_entra_group] : []);
2580
+ const valid = access.audience === "private" ? access.private_owner !== undefined && access.allowed_entra_groups !== undefined && groups.length === 0 && !access.identity_types?.includes("dsr") : access.private_owner === undefined && groups.length > 0;
2581
+ if (!valid)
2582
+ ctx.addIssue({ code: "custom", path: ["audience"], message: "Private access requires a verified private_owner, no groups, and employee identity; group access requires at least one group and no private_owner." });
2583
+ }
2584
+
2585
+ // ../launchpad-engine/dist/schema.js
2571
2586
  var APP_TYPES2 = ["static", "react", "react+api", "container"];
2572
2587
  var AUTH_MODES = ["access", "gateway"];
2573
2588
  var IDENTITY_TYPES = ["employee", "dsr"];
2574
- var IdentityTypesSchema = z.array(z.enum(IDENTITY_TYPES)).min(1).refine((values) => new Set(values).size === values.length, {
2589
+ var IdentityTypesSchema = z2.array(z2.enum(IDENTITY_TYPES)).min(1).refine((values) => new Set(values).size === values.length, {
2575
2590
  message: "identity_types must not contain duplicates"
2576
2591
  }).refine((values) => values.includes("employee"), {
2577
2592
  message: "identity_types must include employee"
@@ -2583,111 +2598,115 @@ var PRIMARY_TARGET = "primary";
2583
2598
  var SLUG_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
2584
2599
  var ENV_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/;
2585
2600
  var SESSION_DURATION_REGEX = /^[0-9]+(s|m|h)$/;
2586
- var MetadataSchema = z.object({
2587
- name: z.string().min(2).max(63).regex(SLUG_REGEX, "must be lowercase letters, digits, and hyphens; start/end alphanumeric"),
2588
- team: z.string().min(1),
2589
- owner: z.string().email(),
2590
- description: z.string().max(280).optional()
2601
+ var MetadataSchema = z2.object({
2602
+ name: z2.string().min(2).max(63).regex(SLUG_REGEX, "must be lowercase letters, digits, and hyphens; start/end alphanumeric"),
2603
+ team: z2.string().min(1),
2604
+ owner: z2.string().email(),
2605
+ description: z2.string().max(280).optional()
2591
2606
  }).strict();
2592
- var DeploymentSchema = z.object({
2593
- type: z.enum(APP_TYPES2),
2594
- runtime: z.enum(RUNTIMES).optional()
2607
+ var DeploymentSchema = z2.object({
2608
+ type: z2.enum(APP_TYPES2),
2609
+ runtime: z2.enum(RUNTIMES).optional()
2595
2610
  }).strict();
2596
- var AccessSchema = z.object({
2597
- allowed_entra_group: z.string().min(1).optional(),
2598
- allowed_entra_groups: z.array(z.string().min(1)).min(1).refine((arr) => new Set(arr).size === arr.length, { message: "allowed_entra_groups must not contain duplicates" }).optional(),
2599
- session_duration: z.string().regex(SESSION_DURATION_REGEX, "must match Go duration format, e.g. 24h / 30m / 60s").optional(),
2611
+ var AccessSchema = z2.object({
2612
+ audience: z2.literal("private").optional(),
2613
+ private_owner: PrivateOwnerSchema.optional(),
2614
+ allowed_entra_group: z2.string().min(1).optional(),
2615
+ allowed_entra_groups: z2.array(z2.string().min(1)).refine((arr) => new Set(arr).size === arr.length, { message: "allowed_entra_groups must not contain duplicates" }).optional(),
2616
+ session_duration: z2.string().regex(SESSION_DURATION_REGEX, "must match Go duration format, e.g. 24h / 30m / 60s").optional(),
2600
2617
  identity_types: IdentityTypesSchema.optional()
2601
2618
  }).strict().superRefine((access, ctx) => {
2602
2619
  const hasSingular = access.allowed_entra_group !== undefined;
2603
2620
  const hasPlural = access.allowed_entra_groups !== undefined;
2604
- if (!hasSingular && !hasPlural) {
2621
+ if (!hasSingular && !hasPlural && access.audience !== "private") {
2605
2622
  ctx.addIssue({
2606
- code: z.ZodIssueCode.custom,
2623
+ code: z2.ZodIssueCode.custom,
2607
2624
  message: "either `allowed_entra_group` (singular, legacy) or `allowed_entra_groups` (plural, preferred) must be set",
2608
2625
  path: ["allowed_entra_groups"]
2609
2626
  });
2610
2627
  }
2611
2628
  if (hasSingular && hasPlural) {
2612
2629
  ctx.addIssue({
2613
- code: z.ZodIssueCode.custom,
2630
+ code: z2.ZodIssueCode.custom,
2614
2631
  message: "`allowed_entra_group` (singular) and `allowed_entra_groups` (plural) are mutually exclusive; use the plural form",
2615
2632
  path: ["allowed_entra_groups"]
2616
2633
  });
2617
2634
  }
2618
- });
2635
+ }).superRefine(validateAudience);
2619
2636
  function allowedEntraGroups(access) {
2637
+ if (access.audience === "private")
2638
+ return [];
2620
2639
  if (access.allowed_entra_groups !== undefined) {
2621
2640
  return access.allowed_entra_groups;
2622
2641
  }
2623
2642
  return [access.allowed_entra_group];
2624
2643
  }
2625
- var ContainerSchema = z.object({
2626
- name: z.string().min(2).max(32).regex(SLUG_REGEX, "container name must be lowercase letters, digits, and hyphens"),
2627
- image: z.string().min(1),
2628
- port: z.number().int().min(1).max(65535),
2629
- ingress: z.boolean().optional(),
2630
- healthcheck: z.string().default("/health"),
2631
- env: z.array(z.string().regex(ENV_NAME_REGEX, "env entries must be UPPER_SNAKE_CASE")).optional(),
2632
- max_instances: z.number().int().min(1).max(100).optional()
2644
+ var ContainerSchema = z2.object({
2645
+ name: z2.string().min(2).max(32).regex(SLUG_REGEX, "container name must be lowercase letters, digits, and hyphens"),
2646
+ image: z2.string().min(1),
2647
+ port: z2.number().int().min(1).max(65535),
2648
+ ingress: z2.boolean().optional(),
2649
+ healthcheck: z2.string().default("/health"),
2650
+ env: z2.array(z2.string().regex(ENV_NAME_REGEX, "env entries must be UPPER_SNAKE_CASE")).optional(),
2651
+ max_instances: z2.number().int().min(1).max(100).optional()
2633
2652
  }).strict();
2634
- var TargetSchema = z.object({
2635
- kind: z.enum(TARGET_KINDS),
2636
- script: z.string().min(2).max(63).regex(SLUG_REGEX, "target script must be lowercase letters, digits, and hyphens").optional(),
2637
- schedule: z.array(z.string().min(1).max(120)).min(1).max(20).optional(),
2638
- d1_binding: z.string().min(1).max(64).regex(ENV_NAME_REGEX, "d1_binding must be an UPPER_SNAKE_CASE binding name").optional()
2653
+ var TargetSchema = z2.object({
2654
+ kind: z2.enum(TARGET_KINDS),
2655
+ script: z2.string().min(2).max(63).regex(SLUG_REGEX, "target script must be lowercase letters, digits, and hyphens").optional(),
2656
+ schedule: z2.array(z2.string().min(1).max(120)).min(1).max(20).optional(),
2657
+ d1_binding: z2.string().min(1).max(64).regex(ENV_NAME_REGEX, "d1_binding must be an UPPER_SNAKE_CASE binding name").optional()
2639
2658
  }).strict();
2640
- var SecretBindingSchema = z.object({
2641
- name: z.string().min(1).max(64).regex(ENV_NAME_REGEX, "secret binding name must be UPPER_SNAKE_CASE"),
2642
- source: z.enum(SECRET_SOURCES),
2643
- description: z.string().max(200).optional(),
2644
- targets: z.union([z.literal("all"), z.array(z.string().min(1)).min(1)]).optional()
2659
+ var SecretBindingSchema = z2.object({
2660
+ name: z2.string().min(1).max(64).regex(ENV_NAME_REGEX, "secret binding name must be UPPER_SNAKE_CASE"),
2661
+ source: z2.enum(SECRET_SOURCES),
2662
+ description: z2.string().max(200).optional(),
2663
+ targets: z2.union([z2.literal("all"), z2.array(z2.string().min(1)).min(1)]).optional()
2645
2664
  }).strict();
2646
- var SecretsSchema = z.object({
2647
- bindings: z.array(SecretBindingSchema).optional()
2665
+ var SecretsSchema = z2.object({
2666
+ bindings: z2.array(SecretBindingSchema).optional()
2648
2667
  }).strict();
2649
- var BuildSchema = z.object({
2650
- command: z.string().min(1).optional(),
2651
- destination_dir: z.string().min(1),
2652
- root_dir: z.string()
2668
+ var BuildSchema = z2.object({
2669
+ command: z2.string().min(1).optional(),
2670
+ destination_dir: z2.string().min(1),
2671
+ root_dir: z2.string()
2653
2672
  }).strict();
2654
2673
  var STATIC_SERVE_DIR = "static";
2655
2674
  function staticNoBuild() {
2656
2675
  return { destination_dir: STATIC_SERVE_DIR, root_dir: "" };
2657
2676
  }
2658
- var AppBoundarySchema = z.object({
2659
- root: z.string().min(1).optional(),
2660
- include: z.array(z.string().min(1)).min(1).optional(),
2661
- exclude: z.array(z.string().min(1)).optional()
2677
+ var AppBoundarySchema = z2.object({
2678
+ root: z2.string().min(1).optional(),
2679
+ include: z2.array(z2.string().min(1)).min(1).optional(),
2680
+ exclude: z2.array(z2.string().min(1)).optional()
2662
2681
  }).strict();
2663
- var ProductionEnvSchema = z.record(z.string().regex(ENV_NAME_REGEX, "production_env keys must be UPPER_SNAKE_CASE"), z.string());
2664
- var VerifySchema = z.object({
2665
- protected_path: z.string().min(1).regex(/^\//, "verify.protected_path must be a root-relative path beginning with '/'")
2682
+ var ProductionEnvSchema = z2.record(z2.string().regex(ENV_NAME_REGEX, "production_env keys must be UPPER_SNAKE_CASE"), z2.string());
2683
+ var VerifySchema = z2.object({
2684
+ protected_path: z2.string().min(1).regex(/^\//, "verify.protected_path must be a root-relative path beginning with '/'")
2666
2685
  }).strict();
2667
- var ManifestSchema = z.object({
2668
- apiVersion: z.literal("launchpad.m-kopa.us/v1alpha1"),
2669
- kind: z.literal("App"),
2686
+ var ManifestSchema = z2.object({
2687
+ apiVersion: z2.literal("launchpad.m-kopa.us/v1alpha1"),
2688
+ kind: z2.literal("App"),
2670
2689
  metadata: MetadataSchema,
2671
2690
  deployment: DeploymentSchema,
2672
2691
  access: AccessSchema,
2673
- auth: z.enum(AUTH_MODES).optional(),
2674
- hostnames: z.array(z.string().min(1)).optional(),
2675
- containers: z.array(ContainerSchema).min(1).optional(),
2692
+ auth: z2.enum(AUTH_MODES).optional(),
2693
+ hostnames: z2.array(z2.string().min(1)).optional(),
2694
+ containers: z2.array(ContainerSchema).min(1).optional(),
2676
2695
  secrets: SecretsSchema.optional(),
2677
- targets: z.array(TargetSchema).optional(),
2696
+ targets: z2.array(TargetSchema).optional(),
2678
2697
  build: BuildSchema.optional(),
2679
2698
  production_env: ProductionEnvSchema.optional(),
2680
2699
  app: AppBoundarySchema.optional(),
2681
2700
  verify: VerifySchema.optional(),
2682
- confine_origin: z.boolean().optional(),
2683
- origin: z.enum(["pages", "dispatch"]).optional(),
2684
- catalogue: z.object({ listed: z.boolean().default(true) }).strict().optional(),
2685
- sensitive: z.boolean().optional()
2701
+ confine_origin: z2.boolean().optional(),
2702
+ origin: z2.enum(["pages", "dispatch"]).optional(),
2703
+ catalogue: z2.object({ listed: z2.boolean().default(true) }).strict().optional(),
2704
+ sensitive: z2.boolean().optional()
2686
2705
  }).strict().superRefine((m, ctx) => {
2687
2706
  const isContainer = m.deployment.type === "container";
2688
2707
  if (m.access.identity_types !== undefined && (m.auth ?? "access") !== "gateway") {
2689
2708
  ctx.addIssue({
2690
- code: z.ZodIssueCode.custom,
2709
+ code: z2.ZodIssueCode.custom,
2691
2710
  path: ["access", "identity_types"],
2692
2711
  message: "access.identity_types is only valid with auth: gateway"
2693
2712
  });
@@ -2695,19 +2714,19 @@ var ManifestSchema = z.object({
2695
2714
  if (m.confine_origin !== undefined) {
2696
2715
  if (isContainer) {
2697
2716
  ctx.addIssue({
2698
- code: z.ZodIssueCode.custom,
2717
+ code: z2.ZodIssueCode.custom,
2699
2718
  path: ["confine_origin"],
2700
2719
  message: "confine_origin is only valid for gateway pages apps (static/react/react+api); container apps have no pages.dev surface."
2701
2720
  });
2702
2721
  } else if (m.auth === "access") {
2703
2722
  ctx.addIssue({
2704
- code: z.ZodIssueCode.custom,
2723
+ code: z2.ZodIssueCode.custom,
2705
2724
  path: ["confine_origin"],
2706
2725
  message: "confine_origin applies to gateway-fronted apps (auth: gateway); it has no meaning under Cloudflare Access."
2707
2726
  });
2708
2727
  } else if (m.deployment.type === "static" && m.confine_origin === false) {
2709
2728
  ctx.addIssue({
2710
- code: z.ZodIssueCode.custom,
2729
+ code: z2.ZodIssueCode.custom,
2711
2730
  path: ["confine_origin"],
2712
2731
  message: "confine_origin: false is not allowed for static apps — they have no verifier, so an unconfined apex would be world-readable (ADR 0031)."
2713
2732
  });
@@ -2715,42 +2734,42 @@ var ManifestSchema = z.object({
2715
2734
  }
2716
2735
  if (isContainer && m.auth === "gateway") {
2717
2736
  ctx.addIssue({
2718
- code: z.ZodIssueCode.custom,
2737
+ code: z2.ZodIssueCode.custom,
2719
2738
  path: ["auth"],
2720
2739
  message: "auth: gateway is only supported for pages-app shapes (static/react/react+api); container apps stay on Cloudflare Access (ADR 0016)."
2721
2740
  });
2722
2741
  }
2723
2742
  if (isContainer && m.containers === undefined) {
2724
2743
  ctx.addIssue({
2725
- code: z.ZodIssueCode.custom,
2744
+ code: z2.ZodIssueCode.custom,
2726
2745
  path: ["containers"],
2727
2746
  message: "containers[] is required when deployment.type is 'container'"
2728
2747
  });
2729
2748
  }
2730
2749
  if (!isContainer && m.containers !== undefined) {
2731
2750
  ctx.addIssue({
2732
- code: z.ZodIssueCode.custom,
2751
+ code: z2.ZodIssueCode.custom,
2733
2752
  path: ["containers"],
2734
2753
  message: `containers[] is only allowed when deployment.type is 'container' (current type: '${m.deployment.type}')`
2735
2754
  });
2736
2755
  }
2737
2756
  if (!isContainer && m.deployment.runtime !== undefined) {
2738
2757
  ctx.addIssue({
2739
- code: z.ZodIssueCode.custom,
2758
+ code: z2.ZodIssueCode.custom,
2740
2759
  path: ["deployment", "runtime"],
2741
2760
  message: "deployment.runtime is only meaningful when deployment.type is 'container'"
2742
2761
  });
2743
2762
  }
2744
2763
  if (isContainer && m.build !== undefined) {
2745
2764
  ctx.addIssue({
2746
- code: z.ZodIssueCode.custom,
2765
+ code: z2.ZodIssueCode.custom,
2747
2766
  path: ["build"],
2748
2767
  message: "build is only allowed for pages-app deployments (static/react/react+api). Container builds live in the Dockerfile."
2749
2768
  });
2750
2769
  }
2751
2770
  if (isContainer && m.production_env !== undefined) {
2752
2771
  ctx.addIssue({
2753
- code: z.ZodIssueCode.custom,
2772
+ code: z2.ZodIssueCode.custom,
2754
2773
  path: ["production_env"],
2755
2774
  message: "production_env is only allowed for pages-app deployments. Container env vars use containers[].env (names only) + the bot's secrets pipeline for values."
2756
2775
  });
@@ -2759,7 +2778,7 @@ var ManifestSchema = z.object({
2759
2778
  const ingressContainers = m.containers.filter((c) => c.ingress === true);
2760
2779
  if (ingressContainers.length !== 1) {
2761
2780
  ctx.addIssue({
2762
- code: z.ZodIssueCode.custom,
2781
+ code: z2.ZodIssueCode.custom,
2763
2782
  path: ["containers"],
2764
2783
  message: `multi-container apps must mark exactly one container ingress: true (found ${ingressContainers.length})`
2765
2784
  });
@@ -2772,7 +2791,7 @@ var ManifestSchema = z.object({
2772
2791
  for (const envName of c.env ?? []) {
2773
2792
  if (!declaredBindings.has(envName)) {
2774
2793
  ctx.addIssue({
2775
- code: z.ZodIssueCode.custom,
2794
+ code: z2.ZodIssueCode.custom,
2776
2795
  path: ["containers", i, "env"],
2777
2796
  message: `env reference '${envName}' has no matching entry in secrets.bindings[]`
2778
2797
  });
@@ -2788,7 +2807,7 @@ var ManifestSchema = z.object({
2788
2807
  continue;
2789
2808
  if (seen.has(name)) {
2790
2809
  ctx.addIssue({
2791
- code: z.ZodIssueCode.custom,
2810
+ code: z2.ZodIssueCode.custom,
2792
2811
  path: ["containers", i, "name"],
2793
2812
  message: `duplicate container name '${name}' — names become DO class identifiers and must be unique`
2794
2813
  });
@@ -2805,7 +2824,7 @@ var ManifestSchema = z.object({
2805
2824
  continue;
2806
2825
  if (seen.has(name)) {
2807
2826
  ctx.addIssue({
2808
- code: z.ZodIssueCode.custom,
2827
+ code: z2.ZodIssueCode.custom,
2809
2828
  path: ["secrets", "bindings", i, "name"],
2810
2829
  message: `duplicate secret binding name '${name}' — names become env-var keys and must be unique`
2811
2830
  });
@@ -2818,21 +2837,21 @@ var ManifestSchema = z.object({
2818
2837
  const t = targets[i];
2819
2838
  if (t.kind === "worker" && t.script === undefined) {
2820
2839
  ctx.addIssue({
2821
- code: z.ZodIssueCode.custom,
2840
+ code: z2.ZodIssueCode.custom,
2822
2841
  path: ["targets", i, "script"],
2823
2842
  message: "a worker target must declare a script (the sibling Workers script name)"
2824
2843
  });
2825
2844
  }
2826
2845
  if (t.kind === "pages" && t.script !== undefined) {
2827
2846
  ctx.addIssue({
2828
- code: z.ZodIssueCode.custom,
2847
+ code: z2.ZodIssueCode.custom,
2829
2848
  path: ["targets", i, "script"],
2830
2849
  message: "a pages target must not declare a script — the primary Pages project is the app slug"
2831
2850
  });
2832
2851
  }
2833
2852
  if (t.kind === "pages" && t.schedule !== undefined) {
2834
2853
  ctx.addIssue({
2835
- code: z.ZodIssueCode.custom,
2854
+ code: z2.ZodIssueCode.custom,
2836
2855
  path: ["targets", i, "schedule"],
2837
2856
  message: "schedule is only valid on a worker target (the cron deploy contract — ADR 0022)"
2838
2857
  });
@@ -2846,7 +2865,7 @@ var ManifestSchema = z.object({
2846
2865
  pagesTargets++;
2847
2866
  if (pagesTargets > 1) {
2848
2867
  ctx.addIssue({
2849
- code: z.ZodIssueCode.custom,
2868
+ code: z2.ZodIssueCode.custom,
2850
2869
  path: ["targets", i, "kind"],
2851
2870
  message: "at most one pages target may be declared (the primary Pages project is singular)"
2852
2871
  });
@@ -2857,7 +2876,7 @@ var ManifestSchema = z.object({
2857
2876
  continue;
2858
2877
  if (t.script === PRIMARY_TARGET || t.script === "pages") {
2859
2878
  ctx.addIssue({
2860
- code: z.ZodIssueCode.custom,
2879
+ code: z2.ZodIssueCode.custom,
2861
2880
  path: ["targets", i, "script"],
2862
2881
  message: `worker target script '${t.script}' is a reserved selector token — use a different script name`
2863
2882
  });
@@ -2865,7 +2884,7 @@ var ManifestSchema = z.object({
2865
2884
  }
2866
2885
  if (seenScripts.has(t.script)) {
2867
2886
  ctx.addIssue({
2868
- code: z.ZodIssueCode.custom,
2887
+ code: z2.ZodIssueCode.custom,
2869
2888
  path: ["targets", i, "script"],
2870
2889
  message: `duplicate worker target script '${t.script}' — target scripts must be unique`
2871
2890
  });
@@ -2883,7 +2902,7 @@ var ManifestSchema = z.object({
2883
2902
  for (const ref of sel) {
2884
2903
  if (!validRefs.has(ref)) {
2885
2904
  ctx.addIssue({
2886
- code: z.ZodIssueCode.custom,
2905
+ code: z2.ZodIssueCode.custom,
2887
2906
  path: ["secrets", "bindings", bi, "targets"],
2888
2907
  message: `secret binding '${b?.name}' references unknown target '${ref}' — declare it in targets[] or use "primary"`
2889
2908
  });
@@ -3299,42 +3318,44 @@ var STATUS_STATUS_KEYS = new Set([
3299
3318
  "status.generated_at"
3300
3319
  ]);
3301
3320
  // ../launchpad-engine/dist/schema-v2.js
3302
- import { z as z2 } from "zod";
3303
- var EnvVarSchema = z2.discriminatedUnion("type", [
3304
- z2.object({ type: z2.literal("secret_text"), secret_ref: z2.string().min(1) }).strict(),
3305
- z2.object({ type: z2.literal("plain_text"), value: z2.string() }).strict()
3321
+ import { z as z3 } from "zod";
3322
+ var EnvVarSchema = z3.discriminatedUnion("type", [
3323
+ z3.object({ type: z3.literal("secret_text"), secret_ref: z3.string().min(1) }).strict(),
3324
+ z3.object({ type: z3.literal("plain_text"), value: z3.string() }).strict()
3306
3325
  ]);
3307
- var SpecSchema = z2.object({
3308
- appType: z2.enum(APP_TYPES2),
3309
- display: z2.object({
3310
- name: z2.string().min(1),
3311
- description: z2.string().max(280).optional(),
3312
- team: z2.string().min(1)
3326
+ var SpecSchema = z3.object({
3327
+ appType: z3.enum(APP_TYPES2),
3328
+ display: z3.object({
3329
+ name: z3.string().min(1),
3330
+ description: z3.string().max(280).optional(),
3331
+ team: z3.string().min(1)
3313
3332
  }).strict(),
3314
- owner: z2.object({ email: z2.string().email() }).strict(),
3315
- access: z2.object({
3316
- allowed_entra_groups: z2.array(z2.string().min(1)).min(1).refine((a) => new Set(a).size === a.length, {
3333
+ owner: z3.object({ email: z3.string().email() }).strict(),
3334
+ access: z3.object({
3335
+ audience: z3.literal("private").optional(),
3336
+ private_owner: PrivateOwnerSchema.optional(),
3337
+ allowed_entra_groups: z3.array(z3.string().min(1)).refine((a) => new Set(a).size === a.length, {
3317
3338
  message: "allowed_entra_groups must not contain duplicates"
3318
3339
  }),
3319
- require_email_domain: z2.string().min(1).optional(),
3340
+ require_email_domain: z3.string().min(1).optional(),
3320
3341
  identity_types: IdentityTypesSchema.optional()
3321
- }).strict(),
3322
- auth: z2.enum(AUTH_MODES).optional(),
3342
+ }).strict().superRefine(validateAudience),
3343
+ auth: z3.enum(AUTH_MODES).optional(),
3323
3344
  build: BuildSchema.optional(),
3324
3345
  app: AppBoundarySchema.optional(),
3325
3346
  verify: VerifySchema.optional(),
3326
- confine_origin: z2.boolean().optional(),
3327
- origin: z2.enum(["pages", "dispatch"]).optional(),
3328
- sensitive: z2.boolean().optional(),
3329
- catalogue: z2.object({ listed: z2.boolean().default(true) }).strict().optional(),
3330
- env_vars: z2.record(z2.string().regex(ENV_NAME_REGEX, "env_vars keys must be UPPER_SNAKE_CASE"), EnvVarSchema).optional(),
3331
- containers: z2.array(ContainerSchema).min(1).optional(),
3347
+ confine_origin: z3.boolean().optional(),
3348
+ origin: z3.enum(["pages", "dispatch"]).optional(),
3349
+ sensitive: z3.boolean().optional(),
3350
+ catalogue: z3.object({ listed: z3.boolean().default(true) }).strict().optional(),
3351
+ env_vars: z3.record(z3.string().regex(ENV_NAME_REGEX, "env_vars keys must be UPPER_SNAKE_CASE"), EnvVarSchema).optional(),
3352
+ containers: z3.array(ContainerSchema).min(1).optional(),
3332
3353
  secrets: SecretsSchema.optional(),
3333
- fronted_by_concierge: z2.boolean().optional()
3354
+ fronted_by_concierge: z3.boolean().optional()
3334
3355
  }).strict().superRefine((s, ctx) => {
3335
3356
  if (s.access.identity_types !== undefined && (s.auth ?? "access") !== "gateway") {
3336
3357
  ctx.addIssue({
3337
- code: z2.ZodIssueCode.custom,
3358
+ code: z3.ZodIssueCode.custom,
3338
3359
  path: ["access", "identity_types"],
3339
3360
  message: "access.identity_types is only valid with auth: gateway"
3340
3361
  });
@@ -3342,7 +3363,7 @@ var SpecSchema = z2.object({
3342
3363
  for (const [k, v] of Object.entries(s.env_vars ?? {})) {
3343
3364
  if (/_(TOKEN|SECRET)$/.test(k) && v.type !== "secret_text") {
3344
3365
  ctx.addIssue({
3345
- code: z2.ZodIssueCode.custom,
3366
+ code: z3.ZodIssueCode.custom,
3346
3367
  path: ["env_vars", k],
3347
3368
  message: `env var '${k}' is secret-bearing (*_TOKEN/*_SECRET) and must be type secret_text, not '${v.type}'`
3348
3369
  });
@@ -3351,7 +3372,7 @@ var SpecSchema = z2.object({
3351
3372
  const isContainer = s.appType === "container";
3352
3373
  if (isContainer && s.containers === undefined) {
3353
3374
  ctx.addIssue({
3354
- code: z2.ZodIssueCode.custom,
3375
+ code: z3.ZodIssueCode.custom,
3355
3376
  path: ["containers"],
3356
3377
  message: "containers[] is required when appType is 'container'"
3357
3378
  });
@@ -3359,19 +3380,19 @@ var SpecSchema = z2.object({
3359
3380
  if (s.confine_origin !== undefined) {
3360
3381
  if (isContainer) {
3361
3382
  ctx.addIssue({
3362
- code: z2.ZodIssueCode.custom,
3383
+ code: z3.ZodIssueCode.custom,
3363
3384
  path: ["confine_origin"],
3364
3385
  message: "confine_origin is only valid for gateway pages apps; container apps have no pages.dev surface."
3365
3386
  });
3366
3387
  } else if (s.auth === "access") {
3367
3388
  ctx.addIssue({
3368
- code: z2.ZodIssueCode.custom,
3389
+ code: z3.ZodIssueCode.custom,
3369
3390
  path: ["confine_origin"],
3370
3391
  message: "confine_origin applies to gateway-fronted apps (auth: gateway); it has no meaning under Cloudflare Access."
3371
3392
  });
3372
3393
  } else if (s.appType === "static" && s.confine_origin === false) {
3373
3394
  ctx.addIssue({
3374
- code: z2.ZodIssueCode.custom,
3395
+ code: z3.ZodIssueCode.custom,
3375
3396
  path: ["confine_origin"],
3376
3397
  message: "confine_origin: false is not allowed for static apps — an unconfined apex with no verifier is world-readable (ADR 0031)."
3377
3398
  });
@@ -3380,129 +3401,124 @@ var SpecSchema = z2.object({
3380
3401
  if (s.origin === "dispatch") {
3381
3402
  if (isContainer) {
3382
3403
  ctx.addIssue({
3383
- code: z2.ZodIssueCode.custom,
3404
+ code: z3.ZodIssueCode.custom,
3384
3405
  path: ["origin"],
3385
3406
  message: "origin: dispatch is not available for container apps — they run on the container runtime (ADR 0015/0016), not in a Workers dispatch namespace."
3386
3407
  });
3387
3408
  }
3388
3409
  if ((s.auth ?? "access") !== "gateway") {
3389
3410
  ctx.addIssue({
3390
- code: z2.ZodIssueCode.custom,
3411
+ code: z3.ZodIssueCode.custom,
3391
3412
  path: ["origin"],
3392
3413
  message: "origin: dispatch requires an explicit auth: gateway (auth defaults to 'access' when omitted). A dispatch-namespace Worker has no public hostname for Cloudflare Access to gate, so the gateway must be the sole gate."
3393
3414
  });
3394
3415
  }
3395
3416
  if (s.confine_origin !== undefined) {
3396
3417
  ctx.addIssue({
3397
- code: z2.ZodIssueCode.custom,
3418
+ code: z3.ZodIssueCode.custom,
3398
3419
  path: ["confine_origin"],
3399
3420
  message: "confine_origin has no meaning with origin: dispatch — there is no public pages.dev apex to confine. Remove it."
3400
3421
  });
3401
3422
  }
3402
- if (s.env_vars !== undefined) {
3403
- ctx.addIssue({
3404
- code: z2.ZodIssueCode.custom,
3405
- path: ["env_vars"],
3406
- message: "env_vars are not yet supported with origin: dispatch — a dispatch workspace has no Pages module to apply them, and accepting them here would silently drop them. Worker-level vars land with the v2 deploy path."
3407
- });
3408
- }
3409
- if (s.build !== undefined) {
3410
- ctx.addIssue({
3411
- code: z2.ZodIssueCode.custom,
3412
- path: ["build"],
3413
- message: "build is not yet supported with origin: dispatch — a dispatch workspace has no Pages module to carry it, and the v2 deploy path that will consume it is not shipped yet. Accepting it here would silently drop it."
3414
- });
3423
+ for (const [name, envVar] of Object.entries(s.env_vars ?? {})) {
3424
+ if (envVar.type === "secret_text") {
3425
+ ctx.addIssue({
3426
+ code: z3.ZodIssueCode.custom,
3427
+ path: ["env_vars", name],
3428
+ message: "secret_text env vars are not supported with origin: dispatch until PS-2740 (secrets on v2) provides their runtime consumer."
3429
+ });
3430
+ }
3415
3431
  }
3416
3432
  }
3417
3433
  if (!isContainer && s.containers !== undefined) {
3418
3434
  ctx.addIssue({
3419
- code: z2.ZodIssueCode.custom,
3435
+ code: z3.ZodIssueCode.custom,
3420
3436
  path: ["containers"],
3421
3437
  message: `containers[] is only allowed when appType is 'container' (current appType: '${s.appType}')`
3422
3438
  });
3423
3439
  }
3424
3440
  if (isContainer && s.build !== undefined) {
3425
3441
  ctx.addIssue({
3426
- code: z2.ZodIssueCode.custom,
3442
+ code: z3.ZodIssueCode.custom,
3427
3443
  path: ["build"],
3428
3444
  message: "build is not allowed when appType is 'container' (container build lives in the Dockerfile)"
3429
3445
  });
3430
3446
  }
3431
3447
  if (isContainer && s.auth === "gateway") {
3432
3448
  ctx.addIssue({
3433
- code: z2.ZodIssueCode.custom,
3449
+ code: z3.ZodIssueCode.custom,
3434
3450
  path: ["auth"],
3435
3451
  message: "auth: gateway is only supported for pages-app shapes (static/react/react+api); container apps stay on Cloudflare Access (ADR 0016)."
3436
3452
  });
3437
3453
  }
3438
3454
  if (!isContainer && s.fronted_by_concierge === true) {
3439
3455
  ctx.addIssue({
3440
- code: z2.ZodIssueCode.custom,
3456
+ code: z3.ZodIssueCode.custom,
3441
3457
  path: ["fronted_by_concierge"],
3442
3458
  message: `fronted_by_concierge is only allowed when appType is 'container' (current appType: '${s.appType}')`
3443
3459
  });
3444
3460
  }
3445
3461
  });
3446
- var nonEmpty = z2.string().min(1);
3447
- var StatusSchema = z2.object({
3448
- schemaVersion: z2.literal(2),
3462
+ var nonEmpty = z3.string().min(1);
3463
+ var StatusSchema = z3.object({
3464
+ schemaVersion: z3.literal(2),
3449
3465
  manifest_spec_sha: nonEmpty,
3450
- cloudflare: z2.object({
3451
- access: z2.object({
3466
+ cloudflare: z3.object({
3467
+ access: z3.object({
3452
3468
  app_id: nonEmpty,
3453
3469
  app_aud: nonEmpty,
3454
3470
  team_domain: nonEmpty,
3455
3471
  policy_id: nonEmpty
3456
3472
  }).strict(),
3457
- pages: z2.object({
3473
+ pages: z3.object({
3458
3474
  project_name: nonEmpty,
3459
3475
  subdomain: nonEmpty,
3460
3476
  latest_deployment_id: nonEmpty
3461
3477
  }).strict().optional(),
3462
- worker: z2.object({
3478
+ worker: z3.object({
3463
3479
  dev_url: nonEmpty,
3464
3480
  script_name: nonEmpty
3465
3481
  }).strict().optional()
3466
3482
  }).strict(),
3467
- dns: z2.object({ custom_hostname: nonEmpty, cname_target: nonEmpty }).strict().optional(),
3468
- terraform: z2.object({
3483
+ dns: z3.object({ custom_hostname: nonEmpty, cname_target: nonEmpty }).strict().optional(),
3484
+ terraform: z3.object({
3469
3485
  workspace_state_key: nonEmpty,
3470
3486
  last_applied_at: nonEmpty,
3471
3487
  last_applied_sha: nonEmpty
3472
3488
  }).strict(),
3473
- registry: z2.object({ record_present: z2.boolean(), lifecycle: nonEmpty }).strict(),
3489
+ registry: z3.object({ record_present: z3.boolean(), lifecycle: nonEmpty }).strict(),
3474
3490
  generated_at: nonEmpty
3475
3491
  }).strict().superRefine((s, ctx) => {
3476
3492
  const hasPages = s.cloudflare.pages !== undefined;
3477
3493
  const hasWorker = s.cloudflare.worker !== undefined;
3478
3494
  if (hasPages === hasWorker) {
3479
3495
  ctx.addIssue({
3480
- code: z2.ZodIssueCode.custom,
3496
+ code: z3.ZodIssueCode.custom,
3481
3497
  path: ["cloudflare"],
3482
3498
  message: "status.cloudflare must carry exactly one of `pages` (pages apps) or `worker` (container apps)"
3483
3499
  });
3484
3500
  }
3485
3501
  if (hasPages && s.dns === undefined) {
3486
3502
  ctx.addIssue({
3487
- code: z2.ZodIssueCode.custom,
3503
+ code: z3.ZodIssueCode.custom,
3488
3504
  path: ["dns"],
3489
3505
  message: "status.dns is required for a pages-shape status (cloudflare.pages present)"
3490
3506
  });
3491
3507
  }
3492
3508
  if (hasWorker && s.dns !== undefined) {
3493
3509
  ctx.addIssue({
3494
- code: z2.ZodIssueCode.custom,
3510
+ code: z3.ZodIssueCode.custom,
3495
3511
  path: ["dns"],
3496
3512
  message: "status.dns must be absent for a workers_dev_only container status (no custom hostname)"
3497
3513
  });
3498
3514
  }
3499
3515
  });
3500
- var ManifestV2Schema = z2.object({
3501
- apiVersion: z2.literal("launchpad.m-kopa.us/v2"),
3502
- kind: z2.literal("App"),
3503
- metadata: z2.object({
3504
- slug: z2.string().min(2).max(63).regex(SLUG_REGEX, "must be lowercase letters, digits, and hyphens; start/end alphanumeric"),
3505
- schemaVersion: z2.literal(2)
3516
+ var ManifestV2Schema = z3.object({
3517
+ apiVersion: z3.literal("launchpad.m-kopa.us/v2"),
3518
+ kind: z3.literal("App"),
3519
+ metadata: z3.object({
3520
+ slug: z3.string().min(2).max(63).regex(SLUG_REGEX, "must be lowercase letters, digits, and hyphens; start/end alphanumeric"),
3521
+ schemaVersion: z3.literal(2)
3506
3522
  }).strict(),
3507
3523
  spec: SpecSchema,
3508
3524
  status: StatusSchema.optional()
@@ -4232,6 +4248,11 @@ var BROAD_GROUPS = [
4232
4248
  uuid: "caf78902-35dd-4833-88ce-8f067eda6036",
4233
4249
  name: "S_ALL_Employees",
4234
4250
  note: "All employees — the broadest grant on the tenant; assigned to the Launchpad enterprise app. Granting this reaches the entire company."
4251
+ },
4252
+ {
4253
+ uuid: "09c87073-2071-4f8e-9e97-aa67f7a153a7",
4254
+ name: "S_ALL",
4255
+ note: "Broad tenant group. Assignment is not a reliable limit on gateway eligibility."
4235
4256
  }
4236
4257
  ];
4237
4258
  function assertBroadGroupsWellFormed(list = BROAD_GROUPS) {
@@ -4250,7 +4271,7 @@ function assertBroadGroupsWellFormed(list = BROAD_GROUPS) {
4250
4271
  assertBroadGroupsWellFormed();
4251
4272
  var BROAD_UUIDS = new Set(BROAD_GROUPS.map((g) => canonicalUuid(g.uuid)).filter((u) => u !== null));
4252
4273
  // ../launchpad-engine/dist/fleet-manifest.js
4253
- import { z as z3 } from "zod";
4274
+ import { z as z4 } from "zod";
4254
4275
  var UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
4255
4276
  var TF_RESIDENCIES = ["per-app-workspace", "main-tf"];
4256
4277
  var APP_SHAPES = [
@@ -4278,22 +4299,22 @@ var FLEET_TIERS = [
4278
4299
  "out-of-scope"
4279
4300
  ];
4280
4301
  var DEVICE_POSTURE = ["gateway-eligible", "retained-access"];
4281
- var GroupId = z3.string().regex(UUID_RE, "Entra group ids are emitted as canonical UUIDs in the JWT groups claim — display names will not match policies.");
4282
- var FleetAppSchema = z3.object({
4283
- slug: z3.string().regex(SLUG_REGEX),
4284
- residency: z3.enum(TF_RESIDENCIES),
4285
- shape: z3.enum(APP_SHAPES),
4286
- declaredAuth: z3.enum(DECLARED_AUTH),
4287
- actualAuth: z3.enum(ACTUAL_AUTH),
4288
- groups: z3.array(GroupId).default([]),
4289
- tier: z3.enum(FLEET_TIERS),
4290
- devicePosture: z3.enum(DEVICE_POSTURE),
4291
- devicePostureReason: z3.string().trim().min(1, "devicePosture is only valid with a non-empty reason — the two-tier determination must be a surveyed output, not a default (AC-T0-3)."),
4292
- notes: z3.string().optional()
4302
+ var GroupId = z4.string().regex(UUID_RE, "Entra group ids are emitted as canonical UUIDs in the JWT groups claim — display names will not match policies.");
4303
+ var FleetAppSchema = z4.object({
4304
+ slug: z4.string().regex(SLUG_REGEX),
4305
+ residency: z4.enum(TF_RESIDENCIES),
4306
+ shape: z4.enum(APP_SHAPES),
4307
+ declaredAuth: z4.enum(DECLARED_AUTH),
4308
+ actualAuth: z4.enum(ACTUAL_AUTH),
4309
+ groups: z4.array(GroupId).default([]),
4310
+ tier: z4.enum(FLEET_TIERS),
4311
+ devicePosture: z4.enum(DEVICE_POSTURE),
4312
+ devicePostureReason: z4.string().trim().min(1, "devicePosture is only valid with a non-empty reason — the two-tier determination must be a surveyed output, not a default (AC-T0-3)."),
4313
+ notes: z4.string().optional()
4293
4314
  }).strict();
4294
- var FleetManifestSchema = z3.object({
4295
- schemaVersion: z3.literal(1),
4296
- apps: z3.array(FleetAppSchema).min(1)
4315
+ var FleetManifestSchema = z4.object({
4316
+ schemaVersion: z4.literal(1),
4317
+ apps: z4.array(FleetAppSchema).min(1)
4297
4318
  }).strict().superRefine((m, ctx) => {
4298
4319
  const seen = new Set;
4299
4320
  for (const app of m.apps) {
@@ -4315,23 +4336,23 @@ var FleetManifestSchema = z3.object({
4315
4336
  }
4316
4337
  });
4317
4338
  // ../launchpad-engine/dist/fleet-secret-sets.js
4318
- import { z as z4 } from "zod";
4339
+ import { z as z5 } from "zod";
4319
4340
  var SET_NAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
4320
- var Slug = z4.string().regex(SLUG_REGEX, "member must be a lowercase-kebab app slug");
4321
- var SecretName = z4.string().regex(ENV_NAME_REGEX, "secret name must be UPPER_SNAKE_CASE");
4322
- var SecretSetExcludeSchema = z4.object({
4341
+ var Slug = z5.string().regex(SLUG_REGEX, "member must be a lowercase-kebab app slug");
4342
+ var SecretName = z5.string().regex(ENV_NAME_REGEX, "secret name must be UPPER_SNAKE_CASE");
4343
+ var SecretSetExcludeSchema = z5.object({
4323
4344
  member: Slug,
4324
4345
  secret: SecretName
4325
4346
  }).strict();
4326
- var SecretSetSchema = z4.object({
4327
- name: z4.string().min(1).max(64).regex(SET_NAME_REGEX, "set name must be lowercase-kebab"),
4328
- secrets: z4.array(SecretName).min(1),
4329
- members: z4.array(Slug).min(1),
4330
- exclude: z4.array(SecretSetExcludeSchema).optional()
4347
+ var SecretSetSchema = z5.object({
4348
+ name: z5.string().min(1).max(64).regex(SET_NAME_REGEX, "set name must be lowercase-kebab"),
4349
+ secrets: z5.array(SecretName).min(1),
4350
+ members: z5.array(Slug).min(1),
4351
+ exclude: z5.array(SecretSetExcludeSchema).optional()
4331
4352
  }).strict();
4332
- var FleetSecretSetsSchema = z4.object({
4333
- schemaVersion: z4.literal(1),
4334
- secretSets: z4.array(SecretSetSchema).min(1)
4353
+ var FleetSecretSetsSchema = z5.object({
4354
+ schemaVersion: z5.literal(1),
4355
+ secretSets: z5.array(SecretSetSchema).min(1)
4335
4356
  }).strict().superRefine((m, ctx) => {
4336
4357
  const seenSets = new Set;
4337
4358
  for (let i = 0;i < m.secretSets.length; i++) {
@@ -4403,6 +4424,21 @@ function membersForSecret(set, secret) {
4403
4424
  }
4404
4425
  // ../launchpad-engine/dist/redact-status.js
4405
4426
  var UNSAFE_KEY = new Set(["__proto__", "prototype", "constructor"]);
4427
+ // ../launchpad-engine/dist/deployment-auth.js
4428
+ function deploymentAuthIssue(appType, auth) {
4429
+ if (appType === "container" || auth === "gateway")
4430
+ return null;
4431
+ return {
4432
+ error: "pages_access_retired",
4433
+ message: "Cloudflare Access login is retired for Pages apps. Set auth: gateway (spec.auth in v2). Containers are unchanged."
4434
+ };
4435
+ }
4436
+ // ../launchpad-engine/dist/new-app-origin.js
4437
+ function newAppOrigin(appType, requested) {
4438
+ if (appType === "container")
4439
+ return;
4440
+ return requested ?? (appType === "react+api" ? "pages" : "dispatch");
4441
+ }
4406
4442
  // src/bundle/boundary.ts
4407
4443
  var MAX_CONTENT_PROBE_BYTES = 1024 * 1024;
4408
4444
  function applyBoundaryToFiles(args) {
@@ -4550,7 +4586,7 @@ async function bundleAndDeploy(args) {
4550
4586
  }
4551
4587
 
4552
4588
  // src/deploy/verify-deploy.ts
4553
- import { readFileSync as readFileSync9, existsSync as existsSync4 } from "node:fs";
4589
+ import { readFileSync as readFileSync10, existsSync as existsSync4 } from "node:fs";
4554
4590
  import { join as join9 } from "node:path";
4555
4591
 
4556
4592
  // src/deploy/redact-diagnostic.ts
@@ -4596,11 +4632,67 @@ async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
4596
4632
  return {
4597
4633
  slug: typeof raw.slug === "string" ? raw.slug : slug,
4598
4634
  supported: raw.supported,
4635
+ ...raw.origin === "pages" || raw.origin === "dispatch" ? { origin: raw.origin } : {},
4636
+ ...projectV2(raw.v2),
4599
4637
  liveDeployment: scrubLiveDeployment(raw.liveDeployment ?? null),
4600
4638
  lastSuccessfulDeployment: projectLastSuccessful(raw.lastSuccessfulDeployment),
4601
4639
  ...raw.reconciledToLive === true ? { reconciledToLive: true } : {}
4602
4640
  };
4603
4641
  }
4642
+ function projectV2(value) {
4643
+ if (value === null || typeof value !== "object")
4644
+ return {};
4645
+ const raw = value;
4646
+ if (!isV2State(raw.state) || !isV2Stage(raw.stage) || typeof raw.commitSha !== "string" || typeof raw.attempts !== "number" || !Number.isFinite(raw.attempts) || typeof raw.updatedAt !== "string") {
4647
+ return {};
4648
+ }
4649
+ if (!isConsistentV2State(raw.state, raw.stage))
4650
+ return {};
4651
+ if (raw.failedAt !== undefined && !isV2FailedStage(raw.failedAt))
4652
+ return {};
4653
+ const v2 = {
4654
+ state: raw.state,
4655
+ stage: raw.stage,
4656
+ commitSha: raw.commitSha,
4657
+ attempts: raw.attempts,
4658
+ updatedAt: raw.updatedAt,
4659
+ ...raw.failedAt !== undefined ? { failedAt: raw.failedAt } : {},
4660
+ ...typeof raw.reason === "string" ? { reason: redactDiagnostic(raw.reason) } : {},
4661
+ ...typeof raw.detail === "string" ? { detail: redactDiagnostic(raw.detail) } : {},
4662
+ ...projectRunUrl(raw.runUrl),
4663
+ ...typeof raw.deliveredAt === "string" ? { deliveredAt: raw.deliveredAt } : {}
4664
+ };
4665
+ return { v2 };
4666
+ }
4667
+ function projectRunUrl(value) {
4668
+ if (typeof value !== "string")
4669
+ return {};
4670
+ try {
4671
+ const url = new URL(value);
4672
+ return url.protocol === "https:" && url.hostname === "github.com" ? { runUrl: value } : {};
4673
+ } catch {
4674
+ return {};
4675
+ }
4676
+ }
4677
+ function isV2State(value) {
4678
+ return value === "pending" || value === "delivered" || value === "refused";
4679
+ }
4680
+ function isV2Stage(value) {
4681
+ return ["committed", "building", "verified", "uploading", "live", "failed"].includes(String(value));
4682
+ }
4683
+ function isConsistentV2State(state, stage) {
4684
+ switch (state) {
4685
+ case "delivered":
4686
+ return stage === "live";
4687
+ case "refused":
4688
+ return stage === "failed";
4689
+ case "pending":
4690
+ return stage !== "live" && stage !== "failed";
4691
+ }
4692
+ }
4693
+ function isV2FailedStage(value) {
4694
+ return ["build", "verify", "download", "expand", "manifest", "plan", "upload"].includes(String(value));
4695
+ }
4604
4696
  function projectLastSuccessful(value) {
4605
4697
  if (value === null || typeof value !== "object")
4606
4698
  return null;
@@ -4650,20 +4742,27 @@ function shaMatches(a, b) {
4650
4742
  return x === y;
4651
4743
  return x.slice(0, n) === y.slice(0, n);
4652
4744
  }
4653
- async function readDeploymentBaseline(deps, slug) {
4745
+ async function readDeploymentContext(deps, slug) {
4654
4746
  let status;
4655
4747
  try {
4656
4748
  status = await deps.fetchStatus(slug);
4657
4749
  } catch {
4658
- return null;
4750
+ return { baseline: null };
4751
+ }
4752
+ if (status === null)
4753
+ return { baseline: null };
4754
+ const origin = status.origin;
4755
+ if (!status.supported || status.liveDeployment === null) {
4756
+ return { baseline: null, ...origin !== undefined ? { origin } : {} };
4659
4757
  }
4660
- if (status === null || !status.supported || status.liveDeployment === null)
4661
- return null;
4662
4758
  const live = status.liveDeployment;
4663
4759
  return {
4664
- id: live.id,
4665
- commitHash: live.commit?.hash ?? null,
4666
- createdOn: live.createdOn
4760
+ baseline: {
4761
+ id: live.id,
4762
+ commitHash: live.commit?.hash ?? null,
4763
+ createdOn: live.createdOn
4764
+ },
4765
+ ...origin !== undefined ? { origin } : {}
4667
4766
  };
4668
4767
  }
4669
4768
  async function waitForCommitDeployment(args, deps) {
@@ -4671,6 +4770,7 @@ async function waitForCommitDeployment(args, deps) {
4671
4770
  const start = deps.now();
4672
4771
  const baselineId = args.baseline?.id ?? null;
4673
4772
  let matchedInProgress = false;
4773
+ let lastV2Stage = null;
4674
4774
  for (;; ) {
4675
4775
  let status;
4676
4776
  try {
@@ -4684,6 +4784,38 @@ async function waitForCommitDeployment(args, deps) {
4684
4784
  return { kind: "unsupported" };
4685
4785
  const live = status.liveDeployment;
4686
4786
  const elapsed = deps.now() - start;
4787
+ if (status.origin === "dispatch" && status.v2 !== undefined && shaMatches(status.v2.commitSha, args.shippedSha)) {
4788
+ const delivery = status.v2;
4789
+ if (delivery.stage !== lastV2Stage) {
4790
+ args.onV2Stage?.(delivery);
4791
+ lastV2Stage = delivery.stage;
4792
+ }
4793
+ if (delivery.state === "refused") {
4794
+ return {
4795
+ kind: "failure",
4796
+ dispatch: true,
4797
+ failedStage: delivery.failedAt ?? delivery.stage,
4798
+ reason: delivery.reason,
4799
+ detail: delivery.detail,
4800
+ runUrl: delivery.runUrl
4801
+ };
4802
+ }
4803
+ if (delivery.state === "delivered" || delivery.stage === "live") {
4804
+ return {
4805
+ kind: "success",
4806
+ deploymentUrl: live?.url ?? null,
4807
+ v2: {
4808
+ commitSha: delivery.commitSha,
4809
+ ...delivery.deliveredAt !== undefined ? { deliveredAt: delivery.deliveredAt } : {}
4810
+ }
4811
+ };
4812
+ }
4813
+ matchedInProgress = true;
4814
+ if (elapsed >= args.settleTimeoutMs)
4815
+ return { kind: "pending" };
4816
+ await deps.sleep(pollIntervalMs);
4817
+ continue;
4818
+ }
4687
4819
  if (live !== null && shaMatches(live.commit?.hash, args.shippedSha)) {
4688
4820
  if (live.buildStatus === "success") {
4689
4821
  return { kind: "success", deploymentUrl: live.url };
@@ -4795,7 +4927,7 @@ async function verifyServed(args, deps = { fetch }) {
4795
4927
  }
4796
4928
 
4797
4929
  // src/commands/status.ts
4798
- import { readFileSync as readFileSync8 } from "node:fs";
4930
+ import { readFileSync as readFileSync9 } from "node:fs";
4799
4931
 
4800
4932
  // src/deploy/manifest-state.ts
4801
4933
  async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
@@ -5604,6 +5736,88 @@ still provisioning after the watch window — re-run \`launchpad watch ${slug}\`
5604
5736
  }
5605
5737
  }
5606
5738
 
5739
+ // src/manifest/load.ts
5740
+ import { readFileSync as readFileSync8 } from "node:fs";
5741
+ import { parse as parseYaml4, YAMLParseError } from "yaml";
5742
+ function loadManifest(path8) {
5743
+ let raw;
5744
+ try {
5745
+ raw = readFileSync8(path8, "utf8");
5746
+ } catch (err) {
5747
+ const e = err;
5748
+ if (e.code === "ENOENT") {
5749
+ return { kind: "file-not-found", path: path8 };
5750
+ }
5751
+ return { kind: "read-error", path: path8, message: e.message ?? String(err) };
5752
+ }
5753
+ return parseManifest2(raw, path8);
5754
+ }
5755
+ function loadManifestAnyVersion(path8) {
5756
+ let raw;
5757
+ try {
5758
+ raw = readFileSync8(path8, "utf8");
5759
+ } catch (err) {
5760
+ const e = err;
5761
+ if (e.code === "ENOENT")
5762
+ return { kind: "file-not-found", path: path8 };
5763
+ return { kind: "read-error", path: path8, message: e.message ?? String(err) };
5764
+ }
5765
+ return parseManifestAnyVersion(raw, path8);
5766
+ }
5767
+ function parseManifestAnyVersion(yamlText, path8) {
5768
+ let parsed;
5769
+ try {
5770
+ parsed = parseYaml4(yamlText);
5771
+ } catch (err) {
5772
+ const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
5773
+ return { kind: "yaml-parse-error", path: path8, message };
5774
+ }
5775
+ const apiVersion = typeof parsed === "object" && parsed !== null ? parsed.apiVersion : undefined;
5776
+ if (apiVersion === "launchpad.m-kopa.us/v2") {
5777
+ const result = parseManifestV2(parsed);
5778
+ return result.kind === "ok" ? { kind: "ok", manifest: result.manifest, schema: apiVersion } : { kind: "schema-error", path: path8, schema: apiVersion, issues: [...result.issues] };
5779
+ }
5780
+ if (apiVersion === "launchpad.m-kopa.us/v1alpha1") {
5781
+ const result = parseManifest(parsed);
5782
+ return result.kind === "ok" ? { kind: "ok", manifest: result.manifest, schema: apiVersion } : { kind: "schema-error", path: path8, schema: apiVersion, issues: [...result.issues] };
5783
+ }
5784
+ return {
5785
+ kind: "schema-error",
5786
+ path: path8,
5787
+ schema: "launchpad.m-kopa.us/v1alpha1 or launchpad.m-kopa.us/v2",
5788
+ issues: [
5789
+ {
5790
+ path: "apiVersion",
5791
+ message: `unsupported apiVersion ${JSON.stringify(apiVersion)}; expected launchpad.m-kopa.us/v1alpha1 or launchpad.m-kopa.us/v2`
5792
+ }
5793
+ ]
5794
+ };
5795
+ }
5796
+ function parseManifest2(yamlText, path8) {
5797
+ let parsed;
5798
+ try {
5799
+ parsed = parseYaml4(yamlText);
5800
+ } catch (err) {
5801
+ const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
5802
+ return { kind: "yaml-parse-error", path: path8, message };
5803
+ }
5804
+ const result = ManifestSchema.safeParse(parsed);
5805
+ if (result.success) {
5806
+ return { kind: "ok", manifest: result.data };
5807
+ }
5808
+ return {
5809
+ kind: "schema-error",
5810
+ path: path8,
5811
+ issues: formatIssues(result.error)
5812
+ };
5813
+ }
5814
+ function formatIssues(error) {
5815
+ return error.issues.map((issue) => ({
5816
+ path: issue.path.map(String).join(".") || "(root)",
5817
+ message: issue.message
5818
+ }));
5819
+ }
5820
+
5607
5821
  // src/commands/status.ts
5608
5822
  var statusCommand = {
5609
5823
  name: "status",
@@ -5699,7 +5913,7 @@ async function runStatus(args, io) {
5699
5913
  }
5700
5914
  let localYaml = null;
5701
5915
  try {
5702
- localYaml = readFileSync8(parsed.file, "utf8");
5916
+ localYaml = readFileSync9(parsed.file, "utf8");
5703
5917
  } catch (e) {
5704
5918
  if (!isEnoent(e)) {
5705
5919
  io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe12(e)}`);
@@ -5708,24 +5922,24 @@ async function runStatus(args, io) {
5708
5922
  }
5709
5923
  let local = null;
5710
5924
  if (localYaml !== null) {
5711
- let localObj;
5712
- try {
5713
- const { parse: parseYaml4 } = await import("yaml");
5714
- localObj = parseYaml4(localYaml);
5715
- } catch (e) {
5716
- io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe12(e)}`);
5717
- return 2;
5718
- }
5719
- const localParse = parseManifest(localObj);
5925
+ const localParse = parseManifestAnyVersion(localYaml, parsed.file);
5720
5926
  if (localParse.kind !== "ok") {
5721
- io.err(`launchpad status: ${parsed.file} failed schema validation:`);
5722
- for (const issue of localParse.issues) {
5723
- io.err(` - ${issue.path}: ${issue.message}`);
5927
+ if (localParse.kind === "yaml-parse-error") {
5928
+ io.err(`launchpad status: ${parsed.file} is not valid YAML: ${localParse.message}`);
5929
+ } else if (localParse.kind === "schema-error") {
5930
+ io.err(`launchpad status: ${parsed.file} failed schema validation against ${localParse.schema}:`);
5931
+ for (const issue of localParse.issues) {
5932
+ io.err(` - ${issue.path}: ${issue.message}`);
5933
+ }
5934
+ } else {
5935
+ io.err(`launchpad status: cannot read local manifest at ${parsed.file}`);
5724
5936
  }
5725
5937
  return 2;
5726
5938
  }
5727
5939
  local = localParse.manifest;
5728
- warnSecretShape(local.production_env, io);
5940
+ if (local.apiVersion === "launchpad.m-kopa.us/v1alpha1") {
5941
+ warnSecretShape(local.production_env, io);
5942
+ }
5729
5943
  }
5730
5944
  let state;
5731
5945
  try {
@@ -5755,9 +5969,9 @@ async function runStatus(args, io) {
5755
5969
  }
5756
5970
  if (state.manifestYaml === null || state.manifestYaml === undefined) {
5757
5971
  const live = deployment?.liveDeployment ?? null;
5758
- const contentIsLive = live !== null && (live.buildStatus === "success" || deployment?.lastSuccessfulDeployment != null);
5972
+ const contentIsLive = deployment?.v2?.state === "delivered" || deployment?.v2?.stage === "live" || live !== null && (live.buildStatus === "success" || deployment?.lastSuccessfulDeployment != null);
5759
5973
  const liveButEmpty = lifecycle !== null && lifecycle.state === "live";
5760
- const platformTracked = state.hasAppFile === true || live?.trigger === "bot";
5974
+ const platformTracked = state.hasAppFile === true || live?.trigger === "bot" || deployment?.origin === "dispatch";
5761
5975
  const out = {
5762
5976
  state: contentIsLive ? platformTracked ? "live_drift_unknown" : "live_content_untracked" : platformTracked && !deploymentKnown ? "live_drift_unknown" : liveButEmpty ? "live_no_content" : "no_deployed_manifest",
5763
5977
  ...local === null ? { drift: null } : {},
@@ -5790,7 +6004,7 @@ async function runStatus(args, io) {
5790
6004
  };
5791
6005
  emit4(out, parsed.json, io);
5792
6006
  if (parsed.strict) {
5793
- if (deployment?.liveDeployment?.buildStatus === "failure") {
6007
+ if (deployment?.liveDeployment?.buildStatus === "failure" || deployment?.v2?.state === "refused") {
5794
6008
  io.err(`launchpad status: --strict: live build FAILED ` + `(drift not evaluated — no local launchpad.yaml here).`);
5795
6009
  return 1;
5796
6010
  }
@@ -5798,26 +6012,40 @@ async function runStatus(args, io) {
5798
6012
  }
5799
6013
  return 0;
5800
6014
  }
5801
- let deployedObj;
5802
- try {
5803
- const { parse: parseYaml4 } = await import("yaml");
5804
- deployedObj = parseYaml4(state.manifestYaml);
5805
- } catch (e) {
5806
- io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} is not valid YAML: ${describe12(e)}`);
5807
- return 2;
5808
- }
5809
- const deployedParse = parseManifest(deployedObj);
6015
+ const deployedParse = parseManifestAnyVersion(state.manifestYaml, `deployed manifest at ${state.deployedManifestSha}`);
5810
6016
  if (deployedParse.kind !== "ok") {
5811
- io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} failed schema validation:`);
5812
- for (const issue of deployedParse.issues) {
5813
- io.err(` - ${issue.path}: ${issue.message}`);
6017
+ if (deployedParse.kind === "yaml-parse-error") {
6018
+ io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} is not valid YAML: ${deployedParse.message}`);
6019
+ } else if (deployedParse.kind === "schema-error") {
6020
+ io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} failed schema validation against ${deployedParse.schema}:`);
6021
+ for (const issue of deployedParse.issues) {
6022
+ io.err(` - ${issue.path}: ${issue.message}`);
6023
+ }
5814
6024
  }
5815
6025
  return 2;
5816
6026
  }
5817
6027
  const deployed = deployedParse.manifest;
6028
+ if (local.apiVersion !== deployed.apiVersion) {
6029
+ const reason = `local manifest uses ${local.apiVersion}, while the deployed manifest uses ${deployed.apiVersion}; ` + "the manifest shapes differ, so drift was not evaluated";
6030
+ const out = {
6031
+ state: "live_drift_unknown",
6032
+ drift: null,
6033
+ driftUnavailableReason: reason,
6034
+ ...statusTruthFields({
6035
+ slug: parsed.slug,
6036
+ deployedSha: state.deployedManifestSha,
6037
+ headSha: state.appRepoHeadSha,
6038
+ openPrNumber: state.openPr?.number ?? null,
6039
+ ...deploymentKnown ? { deployment } : {},
6040
+ ...standingExceptions !== null ? { standingExceptions } : {}
6041
+ })
6042
+ };
6043
+ emit4(out, parsed.json, io);
6044
+ return 0;
6045
+ }
5818
6046
  const localGroups = await resolveStatusGroups({
5819
6047
  cfg,
5820
- refs: allowedEntraGroups(local.access),
6048
+ refs: allowedEntraGroups(manifestAccess(local)),
5821
6049
  source: "local manifest",
5822
6050
  manifestSha: null
5823
6051
  });
@@ -5826,14 +6054,17 @@ async function runStatus(args, io) {
5826
6054
  }
5827
6055
  const deployedGroups = await resolveStatusGroups({
5828
6056
  cfg,
5829
- refs: allowedEntraGroups(deployed.access),
6057
+ refs: allowedEntraGroups(manifestAccess(deployed)),
5830
6058
  source: "deployed manifest",
5831
6059
  manifestSha: state.deployedManifestSha
5832
6060
  });
5833
6061
  if (deployedGroups.kind === "error") {
5834
6062
  return renderStatusGroupError(deployedGroups.envelope, parsed.json, io);
5835
6063
  }
5836
- const drift = computeDrift(local, deployed, {
6064
+ const drift = local.apiVersion === "launchpad.m-kopa.us/v2" && deployed.apiVersion === "launchpad.m-kopa.us/v2" ? computeV2SpecDrift(local, deployed, {
6065
+ local: localGroups.ids,
6066
+ deployed: deployedGroups.ids
6067
+ }) : computeDrift(local, deployed, {
5837
6068
  local: localGroups.ids,
5838
6069
  deployed: deployedGroups.ids
5839
6070
  });
@@ -5850,8 +6081,8 @@ async function runStatus(args, io) {
5850
6081
  }
5851
6082
  accessDrift = detectCanonicalAccessDrift(localGroups.ids, enforcedGroups.ids);
5852
6083
  }
5853
- const localAssignmentGroups = (local.auth ?? "access") === "access" ? localGroups : null;
5854
- const deployedAssignmentGroups = (deployed.auth ?? "access") === "access" ? deployedGroups : null;
6084
+ const localAssignmentGroups = manifestAuth(local) === "access" ? localGroups : null;
6085
+ const deployedAssignmentGroups = manifestAuth(deployed) === "access" ? deployedGroups : null;
5855
6086
  const assignmentCheck = localAssignmentGroups === null && deployedAssignmentGroups === null ? { kind: "ok", issues: [] } : await findAssignmentIssues(cfg, localAssignmentGroups, deployedAssignmentGroups, state.deployedManifestSha);
5856
6087
  if (assignmentCheck.kind === "error") {
5857
6088
  return renderStatusGroupError(assignmentCheck.envelope, parsed.json, io);
@@ -6052,6 +6283,58 @@ function computeDrift(local, deployed, canonicalGroups) {
6052
6283
  }
6053
6284
  return diffs;
6054
6285
  }
6286
+ function computeV2SpecDrift(local, deployed, canonicalGroups) {
6287
+ const localSpec = {
6288
+ ...local.spec,
6289
+ access: {
6290
+ ...local.spec.access,
6291
+ allowed_entra_groups: [
6292
+ ...canonicalGroups?.local ?? local.spec.access.allowed_entra_groups
6293
+ ].sort()
6294
+ }
6295
+ };
6296
+ const deployedSpec = {
6297
+ ...deployed.spec,
6298
+ access: {
6299
+ ...deployed.spec.access,
6300
+ allowed_entra_groups: [
6301
+ ...canonicalGroups?.deployed ?? deployed.spec.access.allowed_entra_groups
6302
+ ].sort()
6303
+ }
6304
+ };
6305
+ const diffs = [];
6306
+ compareV2Value("spec", localSpec, deployedSpec, diffs);
6307
+ return diffs;
6308
+ }
6309
+ function compareV2Value(path8, local, deployed, diffs) {
6310
+ if (Object.is(local, deployed))
6311
+ return;
6312
+ if (Array.isArray(local) || Array.isArray(deployed)) {
6313
+ if (JSON.stringify(local) !== JSON.stringify(deployed)) {
6314
+ diffs.push({ path: path8, local, deployed });
6315
+ }
6316
+ return;
6317
+ }
6318
+ if (typeof local === "object" && local !== null && typeof deployed === "object" && deployed !== null) {
6319
+ const localRecord = local;
6320
+ const deployedRecord = deployed;
6321
+ const keys = new Set([
6322
+ ...Object.keys(localRecord),
6323
+ ...Object.keys(deployedRecord)
6324
+ ]);
6325
+ for (const key of [...keys].sort()) {
6326
+ compareV2Value(`${path8}.${key}`, localRecord[key], deployedRecord[key], diffs);
6327
+ }
6328
+ return;
6329
+ }
6330
+ diffs.push({ path: path8, local, deployed });
6331
+ }
6332
+ function manifestAccess(manifest) {
6333
+ return manifest.apiVersion === "launchpad.m-kopa.us/v2" ? manifest.spec.access : manifest.access;
6334
+ }
6335
+ function manifestAuth(manifest) {
6336
+ return manifest.apiVersion === "launchpad.m-kopa.us/v2" ? manifest.spec.auth ?? "access" : manifest.auth ?? "access";
6337
+ }
6055
6338
  function lifecycleOutput(slug, lc) {
6056
6339
  const state = lc.state === "failed" ? "provisioning_failed" : lc.state === "live" ? "in_sync" : lc.state;
6057
6340
  return {
@@ -6119,7 +6402,7 @@ function emit4(out, asJson, io) {
6119
6402
  surfaceExceptions(out, io);
6120
6403
  return;
6121
6404
  case "live_content_untracked":
6122
- io.out(`${out.slug}: live — content deployed via ` + `${triggerLabel(out.deployment?.liveDeployment?.trigger)} (no platform-tracked manifest; this app deploys outside \`launchpad deploy\`).`);
6405
+ io.out(`${out.slug}: live — content deployed via ` + `${triggerLabel(out.deployment?.liveDeployment?.trigger)} ` + `(no platform-tracked manifest; this app deploys outside \`launchpad deploy\`).`);
6123
6406
  surfaceNoLocalManifestNote(out, io);
6124
6407
  surfaceHeadVsDeployed(out, io);
6125
6408
  surfaceDeployment(out, io);
@@ -6181,6 +6464,10 @@ function surfaceGroupIssues(out, io) {
6181
6464
  function surfaceNoLocalManifestNote(out, io) {
6182
6465
  if (out.drift !== null)
6183
6466
  return;
6467
+ if (out.driftUnavailableReason !== undefined) {
6468
+ io.out(` ${out.driftUnavailableReason}.`);
6469
+ return;
6470
+ }
6184
6471
  io.out(" no local launchpad.yaml here — drift not checked " + "(cd into the app directory or pass --file to compare)");
6185
6472
  }
6186
6473
  function triggerLabel(trigger) {
@@ -6206,7 +6493,13 @@ function surfaceFailedLifecycleDeployment(out, io) {
6206
6493
  }
6207
6494
  function surfaceDeployment(out, io) {
6208
6495
  const dep = out.deployment;
6209
- if (dep === undefined || dep === null || !dep.supported)
6496
+ if (dep === undefined || dep === null)
6497
+ return;
6498
+ if (dep.origin === "dispatch") {
6499
+ surfaceV2Deployment(dep, io);
6500
+ return;
6501
+ }
6502
+ if (!dep.supported)
6210
6503
  return;
6211
6504
  if (dep.reconciledToLive === true) {
6212
6505
  io.out(" ✓ recovered: this app was in a failed state; its verified-live build reconciled the record failed→live (no `launchpad recover` needed).");
@@ -6246,6 +6539,29 @@ function surfaceDeployment(out, io) {
6246
6539
  }
6247
6540
  }
6248
6541
  }
6542
+ function surfaceV2Deployment(dep, io) {
6543
+ for (const line of renderV2DeploymentLines(dep))
6544
+ io.out(line);
6545
+ }
6546
+ function renderV2DeploymentLines(dep) {
6547
+ const delivery = dep.v2;
6548
+ if (delivery === undefined) {
6549
+ return [" v2 delivery: dispatch origin (delivery state unavailable)."];
6550
+ }
6551
+ const lines = [];
6552
+ const deliveredAt = delivery.state === "delivered" && delivery.deliveredAt !== undefined ? `, delivered ${delivery.deliveredAt}` : "";
6553
+ lines.push(` v2 delivery: state ${delivery.state}, stage ${delivery.stage}, commit ${delivery.commitSha.slice(0, 7)}${deliveredAt}`);
6554
+ if (delivery.state === "refused") {
6555
+ const failedAt = delivery.failedAt ?? delivery.stage;
6556
+ lines.push(` failed at ${failedAt}${delivery.reason ? `: ${delivery.reason}` : ""}`);
6557
+ if (delivery.runUrl)
6558
+ lines.push(` GitHub run: ${delivery.runUrl}`);
6559
+ }
6560
+ if (delivery.state !== "delivered" && dep.lastSuccessfulDeployment !== null) {
6561
+ lines.push(` serving: previous successful deployment from ${dep.lastSuccessfulDeployment.createdOn}`);
6562
+ }
6563
+ return lines;
6564
+ }
6249
6565
  function surfaceExceptions(out, io) {
6250
6566
  const exceptions = out.standingExceptions;
6251
6567
  if (exceptions === undefined || exceptions.length === 0)
@@ -6262,7 +6578,7 @@ function surfaceHeadVsDeployed(out, io) {
6262
6578
  if (out.hasOpenPr) {
6263
6579
  io.out(` (deployed sha ${short(out.deployedSha)} is behind app-repo HEAD ${short(out.headSha)};` + ` an apply may be queued — see PR #${out.openPrNumber})`);
6264
6580
  } else {
6265
- io.out(` (deployed sha ${short(out.deployedSha)} is behind app-repo HEAD ${short(out.headSha)}; main is ahead of deployed)`);
6581
+ io.out(` (deployed sha ${short(out.deployedSha)} is behind app-repo HEAD ${short(out.headSha)};` + ` main is ahead of deployed)`);
6266
6582
  }
6267
6583
  }
6268
6584
  }
@@ -6283,7 +6599,7 @@ function warnSecretShape(env, io) {
6283
6599
  const hasPrefix = prefixes.some((p) => v.startsWith(p));
6284
6600
  const longBase64Shape = v.length >= 32 && /^[A-Za-z0-9+/=_-]+$/.test(v);
6285
6601
  if (hasPrefix && longBase64Shape) {
6286
- io.err(`launchpad status: production_env.${k} looks like a secret. production_env is non-secret by contract; move secrets to the secrets bindings path. (This is informational; status will continue.)`);
6602
+ io.err(`launchpad status: production_env.${k} looks like a secret. ` + `production_env is non-secret by contract; move secrets to the ` + `secrets bindings path. (This is informational; status will continue.)`);
6287
6603
  }
6288
6604
  }
6289
6605
  }
@@ -6342,7 +6658,7 @@ function parseArgs2(args, cwd = process.cwd(), warn) {
6342
6658
  if (slug === null) {
6343
6659
  const inferred = inferSlug({ cwd, file, warn });
6344
6660
  if (inferred === null) {
6345
- return `slug not provided + cannot infer from cwd. Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
6661
+ return `slug not provided + cannot infer from cwd. ` + `Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
6346
6662
  }
6347
6663
  slug = inferred;
6348
6664
  }
@@ -6401,7 +6717,7 @@ function readLocalEntrypoint(cwd) {
6401
6717
  for (const rel of ["index.html", join9("static", "index.html")]) {
6402
6718
  const p = join9(cwd, rel);
6403
6719
  if (existsSync4(p))
6404
- return new Uint8Array(readFileSync9(p));
6720
+ return new Uint8Array(readFileSync10(p));
6405
6721
  }
6406
6722
  return null;
6407
6723
  }
@@ -6416,13 +6732,25 @@ function realVerifyDeployDeps(cfg) {
6416
6732
  }
6417
6733
  async function runDeployVerification(args, io, deps) {
6418
6734
  io.out("");
6419
- io.out("Verifying a build of your commit went live …");
6735
+ if (args.dispatchOrigin === true) {
6736
+ io.out("Waiting for your repo build and dispatch delivery …");
6737
+ } else {
6738
+ io.out("Verifying a build of your commit went live …");
6739
+ }
6740
+ const seenV2Stages = new Set;
6741
+ const onV2Stage = (delivery) => {
6742
+ if (seenV2Stages.has(delivery.stage))
6743
+ return;
6744
+ seenV2Stages.add(delivery.stage);
6745
+ io.out(` v2 stage: ${v2StageLabel(delivery.stage)}`);
6746
+ };
6420
6747
  const settle = await deps.wait({
6421
6748
  slug: args.slug,
6422
6749
  shippedSha: args.shippedSha,
6423
6750
  baseline: args.baseline,
6424
6751
  registrationBoundMs: args.registrationBoundMs,
6425
- settleTimeoutMs: args.timeoutMs
6752
+ settleTimeoutMs: args.timeoutMs,
6753
+ onV2Stage
6426
6754
  }, deps.waitDeps);
6427
6755
  switch (settle.kind) {
6428
6756
  case "unsupported":
@@ -6451,10 +6779,25 @@ async function runDeployVerification(args, io, deps) {
6451
6779
  io.out(` It may still be queued. Re-check the outcome: \`launchpad status ${args.slug}\``);
6452
6780
  return 0;
6453
6781
  case "pending":
6782
+ if (args.dispatchOrigin === true || seenV2Stages.size > 0) {
6783
+ io.out(`⏳ Dispatch delivery is still in progress after ${Math.round(args.timeoutMs / 1000)}s - couldn't verify yet.`);
6784
+ io.out(` Re-check the outcome: \`launchpad status ${args.slug}\``);
6785
+ return 0;
6786
+ }
6454
6787
  io.out(`⏳ Your commit's build is still in progress after ${Math.round(args.timeoutMs / 1000)}s — couldn't verify yet.`);
6455
6788
  io.out(` Re-check the outcome: \`launchpad status ${args.slug}\``);
6456
6789
  return 0;
6457
6790
  case "failure":
6791
+ if (settle.dispatch === true) {
6792
+ io.err(`✗ Dispatch delivery REFUSED at ${settle.failedStage ?? "unknown"} - your commit is NOT live.`);
6793
+ if (settle.reason)
6794
+ io.err(` reason: ${redactDiagnostic(settle.reason)}`);
6795
+ if (settle.detail)
6796
+ io.err(` detail: ${redactDiagnostic(settle.detail)}`);
6797
+ if (settle.runUrl)
6798
+ io.err(` GitHub run: ${settle.runUrl}`);
6799
+ return 1;
6800
+ }
6458
6801
  io.err("✗ The Cloudflare Pages build for your commit FAILED — your content is NOT live.");
6459
6802
  if (settle.failedStage)
6460
6803
  io.err(` failed stage: ${redactDiagnostic(settle.failedStage)}`);
@@ -6466,9 +6809,30 @@ async function runDeployVerification(args, io, deps) {
6466
6809
  io.err(` Full logs: \`launchpad status ${args.slug}\``);
6467
6810
  return 1;
6468
6811
  case "success":
6812
+ if (settle.v2 !== undefined) {
6813
+ const deliveredAt = settle.v2.deliveredAt ? ` at ${settle.v2.deliveredAt}` : "";
6814
+ io.out(`✓ Live in dispatch namespace - delivered commit ${settle.v2.commitSha}${deliveredAt}.`);
6815
+ return 0;
6816
+ }
6469
6817
  return verifySuccess(args, settle.deploymentUrl, io, deps);
6470
6818
  }
6471
6819
  }
6820
+ function v2StageLabel(stage) {
6821
+ switch (stage) {
6822
+ case "committed":
6823
+ return "source committed";
6824
+ case "building":
6825
+ return "CI building";
6826
+ case "verified":
6827
+ return "run verified";
6828
+ case "uploading":
6829
+ return "artifact uploading";
6830
+ case "live":
6831
+ return "live in dispatch namespace";
6832
+ case "failed":
6833
+ return "failed";
6834
+ }
6835
+ }
6472
6836
  function realRedeployVerifyDeps(cfg) {
6473
6837
  return {
6474
6838
  wait: waitForDeployment,
@@ -6695,7 +7059,7 @@ function runGit2(sp, cwd, args) {
6695
7059
  }
6696
7060
 
6697
7061
  // src/deploy/manifest-diff.ts
6698
- import { parse as parseYaml4 } from "yaml";
7062
+ import { parse as parseYaml5 } from "yaml";
6699
7063
  var MASK = "«redacted»";
6700
7064
  var SECRET_KEY_SEGMENT = /(secret|token|api[_-]?key|access[_-]?key|private[_-]?key|secret[_-]?key|password|passwd|pwd|authorization|auth[_-]?token|bearer|client[_-]?secret|session|credential|value)/i;
6701
7065
  function isSecretPath(path8) {
@@ -6786,7 +7150,7 @@ function diffManifests(localYaml, deployedYaml) {
6786
7150
  let local;
6787
7151
  let deployed;
6788
7152
  try {
6789
- local = normalize(parseYaml4(localYaml));
7153
+ local = parseYaml5(localYaml);
6790
7154
  } catch (e) {
6791
7155
  return {
6792
7156
  inSync: false,
@@ -6795,7 +7159,7 @@ function diffManifests(localYaml, deployedYaml) {
6795
7159
  };
6796
7160
  }
6797
7161
  try {
6798
- deployed = normalize(parseYaml4(deployedYaml));
7162
+ deployed = parseYaml5(deployedYaml);
6799
7163
  } catch (e) {
6800
7164
  return {
6801
7165
  inSync: false,
@@ -6803,6 +7167,22 @@ function diffManifests(localYaml, deployedYaml) {
6803
7167
  parseError: `deployed manifest did not parse: ${e.message}`
6804
7168
  };
6805
7169
  }
7170
+ const localVersion = manifestApiVersion(local);
7171
+ const deployedVersion = manifestApiVersion(deployed);
7172
+ if (localVersion !== deployedVersion && (isV2(localVersion) || isV2(deployedVersion))) {
7173
+ return {
7174
+ inSync: false,
7175
+ changes: [],
7176
+ parseError: null,
7177
+ shapeMismatch: `local uses ${localVersion ?? "an unknown schema"}; deployed uses ${deployedVersion ?? "an unknown schema"}`
7178
+ };
7179
+ }
7180
+ if (isV2(localVersion) && isV2(deployedVersion)) {
7181
+ local = { spec: manifestSpec(local) };
7182
+ deployed = { spec: manifestSpec(deployed) };
7183
+ }
7184
+ local = normalize(local);
7185
+ deployed = normalize(deployed);
6806
7186
  if (stableStringify(local) === stableStringify(deployed)) {
6807
7187
  return { inSync: true, changes: [], parseError: null };
6808
7188
  }
@@ -6849,6 +7229,18 @@ function diffManifests(localYaml, deployedYaml) {
6849
7229
  }
6850
7230
  return { inSync: false, changes, parseError: null };
6851
7231
  }
7232
+ function manifestApiVersion(value) {
7233
+ if (typeof value !== "object" || value === null)
7234
+ return null;
7235
+ const apiVersion = value.apiVersion;
7236
+ return typeof apiVersion === "string" ? apiVersion : null;
7237
+ }
7238
+ function isV2(apiVersion) {
7239
+ return apiVersion === "launchpad.m-kopa.us/v2";
7240
+ }
7241
+ function manifestSpec(value) {
7242
+ return typeof value === "object" && value !== null ? value.spec : undefined;
7243
+ }
6852
7244
  function renderManifestDiffLines(diff, max = 20) {
6853
7245
  const lines = [];
6854
7246
  for (const c of diff.changes.slice(0, max)) {
@@ -7256,9 +7648,6 @@ function parseDeployFlags(args) {
7256
7648
  if (appType === null) {
7257
7649
  return `--new requires --app-type (one of ${APP_TYPES.join(", ")})`;
7258
7650
  }
7259
- if (allowedGroups.length === 0) {
7260
- return "--new requires at least one --allowed-group";
7261
- }
7262
7651
  return {
7263
7652
  mode: {
7264
7653
  kind: "new",
@@ -7277,47 +7666,6 @@ function parseDeployFlags(args) {
7277
7666
  import { existsSync as existsSync6, rmSync } from "node:fs";
7278
7667
  import { resolve as resolvePath, join as join10 } from "node:path";
7279
7668
 
7280
- // src/manifest/load.ts
7281
- import { readFileSync as readFileSync10 } from "node:fs";
7282
- import { parse as parseYaml5, YAMLParseError } from "yaml";
7283
- function loadManifest(path8) {
7284
- let raw;
7285
- try {
7286
- raw = readFileSync10(path8, "utf8");
7287
- } catch (err) {
7288
- const e = err;
7289
- if (e.code === "ENOENT") {
7290
- return { kind: "file-not-found", path: path8 };
7291
- }
7292
- return { kind: "read-error", path: path8, message: e.message ?? String(err) };
7293
- }
7294
- return parseManifest2(raw, path8);
7295
- }
7296
- function parseManifest2(yamlText, path8) {
7297
- let parsed;
7298
- try {
7299
- parsed = parseYaml5(yamlText);
7300
- } catch (err) {
7301
- const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
7302
- return { kind: "yaml-parse-error", path: path8, message };
7303
- }
7304
- const result = ManifestSchema.safeParse(parsed);
7305
- if (result.success) {
7306
- return { kind: "ok", manifest: result.data };
7307
- }
7308
- return {
7309
- kind: "schema-error",
7310
- path: path8,
7311
- issues: formatIssues(result.error)
7312
- };
7313
- }
7314
- function formatIssues(error) {
7315
- return error.issues.map((issue) => ({
7316
- path: issue.path.map(String).join(".") || "(root)",
7317
- message: issue.message
7318
- }));
7319
- }
7320
-
7321
7669
  // src/status-page-output.ts
7322
7670
  async function surfaceStatusPageUrl(url, io, options) {
7323
7671
  if (typeof url !== "string" || url.length === 0)
@@ -7392,13 +7740,13 @@ async function runDeployApply(opts, io, deps = {}) {
7392
7740
  return 0;
7393
7741
  }
7394
7742
  const manifestPath = resolvePath(process.cwd(), opts.file ?? "launchpad.yaml");
7395
- const loaded = loadManifest(manifestPath);
7743
+ const loaded = loadManifestAnyVersion(manifestPath);
7396
7744
  if (loaded.kind !== "ok") {
7397
7745
  return renderManifestError(loaded, io);
7398
7746
  }
7399
- const slug = loaded.manifest.metadata.name;
7747
+ const slug = manifestSlug(loaded);
7400
7748
  if (!SLUG_REGEX2.test(slug)) {
7401
- io.err(`launchpad deploy --apply: manifest's metadata.name '${slug}' is not a valid slug`);
7749
+ io.err(`launchpad deploy --apply: manifest's ${loaded.schema === "launchpad.m-kopa.us/v2" ? "metadata.slug" : "metadata.name"} '${slug}' is not a valid slug`);
7402
7750
  return 1;
7403
7751
  }
7404
7752
  if (opts.atSha !== undefined && !MANIFEST_SHA_REGEX.test(opts.atSha)) {
@@ -7689,12 +8037,12 @@ function deletePinIfPresent(cfg, slug, io) {
7689
8037
  }
7690
8038
  async function loadSlugForResume(opts, io) {
7691
8039
  const manifestPath = resolvePath(process.cwd(), opts.file ?? "launchpad.yaml");
7692
- const loaded = loadManifest(manifestPath);
8040
+ const loaded = loadManifestAnyVersion(manifestPath);
7693
8041
  if (loaded.kind !== "ok") {
7694
8042
  renderManifestError(loaded, io);
7695
8043
  return null;
7696
8044
  }
7697
- return loaded.manifest.metadata.name;
8045
+ return manifestSlug(loaded);
7698
8046
  }
7699
8047
  async function defaultPrompt(question) {
7700
8048
  const { createInterface } = await import("node:readline/promises");
@@ -7735,6 +8083,10 @@ function mapHttpError(e, slug, io) {
7735
8083
  io.err(`launchpad deploy --apply failed: ${describe14(e)}`);
7736
8084
  return 2;
7737
8085
  }
8086
+ function manifestSlug(loaded) {
8087
+ const { metadata } = loaded.manifest;
8088
+ return "slug" in metadata ? metadata.slug : metadata.name;
8089
+ }
7738
8090
  function renderManifestError(loaded, io) {
7739
8091
  switch (loaded.kind) {
7740
8092
  case "ok":
@@ -8630,6 +8982,10 @@ async function surfaceManifestDiff(cfg, slug, manifestPath, io) {
8630
8982
  io.out(` config diff: skipped (${diff.parseError}).`);
8631
8983
  return;
8632
8984
  }
8985
+ if (diff.shapeMismatch !== undefined) {
8986
+ io.out(` config diff: manifest shapes differ (${diff.shapeMismatch}); drift was not evaluated.`);
8987
+ return;
8988
+ }
8633
8989
  if (diff.inSync) {
8634
8990
  io.out(" config: launchpad.yaml matches the deployed manifest (in sync).");
8635
8991
  return;
@@ -8653,6 +9009,7 @@ async function runModelADeploy(args) {
8653
9009
  const { cwd, manifestPath, io } = args;
8654
9010
  let slug;
8655
9011
  let appType = "static";
9012
+ let declaredOrigin;
8656
9013
  try {
8657
9014
  const manifestObj = parseYaml6(readFileSync11(manifestPath, "utf8"));
8658
9015
  const metaSlug = resolveManifestSlug(manifestObj);
@@ -8662,13 +9019,22 @@ async function runModelADeploy(args) {
8662
9019
  }
8663
9020
  slug = metaSlug;
8664
9021
  const raw = manifestObj;
8665
- const isV2 = raw?.apiVersion === "launchpad.m-kopa.us/v2";
8666
- const deploymentType = isV2 ? raw?.spec?.appType : raw?.deployment?.type;
9022
+ const isV22 = raw?.apiVersion === "launchpad.m-kopa.us/v2";
9023
+ declaredOrigin = declaredOriginOf(isV22, raw?.spec?.origin);
9024
+ const deploymentType = isV22 ? raw?.spec?.appType : raw?.deployment?.type;
8667
9025
  if (deploymentType === "static" || deploymentType === "react" || deploymentType === "react+api" || deploymentType === "container") {
8668
9026
  appType = deploymentType;
8669
9027
  }
9028
+ if (appType !== undefined) {
9029
+ const auth = isV22 ? raw?.spec?.auth : raw?.auth;
9030
+ const issue = deploymentAuthIssue(appType, auth === "gateway" ? "gateway" : auth === "access" ? "access" : undefined);
9031
+ if (issue !== null) {
9032
+ io.err(`launchpad deploy: ${issue.message}`);
9033
+ return 1;
9034
+ }
9035
+ }
8670
9036
  if (deploymentType === "container") {
8671
- const parsed = isV2 ? parseManifestV2(manifestObj) : parseManifest(manifestObj);
9037
+ const parsed = isV22 ? parseManifestV2(manifestObj) : parseManifest(manifestObj);
8672
9038
  if (parsed.kind !== "ok") {
8673
9039
  io.err("launchpad deploy: manifest is structurally invalid:");
8674
9040
  for (const issue of parsed.issues) {
@@ -8720,7 +9086,8 @@ async function runModelADeploy(args) {
8720
9086
  try {
8721
9087
  await surfaceManifestDiff(cfg, slug, manifestPath, io);
8722
9088
  } catch {}
8723
- const deployBaseline = noVerify ? null : await readDeploymentBaseline(realCommitWaitDeps(cfg), slug);
9089
+ const deployContext = await readDeploymentContext(realCommitWaitDeps(cfg), slug);
9090
+ const deployBaseline = noVerify ? null : deployContext.baseline;
8724
9091
  let result;
8725
9092
  try {
8726
9093
  if (args.allowUnguarded) {
@@ -8791,6 +9158,11 @@ async function runModelADeploy(args) {
8791
9158
  allowedHost: cfg.statusPageHost
8792
9159
  });
8793
9160
  const errorCode = body.error;
9161
+ if (errorCode === "audience_confirmation_required") {
9162
+ io.err(String(body.message));
9163
+ io.err("Only the human builder may confirm. Retry this deployment after confirmation.");
9164
+ return 1;
9165
+ }
8794
9166
  if (result.status === 426 && typeof body.minimum === "string" && typeof body.yours === "string") {
8795
9167
  io.err(upgradeRequiredMessage(body.minimum, body.yours));
8796
9168
  return EXIT_CLI_UPGRADE_REQUIRED;
@@ -8850,7 +9222,11 @@ async function runModelADeploy(args) {
8850
9222
  if (accessExit !== 0)
8851
9223
  return accessExit;
8852
9224
  io.out("");
8853
- io.out("Committed; build pending - Cloudflare Pages is building this deploy now.");
9225
+ const dispatchOrigin = handoffIsDispatch({
9226
+ declared: declaredOrigin,
9227
+ baseline: deployContext.origin
9228
+ });
9229
+ surfaceCommittedHandoff(dispatchOrigin, io, "ascii");
8854
9230
  if (noVerify)
8855
9231
  return 0;
8856
9232
  return runDeployVerification({
@@ -8860,7 +9236,8 @@ async function runModelADeploy(args) {
8860
9236
  shippedSha: outcome.manifest_sha,
8861
9237
  baseline: deployBaseline,
8862
9238
  registrationBoundMs,
8863
- timeoutMs: verifyTimeoutMs
9239
+ timeoutMs: verifyTimeoutMs,
9240
+ dispatchOrigin
8864
9241
  }, io, realVerifyDeployDeps(cfg));
8865
9242
  }
8866
9243
  io.err(`launchpad deploy: ${formatErrorEnvelope(body)}`);
@@ -8956,9 +9333,18 @@ async function runModelADeploy(args) {
8956
9333
  }
8957
9334
  surfaceDeployExtras(success, io, slug, args.allowStale);
8958
9335
  io.out("");
8959
- io.out("Committed; build pending — Cloudflare Pages is building this deploy now.");
9336
+ const dispatchOrigin = handoffIsDispatch({
9337
+ v2Delivery: success.v2Delivery,
9338
+ declared: declaredOrigin,
9339
+ baseline: deployContext.origin
9340
+ });
9341
+ surfaceCommittedHandoff(dispatchOrigin, io);
8960
9342
  if (noVerify) {
8961
- io.out(`Run \`launchpad status ${slug}\` to confirm the build outcome (success / failure + log excerpt).`);
9343
+ if (dispatchOrigin) {
9344
+ io.out(`Run \`launchpad status ${slug}\` to confirm the delivery stage and outcome.`);
9345
+ } else {
9346
+ io.out(`Run \`launchpad status ${slug}\` to confirm the build outcome (success / failure + log excerpt).`);
9347
+ }
8962
9348
  return 0;
8963
9349
  }
8964
9350
  return runDeployVerification({
@@ -8968,11 +9354,33 @@ async function runModelADeploy(args) {
8968
9354
  shippedSha: success.commit_sha,
8969
9355
  baseline: deployBaseline,
8970
9356
  registrationBoundMs,
8971
- timeoutMs: verifyTimeoutMs
9357
+ timeoutMs: verifyTimeoutMs,
9358
+ dispatchOrigin
8972
9359
  }, io, realVerifyDeployDeps(cfg));
8973
9360
  }
8974
9361
  }
8975
9362
  }
9363
+ function surfaceCommittedHandoff(dispatchOrigin, io, pagesStyle = "unicode") {
9364
+ io.out(committedHandoffMessage(dispatchOrigin, pagesStyle));
9365
+ }
9366
+ function declaredOriginOf(isV22, origin) {
9367
+ if (!isV22)
9368
+ return "pages";
9369
+ return origin === "dispatch" ? "dispatch" : "pages";
9370
+ }
9371
+ function handoffIsDispatch(input) {
9372
+ if (input.v2Delivery === "pending" || input.v2Delivery === "deferred")
9373
+ return true;
9374
+ if (input.declared !== undefined)
9375
+ return input.declared === "dispatch";
9376
+ return input.baseline === "dispatch";
9377
+ }
9378
+ function committedHandoffMessage(dispatchOrigin, pagesStyle = "unicode") {
9379
+ if (dispatchOrigin) {
9380
+ return "Committed; your repo's build workflow is building this commit, then the bot delivers it to the dispatch namespace.";
9381
+ }
9382
+ return pagesStyle === "ascii" ? "Committed; build pending - Cloudflare Pages is building this deploy now." : "Committed; build pending — Cloudflare Pages is building this deploy now.";
9383
+ }
8976
9384
  function pendingReconcileOutcome(body) {
8977
9385
  const manifestSha = body.manifestSha;
8978
9386
  const id = body.reconcileId;
@@ -10364,7 +10772,7 @@ var initCommand = {
10364
10772
  summary: "scaffold a new launchpad.yaml in the current directory",
10365
10773
  run: (args, io) => runInit(args, io, defaultPrompt2)
10366
10774
  };
10367
- async function runInit(args, io, prompt) {
10775
+ async function runInit(args, io, prompt, identity = () => apiJson(loadConfig(), { path: "/me/identity" })) {
10368
10776
  const parsed = parseArgs6(args);
10369
10777
  if (parsed.kind === "usage-error") {
10370
10778
  io.err(`launchpad init: ${parsed.message}`);
@@ -10400,8 +10808,39 @@ async function runInit(args, io, prompt) {
10400
10808
  io.err(`launchpad init: ${fillResult.message}`);
10401
10809
  return 64;
10402
10810
  }
10403
- const manifest = buildManifest(fillResult.inputs, detected);
10404
- const validation = ManifestSchema.safeParse(manifest);
10811
+ const authIssue = deploymentAuthIssue(fillResult.inputs.type, fillResult.inputs.auth ?? "gateway");
10812
+ if (authIssue !== null) {
10813
+ io.err(`launchpad init: ${authIssue.message}`);
10814
+ return 64;
10815
+ }
10816
+ if (fillResult.inputs.type === "container" && inputs.origin !== undefined) {
10817
+ io.err("launchpad init: --origin is not supported for containers");
10818
+ return 64;
10819
+ }
10820
+ const dispatch = newAppOrigin(fillResult.inputs.type, inputs.origin) === "dispatch";
10821
+ if (dispatch && fillResult.inputs.type === "react+api") {
10822
+ io.err("launchpad init: API starter dispatch is not available yet; use --origin pages");
10823
+ return 64;
10824
+ }
10825
+ if (dispatch && (inputs.hostnames.length > 0 || inputs.sessionDuration !== undefined)) {
10826
+ io.err("launchpad init: dispatch does not support --hostname or --session-duration; use --origin pages for a Pages manifest");
10827
+ return 64;
10828
+ }
10829
+ let privateOwner;
10830
+ if (fillResult.inputs.groups.length === 0) {
10831
+ try {
10832
+ privateOwner = await identity();
10833
+ } catch (error) {
10834
+ io.err(`launchpad init: cannot verify your identity for a private app: ${error instanceof Error ? error.message : String(error)}`);
10835
+ return 1;
10836
+ }
10837
+ io.out(`Audience: private. Only you (${privateOwner.email}) and platform administrators can open this app.`);
10838
+ } else {
10839
+ io.out(`Audience: selected Entra groups (${fillResult.inputs.groups.join(", ")}), plus platform administrators.`);
10840
+ io.out("Company-wide grants require your explicit browser confirmation before publishing.");
10841
+ }
10842
+ const manifest = buildManifest({ ...fillResult.inputs, ...privateOwner === undefined ? {} : { privateOwner } }, detected);
10843
+ const validation = manifest.apiVersion === "launchpad.m-kopa.us/v2" ? ManifestV2Schema.safeParse(manifest) : ManifestSchema.safeParse(manifest);
10405
10844
  if (!validation.success) {
10406
10845
  io.err("launchpad init: invalid manifest input");
10407
10846
  for (const issue of validation.error.issues) {
@@ -10454,8 +10893,11 @@ async function runInit(args, io, prompt) {
10454
10893
  io.out("");
10455
10894
  io.out("Next steps:");
10456
10895
  io.out(" launchpad validate # confirm the manifest");
10457
- io.out(" launchpad secrets template # scaffold .env.example");
10458
- io.out(" launchpad deploy # ship it (Scope 6, pending)");
10896
+ if (!dispatch)
10897
+ io.out(" launchpad secrets template # scaffold .env.example");
10898
+ io.out(" launchpad deploy # build and deploy the app");
10899
+ if (dispatch)
10900
+ io.out(" Dispatch currently supports plain-text environment variables, but not secret bindings.");
10459
10901
  return 0;
10460
10902
  }
10461
10903
  function parseArgs6(args) {
@@ -10466,6 +10908,7 @@ function parseArgs6(args) {
10466
10908
  description: undefined,
10467
10909
  type: undefined,
10468
10910
  auth: undefined,
10911
+ origin: undefined,
10469
10912
  groups: [],
10470
10913
  sessionDuration: undefined,
10471
10914
  hostnames: [],
@@ -10550,6 +10993,16 @@ function parseArgs6(args) {
10550
10993
  inputs.type = got;
10551
10994
  continue;
10552
10995
  }
10996
+ if (a === "--origin") {
10997
+ const got = needsValue();
10998
+ if (typeof got !== "string")
10999
+ return got;
11000
+ if (got !== "pages" && got !== "dispatch") {
11001
+ return { kind: "usage-error", message: "--origin must be pages or dispatch" };
11002
+ }
11003
+ inputs.origin = got;
11004
+ continue;
11005
+ }
10553
11006
  if (a === "--auth") {
10554
11007
  const got = needsValue();
10555
11008
  if (typeof got !== "string")
@@ -10686,18 +11139,7 @@ async function fillRequired(inputs, options, io, prompt) {
10686
11139
  io.err(` must be one of: ${APP_TYPES2.join(", ")}`);
10687
11140
  }
10688
11141
  }
10689
- let groupsR;
10690
- if (inputs.groups.length > 0) {
10691
- groupsR = inputs.groups;
10692
- } else {
10693
- const raw = await askIfMissing(undefined, "allowed_entra_groups (one or more, comma-separated)", (v) => v.length >= 1 ? undefined : "must not be empty");
10694
- if (typeof raw !== "string")
10695
- return raw;
10696
- groupsR = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
10697
- if (groupsR.length === 0) {
10698
- return { kind: "usage-error", message: "at least one allowed Entra group is required" };
10699
- }
10700
- }
11142
+ const groupsR = inputs.groups;
10701
11143
  let containerImage = inputs.containerImage;
10702
11144
  let containerPort = inputs.containerPort;
10703
11145
  if (typeR === "container") {
@@ -10744,6 +11186,7 @@ async function fillRequired(inputs, options, io, prompt) {
10744
11186
  description: inputs.description,
10745
11187
  type: typeR,
10746
11188
  auth: inputs.auth,
11189
+ origin: inputs.origin,
10747
11190
  groups: groupsR,
10748
11191
  sessionDuration: inputs.sessionDuration,
10749
11192
  hostnames: inputs.hostnames,
@@ -10762,7 +11205,7 @@ function buildManifest(inputs, detected) {
10762
11205
  metadata.description = inputs.description;
10763
11206
  }
10764
11207
  const deployment = { type: inputs.type };
10765
- const access = inputs.groups.length === 1 ? { allowed_entra_group: inputs.groups[0] } : { allowed_entra_groups: [...inputs.groups] };
11208
+ const access = inputs.privateOwner !== undefined ? { audience: "private", private_owner: inputs.privateOwner, allowed_entra_groups: [] } : inputs.groups.length === 1 ? { allowed_entra_group: inputs.groups[0] } : { allowed_entra_groups: [...inputs.groups] };
10766
11209
  if (inputs.sessionDuration !== undefined && inputs.sessionDuration !== "") {
10767
11210
  access.session_duration = inputs.sessionDuration;
10768
11211
  }
@@ -10771,6 +11214,7 @@ function buildManifest(inputs, detected) {
10771
11214
  kind: "App",
10772
11215
  metadata,
10773
11216
  deployment,
11217
+ catalogue: { listed: false },
10774
11218
  access
10775
11219
  };
10776
11220
  const resolvedAuth = inputs.auth ?? (inputs.type === "container" ? "access" : "gateway");
@@ -10803,6 +11247,33 @@ function buildManifest(inputs, detected) {
10803
11247
  root: ".",
10804
11248
  include: [...defaultIncludeForAppType(inputs.type)]
10805
11249
  };
11250
+ if (newAppOrigin(inputs.type, inputs.origin) === "dispatch") {
11251
+ return {
11252
+ apiVersion: "launchpad.m-kopa.us/v2",
11253
+ kind: "App",
11254
+ metadata: { slug: inputs.name, schemaVersion: 2 },
11255
+ spec: {
11256
+ appType: inputs.type,
11257
+ origin: "dispatch",
11258
+ auth: resolvedAuth,
11259
+ display: {
11260
+ name: inputs.name,
11261
+ team: inputs.team,
11262
+ ...inputs.description ? { description: inputs.description } : {}
11263
+ },
11264
+ owner: { email: inputs.owner },
11265
+ access: {
11266
+ ...inputs.privateOwner === undefined ? {} : { audience: "private", private_owner: inputs.privateOwner },
11267
+ allowed_entra_groups: [...inputs.groups]
11268
+ },
11269
+ catalogue: { listed: false },
11270
+ app: manifest.app,
11271
+ ...manifest.build === undefined ? {} : { build: manifest.build }
11272
+ }
11273
+ };
11274
+ }
11275
+ if (inputs.origin === "pages")
11276
+ manifest.origin = "pages";
10806
11277
  return manifest;
10807
11278
  }
10808
11279
  function starterIndexHtml(slug) {
@@ -13242,11 +13713,15 @@ async function runSecretsPush(opts, io, deps = {}) {
13242
13713
  const cfg = deps.config ?? loadConfig();
13243
13714
  const fetcher = deps.fetcher ?? fetch;
13244
13715
  const manifestPath = resolvePath4(process.cwd(), opts.file ?? "launchpad.yaml");
13245
- const loaded = loadManifest(manifestPath);
13716
+ const loaded = loadManifestAnyVersion(manifestPath);
13246
13717
  if (loaded.kind !== "ok") {
13247
13718
  return renderManifestError4(loaded, io);
13248
13719
  }
13249
- const bindings = loaded.manifest.secrets?.bindings ?? [];
13720
+ const v2Refusal = refuseV2Manifest(loaded, "launchpad secrets push", io);
13721
+ if (v2Refusal !== null)
13722
+ return v2Refusal;
13723
+ const manifest = loaded.manifest;
13724
+ const bindings = manifest.secrets?.bindings ?? [];
13250
13725
  const envFileBindings = bindings.filter((b) => b.source === "env-file");
13251
13726
  if (envFileBindings.length === 0) {
13252
13727
  io.out("No `env-file` secret bindings declared in the manifest.");
@@ -13328,7 +13803,7 @@ async function runSecretsPush(opts, io, deps = {}) {
13328
13803
  io.out("Re-run without --dry-run to push.");
13329
13804
  return 0;
13330
13805
  }
13331
- const slug = loaded.manifest.metadata.name;
13806
+ const slug = manifest.metadata.name;
13332
13807
  let pushed = 0;
13333
13808
  let partial = 0;
13334
13809
  let anyPagesUpdated = false;
@@ -13453,6 +13928,20 @@ function mapPushError(e, slug, name, io) {
13453
13928
  function describe30(e) {
13454
13929
  return e instanceof Error ? e.message : String(e);
13455
13930
  }
13931
+ function refuseV2Manifest(loaded, command, io) {
13932
+ if (loaded.schema !== "launchpad.m-kopa.us/v2")
13933
+ return null;
13934
+ const origin = loaded.manifest.spec?.origin;
13935
+ if (origin === "dispatch") {
13936
+ io.err(`${command}: this is a dispatch-origin (v2) app.`);
13937
+ const outcome = command.endsWith("push") ? "nothing was pushed" : "nothing was read";
13938
+ io.err(` Secrets are not delivered to the dispatch namespace yet (PS-2740); ${outcome}.`);
13939
+ return 1;
13940
+ }
13941
+ io.err(`${command}: this is a launchpad.m-kopa.us/v2 manifest.`);
13942
+ io.err(" This command reads v1alpha1 `secrets.bindings`; v2 declares secrets as `spec.env_vars` `secret_text` entries, which it does not push yet.");
13943
+ return 1;
13944
+ }
13456
13945
  function renderManifestError4(result, io) {
13457
13946
  switch (result.kind) {
13458
13947
  case "ok":
@@ -13490,16 +13979,20 @@ async function runSecretsStatus(opts, io, deps = {}) {
13490
13979
  const cfg = deps.config ?? loadConfig();
13491
13980
  const fetcher = deps.fetcher ?? fetch;
13492
13981
  const manifestPath = resolvePath5(process.cwd(), opts.file ?? "launchpad.yaml");
13493
- const loaded = loadManifest(manifestPath);
13982
+ const loaded = loadManifestAnyVersion(manifestPath);
13494
13983
  if (loaded.kind !== "ok")
13495
13984
  return renderManifestError5(loaded, io);
13496
- const slug = loaded.manifest.metadata.name;
13985
+ const v2Refusal = refuseV2Manifest(loaded, "launchpad secrets status", io);
13986
+ if (v2Refusal !== null)
13987
+ return v2Refusal;
13988
+ const manifest = loaded.manifest;
13989
+ const slug = manifest.metadata.name;
13497
13990
  if (opts.slug != null && opts.slug !== slug) {
13498
13991
  io.err(`launchpad secrets status: slug "${opts.slug}" does not match this directory's ` + `manifest (metadata.name: "${slug}").`);
13499
13992
  io.err(" secrets status audits the manifest in the current directory — omit the slug, " + "or run from the app's directory.");
13500
13993
  return 2;
13501
13994
  }
13502
- const bindings = loaded.manifest.secrets?.bindings ?? [];
13995
+ const bindings = manifest.secrets?.bindings ?? [];
13503
13996
  if (bindings.length === 0) {
13504
13997
  io.out("No secret bindings declared in the manifest.");
13505
13998
  return 0;
@@ -14867,7 +15360,7 @@ import { existsSync as existsSync16, readFileSync as readFileSync21, readdirSync
14867
15360
  import { dirname as dirname12, resolve as resolve17 } from "node:path";
14868
15361
  var validateCommand = {
14869
15362
  name: "validate",
14870
- summary: "validate launchpad.yaml against the v1alpha1 schema",
15363
+ summary: "validate launchpad.yaml against its v1alpha1 or v2 schema",
14871
15364
  run: runValidate
14872
15365
  };
14873
15366
  async function runValidate(args, io) {
@@ -14879,7 +15372,7 @@ async function runValidate(args, io) {
14879
15372
  }
14880
15373
  const { file, json, strictGroups } = parseResult;
14881
15374
  const path14 = resolve17(process.cwd(), file ?? "launchpad.yaml");
14882
- const result = loadManifest(path14);
15375
+ const result = loadManifestAnyVersion(path14);
14883
15376
  if (result.kind !== "ok") {
14884
15377
  return json ? renderJsonError(result, io) : renderHumanError(result, io);
14885
15378
  }
@@ -14889,16 +15382,17 @@ async function runValidate(args, io) {
14889
15382
  manifest: result.manifest,
14890
15383
  io,
14891
15384
  json,
14892
- structurallyValidName: result.manifest.metadata.name
15385
+ structurallyValidName: manifestView(result.manifest).name
14893
15386
  });
14894
15387
  if (unsupported2 !== null)
14895
15388
  return unsupported2;
14896
- const boundary = checkBoundary(path14, result.manifest.app !== undefined);
15389
+ const view = manifestView(result.manifest);
15390
+ const boundary = checkBoundary(path14, view.appDeclared);
14897
15391
  const serveErrors = checkStaticServeDir(path14, result.manifest);
14898
15392
  if (serveErrors.length > 0) {
14899
15393
  boundary.errors = [...boundary.errors, ...serveErrors];
14900
15394
  }
14901
- const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(result.manifest.access)) : { kind: "skipped" };
15395
+ const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(view.access)) : { kind: "skipped" };
14902
15396
  return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
14903
15397
  }
14904
15398
  function checkBoundary(manifestPath, declared) {
@@ -14922,11 +15416,12 @@ function checkBoundary(manifestPath, declared) {
14922
15416
  return { declared, errors: [], warnings: r.warnings };
14923
15417
  }
14924
15418
  function checkStaticServeDir(manifestPath, manifest) {
14925
- if (manifest.deployment.type !== "static")
15419
+ const view = manifestView(manifest);
15420
+ if (view.deploymentType !== "static")
14926
15421
  return [];
14927
- if (manifest.build?.command !== undefined)
15422
+ if (view.buildCommand !== undefined)
14928
15423
  return [];
14929
- const serveDir = manifest.build?.destination_dir ?? STATIC_SERVE_DIR;
15424
+ const serveDir = view.destinationDir ?? STATIC_SERVE_DIR;
14930
15425
  const normalized = serveDir.replace(/\\/g, "/");
14931
15426
  const segments = normalized.split("/");
14932
15427
  if (normalized === "" || normalized === "." || normalized === "./" || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized) || segments.some((s) => s === "..")) {
@@ -15026,9 +15521,10 @@ async function checkGroups(groups) {
15026
15521
  return ok ? { kind: "all-ok", perGroup } : { kind: "some-failed", perGroup };
15027
15522
  }
15028
15523
  function renderHumanOk(result, group, boundary, io) {
15029
- io.out(`✓ ${result.manifest.metadata.name} — manifest is valid`);
15030
- io.out(` apiVersion: launchpad.m-kopa.us/v1alpha1`);
15031
- io.out(` type: ${result.manifest.deployment.type}`);
15524
+ const view = manifestView(result.manifest);
15525
+ io.out(`✓ ${view.name} — manifest is valid`);
15526
+ io.out(` apiVersion: ${result.schema}`);
15527
+ io.out(` type: ${view.deploymentType}`);
15032
15528
  if (boundary.errors.length > 0) {
15033
15529
  for (const e of boundary.errors) {
15034
15530
  io.err(`✗ app boundary: ${e}`);
@@ -15097,6 +15593,7 @@ function renderHumanError(result, io) {
15097
15593
  return 1;
15098
15594
  case "schema-error":
15099
15595
  io.err(`✗ ${result.path}`);
15596
+ io.err(` validated against schema: ${result.schema}`);
15100
15597
  io.err(` ${result.issues.length} schema issue${result.issues.length === 1 ? "" : "s"}:`);
15101
15598
  for (const issue of result.issues) {
15102
15599
  io.err(` ${issue.path}: ${issue.message}`);
@@ -15105,11 +15602,12 @@ function renderHumanError(result, io) {
15105
15602
  }
15106
15603
  }
15107
15604
  function renderJsonOk(result, group, boundary, io) {
15605
+ const view = manifestView(result.manifest);
15108
15606
  const base = {
15109
15607
  manifest_valid: true,
15110
15608
  runtime_supported: true,
15111
- name: result.manifest.metadata.name,
15112
- deploymentType: result.manifest.deployment.type,
15609
+ name: view.name,
15610
+ deploymentType: view.deploymentType,
15113
15611
  appBoundary: {
15114
15612
  declared: boundary.declared,
15115
15613
  errors: boundary.errors,
@@ -15189,11 +15687,32 @@ function renderJsonError(result, io) {
15189
15687
  ok: false,
15190
15688
  kind: "schema-error",
15191
15689
  path: result.path,
15690
+ schema: result.schema,
15192
15691
  issues: result.issues
15193
15692
  }));
15194
15693
  return 1;
15195
15694
  }
15196
15695
  }
15696
+ function manifestView(manifest) {
15697
+ if (manifest.apiVersion === "launchpad.m-kopa.us/v2") {
15698
+ return {
15699
+ name: manifest.metadata.slug,
15700
+ deploymentType: manifest.spec.appType,
15701
+ access: manifest.spec.access,
15702
+ buildCommand: manifest.spec.build?.command,
15703
+ destinationDir: manifest.spec.build?.destination_dir,
15704
+ appDeclared: manifest.spec.app !== undefined
15705
+ };
15706
+ }
15707
+ return {
15708
+ name: manifest.metadata.name,
15709
+ deploymentType: manifest.deployment.type,
15710
+ access: manifest.access,
15711
+ buildCommand: manifest.build?.command,
15712
+ destinationDir: manifest.build?.destination_dir,
15713
+ appDeclared: manifest.app !== undefined
15714
+ };
15715
+ }
15197
15716
  function parseArgs13(args) {
15198
15717
  let file;
15199
15718
  let json = false;