@m-kopa/launchpad-cli 0.65.0 → 0.66.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 +11 -0
- package/dist/cli.js +444 -74
- package/dist/commands/init.d.ts +2 -2
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/skills/launchpad-content-pr/SKILL.md +1 -1
- package/skills/launchpad-deploy/SKILL.md +37 -4
- package/skills/launchpad-deploy-status/SKILL.md +1 -1
- package/skills/launchpad-destroy/SKILL.md +1 -1
- package/skills/launchpad-identity/SKILL.md +12 -1
- package/skills/launchpad-onboard/SKILL.md +1 -1
- package/skills/launchpad-report/SKILL.md +1 -1
- package/skills/launchpad-status/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
|
|
7
7
|
pre-1.0 minor bumps may carry breaking changes per ADR 0005.
|
|
8
8
|
|
|
9
|
+
## 0.66.0 - 2026-10-04
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Container apps can sign in through the Entra auth gateway with `auth: gateway` (AIO-811, ADR 0057). `validate` and `plan` accept it, and a gateway container app uses no Cloudflare Access app or Zero Trust seat.
|
|
14
|
+
- The platform now operates a one-minute, platform-owned health sweep against live container apps, default-off behind a kill-switch and shipped with no manifest, CLI, or `launchpad.yaml` changes (AIO-269). The sweep authenticates as a dedicated Cloudflare Access service token the platform team provisions and rotates — see `docs/runbooks/container-health-monitoring.md`. App authors take no action; this note exists only so the new, platform-operated probe traffic to the generated Worker's `/__platform/health/container` route and the platform-level Access policy addition on each container app's custom-hostname application are documented, not undocumented infrastructure. See `docs/concepts/containers.md` for a short pointer.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `launchpad init` now writes `auth: gateway` for container apps by default. Pass `--auth access` to keep the old Access sign-in (AIO-811).
|
|
19
|
+
|
|
9
20
|
## 0.65.0 - 2026-10-03
|
|
10
21
|
|
|
11
22
|
### Added
|
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.
|
|
22
|
+
var CLI_VERSION = "0.66.0";
|
|
23
23
|
|
|
24
24
|
// src/config.ts
|
|
25
25
|
import { readFileSync } from "node:fs";
|
|
@@ -2744,13 +2744,6 @@ var ManifestSchema = z2.object({
|
|
|
2744
2744
|
});
|
|
2745
2745
|
}
|
|
2746
2746
|
}
|
|
2747
|
-
if (isContainer && m.auth === "gateway") {
|
|
2748
|
-
ctx.addIssue({
|
|
2749
|
-
code: z2.ZodIssueCode.custom,
|
|
2750
|
-
path: ["auth"],
|
|
2751
|
-
message: "auth: gateway is only supported for pages-app shapes (static/react/react+api); container apps stay on Cloudflare Access (ADR 0016)."
|
|
2752
|
-
});
|
|
2753
|
-
}
|
|
2754
2747
|
if (isContainer && m.containers === undefined) {
|
|
2755
2748
|
ctx.addIssue({
|
|
2756
2749
|
code: z2.ZodIssueCode.custom,
|
|
@@ -3472,13 +3465,6 @@ var SpecSchema = z3.object({
|
|
|
3472
3465
|
message: "build is not allowed when appType is 'container' (container build lives in the Dockerfile)"
|
|
3473
3466
|
});
|
|
3474
3467
|
}
|
|
3475
|
-
if (isContainer && s.auth === "gateway") {
|
|
3476
|
-
ctx.addIssue({
|
|
3477
|
-
code: z3.ZodIssueCode.custom,
|
|
3478
|
-
path: ["auth"],
|
|
3479
|
-
message: "auth: gateway is only supported for pages-app shapes (static/react/react+api); container apps stay on Cloudflare Access (ADR 0016)."
|
|
3480
|
-
});
|
|
3481
|
-
}
|
|
3482
3468
|
if (!isContainer && s.fronted_by_concierge === true) {
|
|
3483
3469
|
ctx.addIssue({
|
|
3484
3470
|
code: z3.ZodIssueCode.custom,
|
|
@@ -3754,10 +3740,17 @@ function validateDeploymentCapability(manifest) {
|
|
|
3754
3740
|
className: containerClassName(view.appName),
|
|
3755
3741
|
bindingName,
|
|
3756
3742
|
dockerfilePathFromContainerDirectory: `../${container.image.slice(2)}`,
|
|
3757
|
-
environmentNames
|
|
3743
|
+
environmentNames,
|
|
3744
|
+
authMode: deploymentAuthMode(manifest)
|
|
3758
3745
|
}
|
|
3759
3746
|
};
|
|
3760
3747
|
}
|
|
3748
|
+
function deploymentAuthMode(manifest) {
|
|
3749
|
+
if (manifest.apiVersion === "launchpad.m-kopa.us/v2") {
|
|
3750
|
+
return manifest.spec.auth === "gateway" ? "gateway" : "access";
|
|
3751
|
+
}
|
|
3752
|
+
return manifest.auth === "gateway" ? "gateway" : "access";
|
|
3753
|
+
}
|
|
3761
3754
|
function containerClassName(containerName) {
|
|
3762
3755
|
return `AppContainer${containerName.split("-").map(capitalise).join("")}`;
|
|
3763
3756
|
}
|
|
@@ -3832,8 +3825,12 @@ function emit(capability) {
|
|
|
3832
3825
|
const app = JSON.stringify(capability.appName);
|
|
3833
3826
|
const className = capability.className;
|
|
3834
3827
|
const bindingName = capability.bindingName;
|
|
3828
|
+
const healthcheckPath = JSON.stringify(capability.container.healthcheck);
|
|
3829
|
+
const isGateway = capability.authMode === "gateway";
|
|
3835
3830
|
const environmentFields = capability.environmentNames.map((name) => ` ${name}: string;`);
|
|
3836
3831
|
const environmentValues = capability.environmentNames.map((name) => ` ${name}: this.env.${name} ?? "",`);
|
|
3832
|
+
const envAuthFields = isGateway ? [" GATEWAY_ISSUER: string;", " GATEWAY_AUD: string;", " GATEWAY_JWKS_URL: string;"] : [" CF_ACCESS_AUD: string;", " CF_ACCESS_TEAM_DOMAIN: string;"];
|
|
3833
|
+
const monitorEnvField = isGateway ? [" MONITOR_SHARED_SECRET: string;"] : [];
|
|
3837
3834
|
const lines = [
|
|
3838
3835
|
"/**",
|
|
3839
3836
|
" * Auto-generated by @m-kopa/launchpad-engine.",
|
|
@@ -3843,17 +3840,19 @@ function emit(capability) {
|
|
|
3843
3840
|
"import {",
|
|
3844
3841
|
" AccessJwtVerificationError,",
|
|
3845
3842
|
" createJwksCache,",
|
|
3846
|
-
" parseAccessJwtFromHeaders,",
|
|
3843
|
+
isGateway ? " parseGatewayAssertionFromHeaders," : " parseAccessJwtFromHeaders,",
|
|
3847
3844
|
" verifyAccessJwt,",
|
|
3845
|
+
...isGateway ? [] : [" verifyJwt,"],
|
|
3848
3846
|
" type User,",
|
|
3847
|
+
...isGateway ? [] : [" type VerifiedPrincipal,"],
|
|
3849
3848
|
'} from "@m-kopa/platform-auth";',
|
|
3850
3849
|
"",
|
|
3851
3850
|
"type Env = {",
|
|
3852
3851
|
` ${bindingName}: DurableObjectNamespace<${className}>;`,
|
|
3853
3852
|
" BINDINGS_PROXY_SECRET: string;",
|
|
3854
|
-
|
|
3855
|
-
" CF_ACCESS_TEAM_DOMAIN: string;",
|
|
3853
|
+
...envAuthFields,
|
|
3856
3854
|
" LAUNCHPAD_DEPLOYMENT_SHA: string;",
|
|
3855
|
+
...isGateway ? monitorEnvField : [" MONITOR_CLIENT_ID: string;"],
|
|
3857
3856
|
...environmentFields,
|
|
3858
3857
|
"};",
|
|
3859
3858
|
"",
|
|
@@ -3868,7 +3867,7 @@ function emit(capability) {
|
|
|
3868
3867
|
"function jwks(env: Env): ReturnType<typeof createJwksCache> {",
|
|
3869
3868
|
" if (jwksCache === null) {",
|
|
3870
3869
|
" jwksCache = createJwksCache(",
|
|
3871
|
-
" `https://${env.CF_ACCESS_TEAM_DOMAIN}/cdn-cgi/access/certs`,",
|
|
3870
|
+
isGateway ? " env.GATEWAY_JWKS_URL," : " `https://${env.CF_ACCESS_TEAM_DOMAIN}/cdn-cgi/access/certs`,",
|
|
3872
3871
|
" );",
|
|
3873
3872
|
" }",
|
|
3874
3873
|
" return jwksCache;",
|
|
@@ -3896,29 +3895,59 @@ function emit(capability) {
|
|
|
3896
3895
|
" return difference === 0;",
|
|
3897
3896
|
"}",
|
|
3898
3897
|
"",
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
"
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
"
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
"
|
|
3921
|
-
|
|
3898
|
+
...isGateway ? [
|
|
3899
|
+
"async function authenticate(request: Request, env: Env): Promise<User | Response> {",
|
|
3900
|
+
" if (!env.GATEWAY_ISSUER || !env.GATEWAY_AUD || !env.GATEWAY_JWKS_URL) {",
|
|
3901
|
+
' return unauthenticated("auth_unconfigured");',
|
|
3902
|
+
" }",
|
|
3903
|
+
" const token = parseGatewayAssertionFromHeaders(request.headers);",
|
|
3904
|
+
" if (token === null) {",
|
|
3905
|
+
' return unauthenticated("gateway_assertion_missing");',
|
|
3906
|
+
" }",
|
|
3907
|
+
" try {",
|
|
3908
|
+
" // verifyAccessJwt is a generic RS256-against-issuer/audience/JWKS",
|
|
3909
|
+
" // check; a gateway-fronted container verifies the Entra-OIDC",
|
|
3910
|
+
" // gateway's assertion (ADR 0057) with the SAME verifier used for",
|
|
3911
|
+
" // Cf Access JWTs, against the GATEWAY_* values instead.",
|
|
3912
|
+
" return await verifyAccessJwt(token, {",
|
|
3913
|
+
" teamDomain: env.GATEWAY_ISSUER,",
|
|
3914
|
+
" audience: env.GATEWAY_AUD,",
|
|
3915
|
+
" jwks: jwks(env),",
|
|
3916
|
+
" clockTolerance: 30,",
|
|
3917
|
+
" });",
|
|
3918
|
+
" } catch (error) {",
|
|
3919
|
+
" if (error instanceof AccessJwtVerificationError) {",
|
|
3920
|
+
" return unauthenticated(error.code);",
|
|
3921
|
+
" }",
|
|
3922
|
+
' console.error(JSON.stringify({ event: "container_runtime_error", kind: "gateway_assertion_verification_failed" }));',
|
|
3923
|
+
' return unauthenticated("gateway_assertion_verification_failed");',
|
|
3924
|
+
" }",
|
|
3925
|
+
"}"
|
|
3926
|
+
] : [
|
|
3927
|
+
"async function authenticate(request: Request, env: Env): Promise<User | Response> {",
|
|
3928
|
+
" if (!env.CF_ACCESS_TEAM_DOMAIN || !env.CF_ACCESS_AUD) {",
|
|
3929
|
+
' return unauthenticated("auth_unconfigured");',
|
|
3930
|
+
" }",
|
|
3931
|
+
" const token = parseAccessJwtFromHeaders(request.headers);",
|
|
3932
|
+
" if (token === null) {",
|
|
3933
|
+
' return unauthenticated("access_jwt_missing");',
|
|
3934
|
+
" }",
|
|
3935
|
+
" try {",
|
|
3936
|
+
" return await verifyAccessJwt(token, {",
|
|
3937
|
+
" teamDomain: `https://${env.CF_ACCESS_TEAM_DOMAIN}`,",
|
|
3938
|
+
" audience: env.CF_ACCESS_AUD,",
|
|
3939
|
+
" jwks: jwks(env),",
|
|
3940
|
+
" clockTolerance: 30,",
|
|
3941
|
+
" });",
|
|
3942
|
+
" } catch (error) {",
|
|
3943
|
+
" if (error instanceof AccessJwtVerificationError) {",
|
|
3944
|
+
" return unauthenticated(error.code);",
|
|
3945
|
+
" }",
|
|
3946
|
+
' console.error(JSON.stringify({ event: "container_runtime_error", kind: "access_jwt_verification_failed" }));',
|
|
3947
|
+
' return unauthenticated("access_jwt_verification_failed");',
|
|
3948
|
+
" }",
|
|
3949
|
+
"}"
|
|
3950
|
+
],
|
|
3922
3951
|
"",
|
|
3923
3952
|
"function forwardedRequest(",
|
|
3924
3953
|
" request: Request,",
|
|
@@ -3955,6 +3984,173 @@ function emit(capability) {
|
|
|
3955
3984
|
" });",
|
|
3956
3985
|
"}",
|
|
3957
3986
|
"",
|
|
3987
|
+
"/**",
|
|
3988
|
+
" * Build the machine-only health-probe request dispatched to the",
|
|
3989
|
+
" * container DO. Deliberately NOT `forwardedRequest`: it carries no",
|
|
3990
|
+
" * inbound header (no auth, no user identity, no bindings-proxy",
|
|
3991
|
+
" * secret/url) and targets the manifest-declared healthcheck path,",
|
|
3992
|
+
" * never the Worker's own reserved `GET /health` liveness route.",
|
|
3993
|
+
" */",
|
|
3994
|
+
"function buildHealthDispatchRequest(",
|
|
3995
|
+
" origin: string,",
|
|
3996
|
+
" requestId: string,",
|
|
3997
|
+
"): Request {",
|
|
3998
|
+
" const headers = new Headers();",
|
|
3999
|
+
' headers.set("X-Request-ID", requestId);',
|
|
4000
|
+
` return new Request(origin + ${healthcheckPath}, {`,
|
|
4001
|
+
' method: "GET",',
|
|
4002
|
+
" headers,",
|
|
4003
|
+
' redirect: "manual",',
|
|
4004
|
+
" });",
|
|
4005
|
+
"}",
|
|
4006
|
+
"",
|
|
4007
|
+
"function healthDenied(code: string): Response {",
|
|
4008
|
+
" return Response.json(",
|
|
4009
|
+
' { error: "health_unavailable", code },',
|
|
4010
|
+
' { status: 404, headers: { "cache-control": "no-store" } },',
|
|
4011
|
+
" );",
|
|
4012
|
+
"}",
|
|
4013
|
+
"",
|
|
4014
|
+
...isGateway ? [
|
|
4015
|
+
"/**",
|
|
4016
|
+
" * `GET /__platform/health/container` — AIO-269 T8. A gateway-",
|
|
4017
|
+
" * fronted app has no Access app and no public hostname (ADR",
|
|
4018
|
+
" * 0057), so there is no Access service-token surface for the",
|
|
4019
|
+
" * monitor to present a credential to. Instead this trusts a",
|
|
4020
|
+
" * shared secret (`MONITOR_SHARED_SECRET`) carried in",
|
|
4021
|
+
" * `X-Launchpad-Monitor-Secret` — safe ONLY because this Worker",
|
|
4022
|
+
" * is reachable exclusively via launchpad-container-router's",
|
|
4023
|
+
" * per-app service binding (never a public hostname), and the",
|
|
4024
|
+
" * monitor is the only caller with a binding to that router.",
|
|
4025
|
+
" * Missing configuration, a missing header, and a mismatched",
|
|
4026
|
+
" * secret all fail closed with no container fetch, same as the",
|
|
4027
|
+
" * Access-mode path below. The container's response body is",
|
|
4028
|
+
" * never forwarded; the reply is a fixed, redacted, `no-store`",
|
|
4029
|
+
" * JSON body.",
|
|
4030
|
+
" */",
|
|
4031
|
+
"async function handleContainerHealth(",
|
|
4032
|
+
" request: Request,",
|
|
4033
|
+
" env: Env,",
|
|
4034
|
+
" requestId: string,",
|
|
4035
|
+
"): Promise<Response> {",
|
|
4036
|
+
' if (request.method !== "GET") {',
|
|
4037
|
+
' return healthDenied("method_not_allowed");',
|
|
4038
|
+
" }",
|
|
4039
|
+
" if (!env.MONITOR_SHARED_SECRET) {",
|
|
4040
|
+
' return healthDenied("monitor_unconfigured");',
|
|
4041
|
+
" }",
|
|
4042
|
+
' const presented = request.headers.get("x-launchpad-monitor-secret");',
|
|
4043
|
+
" if (!presented || !(await secretsEqual(presented, env.MONITOR_SHARED_SECRET))) {",
|
|
4044
|
+
' return healthDenied("monitor_secret_mismatch");',
|
|
4045
|
+
" }",
|
|
4046
|
+
" const url = new URL(request.url);",
|
|
4047
|
+
" const dispatchRequest = buildHealthDispatchRequest(url.origin, requestId);",
|
|
4048
|
+
" const start = Date.now();",
|
|
4049
|
+
" let containerResponse: Response;",
|
|
4050
|
+
" try {",
|
|
4051
|
+
` const id = env.${bindingName}.idFromName("singleton");`,
|
|
4052
|
+
` containerResponse = await env.${bindingName}.get(id).fetch(dispatchRequest);`,
|
|
4053
|
+
" } catch {",
|
|
4054
|
+
" return Response.json(",
|
|
4055
|
+
" {",
|
|
4056
|
+
' outcome: "error",',
|
|
4057
|
+
" workload_status: null,",
|
|
4058
|
+
" deployment_sha: env.LAUNCHPAD_DEPLOYMENT_SHA,",
|
|
4059
|
+
" request_id: requestId,",
|
|
4060
|
+
" duration_ms: Math.min(Date.now() - start, 30_000),",
|
|
4061
|
+
" },",
|
|
4062
|
+
' { status: 200, headers: { "cache-control": "no-store" } },',
|
|
4063
|
+
" );",
|
|
4064
|
+
" }",
|
|
4065
|
+
" return Response.json(",
|
|
4066
|
+
" {",
|
|
4067
|
+
' outcome: containerResponse.ok ? "healthy" : "unhealthy",',
|
|
4068
|
+
" workload_status: containerResponse.status,",
|
|
4069
|
+
" deployment_sha: env.LAUNCHPAD_DEPLOYMENT_SHA,",
|
|
4070
|
+
" request_id: requestId,",
|
|
4071
|
+
" duration_ms: Math.min(Date.now() - start, 30_000),",
|
|
4072
|
+
" },",
|
|
4073
|
+
' { status: 200, headers: { "cache-control": "no-store" } },',
|
|
4074
|
+
" );",
|
|
4075
|
+
"}"
|
|
4076
|
+
] : [
|
|
4077
|
+
"/**",
|
|
4078
|
+
" * `GET /__platform/health/container` — machine-only. Gated on",
|
|
4079
|
+
" * `verifyJwt` returning a service principal whose `commonName`",
|
|
4080
|
+
" * matches the configured monitor client ID. Anonymous callers,",
|
|
4081
|
+
" * human JWTs, wrong service principals, non-GET methods, and a",
|
|
4082
|
+
" * missing monitor-client-ID configuration all fail closed with no",
|
|
4083
|
+
" * container fetch. The container's response body is never",
|
|
4084
|
+
" * forwarded; the reply is a fixed, redacted, `no-store` JSON body.",
|
|
4085
|
+
" */",
|
|
4086
|
+
"async function handleContainerHealth(",
|
|
4087
|
+
" request: Request,",
|
|
4088
|
+
" env: Env,",
|
|
4089
|
+
" requestId: string,",
|
|
4090
|
+
"): Promise<Response> {",
|
|
4091
|
+
' if (request.method !== "GET") {',
|
|
4092
|
+
' return healthDenied("method_not_allowed");',
|
|
4093
|
+
" }",
|
|
4094
|
+
" if (!env.MONITOR_CLIENT_ID || !env.CF_ACCESS_TEAM_DOMAIN || !env.CF_ACCESS_AUD) {",
|
|
4095
|
+
' return healthDenied("monitor_unconfigured");',
|
|
4096
|
+
" }",
|
|
4097
|
+
" const token = parseAccessJwtFromHeaders(request.headers);",
|
|
4098
|
+
" if (token === null) {",
|
|
4099
|
+
' return healthDenied("access_jwt_missing");',
|
|
4100
|
+
" }",
|
|
4101
|
+
" let principal: VerifiedPrincipal;",
|
|
4102
|
+
" try {",
|
|
4103
|
+
" principal = await verifyJwt(token, {",
|
|
4104
|
+
" teamDomain: `https://${env.CF_ACCESS_TEAM_DOMAIN}`,",
|
|
4105
|
+
" audience: env.CF_ACCESS_AUD,",
|
|
4106
|
+
" jwks: jwks(env),",
|
|
4107
|
+
" clockTolerance: 30,",
|
|
4108
|
+
" });",
|
|
4109
|
+
" } catch (error) {",
|
|
4110
|
+
" if (error instanceof AccessJwtVerificationError) {",
|
|
4111
|
+
" return healthDenied(error.code);",
|
|
4112
|
+
" }",
|
|
4113
|
+
' console.error(JSON.stringify({ event: "container_runtime_error", kind: "monitor_jwt_verification_failed" }));',
|
|
4114
|
+
' return healthDenied("monitor_jwt_verification_failed");',
|
|
4115
|
+
" }",
|
|
4116
|
+
' if (principal.kind !== "service") {',
|
|
4117
|
+
' return healthDenied("human_identity_not_allowed");',
|
|
4118
|
+
" }",
|
|
4119
|
+
" if (principal.principal.commonName !== env.MONITOR_CLIENT_ID) {",
|
|
4120
|
+
' return healthDenied("monitor_identity_mismatch");',
|
|
4121
|
+
" }",
|
|
4122
|
+
" const url = new URL(request.url);",
|
|
4123
|
+
" const dispatchRequest = buildHealthDispatchRequest(url.origin, requestId);",
|
|
4124
|
+
" const start = Date.now();",
|
|
4125
|
+
" let containerResponse: Response;",
|
|
4126
|
+
" try {",
|
|
4127
|
+
` const id = env.${bindingName}.idFromName("singleton");`,
|
|
4128
|
+
` containerResponse = await env.${bindingName}.get(id).fetch(dispatchRequest);`,
|
|
4129
|
+
" } catch {",
|
|
4130
|
+
" return Response.json(",
|
|
4131
|
+
" {",
|
|
4132
|
+
' outcome: "error",',
|
|
4133
|
+
" workload_status: null,",
|
|
4134
|
+
" deployment_sha: env.LAUNCHPAD_DEPLOYMENT_SHA,",
|
|
4135
|
+
" request_id: requestId,",
|
|
4136
|
+
" duration_ms: Math.min(Date.now() - start, 30_000),",
|
|
4137
|
+
" },",
|
|
4138
|
+
' { status: 200, headers: { "cache-control": "no-store" } },',
|
|
4139
|
+
" );",
|
|
4140
|
+
" }",
|
|
4141
|
+
" return Response.json(",
|
|
4142
|
+
" {",
|
|
4143
|
+
' outcome: containerResponse.ok ? "healthy" : "unhealthy",',
|
|
4144
|
+
" workload_status: containerResponse.status,",
|
|
4145
|
+
" deployment_sha: env.LAUNCHPAD_DEPLOYMENT_SHA,",
|
|
4146
|
+
" request_id: requestId,",
|
|
4147
|
+
" duration_ms: Math.min(Date.now() - start, 30_000),",
|
|
4148
|
+
" },",
|
|
4149
|
+
' { status: 200, headers: { "cache-control": "no-store" } },',
|
|
4150
|
+
" );",
|
|
4151
|
+
"}"
|
|
4152
|
+
],
|
|
4153
|
+
"",
|
|
3958
4154
|
"function runtimeEvent(",
|
|
3959
4155
|
" env: Env,",
|
|
3960
4156
|
" requestId: string,",
|
|
@@ -3993,6 +4189,11 @@ function emit(capability) {
|
|
|
3993
4189
|
' runtimeEvent(env, requestId, response.status, "bindings_unknown");',
|
|
3994
4190
|
" return response;",
|
|
3995
4191
|
" }",
|
|
4192
|
+
' if (url.pathname === "/__platform/health/container") {',
|
|
4193
|
+
" const response = await handleContainerHealth(request, env, requestId);",
|
|
4194
|
+
' runtimeEvent(env, requestId, response.status, "container_health_probe");',
|
|
4195
|
+
" return response;",
|
|
4196
|
+
" }",
|
|
3996
4197
|
" const identity = await authenticate(request, env);",
|
|
3997
4198
|
" if (identity instanceof Response) {",
|
|
3998
4199
|
' runtimeEvent(env, requestId, identity.status, "authentication_failed");',
|
|
@@ -4033,6 +4234,8 @@ function generateWranglerToml(manifest) {
|
|
|
4033
4234
|
}
|
|
4034
4235
|
function emit2(capability) {
|
|
4035
4236
|
const maxInstances = capability.container.max_instances ?? 1;
|
|
4237
|
+
const isGateway = capability.authMode === "gateway";
|
|
4238
|
+
const authVars = isGateway ? ['GATEWAY_ISSUER = ""', 'GATEWAY_AUD = ""', 'GATEWAY_JWKS_URL = ""'] : ['CF_ACCESS_AUD = ""', 'CF_ACCESS_TEAM_DOMAIN = ""'];
|
|
4036
4239
|
return [
|
|
4037
4240
|
"# Auto-generated by @m-kopa/launchpad-engine. Do not edit by hand.",
|
|
4038
4241
|
`# Source: launchpad.yaml (apiVersion: ${capability.apiVersion})`,
|
|
@@ -4042,14 +4245,14 @@ function emit2(capability) {
|
|
|
4042
4245
|
'main = "worker/index.ts"',
|
|
4043
4246
|
`compatibility_date = "${DEFAULT_COMPATIBILITY_DATE}"`,
|
|
4044
4247
|
`compatibility_flags = [${DEFAULT_COMPATIBILITY_FLAGS.map((flag) => `"${flag}"`).join(", ")}]`,
|
|
4248
|
+
...isGateway ? ["workers_dev = false"] : [],
|
|
4045
4249
|
"",
|
|
4046
4250
|
"[observability]",
|
|
4047
4251
|
"enabled = true",
|
|
4048
4252
|
"head_sampling_rate = 1.0",
|
|
4049
4253
|
"",
|
|
4050
4254
|
"[vars]",
|
|
4051
|
-
|
|
4052
|
-
'CF_ACCESS_TEAM_DOMAIN = ""',
|
|
4255
|
+
...authVars,
|
|
4053
4256
|
'LAUNCHPAD_DEPLOYMENT_SHA = ""',
|
|
4054
4257
|
"",
|
|
4055
4258
|
"[[containers]]",
|
|
@@ -4093,6 +4296,10 @@ function generateGithubDeployWorkflow(manifest) {
|
|
|
4093
4296
|
return { kind: "ok", content: emit3(result.capability), warnings: [] };
|
|
4094
4297
|
}
|
|
4095
4298
|
function emit3(capability) {
|
|
4299
|
+
const isGateway = capability.authMode === "gateway";
|
|
4300
|
+
const authVarNames = isGateway ? ["GATEWAY_ISSUER", "GATEWAY_AUD", "GATEWAY_JWKS_URL"] : ["CF_ACCESS_AUD", "CF_ACCESS_TEAM_DOMAIN"];
|
|
4301
|
+
const authVarEnvLines = authVarNames.map((name) => ` ${name}: \${{ vars.${name} }}`);
|
|
4302
|
+
const authVarWranglerFlags = authVarNames.map((name) => ` --var "${name}:\${${name}}" \\`);
|
|
4096
4303
|
const lines = [
|
|
4097
4304
|
"# Auto-generated by @m-kopa/launchpad-engine. Do not edit by hand.",
|
|
4098
4305
|
`# Source: launchpad.yaml (apiVersion: ${capability.apiVersion})`,
|
|
@@ -4181,29 +4388,22 @@ function emit3(capability) {
|
|
|
4181
4388
|
" cache: npm",
|
|
4182
4389
|
" cache-dependency-path: |",
|
|
4183
4390
|
" container/package-lock.json",
|
|
4184
|
-
" container/backend/package-lock.json",
|
|
4185
4391
|
"",
|
|
4186
4392
|
" - name: Install frozen Worker dependencies",
|
|
4187
4393
|
" working-directory: container",
|
|
4188
4394
|
" run: npm ci --ignore-scripts",
|
|
4189
4395
|
"",
|
|
4190
|
-
" - name: Install frozen workload dependencies",
|
|
4191
|
-
" working-directory: container/backend",
|
|
4192
|
-
" run: npm ci --ignore-scripts",
|
|
4193
|
-
"",
|
|
4194
4396
|
" - name: Verify required deployment inputs",
|
|
4195
4397
|
" env:",
|
|
4196
4398
|
" CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}",
|
|
4197
4399
|
" CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
|
|
4198
|
-
|
|
4199
|
-
" CF_ACCESS_TEAM_DOMAIN: ${{ vars.CF_ACCESS_TEAM_DOMAIN }}",
|
|
4400
|
+
...authVarEnvLines,
|
|
4200
4401
|
" BINDINGS_PROXY_SECRET: ${{ secrets.BINDINGS_PROXY_SECRET }}",
|
|
4201
|
-
"
|
|
4202
|
-
" CF_ACCESS_TEAM_DOMAIN: ${{ vars.CF_ACCESS_TEAM_DOMAIN }}",
|
|
4402
|
+
...isGateway ? [" MONITOR_SHARED_SECRET: ${{ secrets.MONITOR_SHARED_SECRET }}"] : [],
|
|
4203
4403
|
...capability.environmentNames.map((name) => ` ${name}: \${{ secrets.${name} }}`),
|
|
4204
4404
|
" run: |",
|
|
4205
4405
|
" set -euo pipefail",
|
|
4206
|
-
|
|
4406
|
+
` for name in CLOUDFLARE_API_TOKEN CLOUDFLARE_ACCOUNT_ID BINDINGS_PROXY_SECRET${isGateway ? " MONITOR_SHARED_SECRET" : ""} ${authVarNames.join(" ")}; do`,
|
|
4207
4407
|
' if [ -z "${!name:-}" ]; then',
|
|
4208
4408
|
' echo "Required deployment input $name is empty" >&2',
|
|
4209
4409
|
" exit 1",
|
|
@@ -4216,37 +4416,207 @@ function emit3(capability) {
|
|
|
4216
4416
|
" fi"
|
|
4217
4417
|
]),
|
|
4218
4418
|
"",
|
|
4219
|
-
" - name: Deploy Worker
|
|
4419
|
+
" - name: Deploy Worker, container image and secrets",
|
|
4220
4420
|
" working-directory: container",
|
|
4221
4421
|
" env:",
|
|
4222
4422
|
" CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}",
|
|
4223
4423
|
" CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
|
|
4424
|
+
...authVarEnvLines,
|
|
4425
|
+
...secretNames(capability).map((name) => ` ${name}: \${{ secrets.${name} }}`),
|
|
4224
4426
|
" run: |",
|
|
4427
|
+
" set -euo pipefail",
|
|
4428
|
+
' secrets_file="$(mktemp "${RUNNER_TEMP}/launchpad-secrets.XXXXXX")"',
|
|
4429
|
+
` trap 'rm -f "$secrets_file"' EXIT`,
|
|
4430
|
+
` jq -n '${secretsJqFilter(capability)}' > "$secrets_file"`,
|
|
4225
4431
|
" npx --no-install wrangler deploy \\",
|
|
4226
4432
|
' --message "launchpad:${GITHUB_SHA}" \\',
|
|
4227
4433
|
' --tag "launchpad-deployment-sha:${GITHUB_SHA}" \\',
|
|
4228
|
-
|
|
4229
|
-
' --var "
|
|
4230
|
-
' --
|
|
4231
|
-
""
|
|
4232
|
-
...secretSteps(capability)
|
|
4434
|
+
...authVarWranglerFlags,
|
|
4435
|
+
' --var "LAUNCHPAD_DEPLOYMENT_SHA:${GITHUB_SHA}" \\',
|
|
4436
|
+
' --secrets-file "$secrets_file"',
|
|
4437
|
+
""
|
|
4233
4438
|
];
|
|
4234
4439
|
return lines.join(`
|
|
4235
4440
|
`);
|
|
4236
4441
|
}
|
|
4237
|
-
function
|
|
4238
|
-
return ["BINDINGS_PROXY_SECRET", ...capability.environmentNames]
|
|
4239
|
-
` - name: Push ${name}`,
|
|
4240
|
-
" working-directory: container",
|
|
4241
|
-
" env:",
|
|
4242
|
-
" CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}",
|
|
4243
|
-
" CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
|
|
4244
|
-
` ${name}: \${{ secrets.${name} }}`,
|
|
4245
|
-
" run: |",
|
|
4246
|
-
` printf '%s' "$${name}" | npx --no-install wrangler secret put ${name}`,
|
|
4247
|
-
""
|
|
4248
|
-
]);
|
|
4442
|
+
function secretNames(capability) {
|
|
4443
|
+
return capability.authMode === "gateway" ? ["BINDINGS_PROXY_SECRET", "MONITOR_SHARED_SECRET", ...capability.environmentNames] : ["BINDINGS_PROXY_SECRET", ...capability.environmentNames];
|
|
4249
4444
|
}
|
|
4445
|
+
function secretsJqFilter(capability) {
|
|
4446
|
+
return `{${secretNames(capability).map((name) => `${name}: env.${name}`).join(", ")}}`;
|
|
4447
|
+
}
|
|
4448
|
+
// ../launchpad-engine/dist/per-app-lock.js
|
|
4449
|
+
var PER_APP_LOCK_HCL = `# This file is maintained automatically by "terraform init".
|
|
4450
|
+
# Manual edits may be lost in future updates.
|
|
4451
|
+
|
|
4452
|
+
provider "registry.terraform.io/cloudflare/cloudflare" {
|
|
4453
|
+
version = "5.19.1"
|
|
4454
|
+
constraints = "~> 5.19"
|
|
4455
|
+
hashes = [
|
|
4456
|
+
"h1:HkKPMZ/n+QiExkRUSLjGMTGnuIaph+k932LiTp7CKZM=",
|
|
4457
|
+
"h1:mopYISyLkUVUwMjJbuPenxIUVDrna1/7ACB1hfMfciU=",
|
|
4458
|
+
"zh:0651618000db705564dab5a25322b9d76ea54b7dd78931ed3565497b559babeb",
|
|
4459
|
+
"zh:1a7847e9479fb6d21a65ef933ffae1416b1e4b44ca940c0d6c50fc4248cc4a0d",
|
|
4460
|
+
"zh:5597cee5854131045eb9f201ae3a70b59c51955d31a647d9616863c746d902cb",
|
|
4461
|
+
"zh:580786830d93e35b957754fd4c62d4681a3b19abc28b757e41acba26455663b1",
|
|
4462
|
+
"zh:83c4bdfb0e74fd50e56fff3c461d76c1c1ec61af3f679e4de1aa70b5ed05a09f",
|
|
4463
|
+
"zh:abb4d1052cee61d80f9cb51e5421e3c118312403afb7104b98bd7e310ac736ee",
|
|
4464
|
+
"zh:b0aeeb3d66ea4d719989875e778c477065ba941e3a76e9a8caacc3be08208dd9",
|
|
4465
|
+
"zh:e43b4b2dfcec1ce2115f5a5c86042d432deb49bee8eae103eb56d97ea02e2e3b",
|
|
4466
|
+
"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32",
|
|
4467
|
+
]
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
provider "registry.terraform.io/hashicorp/azurerm" {
|
|
4471
|
+
version = "4.76.0"
|
|
4472
|
+
constraints = "~> 4.0"
|
|
4473
|
+
hashes = [
|
|
4474
|
+
"h1:4VD01UBYjsa3w7zeEBKnDtwClwpsPw6Dk+V9evhiKqU=",
|
|
4475
|
+
"h1:bOzo55MQgsvQ+KRwrWM5q+waapuMZ3AGIkK5ZIOe+JU=",
|
|
4476
|
+
"zh:07bb3a087abca8bd14cc28e3d3d4abb71e0ed711ecec65e7c0a2f1a97b948cf4",
|
|
4477
|
+
"zh:0fa6640b5d6c0b0fe0b7fa25cebc0091b7dda2efd83ec0347c5a50dfce0957bc",
|
|
4478
|
+
"zh:3c8618c8a5ab07fadfcb53ecf614f7b99e25bb6c407cf0af703a6b0647d4461d",
|
|
4479
|
+
"zh:5f23fdda6ff290bc99b25b68f612bf67fbb503b04b1e38664e66d3204a51e99e",
|
|
4480
|
+
"zh:68e6794cbe5e3b021657035c2465767b8296beed6c9acc8a9e032c4b90ba5746",
|
|
4481
|
+
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
|
4482
|
+
"zh:8a51dad112248d3fdfb19cdd65a182bd6a37e7fb7f1af801ab72a962813b5188",
|
|
4483
|
+
"zh:945ff59c36de8c2128487f4ae0838655feef7c869112430ffa24ea69ff483b76",
|
|
4484
|
+
"zh:b7306af0ecc8ad78771c17e0e911dd7a561251e4255ffe80516f0654a53f42bb",
|
|
4485
|
+
"zh:c584de4858fc4ece330f1986f371acd235a5444e9b14ecea177779b1214539e0",
|
|
4486
|
+
"zh:daa3675af21f8cdf330532af0b420287388f62ac57eaf0bd4c48e732d1052f35",
|
|
4487
|
+
"zh:e76d972712b48a689f8deae41a3ad796c31d8b3798074d66c6f68e2b3b0aaa1f",
|
|
4488
|
+
]
|
|
4489
|
+
}
|
|
4490
|
+
|
|
4491
|
+
provider "registry.terraform.io/hashicorp/time" {
|
|
4492
|
+
version = "0.14.0"
|
|
4493
|
+
constraints = "~> 0.13"
|
|
4494
|
+
hashes = [
|
|
4495
|
+
"h1:/hlxsUpuN/lvPTNL9+NyVGsOyRsK5NsxwFMsj5CdOp4=",
|
|
4496
|
+
"h1:4EThC3ocCFiFPMZQSUvSGSxoJqBcGWxMcFYmL67uS7Y=",
|
|
4497
|
+
"zh:12abfd6b800e4d7fa6db7310dec8ffd440b31993861ef188c7ed5260b3073937",
|
|
4498
|
+
"zh:23005521e800bb19e1597bf755c5f70d675d30b685d4255001ed5fa47d9df3f1",
|
|
4499
|
+
"zh:2fea249b582ae97cd1cc10385187ea50993bb47c28cc5df0305e57ceaabf0a10",
|
|
4500
|
+
"zh:322018d3b987b7aad08697178029a2bb667bed699e88328f0c89c52a2fd41341",
|
|
4501
|
+
"zh:32a08e98fce2d273cb9b2c89d6c54727cc9f0a32e15bfd896be4e02cc6b48f95",
|
|
4502
|
+
"zh:3db89aabd0e619616bd4b0f8b373a7586dfe60feffcea12a84a0bdbc445714b3",
|
|
4503
|
+
"zh:7488f56c81d742dc020f29063626c8f07ca188aa97be61e7307e8d62397020a2",
|
|
4504
|
+
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
|
4505
|
+
"zh:7cb4067f2e7559b13f7562ef722f948950901eb37834873e98360ab28f66e9d7",
|
|
4506
|
+
"zh:9d552c8345f61e1b7db8e725144981345f18ac1014d58d6f5ddf0928a195fffb",
|
|
4507
|
+
"zh:a8e69fb6b97fc9d86fb19a9f4d42abe33c4a68e700b15387ce2e17d2b9934bed",
|
|
4508
|
+
"zh:aeeb900eb8dd0f790c60ea5c0e0c8d42bd6e4a54f391681d4decca15b544394b",
|
|
4509
|
+
"zh:c239c619101a8c95e1f14061eb973c57a8d15fa0e68878ced5bbd76858ee5b79",
|
|
4510
|
+
]
|
|
4511
|
+
}
|
|
4512
|
+
`;
|
|
4513
|
+
|
|
4514
|
+
// ../launchpad-engine/dist/per-app-container-lock.js
|
|
4515
|
+
var PER_APP_CONTAINER_LOCK_HCL = `# This file is maintained automatically by "terraform init".
|
|
4516
|
+
# Manual edits may be lost in future updates.
|
|
4517
|
+
|
|
4518
|
+
provider "registry.terraform.io/cloudflare/cloudflare" {
|
|
4519
|
+
version = "5.19.1"
|
|
4520
|
+
constraints = "~> 5.19"
|
|
4521
|
+
hashes = [
|
|
4522
|
+
"h1:HkKPMZ/n+QiExkRUSLjGMTGnuIaph+k932LiTp7CKZM=",
|
|
4523
|
+
"h1:mopYISyLkUVUwMjJbuPenxIUVDrna1/7ACB1hfMfciU=",
|
|
4524
|
+
"zh:0651618000db705564dab5a25322b9d76ea54b7dd78931ed3565497b559babeb",
|
|
4525
|
+
"zh:1a7847e9479fb6d21a65ef933ffae1416b1e4b44ca940c0d6c50fc4248cc4a0d",
|
|
4526
|
+
"zh:5597cee5854131045eb9f201ae3a70b59c51955d31a647d9616863c746d902cb",
|
|
4527
|
+
"zh:580786830d93e35b957754fd4c62d4681a3b19abc28b757e41acba26455663b1",
|
|
4528
|
+
"zh:83c4bdfb0e74fd50e56fff3c461d76c1c1ec61af3f679e4de1aa70b5ed05a09f",
|
|
4529
|
+
"zh:abb4d1052cee61d80f9cb51e5421e3c118312403afb7104b98bd7e310ac736ee",
|
|
4530
|
+
"zh:b0aeeb3d66ea4d719989875e778c477065ba941e3a76e9a8caacc3be08208dd9",
|
|
4531
|
+
"zh:e43b4b2dfcec1ce2115f5a5c86042d432deb49bee8eae103eb56d97ea02e2e3b",
|
|
4532
|
+
"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32",
|
|
4533
|
+
]
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4536
|
+
provider "registry.terraform.io/hashicorp/random" {
|
|
4537
|
+
version = "3.9.0"
|
|
4538
|
+
constraints = "~> 3.6"
|
|
4539
|
+
hashes = [
|
|
4540
|
+
"h1:OO+IuvQJSPmWdN8AyyIEvPJbLvDQpgX/zbktoa9KsJE=",
|
|
4541
|
+
"h1:UlBuNVuCGJ39tTv2c5gz2NRZnQbXfbIWbTzWcth5o74=",
|
|
4542
|
+
"zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1",
|
|
4543
|
+
"zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea",
|
|
4544
|
+
"zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f",
|
|
4545
|
+
"zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0",
|
|
4546
|
+
"zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61",
|
|
4547
|
+
"zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc",
|
|
4548
|
+
"zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398",
|
|
4549
|
+
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
|
4550
|
+
"zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e",
|
|
4551
|
+
"zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef",
|
|
4552
|
+
"zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b",
|
|
4553
|
+
"zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257",
|
|
4554
|
+
"zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04",
|
|
4555
|
+
]
|
|
4556
|
+
}
|
|
4557
|
+
|
|
4558
|
+
provider "registry.terraform.io/hashicorp/time" {
|
|
4559
|
+
version = "0.14.0"
|
|
4560
|
+
constraints = "~> 0.13"
|
|
4561
|
+
hashes = [
|
|
4562
|
+
"h1:/hlxsUpuN/lvPTNL9+NyVGsOyRsK5NsxwFMsj5CdOp4=",
|
|
4563
|
+
"h1:4EThC3ocCFiFPMZQSUvSGSxoJqBcGWxMcFYmL67uS7Y=",
|
|
4564
|
+
"zh:12abfd6b800e4d7fa6db7310dec8ffd440b31993861ef188c7ed5260b3073937",
|
|
4565
|
+
"zh:23005521e800bb19e1597bf755c5f70d675d30b685d4255001ed5fa47d9df3f1",
|
|
4566
|
+
"zh:2fea249b582ae97cd1cc10385187ea50993bb47c28cc5df0305e57ceaabf0a10",
|
|
4567
|
+
"zh:322018d3b987b7aad08697178029a2bb667bed699e88328f0c89c52a2fd41341",
|
|
4568
|
+
"zh:32a08e98fce2d273cb9b2c89d6c54727cc9f0a32e15bfd896be4e02cc6b48f95",
|
|
4569
|
+
"zh:3db89aabd0e619616bd4b0f8b373a7586dfe60feffcea12a84a0bdbc445714b3",
|
|
4570
|
+
"zh:7488f56c81d742dc020f29063626c8f07ca188aa97be61e7307e8d62397020a2",
|
|
4571
|
+
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
|
4572
|
+
"zh:7cb4067f2e7559b13f7562ef722f948950901eb37834873e98360ab28f66e9d7",
|
|
4573
|
+
"zh:9d552c8345f61e1b7db8e725144981345f18ac1014d58d6f5ddf0928a195fffb",
|
|
4574
|
+
"zh:a8e69fb6b97fc9d86fb19a9f4d42abe33c4a68e700b15387ce2e17d2b9934bed",
|
|
4575
|
+
"zh:aeeb900eb8dd0f790c60ea5c0e0c8d42bd6e4a54f391681d4decca15b544394b",
|
|
4576
|
+
"zh:c239c619101a8c95e1f14061eb973c57a8d15fa0e68878ced5bbd76858ee5b79",
|
|
4577
|
+
]
|
|
4578
|
+
}
|
|
4579
|
+
|
|
4580
|
+
provider "registry.terraform.io/integrations/github" {
|
|
4581
|
+
version = "6.12.1"
|
|
4582
|
+
constraints = "~> 6.0"
|
|
4583
|
+
hashes = [
|
|
4584
|
+
"h1:bGz4LIep/7PVrqy6P8cTYbAJpdxXGrupUJjkCczlzIs=",
|
|
4585
|
+
"h1:hHOwf554tYL9pQS2uRPbaAGSXRbbBJ/+HtQAoT9mtuA=",
|
|
4586
|
+
"zh:3e1a4081ecb9518fdf0074db83c16ad00dc81ffe8249a6e3cf1894e947e28df6",
|
|
4587
|
+
"zh:4cb8224b7f530795b674ac044675f6b22a7c9154f55eb9f76c5af6c7534056a4",
|
|
4588
|
+
"zh:560bc08637926191f6871a89e986022ca67c70afda5bebca34b5216e6fac69c9",
|
|
4589
|
+
"zh:5a70b5d2ac650c5c9819a1875411ebda229d0fcc6c9f57f9d751852ca3cd77ac",
|
|
4590
|
+
"zh:8668d93bd4dc2ffa2545e1473af600a925d479b16033a71a4498a16f3b683c0c",
|
|
4591
|
+
"zh:86eacc6059fd057948e178b665ba5cce74bd5488a9e1035734e60ff5ef1b6f8f",
|
|
4592
|
+
"zh:a329fac98881d8dfc211a9bdc0ec6f2948f0b0c2704d1b6cbe5307403c7ad1b2",
|
|
4593
|
+
"zh:dadd44abab3c52b9d572955afaef1658790e17ea355ee22b58996d81d28e02d8",
|
|
4594
|
+
"zh:de9f455ef342cc38fb76bce844bfcd376fb81a4b9f9bc2fae023ff99efdf1338",
|
|
4595
|
+
"zh:f8c6d2e8351b334491790358574e0a30a7c6d7f5b80f7daf32a7c0f3e9b1ab19",
|
|
4596
|
+
"zh:fab41971a3edee04ab6eceaeab4eeb9a2b2f38a2af3b06eda93e2117b64994be",
|
|
4597
|
+
"zh:fb1279b566dd9c8c117b2e4e0cc8344413b8fc8f2a3e24be22a9b2610551777b",
|
|
4598
|
+
"zh:fbd1fee2c9df3aa19cf8851ce134dea6e45ea01cb85695c1726670c285797e25",
|
|
4599
|
+
"zh:fe79d2a861fb9af420fa5bd7f02c031b2a0a3edf5dbc46022c8ecc7a33cf2b6d",
|
|
4600
|
+
]
|
|
4601
|
+
}
|
|
4602
|
+
`;
|
|
4603
|
+
var PER_APP_CONTAINER_GATEWAY_LOCK_HCL = (() => {
|
|
4604
|
+
const marker = 'provider "registry.terraform.io/hashicorp/azurerm" {';
|
|
4605
|
+
const start = PER_APP_LOCK_HCL.indexOf(marker);
|
|
4606
|
+
const end = PER_APP_LOCK_HCL.indexOf(`
|
|
4607
|
+
}
|
|
4608
|
+
`, start) + 3;
|
|
4609
|
+
if (start < 0 || end < 3)
|
|
4610
|
+
throw new Error("azurerm block missing from the pages lock");
|
|
4611
|
+
const azurerm = PER_APP_LOCK_HCL.slice(start, end);
|
|
4612
|
+
const before = 'provider "registry.terraform.io/hashicorp/random" {';
|
|
4613
|
+
const at = PER_APP_CONTAINER_LOCK_HCL.indexOf(before);
|
|
4614
|
+
if (at < 0)
|
|
4615
|
+
throw new Error("random block missing from the container lock");
|
|
4616
|
+
return PER_APP_CONTAINER_LOCK_HCL.slice(0, at) + azurerm + `
|
|
4617
|
+
` + PER_APP_CONTAINER_LOCK_HCL.slice(at);
|
|
4618
|
+
})();
|
|
4619
|
+
|
|
4250
4620
|
// ../launchpad-engine/dist/per-app-workspace.js
|
|
4251
4621
|
var CF_ACCOUNT_ID = "c07cb17c9f742e8144c4828b3693ca65";
|
|
4252
4622
|
var R2_S3_ENDPOINT = `https://${CF_ACCOUNT_ID}.r2.cloudflarestorage.com`;
|
|
@@ -11789,7 +12159,7 @@ function buildManifest(inputs, detected) {
|
|
|
11789
12159
|
catalogue: { listed: false },
|
|
11790
12160
|
access
|
|
11791
12161
|
};
|
|
11792
|
-
const resolvedAuth = inputs.auth ??
|
|
12162
|
+
const resolvedAuth = inputs.auth ?? "gateway";
|
|
11793
12163
|
if (resolvedAuth === "gateway") {
|
|
11794
12164
|
manifest.auth = "gateway";
|
|
11795
12165
|
}
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export interface InitInputs {
|
|
|
10
10
|
type: AppType | undefined;
|
|
11
11
|
/**
|
|
12
12
|
* Edge gating mode (sp-gwdflt). Supplied via `--auth`. Undefined ⇒
|
|
13
|
-
* the buildManifest default: `gateway` for
|
|
14
|
-
*
|
|
13
|
+
* the buildManifest default: `gateway` for every shape, containers
|
|
14
|
+
* included (AIO-811).
|
|
15
15
|
*/
|
|
16
16
|
auth: AuthMode | undefined;
|
|
17
17
|
/** New apps choose dispatch explicitly; existing manifests keep their origin. */
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.
|
|
1
|
+
export declare const CLI_VERSION = "0.66.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -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.
|
|
4
|
+
version: 0.66.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-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.
|
|
4
|
+
version: 0.66.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -178,7 +178,7 @@ Do not use this command to migrate a registered app or delete its Pages resource
|
|
|
178
178
|
If the pool is full or inventory cannot be read, report the capacity error; do not fall back to Pages or change authentication to bypass it.
|
|
179
179
|
Do not replace that manifest with a v1 example or add `--origin pages` just because an older app uses Pages.
|
|
180
180
|
Use `--origin pages` only when Pages is deliberately required.
|
|
181
|
-
Containers keep their existing deployment path.
|
|
181
|
+
Containers keep their existing deployment path, with one change: use `auth: gateway` for them (see "Container apps" below).
|
|
182
182
|
Dispatch supports plain-text environment variables and `secret_text` env vars; after the first deploy is delivered, `launchpad secrets push` sets the secret values, live immediately with no redeploy.
|
|
183
183
|
|
|
184
184
|
This is interactive by default. It will ask for:
|
|
@@ -230,7 +230,7 @@ commit to a slug:
|
|
|
230
230
|
| `static` | n/a | n/a | n/a | No bundler, no TS, no React. |
|
|
231
231
|
| `react` | n/a | client-side or remote-fetch only | n/a | SPA only — no server-side anything. |
|
|
232
232
|
| `react+api` | `hono` (exclusive) | Cloudflare D1 / Neon (HTTP) / KV / R2 | **Sibling cron Worker** (Pages has no `scheduled` handler — see "Two-tier apps" below) | Requires `compatibility_flags = ["nodejs_compat"]` in `wrangler.toml` (ADR-0011 carve-out). |
|
|
233
|
-
| `container` | any HTTP server | any (container-local or HTTP backend) | container-local | Single HTTP port.
|
|
233
|
+
| `container` | any HTTP server | any (container-local or HTTP backend) | container-local | Single HTTP port. Use `auth: gateway` (default for containers; see "Container apps" below). The rest of the guided flow in this skill covers `static`/`react`/`react+api`. |
|
|
234
234
|
|
|
235
235
|
**Will not run on the Workers runtime** (advisory — representative,
|
|
236
236
|
not exhaustive): `fastify` / `@fastify/*` / `express` / `koa` /
|
|
@@ -354,6 +354,39 @@ endpoint. Files outside the manifest's app boundary (`app.root` /
|
|
|
354
354
|
`app.include`) and never-shippable files (private keys, `.env`
|
|
355
355
|
material) are stripped with a warning before upload.
|
|
356
356
|
|
|
357
|
+
### Container apps — use `auth: gateway` (AIO-811)
|
|
358
|
+
|
|
359
|
+
Container apps (`type: container`) sign in through the same Entra auth gateway as Pages apps. `launchpad init` scaffolds new container apps with `auth: gateway` by default; keep it. Pass `--auth access` only for a legacy Access app.
|
|
360
|
+
|
|
361
|
+
Request path: user → Entra auth gateway → `launchpad-container-router` (one service binding per app) → the app's container Worker.
|
|
362
|
+
The container Worker has no public hostname and no Cloudflare Access application.
|
|
363
|
+
It verifies the gateway's `X-Launchpad-User-Assertion` and fails closed with 401 when the assertion is missing or invalid.
|
|
364
|
+
|
|
365
|
+
```yaml
|
|
366
|
+
apiVersion: launchpad.m-kopa.us/v1alpha1
|
|
367
|
+
kind: App
|
|
368
|
+
metadata:
|
|
369
|
+
name: my-container-app
|
|
370
|
+
team: my-team
|
|
371
|
+
owner: owner@m-kopa.com
|
|
372
|
+
deployment:
|
|
373
|
+
type: container
|
|
374
|
+
auth: gateway
|
|
375
|
+
catalogue:
|
|
376
|
+
listed: false
|
|
377
|
+
access:
|
|
378
|
+
allowed_entra_group: G_my_group # or omit and use a private audience
|
|
379
|
+
containers:
|
|
380
|
+
- name: my-container-app
|
|
381
|
+
image: ghcr.io/m-kopa/my-container-app:1.0.0
|
|
382
|
+
port: 8080
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
`auth: access` for containers is **legacy**. Every Access-gated app uses a Cloudflare Zero Trust seat and the account has a fixed seat cap (about 50 seats; AIO-811 was prompted by hitting it), so new Access apps can be refused once the cap is reached.
|
|
386
|
+
Only keep `auth: access` on an existing container app that has not been migrated.
|
|
387
|
+
Do not switch an existing container between modes on your own; offer `/launchpad-report` so the platform team can migrate it.
|
|
388
|
+
Reading the signed-in user inside the container is covered in `/launchpad-identity`.
|
|
389
|
+
|
|
357
390
|
**First deploy vs subsequent deploys** (M-1234). Under Model A there
|
|
358
391
|
is no separate "create the app" step — `launchpad deploy` against a
|
|
359
392
|
fresh slug auto-provisions:
|
|
@@ -363,7 +396,7 @@ fresh slug auto-provisions:
|
|
|
363
396
|
workflow (TF apply, cert issuance, edge auth, app-repo scaffold).
|
|
364
397
|
Pages manifests must explicitly set `auth: gateway` to use the platform Entra-OIDC gateway.
|
|
365
398
|
`launchpad init` writes this field. Pages apps cannot opt into Cloudflare Access; only
|
|
366
|
-
containers retain that login mode. The CLI prints `✓ First-time deploy — provisioning
|
|
399
|
+
containers retain that legacy login mode, and container apps should use `auth: gateway` too (AIO-811). The CLI prints `✓ First-time deploy — provisioning
|
|
367
400
|
workflow started` and waits for delivery. If its wait expires, check
|
|
368
401
|
`launchpad status <slug>` before reporting success. Provisioning typically takes
|
|
369
402
|
5–10 minutes. **Your bundle ships with the provisioning run** — when
|
|
@@ -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.
|
|
4
|
+
version: 0.66.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.
|
|
4
|
+
version: 0.66.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.
|
|
4
|
+
version: 0.66.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -198,6 +198,16 @@ if (me.authenticated) {
|
|
|
198
198
|
}
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
+
## Container apps (`type: container`, `auth: gateway`)
|
|
202
|
+
|
|
203
|
+
Container apps behind the gateway (AIO-811) get the same signed-in identity. The path is gateway → `launchpad-container-router` → your app's container Worker, and the gateway still sends `X-Launchpad-User-Assertion`.
|
|
204
|
+
|
|
205
|
+
- **The container Worker verifies the assertion for you.** The generated `container-worker.ts` reads `X-Launchpad-User-Assertion`, verifies it with `@m-kopa/platform-auth` against `GATEWAY_ISSUER`, `GATEWAY_AUD` and `GATEWAY_JWKS_URL`, and returns 401 if it is missing or invalid. It is the only gate, because the container has no public hostname and no Cloudflare Access app.
|
|
206
|
+
- **Your container process receives plain headers after that check:** `X-User-Sub` (Entra `oid`) and `X-User-Email` (tenant UPN), plus `X-User-Groups` when groups are present. Inbound `X-User-*` headers are stripped before the Worker sets its own, so a client cannot forge them.
|
|
207
|
+
- **Do not re-verify in the container unless you need to.** The raw assertion is checked at the container Worker and your process sees only the `X-User-*` headers.
|
|
208
|
+
- Same rules as Pages: identity is for attribution, not authorization. Enforce who may open the app through `access.allowed_entra_group`.
|
|
209
|
+
- The legacy `auth: access` container mode (Cloudflare Access JWT, `CF_ACCESS_*`) still works for existing apps but is limited by the Zero Trust seat cap. New container apps should use `auth: gateway`.
|
|
210
|
+
|
|
201
211
|
## The two app shapes
|
|
202
212
|
|
|
203
213
|
### (a) build-less `type: static` — vendored verifier (INTERIM)
|
|
@@ -323,6 +333,7 @@ An unverified payload can be forged by anyone who can reach your origin
|
|
|
323
333
|
- Do **not** confuse `launchpad whoami` (your CLI session) with in-app
|
|
324
334
|
identity. This skill is about the **end-user signed into your deployed
|
|
325
335
|
app**, read server-side from the forwarded token.
|
|
336
|
+
- Do **not** read identity from `X-User-*` headers in a container unless `auth: gateway` or `auth: access` is set on the app; they are only trustworthy because the container Worker verified the token first.
|
|
326
337
|
- Do **not** trust the header without verifying the signature, and do **not**
|
|
327
338
|
omit the audience check — both open you to forgery / cross-app replay.
|
|
328
339
|
- Do **not** gate access on `/api/me` claims — they are attribution, not
|
|
@@ -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.
|
|
4
|
+
version: 0.66.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.
|
|
4
|
+
version: 0.66.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.
|
|
4
|
+
version: 0.66.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|