@fuzdev/fuz_app 0.111.0 → 0.113.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/CLAUDE.md +15 -11
- package/dist/actions/action_rpc.d.ts +1 -1
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +4 -6
- package/dist/actions/peer_ping.d.ts +8 -0
- package/dist/actions/peer_ping.d.ts.map +1 -1
- package/dist/actions/peer_ping.js +8 -0
- package/dist/actions/peer_request.d.ts +1 -1
- package/dist/actions/peer_request.js +1 -1
- package/dist/actions/perform_action.d.ts +16 -9
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +30 -21
- package/dist/actions/register_action_ws.d.ts +1 -1
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +88 -21
- package/dist/auth/CLAUDE.md +43 -8
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +10 -1
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +36 -16
- package/dist/auth/account_schema.d.ts +20 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +22 -1
- package/dist/auth/actor_search_actions.d.ts +3 -3
- package/dist/auth/actor_search_actions.d.ts.map +1 -1
- package/dist/auth/actor_search_actions.js +10 -6
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +11 -5
- package/dist/auth/app_settings_queries.d.ts.map +1 -1
- package/dist/auth/app_settings_queries.js +6 -4
- package/dist/auth/audit_log_queries.d.ts +6 -0
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +11 -5
- package/dist/auth/audit_log_route_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_route_schema.js +12 -1
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +3 -8
- package/dist/auth/cell_audit_actions.d.ts.map +1 -1
- package/dist/auth/cell_audit_actions.js +1 -1
- package/dist/auth/cell_field_actions.js +1 -1
- package/dist/auth/cell_grant_actions.js +1 -1
- package/dist/auth/cell_item_actions.js +1 -1
- package/dist/auth/invite_queries.d.ts.map +1 -1
- package/dist/auth/invite_queries.js +10 -8
- package/dist/auth/role_grant_offer_action_specs.d.ts +2 -1
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +2 -1
- package/dist/auth/role_grant_offer_ddl.d.ts +11 -0
- package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_ddl.js +20 -2
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +29 -15
- package/dist/auth/role_grant_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.js +21 -7
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +9 -4
- package/dist/db/CLAUDE.md +21 -2
- package/dist/db/cell_audit_queries.js +2 -2
- package/dist/db/cell_field_queries.d.ts +1 -1
- package/dist/db/cell_field_queries.d.ts.map +1 -1
- package/dist/db/cell_field_queries.js +8 -6
- package/dist/db/cell_grant_queries.d.ts +1 -1
- package/dist/db/cell_grant_queries.d.ts.map +1 -1
- package/dist/db/cell_grant_queries.js +12 -10
- package/dist/db/cell_item_queries.d.ts +1 -1
- package/dist/db/cell_item_queries.d.ts.map +1 -1
- package/dist/db/cell_item_queries.js +9 -7
- package/dist/db/cell_queries.d.ts +3 -3
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +6 -4
- package/dist/db/fact_disk_storage.js +1 -1
- package/dist/db/fact_queries.d.ts.map +1 -1
- package/dist/db/fact_queries.js +9 -3
- package/dist/db/sql_columns.d.ts +80 -3
- package/dist/db/sql_columns.d.ts.map +1 -1
- package/dist/db/sql_columns.js +91 -4
- package/dist/http/CLAUDE.md +6 -4
- package/dist/http/auth_shape.d.ts +19 -6
- package/dist/http/auth_shape.d.ts.map +1 -1
- package/dist/http/auth_shape.js +19 -6
- package/dist/http/jsonrpc_helpers.d.ts +13 -0
- package/dist/http/jsonrpc_helpers.d.ts.map +1 -1
- package/dist/http/jsonrpc_helpers.js +13 -0
- package/dist/http/surface_query.d.ts +29 -4
- package/dist/http/surface_query.d.ts.map +1 -1
- package/dist/http/surface_query.js +30 -5
- package/dist/realtime/sse_auth_guard.d.ts +9 -3
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +9 -3
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +7 -2
- package/dist/testing/CLAUDE.md +54 -28
- package/dist/testing/admin_integration.d.ts +0 -3
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +0 -1
- package/dist/testing/adversarial_404.d.ts.map +1 -1
- package/dist/testing/adversarial_404.js +4 -1
- package/dist/testing/adversarial_input.d.ts.map +1 -1
- package/dist/testing/adversarial_input.js +5 -4
- package/dist/testing/app_server.d.ts +4 -3
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +11 -5
- package/dist/testing/attack_surface.d.ts +21 -2
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +78 -27
- package/dist/testing/audit_completeness.d.ts +0 -3
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +0 -1
- package/dist/testing/auth_apps.d.ts +34 -2
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +43 -7
- package/dist/testing/cross_backend/account_lifecycle.d.ts +5 -4
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +7 -0
- package/dist/testing/cross_backend/action_manifest.d.ts +19 -3
- package/dist/testing/cross_backend/action_manifest.d.ts.map +1 -1
- package/dist/testing/cross_backend/action_manifest.js +10 -4
- package/dist/testing/cross_backend/action_manifest_parity.d.ts +9 -3
- package/dist/testing/cross_backend/action_manifest_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/action_manifest_parity.js +6 -0
- package/dist/testing/cross_backend/actor_lookup.d.ts +3 -2
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.d.ts +4 -2
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +1 -1
- package/dist/testing/cross_backend/cell_crud.d.ts +2 -2
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +10 -0
- package/dist/testing/cross_backend/cell_gated_create.d.ts +3 -3
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.d.ts +2 -2
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.d.ts +2 -2
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/fact_serving.d.ts +2 -2
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/make_cross_backend_project.d.ts +17 -2
- package/dist/testing/cross_backend/make_cross_backend_project.d.ts.map +1 -1
- package/dist/testing/cross_backend/make_cross_backend_project.js +23 -3
- package/dist/testing/cross_backend/origin.d.ts +4 -2
- package/dist/testing/cross_backend/origin.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin.js +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.js +11 -7
- package/dist/testing/cross_backend/query_shape.d.ts +6 -5
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
- package/dist/testing/cross_backend/ready.d.ts +2 -2
- package/dist/testing/cross_backend/ready.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_enumeration.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_enumeration.js +7 -1
- package/dist/testing/cross_backend/setup.d.ts +26 -6
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/standard.d.ts +5 -1
- package/dist/testing/cross_backend/standard.d.ts.map +1 -1
- package/dist/testing/cross_backend/standard.js +1 -5
- package/dist/testing/cross_backend/testing_backdoor.js +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +5 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +3 -2
- package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_lifetime.js +6 -0
- package/dist/testing/cross_backend/token_scope_surface.d.ts +2 -2
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +36 -7
- package/dist/testing/cross_backend/wire_shapes.d.ts +22 -0
- package/dist/testing/cross_backend/wire_shapes.d.ts.map +1 -0
- package/dist/testing/cross_backend/wire_shapes.js +41 -0
- package/dist/testing/data_exposure.d.ts +0 -3
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +0 -1
- package/dist/testing/db.d.ts +44 -7
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +62 -27
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +15 -2
- package/dist/testing/round_trip.d.ts +0 -3
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +0 -4
- package/dist/testing/rpc_attack_surface.js +2 -2
- package/dist/testing/rpc_round_trip.d.ts +0 -3
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +0 -1
- package/dist/testing/schema_generators.d.ts +15 -0
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +45 -11
- package/dist/testing/standard.d.ts +5 -1
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +2 -7
- package/dist/testing/surface_invariants.d.ts +7 -1
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +11 -5
- package/dist/timestamp.d.ts +58 -0
- package/dist/timestamp.d.ts.map +1 -0
- package/dist/timestamp.js +57 -0
- package/dist/ui/AdminAccounts.svelte +6 -4
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminInvites.svelte +3 -3
- package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +10 -10
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/AdminRoleGrantHistory.svelte +7 -7
- package/dist/ui/AdminRoleGrantHistory.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSettings.svelte +1 -1
- package/dist/ui/RoleGrantOfferHistory.svelte +3 -1
- package/dist/ui/RoleGrantOfferHistory.svelte.d.ts.map +1 -1
- package/dist/ui/RoleGrantOfferInbox.svelte +4 -1
- package/dist/ui/RoleGrantOfferInbox.svelte.d.ts.map +1 -1
- package/dist/ui/SurfaceExplorer.svelte +33 -17
- package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
- package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -1
- package/dist/ui/role_grant_offers_state.svelte.js +6 -2
- package/package.json +10 -10
|
@@ -150,11 +150,16 @@ const cell_fact_params_schema = z.strictObject({
|
|
|
150
150
|
cell_id: Uuid,
|
|
151
151
|
hash: FactHashSchema
|
|
152
152
|
});
|
|
153
|
-
/**
|
|
153
|
+
/**
|
|
154
|
+
* Shared error-schema entry: tighten the auto-derived 400 to the literal
|
|
155
|
+
* emitted by params validation. `issues` is optional because the framework
|
|
156
|
+
* emits it through `dev_only` — a production body carries the `error`
|
|
157
|
+
* literal alone.
|
|
158
|
+
*/
|
|
154
159
|
const params_400_error = {
|
|
155
160
|
400: z.looseObject({
|
|
156
161
|
error: z.literal(ERROR_INVALID_ROUTE_PARAMS),
|
|
157
|
-
issues: z.array(z.unknown())
|
|
162
|
+
issues: z.array(z.unknown()).optional()
|
|
158
163
|
})
|
|
159
164
|
};
|
|
160
165
|
/**
|
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -121,7 +121,8 @@ factories accept any migration namespace set.
|
|
|
121
121
|
- `IS_CI` — `process.env.CI === 'true'`.
|
|
122
122
|
- `DbFactory` — `{name, create, close, skip, skip_reason?}`.
|
|
123
123
|
- `reset_pglite(db)` — `DROP SCHEMA public CASCADE` + recreate. Reuses a live PGlite instance.
|
|
124
|
-
- `create_pglite_factory(init_schema)` — in-memory; no external deps; `skip: false`. See WASM caching below.
|
|
124
|
+
- `create_pglite_factory(init_schema, options?)` — in-memory; no external deps; `skip: false`. `options.substitutable: false` pins the call site to PGlite when a substitute is installed. See WASM caching below.
|
|
125
|
+
- `set_substitute_db_factory(create_factory | null)` — install a `DbFactoryBuilder` that stands in for `create_pglite_factory`, so a suite written against PGlite runs on another driver without being edited. Injected rather than selected here: the drivers a substitute wraps live in test-side code this shipped module can't import. fuz_app's own switch is `src/test/db_substitute.ts`.
|
|
125
126
|
- `create_pg_factory(init_schema, test_url?)` — PostgreSQL; `skip: true` when `test_url` missing. Drops `schema_version` before `init_schema` so migrations re-evaluate against actual tables (prevents stale tracker rows from skipping migrations when DDL changes between sessions). Pool reused + cleaned up across `create()` calls.
|
|
126
127
|
- `auth_truncate_tables` — `['invite', 'api_token', 'auth_session', 'role_grant', 'role_grant_offer', 'actor', 'account']` in FK-safe order. Excludes `audit_log` — unit DB tests don't need to truncate it.
|
|
127
128
|
- `auth_integration_truncate_tables` — `auth_truncate_tables + ['audit_log']` for integration suites that exercise the audit path.
|
|
@@ -131,14 +132,13 @@ factories accept any migration namespace set.
|
|
|
131
132
|
- `assert_columns_match_live(db, table, columns)` — the named-projection drift guard: asserts a `*_COLUMNS` const (`ACCOUNT_COLUMNS`, `CELL_COLUMNS`, a consumer's own) names exactly the live `public`-schema columns of `table`, via `query_public_columns(db, {table})`. Per module, or iterated from a table → const registry (fuz_app's own: `src/test/db/column_projections.db.test.ts`).
|
|
132
133
|
|
|
133
134
|
**PGlite WASM caching.** `create_pglite_factory` shares a single PGlite
|
|
134
|
-
instance in a module-level ref (`module_db`) across
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
cost again. Each vitest file runs in its own worker, so no cross-file
|
|
138
|
-
contamination — but inside a file, suites share state until the schema is
|
|
135
|
+
instance in a module-level ref (`module_db`) across the PGlite factories it builds.
|
|
136
|
+
Subsequent `create()` calls `DROP SCHEMA public CASCADE` instead of paying the
|
|
137
|
+
~500–700ms WASM cold-start cost again, so suites share state until the next
|
|
139
138
|
reset. The `db` vitest project (opted into by the `.db.test.ts` suffix) runs
|
|
140
139
|
with `isolate: false` + `fileParallelism: false` to amortize WASM boot across
|
|
141
|
-
every DB test file
|
|
140
|
+
every DB test file — which also makes that one instance the whole run's; a
|
|
141
|
+
project that isolates its files gets one per worker instead.
|
|
142
142
|
|
|
143
143
|
## Test app assembly
|
|
144
144
|
|
|
@@ -207,7 +207,7 @@ test-only by construction.
|
|
|
207
207
|
- `AuthTestApps` — `{public, authed, keeper, by_role: Map<string, Hono>}`.
|
|
208
208
|
- `create_auth_test_apps(specs, roles)` — builds one app per auth level. Keeper app uses `credential_type: 'daemon_token'` so `require_credential_types(['daemon_token'])` passes.
|
|
209
209
|
- `select_auth_app(apps, auth)` — map `RouteAuth` → matching Hono app. Throws for missing `role:*` entries.
|
|
210
|
-
- `
|
|
210
|
+
- `create_route_skip_filter(skip_routes)` — the `'METHOD /path'` skip predicate the three adversarial sub-suites apply from their `skip_routes` option.
|
|
211
211
|
|
|
212
212
|
## Cross-impl schema parity
|
|
213
213
|
|
|
@@ -237,8 +237,10 @@ per-method auth shape.
|
|
|
237
237
|
- `cross_backend/action_manifest.ts` — `ActionManifest` / `ActionManifestEntry`
|
|
238
238
|
Zod schema (the `_testing_action_manifest` wire validator) + `build_action_manifest(specs)`,
|
|
239
239
|
which normalizes each spec to `{method, side_effects, account, actor, roles,
|
|
240
|
-
credential_types}` (auth axes pass through; `roles` /
|
|
241
|
-
to sorted arrays
|
|
240
|
+
credential_types, rate_limit}` (auth axes pass through; `roles` /
|
|
241
|
+
`credential_types` flatten to sorted arrays; `rate_limit` is the declared
|
|
242
|
+
class or an explicit `null`, so a missing key reads as drift rather than a
|
|
243
|
+
default). Twin of `schema_introspect.ts`. Excludes the protocol actions
|
|
242
244
|
(`heartbeat` / `cancel` / `peer/ping`) — `create_testing_action_manifest_action`
|
|
243
245
|
filters them via the `protocol_action_specs` method set (Rust drops
|
|
244
246
|
`PROTOCOL_ACTION_SPECS`); the two impls organize them differently (`peer/ping`
|
|
@@ -247,6 +249,8 @@ credential_types}` (auth axes pass through; `roles` / `credential_types` flatten
|
|
|
247
249
|
- `cross_backend/action_manifest_parity.ts` — `diff_action_manifests` /
|
|
248
250
|
`format_action_manifest_diffs` / `assert_action_manifests_equal` (twin of
|
|
249
251
|
`schema_parity.ts`) — asserts exact method-set + per-method auth-shape parity.
|
|
252
|
+
`ActionManifestDiff` kinds: `method_only_in`, `side_effects_differ`,
|
|
253
|
+
`rate_limit_differs`, `auth_field_differs`, `auth_list_differs`.
|
|
250
254
|
- `cross_backend/setup.ts` `capture_action_manifest(handle)` — dumps a backend's
|
|
251
255
|
manifest over the daemon-token `_testing_action_manifest` channel (twin of
|
|
252
256
|
`capture_schema_snapshot`).
|
|
@@ -255,9 +259,9 @@ It runs under the same dual-spawn `cross_backend_parity` project (one TS-spine +
|
|
|
255
259
|
Rust-stub spawn serves both gates). It complements the in-repo
|
|
256
260
|
`spine_method_coverage` reconciliation gate (which proves _mounted ⟹ covered_);
|
|
257
261
|
this proves _TS-mount-set ≡ Rust-mount-set_. The gate asserts **exact** parity —
|
|
258
|
-
every mounted method and its full auth shape (side-effects
|
|
259
|
-
must match across the two impls, with no allowlisted
|
|
260
|
-
`action_manifest_parity.cross.test.ts`).
|
|
262
|
+
every mounted method and its full auth shape (side-effects, the four auth axes,
|
|
263
|
+
the rate-limit class) must match across the two impls, with no allowlisted
|
|
264
|
+
divergences (see `action_manifest_parity.cross.test.ts`).
|
|
261
265
|
|
|
262
266
|
**Cross-impl gate pattern** — a dual-impl consumer running two backends
|
|
263
267
|
(a TS Hono server and a Rust spine server) against a shared schema, plus
|
|
@@ -367,7 +371,7 @@ Walks Zod schemas to generate valid values for adversarial/round-trip tests.
|
|
|
367
371
|
|
|
368
372
|
- `detect_format(field_schema)` — reads `format` / `pattern` from the JSON Schema representation.
|
|
369
373
|
- `generate_valid_value(field, field_schema)` — base-type switch producing a valid sample (UUIDs → nil UUID, strings → `'xxxxxxxxxx'`, numbers → `1`, objects → recurse, enums → first entry, etc.). For branded-string refinements, walks a fallback chain synthesized from the `pattern` string the JSON Schema representation exposes: fixed-length hex (`^[0-9a-f]{N}$` — blake3 / sha256 / md5 digests; `0`.repeat(N)), prefix-lengthed slug (`^<prefix>_[A-Za-z0-9_-]{N}$` — `ApiTokenId`-style ids; `<prefix>_` + `x`.repeat(N)), absolute path prefix, URL prefix. First candidate that `safeParse` accepts is used.
|
|
370
|
-
- `resolve_valid_path(path, params_schema?)` — swaps `:param` for
|
|
374
|
+
- `resolve_valid_path(path, params_schema?)` — swaps `:param` for a value the params schema accepts. Prefers `test_<name>` wherever the field schema admits it (the param name keeps failure messages readable); otherwise a UUID param takes the nil UUID and anything else is synthesized from the schema, percent-encoded. A synthesized value containing `/` is refused (it would route elsewhere) and falls back to `test_<name>`.
|
|
371
375
|
- `generate_valid_body(input_schema) => Record<string, unknown> | undefined` — builds a body satisfying the input schema. Throws with Zod `issues` if the generated body fails validation — surfaces broken generation logic with a descriptive error rather than a confusing 400 downstream.
|
|
372
376
|
|
|
373
377
|
### `integration_helpers.ts` — route lookup + body checks
|
|
@@ -394,11 +398,20 @@ case per route):
|
|
|
394
398
|
|
|
395
399
|
1. **attack surface snapshot** — `matches committed snapshot`, `is deterministic`.
|
|
396
400
|
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`).
|
|
397
|
-
3. **adversarial HTTP auth enforcement** — `unauthenticated → 401`, `wrong role → 403` × roles, `authenticated without role → 403`, `
|
|
401
|
+
3. **adversarial HTTP auth enforcement** — `unauthenticated → 401`, `wrong role → 403` × roles, `authenticated without role → 403`, `credential-gated routes reject a disallowed credential → 403`, `correct auth passes guard`. The role blocks cover role routes the session-credentialed role apps reach (a keeper route's channel gate answers first); the credential block probes every `auth.credential_types` route on the first builtin channel its allowlist omits, using `AuthTestApps.by_credential_type` (one keeper-role app per builtin channel; `keeper` is its `daemon_token` entry).
|
|
398
402
|
4. **adversarial input validation** — delegated to `describe_adversarial_input`.
|
|
399
403
|
5. **adversarial 404 response validation** — delegated to `describe_adversarial_404`.
|
|
400
404
|
|
|
401
|
-
Options: `{build: () => AppSurfaceSpec, snapshot_path, expected_public_routes, expected_api_middleware, roles, api_path_prefix?, security_policy?, error_schema_tightness?}`.
|
|
405
|
+
Options: `{build: () => AppSurfaceSpec, snapshot_path, expected_public_routes, expected_api_middleware, roles, api_path_prefix?, security_policy?, error_schema_tightness?, skip_routes?}`.
|
|
406
|
+
|
|
407
|
+
`skip_routes` is a list of `'METHOD /path'` surface keys the three
|
|
408
|
+
adversarial sub-suites drop, forwarded to each of them (it also sits on
|
|
409
|
+
`AdversarialTestOptions` for the standalone `describe_adversarial_auth` /
|
|
410
|
+
`describe_adversarial_input` / `describe_adversarial_404` calls). It is the
|
|
411
|
+
escape hatch for a route that cannot be driven generically at all — a
|
|
412
|
+
handler needing real seeded state, a path segment no schema describes. A
|
|
413
|
+
route whose params merely carry a format needs no entry: `resolve_valid_path`
|
|
414
|
+
synthesizes a segment the route accepts.
|
|
402
415
|
|
|
403
416
|
Also exported: `describe_adversarial_auth(options)` (group 3 on its own) and
|
|
404
417
|
`build_error_schema_lookup(specs, middleware_specs?)` (pre-built
|
|
@@ -480,7 +493,7 @@ For every route spec, fires a valid request with matching auth and validates
|
|
|
480
493
|
the response against declared schemas. DB-backed via `create_test_app`.
|
|
481
494
|
Per-route test (`test.each`) — one line per route in the vitest output.
|
|
482
495
|
|
|
483
|
-
Options: `{setup_test, surface_source,
|
|
496
|
+
Options: `{setup_test, surface_source, skip_routes?, input_overrides?, success_fixtures?}`.
|
|
484
497
|
`input_overrides` is a `Map<"METHOD /path", body>` — override generated
|
|
485
498
|
bodies for routes whose input schema can't round-trip cleanly (e.g. fields
|
|
486
499
|
that must reference DB state).
|
|
@@ -493,7 +506,7 @@ picks them up separately.
|
|
|
493
506
|
DB-backed round-trip for RPC: one POST test for all methods, one GET test
|
|
494
507
|
for `side_effects: false` methods. Successful responses validate against
|
|
495
508
|
`action.spec.output`; error responses validate as well-formed JSON-RPC error
|
|
496
|
-
envelopes. Options: `{setup_test, surface_source,
|
|
509
|
+
envelopes. Options: `{setup_test, surface_source, session_options, rpc_endpoints, skip_methods?, input_overrides?, success_fixtures?}`.
|
|
497
510
|
The admin RPC auth test picks a session-based identity (`authed` / `admin` /
|
|
498
511
|
bootstrapped keeper) based on `method.auth`; keeper uses the daemon token.
|
|
499
512
|
|
|
@@ -568,7 +581,7 @@ Support functions: `collect_json_schema_property_names(schema)` (walks
|
|
|
568
581
|
`assert_output_schemas_no_sensitive_fields(surface, fields?)`,
|
|
569
582
|
`assert_non_admin_schemas_no_admin_fields(surface, fields?)`.
|
|
570
583
|
|
|
571
|
-
Options: `{setup_test, surface_source,
|
|
584
|
+
Options: `{setup_test, surface_source, sensitive_fields?, admin_only_fields?, skip_routes?}`.
|
|
572
585
|
|
|
573
586
|
### `rate_limiting.ts` — `describe_rate_limiting_tests`
|
|
574
587
|
|
|
@@ -626,7 +639,7 @@ Override with
|
|
|
626
639
|
minimal route sets whose declared error codes outpace the suite's
|
|
627
640
|
denial-path drivers).
|
|
628
641
|
|
|
629
|
-
Options: `{setup_test, surface_source,
|
|
642
|
+
Options: `{setup_test, surface_source, session_options, rpc_endpoints, error_coverage_min?}`.
|
|
630
643
|
|
|
631
644
|
### `admin_integration.ts` — `describe_standard_admin_integration_tests`
|
|
632
645
|
|
|
@@ -646,7 +659,7 @@ header/account mismatch. Direct-grant fixtures (test focuses on revoke or
|
|
|
646
659
|
isolation, not the consent path) go through `create_test_role_grant_direct`
|
|
647
660
|
from `db_entities.ts`.
|
|
648
661
|
|
|
649
|
-
Required options: `{setup_test, surface_source,
|
|
662
|
+
Required options: `{setup_test, surface_source, session_options, rpc_endpoints: RpcEndpointsSuiteOption, roles: RoleSchemaResult, admin_prefix?}`.
|
|
650
663
|
|
|
651
664
|
`rpc_endpoints` is `Array<RpcEndpointSpec> | ((ctx: AppServerContext) => Array<RpcEndpointSpec>)` —
|
|
652
665
|
the same `RpcEndpointsSuiteOption` union every DB-backed suite accepts
|
|
@@ -1179,10 +1192,23 @@ never sees them). Both parse every success response against the verb's Zod
|
|
|
1179
1192
|
**output** schema, so a TS↔Rust envelope drift fails the suite — not just a
|
|
1180
1193
|
payload-field drift. Call-site primitives (`cross_rpc_call` / `error_reason` /
|
|
1181
1194
|
`expect_output`) live in `cross_backend/cell_cross_helpers.ts`; the shared
|
|
1182
|
-
options shape is `
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1195
|
+
options shape is `RpcPathCapabilityGatedCrossSuiteOptions` from
|
|
1196
|
+
`cross_backend/setup.ts`. `cross_backend/wire_shapes.ts` carries
|
|
1197
|
+
`assert_iso8601_seconds` / `assert_iso8601_seconds_nullable` — the byte-level
|
|
1198
|
+
gate on the canonical second-precision UTC timestamp shape, which a
|
|
1199
|
+
`z.string()` output schema can't catch (a millisecond stamp parses fine).
|
|
1200
|
+
The shared suites pin it on the account-status account + role grants, a
|
|
1201
|
+
session, a minted + listed API token, a cell's `created_at` / `updated_at`,
|
|
1202
|
+
an audit row, an admin account listing, and an offer's `created_at` /
|
|
1203
|
+
`expires_at`. `cross_backend/setup.ts` carries the four bases every
|
|
1204
|
+
imperative cross suite aliases: `CrossSuiteOptions` (`setup_test` alone),
|
|
1205
|
+
`CapabilityGatedCrossSuiteOptions` (+ `capabilities` — `ready`),
|
|
1206
|
+
`RpcPathCrossSuiteOptions` (+ `rpc_path?` — origin, body-size, actor
|
|
1207
|
+
lookup/search, app settings, testing backdoor), and the intersection
|
|
1208
|
+
`RpcPathCapabilityGatedCrossSuiteOptions` (every `cell_*` suite, query-shape,
|
|
1209
|
+
token-scope surface, fact-serving, account lifecycle). A suite takes the
|
|
1210
|
+
capability-carrying variant only when it actually gates cases on a flag, so
|
|
1211
|
+
the option type states whether the bundle is read.
|
|
1186
1212
|
|
|
1187
1213
|
- **`describe_cell_crud_cross_tests`** (gates on `capabilities.cell_crud`) —
|
|
1188
1214
|
the create → get → update → delete → list lifecycle threading the id, plus
|
|
@@ -1387,7 +1413,7 @@ censuses). Cross-process only; fuz_app's own wiring is
|
|
|
1387
1413
|
|
|
1388
1414
|
### Origin verification parity — `cross_backend/origin.ts`
|
|
1389
1415
|
|
|
1390
|
-
`describe_origin_cross_tests({setup_test,
|
|
1416
|
+
`describe_origin_cross_tests({setup_test, rpc_path?})` — the
|
|
1391
1417
|
imperative Origin-verification suite: disallowed `Origin` → 403 `forbidden_origin` (refused
|
|
1392
1418
|
before dispatch), absent `Origin` → request passes (non-browser direct access).
|
|
1393
1419
|
Imperative (not a conformance-table row) because origin rejection is
|
|
@@ -1461,7 +1487,7 @@ drift-guarded by `src/test/cross_backend/spine_expected_schema.db.test.ts`
|
|
|
1461
1487
|
|
|
1462
1488
|
### Testing-backdoor credential gate — `cross_backend/testing_backdoor.ts`
|
|
1463
1489
|
|
|
1464
|
-
`describe_testing_backdoor_cross_tests({setup_test,
|
|
1490
|
+
`describe_testing_backdoor_cross_tests({setup_test, rpc_path?})` —
|
|
1465
1491
|
the negative-credential parity suite for the `_testing_*` backdoor actions.
|
|
1466
1492
|
For each of `_testing_reset` / `_testing_mint_session` / `_testing_put_fact` /
|
|
1467
1493
|
`_testing_schema_snapshot` (the three privileged writes plus the schema-dump
|
|
@@ -1471,7 +1497,7 @@ proving the daemon-token gate that fences each backdoor action holds end-to-end
|
|
|
1471
1497
|
on the real dispatcher (the spec-derived `describe_rpc_attack_surface_tests`
|
|
1472
1498
|
never enumerates them because they're off the declared surface). Each method is
|
|
1473
1499
|
sent with **valid** params so each case varies only the credential (validation
|
|
1474
|
-
runs after the gates — order is 401 → authz → 403 → 400); the handler never
|
|
1500
|
+
runs after the gates — order is 401 → authz → 403 → 429 → 400); the handler never
|
|
1475
1501
|
runs. Cited property: `docs/security.md`
|
|
1476
1502
|
§Test Backdoor Actions. Cross-process only (the `_testing_*` actions are
|
|
1477
1503
|
mounted on the spawned binary, not the in-process app — like the ws/sse
|
|
@@ -3,7 +3,6 @@ import type { SessionOptions } from '../auth/session_cookie.js';
|
|
|
3
3
|
import { type RoleSchemaResult } from '../auth/role_schema.js';
|
|
4
4
|
import { type RpcEndpointsSuiteOption } from './rpc_helpers.js';
|
|
5
5
|
import type { AppSurfaceSpec } from '../http/surface.js';
|
|
6
|
-
import type { BackendCapabilities } from './cross_backend/capabilities.js';
|
|
7
6
|
import type { SetupTest } from './cross_backend/setup.js';
|
|
8
7
|
/**
|
|
9
8
|
* Configuration for `describe_standard_admin_integration_tests`.
|
|
@@ -21,8 +20,6 @@ export interface StandardAdminIntegrationTestOptions {
|
|
|
21
20
|
* shape for in-process and cross-process tests).
|
|
22
21
|
*/
|
|
23
22
|
surface_source: AppSurfaceSpec;
|
|
24
|
-
/** Backend capability declarations. */
|
|
25
|
-
capabilities: BackendCapabilities;
|
|
26
23
|
/** Session config — needed for cookie_name + factory-form rpc_endpoints resolution. */
|
|
27
24
|
session_options: SessionOptions<string>;
|
|
28
25
|
/** Role schema result from `create_role_schema()` — used to determine valid/invalid/web-grantable roles. */
|
|
@@ -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,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAc,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAW3E,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAmB1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,
|
|
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,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAc,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAW3E,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAmB1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,0BAA0B,CAAC;AAYvE;;GAEG;AACH,MAAM,WAAW,mCAAmC;IACnD;;;;OAIG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,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,IAk5BF,CAAC"}
|
|
@@ -77,7 +77,6 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
77
77
|
// confusing test failure when `rpc_endpoints` is missing.
|
|
78
78
|
const rpc_endpoints_for_setup = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
|
|
79
79
|
const rpc_path = require_rpc_endpoint_path(rpc_endpoints_for_setup);
|
|
80
|
-
void options.capabilities;
|
|
81
80
|
describe('standard_admin_integration', () => {
|
|
82
81
|
const { cookie_name } = options.session_options;
|
|
83
82
|
const { role_specs } = options.roles;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adversarial_404.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/adversarial_404.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAoB7B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAyBlE;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS,sBAAsB,KAAG,
|
|
1
|
+
{"version":3,"file":"adversarial_404.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/adversarial_404.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAoB7B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAyBlE;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS,sBAAsB,KAAG,IA6E1E,CAAC"}
|
|
@@ -13,7 +13,7 @@ import './assert_dev_env.js';
|
|
|
13
13
|
import { test, assert, describe } from 'vitest';
|
|
14
14
|
import { z } from 'zod';
|
|
15
15
|
import { is_null_schema } from '../http/schema_helpers.js';
|
|
16
|
-
import { create_auth_test_apps, select_auth_app } from './auth_apps.js';
|
|
16
|
+
import { create_auth_test_apps, create_route_skip_filter, select_auth_app } from './auth_apps.js';
|
|
17
17
|
import { resolve_valid_path, generate_valid_body } from './schema_generators.js';
|
|
18
18
|
/**
|
|
19
19
|
* Extract the error code from a 404 Zod schema for use in the stub handler.
|
|
@@ -51,6 +51,7 @@ const extract_404_error_code = (schema) => {
|
|
|
51
51
|
export const describe_adversarial_404 = (options) => {
|
|
52
52
|
const { build, roles } = options;
|
|
53
53
|
const { surface, route_specs } = build();
|
|
54
|
+
const is_skipped = create_route_skip_filter(options.skip_routes);
|
|
54
55
|
// Build spec lookup for Zod schema access
|
|
55
56
|
const spec_lookup = new Map();
|
|
56
57
|
for (const spec of route_specs) {
|
|
@@ -63,6 +64,8 @@ export const describe_adversarial_404 = (options) => {
|
|
|
63
64
|
continue;
|
|
64
65
|
if (!route.error_schemas || !('404' in route.error_schemas))
|
|
65
66
|
continue;
|
|
67
|
+
if (is_skipped(route))
|
|
68
|
+
continue;
|
|
66
69
|
const key = `${route.method} ${route.path}`;
|
|
67
70
|
const spec = spec_lookup.get(key);
|
|
68
71
|
if (!spec?.params || !spec.errors?.[404])
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adversarial_input.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/adversarial_input.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAiB7B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAUxB,OAAO,EAEN,0BAA0B,EAC1B,uBAAuB,EAGvB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AA8ClE,UAAU,aAAa;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,cAAc,EAAE,OAAO,0BAA0B,GAAG,OAAO,uBAAuB,CAAC;CACnF;AAED,UAAU,cAAc;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,UAAU,aAAa;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAID;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,GAAI,cAAc,CAAC,CAAC,OAAO,KAAG,KAAK,CAAC,aAAa,CAsMtF,CAAC;AAIF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,GAAI,eAAe,CAAC,CAAC,SAAS,KAAG,KAAK,CAAC,cAAc,CA+B3F,CAAC;AAIF;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GAAI,cAAc,CAAC,CAAC,SAAS,KAAG,KAAK,CAAC,aAAa,CA4CxF,CAAC;AAqBF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,GAAI,SAAS,sBAAsB,KAAG,
|
|
1
|
+
{"version":3,"file":"adversarial_input.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/adversarial_input.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAiB7B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAUxB,OAAO,EAEN,0BAA0B,EAC1B,uBAAuB,EAGvB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AA8ClE,UAAU,aAAa;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,cAAc,EAAE,OAAO,0BAA0B,GAAG,OAAO,uBAAuB,CAAC;CACnF;AAED,UAAU,cAAc;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,UAAU,aAAa;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAID;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,GAAI,cAAc,CAAC,CAAC,OAAO,KAAG,KAAK,CAAC,aAAa,CAsMtF,CAAC;AAIF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,GAAI,eAAe,CAAC,CAAC,SAAS,KAAG,KAAK,CAAC,cAAc,CA+B3F,CAAC;AAIF;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GAAI,cAAc,CAAC,CAAC,SAAS,KAAG,KAAK,CAAC,aAAa,CA4CxF,CAAC;AAqBF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,GAAI,SAAS,sBAAsB,KAAG,IAmN5E,CAAC"}
|
|
@@ -18,7 +18,7 @@ import { zod_unwrap_to_object, zod_extract_fields } from '@fuzdev/fuz_util/zod.t
|
|
|
18
18
|
import { is_null_schema } from '../http/schema_helpers.js';
|
|
19
19
|
import { filter_routes_with_input, filter_routes_with_params, filter_routes_with_query } from '../http/surface_query.js';
|
|
20
20
|
import { ValidationError, ERROR_INVALID_REQUEST_BODY, ERROR_INVALID_JSON_BODY, ERROR_INVALID_ROUTE_PARAMS, ERROR_INVALID_QUERY_PARAMS } from '../http/error_schemas.js';
|
|
21
|
-
import { create_auth_test_apps, select_auth_app } from './auth_apps.js';
|
|
21
|
+
import { create_auth_test_apps, create_route_skip_filter, select_auth_app } from './auth_apps.js';
|
|
22
22
|
import { detect_format, generate_valid_value, resolve_valid_path } from './schema_generators.js';
|
|
23
23
|
// --- Payload generation ---
|
|
24
24
|
/** One wrong-type value per base type — one representative is sufficient. */
|
|
@@ -374,9 +374,10 @@ const build_query_url = (path, query) => {
|
|
|
374
374
|
export const describe_adversarial_input = (options) => {
|
|
375
375
|
const { build, roles } = options;
|
|
376
376
|
const { surface, route_specs } = build();
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
-
const
|
|
377
|
+
const is_skipped = create_route_skip_filter(options.skip_routes);
|
|
378
|
+
const routes_with_input = filter_routes_with_input(surface).filter((r) => !is_skipped(r));
|
|
379
|
+
const routes_with_params = filter_routes_with_params(surface).filter((r) => !is_skipped(r));
|
|
380
|
+
const routes_with_query = filter_routes_with_query(surface).filter((r) => !is_skipped(r));
|
|
380
381
|
if (routes_with_input.length === 0 &&
|
|
381
382
|
routes_with_params.length === 0 &&
|
|
382
383
|
routes_with_query.length === 0)
|
|
@@ -3,8 +3,9 @@ import './assert_dev_env.js';
|
|
|
3
3
|
* Bootstrapped app server factory for integration tests.
|
|
4
4
|
*
|
|
5
5
|
* Creates a keeper account, API token, and signed session cookie on a
|
|
6
|
-
* database. By default uses a cached in-memory PGlite (shared WASM instance
|
|
7
|
-
* per
|
|
6
|
+
* database. By default uses a cached in-memory PGlite (one shared WASM instance
|
|
7
|
+
* per loaded copy of `testing/db.ts` — once per run under the `db` project's
|
|
8
|
+
* `isolate: false` + `fileParallelism: false`); pass `db` to use
|
|
8
9
|
* an existing database (any driver) instead.
|
|
9
10
|
*
|
|
10
11
|
* Also provides `create_test_app` — a combined helper that creates both
|
|
@@ -312,7 +313,7 @@ export interface TestApp {
|
|
|
312
313
|
*
|
|
313
314
|
* A fresh Hono app is created each call — middleware closures bind to the
|
|
314
315
|
* server's deps (db, keyring), so reuse across servers is unsafe.
|
|
315
|
-
* The expensive resource (PGlite WASM) is cached separately in `
|
|
316
|
+
* The expensive resource (PGlite WASM) is cached separately in `testing/db.ts`.
|
|
316
317
|
*
|
|
317
318
|
* @param options - test app configuration
|
|
318
319
|
* @returns a `TestApp` ready for HTTP testing
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/app_server.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B
|
|
1
|
+
{"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/app_server.ts"],"names":[],"mappings":"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"}
|
|
@@ -31,8 +31,8 @@ export const stub_password_deps = {
|
|
|
31
31
|
/** 64-hex-char test cookie secret — deterministic, never used in production. */
|
|
32
32
|
export const TEST_COOKIE_SECRET = 'a'.repeat(64);
|
|
33
33
|
// Module-level PGlite factory for create_test_app_server when no db is provided.
|
|
34
|
-
// Shares the WASM instance cache from
|
|
35
|
-
// within
|
|
34
|
+
// Shares the WASM instance cache from `testing/db.ts`, avoiding redundant cold starts
|
|
35
|
+
// within one loaded copy of that module. Schema is reset on each create() call.
|
|
36
36
|
const fallback_pglite_factory = create_pglite_factory(async (db) => {
|
|
37
37
|
await run_migrations(db, [auth_migration_ns]);
|
|
38
38
|
});
|
|
@@ -205,10 +205,16 @@ const _build_test_backend = async (options) => {
|
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
207
|
else {
|
|
208
|
-
// In-memory PGlite via cached factory — reuses the WASM instance from
|
|
208
|
+
// In-memory PGlite via cached factory — reuses the WASM instance from `testing/db.ts`
|
|
209
209
|
// instead of creating a new PGlite each time. Schema is reset and migrations re-run
|
|
210
|
-
// on each call, but the expensive WASM cold start only happens once per
|
|
210
|
+
// on each call, but the expensive WASM cold start only happens once per loaded
|
|
211
|
+
// copy of `testing/db.ts` — under the `db` project's `isolate: false` +
|
|
212
|
+
// `fileParallelism: false` that is once for the whole run.
|
|
211
213
|
// `migration_namespaces` selects an auth+extras factory; auth-only is the default.
|
|
214
|
+
// `db_type` below names PGlite unconditionally: a run that installs a substitute
|
|
215
|
+
// factory (`set_substitute_db_factory`) before this module loads gets that driver
|
|
216
|
+
// here, and no caller reads this field to pick behavior — it is reported, not
|
|
217
|
+
// dispatched on.
|
|
212
218
|
const db = await resolve_fallback_factory(migration_namespaces).create();
|
|
213
219
|
const audit = audit_factory({ db, log: test_log });
|
|
214
220
|
backend = {
|
|
@@ -255,7 +261,7 @@ export const create_test_app_server = async (options) => {
|
|
|
255
261
|
*
|
|
256
262
|
* A fresh Hono app is created each call — middleware closures bind to the
|
|
257
263
|
* server's deps (db, keyring), so reuse across servers is unsafe.
|
|
258
|
-
* The expensive resource (PGlite WASM) is cached separately in `
|
|
264
|
+
* The expensive resource (PGlite WASM) is cached separately in `testing/db.ts`.
|
|
259
265
|
*
|
|
260
266
|
* @param options - test app configuration
|
|
261
267
|
* @returns a `TestApp` ready for HTTP testing
|
|
@@ -7,14 +7,25 @@ export interface AdversarialTestOptions {
|
|
|
7
7
|
build: () => AppSurfaceSpec;
|
|
8
8
|
/** All roles in the app (e.g. `['admin', 'keeper']`). */
|
|
9
9
|
roles: Array<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Routes to skip, in `'METHOD /path'` form (the surface key) — the escape
|
|
12
|
+
* hatch every sibling suite carries. Reach for it only when a route
|
|
13
|
+
* cannot be driven generically at all (a handler needing real seeded
|
|
14
|
+
* state, a path segment no schema can describe); a route whose params
|
|
15
|
+
* merely have a format is handled by the synthesizer.
|
|
16
|
+
*/
|
|
17
|
+
skip_routes?: Array<string>;
|
|
10
18
|
}
|
|
11
19
|
/**
|
|
12
20
|
* Generate adversarial HTTP auth enforcement test suites.
|
|
13
21
|
*
|
|
14
22
|
* Describe blocks:
|
|
15
23
|
* - unauthenticated → 401 — every protected route
|
|
16
|
-
* - wrong role → 403 — every role route
|
|
17
|
-
*
|
|
24
|
+
* - wrong role → 403 — every role route the role apps' session credential
|
|
25
|
+
* reaches, tested with all non-matching roles
|
|
26
|
+
* - authenticated without role → 403 — the same routes, no-role context
|
|
27
|
+
* - credential-gated routes reject a disallowed credential → 403 — every
|
|
28
|
+
* route declaring `auth.credential_types`, probed on a channel it refuses
|
|
18
29
|
* - correct auth passes guard — every protected route, assert not 401/403
|
|
19
30
|
*/
|
|
20
31
|
export declare const describe_adversarial_auth: (options: AdversarialTestOptions) => void;
|
|
@@ -63,6 +74,14 @@ export interface StandardAttackSurfaceOptions {
|
|
|
63
74
|
* to skip the assertion and keep the audit log informational-only.
|
|
64
75
|
*/
|
|
65
76
|
error_schema_tightness?: ErrorSchemaTightnessOptions | null;
|
|
77
|
+
/**
|
|
78
|
+
* Routes the three adversarial sub-suites skip, in `'METHOD /path'` form
|
|
79
|
+
* — the escape hatch every sibling suite carries (`round_trip`,
|
|
80
|
+
* `data_exposure`). Reach for it only when a route cannot be driven
|
|
81
|
+
* generically at all; a route whose path params merely carry a format is
|
|
82
|
+
* handled by the params-schema-aware synthesizer.
|
|
83
|
+
*/
|
|
84
|
+
skip_routes?: Array<string>;
|
|
66
85
|
}
|
|
67
86
|
/**
|
|
68
87
|
* Run the standard attack surface test suite.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attack_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/attack_surface.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAoB7B,OAAO,EAQN,KAAK,4BAA4B,EACjC,KAAK,2BAA2B,EAChC,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"attack_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/attack_surface.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAoB7B,OAAO,EAQN,KAAK,4BAA4B,EACjC,KAAK,2BAA2B,EAChC,MAAM,yBAAyB,CAAC;AAejC,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAsDpF,oFAAoF;AACpF,MAAM,WAAW,sBAAsB;IACtC,+EAA+E;IAC/E,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,sBAAsB,KAAG,IA2J3E,CAAC;AAIF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,uCAAuC,GACnD,UAAU,2BAA2B,GAAG,IAAI,GAAG,SAAS,KACtD,2BAA2B,GAAG,IAWhC,CAAC;AAEF,0DAA0D;AAC1D,MAAM,WAAW,4BAA4B;IAC5C,+EAA+E;IAC/E,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,sBAAsB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACtC,gHAAgH;IAChH,uBAAuB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACvC,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iEAAiE;IACjE,eAAe,CAAC,EAAE,4BAA4B,CAAC;IAC/C;;;;;;;;;;;OAWG;IACH,sBAAsB,CAAC,EAAE,2BAA2B,GAAG,IAAI,CAAC;IAC5D;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sCAAsC,GAClD,SAAS,4BAA4B,KACnC,IA4EF,CAAC"}
|