@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.
Files changed (69) hide show
  1. package/dist/actions/CLAUDE.md +7 -6
  2. package/dist/auth/CLAUDE.md +5 -2
  3. package/dist/auth/account_action_specs.d.ts.map +1 -1
  4. package/dist/auth/account_action_specs.js +2 -1
  5. package/dist/auth/account_route_schema.d.ts.map +1 -1
  6. package/dist/auth/account_route_schema.js +9 -2
  7. package/dist/auth/account_routes.d.ts +8 -0
  8. package/dist/auth/account_routes.d.ts.map +1 -1
  9. package/dist/auth/account_routes.js +12 -3
  10. package/dist/auth/request_context.d.ts +23 -0
  11. package/dist/auth/request_context.d.ts.map +1 -1
  12. package/dist/auth/request_context.js +32 -1
  13. package/dist/auth/token_scope.d.ts +22 -14
  14. package/dist/auth/token_scope.d.ts.map +1 -1
  15. package/dist/auth/token_scope.js +23 -14
  16. package/dist/db/CLAUDE.md +7 -3
  17. package/dist/db/fact_ddl.d.ts +1 -0
  18. package/dist/db/fact_ddl.d.ts.map +1 -1
  19. package/dist/db/fact_ddl.js +1 -0
  20. package/dist/db/fact_holder_queries.d.ts +8 -4
  21. package/dist/db/fact_holder_queries.d.ts.map +1 -1
  22. package/dist/db/fact_holder_queries.js +9 -5
  23. package/dist/db/fact_queries.d.ts +10 -7
  24. package/dist/db/fact_queries.d.ts.map +1 -1
  25. package/dist/db/fact_queries.js +31 -10
  26. package/dist/http/CLAUDE.md +2 -2
  27. package/dist/http/auth_shape.d.ts.map +1 -1
  28. package/dist/http/auth_shape.js +11 -6
  29. package/dist/http/proxy.d.ts +22 -21
  30. package/dist/http/proxy.d.ts.map +1 -1
  31. package/dist/http/proxy.js +28 -33
  32. package/dist/http/route_spec.d.ts +8 -0
  33. package/dist/http/route_spec.d.ts.map +1 -1
  34. package/dist/server/env.d.ts.map +1 -1
  35. package/dist/server/env.js +1 -1
  36. package/dist/server/fact_security_headers.d.ts +59 -0
  37. package/dist/server/fact_security_headers.d.ts.map +1 -0
  38. package/dist/server/fact_security_headers.js +58 -0
  39. package/dist/server/serve_fact_route.d.ts +21 -10
  40. package/dist/server/serve_fact_route.d.ts.map +1 -1
  41. package/dist/server/serve_fact_route.js +41 -50
  42. package/dist/server/x_accel.d.ts +135 -31
  43. package/dist/server/x_accel.d.ts.map +1 -1
  44. package/dist/server/x_accel.js +652 -88
  45. package/dist/testing/CLAUDE.md +73 -22
  46. package/dist/testing/cross_backend/capabilities.d.ts +0 -18
  47. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  48. package/dist/testing/cross_backend/capabilities.js +1 -4
  49. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  50. package/dist/testing/cross_backend/default_backend_configs.js +0 -7
  51. package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
  52. package/dist/testing/cross_backend/fact_serving.js +85 -12
  53. package/dist/testing/cross_backend/identity_parity.d.ts +11 -0
  54. package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
  55. package/dist/testing/cross_backend/identity_parity.js +124 -9
  56. package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
  57. package/dist/testing/cross_backend/login_security.js +28 -0
  58. package/dist/testing/cross_backend/revocation_drivers.d.ts +70 -0
  59. package/dist/testing/cross_backend/revocation_drivers.d.ts.map +1 -0
  60. package/dist/testing/cross_backend/revocation_drivers.js +104 -0
  61. package/dist/testing/cross_backend/sse_round_trip.d.ts +6 -1
  62. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
  63. package/dist/testing/cross_backend/sse_round_trip.js +52 -16
  64. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  65. package/dist/testing/cross_backend/token_scope_surface.js +33 -0
  66. package/dist/testing/cross_backend/ws_round_trip.d.ts +13 -1
  67. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  68. package/dist/testing/cross_backend/ws_round_trip.js +155 -39
  69. 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;`, so X-Accel serving can't be enabled against a public location.
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 `c.var.account_id`
73
- * (populated by the `/api/*` session middleware) by resolving the caller's
74
- * single actor and loading their role_grants. Unauthed callers pass through
75
- * with `req_ctx: null` and are admitted only by a `cell.visibility ===
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, applied in all three serve branches). The same
100
- * pair, byte-identical, is set by the Rust twin (`fuz_fact_serving::serve`).
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
- /** `Cache-Control` for fact responses — 5 min revocation window. */
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': 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': 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': 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. Multi-actor accounts return `null`
260
- * (no `acting?` slot on a public route to disambiguate); single active-actor
261
- * accounts resolve their actor and role_grants for owner / grant / admin
262
- * admission paths. Unauthenticated callers return `null`. Soft-deleted
263
- * (tombstoned) actors are excluded from the count
264
- * (`query_active_actors_by_account`), matching the acting-actor resolution
265
- * path — a tombstoned actor is never resolvable here.
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
- const account_id = c.get(ACCOUNT_ID_KEY);
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 `c.var.account_id` and enforces visibility per-reference.
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;
@@ -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. `XAccelConfig` makes that assertion structural: a
9
- * consumer can only obtain the redirect prefix by building an `XAccelConfig`,
10
- * which runs this check and throws **loudly** on a missing or non-`internal;`
11
- * location — it cannot silently fall closed.
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
- * This is a best-effort string check (brace-matched `location` blocks), the TS
14
- * twin of the Rust `fuz_fact_serving` `nginx.rs` — not a real nginx parser, but
15
- * it catches the security-critical omission. This is the only nginx property
16
- * fuz_app validates; there is no general config validator.
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 the facts location exists and is `internal;`. */
128
+ /** `true` when every rule passes. */
24
129
  ok: boolean;
25
- /** Fatal issues — a missing or non-`internal` facts location. */
130
+ /** Fatal issues, each naming the offending location. */
26
131
  errors: Array<string>;
27
132
  }
28
133
  /**
29
- * Assert the nginx `location` serving the X-Accel facts prefix is `internal;`.
30
- *
31
- * `facts_location` is the path the X-Accel redirect prefix points at (e.g.
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 template string to check
37
- * @param facts_location - the facts location path the redirect prefix points at
38
- * @returns `{ok, errors}` — `ok` is `true` only when the location exists and is `internal;`
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 the facts location is
48
- * `internal;` at boot — a public facts location would bypass every
49
- * cell-visibility check. A Zod-branded type: the brand can't be forged without
50
- * an explicit cast, so the factory is the only ordinary construction path.
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 `location` serving
67
- * `redirect_prefix` is `internal;`.
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 template string to validate against
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` when `nginx_config` has no matching `location` for
76
- * `redirect_prefix`, or the matching block is not marked `internal;` — either
77
- * is a public facts location that bypasses every cell-visibility check.
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;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,0CAA0C;AAC1C,MAAM,WAAW,oBAAoB;IACpC,gEAAgE;IAChE,EAAE,EAAE,OAAO,CAAC;IACZ,iEAAiE;IACjE,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAuDD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gCAAgC,GAC5C,QAAQ,MAAM,EACd,gBAAgB,MAAM,KACpB,oBAqBF,CAAC;AAEF;;;;;;;;;;GAUG;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;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GACjC,iBAAiB,MAAM,EACvB,cAAc,MAAM,KAClB,YAMF,CAAC"}
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"}