@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token_lifetime.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/token_lifetime.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"token_lifetime.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/token_lifetime.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAgD9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C;;;;GAIG;AACH,MAAM,WAAW,6BAA6B;IAC7C,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAaD,eAAO,MAAM,mCAAmC,GAC/C,SAAS,6BAA6B,KACpC,IAsHF,CAAC"}
|
|
@@ -37,6 +37,7 @@ import '../assert_dev_env.js';
|
|
|
37
37
|
import { describe, test, assert } from 'vitest';
|
|
38
38
|
import { account_token_create_action_spec, account_token_list_action_spec, TokenCreateOutput, TokenListOutput } from '../../auth/account_action_specs.js';
|
|
39
39
|
import { cross_rpc_call, error_reason, expect_output } from './cell_cross_helpers.js';
|
|
40
|
+
import { assert_iso8601_seconds } from './wire_shapes.js';
|
|
40
41
|
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
41
42
|
/** TTL used by the bounded-mint case. */
|
|
42
43
|
const TTL_DAYS = 30;
|
|
@@ -61,6 +62,9 @@ export const describe_token_lifetime_cross_tests = (options) => {
|
|
|
61
62
|
}, fixture.create_session_headers());
|
|
62
63
|
const created = expect_output(create_res, TokenCreateOutput);
|
|
63
64
|
assert.ok(created.expires_at !== null, 'a ttl mint must return a non-null expires_at — null means the spine dropped the field');
|
|
65
|
+
// Byte-shape gate: `z.string()` accepts a millisecond stamp too, so
|
|
66
|
+
// `expect_output` above can't tell the two spines apart.
|
|
67
|
+
assert_iso8601_seconds(created.expires_at, 'token_create.expires_at');
|
|
64
68
|
const expires_ms = Date.parse(created.expires_at);
|
|
65
69
|
assert.ok(!Number.isNaN(expires_ms), `expires_at must parse as a date: ${created.expires_at}`);
|
|
66
70
|
const expected = before + TTL_DAYS * 86_400_000;
|
|
@@ -71,6 +75,8 @@ export const describe_token_lifetime_cross_tests = (options) => {
|
|
|
71
75
|
const row = listed.tokens.find((t) => t.id === created.id);
|
|
72
76
|
assert.ok(row, 'the minted token must appear in account_token_list');
|
|
73
77
|
assert.ok(row.expires_at !== null, 'account_token_list must report the stored expiry, not null');
|
|
78
|
+
assert_iso8601_seconds(row.expires_at, 'token_list.expires_at');
|
|
79
|
+
assert_iso8601_seconds(row.created_at, 'token_list.created_at');
|
|
74
80
|
});
|
|
75
81
|
test('an eternal mint stores NULL expiry', async () => {
|
|
76
82
|
const fixture = await setup_test();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import '../assert_dev_env.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { RpcPathCapabilityGatedCrossSuiteOptions } from './setup.js';
|
|
3
3
|
/** Options for the token-scope surface parity suite. */
|
|
4
|
-
export interface TokenScopeSurfaceCrossTestOptions extends
|
|
4
|
+
export interface TokenScopeSurfaceCrossTestOptions extends RpcPathCapabilityGatedCrossSuiteOptions {
|
|
5
5
|
/** Audit-log SSE path. Default `/api/admin/audit/stream`. */
|
|
6
6
|
readonly sse_path?: string;
|
|
7
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token_scope_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/token_scope_surface.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"token_scope_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/token_scope_surface.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA8E9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAkB1E,wDAAwD;AACxD,MAAM,WAAW,iCAAkC,SAAQ,uCAAuC;IACjG,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAwFD,eAAO,MAAM,wCAAwC,GACpD,SAAS,iCAAiC,KACxC,IAwHF,CAAC"}
|
|
@@ -26,6 +26,15 @@ import '../assert_dev_env.js';
|
|
|
26
26
|
* `required_scope` capability string, per surface. A spine that renamed a
|
|
27
27
|
* surface, dropped the `surface:` prefix, or emitted the JSON-RPC envelope
|
|
28
28
|
* from a REST route fails here.
|
|
29
|
+
* - **The audit stream's channel denial** — that surface is gated to
|
|
30
|
+
* `credential_types: ['session']` on both spines, so a bearer never reaches
|
|
31
|
+
* its scope gate and the observable is `credential_type_required` instead.
|
|
32
|
+
* The case is kept here rather than moved: it is still the bearer-vs-surface
|
|
33
|
+
* probe, and what it now pins is the coarser-gate-first ordering both spines
|
|
34
|
+
* run (a spine that checked scope first would answer `token_scope_required`).
|
|
35
|
+
* `surface:audit_stream`'s own capability string is consequently unreachable
|
|
36
|
+
* on the default mount and has no cross-impl pin — each spine's census holds
|
|
37
|
+
* its declaration, the way the WS upgrade's is held.
|
|
29
38
|
* - **Scope outranks role** — a narrowed token whose account *also* lacks the
|
|
30
39
|
* gating role must hear `token_scope_required`, not
|
|
31
40
|
* `insufficient_permissions`. This is the observable the TS↔Rust ordering
|
|
@@ -37,16 +46,17 @@ import '../assert_dev_env.js';
|
|
|
37
46
|
* Two non-vacuity controls hold the negatives honest: the narrowed token must
|
|
38
47
|
* still reach the RPC method it *does* name (otherwise a spine that rejected
|
|
39
48
|
* the credential outright would pass every denial), and a **full** bearer must
|
|
40
|
-
* reach
|
|
41
|
-
*
|
|
49
|
+
* reach `fact_bare` (otherwise a route broken for all bearers would pass too).
|
|
50
|
+
* The second has no audit-stream twin by construction — refusing every bearer
|
|
51
|
+
* is exactly what that route's channel gate is for.
|
|
42
52
|
*
|
|
43
53
|
* ## Why the WS upgrade isn't here
|
|
44
54
|
*
|
|
45
55
|
* Rule 3's third probeable surface is the WebSocket upgrade, and it is
|
|
46
56
|
* deliberately omitted: `create_ws_transport` threads cookies, not bearer
|
|
47
57
|
* headers, and a refused upgrade surfaces as a thrown connection error whose
|
|
48
|
-
* body — the thing this suite exists to compare — isn't readable. The
|
|
49
|
-
* surfaces are where
|
|
58
|
+
* body — the thing this suite exists to compare — isn't readable. The HTTP
|
|
59
|
+
* surfaces are where a denial body is observable, which is what the parity
|
|
50
60
|
* gate needs. The WS gate keeps its per-spine coverage (fuz_app's surface
|
|
51
61
|
* census pins the call site; the Rust census pins its own).
|
|
52
62
|
*
|
|
@@ -57,7 +67,7 @@ import '../assert_dev_env.js';
|
|
|
57
67
|
*/
|
|
58
68
|
import { describe, assert } from 'vitest';
|
|
59
69
|
import { account_token_create_action_spec } from '../../auth/account_action_specs.js';
|
|
60
|
-
import { ERROR_TOKEN_SCOPE_REQUIRED } from '../../http/error_schemas.js';
|
|
70
|
+
import { ERROR_CREDENTIAL_TYPE_REQUIRED, ERROR_TOKEN_SCOPE_REQUIRED } from '../../http/error_schemas.js';
|
|
61
71
|
import { test_if } from './capabilities.js';
|
|
62
72
|
import { cross_rpc_call } from './cell_cross_helpers.js';
|
|
63
73
|
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
@@ -107,6 +117,25 @@ const assert_surface_denied = async (fresh_transport, path, token, surface) => {
|
|
|
107
117
|
const body = await res.json();
|
|
108
118
|
assert.deepStrictEqual(body, surface_denial_body(surface), `${path} must return the canonical flat surface denial`);
|
|
109
119
|
};
|
|
120
|
+
/**
|
|
121
|
+
* Assert a bearer GET is refused by the *channel* gate rather than the scope
|
|
122
|
+
* gate — the shape a surface takes once it declares
|
|
123
|
+
* `credential_types: ['session']`.
|
|
124
|
+
*
|
|
125
|
+
* Distinct from `assert_surface_denied` on purpose: a channel-gated surface
|
|
126
|
+
* refuses **every** bearer, narrow or full, so the denial cannot name a scope.
|
|
127
|
+
* A narrowed token is still the right probe — it proves the channel gate runs
|
|
128
|
+
* *first*, since a scope-gated route would have answered
|
|
129
|
+
* `token_scope_required` instead.
|
|
130
|
+
*/
|
|
131
|
+
const assert_channel_denied = async (fresh_transport, path, token, credential_types) => {
|
|
132
|
+
const res = await fresh_transport({ origin: null })(path, {
|
|
133
|
+
headers: { authorization: `Bearer ${token}` }
|
|
134
|
+
});
|
|
135
|
+
assert.strictEqual(res.status, 403, `${path} must refuse a bearer with 403`);
|
|
136
|
+
const body = await res.json();
|
|
137
|
+
assert.deepStrictEqual(body, { error: ERROR_CREDENTIAL_TYPE_REQUIRED, required_credential_types: credential_types }, `${path} must return the canonical flat credential-type denial`);
|
|
138
|
+
};
|
|
110
139
|
export const describe_token_scope_surface_cross_tests = (options) => {
|
|
111
140
|
const { setup_test, capabilities } = options;
|
|
112
141
|
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
@@ -123,10 +152,10 @@ export const describe_token_scope_surface_cross_tests = (options) => {
|
|
|
123
152
|
const res = await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, ADMITTED_METHOD, undefined, { authorization: `Bearer ${token}` });
|
|
124
153
|
assert.ok(res.ok, `admitted method must succeed: ${JSON.stringify(res.error)}`);
|
|
125
154
|
});
|
|
126
|
-
test_if(capabilities.sse, '
|
|
155
|
+
test_if(capabilities.sse, 'the audit stream refuses a bearer at the channel gate (credential_types)', async () => {
|
|
127
156
|
const fixture = await setup_test();
|
|
128
157
|
const token = await mint_narrowed_token(fixture.transport, rpc_path, fixture.create_session_headers());
|
|
129
|
-
await
|
|
158
|
+
await assert_channel_denied(fixture.fresh_transport, sse_path, token, ['session']);
|
|
130
159
|
});
|
|
131
160
|
test_if(capabilities.fact_serving, 'a narrowed token is refused the bare-hash fact read (surface:fact_bare)', async () => {
|
|
132
161
|
const fixture = await setup_test();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Assert `value` is a timestamp in the spine's canonical wire shape:
|
|
4
|
+
* second-precision UTC, exactly 20 characters. The TS spine emits it from
|
|
5
|
+
* the `iso8601_timestamp_column` SQL projection (`db/sql_columns.ts`) and
|
|
6
|
+
* `to_iso8601_seconds` (`timestamp.ts`); the Rust spine from
|
|
7
|
+
* `fuz_db::iso8601_timestamp_column` and `fuz_sys::rfc3339_now`.
|
|
8
|
+
*
|
|
9
|
+
* @param value - the field read off a wire response
|
|
10
|
+
* @param label - the field's name, for the failure message
|
|
11
|
+
*/
|
|
12
|
+
export declare const assert_iso8601_seconds: (value: unknown, label: string) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Assert `value` is either `null` or a canonical second-precision UTC
|
|
15
|
+
* timestamp — the shape of every nullable timestamp on the wire
|
|
16
|
+
* (`expires_at`, `deleted_at`, `updated_at`, …).
|
|
17
|
+
*
|
|
18
|
+
* @param value - the field read off a wire response
|
|
19
|
+
* @param label - the field's name, for the failure message
|
|
20
|
+
*/
|
|
21
|
+
export declare const assert_iso8601_seconds_nullable: (value: unknown, label: string) => void;
|
|
22
|
+
//# sourceMappingURL=wire_shapes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire_shapes.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/wire_shapes.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAkB9B;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,OAAO,EAAE,OAAO,MAAM,KAAG,IAMtE,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,GAAI,OAAO,OAAO,EAAE,OAAO,MAAM,KAAG,IAG/E,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wire-shape assertions shared by the cross-backend suites.
|
|
4
|
+
*
|
|
5
|
+
* A field can carry the right value on both backends and still be a parity
|
|
6
|
+
* defect if the two serialize it differently — a millisecond
|
|
7
|
+
* `2026-05-17T12:34:56.789Z` and a second-precision `2026-05-17T12:34:56Z`
|
|
8
|
+
* both parse as the same instant, so a Zod `z.string()` check and an
|
|
9
|
+
* `assert.ok(value)` pass on either. These helpers pin the bytes.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import { assert } from 'vitest';
|
|
14
|
+
import { ISO8601_SECONDS } from '../../timestamp.js';
|
|
15
|
+
/**
|
|
16
|
+
* Assert `value` is a timestamp in the spine's canonical wire shape:
|
|
17
|
+
* second-precision UTC, exactly 20 characters. The TS spine emits it from
|
|
18
|
+
* the `iso8601_timestamp_column` SQL projection (`db/sql_columns.ts`) and
|
|
19
|
+
* `to_iso8601_seconds` (`timestamp.ts`); the Rust spine from
|
|
20
|
+
* `fuz_db::iso8601_timestamp_column` and `fuz_sys::rfc3339_now`.
|
|
21
|
+
*
|
|
22
|
+
* @param value - the field read off a wire response
|
|
23
|
+
* @param label - the field's name, for the failure message
|
|
24
|
+
*/
|
|
25
|
+
export const assert_iso8601_seconds = (value, label) => {
|
|
26
|
+
assert.isTrue(typeof value === 'string', `${label} must be a string, got ${JSON.stringify(value)}`);
|
|
27
|
+
assert.match(value, ISO8601_SECONDS, `${label} must be second-precision UTC`);
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Assert `value` is either `null` or a canonical second-precision UTC
|
|
31
|
+
* timestamp — the shape of every nullable timestamp on the wire
|
|
32
|
+
* (`expires_at`, `deleted_at`, `updated_at`, …).
|
|
33
|
+
*
|
|
34
|
+
* @param value - the field read off a wire response
|
|
35
|
+
* @param label - the field's name, for the failure message
|
|
36
|
+
*/
|
|
37
|
+
export const assert_iso8601_seconds_nullable = (value, label) => {
|
|
38
|
+
if (value === null)
|
|
39
|
+
return;
|
|
40
|
+
assert_iso8601_seconds(value, label);
|
|
41
|
+
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import './assert_dev_env.js';
|
|
2
2
|
import type { AppSurface, AppSurfaceSpec } from '../http/surface.js';
|
|
3
|
-
import type { BackendCapabilities } from './cross_backend/capabilities.js';
|
|
4
3
|
import type { SetupTest } from './cross_backend/setup.js';
|
|
5
4
|
/**
|
|
6
5
|
* Recursively collect all property names from a JSON Schema.
|
|
@@ -26,8 +25,6 @@ export interface DataExposureTestOptions {
|
|
|
26
25
|
* TS by the consumer; same shape for in-process and cross-process tests.
|
|
27
26
|
*/
|
|
28
27
|
surface_source: AppSurfaceSpec;
|
|
29
|
-
/** Backend capability declarations. */
|
|
30
|
-
capabilities: BackendCapabilities;
|
|
31
28
|
/** Fields that must never appear in any response. Default: `sensitive_field_blocklist`. */
|
|
32
29
|
sensitive_fields?: ReadonlyArray<string>;
|
|
33
30
|
/** Fields that must not appear in non-admin responses. Default: `admin_only_field_blocklist`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"data_exposure.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/data_exposure.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAqB7B,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAYrE,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"data_exposure.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/data_exposure.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAqB7B,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAYrE,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,0BAA0B,CAAC;AAIvE;;;;;GAKG;AACH,eAAO,MAAM,kCAAkC,GAAI,QAAQ,OAAO,KAAG,GAAG,CAAC,MAAM,CAuB9E,CAAC;AAIF;;GAEG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,UAAU,EACnB,mBAAkB,aAAa,CAAC,MAAM,CAA6B,KACjE,IAWF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,wCAAwC,GACpD,SAAS,UAAU,EACnB,oBAAmB,aAAa,CAAC,MAAM,CAA8B,KACnE,IAgBF,CAAC;AAIF,kDAAkD;AAClD,MAAM,WAAW,uBAAuB;IACvC,kEAAkE;IAClE,UAAU,EAAE,SAAS,CAAC;IACtB;;;OAGG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,2FAA2F;IAC3F,gBAAgB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACzC,iGAAiG;IACjG,iBAAiB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC1C,kDAAkD;IAClD,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GAAI,SAAS,uBAAuB,KAAG,IA0L/E,CAAC"}
|
|
@@ -96,7 +96,6 @@ export const describe_data_exposure_tests = (options) => {
|
|
|
96
96
|
const { surface, route_specs } = options.surface_source;
|
|
97
97
|
const { sensitive_fields = sensitive_field_blocklist, admin_only_fields = admin_only_field_blocklist } = options;
|
|
98
98
|
const skip_set = new Set(options.skip_routes);
|
|
99
|
-
void options.capabilities;
|
|
100
99
|
describe('data exposure — schema-level', () => {
|
|
101
100
|
test('no sensitive fields in any output schema', () => {
|
|
102
101
|
assert_output_schemas_no_sensitive_fields(surface, sensitive_fields);
|
package/dist/testing/db.d.ts
CHANGED
|
@@ -15,27 +15,64 @@ export interface DbFactory {
|
|
|
15
15
|
skip_reason?: string;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* Reset a
|
|
18
|
+
* Reset a test database to a clean state by dropping and recreating the public schema.
|
|
19
19
|
*
|
|
20
20
|
* Removes all tables, sequences, indexes, types, and functions.
|
|
21
|
-
* The database instance remains usable after reset.
|
|
21
|
+
* The database instance remains usable after reset. Despite the name it issues
|
|
22
|
+
* plain SQL, so it also resets the other drivers the test factories hand out.
|
|
22
23
|
*
|
|
23
24
|
* @mutates db - drops the `public` schema and recreates it; all rows in all
|
|
24
25
|
* tables are gone after this returns.
|
|
25
26
|
*/
|
|
26
27
|
export declare const reset_pglite: (db: Db) => Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Builds a `DbFactory` from a schema initializer — `create_pglite_factory`'s own shape.
|
|
30
|
+
*/
|
|
31
|
+
export type DbFactoryBuilder = (init_schema: (db: Db) => Promise<void>) => DbFactory;
|
|
32
|
+
/**
|
|
33
|
+
* Options for `create_pglite_factory`.
|
|
34
|
+
*/
|
|
35
|
+
export interface CreatePgliteFactoryOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Whether an installed substitute (`set_substitute_db_factory`) may stand in
|
|
38
|
+
* for the PGlite factory at this call site. Defaults to `true`; pass `false`
|
|
39
|
+
* where PGlite itself is the subject under test.
|
|
40
|
+
*/
|
|
41
|
+
substitutable?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Install a builder that stands in for `create_pglite_factory`, so suites
|
|
45
|
+
* written against PGlite can run against another driver without being edited.
|
|
46
|
+
*
|
|
47
|
+
* Injected rather than selected here: the drivers a substitute wraps live in
|
|
48
|
+
* test-side code this shipped module cannot import. A call site that passes
|
|
49
|
+
* `{substitutable: false}` keeps PGlite regardless. Pass `null` to uninstall.
|
|
50
|
+
*
|
|
51
|
+
* The slot is read when a factory is built, not when it creates a database,
|
|
52
|
+
* so install before the first `create_pglite_factory` call — a vitest
|
|
53
|
+
* `setupFiles` entry, which runs ahead of every test file's imports, is the
|
|
54
|
+
* shape fuz_app's own `db` project uses. A factory built earlier (including
|
|
55
|
+
* the module-level fallback in `testing/app_server.ts`) keeps the driver it
|
|
56
|
+
* was built with, and uninstalling does not revert it.
|
|
57
|
+
*
|
|
58
|
+
* @param create_factory - the builder to install, or `null` to remove the installed one
|
|
59
|
+
*/
|
|
60
|
+
export declare const set_substitute_db_factory: (create_factory: DbFactoryBuilder | null) => void;
|
|
27
61
|
/**
|
|
28
62
|
* Create a pglite (in-memory) database factory for tests.
|
|
29
63
|
*
|
|
30
64
|
* Always enabled — no external dependencies required.
|
|
31
|
-
* Shares a single PGlite WASM instance across
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
65
|
+
* Shares a single PGlite WASM instance across the PGlite factories this module builds.
|
|
66
|
+
* Subsequent `create()` calls reset the schema via `DROP SCHEMA public CASCADE`
|
|
67
|
+
* instead of paying the WASM cold-start cost again.
|
|
68
|
+
*
|
|
69
|
+
* Returns the installed substitute's factory instead when one is installed and
|
|
70
|
+
* `options.substitutable` is not `false` — see `set_substitute_db_factory`.
|
|
35
71
|
*
|
|
36
72
|
* @param init_schema - callback to initialize the database schema
|
|
73
|
+
* @param options - `substitutable: false` pins this call site to PGlite
|
|
37
74
|
*/
|
|
38
|
-
export declare const create_pglite_factory: (init_schema: (db: Db) => Promise<void
|
|
75
|
+
export declare const create_pglite_factory: (init_schema: (db: Db) => Promise<void>, options?: CreatePgliteFactoryOptions) => DbFactory;
|
|
39
76
|
/**
|
|
40
77
|
* Create a pg (PostgreSQL) database factory for tests.
|
|
41
78
|
*
|
package/dist/testing/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/db.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AA8B7B,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAMtC;;GAEG;AACH,eAAO,MAAM,KAAK,SAA4B,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,OAAO,CAAC,EAAE,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/db.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AA8B7B,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAMtC;;GAEG;AACH,eAAO,MAAM,KAAK,SAA4B,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,OAAO,CAAC,EAAE,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,GAAU,IAAI,EAAE,KAAG,OAAO,CAAC,IAAI,CAGvD,CAAC;AAQF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,WAAW,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;AAErF;;GAEG;AACH,MAAM,WAAW,0BAA0B;IAC1C;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACxB;AAKD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,yBAAyB,GAAI,gBAAgB,gBAAgB,GAAG,IAAI,KAAG,IAEnF,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GACjC,aAAa,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,EACtC,UAAU,0BAA0B,KAClC,SAyBF,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,iBAAiB,GAC7B,aAAa,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,EACtC,WAAW,MAAM,KACf,SAgEF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,UAQhC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,UAAyC,CAAC;AAEvF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,GAAU,IAAI,EAAE,KAAG,OAAO,CAAC,IAAI,CAG3D,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,GAC9B,WAAW,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,EACvC,iBAAiB,KAAK,CAAC,MAAM,CAAC,KAC5B,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,KAAK,IAAI,CAwBzD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAAI,WAAW,KAAK,CAAC,SAAS,CAAC,KAAG,IAQnE,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,yBAAyB,GACrC,IAAI,EAAE,EACN,OAAO,MAAM,EACb,SAAS,aAAa,CAAC,MAAM,CAAC,KAC5B,OAAO,CAAC,IAAI,CAId,CAAC"}
|
package/dist/testing/db.js
CHANGED
|
@@ -33,10 +33,11 @@ import { query_public_columns } from '../db/schema_ready.js';
|
|
|
33
33
|
*/
|
|
34
34
|
export const IS_CI = process.env.CI === 'true';
|
|
35
35
|
/**
|
|
36
|
-
* Reset a
|
|
36
|
+
* Reset a test database to a clean state by dropping and recreating the public schema.
|
|
37
37
|
*
|
|
38
38
|
* Removes all tables, sequences, indexes, types, and functions.
|
|
39
|
-
* The database instance remains usable after reset.
|
|
39
|
+
* The database instance remains usable after reset. Despite the name it issues
|
|
40
|
+
* plain SQL, so it also resets the other drivers the test factories hand out.
|
|
40
41
|
*
|
|
41
42
|
* @mutates db - drops the `public` schema and recreates it; all rows in all
|
|
42
43
|
* tables are gone after this returns.
|
|
@@ -45,40 +46,74 @@ export const reset_pglite = async (db) => {
|
|
|
45
46
|
await db.query('DROP SCHEMA public CASCADE');
|
|
46
47
|
await db.query('CREATE SCHEMA public');
|
|
47
48
|
};
|
|
48
|
-
// Module-level PGlite cache —
|
|
49
|
-
//
|
|
49
|
+
// Module-level PGlite cache — one instance shared by the PGlite factories this
|
|
50
|
+
// module builds, for as long as the module is loaded. Under the `db` project's
|
|
51
|
+
// `isolate: false` + `fileParallelism: false` that is the whole run; a project
|
|
52
|
+
// that isolates its files gets one per worker instead.
|
|
50
53
|
let module_db = null;
|
|
54
|
+
// The installed stand-in for `create_pglite_factory`, `null` when none is installed.
|
|
55
|
+
let substitute_db_factory = null;
|
|
56
|
+
/**
|
|
57
|
+
* Install a builder that stands in for `create_pglite_factory`, so suites
|
|
58
|
+
* written against PGlite can run against another driver without being edited.
|
|
59
|
+
*
|
|
60
|
+
* Injected rather than selected here: the drivers a substitute wraps live in
|
|
61
|
+
* test-side code this shipped module cannot import. A call site that passes
|
|
62
|
+
* `{substitutable: false}` keeps PGlite regardless. Pass `null` to uninstall.
|
|
63
|
+
*
|
|
64
|
+
* The slot is read when a factory is built, not when it creates a database,
|
|
65
|
+
* so install before the first `create_pglite_factory` call — a vitest
|
|
66
|
+
* `setupFiles` entry, which runs ahead of every test file's imports, is the
|
|
67
|
+
* shape fuz_app's own `db` project uses. A factory built earlier (including
|
|
68
|
+
* the module-level fallback in `testing/app_server.ts`) keeps the driver it
|
|
69
|
+
* was built with, and uninstalling does not revert it.
|
|
70
|
+
*
|
|
71
|
+
* @param create_factory - the builder to install, or `null` to remove the installed one
|
|
72
|
+
*/
|
|
73
|
+
export const set_substitute_db_factory = (create_factory) => {
|
|
74
|
+
substitute_db_factory = create_factory;
|
|
75
|
+
};
|
|
51
76
|
/**
|
|
52
77
|
* Create a pglite (in-memory) database factory for tests.
|
|
53
78
|
*
|
|
54
79
|
* Always enabled — no external dependencies required.
|
|
55
|
-
* Shares a single PGlite WASM instance across
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
80
|
+
* Shares a single PGlite WASM instance across the PGlite factories this module builds.
|
|
81
|
+
* Subsequent `create()` calls reset the schema via `DROP SCHEMA public CASCADE`
|
|
82
|
+
* instead of paying the WASM cold-start cost again.
|
|
83
|
+
*
|
|
84
|
+
* Returns the installed substitute's factory instead when one is installed and
|
|
85
|
+
* `options.substitutable` is not `false` — see `set_substitute_db_factory`.
|
|
59
86
|
*
|
|
60
87
|
* @param init_schema - callback to initialize the database schema
|
|
88
|
+
* @param options - `substitutable: false` pins this call site to PGlite
|
|
61
89
|
*/
|
|
62
|
-
export const create_pglite_factory = (init_schema) =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
async create() {
|
|
66
|
-
if (module_db) {
|
|
67
|
-
await reset_pglite(module_db);
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
const { PGlite } = await import('@electric-sql/pglite');
|
|
71
|
-
const pglite = new PGlite();
|
|
72
|
-
module_db = create_pglite_db(pglite).db;
|
|
73
|
-
}
|
|
74
|
-
await init_schema(module_db);
|
|
75
|
-
return module_db;
|
|
76
|
-
},
|
|
77
|
-
async close() {
|
|
78
|
-
// No-op: shared instance lives for the worker thread's lifetime.
|
|
79
|
-
// PGlite is cleaned up when the vitest worker exits.
|
|
90
|
+
export const create_pglite_factory = (init_schema, options) => {
|
|
91
|
+
if (substitute_db_factory && options?.substitutable !== false) {
|
|
92
|
+
return substitute_db_factory(init_schema);
|
|
80
93
|
}
|
|
81
|
-
|
|
94
|
+
return {
|
|
95
|
+
name: 'pglite',
|
|
96
|
+
skip: false,
|
|
97
|
+
async create() {
|
|
98
|
+
if (module_db) {
|
|
99
|
+
await reset_pglite(module_db);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const { PGlite } = await import('@electric-sql/pglite');
|
|
103
|
+
const pglite = new PGlite();
|
|
104
|
+
module_db = create_pglite_db(pglite).db;
|
|
105
|
+
}
|
|
106
|
+
await init_schema(module_db);
|
|
107
|
+
return module_db;
|
|
108
|
+
},
|
|
109
|
+
async close() {
|
|
110
|
+
// No-op: the instance is the module-level cache above, so it lives as long
|
|
111
|
+
// as this module stays loaded — under the `db` project's `isolate: false` +
|
|
112
|
+
// `fileParallelism: false` that is the whole run. Nothing frees the PGlite
|
|
113
|
+
// instance earlier.
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
};
|
|
82
117
|
/**
|
|
83
118
|
* Create a pg (PostgreSQL) database factory for tests.
|
|
84
119
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAMhE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAkB1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAW,KAAK,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAMhE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAkB1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAW,KAAK,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAKpF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAG1D;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C;;;;OAIG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;;;;;;OASG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,kEAAkE;IAClE,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;OAIG;IACH,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;;;;;;;;OAYG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,IAs8CF,CAAC"}
|
|
@@ -25,6 +25,7 @@ import { account_verify_action_spec, account_session_list_action_spec, account_s
|
|
|
25
25
|
import { invite_create_action_spec } from '../auth/admin_action_specs.js';
|
|
26
26
|
import { LoginOutput, AccountStatusOutput } from '../auth/account_route_schema.js';
|
|
27
27
|
import { test_if } from './cross_backend/capabilities.js';
|
|
28
|
+
import { assert_iso8601_seconds, assert_iso8601_seconds_nullable } from './cross_backend/wire_shapes.js';
|
|
28
29
|
import { DEFAULT_TEST_PASSWORD } from './test_credentials.js';
|
|
29
30
|
/**
|
|
30
31
|
* Standard integration test suite for fuz_app auth routes.
|
|
@@ -343,6 +344,13 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
343
344
|
// Single-actor keeper: actor resolved, role_grants populated.
|
|
344
345
|
assert.ok(parsed.actor, 'single-actor keeper must resolve a non-null actor');
|
|
345
346
|
assert.ok(parsed.role_grants.length > 0, 'single-actor keeper must have populated role_grants');
|
|
347
|
+
// Byte-shape gate: `z.string()` accepts a millisecond stamp too,
|
|
348
|
+
// so the schema parse above can't tell the two spines apart.
|
|
349
|
+
assert_iso8601_seconds(parsed.account.created_at, 'account.created_at');
|
|
350
|
+
for (const grant of parsed.role_grants) {
|
|
351
|
+
assert_iso8601_seconds(grant.created_at, 'role_grant.created_at');
|
|
352
|
+
assert_iso8601_seconds_nullable(grant.expires_at, 'role_grant.expires_at');
|
|
353
|
+
}
|
|
346
354
|
});
|
|
347
355
|
});
|
|
348
356
|
// --- 2. Cookie attributes ---
|
|
@@ -419,7 +427,12 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
419
427
|
});
|
|
420
428
|
assert.ok(list_res.ok, 'account_session_list should succeed');
|
|
421
429
|
assert.ok(list_res.result.sessions.length >= 1);
|
|
422
|
-
const
|
|
430
|
+
const session = list_res.result.sessions[0];
|
|
431
|
+
// Byte-shape gate on the session timestamps — see the account-status
|
|
432
|
+
// case for why the output schema alone can't catch the divergence.
|
|
433
|
+
assert_iso8601_seconds(session.created_at, 'session.created_at');
|
|
434
|
+
assert_iso8601_seconds(session.expires_at, 'session.expires_at');
|
|
435
|
+
const session_id = session.id;
|
|
423
436
|
// Revoke that session by ID
|
|
424
437
|
const revoke_res = await rpc_call_for_spec({
|
|
425
438
|
app: { request: fixture.transport },
|
|
@@ -691,7 +704,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
691
704
|
// phase doesn't first demand an acting actor → `actor_required`)
|
|
692
705
|
// and input/param/query-free (params and query still validate ahead
|
|
693
706
|
// of the auth phase — only body validation moved behind the gates,
|
|
694
|
-
// per the 401 → authz → 403 → 400 order). Admin surfaces are
|
|
707
|
+
// per the 401 → authz → 403 → 429 → 400 order). Admin surfaces are
|
|
695
708
|
// RPC-first now; a consumer may expose no such REST route (fuz_app
|
|
696
709
|
// itself, zzz) — then this REST-era check is a no-op and RPC denial
|
|
697
710
|
// is covered by `describe_rpc_attack_surface_tests`.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import './assert_dev_env.js';
|
|
2
|
-
import type { BackendCapabilities } from './cross_backend/capabilities.js';
|
|
3
2
|
import type { SetupTest, TestFixture } from './cross_backend/setup.js';
|
|
4
3
|
import type { AppSurfaceSpec } from '../http/surface.js';
|
|
5
4
|
/** Options for `describe_round_trip_validation`. */
|
|
@@ -16,8 +15,6 @@ export interface RoundTripTestOptions {
|
|
|
16
15
|
* TS by the consumer; same shape for in-process and cross-process tests.
|
|
17
16
|
*/
|
|
18
17
|
surface_source: AppSurfaceSpec;
|
|
19
|
-
/** Backend capability declarations — see `cross_backend/capabilities.ts`. */
|
|
20
|
-
capabilities: BackendCapabilities;
|
|
21
18
|
/** Routes to skip, in `'METHOD /path'` format. */
|
|
22
19
|
skip_routes?: Array<string>;
|
|
23
20
|
/** Override generated bodies for specific routes (`'METHOD /path'` → body). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AA2B7B,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AA2B7B,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,oDAAoD;AACpD,MAAM,WAAW,oBAAoB;IACpC;;;;;OAKG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;OAGG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,kDAAkD;IAClD,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC5B,+EAA+E;IAC/E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACvD;;;;;;;;;;;;;OAaG;IACH,gBAAgB,CAAC,EAAE,GAAG,CACrB,MAAM,EACN,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CACnF,CAAC;CACF;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,8BAA8B,GAAI,SAAS,oBAAoB,KAAG,IAyI9E,CAAC"}
|
|
@@ -36,10 +36,6 @@ import { resolve_valid_path, generate_valid_body } from './schema_generators.js'
|
|
|
36
36
|
export const describe_round_trip_validation = (options) => {
|
|
37
37
|
const describe_time_specs = options.surface_source.route_specs;
|
|
38
38
|
const skip_set = new Set(options.skip_routes);
|
|
39
|
-
// `capabilities` is currently unused by this suite (no in-process-only
|
|
40
|
-
// reads, no transport-gated cases) but stays on the options for
|
|
41
|
-
// uniformity with the other Tier 1 suites.
|
|
42
|
-
void options.capabilities;
|
|
43
39
|
describe('round-trip validation', () => {
|
|
44
40
|
let fixture;
|
|
45
41
|
let authed_account;
|
|
@@ -84,8 +84,8 @@ const describe_rpc_auth = (options) => {
|
|
|
84
84
|
if (!app)
|
|
85
85
|
throw new Error(`No test app for role '${wrong_role}'`);
|
|
86
86
|
// Send valid params so the probe varies only the role. Dispatcher
|
|
87
|
-
// ordering is 401 → authz → 403 → 400, so the role gate
|
|
88
|
-
// ahead of validation regardless; valid params also keep the
|
|
87
|
+
// ordering is 401 → authz → 403 → 429 → 400, so the role gate
|
|
88
|
+
// trips ahead of validation regardless; valid params also keep the
|
|
89
89
|
// authorization phase from answering `actor_required` first.
|
|
90
90
|
const action = find_rpc_action(rpc_endpoint_specs, endpoint.path, method.name);
|
|
91
91
|
const params = action ? generate_valid_body(action.spec.input) : undefined;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import './assert_dev_env.js';
|
|
2
2
|
import type { AppSurfaceSpec } from '../http/surface.js';
|
|
3
3
|
import { type RpcEndpointsSuiteOption } from './rpc_helpers.js';
|
|
4
|
-
import type { BackendCapabilities } from './cross_backend/capabilities.js';
|
|
5
4
|
import type { SetupTest, TestFixture } from './cross_backend/setup.js';
|
|
6
5
|
import type { SessionOptions } from '../auth/session_cookie.js';
|
|
7
6
|
/** Options for `describe_rpc_round_trip_tests`. */
|
|
@@ -14,8 +13,6 @@ export interface RpcRoundTripTestOptions {
|
|
|
14
13
|
* in-process and cross-process tests.
|
|
15
14
|
*/
|
|
16
15
|
surface_source: AppSurfaceSpec;
|
|
17
|
-
/** Backend capability declarations. */
|
|
18
|
-
capabilities: BackendCapabilities;
|
|
19
16
|
/**
|
|
20
17
|
* Session config — only needed to resolve factory-form `rpc_endpoints`
|
|
21
18
|
* against a stub `AppServerContext` at setup time (the actions' input
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAyB7B,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,oBAAoB,CAAC;AAE9E,OAAO,EAQN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"rpc_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAyB7B,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,oBAAoB,CAAC;AAE9E,OAAO,EAQN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEhE,mDAAmD;AACnD,MAAM,WAAW,uBAAuB;IACvC,kEAAkE;IAClE,UAAU,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B;;;;;OAKG;IACH,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC,qDAAqD;IACrD,YAAY,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,6EAA6E;IAC7E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACvD;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;CAC3F;AAoDD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,6BAA6B,GAAI,SAAS,uBAAuB,KAAG,IA+JhF,CAAC"}
|
|
@@ -88,7 +88,6 @@ export const describe_rpc_round_trip_tests = (options) => {
|
|
|
88
88
|
// match what the running backend serves.
|
|
89
89
|
const rpc_endpoints_for_setup = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
|
|
90
90
|
const surface_rpc_endpoints = options.surface_source.surface.rpc_endpoints;
|
|
91
|
-
void options.capabilities;
|
|
92
91
|
describe('RPC round-trip validation', () => {
|
|
93
92
|
let fixture;
|
|
94
93
|
let authed_account;
|
|
@@ -20,6 +20,21 @@ export declare const generate_valid_value: (field: ZodFieldInfo, field_schema: z
|
|
|
20
20
|
/**
|
|
21
21
|
* Resolve a route path with valid-ish param values so params validation passes.
|
|
22
22
|
* Used when testing input on routes that also have params.
|
|
23
|
+
*
|
|
24
|
+
* Without a params schema (or for a param the schema doesn't name) each
|
|
25
|
+
* `:name` becomes `test_<name>` — fine for an unconstrained segment. With
|
|
26
|
+
* one, `test_<name>` is still preferred wherever the field schema accepts
|
|
27
|
+
* it, because the param name in the URL is what makes a failure message
|
|
28
|
+
* readable. Only a segment that refuses it falls through: a `uuid` param
|
|
29
|
+
* takes the nil UUID (the fast path — by far the most common shape), and
|
|
30
|
+
* anything else is synthesized by `generate_valid_string`, so a
|
|
31
|
+
* format-constrained segment (a `blake3:…` fact hash, a `tok_…` id)
|
|
32
|
+
* resolves to a value its schema accepts instead of a `test_<name>` that
|
|
33
|
+
* 400s before the route's real gates run.
|
|
34
|
+
*
|
|
35
|
+
* The synthesized value is percent-encoded, and a value containing `/` is
|
|
36
|
+
* refused (it would silently add a path segment and route elsewhere) —
|
|
37
|
+
* both fall back to `test_<name>`.
|
|
23
38
|
*/
|
|
24
39
|
export declare const resolve_valid_path: (path: string, params_schema?: z.ZodObject) => string;
|
|
25
40
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema_generators.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_generators.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAKN,KAAK,YAAY,EACjB,MAAM,yBAAyB,CAAC;AAIjC;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,cAAc,CAAC,CAAC,OAAO,KAAG,MAAM,GAAG,IAkBhE,CAAC;
|
|
1
|
+
{"version":3,"file":"schema_generators.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_generators.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAKN,KAAK,YAAY,EACjB,MAAM,yBAAyB,CAAC;AAIjC;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,cAAc,CAAC,CAAC,OAAO,KAAG,MAAM,GAAG,IAkBhE,CAAC;AAmHF,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,GAAI,OAAO,YAAY,EAAE,cAAc,CAAC,CAAC,OAAO,KAAG,OAgGnF,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,gBAAgB,CAAC,CAAC,SAAS,KAAG,MAiB9E,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAC/B,cAAc,CAAC,CAAC,OAAO,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SA+B5B,CAAC"}
|