@fuzdev/fuz_app 0.42.0 → 0.44.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 +77 -0
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +14 -7
- package/dist/actions/frontend_rpc_client.d.ts +74 -0
- package/dist/actions/frontend_rpc_client.d.ts.map +1 -0
- package/dist/actions/frontend_rpc_client.js +61 -0
- package/dist/actions/rpc_client.d.ts +64 -0
- package/dist/actions/rpc_client.d.ts.map +1 -1
- package/dist/actions/rpc_client.js +77 -0
- package/dist/auth/CLAUDE.md +32 -21
- package/dist/auth/account_action_specs.d.ts +8 -8
- package/dist/auth/account_action_specs.js +4 -4
- package/dist/auth/admin_action_specs.d.ts +8 -8
- package/dist/auth/admin_action_specs.js +4 -4
- package/dist/auth/self_service_role_action_specs.d.ts +20 -48
- package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
- package/dist/auth/self_service_role_action_specs.js +22 -44
- package/dist/auth/self_service_role_actions.d.ts +9 -9
- package/dist/auth/self_service_role_actions.d.ts.map +1 -1
- package/dist/auth/self_service_role_actions.js +48 -53
- package/dist/auth/standard_action_specs.d.ts +31 -0
- package/dist/auth/standard_action_specs.d.ts.map +1 -0
- package/dist/auth/standard_action_specs.js +36 -0
- package/dist/http/schema_helpers.d.ts +9 -0
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +9 -0
- package/dist/testing/admin_integration.js +9 -9
- package/dist/testing/audit_completeness.js +3 -3
- package/dist/testing/integration.js +36 -36
- package/dist/testing/rpc_helpers.d.ts +14 -6
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +8 -5
- package/dist/ui/admin_rpc_adapters.js +4 -4
- package/package.json +1 -1
package/dist/actions/CLAUDE.md
CHANGED
|
@@ -591,6 +591,83 @@ Cast the return to a generated `ActionsApi` interface for full typing:
|
|
|
591
591
|
codegen via `generate_actions_api_method_signature` keeps the shape
|
|
592
592
|
consistent. See ../../docs/usage.md §Typed Client Codegen.
|
|
593
593
|
|
|
594
|
+
### Throwing variants — `create_throwing_rpc_call` + `create_throwing_api`
|
|
595
|
+
|
|
596
|
+
Two helpers wrap a typed `create_rpc_client` Proxy so `{ok: false}` results
|
|
597
|
+
throw an `Error` with `{code, message, data?}` (catch blocks read
|
|
598
|
+
`err.data?.reason` — optional chaining required because JSON-RPC `data`
|
|
599
|
+
is spec-level optional). Same hardening on both: only `{code, data}` cross
|
|
600
|
+
onto the Error, leaving `name` / `stack` as the native Error's own so
|
|
601
|
+
attacker-shaped `result.error` payloads cannot overwrite them.
|
|
602
|
+
|
|
603
|
+
| Helper | Shape | Use at |
|
|
604
|
+
| -------------------------- | -------------------------------- | -------------------------------------------------------------------------- |
|
|
605
|
+
| `create_throwing_rpc_call` | `(method, input?) => Promise<T>` | adapter wiring (e.g. `ui/admin_rpc_adapters.ts`) — method comes from a map |
|
|
606
|
+
| `create_throwing_api` | typed Proxy over `ActionsApi` | direct call sites — `await api.foo(input)` keeps full inference |
|
|
607
|
+
|
|
608
|
+
**Recommended consumer convention.** The throwing form is the common
|
|
609
|
+
case at call sites; the Result form is the composable escape hatch for
|
|
610
|
+
sites that want to inspect `error.data.reason` without try/catch. Bind
|
|
611
|
+
them as `api` (throwing wrapper) and `api_raw` (the unwrapped
|
|
612
|
+
underlying, returning Results):
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
const api_raw = create_rpc_client({peer, environment}) as unknown as MyActionsApi;
|
|
616
|
+
const api = create_throwing_api(api_raw);
|
|
617
|
+
// hot path: await api.foo(input)
|
|
618
|
+
// rare branch: const r = await api_raw.foo(input); if (!r.ok) { … }
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
Composable — feed the same typed Proxy into both: the loose method-keyed
|
|
622
|
+
form for adapter dispatch, the typed Proxy form for hand-written call
|
|
623
|
+
sites. `ThrowingApi<TApi>` mapped type strips
|
|
624
|
+
`Promise<Result<{value: T}, {error: JsonrpcErrorObject}>>` to `Promise<T>`
|
|
625
|
+
on every method that matches the `request_response` / async `local_call`
|
|
626
|
+
return shape; `remote_notification` (`=> void`) and sync `local_call`
|
|
627
|
+
methods pass through. The Proxy implementation inspects each call's
|
|
628
|
+
result shape at runtime and only unwraps when it sees a Result, so
|
|
629
|
+
non-Result returns flow through unchanged.
|
|
630
|
+
|
|
631
|
+
Both helpers throw `"rpc method not found: <name>"` on invocation of an
|
|
632
|
+
unknown method. For `create_throwing_api` the thrower is returned from
|
|
633
|
+
the Proxy get trap so `api.missing()` errors with the same clear
|
|
634
|
+
message rather than the JS default `"api.missing is not a function"`.
|
|
635
|
+
Symbol props and `then` stay `undefined` so the Proxy doesn't get
|
|
636
|
+
probed as a thenable by `await`.
|
|
637
|
+
|
|
638
|
+
### Frontend factory (`frontend_rpc_client.ts`)
|
|
639
|
+
|
|
640
|
+
`create_frontend_rpc_client<TApi>({specs, path?, transports?})` bundles
|
|
641
|
+
the `ActionRegistry + ActionEventEnvironment + Transports + ActionPeer +
|
|
642
|
+
create_rpc_client` boilerplate every consumer repeats — plus the
|
|
643
|
+
`lookup_action_handler: () => undefined` stub (frontend never registers
|
|
644
|
+
`request_response` handlers; every method dispatches over the wire).
|
|
645
|
+
The `as unknown as TApi` cast happens inside the helper, so call sites
|
|
646
|
+
get a typed return without the cast hostility. Returns
|
|
647
|
+
`{api, peer, environment}` so advanced consumers (zzz-style frontends
|
|
648
|
+
needing extra transports / WS notification handlers / action-history
|
|
649
|
+
wiring) can extend without recreating the bundle.
|
|
650
|
+
|
|
651
|
+
Default transport is `FrontendHttpTransport(path ?? '/api/rpc')`. Pass
|
|
652
|
+
`transports` for WS-first or mixed setups — when supplied, the default
|
|
653
|
+
HTTP transport is **not** registered. `local_call` specs in `specs`
|
|
654
|
+
silently no-op because `lookup_action_handler` always returns
|
|
655
|
+
`undefined`; this factory targets wire-dispatched actions.
|
|
656
|
+
|
|
657
|
+
Pair with `create_throwing_api` to land the recommended `api` /
|
|
658
|
+
`api_raw` convention in two lines:
|
|
659
|
+
|
|
660
|
+
```ts
|
|
661
|
+
const {api: api_raw} = create_frontend_rpc_client<MyActionsApi>({
|
|
662
|
+
specs: all_standard_action_specs,
|
|
663
|
+
});
|
|
664
|
+
const api = create_throwing_api(api_raw);
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
`all_standard_action_specs` (in `../auth/standard_action_specs.ts`) is
|
|
668
|
+
the matching aggregate spec list mirroring `create_standard_rpc_actions`
|
|
669
|
+
on the backend — see `../auth/CLAUDE.md` §`standard_rpc_actions.ts`.
|
|
670
|
+
|
|
594
671
|
## Broadcast API (`broadcast_api.ts`)
|
|
595
672
|
|
|
596
673
|
`create_broadcast_api({peer, specs, log?, should_deliver?})` — builds a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action_rpc.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAoB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAE9F,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAEpC,OAAO,EAGN,KAAK,gBAAgB,EAGrB,MAAM,oBAAoB,CAAC;AAW5B;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC7B,+DAA+D;IAC/D,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;IAC5B,iDAAiD;IACjD,UAAU,EAAE,gBAAgB,CAAC;IAC7B,8DAA8D;IAC9D,EAAE,EAAE,EAAE,CAAC;IACP,oFAAoF;IACpF,aAAa,EAAE,EAAE,CAAC;IAClB,2EAA2E;IAC3E,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACtC;;;;;;;OAOG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;;OAQG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;CACpB;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,CAAC,MAAM,GAAG,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,CACxD,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,yBAAyB,CAAC;IAChC,OAAO,EAAE,aAAa,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,SAAS,yBAAyB,EACjE,MAAM,KAAK,EACX,SAAS,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,KACvE,SAGD,CAAC;AAEH,yCAAyC;AACzC,MAAM,WAAW,wBAAwB;IACxC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC1B,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;CACZ;AA4DD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS,wBAAwB,KAAG,KAAK,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"action_rpc.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAoB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAE9F,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAEpC,OAAO,EAGN,KAAK,gBAAgB,EAGrB,MAAM,oBAAoB,CAAC;AAW5B;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC7B,+DAA+D;IAC/D,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;IAC5B,iDAAiD;IACjD,UAAU,EAAE,gBAAgB,CAAC;IAC7B,8DAA8D;IAC9D,EAAE,EAAE,EAAE,CAAC;IACP,oFAAoF;IACpF,aAAa,EAAE,EAAE,CAAC;IAClB,2EAA2E;IAC3E,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACtC;;;;;;;OAOG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;;OAQG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;CACpB;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,CAAC,MAAM,GAAG,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,CACxD,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,yBAAyB,CAAC;IAChC,OAAO,EAAE,aAAa,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,SAAS,yBAAyB,EACjE,MAAM,KAAK,EACX,SAAS,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,KACvE,SAGD,CAAC;AAEH,yCAAyC;AACzC,MAAM,WAAW,wBAAwB;IACxC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC1B,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;CACZ;AA4DD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS,wBAAwB,KAAG,KAAK,CAAC,SAAS,CAsQtF,CAAC"}
|
|
@@ -17,7 +17,7 @@ import {} from '../http/route_spec.js';
|
|
|
17
17
|
import { get_client_ip } from '../http/proxy.js';
|
|
18
18
|
import { get_request_context, has_role } from '../auth/request_context.js';
|
|
19
19
|
import { CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
20
|
-
import { is_null_schema } from '../http/schema_helpers.js';
|
|
20
|
+
import { is_null_schema, is_void_schema } from '../http/schema_helpers.js';
|
|
21
21
|
import { JSONRPC_VERSION, JsonrpcRequest, } from '../http/jsonrpc.js';
|
|
22
22
|
import { jsonrpc_error_messages, jsonrpc_error_code_to_http_status, JSONRPC_ERROR_CODES, } from '../http/jsonrpc_errors.js';
|
|
23
23
|
import { ERROR_INSUFFICIENT_PERMISSIONS, ERROR_KEEPER_REQUIRES_DAEMON_TOKEN, } from '../http/error_schemas.js';
|
|
@@ -127,6 +127,9 @@ export const create_rpc_endpoint = (options) => {
|
|
|
127
127
|
if (action_map.has(action.spec.method)) {
|
|
128
128
|
throw new Error(`Duplicate RPC action method: ${action.spec.method}`);
|
|
129
129
|
}
|
|
130
|
+
if (is_null_schema(action.spec.input)) {
|
|
131
|
+
throw new Error(`RPC action "${action.spec.method}" uses z.null() for input — JSON-RPC 2.0 §4.2 forbids "params": null on the wire (must be omitted or be a Structured value). Use z.void() for parameterless methods.`);
|
|
132
|
+
}
|
|
130
133
|
action_map.set(action.spec.method, action);
|
|
131
134
|
}
|
|
132
135
|
/**
|
|
@@ -163,12 +166,16 @@ export const create_rpc_endpoint = (options) => {
|
|
|
163
166
|
return c.json(error, jsonrpc_error_code_to_http_status(auth_error.code));
|
|
164
167
|
}
|
|
165
168
|
// step 4: validate params
|
|
166
|
-
// Missing `params` on the envelope maps to `
|
|
167
|
-
// schemas and `{}` for object inputs
|
|
168
|
-
// object" convention so callers of all-optional-object
|
|
169
|
-
// omit `params` on the wire
|
|
170
|
-
//
|
|
171
|
-
|
|
169
|
+
// Missing `params` on the envelope maps to `undefined` for `z.void()`
|
|
170
|
+
// input schemas and `{}` for object inputs (matches HTTP's "empty
|
|
171
|
+
// body = empty object" convention so callers of all-optional-object
|
|
172
|
+
// RPC methods can omit `params` on the wire). JSON-RPC 2.0 §4.2
|
|
173
|
+
// forbids `params: null`, so `z.void()` is the spec-correct schema
|
|
174
|
+
// for parameterless methods — registration above rejects `z.null()`
|
|
175
|
+
// inputs to keep this branch from having to consider that legacy
|
|
176
|
+
// shape. When `raw_params` is present it flows through unchanged so
|
|
177
|
+
// contract-violating shapes still fail validation.
|
|
178
|
+
const params = is_void_schema(action.spec.input) ? raw_params : (raw_params ?? {});
|
|
172
179
|
const parse_result = action.spec.input.safeParse(params);
|
|
173
180
|
if (!parse_result.success) {
|
|
174
181
|
const error = jsonrpc_error_response(id, jsonrpc_error_messages.invalid_params('invalid params', {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend-only typed RPC client factory.
|
|
3
|
+
*
|
|
4
|
+
* Bundles the `ActionRegistry + ActionEventEnvironment + Transports +
|
|
5
|
+
* ActionPeer + create_rpc_client` boilerplate every consumer repeats — plus
|
|
6
|
+
* the `lookup_action_handler: () => undefined` stub (frontend never registers
|
|
7
|
+
* `request_response` handlers; every method dispatches over the wire).
|
|
8
|
+
*
|
|
9
|
+
* Generic `TApi` is the consumer's typed Proxy interface. The `as unknown
|
|
10
|
+
* as TApi` double cast happens inside the helper so call sites get a typed
|
|
11
|
+
* return value without the cast hostility.
|
|
12
|
+
*
|
|
13
|
+
* Companion to `create_throwing_api` — typical wiring is two lines:
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const {api: api_raw} = create_frontend_rpc_client<MyActionsApi>({specs: all_specs});
|
|
17
|
+
* const api = create_throwing_api(api_raw);
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Returns the underlying `peer` and `environment` alongside `api` so
|
|
21
|
+
* advanced consumers (zzz-style frontends needing extra transports / WS
|
|
22
|
+
* notification handlers / action-history wiring) can extend without
|
|
23
|
+
* recreating the bundle.
|
|
24
|
+
*
|
|
25
|
+
* Note: `local_call` specs in `specs` will silently no-op because
|
|
26
|
+
* `lookup_action_handler` always returns `undefined` — the frontend
|
|
27
|
+
* factory is for wire-dispatched actions. Frontend-side `local_call`
|
|
28
|
+
* needs a different wiring shape (custom `environment.lookup_action_handler`).
|
|
29
|
+
*
|
|
30
|
+
* @module
|
|
31
|
+
*/
|
|
32
|
+
import { ActionPeer } from './action_peer.js';
|
|
33
|
+
import { type Transport } from './transports.js';
|
|
34
|
+
import type { ActionEventEnvironment } from './action_event_types.js';
|
|
35
|
+
import type { ActionSpecUnion } from './action_spec.js';
|
|
36
|
+
/** Options for `create_frontend_rpc_client`. */
|
|
37
|
+
export interface CreateFrontendRpcClientOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Action specs the typed Proxy can dispatch. Methods absent from this
|
|
40
|
+
* list silently return `undefined` from the Proxy — the generic `TApi`
|
|
41
|
+
* cannot constrain runtime membership, so consumers must keep this list
|
|
42
|
+
* in sync with the typed surface (codegen recommended).
|
|
43
|
+
*/
|
|
44
|
+
specs: ReadonlyArray<ActionSpecUnion>;
|
|
45
|
+
/**
|
|
46
|
+
* HTTP RPC endpoint path for the default `FrontendHttpTransport`.
|
|
47
|
+
* Defaults to `/api/rpc`. Ignored when `transports` is provided.
|
|
48
|
+
*/
|
|
49
|
+
path?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Optional explicit transport list. When provided, the default
|
|
52
|
+
* `FrontendHttpTransport(path)` is **not** registered — the caller is
|
|
53
|
+
* responsible for at least one ready transport. Use for WS-first or
|
|
54
|
+
* WS+HTTP mixed setups.
|
|
55
|
+
*/
|
|
56
|
+
transports?: ReadonlyArray<Transport>;
|
|
57
|
+
}
|
|
58
|
+
/** Bundle returned by `create_frontend_rpc_client`. */
|
|
59
|
+
export interface FrontendRpcClient<TApi> {
|
|
60
|
+
/** Typed Proxy — call `api.method(input)` for `Promise<Result<...>>`. */
|
|
61
|
+
api: TApi;
|
|
62
|
+
/** Underlying peer — exposed for consumers that need to register more transports or send raw messages. */
|
|
63
|
+
peer: ActionPeer;
|
|
64
|
+
/** Action environment — exposed for consumers that need to share it (e.g. attach a notification handler registry). */
|
|
65
|
+
environment: ActionEventEnvironment;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build a frontend-only typed RPC client.
|
|
69
|
+
*
|
|
70
|
+
* @param options - `specs` (required), optional `path` / `transports`
|
|
71
|
+
* @returns `{api, peer, environment}` — typed Proxy plus the underlying primitives
|
|
72
|
+
*/
|
|
73
|
+
export declare const create_frontend_rpc_client: <TApi>(options: CreateFrontendRpcClientOptions) => FrontendRpcClient<TApi>;
|
|
74
|
+
//# sourceMappingURL=frontend_rpc_client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontend_rpc_client.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/frontend_rpc_client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAa,KAAK,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAG3D,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,yBAAyB,CAAC;AACpE,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,kBAAkB,CAAC;AAEtD,gDAAgD;AAChD,MAAM,WAAW,8BAA8B;IAC9C;;;;;OAKG;IACH,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IACtC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,UAAU,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;CACtC;AAED,uDAAuD;AACvD,MAAM,WAAW,iBAAiB,CAAC,IAAI;IACtC,yEAAyE;IACzE,GAAG,EAAE,IAAI,CAAC;IACV,0GAA0G;IAC1G,IAAI,EAAE,UAAU,CAAC;IACjB,sHAAsH;IACtH,WAAW,EAAE,sBAAsB,CAAC;CACpC;AAED;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,GAAI,IAAI,EAC9C,SAAS,8BAA8B,KACrC,iBAAiB,CAAC,IAAI,CAgBxB,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend-only typed RPC client factory.
|
|
3
|
+
*
|
|
4
|
+
* Bundles the `ActionRegistry + ActionEventEnvironment + Transports +
|
|
5
|
+
* ActionPeer + create_rpc_client` boilerplate every consumer repeats — plus
|
|
6
|
+
* the `lookup_action_handler: () => undefined` stub (frontend never registers
|
|
7
|
+
* `request_response` handlers; every method dispatches over the wire).
|
|
8
|
+
*
|
|
9
|
+
* Generic `TApi` is the consumer's typed Proxy interface. The `as unknown
|
|
10
|
+
* as TApi` double cast happens inside the helper so call sites get a typed
|
|
11
|
+
* return value without the cast hostility.
|
|
12
|
+
*
|
|
13
|
+
* Companion to `create_throwing_api` — typical wiring is two lines:
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const {api: api_raw} = create_frontend_rpc_client<MyActionsApi>({specs: all_specs});
|
|
17
|
+
* const api = create_throwing_api(api_raw);
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Returns the underlying `peer` and `environment` alongside `api` so
|
|
21
|
+
* advanced consumers (zzz-style frontends needing extra transports / WS
|
|
22
|
+
* notification handlers / action-history wiring) can extend without
|
|
23
|
+
* recreating the bundle.
|
|
24
|
+
*
|
|
25
|
+
* Note: `local_call` specs in `specs` will silently no-op because
|
|
26
|
+
* `lookup_action_handler` always returns `undefined` — the frontend
|
|
27
|
+
* factory is for wire-dispatched actions. Frontend-side `local_call`
|
|
28
|
+
* needs a different wiring shape (custom `environment.lookup_action_handler`).
|
|
29
|
+
*
|
|
30
|
+
* @module
|
|
31
|
+
*/
|
|
32
|
+
import { ActionRegistry } from './action_registry.js';
|
|
33
|
+
import { ActionPeer } from './action_peer.js';
|
|
34
|
+
import { Transports } from './transports.js';
|
|
35
|
+
import { FrontendHttpTransport } from './transports_http.js';
|
|
36
|
+
import { create_rpc_client } from './rpc_client.js';
|
|
37
|
+
/**
|
|
38
|
+
* Build a frontend-only typed RPC client.
|
|
39
|
+
*
|
|
40
|
+
* @param options - `specs` (required), optional `path` / `transports`
|
|
41
|
+
* @returns `{api, peer, environment}` — typed Proxy plus the underlying primitives
|
|
42
|
+
*/
|
|
43
|
+
export const create_frontend_rpc_client = (options) => {
|
|
44
|
+
const registry = new ActionRegistry([...options.specs]);
|
|
45
|
+
const environment = {
|
|
46
|
+
executor: 'frontend',
|
|
47
|
+
lookup_action_spec: (method) => registry.spec_by_method.get(method),
|
|
48
|
+
lookup_action_handler: () => undefined,
|
|
49
|
+
};
|
|
50
|
+
const transports = new Transports();
|
|
51
|
+
if (options.transports) {
|
|
52
|
+
for (const t of options.transports)
|
|
53
|
+
transports.register_transport(t);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
transports.register_transport(new FrontendHttpTransport(options.path ?? '/api/rpc'));
|
|
57
|
+
}
|
|
58
|
+
const peer = new ActionPeer({ environment, transports });
|
|
59
|
+
const api = create_rpc_client({ peer, environment });
|
|
60
|
+
return { api, peer, environment };
|
|
61
|
+
};
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
|
+
import type { Result } from '@fuzdev/fuz_util/result.js';
|
|
12
13
|
import type { ActionEventEnvironment } from './action_event_types.js';
|
|
13
14
|
import type { ActionPeer, ActionPeerSendOptions } from './action_peer.js';
|
|
14
15
|
import type { ActionEventDataUnion } from './action_event_data.js';
|
|
15
16
|
import type { TransportName } from './transports.js';
|
|
17
|
+
import type { JsonrpcErrorObject } from '../http/jsonrpc.js';
|
|
16
18
|
/**
|
|
17
19
|
* Optional per-method transport selector. Return the transport to use for a
|
|
18
20
|
* given method, or `undefined` to let the peer pick via its fallback rules.
|
|
@@ -113,4 +115,66 @@ export type ThrowingRpcCall = <TOutput = unknown>(method: string, input?: unknow
|
|
|
113
115
|
* notably the consumer's generated `ActionsApi` interface)
|
|
114
116
|
*/
|
|
115
117
|
export declare const create_throwing_rpc_call: <TApi extends Record<keyof TApi, (input?: any) => Promise<any> | void>>(api: TApi) => ThrowingRpcCall;
|
|
118
|
+
/**
|
|
119
|
+
* Maps a typed `ActionsApi` to a throwing variant.
|
|
120
|
+
*
|
|
121
|
+
* For each method whose return type matches the `create_rpc_client` shape
|
|
122
|
+
* (`Promise<Result<{value: T}, {error: JsonrpcErrorObject}>>`), the wrapped
|
|
123
|
+
* method returns `Promise<T>` directly. Other shapes (notifications typed
|
|
124
|
+
* as `=> void`, sync `local_call` methods) pass through unchanged — there
|
|
125
|
+
* is nothing to unwrap.
|
|
126
|
+
*
|
|
127
|
+
* Input + options parameters are preserved verbatim so generics, branded
|
|
128
|
+
* Uuids, and per-call `RpcClientCallOptions` keep working.
|
|
129
|
+
*/
|
|
130
|
+
export type ThrowingApi<TApi> = {
|
|
131
|
+
[K in keyof TApi]: TApi[K] extends (input?: infer TInput, options?: infer TOptions) => Promise<Result<{
|
|
132
|
+
value: infer TValue;
|
|
133
|
+
}, {
|
|
134
|
+
error: JsonrpcErrorObject;
|
|
135
|
+
}>> ? (input?: TInput, options?: TOptions) => Promise<TValue> : TApi[K];
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Wrap a typed RPC client so every call resolves to its unwrapped value or
|
|
139
|
+
* throws an `Error` carrying the JSON-RPC `{code, message, data?}` shape.
|
|
140
|
+
*
|
|
141
|
+
* Implementation is a Proxy because the underlying `create_rpc_client`
|
|
142
|
+
* return is itself a Proxy with no concrete keys — a key-by-key wrap would
|
|
143
|
+
* need to enumerate the typed surface, which only the consumer's generated
|
|
144
|
+
* `ActionsApi` interface knows.
|
|
145
|
+
*
|
|
146
|
+
* Pass-through on non-Result returns is deliberate: sync `local_call`
|
|
147
|
+
* Proxy methods return values directly (see `create_sync_local_call_method`
|
|
148
|
+
* above). The Proxy can't distinguish those at get-time, so the wrapper
|
|
149
|
+
* inspects `result` shape at call-time and only unwraps when it sees a
|
|
150
|
+
* Result. Non-object returns pass through unchanged.
|
|
151
|
+
*
|
|
152
|
+
* Only `{code, data}` cross onto the thrown Error — `name` / `stack` are
|
|
153
|
+
* left as the Error's own properties so attacker-shaped `result.error`
|
|
154
|
+
* payloads cannot overwrite them. Same hardening as
|
|
155
|
+
* `create_throwing_rpc_call`.
|
|
156
|
+
*
|
|
157
|
+
* Composable with `create_throwing_rpc_call` — same typed underlying
|
|
158
|
+
* client feeds both: the Proxy form for direct call sites, the loose
|
|
159
|
+
* method-keyed form for adapter wiring (`ui/admin_rpc_adapters.ts`).
|
|
160
|
+
*
|
|
161
|
+
* Recommended consumer convention: bind the throwing wrapper to `api`
|
|
162
|
+
* (the common case at call sites) and the underlying Result-returning
|
|
163
|
+
* Proxy to `api_raw` (the composable escape hatch for callers that
|
|
164
|
+
* want to inspect `error.data.reason` without try/catch).
|
|
165
|
+
*
|
|
166
|
+
* Catch blocks read `err.data?.reason` — optional chaining required
|
|
167
|
+
* because JSON-RPC `data` is spec-level optional.
|
|
168
|
+
*
|
|
169
|
+
* On unknown string-keyed methods, the get trap returns a function that
|
|
170
|
+
* throws `"rpc method not found: <prop>"` on invocation — symmetric with
|
|
171
|
+
* `create_throwing_rpc_call` and clearer than the JS default
|
|
172
|
+
* `"api.foo is not a function"`. Symbol props and `then` stay
|
|
173
|
+
* undefined so the Proxy isn't accidentally treated as a thenable
|
|
174
|
+
* (`await api` would otherwise probe `then` and trip the thrower).
|
|
175
|
+
*
|
|
176
|
+
* @param api_raw - typed RPC client from `create_rpc_client`, cast
|
|
177
|
+
* to a consumer-generated `ActionsApi` interface
|
|
178
|
+
*/
|
|
179
|
+
export declare const create_throwing_api: <TApi extends object>(api_raw: TApi) => ThrowingApi<TApi>;
|
|
116
180
|
//# sourceMappingURL=rpc_client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc_client.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/rpc_client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"rpc_client.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/rpc_client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,4BAA4B,CAAC;AAQvD,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,yBAAyB,CAAC;AAOpE,OAAO,KAAK,EAAC,UAAU,EAAE,qBAAqB,EAAC,MAAM,kBAAkB,CAAC;AACxE,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAEnD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,oBAAoB,CAAC;AAE3D;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,aAAa,GAAG,SAAS,CAAC;AAM/E,8EAA8E;AAC9E,MAAM,WAAW,sBAAsB;IACtC,aAAa,EAAE,CAAC,IAAI,EAAE;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,oBAAoB,CAAA;KAAC,KAC5E;QACA,sBAAsB,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;KAC5C,GACD,SAAS,CAAC;CACb;AAED,uCAAuC;AACvC,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,EAAE,sBAAsB,CAAC;IACpC,kEAAkE;IAClE,OAAO,CAAC,EAAE,sBAAsB,CAAC;IACjC;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,kBAAkB,CAAC;CAC1C;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,GAC7B,SAAS,sBAAsB,KAC7B,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,GAAG,CAgB7C,CAAC;AA2DF;;;;;GAKG;AACH,MAAM,WAAW,oBAAqB,SAAQ,qBAAqB;CAAG;AAgItE;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,GAAG,OAAO,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,OAAO,KACX,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,wBAAwB,GACpC,IAAI,SAAS,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,EAErE,KAAK,IAAI,KACP,eAcF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,WAAW,CAAC,IAAI,IAAI;KAC9B,CAAC,IAAI,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,CAClC,KAAK,CAAC,EAAE,MAAM,MAAM,EACpB,OAAO,CAAC,EAAE,MAAM,QAAQ,KACpB,OAAO,CAAC,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,MAAM,CAAA;KAAC,EAAE;QAAC,KAAK,EAAE,kBAAkB,CAAA;KAAC,CAAC,CAAC,GACrE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,KAAK,OAAO,CAAC,MAAM,CAAC,GACvD,IAAI,CAAC,CAAC,CAAC;CACV,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,mBAAmB,GAAI,IAAI,SAAS,MAAM,EAAE,SAAS,IAAI,KAAG,WAAW,CAAC,IAAI,CA8BxF,CAAC"}
|
|
@@ -222,3 +222,80 @@ export const create_throwing_rpc_call = (api) => {
|
|
|
222
222
|
return result.value;
|
|
223
223
|
};
|
|
224
224
|
};
|
|
225
|
+
/**
|
|
226
|
+
* Wrap a typed RPC client so every call resolves to its unwrapped value or
|
|
227
|
+
* throws an `Error` carrying the JSON-RPC `{code, message, data?}` shape.
|
|
228
|
+
*
|
|
229
|
+
* Implementation is a Proxy because the underlying `create_rpc_client`
|
|
230
|
+
* return is itself a Proxy with no concrete keys — a key-by-key wrap would
|
|
231
|
+
* need to enumerate the typed surface, which only the consumer's generated
|
|
232
|
+
* `ActionsApi` interface knows.
|
|
233
|
+
*
|
|
234
|
+
* Pass-through on non-Result returns is deliberate: sync `local_call`
|
|
235
|
+
* Proxy methods return values directly (see `create_sync_local_call_method`
|
|
236
|
+
* above). The Proxy can't distinguish those at get-time, so the wrapper
|
|
237
|
+
* inspects `result` shape at call-time and only unwraps when it sees a
|
|
238
|
+
* Result. Non-object returns pass through unchanged.
|
|
239
|
+
*
|
|
240
|
+
* Only `{code, data}` cross onto the thrown Error — `name` / `stack` are
|
|
241
|
+
* left as the Error's own properties so attacker-shaped `result.error`
|
|
242
|
+
* payloads cannot overwrite them. Same hardening as
|
|
243
|
+
* `create_throwing_rpc_call`.
|
|
244
|
+
*
|
|
245
|
+
* Composable with `create_throwing_rpc_call` — same typed underlying
|
|
246
|
+
* client feeds both: the Proxy form for direct call sites, the loose
|
|
247
|
+
* method-keyed form for adapter wiring (`ui/admin_rpc_adapters.ts`).
|
|
248
|
+
*
|
|
249
|
+
* Recommended consumer convention: bind the throwing wrapper to `api`
|
|
250
|
+
* (the common case at call sites) and the underlying Result-returning
|
|
251
|
+
* Proxy to `api_raw` (the composable escape hatch for callers that
|
|
252
|
+
* want to inspect `error.data.reason` without try/catch).
|
|
253
|
+
*
|
|
254
|
+
* Catch blocks read `err.data?.reason` — optional chaining required
|
|
255
|
+
* because JSON-RPC `data` is spec-level optional.
|
|
256
|
+
*
|
|
257
|
+
* On unknown string-keyed methods, the get trap returns a function that
|
|
258
|
+
* throws `"rpc method not found: <prop>"` on invocation — symmetric with
|
|
259
|
+
* `create_throwing_rpc_call` and clearer than the JS default
|
|
260
|
+
* `"api.foo is not a function"`. Symbol props and `then` stay
|
|
261
|
+
* undefined so the Proxy isn't accidentally treated as a thenable
|
|
262
|
+
* (`await api` would otherwise probe `then` and trip the thrower).
|
|
263
|
+
*
|
|
264
|
+
* @param api_raw - typed RPC client from `create_rpc_client`, cast
|
|
265
|
+
* to a consumer-generated `ActionsApi` interface
|
|
266
|
+
*/
|
|
267
|
+
export const create_throwing_api = (api_raw) => {
|
|
268
|
+
return new Proxy(api_raw, {
|
|
269
|
+
get(target, prop) {
|
|
270
|
+
const fn = target[prop];
|
|
271
|
+
if (typeof fn === 'function') {
|
|
272
|
+
return async (...args) => {
|
|
273
|
+
const result = await fn.apply(target, args);
|
|
274
|
+
if (result === null || typeof result !== 'object')
|
|
275
|
+
return result;
|
|
276
|
+
const r = result;
|
|
277
|
+
if (r.ok === true)
|
|
278
|
+
return r.value;
|
|
279
|
+
if (r.ok === false && r.error && typeof r.error === 'object') {
|
|
280
|
+
const e = r.error;
|
|
281
|
+
throw Object.assign(new Error(e.message), {
|
|
282
|
+
code: e.code,
|
|
283
|
+
data: e.data,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (fn !== undefined)
|
|
290
|
+
return fn;
|
|
291
|
+
// Underlying api has no member by this name. Symbol props and
|
|
292
|
+
// `then` must stay undefined — `await tapi` reads `then` and
|
|
293
|
+
// would otherwise trip the thrower.
|
|
294
|
+
if (typeof prop !== 'string' || prop === 'then')
|
|
295
|
+
return undefined;
|
|
296
|
+
return () => {
|
|
297
|
+
throw new Error(`rpc method not found: ${prop}`);
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
};
|
package/dist/auth/CLAUDE.md
CHANGED
|
@@ -845,16 +845,16 @@ auth per-spec, so mixed-auth endpoints compose cleanly.
|
|
|
845
845
|
|
|
846
846
|
| Spec | Side effects | Input | Output |
|
|
847
847
|
| -------------------------------------- | ------------ | --------------------------------------------------------- | ----------------------------- |
|
|
848
|
-
| `admin_account_list_action_spec` | false | `z.
|
|
849
|
-
| `admin_session_list_action_spec` | false | `z.
|
|
848
|
+
| `admin_account_list_action_spec` | false | `z.void()` | `{accounts, grantable_roles}` |
|
|
849
|
+
| `admin_session_list_action_spec` | false | `z.void()` | `{sessions}` |
|
|
850
850
|
| `admin_session_revoke_all_action_spec` | true | `{account_id}` | `{ok, count}` |
|
|
851
851
|
| `admin_token_revoke_all_action_spec` | true | `{account_id}` | `{ok, count}` |
|
|
852
852
|
| `audit_log_list_action_spec` | false | `{event_type?, account_id?, limit?, offset?, since_seq?}` | `{events}` |
|
|
853
853
|
| `audit_log_permit_history_action_spec` | false | `{limit?, offset?}` | `{events}` |
|
|
854
854
|
| `invite_create_action_spec` | true | `{email?, username?}` | `{ok, invite}` |
|
|
855
|
-
| `invite_list_action_spec` | false | `z.
|
|
855
|
+
| `invite_list_action_spec` | false | `z.void()` | `{invites}` |
|
|
856
856
|
| `invite_delete_action_spec` | true | `{invite_id}` | `{ok}` |
|
|
857
|
-
| `app_settings_get_action_spec` | false | `z.
|
|
857
|
+
| `app_settings_get_action_spec` | false | `z.void()` | `{settings}` |
|
|
858
858
|
| `app_settings_update_action_spec` | true | `{open_signup}` | `{ok, settings}` |
|
|
859
859
|
|
|
860
860
|
`AUDIT_LOG_LIST_LIMIT_MAX = 200` — page size clamp (mirrors the former REST
|
|
@@ -1039,6 +1039,14 @@ the admin integration suite exercises `account_token_create` /
|
|
|
1039
1039
|
consumer wiring the admin surface without account actions will hit
|
|
1040
1040
|
`method not found` on first admin-suite run.
|
|
1041
1041
|
|
|
1042
|
+
Frontend mirror: `all_standard_action_specs` (in
|
|
1043
|
+
`./standard_action_specs.ts`) bundles `all_admin_action_specs +
|
|
1044
|
+
all_permit_offer_action_specs + all_account_action_specs` into one
|
|
1045
|
+
`ReadonlyArray<RequestResponseActionSpec>` for typed-client codegen
|
|
1046
|
+
and `create_frontend_rpc_client({specs})` wiring. Self-service role
|
|
1047
|
+
specs are not included (opt-in, app-specific `eligible_roles`) —
|
|
1048
|
+
spread `all_self_service_role_action_specs` separately when needed.
|
|
1049
|
+
|
|
1042
1050
|
### `account_action_specs.ts` + `account_actions.ts` — seven self-service RPC actions
|
|
1043
1051
|
|
|
1044
1052
|
Counterpart to `account_routes.ts`. Cookie-lifecycle flows (`login`,
|
|
@@ -1058,12 +1066,12 @@ exists.
|
|
|
1058
1066
|
|
|
1059
1067
|
| Spec | Side effects | Input | Output |
|
|
1060
1068
|
| ---------------------------------------- | ------------ | -------------- | ----------------------- |
|
|
1061
|
-
| `account_verify_action_spec` | false | `z.
|
|
1062
|
-
| `account_session_list_action_spec` | false | `z.
|
|
1069
|
+
| `account_verify_action_spec` | false | `z.void()` | `SessionAccountJson` |
|
|
1070
|
+
| `account_session_list_action_spec` | false | `z.void()` | `{sessions}` |
|
|
1063
1071
|
| `account_session_revoke_action_spec` | true | `{session_id}` | `{ok, revoked}` |
|
|
1064
|
-
| `account_session_revoke_all_action_spec` | true | `z.
|
|
1072
|
+
| `account_session_revoke_all_action_spec` | true | `z.void()` | `{ok, count}` |
|
|
1065
1073
|
| `account_token_create_action_spec` | true | `{name?}` | `{ok, token, id, name}` |
|
|
1066
|
-
| `account_token_list_action_spec` | false | `z.
|
|
1074
|
+
| `account_token_list_action_spec` | false | `z.void()` | `{tokens}` |
|
|
1067
1075
|
| `account_token_revoke_action_spec` | true | `{token_id}` | `{ok, revoked}` |
|
|
1068
1076
|
|
|
1069
1077
|
`session_id` validates as `Blake3Hash`; `token_id` validates as
|
|
@@ -1084,15 +1092,17 @@ registry of all seven specs.
|
|
|
1084
1092
|
### `self_service_role_action_specs.ts` + `self_service_role_actions.ts` — opt-in self-service role toggle
|
|
1085
1093
|
|
|
1086
1094
|
Same split as the other registries: `*_action_specs.ts` holds the input/output
|
|
1087
|
-
Zod schemas, the
|
|
1095
|
+
Zod schemas, the `satisfies RequestResponseActionSpec` literal, the
|
|
1088
1096
|
`ERROR_ROLE_NOT_SELF_SERVICE_ELIGIBLE` reason constant, and the
|
|
1089
1097
|
`all_self_service_role_action_specs` registry — all client-safe. The
|
|
1090
|
-
`*_actions.ts` factory imports the
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1098
|
+
`*_actions.ts` factory imports the spec and pairs it with the handler.
|
|
1099
|
+
|
|
1100
|
+
One static `request_response` action — `self_service_role_set` — that
|
|
1101
|
+
takes `{role, enabled: boolean}` and toggles a global permit on the
|
|
1102
|
+
caller. Idempotent in both directions: `changed: false` when the
|
|
1103
|
+
post-call state already matched the request (already-held when
|
|
1104
|
+
enabling; not-held when disabling). Output is `{ok, enabled, changed}` —
|
|
1105
|
+
`enabled` echoes the post-call state for self-describing responses.
|
|
1096
1106
|
Audit metadata carries `self_service: true` so admin reviewers can
|
|
1097
1107
|
distinguish self-toggled permits from admin grants/offers. The
|
|
1098
1108
|
`permit_grant` / `permit_revoke` metadata schemas declare
|
|
@@ -1100,7 +1110,7 @@ distinguish self-toggled permits from admin grants/offers. The
|
|
|
1100
1110
|
part of the documented surface rather than riding on `z.looseObject`
|
|
1101
1111
|
permissiveness.
|
|
1102
1112
|
|
|
1103
|
-
Method
|
|
1113
|
+
Method name is static — `role` lives in the input, not the method
|
|
1104
1114
|
name. Mirrors the `permit_offer_create({role})` precedent. Per-role
|
|
1105
1115
|
parameterized methods would break the `satisfies RequestResponseActionSpec`
|
|
1106
1116
|
codegen invariant and grow the surface linearly per role.
|
|
@@ -1110,14 +1120,15 @@ codegen invariant and grow the surface linearly per role.
|
|
|
1110
1120
|
- `eligible_roles: ReadonlyArray<string>` — required allowlist. Roles
|
|
1111
1121
|
outside the list are rejected with `forbidden` + reason
|
|
1112
1122
|
`role_not_self_service_eligible` (exported as
|
|
1113
|
-
`ERROR_ROLE_NOT_SELF_SERVICE_ELIGIBLE`).
|
|
1123
|
+
`ERROR_ROLE_NOT_SELF_SERVICE_ELIGIBLE`). The eligibility check fires
|
|
1124
|
+
before the `enabled` branch — same rejection regardless of direction.
|
|
1114
1125
|
- `roles?: RoleSchemaResult` — optional. When supplied, every entry in
|
|
1115
1126
|
`eligible_roles` is checked against `roles.role_options` at factory
|
|
1116
1127
|
time so typos throw at startup instead of at first call.
|
|
1117
1128
|
|
|
1118
|
-
Grant
|
|
1129
|
+
Grant branch uses `query_permit_has_role` for a benign-TOCTOU pre-check
|
|
1119
1130
|
(distinguishes new grant from idempotent re-grant), then
|
|
1120
|
-
`query_grant_permit` for the actual insert. Revoke
|
|
1131
|
+
`query_grant_permit` for the actual insert. Revoke branch filters
|
|
1121
1132
|
`query_permit_find_active_for_actor` in JS for the matching
|
|
1122
1133
|
`(actor, role, scope_id IS NULL)` row before calling
|
|
1123
1134
|
`query_revoke_permit`. Bundle is **not** included in
|
|
@@ -1126,8 +1137,8 @@ spread alongside the standard bundle when needed.
|
|
|
1126
1137
|
|
|
1127
1138
|
Deps: `SelfServiceRoleActionDeps = Pick<RouteFactoryDeps, 'log' | 'on_audit_event' | 'audit_log_config'>`.
|
|
1128
1139
|
|
|
1129
|
-
`all_self_service_role_action_specs:
|
|
1130
|
-
codegen-ready registry of
|
|
1140
|
+
`all_self_service_role_action_specs: ReadonlyArray<RequestResponseActionSpec>` —
|
|
1141
|
+
codegen-ready registry of the single unified spec.
|
|
1131
1142
|
|
|
1132
1143
|
## Cleanup
|
|
1133
1144
|
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
import type { RequestResponseActionSpec } from '../actions/action_spec.js';
|
|
12
12
|
/** Input for `account_verify`. No parameters — the caller is the subject. */
|
|
13
|
-
export declare const VerifyInput: z.
|
|
13
|
+
export declare const VerifyInput: z.ZodVoid;
|
|
14
14
|
export type VerifyInput = z.infer<typeof VerifyInput>;
|
|
15
15
|
/** Input for `account_session_list`. No parameters. */
|
|
16
|
-
export declare const SessionListInput: z.
|
|
16
|
+
export declare const SessionListInput: z.ZodVoid;
|
|
17
17
|
export type SessionListInput = z.infer<typeof SessionListInput>;
|
|
18
18
|
/** Output for `account_session_list`. */
|
|
19
19
|
export declare const SessionListOutput: z.ZodObject<{
|
|
@@ -38,7 +38,7 @@ export declare const SessionRevokeOutput: z.ZodObject<{
|
|
|
38
38
|
}, z.core.$strict>;
|
|
39
39
|
export type SessionRevokeOutput = z.infer<typeof SessionRevokeOutput>;
|
|
40
40
|
/** Input for `account_session_revoke_all`. No parameters. */
|
|
41
|
-
export declare const SessionRevokeAllInput: z.
|
|
41
|
+
export declare const SessionRevokeAllInput: z.ZodVoid;
|
|
42
42
|
export type SessionRevokeAllInput = z.infer<typeof SessionRevokeAllInput>;
|
|
43
43
|
/** Output for `account_session_revoke_all`. */
|
|
44
44
|
export declare const SessionRevokeAllOutput: z.ZodObject<{
|
|
@@ -60,7 +60,7 @@ export declare const TokenCreateOutput: z.ZodObject<{
|
|
|
60
60
|
}, z.core.$strict>;
|
|
61
61
|
export type TokenCreateOutput = z.infer<typeof TokenCreateOutput>;
|
|
62
62
|
/** Input for `account_token_list`. No parameters. */
|
|
63
|
-
export declare const TokenListInput: z.
|
|
63
|
+
export declare const TokenListInput: z.ZodVoid;
|
|
64
64
|
export type TokenListInput = z.infer<typeof TokenListInput>;
|
|
65
65
|
/** Output for `account_token_list`. Hashes are excluded. */
|
|
66
66
|
export declare const TokenListOutput: z.ZodObject<{
|
|
@@ -92,7 +92,7 @@ export declare const account_verify_action_spec: {
|
|
|
92
92
|
initiator: "frontend";
|
|
93
93
|
auth: "authenticated";
|
|
94
94
|
side_effects: false;
|
|
95
|
-
input: z.
|
|
95
|
+
input: z.ZodVoid;
|
|
96
96
|
output: z.ZodObject<{
|
|
97
97
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
98
98
|
username: z.ZodString;
|
|
@@ -109,7 +109,7 @@ export declare const account_session_list_action_spec: {
|
|
|
109
109
|
initiator: "frontend";
|
|
110
110
|
auth: "authenticated";
|
|
111
111
|
side_effects: false;
|
|
112
|
-
input: z.
|
|
112
|
+
input: z.ZodVoid;
|
|
113
113
|
output: z.ZodObject<{
|
|
114
114
|
sessions: z.ZodArray<z.ZodObject<{
|
|
115
115
|
id: z.ZodString;
|
|
@@ -144,7 +144,7 @@ export declare const account_session_revoke_all_action_spec: {
|
|
|
144
144
|
initiator: "frontend";
|
|
145
145
|
auth: "authenticated";
|
|
146
146
|
side_effects: true;
|
|
147
|
-
input: z.
|
|
147
|
+
input: z.ZodVoid;
|
|
148
148
|
output: z.ZodObject<{
|
|
149
149
|
ok: z.ZodLiteral<true>;
|
|
150
150
|
count: z.ZodNumber;
|
|
@@ -176,7 +176,7 @@ export declare const account_token_list_action_spec: {
|
|
|
176
176
|
initiator: "frontend";
|
|
177
177
|
auth: "authenticated";
|
|
178
178
|
side_effects: false;
|
|
179
|
-
input: z.
|
|
179
|
+
input: z.ZodVoid;
|
|
180
180
|
output: z.ZodObject<{
|
|
181
181
|
tokens: z.ZodArray<z.ZodObject<{
|
|
182
182
|
id: z.ZodString;
|