Skip to content

Commit 1af2903

Browse files
committed
docs(cors): state the transport's CORS default and the one variable an operator sets
HARDENING.md § CORS, the deployment pages and the plugin README now state what the CORS answer admits with and without an origin list, that '*' with credentials is refused at boot, and that OS_TRUSTED_ORIGINS is auth's list, not CORS's. Changeset for @objectstack/plugin-hono-server. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7cee813 commit 1af2903

6 files changed

Lines changed: 87 additions & 15 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/plugin-hono-server': minor
3+
---
4+
5+
fix(plugin-hono-server)!: the CORS default admits no cross-origin caller, and `'*'` with credentials is refused at boot
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a narrowing of the transport's CORS admission default plus a boot refusal of one configuration: no authorable metadata key, spelling, export, type or stored shape moves, so `objectstack migrate meta` has nothing to rewrite. The remedy is a deployment variable, which no ledger entry can derive. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this door (not already-registered); and the change is an admission verdict, not an interface, a type or an export (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
10+
11+
**BREAKING** — an admission narrowing of the transport's CORS answer, shipped as `minor` under the launch-window convention for accept-set narrowings. The governing text is `docs/HARDENING.md` § CORS: never `'*'` with credentials, and an explicit origin list.
12+
13+
- **Before.** With no origin list configured (no `cors.origins` option and no `OS_CORS_ORIGIN`), the CORS middleware admitted every origin for credentialed cross-origin requests. A configured `'*'` with credentials on was accepted and answered the same way.
14+
- **Now.** With no list, no cross-origin caller is admitted. A loopback origin (`localhost`, `127.0.0.1`, `[::1]`, any port) is admitted on a development boot only: `NODE_ENV` exactly `development`, which `os dev` and `os serve --dev` set. A list that contains `'*'` while credentials are on is refused in `init()`, so the boot fails, and the message names both remedies.
15+
- **Unchanged.** Same-origin requests need no CORS answer, so the Console under `/_console` and every page the server itself serves work as before. A configured list, exact origins or wildcard patterns, is admitted exactly as before, credentials included. `'*'` with credentials off still boots. `cors: false` and `OS_CORS_ENABLED=false` still disable the middleware. Allowed and exposed headers, methods and max-age are unchanged.
16+
17+
**What an operator does.** A front-end served from another origin that calls this server is now named: `OS_CORS_ORIGIN=https://app.example.com` (comma-separated for several), or the plugin's `cors.origins` option. A boot that now refuses `'*'` either names its origins or sets `OS_CORS_CREDENTIALS=false` to keep serving `'*'` without credentials. `OS_TRUSTED_ORIGINS`, auth's callback and CSRF list, admits nothing to CORS: a cross-origin front-end that signs in belongs in both lists.

‎content/docs/deployment/environment-variables.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ read at startup unless noted otherwise. Boolean variables accept `true` / `false
3636
> +100) and the banner prints the actual bound port. In **production**
3737
> (`os start`) a busy port is a hard error (exit 1) — it never silently
3838
> drifts, because a shifted port breaks reverse-proxy upstreams, `OS_AUTH_URL`
39-
> callbacks, and `OS_TRUSTED_ORIGINS` (CORS). Pin the port explicitly in
39+
> callbacks, and `OS_TRUSTED_ORIGINS` (auth CSRF). Pin the port explicitly in
4040
> production (`OS_PORT=8080 os start`) and keep `OS_AUTH_URL` / `OS_TRUSTED_ORIGINS`
4141
> in sync with it.
4242
@@ -123,8 +123,8 @@ Auth settings precedence:
123123
| Variable | Type | Default | Description |
124124
|:---|:---|:---|:---|
125125
| `OS_CORS_ENABLED` | boolean | `true` | Enable CORS middleware. |
126-
| `OS_CORS_ORIGIN` | csv | `*` | Allowed origins (comma-separated, or `*`). |
127-
| `OS_CORS_CREDENTIALS` | boolean | `true` | Allow credentialed cross-origin requests. |
126+
| `OS_CORS_ORIGIN` | csv | — | Origins admitted to cross-origin requests: exact origins or patterns such as `https://*.example.com`, comma-separated. Unset, no cross-origin caller is admitted (loopback origins excepted on a development boot); same-origin requests, the Console included, need no entry. `*` is refused at boot unless `OS_CORS_CREDENTIALS=false`. |
127+
| `OS_CORS_CREDENTIALS` | boolean | `true` | Allow credentialed cross-origin requests from the admitted origins. Must be `false` to serve `OS_CORS_ORIGIN=*`. |
128128
| `OS_CORS_MAX_AGE` | number | `86400` | Preflight cache TTL in seconds. |
129129

130130
---

‎content/docs/deployment/production-readiness.mdx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ the [HARDENING.md recipes](https://github.com/objectstack-ai/objectstack/blob/ma
118118
response `X-Request-Id` header.
119119
- [ ] better-auth session TTL / refresh / revoke verified (curl
120120
checklist in HARDENING.md).
121+
- [ ] CORS admits exactly the cross-origin front-ends you serve. With no
122+
`OS_CORS_ORIGIN` none is admitted (same-origin pages, the Console
123+
included, need no entry); otherwise it lists each origin explicitly,
124+
never `*` — the server refuses `*` at boot while credentials are on.
125+
See [HARDENING.md § CORS](https://github.com/objectstack-ai/objectstack/blob/main/docs/HARDENING.md#cors).
121126
- [ ] Cross-tenant negative tests in CI.
122127
- [ ] Multi-org deployments: tenant isolation is **active**, not degraded —
123128
confirm `features.degradedTenancy` is `false` in `/auth/config`. A

‎content/docs/deployment/self-hosting.mdx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,10 +411,17 @@ and keep three things in sync with the public origin:
411411

412412
```bash
413413
OS_AUTH_URL=https://app.example.com # auth callbacks / cookie origin
414-
OS_TRUSTED_ORIGINS=https://app.example.com # CORS allow-list
414+
OS_TRUSTED_ORIGINS=https://app.example.com # auth callback / CSRF allow-list
415415
OS_PORT=8080 # must match the proxy upstream
416416
```
417417

418+
CORS needs no entry here: the Console and the API share the public origin,
419+
and with no `OS_CORS_ORIGIN` the server admits no cross-origin caller. Only a
420+
front-end served from **another** origin needs one, listed explicitly
421+
(`OS_CORS_ORIGIN=https://portal.example.com`, comma-separated for several) —
422+
never `*`, which the server refuses at boot while credentials are on. See
423+
[Environment variables → CORS](/docs/deployment/environment-variables#cors).
424+
418425
```text title="Caddyfile"
419426
app.example.com {
420427
reverse_proxy localhost:8080

‎docs/HARDENING.md‎

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,31 @@ curl -H "Authorization: Bearer $TOK" $API/data/account
192192

193193
## CORS
194194

195-
CORS is intentionally **not** opinionated by the runtime — it's an
196-
app-level policy that depends on which origins host your front-end.
197-
Configure it on the Hono adapter:
195+
Which cross-origin callers are admitted is an app-level decision — it
196+
depends on which origins host your front-end — so the server that
197+
`os serve` / `os start` mount (`HonoServerPlugin`) admits only the
198+
origins you name:
199+
200+
| Configuration | What is admitted |
201+
| --- | --- |
202+
| No origin list (neither `OS_CORS_ORIGIN` nor the plugin's `cors.origins`) | **No cross-origin caller.** On a development boot (`NODE_ENV=development`, which `os dev` and `os serve --dev` set), loopback origins (`localhost`, `127.0.0.1`, `[::1]`, any port) are admitted too. |
203+
| A list (`OS_CORS_ORIGIN=https://app.example.com`, comma-separated; exact origins or patterns such as `https://*.example.com`) | Those origins, with credentials unless `OS_CORS_CREDENTIALS=false`. Loopback origins are admitted alongside any list. |
204+
| `*` while credentials are on | **Refused at boot** — the process exits naming the two remedies. |
205+
206+
Same-origin requests need no CORS answer, so the Console under
207+
`/_console` and any page the server itself serves work with no list at
208+
all. A front-end served from **another origin** needs exactly one
209+
variable:
210+
211+
```bash
212+
OS_CORS_ORIGIN=https://app.example.com
213+
```
214+
215+
`OS_TRUSTED_ORIGINS` is a different list — the origins auth accepts for
216+
its callbacks and CSRF checks — and admits nothing to CORS. A
217+
cross-origin front-end that signs in belongs in both.
218+
219+
Composing the Hono adapter yourself, name the origins there as well:
198220

199221
```ts
200222
import { createHonoApp } from '@objectstack/hono';
@@ -209,9 +231,10 @@ const app = createHonoApp({
209231
});
210232
```
211233

212-
Do **not** use `origin: '*'` with `credentials: true` — the
213-
combination is rejected by every modern browser anyway, but the
214-
misconfiguration is a common red flag in audits.
234+
Do **not** use `origin: '*'` with `credentials: true`: together they
235+
admit every origin for credentialed requests. `HonoServerPlugin`
236+
refuses the combination at boot; `*` is accepted only with credentials
237+
off (`OS_CORS_CREDENTIALS=false`), for an API that takes no cookies.
215238

216239
## Auditing
217240

‎packages/plugins/plugin-hono-server/README.md‎

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,24 @@ await kernel.bootstrap();
3535

3636
The Hono server plugin supports flexible CORS configuration with wildcard pattern matching.
3737

38+
### Default: no cross-origin caller
39+
40+
The CORS middleware is mounted by default, and it admits only the origins you name
41+
(`cors.origins`, else `OS_CORS_ORIGIN`). With neither set, **no cross-origin caller is
42+
admitted**. Same-origin requests need no CORS answer, so pages this server serves are
43+
unaffected. On a development boot (`NODE_ENV=development`, which `os dev` sets), loopback
44+
origins (`localhost`, `127.0.0.1`, `[::1]`, any port) are admitted too.
45+
46+
`'*'` while credentials are on (the default) is **refused at init**, so the boot fails.
47+
Name the origins instead, or turn credentials off to serve `'*'` to callers that send no
48+
cookies:
49+
50+
```typescript
51+
kernel.use(new HonoServerPlugin({
52+
cors: { origins: '*', credentials: false }
53+
}));
54+
```
55+
3856
### Basic CORS
3957

4058
```typescript
@@ -79,19 +97,21 @@ CORS can also be configured via environment variables:
7997

8098
```bash
8199
# Single origin
82-
CORS_ORIGIN=https://app.example.com
100+
OS_CORS_ORIGIN=https://app.example.com
83101

84102
# Wildcard patterns (comma-separated)
85-
CORS_ORIGIN=https://*.objectui.org,https://*.objectstack.ai
103+
OS_CORS_ORIGIN=https://*.objectui.org,https://*.objectstack.ai
86104

87105
# Disable CORS
88-
CORS_ENABLED=false
106+
OS_CORS_ENABLED=false
89107

90108
# Additional options
91-
CORS_CREDENTIALS=true
92-
CORS_MAX_AGE=86400
109+
OS_CORS_CREDENTIALS=true # default; must be false for OS_CORS_ORIGIN=*
110+
OS_CORS_MAX_AGE=86400
93111
```
94112

113+
The legacy `CORS_*` names are still read.
114+
95115
### Disable CORS
96116

97117
```typescript

0 commit comments

Comments
 (0)