@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
|
@@ -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
|
*
|
|
@@ -118,10 +128,9 @@ import { Readable } from 'node:stream';
|
|
|
118
128
|
import { join } from 'node:path';
|
|
119
129
|
import { FactHashSchema, Uuid } from '../primitive_schemas.js';
|
|
120
130
|
import { z } from 'zod';
|
|
121
|
-
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';
|
|
122
132
|
import { ROLE_ADMIN } from '../auth/role_schema.js';
|
|
123
133
|
import { ActingActor } from '../http/auth_shape.js';
|
|
124
|
-
import { ACCOUNT_ID_KEY } from '../hono_context.js';
|
|
125
134
|
import { query_active_actors_by_account } from '../auth/account_queries.js';
|
|
126
135
|
import { get_route_params } from '../http/route_spec.js';
|
|
127
136
|
import { ERROR_INVALID_ROUTE_PARAMS } from '../http/error_schemas.js';
|
|
@@ -131,33 +140,7 @@ import { query_cell_grant_list_for_cell } from '../db/cell_grant_queries.js';
|
|
|
131
140
|
import { can_view_cell } from '../auth/cell_authorize.js';
|
|
132
141
|
import { cell_steward_admits } from '../auth/cell_steward.js';
|
|
133
142
|
import { parse_file_fact_url } from '../db/file_fact_url.js';
|
|
134
|
-
|
|
135
|
-
const CACHE_CONTROL = 'private, max-age=300';
|
|
136
|
-
/**
|
|
137
|
-
* `X-Content-Type-Options` for fact responses — block MIME sniffing so an
|
|
138
|
-
* `application/octet-stream` fact can't be sniffed into executable HTML.
|
|
139
|
-
*/
|
|
140
|
-
const X_CONTENT_TYPE_OPTIONS = 'nosniff';
|
|
141
|
-
/**
|
|
142
|
-
* `Content-Security-Policy` for fact responses. Fact bytes carry a
|
|
143
|
-
* producer-supplied `content_type` served verbatim inline, so a fact stored as
|
|
144
|
-
* (or sniffable to) `text/html` would otherwise execute as stored XSS for any
|
|
145
|
-
* reader of a referencing cell. `default-src 'none'; sandbox` neutralizes
|
|
146
|
-
* script execution and sub-resource loads even when a fact is rendered inline;
|
|
147
|
-
* harmless for directly-served images (they load no sub-resources). The same
|
|
148
|
-
* pair, byte-identical, is set by the Rust twin (`fuz_fact_serving::serve`).
|
|
149
|
-
*/
|
|
150
|
-
const CONTENT_SECURITY_POLICY = "default-src 'none'; sandbox";
|
|
151
|
-
/**
|
|
152
|
-
* The untrusted-content hardening headers applied to every served-fact
|
|
153
|
-
* response across all three branches (embedded, `X-Accel-Redirect`,
|
|
154
|
-
* disk-stream). `Content-Disposition: attachment` is deliberately omitted — it
|
|
155
|
-
* would force-download and break legitimate inline image rendering.
|
|
156
|
-
*/
|
|
157
|
-
const FACT_SECURITY_HEADERS = {
|
|
158
|
-
'X-Content-Type-Options': X_CONTENT_TYPE_OPTIONS,
|
|
159
|
-
'Content-Security-Policy': CONTENT_SECURITY_POLICY
|
|
160
|
-
};
|
|
143
|
+
import { FACT_CACHE_CONTROL, FACT_SECURITY_HEADERS } from './fact_security_headers.js';
|
|
161
144
|
/**
|
|
162
145
|
* Path-param schema for the bare-hash route. Matching the canonical
|
|
163
146
|
* fact-hash form here pushes malformed-hash 400s through the framework's
|
|
@@ -220,7 +203,7 @@ const serve_fact_bytes = async (c, route, hash, config) => {
|
|
|
220
203
|
return c.body(bytes, 200, {
|
|
221
204
|
'Content-Type': content_type,
|
|
222
205
|
'Content-Length': size,
|
|
223
|
-
'Cache-Control':
|
|
206
|
+
'Cache-Control': FACT_CACHE_CONTROL,
|
|
224
207
|
...FACT_SECURITY_HEADERS
|
|
225
208
|
});
|
|
226
209
|
}
|
|
@@ -234,10 +217,12 @@ const serve_fact_bytes = async (c, route, hash, config) => {
|
|
|
234
217
|
const { shard, rest } = parsed;
|
|
235
218
|
if (x_accel !== undefined) {
|
|
236
219
|
// Production: hand off to nginx via the validated `internal;` facts location.
|
|
220
|
+
// nginx answers with that location's own `add_header` set, not these
|
|
221
|
+
// headers, which is why the validator requires the fact pair there.
|
|
237
222
|
return c.body(null, 200, {
|
|
238
223
|
'Content-Type': content_type,
|
|
239
224
|
'Content-Length': size,
|
|
240
|
-
'Cache-Control':
|
|
225
|
+
'Cache-Control': FACT_CACHE_CONTROL,
|
|
241
226
|
'X-Accel-Redirect': `${x_accel.redirect_prefix}${shard}/${rest}`,
|
|
242
227
|
...FACT_SECURITY_HEADERS
|
|
243
228
|
});
|
|
@@ -250,22 +235,27 @@ const serve_fact_bytes = async (c, route, hash, config) => {
|
|
|
250
235
|
return c.body(web_stream, 200, {
|
|
251
236
|
'Content-Type': content_type,
|
|
252
237
|
'Content-Length': size,
|
|
253
|
-
'Cache-Control':
|
|
238
|
+
'Cache-Control': FACT_CACHE_CONTROL,
|
|
254
239
|
...FACT_SECURITY_HEADERS
|
|
255
240
|
});
|
|
256
241
|
};
|
|
257
242
|
/**
|
|
258
243
|
* Resolve the caller's `RequestContext` on a pure-public route from the
|
|
259
|
-
* session-middleware-set account id
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
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.
|
|
266
255
|
*/
|
|
267
256
|
const build_public_request_context = async (c, route) => {
|
|
268
|
-
|
|
257
|
+
// a narrowed token is the anonymous caller here, never a refusal
|
|
258
|
+
const account_id = get_public_account_id(c);
|
|
269
259
|
if (!account_id)
|
|
270
260
|
return null;
|
|
271
261
|
const actors = await query_active_actors_by_account({ db: route.db }, account_id);
|
|
@@ -284,7 +274,8 @@ const build_public_request_context = async (c, route) => {
|
|
|
284
274
|
* `(cell, hash)` edge — never unioned across the fact's other referrers.
|
|
285
275
|
*
|
|
286
276
|
* Pure-public auth — the handler builds the per-request `RequestContext`
|
|
287
|
-
* from `
|
|
277
|
+
* from the resolved account id (`get_public_account_id`) and enforces
|
|
278
|
+
* visibility per-reference.
|
|
288
279
|
*/
|
|
289
280
|
export const create_serve_cell_fact_route_spec = (options) => {
|
|
290
281
|
const { facts_dir, x_accel, log } = options;
|
package/dist/server/x_accel.d.ts
CHANGED
|
@@ -5,37 +5,139 @@
|
|
|
5
5
|
* nginx location being `internal;` — only the authz'd handler's redirect (an
|
|
6
6
|
* internal subrequest) may reach it. A *public* facts location would serve any
|
|
7
7
|
* fact's bytes to anyone who guesses the `<shard>/<rest>` path, bypassing every
|
|
8
|
-
* cell-visibility check. `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* cell-visibility check. A location with its own `add_header` also stops the
|
|
9
|
+
* server-level headers from applying, so the facts location must carry the fact
|
|
10
|
+
* hardening headers itself (`fact_security_header_directives` in
|
|
11
|
+
* `server/fact_security_headers.ts`).
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* `XAccelConfig` makes these assertions structural: a consumer can only obtain
|
|
14
|
+
* the redirect prefix by building an `XAccelConfig`, which runs
|
|
15
|
+
* `validate_facts_internal_location` and throws **loudly** on any violation — it
|
|
16
|
+
* cannot silently fall closed.
|
|
17
|
+
*
|
|
18
|
+
* ## Rules
|
|
19
|
+
*
|
|
20
|
+
* The config is tokenized as nginx reads it (`ngx_conf_read_token`): whitespace
|
|
21
|
+
* is exactly space, tab, CR and LF; an unquoted word ends only at those or at
|
|
22
|
+
* `;` / `{` (a `}` inside a word is part of it, and so is a `{` straight after
|
|
23
|
+
* `$`); `#` starts a comment only where a token would start; quoted strings are
|
|
24
|
+
* one word, with nginx's backslash escapes. It is then parsed into nested
|
|
25
|
+
* blocks, and every `location` at any depth, in every `server` block, is
|
|
26
|
+
* classified against the prefix (trailing slashes ignored). A location *serves
|
|
27
|
+
* the prefix* when it is a prefix-type location (no modifier, or `^~`) whose
|
|
28
|
+
* path is the prefix or below it. Each one must:
|
|
29
|
+
*
|
|
30
|
+
* - use `^~` — a plain prefix location is refused;
|
|
31
|
+
* - be a direct child of a `server` block — one nested in another location is
|
|
32
|
+
* refused;
|
|
33
|
+
* - carry, as top-level directives of its own body, an exact `internal;` and
|
|
34
|
+
* each `fact_security_header_directives` line (token-equal: whitespace and
|
|
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`);
|
|
40
|
+
* - hold only simple directives — any nested block (`location`, `if`,
|
|
41
|
+
* `limit_except`, …) is refused, and so is `include`, since included files
|
|
42
|
+
* are not followed.
|
|
43
|
+
*
|
|
44
|
+
* The first two rules are what keep regex locations out: nginx never evaluates
|
|
45
|
+
* regex locations for a path whose longest prefix match is a `^~` location at
|
|
46
|
+
* server level, and because every prefix location at or below the prefix must
|
|
47
|
+
* be such a location, the longest match for any redirected path always is. (A
|
|
48
|
+
* longer plain location below the prefix can't exist — the `^~` rule refuses
|
|
49
|
+
* it.)
|
|
50
|
+
*
|
|
51
|
+
* The prefix may appear only in its own location lines and their bodies:
|
|
52
|
+
* `internal;` admits every internal redirect and subrequest, so any other
|
|
53
|
+
* directive with a word naming the prefix (trailing slash trimmed, so
|
|
54
|
+
* `/_facts$uri` counts) is refused, in any block at any depth — a `rewrite`,
|
|
55
|
+
* `try_files`, `error_page`, `index`, `add_before_body`, or a `set` / `map`
|
|
56
|
+
* value that a redirect later reads. The only exemptions are each `location`'s
|
|
57
|
+
* own modifier and path words and the bodies of the locations serving the
|
|
58
|
+
* prefix.
|
|
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
|
+
*
|
|
80
|
+
* Also refused: an `=` location on the prefix or below it (it can't serve the
|
|
81
|
+
* nested paths the redirect emits); a regex location (`~` / `~*`) whose pattern
|
|
82
|
+
* spells out the prefix (after unescaping `\/`, ASCII case-folded for `~*`) —
|
|
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.
|
|
88
|
+
*
|
|
89
|
+
* The threat model is operator mistakes, not an adversarial config author: the
|
|
90
|
+
* prefix check is textual, so a value assembled from pieces (`/_fac$1`) evades
|
|
91
|
+
* it. Beyond the config text:
|
|
92
|
+
*
|
|
93
|
+
* - The validator checks URL paths only — another `root` or `alias` that maps
|
|
94
|
+
* onto the facts directory would serve its files publicly, so keep the facts
|
|
95
|
+
* directory outside every other location's root.
|
|
96
|
+
* - nginx honours `X-Accel-Redirect` from any proxied upstream (`proxy_pass`,
|
|
97
|
+
* `fastcgi_pass`, `uwsgi_pass`, `scgi_pass`, `grpc_pass`), so every upstream in
|
|
98
|
+
* the server is trusted to point into the prefix; an untrusted one needs
|
|
99
|
+
* `proxy_ignore_headers X-Accel-Redirect;` (or the `fastcgi_` / `uwsgi_` /
|
|
100
|
+
* `scgi_` equivalent).
|
|
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.
|
|
105
|
+
*
|
|
106
|
+
* The Rust twin is `fuz_fact_serving`'s `nginx.rs`, held to the same rules and
|
|
107
|
+
* the same fixture corpus.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* const x_accel = create_x_accel_config(
|
|
112
|
+
* '/_facts/',
|
|
113
|
+
* `server {
|
|
114
|
+
* location ^~ /_facts/ {
|
|
115
|
+
* internal;
|
|
116
|
+
* alias /srv/app/facts/;
|
|
117
|
+
* ${fact_security_header_directives().join('\n\t\t')}
|
|
118
|
+
* }
|
|
119
|
+
* }`
|
|
120
|
+
* );
|
|
121
|
+
* ```
|
|
17
122
|
*
|
|
18
123
|
* @module
|
|
19
124
|
*/
|
|
20
125
|
import { z } from 'zod';
|
|
21
126
|
/** Result of the facts-location check. */
|
|
22
127
|
export interface NginxFactsValidation {
|
|
23
|
-
/** `true` when
|
|
128
|
+
/** `true` when every rule passes. */
|
|
24
129
|
ok: boolean;
|
|
25
|
-
/** Fatal issues
|
|
130
|
+
/** Fatal issues, each naming the offending location. */
|
|
26
131
|
errors: Array<string>;
|
|
27
132
|
}
|
|
28
133
|
/**
|
|
29
|
-
* Assert the nginx
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* `/_facts/`). Returns a fatal error when no matching `location` block exists,
|
|
33
|
-
* or when the matching block is not marked `internal;` — either is a public
|
|
34
|
-
* facts location that bypasses cell visibility.
|
|
134
|
+
* Assert the nginx config serves the X-Accel facts prefix only from `internal;`
|
|
135
|
+
* locations carrying the fact hardening headers. See the module docs for the
|
|
136
|
+
* rules; every violation is collected, so one call reports them all.
|
|
35
137
|
*
|
|
36
|
-
* @param config - the nginx config
|
|
37
|
-
* @param facts_location - the facts location path the redirect prefix points at
|
|
38
|
-
* @returns `{ok, errors}` — `ok` is `true` only when
|
|
138
|
+
* @param config - the nginx config string to check
|
|
139
|
+
* @param facts_location - the facts location path the redirect prefix points at (e.g. `/_facts/`)
|
|
140
|
+
* @returns `{ok, errors}` — `ok` is `true` only when every rule passes
|
|
39
141
|
*/
|
|
40
142
|
export declare const validate_facts_internal_location: (config: string, facts_location: string) => NginxFactsValidation;
|
|
41
143
|
/**
|
|
@@ -44,10 +146,11 @@ export declare const validate_facts_internal_location: (config: string, facts_lo
|
|
|
44
146
|
*
|
|
45
147
|
* The redirect prefix can be obtained **only** by passing the nginx config
|
|
46
148
|
* through `validate_facts_internal_location` (via `create_x_accel_config`), so
|
|
47
|
-
* X-Accel serving is impossible to enable without proving
|
|
48
|
-
* `internal;`
|
|
49
|
-
* cell-visibility check. A
|
|
50
|
-
*
|
|
149
|
+
* X-Accel serving is impossible to enable without proving at boot that the
|
|
150
|
+
* facts location is `internal;` and carries the fact hardening headers — a
|
|
151
|
+
* public facts location would bypass every cell-visibility check. A
|
|
152
|
+
* Zod-branded type: the brand can't be forged without an explicit cast, so the
|
|
153
|
+
* factory is the only ordinary construction path.
|
|
51
154
|
*/
|
|
52
155
|
export declare const XAccelConfig: z.core.$ZodBranded<z.ZodObject<{
|
|
53
156
|
redirect_prefix: z.ZodString;
|
|
@@ -63,18 +166,19 @@ export declare class XAccelConfigError extends Error {
|
|
|
63
166
|
constructor(errors: Array<string>);
|
|
64
167
|
}
|
|
65
168
|
/**
|
|
66
|
-
* Build a validated `XAccelConfig`, asserting the nginx
|
|
67
|
-
* `redirect_prefix`
|
|
169
|
+
* Build a validated `XAccelConfig`, asserting the nginx config serves
|
|
170
|
+
* `redirect_prefix` only from safe locations (the module-level rules).
|
|
68
171
|
*
|
|
69
172
|
* The make-impossible-states gate: serving can only emit `X-Accel-Redirect`
|
|
70
|
-
* into a location proven `internal;` at boot.
|
|
173
|
+
* into a location proven `internal;` and header-carrying at boot.
|
|
71
174
|
*
|
|
72
|
-
* @param redirect_prefix - the X-Accel redirect prefix (e.g. `/_facts/`)
|
|
73
|
-
* @param nginx_config - the nginx config
|
|
175
|
+
* @param redirect_prefix - the X-Accel redirect prefix, ending in `/` (e.g. `/_facts/`)
|
|
176
|
+
* @param nginx_config - the nginx config string to validate against
|
|
74
177
|
* @returns the validated `XAccelConfig` carrying `redirect_prefix`
|
|
75
|
-
* @throws `XAccelConfigError`
|
|
76
|
-
* `
|
|
77
|
-
*
|
|
178
|
+
* @throws `XAccelConfigError` carrying every violation
|
|
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.
|
|
78
182
|
*/
|
|
79
183
|
export declare const create_x_accel_config: (redirect_prefix: string, nginx_config: string) => XAccelConfig;
|
|
80
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"}
|