@fuzdev/fuz_app 0.64.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/actions/CLAUDE.md +513 -928
  2. package/dist/actions/broadcast_api.d.ts +1 -1
  3. package/dist/actions/broadcast_api.js +1 -1
  4. package/dist/actions/cancel.d.ts +2 -2
  5. package/dist/actions/cancel.js +3 -3
  6. package/dist/actions/connection_closer.d.ts +1 -4
  7. package/dist/actions/connection_closer.d.ts.map +1 -1
  8. package/dist/actions/connection_closer.js +1 -4
  9. package/dist/actions/register_action_ws.d.ts +2 -2
  10. package/dist/actions/register_ws_endpoint.d.ts +1 -1
  11. package/dist/actions/transports_ws_auth_guard.d.ts +1 -2
  12. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  13. package/dist/actions/transports_ws_auth_guard.js +1 -2
  14. package/dist/auth/CLAUDE.md +591 -1871
  15. package/dist/auth/account_schema.d.ts +1 -1
  16. package/dist/auth/account_schema.d.ts.map +1 -1
  17. package/dist/auth/api_token_queries.js +1 -1
  18. package/dist/auth/audit_log_ddl.d.ts +1 -1
  19. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  20. package/dist/auth/audit_log_ddl.js +1 -1
  21. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  22. package/dist/auth/bootstrap_account.js +1 -5
  23. package/dist/auth/bootstrap_routes.d.ts +7 -1
  24. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  25. package/dist/auth/bootstrap_routes.js +15 -11
  26. package/dist/auth/keyring.d.ts +6 -6
  27. package/dist/auth/keyring.js +8 -8
  28. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  29. package/dist/auth/role_grant_offer_actions.js +4 -2
  30. package/dist/db/create_db.d.ts.map +1 -1
  31. package/dist/db/create_db.js +13 -0
  32. package/dist/dev/setup.d.ts +2 -2
  33. package/dist/dev/setup.js +3 -3
  34. package/dist/http/CLAUDE.md +224 -498
  35. package/dist/http/error_schemas.d.ts +0 -4
  36. package/dist/http/error_schemas.d.ts.map +1 -1
  37. package/dist/http/error_schemas.js +0 -4
  38. package/dist/http/ip_canonical.d.ts +5 -4
  39. package/dist/http/ip_canonical.d.ts.map +1 -1
  40. package/dist/http/ip_canonical.js +8 -4
  41. package/dist/http/origin.d.ts +1 -1
  42. package/dist/http/origin.js +1 -1
  43. package/dist/runtime/mock.js +1 -1
  44. package/dist/server/app_server.d.ts +41 -10
  45. package/dist/server/app_server.d.ts.map +1 -1
  46. package/dist/server/app_server.js +10 -4
  47. package/dist/server/env.d.ts +7 -7
  48. package/dist/server/env.d.ts.map +1 -1
  49. package/dist/server/env.js +14 -14
  50. package/dist/server/static.d.ts +4 -4
  51. package/dist/server/static.js +7 -7
  52. package/dist/testing/CLAUDE.md +220 -46
  53. package/dist/testing/admin_integration.d.ts +18 -23
  54. package/dist/testing/admin_integration.d.ts.map +1 -1
  55. package/dist/testing/admin_integration.js +159 -201
  56. package/dist/testing/app_server.d.ts +125 -38
  57. package/dist/testing/app_server.d.ts.map +1 -1
  58. package/dist/testing/app_server.js +140 -42
  59. package/dist/testing/audit_completeness.d.ts +23 -22
  60. package/dist/testing/audit_completeness.d.ts.map +1 -1
  61. package/dist/testing/audit_completeness.js +199 -156
  62. package/dist/testing/bootstrap_success.d.ts +28 -0
  63. package/dist/testing/bootstrap_success.d.ts.map +1 -0
  64. package/dist/testing/bootstrap_success.js +144 -0
  65. package/dist/testing/cross_backend/capabilities.d.ts +64 -0
  66. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
  67. package/dist/testing/cross_backend/capabilities.js +47 -0
  68. package/dist/testing/cross_backend/setup.d.ts +215 -0
  69. package/dist/testing/cross_backend/setup.d.ts.map +1 -0
  70. package/dist/testing/cross_backend/setup.js +101 -0
  71. package/dist/testing/data_exposure.d.ts +14 -15
  72. package/dist/testing/data_exposure.d.ts.map +1 -1
  73. package/dist/testing/data_exposure.js +127 -146
  74. package/dist/testing/db_entities.d.ts +11 -1
  75. package/dist/testing/db_entities.d.ts.map +1 -1
  76. package/dist/testing/db_entities.js +13 -1
  77. package/dist/testing/integration.d.ts +35 -21
  78. package/dist/testing/integration.d.ts.map +1 -1
  79. package/dist/testing/integration.js +231 -291
  80. package/dist/testing/integration_helpers.d.ts +16 -6
  81. package/dist/testing/integration_helpers.d.ts.map +1 -1
  82. package/dist/testing/integration_helpers.js +7 -7
  83. package/dist/testing/mock_fs.d.ts.map +1 -1
  84. package/dist/testing/mock_fs.js +0 -2
  85. package/dist/testing/rate_limiting.d.ts.map +1 -1
  86. package/dist/testing/rate_limiting.js +9 -0
  87. package/dist/testing/role_grant_helpers.d.ts +31 -0
  88. package/dist/testing/role_grant_helpers.d.ts.map +1 -0
  89. package/dist/testing/role_grant_helpers.js +46 -0
  90. package/dist/testing/round_trip.d.ts +21 -16
  91. package/dist/testing/round_trip.d.ts.map +1 -1
  92. package/dist/testing/round_trip.js +65 -86
  93. package/dist/testing/rpc_round_trip.d.ts +24 -21
  94. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  95. package/dist/testing/rpc_round_trip.js +91 -104
  96. package/dist/testing/schema_introspect.d.ts +106 -0
  97. package/dist/testing/schema_introspect.d.ts.map +1 -0
  98. package/dist/testing/schema_introspect.js +123 -0
  99. package/dist/testing/schema_parity.d.ts +144 -0
  100. package/dist/testing/schema_parity.d.ts.map +1 -0
  101. package/dist/testing/schema_parity.js +233 -0
  102. package/dist/testing/standard.d.ts +57 -25
  103. package/dist/testing/standard.d.ts.map +1 -1
  104. package/dist/testing/standard.js +62 -5
  105. package/dist/testing/stubs.d.ts +11 -3
  106. package/dist/testing/stubs.d.ts.map +1 -1
  107. package/dist/testing/stubs.js +24 -21
  108. package/dist/testing/transports/surface_source.d.ts +51 -0
  109. package/dist/testing/transports/surface_source.d.ts.map +1 -0
  110. package/dist/testing/transports/surface_source.js +19 -0
  111. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"data_exposure.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/data_exposure.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAgB7B,OAAO,KAAK,EAAC,UAAU,EAAE,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG9D,OAAO,EAAwB,KAAK,SAAS,EAAC,MAAM,SAAS,CAAC;AAgB9D;;;;;GAKG;AACH,eAAO,MAAM,kCAAkC,GAAI,QAAQ,OAAO,KAAG,GAAG,CAAC,MAAM,CAuB9E,CAAC;AAIF;;GAEG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,UAAU,EACnB,mBAAkB,aAAa,CAAC,MAAM,CAA6B,KACjE,IAWF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,wCAAwC,GACpD,SAAS,UAAU,EACnB,oBAAmB,aAAa,CAAC,MAAM,CAA8B,KACnE,IAcF,CAAC;AAIF,kDAAkD;AAClD,MAAM,WAAW,uBAAuB;IACvC,4DAA4D;IAC5D,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,wCAAwC;IACxC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,4CAA4C;IAC5C,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,2FAA2F;IAC3F,gBAAgB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACzC,iGAAiG;IACjG,iBAAiB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC1C,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC,kDAAkD;IAClD,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GAAI,SAAS,uBAAuB,KAAG,IAmC/E,CAAC"}
1
+ {"version":3,"file":"data_exposure.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/data_exposure.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAqB7B,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,oBAAoB,CAAC;AAYnD,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,iCAAiC,CAAC;AACzE,OAAO,KAAK,EAAC,SAAS,EAAc,MAAM,0BAA0B,CAAC;AACrE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,gCAAgC,CAAC;AAIlE;;;;;GAKG;AACH,eAAO,MAAM,kCAAkC,GAAI,QAAQ,OAAO,KAAG,GAAG,CAAC,MAAM,CAuB9E,CAAC;AAIF;;GAEG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,UAAU,EACnB,mBAAkB,aAAa,CAAC,MAAM,CAA6B,KACjE,IAWF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,wCAAwC,GACpD,SAAS,UAAU,EACnB,oBAAmB,aAAa,CAAC,MAAM,CAA8B,KACnE,IAcF,CAAC;AAIF,kDAAkD;AAClD,MAAM,WAAW,uBAAuB;IACvC,kEAAkE;IAClE,UAAU,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,cAAc,EAAE,aAAa,CAAC;IAC9B,uCAAuC;IACvC,YAAY,EAAE,mBAAmB,CAAC;IAClC,2FAA2F;IAC3F,gBAAgB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACzC,iGAAiG;IACjG,iBAAiB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC1C,kDAAkD;IAClD,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GAAI,SAAS,uBAAuB,KAAG,IAiM/E,CAAC"}
@@ -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,15 @@ 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
+ if (options.surface_source.kind !== 'inline') {
97
+ throw new Error("describe_data_exposure_tests requires surface_source.kind === 'inline' — " +
98
+ 'the cross-process snapshot variant lands with the spawned-backend transport');
99
+ }
100
+ const { surface, route_specs } = options.surface_source.spec;
101
+ const { sensitive_fields = sensitive_field_blocklist, admin_only_fields = admin_only_field_blocklist, } = options;
102
+ const skip_set = new Set(options.skip_routes);
103
+ void options.capabilities;
96
104
  describe('data exposure — schema-level', () => {
97
- const { surface } = build();
98
105
  test('no sensitive fields in any output schema', () => {
99
106
  assert_output_schemas_no_sensitive_fields(surface, sensitive_fields);
100
107
  });
@@ -114,151 +121,125 @@ export const describe_data_exposure_tests = (options) => {
114
121
  }
115
122
  });
116
123
  });
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
- });
124
+ describe('data exposure — runtime', () => {
125
+ let fixture;
126
+ let authed_account;
127
+ let admin_account;
128
+ beforeAll(async () => {
129
+ fixture = await options.setup_test();
130
+ authed_account = await fixture.create_account({
131
+ username: 'exposure_authed',
132
+ roles: [],
150
133
  });
151
- afterAll(async () => {
152
- await test_app.cleanup();
153
- await factory.close(db);
134
+ admin_account = await fixture.create_account({
135
+ username: 'exposure_admin',
136
+ roles: [ROLE_ADMIN],
154
137
  });
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})`);
138
+ });
139
+ // Tests that don't fire authenticated requests run first — they don't
140
+ // invalidate sessions and are independent of test order.
141
+ test('unauthenticated error responses contain no sensitive fields', async () => {
142
+ const protected_specs = route_specs.filter((s) => !is_public_auth(s.auth));
143
+ for (const spec of protected_specs) {
144
+ const route_key = `${spec.method} ${spec.path}`;
145
+ if (skip_set.has(route_key))
146
+ continue;
147
+ const url = resolve_valid_path(spec.path, spec.params);
148
+ const res = await fixture.transport(url, {
149
+ method: spec.method,
150
+ headers: { host: 'localhost', origin: 'http://localhost:5173' },
151
+ });
152
+ if (res.headers.get('Content-Type')?.includes('text/event-stream')) {
153
+ await res.body?.cancel();
154
+ continue;
180
155
  }
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`);
156
+ let error_body;
157
+ try {
158
+ error_body = await res.clone().json();
205
159
  }
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;
160
+ catch {
161
+ continue;
162
+ }
163
+ assert_no_sensitive_fields_in_json(error_body, sensitive_fields, `unauthenticated ${route_key} (${res.status})`);
164
+ }
165
+ });
166
+ // Cross-privilege test runs before 2xx tests admin routes reject
167
+ // without calling handlers, so sessions stay intact.
168
+ test('admin routes return 403 for non-admin user', async () => {
169
+ const admin_specs = route_specs.filter((s) => s.auth.roles?.includes('admin') ?? false);
170
+ for (const spec of admin_specs) {
171
+ const route_key = `${spec.method} ${spec.path}`;
172
+ if (skip_set.has(route_key))
173
+ continue;
174
+ const url = resolve_valid_path(spec.path, spec.params);
175
+ const headers = authed_account.create_session_headers();
176
+ const res = await fixture.transport(url, {
177
+ method: spec.method,
178
+ headers,
219
179
  });
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
- }
180
+ assert.strictEqual(res.status, 403, `${route_key} should return 403 for non-admin user`);
181
+ let error_body;
182
+ try {
183
+ error_body = await res.clone().json();
184
+ }
185
+ catch {
186
+ continue;
260
187
  }
188
+ assert_no_sensitive_fields_in_json(error_body, sensitive_fields, `${route_key} 403`);
189
+ }
190
+ });
191
+ // 2xx tests run last — handlers like logout and session-revoke-all
192
+ // invalidate sessions as a side effect. Sort GET before POST so
193
+ // data-returning routes are checked before destructive routes fire.
194
+ test('all 2xx responses pass field blocklists', async () => {
195
+ // sort GET before mutations to check data-returning routes
196
+ // before destructive routes (logout, revoke-all) invalidate sessions
197
+ const sorted_specs = [...route_specs].sort((a, b) => {
198
+ if (a.method === 'GET' && b.method !== 'GET')
199
+ return -1;
200
+ if (a.method !== 'GET' && b.method === 'GET')
201
+ return 1;
202
+ return 0;
261
203
  });
204
+ for (const spec of sorted_specs) {
205
+ const route_key = `${spec.method} ${spec.path}`;
206
+ if (skip_set.has(route_key))
207
+ continue;
208
+ // keeper auth (daemon token) is strictly more privileged than admin
209
+ const is_elevated = is_keeper_auth(spec.auth) || (spec.auth.roles?.includes('admin') ?? false);
210
+ const url = resolve_valid_path(spec.path, spec.params);
211
+ const body = generate_valid_body(spec.input);
212
+ const headers = pick_auth_headers(spec, fixture, authed_account, admin_account);
213
+ const request_init = {
214
+ method: spec.method,
215
+ headers: {
216
+ ...headers,
217
+ ...(body ? { 'content-type': 'application/json' } : {}),
218
+ },
219
+ ...(body ? { body: JSON.stringify(body) } : {}),
220
+ };
221
+ const res = await fixture.transport(url, request_init);
222
+ if (res.headers.get('Content-Type')?.includes('text/event-stream')) {
223
+ await res.body?.cancel();
224
+ continue;
225
+ }
226
+ if (!res.ok)
227
+ continue;
228
+ let response_body;
229
+ try {
230
+ response_body = await res.clone().json();
231
+ }
232
+ catch {
233
+ continue;
234
+ }
235
+ assert_no_sensitive_fields_in_json(response_body, sensitive_fields, `${route_key} (${res.status})`);
236
+ // Admin-only field check applies to non-elevated routes with strict
237
+ // output schemas. Loose schemas (e.g. surface route returning JSON
238
+ // Schema representations) may contain admin field names as metadata.
239
+ if (!is_elevated && !is_null_schema(spec.output) && is_strict_object_schema(spec.output)) {
240
+ assert_no_sensitive_fields_in_json(response_body, admin_only_fields, `non-admin ${route_key} (${res.status})`);
241
+ }
242
+ }
262
243
  });
263
- }
244
+ });
264
245
  };
@@ -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,14 @@ 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`, bypassing
24
+ * the production offer/accept consent flow. Use only when the test focuses on
25
+ * revoke or isolation semantics rather than the consent path itself — the
26
+ * schema permits null `source_offer_id` for exactly this case
27
+ * (`account_schema.ts`). For tests that exercise the production grant flow,
28
+ * drive `role_grant_offer_create_action_spec` + `role_grant_offer_accept_action_spec`
29
+ * over RPC instead.
30
+ */
31
+ export declare const create_test_role_grant_direct: (db: Db, input: CreateRoleGrantInput) => Promise<RoleGrant>;
22
32
  //# 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;;;;;;;;GAQG;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,13 @@ 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`, bypassing
33
+ * the production offer/accept consent flow. Use only when the test focuses on
34
+ * revoke or isolation semantics rather than the consent path itself — the
35
+ * schema permits null `source_offer_id` for exactly this case
36
+ * (`account_schema.ts`). For tests that exercise the production grant flow,
37
+ * drive `role_grant_offer_create_action_spec` + `role_grant_offer_accept_action_spec`
38
+ * over RPC instead.
39
+ */
40
+ export const create_test_role_grant_direct = async (db, input) => query_create_role_grant({ db }, input);
@@ -1,25 +1,34 @@
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 { BackendCapabilities } from './cross_backend/capabilities.js';
5
+ import type { SetupTest } from './cross_backend/setup.js';
6
+ import type { SurfaceSource } from './transports/surface_source.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
+ * Source of the app surface for route iteration and error-coverage
19
+ * scoping. Currently requires `kind: 'inline'` — the cross-process
20
+ * snapshot variant lands alongside the spawned-backend transport plumbing.
21
+ */
22
+ surface_source: SurfaceSource;
23
+ /** Backend capability declarations — companion to `fixture.in_process` narrowing. */
24
+ capabilities: BackendCapabilities;
25
+ /**
26
+ * Session config — needed to resolve factory-form `rpc_endpoints`
27
+ * against a stub `AppServerContext` at setup time and to read
28
+ * `cookie_name` for manual cookie composition in the origin-verify
29
+ * cases.
21
30
  */
22
- db_factories?: Array<DbFactory>;
31
+ session_options: SessionOptions<string>;
23
32
  /**
24
33
  * RPC endpoint specs — required. This suite dispatches
25
34
  * `account_verify`, `account_session_*`, and `account_token_*` via
@@ -28,16 +37,21 @@ export interface StandardIntegrationTestOptions {
28
37
  * `require_rpc_endpoint_path` on setup so consumer projects see a
29
38
  * clear setup error instead of confusing test failures.
30
39
  *
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.
40
+ * Accepts either an array (eager) or a factory — see `rpc_helpers.ts`
41
+ * for the union semantics. The factory must return the same endpoint
42
+ * `path` regardless of ctx invoked once at setup with a stub ctx
43
+ * for path lookup; the running backend handles live dispatch.
39
44
  */
40
45
  rpc_endpoints: RpcEndpointsSuiteOption;
46
+ /**
47
+ * Minimum error-coverage ratio to enforce on the scoped REST surface
48
+ * (login / logout / password / signup + the shared RPC endpoint).
49
+ * Default `DEFAULT_INTEGRATION_ERROR_COVERAGE` (0.2). Set to `0` to
50
+ * skip the assertion entirely — useful for consumers with minimal
51
+ * route sets whose declared error codes outpace the suite's
52
+ * denial-path drivers.
53
+ */
54
+ error_coverage_min?: number;
41
55
  }
42
56
  /**
43
57
  * Standard integration test suite for fuz_app auth routes.
@@ -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,mBAAmB,EAAC,MAAM,iCAAiC,CAAC;AACzE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0BAA0B,CAAC;AACxD,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,gCAAgC,CAAC;AAElE;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C;;;;OAIG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,cAAc,EAAE,aAAa,CAAC;IAC9B,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;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,IAy3CF,CAAC"}