@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
|
@@ -18,11 +18,14 @@ import { test, assert, describe } from 'vitest';
|
|
|
18
18
|
import { assert_surface_invariants, assert_rpc_ws_surface_invariants, assert_surface_security_policy, audit_error_schema_tightness, assert_error_schema_tightness, default_error_schema_tightness, fuz_app_stock_route_tightness_allowlist } from './surface_invariants.js';
|
|
19
19
|
import { describe_adversarial_input } from './adversarial_input.js';
|
|
20
20
|
import { describe_adversarial_404 } from './adversarial_404.js';
|
|
21
|
-
import {
|
|
21
|
+
import { create_auth_test_apps, create_route_skip_filter, select_auth_app } from './auth_apps.js';
|
|
22
|
+
import { resolve_valid_path } from './schema_generators.js';
|
|
22
23
|
import { assert_surface_matches_snapshot, assert_surface_deterministic, assert_only_expected_public_routes, assert_full_middleware_stack, assert_error_schema_valid } from './assertions.js';
|
|
23
24
|
import { merge_error_schemas } from '../http/schema_helpers.js';
|
|
24
25
|
import { collect_middleware_errors } from '../http/surface.js';
|
|
25
|
-
import { filter_protected_routes, filter_role_routes,
|
|
26
|
+
import { filter_protected_routes, filter_role_routes, filter_credential_gated_routes } from '../http/surface_query.js';
|
|
27
|
+
import { CREDENTIAL_TYPES } from '../hono_context.js';
|
|
28
|
+
import { CREDENTIAL_TYPE_SESSION } from '../auth/credential_type_schema.js';
|
|
26
29
|
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, ERROR_CREDENTIAL_TYPE_REQUIRED } from '../http/error_schemas.js';
|
|
27
30
|
// --- Adversarial test runner ---
|
|
28
31
|
/**
|
|
@@ -45,31 +48,54 @@ const build_error_schema_lookup = (route_specs, middleware_specs) => {
|
|
|
45
48
|
}
|
|
46
49
|
return lookup;
|
|
47
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* Build a lookup from `"METHOD /path"` to the route's own `RouteSpec` — the
|
|
53
|
+
* companion of `build_error_schema_lookup`, so a generated request can reach
|
|
54
|
+
* the route's `params` schema and synthesize path segments the route will
|
|
55
|
+
* actually accept.
|
|
56
|
+
*/
|
|
57
|
+
const build_route_spec_lookup = (route_specs) => {
|
|
58
|
+
const lookup = new Map();
|
|
59
|
+
for (const spec of route_specs) {
|
|
60
|
+
lookup.set(`${spec.method} ${spec.path}`, spec);
|
|
61
|
+
}
|
|
62
|
+
return lookup;
|
|
63
|
+
};
|
|
48
64
|
/**
|
|
49
65
|
* Generate adversarial HTTP auth enforcement test suites.
|
|
50
66
|
*
|
|
51
67
|
* Describe blocks:
|
|
52
68
|
* - unauthenticated → 401 — every protected route
|
|
53
|
-
* - wrong role → 403 — every role route
|
|
54
|
-
*
|
|
69
|
+
* - wrong role → 403 — every role route the role apps' session credential
|
|
70
|
+
* reaches, tested with all non-matching roles
|
|
71
|
+
* - authenticated without role → 403 — the same routes, no-role context
|
|
72
|
+
* - credential-gated routes reject a disallowed credential → 403 — every
|
|
73
|
+
* route declaring `auth.credential_types`, probed on a channel it refuses
|
|
55
74
|
* - correct auth passes guard — every protected route, assert not 401/403
|
|
56
75
|
*/
|
|
57
76
|
export const describe_adversarial_auth = (options) => {
|
|
58
77
|
const { build, roles } = options;
|
|
59
78
|
const { surface, route_specs, middleware_specs } = build();
|
|
60
|
-
const
|
|
79
|
+
const is_skipped = create_route_skip_filter(options.skip_routes);
|
|
80
|
+
const protected_routes = filter_protected_routes(surface).filter((r) => !is_skipped(r));
|
|
61
81
|
if (protected_routes.length === 0)
|
|
62
82
|
return;
|
|
63
|
-
const role_routes = filter_role_routes(surface);
|
|
64
|
-
const
|
|
83
|
+
const role_routes = filter_role_routes(surface).filter((r) => !is_skipped(r));
|
|
84
|
+
const credential_gated_routes = filter_credential_gated_routes(surface).filter((r) => !is_skipped(r));
|
|
65
85
|
// merged error schemas (auto-derived + middleware + handler-specific) for response validation
|
|
66
86
|
const error_schema_lookup = build_error_schema_lookup(route_specs, middleware_specs);
|
|
87
|
+
// The route's own spec, so a path param with a declared format (a
|
|
88
|
+
// `blake3:…` fact hash, a `tok_…` id) is synthesized into a value params
|
|
89
|
+
// validation accepts. Otherwise the generated request 400s on the path
|
|
90
|
+
// before ever reaching the 401 / 403 gate the case is asserting.
|
|
91
|
+
const spec_lookup = build_route_spec_lookup(route_specs);
|
|
92
|
+
const test_path = (route) => resolve_valid_path(route.path, spec_lookup.get(`${route.method} ${route.path}`)?.params);
|
|
67
93
|
const apps = create_auth_test_apps(route_specs, roles);
|
|
68
94
|
describe('adversarial HTTP auth enforcement', () => {
|
|
69
95
|
describe('unauthenticated → 401', () => {
|
|
70
96
|
for (const route of protected_routes) {
|
|
71
97
|
test(`${route.method} ${route.path}`, async () => {
|
|
72
|
-
const res = await apps.public.request(
|
|
98
|
+
const res = await apps.public.request(test_path(route), {
|
|
73
99
|
method: route.method
|
|
74
100
|
});
|
|
75
101
|
assert.strictEqual(res.status, 401, `${route.method} ${route.path}`);
|
|
@@ -79,10 +105,17 @@ export const describe_adversarial_auth = (options) => {
|
|
|
79
105
|
});
|
|
80
106
|
}
|
|
81
107
|
});
|
|
82
|
-
// Role
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
108
|
+
// Role routes the role apps can actually reach. Those apps carry a
|
|
109
|
+
// session credential, so a route gated to another channel (keeper) is
|
|
110
|
+
// refused by the pre-authorization credential gate before its role gate
|
|
111
|
+
// ever runs — probing it here would assert `insufficient_permissions`
|
|
112
|
+
// and see `credential_type_required`. A route gated *to* the session
|
|
113
|
+
// channel (the audit SSE stream) stays in: its role gate is reachable,
|
|
114
|
+
// and dropping it would silently retire this coverage the day a route
|
|
115
|
+
// gained a session gate. Either way the credential block below probes
|
|
116
|
+
// the gate itself.
|
|
117
|
+
const role_only_routes = role_routes.filter((r) => !r.auth.credential_types?.length ||
|
|
118
|
+
r.auth.credential_types.includes(CREDENTIAL_TYPE_SESSION));
|
|
86
119
|
if (role_only_routes.length > 0) {
|
|
87
120
|
describe('wrong role → 403', () => {
|
|
88
121
|
for (const route of role_only_routes) {
|
|
@@ -93,7 +126,7 @@ export const describe_adversarial_auth = (options) => {
|
|
|
93
126
|
const app = apps.by_role.get(wrong_role);
|
|
94
127
|
if (!app)
|
|
95
128
|
throw new Error(`No test app for role '${wrong_role}'`);
|
|
96
|
-
const res = await app.request(
|
|
129
|
+
const res = await app.request(test_path(route), {
|
|
97
130
|
method: route.method
|
|
98
131
|
});
|
|
99
132
|
assert.strictEqual(res.status, 403, `${route.method} ${route.path}`);
|
|
@@ -108,7 +141,7 @@ export const describe_adversarial_auth = (options) => {
|
|
|
108
141
|
describe('authenticated without role → 403', () => {
|
|
109
142
|
for (const route of role_only_routes) {
|
|
110
143
|
test(`${route.method} ${route.path}`, async () => {
|
|
111
|
-
const res = await apps.authed.request(
|
|
144
|
+
const res = await apps.authed.request(test_path(route), {
|
|
112
145
|
method: route.method
|
|
113
146
|
});
|
|
114
147
|
assert.strictEqual(res.status, 403, `${route.method} ${route.path}`);
|
|
@@ -120,19 +153,35 @@ export const describe_adversarial_auth = (options) => {
|
|
|
120
153
|
}
|
|
121
154
|
});
|
|
122
155
|
}
|
|
123
|
-
if (
|
|
124
|
-
describe('
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
for
|
|
128
|
-
|
|
129
|
-
|
|
156
|
+
if (credential_gated_routes.length > 0) {
|
|
157
|
+
describe('credential-gated routes reject a disallowed credential → 403', () => {
|
|
158
|
+
// Each route is probed on the first channel its own allowlist
|
|
159
|
+
// omits, in `CREDENTIAL_TYPES` order: a session for keeper's
|
|
160
|
+
// `daemon_token` gate, an api token for the session-only routes —
|
|
161
|
+
// which is the bearer channel those gates exist to refuse. The
|
|
162
|
+
// channel apps hold the keeper role, so no route's role gate can
|
|
163
|
+
// be what answers; it could not be anyway, the credential gate
|
|
164
|
+
// being pre-authorization.
|
|
165
|
+
for (const route of credential_gated_routes) {
|
|
166
|
+
const admitted = route.auth.credential_types ?? [];
|
|
167
|
+
const refused = CREDENTIAL_TYPES.find((t) => !admitted.includes(t));
|
|
168
|
+
if (!refused) {
|
|
169
|
+
// A gate listing every builtin channel refuses none of them, so
|
|
170
|
+
// there is no probe to make. Reported as a skip rather than
|
|
171
|
+
// dropped silently: a route that reads as gated but is not is
|
|
172
|
+
// worth seeing in the run, and a `continue` here would hide
|
|
173
|
+
// exactly the shape this block exists to surface.
|
|
174
|
+
test.skip(`${route.method} ${route.path} (gate admits every builtin channel — nothing to refuse)`, () => { });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
test(`${route.method} ${route.path} (${refused})`, async () => {
|
|
178
|
+
const res = await apps.by_credential_type.get(refused).request(test_path(route), {
|
|
130
179
|
method: route.method
|
|
131
180
|
});
|
|
132
181
|
assert.strictEqual(res.status, 403, `${route.method} ${route.path}`);
|
|
133
182
|
const body = await res.json();
|
|
134
183
|
assert.strictEqual(body.error, ERROR_CREDENTIAL_TYPE_REQUIRED);
|
|
135
|
-
assert.deepStrictEqual(body.required_credential_types,
|
|
184
|
+
assert.deepStrictEqual(body.required_credential_types, admitted);
|
|
136
185
|
assert_error_schema_valid(error_schema_lookup, route, 403, body);
|
|
137
186
|
});
|
|
138
187
|
}
|
|
@@ -141,7 +190,9 @@ export const describe_adversarial_auth = (options) => {
|
|
|
141
190
|
describe('correct auth passes guard', () => {
|
|
142
191
|
for (const route of protected_routes) {
|
|
143
192
|
test(`${route.method} ${route.path}`, async () => {
|
|
144
|
-
const res = await select_auth_app(apps, route.auth).request(
|
|
193
|
+
const res = await select_auth_app(apps, route.auth).request(test_path(route), {
|
|
194
|
+
method: route.method
|
|
195
|
+
});
|
|
145
196
|
// handler may error (500) or return 404 (stub deps) — that's fine, we only verify auth passed
|
|
146
197
|
assert.notStrictEqual(res.status, 401, 'should not be 401 (auth rejected)');
|
|
147
198
|
assert.notStrictEqual(res.status, 403, 'should not be 403 (role rejected)');
|
|
@@ -210,7 +261,7 @@ export const resolve_standard_error_schema_tightness = (consumer) => {
|
|
|
210
261
|
* any project-specific assertions in additional `describe` blocks.
|
|
211
262
|
*/
|
|
212
263
|
export const describe_standard_attack_surface_tests = (options) => {
|
|
213
|
-
const { build, snapshot_path, expected_public_routes, expected_api_middleware, roles, api_path_prefix = '/api/', security_policy } = options;
|
|
264
|
+
const { build, snapshot_path, expected_public_routes, expected_api_middleware, roles, api_path_prefix = '/api/', security_policy, skip_routes } = options;
|
|
214
265
|
const error_schema_tightness = resolve_standard_error_schema_tightness(options.error_schema_tightness);
|
|
215
266
|
const built = build();
|
|
216
267
|
const { surface } = built;
|
|
@@ -254,7 +305,7 @@ export const describe_standard_attack_surface_tests = (options) => {
|
|
|
254
305
|
}
|
|
255
306
|
});
|
|
256
307
|
});
|
|
257
|
-
describe_adversarial_auth({ build: () => built, roles });
|
|
258
|
-
describe_adversarial_input({ build: () => built, roles });
|
|
259
|
-
describe_adversarial_404({ build: () => built, roles });
|
|
308
|
+
describe_adversarial_auth({ build: () => built, roles, skip_routes });
|
|
309
|
+
describe_adversarial_input({ build: () => built, roles, skip_routes });
|
|
310
|
+
describe_adversarial_404({ build: () => built, roles, skip_routes });
|
|
260
311
|
};
|
|
@@ -2,7 +2,6 @@ import './assert_dev_env.js';
|
|
|
2
2
|
import type { SessionOptions } from '../auth/session_cookie.js';
|
|
3
3
|
import { type RpcEndpointsSuiteOption } from './rpc_helpers.js';
|
|
4
4
|
import type { AppSurfaceSpec } from '../http/surface.js';
|
|
5
|
-
import type { BackendCapabilities } from './cross_backend/capabilities.js';
|
|
6
5
|
import type { SetupTest } from './cross_backend/setup.js';
|
|
7
6
|
/**
|
|
8
7
|
* Configuration for `describe_audit_completeness_tests`.
|
|
@@ -23,8 +22,6 @@ export interface AuditCompletenessTestOptions {
|
|
|
23
22
|
* options uniformity.
|
|
24
23
|
*/
|
|
25
24
|
surface_source: AppSurfaceSpec;
|
|
26
|
-
/** Backend capability declarations. */
|
|
27
|
-
capabilities: BackendCapabilities;
|
|
28
25
|
/** Session config — needed for factory-form rpc_endpoints resolution. */
|
|
29
26
|
session_options: SessionOptions<string>;
|
|
30
27
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit_completeness.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_completeness.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AA4B7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAUhE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AA2B1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"audit_completeness.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_completeness.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AA4B7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAUhE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AA2B1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,0BAA0B,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC5C;;;;;OAKG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;;;OAMG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,yEAAyE;IACzE,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;OAGG;IACH,aAAa,EAAE,uBAAuB,CAAC;CACvC;AAgGD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iCAAiC,GAAI,SAAS,4BAA4B,KAAG,IAgnBzF,CAAC"}
|
|
@@ -111,7 +111,6 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
111
111
|
// confusing test failure when `rpc_endpoints` is missing.
|
|
112
112
|
const rpc_endpoints_for_setup = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
|
|
113
113
|
const rpc_path = require_rpc_endpoint_path(rpc_endpoints_for_setup);
|
|
114
|
-
void options.capabilities;
|
|
115
114
|
describe('audit_log_completeness', () => {
|
|
116
115
|
// --- Account routes ---
|
|
117
116
|
describe('account mutation audit events', () => {
|
|
@@ -29,6 +29,16 @@ export interface AuthTestApps {
|
|
|
29
29
|
public: Hono;
|
|
30
30
|
authed: Hono;
|
|
31
31
|
keeper: Hono;
|
|
32
|
+
/**
|
|
33
|
+
* One authenticated app per builtin credential type, each holding the
|
|
34
|
+
* keeper role — the channel axis, where `by_role` is the authority axis.
|
|
35
|
+
*
|
|
36
|
+
* A credential gate is pre-authorization, so probing it needs an app on a
|
|
37
|
+
* channel the route refuses, and proving a gated route still works needs
|
|
38
|
+
* one on a channel it admits. `keeper` is the `'daemon_token'` entry of
|
|
39
|
+
* this map, not a second app.
|
|
40
|
+
*/
|
|
41
|
+
by_credential_type: Map<CredentialType, Hono>;
|
|
32
42
|
by_role: Map<string, Hono>;
|
|
33
43
|
}
|
|
34
44
|
/**
|
|
@@ -41,11 +51,33 @@ export declare const create_auth_test_apps: (route_specs: Array<RouteSpec>, role
|
|
|
41
51
|
/**
|
|
42
52
|
* Select the Hono test app with correct auth for a route.
|
|
43
53
|
*
|
|
54
|
+
* The credential channel is decided first, because the channel gate is
|
|
55
|
+
* pre-authorization: an app whose credential the route refuses never reaches
|
|
56
|
+
* the role gate, so a role-correct app on the wrong channel answers
|
|
57
|
+
* `credential_type_required`, and a "correct auth passes guard" case reads it
|
|
58
|
+
* as a failure of the role gate that never ran.
|
|
59
|
+
*
|
|
60
|
+
* A gate admitting `'session'` falls through to the `by_role` /
|
|
61
|
+
* `authed` apps, which carry that channel. Any other gate leaves the
|
|
62
|
+
* `by_credential_type` app as the only candidate — those hold the keeper
|
|
63
|
+
* role, which is why a route pairing a non-session channel with some *other*
|
|
64
|
+
* role has no app and says so.
|
|
65
|
+
*
|
|
44
66
|
* @throws Error if `auth.roles` names a role not present in `apps.by_role` —
|
|
45
67
|
* surfaces a missing entry in the `roles` array passed to
|
|
46
68
|
* `create_auth_test_apps`.
|
|
69
|
+
* @throws Error if the route pairs a non-session channel with a role the
|
|
70
|
+
* channel apps don't hold. Silently handing back a session app would turn
|
|
71
|
+
* every case on the route into a misattributed 403.
|
|
47
72
|
*/
|
|
48
73
|
export declare const select_auth_app: (apps: AuthTestApps, auth: RouteAuth) => Hono;
|
|
49
|
-
/**
|
|
50
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Build the skip predicate an adversarial suite applies from its
|
|
76
|
+
* `skip_routes` option — routes named in `'METHOD /path'` form, the surface
|
|
77
|
+
* key. `undefined` skips nothing.
|
|
78
|
+
*/
|
|
79
|
+
export declare const create_route_skip_filter: (skip_routes: Array<string> | undefined) => ((route: {
|
|
80
|
+
method: string;
|
|
81
|
+
path: string;
|
|
82
|
+
}) => boolean);
|
|
51
83
|
//# sourceMappingURL=auth_apps.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth_apps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/auth_apps.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;GAOG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"auth_apps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/auth_apps.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;GAOG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAOvE,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAKN,KAAK,cAAc,EACnB,MAAM,oBAAoB,CAAC;AAI5B;;GAEG;AACH,eAAO,MAAM,2BAA2B,GAAI,OAAO,MAAM,KAAG,cAI1D,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,GACtC,aAAa,KAAK,CAAC,SAAS,CAAC,EAC7B,WAAW,cAAc,EACzB,kBAAkB,cAAc,KAC9B,IAuBF,CAAC;AAEF,sFAAsF;AACtF,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,IAAI,CAAC;IACb;;;;;;;;OAQG;IACH,kBAAkB,EAAE,GAAG,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;IAC9C,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;CAC3B;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GACjC,aAAa,KAAK,CAAC,SAAS,CAAC,EAC7B,OAAO,KAAK,CAAC,MAAM,CAAC,KAClB,YAsBF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,YAAY,EAAE,MAAM,SAAS,KAAG,IAwBrE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GACpC,aAAa,KAAK,CAAC,MAAM,CAAC,GAAG,SAAS,KACpC,CAAC,CAAC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KAAK,OAAO,CAGvD,CAAC"}
|
|
@@ -11,9 +11,11 @@ import { Hono } from 'hono';
|
|
|
11
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
|
+
import { CREDENTIAL_TYPE_DAEMON_TOKEN, CREDENTIAL_TYPE_SESSION } from '../auth/credential_type_schema.js';
|
|
15
|
+
import { ROLE_KEEPER } from '../auth/role_schema.js';
|
|
14
16
|
import { fuz_auth_guard_resolver } from '../auth/auth_guard_resolver.js';
|
|
15
17
|
import { REQUEST_CONTEXT_KEY, create_fuz_authorization_handler } from '../auth/request_context.js';
|
|
16
|
-
import { ACCOUNT_ID_KEY, CREDENTIAL_TYPE_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
|
|
18
|
+
import { ACCOUNT_ID_KEY, CREDENTIAL_TYPES, CREDENTIAL_TYPE_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
|
|
17
19
|
import { create_stub_db } from './stubs.js';
|
|
18
20
|
import { create_test_account, create_test_actor, create_test_role_grant } from './entities.js';
|
|
19
21
|
/**
|
|
@@ -40,7 +42,7 @@ export const create_test_app_from_specs = (route_specs, auth_ctx, credential_typ
|
|
|
40
42
|
if (auth_ctx) {
|
|
41
43
|
c.set(ACCOUNT_ID_KEY, auth_ctx.account.id);
|
|
42
44
|
c.set(REQUEST_CONTEXT_KEY, auth_ctx);
|
|
43
|
-
c.set(CREDENTIAL_TYPE_KEY, credential_type ??
|
|
45
|
+
c.set(CREDENTIAL_TYPE_KEY, credential_type ?? CREDENTIAL_TYPE_SESSION);
|
|
44
46
|
c.set(TEST_CONTEXT_PRESET_KEY, true);
|
|
45
47
|
}
|
|
46
48
|
await next();
|
|
@@ -59,25 +61,52 @@ export const create_auth_test_apps = (route_specs, roles) => {
|
|
|
59
61
|
for (const role of roles) {
|
|
60
62
|
by_role.set(role, create_test_app_from_specs(route_specs, create_test_request_context(role)));
|
|
61
63
|
}
|
|
64
|
+
const by_credential_type = new Map(CREDENTIAL_TYPES.map((credential_type) => [
|
|
65
|
+
credential_type,
|
|
66
|
+
create_test_app_from_specs(route_specs, create_test_request_context(ROLE_KEEPER), credential_type)
|
|
67
|
+
]));
|
|
62
68
|
return {
|
|
63
69
|
public: create_test_app_from_specs(route_specs),
|
|
64
70
|
authed: create_test_app_from_specs(route_specs, create_test_request_context()),
|
|
65
|
-
keeper:
|
|
71
|
+
keeper: by_credential_type.get(CREDENTIAL_TYPE_DAEMON_TOKEN),
|
|
72
|
+
by_credential_type,
|
|
66
73
|
by_role
|
|
67
74
|
};
|
|
68
75
|
};
|
|
69
76
|
/**
|
|
70
77
|
* Select the Hono test app with correct auth for a route.
|
|
71
78
|
*
|
|
79
|
+
* The credential channel is decided first, because the channel gate is
|
|
80
|
+
* pre-authorization: an app whose credential the route refuses never reaches
|
|
81
|
+
* the role gate, so a role-correct app on the wrong channel answers
|
|
82
|
+
* `credential_type_required`, and a "correct auth passes guard" case reads it
|
|
83
|
+
* as a failure of the role gate that never ran.
|
|
84
|
+
*
|
|
85
|
+
* A gate admitting `'session'` falls through to the `by_role` /
|
|
86
|
+
* `authed` apps, which carry that channel. Any other gate leaves the
|
|
87
|
+
* `by_credential_type` app as the only candidate — those hold the keeper
|
|
88
|
+
* role, which is why a route pairing a non-session channel with some *other*
|
|
89
|
+
* role has no app and says so.
|
|
90
|
+
*
|
|
72
91
|
* @throws Error if `auth.roles` names a role not present in `apps.by_role` —
|
|
73
92
|
* surfaces a missing entry in the `roles` array passed to
|
|
74
93
|
* `create_auth_test_apps`.
|
|
94
|
+
* @throws Error if the route pairs a non-session channel with a role the
|
|
95
|
+
* channel apps don't hold. Silently handing back a session app would turn
|
|
96
|
+
* every case on the route into a misattributed 403.
|
|
75
97
|
*/
|
|
76
98
|
export const select_auth_app = (apps, auth) => {
|
|
77
99
|
if (is_public_auth(auth))
|
|
78
100
|
return apps.public;
|
|
79
|
-
|
|
80
|
-
|
|
101
|
+
const credential_types = auth.credential_types;
|
|
102
|
+
if (credential_types?.length && !credential_types.includes(CREDENTIAL_TYPE_SESSION)) {
|
|
103
|
+
const channel = CREDENTIAL_TYPES.find((t) => credential_types.includes(t));
|
|
104
|
+
const app = channel === undefined ? undefined : apps.by_credential_type.get(channel);
|
|
105
|
+
const roles = auth.roles;
|
|
106
|
+
if (app && (!roles?.length || roles.includes(ROLE_KEEPER)))
|
|
107
|
+
return app;
|
|
108
|
+
throw new Error(`No test app for credential types '${credential_types.join("', '")}'${roles?.length ? ` with roles '${roles.join("', '")}'` : ''} — the channel apps hold the '${ROLE_KEEPER}' role, and only the session channel has per-role apps`);
|
|
109
|
+
}
|
|
81
110
|
if (auth.roles?.length) {
|
|
82
111
|
// Multi-role disjunction: any of the named roles admits the caller.
|
|
83
112
|
// Tests pick the first role's app; consumers wanting per-role coverage
|
|
@@ -90,5 +119,12 @@ export const select_auth_app = (apps, auth) => {
|
|
|
90
119
|
}
|
|
91
120
|
return apps.authed;
|
|
92
121
|
};
|
|
93
|
-
/**
|
|
94
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Build the skip predicate an adversarial suite applies from its
|
|
124
|
+
* `skip_routes` option — routes named in `'METHOD /path'` form, the surface
|
|
125
|
+
* key. `undefined` skips nothing.
|
|
126
|
+
*/
|
|
127
|
+
export const create_route_skip_filter = (skip_routes) => {
|
|
128
|
+
const skip_set = new Set(skip_routes);
|
|
129
|
+
return (route) => skip_set.has(`${route.method} ${route.path}`);
|
|
130
|
+
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import '../assert_dev_env.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { RpcPathCapabilityGatedCrossSuiteOptions } from './setup.js';
|
|
3
3
|
/**
|
|
4
4
|
* Options for the account-lifecycle parity suite. The standard
|
|
5
|
-
* RPC-dispatched cross-suite shape (`setup_test` /
|
|
6
|
-
* `rpc_path`); aliases
|
|
5
|
+
* capability-gating RPC-dispatched cross-suite shape (`setup_test` /
|
|
6
|
+
* `capabilities` / `rpc_path`); aliases
|
|
7
|
+
* `RpcPathCapabilityGatedCrossSuiteOptions` rather than duplicating.
|
|
7
8
|
*/
|
|
8
|
-
export type AccountLifecycleCrossTestOptions =
|
|
9
|
+
export type AccountLifecycleCrossTestOptions = RpcPathCapabilityGatedCrossSuiteOptions;
|
|
9
10
|
export declare const describe_account_lifecycle_cross_tests: (options: AccountLifecycleCrossTestOptions) => void;
|
|
10
11
|
//# sourceMappingURL=account_lifecycle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_lifecycle.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/account_lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"account_lifecycle.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/account_lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsC9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E;;;;;GAKG;AACH,MAAM,MAAM,gCAAgC,GAAG,uCAAuC,CAAC;AAEvF,eAAO,MAAM,sCAAsC,GAClD,SAAS,gCAAgC,KACvC,IAqUF,CAAC"}
|
|
@@ -22,6 +22,7 @@ import { AccountDeleteOutput, AccountUndeleteOutput, AccountPurgeOutput, AdminAc
|
|
|
22
22
|
import { ERROR_ACCOUNT_NOT_FOUND, ERROR_AUTHENTICATION_REQUIRED } from '../../http/error_schemas.js';
|
|
23
23
|
import { test_if } from './capabilities.js';
|
|
24
24
|
import { cross_rpc_call, error_reason, expect_output } from './cell_cross_helpers.js';
|
|
25
|
+
import { assert_iso8601_seconds } from './wire_shapes.js';
|
|
25
26
|
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
26
27
|
export const describe_account_lifecycle_cross_tests = (options) => {
|
|
27
28
|
const { setup_test, capabilities } = options;
|
|
@@ -99,6 +100,9 @@ export const describe_account_lifecycle_cross_tests = (options) => {
|
|
|
99
100
|
const failure = listed.events.find((e) => e.outcome === 'failure' &&
|
|
100
101
|
e.metadata?.reason === ERROR_CANNOT_DELETE_KEEPER);
|
|
101
102
|
assert.ok(failure, 'keeper-removal guard must emit an account_delete outcome=failure audit row with reason cannot_delete_keeper');
|
|
103
|
+
// Byte-shape gate on the audit row's timestamp — `z.string()` in
|
|
104
|
+
// `AuditLogEventWithUsernamesJson` accepts a millisecond stamp too.
|
|
105
|
+
assert_iso8601_seconds(failure.created_at, 'audit_log_list.created_at');
|
|
102
106
|
});
|
|
103
107
|
test_if(capabilities.account_lifecycle, 'deterministic: double-undelete → second call is not_found', async () => {
|
|
104
108
|
const fixture = await setup_test();
|
|
@@ -139,6 +143,9 @@ export const describe_account_lifecycle_cross_tests = (options) => {
|
|
|
139
143
|
const row = with_deleted.accounts.find((a) => a.account.id === victim.account.id);
|
|
140
144
|
assert.ok(row, 'include_deleted surfaces the tombstoned row');
|
|
141
145
|
assert.ok(row.account.deleted_at !== null, 'tombstoned row carries a non-null deleted_at on both spines');
|
|
146
|
+
assert_iso8601_seconds(row.account.created_at, 'admin_account_list.created_at');
|
|
147
|
+
assert_iso8601_seconds(row.account.updated_at, 'admin_account_list.updated_at');
|
|
148
|
+
assert_iso8601_seconds(row.account.deleted_at, 'admin_account_list.deleted_at');
|
|
142
149
|
});
|
|
143
150
|
});
|
|
144
151
|
};
|
|
@@ -2,7 +2,7 @@ import '../assert_dev_env.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Cross-impl RPC **action-manifest** introspection — a normalized,
|
|
4
4
|
* JSON-serializable dump of a backend's live RPC method set, one entry per
|
|
5
|
-
* method carrying its auth shape
|
|
5
|
+
* method carrying its auth shape, side-effect flag, and rate-limit class.
|
|
6
6
|
*
|
|
7
7
|
* The sibling of `schema_introspect.ts`'s `SchemaSnapshot`: where that
|
|
8
8
|
* captures the live *database* shape for the schema-parity gate, this
|
|
@@ -35,13 +35,16 @@ import '../assert_dev_env.js';
|
|
|
35
35
|
*/
|
|
36
36
|
import { z } from 'zod';
|
|
37
37
|
import { type RouteAuth } from '../../http/auth_shape.js';
|
|
38
|
+
import { RateLimitKey } from '../../http/error_schemas.js';
|
|
38
39
|
import type { RequestResponseActionSpec } from '../../actions/action_spec.js';
|
|
39
40
|
/**
|
|
40
41
|
* One method's normalized RPC metadata — the cross-impl-comparable unit.
|
|
41
42
|
* `roles` / `credential_types` are always present + sorted (an absent gate
|
|
42
43
|
* and an empty list both serialize to `[]`) so the diff never trips on a
|
|
43
44
|
* `undefined`-vs-`[]` or declaration-order difference between impls; the
|
|
44
|
-
* auth axes reuse the canonical `AuthAxisState` enum.
|
|
45
|
+
* auth axes reuse the canonical `AuthAxisState` enum. `rate_limit` is
|
|
46
|
+
* nullable rather than optional for the same reason — an unthrottled action
|
|
47
|
+
* serializes an explicit `null`, so a missing key is drift, not a default.
|
|
45
48
|
*/
|
|
46
49
|
export declare const ActionManifestEntry: z.ZodObject<{
|
|
47
50
|
/** RPC method name (`spec.method`). */
|
|
@@ -64,6 +67,12 @@ export declare const ActionManifestEntry: z.ZodObject<{
|
|
|
64
67
|
roles: z.ZodArray<z.ZodString>;
|
|
65
68
|
/** Permitted credential channels, sorted; `[]` when ungated (any credential). */
|
|
66
69
|
credential_types: z.ZodArray<z.ZodString>;
|
|
70
|
+
/** Per-action rate-limit class, or `null` when the action declares none. */
|
|
71
|
+
rate_limit: z.ZodNullable<z.ZodEnum<{
|
|
72
|
+
account: "account";
|
|
73
|
+
ip: "ip";
|
|
74
|
+
both: "both";
|
|
75
|
+
}>>;
|
|
67
76
|
}, z.core.$strict>;
|
|
68
77
|
export type ActionManifestEntry = z.infer<typeof ActionManifestEntry>;
|
|
69
78
|
/** The full action manifest — every entry, sorted by `method`. */
|
|
@@ -89,6 +98,12 @@ export declare const ActionManifest: z.ZodObject<{
|
|
|
89
98
|
roles: z.ZodArray<z.ZodString>;
|
|
90
99
|
/** Permitted credential channels, sorted; `[]` when ungated (any credential). */
|
|
91
100
|
credential_types: z.ZodArray<z.ZodString>;
|
|
101
|
+
/** Per-action rate-limit class, or `null` when the action declares none. */
|
|
102
|
+
rate_limit: z.ZodNullable<z.ZodEnum<{
|
|
103
|
+
account: "account";
|
|
104
|
+
ip: "ip";
|
|
105
|
+
both: "both";
|
|
106
|
+
}>>;
|
|
92
107
|
}, z.core.$strict>>;
|
|
93
108
|
}, z.core.$strict>;
|
|
94
109
|
export type ActionManifest = z.infer<typeof ActionManifest>;
|
|
@@ -101,6 +116,7 @@ export declare const action_manifest_entry: (spec: {
|
|
|
101
116
|
readonly method: string;
|
|
102
117
|
readonly auth: RouteAuth;
|
|
103
118
|
readonly side_effects: boolean;
|
|
119
|
+
readonly rate_limit?: RateLimitKey;
|
|
104
120
|
}) => ActionManifestEntry;
|
|
105
121
|
/**
|
|
106
122
|
* Build the normalized `ActionManifest` from a list of request-response
|
|
@@ -109,5 +125,5 @@ export declare const action_manifest_entry: (spec: {
|
|
|
109
125
|
* it passes (the TS spine passes its full mount; the Rust stub filters
|
|
110
126
|
* `PROTOCOL_ACTION_SPECS` first) — see the module doc.
|
|
111
127
|
*/
|
|
112
|
-
export declare const build_action_manifest: (specs: ReadonlyArray<Pick<RequestResponseActionSpec, "method" | "auth" | "side_effects">>) => ActionManifest;
|
|
128
|
+
export declare const build_action_manifest: (specs: ReadonlyArray<Pick<RequestResponseActionSpec, "method" | "auth" | "side_effects" | "rate_limit">>) => ActionManifest;
|
|
113
129
|
//# sourceMappingURL=action_manifest.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action_manifest.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/action_manifest.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,8BAA8B,CAAC;AAE9E
|
|
1
|
+
{"version":3,"file":"action_manifest.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/action_manifest.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,8BAA8B,CAAC;AAE9E;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB;IAC/B,uCAAuC;;IAEvC,iGAAiG;;IAEjG,+BAA+B;;;;;;IAE/B,6BAA6B;;;;;;IAE7B,mFAAmF;;IAEnF,iFAAiF;;IAEjF,4EAA4E;;;;;;kBAE3E,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,kEAAkE;AAClE,eAAO,MAAM,cAAc;;QAlB1B,uCAAuC;;QAEvC,iGAAiG;;QAEjG,+BAA+B;;;;;;QAE/B,6BAA6B;;;;;;QAE7B,mFAAmF;;QAEnF,iFAAiF;;QAEjF,4EAA4E;;;;;;;kBAQ3E,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAM5D;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAAI,MAAM;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,YAAY,CAAC;CACnC,KAAG,mBAQF,CAAC;AAMH;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GACjC,OAAO,aAAa,CACnB,IAAI,CAAC,yBAAyB,EAAE,QAAQ,GAAG,MAAM,GAAG,cAAc,GAAG,YAAY,CAAC,CAClF,KACC,cAED,CAAC"}
|
|
@@ -2,7 +2,7 @@ import '../assert_dev_env.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Cross-impl RPC **action-manifest** introspection — a normalized,
|
|
4
4
|
* JSON-serializable dump of a backend's live RPC method set, one entry per
|
|
5
|
-
* method carrying its auth shape
|
|
5
|
+
* method carrying its auth shape, side-effect flag, and rate-limit class.
|
|
6
6
|
*
|
|
7
7
|
* The sibling of `schema_introspect.ts`'s `SchemaSnapshot`: where that
|
|
8
8
|
* captures the live *database* shape for the schema-parity gate, this
|
|
@@ -35,12 +35,15 @@ import '../assert_dev_env.js';
|
|
|
35
35
|
*/
|
|
36
36
|
import { z } from 'zod';
|
|
37
37
|
import { AuthAxisState } from '../../http/auth_shape.js';
|
|
38
|
+
import { RateLimitKey } from '../../http/error_schemas.js';
|
|
38
39
|
/**
|
|
39
40
|
* One method's normalized RPC metadata — the cross-impl-comparable unit.
|
|
40
41
|
* `roles` / `credential_types` are always present + sorted (an absent gate
|
|
41
42
|
* and an empty list both serialize to `[]`) so the diff never trips on a
|
|
42
43
|
* `undefined`-vs-`[]` or declaration-order difference between impls; the
|
|
43
|
-
* auth axes reuse the canonical `AuthAxisState` enum.
|
|
44
|
+
* auth axes reuse the canonical `AuthAxisState` enum. `rate_limit` is
|
|
45
|
+
* nullable rather than optional for the same reason — an unthrottled action
|
|
46
|
+
* serializes an explicit `null`, so a missing key is drift, not a default.
|
|
44
47
|
*/
|
|
45
48
|
export const ActionManifestEntry = z.strictObject({
|
|
46
49
|
/** RPC method name (`spec.method`). */
|
|
@@ -54,7 +57,9 @@ export const ActionManifestEntry = z.strictObject({
|
|
|
54
57
|
/** Required roles (any-of), sorted; `[]` when the action declares no role gate. */
|
|
55
58
|
roles: z.array(z.string()),
|
|
56
59
|
/** Permitted credential channels, sorted; `[]` when ungated (any credential). */
|
|
57
|
-
credential_types: z.array(z.string())
|
|
60
|
+
credential_types: z.array(z.string()),
|
|
61
|
+
/** Per-action rate-limit class, or `null` when the action declares none. */
|
|
62
|
+
rate_limit: RateLimitKey.nullable()
|
|
58
63
|
});
|
|
59
64
|
/** The full action manifest — every entry, sorted by `method`. */
|
|
60
65
|
export const ActionManifest = z.strictObject({
|
|
@@ -73,7 +78,8 @@ export const action_manifest_entry = (spec) => ({
|
|
|
73
78
|
account: spec.auth.account,
|
|
74
79
|
actor: spec.auth.actor,
|
|
75
80
|
roles: sorted_strings(spec.auth.roles),
|
|
76
|
-
credential_types: sorted_strings(spec.auth.credential_types)
|
|
81
|
+
credential_types: sorted_strings(spec.auth.credential_types),
|
|
82
|
+
rate_limit: spec.rate_limit ?? null
|
|
77
83
|
});
|
|
78
84
|
/** Deterministic byte-lexicographic method order — matches the Rust `str::cmp` the stub sorts by. */
|
|
79
85
|
const compare_method = (a, b) => a.method < b.method ? -1 : a.method > b.method ? 1 : 0;
|
|
@@ -7,8 +7,8 @@ import '../assert_dev_env.js';
|
|
|
7
7
|
* fuz_app spine and the Rust `testing_spine_stub`) are each other's parity
|
|
8
8
|
* reference. After both bootstrap, dump each one's live RPC method set, diff,
|
|
9
9
|
* fail loudly on drift. The diff entries name the specific divergence (a
|
|
10
|
-
* method only one impl mounts, a per-method auth-axis
|
|
11
|
-
* mismatch) so the error message points at the source.
|
|
10
|
+
* method only one impl mounts, a per-method auth-axis, side-effect, or
|
|
11
|
+
* rate-limit-class mismatch) so the error message points at the source.
|
|
12
12
|
*
|
|
13
13
|
* ```ts
|
|
14
14
|
* const manifest_a = await capture_action_manifest(ts_handle);
|
|
@@ -17,13 +17,14 @@ import '../assert_dev_env.js';
|
|
|
17
17
|
* ```
|
|
18
18
|
*
|
|
19
19
|
* Non-coverage: the manifest captures `{method, side_effects, account, actor,
|
|
20
|
-
* roles, credential_types}` — the wire-relevant auth shape. It does **not**
|
|
20
|
+
* roles, credential_types, rate_limit}` — the wire-relevant auth shape. It does **not**
|
|
21
21
|
* capture input/output schemas (the declared-surface wire shape is gated by
|
|
22
22
|
* `rpc_round_trip`) nor the protocol actions `heartbeat` / `cancel` (excluded
|
|
23
23
|
* by construction — see `action_manifest.ts`).
|
|
24
24
|
*
|
|
25
25
|
* @module
|
|
26
26
|
*/
|
|
27
|
+
import type { RateLimitKey } from '../../http/error_schemas.js';
|
|
27
28
|
import type { ActionManifest } from './action_manifest.js';
|
|
28
29
|
/** Structured drift entry. `where` is the named source impl ('a' or 'b'). */
|
|
29
30
|
export type ActionManifestDiff = {
|
|
@@ -35,6 +36,11 @@ export type ActionManifestDiff = {
|
|
|
35
36
|
readonly method: string;
|
|
36
37
|
readonly a: boolean;
|
|
37
38
|
readonly b: boolean;
|
|
39
|
+
} | {
|
|
40
|
+
readonly kind: 'rate_limit_differs';
|
|
41
|
+
readonly method: string;
|
|
42
|
+
readonly a: RateLimitKey | null;
|
|
43
|
+
readonly b: RateLimitKey | null;
|
|
38
44
|
} | {
|
|
39
45
|
readonly kind: 'auth_field_differs';
|
|
40
46
|
readonly method: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action_manifest_parity.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/action_manifest_parity.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,sBAAsB,CAAC;AAWhF,6EAA6E;AAC7E,MAAM,MAAM,kBAAkB,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvF;IACA,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;CACnB,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,kBAAkB,CAAC;IACnE,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAML;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GACjC,GAAG,cAAc,EACjB,GAAG,cAAc,KACf,KAAK,CAAC,kBAAkB,
|
|
1
|
+
{"version":3,"file":"action_manifest_parity.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/action_manifest_parity.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,sBAAsB,CAAC;AAWhF,6EAA6E;AAC7E,MAAM,MAAM,kBAAkB,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvF;IACA,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;CACnB,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;CAC/B,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,kBAAkB,CAAC;IACnE,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAML;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GACjC,GAAG,cAAc,EACjB,GAAG,cAAc,KACf,KAAK,CAAC,kBAAkB,CAkC1B,CAAC;AAEF,qEAAqE;AACrE,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,OAAO,aAAa,CAAC,kBAAkB,CAAC,EACxC,SAAQ,wBAA6B,KACnC,MAqCF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,6BAA6B,GACzC,GAAG,cAAc,EACjB,GAAG,cAAc,EACjB,SAAQ,wBAA6B,KACnC,IAUF,CAAC"}
|
|
@@ -30,6 +30,9 @@ export const diff_action_manifests = (a, b) => {
|
|
|
30
30
|
if (ea.side_effects !== eb.side_effects) {
|
|
31
31
|
diffs.push({ kind: 'side_effects_differ', method, a: ea.side_effects, b: eb.side_effects });
|
|
32
32
|
}
|
|
33
|
+
if (ea.rate_limit !== eb.rate_limit) {
|
|
34
|
+
diffs.push({ kind: 'rate_limit_differs', method, a: ea.rate_limit, b: eb.rate_limit });
|
|
35
|
+
}
|
|
33
36
|
for (const field of AUTH_SCALAR_FIELDS) {
|
|
34
37
|
if (ea[field] !== eb[field]) {
|
|
35
38
|
diffs.push({ kind: 'auth_field_differs', method, field, a: ea[field], b: eb[field] });
|
|
@@ -62,6 +65,9 @@ export const format_action_manifest_diffs = (diffs, labels = {}) => {
|
|
|
62
65
|
case 'side_effects_differ':
|
|
63
66
|
lines.push(` ${d.method} side_effects differs: ${label_a}=${d.a}, ${label_b}=${d.b}`);
|
|
64
67
|
break;
|
|
68
|
+
case 'rate_limit_differs':
|
|
69
|
+
lines.push(` ${d.method} rate_limit differs: ${label_a}=${JSON.stringify(d.a)}, ${label_b}=${JSON.stringify(d.b)}`);
|
|
70
|
+
break;
|
|
65
71
|
case 'auth_field_differs':
|
|
66
72
|
lines.push(` ${d.method} auth.${d.field} differs: ${label_a}=${JSON.stringify(d.a)}, ${label_b}=${JSON.stringify(d.b)}`);
|
|
67
73
|
break;
|