You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(runtime)!: the /auth domain answers 501 instead of fabricating a login (#4113) (#4179)
`domains/auth.ts` carried a `mockAuthFallback` that answered
`POST /auth/sign-up/email`, `/register`, `/sign-in/email`, `/login`,
`GET /get-session` and `POST /sign-out` with 200 and a fabricated user plus a
24-hour `mock_token_*` session — for ANY email and ANY password, which was
never read. It shipped in `@objectstack/runtime`, not behind a dev-only
plugin, and gated on nothing but an empty `auth` slot, so
`os serve --preset minimal` and any embedder that mounts the dispatcher
without plugin-auth served it.
It was never a bypass: no session store backs that token, so
`resolve-execution-context.ts` still resolved anonymous and
`shouldDenyAnonymous` still denied data access. It was wrong in a different
way — it told the client the one thing a server must never lie about, that it
had authenticated someone, while discovery simultaneously reported
`auth: unavailable` and advertised no `routes.auth`.
Its stated justification ("MSW/browser-only environments") had no consumer in
this repo or in objectui, whose auth tests mock at the HTTP client layer. The
only things pinning it were nine tests asserting the mock itself.
ADR-0115 retired this class of fabricating fallback inside plugin-dev. This
was its last surviving member and the only one that shipped to production;
the lineage before it (#3891's analytics shim, #4000's dev stub, the three in
#4058/#4086, #4126's security trio) was retired the same way: deleted, not put
behind a flag, which would keep the code path in every release and become the
next thing nobody audits.
501, not 404, following `/i18n` — the nearest precedent in shape: a core
capability, a dispatcher-owned domain, an optional plugin behind it, and a
route discovery already declines to advertise when the slot is empty. The
route IS mounted; what is missing is the implementation behind it, which is
what 501 states and 404 would misdescribe. It also keeps faith with the one
true observation the mock was built on — its comment said it existed to keep
sign-in "from 404ing" — without the lie it answered that concern with. The
message names the remedy. A wrong-shaped occupant (a service without the
contract's `handleRequest`) takes the same 501, and that is the sharper case:
the slot is FILLED, so discovery advertises `routes.auth` while the request
used to get a fabricated session.
The `randomUUID` helper goes with it: a CSPRNG built solely to mint mock
session ids, dead the moment the mock is. `domains/auth.ts` drops 141 → 81
lines.
Two things the deletion left behind, both caught after the fact: the
route-envelope ledger's `handBuilt: 4` exemption for `auth.ts` drops to 0 —
those four better-auth-shaped bodies were the MOCK's, not the bridge's, since
the real service returns its own `Response` built nowhere in this file — and
`content/docs/permissions/authentication.mdx`, which documented all six
fabricating endpoints as a feature, now documents the 501 and points anyone
who relied on it at mocking in the HTTP client layer or loading AuthPlugin
(which needs no HTTP server).
Tests replace the nine that pinned the mock: all six formerly-mocked paths
parametrized so a partial re-introduction names itself, no `mock_`/user/session
in the body, the remedy named, an identical answer for any credentials (a 501
that varied by password would be an oracle), a registered service still
delegated to, and the wrong-shaped-occupant case.
Verified: runtime 940, plugin-hono-server 149, plugin-auth 579, rest 512 pass;
build 71/71; pnpm lint and all ten check:* scripts clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `/auth` domain no longer fabricates a login. With no auth service registered it answers **501**; the mock that answered 200 with a made-up session is deleted (#4113).
6
+
7
+
`packages/runtime/src/domains/auth.ts` carried a `mockAuthFallback` that answered `POST /auth/sign-up/email`, `/register`, `/sign-in/email`, `/login`, `GET /get-session` and `POST /sign-out` with **200 and a fabricated user plus a 24-hour `mock_token_*` session — for any email and any password, which was never read**. It shipped in `@objectstack/runtime` rather than behind a dev-only plugin, and gated on nothing but an empty `auth` slot, so `os serve --preset minimal` and any embedder that mounts the dispatcher without `@objectstack/plugin-auth` served it.
8
+
9
+
It was never a bypass: no session store backs the token, so `resolve-execution-context.ts` still resolved anonymous and `shouldDenyAnonymous` still denied data access. It was worse in a different way — it told the client the one thing a server must never lie about, that it had authenticated someone, while discovery simultaneously reported `auth: unavailable` and advertised no `routes.auth`. Its stated justification ("MSW/browser-only environments") had no consumer in this repository or in `objectui`, whose auth tests mock at the HTTP client layer; the only things pinning it were tests asserting the mock itself.
10
+
11
+
ADR-0115 retired this whole class of fabricating fallback inside `plugin-dev`. This was its last surviving member and the only one that shipped to production; the lineage before it — the #3891 analytics shim, #4000's dev stub, the three in #4058/#4086, #4126's security trio — was retired the same way: deleted, not put behind a flag.
12
+
13
+
**501 rather than 404**, following `/i18n`, the nearest precedent in shape: a core capability, a dispatcher-owned domain, an optional plugin behind it, and a route discovery already declines to advertise when the slot is empty. The route is mounted; what is missing is the implementation behind it — which is what 501 states and 404 would misdescribe. A wrong-shaped occupant (a service without the contract's `handleRequest`) takes the same 501, which is the sharper case: the slot is filled, so discovery advertises `routes.auth`, and that request previously got a fabricated session.
14
+
15
+
FROM → TO: a deployment without an auth service now gets `501 "Auth service not available — register @objectstack/plugin-auth to enable authentication"` on `/api/v1/auth/*` instead of a 200 carrying a session that never worked. Install `@objectstack/plugin-auth` (it is in the default `os serve` preset), or treat the absence as production already required — the 200 never produced a session the identity path accepted, so no working flow depended on it. Front-ends that mocked auth through this fallback should mock at the HTTP client layer or with an MSW handler, as `objectui` already does.
Copy file name to clipboardExpand all lines: content/docs/permissions/authentication.mdx
+11-12Lines changed: 11 additions & 12 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1068,22 +1068,21 @@ await kernel.bootstrap();
1068
1068
1069
1069
> ⚠️ **Warning:** The secret above is for **local development only**. In production, always use a strong random secret from an environment variable (`process.env.OS_AUTH_SECRET`).
1070
1070
1071
-
### Mock Fallback Endpoints
1071
+
### No auth service? `/auth/*` answers 501 — it never fabricates a session
1072
1072
1073
-
When no auth service handler is registered and the legacy broker login is unavailable, `HttpDispatcher.handleAuth()` automatically provides mock responses for:
1073
+
The dispatcher used to carry a **mock fallback**: with no auth service in the slot, `sign-up/email`, `register`, `sign-in/email`, `login`, `get-session` and `sign-out` all answered `200` with a made-up user and a 24-hour `mock_token_*` session — for any email and any password, which was never read.
1074
1074
1075
-
| Endpoint | Method | Description |
1076
-
|:---|:---:|:---|
1077
-
|`sign-up/email`| POST | Returns mock user + session |
1078
-
|`sign-in/email`| POST | Returns mock user + session |
1079
-
|`login`| POST | Legacy login — returns mock user + session |
1080
-
|`register`| POST | Alias for sign-up |
1081
-
|`get-session`| GET | Returns `{ session: null, user: null }`|
1082
-
|`sign-out`| POST | Returns `{ success: true }`|
1075
+
**That is removed** ([#4113](https://github.com/objectstack-ai/objectstack/issues/4113)). An unserved `auth` slot now answers:
1083
1076
1084
-
This ensures that registration and sign-in flows do not return 404 errors in MSW/browser-only environments.
1077
+
```
1078
+
501 Auth service not available — register @objectstack/plugin-auth to enable authentication
1079
+
```
1080
+
1081
+
It was never an authentication bypass — no session store backed that token, so the identity path still resolved anonymous and anonymous data access was still denied. The problem was that it told the client it had authenticated someone when it had not, while discovery simultaneously reported `auth: unavailable` and advertised no `routes.auth`. A capability the runtime does not have must not be advertised by pretending to serve it (ADR-0076 D12, ADR-0115).
1082
+
1083
+
A service registered in the slot but not implementing the contract's `handleRequest` takes the same 501.
1085
1084
1086
-
> **Note:**In server mode with AuthPlugin loaded, the auth service handler takes priority and the mock fallback is never reached. The mock fallback only activates when AuthPlugin is not loaded (e.g. browser-only Console/MSW builds where `better-auth` is unavailable).
1085
+
**If you were relying on the mock**for a browser-only or MSW build: mock at the HTTP client layer, or with an MSW handler in your own test setup, which is what the console does. Load `AuthPlugin` (it needs no HTTP server — see above) and the real service answers instead.
Copy file name to clipboardExpand all lines: docs/adr/0115-plugin-dev-assembly-not-stub-table.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -115,7 +115,7 @@ Three facts changed between ratification and execution, all shrinking the work:
115
115
1.**#4086's merged form deleted nothing.** It gated the six dispatcher domains on `handlerReady` and explicitly deferred "should the fabricators keep occupying slots" to this ADR's Tier A. So `ai` / `automation` / `notification` join Tier A's deletion list here — the Context table's "retired" row described their HTTP surface (gated to empty-slot answers since #4086), not their registration.
116
116
2.**#4089 closed at the source.**#4082 had already given core's five fallbacks their `__serviceInfo` self-descriptions, so Tier B's core half was done before PR-2 existed; what remained of Tier B was only plugin-dev's `metadata` copy and the four wrappers.
117
117
3.**With the core half gone, the PR-1/PR-2 boundary lost its reason** (Tier B no longer reached into core), and the maintainer directed completing all remaining work at once. The implementation therefore landed as one PR: Tiers A+B+C, the D6 guard, the D4 assembly auto-wire, and the D7 docs convergence — the full end state above, under a single FROM → TO changeset narrative.
118
-
4.**#4126 landed the D2 security trio + D6 guard as a first subset while the full PR was in flight.** Its choices are canonical where they overlap: the escape hatch is `OS_ALLOW_DEV_PLUGIN` (not the longer name this ADR first wrote), the guard is a module-level `assertNotProduction()`, and an empty security slot gets one loud boot-log warn ("RBAC, row-level security and field masking are NOT enforced") instead of silence. It also filed #4113 — the dispatcher's `/auth` domain carries its own mock fallback in `packages/runtime` — as the remaining fabricator OUTSIDE plugin-dev; retiring plugin-dev's `auth` stub neither worsens nor fixes that path (both the stub and the runtime mock fabricate a 200; neither yields a session the identity resolver accepts), so #4113 stays the one place that class of fake survives.
118
+
4. **#4126 landed the D2 security trio + D6 guard as a first subset while the full PR was in flight.** Its choices are canonical where they overlap: the escape hatch is `OS_ALLOW_DEV_PLUGIN` (not the longer name this ADR first wrote), the guard is a module-level `assertNotProduction()`, and an empty security slot gets one loud boot-log warn ("RBAC, row-level security and field masking are NOT enforced") instead of silence. It also filed #4113 — the dispatcher's `/auth` domain carries its own mock fallback in `packages/runtime` — as the remaining fabricator OUTSIDE plugin-dev; retiring plugin-dev's `auth` stub neither worsens nor fixes that path (both the stub and the runtime mock fabricate a 200; neither yields a session the identity resolver accepts), so #4113 stays the one place that class of fake survives. *(Update: #4113 is now closed too — the `/auth` mock is deleted and an unserved `auth` slot answers 501. That makes this ADR's rule hold platform-wide rather than only inside plugin-dev, and it was the one member of the class that shipped to production rather than to a dev assembly. 501 rather than the 404 an empty optional slot usually takes, following `/i18n`: the route is mounted, the implementation behind it is not, and 404 would misdescribe that.)*
0 commit comments