@m-kopa/launchpad-cli 0.31.0 → 0.32.1
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 +28 -0
- package/dist/cli.js +2 -1
- package/dist/commands/skills.d.ts +7 -0
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/skills/README.md +27 -0
- package/skills/launchpad-content-pr/SKILL.md +1 -1
- package/skills/launchpad-deploy/SKILL.md +1 -1
- package/skills/launchpad-deploy-status/SKILL.md +1 -1
- package/skills/launchpad-destroy/SKILL.md +1 -1
- package/skills/launchpad-identity/SKILL.md +307 -0
- package/skills/launchpad-onboard/SKILL.md +1 -1
- package/skills/launchpad-status/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,34 @@ 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.32.1 — 2026-06-17
|
|
10
|
+
|
|
11
|
+
Fix: `launchpad skills install/update` now installs `launchpad-identity`. The
|
|
12
|
+
0.32.0 skill shipped as a `skills/` directory but was missing from the
|
|
13
|
+
installer's `BUNDLED_SKILLS` list, so it was never copied into
|
|
14
|
+
`~/.claude/skills/`. Added it, and added an anti-drift test that fails if any
|
|
15
|
+
`launchpad-*` skill directory is missing from `BUNDLED_SKILLS` (so this can't
|
|
16
|
+
recur). Re-run `launchpad update` (or `launchpad skills update`) to pick it up.
|
|
17
|
+
|
|
18
|
+
## 0.32.0 — 2026-06-17
|
|
19
|
+
|
|
20
|
+
New Claude Code skill: `/launchpad-identity` (sp-idnt7k). Teaches app authors how
|
|
21
|
+
to read the signed-in user's identity inside a Launchpad app — verify the
|
|
22
|
+
gateway-forwarded `X-Launchpad-User-Assertion` in a Pages Function with
|
|
23
|
+
`@m-kopa/platform-auth` (RS256 signature + issuer + audience pinned to the app's
|
|
24
|
+
own host), **fail closed**, and read `{ sub, email, name }`. The first
|
|
25
|
+
how-to-build (dev) skill, distinct from the CLI-verb operator skills.
|
|
26
|
+
|
|
27
|
+
- Covers both app shapes: build-less `type: static` (vendored verifier, documented
|
|
28
|
+
as interim with a staleness warning + a pointer to the maintained-build
|
|
29
|
+
follow-up) and `react+api` (`import @m-kopa/platform-auth`).
|
|
30
|
+
- Teaches the `secret_text` env requirement (`GATEWAY_ISSUER` / `GATEWAY_AUD` /
|
|
31
|
+
`GATEWAY_JWKS_URL`) and the plain-text → deploy-wipe → fail-closed-anonymous
|
|
32
|
+
failure mode; warns against unsigned `decodeJwtPayload` token-reading.
|
|
33
|
+
- Examples are extracted from the deployed, verified peoplemgr app and pinned by a
|
|
34
|
+
drift-guard test so they cannot silently diverge from the live contract.
|
|
35
|
+
- Docs: extends the Auth & identity concept page; no new CLI command.
|
|
36
|
+
|
|
9
37
|
## 0.31.0 — 2026-06-17
|
|
10
38
|
|
|
11
39
|
Telemetry: repoint at a fresh PostHog project (sp-phcli2). The prior project was
|
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.32.1";
|
|
23
23
|
|
|
24
24
|
// src/config.ts
|
|
25
25
|
import * as os from "node:os";
|
|
@@ -10260,6 +10260,7 @@ var BUNDLED_SKILLS = [
|
|
|
10260
10260
|
"launchpad-content-pr",
|
|
10261
10261
|
"launchpad-status",
|
|
10262
10262
|
"launchpad-destroy",
|
|
10263
|
+
"launchpad-identity",
|
|
10263
10264
|
"marquee-share"
|
|
10264
10265
|
];
|
|
10265
10266
|
function isBundleManaged(name) {
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { Command } from "../dispatcher.js";
|
|
2
|
+
/**
|
|
3
|
+
* Skills shipped in this version of the CLI. Must match the `launchpad-*`
|
|
4
|
+
* dirs under `<pkg>/skills/` (plus the cross-repo `marquee-share`); the
|
|
5
|
+
* "bundled list matches the skills dir" test in `tests/skills-command.test.ts`
|
|
6
|
+
* enforces this so a newly-added skill dir can't silently go un-installed.
|
|
7
|
+
*/
|
|
8
|
+
export declare const BUNDLED_SKILLS: readonly ["launchpad-onboard", "launchpad-deploy", "launchpad-deploy-status", "launchpad-content-pr", "launchpad-status", "launchpad-destroy", "launchpad-identity", "marquee-share"];
|
|
2
9
|
export declare const skillsCommand: Command;
|
|
3
10
|
interface InstallEnv {
|
|
4
11
|
readonly bundleDir: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/commands/skills.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/commands/skills.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;AASjE;;;;;GAKG;AACH,eAAO,MAAM,cAAc,uLAajB,CAAC;AAcX,eAAO,MAAM,aAAa,EAAE,OAK3B,CAAC;AA2CF,UAAU,UAAU;IAClB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,UAAU,CAQ9C"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.
|
|
1
|
+
export declare const CLI_VERSION = "0.32.1";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
package/skills/README.md
CHANGED
|
@@ -15,11 +15,38 @@ skills/
|
|
|
15
15
|
├── launchpad-content-pr/SKILL.md
|
|
16
16
|
├── launchpad-status/SKILL.md
|
|
17
17
|
├── launchpad-destroy/SKILL.md
|
|
18
|
+
├── launchpad-identity/SKILL.md # dev / how-to-build skill (see below)
|
|
18
19
|
└── marquee-share/ # vendored from M-KOPA/marquee, do
|
|
19
20
|
# not hand-edit — managed by
|
|
20
21
|
# scripts/sync-marquee-share-skill.sh
|
|
21
22
|
```
|
|
22
23
|
|
|
24
|
+
## Two kinds of skill: operator vs dev (how-to-build)
|
|
25
|
+
|
|
26
|
+
Most skills here are **operator skills** — they wrap a `launchpad` CLI verb
|
|
27
|
+
and walk the user through running it (`launchpad-deploy`, `-status`,
|
|
28
|
+
`-content-pr`, `-destroy`, `-onboard`). Their bash blocks invoke the CLI, so
|
|
29
|
+
they live under the Shell Contract.
|
|
30
|
+
|
|
31
|
+
`launchpad-identity` is the first **dev / how-to-build skill**: it teaches an
|
|
32
|
+
app author how to write *application code* (here: verifying the
|
|
33
|
+
gateway-forwarded identity in a Pages Function), not how to run a CLI verb. The
|
|
34
|
+
convention for this kind:
|
|
35
|
+
|
|
36
|
+
- It still ships through `launchpad skills install` and is registered in the
|
|
37
|
+
same gate enumerations (`tests/skill-contract.test.ts`,
|
|
38
|
+
`check-skill-bash-dialect.sh`, `check-skill-bash-parse.sh`,
|
|
39
|
+
`sync-skill-contract.sh`) so `version:` lock-step + the Shell Contract apply.
|
|
40
|
+
- Its code examples are **extracted from a deployed, verified app** (not
|
|
41
|
+
transcribed) and pinned by a drift-guard test
|
|
42
|
+
(`tests/skill-identity-extraction.test.ts`), because a how-to-build skill
|
|
43
|
+
that teaches a security path must not drift from the live contract.
|
|
44
|
+
- It stays a **thin pointer to one source of truth** (the contract doc +
|
|
45
|
+
`@m-kopa/platform-auth` + the verified example), so it can't fork into a
|
|
46
|
+
divergent copy.
|
|
47
|
+
|
|
48
|
+
The next dev-skill should follow this shape.
|
|
49
|
+
|
|
23
50
|
## Cross-platform shell contract
|
|
24
51
|
|
|
25
52
|
Claude Code's `Bash` tool **always** runs `bash`, on every OS — on
|
|
@@ -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.32.1
|
|
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.32.1
|
|
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-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). 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", "my app says failed but it's serving", or after `/launchpad-deploy` reports a non-`done` terminal stage.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.32.1
|
|
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.32.1
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: launchpad-identity
|
|
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.32.1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
8
|
+
## Shell contract — read this first
|
|
9
|
+
|
|
10
|
+
Every fenced `bash` block below MUST be sent to the `Bash` tool **verbatim**.
|
|
11
|
+
Do not rewrite into PowerShell, cmd, zsh-isms, or "equivalent" forms.
|
|
12
|
+
|
|
13
|
+
- macOS / Linux: the `Bash` tool runs system bash.
|
|
14
|
+
- Windows: the `Bash` tool runs Git for Windows (MSYS) bash. `$HOME`,
|
|
15
|
+
forward slashes, `test -f`, `command -v`, heredocs, and `[[ … ]]` all
|
|
16
|
+
work. There is no reason to translate to `Test-Path`, `$env:USERPROFILE`,
|
|
17
|
+
`Get-Content`, `Where-Object`, `Get-ChildItem`, or backslash paths —
|
|
18
|
+
doing so will fail with `/usr/bin/bash: syntax error`.
|
|
19
|
+
|
|
20
|
+
If a step genuinely needs OS branching, branch *inside* bash:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
case "$(uname -s)" in
|
|
24
|
+
Darwin) : ;;
|
|
25
|
+
Linux) : ;;
|
|
26
|
+
MINGW*|MSYS*|CYGWIN*) : ;;
|
|
27
|
+
esac
|
|
28
|
+
```
|
|
29
|
+
<!-- END shell-contract -->
|
|
30
|
+
|
|
31
|
+
# /launchpad-identity
|
|
32
|
+
|
|
33
|
+
How an app author reads **who is signed in** inside a Launchpad app, the
|
|
34
|
+
right way: verify the gateway-forwarded identity token and fail closed.
|
|
35
|
+
|
|
36
|
+
This is a **how-to-build** skill — it writes app code, it does not wrap a
|
|
37
|
+
`launchpad` CLI verb. (`launchpad whoami` reports your *CLI session*, not the
|
|
38
|
+
end-user signed into your deployed app — different thing; see § Don'ts.)
|
|
39
|
+
|
|
40
|
+
## What you get, in one sentence
|
|
41
|
+
|
|
42
|
+
A Launchpad app behind the Entra-OIDC gateway receives the signed-in user's
|
|
43
|
+
**already-verified** identity as a signed token on every request; your app
|
|
44
|
+
verifies that token and reads `{ sub, email, name }`. That is the whole job.
|
|
45
|
+
|
|
46
|
+
## Prerequisite — you need a Pages Function (not bare static HTML)
|
|
47
|
+
|
|
48
|
+
**Bare static HTML cannot read request headers**, so it cannot see the
|
|
49
|
+
identity token. To read identity your app needs a server-side **Pages
|
|
50
|
+
Function** (a file under `functions/`). Two app shapes qualify:
|
|
51
|
+
|
|
52
|
+
- **`type: static` with a `functions/` directory** — a build-less static app
|
|
53
|
+
that still ships Pages Functions (this is what peoplemgr does).
|
|
54
|
+
- **`react+api`** (or any app with a build) — your `/api/*` is already a
|
|
55
|
+
Pages Function.
|
|
56
|
+
|
|
57
|
+
If your app is pure HTML/CSS/JS with no `functions/`, you must add a Pages
|
|
58
|
+
Function first. There is no client-only way to read the verified identity.
|
|
59
|
+
|
|
60
|
+
## The contract (read this before copying code)
|
|
61
|
+
|
|
62
|
+
The platform gateway authenticates the user against Entra, then forwards its
|
|
63
|
+
**own RS256-signed user token** to your origin under a dedicated header:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
X-Launchpad-User-Assertion: <RS256 JWT minted by the gateway>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The canonical contract — claims, trust model, the header name — lives in ONE
|
|
70
|
+
place: **`docs/identity-forwarding-to-confined-origins.md`** (sp-xush3d) in
|
|
71
|
+
the launchpad-platform repo. This skill is the worked example; that doc is
|
|
72
|
+
the source of truth. If they ever disagree, the contract doc wins.
|
|
73
|
+
|
|
74
|
+
> **Trust by SIGNATURE, not by network position.** Do **not** trust the
|
|
75
|
+
> header just because it arrived. Cloudflare's origin-confinement locks the
|
|
76
|
+
> origin, but it is *punctured* by the unauthenticated `/.well-known/*` path
|
|
77
|
+
> and any future non-gateway route. The only thing that makes the token
|
|
78
|
+
> trustworthy is that **you verified its RS256 signature** against the
|
|
79
|
+
> gateway JWKS. A forged header on a bypass path fails verification — and you
|
|
80
|
+
> reject it. That is why a signed token exists instead of a plain header.
|
|
81
|
+
|
|
82
|
+
### The two things you MUST get right
|
|
83
|
+
|
|
84
|
+
1. **Verify, never decode-and-trust.** Use `@m-kopa/platform-auth`'s
|
|
85
|
+
`verifyAccessJwt` (it checks the RS256 signature, issuer, and audience).
|
|
86
|
+
**Never** read claims out of the token with an unsigned
|
|
87
|
+
`decodeJwtPayload` / base64-split — an unverified payload is attacker-controlled.
|
|
88
|
+
2. **Pin the audience to YOUR app's host.** Verification must require
|
|
89
|
+
`audience = <your app's hostname>`. If you omit the audience check or share
|
|
90
|
+
one audience across apps, a token minted for app A is **replayable** against
|
|
91
|
+
app B (cross-app token replay). Audience-pinning is what stops that.
|
|
92
|
+
|
|
93
|
+
## The canonical Pages Function — `functions/api/me.js`
|
|
94
|
+
|
|
95
|
+
Lifted from the **deployed-and-verified** peoplemgr app
|
|
96
|
+
(`repos/m-kopa-peoplemgr-hub/functions/api/me.js`). Copy it; change only the
|
|
97
|
+
app-specific bits called out below.
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
import { verifyAccessJwt, createJwksCache } from '../_lib/platform-auth.mjs';
|
|
101
|
+
|
|
102
|
+
const IDENTITY_HEADER = 'X-Launchpad-User-Assertion';
|
|
103
|
+
|
|
104
|
+
// Memoise the JWKS cache per isolate (env is only available per-request),
|
|
105
|
+
// keyed by URL so a config change rebuilds it.
|
|
106
|
+
let _jwks = null;
|
|
107
|
+
let _jwksUrl = null;
|
|
108
|
+
function jwksFor(url) {
|
|
109
|
+
if (!_jwks || _jwksUrl !== url) {
|
|
110
|
+
_jwks = createJwksCache(url);
|
|
111
|
+
_jwksUrl = url;
|
|
112
|
+
}
|
|
113
|
+
return _jwks;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function json(body) {
|
|
117
|
+
return new Response(JSON.stringify(body), {
|
|
118
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Fail-closed anonymous result. The shape your client gates on.
|
|
123
|
+
function anonymous() {
|
|
124
|
+
return json({ authenticated: false, email: null, name: null, sub: null });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function onRequestGet(context) {
|
|
128
|
+
const { request, env } = context;
|
|
129
|
+
|
|
130
|
+
const token = request.headers.get(IDENTITY_HEADER);
|
|
131
|
+
if (!token) return anonymous(); // no token → anonymous / public path
|
|
132
|
+
|
|
133
|
+
const issuer = env.GATEWAY_ISSUER; // gateway iss
|
|
134
|
+
const audience = env.GATEWAY_AUD; // this app's aud (pin to YOUR host)
|
|
135
|
+
const jwksUrl =
|
|
136
|
+
env.GATEWAY_JWKS_URL ||
|
|
137
|
+
(issuer ? `${issuer.replace(/\/$/, '')}/.well-known/jwks.json` : null);
|
|
138
|
+
|
|
139
|
+
// Misconfigured verify env → fail CLOSED. Never trust an unverifiable token.
|
|
140
|
+
if (!jwksUrl || !issuer || !audience) return anonymous();
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const user = await verifyAccessJwt(token, {
|
|
144
|
+
teamDomain: issuer, // iss
|
|
145
|
+
audience, // aud (this app) — the replay guard
|
|
146
|
+
jwks: jwksFor(jwksUrl),
|
|
147
|
+
});
|
|
148
|
+
return json({
|
|
149
|
+
authenticated: true,
|
|
150
|
+
sub: user.sub,
|
|
151
|
+
email: user.preferredUsername, // tenant-bound UPN (the verified identity)
|
|
152
|
+
name: user.name || null, // display name (absent → null; never gates)
|
|
153
|
+
});
|
|
154
|
+
} catch {
|
|
155
|
+
// Forged / expired / wrong-signature / wrong-aud / JWKS failure → anonymous,
|
|
156
|
+
// NEVER the (possibly forged) user. This is the load-bearing fail-closed.
|
|
157
|
+
return anonymous();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
What the verified `user` carries (from the contract doc):
|
|
163
|
+
|
|
164
|
+
| Field returned | From | Notes |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| `sub` | `user.sub` | Entra `oid` — stable identifier. |
|
|
167
|
+
| `email` | `user.preferredUsername` | Tenant-bound UPN — the stable email-equivalent. |
|
|
168
|
+
| `name` | `user.name` | Display-only (Entra `profile` scope). Fail-soft: `null` if absent — never gate on it. |
|
|
169
|
+
|
|
170
|
+
> **No groups, no authz here.** This mechanism forwards `sub`/`email`/`name`
|
|
171
|
+
> only — it is read-only identity for **attribution** (analytics, audit,
|
|
172
|
+
> personalisation), **not** an authorization input. Group membership is the
|
|
173
|
+
> gateway's **login-time snapshot**; never make an access decision from these
|
|
174
|
+
> values. Enforce authorization at the gateway (allowed groups) and
|
|
175
|
+
> server-side, not from `/api/me`.
|
|
176
|
+
|
|
177
|
+
## Reading it from the client
|
|
178
|
+
|
|
179
|
+
Your front-end calls `/api/me` and personalises on the result. Keep the same
|
|
180
|
+
fail-closed posture: treat `authenticated: false` as anonymous.
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
const me = await fetch('/api/me').then((r) => r.json());
|
|
184
|
+
if (me.authenticated) {
|
|
185
|
+
// show who's logged in
|
|
186
|
+
document.querySelector('#who').textContent = `${me.name ?? me.email}`;
|
|
187
|
+
// e.g. analytics identify — attribution only, never authorization
|
|
188
|
+
// posthog.identify(me.sub, { email: me.email, name: me.name });
|
|
189
|
+
} else {
|
|
190
|
+
// anonymous / public view
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## The two app shapes
|
|
195
|
+
|
|
196
|
+
### (a) build-less `type: static` — vendored verifier (INTERIM)
|
|
197
|
+
|
|
198
|
+
A build-less static app has **no `package.json` and no install step at
|
|
199
|
+
deploy**, so its Pages Functions cannot `import` an npm package by name. The
|
|
200
|
+
verifier is **pre-bundled** into a committed file and imported by relative
|
|
201
|
+
path — that is the `import … from '../_lib/platform-auth.mjs'` in the example
|
|
202
|
+
above. To produce that file, esbuild `@m-kopa/platform-auth` into a single
|
|
203
|
+
ESM bundle; the recipe lives in
|
|
204
|
+
`repos/m-kopa-peoplemgr-hub/functions/_lib/README.md`.
|
|
205
|
+
|
|
206
|
+
> ⚠️ **Vendoring is INTERIM — a pinned copy goes stale.** A hand-vendored
|
|
207
|
+
> `platform-auth.mjs` is frozen at the version you bundled. **A security fix
|
|
208
|
+
> to the verifier does NOT reach it** until someone re-bundles and redeploys.
|
|
209
|
+
> Pin the version in a `_lib/README.md`, watch `@m-kopa/platform-auth`
|
|
210
|
+
> releases, and re-vendor on a security advisory. The maintained replacement
|
|
211
|
+
> for this whole pattern is tracked as **sp-pubvf3** (publish a maintained
|
|
212
|
+
> static/Pages verifier build so a fix propagates via a version bump). Prefer
|
|
213
|
+
> it once it lands; until then, vendoring is the documented reality — with
|
|
214
|
+
> this staleness caveat attached.
|
|
215
|
+
|
|
216
|
+
### (b) `react+api` / any app with a build — import the package
|
|
217
|
+
|
|
218
|
+
If your app has a build step, drop the vendored file and import the package
|
|
219
|
+
directly:
|
|
220
|
+
|
|
221
|
+
```js
|
|
222
|
+
import { verifyAccessJwt, createJwksCache } from '@m-kopa/platform-auth';
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Everything else — the header, the verify call, the fail-closed shape, the env
|
|
226
|
+
vars — is identical to the example above.
|
|
227
|
+
|
|
228
|
+
## Required env vars — `secret_text`, MANDATORY
|
|
229
|
+
|
|
230
|
+
Verification needs three env vars on your app. **Set them as `secret_text`,
|
|
231
|
+
not plain text:**
|
|
232
|
+
|
|
233
|
+
| Var | Value | Notes |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| `GATEWAY_ISSUER` | `https://auth.launchpad.m-kopa.us` | the gateway token issuer (`iss`) |
|
|
236
|
+
| `GATEWAY_AUD` | `<your-slug>.launchpad.m-kopa.us` | **your app's** audience (`aud`) — the replay guard |
|
|
237
|
+
| `GATEWAY_JWKS_URL` | `https://auth.launchpad.m-kopa.us/.well-known/jwks.json` | optional — derived from `GATEWAY_ISSUER` if unset |
|
|
238
|
+
|
|
239
|
+
> ⚠️ **Why `secret_text` is mandatory — the wipe → fail-closed chain.** If you
|
|
240
|
+
> set these as **plain-text** env, the next `launchpad deploy` **wipes them**
|
|
241
|
+
> (only `secret_text` vars survive a deploy — the CF-Access-env-vars-must-ship
|
|
242
|
+
> runbook). With the env gone, `issuer`/`audience` are `undefined`, the
|
|
243
|
+
> `if (!jwksUrl || !issuer || !audience) return anonymous()` guard fires, and
|
|
244
|
+
> **every** user silently goes anonymous in production. The code is correct —
|
|
245
|
+
> it fails *closed*, never open — but identity is dark until you re-add the
|
|
246
|
+
> vars. Set them `secret_text` from the start and this never happens. Ship the
|
|
247
|
+
> env in the **same** deploy as the consumer, or verification is dark.
|
|
248
|
+
|
|
249
|
+
The canonical names are **`GATEWAY_ISSUER` / `GATEWAY_AUD` / `GATEWAY_JWKS_URL`**
|
|
250
|
+
— what the deployed app reads and what is live on Cloudflare. (You may see the
|
|
251
|
+
illustrative names `AUTH_ISS` / `APP_AUD` / `JWKS_URL` in older contract-doc
|
|
252
|
+
snippets — the deployed convention is the `GATEWAY_*` set; use it. Note: it is
|
|
253
|
+
`GATEWAY_AUD`, **not** `GATEWAY_AUDIENCE`.)
|
|
254
|
+
|
|
255
|
+
## Anti-pattern — do NOT do this
|
|
256
|
+
|
|
257
|
+
Some apps "read" the token by base64-decoding its payload without verifying
|
|
258
|
+
the signature:
|
|
259
|
+
|
|
260
|
+
```js
|
|
261
|
+
// ❌ WRONG — unsigned, attacker-controlled. Never do this.
|
|
262
|
+
// const payload = decodeJwtPayload(token); // no signature check!
|
|
263
|
+
// const email = payload.email; // trusting a forgeable claim
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
An unverified payload can be forged by anyone who can reach your origin
|
|
267
|
+
(including via the `/.well-known/*` bypass). **Always** `verifyAccessJwt`
|
|
268
|
+
(signature + issuer + audience) and **fail closed** on any error. If you find
|
|
269
|
+
`decodeJwtPayload` on an auth path, it is a bug — replace it with verification.
|
|
270
|
+
|
|
271
|
+
## Checklist before you ship
|
|
272
|
+
|
|
273
|
+
- [ ] App has a **Pages Function** (not bare static HTML).
|
|
274
|
+
- [ ] Reads `X-Launchpad-User-Assertion` and **verifies** with
|
|
275
|
+
`verifyAccessJwt` (signature + issuer + **audience pinned to your host**).
|
|
276
|
+
- [ ] **Fails closed** to anonymous on missing token, missing env, or any
|
|
277
|
+
verify error — never returns the unverified user.
|
|
278
|
+
- [ ] No unsigned `decodeJwtPayload` on the auth path.
|
|
279
|
+
- [ ] `GATEWAY_ISSUER` / `GATEWAY_AUD` / `GATEWAY_JWKS_URL` set as
|
|
280
|
+
**`secret_text`**, shipped in the **same** deploy.
|
|
281
|
+
- [ ] No authorization decision is made from `/api/me` — attribution only.
|
|
282
|
+
|
|
283
|
+
## Don'ts
|
|
284
|
+
|
|
285
|
+
- Do **not** confuse `launchpad whoami` (your CLI session) with in-app
|
|
286
|
+
identity. This skill is about the **end-user signed into your deployed
|
|
287
|
+
app**, read server-side from the forwarded token.
|
|
288
|
+
- Do **not** trust the header without verifying the signature, and do **not**
|
|
289
|
+
omit the audience check — both open you to forgery / cross-app replay.
|
|
290
|
+
- Do **not** gate access on `/api/me` claims — they are attribution, not
|
|
291
|
+
authorization. Authorization is the gateway's allowed-groups + your
|
|
292
|
+
server-side checks.
|
|
293
|
+
- Do **not** invent new claim names — `sub` / `email` (`preferredUsername`) /
|
|
294
|
+
`name` are the only fields this channel carries. Directory attributes
|
|
295
|
+
(jobTitle, department, …) are a separate, forthcoming channel (sp-dirat9) —
|
|
296
|
+
not available here yet.
|
|
297
|
+
|
|
298
|
+
## Related
|
|
299
|
+
|
|
300
|
+
- **`docs/identity-forwarding-to-confined-origins.md`** — the canonical
|
|
301
|
+
contract (claims, trust model, header). Source of truth.
|
|
302
|
+
- [Concepts → Auth & groups](https://get.launchpad.m-kopa.us/concepts/auth-groups)
|
|
303
|
+
— the gateway, the first-visit bounce, and `/api/me`.
|
|
304
|
+
- `repos/m-kopa-peoplemgr-hub/functions/api/me.js` — the deployed worked
|
|
305
|
+
example this skill is lifted from.
|
|
306
|
+
- **sp-pubvf3** — the maintained static verifier build that will retire
|
|
307
|
+
hand-vendoring.
|
|
@@ -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.32.1
|
|
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: Show whether a Launchpad app's local launchpad.yaml matches what's deployed, and read the deployed manifest. Wraps `launchpad pull` (fetch deployed YAML) and `launchpad status` (drift report). Use when someone says "is my app in sync", "what's deployed", "show drift", "/launchpad-status", "/launchpad-pull", or after `launchpad deploy` to verify the change landed.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.32.1
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|