@fuzdev/fuz_app 0.63.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 (181) hide show
  1. package/dist/actions/CLAUDE.md +525 -827
  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 +65 -0
  7. package/dist/actions/connection_closer.d.ts.map +1 -0
  8. package/dist/actions/connection_closer.js +38 -0
  9. package/dist/actions/register_action_ws.d.ts +2 -2
  10. package/dist/actions/register_action_ws.d.ts.map +1 -1
  11. package/dist/actions/register_action_ws.js +23 -2
  12. package/dist/actions/register_ws_endpoint.d.ts +12 -10
  13. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  14. package/dist/actions/register_ws_endpoint.js +5 -5
  15. package/dist/actions/transports_ws_auth_guard.d.ts +25 -10
  16. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  17. package/dist/actions/transports_ws_auth_guard.js +24 -9
  18. package/dist/actions/ws_endpoint_spec.d.ts +119 -0
  19. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -0
  20. package/dist/actions/ws_endpoint_spec.js +13 -0
  21. package/dist/auth/CLAUDE.md +592 -1808
  22. package/dist/auth/account_action_specs.d.ts +1 -1
  23. package/dist/auth/account_actions.d.ts +13 -0
  24. package/dist/auth/account_actions.d.ts.map +1 -1
  25. package/dist/auth/account_actions.js +31 -1
  26. package/dist/auth/account_routes.d.ts +12 -2
  27. package/dist/auth/account_routes.d.ts.map +1 -1
  28. package/dist/auth/account_routes.js +55 -8
  29. package/dist/auth/account_schema.d.ts +4 -4
  30. package/dist/auth/account_schema.d.ts.map +1 -1
  31. package/dist/auth/admin_action_specs.d.ts +8 -8
  32. package/dist/auth/admin_actions.d.ts +11 -0
  33. package/dist/auth/admin_actions.d.ts.map +1 -1
  34. package/dist/auth/admin_actions.js +25 -0
  35. package/dist/auth/api_token_queries.js +1 -1
  36. package/dist/auth/audit_emitter.d.ts +56 -12
  37. package/dist/auth/audit_emitter.d.ts.map +1 -1
  38. package/dist/auth/audit_emitter.js +38 -12
  39. package/dist/auth/audit_log_ddl.d.ts +1 -1
  40. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  41. package/dist/auth/audit_log_ddl.js +1 -1
  42. package/dist/auth/audit_log_schema.d.ts +5 -3
  43. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  44. package/dist/auth/audit_log_schema.js +5 -3
  45. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  46. package/dist/auth/bootstrap_account.js +1 -5
  47. package/dist/auth/bootstrap_routes.d.ts +8 -2
  48. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  49. package/dist/auth/bootstrap_routes.js +15 -11
  50. package/dist/auth/invite_schema.d.ts +2 -2
  51. package/dist/auth/keyring.d.ts +6 -6
  52. package/dist/auth/keyring.js +8 -8
  53. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  54. package/dist/auth/role_grant_offer_actions.js +4 -2
  55. package/dist/auth/signup_routes.d.ts +1 -1
  56. package/dist/auth/standard_rpc_actions.d.ts +1 -0
  57. package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
  58. package/dist/auth/standard_rpc_actions.js +1 -0
  59. package/dist/db/create_db.d.ts.map +1 -1
  60. package/dist/db/create_db.js +13 -0
  61. package/dist/dev/setup.d.ts +2 -2
  62. package/dist/dev/setup.js +3 -3
  63. package/dist/http/CLAUDE.md +225 -483
  64. package/dist/http/error_schemas.d.ts +0 -4
  65. package/dist/http/error_schemas.d.ts.map +1 -1
  66. package/dist/http/error_schemas.js +0 -4
  67. package/dist/http/ip_canonical.d.ts +100 -0
  68. package/dist/http/ip_canonical.d.ts.map +1 -0
  69. package/dist/http/ip_canonical.js +195 -0
  70. package/dist/http/origin.d.ts +14 -6
  71. package/dist/http/origin.d.ts.map +1 -1
  72. package/dist/http/origin.js +14 -32
  73. package/dist/http/pending_effects.d.ts +1 -1
  74. package/dist/http/pending_effects.js +1 -1
  75. package/dist/http/proxy.d.ts +13 -5
  76. package/dist/http/proxy.d.ts.map +1 -1
  77. package/dist/http/proxy.js +15 -23
  78. package/dist/http/surface.d.ts +50 -0
  79. package/dist/http/surface.d.ts.map +1 -1
  80. package/dist/http/surface.js +27 -1
  81. package/dist/primitive_schemas.d.ts +20 -4
  82. package/dist/primitive_schemas.d.ts.map +1 -1
  83. package/dist/primitive_schemas.js +25 -4
  84. package/dist/realtime/sse_auth_guard.d.ts +16 -4
  85. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  86. package/dist/realtime/sse_auth_guard.js +15 -3
  87. package/dist/runtime/mock.js +1 -1
  88. package/dist/server/app_backend.d.ts +66 -19
  89. package/dist/server/app_backend.d.ts.map +1 -1
  90. package/dist/server/app_backend.js +57 -34
  91. package/dist/server/app_server.d.ts +101 -10
  92. package/dist/server/app_server.d.ts.map +1 -1
  93. package/dist/server/app_server.js +105 -6
  94. package/dist/server/env.d.ts +7 -7
  95. package/dist/server/env.d.ts.map +1 -1
  96. package/dist/server/env.js +14 -14
  97. package/dist/server/startup.d.ts.map +1 -1
  98. package/dist/server/startup.js +12 -0
  99. package/dist/server/static.d.ts +4 -4
  100. package/dist/server/static.js +7 -7
  101. package/dist/testing/CLAUDE.md +269 -59
  102. package/dist/testing/admin_integration.d.ts +18 -23
  103. package/dist/testing/admin_integration.d.ts.map +1 -1
  104. package/dist/testing/admin_integration.js +159 -202
  105. package/dist/testing/adversarial_headers.d.ts +6 -0
  106. package/dist/testing/adversarial_headers.d.ts.map +1 -1
  107. package/dist/testing/adversarial_headers.js +13 -5
  108. package/dist/testing/app_server.d.ts +148 -60
  109. package/dist/testing/app_server.d.ts.map +1 -1
  110. package/dist/testing/app_server.js +143 -54
  111. package/dist/testing/attack_surface.d.ts +8 -7
  112. package/dist/testing/attack_surface.d.ts.map +1 -1
  113. package/dist/testing/attack_surface.js +12 -8
  114. package/dist/testing/audit_completeness.d.ts +23 -22
  115. package/dist/testing/audit_completeness.d.ts.map +1 -1
  116. package/dist/testing/audit_completeness.js +199 -158
  117. package/dist/testing/audit_drift_guard.d.ts +116 -0
  118. package/dist/testing/audit_drift_guard.d.ts.map +1 -0
  119. package/dist/testing/audit_drift_guard.js +134 -0
  120. package/dist/testing/bootstrap_success.d.ts +28 -0
  121. package/dist/testing/bootstrap_success.d.ts.map +1 -0
  122. package/dist/testing/bootstrap_success.js +144 -0
  123. package/dist/testing/connection_closer_helpers.d.ts +44 -0
  124. package/dist/testing/connection_closer_helpers.d.ts.map +1 -0
  125. package/dist/testing/connection_closer_helpers.js +48 -0
  126. package/dist/testing/cross_backend/capabilities.d.ts +64 -0
  127. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
  128. package/dist/testing/cross_backend/capabilities.js +47 -0
  129. package/dist/testing/cross_backend/setup.d.ts +215 -0
  130. package/dist/testing/cross_backend/setup.d.ts.map +1 -0
  131. package/dist/testing/cross_backend/setup.js +101 -0
  132. package/dist/testing/data_exposure.d.ts +14 -15
  133. package/dist/testing/data_exposure.d.ts.map +1 -1
  134. package/dist/testing/data_exposure.js +127 -146
  135. package/dist/testing/db_entities.d.ts +11 -1
  136. package/dist/testing/db_entities.d.ts.map +1 -1
  137. package/dist/testing/db_entities.js +13 -1
  138. package/dist/testing/integration.d.ts +35 -21
  139. package/dist/testing/integration.d.ts.map +1 -1
  140. package/dist/testing/integration.js +231 -293
  141. package/dist/testing/integration_helpers.d.ts +16 -6
  142. package/dist/testing/integration_helpers.d.ts.map +1 -1
  143. package/dist/testing/integration_helpers.js +7 -7
  144. package/dist/testing/mock_fs.d.ts.map +1 -1
  145. package/dist/testing/mock_fs.js +0 -2
  146. package/dist/testing/rate_limiting.d.ts.map +1 -1
  147. package/dist/testing/rate_limiting.js +13 -4
  148. package/dist/testing/role_grant_helpers.d.ts +31 -0
  149. package/dist/testing/role_grant_helpers.d.ts.map +1 -0
  150. package/dist/testing/role_grant_helpers.js +46 -0
  151. package/dist/testing/round_trip.d.ts +21 -16
  152. package/dist/testing/round_trip.d.ts.map +1 -1
  153. package/dist/testing/round_trip.js +65 -86
  154. package/dist/testing/rpc_helpers.d.ts +2 -1
  155. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  156. package/dist/testing/rpc_round_trip.d.ts +24 -21
  157. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  158. package/dist/testing/rpc_round_trip.js +91 -106
  159. package/dist/testing/schema_introspect.d.ts +106 -0
  160. package/dist/testing/schema_introspect.d.ts.map +1 -0
  161. package/dist/testing/schema_introspect.js +123 -0
  162. package/dist/testing/schema_parity.d.ts +144 -0
  163. package/dist/testing/schema_parity.d.ts.map +1 -0
  164. package/dist/testing/schema_parity.js +233 -0
  165. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  166. package/dist/testing/sse_round_trip.js +12 -6
  167. package/dist/testing/standard.d.ts +57 -25
  168. package/dist/testing/standard.d.ts.map +1 -1
  169. package/dist/testing/standard.js +62 -5
  170. package/dist/testing/stubs.d.ts +22 -3
  171. package/dist/testing/stubs.d.ts.map +1 -1
  172. package/dist/testing/stubs.js +28 -21
  173. package/dist/testing/surface_invariants.d.ts +66 -1
  174. package/dist/testing/surface_invariants.d.ts.map +1 -1
  175. package/dist/testing/surface_invariants.js +103 -1
  176. package/dist/testing/transports/surface_source.d.ts +51 -0
  177. package/dist/testing/transports/surface_source.d.ts.map +1 -0
  178. package/dist/testing/transports/surface_source.js +19 -0
  179. package/dist/ui/SurfaceExplorer.svelte +161 -2
  180. package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
  181. 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,17 +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
- app_options: {
54
- ...options.app_options,
55
- rpc_endpoints: options.rpc_endpoints,
56
- },
57
- });
58
84
  /** Headers for unauthenticated JSON requests (login, signup). */
59
85
  const UNAUTHENTICATED_JSON_HEADERS = {
60
86
  host: 'localhost',
@@ -62,7 +88,7 @@ const UNAUTHENTICATED_JSON_HEADERS = {
62
88
  'content-type': 'application/json',
63
89
  };
64
90
  /** Standard request headers for session-authenticated JSON requests. */
65
- const json_session_headers = (test_app, extra) => test_app.create_session_headers({
91
+ const json_session_headers = (fixture, extra) => fixture.create_session_headers({
66
92
  'content-type': 'application/json',
67
93
  ...extra,
68
94
  });
@@ -71,7 +97,8 @@ const json_session_headers = (test_app, extra) => test_app.create_session_header
71
97
  *
72
98
  * Verifies that every auth mutation route produces the correct audit log
73
99
  * event type. Exercises routes via HTTP requests against a real PGlite
74
- * 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).
75
102
  *
76
103
  * @throws Error at setup time when `options.rpc_endpoints` is empty — the
77
104
  * mutation-audit tests drive role_grant flow, session/token revoke-all, and
@@ -79,294 +106,301 @@ const json_session_headers = (test_app, extra) => test_app.create_session_header
79
106
  * `require_rpc_endpoint_path`.
80
107
  */
81
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;
82
114
  // Hard-fail early so consumers see a clear setup error instead of a
83
- // confusing test failure when `rpc_endpoints` is missing. Factory-form
84
- // callers are resolved with a stub ctx purely to extract the endpoint
85
- // path; real handlers run per-test via `app_options.rpc_endpoints`.
115
+ // confusing test failure when `rpc_endpoints` is missing.
86
116
  const rpc_endpoints_for_setup = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
87
117
  const rpc_path = require_rpc_endpoint_path(rpc_endpoints_for_setup);
88
- const init_schema = async (db) => {
89
- await run_migrations(db, [auth_migration_ns]);
90
- };
91
- const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
92
- const describe_db = create_describe_db(factories, auth_integration_truncate_tables);
93
- describe_db('audit_log_completeness', (get_db) => {
118
+ void options.capabilities;
119
+ describe('audit_log_completeness', () => {
94
120
  // --- Account routes ---
95
121
  describe('account mutation audit events', () => {
96
122
  test('login success produces login event', async () => {
97
- const test_app = await create_test_app(build_options(options, get_db()));
98
- 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');
99
126
  assert.ok(login_route, 'Expected POST /login route');
100
- const res = await test_app.app.request(login_route.path, {
127
+ const res = await fixture.transport(login_route.path, {
101
128
  method: 'POST',
102
129
  headers: UNAUTHENTICATED_JSON_HEADERS,
103
130
  body: JSON.stringify({
104
- username: test_app.backend.account.username,
131
+ username: fixture.account.username,
105
132
  password: 'test-password-123',
106
133
  }),
107
134
  });
108
135
  assert.strictEqual(res.status, 200);
109
- const events = await query_audit_events(test_app.backend.deps.db);
136
+ const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
110
137
  assert_has_event(events, 'login', 'POST /login (success)');
111
138
  });
112
139
  test('login failure produces login event with failure outcome', async () => {
113
- const test_app = await create_test_app(build_options(options, get_db()));
114
- 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');
115
143
  assert.ok(login_route, 'Expected POST /login route');
116
- const res = await test_app.app.request(login_route.path, {
144
+ const res = await fixture.transport(login_route.path, {
117
145
  method: 'POST',
118
146
  headers: UNAUTHENTICATED_JSON_HEADERS,
119
147
  body: JSON.stringify({
120
- username: test_app.backend.account.username,
148
+ username: fixture.account.username,
121
149
  password: 'wrong-password',
122
150
  }),
123
151
  });
124
152
  assert.strictEqual(res.status, 401);
125
- const events = await query_audit_events(test_app.backend.deps.db);
153
+ const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
126
154
  assert_has_event(events, 'login', 'POST /login (failure)');
127
155
  });
128
156
  test('logout produces logout event', async () => {
129
- const test_app = await create_test_app(build_options(options, get_db()));
130
- 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');
131
160
  assert.ok(logout_route, 'Expected POST /logout route');
132
- const res = await test_app.app.request(logout_route.path, {
161
+ const res = await fixture.transport(logout_route.path, {
133
162
  method: 'POST',
134
- headers: test_app.create_session_headers(),
163
+ headers: fixture.create_session_headers(),
135
164
  });
136
165
  assert.strictEqual(res.status, 200);
137
- const events = await query_audit_events(test_app.backend.deps.db);
166
+ const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
138
167
  assert_has_event(events, 'logout', 'POST /logout');
139
168
  });
140
169
  test('token create produces token_create event', async () => {
141
- 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);
142
172
  const res = await rpc_call_for_spec({
143
- app: test_app.app,
173
+ app: { request: fixture.transport },
144
174
  path: rpc_path,
145
175
  spec: account_token_create_action_spec,
146
176
  params: { name: 'audit-test' },
147
- headers: test_app.create_session_headers(),
177
+ headers: fixture.create_session_headers(),
148
178
  });
149
179
  assert.ok(res.ok, `account_token_create failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
150
- const events = await query_audit_events(test_app.backend.deps.db);
180
+ const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
151
181
  assert_has_event(events, 'token_create', 'account_token_create RPC');
152
182
  assert_event_credential_type(events, 'token_create', 'session', 'account_token_create RPC');
153
183
  });
154
184
  test('token revoke produces token_revoke event', async () => {
155
- 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);
156
187
  // get a token ID to revoke
157
188
  const list_res = await rpc_call_for_spec({
158
- app: test_app.app,
189
+ app: { request: fixture.transport },
159
190
  path: rpc_path,
160
191
  spec: account_token_list_action_spec,
161
192
  params: undefined,
162
- headers: test_app.create_session_headers(),
193
+ headers: fixture.create_session_headers(),
163
194
  });
164
195
  assert.ok(list_res.ok, 'account_token_list should succeed');
165
196
  const { tokens } = list_res.result;
166
197
  assert.ok(tokens.length > 0, 'Expected at least one token');
167
198
  const res = await rpc_call_for_spec({
168
- app: test_app.app,
199
+ app: { request: fixture.transport },
169
200
  path: rpc_path,
170
201
  spec: account_token_revoke_action_spec,
171
202
  params: { token_id: tokens[0].id },
172
- headers: test_app.create_session_headers(),
203
+ headers: fixture.create_session_headers(),
173
204
  });
174
205
  assert.ok(res.ok, `account_token_revoke failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
175
- const events = await query_audit_events(test_app.backend.deps.db);
206
+ const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
176
207
  assert_has_event(events, 'token_revoke', 'account_token_revoke RPC');
177
208
  assert_event_credential_type(events, 'token_revoke', 'session', 'account_token_revoke RPC');
178
209
  });
179
210
  test('session revoke produces session_revoke event', async () => {
180
- 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);
181
213
  // login to create a second session we can revoke
182
- const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
214
+ const login_route = find_auth_route(route_specs, '/login', 'POST');
183
215
  assert.ok(login_route, 'Expected POST /login route');
184
- await test_app.app.request(login_route.path, {
216
+ await fixture.transport(login_route.path, {
185
217
  method: 'POST',
186
218
  headers: UNAUTHENTICATED_JSON_HEADERS,
187
219
  body: JSON.stringify({
188
- username: test_app.backend.account.username,
220
+ username: fixture.account.username,
189
221
  password: 'test-password-123',
190
222
  }),
191
223
  });
192
- // 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).
193
227
  const list_res = await rpc_call_for_spec({
194
- app: test_app.app,
228
+ app: { request: fixture.transport },
195
229
  path: rpc_path,
196
230
  spec: account_session_list_action_spec,
197
231
  params: undefined,
198
- headers: test_app.create_session_headers(),
232
+ headers: fixture.create_session_headers(),
199
233
  });
200
234
  assert.ok(list_res.ok, 'account_session_list should succeed');
201
235
  const { sessions } = list_res.result;
202
236
  assert.ok(sessions.length >= 2, 'Expected at least 2 sessions');
203
- // revoke the second session (not the one used for auth)
237
+ // revoke the newest session not the bootstrap one driving auth.
204
238
  const res = await rpc_call_for_spec({
205
- app: test_app.app,
239
+ app: { request: fixture.transport },
206
240
  path: rpc_path,
207
241
  spec: account_session_revoke_action_spec,
208
- params: { session_id: sessions[1].id },
209
- headers: test_app.create_session_headers(),
242
+ params: { session_id: sessions[0].id },
243
+ headers: fixture.create_session_headers(),
210
244
  });
211
245
  assert.ok(res.ok, `account_session_revoke failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
212
- const events = await query_audit_events(test_app.backend.deps.db);
246
+ const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
213
247
  assert_has_event(events, 'session_revoke', 'account_session_revoke RPC');
214
248
  assert_event_credential_type(events, 'session_revoke', 'session', 'account_session_revoke RPC');
215
249
  });
216
250
  test('session revoke-all produces session_revoke_all event', async () => {
217
- 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);
218
253
  const res = await rpc_call_for_spec({
219
- app: test_app.app,
254
+ app: { request: fixture.transport },
220
255
  path: rpc_path,
221
256
  spec: account_session_revoke_all_action_spec,
222
257
  params: undefined,
223
- headers: test_app.create_session_headers(),
258
+ headers: fixture.create_session_headers(),
224
259
  });
225
260
  assert.ok(res.ok, `account_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
226
- const events = await query_audit_events(test_app.backend.deps.db);
261
+ const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
227
262
  assert_has_event(events, 'session_revoke_all', 'account_session_revoke_all RPC');
228
263
  assert_event_credential_type(events, 'session_revoke_all', 'session', 'account_session_revoke_all RPC');
229
264
  });
230
265
  test('password change produces password_change event', async () => {
231
- const test_app = await create_test_app(build_options(options, get_db()));
232
- 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');
233
269
  assert.ok(route, 'Expected POST /password route');
234
- const res = await test_app.app.request(route.path, {
270
+ const res = await fixture.transport(route.path, {
235
271
  method: 'POST',
236
- headers: json_session_headers(test_app),
272
+ headers: json_session_headers(fixture),
237
273
  body: JSON.stringify({
238
274
  current_password: 'test-password-123',
239
275
  new_password: 'new-password-456',
240
276
  }),
241
277
  });
242
278
  assert.strictEqual(res.status, 200);
243
- const events = await query_audit_events(test_app.backend.deps.db);
279
+ const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
244
280
  assert_has_event(events, 'password_change', 'POST /password');
245
281
  assert_event_credential_type(events, 'password_change', 'session', 'POST /password');
246
282
  });
247
283
  });
248
284
  // --- Admin routes ---
249
285
  describe('admin mutation audit events', () => {
250
- test('admin offer (RPC) + accept produces role_grant_offer_create and role_grant_create events', async () => {
251
- const test_app = await create_test_app(build_options(options, get_db()));
252
- 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' });
253
290
  const offer_res = await rpc_call_for_spec({
254
- app: test_app.app,
291
+ app: { request: fixture.transport },
255
292
  path: rpc_path,
256
293
  spec: role_grant_offer_create_action_spec,
257
294
  params: { to_account_id: target.account.id, role: ROLE_ADMIN },
258
- headers: test_app.create_session_headers(),
295
+ headers: fixture.create_session_headers(),
259
296
  });
260
297
  assert.ok(offer_res.ok, `role_grant_offer_create failed: ${offer_res.ok ? '' : JSON.stringify(offer_res.error)}`);
261
298
  const { offer } = offer_res.result;
262
299
  // Admin offer emits `role_grant_offer_create` only — the role_grant doesn't
263
- // exist yet. Drive the accept to confirm `role_grant_create` fires on the
264
- // downstream consent transition.
265
- 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);
266
303
  assert_has_event(events_after_offer, 'role_grant_offer_create', 'role_grant_offer_create RPC');
267
- await get_db().transaction(async (tx) => {
268
- await query_accept_offer({ db: tx }, {
269
- offer_id: offer.id,
270
- to_account_id: target.account.id,
271
- actor_id: target.actor.id,
272
- ip: null,
273
- });
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(),
274
310
  });
275
- const events_after_accept = await query_audit_events(test_app.backend.deps.db);
276
- 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');
277
315
  });
278
316
  test('role_grant revoke (RPC) produces role_grant_revoke event with both target columns', async () => {
279
- const test_app = await create_test_app(build_options(options, get_db()));
280
- const target = await test_app.create_account({ username: 'audit_revoke_target' });
281
- // Offer + accept to materialize a role_grant we can revoke.
282
- const offer_res = await rpc_call_for_spec({
283
- app: test_app.app,
284
- path: rpc_path,
285
- spec: role_grant_offer_create_action_spec,
286
- params: { to_account_id: target.account.id, role: ROLE_ADMIN },
287
- headers: test_app.create_session_headers(),
288
- });
289
- assert.ok(offer_res.ok, `role_grant_offer_create failed: ${offer_res.ok ? '' : JSON.stringify(offer_res.error)}`);
290
- const { offer } = offer_res.result;
291
- const accept_result = await get_db().transaction(async (tx) => {
292
- return query_accept_offer({ db: tx }, {
293
- offer_id: offer.id,
294
- to_account_id: target.account.id,
295
- actor_id: target.actor.id,
296
- ip: null,
297
- });
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,
298
329
  });
299
330
  // Revoke via RPC.
300
331
  const revoke_res = await rpc_call_for_spec({
301
- app: test_app.app,
332
+ app: { request: fixture.transport },
302
333
  path: rpc_path,
303
334
  spec: role_grant_revoke_action_spec,
304
- params: { actor_id: target.actor.id, role_grant_id: accept_result.role_grant.id },
305
- headers: test_app.create_session_headers(),
335
+ params: { actor_id: target.actor.id, role_grant_id },
336
+ headers: fixture.create_session_headers(),
306
337
  });
307
338
  assert.ok(revoke_res.ok, `role_grant_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
308
- const events = await query_audit_events(test_app.backend.deps.db);
339
+ const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
309
340
  assert_has_event(events, 'role_grant_revoke', 'role_grant_revoke RPC');
310
341
  // Audit envelope must populate both target columns —
311
342
  // `role_grant_revoke` is the canonical actor-bound-subject event.
312
- const revoke_rows = await test_app.backend.deps.db.query(`SELECT target_account_id, target_actor_id FROM audit_log
313
- WHERE event_type = 'role_grant_revoke' ORDER BY seq DESC LIMIT 1`);
314
- const row = revoke_rows[0];
315
- assert.strictEqual(row.target_account_id, target.account.id);
316
- 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);
317
348
  });
318
349
  test('admin session revoke-all produces session_revoke_all event', async () => {
319
- const test_app = await create_test_app(build_options(options, get_db()));
320
- 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' });
321
353
  const res = await rpc_call_for_spec({
322
- app: test_app.app,
354
+ app: { request: fixture.transport },
323
355
  path: rpc_path,
324
356
  spec: admin_session_revoke_all_action_spec,
325
357
  params: { account_id: target.account.id },
326
- headers: test_app.create_session_headers(),
358
+ headers: fixture.create_session_headers(),
327
359
  });
328
360
  assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
329
- const events = await query_audit_events(test_app.backend.deps.db);
361
+ const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
330
362
  // admin session revoke-all also produces session_revoke_all
331
363
  assert_has_event(events, 'session_revoke_all', 'admin_session_revoke_all RPC');
332
364
  });
333
365
  test('admin token revoke-all produces token_revoke_all event', async () => {
334
- const test_app = await create_test_app(build_options(options, get_db()));
335
- 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' });
336
369
  const res = await rpc_call_for_spec({
337
- app: test_app.app,
370
+ app: { request: fixture.transport },
338
371
  path: rpc_path,
339
372
  spec: admin_token_revoke_all_action_spec,
340
373
  params: { account_id: target.account.id },
341
- headers: test_app.create_session_headers(),
374
+ headers: fixture.create_session_headers(),
342
375
  });
343
376
  assert.ok(res.ok, `admin_token_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
344
- const events = await query_audit_events(test_app.backend.deps.db);
377
+ const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
345
378
  assert_has_event(events, 'token_revoke_all', 'admin_token_revoke_all RPC');
346
379
  });
347
380
  });
348
381
  // --- Invite RPC actions ---
349
382
  describe('invite mutation audit events', () => {
350
383
  test('invite create and delete produce audit events', async () => {
351
- 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);
352
386
  const create_res = await rpc_call_for_spec({
353
- app: test_app.app,
387
+ app: { request: fixture.transport },
354
388
  path: rpc_path,
355
389
  spec: invite_create_action_spec,
356
390
  params: { username: 'invited_user' },
357
- headers: test_app.create_session_headers(),
391
+ headers: fixture.create_session_headers(),
358
392
  });
359
393
  assert.ok(create_res.ok, `invite_create failed: ${create_res.ok ? '' : JSON.stringify(create_res.error)}`);
360
394
  const { invite } = create_res.result;
361
395
  const delete_res = await rpc_call_for_spec({
362
- app: test_app.app,
396
+ app: { request: fixture.transport },
363
397
  path: rpc_path,
364
398
  spec: invite_delete_action_spec,
365
399
  params: { invite_id: invite.id },
366
- headers: test_app.create_session_headers(),
400
+ headers: fixture.create_session_headers(),
367
401
  });
368
402
  assert.ok(delete_res.ok, `invite_delete failed: ${delete_res.ok ? '' : JSON.stringify(delete_res.error)}`);
369
- const events = await query_audit_events(test_app.backend.deps.db);
403
+ const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
370
404
  assert_has_event(events, 'invite_create', 'invite_create RPC');
371
405
  assert_has_event(events, 'invite_delete', 'invite_delete RPC');
372
406
  });
@@ -374,36 +408,42 @@ export const describe_audit_completeness_tests = (options) => {
374
408
  // --- App settings RPC action ---
375
409
  describe('app settings mutation audit events', () => {
376
410
  test('settings update produces app_settings_update event', async () => {
377
- 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);
378
413
  const res = await rpc_call_for_spec({
379
- app: test_app.app,
414
+ app: { request: fixture.transport },
380
415
  path: rpc_path,
381
416
  spec: app_settings_update_action_spec,
382
417
  params: { open_signup: true },
383
- headers: test_app.create_session_headers(),
418
+ headers: fixture.create_session_headers(),
384
419
  });
385
420
  assert.ok(res.ok, `app_settings_update failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
386
- const events = await query_audit_events(test_app.backend.deps.db);
421
+ const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
387
422
  assert_has_event(events, 'app_settings_update', 'app_settings_update RPC');
388
423
  });
389
424
  });
390
425
  // --- Signup route ---
391
426
  describe('signup audit events', () => {
392
427
  test('signup produces signup event', async () => {
393
- 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);
394
437
  // enable open signup via RPC
395
438
  const settings_res = await rpc_call_for_spec({
396
- app: test_app.app,
439
+ app: { request: fixture.transport },
397
440
  path: rpc_path,
398
441
  spec: app_settings_update_action_spec,
399
442
  params: { open_signup: true },
400
- headers: test_app.create_session_headers(),
443
+ headers: fixture.create_session_headers(),
401
444
  });
402
445
  assert.ok(settings_res.ok, `app_settings_update failed: ${settings_res.ok ? '' : JSON.stringify(settings_res.error)}`);
403
- // signup
404
- const signup_route = find_auth_route(test_app.route_specs, '/signup', 'POST');
405
- assert.ok(signup_route, 'Expected POST /signup route');
406
- const res = await test_app.app.request(signup_route.path, {
446
+ const res = await fixture.transport(signup_route.path, {
407
447
  method: 'POST',
408
448
  headers: UNAUTHENTICATED_JSON_HEADERS,
409
449
  body: JSON.stringify({
@@ -412,7 +452,7 @@ export const describe_audit_completeness_tests = (options) => {
412
452
  }),
413
453
  });
414
454
  assert.strictEqual(res.status, 200);
415
- const events = await query_audit_events(test_app.backend.deps.db);
455
+ const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
416
456
  assert_has_event(events, 'signup', 'POST /signup');
417
457
  });
418
458
  });
@@ -433,6 +473,7 @@ export const describe_audit_completeness_tests = (options) => {
433
473
  'token_revoke',
434
474
  'token_revoke_all',
435
475
  'role_grant_offer_create',
476
+ 'role_grant_offer_accept',
436
477
  'role_grant_create',
437
478
  'role_grant_revoke',
438
479
  'invite_create',
@@ -442,8 +483,9 @@ export const describe_audit_completeness_tests = (options) => {
442
483
  /** Event types excluded with justification. */
443
484
  const EXCLUDED_EVENT_TYPES = new Set([
444
485
  'bootstrap', // requires filesystem token — tested in bootstrap_account.db.test.ts
445
- // The remaining `role_grant_offer_*` events fire only via the RPC
446
- // 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
447
489
  // coverage lives in `role_grant_offer_queries.db.test.ts`,
448
490
  // `role_grant_offer_actions.db.test.ts`,
449
491
  // `role_grant_offer_actions.notifications.db.test.ts`, and
@@ -451,7 +493,6 @@ export const describe_audit_completeness_tests = (options) => {
451
493
  // `role_grant_offer_expire` fires from the cleanup sweep
452
494
  // (`cleanup_expired_role_grant_offers` in `auth/cleanup.ts`) —
453
495
  // covered in `cleanup.db.test.ts`.
454
- 'role_grant_offer_accept',
455
496
  'role_grant_offer_decline',
456
497
  'role_grant_offer_retract',
457
498
  'role_grant_offer_expire',