@fuzdev/fuz_app 0.144.0 → 0.146.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/CLAUDE.md +7 -6
- package/dist/auth/CLAUDE.md +5 -2
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +2 -1
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +9 -2
- package/dist/auth/account_routes.d.ts +8 -0
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +12 -3
- package/dist/auth/request_context.d.ts +23 -0
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +32 -1
- package/dist/auth/token_scope.d.ts +22 -14
- package/dist/auth/token_scope.d.ts.map +1 -1
- package/dist/auth/token_scope.js +23 -14
- package/dist/db/CLAUDE.md +7 -3
- package/dist/db/fact_ddl.d.ts +1 -0
- package/dist/db/fact_ddl.d.ts.map +1 -1
- package/dist/db/fact_ddl.js +1 -0
- package/dist/db/fact_holder_queries.d.ts +8 -4
- package/dist/db/fact_holder_queries.d.ts.map +1 -1
- package/dist/db/fact_holder_queries.js +9 -5
- package/dist/db/fact_queries.d.ts +10 -7
- package/dist/db/fact_queries.d.ts.map +1 -1
- package/dist/db/fact_queries.js +31 -10
- package/dist/http/CLAUDE.md +2 -2
- package/dist/http/auth_shape.d.ts.map +1 -1
- package/dist/http/auth_shape.js +11 -6
- package/dist/http/proxy.d.ts +22 -21
- package/dist/http/proxy.d.ts.map +1 -1
- package/dist/http/proxy.js +28 -33
- package/dist/http/route_spec.d.ts +8 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +1 -1
- package/dist/server/fact_security_headers.d.ts +59 -0
- package/dist/server/fact_security_headers.d.ts.map +1 -0
- package/dist/server/fact_security_headers.js +58 -0
- package/dist/server/serve_fact_route.d.ts +21 -10
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +41 -50
- package/dist/server/x_accel.d.ts +135 -31
- package/dist/server/x_accel.d.ts.map +1 -1
- package/dist/server/x_accel.js +652 -88
- package/dist/testing/CLAUDE.md +73 -22
- package/dist/testing/cross_backend/capabilities.d.ts +0 -18
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +1 -4
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +0 -7
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/fact_serving.js +85 -12
- package/dist/testing/cross_backend/identity_parity.d.ts +11 -0
- package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/identity_parity.js +124 -9
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +28 -0
- package/dist/testing/cross_backend/revocation_drivers.d.ts +70 -0
- package/dist/testing/cross_backend/revocation_drivers.d.ts.map +1 -0
- package/dist/testing/cross_backend/revocation_drivers.js +104 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts +6 -1
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/sse_round_trip.js +52 -16
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +33 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +13 -1
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +155 -39
- package/package.json +3 -3
package/dist/db/fact_queries.js
CHANGED
|
@@ -113,6 +113,21 @@ export const query_delete_fact = async (deps, hash) => {
|
|
|
113
113
|
return null;
|
|
114
114
|
return { size: Number(row.size), external_url: row.external_url };
|
|
115
115
|
};
|
|
116
|
+
/**
|
|
117
|
+
* The grace-window half of the orphan cutoff: no `fact_holder` row was
|
|
118
|
+
* recorded (or refreshed) at or after `$1`. A dedup put adds no `fact` row,
|
|
119
|
+
* so `fact.created_at` stays at the first insert — without this, an old
|
|
120
|
+
* orphan an actor re-uploads looks stale, and a sweep between the upload and
|
|
121
|
+
* the cell write that references it deletes the fact (and, by cascade, the
|
|
122
|
+
* possession the write needs). A fact counts as recent when its own
|
|
123
|
+
* `created_at` or any holder's is within the window. Served by
|
|
124
|
+
* `idx_fact_holder_hash`. Twin of the Rust `ORPHAN_FACT_NOT_RECENTLY_HELD`.
|
|
125
|
+
*/
|
|
126
|
+
const ORPHAN_FACT_NOT_RECENTLY_HELD = `NOT EXISTS (
|
|
127
|
+
SELECT 1 FROM fact_holder h
|
|
128
|
+
WHERE h.hash = f.hash
|
|
129
|
+
AND h.created_at >= $1::timestamptz
|
|
130
|
+
)`;
|
|
116
131
|
/**
|
|
117
132
|
* Compute the "orphan facts" set: rows in `fact` where no active
|
|
118
133
|
* (non-tombstone) `cell.refs` array contains the hash.
|
|
@@ -124,14 +139,15 @@ export const query_delete_fact = async (deps, hash) => {
|
|
|
124
139
|
* outputs? external fact mirrors?) the predicate moves to a generic
|
|
125
140
|
* `fact_consumers` registry; today the cell layer is the only consumer.
|
|
126
141
|
*
|
|
127
|
-
* The `older_than` filter
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
142
|
+
* The `older_than` filter is the grace cutoff: a fact passes it when both
|
|
143
|
+
* its `created_at` and every `fact_holder` row's are older (a re-put
|
|
144
|
+
* refreshes the holder row). Pass `null` to skip the filter (used by the
|
|
145
|
+
* list-summary preview); the delete handler always passes a non-null
|
|
146
|
+
* cutoff (default 0, meaning "any orphan").
|
|
131
147
|
*
|
|
132
148
|
* @param deps - query deps
|
|
133
|
-
* @param older_than - filter to facts created
|
|
134
|
-
* to skip)
|
|
149
|
+
* @param older_than - filter to facts created, and last put by any holder,
|
|
150
|
+
* before this Date (or null to skip)
|
|
135
151
|
* @param sample_limit - row cap for the returned `sample`
|
|
136
152
|
*/
|
|
137
153
|
export const query_orphan_facts_list = async (deps, older_than, sample_limit) => {
|
|
@@ -142,7 +158,8 @@ export const query_orphan_facts_list = async (deps, older_than, sample_limit) =>
|
|
|
142
158
|
WHERE c.refs @> ARRAY[f.hash]::text[]
|
|
143
159
|
AND c.deleted_at IS NULL
|
|
144
160
|
)
|
|
145
|
-
AND ($1::timestamptz IS NULL
|
|
161
|
+
AND ($1::timestamptz IS NULL
|
|
162
|
+
OR (f.created_at < $1::timestamptz AND ${ORPHAN_FACT_NOT_RECENTLY_HELD}))`, [older_than]);
|
|
146
163
|
const sample_rows = await deps.db.query(`SELECT hash, size, ${iso8601_timestamp_column('f', 'created_at')} AS created_at, external_url
|
|
147
164
|
FROM fact f
|
|
148
165
|
WHERE NOT EXISTS (
|
|
@@ -150,7 +167,8 @@ export const query_orphan_facts_list = async (deps, older_than, sample_limit) =>
|
|
|
150
167
|
WHERE c.refs @> ARRAY[f.hash]::text[]
|
|
151
168
|
AND c.deleted_at IS NULL
|
|
152
169
|
)
|
|
153
|
-
AND ($1::timestamptz IS NULL
|
|
170
|
+
AND ($1::timestamptz IS NULL
|
|
171
|
+
OR (f.created_at < $1::timestamptz AND ${ORPHAN_FACT_NOT_RECENTLY_HELD}))
|
|
154
172
|
ORDER BY f.created_at ASC
|
|
155
173
|
LIMIT $2`, [older_than, sample_limit]);
|
|
156
174
|
return {
|
|
@@ -169,7 +187,9 @@ export const query_orphan_facts_list = async (deps, older_than, sample_limit) =>
|
|
|
169
187
|
* (no row-count limit) — callers iterate to unlink disk files. The
|
|
170
188
|
* `older_than` cutoff is required (non-null) here: bulk delete should
|
|
171
189
|
* always be operator-scoped to a time window. A "delete all" sweep
|
|
172
|
-
* passes a far-future cutoff, not `null`.
|
|
190
|
+
* passes a far-future cutoff, not `null`. It is the same grace cutoff as
|
|
191
|
+
* `query_orphan_facts_list`: a fact put — or re-put, deduplicated — by an
|
|
192
|
+
* actor within the window is kept, whatever its own `created_at`.
|
|
173
193
|
*/
|
|
174
194
|
export const query_orphan_facts_select_for_delete = async (deps, older_than) => {
|
|
175
195
|
const rows = await deps.db.query(`SELECT hash, size, external_url
|
|
@@ -179,6 +199,7 @@ export const query_orphan_facts_select_for_delete = async (deps, older_than) =>
|
|
|
179
199
|
WHERE c.refs @> ARRAY[f.hash]::text[]
|
|
180
200
|
AND c.deleted_at IS NULL
|
|
181
201
|
)
|
|
182
|
-
AND f.created_at < $1::timestamptz
|
|
202
|
+
AND f.created_at < $1::timestamptz
|
|
203
|
+
AND ${ORPHAN_FACT_NOT_RECENTLY_HELD}`, [older_than]);
|
|
183
204
|
return rows.map((r) => ({ hash: r.hash, size: Number(r.size), external_url: r.external_url }));
|
|
184
205
|
};
|
package/dist/http/CLAUDE.md
CHANGED
|
@@ -52,7 +52,7 @@ consumers.
|
|
|
52
52
|
- `input: z.ZodType` — request body schema; `z.null()` for no-body (GET/DELETE)
|
|
53
53
|
- `output: z.ZodType` — success response schema
|
|
54
54
|
- `mints_credential?: boolean` — the route issues a session cookie while declaring no auth (login, signup, bootstrap); assembly refuses it outside `AUTH_MIDDLEWARE_PATH`, as it does any route whose `auth.account` is not `'none'` (`assert_credential_routes_in_auth_scope` in `auth/middleware.ts`)
|
|
55
|
-
- `reads_credential?: boolean` — the handler reads the resolved account while the route declares no auth gate, answering an anonymous caller itself (account status, the cell-scoped fact read, `create_rpc_endpoint`'s pair, whose dispatcher gates per action); held inside `AUTH_MIDDLEWARE_PATH` by the same assertion
|
|
55
|
+
- `reads_credential?: boolean` — the handler reads the resolved account while the route declares no auth gate, answering an anonymous caller itself (account status, the cell-scoped fact read, `create_rpc_endpoint`'s pair, whose dispatcher gates per action); held inside `AUTH_MIDDLEWARE_PATH` by the same assertion. Such a handler reads the account through `get_public_account_id` (`auth/request_context.ts`), which answers a scope-narrowed token as anonymous — the RPC pair excepted, its dispatcher gating each method
|
|
56
56
|
- `rate_limit?: RateLimitKey` — metadata only (`'ip' | 'account' | 'both'`); auto-derives 429
|
|
57
57
|
- `errors?: RouteErrorSchemas` — handler-specific error schemas keyed by HTTP status
|
|
58
58
|
- `transaction?: boolean` — declarative transaction wrapping (see below)
|
|
@@ -315,7 +315,7 @@ rate-limiting middleware (see root ../../../CLAUDE.md §Middleware Ordering).
|
|
|
315
315
|
|
|
316
316
|
Per-symbol semantics on TSDoc; the cross-cutting properties:
|
|
317
317
|
|
|
318
|
-
- **Rightmost-first XFF walk
|
|
318
|
+
- **Rightmost-first XFF walk, skipping trusted entries; the first entry that fails strict validation stops the walk and the request keys on the connection IP.** Stopping, not skipping, keeps a client-written entry left of a non-bare-IP hop (`ip:port`, `unknown`) from picking the bucket.
|
|
319
319
|
- **`validate_ip_strict(ip)`** is defensive against Hono's lax `distinctRemoteAddr` (which classifies anything-with-colons as IPv6 and accepts `'[::1]:8080'`, `'::1\n'` as binary-valid). Two-layer check: character-set pre-filter + round-trip through `convertIPv*ToBinary`.
|
|
320
320
|
- **`normalize_ip(ip)`** delegates to `canonicalize_ip` from `http/ip_canonical.ts` — RFC 5952 lowercase + longest-zero-run compression, IPv4-mapped IPv6 stripped to plain IPv4 so buckets collapse. Idempotent, safe on non-IP strings, strict char-set filter preserves malformed forms unchanged.
|
|
321
321
|
- **Three-branch middleware logic**: no XFF → use connection IP; XFF + connection untrusted → ignore XFF, use connection IP (spoof-proof, debug log); XFF + connection trusted → resolve from header, warn if all entries turn out trusted.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth_shape.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/auth_shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,WAAW,6DAGtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,SAGtD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa;;;;EAA2C,CAAC;AACtE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"auth_shape.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/auth_shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,WAAW,6DAGtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,SAGtD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa;;;;EAA2C,CAAC;AACtE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;kBA8EnB,CAAC;AACJ,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAUlD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,KAAG,OACA,CAAC;AAElD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,SAAS,KAAG,OAAgC,CAAC;AAE/E;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,SAAS,KAAG,OAAkC,CAAC;AAEnF,wEAAwE;AACxE,eAAO,MAAM,YAAY,GAAI,MAAM,SAAS,KAAG,OAA+B,CAAC;AAE/E,8FAA8F;AAC9F,eAAO,MAAM,wBAAwB,GAAI,MAAM,SAAS,KAAG,OAC3B,CAAC;AAEjC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,KAAG,OACQ,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,SAAS,KAAG,OACwB,CAAC;AAYvF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAIhE,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sCAAsC,GAClD,MAAM,SAAS,EACf,OAAO,WAAW,EAClB,SAAS,MAAM,KACb,IAmBF,CAAC"}
|
package/dist/http/auth_shape.js
CHANGED
|
@@ -138,9 +138,9 @@ export const RouteAuth = z
|
|
|
138
138
|
*
|
|
139
139
|
* - `surface:<name>` — a non-RPC surface, refused to every narrowed token
|
|
140
140
|
* whatever its method list says (token scoping's rule that a narrowed
|
|
141
|
-
* token is RPC-only). `surface:audit_stream`, `surface:fact_bare`,
|
|
142
|
-
* `surface:app_surface` are the spine's
|
|
143
|
-
* its own without registering it.
|
|
141
|
+
* token is RPC-only). `surface:audit_stream`, `surface:fact_bare`,
|
|
142
|
+
* `surface:account_verify`, and `surface:app_surface` are the spine's
|
|
143
|
+
* route-spec ones; a consumer names its own without registering it.
|
|
144
144
|
* - `rpc:<method>` — one action method, refused unless the token lists it.
|
|
145
145
|
* What a route bridged from an `ActionSpec` declares: it carries the
|
|
146
146
|
* method's identity but never reaches the dispatcher's per-method gate.
|
|
@@ -151,9 +151,14 @@ export const RouteAuth = z
|
|
|
151
151
|
* account lacks the role would be told about the role rather than about the
|
|
152
152
|
* scope).
|
|
153
153
|
*
|
|
154
|
-
* Opt-in rather than blanket:
|
|
155
|
-
*
|
|
156
|
-
*
|
|
154
|
+
* Opt-in rather than blanket: no narrowed token reaches login (which reads
|
|
155
|
+
* no credential) or logout and the password change (session-only), the
|
|
156
|
+
* RPC endpoint runs its own per-method check inside the dispatcher, the
|
|
157
|
+
* `reads_credential` routes that answer anonymous callers (account status,
|
|
158
|
+
* the cell-scoped fact read) can't take the field and answer a narrowed
|
|
159
|
+
* token as anonymous in their handler (`get_public_account_id`), and the
|
|
160
|
+
* server-status route admits a narrowed token by design — and all are
|
|
161
|
+
* route specs too.
|
|
157
162
|
*
|
|
158
163
|
* Typed as a plain string here to keep `http/` from importing `auth/`; the
|
|
159
164
|
* resolver parses it at registration and throws on a malformed one.
|
package/dist/http/proxy.d.ts
CHANGED
|
@@ -102,30 +102,30 @@ export declare const is_trusted_ip: (ip: string, proxies: Array<ParsedProxy>) =>
|
|
|
102
102
|
/**
|
|
103
103
|
* Resolve the real client IP from an `X-Forwarded-For` header value.
|
|
104
104
|
*
|
|
105
|
-
* Walks right-to-left, skipping trusted proxy entries
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
105
|
+
* Walks right-to-left, skipping trusted proxy entries. The first untrusted
|
|
106
|
+
* entry is the client IP. The walk **stops** at the first entry that fails
|
|
107
|
+
* strict IP validation (`validate_ip_strict`) and returns `undefined`, so the
|
|
108
|
+
* middleware falls back to the connection IP: an entry is only as
|
|
109
|
+
* trustworthy as the hop that wrote it, and everything left of a malformed
|
|
110
|
+
* one was written before any trusted hop vouched for it. If every entry is
|
|
111
|
+
* trusted, returns the leftmost one (likely-misconfigured all-trusted case).
|
|
112
|
+
* Empty entries are dropped before the walk. All entries are normalized
|
|
113
|
+
* before matching and in the returned value.
|
|
114
|
+
*
|
|
115
|
+
* Stopping, not skipping, is the rate-limit-key rule. Returning a malformed
|
|
116
|
+
* entry would let an attacker rotate arbitrary strings (incl.
|
|
117
117
|
* `'attacker:controlled'`, which Hono's lax `distinctRemoteAddr`
|
|
118
|
-
* misclassifies as IPv6)
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* behind
|
|
124
|
-
*
|
|
118
|
+
* misclassifies as IPv6) into fresh per-IP rate-limit buckets; skipping it
|
|
119
|
+
* and walking on would hand the key to the entry the client wrote whenever a
|
|
120
|
+
* trusted hop appends a non-bare-IP token (`ip:port`, `[v6]:port`,
|
|
121
|
+
* `unknown`, `unix:`). Tradeoff: behind such a proxy every client collapses
|
|
122
|
+
* to the proxy's connection IP in rate limiting (one bucket for everyone
|
|
123
|
+
* behind it). Standard proxies (nginx's `$remote_addr`, cloud LBs) append
|
|
124
|
+
* bare IPs.
|
|
125
125
|
*
|
|
126
126
|
* @param forwarded_for - the `X-Forwarded-For` header value
|
|
127
127
|
* @param proxies - parsed trusted proxy entries
|
|
128
|
-
* @returns the normalized client IP, or `undefined` if the header is empty
|
|
128
|
+
* @returns the normalized client IP, or `undefined` if the header is empty or the walk met a malformed entry
|
|
129
129
|
*/
|
|
130
130
|
export declare const resolve_client_ip: (forwarded_for: string, proxies: Array<ParsedProxy>) => string | undefined;
|
|
131
131
|
/**
|
|
@@ -139,7 +139,8 @@ export declare const resolve_client_ip: (forwarded_for: string, proxies: Array<P
|
|
|
139
139
|
* 2. `X-Forwarded-For` present but connection is untrusted → ignore header
|
|
140
140
|
* (spoofed by a direct attacker), use connection IP.
|
|
141
141
|
* 3. `X-Forwarded-For` present and connection is trusted → walk header
|
|
142
|
-
* right-to-left, strip trusted entries, use first untrusted entry
|
|
142
|
+
* right-to-left, strip trusted entries, use first untrusted entry; a
|
|
143
|
+
* malformed entry stops the walk and the connection IP is used.
|
|
143
144
|
*
|
|
144
145
|
* @param options - trusted proxy configuration
|
|
145
146
|
* @mutates `c.var.client_ip` - set to the resolved (or `'unknown'`) client IP per request
|
package/dist/http/proxy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAEvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY,GAAI,IAAI,MAAM,KAAG,MAA6B,CAAC;AAExE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,sFAAsF;IACtF,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,+DAA+D;IAC/D,iBAAiB,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;IACtD,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,GACpB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC;AAEpF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,KAAG,WA6CjD,CAAC;AAiBF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,kBAAkB,GAAI,IAAI,MAAM,KAAG,MAAM,GAAG,MAAM,GAAG,SAWjE,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,IAAI,MAAM,EAAE,SAAS,KAAK,CAAC,WAAW,CAAC,KAAG,OAqBvE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,iBAAiB,GAC7B,eAAe,MAAM,EACrB,SAAS,KAAK,CAAC,WAAW,CAAC,KACzB,MAAM,GAAG,
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAEvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY,GAAI,IAAI,MAAM,KAAG,MAA6B,CAAC;AAExE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,sFAAsF;IACtF,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,+DAA+D;IAC/D,iBAAiB,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;IACtD,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,GACpB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC;AAEpF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,KAAG,WA6CjD,CAAC;AAiBF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,kBAAkB,GAAI,IAAI,MAAM,KAAG,MAAM,GAAG,MAAM,GAAG,SAWjE,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,IAAI,MAAM,EAAE,SAAS,KAAK,CAAC,WAAW,CAAC,KAAG,OAqBvE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,iBAAiB,GAC7B,eAAe,MAAM,EACrB,SAAS,KAAK,CAAC,WAAW,CAAC,KACzB,MAAM,GAAG,SAqBX,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,uBAAuB,GAAI,SAAS,YAAY,KAAG,iBAyC/D,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GAAI,SAAS,YAAY,KAAG,cAInE,CAAC"}
|
package/dist/http/proxy.js
CHANGED
|
@@ -172,30 +172,30 @@ export const is_trusted_ip = (ip, proxies) => {
|
|
|
172
172
|
/**
|
|
173
173
|
* Resolve the real client IP from an `X-Forwarded-For` header value.
|
|
174
174
|
*
|
|
175
|
-
* Walks right-to-left, skipping trusted proxy entries
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
175
|
+
* Walks right-to-left, skipping trusted proxy entries. The first untrusted
|
|
176
|
+
* entry is the client IP. The walk **stops** at the first entry that fails
|
|
177
|
+
* strict IP validation (`validate_ip_strict`) and returns `undefined`, so the
|
|
178
|
+
* middleware falls back to the connection IP: an entry is only as
|
|
179
|
+
* trustworthy as the hop that wrote it, and everything left of a malformed
|
|
180
|
+
* one was written before any trusted hop vouched for it. If every entry is
|
|
181
|
+
* trusted, returns the leftmost one (likely-misconfigured all-trusted case).
|
|
182
|
+
* Empty entries are dropped before the walk. All entries are normalized
|
|
183
|
+
* before matching and in the returned value.
|
|
183
184
|
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* without the skip, an attacker could rotate arbitrary strings (incl.
|
|
185
|
+
* Stopping, not skipping, is the rate-limit-key rule. Returning a malformed
|
|
186
|
+
* entry would let an attacker rotate arbitrary strings (incl.
|
|
187
187
|
* `'attacker:controlled'`, which Hono's lax `distinctRemoteAddr`
|
|
188
|
-
* misclassifies as IPv6)
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* behind
|
|
194
|
-
*
|
|
188
|
+
* misclassifies as IPv6) into fresh per-IP rate-limit buckets; skipping it
|
|
189
|
+
* and walking on would hand the key to the entry the client wrote whenever a
|
|
190
|
+
* trusted hop appends a non-bare-IP token (`ip:port`, `[v6]:port`,
|
|
191
|
+
* `unknown`, `unix:`). Tradeoff: behind such a proxy every client collapses
|
|
192
|
+
* to the proxy's connection IP in rate limiting (one bucket for everyone
|
|
193
|
+
* behind it). Standard proxies (nginx's `$remote_addr`, cloud LBs) append
|
|
194
|
+
* bare IPs.
|
|
195
195
|
*
|
|
196
196
|
* @param forwarded_for - the `X-Forwarded-For` header value
|
|
197
197
|
* @param proxies - parsed trusted proxy entries
|
|
198
|
-
* @returns the normalized client IP, or `undefined` if the header is empty
|
|
198
|
+
* @returns the normalized client IP, or `undefined` if the header is empty or the walk met a malformed entry
|
|
199
199
|
*/
|
|
200
200
|
export const resolve_client_ip = (forwarded_for, proxies) => {
|
|
201
201
|
const entries = [];
|
|
@@ -206,26 +206,20 @@ export const resolve_client_ip = (forwarded_for, proxies) => {
|
|
|
206
206
|
}
|
|
207
207
|
if (entries.length === 0)
|
|
208
208
|
return undefined;
|
|
209
|
-
// Walk from right to left,
|
|
210
|
-
//
|
|
211
|
-
//
|
|
209
|
+
// Walk from right to left, skipping trusted proxies; stop at a malformed
|
|
210
|
+
// entry. Everything left of it is client-written, so neither it nor any
|
|
211
|
+
// entry past it may pick the per-IP rate-limit key.
|
|
212
212
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
213
213
|
const entry = entries[i];
|
|
214
214
|
if (!validate_ip_strict(entry))
|
|
215
|
-
|
|
215
|
+
return undefined;
|
|
216
216
|
if (!is_trusted_ip(entry, proxies)) {
|
|
217
217
|
return entry;
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
|
-
// Every entry was
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
// and let the middleware fall back to the connection IP.
|
|
224
|
-
for (const entry of entries) {
|
|
225
|
-
if (validate_ip_strict(entry))
|
|
226
|
-
return entry;
|
|
227
|
-
}
|
|
228
|
-
return undefined;
|
|
220
|
+
// Every entry was valid and trusted — the leftmost one; the
|
|
221
|
+
// misconfiguration warn in the middleware fires on it.
|
|
222
|
+
return entries[0];
|
|
229
223
|
};
|
|
230
224
|
/**
|
|
231
225
|
* Create a Hono middleware that resolves the client IP from trusted proxies.
|
|
@@ -238,7 +232,8 @@ export const resolve_client_ip = (forwarded_for, proxies) => {
|
|
|
238
232
|
* 2. `X-Forwarded-For` present but connection is untrusted → ignore header
|
|
239
233
|
* (spoofed by a direct attacker), use connection IP.
|
|
240
234
|
* 3. `X-Forwarded-For` present and connection is trusted → walk header
|
|
241
|
-
* right-to-left, strip trusted entries, use first untrusted entry
|
|
235
|
+
* right-to-left, strip trusted entries, use first untrusted entry; a
|
|
236
|
+
* malformed entry stops the walk and the connection IP is used.
|
|
242
237
|
*
|
|
243
238
|
* @param options - trusted proxy configuration
|
|
244
239
|
* @mutates `c.var.client_ip` - set to the resolved (or `'unknown'`) client IP per request
|
|
@@ -169,6 +169,14 @@ export interface RouteSpec {
|
|
|
169
169
|
* and it would see every caller as anonymous, so assembly refuses it
|
|
170
170
|
* there (`assert_credential_routes_in_auth_scope`), as it does a route
|
|
171
171
|
* whose `auth.account` is not `'none'`.
|
|
172
|
+
*
|
|
173
|
+
* The handler owns the token-scope rule a gate would otherwise apply: a
|
|
174
|
+
* scope-narrowed API token reaches no non-RPC surface that reads account
|
|
175
|
+
* data, and `auth.required_scope` is refused on a route that admits
|
|
176
|
+
* anonymous callers. Read the credential through `get_public_account_id`
|
|
177
|
+
* (`auth/request_context.ts`), not `ACCOUNT_ID_KEY`, so a narrowed token is
|
|
178
|
+
* answered as the anonymous caller. The RPC endpoint is the exception — its
|
|
179
|
+
* dispatcher gates each method against the scope.
|
|
172
180
|
*/
|
|
173
181
|
reads_credential?: boolean;
|
|
174
182
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/route_spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAW,IAAI,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACtE,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EACN,KAAK,iBAAiB,EACtB,KAAK,YAAY,EAMjB,MAAM,oBAAoB,CAAC;AAU5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAA0C,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEzF;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,UAAU;IAC1B,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,CAAC,CAAC;IAC5C,kBAAkB,EAAE,KAAK,CAAC,iBAAiB,CAAC,CAAC;CAC7C;AAED;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,UAAU,CAAC;AAEhE;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;AAE7F,6CAA6C;AAC7C,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEtE;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC5B;;;OAGG;IACH,EAAE,EAAE,EAAE,CAAC;IACP;;;;;OAKG;IACH,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACtC;;;;;;;OAOG;IACH,mBAAmB,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CACvD;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE7F;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACzB,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,YAAY,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACrB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACpB,mEAAmE;IACnE,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,oCAAoC;IACpC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC;IAClB;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B
|
|
1
|
+
{"version":3,"file":"route_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/route_spec.ts"],"names":[],"mappings":"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"}
|
package/dist/server/env.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAA4B,KAAK,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG5E;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAA4B,KAAK,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG5E;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;kBAqDxB,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,wBAAwB,GAAG,qBAAqB,CAAC;IACxD,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAED,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,qBAAqB,CAAC;AAE9E;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,GAAI,KAAK,aAAa,KAAG,sBAmBxD,CAAC"}
|
package/dist/server/env.js
CHANGED
|
@@ -69,7 +69,7 @@ export const BaseServerEnv = z.strictObject({
|
|
|
69
69
|
description: 'Directory for referenced (large) fact bytes, sharded <shard>/<rest>'
|
|
70
70
|
}),
|
|
71
71
|
FUZ_FACTS_X_ACCEL_REDIRECT_PREFIX: z.string().optional().meta({
|
|
72
|
-
description: 'Internal nginx prefix for X-Accel-Redirect fact delivery (production only)'
|
|
72
|
+
description: 'Internal nginx prefix for X-Accel-Redirect fact delivery, ending in `/` (production only)'
|
|
73
73
|
})
|
|
74
74
|
});
|
|
75
75
|
/**
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The untrusted-content hardening headers every served fact carries.
|
|
3
|
+
*
|
|
4
|
+
* Fact bytes are served with a producer-supplied `content_type` emitted
|
|
5
|
+
* verbatim, inline. To stop a fact stored as (or sniffable to) `text/html` from
|
|
6
|
+
* executing as stored XSS for any reader of a referencing cell, every fact
|
|
7
|
+
* response carries `FACT_SECURITY_HEADERS`: `X-Content-Type-Options: nosniff` +
|
|
8
|
+
* `Content-Security-Policy: default-src 'none'; sandbox`.
|
|
9
|
+
*
|
|
10
|
+
* The embedded and disk-stream branches of `server/serve_fact_route.ts` set them
|
|
11
|
+
* on the response directly. On the `X-Accel-Redirect` branch nginx serves the
|
|
12
|
+
* bytes from its internal facts location, and a location with its own
|
|
13
|
+
* `add_header` stops the server-level headers from applying — so that location
|
|
14
|
+
* must carry the set itself, as the lines `fact_security_header_directives`
|
|
15
|
+
* renders. The X-Accel validator (`server/x_accel.ts`) refuses a facts location
|
|
16
|
+
* without them.
|
|
17
|
+
*
|
|
18
|
+
* The Rust twin is `fuz_fact_serving::fact_security_headers`; both pin the same
|
|
19
|
+
* literals.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* `X-Content-Type-Options` for fact responses — block MIME sniffing so an
|
|
25
|
+
* `application/octet-stream` fact can't be sniffed into executable HTML.
|
|
26
|
+
*/
|
|
27
|
+
export declare const FACT_X_CONTENT_TYPE_OPTIONS = "nosniff";
|
|
28
|
+
/**
|
|
29
|
+
* `Content-Security-Policy` for fact responses. A fact stored as (or sniffable
|
|
30
|
+
* to) `text/html` would otherwise execute as stored XSS for any reader of a
|
|
31
|
+
* referencing cell. `default-src 'none'; sandbox` neutralizes script execution
|
|
32
|
+
* and sub-resource loads even when a fact is rendered inline; harmless for
|
|
33
|
+
* directly-served images (they load no sub-resources).
|
|
34
|
+
*/
|
|
35
|
+
export declare const FACT_CONTENT_SECURITY_POLICY = "default-src 'none'; sandbox";
|
|
36
|
+
/**
|
|
37
|
+
* `Cache-Control` for fact responses — private, five minutes: the window a
|
|
38
|
+
* cached read outlives a revoked view.
|
|
39
|
+
*/
|
|
40
|
+
export declare const FACT_CACHE_CONTROL = "private, max-age=300";
|
|
41
|
+
/**
|
|
42
|
+
* The fact hardening headers, in render order. `Content-Disposition:
|
|
43
|
+
* attachment` is deliberately absent — it would force-download and break
|
|
44
|
+
* legitimate inline image rendering.
|
|
45
|
+
*/
|
|
46
|
+
export declare const FACT_SECURITY_HEADERS: {
|
|
47
|
+
readonly 'X-Content-Type-Options': "nosniff";
|
|
48
|
+
readonly 'Content-Security-Policy': "default-src 'none'; sandbox";
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The exact nginx `add_header … always;` lines the internal facts location must
|
|
52
|
+
* carry, rendered from `FACT_SECURITY_HEADERS`. `always` makes nginx add the
|
|
53
|
+
* header on every status, not just 2xx/3xx. The X-Accel validator refuses a
|
|
54
|
+
* facts location missing any of them.
|
|
55
|
+
*
|
|
56
|
+
* @returns one `add_header` directive per header, each ending in `always;`
|
|
57
|
+
*/
|
|
58
|
+
export declare const fact_security_header_directives: () => Array<string>;
|
|
59
|
+
//# sourceMappingURL=fact_security_headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fact_security_headers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/fact_security_headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;GAGG;AACH,eAAO,MAAM,2BAA2B,YAAY,CAAC;AAErD;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,gCAAgC,CAAC;AAE1E;;;GAGG;AACH,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AAEzD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB;;;CAGxB,CAAC;AAEX;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,QAAO,KAAK,CAAC,MAAM,CAG7D,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The untrusted-content hardening headers every served fact carries.
|
|
3
|
+
*
|
|
4
|
+
* Fact bytes are served with a producer-supplied `content_type` emitted
|
|
5
|
+
* verbatim, inline. To stop a fact stored as (or sniffable to) `text/html` from
|
|
6
|
+
* executing as stored XSS for any reader of a referencing cell, every fact
|
|
7
|
+
* response carries `FACT_SECURITY_HEADERS`: `X-Content-Type-Options: nosniff` +
|
|
8
|
+
* `Content-Security-Policy: default-src 'none'; sandbox`.
|
|
9
|
+
*
|
|
10
|
+
* The embedded and disk-stream branches of `server/serve_fact_route.ts` set them
|
|
11
|
+
* on the response directly. On the `X-Accel-Redirect` branch nginx serves the
|
|
12
|
+
* bytes from its internal facts location, and a location with its own
|
|
13
|
+
* `add_header` stops the server-level headers from applying — so that location
|
|
14
|
+
* must carry the set itself, as the lines `fact_security_header_directives`
|
|
15
|
+
* renders. The X-Accel validator (`server/x_accel.ts`) refuses a facts location
|
|
16
|
+
* without them.
|
|
17
|
+
*
|
|
18
|
+
* The Rust twin is `fuz_fact_serving::fact_security_headers`; both pin the same
|
|
19
|
+
* literals.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* `X-Content-Type-Options` for fact responses — block MIME sniffing so an
|
|
25
|
+
* `application/octet-stream` fact can't be sniffed into executable HTML.
|
|
26
|
+
*/
|
|
27
|
+
export const FACT_X_CONTENT_TYPE_OPTIONS = 'nosniff';
|
|
28
|
+
/**
|
|
29
|
+
* `Content-Security-Policy` for fact responses. A fact stored as (or sniffable
|
|
30
|
+
* to) `text/html` would otherwise execute as stored XSS for any reader of a
|
|
31
|
+
* referencing cell. `default-src 'none'; sandbox` neutralizes script execution
|
|
32
|
+
* and sub-resource loads even when a fact is rendered inline; harmless for
|
|
33
|
+
* directly-served images (they load no sub-resources).
|
|
34
|
+
*/
|
|
35
|
+
export const FACT_CONTENT_SECURITY_POLICY = "default-src 'none'; sandbox";
|
|
36
|
+
/**
|
|
37
|
+
* `Cache-Control` for fact responses — private, five minutes: the window a
|
|
38
|
+
* cached read outlives a revoked view.
|
|
39
|
+
*/
|
|
40
|
+
export const FACT_CACHE_CONTROL = 'private, max-age=300';
|
|
41
|
+
/**
|
|
42
|
+
* The fact hardening headers, in render order. `Content-Disposition:
|
|
43
|
+
* attachment` is deliberately absent — it would force-download and break
|
|
44
|
+
* legitimate inline image rendering.
|
|
45
|
+
*/
|
|
46
|
+
export const FACT_SECURITY_HEADERS = {
|
|
47
|
+
'X-Content-Type-Options': FACT_X_CONTENT_TYPE_OPTIONS,
|
|
48
|
+
'Content-Security-Policy': FACT_CONTENT_SECURITY_POLICY
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The exact nginx `add_header … always;` lines the internal facts location must
|
|
52
|
+
* carry, rendered from `FACT_SECURITY_HEADERS`. `always` makes nginx add the
|
|
53
|
+
* header on every status, not just 2xx/3xx. The X-Accel validator refuses a
|
|
54
|
+
* facts location missing any of them.
|
|
55
|
+
*
|
|
56
|
+
* @returns one `add_header` directive per header, each ending in `always;`
|
|
57
|
+
*/
|
|
58
|
+
export const fact_security_header_directives = () => Object.entries(FACT_SECURITY_HEADERS).map(([name, value]) => `add_header ${name} "${value}" always;`);
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
* `XAccelConfig` (built via
|
|
21
21
|
* `create_x_accel_config` in `server/x_accel.ts`) set in prod, unset in dev.
|
|
22
22
|
* The handle can only be obtained by proving the facts nginx location is
|
|
23
|
-
* `internal
|
|
23
|
+
* `internal;` and carries the fact hardening headers, so X-Accel serving can't
|
|
24
|
+
* be enabled against a public or header-less location.
|
|
24
25
|
*
|
|
25
26
|
* REST, not RPC: binary responses don't fit the JSON-RPC envelope.
|
|
26
27
|
*
|
|
@@ -69,14 +70,18 @@
|
|
|
69
70
|
*
|
|
70
71
|
* Auth shape on the cell-scoped route is `{account: 'none', actor: 'none'}`
|
|
71
72
|
* — the dispatcher's authorization phase is skipped for pure-public routes,
|
|
72
|
-
* so the handler builds the `RequestContext` itself from
|
|
73
|
-
* (
|
|
74
|
-
* single actor and loading their role_grants.
|
|
75
|
-
* with `req_ctx: null` and are admitted only by
|
|
76
|
-
* 'public'` cell whose `moderation` admits an audience (unmoderated or
|
|
73
|
+
* so the handler builds the `RequestContext` itself from the resolved account
|
|
74
|
+
* id (`get_public_account_id`, over what the `/api/*` auth middleware set) by
|
|
75
|
+
* resolving the caller's single actor and loading their role_grants.
|
|
76
|
+
* Unauthed callers pass through with `req_ctx: null` and are admitted only by
|
|
77
|
+
* a `cell.visibility === 'public'` cell whose `moderation` admits an audience (unmoderated or
|
|
77
78
|
* `'approved'`). Multi-actor accounts fall through with `req_ctx: null`
|
|
78
79
|
* — there's no `acting?` slot on a pure-public route, so multi-actor
|
|
79
|
-
* callers are treated as anonymous.
|
|
80
|
+
* callers are treated as anonymous. A scope-narrowed API token is treated as
|
|
81
|
+
* anonymous too, not refused (`get_public_account_id`): a narrowed token
|
|
82
|
+
* reaches no non-RPC surface, but on a route that admits anonymous callers
|
|
83
|
+
* a 403 would enforce nothing — the holder drops the `Authorization` header and
|
|
84
|
+
* re-reads every public cell anyway.
|
|
80
85
|
*
|
|
81
86
|
* 404 is the universal "not viewable" response: missing fact, missing or
|
|
82
87
|
* unviewable cell, or the cell doesn't reference the hash. We deliberately
|
|
@@ -96,8 +101,13 @@
|
|
|
96
101
|
* from executing as stored XSS for any reader of a referencing cell, **every**
|
|
97
102
|
* served-fact response carries `X-Content-Type-Options: nosniff` +
|
|
98
103
|
* `Content-Security-Policy: default-src 'none'; sandbox` (the
|
|
99
|
-
* `FACT_SECURITY_HEADERS` set
|
|
100
|
-
*
|
|
104
|
+
* `FACT_SECURITY_HEADERS` set from `server/fact_security_headers.ts`,
|
|
105
|
+
* byte-identical with the Rust twin `fuz_fact_serving`). The embedded and
|
|
106
|
+
* disk-stream branches set them on the response. On the `X-Accel-Redirect`
|
|
107
|
+
* branch nginx serves the bytes, and the headers the client sees come from the
|
|
108
|
+
* internal facts location, not this response — that location must carry the
|
|
109
|
+
* lines `fact_security_header_directives` renders, which `create_x_accel_config`
|
|
110
|
+
* enforces.
|
|
101
111
|
* `Content-Disposition: attachment` is deliberately **not** added — it would
|
|
102
112
|
* force-download and break legitimate inline image rendering.
|
|
103
113
|
*
|
|
@@ -150,7 +160,8 @@ export interface CreateServeFactRouteSpecOptions {
|
|
|
150
160
|
* `(cell, hash)` edge — never unioned across the fact's other referrers.
|
|
151
161
|
*
|
|
152
162
|
* Pure-public auth — the handler builds the per-request `RequestContext`
|
|
153
|
-
* from `
|
|
163
|
+
* from the resolved account id (`get_public_account_id`) and enforces
|
|
164
|
+
* visibility per-reference.
|
|
154
165
|
*/
|
|
155
166
|
export declare const create_serve_cell_fact_route_spec: (options: CreateServeFactRouteSpecOptions) => RouteSpec;
|
|
156
167
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve_fact_route.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/serve_fact_route.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"serve_fact_route.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/serve_fact_route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4HG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAetD,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAE5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAO/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAoCjD,MAAM,WAAW,+BAA+B;IAC/C;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;CACZ;AAyHD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,+BAA+B,KACtC,SAuDF,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,+BAA+B,KACtC,SA2CF,CAAC"}
|