@fuzdev/fuz_app 0.30.0 → 0.31.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 +630 -0
- package/dist/actions/action_rpc.d.ts +29 -0
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +42 -6
- package/dist/actions/action_types.d.ts +2 -2
- package/dist/actions/cancel.d.ts +12 -13
- package/dist/actions/cancel.d.ts.map +1 -1
- package/dist/actions/cancel.js +10 -13
- package/dist/actions/heartbeat.d.ts +8 -13
- package/dist/actions/heartbeat.d.ts.map +1 -1
- package/dist/actions/heartbeat.js +5 -8
- package/dist/actions/register_action_ws.d.ts +3 -3
- package/dist/actions/register_action_ws.js +2 -2
- package/dist/actions/register_ws_endpoint.d.ts +4 -4
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +3 -3
- package/dist/actions/socket.svelte.d.ts +16 -16
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +15 -15
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +923 -0
- package/dist/auth/account_action_specs.d.ts +216 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -0
- package/dist/auth/account_action_specs.js +159 -0
- package/dist/auth/account_actions.d.ts +51 -0
- package/dist/auth/account_actions.d.ts.map +1 -0
- package/dist/auth/account_actions.js +119 -0
- package/dist/auth/account_queries.d.ts +6 -2
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +40 -4
- package/dist/auth/account_routes.d.ts +94 -16
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +108 -180
- package/dist/auth/account_schema.d.ts +85 -30
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +40 -8
- package/dist/auth/admin_action_specs.d.ts +674 -0
- package/dist/auth/admin_action_specs.d.ts.map +1 -0
- package/dist/auth/admin_action_specs.js +287 -0
- package/dist/auth/admin_actions.d.ts +69 -0
- package/dist/auth/admin_actions.d.ts.map +1 -0
- package/dist/auth/admin_actions.js +256 -0
- package/dist/auth/api_token.d.ts +10 -0
- package/dist/auth/api_token.d.ts.map +1 -1
- package/dist/auth/api_token.js +9 -0
- package/dist/auth/api_token_queries.d.ts +3 -3
- package/dist/auth/api_token_queries.js +3 -3
- package/dist/auth/app_settings_schema.d.ts +4 -3
- package/dist/auth/app_settings_schema.d.ts.map +1 -1
- package/dist/auth/app_settings_schema.js +2 -1
- package/dist/auth/audit_log_routes.d.ts +14 -6
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +22 -79
- package/dist/auth/audit_log_schema.d.ts +100 -29
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +83 -11
- package/dist/auth/bootstrap_routes.d.ts +14 -0
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +10 -3
- package/dist/auth/cleanup.d.ts +63 -0
- package/dist/auth/cleanup.d.ts.map +1 -0
- package/dist/auth/cleanup.js +80 -0
- package/dist/auth/invite_schema.d.ts +11 -10
- package/dist/auth/invite_schema.d.ts.map +1 -1
- package/dist/auth/invite_schema.js +4 -3
- package/dist/auth/migrations.d.ts +6 -0
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +28 -0
- package/dist/auth/permit_offer_action_specs.d.ts +364 -0
- package/dist/auth/permit_offer_action_specs.d.ts.map +1 -0
- package/dist/auth/permit_offer_action_specs.js +216 -0
- package/dist/auth/permit_offer_actions.d.ts +96 -0
- package/dist/auth/permit_offer_actions.d.ts.map +1 -0
- package/dist/auth/permit_offer_actions.js +428 -0
- package/dist/auth/permit_offer_notifications.d.ts +361 -0
- package/dist/auth/permit_offer_notifications.d.ts.map +1 -0
- package/dist/auth/permit_offer_notifications.js +179 -0
- package/dist/auth/permit_offer_queries.d.ts +165 -0
- package/dist/auth/permit_offer_queries.d.ts.map +1 -0
- package/dist/auth/permit_offer_queries.js +390 -0
- package/dist/auth/permit_offer_schema.d.ts +103 -0
- package/dist/auth/permit_offer_schema.d.ts.map +1 -0
- package/dist/auth/permit_offer_schema.js +142 -0
- package/dist/auth/permit_queries.d.ts +77 -14
- package/dist/auth/permit_queries.d.ts.map +1 -1
- package/dist/auth/permit_queries.js +119 -24
- package/dist/auth/session_queries.d.ts +4 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +4 -2
- package/dist/auth/signup_routes.d.ts +13 -0
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +14 -7
- package/dist/http/CLAUDE.md +584 -0
- package/dist/http/pending_effects.d.ts +29 -0
- package/dist/http/pending_effects.d.ts.map +1 -0
- package/dist/http/pending_effects.js +31 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +4 -3
- package/dist/rate_limiter.d.ts +30 -0
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +25 -2
- package/dist/realtime/sse_auth_guard.d.ts +2 -0
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +5 -3
- package/dist/testing/CLAUDE.md +668 -1
- package/dist/testing/admin_integration.d.ts +10 -7
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +382 -482
- package/dist/testing/app_server.d.ts +7 -6
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/attack_surface.d.ts +9 -3
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +4 -4
- package/dist/testing/audit_completeness.d.ts +6 -0
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +158 -134
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +4 -33
- package/dist/testing/db.d.ts +1 -1
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +2 -0
- package/dist/testing/entities.d.ts +35 -13
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +17 -0
- package/dist/testing/integration.d.ts +10 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +352 -340
- package/dist/testing/integration_helpers.d.ts +16 -5
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +24 -4
- package/dist/testing/rate_limiting.d.ts +7 -0
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +41 -10
- package/dist/testing/rpc_helpers.d.ts +153 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +184 -8
- package/dist/testing/sse_round_trip.d.ts +8 -0
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +10 -3
- package/dist/testing/standard.d.ts +9 -1
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +6 -2
- package/dist/testing/surface_invariants.d.ts +7 -3
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +5 -4
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +9 -38
- package/dist/ui/AccountSessions.svelte +8 -4
- package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
- package/dist/ui/AdminAccounts.svelte +61 -33
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminAuditLog.svelte +3 -2
- package/dist/ui/AdminAuditLog.svelte.d.ts.map +1 -1
- package/dist/ui/AdminInvites.svelte +3 -2
- package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +14 -9
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/AdminPermitHistory.svelte +3 -2
- package/dist/ui/AdminPermitHistory.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSessions.svelte +29 -25
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +351 -0
- package/dist/ui/OpenSignupToggle.svelte +6 -3
- package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
- package/dist/ui/PermitOfferForm.svelte +141 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts +14 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -0
- package/dist/ui/PermitOfferHistory.svelte +109 -0
- package/dist/ui/PermitOfferHistory.svelte.d.ts +11 -0
- package/dist/ui/PermitOfferHistory.svelte.d.ts.map +1 -0
- package/dist/ui/PermitOfferInbox.svelte +121 -0
- package/dist/ui/PermitOfferInbox.svelte.d.ts +12 -0
- package/dist/ui/PermitOfferInbox.svelte.d.ts.map +1 -0
- package/dist/ui/account_sessions_state.svelte.d.ts +53 -3
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.js +39 -16
- package/dist/ui/admin_accounts_state.svelte.d.ts +118 -2
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +99 -23
- package/dist/ui/admin_invites_state.svelte.d.ts +47 -1
- package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_invites_state.svelte.js +38 -26
- package/dist/ui/admin_sessions_state.svelte.d.ts +26 -0
- package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_sessions_state.svelte.js +35 -21
- package/dist/ui/app_settings_state.svelte.d.ts +39 -0
- package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
- package/dist/ui/app_settings_state.svelte.js +34 -18
- package/dist/ui/audit_log_state.svelte.d.ts +40 -3
- package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
- package/dist/ui/audit_log_state.svelte.js +36 -42
- package/dist/ui/auth_state.svelte.d.ts +4 -3
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +4 -1
- package/dist/ui/permit_offers_state.svelte.d.ts +125 -0
- package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -0
- package/dist/ui/permit_offers_state.svelte.js +197 -0
- package/package.json +3 -3
- package/dist/auth/admin_routes.d.ts +0 -29
- package/dist/auth/admin_routes.d.ts.map +0 -1
- package/dist/auth/admin_routes.js +0 -226
- package/dist/auth/app_settings_routes.d.ts +0 -27
- package/dist/auth/app_settings_routes.d.ts.map +0 -1
- package/dist/auth/app_settings_routes.js +0 -66
- package/dist/auth/invite_routes.d.ts +0 -18
- package/dist/auth/invite_routes.d.ts.map +0 -1
- package/dist/auth/invite_routes.js +0 -129
|
@@ -15,17 +15,28 @@ import type { TestApp, TestAccount } from './app_server.js';
|
|
|
15
15
|
*/
|
|
16
16
|
export declare const find_route_spec: (specs: Array<RouteSpec>, method: string, path: string) => RouteSpec | undefined;
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* REST auth route suffixes still on the account/bootstrap surface after the
|
|
19
|
+
* 2026-04-22 RPC migration. `find_auth_route` rejects any other suffix at
|
|
20
|
+
* runtime — session/token CRUD, admin operations, and permit flows live on
|
|
21
|
+
* the RPC surface and should be reached via `rpc_call`.
|
|
22
|
+
*/
|
|
23
|
+
export declare const REST_AUTH_ROUTE_SUFFIXES: readonly ["/login", "/logout", "/password", "/verify", "/signup", "/bootstrap"];
|
|
24
|
+
export type RestAuthRouteSuffix = (typeof REST_AUTH_ROUTE_SUFFIXES)[number];
|
|
25
|
+
/**
|
|
26
|
+
* Find a REST auth route by suffix and method.
|
|
19
27
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
28
|
+
* Decouples tests from consumer route prefix (`/api/account/login`,
|
|
29
|
+
* `/api/auth/login`, etc.). `suffix` must be one of
|
|
30
|
+
* `REST_AUTH_ROUTE_SUFFIXES` — throws otherwise so a post-migration RPC
|
|
31
|
+
* method name (e.g. `/sessions/revoke-all`) fails loudly at the call site
|
|
32
|
+
* instead of silently returning `undefined`.
|
|
22
33
|
*
|
|
23
34
|
* @param specs - route specs to search
|
|
24
|
-
* @param suffix - path suffix
|
|
35
|
+
* @param suffix - REST auth path suffix
|
|
25
36
|
* @param method - HTTP method
|
|
26
37
|
* @returns matching route spec, or `undefined`
|
|
27
38
|
*/
|
|
28
|
-
export declare const find_auth_route: (specs: Array<RouteSpec>, suffix:
|
|
39
|
+
export declare const find_auth_route: (specs: Array<RouteSpec>, suffix: RestAuthRouteSuffix, method: RouteMethod) => RouteSpec | undefined;
|
|
29
40
|
/**
|
|
30
41
|
* Validate a response body against the route spec's declared schemas.
|
|
31
42
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAU7B,OAAO,KAAK,EAAC,SAAS,EAAE,WAAW,EAAC,MAAM,uBAAuB,CAAC;AAElE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAA8B,KAAK,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAE3F,OAAO,KAAK,EAAC,OAAO,EAAE,WAAW,EAAC,MAAM,iBAAiB,CAAC;AAE1D;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAC3B,OAAO,KAAK,CAAC,SAAS,CAAC,EACvB,QAAQ,MAAM,EACd,MAAM,MAAM,KACV,SAAS,GAAG,SAad,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"integration_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAU7B,OAAO,KAAK,EAAC,SAAS,EAAE,WAAW,EAAC,MAAM,uBAAuB,CAAC;AAElE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAA8B,KAAK,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAE3F,OAAO,KAAK,EAAC,OAAO,EAAE,WAAW,EAAC,MAAM,iBAAiB,CAAC;AAE1D;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAC3B,OAAO,KAAK,CAAC,SAAS,CAAC,EACvB,QAAQ,MAAM,EACd,MAAM,MAAM,KACV,SAAS,GAAG,SAad,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,iFAO3B,CAAC;AACX,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5E;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,GAC3B,OAAO,KAAK,CAAC,SAAS,CAAC,EACvB,QAAQ,mBAAmB,EAC3B,QAAQ,WAAW,KACjB,SAAS,GAAG,SAOd,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,aAAa,KAAK,CAAC,SAAS,CAAC,EAC7B,QAAQ,MAAM,EACd,MAAM,MAAM,EACZ,UAAU,QAAQ,KAChB,OAAO,CAAC,IAAI,CAmDd,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,GACtC,SAAS,OAAO,EAChB,iBAAiB,cAAc,CAAC,MAAM,CAAC,KACrC,OAAO,CAAC,MAAM,CAGhB,CAAC;AAuCF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,KAAK,CAAC,MAAM,CAQvF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,SAAS,MAAM,KACb,IAkBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oCAAoC,GAChD,UAAU,QAAQ,EAClB,MAAM;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,KACzB,IAUF,CAAC;AAIF,oEAAoE;AACpE,eAAO,MAAM,yBAAyB,EAAE,aAAa,CAAC,MAAM,CAAmC,CAAC;AAEhG,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,EAAE,aAAa,CAAC,MAAM,CAAgC,CAAC;AAE9F;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,GAAI,OAAO,OAAO,KAAG,GAAG,CAAC,MAAM,CAetE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,kCAAkC,GAC9C,MAAM,OAAO,EACb,WAAW,aAAa,CAAC,MAAM,CAAC,EAChC,SAAS,MAAM,KACb,IAKF,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,SAAS,EACf,UAAU,OAAO,EACjB,gBAAgB,WAAW,EAC3B,eAAe,WAAW,KACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CAcvB,CAAC"}
|
|
@@ -35,17 +35,37 @@ export const find_route_spec = (specs, method, path) => {
|
|
|
35
35
|
});
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* REST auth route suffixes still on the account/bootstrap surface after the
|
|
39
|
+
* 2026-04-22 RPC migration. `find_auth_route` rejects any other suffix at
|
|
40
|
+
* runtime — session/token CRUD, admin operations, and permit flows live on
|
|
41
|
+
* the RPC surface and should be reached via `rpc_call`.
|
|
42
|
+
*/
|
|
43
|
+
export const REST_AUTH_ROUTE_SUFFIXES = [
|
|
44
|
+
'/login',
|
|
45
|
+
'/logout',
|
|
46
|
+
'/password',
|
|
47
|
+
'/verify',
|
|
48
|
+
'/signup',
|
|
49
|
+
'/bootstrap',
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Find a REST auth route by suffix and method.
|
|
39
53
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
54
|
+
* Decouples tests from consumer route prefix (`/api/account/login`,
|
|
55
|
+
* `/api/auth/login`, etc.). `suffix` must be one of
|
|
56
|
+
* `REST_AUTH_ROUTE_SUFFIXES` — throws otherwise so a post-migration RPC
|
|
57
|
+
* method name (e.g. `/sessions/revoke-all`) fails loudly at the call site
|
|
58
|
+
* instead of silently returning `undefined`.
|
|
42
59
|
*
|
|
43
60
|
* @param specs - route specs to search
|
|
44
|
-
* @param suffix - path suffix
|
|
61
|
+
* @param suffix - REST auth path suffix
|
|
45
62
|
* @param method - HTTP method
|
|
46
63
|
* @returns matching route spec, or `undefined`
|
|
47
64
|
*/
|
|
48
65
|
export const find_auth_route = (specs, suffix, method) => {
|
|
66
|
+
if (!REST_AUTH_ROUTE_SUFFIXES.includes(suffix)) {
|
|
67
|
+
throw new Error(`find_auth_route: unknown suffix ${JSON.stringify(suffix)} — expected one of ${REST_AUTH_ROUTE_SUFFIXES.join(', ')}. Use rpc_call for RPC methods.`);
|
|
68
|
+
}
|
|
49
69
|
return specs.find((s) => s.method === method && s.path.endsWith(suffix));
|
|
50
70
|
};
|
|
51
71
|
/**
|
|
@@ -3,6 +3,7 @@ import type { SessionOptions } from '../auth/session_cookie.js';
|
|
|
3
3
|
import type { AppServerContext, AppServerOptions } from '../server/app_server.js';
|
|
4
4
|
import type { RouteSpec } from '../http/route_spec.js';
|
|
5
5
|
import { type DbFactory } from './db.js';
|
|
6
|
+
import type { RpcEndpointSpec } from '../http/surface.js';
|
|
6
7
|
/**
|
|
7
8
|
* Configuration for `describe_rate_limiting_tests`.
|
|
8
9
|
*/
|
|
@@ -22,6 +23,12 @@ export interface RateLimitingTestOptions {
|
|
|
22
23
|
* Default: `2` (tight limit for fast tests).
|
|
23
24
|
*/
|
|
24
25
|
max_attempts?: number;
|
|
26
|
+
/**
|
|
27
|
+
* RPC endpoint specs — required so the bearer-auth rate limiting test
|
|
28
|
+
* can probe an authenticated method via the `account_verify` RPC
|
|
29
|
+
* action. Hard-fails via `require_rpc_endpoint_path` on setup.
|
|
30
|
+
*/
|
|
31
|
+
rpc_endpoints: Array<RpcEndpointSpec>;
|
|
25
32
|
}
|
|
26
33
|
/**
|
|
27
34
|
* Standard rate limiting integration test suite.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate_limiting.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rate_limiting.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAiB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAKrD,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAKjB;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACvC,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF;;OAEG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"rate_limiting.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rate_limiting.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAiB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAKrD,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAKjB,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,oBAAoB,CAAC;AAGxD;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACvC,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF;;OAEG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,aAAa,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;CACtC;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,GAAI,SAAS,uBAAuB,KAAG,IAqP/E,CAAC"}
|
|
@@ -18,7 +18,9 @@ import { AUTH_MIGRATION_NS } from '../auth/migrations.js';
|
|
|
18
18
|
import { create_test_app } from './app_server.js';
|
|
19
19
|
import { create_pglite_factory, create_describe_db, AUTH_INTEGRATION_TRUNCATE_TABLES, } from './db.js';
|
|
20
20
|
import { find_auth_route, assert_rate_limit_retry_after_header } from './integration_helpers.js';
|
|
21
|
+
import { rpc_call_non_browser, require_rpc_endpoint_path } from './rpc_helpers.js';
|
|
21
22
|
import { run_migrations } from '../db/migrate.js';
|
|
23
|
+
import { account_verify_action_spec } from '../auth/account_action_specs.js';
|
|
22
24
|
/**
|
|
23
25
|
* Standard rate limiting integration test suite.
|
|
24
26
|
*
|
|
@@ -37,6 +39,9 @@ import { run_migrations } from '../db/migrate.js';
|
|
|
37
39
|
*/
|
|
38
40
|
export const describe_rate_limiting_tests = (options) => {
|
|
39
41
|
const max_attempts = options.max_attempts ?? 2;
|
|
42
|
+
// Hard-fail early so consumers see a clear setup error instead of a
|
|
43
|
+
// confusing test failure when `rpc_endpoints` is missing.
|
|
44
|
+
const rpc_path = require_rpc_endpoint_path(options.rpc_endpoints);
|
|
40
45
|
const init_schema = async (db) => {
|
|
41
46
|
await run_migrations(db, [AUTH_MIGRATION_NS]);
|
|
42
47
|
};
|
|
@@ -177,24 +182,50 @@ export const describe_rate_limiting_tests = (options) => {
|
|
|
177
182
|
bearer_ip_rate_limiter,
|
|
178
183
|
},
|
|
179
184
|
});
|
|
180
|
-
|
|
181
|
-
|
|
185
|
+
// Probe `account_verify` via RPC with an invalid bearer token.
|
|
186
|
+
// The REST `/api/account/verify` shim is status-only (empty body
|
|
187
|
+
// for nginx `auth_request`), so we use the RPC surface to exercise
|
|
188
|
+
// a typed authenticated method. The bearer_auth rate limiter
|
|
189
|
+
// increments per attempt regardless of the route's own auth outcome.
|
|
190
|
+
// Use `rpc_call_non_browser` so the default `origin` header is
|
|
191
|
+
// suppressed — bearer_auth discards the token when Origin or
|
|
192
|
+
// Referer is present (browser context), which would short-circuit
|
|
193
|
+
// before the rate limiter records the attempt.
|
|
194
|
+
//
|
|
195
|
+
// Note: the rate limiter short-circuits before the RPC dispatcher,
|
|
196
|
+
// so the 429 response is a REST-shaped `RateLimitError`, not a
|
|
197
|
+
// JSON-RPC envelope. We use the underlying `app.request` for the
|
|
198
|
+
// blocked probe so `rpc_call_non_browser` doesn't throw on the
|
|
199
|
+
// non-envelope body.
|
|
200
|
+
const bearer_probe_headers = {
|
|
201
|
+
authorization: 'Bearer secret_fuz_token_invalid',
|
|
202
|
+
};
|
|
182
203
|
// Fire max_attempts invalid bearer requests (sequential — must exhaust the window)
|
|
183
204
|
for (let i = 0; i < max_attempts; i++) {
|
|
184
|
-
const res = await
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
205
|
+
const res = await rpc_call_non_browser({
|
|
206
|
+
app: test_app.app,
|
|
207
|
+
path: rpc_path,
|
|
208
|
+
method: account_verify_action_spec.method,
|
|
209
|
+
id: 'rl-probe',
|
|
210
|
+
headers: bearer_probe_headers,
|
|
189
211
|
});
|
|
190
212
|
assert.notStrictEqual(res.status, 429, `Request ${i + 1}/${max_attempts} should not be rate limited`);
|
|
191
213
|
}
|
|
192
|
-
// The next request should be rate limited
|
|
193
|
-
|
|
214
|
+
// The next request should be rate limited. The 429 body is REST-shape
|
|
215
|
+
// (middleware short-circuits before the RPC dispatcher), so go
|
|
216
|
+
// direct — `rpc_call_non_browser` would throw on the non-envelope body.
|
|
217
|
+
const blocked_res = await test_app.app.request(rpc_path, {
|
|
218
|
+
method: 'POST',
|
|
194
219
|
headers: {
|
|
195
220
|
host: 'localhost',
|
|
196
|
-
|
|
221
|
+
'content-type': 'application/json',
|
|
222
|
+
...bearer_probe_headers,
|
|
197
223
|
},
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
jsonrpc: '2.0',
|
|
226
|
+
method: account_verify_action_spec.method,
|
|
227
|
+
id: 'rl-probe-blocked',
|
|
228
|
+
}),
|
|
198
229
|
});
|
|
199
230
|
assert.strictEqual(blocked_res.status, 429);
|
|
200
231
|
const body = await blocked_res.json();
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import './assert_dev_env.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { type JsonrpcErrorCode } from '../http/jsonrpc.js';
|
|
4
|
+
import type { RequestResponseActionSpec } from '../actions/action_spec.js';
|
|
5
|
+
import type { RpcAction } from '../actions/action_rpc.js';
|
|
6
|
+
import type { AppSurfaceRpcEndpoint, AppSurfaceRpcMethod, RpcEndpointSpec } from '../http/surface.js';
|
|
4
7
|
/**
|
|
5
8
|
* Create a `RequestInit` for a JSON-RPC POST request.
|
|
6
9
|
*
|
|
7
10
|
* @param method - JSON-RPC method name
|
|
8
|
-
* @param params - params object (omit for null-input methods
|
|
11
|
+
* @param params - params object (omit or pass `null` for null-input methods;
|
|
12
|
+
* both are serialized without a `params` field so the envelope
|
|
13
|
+
* schema accepts the request — `"params":null` is not a valid
|
|
14
|
+
* JSON-RPC value)
|
|
9
15
|
* @param id - request id (default `'test'`)
|
|
10
16
|
* @returns a `RequestInit` with the JSON-RPC envelope as body
|
|
11
17
|
*/
|
|
@@ -41,4 +47,150 @@ export declare const assert_jsonrpc_error_response: (body: unknown, expected_cod
|
|
|
41
47
|
* @param output_schema - optional Zod schema to validate the `result` field against
|
|
42
48
|
*/
|
|
43
49
|
export declare const assert_jsonrpc_success_response: (body: unknown, output_schema?: z.ZodType) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Minimal transport surface — the duck type `Hono.request` already satisfies.
|
|
52
|
+
* Extracted so test setups that want an in-process / WS / mock path can plug
|
|
53
|
+
* a different dispatcher without changing call sites.
|
|
54
|
+
*/
|
|
55
|
+
export type RpcTestTransport = (url: string, init: RequestInit) => Promise<Response>;
|
|
56
|
+
/** Adapt a `Hono`-style app into an `RpcTestTransport`. */
|
|
57
|
+
export declare const http_transport: (app: {
|
|
58
|
+
request: (input: string, init: RequestInit) => Promise<Response> | Response;
|
|
59
|
+
}) => RpcTestTransport;
|
|
60
|
+
/** Discriminated return from `rpc_call`. `status` is the HTTP status. */
|
|
61
|
+
export type RpcCallResult = {
|
|
62
|
+
ok: true;
|
|
63
|
+
status: number;
|
|
64
|
+
result: unknown;
|
|
65
|
+
} | {
|
|
66
|
+
ok: false;
|
|
67
|
+
status: number;
|
|
68
|
+
error: {
|
|
69
|
+
code: number;
|
|
70
|
+
message: string;
|
|
71
|
+
data?: unknown;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
/** Arguments for `rpc_call`. */
|
|
75
|
+
export interface RpcCallArgs {
|
|
76
|
+
/** Hono-like app (anything with a `request(url, init)` method). */
|
|
77
|
+
app: {
|
|
78
|
+
request: (input: string, init: RequestInit) => Promise<Response> | Response;
|
|
79
|
+
};
|
|
80
|
+
/** RPC endpoint path, e.g. `'/api/rpc'`. */
|
|
81
|
+
path: string;
|
|
82
|
+
/** JSON-RPC method name. */
|
|
83
|
+
method: string;
|
|
84
|
+
/** Params object. Omit or pass `null` for null-input methods. */
|
|
85
|
+
params?: unknown;
|
|
86
|
+
/** Extra request headers (session cookie, bearer, etc.). Overrides defaults. */
|
|
87
|
+
headers?: Record<string, string>;
|
|
88
|
+
/** Request id. Defaults to `'test'`. */
|
|
89
|
+
id?: string | number;
|
|
90
|
+
/** HTTP verb — `'POST'` (default) or `'GET'` for `side_effects: false` methods. */
|
|
91
|
+
verb?: 'POST' | 'GET';
|
|
92
|
+
/**
|
|
93
|
+
* Suppress the default `origin` header. Required for bearer-auth paths:
|
|
94
|
+
* `bearer_auth` discards the token when Origin or Referer is present
|
|
95
|
+
* (browser context), so probing it via `rpc_call` needs this flag — or
|
|
96
|
+
* use `rpc_call_non_browser`, which sets it for you.
|
|
97
|
+
*/
|
|
98
|
+
suppress_default_origin?: boolean;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* One-shot JSON-RPC call over a Hono app.
|
|
102
|
+
*
|
|
103
|
+
* Merges sensible defaults (`host`, `origin`, `Content-Type`) under
|
|
104
|
+
* caller-provided headers, fires POST (default) or GET, parses the envelope,
|
|
105
|
+
* and returns a discriminated result.
|
|
106
|
+
*
|
|
107
|
+
* Throws `Error` only on envelope-shape violations (neither
|
|
108
|
+
* `JsonrpcResponse` nor `JsonrpcErrorResponse` parses) — protocol-level
|
|
109
|
+
* failures the caller should never tolerate. All JSON-RPC errors come back
|
|
110
|
+
* via `{ok: false, error}` so assertions can focus on `error.code` /
|
|
111
|
+
* `error.data.reason`.
|
|
112
|
+
*/
|
|
113
|
+
export declare const rpc_call: (args: RpcCallArgs) => Promise<RpcCallResult>;
|
|
114
|
+
/**
|
|
115
|
+
* Same as `rpc_call` but without the default `origin` header. Use for
|
|
116
|
+
* bearer-auth probes: `bearer_auth` discards the token when Origin or
|
|
117
|
+
* Referer is present (browser context), so a bearer probe via `rpc_call`
|
|
118
|
+
* would short-circuit to 401 before the token is ever validated.
|
|
119
|
+
*
|
|
120
|
+
* Equivalent to `rpc_call({...args, suppress_default_origin: true})`.
|
|
121
|
+
*/
|
|
122
|
+
export declare const rpc_call_non_browser: (args: Omit<RpcCallArgs, "suppress_default_origin">) => Promise<RpcCallResult>;
|
|
123
|
+
/**
|
|
124
|
+
* Typed discriminated result returned by `rpc_call_for_spec`. The success
|
|
125
|
+
* branch's `result` is inferred from `TSpec['output']`. The error branch
|
|
126
|
+
* stays untyped because JSON-RPC `error.data` shapes vary per error and
|
|
127
|
+
* are asserted per call site.
|
|
128
|
+
*/
|
|
129
|
+
export type RpcCallResultForSpec<TSpec extends RequestResponseActionSpec> = {
|
|
130
|
+
ok: true;
|
|
131
|
+
status: number;
|
|
132
|
+
result: z.infer<TSpec['output']>;
|
|
133
|
+
} | {
|
|
134
|
+
ok: false;
|
|
135
|
+
status: number;
|
|
136
|
+
error: {
|
|
137
|
+
code: number;
|
|
138
|
+
message: string;
|
|
139
|
+
data?: unknown;
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
/** Arguments for `rpc_call_for_spec`. `spec` replaces the loose `method` field. */
|
|
143
|
+
export type RpcCallForSpecArgs<TSpec extends RequestResponseActionSpec> = Omit<RpcCallArgs, 'method' | 'params'> & {
|
|
144
|
+
/** Action spec whose `method` drives the envelope and whose `input`/`output` types pin params + result. */
|
|
145
|
+
spec: TSpec;
|
|
146
|
+
/** Params, typed against `spec.input`. */
|
|
147
|
+
params: z.infer<TSpec['input']>;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Typed wrapper over `rpc_call` — binds `params` to `z.infer<spec.input>`
|
|
151
|
+
* and the success `result` to `z.infer<spec.output>` via the generic.
|
|
152
|
+
*
|
|
153
|
+
* Success results are validated at runtime against `spec.output` (same
|
|
154
|
+
* contract as `rpc_call_typed`); a mismatch throws. Error responses come
|
|
155
|
+
* back on the discriminated `{ok: false, error}` branch — use this for
|
|
156
|
+
* happy-path + denial-path assertions where the error `data.reason` shape
|
|
157
|
+
* is still asserted manually. For adversarial input tests that send
|
|
158
|
+
* malformed params, use the untyped `rpc_call`.
|
|
159
|
+
*/
|
|
160
|
+
export declare const rpc_call_for_spec: <TSpec extends RequestResponseActionSpec>(args: RpcCallForSpecArgs<TSpec>) => Promise<RpcCallResultForSpec<TSpec>>;
|
|
161
|
+
/**
|
|
162
|
+
* Same as `rpc_call` but parses the success `result` through the given
|
|
163
|
+
* output schema and returns typed data. Envelope-level failures or error
|
|
164
|
+
* responses throw — use the untyped `rpc_call` for tests that need to
|
|
165
|
+
* assert on specific error shapes.
|
|
166
|
+
*/
|
|
167
|
+
export declare const rpc_call_typed: <T>(args: RpcCallArgs, output_schema: z.ZodType<T>) => Promise<T>;
|
|
168
|
+
/**
|
|
169
|
+
* Find the `RpcAction` for a method within a set of RPC endpoint specs.
|
|
170
|
+
* Returns both the endpoint path and the matched action. `undefined` when
|
|
171
|
+
* the method is not registered.
|
|
172
|
+
*/
|
|
173
|
+
export declare const find_rpc_action: (rpc_endpoints: ReadonlyArray<RpcEndpointSpec>, method: string) => {
|
|
174
|
+
path: string;
|
|
175
|
+
action: RpcAction;
|
|
176
|
+
} | undefined;
|
|
177
|
+
/**
|
|
178
|
+
* Find the generated surface entry for a method — the shape returned by
|
|
179
|
+
* `generate_app_surface` (JSON-serializable, useful for schema assertions
|
|
180
|
+
* at the boundary of a consumer test).
|
|
181
|
+
*/
|
|
182
|
+
export declare const find_rpc_method: (rpc_endpoints: ReadonlyArray<AppSurfaceRpcEndpoint>, method: string) => {
|
|
183
|
+
path: string;
|
|
184
|
+
method_spec: AppSurfaceRpcMethod;
|
|
185
|
+
} | undefined;
|
|
186
|
+
/**
|
|
187
|
+
* Resolve a single RPC endpoint path — the common case where a consumer
|
|
188
|
+
* mounts exactly one `create_rpc_endpoint`. Throws when `rpc_endpoints` is
|
|
189
|
+
* empty (hard-fail; see the suite options docs) or ambiguous (more than one
|
|
190
|
+
* endpoint registered).
|
|
191
|
+
*
|
|
192
|
+
* Callers that need multi-endpoint support should iterate `rpc_endpoints`
|
|
193
|
+
* directly.
|
|
194
|
+
*/
|
|
195
|
+
export declare const require_rpc_endpoint_path: (rpc_endpoints: ReadonlyArray<RpcEndpointSpec>) => string;
|
|
44
196
|
//# sourceMappingURL=rpc_helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"rpc_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_helpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import './assert_dev_env.js';
|
|
2
2
|
/**
|
|
3
|
-
* JSON-RPC request construction
|
|
3
|
+
* JSON-RPC test helpers — request construction, response assertion, and
|
|
4
|
+
* one-shot call ergonomics.
|
|
4
5
|
*
|
|
5
|
-
* Shared by `rpc_attack_surface.ts
|
|
6
|
+
* Shared by `rpc_attack_surface.ts`, `rpc_round_trip.ts`, and
|
|
7
|
+
* consumer-facing admin/audit suites that exercise RPC methods directly.
|
|
6
8
|
*
|
|
7
9
|
* @module
|
|
8
10
|
*/
|
|
@@ -13,15 +15,23 @@ import { JSONRPC_VERSION, JsonrpcErrorResponse, JsonrpcResponse, } from '../http
|
|
|
13
15
|
* Create a `RequestInit` for a JSON-RPC POST request.
|
|
14
16
|
*
|
|
15
17
|
* @param method - JSON-RPC method name
|
|
16
|
-
* @param params - params object (omit for null-input methods
|
|
18
|
+
* @param params - params object (omit or pass `null` for null-input methods;
|
|
19
|
+
* both are serialized without a `params` field so the envelope
|
|
20
|
+
* schema accepts the request — `"params":null` is not a valid
|
|
21
|
+
* JSON-RPC value)
|
|
17
22
|
* @param id - request id (default `'test'`)
|
|
18
23
|
* @returns a `RequestInit` with the JSON-RPC envelope as body
|
|
19
24
|
*/
|
|
20
|
-
export const create_rpc_post_init = (method, params, id = 'test') =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
export const create_rpc_post_init = (method, params, id = 'test') => {
|
|
26
|
+
const envelope = { jsonrpc: JSONRPC_VERSION, method, id };
|
|
27
|
+
if (params !== undefined && params !== null)
|
|
28
|
+
envelope.params = params;
|
|
29
|
+
return {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify(envelope),
|
|
33
|
+
};
|
|
34
|
+
};
|
|
25
35
|
/**
|
|
26
36
|
* Build a GET URL with JSON-RPC query parameters.
|
|
27
37
|
*
|
|
@@ -72,3 +82,169 @@ export const assert_jsonrpc_success_response = (body, output_schema) => {
|
|
|
72
82
|
assert.ok(output_result.success, `JSON-RPC result does not match output schema: ${JSON.stringify(output_result.error?.issues)}`);
|
|
73
83
|
}
|
|
74
84
|
};
|
|
85
|
+
/** Adapt a `Hono`-style app into an `RpcTestTransport`. */
|
|
86
|
+
export const http_transport = (app) => async (url, init) => app.request(url, init);
|
|
87
|
+
/** Base default headers merged into every `rpc_call` request. */
|
|
88
|
+
const RPC_CALL_DEFAULT_HEADERS_BASE = {
|
|
89
|
+
host: 'localhost',
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
};
|
|
92
|
+
/** Default headers merged into every `rpc_call` request. Includes `origin`. */
|
|
93
|
+
const RPC_CALL_DEFAULT_HEADERS = {
|
|
94
|
+
...RPC_CALL_DEFAULT_HEADERS_BASE,
|
|
95
|
+
origin: 'http://localhost:5173',
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* One-shot JSON-RPC call over a Hono app.
|
|
99
|
+
*
|
|
100
|
+
* Merges sensible defaults (`host`, `origin`, `Content-Type`) under
|
|
101
|
+
* caller-provided headers, fires POST (default) or GET, parses the envelope,
|
|
102
|
+
* and returns a discriminated result.
|
|
103
|
+
*
|
|
104
|
+
* Throws `Error` only on envelope-shape violations (neither
|
|
105
|
+
* `JsonrpcResponse` nor `JsonrpcErrorResponse` parses) — protocol-level
|
|
106
|
+
* failures the caller should never tolerate. All JSON-RPC errors come back
|
|
107
|
+
* via `{ok: false, error}` so assertions can focus on `error.code` /
|
|
108
|
+
* `error.data.reason`.
|
|
109
|
+
*/
|
|
110
|
+
export const rpc_call = async (args) => {
|
|
111
|
+
const { app, path, method, params, headers, id = 'test', verb = 'POST', suppress_default_origin, } = args;
|
|
112
|
+
const defaults = suppress_default_origin
|
|
113
|
+
? RPC_CALL_DEFAULT_HEADERS_BASE
|
|
114
|
+
: RPC_CALL_DEFAULT_HEADERS;
|
|
115
|
+
let url;
|
|
116
|
+
let init;
|
|
117
|
+
if (verb === 'GET') {
|
|
118
|
+
url = create_rpc_get_url(path, method, params, id);
|
|
119
|
+
init = { method: 'GET', headers: { ...defaults, ...headers } };
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
url = path;
|
|
123
|
+
const post = create_rpc_post_init(method, params, id);
|
|
124
|
+
init = {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: {
|
|
127
|
+
...defaults,
|
|
128
|
+
...post.headers,
|
|
129
|
+
...headers,
|
|
130
|
+
},
|
|
131
|
+
body: post.body,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const res = await app.request(url, init);
|
|
135
|
+
const status = res.status;
|
|
136
|
+
const body = await res.json();
|
|
137
|
+
const success = JsonrpcResponse.safeParse(body);
|
|
138
|
+
if (success.success) {
|
|
139
|
+
return { ok: true, status, result: success.data.result };
|
|
140
|
+
}
|
|
141
|
+
const error = JsonrpcErrorResponse.safeParse(body);
|
|
142
|
+
if (error.success) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
status,
|
|
146
|
+
error: {
|
|
147
|
+
code: error.data.error.code,
|
|
148
|
+
message: error.data.error.message,
|
|
149
|
+
data: error.data.error.data,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`rpc_call: response is not a valid JSON-RPC envelope (method=${method}, status=${status}): ${JSON.stringify(body)}`);
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Same as `rpc_call` but without the default `origin` header. Use for
|
|
157
|
+
* bearer-auth probes: `bearer_auth` discards the token when Origin or
|
|
158
|
+
* Referer is present (browser context), so a bearer probe via `rpc_call`
|
|
159
|
+
* would short-circuit to 401 before the token is ever validated.
|
|
160
|
+
*
|
|
161
|
+
* Equivalent to `rpc_call({...args, suppress_default_origin: true})`.
|
|
162
|
+
*/
|
|
163
|
+
export const rpc_call_non_browser = (args) => rpc_call({ ...args, suppress_default_origin: true });
|
|
164
|
+
/**
|
|
165
|
+
* Typed wrapper over `rpc_call` — binds `params` to `z.infer<spec.input>`
|
|
166
|
+
* and the success `result` to `z.infer<spec.output>` via the generic.
|
|
167
|
+
*
|
|
168
|
+
* Success results are validated at runtime against `spec.output` (same
|
|
169
|
+
* contract as `rpc_call_typed`); a mismatch throws. Error responses come
|
|
170
|
+
* back on the discriminated `{ok: false, error}` branch — use this for
|
|
171
|
+
* happy-path + denial-path assertions where the error `data.reason` shape
|
|
172
|
+
* is still asserted manually. For adversarial input tests that send
|
|
173
|
+
* malformed params, use the untyped `rpc_call`.
|
|
174
|
+
*/
|
|
175
|
+
export const rpc_call_for_spec = async (args) => {
|
|
176
|
+
const { spec, params, ...rest } = args;
|
|
177
|
+
const res = await rpc_call({ ...rest, method: spec.method, params });
|
|
178
|
+
if (!res.ok) {
|
|
179
|
+
return res;
|
|
180
|
+
}
|
|
181
|
+
const parsed = spec.output.safeParse(res.result);
|
|
182
|
+
if (!parsed.success) {
|
|
183
|
+
throw new Error(`rpc_call_for_spec(${spec.method}) result did not match spec.output: ${JSON.stringify(parsed.error.issues)}`);
|
|
184
|
+
}
|
|
185
|
+
return { ok: true, status: res.status, result: parsed.data };
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Same as `rpc_call` but parses the success `result` through the given
|
|
189
|
+
* output schema and returns typed data. Envelope-level failures or error
|
|
190
|
+
* responses throw — use the untyped `rpc_call` for tests that need to
|
|
191
|
+
* assert on specific error shapes.
|
|
192
|
+
*/
|
|
193
|
+
export const rpc_call_typed = async (args, output_schema) => {
|
|
194
|
+
const res = await rpc_call(args);
|
|
195
|
+
if (!res.ok) {
|
|
196
|
+
throw new Error(`rpc_call_typed(${args.method}) returned error: code=${res.error.code} message=${res.error.message} data=${JSON.stringify(res.error.data)}`);
|
|
197
|
+
}
|
|
198
|
+
const parsed = output_schema.safeParse(res.result);
|
|
199
|
+
if (!parsed.success) {
|
|
200
|
+
throw new Error(`rpc_call_typed(${args.method}) result did not match output schema: ${JSON.stringify(parsed.error.issues)}`);
|
|
201
|
+
}
|
|
202
|
+
return parsed.data;
|
|
203
|
+
};
|
|
204
|
+
// -- registry/surface lookup helpers -----------------------------------------
|
|
205
|
+
/**
|
|
206
|
+
* Find the `RpcAction` for a method within a set of RPC endpoint specs.
|
|
207
|
+
* Returns both the endpoint path and the matched action. `undefined` when
|
|
208
|
+
* the method is not registered.
|
|
209
|
+
*/
|
|
210
|
+
export const find_rpc_action = (rpc_endpoints, method) => {
|
|
211
|
+
for (const ep of rpc_endpoints) {
|
|
212
|
+
for (const action of ep.actions) {
|
|
213
|
+
if (action.spec.method === method)
|
|
214
|
+
return { path: ep.path, action };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Find the generated surface entry for a method — the shape returned by
|
|
221
|
+
* `generate_app_surface` (JSON-serializable, useful for schema assertions
|
|
222
|
+
* at the boundary of a consumer test).
|
|
223
|
+
*/
|
|
224
|
+
export const find_rpc_method = (rpc_endpoints, method) => {
|
|
225
|
+
for (const ep of rpc_endpoints) {
|
|
226
|
+
for (const method_spec of ep.methods) {
|
|
227
|
+
if (method_spec.name === method)
|
|
228
|
+
return { path: ep.path, method_spec };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* Resolve a single RPC endpoint path — the common case where a consumer
|
|
235
|
+
* mounts exactly one `create_rpc_endpoint`. Throws when `rpc_endpoints` is
|
|
236
|
+
* empty (hard-fail; see the suite options docs) or ambiguous (more than one
|
|
237
|
+
* endpoint registered).
|
|
238
|
+
*
|
|
239
|
+
* Callers that need multi-endpoint support should iterate `rpc_endpoints`
|
|
240
|
+
* directly.
|
|
241
|
+
*/
|
|
242
|
+
export const require_rpc_endpoint_path = (rpc_endpoints) => {
|
|
243
|
+
if (rpc_endpoints.length === 0) {
|
|
244
|
+
throw new Error('rpc_endpoints is empty — the admin/audit integration suites require an RPC endpoint. Pass `rpc_endpoints` on the suite options.');
|
|
245
|
+
}
|
|
246
|
+
if (rpc_endpoints.length > 1) {
|
|
247
|
+
throw new Error(`rpc_endpoints has ${rpc_endpoints.length} entries; this helper expects exactly one. Iterate rpc_endpoints manually for multi-endpoint setups.`);
|
|
248
|
+
}
|
|
249
|
+
return rpc_endpoints[0].path;
|
|
250
|
+
};
|
|
@@ -6,6 +6,7 @@ import { type EventSpec } from '../realtime/sse.js';
|
|
|
6
6
|
import type { AuditLogEvent } from '../auth/audit_log_schema.js';
|
|
7
7
|
import { type TestApp, type TestAccount } from './app_server.js';
|
|
8
8
|
import { type DbFactory } from './db.js';
|
|
9
|
+
import type { RpcEndpointSpec } from '../http/surface.js';
|
|
9
10
|
/** Config for a single SSE route under test. */
|
|
10
11
|
export interface SseRouteTestSpec {
|
|
11
12
|
/** Full HTTP path of the SSE endpoint (e.g., `'/api/tx/subscribe'`). */
|
|
@@ -48,6 +49,13 @@ export interface SseRouteTestOptions {
|
|
|
48
49
|
* closes the tested streams.
|
|
49
50
|
*/
|
|
50
51
|
on_audit_event?: (event: AuditLogEvent) => void;
|
|
52
|
+
/**
|
|
53
|
+
* RPC endpoint specs — required so the close-on-revoke assertion can
|
|
54
|
+
* dispatch `account_session_revoke_all` via RPC (the former REST route
|
|
55
|
+
* `POST /api/account/sessions/revoke-all` was removed in the 2026-04-23
|
|
56
|
+
* migration). Hard-fails via `require_rpc_endpoint_path` on setup.
|
|
57
|
+
*/
|
|
58
|
+
rpc_endpoints: Array<RpcEndpointSpec>;
|
|
51
59
|
/** SSE routes to exercise. */
|
|
52
60
|
routes: Array<SseRouteTestSpec>;
|
|
53
61
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/sse_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAmB7B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAwB,KAAK,SAAS,EAAuB,MAAM,oBAAoB,CAAC;AAE/F,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAkB,KAAK,OAAO,EAAE,KAAK,WAAW,EAAC,MAAM,iBAAiB,CAAC;AAChF,OAAO,EAAwB,KAAK,SAAS,EAAC,MAAM,SAAS,CAAC;AAM9D,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAChC,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,OAAO,EAAE,CAAC,GAAG,EAAE;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,WAAW,CAAA;KAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E;;;OAGG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC/B;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,8CAA8C;AAC9C,MAAM,WAAW,mBAAmB;IACnC,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,qDAAqD;IACrD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAChD,8BAA8B;IAC9B,MAAM,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;CAChC;AAyHD;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS,mBAAmB,KAAG,
|
|
1
|
+
{"version":3,"file":"sse_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/sse_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAmB7B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAwB,KAAK,SAAS,EAAuB,MAAM,oBAAoB,CAAC;AAE/F,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAkB,KAAK,OAAO,EAAE,KAAK,WAAW,EAAC,MAAM,iBAAiB,CAAC;AAChF,OAAO,EAAwB,KAAK,SAAS,EAAC,MAAM,SAAS,CAAC;AAM9D,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,oBAAoB,CAAC;AAGxD,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAChC,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,OAAO,EAAE,CAAC,GAAG,EAAE;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,WAAW,CAAA;KAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E;;;OAGG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC/B;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,8CAA8C;AAC9C,MAAM,WAAW,mBAAmB;IACnC,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,qDAAqD;IACrD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAChD;;;;;OAKG;IACH,aAAa,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;IACtC,8BAA8B;IAC9B,MAAM,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;CAChC;AAyHD;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS,mBAAmB,KAAG,IAkHvE,CAAC"}
|