@fuzdev/fuz_app 0.64.0 → 0.66.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 (223) hide show
  1. package/dist/actions/CLAUDE.md +510 -946
  2. package/dist/actions/action_codegen.d.ts +1 -1
  3. package/dist/actions/action_codegen.js +1 -1
  4. package/dist/actions/action_event_data.d.ts +1 -1
  5. package/dist/actions/broadcast_api.d.ts +1 -1
  6. package/dist/actions/broadcast_api.js +1 -1
  7. package/dist/actions/cancel.d.ts +2 -2
  8. package/dist/actions/cancel.js +3 -3
  9. package/dist/actions/connection_closer.d.ts +1 -4
  10. package/dist/actions/connection_closer.d.ts.map +1 -1
  11. package/dist/actions/connection_closer.js +1 -4
  12. package/dist/actions/register_action_ws.d.ts +2 -2
  13. package/dist/actions/register_ws_endpoint.d.ts +1 -1
  14. package/dist/actions/transports_ws_auth_guard.d.ts +1 -2
  15. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  16. package/dist/actions/transports_ws_auth_guard.js +1 -2
  17. package/dist/auth/CLAUDE.md +570 -1871
  18. package/dist/auth/account_schema.d.ts +1 -1
  19. package/dist/auth/account_schema.d.ts.map +1 -1
  20. package/dist/auth/api_token_queries.js +1 -1
  21. package/dist/auth/audit_log_ddl.d.ts +1 -1
  22. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  23. package/dist/auth/audit_log_ddl.js +1 -1
  24. package/dist/auth/audit_log_schema.js +2 -2
  25. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  26. package/dist/auth/bootstrap_account.js +1 -5
  27. package/dist/auth/bootstrap_routes.d.ts +7 -1
  28. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  29. package/dist/auth/bootstrap_routes.js +15 -11
  30. package/dist/auth/daemon_token_middleware.d.ts +15 -5
  31. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  32. package/dist/auth/daemon_token_middleware.js +24 -15
  33. package/dist/auth/invite_queries.d.ts +17 -7
  34. package/dist/auth/invite_queries.d.ts.map +1 -1
  35. package/dist/auth/invite_queries.js +19 -8
  36. package/dist/auth/keyring.d.ts +6 -6
  37. package/dist/auth/keyring.js +8 -8
  38. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  39. package/dist/auth/role_grant_offer_actions.js +4 -2
  40. package/dist/auth/signup_routes.d.ts +47 -1
  41. package/dist/auth/signup_routes.d.ts.map +1 -1
  42. package/dist/auth/signup_routes.js +103 -52
  43. package/dist/db/create_db.d.ts.map +1 -1
  44. package/dist/db/create_db.js +13 -0
  45. package/dist/dev/setup.d.ts +2 -2
  46. package/dist/dev/setup.js +3 -3
  47. package/dist/env/resolve.d.ts +44 -7
  48. package/dist/env/resolve.d.ts.map +1 -1
  49. package/dist/env/resolve.js +94 -27
  50. package/dist/http/CLAUDE.md +243 -522
  51. package/dist/http/error_schemas.d.ts +0 -4
  52. package/dist/http/error_schemas.d.ts.map +1 -1
  53. package/dist/http/error_schemas.js +0 -4
  54. package/dist/http/ip_canonical.d.ts +5 -4
  55. package/dist/http/ip_canonical.d.ts.map +1 -1
  56. package/dist/http/ip_canonical.js +8 -4
  57. package/dist/http/jsonrpc.d.ts +23 -7
  58. package/dist/http/jsonrpc.d.ts.map +1 -1
  59. package/dist/http/jsonrpc.js +19 -3
  60. package/dist/http/origin.d.ts +1 -1
  61. package/dist/http/origin.js +1 -1
  62. package/dist/http/surface.d.ts +9 -2
  63. package/dist/http/surface.d.ts.map +1 -1
  64. package/dist/runtime/mock.d.ts +1 -1
  65. package/dist/runtime/mock.js +2 -2
  66. package/dist/server/app_server.d.ts +41 -10
  67. package/dist/server/app_server.d.ts.map +1 -1
  68. package/dist/server/app_server.js +10 -4
  69. package/dist/server/env.d.ts +7 -7
  70. package/dist/server/env.d.ts.map +1 -1
  71. package/dist/server/env.js +14 -14
  72. package/dist/server/static.d.ts +4 -4
  73. package/dist/server/static.js +7 -7
  74. package/dist/testing/CLAUDE.md +740 -418
  75. package/dist/testing/admin_integration.d.ts +18 -23
  76. package/dist/testing/admin_integration.d.ts.map +1 -1
  77. package/dist/testing/admin_integration.js +230 -216
  78. package/dist/testing/app_server.d.ts +141 -39
  79. package/dist/testing/app_server.d.ts.map +1 -1
  80. package/dist/testing/app_server.js +157 -44
  81. package/dist/testing/audit_completeness.d.ts +25 -22
  82. package/dist/testing/audit_completeness.d.ts.map +1 -1
  83. package/dist/testing/audit_completeness.js +198 -159
  84. package/dist/testing/bootstrap_success.d.ts +28 -0
  85. package/dist/testing/bootstrap_success.d.ts.map +1 -0
  86. package/dist/testing/bootstrap_success.js +144 -0
  87. package/dist/testing/cross_backend/backend_config.d.ts +113 -0
  88. package/dist/testing/cross_backend/backend_config.d.ts.map +1 -0
  89. package/dist/testing/cross_backend/backend_config.js +1 -0
  90. package/dist/testing/cross_backend/bench/bench_report.d.ts +46 -0
  91. package/dist/testing/cross_backend/bench/bench_report.d.ts.map +1 -0
  92. package/dist/testing/cross_backend/bench/bench_report.js +83 -0
  93. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +44 -0
  94. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts.map +1 -0
  95. package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +38 -0
  96. package/dist/testing/cross_backend/bench/scenario.d.ts +57 -0
  97. package/dist/testing/cross_backend/bench/scenario.d.ts.map +1 -0
  98. package/dist/testing/cross_backend/bench/scenario.js +28 -0
  99. package/dist/testing/cross_backend/bootstrap_backend.d.ts +41 -0
  100. package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -0
  101. package/dist/testing/cross_backend/bootstrap_backend.js +34 -0
  102. package/dist/testing/cross_backend/build_test_backend_paths.d.ts +24 -0
  103. package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -0
  104. package/dist/testing/cross_backend/build_test_backend_paths.js +33 -0
  105. package/dist/testing/cross_backend/capabilities.d.ts +65 -0
  106. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
  107. package/dist/testing/cross_backend/capabilities.js +47 -0
  108. package/dist/testing/cross_backend/default_backend_configs.d.ts +122 -0
  109. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -0
  110. package/dist/testing/cross_backend/default_backend_configs.js +111 -0
  111. package/dist/testing/cross_backend/default_secrets.d.ts +40 -0
  112. package/dist/testing/cross_backend/default_secrets.d.ts.map +1 -0
  113. package/dist/testing/cross_backend/default_secrets.js +39 -0
  114. package/dist/testing/cross_backend/default_spine_surface.d.ts +64 -0
  115. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -0
  116. package/dist/testing/cross_backend/default_spine_surface.js +121 -0
  117. package/dist/testing/cross_backend/setup.d.ts +451 -0
  118. package/dist/testing/cross_backend/setup.d.ts.map +1 -0
  119. package/dist/testing/cross_backend/setup.js +581 -0
  120. package/dist/testing/cross_backend/spawn_backend.d.ts +58 -0
  121. package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -0
  122. package/dist/testing/cross_backend/spawn_backend.js +229 -0
  123. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts +66 -0
  124. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts.map +1 -0
  125. package/dist/testing/cross_backend/spine_stub_backend_config.js +49 -0
  126. package/dist/testing/cross_backend/sse_round_trip.d.ts +37 -0
  127. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -0
  128. package/dist/testing/cross_backend/sse_round_trip.js +137 -0
  129. package/dist/testing/cross_backend/standard.d.ts +96 -0
  130. package/dist/testing/cross_backend/standard.d.ts.map +1 -0
  131. package/dist/testing/cross_backend/standard.js +49 -0
  132. package/dist/testing/cross_backend/testing_reset_actions.d.ts +171 -0
  133. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -0
  134. package/dist/testing/cross_backend/testing_reset_actions.js +213 -0
  135. package/dist/testing/cross_backend/testing_server_bun.d.ts +5 -0
  136. package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -0
  137. package/dist/testing/cross_backend/testing_server_bun.js +59 -0
  138. package/dist/testing/cross_backend/testing_server_core.d.ts +140 -0
  139. package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -0
  140. package/dist/testing/cross_backend/testing_server_core.js +68 -0
  141. package/dist/testing/cross_backend/testing_server_deno.d.ts +5 -0
  142. package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -0
  143. package/dist/testing/cross_backend/testing_server_deno.js +37 -0
  144. package/dist/testing/cross_backend/testing_server_node.d.ts +5 -0
  145. package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -0
  146. package/dist/testing/cross_backend/testing_server_node.js +50 -0
  147. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +72 -0
  148. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -0
  149. package/dist/testing/cross_backend/ts_spine_backend_config.js +112 -0
  150. package/dist/testing/cross_backend/ws_round_trip.d.ts +35 -0
  151. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -0
  152. package/dist/testing/cross_backend/ws_round_trip.js +113 -0
  153. package/dist/testing/data_exposure.d.ts +11 -14
  154. package/dist/testing/data_exposure.d.ts.map +1 -1
  155. package/dist/testing/data_exposure.js +123 -146
  156. package/dist/testing/db_entities.d.ts +22 -1
  157. package/dist/testing/db_entities.d.ts.map +1 -1
  158. package/dist/testing/db_entities.js +24 -1
  159. package/dist/testing/integration.d.ts +56 -21
  160. package/dist/testing/integration.d.ts.map +1 -1
  161. package/dist/testing/integration.js +294 -319
  162. package/dist/testing/integration_helpers.d.ts +16 -6
  163. package/dist/testing/integration_helpers.d.ts.map +1 -1
  164. package/dist/testing/integration_helpers.js +7 -7
  165. package/dist/testing/mock_fs.d.ts.map +1 -1
  166. package/dist/testing/mock_fs.js +0 -2
  167. package/dist/testing/rate_limiting.d.ts.map +1 -1
  168. package/dist/testing/rate_limiting.js +9 -0
  169. package/dist/testing/role_grant_helpers.d.ts +31 -0
  170. package/dist/testing/role_grant_helpers.d.ts.map +1 -0
  171. package/dist/testing/role_grant_helpers.js +46 -0
  172. package/dist/testing/round_trip.d.ts +20 -16
  173. package/dist/testing/round_trip.d.ts.map +1 -1
  174. package/dist/testing/round_trip.js +61 -86
  175. package/dist/testing/rpc_helpers.d.ts +10 -4
  176. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  177. package/dist/testing/rpc_helpers.js +1 -1
  178. package/dist/testing/rpc_round_trip.d.ts +24 -21
  179. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  180. package/dist/testing/rpc_round_trip.js +87 -104
  181. package/dist/testing/schema_introspect.d.ts +106 -0
  182. package/dist/testing/schema_introspect.d.ts.map +1 -0
  183. package/dist/testing/schema_introspect.js +123 -0
  184. package/dist/testing/schema_parity.d.ts +144 -0
  185. package/dist/testing/schema_parity.d.ts.map +1 -0
  186. package/dist/testing/schema_parity.js +233 -0
  187. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  188. package/dist/testing/sse_round_trip.js +1 -68
  189. package/dist/testing/standard.d.ts +56 -25
  190. package/dist/testing/standard.d.ts.map +1 -1
  191. package/dist/testing/standard.js +62 -5
  192. package/dist/testing/stubs.d.ts +21 -6
  193. package/dist/testing/stubs.d.ts.map +1 -1
  194. package/dist/testing/stubs.js +33 -23
  195. package/dist/testing/testing_rate_limiter.d.ts +59 -0
  196. package/dist/testing/testing_rate_limiter.d.ts.map +1 -0
  197. package/dist/testing/testing_rate_limiter.js +74 -0
  198. package/dist/testing/transports/bootstrap.d.ts +52 -0
  199. package/dist/testing/transports/bootstrap.d.ts.map +1 -0
  200. package/dist/testing/transports/bootstrap.js +70 -0
  201. package/dist/testing/transports/fetch_transport.d.ts +81 -0
  202. package/dist/testing/transports/fetch_transport.d.ts.map +1 -0
  203. package/dist/testing/transports/fetch_transport.js +74 -0
  204. package/dist/testing/transports/sse_frame_reader.d.ts +41 -0
  205. package/dist/testing/transports/sse_frame_reader.d.ts.map +1 -0
  206. package/dist/testing/transports/sse_frame_reader.js +84 -0
  207. package/dist/testing/transports/sse_transport.d.ts +54 -0
  208. package/dist/testing/transports/sse_transport.d.ts.map +1 -0
  209. package/dist/testing/transports/sse_transport.js +51 -0
  210. package/dist/testing/transports/ws_client.d.ts +108 -0
  211. package/dist/testing/transports/ws_client.d.ts.map +1 -0
  212. package/dist/testing/transports/ws_client.js +56 -0
  213. package/dist/testing/transports/ws_transport.d.ts +43 -0
  214. package/dist/testing/transports/ws_transport.d.ts.map +1 -0
  215. package/dist/testing/transports/ws_transport.js +169 -0
  216. package/dist/testing/ws_round_trip.d.ts +21 -103
  217. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  218. package/dist/testing/ws_round_trip.js +42 -40
  219. package/dist/ui/CLAUDE.md +5 -3
  220. package/dist/ui/MenuLink.svelte +16 -16
  221. package/dist/ui/MenuLink.svelte.d.ts +13 -4
  222. package/dist/ui/MenuLink.svelte.d.ts.map +1 -1
  223. package/package.json +10 -4
@@ -8,15 +8,16 @@ import './assert_dev_env.js';
8
8
  * - Cross-privilege: verifies admin routes return 403 for non-admin users,
9
9
  * and non-admin responses exclude admin-only fields
10
10
  *
11
+ * Cadence: per-describe `setup_test()` call (see `round_trip.ts` module
12
+ * docstring). The runtime body manages its own request ordering (auth-free
13
+ * → cross-privilege → 2xx) to avoid session-invalidation contamination
14
+ * between assertions, so per-test fixture re-creation isn't needed.
15
+ *
11
16
  * @module
12
17
  */
13
- import { describe, test, beforeAll, afterAll, assert } from 'vitest';
18
+ import { describe, test, beforeAll, assert } from 'vitest';
14
19
  import { ROLE_ADMIN } from '../auth/role_schema.js';
15
- import { create_test_app } from './app_server.js';
16
- import { create_pglite_factory } from './db.js';
17
20
  import { resolve_valid_path, generate_valid_body } from './schema_generators.js';
18
- import { run_migrations } from '../db/migrate.js';
19
- import { auth_migration_ns } from '../auth/migrations.js';
20
21
  import { is_null_schema, is_strict_object_schema } from '../http/schema_helpers.js';
21
22
  import { is_keeper_auth, is_public_auth } from '../http/auth_shape.js';
22
23
  import { sensitive_field_blocklist, admin_only_field_blocklist, assert_no_sensitive_fields_in_json, pick_auth_headers, } from './integration_helpers.js';
@@ -92,9 +93,11 @@ export const assert_non_admin_schemas_no_admin_fields = (surface, admin_only_fie
92
93
  * contain no sensitive fields
93
94
  */
94
95
  export const describe_data_exposure_tests = (options) => {
95
- const { build, sensitive_fields = sensitive_field_blocklist, admin_only_fields = admin_only_field_blocklist, } = options;
96
+ const { surface, route_specs } = options.surface_source;
97
+ const { sensitive_fields = sensitive_field_blocklist, admin_only_fields = admin_only_field_blocklist, } = options;
98
+ const skip_set = new Set(options.skip_routes);
99
+ void options.capabilities;
96
100
  describe('data exposure — schema-level', () => {
97
- const { surface } = build();
98
101
  test('no sensitive fields in any output schema', () => {
99
102
  assert_output_schemas_no_sensitive_fields(surface, sensitive_fields);
100
103
  });
@@ -114,151 +117,125 @@ export const describe_data_exposure_tests = (options) => {
114
117
  }
115
118
  });
116
119
  });
117
- describe_data_exposure_runtime_tests(options);
118
- };
119
- // --- Runtime tests ---
120
- const describe_data_exposure_runtime_tests = (options) => {
121
- const { sensitive_fields = sensitive_field_blocklist, admin_only_fields = admin_only_field_blocklist, } = options;
122
- const skip_set = new Set(options.skip_routes);
123
- const init_schema = async (db) => {
124
- await run_migrations(db, [auth_migration_ns]);
125
- };
126
- const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
127
- for (const factory of factories) {
128
- const describe_fn = factory.skip ? describe.skip : describe;
129
- describe_fn(`data exposure — runtime (${factory.name})`, () => {
130
- let test_app;
131
- let authed_account;
132
- let admin_account;
133
- let db;
134
- beforeAll(async () => {
135
- db = await factory.create();
136
- test_app = await create_test_app({
137
- session_options: options.session_options,
138
- create_route_specs: options.create_route_specs,
139
- db,
140
- app_options: options.app_options,
141
- });
142
- authed_account = await test_app.create_account({
143
- username: 'exposure_authed',
144
- roles: [],
145
- });
146
- admin_account = await test_app.create_account({
147
- username: 'exposure_admin',
148
- roles: [ROLE_ADMIN],
149
- });
120
+ describe('data exposure — runtime', () => {
121
+ let fixture;
122
+ let authed_account;
123
+ let admin_account;
124
+ beforeAll(async () => {
125
+ fixture = await options.setup_test();
126
+ authed_account = await fixture.create_account({
127
+ username: 'exposure_authed',
128
+ roles: [],
150
129
  });
151
- afterAll(async () => {
152
- await test_app.cleanup();
153
- await factory.close(db);
130
+ admin_account = await fixture.create_account({
131
+ username: 'exposure_admin',
132
+ roles: [ROLE_ADMIN],
154
133
  });
155
- // Tests that don't fire authenticated requests run first — they don't
156
- // invalidate sessions and are independent of test order.
157
- test('unauthenticated error responses contain no sensitive fields', async () => {
158
- const protected_specs = test_app.route_specs.filter((s) => !is_public_auth(s.auth));
159
- for (const spec of protected_specs) {
160
- const route_key = `${spec.method} ${spec.path}`;
161
- if (skip_set.has(route_key))
162
- continue;
163
- const url = resolve_valid_path(spec.path, spec.params);
164
- const res = await test_app.app.request(url, {
165
- method: spec.method,
166
- headers: { host: 'localhost', origin: 'http://localhost:5173' },
167
- });
168
- if (res.headers.get('Content-Type')?.includes('text/event-stream')) {
169
- await res.body?.cancel();
170
- continue;
171
- }
172
- let error_body;
173
- try {
174
- error_body = await res.clone().json();
175
- }
176
- catch {
177
- continue;
178
- }
179
- assert_no_sensitive_fields_in_json(error_body, sensitive_fields, `unauthenticated ${route_key} (${res.status})`);
134
+ });
135
+ // Tests that don't fire authenticated requests run first — they don't
136
+ // invalidate sessions and are independent of test order.
137
+ test('unauthenticated error responses contain no sensitive fields', async () => {
138
+ const protected_specs = route_specs.filter((s) => !is_public_auth(s.auth));
139
+ for (const spec of protected_specs) {
140
+ const route_key = `${spec.method} ${spec.path}`;
141
+ if (skip_set.has(route_key))
142
+ continue;
143
+ const url = resolve_valid_path(spec.path, spec.params);
144
+ const res = await fixture.transport(url, {
145
+ method: spec.method,
146
+ headers: { host: 'localhost', origin: 'http://localhost:5173' },
147
+ });
148
+ if (res.headers.get('Content-Type')?.includes('text/event-stream')) {
149
+ await res.body?.cancel();
150
+ continue;
180
151
  }
181
- });
182
- // Cross-privilege test runs before 2xx tests — admin routes reject
183
- // without calling handlers, so sessions stay intact.
184
- test('admin routes return 403 for non-admin user', async () => {
185
- const admin_specs = test_app.route_specs.filter((s) => s.auth.roles?.includes('admin') ?? false);
186
- for (const spec of admin_specs) {
187
- const route_key = `${spec.method} ${spec.path}`;
188
- if (skip_set.has(route_key))
189
- continue;
190
- const url = resolve_valid_path(spec.path, spec.params);
191
- const headers = authed_account.create_session_headers();
192
- const res = await test_app.app.request(url, {
193
- method: spec.method,
194
- headers,
195
- });
196
- assert.strictEqual(res.status, 403, `${route_key} should return 403 for non-admin user`);
197
- let error_body;
198
- try {
199
- error_body = await res.clone().json();
200
- }
201
- catch {
202
- continue;
203
- }
204
- assert_no_sensitive_fields_in_json(error_body, sensitive_fields, `${route_key} 403`);
152
+ let error_body;
153
+ try {
154
+ error_body = await res.clone().json();
205
155
  }
206
- });
207
- // 2xx tests run last — handlers like logout and session-revoke-all
208
- // invalidate sessions as a side effect. Sort GET before POST so
209
- // data-returning routes are checked before destructive routes fire.
210
- test('all 2xx responses pass field blocklists', async () => {
211
- // sort GET before mutations to check data-returning routes
212
- // before destructive routes (logout, revoke-all) invalidate sessions
213
- const sorted_specs = [...test_app.route_specs].sort((a, b) => {
214
- if (a.method === 'GET' && b.method !== 'GET')
215
- return -1;
216
- if (a.method !== 'GET' && b.method === 'GET')
217
- return 1;
218
- return 0;
156
+ catch {
157
+ continue;
158
+ }
159
+ assert_no_sensitive_fields_in_json(error_body, sensitive_fields, `unauthenticated ${route_key} (${res.status})`);
160
+ }
161
+ });
162
+ // Cross-privilege test runs before 2xx tests admin routes reject
163
+ // without calling handlers, so sessions stay intact.
164
+ test('admin routes return 403 for non-admin user', async () => {
165
+ const admin_specs = route_specs.filter((s) => s.auth.roles?.includes('admin') ?? false);
166
+ for (const spec of admin_specs) {
167
+ const route_key = `${spec.method} ${spec.path}`;
168
+ if (skip_set.has(route_key))
169
+ continue;
170
+ const url = resolve_valid_path(spec.path, spec.params);
171
+ const headers = authed_account.create_session_headers();
172
+ const res = await fixture.transport(url, {
173
+ method: spec.method,
174
+ headers,
219
175
  });
220
- for (const spec of sorted_specs) {
221
- const route_key = `${spec.method} ${spec.path}`;
222
- if (skip_set.has(route_key))
223
- continue;
224
- // keeper auth (daemon token) is strictly more privileged than admin
225
- const is_elevated = is_keeper_auth(spec.auth) || (spec.auth.roles?.includes('admin') ?? false);
226
- const url = resolve_valid_path(spec.path, spec.params);
227
- const body = generate_valid_body(spec.input);
228
- const headers = pick_auth_headers(spec, test_app, authed_account, admin_account);
229
- const request_init = {
230
- method: spec.method,
231
- headers: {
232
- ...headers,
233
- ...(body ? { 'content-type': 'application/json' } : {}),
234
- },
235
- ...(body ? { body: JSON.stringify(body) } : {}),
236
- };
237
- const res = await test_app.app.request(url, request_init);
238
- if (res.headers.get('Content-Type')?.includes('text/event-stream')) {
239
- await res.body?.cancel();
240
- continue;
241
- }
242
- if (!res.ok)
243
- continue;
244
- let response_body;
245
- try {
246
- response_body = await res.clone().json();
247
- }
248
- catch {
249
- continue;
250
- }
251
- assert_no_sensitive_fields_in_json(response_body, sensitive_fields, `${route_key} (${res.status})`);
252
- // Admin-only field check applies to non-elevated routes with strict
253
- // output schemas. Loose schemas (e.g. surface route returning JSON
254
- // Schema representations) may contain admin field names as metadata.
255
- if (!is_elevated &&
256
- !is_null_schema(spec.output) &&
257
- is_strict_object_schema(spec.output)) {
258
- assert_no_sensitive_fields_in_json(response_body, admin_only_fields, `non-admin ${route_key} (${res.status})`);
259
- }
176
+ assert.strictEqual(res.status, 403, `${route_key} should return 403 for non-admin user`);
177
+ let error_body;
178
+ try {
179
+ error_body = await res.clone().json();
180
+ }
181
+ catch {
182
+ continue;
260
183
  }
184
+ assert_no_sensitive_fields_in_json(error_body, sensitive_fields, `${route_key} 403`);
185
+ }
186
+ });
187
+ // 2xx tests run last — handlers like logout and session-revoke-all
188
+ // invalidate sessions as a side effect. Sort GET before POST so
189
+ // data-returning routes are checked before destructive routes fire.
190
+ test('all 2xx responses pass field blocklists', async () => {
191
+ // sort GET before mutations to check data-returning routes
192
+ // before destructive routes (logout, revoke-all) invalidate sessions
193
+ const sorted_specs = [...route_specs].sort((a, b) => {
194
+ if (a.method === 'GET' && b.method !== 'GET')
195
+ return -1;
196
+ if (a.method !== 'GET' && b.method === 'GET')
197
+ return 1;
198
+ return 0;
261
199
  });
200
+ for (const spec of sorted_specs) {
201
+ const route_key = `${spec.method} ${spec.path}`;
202
+ if (skip_set.has(route_key))
203
+ continue;
204
+ // keeper auth (daemon token) is strictly more privileged than admin
205
+ const is_elevated = is_keeper_auth(spec.auth) || (spec.auth.roles?.includes('admin') ?? false);
206
+ const url = resolve_valid_path(spec.path, spec.params);
207
+ const body = generate_valid_body(spec.input);
208
+ const headers = pick_auth_headers(spec, fixture, authed_account, admin_account);
209
+ const request_init = {
210
+ method: spec.method,
211
+ headers: {
212
+ ...headers,
213
+ ...(body ? { 'content-type': 'application/json' } : {}),
214
+ },
215
+ ...(body ? { body: JSON.stringify(body) } : {}),
216
+ };
217
+ const res = await fixture.transport(url, request_init);
218
+ if (res.headers.get('Content-Type')?.includes('text/event-stream')) {
219
+ await res.body?.cancel();
220
+ continue;
221
+ }
222
+ if (!res.ok)
223
+ continue;
224
+ let response_body;
225
+ try {
226
+ response_body = await res.clone().json();
227
+ }
228
+ catch {
229
+ continue;
230
+ }
231
+ assert_no_sensitive_fields_in_json(response_body, sensitive_fields, `${route_key} (${res.status})`);
232
+ // Admin-only field check applies to non-elevated routes with strict
233
+ // output schemas. Loose schemas (e.g. surface route returning JSON
234
+ // Schema representations) may contain admin field names as metadata.
235
+ if (!is_elevated && !is_null_schema(spec.output) && is_strict_object_schema(spec.output)) {
236
+ assert_no_sensitive_fields_in_json(response_body, admin_only_fields, `non-admin ${route_key} (${res.status})`);
237
+ }
238
+ }
262
239
  });
263
- }
240
+ });
264
241
  };
@@ -1,5 +1,5 @@
1
1
  import './assert_dev_env.js';
2
- import type { Account, Actor } from '../auth/account_schema.js';
2
+ import type { Account, Actor, CreateRoleGrantInput, RoleGrant } from '../auth/account_schema.js';
3
3
  import type { Db } from '../db/db.js';
4
4
  /** The `{account, actor}` row pair returned by `create_test_account_with_actor`. */
5
5
  export interface TestAccountWithActor {
@@ -19,4 +19,25 @@ export declare const create_test_account_with_actor: (db: Db, options: {
19
19
  username: string;
20
20
  password_hash?: string;
21
21
  }) => Promise<TestAccountWithActor>;
22
+ /**
23
+ * Materialize a `role_grant` directly via `query_create_role_grant`,
24
+ * bypassing the production offer/accept consent flow.
25
+ *
26
+ * **In-process only.** This helper takes a raw `Db` handle and seeds
27
+ * rows without firing audit fan-out, WebSocket broadcasts, or the
28
+ * `_supersede` notification chain a real grant emits. Cross-process
29
+ * suites must instead drive `role_grant_offer_create_action_spec` +
30
+ * `role_grant_offer_accept_action_spec` via
31
+ * `role_grant_helpers.ts`'s `role_grant_offer_and_accept` so the
32
+ * fixture observes the full post-commit fan-out the way production
33
+ * does — otherwise tests would mask real divergence between the TS
34
+ * and Rust spines.
35
+ *
36
+ * Use this helper for query-level (`*.db.test.ts`) tests that
37
+ * exercise revoke or isolation semantics — not the consent path
38
+ * itself. The schema's `source_offer_id = null` shape is an
39
+ * intentional admin-direct escape; this helper exposes it so
40
+ * suites don't reimplement the same direct-seed wrapper.
41
+ */
42
+ export declare const create_test_role_grant_direct: (db: Db, input: CreateRoleGrantInput) => Promise<RoleGrant>;
22
43
  //# sourceMappingURL=db_entities.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"db_entities.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/db_entities.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAoB7B,OAAO,KAAK,EAAC,OAAO,EAAE,KAAK,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAEpC,oFAAoF;AACpF,MAAM,WAAW,oBAAoB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;CACb;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,GAC1C,IAAI,EAAE,EACN,SAAS;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAC,KACjD,OAAO,CAAC,oBAAoB,CAI7B,CAAC"}
1
+ {"version":3,"file":"db_entities.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/db_entities.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,OAAO,KAAK,EAAC,OAAO,EAAE,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAC,MAAM,2BAA2B,CAAC;AAC/F,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAEpC,oFAAoF;AACpF,MAAM,WAAW,oBAAoB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;CACb;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,GAC1C,IAAI,EAAE,EACN,SAAS;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAC,KACjD,OAAO,CAAC,oBAAoB,CAI7B,CAAC;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,6BAA6B,GACzC,IAAI,EAAE,EACN,OAAO,oBAAoB,KACzB,OAAO,CAAC,SAAS,CAAyC,CAAC"}
@@ -10,12 +10,14 @@ import './assert_dev_env.js';
10
10
  * wrapper in every file.
11
11
  *
12
12
  * For full-fledged test accounts that also need an API token + signed
13
- * session cookie + role_grants, use `bootstrap_test_account` from
13
+ * session cookie + role_grants, use `bootstrap_test_keeper` (keeper) or
14
+ * `create_test_account_with_credentials` (additional accounts) from
14
15
  * `app_server.ts` instead.
15
16
  *
16
17
  * @module
17
18
  */
18
19
  import { query_create_account_with_actor } from '../auth/account_queries.js';
20
+ import { query_create_role_grant } from '../auth/role_grant_queries.js';
19
21
  /**
20
22
  * Create an `account` + `actor` row pair in the database for tests.
21
23
  *
@@ -26,3 +28,24 @@ import { query_create_account_with_actor } from '../auth/account_queries.js';
26
28
  * test suite.
27
29
  */
28
30
  export const create_test_account_with_actor = async (db, options) => query_create_account_with_actor({ db }, { username: options.username, password_hash: options.password_hash ?? 'hash' });
31
+ /**
32
+ * Materialize a `role_grant` directly via `query_create_role_grant`,
33
+ * bypassing the production offer/accept consent flow.
34
+ *
35
+ * **In-process only.** This helper takes a raw `Db` handle and seeds
36
+ * rows without firing audit fan-out, WebSocket broadcasts, or the
37
+ * `_supersede` notification chain a real grant emits. Cross-process
38
+ * suites must instead drive `role_grant_offer_create_action_spec` +
39
+ * `role_grant_offer_accept_action_spec` via
40
+ * `role_grant_helpers.ts`'s `role_grant_offer_and_accept` so the
41
+ * fixture observes the full post-commit fan-out the way production
42
+ * does — otherwise tests would mask real divergence between the TS
43
+ * and Rust spines.
44
+ *
45
+ * Use this helper for query-level (`*.db.test.ts`) tests that
46
+ * exercise revoke or isolation semantics — not the consent path
47
+ * itself. The schema's `source_offer_id = null` shape is an
48
+ * intentional admin-direct escape; this helper exposes it so
49
+ * suites don't reimplement the same direct-seed wrapper.
50
+ */
51
+ export const create_test_role_grant_direct = async (db, input) => query_create_role_grant({ db }, input);
@@ -1,25 +1,39 @@
1
1
  import './assert_dev_env.js';
2
2
  import type { SessionOptions } from '../auth/session_cookie.js';
3
- import type { AppServerContext } from '../server/app_server.js';
4
- import type { RouteSpec } from '../http/route_spec.js';
5
- import { type SuiteAppOptions } from './app_server.js';
6
- import { type DbFactory } from './db.js';
7
3
  import { type RpcEndpointsSuiteOption } from './rpc_helpers.js';
4
+ import type { AppSurfaceSpec } from '../http/surface.js';
5
+ import { type BackendCapabilities } from './cross_backend/capabilities.js';
6
+ import type { SetupTest } from './cross_backend/setup.js';
8
7
  /**
9
8
  * Configuration for `describe_standard_integration_tests`.
10
9
  */
11
10
  export interface StandardIntegrationTestOptions {
12
- /** Session config for cookie-based auth. */
13
- session_options: SessionOptions<string>;
14
- /** Route spec factory — same one used in production. */
15
- create_route_specs: (ctx: AppServerContext) => Array<RouteSpec>;
16
- /** Optional overrides for `AppServerOptions`. */
17
- app_options?: SuiteAppOptions;
18
11
  /**
19
- * Database factories to run tests against. Default: pglite only.
20
- * Pass consumer factories (e.g. `[pglite_factory, pg_factory]`) to also test against PostgreSQL.
12
+ * Per-test fixture-producing function. The integration suite calls
13
+ * this in every `test()` body auth_integration_truncate_tables
14
+ * clears `account`, so each test re-bootstraps the keeper.
15
+ */
16
+ setup_test: SetupTest;
17
+ /**
18
+ * App surface (with route specs + middleware specs) for route iteration
19
+ * and error-coverage scoping. The same shape feeds both in-process and
20
+ * cross-process tests — the test process always constructs the spec in
21
+ * TS (via `create_test_app_surface_spec` or a consumer's equivalent);
22
+ * cross-process-ness is a property of the transport + per-test fixture,
23
+ * not the schema source. The on-disk `auth_attack_surface.json` is an
24
+ * observability artifact for human inspection + gen-time drift gating,
25
+ * not the source the test runtime reads from.
21
26
  */
22
- db_factories?: Array<DbFactory>;
27
+ surface_source: AppSurfaceSpec;
28
+ /** Backend capability declarations — companion to `fixture.in_process` narrowing. */
29
+ capabilities: BackendCapabilities;
30
+ /**
31
+ * Session config — needed to resolve factory-form `rpc_endpoints`
32
+ * against a stub `AppServerContext` at setup time and to read
33
+ * `cookie_name` for manual cookie composition in the origin-verify
34
+ * cases.
35
+ */
36
+ session_options: SessionOptions<string>;
23
37
  /**
24
38
  * RPC endpoint specs — required. This suite dispatches
25
39
  * `account_verify`, `account_session_*`, and `account_token_*` via
@@ -28,16 +42,21 @@ export interface StandardIntegrationTestOptions {
28
42
  * `require_rpc_endpoint_path` on setup so consumer projects see a
29
43
  * clear setup error instead of confusing test failures.
30
44
  *
31
- * Accepts either an array (eager) or a factory
32
- * `(ctx: AppServerContext) => Array<RpcEndpointSpec>` the factory form
33
- * is required when action handlers must close over the per-test
34
- * `ctx.app_settings` / `ctx.deps` (e.g. the canonical
35
- * `create_standard_rpc_actions(ctx.deps, {app_settings: ctx.app_settings})`
36
- * pattern). The factory must return the same endpoint `path` regardless
37
- * of ctx — it is invoked once at setup with a stub ctx for path lookup
38
- * and again per-test by `create_app_server` for live dispatch.
45
+ * Accepts either an array (eager) or a factory — see `rpc_helpers.ts`
46
+ * for the union semantics. The factory must return the same endpoint
47
+ * `path` regardless of ctx invoked once at setup with a stub ctx
48
+ * for path lookup; the running backend handles live dispatch.
39
49
  */
40
50
  rpc_endpoints: RpcEndpointsSuiteOption;
51
+ /**
52
+ * Minimum error-coverage ratio to enforce on the scoped REST surface
53
+ * (login / logout / password / signup + the shared RPC endpoint).
54
+ * Default `DEFAULT_INTEGRATION_ERROR_COVERAGE` (0.2). Set to `0` to
55
+ * skip the assertion entirely — useful for consumers with minimal
56
+ * route sets whose declared error codes outpace the suite's
57
+ * denial-path drivers.
58
+ */
59
+ error_coverage_min?: number;
41
60
  }
42
61
  /**
43
62
  * Standard integration test suite for fuz_app auth routes.
@@ -51,6 +70,22 @@ export interface StandardIntegrationTestOptions {
51
70
  * Each test group asserts that required routes exist, failing with a descriptive
52
71
  * message if the consumer's route specs are misconfigured.
53
72
  *
73
+ * The two signup-invite-edge-case tests call `invite_create_action_spec`
74
+ * (admin-gated) over the fixture's session, so consumers wiring signup +
75
+ * admin actions must thread `extra_keeper_roles: [ROLE_ADMIN]` through
76
+ * either `default_in_process_suite_options` or
77
+ * `default_cross_process_setup(handle, {extra_keeper_roles:
78
+ * [ROLE_ADMIN]})`. In both modes the fixture's `fixture.account` is the
79
+ * fresh keeper, and the extra-keeper-roles list grants the bootstrapped
80
+ * keeper additional roles inline (cross-process via `_testing_reset`'s
81
+ * bootstrap-time seeding; in-process via `bootstrap_test_keeper`) — no
82
+ * per-role RPC cost, no offer/accept round-trip. The tests run against
83
+ * the production `open_signup: false` default — the cross-process
84
+ * per-test secondary mint via `fixture.create_account()` is
85
+ * invite-gated (keeper drives `invite_create` before signup) so the
86
+ * harness doesn't need to flip the setting. Consumers that don't wire
87
+ * signup or `invite_create` silently skip these two tests.
88
+ *
54
89
  * @throws Error at setup time when `options.rpc_endpoints` is empty — the
55
90
  * suite hard-fails via `require_rpc_endpoint_path` rather than running
56
91
  * tests that would crash mid-suite trying to dispatch
@@ -1 +1 @@
1
- {"version":3,"file":"integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAC9D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAErD,OAAO,EAA6C,KAAK,eAAe,EAAC,MAAM,iBAAiB,CAAC;AACjG,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAOjB,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAsB1B;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC;;;;;;;;;;;;;;;;OAgBG;IACH,aAAa,EAAE,uBAAuB,CAAC;CACvC;AAoBD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,IA87CF,CAAC"}
1
+ {"version":3,"file":"integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAO9D,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAkB1B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAU,KAAK,mBAAmB,EAAC,MAAM,iCAAiC,CAAC;AAClF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAGxD;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C;;;;OAIG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;;;;;;OASG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,qFAAqF;IACrF,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;;OAKG;IACH,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;;;;;;;;OAYG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,IAy5CF,CAAC"}