@m-kopa/launchpad-cli 0.60.1 → 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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,19 @@ pre-1.0 minor bumps may carry breaking changes per ADR 0005.
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 0.61.0 - 2026-09-17
12
+
13
+ ### Changed
14
+
15
+ - New apps start private to their verified creator and platform administrators, and are unlisted by default.
16
+ - Company-wide grants require explicit human confirmation before publication.
17
+ Sensitive apps also require administrator approval.
18
+ - Deployment skills explain the audience and hand confirmation to the builder.
19
+ Existing audiences and unchanged content deployments retain their behaviour.
20
+ - The platform minimum supported CLI version is 0.60.1 for deployments and other supported mutating commands.
21
+ Clients reporting stable versions below 0.60.1 receive an upgrade-required error directing users to `launchpad update`.
22
+ Existing apps and read-only commands remain available.
23
+
11
24
  ## 0.60.1 - 2026-09-17
12
25
 
13
26
  ### Changed
package/README.md CHANGED
@@ -107,3 +107,13 @@ than a hand-maintained second copy.
107
107
  Pre-1.0 consumer-pinning policy (per ADR 0005): consumers must pin
108
108
  exact versions until v1.0.0. Caret ranges are forbidden. Each
109
109
  minor-version bump may carry breaking changes.
110
+
111
+ ## Default app access
112
+
113
+ New apps are private to their verified creator and platform administrators, and are unlisted by default.
114
+ Omit `--group` when running `launchpad init` to keep that audience.
115
+ Selecting groups grants access to members of any selected group.
116
+ A new company-wide grant requires the builder's explicit browser confirmation before publishing; sensitive apps also require platform-admin approval.
117
+ Agents must never submit that confirmation for the human.
118
+ Catalogue listing changes discovery, not access.
119
+ See [App access](../../docs/app-access.md) for the manifest and deployment contract.
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.60.1";
22
+ var CLI_VERSION = "0.61.0";
23
23
 
24
24
  // src/config.ts
25
25
  import { readFileSync } from "node:fs";
@@ -2570,11 +2570,23 @@ import { join as join6 } from "node:path";
2570
2570
  import { parse as parseYaml2 } from "yaml";
2571
2571
 
2572
2572
  // ../launchpad-engine/dist/schema.js
2573
+ import { z as z2 } from "zod";
2574
+
2575
+ // ../launchpad-engine/dist/audience.js
2573
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
2574
2586
  var APP_TYPES2 = ["static", "react", "react+api", "container"];
2575
2587
  var AUTH_MODES = ["access", "gateway"];
2576
2588
  var IDENTITY_TYPES = ["employee", "dsr"];
2577
- 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, {
2578
2590
  message: "identity_types must not contain duplicates"
2579
2591
  }).refine((values) => values.includes("employee"), {
2580
2592
  message: "identity_types must include employee"
@@ -2586,111 +2598,115 @@ var PRIMARY_TARGET = "primary";
2586
2598
  var SLUG_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
2587
2599
  var ENV_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/;
2588
2600
  var SESSION_DURATION_REGEX = /^[0-9]+(s|m|h)$/;
2589
- var MetadataSchema = z.object({
2590
- name: z.string().min(2).max(63).regex(SLUG_REGEX, "must be lowercase letters, digits, and hyphens; start/end alphanumeric"),
2591
- team: z.string().min(1),
2592
- owner: z.string().email(),
2593
- 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()
2594
2606
  }).strict();
2595
- var DeploymentSchema = z.object({
2596
- type: z.enum(APP_TYPES2),
2597
- runtime: z.enum(RUNTIMES).optional()
2607
+ var DeploymentSchema = z2.object({
2608
+ type: z2.enum(APP_TYPES2),
2609
+ runtime: z2.enum(RUNTIMES).optional()
2598
2610
  }).strict();
2599
- var AccessSchema = z.object({
2600
- allowed_entra_group: z.string().min(1).optional(),
2601
- 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(),
2602
- 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(),
2603
2617
  identity_types: IdentityTypesSchema.optional()
2604
2618
  }).strict().superRefine((access, ctx) => {
2605
2619
  const hasSingular = access.allowed_entra_group !== undefined;
2606
2620
  const hasPlural = access.allowed_entra_groups !== undefined;
2607
- if (!hasSingular && !hasPlural) {
2621
+ if (!hasSingular && !hasPlural && access.audience !== "private") {
2608
2622
  ctx.addIssue({
2609
- code: z.ZodIssueCode.custom,
2623
+ code: z2.ZodIssueCode.custom,
2610
2624
  message: "either `allowed_entra_group` (singular, legacy) or `allowed_entra_groups` (plural, preferred) must be set",
2611
2625
  path: ["allowed_entra_groups"]
2612
2626
  });
2613
2627
  }
2614
2628
  if (hasSingular && hasPlural) {
2615
2629
  ctx.addIssue({
2616
- code: z.ZodIssueCode.custom,
2630
+ code: z2.ZodIssueCode.custom,
2617
2631
  message: "`allowed_entra_group` (singular) and `allowed_entra_groups` (plural) are mutually exclusive; use the plural form",
2618
2632
  path: ["allowed_entra_groups"]
2619
2633
  });
2620
2634
  }
2621
- });
2635
+ }).superRefine(validateAudience);
2622
2636
  function allowedEntraGroups(access) {
2637
+ if (access.audience === "private")
2638
+ return [];
2623
2639
  if (access.allowed_entra_groups !== undefined) {
2624
2640
  return access.allowed_entra_groups;
2625
2641
  }
2626
2642
  return [access.allowed_entra_group];
2627
2643
  }
2628
- var ContainerSchema = z.object({
2629
- name: z.string().min(2).max(32).regex(SLUG_REGEX, "container name must be lowercase letters, digits, and hyphens"),
2630
- image: z.string().min(1),
2631
- port: z.number().int().min(1).max(65535),
2632
- ingress: z.boolean().optional(),
2633
- healthcheck: z.string().default("/health"),
2634
- env: z.array(z.string().regex(ENV_NAME_REGEX, "env entries must be UPPER_SNAKE_CASE")).optional(),
2635
- 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()
2636
2652
  }).strict();
2637
- var TargetSchema = z.object({
2638
- kind: z.enum(TARGET_KINDS),
2639
- script: z.string().min(2).max(63).regex(SLUG_REGEX, "target script must be lowercase letters, digits, and hyphens").optional(),
2640
- schedule: z.array(z.string().min(1).max(120)).min(1).max(20).optional(),
2641
- 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()
2642
2658
  }).strict();
2643
- var SecretBindingSchema = z.object({
2644
- name: z.string().min(1).max(64).regex(ENV_NAME_REGEX, "secret binding name must be UPPER_SNAKE_CASE"),
2645
- source: z.enum(SECRET_SOURCES),
2646
- description: z.string().max(200).optional(),
2647
- 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()
2648
2664
  }).strict();
2649
- var SecretsSchema = z.object({
2650
- bindings: z.array(SecretBindingSchema).optional()
2665
+ var SecretsSchema = z2.object({
2666
+ bindings: z2.array(SecretBindingSchema).optional()
2651
2667
  }).strict();
2652
- var BuildSchema = z.object({
2653
- command: z.string().min(1).optional(),
2654
- destination_dir: z.string().min(1),
2655
- 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()
2656
2672
  }).strict();
2657
2673
  var STATIC_SERVE_DIR = "static";
2658
2674
  function staticNoBuild() {
2659
2675
  return { destination_dir: STATIC_SERVE_DIR, root_dir: "" };
2660
2676
  }
2661
- var AppBoundarySchema = z.object({
2662
- root: z.string().min(1).optional(),
2663
- include: z.array(z.string().min(1)).min(1).optional(),
2664
- 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()
2665
2681
  }).strict();
2666
- var ProductionEnvSchema = z.record(z.string().regex(ENV_NAME_REGEX, "production_env keys must be UPPER_SNAKE_CASE"), z.string());
2667
- var VerifySchema = z.object({
2668
- 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 '/'")
2669
2685
  }).strict();
2670
- var ManifestSchema = z.object({
2671
- apiVersion: z.literal("launchpad.m-kopa.us/v1alpha1"),
2672
- kind: z.literal("App"),
2686
+ var ManifestSchema = z2.object({
2687
+ apiVersion: z2.literal("launchpad.m-kopa.us/v1alpha1"),
2688
+ kind: z2.literal("App"),
2673
2689
  metadata: MetadataSchema,
2674
2690
  deployment: DeploymentSchema,
2675
2691
  access: AccessSchema,
2676
- auth: z.enum(AUTH_MODES).optional(),
2677
- hostnames: z.array(z.string().min(1)).optional(),
2678
- 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(),
2679
2695
  secrets: SecretsSchema.optional(),
2680
- targets: z.array(TargetSchema).optional(),
2696
+ targets: z2.array(TargetSchema).optional(),
2681
2697
  build: BuildSchema.optional(),
2682
2698
  production_env: ProductionEnvSchema.optional(),
2683
2699
  app: AppBoundarySchema.optional(),
2684
2700
  verify: VerifySchema.optional(),
2685
- confine_origin: z.boolean().optional(),
2686
- origin: z.enum(["pages", "dispatch"]).optional(),
2687
- catalogue: z.object({ listed: z.boolean().default(true) }).strict().optional(),
2688
- 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()
2689
2705
  }).strict().superRefine((m, ctx) => {
2690
2706
  const isContainer = m.deployment.type === "container";
2691
2707
  if (m.access.identity_types !== undefined && (m.auth ?? "access") !== "gateway") {
2692
2708
  ctx.addIssue({
2693
- code: z.ZodIssueCode.custom,
2709
+ code: z2.ZodIssueCode.custom,
2694
2710
  path: ["access", "identity_types"],
2695
2711
  message: "access.identity_types is only valid with auth: gateway"
2696
2712
  });
@@ -2698,19 +2714,19 @@ var ManifestSchema = z.object({
2698
2714
  if (m.confine_origin !== undefined) {
2699
2715
  if (isContainer) {
2700
2716
  ctx.addIssue({
2701
- code: z.ZodIssueCode.custom,
2717
+ code: z2.ZodIssueCode.custom,
2702
2718
  path: ["confine_origin"],
2703
2719
  message: "confine_origin is only valid for gateway pages apps (static/react/react+api); container apps have no pages.dev surface."
2704
2720
  });
2705
2721
  } else if (m.auth === "access") {
2706
2722
  ctx.addIssue({
2707
- code: z.ZodIssueCode.custom,
2723
+ code: z2.ZodIssueCode.custom,
2708
2724
  path: ["confine_origin"],
2709
2725
  message: "confine_origin applies to gateway-fronted apps (auth: gateway); it has no meaning under Cloudflare Access."
2710
2726
  });
2711
2727
  } else if (m.deployment.type === "static" && m.confine_origin === false) {
2712
2728
  ctx.addIssue({
2713
- code: z.ZodIssueCode.custom,
2729
+ code: z2.ZodIssueCode.custom,
2714
2730
  path: ["confine_origin"],
2715
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)."
2716
2732
  });
@@ -2718,42 +2734,42 @@ var ManifestSchema = z.object({
2718
2734
  }
2719
2735
  if (isContainer && m.auth === "gateway") {
2720
2736
  ctx.addIssue({
2721
- code: z.ZodIssueCode.custom,
2737
+ code: z2.ZodIssueCode.custom,
2722
2738
  path: ["auth"],
2723
2739
  message: "auth: gateway is only supported for pages-app shapes (static/react/react+api); container apps stay on Cloudflare Access (ADR 0016)."
2724
2740
  });
2725
2741
  }
2726
2742
  if (isContainer && m.containers === undefined) {
2727
2743
  ctx.addIssue({
2728
- code: z.ZodIssueCode.custom,
2744
+ code: z2.ZodIssueCode.custom,
2729
2745
  path: ["containers"],
2730
2746
  message: "containers[] is required when deployment.type is 'container'"
2731
2747
  });
2732
2748
  }
2733
2749
  if (!isContainer && m.containers !== undefined) {
2734
2750
  ctx.addIssue({
2735
- code: z.ZodIssueCode.custom,
2751
+ code: z2.ZodIssueCode.custom,
2736
2752
  path: ["containers"],
2737
2753
  message: `containers[] is only allowed when deployment.type is 'container' (current type: '${m.deployment.type}')`
2738
2754
  });
2739
2755
  }
2740
2756
  if (!isContainer && m.deployment.runtime !== undefined) {
2741
2757
  ctx.addIssue({
2742
- code: z.ZodIssueCode.custom,
2758
+ code: z2.ZodIssueCode.custom,
2743
2759
  path: ["deployment", "runtime"],
2744
2760
  message: "deployment.runtime is only meaningful when deployment.type is 'container'"
2745
2761
  });
2746
2762
  }
2747
2763
  if (isContainer && m.build !== undefined) {
2748
2764
  ctx.addIssue({
2749
- code: z.ZodIssueCode.custom,
2765
+ code: z2.ZodIssueCode.custom,
2750
2766
  path: ["build"],
2751
2767
  message: "build is only allowed for pages-app deployments (static/react/react+api). Container builds live in the Dockerfile."
2752
2768
  });
2753
2769
  }
2754
2770
  if (isContainer && m.production_env !== undefined) {
2755
2771
  ctx.addIssue({
2756
- code: z.ZodIssueCode.custom,
2772
+ code: z2.ZodIssueCode.custom,
2757
2773
  path: ["production_env"],
2758
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."
2759
2775
  });
@@ -2762,7 +2778,7 @@ var ManifestSchema = z.object({
2762
2778
  const ingressContainers = m.containers.filter((c) => c.ingress === true);
2763
2779
  if (ingressContainers.length !== 1) {
2764
2780
  ctx.addIssue({
2765
- code: z.ZodIssueCode.custom,
2781
+ code: z2.ZodIssueCode.custom,
2766
2782
  path: ["containers"],
2767
2783
  message: `multi-container apps must mark exactly one container ingress: true (found ${ingressContainers.length})`
2768
2784
  });
@@ -2775,7 +2791,7 @@ var ManifestSchema = z.object({
2775
2791
  for (const envName of c.env ?? []) {
2776
2792
  if (!declaredBindings.has(envName)) {
2777
2793
  ctx.addIssue({
2778
- code: z.ZodIssueCode.custom,
2794
+ code: z2.ZodIssueCode.custom,
2779
2795
  path: ["containers", i, "env"],
2780
2796
  message: `env reference '${envName}' has no matching entry in secrets.bindings[]`
2781
2797
  });
@@ -2791,7 +2807,7 @@ var ManifestSchema = z.object({
2791
2807
  continue;
2792
2808
  if (seen.has(name)) {
2793
2809
  ctx.addIssue({
2794
- code: z.ZodIssueCode.custom,
2810
+ code: z2.ZodIssueCode.custom,
2795
2811
  path: ["containers", i, "name"],
2796
2812
  message: `duplicate container name '${name}' — names become DO class identifiers and must be unique`
2797
2813
  });
@@ -2808,7 +2824,7 @@ var ManifestSchema = z.object({
2808
2824
  continue;
2809
2825
  if (seen.has(name)) {
2810
2826
  ctx.addIssue({
2811
- code: z.ZodIssueCode.custom,
2827
+ code: z2.ZodIssueCode.custom,
2812
2828
  path: ["secrets", "bindings", i, "name"],
2813
2829
  message: `duplicate secret binding name '${name}' — names become env-var keys and must be unique`
2814
2830
  });
@@ -2821,21 +2837,21 @@ var ManifestSchema = z.object({
2821
2837
  const t = targets[i];
2822
2838
  if (t.kind === "worker" && t.script === undefined) {
2823
2839
  ctx.addIssue({
2824
- code: z.ZodIssueCode.custom,
2840
+ code: z2.ZodIssueCode.custom,
2825
2841
  path: ["targets", i, "script"],
2826
2842
  message: "a worker target must declare a script (the sibling Workers script name)"
2827
2843
  });
2828
2844
  }
2829
2845
  if (t.kind === "pages" && t.script !== undefined) {
2830
2846
  ctx.addIssue({
2831
- code: z.ZodIssueCode.custom,
2847
+ code: z2.ZodIssueCode.custom,
2832
2848
  path: ["targets", i, "script"],
2833
2849
  message: "a pages target must not declare a script — the primary Pages project is the app slug"
2834
2850
  });
2835
2851
  }
2836
2852
  if (t.kind === "pages" && t.schedule !== undefined) {
2837
2853
  ctx.addIssue({
2838
- code: z.ZodIssueCode.custom,
2854
+ code: z2.ZodIssueCode.custom,
2839
2855
  path: ["targets", i, "schedule"],
2840
2856
  message: "schedule is only valid on a worker target (the cron deploy contract — ADR 0022)"
2841
2857
  });
@@ -2849,7 +2865,7 @@ var ManifestSchema = z.object({
2849
2865
  pagesTargets++;
2850
2866
  if (pagesTargets > 1) {
2851
2867
  ctx.addIssue({
2852
- code: z.ZodIssueCode.custom,
2868
+ code: z2.ZodIssueCode.custom,
2853
2869
  path: ["targets", i, "kind"],
2854
2870
  message: "at most one pages target may be declared (the primary Pages project is singular)"
2855
2871
  });
@@ -2860,7 +2876,7 @@ var ManifestSchema = z.object({
2860
2876
  continue;
2861
2877
  if (t.script === PRIMARY_TARGET || t.script === "pages") {
2862
2878
  ctx.addIssue({
2863
- code: z.ZodIssueCode.custom,
2879
+ code: z2.ZodIssueCode.custom,
2864
2880
  path: ["targets", i, "script"],
2865
2881
  message: `worker target script '${t.script}' is a reserved selector token — use a different script name`
2866
2882
  });
@@ -2868,7 +2884,7 @@ var ManifestSchema = z.object({
2868
2884
  }
2869
2885
  if (seenScripts.has(t.script)) {
2870
2886
  ctx.addIssue({
2871
- code: z.ZodIssueCode.custom,
2887
+ code: z2.ZodIssueCode.custom,
2872
2888
  path: ["targets", i, "script"],
2873
2889
  message: `duplicate worker target script '${t.script}' — target scripts must be unique`
2874
2890
  });
@@ -2886,7 +2902,7 @@ var ManifestSchema = z.object({
2886
2902
  for (const ref of sel) {
2887
2903
  if (!validRefs.has(ref)) {
2888
2904
  ctx.addIssue({
2889
- code: z.ZodIssueCode.custom,
2905
+ code: z2.ZodIssueCode.custom,
2890
2906
  path: ["secrets", "bindings", bi, "targets"],
2891
2907
  message: `secret binding '${b?.name}' references unknown target '${ref}' — declare it in targets[] or use "primary"`
2892
2908
  });
@@ -3302,42 +3318,44 @@ var STATUS_STATUS_KEYS = new Set([
3302
3318
  "status.generated_at"
3303
3319
  ]);
3304
3320
  // ../launchpad-engine/dist/schema-v2.js
3305
- import { z as z2 } from "zod";
3306
- var EnvVarSchema = z2.discriminatedUnion("type", [
3307
- z2.object({ type: z2.literal("secret_text"), secret_ref: z2.string().min(1) }).strict(),
3308
- 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()
3309
3325
  ]);
3310
- var SpecSchema = z2.object({
3311
- appType: z2.enum(APP_TYPES2),
3312
- display: z2.object({
3313
- name: z2.string().min(1),
3314
- description: z2.string().max(280).optional(),
3315
- 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)
3316
3332
  }).strict(),
3317
- owner: z2.object({ email: z2.string().email() }).strict(),
3318
- access: z2.object({
3319
- 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, {
3320
3338
  message: "allowed_entra_groups must not contain duplicates"
3321
3339
  }),
3322
- require_email_domain: z2.string().min(1).optional(),
3340
+ require_email_domain: z3.string().min(1).optional(),
3323
3341
  identity_types: IdentityTypesSchema.optional()
3324
- }).strict(),
3325
- auth: z2.enum(AUTH_MODES).optional(),
3342
+ }).strict().superRefine(validateAudience),
3343
+ auth: z3.enum(AUTH_MODES).optional(),
3326
3344
  build: BuildSchema.optional(),
3327
3345
  app: AppBoundarySchema.optional(),
3328
3346
  verify: VerifySchema.optional(),
3329
- confine_origin: z2.boolean().optional(),
3330
- origin: z2.enum(["pages", "dispatch"]).optional(),
3331
- sensitive: z2.boolean().optional(),
3332
- catalogue: z2.object({ listed: z2.boolean().default(true) }).strict().optional(),
3333
- env_vars: z2.record(z2.string().regex(ENV_NAME_REGEX, "env_vars keys must be UPPER_SNAKE_CASE"), EnvVarSchema).optional(),
3334
- 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(),
3335
3353
  secrets: SecretsSchema.optional(),
3336
- fronted_by_concierge: z2.boolean().optional()
3354
+ fronted_by_concierge: z3.boolean().optional()
3337
3355
  }).strict().superRefine((s, ctx) => {
3338
3356
  if (s.access.identity_types !== undefined && (s.auth ?? "access") !== "gateway") {
3339
3357
  ctx.addIssue({
3340
- code: z2.ZodIssueCode.custom,
3358
+ code: z3.ZodIssueCode.custom,
3341
3359
  path: ["access", "identity_types"],
3342
3360
  message: "access.identity_types is only valid with auth: gateway"
3343
3361
  });
@@ -3345,7 +3363,7 @@ var SpecSchema = z2.object({
3345
3363
  for (const [k, v] of Object.entries(s.env_vars ?? {})) {
3346
3364
  if (/_(TOKEN|SECRET)$/.test(k) && v.type !== "secret_text") {
3347
3365
  ctx.addIssue({
3348
- code: z2.ZodIssueCode.custom,
3366
+ code: z3.ZodIssueCode.custom,
3349
3367
  path: ["env_vars", k],
3350
3368
  message: `env var '${k}' is secret-bearing (*_TOKEN/*_SECRET) and must be type secret_text, not '${v.type}'`
3351
3369
  });
@@ -3354,7 +3372,7 @@ var SpecSchema = z2.object({
3354
3372
  const isContainer = s.appType === "container";
3355
3373
  if (isContainer && s.containers === undefined) {
3356
3374
  ctx.addIssue({
3357
- code: z2.ZodIssueCode.custom,
3375
+ code: z3.ZodIssueCode.custom,
3358
3376
  path: ["containers"],
3359
3377
  message: "containers[] is required when appType is 'container'"
3360
3378
  });
@@ -3362,19 +3380,19 @@ var SpecSchema = z2.object({
3362
3380
  if (s.confine_origin !== undefined) {
3363
3381
  if (isContainer) {
3364
3382
  ctx.addIssue({
3365
- code: z2.ZodIssueCode.custom,
3383
+ code: z3.ZodIssueCode.custom,
3366
3384
  path: ["confine_origin"],
3367
3385
  message: "confine_origin is only valid for gateway pages apps; container apps have no pages.dev surface."
3368
3386
  });
3369
3387
  } else if (s.auth === "access") {
3370
3388
  ctx.addIssue({
3371
- code: z2.ZodIssueCode.custom,
3389
+ code: z3.ZodIssueCode.custom,
3372
3390
  path: ["confine_origin"],
3373
3391
  message: "confine_origin applies to gateway-fronted apps (auth: gateway); it has no meaning under Cloudflare Access."
3374
3392
  });
3375
3393
  } else if (s.appType === "static" && s.confine_origin === false) {
3376
3394
  ctx.addIssue({
3377
- code: z2.ZodIssueCode.custom,
3395
+ code: z3.ZodIssueCode.custom,
3378
3396
  path: ["confine_origin"],
3379
3397
  message: "confine_origin: false is not allowed for static apps — an unconfined apex with no verifier is world-readable (ADR 0031)."
3380
3398
  });
@@ -3383,21 +3401,21 @@ var SpecSchema = z2.object({
3383
3401
  if (s.origin === "dispatch") {
3384
3402
  if (isContainer) {
3385
3403
  ctx.addIssue({
3386
- code: z2.ZodIssueCode.custom,
3404
+ code: z3.ZodIssueCode.custom,
3387
3405
  path: ["origin"],
3388
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."
3389
3407
  });
3390
3408
  }
3391
3409
  if ((s.auth ?? "access") !== "gateway") {
3392
3410
  ctx.addIssue({
3393
- code: z2.ZodIssueCode.custom,
3411
+ code: z3.ZodIssueCode.custom,
3394
3412
  path: ["origin"],
3395
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."
3396
3414
  });
3397
3415
  }
3398
3416
  if (s.confine_origin !== undefined) {
3399
3417
  ctx.addIssue({
3400
- code: z2.ZodIssueCode.custom,
3418
+ code: z3.ZodIssueCode.custom,
3401
3419
  path: ["confine_origin"],
3402
3420
  message: "confine_origin has no meaning with origin: dispatch — there is no public pages.dev apex to confine. Remove it."
3403
3421
  });
@@ -3405,7 +3423,7 @@ var SpecSchema = z2.object({
3405
3423
  for (const [name, envVar] of Object.entries(s.env_vars ?? {})) {
3406
3424
  if (envVar.type === "secret_text") {
3407
3425
  ctx.addIssue({
3408
- code: z2.ZodIssueCode.custom,
3426
+ code: z3.ZodIssueCode.custom,
3409
3427
  path: ["env_vars", name],
3410
3428
  message: "secret_text env vars are not supported with origin: dispatch until PS-2740 (secrets on v2) provides their runtime consumer."
3411
3429
  });
@@ -3414,93 +3432,93 @@ var SpecSchema = z2.object({
3414
3432
  }
3415
3433
  if (!isContainer && s.containers !== undefined) {
3416
3434
  ctx.addIssue({
3417
- code: z2.ZodIssueCode.custom,
3435
+ code: z3.ZodIssueCode.custom,
3418
3436
  path: ["containers"],
3419
3437
  message: `containers[] is only allowed when appType is 'container' (current appType: '${s.appType}')`
3420
3438
  });
3421
3439
  }
3422
3440
  if (isContainer && s.build !== undefined) {
3423
3441
  ctx.addIssue({
3424
- code: z2.ZodIssueCode.custom,
3442
+ code: z3.ZodIssueCode.custom,
3425
3443
  path: ["build"],
3426
3444
  message: "build is not allowed when appType is 'container' (container build lives in the Dockerfile)"
3427
3445
  });
3428
3446
  }
3429
3447
  if (isContainer && s.auth === "gateway") {
3430
3448
  ctx.addIssue({
3431
- code: z2.ZodIssueCode.custom,
3449
+ code: z3.ZodIssueCode.custom,
3432
3450
  path: ["auth"],
3433
3451
  message: "auth: gateway is only supported for pages-app shapes (static/react/react+api); container apps stay on Cloudflare Access (ADR 0016)."
3434
3452
  });
3435
3453
  }
3436
3454
  if (!isContainer && s.fronted_by_concierge === true) {
3437
3455
  ctx.addIssue({
3438
- code: z2.ZodIssueCode.custom,
3456
+ code: z3.ZodIssueCode.custom,
3439
3457
  path: ["fronted_by_concierge"],
3440
3458
  message: `fronted_by_concierge is only allowed when appType is 'container' (current appType: '${s.appType}')`
3441
3459
  });
3442
3460
  }
3443
3461
  });
3444
- var nonEmpty = z2.string().min(1);
3445
- var StatusSchema = z2.object({
3446
- schemaVersion: z2.literal(2),
3462
+ var nonEmpty = z3.string().min(1);
3463
+ var StatusSchema = z3.object({
3464
+ schemaVersion: z3.literal(2),
3447
3465
  manifest_spec_sha: nonEmpty,
3448
- cloudflare: z2.object({
3449
- access: z2.object({
3466
+ cloudflare: z3.object({
3467
+ access: z3.object({
3450
3468
  app_id: nonEmpty,
3451
3469
  app_aud: nonEmpty,
3452
3470
  team_domain: nonEmpty,
3453
3471
  policy_id: nonEmpty
3454
3472
  }).strict(),
3455
- pages: z2.object({
3473
+ pages: z3.object({
3456
3474
  project_name: nonEmpty,
3457
3475
  subdomain: nonEmpty,
3458
3476
  latest_deployment_id: nonEmpty
3459
3477
  }).strict().optional(),
3460
- worker: z2.object({
3478
+ worker: z3.object({
3461
3479
  dev_url: nonEmpty,
3462
3480
  script_name: nonEmpty
3463
3481
  }).strict().optional()
3464
3482
  }).strict(),
3465
- dns: z2.object({ custom_hostname: nonEmpty, cname_target: nonEmpty }).strict().optional(),
3466
- terraform: z2.object({
3483
+ dns: z3.object({ custom_hostname: nonEmpty, cname_target: nonEmpty }).strict().optional(),
3484
+ terraform: z3.object({
3467
3485
  workspace_state_key: nonEmpty,
3468
3486
  last_applied_at: nonEmpty,
3469
3487
  last_applied_sha: nonEmpty
3470
3488
  }).strict(),
3471
- registry: z2.object({ record_present: z2.boolean(), lifecycle: nonEmpty }).strict(),
3489
+ registry: z3.object({ record_present: z3.boolean(), lifecycle: nonEmpty }).strict(),
3472
3490
  generated_at: nonEmpty
3473
3491
  }).strict().superRefine((s, ctx) => {
3474
3492
  const hasPages = s.cloudflare.pages !== undefined;
3475
3493
  const hasWorker = s.cloudflare.worker !== undefined;
3476
3494
  if (hasPages === hasWorker) {
3477
3495
  ctx.addIssue({
3478
- code: z2.ZodIssueCode.custom,
3496
+ code: z3.ZodIssueCode.custom,
3479
3497
  path: ["cloudflare"],
3480
3498
  message: "status.cloudflare must carry exactly one of `pages` (pages apps) or `worker` (container apps)"
3481
3499
  });
3482
3500
  }
3483
3501
  if (hasPages && s.dns === undefined) {
3484
3502
  ctx.addIssue({
3485
- code: z2.ZodIssueCode.custom,
3503
+ code: z3.ZodIssueCode.custom,
3486
3504
  path: ["dns"],
3487
3505
  message: "status.dns is required for a pages-shape status (cloudflare.pages present)"
3488
3506
  });
3489
3507
  }
3490
3508
  if (hasWorker && s.dns !== undefined) {
3491
3509
  ctx.addIssue({
3492
- code: z2.ZodIssueCode.custom,
3510
+ code: z3.ZodIssueCode.custom,
3493
3511
  path: ["dns"],
3494
3512
  message: "status.dns must be absent for a workers_dev_only container status (no custom hostname)"
3495
3513
  });
3496
3514
  }
3497
3515
  });
3498
- var ManifestV2Schema = z2.object({
3499
- apiVersion: z2.literal("launchpad.m-kopa.us/v2"),
3500
- kind: z2.literal("App"),
3501
- metadata: z2.object({
3502
- slug: z2.string().min(2).max(63).regex(SLUG_REGEX, "must be lowercase letters, digits, and hyphens; start/end alphanumeric"),
3503
- 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)
3504
3522
  }).strict(),
3505
3523
  spec: SpecSchema,
3506
3524
  status: StatusSchema.optional()
@@ -4230,6 +4248,11 @@ var BROAD_GROUPS = [
4230
4248
  uuid: "caf78902-35dd-4833-88ce-8f067eda6036",
4231
4249
  name: "S_ALL_Employees",
4232
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."
4233
4256
  }
4234
4257
  ];
4235
4258
  function assertBroadGroupsWellFormed(list = BROAD_GROUPS) {
@@ -4248,7 +4271,7 @@ function assertBroadGroupsWellFormed(list = BROAD_GROUPS) {
4248
4271
  assertBroadGroupsWellFormed();
4249
4272
  var BROAD_UUIDS = new Set(BROAD_GROUPS.map((g) => canonicalUuid(g.uuid)).filter((u) => u !== null));
4250
4273
  // ../launchpad-engine/dist/fleet-manifest.js
4251
- import { z as z3 } from "zod";
4274
+ import { z as z4 } from "zod";
4252
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}$/;
4253
4276
  var TF_RESIDENCIES = ["per-app-workspace", "main-tf"];
4254
4277
  var APP_SHAPES = [
@@ -4276,22 +4299,22 @@ var FLEET_TIERS = [
4276
4299
  "out-of-scope"
4277
4300
  ];
4278
4301
  var DEVICE_POSTURE = ["gateway-eligible", "retained-access"];
4279
- 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.");
4280
- var FleetAppSchema = z3.object({
4281
- slug: z3.string().regex(SLUG_REGEX),
4282
- residency: z3.enum(TF_RESIDENCIES),
4283
- shape: z3.enum(APP_SHAPES),
4284
- declaredAuth: z3.enum(DECLARED_AUTH),
4285
- actualAuth: z3.enum(ACTUAL_AUTH),
4286
- groups: z3.array(GroupId).default([]),
4287
- tier: z3.enum(FLEET_TIERS),
4288
- devicePosture: z3.enum(DEVICE_POSTURE),
4289
- 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)."),
4290
- 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()
4291
4314
  }).strict();
4292
- var FleetManifestSchema = z3.object({
4293
- schemaVersion: z3.literal(1),
4294
- apps: z3.array(FleetAppSchema).min(1)
4315
+ var FleetManifestSchema = z4.object({
4316
+ schemaVersion: z4.literal(1),
4317
+ apps: z4.array(FleetAppSchema).min(1)
4295
4318
  }).strict().superRefine((m, ctx) => {
4296
4319
  const seen = new Set;
4297
4320
  for (const app of m.apps) {
@@ -4313,23 +4336,23 @@ var FleetManifestSchema = z3.object({
4313
4336
  }
4314
4337
  });
4315
4338
  // ../launchpad-engine/dist/fleet-secret-sets.js
4316
- import { z as z4 } from "zod";
4339
+ import { z as z5 } from "zod";
4317
4340
  var SET_NAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
4318
- var Slug = z4.string().regex(SLUG_REGEX, "member must be a lowercase-kebab app slug");
4319
- var SecretName = z4.string().regex(ENV_NAME_REGEX, "secret name must be UPPER_SNAKE_CASE");
4320
- 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({
4321
4344
  member: Slug,
4322
4345
  secret: SecretName
4323
4346
  }).strict();
4324
- var SecretSetSchema = z4.object({
4325
- name: z4.string().min(1).max(64).regex(SET_NAME_REGEX, "set name must be lowercase-kebab"),
4326
- secrets: z4.array(SecretName).min(1),
4327
- members: z4.array(Slug).min(1),
4328
- 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()
4329
4352
  }).strict();
4330
- var FleetSecretSetsSchema = z4.object({
4331
- schemaVersion: z4.literal(1),
4332
- secretSets: z4.array(SecretSetSchema).min(1)
4353
+ var FleetSecretSetsSchema = z5.object({
4354
+ schemaVersion: z5.literal(1),
4355
+ secretSets: z5.array(SecretSetSchema).min(1)
4333
4356
  }).strict().superRefine((m, ctx) => {
4334
4357
  const seenSets = new Set;
4335
4358
  for (let i = 0;i < m.secretSets.length; i++) {
@@ -7625,9 +7648,6 @@ function parseDeployFlags(args) {
7625
7648
  if (appType === null) {
7626
7649
  return `--new requires --app-type (one of ${APP_TYPES.join(", ")})`;
7627
7650
  }
7628
- if (allowedGroups.length === 0) {
7629
- return "--new requires at least one --allowed-group";
7630
- }
7631
7651
  return {
7632
7652
  mode: {
7633
7653
  kind: "new",
@@ -9138,6 +9158,11 @@ async function runModelADeploy(args) {
9138
9158
  allowedHost: cfg.statusPageHost
9139
9159
  });
9140
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
+ }
9141
9166
  if (result.status === 426 && typeof body.minimum === "string" && typeof body.yours === "string") {
9142
9167
  io.err(upgradeRequiredMessage(body.minimum, body.yours));
9143
9168
  return EXIT_CLI_UPGRADE_REQUIRED;
@@ -10747,7 +10772,7 @@ var initCommand = {
10747
10772
  summary: "scaffold a new launchpad.yaml in the current directory",
10748
10773
  run: (args, io) => runInit(args, io, defaultPrompt2)
10749
10774
  };
10750
- async function runInit(args, io, prompt) {
10775
+ async function runInit(args, io, prompt, identity = () => apiJson(loadConfig(), { path: "/me/identity" })) {
10751
10776
  const parsed = parseArgs6(args);
10752
10777
  if (parsed.kind === "usage-error") {
10753
10778
  io.err(`launchpad init: ${parsed.message}`);
@@ -10801,7 +10826,20 @@ async function runInit(args, io, prompt) {
10801
10826
  io.err("launchpad init: dispatch does not support --hostname or --session-duration; use --origin pages for a Pages manifest");
10802
10827
  return 64;
10803
10828
  }
10804
- const manifest = buildManifest(fillResult.inputs, detected);
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);
10805
10843
  const validation = manifest.apiVersion === "launchpad.m-kopa.us/v2" ? ManifestV2Schema.safeParse(manifest) : ManifestSchema.safeParse(manifest);
10806
10844
  if (!validation.success) {
10807
10845
  io.err("launchpad init: invalid manifest input");
@@ -11101,18 +11139,7 @@ async function fillRequired(inputs, options, io, prompt) {
11101
11139
  io.err(` must be one of: ${APP_TYPES2.join(", ")}`);
11102
11140
  }
11103
11141
  }
11104
- let groupsR;
11105
- if (inputs.groups.length > 0) {
11106
- groupsR = inputs.groups;
11107
- } else {
11108
- const raw = await askIfMissing(undefined, "allowed_entra_groups (one or more, comma-separated)", (v) => v.length >= 1 ? undefined : "must not be empty");
11109
- if (typeof raw !== "string")
11110
- return raw;
11111
- groupsR = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
11112
- if (groupsR.length === 0) {
11113
- return { kind: "usage-error", message: "at least one allowed Entra group is required" };
11114
- }
11115
- }
11142
+ const groupsR = inputs.groups;
11116
11143
  let containerImage = inputs.containerImage;
11117
11144
  let containerPort = inputs.containerPort;
11118
11145
  if (typeR === "container") {
@@ -11178,7 +11205,7 @@ function buildManifest(inputs, detected) {
11178
11205
  metadata.description = inputs.description;
11179
11206
  }
11180
11207
  const deployment = { type: inputs.type };
11181
- 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] };
11182
11209
  if (inputs.sessionDuration !== undefined && inputs.sessionDuration !== "") {
11183
11210
  access.session_duration = inputs.sessionDuration;
11184
11211
  }
@@ -11187,6 +11214,7 @@ function buildManifest(inputs, detected) {
11187
11214
  kind: "App",
11188
11215
  metadata,
11189
11216
  deployment,
11217
+ catalogue: { listed: false },
11190
11218
  access
11191
11219
  };
11192
11220
  const resolvedAuth = inputs.auth ?? (inputs.type === "container" ? "access" : "gateway");
@@ -11234,7 +11262,11 @@ function buildManifest(inputs, detected) {
11234
11262
  ...inputs.description ? { description: inputs.description } : {}
11235
11263
  },
11236
11264
  owner: { email: inputs.owner },
11237
- access: { allowed_entra_groups: [...inputs.groups] },
11265
+ access: {
11266
+ ...inputs.privateOwner === undefined ? {} : { audience: "private", private_owner: inputs.privateOwner },
11267
+ allowed_entra_groups: [...inputs.groups]
11268
+ },
11269
+ catalogue: { listed: false },
11238
11270
  app: manifest.app,
11239
11271
  ...manifest.build === undefined ? {} : { build: manifest.build }
11240
11272
  }
@@ -1 +1 @@
1
- {"version":3,"file":"deploy-flags.d.ts","sourceRoot":"","sources":["../../src/commands/deploy-flags.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAa,KAAK,OAAO,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,MAAM,UAAU,GAClB;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B;uEACmE;IACnE,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;gEAC4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,yEAAyE;IACzE,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;CAClC,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AACrD;;;;;;;GAOG;GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE;AACpF;;;;;;;;;;GAUG;GACD;IACE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC,CAAC;AAEN,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,uDAAuD;IACvD,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC;CACxB;AAQD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,WAAW,GAAG,MAAM,CAgZ9E"}
1
+ {"version":3,"file":"deploy-flags.d.ts","sourceRoot":"","sources":["../../src/commands/deploy-flags.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAa,KAAK,OAAO,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,MAAM,UAAU,GAClB;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B;uEACmE;IACnE,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;gEAC4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,yEAAyE;IACzE,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;CAClC,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AACrD;;;;;;;GAOG;GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE;AACpF;;;;;;;;;;GAUG;GACD;IACE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC,CAAC;AAEN,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,uDAAuD;IACvD,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC;CACxB;AAQD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,WAAW,GAAG,MAAM,CA6Y9E"}
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AA0EA,OAAO,EAGL,4BAA4B,EAC7B,MAAM,uBAAuB,CAAC;AAY/B,OAAO,EAAa,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACnF,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;AAQjE,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;AAEjD,eAAO,MAAM,aAAa,EAAE,OAI3B,CAAC;AAEF,UAAU,UAAU;IAClB,6BAA6B;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AA6RD;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAoBpC;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,GAAG,IAAI,CA4BpE;AAiMD,2BAA2B;AAC3B,OAAO,EAAE,4BAA4B,EAAE,CAAC;AAitBxC,2EAA2E;AAC3E;+EAC+E;AAC/E,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;AAE9D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,cAAc,CAG/E;AAED;;;4EAG4E;AAC5E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;CACrD,GAAG,OAAO,CAIV;AAED,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,OAAO,EACvB,UAAU,GAAE,OAAO,GAAG,SAAqB,GAC1C,MAAM,CAOR"}
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AA0EA,OAAO,EAGL,4BAA4B,EAC7B,MAAM,uBAAuB,CAAC;AAY/B,OAAO,EAAa,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACnF,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;AAQjE,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;AAEjD,eAAO,MAAM,aAAa,EAAE,OAI3B,CAAC;AAEF,UAAU,UAAU;IAClB,6BAA6B;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AA6RD;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAoBpC;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,GAAG,IAAI,CA4BpE;AAiMD,2BAA2B;AAC3B,OAAO,EAAE,4BAA4B,EAAE,CAAC;AAstBxC,2EAA2E;AAC3E;+EAC+E;AAC/E,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;AAE9D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,cAAc,CAG/E;AAED;;;4EAG4E;AAC5E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;CACrD,GAAG,OAAO,CAIV;AAED,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,OAAO,EACvB,UAAU,GAAE,OAAO,GAAG,SAAqB,GAC1C,MAAM,CAOR"}
@@ -1,5 +1,6 @@
1
1
  import type { CliIo, Command, ExitCode } from "../dispatcher.js";
2
2
  import { type AppType, type AuthMode } from "../manifest/schema.js";
3
+ import type { PrivateOwner } from "@m-kopa/launchpad-engine";
3
4
  /** Required-and-optional fields the verb consumes. */
4
5
  export interface InitInputs {
5
6
  name: string | undefined;
@@ -43,5 +44,5 @@ export interface InitOptions {
43
44
  export type PromptFn = (question: string, fallback?: string) => Promise<string>;
44
45
  export declare const initCommand: Command;
45
46
  /** Validate inputs and write a new manifest without changing an existing app implicitly. */
46
- export declare function runInit(args: readonly string[], io: CliIo, prompt: PromptFn): Promise<ExitCode>;
47
+ export declare function runInit(args: readonly string[], io: CliIo, prompt: PromptFn, identity?: () => Promise<PrivateOwner>): Promise<ExitCode>;
47
48
  //# sourceMappingURL=init.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAIL,KAAK,OAAO,EACZ,KAAK,QAAQ,EACd,MAAM,uBAAuB,CAAC;AAM/B,sDAAsD;AACtD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,IAAI,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1B;;;;OAIG;IACH,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC3B,iFAAiF;IACjF,MAAM,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;IACzC;;;;OAIG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAED,iEAAiE;AACjE,MAAM,MAAM,QAAQ,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAEhF,eAAO,MAAM,WAAW,EAAE,OAIzB,CAAC;AAEF,4FAA4F;AAC5F,wBAAsB,OAAO,CAC3B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,KAAK,EACT,MAAM,EAAE,QAAQ,GACf,OAAO,CAAC,QAAQ,CAAC,CAiLnB"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAIL,KAAK,OAAO,EACZ,KAAK,QAAQ,EACd,MAAM,uBAAuB,CAAC;AAI/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAK7D,sDAAsD;AACtD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,IAAI,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1B;;;;OAIG;IACH,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC3B,iFAAiF;IACjF,MAAM,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;IACzC;;;;OAIG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAED,iEAAiE;AACjE,MAAM,MAAM,QAAQ,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAEhF,eAAO,MAAM,WAAW,EAAE,OAIzB,CAAC;AAEF,4FAA4F;AAC5F,wBAAsB,OAAO,CAC3B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,KAAK,EACT,MAAM,EAAE,QAAQ,EAChB,QAAQ,GAAE,MAAM,OAAO,CAAC,YAAY,CAAuE,GAC1G,OAAO,CAAC,QAAQ,CAAC,CA8LnB"}
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.60.1";
1
+ export declare const CLI_VERSION = "0.61.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m-kopa/launchpad-cli",
3
- "version": "0.60.1",
3
+ "version": "0.61.0",
4
4
  "description": "Launchpad CLI — clone / deploy / review / merge against Launchpad-managed apps. Talks to the portal-bot endpoints (SCOPE-M-760 / T4).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-content-pr
3
3
  description: Push a content change to a Launchpad app via `launchpad deploy` and verify it shipped via `launchpad status`. Covers the post-first-deploy iteration loop (edit → deploy → verify) — subsequent deploys commit directly to the app repo's main and the Pages build runs asynchronously, so verification is its own step. Use when someone says "push a content change", "ship an update", "/launchpad-content-pr", "verify my deploy", or after `/launchpad-deploy` reports `done` and they want to follow up with an edit.
4
- version: 0.60.1
4
+ version: 0.61.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -51,6 +51,18 @@ This skill is therefore the **iteration** companion to
51
51
  `/launchpad-deploy`: once an app is live, how do you ship the next
52
52
  change — and how do you know it's actually serving.
53
53
 
54
+ ## Audience changes
55
+
56
+ Keep the existing audience for ordinary content updates.
57
+ Do not broaden groups to bypass a validation or deployment failure.
58
+ If this update newly grants a company-wide audience, ask: **"Are you sure you want every employee to be able to open this app?"**
59
+ Name the app and groups, explain that any selected group grants access, and give the human the server's confirmation URL.
60
+ Never submit the browser decision on their behalf.
61
+ Wait for their confirmation, then retry the unchanged deployment.
62
+ Sensitive apps also require platform-admin approval.
63
+ Do not review or recertify unchanged existing audiences as part of a routine update.
64
+ Catalogue visibility controls discovery, not access; platform administrators retain entry independently of app groups.
65
+
54
66
  ## Pre-flight
55
67
 
56
68
  ```bash
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-deploy
3
3
  description: Walk a Launchpad user through deploying an app from their local working directory (Model A — `launchpad init` + `launchpad deploy`). Wraps the CLI verbs end-to-end: detects the app shape, scaffolds `launchpad.yaml`, resolves the allowed Entra group via `launchpad groups`, bundles the CWD via `launchpad deploy`, and watches the rollout via `launchpad status`. Use when someone says "deploy a new app", "ship my app to Launchpad", "/launchpad-deploy", "I have an app locally — get it on Launchpad", or any variant. Resume/abandon for legacy in-flight provisioning is at the bottom.
4
- version: 0.60.1
4
+ version: 0.61.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -39,6 +39,26 @@ required; no M-KOPA GitHub access required.** External users without
39
39
  M-KOPA GitHub access are first-class — a Launchpad sign-in
40
40
  (`launchpad login`, M-KOPA Microsoft account) is all they need.
41
41
 
42
+ ## App audience and human confirmation
43
+
44
+ New apps are private and unlisted by default.
45
+ Only the verified creator and platform administrators, including Kevin Robertson, can open a private app.
46
+ Do not select a group unless the user deliberately wants to share the app with that audience.
47
+ Never select `S_ALL_Employees`, `S_ALL`, or another broad group to fix a missing-group error or make a deployment succeed.
48
+
49
+ Before requesting a new company-wide grant, ask the human exactly: **"Are you sure you want every employee to be able to open this app?"**
50
+ Name the app and the selected groups, and explain that membership in any selected group grants access.
51
+ A general instruction to deploy, a generic yes flag, and a previous approval for another app or revision do not answer this question.
52
+ When the server returns `audience_confirmation_required`, give the user its confirmation URL and wait for them to complete the browser decision.
53
+ Never open the decision endpoint with automation to approve it, submit its form, or manufacture an acknowledgement for the user.
54
+ Retry the same deployment only after the human confirms completion.
55
+ Sensitive apps also require a platform administrator's approval.
56
+ A changed audience or revision needs a new decision; unchanged existing audiences do not require recertification.
57
+
58
+ Catalogue listing controls discovery only.
59
+ It neither grants access nor makes an app private.
60
+ Platform-admin entry is independent of app groups, ownership and listing, and does not assign app-specific business roles.
61
+
42
62
  ## Constants (single source of truth)
43
63
 
44
64
  These values are referenced by `/launchpad-deploy-status`,
@@ -147,25 +167,11 @@ This is interactive by default. It will ask for:
147
167
  - **deployment type** — `static`, `react`, `react+api`, or
148
168
  `container`. The detector pre-selects the right one for Vite/React
149
169
  layouts; the user overrides if needed.
150
- - **allowed_entra_groups** — one or more, comma-separated; see §A.3.
151
-
152
- By default the app is **listed in the app catalogue**
153
- (get.launchpad/catalogue — the employee-facing app directory). Listing
154
- is discovery only and grants no one access. To keep an internal-only
155
- app out of it, add this under `spec` in a v2 manifest:
170
+ - **audience**: private unless the user deliberately selects groups; see §A.3.
156
171
 
157
- ```yaml
158
- spec:
159
- catalogue:
160
- listed: false
161
- ```
162
-
163
- For a v1 Pages manifest, use top-level `catalogue` instead:
164
-
165
- ```yaml
166
- catalogue:
167
- listed: false
168
- ```
172
+ New apps are unlisted in the employee app catalogue.
173
+ To make a v2 app discoverable, set `spec.catalogue.listed: true`; v1 uses top-level `catalogue.listed`.
174
+ This changes discovery only and does not change who can open the app.
169
175
 
170
176
  Non-interactive form for scripting:
171
177
 
@@ -174,12 +180,11 @@ launchpad init --non-interactive \
174
180
  --name <slug> \
175
181
  --type <apptype> \
176
182
  --team <team> \
177
- --owner <owner-email> \
178
- --group <group>
183
+ --owner <owner-email>
179
184
  ```
180
185
 
181
186
  `--type` must be one of `static`, `react`, `react+api`, or
182
- `container`. `--group` (preferred long form `--allowed-group`,
187
+ `container`. Omit `--group` for private access. `--group` (preferred long form `--allowed-group`,
183
188
  repeatable for multiple groups) accepts the Entra group's display
184
189
  name or UUID (the bot canonicalises to the UUID). Other flags:
185
190
  `--description <text>`, `--auth <mode>` (see §A.5),
@@ -215,7 +220,7 @@ the existing app uses any of these, plan the swap **before** picking
215
220
  a slug — do not deploy first and port later. What the bot *does*
216
221
  enforce at deploy time is the gate set in §A.5.
217
222
 
218
- ### A.3 Allowed Entra group
223
+ ### A.3 Deliberately share with an Entra group
219
224
 
220
225
  The CLI resolves Entra groups via the bot's `/groups` endpoint
221
226
  (Entra-canonical, per M-940). The playbook never speaks to that
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-deploy-status
3
3
  description: Show the current provisioning stage + failure reason for a Launchpad app via `launchpad status` (Model A drift + deployment_verified) and `launchpad apps` (lifecycle bucket), or watch provisioning live with `launchpad watch`. Renders the M-892 stage trace for in-flight provisioning, and is the canonical home for `launchpad recover` (repair a terminal-failed app record that is actually live). Use when someone says "what's the status of demo-X", "/launchpad-deploy-status", "is my deploy stuck", "watch my deploy go live", "watch provisioning", "my app says failed but it's serving", or after `/launchpad-deploy` reports a non-`done` terminal stage.
4
- version: 0.60.1
4
+ version: 0.61.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-destroy
3
3
  description: Tear down a Launchpad app end-to-end via `launchpad destroy` — Cloudflare Pages project, edge-auth wiring (gateway KV/audience entries, or the Access app for `auth: access` apps), custom hostname, platform-repo TF, and the app repo (archive-renamed). Owner-only verb with a two-step destructive confirmation. Use when someone says "destroy this app", "/launchpad-destroy", "tear down `<slug>`", "delete the app", or asks to clean up a smoke-test / orphan / retired app.
4
- version: 0.60.1
4
+ version: 0.61.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-identity
3
3
  description: Teach an app author how to use the signed-in user's identity inside a Launchpad app — read the gateway-forwarded X-Launchpad-User-Assertion in a Pages Function, VERIFY it with @m-kopa/platform-auth (fail-closed), and show who's logged in (sub/email/name). Use when someone says "who is logged in", "show the current user", "get the user's email in my app", "auth in my launchpad app", "read the user identity", "/launchpad-identity", or is wiring up an /api/me for a gateway-fronted app.
4
- version: 0.60.1
4
+ version: 0.61.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-onboard
3
3
  description: One-time setup for the Launchpad CLI + Claude Code skill bundle. Verifies the `launchpad` CLI is installed and current, runs `launchpad whoami` to confirm the session is fresh, and checks the bundled skills are installed and in lock-step with the CLI. Idempotent — safe to re-run any time. Use when someone says "set me up for Launchpad", "I just got a new machine and want to use Launchpad", "/launchpad-onboard", or any of the other launchpad-* skills fails on a prereq check.
4
- version: 0.60.1
4
+ version: 0.61.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-report
3
3
  description: File a bug report or feature request to the Launchpad team's tracker from the CLI. Use when someone reports something broken, hits an error in a launchpad command, or wishes a feature existed — e.g. "this is broken", "report a bug", "can you file that", "I wish launchpad could…", "/launchpad-bug", "/launchpad-feature". Always confirm and show exactly what you'll send before filing; never file silently.
4
- version: 0.60.1
4
+ version: 0.61.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-status
3
3
  description: Safely refresh a Launchpad clone, show manifest drift, or read the deployed manifest. Wraps bare `launchpad pull` for source sync, `launchpad pull --manifest` for deployed YAML, and `launchpad status` for drift. Use for "sync my clone", "what's deployed", "show drift", "/launchpad-status", "/launchpad-pull", or post-deploy verification.
4
- version: 0.60.1
4
+ version: 0.61.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->