@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.
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/auth/account_routes.d.ts +30 -0
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +44 -9
- package/dist/auth/admin_routes.d.ts.map +1 -1
- package/dist/auth/admin_routes.js +33 -2
- package/dist/auth/audit_log_routes.d.ts +2 -1
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +11 -2
- package/dist/auth/audit_log_schema.d.ts +1 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +3 -1
- package/dist/auth/permit_queries.d.ts +19 -0
- package/dist/auth/permit_queries.d.ts.map +1 -1
- package/dist/auth/permit_queries.js +21 -0
- package/dist/auth/request_context.d.ts +10 -0
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +14 -0
- package/dist/hono_context.d.ts +7 -0
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.d.ts +23 -3
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +38 -2
- package/dist/realtime/subscriber_registry.d.ts +62 -17
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +64 -21
- package/dist/server/validate_nginx.d.ts.map +1 -1
- package/dist/server/validate_nginx.js +61 -7
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +8 -8
- package/dist/testing/app_server.d.ts +9 -0
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +4 -3
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +1 -20
- package/dist/testing/error_coverage.d.ts +93 -27
- package/dist/testing/error_coverage.d.ts.map +1 -1
- package/dist/testing/error_coverage.js +160 -67
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +6 -6
- package/dist/testing/integration_helpers.d.ts +17 -0
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +31 -0
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +41 -55
- package/dist/testing/sse_round_trip.d.ts +64 -0
- package/dist/testing/sse_round_trip.d.ts.map +1 -0
- package/dist/testing/sse_round_trip.js +241 -0
- 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;
|
|
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;
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
|
|
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
|
|
126
|
-
//
|
|
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(
|
|
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(
|
|
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(
|
|
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;
|
|
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>,
|
|
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;
|
|
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,
|
|
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":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAItB,oCAAoC;AACpC,eAAO,MAAM,iBAAiB,8PAgBpB,CAAC;AAEX,wCAAwC;AACxC,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;EAA4B,CAAC;AACxD,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,2CAA2C;AAC3C,eAAO,MAAM,YAAY;;;EAAiC,CAAC;AAC3D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
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
|
-
|
|
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,
|
|
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();
|
package/dist/hono_context.d.ts
CHANGED
|
@@ -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
|
|
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>,
|
|
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;
|
|
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"}
|