@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.
Files changed (115) hide show
  1. package/dist/actions/CLAUDE.md +42 -23
  2. package/dist/actions/action_bridge.d.ts +34 -3
  3. package/dist/actions/action_bridge.d.ts.map +1 -1
  4. package/dist/actions/action_bridge.js +32 -1
  5. package/dist/actions/action_rpc.d.ts +5 -4
  6. package/dist/actions/action_rpc.d.ts.map +1 -1
  7. package/dist/actions/action_rpc.js +4 -3
  8. package/dist/actions/action_spec.d.ts +8 -2
  9. package/dist/actions/action_spec.d.ts.map +1 -1
  10. package/dist/actions/action_spec.js +5 -2
  11. package/dist/actions/compile_action_registry.d.ts +26 -14
  12. package/dist/actions/compile_action_registry.d.ts.map +1 -1
  13. package/dist/actions/compile_action_registry.js +29 -14
  14. package/dist/actions/perform_action.d.ts +26 -16
  15. package/dist/actions/perform_action.d.ts.map +1 -1
  16. package/dist/actions/perform_action.js +69 -44
  17. package/dist/actions/register_action_ws.d.ts +4 -4
  18. package/dist/actions/register_action_ws.js +5 -5
  19. package/dist/actions/register_ws_endpoint.d.ts +13 -5
  20. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  21. package/dist/actions/register_ws_endpoint.js +15 -6
  22. package/dist/auth/CLAUDE.md +16 -9
  23. package/dist/auth/account_route_schema.d.ts +10 -1
  24. package/dist/auth/account_route_schema.d.ts.map +1 -1
  25. package/dist/auth/account_route_schema.js +10 -1
  26. package/dist/auth/account_routes.d.ts +18 -4
  27. package/dist/auth/account_routes.d.ts.map +1 -1
  28. package/dist/auth/account_routes.js +32 -29
  29. package/dist/auth/api_token_queries.d.ts +4 -0
  30. package/dist/auth/api_token_queries.d.ts.map +1 -1
  31. package/dist/auth/api_token_queries.js +5 -1
  32. package/dist/auth/audit_log_route_schema.d.ts.map +1 -1
  33. package/dist/auth/audit_log_route_schema.js +11 -1
  34. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  35. package/dist/auth/audit_log_routes.js +4 -9
  36. package/dist/auth/auth_guard_resolver.d.ts +48 -21
  37. package/dist/auth/auth_guard_resolver.d.ts.map +1 -1
  38. package/dist/auth/auth_guard_resolver.js +60 -26
  39. package/dist/auth/bearer_auth.d.ts +10 -7
  40. package/dist/auth/bearer_auth.d.ts.map +1 -1
  41. package/dist/auth/bearer_auth.js +13 -22
  42. package/dist/auth/bootstrap_routes.d.ts +12 -2
  43. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  44. package/dist/auth/bootstrap_routes.js +15 -9
  45. package/dist/auth/middleware.d.ts +0 -3
  46. package/dist/auth/middleware.d.ts.map +1 -1
  47. package/dist/auth/middleware.js +11 -10
  48. package/dist/auth/migrations.d.ts.map +1 -1
  49. package/dist/auth/migrations.js +24 -0
  50. package/dist/auth/request_context.d.ts +54 -24
  51. package/dist/auth/request_context.d.ts.map +1 -1
  52. package/dist/auth/request_context.js +117 -39
  53. package/dist/auth/session_queries.d.ts +13 -2
  54. package/dist/auth/session_queries.d.ts.map +1 -1
  55. package/dist/auth/session_queries.js +14 -3
  56. package/dist/auth/signup_routes.d.ts +10 -0
  57. package/dist/auth/signup_routes.d.ts.map +1 -1
  58. package/dist/auth/signup_routes.js +14 -11
  59. package/dist/auth/token_scope.d.ts +109 -21
  60. package/dist/auth/token_scope.d.ts.map +1 -1
  61. package/dist/auth/token_scope.js +106 -21
  62. package/dist/http/CLAUDE.md +23 -11
  63. package/dist/http/auth_shape.d.ts +58 -3
  64. package/dist/http/auth_shape.d.ts.map +1 -1
  65. package/dist/http/auth_shape.js +63 -5
  66. package/dist/http/error_schemas.d.ts +25 -2
  67. package/dist/http/error_schemas.d.ts.map +1 -1
  68. package/dist/http/error_schemas.js +37 -15
  69. package/dist/http/route_spec.d.ts +29 -26
  70. package/dist/http/route_spec.d.ts.map +1 -1
  71. package/dist/http/route_spec.js +12 -16
  72. package/dist/http/surface_query.d.ts +2 -2
  73. package/dist/http/surface_query.js +2 -2
  74. package/dist/rate_limiter.d.ts +36 -3
  75. package/dist/rate_limiter.d.ts.map +1 -1
  76. package/dist/rate_limiter.js +36 -3
  77. package/dist/server/app_server.d.ts +27 -10
  78. package/dist/server/app_server.d.ts.map +1 -1
  79. package/dist/server/app_server.js +30 -20
  80. package/dist/server/app_server_context.d.ts +10 -2
  81. package/dist/server/app_server_context.d.ts.map +1 -1
  82. package/dist/server/serve_fact_route.d.ts.map +1 -1
  83. package/dist/server/serve_fact_route.js +12 -10
  84. package/dist/testing/CLAUDE.md +60 -11
  85. package/dist/testing/app_server.d.ts.map +1 -1
  86. package/dist/testing/app_server.js +6 -4
  87. package/dist/testing/cross_backend/conformance_case.js +1 -1
  88. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
  89. package/dist/testing/cross_backend/default_spine_surface.js +9 -8
  90. package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
  91. package/dist/testing/cross_backend/login_security.js +59 -0
  92. package/dist/testing/cross_backend/session_cap.d.ts +19 -0
  93. package/dist/testing/cross_backend/session_cap.d.ts.map +1 -0
  94. package/dist/testing/cross_backend/session_cap.js +93 -0
  95. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  96. package/dist/testing/cross_backend/testing_backdoor.js +6 -5
  97. package/dist/testing/cross_backend/token_scope_surface.d.ts +9 -0
  98. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -0
  99. package/dist/testing/cross_backend/token_scope_surface.js +170 -0
  100. package/dist/testing/integration.d.ts.map +1 -1
  101. package/dist/testing/integration.js +3 -2
  102. package/dist/testing/middleware.d.ts +2 -5
  103. package/dist/testing/middleware.d.ts.map +1 -1
  104. package/dist/testing/middleware.js +5 -5
  105. package/dist/testing/rate_limiting.d.ts +10 -16
  106. package/dist/testing/rate_limiting.d.ts.map +1 -1
  107. package/dist/testing/rate_limiting.js +11 -95
  108. package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
  109. package/dist/testing/rpc_attack_surface.js +4 -2
  110. package/dist/testing/stubs.d.ts.map +1 -1
  111. package/dist/testing/stubs.js +3 -1
  112. package/dist/testing/testing_rate_limiter.d.ts +7 -1
  113. package/dist/testing/testing_rate_limiter.d.ts.map +1 -1
  114. package/dist/testing/testing_rate_limiter.js +7 -1
  115. 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;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAYhB,CAAC"}
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,CAQ1B,CAAC"}
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: { account: 'required', actor: 'required', roles: [required_role] },
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;AAQ3E,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,CAiC5F,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, token_scope_surface_denial } from './request_context.js';
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 — a narrowed token holds no audit feed. Same reasoning as
36
- // the WS upgrade: this is a long-lived server→client stream whose
37
- // contents are decided by the account's role, not by any
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 four-axis `RouteAuth` (`account` / `actor` / `roles` /
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
- * - `pre_validation` runs before input validation. `require_auth` lands
9
- * here whenever `auth.account === 'required'` or `auth.actor ===
10
- * 'required'` (per registry-time invariant 3, `actor: 'required'`
11
- * today implies a credential — accountless actors are out of scope
12
- * for v1). Pre-validation 401 fires before any body parsing so
13
- * unauthenticated callers never see route-shape information from
14
- * parse failures.
15
- * - `post_authorization` runs after the dispatcher's authorization
16
- * phase has populated `RequestContext`. `require_role(roles)` fires
17
- * whenever `auth.roles?.length`. `require_credential_types(types)`
18
- * fires whenever `auth.credential_types?.length`.
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-validation 401; the authorization phase sets `RequestContext`
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 four-axis `RouteAuth` shape and emits the
33
- * corresponding middleware:
42
+ * Reads each axis of the `RouteAuth` shape and emits the corresponding
43
+ * middleware:
34
44
  *
35
- * - `account === 'required'` or `actor === 'required'` → pre-validation `require_auth`
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
- * Multiple post-authorization guards run in declaration order: credential
40
- * type check first (since failing it implies the request can never
41
- * resolve a usable identity), role check second.
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;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE/D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,uBAAuB,EAAE,iBAerC,CAAC"}
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 four-axis `RouteAuth` (`account` / `actor` / `roles` /
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
- * - `pre_validation` runs before input validation. `require_auth` lands
9
- * here whenever `auth.account === 'required'` or `auth.actor ===
10
- * 'required'` (per registry-time invariant 3, `actor: 'required'`
11
- * today implies a credential — accountless actors are out of scope
12
- * for v1). Pre-validation 401 fires before any body parsing so
13
- * unauthenticated callers never see route-shape information from
14
- * parse failures.
15
- * - `post_authorization` runs after the dispatcher's authorization
16
- * phase has populated `RequestContext`. `require_role(roles)` fires
17
- * whenever `auth.roles?.length`. `require_credential_types(types)`
18
- * fires whenever `auth.credential_types?.length`.
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-validation 401; the authorization phase sets `RequestContext`
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 four-axis `RouteAuth` shape and emits the
33
- * corresponding middleware:
43
+ * Reads each axis of the `RouteAuth` shape and emits the corresponding
44
+ * middleware:
34
45
  *
35
- * - `account === 'required'` or `actor === 'required'` → pre-validation `require_auth`
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
- * Multiple post-authorization guards run in declaration order: credential
40
- * type check first (since failing it implies the request can never
41
- * resolve a usable identity), role check second.
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 pre_validation = [];
72
+ const pre_authorization = [];
45
73
  const post_authorization = [];
46
74
  if (auth.account === 'required' || auth.actor === 'required') {
47
- pre_validation.push(require_auth);
75
+ pre_authorization.push(require_auth);
48
76
  }
49
77
  if (auth.credential_types?.length) {
50
- post_authorization.push(require_credential_types(auth.credential_types));
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 { pre_validation, post_authorization };
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-validation / post-authorization auth gates or
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
- * Rate limiting (429) is the only hard-fail — it's a throttling concern
41
- * independent of auth identity.
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, ip_rate_limiter: RateLimiter | null, log: Logger) => MiddlewareHandler;
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;AAGrD,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEpF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,iBAAiB,WAAW,GAAG,IAAI,EACnC,KAAK,MAAM,KACT,iBAoFF,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"}
@@ -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-validation / post-authorization auth gates or
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
- * Rate limiting (429) is the only hard-fail — it's a throttling concern
44
- * independent of auth identity.
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, ip_rate_limiter, log) => {
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. Rate limit counter stays
105
- // incremented (recorded above), correctly penalizing bad attempts.
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
- /** Rate limiter for bootstrap attempts (per-IP). Pass `null` to disable. */
41
- ip_rate_limiter: RateLimiter | null;
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,4EAA4E;IAC5E,eAAe,EAAE,WAAW,GAAG,IAAI,CAAC;CACpC;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,CAmGjB,CAAC"}
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, ip_rate_limiter } = options;
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 = ip_rate_limiter ? get_client_ip(c) : null;
72
- if (ip_rate_limiter && ip) {
73
- const check = ip_rate_limiter.check(ip);
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 (ip_rate_limiter && ip)
91
- ip_rate_limiter.record(ip);
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
- if (ip_rate_limiter && ip)
102
- ip_rate_limiter.reset(ip);
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,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,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;IACtC,oFAAoF;IACpF,sBAAsB,EAAE,WAAW,GAAG,IAAI,CAAC;CAC3C;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAgF/B,CAAC"}
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"}
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module
8
8
  */
9
- import { ApiError, RateLimitError } from '../http/error_schemas.js';
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, bearer_ip_rate_limiter } = options;
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, bearer_ip_rate_limiter, deps.log);
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
- // Bearer middleware soft-fails for invalid/expired tokens (calls next()
54
- // without setting context). Only 429 is a hard-fail from this layer.
55
- // Auth enforcement (401/403) happens downstream — the RPC dispatcher's
56
- // pre-validation / post-authorization auth gates, or `require_auth` /
57
- // `require_role` on REST — producing consistent JSON-RPC or
58
- // route-level errors.
59
- errors: { 429: RateLimitError }
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,CA6H5C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
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"}