@fuzdev/fuz_app 0.93.0 → 0.94.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/README.md +2 -2
- package/dist/actions/action_codegen.d.ts +18 -18
- package/dist/actions/action_codegen.js +30 -30
- package/dist/actions/action_event.js +1 -1
- package/dist/actions/action_event_helpers.d.ts +1 -1
- package/dist/actions/action_event_types.d.ts +1 -1
- package/dist/actions/action_rpc.d.ts +2 -2
- package/dist/actions/broadcast_api.d.ts +1 -1
- package/dist/actions/broadcast_api.js +1 -1
- package/dist/actions/perform_action.d.ts +2 -2
- package/dist/actions/register_action_ws.d.ts +2 -2
- package/dist/actions/register_action_ws.js +2 -2
- package/dist/actions/register_ws_endpoint.js +1 -1
- package/dist/actions/request_tracker.svelte.d.ts +1 -1
- package/dist/actions/request_tracker.svelte.js +1 -1
- package/dist/actions/rpc_client.d.ts +1 -1
- package/dist/actions/socket.svelte.d.ts +2 -2
- package/dist/actions/transports_ws.js +1 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +1 -1
- package/dist/actions/transports_ws_backend.d.ts +1 -1
- package/dist/actions/transports_ws_backend.js +2 -2
- package/dist/auth/account_action_specs.js +1 -1
- package/dist/auth/account_schema.d.ts +1 -1
- package/dist/auth/account_schema.js +1 -1
- package/dist/auth/actor_lookup_action_specs.js +1 -1
- package/dist/auth/actor_lookup_queries.d.ts +1 -1
- package/dist/auth/actor_search_action_specs.js +1 -1
- package/dist/auth/actor_search_queries.d.ts +1 -1
- package/dist/auth/admin_action_specs.js +1 -1
- package/dist/auth/api_token.js +1 -1
- package/dist/auth/api_token_queries.d.ts +1 -1
- package/dist/auth/app_settings_schema.d.ts +1 -1
- package/dist/auth/app_settings_schema.js +1 -1
- package/dist/auth/audit_emitter.d.ts +2 -2
- package/dist/auth/audit_log_routes.d.ts +1 -1
- package/dist/auth/audit_log_schema.d.ts +1 -1
- package/dist/auth/audit_log_schema.js +2 -2
- package/dist/auth/bearer_auth.d.ts +1 -1
- package/dist/auth/bootstrap_account.d.ts +1 -1
- package/dist/auth/bootstrap_route_schema.js +1 -1
- package/dist/auth/bootstrap_routes.d.ts +1 -1
- package/dist/auth/bootstrap_routes.js +1 -1
- package/dist/auth/cell_action_specs.js +2 -2
- package/dist/auth/cell_audit_action_specs.js +1 -1
- package/dist/auth/cell_audit_metadata.js +1 -1
- package/dist/auth/cell_field_action_specs.js +1 -1
- package/dist/auth/cell_field_audit_metadata.js +1 -1
- package/dist/auth/cell_grant_action_specs.js +1 -1
- package/dist/auth/cell_grant_audit_metadata.js +1 -1
- package/dist/auth/cell_item_action_specs.d.ts +1 -1
- package/dist/auth/cell_item_action_specs.js +3 -3
- package/dist/auth/cell_item_audit_metadata.js +1 -1
- package/dist/auth/cell_relation_visibility.d.ts +1 -1
- package/dist/auth/cleanup.d.ts +1 -1
- package/dist/auth/daemon_token_middleware.d.ts +1 -1
- package/dist/auth/deps.d.ts +2 -2
- package/dist/auth/invite_schema.d.ts +1 -1
- package/dist/auth/invite_schema.js +1 -1
- package/dist/auth/request_context.d.ts +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +1 -1
- package/dist/auth/role_grant_offer_notifications.d.ts +1 -1
- package/dist/auth/role_grant_offer_notifications.js +1 -1
- package/dist/auth/role_grant_offer_queries.d.ts +1 -1
- package/dist/auth/role_grant_offer_schema.d.ts +1 -1
- package/dist/auth/role_grant_offer_schema.js +1 -1
- package/dist/auth/role_grant_queries.d.ts +1 -1
- package/dist/auth/session_queries.d.ts +1 -1
- package/dist/auth/session_queries.js +1 -1
- package/dist/auth/signup_route_schema.js +1 -1
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.js +2 -2
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +1 -1
- package/dist/cli/logger.d.ts +1 -1
- package/dist/cli/logger.js +1 -1
- package/dist/db/CLAUDE.md +1 -1
- package/dist/db/cell_audit_queries.d.ts +1 -1
- package/dist/db/cell_field_queries.d.ts +1 -1
- package/dist/db/cell_grant_queries.d.ts +1 -1
- package/dist/db/cell_item_queries.d.ts +1 -1
- package/dist/db/cell_queries.d.ts +3 -3
- package/dist/db/cell_queries.js +1 -1
- package/dist/db/fact_disk_storage.d.ts +2 -2
- package/dist/db/fact_disk_storage.js +4 -4
- package/dist/db/fact_queries.d.ts +1 -1
- package/dist/db/fact_store.d.ts +3 -3
- package/dist/db/fact_store.js +2 -2
- package/dist/db/file_fact_url.d.ts +1 -1
- package/dist/db/file_fact_url.js +1 -1
- package/dist/db/migrate.js +1 -1
- package/dist/db/status.js +1 -1
- package/dist/http/auth_shape.js +2 -2
- package/dist/http/common_routes.d.ts +1 -1
- package/dist/http/db_routes.d.ts +1 -1
- package/dist/http/origin.js +1 -1
- package/dist/http/pending_effects.d.ts +1 -1
- package/dist/http/proxy.d.ts +1 -1
- package/dist/http/route_spec.d.ts +1 -1
- package/dist/rate_limiter.js +1 -1
- package/dist/realtime/sse.d.ts +1 -1
- package/dist/realtime/sse_auth_guard.d.ts +1 -1
- package/dist/runtime/deno.js +1 -1
- package/dist/server/app_backend.d.ts +1 -1
- package/dist/server/app_backend.js +1 -1
- package/dist/server/env.js +1 -1
- package/dist/server/fact_write.d.ts +2 -2
- package/dist/server/fact_write.js +1 -1
- package/dist/server/serve_fact_route.d.ts +1 -1
- package/dist/server/serve_fact_route.js +2 -2
- package/dist/server/startup.d.ts +1 -1
- package/dist/testing/CLAUDE.md +23 -5
- package/dist/testing/adversarial_input.js +1 -1
- package/dist/testing/app_server.d.ts +1 -1
- package/dist/testing/app_server.js +1 -1
- package/dist/testing/auth_apps.js +1 -1
- package/dist/testing/cross_backend/bench/bench_report.d.ts +1 -1
- package/dist/testing/cross_backend/bench/bench_report.js +2 -2
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +1 -1
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +1 -1
- package/dist/testing/cross_backend/bootstrap_backend.d.ts +1 -1
- package/dist/testing/cross_backend/cell_relations.js +1 -1
- package/dist/testing/cross_backend/conformance_case.d.ts +4 -0
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +26 -0
- package/dist/testing/cross_backend/conformance_table.d.ts +21 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +109 -7
- package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts +1 -1
- package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts +1 -1
- package/dist/testing/cross_backend/default_spine_surface.d.ts +1 -1
- package/dist/testing/cross_backend/fact_serving.js +1 -1
- package/dist/testing/cross_backend/setup.d.ts +1 -1
- package/dist/testing/cross_backend/setup.js +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +2 -2
- package/dist/testing/cross_backend/testing_server_core.d.ts +1 -1
- package/dist/testing/cross_backend/testing_server_core.js +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +1 -1
- package/dist/testing/db.js +4 -4
- package/dist/testing/middleware.js +1 -1
- package/dist/testing/role_grant_helpers.d.ts +1 -1
- package/dist/testing/rpc_helpers.d.ts +18 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +24 -2
- package/dist/testing/schema_generators.d.ts +1 -1
- package/dist/testing/schema_generators.js +1 -1
- package/dist/testing/stubs.js +1 -1
- package/dist/testing/testing_rate_limiter.d.ts +1 -1
- package/dist/testing/testing_rate_limiter.js +1 -1
- package/dist/testing/transports/bootstrap.d.ts +1 -1
- package/dist/testing/transports/bootstrap.js +1 -1
- package/dist/testing/ws_round_trip.d.ts +2 -2
- package/dist/testing/ws_round_trip.js +2 -2
- package/dist/ui/AdminSurface.svelte +1 -1
- package/dist/ui/AppShell.svelte +1 -1
- package/dist/ui/BootstrapForm.svelte +1 -1
- package/dist/ui/CLAUDE.md +1 -1
- package/dist/ui/ConfirmButton.svelte +2 -2
- package/dist/ui/ConfirmButton.svelte.d.ts +1 -1
- package/dist/ui/LoginForm.svelte +1 -1
- package/dist/ui/MenuLink.svelte +1 -1
- package/dist/ui/MenuLink.svelte.d.ts +1 -1
- package/dist/ui/PopoverButton.svelte +1 -1
- package/dist/ui/PopoverButton.svelte.d.ts +1 -1
- package/dist/ui/SignupForm.svelte +1 -1
- package/dist/ui/account_sessions_state.svelte.js +1 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +1 -1
- package/dist/ui/admin_invites_state.svelte.d.ts +1 -1
- package/dist/ui/admin_invites_state.svelte.js +1 -1
- package/dist/ui/admin_sessions_state.svelte.d.ts +1 -1
- package/dist/ui/app_settings_state.svelte.js +1 -1
- package/dist/ui/async_slot.svelte.d.ts +2 -2
- package/dist/ui/async_slot.svelte.js +2 -2
- package/dist/ui/audit_log_state.svelte.js +1 -1
- package/dist/ui/auth_state.svelte.d.ts +1 -1
- package/dist/ui/auth_state.svelte.js +3 -3
- package/dist/ui/format_scope.js +1 -1
- package/dist/ui/popover.svelte.d.ts +1 -1
- package/dist/ui/popover.svelte.js +2 -2
- package/dist/ui/position_helpers.js +1 -1
- package/dist/ui/role_grant_offers_state.svelte.js +1 -1
- package/dist/ui/sidebar_state.svelte.js +1 -1
- package/dist/ui/table_state.svelte.d.ts +1 -1
- package/dist/ui/table_state.svelte.js +2 -2
- package/package.json +2 -2
package/dist/http/db_routes.d.ts
CHANGED
package/dist/http/origin.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
|
-
import { escape_regexp } from '@fuzdev/fuz_util/regexp.
|
|
12
|
+
import { escape_regexp } from '@fuzdev/fuz_util/regexp.ts';
|
|
13
13
|
import { ERROR_FORBIDDEN_ORIGIN } from './error_schemas.js';
|
|
14
14
|
/**
|
|
15
15
|
* Parses FUZ_ALLOWED_ORIGINS env var into regex matchers for request source verification.
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
*
|
|
40
40
|
* @module
|
|
41
41
|
*/
|
|
42
|
-
import type { Logger } from '@fuzdev/fuz_util/log.
|
|
42
|
+
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
43
43
|
/**
|
|
44
44
|
* Minimal structural context required by `emit_after_commit`. Both
|
|
45
45
|
* `RouteContext` and `ActionContext` satisfy this — they each carry
|
package/dist/http/proxy.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
import type { Context, MiddlewareHandler } from 'hono';
|
|
11
|
-
import type { Logger } from '@fuzdev/fuz_util/log.
|
|
11
|
+
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
12
12
|
import type { MiddlewareSpec } from './middleware_spec.js';
|
|
13
13
|
/**
|
|
14
14
|
* Normalize an IP address for consistent matching and storage.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { Context, Hono, MiddlewareHandler } from 'hono';
|
|
16
16
|
import type { z } from 'zod';
|
|
17
|
-
import type { Logger } from '@fuzdev/fuz_util/log.
|
|
17
|
+
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
18
18
|
import type { Db } from '../db/db.js';
|
|
19
19
|
import { type RouteErrorSchemas, type RateLimitKey } from './error_schemas.js';
|
|
20
20
|
import type { MiddlewareSpec } from './middleware_spec.js';
|
package/dist/rate_limiter.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
-
import { LruMap } from '@fuzdev/fuz_util/lru_map.
|
|
9
|
+
import { LruMap } from '@fuzdev/fuz_util/lru_map.ts';
|
|
10
10
|
import { ERROR_RATE_LIMIT_EXCEEDED } from './http/error_schemas.js';
|
|
11
11
|
/**
|
|
12
12
|
* Default tracked-key cap: bounds worst-case memory under key-enumeration
|
package/dist/realtime/sse.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @module
|
|
12
12
|
*/
|
|
13
|
-
import type { Logger } from '@fuzdev/fuz_util/log.
|
|
13
|
+
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
14
14
|
import { type AuditLogEvent } from '../auth/audit_log_schema.js';
|
|
15
15
|
import { SubscriberRegistry, type SubscribeOptions } from './subscriber_registry.js';
|
|
16
16
|
import type { SseStream, SseNotification, EventSpec } from './sse.js';
|
package/dist/runtime/deno.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @module
|
|
12
12
|
*/
|
|
13
|
-
import { Logger } from '@fuzdev/fuz_util/log.
|
|
13
|
+
import { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
14
14
|
import type { AppDeps } from '../auth/deps.js';
|
|
15
15
|
import { type AuditEmitter } from '../auth/audit_emitter.js';
|
|
16
16
|
import type { DbType, Db } from '../db/db.js';
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @module
|
|
12
12
|
*/
|
|
13
|
-
import { Logger } from '@fuzdev/fuz_util/log.
|
|
13
|
+
import { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
14
14
|
import { create_audit_emitter } from '../auth/audit_emitter.js';
|
|
15
15
|
import { run_migrations } from '../db/migrate.js';
|
|
16
16
|
import { auth_migration_ns, reserved_migration_namespaces } from '../auth/migrations.js';
|
package/dist/server/env.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
-
import { to_error_message } from '@fuzdev/fuz_util/error.
|
|
12
|
+
import { to_error_message } from '@fuzdev/fuz_util/error.ts';
|
|
13
13
|
import { create_validated_keyring } from '../auth/keyring.js';
|
|
14
14
|
import { parse_allowed_origins } from '../http/origin.js';
|
|
15
15
|
/**
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @module
|
|
12
12
|
*/
|
|
13
|
-
import { type FactHash } from '@fuzdev/fuz_util/fact_hash.
|
|
14
|
-
import type { FactStore } from '@fuzdev/fuz_util/fact_store.
|
|
13
|
+
import { type FactHash } from '@fuzdev/fuz_util/fact_hash.ts';
|
|
14
|
+
import type { FactStore } from '@fuzdev/fuz_util/fact_store.ts';
|
|
15
15
|
export interface WriteFactOptions {
|
|
16
16
|
content_type: string;
|
|
17
17
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { randomBytes } from 'node:crypto';
|
|
14
14
|
import { writeFile, rename, mkdir, unlink } from 'node:fs/promises';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
|
-
import { FACT_HASH_PREFIX, fact_hash_bytes } from '@fuzdev/fuz_util/fact_hash.
|
|
16
|
+
import { FACT_HASH_PREFIX, fact_hash_bytes } from '@fuzdev/fuz_util/fact_hash.ts';
|
|
17
17
|
import { mint_file_fact_url } from '../db/file_fact_url.js';
|
|
18
18
|
/**
|
|
19
19
|
* Write `bytes` as a fact, choosing embedded (PG) vs external (disk +
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
*
|
|
73
73
|
* @module
|
|
74
74
|
*/
|
|
75
|
-
import type { Logger } from '@fuzdev/fuz_util/log.
|
|
75
|
+
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
76
76
|
import { type RouteSpec } from '../http/route_spec.js';
|
|
77
77
|
import type { AppDeps } from '../auth/deps.js';
|
|
78
78
|
export interface CreateServeFactRouteSpecOptions {
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
import { createReadStream } from 'node:fs';
|
|
76
76
|
import { Readable } from 'node:stream';
|
|
77
77
|
import { join } from 'node:path';
|
|
78
|
-
import { FactHashSchema } from '@fuzdev/fuz_util/fact_hash.
|
|
79
|
-
import { Uuid } from '@fuzdev/fuz_util/id.
|
|
78
|
+
import { FactHashSchema } from '@fuzdev/fuz_util/fact_hash.ts';
|
|
79
|
+
import { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
80
80
|
import { z } from 'zod';
|
|
81
81
|
import { build_request_context, get_request_context, has_role, } from '../auth/request_context.js';
|
|
82
82
|
import { ROLE_ADMIN } from '../auth/role_schema.js';
|
package/dist/server/startup.d.ts
CHANGED
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -13,7 +13,7 @@ testing-patterns. This file is a reference index for the helpers themselves.
|
|
|
13
13
|
|
|
14
14
|
## Production guard — always the first import
|
|
15
15
|
|
|
16
|
-
Every runtime-reachable module here starts with `import './assert_dev_env.
|
|
16
|
+
Every runtime-reachable module here starts with `import './assert_dev_env.ts';`
|
|
17
17
|
— reads `DEV` from `esm-env` and throws if false, preventing production-bundle
|
|
18
18
|
inclusion. Make this the first line in new modules. Enforced by
|
|
19
19
|
`src/test/testing/assert_dev_env_coverage.test.ts`, which fails if any module
|
|
@@ -770,7 +770,8 @@ One-shot transport:
|
|
|
770
770
|
|
|
771
771
|
- `RpcTestTransport = (url, init) => Promise<Response>` — duck type `Hono.request` already satisfies.
|
|
772
772
|
- `http_transport(app)` — adapter for anything with a `request()` method.
|
|
773
|
-
- `RpcCallResult` — discriminated `{ok: true, status, result}` / `{ok: false, status, error: {code, message, data?}}`.
|
|
773
|
+
- `RpcCallResult` — discriminated `{ok: true, status, headers, result}` / `{ok: false, status, headers, error: {code, message, data?}}`. `headers` is the lowercased-key response-header snapshot so callers can assert header-level properties (e.g. the conformance runner's no-fingerprint invariant).
|
|
774
|
+
- `headers_to_record(headers)` — snapshot a `Headers` into a lowercased-key `Record<string, string>` (multi-value collapses to the comma-joined form; `Set-Cookie` excluded). Used by `rpc_call` and the conformance table's REST branch to thread response headers through for assertions.
|
|
774
775
|
- `RpcCallArgs` — `{app, path, method, params?, headers?, id?, verb?}`. `verb` defaults to `'POST'`; use `'GET'` for `side_effects: false` methods.
|
|
775
776
|
- `rpc_call(args)` — merges `RPC_CALL_DEFAULT_HEADERS` (`host: 'localhost'`, `origin: 'http://localhost:5173'`, `Content-Type: 'application/json'`) under caller headers. Envelope-shape violations throw; JSON-RPC errors return `{ok: false, error}` so callers assert on `error.code` / `error.data.reason`.
|
|
776
777
|
- `rpc_call_typed<T>(args, output_schema)` — parses the success `result` through the schema; throws on envelope failure, error response, or schema mismatch. Use `rpc_call` when the test needs to assert on error shapes.
|
|
@@ -822,7 +823,7 @@ points:
|
|
|
822
823
|
|
|
823
824
|
- **`await_pending_effects: true`** is set by `create_test_app`. Fire-and-forget effects (audit logs, session touches, WS fan-out via `emit_after_commit`) resolve before the response returns, so tests can assert on side effects inline without manual flushing.
|
|
824
825
|
- **Deep-path imports only.** Import from the canonical module (`testing/db.js`, `testing/rpc_helpers.js`, etc.); fuz_app's `dist/` ships no barrel.
|
|
825
|
-
- **DI via small `*Deps` interfaces.** Stub factories accept the same narrow `*Deps` contracts production code uses — never `Pick<GodType, ...>`. New helpers needing env/fs/logger take `EnvDeps` / `FsReadDeps` / `Logger` from `runtime/deps.ts` or `@fuzdev/fuz_util/log.
|
|
826
|
+
- **DI via small `*Deps` interfaces.** Stub factories accept the same narrow `*Deps` contracts production code uses — never `Pick<GodType, ...>`. New helpers needing env/fs/logger take `EnvDeps` / `FsReadDeps` / `Logger` from `runtime/deps.ts` or `@fuzdev/fuz_util/log.ts`.
|
|
826
827
|
- **Keep the shared echo routes in sync with public surface.** When middleware or public API gains a new context variable, header, or field, update the echo in `middleware.ts` (`create_bearer_auth_test_app`, `create_test_middleware_stack_app`) alongside the assertions in `src/test/auth/*.test.ts`. Drift surfaces as a missed assertion, not a test failure.
|
|
827
828
|
|
|
828
829
|
## Cross-backend integration layer
|
|
@@ -966,7 +967,7 @@ wire-shape check passes green on even when behavior is wrong.
|
|
|
966
967
|
|
|
967
968
|
- `conformance_case.ts` — `ConformanceCase` Zod schema:
|
|
968
969
|
`{name, request: {method, params?, as, verb?}, expect: {status,
|
|
969
|
-
error_reason?, fields?}, note?, xfail?}`. A case is **data** — `method`
|
|
970
|
+
error_reason?, fields?, headers?, equivalence_group?}, note?, xfail?}`. A case is **data** — `method`
|
|
970
971
|
resolves its `input`/`output` from the live registry (RPC) or `RouteSpec`
|
|
971
972
|
(the 6 REST auth routes), so the case never carries a schema. `as` is the
|
|
972
973
|
closed `ConformancePrincipal` enum (`keeper` / `daemon` / `token` /
|
|
@@ -978,7 +979,24 @@ error_reason?, fields?}, note?, xfail?}`. A case is **data** — `method`
|
|
|
978
979
|
`error_reason` is the imported
|
|
979
980
|
`ERROR_*` constant (asserted against the RPC `error.data.reason` or the
|
|
980
981
|
REST flat-body `error`; the bare `unauthenticated()` 401 carries no
|
|
981
|
-
reason, so `status` pins that denial class).
|
|
982
|
+
reason, so `status` pins that denial class). `equivalence_group` is the
|
|
983
|
+
**negative-space** axis: every case tagged with the same group string must
|
|
984
|
+
produce a **byte-identical** normalized response (`{status, body}`),
|
|
985
|
+
asserted per impl after the case loop. It promotes a masked pair
|
|
986
|
+
(wrong-password ≡ account-not-found, found-but-unauthorized ≡ not-found)
|
|
987
|
+
from "same status + reason" to "wire-indistinguishable", so a prober
|
|
988
|
+
hitting either spine can't tell the members apart — the twin of the
|
|
989
|
+
positive `output`-schema parity the runner already enforces. The
|
|
990
|
+
per-case status/reason assertions stay the non-vacuous positive control
|
|
991
|
+
(a both-paths-identically-broken group still fails the per-case checks).
|
|
992
|
+
`headers` is the **header** negative-space axis: a string value pins a
|
|
993
|
+
header present-and-equal (case-insensitive name), `null` pins it absent.
|
|
994
|
+
Independent of it, the runner enforces an always-on **no-fingerprint**
|
|
995
|
+
invariant on _every_ response (`conformance_table.ts` `FINGERPRINT_HEADERS`
|
|
996
|
+
— `Server` / `X-Powered-By` / `WWW-Authenticate` must stay absent on both
|
|
997
|
+
spines, so a framework upgrade adding one can't become a silent
|
|
998
|
+
backend-fingerprinting oracle). Headers are deliberately kept out of the
|
|
999
|
+
`equivalence_group` `{status, body}` comparison (`Set-Cookie` / `Date` vary).
|
|
982
1000
|
- `conformance_table.ts` — `describe_conformance_table_tests({cases,
|
|
983
1001
|
setup_test, surface_source, capabilities, rpc_endpoints, session_options,
|
|
984
1002
|
principals?, suite_name?})`. Same `{setup_test, surface_source,
|
|
@@ -14,7 +14,7 @@ import './assert_dev_env.js';
|
|
|
14
14
|
*/
|
|
15
15
|
import { test, assert, describe } from 'vitest';
|
|
16
16
|
import { z } from 'zod';
|
|
17
|
-
import { zod_unwrap_to_object, zod_extract_fields } from '@fuzdev/fuz_util/zod.
|
|
17
|
+
import { zod_unwrap_to_object, zod_extract_fields } from '@fuzdev/fuz_util/zod.ts';
|
|
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';
|
|
@@ -13,7 +13,7 @@ import './assert_dev_env.js';
|
|
|
13
13
|
* @module
|
|
14
14
|
*/
|
|
15
15
|
import type { Hono } from 'hono';
|
|
16
|
-
import type { Uuid } from '@fuzdev/fuz_util/id.
|
|
16
|
+
import type { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
17
17
|
import { type Keyring } from '../auth/keyring.js';
|
|
18
18
|
import type { Db, DbType } from '../db/db.js';
|
|
19
19
|
import type { PasswordHashDeps } from '../auth/password.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import './assert_dev_env.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { Logger } from '@fuzdev/fuz_util/log.
|
|
3
|
+
import { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
4
4
|
import { DEFAULT_TEST_PASSWORD } from './test_credentials.js';
|
|
5
5
|
import { ROLE_KEEPER } from '../auth/role_schema.js';
|
|
6
6
|
import { create_validated_keyring } from '../auth/keyring.js';
|
|
@@ -8,7 +8,7 @@ import './assert_dev_env.js';
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
import { Hono } from 'hono';
|
|
11
|
-
import { Logger } from '@fuzdev/fuz_util/log.
|
|
11
|
+
import { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
12
12
|
import { apply_route_specs } from '../http/route_spec.js';
|
|
13
13
|
import { is_public_auth } from '../http/auth_shape.js';
|
|
14
14
|
import { fuz_auth_guard_resolver } from '../auth/auth_guard_resolver.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import '../../assert_dev_env.js';
|
|
2
|
-
import { type BenchmarkComparison } from '@fuzdev/fuz_util/benchmark_stats.
|
|
2
|
+
import { type BenchmarkComparison } from '@fuzdev/fuz_util/benchmark_stats.ts';
|
|
3
3
|
import type { CrossImplBenchResult } from './run_cross_impl_bench.js';
|
|
4
4
|
/**
|
|
5
5
|
* Reporting adapters over a `CrossImplBenchResult` — all built on fuz_util's
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import '../../assert_dev_env.js';
|
|
2
|
-
import { benchmark_format_markdown } from '@fuzdev/fuz_util/benchmark_format.
|
|
3
|
-
import { benchmark_stats_compare, } from '@fuzdev/fuz_util/benchmark_stats.
|
|
2
|
+
import { benchmark_format_markdown } from '@fuzdev/fuz_util/benchmark_format.ts';
|
|
3
|
+
import { benchmark_stats_compare, } from '@fuzdev/fuz_util/benchmark_stats.ts';
|
|
4
4
|
/**
|
|
5
5
|
* Reporting adapters over a `CrossImplBenchResult` — all built on fuz_util's
|
|
6
6
|
* formatters + Welch comparison. Markdown for human eyeballs, a per-scenario
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import '../../assert_dev_env.js';
|
|
2
|
-
import type { BenchmarkConfig, BenchmarkResult } from '@fuzdev/fuz_util/benchmark_types.
|
|
2
|
+
import type { BenchmarkConfig, BenchmarkResult } from '@fuzdev/fuz_util/benchmark_types.ts';
|
|
3
3
|
import type { BootstrappedBackendHandle } from '../setup.js';
|
|
4
4
|
import type { BenchScenario } from './scenario.js';
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import '../../assert_dev_env.js';
|
|
2
|
-
import { Benchmark } from '@fuzdev/fuz_util/benchmark.
|
|
2
|
+
import { Benchmark } from '@fuzdev/fuz_util/benchmark.ts';
|
|
3
3
|
/**
|
|
4
4
|
* Network-tuned defaults — fuz_util's micro-benchmark defaults (warmup 10,
|
|
5
5
|
* min 30, duration 1000ms) are sized for sub-microsecond functions. RPC round
|
|
@@ -6,7 +6,7 @@ import '../assert_dev_env.js';
|
|
|
6
6
|
* consumer's vitest `globalSetup` reduces to a single await:
|
|
7
7
|
*
|
|
8
8
|
* ```ts
|
|
9
|
-
* import {bootstrap_backend} from '@fuzdev/fuz_app/testing/cross_backend/bootstrap_backend.
|
|
9
|
+
* import {bootstrap_backend} from '@fuzdev/fuz_app/testing/cross_backend/bootstrap_backend.ts';
|
|
10
10
|
*
|
|
11
11
|
* export default async function ({provide}) {
|
|
12
12
|
* const bootstrapped = await bootstrap_backend(deno_backend_config());
|
|
@@ -52,7 +52,7 @@ import '../assert_dev_env.js';
|
|
|
52
52
|
* @module
|
|
53
53
|
*/
|
|
54
54
|
import { describe, assert } from 'vitest';
|
|
55
|
-
import { fractional_index_between } from '@fuzdev/fuz_util/fractional_index.
|
|
55
|
+
import { fractional_index_between } from '@fuzdev/fuz_util/fractional_index.ts';
|
|
56
56
|
import { CellCreateOutput, CellUpdateOutput, CellCloneOutput, CellGetOutput, } from '../../auth/cell_action_specs.js';
|
|
57
57
|
import { AuditLogListOutput } from '../../auth/admin_action_specs.js';
|
|
58
58
|
import { CellGrantCreateOutput, CellGrantListOutput, CellGrantRevokeOutput, } from '../../auth/cell_grant_action_specs.js';
|
|
@@ -85,6 +85,8 @@ export declare const ConformanceCaseExpectation: z.ZodObject<{
|
|
|
85
85
|
status: z.ZodNumber;
|
|
86
86
|
error_reason: z.ZodOptional<z.ZodString>;
|
|
87
87
|
fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
88
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
|
|
89
|
+
equivalence_group: z.ZodOptional<z.ZodString>;
|
|
88
90
|
}, z.core.$strict>;
|
|
89
91
|
export type ConformanceCaseExpectation = z.infer<typeof ConformanceCaseExpectation>;
|
|
90
92
|
/**
|
|
@@ -133,6 +135,8 @@ export declare const ConformanceCase: z.ZodObject<{
|
|
|
133
135
|
status: z.ZodNumber;
|
|
134
136
|
error_reason: z.ZodOptional<z.ZodString>;
|
|
135
137
|
fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
138
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
|
|
139
|
+
equivalence_group: z.ZodOptional<z.ZodString>;
|
|
136
140
|
}, z.core.$strict>;
|
|
137
141
|
note: z.ZodOptional<z.ZodString>;
|
|
138
142
|
xfail: z.ZodOptional<z.ZodObject<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conformance_case.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/conformance_case.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;EAS/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,6CAA6C;AAC7C,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;kBAgBjC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE5E,8DAA8D;AAC9D,eAAO,MAAM,0BAA0B
|
|
1
|
+
{"version":3,"file":"conformance_case.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/conformance_case.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;EAS/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,6CAA6C;AAC7C,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;kBAgBjC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE5E,8DAA8D;AAC9D,eAAO,MAAM,0BAA0B;;;;;;kBAiDrC,CAAC;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAEpF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB;;;kBAK/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAS1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC"}
|
|
@@ -96,6 +96,32 @@ export const ConformanceCaseExpectation = z.strictObject({
|
|
|
96
96
|
description: 'Specific field-value assertions on the success `result` (2xx) or the error ' +
|
|
97
97
|
'`error.data` (non-2xx). Each key must deep-equal the corresponding response field.',
|
|
98
98
|
}),
|
|
99
|
+
headers: z
|
|
100
|
+
.record(z.string(), z.string().nullable())
|
|
101
|
+
.optional()
|
|
102
|
+
.meta({
|
|
103
|
+
description: 'Per-header expectations on the response. Each key is a header name ' +
|
|
104
|
+
'(case-insensitive); a string value asserts the header is present and equals ' +
|
|
105
|
+
'it exactly, `null` asserts the header is ABSENT. Independent of this, the ' +
|
|
106
|
+
'runner enforces an unconditional no-fingerprint invariant on EVERY response ' +
|
|
107
|
+
'(`Server` / `X-Powered-By` / `WWW-Authenticate` must stay absent on both ' +
|
|
108
|
+
'spines), so a case only needs `headers` to pin a header beyond that ' +
|
|
109
|
+
'always-on floor.',
|
|
110
|
+
}),
|
|
111
|
+
equivalence_group: z
|
|
112
|
+
.string()
|
|
113
|
+
.optional()
|
|
114
|
+
.meta({
|
|
115
|
+
description: 'Tags this case as a member of an indistinguishability group. After all cases ' +
|
|
116
|
+
'run, the runner asserts every member of a group produced a BYTE-IDENTICAL ' +
|
|
117
|
+
'normalized response (`{status, body}`) — checked per impl. This promotes a ' +
|
|
118
|
+
'masked pair (found-but-unauthorized ≡ not-found, wrong-password ≡ ' +
|
|
119
|
+
'account-not-found) from "same status + reason" to "wire-indistinguishable", ' +
|
|
120
|
+
'and holds BOTH impls to it: a prober hitting either spine cannot tell the ' +
|
|
121
|
+
'members apart. A group needs >= 2 members; a member may still set `fields`. ' +
|
|
122
|
+
'The negative-space twin of the positive `output`-schema parity the runner ' +
|
|
123
|
+
'already asserts.',
|
|
124
|
+
}),
|
|
99
125
|
});
|
|
100
126
|
/**
|
|
101
127
|
* Marks a case as a deferred-by-design gap. The runner routes it through
|
|
@@ -36,6 +36,27 @@ export interface ConformanceTableOptions {
|
|
|
36
36
|
/** `describe` block label. Defaults to `'conformance table'`. */
|
|
37
37
|
readonly suite_name?: string;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Response headers that fingerprint the backend implementation or framework.
|
|
41
|
+
* Neither spine emits these; the runner asserts they stay absent on EVERY
|
|
42
|
+
* conformance response so a framework upgrade or consumer middleware that adds
|
|
43
|
+
* (say) `Server:` to one spine can't silently become a backend-identifying
|
|
44
|
+
* oracle. Lowercased — matched against the lowercased-key snapshot
|
|
45
|
+
* `headers_to_record` produces.
|
|
46
|
+
*/
|
|
47
|
+
export declare const FINGERPRINT_HEADERS: ReadonlyArray<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Assert a response carries none of the `FINGERPRINT_HEADERS`. Run on every
|
|
50
|
+
* case unconditionally — the always-on no-fingerprint floor.
|
|
51
|
+
*/
|
|
52
|
+
export declare const assert_no_fingerprint_headers: (headers: Record<string, string>, label: string) => void;
|
|
53
|
+
/**
|
|
54
|
+
* Assert each declared header expectation: a string value must be present and
|
|
55
|
+
* equal (header name matched case-insensitively), `null` must be absent. The
|
|
56
|
+
* negative-space twin for headers — `expect.headers` pins a header beyond the
|
|
57
|
+
* always-on no-fingerprint floor.
|
|
58
|
+
*/
|
|
59
|
+
export declare const assert_expected_headers: (headers: Record<string, string>, expected: Record<string, string | null>, label: string) => void;
|
|
39
60
|
/**
|
|
40
61
|
* Register a `describe` block running every `ConformanceCase` as a
|
|
41
62
|
* vitest `test` (or `xfail_until` for deferred-by-design rows). Drives
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conformance_table.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/conformance_table.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"conformance_table.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/conformance_table.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA8B9B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAMjE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAC,KAAK,eAAe,EAA4B,MAAM,uBAAuB,CAAC;AACtF,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,mBAAmB,CAAC;AAC3D,OAAO,KAAK,EAAC,SAAS,EAAc,MAAM,YAAY,CAAC;AAGvD;;;;;GAKG;AACH,MAAM,WAAW,0BAA0B;IAC1C,iEAAiE;IACjE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,sDAAsD;AACtD,MAAM,WAAW,uBAAuB;IACvC,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC/C,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,oEAAoE;IACpE,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,kEAAkE;IAClE,QAAQ,CAAC,aAAa,EAAE,uBAAuB,CAAC;IAChD,4EAA4E;IAC5E,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACjD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,0BAA0B,CAAC;IACjD,iEAAiE;IACjE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AA+FD;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAE,aAAa,CAAC,MAAM,CAIrD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,6BAA6B,GACzC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,OAAO,MAAM,KACX,IAQF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,EACvC,OAAO,MAAM,KACX,IAYF,CAAC;AAgKF;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAAI,SAAS,uBAAuB,KAAG,IAkEnF,CAAC"}
|
|
@@ -16,11 +16,17 @@ import '../assert_dev_env.js';
|
|
|
16
16
|
* row runs `as` resolves to a `TestFixture` accessor via
|
|
17
17
|
* `resolve_principal` — no inline credential minting.
|
|
18
18
|
*
|
|
19
|
+
* Every response also passes an always-on **no-fingerprint** invariant
|
|
20
|
+
* (`assert_no_fingerprint_headers` over `FINGERPRINT_HEADERS` — `Server` /
|
|
21
|
+
* `X-Powered-By` / `WWW-Authenticate` must stay absent on both spines), and
|
|
22
|
+
* a row may pin further header expectations via `expect.headers`. Headers are
|
|
23
|
+
* deliberately kept out of the equivalence-group `{status, body}` comparison.
|
|
24
|
+
*
|
|
19
25
|
* @module
|
|
20
26
|
*/
|
|
21
27
|
import { assert, describe, test } from 'vitest';
|
|
22
28
|
import { find_auth_route, rest_auth_route_suffixes, } from '../integration_helpers.js';
|
|
23
|
-
import { find_rpc_action, rpc_call, resolve_rpc_endpoints_for_setup, } from '../rpc_helpers.js';
|
|
29
|
+
import { find_rpc_action, headers_to_record, rpc_call, resolve_rpc_endpoints_for_setup, } from '../rpc_helpers.js';
|
|
24
30
|
import {} from './conformance_case.js';
|
|
25
31
|
import { xfail_until } from './xfail.js';
|
|
26
32
|
/**
|
|
@@ -91,19 +97,70 @@ const assert_fields = (actual, fields, label) => {
|
|
|
91
97
|
assert.deepEqual(record[key], expected, `${label}: field '${key}'`);
|
|
92
98
|
}
|
|
93
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Response headers that fingerprint the backend implementation or framework.
|
|
102
|
+
* Neither spine emits these; the runner asserts they stay absent on EVERY
|
|
103
|
+
* conformance response so a framework upgrade or consumer middleware that adds
|
|
104
|
+
* (say) `Server:` to one spine can't silently become a backend-identifying
|
|
105
|
+
* oracle. Lowercased — matched against the lowercased-key snapshot
|
|
106
|
+
* `headers_to_record` produces.
|
|
107
|
+
*/
|
|
108
|
+
export const FINGERPRINT_HEADERS = [
|
|
109
|
+
'server',
|
|
110
|
+
'x-powered-by',
|
|
111
|
+
'www-authenticate',
|
|
112
|
+
];
|
|
113
|
+
/**
|
|
114
|
+
* Assert a response carries none of the `FINGERPRINT_HEADERS`. Run on every
|
|
115
|
+
* case unconditionally — the always-on no-fingerprint floor.
|
|
116
|
+
*/
|
|
117
|
+
export const assert_no_fingerprint_headers = (headers, label) => {
|
|
118
|
+
for (const name of FINGERPRINT_HEADERS) {
|
|
119
|
+
assert.ok(!(name in headers), `${label}: response leaked backend-fingerprinting header '${name}: ${headers[name]}' — ` +
|
|
120
|
+
`Server / X-Powered-By / WWW-Authenticate must stay absent on both spines.`);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Assert each declared header expectation: a string value must be present and
|
|
125
|
+
* equal (header name matched case-insensitively), `null` must be absent. The
|
|
126
|
+
* negative-space twin for headers — `expect.headers` pins a header beyond the
|
|
127
|
+
* always-on no-fingerprint floor.
|
|
128
|
+
*/
|
|
129
|
+
export const assert_expected_headers = (headers, expected, label) => {
|
|
130
|
+
for (const [name, value] of Object.entries(expected)) {
|
|
131
|
+
const key = name.toLowerCase();
|
|
132
|
+
if (value === null) {
|
|
133
|
+
assert.ok(!(key in headers), `${label}: header '${name}' expected absent but present as '${headers[key]}'`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
assert.strictEqual(headers[key], value, `${label}: header '${name}'`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Run a case's header invariants: the always-on no-fingerprint floor on every
|
|
142
|
+
* response plus any declared `expect.headers`. Header assertions are kept off
|
|
143
|
+
* the equivalence-group `{status, body}` normalized response — `Set-Cookie` /
|
|
144
|
+
* `Date` legitimately vary, so headers are never compared for byte-identity.
|
|
145
|
+
*/
|
|
146
|
+
const assert_response_headers = (headers, c) => {
|
|
147
|
+
assert_no_fingerprint_headers(headers, c.name);
|
|
148
|
+
if (c.expect.headers)
|
|
149
|
+
assert_expected_headers(headers, c.expect.headers, c.name);
|
|
150
|
+
};
|
|
94
151
|
const is_success_status = (status) => status >= 200 && status < 300;
|
|
95
152
|
/**
|
|
96
153
|
* Run one conformance case end-to-end: resolve the principal, dispatch the
|
|
97
|
-
* request, and assert the expected status / reason / fields.
|
|
154
|
+
* request, and assert the expected status / reason / fields. Returns the
|
|
155
|
+
* normalized response so the caller can feed an equivalence group.
|
|
98
156
|
*/
|
|
99
157
|
const run_case = async (c, options, resolved_rpc_endpoints) => {
|
|
100
158
|
const fixture = await options.setup_test();
|
|
101
159
|
const { transport, headers, suppress_default_origin } = await resolve_principal(fixture, c.request.as, options.principals);
|
|
102
160
|
if (c.request.method.startsWith('/')) {
|
|
103
|
-
|
|
104
|
-
return;
|
|
161
|
+
return run_rest_case(c, options, transport, headers);
|
|
105
162
|
}
|
|
106
|
-
|
|
163
|
+
return run_rpc_case(c, transport, headers, suppress_default_origin, resolved_rpc_endpoints);
|
|
107
164
|
};
|
|
108
165
|
/** Dispatch + assert a case targeting an RPC method. */
|
|
109
166
|
const run_rpc_case = async (c, transport, headers, suppress_default_origin, resolved_rpc_endpoints) => {
|
|
@@ -121,6 +178,7 @@ const run_rpc_case = async (c, transport, headers, suppress_default_origin, reso
|
|
|
121
178
|
...(c.request.verb && { verb: c.request.verb }),
|
|
122
179
|
...(suppress_default_origin && { suppress_default_origin: true }),
|
|
123
180
|
});
|
|
181
|
+
assert_response_headers(res.headers, c);
|
|
124
182
|
if (is_success_status(c.expect.status)) {
|
|
125
183
|
assert.ok(res.ok, `${c.name}: expected success (${c.expect.status}) but got error ${JSON.stringify(res.ok ? undefined : res.error)}`);
|
|
126
184
|
assert.strictEqual(res.status, c.expect.status, `${c.name}: status`);
|
|
@@ -128,7 +186,7 @@ const run_rpc_case = async (c, transport, headers, suppress_default_origin, reso
|
|
|
128
186
|
assert.ok(parsed.success, `${c.name}: result does not match spec.output: ${JSON.stringify(parsed.success ? undefined : parsed.error.issues)}`);
|
|
129
187
|
if (c.expect.fields)
|
|
130
188
|
assert_fields(res.result, c.expect.fields, c.name);
|
|
131
|
-
return;
|
|
189
|
+
return { status: res.status, body: res.result };
|
|
132
190
|
}
|
|
133
191
|
assert.ok(!res.ok, `${c.name}: expected error status ${c.expect.status} but got success`);
|
|
134
192
|
assert.strictEqual(res.status, c.expect.status, `${c.name}: error status`);
|
|
@@ -145,6 +203,7 @@ const run_rpc_case = async (c, transport, headers, suppress_default_origin, reso
|
|
|
145
203
|
}
|
|
146
204
|
if (c.expect.fields)
|
|
147
205
|
assert_fields(res.error.data, c.expect.fields, c.name);
|
|
206
|
+
return { status: res.status, body: res.error };
|
|
148
207
|
};
|
|
149
208
|
/** Dispatch + assert a case targeting one of the 6 REST auth routes. */
|
|
150
209
|
const run_rest_case = async (c, options, transport, headers) => {
|
|
@@ -165,6 +224,7 @@ const run_rest_case = async (c, options, transport, headers) => {
|
|
|
165
224
|
c.request.params !== undefined && { body: JSON.stringify(c.request.params) }),
|
|
166
225
|
};
|
|
167
226
|
const response = await transport(route.path, init);
|
|
227
|
+
assert_response_headers(headers_to_record(response.headers), c);
|
|
168
228
|
assert.strictEqual(response.status, c.expect.status, `${c.name}: status`);
|
|
169
229
|
const body = await response.json().catch(() => undefined);
|
|
170
230
|
if (is_success_status(c.expect.status)) {
|
|
@@ -177,6 +237,7 @@ const run_rest_case = async (c, options, transport, headers) => {
|
|
|
177
237
|
}
|
|
178
238
|
if (c.expect.fields)
|
|
179
239
|
assert_fields(body, c.expect.fields, c.name);
|
|
240
|
+
return { status: response.status, body };
|
|
180
241
|
};
|
|
181
242
|
/**
|
|
182
243
|
* Register a `describe` block running every `ConformanceCase` as a
|
|
@@ -186,10 +247,28 @@ const run_rest_case = async (c, options, transport, headers) => {
|
|
|
186
247
|
*/
|
|
187
248
|
export const describe_conformance_table_tests = (options) => {
|
|
188
249
|
const resolved_rpc_endpoints = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
|
|
250
|
+
// Per-group normalized responses, populated as the member case tests run
|
|
251
|
+
// and consulted by the per-group byte-identity assertions registered after
|
|
252
|
+
// the case loop — vitest runs a describe's tests in registration order, so
|
|
253
|
+
// the group assertions execute last, after every member has recorded.
|
|
254
|
+
const equivalence_groups = new Map();
|
|
255
|
+
const group_names = new Set();
|
|
256
|
+
for (const c of options.cases) {
|
|
257
|
+
if (c.expect.equivalence_group)
|
|
258
|
+
group_names.add(c.expect.equivalence_group);
|
|
259
|
+
}
|
|
189
260
|
describe(options.suite_name ?? 'conformance table', () => {
|
|
190
261
|
for (const c of options.cases) {
|
|
191
262
|
const label = c.note ? `${c.name} — ${c.note}` : c.name;
|
|
192
|
-
const
|
|
263
|
+
const group = c.expect.equivalence_group;
|
|
264
|
+
const body = async () => {
|
|
265
|
+
const normalized = await run_case(c, options, resolved_rpc_endpoints);
|
|
266
|
+
if (group) {
|
|
267
|
+
const members = equivalence_groups.get(group) ?? [];
|
|
268
|
+
members.push({ name: c.name, normalized });
|
|
269
|
+
equivalence_groups.set(group, members);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
193
272
|
if (c.xfail) {
|
|
194
273
|
xfail_until(c.xfail.tracking_id, c.xfail.reason, label, body);
|
|
195
274
|
}
|
|
@@ -197,5 +276,28 @@ export const describe_conformance_table_tests = (options) => {
|
|
|
197
276
|
test(label, body);
|
|
198
277
|
}
|
|
199
278
|
}
|
|
279
|
+
// Negative-space gate: every member of an equivalence group must have
|
|
280
|
+
// produced a byte-identical `{status, body}`. This holds the impl under
|
|
281
|
+
// test to "a prober cannot distinguish these masked paths" — the
|
|
282
|
+
// promotion of a masked pair (wrong-password ≡ account-not-found,
|
|
283
|
+
// found-but-unauthorized ≡ not-found) from "same status + reason" to
|
|
284
|
+
// "wire-indistinguishable". Runs per impl, so each spine is held to it.
|
|
285
|
+
for (const group of group_names) {
|
|
286
|
+
test(`equivalence group '${group}' — members are wire-indistinguishable`, () => {
|
|
287
|
+
const members = equivalence_groups.get(group) ?? [];
|
|
288
|
+
assert.ok(members.length >= 2, `equivalence group '${group}': expected >= 2 members to have recorded, got ` +
|
|
289
|
+
`${members.length} — a group pins indistinguishability between paths, so it ` +
|
|
290
|
+
`needs at least a pair (check the member cases ran and weren't all xfail).`);
|
|
291
|
+
const [first, ...rest] = members;
|
|
292
|
+
// `members.length >= 2` guarantees `first` is defined; the explicit
|
|
293
|
+
// assert narrows it for the typechecker (noUncheckedIndexedAccess).
|
|
294
|
+
assert.ok(first);
|
|
295
|
+
for (const member of rest) {
|
|
296
|
+
assert.deepStrictEqual(member.normalized, first.normalized, `equivalence group '${group}': '${member.name}' is distinguishable from ` +
|
|
297
|
+
`'${first.name}' — a prober could tell these masked paths apart. The ` +
|
|
298
|
+
`normalized {status, body} must be byte-identical across all members.`);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
200
302
|
});
|
|
201
303
|
};
|