@fuzdev/fuz_app 0.141.0 → 0.143.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/CLAUDE.md +10 -5
- package/dist/actions/connection_closer.d.ts +19 -2
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +25 -2
- package/dist/actions/perform_action.d.ts +10 -0
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +23 -9
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +5 -2
- package/dist/auth/CLAUDE.md +33 -13
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +12 -1
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +8 -0
- package/dist/auth/account_routes.d.ts +7 -2
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +240 -187
- package/dist/auth/admin_action_specs.d.ts +6 -1
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +6 -1
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +14 -2
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +85 -76
- package/dist/auth/cell_action_specs.d.ts +7 -3
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +9 -5
- package/dist/auth/cell_actions.d.ts +3 -3
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +67 -38
- package/dist/auth/cell_authorize.d.ts +69 -8
- package/dist/auth/cell_authorize.d.ts.map +1 -1
- package/dist/auth/cell_authorize.js +89 -9
- package/dist/auth/cell_field_action_specs.d.ts +5 -4
- package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_field_action_specs.js +7 -7
- package/dist/auth/cell_field_actions.d.ts +7 -5
- package/dist/auth/cell_field_actions.d.ts.map +1 -1
- package/dist/auth/cell_field_actions.js +24 -22
- package/dist/auth/cell_grant_action_specs.d.ts +6 -0
- package/dist/auth/cell_grant_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_grant_action_specs.js +6 -0
- package/dist/auth/cell_grant_actions.d.ts +3 -1
- package/dist/auth/cell_grant_actions.d.ts.map +1 -1
- package/dist/auth/cell_grant_actions.js +16 -2
- package/dist/auth/cell_item_action_specs.d.ts +5 -4
- package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_item_action_specs.js +7 -7
- package/dist/auth/cell_item_actions.d.ts +6 -5
- package/dist/auth/cell_item_actions.d.ts.map +1 -1
- package/dist/auth/cell_item_actions.js +23 -22
- package/dist/auth/cell_relation_list_limit.d.ts +20 -0
- package/dist/auth/cell_relation_list_limit.d.ts.map +1 -0
- package/dist/auth/cell_relation_list_limit.js +27 -0
- package/dist/auth/cell_relation_visibility.d.ts +4 -3
- package/dist/auth/cell_relation_visibility.d.ts.map +1 -1
- package/dist/auth/cell_relation_visibility.js +6 -5
- package/dist/auth/cell_steward.d.ts +45 -0
- package/dist/auth/cell_steward.d.ts.map +1 -0
- package/dist/auth/cell_steward.js +53 -0
- package/dist/auth/request_context.d.ts +4 -0
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +6 -1
- package/dist/auth/role_grant_offer_action_specs.d.ts +5 -0
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +6 -1
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +23 -16
- package/dist/auth/role_grant_offer_queries.d.ts +8 -6
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +16 -8
- package/dist/auth/signup_routes.d.ts +4 -3
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +157 -134
- package/dist/auth/token_scope.d.ts +7 -1
- package/dist/auth/token_scope.d.ts.map +1 -1
- package/dist/auth/token_scope.js +8 -2
- package/dist/db/CLAUDE.md +32 -19
- package/dist/db/cell_ddl.d.ts +1 -2
- package/dist/db/cell_ddl.d.ts.map +1 -1
- package/dist/db/cell_ddl.js +1 -2
- package/dist/db/cell_field_queries.d.ts +20 -18
- package/dist/db/cell_field_queries.d.ts.map +1 -1
- package/dist/db/cell_field_queries.js +28 -22
- package/dist/db/cell_item_queries.d.ts +20 -14
- package/dist/db/cell_item_queries.d.ts.map +1 -1
- package/dist/db/cell_item_queries.js +28 -18
- package/dist/db/cell_moderation.d.ts +67 -0
- package/dist/db/cell_moderation.d.ts.map +1 -0
- package/dist/db/cell_moderation.js +66 -0
- package/dist/db/cell_queries.d.ts +44 -14
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +68 -45
- package/dist/db/cell_view_sql.d.ts +102 -0
- package/dist/db/cell_view_sql.d.ts.map +1 -0
- package/dist/db/cell_view_sql.js +112 -0
- package/dist/db/db_pg.d.ts +1 -1
- package/dist/db/db_pg.js +1 -1
- package/dist/http/CLAUDE.md +1 -1
- package/dist/http/auth_shape.d.ts +5 -2
- package/dist/http/auth_shape.d.ts.map +1 -1
- package/dist/http/auth_shape.js +5 -2
- package/dist/http/ip_canonical.d.ts +56 -5
- package/dist/http/ip_canonical.d.ts.map +1 -1
- package/dist/http/ip_canonical.js +147 -35
- package/dist/primitive_schemas.d.ts +10 -1
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +11 -2
- package/dist/rate_limiter.d.ts +179 -12
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +352 -42
- package/dist/server/serve_fact_route.d.ts +8 -4
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +15 -6
- package/dist/testing/CLAUDE.md +16 -7
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +37 -2
- package/dist/testing/cross_backend/cell_gated_create.d.ts +38 -0
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +401 -5
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +145 -2
- package/dist/testing/cross_backend/expected_schema.json +0 -1
- package/dist/testing/cross_backend/full_spine_mount.js +2 -1
- package/dist/testing/cross_backend/login_security.d.ts +16 -0
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +154 -0
- package/dist/testing/cross_backend/origin_conformance.d.ts +1 -1
- package/dist/testing/cross_backend/origin_conformance.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin_conformance.js +1 -1
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
- package/dist/testing/cross_backend/query_shape.js +24 -2
- package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_participation.js +64 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +13 -21
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +13 -21
- package/dist/testing/cross_backend/testing_origin_check.d.ts +128 -0
- package/dist/testing/cross_backend/testing_origin_check.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_origin_check.js +87 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +7 -119
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +8 -92
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_action_rate_limit.js +21 -1
- package/dist/testing/schema_introspect.js +1 -1
- package/dist/testing/schema_parity.d.ts +4 -3
- package/dist/testing/schema_parity.d.ts.map +1 -1
- package/dist/testing/schema_parity.js +4 -3
- package/dist/testing/schema_ready_fixture.d.ts +2 -4
- package/dist/testing/schema_ready_fixture.d.ts.map +1 -1
- package/dist/testing/schema_ready_fixture.js +2 -4
- package/dist/ui/auth_state.svelte.d.ts +10 -0
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +22 -4
- package/package.json +1 -1
- package/dist/db/cell_history_ddl.d.ts +0 -38
- package/dist/db/cell_history_ddl.d.ts.map +0 -1
- package/dist/db/cell_history_ddl.js +0 -61
package/dist/db/cell_queries.js
CHANGED
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { fact_hash_extract_refs } from '@fuzdev/fuz_util/fact_hash.ts';
|
|
24
24
|
import { assert_row } from './assert_row.js';
|
|
25
|
-
import { iso8601_timestamp_expr, qualify_columns } from './sql_columns.js';
|
|
25
|
+
import { columns_sql, iso8601_timestamp_expr, qualify_columns } from './sql_columns.js';
|
|
26
|
+
import { cell_moderation_admits_audience_sql, cell_moderation_is_stewardable_sql } from './cell_moderation.js';
|
|
27
|
+
import { can_view_cell_sql, caller_role_grants_cte, cell_grant_admits_caller_sql } from './cell_view_sql.js';
|
|
26
28
|
/**
|
|
27
29
|
* The full `cell` column set, named explicitly so a row read fails loud on
|
|
28
30
|
* schema drift — a `SELECT *` silently omits a dropped column and the
|
|
@@ -69,6 +71,18 @@ const cell_expr = iso8601_timestamp_expr(CELL_COLUMNS, ['created_at', 'updated_a
|
|
|
69
71
|
* actually produces one.
|
|
70
72
|
*/
|
|
71
73
|
export const cell_row_projection = (alias) => `${qualify_columns(CELL_COLUMNS, alias, cell_expr(alias))}, (SELECT COUNT(*)::int FROM cell_grant WHERE cell_id = ${alias}.id) AS grant_count`;
|
|
74
|
+
/**
|
|
75
|
+
* The columns a view check reads (`can_view_cell`) — the `CELL_COLUMNS`
|
|
76
|
+
* subset `query_cell_view_load_many` projects, typed against `CELL_COLUMNS`
|
|
77
|
+
* so the base const's drift guard covers it. Twin of `fuz_cell`'s
|
|
78
|
+
* `CELL_VIEW_COLUMNS`.
|
|
79
|
+
*/
|
|
80
|
+
export const CELL_VIEW_COLUMNS = [
|
|
81
|
+
'id',
|
|
82
|
+
'visibility',
|
|
83
|
+
'moderation',
|
|
84
|
+
'created_by'
|
|
85
|
+
];
|
|
72
86
|
/**
|
|
73
87
|
* Insert a cell row, deriving `refs` from `data`.
|
|
74
88
|
*
|
|
@@ -133,11 +147,10 @@ export const query_cell_get_by_path = async (deps, path) => {
|
|
|
133
147
|
return row ?? null;
|
|
134
148
|
};
|
|
135
149
|
/**
|
|
136
|
-
* Bulk-load active cell rows by id, **no visibility filter applied
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* to tombstones never surface.
|
|
150
|
+
* Bulk-load active cell rows by id, **no visibility filter applied** — the
|
|
151
|
+
* full rows, for a caller that copies or returns them (the deep-clone walk).
|
|
152
|
+
* A caller that needs only the view check reads `query_cell_view_load_many`.
|
|
153
|
+
* Soft-deleted rows are excluded so relations to tombstones never surface.
|
|
141
154
|
*
|
|
142
155
|
* @param deps - query deps
|
|
143
156
|
* @param ids - cell ids to load (duplicates are harmless)
|
|
@@ -150,6 +163,24 @@ export const query_cell_load_many = async (deps, ids) => {
|
|
|
150
163
|
FROM cell
|
|
151
164
|
WHERE id = ANY($1::uuid[]) AND deleted_at IS NULL`, [ids]);
|
|
152
165
|
};
|
|
166
|
+
/**
|
|
167
|
+
* Bulk-load the active cells among `ids` for a view check — the
|
|
168
|
+
* `CELL_VIEW_COLUMNS` alone, never the payload. Backs the strict
|
|
169
|
+
* relation-read filter (`auth/cell_relation_visibility.ts`'s
|
|
170
|
+
* `filter_visible_target_ids`), which runs `can_view_cell` per row in memory
|
|
171
|
+
* rather than in SQL. Soft-deleted rows and ids with no row are absent.
|
|
172
|
+
*
|
|
173
|
+
* @param deps - query deps
|
|
174
|
+
* @param ids - cell ids to load (duplicates are harmless)
|
|
175
|
+
* @returns active rows in arbitrary order (caller indexes by `id`)
|
|
176
|
+
*/
|
|
177
|
+
export const query_cell_view_load_many = async (deps, ids) => {
|
|
178
|
+
if (ids.length === 0)
|
|
179
|
+
return [];
|
|
180
|
+
return deps.db.query(`SELECT ${columns_sql(CELL_VIEW_COLUMNS)}
|
|
181
|
+
FROM cell
|
|
182
|
+
WHERE id = ANY($1::uuid[]) AND deleted_at IS NULL`, [ids]);
|
|
183
|
+
};
|
|
153
184
|
/**
|
|
154
185
|
* Update a cell. Fields left `undefined` in the patch keep their existing
|
|
155
186
|
* value; explicit `null` writes `NULL`. `refs` is re-derived from `data`
|
|
@@ -274,15 +305,24 @@ export const query_cell_list_by_creator = async (deps, actor_id, options) => dep
|
|
|
274
305
|
* dimension — keep it simple) plus an optional viewer-aware visibility
|
|
275
306
|
* predicate.
|
|
276
307
|
*
|
|
277
|
-
* The visibility predicate mirrors `can_view_cell` in SQL
|
|
308
|
+
* The visibility predicate mirrors `can_view_cell || can_steward_cell` in SQL
|
|
309
|
+
* form:
|
|
278
310
|
*
|
|
279
311
|
* ```
|
|
280
312
|
* (viewer_is_admin
|
|
281
|
-
* OR cell.visibility = 'public'
|
|
282
313
|
* OR (viewer_actor_id IS NOT NULL AND created_by = viewer_actor_id)
|
|
283
|
-
* OR (
|
|
314
|
+
* OR (<moderation admits an audience> AND (
|
|
315
|
+
* cell.visibility = 'public'
|
|
316
|
+
* OR (viewer_actor_id IS NOT NULL AND <grant admits caller>)))
|
|
317
|
+
* OR (viewer_actor_id IS NOT NULL AND <pending or rejected contribution>
|
|
318
|
+
* AND <viewer owns its live governing root>))
|
|
284
319
|
* ```
|
|
285
320
|
*
|
|
321
|
+
* The moderation rules are `cell_moderation_admits_audience_sql` and
|
|
322
|
+
* `cell_moderation_is_stewardable_sql` (`db/cell_moderation.ts`). The last arm
|
|
323
|
+
* is the steward arm (`auth/cell_steward.ts`) — a root owner reads the
|
|
324
|
+
* moderation queue under their root; a global admin is already admitted.
|
|
325
|
+
*
|
|
286
326
|
* The grants branch closes parity with `can_view_cell`: a SQL `EXISTS`
|
|
287
327
|
* over `cell_grant`, parameterized by the caller's `actor_id` and the
|
|
288
328
|
* parallel `(role[], scope_id[])` projection of `auth.role_grants`. The
|
|
@@ -304,7 +344,9 @@ export const query_cell_list_by_creator = async (deps, actor_id, options) => dep
|
|
|
304
344
|
* owner-excluding, but the explicit `created_by IS DISTINCT FROM caller`
|
|
305
345
|
* guards against any future deviation. The shared_with branch does NOT
|
|
306
346
|
* bypass for admin: an admin asking "what's shared with me" wants their
|
|
307
|
-
* own grant footprint, not every cell.
|
|
347
|
+
* own grant footprint, not every cell. A grant admits there only while the
|
|
348
|
+
* cell's moderation admits an audience, as in `can_view_cell`, and the steward
|
|
349
|
+
* arm doesn't apply — a grant footprint is not a moderation queue.
|
|
308
350
|
*
|
|
309
351
|
* Soft-deleted rows are excluded by default; opt-in via `include_deleted`.
|
|
310
352
|
*
|
|
@@ -369,36 +411,17 @@ export const query_cell_list = async (deps, params) => {
|
|
|
369
411
|
]);
|
|
370
412
|
};
|
|
371
413
|
/**
|
|
372
|
-
* The `
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* Resolves true when the grant row at `g_alias` admits the caller via
|
|
376
|
-
* either an actor-shaped principal (`g.actor_id = $11`) or a
|
|
377
|
-
* role-shaped principal whose `(role, scope_id)` matches a row in the
|
|
378
|
-
* `caller_role_grants` CTE. NULL `g.scope_id` matches any scope, mirroring
|
|
379
|
-
* `grant_admits` in `auth/cell_authorize.ts`.
|
|
414
|
+
* The caller's `(role, scope_id)` role_grant pairs, materialized once per
|
|
415
|
+
* query so the role-grant predicate doesn't re-scan `unnest()` per outer row.
|
|
416
|
+
* Shared by both SQL shapes.
|
|
380
417
|
*/
|
|
381
|
-
const
|
|
382
|
-
($11::uuid IS NOT NULL AND ${g_alias}.actor_id = $11)
|
|
383
|
-
OR (
|
|
384
|
-
${g_alias}.role IS NOT NULL
|
|
385
|
-
AND EXISTS (
|
|
386
|
-
SELECT 1 FROM caller_role_grants p
|
|
387
|
-
WHERE p.role = ${g_alias}.role
|
|
388
|
-
AND (${g_alias}.scope_id IS NULL OR p.scope_id IS NOT DISTINCT FROM ${g_alias}.scope_id)
|
|
389
|
-
)
|
|
390
|
-
)
|
|
391
|
-
)`;
|
|
418
|
+
const CALLER_ROLE_GRANTS_CTE = `WITH ${caller_role_grants_cte('$12', '$13')}`;
|
|
392
419
|
/**
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
420
|
+
* General `cell_list` SQL: cell-driven scan, full visibility predicate — the
|
|
421
|
+
* `can_view_cell` arms (`can_view_cell_sql`), then the steward arm
|
|
422
|
+
* (`can_steward_cell`: the caller owns the live governing root of a pending or
|
|
423
|
+
* rejected contribution; a global admin is already in through `$6`).
|
|
397
424
|
*/
|
|
398
|
-
const CALLER_ROLE_GRANTS_CTE = `WITH caller_role_grants AS (
|
|
399
|
-
SELECT role, scope_id FROM unnest($12::text[], $13::uuid[]) AS p(role, scope_id)
|
|
400
|
-
)`;
|
|
401
|
-
/** General `cell_list` SQL: cell-driven scan, full visibility predicate. */
|
|
402
425
|
const build_general_sql = (order_column, order_direction) => `${CALLER_ROLE_GRANTS_CTE}
|
|
403
426
|
SELECT ${cell_row_projection('c')} FROM cell c
|
|
404
427
|
WHERE ($1::bool OR c.deleted_at IS NULL)
|
|
@@ -411,13 +434,12 @@ const build_general_sql = (order_column, order_direction) => `${CALLER_ROLE_GRAN
|
|
|
411
434
|
AND ($15::uuid IS NULL OR c.root_id = $15)
|
|
412
435
|
AND ($16::text IS NULL OR c.moderation = $16::text)
|
|
413
436
|
AND (
|
|
414
|
-
$6
|
|
415
|
-
OR c.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
))
|
|
437
|
+
${can_view_cell_sql('c', { is_admin: '$6', actor_id: '$7', grant_actor_id: '$11' })}
|
|
438
|
+
OR ($7::uuid IS NOT NULL AND c.root_id IS NOT NULL AND ${cell_moderation_is_stewardable_sql('c')}
|
|
439
|
+
AND EXISTS (
|
|
440
|
+
SELECT 1 FROM cell r
|
|
441
|
+
WHERE r.id = c.root_id AND r.deleted_at IS NULL AND r.created_by = $7
|
|
442
|
+
))
|
|
421
443
|
)
|
|
422
444
|
ORDER BY c.${order_column} ${order_direction} NULLS LAST
|
|
423
445
|
LIMIT $8 OFFSET $9`;
|
|
@@ -450,9 +472,10 @@ const build_shared_with_sql = (order_column, order_direction) => `${CALLER_ROLE_
|
|
|
450
472
|
AND ($16::text IS NULL OR c.moderation = $16::text)
|
|
451
473
|
AND $7::uuid IS NOT NULL
|
|
452
474
|
AND c.created_by IS DISTINCT FROM $7
|
|
475
|
+
AND ${cell_moderation_admits_audience_sql('c')}
|
|
453
476
|
AND c.id IN (
|
|
454
477
|
SELECT g.cell_id FROM cell_grant g
|
|
455
|
-
WHERE ${
|
|
478
|
+
WHERE ${cell_grant_admits_caller_sql('g', '$11')}
|
|
456
479
|
)
|
|
457
480
|
-- $6 (viewer_is_admin) intentionally not consulted: an admin
|
|
458
481
|
-- asking "what's shared with me" wants their grant footprint, not
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `can_view_cell` in SQL — the view predicate a query applies to a cell row
|
|
3
|
+
* before `LIMIT`, so a page holds only rows the caller may view.
|
|
4
|
+
*
|
|
5
|
+
* `can_view_cell_sql` is the one SQL form of the predicate: `query_cell_list`
|
|
6
|
+
* embeds it (adding the steward arm beside it), and the relation lists apply
|
|
7
|
+
* it to the cell at the other end of each edge. It binds the caller through a
|
|
8
|
+
* `CellViewer` (`auth/cell_authorize.ts`'s `cell_viewer_from_auth` builds one
|
|
9
|
+
* from a request context), and the role-shaped grant arm reads the
|
|
10
|
+
* `caller_role_grants` CTE `caller_role_grants_cte` defines. Twin of the Rust
|
|
11
|
+
* `fuz_cell::cell_view_sql`.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import type { Uuid } from '../primitive_schemas.js';
|
|
16
|
+
/**
|
|
17
|
+
* The caller a view predicate binds — what `can_view_cell` reads off the
|
|
18
|
+
* request context.
|
|
19
|
+
*
|
|
20
|
+
* `role_grant_roles` and `role_grant_scope_ids` are parallel, one entry per
|
|
21
|
+
* active role grant (`null` = a global grant): `unnest` pads the shorter with
|
|
22
|
+
* NULLs, and a NULL scope reads as a global grant, so `cell_viewer_params`
|
|
23
|
+
* refuses a mismatch.
|
|
24
|
+
*/
|
|
25
|
+
export interface CellViewer {
|
|
26
|
+
/** The acting actor; `null` for an anonymous or account-grain caller. */
|
|
27
|
+
readonly actor_id: Uuid | null;
|
|
28
|
+
/** Whether the caller holds the global admin role — the arm that admits every cell. */
|
|
29
|
+
readonly is_admin: boolean;
|
|
30
|
+
/** The roles of the caller's active role grants, parallel to `role_grant_scope_ids`. */
|
|
31
|
+
readonly role_grant_roles: ReadonlyArray<string>;
|
|
32
|
+
/** The scopes of the caller's active role grants, parallel to `role_grant_roles`. */
|
|
33
|
+
readonly role_grant_scope_ids: ReadonlyArray<Uuid | null>;
|
|
34
|
+
}
|
|
35
|
+
/** An anonymous caller — admitted by the public arm alone. */
|
|
36
|
+
export declare const CELL_VIEWER_ANONYMOUS: CellViewer;
|
|
37
|
+
/**
|
|
38
|
+
* A server-side read that filters nothing — the admin arm admits every cell.
|
|
39
|
+
* For a caller that applies its own view check afterwards (the `cell_get`
|
|
40
|
+
* bundle) or acts for the server itself.
|
|
41
|
+
*/
|
|
42
|
+
export declare const CELL_VIEWER_UNRESTRICTED: CellViewer;
|
|
43
|
+
/** Where a query binds the `CellViewer` facts `can_view_cell_sql` reads — each a `$N` placeholder. */
|
|
44
|
+
export interface CellViewerSqlParams {
|
|
45
|
+
/** The global-admin flag (`bool`). */
|
|
46
|
+
readonly is_admin: string;
|
|
47
|
+
/** The acting actor (`uuid`, NULL when anonymous) — the owner arm and the grant arm's guard. */
|
|
48
|
+
readonly actor_id: string;
|
|
49
|
+
/**
|
|
50
|
+
* The actor an actor-shaped grant matches, when a query binds it apart
|
|
51
|
+
* from `actor_id` (`query_cell_list` does). Defaults to `actor_id`.
|
|
52
|
+
*/
|
|
53
|
+
readonly grant_actor_id?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The viewer's bind values, in the order `[is_admin, actor_id, roles,
|
|
57
|
+
* scope_ids]`.
|
|
58
|
+
*
|
|
59
|
+
* @param viewer - the caller
|
|
60
|
+
* @returns the four bind values
|
|
61
|
+
* @throws {Error} when the role-grant arrays aren't parallel
|
|
62
|
+
*/
|
|
63
|
+
export declare const cell_viewer_params: (viewer: CellViewer) => [boolean, Uuid | null, ReadonlyArray<string>, ReadonlyArray<Uuid | null>];
|
|
64
|
+
/**
|
|
65
|
+
* The `caller_role_grants` CTE body (no `WITH`): the caller's
|
|
66
|
+
* `(role, scope_id)` pairs, unnested once per query from the parallel arrays
|
|
67
|
+
* at placeholders `roles` and `scope_ids`. PG 12+ inlines a non-recursive
|
|
68
|
+
* single-use CTE, so this is planner-equivalent to a subquery — the form is
|
|
69
|
+
* for clarity.
|
|
70
|
+
*
|
|
71
|
+
* @param roles - the placeholder of the roles array (`text[]`)
|
|
72
|
+
* @param scope_ids - the placeholder of the scope-id array (`uuid[]`)
|
|
73
|
+
* @returns the CTE body
|
|
74
|
+
*/
|
|
75
|
+
export declare const caller_role_grants_cte: (roles: string, scope_ids: string) => string;
|
|
76
|
+
/**
|
|
77
|
+
* The `cell_grant` admits-caller predicate over grant alias `g_alias`: an
|
|
78
|
+
* actor-shaped grant matches the actor at placeholder `actor_id`; a
|
|
79
|
+
* role-shaped grant matches a `(role, scope_id)` pair in the
|
|
80
|
+
* `caller_role_grants` CTE (a NULL grant `scope_id` matches any scope).
|
|
81
|
+
* Mirrors `grant_admits` in `auth/cell_authorize.ts`.
|
|
82
|
+
*
|
|
83
|
+
* @param g_alias - the `cell_grant` alias in the enclosing query
|
|
84
|
+
* @param actor_id - the placeholder of the caller's actor (`uuid`)
|
|
85
|
+
* @returns the SQL predicate
|
|
86
|
+
*/
|
|
87
|
+
export declare const cell_grant_admits_caller_sql: (g_alias: string, actor_id: string) => string;
|
|
88
|
+
/**
|
|
89
|
+
* `can_view_cell` over the cell alias `c_alias`: admin, the owner, or — while
|
|
90
|
+
* the cell's `moderation` admits an audience — `public` or a `cell_grant`
|
|
91
|
+
* admitting the caller.
|
|
92
|
+
*
|
|
93
|
+
* Reads the caller at `params`; the grant arm needs the
|
|
94
|
+
* `caller_role_grants_cte` in scope. Liveness (`deleted_at IS NULL`) is the
|
|
95
|
+
* query's own clause, as `can_view_cell` reads a loaded live row.
|
|
96
|
+
*
|
|
97
|
+
* @param c_alias - the `cell` alias in the enclosing query
|
|
98
|
+
* @param params - where the query binds the caller
|
|
99
|
+
* @returns the SQL predicate (parenthesized)
|
|
100
|
+
*/
|
|
101
|
+
export declare const can_view_cell_sql: (c_alias: string, params: CellViewerSqlParams) => string;
|
|
102
|
+
//# sourceMappingURL=cell_view_sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_view_sql.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_view_sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAGpD;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IAC1B,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IAC/B,uFAAuF;IACvF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,wFAAwF;IACxF,QAAQ,CAAC,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACjD,qFAAqF;IACrF,QAAQ,CAAC,oBAAoB,EAAE,aAAa,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,8DAA8D;AAC9D,eAAO,MAAM,qBAAqB,EAAE,UAKnC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,UAKtC,CAAC;AAEF,sGAAsG;AACtG,MAAM,WAAW,mBAAmB;IACnC,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,gGAAgG;IAChG,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAC9B,QAAQ,UAAU,KAChB,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC,IAAI,GAAG,IAAI,CAAC,CAQ1E,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,MAAM,EAAE,WAAW,MAAM,KAAG,MAGtE,CAAC;AAEL;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,MAU3E,CAAC;AAEP;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,GAAI,SAAS,MAAM,EAAE,QAAQ,mBAAmB,KAAG,MAchF,CAAC"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `can_view_cell` in SQL — the view predicate a query applies to a cell row
|
|
3
|
+
* before `LIMIT`, so a page holds only rows the caller may view.
|
|
4
|
+
*
|
|
5
|
+
* `can_view_cell_sql` is the one SQL form of the predicate: `query_cell_list`
|
|
6
|
+
* embeds it (adding the steward arm beside it), and the relation lists apply
|
|
7
|
+
* it to the cell at the other end of each edge. It binds the caller through a
|
|
8
|
+
* `CellViewer` (`auth/cell_authorize.ts`'s `cell_viewer_from_auth` builds one
|
|
9
|
+
* from a request context), and the role-shaped grant arm reads the
|
|
10
|
+
* `caller_role_grants` CTE `caller_role_grants_cte` defines. Twin of the Rust
|
|
11
|
+
* `fuz_cell::cell_view_sql`.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { cell_moderation_admits_audience_sql } from './cell_moderation.js';
|
|
16
|
+
/** An anonymous caller — admitted by the public arm alone. */
|
|
17
|
+
export const CELL_VIEWER_ANONYMOUS = {
|
|
18
|
+
actor_id: null,
|
|
19
|
+
is_admin: false,
|
|
20
|
+
role_grant_roles: [],
|
|
21
|
+
role_grant_scope_ids: []
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A server-side read that filters nothing — the admin arm admits every cell.
|
|
25
|
+
* For a caller that applies its own view check afterwards (the `cell_get`
|
|
26
|
+
* bundle) or acts for the server itself.
|
|
27
|
+
*/
|
|
28
|
+
export const CELL_VIEWER_UNRESTRICTED = {
|
|
29
|
+
actor_id: null,
|
|
30
|
+
is_admin: true,
|
|
31
|
+
role_grant_roles: [],
|
|
32
|
+
role_grant_scope_ids: []
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The viewer's bind values, in the order `[is_admin, actor_id, roles,
|
|
36
|
+
* scope_ids]`.
|
|
37
|
+
*
|
|
38
|
+
* @param viewer - the caller
|
|
39
|
+
* @returns the four bind values
|
|
40
|
+
* @throws {Error} when the role-grant arrays aren't parallel
|
|
41
|
+
*/
|
|
42
|
+
export const cell_viewer_params = (viewer) => {
|
|
43
|
+
if (viewer.role_grant_roles.length !== viewer.role_grant_scope_ids.length) {
|
|
44
|
+
throw new Error(`CellViewer: role_grant_roles (len=${viewer.role_grant_roles.length}) and ` +
|
|
45
|
+
`role_grant_scope_ids (len=${viewer.role_grant_scope_ids.length}) must be parallel arrays`);
|
|
46
|
+
}
|
|
47
|
+
return [viewer.is_admin, viewer.actor_id, viewer.role_grant_roles, viewer.role_grant_scope_ids];
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The `caller_role_grants` CTE body (no `WITH`): the caller's
|
|
51
|
+
* `(role, scope_id)` pairs, unnested once per query from the parallel arrays
|
|
52
|
+
* at placeholders `roles` and `scope_ids`. PG 12+ inlines a non-recursive
|
|
53
|
+
* single-use CTE, so this is planner-equivalent to a subquery — the form is
|
|
54
|
+
* for clarity.
|
|
55
|
+
*
|
|
56
|
+
* @param roles - the placeholder of the roles array (`text[]`)
|
|
57
|
+
* @param scope_ids - the placeholder of the scope-id array (`uuid[]`)
|
|
58
|
+
* @returns the CTE body
|
|
59
|
+
*/
|
|
60
|
+
export const caller_role_grants_cte = (roles, scope_ids) => `caller_role_grants AS (
|
|
61
|
+
SELECT role, scope_id FROM unnest(${roles}::text[], ${scope_ids}::uuid[]) AS p(role, scope_id)
|
|
62
|
+
)`;
|
|
63
|
+
/**
|
|
64
|
+
* The `cell_grant` admits-caller predicate over grant alias `g_alias`: an
|
|
65
|
+
* actor-shaped grant matches the actor at placeholder `actor_id`; a
|
|
66
|
+
* role-shaped grant matches a `(role, scope_id)` pair in the
|
|
67
|
+
* `caller_role_grants` CTE (a NULL grant `scope_id` matches any scope).
|
|
68
|
+
* Mirrors `grant_admits` in `auth/cell_authorize.ts`.
|
|
69
|
+
*
|
|
70
|
+
* @param g_alias - the `cell_grant` alias in the enclosing query
|
|
71
|
+
* @param actor_id - the placeholder of the caller's actor (`uuid`)
|
|
72
|
+
* @returns the SQL predicate
|
|
73
|
+
*/
|
|
74
|
+
export const cell_grant_admits_caller_sql = (g_alias, actor_id) => `(
|
|
75
|
+
(${actor_id}::uuid IS NOT NULL AND ${g_alias}.actor_id = ${actor_id})
|
|
76
|
+
OR (
|
|
77
|
+
${g_alias}.role IS NOT NULL
|
|
78
|
+
AND EXISTS (
|
|
79
|
+
SELECT 1 FROM caller_role_grants p
|
|
80
|
+
WHERE p.role = ${g_alias}.role
|
|
81
|
+
AND (${g_alias}.scope_id IS NULL OR p.scope_id IS NOT DISTINCT FROM ${g_alias}.scope_id)
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
)`;
|
|
85
|
+
/**
|
|
86
|
+
* `can_view_cell` over the cell alias `c_alias`: admin, the owner, or — while
|
|
87
|
+
* the cell's `moderation` admits an audience — `public` or a `cell_grant`
|
|
88
|
+
* admitting the caller.
|
|
89
|
+
*
|
|
90
|
+
* Reads the caller at `params`; the grant arm needs the
|
|
91
|
+
* `caller_role_grants_cte` in scope. Liveness (`deleted_at IS NULL`) is the
|
|
92
|
+
* query's own clause, as `can_view_cell` reads a loaded live row.
|
|
93
|
+
*
|
|
94
|
+
* @param c_alias - the `cell` alias in the enclosing query
|
|
95
|
+
* @param params - where the query binds the caller
|
|
96
|
+
* @returns the SQL predicate (parenthesized)
|
|
97
|
+
*/
|
|
98
|
+
export const can_view_cell_sql = (c_alias, params) => {
|
|
99
|
+
const { is_admin, actor_id } = params;
|
|
100
|
+
const grant_actor_id = params.grant_actor_id ?? actor_id;
|
|
101
|
+
return `(
|
|
102
|
+
${is_admin}::bool
|
|
103
|
+
OR (${actor_id}::uuid IS NOT NULL AND ${c_alias}.created_by = ${actor_id})
|
|
104
|
+
OR (${cell_moderation_admits_audience_sql(c_alias)} AND (
|
|
105
|
+
${c_alias}.visibility = 'public'
|
|
106
|
+
OR (${actor_id}::uuid IS NOT NULL AND EXISTS (
|
|
107
|
+
SELECT 1 FROM cell_grant g
|
|
108
|
+
WHERE g.cell_id = ${c_alias}.id AND ${cell_grant_admits_caller_sql('g', grant_actor_id)}
|
|
109
|
+
))
|
|
110
|
+
))
|
|
111
|
+
)`;
|
|
112
|
+
};
|
package/dist/db/db_pg.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { type DbDriverResult } from './db.js';
|
|
|
18
18
|
* call this, don't need `pg` installed) and coerces int8 (`BIGINT`, OID 20)
|
|
19
19
|
* to a JS number. pg defaults to returning
|
|
20
20
|
* int8 as a string to avoid 2^53 precision loss; our int8 columns today
|
|
21
|
-
* (`audit_log.seq`, `
|
|
21
|
+
* (`audit_log.seq`, `fact.size`) stay well under that
|
|
22
22
|
* bound, and reading as a number keeps the wire shape uniform with PGlite —
|
|
23
23
|
* which returns int8 as a number — so `AuditLogEvent.seq` and friends
|
|
24
24
|
* validate identically across both drivers.
|
package/dist/db/db_pg.js
CHANGED
|
@@ -17,7 +17,7 @@ import { Db, no_nested_transaction } from './db.js';
|
|
|
17
17
|
* call this, don't need `pg` installed) and coerces int8 (`BIGINT`, OID 20)
|
|
18
18
|
* to a JS number. pg defaults to returning
|
|
19
19
|
* int8 as a string to avoid 2^53 precision loss; our int8 columns today
|
|
20
|
-
* (`audit_log.seq`, `
|
|
20
|
+
* (`audit_log.seq`, `fact.size`) stay well under that
|
|
21
21
|
* bound, and reading as a number keeps the wire shape uniform with PGlite —
|
|
22
22
|
* which returns int8 as a number — so `AuditLogEvent.seq` and friends
|
|
23
23
|
* validate identically across both drivers.
|
package/dist/http/CLAUDE.md
CHANGED
|
@@ -24,7 +24,7 @@ effects, see ../../../docs/architecture.md.
|
|
|
24
24
|
- `http/surface_query.ts` — pure filters/groupings over `AppSurface`.
|
|
25
25
|
- `http/proxy.ts` — trusted-proxy middleware, CIDR parsing, rightmost-first XFF resolution. Pulls `hono/utils/ipaddr` for CIDR matching.
|
|
26
26
|
- `http/client_ip.ts` — `get_client_ip(c)` — reads the resolved IP off the context (set by `proxy.ts`'s middleware). Split out of `proxy.ts` so it's hono-free: dispatch + route modules that only _read_ the IP don't pull `hono/utils/ipaddr`, keeping cross-process test surfaces free of the optional `hono` peer.
|
|
27
|
-
- `http/ip_canonical.ts` — RFC 5952 IPv6 canonicalization + IPv4-mapped collapse; `IP_LITERAL_CHARS` regex.
|
|
27
|
+
- `http/ip_canonical.ts` — RFC 5952 IPv6 canonicalization + IPv4-mapped collapse, `rate_limit_ip_key` (an IPv6 client's `/64`), and its own `parse_ipv4_bits` / `parse_ipv6_bits` (the grammar of Rust's `IpAddr::from_str`); `IP_LITERAL_CHARS` regex. Imports nothing — the dispatcher keys its IP limiter through it, so it must stay hono-free (`src/test/hono_free_modules.test.ts` walks every `src/lib` module's graph).
|
|
28
28
|
- `http/origin.ts` — origin allowlist: the strict pattern grammar shared with the Rust spine, typed refusals, and the Origin-only middleware.
|
|
29
29
|
- `http/jsonrpc.ts` — JSON-RPC 2.0 envelope schemas (MCP superset), `JsonrpcErrorCode`, `_meta`.
|
|
30
30
|
- `http/jsonrpc_errors.ts` — `ThrownJsonrpcError`, `jsonrpc_errors` throwers, HTTP-status mappings.
|
|
@@ -68,9 +68,12 @@ export type ActingActor = z.infer<typeof ActingActor>;
|
|
|
68
68
|
* instead would put an input-shape complaint ahead of the authority gates,
|
|
69
69
|
* which is the ordering this pipeline exists to avoid.
|
|
70
70
|
*
|
|
71
|
-
*
|
|
71
|
+
* The dispatchers pass through here so that rule has one home: the action
|
|
72
72
|
* dispatcher's `read_acting` and the REST authorization handler's
|
|
73
|
-
* `read_route_acting`. Twin of the Rust dispatcher's `read_acting`.
|
|
73
|
+
* `read_route_acting`. Twin of the Rust dispatcher's `read_acting`. The status
|
|
74
|
+
* route and the WS upgrade read the raw `?acting=` through it too; neither has
|
|
75
|
+
* an input schema behind it, so there a malformed selector is final (the
|
|
76
|
+
* status route answers account-only).
|
|
74
77
|
*
|
|
75
78
|
* @param value - the raw `acting` field, from params / query / body
|
|
76
79
|
* @returns the actor id, or `undefined` when absent or malformed
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth_shape.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/auth_shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,WAAW,6DAGtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD
|
|
1
|
+
{"version":3,"file":"auth_shape.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/auth_shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,WAAW,6DAGtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,SAGtD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa;;;;EAA2C,CAAC;AACtE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;kBAyEnB,CAAC;AACJ,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAUlD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,KAAG,OACA,CAAC;AAElD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,SAAS,KAAG,OAAgC,CAAC;AAE/E;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,SAAS,KAAG,OAAkC,CAAC;AAEnF,wEAAwE;AACxE,eAAO,MAAM,YAAY,GAAI,MAAM,SAAS,KAAG,OAA+B,CAAC;AAE/E,8FAA8F;AAC9F,eAAO,MAAM,wBAAwB,GAAI,MAAM,SAAS,KAAG,OAC3B,CAAC;AAEjC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,KAAG,OACQ,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,SAAS,KAAG,OACwB,CAAC;AAYvF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAIhE,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sCAAsC,GAClD,MAAM,SAAS,EACf,OAAO,WAAW,EAClB,SAAS,MAAM,KACb,IAmBF,CAAC"}
|
package/dist/http/auth_shape.js
CHANGED
|
@@ -71,9 +71,12 @@ export const ActingActor = Uuid.optional().meta({
|
|
|
71
71
|
* instead would put an input-shape complaint ahead of the authority gates,
|
|
72
72
|
* which is the ordering this pipeline exists to avoid.
|
|
73
73
|
*
|
|
74
|
-
*
|
|
74
|
+
* The dispatchers pass through here so that rule has one home: the action
|
|
75
75
|
* dispatcher's `read_acting` and the REST authorization handler's
|
|
76
|
-
* `read_route_acting`. Twin of the Rust dispatcher's `read_acting`.
|
|
76
|
+
* `read_route_acting`. Twin of the Rust dispatcher's `read_acting`. The status
|
|
77
|
+
* route and the WS upgrade read the raw `?acting=` through it too; neither has
|
|
78
|
+
* an input schema behind it, so there a malformed selector is final (the
|
|
79
|
+
* status route answers account-only).
|
|
77
80
|
*
|
|
78
81
|
* @param value - the raw `acting` field, from params / query / body
|
|
79
82
|
* @returns the actor id, or `undefined` when absent or malformed
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* strings (`'unknown'`, `'attacker:controlled'`, `'::1\n'`) pass
|
|
19
19
|
* through unchanged so downstream strict validators can still
|
|
20
20
|
* reject them.
|
|
21
|
-
* 2. Parses
|
|
21
|
+
* 2. Parses with `parse_ipv4_bits` / `parse_ipv6_bits` — the grammar of
|
|
22
|
+
* Rust's `IpAddr::from_str`, which the spine parses with.
|
|
22
23
|
* 3. Re-emits the canonical RFC 5952 string (lowercase hex,
|
|
23
24
|
* longest-zero-run compressed, IPv4-mapped emitted in the dotted
|
|
24
25
|
* form mandated by RFC 5952 §5).
|
|
@@ -27,12 +28,41 @@
|
|
|
27
28
|
* canonicalization because the dotted form is the only form the
|
|
28
29
|
* strip can recognize symmetrically.
|
|
29
30
|
*
|
|
30
|
-
* Mirrors `
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* Mirrors the Rust spine's `fuz_http::normalize_ip`, which uses the same
|
|
32
|
+
* parse-then-canonicalize-then-strip ordering for the same
|
|
33
|
+
* rate-limit-key-poisoning surface.
|
|
34
|
+
*
|
|
35
|
+
* `rate_limit_ip_key` is the bucket key an IP-keyed rate limiter uses: an
|
|
36
|
+
* IPv6 address collapses to its `/64`, the smallest block a host is commonly
|
|
37
|
+
* assigned, so rotating through one's own subnet buys no fresh budget. Only
|
|
38
|
+
* the limiter call sites apply it — the client IP stays the full address
|
|
39
|
+
* everywhere else (audit rows, token `last_used_ip`, `REMOTE_ADDR`).
|
|
40
|
+
*
|
|
41
|
+
* The module imports nothing: the action dispatcher keys its IP limiter
|
|
42
|
+
* through it, so it must not pull the optional `hono` peer.
|
|
33
43
|
*
|
|
34
44
|
* @module
|
|
35
45
|
*/
|
|
46
|
+
/**
|
|
47
|
+
* Parse a dotted-quad IPv4 address to its 32-bit value.
|
|
48
|
+
*
|
|
49
|
+
* Four decimal octets, each 0–255 with no leading zeros (`01.2.3.4` is
|
|
50
|
+
* refused, as Rust's `Ipv4Addr::from_str` refuses it).
|
|
51
|
+
*
|
|
52
|
+
* @returns the address as an unsigned 32-bit number, or `null` when `ip` isn't one
|
|
53
|
+
*/
|
|
54
|
+
export declare const parse_ipv4_bits: (ip: string) => number | null;
|
|
55
|
+
/**
|
|
56
|
+
* Parse an IPv6 address to its 128-bit value.
|
|
57
|
+
*
|
|
58
|
+
* The grammar of Rust's `Ipv6Addr::from_str`: eight groups of one to four
|
|
59
|
+
* hex digits (either case), or fewer around a single `::` standing for one or
|
|
60
|
+
* more zero groups, the last two groups optionally written as a dotted-quad
|
|
61
|
+
* IPv4 address. No zone (`%eth0`) — callers strip it first.
|
|
62
|
+
*
|
|
63
|
+
* @returns the address as a `bigint` in `[0, 2^128)`, or `null` when `ip` isn't one
|
|
64
|
+
*/
|
|
65
|
+
export declare const parse_ipv6_bits: (ip: string) => bigint | null;
|
|
36
66
|
/**
|
|
37
67
|
* Allowed character set for a bare IP literal.
|
|
38
68
|
*
|
|
@@ -89,7 +119,7 @@ export declare const canonicalize_ip: (ip: string) => string;
|
|
|
89
119
|
*
|
|
90
120
|
* Pure helper exported for the test suite to exercise the
|
|
91
121
|
* canonicalization invariants directly without a full
|
|
92
|
-
* `
|
|
122
|
+
* `parse_ipv6_bits` round-trip.
|
|
93
123
|
*
|
|
94
124
|
* @param bits - the 128-bit IPv6 value as `bigint`. Must satisfy `0n <= bits < 2n ** 128n`;
|
|
95
125
|
* throws `RangeError` otherwise. Silent truncation would mask caller bugs since the
|
|
@@ -97,4 +127,25 @@ export declare const canonicalize_ip: (ip: string) => string;
|
|
|
97
127
|
* @throws {RangeError} when `bits` is negative or exceeds 128 bits
|
|
98
128
|
*/
|
|
99
129
|
export declare const ipv6_bigint_to_canonical: (bits: bigint) => string;
|
|
130
|
+
/**
|
|
131
|
+
* The bucket key an IP-keyed rate limiter uses for client IP `ip`.
|
|
132
|
+
*
|
|
133
|
+
* An IPv6 address keys its `/64`: the low 64 bits masked off, rendered in
|
|
134
|
+
* RFC 5952 canonical form with a `/64` suffix (`2001:db8:1:2::/64`; the
|
|
135
|
+
* all-zero prefix is `::/64`). A zone suffix (`%eth0`) is dropped first. An
|
|
136
|
+
* IPv4 address — an IPv4-mapped IPv6 one included — keys its canonical form,
|
|
137
|
+
* with no suffix. Anything that doesn't parse (`'unknown'` among them) is
|
|
138
|
+
* returned unchanged. Twin of the Rust spine's `fuz_http::rate_limit_ip_key`.
|
|
139
|
+
*
|
|
140
|
+
* For limiter keys only: audit rows and every other record of the client
|
|
141
|
+
* keep the full address.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* rate_limit_ip_key('2001:db8:1:2::1') // → '2001:db8:1:2::/64'
|
|
145
|
+
* rate_limit_ip_key('2001:db8:1:2:8000::') // → '2001:db8:1:2::/64'
|
|
146
|
+
* rate_limit_ip_key('fe80::1%eth0') // → 'fe80::/64'
|
|
147
|
+
* rate_limit_ip_key('203.0.113.7') // → '203.0.113.7'
|
|
148
|
+
* rate_limit_ip_key('unknown') // → 'unknown'
|
|
149
|
+
*/
|
|
150
|
+
export declare const rate_limit_ip_key: (ip: string) => string;
|
|
100
151
|
//# sourceMappingURL=ip_canonical.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ip_canonical.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/ip_canonical.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ip_canonical.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/ip_canonical.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AASH;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAAM,GAAG,IAKrD,CAAC;AA0BF;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAAM,GAAG,IAkBrD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,QAAqB,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAsB5C,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,wBAAwB,GAAI,MAAM,MAAM,KAAG,MAgEvD,CAAC;AAKF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,iBAAiB,GAAI,IAAI,MAAM,KAAG,MAQ9C,CAAC"}
|