@fuzdev/fuz_app 0.63.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/dist/actions/CLAUDE.md +525 -827
  2. package/dist/actions/broadcast_api.d.ts +1 -1
  3. package/dist/actions/broadcast_api.js +1 -1
  4. package/dist/actions/cancel.d.ts +2 -2
  5. package/dist/actions/cancel.js +3 -3
  6. package/dist/actions/connection_closer.d.ts +65 -0
  7. package/dist/actions/connection_closer.d.ts.map +1 -0
  8. package/dist/actions/connection_closer.js +38 -0
  9. package/dist/actions/register_action_ws.d.ts +2 -2
  10. package/dist/actions/register_action_ws.d.ts.map +1 -1
  11. package/dist/actions/register_action_ws.js +23 -2
  12. package/dist/actions/register_ws_endpoint.d.ts +12 -10
  13. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  14. package/dist/actions/register_ws_endpoint.js +5 -5
  15. package/dist/actions/transports_ws_auth_guard.d.ts +25 -10
  16. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  17. package/dist/actions/transports_ws_auth_guard.js +24 -9
  18. package/dist/actions/ws_endpoint_spec.d.ts +119 -0
  19. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -0
  20. package/dist/actions/ws_endpoint_spec.js +13 -0
  21. package/dist/auth/CLAUDE.md +592 -1808
  22. package/dist/auth/account_action_specs.d.ts +1 -1
  23. package/dist/auth/account_actions.d.ts +13 -0
  24. package/dist/auth/account_actions.d.ts.map +1 -1
  25. package/dist/auth/account_actions.js +31 -1
  26. package/dist/auth/account_routes.d.ts +12 -2
  27. package/dist/auth/account_routes.d.ts.map +1 -1
  28. package/dist/auth/account_routes.js +55 -8
  29. package/dist/auth/account_schema.d.ts +4 -4
  30. package/dist/auth/account_schema.d.ts.map +1 -1
  31. package/dist/auth/admin_action_specs.d.ts +8 -8
  32. package/dist/auth/admin_actions.d.ts +11 -0
  33. package/dist/auth/admin_actions.d.ts.map +1 -1
  34. package/dist/auth/admin_actions.js +25 -0
  35. package/dist/auth/api_token_queries.js +1 -1
  36. package/dist/auth/audit_emitter.d.ts +56 -12
  37. package/dist/auth/audit_emitter.d.ts.map +1 -1
  38. package/dist/auth/audit_emitter.js +38 -12
  39. package/dist/auth/audit_log_ddl.d.ts +1 -1
  40. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  41. package/dist/auth/audit_log_ddl.js +1 -1
  42. package/dist/auth/audit_log_schema.d.ts +5 -3
  43. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  44. package/dist/auth/audit_log_schema.js +5 -3
  45. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  46. package/dist/auth/bootstrap_account.js +1 -5
  47. package/dist/auth/bootstrap_routes.d.ts +8 -2
  48. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  49. package/dist/auth/bootstrap_routes.js +15 -11
  50. package/dist/auth/invite_schema.d.ts +2 -2
  51. package/dist/auth/keyring.d.ts +6 -6
  52. package/dist/auth/keyring.js +8 -8
  53. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  54. package/dist/auth/role_grant_offer_actions.js +4 -2
  55. package/dist/auth/signup_routes.d.ts +1 -1
  56. package/dist/auth/standard_rpc_actions.d.ts +1 -0
  57. package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
  58. package/dist/auth/standard_rpc_actions.js +1 -0
  59. package/dist/db/create_db.d.ts.map +1 -1
  60. package/dist/db/create_db.js +13 -0
  61. package/dist/dev/setup.d.ts +2 -2
  62. package/dist/dev/setup.js +3 -3
  63. package/dist/http/CLAUDE.md +225 -483
  64. package/dist/http/error_schemas.d.ts +0 -4
  65. package/dist/http/error_schemas.d.ts.map +1 -1
  66. package/dist/http/error_schemas.js +0 -4
  67. package/dist/http/ip_canonical.d.ts +100 -0
  68. package/dist/http/ip_canonical.d.ts.map +1 -0
  69. package/dist/http/ip_canonical.js +195 -0
  70. package/dist/http/origin.d.ts +14 -6
  71. package/dist/http/origin.d.ts.map +1 -1
  72. package/dist/http/origin.js +14 -32
  73. package/dist/http/pending_effects.d.ts +1 -1
  74. package/dist/http/pending_effects.js +1 -1
  75. package/dist/http/proxy.d.ts +13 -5
  76. package/dist/http/proxy.d.ts.map +1 -1
  77. package/dist/http/proxy.js +15 -23
  78. package/dist/http/surface.d.ts +50 -0
  79. package/dist/http/surface.d.ts.map +1 -1
  80. package/dist/http/surface.js +27 -1
  81. package/dist/primitive_schemas.d.ts +20 -4
  82. package/dist/primitive_schemas.d.ts.map +1 -1
  83. package/dist/primitive_schemas.js +25 -4
  84. package/dist/realtime/sse_auth_guard.d.ts +16 -4
  85. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  86. package/dist/realtime/sse_auth_guard.js +15 -3
  87. package/dist/runtime/mock.js +1 -1
  88. package/dist/server/app_backend.d.ts +66 -19
  89. package/dist/server/app_backend.d.ts.map +1 -1
  90. package/dist/server/app_backend.js +57 -34
  91. package/dist/server/app_server.d.ts +101 -10
  92. package/dist/server/app_server.d.ts.map +1 -1
  93. package/dist/server/app_server.js +105 -6
  94. package/dist/server/env.d.ts +7 -7
  95. package/dist/server/env.d.ts.map +1 -1
  96. package/dist/server/env.js +14 -14
  97. package/dist/server/startup.d.ts.map +1 -1
  98. package/dist/server/startup.js +12 -0
  99. package/dist/server/static.d.ts +4 -4
  100. package/dist/server/static.js +7 -7
  101. package/dist/testing/CLAUDE.md +269 -59
  102. package/dist/testing/admin_integration.d.ts +18 -23
  103. package/dist/testing/admin_integration.d.ts.map +1 -1
  104. package/dist/testing/admin_integration.js +159 -202
  105. package/dist/testing/adversarial_headers.d.ts +6 -0
  106. package/dist/testing/adversarial_headers.d.ts.map +1 -1
  107. package/dist/testing/adversarial_headers.js +13 -5
  108. package/dist/testing/app_server.d.ts +148 -60
  109. package/dist/testing/app_server.d.ts.map +1 -1
  110. package/dist/testing/app_server.js +143 -54
  111. package/dist/testing/attack_surface.d.ts +8 -7
  112. package/dist/testing/attack_surface.d.ts.map +1 -1
  113. package/dist/testing/attack_surface.js +12 -8
  114. package/dist/testing/audit_completeness.d.ts +23 -22
  115. package/dist/testing/audit_completeness.d.ts.map +1 -1
  116. package/dist/testing/audit_completeness.js +199 -158
  117. package/dist/testing/audit_drift_guard.d.ts +116 -0
  118. package/dist/testing/audit_drift_guard.d.ts.map +1 -0
  119. package/dist/testing/audit_drift_guard.js +134 -0
  120. package/dist/testing/bootstrap_success.d.ts +28 -0
  121. package/dist/testing/bootstrap_success.d.ts.map +1 -0
  122. package/dist/testing/bootstrap_success.js +144 -0
  123. package/dist/testing/connection_closer_helpers.d.ts +44 -0
  124. package/dist/testing/connection_closer_helpers.d.ts.map +1 -0
  125. package/dist/testing/connection_closer_helpers.js +48 -0
  126. package/dist/testing/cross_backend/capabilities.d.ts +64 -0
  127. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
  128. package/dist/testing/cross_backend/capabilities.js +47 -0
  129. package/dist/testing/cross_backend/setup.d.ts +215 -0
  130. package/dist/testing/cross_backend/setup.d.ts.map +1 -0
  131. package/dist/testing/cross_backend/setup.js +101 -0
  132. package/dist/testing/data_exposure.d.ts +14 -15
  133. package/dist/testing/data_exposure.d.ts.map +1 -1
  134. package/dist/testing/data_exposure.js +127 -146
  135. package/dist/testing/db_entities.d.ts +11 -1
  136. package/dist/testing/db_entities.d.ts.map +1 -1
  137. package/dist/testing/db_entities.js +13 -1
  138. package/dist/testing/integration.d.ts +35 -21
  139. package/dist/testing/integration.d.ts.map +1 -1
  140. package/dist/testing/integration.js +231 -293
  141. package/dist/testing/integration_helpers.d.ts +16 -6
  142. package/dist/testing/integration_helpers.d.ts.map +1 -1
  143. package/dist/testing/integration_helpers.js +7 -7
  144. package/dist/testing/mock_fs.d.ts.map +1 -1
  145. package/dist/testing/mock_fs.js +0 -2
  146. package/dist/testing/rate_limiting.d.ts.map +1 -1
  147. package/dist/testing/rate_limiting.js +13 -4
  148. package/dist/testing/role_grant_helpers.d.ts +31 -0
  149. package/dist/testing/role_grant_helpers.d.ts.map +1 -0
  150. package/dist/testing/role_grant_helpers.js +46 -0
  151. package/dist/testing/round_trip.d.ts +21 -16
  152. package/dist/testing/round_trip.d.ts.map +1 -1
  153. package/dist/testing/round_trip.js +65 -86
  154. package/dist/testing/rpc_helpers.d.ts +2 -1
  155. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  156. package/dist/testing/rpc_round_trip.d.ts +24 -21
  157. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  158. package/dist/testing/rpc_round_trip.js +91 -106
  159. package/dist/testing/schema_introspect.d.ts +106 -0
  160. package/dist/testing/schema_introspect.d.ts.map +1 -0
  161. package/dist/testing/schema_introspect.js +123 -0
  162. package/dist/testing/schema_parity.d.ts +144 -0
  163. package/dist/testing/schema_parity.d.ts.map +1 -0
  164. package/dist/testing/schema_parity.js +233 -0
  165. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  166. package/dist/testing/sse_round_trip.js +12 -6
  167. package/dist/testing/standard.d.ts +57 -25
  168. package/dist/testing/standard.d.ts.map +1 -1
  169. package/dist/testing/standard.js +62 -5
  170. package/dist/testing/stubs.d.ts +22 -3
  171. package/dist/testing/stubs.d.ts.map +1 -1
  172. package/dist/testing/stubs.js +28 -21
  173. package/dist/testing/surface_invariants.d.ts +66 -1
  174. package/dist/testing/surface_invariants.d.ts.map +1 -1
  175. package/dist/testing/surface_invariants.js +103 -1
  176. package/dist/testing/transports/surface_source.d.ts +51 -0
  177. package/dist/testing/transports/surface_source.d.ts.map +1 -0
  178. package/dist/testing/transports/surface_source.js +19 -0
  179. package/dist/ui/SurfaceExplorer.svelte +161 -2
  180. package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
  181. package/package.json +4 -4
@@ -22,21 +22,21 @@ Enforced by grep, not a linter; make this the first line in new modules.
22
22
 
23
23
  ### `stubs.ts` — `AppDeps` + `AppServerContext` stubs
24
24
 
25
- | Helper | Role |
26
- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
27
- | `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 `{}`. |
28
- | `create_noop_stub<T>(label, overrides?)` | Proxy whose every method returns `async () => undefined`; `overrides` lets callers pin specific props. |
29
- | `stub` | Pre-built throwing stub labelled `'stub'`. |
30
- | `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. |
31
- | `stub_handler()` | Returns a fresh `Response('stub')`. |
32
- | `stub_mw` | Pass-through middleware handler (`async (_c, next) => next()`). |
33
- | `stub_app_deps` | Frozen `AppDeps` — every capability is a throwing stub, `audit` is a no-op `AuditEmitter` from `create_test_audit_emitter`. |
34
- | `create_stub_app_deps()` | Factory returning fresh `AppDeps` with no-op FS/keyring/password, a `create_noop_stub` DB, silent `Logger`, no-op `audit`. |
35
- | `create_test_audit_emitter()` | No-op `AuditEmitter` for tests that don't assert on audit fan-out. `emit` / `emit_role_grant_target` are no-ops; `emit_pool` resolves immediately; `notify` is a no-op; `on_event_chain` is empty. |
36
- | `create_stub_audit_sse()` | No-op `AuditLogSse` for surface-test wiring without booting real SSE. `subscribe` returns a no-op cleanup; `on_audit_event` is a no-op; the `registry` is a fresh `SubscriberRegistry` (live `.size` / `.close_*` for tests touching registry state, isolated per call). For real SSE plumbing, build via `create_audit_log_sse` against `create_test_app`. |
37
- | `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. |
38
- | `create_stub_app_server_context(session_options)` | Stub `AppServerContext` — rate limiters null, `bootstrap_status.available: false`, `app_settings.open_signup: false`. |
39
- | `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. |
25
+ | Helper | Role |
26
+ | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
27
+ | `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 `{}`. |
28
+ | `create_noop_stub<T>(label, overrides?)` | Proxy whose every method returns `async () => undefined`; `overrides` lets callers pin specific props. |
29
+ | `stub` | Pre-built throwing stub labelled `'stub'`. |
30
+ | `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. |
31
+ | `stub_handler()` | Returns a fresh `Response('stub')`. |
32
+ | `stub_mw` | Pass-through middleware handler (`async (_c, next) => next()`). |
33
+ | `stub_app_deps` | Frozen `AppDeps` — every capability is a throwing stub, `audit` is a no-op `AuditEmitter` from `create_test_audit_emitter`. |
34
+ | `create_stub_app_deps()` | Factory returning fresh `AppDeps` with no-op FS/keyring/password, a `create_noop_stub` DB, silent `Logger`, no-op `audit`. |
35
+ | `create_test_audit_emitter()` | No-op `AuditEmitter` for tests that don't assert on audit fan-out. `emit` / `emit_role_grant_target` are no-ops; `emit_pool` resolves immediately; `notify` is a no-op; `on_event_chain` is empty. |
36
+ | `create_stub_audit_sse()` | No-op `AuditLogSse` for surface-test wiring without booting real SSE. `subscribe` returns a no-op cleanup; `on_audit_event` is a no-op; the `registry` is a fresh `SubscriberRegistry` (live `.size` / `.close_*` for tests touching registry state, isolated per call). For real SSE plumbing, build via `create_audit_log_sse` against `create_test_app`. |
37
+ | `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. |
38
+ | `create_stub_app_server_context(session_options)` | Stub `AppServerContext` — rate limiters null, `bootstrap_status.available: false`, `app_settings.open_signup: false`. |
39
+ | `create_test_app_surface_spec(options)` | Builds an `AppSurfaceSpec` that mirrors `create_app_server`'s route assembly: consumer routes + stub middleware + surface generation. `CreateTestAppSurfaceSpecOptions` accepts `session_options`, `create_route_specs`, `env_schema?`, `event_specs?`, `rpc_endpoints?`, `ws_endpoints?`, `transform_middleware?`, `bootstrap?`. Bootstrap is opt-in (symmetric with `create_app_server` — omit to skip; pass the same value you'd pass in production to mount the routes at `bootstrap.route_prefix ?? '/api/account'`). Single source of truth for attack-surface tests — track `create_app_server` wiring changes here. |
40
40
 
41
41
  Throwing stubs surface mock escape: a test that accidentally reaches into
42
42
  stub territory breaks immediately with a label-scoped error rather than
@@ -79,9 +79,50 @@ Returns `{account, actor}`. Replaces the per-file `create_user` /
79
79
  `create_test_actor` / `create_test_account` helpers that had accumulated
80
80
  across the auth test suite. Use for query-level tests that need real
81
81
  DB rows but not a full session/token bundle. For tests that also need
82
- an API token + session cookie + role_grants, use `bootstrap_test_account`
82
+ an API token + session cookie + role_grants, use `bootstrap_test_keeper`
83
83
  from `app_server.ts` instead.
84
84
 
85
+ `create_test_role_grant_direct(db, input)` wraps `query_create_role_grant`
86
+ for tests that need an active role_grant seeded directly, bypassing the
87
+ production offer/accept consent flow. Use only when the test focuses on
88
+ revoke or isolation semantics rather than the consent path itself — the
89
+ schema permits null `source_offer_id` for exactly this case. For tests
90
+ that exercise the production grant flow, drive
91
+ `role_grant_offer_and_accept` from `role_grant_helpers.ts` instead.
92
+
93
+ ### `role_grant_helpers.ts` — RPC-flow role_grant helpers
94
+
95
+ `role_grant_offer_and_accept({app, rpc_path, grantor, recipient, role})`
96
+ drives the full consent flow (grantor `role_grant_offer_create` →
97
+ recipient `role_grant_offer_accept`) over the production RPC surface and
98
+ returns `{offer_id, role_grant_id}`. Sibling to
99
+ `create_test_role_grant_direct` in `db_entities.ts` — that one bypasses
100
+ the consent flow; this one exercises it end-to-end so the suite picks up
101
+ post-commit fan-out (audit, SSE broadcasts, `_supersede` notifications)
102
+ that a direct DB seed would miss. `grantor` and `recipient` accept
103
+ `TestApp | TestAccount` / `TestAccount` so the call site passes the same
104
+ object that already owns the headers + account id, ruling out caller-side
105
+ mismatch.
106
+
107
+ ### `audit_drift_guard.ts` — audit-emission validation
108
+
109
+ | Helper | Role |
110
+ | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
111
+ | `install_audit_drift_guard()` | `beforeEach` resets + `afterEach` zero-checks the `audit_metadata_validation_failures` + `audit_unknown_event_type_failures` counters from `auth/audit_log_queries.ts`. Call once at the top of any `describe_db` block that fires audit emits — production validation is fail-open, so without this any regression that ships a typo'd `event_type` or an undeclared metadata field is silent. Pair with `await_pending_effects: true` (the `create_test_app` default) so fire-and-forget audit writes have completed by response time. |
112
+ | `create_emit_ordering_audit_factory<E>(seq_ref, events_ref, build_inner)` | Returns an `AuditFactory` that wraps the result of `build_inner({db, log})` so every `emit` call pushes `{kind: 'emit', at: seq.value++}` into a shared sequence + events array. Pass through `create_test_app({audit_factory: …})` — the test backend invokes it with its constructed `{db, log}` and lands the wrapped emitter on `deps.audit`. Generic `E extends {kind: string; at: number}` so the events array typechecks against the caller's own `close` / custom marker shape. Pair with `create_recording_closer(seq_ref)` (in `connection_closer_helpers.ts`) for close-vs-emit ordering tests. Scope is `emit` only — `emit_role_grant_target`, `emit_pool`, `notify` forward to the inner emitter unwrapped (same caveat as the previous `patch_audit_emit_capture`). |
113
+ | `AuditEmitMarker` | `{kind: 'emit'; at: number}` — the type of marker `create_emit_ordering_audit_factory` pushes. |
114
+ | `create_recording_audit_emitter(calls_ref?)` | Build a no-op `AuditEmitter` that pushes every `emit` and `emit_pool` call into `calls`. Pass `calls_ref` to write into a caller-owned array; omit to let the helper allocate one. Returns `{emitter, calls}` — destructure `emitter` as the `audit` dep and read `calls` to assert on captured metadata. Replaces per-file capturing emitters previously duplicated across `password_change.test.ts`, `audit_log.test.ts`, etc. |
115
+ | `RecordingAuditEmitter` | `{emitter: AuditEmitter; calls: Array<AuditLogInput>}` — return shape of `create_recording_audit_emitter`. |
116
+
117
+ ### `connection_closer_helpers.ts` — `ConnectionCloser` test doubles
118
+
119
+ | Helper | Role |
120
+ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
121
+ | `create_recording_closer(seq_ref?)` | Returns `{closer, calls}` where every method on `closer` records `{method, id, at}` into `calls`. Pass `seq_ref` to share the sequence counter with `create_emit_ordering_audit_factory` so close + emit markers compose for ordering tests. |
122
+ | `assert_close_call(call, method, id)` | Pins `{method, id}` on a single recorded close call without baking in the `at: N` sequence number. Use at every "did the closer fire?" assertion site; reserve `at: N` assertions for the dedicated ordering test paired with the capture helper. |
123
+ | `RecordedClose` | `{method: 'session' \| 'token' \| 'account', id, at}` — recorded shape pushed by the closer. |
124
+ | `RecordingCloser` | `{closer, calls}` — return shape of `create_recording_closer`. |
125
+
85
126
  ## Database — `db.ts`
86
127
 
87
128
  Factory builders for parameterized DB tests. Consumer projects pass their
@@ -135,18 +176,35 @@ Key module-scope values:
135
176
  `create_test_app_server` uses when no `db` is passed. Reuses the WASM
136
177
  cache via `create_pglite_factory`.
137
178
 
138
- `bootstrap_test_account(options)` is extracted because both
139
- `create_test_app_server` and `TestApp.create_account` reuse the same
140
- "insert account + actor + roles + API token + session + cookie" flow.
141
- Takes `{db, keyring, session_options, password, username?, password_value?, roles?}`.
142
-
143
- | Type | Shape |
144
- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
145
- | `TestAppServer extends AppBackend` | Adds `account`, `actor`, `api_token`, `session_cookie`, `keyring`, `cleanup()`. |
146
- | `TestAppServerOptions` | `session_options` (required), optional `db`, `db_type`, `password`, `username`, `password_value`, `roles`, `on_audit_event`, `audit_log_config`. |
147
- | `CreateTestAppOptions extends TestAppServerOptions` | Adds `create_route_specs` (required) + `app_options` (narrow `Partial<AppServerOptions>` excluding the three the helper manages). |
148
- | `TestAccount` | `{account, actor, session_cookie, api_token, create_session_headers, create_bearer_headers}`. |
149
- | `TestApp` | `{app, backend, surface_spec, surface, route_specs, create_session_headers, create_bearer_headers, create_daemon_token_headers, create_account, cleanup}`. |
179
+ Two helpers share the "insert account + actor + roles + API token +
180
+ session + cookie" flow, split by intent:
181
+
182
+ - `bootstrap_test_keeper(options)` keeper path used by
183
+ `create_test_app_server`. Same body as the general helper plus a
184
+ lock flip (`UPDATE bootstrap_lock SET bootstrapped = true ...`) so
185
+ test DB state matches a real bootstrap completion, letting
186
+ production code trust the lock as the single signal.
187
+ - `create_test_account_with_credentials(options)` general path used
188
+ by `TestApp.create_account` for additional non-keeper accounts. Same
189
+ body, no lock interaction (additional accounts aren't bootstraps).
190
+
191
+ Both take `{db, keyring, session_options, password, username?, password_value?, roles?}`
192
+ (the shared `CreateTestAccountWithCredentialsOptions` / `BootstrapTestKeeperOptions`).
193
+
194
+ For exercising the bootstrap success path end-to-end against an empty
195
+ DB (no pre-keeper, lock unflipped), use `create_test_app_for_bootstrap`
196
+ instead — pair with `describe_bootstrap_success_tests` for the
197
+ consumer-runnable suite.
198
+
199
+ | Type | Shape |
200
+ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
201
+ | `TestAppServer extends AppBackend` | Adds `account`, `actor`, `api_token`, `session_cookie`, `keyring`, `cleanup()`. |
202
+ | `TestAppServerOptions` | `session_options` (required), optional `db`, `db_type`, `password`, `username`, `password_value`, `roles`, `audit_factory`. The optional `audit_factory` defaults to `default_audit_factory` (no-listener `create_audit_emitter` over the test backend's `{db, log}`); pass a custom factory to compose `on_audit_event` / `audit_log_config`, wrap with `emit_decorator` (via `create_emit_ordering_audit_factory`), or otherwise replace the emitter. Mirrors `CreateAppBackendOptions` end-to-end — the previous `on_audit_event` / `audit_log_config` sugar was removed alongside the production rename. |
203
+ | `CreateTestAppOptions extends TestAppServerOptions` | Adds `create_route_specs` (required), `rpc_endpoints?: RpcEndpointsSuiteOption` (top-level only — single source of truth, symmetric with the suite-level option), `bootstrap?: BootstrapServerOptions` (top-level only — same precedent as `rpc_endpoints`), and `app_options?: SuiteAppOptions` (`Partial<AppServerOptions>` excluding the five fields the helper manages: `backend`, `session_options`, `create_route_specs`, `rpc_endpoints`, `bootstrap`). |
204
+ | `TestAccount` | `{account, actor, session_cookie, api_token, create_session_headers, create_bearer_headers}`. |
205
+ | `TestApp` | `{app, backend, surface_spec, surface, route_specs, create_session_headers, create_bearer_headers, create_daemon_token_headers, create_account, cleanup}`. |
206
+ | `CreateTestAppForBootstrapOptions` | `{session_options, create_route_specs, rpc_endpoints?, bootstrap: BootstrapLiveOptions, bootstrap_token, app_options?, db?, db_type?, password?, audit_factory?}`. `bootstrap` is required + narrowed to `live` mode (the helper exists for the success-path test). |
207
+ | `TestAppForBootstrap` | `{app, backend, surface_spec, surface, route_specs, create_request_headers, cleanup}`. No keeper credentials (test drives bootstrap itself). |
150
208
 
151
209
  `create_test_app` hard-codes the test-friendly `AppServerOptions`:
152
210
  `allowed_origins: [/^http:\/\/localhost/]`, stub proxy pinned to
@@ -181,6 +239,41 @@ hatch is test-only by construction.
181
239
  | `select_auth_app(apps, auth)` | Map `RouteAuth` → matching Hono app. Throws for missing `role:*` entries. |
182
240
  | `resolve_test_path(path)` | `:foo` → `test_foo` — adequate for routes without format-constrained params. |
183
241
 
242
+ ## Cross-impl schema parity
243
+
244
+ ### `schema_introspect.ts` — `query_schema_snapshot`
245
+
246
+ | Helper | Role |
247
+ | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
248
+ | `query_schema_snapshot(db, options?)` | Introspect a live DB into a deterministic `SchemaSnapshot` via `pg_catalog` + `information_schema`. Captures tables, columns (with `udt_name` to distinguish int4/int8), indexes (`indexdef`), constraints (`pg_get_constraintdef`), sequences, and `schema_version` rows. |
249
+ | `SchemaSnapshot` | Fully JSON-serializable shape — every collection is deterministically sorted on capture so structural equality is stable across runs. `applied_at` is excluded from `schema_version` rows so timestamps don't drift the snapshot. |
250
+
251
+ ### `schema_parity.ts` — `assert_schema_snapshots_equal`
252
+
253
+ | Helper | Role |
254
+ | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
255
+ | `diff_schema_snapshots(a, b)` | Structured `Array<SchemaDiff>` between two snapshots — empty array means parity holds. |
256
+ | `format_schema_diffs(diffs, labels?)` | Human-readable multi-line rendering; labels name the impl on each side (e.g., `{a: 'deno', b: 'rust'}`). |
257
+ | `assert_schema_snapshots_equal(a, b, labels?)` | Throw on drift with a fully-formatted diff message. |
258
+ | `SchemaDiff` | Tagged-union for each drift kind: `schema_version_only_in`, `schema_version_sequence_differs`, `table_only_in`, `column_only_in`, `column_field_differs`, `index_only_in`, `index_definition_differs`, `constraint_only_in`, `constraint_differs`, `sequence_only_in`, `sequence_data_type_differs`. |
259
+
260
+ **Cross-impl gate pattern** — consumers running two backends against a
261
+ shared schema (zzz's `--backend=both`, fuz_app's cross-backend suite)
262
+ bootstrap each impl against an isolated DB, snapshot, then compare:
263
+
264
+ ```ts
265
+ await drop_recreate_db('zzz_test');
266
+ await spawn_backend(deno_config);
267
+ const snapshot_deno = await query_schema_snapshot(db);
268
+ await drop_recreate_db('zzz_test');
269
+ await spawn_backend(rust_config);
270
+ const snapshot_rust = await query_schema_snapshot(db);
271
+ assert_schema_snapshots_equal(snapshot_deno, snapshot_rust, {a: 'deno', b: 'rust'});
272
+ ```
273
+
274
+ Each impl's _own_ tests still gate its DDL correctness independently —
275
+ this pair is purely the cross-impl drift check.
276
+
184
277
  ## Assertions, coverage, helpers
185
278
 
186
279
  ### `assertions.ts` — surface + error-schema assertions
@@ -213,6 +306,21 @@ Structural invariants (options-free, apply universally):
213
306
  | `assert_error_code_status_consistency` | The same `z.literal()` error code never appears at two different HTTP statuses. |
214
307
  | `assert_404_schemas_use_specific_errors` | Routes with params declaring 404 must use `z.literal()` or `z.enum()`, not generic `z.string()`. |
215
308
 
309
+ RPC / WS structural invariants (options-free, apply universally over
310
+ `surface.rpc_endpoints` + `surface.ws_endpoints`):
311
+
312
+ | Assertion | Checks |
313
+ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
314
+ | `assert_rpc_method_descriptions_present` | Every RPC method on every endpoint has a non-empty `description`. |
315
+ | `assert_ws_method_descriptions_present` | Every WS method on every endpoint has a non-empty `description`. |
316
+ | `assert_ws_endpoints_include_protocol_actions` | Every WS endpoint includes `heartbeat` + `cancel` (the `protocol_actions` spread from `actions/protocol.js`). |
317
+ | `assert_ws_notifications_have_null_auth` | WS method `kind === 'remote_notification' ⟺ auth === null` — guards against drift between spec union and surface emitter. |
318
+
319
+ Per-endpoint duplicate method names and the auth-shape biconditional are
320
+ already enforced at startup by `compile_action_registry` (see
321
+ `actions/CLAUDE.md` §Registry compile) — these assertions only cover
322
+ contract-surface concerns a runtime registration check cannot reach.
323
+
216
324
  Policy invariants (configurable, sensible defaults):
217
325
 
218
326
  | Assertion | Checks |
@@ -248,7 +356,8 @@ Tightness audit:
248
356
 
249
357
  Aggregate runners (called by the standard attack-surface suite):
250
358
 
251
- - `assert_surface_invariants(surface)` — runs all structural assertions.
359
+ - `assert_surface_invariants(surface)` — runs all route-level structural assertions.
360
+ - `assert_rpc_ws_surface_invariants(surface)` — runs all RPC/WS structural assertions.
252
361
  - `assert_surface_security_policy(surface, options?)` — runs all policy assertions.
253
362
 
254
363
  ### `error_coverage.ts` — reachability tracking
@@ -331,7 +440,7 @@ Single-call bundle of 5 top-level groups (10 named tests + every
331
440
  adversarial case per route):
332
441
 
333
442
  1. **attack surface snapshot** — `matches committed snapshot`, `is deterministic`.
334
- 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`).
443
+ 2. **attack surface structure** — `only expected public routes`, `full middleware stack on API routes`, `surface invariants`, `rpc/ws 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`).
335
444
  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`.
336
445
  4. **adversarial input validation** — delegated to `describe_adversarial_input`.
337
446
  5. **adversarial 404 response validation** — delegated to `describe_adversarial_404`.
@@ -384,8 +493,8 @@ body matches the declared 404 Zod schema. No DB needed.
384
493
  3. no auth headers → passes through
385
494
  4. bearer + empty Origin → 403 `ERROR_FORBIDDEN_ORIGIN` (defense-in-depth)
386
495
  5. lowercase `bearer` scheme → RFC 7235 §2.1 soft-fail
387
- 6. bearer + rogue Referer → 403 `ERROR_FORBIDDEN_REFERER`
388
- 7. bearer + allowed Referer → bearer silently discarded
496
+ 6. bearer + rogue Referer (no Origin) passes origin check (Origin-only posture), bearer silently discarded (Referer is still a browser-context indicator for bearer auth)
497
+ 7. bearer + allowed Referer (no Origin) → bearer silently discarded (browser context)
389
498
 
390
499
  Each case declares `validate_expectation: 'called' | 'not_called'` so the
391
500
  suite asserts that short-circuit middleware actually fires before token
@@ -428,7 +537,7 @@ validates the response against declared schemas. DB-backed via
428
537
  `create_test_app`. Per-route test (`test.each`) — one line per route
429
538
  in the vitest output.
430
539
 
431
- Options: `{session_options, create_route_specs, app_options?, db_factories?, skip_routes?, input_overrides?}`.
540
+ Options: `{setup_test, surface_source, capabilities, skip_routes?, input_overrides?}`.
432
541
  `input_overrides` is a `Map<"METHOD /path", body>` — override generated
433
542
  bodies for routes whose input schema can't round-trip cleanly (e.g.
434
543
  fields that must reference DB state).
@@ -441,7 +550,7 @@ picks them up separately.
441
550
  DB-backed round-trip for RPC: one POST test for all methods, one GET
442
551
  test for `side_effects: false` methods. Successful responses validate
443
552
  against `action.spec.output`; error responses validate as well-formed
444
- JSON-RPC error envelopes. Required: `{session_options, create_route_specs, rpc_endpoints, ...}`.
553
+ JSON-RPC error envelopes. Options: `{setup_test, surface_source, capabilities, session_options, rpc_endpoints, skip_methods?, input_overrides?}`.
445
554
  The admin RPC auth test picks a session-based identity (`authed` /
446
555
  `admin` / bootstrapped keeper) based on `method.auth`; keeper uses the
447
556
  daemon token.
@@ -518,7 +627,7 @@ Support functions: `collect_json_schema_property_names(schema)` (walks
518
627
  `assert_output_schemas_no_sensitive_fields(surface, fields?)`,
519
628
  `assert_non_admin_schemas_no_admin_fields(surface, fields?)`.
520
629
 
521
- Options: `{build, session_options, create_route_specs, sensitive_fields?, admin_only_fields?, app_options?, db_factories?, skip_routes?}`.
630
+ Options: `{setup_test, surface_source, capabilities, sensitive_fields?, admin_only_fields?, skip_routes?}`.
522
631
 
523
632
  ### `rate_limiting.ts` — `describe_rate_limiting_tests`
524
633
 
@@ -532,7 +641,7 @@ Each group asserts its required route exists with a descriptive
532
641
  message. Creates a tight rate limiter (default `max_attempts: 2`,
533
642
  `window_ms: 60_000`) per test and disposes it in `finally`.
534
643
 
535
- Options: `{session_options, create_route_specs, app_options?, db_factories?, max_attempts?}`.
644
+ Options: `{session_options, create_route_specs, rpc_endpoints, app_options?, db_factories?, max_attempts?}`. Reads inputs directly from the options bag instead of going through the `setup_test` fixture protocol — the per-test rate-limiter overrides need a fresh `TestApp` per test that the single-fixture model can't carry. Consumers still pass `default_in_process_suite_options(...)` for shape uniformity; the extra `{setup_test, surface_source, capabilities}` fields on the spread are ignored by the suite.
536
645
 
537
646
  ## Integration suites
538
647
 
@@ -552,31 +661,47 @@ these thematic areas:
552
661
  8. Bearer auth + browser-context discard on mutations
553
662
  9. Token revocation + cross-account isolation
554
663
  10. Response body schema validation + error-response information leakage
555
- 11. Signup invite edge cases + rate-limiting smoke + expired credential rejection + error-coverage breadth
664
+ 11. Signup invite edge cases + expired credential rejection + error-coverage breadth
556
665
 
557
666
  An `ErrorCoverageCollector` runs across groups; `afterAll` filters to
558
667
  auth-related routes (login/logout/verify/sessions/tokens/password/
559
- signup/bootstrap) and asserts `DEFAULT_INTEGRATION_ERROR_COVERAGE`
560
- (20%). Consumer-specific routes aren't exercised herethey don't
561
- count against the baseline.
668
+ signup) and asserts `DEFAULT_INTEGRATION_ERROR_COVERAGE` (20%). Bootstrap
669
+ is excluded because no describe block in this suite drives it its
670
+ declared codes would always be uncovered. Consumer-specific routes
671
+ aren't exercised here either — they don't count against the baseline.
672
+ Override the threshold with the `error_coverage_min?: number` option
673
+ (set to `0` to skip the assertion entirely — useful for minimal route
674
+ sets whose declared error codes outpace the suite's denial-path drivers).
562
675
 
563
- Options: `{session_options, create_route_specs, app_options?, db_factories?}`.
676
+ Options: `{setup_test, surface_source, capabilities, session_options, rpc_endpoints, error_coverage_min?}`.
564
677
 
565
678
  ### `admin_integration.ts` — `describe_standard_admin_integration_tests`
566
679
 
567
680
  7 test groups covering admin surface: account listing, role_grant grant
568
- lifecycle (via `role_grant_offer_create` + `role_grant_revoke` RPC flows —
569
- **not** REST; see `auth/CLAUDE.md` for `role_grant_offer_action_specs.ts` + `role_grant_offer_actions.ts`), session / token management, audit log reads (RPC),
570
- admin-to-admin isolation, error coverage, response schema validation.
571
-
572
- Required options: `{session_options, create_route_specs, roles: RoleSchemaResult, rpc_endpoints: RpcEndpointsSuiteOption, admin_prefix?, app_options?, db_factories?}`.
681
+ lifecycle (via `role_grant_offer_create` + `role_grant_offer_accept` +
682
+ `role_grant_revoke` RPC flows **not** REST, **not** direct
683
+ `query_accept_offer`; see `auth/CLAUDE.md` for
684
+ `role_grant_offer_action_specs.ts` + `role_grant_offer_actions.ts`),
685
+ session / token management, audit log reads (RPC), admin-to-admin
686
+ isolation, error coverage, response schema validation.
687
+
688
+ The shared `role_grant_offer_and_accept` helper (`role_grant_helpers.ts`)
689
+ composes both RPCs end-to-end and takes
690
+ `{grantor: TestApp | TestAccount, recipient: TestAccount}` — closing
691
+ the headers/account loop on a single object per party rules out caller-side
692
+ header/account mismatch. Direct-grant fixtures (where the test focuses on
693
+ revoke or isolation, not the consent path) go through
694
+ `create_test_role_grant_direct` from `db_entities.ts`.
695
+
696
+ Required options: `{setup_test, surface_source, capabilities, session_options, rpc_endpoints: RpcEndpointsSuiteOption, roles: RoleSchemaResult, admin_prefix?}`.
573
697
 
574
698
  `rpc_endpoints` is `Array<RpcEndpointSpec> | ((ctx: AppServerContext) => Array<RpcEndpointSpec>)` —
575
699
  the same `RpcEndpointsSuiteOption` union every DB-backed suite accepts
576
700
  (`integration`, `admin_integration`, `audit_completeness`, `rate_limiting`,
577
701
  `rpc_round_trip`, `sse_round_trip`). Prefer the factory form: it forwards
578
- raw to `app_options.rpc_endpoints` so `create_app_server` resolves it per-test
579
- with the real ctx — the only way action handlers can close over
702
+ raw to the top-level `rpc_endpoints` slot on `CreateTestAppOptions` so
703
+ `create_app_server` resolves it per-test with the real ctx — the only way
704
+ action handlers can close over
580
705
  `ctx.deps` / `ctx.app_settings` (e.g. `create_standard_rpc_actions(ctx.deps,
581
706
  {app_settings: ctx.app_settings})`). Factory must return the same endpoint
582
707
  `path` regardless of ctx — `resolve_rpc_endpoints_for_setup` invokes it
@@ -608,28 +733,70 @@ branch.
608
733
 
609
734
  ### `audit_completeness.ts` — `describe_audit_completeness_tests`
610
735
 
611
- Verifies every auth mutation produces the expected `audit_log` row by
612
- querying the table after each request. Uses the real middleware stack.
736
+ Verifies every auth mutation produces the expected `audit_log` row.
737
+ Mutations fire over the real middleware stack; reads go back through the
738
+ `audit_log_list` RPC (the same path the admin UI consumes) — intentional
739
+ end-to-end coverage of emit → persist → query → wire response. For
740
+ unit-level "did the handler emit?" assertions without the persistence
741
+ path, use `create_recording_audit_emitter` from `audit_drift_guard.ts`.
742
+
613
743
  Same `rpc_endpoints` hard-fail as the admin suite — the mutation-audit
614
744
  tests drive role_grant flow, session/token revoke-all, and invite
615
745
  create/delete through `role_grant_offer_create_action_spec` /
616
- `role_grant_revoke_action_spec` / `admin_session_revoke_all_action_spec` /
746
+ `role_grant_offer_accept_action_spec` / `role_grant_revoke_action_spec` /
747
+ `admin_session_revoke_all_action_spec` /
617
748
  `admin_token_revoke_all_action_spec` / `app_settings_update_action_spec` /
618
749
  `invite_create_action_spec` / `invite_delete_action_spec`.
619
750
 
751
+ **Observer-account pattern.** Each audit-touching test mints a dedicated
752
+ admin account (`create_admin_observer`) whose sole job is reading the
753
+ audit log via RPC. Decoupling the observer from the subject keeps the
754
+ helper shape uniform across every test — even mutations that revoke the
755
+ bootstrapped admin's credentials (logout, session_revoke, password_change).
756
+
620
757
  Bootstrap audit logging is excluded because `create_test_app` doesn't
621
758
  provide the filesystem token state; covered separately in
622
759
  `bootstrap_account.db.test.ts`.
623
760
 
624
761
  ### `standard.ts` — `describe_standard_tests`
625
762
 
626
- Convenience wrapper: always runs `describe_standard_integration_tests`;
627
- runs `describe_standard_admin_integration_tests` only when `roles` is
628
- provided. `rpc_endpoints: RpcEndpointsSuiteOption` is a required field on
629
- `StandardTestOptions` the admin suite's requirement is enforced at the
630
- type level, so a missing `rpc_endpoints` is a compile error rather than a
631
- runtime throw. Round-trips the union through unchanged so consumers can
632
- pass either an eager array or the factory form.
763
+ Bundles every DB-backed suite carrying the standard option shape, each
764
+ gated on its relevant config — silent-skip when the gate isn't met:
765
+
766
+ | Suite | Gate |
767
+ | -------------------- | --------------------------------------------------------------------------------------------------------- |
768
+ | `integration` | always |
769
+ | `admin` | `roles` provided |
770
+ | `audit_completeness` | `roles` provided (proxy for consumer admin wiring; `rpc_endpoints` is bundle-required) |
771
+ | `bootstrap_success` | `bootstrap.mode === 'live'` |
772
+ | `round_trip` | always |
773
+ | `rpc_round_trip` | `rpc_endpoints` provided |
774
+ | `data_exposure` | always |
775
+ | `rate_limiting` | always (owns its own per-test setup, bypasses the fixture protocol — needs `create_route_specs` directly) |
776
+
777
+ Realization that lifted the bundle from 2 suites to 8: fold-in cost
778
+ between suites is zero because each `describe_*` block owns its own
779
+ setup via the `{setup_test, surface_source, capabilities}` protocol, so
780
+ suites whose tests need opposite-shaped default DB state (e.g. the
781
+ bootstrap-success suite needs an empty DB while the integration suite
782
+ needs the pre-bootstrapped keeper) coexist in one bundle without cost.
783
+ Each test invokes the right per-test fixture. Consumers wiring the
784
+ standard surface call once instead of seven times; forgetting a suite
785
+ no longer silently loses coverage.
786
+
787
+ `StandardTestOptions` requires `create_route_specs` (for rate_limiting)
788
+ and `rpc_endpoints` (for admin/audit_completeness/rpc_round_trip); the
789
+ admin suite's requirement is enforced at the type level so a missing
790
+ `rpc_endpoints` is a compile error rather than a runtime throw. Optional
791
+ `bootstrap` (top-level, same precedent as `rpc_endpoints`) feeds both
792
+ the disabled/surface_only/live wire-shape gating and the
793
+ bootstrap-success suite gate.
794
+
795
+ Attack surface suites stay separate — their option shape is
796
+ `{build, snapshot_path, expected_public_routes, ...}` rather than the
797
+ shared `{setup_test, surface_source, capabilities}`. A peer
798
+ `describe_standard_surface_tests` bundler lives for that side if/when
799
+ needed.
633
800
 
634
801
  ## RPC helpers
635
802
 
@@ -682,7 +849,7 @@ Registry lookups:
682
849
  - unauthenticated → `unauthenticated` (code -32001)
683
850
  - wrong role → `forbidden` (-32002)
684
851
  - authenticated without role → `forbidden`
685
- - **keeper rejects non-daemon credentials** — session and api_token credentials are rejected even when the account has the keeper role (only `daemon_token` passes). The credential-type gate fires before the role gate (see `auth/CLAUDE.md` §`request_context.ts` for `require_credential_types`).
852
+ - **keeper rejects non-daemon credentials** — session and api_token credentials are rejected even when the account has the keeper role (only `daemon_token` passes). The credential-type gate fires before the role gate (see `auth/CLAUDE.md` §Keeper auth shape).
686
853
  - correct auth passes (not 401/403)
687
854
  - GET unauthenticated for `side_effects: false` reads
688
855
  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`.
@@ -728,3 +895,46 @@ fuz_app-specific points:
728
895
  (`create_bearer_auth_test_app`, `create_test_middleware_stack_app`)
729
896
  alongside the assertions in `src/test/auth/*.test.ts`. Drift surfaces
730
897
  as a missed assertion, not a test failure.
898
+
899
+ ## Cross-backend integration layer
900
+
901
+ The standard test suites take a unified
902
+ `{setup_test, surface_source, capabilities}` shape so the same suite
903
+ bodies run against an in-process Hono harness today and against a
904
+ spawned non-TS backend (Rust `zzz_server`, `fuz_webui`) over real HTTP
905
+ once the cross-process transport lands. In-process is the fast feedback
906
+ path; cross-process is the source of truth for wire-shape conformance.
907
+
908
+ The shape:
909
+
910
+ - `testing/cross_backend/setup.ts` — `SetupTest` / `TestFixture` /
911
+ `TestAccountFixture` / `CreateTestAccountOptions` types,
912
+ `default_in_process_setup(options)` (wraps `create_test_app`), and
913
+ `default_in_process_suite_options(options)` (emits the full Tier 1
914
+ suite options bag: the `{setup_test, surface_source, capabilities}`
915
+ triple plus `session_options` / `create_route_specs` / `rpc_endpoints`
916
+ pass-through; call sites pass the output directly or spread it
917
+ alongside suite-specific extras like `roles`, `skip_routes`,
918
+ `input_overrides`, `db_factories`).
919
+ - `testing/cross_backend/capabilities.ts` — `BackendCapabilities`
920
+ vocabulary (`bearer_auth` / `trusted_proxy` / `login_rate_limit` /
921
+ `ws` / `sse` / `in_process_only`), `test_if(cond, name, fn)` for
922
+ capability-gated cases, and `in_process_capabilities` preset.
923
+ - `testing/transports/surface_source.ts` — `SurfaceSource` union
924
+ (`inline` for source-of-truth route closures; `snapshot` for
925
+ committed JSON read cross-process).
926
+
927
+ Three suites stay in-process by design — `ws_round_trip` (no HTTP
928
+ transport at all), `sse_round_trip` (streaming + in-process audit
929
+ hook for close-on-revoke), `audit_completeness` (FK-structural
930
+ introspection beyond the `audit_log_list` RPC reads). Cross-process
931
+ variants land alongside the spawned-backend work for the first two;
932
+ audit_completeness's introspection is structurally in-process.
933
+
934
+ The auth-cost handling for cross-process testing is consumer-side:
935
+ each consumer ships a separate test binary wiring a fast-params
936
+ `TestingArgon2idHasher` from a sibling Rust testing crate. Cross-process
937
+ `bootstrap` + `create_account` are then plain RPC calls against the
938
+ test binary — no DB-direct surgery in fuz_app's testing library, no
939
+ runtime knobs in production code, no shared cookie key with the
940
+ backend.
@@ -1,33 +1,35 @@
1
1
  import './assert_dev_env.js';
2
2
  import type { SessionOptions } from '../auth/session_cookie.js';
3
- import type { AppServerContext } from '../server/app_server.js';
4
- import type { RouteSpec } from '../http/route_spec.js';
5
3
  import { type RoleSchemaResult } from '../auth/role_schema.js';
6
- import { type SuiteAppOptions } from './app_server.js';
7
- import { type DbFactory } from './db.js';
8
4
  import { type RpcEndpointsSuiteOption } from './rpc_helpers.js';
5
+ import type { BackendCapabilities } from './cross_backend/capabilities.js';
6
+ import type { SetupTest } from './cross_backend/setup.js';
7
+ import type { SurfaceSource } from './transports/surface_source.js';
9
8
  /**
10
9
  * Configuration for `describe_standard_admin_integration_tests`.
11
10
  */
12
11
  export interface StandardAdminIntegrationTestOptions {
13
- /** Session config for cookie-based auth. */
12
+ /**
13
+ * Per-test fixture-producing function. The admin suite calls this in
14
+ * every `test()` body — auth_integration_truncate_tables clears
15
+ * `account`, so each test re-bootstraps the keeper.
16
+ */
17
+ setup_test: SetupTest;
18
+ /**
19
+ * Source of the app surface for route iteration and error-coverage
20
+ * scoping. Currently requires `kind: 'inline'` — the cross-process
21
+ * snapshot variant lands alongside the spawned-backend transport plumbing.
22
+ */
23
+ surface_source: SurfaceSource;
24
+ /** Backend capability declarations. */
25
+ capabilities: BackendCapabilities;
26
+ /** Session config — needed for cookie_name + factory-form rpc_endpoints resolution. */
14
27
  session_options: SessionOptions<string>;
15
- /** Route spec factory — same one used in production. */
16
- create_route_specs: (ctx: AppServerContext) => Array<RouteSpec>;
17
28
  /** Role schema result from `create_role_schema()` — used to determine valid/invalid/web-grantable roles. */
18
29
  roles: RoleSchemaResult;
19
30
  /**
20
31
  * RPC endpoint specs — the source `RpcAction` arrays. Required; role_grant
21
32
  * grant/revoke are RPC-only and the suite hard-fails without them.
22
- *
23
- * Accepts either an array (eager) or a factory
24
- * `(ctx: AppServerContext) => Array<RpcEndpointSpec>` — the factory form
25
- * is required when action handlers must close over the per-test
26
- * `ctx.app_settings` / `ctx.deps` (e.g. the canonical
27
- * `create_standard_rpc_actions(ctx.deps, {app_settings: ctx.app_settings})`
28
- * pattern). The factory must return the same endpoint `path` regardless
29
- * of ctx — it is invoked once at setup with a stub ctx for path lookup
30
- * and again per-test by `create_app_server` for live dispatch.
31
33
  */
32
34
  rpc_endpoints: RpcEndpointsSuiteOption;
33
35
  /**
@@ -37,13 +39,6 @@ export interface StandardAdminIntegrationTestOptions {
37
39
  * stub deps. Default `'/api/admin'`.
38
40
  */
39
41
  admin_prefix?: string;
40
- /** Optional overrides for `AppServerOptions`. */
41
- app_options?: SuiteAppOptions;
42
- /**
43
- * Database factories to run tests against. Default: pglite only.
44
- * Pass consumer factories (e.g. `[pglite_factory, pg_factory]`) to also test against PostgreSQL.
45
- */
46
- db_factories?: Array<DbFactory>;
47
42
  }
48
43
  /**
49
44
  * Standard admin integration test suite for fuz_app admin routes.
@@ -1 +1 @@
1
- {"version":3,"file":"admin_integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/admin_integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAgC7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAC9D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAA0B,KAAK,gBAAgB,EAAC,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAA6C,KAAK,eAAe,EAAC,MAAM,iBAAiB,CAAC;AACjG,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AASjB,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAoB1B;;GAEG;AACH,MAAM,WAAW,mCAAmC;IACnD,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,4GAA4G;IAC5G,KAAK,EAAE,gBAAgB,CAAC;IACxB;;;;;;;;;;;;OAYG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAChC;AAmCD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,mCAAmC,KAC1C,IA+1BF,CAAC"}
1
+ {"version":3,"file":"admin_integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/admin_integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAgC7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAA0B,KAAK,gBAAgB,EAAC,MAAM,wBAAwB,CAAC;AAWtF,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAiB1B,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,iCAAiC,CAAC;AACzE,OAAO,KAAK,EAAC,SAAS,EAAc,MAAM,0BAA0B,CAAC;AACrE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,gCAAgC,CAAC;AAElE;;GAEG;AACH,MAAM,WAAW,mCAAmC;IACnD;;;;OAIG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,cAAc,EAAE,aAAa,CAAC;IAC9B,uCAAuC;IACvC,YAAY,EAAE,mBAAmB,CAAC;IAClC,uFAAuF;IACvF,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,4GAA4G;IAC5G,KAAK,EAAE,gBAAgB,CAAC;IACxB;;;OAGG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAiBD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,mCAAmC,KAC1C,IAqzBF,CAAC"}