@fuzdev/fuz_app 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/CLAUDE.md +19 -0
- package/dist/actions/transports_ws_auth_guard.d.ts +31 -0
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +45 -0
- package/dist/auth/CLAUDE.md +27 -5
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +14 -4
- package/dist/auth/audit_log_queries.d.ts +9 -2
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +30 -5
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +4 -1
- package/dist/auth/permit_offer_actions.d.ts.map +1 -1
- package/dist/auth/permit_offer_actions.js +3 -0
- package/dist/auth/session_queries.d.ts +9 -7
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +9 -7
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +4 -1
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +12 -4
- package/dist/http/error_schemas.d.ts +2 -0
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +2 -0
- package/dist/testing/CLAUDE.md +17 -4
- package/dist/testing/app_server.d.ts +11 -0
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +4 -0
- package/dist/testing/attack_surface.d.ts +24 -3
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +32 -2
- package/dist/testing/rpc_helpers.d.ts +9 -6
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +29 -9
- package/dist/testing/surface_invariants.d.ts +26 -3
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +27 -3
- package/package.json +1 -1
package/dist/actions/CLAUDE.md
CHANGED
|
@@ -280,6 +280,25 @@ Event dispatch:
|
|
|
280
280
|
attacker-controlled identifiers. Reacting to them would let an authenticated
|
|
281
281
|
caller close another user's socket by guessing a session hash or token id.
|
|
282
282
|
|
|
283
|
+
`create_ws_logout_closer(transport, log)` is the sibling helper for
|
|
284
|
+
user-initiated `logout` events — kept separate because
|
|
285
|
+
`WS_DISCONNECT_EVENT_TYPES` deliberately omits `logout` (admin-initiated
|
|
286
|
+
revocations use `session_revoke`, while `logout` is the user-initiated
|
|
287
|
+
case). Compose the two on `on_audit_event`:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
const ws_guard = create_ws_auth_guard(transport, log);
|
|
291
|
+
const ws_logout_closer = create_ws_logout_closer(transport, log);
|
|
292
|
+
const on_audit_event = (event: AuditLogEvent): void => {
|
|
293
|
+
ws_guard(event);
|
|
294
|
+
ws_logout_closer(event);
|
|
295
|
+
};
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Same `outcome === 'failure'` guard as `create_ws_auth_guard`. Closes via
|
|
299
|
+
`close_sockets_for_account(event.account_id)` — `logout` is always
|
|
300
|
+
self-service, so there is no `target_account_id` to fall back on.
|
|
301
|
+
|
|
283
302
|
## WebSocket dispatch
|
|
284
303
|
|
|
285
304
|
Two layered entry points:
|
|
@@ -40,4 +40,35 @@ export declare const WS_DISCONNECT_EVENT_TYPES: ReadonlySet<string>;
|
|
|
40
40
|
* @returns an `on_audit_event` callback suitable for `CreateAppBackendOptions`
|
|
41
41
|
*/
|
|
42
42
|
export declare const create_ws_auth_guard: (transport: BackendWebsocketTransport, log: Logger) => ((event: AuditLogEvent) => void);
|
|
43
|
+
/**
|
|
44
|
+
* Create an audit event handler that closes WebSocket connections on
|
|
45
|
+
* user-initiated logout.
|
|
46
|
+
*
|
|
47
|
+
* Sibling helper to `create_ws_auth_guard` — kept separate because
|
|
48
|
+
* `WS_DISCONNECT_EVENT_TYPES` deliberately omits `logout` (admin-initiated
|
|
49
|
+
* revocations use `session_revoke`, while `logout` is the user-initiated
|
|
50
|
+
* case). Three consumers (tx, undying, zzz) hand-rolled this same branch
|
|
51
|
+
* before extraction.
|
|
52
|
+
*
|
|
53
|
+
* Compose with `create_ws_auth_guard` to handle both kinds of disconnect:
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* const ws_guard = create_ws_auth_guard(transport, log);
|
|
57
|
+
* const ws_logout_closer = create_ws_logout_closer(transport, log);
|
|
58
|
+
* const on_audit_event = (event: AuditLogEvent): void => {
|
|
59
|
+
* ws_guard(event);
|
|
60
|
+
* ws_logout_closer(event);
|
|
61
|
+
* };
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* Ignores `outcome === 'failure'` events — failed logouts carry
|
|
65
|
+
* unauthenticated identifiers (no session to close anyway), and reacting
|
|
66
|
+
* to them would let an unauthenticated probe close the targeted account's
|
|
67
|
+
* sockets by submitting a logout for an arbitrary `account_id`.
|
|
68
|
+
*
|
|
69
|
+
* @param transport - the backend WebSocket transport to guard
|
|
70
|
+
* @param log - logger for disconnect events (info level on non-zero closures)
|
|
71
|
+
* @returns an `on_audit_event` callback wireable alongside `create_ws_auth_guard`
|
|
72
|
+
*/
|
|
73
|
+
export declare const create_ws_logout_closer: (transport: BackendWebsocketTransport, log: Logger) => ((event: AuditLogEvent) => void);
|
|
43
74
|
//# sourceMappingURL=transports_ws_auth_guard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transports_ws_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,4BAA4B,CAAC;AAE1E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAMxD,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,GAChC,WAAW,yBAAyB,EACpC,KAAK,MAAM,KACT,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CA6CjC,CAAC"}
|
|
1
|
+
{"version":3,"file":"transports_ws_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,4BAA4B,CAAC;AAE1E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAMxD,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,GAChC,WAAW,yBAAyB,EACpC,KAAK,MAAM,KACT,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CA6CjC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,uBAAuB,GACnC,WAAW,yBAAyB,EACpC,KAAK,MAAM,KACT,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAajC,CAAC"}
|
|
@@ -84,3 +84,48 @@ export const create_ws_auth_guard = (transport, log) => {
|
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* Create an audit event handler that closes WebSocket connections on
|
|
89
|
+
* user-initiated logout.
|
|
90
|
+
*
|
|
91
|
+
* Sibling helper to `create_ws_auth_guard` — kept separate because
|
|
92
|
+
* `WS_DISCONNECT_EVENT_TYPES` deliberately omits `logout` (admin-initiated
|
|
93
|
+
* revocations use `session_revoke`, while `logout` is the user-initiated
|
|
94
|
+
* case). Three consumers (tx, undying, zzz) hand-rolled this same branch
|
|
95
|
+
* before extraction.
|
|
96
|
+
*
|
|
97
|
+
* Compose with `create_ws_auth_guard` to handle both kinds of disconnect:
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* const ws_guard = create_ws_auth_guard(transport, log);
|
|
101
|
+
* const ws_logout_closer = create_ws_logout_closer(transport, log);
|
|
102
|
+
* const on_audit_event = (event: AuditLogEvent): void => {
|
|
103
|
+
* ws_guard(event);
|
|
104
|
+
* ws_logout_closer(event);
|
|
105
|
+
* };
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* Ignores `outcome === 'failure'` events — failed logouts carry
|
|
109
|
+
* unauthenticated identifiers (no session to close anyway), and reacting
|
|
110
|
+
* to them would let an unauthenticated probe close the targeted account's
|
|
111
|
+
* sockets by submitting a logout for an arbitrary `account_id`.
|
|
112
|
+
*
|
|
113
|
+
* @param transport - the backend WebSocket transport to guard
|
|
114
|
+
* @param log - logger for disconnect events (info level on non-zero closures)
|
|
115
|
+
* @returns an `on_audit_event` callback wireable alongside `create_ws_auth_guard`
|
|
116
|
+
*/
|
|
117
|
+
export const create_ws_logout_closer = (transport, log) => {
|
|
118
|
+
return (event) => {
|
|
119
|
+
if (event.event_type !== 'logout')
|
|
120
|
+
return;
|
|
121
|
+
if (event.outcome === 'failure')
|
|
122
|
+
return;
|
|
123
|
+
const account_id = event.account_id;
|
|
124
|
+
if (!account_id)
|
|
125
|
+
return;
|
|
126
|
+
const closed = transport.close_sockets_for_account(account_id);
|
|
127
|
+
if (closed > 0) {
|
|
128
|
+
log.info(`WS logout closer: closed ${closed} socket(s) for account ${account_id} (logout)`);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
};
|
package/dist/auth/CLAUDE.md
CHANGED
|
@@ -364,9 +364,11 @@ Server-side sessions, keyed by blake3 hash of the session token:
|
|
|
364
364
|
- `query_session_touch` — updates `last_seen_at`; extends `expires_at` only
|
|
365
365
|
when less than `AUTH_SESSION_EXTEND_THRESHOLD_MS` remains (avoids a write
|
|
366
366
|
on every request).
|
|
367
|
-
- **`
|
|
368
|
-
|
|
369
|
-
|
|
367
|
+
- **`query_session_revoke_by_hash_unscoped`** — unscoped DELETE. The
|
|
368
|
+
`_unscoped` suffix is the safety signal — there is no `account_id`
|
|
369
|
+
constraint, so this is only safe from the authenticated session cookie
|
|
370
|
+
path (logout). For user-facing revocation by ID, use
|
|
371
|
+
`query_session_revoke_for_account`.
|
|
370
372
|
- `query_session_revoke_for_account(deps, hash, account_id)` — IDOR guarded.
|
|
371
373
|
- `query_session_revoke_all_for_account` — returns count.
|
|
372
374
|
- `query_session_list_for_account`, `query_session_list_all_active` (admin).
|
|
@@ -420,10 +422,18 @@ run'` if the seed somehow missed (defensive — migrations always seed).
|
|
|
420
422
|
### `audit_log_queries.ts`
|
|
421
423
|
|
|
422
424
|
- `AUDIT_LOG_DEFAULT_LIMIT = 50`.
|
|
423
|
-
- `query_audit_log<T>(deps, input)` —
|
|
424
|
-
`AUDIT_METADATA_SCHEMAS[event_type]`
|
|
425
|
+
- `query_audit_log<T>(deps, input)` — validates metadata against
|
|
426
|
+
`AUDIT_METADATA_SCHEMAS[event_type]` in production + DEV both.
|
|
427
|
+
Mismatches `console.error` and increment
|
|
428
|
+
`audit_metadata_validation_failures` (sample via
|
|
429
|
+
`get_audit_metadata_validation_failures()`), but never throw — fail-open
|
|
430
|
+
by design, matching the rest of the fire-and-forget audit pipeline.
|
|
425
431
|
Returns the inserted row via `RETURNING *` (so callers get `id`, `seq`,
|
|
426
432
|
`created_at`).
|
|
433
|
+
- `get_audit_metadata_validation_failures()` / `reset_audit_metadata_validation_failures()` —
|
|
434
|
+
read / clear the in-process counter. Single-process scope (resets on
|
|
435
|
+
restart); operators thread it into a future `/metrics` surface or a
|
|
436
|
+
debug RPC handler when external observability is needed.
|
|
427
437
|
- `query_audit_log_list(deps, options?)` — supports `event_type`,
|
|
428
438
|
`event_type_in`, `account_id` (matches either `account_id` OR
|
|
429
439
|
`target_account_id`), `outcome`, `since_seq`, `limit`, `offset`.
|
|
@@ -755,6 +765,18 @@ Deps: `AdminActionDeps = Pick<RouteFactoryDeps, 'log' | 'on_audit_event'>`.
|
|
|
755
765
|
|
|
756
766
|
### `permit_offer_action_specs.ts` + `permit_offer_actions.ts` — seven RPC actions
|
|
757
767
|
|
|
768
|
+
> **Hazard — admin `permit_offer_create` does not auto-accept.** The action
|
|
769
|
+
> returns `{offer}` only — no `permit` is inserted. Acceptance is a separate
|
|
770
|
+
> RPC call (`permit_offer_accept`); admin-side tests that need to materialize
|
|
771
|
+
> a permit synchronously call `query_accept_offer` directly (see the
|
|
772
|
+
> `offer_and_accept` helper in `testing/admin_integration.ts`). The CHANGELOG
|
|
773
|
+
> v0.31 entry "admin grant_permit routes emit offers instead of direct
|
|
774
|
+
> grants" was the first signal of this two-step flow; consumers reading the
|
|
775
|
+
> standard admin suite assume auto-accept and have to redesign their tests
|
|
776
|
+
> when they discover otherwise. If you need direct grant for a programmatic
|
|
777
|
+
> path that already proves consent, reach for `query_grant_permit` rather
|
|
778
|
+
> than the RPC action.
|
|
779
|
+
|
|
758
780
|
Six offer-lifecycle methods plus `permit_revoke`. Authorization is a mix:
|
|
759
781
|
|
|
760
782
|
- `permit_offer_create` — `auth: 'authenticated'`. The **`web_grantable`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AAsBxD,OAAO,EAAkB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAElF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -25,7 +25,7 @@ import { z } from 'zod';
|
|
|
25
25
|
import { clear_session_cookie } from './session_middleware.js';
|
|
26
26
|
import { create_session_and_set_cookie } from './session_lifecycle.js';
|
|
27
27
|
import { ActorSummaryJson, PermitSummaryJson, SessionAccountJson, to_session_account, UsernameProvided, } from './account_schema.js';
|
|
28
|
-
import { hash_session_token, query_session_revoke_all_for_account,
|
|
28
|
+
import { hash_session_token, query_session_revoke_all_for_account, query_session_revoke_by_hash_unscoped, } from './session_queries.js';
|
|
29
29
|
import { query_account_by_username_or_email, query_update_account_password, } from './account_queries.js';
|
|
30
30
|
import { query_revoke_all_api_tokens_for_account } from './api_token_queries.js';
|
|
31
31
|
import { audit_log_fire_and_forget } from './audit_log_queries.js';
|
|
@@ -34,7 +34,7 @@ import { get_route_input } from '../http/route_spec.js';
|
|
|
34
34
|
import { get_client_ip } from '../http/proxy.js';
|
|
35
35
|
import { rate_limit_exceeded_response } from '../rate_limiter.js';
|
|
36
36
|
import { Password, PasswordProvided } from './password.js';
|
|
37
|
-
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INVALID_CREDENTIALS } from '../http/error_schemas.js';
|
|
37
|
+
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INVALID_CREDENTIALS, ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY, } from '../http/error_schemas.js';
|
|
38
38
|
/** Input for `GET /api/account/status`. No parameters — caller is the subject. */
|
|
39
39
|
export const AccountStatusInput = z.null();
|
|
40
40
|
/**
|
|
@@ -197,7 +197,12 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
197
197
|
input: LoginInput,
|
|
198
198
|
output: LoginOutput,
|
|
199
199
|
rate_limit: 'both',
|
|
200
|
-
errors: {
|
|
200
|
+
errors: {
|
|
201
|
+
400: z.looseObject({
|
|
202
|
+
error: z.enum([ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY]),
|
|
203
|
+
}),
|
|
204
|
+
401: z.looseObject({ error: z.literal(ERROR_INVALID_CREDENTIALS) }),
|
|
205
|
+
},
|
|
201
206
|
handler: async (c, route) => {
|
|
202
207
|
// Per-IP rate limit check (before any DB/password work)
|
|
203
208
|
const ip = ip_rate_limiter ? get_client_ip(c) : null;
|
|
@@ -291,7 +296,7 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
291
296
|
const session_token = c.get(session_options.context_key) ?? null;
|
|
292
297
|
if (session_token) {
|
|
293
298
|
const token_hash = hash_session_token(session_token);
|
|
294
|
-
await
|
|
299
|
+
await query_session_revoke_by_hash_unscoped(route, token_hash);
|
|
295
300
|
}
|
|
296
301
|
clear_session_cookie(c, session_options);
|
|
297
302
|
void audit_log_fire_and_forget(route, {
|
|
@@ -311,6 +316,11 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
311
316
|
input: PasswordChangeInput,
|
|
312
317
|
output: PasswordChangeOutput,
|
|
313
318
|
rate_limit: login_account_rate_limiter ? 'both' : 'ip',
|
|
319
|
+
errors: {
|
|
320
|
+
400: z.looseObject({
|
|
321
|
+
error: z.enum([ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY]),
|
|
322
|
+
}),
|
|
323
|
+
},
|
|
314
324
|
handler: async (c, route) => {
|
|
315
325
|
// per-IP rate limit check (before argon2 work)
|
|
316
326
|
const ip = ip_rate_limiter ? get_client_ip(c) : null;
|
|
@@ -17,14 +17,21 @@ import type { RouteContext } from '../http/route_spec.js';
|
|
|
17
17
|
import { type AuditEventType, type AuditLogEvent, type AuditLogInput, type AuditLogListOptions, type AuditLogEventWithUsernamesJson, type PermitHistoryEventJson } from './audit_log_schema.js';
|
|
18
18
|
/** Default limit for audit log listings. */
|
|
19
19
|
export declare const AUDIT_LOG_DEFAULT_LIMIT = 50;
|
|
20
|
+
/** Number of audit metadata validation failures observed since process start. */
|
|
21
|
+
export declare const get_audit_metadata_validation_failures: () => number;
|
|
22
|
+
/** Reset the counter — for tests only; production code should not call this. */
|
|
23
|
+
export declare const reset_audit_metadata_validation_failures: () => void;
|
|
20
24
|
/**
|
|
21
25
|
* Insert an audit log entry.
|
|
22
26
|
*
|
|
23
27
|
* Uses `RETURNING *` to return the full inserted row including
|
|
24
28
|
* DB-assigned fields (`id`, `seq`, `created_at`).
|
|
25
29
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
30
|
+
* Validates `metadata` against the per-event-type schema in production +
|
|
31
|
+
* DEV both. Mismatches log to `console.error` and increment
|
|
32
|
+
* `audit_metadata_validation_failures` (sampled via the exported getter)
|
|
33
|
+
* but never throw — the audit row is still written. Schema-vs-runtime
|
|
34
|
+
* drift is an operator signal, not a request-failing condition.
|
|
28
35
|
*
|
|
29
36
|
* @param deps - query dependencies
|
|
30
37
|
* @param input - the audit event to record
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit_log_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"audit_log_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAEN,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,8BAA8B,EACnC,KAAK,sBAAsB,EAC3B,MAAM,uBAAuB,CAAC;AAE/B,4CAA4C;AAC5C,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAmB1C,iFAAiF;AACjF,eAAO,MAAM,sCAAsC,QAAO,MACvB,CAAC;AAEpC,gFAAgF;AAChF,eAAO,MAAM,wCAAwC,QAAO,IAE3D,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,eAAe,GAAU,CAAC,SAAS,cAAc,EAC7D,MAAM,SAAS,EACf,OAAO,aAAa,CAAC,CAAC,CAAC,KACrB,OAAO,CAAC,aAAa,CA2BvB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,UAAU,mBAAmB,KAC3B,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAwC9B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mCAAmC,GAC/C,MAAM,SAAS,EACf,UAAU,mBAAmB,KAC3B,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,CA8C/C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAA+B,KAC7B,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAO9B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,mCAAmC,GAC/C,MAAM,SAAS,EACf,cAA+B,EAC/B,eAAU,KACR,OAAO,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAYvC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,QAAQ,IAAI,KACV,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,GAAI,CAAC,SAAS,cAAc,EACjE,OAAO,IAAI,CAAC,YAAY,EAAE,eAAe,GAAG,iBAAiB,CAAC,EAC9D,OAAO,aAAa,CAAC,CAAC,CAAC,EACvB,KAAK,MAAM,EACX,UAAU,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,KACtC,OAAO,CAAC,IAAI,CAcd,CAAC"}
|
|
@@ -11,30 +11,55 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
|
-
import { DEV } from 'esm-env';
|
|
15
14
|
import { assert_row } from '../db/assert_row.js';
|
|
16
15
|
import { AUDIT_METADATA_SCHEMAS, } from './audit_log_schema.js';
|
|
17
16
|
/** Default limit for audit log listings. */
|
|
18
17
|
export const AUDIT_LOG_DEFAULT_LIMIT = 50;
|
|
18
|
+
/**
|
|
19
|
+
* Process-wide counter for audit metadata validation failures.
|
|
20
|
+
*
|
|
21
|
+
* `query_audit_log` validates each `metadata` payload against the per-event
|
|
22
|
+
* schema in `AUDIT_METADATA_SCHEMAS` and increments this counter on
|
|
23
|
+
* mismatch. The audit row is still written — validation is fail-open by
|
|
24
|
+
* design, matching the rest of the fire-and-forget audit pipeline (a
|
|
25
|
+
* schema-vs-runtime drift should not break auth flows). Operators sample
|
|
26
|
+
* the counter via `get_audit_metadata_validation_failures()` (e.g. from
|
|
27
|
+
* a future `/metrics` surface or a debug RPC handler).
|
|
28
|
+
*
|
|
29
|
+
* Single-process scope: fuz_app's runtime state lives in-process (rate
|
|
30
|
+
* limiter, daemon token state); this counter follows the same pattern
|
|
31
|
+
* and resets on server restart.
|
|
32
|
+
*/
|
|
33
|
+
let audit_metadata_validation_failures = 0;
|
|
34
|
+
/** Number of audit metadata validation failures observed since process start. */
|
|
35
|
+
export const get_audit_metadata_validation_failures = () => audit_metadata_validation_failures;
|
|
36
|
+
/** Reset the counter — for tests only; production code should not call this. */
|
|
37
|
+
export const reset_audit_metadata_validation_failures = () => {
|
|
38
|
+
audit_metadata_validation_failures = 0;
|
|
39
|
+
};
|
|
19
40
|
/**
|
|
20
41
|
* Insert an audit log entry.
|
|
21
42
|
*
|
|
22
43
|
* Uses `RETURNING *` to return the full inserted row including
|
|
23
44
|
* DB-assigned fields (`id`, `seq`, `created_at`).
|
|
24
45
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
46
|
+
* Validates `metadata` against the per-event-type schema in production +
|
|
47
|
+
* DEV both. Mismatches log to `console.error` and increment
|
|
48
|
+
* `audit_metadata_validation_failures` (sampled via the exported getter)
|
|
49
|
+
* but never throw — the audit row is still written. Schema-vs-runtime
|
|
50
|
+
* drift is an operator signal, not a request-failing condition.
|
|
27
51
|
*
|
|
28
52
|
* @param deps - query dependencies
|
|
29
53
|
* @param input - the audit event to record
|
|
30
54
|
* @returns the inserted audit log row
|
|
31
55
|
*/
|
|
32
56
|
export const query_audit_log = async (deps, input) => {
|
|
33
|
-
if (
|
|
57
|
+
if (input.metadata != null) {
|
|
34
58
|
const schema = AUDIT_METADATA_SCHEMAS[input.event_type];
|
|
35
59
|
const result = schema.safeParse(input.metadata);
|
|
36
60
|
if (!result.success) {
|
|
37
|
-
|
|
61
|
+
audit_metadata_validation_failures++;
|
|
62
|
+
console.error(`[audit_log] metadata mismatch for '${input.event_type}':`, result.error.issues);
|
|
38
63
|
}
|
|
39
64
|
}
|
|
40
65
|
const rows = await deps.db.query(`INSERT INTO audit_log (event_type, outcome, actor_id, account_id, target_account_id, ip, metadata)
|
|
@@ -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,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AACtB,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,MAAM,CAAC;AAClC,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AAExD,OAAO,EAAoB,KAAK,uBAAuB,EAAC,MAAM,wBAAwB,CAAC;AAGvF,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AACpC,OAAO,EAAkB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAClF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,oBAAoB,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,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AACtB,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,MAAM,CAAC;AAClC,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AAExD,OAAO,EAAoB,KAAK,uBAAuB,EAAC,MAAM,wBAAwB,CAAC;AAGvF,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AACpC,OAAO,EAAkB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAClF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,oBAAoB,CAAC;AAanD,gFAAgF;AAChF,eAAO,MAAM,cAAc;;;;kBAIzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,iFAAiF;AACjF,eAAO,MAAM,eAAe;;;kBAG1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D;;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;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAC,KAClC,OAAO,CAAC,eAAe,CAwBzB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,gBAAgB,EACtB,SAAS,qBAAqB,KAC5B,KAAK,CAAC,SAAS,CAyHjB,CAAC"}
|
|
@@ -14,7 +14,7 @@ import { Password } from './password.js';
|
|
|
14
14
|
import { get_route_input } from '../http/route_spec.js';
|
|
15
15
|
import { get_client_ip } from '../http/proxy.js';
|
|
16
16
|
import { rate_limit_exceeded_response } from '../rate_limiter.js';
|
|
17
|
-
import { ERROR_BOOTSTRAP_NOT_CONFIGURED, ERROR_INVALID_TOKEN, ERROR_ALREADY_BOOTSTRAPPED, ERROR_TOKEN_FILE_MISSING, } from '../http/error_schemas.js';
|
|
17
|
+
import { ERROR_BOOTSTRAP_NOT_CONFIGURED, ERROR_INVALID_TOKEN, ERROR_ALREADY_BOOTSTRAPPED, ERROR_TOKEN_FILE_MISSING, ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY, } from '../http/error_schemas.js';
|
|
18
18
|
import { audit_log_fire_and_forget } from './audit_log_queries.js';
|
|
19
19
|
// -- Input/output schemas ---------------------------------------------------
|
|
20
20
|
/** Input for `POST /bootstrap`. `token` is the one-shot token file contents. */
|
|
@@ -81,6 +81,9 @@ export const create_bootstrap_route_specs = (deps, options) => {
|
|
|
81
81
|
output: BootstrapOutput,
|
|
82
82
|
rate_limit: 'ip',
|
|
83
83
|
errors: {
|
|
84
|
+
400: z.looseObject({
|
|
85
|
+
error: z.enum([ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY]),
|
|
86
|
+
}),
|
|
84
87
|
401: z.looseObject({ error: z.literal(ERROR_INVALID_TOKEN) }),
|
|
85
88
|
403: z.looseObject({ error: z.literal(ERROR_ALREADY_BOOTSTRAPPED) }),
|
|
86
89
|
404: z.looseObject({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permit_offer_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/permit_offer_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAa,KAAK,aAAa,EAAE,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAGxF,OAAO,EAAmC,KAAK,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAsBzF,OAAO,EAAW,KAAK,cAAc,EAAC,MAAM,sBAAsB,CAAC;AACnE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAChD,OAAO,EAON,KAAK,kBAAkB,EACvB,MAAM,iCAAiC,CAAC;AAmCzC;;;;;;;;GAQG;AACH,MAAM,MAAM,0BAA0B,GAAG,CACxC,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAC,EACrE,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,EACnC,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,iDAAiD;AACjD,MAAM,WAAW,wBAAwB;IACxC;;;OAGG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,sFAAsF;IACtF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,0BAA0B,CAAC;CACvC;AAqCD;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,gBAAgB,CAAC;IAC9F,+EAA+E;IAC/E,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAChD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,qBAAqB,EAC3B,UAAS,wBAA6B,KACpC,KAAK,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"permit_offer_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/permit_offer_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAa,KAAK,aAAa,EAAE,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAGxF,OAAO,EAAmC,KAAK,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAsBzF,OAAO,EAAW,KAAK,cAAc,EAAC,MAAM,sBAAsB,CAAC;AACnE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAChD,OAAO,EAON,KAAK,kBAAkB,EACvB,MAAM,iCAAiC,CAAC;AAmCzC;;;;;;;;GAQG;AACH,MAAM,MAAM,0BAA0B,GAAG,CACxC,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAC,EACrE,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,EACnC,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,iDAAiD;AACjD,MAAM,WAAW,wBAAwB;IACxC;;;OAGG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,sFAAsF;IACtF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,0BAA0B,CAAC;CACvC;AAqCD;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,gBAAgB,CAAC;IAC9F,+EAA+E;IAC/E,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAChD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,qBAAqB,EAC3B,UAAS,wBAA6B,KACpC,KAAK,CAAC,SAAS,CA8djB,CAAC"}
|
|
@@ -106,6 +106,9 @@ export const create_permit_offer_actions = (deps, options = {}) => {
|
|
|
106
106
|
},
|
|
107
107
|
}, log, on_audit_event);
|
|
108
108
|
};
|
|
109
|
+
// Returns {offer} only — no auto-accept. Recipient must call
|
|
110
|
+
// permit_offer_accept; admin tests materialize permits via
|
|
111
|
+
// query_accept_offer (see testing/admin_integration.ts `offer_and_accept`).
|
|
109
112
|
const create_handler = async (input, ctx) => {
|
|
110
113
|
const auth = require_request_auth(ctx.auth);
|
|
111
114
|
// Role must be web_grantable — same gate as admin direct-grant.
|
|
@@ -52,14 +52,16 @@ export declare const query_session_get_valid: (deps: QueryDeps, token_hash: stri
|
|
|
52
52
|
*/
|
|
53
53
|
export declare const query_session_touch: (deps: QueryDeps, token_hash: string) => Promise<void>;
|
|
54
54
|
/**
|
|
55
|
-
* Revoke (delete) a session by its token hash.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
55
|
+
* Revoke (delete) a session by its token hash, with no account scoping.
|
|
56
|
+
*
|
|
57
|
+
* The `_unscoped` suffix is the safety signal — there is no `account_id`
|
|
58
|
+
* constraint, so callers must guarantee the hash came from a trusted
|
|
59
|
+
* source (the authenticated session cookie path is the only safe production
|
|
60
|
+
* caller — see `account_routes.ts` `/logout`). For user-facing revocation
|
|
61
|
+
* of a specific session by ID, use `query_session_revoke_for_account`
|
|
62
|
+
* (IDOR-guarded).
|
|
61
63
|
*/
|
|
62
|
-
export declare const
|
|
64
|
+
export declare const query_session_revoke_by_hash_unscoped: (deps: QueryDeps, token_hash: string) => Promise<void>;
|
|
63
65
|
/**
|
|
64
66
|
* Revoke a session only if it belongs to the specified account.
|
|
65
67
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAGpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,qBAAqB,CAAC;AAErD,kDAAkD;AAClD,eAAO,MAAM,wBAAwB,QAA2B,CAAC;AAEjE,yEAAyE;AACzE,eAAO,MAAM,gCAAgC,QAAsB,CAAC;AAEpE;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,MAElD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAO,MAEzC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,EAClB,YAAY,IAAI,KACd,OAAO,CAAC,IAAI,CAMd,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,WAAW,GAAG,SAAS,CAKjC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,GAAU,MAAM,SAAS,EAAE,YAAY,MAAM,KAAG,OAAO,CAAC,IAAI,CAY3F,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAGpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,qBAAqB,CAAC;AAErD,kDAAkD;AAClD,eAAO,MAAM,wBAAwB,QAA2B,CAAC;AAEjE,yEAAyE;AACzE,eAAO,MAAM,gCAAgC,QAAsB,CAAC;AAEpE;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,MAElD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAO,MAEzC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,EAClB,YAAY,IAAI,KACd,OAAO,CAAC,IAAI,CAMd,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,WAAW,GAAG,SAAS,CAKjC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,GAAU,MAAM,SAAS,EAAE,YAAY,MAAM,KAAG,OAAO,CAAC,IAAI,CAY3F,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAU,KACR,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAK5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAc,MAAM,KAClB,OAAO,CAAC,MAAM,CAYhB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,cAAW,KACT,OAAO,CAAC,KAAK,CAAC,WAAW,GAAG;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,CAAC,CASjD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,CAKnF,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,iBAAiB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,EACjD,KAAK,MAAM,KACT,OAAO,CAAC,IAAI,CAMd,CAAC"}
|
|
@@ -72,14 +72,16 @@ export const query_session_touch = async (deps, token_hash) => {
|
|
|
72
72
|
WHERE id = $1`, [token_hash, new_expires.toISOString()]);
|
|
73
73
|
};
|
|
74
74
|
/**
|
|
75
|
-
* Revoke (delete) a session by its token hash.
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
75
|
+
* Revoke (delete) a session by its token hash, with no account scoping.
|
|
76
|
+
*
|
|
77
|
+
* The `_unscoped` suffix is the safety signal — there is no `account_id`
|
|
78
|
+
* constraint, so callers must guarantee the hash came from a trusted
|
|
79
|
+
* source (the authenticated session cookie path is the only safe production
|
|
80
|
+
* caller — see `account_routes.ts` `/logout`). For user-facing revocation
|
|
81
|
+
* of a specific session by ID, use `query_session_revoke_for_account`
|
|
82
|
+
* (IDOR-guarded).
|
|
81
83
|
*/
|
|
82
|
-
export const
|
|
84
|
+
export const query_session_revoke_by_hash_unscoped = async (deps, token_hash) => {
|
|
83
85
|
await deps.db.query(`DELETE FROM auth_session WHERE id = $1`, [token_hash]);
|
|
84
86
|
};
|
|
85
87
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signup_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/signup_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAStB,OAAO,EAAkB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAClF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"signup_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/signup_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAStB,OAAO,EAAkB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAClF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAQhD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAE1D,OAAO,KAAK,EAAC,uBAAuB,EAAC,MAAM,qBAAqB,CAAC;AAEjE;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,uBAAuB;IAClE,6FAA6F;IAC7F,2BAA2B,EAAE,WAAW,GAAG,IAAI,CAAC;IAChD,yFAAyF;IACzF,YAAY,EAAE,WAAW,CAAC;CAC1B;AAID,0FAA0F;AAC1F,eAAO,MAAM,WAAW;;;;kBAItB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,8EAA8E;AAC9E,eAAO,MAAM,YAAY;;kBAEvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,gBAAgB,EACtB,SAAS,kBAAkB,KACzB,KAAK,CAAC,SAAS,CA4HjB,CAAC"}
|
|
@@ -16,7 +16,7 @@ import { Password } from './password.js';
|
|
|
16
16
|
import { get_route_input } from '../http/route_spec.js';
|
|
17
17
|
import { get_client_ip } from '../http/proxy.js';
|
|
18
18
|
import { rate_limit_exceeded_response } from '../rate_limiter.js';
|
|
19
|
-
import { ERROR_NO_MATCHING_INVITE, ERROR_SIGNUP_CONFLICT } from '../http/error_schemas.js';
|
|
19
|
+
import { ERROR_NO_MATCHING_INVITE, ERROR_SIGNUP_CONFLICT, ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY, } from '../http/error_schemas.js';
|
|
20
20
|
import { audit_log_fire_and_forget } from './audit_log_queries.js';
|
|
21
21
|
import { is_pg_unique_violation } from '../db/pg_error.js';
|
|
22
22
|
// -- Input/output schemas ---------------------------------------------------
|
|
@@ -51,6 +51,9 @@ export const create_signup_route_specs = (deps, options) => {
|
|
|
51
51
|
output: SignupOutput,
|
|
52
52
|
rate_limit: signup_account_rate_limiter ? 'both' : 'ip',
|
|
53
53
|
errors: {
|
|
54
|
+
400: z.looseObject({
|
|
55
|
+
error: z.enum([ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY]),
|
|
56
|
+
}),
|
|
54
57
|
403: z.looseObject({ error: z.literal(ERROR_NO_MATCHING_INVITE) }),
|
|
55
58
|
409: z.looseObject({ error: z.literal(ERROR_SIGNUP_CONFLICT) }),
|
|
56
59
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/db_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAmB,KAAK,SAAS,EAAC,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"db_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/db_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAmB,KAAK,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAYjE;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,WAAW,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3D,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,SAAS,cAAc,KAAG,KAAK,CAAC,SAAS,CA8N9E,CAAC"}
|
package/dist/http/db_routes.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
import { get_route_params } from './route_spec.js';
|
|
11
|
-
import {
|
|
11
|
+
import { ForeignKeyError, ERROR_TABLE_NOT_FOUND, ERROR_TABLE_NO_PRIMARY_KEY, ERROR_ROW_NOT_FOUND, ERROR_FOREIGN_KEY_VIOLATION, ERROR_INVALID_ROUTE_PARAMS, ERROR_DATABASE_CONNECTION_FAILED, } from './error_schemas.js';
|
|
12
12
|
import { assert_valid_sql_identifier, VALID_SQL_IDENTIFIER } from '../db/sql_identifier.js';
|
|
13
13
|
/**
|
|
14
14
|
* Create the db API route specs.
|
|
@@ -26,7 +26,9 @@ export const create_db_route_specs = (options) => {
|
|
|
26
26
|
description: 'Database health and stats',
|
|
27
27
|
input: z.null(),
|
|
28
28
|
output: z.looseObject({ connected: z.boolean() }),
|
|
29
|
-
errors: {
|
|
29
|
+
errors: {
|
|
30
|
+
503: z.looseObject({ error: z.literal(ERROR_DATABASE_CONNECTION_FAILED) }),
|
|
31
|
+
},
|
|
30
32
|
handler: async (c, route) => {
|
|
31
33
|
try {
|
|
32
34
|
await route.db.query('SELECT 1');
|
|
@@ -46,7 +48,7 @@ export const create_db_route_specs = (options) => {
|
|
|
46
48
|
return c.json({
|
|
47
49
|
connected: false,
|
|
48
50
|
type: db_type,
|
|
49
|
-
error:
|
|
51
|
+
error: ERROR_DATABASE_CONNECTION_FAILED,
|
|
50
52
|
}, 503);
|
|
51
53
|
}
|
|
52
54
|
},
|
|
@@ -82,7 +84,10 @@ export const create_db_route_specs = (options) => {
|
|
|
82
84
|
description: 'Get table columns and rows (paginated)',
|
|
83
85
|
params: z.strictObject({ name: z.string().regex(VALID_SQL_IDENTIFIER) }),
|
|
84
86
|
input: z.null(),
|
|
85
|
-
errors: {
|
|
87
|
+
errors: {
|
|
88
|
+
400: z.looseObject({ error: z.literal(ERROR_INVALID_ROUTE_PARAMS) }),
|
|
89
|
+
404: z.looseObject({ error: z.literal(ERROR_TABLE_NOT_FOUND) }),
|
|
90
|
+
},
|
|
86
91
|
output: z.looseObject({
|
|
87
92
|
columns: z.array(z.strictObject({ column_name: z.string(), data_type: z.string(), is_nullable: z.string() })),
|
|
88
93
|
rows: z.array(z.record(z.string(), z.unknown())),
|
|
@@ -132,6 +137,9 @@ export const create_db_route_specs = (options) => {
|
|
|
132
137
|
input: z.null(),
|
|
133
138
|
output: z.looseObject({ success: z.boolean() }),
|
|
134
139
|
errors: {
|
|
140
|
+
400: z.looseObject({
|
|
141
|
+
error: z.enum([ERROR_INVALID_ROUTE_PARAMS, ERROR_TABLE_NO_PRIMARY_KEY]),
|
|
142
|
+
}),
|
|
135
143
|
404: z.looseObject({
|
|
136
144
|
error: z.enum([ERROR_TABLE_NOT_FOUND, ERROR_ROW_NOT_FOUND]),
|
|
137
145
|
}),
|
|
@@ -83,6 +83,8 @@ export declare const ERROR_TABLE_NOT_FOUND: "table_not_found";
|
|
|
83
83
|
export declare const ERROR_TABLE_NO_PRIMARY_KEY: "table_no_primary_key";
|
|
84
84
|
/** Row with the given PK value not found. */
|
|
85
85
|
export declare const ERROR_ROW_NOT_FOUND: "row_not_found";
|
|
86
|
+
/** Database health-check query failed (connectivity or query error). */
|
|
87
|
+
export declare const ERROR_DATABASE_CONNECTION_FAILED: "database_connection_failed";
|
|
86
88
|
/** Base API error — all JSON error responses have at least `{error: string}`. */
|
|
87
89
|
export declare const ApiError: z.ZodObject<{
|
|
88
90
|
error: z.ZodString;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error_schemas.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/error_schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAI/C,0CAA0C;AAC1C,eAAO,MAAM,0BAA0B,EAAG,sBAA+B,CAAC;AAE1E,uDAAuD;AACvD,eAAO,MAAM,uBAAuB,EAAG,mBAA4B,CAAC;AAEpE,6CAA6C;AAC7C,eAAO,MAAM,0BAA0B,EAAG,sBAA+B,CAAC;AAE1E,8CAA8C;AAC9C,eAAO,MAAM,0BAA0B,EAAG,sBAA+B,CAAC;AAI1E,wCAAwC;AACxC,eAAO,MAAM,6BAA6B,EAAG,yBAAkC,CAAC;AAEhF,+CAA+C;AAC/C,eAAO,MAAM,8BAA8B,EAAG,0BAAmC,CAAC;AAElF,yCAAyC;AACzC,eAAO,MAAM,yBAAyB,EAAG,qBAA8B,CAAC;AAExE,sFAAsF;AACtF,eAAO,MAAM,yBAAyB,EAAG,qBAA8B,CAAC;AAExE,qDAAqD;AACrD,eAAO,MAAM,uBAAuB,EAAG,mBAA4B,CAAC;AAIpE,uCAAuC;AACvC,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,wCAAwC;AACxC,eAAO,MAAM,uBAAuB,EAAG,mBAA4B,CAAC;AAEpE,sEAAsE;AACtE,eAAO,MAAM,6BAA6B,EAAG,0CAAmD,CAAC;AAEjG,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,EAAG,eAAwB,CAAC;AAE5D,0CAA0C;AAC1C,eAAO,MAAM,uBAAuB,EAAG,mBAA4B,CAAC;AAIpE,0DAA0D;AAC1D,eAAO,MAAM,kCAAkC,EAAG,8BAAuC,CAAC;AAE1F,wFAAwF;AACxF,eAAO,MAAM,0BAA0B,EAAG,sBAA+B,CAAC;AAE1E,8EAA8E;AAC9E,eAAO,MAAM,mCAAmC,EAAG,+BAAwC,CAAC;AAE5F,uDAAuD;AACvD,eAAO,MAAM,8BAA8B,EAAG,0BAAmC,CAAC;AAIlF,qEAAqE;AACrE,eAAO,MAAM,0BAA0B,EAAG,sBAA+B,CAAC;AAE1E,8CAA8C;AAC9C,eAAO,MAAM,wBAAwB,EAAG,oBAA6B,CAAC;AAEtE,8DAA8D;AAC9D,eAAO,MAAM,8BAA8B,EAAG,0BAAmC,CAAC;AAIlF,0DAA0D;AAC1D,eAAO,MAAM,wBAAwB,EAAG,oBAA6B,CAAC;AAEtE,0GAA0G;AAC1G,eAAO,MAAM,qBAAqB,EAAG,iBAA0B,CAAC;AAEhE,gDAAgD;AAChD,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,sDAAsD;AACtD,eAAO,MAAM,+BAA+B,EAAG,2BAAoC,CAAC;AAEpF,qEAAqE;AACrE,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,6DAA6D;AAC7D,eAAO,MAAM,oCAAoC,EAAG,gCAAyC,CAAC;AAE9F,0DAA0D;AAC1D,eAAO,MAAM,iCAAiC,EAAG,6BAAsC,CAAC;AAIxF,6DAA6D;AAC7D,eAAO,MAAM,4BAA4B,EAAG,wBAAiC,CAAC;AAE9E,4DAA4D;AAC5D,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,oEAAoE;AACpE,eAAO,MAAM,wBAAwB,EAAG,oBAA6B,CAAC;AAItE,kDAAkD;AAClD,eAAO,MAAM,2BAA2B,EAAG,uBAAgC,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,qBAAqB,EAAG,iBAA0B,CAAC;AAEhE,iEAAiE;AACjE,eAAO,MAAM,0BAA0B,EAAG,sBAA+B,CAAC;AAE1E,6CAA6C;AAC7C,eAAO,MAAM,mBAAmB,EAAG,eAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"error_schemas.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/error_schemas.ts"],"names":[],"mappings":"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"}
|
|
@@ -90,6 +90,8 @@ export const ERROR_TABLE_NOT_FOUND = 'table_not_found';
|
|
|
90
90
|
export const ERROR_TABLE_NO_PRIMARY_KEY = 'table_no_primary_key';
|
|
91
91
|
/** Row with the given PK value not found. */
|
|
92
92
|
export const ERROR_ROW_NOT_FOUND = 'row_not_found';
|
|
93
|
+
/** Database health-check query failed (connectivity or query error). */
|
|
94
|
+
export const ERROR_DATABASE_CONNECTION_FAILED = 'database_connection_failed';
|
|
93
95
|
// --- Standard error shapes ---
|
|
94
96
|
// Using z.looseObject — error responses may carry extra context fields.
|
|
95
97
|
/** Base API error — all JSON error responses have at least `{error: string}`. */
|
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -217,10 +217,23 @@ Tightness audit:
|
|
|
217
217
|
classifies every route × status combination as `'literal' | 'enum' | 'generic'`.
|
|
218
218
|
- `assert_error_schema_tightness(surface, options?)` — fails routes below a
|
|
219
219
|
threshold (`min_specificity`, default `'enum'`) with `allowlist` + `ignore_statuses` escape hatches.
|
|
220
|
-
- `
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
220
|
+
- `FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST` — currently empty. Every
|
|
221
|
+
fuz_app-shipped route (account login/password/bootstrap/signup, db
|
|
222
|
+
health/tables/:name/tables/:name/rows/:id) has been tightened in place to
|
|
223
|
+
`z.enum([...])` / `z.literal(...)` against every emit-site code. Kept as a
|
|
224
|
+
forward-compatibility hook for future stock routes that need an interim
|
|
225
|
+
exemption; paths assume the standard `/api/account` + `/api/db` prefixes.
|
|
226
|
+
- `DEFAULT_ERROR_SCHEMA_TIGHTNESS` — `{ignore_statuses: [401, 403, 429], allowlist: FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST}`.
|
|
227
|
+
Applied by `describe_standard_attack_surface_tests` when
|
|
228
|
+
`error_schema_tightness` is omitted; pass an override config or `null` to
|
|
229
|
+
opt out.
|
|
230
|
+
- **Merge semantics in `describe_standard_attack_surface_tests`**:
|
|
231
|
+
consumer-supplied `allowlist` and `ignore_statuses` are concatenated
|
|
232
|
+
underneath the defaults (stock entries first, consumer entries last),
|
|
233
|
+
so consumer allowlists are additive rather than replacing. Scalar fields
|
|
234
|
+
like `min_specificity` are overwritten by the consumer. Exported as
|
|
235
|
+
`resolve_standard_error_schema_tightness(consumer_options)` for consumers
|
|
236
|
+
calling `assert_error_schema_tightness` directly outside the suite.
|
|
224
237
|
|
|
225
238
|
Aggregate runners (called by the standard attack-surface suite):
|
|
226
239
|
|
|
@@ -23,6 +23,7 @@ import type { AppBackend } from '../server/app_backend.js';
|
|
|
23
23
|
import { type AppServerOptions, type AppServerContext } from '../server/app_server.js';
|
|
24
24
|
import type { AppSurface, AppSurfaceSpec } from '../http/surface.js';
|
|
25
25
|
import type { RouteSpec } from '../http/route_spec.js';
|
|
26
|
+
import type { RpcEndpointsSuiteOption } from './rpc_helpers.js';
|
|
26
27
|
/**
|
|
27
28
|
* Fast password stub for tests that don't exercise login/password flows.
|
|
28
29
|
*
|
|
@@ -118,6 +119,16 @@ export declare const create_test_app_server: (options: TestAppServerOptions) =>
|
|
|
118
119
|
export interface CreateTestAppOptions extends TestAppServerOptions {
|
|
119
120
|
/** Route spec factory — called with the assembled `AppServerContext`. */
|
|
120
121
|
create_route_specs: (context: AppServerContext) => Array<RouteSpec>;
|
|
122
|
+
/**
|
|
123
|
+
* RPC endpoints mounted by `create_app_server` — eager array or
|
|
124
|
+
* `(ctx: AppServerContext) => Array<RpcEndpointSpec>` factory. Symmetric
|
|
125
|
+
* with the suite-level `rpc_endpoints` option on
|
|
126
|
+
* `describe_standard_admin_integration_tests` etc., so callers wiring a
|
|
127
|
+
* full RPC stack don't have to switch shapes between low-level and
|
|
128
|
+
* suite-level helpers. Equivalent to `app_options.rpc_endpoints`; when
|
|
129
|
+
* both are set `app_options` wins and `console.warn` fires.
|
|
130
|
+
*/
|
|
131
|
+
rpc_endpoints?: RpcEndpointsSuiteOption;
|
|
121
132
|
/** Optional overrides for `AppServerOptions` (backend, session_options, and create_route_specs are managed). */
|
|
122
133
|
app_options?: Partial<Omit<AppServerOptions, 'backend' | 'session_options' | 'create_route_specs'>>;
|
|
123
134
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/app_server.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,MAAM,CAAC;AAK/B,OAAO,EAA2B,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAE1E,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,qBAAqB,CAAC;AAU1D,OAAO,EAA8B,KAAK,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG3F,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,YAAY,CAAC;AACrC,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAEN,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAC,UAAU,EAAE,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/app_server.ts"],"names":[],"mappings":"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"}
|
|
@@ -183,6 +183,9 @@ export const create_test_app = async (options) => {
|
|
|
183
183
|
rotated_at: new Date(),
|
|
184
184
|
keeper_account_id: test_server.account.id,
|
|
185
185
|
};
|
|
186
|
+
if (options.rpc_endpoints !== undefined && options.app_options?.rpc_endpoints !== undefined) {
|
|
187
|
+
console.warn('create_test_app: both top-level `rpc_endpoints` and `app_options.rpc_endpoints` are set; preferring `app_options.rpc_endpoints` (back-compat).');
|
|
188
|
+
}
|
|
186
189
|
const result = await create_app_server({
|
|
187
190
|
backend: test_server,
|
|
188
191
|
session_options: options.session_options,
|
|
@@ -195,6 +198,7 @@ export const create_test_app = async (options) => {
|
|
|
195
198
|
bearer_ip_rate_limiter: null,
|
|
196
199
|
await_pending_effects: true,
|
|
197
200
|
daemon_token_state,
|
|
201
|
+
rpc_endpoints: options.rpc_endpoints,
|
|
198
202
|
...options.app_options,
|
|
199
203
|
create_route_specs: options.create_route_specs,
|
|
200
204
|
});
|
|
@@ -20,6 +20,22 @@ export interface AdversarialTestOptions {
|
|
|
20
20
|
* @param options - the test configuration
|
|
21
21
|
*/
|
|
22
22
|
export declare const describe_adversarial_auth: (options: AdversarialTestOptions) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Merge a consumer's `error_schema_tightness` option with
|
|
25
|
+
* `DEFAULT_ERROR_SCHEMA_TIGHTNESS` so `allowlist` and `ignore_statuses` are
|
|
26
|
+
* additive rather than replacing.
|
|
27
|
+
*
|
|
28
|
+
* - `undefined` → return the default as-is.
|
|
29
|
+
* - `null` → return `null` (opt out of the assertion).
|
|
30
|
+
* - object → spread the default, then consumer overrides for scalar fields
|
|
31
|
+
* (`min_specificity`), then concat stock-then-consumer for the list fields
|
|
32
|
+
* (`allowlist`, `ignore_statuses`) so consumer entries extend rather than
|
|
33
|
+
* replace.
|
|
34
|
+
*
|
|
35
|
+
* Exported for direct use when a consumer calls `assert_error_schema_tightness`
|
|
36
|
+
* outside the standard suite but still wants the additive merge.
|
|
37
|
+
*/
|
|
38
|
+
export declare const resolve_standard_error_schema_tightness: (consumer: ErrorSchemaTightnessOptions | null | undefined) => ErrorSchemaTightnessOptions | null;
|
|
23
39
|
/** Options for the standard attack surface test suite. */
|
|
24
40
|
export interface StandardAttackSurfaceOptions {
|
|
25
41
|
/** Build the app surface bundle (surface + route specs + middleware specs). */
|
|
@@ -39,9 +55,14 @@ export interface StandardAttackSurfaceOptions {
|
|
|
39
55
|
/**
|
|
40
56
|
* Error schema tightness assertion config. Defaults to
|
|
41
57
|
* `DEFAULT_ERROR_SCHEMA_TIGHTNESS` (ignores 401/403/429,
|
|
42
|
-
* `min_specificity: 'enum'
|
|
43
|
-
*
|
|
44
|
-
*
|
|
58
|
+
* `min_specificity: 'enum'`, allowlist seeded with
|
|
59
|
+
* `FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST`).
|
|
60
|
+
*
|
|
61
|
+
* Consumer-supplied `allowlist` and `ignore_statuses` are **additive** —
|
|
62
|
+
* the suite merges them underneath the stock defaults, so project-specific
|
|
63
|
+
* entries don't need to re-list fuz_app's own stock routes. Pass a narrower
|
|
64
|
+
* config to extend either list or tighten `min_specificity`; pass `null`
|
|
65
|
+
* to skip the assertion and keep the audit log informational-only.
|
|
45
66
|
*/
|
|
46
67
|
error_schema_tightness?: ErrorSchemaTightnessOptions | null;
|
|
47
68
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attack_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/attack_surface.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAoB7B,OAAO,
|
|
1
|
+
{"version":3,"file":"attack_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/attack_surface.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAoB7B,OAAO,EAON,KAAK,4BAA4B,EACjC,KAAK,2BAA2B,EAChC,MAAM,yBAAyB,CAAC;AAoBjC,OAAO,EAA4B,KAAK,cAAc,EAAC,MAAM,oBAAoB,CAAC;AAsClF,oFAAoF;AACpF,MAAM,WAAW,sBAAsB;IACtC,+EAA+E;IAC/E,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACrB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,sBAAsB,KAAG,IAkH3E,CAAC;AAIF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,uCAAuC,GACnD,UAAU,2BAA2B,GAAG,IAAI,GAAG,SAAS,KACtD,2BAA2B,GAAG,IAWhC,CAAC;AAEF,0DAA0D;AAC1D,MAAM,WAAW,4BAA4B;IAC5C,+EAA+E;IAC/E,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,sBAAsB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACtC,gHAAgH;IAChH,uBAAuB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACvC,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iEAAiE;IACjE,eAAe,CAAC,EAAE,4BAA4B,CAAC;IAC/C;;;;;;;;;;;OAWG;IACH,sBAAsB,CAAC,EAAE,2BAA2B,GAAG,IAAI,CAAC;CAC5D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,sCAAsC,GAClD,SAAS,4BAA4B,KACnC,IAuEF,CAAC"}
|
|
@@ -15,7 +15,7 @@ import './assert_dev_env.js';
|
|
|
15
15
|
* @module
|
|
16
16
|
*/
|
|
17
17
|
import { test, assert, describe } from 'vitest';
|
|
18
|
-
import { assert_surface_invariants, assert_surface_security_policy, audit_error_schema_tightness, assert_error_schema_tightness, DEFAULT_ERROR_SCHEMA_TIGHTNESS, } from './surface_invariants.js';
|
|
18
|
+
import { assert_surface_invariants, assert_surface_security_policy, audit_error_schema_tightness, assert_error_schema_tightness, DEFAULT_ERROR_SCHEMA_TIGHTNESS, FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST, } from './surface_invariants.js';
|
|
19
19
|
import { describe_adversarial_input } from './adversarial_input.js';
|
|
20
20
|
import { describe_adversarial_404 } from './adversarial_404.js';
|
|
21
21
|
import { create_test_app_from_specs, create_test_request_context, create_auth_test_apps, select_auth_app, resolve_test_path, } from './auth_apps.js';
|
|
@@ -157,6 +157,35 @@ export const describe_adversarial_auth = (options) => {
|
|
|
157
157
|
});
|
|
158
158
|
});
|
|
159
159
|
};
|
|
160
|
+
// --- Standard attack surface test suite ---
|
|
161
|
+
/**
|
|
162
|
+
* Merge a consumer's `error_schema_tightness` option with
|
|
163
|
+
* `DEFAULT_ERROR_SCHEMA_TIGHTNESS` so `allowlist` and `ignore_statuses` are
|
|
164
|
+
* additive rather than replacing.
|
|
165
|
+
*
|
|
166
|
+
* - `undefined` → return the default as-is.
|
|
167
|
+
* - `null` → return `null` (opt out of the assertion).
|
|
168
|
+
* - object → spread the default, then consumer overrides for scalar fields
|
|
169
|
+
* (`min_specificity`), then concat stock-then-consumer for the list fields
|
|
170
|
+
* (`allowlist`, `ignore_statuses`) so consumer entries extend rather than
|
|
171
|
+
* replace.
|
|
172
|
+
*
|
|
173
|
+
* Exported for direct use when a consumer calls `assert_error_schema_tightness`
|
|
174
|
+
* outside the standard suite but still wants the additive merge.
|
|
175
|
+
*/
|
|
176
|
+
export const resolve_standard_error_schema_tightness = (consumer) => {
|
|
177
|
+
if (consumer === null)
|
|
178
|
+
return null;
|
|
179
|
+
return {
|
|
180
|
+
...DEFAULT_ERROR_SCHEMA_TIGHTNESS,
|
|
181
|
+
...consumer,
|
|
182
|
+
allowlist: [...FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST, ...(consumer?.allowlist ?? [])],
|
|
183
|
+
ignore_statuses: [
|
|
184
|
+
...(DEFAULT_ERROR_SCHEMA_TIGHTNESS.ignore_statuses ?? []),
|
|
185
|
+
...(consumer?.ignore_statuses ?? []),
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
};
|
|
160
189
|
/**
|
|
161
190
|
* Run the standard attack surface test suite.
|
|
162
191
|
*
|
|
@@ -178,7 +207,8 @@ export const describe_adversarial_auth = (options) => {
|
|
|
178
207
|
* @param options - the test configuration
|
|
179
208
|
*/
|
|
180
209
|
export const describe_standard_attack_surface_tests = (options) => {
|
|
181
|
-
const { build, snapshot_path, expected_public_routes, expected_api_middleware, roles, api_path_prefix = '/api/', security_policy,
|
|
210
|
+
const { build, snapshot_path, expected_public_routes, expected_api_middleware, roles, api_path_prefix = '/api/', security_policy, } = options;
|
|
211
|
+
const error_schema_tightness = resolve_standard_error_schema_tightness(options.error_schema_tightness);
|
|
182
212
|
const built = build();
|
|
183
213
|
const { surface } = built;
|
|
184
214
|
describe('attack surface snapshot', () => {
|
|
@@ -20,17 +20,20 @@ export type RpcEndpointsSuiteOption = Array<RpcEndpointSpec> | ((ctx: AppServerC
|
|
|
20
20
|
* Resolve a suite's `rpc_endpoints` option to an array for setup-time
|
|
21
21
|
* inspection (path lookup, action presence checks).
|
|
22
22
|
*
|
|
23
|
-
* For the factory form this invokes the factory
|
|
24
|
-
* `AppServerContext`
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* For the factory form this invokes the factory twice with stub
|
|
24
|
+
* `AppServerContext`s and asserts that both invocations produce the same
|
|
25
|
+
* (path, method-list) shape — catching factories that close over mutable
|
|
26
|
+
* state or otherwise diverge across calls. The first array is returned;
|
|
27
|
+
* the second is discarded after the comparison. `create_app_server`
|
|
28
|
+
* invokes the factory again per-test with its real ctx, and those are
|
|
29
|
+
* the handlers that actually serve requests.
|
|
28
30
|
*
|
|
29
31
|
* Safe as long as the factory is pure with respect to the endpoint `path`
|
|
30
32
|
* and the action `spec.method` list — the canonical helpers
|
|
31
33
|
* (`create_standard_rpc_actions`, `create_admin_actions`, `create_account_actions`,
|
|
32
34
|
* etc.) are. Factories that return a different `path` based on `ctx` will
|
|
33
|
-
* produce a setup/runtime mismatch;
|
|
35
|
+
* produce a setup/runtime mismatch; the path-purity assert below surfaces
|
|
36
|
+
* that as a clear `gro check` error rather than a silent test/runtime drift.
|
|
34
37
|
*/
|
|
35
38
|
export declare const resolve_rpc_endpoints_for_setup: (rpc_endpoints: RpcEndpointsSuiteOption, session_options: SessionOptions<string>) => Array<RpcEndpointSpec>;
|
|
36
39
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAa7B,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,EAIN,KAAK,gBAAgB,EACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,2BAA2B,CAAC;AACzE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0BAA0B,CAAC;AACxD,OAAO,KAAK,EAAC,qBAAqB,EAAE,mBAAmB,EAAE,eAAe,EAAC,MAAM,oBAAoB,CAAC;AACpG,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAC9D,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG9D;;;;;;;;GAQG;AACH,MAAM,MAAM,uBAAuB,GAChC,KAAK,CAAC,eAAe,CAAC,GACtB,CAAC,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC;AAEvD
|
|
1
|
+
{"version":3,"file":"rpc_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_helpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,21 +16,41 @@ import { create_stub_app_server_context } from './stubs.js';
|
|
|
16
16
|
* Resolve a suite's `rpc_endpoints` option to an array for setup-time
|
|
17
17
|
* inspection (path lookup, action presence checks).
|
|
18
18
|
*
|
|
19
|
-
* For the factory form this invokes the factory
|
|
20
|
-
* `AppServerContext`
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* For the factory form this invokes the factory twice with stub
|
|
20
|
+
* `AppServerContext`s and asserts that both invocations produce the same
|
|
21
|
+
* (path, method-list) shape — catching factories that close over mutable
|
|
22
|
+
* state or otherwise diverge across calls. The first array is returned;
|
|
23
|
+
* the second is discarded after the comparison. `create_app_server`
|
|
24
|
+
* invokes the factory again per-test with its real ctx, and those are
|
|
25
|
+
* the handlers that actually serve requests.
|
|
24
26
|
*
|
|
25
27
|
* Safe as long as the factory is pure with respect to the endpoint `path`
|
|
26
28
|
* and the action `spec.method` list — the canonical helpers
|
|
27
29
|
* (`create_standard_rpc_actions`, `create_admin_actions`, `create_account_actions`,
|
|
28
30
|
* etc.) are. Factories that return a different `path` based on `ctx` will
|
|
29
|
-
* produce a setup/runtime mismatch;
|
|
31
|
+
* produce a setup/runtime mismatch; the path-purity assert below surfaces
|
|
32
|
+
* that as a clear `gro check` error rather than a silent test/runtime drift.
|
|
30
33
|
*/
|
|
31
|
-
export const resolve_rpc_endpoints_for_setup = (rpc_endpoints, session_options) =>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
export const resolve_rpc_endpoints_for_setup = (rpc_endpoints, session_options) => {
|
|
35
|
+
if (typeof rpc_endpoints !== 'function')
|
|
36
|
+
return rpc_endpoints;
|
|
37
|
+
const first = rpc_endpoints(create_stub_app_server_context(session_options));
|
|
38
|
+
const second = rpc_endpoints(create_stub_app_server_context(session_options));
|
|
39
|
+
const summarize = (eps) => JSON.stringify(eps
|
|
40
|
+
.map((ep) => ({
|
|
41
|
+
path: ep.path,
|
|
42
|
+
methods: ep.actions.map((a) => a.spec.method).sort(),
|
|
43
|
+
}))
|
|
44
|
+
.sort((a, b) => a.path.localeCompare(b.path)));
|
|
45
|
+
const summary_a = summarize(first);
|
|
46
|
+
const summary_b = summarize(second);
|
|
47
|
+
if (summary_a !== summary_b) {
|
|
48
|
+
throw new Error('rpc_endpoints factory is not path-pure: two invocations with equivalent stub ctxs produced different (path, method) shapes. ' +
|
|
49
|
+
`The factory must be pure wrt endpoint path and action method list — see ../testing/rpc_helpers.ts. ` +
|
|
50
|
+
`first=${summary_a} second=${summary_b}`);
|
|
51
|
+
}
|
|
52
|
+
return first;
|
|
53
|
+
};
|
|
34
54
|
/**
|
|
35
55
|
* Create a `RequestInit` for a JSON-RPC POST request.
|
|
36
56
|
*
|
|
@@ -158,15 +158,38 @@ export interface ErrorSchemaTightnessOptions {
|
|
|
158
158
|
/** Routes to skip, in `'METHOD /path'` format. */
|
|
159
159
|
allowlist?: Array<string>;
|
|
160
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Routes shipped by fuz_app whose error schemas require a tightness exemption.
|
|
163
|
+
*
|
|
164
|
+
* Currently empty — every fuz_app-shipped route (account login/password/
|
|
165
|
+
* bootstrap/signup, db health/tables/:name/tables/:name/rows/:id) was tightened
|
|
166
|
+
* in place to `z.enum([...])` / `z.literal(...)` against every emit-site error
|
|
167
|
+
* code.
|
|
168
|
+
*
|
|
169
|
+
* Kept as a forward-compatibility hook: when new stock routes ship with
|
|
170
|
+
* heterogeneous error surfaces that need an interim generic schema, add
|
|
171
|
+
* them here instead of forcing every consumer to hand-maintain the entry.
|
|
172
|
+
*
|
|
173
|
+
* Paths assume the standard `/api/account` + `/api/db` prefixes used by every
|
|
174
|
+
* fuz_app consumer. Merged into `DEFAULT_ERROR_SCHEMA_TIGHTNESS.allowlist` so
|
|
175
|
+
* consumers calling `assert_error_schema_tightness` directly inherit the
|
|
176
|
+
* exemptions; the standard attack-surface suite also prepends these entries
|
|
177
|
+
* underneath any consumer-supplied allowlist so project-specific entries are
|
|
178
|
+
* additive.
|
|
179
|
+
*/
|
|
180
|
+
export declare const FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST: ReadonlyArray<string>;
|
|
161
181
|
/**
|
|
162
182
|
* Baseline error schema tightness applied by
|
|
163
183
|
* `describe_standard_attack_surface_tests` when no config is passed.
|
|
164
184
|
*
|
|
165
185
|
* Uses `min_specificity: 'enum'` (the assertion default) with `ignore_statuses`
|
|
166
186
|
* for middleware-derived status codes that are commonly generic (auth middleware
|
|
167
|
-
* produces multiple error codes at 401/403, and 429 comes from rate limiters)
|
|
168
|
-
*
|
|
169
|
-
*
|
|
187
|
+
* produces multiple error codes at 401/403, and 429 comes from rate limiters),
|
|
188
|
+
* and `allowlist` seeded with `FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST` so
|
|
189
|
+
* fuz_app-shipped routes with heterogeneous generic schemas don't force every
|
|
190
|
+
* consumer to hand-maintain an identical allowlist. Consumers can pass a
|
|
191
|
+
* narrower config with project-specific `allowlist` entries, or pass `null`
|
|
192
|
+
* to skip the assertion entirely.
|
|
170
193
|
*/
|
|
171
194
|
export declare const DEFAULT_ERROR_SCHEMA_TIGHTNESS: ErrorSchemaTightnessOptions;
|
|
172
195
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"surface_invariants.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/surface_invariants.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAuB7B,OAAO,KAAK,EAAC,UAAU,EAAuB,MAAM,oBAAoB,CAAC;AAczE;;GAEG;AACH,eAAO,MAAM,mCAAmC,GAAI,SAAS,UAAU,KAAG,IAQzE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,8BAA8B,GAAI,SAAS,UAAU,KAAG,IASpE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,+BAA+B,GAAI,SAAS,UAAU,KAAG,IAQrE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,gCAAgC,GAAI,SAAS,UAAU,KAAG,IAQtE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,+BAA+B,GAAI,SAAS,UAAU,KAAG,IAQrE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,2BAA2B,GAAI,SAAS,UAAU,KAAG,IAIjE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,0BAA0B,GAAI,SAAS,UAAU,KAAG,IAOhE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mCAAmC,GAAI,SAAS,UAAU,KAAG,IAezE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,uCAAuC,GAAI,SAAS,UAAU,KAAG,IAgB7E,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oCAAoC,GAAI,SAAS,UAAU,KAAG,IAuC1E,CAAC;AA0CF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,sCAAsC,GAAI,SAAS,UAAU,KAAG,IAU5E,CAAC;AAIF,4DAA4D;AAC5D,MAAM,MAAM,sBAAsB,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;AAEpE,iEAAiE;AACjE,MAAM,WAAW,qBAAqB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,sBAAsB,CAAC;IACpC,qDAAqD;IACrD,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;CAClC;AA+BD;;;;;;;;;GASG;AACH,eAAO,MAAM,4BAA4B,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,qBAAqB,CAgB7F,CAAC;AAIF;;;;GAIG;AACH,MAAM,WAAW,4BAA4B;IAC5C;;;;;OAKG;IACH,wBAAwB,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IAClD;;;OAGG;IACH,yBAAyB,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1C;;;OAGG;IACH,qBAAqB,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtC;AASD;;;;;;GAMG;AACH,eAAO,MAAM,oCAAoC,GAChD,SAAS,UAAU,EACnB,qBAAoB,KAAK,CAAC,MAAM,GAAG,MAAM,CAA8B,KACrE,IAcF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qCAAqC,GACjD,SAAS,UAAU,EACnB,YAAW,KAAK,CAAC,MAAM,CAAM,KAC3B,IAYF,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,+BAA+B,GAAI,SAAS,UAAU,KAAG,IAQrE,CAAC;AAKF;;;;;GAKG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,UAAU,EACnB,WAAU,KAAK,CAAC,MAAM,CAAiC,KACrD,IASF,CAAC;AAWF,mDAAmD;AACnD,MAAM,WAAW,2BAA2B;IAC3C,6FAA6F;IAC7F,eAAe,CAAC,EAAE,sBAAsB,CAAC;IACzC,mEAAmE;IACnE,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAChC,kDAAkD;IAClD,SAAS,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC1B;AAED
|
|
1
|
+
{"version":3,"file":"surface_invariants.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/surface_invariants.ts"],"names":[],"mappings":"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"}
|
|
@@ -379,18 +379,42 @@ const SPECIFICITY_ORDER = {
|
|
|
379
379
|
enum: 1,
|
|
380
380
|
generic: 0,
|
|
381
381
|
};
|
|
382
|
+
/**
|
|
383
|
+
* Routes shipped by fuz_app whose error schemas require a tightness exemption.
|
|
384
|
+
*
|
|
385
|
+
* Currently empty — every fuz_app-shipped route (account login/password/
|
|
386
|
+
* bootstrap/signup, db health/tables/:name/tables/:name/rows/:id) was tightened
|
|
387
|
+
* in place to `z.enum([...])` / `z.literal(...)` against every emit-site error
|
|
388
|
+
* code.
|
|
389
|
+
*
|
|
390
|
+
* Kept as a forward-compatibility hook: when new stock routes ship with
|
|
391
|
+
* heterogeneous error surfaces that need an interim generic schema, add
|
|
392
|
+
* them here instead of forcing every consumer to hand-maintain the entry.
|
|
393
|
+
*
|
|
394
|
+
* Paths assume the standard `/api/account` + `/api/db` prefixes used by every
|
|
395
|
+
* fuz_app consumer. Merged into `DEFAULT_ERROR_SCHEMA_TIGHTNESS.allowlist` so
|
|
396
|
+
* consumers calling `assert_error_schema_tightness` directly inherit the
|
|
397
|
+
* exemptions; the standard attack-surface suite also prepends these entries
|
|
398
|
+
* underneath any consumer-supplied allowlist so project-specific entries are
|
|
399
|
+
* additive.
|
|
400
|
+
*/
|
|
401
|
+
export const FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST = [];
|
|
382
402
|
/**
|
|
383
403
|
* Baseline error schema tightness applied by
|
|
384
404
|
* `describe_standard_attack_surface_tests` when no config is passed.
|
|
385
405
|
*
|
|
386
406
|
* Uses `min_specificity: 'enum'` (the assertion default) with `ignore_statuses`
|
|
387
407
|
* for middleware-derived status codes that are commonly generic (auth middleware
|
|
388
|
-
* produces multiple error codes at 401/403, and 429 comes from rate limiters)
|
|
389
|
-
*
|
|
390
|
-
*
|
|
408
|
+
* produces multiple error codes at 401/403, and 429 comes from rate limiters),
|
|
409
|
+
* and `allowlist` seeded with `FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST` so
|
|
410
|
+
* fuz_app-shipped routes with heterogeneous generic schemas don't force every
|
|
411
|
+
* consumer to hand-maintain an identical allowlist. Consumers can pass a
|
|
412
|
+
* narrower config with project-specific `allowlist` entries, or pass `null`
|
|
413
|
+
* to skip the assertion entirely.
|
|
391
414
|
*/
|
|
392
415
|
export const DEFAULT_ERROR_SCHEMA_TIGHTNESS = {
|
|
393
416
|
ignore_statuses: [401, 403, 429],
|
|
417
|
+
allowlist: [...FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST],
|
|
394
418
|
};
|
|
395
419
|
/**
|
|
396
420
|
* Assert that all error schemas meet a minimum specificity threshold.
|