@fuzdev/fuz_app 0.106.1 → 0.108.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 +42 -23
- package/dist/actions/action_bridge.d.ts +34 -3
- package/dist/actions/action_bridge.d.ts.map +1 -1
- package/dist/actions/action_bridge.js +32 -1
- package/dist/actions/action_rpc.d.ts +5 -4
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +4 -3
- package/dist/actions/action_spec.d.ts +8 -2
- package/dist/actions/action_spec.d.ts.map +1 -1
- package/dist/actions/action_spec.js +5 -2
- package/dist/actions/compile_action_registry.d.ts +26 -14
- package/dist/actions/compile_action_registry.d.ts.map +1 -1
- package/dist/actions/compile_action_registry.js +29 -14
- package/dist/actions/perform_action.d.ts +26 -16
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +69 -44
- package/dist/actions/register_action_ws.d.ts +4 -4
- package/dist/actions/register_action_ws.js +5 -5
- package/dist/actions/register_ws_endpoint.d.ts +13 -5
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +15 -6
- package/dist/auth/CLAUDE.md +16 -9
- package/dist/auth/account_route_schema.d.ts +10 -1
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +10 -1
- package/dist/auth/account_routes.d.ts +18 -4
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +32 -29
- package/dist/auth/api_token_queries.d.ts +4 -0
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +5 -1
- package/dist/auth/audit_log_route_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_route_schema.js +11 -1
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +4 -9
- package/dist/auth/auth_guard_resolver.d.ts +48 -21
- package/dist/auth/auth_guard_resolver.d.ts.map +1 -1
- package/dist/auth/auth_guard_resolver.js +60 -26
- package/dist/auth/bearer_auth.d.ts +10 -7
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +13 -22
- package/dist/auth/bootstrap_routes.d.ts +12 -2
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +15 -9
- package/dist/auth/middleware.d.ts +0 -3
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +11 -10
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +24 -0
- package/dist/auth/request_context.d.ts +54 -24
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +117 -39
- package/dist/auth/session_queries.d.ts +13 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +14 -3
- package/dist/auth/signup_routes.d.ts +10 -0
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +14 -11
- package/dist/auth/token_scope.d.ts +109 -21
- package/dist/auth/token_scope.d.ts.map +1 -1
- package/dist/auth/token_scope.js +106 -21
- package/dist/http/CLAUDE.md +23 -11
- package/dist/http/auth_shape.d.ts +58 -3
- package/dist/http/auth_shape.d.ts.map +1 -1
- package/dist/http/auth_shape.js +63 -5
- package/dist/http/error_schemas.d.ts +25 -2
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +37 -15
- package/dist/http/route_spec.d.ts +29 -26
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +12 -16
- package/dist/http/surface_query.d.ts +2 -2
- package/dist/http/surface_query.js +2 -2
- package/dist/rate_limiter.d.ts +36 -3
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +36 -3
- package/dist/server/app_server.d.ts +27 -10
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +30 -20
- package/dist/server/app_server_context.d.ts +10 -2
- package/dist/server/app_server_context.d.ts.map +1 -1
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +12 -10
- package/dist/testing/CLAUDE.md +60 -11
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +6 -4
- package/dist/testing/cross_backend/conformance_case.js +1 -1
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +9 -8
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +59 -0
- package/dist/testing/cross_backend/session_cap.d.ts +19 -0
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -0
- package/dist/testing/cross_backend/session_cap.js +93 -0
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +6 -5
- package/dist/testing/cross_backend/token_scope_surface.d.ts +9 -0
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -0
- package/dist/testing/cross_backend/token_scope_surface.js +170 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +3 -2
- package/dist/testing/middleware.d.ts +2 -5
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +5 -5
- package/dist/testing/rate_limiting.d.ts +10 -16
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +11 -95
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +4 -2
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +3 -1
- package/dist/testing/testing_rate_limiter.d.ts +7 -1
- package/dist/testing/testing_rate_limiter.d.ts.map +1 -1
- package/dist/testing/testing_rate_limiter.js +7 -1
- package/package.json +1 -1
|
@@ -82,6 +82,10 @@ export declare const query_api_token_list_for_account: (deps: QueryDeps, account
|
|
|
82
82
|
* `(account_id, credential_kind)` before count/evict/insert — see the matching
|
|
83
83
|
* note on `query_session_enforce_limit`.
|
|
84
84
|
*
|
|
85
|
+
* Ordering carries the same `id DESC` stability tie-breaker, with the same
|
|
86
|
+
* caveat, as `query_session_enforce_limit` — deterministic survivors under a
|
|
87
|
+
* `created_at` tie, not a guarantee that the row just inserted survives.
|
|
88
|
+
*
|
|
85
89
|
* @param deps - query dependencies (must be transaction-scoped)
|
|
86
90
|
* @param account_id - the account to enforce the limit for
|
|
87
91
|
* @param max_tokens - maximum number of tokens to keep
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api_token_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/api_token_queries.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,KAAK,UAAU,EAAmD,MAAM,kBAAkB,CAAC;AAEpG,yEAAyE;AACzE,MAAM,WAAW,iBAAkB,SAAQ,SAAS;IACnD,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,SAAS,EACf,IAAI,MAAM,EACV,YAAY,MAAM,EAClB,MAAM,MAAM,EACZ,YAAY,MAAM,EAClB,OAAO,UAAU,EACjB,aAAa,IAAI,GAAG,IAAI,KACtB,OAAO,CAAC,QAAQ,CAelB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,iBAAiB,EACvB,WAAW,MAAM,EACjB,IAAI,MAAM,GAAG,SAAS,EACtB,iBAAiB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,KAC/C,OAAO,CAAC,QAAQ,GAAG,SAAS,CAgC9B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,uCAAuC,GACnD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kCAAkC,GAC9C,MAAM,SAAS,EACf,IAAI,MAAM,EACV,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAM7C,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"api_token_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/api_token_queries.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,KAAK,UAAU,EAAmD,MAAM,kBAAkB,CAAC;AAEpG,yEAAyE;AACzE,MAAM,WAAW,iBAAkB,SAAQ,SAAS;IACnD,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,SAAS,EACf,IAAI,MAAM,EACV,YAAY,MAAM,EAClB,MAAM,MAAM,EACZ,YAAY,MAAM,EAClB,OAAO,UAAU,EACjB,aAAa,IAAI,GAAG,IAAI,KACtB,OAAO,CAAC,QAAQ,CAelB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,iBAAiB,EACvB,WAAW,MAAM,EACjB,IAAI,MAAM,GAAG,SAAS,EACtB,iBAAiB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,KAC/C,OAAO,CAAC,QAAQ,GAAG,SAAS,CAgC9B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,uCAAuC,GACnD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kCAAkC,GAC9C,MAAM,SAAS,EACf,IAAI,MAAM,EACV,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAM7C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAYhB,CAAC"}
|
|
@@ -125,6 +125,10 @@ export const query_api_token_list_for_account = async (deps, account_id) => {
|
|
|
125
125
|
* `(account_id, credential_kind)` before count/evict/insert — see the matching
|
|
126
126
|
* note on `query_session_enforce_limit`.
|
|
127
127
|
*
|
|
128
|
+
* Ordering carries the same `id DESC` stability tie-breaker, with the same
|
|
129
|
+
* caveat, as `query_session_enforce_limit` — deterministic survivors under a
|
|
130
|
+
* `created_at` tie, not a guarantee that the row just inserted survives.
|
|
131
|
+
*
|
|
128
132
|
* @param deps - query dependencies (must be transaction-scoped)
|
|
129
133
|
* @param account_id - the account to enforce the limit for
|
|
130
134
|
* @param max_tokens - maximum number of tokens to keep
|
|
@@ -136,7 +140,7 @@ export const query_api_token_enforce_limit = async (deps, account_id, max_tokens
|
|
|
136
140
|
WHERE id IN (
|
|
137
141
|
SELECT id FROM api_token
|
|
138
142
|
WHERE account_id = $1
|
|
139
|
-
ORDER BY created_at DESC
|
|
143
|
+
ORDER BY created_at DESC, id DESC
|
|
140
144
|
OFFSET $2
|
|
141
145
|
) RETURNING id`, [account_id, max_tokens]);
|
|
142
146
|
return rows.length;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit_log_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAGvD,2FAA2F;AAC3F,eAAO,MAAM,gBAAgB;;kBAA0C,CAAC;AACxE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE,+DAA+D;AAC/D,eAAO,MAAM,yBAAyB,UAAU,CAAC;AAEjD;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GACxC,gBAAe,MAAkC,KAC/C,IAAI,CAAC,SAAS,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"audit_log_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAGvD,2FAA2F;AAC3F,eAAO,MAAM,gBAAgB;;kBAA0C,CAAC;AACxE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE,+DAA+D;AAC/D,eAAO,MAAM,yBAAyB,UAAU,CAAC;AAEjD;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GACxC,gBAAe,MAAkC,KAC/C,IAAI,CAAC,SAAS,EAAE,SAAS,CAkB1B,CAAC"}
|
|
@@ -28,7 +28,17 @@ export const DEFAULT_AUDIT_STREAM_ROLE = 'admin';
|
|
|
28
28
|
export const create_audit_log_route_shape = (required_role = DEFAULT_AUDIT_STREAM_ROLE) => ({
|
|
29
29
|
method: 'GET',
|
|
30
30
|
path: '/audit/stream',
|
|
31
|
-
auth: {
|
|
31
|
+
auth: {
|
|
32
|
+
account: 'required',
|
|
33
|
+
actor: 'required',
|
|
34
|
+
roles: [required_role],
|
|
35
|
+
// Rule 3 — a narrowed token holds no audit feed. Same reasoning as the
|
|
36
|
+
// WS upgrade: this is a long-lived server→client stream whose contents
|
|
37
|
+
// are decided by the account's role, not by any per-message scope
|
|
38
|
+
// check, so there is no point after open at which a narrowing could
|
|
39
|
+
// apply.
|
|
40
|
+
required_scope: 'surface:audit_stream'
|
|
41
|
+
},
|
|
32
42
|
description: 'Subscribe to realtime audit log events',
|
|
33
43
|
query: AuditStreamQuery,
|
|
34
44
|
input: z.null(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit_log_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,EAAuB,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC/F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"audit_log_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,EAAuB,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC/F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAI3E,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACpC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,MAAM,CAAC,EAAE;QACR,SAAS,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;QAC1F,GAAG,EAAE,MAAM,CAAC;KACZ,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GAAI,UAAU,oBAAoB,KAAG,KAAK,CAAC,SAAS,CA4B5F,CAAC"}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { create_audit_log_route_shape } from './audit_log_route_schema.js';
|
|
15
15
|
import { create_sse_response } from '../realtime/sse.js';
|
|
16
|
-
import { AUTH_SESSION_TOKEN_HASH_KEY, require_request_context
|
|
16
|
+
import { AUTH_SESSION_TOKEN_HASH_KEY, require_request_context } from './request_context.js';
|
|
17
17
|
import { AUDIT_LOG_CHANNEL } from '../realtime/sse_auth_guard.js';
|
|
18
18
|
/**
|
|
19
19
|
* Create the optional audit-log SSE route spec.
|
|
@@ -32,14 +32,9 @@ export const create_audit_log_route_specs = (options) => {
|
|
|
32
32
|
{
|
|
33
33
|
...create_audit_log_route_shape(options.required_role),
|
|
34
34
|
handler: (c) => {
|
|
35
|
-
// Rule 3
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// per-message scope check, so there is no point after open at
|
|
39
|
-
// which a narrowing could apply.
|
|
40
|
-
const denied = token_scope_surface_denial(c, 'audit_stream');
|
|
41
|
-
if (denied)
|
|
42
|
-
return denied;
|
|
35
|
+
// Rule 3 is declared on the route shape's `auth.required_scope`,
|
|
36
|
+
// so a narrowed token is refused ahead of the role gate rather
|
|
37
|
+
// than here.
|
|
43
38
|
const ctx = require_request_context(c);
|
|
44
39
|
// scope = session hash (capped → tabs-per-session limit and
|
|
45
40
|
// session-specific `session_revoke` close). groups = [account_id]
|
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth guard resolver for the route spec system.
|
|
3
3
|
*
|
|
4
|
-
* Maps the
|
|
5
|
-
* `credential_types`) to two-phase middleware sets that
|
|
4
|
+
* Maps the five-axis `RouteAuth` (`account` / `actor` / `roles` /
|
|
5
|
+
* `credential_types` / `required_scope`) to two-phase middleware sets that
|
|
6
6
|
* `apply_route_specs` weaves into the per-route pipeline:
|
|
7
7
|
*
|
|
8
|
-
* - `
|
|
9
|
-
*
|
|
10
|
-
* 'required'`
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
8
|
+
* - `pre_authorization` runs before the authorization phase, and holds every
|
|
9
|
+
* gate that reads only what the auth middleware already set: `require_auth`
|
|
10
|
+
* whenever `auth.account === 'required'` or `auth.actor === 'required'`
|
|
11
|
+
* (per registry-time invariant 3, `actor: 'required'` today implies a
|
|
12
|
+
* credential — accountless actors are out of scope for v1),
|
|
13
|
+
* `require_credential_types(types)` whenever `auth.credential_types?.length`,
|
|
14
|
+
* and the token-scope guard whenever `auth.required_scope` names a capability.
|
|
15
|
+
* All three fire before any body parsing, so a caller they refuse never sees
|
|
16
|
+
* route-shape information — and before the authorization phase, so a wrong
|
|
17
|
+
* channel costs no actor resolution.
|
|
18
|
+
* - `post_authorization` runs after the dispatcher's authorization phase has
|
|
19
|
+
* populated `RequestContext`, and holds the one gate that needs it:
|
|
20
|
+
* `require_role(roles)`, whenever `auth.roles?.length`, reads
|
|
21
|
+
* `c.var.request_context.role_grants`.
|
|
22
|
+
*
|
|
23
|
+
* The resulting order — credential presence, then credential type, then scope,
|
|
24
|
+
* then role — is the spine's canonical coarse-to-fine ordering, the same one
|
|
25
|
+
* `check_action_auth_post_authorization` runs for actions. A route declaring
|
|
26
|
+
* both a credential gate and a surface gate therefore answers with the
|
|
27
|
+
* coarser fact ("this channel may never call me") rather than the finer one
|
|
28
|
+
* ("your token is scoped"), on both dispatch surfaces alike.
|
|
19
29
|
*
|
|
20
30
|
* Public routes (`auth.account === 'none' && auth.actor === 'none'`)
|
|
21
31
|
* yield empty guard arrays. `'optional'` axes contribute no
|
|
22
|
-
* pre-
|
|
32
|
+
* pre-authorization 401; the authorization phase sets `RequestContext`
|
|
23
33
|
* to whatever the credential supports and the post-authorization
|
|
24
34
|
* gates decide whether the actor's role_grants / credential type match.
|
|
25
35
|
*
|
|
@@ -29,16 +39,33 @@ import type { AuthGuardResolver } from '../http/route_spec.js';
|
|
|
29
39
|
/**
|
|
30
40
|
* Standard auth guard resolver for fuz_app.
|
|
31
41
|
*
|
|
32
|
-
* Reads each axis of the
|
|
33
|
-
*
|
|
42
|
+
* Reads each axis of the `RouteAuth` shape and emits the corresponding
|
|
43
|
+
* middleware:
|
|
34
44
|
*
|
|
35
|
-
* - `account === 'required'` or `actor === 'required'` → pre-
|
|
45
|
+
* - `account === 'required'` or `actor === 'required'` → pre-authorization `require_auth`
|
|
46
|
+
* - `credential_types?.length` → pre-authorization `require_credential_types(types)`
|
|
47
|
+
* - `required_scope` → pre-authorization `require_token_scope(capability)`
|
|
36
48
|
* - `roles?.length` → post-authorization `require_role(roles)` (multi-role any-of)
|
|
37
|
-
* - `credential_types?.length` → post-authorization `require_credential_types(types)`
|
|
38
49
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
50
|
+
* Guards run in declaration order, which is the coarse-to-fine order above:
|
|
51
|
+
* presence first (an anonymous caller holds neither a credential type nor a
|
|
52
|
+
* scope to check), then the channel, then the scope, then the role — the last
|
|
53
|
+
* being the only one that needs the resolved `RequestContext`.
|
|
54
|
+
*
|
|
55
|
+
* @throws Error if `auth.required_scope` is not a well-formed capability string
|
|
56
|
+
* (via `require_token_scope`) — `RouteAuth` types it as a plain string to keep
|
|
57
|
+
* `http/` free of an `auth/` import, so this is where the value is checked.
|
|
58
|
+
* The check is on *shape*, not membership: the vocabulary is deliberately open
|
|
59
|
+
* so a consumer can name its own surface, and a `surface:` name decides
|
|
60
|
+
* nothing (rule 3 is all-or-nothing), so the closed set would only have been
|
|
61
|
+
* a spelling check on a diagnostic string. fuz_app's own declarations are
|
|
62
|
+
* pinned to the surfaces it actually mounts by the surface census.
|
|
63
|
+
* @throws Error if `auth.required_scope` sits on an unrestricted route
|
|
64
|
+
* (`account: 'none' && actor: 'none'`). The same holder reaches that route
|
|
65
|
+
* by dropping the credential, so the guard reads as a control and enforces
|
|
66
|
+
* nothing — the shape `compile_action_registry` refuses on an `ActionSpec`
|
|
67
|
+
* for the same reason. Extends `RouteAuth`'s leaf invariant, which the
|
|
68
|
+
* route pipeline never parses.
|
|
42
69
|
*/
|
|
43
70
|
export declare const fuz_auth_guard_resolver: AuthGuardResolver;
|
|
44
71
|
//# sourceMappingURL=auth_guard_resolver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth_guard_resolver.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/auth_guard_resolver.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"auth_guard_resolver.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/auth_guard_resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AASH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,uBAAuB,EAAE,iBAuBrC,CAAC"}
|
|
@@ -1,56 +1,90 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth guard resolver for the route spec system.
|
|
3
3
|
*
|
|
4
|
-
* Maps the
|
|
5
|
-
* `credential_types`) to two-phase middleware sets that
|
|
4
|
+
* Maps the five-axis `RouteAuth` (`account` / `actor` / `roles` /
|
|
5
|
+
* `credential_types` / `required_scope`) to two-phase middleware sets that
|
|
6
6
|
* `apply_route_specs` weaves into the per-route pipeline:
|
|
7
7
|
*
|
|
8
|
-
* - `
|
|
9
|
-
*
|
|
10
|
-
* 'required'`
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
8
|
+
* - `pre_authorization` runs before the authorization phase, and holds every
|
|
9
|
+
* gate that reads only what the auth middleware already set: `require_auth`
|
|
10
|
+
* whenever `auth.account === 'required'` or `auth.actor === 'required'`
|
|
11
|
+
* (per registry-time invariant 3, `actor: 'required'` today implies a
|
|
12
|
+
* credential — accountless actors are out of scope for v1),
|
|
13
|
+
* `require_credential_types(types)` whenever `auth.credential_types?.length`,
|
|
14
|
+
* and the token-scope guard whenever `auth.required_scope` names a capability.
|
|
15
|
+
* All three fire before any body parsing, so a caller they refuse never sees
|
|
16
|
+
* route-shape information — and before the authorization phase, so a wrong
|
|
17
|
+
* channel costs no actor resolution.
|
|
18
|
+
* - `post_authorization` runs after the dispatcher's authorization phase has
|
|
19
|
+
* populated `RequestContext`, and holds the one gate that needs it:
|
|
20
|
+
* `require_role(roles)`, whenever `auth.roles?.length`, reads
|
|
21
|
+
* `c.var.request_context.role_grants`.
|
|
22
|
+
*
|
|
23
|
+
* The resulting order — credential presence, then credential type, then scope,
|
|
24
|
+
* then role — is the spine's canonical coarse-to-fine ordering, the same one
|
|
25
|
+
* `check_action_auth_post_authorization` runs for actions. A route declaring
|
|
26
|
+
* both a credential gate and a surface gate therefore answers with the
|
|
27
|
+
* coarser fact ("this channel may never call me") rather than the finer one
|
|
28
|
+
* ("your token is scoped"), on both dispatch surfaces alike.
|
|
19
29
|
*
|
|
20
30
|
* Public routes (`auth.account === 'none' && auth.actor === 'none'`)
|
|
21
31
|
* yield empty guard arrays. `'optional'` axes contribute no
|
|
22
|
-
* pre-
|
|
32
|
+
* pre-authorization 401; the authorization phase sets `RequestContext`
|
|
23
33
|
* to whatever the credential supports and the post-authorization
|
|
24
34
|
* gates decide whether the actor's role_grants / credential type match.
|
|
25
35
|
*
|
|
26
36
|
* @module
|
|
27
37
|
*/
|
|
28
|
-
import { require_auth, require_credential_types, require_role } from './request_context.js';
|
|
38
|
+
import { require_auth, require_credential_types, require_role, require_token_scope } from './request_context.js';
|
|
39
|
+
import { is_public_auth } from '../http/auth_shape.js';
|
|
29
40
|
/**
|
|
30
41
|
* Standard auth guard resolver for fuz_app.
|
|
31
42
|
*
|
|
32
|
-
* Reads each axis of the
|
|
33
|
-
*
|
|
43
|
+
* Reads each axis of the `RouteAuth` shape and emits the corresponding
|
|
44
|
+
* middleware:
|
|
34
45
|
*
|
|
35
|
-
* - `account === 'required'` or `actor === 'required'` → pre-
|
|
46
|
+
* - `account === 'required'` or `actor === 'required'` → pre-authorization `require_auth`
|
|
47
|
+
* - `credential_types?.length` → pre-authorization `require_credential_types(types)`
|
|
48
|
+
* - `required_scope` → pre-authorization `require_token_scope(capability)`
|
|
36
49
|
* - `roles?.length` → post-authorization `require_role(roles)` (multi-role any-of)
|
|
37
|
-
* - `credential_types?.length` → post-authorization `require_credential_types(types)`
|
|
38
50
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
51
|
+
* Guards run in declaration order, which is the coarse-to-fine order above:
|
|
52
|
+
* presence first (an anonymous caller holds neither a credential type nor a
|
|
53
|
+
* scope to check), then the channel, then the scope, then the role — the last
|
|
54
|
+
* being the only one that needs the resolved `RequestContext`.
|
|
55
|
+
*
|
|
56
|
+
* @throws Error if `auth.required_scope` is not a well-formed capability string
|
|
57
|
+
* (via `require_token_scope`) — `RouteAuth` types it as a plain string to keep
|
|
58
|
+
* `http/` free of an `auth/` import, so this is where the value is checked.
|
|
59
|
+
* The check is on *shape*, not membership: the vocabulary is deliberately open
|
|
60
|
+
* so a consumer can name its own surface, and a `surface:` name decides
|
|
61
|
+
* nothing (rule 3 is all-or-nothing), so the closed set would only have been
|
|
62
|
+
* a spelling check on a diagnostic string. fuz_app's own declarations are
|
|
63
|
+
* pinned to the surfaces it actually mounts by the surface census.
|
|
64
|
+
* @throws Error if `auth.required_scope` sits on an unrestricted route
|
|
65
|
+
* (`account: 'none' && actor: 'none'`). The same holder reaches that route
|
|
66
|
+
* by dropping the credential, so the guard reads as a control and enforces
|
|
67
|
+
* nothing — the shape `compile_action_registry` refuses on an `ActionSpec`
|
|
68
|
+
* for the same reason. Extends `RouteAuth`'s leaf invariant, which the
|
|
69
|
+
* route pipeline never parses.
|
|
42
70
|
*/
|
|
43
71
|
export const fuz_auth_guard_resolver = (auth) => {
|
|
44
|
-
const
|
|
72
|
+
const pre_authorization = [];
|
|
45
73
|
const post_authorization = [];
|
|
46
74
|
if (auth.account === 'required' || auth.actor === 'required') {
|
|
47
|
-
|
|
75
|
+
pre_authorization.push(require_auth);
|
|
48
76
|
}
|
|
49
77
|
if (auth.credential_types?.length) {
|
|
50
|
-
|
|
78
|
+
pre_authorization.push(require_credential_types(auth.credential_types));
|
|
79
|
+
}
|
|
80
|
+
if (auth.required_scope !== undefined) {
|
|
81
|
+
if (is_public_auth(auth)) {
|
|
82
|
+
throw new Error(`auth.required_scope "${auth.required_scope}" on an unrestricted route (account: 'none', actor: 'none') — the same holder reaches it by dropping the credential, so the guard would enforce nothing. Gate the route on a credential, or drop the field.`);
|
|
83
|
+
}
|
|
84
|
+
pre_authorization.push(require_token_scope(auth.required_scope));
|
|
51
85
|
}
|
|
52
86
|
if (auth.roles?.length) {
|
|
53
87
|
post_authorization.push(require_role(auth.roles));
|
|
54
88
|
}
|
|
55
|
-
return {
|
|
89
|
+
return { pre_authorization, post_authorization };
|
|
56
90
|
};
|
|
@@ -17,13 +17,12 @@
|
|
|
17
17
|
import type { MiddlewareHandler } from 'hono';
|
|
18
18
|
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
19
19
|
import type { QueryDeps } from '../db/query_deps.js';
|
|
20
|
-
import { type RateLimiter } from '../rate_limiter.js';
|
|
21
20
|
/**
|
|
22
21
|
* Create middleware that authenticates via bearer token.
|
|
23
22
|
*
|
|
24
23
|
* Soft-fails for invalid, expired, or empty tokens — calls `next()` without
|
|
25
24
|
* setting account identity, letting downstream auth enforcement (the RPC
|
|
26
|
-
* dispatcher's pre-
|
|
25
|
+
* dispatcher's pre-authorization / post-authorization auth gates or
|
|
27
26
|
* `require_auth`) return a consistent JSON-RPC or route-level error. This
|
|
28
27
|
* avoids leaking token-specific diagnostics
|
|
29
28
|
* (`invalid_token`, `account_not_found`) that could aid enumeration attacks,
|
|
@@ -37,14 +36,18 @@ import { type RateLimiter } from '../rate_limiter.js';
|
|
|
37
36
|
* (e.g. by session middleware). Acting-actor resolution + `RequestContext`
|
|
38
37
|
* construction are deferred to the dispatcher's authorization phase.
|
|
39
38
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* There is deliberately **no rate limit on this path**, and no 429 — every
|
|
40
|
+
* failure soft-fails to "no credential". An API token is 32 bytes of CSPRNG
|
|
41
|
+
* output resolved by a blake3 hash lookup, so guessing is bounded by entropy,
|
|
42
|
+
* not by throttling; a limiter here would buy nothing measurable while costing
|
|
43
|
+
* availability (the check/record has to precede the async lookup to close its
|
|
44
|
+
* own TOCTOU window, so concurrent requests bearing a *valid* token race each
|
|
45
|
+
* other into a 429). The Rust spine never had one here; this is the converged
|
|
46
|
+
* shape. See `docs/security.md` §Why bearer auth is not rate limited.
|
|
42
47
|
*
|
|
43
48
|
* @param deps - query dependencies (pool-level db for middleware)
|
|
44
|
-
* @param ip_rate_limiter - per-IP rate limiter for bearer token attempts (null to disable)
|
|
45
49
|
* @param log - the logger instance
|
|
46
50
|
* @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, and `AUTH_API_TOKEN_ID_KEY` on success
|
|
47
|
-
* @mutates `ip_rate_limiter` - records on attempt; resets on a valid token
|
|
48
51
|
*/
|
|
49
|
-
export declare const create_bearer_auth_middleware: (deps: QueryDeps,
|
|
52
|
+
export declare const create_bearer_auth_middleware: (deps: QueryDeps, log: Logger) => MiddlewareHandler;
|
|
50
53
|
//# sourceMappingURL=bearer_auth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bearer_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bearer_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAUtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"bearer_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bearer_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAUtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAIrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,6BAA6B,GAAI,MAAM,SAAS,EAAE,KAAK,MAAM,KAAG,iBAuE5E,CAAC"}
|
package/dist/auth/bearer_auth.js
CHANGED
|
@@ -20,13 +20,12 @@ import { parse_stored_token_scope, token_scope_full } from './token_scope.js';
|
|
|
20
20
|
import { query_validate_api_token } from './api_token_queries.js';
|
|
21
21
|
import { get_client_ip } from '../http/client_ip.js';
|
|
22
22
|
import { is_browser_context } from '../http/origin.js';
|
|
23
|
-
import { rate_limit_exceeded_response } from '../rate_limiter.js';
|
|
24
23
|
/**
|
|
25
24
|
* Create middleware that authenticates via bearer token.
|
|
26
25
|
*
|
|
27
26
|
* Soft-fails for invalid, expired, or empty tokens — calls `next()` without
|
|
28
27
|
* setting account identity, letting downstream auth enforcement (the RPC
|
|
29
|
-
* dispatcher's pre-
|
|
28
|
+
* dispatcher's pre-authorization / post-authorization auth gates or
|
|
30
29
|
* `require_auth`) return a consistent JSON-RPC or route-level error. This
|
|
31
30
|
* avoids leaking token-specific diagnostics
|
|
32
31
|
* (`invalid_token`, `account_not_found`) that could aid enumeration attacks,
|
|
@@ -40,16 +39,20 @@ import { rate_limit_exceeded_response } from '../rate_limiter.js';
|
|
|
40
39
|
* (e.g. by session middleware). Acting-actor resolution + `RequestContext`
|
|
41
40
|
* construction are deferred to the dispatcher's authorization phase.
|
|
42
41
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* There is deliberately **no rate limit on this path**, and no 429 — every
|
|
43
|
+
* failure soft-fails to "no credential". An API token is 32 bytes of CSPRNG
|
|
44
|
+
* output resolved by a blake3 hash lookup, so guessing is bounded by entropy,
|
|
45
|
+
* not by throttling; a limiter here would buy nothing measurable while costing
|
|
46
|
+
* availability (the check/record has to precede the async lookup to close its
|
|
47
|
+
* own TOCTOU window, so concurrent requests bearing a *valid* token race each
|
|
48
|
+
* other into a 429). The Rust spine never had one here; this is the converged
|
|
49
|
+
* shape. See `docs/security.md` §Why bearer auth is not rate limited.
|
|
45
50
|
*
|
|
46
51
|
* @param deps - query dependencies (pool-level db for middleware)
|
|
47
|
-
* @param ip_rate_limiter - per-IP rate limiter for bearer token attempts (null to disable)
|
|
48
52
|
* @param log - the logger instance
|
|
49
53
|
* @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, and `AUTH_API_TOKEN_ID_KEY` on success
|
|
50
|
-
* @mutates `ip_rate_limiter` - records on attempt; resets on a valid token
|
|
51
54
|
*/
|
|
52
|
-
export const create_bearer_auth_middleware = (deps,
|
|
55
|
+
export const create_bearer_auth_middleware = (deps, log) => {
|
|
53
56
|
return async (c, next) => {
|
|
54
57
|
// Skip if an account is already authenticated (e.g. by session middleware)
|
|
55
58
|
if (c.get(ACCOUNT_ID_KEY) != null) {
|
|
@@ -88,28 +91,16 @@ export const create_bearer_auth_middleware = (deps, ip_rate_limiter, log) => {
|
|
|
88
91
|
await next();
|
|
89
92
|
return;
|
|
90
93
|
}
|
|
94
|
+
// `ip` feeds the token's `last_used_ip` bookkeeping, not a limiter.
|
|
91
95
|
const ip = get_client_ip(c);
|
|
92
|
-
// Per-IP rate limit: record before async DB work to close the TOCTOU
|
|
93
|
-
// window where concurrent requests could all pass check() before any
|
|
94
|
-
// reaches record(). On valid token, reset the counter below.
|
|
95
|
-
if (ip_rate_limiter) {
|
|
96
|
-
const check = ip_rate_limiter.check(ip);
|
|
97
|
-
if (!check.allowed) {
|
|
98
|
-
return rate_limit_exceeded_response(c, check.retry_after);
|
|
99
|
-
}
|
|
100
|
-
ip_rate_limiter.record(ip);
|
|
101
|
-
}
|
|
102
96
|
const api_token = await query_validate_api_token({ ...deps, log }, raw_token, ip, c.var.pending_effects);
|
|
103
97
|
if (!api_token) {
|
|
104
|
-
// Invalid or expired token — soft-fail
|
|
105
|
-
//
|
|
98
|
+
// Invalid or expired token — soft-fail, indistinguishable from
|
|
99
|
+
// sending no credential at all.
|
|
106
100
|
log.debug('bearer auth soft-fail: token not found or expired');
|
|
107
101
|
await next();
|
|
108
102
|
return;
|
|
109
103
|
}
|
|
110
|
-
// Valid token — reset rate limit counter
|
|
111
|
-
if (ip_rate_limiter)
|
|
112
|
-
ip_rate_limiter.reset(ip);
|
|
113
104
|
c.set(ACCOUNT_ID_KEY, api_token.account_id);
|
|
114
105
|
c.set(CREDENTIAL_TYPE_KEY, 'api_token');
|
|
115
106
|
c.set(AUTH_API_TOKEN_ID_KEY, api_token.id);
|
|
@@ -37,8 +37,18 @@ export interface BootstrapRouteOptions {
|
|
|
37
37
|
* Use for app-specific post-bootstrap work like generating API tokens.
|
|
38
38
|
*/
|
|
39
39
|
on_bootstrap?: (result: BootstrapAccountSuccess, c: Context) => Promise<void>;
|
|
40
|
-
/**
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Rate limiter for bootstrap attempts, keyed by client IP. Pass `null` to
|
|
42
|
+
* disable. Its own instance, not login's: bootstrap is one-shot and its
|
|
43
|
+
* bucket is never refunded on success (see `RateLimiter.reset`), so a
|
|
44
|
+
* fumbled token would otherwise leave the operator's *login* budget nearly
|
|
45
|
+
* spent on a deployment where their new account is the only one that
|
|
46
|
+
* exists. The Rust spine rate-limits bootstrap not at all (the token is
|
|
47
|
+
* 32 bytes of CSPRNG compared in constant time); this is the tighter side
|
|
48
|
+
* of that divergence, kept because the token also sits in a file whose
|
|
49
|
+
* read path an operator can misconfigure.
|
|
50
|
+
*/
|
|
51
|
+
bootstrap_ip_rate_limiter: RateLimiter | null;
|
|
42
52
|
}
|
|
43
53
|
/**
|
|
44
54
|
* Dependencies for checking bootstrap status at startup.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrap_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EAAqB,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEzF,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGrD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,8EAA8E;IAC9E,gBAAgB,EAAE,eAAe,CAAC;IAClC;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,uBAAuB,EAAE,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E
|
|
1
|
+
{"version":3,"file":"bootstrap_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EAAqB,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEzF,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGrD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,8EAA8E;IAC9E,gBAAgB,EAAE,eAAe,CAAC;IAClC;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,uBAAuB,EAAE,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E;;;;;;;;;;OAUG;IACH,yBAAyB,EAAE,WAAW,GAAG,IAAI,CAAC;CAC9C;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnD,EAAE,EAAE,EAAE,CAAC;IACP,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,wBAAwB,EAC9B,SAAS;IAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,KACpC,OAAO,CAAC,eAAe,CAwBzB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,gBAAgB,EACtB,SAAS,qBAAqB,KAC5B,KAAK,CAAC,SAAS,CA0GjB,CAAC"}
|
|
@@ -54,7 +54,7 @@ export const check_bootstrap_status = async (deps, options) => {
|
|
|
54
54
|
*/
|
|
55
55
|
export const create_bootstrap_route_specs = (deps, options) => {
|
|
56
56
|
const { keyring } = deps;
|
|
57
|
-
const { session_options, bootstrap_status, on_bootstrap,
|
|
57
|
+
const { session_options, bootstrap_status, on_bootstrap, bootstrap_ip_rate_limiter } = options;
|
|
58
58
|
const { token_path } = bootstrap_status;
|
|
59
59
|
return [
|
|
60
60
|
{
|
|
@@ -68,9 +68,9 @@ export const create_bootstrap_route_specs = (deps, options) => {
|
|
|
68
68
|
return c.json({ error: ERROR_ALREADY_BOOTSTRAPPED }, 403);
|
|
69
69
|
}
|
|
70
70
|
// Per-IP rate limit check (before any token/DB work)
|
|
71
|
-
const ip =
|
|
72
|
-
if (
|
|
73
|
-
const check =
|
|
71
|
+
const ip = bootstrap_ip_rate_limiter ? get_client_ip(c) : null;
|
|
72
|
+
if (bootstrap_ip_rate_limiter && ip) {
|
|
73
|
+
const check = bootstrap_ip_rate_limiter.check(ip);
|
|
74
74
|
if (!check.allowed) {
|
|
75
75
|
return rate_limit_exceeded_response(c, check.retry_after);
|
|
76
76
|
}
|
|
@@ -87,8 +87,8 @@ export const create_bootstrap_route_specs = (deps, options) => {
|
|
|
87
87
|
log: deps.log
|
|
88
88
|
}, input.token, input);
|
|
89
89
|
if (!result.ok) {
|
|
90
|
-
if (
|
|
91
|
-
|
|
90
|
+
if (bootstrap_ip_rate_limiter && ip)
|
|
91
|
+
bootstrap_ip_rate_limiter.record(ip);
|
|
92
92
|
deps.audit.emit(route, {
|
|
93
93
|
event_type: 'bootstrap',
|
|
94
94
|
outcome: 'failure',
|
|
@@ -97,9 +97,15 @@ export const create_bootstrap_route_specs = (deps, options) => {
|
|
|
97
97
|
});
|
|
98
98
|
return c.json({ error: result.error }, result.status);
|
|
99
99
|
}
|
|
100
|
-
// Successful bootstrap — update state immediately
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
// Successful bootstrap — update state immediately. Nothing is
|
|
101
|
+
// reset: bootstrap has no account-grain bucket to forgive (it
|
|
102
|
+
// predates any account), and the IP bucket is never refunded on
|
|
103
|
+
// success (see `RateLimiter.reset`). So this is the one auth
|
|
104
|
+
// surface where a success clears nothing at all. Bootstrap is
|
|
105
|
+
// one-shot, so at most `max_attempts - 1` residual entries
|
|
106
|
+
// survive — and they sit on this route's own limiter, not
|
|
107
|
+
// login's, so a fumbled token can't spend the operator's login
|
|
108
|
+
// budget on a deployment where their account is the only one.
|
|
103
109
|
bootstrap_status.available = false;
|
|
104
110
|
await create_session_and_set_cookie({
|
|
105
111
|
keyring,
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import type { SessionOptions } from './session_cookie.js';
|
|
10
10
|
import type { AppDeps } from './deps.js';
|
|
11
11
|
import type { DaemonTokenState } from './daemon_token.js';
|
|
12
|
-
import type { RateLimiter } from '../rate_limiter.js';
|
|
13
12
|
import type { MiddlewareSpec } from '../http/middleware_spec.js';
|
|
14
13
|
/**
|
|
15
14
|
* Per-factory configuration for the standard auth middleware stack.
|
|
@@ -21,8 +20,6 @@ export interface AuthMiddlewareOptions {
|
|
|
21
20
|
path?: string;
|
|
22
21
|
/** Daemon token state for keeper auth. Omit to disable daemon token middleware. */
|
|
23
22
|
daemon_token_state?: DaemonTokenState;
|
|
24
|
-
/** Rate limiter for bearer token auth attempts (per-IP). Pass `null` to disable. */
|
|
25
|
-
bearer_ip_rate_limiter: RateLimiter | null;
|
|
26
23
|
}
|
|
27
24
|
/**
|
|
28
25
|
* Create the auth middleware stack.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAGjE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,gBAAgB,CAAC;CACtC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAuE/B,CAAC"}
|
package/dist/auth/middleware.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
-
import { ApiError
|
|
9
|
+
import { ApiError } from '../http/error_schemas.js';
|
|
10
10
|
/**
|
|
11
11
|
* Create the auth middleware stack.
|
|
12
12
|
*
|
|
@@ -21,7 +21,7 @@ import { ApiError, RateLimitError } from '../http/error_schemas.js';
|
|
|
21
21
|
*/
|
|
22
22
|
export const create_auth_middleware_specs = async (deps, options) => {
|
|
23
23
|
const { keyring, db } = deps;
|
|
24
|
-
const { allowed_origins, session_options, path = '/api/*', daemon_token_state
|
|
24
|
+
const { allowed_origins, session_options, path = '/api/*', daemon_token_state } = options;
|
|
25
25
|
const query_deps = { db };
|
|
26
26
|
// Dynamic imports preserve the bundle-split for type-only consumers of
|
|
27
27
|
// this module (`MiddlewareSpec`, `AuthMiddlewareOptions`, etc.). The
|
|
@@ -36,7 +36,7 @@ export const create_auth_middleware_specs = async (deps, options) => {
|
|
|
36
36
|
]);
|
|
37
37
|
const session_middleware = create_session_middleware(keyring, session_options);
|
|
38
38
|
const request_context_middleware = create_request_context_middleware(query_deps, deps.log);
|
|
39
|
-
const bearer_auth_middleware = create_bearer_auth_middleware(query_deps,
|
|
39
|
+
const bearer_auth_middleware = create_bearer_auth_middleware(query_deps, deps.log);
|
|
40
40
|
const specs = [
|
|
41
41
|
{
|
|
42
42
|
name: 'origin',
|
|
@@ -50,13 +50,14 @@ export const create_auth_middleware_specs = async (deps, options) => {
|
|
|
50
50
|
name: 'bearer_auth',
|
|
51
51
|
path,
|
|
52
52
|
handler: bearer_auth_middleware,
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
53
|
+
// Soft-fails on every non-success path — browser context, malformed,
|
|
54
|
+
// invalid, and expired tokens all `next()` through without setting
|
|
55
|
+
// context. The layer returns no error response of its own (it carries
|
|
56
|
+
// no rate limiter, so not even a 429). Auth enforcement (401/403)
|
|
57
|
+
// happens downstream — the RPC dispatcher's pre-authorization /
|
|
58
|
+
// post-authorization auth gates, or `require_auth` / `require_role` on
|
|
59
|
+
// REST — producing consistent JSON-RPC or route-level errors.
|
|
60
|
+
errors: {}
|
|
60
61
|
}
|
|
61
62
|
];
|
|
62
63
|
if (daemon_token_state) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AA+BH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,aAAa,CAAC,MAAM,CAA8B,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AA+BH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,aAAa,CAAC,MAAM,CAA8B,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CAuJ5C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
|