@fuzdev/fuz_app 0.144.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/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/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 +9 -3
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +12 -28
- 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/package.json +3 -3
package/dist/db/CLAUDE.md
CHANGED
|
@@ -177,9 +177,13 @@ records who holds them. Optional — minimal consumers never migrate it.
|
|
|
177
177
|
DO NOTHING`), `_put_fact_refs`, `_get_fact` / `_get_fact_meta` / `_has_fact`
|
|
178
178
|
/ `_get_fact_refs`, `_delete_fact` (returns `{size, external_url}` for
|
|
179
179
|
external unlink), and the cell-coupled orphan queries `query_orphan_facts_list`
|
|
180
|
-
/ `_select_for_delete` (a fact is orphan when no active `cell.refs` names it
|
|
180
|
+
/ `_select_for_delete` (a fact is orphan when no active `cell.refs` names it;
|
|
181
|
+
the `older_than` cutoff keeps a fact whose `created_at` or any
|
|
182
|
+
`fact_holder.created_at` is within the window, so a deduplicated re-upload
|
|
183
|
+
survives any sweep whose window covers it).
|
|
181
184
|
- **`fact_holder_queries.ts`** — `FACT_HOLDER_COLUMNS`, `query_put_fact_holder`
|
|
182
|
-
(idempotent
|
|
185
|
+
(idempotent upsert — a repeat put refreshes the row's `created_at` to the
|
|
186
|
+
most recent put), and the admission reads `query_fact_holder_table_exists` /
|
|
183
187
|
`query_fact_holder_unheld` (answered from holder rows alone).
|
|
184
188
|
- **`fact_store.ts`** — `PgFactStore`, implementing `HeldFactStore` (reads per
|
|
185
189
|
the `@fuzdev/fuz_util/fact_store.ts` `FactStore` contract; puts take
|
|
@@ -212,7 +216,7 @@ DO NOTHING`), `_put_fact_refs`, `_get_fact` / `_get_fact_meta` / `_has_fact`
|
|
|
212
216
|
- **`fact_store_errors.ts`** — `FactPayloadTooLargeError` / `FactStorageFullError` (+
|
|
213
217
|
`is_enospc_error`) thrown by `put_stream`, for a consumer route's 413 / 507.
|
|
214
218
|
- The HTTP serving plumbing lives under `server/` (`serve_fact_route.ts`,
|
|
215
|
-
`x_accel.ts`).
|
|
219
|
+
`x_accel.ts`, `fact_security_headers.ts`).
|
|
216
220
|
|
|
217
221
|
### Migration namespace order
|
|
218
222
|
|
package/dist/db/fact_ddl.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export declare const MEMOS_SCHEMA = "\nCREATE TABLE IF NOT EXISTS memo (\n\tfn_i
|
|
|
48
48
|
* Both FKs cascade: deleting an actor erases its possession, and deleting a
|
|
49
49
|
* fact (the orphan sweep) drops its holders. The primary key leads with
|
|
50
50
|
* `actor_id`, serving the admission lookup and the actor-side cascade.
|
|
51
|
+
* `created_at` is the holder's most recent put, not the first.
|
|
51
52
|
*/
|
|
52
53
|
export declare const FACT_HOLDER_SCHEMA = "\nCREATE TABLE IF NOT EXISTS fact_holder (\n\tactor_id UUID NOT NULL REFERENCES actor(id) ON DELETE CASCADE,\n\thash TEXT NOT NULL REFERENCES fact(hash) ON DELETE CASCADE,\n\tcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n\tPRIMARY KEY (actor_id, hash)\n)";
|
|
53
54
|
/** Serves the fact-side cascade of `fact_holder`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fact_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElE,mDAAmD;AACnD,eAAO,MAAM,YAAY,qSASvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,6LAK3B,CAAC;AAEH,4DAA4D;AAC5D,eAAO,MAAM,sBAAsB,8EACqC,CAAC;AAEzE,oFAAoF;AACpF,eAAO,MAAM,YAAY,mNAOvB,CAAC;AAEH
|
|
1
|
+
{"version":3,"file":"fact_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElE,mDAAmD;AACnD,eAAO,MAAM,YAAY,qSASvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,6LAK3B,CAAC;AAEH,4DAA4D;AAC5D,eAAO,MAAM,sBAAsB,8EACqC,CAAC;AAEzE,oFAAoF;AACpF,eAAO,MAAM,YAAY,mNAOvB,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,qQAM7B,CAAC;AAEH,qDAAqD;AACrD,eAAO,MAAM,sBAAsB,2EACkC,CAAC;AAEtE,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,sDAAuD,CAAC;AAErF,sFAAsF;AACtF,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CAiB5C,CAAC;AAEF,uDAAuD;AACvD,eAAO,MAAM,wBAAwB,cAAc,CAAC;AAEpD,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
|
package/dist/db/fact_ddl.js
CHANGED
|
@@ -69,6 +69,7 @@ CREATE TABLE IF NOT EXISTS memo (
|
|
|
69
69
|
* Both FKs cascade: deleting an actor erases its possession, and deleting a
|
|
70
70
|
* fact (the orphan sweep) drops its holders. The primary key leads with
|
|
71
71
|
* `actor_id`, serving the admission lookup and the actor-side cascade.
|
|
72
|
+
* `created_at` is the holder's most recent put, not the first.
|
|
72
73
|
*/
|
|
73
74
|
export const FACT_HOLDER_SCHEMA = `
|
|
74
75
|
CREATE TABLE IF NOT EXISTS fact_holder (
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* ref only for a fact the acting actor holds.
|
|
10
10
|
*
|
|
11
11
|
* Same discipline as `db/fact_queries.ts`: `deps: QueryDeps` first (the caller
|
|
12
|
-
* owns the transaction), no audit side effects, idempotent
|
|
12
|
+
* owns the transaction), no audit side effects, idempotent writes.
|
|
13
13
|
*
|
|
14
14
|
* @module
|
|
15
15
|
*/
|
|
@@ -31,13 +31,17 @@ export interface FactHolderRow {
|
|
|
31
31
|
*/
|
|
32
32
|
export declare const FACT_HOLDER_COLUMNS: readonly ["actor_id", "hash", "created_at"];
|
|
33
33
|
/**
|
|
34
|
-
* Record `actor_id` as a holder of `hash
|
|
35
|
-
*
|
|
34
|
+
* Record `actor_id` as a holder of `hash`, or refresh the row's `created_at`
|
|
35
|
+
* when it already holds it. The `fact` row must already exist (FK).
|
|
36
|
+
*
|
|
37
|
+
* `created_at` is the holder's most recent put, which the orphan sweep reads
|
|
38
|
+
* as the fact's grace window: a re-put keeps an old orphan out of any
|
|
39
|
+
* sweep whose window covers the put.
|
|
36
40
|
*
|
|
37
41
|
* @param deps - query deps
|
|
38
42
|
* @param actor_id - the actor that brought the bytes
|
|
39
43
|
* @param hash - the fact's hash
|
|
40
|
-
* @mutates fact_holder - inserts the `(actor_id, hash)` row
|
|
44
|
+
* @mutates fact_holder - inserts the `(actor_id, hash)` row, or sets an existing row's `created_at` to now
|
|
41
45
|
*/
|
|
42
46
|
export declare const query_put_fact_holder: (deps: QueryDeps, actor_id: Uuid, hash: FactHash) => Promise<void>;
|
|
43
47
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fact_holder_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_holder_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAEjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEpD,kCAAkC;AAClC,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,IAAI,CAAC;IACf,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,EAAE,IAAI,CAAC;CACjB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,6CAIuB,CAAC;AAExD
|
|
1
|
+
{"version":3,"file":"fact_holder_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_holder_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAEjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEpD,kCAAkC;AAClC,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,IAAI,CAAC;IACf,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,EAAE,IAAI,CAAC;CACjB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,6CAIuB,CAAC;AAExD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,GACjC,MAAM,SAAS,EACf,UAAU,IAAI,EACd,MAAM,QAAQ,KACZ,OAAO,CAAC,IAAI,CAMd,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,OAAO,CAQrF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,SAAS,EACf,UAAU,IAAI,EACd,QAAQ,aAAa,CAAC,MAAM,CAAC,KAC3B,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAQvB,CAAC"}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* ref only for a fact the acting actor holds.
|
|
10
10
|
*
|
|
11
11
|
* Same discipline as `db/fact_queries.ts`: `deps: QueryDeps` first (the caller
|
|
12
|
-
* owns the transaction), no audit side effects, idempotent
|
|
12
|
+
* owns the transaction), no audit side effects, idempotent writes.
|
|
13
13
|
*
|
|
14
14
|
* @module
|
|
15
15
|
*/
|
|
@@ -26,17 +26,21 @@ export const FACT_HOLDER_COLUMNS = [
|
|
|
26
26
|
'created_at'
|
|
27
27
|
];
|
|
28
28
|
/**
|
|
29
|
-
* Record `actor_id` as a holder of `hash
|
|
30
|
-
*
|
|
29
|
+
* Record `actor_id` as a holder of `hash`, or refresh the row's `created_at`
|
|
30
|
+
* when it already holds it. The `fact` row must already exist (FK).
|
|
31
|
+
*
|
|
32
|
+
* `created_at` is the holder's most recent put, which the orphan sweep reads
|
|
33
|
+
* as the fact's grace window: a re-put keeps an old orphan out of any
|
|
34
|
+
* sweep whose window covers the put.
|
|
31
35
|
*
|
|
32
36
|
* @param deps - query deps
|
|
33
37
|
* @param actor_id - the actor that brought the bytes
|
|
34
38
|
* @param hash - the fact's hash
|
|
35
|
-
* @mutates fact_holder - inserts the `(actor_id, hash)` row
|
|
39
|
+
* @mutates fact_holder - inserts the `(actor_id, hash)` row, or sets an existing row's `created_at` to now
|
|
36
40
|
*/
|
|
37
41
|
export const query_put_fact_holder = async (deps, actor_id, hash) => {
|
|
38
42
|
await deps.db.query(`INSERT INTO fact_holder (actor_id, hash) VALUES ($1, $2)
|
|
39
|
-
ON CONFLICT (actor_id, hash) DO
|
|
43
|
+
ON CONFLICT (actor_id, hash) DO UPDATE SET created_at = NOW()`, [actor_id, hash]);
|
|
40
44
|
};
|
|
41
45
|
/**
|
|
42
46
|
* Whether the `fact_holder` table exists — `false` on a database that mounts
|
|
@@ -123,14 +123,15 @@ export interface OrphanFactsListResult {
|
|
|
123
123
|
* outputs? external fact mirrors?) the predicate moves to a generic
|
|
124
124
|
* `fact_consumers` registry; today the cell layer is the only consumer.
|
|
125
125
|
*
|
|
126
|
-
* The `older_than` filter
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
126
|
+
* The `older_than` filter is the grace cutoff: a fact passes it when both
|
|
127
|
+
* its `created_at` and every `fact_holder` row's are older (a re-put
|
|
128
|
+
* refreshes the holder row). Pass `null` to skip the filter (used by the
|
|
129
|
+
* list-summary preview); the delete handler always passes a non-null
|
|
130
|
+
* cutoff (default 0, meaning "any orphan").
|
|
130
131
|
*
|
|
131
132
|
* @param deps - query deps
|
|
132
|
-
* @param older_than - filter to facts created
|
|
133
|
-
* to skip)
|
|
133
|
+
* @param older_than - filter to facts created, and last put by any holder,
|
|
134
|
+
* before this Date (or null to skip)
|
|
134
135
|
* @param sample_limit - row cap for the returned `sample`
|
|
135
136
|
*/
|
|
136
137
|
export declare const query_orphan_facts_list: (deps: QueryDeps, older_than: Date | null, sample_limit: number) => Promise<OrphanFactsListResult>;
|
|
@@ -139,7 +140,9 @@ export declare const query_orphan_facts_list: (deps: QueryDeps, older_than: Date
|
|
|
139
140
|
* (no row-count limit) — callers iterate to unlink disk files. The
|
|
140
141
|
* `older_than` cutoff is required (non-null) here: bulk delete should
|
|
141
142
|
* always be operator-scoped to a time window. A "delete all" sweep
|
|
142
|
-
* passes a far-future cutoff, not `null`.
|
|
143
|
+
* passes a far-future cutoff, not `null`. It is the same grace cutoff as
|
|
144
|
+
* `query_orphan_facts_list`: a fact put — or re-put, deduplicated — by an
|
|
145
|
+
* actor within the window is kept, whatever its own `created_at`.
|
|
143
146
|
*/
|
|
144
147
|
export declare const query_orphan_facts_select_for_delete: (deps: QueryDeps, older_than: Date) => Promise<Array<{
|
|
145
148
|
hash: FactHash;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fact_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAGjE,0CAA0C;AAC1C,MAAM,WAAW,OAAO;IACvB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAK5B,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,UAAU,EAAE,IAAI,CAAC;CACjB;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,kFAOwB,CAAC;AAYlD,qEAAqE;AACrE,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,QAAQ,CAAC;IACf,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,UAAU,EAAE,IAAI,CAAC;CACjB;AAED,oFAAoF;AACpF,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GAAU,MAAM,SAAS,EAAE,OAAO,YAAY,KAAG,OAAO,CAAC,OAAO,CAS1F,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,aAAa,QAAQ,EACrB,eAAe,KAAK,CAAC,QAAQ,CAAC,KAC5B,OAAO,CAAC,IAAI,CAQd,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAU,MAAM,SAAS,EAAE,MAAM,QAAQ,KAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAO5F,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,MAAM,QAAQ,KACZ,OAAO,CAAC,WAAW,GAAG,IAAI,CAO5B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,GAAU,MAAM,SAAS,EAAE,MAAM,QAAQ,KAAG,OAAO,CAAC,OAAO,CAMrF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,aAAa,QAAQ,KACnB,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAMzB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,SAAS,EACf,MAAM,QAAQ,KACZ,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,CAQ9D,CAAC;
|
|
1
|
+
{"version":3,"file":"fact_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_queries.ts"],"names":[],"mappings":"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"}
|
package/dist/db/fact_queries.js
CHANGED
|
@@ -113,6 +113,21 @@ export const query_delete_fact = async (deps, hash) => {
|
|
|
113
113
|
return null;
|
|
114
114
|
return { size: Number(row.size), external_url: row.external_url };
|
|
115
115
|
};
|
|
116
|
+
/**
|
|
117
|
+
* The grace-window half of the orphan cutoff: no `fact_holder` row was
|
|
118
|
+
* recorded (or refreshed) at or after `$1`. A dedup put adds no `fact` row,
|
|
119
|
+
* so `fact.created_at` stays at the first insert — without this, an old
|
|
120
|
+
* orphan an actor re-uploads looks stale, and a sweep between the upload and
|
|
121
|
+
* the cell write that references it deletes the fact (and, by cascade, the
|
|
122
|
+
* possession the write needs). A fact counts as recent when its own
|
|
123
|
+
* `created_at` or any holder's is within the window. Served by
|
|
124
|
+
* `idx_fact_holder_hash`. Twin of the Rust `ORPHAN_FACT_NOT_RECENTLY_HELD`.
|
|
125
|
+
*/
|
|
126
|
+
const ORPHAN_FACT_NOT_RECENTLY_HELD = `NOT EXISTS (
|
|
127
|
+
SELECT 1 FROM fact_holder h
|
|
128
|
+
WHERE h.hash = f.hash
|
|
129
|
+
AND h.created_at >= $1::timestamptz
|
|
130
|
+
)`;
|
|
116
131
|
/**
|
|
117
132
|
* Compute the "orphan facts" set: rows in `fact` where no active
|
|
118
133
|
* (non-tombstone) `cell.refs` array contains the hash.
|
|
@@ -124,14 +139,15 @@ export const query_delete_fact = async (deps, hash) => {
|
|
|
124
139
|
* outputs? external fact mirrors?) the predicate moves to a generic
|
|
125
140
|
* `fact_consumers` registry; today the cell layer is the only consumer.
|
|
126
141
|
*
|
|
127
|
-
* The `older_than` filter
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
142
|
+
* The `older_than` filter is the grace cutoff: a fact passes it when both
|
|
143
|
+
* its `created_at` and every `fact_holder` row's are older (a re-put
|
|
144
|
+
* refreshes the holder row). Pass `null` to skip the filter (used by the
|
|
145
|
+
* list-summary preview); the delete handler always passes a non-null
|
|
146
|
+
* cutoff (default 0, meaning "any orphan").
|
|
131
147
|
*
|
|
132
148
|
* @param deps - query deps
|
|
133
|
-
* @param older_than - filter to facts created
|
|
134
|
-
* to skip)
|
|
149
|
+
* @param older_than - filter to facts created, and last put by any holder,
|
|
150
|
+
* before this Date (or null to skip)
|
|
135
151
|
* @param sample_limit - row cap for the returned `sample`
|
|
136
152
|
*/
|
|
137
153
|
export const query_orphan_facts_list = async (deps, older_than, sample_limit) => {
|
|
@@ -142,7 +158,8 @@ export const query_orphan_facts_list = async (deps, older_than, sample_limit) =>
|
|
|
142
158
|
WHERE c.refs @> ARRAY[f.hash]::text[]
|
|
143
159
|
AND c.deleted_at IS NULL
|
|
144
160
|
)
|
|
145
|
-
AND ($1::timestamptz IS NULL
|
|
161
|
+
AND ($1::timestamptz IS NULL
|
|
162
|
+
OR (f.created_at < $1::timestamptz AND ${ORPHAN_FACT_NOT_RECENTLY_HELD}))`, [older_than]);
|
|
146
163
|
const sample_rows = await deps.db.query(`SELECT hash, size, ${iso8601_timestamp_column('f', 'created_at')} AS created_at, external_url
|
|
147
164
|
FROM fact f
|
|
148
165
|
WHERE NOT EXISTS (
|
|
@@ -150,7 +167,8 @@ export const query_orphan_facts_list = async (deps, older_than, sample_limit) =>
|
|
|
150
167
|
WHERE c.refs @> ARRAY[f.hash]::text[]
|
|
151
168
|
AND c.deleted_at IS NULL
|
|
152
169
|
)
|
|
153
|
-
AND ($1::timestamptz IS NULL
|
|
170
|
+
AND ($1::timestamptz IS NULL
|
|
171
|
+
OR (f.created_at < $1::timestamptz AND ${ORPHAN_FACT_NOT_RECENTLY_HELD}))
|
|
154
172
|
ORDER BY f.created_at ASC
|
|
155
173
|
LIMIT $2`, [older_than, sample_limit]);
|
|
156
174
|
return {
|
|
@@ -169,7 +187,9 @@ export const query_orphan_facts_list = async (deps, older_than, sample_limit) =>
|
|
|
169
187
|
* (no row-count limit) — callers iterate to unlink disk files. The
|
|
170
188
|
* `older_than` cutoff is required (non-null) here: bulk delete should
|
|
171
189
|
* always be operator-scoped to a time window. A "delete all" sweep
|
|
172
|
-
* passes a far-future cutoff, not `null`.
|
|
190
|
+
* passes a far-future cutoff, not `null`. It is the same grace cutoff as
|
|
191
|
+
* `query_orphan_facts_list`: a fact put — or re-put, deduplicated — by an
|
|
192
|
+
* actor within the window is kept, whatever its own `created_at`.
|
|
173
193
|
*/
|
|
174
194
|
export const query_orphan_facts_select_for_delete = async (deps, older_than) => {
|
|
175
195
|
const rows = await deps.db.query(`SELECT hash, size, external_url
|
|
@@ -179,6 +199,7 @@ export const query_orphan_facts_select_for_delete = async (deps, older_than) =>
|
|
|
179
199
|
WHERE c.refs @> ARRAY[f.hash]::text[]
|
|
180
200
|
AND c.deleted_at IS NULL
|
|
181
201
|
)
|
|
182
|
-
AND f.created_at < $1::timestamptz
|
|
202
|
+
AND f.created_at < $1::timestamptz
|
|
203
|
+
AND ${ORPHAN_FACT_NOT_RECENTLY_HELD}`, [older_than]);
|
|
183
204
|
return rows.map((r) => ({ hash: r.hash, size: Number(r.size), external_url: r.external_url }));
|
|
184
205
|
};
|
|
@@ -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
|
*
|
|
@@ -96,8 +97,13 @@
|
|
|
96
97
|
* from executing as stored XSS for any reader of a referencing cell, **every**
|
|
97
98
|
* served-fact response carries `X-Content-Type-Options: nosniff` +
|
|
98
99
|
* `Content-Security-Policy: default-src 'none'; sandbox` (the
|
|
99
|
-
* `FACT_SECURITY_HEADERS` set
|
|
100
|
-
*
|
|
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.
|
|
101
107
|
* `Content-Disposition: attachment` is deliberately **not** added — it would
|
|
102
108
|
* force-download and break legitimate inline image rendering.
|
|
103
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
|
*
|
|
@@ -96,8 +97,13 @@
|
|
|
96
97
|
* from executing as stored XSS for any reader of a referencing cell, **every**
|
|
97
98
|
* served-fact response carries `X-Content-Type-Options: nosniff` +
|
|
98
99
|
* `Content-Security-Policy: default-src 'none'; sandbox` (the
|
|
99
|
-
* `FACT_SECURITY_HEADERS` set
|
|
100
|
-
*
|
|
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.
|
|
101
107
|
* `Content-Disposition: attachment` is deliberately **not** added — it would
|
|
102
108
|
* force-download and break legitimate inline image rendering.
|
|
103
109
|
*
|
|
@@ -131,33 +137,9 @@ import { query_cell_grant_list_for_cell } from '../db/cell_grant_queries.js';
|
|
|
131
137
|
import { can_view_cell } from '../auth/cell_authorize.js';
|
|
132
138
|
import { cell_steward_admits } from '../auth/cell_steward.js';
|
|
133
139
|
import { parse_file_fact_url } from '../db/file_fact_url.js';
|
|
140
|
+
import { FACT_SECURITY_HEADERS } from './fact_security_headers.js';
|
|
134
141
|
/** `Cache-Control` for fact responses — 5 min revocation window. */
|
|
135
142
|
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
|
-
};
|
|
161
143
|
/**
|
|
162
144
|
* Path-param schema for the bare-hash route. Matching the canonical
|
|
163
145
|
* fact-hash form here pushes malformed-hash 400s through the framework's
|
|
@@ -234,6 +216,8 @@ const serve_fact_bytes = async (c, route, hash, config) => {
|
|
|
234
216
|
const { shard, rest } = parsed;
|
|
235
217
|
if (x_accel !== undefined) {
|
|
236
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.
|
|
237
221
|
return c.body(null, 200, {
|
|
238
222
|
'Content-Type': content_type,
|
|
239
223
|
'Content-Length': size,
|