@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.
- package/assets/authentication/distribution.json +90 -0
- package/assets/components.json +18 -4
- package/assets/core/distribution.json +12 -0
- package/assets/deployment/distribution.json +292 -68
- package/assets/generated/ASSEMBLY.json +152 -6
- package/assets/generated/authentication/docs/AUTH_RECOVERY_RETENTION.md +101 -0
- package/assets/generated/authentication/docs/HOUSEHOLD_INFRASTRUCTURE_ADOPTION.md +55 -0
- package/assets/generated/authentication/docs/HOUSEHOLD_REGISTRY.md +246 -0
- package/assets/generated/authentication/docs/PROJECT_AUTH_REFERENCE.md +878 -0
- package/assets/generated/authentication/samples/ProjectForgeAuth.Api/Program.cs +40 -0
- package/assets/generated/authentication/samples/ProjectForgeAuth.Api/ProjectForgeAuth.Api.csproj +13 -0
- package/assets/generated/authentication/samples/ProjectForgeAuth.Api/wwwroot/auth.js +65 -0
- package/assets/generated/authentication/samples/ProjectForgeAuth.Api/wwwroot/index.html +44 -0
- package/assets/generated/authentication/src/ProjectForge.Auth/AdmissionPolicy.cs +167 -0
- package/assets/generated/authentication/src/ProjectForge.Auth/AuthenticationDiagnostics.cs +159 -0
- package/assets/generated/authentication/src/ProjectForge.Auth/ProjectForge.Auth.csproj +9 -0
- package/assets/generated/authentication/src/ProjectForge.Auth/PwaExistingProjectCompatibility.cs +64 -0
- package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ProjectAuthStore.cs +471 -0
- package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ProjectForge.Auth.Runtime.csproj +15 -0
- package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ProjectForgeAuthRuntimeExtensions.cs +472 -0
- package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ResendProjectInvitationDelivery.cs +105 -0
- package/assets/generated/authentication/src/ProjectForge.Transport/ProjectForge.Transport.csproj +11 -0
- package/assets/generated/authentication/src/ProjectForge.Transport/StrictPublicOriginTransport.cs +95 -0
- package/assets/generated/authentication/tools/household_connect.py +145 -0
- package/assets/generated/authentication/tools/project_auth_recovery.py +716 -0
- package/assets/generated/core/core/process/ENGINEERING_BACKLOG.md +150 -0
- package/assets/generated/core/core/skills/manage-backlog/SKILL.md +115 -0
- package/assets/generated/core/core/skills/review-user-feedback/SKILL.md +50 -0
- package/assets/generated/core/tools/setup_maintenance/maintenance.py +162 -6
- package/assets/generated/deployment/docs/RECOVERY_READINESS.md +103 -0
- package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_capture.py +164 -0
- package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_guard.py +225 -0
- package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_lifecycle.py +455 -0
- package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_retention.py +220 -0
- package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_schedule.py +489 -0
- package/package.json +1 -1
- package/src/cli.mjs +32 -3
- 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.
|