@fuzdev/fuz_app 0.143.0 → 0.145.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/auth/CLAUDE.md +8 -0
- package/dist/auth/cell_action_specs.d.ts +13 -0
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +13 -0
- package/dist/auth/cell_actions.d.ts +7 -0
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +30 -0
- package/dist/auth/cell_ref_admission.d.ts +51 -0
- package/dist/auth/cell_ref_admission.d.ts.map +1 -0
- package/dist/auth/cell_ref_admission.js +69 -0
- package/dist/auth/deps.d.ts +6 -3
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/db/CLAUDE.md +40 -23
- package/dist/db/cell_ddl.d.ts +3 -2
- package/dist/db/cell_ddl.d.ts.map +1 -1
- package/dist/db/cell_ddl.js +3 -2
- package/dist/db/cell_queries.d.ts +31 -6
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +45 -18
- package/dist/db/fact_ddl.d.ts +30 -4
- package/dist/db/fact_ddl.d.ts.map +1 -1
- package/dist/db/fact_ddl.js +44 -4
- package/dist/db/fact_holder_queries.d.ts +70 -0
- package/dist/db/fact_holder_queries.d.ts.map +1 -0
- package/dist/db/fact_holder_queries.js +79 -0
- package/dist/db/fact_queries.d.ts +19 -14
- package/dist/db/fact_queries.d.ts.map +1 -1
- package/dist/db/fact_queries.js +31 -10
- package/dist/db/fact_store.d.ts +65 -9
- package/dist/db/fact_store.d.ts.map +1 -1
- package/dist/db/fact_store.js +49 -20
- package/dist/server/fact_security_headers.d.ts +54 -0
- package/dist/server/fact_security_headers.d.ts.map +1 -0
- package/dist/server/fact_security_headers.js +53 -0
- package/dist/server/serve_fact_route.d.ts +33 -7
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +36 -32
- package/dist/server/x_accel.d.ts +106 -30
- package/dist/server/x_accel.d.ts.map +1 -1
- package/dist/server/x_accel.js +533 -86
- package/dist/testing/CLAUDE.md +25 -1
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +6 -3
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +25 -16
- package/dist/testing/cross_backend/cell_ref_admission.d.ts +4 -0
- package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_ref_admission.js +268 -0
- package/dist/testing/cross_backend/expected_schema.json +66 -10
- package/dist/testing/cross_backend/fact_serving.d.ts +19 -1
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/fact_serving.js +31 -9
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +6 -2
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +13 -3
- package/package.json +3 -3
package/dist/db/fact_store.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PG-backed `
|
|
2
|
+
* PG-backed `HeldFactStore` implementation.
|
|
3
3
|
*
|
|
4
4
|
* Wraps the raw queries in `db/fact_queries.ts` with the lifecycle the
|
|
5
|
-
* `
|
|
5
|
+
* `HeldFactStore` interface promises:
|
|
6
6
|
*
|
|
7
7
|
* - sync hash on `put`; `put_stream` hashes as it streams
|
|
8
8
|
* - idempotent insert (`ON CONFLICT DO NOTHING` in the queries layer)
|
|
@@ -33,13 +33,63 @@
|
|
|
33
33
|
* `external_url` has any other shape, and any external row on a store with no
|
|
34
34
|
* disk CAS configured, as unavailable: a warning and `null`.
|
|
35
35
|
*
|
|
36
|
+
* ## Holders
|
|
37
|
+
*
|
|
38
|
+
* Every put names its holder (`HeldFactPutOptions.holder`): the actor whose
|
|
39
|
+
* request carried the bytes, or `null` for a system put (seeds, server-side
|
|
40
|
+
* imports). A named holder gets a `fact_holder` row on every put — a
|
|
41
|
+
* deduplicated put included, since bringing identical bytes is possession. The
|
|
42
|
+
* `fact` row, its refs, and the holder row commit in one transaction. Cell ref
|
|
43
|
+
* admission (`auth/cell_ref_admission.ts`) reads the holder rows. The holder is
|
|
44
|
+
* a spine concept, so `PgFactStore` implements `HeldFactStore` — the
|
|
45
|
+
* `@fuzdev/fuz_util` `FactStore` reads plus puts that take `HeldFactPutOptions` —
|
|
46
|
+
* rather than the backend-agnostic `FactStore`, whose puts name no actor.
|
|
47
|
+
*
|
|
36
48
|
* @module
|
|
37
49
|
*/
|
|
38
50
|
import type { QueryDeps } from './query_deps.js';
|
|
51
|
+
import type { Uuid } from '../primitive_schemas.js';
|
|
39
52
|
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
40
53
|
import type { FactHash } from '@fuzdev/fuz_util/hash_schemas.ts';
|
|
41
54
|
import type { FactMeta, FactPutOptions, FactStore, PutStreamOutcome } from '@fuzdev/fuz_util/fact_store.ts';
|
|
42
55
|
import { type FactDiskStorageDeps } from './fact_disk_storage.js';
|
|
56
|
+
/**
|
|
57
|
+
* Options for `HeldFactStore.put` / `put_stream`: the `@fuzdev/fuz_util`
|
|
58
|
+
* `FactPutOptions` plus the required `holder`.
|
|
59
|
+
*
|
|
60
|
+
* `holder` is the actor that brought these bytes, recorded in `fact_holder`
|
|
61
|
+
* whether or not the fact already existed. `null` is a system put (seeds,
|
|
62
|
+
* server-side imports) that records no holder — a cell write can newly
|
|
63
|
+
* reference such a fact only as a global admin or as an actor that puts the
|
|
64
|
+
* same bytes itself (refs a cell already carries stay admitted through update
|
|
65
|
+
* and clone). Required, so a put on behalf of a caller can't silently skip the
|
|
66
|
+
* possession record. Twin of the Rust `PutOptions::holder`.
|
|
67
|
+
*/
|
|
68
|
+
export interface HeldFactPutOptions extends FactPutOptions {
|
|
69
|
+
holder: Uuid | null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A fact store whose puts record possession: the `@fuzdev/fuz_util`
|
|
73
|
+
* `FactStore` reads, plus `put` / `put_stream` taking `HeldFactPutOptions` with
|
|
74
|
+
* its required `holder`.
|
|
75
|
+
*
|
|
76
|
+
* The holder is required — a store typed by the generic `FactStore` cannot
|
|
77
|
+
* record possession, so cell ref admission (`auth/cell_ref_admission.ts`)
|
|
78
|
+
* could never admit a ref to a fact put through it. `AppDeps.fact_store` is
|
|
79
|
+
* typed by this interface; `PgFactStore` implements it.
|
|
80
|
+
*/
|
|
81
|
+
export interface HeldFactStore extends Omit<FactStore, 'put' | 'put_stream'> {
|
|
82
|
+
/**
|
|
83
|
+
* Idempotent like `FactStore.put`; records `options.holder` (when non-null)
|
|
84
|
+
* on a fresh insert and a dedup hit alike.
|
|
85
|
+
*/
|
|
86
|
+
put: (bytes: Uint8Array, options: HeldFactPutOptions) => Promise<FactHash>;
|
|
87
|
+
/**
|
|
88
|
+
* Idempotent like `FactStore.put_stream`; records `options.holder` (when
|
|
89
|
+
* non-null) on a fresh insert and a dedup hit alike.
|
|
90
|
+
*/
|
|
91
|
+
put_stream: (stream: ReadableStream<Uint8Array>, max_bytes: number, options: HeldFactPutOptions) => Promise<PutStreamOutcome>;
|
|
92
|
+
}
|
|
43
93
|
/** Default embedded-vs-referenced cutoff (1 MiB). */
|
|
44
94
|
export declare const FACT_EMBEDDED_THRESHOLD_DEFAULT: number;
|
|
45
95
|
/**
|
|
@@ -70,10 +120,11 @@ export interface PgFactStoreDeps {
|
|
|
70
120
|
log?: Logger;
|
|
71
121
|
}
|
|
72
122
|
/**
|
|
73
|
-
* PG-backed
|
|
74
|
-
* the lifecycle layer described in the module doc.
|
|
123
|
+
* PG-backed fact store. Delegates to `db/fact_queries.ts` for I/O and adds
|
|
124
|
+
* the lifecycle layer described in the module doc. Implements `HeldFactStore`:
|
|
125
|
+
* the `@fuzdev/fuz_util` `FactStore` reads, puts taking `HeldFactPutOptions`.
|
|
75
126
|
*/
|
|
76
|
-
export declare class PgFactStore implements
|
|
127
|
+
export declare class PgFactStore implements HeldFactStore {
|
|
77
128
|
#private;
|
|
78
129
|
constructor(options: PgFactStoreDeps);
|
|
79
130
|
/**
|
|
@@ -82,9 +133,12 @@ export declare class PgFactStore implements FactStore {
|
|
|
82
133
|
* are configured) at `<facts_dir>/<shard>/<rest>` with a `file:` URL. Oversize
|
|
83
134
|
* without a disk root throws: the store has nowhere to put bytes it cannot
|
|
84
135
|
* embed. Idempotent — `ON CONFLICT DO NOTHING` + content-addressed disk
|
|
85
|
-
* filenames make a re-write a no-op.
|
|
136
|
+
* filenames make a re-write a no-op. `options.holder`, when set, is recorded
|
|
137
|
+
* as a holder on a fresh insert and a dedup hit alike.
|
|
138
|
+
*
|
|
139
|
+
* @mutates fact_holder - records `options.holder` (when non-null) in the transaction that writes the `fact` row
|
|
86
140
|
*/
|
|
87
|
-
put(bytes: Uint8Array, options
|
|
141
|
+
put(bytes: Uint8Array, options: HeldFactPutOptions): Promise<FactHash>;
|
|
88
142
|
/**
|
|
89
143
|
* Stream bytes into the store with bounded memory, returning the finalized
|
|
90
144
|
* digests + size. Delegates the byte path to `stream_fact_to_disk` (hash
|
|
@@ -100,9 +154,11 @@ export declare class PgFactStore implements FactStore {
|
|
|
100
154
|
*
|
|
101
155
|
* Requires `fs` (and, for the over-threshold spill, `disk_root`) to be
|
|
102
156
|
* configured. The streaming twin of `put`; mirrors the Rust
|
|
103
|
-
* `FactStore::put_stream`.
|
|
157
|
+
* `FactStore::put_stream`. Records `options.holder` the way `put` does.
|
|
158
|
+
*
|
|
159
|
+
* @mutates fact_holder - records `options.holder` (when non-null) in the transaction that writes the `fact` row
|
|
104
160
|
*/
|
|
105
|
-
put_stream(stream: ReadableStream<Uint8Array>, max_bytes: number, options
|
|
161
|
+
put_stream(stream: ReadableStream<Uint8Array>, max_bytes: number, options: HeldFactPutOptions): Promise<PutStreamOutcome>;
|
|
106
162
|
/**
|
|
107
163
|
* Retrieve bytes. Embedded reads return PG bytes directly. An external
|
|
108
164
|
* read comes from the disk CAS and nowhere else: the bytes at the row's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fact_store.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_store.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"fact_store.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAOtD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,KAAK,EACX,QAAQ,EACR,cAAc,EACd,SAAS,EACT,gBAAgB,EAChB,MAAM,gCAAgC,CAAC;AAcxC,OAAO,EAIN,KAAK,mBAAmB,EACxB,MAAM,wBAAwB,CAAC;AAEhC;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAmB,SAAQ,cAAc;IACzD,MAAM,EAAE,IAAI,GAAG,IAAI,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,SAAS,EAAE,KAAK,GAAG,YAAY,CAAC;IAC3E;;;OAGG;IACH,GAAG,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,kBAAkB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC3E;;;OAGG;IACH,UAAU,EAAE,CACX,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kBAAkB,KACvB,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC/B;AAED,qDAAqD;AACrD,eAAO,MAAM,+BAA+B,QAAc,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,SAAS,CAAC;IAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,CAAC,EAAE,mBAAmB,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;GAIG;AACH,qBAAa,WAAY,YAAW,aAAa;;gBAOpC,OAAO,EAAE,eAAe;IAQpC;;;;;;;;;;OAUG;IACG,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAgC5E;;;;;;;;;;;;;;;;;;OAkBG;IACG,UAAU,CACf,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kBAAkB,GACzB,OAAO,CAAC,gBAAgB,CAAC;IA+C5B;;;;;;;;;OASG;IACG,GAAG,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IA0C/C,GAAG,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAIrC,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAWlD,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAIxD;;;;;;;;;;;;;;;;;;OAkBG;IACG,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CAG3F"}
|
package/dist/db/fact_store.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PG-backed `
|
|
2
|
+
* PG-backed `HeldFactStore` implementation.
|
|
3
3
|
*
|
|
4
4
|
* Wraps the raw queries in `db/fact_queries.ts` with the lifecycle the
|
|
5
|
-
* `
|
|
5
|
+
* `HeldFactStore` interface promises:
|
|
6
6
|
*
|
|
7
7
|
* - sync hash on `put`; `put_stream` hashes as it streams
|
|
8
8
|
* - idempotent insert (`ON CONFLICT DO NOTHING` in the queries layer)
|
|
@@ -33,18 +33,32 @@
|
|
|
33
33
|
* `external_url` has any other shape, and any external row on a store with no
|
|
34
34
|
* disk CAS configured, as unavailable: a warning and `null`.
|
|
35
35
|
*
|
|
36
|
+
* ## Holders
|
|
37
|
+
*
|
|
38
|
+
* Every put names its holder (`HeldFactPutOptions.holder`): the actor whose
|
|
39
|
+
* request carried the bytes, or `null` for a system put (seeds, server-side
|
|
40
|
+
* imports). A named holder gets a `fact_holder` row on every put — a
|
|
41
|
+
* deduplicated put included, since bringing identical bytes is possession. The
|
|
42
|
+
* `fact` row, its refs, and the holder row commit in one transaction. Cell ref
|
|
43
|
+
* admission (`auth/cell_ref_admission.ts`) reads the holder rows. The holder is
|
|
44
|
+
* a spine concept, so `PgFactStore` implements `HeldFactStore` — the
|
|
45
|
+
* `@fuzdev/fuz_util` `FactStore` reads plus puts that take `HeldFactPutOptions` —
|
|
46
|
+
* rather than the backend-agnostic `FactStore`, whose puts name no actor.
|
|
47
|
+
*
|
|
36
48
|
* @module
|
|
37
49
|
*/
|
|
38
50
|
import { to_error_message } from '@fuzdev/fuz_util/error.ts';
|
|
39
51
|
import { fact_hash_bytes, fact_hash_verify, fact_hash_extract_refs } from '@fuzdev/fuz_util/fact_hash.ts';
|
|
40
52
|
import { query_delete_fact, query_get_fact, query_get_fact_meta, query_get_fact_refs, query_has_fact, query_put_fact, query_put_fact_refs } from './fact_queries.js';
|
|
53
|
+
import { query_put_fact_holder } from './fact_holder_queries.js';
|
|
41
54
|
import { is_file_fact_url } from './file_fact_url.js';
|
|
42
55
|
import { read_fact_bytes_from_disk, stream_fact_to_disk, write_fact_bytes_to_disk } from './fact_disk_storage.js';
|
|
43
56
|
/** Default embedded-vs-referenced cutoff (1 MiB). */
|
|
44
57
|
export const FACT_EMBEDDED_THRESHOLD_DEFAULT = 1024 * 1024;
|
|
45
58
|
/**
|
|
46
|
-
* PG-backed
|
|
47
|
-
* the lifecycle layer described in the module doc.
|
|
59
|
+
* PG-backed fact store. Delegates to `db/fact_queries.ts` for I/O and adds
|
|
60
|
+
* the lifecycle layer described in the module doc. Implements `HeldFactStore`:
|
|
61
|
+
* the `@fuzdev/fuz_util` `FactStore` reads, puts taking `HeldFactPutOptions`.
|
|
48
62
|
*/
|
|
49
63
|
export class PgFactStore {
|
|
50
64
|
#deps;
|
|
@@ -65,7 +79,10 @@ export class PgFactStore {
|
|
|
65
79
|
* are configured) at `<facts_dir>/<shard>/<rest>` with a `file:` URL. Oversize
|
|
66
80
|
* without a disk root throws: the store has nowhere to put bytes it cannot
|
|
67
81
|
* embed. Idempotent — `ON CONFLICT DO NOTHING` + content-addressed disk
|
|
68
|
-
* filenames make a re-write a no-op.
|
|
82
|
+
* filenames make a re-write a no-op. `options.holder`, when set, is recorded
|
|
83
|
+
* as a holder on a fresh insert and a dedup hit alike.
|
|
84
|
+
*
|
|
85
|
+
* @mutates fact_holder - records `options.holder` (when non-null) in the transaction that writes the `fact` row
|
|
69
86
|
*/
|
|
70
87
|
async put(bytes, options) {
|
|
71
88
|
const hash = fact_hash_bytes(bytes);
|
|
@@ -82,16 +99,13 @@ export class PgFactStore {
|
|
|
82
99
|
row_bytes = bytes;
|
|
83
100
|
row_external_url = null;
|
|
84
101
|
}
|
|
85
|
-
|
|
102
|
+
await this.#insert_row({
|
|
86
103
|
hash,
|
|
87
104
|
bytes: row_bytes,
|
|
88
105
|
external_url: row_external_url,
|
|
89
|
-
content_type: options
|
|
106
|
+
content_type: options.content_type ?? null,
|
|
90
107
|
size: bytes.length
|
|
91
|
-
});
|
|
92
|
-
if (inserted) {
|
|
93
|
-
await query_put_fact_refs(this.#deps, hash, resolve_refs(bytes, options));
|
|
94
|
-
}
|
|
108
|
+
}, resolve_refs(bytes, options), options.holder);
|
|
95
109
|
return hash;
|
|
96
110
|
}
|
|
97
111
|
/**
|
|
@@ -109,7 +123,9 @@ export class PgFactStore {
|
|
|
109
123
|
*
|
|
110
124
|
* Requires `fs` (and, for the over-threshold spill, `disk_root`) to be
|
|
111
125
|
* configured. The streaming twin of `put`; mirrors the Rust
|
|
112
|
-
* `FactStore::put_stream`.
|
|
126
|
+
* `FactStore::put_stream`. Records `options.holder` the way `put` does.
|
|
127
|
+
*
|
|
128
|
+
* @mutates fact_holder - records `options.holder` (when non-null) in the transaction that writes the `fact` row
|
|
113
129
|
*/
|
|
114
130
|
async put_stream(stream, max_bytes, options) {
|
|
115
131
|
if (this.#fs === undefined) {
|
|
@@ -118,18 +134,31 @@ export class PgFactStore {
|
|
|
118
134
|
const streamed = await stream_fact_to_disk(this.#fs, this.#disk_root, stream, max_bytes, this.#embedded_threshold);
|
|
119
135
|
const row_bytes = streamed.placement.kind === 'embedded' ? streamed.placement.bytes : null;
|
|
120
136
|
const row_external_url = streamed.placement.kind === 'disk' ? streamed.placement.external_url : null;
|
|
121
|
-
|
|
137
|
+
await this.#insert_row({
|
|
122
138
|
hash: streamed.hash,
|
|
123
139
|
bytes: row_bytes,
|
|
124
140
|
external_url: row_external_url,
|
|
125
|
-
content_type: options
|
|
141
|
+
content_type: options.content_type ?? null,
|
|
126
142
|
size: streamed.size
|
|
127
|
-
});
|
|
128
|
-
if (inserted && options?.refs && options.refs.length > 0) {
|
|
129
|
-
await query_put_fact_refs(this.#deps, streamed.hash, options.refs);
|
|
130
|
-
}
|
|
143
|
+
}, options.refs ?? [], options.holder);
|
|
131
144
|
return { hash: streamed.hash, sha256: streamed.sha256, size: streamed.size };
|
|
132
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Insert the `fact` row, then — in the same transaction — its refs (fresh
|
|
148
|
+
* insert only) and the holder row (every put, a dedup hit included). Shared
|
|
149
|
+
* by `put` and `put_stream`; twin of the Rust `PgFactStore::insert_row`.
|
|
150
|
+
*/
|
|
151
|
+
async #insert_row(row, refs, holder) {
|
|
152
|
+
await this.#deps.db.transaction(async (db) => {
|
|
153
|
+
const inserted = await query_put_fact({ db }, row);
|
|
154
|
+
if (inserted && refs.length > 0) {
|
|
155
|
+
await query_put_fact_refs({ db }, row.hash, refs);
|
|
156
|
+
}
|
|
157
|
+
if (holder !== null) {
|
|
158
|
+
await query_put_fact_holder({ db }, holder, row.hash);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
133
162
|
/**
|
|
134
163
|
* Retrieve bytes. Embedded reads return PG bytes directly. An external
|
|
135
164
|
* read comes from the disk CAS and nowhere else: the bytes at the row's
|
|
@@ -219,9 +248,9 @@ export class PgFactStore {
|
|
|
219
248
|
* from JSON content; otherwise no refs.
|
|
220
249
|
*/
|
|
221
250
|
const resolve_refs = (bytes, options) => {
|
|
222
|
-
if (options
|
|
251
|
+
if (options.refs !== undefined)
|
|
223
252
|
return options.refs;
|
|
224
|
-
if (options
|
|
253
|
+
if (options.content_type !== 'application/json')
|
|
225
254
|
return [];
|
|
226
255
|
let value;
|
|
227
256
|
try {
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
* The fact hardening headers, in render order. `Content-Disposition:
|
|
38
|
+
* attachment` is deliberately absent — it would force-download and break
|
|
39
|
+
* legitimate inline image rendering.
|
|
40
|
+
*/
|
|
41
|
+
export declare const FACT_SECURITY_HEADERS: {
|
|
42
|
+
readonly 'X-Content-Type-Options': "nosniff";
|
|
43
|
+
readonly 'Content-Security-Policy': "default-src 'none'; sandbox";
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* The exact nginx `add_header … always;` lines the internal facts location must
|
|
47
|
+
* carry, rendered from `FACT_SECURITY_HEADERS`. `always` makes nginx add the
|
|
48
|
+
* header on every status, not just 2xx/3xx. The X-Accel validator refuses a
|
|
49
|
+
* facts location missing any of them.
|
|
50
|
+
*
|
|
51
|
+
* @returns one `add_header` directive per header, each ending in `always;`
|
|
52
|
+
*/
|
|
53
|
+
export declare const fact_security_header_directives: () => Array<string>;
|
|
54
|
+
//# 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;;;;GAIG;AACH,eAAO,MAAM,qBAAqB;;;CAGxB,CAAC;AAEX;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,QAAO,KAAK,CAAC,MAAM,CAG7D,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
* The fact hardening headers, in render order. `Content-Disposition:
|
|
38
|
+
* attachment` is deliberately absent — it would force-download and break
|
|
39
|
+
* legitimate inline image rendering.
|
|
40
|
+
*/
|
|
41
|
+
export const FACT_SECURITY_HEADERS = {
|
|
42
|
+
'X-Content-Type-Options': FACT_X_CONTENT_TYPE_OPTIONS,
|
|
43
|
+
'Content-Security-Policy': FACT_CONTENT_SECURITY_POLICY
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* The exact nginx `add_header … always;` lines the internal facts location must
|
|
47
|
+
* carry, rendered from `FACT_SECURITY_HEADERS`. `always` makes nginx add the
|
|
48
|
+
* header on every status, not just 2xx/3xx. The X-Accel validator refuses a
|
|
49
|
+
* facts location missing any of them.
|
|
50
|
+
*
|
|
51
|
+
* @returns one `add_header` directive per header, each ending in `always;`
|
|
52
|
+
*/
|
|
53
|
+
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
|
*
|
|
@@ -41,6 +42,25 @@
|
|
|
41
42
|
* hash. B publishing identical bytes from B's public cell makes them
|
|
42
43
|
* readable *via B's cell* — it never touches A's private reference.
|
|
43
44
|
*
|
|
45
|
+
* The edge is only as safe as what may create it. A cell's `refs` derive
|
|
46
|
+
* from its `data`, and any account that can write a cell writes its `data` —
|
|
47
|
+
* so this route alone would make a known hash a read capability: name it in
|
|
48
|
+
* a cell you own, flip the cell public, and the bytes serve to the world,
|
|
49
|
+
* while a 200 against a 404 says whether the fact exists. What closes that is
|
|
50
|
+
* **possession-gated ref admission** on the cell verbs
|
|
51
|
+
* (`auth/cell_ref_admission.ts`): a non-admin `cell_create` / `cell_update` /
|
|
52
|
+
* `cell_clone` may introduce a ref only to a fact its acting actor holds —
|
|
53
|
+
* one it put the bytes of, recorded in `fact_holder` on every put, a
|
|
54
|
+
* deduplicated put included — and is refused `403 cell_ref_not_held`
|
|
55
|
+
* otherwise, the same answer whether or not a fact is stored. Refs a cell
|
|
56
|
+
* already carries stay admitted through updates and clones, so a viewer who
|
|
57
|
+
* clones a cell carries its refs: what a viewer can read through a cell, it
|
|
58
|
+
* can republish — no more than downloading the bytes and uploading them
|
|
59
|
+
* again. A serve is a snapshot of the edge at read time; revoking someone's
|
|
60
|
+
* access to a cell does not withdraw refs they already wrote elsewhere.
|
|
61
|
+
* Writers that bypass the verbs (seeds, raw inserts) are trusted and not
|
|
62
|
+
* admitted.
|
|
63
|
+
*
|
|
44
64
|
* The bare-hash route is **admin-only**: an admin's reach already spans
|
|
45
65
|
* every cell, so serving by bare hash grants no escalation. Non-admin
|
|
46
66
|
* callers are rejected at the auth phase and never reach the handler.
|
|
@@ -64,10 +84,11 @@
|
|
|
64
84
|
* don't distinguish 403 from 404 — neither the existence of a fact nor the
|
|
65
85
|
* existence of a cell→fact edge should leak through the public surface.
|
|
66
86
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
87
|
+
* A ref is a `data` string value that is exactly a fact hash
|
|
88
|
+
* (`derive_cell_refs`) — a hash inside prose or markup is not one. So a doc
|
|
89
|
+
* cell that shows images stores each image's hash as its own string value
|
|
90
|
+
* (an attachment list, say) beside the text that places them; serving then
|
|
91
|
+
* goes view-doc-cell → doc-cell-refs-include-hash → serve.
|
|
71
92
|
*
|
|
72
93
|
* ## Untrusted-content hardening
|
|
73
94
|
*
|
|
@@ -76,8 +97,13 @@
|
|
|
76
97
|
* from executing as stored XSS for any reader of a referencing cell, **every**
|
|
77
98
|
* served-fact response carries `X-Content-Type-Options: nosniff` +
|
|
78
99
|
* `Content-Security-Policy: default-src 'none'; sandbox` (the
|
|
79
|
-
* `FACT_SECURITY_HEADERS` set
|
|
80
|
-
*
|
|
100
|
+
* `FACT_SECURITY_HEADERS` set from `server/fact_security_headers.ts`,
|
|
101
|
+
* byte-identical with the Rust twin `fuz_fact_serving`). The embedded and
|
|
102
|
+
* disk-stream branches set them on the response. On the `X-Accel-Redirect`
|
|
103
|
+
* branch nginx serves the bytes, and the headers the client sees come from the
|
|
104
|
+
* internal facts location, not this response — that location must carry the
|
|
105
|
+
* lines `fact_security_header_directives` renders, which `create_x_accel_config`
|
|
106
|
+
* enforces.
|
|
81
107
|
* `Content-Disposition: attachment` is deliberately **not** added — it would
|
|
82
108
|
* force-download and break legitimate inline image rendering.
|
|
83
109
|
*
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwHG;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;AAuCjD,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;AAoHD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,+BAA+B,KACtC,SAuDF,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,+BAA+B,KACtC,SA2CF,CAAC"}
|
|
@@ -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
|
*
|
|
@@ -41,6 +42,25 @@
|
|
|
41
42
|
* hash. B publishing identical bytes from B's public cell makes them
|
|
42
43
|
* readable *via B's cell* — it never touches A's private reference.
|
|
43
44
|
*
|
|
45
|
+
* The edge is only as safe as what may create it. A cell's `refs` derive
|
|
46
|
+
* from its `data`, and any account that can write a cell writes its `data` —
|
|
47
|
+
* so this route alone would make a known hash a read capability: name it in
|
|
48
|
+
* a cell you own, flip the cell public, and the bytes serve to the world,
|
|
49
|
+
* while a 200 against a 404 says whether the fact exists. What closes that is
|
|
50
|
+
* **possession-gated ref admission** on the cell verbs
|
|
51
|
+
* (`auth/cell_ref_admission.ts`): a non-admin `cell_create` / `cell_update` /
|
|
52
|
+
* `cell_clone` may introduce a ref only to a fact its acting actor holds —
|
|
53
|
+
* one it put the bytes of, recorded in `fact_holder` on every put, a
|
|
54
|
+
* deduplicated put included — and is refused `403 cell_ref_not_held`
|
|
55
|
+
* otherwise, the same answer whether or not a fact is stored. Refs a cell
|
|
56
|
+
* already carries stay admitted through updates and clones, so a viewer who
|
|
57
|
+
* clones a cell carries its refs: what a viewer can read through a cell, it
|
|
58
|
+
* can republish — no more than downloading the bytes and uploading them
|
|
59
|
+
* again. A serve is a snapshot of the edge at read time; revoking someone's
|
|
60
|
+
* access to a cell does not withdraw refs they already wrote elsewhere.
|
|
61
|
+
* Writers that bypass the verbs (seeds, raw inserts) are trusted and not
|
|
62
|
+
* admitted.
|
|
63
|
+
*
|
|
44
64
|
* The bare-hash route is **admin-only**: an admin's reach already spans
|
|
45
65
|
* every cell, so serving by bare hash grants no escalation. Non-admin
|
|
46
66
|
* callers are rejected at the auth phase and never reach the handler.
|
|
@@ -64,10 +84,11 @@
|
|
|
64
84
|
* don't distinguish 403 from 404 — neither the existence of a fact nor the
|
|
65
85
|
* existence of a cell→fact edge should leak through the public surface.
|
|
66
86
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
87
|
+
* A ref is a `data` string value that is exactly a fact hash
|
|
88
|
+
* (`derive_cell_refs`) — a hash inside prose or markup is not one. So a doc
|
|
89
|
+
* cell that shows images stores each image's hash as its own string value
|
|
90
|
+
* (an attachment list, say) beside the text that places them; serving then
|
|
91
|
+
* goes view-doc-cell → doc-cell-refs-include-hash → serve.
|
|
71
92
|
*
|
|
72
93
|
* ## Untrusted-content hardening
|
|
73
94
|
*
|
|
@@ -76,8 +97,13 @@
|
|
|
76
97
|
* from executing as stored XSS for any reader of a referencing cell, **every**
|
|
77
98
|
* served-fact response carries `X-Content-Type-Options: nosniff` +
|
|
78
99
|
* `Content-Security-Policy: default-src 'none'; sandbox` (the
|
|
79
|
-
* `FACT_SECURITY_HEADERS` set
|
|
80
|
-
*
|
|
100
|
+
* `FACT_SECURITY_HEADERS` set from `server/fact_security_headers.ts`,
|
|
101
|
+
* byte-identical with the Rust twin `fuz_fact_serving`). The embedded and
|
|
102
|
+
* disk-stream branches set them on the response. On the `X-Accel-Redirect`
|
|
103
|
+
* branch nginx serves the bytes, and the headers the client sees come from the
|
|
104
|
+
* internal facts location, not this response — that location must carry the
|
|
105
|
+
* lines `fact_security_header_directives` renders, which `create_x_accel_config`
|
|
106
|
+
* enforces.
|
|
81
107
|
* `Content-Disposition: attachment` is deliberately **not** added — it would
|
|
82
108
|
* force-download and break legitimate inline image rendering.
|
|
83
109
|
*
|
|
@@ -111,33 +137,9 @@ import { query_cell_grant_list_for_cell } from '../db/cell_grant_queries.js';
|
|
|
111
137
|
import { can_view_cell } from '../auth/cell_authorize.js';
|
|
112
138
|
import { cell_steward_admits } from '../auth/cell_steward.js';
|
|
113
139
|
import { parse_file_fact_url } from '../db/file_fact_url.js';
|
|
140
|
+
import { FACT_SECURITY_HEADERS } from './fact_security_headers.js';
|
|
114
141
|
/** `Cache-Control` for fact responses — 5 min revocation window. */
|
|
115
142
|
const CACHE_CONTROL = 'private, max-age=300';
|
|
116
|
-
/**
|
|
117
|
-
* `X-Content-Type-Options` for fact responses — block MIME sniffing so an
|
|
118
|
-
* `application/octet-stream` fact can't be sniffed into executable HTML.
|
|
119
|
-
*/
|
|
120
|
-
const X_CONTENT_TYPE_OPTIONS = 'nosniff';
|
|
121
|
-
/**
|
|
122
|
-
* `Content-Security-Policy` for fact responses. Fact bytes carry a
|
|
123
|
-
* producer-supplied `content_type` served verbatim inline, so a fact stored as
|
|
124
|
-
* (or sniffable to) `text/html` would otherwise execute as stored XSS for any
|
|
125
|
-
* reader of a referencing cell. `default-src 'none'; sandbox` neutralizes
|
|
126
|
-
* script execution and sub-resource loads even when a fact is rendered inline;
|
|
127
|
-
* harmless for directly-served images (they load no sub-resources). The same
|
|
128
|
-
* pair, byte-identical, is set by the Rust twin (`fuz_fact_serving::serve`).
|
|
129
|
-
*/
|
|
130
|
-
const CONTENT_SECURITY_POLICY = "default-src 'none'; sandbox";
|
|
131
|
-
/**
|
|
132
|
-
* The untrusted-content hardening headers applied to every served-fact
|
|
133
|
-
* response across all three branches (embedded, `X-Accel-Redirect`,
|
|
134
|
-
* disk-stream). `Content-Disposition: attachment` is deliberately omitted — it
|
|
135
|
-
* would force-download and break legitimate inline image rendering.
|
|
136
|
-
*/
|
|
137
|
-
const FACT_SECURITY_HEADERS = {
|
|
138
|
-
'X-Content-Type-Options': X_CONTENT_TYPE_OPTIONS,
|
|
139
|
-
'Content-Security-Policy': CONTENT_SECURITY_POLICY
|
|
140
|
-
};
|
|
141
143
|
/**
|
|
142
144
|
* Path-param schema for the bare-hash route. Matching the canonical
|
|
143
145
|
* fact-hash form here pushes malformed-hash 400s through the framework's
|
|
@@ -214,6 +216,8 @@ const serve_fact_bytes = async (c, route, hash, config) => {
|
|
|
214
216
|
const { shard, rest } = parsed;
|
|
215
217
|
if (x_accel !== undefined) {
|
|
216
218
|
// Production: hand off to nginx via the validated `internal;` facts location.
|
|
219
|
+
// nginx answers with that location's own `add_header` set, not these
|
|
220
|
+
// headers, which is why the validator requires the fact pair there.
|
|
217
221
|
return c.body(null, 200, {
|
|
218
222
|
'Content-Type': content_type,
|
|
219
223
|
'Content-Length': size,
|