@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.
Files changed (111) hide show
  1. package/dist/actions/CLAUDE.md +513 -928
  2. package/dist/actions/broadcast_api.d.ts +1 -1
  3. package/dist/actions/broadcast_api.js +1 -1
  4. package/dist/actions/cancel.d.ts +2 -2
  5. package/dist/actions/cancel.js +3 -3
  6. package/dist/actions/connection_closer.d.ts +1 -4
  7. package/dist/actions/connection_closer.d.ts.map +1 -1
  8. package/dist/actions/connection_closer.js +1 -4
  9. package/dist/actions/register_action_ws.d.ts +2 -2
  10. package/dist/actions/register_ws_endpoint.d.ts +1 -1
  11. package/dist/actions/transports_ws_auth_guard.d.ts +1 -2
  12. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  13. package/dist/actions/transports_ws_auth_guard.js +1 -2
  14. package/dist/auth/CLAUDE.md +591 -1871
  15. package/dist/auth/account_schema.d.ts +1 -1
  16. package/dist/auth/account_schema.d.ts.map +1 -1
  17. package/dist/auth/api_token_queries.js +1 -1
  18. package/dist/auth/audit_log_ddl.d.ts +1 -1
  19. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  20. package/dist/auth/audit_log_ddl.js +1 -1
  21. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  22. package/dist/auth/bootstrap_account.js +1 -5
  23. package/dist/auth/bootstrap_routes.d.ts +7 -1
  24. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  25. package/dist/auth/bootstrap_routes.js +15 -11
  26. package/dist/auth/keyring.d.ts +6 -6
  27. package/dist/auth/keyring.js +8 -8
  28. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  29. package/dist/auth/role_grant_offer_actions.js +4 -2
  30. package/dist/db/create_db.d.ts.map +1 -1
  31. package/dist/db/create_db.js +13 -0
  32. package/dist/dev/setup.d.ts +2 -2
  33. package/dist/dev/setup.js +3 -3
  34. package/dist/http/CLAUDE.md +224 -498
  35. package/dist/http/error_schemas.d.ts +0 -4
  36. package/dist/http/error_schemas.d.ts.map +1 -1
  37. package/dist/http/error_schemas.js +0 -4
  38. package/dist/http/ip_canonical.d.ts +5 -4
  39. package/dist/http/ip_canonical.d.ts.map +1 -1
  40. package/dist/http/ip_canonical.js +8 -4
  41. package/dist/http/origin.d.ts +1 -1
  42. package/dist/http/origin.js +1 -1
  43. package/dist/runtime/mock.js +1 -1
  44. package/dist/server/app_server.d.ts +41 -10
  45. package/dist/server/app_server.d.ts.map +1 -1
  46. package/dist/server/app_server.js +10 -4
  47. package/dist/server/env.d.ts +7 -7
  48. package/dist/server/env.d.ts.map +1 -1
  49. package/dist/server/env.js +14 -14
  50. package/dist/server/static.d.ts +4 -4
  51. package/dist/server/static.js +7 -7
  52. package/dist/testing/CLAUDE.md +220 -46
  53. package/dist/testing/admin_integration.d.ts +18 -23
  54. package/dist/testing/admin_integration.d.ts.map +1 -1
  55. package/dist/testing/admin_integration.js +159 -201
  56. package/dist/testing/app_server.d.ts +125 -38
  57. package/dist/testing/app_server.d.ts.map +1 -1
  58. package/dist/testing/app_server.js +140 -42
  59. package/dist/testing/audit_completeness.d.ts +23 -22
  60. package/dist/testing/audit_completeness.d.ts.map +1 -1
  61. package/dist/testing/audit_completeness.js +199 -156
  62. package/dist/testing/bootstrap_success.d.ts +28 -0
  63. package/dist/testing/bootstrap_success.d.ts.map +1 -0
  64. package/dist/testing/bootstrap_success.js +144 -0
  65. package/dist/testing/cross_backend/capabilities.d.ts +64 -0
  66. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
  67. package/dist/testing/cross_backend/capabilities.js +47 -0
  68. package/dist/testing/cross_backend/setup.d.ts +215 -0
  69. package/dist/testing/cross_backend/setup.d.ts.map +1 -0
  70. package/dist/testing/cross_backend/setup.js +101 -0
  71. package/dist/testing/data_exposure.d.ts +14 -15
  72. package/dist/testing/data_exposure.d.ts.map +1 -1
  73. package/dist/testing/data_exposure.js +127 -146
  74. package/dist/testing/db_entities.d.ts +11 -1
  75. package/dist/testing/db_entities.d.ts.map +1 -1
  76. package/dist/testing/db_entities.js +13 -1
  77. package/dist/testing/integration.d.ts +35 -21
  78. package/dist/testing/integration.d.ts.map +1 -1
  79. package/dist/testing/integration.js +231 -291
  80. package/dist/testing/integration_helpers.d.ts +16 -6
  81. package/dist/testing/integration_helpers.d.ts.map +1 -1
  82. package/dist/testing/integration_helpers.js +7 -7
  83. package/dist/testing/mock_fs.d.ts.map +1 -1
  84. package/dist/testing/mock_fs.js +0 -2
  85. package/dist/testing/rate_limiting.d.ts.map +1 -1
  86. package/dist/testing/rate_limiting.js +9 -0
  87. package/dist/testing/role_grant_helpers.d.ts +31 -0
  88. package/dist/testing/role_grant_helpers.d.ts.map +1 -0
  89. package/dist/testing/role_grant_helpers.js +46 -0
  90. package/dist/testing/round_trip.d.ts +21 -16
  91. package/dist/testing/round_trip.d.ts.map +1 -1
  92. package/dist/testing/round_trip.js +65 -86
  93. package/dist/testing/rpc_round_trip.d.ts +24 -21
  94. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  95. package/dist/testing/rpc_round_trip.js +91 -104
  96. package/dist/testing/schema_introspect.d.ts +106 -0
  97. package/dist/testing/schema_introspect.d.ts.map +1 -0
  98. package/dist/testing/schema_introspect.js +123 -0
  99. package/dist/testing/schema_parity.d.ts +144 -0
  100. package/dist/testing/schema_parity.d.ts.map +1 -0
  101. package/dist/testing/schema_parity.js +233 -0
  102. package/dist/testing/standard.d.ts +57 -25
  103. package/dist/testing/standard.d.ts.map +1 -1
  104. package/dist/testing/standard.js +62 -5
  105. package/dist/testing/stubs.d.ts +11 -3
  106. package/dist/testing/stubs.d.ts.map +1 -1
  107. package/dist/testing/stubs.js +24 -21
  108. package/dist/testing/transports/surface_source.d.ts +51 -0
  109. package/dist/testing/transports/surface_source.d.ts.map +1 -0
  110. package/dist/testing/transports/surface_source.js +19 -0
  111. 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 audit events are
7
- * verified by querying the `audit_log` table after each request.
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 { ROLE_KEEPER, ROLE_ADMIN } from '../auth/role_schema.js';
17
- import { AUDIT_EVENT_TYPES } from '../auth/audit_log_schema.js';
18
- import { auth_migration_ns } from '../auth/migrations.js';
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 { role_grant_offer_create_action_spec, role_grant_revoke_action_spec, } from '../auth/role_grant_offer_action_specs.js';
26
- import { admin_session_revoke_all_action_spec, admin_token_revoke_all_action_spec, app_settings_update_action_spec, invite_create_action_spec, invite_delete_action_spec, } from '../auth/admin_action_specs.js';
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
- /** Query audit log events from the database. */
29
- const query_audit_events = async (db) => {
30
- return db.query('SELECT event_type, seq, metadata FROM audit_log ORDER BY seq');
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 = (test_app, extra) => test_app.create_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 queries the `audit_log` table to verify events.
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. Factory-form
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
- const init_schema = async (db) => {
87
- await run_migrations(db, [auth_migration_ns]);
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 test_app = await create_test_app(build_options(options, get_db()));
96
- const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
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 test_app.app.request(login_route.path, {
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: test_app.backend.account.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 query_audit_events(test_app.backend.deps.db);
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 test_app = await create_test_app(build_options(options, get_db()));
112
- const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
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 test_app.app.request(login_route.path, {
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: test_app.backend.account.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 query_audit_events(test_app.backend.deps.db);
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 test_app = await create_test_app(build_options(options, get_db()));
128
- const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
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 test_app.app.request(logout_route.path, {
161
+ const res = await fixture.transport(logout_route.path, {
131
162
  method: 'POST',
132
- headers: test_app.create_session_headers(),
163
+ headers: fixture.create_session_headers(),
133
164
  });
134
165
  assert.strictEqual(res.status, 200);
135
- const events = await query_audit_events(test_app.backend.deps.db);
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 test_app = await create_test_app(build_options(options, get_db()));
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: test_app.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: test_app.create_session_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 query_audit_events(test_app.backend.deps.db);
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 test_app = await create_test_app(build_options(options, get_db()));
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: test_app.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: test_app.create_session_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: test_app.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: test_app.create_session_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 query_audit_events(test_app.backend.deps.db);
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 test_app = await create_test_app(build_options(options, get_db()));
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(test_app.route_specs, '/login', 'POST');
214
+ const login_route = find_auth_route(route_specs, '/login', 'POST');
181
215
  assert.ok(login_route, 'Expected POST /login route');
182
- await test_app.app.request(login_route.path, {
216
+ await fixture.transport(login_route.path, {
183
217
  method: 'POST',
184
218
  headers: UNAUTHENTICATED_JSON_HEADERS,
185
219
  body: JSON.stringify({
186
- username: test_app.backend.account.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: test_app.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: test_app.create_session_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 second session (not the one used for auth)
237
+ // revoke the newest session not the bootstrap one driving auth.
202
238
  const res = await rpc_call_for_spec({
203
- app: test_app.app,
239
+ app: { request: fixture.transport },
204
240
  path: rpc_path,
205
241
  spec: account_session_revoke_action_spec,
206
- params: { session_id: sessions[1].id },
207
- headers: test_app.create_session_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 query_audit_events(test_app.backend.deps.db);
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 test_app = await create_test_app(build_options(options, get_db()));
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: test_app.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: test_app.create_session_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 query_audit_events(test_app.backend.deps.db);
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 test_app = await create_test_app(build_options(options, get_db()));
230
- const route = find_auth_route(test_app.route_specs, '/password', 'POST');
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 test_app.app.request(route.path, {
270
+ const res = await fixture.transport(route.path, {
233
271
  method: 'POST',
234
- headers: json_session_headers(test_app),
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 query_audit_events(test_app.backend.deps.db);
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 test_app = await create_test_app(build_options(options, get_db()));
250
- const target = await test_app.create_account({ username: 'audit_target' });
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: test_app.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: test_app.create_session_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 `role_grant_create` fires on the
262
- // downstream consent transition.
263
- const events_after_offer = await query_audit_events(test_app.backend.deps.db);
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
- await get_db().transaction(async (tx) => {
266
- await query_accept_offer({ db: tx }, {
267
- offer_id: offer.id,
268
- to_account_id: target.account.id,
269
- actor_id: target.actor.id,
270
- ip: null,
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
- const events_after_accept = await query_audit_events(test_app.backend.deps.db);
274
- assert_has_event(events_after_accept, 'role_grant_create', 'offer accept');
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 test_app = await create_test_app(build_options(options, get_db()));
278
- const target = await test_app.create_account({ username: 'audit_revoke_target' });
279
- // Offer + accept to materialize a role_grant we can revoke.
280
- const offer_res = await rpc_call_for_spec({
281
- app: test_app.app,
282
- path: rpc_path,
283
- spec: role_grant_offer_create_action_spec,
284
- params: { to_account_id: target.account.id, role: ROLE_ADMIN },
285
- headers: test_app.create_session_headers(),
286
- });
287
- assert.ok(offer_res.ok, `role_grant_offer_create failed: ${offer_res.ok ? '' : JSON.stringify(offer_res.error)}`);
288
- const { offer } = offer_res.result;
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: test_app.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: accept_result.role_grant.id },
303
- headers: test_app.create_session_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 query_audit_events(test_app.backend.deps.db);
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
- const revoke_rows = await test_app.backend.deps.db.query(`SELECT target_account_id, target_actor_id FROM audit_log
311
- WHERE event_type = 'role_grant_revoke' ORDER BY seq DESC LIMIT 1`);
312
- const row = revoke_rows[0];
313
- assert.strictEqual(row.target_account_id, target.account.id);
314
- assert.strictEqual(row.target_actor_id, target.actor.id);
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 test_app = await create_test_app(build_options(options, get_db()));
318
- const target = await test_app.create_account({ username: 'audit_sessions_target' });
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: test_app.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: test_app.create_session_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 query_audit_events(test_app.backend.deps.db);
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 test_app = await create_test_app(build_options(options, get_db()));
333
- const target = await test_app.create_account({ username: 'audit_tokens_target' });
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: test_app.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: test_app.create_session_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 query_audit_events(test_app.backend.deps.db);
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 test_app = await create_test_app(build_options(options, get_db()));
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: test_app.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: test_app.create_session_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: test_app.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: test_app.create_session_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 query_audit_events(test_app.backend.deps.db);
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 test_app = await create_test_app(build_options(options, get_db()));
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: test_app.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: test_app.create_session_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 query_audit_events(test_app.backend.deps.db);
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 test_app = await create_test_app(build_options(options, get_db()));
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: test_app.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: test_app.create_session_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
- // signup
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 query_audit_events(test_app.backend.deps.db);
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 the RPC
444
- // endpoint or via downstream effects of `role_grant_revoke`. Direct
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',