@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;
|
|
1
|
+
{"version":3,"file":"account_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAwB9F,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAC;AAyB3D,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACpC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,yBAAyB,EAC/B,UAAS,oBAAyB,KAChC,KAAK,CAAC,SAAS,CAsLjB,CAAC"}
|
|
@@ -27,12 +27,14 @@
|
|
|
27
27
|
*
|
|
28
28
|
* @module
|
|
29
29
|
*/
|
|
30
|
+
import { to_error_message } from '@fuzdev/fuz_util/error.ts';
|
|
30
31
|
import { rpc_action } from '../actions/action_rpc.js';
|
|
32
|
+
import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
|
|
31
33
|
import { queue_connection_close } from '../actions/connection_closer.js';
|
|
32
34
|
import { to_session_account } from './account_schema.js';
|
|
33
35
|
import { query_session_list_for_account, query_session_revoke_for_account, query_session_revoke_all_for_account } from './session_queries.js';
|
|
34
36
|
import { query_api_token_enforce_limit, query_api_token_list_for_account, query_create_api_token, query_revoke_api_token_for_account } from './api_token_queries.js';
|
|
35
|
-
import { parse_stored_token_scope, token_scope_from_input, token_scope_label } from './token_scope.js';
|
|
37
|
+
import { parse_stored_token_scope, serialize_token_scope, token_scope_from_input, token_scope_label } from './token_scope.js';
|
|
36
38
|
import { token_lifetime_to_expires_at } from './token_lifetime.js';
|
|
37
39
|
import { generate_api_token } from './api_token.js';
|
|
38
40
|
import { DEFAULT_MAX_TOKENS } from './account_route_schema.js';
|
|
@@ -108,6 +110,15 @@ export const create_account_actions = (deps, options = {}) => {
|
|
|
108
110
|
// There is deliberately no permissive fallback here: an omitted scope is
|
|
109
111
|
// a validation error upstream, not a full-authority token.
|
|
110
112
|
const scope = token_scope_from_input(input.scope);
|
|
113
|
+
// Over the stored-document cap is a malformed request, refused before
|
|
114
|
+
// any write (the insert would otherwise throw it as a 500). Twin of the
|
|
115
|
+
// Rust spine's `invalid_params` on `TokenScopeError::TooLarge`.
|
|
116
|
+
try {
|
|
117
|
+
serialize_token_scope(scope);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
throw jsonrpc_errors.invalid_params(`invalid scope: ${to_error_message(err)}`);
|
|
121
|
+
}
|
|
111
122
|
// `input.lifetime` is required for the same reason — an omitted lifetime
|
|
112
123
|
// is a validation error, never an eternal token.
|
|
113
124
|
const expires_at = token_lifetime_to_expires_at(input.lifetime);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAQvD,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAC3C,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,4CAA4C;AAC5C,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;kBAI9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,4EAA4E;AAC5E,eAAO,MAAM,iCAAiC;;;iBAG5C,CAAC;AACH,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAElG,oFAAoF;AACpF,eAAO,MAAM,UAAU;;;kBAGrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD,6EAA6E;AAC7E,eAAO,MAAM,WAAW;;kBAEtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,2EAA2E;AAC3E,eAAO,MAAM,WAAW,WAAW,CAAC;AACpC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,wFAAwF;AACxF,eAAO,MAAM,YAAY;;;kBAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD,sHAAsH;AACtH,eAAO,MAAM,mBAAmB;;;kBAG9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,uGAAuG;AACvG,eAAO,MAAM,oBAAoB;;;;kBAI/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,8CAA8C;AAC9C,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B
|
|
1
|
+
{"version":3,"file":"account_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAQvD,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAC3C,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,4CAA4C;AAC5C,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;kBAI9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,4EAA4E;AAC5E,eAAO,MAAM,iCAAiC;;;iBAG5C,CAAC;AACH,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAElG,oFAAoF;AACpF,eAAO,MAAM,UAAU;;;kBAGrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD,6EAA6E;AAC7E,eAAO,MAAM,WAAW;;kBAEtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,2EAA2E;AAC3E,eAAO,MAAM,WAAW,WAAW,CAAC;AACpC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,wFAAwF;AACxF,eAAO,MAAM,YAAY;;;kBAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD,sHAAsH;AACtH,eAAO,MAAM,mBAAmB;;;kBAG9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,uGAAuG;AACvG,eAAO,MAAM,oBAAoB;;;;kBAI/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,8CAA8C;AAC9C,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAaD,CAAC;AAEvC,8EAA8E;AAC9E,MAAM,WAAW,wBAAwB;IACxC,8FAA8F;IAC9F,0BAA0B,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,GACvC,SAAS,wBAAwB,KAC/B,CACF,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CA6D1B,CAAC"}
|
|
@@ -85,6 +85,9 @@ export const account_status_route_shape = {
|
|
|
85
85
|
method: 'GET',
|
|
86
86
|
path: '/status',
|
|
87
87
|
auth: { account: 'none', actor: 'none' },
|
|
88
|
+
// no gate — the handler answers an anonymous caller with its own 401 — but
|
|
89
|
+
// it reads the account the auth middleware resolved
|
|
90
|
+
reads_credential: true,
|
|
88
91
|
description: 'Current account info (unauthenticated: 401 with bootstrap status)',
|
|
89
92
|
input: AccountStatusInput,
|
|
90
93
|
output: AccountStatusOutput,
|
|
@@ -115,10 +118,15 @@ export const create_account_route_shapes = (options) => [
|
|
|
115
118
|
method: 'POST',
|
|
116
119
|
path: '/login',
|
|
117
120
|
auth: { account: 'none', actor: 'none' },
|
|
121
|
+
mints_credential: true,
|
|
118
122
|
description: 'Exchange credentials for session',
|
|
119
123
|
input: LoginInput,
|
|
120
124
|
output: LoginOutput,
|
|
121
125
|
rate_limit: 'both',
|
|
126
|
+
// The handler opens its own transaction around just the session writes,
|
|
127
|
+
// so a reservation waiting for a slot, and the Argon2 verify, hold no
|
|
128
|
+
// database connection.
|
|
129
|
+
transaction: false,
|
|
122
130
|
errors: {
|
|
123
131
|
400: z.looseObject({
|
|
124
132
|
error: z.enum([ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY])
|
|
@@ -147,6 +155,10 @@ export const create_account_route_shapes = (options) => [
|
|
|
147
155
|
input: PasswordChangeInput,
|
|
148
156
|
output: PasswordChangeOutput,
|
|
149
157
|
rate_limit: options.login_account_rate_limited ? 'both' : 'ip',
|
|
158
|
+
// The handler opens its own transaction around just the password update
|
|
159
|
+
// and the revocations, so a reservation waiting for a slot, and the
|
|
160
|
+
// Argon2 verify and hash, hold no database connection.
|
|
161
|
+
transaction: false,
|
|
150
162
|
errors: {
|
|
151
163
|
400: z.looseObject({
|
|
152
164
|
error: z.enum([ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY])
|
|
@@ -134,8 +134,13 @@ export interface AccountRouteOptions extends AuthSessionRouteOptions {
|
|
|
134
134
|
* Self-service session/token management is on `auth/account_actions.ts`.
|
|
135
135
|
*
|
|
136
136
|
* `/login` closes the connections of the sessions its cap evicts, and
|
|
137
|
-
* `/logout` and `/password` close the account's, each
|
|
138
|
-
*
|
|
137
|
+
* `/logout` and `/password` close the account's, each once its revocation has
|
|
138
|
+
* committed: `/logout` through the route transaction's post-commit queue
|
|
139
|
+
* (`queue_connection_close`), `/login` and `/password` — which open their own
|
|
140
|
+
* transaction around just their writes — straight after it commits
|
|
141
|
+
* (`close_connections_after_commit`, which logs a failing close rather than
|
|
142
|
+
* failing the committed request), so a later throw can't drop a close a
|
|
143
|
+
* committed revocation owes. All on `deps.connection_closer`.
|
|
139
144
|
*
|
|
140
145
|
* @param deps - stateless capabilities (keyring, password, log, audit, connection_closer)
|
|
141
146
|
* @param options - per-factory configuration (session_options, login_ip_rate_limiter, login_account_rate_limiter, bootstrap_status)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA4B1D,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA4B1D,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIxE,OAAO,EAGN,KAAK,WAAW,EAEhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAOlD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gCAAgC,GAAI,UAAU,oBAAoB,KAAG,SAkFhF,CAAC;AAEH,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACpC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;CAC1C;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAQ/C;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,uBAAuB;IACvC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,uBAAuB;IACnE;;;;;;;OAOG;IACH,qBAAqB,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1C,4FAA4F;IAC5F,0BAA0B,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/C,2FAA2F;IAC3F,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;CAC1C;AAID;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,0BAA0B,GACtC,MAAM,gBAAgB,EACtB,SAAS,mBAAmB,KAC1B,KAAK,CAAC,SAAS,CA+VjB,CAAC"}
|
|
@@ -31,8 +31,10 @@ import { build_account_context, build_request_context, get_request_context, requ
|
|
|
31
31
|
import { ACCOUNT_ID_KEY, CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
32
32
|
import { get_route_input } from '../http/route_spec.js';
|
|
33
33
|
import { get_client_ip } from '../http/client_ip.js';
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
34
|
+
import { rate_limit_ip_key } from '../http/ip_canonical.js';
|
|
35
|
+
import { parse_acting } from '../http/auth_shape.js';
|
|
36
|
+
import { rate_limit_exceeded_response, reserve_optional } from '../rate_limiter.js';
|
|
37
|
+
import { close_connections_after_commit, queue_connection_close } from '../actions/connection_closer.js';
|
|
36
38
|
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INVALID_CREDENTIALS } from '../http/error_schemas.js';
|
|
37
39
|
/**
|
|
38
40
|
* Create the account status route spec.
|
|
@@ -82,10 +84,15 @@ export const create_account_status_route_spec = (options) => ({
|
|
|
82
84
|
// Resolve actor + role_grants when the caller is unambiguous (single-actor
|
|
83
85
|
// account, or supplied `?acting=<uuid>`). On multi-actor accounts
|
|
84
86
|
// without `acting`, fall back to account-only so the frontend can
|
|
85
|
-
// surface a persona picker.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
// surface a persona picker. The selector goes through the `ActingActor`
|
|
88
|
+
// schema, lowercased; a malformed value is a missing persona —
|
|
89
|
+
// account-only, never a default-actor resolve (as on the Rust spine).
|
|
90
|
+
const acting_raw = c.req.query('acting');
|
|
91
|
+
const acting = parse_acting(acting_raw);
|
|
92
|
+
const acting_result = acting_raw !== undefined && acting === undefined
|
|
93
|
+
? null
|
|
94
|
+
: await resolve_acting_actor(route, account_id, acting);
|
|
95
|
+
if (acting_result?.ok) {
|
|
89
96
|
const ctx = await build_request_context(route, account_id, acting_result.actor_id);
|
|
90
97
|
if (ctx) {
|
|
91
98
|
const role_grants = ctx.role_grants.map((p) => ({
|
|
@@ -156,8 +163,13 @@ const login_fail_delay = (floor_ms, jitter_ms) => {
|
|
|
156
163
|
* Self-service session/token management is on `auth/account_actions.ts`.
|
|
157
164
|
*
|
|
158
165
|
* `/login` closes the connections of the sessions its cap evicts, and
|
|
159
|
-
* `/logout` and `/password` close the account's, each
|
|
160
|
-
*
|
|
166
|
+
* `/logout` and `/password` close the account's, each once its revocation has
|
|
167
|
+
* committed: `/logout` through the route transaction's post-commit queue
|
|
168
|
+
* (`queue_connection_close`), `/login` and `/password` — which open their own
|
|
169
|
+
* transaction around just their writes — straight after it commits
|
|
170
|
+
* (`close_connections_after_commit`, which logs a failing close rather than
|
|
171
|
+
* failing the committed request), so a later throw can't drop a close a
|
|
172
|
+
* committed revocation owes. All on `deps.connection_closer`.
|
|
161
173
|
*
|
|
162
174
|
* @param deps - stateless capabilities (keyring, password, log, audit, connection_closer)
|
|
163
175
|
* @param options - per-factory configuration (session_options, login_ip_rate_limiter, login_account_rate_limiter, bootstrap_status)
|
|
@@ -185,99 +197,122 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
185
197
|
{
|
|
186
198
|
...login_shape,
|
|
187
199
|
handler: async (c, route) => {
|
|
188
|
-
// Per-IP rate limit
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
200
|
+
// Per-IP rate limit (before any DB/password work). Reserved, not
|
|
201
|
+
// checked: the attempt counts against the cap from here until it
|
|
202
|
+
// settles, so a concurrent burst can't all pass before any of it
|
|
203
|
+
// records. A key at its cap only because of attempts in flight
|
|
204
|
+
// waits briefly for a slot (`RESERVATION_WAIT_MAX_MS`) rather than
|
|
205
|
+
// refusing a burst of correct logins outright. Order: IP, then the
|
|
206
|
+
// lookup, then the account (its key is the looked-up id). A wait on
|
|
207
|
+
// the account bucket keeps the IP slot: it belongs to the same
|
|
208
|
+
// address, the wait is bounded, and giving it up would let that
|
|
209
|
+
// address exceed its cap meanwhile. The `finally` releases whatever
|
|
210
|
+
// no branch recorded.
|
|
211
|
+
// keyed by the address's rate-limit key (an IPv6 client's /64)
|
|
212
|
+
const ip_key = login_ip_rate_limiter ? rate_limit_ip_key(get_client_ip(c)) : null;
|
|
213
|
+
const ip_reserved = await reserve_optional(login_ip_rate_limiter, ip_key, {
|
|
214
|
+
signal: c.req.raw.signal
|
|
215
|
+
});
|
|
216
|
+
if (!ip_reserved.ok) {
|
|
217
|
+
return rate_limit_exceeded_response(c, ip_reserved.retry_after);
|
|
195
218
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
219
|
+
let account_reservation = null;
|
|
220
|
+
try {
|
|
221
|
+
// `UsernameProvided` canonicalizes via `.trim().toLowerCase()` at
|
|
222
|
+
// parse time — the validated value lands canonical in
|
|
223
|
+
// `c.var.validated_input`, so the per-account rate-limit key,
|
|
224
|
+
// DB lookup, and audit metadata see one form. See
|
|
225
|
+
// `primitive_schemas.ts` for the schema-layer canonicalization.
|
|
226
|
+
const { username, password: pw } = get_route_input(c);
|
|
227
|
+
// DB lookup first so we can key the per-account rate limit by a canonical value
|
|
228
|
+
// (account.id) rather than the submitted identifier. Otherwise an attacker could
|
|
229
|
+
// alternate between username and email to double the per-account bucket.
|
|
230
|
+
const account = await query_account_by_username_or_email(route, username);
|
|
231
|
+
const account_rate_key = account ? account.id : username;
|
|
232
|
+
// Per-account rate limit (after DB lookup so the key is canonical)
|
|
233
|
+
const account_reserved = await reserve_optional(login_account_rate_limiter, account_rate_key, { signal: c.req.raw.signal });
|
|
234
|
+
if (!account_reserved.ok) {
|
|
235
|
+
return rate_limit_exceeded_response(c, account_reserved.retry_after);
|
|
212
236
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
account_reservation = account_reserved.reservation;
|
|
238
|
+
// Start the minimum-delay timer concurrently with failure work.
|
|
239
|
+
// Observed response time is max(work, delay) so all 401 paths
|
|
240
|
+
// (found-wrong-pw, not-found) return in similar time.
|
|
241
|
+
const delay = login_fail_delay(login_fail_floor_ms, login_fail_jitter_ms);
|
|
242
|
+
if (!account) {
|
|
243
|
+
// enumeration prevention: verify_dummy equalizes Argon2id timing;
|
|
244
|
+
// login_fail_delay equalizes every other path difference.
|
|
245
|
+
await password.verify_dummy(pw);
|
|
246
|
+
ip_reserved.reservation.record();
|
|
247
|
+
account_reservation?.record();
|
|
248
|
+
deps.audit.emit(route, {
|
|
249
|
+
event_type: 'login',
|
|
250
|
+
outcome: 'failure',
|
|
251
|
+
ip: get_client_ip(c),
|
|
252
|
+
metadata: { username }
|
|
253
|
+
});
|
|
254
|
+
await delay;
|
|
255
|
+
return c.json({ error: ERROR_INVALID_CREDENTIALS }, 401);
|
|
256
|
+
}
|
|
257
|
+
const valid = await password.verify_password(pw, account.password_hash);
|
|
258
|
+
if (!valid) {
|
|
259
|
+
ip_reserved.reservation.record();
|
|
260
|
+
account_reservation?.record();
|
|
261
|
+
deps.audit.emit(route, {
|
|
262
|
+
event_type: 'login',
|
|
263
|
+
outcome: 'failure',
|
|
264
|
+
account_id: account.id,
|
|
265
|
+
ip: get_client_ip(c),
|
|
266
|
+
metadata: { username }
|
|
267
|
+
});
|
|
268
|
+
await delay;
|
|
269
|
+
return c.json({ error: ERROR_INVALID_CREDENTIALS }, 401);
|
|
270
|
+
}
|
|
271
|
+
// Successful login — clear the account-grain failure history only.
|
|
272
|
+
// The IP aggregate is deliberately left standing: it is the
|
|
273
|
+
// distributed-spray backstop, so a success on one account must not
|
|
274
|
+
// refund the budget shared by every other account reachable from
|
|
275
|
+
// that address. See `RateLimiter.reset`. A success records nothing:
|
|
276
|
+
// both reservations are released.
|
|
277
|
+
ip_reserved.reservation.release();
|
|
278
|
+
account_reservation?.release();
|
|
239
279
|
if (login_account_rate_limiter)
|
|
240
|
-
login_account_rate_limiter.
|
|
280
|
+
login_account_rate_limiter.reset(account_rate_key);
|
|
281
|
+
// The session INSERT and the cap eviction are the route's only
|
|
282
|
+
// writes, and only atomic together: one transaction around just
|
|
283
|
+
// them (the route is `transaction: false`, so the reservation
|
|
284
|
+
// waits above and the Argon2 verify held no connection). Twin of
|
|
285
|
+
// the Rust login's transaction around `create_capped_session`.
|
|
286
|
+
const evicted = await route.db.transaction((tx) => create_session_and_set_cookie({
|
|
287
|
+
keyring,
|
|
288
|
+
deps: { db: tx },
|
|
289
|
+
c,
|
|
290
|
+
account_id: account.id,
|
|
291
|
+
session_options,
|
|
292
|
+
max_sessions
|
|
293
|
+
}));
|
|
294
|
+
// The sessions the cap evicted are gone from the table, but not
|
|
295
|
+
// from the connections they opened — a WebSocket keeps the
|
|
296
|
+
// authority it resolved at upgrade, and an SSE stream never
|
|
297
|
+
// rechecks. Close them now the eviction has committed — inline, not
|
|
298
|
+
// queued: with no route transaction, a throw later in the handler
|
|
299
|
+
// would drop a queued close while the eviction stands. No audit
|
|
300
|
+
// row: there is no eviction event, and the `login` row records
|
|
301
|
+
// the cause.
|
|
302
|
+
for (const session_token_hash of evicted) {
|
|
303
|
+
close_connections_after_commit(connection_closer, { kind: 'session', session_token_hash }, deps.log);
|
|
304
|
+
}
|
|
241
305
|
deps.audit.emit(route, {
|
|
242
306
|
event_type: 'login',
|
|
243
|
-
outcome: 'failure',
|
|
244
307
|
account_id: account.id,
|
|
245
|
-
ip: get_client_ip(c)
|
|
246
|
-
metadata: { username }
|
|
308
|
+
ip: get_client_ip(c)
|
|
247
309
|
});
|
|
248
|
-
|
|
249
|
-
return c.json({ error: ERROR_INVALID_CREDENTIALS }, 401);
|
|
310
|
+
return c.json({ ok: true });
|
|
250
311
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// refund the budget shared by every other account reachable from
|
|
255
|
-
// that address. See `RateLimiter.reset`.
|
|
256
|
-
if (login_account_rate_limiter)
|
|
257
|
-
login_account_rate_limiter.reset(account_rate_key);
|
|
258
|
-
const evicted = await create_session_and_set_cookie({
|
|
259
|
-
keyring,
|
|
260
|
-
deps: route,
|
|
261
|
-
c,
|
|
262
|
-
account_id: account.id,
|
|
263
|
-
session_options,
|
|
264
|
-
max_sessions
|
|
265
|
-
});
|
|
266
|
-
// The sessions the cap evicted are gone from the table, but not
|
|
267
|
-
// from the connections they opened — a WebSocket keeps the
|
|
268
|
-
// authority it resolved at upgrade, and an SSE stream never
|
|
269
|
-
// rechecks. Close them once the eviction has committed. No audit
|
|
270
|
-
// row: there is no eviction event, and the `login` row records
|
|
271
|
-
// the cause.
|
|
272
|
-
for (const session_token_hash of evicted) {
|
|
273
|
-
queue_connection_close(route, connection_closer, { kind: 'session', session_token_hash });
|
|
312
|
+
finally {
|
|
313
|
+
ip_reserved.reservation.release();
|
|
314
|
+
account_reservation?.release();
|
|
274
315
|
}
|
|
275
|
-
deps.audit.emit(route, {
|
|
276
|
-
event_type: 'login',
|
|
277
|
-
account_id: account.id,
|
|
278
|
-
ip: get_client_ip(c)
|
|
279
|
-
});
|
|
280
|
-
return c.json({ ok: true });
|
|
281
316
|
}
|
|
282
317
|
},
|
|
283
318
|
{
|
|
@@ -316,111 +351,129 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
316
351
|
{
|
|
317
352
|
...password_shape,
|
|
318
353
|
handler: async (c, route) => {
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
354
|
+
// Per-IP rate limit (before argon2 work). Reserved, not checked:
|
|
355
|
+
// the attempt counts against the shared login limiters from here
|
|
356
|
+
// until it settles, so a concurrent burst of guesses at
|
|
357
|
+
// `current_password` can't all pass before any of it records. The
|
|
358
|
+
// `finally` releases whatever no branch recorded.
|
|
359
|
+
// keyed by the address's rate-limit key (an IPv6 client's /64)
|
|
360
|
+
const ip_key = login_ip_rate_limiter ? rate_limit_ip_key(get_client_ip(c)) : null;
|
|
361
|
+
const ip_reserved = await reserve_optional(login_ip_rate_limiter, ip_key, {
|
|
362
|
+
signal: c.req.raw.signal
|
|
363
|
+
});
|
|
364
|
+
if (!ip_reserved.ok) {
|
|
365
|
+
return rate_limit_exceeded_response(c, ip_reserved.retry_after);
|
|
326
366
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
367
|
+
let account_reservation = null;
|
|
368
|
+
try {
|
|
369
|
+
const ctx = require_request_context(c);
|
|
370
|
+
const { current_password, new_password } = get_route_input(c);
|
|
371
|
+
// Defense in depth — see `docs/security.md` §Credential-channel gating.
|
|
372
|
+
const credential_type = c.get(CREDENTIAL_TYPE_KEY) ?? undefined;
|
|
373
|
+
// per-account rate limit (after context resolution, before argon2 work)
|
|
374
|
+
const account_reserved = await reserve_optional(login_account_rate_limiter, ctx.account.id, { signal: c.req.raw.signal });
|
|
375
|
+
if (!account_reserved.ok) {
|
|
376
|
+
return rate_limit_exceeded_response(c, account_reserved.retry_after);
|
|
336
377
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
378
|
+
account_reservation = account_reserved.reservation;
|
|
379
|
+
const valid = await password.verify_password(current_password, ctx.account.password_hash);
|
|
380
|
+
if (!valid) {
|
|
381
|
+
ip_reserved.reservation.record();
|
|
382
|
+
account_reservation?.record();
|
|
383
|
+
deps.audit.emit(route, {
|
|
384
|
+
event_type: 'password_change',
|
|
385
|
+
outcome: 'failure',
|
|
386
|
+
account_id: ctx.account.id,
|
|
387
|
+
ip: get_client_ip(c),
|
|
388
|
+
metadata: { credential_type }
|
|
389
|
+
});
|
|
390
|
+
return c.json({ error: ERROR_INVALID_CREDENTIALS }, 401);
|
|
391
|
+
}
|
|
392
|
+
// Verify succeeded — do the throw-y operations FIRST so a fault
|
|
393
|
+
// (Argon2 OOM, native binding error, DB outage on the UPDATE)
|
|
394
|
+
// can't wipe the rate-limit history of a caller observing 500s.
|
|
395
|
+
// Resets happen below, after both calls have settled.
|
|
396
|
+
const new_hash = await password.hash_password(new_password);
|
|
397
|
+
// Conditional UPDATE keyed on the verified hash: closes the
|
|
398
|
+
// verify-write race with a concurrent password change that
|
|
399
|
+
// already committed against the same starting hash. Account-grain
|
|
400
|
+
// operation — `updated_by` stays null (the per-request actor is
|
|
401
|
+
// incidental; password is account-level state).
|
|
402
|
+
//
|
|
403
|
+
// The UPDATE and the two revocations are the route's writes, and
|
|
404
|
+
// commit together or not at all: one transaction around just them
|
|
405
|
+
// (the route is `transaction: false`, so the reservation waits
|
|
406
|
+
// above and the Argon2 verify and hash held no connection). Twin of
|
|
407
|
+
// the Rust spine's `change_password`. `null` when the UPDATE matched
|
|
408
|
+
// nothing — a concurrent change committed first.
|
|
409
|
+
const change = await route.db.transaction(async (tx) => {
|
|
410
|
+
const tx_deps = { db: tx };
|
|
411
|
+
const updated = await query_update_account_password(tx_deps, ctx.account.id, new_hash, null, ctx.account.password_hash);
|
|
412
|
+
if (!updated)
|
|
413
|
+
return null;
|
|
414
|
+
// revoke all sessions and API tokens (force re-auth everywhere)
|
|
415
|
+
const sessions_revoked = await query_session_revoke_all_for_account(tx_deps, ctx.account.id);
|
|
416
|
+
const tokens_revoked = await query_revoke_all_api_tokens_for_account(tx_deps, ctx.account.id);
|
|
417
|
+
return { sessions_revoked, tokens_revoked };
|
|
350
418
|
});
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const updated = await query_update_account_password(route, ctx.account.id, new_hash, null, ctx.account.password_hash);
|
|
364
|
-
// Verify-success contract — the caller proved knowledge, so wipe
|
|
365
|
-
// their account-grain failure history. The race-loser branch below
|
|
366
|
-
// re-records one on top of the wiped slate so net cost stays 1 on
|
|
367
|
-
// that bucket. Deferring from "after verify" to "after UPDATE
|
|
368
|
-
// settled" is what closes the S1 bypass — a throw between reset
|
|
369
|
-
// and the UPDATE could have wiped an attacker's budget.
|
|
370
|
-
//
|
|
371
|
-
// The IP aggregate is deliberately not cleared: it is the same
|
|
372
|
-
// instance login uses, so refunding it here reopens the spray path
|
|
373
|
-
// through a route that only needs the caller's *own* password. Its
|
|
374
|
-
// record on the race-loser branch below therefore accumulates
|
|
375
|
-
// rather than netting out, which is the intent.
|
|
376
|
-
if (login_account_rate_limiter)
|
|
377
|
-
login_account_rate_limiter.reset(ctx.account.id);
|
|
378
|
-
if (!updated) {
|
|
379
|
-
// A concurrent password change committed first — our
|
|
380
|
-
// `current_password` was correct at read-time but the row's
|
|
381
|
-
// `password_hash` no longer matches. Mirrors the wrong-password
|
|
382
|
-
// 401 shape; tag the failure metadata so admins reading the
|
|
383
|
-
// audit log can distinguish "user typoed" from "two clients
|
|
384
|
-
// raced." Sessions/tokens were already revoked by the winner;
|
|
385
|
-
// no cookie clear here either.
|
|
386
|
-
if (login_ip_rate_limiter && ip)
|
|
387
|
-
login_ip_rate_limiter.record(ip);
|
|
419
|
+
// Verify-success contract — the caller proved knowledge, so wipe
|
|
420
|
+
// their account-grain failure history. The race-loser branch below
|
|
421
|
+
// re-records one on top of the wiped slate so net cost stays 1 on
|
|
422
|
+
// that bucket. Deferring from "after verify" to "after UPDATE
|
|
423
|
+
// settled" is what closes the S1 bypass — a throw between reset
|
|
424
|
+
// and the UPDATE could have wiped an attacker's budget.
|
|
425
|
+
//
|
|
426
|
+
// The IP aggregate is deliberately not cleared: it is the same
|
|
427
|
+
// instance login uses, so refunding it here reopens the spray path
|
|
428
|
+
// through a route that only needs the caller's *own* password. Its
|
|
429
|
+
// record on the race-loser branch below therefore accumulates
|
|
430
|
+
// rather than netting out, which is the intent.
|
|
388
431
|
if (login_account_rate_limiter)
|
|
389
|
-
login_account_rate_limiter.
|
|
432
|
+
login_account_rate_limiter.reset(ctx.account.id);
|
|
433
|
+
if (!change) {
|
|
434
|
+
// A concurrent password change committed first — our
|
|
435
|
+
// `current_password` was correct at read-time but the row's
|
|
436
|
+
// `password_hash` no longer matches. Mirrors the wrong-password
|
|
437
|
+
// 401 shape; tag the failure metadata so admins reading the
|
|
438
|
+
// audit log can distinguish "user typoed" from "two clients
|
|
439
|
+
// raced." Sessions/tokens were already revoked by the winner;
|
|
440
|
+
// no cookie clear here either.
|
|
441
|
+
ip_reserved.reservation.record();
|
|
442
|
+
account_reservation?.record();
|
|
443
|
+
deps.audit.emit(route, {
|
|
444
|
+
event_type: 'password_change',
|
|
445
|
+
outcome: 'failure',
|
|
446
|
+
account_id: ctx.account.id,
|
|
447
|
+
ip: get_client_ip(c),
|
|
448
|
+
metadata: { reason: 'concurrent_change', credential_type }
|
|
449
|
+
});
|
|
450
|
+
return c.json({ error: ERROR_INVALID_CREDENTIALS }, 401);
|
|
451
|
+
}
|
|
452
|
+
const { sessions_revoked, tokens_revoked } = change;
|
|
453
|
+
// Close every live connection of this account now the password
|
|
454
|
+
// update and the revoke-all cascade have committed — inline, not
|
|
455
|
+
// queued: with no route transaction, a throw later in the handler
|
|
456
|
+
// would drop a queued close while the revocation stands. It does
|
|
457
|
+
// not depend on the fail-open audit write below; the audit
|
|
458
|
+
// listeners repeat it on the announced `password_change` row.
|
|
459
|
+
close_connections_after_commit(connection_closer, { kind: 'account', account_id: ctx.account.id }, deps.log);
|
|
460
|
+
clear_session_cookie(c, session_options);
|
|
461
|
+
// Account-grain operation — no `actor_id`. The password is
|
|
462
|
+
// account-level state; which per-request actor was resolved
|
|
463
|
+
// has no semantic bearing on "this account changed its
|
|
464
|
+
// password". Mirrors `login`/`logout`.
|
|
390
465
|
deps.audit.emit(route, {
|
|
391
466
|
event_type: 'password_change',
|
|
392
|
-
outcome: 'failure',
|
|
393
467
|
account_id: ctx.account.id,
|
|
394
468
|
ip: get_client_ip(c),
|
|
395
|
-
metadata: {
|
|
469
|
+
metadata: { sessions_revoked, tokens_revoked, credential_type }
|
|
396
470
|
});
|
|
397
|
-
return c.json({
|
|
471
|
+
return c.json({ ok: true, sessions_revoked, tokens_revoked });
|
|
472
|
+
}
|
|
473
|
+
finally {
|
|
474
|
+
ip_reserved.reservation.release();
|
|
475
|
+
account_reservation?.release();
|
|
398
476
|
}
|
|
399
|
-
// revoke all sessions and API tokens (force re-auth everywhere)
|
|
400
|
-
const sessions_revoked = await query_session_revoke_all_for_account(route, ctx.account.id);
|
|
401
|
-
const tokens_revoked = await query_revoke_all_api_tokens_for_account(route, ctx.account.id);
|
|
402
|
-
// Close every live connection of this account once the password
|
|
403
|
-
// update and the revoke-all cascade have committed — this route
|
|
404
|
-
// runs with the default `transaction: true`, and the queued close
|
|
405
|
-
// never runs before that commit or on rollback. It does not
|
|
406
|
-
// depend on the fail-open audit write below; the audit listeners
|
|
407
|
-
// repeat it on the announced `password_change` row.
|
|
408
|
-
queue_connection_close(route, connection_closer, {
|
|
409
|
-
kind: 'account',
|
|
410
|
-
account_id: ctx.account.id
|
|
411
|
-
});
|
|
412
|
-
clear_session_cookie(c, session_options);
|
|
413
|
-
// Account-grain operation — no `actor_id`. The password is
|
|
414
|
-
// account-level state; which per-request actor was resolved
|
|
415
|
-
// has no semantic bearing on "this account changed its
|
|
416
|
-
// password". Mirrors `login`/`logout`.
|
|
417
|
-
deps.audit.emit(route, {
|
|
418
|
-
event_type: 'password_change',
|
|
419
|
-
account_id: ctx.account.id,
|
|
420
|
-
ip: get_client_ip(c),
|
|
421
|
-
metadata: { sessions_revoked, tokens_revoked, credential_type }
|
|
422
|
-
});
|
|
423
|
-
return c.json({ ok: true, sessions_revoked, tokens_revoked });
|
|
424
477
|
}
|
|
425
478
|
}
|
|
426
479
|
];
|