@mriekki/project-forge 4.26.0 → 4.27.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.
Files changed (38) hide show
  1. package/assets/authentication/distribution.json +90 -0
  2. package/assets/components.json +18 -4
  3. package/assets/core/distribution.json +12 -0
  4. package/assets/deployment/distribution.json +292 -68
  5. package/assets/generated/ASSEMBLY.json +152 -6
  6. package/assets/generated/authentication/docs/AUTH_RECOVERY_RETENTION.md +101 -0
  7. package/assets/generated/authentication/docs/HOUSEHOLD_INFRASTRUCTURE_ADOPTION.md +55 -0
  8. package/assets/generated/authentication/docs/HOUSEHOLD_REGISTRY.md +246 -0
  9. package/assets/generated/authentication/docs/PROJECT_AUTH_REFERENCE.md +878 -0
  10. package/assets/generated/authentication/samples/ProjectForgeAuth.Api/Program.cs +40 -0
  11. package/assets/generated/authentication/samples/ProjectForgeAuth.Api/ProjectForgeAuth.Api.csproj +13 -0
  12. package/assets/generated/authentication/samples/ProjectForgeAuth.Api/wwwroot/auth.js +65 -0
  13. package/assets/generated/authentication/samples/ProjectForgeAuth.Api/wwwroot/index.html +44 -0
  14. package/assets/generated/authentication/src/ProjectForge.Auth/AdmissionPolicy.cs +167 -0
  15. package/assets/generated/authentication/src/ProjectForge.Auth/AuthenticationDiagnostics.cs +159 -0
  16. package/assets/generated/authentication/src/ProjectForge.Auth/ProjectForge.Auth.csproj +9 -0
  17. package/assets/generated/authentication/src/ProjectForge.Auth/PwaExistingProjectCompatibility.cs +64 -0
  18. package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ProjectAuthStore.cs +471 -0
  19. package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ProjectForge.Auth.Runtime.csproj +15 -0
  20. package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ProjectForgeAuthRuntimeExtensions.cs +472 -0
  21. package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ResendProjectInvitationDelivery.cs +105 -0
  22. package/assets/generated/authentication/src/ProjectForge.Transport/ProjectForge.Transport.csproj +11 -0
  23. package/assets/generated/authentication/src/ProjectForge.Transport/StrictPublicOriginTransport.cs +95 -0
  24. package/assets/generated/authentication/tools/household_connect.py +145 -0
  25. package/assets/generated/authentication/tools/project_auth_recovery.py +716 -0
  26. package/assets/generated/core/core/process/ENGINEERING_BACKLOG.md +150 -0
  27. package/assets/generated/core/core/skills/manage-backlog/SKILL.md +115 -0
  28. package/assets/generated/core/core/skills/review-user-feedback/SKILL.md +50 -0
  29. package/assets/generated/core/tools/setup_maintenance/maintenance.py +162 -6
  30. package/assets/generated/deployment/docs/RECOVERY_READINESS.md +103 -0
  31. package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_capture.py +164 -0
  32. package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_guard.py +225 -0
  33. package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_lifecycle.py +455 -0
  34. package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_retention.py +220 -0
  35. package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_schedule.py +489 -0
  36. package/package.json +1 -1
  37. package/src/cli.mjs +32 -3
  38. package/src/engine.mjs +87 -5
@@ -0,0 +1,878 @@
1
+ # Generated Project Auth API and Google/Resend setup
2
+
3
+ **Status:** reference consumer code, not a Production-qualified release.
4
+ The Authentication Kit is a separately managed component. An approved
5
+ `householdAuth` Bootstrap selection copies the tested
6
+ `samples/ProjectForgeAuth.Api` starter into a new Project's
7
+ `src/ProjectForgeAuth.Api` and leaves its pinned, reusable domain/runtime/
8
+ transport source under `tools/project-forge-auth-kit/`.
9
+
10
+ Normal Projects use the standard package profile and do not receive this
11
+ capability. Household-enabled Projects use the explicit internal package
12
+ profile `greenfield-household-project`. This is **not** a different
13
+ user-facing New Project start mode.
14
+
15
+ ## Separate application ownership
16
+
17
+ The host is a working ASP.NET Core 10 reference API using per-Project
18
+ SQLite, invite-only account admission, Google OIDC code + PKCE,
19
+ issuer/audience/state/nonce validation through ASP.NET middleware,
20
+ cryptographically random hashed invitation and opaque session tokens,
21
+ authenticated per-Project APIs, antiforgery checks, and strict exact
22
+ Caddy-to-application transport validation.
23
+
24
+ The application owns its account IDs, SQLite database, private
25
+ DataProtection key ring, sessions, initial admin, Google OAuth client,
26
+ and Resend sending key. The Household Registry serves only authorized
27
+ shared infrastructure references and Developer/household membership.
28
+ The Registry is not contacted during application login and does not
29
+ grant automatic access.
30
+
31
+ ## Required protected Project configuration
32
+
33
+ Use a protected configuration mechanism already supported by the Project.
34
+ Never commit these values to Git, `HOUSEHOLD_AUTH_SETUP.json`, chat, logs,
35
+ or GitHub Actions:
36
+
37
+ - `ProjectForge:Authentication:DbPath` — private, per-Project SQLite path
38
+ - `ProjectForge:Authentication:DataProtectionKeysPath` — persistent
39
+ owner-only directory for OIDC state/correlation and external cookies
40
+ - `ProjectForge:Authentication:Google:ClientId` — unique OAuth client ID
41
+ - `ProjectForge:Authentication:Google:ClientSecret` — private client secret, **or** `:ClientSecretFile` — absolute path to a protected secret file (never both)
42
+ - `ProjectForge:Authentication:PublicOrigin` — exact public HTTPS origin
43
+ - `ProjectForge:Authentication:TrustedProxyIp` — exact reviewed Caddy peer
44
+ - Optional `ProjectForge:Authentication:Resend:ApiKey` (or
45
+ `:ApiKeyFile`, an absolute protected secret-file path) and `:From`
46
+ — **both required together** for automatic invitation delivery with a
47
+ separately scoped Project email key and a verified sender address.
48
+ The inline and file forms must never be configured together.
49
+
50
+ The Google OAuth callback is
51
+ `https://<approved-public-origin>/signin-pf-google`.
52
+ The app's TLS, Caddy network, Cloudflare Tunnel and exact trusted-peer
53
+ reconciliation remain Deployment Kit operations. Do not trust a Docker
54
+ subnet, direct LAN requests, forged X-Forwarded headers or an
55
+ unreviewed proxy address.
56
+
57
+ Bootstrap writes a non-secret `HOUSEHOLD_AUTH_SETUP.json` with
58
+ `requires-reviewed-provider-bindings`. The application refuses startup
59
+ without a complete reviewed configuration. The reference backend is
60
+ built locally/CI with:
61
+
62
+ ```sh
63
+ dotnet restore src/ProjectForgeAuth.Api/ProjectForgeAuth.Api.csproj
64
+ dotnet build src/ProjectForgeAuth.Api/ProjectForgeAuth.Api.csproj --no-restore
65
+ ```
66
+
67
+ ## One-time administrator setup
68
+
69
+ Only from a protected, reviewed local host session, preconfigure
70
+ `PF_INITIAL_APP_ADMIN_SUBJECT` with the exact intended Google subject
71
+ and `PF_INITIAL_APP_ADMIN_EMAIL` with that verified identity's address,
72
+ then execute the host's `admin-init` command with the correct protected
73
+ per-Project database path. No public endpoint may create the first admin.
74
+ Repeated bootstrap is rejected; never infer administrator status from
75
+ Household Registry Developer membership.
76
+
77
+ ## Invitation flow
78
+
79
+ With a verified administrator session, the reference UI can create a
80
+ 48-hour one-time Project invitation for a Google email address.
81
+
82
+ With Project-specific Resend configuration, the API sends the token to
83
+ the invited address using the fixed HTTPS Resend API and a deterministic
84
+ hashed idempotency key. No invitation token is returned to the browser,
85
+ stored in the Registry, or placed in a URL. Provider failures return
86
+ generic failure; use separate reviewed retry/recovery processes.
87
+
88
+ Without a configured Resend sender, the administrator may receive the
89
+ one-time token through the secured authenticated API and share it
90
+ privately. The invited user enters the token in the browser form.
91
+ Google issuer, immutable subject and email verification are checked
92
+ before admitting the Project account. Email-only matching never grants
93
+ access. Sign-in creates an opaque seven-day Project session, not a
94
+ Registry session.
95
+
96
+ ## Disposable container deployment (review-only templates)
97
+
98
+ The Auth Kit now includes
99
+ `samples/ProjectForgeAuth.Api/Dockerfile` and
100
+ `deploy/disposable-project-auth.compose.yml`. They package the actual
101
+ reference API and attach it to an **already reviewed exact Caddy network**
102
+ without publishing any host port or changing DNS, Caddy, Cloudflare,
103
+ Google or Resend. The API uses its own SQLite database and key ring;
104
+ the image's fixed non-root application user must be able to write its **disposable only**
105
+ `/data` directory. Do **not** point it at the PWA or Registry volume.
106
+ The existing PWA Unix-socket origin and production tunnel ingress must
107
+ remain untouched.
108
+
109
+ The operator must first obtain an authorized, **separate** disposable
110
+ HTTPS hostname, dedicated Google web OAuth client with callback
111
+ `https://<approved-disposable-hostname>/signin-pf-google`, reviewed
112
+ physical Caddy peer and exact network, a private data directory, and a
113
+ protected local Google client-secret file. No new credential, public
114
+ hostname, DNS route or provider write is authorized by this template.
115
+ Never fabricate the peer IP or silently route through the existing PWA
116
+ hostname.
117
+
118
+ Set these reviewed non-secret deployment inputs in a trusted private
119
+ host session: `PF_AUTH_DISPOSABLE_DATA_DIR`, `PF_AUTH_GOOGLE_CLIENT_ID`,
120
+ `PF_AUTH_PUBLIC_ORIGIN`, `PF_REVIEWED_CADDY_IP`, and
121
+ `PF_CADDY_NETWORK`. Set
122
+ `PF_AUTH_GOOGLE_CLIENT_SECRET_FILE` to a separately protected local
123
+ file outside Git. Keep the `PF_AUTH_DISPOSABLE_DATA_DIR` and any
124
+ local evidence paths private. The base Compose file has **no Resend
125
+ sender** and is safe to inspect without sending email.
126
+
127
+ From the Auth Kit checkout, use the *non-mutating* composition check
128
+ after all required values have been independently reviewed:
129
+
130
+ ```sh
131
+ docker compose -f deploy/disposable-project-auth.compose.yml config --quiet
132
+ ```
133
+
134
+ After an explicit deployment authorization, the same Compose file may
135
+ be used to start a disposable instance. First qualify the exact
136
+ physical peer/Caddy route, public origin, real Google callback,
137
+ administrator identity and negative spoofing paths. The template
138
+ does **not** deploy a Caddy route or prove Google login by itself.
139
+
140
+ Only for a separately approved test-email acceptance, add
141
+ `deploy/disposable-project-auth.resend.compose.yml` **after** setting
142
+ `PF_AUTH_RESEND_FROM` and `PF_AUTH_RESEND_KEY_FILE` to the approved
143
+ disposable Project's verified sender and protected Project-scoped
144
+ sending key. Its overlay adds a `/run/secrets/resend_project_key`
145
+ mount and turns on Resend invitation delivery. Run `config --quiet`
146
+ with both `-f` files to validate the combined settings before any
147
+ authorized send. Do not reuse the PWA or Registry's keys, and never
148
+ put secrets on the CLI or into GitHub reports.
149
+
150
+ Rollback is isolation-first: stop the disposable app, remove any
151
+ separately approved test DNS/Caddy ingress through Deployment Kit's
152
+ reviewed rollback, revoke the disposable credentials and retire
153
+ disposable state only after preserving required acceptance receipts.
154
+ PWA authentication, its hostname, and its data must remain unchanged.
155
+
156
+ ## Exact reviewed shared-host route binding (disposable)
157
+
158
+ The existing host has a Kit-owned Caddy container `project-forge-caddy`
159
+ attached to the external Docker network `caddy-net`. The reviewed
160
+ physical peer was `172.19.0.2`; that IP is **observed, not permanent**.
161
+ Before each actual change, inspect the current exact container/network
162
+ identity and rerun the normal Deployment Kit plan. Do not pin an old peer
163
+ after Docker/network reconciliation.
164
+
165
+ The disposable Compose service has the stable container name
166
+ `project-forge-auth-test--disposable`. The proposed **separate** route
167
+ ownership is:
168
+
169
+ | Layer | Reviewed proposed value |
170
+ | --- | --- |
171
+ | Host target | `production` (shared Ubuntu host; not PWA Production state) |
172
+ | Owner repository | `mriekki/project-forge-auth-kit` |
173
+ | Environment | `disposable` |
174
+ | Application slug | `project-forge-auth-test` |
175
+ | Route ID | `project-forge-auth-test--disposable` |
176
+ | Caddy upstream | `project-forge-auth-test--disposable:8080` |
177
+ | Trusted-LAN route | `project-forge-auth-test-disposable.local` |
178
+ | Public hostname | `auth-test.nalasuniverse.com` |
179
+ | Existing Cloudflare scope | `production` (must first reverify current authenticated-origin state) |
180
+ | Google callback | `https://auth-test.nalasuniverse.com/signin-pf-google` |
181
+
182
+ These are **proposed bindings**, not completed registrations. The route
183
+ IDs are unique and must be checked against the fresh host inventory before
184
+ registration. The existing PWA route and `mealplanner.nalasuniverse.com`
185
+ remain under their original owner.
186
+
187
+ **Read-only host/secret staging preflight:** after the PWA's Deployment
188
+ Kit SSH-agent session is working, use Auth Kit's pinned branch tooling from
189
+ the trusted local WSL2 workstation (the example assumes the current directory
190
+ is the Auth Kit checkout and the PWA checkout is at the existing WSL2 path):
191
+
192
+ ```sh
193
+ cd "$HOME/Developer/Projects/fmp"
194
+ python3 tools/project-forge-deployment/project-forge-deploy.py \\
195
+ target agent-run production -- \\
196
+ python3 "$HOME/Developer/Projects/project-forge-auth-kit/tools/disposable_project_auth_host_preflight.py" \\
197
+ --pwa-checkout "$PWD" --expected-caddy-peer 172.19.0.2
198
+ ```
199
+
200
+ Adjust the **Auth Kit checkout path** to the actual trusted clone; never
201
+ assume it exists. The reviewed peer value is an example from the
202
+ 2026-11-13 observation, not a permanent default. This command uses the
203
+ existing strict target identity, verifies current managed Caddy/network
204
+ and the PWA-owned LAN route, tests remote Docker/Compose availability,
205
+ checks that the disposable container and namespace are absent, and
206
+ checks only the **local** secret file owner/mode/size. It does not read
207
+ the secret contents, transfer credentials, configure OAuth, create
208
+ host files or query Cloudflare DNS. It fails closed on stale identities
209
+ or collisions. Successful preflight is **not** deployment permission.
210
+ Any new host-stage command must separately protect remote data/secret
211
+ permissions (non-root app user, private data directory, root-owned
212
+ Project-only secret) and preserve every existing PWA service.
213
+
214
+ **First explicit host-stage write (not application deployment):** when the
215
+ read-only preflight has completed with every positive gate true and
216
+ `mutated=false`, run the Kit's separately authorized staging program.
217
+ It rechecks the exact Caddy identity and exclusively creates a new,
218
+ **disposable-only** host namespace. The OAuth client secret travels from the
219
+ protected WSL2 file over the verified Deployment Kit SSH channel's stdin.
220
+ The secret is neither included in remote command arguments nor copied to Git.
221
+ The resulting file is root-owned, group-readable only by the ASP.NET
222
+ application UID/GID 1654 (`0640`); its parent directory is root-only.
223
+ The app's dedicated data directory is owned by 1654, private (`0700`).
224
+ All paths are new and fail closed on collision or symbolic links.
225
+
226
+ The command below assumes `AUTH_KIT_CHECKOUT` is the current, inspected
227
+ Auth Kit checkout. Set it to your actual clone directory (not a made-up
228
+ relative path). It uses a previously reviewed exact Caddy container/network
229
+ baseline; **refresh those identifiers if the host changes**.
230
+
231
+ ```sh
232
+ cd "$HOME/Developer/Projects/fmp"
233
+ python3 tools/project-forge-deployment/project-forge-deploy.py \\
234
+ target agent-run production -- \\
235
+ python3 "$AUTH_KIT_CHECKOUT/tools/disposable_project_auth_host_stage.py" \\
236
+ --pwa-checkout "$PWD" \\
237
+ --expected-caddy-container-id b1f1916698ee9b76b8e1cceeec38beaa495369ce49a5ee741a2971519b67aaaf \\
238
+ --expected-caddy-network-id 96dbe39067aa21441cd796cb8363d45b07c324f22e8ca42cda8ef67ed6beac2f \\
239
+ --expected-caddy-peer 172.19.0.2 \\
240
+ --apply
241
+ ```
242
+
243
+ **Review before running:** `--apply` is the explicit authorization for a
244
+ new directory and a credential file on the shared Ubuntu host. Without it,
245
+ the command prints only a non-mutating plan. The local Google secret must
246
+ already be stored as
247
+ `~/.local/share/project-forge/disposable-auth/secrets/google-client-secret`.
248
+ The program never prints the credential or includes it in an SSH command.
249
+ It will not create routes, containers, DNS, Google clients, Registry
250
+ members or PWA records. A stage failure may leave a partial protected
251
+ disposable namespace; **do not rerun blindly**. Inspect exact stage-owned
252
+ paths and reconcile under a separate reviewed recovery action.
253
+
254
+ A successful receipt `state=staged` establishes only host data directory
255
+ and secret placement. The reference API source/build context, container
256
+ image, Compose installation, live Caddy route and public hostname are
257
+ **not** staged or deployed by this operation. Those remain separately
258
+ reviewed phases; never treat a successful credential stage as a passed
259
+ live Google login or a supported authentication release.
260
+
261
+ **Source-pinned disposable image build and launch (separate write phase):**
262
+ After a successful `state=staged` host receipt, the Auth Kit's
263
+ `tools/disposable_project_auth_launch.py` can build a dedicated image from
264
+ only four committed Auth Kit source directories and launch it. It uses the
265
+ **same exact existing Deployment Kit SSH target**, checks the reviewed full
266
+ Caddy container and network IDs, checks the independent host-stage receipt
267
+ and secret/data metadata, and requires a specific reviewed Git commit.
268
+ Source travels as a `git archive` streamed to remote `docker build`
269
+ standard input, not as a persistent host checkout; no secret is included
270
+ in the archive or Docker build. The image gets its own deterministic
271
+ disposable tag; build may access approved public .NET dependencies.
272
+
273
+ The container starts non-root (`1654:1654`) on external `caddy-net`
274
+ with `--read-only`, `--cap-drop ALL`, no-new-privileges, a private
275
+ `/tmp` tmpfs, and a separate durable `/data` bind. The Google
276
+ credential is mounted as a read-only protected **file** and is never
277
+ added to Docker argv, image layers or environment variables. The launch
278
+ spec declares port 8080 internally but **publishes no host ports**.
279
+ There is no Resend sender, no first-admin bootstrap, and no Google
280
+ callback from this launch alone.
281
+
282
+ Run this from the current trusted PWA checkout using a newly cloned Auth
283
+ Kit feature branch. Use the **exact Git commit just independently reviewed
284
+ for that branch**, not a blind local HEAD:
285
+
286
+ ```sh
287
+ cd "$HOME/Developer/Projects/fmp"
288
+ python3 tools/project-forge-deployment/project-forge-deploy.py \\
289
+ target agent-run production -- \\
290
+ python3 "$AUTH_KIT_CHECKOUT/tools/disposable_project_auth_launch.py" \\
291
+ --pwa-checkout "$PWD" \\
292
+ --auth-kit-checkout "$AUTH_KIT_CHECKOUT" \\
293
+ --expected-auth-commit "$REVIEWED_AUTH_KIT_COMMIT" \\
294
+ --expected-caddy-container-id b1f1916698ee9b76b8e1cceeec38beaa495369ce49a5ee741a2971519b67aaaf \\
295
+ --expected-caddy-network-id 96dbe39067aa21441cd796cb8363d45b07c324f22e8ca42cda8ef67ed6beac2f \\
296
+ --expected-caddy-peer 172.19.0.2 \\
297
+ --apply
298
+ ```
299
+
300
+ `--apply` is the explicit authorization for **Docker image creation
301
+ and disposable container launch only**. Omit it to review the exact
302
+ readiness plan without writing. It will not mutate Caddy, Cloudflare,
303
+ the PWA, the Household Registry, or any application route. The complete
304
+ image SHA256 and container ID in its non-secret receipt must be retained
305
+ as release acceptance evidence. If build/launch fails or the container
306
+ exits, **do not blindly rerun**: a tagged image or stopped container may
307
+ remain and requires scoped inspection/reconciliation first.
308
+
309
+ An image and container running after the startup grace period is
310
+ **not proof of HTTP readiness, live Google identity or inbox
311
+ delivery**. Subsequent owned LAN route, authenticated Cloudflare
312
+ public route, Google callback and invitation tests retain their own
313
+ release gates. Reinspect the exact physical proxy IP immediately
314
+ before route registration; do not broaden proxy trust.
315
+
316
+ **Running container verification and isolated LAN route:** After the
317
+ image/container receipt confirms exact source revision and no published ports,
318
+ run the **read-only** `tools/disposable_project_auth_runtime_probe.py`
319
+ from a trusted current Auth Kit checkout, via the PWA's verified Deployment
320
+ Kit SSH-agent session. It verifies the launched container's exact image,
321
+ revision and network identity, then connects to its private bridge IP from
322
+ the Ubuntu host. Both a direct request without forwarding and a direct
323
+ request with forged forwarding headers must return **403**; requests from
324
+ the unreviewed host physical peer must not bypass the Caddy-only trust
325
+ boundary. Expected successful state:
326
+ `ready-for-isolated-lan-route`, `mutated=false`.
327
+
328
+ The read-only probe uses the **specific, already verified disposable
329
+ container** receipt of 2026-11-13: Docker container
330
+ `b01e6838104d2fe0f7de61712452b4551ed08c1abbb29c9464efadc31a76f0f9`
331
+ and image
332
+ `sha256:aef68c599763576938c4241d77f5ad466fe530e2c32799c6fc1e9b107efeca3c`,
333
+ built from commit `838efecd406b83b01664f3c9e99749dd9dc0be91`.
334
+ If that instance is replaced or restarted with a different image/container,
335
+ do not reinterpret a failure as success; update reviewed receipt evidence
336
+ under a separately approved action. A 403 proves origin peer rejection
337
+ only—not health through Caddy or live Google sign-in.
338
+
339
+ Only after this probe passes and shared platform/route inventory remains
340
+ unchanged should the Deployment Kit register the **separate internal
341
+ application route**. Its existing plan-and-confirmation write gate applies:
342
+
343
+ ```sh
344
+ cd "$HOME/Developer/Projects/fmp"
345
+ python3 tools/project-forge-deployment/project-forge-deploy.py \\
346
+ target agent-session production -- \\
347
+ route register production \\
348
+ --owner-repo mriekki/project-forge-auth-kit \\
349
+ --environment disposable \\
350
+ --app-slug project-forge-auth-test \\
351
+ --upstream-container project-forge-auth-test--disposable \\
352
+ --upstream-port 8080 \\
353
+ --hostname project-forge-auth-test-disposable.local
354
+ ```
355
+
356
+ Review the printed change plan; approve only the new
357
+ `project-forge-auth-test--disposable` route, not an update to the
358
+ PWA route. The command requires the Deployment Kit's explicit
359
+ `[ROUTE WRITE]` confirmation. Do not add the public Cloudflare
360
+ hostname through the LAN command. The LAN route is an owned
361
+ Caddy upstream prerequisite only; in authenticated-origin mode,
362
+ the **public** route's Caddy handler must still pass the strict
363
+ Host and forwarding contract. Verify that independently before
364
+ claiming successful authentication.
365
+
366
+ **Cloudflare API credentials for this route:** A token that can read the
367
+ existing tunnel configuration but receives HTTP 403 when listing DNS
368
+ records is **insufficient**, even for read-only planning. Do not
369
+ attempt `--apply` with it. The existing WSL2
370
+ `PROJECT_FORGE_CLOUDFLARE_API_TOKEN`, if set, is preferred for plan-only
371
+ reads over the narrower `PF_CF_READ_TOKEN`; selecting it does **not**
372
+ authorize a write. To inspect configuration without printing secrets:
373
+
374
+ ```sh
375
+ if [[ -n "${PROJECT_FORGE_CLOUDFLARE_API_TOKEN:-}" ]]; then
376
+ echo "Reusable route token already set (value not shown)."
377
+ else
378
+ echo "No existing route token set in this shell."
379
+ fi
380
+ ```
381
+
382
+ A provider-supported single appropriately scoped route token needs
383
+ **Zone: Zone Read** for listing the exact zone, **Zone: DNS Edit** for
384
+ listing/creating the exact test CNAME, and **Account: Cloudflare Tunnel
385
+ Edit** (or corresponding current Cloudflare One connector write grant)
386
+ for reading/updating the existing remotely managed tunnel. Limit to
387
+ the known `nalasuniverse.com` zone and reviewed Cloudflare account;
388
+ prefer the shortest suitable lifetime. An existing token with equivalent
389
+ permissions can be reused; do not create another solely to obtain a new
390
+ secret. Never paste API tokens into chats, issue comments, shell
391
+ arguments or tracked files. Set the token in the current private shell
392
+ using `read -rs` and `export` when a new credential is actually
393
+ necessary. Re-run read-only planning before any explicit write gate.
394
+
395
+ **Separate public Cloudflare route — read-only review first:** After
396
+ the owned LAN route inventory lists `project-forge-auth-test--disposable`,
397
+ use `tools/disposable_project_auth_public_route.py` from the current Auth
398
+ Kit feature branch. It delegates to the **existing PWA Deployment Kit's**
399
+ `cloudflare_ingress.plan_public_route` and
400
+ `cloudflare_route_apply.apply_public_route`; no independent/unmanaged
401
+ Cloudflare mutation code is used.
402
+
403
+ The review accepts only the verified `project-forge-production`
404
+ authenticated-origin tunnel, zone `nalasuniverse.com`, exact Auth Kit
405
+ owner/environment/route, current PWA Unix-socket ingress, unowned test
406
+ DNS name, unchanged non-ingress configuration, unchanged existing ingress
407
+ rules, and the final 404. Existing provider configuration may include
408
+ unrelated rules; they must be preserved, not replaced. Its summary hides
409
+ raw provider configuration, API credentials and existing DNS records.
410
+
411
+ In the **same trusted WSL2 SSH-agent session**, with a locally available
412
+ read-only Cloudflare token in `PF_CF_READ_TOKEN`:
413
+
414
+ ```sh
415
+ cd "$HOME/Developer/Projects/fmp"
416
+ python3 tools/project-forge-deployment/project-forge-deploy.py \\
417
+ target agent-run production -- \\
418
+ python3 "$AUTH_KIT_CHECKOUT/tools/disposable_project_auth_public_route.py" \\
419
+ --pwa-checkout "$PWD"
420
+ ```
421
+
422
+ If this read-only plan returns `state=blocked`, it now reports the
423
+ specific `stage` and sanitized `category`, not the raw Cloudflare
424
+ error. A `dns-read` / `cloudflare-permission-denied` result means
425
+ the **read token cannot read DNS records** at this zone; a read-only
426
+ tunnel-config success does not imply DNS list access. A
427
+ `strict-plan-review` / `strict-contract-mismatch` result means the
428
+ reviewed route or existing PWA ingress does not match the expected
429
+ exact state and must be investigated, **not bypassed**. The
430
+ `managed-lan-route`, `managed-public-routes`,
431
+ `tunnel-config-read`, and `kit-plan` stages distinguish the other
432
+ bounded preflight dependencies. These checks never grant write
433
+ permissions or activate a public hostname; they only classify which
434
+ reviewed dependency failed. If blocked, do not try `--apply` or
435
+ change an existing PWA route.
436
+
437
+ This prints `ready-for-explicit-public-route-registration` if the
438
+ reviewed change would append **only** the Auth Kit's
439
+ `auth-test.nalasuniverse.com` authenticated Unix-socket ingress to the
440
+ shared tunnel, alongside an owned DNS CNAME. **It makes no provider or host
441
+ writes.** A read token with insufficient DNS read access may block the
442
+ plan; do not assume an unverified hostname is available.
443
+
444
+ Only after inspecting the exact printed plan should the operator
445
+ consider running the *same* command with `--apply`. That mode requires
446
+ a **separate** locally configured `PROJECT_FORGE_CLOUDFLARE_API_TOKEN`
447
+ with the necessary reviewed DNS and tunnel edit authority and an
448
+ interactive terminal. The operator must enter the exact Kit
449
+ `[PUBLIC INGRESS ROUTE WRITE]` confirmation. Provider changes are made
450
+ only by the Deployment Kit's existing transaction-locked, verified,
451
+ post-write-recovery path. The edit token remains in the trusted WSL2
452
+ process environment, never Git, chat, Docker or printed plans.
453
+
454
+ A successful public-route receipt **does not** prove public HTTPS,
455
+ the app's strict forwarded-header contract, Google OAuth redirect,
456
+ initial administrator bootstrap, or email delivery. These are separate
457
+ live checks. A failed or ambiguous apply must be inspected before retry;
458
+ do not edit or remove shared `project-forge-production` tunnel rules or
459
+ the existing PWA entry manually.
460
+
461
+ **Order of operations (independent explicit approvals at write boundaries):**
462
+
463
+ 1. Revalidate exact Caddy peer, current shared network, and both route
464
+ inventories. Confirm the proposed hostname is not already used by
465
+ managed or unmanaged DNS/ingress; DNS non-resolution alone is not proof
466
+ of ownership. Review the existing Cloudflare `production` scope's
467
+ authenticated-origin mode. The accepted PWA configuration uses
468
+ `unix:/run/project-forge-origin/origin.sock`; do not replace it.
469
+ 2. Stage the **separate** app-only private directory and Google secret
470
+ on the reviewed Ubuntu host. A WSL2-only secret file is not magically
471
+ available in remote-host Docker Compose. Transfer any approved secret
472
+ through an established protected host workflow, never Git or a public
473
+ PR. Verify the image user can write only the disposable data directory.
474
+ 3. Build/launch only the disposable service with **no host-published
475
+ port**, then verify its exact container/network peer, restart and health.
476
+ Do not start it using the PWA-owned Production release/deploy scripts.
477
+ 4. Use the **existing Deployment Kit** route-registry planning and write
478
+ gate for a new, owned `.local` Caddy route with the exact spec above.
479
+ Do not route public DNS to an unverified application.
480
+ 5. After the LAN route is registered and the authenticated Cloudflare
481
+ origin is reverified, use the Deployment Kit's **separate** public-route
482
+ plan/authorization for the proposed hostname and origin. That plan
483
+ must preserve every existing ingress rule (including the PWA Unix
484
+ route), the final 404 rule, and other DNS records. Do **not** hand-edit
485
+ Caddyfiles or tunnel configuration. The previously created Cloudflare
486
+ Zone/Tunnel **Read** token cannot authorize DNS or Tunnel writes; any
487
+ write credential and action require separate explicit review.
488
+ 6. Only then perform real Google OIDC callback acceptance. Initial
489
+ Project administrator provisioning requires a separately verified
490
+ exact Google subject; no first-visitor privilege and no email-only
491
+ binding. Keep the Resend sending overlay disabled until a distinct
492
+ approved email test.
493
+
494
+ **Rollback:** Remove only the owned disposable public route via the
495
+ Deployment Kit's exact route-removal procedure, then the owned LAN route;
496
+ stop the disposable application; retain a private evidence/rollback
497
+ receipt. Do not tear down shared Caddy, cloudflared, their network,
498
+ existing tunnel, the PWA route, or the PWA app/data.
499
+
500
+ ## Exact disposable recovery after host restart — separate write gate
501
+
502
+ The operator's 2026-11-13 read-only receipt establishes that the
503
+ **original disposable Auth Kit container is exited** and has Docker
504
+ restart policy `no`. The existing managed Caddy **container ID** and
505
+ Docker **network ID** are unchanged, but the Caddy bridge IP moved from
506
+ `172.19.0.2` to `172.19.0.4`. The shared cloudflared connector
507
+ and Caddy are running. This explains the Auth Kit public 502 and
508
+ requires both restarting the isolated backend **and** replacing its
509
+ immutable `TrustedProxyIp` setting: a blind `docker start` would
510
+ retain the obsolete trusted physical peer. The earlier PWA public
511
+ `403` is not an authorized reason to change the PWA.
512
+
513
+ `tools/disposable_project_auth_recover.py` is an exact **one-off**
514
+ reviewed recovery helper for this initial disposable instance. By
515
+ default it only prints a plan. Before any mutation it verifies the
516
+ Deployment Kit's current Caddy image/network/peer, shared connector,
517
+ existing managed PWA/Auth Kit LAN and public-route inventory, original
518
+ stopped container full ID/source/image/owner, original isolated Docker
519
+ configuration, protected data/secret mounts and stage receipt, and an
520
+ available backup name. It uses the original image and the same private
521
+ SQLite, keyring and Google client secret files; neither image build nor
522
+ credential replacement is needed.
523
+
524
+ From the already trusted workstation and Deployment Kit SSH-agent:
525
+
526
+ ```sh
527
+ cd "$HOME/Developer/Projects/fmp" || exit
528
+ git -C "$AUTH_ROUTE_DIR/auth-kit" pull --ff-only || exit
529
+
530
+ python3 tools/project-forge-deployment/project-forge-deploy.py \\
531
+ target agent-run production -- \\
532
+ python3 "$AUTH_ROUTE_DIR/auth-kit/tools/disposable_project_auth_recover.py" \\
533
+ --pwa-checkout "$PWD"
534
+ ```
535
+
536
+ Only after the operator reviews an exact
537
+ `state=ready-for-exact-disposable-recovery` plan, the same command may
538
+ be invoked with `--apply` and the explicit interactive phrase
539
+ `[DISPOSABLE AUTH RECOVERY WRITE]`. The approved narrow operation
540
+ renames the original **stopped** container to a reserved backup name,
541
+ starts a replacement using the same reviewed original image and data,
542
+ sets the exact newly validated physical Caddy peer to
543
+ `172.19.0.4`, and applies `--restart unless-stopped`. It rechecks
544
+ all original host guards immediately before its Docker writes. The
545
+ private authenticated-origin Unix socket must then answer `401` for
546
+ an anonymous Caddy-routed request. The resulting receipt sets
547
+ `livePublicHttpsVerified=false` until separately tested from the
548
+ workstation. The previous stopped container is preserved for rollback;
549
+ its data directory and secret remain Project-owned and are not copied
550
+ or written by the helper.
551
+
552
+ The `unless-stopped` policy improves survival of Docker daemon
553
+ restarts (<https://docs.docker.com/engine/containers/start-containers-automatically/>),
554
+ but does **not** make the authenticated physical Caddy IP stable.
555
+ A future Caddy IP change still requires reviewed exact-peer convergence.
556
+ That durable identity/lifecycle improvement belongs to the canonical
557
+ Project Forge Deployment Kit, not a permanent untracked edit inside
558
+ this consumer.
559
+
560
+ If the plan is blocked, the image is unavailable, host identity changes,
561
+ or the replacement fails after the old container is renamed, **stop**;
562
+ inspect the retained original backup and the exact Docker name before
563
+ another attempt. Do not automatically remove either container, reuse
564
+ PWA private resources, change shared Caddy, or loosen transport trust.
565
+
566
+ ## Caddy identity rollover: status-only recovery check
567
+
568
+ After the operator observed public Auth Kit HTTP 502, the original
569
+ `disposable_project_auth_socket_probe.py` safely refused to continue
570
+ because managed Caddy's container identity changed from the previously
571
+ reviewed exact baseline. In the same operator run, the existing PWA
572
+ hostname returned public HTTP 403; this alone does **not** establish
573
+ that the PWA is down (it may enforce request admission rules).
574
+
575
+ The current probe now calls the Deployment Kit's **read-only** verified
576
+ `platform_proxy_peer` against the previous full Caddy container/network
577
+ IDs and configured peer IP. When Caddy has been recreated, it reports
578
+ `reviewedCaddyIdentityChanged`, validated
579
+ `currentCaddyIdentity`, and the disposable app's explicit
580
+ `appContainerStatus`, restart policy, Caddy/cloudflared states.
581
+ The embedded private-socket probe retains the old pinned identity
582
+ check and does **not** attempt private-socket traffic against a new
583
+ unreviewed Caddy container. No trust configuration or application
584
+ route is automatically rewritten.
585
+
586
+ Rerun this revised read-only probe (as documented below) before deciding
587
+ whether the disposable container alone should be restarted or a
588
+ separate reviewed Caddy peer convergence is needed. Do not re-use
589
+ the old peer IP as proof of the new Caddy's physical identity, and do
590
+ not treat public 403 or 502 as proof that Google login is broken.
591
+
592
+ ## Next-day public 502 versus Python-style 403 — availability triage
593
+
594
+ On 2026-11-13 the operator repeated public HTTPS tests twice: normal
595
+ `curl` returned `502` from **all three** Auth Kit API endpoints,
596
+ while a `Python-urllib` User-Agent request returned `403`.
597
+ Previously a plain public `curl /api/auth/me` returned the correct
598
+ `401`, and the protected Caddy Unix-socket diagnostic returned
599
+ `401` for the canonical request, `403` for the missing-client-IP
600
+ request, and `404` for an unrelated host.
601
+
602
+ **Do not infer a Google callback failure from a 502.** These are
603
+ different symptoms: a 502 means the public request cannot be completed
604
+ through the origin/upstream path; a 403 for a particular client can be
605
+ a separate edge filtering or strict-transport rejection. The initial
606
+ disposable container was explicitly created with Docker
607
+ `--restart no`, so a subsequent daemon or host reboot could leave
608
+ the exact disposable container stopped without affecting existing
609
+ public DNS/route ownership. This is a hypothesis to verify; there is
610
+ not yet an operator receipt showing a stopped container.
611
+
612
+ Re-run `disposable_project_auth_socket_probe.py` unchanged through
613
+ the verified PWA Deployment Kit `target agent-run production` wrapper.
614
+ Its revised read-only output includes `appContainerStatus`,
615
+ `appRestartPolicy`, `caddyRunning`, `cloudflaredRunning`, and
616
+ `cloudflaredAttachedToCaddyNetwork`, along with existing exact
617
+ Caddy/app identity, authenticated Unix-socket and public HTTPS checks.
618
+
619
+ - If assessment is `disposable-auth-container-not-running` and
620
+ `appRestartPolicy=no`, inspect the **same exact** container ID/image,
621
+ read-only volumes, network and origin route before requesting one
622
+ separately authorized, narrowly scoped container start.
623
+ - If the disposable container is running but Caddy or cloudflared is
624
+ not, investigate the unavailable shared service without blindly
625
+ restarting or modifying shared infrastructure.
626
+ - If Caddy's canonical request is 401 but public requests remain 502,
627
+ investigate tunnel connector/origin routing and Cloudflare edge
628
+ independently. Confirm that the existing PWA hostname is still
629
+ responding without assuming its status code.
630
+ - Do not troubleshoot Google login or reissue OAuth/Cloudflare tokens
631
+ until the public `/api/auth/me` request consistently returns 401.
632
+
633
+ No DNS rewrite, route re-registration, secret replacement, Caddy
634
+ transport relaxation or PWA production deployment is authorized by
635
+ this diagnostic.
636
+
637
+ ## Public 403 routing diagnosis — read-only, no Caddy changes
638
+
639
+ After the 2026-11-13 live public HTTPS probe returned **403 for all
640
+ three** `/api/auth/me`, `/api/auth/csrf`, and `/api/auth/google`
641
+ endpoints, do not reconfigure Google or weaken the strict origin
642
+ middleware. A 403 from Auth Kit's early transport middleware is
643
+ indistinguishable by status alone from a separate Cloudflare edge
644
+ challenge or policy rejection.
645
+
646
+ `tools/disposable_project_auth_socket_probe.py` is a **read-only**
647
+ diagnostic that uses the PWA Deployment Kit's exact SSH target and
648
+ rechecks managed Caddy and disposable app container identities and
649
+ Cloudflare public route ownership. Through the authenticated-origin
650
+ Unix socket, it makes three bounded GET `/api/auth/me` requests:
651
+
652
+ - Correct test hostname plus **synthetic** `CF-Connecting-IP:
653
+ 198.51.100.42` (reserved documentation address) — expect **401**
654
+ when the Caddy -> application forwarding contract is working.
655
+ - Correct test hostname without `CF-Connecting-IP` — expect **403**
656
+ from the application transport guard.
657
+ - Unrelated hostname with synthetic client IP — expect **404** from
658
+ Caddy's fail-closed public listener.
659
+
660
+ It separately makes one TLS-validated public HTTPS GET to the exact test
661
+ `/api/auth/me` endpoint, reporting only the numeric status and
662
+ boolean Cloudflare challenge/header fingerprints. It never outputs
663
+ headers, cookies, raw bodies, source IP addresses, tokens or OAuth
664
+ state, makes no database writes or login challenges, and changes
665
+ neither Caddy nor PWA/provider routes. It requires the already observed
666
+ test container/image/Caddy identities and refuses to silently probe a
667
+ replacement container.
668
+
669
+ ```sh
670
+ cd "$HOME/Developer/Projects/fmp" || exit
671
+ git -C "$AUTH_ROUTE_DIR/auth-kit" pull --ff-only || exit
672
+ python3 tools/project-forge-deployment/project-forge-deploy.py \\
673
+ target agent-run production -- \\
674
+ python3 "$AUTH_ROUTE_DIR/auth-kit/tools/disposable_project_auth_socket_probe.py" \\
675
+ --pwa-checkout "$PWD"
676
+ ```
677
+
678
+ If the synthetic canonical request also returns 403, investigate the
679
+ exact Caddy/ASP.NET forwarding contract. If canonical reaches 401 but
680
+ public still gets 403, investigate Cloudflare-edge policy/header
681
+ delivery without changing the trusted Caddy peer. Caddy's
682
+ `reverse_proxy header_up -CF-Connecting-IP` strips that header from
683
+ the **upstream** request; it is not itself proof that the original
684
+ `http.request.header.CF-Connecting-IP` placeholder was lost. Any
685
+ confirmed reusable Caddy/forwarding bug must be fixed in the canonical
686
+ Deployment Kit owner with tests and separately reviewed host rollout.
687
+ No shared Production PWA proxy changes are justified by 403 alone.
688
+
689
+ ## Recovered public HTTPS — 401 / 200 / 302 and client-specific 403
690
+
691
+ Following exact recovery of the isolated container (new Docker ID
692
+ `fbcd0420d33dd3bae594294c49e96ee8cce4bbdc4cd93abdbf483ef0b5992c58`,
693
+ reviewed Caddy peer `172.19.0.4`, original data and keys retained),
694
+ the operator's public `curl` matrix returned the expected:
695
+
696
+ - `GET /api/auth/me` → HTTP **401** (anonymous request rejected)
697
+ - `GET /api/auth/csrf` → HTTP **200**
698
+ - `GET /api/auth/google` → HTTP **302** (Google challenge initiated)
699
+
700
+ With a `Python-urllib/3.12` User-Agent the `/api/auth/me`
701
+ request separately returned HTTP **403**. That is client-dependent
702
+ behavior, potentially provider/edge enforcement, not evidence that
703
+ normal browser or curl requests are unavailable; source layer is not
704
+ yet proven. Avoid changing Caddy, Cloudflare security policies or the
705
+ ASP.NET origin checks solely to force the Python client through.
706
+
707
+ `tools/disposable_project_auth_curl_probe.py` uses the actual
708
+ working **curl** HTTPS client rather than `urllib`, invokes only those
709
+ three fixed public GET endpoints, **does not follow redirects**, and
710
+ validates the CSRF token shape, auth security headers and the redirect
711
+ to `accounts.google.com` with the **separately reviewed test client**,
712
+ exact callback `https://auth-test.nalasuniverse.com/signin-pf-google`,
713
+ authorization code/PKCE S256, state and nonce. It prints only status
714
+ integers and boolean checks, never Google authorization URLs, raw
715
+ headers, Set-Cookie, CSRF tokens or OAuth state/nonce. It writes
716
+ nothing to the Project, provider, PWA or host.
717
+
718
+ ```sh
719
+ cd "$HOME/Developer/Projects/fmp" || exit
720
+ git -C "$AUTH_ROUTE_DIR/auth-kit" pull --ff-only || exit
721
+ python3 "$AUTH_ROUTE_DIR/auth-kit/tools/disposable_project_auth_curl_probe.py" \\
722
+ --check-google-challenge
723
+ ```
724
+
725
+ A successful result `state=ready-for-live-google-login` means only
726
+ that the public **challenge redirect** is correct. It does not
727
+ demonstrate Google consent, callback validation, session creation, or
728
+ administrator admission.
729
+
730
+ **Do not assume the first Google visitor becomes an administrator.**
731
+ The app requires an independently verified immutable Google `sub`
732
+ claim for the approved initial administrator, paired with a verified
733
+ email in a **protected host-only** `admin-init` action. Until this is
734
+ provisioned, a successful but unapproved Google callback is expected
735
+ to redirect generically to `/?auth=unavailable`. Never paste OAuth
736
+ codes, ID tokens, subject values, session cookies, Google client
737
+ secrets, invitation tokens or callback URLs with state/code into
738
+ GitHub issues or ChatGPT. The current disposable image is the
739
+ reference code built from commit `838efecd406b` and is independent
740
+ of the evolving Auth Kit draft PR's test tooling.
741
+
742
+ ## Post-registration public HTTPS smoke proof
743
+
744
+ When the Deployment Kit returns `state=public-route-registered`, the
745
+ hostname, DNS record and tunnel ingress have been written, but
746
+ `liveHttpsVerified=false` is still authoritative: a successful provider
747
+ write does **not** prove end-to-end HTTP, TLS, strict proxy handling or
748
+ Google authorization. From a workstation with real public DNS/network
749
+ access, run the bounded Auth Kit probe:
750
+
751
+ ```sh
752
+ cd "$HOME/Developer/Projects/fmp"
753
+ git -C "$AUTH_ROUTE_DIR/auth-kit" pull --ff-only
754
+ python3 "$AUTH_ROUTE_DIR/auth-kit/tools/disposable_project_auth_public_probe.py" \\
755
+ --check-google-challenge
756
+ ```
757
+
758
+ This performs only public HTTPS GETs over a normal TLS-validated
759
+ connection, **never** SSH or Cloudflare writes. It requires unauthenticated
760
+ `/api/auth/me` to return `401`, `/api/auth/csrf` to return `200`
761
+ with an opaque token, and expected cache/HSTS/security headers. With
762
+ `--check-google-challenge` it also requests
763
+ `/api/auth/google` and expects an **unfollowed** `302` to
764
+ `accounts.google.com` with the separately reviewed test Google client,
765
+ exact `https://auth-test.nalasuniverse.com/signin-pf-google` callback,
766
+ authorization-code flow, PKCE S256, opaque state, and nonce. It does
767
+ not complete Google login, persist its own cookies, return credentials,
768
+ print OAuth state or raw response headers, send emails, or access
769
+ the PWA's APIs/database. The server may generate ordinary OIDC
770
+ correlation/DataProtection state as part of handling the challenge.
771
+
772
+ If the public smoke check reports `state=blocked` with all boolean
773
+ checks false, do **not** assume an OAuth configuration fault. As of the
774
+ 2026-11-13 test, the initial version did not include raw HTTP statuses.
775
+ The revised probe includes only `observedHttpStatuses` for the three
776
+ fixed endpoints (e.g. 403, 404, 502), without dumping response bodies,
777
+ cookies, OAuth redirects or Cloudflare identifiers. This lets us
778
+ distinguish strict origin/forwarding rejection from route or upstream
779
+ failure before touching Caddy or Google configuration.
780
+
781
+ **Potential shared Deployment Kit forwarding defect to verify against
782
+ actual status evidence:** the authenticated-origin Caddy template
783
+ `cloudflare_ingress_remote.py` currently contains
784
+ `request_header -CF-Connecting-IP` before
785
+ `header_up X-Forwarded-For {http.request.header.Cf-Connecting-Ip}`
786
+ and the same `X-Real-IP` substitution. If Caddy removes the source
787
+ header before those upstream placeholders are evaluated, the exact
788
+ `StrictPublicOriginTransport` contract will reject all requests
789
+ because forwarded client/real IP are absent. This is **not** yet
790
+ confirmed on the live test route; any reusable fix must be made in
791
+ the canonical Deployment Kit owner with regression/security proofs,
792
+ not as an untracked hand edit to the running PWA proxy. Preserve the
793
+ live PWA route, its existing working session and current public
794
+ Cloudflare route during diagnosis.
795
+
796
+ Expected result: `state=ready-for-live-google-login` with all
797
+ reported checks `true` and `googleLoginCompleted=false`. A failure
798
+ may mean DNS propagation, TLS, proxy transport, application startup, or
799
+ OIDC configuration has not been proven; do not compensate by disabling
800
+ strict proxy validation, reusing the PWA OAuth client, or relaxing TLS.
801
+ If the runner cannot resolve the hostname due to its own network
802
+ environment, that is not sufficient to prove a universal public outage.
803
+
804
+ A passed challenge is not permission to enroll the first arbitrary
805
+ visitor. Before an actual approved administrator signs in, use the
806
+ separate per-Project `admin-init` procedure with a independently
807
+ verified exact Google `issuer+subject` and intended email. Never
808
+ bootstrap from a guessed subject, email alone, the household Registry,
809
+ or an unauthenticated first visitor.
810
+
811
+ ## Live-provider acceptance sequence (isolated Project only)
812
+
813
+ **Prerequisite:** The existing household provider inventory is a *read-only*
814
+ proof of resource identity, not an authorization to deploy or send emails.
815
+ A healthy PWA tunnel route must remain unchanged throughout this sequence.
816
+ The reference API has offline fake-Google, fake-Resend, and disposable
817
+ transport contract tests; none substitutes for these live steps.
818
+
819
+ 1. **Prepare a new, separately reviewed application origin.** Obtain the
820
+ Deployment Kit's exact Caddy network, physical peer address and independent
821
+ hostname routing plan. Do not repurpose the existing PWA hostname,
822
+ OAuth client, socket, API key, session cookies, SQLite file or
823
+ DataProtection key ring. Start with a disposable instance that has no public
824
+ Caddy/Cloudflare route and no host-published TCP port.
825
+ 2. **Qualify Google first.** Create or select a *separate* authorized Google
826
+ Web OAuth client for this exact disposable application origin; register
827
+ only its exact `https://<approved-app-host>/signin-pf-google` callback.
828
+ Store its client secret via local protected configuration. Run one real
829
+ approved-admin Google code flow end to end and confirm the expected
830
+ issuer+subject, opaque cookie, /me result, logout and denied-uninvited-user
831
+ path. Reject wrong audience, host, state, nonce, forwarded headers and
832
+ unreviewed physical peers. Do not infer Google callback readiness from the
833
+ existing PWA's working OAuth client.
834
+ 3. **Qualify invitations separately.** The successful **read-only Resend
835
+ sender-domain verification** confirms the shared sender domain is
836
+ provisioned but grants no application sending authority. Configure a
837
+ separately approved Project-scoped sending credential and a
838
+ `From` address at the verified domain using protected local configuration.
839
+ Send a single controlled invitation to an explicitly approved test mailbox
840
+ only after the operator authorizes that real email. Confirm receipt,
841
+ exact recipient, 48-hour expiry, single redemption after verified Google
842
+ sign-in and replay rejection. Exercise provider rejection and unavailable
843
+ network paths without emailing real users. The application must not
844
+ return invitation tokens to the browser when Resend is configured.
845
+ 4. **Restart and restore.** Prove the durable Project-owned account/session DB
846
+ and private OIDC key directory survive restart. Rehearse a stopped disposable
847
+ database backup and restore, session revocation, one-time administrator
848
+ bootstrap rejection on repeat, and no cross-Project identity leakage.
849
+ 5. **Only then review rollout.** Capture sanitized results, exact source
850
+ revisions, successful CI runs, reviewed DNS/Caddy changes, rollback
851
+ procedure and explicit authorization for any provider or host write.
852
+ The PWA migration is a separate compatibility/rehearsal gate. Never use
853
+ the household Registry's Developer membership as application admission.
854
+
855
+ **Evidence hygiene:** Keep API keys, OAuth client secrets, Google subjects,
856
+ invitation tokens, user email addresses, local SQLite DBs and raw provider
857
+ responses out of GitHub comments, public artifacts, and this checklist.
858
+ Record success/failure, code revision, time and non-secret resource references
859
+ in the release acceptance issue. A missing credential or public route is
860
+ **blocked**, not passed, and must not be bypassed by swapping in synthetic
861
+ provider results.
862
+
863
+ ## Deployment and migration boundaries
864
+
865
+ The optional new Project starter is not PWA's existing FB-001
866
+ authentication engine. PWA's current Google/password/email recovery,
867
+ session and FB-002 membership code remains working and its opt-in adapter
868
+ is separately reviewed. Do not replace PWA databases or use the new
869
+ Project SQLite store as a destination for silent user copy.
870
+
871
+ To qualify release: run the generated consumer on disposable current
872
+ Docker/Caddy/Cloudflare topology with real Google callbacks and negative
873
+ spoofing, invitation expiry/replay/rate tests, email delivery and
874
+ provider failure, durable backup/restore, cookie key persistence across
875
+ restart, per-Project isolation, host bootstrap, and old PWA parity.
876
+
877
+ Neither package assembly nor unit tests are authorization to publish
878
+ the next kit or deploy Production.