@fuzdev/fuz_app 0.140.0 → 0.142.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 +72 -22
- package/dist/actions/action_rpc.d.ts +5 -1
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +9 -1
- package/dist/actions/connection_closer.d.ts +44 -4
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +33 -3
- 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_action_ws.d.ts +42 -6
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +87 -19
- package/dist/actions/register_ws_endpoint.d.ts +15 -6
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +10 -4
- package/dist/actions/socket.svelte.d.ts +19 -4
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +31 -4
- package/dist/actions/transports.d.ts +30 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +30 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +13 -6
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +24 -10
- package/dist/actions/transports_ws_backend.d.ts +51 -5
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +62 -9
- package/dist/actions/ws_endpoint_spec.d.ts +11 -10
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +44 -22
- 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 +1 -0
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +12 -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/api_token.d.ts +20 -0
- package/dist/auth/api_token.d.ts.map +1 -1
- package/dist/auth/api_token.js +21 -1
- package/dist/auth/api_token_queries.d.ts +22 -4
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +11 -6
- package/dist/auth/audit_log_routes.d.ts +10 -0
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +20 -0
- package/dist/auth/audit_log_schema.d.ts +30 -5
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +30 -0
- package/dist/auth/bearer_auth.d.ts +4 -2
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +19 -8
- package/dist/auth/bootstrap_route_schema.d.ts +1 -0
- package/dist/auth/bootstrap_route_schema.d.ts.map +1 -1
- package/dist/auth/bootstrap_route_schema.js +1 -0
- 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/cleanup.d.ts +9 -9
- package/dist/auth/cleanup.js +9 -9
- package/dist/auth/credential_deadline.d.ts +95 -0
- package/dist/auth/credential_deadline.d.ts.map +1 -0
- package/dist/auth/credential_deadline.js +139 -0
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +4 -1
- package/dist/auth/middleware.d.ts +90 -12
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +115 -16
- package/dist/auth/request_context.d.ts +8 -2
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +20 -4
- package/dist/auth/resolved_auth.d.ts +13 -2
- package/dist/auth/resolved_auth.d.ts.map +1 -1
- package/dist/auth/resolved_auth.js +5 -4
- 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/session_cookie.d.ts +4 -0
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +3 -2
- package/dist/auth/session_middleware.d.ts +1 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +4 -1
- package/dist/auth/session_queries.d.ts +19 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +8 -2
- package/dist/auth/signup_route_schema.d.ts.map +1 -1
- package/dist/auth/signup_route_schema.js +1 -0
- 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/hono_context.d.ts +23 -0
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +19 -0
- package/dist/http/CLAUDE.md +41 -15
- 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 +30 -3
- package/dist/http/ip_canonical.d.ts.map +1 -1
- package/dist/http/ip_canonical.js +50 -3
- package/dist/http/middleware_spec.d.ts +16 -0
- package/dist/http/middleware_spec.d.ts.map +1 -1
- package/dist/http/origin.d.ts +113 -18
- package/dist/http/origin.d.ts.map +1 -1
- package/dist/http/origin.js +247 -127
- package/dist/http/route_spec.d.ts +21 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/schema_helpers.d.ts +22 -0
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +24 -0
- package/dist/http/surface.d.ts +6 -1
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +12 -5
- 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/realtime/sse_auth_guard.d.ts +22 -6
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +31 -12
- package/dist/realtime/subscriber_registry.d.ts +10 -1
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +10 -1
- package/dist/server/app_server.d.ts +20 -7
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +8 -3
- package/dist/server/env.d.ts +2 -1
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +7 -17
- 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 +16 -6
- package/dist/testing/CLAUDE.md +164 -25
- package/dist/testing/adversarial_headers.d.ts.map +1 -1
- package/dist/testing/adversarial_headers.js +13 -6
- package/dist/testing/connection_closer_helpers.d.ts +6 -1
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
- package/dist/testing/connection_closer_helpers.js +4 -0
- 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/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +14 -65
- package/dist/testing/cross_backend/capabilities.d.ts +26 -2
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +9 -3
- 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/conformance_case.d.ts +11 -3
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +6 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +20 -19
- package/dist/testing/cross_backend/credential_deadline.d.ts +25 -0
- package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -0
- package/dist/testing/cross_backend/credential_deadline.js +185 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +13 -2
- package/dist/testing/cross_backend/expected_schema.json +0 -1
- package/dist/testing/cross_backend/full_spine_mount.d.ts +3 -0
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +7 -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/narrowed_api_token.d.ts +23 -0
- package/dist/testing/cross_backend/narrowed_api_token.d.ts.map +1 -0
- package/dist/testing/cross_backend/narrowed_api_token.js +40 -0
- package/dist/testing/cross_backend/origin.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin.js +6 -26
- package/dist/testing/cross_backend/origin_conformance.d.ts +152 -0
- package/dist/testing/cross_backend/origin_conformance.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin_conformance.js +136 -0
- package/dist/testing/cross_backend/origin_conformance.json +1679 -0
- package/dist/testing/cross_backend/origin_scope.d.ts +16 -0
- package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin_scope.js +140 -0
- 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_cross_helpers.d.ts +35 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.js +64 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +8 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +44 -33
- 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/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -4
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +20 -1
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +23 -1
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +20 -7
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +180 -5
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +154 -5
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +25 -21
- package/dist/testing/cross_backend/transport_composition.d.ts +60 -0
- package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -0
- package/dist/testing/cross_backend/transport_composition.js +364 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +21 -6
- 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/cross_backend/ws_role_gated.d.ts +38 -0
- package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_role_gated.js +142 -0
- package/dist/testing/entities.d.ts +23 -0
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +37 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +3 -1
- package/dist/testing/middleware.d.ts +10 -0
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +31 -7
- 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/testing/stubs.d.ts +16 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +20 -7
- package/dist/testing/surface_invariants.js +2 -2
- 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
|
@@ -14,7 +14,8 @@ import { query_invite_find_unclaimed_match_for_update, query_invite_claim_unscop
|
|
|
14
14
|
import { create_signup_route_shape } from './signup_route_schema.js';
|
|
15
15
|
import { get_route_input } from '../http/route_spec.js';
|
|
16
16
|
import { get_client_ip } from '../http/client_ip.js';
|
|
17
|
-
import {
|
|
17
|
+
import { rate_limit_ip_key } from '../http/ip_canonical.js';
|
|
18
|
+
import { rate_limit_exceeded_response, reserve_optional } from '../rate_limiter.js';
|
|
18
19
|
import { ERROR_NO_MATCHING_INVITE, ERROR_SIGNUP_CONFLICT } from '../http/error_schemas.js';
|
|
19
20
|
import { is_pg_unique_violation } from '../db/pg_error.js';
|
|
20
21
|
/**
|
|
@@ -62,148 +63,170 @@ export const create_signup_route_specs = (deps, options) => {
|
|
|
62
63
|
signup_account_rate_limited: signup_account_rate_limiter !== null
|
|
63
64
|
}),
|
|
64
65
|
handler: async (c, route) => {
|
|
65
|
-
// Per-IP rate limit
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
// Per-IP rate limit (before any work). 429 stays fast. Reserved,
|
|
67
|
+
// not checked: the attempt counts against the cap until it settles
|
|
68
|
+
// below, so a concurrent burst can't all pass before any of it
|
|
69
|
+
// records — and the Argon2 hash every attempt pays is bounded with
|
|
70
|
+
// it. The `finally` releases whatever no branch recorded.
|
|
71
|
+
// keyed by the address's rate-limit key (an IPv6 client's /64)
|
|
72
|
+
const ip_key = signup_ip_rate_limiter ? rate_limit_ip_key(get_client_ip(c)) : null;
|
|
73
|
+
const ip_reserved = await reserve_optional(signup_ip_rate_limiter, ip_key, {
|
|
74
|
+
signal: c.req.raw.signal
|
|
75
|
+
});
|
|
76
|
+
if (!ip_reserved.ok) {
|
|
77
|
+
return rate_limit_exceeded_response(c, ip_reserved.retry_after);
|
|
72
78
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
let account_reservation = null;
|
|
80
|
+
try {
|
|
81
|
+
const { username, password: pw, email } = get_route_input(c);
|
|
82
|
+
// Per-account rate limit (after input parsing, before DB work).
|
|
83
|
+
// 429 stays fast — same precedent as login.
|
|
84
|
+
const account_key = username.toLowerCase();
|
|
85
|
+
const account_reserved = await reserve_optional(signup_account_rate_limiter, account_key, { signal: c.req.raw.signal });
|
|
86
|
+
if (!account_reserved.ok) {
|
|
87
|
+
return rate_limit_exceeded_response(c, account_reserved.retry_after);
|
|
81
88
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
89
|
+
account_reservation = account_reserved.reservation;
|
|
90
|
+
// Load the open-signup toggle fresh from the DB on every
|
|
91
|
+
// request — the authoritative source, so multiple server
|
|
92
|
+
// processes never serve a stale in-memory value. Its cost, and
|
|
93
|
+
// the hash below, are bounded per address by the per-IP
|
|
94
|
+
// reservation above: every classified failure and every
|
|
95
|
+
// successful open signup records it. What escapes is an
|
|
96
|
+
// invite-gated success (which spends an invite), a 5xx, and a
|
|
97
|
+
// request with no resolved client IP. The per-account limiter
|
|
98
|
+
// bounds retries of one username only.
|
|
99
|
+
const { open_signup } = await query_app_settings_load(route);
|
|
100
|
+
// Start the denial-time floor concurrently with failure work.
|
|
101
|
+
// Observed response time for 403 / 409 is `max(work, delay)`
|
|
102
|
+
// so the cheap `no_match` path (no Argon2, find returns
|
|
103
|
+
// nothing) and the expensive `signup_conflict` path (Argon2
|
|
104
|
+
// + tx + rollback) converge — closes the username-enumeration
|
|
105
|
+
// timing oracle. Mirrors login's `login_fail_delay`. Started
|
|
106
|
+
// after rate-limit checks so 429 stays fast.
|
|
107
|
+
const delay = signup_fail_delay(signup_fail_floor_ms, signup_fail_jitter_ms);
|
|
108
|
+
// Hash before the transaction so the connection isn't held
|
|
109
|
+
// across the ~100ms Argon2id. Paid on every attempt that gets
|
|
110
|
+
// here — bounded as described above.
|
|
111
|
+
const password_hash = await password.hash_password(pw);
|
|
112
|
+
// `invite` is assigned inside the tx by the FOR UPDATE find;
|
|
113
|
+
// captured at the outer scope so the failure-audit catch
|
|
114
|
+
// branch can still reference `invite.id` after the tx rolls
|
|
115
|
+
// back on PG unique violation.
|
|
116
|
+
let invite;
|
|
117
|
+
const emit_failure_audit = (reason) => {
|
|
118
|
+
deps.audit.emit(route, {
|
|
119
|
+
event_type: 'signup',
|
|
120
|
+
outcome: 'failure',
|
|
121
|
+
ip: get_client_ip(c),
|
|
122
|
+
metadata: {
|
|
123
|
+
username,
|
|
124
|
+
reason,
|
|
125
|
+
...(invite && { invite_id: invite.id }),
|
|
126
|
+
...(email != null && { email }),
|
|
127
|
+
...(open_signup && { open_signup: true })
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
let result;
|
|
132
|
+
try {
|
|
133
|
+
result = await route.db.transaction(async (tx) => {
|
|
134
|
+
const tx_deps = { db: tx };
|
|
135
|
+
// Find + claim run inside the same transaction so the
|
|
136
|
+
// row lock makes them atomic. Concurrent signups for
|
|
137
|
+
// the same (username, email) tuple block on the lock
|
|
138
|
+
// and observe the post-commit state on retry — the
|
|
139
|
+
// loser's `find_for_update` returns no row (winner
|
|
140
|
+
// flipped `claimed_at`) and falls through to
|
|
141
|
+
// `ERROR_NO_MATCHING_INVITE`. No race window.
|
|
142
|
+
if (!open_signup) {
|
|
143
|
+
invite = await query_invite_find_unclaimed_match_for_update(tx_deps, email ?? null, username);
|
|
144
|
+
if (!invite) {
|
|
145
|
+
throw new NoMatchingInviteError();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const { account, actor } = await query_create_account_with_actor(tx_deps, {
|
|
149
|
+
username,
|
|
150
|
+
password_hash,
|
|
151
|
+
email
|
|
152
|
+
});
|
|
153
|
+
if (invite) {
|
|
154
|
+
// Guaranteed to succeed: FOR UPDATE held the row
|
|
155
|
+
// for the duration of the tx, so no concurrent
|
|
156
|
+
// claim could flip `claimed_at` between the find
|
|
157
|
+
// and this UPDATE.
|
|
158
|
+
await query_invite_claim_unscoped(tx_deps, invite.id, account.id);
|
|
159
|
+
}
|
|
160
|
+
await create_session_and_set_cookie({
|
|
161
|
+
keyring,
|
|
162
|
+
deps: tx_deps,
|
|
163
|
+
c,
|
|
164
|
+
account_id: account.id,
|
|
165
|
+
session_options
|
|
166
|
+
});
|
|
167
|
+
return { account, actor };
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
if (e instanceof NoMatchingInviteError) {
|
|
172
|
+
ip_reserved.reservation.record();
|
|
173
|
+
account_reservation?.record();
|
|
174
|
+
emit_failure_audit('no_match');
|
|
175
|
+
await delay;
|
|
176
|
+
return c.json({ error: ERROR_NO_MATCHING_INVITE }, 403);
|
|
177
|
+
}
|
|
178
|
+
// Unique constraint violation: username or email already exists.
|
|
179
|
+
if (is_pg_unique_violation(e)) {
|
|
180
|
+
ip_reserved.reservation.record();
|
|
181
|
+
account_reservation?.record();
|
|
182
|
+
emit_failure_audit('signup_conflict');
|
|
183
|
+
await delay;
|
|
184
|
+
return c.json({ error: ERROR_SIGNUP_CONFLICT }, 409);
|
|
185
|
+
}
|
|
186
|
+
// Unclassified failure (e.g. session create error, Argon2
|
|
187
|
+
// fault on hash, DB outage mid-tx). Tx is rolled back so
|
|
188
|
+
// no account persists, but the *attempt* should leave a
|
|
189
|
+
// forensic trail — emit `outcome: 'failure'` with reason
|
|
190
|
+
// `internal_error` before rethrowing. 5xx responses are
|
|
191
|
+
// not floored: they aren't response-time-controlled
|
|
192
|
+
// enumeration oracles.
|
|
193
|
+
emit_failure_audit('internal_error');
|
|
194
|
+
throw e;
|
|
195
|
+
}
|
|
196
|
+
// Clear the account-grain bucket only — the IP aggregate stands.
|
|
197
|
+
// This is the cheapest version of the spray path `RateLimiter.reset`
|
|
198
|
+
// describes: where login at least costs the attacker a credential,
|
|
199
|
+
// an open-signup deployment let an *unauthenticated* caller zero a
|
|
200
|
+
// shared IP budget by creating throwaway accounts, and the
|
|
201
|
+
// per-account limiter never applies to the attacker's own signups.
|
|
202
|
+
account_reservation?.release();
|
|
203
|
+
if (signup_account_rate_limiter)
|
|
204
|
+
signup_account_rate_limiter.reset(account_key);
|
|
205
|
+
// A successful *open* signup is charged to the IP bucket like a
|
|
206
|
+
// failure: with no invite to spend, account creation is otherwise
|
|
207
|
+
// unbounded from a single address — throwaway accounts, each one a
|
|
208
|
+
// session and a row. An invite-gated success is bounded by the
|
|
209
|
+
// invites an admin minted, so it records nothing (a class signing up
|
|
210
|
+
// on its invites from one NAT egress would otherwise run the bucket
|
|
211
|
+
// dry); the `finally` releases it.
|
|
212
|
+
if (open_signup)
|
|
213
|
+
ip_reserved.reservation.record();
|
|
106
214
|
deps.audit.emit(route, {
|
|
107
215
|
event_type: 'signup',
|
|
108
|
-
|
|
216
|
+
account_id: result.account.id,
|
|
109
217
|
ip: get_client_ip(c),
|
|
110
|
-
metadata: {
|
|
111
|
-
username,
|
|
112
|
-
reason,
|
|
113
|
-
...(invite && { invite_id: invite.id }),
|
|
114
|
-
...(email != null && { email }),
|
|
115
|
-
...(open_signup && { open_signup: true })
|
|
116
|
-
}
|
|
218
|
+
metadata: invite ? { invite_id: invite.id, username } : { open_signup: true, username }
|
|
117
219
|
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const tx_deps = { db: tx };
|
|
123
|
-
// Find + claim run inside the same transaction so the
|
|
124
|
-
// row lock makes them atomic. Concurrent signups for
|
|
125
|
-
// the same (username, email) tuple block on the lock
|
|
126
|
-
// and observe the post-commit state on retry — the
|
|
127
|
-
// loser's `find_for_update` returns no row (winner
|
|
128
|
-
// flipped `claimed_at`) and falls through to
|
|
129
|
-
// `ERROR_NO_MATCHING_INVITE`. No race window.
|
|
130
|
-
if (!open_signup) {
|
|
131
|
-
invite = await query_invite_find_unclaimed_match_for_update(tx_deps, email ?? null, username);
|
|
132
|
-
if (!invite) {
|
|
133
|
-
throw new NoMatchingInviteError();
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const { account, actor } = await query_create_account_with_actor(tx_deps, {
|
|
137
|
-
username,
|
|
138
|
-
password_hash,
|
|
139
|
-
email
|
|
140
|
-
});
|
|
141
|
-
if (invite) {
|
|
142
|
-
// Guaranteed to succeed: FOR UPDATE held the row
|
|
143
|
-
// for the duration of the tx, so no concurrent
|
|
144
|
-
// claim could flip `claimed_at` between the find
|
|
145
|
-
// and this UPDATE.
|
|
146
|
-
await query_invite_claim_unscoped(tx_deps, invite.id, account.id);
|
|
147
|
-
}
|
|
148
|
-
await create_session_and_set_cookie({
|
|
149
|
-
keyring,
|
|
150
|
-
deps: tx_deps,
|
|
151
|
-
c,
|
|
152
|
-
account_id: account.id,
|
|
153
|
-
session_options
|
|
154
|
-
});
|
|
155
|
-
return { account, actor };
|
|
220
|
+
return c.json({
|
|
221
|
+
ok: true,
|
|
222
|
+
account: { id: result.account.id, username: result.account.username },
|
|
223
|
+
actor: { id: result.actor.id }
|
|
156
224
|
});
|
|
157
225
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
signup_ip_rate_limiter.record(ip);
|
|
162
|
-
if (signup_account_rate_limiter)
|
|
163
|
-
signup_account_rate_limiter.record(account_key);
|
|
164
|
-
emit_failure_audit('no_match');
|
|
165
|
-
await delay;
|
|
166
|
-
return c.json({ error: ERROR_NO_MATCHING_INVITE }, 403);
|
|
167
|
-
}
|
|
168
|
-
// Unique constraint violation: username or email already exists.
|
|
169
|
-
if (is_pg_unique_violation(e)) {
|
|
170
|
-
if (signup_ip_rate_limiter && ip)
|
|
171
|
-
signup_ip_rate_limiter.record(ip);
|
|
172
|
-
if (signup_account_rate_limiter)
|
|
173
|
-
signup_account_rate_limiter.record(account_key);
|
|
174
|
-
emit_failure_audit('signup_conflict');
|
|
175
|
-
await delay;
|
|
176
|
-
return c.json({ error: ERROR_SIGNUP_CONFLICT }, 409);
|
|
177
|
-
}
|
|
178
|
-
// Unclassified failure (e.g. session create error, Argon2
|
|
179
|
-
// fault on hash, DB outage mid-tx). Tx is rolled back so
|
|
180
|
-
// no account persists, but the *attempt* should leave a
|
|
181
|
-
// forensic trail — emit `outcome: 'failure'` with reason
|
|
182
|
-
// `internal_error` before rethrowing. 5xx responses are
|
|
183
|
-
// not floored: they aren't response-time-controlled
|
|
184
|
-
// enumeration oracles.
|
|
185
|
-
emit_failure_audit('internal_error');
|
|
186
|
-
throw e;
|
|
226
|
+
finally {
|
|
227
|
+
ip_reserved.reservation.release();
|
|
228
|
+
account_reservation?.release();
|
|
187
229
|
}
|
|
188
|
-
// Clear the account-grain bucket only — the IP aggregate stands.
|
|
189
|
-
// This is the cheapest version of the spray path `RateLimiter.reset`
|
|
190
|
-
// describes: where login at least costs the attacker a credential,
|
|
191
|
-
// an open-signup deployment let an *unauthenticated* caller zero a
|
|
192
|
-
// shared IP budget by creating throwaway accounts, and the
|
|
193
|
-
// per-account limiter never applies to the attacker's own signups.
|
|
194
|
-
if (signup_account_rate_limiter)
|
|
195
|
-
signup_account_rate_limiter.reset(account_key);
|
|
196
|
-
deps.audit.emit(route, {
|
|
197
|
-
event_type: 'signup',
|
|
198
|
-
account_id: result.account.id,
|
|
199
|
-
ip: get_client_ip(c),
|
|
200
|
-
metadata: invite ? { invite_id: invite.id, username } : { open_signup: true, username }
|
|
201
|
-
});
|
|
202
|
-
return c.json({
|
|
203
|
-
ok: true,
|
|
204
|
-
account: { id: result.account.id, username: result.account.username },
|
|
205
|
-
actor: { id: result.actor.id }
|
|
206
|
-
});
|
|
207
230
|
}
|
|
208
231
|
}
|
|
209
232
|
];
|
|
@@ -77,7 +77,13 @@ export type TokenScopeInput = z.infer<typeof TokenScopeInput>;
|
|
|
77
77
|
export declare const token_scope_full: () => TokenScope;
|
|
78
78
|
/** A narrowed scope over `methods`. */
|
|
79
79
|
export declare const token_scope_methods: (methods: Array<string>) => TokenScope;
|
|
80
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Build the stored scope from a mint-time input.
|
|
82
|
+
*
|
|
83
|
+
* Duplicate method names collapse into one, first occurrence kept: the set is
|
|
84
|
+
* what the scope admits, and the Rust spine stores a set, so both count the
|
|
85
|
+
* same methods (the `account_token_list` label reads `N methods`).
|
|
86
|
+
*/
|
|
81
87
|
export declare const token_scope_from_input: (input: TokenScopeInput) => TokenScope;
|
|
82
88
|
/**
|
|
83
89
|
* Does `scope` admit dispatching `method`?
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token_scope.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/token_scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAW,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU;;;;;;;;;4BAYrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;;;4BAG1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,kDAAkD;AAClD,eAAO,MAAM,gBAAgB,QAAO,UAGlC,CAAC;AAEH,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,GAAI,SAAS,KAAK,CAAC,MAAM,CAAC,KAAG,UAI3D,CAAC;AAEH
|
|
1
|
+
{"version":3,"file":"token_scope.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/token_scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAW,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU;;;;;;;;;4BAYrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;;;4BAG1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,kDAAkD;AAClD,eAAO,MAAM,gBAAgB,QAAO,UAGlC,CAAC;AAEH,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,GAAI,SAAS,KAAK,CAAC,MAAM,CAAC,KAAG,UAI3D,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,eAAe,KAAG,UAC8B,CAAC;AAE/F;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,OAAO,UAAU,EAAE,QAAQ,MAAM,KAAG,OACtB,CAAC;AAEzD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,0BAA0B,GAAI,OAAO,UAAU,KAAG,OAAgC,CAAC;AAEhG;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,iFAMjB,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,KAAG,KAAK,IAAI,YACA,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,QAAgC,CAAC;AAQtE;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,GAAI,QAAQ,MAAM,KAAG,MAC3B,CAAC;AAErC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,oBAAoB,GAC/B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/F;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,4BAA4B,GAAI,OAAO,MAAM,KAAG,oBAAoB,GAAG,IAUnF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GACzC,OAAO,UAAU,EACjB,YAAY,oBAAoB,KAC9B,OAGkC,CAAC;AAEtC;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,GACnC,YAAY,MAAM,KAChB;IAAE,KAAK,EAAE,sBAAsB,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAGxD,CAAC;AAEH,4CAA4C;AAC5C,eAAO,MAAM,iBAAiB,GAAI,OAAO,UAAU,KAAG,MAGrD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,KAAG,UAAU,GAAG,IAU5D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,UAAU,KAAG,MAQzD,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,OAAO,KAAG,UAAU,GAAG,IAQtE,CAAC"}
|
package/dist/auth/token_scope.js
CHANGED
|
@@ -83,8 +83,14 @@ export const token_scope_methods = (methods) => ({
|
|
|
83
83
|
kind: 'methods',
|
|
84
84
|
methods
|
|
85
85
|
});
|
|
86
|
-
/**
|
|
87
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Build the stored scope from a mint-time input.
|
|
88
|
+
*
|
|
89
|
+
* Duplicate method names collapse into one, first occurrence kept: the set is
|
|
90
|
+
* what the scope admits, and the Rust spine stores a set, so both count the
|
|
91
|
+
* same methods (the `account_token_list` label reads `N methods`).
|
|
92
|
+
*/
|
|
93
|
+
export const token_scope_from_input = (input) => input.kind === 'full' ? token_scope_full() : token_scope_methods([...new Set(input.methods)]);
|
|
88
94
|
/**
|
|
89
95
|
* Does `scope` admit dispatching `method`?
|
|
90
96
|
*
|
package/dist/db/CLAUDE.md
CHANGED
|
@@ -95,21 +95,33 @@ The wire schemas + RPC handlers + authz predicates for this layer live in
|
|
|
95
95
|
authorizer's verdict + the `cell_moderate` verb, never in `data`. Nullable
|
|
96
96
|
`created_by` / `updated_by` FKs to `actor` (NULL = system origin). GIN on `data`
|
|
97
97
|
/ `refs`; global partial-unique on `path`.
|
|
98
|
-
- **`cell_history_ddl.ts`** — dormant `cell_history` table
|
|
99
|
-
(`CELL_HISTORY_MIGRATION_NS`, namespace `fuz_cell_history`), FK → `cell.id`.
|
|
100
|
-
Ships present-but-unwritten; no snapshot lifecycle yet.
|
|
101
98
|
- **`cell_queries.ts`** — `query_cell_create / get / get_by_path / update /
|
|
102
99
|
delete / set_moderation`, `_list_by_kind / _list_by_creator`, the
|
|
103
100
|
generic `query_cell_list` (filter + SQL-side visibility predicate mirroring
|
|
104
|
-
`can_view_cell
|
|
105
|
-
`ref` filter by `cell.refs`, plus `root_id` /
|
|
106
|
-
per-root feed + mod queue),
|
|
107
|
-
`query_cell_load_many` (bulk id load, no visibility filter —
|
|
108
|
-
|
|
101
|
+
`can_view_cell`, moderation admission included; the `kind` filter narrows
|
|
102
|
+
by the `cell.kind` column, the `ref` filter by `cell.refs`, plus `root_id` /
|
|
103
|
+
`moderation` filters for the per-root feed + mod queue),
|
|
104
|
+
`query_cell_load_many` (bulk id load of full rows, no visibility filter —
|
|
105
|
+
the deep-clone walk), and `query_cell_view_load_many` (the
|
|
106
|
+
`CELL_VIEW_COLUMNS` view facts alone — feeds `filter_visible_target_ids`,
|
|
107
|
+
the in-memory relation filter the `cell_get` bundle and the clone walk
|
|
108
|
+
run). `query_cell_set_moderation` is the dedicated
|
|
109
109
|
`cell_moderate` write (`moderation` is absent from `CellUpdatePatch`, so an
|
|
110
110
|
author can't self-approve via `cell_update`). `cell.refs` derived from
|
|
111
111
|
`data` via `extract_refs` on create/update. `CellRow.grant_count` is a
|
|
112
112
|
derived projection (correlated subquery on `idx_cell_grant_cell`).
|
|
113
|
+
- **`cell_moderation.ts`** — the `cell.moderation` read rules in pure and
|
|
114
|
+
SQL form side by side (`cell_moderation_admits_audience` /
|
|
115
|
+
`_sql`: the allowlist unmoderated ‖ `'approved'`), composed by
|
|
116
|
+
`auth/cell_authorize.ts` and `query_cell_list` so the two can't drift; the
|
|
117
|
+
`CELL_MODERATION_*` literals. Twin of the Rust `fuz_cell::cell_moderation`.
|
|
118
|
+
- **`cell_view_sql.ts`** — `can_view_cell` in SQL: `can_view_cell_sql`
|
|
119
|
+
(admin ‖ owner ‖ moderation-admitted public or grant), the
|
|
120
|
+
`caller_role_grants_cte` its role-shaped grant arm reads, and `CellViewer`
|
|
121
|
+
(the caller it binds — `auth/cell_authorize.ts`'s `cell_viewer_from_auth`
|
|
122
|
+
builds one; `CELL_VIEWER_UNRESTRICTED` filters nothing). `query_cell_list`
|
|
123
|
+
embeds it beside its steward arm, and the relation lists apply it to the
|
|
124
|
+
far end before `LIMIT`. Twin of the Rust `fuz_cell::cell_view_sql`.
|
|
113
125
|
- **`cell_grant_queries.ts`** — resource-side ACL: `query_cell_grant_create`
|
|
114
126
|
(UPSERT on the partial unique index, actor- vs role-shaped principal),
|
|
115
127
|
`_get`, `_delete` (returns deleted row), `_list_for_cell`,
|
|
@@ -119,11 +131,14 @@ delete / set_moderation`, `_list_by_kind / _list_by_creator`, the
|
|
|
119
131
|
- **`cell_field_queries.ts`** — named relations (`(source_id, name) →
|
|
120
132
|
target_id`): `query_cell_field_set` (UPSERT), `_get`, `_delete`,
|
|
121
133
|
`_list_for_source` (`{limit, name_after}`), `_list_for_target`. Reads JOIN
|
|
122
|
-
cell with `deleted_at IS NULL` so dangling relations don't surface
|
|
134
|
+
cell with `deleted_at IS NULL` so dangling relations don't surface; the
|
|
135
|
+
list reads take a `CellViewer` and keep only edges whose far end it can
|
|
136
|
+
view, before `LIMIT`.
|
|
123
137
|
- **`cell_item_queries.ts`** — ordered children (`(parent_id, position) →
|
|
124
138
|
child_id`, fractional-index keyed): `query_cell_item_insert` (throws
|
|
125
139
|
`23505` on collision), `_get`, `_move`, `_delete`, `_list_for_parent`
|
|
126
|
-
(`{limit, position_after}`), `_list_for_child`. Same soft-delete JOIN
|
|
140
|
+
(`{limit, position_after}`), `_list_for_child`. Same soft-delete JOIN and
|
|
141
|
+
viewer filter.
|
|
127
142
|
- **`cell_audit_queries.ts`** — `query_audit_log_list_by_cell`: matches
|
|
128
143
|
`audit_log` rows whose `metadata` names the cell on any of six cell-domain
|
|
129
144
|
keys (`cell_id` / `source_id` / `new_id` / `parent_id` / `child_id` /
|
|
@@ -191,22 +206,20 @@ DO NOTHING`), `_put_fact_refs`, `_get_fact` / `_get_fact_meta` / `_has_fact`
|
|
|
191
206
|
FK dependency dictates order; consumers register via `migration_namespaces`
|
|
192
207
|
on `create_app_backend` (after the built-in `fuz_auth` namespace). The fact
|
|
193
208
|
namespace is FK-independent so its slot is free; the spine binary and every
|
|
194
|
-
consumer place it last
|
|
195
|
-
|
|
196
|
-
is assigned per-namespace — so only the cell→cell_history FK constrains it):
|
|
209
|
+
consumer place it last (cross-namespace order is immaterial to the
|
|
210
|
+
`schema_version` rows — `sequence` is assigned per-namespace):
|
|
197
211
|
|
|
198
212
|
```
|
|
199
213
|
auth_migration_ns (built-in, fuz_auth)
|
|
200
|
-
→ CELL_MIGRATION_NS
|
|
201
|
-
→
|
|
202
|
-
→ FACT_MIGRATION_NS (fuz_facts — no external FK; slot is free)
|
|
214
|
+
→ CELL_MIGRATION_NS (fuz_cell — FKs actor.id)
|
|
215
|
+
→ FACT_MIGRATION_NS (fuz_facts — no external FK; slot is free)
|
|
203
216
|
```
|
|
204
217
|
|
|
205
218
|
### Pre-stable migration policy
|
|
206
219
|
|
|
207
|
-
Scoped to the namespaces on this page (`fuz_cell` / `
|
|
208
|
-
|
|
209
|
-
|
|
220
|
+
Scoped to the namespaces on this page (`fuz_cell` / `fuz_facts`): each is a
|
|
221
|
+
single `full_*_schema` entry, so when it needs to change shape, **rewrite the
|
|
222
|
+
original in place** rather than appending a
|
|
210
223
|
follow-up — consumers re-bootstrap dev DBs on upgrade. The `fuz_auth`
|
|
211
224
|
namespace is the opposite — frozen and append-only, since consumers hold
|
|
212
225
|
deployed databases on it. See ../../../docs/migrations.md §Schema stability.
|
package/dist/db/cell_ddl.d.ts
CHANGED
|
@@ -29,8 +29,7 @@
|
|
|
29
29
|
*
|
|
30
30
|
* **Single-migration shape**: `full_cell_schema` creates the canonical
|
|
31
31
|
* cell + cell_grant + cell_field + cell_item layout in one shot from the
|
|
32
|
-
* live exported constants.
|
|
33
|
-
* separate `fuz_cell_history` namespace (`cell_history_ddl.ts`).
|
|
32
|
+
* live exported constants.
|
|
34
33
|
*
|
|
35
34
|
* @module
|
|
36
35
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_ddl.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cell_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,oIAI1B,CAAC;AAER;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,WAAW,gkBAgBtB,CAAC;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,MAAM,CAkBtC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,0iBAc5B,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAW5C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,+RAO5B,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAE5C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,qSAO3B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,MAAM,CAG3C,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,4DAA6D,CAAC;AAE3F,uBAAuB;AACvB,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CAuB5C,CAAC;AAEF,gDAAgD;AAChD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
|
package/dist/db/cell_ddl.js
CHANGED
|
@@ -29,8 +29,7 @@
|
|
|
29
29
|
*
|
|
30
30
|
* **Single-migration shape**: `full_cell_schema` creates the canonical
|
|
31
31
|
* cell + cell_grant + cell_field + cell_item layout in one shot from the
|
|
32
|
-
* live exported constants.
|
|
33
|
-
* separate `fuz_cell_history` namespace (`cell_history_ddl.ts`).
|
|
32
|
+
* live exported constants.
|
|
34
33
|
*
|
|
35
34
|
* @module
|
|
36
35
|
*/
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* not by allowing duplicate field rows.
|
|
9
9
|
*
|
|
10
10
|
* Reads filter both endpoints by `cell.deleted_at IS NULL` so relations
|
|
11
|
-
* dangling off a soft-deleted cell don't surface to the live graph
|
|
11
|
+
* dangling off a soft-deleted cell don't surface to the live graph, and the
|
|
12
|
+
* list reads apply the view predicate (`db/cell_view_sql.ts`) to the far end
|
|
13
|
+
* before `LIMIT` — a page is up to `limit` edges whose far end the caller can
|
|
14
|
+
* view.
|
|
12
15
|
*
|
|
13
16
|
* `query_cell_field_set` upserts on the `(source_id, name)` PK so
|
|
14
17
|
* re-pointing a name updates `target_id` in place — JSON-object semantics
|
|
@@ -18,6 +21,7 @@
|
|
|
18
21
|
*/
|
|
19
22
|
import type { QueryDeps } from './query_deps.js';
|
|
20
23
|
import type { Uuid } from '../primitive_schemas.js';
|
|
24
|
+
import { type CellViewer } from './cell_view_sql.js';
|
|
21
25
|
/** Row shape returned by `cell_field` SELECTs. */
|
|
22
26
|
export interface CellFieldRow {
|
|
23
27
|
source_id: Uuid;
|
|
@@ -77,38 +81,36 @@ export declare const query_cell_field_delete: (deps: QueryDeps, source_id: Uuid,
|
|
|
77
81
|
/**
|
|
78
82
|
* Forward fields list (`source.fields[]`).
|
|
79
83
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
84
|
+
* Up to `limit` fields whose target is live and one `viewer` may view
|
|
85
|
+
* (`can_view_cell_sql`, applied before the limit); `name_after` pages on.
|
|
86
|
+
* The source-view gate is the caller's responsibility.
|
|
83
87
|
*
|
|
84
88
|
* @param deps - query deps
|
|
89
|
+
* @param viewer - who is reading; `CELL_VIEWER_UNRESTRICTED` filters nothing
|
|
85
90
|
* @param source_id - source cell id
|
|
86
|
-
* @param options - `limit` caps the row count; `name_after`
|
|
87
|
-
* name (keyset pagination)
|
|
91
|
+
* @param options - `limit` caps the row count (omitted = unbounded); `name_after`
|
|
92
|
+
* resumes after a name (keyset pagination)
|
|
88
93
|
* @returns matching rows in lex `name` order
|
|
89
94
|
*/
|
|
90
|
-
export declare const query_cell_field_list_for_source: (deps: QueryDeps, source_id: Uuid, options?: {
|
|
95
|
+
export declare const query_cell_field_list_for_source: (deps: QueryDeps, viewer: CellViewer, source_id: Uuid, options?: {
|
|
91
96
|
limit?: number;
|
|
92
97
|
name_after?: string;
|
|
93
98
|
}) => Promise<Array<CellFieldRow>>;
|
|
94
99
|
/**
|
|
95
100
|
* Reverse fields list (`target.upfields[]`).
|
|
96
101
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* Bounded by `limit` (the wire `cell_field_list` cap) so a heavily
|
|
103
|
-
* inbound-linked target can't force an unbounded fetch + per-source authz
|
|
104
|
-
* pass on the public, IP-rate-limited reverse endpoint.
|
|
102
|
+
* Up to `limit` fields whose source is live and one `viewer` may view —
|
|
103
|
+
* filtered before the limit (`can_view_cell_sql`), so sources the caller
|
|
104
|
+
* can't see never crowd out ones it can. The target-view gate is the
|
|
105
|
+
* caller's responsibility.
|
|
105
106
|
*
|
|
106
107
|
* @param deps - query deps
|
|
108
|
+
* @param viewer - who is reading; `CELL_VIEWER_UNRESTRICTED` filters nothing
|
|
107
109
|
* @param target_id - target cell id
|
|
108
|
-
* @param options - `limit` caps the row count
|
|
109
|
-
* @returns matching rows, oldest field first (by the field row's `created_at
|
|
110
|
+
* @param options - `limit` caps the row count (omitted = unbounded)
|
|
111
|
+
* @returns matching rows, oldest field first (by the field row's `created_at`, then its key)
|
|
110
112
|
*/
|
|
111
|
-
export declare const query_cell_field_list_for_target: (deps: QueryDeps, target_id: Uuid, options?: {
|
|
113
|
+
export declare const query_cell_field_list_for_target: (deps: QueryDeps, viewer: CellViewer, target_id: Uuid, options?: {
|
|
112
114
|
limit?: number;
|
|
113
115
|
}) => Promise<Array<CellFieldRow>>;
|
|
114
116
|
//# sourceMappingURL=cell_field_queries.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_field_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_field_queries.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cell_field_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_field_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAGpD,OAAO,EAIN,KAAK,UAAU,EACf,MAAM,oBAAoB,CAAC;AAE5B,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC5B,SAAS,EAAE,IAAI,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,2DAKuB,CAAC;AAKvD,wCAAwC;AACxC,MAAM,WAAW,sBAAsB;IACtC,SAAS,EAAE,IAAI,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,IAAI,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,OAAO,sBAAsB,KAC3B,OAAO,CAAC,YAAY,CAUtB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,WAAW,IAAI,EACf,MAAM,MAAM,KACV,OAAO,CAAC,YAAY,GAAG,IAAI,CAM7B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,WAAW,IAAI,EACf,MAAM,MAAM,KACV,OAAO,CAAC,YAAY,GAAG,IAAI,CAO7B,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,QAAQ,UAAU,EAClB,WAAW,IAAI,EACf,UAAU;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,KAC/C,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAe7B,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,QAAQ,UAAU,EAClB,WAAW,IAAI,EACf,UAAU;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,KAC1B,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAW5B,CAAC"}
|