@fuzdev/fuz_app 0.58.0 → 0.60.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.
- package/dist/actions/CLAUDE.md +13 -8
- package/dist/actions/action_codegen.d.ts +1 -1
- package/dist/actions/action_codegen.js +2 -2
- package/dist/actions/action_event_helpers.d.ts +3 -3
- package/dist/actions/action_event_helpers.js +8 -8
- package/dist/actions/action_event_types.d.ts +3 -3
- package/dist/actions/action_event_types.js +3 -3
- package/dist/actions/transports_ws_auth_guard.d.ts +2 -2
- package/dist/actions/transports_ws_auth_guard.js +3 -3
- package/dist/auth/CLAUDE.md +215 -45
- package/dist/auth/account_action_specs.d.ts +9 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +9 -0
- package/dist/auth/actor_lookup_action_specs.d.ts +127 -0
- package/dist/auth/actor_lookup_action_specs.d.ts.map +1 -0
- package/dist/auth/actor_lookup_action_specs.js +93 -0
- package/dist/auth/actor_lookup_actions.d.ts +19 -0
- package/dist/auth/actor_lookup_actions.d.ts.map +1 -0
- package/dist/auth/actor_lookup_actions.js +32 -0
- package/dist/auth/actor_lookup_queries.d.ts +44 -0
- package/dist/auth/actor_lookup_queries.d.ts.map +1 -0
- package/dist/auth/actor_lookup_queries.js +42 -0
- package/dist/auth/actor_search_action_specs.d.ts +166 -0
- package/dist/auth/actor_search_action_specs.d.ts.map +1 -0
- package/dist/auth/actor_search_action_specs.js +139 -0
- package/dist/auth/actor_search_actions.d.ts +31 -0
- package/dist/auth/actor_search_actions.d.ts.map +1 -0
- package/dist/auth/actor_search_actions.js +61 -0
- package/dist/auth/actor_search_queries.d.ts +75 -0
- package/dist/auth/actor_search_queries.d.ts.map +1 -0
- package/dist/auth/actor_search_queries.js +91 -0
- package/dist/auth/admin_action_specs.d.ts +35 -0
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +35 -0
- package/dist/auth/admin_actions.js +2 -2
- package/dist/auth/all_action_spec_registries.d.ts +55 -0
- package/dist/auth/all_action_spec_registries.d.ts.map +1 -0
- package/dist/auth/all_action_spec_registries.js +59 -0
- package/dist/auth/audit_emitter.d.ts +1 -1
- package/dist/auth/audit_emitter.js +2 -2
- package/dist/auth/audit_log_queries.d.ts +1 -1
- package/dist/auth/audit_log_queries.js +3 -3
- package/dist/auth/audit_log_routes.d.ts +1 -1
- package/dist/auth/audit_log_routes.js +1 -1
- package/dist/auth/audit_log_schema.d.ts +5 -5
- package/dist/auth/audit_log_schema.js +7 -7
- package/dist/auth/auth_ddl.d.ts +7 -0
- package/dist/auth/auth_ddl.d.ts.map +1 -1
- package/dist/auth/auth_ddl.js +8 -0
- package/dist/auth/credential_type_schema.d.ts +1 -1
- package/dist/auth/credential_type_schema.js +3 -3
- package/dist/auth/grant_path_schema.d.ts +1 -1
- package/dist/auth/grant_path_schema.js +3 -3
- package/dist/auth/migrations.d.ts +4 -4
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +7 -6
- package/dist/auth/role_grant_offer_action_specs.d.ts +17 -0
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +17 -0
- package/dist/auth/role_grant_offer_actions.js +2 -2
- package/dist/auth/role_grant_offer_notifications.d.ts +2 -2
- package/dist/auth/role_grant_offer_notifications.js +2 -2
- package/dist/auth/role_grant_queries.d.ts +21 -0
- package/dist/auth/role_grant_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.js +31 -0
- package/dist/auth/role_schema.d.ts +2 -2
- package/dist/auth/role_schema.js +3 -3
- package/dist/auth/self_service_role_action_specs.d.ts +8 -0
- package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
- package/dist/auth/self_service_role_action_specs.js +8 -0
- package/dist/auth/self_service_role_actions.d.ts +1 -1
- package/dist/auth/self_service_role_actions.js +2 -2
- package/dist/auth/session_cookie.d.ts +1 -1
- package/dist/auth/session_cookie.js +1 -1
- package/dist/auth/session_middleware.d.ts +1 -1
- package/dist/auth/session_middleware.js +5 -5
- package/dist/rate_limiter.d.ts +5 -5
- package/dist/rate_limiter.js +6 -6
- package/dist/realtime/sse_auth_guard.d.ts +3 -3
- package/dist/realtime/sse_auth_guard.js +4 -4
- package/dist/server/app_backend.d.ts +3 -3
- package/dist/server/app_backend.js +4 -4
- package/dist/server/app_server.d.ts +1 -1
- package/dist/server/app_server.js +10 -10
- package/dist/testing/CLAUDE.md +22 -12
- package/dist/testing/admin_integration.js +4 -4
- package/dist/testing/app_server.d.ts +1 -1
- package/dist/testing/app_server.js +2 -2
- package/dist/testing/attack_surface.d.ts +4 -4
- package/dist/testing/attack_surface.js +6 -6
- package/dist/testing/audit_completeness.js +4 -4
- package/dist/testing/data_exposure.d.ts +2 -2
- package/dist/testing/data_exposure.js +7 -7
- package/dist/testing/db.d.ts +8 -8
- package/dist/testing/db.js +11 -11
- package/dist/testing/integration.js +4 -4
- package/dist/testing/integration_helpers.d.ts +6 -6
- package/dist/testing/integration_helpers.js +7 -7
- package/dist/testing/rate_limiting.js +4 -4
- package/dist/testing/round_trip.js +2 -2
- package/dist/testing/rpc_round_trip.js +2 -2
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +23 -2
- package/dist/testing/sse_round_trip.js +2 -2
- package/dist/testing/surface_invariants.d.ts +4 -4
- package/dist/testing/surface_invariants.js +5 -5
- package/package.json +1 -1
|
@@ -16,10 +16,10 @@ import { create_test_app } from './app_server.js';
|
|
|
16
16
|
import { create_pglite_factory } from './db.js';
|
|
17
17
|
import { resolve_valid_path, generate_valid_body } from './schema_generators.js';
|
|
18
18
|
import { run_migrations } from '../db/migrate.js';
|
|
19
|
-
import {
|
|
19
|
+
import { auth_migration_ns } from '../auth/migrations.js';
|
|
20
20
|
import { is_null_schema, is_strict_object_schema } from '../http/schema_helpers.js';
|
|
21
21
|
import { is_keeper_auth, is_public_auth } from '../http/auth_shape.js';
|
|
22
|
-
import {
|
|
22
|
+
import { sensitive_field_blocklist, admin_only_field_blocklist, assert_no_sensitive_fields_in_json, pick_auth_headers, } from './integration_helpers.js';
|
|
23
23
|
// --- Schema introspection ---
|
|
24
24
|
/**
|
|
25
25
|
* Recursively collect all property names from a JSON Schema.
|
|
@@ -58,7 +58,7 @@ export const collect_json_schema_property_names = (schema) => {
|
|
|
58
58
|
/**
|
|
59
59
|
* Assert that no output schema in the surface contains sensitive field names.
|
|
60
60
|
*/
|
|
61
|
-
export const assert_output_schemas_no_sensitive_fields = (surface, sensitive_fields =
|
|
61
|
+
export const assert_output_schemas_no_sensitive_fields = (surface, sensitive_fields = sensitive_field_blocklist) => {
|
|
62
62
|
for (const route of surface.routes) {
|
|
63
63
|
if (route.output_schema === null)
|
|
64
64
|
continue;
|
|
@@ -71,7 +71,7 @@ export const assert_output_schemas_no_sensitive_fields = (surface, sensitive_fie
|
|
|
71
71
|
/**
|
|
72
72
|
* Assert that non-admin route output schemas don't contain admin-only fields.
|
|
73
73
|
*/
|
|
74
|
-
export const assert_non_admin_schemas_no_admin_fields = (surface, admin_only_fields =
|
|
74
|
+
export const assert_non_admin_schemas_no_admin_fields = (surface, admin_only_fields = admin_only_field_blocklist) => {
|
|
75
75
|
const non_admin = surface.routes.filter((r) => !is_keeper_auth(r.auth) && !(r.auth.roles?.includes('admin') ?? false));
|
|
76
76
|
for (const route of non_admin) {
|
|
77
77
|
if (route.output_schema === null)
|
|
@@ -92,7 +92,7 @@ export const assert_non_admin_schemas_no_admin_fields = (surface, admin_only_fie
|
|
|
92
92
|
* contain no sensitive fields
|
|
93
93
|
*/
|
|
94
94
|
export const describe_data_exposure_tests = (options) => {
|
|
95
|
-
const { build, sensitive_fields =
|
|
95
|
+
const { build, sensitive_fields = sensitive_field_blocklist, admin_only_fields = admin_only_field_blocklist, } = options;
|
|
96
96
|
describe('data exposure — schema-level', () => {
|
|
97
97
|
const { surface } = build();
|
|
98
98
|
test('no sensitive fields in any output schema', () => {
|
|
@@ -118,10 +118,10 @@ export const describe_data_exposure_tests = (options) => {
|
|
|
118
118
|
};
|
|
119
119
|
// --- Runtime tests ---
|
|
120
120
|
const describe_data_exposure_runtime_tests = (options) => {
|
|
121
|
-
const { sensitive_fields =
|
|
121
|
+
const { sensitive_fields = sensitive_field_blocklist, admin_only_fields = admin_only_field_blocklist, } = options;
|
|
122
122
|
const skip_set = new Set(options.skip_routes);
|
|
123
123
|
const init_schema = async (db) => {
|
|
124
|
-
await run_migrations(db, [
|
|
124
|
+
await run_migrations(db, [auth_migration_ns]);
|
|
125
125
|
};
|
|
126
126
|
const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
|
|
127
127
|
for (const factory of factories) {
|
package/dist/testing/db.d.ts
CHANGED
|
@@ -61,25 +61,25 @@ export declare const create_pg_factory: (init_schema: (db: Db) => Promise<void>,
|
|
|
61
61
|
*
|
|
62
62
|
* Consumer projects can spread this into their own list and append app-specific tables.
|
|
63
63
|
*/
|
|
64
|
-
export declare const
|
|
64
|
+
export declare const auth_truncate_tables: string[];
|
|
65
65
|
/**
|
|
66
66
|
* Auth tables including `audit_log` — for integration tests that exercise
|
|
67
67
|
* the full middleware stack (login, admin, rate limiting).
|
|
68
68
|
*
|
|
69
|
-
* Separate from `
|
|
69
|
+
* Separate from `auth_truncate_tables` because unit-level DB tests that don't
|
|
70
70
|
* touch audit logging don't need to truncate it.
|
|
71
71
|
*/
|
|
72
|
-
export declare const
|
|
72
|
+
export declare const auth_integration_truncate_tables: string[];
|
|
73
73
|
/**
|
|
74
74
|
* All auth tables in drop order (children first for FK safety).
|
|
75
75
|
*
|
|
76
|
-
* The full set created by `
|
|
77
|
-
* test DB initialization. `
|
|
76
|
+
* The full set created by `auth_migrations` — use for clean-slate
|
|
77
|
+
* test DB initialization. `auth_truncate_tables` is the subset for
|
|
78
78
|
* between-test data cleanup (excludes `audit_log`).
|
|
79
79
|
*
|
|
80
|
-
* When adding tables to `
|
|
80
|
+
* When adding tables to `auth_migrations`, add them here too.
|
|
81
81
|
*/
|
|
82
|
-
export declare const
|
|
82
|
+
export declare const auth_drop_tables: readonly ["app_settings", "invite", "audit_log", "api_token", "auth_session", "role_grant", "role_grant_offer", "actor", "account", "bootstrap_lock"];
|
|
83
83
|
/**
|
|
84
84
|
* Drop all auth tables and schema version tracking for a clean slate.
|
|
85
85
|
*
|
|
@@ -89,7 +89,7 @@ export declare const AUTH_DROP_TABLES: readonly ["app_settings", "invite", "audi
|
|
|
89
89
|
* Safe on fresh databases (`IF EXISTS` on all statements). No-op effect for
|
|
90
90
|
* PGlite (already fresh), but harmless to call unconditionally.
|
|
91
91
|
*
|
|
92
|
-
* @mutates db - drops every table in `
|
|
92
|
+
* @mutates db - drops every table in `auth_drop_tables` plus `schema_version`.
|
|
93
93
|
*/
|
|
94
94
|
export declare const drop_auth_schema: (db: Db) => Promise<void>;
|
|
95
95
|
/**
|
package/dist/testing/db.js
CHANGED
|
@@ -10,10 +10,10 @@ import './assert_dev_env.js';
|
|
|
10
10
|
* ```ts
|
|
11
11
|
* import {create_pglite_factory, create_pg_factory} from '@fuzdev/fuz_app/testing/db.js';
|
|
12
12
|
* import {run_migrations} from '@fuzdev/fuz_app/db/migrate.js';
|
|
13
|
-
* import {
|
|
13
|
+
* import {auth_migration_ns} from '@fuzdev/fuz_app/auth/migrations.js';
|
|
14
14
|
*
|
|
15
15
|
* const init_schema = async (db) => {
|
|
16
|
-
* await run_migrations(db, [
|
|
16
|
+
* await run_migrations(db, [auth_migration_ns]);
|
|
17
17
|
* };
|
|
18
18
|
* const pglite_factory = create_pglite_factory(init_schema);
|
|
19
19
|
* const pg_factory = create_pg_factory(init_schema, process.env.TEST_DATABASE_URL);
|
|
@@ -160,7 +160,7 @@ export const create_pg_factory = (init_schema, test_url) => {
|
|
|
160
160
|
*
|
|
161
161
|
* Consumer projects can spread this into their own list and append app-specific tables.
|
|
162
162
|
*/
|
|
163
|
-
export const
|
|
163
|
+
export const auth_truncate_tables = [
|
|
164
164
|
'invite',
|
|
165
165
|
'api_token',
|
|
166
166
|
'auth_session',
|
|
@@ -173,20 +173,20 @@ export const AUTH_TRUNCATE_TABLES = [
|
|
|
173
173
|
* Auth tables including `audit_log` — for integration tests that exercise
|
|
174
174
|
* the full middleware stack (login, admin, rate limiting).
|
|
175
175
|
*
|
|
176
|
-
* Separate from `
|
|
176
|
+
* Separate from `auth_truncate_tables` because unit-level DB tests that don't
|
|
177
177
|
* touch audit logging don't need to truncate it.
|
|
178
178
|
*/
|
|
179
|
-
export const
|
|
179
|
+
export const auth_integration_truncate_tables = [...auth_truncate_tables, 'audit_log'];
|
|
180
180
|
/**
|
|
181
181
|
* All auth tables in drop order (children first for FK safety).
|
|
182
182
|
*
|
|
183
|
-
* The full set created by `
|
|
184
|
-
* test DB initialization. `
|
|
183
|
+
* The full set created by `auth_migrations` — use for clean-slate
|
|
184
|
+
* test DB initialization. `auth_truncate_tables` is the subset for
|
|
185
185
|
* between-test data cleanup (excludes `audit_log`).
|
|
186
186
|
*
|
|
187
|
-
* When adding tables to `
|
|
187
|
+
* When adding tables to `auth_migrations`, add them here too.
|
|
188
188
|
*/
|
|
189
|
-
export const
|
|
189
|
+
export const auth_drop_tables = [
|
|
190
190
|
'app_settings',
|
|
191
191
|
'invite',
|
|
192
192
|
'audit_log',
|
|
@@ -207,10 +207,10 @@ export const AUTH_DROP_TABLES = [
|
|
|
207
207
|
* Safe on fresh databases (`IF EXISTS` on all statements). No-op effect for
|
|
208
208
|
* PGlite (already fresh), but harmless to call unconditionally.
|
|
209
209
|
*
|
|
210
|
-
* @mutates db - drops every table in `
|
|
210
|
+
* @mutates db - drops every table in `auth_drop_tables` plus `schema_version`.
|
|
211
211
|
*/
|
|
212
212
|
export const drop_auth_schema = async (db) => {
|
|
213
|
-
for (const table of
|
|
213
|
+
for (const table of auth_drop_tables) {
|
|
214
214
|
await db.query(`DROP TABLE IF EXISTS ${assert_valid_sql_identifier(table)} CASCADE`);
|
|
215
215
|
}
|
|
216
216
|
await db.query('DROP TABLE IF EXISTS schema_version CASCADE');
|
|
@@ -17,9 +17,9 @@ import './assert_dev_env.js';
|
|
|
17
17
|
* @module
|
|
18
18
|
*/
|
|
19
19
|
import { describe, test, assert, beforeAll, afterAll } from 'vitest';
|
|
20
|
-
import {
|
|
20
|
+
import { auth_migration_ns } from '../auth/migrations.js';
|
|
21
21
|
import { create_test_app } from './app_server.js';
|
|
22
|
-
import { create_pglite_factory, create_describe_db,
|
|
22
|
+
import { create_pglite_factory, create_describe_db, auth_integration_truncate_tables, } from './db.js';
|
|
23
23
|
import { find_auth_route, assert_response_matches_spec, create_expired_test_cookie, assert_no_error_info_leakage, } from './integration_helpers.js';
|
|
24
24
|
import { find_rpc_action, rpc_call_for_spec, require_rpc_endpoint_path, resolve_rpc_endpoints_for_setup, } from './rpc_helpers.js';
|
|
25
25
|
import { RateLimiter } from '../rate_limiter.js';
|
|
@@ -70,10 +70,10 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
70
70
|
const rpc_endpoints_for_setup = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
|
|
71
71
|
const rpc_path = require_rpc_endpoint_path(rpc_endpoints_for_setup);
|
|
72
72
|
const init_schema = async (db) => {
|
|
73
|
-
await run_migrations(db, [
|
|
73
|
+
await run_migrations(db, [auth_migration_ns]);
|
|
74
74
|
};
|
|
75
75
|
const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
|
|
76
|
-
const describe_db = create_describe_db(factories,
|
|
76
|
+
const describe_db = create_describe_db(factories, auth_integration_truncate_tables);
|
|
77
77
|
describe_db('standard_integration', (get_db) => {
|
|
78
78
|
const { cookie_name } = options.session_options;
|
|
79
79
|
// Error coverage tracking across test groups
|
|
@@ -17,18 +17,18 @@ export declare const find_route_spec: (specs: Array<RouteSpec>, method: string,
|
|
|
17
17
|
* session/token CRUD, admin operations, and role_grant flows live on the RPC
|
|
18
18
|
* surface and should be reached via `rpc_call`.
|
|
19
19
|
*/
|
|
20
|
-
export declare const
|
|
21
|
-
export type RestAuthRouteSuffix = (typeof
|
|
20
|
+
export declare const rest_auth_route_suffixes: readonly ["/login", "/logout", "/password", "/verify", "/signup", "/bootstrap"];
|
|
21
|
+
export type RestAuthRouteSuffix = (typeof rest_auth_route_suffixes)[number];
|
|
22
22
|
/**
|
|
23
23
|
* Find a REST auth route by suffix and method.
|
|
24
24
|
*
|
|
25
25
|
* Decouples tests from consumer route prefix (`/api/account/login`,
|
|
26
26
|
* `/api/auth/login`, etc.). `suffix` must be one of
|
|
27
|
-
* `
|
|
27
|
+
* `rest_auth_route_suffixes` — throws otherwise so a post-migration RPC
|
|
28
28
|
* method name (e.g. `/sessions/revoke-all`) fails loudly at the call site
|
|
29
29
|
* instead of silently returning `undefined`.
|
|
30
30
|
*
|
|
31
|
-
* @throws Error if `suffix` is not in `
|
|
31
|
+
* @throws Error if `suffix` is not in `rest_auth_route_suffixes`.
|
|
32
32
|
*/
|
|
33
33
|
export declare const find_auth_route: (specs: Array<RouteSpec>, suffix: RestAuthRouteSuffix, method: RouteMethod) => RouteSpec | undefined;
|
|
34
34
|
/**
|
|
@@ -74,9 +74,9 @@ export declare const assert_rate_limit_retry_after_header: (response: Response,
|
|
|
74
74
|
retry_after: number;
|
|
75
75
|
}) => void;
|
|
76
76
|
/** Field names that must never appear in any HTTP response body. */
|
|
77
|
-
export declare const
|
|
77
|
+
export declare const sensitive_field_blocklist: ReadonlyArray<string>;
|
|
78
78
|
/** Field names that must not appear in non-admin HTTP response bodies. */
|
|
79
|
-
export declare const
|
|
79
|
+
export declare const admin_only_field_blocklist: ReadonlyArray<string>;
|
|
80
80
|
/**
|
|
81
81
|
* Recursively collect all key names from a parsed JSON value.
|
|
82
82
|
*
|
|
@@ -38,7 +38,7 @@ export const find_route_spec = (specs, method, path) => {
|
|
|
38
38
|
* session/token CRUD, admin operations, and role_grant flows live on the RPC
|
|
39
39
|
* surface and should be reached via `rpc_call`.
|
|
40
40
|
*/
|
|
41
|
-
export const
|
|
41
|
+
export const rest_auth_route_suffixes = [
|
|
42
42
|
'/login',
|
|
43
43
|
'/logout',
|
|
44
44
|
'/password',
|
|
@@ -51,15 +51,15 @@ export const REST_AUTH_ROUTE_SUFFIXES = [
|
|
|
51
51
|
*
|
|
52
52
|
* Decouples tests from consumer route prefix (`/api/account/login`,
|
|
53
53
|
* `/api/auth/login`, etc.). `suffix` must be one of
|
|
54
|
-
* `
|
|
54
|
+
* `rest_auth_route_suffixes` — throws otherwise so a post-migration RPC
|
|
55
55
|
* method name (e.g. `/sessions/revoke-all`) fails loudly at the call site
|
|
56
56
|
* instead of silently returning `undefined`.
|
|
57
57
|
*
|
|
58
|
-
* @throws Error if `suffix` is not in `
|
|
58
|
+
* @throws Error if `suffix` is not in `rest_auth_route_suffixes`.
|
|
59
59
|
*/
|
|
60
60
|
export const find_auth_route = (specs, suffix, method) => {
|
|
61
|
-
if (!
|
|
62
|
-
throw new Error(`find_auth_route: unknown suffix ${JSON.stringify(suffix)} — expected one of ${
|
|
61
|
+
if (!rest_auth_route_suffixes.includes(suffix)) {
|
|
62
|
+
throw new Error(`find_auth_route: unknown suffix ${JSON.stringify(suffix)} — expected one of ${rest_auth_route_suffixes.join(', ')}. Use rpc_call for RPC methods.`);
|
|
63
63
|
}
|
|
64
64
|
return specs.find((s) => s.method === method && s.path.endsWith(suffix));
|
|
65
65
|
};
|
|
@@ -207,9 +207,9 @@ export const assert_rate_limit_retry_after_header = (response, body) => {
|
|
|
207
207
|
};
|
|
208
208
|
// --- Data exposure helpers ---
|
|
209
209
|
/** Field names that must never appear in any HTTP response body. */
|
|
210
|
-
export const
|
|
210
|
+
export const sensitive_field_blocklist = ['password_hash', 'token_hash'];
|
|
211
211
|
/** Field names that must not appear in non-admin HTTP response bodies. */
|
|
212
|
-
export const
|
|
212
|
+
export const admin_only_field_blocklist = ['updated_by', 'created_by'];
|
|
213
213
|
/**
|
|
214
214
|
* Recursively collect all key names from a parsed JSON value.
|
|
215
215
|
*
|
|
@@ -14,9 +14,9 @@ import './assert_dev_env.js';
|
|
|
14
14
|
import { describe, test, assert } from 'vitest';
|
|
15
15
|
import { RateLimiter } from '../rate_limiter.js';
|
|
16
16
|
import { RateLimitError } from '../http/error_schemas.js';
|
|
17
|
-
import {
|
|
17
|
+
import { auth_migration_ns } from '../auth/migrations.js';
|
|
18
18
|
import { create_test_app } from './app_server.js';
|
|
19
|
-
import { create_pglite_factory, create_describe_db,
|
|
19
|
+
import { create_pglite_factory, create_describe_db, auth_integration_truncate_tables, } from './db.js';
|
|
20
20
|
import { find_auth_route, assert_rate_limit_retry_after_header } from './integration_helpers.js';
|
|
21
21
|
import { rpc_call_non_browser, require_rpc_endpoint_path, resolve_rpc_endpoints_for_setup, } from './rpc_helpers.js';
|
|
22
22
|
import { run_migrations } from '../db/migrate.js';
|
|
@@ -48,10 +48,10 @@ export const describe_rate_limiting_tests = (options) => {
|
|
|
48
48
|
const rpc_endpoints_for_setup = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
|
|
49
49
|
const rpc_path = require_rpc_endpoint_path(rpc_endpoints_for_setup);
|
|
50
50
|
const init_schema = async (db) => {
|
|
51
|
-
await run_migrations(db, [
|
|
51
|
+
await run_migrations(db, [auth_migration_ns]);
|
|
52
52
|
};
|
|
53
53
|
const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
|
|
54
|
-
const describe_db = create_describe_db(factories,
|
|
54
|
+
const describe_db = create_describe_db(factories, auth_integration_truncate_tables);
|
|
55
55
|
/** Create a tight rate limiter for testing — low attempt count, long window. */
|
|
56
56
|
const create_test_rate_limiter = () => new RateLimiter({ max_attempts, window_ms: 60_000, cleanup_interval_ms: 0 });
|
|
57
57
|
describe_db('rate_limiting', (get_db) => {
|
|
@@ -15,7 +15,7 @@ import { create_pglite_factory } from './db.js';
|
|
|
15
15
|
import { assert_response_matches_spec, pick_auth_headers } from './integration_helpers.js';
|
|
16
16
|
import { resolve_valid_path, generate_valid_body } from './schema_generators.js';
|
|
17
17
|
import { run_migrations } from '../db/migrate.js';
|
|
18
|
-
import {
|
|
18
|
+
import { auth_migration_ns } from '../auth/migrations.js';
|
|
19
19
|
import { create_stub_app_server_context } from './stubs.js';
|
|
20
20
|
/**
|
|
21
21
|
* Run schema-driven round-trip validation tests.
|
|
@@ -33,7 +33,7 @@ import { create_stub_app_server_context } from './stubs.js';
|
|
|
33
33
|
export const describe_round_trip_validation = (options) => {
|
|
34
34
|
const skip_set = new Set(options.skip_routes);
|
|
35
35
|
const init_schema = async (db) => {
|
|
36
|
-
await run_migrations(db, [
|
|
36
|
+
await run_migrations(db, [auth_migration_ns]);
|
|
37
37
|
};
|
|
38
38
|
const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
|
|
39
39
|
// Pre-compute route specs at describe time so test.each can register one
|
|
@@ -15,7 +15,7 @@ import { create_test_app, } from './app_server.js';
|
|
|
15
15
|
import { create_pglite_factory } from './db.js';
|
|
16
16
|
import { generate_valid_body } from './schema_generators.js';
|
|
17
17
|
import { run_migrations } from '../db/migrate.js';
|
|
18
|
-
import {
|
|
18
|
+
import { auth_migration_ns } from '../auth/migrations.js';
|
|
19
19
|
import { is_public_auth } from '../http/auth_shape.js';
|
|
20
20
|
import { create_rpc_post_init, create_rpc_get_url, assert_jsonrpc_error_response, assert_jsonrpc_success_response, resolve_rpc_endpoints_for_setup, } from './rpc_helpers.js';
|
|
21
21
|
/**
|
|
@@ -60,7 +60,7 @@ export const describe_rpc_round_trip_tests = (options) => {
|
|
|
60
60
|
// what the live dispatcher serves.
|
|
61
61
|
const rpc_endpoints_for_setup = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
|
|
62
62
|
const init_schema = async (db) => {
|
|
63
|
-
await run_migrations(db, [
|
|
63
|
+
await run_migrations(db, [auth_migration_ns]);
|
|
64
64
|
};
|
|
65
65
|
const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
|
|
66
66
|
for (const factory of factories) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema_generators.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_generators.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AACtB,OAAO,EAKN,KAAK,YAAY,EACjB,MAAM,yBAAyB,CAAC;AAIjC;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,cAAc,CAAC,CAAC,OAAO,KAAG,MAAM,GAAG,IAkBhE,CAAC;AA+FF,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,GAAI,OAAO,YAAY,EAAE,cAAc,CAAC,CAAC,OAAO,KAAG,
|
|
1
|
+
{"version":3,"file":"schema_generators.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_generators.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AACtB,OAAO,EAKN,KAAK,YAAY,EACjB,MAAM,yBAAyB,CAAC;AAIjC;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,cAAc,CAAC,CAAC,OAAO,KAAG,MAAM,GAAG,IAkBhE,CAAC;AA+FF,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,GAAI,OAAO,YAAY,EAAE,cAAc,CAAC,CAAC,OAAO,KAAG,OAgGnF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,gBAAgB,CAAC,CAAC,SAAS,KAAG,MAa9E,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAC/B,cAAc,CAAC,CAAC,OAAO,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SA6B5B,CAAC"}
|
|
@@ -158,8 +158,29 @@ export const generate_valid_value = (field, field_schema) => {
|
|
|
158
158
|
return 1;
|
|
159
159
|
case 'boolean':
|
|
160
160
|
return true;
|
|
161
|
-
case 'array':
|
|
162
|
-
|
|
161
|
+
case 'array': {
|
|
162
|
+
let min_items = 0;
|
|
163
|
+
try {
|
|
164
|
+
const json = z.toJSONSchema(field_schema);
|
|
165
|
+
if (typeof json.minItems === 'number')
|
|
166
|
+
min_items = json.minItems;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// no constraint
|
|
170
|
+
}
|
|
171
|
+
if (min_items === 0)
|
|
172
|
+
return [];
|
|
173
|
+
const def = zod_unwrap_def(field_schema);
|
|
174
|
+
const element_schema = def.element;
|
|
175
|
+
if (!element_schema)
|
|
176
|
+
return [];
|
|
177
|
+
const element_field = {
|
|
178
|
+
...field,
|
|
179
|
+
base_type: zod_get_base_type(element_schema),
|
|
180
|
+
};
|
|
181
|
+
const item = generate_valid_value(element_field, element_schema);
|
|
182
|
+
return Array.from({ length: min_items }, () => item);
|
|
183
|
+
}
|
|
163
184
|
case 'object': {
|
|
164
185
|
// Recursively generate valid nested objects
|
|
165
186
|
const nested_schema = zod_unwrap_to_object(field_schema);
|
|
@@ -21,7 +21,7 @@ import { create_pglite_factory } from './db.js';
|
|
|
21
21
|
import { find_route_spec, pick_auth_headers } from './integration_helpers.js';
|
|
22
22
|
import { rpc_call, require_rpc_endpoint_path, resolve_rpc_endpoints_for_setup, } from './rpc_helpers.js';
|
|
23
23
|
import { run_migrations } from '../db/migrate.js';
|
|
24
|
-
import {
|
|
24
|
+
import { auth_migration_ns } from '../auth/migrations.js';
|
|
25
25
|
import { account_session_revoke_all_action_spec } from '../auth/account_action_specs.js';
|
|
26
26
|
/**
|
|
27
27
|
* Read one complete SSE frame (up to `\n\n`) from a stream reader.
|
|
@@ -138,7 +138,7 @@ export const describe_sse_route_tests = (options) => {
|
|
|
138
138
|
const rpc_endpoints_for_setup = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
|
|
139
139
|
const rpc_path = require_rpc_endpoint_path(rpc_endpoints_for_setup);
|
|
140
140
|
const init_schema = async (db) => {
|
|
141
|
-
await run_migrations(db, [
|
|
141
|
+
await run_migrations(db, [auth_migration_ns]);
|
|
142
142
|
};
|
|
143
143
|
const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
|
|
144
144
|
for (const factory of factories) {
|
|
@@ -178,13 +178,13 @@ export interface ErrorSchemaTightnessOptions {
|
|
|
178
178
|
* them here instead of forcing every consumer to hand-maintain the entry.
|
|
179
179
|
*
|
|
180
180
|
* Paths assume the standard `/api/account` + `/api/db` prefixes used by every
|
|
181
|
-
* fuz_app consumer. Merged into `
|
|
181
|
+
* fuz_app consumer. Merged into `default_error_schema_tightness.allowlist` so
|
|
182
182
|
* consumers calling `assert_error_schema_tightness` directly inherit the
|
|
183
183
|
* exemptions; the standard attack-surface suite also prepends these entries
|
|
184
184
|
* underneath any consumer-supplied allowlist so project-specific entries are
|
|
185
185
|
* additive.
|
|
186
186
|
*/
|
|
187
|
-
export declare const
|
|
187
|
+
export declare const fuz_app_stock_route_tightness_allowlist: ReadonlyArray<string>;
|
|
188
188
|
/**
|
|
189
189
|
* Baseline error schema tightness applied by
|
|
190
190
|
* `describe_standard_attack_surface_tests` when no config is passed.
|
|
@@ -192,13 +192,13 @@ export declare const FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST: ReadonlyArray<stri
|
|
|
192
192
|
* Uses `min_specificity: 'enum'` (the assertion default) with `ignore_statuses`
|
|
193
193
|
* for middleware-derived status codes that are commonly generic (auth middleware
|
|
194
194
|
* produces multiple error codes at 401/403, and 429 comes from rate limiters),
|
|
195
|
-
* and `allowlist` seeded with `
|
|
195
|
+
* and `allowlist` seeded with `fuz_app_stock_route_tightness_allowlist` so
|
|
196
196
|
* fuz_app-shipped routes with heterogeneous generic schemas don't force every
|
|
197
197
|
* consumer to hand-maintain an identical allowlist. Consumers can pass a
|
|
198
198
|
* narrower config with project-specific `allowlist` entries, or pass `null`
|
|
199
199
|
* to skip the assertion entirely.
|
|
200
200
|
*/
|
|
201
|
-
export declare const
|
|
201
|
+
export declare const default_error_schema_tightness: ErrorSchemaTightnessOptions;
|
|
202
202
|
/**
|
|
203
203
|
* Assert that all error schemas meet a minimum specificity threshold.
|
|
204
204
|
*
|
|
@@ -467,13 +467,13 @@ const SPECIFICITY_ORDER = {
|
|
|
467
467
|
* them here instead of forcing every consumer to hand-maintain the entry.
|
|
468
468
|
*
|
|
469
469
|
* Paths assume the standard `/api/account` + `/api/db` prefixes used by every
|
|
470
|
-
* fuz_app consumer. Merged into `
|
|
470
|
+
* fuz_app consumer. Merged into `default_error_schema_tightness.allowlist` so
|
|
471
471
|
* consumers calling `assert_error_schema_tightness` directly inherit the
|
|
472
472
|
* exemptions; the standard attack-surface suite also prepends these entries
|
|
473
473
|
* underneath any consumer-supplied allowlist so project-specific entries are
|
|
474
474
|
* additive.
|
|
475
475
|
*/
|
|
476
|
-
export const
|
|
476
|
+
export const fuz_app_stock_route_tightness_allowlist = [];
|
|
477
477
|
/**
|
|
478
478
|
* Baseline error schema tightness applied by
|
|
479
479
|
* `describe_standard_attack_surface_tests` when no config is passed.
|
|
@@ -481,15 +481,15 @@ export const FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST = [];
|
|
|
481
481
|
* Uses `min_specificity: 'enum'` (the assertion default) with `ignore_statuses`
|
|
482
482
|
* for middleware-derived status codes that are commonly generic (auth middleware
|
|
483
483
|
* produces multiple error codes at 401/403, and 429 comes from rate limiters),
|
|
484
|
-
* and `allowlist` seeded with `
|
|
484
|
+
* and `allowlist` seeded with `fuz_app_stock_route_tightness_allowlist` so
|
|
485
485
|
* fuz_app-shipped routes with heterogeneous generic schemas don't force every
|
|
486
486
|
* consumer to hand-maintain an identical allowlist. Consumers can pass a
|
|
487
487
|
* narrower config with project-specific `allowlist` entries, or pass `null`
|
|
488
488
|
* to skip the assertion entirely.
|
|
489
489
|
*/
|
|
490
|
-
export const
|
|
490
|
+
export const default_error_schema_tightness = {
|
|
491
491
|
ignore_statuses: [401, 403, 429],
|
|
492
|
-
allowlist: [...
|
|
492
|
+
allowlist: [...fuz_app_stock_route_tightness_allowlist],
|
|
493
493
|
};
|
|
494
494
|
/**
|
|
495
495
|
* Assert that all error schemas meet a minimum specificity threshold.
|