@fuzdev/fuz_app 0.30.0 → 0.32.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 (222) hide show
  1. package/dist/actions/CLAUDE.md +630 -0
  2. package/dist/actions/action_rpc.d.ts +29 -0
  3. package/dist/actions/action_rpc.d.ts.map +1 -1
  4. package/dist/actions/action_rpc.js +42 -6
  5. package/dist/actions/action_types.d.ts +2 -2
  6. package/dist/actions/cancel.d.ts +12 -13
  7. package/dist/actions/cancel.d.ts.map +1 -1
  8. package/dist/actions/cancel.js +10 -13
  9. package/dist/actions/heartbeat.d.ts +8 -13
  10. package/dist/actions/heartbeat.d.ts.map +1 -1
  11. package/dist/actions/heartbeat.js +5 -8
  12. package/dist/actions/register_action_ws.d.ts +3 -3
  13. package/dist/actions/register_action_ws.js +2 -2
  14. package/dist/actions/register_ws_endpoint.d.ts +4 -4
  15. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  16. package/dist/actions/register_ws_endpoint.js +3 -3
  17. package/dist/actions/rpc_client.d.ts +29 -0
  18. package/dist/actions/rpc_client.d.ts.map +1 -1
  19. package/dist/actions/rpc_client.js +31 -0
  20. package/dist/actions/socket.svelte.d.ts +16 -16
  21. package/dist/actions/socket.svelte.d.ts.map +1 -1
  22. package/dist/actions/socket.svelte.js +15 -15
  23. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  24. package/dist/auth/CLAUDE.md +945 -0
  25. package/dist/auth/account_action_specs.d.ts +216 -0
  26. package/dist/auth/account_action_specs.d.ts.map +1 -0
  27. package/dist/auth/account_action_specs.js +159 -0
  28. package/dist/auth/account_actions.d.ts +51 -0
  29. package/dist/auth/account_actions.d.ts.map +1 -0
  30. package/dist/auth/account_actions.js +119 -0
  31. package/dist/auth/account_queries.d.ts +6 -2
  32. package/dist/auth/account_queries.d.ts.map +1 -1
  33. package/dist/auth/account_queries.js +40 -4
  34. package/dist/auth/account_routes.d.ts +94 -16
  35. package/dist/auth/account_routes.d.ts.map +1 -1
  36. package/dist/auth/account_routes.js +108 -180
  37. package/dist/auth/account_schema.d.ts +85 -30
  38. package/dist/auth/account_schema.d.ts.map +1 -1
  39. package/dist/auth/account_schema.js +40 -8
  40. package/dist/auth/admin_action_specs.d.ts +674 -0
  41. package/dist/auth/admin_action_specs.d.ts.map +1 -0
  42. package/dist/auth/admin_action_specs.js +287 -0
  43. package/dist/auth/admin_actions.d.ts +69 -0
  44. package/dist/auth/admin_actions.d.ts.map +1 -0
  45. package/dist/auth/admin_actions.js +256 -0
  46. package/dist/auth/admin_rpc_actions.d.ts +49 -0
  47. package/dist/auth/admin_rpc_actions.d.ts.map +1 -0
  48. package/dist/auth/admin_rpc_actions.js +32 -0
  49. package/dist/auth/api_token.d.ts +10 -0
  50. package/dist/auth/api_token.d.ts.map +1 -1
  51. package/dist/auth/api_token.js +9 -0
  52. package/dist/auth/api_token_queries.d.ts +3 -3
  53. package/dist/auth/api_token_queries.js +3 -3
  54. package/dist/auth/app_settings_schema.d.ts +4 -3
  55. package/dist/auth/app_settings_schema.d.ts.map +1 -1
  56. package/dist/auth/app_settings_schema.js +2 -1
  57. package/dist/auth/audit_log_routes.d.ts +14 -6
  58. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  59. package/dist/auth/audit_log_routes.js +22 -79
  60. package/dist/auth/audit_log_schema.d.ts +100 -29
  61. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  62. package/dist/auth/audit_log_schema.js +83 -11
  63. package/dist/auth/bootstrap_routes.d.ts +14 -0
  64. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  65. package/dist/auth/bootstrap_routes.js +10 -3
  66. package/dist/auth/cleanup.d.ts +63 -0
  67. package/dist/auth/cleanup.d.ts.map +1 -0
  68. package/dist/auth/cleanup.js +80 -0
  69. package/dist/auth/invite_schema.d.ts +11 -10
  70. package/dist/auth/invite_schema.d.ts.map +1 -1
  71. package/dist/auth/invite_schema.js +4 -3
  72. package/dist/auth/migrations.d.ts +6 -0
  73. package/dist/auth/migrations.d.ts.map +1 -1
  74. package/dist/auth/migrations.js +28 -0
  75. package/dist/auth/permit_offer_action_specs.d.ts +364 -0
  76. package/dist/auth/permit_offer_action_specs.d.ts.map +1 -0
  77. package/dist/auth/permit_offer_action_specs.js +216 -0
  78. package/dist/auth/permit_offer_actions.d.ts +96 -0
  79. package/dist/auth/permit_offer_actions.d.ts.map +1 -0
  80. package/dist/auth/permit_offer_actions.js +428 -0
  81. package/dist/auth/permit_offer_notifications.d.ts +361 -0
  82. package/dist/auth/permit_offer_notifications.d.ts.map +1 -0
  83. package/dist/auth/permit_offer_notifications.js +179 -0
  84. package/dist/auth/permit_offer_queries.d.ts +165 -0
  85. package/dist/auth/permit_offer_queries.d.ts.map +1 -0
  86. package/dist/auth/permit_offer_queries.js +390 -0
  87. package/dist/auth/permit_offer_schema.d.ts +103 -0
  88. package/dist/auth/permit_offer_schema.d.ts.map +1 -0
  89. package/dist/auth/permit_offer_schema.js +142 -0
  90. package/dist/auth/permit_queries.d.ts +77 -14
  91. package/dist/auth/permit_queries.d.ts.map +1 -1
  92. package/dist/auth/permit_queries.js +119 -24
  93. package/dist/auth/session_queries.d.ts +4 -2
  94. package/dist/auth/session_queries.d.ts.map +1 -1
  95. package/dist/auth/session_queries.js +4 -2
  96. package/dist/auth/signup_routes.d.ts +13 -0
  97. package/dist/auth/signup_routes.d.ts.map +1 -1
  98. package/dist/auth/signup_routes.js +14 -7
  99. package/dist/http/CLAUDE.md +584 -0
  100. package/dist/http/pending_effects.d.ts +29 -0
  101. package/dist/http/pending_effects.d.ts.map +1 -0
  102. package/dist/http/pending_effects.js +31 -0
  103. package/dist/http/route_spec.d.ts.map +1 -1
  104. package/dist/http/route_spec.js +4 -3
  105. package/dist/rate_limiter.d.ts +30 -0
  106. package/dist/rate_limiter.d.ts.map +1 -1
  107. package/dist/rate_limiter.js +25 -2
  108. package/dist/realtime/sse_auth_guard.d.ts +2 -0
  109. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  110. package/dist/realtime/sse_auth_guard.js +5 -3
  111. package/dist/server/app_server.d.ts +13 -2
  112. package/dist/server/app_server.d.ts.map +1 -1
  113. package/dist/server/app_server.js +12 -1
  114. package/dist/testing/CLAUDE.md +668 -1
  115. package/dist/testing/admin_integration.d.ts +10 -7
  116. package/dist/testing/admin_integration.d.ts.map +1 -1
  117. package/dist/testing/admin_integration.js +382 -482
  118. package/dist/testing/app_server.d.ts +7 -6
  119. package/dist/testing/app_server.d.ts.map +1 -1
  120. package/dist/testing/attack_surface.d.ts +9 -3
  121. package/dist/testing/attack_surface.d.ts.map +1 -1
  122. package/dist/testing/attack_surface.js +4 -4
  123. package/dist/testing/audit_completeness.d.ts +11 -0
  124. package/dist/testing/audit_completeness.d.ts.map +1 -1
  125. package/dist/testing/audit_completeness.js +169 -134
  126. package/dist/testing/auth_apps.d.ts.map +1 -1
  127. package/dist/testing/auth_apps.js +4 -33
  128. package/dist/testing/db.d.ts +1 -1
  129. package/dist/testing/db.d.ts.map +1 -1
  130. package/dist/testing/db.js +2 -0
  131. package/dist/testing/entities.d.ts +35 -13
  132. package/dist/testing/entities.d.ts.map +1 -1
  133. package/dist/testing/entities.js +17 -0
  134. package/dist/testing/integration.d.ts +10 -0
  135. package/dist/testing/integration.d.ts.map +1 -1
  136. package/dist/testing/integration.js +352 -340
  137. package/dist/testing/integration_helpers.d.ts +16 -5
  138. package/dist/testing/integration_helpers.d.ts.map +1 -1
  139. package/dist/testing/integration_helpers.js +24 -4
  140. package/dist/testing/rate_limiting.d.ts +7 -0
  141. package/dist/testing/rate_limiting.d.ts.map +1 -1
  142. package/dist/testing/rate_limiting.js +41 -10
  143. package/dist/testing/rpc_helpers.d.ts +153 -1
  144. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  145. package/dist/testing/rpc_helpers.js +184 -8
  146. package/dist/testing/sse_round_trip.d.ts +8 -0
  147. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  148. package/dist/testing/sse_round_trip.js +10 -3
  149. package/dist/testing/standard.d.ts +9 -1
  150. package/dist/testing/standard.d.ts.map +1 -1
  151. package/dist/testing/standard.js +6 -2
  152. package/dist/testing/stubs.d.ts +10 -2
  153. package/dist/testing/stubs.d.ts.map +1 -1
  154. package/dist/testing/stubs.js +17 -2
  155. package/dist/testing/surface_invariants.d.ts +7 -3
  156. package/dist/testing/surface_invariants.d.ts.map +1 -1
  157. package/dist/testing/surface_invariants.js +5 -4
  158. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  159. package/dist/testing/ws_round_trip.js +9 -38
  160. package/dist/ui/AccountSessions.svelte +8 -4
  161. package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
  162. package/dist/ui/AdminAccounts.svelte +61 -33
  163. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  164. package/dist/ui/AdminAuditLog.svelte +3 -2
  165. package/dist/ui/AdminAuditLog.svelte.d.ts.map +1 -1
  166. package/dist/ui/AdminInvites.svelte +3 -2
  167. package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
  168. package/dist/ui/AdminOverview.svelte +14 -9
  169. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  170. package/dist/ui/AdminPermitHistory.svelte +3 -2
  171. package/dist/ui/AdminPermitHistory.svelte.d.ts.map +1 -1
  172. package/dist/ui/AdminSessions.svelte +29 -25
  173. package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
  174. package/dist/ui/CLAUDE.md +363 -0
  175. package/dist/ui/OpenSignupToggle.svelte +6 -3
  176. package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
  177. package/dist/ui/PermitOfferForm.svelte +141 -0
  178. package/dist/ui/PermitOfferForm.svelte.d.ts +14 -0
  179. package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -0
  180. package/dist/ui/PermitOfferHistory.svelte +109 -0
  181. package/dist/ui/PermitOfferHistory.svelte.d.ts +11 -0
  182. package/dist/ui/PermitOfferHistory.svelte.d.ts.map +1 -0
  183. package/dist/ui/PermitOfferInbox.svelte +121 -0
  184. package/dist/ui/PermitOfferInbox.svelte.d.ts +12 -0
  185. package/dist/ui/PermitOfferInbox.svelte.d.ts.map +1 -0
  186. package/dist/ui/account_sessions_state.svelte.d.ts +53 -3
  187. package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
  188. package/dist/ui/account_sessions_state.svelte.js +39 -16
  189. package/dist/ui/admin_accounts_state.svelte.d.ts +118 -2
  190. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  191. package/dist/ui/admin_accounts_state.svelte.js +99 -23
  192. package/dist/ui/admin_invites_state.svelte.d.ts +47 -1
  193. package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
  194. package/dist/ui/admin_invites_state.svelte.js +38 -26
  195. package/dist/ui/admin_rpc_adapters.d.ts +94 -0
  196. package/dist/ui/admin_rpc_adapters.d.ts.map +1 -0
  197. package/dist/ui/admin_rpc_adapters.js +100 -0
  198. package/dist/ui/admin_sessions_state.svelte.d.ts +26 -0
  199. package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
  200. package/dist/ui/admin_sessions_state.svelte.js +35 -21
  201. package/dist/ui/app_settings_state.svelte.d.ts +39 -0
  202. package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
  203. package/dist/ui/app_settings_state.svelte.js +34 -18
  204. package/dist/ui/audit_log_state.svelte.d.ts +40 -3
  205. package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
  206. package/dist/ui/audit_log_state.svelte.js +36 -42
  207. package/dist/ui/auth_state.svelte.d.ts +4 -3
  208. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  209. package/dist/ui/auth_state.svelte.js +4 -1
  210. package/dist/ui/permit_offers_state.svelte.d.ts +125 -0
  211. package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -0
  212. package/dist/ui/permit_offers_state.svelte.js +197 -0
  213. package/package.json +3 -3
  214. package/dist/auth/admin_routes.d.ts +0 -29
  215. package/dist/auth/admin_routes.d.ts.map +0 -1
  216. package/dist/auth/admin_routes.js +0 -226
  217. package/dist/auth/app_settings_routes.d.ts +0 -27
  218. package/dist/auth/app_settings_routes.d.ts.map +0 -1
  219. package/dist/auth/app_settings_routes.js +0 -66
  220. package/dist/auth/invite_routes.d.ts +0 -18
  221. package/dist/auth/invite_routes.d.ts.map +0 -1
  222. package/dist/auth/invite_routes.js +0 -129
@@ -3,11 +3,12 @@ import './assert_dev_env.js';
3
3
  * Standard admin integration test suite for fuz_app admin routes.
4
4
  *
5
5
  * `describe_standard_admin_integration_tests` creates a composable test suite
6
- * that exercises admin account listing, permit grant/revoke, session/token
6
+ * that exercises admin account listing, permit grant/revoke (via the RPC
7
+ * surface — see `permit_offer_create` / `permit_revoke`), session/token
7
8
  * management, and audit log routes against a real PGlite database.
8
9
  *
9
- * Consumers call it with their route factory, session config, and role schema
10
- * all admin route tests come for free.
10
+ * Consumers call it with their route factory, session config, role schema,
11
+ * and RPC endpoint specs — all admin route tests come for free.
11
12
  *
12
13
  * @module
13
14
  */
@@ -19,16 +20,13 @@ import { create_pglite_factory, create_describe_db, AUTH_INTEGRATION_TRUNCATE_TA
19
20
  import { find_auth_route, assert_response_matches_spec } from './integration_helpers.js';
20
21
  import { run_migrations } from '../db/migrate.js';
21
22
  import { ErrorCoverageCollector, assert_error_coverage, DEFAULT_INTEGRATION_ERROR_COVERAGE, } from './error_coverage.js';
22
- /**
23
- * Find an admin route by suffix, method, and role requirement.
24
- *
25
- * Disambiguates admin routes (e.g., `GET /admin/sessions`) from account-scoped
26
- * routes (e.g., `GET /account/sessions`) by checking `auth.type === 'role'`.
27
- */
28
- const find_admin_route = (specs, suffix, method) => specs.find((s) => s.method === method &&
29
- s.path.endsWith(suffix) &&
30
- s.auth.type === 'role' &&
31
- s.auth.role === 'admin');
23
+ import { rpc_call, rpc_call_non_browser, require_rpc_endpoint_path } from './rpc_helpers.js';
24
+ import { permit_offer_create_action_spec, permit_revoke_action_spec, } from '../auth/permit_offer_action_specs.js';
25
+ import { admin_account_list_action_spec, admin_session_list_action_spec, admin_session_revoke_all_action_spec, admin_token_revoke_all_action_spec, audit_log_list_action_spec, audit_log_permit_history_action_spec, } from '../auth/admin_action_specs.js';
26
+ import { account_token_create_action_spec, account_verify_action_spec, } from '../auth/account_action_specs.js';
27
+ import { query_grant_permit } from '../auth/permit_queries.js';
28
+ import { query_actor_by_account } from '../auth/account_queries.js';
29
+ import { query_accept_offer } from '../auth/permit_offer_queries.js';
32
30
  /**
33
31
  * Pick a web-grantable role for testing, preferring a non-admin app-defined role.
34
32
  */
@@ -47,21 +45,24 @@ const build_admin_test_app_options = (options, db, roles) => ({
47
45
  create_route_specs: options.create_route_specs,
48
46
  db,
49
47
  roles: roles ?? [ROLE_KEEPER, ROLE_ADMIN],
50
- app_options: options.app_options,
48
+ app_options: {
49
+ rpc_endpoints: options.rpc_endpoints,
50
+ ...options.app_options,
51
+ },
51
52
  });
52
53
  /**
53
54
  * Standard admin integration test suite for fuz_app admin routes.
54
55
  *
55
- * Exercises account listing, permit grant/revoke, session management, token
56
- * management, audit log routes, admin-to-admin isolation, and response
57
- * schema validation.
58
- *
59
- * Each test group asserts that required routes exist, failing with a descriptive
60
- * message if the consumer's route specs are misconfigured.
56
+ * Exercises account listing, permit grant/revoke (via RPC), session
57
+ * management, token management, audit log routes, admin-to-admin isolation,
58
+ * and response schema validation.
61
59
  *
62
- * @param options - session config, route factory, and role schema
60
+ * @param options - session config, route factory, role schema, RPC endpoints
63
61
  */
64
62
  export const describe_standard_admin_integration_tests = (options) => {
63
+ // Hard-fail early so consumers see a clear setup error instead of a
64
+ // confusing test failure when `rpc_endpoints` is missing.
65
+ const rpc_path = require_rpc_endpoint_path(options.rpc_endpoints);
65
66
  const init_schema = async (db) => {
66
67
  await run_migrations(db, [AUTH_MIGRATION_NS]);
67
68
  };
@@ -76,20 +77,13 @@ export const describe_standard_admin_integration_tests = (options) => {
76
77
  let captured_route_specs = null;
77
78
  afterAll(() => {
78
79
  if (captured_route_specs) {
79
- // Scope coverage to admin auth-related routes.
80
- const admin_suffixes = [
81
- '/accounts',
82
- '/permits/grant',
83
- '/sessions',
84
- '/sessions/revoke-all',
85
- '/tokens/revoke-all',
86
- '/audit-log',
87
- '/audit-log/permit-history',
88
- '/invites',
89
- ];
90
- const admin_routes = captured_route_specs.filter((s) => (admin_suffixes.some((suffix) => s.path.endsWith(suffix)) ||
91
- s.path.includes('/permits/:') ||
92
- s.path.includes('/invites/:')) &&
80
+ // Scope coverage to admin auth-related routes. Post-2026-04-23
81
+ // RPC migration: account listing, session/token revoke-all,
82
+ // audit-log reads, and invite CRUD are RPC-only. The only
83
+ // admin REST route remaining is the optional
84
+ // `GET /audit-log/stream` SSE, plus the shared RPC endpoint
85
+ // path itself (admin methods live behind spec-level role auth).
86
+ const admin_routes = captured_route_specs.filter((s) => s.path.endsWith('/audit-log/stream') &&
93
87
  s.auth.type === 'role' &&
94
88
  s.auth.role === 'admin');
95
89
  assert_error_coverage(error_collector, admin_routes.length > 0 ? admin_routes : captured_route_specs, { min_coverage: DEFAULT_INTEGRATION_ERROR_COVERAGE });
@@ -102,279 +96,134 @@ export const describe_standard_admin_integration_tests = (options) => {
102
96
  cookie: `${cookie_name}=${session_cookie}`,
103
97
  ...extra,
104
98
  });
105
- // --- 1. Admin account listing ---
99
+ /**
100
+ * Drive the full consent flow (admin offer → recipient accept) and
101
+ * return the materialized permit id. Accept is a direct transactional
102
+ * `query_accept_offer` call because the suite focuses on the admin
103
+ * side; exercising the recipient's UI-wired accept path is covered by
104
+ * `describe_rpc_round_trip_tests` + fuz_app's own action suite.
105
+ */
106
+ const offer_and_accept = async (args) => {
107
+ const res = await rpc_call({
108
+ app: args.app,
109
+ path: rpc_path,
110
+ method: permit_offer_create_action_spec.method,
111
+ params: { to_account_id: args.to_account_id, role: args.role },
112
+ headers: args.admin_headers,
113
+ });
114
+ assert.ok(res.ok, `permit_offer_create failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
115
+ const offer = res.result.offer;
116
+ const accept_result = await get_db().transaction(async (tx) => query_accept_offer({ db: tx }, { offer_id: offer.id, to_account_id: args.to_account_id, ip: null }));
117
+ return { offer_id: offer.id, permit_id: accept_result.permit.id };
118
+ };
119
+ // --- 1. Admin account listing (RPC) ---
106
120
  describe('admin account listing', () => {
107
121
  test('admin can list all accounts', async () => {
108
122
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
109
- const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
110
- assert.ok(accounts_route, 'Expected admin GET /accounts route — ensure create_route_specs includes admin routes');
111
123
  const user_two = await test_app.create_account({ username: 'user_two' });
112
- const res = await test_app.app.request(accounts_route.path, {
124
+ const res = await rpc_call({
125
+ app: test_app.app,
126
+ path: rpc_path,
127
+ method: admin_account_list_action_spec.method,
113
128
  headers: test_app.create_session_headers(),
114
129
  });
115
- assert.strictEqual(res.status, 200);
116
- const body = await res.json();
117
- assert.ok(Array.isArray(body.accounts), 'Expected accounts array');
118
- assert.ok(body.accounts.length >= 2, 'Expected at least 2 accounts');
119
- assert.ok(Array.isArray(body.grantable_roles), 'Expected grantable_roles array');
130
+ assert.ok(res.ok, `admin_account_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
131
+ const result = res.result;
132
+ assert.ok(Array.isArray(result.accounts), 'Expected accounts array');
133
+ assert.ok(result.accounts.length >= 2, 'Expected at least 2 accounts');
134
+ assert.ok(Array.isArray(result.grantable_roles), 'Expected grantable_roles array');
120
135
  // Verify user_two appears in the listing
121
- const found = body.accounts.find((e) => e.account.id === user_two.account.id);
136
+ const found = result.accounts.find((e) => e.account.id === user_two.account.id);
122
137
  assert.ok(found, 'Expected user_two in accounts listing');
123
138
  });
124
139
  test('non-admin cannot list accounts', async () => {
125
140
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db(), [ROLE_KEEPER]));
126
141
  captured_route_specs ??= test_app.route_specs;
127
- const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
128
- assert.ok(accounts_route, 'Expected admin GET /accounts route — ensure create_route_specs includes admin routes');
129
- const res = await test_app.app.request(accounts_route.path, {
142
+ const res = await rpc_call({
143
+ app: test_app.app,
144
+ path: rpc_path,
145
+ method: admin_account_list_action_spec.method,
130
146
  headers: test_app.create_session_headers(),
131
147
  });
148
+ assert.ok(!res.ok, 'Expected admin_account_list to fail for non-admin');
132
149
  assert.strictEqual(res.status, 403);
133
- const body = await res.clone().json();
134
- assert.strictEqual(body.error, 'insufficient_permissions');
135
- await error_collector.assert_and_record(test_app.route_specs, 'GET', accounts_route.path, res);
136
- });
137
- });
138
- // --- 2. Permit grant lifecycle ---
139
- describe('permit grant lifecycle', () => {
140
- test('admin can grant a web-grantable role', async () => {
141
- const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
142
- const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
143
- assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
144
- const user_two = await test_app.create_account({ username: 'user_two' });
145
- const path = grant_route.path.replace(':account_id', user_two.account.id);
146
- const res = await test_app.app.request(path, {
147
- method: 'POST',
148
- headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
149
- body: JSON.stringify({ role: grantable_role }),
150
- });
151
- assert.strictEqual(res.status, 200);
152
- const body = await res.json();
153
- assert.strictEqual(body.ok, true);
154
- assert.ok(body.permit);
155
- assert.strictEqual(body.permit.role, grantable_role);
156
- assert.ok(body.permit.id, 'Expected permit id');
157
- });
158
- test('admin cannot grant a non-web-grantable role', async () => {
159
- const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
160
- const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
161
- assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
162
- const user_two = await test_app.create_account({ username: 'user_two' });
163
- const path = grant_route.path.replace(':account_id', user_two.account.id);
164
- const res = await test_app.app.request(path, {
165
- method: 'POST',
166
- headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
167
- body: JSON.stringify({ role: ROLE_KEEPER }),
168
- });
169
- assert.strictEqual(res.status, 403);
170
- const body = await res.clone().json();
171
- assert.strictEqual(body.error, 'role_not_web_grantable');
172
- await error_collector.assert_and_record(test_app.route_specs, 'POST', grant_route.path, res);
173
- });
174
- test('granting same role twice is idempotent (returns same permit)', async () => {
175
- const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
176
- const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
177
- assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
178
- const user_two = await test_app.create_account({ username: 'user_two' });
179
- const path = grant_route.path.replace(':account_id', user_two.account.id);
180
- const headers = test_app.create_session_headers({ 'content-type': 'application/json' });
181
- const body = JSON.stringify({ role: grantable_role });
182
- // First grant
183
- const res1 = await test_app.app.request(path, {
184
- method: 'POST',
185
- headers,
186
- body,
187
- });
188
- assert.strictEqual(res1.status, 200);
189
- const body1 = await res1.json();
190
- assert.strictEqual(body1.ok, true);
191
- const permit_id_1 = body1.permit.id;
192
- // Second grant — same role, same account
193
- const res2 = await test_app.app.request(path, {
194
- method: 'POST',
195
- headers,
196
- body,
197
- });
198
- assert.strictEqual(res2.status, 200);
199
- const body2 = await res2.json();
200
- assert.strictEqual(body2.ok, true);
201
- assert.strictEqual(body2.permit.id, permit_id_1, 'Expected same permit ID on idempotent grant');
202
- assert.strictEqual(body2.permit.role, grantable_role);
203
- });
204
- test('grant with unknown role returns 400', async () => {
205
- const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
206
- const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
207
- assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
208
- const user_two = await test_app.create_account({ username: 'user_two' });
209
- const path = grant_route.path.replace(':account_id', user_two.account.id);
210
- const res = await test_app.app.request(path, {
211
- method: 'POST',
212
- headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
213
- body: JSON.stringify({ role: 'nonexistent_role' }),
214
- });
215
- assert.strictEqual(res.status, 400);
216
- error_collector.record(test_app.route_specs, 'POST', grant_route.path, 400);
217
- });
218
- test('grant to nonexistent account returns 404', async () => {
219
- const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
220
- const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
221
- assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
222
- const fake_id = '00000000-0000-0000-0000-000000000000';
223
- const path = grant_route.path.replace(':account_id', fake_id);
224
- const res = await test_app.app.request(path, {
225
- method: 'POST',
226
- headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
227
- body: JSON.stringify({ role: grantable_role }),
228
- });
229
- assert.strictEqual(res.status, 404);
230
- const body = await res.clone().json();
231
- assert.strictEqual(body.error, 'account_not_found');
232
- await error_collector.assert_and_record(test_app.route_specs, 'POST', grant_route.path, res);
233
- });
234
- test('admin can revoke a permit', async () => {
235
- const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
236
- const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
237
- const revoke_route = find_admin_route(test_app.route_specs, '/permits/:permit_id/revoke', 'POST');
238
- const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
239
- assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
240
- assert.ok(revoke_route, 'Expected admin POST /permits/:permit_id/revoke route — ensure create_route_specs includes admin routes');
241
- assert.ok(accounts_route, 'Expected admin GET /accounts route — ensure create_route_specs includes admin routes');
242
- const user_two = await test_app.create_account({ username: 'user_two' });
243
- const admin_headers = test_app.create_session_headers({ 'content-type': 'application/json' });
244
- // Grant
245
- const grant_path = grant_route.path.replace(':account_id', user_two.account.id);
246
- await test_app.app.request(grant_path, {
247
- method: 'POST',
248
- headers: admin_headers,
249
- body: JSON.stringify({ role: grantable_role }),
250
- });
251
- // Find the permit ID via account listing
252
- const list_res = await test_app.app.request(accounts_route.path, {
253
- headers: test_app.create_session_headers(),
254
- });
255
- const list_body = await list_res.json();
256
- const entry = list_body.accounts.find((e) => e.account.id === user_two.account.id);
257
- const permit = entry.permits.find((p) => p.role === grantable_role);
258
- assert.ok(permit, 'Expected granted permit in listing');
259
- // Revoke
260
- const revoke_path = revoke_route.path
261
- .replace(':account_id', user_two.account.id)
262
- .replace(':permit_id', permit.id);
263
- const revoke_res = await test_app.app.request(revoke_path, {
264
- method: 'POST',
265
- headers: test_app.create_session_headers(),
266
- });
267
- assert.strictEqual(revoke_res.status, 200);
268
- const revoke_body = await revoke_res.json();
269
- assert.strictEqual(revoke_body.ok, true);
270
- assert.strictEqual(revoke_body.revoked, true);
271
- });
272
- test('revoking an already-revoked permit returns 404', async () => {
273
- const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
274
- const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
275
- const revoke_route = find_admin_route(test_app.route_specs, '/permits/:permit_id/revoke', 'POST');
276
- const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
277
- assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
278
- assert.ok(revoke_route, 'Expected admin POST /permits/:permit_id/revoke route — ensure create_route_specs includes admin routes');
279
- assert.ok(accounts_route, 'Expected admin GET /accounts route — ensure create_route_specs includes admin routes');
280
- const user_two = await test_app.create_account({ username: 'user_two' });
281
- const admin_headers = test_app.create_session_headers({ 'content-type': 'application/json' });
282
- // Grant
283
- const grant_path = grant_route.path.replace(':account_id', user_two.account.id);
284
- await test_app.app.request(grant_path, {
285
- method: 'POST',
286
- headers: admin_headers,
287
- body: JSON.stringify({ role: grantable_role }),
288
- });
289
- // Find permit ID
290
- const list_res = await test_app.app.request(accounts_route.path, {
291
- headers: test_app.create_session_headers(),
292
- });
293
- const list_body = await list_res.json();
294
- const entry = list_body.accounts.find((e) => e.account.id === user_two.account.id);
295
- const permit = entry.permits.find((p) => p.role === grantable_role);
296
- assert.ok(permit);
297
- const revoke_path = revoke_route.path
298
- .replace(':account_id', user_two.account.id)
299
- .replace(':permit_id', permit.id);
300
- // First revoke — succeeds
301
- const first = await test_app.app.request(revoke_path, {
302
- method: 'POST',
303
- headers: test_app.create_session_headers(),
304
- });
305
- assert.strictEqual(first.status, 200);
306
- // Second revoke — already revoked, returns 404
307
- const second = await test_app.app.request(revoke_path, {
308
- method: 'POST',
309
- headers: test_app.create_session_headers(),
310
- });
311
- assert.strictEqual(second.status, 404);
312
- const body = await second.clone().json();
313
- assert.strictEqual(body.error, 'permit_not_found');
314
- await error_collector.assert_and_record(test_app.route_specs, 'POST', revoke_route.path, second);
315
150
  });
316
151
  });
152
+ // --- 2. Permit grant/revoke lifecycle ---
153
+ // Permit grant/revoke are RPC-only (see `permit_offer_create` /
154
+ // `permit_revoke`). End-to-end coverage lives in
155
+ // `describe_rpc_round_trip_tests` + fuz_app's own
156
+ // `permit_offer_actions.db.test.ts` /
157
+ // `permit_offer_actions.notifications.revoke.db.test.ts`. The
158
+ // audit/isolation groups below exercise them as preconditions for
159
+ // cross-cutting checks (event emission, admin-to-admin isolation).
317
160
  // --- 3. Admin session management ---
318
161
  describe('admin session management', () => {
319
162
  test('admin can list all active sessions', async () => {
320
163
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
321
- const sessions_route = find_admin_route(test_app.route_specs, '/sessions', 'GET');
322
- assert.ok(sessions_route, 'Expected admin GET /sessions route — ensure create_route_specs includes admin routes');
323
164
  await test_app.create_account({ username: 'user_two' });
324
- const res = await test_app.app.request(sessions_route.path, {
165
+ const res = await rpc_call({
166
+ app: test_app.app,
167
+ path: rpc_path,
168
+ method: admin_session_list_action_spec.method,
325
169
  headers: test_app.create_session_headers(),
326
170
  });
327
- assert.strictEqual(res.status, 200);
328
- const body = await res.json();
171
+ assert.ok(res.ok, `admin_session_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
172
+ const body = res.result;
329
173
  assert.ok(Array.isArray(body.sessions), 'Expected sessions array');
330
174
  assert.ok(body.sessions.length >= 2, 'Expected sessions from multiple accounts');
331
175
  });
332
176
  test('admin can revoke all sessions for another account', async () => {
333
177
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
334
- const revoke_sessions_route = find_admin_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
335
- const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
336
- assert.ok(revoke_sessions_route, 'Expected admin POST /sessions/revoke-all route — ensure create_route_specs includes admin routes');
337
- assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
338
178
  const user_two = await test_app.create_account({ username: 'user_two' });
339
- // Verify user_two's session works
340
- const before = await test_app.app.request(verify_route.path, {
179
+ // Verify user_two's session works via `account_verify` RPC
180
+ const before = await rpc_call({
181
+ app: test_app.app,
182
+ path: rpc_path,
183
+ method: account_verify_action_spec.method,
341
184
  headers: create_headers(user_two.session_cookie),
342
185
  });
343
186
  assert.strictEqual(before.status, 200);
344
- // Admin revokes all sessions for user_two
345
- const path = revoke_sessions_route.path.replace(':account_id', user_two.account.id);
346
- const res = await test_app.app.request(path, {
347
- method: 'POST',
187
+ // Admin revokes all sessions for user_two via RPC
188
+ const res = await rpc_call({
189
+ app: test_app.app,
190
+ path: rpc_path,
191
+ method: admin_session_revoke_all_action_spec.method,
192
+ params: { account_id: user_two.account.id },
348
193
  headers: test_app.create_session_headers(),
349
194
  });
350
- assert.strictEqual(res.status, 200);
351
- const body = await res.json();
352
- assert.strictEqual(body.ok, true);
353
- assert.ok(body.count >= 1, 'Expected at least 1 revoked session');
195
+ assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
196
+ const result = res.result;
197
+ assert.strictEqual(result.ok, true);
198
+ assert.ok(result.count >= 1, 'Expected at least 1 revoked session');
354
199
  // Verify user_two's session no longer works
355
- const after = await test_app.app.request(verify_route.path, {
200
+ const after = await rpc_call({
201
+ app: test_app.app,
202
+ path: rpc_path,
203
+ method: account_verify_action_spec.method,
356
204
  headers: create_headers(user_two.session_cookie),
357
205
  });
358
206
  assert.strictEqual(after.status, 401);
359
207
  });
360
208
  test('admin revoking own sessions invalidates own session', async () => {
361
209
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
362
- const revoke_sessions_route = find_admin_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
363
- const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
364
- assert.ok(revoke_sessions_route, 'Expected admin POST /sessions/revoke-all route — ensure create_route_specs includes admin routes');
365
- assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
366
- // Admin revokes own sessions
367
- const path = revoke_sessions_route.path.replace(':account_id', test_app.backend.account.id);
368
- const res = await test_app.app.request(path, {
369
- method: 'POST',
210
+ // Admin revokes own sessions via RPC
211
+ const res = await rpc_call({
212
+ app: test_app.app,
213
+ path: rpc_path,
214
+ method: admin_session_revoke_all_action_spec.method,
215
+ params: { account_id: test_app.backend.account.id },
370
216
  headers: test_app.create_session_headers(),
371
217
  });
372
- assert.strictEqual(res.status, 200);
373
- const body = await res.json();
374
- assert.strictEqual(body.ok, true);
375
- assert.ok(body.count >= 1, 'Expected at least 1 revoked session');
218
+ assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
219
+ const result = res.result;
220
+ assert.strictEqual(result.ok, true);
221
+ assert.ok(result.count >= 1, 'Expected at least 1 revoked session');
376
222
  // Admin's own session should no longer work
377
- const after = await test_app.app.request(verify_route.path, {
223
+ const after = await rpc_call({
224
+ app: test_app.app,
225
+ path: rpc_path,
226
+ method: account_verify_action_spec.method,
378
227
  headers: test_app.create_session_headers(),
379
228
  });
380
229
  assert.strictEqual(after.status, 401);
@@ -384,92 +233,97 @@ export const describe_standard_admin_integration_tests = (options) => {
384
233
  describe('admin token management', () => {
385
234
  test('admin can revoke all tokens for another account', async () => {
386
235
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
387
- const revoke_tokens_route = find_admin_route(test_app.route_specs, '/tokens/revoke-all', 'POST');
388
- const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
389
- assert.ok(revoke_tokens_route, 'Expected admin POST /tokens/revoke-all route — ensure create_route_specs includes admin routes');
390
- assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
391
236
  const user_two = await test_app.create_account({ username: 'user_two' });
392
- // Verify user_two's bearer token works
393
- const before = await test_app.app.request(verify_route.path, {
394
- headers: { host: 'localhost', authorization: `Bearer ${user_two.api_token}` },
237
+ // Verify user_two's bearer token works via `account_verify` RPC
238
+ const before = await rpc_call_non_browser({
239
+ app: test_app.app,
240
+ path: rpc_path,
241
+ method: account_verify_action_spec.method,
242
+ headers: { authorization: `Bearer ${user_two.api_token}` },
395
243
  });
396
244
  assert.strictEqual(before.status, 200);
397
- // Admin revokes all tokens for user_two
398
- const path = revoke_tokens_route.path.replace(':account_id', user_two.account.id);
399
- const res = await test_app.app.request(path, {
400
- method: 'POST',
245
+ // Admin revokes all tokens for user_two via RPC
246
+ const res = await rpc_call({
247
+ app: test_app.app,
248
+ path: rpc_path,
249
+ method: admin_token_revoke_all_action_spec.method,
250
+ params: { account_id: user_two.account.id },
401
251
  headers: test_app.create_session_headers(),
402
252
  });
403
- assert.strictEqual(res.status, 200);
404
- const body = await res.json();
405
- assert.strictEqual(body.ok, true);
406
- assert.ok(body.count >= 1, 'Expected at least 1 revoked token');
253
+ assert.ok(res.ok, `admin_token_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
254
+ const result = res.result;
255
+ assert.strictEqual(result.ok, true);
256
+ assert.ok(result.count >= 1, 'Expected at least 1 revoked token');
407
257
  // Verify user_two's bearer token no longer works
408
- const after = await test_app.app.request(verify_route.path, {
409
- headers: { host: 'localhost', authorization: `Bearer ${user_two.api_token}` },
258
+ const after = await rpc_call_non_browser({
259
+ app: test_app.app,
260
+ path: rpc_path,
261
+ method: account_verify_action_spec.method,
262
+ headers: { authorization: `Bearer ${user_two.api_token}` },
410
263
  });
411
264
  assert.strictEqual(after.status, 401);
412
265
  });
413
266
  });
414
- // --- 5. Audit log routes ---
415
- describe('audit log routes', () => {
267
+ // --- 5. Audit log RPC reads ---
268
+ describe('audit log RPC reads', () => {
416
269
  test('admin can list audit log events', async () => {
417
270
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
418
- const audit_route = find_admin_route(test_app.route_specs, '/audit-log', 'GET');
419
- assert.ok(audit_route, 'Expected admin GET /audit-log route — ensure create_route_specs includes admin routes');
420
- const res = await test_app.app.request(audit_route.path, {
271
+ const res = await rpc_call({
272
+ app: test_app.app,
273
+ path: rpc_path,
274
+ method: audit_log_list_action_spec.method,
421
275
  headers: test_app.create_session_headers(),
422
276
  });
423
- assert.strictEqual(res.status, 200);
424
- const body = await res.json();
277
+ assert.ok(res.ok, `audit_log_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
278
+ const body = res.result;
425
279
  assert.ok(Array.isArray(body.events), 'Expected events array');
426
280
  });
427
281
  test('audit log supports event_type filter', async () => {
428
282
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
429
- const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
430
- const audit_route = find_admin_route(test_app.route_specs, '/audit-log', 'GET');
431
- assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
432
- assert.ok(audit_route, 'Expected admin GET /audit-log route — ensure create_route_specs includes admin routes');
433
- // Create a grant to produce an audit event
283
+ // Admin offer emits `permit_offer_create`. The downstream
284
+ // `permit_grant` only fires on accept — out of scope for this test.
434
285
  const user_two = await test_app.create_account({ username: 'user_two' });
435
- const grant_path = grant_route.path.replace(':account_id', user_two.account.id);
436
- await test_app.app.request(grant_path, {
437
- method: 'POST',
438
- headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
439
- body: JSON.stringify({ role: grantable_role }),
286
+ const offer_res = await rpc_call({
287
+ app: test_app.app,
288
+ path: rpc_path,
289
+ method: permit_offer_create_action_spec.method,
290
+ params: { to_account_id: user_two.account.id, role: grantable_role },
291
+ headers: test_app.create_session_headers(),
440
292
  });
441
- // Filter by event_type
442
- const res = await test_app.app.request(`${audit_route.path}?event_type=permit_grant`, {
293
+ assert.ok(offer_res.ok, 'permit_offer_create should succeed');
294
+ const res = await rpc_call({
295
+ app: test_app.app,
296
+ path: rpc_path,
297
+ method: audit_log_list_action_spec.method,
298
+ params: { event_type: 'permit_offer_create' },
443
299
  headers: test_app.create_session_headers(),
444
300
  });
445
- assert.strictEqual(res.status, 200);
446
- const body = await res.json();
447
- assert.ok(Array.isArray(body.events));
448
- assert.ok(body.events.length >= 1, 'Expected at least 1 permit_grant event');
301
+ assert.ok(res.ok, `audit_log_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
302
+ const body = res.result;
303
+ assert.ok(body.events.length >= 1, 'Expected at least 1 permit_offer_create event');
449
304
  for (const event of body.events) {
450
- assert.strictEqual(event.event_type, 'permit_grant');
305
+ assert.strictEqual(event.event_type, 'permit_offer_create');
451
306
  }
452
307
  });
453
308
  test('admin can view permit history', async () => {
454
309
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
455
- const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
456
- const history_route = find_admin_route(test_app.route_specs, '/audit-log/permit-history', 'GET');
457
- assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
458
- assert.ok(history_route, 'Expected admin GET /audit-log/permit-history route — ensure create_route_specs includes admin routes');
459
- // Create a grant to produce audit data
310
+ // Drive the full consent flow so `permit_grant` lands in the audit log
311
+ // `query_audit_log_list_permit_history` filters to (permit_grant, permit_revoke).
460
312
  const user_two = await test_app.create_account({ username: 'user_two' });
461
- const grant_path = grant_route.path.replace(':account_id', user_two.account.id);
462
- await test_app.app.request(grant_path, {
463
- method: 'POST',
464
- headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
465
- body: JSON.stringify({ role: grantable_role }),
466
- });
467
- const res = await test_app.app.request(history_route.path, {
313
+ await offer_and_accept({
314
+ app: test_app.app,
315
+ admin_headers: test_app.create_session_headers(),
316
+ to_account_id: user_two.account.id,
317
+ role: grantable_role,
318
+ });
319
+ const res = await rpc_call({
320
+ app: test_app.app,
321
+ path: rpc_path,
322
+ method: audit_log_permit_history_action_spec.method,
468
323
  headers: test_app.create_session_headers(),
469
324
  });
470
- assert.strictEqual(res.status, 200);
471
- const body = await res.json();
472
- assert.ok(Array.isArray(body.events), 'Expected events array');
325
+ assert.ok(res.ok, `audit_log_permit_history failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
326
+ const body = res.result;
473
327
  assert.ok(body.events.length >= 1, 'Expected at least 1 permit history event');
474
328
  });
475
329
  });
@@ -477,85 +331,147 @@ export const describe_standard_admin_integration_tests = (options) => {
477
331
  describe('admin audit trail', () => {
478
332
  test('permit revoke creates audit event', async () => {
479
333
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
480
- const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
481
- const revoke_route = find_admin_route(test_app.route_specs, '/permits/:permit_id/revoke', 'POST');
482
- const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
483
- const audit_route = find_admin_route(test_app.route_specs, '/audit-log', 'GET');
484
- assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
485
- assert.ok(revoke_route, 'Expected admin POST /permits/:permit_id/revoke route — ensure create_route_specs includes admin routes');
486
- assert.ok(accounts_route, 'Expected admin GET /accounts route — ensure create_route_specs includes admin routes');
487
- assert.ok(audit_route, 'Expected admin GET /audit-log route — ensure create_route_specs includes admin routes');
488
334
  const user_two = await test_app.create_account({ username: 'user_two' });
489
- const admin_headers = test_app.create_session_headers({ 'content-type': 'application/json' });
490
- // Grant a role
491
- const grant_path = grant_route.path.replace(':account_id', user_two.account.id);
492
- await test_app.app.request(grant_path, {
493
- method: 'POST',
494
- headers: admin_headers,
495
- body: JSON.stringify({ role: grantable_role }),
496
- });
497
- // Find the permit ID
498
- const list_res = await test_app.app.request(accounts_route.path, {
335
+ const target_actor = await query_actor_by_account({ db: get_db() }, user_two.account.id);
336
+ assert.ok(target_actor);
337
+ const permit = await query_grant_permit({ db: get_db() }, {
338
+ actor_id: target_actor.id,
339
+ role: grantable_role,
340
+ granted_by: test_app.backend.actor.id,
341
+ });
342
+ // Revoke via RPC
343
+ const revoke_res = await rpc_call({
344
+ app: test_app.app,
345
+ path: rpc_path,
346
+ method: permit_revoke_action_spec.method,
347
+ params: { actor_id: target_actor.id, permit_id: permit.id },
499
348
  headers: test_app.create_session_headers(),
500
349
  });
501
- const list_body = await list_res.json();
502
- const entry = list_body.accounts.find((e) => e.account.id === user_two.account.id);
503
- const permit = entry.permits.find((p) => p.role === grantable_role);
504
- // Revoke the permit
505
- const revoke_path = revoke_route.path
506
- .replace(':account_id', user_two.account.id)
507
- .replace(':permit_id', permit.id);
508
- await test_app.app.request(revoke_path, {
509
- method: 'POST',
350
+ assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
351
+ // Check audit log for permit_revoke event
352
+ const audit_res = await rpc_call({
353
+ app: test_app.app,
354
+ path: rpc_path,
355
+ method: audit_log_list_action_spec.method,
356
+ params: { event_type: 'permit_revoke' },
510
357
  headers: test_app.create_session_headers(),
511
358
  });
512
- // Check audit log for permit_revoke event
513
- const audit_res = await test_app.app.request(`${audit_route.path}?event_type=permit_revoke`, { headers: test_app.create_session_headers() });
514
- assert.strictEqual(audit_res.status, 200);
515
- const audit_body = await audit_res.json();
359
+ assert.ok(audit_res.ok, `audit_log_list failed: ${audit_res.ok ? '' : JSON.stringify(audit_res.error)}`);
360
+ const audit_body = audit_res.result;
516
361
  assert.ok(audit_body.events.length >= 1, 'Expected permit_revoke audit event');
517
362
  assert.strictEqual(audit_body.events[0].event_type, 'permit_revoke');
518
363
  });
519
364
  test('admin session revoke-all creates audit event', async () => {
520
365
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
521
- const revoke_sessions_route = find_admin_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
522
- const audit_route = find_admin_route(test_app.route_specs, '/audit-log', 'GET');
523
- assert.ok(revoke_sessions_route, 'Expected admin POST /sessions/revoke-all route — ensure create_route_specs includes admin routes');
524
- assert.ok(audit_route, 'Expected admin GET /audit-log route — ensure create_route_specs includes admin routes');
525
366
  const user_two = await test_app.create_account({ username: 'user_two' });
526
- // Revoke all sessions for user_two
527
- const path = revoke_sessions_route.path.replace(':account_id', user_two.account.id);
528
- await test_app.app.request(path, {
529
- method: 'POST',
367
+ // Revoke all sessions for user_two via RPC
368
+ const revoke_res = await rpc_call({
369
+ app: test_app.app,
370
+ path: rpc_path,
371
+ method: admin_session_revoke_all_action_spec.method,
372
+ params: { account_id: user_two.account.id },
530
373
  headers: test_app.create_session_headers(),
531
374
  });
375
+ assert.ok(revoke_res.ok, `admin_session_revoke_all failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
532
376
  // Check audit log
533
- const audit_res = await test_app.app.request(`${audit_route.path}?event_type=session_revoke_all`, { headers: test_app.create_session_headers() });
534
- assert.strictEqual(audit_res.status, 200);
535
- const audit_body = await audit_res.json();
377
+ const audit_res = await rpc_call({
378
+ app: test_app.app,
379
+ path: rpc_path,
380
+ method: audit_log_list_action_spec.method,
381
+ params: { event_type: 'session_revoke_all' },
382
+ headers: test_app.create_session_headers(),
383
+ });
384
+ assert.ok(audit_res.ok, 'audit_log_list should succeed');
385
+ const audit_body = audit_res.result;
536
386
  assert.ok(audit_body.events.length >= 1, 'Expected session_revoke_all audit event');
537
387
  assert.strictEqual(audit_body.events[0].event_type, 'session_revoke_all');
538
388
  });
539
389
  test('admin token revoke-all creates audit event', async () => {
540
390
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
541
- const revoke_tokens_route = find_admin_route(test_app.route_specs, '/tokens/revoke-all', 'POST');
542
- const audit_route = find_admin_route(test_app.route_specs, '/audit-log', 'GET');
543
- assert.ok(revoke_tokens_route, 'Expected admin POST /tokens/revoke-all route — ensure create_route_specs includes admin routes');
544
- assert.ok(audit_route, 'Expected admin GET /audit-log route — ensure create_route_specs includes admin routes');
545
391
  const user_two = await test_app.create_account({ username: 'user_two' });
546
- // Revoke all tokens for user_two
547
- const path = revoke_tokens_route.path.replace(':account_id', user_two.account.id);
548
- await test_app.app.request(path, {
549
- method: 'POST',
392
+ // Revoke all tokens for user_two via RPC
393
+ const revoke_res = await rpc_call({
394
+ app: test_app.app,
395
+ path: rpc_path,
396
+ method: admin_token_revoke_all_action_spec.method,
397
+ params: { account_id: user_two.account.id },
550
398
  headers: test_app.create_session_headers(),
551
399
  });
400
+ assert.ok(revoke_res.ok, `admin_token_revoke_all failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
552
401
  // Check audit log
553
- const audit_res = await test_app.app.request(`${audit_route.path}?event_type=token_revoke_all`, { headers: test_app.create_session_headers() });
554
- assert.strictEqual(audit_res.status, 200);
555
- const audit_body = await audit_res.json();
402
+ const audit_res = await rpc_call({
403
+ app: test_app.app,
404
+ path: rpc_path,
405
+ method: audit_log_list_action_spec.method,
406
+ params: { event_type: 'token_revoke_all' },
407
+ headers: test_app.create_session_headers(),
408
+ });
409
+ assert.ok(audit_res.ok, 'audit_log_list should succeed');
410
+ const audit_body = audit_res.result;
556
411
  assert.ok(audit_body.events.length >= 1, 'Expected token_revoke_all audit event');
557
412
  assert.strictEqual(audit_body.events[0].event_type, 'token_revoke_all');
558
413
  });
414
+ test('admin session revoke-all 404 emits failure audit', async () => {
415
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
416
+ // `Uuid = z.uuid()` is v4-strict; use a valid v4 shape so we hit the
417
+ // handler's account lookup rather than failing at param validation.
418
+ const missing_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaa01';
419
+ const res = await rpc_call({
420
+ app: test_app.app,
421
+ path: rpc_path,
422
+ method: admin_session_revoke_all_action_spec.method,
423
+ params: { account_id: missing_id },
424
+ headers: test_app.create_session_headers(),
425
+ });
426
+ assert.ok(!res.ok, 'Expected 404 for missing account');
427
+ assert.strictEqual(res.status, 404);
428
+ assert.strictEqual(res.error.data.reason, 'account_not_found');
429
+ // Failure audit row should be visible on the audit-log feed.
430
+ // `target_account_id` is null (FK prevents referencing a missing id)
431
+ // — the probed id is preserved under `metadata.attempted_account_id`.
432
+ const audit_res = await rpc_call({
433
+ app: test_app.app,
434
+ path: rpc_path,
435
+ method: audit_log_list_action_spec.method,
436
+ params: { event_type: 'session_revoke_all' },
437
+ headers: test_app.create_session_headers(),
438
+ });
439
+ assert.ok(audit_res.ok, 'audit_log_list should succeed');
440
+ const audit_body = audit_res.result;
441
+ const failure = audit_body.events.find((e) => e.outcome === 'failure');
442
+ assert.ok(failure, 'Expected a failure-outcome session_revoke_all audit event');
443
+ assert.strictEqual(failure.target_account_id, null);
444
+ assert.strictEqual(failure.metadata.reason, 'account_not_found');
445
+ assert.strictEqual(failure.metadata.attempted_account_id, missing_id);
446
+ });
447
+ test('admin token revoke-all 404 emits failure audit', async () => {
448
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
449
+ const missing_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaa02';
450
+ const res = await rpc_call({
451
+ app: test_app.app,
452
+ path: rpc_path,
453
+ method: admin_token_revoke_all_action_spec.method,
454
+ params: { account_id: missing_id },
455
+ headers: test_app.create_session_headers(),
456
+ });
457
+ assert.ok(!res.ok, 'Expected 404 for missing account');
458
+ assert.strictEqual(res.status, 404);
459
+ assert.strictEqual(res.error.data.reason, 'account_not_found');
460
+ const audit_res = await rpc_call({
461
+ app: test_app.app,
462
+ path: rpc_path,
463
+ method: audit_log_list_action_spec.method,
464
+ params: { event_type: 'token_revoke_all' },
465
+ headers: test_app.create_session_headers(),
466
+ });
467
+ assert.ok(audit_res.ok, 'audit_log_list should succeed');
468
+ const audit_body = audit_res.result;
469
+ const failure = audit_body.events.find((e) => e.outcome === 'failure');
470
+ assert.ok(failure, 'Expected a failure-outcome token_revoke_all audit event');
471
+ assert.strictEqual(failure.target_account_id, null);
472
+ assert.strictEqual(failure.metadata.reason, 'account_not_found');
473
+ assert.strictEqual(failure.metadata.attempted_account_id, missing_id);
474
+ });
559
475
  });
560
476
  // --- 7. Audit log completeness ---
561
477
  describe('audit log completeness', () => {
@@ -563,26 +479,13 @@ export const describe_standard_admin_integration_tests = (options) => {
563
479
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
564
480
  const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
565
481
  const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
566
- const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
567
- const revoke_route = find_admin_route(test_app.route_specs, '/permits/:permit_id/revoke', 'POST');
568
- const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
569
- const create_token_route = find_auth_route(test_app.route_specs, '/tokens/create', 'POST');
570
482
  const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
571
- const audit_route = find_admin_route(test_app.route_specs, '/audit-log', 'GET');
572
- assert.ok(audit_route, 'Expected admin GET /audit-log route');
573
- // skip if required routes are missing (consumer may not wire all routes)
574
- if (!login_route ||
575
- !logout_route ||
576
- !grant_route ||
577
- !revoke_route ||
578
- !accounts_route ||
579
- !create_token_route ||
580
- !password_route)
483
+ // skip if required routes are missing (consumer may not wire all routes).
484
+ // Token creation goes through `account_token_create` RPC — always wired
485
+ // because `rpc_endpoints` is required at the suite level.
486
+ if (!login_route || !logout_route || !password_route)
581
487
  return;
582
488
  const user_two = await test_app.create_account({ username: 'audit_user' });
583
- const admin_headers = test_app.create_session_headers({
584
- 'content-type': 'application/json',
585
- });
586
489
  // 1. login (user_two logs in)
587
490
  const login_res = await test_app.app.request(login_route.path, {
588
491
  method: 'POST',
@@ -609,36 +512,35 @@ export const describe_standard_admin_integration_tests = (options) => {
609
512
  },
610
513
  });
611
514
  }
612
- // 3. grant permit (admin grants grantable_role to user_two)
613
- const grant_path = grant_route.path.replace(':account_id', user_two.account.id);
614
- await test_app.app.request(grant_path, {
615
- method: 'POST',
616
- headers: admin_headers,
617
- body: JSON.stringify({ role: grantable_role }),
618
- });
619
- // find permit ID
620
- const list_res = await test_app.app.request(accounts_route.path, {
515
+ // 3. offer permit (admin offers grantable_role to user_two) — full
516
+ // consentful flow: offer + accept so both `permit_offer_create` and
517
+ // `permit_grant` audit events land.
518
+ const { permit_id } = await offer_and_accept({
519
+ app: test_app.app,
520
+ admin_headers: test_app.create_session_headers(),
521
+ to_account_id: user_two.account.id,
522
+ role: grantable_role,
523
+ });
524
+ // 4. revoke permit (RPC)
525
+ const target_actor = await query_actor_by_account({ db: get_db() }, user_two.account.id);
526
+ assert.ok(target_actor);
527
+ const revoke_res = await rpc_call({
528
+ app: test_app.app,
529
+ path: rpc_path,
530
+ method: permit_revoke_action_spec.method,
531
+ params: { actor_id: target_actor.id, permit_id },
621
532
  headers: test_app.create_session_headers(),
622
533
  });
623
- const list_body = await list_res.json();
624
- const entry = list_body.accounts.find((e) => e.account.id === user_two.account.id);
625
- const permit = entry?.permits?.find((p) => p.role === grantable_role);
626
- // 4. revoke permit
627
- if (permit) {
628
- const rev_path = revoke_route.path
629
- .replace(':account_id', user_two.account.id)
630
- .replace(':permit_id', permit.id);
631
- await test_app.app.request(rev_path, {
632
- method: 'POST',
633
- headers: test_app.create_session_headers(),
634
- });
635
- }
636
- // 5. create token
637
- await test_app.app.request(create_token_route.path, {
638
- method: 'POST',
639
- headers: admin_headers,
640
- body: JSON.stringify({ name: 'audit-test-token' }),
534
+ assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
535
+ // 5. create token (RPC)
536
+ const token_res = await rpc_call({
537
+ app: test_app.app,
538
+ path: rpc_path,
539
+ method: account_token_create_action_spec.method,
540
+ params: { name: 'audit-test-token' },
541
+ headers: test_app.create_session_headers(),
641
542
  });
543
+ assert.ok(token_res.ok, `account_token_create failed: ${token_res.ok ? '' : JSON.stringify(token_res.error)}`);
642
544
  // 6. password change
643
545
  await test_app.app.request(password_route.path, {
644
546
  method: 'POST',
@@ -673,16 +575,23 @@ export const describe_standard_admin_integration_tests = (options) => {
673
575
  origin: 'http://localhost:5173',
674
576
  cookie: `${cookie_name}=${relogin_match[1]}`,
675
577
  };
676
- const audit_res = await test_app.app.request(audit_route.path, {
578
+ const audit_res = await rpc_call({
579
+ app: test_app.app,
580
+ path: rpc_path,
581
+ method: audit_log_list_action_spec.method,
677
582
  headers: relogin_headers,
678
583
  });
679
- assert.strictEqual(audit_res.status, 200);
680
- const audit_body = await audit_res.json();
584
+ assert.ok(audit_res.ok, `audit_log_list failed: ${audit_res.ok ? '' : JSON.stringify(audit_res.error)}`);
585
+ const audit_body = audit_res.result;
681
586
  const events = audit_body.events;
682
- // check that each operation produced at least one event
587
+ // check that each operation produced at least one event.
588
+ // `permit_offer_create` fires on the admin RPC; `permit_grant`
589
+ // fires when the recipient accepts (driven by offer_and_accept).
683
590
  const expected_types = [
684
591
  'login',
685
592
  'logout',
593
+ 'permit_offer_create',
594
+ 'permit_offer_accept',
686
595
  'permit_grant',
687
596
  'permit_revoke',
688
597
  'token_create',
@@ -697,7 +606,7 @@ export const describe_standard_admin_integration_tests = (options) => {
697
606
  });
698
607
  // --- 8. Admin-to-admin isolation ---
699
608
  describe('admin-to-admin isolation', () => {
700
- test('admin A cannot revoke admin B permits via mismatched account_id', async () => {
609
+ test('admin B revoking own permit via RPC succeeds', async () => {
701
610
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
702
611
  captured_route_specs ??= test_app.route_specs;
703
612
  // Bootstrap user is admin A. Create admin B.
@@ -705,36 +614,24 @@ export const describe_standard_admin_integration_tests = (options) => {
705
614
  username: 'admin_b_iso',
706
615
  roles: ['admin'],
707
616
  });
708
- // Find the permit grant route to give admin B a grantable role
709
- const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
710
- assert.ok(grant_route, 'Expected POST /permits/grant admin route');
711
- // Admin A grants a role to admin B
712
- const grant_res = await test_app.app.request(grant_route.path.replace(':account_id', admin_b.account.id), {
713
- method: 'POST',
714
- headers: create_headers(test_app.backend.session_cookie, {
715
- 'content-type': 'application/json',
716
- }),
717
- body: JSON.stringify({ role: grantable_role }),
718
- });
719
- assert.strictEqual(grant_res.status, 200);
720
- const grant_body = await grant_res.json();
721
- assert.ok(grant_body.permit, 'Expected permit in grant response');
722
- const permit_id = grant_body.permit.id;
723
- // Admin B revokes their own permit via admin route — should succeed
724
- const revoke_route = test_app.route_specs.find((s) => s.method === 'POST' &&
725
- s.path.includes('/permits/:permit_id/revoke') &&
726
- s.auth.type === 'role' &&
727
- s.auth.role === 'admin');
728
- assert.ok(revoke_route, 'Expected POST /permits/:permit_id/revoke admin route');
729
- const revoke_res = await test_app.app.request(revoke_route.path
730
- .replace(':account_id', admin_b.account.id)
731
- .replace(':permit_id', permit_id), {
732
- method: 'POST',
617
+ // Seed an active permit directly the revoke IDOR check is the
618
+ // subject of this test, not the grant→accept cycle.
619
+ const permit = await query_grant_permit({ db: get_db() }, {
620
+ actor_id: admin_b.actor.id,
621
+ role: grantable_role,
622
+ granted_by: test_app.backend.actor.id,
623
+ });
624
+ // Admin B revokes their own permit via RPC — should succeed
625
+ const revoke_res = await rpc_call({
626
+ app: test_app.app,
627
+ path: rpc_path,
628
+ method: permit_revoke_action_spec.method,
629
+ params: { actor_id: admin_b.actor.id, permit_id: permit.id },
733
630
  headers: create_headers(admin_b.session_cookie),
734
631
  });
735
- assert.strictEqual(revoke_res.status, 200);
736
- const revoke_body = await revoke_res.json();
737
- assert.strictEqual(revoke_body.revoked, true);
632
+ assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
633
+ const result = revoke_res.result;
634
+ assert.strictEqual(result.revoked, true);
738
635
  });
739
636
  test('admin revoke-all sessions for another admin works', async () => {
740
637
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
@@ -742,17 +639,18 @@ export const describe_standard_admin_integration_tests = (options) => {
742
639
  username: 'admin_b_sess',
743
640
  roles: ['admin'],
744
641
  });
745
- const revoke_sessions_route = find_admin_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
746
- assert.ok(revoke_sessions_route, 'Expected POST /sessions/revoke-all admin route');
747
- // Admin A revokes all of admin B's sessions
748
- const res = await test_app.app.request(revoke_sessions_route.path.replace(':account_id', admin_b.account.id), {
749
- method: 'POST',
642
+ // Admin A revokes all of admin B's sessions via RPC
643
+ const res = await rpc_call({
644
+ app: test_app.app,
645
+ path: rpc_path,
646
+ method: admin_session_revoke_all_action_spec.method,
647
+ params: { account_id: admin_b.account.id },
750
648
  headers: create_headers(test_app.backend.session_cookie),
751
649
  });
752
- assert.strictEqual(res.status, 200);
753
- const body = await res.json();
754
- assert.ok(typeof body.count === 'number', 'Expected count field in response');
755
- assert.ok(body.count >= 1, 'Expected at least 1 session revoked');
650
+ assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
651
+ const result = res.result;
652
+ assert.ok(typeof result.count === 'number', 'Expected count field in response');
653
+ assert.ok(result.count >= 1, 'Expected at least 1 session revoked');
756
654
  });
757
655
  test('admin revoke-all tokens for another admin works', async () => {
758
656
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
@@ -760,39 +658,41 @@ export const describe_standard_admin_integration_tests = (options) => {
760
658
  username: 'admin_b_tok',
761
659
  roles: ['admin'],
762
660
  });
763
- // Admin B creates an API token
764
- const token_create_route = test_app.route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/tokens/create'));
765
- if (token_create_route) {
766
- await test_app.app.request(token_create_route.path, {
767
- method: 'POST',
768
- headers: create_headers(admin_b.session_cookie, {
769
- 'content-type': 'application/json',
770
- }),
771
- body: JSON.stringify({ name: 'admin-b-token' }),
772
- });
773
- }
774
- const revoke_tokens_route = find_admin_route(test_app.route_specs, '/tokens/revoke-all', 'POST');
775
- assert.ok(revoke_tokens_route, 'Expected POST /tokens/revoke-all admin route');
776
- // Admin A revokes all of admin B's tokens
777
- const res = await test_app.app.request(revoke_tokens_route.path.replace(':account_id', admin_b.account.id), {
778
- method: 'POST',
661
+ // Admin B creates an API token via RPC
662
+ const token_res = await rpc_call({
663
+ app: test_app.app,
664
+ path: rpc_path,
665
+ method: account_token_create_action_spec.method,
666
+ params: { name: 'admin-b-token' },
667
+ headers: create_headers(admin_b.session_cookie),
668
+ });
669
+ assert.ok(token_res.ok, `account_token_create failed: ${token_res.ok ? '' : JSON.stringify(token_res.error)}`);
670
+ // Admin A revokes all of admin B's tokens via RPC
671
+ const res = await rpc_call({
672
+ app: test_app.app,
673
+ path: rpc_path,
674
+ method: admin_token_revoke_all_action_spec.method,
675
+ params: { account_id: admin_b.account.id },
779
676
  headers: create_headers(test_app.backend.session_cookie),
780
677
  });
781
- assert.strictEqual(res.status, 200);
782
- const body = await res.json();
783
- assert.ok(typeof body.count === 'number', 'Expected count field in response');
678
+ assert.ok(res.ok, `admin_token_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
679
+ const result = res.result;
680
+ assert.ok(typeof result.count === 'number', 'Expected count field in response');
681
+ // Token was created above, so revoke should evict at least one.
682
+ assert.ok(result.count >= 1, 'Expected at least 1 token revoked');
784
683
  });
785
684
  test('non-admin cannot access admin routes for another account', async () => {
786
685
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
787
686
  const regular_user = await test_app.create_account({ username: 'regular_user_iso' });
788
- const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
789
- assert.ok(accounts_route, 'Expected GET /accounts admin route');
790
- // Regular user tries to list accounts — should get 403
791
- const res = await test_app.app.request(accounts_route.path, {
687
+ // Regular user tries to list accounts via the admin RPC — should 403
688
+ const res = await rpc_call({
689
+ app: test_app.app,
690
+ path: rpc_path,
691
+ method: admin_account_list_action_spec.method,
792
692
  headers: create_headers(regular_user.session_cookie),
793
693
  });
694
+ assert.ok(!res.ok, 'Expected admin_account_list to fail for non-admin');
794
695
  assert.strictEqual(res.status, 403);
795
- error_collector.record(test_app.route_specs, 'GET', accounts_route.path, 403);
796
696
  });
797
697
  });
798
698
  // --- 8a. Error coverage: unauthenticated access to admin routes ---