@auth-craft/aws-cf-stack 1.3.0 → 1.6.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/.env.example CHANGED
@@ -46,6 +46,13 @@ LAMBDA_APP_BASE_PATH= # recommended — set a stable, non-guessable valu
46
46
  # changes between deploys, so clients/workers don't break.
47
47
 
48
48
  # ─── Cloudflare gateway workers ──────────────────────────────────────────────
49
+ # Deploy model: 'multi' (default — 3 workers, one scope each; AUTH_SCOPE fixed per worker,
50
+ # so scope is ENFORCED) or 'shared' (1 worker that derives scope from the path's first
51
+ # segment). Shared is simpler (1 deploy/binding) but does NOT enforce scope on its own —
52
+ # pair it with a scope-locked Pages proxy (@auth-craft/pages-auth-proxy `scope` option) if
53
+ # customer apps must never reach system/tenant. In shared mode CORS unions all scopes and
54
+ # the CORS_KV uses CF_CORS_KV_ID_SHARED (or falls back to CF_CORS_KV_ID_CUSTOMER).
55
+ CF_GATEWAY_MODE=multi # multi | shared
49
56
  CLOUDFLARE_API_TOKEN= # required for gateway (flag --cf-api-token)
50
57
  CLOUDFLARE_ACCOUNT_ID= # required for gateway (flag --cf-account-id)
51
58
  CF_EDGE_GATE_SECRET= # required for gateway (flag --edge-gate-secret) — Client → Worker gate
@@ -84,6 +91,15 @@ CF_WORKER_DISABLE_PUBLIC_URL= # shared default (e.g. true to disable al
84
91
  CF_WORKER_DISABLE_PUBLIC_URL_SYSTEM= # e.g. true — admin app via Pages proxy
85
92
  CF_WORKER_DISABLE_PUBLIC_URL_TENANT=
86
93
  CF_WORKER_DISABLE_PUBLIC_URL_CUSTOMER=
94
+ # Skip the X-Edge-Gate (Client → Worker) check. Redundant when the worker has NO public
95
+ # URL and is reached only via a Service Binding (Pages proxy) — so it's ONLY honored when
96
+ # CF_WORKER_DISABLE_PUBLIC_URL is also set for that scope (else ignored with a warning).
97
+ # Does NOT affect the Worker → Backend secret (X-Gateway-Secret stays required). Truthy:
98
+ # 1/true/yes. When skipped, EDGE_GATE_SECRET is not needed.
99
+ CF_WORKER_SKIP_EDGE_GATE= # shared default
100
+ CF_WORKER_SKIP_EDGE_GATE_SYSTEM= # e.g. true (pairs with DISABLE_PUBLIC_URL_SYSTEM)
101
+ CF_WORKER_SKIP_EDGE_GATE_TENANT=
102
+ CF_WORKER_SKIP_EDGE_GATE_CUSTOMER=
87
103
 
88
104
  # ─── Internal service JWT (optional — enables /internal/* + service worker verify) ─
89
105
  LAMBDA_INTERNAL_JWT_PUBLIC_KEY= # optional — also fed to workers as SERVICE_JWT_PUBLIC_KEY
@@ -149,8 +165,10 @@ LAMBDA_FACEBOOK_CLIENT_ID=
149
165
  LAMBDA_FACEBOOK_CLIENT_SECRET=
150
166
  LAMBDA_FACEBOOK_REDIRECT_URI=
151
167
 
152
- # ─── Super-admin bootstrap (optional — skipped if any are unset) ─────────────
168
+ # ─── Super-admin bootstrap (optional — the `admin` stage is skipped unless BOTH ───
169
+ # EMAIL and PASSWORD are set; the skip message names which are missing). The admin CLI
170
+ # talks to DynamoDB DIRECTLY and is idempotent: if an active owner already exists in the
171
+ # system tenant it's a no-op, so re-runs are safe. No setup token is needed.
153
172
  LAMBDA_SUPER_ADMIN_EMAIL=
154
173
  LAMBDA_SUPER_ADMIN_PASSWORD=
155
174
  LAMBDA_SUPER_ADMIN_NAME=Super Admin
156
- LAMBDA_AUTH_SETUP_TOKEN=
package/README.md CHANGED
@@ -17,7 +17,9 @@ or building the auth-craft repo** — just pass env and run.
17
17
 
18
18
  1. **DynamoDB + Lambda** (CDK) — the auth backend. Emits a Function URL + a base path.
19
19
  2. **Super-admin** (idempotent) — bootstraps the `__system__` tenant owner.
20
- 3. **3 gateway workers** (system / tenant / customer) on Cloudflare, via wrangler.
20
+ 3. **Gateway worker(s)** on Cloudflare, via wrangler. Default `multi` = **3 workers**
21
+ (system / tenant / customer); `CF_GATEWAY_MODE=shared` = **1 worker**. See
22
+ [Deploy model](#deploy-model-multi-vs-shared).
21
23
 
22
24
  The gateway's `BACKEND_URL` is computed as `FunctionURL + ApiBasePath` read back from the
23
25
  **live CDK outputs** of the same deploy — so the worker config can never drift from the
@@ -33,18 +35,38 @@ Everything is named from `--stage` (`dev|staging|prod`) and `--project` (default
33
35
  |----------|------|
34
36
  | CDK stacks | `<app>-<stage>-data`, `<app>-<stage>-api` |
35
37
  | Lambda fn + DynamoDB table | `<app>-<stage>` |
36
- | Gateway workers | `<app>-<stage>-system` / `-tenant` / `-customer` |
38
+ | Gateway workers (`multi`) | `<app>-<stage>-system` / `-tenant` / `-customer` |
39
+ | Gateway worker (`shared`) | `<app>-<stage>` |
37
40
 
38
41
  So `outputs`/`gateway` look up the api stack `<app>-<stage>-api`. The CDK output keys are
39
42
  `LambdaFunctionUrl`, `ApiBasePath`, `AuthSystemName`, `DynamoDBTableName`.
40
43
 
44
+ ### Deploy model (multi vs shared)
45
+
46
+ `CF_GATEWAY_MODE` picks how the gateway is deployed:
47
+
48
+ - **`multi`** (default) — 3 workers, one scope each. Each worker has a fixed `AUTH_SCOPE`,
49
+ so the scope is **enforced server-side**: a customer worker can never act as system,
50
+ whatever path the client sends. Strongest isolation; 3 deploys + 3 Service Bindings.
51
+ - **`shared`** — 1 worker. Scope is derived from the path's first segment
52
+ (`/system/...` → `system`), so the worker by itself does **not** enforce scope (a client
53
+ could send `/system/...`). Simpler: 1 deploy, 1 binding, unioned CORS. **Pair it with a
54
+ scope-locked Pages proxy** (`createPagesAuthProxy({ scope: 'customer' })` from
55
+ [@auth-craft/pages-auth-proxy](../pages-auth-proxy/README.md)) so the *proxy* prepends
56
+ the scope and the client never picks it — that restores the "customer can't reach
57
+ system" guarantee at the edge.
58
+
59
+ In `shared` mode CORS unions all per-scope origins/suffixes, and the dynamic-CORS KV uses
60
+ `CF_CORS_KV_ID_SHARED` (falling back to `CF_CORS_KV_ID_CUSTOMER`). `outputs.workerNames.*`
61
+ all point at the single worker.
62
+
41
63
  ## Commands
42
64
 
43
65
  | Command | Does |
44
66
  |---------|------|
45
67
  | `lambda` | Deploy the DynamoDB + Lambda CDK stacks. `--outputs-file <p>` writes the raw CDK outputs JSON. Runs a best-effort `/health` check. |
46
- | `admin` | Create the super-admin (idempotent; **skips** if `LAMBDA_SUPER_ADMIN_*` / `LAMBDA_AUTH_SETUP_TOKEN` unset; "already exists" = success). |
47
- | `gateway` | Deploy the 3 gateway workers. Re-reads the Lambda outputs from CloudFormation if run as a separate invocation. |
68
+ | `admin` | Create the super-admin (idempotent; **skips** if `LAMBDA_SUPER_ADMIN_EMAIL`/`LAMBDA_SUPER_ADMIN_PASSWORD` unset; "already exists" = success). |
69
+ | `gateway` | Deploy the gateway worker(s) — 3 (`multi`, default) or 1 (`CF_GATEWAY_MODE=shared`). Re-reads the Lambda outputs from CloudFormation if run as a separate invocation. |
48
70
  | `all` | `lambda` → `admin` → `gateway`, one run. |
49
71
  | `outputs` | Print the live api-stack outputs as JSON (no deploy): `backendUrl`, `apiBasePath`, `dynamodbTable`, and the auth keys read **from the live Lambda config** (`jwtPublicKey`, `jwtIssuer`, `jwtAlgorithm`, `serviceJwtPublicKey`) + `gatewayUrls`/`routePrefixes`. Self-contained — works standalone (no env), so an orchestrator can auto-fill its `*_AUTH_*` vars. |
50
72
 
@@ -76,7 +98,7 @@ Inputs come from **flags or environment variables** (flag wins, then env, then
76
98
  | Stage | Required |
77
99
  |-------|----------|
78
100
  | `lambda` | `LAMBDA_JWT_PRIVATE_KEY`, `LAMBDA_JWT_PUBLIC_KEY`, and `LAMBDA_GATEWAY_SECRET` (required for staging/prod; optional in dev). AWS creds in env. |
79
- | `admin` | `LAMBDA_SUPER_ADMIN_EMAIL`, `LAMBDA_SUPER_ADMIN_PASSWORD`, `LAMBDA_AUTH_SETUP_TOKEN` (else skipped). AWS creds. |
101
+ | `admin` | `LAMBDA_SUPER_ADMIN_EMAIL`, `LAMBDA_SUPER_ADMIN_PASSWORD` (else skipped). AWS creds. |
80
102
  | `gateway` | Hard-required: `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`. Needed in practice (pushed as worker secrets; empty ones are skipped but the gateway then rejects clients / can't reach the backend): `CF_EDGE_GATE_SECRET`, `LAMBDA_GATEWAY_SECRET`, `LAMBDA_JWT_PUBLIC_KEY`. |
81
103
 
82
104
  Set `LAMBDA_APP_BASE_PATH` to a **stable** value — if unset, a random base path is generated
@@ -230,6 +252,16 @@ domain). This applies only when no `CF_WORKER_CUSTOM_DOMAIN_<SCOPE>` is set for
230
252
  if you're using `AUTH_GATEWAY_URL` (no binding) leave it off, since that path needs the
231
253
  public URL.
232
254
 
255
+ **Skip the edge gate (optional).** The `X-Edge-Gate` secret exists to stop the public
256
+ internet from hitting the worker. Once the public URL is disabled and the worker is
257
+ reached only via a Service Binding, that gate is redundant — set
258
+ `CF_WORKER_SKIP_EDGE_GATE=true` (or per scope) to drop it; the Pages proxy then needs no
259
+ `EDGE_GATE_SECRET`. For safety it's **only honored when the public URL is also disabled**
260
+ for that scope (otherwise ignored with a warning). It does **not** touch the
261
+ Worker → Backend secret: `X-Gateway-Secret` stays required because the backend (Lambda
262
+ Function URL) is still on the public internet — without it, anyone could call the Lambda
263
+ directly.
264
+
233
265
  ### Per-scope token transport
234
266
 
235
267
  `REFRESH_TOKEN_STRATEGY` sets the transport for every scope. To mix, use the per-scope