@fuzdev/fuz_app 0.62.0 → 0.64.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 +139 -24
- package/dist/actions/action_rpc.d.ts +10 -0
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +1 -1
- package/dist/actions/action_spec.d.ts +1 -1
- package/dist/actions/action_spec.js +1 -1
- package/dist/actions/connection_closer.d.ts +68 -0
- package/dist/actions/connection_closer.d.ts.map +1 -0
- package/dist/actions/connection_closer.js +41 -0
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +1 -0
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +23 -2
- package/dist/actions/register_ws_endpoint.d.ts +11 -9
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +5 -5
- package/dist/actions/transports_ws_auth_guard.d.ts +24 -8
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +23 -7
- package/dist/actions/ws_endpoint_spec.d.ts +119 -0
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -0
- package/dist/actions/ws_endpoint_spec.js +13 -0
- package/dist/auth/CLAUDE.md +124 -39
- package/dist/auth/account_action_specs.d.ts +7 -1
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +11 -4
- package/dist/auth/account_actions.d.ts +13 -0
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +40 -5
- package/dist/auth/account_routes.d.ts +12 -2
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +63 -12
- package/dist/auth/account_schema.d.ts +5 -5
- package/dist/auth/account_schema.js +2 -2
- package/dist/auth/actor_lookup_actions.d.ts +1 -1
- package/dist/auth/actor_lookup_actions.js +1 -1
- package/dist/auth/actor_lookup_queries.d.ts +1 -1
- package/dist/auth/actor_lookup_queries.js +1 -1
- package/dist/auth/actor_search_action_specs.d.ts +1 -1
- package/dist/auth/actor_search_action_specs.js +1 -1
- package/dist/auth/actor_search_actions.d.ts +1 -1
- package/dist/auth/actor_search_actions.js +1 -1
- package/dist/auth/actor_search_queries.d.ts +1 -1
- package/dist/auth/actor_search_queries.js +1 -1
- package/dist/auth/admin_action_specs.d.ts +8 -8
- package/dist/auth/admin_actions.d.ts +11 -0
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +25 -0
- package/dist/auth/all_action_spec_registries.d.ts +2 -2
- package/dist/auth/all_action_spec_registries.js +2 -2
- package/dist/auth/audit_emitter.d.ts +56 -12
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +38 -12
- package/dist/auth/audit_log_routes.d.ts +1 -1
- package/dist/auth/audit_log_routes.js +1 -1
- package/dist/auth/audit_log_schema.d.ts +30 -3
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +21 -3
- package/dist/auth/bootstrap_routes.d.ts +1 -1
- package/dist/auth/invite_schema.d.ts +2 -2
- package/dist/auth/request_context.d.ts +1 -1
- package/dist/auth/signup_routes.d.ts +1 -1
- package/dist/auth/standard_rpc_actions.d.ts +1 -0
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +1 -0
- package/dist/env/update_env_variable.js +1 -1
- package/dist/http/CLAUDE.md +42 -26
- package/dist/http/ip_canonical.d.ts +99 -0
- package/dist/http/ip_canonical.d.ts.map +1 -0
- package/dist/http/ip_canonical.js +191 -0
- package/dist/http/origin.d.ts +13 -5
- package/dist/http/origin.d.ts.map +1 -1
- package/dist/http/origin.js +13 -31
- package/dist/http/pending_effects.d.ts +1 -1
- package/dist/http/pending_effects.js +1 -1
- package/dist/http/proxy.d.ts +13 -5
- package/dist/http/proxy.d.ts.map +1 -1
- package/dist/http/proxy.js +15 -23
- package/dist/http/surface.d.ts +50 -0
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +27 -1
- package/dist/primitive_schemas.d.ts +20 -4
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +25 -4
- package/dist/realtime/sse_auth_guard.d.ts +16 -4
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +15 -3
- package/dist/server/app_backend.d.ts +66 -19
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +57 -34
- package/dist/server/app_server.d.ts +60 -0
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +95 -2
- package/dist/server/startup.d.ts.map +1 -1
- package/dist/server/startup.js +12 -0
- package/dist/testing/CLAUDE.md +91 -71
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +4 -5
- package/dist/testing/adversarial_headers.d.ts +6 -0
- package/dist/testing/adversarial_headers.d.ts.map +1 -1
- package/dist/testing/adversarial_headers.js +13 -5
- package/dist/testing/app_server.d.ts +33 -32
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +4 -13
- package/dist/testing/attack_surface.d.ts +8 -7
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +12 -8
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +20 -6
- package/dist/testing/audit_drift_guard.d.ts +116 -0
- package/dist/testing/audit_drift_guard.d.ts.map +1 -0
- package/dist/testing/audit_drift_guard.js +134 -0
- package/dist/testing/connection_closer_helpers.d.ts +44 -0
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -0
- package/dist/testing/connection_closer_helpers.js +48 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +7 -9
- package/dist/testing/rate_limiting.js +4 -4
- package/dist/testing/rpc_helpers.d.ts +2 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +6 -8
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +12 -6
- package/dist/testing/stubs.d.ts +11 -0
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +4 -0
- package/dist/testing/surface_invariants.d.ts +66 -1
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +103 -1
- package/dist/ui/CLAUDE.md +13 -18
- package/dist/ui/SurfaceExplorer.svelte +161 -2
- package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
- package/dist/ui/keyed_async_slot.svelte.d.ts +1 -1
- package/dist/ui/keyed_async_slot.svelte.js +1 -1
- package/package.json +1 -1
|
@@ -15,6 +15,12 @@ export interface AdversarialHeaderCase {
|
|
|
15
15
|
/**
|
|
16
16
|
* 7 standard adversarial header cases applicable to any middleware stack.
|
|
17
17
|
*
|
|
18
|
+
* Origin verification is Origin-only — fuz_app's `verify_request_source`
|
|
19
|
+
* no longer falls back to `Referer` (matches `zzz_server::auth::is_request_origin_allowed`).
|
|
20
|
+
* Bearer auth still treats a `Referer` header as a browser-context
|
|
21
|
+
* indicator and silently discards the bearer token — so Referer-bearing
|
|
22
|
+
* requests reach the route as unauthenticated rather than 403.
|
|
23
|
+
*
|
|
18
24
|
* @param allowed_origin - an origin that passes the origin check
|
|
19
25
|
*/
|
|
20
26
|
export declare const create_standard_adversarial_cases: (allowed_origin: string) => Array<AdversarialHeaderCase>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adversarial_headers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/adversarial_headers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAY7B,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAG3B,OAAO,EAGN,KAAK,0BAA0B,EAC/B,MAAM,iBAAiB,CAAC;AAIzB,+DAA+D;AAC/D,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+GAA+G;IAC/G,qBAAqB,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;IAClC,qGAAqG;IACrG,oBAAoB,EAAE,QAAQ,GAAG,YAAY,CAAC;CAC9C;AAID
|
|
1
|
+
{"version":3,"file":"adversarial_headers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/adversarial_headers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAY7B,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAG3B,OAAO,EAGN,KAAK,0BAA0B,EAC/B,MAAM,iBAAiB,CAAC;AAIzB,+DAA+D;AAC/D,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+GAA+G;IAC/G,qBAAqB,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;IAClC,qGAAqG;IACrG,oBAAoB,EAAE,QAAQ,GAAG,YAAY,CAAC;CAC9C;AAID;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iCAAiC,GAC7C,gBAAgB,MAAM,KACpB,KAAK,CAAC,qBAAqB,CAiE7B,CAAC;AAIF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qCAAqC,GACjD,YAAY,MAAM,EAClB,SAAS,0BAA0B,EACnC,gBAAgB,MAAM,EACtB,cAAc,KAAK,CAAC,qBAAqB,CAAC,KACxC,IAkCF,CAAC"}
|
|
@@ -8,12 +8,18 @@ import './assert_dev_env.js';
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
import { test, assert, describe } from 'vitest';
|
|
11
|
-
import { ApiError, ERROR_FORBIDDEN_ORIGIN
|
|
11
|
+
import { ApiError, ERROR_FORBIDDEN_ORIGIN } from '../http/error_schemas.js';
|
|
12
12
|
import { create_test_middleware_stack_app, TEST_MIDDLEWARE_PATH, } from './middleware.js';
|
|
13
13
|
// --- Standard adversarial header cases ---
|
|
14
14
|
/**
|
|
15
15
|
* 7 standard adversarial header cases applicable to any middleware stack.
|
|
16
16
|
*
|
|
17
|
+
* Origin verification is Origin-only — fuz_app's `verify_request_source`
|
|
18
|
+
* no longer falls back to `Referer` (matches `zzz_server::auth::is_request_origin_allowed`).
|
|
19
|
+
* Bearer auth still treats a `Referer` header as a browser-context
|
|
20
|
+
* indicator and silently discards the bearer token — so Referer-bearing
|
|
21
|
+
* requests reach the route as unauthenticated rather than 403.
|
|
22
|
+
*
|
|
17
23
|
* @param allowed_origin - an origin that passes the origin check
|
|
18
24
|
*/
|
|
19
25
|
export const create_standard_adversarial_cases = (allowed_origin) => [
|
|
@@ -61,17 +67,19 @@ export const create_standard_adversarial_cases = (allowed_origin) => [
|
|
|
61
67
|
validate_expectation: 'called',
|
|
62
68
|
},
|
|
63
69
|
{
|
|
64
|
-
name: 'bearer token with Referer
|
|
70
|
+
name: 'bearer token with rogue Referer (no Origin) passes origin check, bearer silently discarded (browser-context indicator)',
|
|
71
|
+
// Origin-only verification ignores Referer entirely. Bearer auth still
|
|
72
|
+
// treats Referer presence as a browser-context indicator and discards
|
|
73
|
+
// the token, so the request reaches the route as unauthenticated.
|
|
65
74
|
headers: {
|
|
66
75
|
Authorization: 'Bearer secret_fuz_token_test',
|
|
67
76
|
Referer: 'https://attacker.com/page',
|
|
68
77
|
},
|
|
69
|
-
expected_status:
|
|
70
|
-
expected_error: ERROR_FORBIDDEN_REFERER,
|
|
78
|
+
expected_status: 200,
|
|
71
79
|
validate_expectation: 'not_called',
|
|
72
80
|
},
|
|
73
81
|
{
|
|
74
|
-
name: 'bearer token with Referer
|
|
82
|
+
name: 'bearer token with allowed Referer (no Origin) — bearer silently discarded (browser context)',
|
|
75
83
|
headers: {
|
|
76
84
|
Authorization: 'Bearer secret_fuz_token_test',
|
|
77
85
|
Referer: `${allowed_origin}/page`,
|
|
@@ -18,8 +18,7 @@ import { type Keyring } from '../auth/keyring.js';
|
|
|
18
18
|
import type { Db, DbType } from '../db/db.js';
|
|
19
19
|
import type { PasswordHashDeps } from '../auth/password.js';
|
|
20
20
|
import { type SessionOptions } from '../auth/session_cookie.js';
|
|
21
|
-
import type
|
|
22
|
-
import type { AppBackend } from '../server/app_backend.js';
|
|
21
|
+
import { type AppBackend, type AuditFactory } from '../server/app_backend.js';
|
|
23
22
|
import { type AppServerOptions, type AppServerContext } from '../server/app_server.js';
|
|
24
23
|
import type { AppSurface, AppSurfaceSpec } from '../http/surface.js';
|
|
25
24
|
import type { RouteSpec } from '../http/route_spec.js';
|
|
@@ -107,23 +106,21 @@ export interface TestAppServerOptions {
|
|
|
107
106
|
/** Roles to grant. Default: `[ROLE_KEEPER]`. */
|
|
108
107
|
roles?: Array<string>;
|
|
109
108
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
* Optional audit log config — threaded into `create_audit_emitter` and
|
|
119
|
-
* captured inside `backend.deps.audit`'s closure.
|
|
109
|
+
* Build the bound `AuditEmitter` used by the test backend. Defaults to
|
|
110
|
+
* `default_audit_factory` (a no-listener `create_audit_emitter` over
|
|
111
|
+
* the test backend's `{db, log}`). Pass a custom factory when a test
|
|
112
|
+
* needs:
|
|
113
|
+
* - to capture audit events (compose `on_audit_event` inside the body)
|
|
114
|
+
* - to register consumer event-type schemas (pass `audit_log_config`)
|
|
115
|
+
* - to instrument `emit` ordering (`create_emit_ordering_audit_factory`)
|
|
116
|
+
* - to wrap or replace the emitter for some other reason
|
|
120
117
|
*
|
|
121
|
-
*
|
|
122
|
-
* `
|
|
123
|
-
*
|
|
124
|
-
*
|
|
118
|
+
* Matches the production shape — `create_app_backend` requires an
|
|
119
|
+
* `audit_factory` and `create_test_app_server` mirrors that contract
|
|
120
|
+
* end-to-end. The earlier `on_audit_event` / `audit_log_config` sugar
|
|
121
|
+
* fields were removed alongside the `CreateAppBackendOptions` rename.
|
|
125
122
|
*/
|
|
126
|
-
|
|
123
|
+
audit_factory?: AuditFactory;
|
|
127
124
|
}
|
|
128
125
|
/**
|
|
129
126
|
* Create an app server with a bootstrapped account for testing.
|
|
@@ -154,25 +151,29 @@ export interface CreateTestAppOptions extends TestAppServerOptions {
|
|
|
154
151
|
create_route_specs: (context: AppServerContext) => Array<RouteSpec>;
|
|
155
152
|
/**
|
|
156
153
|
* RPC endpoints mounted by `create_app_server` — eager array or
|
|
157
|
-
* `(ctx: AppServerContext) => Array<RpcEndpointSpec>` factory.
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* both are set `app_options` wins and `console.warn` fires.
|
|
154
|
+
* `(ctx: AppServerContext) => Array<RpcEndpointSpec>` factory. Single
|
|
155
|
+
* source of truth; the equivalent slot under `app_options` is `Omit`'d
|
|
156
|
+
* so setup-time path lookup and runtime dispatch read from one place.
|
|
157
|
+
* Symmetric with the suite-level `rpc_endpoints` option on
|
|
158
|
+
* `describe_standard_admin_integration_tests` etc.
|
|
163
159
|
*/
|
|
164
160
|
rpc_endpoints?: RpcEndpointsSuiteOption;
|
|
165
|
-
/**
|
|
166
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Optional overrides for `AppServerOptions`. The four fields
|
|
163
|
+
* `create_test_app` manages are excluded: `backend`, `session_options`,
|
|
164
|
+
* `create_route_specs`, and `rpc_endpoints` (see top-level slot above).
|
|
165
|
+
*/
|
|
166
|
+
app_options?: SuiteAppOptions;
|
|
167
167
|
}
|
|
168
168
|
/**
|
|
169
|
-
* `app_options` shape accepted by DB-backed suite
|
|
170
|
-
* (`describe_standard_integration_tests`,
|
|
171
|
-
* etc.). Excludes
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
169
|
+
* `app_options` shape accepted by `create_test_app` and the DB-backed suite
|
|
170
|
+
* helpers (`describe_standard_integration_tests`,
|
|
171
|
+
* `describe_audit_completeness_tests`, etc.). Excludes the four fields the
|
|
172
|
+
* helpers manage directly — `backend` / `session_options` /
|
|
173
|
+
* `create_route_specs` are constructed by the helper itself; `rpc_endpoints`
|
|
174
|
+
* lives on the top-level option (hard-failed by `require_rpc_endpoint_path`
|
|
175
|
+
* in the suites) so setup-time path lookup and runtime dispatch read from
|
|
176
|
+
* one source of truth.
|
|
176
177
|
*/
|
|
177
178
|
export type SuiteAppOptions = Partial<Omit<AppServerOptions, 'backend' | 'session_options' | 'create_route_specs' | 'rpc_endpoints'>>;
|
|
178
179
|
/**
|
|
@@ -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;AAG/B,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAGjD,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,
|
|
1
|
+
{"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/app_server.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,7 +11,7 @@ import { query_create_api_token } from '../auth/api_token_queries.js';
|
|
|
11
11
|
import { create_session_cookie_value } from '../auth/session_cookie.js';
|
|
12
12
|
import { run_migrations } from '../db/migrate.js';
|
|
13
13
|
import { auth_migration_ns } from '../auth/migrations.js';
|
|
14
|
-
import {
|
|
14
|
+
import { default_audit_factory } from '../server/app_backend.js';
|
|
15
15
|
import { create_app_server, } from '../server/app_server.js';
|
|
16
16
|
import { generate_daemon_token, DAEMON_TOKEN_HEADER, } from '../auth/daemon_token.js';
|
|
17
17
|
import { create_pglite_factory } from './db.js';
|
|
@@ -96,8 +96,7 @@ const test_log = new Logger('test', { level: 'off' });
|
|
|
96
96
|
* API token, and session row.
|
|
97
97
|
*/
|
|
98
98
|
export const create_test_app_server = async (options) => {
|
|
99
|
-
const { session_options, db: existing_db, db_type = 'pglite-memory', password = stub_password_deps, username = 'keeper', password_value = 'test-password-123', roles = [ROLE_KEEPER],
|
|
100
|
-
audit_log_config, } = options;
|
|
99
|
+
const { session_options, db: existing_db, db_type = 'pglite-memory', password = stub_password_deps, username = 'keeper', password_value = 'test-password-123', roles = [ROLE_KEEPER], audit_factory = default_audit_factory, } = options;
|
|
101
100
|
// Keyring from test secret
|
|
102
101
|
const keyring_result = create_validated_keyring(TEST_COOKIE_SECRET);
|
|
103
102
|
if (!keyring_result.ok) {
|
|
@@ -116,12 +115,7 @@ export const create_test_app_server = async (options) => {
|
|
|
116
115
|
await existing_db.query('UPDATE app_settings SET open_signup = false, updated_at = NULL, updated_by = NULL WHERE open_signup = true OR updated_at IS NOT NULL');
|
|
117
116
|
// Use the caller's database — tables already created by the factory's init_schema.
|
|
118
117
|
// Caller owns the DB lifecycle — close is a no-op.
|
|
119
|
-
const audit =
|
|
120
|
-
db: existing_db,
|
|
121
|
-
log: test_log,
|
|
122
|
-
on_audit_event,
|
|
123
|
-
audit_log_config,
|
|
124
|
-
});
|
|
118
|
+
const audit = audit_factory({ db: existing_db, log: test_log });
|
|
125
119
|
backend = {
|
|
126
120
|
db_type,
|
|
127
121
|
db_name: 'test',
|
|
@@ -142,7 +136,7 @@ export const create_test_app_server = async (options) => {
|
|
|
142
136
|
// instead of creating a new PGlite each time. Schema is reset and migrations re-run
|
|
143
137
|
// on each call, but the expensive WASM cold start only happens once per worker thread.
|
|
144
138
|
const db = await fallback_pglite_factory.create();
|
|
145
|
-
const audit =
|
|
139
|
+
const audit = audit_factory({ db, log: test_log });
|
|
146
140
|
backend = {
|
|
147
141
|
db_type: 'pglite-memory',
|
|
148
142
|
db_name: '(memory)',
|
|
@@ -198,9 +192,6 @@ export const create_test_app = async (options) => {
|
|
|
198
192
|
rotated_at: new Date(),
|
|
199
193
|
keeper_account_id: test_server.account.id,
|
|
200
194
|
};
|
|
201
|
-
if (options.rpc_endpoints !== undefined && options.app_options?.rpc_endpoints !== undefined) {
|
|
202
|
-
console.warn('create_test_app: both top-level `rpc_endpoints` and `app_options.rpc_endpoints` are set; preferring `app_options.rpc_endpoints` (back-compat).');
|
|
203
|
-
}
|
|
204
195
|
const result = await create_app_server({
|
|
205
196
|
backend: test_server,
|
|
206
197
|
session_options: options.session_options,
|
|
@@ -67,17 +67,18 @@ export interface StandardAttackSurfaceOptions {
|
|
|
67
67
|
/**
|
|
68
68
|
* Run the standard attack surface test suite.
|
|
69
69
|
*
|
|
70
|
-
*
|
|
70
|
+
* Test groups:
|
|
71
71
|
* 1. Snapshot — live surface matches committed JSON
|
|
72
72
|
* 2. Determinism — building twice yields identical results
|
|
73
73
|
* 3. Public routes — bidirectional check (no unexpected, no missing)
|
|
74
74
|
* 4. Middleware stack — every API route has the full middleware chain
|
|
75
|
-
* 5. Surface invariants — structural assertions (error schemas, descriptions, duplicates, consistency)
|
|
76
|
-
* 6.
|
|
77
|
-
* 7.
|
|
78
|
-
* 8.
|
|
79
|
-
* 9. Adversarial
|
|
80
|
-
* 10. Adversarial
|
|
75
|
+
* 5. Surface invariants — structural assertions over `surface.routes` (error schemas, descriptions, duplicates, consistency)
|
|
76
|
+
* 6. RPC/WS surface invariants — structural assertions over `surface.rpc_endpoints` + `surface.ws_endpoints` (descriptions, protocol-action spread, kind ⇔ auth)
|
|
77
|
+
* 7. Security policy — rate limiting on sensitive routes, no unexpected public mutations, method conventions
|
|
78
|
+
* 8. Error schema tightness — informational log of generic vs specific error schemas, plus assertion against `default_error_schema_tightness` by default (opt out with `error_schema_tightness: null`)
|
|
79
|
+
* 9. Adversarial auth — unauthenticated/wrong-role/correct-auth enforcement
|
|
80
|
+
* 10. Adversarial input — input body and params validation
|
|
81
|
+
* 11. Adversarial 404 — stub 404 handlers, validate response bodies against declared schemas
|
|
81
82
|
*
|
|
82
83
|
* Consumer test files call this with project-specific options, then add
|
|
83
84
|
* any project-specific assertions in additional `describe` blocks.
|
|
@@ -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,EAQN,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;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,sBAAsB,KAAG,IA4H3E,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;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sCAAsC,GAClD,SAAS,4BAA4B,KACnC,IA2EF,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, fuz_app_stock_route_tightness_allowlist, } from './surface_invariants.js';
|
|
18
|
+
import { assert_surface_invariants, assert_rpc_ws_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';
|
|
@@ -193,17 +193,18 @@ export const resolve_standard_error_schema_tightness = (consumer) => {
|
|
|
193
193
|
/**
|
|
194
194
|
* Run the standard attack surface test suite.
|
|
195
195
|
*
|
|
196
|
-
*
|
|
196
|
+
* Test groups:
|
|
197
197
|
* 1. Snapshot — live surface matches committed JSON
|
|
198
198
|
* 2. Determinism — building twice yields identical results
|
|
199
199
|
* 3. Public routes — bidirectional check (no unexpected, no missing)
|
|
200
200
|
* 4. Middleware stack — every API route has the full middleware chain
|
|
201
|
-
* 5. Surface invariants — structural assertions (error schemas, descriptions, duplicates, consistency)
|
|
202
|
-
* 6.
|
|
203
|
-
* 7.
|
|
204
|
-
* 8.
|
|
205
|
-
* 9. Adversarial
|
|
206
|
-
* 10. Adversarial
|
|
201
|
+
* 5. Surface invariants — structural assertions over `surface.routes` (error schemas, descriptions, duplicates, consistency)
|
|
202
|
+
* 6. RPC/WS surface invariants — structural assertions over `surface.rpc_endpoints` + `surface.ws_endpoints` (descriptions, protocol-action spread, kind ⇔ auth)
|
|
203
|
+
* 7. Security policy — rate limiting on sensitive routes, no unexpected public mutations, method conventions
|
|
204
|
+
* 8. Error schema tightness — informational log of generic vs specific error schemas, plus assertion against `default_error_schema_tightness` by default (opt out with `error_schema_tightness: null`)
|
|
205
|
+
* 9. Adversarial auth — unauthenticated/wrong-role/correct-auth enforcement
|
|
206
|
+
* 10. Adversarial input — input body and params validation
|
|
207
|
+
* 11. Adversarial 404 — stub 404 handlers, validate response bodies against declared schemas
|
|
207
208
|
*
|
|
208
209
|
* Consumer test files call this with project-specific options, then add
|
|
209
210
|
* any project-specific assertions in additional `describe` blocks.
|
|
@@ -231,6 +232,9 @@ export const describe_standard_attack_surface_tests = (options) => {
|
|
|
231
232
|
test('surface invariants', () => {
|
|
232
233
|
assert_surface_invariants(surface);
|
|
233
234
|
});
|
|
235
|
+
test('rpc/ws surface invariants', () => {
|
|
236
|
+
assert_rpc_ws_surface_invariants(surface);
|
|
237
|
+
});
|
|
234
238
|
test('security policy', () => {
|
|
235
239
|
assert_surface_security_policy(surface, security_policy);
|
|
236
240
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit_completeness.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_completeness.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAkB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAC9D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAIrD,OAAO,EAGN,KAAK,eAAe,EAEpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAKjB,OAAO,EAIN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAqB1B;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC5C,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE;;;;;;;;;;;OAWG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC,iDAAiD;IACjD,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAChC;
|
|
1
|
+
{"version":3,"file":"audit_completeness.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_completeness.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAkB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAC9D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAIrD,OAAO,EAGN,KAAK,eAAe,EAEpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAKjB,OAAO,EAIN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAqB1B;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC5C,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE;;;;;;;;;;;OAWG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC,iDAAiD;IACjD,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAChC;AAyED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iCAAiC,GAAI,SAAS,4BAA4B,KAAG,IAihBzF,CAAC"}
|
|
@@ -27,22 +27,31 @@ import { admin_session_revoke_all_action_spec, admin_token_revoke_all_action_spe
|
|
|
27
27
|
import { account_session_list_action_spec, account_session_revoke_action_spec, account_session_revoke_all_action_spec, account_token_create_action_spec, account_token_list_action_spec, account_token_revoke_action_spec, } from '../auth/account_action_specs.js';
|
|
28
28
|
/** Query audit log events from the database. */
|
|
29
29
|
const query_audit_events = async (db) => {
|
|
30
|
-
return db.query('SELECT event_type, seq FROM audit_log ORDER BY seq');
|
|
30
|
+
return db.query('SELECT event_type, seq, metadata FROM audit_log ORDER BY seq');
|
|
31
31
|
};
|
|
32
32
|
/** Assert that audit events contain the expected event type. */
|
|
33
33
|
const assert_has_event = (events, expected, context) => {
|
|
34
34
|
assert.ok(events.some((e) => e.event_type === expected), `Expected '${expected}' audit event after ${context}`);
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Assert that an event type was emitted with the expected `credential_type`
|
|
38
|
+
* recorded in metadata — defense-in-depth coverage for the spec gate
|
|
39
|
+
* documented in `docs/security.md` §Credential-channel gating.
|
|
40
|
+
*/
|
|
41
|
+
const assert_event_credential_type = (events, expected, credential_type, context) => {
|
|
42
|
+
const match = events.find((e) => e.event_type === expected);
|
|
43
|
+
assert.ok(match, `Expected '${expected}' audit event after ${context}`);
|
|
44
|
+
const recorded = (match.metadata ?? {}).credential_type;
|
|
45
|
+
assert.strictEqual(recorded, credential_type, `Expected '${expected}' audit metadata.credential_type === '${credential_type}' after ${context} (got ${JSON.stringify(recorded)})`);
|
|
46
|
+
};
|
|
36
47
|
/** Build CreateTestAppOptions with admin+keeper roles. */
|
|
37
48
|
const build_options = (options, db) => ({
|
|
38
49
|
session_options: options.session_options,
|
|
39
50
|
create_route_specs: options.create_route_specs,
|
|
40
51
|
db,
|
|
41
52
|
roles: [ROLE_KEEPER, ROLE_ADMIN],
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
rpc_endpoints: options.rpc_endpoints,
|
|
45
|
-
},
|
|
53
|
+
rpc_endpoints: options.rpc_endpoints,
|
|
54
|
+
app_options: options.app_options,
|
|
46
55
|
});
|
|
47
56
|
/** Headers for unauthenticated JSON requests (login, signup). */
|
|
48
57
|
const UNAUTHENTICATED_JSON_HEADERS = {
|
|
@@ -71,7 +80,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
71
80
|
// Hard-fail early so consumers see a clear setup error instead of a
|
|
72
81
|
// confusing test failure when `rpc_endpoints` is missing. Factory-form
|
|
73
82
|
// callers are resolved with a stub ctx purely to extract the endpoint
|
|
74
|
-
// path; real handlers run per-test via `
|
|
83
|
+
// path; real handlers run per-test via the top-level `rpc_endpoints` slot on `CreateTestAppOptions`.
|
|
75
84
|
const rpc_endpoints_for_setup = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
|
|
76
85
|
const rpc_path = require_rpc_endpoint_path(rpc_endpoints_for_setup);
|
|
77
86
|
const init_schema = async (db) => {
|
|
@@ -138,6 +147,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
138
147
|
assert.ok(res.ok, `account_token_create failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
139
148
|
const events = await query_audit_events(test_app.backend.deps.db);
|
|
140
149
|
assert_has_event(events, 'token_create', 'account_token_create RPC');
|
|
150
|
+
assert_event_credential_type(events, 'token_create', 'session', 'account_token_create RPC');
|
|
141
151
|
});
|
|
142
152
|
test('token revoke produces token_revoke event', async () => {
|
|
143
153
|
const test_app = await create_test_app(build_options(options, get_db()));
|
|
@@ -162,6 +172,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
162
172
|
assert.ok(res.ok, `account_token_revoke failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
163
173
|
const events = await query_audit_events(test_app.backend.deps.db);
|
|
164
174
|
assert_has_event(events, 'token_revoke', 'account_token_revoke RPC');
|
|
175
|
+
assert_event_credential_type(events, 'token_revoke', 'session', 'account_token_revoke RPC');
|
|
165
176
|
});
|
|
166
177
|
test('session revoke produces session_revoke event', async () => {
|
|
167
178
|
const test_app = await create_test_app(build_options(options, get_db()));
|
|
@@ -198,6 +209,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
198
209
|
assert.ok(res.ok, `account_session_revoke failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
199
210
|
const events = await query_audit_events(test_app.backend.deps.db);
|
|
200
211
|
assert_has_event(events, 'session_revoke', 'account_session_revoke RPC');
|
|
212
|
+
assert_event_credential_type(events, 'session_revoke', 'session', 'account_session_revoke RPC');
|
|
201
213
|
});
|
|
202
214
|
test('session revoke-all produces session_revoke_all event', async () => {
|
|
203
215
|
const test_app = await create_test_app(build_options(options, get_db()));
|
|
@@ -211,6 +223,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
211
223
|
assert.ok(res.ok, `account_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
212
224
|
const events = await query_audit_events(test_app.backend.deps.db);
|
|
213
225
|
assert_has_event(events, 'session_revoke_all', 'account_session_revoke_all RPC');
|
|
226
|
+
assert_event_credential_type(events, 'session_revoke_all', 'session', 'account_session_revoke_all RPC');
|
|
214
227
|
});
|
|
215
228
|
test('password change produces password_change event', async () => {
|
|
216
229
|
const test_app = await create_test_app(build_options(options, get_db()));
|
|
@@ -227,6 +240,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
227
240
|
assert.strictEqual(res.status, 200);
|
|
228
241
|
const events = await query_audit_events(test_app.backend.deps.db);
|
|
229
242
|
assert_has_event(events, 'password_change', 'POST /password');
|
|
243
|
+
assert_event_credential_type(events, 'password_change', 'session', 'POST /password');
|
|
230
244
|
});
|
|
231
245
|
});
|
|
232
246
|
// --- Admin routes ---
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
import { type AuditEmitter, type CreateAuditEmitterOptions } from '../auth/audit_emitter.js';
|
|
3
|
+
import type { AuditLogInput } from '../auth/audit_log_schema.js';
|
|
4
|
+
import type { AuditFactory } from '../server/app_backend.js';
|
|
5
|
+
/**
|
|
6
|
+
* Register per-test `beforeEach` + `afterEach` hooks that catch any audit
|
|
7
|
+
* emission with a metadata shape that fails its `audit_metadata_schemas`
|
|
8
|
+
* entry, or an `event_type` not present in the active `AuditLogConfig`.
|
|
9
|
+
*
|
|
10
|
+
* The production validation in `query_audit_log` is fail-open — it bumps
|
|
11
|
+
* process-wide counters and proceeds, so a regression that emits an
|
|
12
|
+
* undeclared metadata field or a typo'd event-type lands a row that
|
|
13
|
+
* passes downstream queries but breaks forensics. Tests that exercise
|
|
14
|
+
* audit emits should fail loudly when this happens.
|
|
15
|
+
*
|
|
16
|
+
* Call at the top of every `describe` / `describe_db` block that fires
|
|
17
|
+
* audit writes through `deps.audit.emit`. Resets counters before each
|
|
18
|
+
* test and asserts zero on completion.
|
|
19
|
+
*
|
|
20
|
+
* Pair with `await_pending_effects: true` (the default for
|
|
21
|
+
* `create_test_app`) so fire-and-forget audit writes have completed by
|
|
22
|
+
* the time the after-each check observes counter state.
|
|
23
|
+
*/
|
|
24
|
+
export declare const install_audit_drift_guard: () => void;
|
|
25
|
+
/**
|
|
26
|
+
* Marker pushed into a shared sequence array by an emit-recording
|
|
27
|
+
* `audit_factory`. Pair with `RecordedClose` from
|
|
28
|
+
* `connection_closer_helpers.ts` to test close-vs-emit ordering at
|
|
29
|
+
* handler call sites — see `create_emit_ordering_audit_factory` below.
|
|
30
|
+
*/
|
|
31
|
+
export interface AuditEmitMarker {
|
|
32
|
+
kind: 'emit';
|
|
33
|
+
at: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pair returned by {@link create_recording_audit_emitter} — the
|
|
37
|
+
* `AuditEmitter` to inject as `deps.audit`, plus the shared `calls`
|
|
38
|
+
* array that records every captured emission. Both fields are live —
|
|
39
|
+
* callers read `calls` after exercising the handler to assert on the
|
|
40
|
+
* audit metadata shape.
|
|
41
|
+
*/
|
|
42
|
+
export interface RecordingAuditEmitter {
|
|
43
|
+
emitter: AuditEmitter;
|
|
44
|
+
calls: Array<AuditLogInput>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build a no-op `AuditEmitter` that records every `emit`, `emit_pool`, and
|
|
48
|
+
* `emit_role_grant_target` call into `calls` as an `AuditLogInput`. Use to
|
|
49
|
+
* capture audit metadata shapes in unit tests (e.g. password change failure
|
|
50
|
+
* outcome, role-grant create denial) without standing up the full PGlite +
|
|
51
|
+
* `query_audit_log` pipeline.
|
|
52
|
+
*
|
|
53
|
+
* **Capture scope — all four production fan-out shapes.**
|
|
54
|
+
* `emit_role_grant_target` mirrors `create_audit_emitter`'s lift logic in
|
|
55
|
+
* place — `actor_id` / `account_id` / `ip` are populated from `auth` + `ctx`
|
|
56
|
+
* and the `event_type` / `outcome` / `target_*_id` / `metadata` fields
|
|
57
|
+
* forward from the input envelope. Tests asserting on role-grant-shape
|
|
58
|
+
* emissions read out of the same homogeneous `calls` array.
|
|
59
|
+
* `notify` is a no-op; `on_event_chain` is an empty array.
|
|
60
|
+
*
|
|
61
|
+
* `emit` AND `emit_pool` both append to `calls` so cleanup-sweep tests
|
|
62
|
+
* (which use `emit_pool` exclusively — see `auth/cleanup.ts`) can also
|
|
63
|
+
* read assertions off the same array.
|
|
64
|
+
*
|
|
65
|
+
* Pass `calls_ref` to write into a caller-owned array (callers that
|
|
66
|
+
* declared `const events: Array<AuditLogInput> = []` and want to keep
|
|
67
|
+
* the reference). Omit to let the helper allocate a fresh array and
|
|
68
|
+
* return it on the `calls` field of the result.
|
|
69
|
+
*
|
|
70
|
+
* The returned emitter is deliberately NOT frozen — slots stay mutable
|
|
71
|
+
* so a test can override one when it needs bespoke shape (e.g. an
|
|
72
|
+
* `emit_pool` that throws on the first call). The production
|
|
73
|
+
* `create_audit_emitter` freeze invariant exists to catch the
|
|
74
|
+
* `patch_audit_emit_capture` hot-patch footgun against the
|
|
75
|
+
* closure-captured `emit`; the recording emitter has no inner closure,
|
|
76
|
+
* so the freeze isn't load-bearing here.
|
|
77
|
+
*/
|
|
78
|
+
export declare const create_recording_audit_emitter: (calls_ref?: Array<AuditLogInput>) => RecordingAuditEmitter;
|
|
79
|
+
/**
|
|
80
|
+
* Build an `audit_factory` that produces a real `create_audit_emitter`
|
|
81
|
+
* with its `emit` decorated to push a `{kind: 'emit', at: seq.value++}`
|
|
82
|
+
* marker into a shared sequence + events array. Used by the close-vs-emit
|
|
83
|
+
* ordering test to compose against a shared sequence counter (typically
|
|
84
|
+
* `create_recording_closer(seq_ref)` capturing eager-close calls).
|
|
85
|
+
*
|
|
86
|
+
* Pass the returned factory through `create_test_app({audit_factory: …})`
|
|
87
|
+
* — the test backend invokes it with its constructed `{db, log}` and
|
|
88
|
+
* lands the decorated emitter on `backend.deps.audit`. Production
|
|
89
|
+
* handlers dereference `deps.audit.emit` at call time, so the decorator
|
|
90
|
+
* sees every subsequent handler invocation. The underlying `emit` still
|
|
91
|
+
* runs — the decorator records the call, it does not suppress side
|
|
92
|
+
* effects.
|
|
93
|
+
*
|
|
94
|
+
* **Scope — both `emit` and `emit_role_grant_target`.** The decorator
|
|
95
|
+
* is captured by `emit_role_grant_target`'s closure inside
|
|
96
|
+
* `create_audit_emitter` (and re-exposed as the outer `emit` slot), so
|
|
97
|
+
* role-grant-shape emissions land in `events_ref` alongside bare `emit`
|
|
98
|
+
* calls. `emit_pool` and `notify` are not decorated — they take
|
|
99
|
+
* `AuditLogInput` / `AuditLogEvent` directly without going through
|
|
100
|
+
* `emit`, so handler-side `emit_pool` writes (today only
|
|
101
|
+
* `auth/cleanup.ts`) skip capture. Close-firing handlers all reach for
|
|
102
|
+
* `emit` or `emit_role_grant_target`, so the ordering test sees them
|
|
103
|
+
* regardless of which entry point a future refactor picks.
|
|
104
|
+
*
|
|
105
|
+
* Optionally accept `extra_options` to thread `on_audit_event` /
|
|
106
|
+
* `audit_log_config` into the inner emitter — useful when a test wants
|
|
107
|
+
* both ordering capture and a real SSE/WS guard wired into the same
|
|
108
|
+
* emitter chain.
|
|
109
|
+
*/
|
|
110
|
+
export declare const create_emit_ordering_audit_factory: <E extends {
|
|
111
|
+
kind: string;
|
|
112
|
+
at: number;
|
|
113
|
+
}>(seq_ref: {
|
|
114
|
+
value: number;
|
|
115
|
+
}, events_ref: Array<AuditEmitMarker | E>, extra_options?: Omit<CreateAuditEmitterOptions, "db" | "log" | "emit_decorator">) => AuditFactory;
|
|
116
|
+
//# sourceMappingURL=audit_drift_guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit_drift_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_drift_guard.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAU7B,OAAO,EAEN,KAAK,YAAY,EACjB,KAAK,yBAAyB,EAC9B,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,0BAA0B,CAAC;AAE3D;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,yBAAyB,QAAO,IAiB5C,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACX;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACrC,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,8BAA8B,GAC1C,YAAY,KAAK,CAAC,aAAa,CAAC,KAC9B,qBA0BF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,kCAAkC,GAAI,CAAC,SAAS;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAC,EACtF,SAAS;IAAC,KAAK,EAAE,MAAM,CAAA;CAAC,EACxB,YAAY,KAAK,CAAC,eAAe,GAAG,CAAC,CAAC,EACtC,gBAAgB,IAAI,CAAC,yBAAyB,EAAE,IAAI,GAAG,KAAK,GAAG,gBAAgB,CAAC,KAC9E,YAWF,CAAC"}
|