@fuzdev/fuz_app 0.29.0 → 0.31.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 (210) hide show
  1. package/dist/actions/CLAUDE.md +630 -0
  2. package/dist/actions/action_rpc.d.ts +29 -0
  3. package/dist/actions/action_rpc.d.ts.map +1 -1
  4. package/dist/actions/action_rpc.js +42 -6
  5. package/dist/actions/action_types.d.ts +2 -2
  6. package/dist/actions/cancel.d.ts +12 -13
  7. package/dist/actions/cancel.d.ts.map +1 -1
  8. package/dist/actions/cancel.js +10 -13
  9. package/dist/actions/heartbeat.d.ts +8 -13
  10. package/dist/actions/heartbeat.d.ts.map +1 -1
  11. package/dist/actions/heartbeat.js +5 -8
  12. package/dist/actions/register_action_ws.d.ts +3 -3
  13. package/dist/actions/register_action_ws.js +2 -2
  14. package/dist/actions/register_ws_endpoint.d.ts +4 -4
  15. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  16. package/dist/actions/register_ws_endpoint.js +3 -3
  17. package/dist/actions/socket.svelte.d.ts +16 -16
  18. package/dist/actions/socket.svelte.d.ts.map +1 -1
  19. package/dist/actions/socket.svelte.js +15 -15
  20. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  21. package/dist/actions/transports_ws_backend.d.ts +15 -0
  22. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  23. package/dist/actions/transports_ws_backend.js +17 -0
  24. package/dist/auth/CLAUDE.md +923 -0
  25. package/dist/auth/account_action_specs.d.ts +216 -0
  26. package/dist/auth/account_action_specs.d.ts.map +1 -0
  27. package/dist/auth/account_action_specs.js +159 -0
  28. package/dist/auth/account_actions.d.ts +51 -0
  29. package/dist/auth/account_actions.d.ts.map +1 -0
  30. package/dist/auth/account_actions.js +119 -0
  31. package/dist/auth/account_queries.d.ts +6 -2
  32. package/dist/auth/account_queries.d.ts.map +1 -1
  33. package/dist/auth/account_queries.js +40 -4
  34. package/dist/auth/account_routes.d.ts +94 -16
  35. package/dist/auth/account_routes.d.ts.map +1 -1
  36. package/dist/auth/account_routes.js +108 -180
  37. package/dist/auth/account_schema.d.ts +85 -30
  38. package/dist/auth/account_schema.d.ts.map +1 -1
  39. package/dist/auth/account_schema.js +40 -8
  40. package/dist/auth/admin_action_specs.d.ts +674 -0
  41. package/dist/auth/admin_action_specs.d.ts.map +1 -0
  42. package/dist/auth/admin_action_specs.js +287 -0
  43. package/dist/auth/admin_actions.d.ts +69 -0
  44. package/dist/auth/admin_actions.d.ts.map +1 -0
  45. package/dist/auth/admin_actions.js +256 -0
  46. package/dist/auth/api_token.d.ts +10 -0
  47. package/dist/auth/api_token.d.ts.map +1 -1
  48. package/dist/auth/api_token.js +9 -0
  49. package/dist/auth/api_token_queries.d.ts +3 -3
  50. package/dist/auth/api_token_queries.js +3 -3
  51. package/dist/auth/app_settings_schema.d.ts +4 -3
  52. package/dist/auth/app_settings_schema.d.ts.map +1 -1
  53. package/dist/auth/app_settings_schema.js +2 -1
  54. package/dist/auth/audit_log_routes.d.ts +14 -6
  55. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  56. package/dist/auth/audit_log_routes.js +22 -79
  57. package/dist/auth/audit_log_schema.d.ts +100 -29
  58. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  59. package/dist/auth/audit_log_schema.js +83 -11
  60. package/dist/auth/bootstrap_routes.d.ts +14 -0
  61. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  62. package/dist/auth/bootstrap_routes.js +10 -3
  63. package/dist/auth/cleanup.d.ts +63 -0
  64. package/dist/auth/cleanup.d.ts.map +1 -0
  65. package/dist/auth/cleanup.js +80 -0
  66. package/dist/auth/invite_schema.d.ts +11 -10
  67. package/dist/auth/invite_schema.d.ts.map +1 -1
  68. package/dist/auth/invite_schema.js +4 -3
  69. package/dist/auth/migrations.d.ts +6 -0
  70. package/dist/auth/migrations.d.ts.map +1 -1
  71. package/dist/auth/migrations.js +28 -0
  72. package/dist/auth/permit_offer_action_specs.d.ts +364 -0
  73. package/dist/auth/permit_offer_action_specs.d.ts.map +1 -0
  74. package/dist/auth/permit_offer_action_specs.js +216 -0
  75. package/dist/auth/permit_offer_actions.d.ts +96 -0
  76. package/dist/auth/permit_offer_actions.d.ts.map +1 -0
  77. package/dist/auth/permit_offer_actions.js +428 -0
  78. package/dist/auth/permit_offer_notifications.d.ts +361 -0
  79. package/dist/auth/permit_offer_notifications.d.ts.map +1 -0
  80. package/dist/auth/permit_offer_notifications.js +179 -0
  81. package/dist/auth/permit_offer_queries.d.ts +165 -0
  82. package/dist/auth/permit_offer_queries.d.ts.map +1 -0
  83. package/dist/auth/permit_offer_queries.js +390 -0
  84. package/dist/auth/permit_offer_schema.d.ts +103 -0
  85. package/dist/auth/permit_offer_schema.d.ts.map +1 -0
  86. package/dist/auth/permit_offer_schema.js +142 -0
  87. package/dist/auth/permit_queries.d.ts +77 -14
  88. package/dist/auth/permit_queries.d.ts.map +1 -1
  89. package/dist/auth/permit_queries.js +119 -24
  90. package/dist/auth/session_queries.d.ts +4 -2
  91. package/dist/auth/session_queries.d.ts.map +1 -1
  92. package/dist/auth/session_queries.js +4 -2
  93. package/dist/auth/signup_routes.d.ts +13 -0
  94. package/dist/auth/signup_routes.d.ts.map +1 -1
  95. package/dist/auth/signup_routes.js +14 -7
  96. package/dist/http/CLAUDE.md +584 -0
  97. package/dist/http/pending_effects.d.ts +29 -0
  98. package/dist/http/pending_effects.d.ts.map +1 -0
  99. package/dist/http/pending_effects.js +31 -0
  100. package/dist/http/route_spec.d.ts.map +1 -1
  101. package/dist/http/route_spec.js +4 -3
  102. package/dist/rate_limiter.d.ts +30 -0
  103. package/dist/rate_limiter.d.ts.map +1 -1
  104. package/dist/rate_limiter.js +25 -2
  105. package/dist/realtime/sse_auth_guard.d.ts +2 -0
  106. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  107. package/dist/realtime/sse_auth_guard.js +5 -3
  108. package/dist/testing/CLAUDE.md +668 -1
  109. package/dist/testing/admin_integration.d.ts +10 -7
  110. package/dist/testing/admin_integration.d.ts.map +1 -1
  111. package/dist/testing/admin_integration.js +382 -482
  112. package/dist/testing/app_server.d.ts +7 -6
  113. package/dist/testing/app_server.d.ts.map +1 -1
  114. package/dist/testing/attack_surface.d.ts +9 -3
  115. package/dist/testing/attack_surface.d.ts.map +1 -1
  116. package/dist/testing/attack_surface.js +4 -4
  117. package/dist/testing/audit_completeness.d.ts +6 -0
  118. package/dist/testing/audit_completeness.d.ts.map +1 -1
  119. package/dist/testing/audit_completeness.js +158 -134
  120. package/dist/testing/auth_apps.d.ts.map +1 -1
  121. package/dist/testing/auth_apps.js +4 -33
  122. package/dist/testing/db.d.ts +1 -1
  123. package/dist/testing/db.d.ts.map +1 -1
  124. package/dist/testing/db.js +2 -0
  125. package/dist/testing/entities.d.ts +35 -13
  126. package/dist/testing/entities.d.ts.map +1 -1
  127. package/dist/testing/entities.js +17 -0
  128. package/dist/testing/integration.d.ts +10 -0
  129. package/dist/testing/integration.d.ts.map +1 -1
  130. package/dist/testing/integration.js +352 -340
  131. package/dist/testing/integration_helpers.d.ts +16 -5
  132. package/dist/testing/integration_helpers.d.ts.map +1 -1
  133. package/dist/testing/integration_helpers.js +24 -4
  134. package/dist/testing/rate_limiting.d.ts +7 -0
  135. package/dist/testing/rate_limiting.d.ts.map +1 -1
  136. package/dist/testing/rate_limiting.js +41 -10
  137. package/dist/testing/rpc_helpers.d.ts +153 -1
  138. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  139. package/dist/testing/rpc_helpers.js +184 -8
  140. package/dist/testing/sse_round_trip.d.ts +8 -0
  141. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  142. package/dist/testing/sse_round_trip.js +10 -3
  143. package/dist/testing/standard.d.ts +9 -1
  144. package/dist/testing/standard.d.ts.map +1 -1
  145. package/dist/testing/standard.js +6 -2
  146. package/dist/testing/surface_invariants.d.ts +7 -3
  147. package/dist/testing/surface_invariants.d.ts.map +1 -1
  148. package/dist/testing/surface_invariants.js +5 -4
  149. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  150. package/dist/testing/ws_round_trip.js +9 -38
  151. package/dist/ui/AccountSessions.svelte +8 -4
  152. package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
  153. package/dist/ui/AdminAccounts.svelte +61 -33
  154. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  155. package/dist/ui/AdminAuditLog.svelte +3 -2
  156. package/dist/ui/AdminAuditLog.svelte.d.ts.map +1 -1
  157. package/dist/ui/AdminInvites.svelte +3 -2
  158. package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
  159. package/dist/ui/AdminOverview.svelte +14 -9
  160. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  161. package/dist/ui/AdminPermitHistory.svelte +3 -2
  162. package/dist/ui/AdminPermitHistory.svelte.d.ts.map +1 -1
  163. package/dist/ui/AdminSessions.svelte +29 -25
  164. package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
  165. package/dist/ui/CLAUDE.md +351 -0
  166. package/dist/ui/OpenSignupToggle.svelte +6 -3
  167. package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
  168. package/dist/ui/PermitOfferForm.svelte +141 -0
  169. package/dist/ui/PermitOfferForm.svelte.d.ts +14 -0
  170. package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -0
  171. package/dist/ui/PermitOfferHistory.svelte +109 -0
  172. package/dist/ui/PermitOfferHistory.svelte.d.ts +11 -0
  173. package/dist/ui/PermitOfferHistory.svelte.d.ts.map +1 -0
  174. package/dist/ui/PermitOfferInbox.svelte +121 -0
  175. package/dist/ui/PermitOfferInbox.svelte.d.ts +12 -0
  176. package/dist/ui/PermitOfferInbox.svelte.d.ts.map +1 -0
  177. package/dist/ui/account_sessions_state.svelte.d.ts +53 -3
  178. package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
  179. package/dist/ui/account_sessions_state.svelte.js +39 -16
  180. package/dist/ui/admin_accounts_state.svelte.d.ts +118 -2
  181. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  182. package/dist/ui/admin_accounts_state.svelte.js +99 -23
  183. package/dist/ui/admin_invites_state.svelte.d.ts +47 -1
  184. package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
  185. package/dist/ui/admin_invites_state.svelte.js +38 -26
  186. package/dist/ui/admin_sessions_state.svelte.d.ts +26 -0
  187. package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
  188. package/dist/ui/admin_sessions_state.svelte.js +35 -21
  189. package/dist/ui/app_settings_state.svelte.d.ts +39 -0
  190. package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
  191. package/dist/ui/app_settings_state.svelte.js +34 -18
  192. package/dist/ui/audit_log_state.svelte.d.ts +40 -3
  193. package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
  194. package/dist/ui/audit_log_state.svelte.js +36 -42
  195. package/dist/ui/auth_state.svelte.d.ts +4 -3
  196. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  197. package/dist/ui/auth_state.svelte.js +4 -1
  198. package/dist/ui/permit_offers_state.svelte.d.ts +125 -0
  199. package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -0
  200. package/dist/ui/permit_offers_state.svelte.js +197 -0
  201. package/package.json +3 -3
  202. package/dist/auth/admin_routes.d.ts +0 -29
  203. package/dist/auth/admin_routes.d.ts.map +0 -1
  204. package/dist/auth/admin_routes.js +0 -226
  205. package/dist/auth/app_settings_routes.d.ts +0 -27
  206. package/dist/auth/app_settings_routes.d.ts.map +0 -1
  207. package/dist/auth/app_settings_routes.js +0 -66
  208. package/dist/auth/invite_routes.d.ts +0 -18
  209. package/dist/auth/invite_routes.d.ts.map +0 -1
  210. package/dist/auth/invite_routes.js +0 -129
@@ -1,3 +1,670 @@
1
1
  # testing/
2
2
 
3
- Every module in this directory starts with `import './assert_dev_env.js';` as its first line. This side-effect import throws at runtime if `DEV` (from `esm-env`) is false, preventing accidental inclusion in production bundles. Always add this import as the first line when creating new testing modules.
3
+ Composable test utilities exported to consumer projects. Stubs, factories,
4
+ attack-surface generators, middleware mocks, integration suites, and RPC/SSE/WS
5
+ round-trip harnesses. Consumers import these to assemble their own test suites
6
+ against a fuz_app-derived server.
7
+
8
+ For narrative wiring examples (how to call these from a consumer's vitest
9
+ setup), see `../../../docs/testing.md`. For fuz_app's own test suite
10
+ conventions (`.db.test.ts` suffix, the `db` vitest project, `assert_rejects`),
11
+ see `../../test/CLAUDE.md`. This file is a reference index for the helpers
12
+ themselves.
13
+
14
+ ## Production guard — always the first import
15
+
16
+ Every module in this directory starts with `import './assert_dev_env.js';`
17
+ as its first line. The side-effect import reads `DEV` from `esm-env` and
18
+ throws if it is false — preventing accidental inclusion in production
19
+ bundles. SvelteKit and Vite set `DEV` correctly for dev + tests; the
20
+ production code path explodes at the first testing-module import.
21
+
22
+ When adding a new module to this directory, make this import the first
23
+ line. The convention is enforced by grep, not by a linter — break it and
24
+ the production bundle still builds, then crashes at runtime on first
25
+ module load.
26
+
27
+ ## Stubs, factories, mocks
28
+
29
+ ### `stubs.ts` — `AppDeps` + `AppServerContext` stubs
30
+
31
+ | Helper | Role |
32
+ | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
33
+ | `create_throwing_stub<T>(label)` | Proxy whose every property access throws `Throwing stub 'label' — unexpected access to 'prop'`; JS-internal probes return `undefined`; `toJSON` returns `"[throwing_stub:label]"` so accidental serialization is visible rather than `{}`. |
34
+ | `create_noop_stub<T>(label, overrides?)` | Proxy whose every method returns `async () => undefined`; `overrides` lets callers pin specific props. |
35
+ | `stub` | Pre-built throwing stub labelled `'stub'`. |
36
+ | `create_stub_db()` | Returns a real `Db` whose `client.query` yields `{rows: []}` and whose `transaction(fn)` synchronously calls `fn(inner_stub_db)`. Safe for `apply_route_specs`'s declarative transaction wrapper. |
37
+ | `stub_handler()` | Returns a fresh `Response('stub')`. |
38
+ | `stub_mw` | Pass-through middleware handler (`async (_c, next) => next()`). |
39
+ | `stub_app_deps` | Frozen `AppDeps` — every capability is a throwing stub, `on_audit_event` is a noop. |
40
+ | `create_stub_app_deps()` | Factory returning fresh `AppDeps` with no-op FS/keyring/password, a `create_noop_stub` DB, silent `Logger`. |
41
+ | `create_stub_api_middleware({include_daemon_token?})` | Stub `MiddlewareSpec[]` matching `create_auth_middleware_specs`'s output (origin/session/request_context/bearer_auth, optional daemon_token) for surface generation without booting real auth. See `../auth/CLAUDE.md` §Middleware for the real stack. |
42
+ | `create_stub_app_server_context(session_options)` | Stub `AppServerContext` — rate limiters null, `bootstrap_status.available: false`, `app_settings.open_signup: false`. |
43
+ | `create_test_app_surface_spec(options)` | Builds an `AppSurfaceSpec` that mirrors `create_app_server`'s route assembly: consumer routes + factory-managed bootstrap routes (prefixed via `bootstrap_route_prefix`, default `'/api/account'`) + stub middleware + surface generation. `CreateTestAppSurfaceSpecOptions` accepts `session_options`, `create_route_specs`, `env_schema?`, `event_specs?`, `rpc_endpoints?`, `transform_middleware?`, `bootstrap_route_prefix?`. Single source of truth for attack-surface tests — track `create_app_server` wiring changes here. |
44
+
45
+ Throwing stubs surface mock escape: a test that accidentally reaches into
46
+ stub territory breaks immediately with a label-scoped error rather than
47
+ silently returning `undefined` or `{}`. Use throwing stubs by default;
48
+ use no-op stubs only when a dep is known to be reached with a don't-care
49
+ result.
50
+
51
+ ### `entities.ts` — test entity factories
52
+
53
+ Plain `(overrides?) => Entity` constructors with sensible defaults —
54
+ callers set only the fields the test cares about. Names prefix with
55
+ `create_test_*` to avoid collisions with real `account_queries.ts`
56
+ factories.
57
+
58
+ Override types widen branded `Uuid` fields to `string` so tests pass
59
+ literal ids without per-site casts — the factory brands internally.
60
+ Exported as `TestAccountOverrides` / `TestActorOverrides` /
61
+ `TestPermitOverrides` / `TestAuditEventOverrides`.
62
+
63
+ | Factory | Default id / role |
64
+ | ------------------------------------- | --------------------------------------------------------------------------------------------- |
65
+ | `create_test_account(overrides?)` | `{id: 'acct-test', username: 'test_user', …}` |
66
+ | `create_test_actor(overrides?)` | `{id: 'actor-test', account_id: 'acct-test', …}` |
67
+ | `create_test_permit(overrides?)` | `{id: 'permit-test', actor_id: 'actor-test', role: 'admin', scope_id: null, …}` |
68
+ | `create_test_context(permits?)` | `{account, actor, permits}` — pass `[{role: 'keeper'}, {role: 'admin'}]` for multi-role. |
69
+ | `create_test_audit_event(overrides?)` | `{id: 'evt-test', event_type: 'login', outcome: 'success', …}` — for SSE guard / audit tests. |
70
+
71
+ ### `mock_fs.ts` — in-memory filesystem
72
+
73
+ `create_mock_fs(initial_files?) => {read_file, write_file, get_file}`.
74
+ Missing-path reads throw an `Error` with `.code = 'ENOENT'` so callers
75
+ exercise the same branches as `node:fs`. Use for DI-based filesystem
76
+ tests; never replaces `node:fs` globally.
77
+
78
+ ## Database — `db.ts`
79
+
80
+ Factory builders for parameterized DB tests. Consumer projects pass their
81
+ `init_schema` callback (which calls `run_migrations(db, [AUTH_MIGRATION_NS, ...app_migrations])`);
82
+ factories accept any migration namespace set.
83
+
84
+ | Helper | Role |
85
+ | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
86
+ | `IS_CI` | `process.env.CI === 'true'` — CI detection. |
87
+ | `DbFactory` interface | `{name, create, close, skip, skip_reason?}`. |
88
+ | `reset_pglite(db)` | `DROP SCHEMA public CASCADE` + recreate. Reuses a live PGlite instance. |
89
+ | `create_pglite_factory(init_schema)` | In-memory; no external deps; `skip: false`. See WASM caching below. |
90
+ | `create_pg_factory(init_schema, test_url?)` | PostgreSQL; `skip: true` when `test_url` is missing; drops `schema_version` before `init_schema` so migrations re-evaluate against actual tables; pool is reused + cleaned up across `create()` calls. |
91
+ | `AUTH_TRUNCATE_TABLES` | `['invite', 'api_token', 'auth_session', 'permit', 'permit_offer', 'actor', 'account']` in FK-safe order. Excludes `audit_log` — unit DB tests don't need to truncate it. |
92
+ | `AUTH_INTEGRATION_TRUNCATE_TABLES` | `AUTH_TRUNCATE_TABLES + ['audit_log']` — for integration suites that exercise the audit path. |
93
+ | `AUTH_DROP_TABLES` | Full set from `AUTH_MIGRATIONS` in drop order; call `drop_auth_schema(db)` at the top of `init_schema` on persistent pg databases that may hold stale DDL from previous fuz_app versions. |
94
+ | `drop_auth_schema(db)` | `DROP TABLE IF EXISTS <table> CASCADE` for every entry in `AUTH_DROP_TABLES` plus `schema_version`. Safe on fresh DBs. |
95
+ | `create_describe_db(factories, truncate_tables)` | Returns `describe_db(name, fn)` that runs `fn(get_db)` once per factory, inside a `describe` block with shared `beforeAll(create)` + `beforeEach(TRUNCATE)` + `afterAll(close)`. Skipped factories use `describe.skip`. |
96
+ | `log_db_factory_status(factories)` | Console summary of enabled / skipped factories. |
97
+
98
+ **PGlite WASM caching.** `create_pglite_factory` shares a single PGlite
99
+ instance in a module-level ref (`module_db`) across all factories in the
100
+ same vitest worker thread. Subsequent `create()` calls
101
+ `DROP SCHEMA public CASCADE` instead of paying the ~500–700ms WASM
102
+ cold-start cost again. Since each vitest file runs in its own worker,
103
+ there is no cross-file contamination — but inside a file, suites share
104
+ state until the schema is reset. The `db` vitest project (opted into by
105
+ the `.db.test.ts` suffix) runs with `isolate: false` +
106
+ `fileParallelism: false` to amortize the WASM boot across every DB test
107
+ file in the run.
108
+
109
+ ## Test app assembly
110
+
111
+ ### `app_server.ts`
112
+
113
+ `create_test_app_server(options)` bootstraps a minimal `AppBackend` with a
114
+ keeper account, API token, session cookie, and signed `Keyring`.
115
+ `create_test_app(options)` layers `create_app_server` on top, returning a
116
+ fully assembled Hono app + the backend + helpers.
117
+
118
+ Key module-scope values:
119
+
120
+ - `stub_password_deps` — `PasswordHashDeps` that hashes via
121
+ `stub_hash_${password}` and verifies by equality. Deterministic, no
122
+ Argon2 cost — use for every test that isn't specifically exercising
123
+ password hashing.
124
+ - `TEST_COOKIE_SECRET` — 64-hex-char deterministic cookie secret.
125
+ Produces a valid `Keyring` via `create_validated_keyring`. Never used
126
+ in production — the stub guard plus fixed value is the contract.
127
+ - `fallback_pglite_factory` — module-level PGlite factory that
128
+ `create_test_app_server` uses when no `db` is passed. Reuses the WASM
129
+ cache via `create_pglite_factory`.
130
+
131
+ `bootstrap_test_account(options)` is extracted because both
132
+ `create_test_app_server` and `TestApp.create_account` reuse the same
133
+ "insert account + actor + roles + API token + session + cookie" flow.
134
+ Takes `{db, keyring, session_options, password, username?, password_value?, roles?}`.
135
+
136
+ | Type | Shape |
137
+ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
138
+ | `TestAppServer extends AppBackend` | Adds `account`, `actor`, `api_token`, `session_cookie`, `keyring`, `cleanup()`. |
139
+ | `TestAppServerOptions` | `session_options` (required), optional `db`, `db_type`, `password`, `username`, `password_value`, `roles`, `on_audit_event`. |
140
+ | `CreateTestAppOptions extends TestAppServerOptions` | Adds `create_route_specs` (required) + `app_options` (narrow `Partial<AppServerOptions>` excluding the three the helper manages). |
141
+ | `TestAccount` | `{account, actor, session_cookie, api_token, create_session_headers, create_bearer_headers}`. |
142
+ | `TestApp` | `{app, backend, surface_spec, surface, route_specs, create_session_headers, create_bearer_headers, create_daemon_token_headers, create_account, cleanup}`. |
143
+
144
+ `create_test_app` hard-codes the test-friendly `AppServerOptions`:
145
+ `allowed_origins: [/^http:\/\/localhost/]`, stub proxy pinned to
146
+ `127.0.0.1`, `env_schema: z.object({})`, every rate limiter `null`,
147
+ static daemon token state (no rotation, keeper already set),
148
+ **`await_pending_effects: true`** (fire-and-forget effects complete
149
+ before the response returns so tests can assert on side effects inline),
150
+ and silent logger. Override via `app_options`.
151
+
152
+ A fresh Hono app is created on every call because middleware closures
153
+ bind to the server's deps (db, keyring). Hono assembly is cheap
154
+ (~10–50ms); PGlite WASM caching in `db.ts` is where the real savings are.
155
+
156
+ ### `auth_apps.ts` — adversarial-auth app factories
157
+
158
+ Pre-built Hono apps at each auth level (public / authed / keeper / per-role)
159
+ for attack-surface testing. No middleware stack — a single `/*` middleware
160
+ injects the `REQUEST_CONTEXT_KEY` + `CREDENTIAL_TYPE_KEY` (default
161
+ `'session'`) and hands off to `apply_route_specs` with
162
+ `fuz_auth_guard_resolver`.
163
+
164
+ | Helper | Role |
165
+ | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
166
+ | `create_test_request_context(role?)` | Minimal `RequestContext` — one account, one actor, one permit for `role` (or none). |
167
+ | `create_test_app_from_specs(specs, auth_ctx?, credential_type?)` | Hono app with pre-set context + `apply_route_specs`. `credential_type` defaults to `'session'` when an auth context is supplied — override for `'daemon_token'` / `'api_token'` tests. |
168
+ | `AuthTestApps` | `{public, authed, keeper, by_role: Map<string, Hono>}`. |
169
+ | `create_auth_test_apps(specs, roles)` | Builds one app per auth level. Keeper app uses `credential_type: 'daemon_token'` so `require_keeper` passes. |
170
+ | `select_auth_app(apps, auth)` | Map `RouteAuth` → matching Hono app. Throws for missing `role:*` entries. |
171
+ | `resolve_test_path(path)` | `:foo` → `test_foo` — adequate for routes without format-constrained params. |
172
+
173
+ ## Assertions, coverage, helpers
174
+
175
+ ### `assertions.ts` — surface + error-schema assertions
176
+
177
+ | Helper | Role |
178
+ | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
179
+ | `resolve_fixture_path(filename, import_meta_url)` | Absolute path relative to the caller's module (use `import.meta.url`). |
180
+ | `assert_surface_matches_snapshot(surface, path)` | Compares live `AppSurface` against a committed JSON snapshot; failure message instructs `gro gen`. |
181
+ | `assert_surface_deterministic(build_surface)` | Build twice, `deepStrictEqual` the two results — catches nondeterminism in surface generation. |
182
+ | `assert_only_expected_public_routes(surface, list)` | Bidirectional: no unexpected public routes, no missing expected ones. Format: `['GET /health', 'POST /api/account/login']`. |
183
+ | `assert_full_middleware_stack(surface, prefix, mws)` | Every route under `prefix` has exactly `mws` as its middleware chain. |
184
+ | `get_route_error_schema(lookup, route, status)` | Read out of a pre-built merged-error-schema map. |
185
+ | `assert_error_schema_valid(lookup, route, status, body)` | Assert a schema exists + parses the body. |
186
+
187
+ ### `surface_invariants.ts` — structural + policy invariants
188
+
189
+ Structural invariants (options-free, apply universally):
190
+
191
+ | Assertion | Checks |
192
+ | ----------------------------------------- | ------------------------------------------------------------------------------------------------ |
193
+ | `assert_protected_routes_declare_401` | Every protected route has 401 in `error_schemas`. |
194
+ | `assert_role_routes_declare_403` | Every role/keeper route has 403. |
195
+ | `assert_input_routes_declare_400` | Every route with input has 400. |
196
+ | `assert_params_routes_declare_400` | Every route with params has 400. |
197
+ | `assert_query_routes_declare_400` | Every route with query has 400. |
198
+ | `assert_descriptions_present` | Every route has a non-empty description. |
199
+ | `assert_no_duplicate_routes` | No duplicate method+path pairs. |
200
+ | `assert_middleware_errors_propagated` | Every middleware-declared error status appears on every applicable route. |
201
+ | `assert_error_schemas_structurally_valid` | Every declared error schema has an `error` property at the top level (matches `ApiError`). |
202
+ | `assert_error_code_status_consistency` | The same `z.literal()` error code never appears at two different HTTP statuses. |
203
+ | `assert_404_schemas_use_specific_errors` | Routes with params declaring 404 must use `z.literal()` or `z.enum()`, not generic `z.string()`. |
204
+
205
+ Policy invariants (configurable, sensible defaults):
206
+
207
+ | Assertion | Checks |
208
+ | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
209
+ | `assert_sensitive_routes_rate_limited` | Routes matching `sensitive_route_patterns` (default: `/login`, `/password`, `/bootstrap`, `/tokens/create`) declare rate limiting or a 429 schema. |
210
+ | `assert_no_unexpected_public_mutations` | Public mutation routes must be in `public_mutation_allowlist`. |
211
+ | `assert_mutation_routes_use_post` | Routes with input schemas must not be GET (bypasses browser GET idempotency assumptions). |
212
+ | `assert_keeper_routes_under_prefix` | Keeper routes must be under `keeper_route_prefixes` (default `['/api/']`). |
213
+
214
+ Tightness audit:
215
+
216
+ - `audit_error_schema_tightness(surface) => Array<ErrorSchemaAuditEntry>` —
217
+ classifies every route × status combination as `'literal' | 'enum' | 'generic'`.
218
+ - `assert_error_schema_tightness(surface, options?)` — fails routes below a
219
+ threshold (`min_specificity`, default `'enum'`) with `allowlist` + `ignore_statuses` escape hatches.
220
+ - `DEFAULT_ERROR_SCHEMA_TIGHTNESS` — `{ignore_statuses: [401, 403, 429]}`
221
+ (middleware-injected codes that commonly use generic schemas). Applied
222
+ by `describe_standard_attack_surface_tests` when `error_schema_tightness`
223
+ is omitted; pass an override config or `null` to opt out.
224
+
225
+ Aggregate runners (called by the standard attack-surface suite):
226
+
227
+ - `assert_surface_invariants(surface)` — runs all structural assertions.
228
+ - `assert_surface_security_policy(surface, options?)` — runs all policy assertions.
229
+
230
+ ### `error_coverage.ts` — reachability tracking
231
+
232
+ `ErrorCoverageCollector` tracks which declared error paths get exercised.
233
+ Observations live in a `Set<string>` keyed by `"METHOD /spec-path:STATUS"` or
234
+ `"METHOD /spec-path:STATUS:CODE"` — the two shapes coexist and a
235
+ status-only observation satisfies the "any-code" coverage rule for all
236
+ declared codes at that status.
237
+
238
+ Methods:
239
+
240
+ - `record(specs, method, path, status, code?)` — resolves concrete paths
241
+ back to spec templates (e.g. `/api/accounts/abc` → `/api/accounts/:id`).
242
+ - `assert_and_record(specs, method, path, response, code?)` — wraps
243
+ `assert_response_matches_spec` and auto-extracts `body.error` from the
244
+ JSON body via `response.clone()`. Pass an explicit `code` when the
245
+ body was already consumed.
246
+ - `uncovered(specs, options?)` — per-status rows for generic schemas,
247
+ per-code rows for `z.literal` / `z.enum` schemas.
248
+
249
+ Support functions:
250
+
251
+ - `extract_declared_error_codes(schema)` — reads `schema.shape.error`;
252
+ returns the literal value(s) for `z.literal` / `z.enum`, `null`
253
+ otherwise.
254
+ - `assert_error_coverage(collector, specs, options?)` — logs
255
+ `[error coverage] covered/total (N.M%)` with uncovered list; fails
256
+ when `min_coverage > 0` and the ratio falls below.
257
+ - `DEFAULT_INTEGRATION_ERROR_COVERAGE = 0.2` — conservative baseline
258
+ for the standard integration/admin suites; consumers tighten as
259
+ their own test coverage matures.
260
+
261
+ ### `schema_generators.ts` — valid-value generation
262
+
263
+ Walks Zod schemas to generate valid values for adversarial/round-trip tests.
264
+
265
+ - `detect_format(field_schema)` — reads `format` / `pattern` from the
266
+ JSON Schema representation.
267
+ - `generate_valid_value(field, field_schema)` — base-type switch
268
+ producing a valid sample (UUIDs → nil UUID, strings → `'xxxxxxxxxx'`,
269
+ numbers → `1`, objects → recurse, enums → first entry, etc.).
270
+ Falls back through `/` + URL prefixes if a branded-string refinement
271
+ rejects the plain base.
272
+ - `resolve_valid_path(path, params_schema?)` — swaps `:param` for
273
+ valid-format values (nil UUID for UUID params, `test_param` otherwise).
274
+ - `generate_valid_body(input_schema) => Record<string, unknown> | undefined` —
275
+ builds a body that satisfies the input schema. Throws with Zod
276
+ `issues` if the generated body fails validation — surfaces broken
277
+ generation logic with a descriptive error rather than a confusing 400
278
+ downstream.
279
+
280
+ ### `integration_helpers.ts` — route lookup + body checks
281
+
282
+ | Helper | Role |
283
+ | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
284
+ | `find_route_spec(specs, method, path)` | Exact match then parameterized match (`:foo` matches any segment). |
285
+ | `find_auth_route(specs, suffix, method)` | Suffix-ending match for REST auth routes — decouples tests from consumer prefix. `suffix` is typed as `RestAuthRouteSuffix` and throws at runtime on unknown values (post-RPC-migration, only login/logout/password/verify/signup/bootstrap remain). |
286
+ | `assert_response_matches_spec(specs, method, path, response)` | 2xx → validates against `spec.output`; non-2xx → validates against merged error schemas for that status. Non-JSON responses allowed only when no schema applies. |
287
+ | `create_expired_test_cookie(keyring, session_options)` | Validly signed cookie with `expires_at` in 1970. |
288
+ | `check_error_response_fields(body)` | Returns the list of fields outside `KNOWN_SAFE_ERROR_FIELDS` (`error`, `issues`, `required_role`, `retry_after`, `credential_type`, `has_references`, `ok`). |
289
+ | `assert_no_error_info_leakage(body, context)` | Rejects field-name patterns (`stack`, `trace`, `sql`, …) + value patterns (`node_modules`, stack-like `at …`, `.ts:NN`). |
290
+ | `assert_rate_limit_retry_after_header(response, body)` | `Retry-After` numeric header equals `Math.ceil(body.retry_after)`. |
291
+ | `SENSITIVE_FIELD_BLOCKLIST` | `['password_hash', 'token_hash']` — never in any response body. |
292
+ | `ADMIN_ONLY_FIELD_BLOCKLIST` | `['updated_by', 'created_by']` — never in non-admin response bodies. |
293
+ | `collect_json_keys_recursive(value)` | Deep walk; returns `Set<string>` of every key at every nesting depth. |
294
+ | `assert_no_sensitive_fields_in_json(body, blocklist, context)` | Rejects any key in the blocklist at any depth. |
295
+ | `pick_auth_headers(spec, test_app, authed_account, admin_account)` | `RouteAuth` → appropriate test credentials; role `admin` uses `admin_account`, other roles use bootstrapped keeper, `keeper` uses daemon token. |
296
+
297
+ ## Attack surface suites
298
+
299
+ ### `attack_surface.ts` — `describe_standard_attack_surface_tests`
300
+
301
+ Single-call bundle of 5 top-level groups (10 named tests + every
302
+ adversarial case per route):
303
+
304
+ 1. **attack surface snapshot** — `matches committed snapshot`, `is deterministic`.
305
+ 2. **attack surface structure** — `only expected public routes`, `full middleware stack on API routes`, `surface invariants`, `security policy`, `error schema tightness` (logs counts and asserts against `DEFAULT_ERROR_SCHEMA_TIGHTNESS` by default; pass an override config or `null` via `error_schema_tightness`).
306
+ 3. **adversarial HTTP auth enforcement** — `unauthenticated → 401`, `wrong role → 403` × roles, `authenticated without role → 403`, `keeper routes reject session credential → 403`, `correct auth passes guard`.
307
+ 4. **adversarial input validation** — delegated to `describe_adversarial_input`.
308
+ 5. **adversarial 404 response validation** — delegated to `describe_adversarial_404`.
309
+
310
+ Options: `{build: () => AppSurfaceSpec, snapshot_path, expected_public_routes, expected_api_middleware, roles, api_path_prefix?, security_policy?, error_schema_tightness?}`.
311
+
312
+ Also exported: `describe_adversarial_auth(options)` (groups 3 on its own)
313
+ and `build_error_schema_lookup(specs, middleware_specs?)` (pre-built
314
+ `Map<string, RouteErrorSchemas>` for per-response validation).
315
+
316
+ ### `adversarial_input.ts` — schema-walk payload generation
317
+
318
+ `describe_adversarial_input({build, roles})` — fires input body / params /
319
+ query validation failures at every route with correct-auth credentials
320
+ so validation middleware is actually exercised (not short-circuited by
321
+ 401). All cases expect 400 with one of `ERROR_INVALID_REQUEST_BODY` /
322
+ `_INVALID_JSON_BODY` / `_INVALID_ROUTE_PARAMS` / `_INVALID_QUERY_PARAMS`.
323
+
324
+ Exported generators:
325
+
326
+ - `generate_input_test_cases(input_schema)` — whole-body structural
327
+ (non-object, extra key when `strictObject`), missing required fields,
328
+ one wrong-type per field, null for required non-nullable, one format
329
+ violation per constrained field, numeric/array/string boundary cases
330
+ via JSON Schema introspection.
331
+ - `generate_params_test_cases(params_schema)` — format violations only
332
+ (unconstrained string params accept anything).
333
+ - `generate_query_test_cases(query_schema)` — missing required +
334
+ format violations.
335
+
336
+ GET-with-input routes hit the RPC `?params=` query convention; invalid-
337
+ JSON arrays there collapse to `ERROR_INVALID_REQUEST_BODY` (schema
338
+ failure) rather than `ERROR_INVALID_JSON_BODY`.
339
+
340
+ ### `adversarial_404.ts` — 404 schema conformance
341
+
342
+ `describe_adversarial_404({build, roles})` — for every route with
343
+ `params` + 404 in `error_schemas` + an extractable error code
344
+ (`z.literal` or first `z.enum`), replaces the handler with a stub
345
+ returning `{error: <code>}`, fires with nil-UUID params, asserts 404 +
346
+ body matches the declared 404 Zod schema. No DB needed.
347
+
348
+ ### `adversarial_headers.ts` — header injection suite
349
+
350
+ `describe_standard_adversarial_headers(suite_name, options, allowed_origin, extra_cases?)`
351
+ — 7 standard cases:
352
+
353
+ 1. bearer + rogue Origin → 403 `ERROR_FORBIDDEN_ORIGIN`
354
+ 2. bearer + allowed Origin → bearer silently discarded (browser context)
355
+ 3. no auth headers → passes through
356
+ 4. bearer + empty Origin → 403 `ERROR_FORBIDDEN_ORIGIN` (defense-in-depth)
357
+ 5. lowercase `bearer` scheme → RFC 7235 §2.1 soft-fail
358
+ 6. bearer + rogue Referer → 403 `ERROR_FORBIDDEN_REFERER`
359
+ 7. bearer + allowed Referer → bearer silently discarded
360
+
361
+ Each case declares `validate_expectation: 'called' | 'not_called'` so the
362
+ suite asserts that short-circuit middleware actually fires before token
363
+ validation. Extra cases append to the standard list.
364
+
365
+ ## Middleware stack — `middleware.ts`
366
+
367
+ Module-level `vi.mock()` for the four query modules bearer auth touches:
368
+ `api_token_queries`, `account_queries`, `permit_queries`. Because
369
+ `vi.mock()` is hoisted, these run before any imports resolve — so any
370
+ test file that imports from `middleware.ts` gets these mocks globally.
371
+ Pair with `vi.restoreAllMocks()` in `afterEach` when mixing into
372
+ `.db.test.ts` files (see DB test caveat below).
373
+
374
+ | Helper | Role |
375
+ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
376
+ | `BearerAuthTestOptions`, `BearerAuthTestCase` | Test-case table shape for the bearer auth runner. |
377
+ | `create_bearer_auth_mocks(tc)` | Configures the module-level mocks per test case; returns spy references. |
378
+ | `TEST_CLIENT_IP = '127.0.0.1'` | IP set by the proxy stub in `create_bearer_auth_test_app`. |
379
+ | `create_bearer_auth_test_app(tc, ip_rate_limiter?)` | Hono app with bearer middleware + echo route at `/api/test` returning `{ok, has_context, credential_type, account_id, actor_id, permit_count, api_token_id}`. |
380
+ | `describe_bearer_auth_cases(suite_name, cases, ip_rate_limiter?)` | Table-driven runner — one `test()` per case; asserts status, error, body fields, `api_token_id`, context preservation. |
381
+ | `TEST_MIDDLEWARE_PATH = '/api/test'` | Path used by the echo route in the stack factory. |
382
+ | `create_test_middleware_stack_app(options?)` | Real proxy + origin + bearer middleware for integration-shape testing. Echo route returns `{ok, client_ip, has_context}`. |
383
+
384
+ The echo route under `create_bearer_auth_test_app` deliberately surfaces
385
+ every middleware-written context variable (`REQUEST_CONTEXT_KEY`,
386
+ `CREDENTIAL_TYPE_KEY`, `AUTH_API_TOKEN_ID_KEY`). When public auth surface
387
+ gains a new context variable, header, or field, update this echo
388
+ alongside the assertions in `src/test/auth/*.test.ts` — the two move
389
+ together.
390
+
391
+ ## Round-trip suites
392
+
393
+ ### `round_trip.ts` — `describe_round_trip_validation`
394
+
395
+ For every route spec, fires a valid request with matching auth and
396
+ validates the response against declared schemas. DB-backed via
397
+ `create_test_app`. Per-route test (`test.each`) — one line per route
398
+ in the vitest output.
399
+
400
+ Options: `{session_options, create_route_specs, app_options?, db_factories?, skip_routes?, input_overrides?}`.
401
+ `input_overrides` is a `Map<"METHOD /path", body>` — override generated
402
+ bodies for routes whose input schema can't round-trip cleanly (e.g.
403
+ fields that must reference DB state).
404
+
405
+ SSE routes are skipped by Content-Type sniff; `describe_sse_route_tests`
406
+ picks them up separately.
407
+
408
+ ### `rpc_round_trip.ts` — `describe_rpc_round_trip_tests`
409
+
410
+ DB-backed round-trip for RPC: one POST test for all methods, one GET
411
+ test for `side_effects: false` methods. Successful responses validate
412
+ against `action.spec.output`; error responses validate as well-formed
413
+ JSON-RPC error envelopes. Required: `{session_options, create_route_specs, rpc_endpoints, ...}`.
414
+ The admin RPC auth test picks a session-based identity (`authed` /
415
+ `admin` / bootstrapped keeper) based on `method.auth`; keeper uses the
416
+ daemon token.
417
+
418
+ ### `sse_round_trip.ts` — `describe_sse_route_tests`
419
+
420
+ Per SSE route: open stream with matching auth, assert the
421
+ `SSE_CONNECTED_COMMENT` comment, fire a consumer-supplied `trigger()`,
422
+ validate the next `data:` frame as `{method, params}` against declared
423
+ `EventSpec`s, then (by default) fire `POST /api/account/sessions/revoke-all`
424
+ and assert the stream closes within 2s.
425
+
426
+ `SseRouteTestSpec` per route: `{path, trigger, event_specs?, assert_closes_on_revoke?}`.
427
+ Pass `on_audit_event` on the suite options to wire a close-on-revoke
428
+ guard (e.g. via `create_sse_auth_guard`) for consumer SSE registries —
429
+ without it, the revoke assertion hangs because the guard never fires.
430
+
431
+ Frame reader (`create_sse_frame_reader`) is internal but handles
432
+ `\n\n` framing, a 2s per-read timeout (prevents vitest hangs), and
433
+ `wait_for_close` for the revocation check.
434
+
435
+ ### `ws_round_trip.ts` — WebSocket harness (non-HTTP)
436
+
437
+ In-process test driver for `register_action_ws`. Consumers pass specs +
438
+ handlers, receive `{transport, connect()}` back. The full dispatch path
439
+ is exercised (per-action auth, input validation, `ctx.notify`,
440
+ broadcast via `BackendWebsocketTransport`, close-on-revoke), but Hono's
441
+ wire upgrade is skipped (the Node test runtime has no
442
+ `@hono/node-ws` adapter).
443
+
444
+ Three layers:
445
+
446
+ 1. **Primitives** — `create_fake_ws()`, `create_fake_hono_context(opts)`,
447
+ `create_stub_upgrade()`, `MinimalActionEnvironment`,
448
+ `dispatch_ws_message(on_message, event, ws)`.
449
+ 2. **Harness** — `create_ws_test_harness<TCtx>({actions, extend_context?, transport?, heartbeat?, log?, on_socket_open?, on_socket_close?})` → `WsTestHarness`. `connect(identity?)` is async and resolves after `on_socket_open` completes, so broadcasts sent immediately after `await harness.connect()` reach the client.
450
+ 3. **Round-trip helpers** — `is_notification(method)`,
451
+ `is_notification_with<P>(method, match)` (type-guard combinator —
452
+ narrows `wait_for` return type), `is_response_for(id)`.
453
+ `JsonrpcNotificationFrame<P>` / `JsonrpcSuccessResponseFrame<R>` /
454
+ `JsonrpcErrorResponseFrame<D>` — typed wire-frame shapes distinct
455
+ from the runtime Zod schemas in `http/jsonrpc.ts` (generic over
456
+ `params` / `result` / `data` so tests narrow without casts).
457
+ `build_broadcast_api<TApi>({harness, specs})` — wires a typed
458
+ broadcast API against the harness transport.
459
+
460
+ `MockWsClient`: `{send, request<R>, close, messages, wait_for}`.
461
+ `request` throws with code + message + data on error frames (so
462
+ asserting `result.foo` on a failed request surfaces the real cause,
463
+ not a `Cannot read property 'foo' of undefined`). `wait_for(predicate,
464
+ timeout_ms?)` checks already-received messages first, then waits for
465
+ new arrivals (default 1000ms); drops the waiter on timeout so the
466
+ `waiters` array doesn't grow.
467
+
468
+ `keeper_identity()` — convenience for `{credential_type: 'daemon_token', roles: [ROLE_KEEPER]}`.
469
+
470
+ ## Data exposure + rate limiting
471
+
472
+ ### `data_exposure.ts` — `describe_data_exposure_tests`
473
+
474
+ Six tests in two top-level groups:
475
+
476
+ 1. **schema-level** (3 tests, no DB) — walks JSON Schema representations:
477
+ - `no sensitive fields in any output schema` — `SENSITIVE_FIELD_BLOCKLIST`
478
+ - `no admin-only fields in non-admin output schemas` — `ADMIN_ONLY_FIELD_BLOCKLIST`
479
+ - `no sensitive fields in any error schema`
480
+ 2. **runtime** (3 tests, DB-backed via `create_test_app`):
481
+ - `unauthenticated error responses contain no sensitive fields`
482
+ - `admin routes return 403 for non-admin user` — cross-privilege check
483
+ - `all 2xx responses pass field blocklists` — GETs sorted before POSTs so data-returning routes fire before destructive ones (logout, revoke-all) invalidate sessions
484
+
485
+ Support functions: `collect_json_schema_property_names(schema)` (walks
486
+ `properties`/`items`/`allOf`/`anyOf`/`oneOf`/`additionalProperties`),
487
+ `assert_output_schemas_no_sensitive_fields(surface, fields?)`,
488
+ `assert_non_admin_schemas_no_admin_fields(surface, fields?)`.
489
+
490
+ Options: `{build, session_options, create_route_specs, sensitive_fields?, admin_only_fields?, app_options?, db_factories?, skip_routes?}`.
491
+
492
+ ### `rate_limiting.ts` — `describe_rate_limiting_tests`
493
+
494
+ Three test groups:
495
+
496
+ 1. IP rate limiting on login — fires `max_attempts + 1` requests; last one should be 429 with `RateLimitError` body + valid `Retry-After` header.
497
+ 2. Per-account rate limiting on login — same username exhausts the bucket; a different username is not blocked.
498
+ 3. Bearer auth IP rate limiting — invalid bearer tokens exhaust the IP bucket via the `account_verify` RPC method.
499
+
500
+ Each group asserts its required route exists with a descriptive
501
+ message. Creates a tight rate limiter (default `max_attempts: 2`,
502
+ `window_ms: 60_000`) per test and disposes it in `finally`.
503
+
504
+ Options: `{session_options, create_route_specs, app_options?, db_factories?, max_attempts?}`.
505
+
506
+ ## Integration suites
507
+
508
+ ### `integration.ts` — `describe_standard_integration_tests`
509
+
510
+ Exercises the full stack against real PGlite + auth middleware + session
511
+ cookies + bearer tokens. The suite has ~19 `describe` blocks grouped under
512
+ these thematic areas:
513
+
514
+ 1. Login/logout lifecycle
515
+ 2. Login response body (strict schema)
516
+ 3. Cookie attributes (HttpOnly, Secure-in-prod, SameSite)
517
+ 4. Session security (tampering, forgery)
518
+ 5. Session revocation (self + revoke-all)
519
+ 6. Password change (revokes all sessions + API tokens)
520
+ 7. Origin verification
521
+ 8. Bearer auth + browser-context discard on mutations
522
+ 9. Token revocation + cross-account isolation
523
+ 10. Response body schema validation + error-response information leakage
524
+ 11. Signup invite edge cases + rate-limiting smoke + expired credential rejection + error-coverage breadth
525
+
526
+ An `ErrorCoverageCollector` runs across groups; `afterAll` filters to
527
+ auth-related routes (login/logout/verify/sessions/tokens/password/
528
+ signup/bootstrap) and asserts `DEFAULT_INTEGRATION_ERROR_COVERAGE`
529
+ (20%). Consumer-specific routes aren't exercised here — they don't
530
+ count against the baseline.
531
+
532
+ Options: `{session_options, create_route_specs, app_options?, db_factories?}`.
533
+
534
+ ### `admin_integration.ts` — `describe_standard_admin_integration_tests`
535
+
536
+ 7 test groups covering admin surface: account listing, permit grant
537
+ lifecycle (via `permit_offer_create` + `permit_revoke` RPC flows —
538
+ **not** REST; see `../auth/CLAUDE.md` for `permit_offer_action_specs.ts` + `permit_offer_actions.ts`), session / token management, audit log reads (RPC),
539
+ admin-to-admin isolation, error coverage, response schema validation.
540
+
541
+ Required options: `{session_options, create_route_specs, roles: RoleSchemaResult, rpc_endpoints: Array<RpcEndpointSpec>, admin_prefix?, app_options?, db_factories?}`.
542
+
543
+ **Hard-fails via `require_rpc_endpoint_path(options.rpc_endpoints)`** at
544
+ setup time when `rpc_endpoints` is empty — admin permit grant/revoke
545
+ plus session/token revoke-all plus audit-log list/history are all
546
+ RPC-only since the 2026-04-22 migration. A confusing test failure
547
+ mid-suite is worse than a clear setup error.
548
+
549
+ Error-coverage scope is narrowed to the REST suffixes still on the
550
+ admin surface (`/sessions`, `/audit-log/stream`); the RPC surface is
551
+ covered by `describe_rpc_round_trip_tests`.
552
+
553
+ ### `audit_completeness.ts` — `describe_audit_completeness_tests`
554
+
555
+ Verifies every auth mutation produces the expected `audit_log` row by
556
+ querying the table after each request. Uses the real middleware stack.
557
+ Same `rpc_endpoints` hard-fail as the admin suite — the mutation-audit
558
+ tests drive permit flow, session/token revoke-all, and invite
559
+ create/delete through `permit_offer_create_action_spec` /
560
+ `permit_revoke_action_spec` / `admin_session_revoke_all_action_spec` /
561
+ `admin_token_revoke_all_action_spec` / `app_settings_update_action_spec` /
562
+ `invite_create_action_spec` / `invite_delete_action_spec`.
563
+
564
+ Bootstrap audit logging is excluded because `create_test_app` doesn't
565
+ provide the filesystem token state; covered separately in
566
+ `bootstrap_account.db.test.ts`.
567
+
568
+ ### `standard.ts` — `describe_standard_tests`
569
+
570
+ Convenience wrapper: always runs `describe_standard_integration_tests`;
571
+ runs `describe_standard_admin_integration_tests` only when `roles` is
572
+ provided. `rpc_endpoints` is a required field on `StandardTestOptions`
573
+ — the admin suite's requirement is enforced at the type level, so a
574
+ missing `rpc_endpoints` is a compile error rather than a runtime throw.
575
+
576
+ ## RPC helpers
577
+
578
+ ### `rpc_helpers.ts` — envelope construction + response assertions
579
+
580
+ Shared by `rpc_attack_surface.ts`, `rpc_round_trip.ts`, the admin and
581
+ audit integration suites, and consumer tests that hit RPC methods
582
+ directly.
583
+
584
+ Request builders:
585
+
586
+ - `create_rpc_post_init(method, params?, id?)` — `RequestInit` with
587
+ JSON-RPC envelope body. `params === undefined || params === null` →
588
+ envelope has no `params` field (JSON-RPC doesn't accept
589
+ `"params": null`).
590
+ - `create_rpc_get_url(endpoint_path, method, params?, id?)` — GET URL
591
+ with `?method=&id=&params=<JSON>`.
592
+
593
+ Response assertions:
594
+
595
+ - `assert_jsonrpc_error_response(body, expected_code?)` — validates
596
+ `JsonrpcErrorResponse`; optional code check.
597
+ - `assert_jsonrpc_success_response(body, output_schema?)` — validates
598
+ `JsonrpcResponse`; optional `output_schema.safeParse(result)`.
599
+
600
+ One-shot transport:
601
+
602
+ - `RpcTestTransport = (url, init) => Promise<Response>` — duck type
603
+ `Hono.request` already satisfies.
604
+ - `http_transport(app)` — adapter for anything with a `request()` method.
605
+ - `RpcCallResult` — discriminated `{ok: true, status, result}` / `{ok: false, status, error: {code, message, data?}}`.
606
+ - `RpcCallArgs` — `{app, path, method, params?, headers?, id?, verb?}`. `verb` defaults to `'POST'`; use `'GET'` for `side_effects: false` methods.
607
+ - `rpc_call(args)` — merges `RPC_CALL_DEFAULT_HEADERS` (`host: 'localhost'`, `origin: 'http://localhost:5173'`, `Content-Type: 'application/json'`) under caller headers. Envelope-shape violations throw; JSON-RPC errors return `{ok: false, error}` so callers assert on `error.code` / `error.data.reason`.
608
+ - `rpc_call_typed<T>(args, output_schema)` — parses the success `result` through the schema; throws on envelope failure, error response, or schema mismatch. Use `rpc_call` when the test needs to assert on error shapes.
609
+ - `rpc_call_for_spec<TSpec>(args)` — spec-bound variant: takes `{..., spec, params}` in place of `{..., method, params}`. `params` is typed from `spec.input` and the success `result` is typed from `spec.output` (runtime-validated, same contract as `rpc_call_typed`). Error branch stays untyped (JSON-RPC `error.data` shapes vary per call site). Use at happy-path + denial-path call sites; fall back to `rpc_call` for adversarial tests that send deliberately-malformed params.
610
+
611
+ Registry lookups:
612
+
613
+ - `find_rpc_action(rpc_endpoints, method)` — endpoint path + `RpcAction` source.
614
+ - `find_rpc_method(rpc_endpoints, method)` — surface-shape lookup over `AppSurfaceRpcEndpoint[]` (generated by `generate_app_surface`).
615
+ - `require_rpc_endpoint_path(rpc_endpoints)` — returns the single endpoint path; throws descriptively on zero or multiple endpoints. Used by the admin/audit suites to hard-fail at setup.
616
+
617
+ ### `rpc_attack_surface.ts` — `describe_rpc_attack_surface_tests`
618
+
619
+ 3 test groups for JSON-RPC endpoints:
620
+
621
+ 1. **RPC auth enforcement** — per-endpoint, per-method:
622
+ - unauthenticated → `unauthenticated` (code -32001)
623
+ - wrong role → `forbidden` (-32002)
624
+ - authenticated without role → `forbidden`
625
+ - **keeper rejects non-daemon credentials** — session and api_token credentials are rejected even when the account has the keeper role (only `daemon_token` passes). Mirrors `require_keeper`'s two-part guard (see `../auth/CLAUDE.md` for `require_keeper.ts`).
626
+ - correct auth passes (not 401/403)
627
+ - GET unauthenticated for `side_effects: false` reads
628
+ 2. **RPC adversarial envelopes** — fixed set exercising dispatcher steps 1–2: non-JSON body, wrong `jsonrpc` version, missing `jsonrpc` / `method` / `id`, batch array, unknown method, GET missing `method`/`id`, GET invalid JSON params, GET non-object params, GET mutation method → `invalid_request`.
629
+ 3. **RPC adversarial params** — reuses `generate_input_test_cases` but filters out structural cases (those hit envelope validation at step 1, not params validation at step 5). Every case expects 400 `invalid_params`.
630
+
631
+ Skips silently when `surface.rpc_endpoints` is empty. Uses stub
632
+ deps — no DB needed.
633
+
634
+ Options: `{build: () => AppSurfaceSpec, roles: Array<string>}`.
635
+
636
+ ## Cross-cutting conventions
637
+
638
+ - **`assert` from vitest, not `expect`.** Project-wide convention
639
+ (mirrored in `src/test/CLAUDE.md`). Use `assert_rejects` from
640
+ `@fuzdev/fuz_util/testing.js` for async rejection assertions.
641
+ - **`.db.test.ts` suffix** for any test file that instantiates a `Db`
642
+ (directly or via `create_test_app`, `create_describe_db`,
643
+ `create_pglite_factory`). The suffix opts the file into the `db`
644
+ vitest project (`isolate: false`, `fileParallelism: false`) so the
645
+ PGlite WASM cache is shared across every DB test file.
646
+ - **`await_pending_effects: true`** is set by `create_test_app`.
647
+ Fire-and-forget effects (audit logs, session touches, WS fan-out via
648
+ `emit_after_commit`) resolve before the response returns, so tests
649
+ can assert on side effects inline without manual flushing.
650
+ - **Avoid `vi.mock()` inside `.db.test.ts`.** With `isolate: false`,
651
+ module-level mocks leak across files. When a mock is unavoidable
652
+ (e.g. `middleware.ts` uses them module-level for bearer auth tests),
653
+ always pair with `vi.restoreAllMocks()` in `afterEach` to contain
654
+ the blast radius.
655
+ - **Deep-path imports only.** `testing/` follows the package
656
+ convention — import from the canonical module (`./db.js`,
657
+ `./rpc_helpers.js`, etc.), never a barrel. fuz_app's `dist/` doesn't
658
+ ship one.
659
+ - **DI via small `*Deps` interfaces.** Stub factories here accept the
660
+ same narrow `*Deps` contracts production code uses — never
661
+ `Pick<GodType, ...>`. New helpers that need env/fs/logger access
662
+ should take `EnvDeps` / `FsReadDeps` / `Logger` from
663
+ `runtime/deps.ts` or `@fuzdev/fuz_util/log.js`.
664
+ - **Keep the shared echo routes in sync with public surface.** When
665
+ middleware or public API gains a new context variable, header, or
666
+ field, update the echo in `middleware.ts`
667
+ (`create_bearer_auth_test_app`, `create_test_middleware_stack_app`)
668
+ alongside the assertions in `src/test/auth/*.test.ts`. The two move
669
+ together — drift between them shows up as a missed assertion, not a
670
+ test failure.