@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.
Files changed (107) hide show
  1. package/dist/actions/CLAUDE.md +13 -8
  2. package/dist/actions/action_codegen.d.ts +1 -1
  3. package/dist/actions/action_codegen.js +2 -2
  4. package/dist/actions/action_event_helpers.d.ts +3 -3
  5. package/dist/actions/action_event_helpers.js +8 -8
  6. package/dist/actions/action_event_types.d.ts +3 -3
  7. package/dist/actions/action_event_types.js +3 -3
  8. package/dist/actions/transports_ws_auth_guard.d.ts +2 -2
  9. package/dist/actions/transports_ws_auth_guard.js +3 -3
  10. package/dist/auth/CLAUDE.md +215 -45
  11. package/dist/auth/account_action_specs.d.ts +9 -0
  12. package/dist/auth/account_action_specs.d.ts.map +1 -1
  13. package/dist/auth/account_action_specs.js +9 -0
  14. package/dist/auth/actor_lookup_action_specs.d.ts +127 -0
  15. package/dist/auth/actor_lookup_action_specs.d.ts.map +1 -0
  16. package/dist/auth/actor_lookup_action_specs.js +93 -0
  17. package/dist/auth/actor_lookup_actions.d.ts +19 -0
  18. package/dist/auth/actor_lookup_actions.d.ts.map +1 -0
  19. package/dist/auth/actor_lookup_actions.js +32 -0
  20. package/dist/auth/actor_lookup_queries.d.ts +44 -0
  21. package/dist/auth/actor_lookup_queries.d.ts.map +1 -0
  22. package/dist/auth/actor_lookup_queries.js +42 -0
  23. package/dist/auth/actor_search_action_specs.d.ts +166 -0
  24. package/dist/auth/actor_search_action_specs.d.ts.map +1 -0
  25. package/dist/auth/actor_search_action_specs.js +139 -0
  26. package/dist/auth/actor_search_actions.d.ts +31 -0
  27. package/dist/auth/actor_search_actions.d.ts.map +1 -0
  28. package/dist/auth/actor_search_actions.js +61 -0
  29. package/dist/auth/actor_search_queries.d.ts +75 -0
  30. package/dist/auth/actor_search_queries.d.ts.map +1 -0
  31. package/dist/auth/actor_search_queries.js +91 -0
  32. package/dist/auth/admin_action_specs.d.ts +35 -0
  33. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  34. package/dist/auth/admin_action_specs.js +35 -0
  35. package/dist/auth/admin_actions.js +2 -2
  36. package/dist/auth/all_action_spec_registries.d.ts +55 -0
  37. package/dist/auth/all_action_spec_registries.d.ts.map +1 -0
  38. package/dist/auth/all_action_spec_registries.js +59 -0
  39. package/dist/auth/audit_emitter.d.ts +1 -1
  40. package/dist/auth/audit_emitter.js +2 -2
  41. package/dist/auth/audit_log_queries.d.ts +1 -1
  42. package/dist/auth/audit_log_queries.js +3 -3
  43. package/dist/auth/audit_log_routes.d.ts +1 -1
  44. package/dist/auth/audit_log_routes.js +1 -1
  45. package/dist/auth/audit_log_schema.d.ts +5 -5
  46. package/dist/auth/audit_log_schema.js +7 -7
  47. package/dist/auth/auth_ddl.d.ts +7 -0
  48. package/dist/auth/auth_ddl.d.ts.map +1 -1
  49. package/dist/auth/auth_ddl.js +8 -0
  50. package/dist/auth/credential_type_schema.d.ts +1 -1
  51. package/dist/auth/credential_type_schema.js +3 -3
  52. package/dist/auth/grant_path_schema.d.ts +1 -1
  53. package/dist/auth/grant_path_schema.js +3 -3
  54. package/dist/auth/migrations.d.ts +4 -4
  55. package/dist/auth/migrations.d.ts.map +1 -1
  56. package/dist/auth/migrations.js +7 -6
  57. package/dist/auth/role_grant_offer_action_specs.d.ts +17 -0
  58. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  59. package/dist/auth/role_grant_offer_action_specs.js +17 -0
  60. package/dist/auth/role_grant_offer_actions.js +2 -2
  61. package/dist/auth/role_grant_offer_notifications.d.ts +2 -2
  62. package/dist/auth/role_grant_offer_notifications.js +2 -2
  63. package/dist/auth/role_grant_queries.d.ts +21 -0
  64. package/dist/auth/role_grant_queries.d.ts.map +1 -1
  65. package/dist/auth/role_grant_queries.js +31 -0
  66. package/dist/auth/role_schema.d.ts +2 -2
  67. package/dist/auth/role_schema.js +3 -3
  68. package/dist/auth/self_service_role_action_specs.d.ts +8 -0
  69. package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
  70. package/dist/auth/self_service_role_action_specs.js +8 -0
  71. package/dist/auth/self_service_role_actions.d.ts +1 -1
  72. package/dist/auth/self_service_role_actions.js +2 -2
  73. package/dist/auth/session_cookie.d.ts +1 -1
  74. package/dist/auth/session_cookie.js +1 -1
  75. package/dist/auth/session_middleware.d.ts +1 -1
  76. package/dist/auth/session_middleware.js +5 -5
  77. package/dist/rate_limiter.d.ts +5 -5
  78. package/dist/rate_limiter.js +6 -6
  79. package/dist/realtime/sse_auth_guard.d.ts +3 -3
  80. package/dist/realtime/sse_auth_guard.js +4 -4
  81. package/dist/server/app_backend.d.ts +3 -3
  82. package/dist/server/app_backend.js +4 -4
  83. package/dist/server/app_server.d.ts +1 -1
  84. package/dist/server/app_server.js +10 -10
  85. package/dist/testing/CLAUDE.md +22 -12
  86. package/dist/testing/admin_integration.js +4 -4
  87. package/dist/testing/app_server.d.ts +1 -1
  88. package/dist/testing/app_server.js +2 -2
  89. package/dist/testing/attack_surface.d.ts +4 -4
  90. package/dist/testing/attack_surface.js +6 -6
  91. package/dist/testing/audit_completeness.js +4 -4
  92. package/dist/testing/data_exposure.d.ts +2 -2
  93. package/dist/testing/data_exposure.js +7 -7
  94. package/dist/testing/db.d.ts +8 -8
  95. package/dist/testing/db.js +11 -11
  96. package/dist/testing/integration.js +4 -4
  97. package/dist/testing/integration_helpers.d.ts +6 -6
  98. package/dist/testing/integration_helpers.js +7 -7
  99. package/dist/testing/rate_limiting.js +4 -4
  100. package/dist/testing/round_trip.js +2 -2
  101. package/dist/testing/rpc_round_trip.js +2 -2
  102. package/dist/testing/schema_generators.d.ts.map +1 -1
  103. package/dist/testing/schema_generators.js +23 -2
  104. package/dist/testing/sse_round_trip.js +2 -2
  105. package/dist/testing/surface_invariants.d.ts +4 -4
  106. package/dist/testing/surface_invariants.js +5 -5
  107. 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 { AUTH_MIGRATION_NS } from '../auth/migrations.js';
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 { SENSITIVE_FIELD_BLOCKLIST, ADMIN_ONLY_FIELD_BLOCKLIST, assert_no_sensitive_fields_in_json, pick_auth_headers, } from './integration_helpers.js';
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 = SENSITIVE_FIELD_BLOCKLIST) => {
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 = ADMIN_ONLY_FIELD_BLOCKLIST) => {
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 = SENSITIVE_FIELD_BLOCKLIST, admin_only_fields = ADMIN_ONLY_FIELD_BLOCKLIST, } = options;
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 = SENSITIVE_FIELD_BLOCKLIST, admin_only_fields = ADMIN_ONLY_FIELD_BLOCKLIST, } = options;
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, [AUTH_MIGRATION_NS]);
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) {
@@ -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 AUTH_TRUNCATE_TABLES: string[];
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 `AUTH_TRUNCATE_TABLES` because unit-level DB tests that don't
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 AUTH_INTEGRATION_TRUNCATE_TABLES: string[];
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 `AUTH_MIGRATIONS` — use for clean-slate
77
- * test DB initialization. `AUTH_TRUNCATE_TABLES` is the subset for
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 `AUTH_MIGRATIONS`, add them here too.
80
+ * When adding tables to `auth_migrations`, add them here too.
81
81
  */
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"];
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 `AUTH_DROP_TABLES` plus `schema_version`.
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
  /**
@@ -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 {AUTH_MIGRATION_NS} from '@fuzdev/fuz_app/auth/migrations.js';
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, [AUTH_MIGRATION_NS]);
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 AUTH_TRUNCATE_TABLES = [
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 `AUTH_TRUNCATE_TABLES` because unit-level DB tests that don't
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 AUTH_INTEGRATION_TRUNCATE_TABLES = [...AUTH_TRUNCATE_TABLES, 'audit_log'];
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 `AUTH_MIGRATIONS` — use for clean-slate
184
- * test DB initialization. `AUTH_TRUNCATE_TABLES` is the subset for
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 `AUTH_MIGRATIONS`, add them here too.
187
+ * When adding tables to `auth_migrations`, add them here too.
188
188
  */
189
- export const AUTH_DROP_TABLES = [
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 `AUTH_DROP_TABLES` plus `schema_version`.
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 AUTH_DROP_TABLES) {
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 { AUTH_MIGRATION_NS } from '../auth/migrations.js';
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, AUTH_INTEGRATION_TRUNCATE_TABLES, } from './db.js';
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, [AUTH_MIGRATION_NS]);
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, AUTH_INTEGRATION_TRUNCATE_TABLES);
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 REST_AUTH_ROUTE_SUFFIXES: readonly ["/login", "/logout", "/password", "/verify", "/signup", "/bootstrap"];
21
- export type RestAuthRouteSuffix = (typeof REST_AUTH_ROUTE_SUFFIXES)[number];
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
- * `REST_AUTH_ROUTE_SUFFIXES` — throws otherwise so a post-migration RPC
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 `REST_AUTH_ROUTE_SUFFIXES`.
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 SENSITIVE_FIELD_BLOCKLIST: ReadonlyArray<string>;
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 ADMIN_ONLY_FIELD_BLOCKLIST: ReadonlyArray<string>;
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 REST_AUTH_ROUTE_SUFFIXES = [
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
- * `REST_AUTH_ROUTE_SUFFIXES` — throws otherwise so a post-migration RPC
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 `REST_AUTH_ROUTE_SUFFIXES`.
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 (!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.`);
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 SENSITIVE_FIELD_BLOCKLIST = ['password_hash', 'token_hash'];
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 ADMIN_ONLY_FIELD_BLOCKLIST = ['updated_by', 'created_by'];
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 { AUTH_MIGRATION_NS } from '../auth/migrations.js';
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, AUTH_INTEGRATION_TRUNCATE_TABLES, } from './db.js';
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, [AUTH_MIGRATION_NS]);
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, AUTH_INTEGRATION_TRUNCATE_TABLES);
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 { AUTH_MIGRATION_NS } from '../auth/migrations.js';
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, [AUTH_MIGRATION_NS]);
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 { AUTH_MIGRATION_NS } from '../auth/migrations.js';
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, [AUTH_MIGRATION_NS]);
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,OA+EnF,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"}
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
- return [];
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 { AUTH_MIGRATION_NS } from '../auth/migrations.js';
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, [AUTH_MIGRATION_NS]);
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 `DEFAULT_ERROR_SCHEMA_TIGHTNESS.allowlist` so
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 FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST: ReadonlyArray<string>;
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 `FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST` so
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 DEFAULT_ERROR_SCHEMA_TIGHTNESS: ErrorSchemaTightnessOptions;
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 `DEFAULT_ERROR_SCHEMA_TIGHTNESS.allowlist` so
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 FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST = [];
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 `FUZ_APP_STOCK_ROUTE_TIGHTNESS_ALLOWLIST` so
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 DEFAULT_ERROR_SCHEMA_TIGHTNESS = {
490
+ export const default_error_schema_tightness = {
491
491
  ignore_statuses: [401, 403, 429],
492
- allowlist: [...FUZ_APP_STOCK_ROUTE_TIGHTNESS_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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.58.0",
3
+ "version": "0.60.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",