@fuzdev/fuz_app 0.43.0 → 0.45.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.
Files changed (39) hide show
  1. package/dist/actions/CLAUDE.md +108 -5
  2. package/dist/actions/action_event.d.ts +10 -1
  3. package/dist/actions/action_event.d.ts.map +1 -1
  4. package/dist/actions/action_event.js +7 -0
  5. package/dist/actions/broadcast_api.d.ts +1 -1
  6. package/dist/actions/broadcast_api.d.ts.map +1 -1
  7. package/dist/actions/frontend_rpc_client.d.ts +128 -0
  8. package/dist/actions/frontend_rpc_client.d.ts.map +1 -0
  9. package/dist/actions/frontend_rpc_client.js +86 -0
  10. package/dist/actions/rpc_client.d.ts +86 -60
  11. package/dist/actions/rpc_client.d.ts.map +1 -1
  12. package/dist/actions/rpc_client.js +102 -80
  13. package/dist/auth/CLAUDE.md +24 -13
  14. package/dist/auth/self_service_role_action_specs.d.ts +20 -48
  15. package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
  16. package/dist/auth/self_service_role_action_specs.js +22 -44
  17. package/dist/auth/self_service_role_actions.d.ts +9 -9
  18. package/dist/auth/self_service_role_actions.d.ts.map +1 -1
  19. package/dist/auth/self_service_role_actions.js +48 -53
  20. package/dist/auth/standard_action_specs.d.ts +31 -0
  21. package/dist/auth/standard_action_specs.d.ts.map +1 -0
  22. package/dist/auth/standard_action_specs.js +36 -0
  23. package/dist/testing/ws_round_trip.d.ts +1 -1
  24. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  25. package/dist/ui/CLAUDE.md +10 -11
  26. package/dist/ui/admin_accounts_state.svelte.d.ts +20 -41
  27. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  28. package/dist/ui/admin_invites_state.svelte.d.ts +9 -18
  29. package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
  30. package/dist/ui/admin_rpc_adapters.d.ts +41 -29
  31. package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
  32. package/dist/ui/admin_rpc_adapters.js +28 -31
  33. package/dist/ui/admin_sessions_state.svelte.d.ts +3 -2
  34. package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
  35. package/dist/ui/app_settings_state.svelte.d.ts +5 -10
  36. package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
  37. package/dist/ui/audit_log_state.svelte.d.ts +6 -18
  38. package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
  39. package/package.json +1 -1
@@ -5,7 +5,8 @@
5
5
  * - **Tier 1** (simple, for tx): transport send/receive, Result return. No `environment`.
6
6
  * - **Tier 2** (full, for zzz): ActionEvent lifecycle with `environment`.
7
7
  *
8
- * Consumers cast the return to their generated `ActionsApi` interface for full type safety.
8
+ * Pass the consumer's generated `ActionsApi` interface as `<TApi>` to flow
9
+ * full type safety through without an explicit cast at the call site.
9
10
  *
10
11
  * @module
11
12
  */
@@ -20,18 +21,35 @@ import { jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
20
21
  * - `remote_notification` → send notification, return Result
21
22
  * - `local_call` → execute locally (sync or async), return Result or throw
22
23
  *
23
- * @param options - client options (peer, environment, optional action history)
24
- * @returns a Proxy that responds to any method name found in the environment's specs
24
+ * Generic `TApi` is the consumer's typed Proxy interface (typically a
25
+ * codegen-derived `ActionsApi`). Required no default, so forgetting it
26
+ * is a type error rather than a silent slide into `any`. The `as unknown
27
+ * as TApi` coercion lives inside this function so call sites get a typed
28
+ * return without a cast at the seam. `TApi` is a type-layer promise about
29
+ * what the Proxy responds to; the runtime walks `specs` (kept in sync by
30
+ * the consumer, codegen recommended).
31
+ *
32
+ * ```ts
33
+ * const api_result = create_rpc_client<MyActionsApi>({peer, environment});
34
+ * ```
35
+ *
36
+ * @param options - client options (peer, environment, optional callbacks)
37
+ * @returns a Proxy typed as `TApi` that responds to any method name found in the environment's specs
25
38
  */
26
39
  export const create_rpc_client = (options) => {
27
- const { peer, environment, actions, transport_for_method } = options;
40
+ const { peer, environment, on_action_event, transport_for_method } = options;
41
+ // Internal factories construct broadly-typed `ActionEvent` instances; the
42
+ // public callback narrows `event.spec.method` to `keyof TApi & string`.
43
+ // Cast once here — function parameters are contravariant, so the narrow
44
+ // callback isn't directly assignable to the broad slot the helpers take.
45
+ const broad_on_action_event = on_action_event;
28
46
  return new Proxy({}, {
29
47
  get(_target, method) {
30
48
  const spec = environment.lookup_action_spec(method);
31
49
  if (!spec) {
32
50
  return undefined;
33
51
  }
34
- return create_action_method(peer, environment, spec, actions, transport_for_method);
52
+ return create_action_method(peer, environment, spec, broad_on_action_event, transport_for_method);
35
53
  },
36
54
  has(_target, method) {
37
55
  return environment.lookup_action_spec(method) !== undefined;
@@ -41,30 +59,26 @@ export const create_rpc_client = (options) => {
41
59
  /**
42
60
  * Creates a method that executes an action through its complete lifecycle.
43
61
  */
44
- const create_action_method = (peer, environment, spec, actions, transport_for_method) => {
62
+ const create_action_method = (peer, environment, spec, on_action_event, transport_for_method) => {
45
63
  switch (spec.kind) {
46
64
  case 'local_call':
47
65
  return spec.async
48
- ? create_async_local_call_method(environment, spec, actions)
49
- : create_sync_local_call_method(environment, spec, actions);
66
+ ? create_async_local_call_method(environment, spec, on_action_event)
67
+ : create_sync_local_call_method(environment, spec, on_action_event);
50
68
  case 'request_response':
51
- return create_request_response_method(peer, environment, spec, actions, transport_for_method);
69
+ return create_request_response_method(peer, environment, spec, on_action_event, transport_for_method);
52
70
  case 'remote_notification':
53
- return create_remote_notification_method(peer, environment, spec, actions, transport_for_method);
71
+ return create_remote_notification_method(peer, environment, spec, on_action_event, transport_for_method);
54
72
  }
55
73
  };
56
74
  /**
57
75
  * Creates a synchronous local call method.
58
76
  * Returns value directly - can throw on error (sync methods cannot return Result).
59
77
  */
60
- const create_sync_local_call_method = (environment, spec, actions) => {
78
+ const create_sync_local_call_method = (environment, spec, on_action_event) => {
61
79
  return (input) => {
62
80
  const event = create_action_event(environment, spec, input);
63
- const action = actions?.add_from_json({
64
- method: spec.method,
65
- action_event_data: event.toJSON(),
66
- });
67
- action?.listen_to_action_event(event);
81
+ on_action_event?.(event);
68
82
  event.parse().handle_sync();
69
83
  const result = extract_action_result(event);
70
84
  if (result.ok) {
@@ -84,7 +98,7 @@ const create_sync_local_call_method = (environment, spec, actions) => {
84
98
  * `signal` can only short-circuit before the synchronous handler runs (no
85
99
  * cooperative interrupt mid-handler).
86
100
  */
87
- const create_async_local_call_method = (environment, spec, actions) => {
101
+ const create_async_local_call_method = (environment, spec, on_action_event) => {
88
102
  return async (input, options) => {
89
103
  if (options?.signal?.aborted) {
90
104
  return {
@@ -93,11 +107,7 @@ const create_async_local_call_method = (environment, spec, actions) => {
93
107
  };
94
108
  }
95
109
  const event = create_action_event(environment, spec, input);
96
- const action = actions?.add_from_json({
97
- method: spec.method,
98
- action_event_data: event.toJSON(),
99
- });
100
- action?.listen_to_action_event(event);
110
+ on_action_event?.(event);
101
111
  await event.parse().handle_async();
102
112
  return extract_action_result(event);
103
113
  };
@@ -105,14 +115,10 @@ const create_async_local_call_method = (environment, spec, actions) => {
105
115
  /**
106
116
  * Creates a request/response method that communicates over the network.
107
117
  */
108
- const create_request_response_method = (peer, environment, spec, actions, transport_for_method) => {
118
+ const create_request_response_method = (peer, environment, spec, on_action_event, transport_for_method) => {
109
119
  return async (input, options) => {
110
120
  const event = create_action_event(environment, spec, input);
111
- const action = actions?.add_from_json({
112
- method: spec.method,
113
- action_event_data: event.toJSON(),
114
- });
115
- action?.listen_to_action_event(event);
121
+ on_action_event?.(event);
116
122
  await event.parse().handle_async();
117
123
  // Check if we're in send_error phase before type narrowing
118
124
  if (event.data.kind === 'request_response' && event.data.phase === 'send_error') {
@@ -141,14 +147,10 @@ const create_request_response_method = (peer, environment, spec, actions, transp
141
147
  * Creates a remote notification method (fire and forget).
142
148
  * Returns Result<{value: void}> for consistency.
143
149
  */
144
- const create_remote_notification_method = (peer, environment, spec, actions, transport_for_method) => {
150
+ const create_remote_notification_method = (peer, environment, spec, on_action_event, transport_for_method) => {
145
151
  return async (input, options) => {
146
152
  const event = create_action_event(environment, spec, input);
147
- const action = actions?.add_from_json({
148
- method: spec.method,
149
- action_event_data: event.toJSON(),
150
- });
151
- action?.listen_to_action_event(event);
153
+ on_action_event?.(event);
152
154
  await event.parse().handle_async();
153
155
  if (!is_notification_send(event.data))
154
156
  throw Error(); // TODO @many maybe make this an assertion helper?
@@ -169,56 +171,76 @@ const create_remote_notification_method = (peer, environment, spec, actions, tra
169
171
  };
170
172
  };
171
173
  /**
172
- * Wrap a typed RPC client so every call returns its unwrapped value or throws.
174
+ * Wrap a typed RPC client so every call resolves to its unwrapped value or
175
+ * throws an `Error` carrying the JSON-RPC `{code, message, data?}` shape.
176
+ *
177
+ * Implementation is a Proxy because the underlying `create_rpc_client`
178
+ * return is itself a Proxy with no concrete keys — a key-by-key wrap would
179
+ * need to enumerate the typed surface, which only the consumer's generated
180
+ * `ActionsApi` interface knows.
181
+ *
182
+ * Pass-through on non-Result returns is deliberate: sync `local_call`
183
+ * Proxy methods return values directly (see `create_sync_local_call_method`
184
+ * above). The Proxy can't distinguish those at get-time, so the wrapper
185
+ * inspects `result` shape at call-time and only unwraps when it sees a
186
+ * Result. Non-object returns pass through unchanged.
173
187
  *
174
- * On `{ok: false}`, throws an `Error` whose `message` comes from the
175
- * JSON-RPC error object, plus `{code, data}` as own properties so
176
- * catch blocks reading `err.message` / `err.code` / `err.data?.reason`
177
- * all work. On unknown method, throws a clear "rpc method not found"
178
- * error instead of the cryptic `undefined is not a function` that
179
- * would otherwise surface.
188
+ * Only `{code, data}` cross onto the thrown Error `name` / `stack` are
189
+ * left as the Error's own properties so attacker-shaped `result.error`
190
+ * payloads cannot overwrite them.
180
191
  *
181
- * Invariant upheld by `create_rpc_client`: every `{ok: false}` return
182
- * carries a well-formed `JsonrpcErrorObject` with `code` + `message`.
183
- * Callers must still use optional chaining on `err.data` because the
184
- * JSON-RPC `data` field is spec-level optional a handler that throws
185
- * `jsonrpc_errors.forbidden()` without a `data` argument produces
186
- * `err.data === undefined`.
192
+ * Recommended consumer convention: `create_frontend_rpc_client` ships
193
+ * both shapes by default `api` (throwing) for hot-path call sites and
194
+ * `api_result` (Result) for sites that inspect `error.data.reason`
195
+ * without try/catch. Result is the protocol primitive; this wrapper is
196
+ * the ergonomic layer over it. Picking is per call site — both Proxies
197
+ * share the same underlying transport.
187
198
  *
188
- * Only `{code, data}` cross onto the thrown Error — `message` flows
189
- * through the `Error` constructor argument, and `name` / `stack` are
190
- * left as the Error's own so attacker-shaped `result.error` payloads
191
- * cannot overwrite them.
199
+ * Catch blocks read `err.data?.reason` optional chaining required
200
+ * because JSON-RPC `data` is spec-level optional.
192
201
  *
193
- * The mapped-type generic constraint accepts both shapes without a cast:
194
- * a codegen-derived typed `ActionsApi` (named-method interface, e.g.
195
- * `{account_verify: (input) => Promise<Result<...>>, ...}`) and a loose
196
- * `Record<string, (input?: any) => Promise<any> | void>`. Using `keyof TApi`
197
- * in the constraint avoids the index-signature requirement that would
198
- * otherwise force consumers to `as unknown as Record<string, …>` their
199
- * generated client. The `| void` arm tolerates `remote_notification`
200
- * methods, whose `ActionsApi` signature is `(input) => void` even though
201
- * `create_remote_notification_method` returns a Promise at runtime — the
202
- * throwing wrapper is intended for `request_response` calls but must
203
- * accept mixed `ActionsApi` shapes without forcing a cast at the seam.
202
+ * On unknown string-keyed methods, the get trap returns a function that
203
+ * throws `"rpc method not found: <prop>"` on invocation — clearer than
204
+ * the JS default `"api.foo is not a function"`. Symbol props and `then`
205
+ * stay undefined so the Proxy isn't accidentally treated as a thenable
206
+ * (`await api` would otherwise probe `then` and trip the thrower).
204
207
  *
205
- * @param api - typed RPC client from `create_rpc_client` (or any object
206
- * whose values are all `(input?) => Promise<...> | void` functions —
207
- * notably the consumer's generated `ActionsApi` interface)
208
+ * @param api_result - typed Result-returning RPC client from
209
+ * `create_rpc_client<ActionsApi>(...)`. The "_result" suffix names
210
+ * what the underlying calls return (`Result<{value}, {error}>`).
208
211
  */
209
- export const create_throwing_rpc_call = (api) => {
210
- const rec = api;
211
- return async (method, input) => {
212
- const fn = rec[method];
213
- if (!fn)
214
- throw new Error(`rpc method not found: ${method}`);
215
- const result = await fn(input);
216
- if (!result.ok) {
217
- throw Object.assign(new Error(result.error?.message ?? 'rpc error'), {
218
- code: result.error?.code,
219
- data: result.error?.data,
220
- });
221
- }
222
- return result.value;
223
- };
212
+ export const create_throwing_api = (api_result) => {
213
+ return new Proxy(api_result, {
214
+ get(target, prop) {
215
+ const fn = target[prop];
216
+ if (typeof fn === 'function') {
217
+ return async (...args) => {
218
+ const result = await fn.apply(target, args);
219
+ if (result === null || typeof result !== 'object')
220
+ return result;
221
+ const r = result;
222
+ if (r.ok === true)
223
+ return r.value;
224
+ if (r.ok === false && r.error && typeof r.error === 'object') {
225
+ const e = r.error;
226
+ throw Object.assign(new Error(e.message), {
227
+ code: e.code,
228
+ data: e.data,
229
+ });
230
+ }
231
+ return result;
232
+ };
233
+ }
234
+ if (fn !== undefined)
235
+ return fn;
236
+ // Underlying api has no member by this name. Symbol props and
237
+ // `then` must stay undefined — `await tapi` reads `then` and
238
+ // would otherwise trip the thrower.
239
+ if (typeof prop !== 'string' || prop === 'then')
240
+ return undefined;
241
+ return () => {
242
+ throw new Error(`rpc method not found: ${prop}`);
243
+ };
244
+ },
245
+ });
224
246
  };
@@ -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`,
@@ -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 two `satisfies RequestResponseActionSpec` literals, 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 specs and pairs them with handlers.
1091
-
1092
- Two static `request_response` actions — `self_service_role_grant` and
1093
- `self_service_role_revoke` — that take `{role}` as input and toggle a
1094
- global permit on the caller. Both are idempotent: `granted: false` when
1095
- the caller already holds the role, `revoked: false` when they don't.
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 names are static — `role` lives in the input, not the 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 path uses `query_permit_has_role` for a benign-TOCTOU pre-check
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 path filters
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: Array<RequestResponseActionSpec>` —
1130
- codegen-ready registry of both specs.
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
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Self-service role grant/revoke action specs — schemas, error reasons,
2
+ * Unified self-service role toggle action spec — schemas, error reasons,
3
3
  * and the codegen-ready registry.
4
4
  *
5
5
  * Client-safe: no query-layer or audit-write imports. Handler factory
@@ -11,54 +11,24 @@ import { z } from 'zod';
11
11
  import type { RequestResponseActionSpec } from '../actions/action_spec.js';
12
12
  /** Error reason — caller asked to self-toggle a role outside the configured allowlist. */
13
13
  export declare const ERROR_ROLE_NOT_SELF_SERVICE_ELIGIBLE: "role_not_self_service_eligible";
14
- /** Input for `self_service_role_grant`. */
15
- export declare const SelfServiceRoleGrantInput: z.ZodObject<{
14
+ /** Input for `self_service_role_set`. */
15
+ export declare const SelfServiceRoleSetInput: z.ZodObject<{
16
16
  role: z.ZodString;
17
+ enabled: z.ZodBoolean;
17
18
  }, z.core.$strict>;
18
- export type SelfServiceRoleGrantInput = z.infer<typeof SelfServiceRoleGrantInput>;
19
+ export type SelfServiceRoleSetInput = z.infer<typeof SelfServiceRoleSetInput>;
19
20
  /**
20
- * Output for `self_service_role_grant`. `granted` is `false` on idempotent
21
- * re-grant (caller already held the role globally); `permit_id` is set on
22
- * new grants only.
21
+ * Output for `self_service_role_set`. `enabled` echoes the post-call state
22
+ * (always equals the input `enabled` on success). `changed` is `true` only
23
+ * when the call mutated — re-grants / re-revokes return `false`.
23
24
  */
24
- export declare const SelfServiceRoleGrantOutput: z.ZodObject<{
25
+ export declare const SelfServiceRoleSetOutput: z.ZodObject<{
25
26
  ok: z.ZodLiteral<true>;
26
- granted: z.ZodBoolean;
27
- permit_id: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
27
+ enabled: z.ZodBoolean;
28
+ changed: z.ZodBoolean;
28
29
  }, z.core.$strict>;
29
- export type SelfServiceRoleGrantOutput = z.infer<typeof SelfServiceRoleGrantOutput>;
30
- /** Input for `self_service_role_revoke`. */
31
- export declare const SelfServiceRoleRevokeInput: z.ZodObject<{
32
- role: z.ZodString;
33
- }, z.core.$strict>;
34
- export type SelfServiceRoleRevokeInput = z.infer<typeof SelfServiceRoleRevokeInput>;
35
- /**
36
- * Output for `self_service_role_revoke`. `revoked` is `false` when the
37
- * caller held no active global permit for the role (idempotent).
38
- */
39
- export declare const SelfServiceRoleRevokeOutput: z.ZodObject<{
40
- ok: z.ZodLiteral<true>;
41
- revoked: z.ZodBoolean;
42
- }, z.core.$strict>;
43
- export type SelfServiceRoleRevokeOutput = z.infer<typeof SelfServiceRoleRevokeOutput>;
44
- export declare const self_service_role_grant_action_spec: {
45
- method: string;
46
- kind: "request_response";
47
- initiator: "frontend";
48
- auth: "authenticated";
49
- side_effects: true;
50
- input: z.ZodObject<{
51
- role: z.ZodString;
52
- }, z.core.$strict>;
53
- output: z.ZodObject<{
54
- ok: z.ZodLiteral<true>;
55
- granted: z.ZodBoolean;
56
- permit_id: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
57
- }, z.core.$strict>;
58
- async: true;
59
- description: string;
60
- };
61
- export declare const self_service_role_revoke_action_spec: {
30
+ export type SelfServiceRoleSetOutput = z.infer<typeof SelfServiceRoleSetOutput>;
31
+ export declare const self_service_role_set_action_spec: {
62
32
  method: string;
63
33
  kind: "request_response";
64
34
  initiator: "frontend";
@@ -66,18 +36,20 @@ export declare const self_service_role_revoke_action_spec: {
66
36
  side_effects: true;
67
37
  input: z.ZodObject<{
68
38
  role: z.ZodString;
39
+ enabled: z.ZodBoolean;
69
40
  }, z.core.$strict>;
70
41
  output: z.ZodObject<{
71
42
  ok: z.ZodLiteral<true>;
72
- revoked: z.ZodBoolean;
43
+ enabled: z.ZodBoolean;
44
+ changed: z.ZodBoolean;
73
45
  }, z.core.$strict>;
74
46
  async: true;
75
47
  description: string;
76
48
  };
77
49
  /**
78
- * All self-service role action specs — a codegen-ready registry. Method
79
- * names are static, so consumer typed-client codegen picks them up the
80
- * same way it picks up `account_*_action_specs`.
50
+ * All self-service role action specs — a codegen-ready registry. Single-element
51
+ * post-unification, kept for symmetry with the other `all_*_action_specs`
52
+ * exports so codegen and frontend bundles import the same shape.
81
53
  */
82
- export declare const all_self_service_role_action_specs: Array<RequestResponseActionSpec>;
54
+ export declare const all_self_service_role_action_specs: ReadonlyArray<RequestResponseActionSpec>;
83
55
  //# sourceMappingURL=self_service_role_action_specs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"self_service_role_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/self_service_role_action_specs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,2BAA2B,CAAC;AAGzE,0FAA0F;AAC1F,eAAO,MAAM,oCAAoC,EAAG,gCAAyC,CAAC;AAE9F,2CAA2C;AAC3C,eAAO,MAAM,yBAAyB;;kBAEpC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAElF;;;;GAIG;AACH,eAAO,MAAM,0BAA0B;;;;kBAIrC,CAAC;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAEpF,4CAA4C;AAC5C,eAAO,MAAM,0BAA0B;;kBAErC,CAAC;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAEpF;;;GAGG;AACH,eAAO,MAAM,2BAA2B;;;kBAGtC,CAAC;AACH,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEtF,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;CAWX,CAAC;AAEtC,eAAO,MAAM,oCAAoC;;;;;;;;;;;;;;;CAWZ,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,EAAE,KAAK,CAAC,yBAAyB,CAG/E,CAAC"}
1
+ {"version":3,"file":"self_service_role_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/self_service_role_action_specs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,2BAA2B,CAAC;AAGzE,0FAA0F;AAC1F,eAAO,MAAM,oCAAoC,EAAG,gCAAyC,CAAC;AAE9F,yCAAyC;AACzC,eAAO,MAAM,uBAAuB;;;kBAMlC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAE9E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;;;;kBAInC,CAAC;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;;CAWT,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,EAAE,aAAa,CAAC,yBAAyB,CAEvF,CAAC"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Self-service role grant/revoke action specs — schemas, error reasons,
2
+ * Unified self-service role toggle action spec — schemas, error reasons,
3
3
  * and the codegen-ready registry.
4
4
  *
5
5
  * Client-safe: no query-layer or audit-write imports. Handler factory
@@ -8,64 +8,42 @@
8
8
  * @module
9
9
  */
10
10
  import { z } from 'zod';
11
- import { Uuid } from '@fuzdev/fuz_util/id.js';
12
11
  import { RoleName } from './role_schema.js';
13
12
  /** Error reason — caller asked to self-toggle a role outside the configured allowlist. */
14
13
  export const ERROR_ROLE_NOT_SELF_SERVICE_ELIGIBLE = 'role_not_self_service_eligible';
15
- /** Input for `self_service_role_grant`. */
16
- export const SelfServiceRoleGrantInput = z.strictObject({
17
- role: RoleName.meta({ description: 'Role to self-grant. Must be in the configured allowlist.' }),
14
+ /** Input for `self_service_role_set`. */
15
+ export const SelfServiceRoleSetInput = z.strictObject({
16
+ role: RoleName.meta({ description: 'Role to toggle. Must be in the configured allowlist.' }),
17
+ enabled: z.boolean().meta({
18
+ description: 'Desired post-call state. `true` grants if not held; `false` revokes if held. Idempotent in both directions.',
19
+ }),
18
20
  });
19
21
  /**
20
- * Output for `self_service_role_grant`. `granted` is `false` on idempotent
21
- * re-grant (caller already held the role globally); `permit_id` is set on
22
- * new grants only.
22
+ * Output for `self_service_role_set`. `enabled` echoes the post-call state
23
+ * (always equals the input `enabled` on success). `changed` is `true` only
24
+ * when the call mutated — re-grants / re-revokes return `false`.
23
25
  */
24
- export const SelfServiceRoleGrantOutput = z.strictObject({
26
+ export const SelfServiceRoleSetOutput = z.strictObject({
25
27
  ok: z.literal(true),
26
- granted: z.boolean(),
27
- permit_id: Uuid.optional(),
28
+ enabled: z.boolean(),
29
+ changed: z.boolean(),
28
30
  });
29
- /** Input for `self_service_role_revoke`. */
30
- export const SelfServiceRoleRevokeInput = z.strictObject({
31
- role: RoleName.meta({ description: 'Role to self-revoke. Must be in the configured allowlist.' }),
32
- });
33
- /**
34
- * Output for `self_service_role_revoke`. `revoked` is `false` when the
35
- * caller held no active global permit for the role (idempotent).
36
- */
37
- export const SelfServiceRoleRevokeOutput = z.strictObject({
38
- ok: z.literal(true),
39
- revoked: z.boolean(),
40
- });
41
- export const self_service_role_grant_action_spec = {
42
- method: 'self_service_role_grant',
43
- kind: 'request_response',
44
- initiator: 'frontend',
45
- auth: 'authenticated',
46
- side_effects: true,
47
- input: SelfServiceRoleGrantInput,
48
- output: SelfServiceRoleGrantOutput,
49
- async: true,
50
- description: 'Self-grant an active permit for an allowlisted role. Idempotent — already-granted callers receive `granted: false`.',
51
- };
52
- export const self_service_role_revoke_action_spec = {
53
- method: 'self_service_role_revoke',
31
+ export const self_service_role_set_action_spec = {
32
+ method: 'self_service_role_set',
54
33
  kind: 'request_response',
55
34
  initiator: 'frontend',
56
35
  auth: 'authenticated',
57
36
  side_effects: true,
58
- input: SelfServiceRoleRevokeInput,
59
- output: SelfServiceRoleRevokeOutput,
37
+ input: SelfServiceRoleSetInput,
38
+ output: SelfServiceRoleSetOutput,
60
39
  async: true,
61
- description: 'Self-revoke an active global permit for an allowlisted role. Idempotent callers without an active permit receive `revoked: false`.',
40
+ description: 'Toggle a self-service role. Idempotent in both directions `changed: false` when post-call state already matched the request.',
62
41
  };
63
42
  /**
64
- * All self-service role action specs — a codegen-ready registry. Method
65
- * names are static, so consumer typed-client codegen picks them up the
66
- * same way it picks up `account_*_action_specs`.
43
+ * All self-service role action specs — a codegen-ready registry. Single-element
44
+ * post-unification, kept for symmetry with the other `all_*_action_specs`
45
+ * exports so codegen and frontend bundles import the same shape.
67
46
  */
68
47
  export const all_self_service_role_action_specs = [
69
- self_service_role_grant_action_spec,
70
- self_service_role_revoke_action_spec,
48
+ self_service_role_set_action_spec,
71
49
  ];
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Self-service role grant/revoke RPC actions.
2
+ * Unified self-service role toggle RPC action.
3
3
  *
4
- * Two static `request_response` actions — `self_service_role_grant` and
5
- * `self_service_role_revoke` — that take `{role}` as input and toggle a
6
- * permit on the caller for an allowlisted role. Idempotent in both
7
- * directions: re-granting an already-held role returns `granted: false`;
8
- * revoking a role the caller doesn't hold returns `revoked: false`.
4
+ * One static `request_response` action — `self_service_role_set` — that
5
+ * takes `{role, enabled}` and toggles a global permit on the caller for an
6
+ * allowlisted role. Idempotent in both directions: re-enabling an
7
+ * already-held role returns `changed: false`; disabling a role the caller
8
+ * doesn't hold returns `changed: false`.
9
9
  *
10
10
  * The factory takes an `eligible_roles` allowlist (validated against the
11
11
  * supplied `roles.role_options` at factory time so typos surface at startup
@@ -19,8 +19,8 @@
19
19
  * part of the documented schema surface and is round-trip-validated by
20
20
  * `query_audit_log`.
21
21
  *
22
- * Static method names — `role` lives in the input, not the method name —
23
- * so specs are codegen-compatible (`satisfies RequestResponseActionSpec`)
22
+ * Static method name — `role` lives in the input, not the method name —
23
+ * so the spec is codegen-compatible (`satisfies RequestResponseActionSpec`)
24
24
  * and the surface stays constant as consumers add eligible roles. Mirrors
25
25
  * the existing `permit_offer_create({role})` precedent rather than
26
26
  * generating per-role methods.
@@ -57,7 +57,7 @@ export interface SelfServiceRoleActionsOptions {
57
57
  */
58
58
  export type SelfServiceRoleActionDeps = Pick<RouteFactoryDeps, 'log' | 'on_audit_event' | 'audit_log_config'>;
59
59
  /**
60
- * Build the self-service role grant/revoke RPC actions.
60
+ * Build the unified self-service role toggle RPC action.
61
61
  *
62
62
  * @param deps - `SelfServiceRoleActionDeps` slice of `AppDeps` (`log`, `on_audit_event`, optional `audit_log_config`)
63
63
  * @param options - eligible-role allowlist plus optional role schema for typo-checking
@@ -1 +1 @@
1
- {"version":3,"file":"self_service_role_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/self_service_role_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAiC,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAExF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAmBhD,sDAAsD;AACtD,MAAM,WAAW,6BAA6B;IAC7C;;;;OAIG;IACH,cAAc,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACtC;;;;OAIG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAC3C,gBAAgB,EAChB,KAAK,GAAG,gBAAgB,GAAG,kBAAkB,CAC7C,CAAC;AAOF;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,yBAAyB,EAC/B,SAAS,6BAA6B,KACpC,KAAK,CAAC,SAAS,CAqHjB,CAAC"}
1
+ {"version":3,"file":"self_service_role_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/self_service_role_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAiC,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAExF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAgBhD,sDAAsD;AACtD,MAAM,WAAW,6BAA6B;IAC7C;;;;OAIG;IACH,cAAc,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACtC;;;;OAIG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAC3C,gBAAgB,EAChB,KAAK,GAAG,gBAAgB,GAAG,kBAAkB,CAC7C,CAAC;AAOF;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,yBAAyB,EAC/B,SAAS,6BAA6B,KACpC,KAAK,CAAC,SAAS,CA4GjB,CAAC"}