@fuzdev/fuz_app 0.145.0 → 0.146.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/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/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 +5 -0
- package/dist/server/fact_security_headers.d.ts.map +1 -1
- package/dist/server/fact_security_headers.js +5 -0
- package/dist/server/serve_fact_route.d.ts +12 -7
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +30 -23
- package/dist/server/x_accel.d.ts +37 -9
- package/dist/server/x_accel.d.ts.map +1 -1
- package/dist/server/x_accel.js +136 -19
- 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/dist/ui/Datatable.svelte +3 -3
- package/package.json +1 -1
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
|
/**
|
|
@@ -33,6 +33,11 @@ export declare const FACT_X_CONTENT_TYPE_OPTIONS = "nosniff";
|
|
|
33
33
|
* directly-served images (they load no sub-resources).
|
|
34
34
|
*/
|
|
35
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";
|
|
36
41
|
/**
|
|
37
42
|
* The fact hardening headers, in render order. `Content-Disposition:
|
|
38
43
|
* attachment` is deliberately absent — it would force-download and break
|
|
@@ -1 +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;;;;GAIG;AACH,eAAO,MAAM,qBAAqB;;;CAGxB,CAAC;AAEX;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,QAAO,KAAK,CAAC,MAAM,CAG7D,CAAC"}
|
|
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"}
|
|
@@ -33,6 +33,11 @@ export const FACT_X_CONTENT_TYPE_OPTIONS = 'nosniff';
|
|
|
33
33
|
* directly-served images (they load no sub-resources).
|
|
34
34
|
*/
|
|
35
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';
|
|
36
41
|
/**
|
|
37
42
|
* The fact hardening headers, in render order. `Content-Disposition:
|
|
38
43
|
* attachment` is deliberately absent — it would force-download and break
|
|
@@ -70,14 +70,18 @@
|
|
|
70
70
|
*
|
|
71
71
|
* Auth shape on the cell-scoped route is `{account: 'none', actor: 'none'}`
|
|
72
72
|
* — the dispatcher's authorization phase is skipped for pure-public routes,
|
|
73
|
-
* so the handler builds the `RequestContext` itself from
|
|
74
|
-
* (
|
|
75
|
-
* single actor and loading their role_grants.
|
|
76
|
-
* with `req_ctx: null` and are admitted only by
|
|
77
|
-
* '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
|
|
78
78
|
* `'approved'`). Multi-actor accounts fall through with `req_ctx: null`
|
|
79
79
|
* — there's no `acting?` slot on a pure-public route, so multi-actor
|
|
80
|
-
* 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.
|
|
81
85
|
*
|
|
82
86
|
* 404 is the universal "not viewable" response: missing fact, missing or
|
|
83
87
|
* unviewable cell, or the cell doesn't reference the hash. We deliberately
|
|
@@ -156,7 +160,8 @@ export interface CreateServeFactRouteSpecOptions {
|
|
|
156
160
|
* `(cell, hash)` edge — never unioned across the fact's other referrers.
|
|
157
161
|
*
|
|
158
162
|
* Pure-public auth — the handler builds the per-request `RequestContext`
|
|
159
|
-
* from `
|
|
163
|
+
* from the resolved account id (`get_public_account_id`) and enforces
|
|
164
|
+
* visibility per-reference.
|
|
160
165
|
*/
|
|
161
166
|
export declare const create_serve_cell_fact_route_spec: (options: CreateServeFactRouteSpecOptions) => RouteSpec;
|
|
162
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"}
|
|
@@ -70,14 +70,18 @@
|
|
|
70
70
|
*
|
|
71
71
|
* Auth shape on the cell-scoped route is `{account: 'none', actor: 'none'}`
|
|
72
72
|
* — the dispatcher's authorization phase is skipped for pure-public routes,
|
|
73
|
-
* so the handler builds the `RequestContext` itself from
|
|
74
|
-
* (
|
|
75
|
-
* single actor and loading their role_grants.
|
|
76
|
-
* with `req_ctx: null` and are admitted only by
|
|
77
|
-
* '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
|
|
78
78
|
* `'approved'`). Multi-actor accounts fall through with `req_ctx: null`
|
|
79
79
|
* — there's no `acting?` slot on a pure-public route, so multi-actor
|
|
80
|
-
* 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.
|
|
81
85
|
*
|
|
82
86
|
* 404 is the universal "not viewable" response: missing fact, missing or
|
|
83
87
|
* unviewable cell, or the cell doesn't reference the hash. We deliberately
|
|
@@ -124,10 +128,9 @@ import { Readable } from 'node:stream';
|
|
|
124
128
|
import { join } from 'node:path';
|
|
125
129
|
import { FactHashSchema, Uuid } from '../primitive_schemas.js';
|
|
126
130
|
import { z } from 'zod';
|
|
127
|
-
import { build_request_context, get_request_context, has_scoped_role } from '../auth/request_context.js';
|
|
131
|
+
import { build_request_context, get_public_account_id, get_request_context, has_scoped_role } from '../auth/request_context.js';
|
|
128
132
|
import { ROLE_ADMIN } from '../auth/role_schema.js';
|
|
129
133
|
import { ActingActor } from '../http/auth_shape.js';
|
|
130
|
-
import { ACCOUNT_ID_KEY } from '../hono_context.js';
|
|
131
134
|
import { query_active_actors_by_account } from '../auth/account_queries.js';
|
|
132
135
|
import { get_route_params } from '../http/route_spec.js';
|
|
133
136
|
import { ERROR_INVALID_ROUTE_PARAMS } from '../http/error_schemas.js';
|
|
@@ -137,9 +140,7 @@ import { query_cell_grant_list_for_cell } from '../db/cell_grant_queries.js';
|
|
|
137
140
|
import { can_view_cell } from '../auth/cell_authorize.js';
|
|
138
141
|
import { cell_steward_admits } from '../auth/cell_steward.js';
|
|
139
142
|
import { parse_file_fact_url } from '../db/file_fact_url.js';
|
|
140
|
-
import { FACT_SECURITY_HEADERS } from './fact_security_headers.js';
|
|
141
|
-
/** `Cache-Control` for fact responses — 5 min revocation window. */
|
|
142
|
-
const CACHE_CONTROL = 'private, max-age=300';
|
|
143
|
+
import { FACT_CACHE_CONTROL, FACT_SECURITY_HEADERS } from './fact_security_headers.js';
|
|
143
144
|
/**
|
|
144
145
|
* Path-param schema for the bare-hash route. Matching the canonical
|
|
145
146
|
* fact-hash form here pushes malformed-hash 400s through the framework's
|
|
@@ -202,7 +203,7 @@ const serve_fact_bytes = async (c, route, hash, config) => {
|
|
|
202
203
|
return c.body(bytes, 200, {
|
|
203
204
|
'Content-Type': content_type,
|
|
204
205
|
'Content-Length': size,
|
|
205
|
-
'Cache-Control':
|
|
206
|
+
'Cache-Control': FACT_CACHE_CONTROL,
|
|
206
207
|
...FACT_SECURITY_HEADERS
|
|
207
208
|
});
|
|
208
209
|
}
|
|
@@ -221,7 +222,7 @@ const serve_fact_bytes = async (c, route, hash, config) => {
|
|
|
221
222
|
return c.body(null, 200, {
|
|
222
223
|
'Content-Type': content_type,
|
|
223
224
|
'Content-Length': size,
|
|
224
|
-
'Cache-Control':
|
|
225
|
+
'Cache-Control': FACT_CACHE_CONTROL,
|
|
225
226
|
'X-Accel-Redirect': `${x_accel.redirect_prefix}${shard}/${rest}`,
|
|
226
227
|
...FACT_SECURITY_HEADERS
|
|
227
228
|
});
|
|
@@ -234,22 +235,27 @@ const serve_fact_bytes = async (c, route, hash, config) => {
|
|
|
234
235
|
return c.body(web_stream, 200, {
|
|
235
236
|
'Content-Type': content_type,
|
|
236
237
|
'Content-Length': size,
|
|
237
|
-
'Cache-Control':
|
|
238
|
+
'Cache-Control': FACT_CACHE_CONTROL,
|
|
238
239
|
...FACT_SECURITY_HEADERS
|
|
239
240
|
});
|
|
240
241
|
};
|
|
241
242
|
/**
|
|
242
243
|
* Resolve the caller's `RequestContext` on a pure-public route from the
|
|
243
|
-
* session-middleware-set account id
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
244
|
+
* session-middleware-set account id, read through `get_public_account_id`, so
|
|
245
|
+
* a scope-narrowed API token returns `null` — downgraded to anonymous, not
|
|
246
|
+
* refused (see the module doc). Twin of the Rust
|
|
247
|
+
* `AuthGate::public_request_context`.
|
|
248
|
+
*
|
|
249
|
+
* Multi-actor accounts return `null` (no `acting?` slot on a public route to
|
|
250
|
+
* disambiguate); single active-actor accounts resolve their actor and
|
|
251
|
+
* role_grants for owner / grant / admin admission paths. Unauthenticated
|
|
252
|
+
* callers return `null`. Soft-deleted (tombstoned) actors are excluded from the
|
|
253
|
+
* count (`query_active_actors_by_account`), matching the acting-actor
|
|
254
|
+
* resolution path — a tombstoned actor is never resolvable here.
|
|
250
255
|
*/
|
|
251
256
|
const build_public_request_context = async (c, route) => {
|
|
252
|
-
|
|
257
|
+
// a narrowed token is the anonymous caller here, never a refusal
|
|
258
|
+
const account_id = get_public_account_id(c);
|
|
253
259
|
if (!account_id)
|
|
254
260
|
return null;
|
|
255
261
|
const actors = await query_active_actors_by_account({ db: route.db }, account_id);
|
|
@@ -268,7 +274,8 @@ const build_public_request_context = async (c, route) => {
|
|
|
268
274
|
* `(cell, hash)` edge — never unioned across the fact's other referrers.
|
|
269
275
|
*
|
|
270
276
|
* Pure-public auth — the handler builds the per-request `RequestContext`
|
|
271
|
-
* from `
|
|
277
|
+
* from the resolved account id (`get_public_account_id`) and enforces
|
|
278
|
+
* visibility per-reference.
|
|
272
279
|
*/
|
|
273
280
|
export const create_serve_cell_fact_route_spec = (options) => {
|
|
274
281
|
const { facts_dir, x_accel, log } = options;
|
package/dist/server/x_accel.d.ts
CHANGED
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
* - carry, as top-level directives of its own body, an exact `internal;` and
|
|
34
34
|
* each `fact_security_header_directives` line (token-equal: whitespace and
|
|
35
35
|
* quoting are free, the words are not);
|
|
36
|
+
* - set those headers with no other `add_header` — one naming a
|
|
37
|
+
* `FACT_SECURITY_HEADERS` header (ASCII case-insensitive) that is not
|
|
38
|
+
* token-equal to its required line is refused, since nginx sends both values
|
|
39
|
+
* and a client may read only the first (`sniff, nosniff`);
|
|
36
40
|
* - hold only simple directives — any nested block (`location`, `if`,
|
|
37
41
|
* `limit_except`, …) is refused, and so is `include`, since included files
|
|
38
42
|
* are not followed.
|
|
@@ -53,13 +57,34 @@
|
|
|
53
57
|
* own modifier and path words and the bodies of the locations serving the
|
|
54
58
|
* prefix.
|
|
55
59
|
*
|
|
60
|
+
* The prefix itself must be an absolute path ending in exactly one `/` — the
|
|
61
|
+
* server appends `<shard>/<rest>` straight onto it, so `/_facts` would redirect
|
|
62
|
+
* to `/_factsab/…`, outside the validated location. At least one location
|
|
63
|
+
* serving the prefix must sit exactly at it (`/_facts` or `/_facts/`): one only
|
|
64
|
+
* below it (`location ^~ /_facts/00/`) leaves every other redirect path to
|
|
65
|
+
* whatever location matches next, and nginx compares location paths
|
|
66
|
+
* byte-for-byte, so `/_facts//` sits below it too.
|
|
67
|
+
*
|
|
68
|
+
* Module directives that undo those guarantees are refused too:
|
|
69
|
+
*
|
|
70
|
+
* - `ssi` with any value but `off`, anywhere in the config. SSI `include
|
|
71
|
+
* virtual` is a subrequest, which `internal;` admits, so an SSI-processed
|
|
72
|
+
* response — a fact inheriting `ssi on` from `http` or `server`, or any page
|
|
73
|
+
* carrying user content — could include another fact's bytes.
|
|
74
|
+
* - `more_set_headers` / `more_clear_headers` (headers-more) everywhere except
|
|
75
|
+
* inside a `location` that doesn't serve the prefix (or a block within one):
|
|
76
|
+
* they run after `add_header` and can strip the hardening headers, so they
|
|
77
|
+
* are refused in the facts location and at every level above it, including
|
|
78
|
+
* any block there (`if`, another `server`).
|
|
79
|
+
*
|
|
56
80
|
* Also refused: an `=` location on the prefix or below it (it can't serve the
|
|
57
81
|
* nested paths the redirect emits); a regex location (`~` / `~*`) whose pattern
|
|
58
82
|
* spells out the prefix (after unescaping `\/`, ASCII case-folded for `~*`) —
|
|
59
|
-
* dead under the rules above, refused for the clearer message;
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
83
|
+
* dead under the rules above, refused for the clearer message; a config with no
|
|
84
|
+
* location at the prefix; and blocks nested more than 64 deep (a parse error, so
|
|
85
|
+
* the walks can't exhaust the stack). `include` at server level is allowed, but
|
|
86
|
+
* its file is not read — keep facts-related locations and the directives above
|
|
87
|
+
* out of included files.
|
|
63
88
|
*
|
|
64
89
|
* The threat model is operator mistakes, not an adversarial config author: the
|
|
65
90
|
* prefix check is textual, so a value assembled from pieces (`/_fac$1`) evades
|
|
@@ -73,8 +98,10 @@
|
|
|
73
98
|
* the server is trusted to point into the prefix; an untrusted one needs
|
|
74
99
|
* `proxy_ignore_headers X-Accel-Redirect;` (or the `fastcgi_` / `uwsgi_` /
|
|
75
100
|
* `scgi_` equivalent).
|
|
76
|
-
* -
|
|
77
|
-
*
|
|
101
|
+
* - Other module code can redirect internally too (njs `internalRedirect`, perl,
|
|
102
|
+
* echo), or strip the hardening headers as headers-more can (njs
|
|
103
|
+
* `js_header_filter`, lua-nginx-module `header_filter_by_lua*`), and the
|
|
104
|
+
* check sees none of it, so run trusted modules and content only.
|
|
78
105
|
*
|
|
79
106
|
* The Rust twin is `fuz_fact_serving`'s `nginx.rs`, held to the same rules and
|
|
80
107
|
* the same fixture corpus.
|
|
@@ -145,12 +172,13 @@ export declare class XAccelConfigError extends Error {
|
|
|
145
172
|
* The make-impossible-states gate: serving can only emit `X-Accel-Redirect`
|
|
146
173
|
* into a location proven `internal;` and header-carrying at boot.
|
|
147
174
|
*
|
|
148
|
-
* @param redirect_prefix - the X-Accel redirect prefix (e.g. `/_facts/`)
|
|
175
|
+
* @param redirect_prefix - the X-Accel redirect prefix, ending in `/` (e.g. `/_facts/`)
|
|
149
176
|
* @param nginx_config - the nginx config string to validate against
|
|
150
177
|
* @returns the validated `XAccelConfig` carrying `redirect_prefix`
|
|
151
178
|
* @throws `XAccelConfigError` carrying every violation
|
|
152
|
-
* `validate_facts_internal_location` finds — a
|
|
153
|
-
* exact-match, or regex-shadowed
|
|
179
|
+
* `validate_facts_internal_location` finds — a prefix not ending in `/`, a
|
|
180
|
+
* missing, public, header-less, re-headered, exact-match, or regex-shadowed
|
|
181
|
+
* facts location, or `ssi` / headers-more reaching it.
|
|
154
182
|
*/
|
|
155
183
|
export declare const create_x_accel_config: (redirect_prefix: string, nginx_config: string) => XAccelConfig;
|
|
156
184
|
//# sourceMappingURL=x_accel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"x_accel.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/x_accel.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"x_accel.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/x_accel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2HG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,0CAA0C;AAC1C,MAAM,WAAW,oBAAoB;IACpC,qCAAqC;IACrC,EAAE,EAAE,OAAO,CAAC;IACZ,wDAAwD;IACxD,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gCAAgC,GAC5C,QAAQ,MAAM,EACd,gBAAgB,MAAM,KACpB,oBAYF,CAAC;AA+IF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,YAAY;;0CAKF,CAAC;AACxB,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;gBACnB,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC;CAKjC;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,GACjC,iBAAiB,MAAM,EACvB,cAAc,MAAM,KAClB,YAMF,CAAC"}
|