@fuzdev/fuz_app 0.12.0 → 0.13.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 (49) hide show
  1. package/dist/actions/action_codegen.d.ts.map +1 -1
  2. package/dist/auth/account_routes.d.ts +30 -0
  3. package/dist/auth/account_routes.d.ts.map +1 -1
  4. package/dist/auth/account_routes.js +44 -9
  5. package/dist/auth/admin_routes.d.ts.map +1 -1
  6. package/dist/auth/admin_routes.js +33 -2
  7. package/dist/auth/audit_log_routes.d.ts +2 -1
  8. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  9. package/dist/auth/audit_log_routes.js +11 -2
  10. package/dist/auth/audit_log_schema.d.ts +1 -1
  11. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  12. package/dist/auth/audit_log_schema.js +3 -1
  13. package/dist/auth/permit_queries.d.ts +19 -0
  14. package/dist/auth/permit_queries.d.ts.map +1 -1
  15. package/dist/auth/permit_queries.js +21 -0
  16. package/dist/auth/request_context.d.ts +10 -0
  17. package/dist/auth/request_context.d.ts.map +1 -1
  18. package/dist/auth/request_context.js +14 -0
  19. package/dist/hono_context.d.ts +7 -0
  20. package/dist/hono_context.d.ts.map +1 -1
  21. package/dist/realtime/sse_auth_guard.d.ts +23 -3
  22. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  23. package/dist/realtime/sse_auth_guard.js +38 -2
  24. package/dist/realtime/subscriber_registry.d.ts +62 -17
  25. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  26. package/dist/realtime/subscriber_registry.js +64 -21
  27. package/dist/server/validate_nginx.d.ts.map +1 -1
  28. package/dist/server/validate_nginx.js +61 -7
  29. package/dist/testing/admin_integration.d.ts.map +1 -1
  30. package/dist/testing/admin_integration.js +8 -8
  31. package/dist/testing/app_server.d.ts +9 -0
  32. package/dist/testing/app_server.d.ts.map +1 -1
  33. package/dist/testing/app_server.js +4 -3
  34. package/dist/testing/data_exposure.d.ts.map +1 -1
  35. package/dist/testing/data_exposure.js +1 -20
  36. package/dist/testing/error_coverage.d.ts +93 -27
  37. package/dist/testing/error_coverage.d.ts.map +1 -1
  38. package/dist/testing/error_coverage.js +160 -67
  39. package/dist/testing/integration.d.ts.map +1 -1
  40. package/dist/testing/integration.js +6 -6
  41. package/dist/testing/integration_helpers.d.ts +17 -0
  42. package/dist/testing/integration_helpers.d.ts.map +1 -1
  43. package/dist/testing/integration_helpers.js +31 -0
  44. package/dist/testing/round_trip.d.ts.map +1 -1
  45. package/dist/testing/round_trip.js +41 -55
  46. package/dist/testing/sse_round_trip.d.ts +64 -0
  47. package/dist/testing/sse_round_trip.d.ts.map +1 -0
  48. package/dist/testing/sse_round_trip.js +241 -0
  49. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"action_codegen.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_codegen.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,OAAO,KAAK,EAAC,eAAe,EAAE,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAKxE;;GAEG;AACH,UAAU,UAAU;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,WAAW,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,aAAa;;IACzB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAa;IAE1D;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAQrC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAI1C;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI;IAOrD;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI;IAgCtD;;;OAGG;IACH,KAAK,IAAI,MAAM;IAIf;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;;OAGG;IACH,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC;IAIxB;;OAEG;IACH,KAAK,IAAI,IAAI;CAqDb;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,eAAe,EACrB,UAAU,UAAU,GAAG,SAAS,KAC9B,KAAK,CAAC,gBAAgB,CA4DxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,eAAe,EACrB,OAAO,gBAAgB,EACvB,SAAS,aAAa,EACtB,aAAa,MAAM,KACjB,MAkBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,eAAe,EACrB,UAAU,UAAU,GAAG,SAAS,EAChC,SAAS,aAAa,EACtB,UAAU;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAC,KACpC,MA4BF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,aAAa,MAAM,KAAG,MACU,CAAC;AAG/D,eAAO,MAAM,yBAAyB,GAAI,QAAQ,MAAM,KAAG,MAAiC,CAAC;AAC7F,eAAO,MAAM,+BAA+B,GAAI,QAAQ,MAAM,KAAG,MACpB,CAAC;AAC9C,eAAO,MAAM,gCAAgC,GAAI,QAAQ,MAAM,KAAG,MACpB,CAAC;AAE/C;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,CAAC,CAAC,OAwBxD,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,MAI3D,CAAC"}
1
+ {"version":3,"file":"action_codegen.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_codegen.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,OAAO,KAAK,EAAC,eAAe,EAAE,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAOxE;;GAEG;AACH,UAAU,UAAU;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,WAAW,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,aAAa;;IACzB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAa;IAE1D;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAQrC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAI1C;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI;IAOrD;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI;IAgCtD;;;OAGG;IACH,KAAK,IAAI,MAAM;IAIf;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;;OAGG;IACH,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC;IAIxB;;OAEG;IACH,KAAK,IAAI,IAAI;CAqDb;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,eAAe,EACrB,UAAU,UAAU,GAAG,SAAS,KAC9B,KAAK,CAAC,gBAAgB,CA4DxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,eAAe,EACrB,OAAO,gBAAgB,EACvB,SAAS,aAAa,EACtB,aAAa,MAAM,KACjB,MAkBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,eAAe,EACrB,UAAU,UAAU,GAAG,SAAS,EAChC,SAAS,aAAa,EACtB,UAAU;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAC,KACpC,MA4BF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,aAAa,MAAM,KAAG,MACU,CAAC;AAG/D,eAAO,MAAM,yBAAyB,GAAI,QAAQ,MAAM,KAAG,MAAiC,CAAC;AAC7F,eAAO,MAAM,+BAA+B,GAAI,QAAQ,MAAM,KAAG,MACpB,CAAC;AAC9C,eAAO,MAAM,gCAAgC,GAAI,QAAQ,MAAM,KAAG,MACpB,CAAC;AAE/C;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,CAAC,CAAC,OAwBxD,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,MAI3D,CAAC"}
@@ -51,6 +51,25 @@ export interface AccountStatusOptions {
51
51
  export declare const DEFAULT_MAX_SESSIONS = 5;
52
52
  /** Default maximum API tokens per account. */
53
53
  export declare const DEFAULT_MAX_TOKENS = 10;
54
+ /**
55
+ * Default minimum wall-clock time (ms) for a login failure (401) response.
56
+ *
57
+ * Picked to exceed the p99 of every 401 code path (Argon2id dominates at
58
+ * ~100ms, plus DB + overhead). The handler races failure work against
59
+ * `sleep(floor + jitter)` via `await`, so observed response time = max(work,
60
+ * delay). Found-vs-not-found and rate-limit-skipped-vs-not paths converge.
61
+ * Only 401 is padded — 429 stays fast by design to keep rate-limit DoS
62
+ * handling cheap.
63
+ */
64
+ export declare const DEFAULT_LOGIN_FAIL_FLOOR_MS = 250;
65
+ /**
66
+ * Default uniform jitter window (±ms) layered on the floor.
67
+ *
68
+ * Random jitter prevents a stable clamp point from leaking whenever a path
69
+ * occasionally exceeds the floor. `Math.random` is sufficient — we only need
70
+ * unpredictability of the exact delay, not cryptographic guarantees.
71
+ */
72
+ export declare const DEFAULT_LOGIN_FAIL_JITTER_MS = 25;
54
73
  /**
55
74
  * Shared options for route factories that create sessions and rate-limit by IP.
56
75
  *
@@ -72,6 +91,17 @@ export interface AccountRouteOptions extends AuthSessionRouteOptions {
72
91
  max_sessions?: number | null;
73
92
  /** Max API tokens per account. Evicts oldest on creation. Default 10, `null` disables. */
74
93
  max_tokens?: number | null;
94
+ /**
95
+ * Minimum wall-clock time (ms) for login 401 responses. Set to `0` or a
96
+ * negative number to disable (e.g., in tests). Default
97
+ * `DEFAULT_LOGIN_FAIL_FLOOR_MS`.
98
+ */
99
+ login_fail_floor_ms?: number;
100
+ /**
101
+ * Uniform jitter window (±ms) layered on the floor. Set to `0` to disable
102
+ * jitter while keeping the floor. Default `DEFAULT_LOGIN_FAIL_JITTER_MS`.
103
+ */
104
+ login_fail_jitter_ms?: number;
75
105
  }
76
106
  /**
77
107
  * Create account route specs for session-based auth.
@@ -1 +1 @@
1
- {"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AA+BxD,OAAO,EAAoC,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAExF,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAElF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAGhD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gCAAgC,GAAI,UAAU,oBAAoB,KAAG,SA0BhF,CAAC;AAEH,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACpC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE;QAAC,SAAS,EAAE,OAAO,CAAA;KAAC,CAAC;CACxC;AAED,4CAA4C;AAC5C,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,8CAA8C;AAC9C,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,kFAAkF;IAClF,eAAe,EAAE,WAAW,GAAG,IAAI,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,uBAAuB;IACnE,4FAA4F;IAC5F,0BAA0B,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/C,2FAA2F;IAC3F,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,0FAA0F;IAC1F,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,GACtC,MAAM,gBAAgB,EACtB,SAAS,mBAAmB,KAC1B,KAAK,CAAC,SAAS,CAkYjB,CAAC"}
1
+ {"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AA+BxD,OAAO,EAAoC,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAExF,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAElF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAGhD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gCAAgC,GAAI,UAAU,oBAAoB,KAAG,SA0BhF,CAAC;AAEH,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACpC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE;QAAC,SAAS,EAAE,OAAO,CAAA;KAAC,CAAC;CACxC;AAED,4CAA4C;AAC5C,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,8CAA8C;AAC9C,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAQ/C;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,kFAAkF;IAClF,eAAe,EAAE,WAAW,GAAG,IAAI,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,uBAAuB;IACnE,4FAA4F;IAC5F,0BAA0B,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/C,2FAA2F;IAC3F,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,0FAA0F;IAC1F,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,GACtC,MAAM,gBAAgB,EACtB,SAAS,mBAAmB,KAC1B,KAAK,CAAC,SAAS,CAgZjB,CAAC"}
@@ -77,6 +77,31 @@ export const create_account_status_route_spec = (options) => ({
77
77
  export const DEFAULT_MAX_SESSIONS = 5;
78
78
  /** Default maximum API tokens per account. */
79
79
  export const DEFAULT_MAX_TOKENS = 10;
80
+ /**
81
+ * Default minimum wall-clock time (ms) for a login failure (401) response.
82
+ *
83
+ * Picked to exceed the p99 of every 401 code path (Argon2id dominates at
84
+ * ~100ms, plus DB + overhead). The handler races failure work against
85
+ * `sleep(floor + jitter)` via `await`, so observed response time = max(work,
86
+ * delay). Found-vs-not-found and rate-limit-skipped-vs-not paths converge.
87
+ * Only 401 is padded — 429 stays fast by design to keep rate-limit DoS
88
+ * handling cheap.
89
+ */
90
+ export const DEFAULT_LOGIN_FAIL_FLOOR_MS = 250;
91
+ /**
92
+ * Default uniform jitter window (±ms) layered on the floor.
93
+ *
94
+ * Random jitter prevents a stable clamp point from leaking whenever a path
95
+ * occasionally exceeds the floor. `Math.random` is sufficient — we only need
96
+ * unpredictability of the exact delay, not cryptographic guarantees.
97
+ */
98
+ export const DEFAULT_LOGIN_FAIL_JITTER_MS = 25;
99
+ const login_fail_delay = (floor_ms, jitter_ms) => {
100
+ if (floor_ms <= 0)
101
+ return Promise.resolve();
102
+ const jitter = jitter_ms > 0 ? Math.floor(Math.random() * (jitter_ms * 2 + 1)) - jitter_ms : 0;
103
+ return new Promise((resolve) => setTimeout(resolve, floor_ms + jitter));
104
+ };
80
105
  /**
81
106
  * Create account route specs for session-based auth.
82
107
  *
@@ -88,7 +113,7 @@ export const DEFAULT_MAX_TOKENS = 10;
88
113
  */
89
114
  export const create_account_route_specs = (deps, options) => {
90
115
  const { keyring, password, on_audit_event } = deps;
91
- const { session_options, ip_rate_limiter, login_account_rate_limiter, max_sessions = DEFAULT_MAX_SESSIONS, max_tokens = DEFAULT_MAX_TOKENS, } = options;
116
+ const { session_options, ip_rate_limiter, login_account_rate_limiter, max_sessions = DEFAULT_MAX_SESSIONS, max_tokens = DEFAULT_MAX_TOKENS, login_fail_floor_ms = DEFAULT_LOGIN_FAIL_FLOOR_MS, login_fail_jitter_ms = DEFAULT_LOGIN_FAIL_JITTER_MS, } = options;
92
117
  return [
93
118
  {
94
119
  method: 'POST',
@@ -113,28 +138,37 @@ export const create_account_route_specs = (deps, options) => {
113
138
  }
114
139
  const { username: raw_username, password: pw } = get_route_input(c);
115
140
  const username = raw_username.trim().toLowerCase();
116
- // Per-account rate limit check (after input parsing, before DB work)
141
+ // DB lookup first so we can key the per-account rate limit by a canonical value
142
+ // (account.id) rather than the submitted identifier. Otherwise an attacker could
143
+ // alternate between username and email to double the per-account bucket.
144
+ const account = await query_account_by_username_or_email(route, username);
145
+ const account_rate_key = account ? account.id : username;
146
+ // Per-account rate limit check (after DB lookup so the key is canonical)
117
147
  if (login_account_rate_limiter) {
118
- const check = login_account_rate_limiter.check(username);
148
+ const check = login_account_rate_limiter.check(account_rate_key);
119
149
  if (!check.allowed) {
120
150
  return rate_limit_exceeded_response(c, check.retry_after);
121
151
  }
122
152
  }
123
- const account = await query_account_by_username_or_email(route, username);
153
+ // Start the minimum-delay timer concurrently with failure work.
154
+ // Observed response time is max(work, delay) so all 401 paths
155
+ // (found-wrong-pw, not-found) return in similar time.
156
+ const delay = login_fail_delay(login_fail_floor_ms, login_fail_jitter_ms);
124
157
  if (!account) {
125
- // enumeration prevention: verify_dummy equalizes timing, and both failure
126
- // paths return identical errors with identical rate limiting behavior
158
+ // enumeration prevention: verify_dummy equalizes Argon2id timing;
159
+ // login_fail_delay equalizes every other path difference.
127
160
  await password.verify_dummy(pw);
128
161
  if (ip_rate_limiter && ip)
129
162
  ip_rate_limiter.record(ip);
130
163
  if (login_account_rate_limiter)
131
- login_account_rate_limiter.record(username);
164
+ login_account_rate_limiter.record(account_rate_key);
132
165
  void audit_log_fire_and_forget(route, {
133
166
  event_type: 'login',
134
167
  outcome: 'failure',
135
168
  ip: get_client_ip(c),
136
169
  metadata: { username },
137
170
  }, deps.log, on_audit_event);
171
+ await delay;
138
172
  return c.json({ error: ERROR_INVALID_CREDENTIALS }, 401);
139
173
  }
140
174
  const valid = await password.verify_password(pw, account.password_hash);
@@ -142,7 +176,7 @@ export const create_account_route_specs = (deps, options) => {
142
176
  if (ip_rate_limiter && ip)
143
177
  ip_rate_limiter.record(ip);
144
178
  if (login_account_rate_limiter)
145
- login_account_rate_limiter.record(username);
179
+ login_account_rate_limiter.record(account_rate_key);
146
180
  void audit_log_fire_and_forget(route, {
147
181
  event_type: 'login',
148
182
  outcome: 'failure',
@@ -150,13 +184,14 @@ export const create_account_route_specs = (deps, options) => {
150
184
  ip: get_client_ip(c),
151
185
  metadata: { username },
152
186
  }, deps.log, on_audit_event);
187
+ await delay;
153
188
  return c.json({ error: ERROR_INVALID_CREDENTIALS }, 401);
154
189
  }
155
190
  // Successful login — reset rate limits
156
191
  if (ip_rate_limiter && ip)
157
192
  ip_rate_limiter.reset(ip);
158
193
  if (login_account_rate_limiter)
159
- login_account_rate_limiter.reset(username);
194
+ login_account_rate_limiter.reset(account_rate_key);
160
195
  await create_session_and_set_cookie({
161
196
  keyring,
162
197
  deps: route,
@@ -1 +1 @@
1
- {"version":3,"file":"admin_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/admin_routes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAA8C,KAAK,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAGpG,OAAO,EAAoC,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAUxF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAShD,qCAAqC;AACrC,MAAM,WAAW,iBAAiB;IACjC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,gBAAgB,CAAC,EACtD,UAAU,iBAAiB,KACzB,KAAK,CAAC,SAAS,CAoMjB,CAAC"}
1
+ {"version":3,"file":"admin_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/admin_routes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAA8C,KAAK,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAGpG,OAAO,EAAoC,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAcxF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAShD,qCAAqC;AACrC,MAAM,WAAW,iBAAiB;IACjC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,gBAAgB,CAAC,EACtD,UAAU,iBAAiB,KACzB,KAAK,CAAC,SAAS,CAkPjB,CAAC"}
@@ -11,7 +11,7 @@ import { AdminAccountEntryJson } from './account_schema.js';
11
11
  import { require_request_context } from './request_context.js';
12
12
  import { get_route_input, get_route_params } from '../http/route_spec.js';
13
13
  import { query_account_by_id, query_actor_by_account, query_admin_account_list, } from './account_queries.js';
14
- import { query_grant_permit, query_revoke_permit } from './permit_queries.js';
14
+ import { query_grant_permit, query_permit_find_active_role_for_actor, query_revoke_permit, } from './permit_queries.js';
15
15
  import { query_session_revoke_all_for_account } from './session_queries.js';
16
16
  import { query_revoke_all_api_tokens_for_account } from './api_token_queries.js';
17
17
  import { audit_log_fire_and_forget } from './audit_log_queries.js';
@@ -70,17 +70,26 @@ export const create_admin_account_route_specs = (deps, options) => {
70
70
  handler: async (c, route) => {
71
71
  const { account_id } = get_route_params(c);
72
72
  const { role: role_name } = get_route_input(c);
73
+ const ctx = require_request_context(c);
73
74
  // Enforce web_grantable — direct API calls must respect the same
74
75
  // restrictions as the UI. Keeper role can only be granted via daemon token.
75
76
  const rc = role_options.get(role_name);
76
77
  if (!rc?.web_grantable) {
78
+ void audit_log_fire_and_forget(route, {
79
+ event_type: 'permit_grant',
80
+ outcome: 'failure',
81
+ actor_id: ctx.actor.id,
82
+ account_id: ctx.account.id,
83
+ target_account_id: account_id,
84
+ ip: get_client_ip(c),
85
+ metadata: { role: role_name },
86
+ }, deps.log, on_audit_event);
77
87
  return c.json({ error: ERROR_ROLE_NOT_WEB_GRANTABLE }, 403);
78
88
  }
79
89
  const actor = await query_actor_by_account(route, account_id);
80
90
  if (!actor) {
81
91
  return c.json({ error: ERROR_ACCOUNT_NOT_FOUND }, 404);
82
92
  }
83
- const ctx = require_request_context(c);
84
93
  const permit = await query_grant_permit(route, {
85
94
  actor_id: actor.id,
86
95
  role: role_name,
@@ -162,6 +171,7 @@ export const create_admin_account_route_specs = (deps, options) => {
162
171
  input: z.null(),
163
172
  output: z.strictObject({ ok: z.literal(true), revoked: z.literal(true) }),
164
173
  errors: {
174
+ 403: z.looseObject({ error: z.literal(ERROR_ROLE_NOT_WEB_GRANTABLE) }),
165
175
  404: z.looseObject({
166
176
  error: z.enum([ERROR_ACCOUNT_NOT_FOUND, ERROR_PERMIT_NOT_FOUND]),
167
177
  }),
@@ -174,6 +184,27 @@ export const create_admin_account_route_specs = (deps, options) => {
174
184
  if (!target_actor) {
175
185
  return c.json({ error: ERROR_ACCOUNT_NOT_FOUND }, 404);
176
186
  }
187
+ // Look up the permit's role so we can enforce web_grantable symmetrically
188
+ // with the grant route. Without this, an admin could revoke the keeper
189
+ // permit via the web, breaking the "only daemon token manages keeper" invariant.
190
+ // Route wraps POST handlers in a transaction, so SELECT-then-UPDATE is atomic.
191
+ const permit_row = await query_permit_find_active_role_for_actor(route, permit_id, target_actor.id);
192
+ if (!permit_row) {
193
+ return c.json({ error: ERROR_PERMIT_NOT_FOUND }, 404);
194
+ }
195
+ const rc = role_options.get(permit_row.role);
196
+ if (!rc?.web_grantable) {
197
+ void audit_log_fire_and_forget(route, {
198
+ event_type: 'permit_revoke',
199
+ outcome: 'failure',
200
+ actor_id: ctx.actor.id,
201
+ account_id: ctx.account.id,
202
+ target_account_id: account_id,
203
+ ip: get_client_ip(c),
204
+ metadata: { role: permit_row.role, permit_id },
205
+ }, deps.log, on_audit_event);
206
+ return c.json({ error: ERROR_ROLE_NOT_WEB_GRANTABLE }, 403);
207
+ }
177
208
  const result = await query_revoke_permit(route, permit_id, target_actor.id, ctx.actor.id);
178
209
  if (!result) {
179
210
  return c.json({ error: ERROR_PERMIT_NOT_FOUND }, 404);
@@ -9,6 +9,7 @@
9
9
  import type { Logger } from '@fuzdev/fuz_util/log.js';
10
10
  import type { RouteSpec } from '../http/route_spec.js';
11
11
  import { type SseStream, type SseNotification } from '../realtime/sse.js';
12
+ import type { SubscribeOptions } from '../realtime/subscriber_registry.js';
12
13
  /** Options for audit log route specs. */
13
14
  export interface AuditLogRouteOptions {
14
15
  /** Role required to access audit routes. Default `'admin'`. */
@@ -19,7 +20,7 @@ export interface AuditLogRouteOptions {
19
20
  * as an identity key — enabling `close_by_identity()` for auth revocation.
20
21
  */
21
22
  stream?: {
22
- subscribe: (stream: SseStream<SseNotification>, channels?: Array<string>, identity?: string) => () => void;
23
+ subscribe: (stream: SseStream<SseNotification>, options?: SubscribeOptions) => () => void;
23
24
  log: Logger;
24
25
  };
25
26
  }
@@ -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;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAQpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAQrD,OAAO,EAAsB,KAAK,SAAS,EAAE,KAAK,eAAe,EAAC,MAAM,oBAAoB,CAAC;AAU7F,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACpC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,MAAM,CAAC,EAAE;QACR,SAAS,EAAE,CACV,MAAM,EAAE,SAAS,CAAC,eAAe,CAAC,EAClC,QAAQ,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,EACxB,QAAQ,CAAC,EAAE,MAAM,KACb,MAAM,IAAI,CAAC;QAChB,GAAG,EAAE,MAAM,CAAC;KACZ,CAAC;CACF;AAED;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,GAAI,UAAU,oBAAoB,KAAG,KAAK,CAAC,SAAS,CAuF5F,CAAC"}
1
+ {"version":3,"file":"audit_log_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAQpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAQrD,OAAO,EAAsB,KAAK,SAAS,EAAE,KAAK,eAAe,EAAC,MAAM,oBAAoB,CAAC;AAC7F,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oCAAoC,CAAC;AAUzE,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;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,GAAI,UAAU,oBAAoB,KAAG,KAAK,CAAC,SAAS,CAgG5F,CAAC"}
@@ -12,7 +12,7 @@ import { AUDIT_LOG_DEFAULT_LIMIT, query_audit_log_list_with_usernames, query_aud
12
12
  import { query_session_list_all_active } from './session_queries.js';
13
13
  import { ERROR_INVALID_EVENT_TYPE } from '../http/error_schemas.js';
14
14
  import { create_sse_response } from '../realtime/sse.js';
15
- import { require_request_context } from './request_context.js';
15
+ import { AUTH_SESSION_TOKEN_HASH_KEY, require_request_context } from './request_context.js';
16
16
  // TODO upstream to fuz_util
17
17
  /** Parse a string to an integer, returning `undefined` for non-numeric input (including `NaN`). */
18
18
  const parse_int_or_undefined = (value) => {
@@ -95,8 +95,17 @@ export const create_audit_log_route_specs = (options) => {
95
95
  output: z.null(), // SSE — no JSON response
96
96
  handler: (c) => {
97
97
  const ctx = require_request_context(c);
98
+ // scope = session hash (capped → tabs-per-session limit and
99
+ // session-specific `session_revoke` close). groups = [account_id]
100
+ // (uncapped → coarse close on permit_revoke / session_revoke_all
101
+ // / password_change).
102
+ const token_hash = c.get(AUTH_SESSION_TOKEN_HASH_KEY) ?? null;
98
103
  const { response, stream } = create_sse_response(c, log);
99
- const unsubscribe = subscribe(stream, ['audit_log'], ctx.account.id);
104
+ const unsubscribe = subscribe(stream, {
105
+ channels: ['audit_log'],
106
+ scope: token_hash ?? undefined,
107
+ groups: [ctx.account.id],
108
+ });
100
109
  stream.on_close(unsubscribe);
101
110
  return response;
102
111
  },
@@ -75,7 +75,7 @@ export declare const AUDIT_METADATA_SCHEMAS: {
75
75
  }, z.core.$loose>;
76
76
  permit_grant: z.ZodObject<{
77
77
  role: z.ZodString;
78
- permit_id: z.ZodString;
78
+ permit_id: z.ZodOptional<z.ZodString>;
79
79
  }, z.core.$loose>;
80
80
  permit_revoke: z.ZodObject<{
81
81
  role: z.ZodString;
@@ -1 +1 @@
1
- {"version":3,"file":"audit_log_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"audit_log_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_schema.ts"],"names":[],"mappings":"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"}
@@ -52,7 +52,9 @@ export const AUDIT_METADATA_SCHEMAS = {
52
52
  token_create: z.looseObject({ token_id: z.string(), name: z.string() }),
53
53
  token_revoke: z.looseObject({ token_id: z.string() }),
54
54
  token_revoke_all: z.looseObject({ count: z.number() }),
55
- permit_grant: z.looseObject({ role: z.string(), permit_id: z.string() }),
55
+ // `permit_id` is optional on `permit_grant` because failed grants
56
+ // (e.g. `web_grantable` denied) never produce a permit row.
57
+ permit_grant: z.looseObject({ role: z.string(), permit_id: z.string().optional() }),
56
58
  permit_revoke: z.looseObject({ role: z.string(), permit_id: z.string() }),
57
59
  invite_create: z.looseObject({
58
60
  invite_id: z.string(),
@@ -18,6 +18,25 @@ import type { Permit, GrantPermitInput } from './account_schema.js';
18
18
  * @returns the created or existing active permit
19
19
  */
20
20
  export declare const query_grant_permit: (deps: QueryDeps, input: GrantPermitInput) => Promise<Permit>;
21
+ /**
22
+ * Look up the role of an active permit, constrained to a specific actor.
23
+ *
24
+ * Used by admin routes to inspect the permit's role before acting
25
+ * (e.g., enforcing `web_grantable` on revoke). The actor constraint
26
+ * mirrors `query_revoke_permit` so IDOR protection is consistent:
27
+ * a caller can only see permits belonging to the target actor.
28
+ *
29
+ * Returns `null` if the permit is not found, already revoked, or
30
+ * belongs to a different actor.
31
+ *
32
+ * @param deps - query dependencies
33
+ * @param permit_id - the permit id to look up
34
+ * @param actor_id - the actor that must own the permit
35
+ * @returns `{role}` on a match, or `null`
36
+ */
37
+ export declare const query_permit_find_active_role_for_actor: (deps: QueryDeps, permit_id: string, actor_id: string) => Promise<{
38
+ role: string;
39
+ } | null>;
21
40
  /**
22
41
  * Revoke a permit by id, constrained to a specific actor.
23
42
  *
@@ -1 +1 @@
1
- {"version":3,"file":"permit_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/permit_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAC,MAAM,EAAE,gBAAgB,EAAC,MAAM,qBAAqB,CAAC;AAGlE;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC9B,MAAM,SAAS,EACf,OAAO,gBAAgB,KACrB,OAAO,CAAC,MAAM,CAiBhB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,WAAW,MAAM,EACjB,UAAU,MAAM,EAChB,YAAY,MAAM,GAAG,IAAI,KACvB,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,GAAG,IAAI,CAQ3C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kCAAkC,GAC9C,MAAM,SAAS,EACf,UAAU,MAAM,KACd,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CASvB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GACjC,MAAM,SAAS,EACf,UAAU,MAAM,EAChB,MAAM,MAAM,KACV,OAAO,CAAC,OAAO,CAYjB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,UAAU,MAAM,KACd,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAKvB,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,MAAM,MAAM,KACV,OAAO,CAAC,MAAM,GAAG,IAAI,CAavB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,SAAS,EACf,UAAU,MAAM,EAChB,MAAM,MAAM,EACZ,YAAY,MAAM,GAAG,IAAI,KACvB,OAAO,CAAC,OAAO,CAQjB,CAAC"}
1
+ {"version":3,"file":"permit_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/permit_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAC,MAAM,EAAE,gBAAgB,EAAC,MAAM,qBAAqB,CAAC;AAGlE;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC9B,MAAM,SAAS,EACf,OAAO,gBAAgB,KACrB,OAAO,CAAC,MAAM,CAiBhB,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,uCAAuC,GACnD,MAAM,SAAS,EACf,WAAW,MAAM,EACjB,UAAU,MAAM,KACd,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,GAAG,IAAI,CAO/B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,WAAW,MAAM,EACjB,UAAU,MAAM,EAChB,YAAY,MAAM,GAAG,IAAI,KACvB,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,GAAG,IAAI,CAQ3C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kCAAkC,GAC9C,MAAM,SAAS,EACf,UAAU,MAAM,KACd,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CASvB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GACjC,MAAM,SAAS,EACf,UAAU,MAAM,EAChB,MAAM,MAAM,KACV,OAAO,CAAC,OAAO,CAYjB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,UAAU,MAAM,KACd,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAKvB,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,MAAM,MAAM,KACV,OAAO,CAAC,MAAM,GAAG,IAAI,CAavB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,SAAS,EACf,UAAU,MAAM,EAChB,MAAM,MAAM,EACZ,YAAY,MAAM,GAAG,IAAI,KACvB,OAAO,CAAC,OAAO,CAQjB,CAAC"}
@@ -29,6 +29,27 @@ export const query_grant_permit = async (deps, input) => {
29
29
  WHERE actor_id = $1 AND role = $2 AND revoked_at IS NULL`, [input.actor_id, input.role]);
30
30
  return assert_row(existing, 'idempotent permit grant');
31
31
  };
32
+ /**
33
+ * Look up the role of an active permit, constrained to a specific actor.
34
+ *
35
+ * Used by admin routes to inspect the permit's role before acting
36
+ * (e.g., enforcing `web_grantable` on revoke). The actor constraint
37
+ * mirrors `query_revoke_permit` so IDOR protection is consistent:
38
+ * a caller can only see permits belonging to the target actor.
39
+ *
40
+ * Returns `null` if the permit is not found, already revoked, or
41
+ * belongs to a different actor.
42
+ *
43
+ * @param deps - query dependencies
44
+ * @param permit_id - the permit id to look up
45
+ * @param actor_id - the actor that must own the permit
46
+ * @returns `{role}` on a match, or `null`
47
+ */
48
+ export const query_permit_find_active_role_for_actor = async (deps, permit_id, actor_id) => {
49
+ const row = await deps.db.query_one(`SELECT role FROM permit
50
+ WHERE id = $1 AND actor_id = $2 AND revoked_at IS NULL`, [permit_id, actor_id]);
51
+ return row ?? null;
52
+ };
32
53
  /**
33
54
  * Revoke a permit by id, constrained to a specific actor.
34
55
  *
@@ -23,6 +23,16 @@ export interface RequestContext {
23
23
  }
24
24
  /** Hono context variable name for the request context. */
25
25
  export declare const REQUEST_CONTEXT_KEY = "request_context";
26
+ /**
27
+ * Hono context variable name for the authenticated session token hash.
28
+ *
29
+ * Set by `create_request_context_middleware` after a successful session lookup.
30
+ * `null` when the request is unauthenticated or authenticated via a non-session
31
+ * credential (bearer token, daemon token). Exposed so handlers can scope
32
+ * per-session resources (e.g., SSE stream identity for targeted disconnection
33
+ * on `session_revoke`) without re-hashing the token.
34
+ */
35
+ export declare const AUTH_SESSION_TOKEN_HASH_KEY = "auth_session_token_hash";
26
36
  /**
27
37
  * Get the request context from a Hono context, or `null` if unauthenticated.
28
38
  *
@@ -1 +1 @@
1
- {"version":3,"file":"request_context.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/request_context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAE,iBAAiB,EAAC,MAAM,MAAM,CAAC;AACrD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAAC,KAAK,OAAO,EAAE,KAAK,KAAK,EAAoB,KAAK,MAAM,EAAC,MAAM,qBAAqB,CAAC;AAQ5F,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAOnD,kEAAkE;AAClE,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACvB;AAED,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,GAAG,OAAO,KAAG,cAAc,GAAG,IAEjE,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,GAAI,GAAG,OAAO,KAAG,cAMpD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,GAAI,KAAK,cAAc,EAAE,MAAM,MAAM,EAAE,MAAK,IAAiB,KAAG,OAChB,CAAC;AAEtE;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iCAAiC,GAC7C,MAAM,SAAS,EACf,KAAK,MAAM,EACX,4BAAuC,KACrC,iBAqCF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,iBAM1B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,iBAW3C,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,GAC3B,KAAK,cAAc,EACnB,MAAM,SAAS,KACb,OAAO,CAAC,cAAc,CAGxB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,GACjC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAS/B,CAAC"}
1
+ {"version":3,"file":"request_context.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/request_context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAE,iBAAiB,EAAC,MAAM,MAAM,CAAC;AACrD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAAC,KAAK,OAAO,EAAE,KAAK,KAAK,EAAoB,KAAK,MAAM,EAAC,MAAM,qBAAqB,CAAC;AAQ5F,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAOnD,kEAAkE;AAClE,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACvB;AAED,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,4BAA4B,CAAC;AAErE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,GAAG,OAAO,KAAG,cAAc,GAAG,IAEjE,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,GAAI,GAAG,OAAO,KAAG,cAMpD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,GAAI,KAAK,cAAc,EAAE,MAAM,MAAM,EAAE,MAAK,IAAiB,KAAG,OAChB,CAAC;AAEtE;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iCAAiC,GAC7C,MAAM,SAAS,EACf,KAAK,MAAM,EACX,4BAAuC,KACrC,iBAyCF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,iBAM1B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,iBAW3C,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,GAC3B,KAAK,cAAc,EACnB,MAAM,SAAS,KACb,OAAO,CAAC,cAAc,CAGxB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,GACjC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAS/B,CAAC"}
@@ -19,6 +19,16 @@ import { CREDENTIAL_TYPE_KEY } from '../hono_context.js';
19
19
  import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, } from '../http/error_schemas.js';
20
20
  /** Hono context variable name for the request context. */
21
21
  export const REQUEST_CONTEXT_KEY = 'request_context';
22
+ /**
23
+ * Hono context variable name for the authenticated session token hash.
24
+ *
25
+ * Set by `create_request_context_middleware` after a successful session lookup.
26
+ * `null` when the request is unauthenticated or authenticated via a non-session
27
+ * credential (bearer token, daemon token). Exposed so handlers can scope
28
+ * per-session resources (e.g., SSE stream identity for targeted disconnection
29
+ * on `session_revoke`) without re-hashing the token.
30
+ */
31
+ export const AUTH_SESSION_TOKEN_HASH_KEY = 'auth_session_token_hash';
22
32
  /**
23
33
  * Get the request context from a Hono context, or `null` if unauthenticated.
24
34
  *
@@ -78,6 +88,7 @@ export const create_request_context_middleware = (deps, log, session_context_key
78
88
  if (!session_token) {
79
89
  c.set(REQUEST_CONTEXT_KEY, null);
80
90
  c.set(CREDENTIAL_TYPE_KEY, null);
91
+ c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
81
92
  await next();
82
93
  return;
83
94
  }
@@ -86,6 +97,7 @@ export const create_request_context_middleware = (deps, log, session_context_key
86
97
  if (!session) {
87
98
  c.set(REQUEST_CONTEXT_KEY, null);
88
99
  c.set(CREDENTIAL_TYPE_KEY, null);
100
+ c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
89
101
  await next();
90
102
  return;
91
103
  }
@@ -93,11 +105,13 @@ export const create_request_context_middleware = (deps, log, session_context_key
93
105
  if (!ctx) {
94
106
  c.set(REQUEST_CONTEXT_KEY, null);
95
107
  c.set(CREDENTIAL_TYPE_KEY, null);
108
+ c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
96
109
  await next();
97
110
  return;
98
111
  }
99
112
  c.set(REQUEST_CONTEXT_KEY, ctx);
100
113
  c.set(CREDENTIAL_TYPE_KEY, 'session');
114
+ c.set(AUTH_SESSION_TOKEN_HASH_KEY, token_hash);
101
115
  // Touch session (fire-and-forget, don't block the request)
102
116
  void session_touch_fire_and_forget(deps, token_hash, c.var.pending_effects, log);
103
117
  await next();
@@ -37,6 +37,13 @@ declare module 'hono' {
37
37
  validated_query: unknown;
38
38
  /** How the request was authenticated (`'session'`, `'api_token'`, or `'daemon_token'`). */
39
39
  credential_type: CredentialType | null;
40
+ /**
41
+ * blake3 hash of the authenticated session token, or `null` for non-session
42
+ * credentials. Set by `create_request_context_middleware`. Used to scope
43
+ * per-session resources (e.g., SSE stream identity for `session_revoke`
44
+ * disconnection) without re-hashing the cookie in every handler.
45
+ */
46
+ auth_session_token_hash: string | null;
40
47
  /**
41
48
  * Pending fire-and-forget effects for this request (audit logs, usage tracking, etc.).
42
49
  * Initialized by `create_app_server`. In test mode (`await_pending_effects: true`),
@@ -1 +1 @@
1
- {"version":3,"file":"hono_context.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/hono_context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAE9D,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB,mDAAoD,CAAC;AAElF,yDAAyD;AACzD,eAAO,MAAM,cAAc;;;;EAA2B,CAAC;AACvD,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD,OAAO,QAAQ,MAAM,CAAC;IACrB,UAAU,kBAAkB;QAC3B,+DAA+D;QAC/D,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC,eAAe,EAAE,OAAO,CAAC;QACzB,gBAAgB,EAAE,OAAO,CAAC;QAC1B,eAAe,EAAE,OAAO,CAAC;QACzB,2FAA2F;QAC3F,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC;;;;WAIG;QACH,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;KACtC;CACD"}
1
+ {"version":3,"file":"hono_context.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/hono_context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAE9D,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB,mDAAoD,CAAC;AAElF,yDAAyD;AACzD,eAAO,MAAM,cAAc;;;;EAA2B,CAAC;AACvD,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD,OAAO,QAAQ,MAAM,CAAC;IACrB,UAAU,kBAAkB;QAC3B,+DAA+D;QAC/D,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC,eAAe,EAAE,OAAO,CAAC;QACzB,gBAAgB,EAAE,OAAO,CAAC;QAC1B,eAAe,EAAE,OAAO,CAAC;QACzB,2FAA2F;QAC3F,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC;;;;;WAKG;QACH,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;QACvC;;;;WAIG;QACH,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;KACtC;CACD"}
@@ -12,13 +12,16 @@
12
12
  */
13
13
  import type { Logger } from '@fuzdev/fuz_util/log.js';
14
14
  import { type AuditLogEvent } from '../auth/audit_log_schema.js';
15
- import { SubscriberRegistry } from './subscriber_registry.js';
15
+ import { SubscriberRegistry, type SubscribeOptions } from './subscriber_registry.js';
16
16
  import type { SseStream, SseNotification, EventSpec } from './sse.js';
17
17
  /**
18
18
  * Audit event types that trigger SSE stream disconnection.
19
19
  *
20
20
  * `permit_revoke` requires the revoked role to match the guard's `required_role`.
21
- * `session_revoke_all` and `password_change` close unconditionally for the target account.
21
+ * `session_revoke_all` and `password_change` close every stream for the target account.
22
+ * `session_revoke` closes only the stream tied to the specific revoked session
23
+ * (matched by the blake3 session hash in `event.metadata.session_id`) — closing
24
+ * all of a user's streams for a single-session revoke would be over-aggressive.
22
25
  */
23
26
  export declare const DISCONNECT_EVENT_TYPES: ReadonlySet<string>;
24
27
  /**
@@ -46,7 +49,7 @@ export declare const create_sse_auth_guard: <T>(registry: SubscriberRegistry<T>,
46
49
  */
47
50
  export interface AuditLogSse {
48
51
  /** Subscribe function — pass as part of `stream` option to `create_audit_log_route_specs`. */
49
- subscribe: (stream: SseStream<SseNotification>, channels?: Array<string>, identity?: string) => () => void;
52
+ subscribe: (stream: SseStream<SseNotification>, options?: SubscribeOptions) => () => void;
50
53
  /** Logger — pass as part of `stream` option to `create_audit_log_route_specs`. */
51
54
  log: Logger;
52
55
  /** Combined broadcast + guard callback. Pass as `on_audit_event` on `CreateAppBackendOptions`. */
@@ -85,9 +88,26 @@ export interface AuditLogSse {
85
88
  * Pass to `create_app_server`'s `event_specs` for surface generation and DEV validation.
86
89
  */
87
90
  export declare const AUDIT_LOG_EVENT_SPECS: Array<EventSpec>;
91
+ /**
92
+ * Default max concurrent SSE subscribers per session scope for the audit log.
93
+ *
94
+ * The audit log SSE subscribes with `scope = session_hash` and
95
+ * `groups = [account_id]`. Only `scope` is capped — so this limits tabs
96
+ * per session. An account's total streams across all sessions is bounded
97
+ * transitively by `max_sessions × AUDIT_LOG_SSE_MAX_PER_SCOPE`. 10 tabs
98
+ * per session is a comfortable ceiling for normal use; consumers raising
99
+ * it above ~50 should consider server-side connection limits.
100
+ */
101
+ export declare const AUDIT_LOG_SSE_MAX_PER_SCOPE = 10;
88
102
  export declare const create_audit_log_sse: (options: {
89
103
  /** Role required to access the SSE endpoint. Default `'admin'`. */
90
104
  role?: string;
91
105
  log: Logger;
106
+ /**
107
+ * Max concurrent SSE subscribers per session scope. On overflow, the oldest
108
+ * matching subscriber is closed. Default `AUDIT_LOG_SSE_MAX_PER_SCOPE`.
109
+ * Pass `null` to disable the cap.
110
+ */
111
+ max_per_scope?: number | null;
92
112
  }) => AuditLogSse;
93
113
  //# sourceMappingURL=sse_auth_guard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sse_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAC,kBAAkB,EAAC,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAC,SAAS,EAAE,eAAe,EAAE,SAAS,EAAC,MAAM,UAAU,CAAC;AAEpE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAIrD,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,qBAAqB,GAAI,CAAC,EACtC,UAAU,kBAAkB,CAAC,CAAC,CAAC,EAC/B,eAAe,MAAM,EACrB,KAAK,MAAM,KACT,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAqBjC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,8FAA8F;IAC9F,SAAS,EAAE,CACV,MAAM,EAAE,SAAS,CAAC,eAAe,CAAC,EAClC,QAAQ,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,EACxB,QAAQ,CAAC,EAAE,MAAM,KACb,MAAM,IAAI,CAAC;IAChB,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,kGAAkG;IAClG,cAAc,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,yEAAyE;IACzE,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;CAC9C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,SAAS,CAOlD,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,SAAS;IAC7C,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACZ,KAAG,WAcH,CAAC"}
1
+ {"version":3,"file":"sse_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAC,kBAAkB,EAAE,KAAK,gBAAgB,EAAC,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAC,SAAS,EAAE,eAAe,EAAE,SAAS,EAAC,MAAM,UAAU,CAAC;AAEpE;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAKrD,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,qBAAqB,GAAI,CAAC,EACtC,UAAU,kBAAkB,CAAC,CAAC,CAAC,EAC/B,eAAe,MAAM,EACrB,KAAK,MAAM,KACT,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CA2CjC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,8FAA8F;IAC9F,SAAS,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;IAC1F,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,kGAAkG;IAClG,cAAc,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,yEAAyE;IACzE,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;CAC9C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,SAAS,CAOlD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C,eAAO,MAAM,oBAAoB,GAAI,SAAS;IAC7C,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,KAAG,WAgBH,CAAC"}