@fuzdev/fuz_app 0.64.0 → 0.65.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 +513 -928
- package/dist/actions/broadcast_api.d.ts +1 -1
- package/dist/actions/broadcast_api.js +1 -1
- package/dist/actions/cancel.d.ts +2 -2
- package/dist/actions/cancel.js +3 -3
- package/dist/actions/connection_closer.d.ts +1 -4
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +1 -4
- package/dist/actions/register_action_ws.d.ts +2 -2
- package/dist/actions/register_ws_endpoint.d.ts +1 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +1 -2
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +1 -2
- package/dist/auth/CLAUDE.md +591 -1871
- package/dist/auth/account_schema.d.ts +1 -1
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +1 -1
- package/dist/auth/audit_log_ddl.d.ts +1 -1
- package/dist/auth/audit_log_ddl.d.ts.map +1 -1
- package/dist/auth/audit_log_ddl.js +1 -1
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +1 -5
- package/dist/auth/bootstrap_routes.d.ts +7 -1
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +15 -11
- package/dist/auth/keyring.d.ts +6 -6
- package/dist/auth/keyring.js +8 -8
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +4 -2
- package/dist/db/create_db.d.ts.map +1 -1
- package/dist/db/create_db.js +13 -0
- package/dist/dev/setup.d.ts +2 -2
- package/dist/dev/setup.js +3 -3
- package/dist/http/CLAUDE.md +224 -498
- package/dist/http/error_schemas.d.ts +0 -4
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +0 -4
- package/dist/http/ip_canonical.d.ts +5 -4
- package/dist/http/ip_canonical.d.ts.map +1 -1
- package/dist/http/ip_canonical.js +8 -4
- package/dist/http/origin.d.ts +1 -1
- package/dist/http/origin.js +1 -1
- package/dist/runtime/mock.js +1 -1
- package/dist/server/app_server.d.ts +41 -10
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +10 -4
- package/dist/server/env.d.ts +7 -7
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +14 -14
- package/dist/server/static.d.ts +4 -4
- package/dist/server/static.js +7 -7
- package/dist/testing/CLAUDE.md +220 -46
- package/dist/testing/admin_integration.d.ts +18 -23
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +159 -201
- package/dist/testing/app_server.d.ts +125 -38
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +140 -42
- package/dist/testing/audit_completeness.d.ts +23 -22
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +199 -156
- package/dist/testing/bootstrap_success.d.ts +28 -0
- package/dist/testing/bootstrap_success.d.ts.map +1 -0
- package/dist/testing/bootstrap_success.js +144 -0
- package/dist/testing/cross_backend/capabilities.d.ts +64 -0
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
- package/dist/testing/cross_backend/capabilities.js +47 -0
- package/dist/testing/cross_backend/setup.d.ts +215 -0
- package/dist/testing/cross_backend/setup.d.ts.map +1 -0
- package/dist/testing/cross_backend/setup.js +101 -0
- package/dist/testing/data_exposure.d.ts +14 -15
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +127 -146
- package/dist/testing/db_entities.d.ts +11 -1
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +13 -1
- package/dist/testing/integration.d.ts +35 -21
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +231 -291
- package/dist/testing/integration_helpers.d.ts +16 -6
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +7 -7
- package/dist/testing/mock_fs.d.ts.map +1 -1
- package/dist/testing/mock_fs.js +0 -2
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +9 -0
- package/dist/testing/role_grant_helpers.d.ts +31 -0
- package/dist/testing/role_grant_helpers.d.ts.map +1 -0
- package/dist/testing/role_grant_helpers.js +46 -0
- package/dist/testing/round_trip.d.ts +21 -16
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +65 -86
- package/dist/testing/rpc_round_trip.d.ts +24 -21
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +91 -104
- package/dist/testing/schema_introspect.d.ts +106 -0
- package/dist/testing/schema_introspect.d.ts.map +1 -0
- package/dist/testing/schema_introspect.js +123 -0
- package/dist/testing/schema_parity.d.ts +144 -0
- package/dist/testing/schema_parity.d.ts.map +1 -0
- package/dist/testing/schema_parity.js +233 -0
- package/dist/testing/standard.d.ts +57 -25
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +62 -5
- package/dist/testing/stubs.d.ts +11 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +24 -21
- package/dist/testing/transports/surface_source.d.ts +51 -0
- package/dist/testing/transports/surface_source.d.ts.map +1 -0
- package/dist/testing/transports/surface_source.js +19 -0
- package/package.json +4 -4
|
@@ -3,8 +3,18 @@ import './assert_dev_env.js';
|
|
|
3
3
|
* Composable audit log completeness test suite.
|
|
4
4
|
*
|
|
5
5
|
* Verifies that every auth mutation route produces the expected audit log
|
|
6
|
-
* event. Uses the real middleware stack and database
|
|
7
|
-
*
|
|
6
|
+
* event. Uses the real middleware stack and database, then **reads back
|
|
7
|
+
* through the `audit_log_list` RPC** — the production observation path the
|
|
8
|
+
* admin UI consumes. This is intentional end-to-end coverage: emit →
|
|
9
|
+
* persist → query → wire response, all in one round-trip.
|
|
10
|
+
*
|
|
11
|
+
* The trade is a deliberate transport coupling: a regression in
|
|
12
|
+
* `audit_log_list_action_spec`'s auth or response shape can surface here as
|
|
13
|
+
* a secondary failure. `describe_rpc_round_trip_tests` covers that RPC
|
|
14
|
+
* directly, so primary breakages localize there first. For *unit-level*
|
|
15
|
+
* "did the handler emit?" assertions without the persistence path, use
|
|
16
|
+
* `create_recording_audit_emitter` from `audit_drift_guard.ts` — that
|
|
17
|
+
* captures emits before they hit DB or transport.
|
|
8
18
|
*
|
|
9
19
|
* Bootstrap is excluded because it requires filesystem token state that
|
|
10
20
|
* `create_test_app` does not provide. Bootstrap audit logging is tested
|
|
@@ -13,21 +23,48 @@ import './assert_dev_env.js';
|
|
|
13
23
|
* @module
|
|
14
24
|
*/
|
|
15
25
|
import { describe, test, assert } from 'vitest';
|
|
16
|
-
import {
|
|
17
|
-
import { AUDIT_EVENT_TYPES } from '../auth/audit_log_schema.js';
|
|
18
|
-
import {
|
|
19
|
-
import { create_test_app, } from './app_server.js';
|
|
20
|
-
import { create_pglite_factory, create_describe_db, auth_integration_truncate_tables, } from './db.js';
|
|
26
|
+
import { ROLE_ADMIN } from '../auth/role_schema.js';
|
|
27
|
+
import { AUDIT_EVENT_TYPES, } from '../auth/audit_log_schema.js';
|
|
28
|
+
import {} from './app_server.js';
|
|
21
29
|
import { find_auth_route } from './integration_helpers.js';
|
|
22
|
-
import { run_migrations } from '../db/migrate.js';
|
|
23
|
-
import { query_accept_offer } from '../auth/role_grant_offer_queries.js';
|
|
24
30
|
import { rpc_call_for_spec, require_rpc_endpoint_path, resolve_rpc_endpoints_for_setup, } from './rpc_helpers.js';
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
31
|
+
import { role_grant_offer_and_accept } from './role_grant_helpers.js';
|
|
32
|
+
import { role_grant_offer_accept_action_spec, role_grant_offer_create_action_spec, role_grant_revoke_action_spec, } from '../auth/role_grant_offer_action_specs.js';
|
|
33
|
+
import { admin_session_revoke_all_action_spec, admin_token_revoke_all_action_spec, app_settings_update_action_spec, audit_log_list_action_spec, AUDIT_LOG_LIST_LIMIT_MAX, invite_create_action_spec, invite_delete_action_spec, } from '../auth/admin_action_specs.js';
|
|
27
34
|
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
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Mint a dedicated admin account whose sole job is to read the audit log
|
|
37
|
+
* via RPC. Decoupling the *observer* from the *subject* keeps the helper
|
|
38
|
+
* shape uniform across every audit-touching test — even ones whose
|
|
39
|
+
* mutation revokes the bootstrapped admin's credentials (logout,
|
|
40
|
+
* session_revoke, password_change). The observer has no role-grants the
|
|
41
|
+
* test exercises and no credentials the test mutates, so it survives
|
|
42
|
+
* every flow.
|
|
43
|
+
*/
|
|
44
|
+
const create_admin_observer = (fixture) => fixture.create_account({ username: 'audit_observer', roles: [ROLE_ADMIN] });
|
|
45
|
+
/**
|
|
46
|
+
* List audit log events via the `audit_log_list` RPC. Replaces the previous
|
|
47
|
+
* raw `SELECT FROM audit_log` query — the RPC is the documented contract and
|
|
48
|
+
* the same path the admin UI consumes. The RPC orders newest-first
|
|
49
|
+
* (`ORDER BY seq DESC`); assertions use `.some()` / `.find()` so ordering is
|
|
50
|
+
* invisible to test logic. Default `limit: AUDIT_LOG_LIST_LIMIT_MAX` (200)
|
|
51
|
+
* future-proofs against tests with more emissions; per-test
|
|
52
|
+
* `auth_integration_truncate_tables` keeps the table empty between cases.
|
|
53
|
+
*
|
|
54
|
+
* `observer` is a dedicated admin account (see {@link create_admin_observer})
|
|
55
|
+
* — its credentials are never the subject of the mutation under test, so the
|
|
56
|
+
* read works uniformly across every flow including session-revoking ones.
|
|
57
|
+
*/
|
|
58
|
+
const list_audit_events = async (app, rpc_path, observer, params = {}) => {
|
|
59
|
+
const res = await rpc_call_for_spec({
|
|
60
|
+
app,
|
|
61
|
+
path: rpc_path,
|
|
62
|
+
spec: audit_log_list_action_spec,
|
|
63
|
+
params: { limit: AUDIT_LOG_LIST_LIMIT_MAX, ...params },
|
|
64
|
+
headers: observer.create_session_headers(),
|
|
65
|
+
});
|
|
66
|
+
assert.ok(res.ok, `audit_log_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
67
|
+
return res.result.events;
|
|
31
68
|
};
|
|
32
69
|
/** Assert that audit events contain the expected event type. */
|
|
33
70
|
const assert_has_event = (events, expected, context) => {
|
|
@@ -44,15 +81,6 @@ const assert_event_credential_type = (events, expected, credential_type, context
|
|
|
44
81
|
const recorded = (match.metadata ?? {}).credential_type;
|
|
45
82
|
assert.strictEqual(recorded, credential_type, `Expected '${expected}' audit metadata.credential_type === '${credential_type}' after ${context} (got ${JSON.stringify(recorded)})`);
|
|
46
83
|
};
|
|
47
|
-
/** Build CreateTestAppOptions with admin+keeper roles. */
|
|
48
|
-
const build_options = (options, db) => ({
|
|
49
|
-
session_options: options.session_options,
|
|
50
|
-
create_route_specs: options.create_route_specs,
|
|
51
|
-
db,
|
|
52
|
-
roles: [ROLE_KEEPER, ROLE_ADMIN],
|
|
53
|
-
rpc_endpoints: options.rpc_endpoints,
|
|
54
|
-
app_options: options.app_options,
|
|
55
|
-
});
|
|
56
84
|
/** Headers for unauthenticated JSON requests (login, signup). */
|
|
57
85
|
const UNAUTHENTICATED_JSON_HEADERS = {
|
|
58
86
|
host: 'localhost',
|
|
@@ -60,7 +88,7 @@ const UNAUTHENTICATED_JSON_HEADERS = {
|
|
|
60
88
|
'content-type': 'application/json',
|
|
61
89
|
};
|
|
62
90
|
/** Standard request headers for session-authenticated JSON requests. */
|
|
63
|
-
const json_session_headers = (
|
|
91
|
+
const json_session_headers = (fixture, extra) => fixture.create_session_headers({
|
|
64
92
|
'content-type': 'application/json',
|
|
65
93
|
...extra,
|
|
66
94
|
});
|
|
@@ -69,7 +97,8 @@ const json_session_headers = (test_app, extra) => test_app.create_session_header
|
|
|
69
97
|
*
|
|
70
98
|
* Verifies that every auth mutation route produces the correct audit log
|
|
71
99
|
* event type. Exercises routes via HTTP requests against a real PGlite
|
|
72
|
-
* database, then
|
|
100
|
+
* database, then reads events back through the `audit_log_list` RPC
|
|
101
|
+
* (the production observation path the admin UI consumes).
|
|
73
102
|
*
|
|
74
103
|
* @throws Error at setup time when `options.rpc_endpoints` is empty — the
|
|
75
104
|
* mutation-audit tests drive role_grant flow, session/token revoke-all, and
|
|
@@ -77,294 +106,301 @@ const json_session_headers = (test_app, extra) => test_app.create_session_header
|
|
|
77
106
|
* `require_rpc_endpoint_path`.
|
|
78
107
|
*/
|
|
79
108
|
export const describe_audit_completeness_tests = (options) => {
|
|
109
|
+
if (options.surface_source.kind !== 'inline') {
|
|
110
|
+
throw new Error("describe_audit_completeness_tests requires surface_source.kind === 'inline' — " +
|
|
111
|
+
'the cross-process snapshot variant lands with the spawned-backend transport');
|
|
112
|
+
}
|
|
113
|
+
const route_specs = options.surface_source.spec.route_specs;
|
|
80
114
|
// Hard-fail early so consumers see a clear setup error instead of a
|
|
81
|
-
// confusing test failure when `rpc_endpoints` is missing.
|
|
82
|
-
// callers are resolved with a stub ctx purely to extract the endpoint
|
|
83
|
-
// path; real handlers run per-test via the top-level `rpc_endpoints` slot on `CreateTestAppOptions`.
|
|
115
|
+
// confusing test failure when `rpc_endpoints` is missing.
|
|
84
116
|
const rpc_endpoints_for_setup = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
|
|
85
117
|
const rpc_path = require_rpc_endpoint_path(rpc_endpoints_for_setup);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
};
|
|
89
|
-
const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
|
|
90
|
-
const describe_db = create_describe_db(factories, auth_integration_truncate_tables);
|
|
91
|
-
describe_db('audit_log_completeness', (get_db) => {
|
|
118
|
+
void options.capabilities;
|
|
119
|
+
describe('audit_log_completeness', () => {
|
|
92
120
|
// --- Account routes ---
|
|
93
121
|
describe('account mutation audit events', () => {
|
|
94
122
|
test('login success produces login event', async () => {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
123
|
+
const fixture = await options.setup_test();
|
|
124
|
+
const observer = await create_admin_observer(fixture);
|
|
125
|
+
const login_route = find_auth_route(route_specs, '/login', 'POST');
|
|
97
126
|
assert.ok(login_route, 'Expected POST /login route');
|
|
98
|
-
const res = await
|
|
127
|
+
const res = await fixture.transport(login_route.path, {
|
|
99
128
|
method: 'POST',
|
|
100
129
|
headers: UNAUTHENTICATED_JSON_HEADERS,
|
|
101
130
|
body: JSON.stringify({
|
|
102
|
-
username:
|
|
131
|
+
username: fixture.account.username,
|
|
103
132
|
password: 'test-password-123',
|
|
104
133
|
}),
|
|
105
134
|
});
|
|
106
135
|
assert.strictEqual(res.status, 200);
|
|
107
|
-
const events = await
|
|
136
|
+
const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
108
137
|
assert_has_event(events, 'login', 'POST /login (success)');
|
|
109
138
|
});
|
|
110
139
|
test('login failure produces login event with failure outcome', async () => {
|
|
111
|
-
const
|
|
112
|
-
const
|
|
140
|
+
const fixture = await options.setup_test();
|
|
141
|
+
const observer = await create_admin_observer(fixture);
|
|
142
|
+
const login_route = find_auth_route(route_specs, '/login', 'POST');
|
|
113
143
|
assert.ok(login_route, 'Expected POST /login route');
|
|
114
|
-
const res = await
|
|
144
|
+
const res = await fixture.transport(login_route.path, {
|
|
115
145
|
method: 'POST',
|
|
116
146
|
headers: UNAUTHENTICATED_JSON_HEADERS,
|
|
117
147
|
body: JSON.stringify({
|
|
118
|
-
username:
|
|
148
|
+
username: fixture.account.username,
|
|
119
149
|
password: 'wrong-password',
|
|
120
150
|
}),
|
|
121
151
|
});
|
|
122
152
|
assert.strictEqual(res.status, 401);
|
|
123
|
-
const events = await
|
|
153
|
+
const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
124
154
|
assert_has_event(events, 'login', 'POST /login (failure)');
|
|
125
155
|
});
|
|
126
156
|
test('logout produces logout event', async () => {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
157
|
+
const fixture = await options.setup_test();
|
|
158
|
+
const observer = await create_admin_observer(fixture);
|
|
159
|
+
const logout_route = find_auth_route(route_specs, '/logout', 'POST');
|
|
129
160
|
assert.ok(logout_route, 'Expected POST /logout route');
|
|
130
|
-
const res = await
|
|
161
|
+
const res = await fixture.transport(logout_route.path, {
|
|
131
162
|
method: 'POST',
|
|
132
|
-
headers:
|
|
163
|
+
headers: fixture.create_session_headers(),
|
|
133
164
|
});
|
|
134
165
|
assert.strictEqual(res.status, 200);
|
|
135
|
-
const events = await
|
|
166
|
+
const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
136
167
|
assert_has_event(events, 'logout', 'POST /logout');
|
|
137
168
|
});
|
|
138
169
|
test('token create produces token_create event', async () => {
|
|
139
|
-
const
|
|
170
|
+
const fixture = await options.setup_test();
|
|
171
|
+
const observer = await create_admin_observer(fixture);
|
|
140
172
|
const res = await rpc_call_for_spec({
|
|
141
|
-
app:
|
|
173
|
+
app: { request: fixture.transport },
|
|
142
174
|
path: rpc_path,
|
|
143
175
|
spec: account_token_create_action_spec,
|
|
144
176
|
params: { name: 'audit-test' },
|
|
145
|
-
headers:
|
|
177
|
+
headers: fixture.create_session_headers(),
|
|
146
178
|
});
|
|
147
179
|
assert.ok(res.ok, `account_token_create failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
148
|
-
const events = await
|
|
180
|
+
const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
149
181
|
assert_has_event(events, 'token_create', 'account_token_create RPC');
|
|
150
182
|
assert_event_credential_type(events, 'token_create', 'session', 'account_token_create RPC');
|
|
151
183
|
});
|
|
152
184
|
test('token revoke produces token_revoke event', async () => {
|
|
153
|
-
const
|
|
185
|
+
const fixture = await options.setup_test();
|
|
186
|
+
const observer = await create_admin_observer(fixture);
|
|
154
187
|
// get a token ID to revoke
|
|
155
188
|
const list_res = await rpc_call_for_spec({
|
|
156
|
-
app:
|
|
189
|
+
app: { request: fixture.transport },
|
|
157
190
|
path: rpc_path,
|
|
158
191
|
spec: account_token_list_action_spec,
|
|
159
192
|
params: undefined,
|
|
160
|
-
headers:
|
|
193
|
+
headers: fixture.create_session_headers(),
|
|
161
194
|
});
|
|
162
195
|
assert.ok(list_res.ok, 'account_token_list should succeed');
|
|
163
196
|
const { tokens } = list_res.result;
|
|
164
197
|
assert.ok(tokens.length > 0, 'Expected at least one token');
|
|
165
198
|
const res = await rpc_call_for_spec({
|
|
166
|
-
app:
|
|
199
|
+
app: { request: fixture.transport },
|
|
167
200
|
path: rpc_path,
|
|
168
201
|
spec: account_token_revoke_action_spec,
|
|
169
202
|
params: { token_id: tokens[0].id },
|
|
170
|
-
headers:
|
|
203
|
+
headers: fixture.create_session_headers(),
|
|
171
204
|
});
|
|
172
205
|
assert.ok(res.ok, `account_token_revoke failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
173
|
-
const events = await
|
|
206
|
+
const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
174
207
|
assert_has_event(events, 'token_revoke', 'account_token_revoke RPC');
|
|
175
208
|
assert_event_credential_type(events, 'token_revoke', 'session', 'account_token_revoke RPC');
|
|
176
209
|
});
|
|
177
210
|
test('session revoke produces session_revoke event', async () => {
|
|
178
|
-
const
|
|
211
|
+
const fixture = await options.setup_test();
|
|
212
|
+
const observer = await create_admin_observer(fixture);
|
|
179
213
|
// login to create a second session we can revoke
|
|
180
|
-
const login_route = find_auth_route(
|
|
214
|
+
const login_route = find_auth_route(route_specs, '/login', 'POST');
|
|
181
215
|
assert.ok(login_route, 'Expected POST /login route');
|
|
182
|
-
await
|
|
216
|
+
await fixture.transport(login_route.path, {
|
|
183
217
|
method: 'POST',
|
|
184
218
|
headers: UNAUTHENTICATED_JSON_HEADERS,
|
|
185
219
|
body: JSON.stringify({
|
|
186
|
-
username:
|
|
220
|
+
username: fixture.account.username,
|
|
187
221
|
password: 'test-password-123',
|
|
188
222
|
}),
|
|
189
223
|
});
|
|
190
|
-
// get session IDs (newest first
|
|
224
|
+
// get session IDs (newest first — `account_session_list` orders DESC
|
|
225
|
+
// by `created_at`, so [0] is the just-logged-in session and [1] is
|
|
226
|
+
// the bootstrap session driving the RPC call).
|
|
191
227
|
const list_res = await rpc_call_for_spec({
|
|
192
|
-
app:
|
|
228
|
+
app: { request: fixture.transport },
|
|
193
229
|
path: rpc_path,
|
|
194
230
|
spec: account_session_list_action_spec,
|
|
195
231
|
params: undefined,
|
|
196
|
-
headers:
|
|
232
|
+
headers: fixture.create_session_headers(),
|
|
197
233
|
});
|
|
198
234
|
assert.ok(list_res.ok, 'account_session_list should succeed');
|
|
199
235
|
const { sessions } = list_res.result;
|
|
200
236
|
assert.ok(sessions.length >= 2, 'Expected at least 2 sessions');
|
|
201
|
-
// revoke the
|
|
237
|
+
// revoke the newest session — not the bootstrap one driving auth.
|
|
202
238
|
const res = await rpc_call_for_spec({
|
|
203
|
-
app:
|
|
239
|
+
app: { request: fixture.transport },
|
|
204
240
|
path: rpc_path,
|
|
205
241
|
spec: account_session_revoke_action_spec,
|
|
206
|
-
params: { session_id: sessions[
|
|
207
|
-
headers:
|
|
242
|
+
params: { session_id: sessions[0].id },
|
|
243
|
+
headers: fixture.create_session_headers(),
|
|
208
244
|
});
|
|
209
245
|
assert.ok(res.ok, `account_session_revoke failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
210
|
-
const events = await
|
|
246
|
+
const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
211
247
|
assert_has_event(events, 'session_revoke', 'account_session_revoke RPC');
|
|
212
248
|
assert_event_credential_type(events, 'session_revoke', 'session', 'account_session_revoke RPC');
|
|
213
249
|
});
|
|
214
250
|
test('session revoke-all produces session_revoke_all event', async () => {
|
|
215
|
-
const
|
|
251
|
+
const fixture = await options.setup_test();
|
|
252
|
+
const observer = await create_admin_observer(fixture);
|
|
216
253
|
const res = await rpc_call_for_spec({
|
|
217
|
-
app:
|
|
254
|
+
app: { request: fixture.transport },
|
|
218
255
|
path: rpc_path,
|
|
219
256
|
spec: account_session_revoke_all_action_spec,
|
|
220
257
|
params: undefined,
|
|
221
|
-
headers:
|
|
258
|
+
headers: fixture.create_session_headers(),
|
|
222
259
|
});
|
|
223
260
|
assert.ok(res.ok, `account_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
224
|
-
const events = await
|
|
261
|
+
const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
225
262
|
assert_has_event(events, 'session_revoke_all', 'account_session_revoke_all RPC');
|
|
226
263
|
assert_event_credential_type(events, 'session_revoke_all', 'session', 'account_session_revoke_all RPC');
|
|
227
264
|
});
|
|
228
265
|
test('password change produces password_change event', async () => {
|
|
229
|
-
const
|
|
230
|
-
const
|
|
266
|
+
const fixture = await options.setup_test();
|
|
267
|
+
const observer = await create_admin_observer(fixture);
|
|
268
|
+
const route = find_auth_route(route_specs, '/password', 'POST');
|
|
231
269
|
assert.ok(route, 'Expected POST /password route');
|
|
232
|
-
const res = await
|
|
270
|
+
const res = await fixture.transport(route.path, {
|
|
233
271
|
method: 'POST',
|
|
234
|
-
headers: json_session_headers(
|
|
272
|
+
headers: json_session_headers(fixture),
|
|
235
273
|
body: JSON.stringify({
|
|
236
274
|
current_password: 'test-password-123',
|
|
237
275
|
new_password: 'new-password-456',
|
|
238
276
|
}),
|
|
239
277
|
});
|
|
240
278
|
assert.strictEqual(res.status, 200);
|
|
241
|
-
const events = await
|
|
279
|
+
const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
242
280
|
assert_has_event(events, 'password_change', 'POST /password');
|
|
243
281
|
assert_event_credential_type(events, 'password_change', 'session', 'POST /password');
|
|
244
282
|
});
|
|
245
283
|
});
|
|
246
284
|
// --- Admin routes ---
|
|
247
285
|
describe('admin mutation audit events', () => {
|
|
248
|
-
test('admin offer (RPC) + accept produces role_grant_offer_create and role_grant_create events', async () => {
|
|
249
|
-
const
|
|
250
|
-
const
|
|
286
|
+
test('admin offer (RPC) + accept (RPC) produces role_grant_offer_create, role_grant_offer_accept, and role_grant_create events', async () => {
|
|
287
|
+
const fixture = await options.setup_test();
|
|
288
|
+
const observer = await create_admin_observer(fixture);
|
|
289
|
+
const target = await fixture.create_account({ username: 'audit_target' });
|
|
251
290
|
const offer_res = await rpc_call_for_spec({
|
|
252
|
-
app:
|
|
291
|
+
app: { request: fixture.transport },
|
|
253
292
|
path: rpc_path,
|
|
254
293
|
spec: role_grant_offer_create_action_spec,
|
|
255
294
|
params: { to_account_id: target.account.id, role: ROLE_ADMIN },
|
|
256
|
-
headers:
|
|
295
|
+
headers: fixture.create_session_headers(),
|
|
257
296
|
});
|
|
258
297
|
assert.ok(offer_res.ok, `role_grant_offer_create failed: ${offer_res.ok ? '' : JSON.stringify(offer_res.error)}`);
|
|
259
298
|
const { offer } = offer_res.result;
|
|
260
299
|
// Admin offer emits `role_grant_offer_create` only — the role_grant doesn't
|
|
261
|
-
// exist yet. Drive the accept to confirm `
|
|
262
|
-
// downstream consent transition.
|
|
263
|
-
const events_after_offer = await
|
|
300
|
+
// exist yet. Drive the accept to confirm `role_grant_offer_accept` and
|
|
301
|
+
// `role_grant_create` both fire on the downstream consent transition.
|
|
302
|
+
const events_after_offer = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
264
303
|
assert_has_event(events_after_offer, 'role_grant_offer_create', 'role_grant_offer_create RPC');
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
});
|
|
304
|
+
const accept_res = await rpc_call_for_spec({
|
|
305
|
+
app: { request: fixture.transport },
|
|
306
|
+
path: rpc_path,
|
|
307
|
+
spec: role_grant_offer_accept_action_spec,
|
|
308
|
+
params: { offer_id: offer.id },
|
|
309
|
+
headers: target.create_session_headers(),
|
|
272
310
|
});
|
|
273
|
-
|
|
274
|
-
|
|
311
|
+
assert.ok(accept_res.ok, `role_grant_offer_accept failed: ${accept_res.ok ? '' : JSON.stringify(accept_res.error)}`);
|
|
312
|
+
const events_after_accept = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
313
|
+
assert_has_event(events_after_accept, 'role_grant_offer_accept', 'offer accept RPC');
|
|
314
|
+
assert_has_event(events_after_accept, 'role_grant_create', 'offer accept RPC');
|
|
275
315
|
});
|
|
276
316
|
test('role_grant revoke (RPC) produces role_grant_revoke event with both target columns', async () => {
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const accept_result = await get_db().transaction(async (tx) => {
|
|
290
|
-
return query_accept_offer({ db: tx }, {
|
|
291
|
-
offer_id: offer.id,
|
|
292
|
-
to_account_id: target.account.id,
|
|
293
|
-
actor_id: target.actor.id,
|
|
294
|
-
ip: null,
|
|
295
|
-
});
|
|
317
|
+
const fixture = await options.setup_test();
|
|
318
|
+
const observer = await create_admin_observer(fixture);
|
|
319
|
+
const target = await fixture.create_account({ username: 'audit_revoke_target' });
|
|
320
|
+
// Offer + accept to materialize a role_grant we can revoke. The
|
|
321
|
+
// consent path itself is covered by the `offer + accept` test above;
|
|
322
|
+
// here we only need the role_grant to exist.
|
|
323
|
+
const { role_grant_id } = await role_grant_offer_and_accept({
|
|
324
|
+
app: { request: fixture.transport },
|
|
325
|
+
rpc_path,
|
|
326
|
+
grantor: fixture,
|
|
327
|
+
recipient: target,
|
|
328
|
+
role: ROLE_ADMIN,
|
|
296
329
|
});
|
|
297
330
|
// Revoke via RPC.
|
|
298
331
|
const revoke_res = await rpc_call_for_spec({
|
|
299
|
-
app:
|
|
332
|
+
app: { request: fixture.transport },
|
|
300
333
|
path: rpc_path,
|
|
301
334
|
spec: role_grant_revoke_action_spec,
|
|
302
|
-
params: { actor_id: target.actor.id, role_grant_id
|
|
303
|
-
headers:
|
|
335
|
+
params: { actor_id: target.actor.id, role_grant_id },
|
|
336
|
+
headers: fixture.create_session_headers(),
|
|
304
337
|
});
|
|
305
338
|
assert.ok(revoke_res.ok, `role_grant_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
306
|
-
const events = await
|
|
339
|
+
const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
307
340
|
assert_has_event(events, 'role_grant_revoke', 'role_grant_revoke RPC');
|
|
308
341
|
// Audit envelope must populate both target columns —
|
|
309
342
|
// `role_grant_revoke` is the canonical actor-bound-subject event.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
assert.strictEqual(
|
|
314
|
-
assert.strictEqual(
|
|
343
|
+
// RPC orders newest-first, so `.find` picks up the just-emitted row.
|
|
344
|
+
const revoke = events.find((e) => e.event_type === 'role_grant_revoke');
|
|
345
|
+
assert.ok(revoke, 'Expected role_grant_revoke audit event');
|
|
346
|
+
assert.strictEqual(revoke.target_account_id, target.account.id);
|
|
347
|
+
assert.strictEqual(revoke.target_actor_id, target.actor.id);
|
|
315
348
|
});
|
|
316
349
|
test('admin session revoke-all produces session_revoke_all event', async () => {
|
|
317
|
-
const
|
|
318
|
-
const
|
|
350
|
+
const fixture = await options.setup_test();
|
|
351
|
+
const observer = await create_admin_observer(fixture);
|
|
352
|
+
const target = await fixture.create_account({ username: 'audit_sessions_target' });
|
|
319
353
|
const res = await rpc_call_for_spec({
|
|
320
|
-
app:
|
|
354
|
+
app: { request: fixture.transport },
|
|
321
355
|
path: rpc_path,
|
|
322
356
|
spec: admin_session_revoke_all_action_spec,
|
|
323
357
|
params: { account_id: target.account.id },
|
|
324
|
-
headers:
|
|
358
|
+
headers: fixture.create_session_headers(),
|
|
325
359
|
});
|
|
326
360
|
assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
327
|
-
const events = await
|
|
361
|
+
const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
328
362
|
// admin session revoke-all also produces session_revoke_all
|
|
329
363
|
assert_has_event(events, 'session_revoke_all', 'admin_session_revoke_all RPC');
|
|
330
364
|
});
|
|
331
365
|
test('admin token revoke-all produces token_revoke_all event', async () => {
|
|
332
|
-
const
|
|
333
|
-
const
|
|
366
|
+
const fixture = await options.setup_test();
|
|
367
|
+
const observer = await create_admin_observer(fixture);
|
|
368
|
+
const target = await fixture.create_account({ username: 'audit_tokens_target' });
|
|
334
369
|
const res = await rpc_call_for_spec({
|
|
335
|
-
app:
|
|
370
|
+
app: { request: fixture.transport },
|
|
336
371
|
path: rpc_path,
|
|
337
372
|
spec: admin_token_revoke_all_action_spec,
|
|
338
373
|
params: { account_id: target.account.id },
|
|
339
|
-
headers:
|
|
374
|
+
headers: fixture.create_session_headers(),
|
|
340
375
|
});
|
|
341
376
|
assert.ok(res.ok, `admin_token_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
342
|
-
const events = await
|
|
377
|
+
const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
343
378
|
assert_has_event(events, 'token_revoke_all', 'admin_token_revoke_all RPC');
|
|
344
379
|
});
|
|
345
380
|
});
|
|
346
381
|
// --- Invite RPC actions ---
|
|
347
382
|
describe('invite mutation audit events', () => {
|
|
348
383
|
test('invite create and delete produce audit events', async () => {
|
|
349
|
-
const
|
|
384
|
+
const fixture = await options.setup_test();
|
|
385
|
+
const observer = await create_admin_observer(fixture);
|
|
350
386
|
const create_res = await rpc_call_for_spec({
|
|
351
|
-
app:
|
|
387
|
+
app: { request: fixture.transport },
|
|
352
388
|
path: rpc_path,
|
|
353
389
|
spec: invite_create_action_spec,
|
|
354
390
|
params: { username: 'invited_user' },
|
|
355
|
-
headers:
|
|
391
|
+
headers: fixture.create_session_headers(),
|
|
356
392
|
});
|
|
357
393
|
assert.ok(create_res.ok, `invite_create failed: ${create_res.ok ? '' : JSON.stringify(create_res.error)}`);
|
|
358
394
|
const { invite } = create_res.result;
|
|
359
395
|
const delete_res = await rpc_call_for_spec({
|
|
360
|
-
app:
|
|
396
|
+
app: { request: fixture.transport },
|
|
361
397
|
path: rpc_path,
|
|
362
398
|
spec: invite_delete_action_spec,
|
|
363
399
|
params: { invite_id: invite.id },
|
|
364
|
-
headers:
|
|
400
|
+
headers: fixture.create_session_headers(),
|
|
365
401
|
});
|
|
366
402
|
assert.ok(delete_res.ok, `invite_delete failed: ${delete_res.ok ? '' : JSON.stringify(delete_res.error)}`);
|
|
367
|
-
const events = await
|
|
403
|
+
const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
368
404
|
assert_has_event(events, 'invite_create', 'invite_create RPC');
|
|
369
405
|
assert_has_event(events, 'invite_delete', 'invite_delete RPC');
|
|
370
406
|
});
|
|
@@ -372,36 +408,42 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
372
408
|
// --- App settings RPC action ---
|
|
373
409
|
describe('app settings mutation audit events', () => {
|
|
374
410
|
test('settings update produces app_settings_update event', async () => {
|
|
375
|
-
const
|
|
411
|
+
const fixture = await options.setup_test();
|
|
412
|
+
const observer = await create_admin_observer(fixture);
|
|
376
413
|
const res = await rpc_call_for_spec({
|
|
377
|
-
app:
|
|
414
|
+
app: { request: fixture.transport },
|
|
378
415
|
path: rpc_path,
|
|
379
416
|
spec: app_settings_update_action_spec,
|
|
380
417
|
params: { open_signup: true },
|
|
381
|
-
headers:
|
|
418
|
+
headers: fixture.create_session_headers(),
|
|
382
419
|
});
|
|
383
420
|
assert.ok(res.ok, `app_settings_update failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
384
|
-
const events = await
|
|
421
|
+
const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
385
422
|
assert_has_event(events, 'app_settings_update', 'app_settings_update RPC');
|
|
386
423
|
});
|
|
387
424
|
});
|
|
388
425
|
// --- Signup route ---
|
|
389
426
|
describe('signup audit events', () => {
|
|
390
427
|
test('signup produces signup event', async () => {
|
|
391
|
-
const
|
|
428
|
+
const fixture = await options.setup_test();
|
|
429
|
+
// signup is optional — consumers that don't wire `POST /signup` (e.g.
|
|
430
|
+
// admin-only apps) skip this audit check; signup completeness for
|
|
431
|
+
// surfaces that DO wire it is still asserted by COVERED_EVENT_TYPES
|
|
432
|
+
// below. Mirrors `integration.ts`'s signup-block presence-gate.
|
|
433
|
+
const signup_route = find_auth_route(route_specs, '/signup', 'POST');
|
|
434
|
+
if (!signup_route)
|
|
435
|
+
return;
|
|
436
|
+
const observer = await create_admin_observer(fixture);
|
|
392
437
|
// enable open signup via RPC
|
|
393
438
|
const settings_res = await rpc_call_for_spec({
|
|
394
|
-
app:
|
|
439
|
+
app: { request: fixture.transport },
|
|
395
440
|
path: rpc_path,
|
|
396
441
|
spec: app_settings_update_action_spec,
|
|
397
442
|
params: { open_signup: true },
|
|
398
|
-
headers:
|
|
443
|
+
headers: fixture.create_session_headers(),
|
|
399
444
|
});
|
|
400
445
|
assert.ok(settings_res.ok, `app_settings_update failed: ${settings_res.ok ? '' : JSON.stringify(settings_res.error)}`);
|
|
401
|
-
|
|
402
|
-
const signup_route = find_auth_route(test_app.route_specs, '/signup', 'POST');
|
|
403
|
-
assert.ok(signup_route, 'Expected POST /signup route');
|
|
404
|
-
const res = await test_app.app.request(signup_route.path, {
|
|
446
|
+
const res = await fixture.transport(signup_route.path, {
|
|
405
447
|
method: 'POST',
|
|
406
448
|
headers: UNAUTHENTICATED_JSON_HEADERS,
|
|
407
449
|
body: JSON.stringify({
|
|
@@ -410,7 +452,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
410
452
|
}),
|
|
411
453
|
});
|
|
412
454
|
assert.strictEqual(res.status, 200);
|
|
413
|
-
const events = await
|
|
455
|
+
const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
|
|
414
456
|
assert_has_event(events, 'signup', 'POST /signup');
|
|
415
457
|
});
|
|
416
458
|
});
|
|
@@ -431,6 +473,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
431
473
|
'token_revoke',
|
|
432
474
|
'token_revoke_all',
|
|
433
475
|
'role_grant_offer_create',
|
|
476
|
+
'role_grant_offer_accept',
|
|
434
477
|
'role_grant_create',
|
|
435
478
|
'role_grant_revoke',
|
|
436
479
|
'invite_create',
|
|
@@ -440,8 +483,9 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
440
483
|
/** Event types excluded with justification. */
|
|
441
484
|
const EXCLUDED_EVENT_TYPES = new Set([
|
|
442
485
|
'bootstrap', // requires filesystem token — tested in bootstrap_account.db.test.ts
|
|
443
|
-
// The remaining `role_grant_offer_*` events fire only via
|
|
444
|
-
//
|
|
486
|
+
// The remaining `role_grant_offer_*` events fire only via terminal
|
|
487
|
+
// transitions (decline, retract) or downstream effects (supersede on
|
|
488
|
+
// accept of a sibling, or as a fan-out of `role_grant_revoke`). Direct
|
|
445
489
|
// coverage lives in `role_grant_offer_queries.db.test.ts`,
|
|
446
490
|
// `role_grant_offer_actions.db.test.ts`,
|
|
447
491
|
// `role_grant_offer_actions.notifications.db.test.ts`, and
|
|
@@ -449,7 +493,6 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
449
493
|
// `role_grant_offer_expire` fires from the cleanup sweep
|
|
450
494
|
// (`cleanup_expired_role_grant_offers` in `auth/cleanup.ts`) —
|
|
451
495
|
// covered in `cleanup.db.test.ts`.
|
|
452
|
-
'role_grant_offer_accept',
|
|
453
496
|
'role_grant_offer_decline',
|
|
454
497
|
'role_grant_offer_retract',
|
|
455
498
|
'role_grant_offer_expire',
|