@fuzdev/fuz_app 0.95.0 → 0.96.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/account_queries.d.ts +20 -5
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +21 -4
- package/dist/auth/request_context.js +2 -2
- package/dist/db/CLAUDE.md +6 -3
- package/dist/db/cell_queries.js +8 -1
- package/dist/db/create_db.d.ts.map +1 -1
- package/dist/db/create_db.js +2 -14
- package/dist/db/db_pg.d.ts +30 -2
- package/dist/db/db_pg.d.ts.map +1 -1
- package/dist/db/db_pg.js +33 -2
- package/dist/db/fact_queries.d.ts.map +1 -1
- package/dist/server/serve_fact_route.d.ts +12 -0
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +40 -0
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +23 -0
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +6 -1
- package/dist/testing/middleware.js +4 -4
- package/package.json +6 -2
|
@@ -159,14 +159,29 @@ export declare const query_account_has_any: (deps: QueryDeps) => Promise<boolean
|
|
|
159
159
|
*/
|
|
160
160
|
export declare const query_create_actor: (deps: QueryDeps, account_id: string, name: string) => Promise<Actor>;
|
|
161
161
|
/**
|
|
162
|
-
* List every actor on an account, ordered by `created_at
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* `
|
|
162
|
+
* List every actor on an account, ordered by `created_at`, **including
|
|
163
|
+
* soft-deleted (tombstoned) rows**.
|
|
164
|
+
*
|
|
165
|
+
* Used by the admin/snapshot handlers (`account_delete` / `account_purge` /
|
|
166
|
+
* `account_undelete`) that must enumerate every actor that ever existed on
|
|
167
|
+
* the account to snapshot names into per-actor audit events. For the
|
|
168
|
+
* acting-actor resolution path — which must never resolve a tombstoned
|
|
169
|
+
* actor — use `query_active_actors_by_account`. For lookups by id, use
|
|
167
170
|
* `query_actor_by_id` instead.
|
|
168
171
|
*/
|
|
169
172
|
export declare const query_actors_by_account: (deps: QueryDeps, account_id: string) => Promise<Array<Actor>>;
|
|
173
|
+
/**
|
|
174
|
+
* List **active** (non-tombstoned) actors on an account, ordered by
|
|
175
|
+
* `created_at`.
|
|
176
|
+
*
|
|
177
|
+
* Filters `deleted_at IS NULL` so a soft-deleted actor can never be resolved
|
|
178
|
+
* as the acting actor (and carry its `role_grants` into the role gate). Used
|
|
179
|
+
* by `resolve_acting_actor` to resolve the acting actor for a request: 1
|
|
180
|
+
* actor picks transparently, multiple require an explicit `acting` field on
|
|
181
|
+
* the request payload. The admin/snapshot handlers that legitimately need
|
|
182
|
+
* tombstoned rows stay on the unfiltered `query_actors_by_account`.
|
|
183
|
+
*/
|
|
184
|
+
export declare const query_active_actors_by_account: (deps: QueryDeps, account_id: string) => Promise<Array<Actor>>;
|
|
170
185
|
/**
|
|
171
186
|
* Find an actor by id.
|
|
172
187
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAEN,KAAK,OAAO,EACZ,KAAK,KAAK,EACV,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,MAAM,qBAAqB,CAAC;AAqB7B;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,OAAO,kBAAkB,KACvB,OAAO,CAAC,OAAO,CAQjB,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,IAAI,MAAM,KACR,OAAO,CAAC,OAAO,GAAG,SAAS,CAK7B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,SAAS,EACf,UAAU,MAAM,KACd,OAAO,CAAC,OAAO,GAAG,SAAS,CAK7B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,SAAS,EACf,OAAO,MAAM,KACX,OAAO,CAAC,OAAO,GAAG,SAAS,CAK7B,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,kCAAkC,GAC9C,MAAM,SAAS,EACf,OAAO,MAAM,KACX,OAAO,CAAC,OAAO,GAAG,SAAS,CAS7B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,IAAI,MAAM,EACV,eAAe,MAAM,EACrB,YAAY,MAAM,GAAG,IAAI,EACzB,eAAe,MAAM,KACnB,OAAO,CAAC,OAAO,CAQjB,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,SAAS,EACf,IAAI,MAAM,EACV,YAAY,MAAM,GAAG,IAAI,KACvB,OAAO,CAAC,uBAAuB,GAAG,SAAS,CAO7C,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,IAAI,MAAM,KACR,OAAO,CAAC,uBAAuB,GAAG,SAAS,CAK7C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,IAAI,MAAM,EACV,YAAY,MAAM,GAAG,IAAI,KACvB,OAAO,CAAC,OAAO,CAQjB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,SAAS,EACf,IAAI,MAAM,KACR,OAAO,CAAC,uBAAuB,GAAG,SAAS,CAO7C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAU,MAAM,SAAS,EAAE,IAAI,MAAM,KAAG,OAAO,CAAC,OAAO,CAQvF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,OAAO,CAK5E,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC9B,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,MAAM,MAAM,KACV,OAAO,CAAC,KAAK,CAMf,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"account_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_queries.ts"],"names":[],"mappings":"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"}
|
|
@@ -218,16 +218,33 @@ export const query_create_actor = async (deps, account_id, name) => {
|
|
|
218
218
|
return assert_row(row, 'INSERT INTO actor');
|
|
219
219
|
};
|
|
220
220
|
/**
|
|
221
|
-
* List every actor on an account, ordered by `created_at
|
|
221
|
+
* List every actor on an account, ordered by `created_at`, **including
|
|
222
|
+
* soft-deleted (tombstoned) rows**.
|
|
222
223
|
*
|
|
223
|
-
* Used by
|
|
224
|
-
*
|
|
225
|
-
*
|
|
224
|
+
* Used by the admin/snapshot handlers (`account_delete` / `account_purge` /
|
|
225
|
+
* `account_undelete`) that must enumerate every actor that ever existed on
|
|
226
|
+
* the account to snapshot names into per-actor audit events. For the
|
|
227
|
+
* acting-actor resolution path — which must never resolve a tombstoned
|
|
228
|
+
* actor — use `query_active_actors_by_account`. For lookups by id, use
|
|
226
229
|
* `query_actor_by_id` instead.
|
|
227
230
|
*/
|
|
228
231
|
export const query_actors_by_account = async (deps, account_id) => {
|
|
229
232
|
return deps.db.query(`SELECT * FROM actor WHERE account_id = $1 ORDER BY created_at ASC, id ASC`, [account_id]);
|
|
230
233
|
};
|
|
234
|
+
/**
|
|
235
|
+
* List **active** (non-tombstoned) actors on an account, ordered by
|
|
236
|
+
* `created_at`.
|
|
237
|
+
*
|
|
238
|
+
* Filters `deleted_at IS NULL` so a soft-deleted actor can never be resolved
|
|
239
|
+
* as the acting actor (and carry its `role_grants` into the role gate). Used
|
|
240
|
+
* by `resolve_acting_actor` to resolve the acting actor for a request: 1
|
|
241
|
+
* actor picks transparently, multiple require an explicit `acting` field on
|
|
242
|
+
* the request payload. The admin/snapshot handlers that legitimately need
|
|
243
|
+
* tombstoned rows stay on the unfiltered `query_actors_by_account`.
|
|
244
|
+
*/
|
|
245
|
+
export const query_active_actors_by_account = async (deps, account_id) => {
|
|
246
|
+
return deps.db.query(`SELECT * FROM actor WHERE account_id = $1 AND deleted_at IS NULL ORDER BY created_at ASC, id ASC`, [account_id]);
|
|
247
|
+
};
|
|
231
248
|
/**
|
|
232
249
|
* Find an actor by id.
|
|
233
250
|
*/
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import { is_role_grant_active } from './account_schema.js';
|
|
38
38
|
import { hash_session_token, session_touch_fire_and_forget, query_session_get_valid, } from './session_queries.js';
|
|
39
|
-
import { query_account_by_id, query_actor_by_id,
|
|
39
|
+
import { query_account_by_id, query_actor_by_id, query_active_actors_by_account, } from './account_queries.js';
|
|
40
40
|
import { query_role_grant_find_active_for_actor } from './role_grant_queries.js';
|
|
41
41
|
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY, TEST_CONTEXT_PRESET_KEY, } from '../hono_context.js';
|
|
42
42
|
import { is_public_auth, needs_actor } from '../http/auth_shape.js';
|
|
@@ -174,7 +174,7 @@ export const has_any_scoped_role = (ctx, roles, scope_id, now = new Date()) => {
|
|
|
174
174
|
* @param acting_actor_id - the requested acting actor id, or `undefined`
|
|
175
175
|
*/
|
|
176
176
|
export const resolve_acting_actor = async (deps, account_id, acting_actor_id) => {
|
|
177
|
-
const actors = await
|
|
177
|
+
const actors = await query_active_actors_by_account(deps, account_id);
|
|
178
178
|
if (actors.length === 0)
|
|
179
179
|
return { ok: false, reason: 'no_actors' };
|
|
180
180
|
if (acting_actor_id == null) {
|
package/dist/db/CLAUDE.md
CHANGED
|
@@ -15,9 +15,12 @@ the code.
|
|
|
15
15
|
- `db.ts` — `Db` abstraction over pg + PGlite, `query` / `query_one` /
|
|
16
16
|
`transaction`, `no_nested_transaction` guard.
|
|
17
17
|
- `create_db.ts` — URL-based driver auto-detection (`postgres` /
|
|
18
|
-
`pglite-file` / `pglite-memory`)
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
`pglite-file` / `pglite-memory`).
|
|
19
|
+
- `db_pg.ts` / `db_pglite.ts` — driver adapters. `db_pg.ts` also exports
|
|
20
|
+
`register_pg_type_parsers`, the shared int8→number coercion (so
|
|
21
|
+
`BIGSERIAL` columns read as JS numbers, matching PGlite) — called by both
|
|
22
|
+
`create_db` and the test pg factory (`testing/db.ts`) so prod and tests
|
|
23
|
+
read the same wire shape from one registration site.
|
|
21
24
|
- `migrate.ts` — advisory-lock migration runner (`run_migrations`,
|
|
22
25
|
`baseline`), `Migration` / `MigrationNamespace` / `MigrationError`.
|
|
23
26
|
- `query_deps.ts` — `QueryDeps = {db}`, the first param to every `query_*`.
|
package/dist/db/cell_queries.js
CHANGED
|
@@ -394,8 +394,15 @@ const build_shared_with_sql = (order_column, order_direction) => `${CALLER_ROLE_
|
|
|
394
394
|
* Returns `null` (rather than `[]`) when no refs are present — the column
|
|
395
395
|
* is nullable and the `idx_cell_refs` partial index is `WHERE refs IS NOT
|
|
396
396
|
* NULL`, so an empty array would force every cell into the index.
|
|
397
|
+
*
|
|
398
|
+
* Sorts the deduped refs lexicographically so the stored `text[]` is
|
|
399
|
+
* deterministic and byte-identical to the Rust twin (`fuz_cell`'s
|
|
400
|
+
* `extract_refs`, which sorts the same `blake3:`-prefixed element form).
|
|
401
|
+
* The shared `fact_hash_extract_refs` deliberately preserves traversal
|
|
402
|
+
* order per its contract, so the sort lives here at the storage boundary,
|
|
403
|
+
* not in the util.
|
|
397
404
|
*/
|
|
398
405
|
const derive_refs = (data) => {
|
|
399
406
|
const refs = fact_hash_extract_refs(data);
|
|
400
|
-
return refs.length > 0 ? refs : null;
|
|
407
|
+
return refs.length > 0 ? refs.sort() : null;
|
|
401
408
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create_db.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/create_db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,SAAS,CAAC;AAIxC,yCAAyC;AACzC,MAAM,WAAW,cAAc;IAC9B,EAAE,EAAE,EAAE,CAAC;IACP,iFAAiF;IACjF,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,GAAU,cAAc,MAAM,KAAG,OAAO,CAAC,cAAc,
|
|
1
|
+
{"version":3,"file":"create_db.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/create_db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,SAAS,CAAC;AAIxC,yCAAyC;AACzC,MAAM,WAAW,cAAc;IAC9B,EAAE,EAAE,EAAE,CAAC;IACP,iFAAiF;IACjF,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,GAAU,cAAc,MAAM,KAAG,OAAO,CAAC,cAAc,CAiC5E,CAAC"}
|
package/dist/db/create_db.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* @module
|
|
14
14
|
*/
|
|
15
|
-
import { create_pg_db } from './db_pg.js';
|
|
15
|
+
import { create_pg_db, register_pg_type_parsers } from './db_pg.js';
|
|
16
16
|
import { create_pglite_db } from './db_pglite.js';
|
|
17
17
|
/**
|
|
18
18
|
* Create a database connection based on a URL.
|
|
@@ -32,19 +32,7 @@ import { create_pglite_db } from './db_pglite.js';
|
|
|
32
32
|
export const create_db = async (database_url) => {
|
|
33
33
|
if (database_url.startsWith('postgres://') || database_url.startsWith('postgresql://')) {
|
|
34
34
|
const { default: pg } = await import('pg');
|
|
35
|
-
|
|
36
|
-
// as a string to avoid 2^53 precision loss; our only int8 column
|
|
37
|
-
// today (`audit_log.seq`) stays well under that bound, and reading
|
|
38
|
-
// as number keeps the wire shape uniform across the SERIAL→BIGSERIAL
|
|
39
|
-
// widening.
|
|
40
|
-
//
|
|
41
|
-
// CAVEAT: pg.types.setTypeParser mutates pg.types globally — every
|
|
42
|
-
// pg.Pool in the process inherits the coercion, including pools the
|
|
43
|
-
// consumer constructs against unrelated databases. Any future int8
|
|
44
|
-
// column that could legitimately exceed 2^53 (file sizes, byte
|
|
45
|
-
// offsets) will silently round; if one lands, localize via a
|
|
46
|
-
// per-pool `types` override instead of widening this global parser.
|
|
47
|
-
pg.types.setTypeParser(20, (val) => Number(val));
|
|
35
|
+
await register_pg_type_parsers();
|
|
48
36
|
const pool = new pg.Pool({ connectionString: database_url });
|
|
49
37
|
const { db, close } = create_pg_db(pool);
|
|
50
38
|
return {
|
package/dist/db/db_pg.d.ts
CHANGED
|
@@ -2,13 +2,41 @@
|
|
|
2
2
|
* PostgreSQL driver adapter for `Db`.
|
|
3
3
|
*
|
|
4
4
|
* Provides `create_pg_db()` to construct a `Db` backed by a `pg.Pool`.
|
|
5
|
-
*
|
|
6
|
-
* by callers (e.g., `create_db`) that construct the
|
|
5
|
+
* Statically imports only `pg` types; the `pg` runtime is dynamically
|
|
6
|
+
* imported where needed — by callers (e.g., `create_db`) that construct the
|
|
7
|
+
* `Pool`, and by `register_pg_type_parsers` to reach `pg.types`. pglite-only
|
|
8
|
+
* consumers never reach either path, so `pg` stays an optional peer dep.
|
|
7
9
|
*
|
|
8
10
|
* @module
|
|
9
11
|
*/
|
|
10
12
|
import type { Pool } from 'pg';
|
|
11
13
|
import { type DbDriverResult } from './db.js';
|
|
14
|
+
/**
|
|
15
|
+
* Register the shared pg type-parser overrides on the module-global `pg.types`.
|
|
16
|
+
*
|
|
17
|
+
* Dynamically imports the `pg` runtime (so pglite-only consumers, who never
|
|
18
|
+
* call this, don't need `pg` installed) and coerces int8 (`BIGINT`, OID 20)
|
|
19
|
+
* to a JS number. pg defaults to returning
|
|
20
|
+
* int8 as a string to avoid 2^53 precision loss; our int8 columns today
|
|
21
|
+
* (`audit_log.seq`, `cell_history.id`, `fact.size`) stay well under that
|
|
22
|
+
* bound, and reading as a number keeps the wire shape uniform with PGlite —
|
|
23
|
+
* which returns int8 as a number — so `AuditLogEvent.seq` and friends
|
|
24
|
+
* validate identically across both drivers.
|
|
25
|
+
*
|
|
26
|
+
* Both `create_db` (production) and the test pg factory (`testing/db.ts`)
|
|
27
|
+
* register through this single site so test and prod read the same shape; a
|
|
28
|
+
* divergence here is exactly the test/prod write-semantics gap the parser
|
|
29
|
+
* exists to close.
|
|
30
|
+
*
|
|
31
|
+
* CAVEAT: `setTypeParser` mutates `pg.types` globally — every `pg.Pool` in
|
|
32
|
+
* the process inherits the coercion, including pools the consumer constructs
|
|
33
|
+
* against unrelated databases. Any future int8 column that could legitimately
|
|
34
|
+
* exceed 2^53 (byte offsets, counters) will silently round; if one lands,
|
|
35
|
+
* localize via a per-pool `types` override instead of widening this global.
|
|
36
|
+
*
|
|
37
|
+
* @mutates `pg.types` - registers the int8 parser on the global pg type registry
|
|
38
|
+
*/
|
|
39
|
+
export declare const register_pg_type_parsers: () => Promise<void>;
|
|
12
40
|
/**
|
|
13
41
|
* Create a `Db` backed by a `pg.Pool`.
|
|
14
42
|
*
|
package/dist/db/db_pg.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db_pg.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/db_pg.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"db_pg.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/db_pg.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,IAAI,CAAC;AAE7B,OAAO,EAA4B,KAAK,cAAc,EAAC,MAAM,SAAS,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,wBAAwB,QAAa,OAAO,CAAC,IAAI,CAG7D,CAAC;AA2BF;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,IAAI,KAAG,cAGxC,CAAC"}
|
package/dist/db/db_pg.js
CHANGED
|
@@ -2,12 +2,43 @@
|
|
|
2
2
|
* PostgreSQL driver adapter for `Db`.
|
|
3
3
|
*
|
|
4
4
|
* Provides `create_pg_db()` to construct a `Db` backed by a `pg.Pool`.
|
|
5
|
-
*
|
|
6
|
-
* by callers (e.g., `create_db`) that construct the
|
|
5
|
+
* Statically imports only `pg` types; the `pg` runtime is dynamically
|
|
6
|
+
* imported where needed — by callers (e.g., `create_db`) that construct the
|
|
7
|
+
* `Pool`, and by `register_pg_type_parsers` to reach `pg.types`. pglite-only
|
|
8
|
+
* consumers never reach either path, so `pg` stays an optional peer dep.
|
|
7
9
|
*
|
|
8
10
|
* @module
|
|
9
11
|
*/
|
|
10
12
|
import { Db, no_nested_transaction } from './db.js';
|
|
13
|
+
/**
|
|
14
|
+
* Register the shared pg type-parser overrides on the module-global `pg.types`.
|
|
15
|
+
*
|
|
16
|
+
* Dynamically imports the `pg` runtime (so pglite-only consumers, who never
|
|
17
|
+
* call this, don't need `pg` installed) and coerces int8 (`BIGINT`, OID 20)
|
|
18
|
+
* to a JS number. pg defaults to returning
|
|
19
|
+
* int8 as a string to avoid 2^53 precision loss; our int8 columns today
|
|
20
|
+
* (`audit_log.seq`, `cell_history.id`, `fact.size`) stay well under that
|
|
21
|
+
* bound, and reading as a number keeps the wire shape uniform with PGlite —
|
|
22
|
+
* which returns int8 as a number — so `AuditLogEvent.seq` and friends
|
|
23
|
+
* validate identically across both drivers.
|
|
24
|
+
*
|
|
25
|
+
* Both `create_db` (production) and the test pg factory (`testing/db.ts`)
|
|
26
|
+
* register through this single site so test and prod read the same shape; a
|
|
27
|
+
* divergence here is exactly the test/prod write-semantics gap the parser
|
|
28
|
+
* exists to close.
|
|
29
|
+
*
|
|
30
|
+
* CAVEAT: `setTypeParser` mutates `pg.types` globally — every `pg.Pool` in
|
|
31
|
+
* the process inherits the coercion, including pools the consumer constructs
|
|
32
|
+
* against unrelated databases. Any future int8 column that could legitimately
|
|
33
|
+
* exceed 2^53 (byte offsets, counters) will silently round; if one lands,
|
|
34
|
+
* localize via a per-pool `types` override instead of widening this global.
|
|
35
|
+
*
|
|
36
|
+
* @mutates `pg.types` - registers the int8 parser on the global pg type registry
|
|
37
|
+
*/
|
|
38
|
+
export const register_pg_type_parsers = async () => {
|
|
39
|
+
const { types } = await import('pg');
|
|
40
|
+
types.setTypeParser(20, (val) => Number(val));
|
|
41
|
+
};
|
|
11
42
|
/**
|
|
12
43
|
* Create a transaction implementation for a `pg.Pool`.
|
|
13
44
|
*
|
|
@@ -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,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAE/C,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,+BAA+B,CAAC;AAE5D,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;
|
|
1
|
+
{"version":3,"file":"fact_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_queries.ts"],"names":[],"mappings":"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"}
|
|
@@ -63,6 +63,18 @@
|
|
|
63
63
|
* is the doc cell, so serving goes view-doc-cell → doc-cell-refs-include-hash
|
|
64
64
|
* → serve.
|
|
65
65
|
*
|
|
66
|
+
* ## Untrusted-content hardening
|
|
67
|
+
*
|
|
68
|
+
* Fact bytes are served with a producer-supplied `content_type` emitted
|
|
69
|
+
* verbatim, inline. To stop a fact stored as (or sniffable to) `text/html`
|
|
70
|
+
* from executing as stored XSS for any reader of a referencing cell, **every**
|
|
71
|
+
* served-fact response carries `X-Content-Type-Options: nosniff` +
|
|
72
|
+
* `Content-Security-Policy: default-src 'none'; sandbox` (the
|
|
73
|
+
* `FACT_SECURITY_HEADERS` set, applied in all three serve branches). The same
|
|
74
|
+
* pair, byte-identical, is set by the Rust twin (`fuz_fact_serving::serve`).
|
|
75
|
+
* `Content-Disposition: attachment` is deliberately **not** added — it would
|
|
76
|
+
* force-download and break legitimate inline image rendering.
|
|
77
|
+
*
|
|
66
78
|
* ## Defense-in-depth
|
|
67
79
|
*
|
|
68
80
|
* The `external_url` regex is re-validated before issuing
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AAMH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAepD,OAAO,EAAsC,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAE1F,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAkE7C,MAAM,WAAW,+BAA+B;IAC/C;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,GAAG,EAAE,MAAM,CAAC;CACZ;AA2GD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,+BAA+B,KACtC,SA8CF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,+BAA+B,KACtC,SAgCF,CAAC"}
|
|
@@ -63,6 +63,18 @@
|
|
|
63
63
|
* is the doc cell, so serving goes view-doc-cell → doc-cell-refs-include-hash
|
|
64
64
|
* → serve.
|
|
65
65
|
*
|
|
66
|
+
* ## Untrusted-content hardening
|
|
67
|
+
*
|
|
68
|
+
* Fact bytes are served with a producer-supplied `content_type` emitted
|
|
69
|
+
* verbatim, inline. To stop a fact stored as (or sniffable to) `text/html`
|
|
70
|
+
* from executing as stored XSS for any reader of a referencing cell, **every**
|
|
71
|
+
* served-fact response carries `X-Content-Type-Options: nosniff` +
|
|
72
|
+
* `Content-Security-Policy: default-src 'none'; sandbox` (the
|
|
73
|
+
* `FACT_SECURITY_HEADERS` set, applied in all three serve branches). The same
|
|
74
|
+
* pair, byte-identical, is set by the Rust twin (`fuz_fact_serving::serve`).
|
|
75
|
+
* `Content-Disposition: attachment` is deliberately **not** added — it would
|
|
76
|
+
* force-download and break legitimate inline image rendering.
|
|
77
|
+
*
|
|
66
78
|
* ## Defense-in-depth
|
|
67
79
|
*
|
|
68
80
|
* The `external_url` regex is re-validated before issuing
|
|
@@ -92,6 +104,31 @@ import { can_view_cell } from '../auth/cell_authorize.js';
|
|
|
92
104
|
import { parse_file_fact_url } from '../db/file_fact_url.js';
|
|
93
105
|
/** `Cache-Control` for fact responses — 5 min revocation window. */
|
|
94
106
|
const CACHE_CONTROL = 'private, max-age=300';
|
|
107
|
+
/**
|
|
108
|
+
* `X-Content-Type-Options` for fact responses — block MIME sniffing so an
|
|
109
|
+
* `application/octet-stream` fact can't be sniffed into executable HTML.
|
|
110
|
+
*/
|
|
111
|
+
const X_CONTENT_TYPE_OPTIONS = 'nosniff';
|
|
112
|
+
/**
|
|
113
|
+
* `Content-Security-Policy` for fact responses. Fact bytes carry a
|
|
114
|
+
* producer-supplied `content_type` served verbatim inline, so a fact stored as
|
|
115
|
+
* (or sniffable to) `text/html` would otherwise execute as stored XSS for any
|
|
116
|
+
* reader of a referencing cell. `default-src 'none'; sandbox` neutralizes
|
|
117
|
+
* script execution and sub-resource loads even when a fact is rendered inline;
|
|
118
|
+
* harmless for directly-served images (they load no sub-resources). The same
|
|
119
|
+
* pair, byte-identical, is set by the Rust twin (`fuz_fact_serving::serve`).
|
|
120
|
+
*/
|
|
121
|
+
const CONTENT_SECURITY_POLICY = "default-src 'none'; sandbox";
|
|
122
|
+
/**
|
|
123
|
+
* The untrusted-content hardening headers applied to every served-fact
|
|
124
|
+
* response across all three branches (embedded, `X-Accel-Redirect`,
|
|
125
|
+
* disk-stream). `Content-Disposition: attachment` is deliberately omitted — it
|
|
126
|
+
* would force-download and break legitimate inline image rendering.
|
|
127
|
+
*/
|
|
128
|
+
const FACT_SECURITY_HEADERS = {
|
|
129
|
+
'X-Content-Type-Options': X_CONTENT_TYPE_OPTIONS,
|
|
130
|
+
'Content-Security-Policy': CONTENT_SECURITY_POLICY,
|
|
131
|
+
};
|
|
95
132
|
/**
|
|
96
133
|
* Path-param schema for the bare-hash route. Matching the canonical
|
|
97
134
|
* fact-hash form here pushes malformed-hash 400s through the framework's
|
|
@@ -150,6 +187,7 @@ const serve_fact_bytes = async (c, route, hash, config) => {
|
|
|
150
187
|
'Content-Type': content_type,
|
|
151
188
|
'Content-Length': size,
|
|
152
189
|
'Cache-Control': CACHE_CONTROL,
|
|
190
|
+
...FACT_SECURITY_HEADERS,
|
|
153
191
|
});
|
|
154
192
|
}
|
|
155
193
|
// External — defense-in-depth re-validate the URL before trusting it
|
|
@@ -167,6 +205,7 @@ const serve_fact_bytes = async (c, route, hash, config) => {
|
|
|
167
205
|
'Content-Length': size,
|
|
168
206
|
'Cache-Control': CACHE_CONTROL,
|
|
169
207
|
'X-Accel-Redirect': `${x_accel_redirect_prefix}${shard}/${rest}`,
|
|
208
|
+
...FACT_SECURITY_HEADERS,
|
|
170
209
|
});
|
|
171
210
|
}
|
|
172
211
|
// Dev / tests: stream from disk. `createReadStream` errors land on the
|
|
@@ -178,6 +217,7 @@ const serve_fact_bytes = async (c, route, hash, config) => {
|
|
|
178
217
|
'Content-Type': content_type,
|
|
179
218
|
'Content-Length': size,
|
|
180
219
|
'Cache-Control': CACHE_CONTROL,
|
|
220
|
+
...FACT_SECURITY_HEADERS,
|
|
181
221
|
});
|
|
182
222
|
};
|
|
183
223
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_crud.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_crud.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsD9B,OAAO,KAAK,EAAC,wBAAwB,EAAC,MAAM,YAAY,CAAC;AAGzD,eAAO,MAAM,8BAA8B,GAAI,SAAS,wBAAwB,KAAG,
|
|
1
|
+
{"version":3,"file":"cell_crud.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_crud.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsD9B,OAAO,KAAK,EAAC,wBAAwB,EAAC,MAAM,YAAY,CAAC;AAGzD,eAAO,MAAM,8BAA8B,GAAI,SAAS,wBAAwB,KAAG,IA6VlF,CAAC"}
|
|
@@ -83,6 +83,29 @@ export const describe_cell_crud_cross_tests = (options) => {
|
|
|
83
83
|
assert.ok(!gone.ok, 'deleted cell still readable');
|
|
84
84
|
assert.strictEqual(error_reason(gone), 'cell_not_found');
|
|
85
85
|
});
|
|
86
|
+
test_if(capabilities.cell_crud, 'refs are deduped and lexicographically sorted (deterministic, cross-impl-identical)', async () => {
|
|
87
|
+
const fixture = await setup_test();
|
|
88
|
+
const owner = await fixture.create_account({ username: 'cell_refs_owner' });
|
|
89
|
+
const t = fixture.fresh_transport();
|
|
90
|
+
const owner_headers = owner.create_session_headers();
|
|
91
|
+
// Three valid distinct fact hashes that sort a < b < c, embedded in
|
|
92
|
+
// `data` in a deliberately unsorted, nested, duplicated order. Both
|
|
93
|
+
// spines must derive `refs` as the sorted [a, b, c] regardless of
|
|
94
|
+
// traversal order or `HashSet` iteration — the byte-identical
|
|
95
|
+
// cross-backend contract (Rust `extract_refs` ↔ TS `derive_refs`).
|
|
96
|
+
const ref_a = `blake3:${'a'.repeat(64)}`;
|
|
97
|
+
const ref_b = `blake3:${'b'.repeat(64)}`;
|
|
98
|
+
const ref_c = `blake3:${'c'.repeat(64)}`;
|
|
99
|
+
const sorted = [ref_a, ref_b, ref_c];
|
|
100
|
+
const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note', cover: ref_c, nested: { attachments: [ref_b, ref_a, ref_b] } } }, owner_headers), CellCreateOutput);
|
|
101
|
+
assert.deepStrictEqual(created.cell.refs, sorted, 'create did not return sorted refs');
|
|
102
|
+
// Read it back — the stored column round-trips sorted too.
|
|
103
|
+
const got = expect_output(await cross_rpc_call(t, rpc_path, 'cell_get', { id: created.cell.id }, owner_headers), CellGetOutput);
|
|
104
|
+
assert.deepStrictEqual(got.cell.refs, sorted, 'get did not return sorted refs');
|
|
105
|
+
// Update reorders the embedded refs — the new column is still sorted.
|
|
106
|
+
const updated = expect_output(await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id: created.cell.id, data: { refs: [ref_b, ref_c, ref_a] } }, owner_headers), CellUpdateOutput);
|
|
107
|
+
assert.deepStrictEqual(updated.cell.refs, sorted, 'update did not return sorted refs');
|
|
108
|
+
});
|
|
86
109
|
test_if(capabilities.cell_crud, 'anon sees public cells only; private is 404', async () => {
|
|
87
110
|
const fixture = await setup_test();
|
|
88
111
|
const owner = await fixture.create_account({ username: 'cell_anon_owner' });
|
package/dist/testing/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/db.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AA8B7B,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAKpC;;GAEG;AACH,eAAO,MAAM,KAAK,SAA4B,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,OAAO,CAAC,EAAE,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GAAU,IAAI,EAAE,KAAG,OAAO,CAAC,IAAI,CAGvD,CAAC;AAMF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,GAAI,aAAa,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,KAAG,SAkB7E,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,iBAAiB,GAC7B,aAAa,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,EACtC,WAAW,MAAM,KACf,
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/db.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AA8B7B,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAKpC;;GAEG;AACH,eAAO,MAAM,KAAK,SAA4B,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,OAAO,CAAC,EAAE,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GAAU,IAAI,EAAE,KAAG,OAAO,CAAC,IAAI,CAGvD,CAAC;AAMF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,GAAI,aAAa,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,KAAG,SAkB7E,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,iBAAiB,GAC7B,aAAa,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,EACtC,WAAW,MAAM,KACf,SAgEF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,UAQhC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,UAAyC,CAAC;AAEvF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,GAAU,IAAI,EAAE,KAAG,OAAO,CAAC,IAAI,CAG3D,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,GAC9B,WAAW,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,EACvC,iBAAiB,KAAK,CAAC,MAAM,CAAC,KAC5B,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,KAAK,IAAI,CAwBzD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAAI,WAAW,KAAK,CAAC,SAAS,CAAC,KAAG,IAMnE,CAAC"}
|
package/dist/testing/db.js
CHANGED
|
@@ -26,7 +26,7 @@ import { describe, beforeAll, beforeEach, afterAll } from 'vitest';
|
|
|
26
26
|
import { to_error_message } from '@fuzdev/fuz_util/error.ts';
|
|
27
27
|
import { create_pglite_db } from '../db/db_pglite.js';
|
|
28
28
|
import { assert_valid_sql_identifier } from '../db/sql_identifier.js';
|
|
29
|
-
import { create_pg_db } from '../db/db_pg.js';
|
|
29
|
+
import { create_pg_db, register_pg_type_parsers } from '../db/db_pg.js';
|
|
30
30
|
/**
|
|
31
31
|
* CI detection — `CI=true` is set automatically by GitHub Actions, GitLab CI, etc.
|
|
32
32
|
*/
|
|
@@ -121,6 +121,11 @@ export const create_pg_factory = (init_schema, test_url) => {
|
|
|
121
121
|
pool_ref = null;
|
|
122
122
|
}
|
|
123
123
|
const { Pool } = await import('pg');
|
|
124
|
+
// Mirror production's int8→number coercion (`create_db`) so the
|
|
125
|
+
// test pg pool reads `BIGSERIAL` columns (e.g. `audit_log.seq`)
|
|
126
|
+
// as numbers like PGlite does — without this the pg leg of a
|
|
127
|
+
// `.db.test.ts` sees `seq` as a string and wire-schema parses fail.
|
|
128
|
+
await register_pg_type_parsers();
|
|
124
129
|
const pool = new Pool({ connectionString: test_url });
|
|
125
130
|
pool_ref = pool;
|
|
126
131
|
const { db } = create_pg_db(pool);
|
|
@@ -13,7 +13,7 @@ import { Hono } from 'hono';
|
|
|
13
13
|
import { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
14
14
|
import { create_bearer_auth_middleware } from '../auth/bearer_auth.js';
|
|
15
15
|
import { query_validate_api_token } from '../auth/api_token_queries.js';
|
|
16
|
-
import { query_account_by_id, query_actor_by_id,
|
|
16
|
+
import { query_account_by_id, query_actor_by_id, query_active_actors_by_account, } from '../auth/account_queries.js';
|
|
17
17
|
import { query_role_grant_find_active_for_actor } from '../auth/role_grant_queries.js';
|
|
18
18
|
import { create_proxy_middleware } from '../http/proxy.js';
|
|
19
19
|
import { get_client_ip } from '../http/client_ip.js';
|
|
@@ -29,7 +29,7 @@ vi.mock('../auth/api_token_queries.js', () => ({
|
|
|
29
29
|
vi.mock('../auth/account_queries.js', () => ({
|
|
30
30
|
query_account_by_id: vi.fn(),
|
|
31
31
|
query_actor_by_id: vi.fn(),
|
|
32
|
-
|
|
32
|
+
query_active_actors_by_account: vi.fn(),
|
|
33
33
|
}));
|
|
34
34
|
vi.mock('../auth/role_grant_queries.js', () => ({
|
|
35
35
|
query_role_grant_find_active_for_actor: vi.fn(),
|
|
@@ -52,7 +52,7 @@ export const create_bearer_auth_mocks = (tc) => {
|
|
|
52
52
|
const mock_validate = vi.mocked(query_validate_api_token);
|
|
53
53
|
const mock_find_by_id = vi.mocked(query_account_by_id);
|
|
54
54
|
const mock_find_actor_by_id = vi.mocked(query_actor_by_id);
|
|
55
|
-
const mock_find_actors_by_account = vi.mocked(
|
|
55
|
+
const mock_find_actors_by_account = vi.mocked(query_active_actors_by_account);
|
|
56
56
|
const mock_find_active_for_actor = vi.mocked(query_role_grant_find_active_for_actor);
|
|
57
57
|
mock_validate
|
|
58
58
|
.mockReset()
|
|
@@ -211,7 +211,7 @@ export const create_test_middleware_stack_app = (options) => {
|
|
|
211
211
|
const mock_validate = vi.mocked(query_validate_api_token);
|
|
212
212
|
const mock_find_by_id = vi.mocked(query_account_by_id);
|
|
213
213
|
const mock_find_actor_by_id = vi.mocked(query_actor_by_id);
|
|
214
|
-
const mock_find_actors_by_account = vi.mocked(
|
|
214
|
+
const mock_find_actors_by_account = vi.mocked(query_active_actors_by_account);
|
|
215
215
|
const mock_find_active_for_actor = vi.mocked(query_role_grant_find_active_for_actor);
|
|
216
216
|
mock_validate.mockReset().mockImplementation(() => Promise.resolve(undefined));
|
|
217
217
|
mock_find_by_id.mockReset().mockImplementation(() => Promise.resolve(undefined));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/fuz_app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.96.0",
|
|
4
4
|
"description": "fullstack app library",
|
|
5
5
|
"glyph": "🗝",
|
|
6
6
|
"logo": "logo.svg",
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"deploy": "gro deploy"
|
|
24
24
|
},
|
|
25
25
|
"type": "module",
|
|
26
|
+
"imports": {
|
|
27
|
+
"#lib/*": "./src/lib/*",
|
|
28
|
+
"#routes/*": "./src/routes/*"
|
|
29
|
+
},
|
|
26
30
|
"engines": {
|
|
27
31
|
"node": ">=24.14"
|
|
28
32
|
},
|
|
@@ -79,7 +83,7 @@
|
|
|
79
83
|
"@hono/node-server": "^1.19.14",
|
|
80
84
|
"@hono/node-ws": "^1.3.1",
|
|
81
85
|
"@node-rs/argon2": "^2.0.2",
|
|
82
|
-
"@ryanatkn/eslint-config": "^0.12.
|
|
86
|
+
"@ryanatkn/eslint-config": "^0.12.2",
|
|
83
87
|
"@sveltejs/acorn-typescript": "^1.0.9",
|
|
84
88
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
85
89
|
"@sveltejs/kit": "^2.63.0",
|