@fuzdev/fuz_app 0.63.0 → 0.64.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 (107) hide show
  1. package/dist/actions/CLAUDE.md +124 -11
  2. package/dist/actions/connection_closer.d.ts +68 -0
  3. package/dist/actions/connection_closer.d.ts.map +1 -0
  4. package/dist/actions/connection_closer.js +41 -0
  5. package/dist/actions/register_action_ws.d.ts.map +1 -1
  6. package/dist/actions/register_action_ws.js +23 -2
  7. package/dist/actions/register_ws_endpoint.d.ts +11 -9
  8. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  9. package/dist/actions/register_ws_endpoint.js +5 -5
  10. package/dist/actions/transports_ws_auth_guard.d.ts +24 -8
  11. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  12. package/dist/actions/transports_ws_auth_guard.js +23 -7
  13. package/dist/actions/ws_endpoint_spec.d.ts +119 -0
  14. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -0
  15. package/dist/actions/ws_endpoint_spec.js +13 -0
  16. package/dist/auth/CLAUDE.md +79 -15
  17. package/dist/auth/account_action_specs.d.ts +1 -1
  18. package/dist/auth/account_actions.d.ts +13 -0
  19. package/dist/auth/account_actions.d.ts.map +1 -1
  20. package/dist/auth/account_actions.js +31 -1
  21. package/dist/auth/account_routes.d.ts +12 -2
  22. package/dist/auth/account_routes.d.ts.map +1 -1
  23. package/dist/auth/account_routes.js +55 -8
  24. package/dist/auth/account_schema.d.ts +3 -3
  25. package/dist/auth/admin_action_specs.d.ts +8 -8
  26. package/dist/auth/admin_actions.d.ts +11 -0
  27. package/dist/auth/admin_actions.d.ts.map +1 -1
  28. package/dist/auth/admin_actions.js +25 -0
  29. package/dist/auth/audit_emitter.d.ts +56 -12
  30. package/dist/auth/audit_emitter.d.ts.map +1 -1
  31. package/dist/auth/audit_emitter.js +38 -12
  32. package/dist/auth/audit_log_schema.d.ts +5 -3
  33. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  34. package/dist/auth/audit_log_schema.js +5 -3
  35. package/dist/auth/bootstrap_routes.d.ts +1 -1
  36. package/dist/auth/invite_schema.d.ts +2 -2
  37. package/dist/auth/signup_routes.d.ts +1 -1
  38. package/dist/auth/standard_rpc_actions.d.ts +1 -0
  39. package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
  40. package/dist/auth/standard_rpc_actions.js +1 -0
  41. package/dist/http/CLAUDE.md +26 -10
  42. package/dist/http/ip_canonical.d.ts +99 -0
  43. package/dist/http/ip_canonical.d.ts.map +1 -0
  44. package/dist/http/ip_canonical.js +191 -0
  45. package/dist/http/origin.d.ts +13 -5
  46. package/dist/http/origin.d.ts.map +1 -1
  47. package/dist/http/origin.js +13 -31
  48. package/dist/http/pending_effects.d.ts +1 -1
  49. package/dist/http/pending_effects.js +1 -1
  50. package/dist/http/proxy.d.ts +13 -5
  51. package/dist/http/proxy.d.ts.map +1 -1
  52. package/dist/http/proxy.js +15 -23
  53. package/dist/http/surface.d.ts +50 -0
  54. package/dist/http/surface.d.ts.map +1 -1
  55. package/dist/http/surface.js +27 -1
  56. package/dist/primitive_schemas.d.ts +20 -4
  57. package/dist/primitive_schemas.d.ts.map +1 -1
  58. package/dist/primitive_schemas.js +25 -4
  59. package/dist/realtime/sse_auth_guard.d.ts +16 -4
  60. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  61. package/dist/realtime/sse_auth_guard.js +15 -3
  62. package/dist/server/app_backend.d.ts +66 -19
  63. package/dist/server/app_backend.d.ts.map +1 -1
  64. package/dist/server/app_backend.js +57 -34
  65. package/dist/server/app_server.d.ts +60 -0
  66. package/dist/server/app_server.d.ts.map +1 -1
  67. package/dist/server/app_server.js +95 -2
  68. package/dist/server/startup.d.ts.map +1 -1
  69. package/dist/server/startup.js +12 -0
  70. package/dist/testing/CLAUDE.md +64 -28
  71. package/dist/testing/admin_integration.d.ts.map +1 -1
  72. package/dist/testing/admin_integration.js +4 -5
  73. package/dist/testing/adversarial_headers.d.ts +6 -0
  74. package/dist/testing/adversarial_headers.d.ts.map +1 -1
  75. package/dist/testing/adversarial_headers.js +13 -5
  76. package/dist/testing/app_server.d.ts +33 -32
  77. package/dist/testing/app_server.d.ts.map +1 -1
  78. package/dist/testing/app_server.js +4 -13
  79. package/dist/testing/attack_surface.d.ts +8 -7
  80. package/dist/testing/attack_surface.d.ts.map +1 -1
  81. package/dist/testing/attack_surface.js +12 -8
  82. package/dist/testing/audit_completeness.d.ts.map +1 -1
  83. package/dist/testing/audit_completeness.js +3 -5
  84. package/dist/testing/audit_drift_guard.d.ts +116 -0
  85. package/dist/testing/audit_drift_guard.d.ts.map +1 -0
  86. package/dist/testing/audit_drift_guard.js +134 -0
  87. package/dist/testing/connection_closer_helpers.d.ts +44 -0
  88. package/dist/testing/connection_closer_helpers.d.ts.map +1 -0
  89. package/dist/testing/connection_closer_helpers.js +48 -0
  90. package/dist/testing/integration.d.ts.map +1 -1
  91. package/dist/testing/integration.js +7 -9
  92. package/dist/testing/rate_limiting.js +4 -4
  93. package/dist/testing/rpc_helpers.d.ts +2 -1
  94. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  95. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  96. package/dist/testing/rpc_round_trip.js +6 -8
  97. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  98. package/dist/testing/sse_round_trip.js +12 -6
  99. package/dist/testing/stubs.d.ts +11 -0
  100. package/dist/testing/stubs.d.ts.map +1 -1
  101. package/dist/testing/stubs.js +4 -0
  102. package/dist/testing/surface_invariants.d.ts +66 -1
  103. package/dist/testing/surface_invariants.d.ts.map +1 -1
  104. package/dist/testing/surface_invariants.js +103 -1
  105. package/dist/ui/SurfaceExplorer.svelte +161 -2
  106. package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
  107. package/package.json +1 -1
@@ -96,6 +96,52 @@ export interface ErrorSchemaAuditEntry {
96
96
  * @returns audit entries for every route x status combination
97
97
  */
98
98
  export declare const audit_error_schema_tightness: (surface: AppSurface) => Array<ErrorSchemaAuditEntry>;
99
+ /**
100
+ * Every RPC method on every endpoint has a non-empty `description`.
101
+ *
102
+ * Parallel of `assert_descriptions_present` over `surface.rpc_endpoints`.
103
+ * Empty descriptions on RPC methods leak through `library.json` codegen and
104
+ * consumer-facing docs without the route-level check catching them.
105
+ */
106
+ export declare const assert_rpc_method_descriptions_present: (surface: AppSurface) => void;
107
+ /**
108
+ * Every WS method on every endpoint has a non-empty `description`.
109
+ *
110
+ * Parallel of `assert_descriptions_present` over `surface.ws_endpoints`.
111
+ * Same rationale as `assert_rpc_method_descriptions_present` — codegen +
112
+ * surface explorer consume the description, blank values surface as `''`.
113
+ */
114
+ export declare const assert_ws_method_descriptions_present: (surface: AppSurface) => void;
115
+ /**
116
+ * Every WS endpoint's `methods` includes every protocol action method
117
+ * (`heartbeat`, `cancel`).
118
+ *
119
+ * Consumers register WS endpoints by spreading `protocol_actions` from
120
+ * `actions/protocol.ts` before their own actions:
121
+ *
122
+ * ```ts
123
+ * ws_endpoints: [{path: '/api/ws', actions: [...protocol_actions, ...consumer_actions], ...}]
124
+ * ```
125
+ *
126
+ * Forgetting the spread compiles cleanly but breaks at runtime: client-side
127
+ * heartbeats and `cancel` notifications get `method_not_found` from the
128
+ * dispatcher, so disconnect detection silently regresses and per-request
129
+ * cancel never aborts the matching handler. Catch the mistake at the
130
+ * surface layer rather than at runtime.
131
+ */
132
+ export declare const assert_ws_endpoints_include_protocol_actions: (surface: AppSurface) => void;
133
+ /**
134
+ * WS methods follow the kind ⇔ auth biconditional emitted by surface
135
+ * generation: `kind === 'remote_notification' ⟺ auth === null`.
136
+ *
137
+ * `generate_app_surface` produces this shape directly from the action
138
+ * spec union (notifications carry `auth: null` per `ActionSpecUnion`;
139
+ * `request_response` carries a `RouteAuth`). The assertion guards against
140
+ * drift if a future surface emitter, transform, or test fixture violates
141
+ * it — and gives consumers a clear failure message when a hand-built
142
+ * surface mocks the shape incorrectly.
143
+ */
144
+ export declare const assert_ws_notifications_have_null_auth: (surface: AppSurface) => void;
99
145
  /**
100
146
  * Configuration for security policy invariants.
101
147
  *
@@ -146,7 +192,9 @@ export declare const assert_no_unexpected_public_mutations: (surface: AppSurface
146
192
  *
147
193
  * Note: RPC endpoints (`create_rpc_endpoint`) use `input: z.null()` on their
148
194
  * route specs — the dispatcher handles body/query parsing internally. Real input
149
- * schemas live in `rpc_endpoints` surface, not on routes.
195
+ * schemas live in `rpc_endpoints` surface, not on routes; see
196
+ * `assert_rpc_ws_surface_invariants` for the parallel checks over RPC/WS
197
+ * method shapes.
150
198
  */
151
199
  export declare const assert_mutation_routes_use_post: (surface: AppSurface) => void;
152
200
  /**
@@ -223,6 +271,23 @@ export declare const assert_error_schema_tightness: (surface: AppSurface, option
223
271
  * the offending route and the missing/inconsistent field.
224
272
  */
225
273
  export declare const assert_surface_invariants: (surface: AppSurface) => void;
274
+ /**
275
+ * Run all RPC / WS structural invariants. Options-free — applies
276
+ * universally to the `surface.rpc_endpoints` and `surface.ws_endpoints`
277
+ * slots produced by `generate_app_surface`.
278
+ *
279
+ * Parallel of `assert_surface_invariants` for the non-REST surfaces.
280
+ * Within-endpoint duplicate method names and the auth-shape biconditional
281
+ * are already enforced at startup by `compile_action_registry` (see
282
+ * `actions/CLAUDE.md` §Registry compile) — these assertions cover only
283
+ * the contract-surface concerns that a runtime registration check
284
+ * cannot: empty descriptions, missing protocol-action spread on WS
285
+ * endpoints, and kind ⇔ auth drift on WS methods.
286
+ *
287
+ * @throws AssertionError on the first invariant violation; the message
288
+ * names the offending endpoint, method, and field.
289
+ */
290
+ export declare const assert_rpc_ws_surface_invariants: (surface: AppSurface) => void;
226
291
  /**
227
292
  * Run security policy invariants. Configurable with sensible defaults.
228
293
  *
@@ -1 +1 @@
1
- {"version":3,"file":"surface_invariants.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/surface_invariants.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"surface_invariants.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/surface_invariants.ts"],"names":[],"mappings":"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"}
@@ -19,6 +19,7 @@ import './assert_dev_env.js';
19
19
  import { assert } from 'vitest';
20
20
  import { middleware_applies } from '../http/schema_helpers.js';
21
21
  import { filter_protected_routes, filter_role_routes, filter_keeper_routes, filter_routes_with_input, filter_routes_with_params, filter_routes_with_query, filter_public_routes, format_route_key, } from '../http/surface_query.js';
22
+ import { PROTOCOL_ACTION_METHODS } from '../actions/action_codegen.js';
22
23
  // --- Structural invariants ---
23
24
  /**
24
25
  * Every protected route has 401 in `error_schemas`.
@@ -374,6 +375,83 @@ export const audit_error_schema_tightness = (surface) => {
374
375
  }
375
376
  return entries;
376
377
  };
378
+ // --- RPC / WS structural invariants ---
379
+ /**
380
+ * Every RPC method on every endpoint has a non-empty `description`.
381
+ *
382
+ * Parallel of `assert_descriptions_present` over `surface.rpc_endpoints`.
383
+ * Empty descriptions on RPC methods leak through `library.json` codegen and
384
+ * consumer-facing docs without the route-level check catching them.
385
+ */
386
+ export const assert_rpc_method_descriptions_present = (surface) => {
387
+ for (const ep of surface.rpc_endpoints) {
388
+ for (const method of ep.methods) {
389
+ assert.ok(method.description.length > 0, `RPC method '${method.name}' on endpoint '${ep.path}' has empty description`);
390
+ }
391
+ }
392
+ };
393
+ /**
394
+ * Every WS method on every endpoint has a non-empty `description`.
395
+ *
396
+ * Parallel of `assert_descriptions_present` over `surface.ws_endpoints`.
397
+ * Same rationale as `assert_rpc_method_descriptions_present` — codegen +
398
+ * surface explorer consume the description, blank values surface as `''`.
399
+ */
400
+ export const assert_ws_method_descriptions_present = (surface) => {
401
+ for (const ep of surface.ws_endpoints) {
402
+ for (const method of ep.methods) {
403
+ assert.ok(method.description.length > 0, `WS method '${method.name}' on endpoint '${ep.path}' has empty description`);
404
+ }
405
+ }
406
+ };
407
+ /**
408
+ * Every WS endpoint's `methods` includes every protocol action method
409
+ * (`heartbeat`, `cancel`).
410
+ *
411
+ * Consumers register WS endpoints by spreading `protocol_actions` from
412
+ * `actions/protocol.ts` before their own actions:
413
+ *
414
+ * ```ts
415
+ * ws_endpoints: [{path: '/api/ws', actions: [...protocol_actions, ...consumer_actions], ...}]
416
+ * ```
417
+ *
418
+ * Forgetting the spread compiles cleanly but breaks at runtime: client-side
419
+ * heartbeats and `cancel` notifications get `method_not_found` from the
420
+ * dispatcher, so disconnect detection silently regresses and per-request
421
+ * cancel never aborts the matching handler. Catch the mistake at the
422
+ * surface layer rather than at runtime.
423
+ */
424
+ export const assert_ws_endpoints_include_protocol_actions = (surface) => {
425
+ for (const ep of surface.ws_endpoints) {
426
+ const method_names = new Set(ep.methods.map((m) => m.name));
427
+ for (const expected of PROTOCOL_ACTION_METHODS) {
428
+ assert.ok(method_names.has(expected), `WS endpoint '${ep.path}' is missing protocol action method '${expected}' — ` +
429
+ `spread \`protocol_actions\` from 'actions/protocol.js' into \`actions\``);
430
+ }
431
+ }
432
+ };
433
+ /**
434
+ * WS methods follow the kind ⇔ auth biconditional emitted by surface
435
+ * generation: `kind === 'remote_notification' ⟺ auth === null`.
436
+ *
437
+ * `generate_app_surface` produces this shape directly from the action
438
+ * spec union (notifications carry `auth: null` per `ActionSpecUnion`;
439
+ * `request_response` carries a `RouteAuth`). The assertion guards against
440
+ * drift if a future surface emitter, transform, or test fixture violates
441
+ * it — and gives consumers a clear failure message when a hand-built
442
+ * surface mocks the shape incorrectly.
443
+ */
444
+ export const assert_ws_notifications_have_null_auth = (surface) => {
445
+ for (const ep of surface.ws_endpoints) {
446
+ for (const method of ep.methods) {
447
+ const is_notification = method.kind === 'remote_notification';
448
+ const has_null_auth = method.auth === null;
449
+ assert.ok(is_notification === has_null_auth, `WS method '${method.name}' on endpoint '${ep.path}' violates kind ⇔ auth: ` +
450
+ `kind='${method.kind}', auth=${has_null_auth ? 'null' : 'non-null'} ` +
451
+ `(notifications must have auth: null; request_response must have a RouteAuth)`);
452
+ }
453
+ }
454
+ };
377
455
  /** Default patterns for sensitive REST routes that should be rate-limited. */
378
456
  const DEFAULT_SENSITIVE_PATTERNS = [
379
457
  /\/login$/,
@@ -424,7 +502,9 @@ export const assert_no_unexpected_public_mutations = (surface, allowlist = []) =
424
502
  *
425
503
  * Note: RPC endpoints (`create_rpc_endpoint`) use `input: z.null()` on their
426
504
  * route specs — the dispatcher handles body/query parsing internally. Real input
427
- * schemas live in `rpc_endpoints` surface, not on routes.
505
+ * schemas live in `rpc_endpoints` surface, not on routes; see
506
+ * `assert_rpc_ws_surface_invariants` for the parallel checks over RPC/WS
507
+ * method shapes.
428
508
  */
429
509
  export const assert_mutation_routes_use_post = (surface) => {
430
510
  const input_routes = filter_routes_with_input(surface);
@@ -546,6 +626,28 @@ export const assert_surface_invariants = (surface) => {
546
626
  assert_error_code_status_consistency(surface);
547
627
  assert_404_schemas_use_specific_errors(surface);
548
628
  };
629
+ /**
630
+ * Run all RPC / WS structural invariants. Options-free — applies
631
+ * universally to the `surface.rpc_endpoints` and `surface.ws_endpoints`
632
+ * slots produced by `generate_app_surface`.
633
+ *
634
+ * Parallel of `assert_surface_invariants` for the non-REST surfaces.
635
+ * Within-endpoint duplicate method names and the auth-shape biconditional
636
+ * are already enforced at startup by `compile_action_registry` (see
637
+ * `actions/CLAUDE.md` §Registry compile) — these assertions cover only
638
+ * the contract-surface concerns that a runtime registration check
639
+ * cannot: empty descriptions, missing protocol-action spread on WS
640
+ * endpoints, and kind ⇔ auth drift on WS methods.
641
+ *
642
+ * @throws AssertionError on the first invariant violation; the message
643
+ * names the offending endpoint, method, and field.
644
+ */
645
+ export const assert_rpc_ws_surface_invariants = (surface) => {
646
+ assert_rpc_method_descriptions_present(surface);
647
+ assert_ws_method_descriptions_present(surface);
648
+ assert_ws_endpoints_include_protocol_actions(surface);
649
+ assert_ws_notifications_have_null_auth(surface);
650
+ };
549
651
  /**
550
652
  * Run security policy invariants. Configurable with sensible defaults.
551
653
  *
@@ -17,6 +17,7 @@
17
17
  is_plain_authenticated_auth,
18
18
  is_public_auth,
19
19
  is_role_auth,
20
+ type RouteAuth,
20
21
  } from '../http/auth_shape.js';
21
22
 
22
23
  const {surface}: {surface: AppSurface} = $props();
@@ -28,6 +29,13 @@
28
29
 
29
30
  const summary = $derived(surface_auth_summary(surface));
30
31
 
32
+ const rpc_method_count = $derived(
33
+ surface.rpc_endpoints.reduce((sum, ep) => sum + ep.methods.length, 0),
34
+ );
35
+ const ws_method_count = $derived(
36
+ surface.ws_endpoints.reduce((sum, ep) => sum + ep.methods.length, 0),
37
+ );
38
+
31
39
  const auth_matches_filter = (
32
40
  auth: AppSurfaceRoute['auth'],
33
41
  filter: (typeof auth_types)[number],
@@ -51,6 +59,8 @@
51
59
  );
52
60
 
53
61
  let expanded_event: string | null = $state.raw(null);
62
+ let expanded_rpc_method: string | null = $state.raw(null);
63
+ let expanded_ws_method: string | null = $state.raw(null);
54
64
 
55
65
  const toggle_route = (key: string): void => {
56
66
  expanded_route = expanded_route === key ? null : key;
@@ -60,7 +70,15 @@
60
70
  expanded_event = expanded_event === method ? null : method;
61
71
  };
62
72
 
63
- const format_auth = (auth: AppSurfaceRoute['auth']): string => {
73
+ const toggle_rpc_method = (key: string): void => {
74
+ expanded_rpc_method = expanded_rpc_method === key ? null : key;
75
+ };
76
+
77
+ const toggle_ws_method = (key: string): void => {
78
+ expanded_ws_method = expanded_ws_method === key ? null : key;
79
+ };
80
+
81
+ const format_auth = (auth: RouteAuth): string => {
64
82
  if (is_public_auth(auth)) return 'none';
65
83
  if (is_keeper_auth(auth)) return 'keeper';
66
84
  if (is_role_auth(auth)) return `role:${auth.roles!.join('|')}`;
@@ -68,7 +86,7 @@
68
86
  return 'other';
69
87
  };
70
88
 
71
- const auth_chip_class = (auth: AppSurfaceRoute['auth']): string => {
89
+ const auth_chip_class = (auth: RouteAuth): string => {
72
90
  if (is_public_auth(auth)) return 'chip color_b';
73
91
  if (is_keeper_auth(auth)) return 'chip color_c';
74
92
  if (is_role_auth(auth)) return 'chip color_d';
@@ -89,6 +107,8 @@
89
107
  {#if role_count > 0}<span class="chip color_d">{role_count} role</span>{/if}
90
108
  {#if summary.keeper > 0}<span class="chip color_c">{summary.keeper} keeper</span>{/if}
91
109
  <span class="chip">{surface.middleware.length} middleware</span>
110
+ {#if rpc_method_count > 0}<span class="chip">{rpc_method_count} rpc methods</span>{/if}
111
+ {#if ws_method_count > 0}<span class="chip">{ws_method_count} ws methods</span>{/if}
92
112
  {#if surface.env.length}<span class="chip">{surface.env.length} env</span>{/if}
93
113
  {#if surface.events.length}<span class="chip">{surface.events.length} events</span>{/if}
94
114
  {#if surface.diagnostics.length}{@const warnings = surface.diagnostics.filter(
@@ -283,6 +303,145 @@
283
303
  </div>
284
304
  {/if}
285
305
 
306
+ {#if surface.rpc_endpoints.length}
307
+ <h3>rpc endpoints</h3>
308
+ {#each surface.rpc_endpoints as endpoint (endpoint.path)}
309
+ <div class="row" style:gap="var(--space_sm)" style:align-items="center">
310
+ <code>{endpoint.path}</code>
311
+ <span class="chip">{endpoint.methods.length} methods</span>
312
+ </div>
313
+ {#if endpoint.methods.length === 0}
314
+ <p class="text_50">no methods</p>
315
+ {:else}
316
+ <div style:overflow-x="auto">
317
+ <table>
318
+ <thead>
319
+ <tr>
320
+ <th>method</th>
321
+ <th>auth</th>
322
+ <th>side effects</th>
323
+ <th>rate limit</th>
324
+ <th>description</th>
325
+ </tr>
326
+ </thead>
327
+ <tbody>
328
+ {#each endpoint.methods as method (method.name)}
329
+ {@const key = `${endpoint.path}|${method.name}`}
330
+ <tr onclick={() => toggle_rpc_method(key)} style:cursor="pointer">
331
+ <td><code>{method.name}</code></td>
332
+ <td>
333
+ <span class={auth_chip_class(method.auth)}>{format_auth(method.auth)}</span>
334
+ </td>
335
+ <td>{method.side_effects ? 'yes' : 'no'}</td>
336
+ <td class="text_50">{method.rate_limit_key ?? '-'}</td>
337
+ <td class="text_50">{method.description}</td>
338
+ </tr>
339
+ {#if expanded_rpc_method === key}
340
+ <tr transition:slide>
341
+ <td colspan="5">
342
+ <div class="column" style:gap="var(--space_sm)">
343
+ <div>
344
+ <strong>input</strong>
345
+ {#if method.input_schema}
346
+ <pre>{JSON.stringify(method.input_schema, null, 2)}</pre>
347
+ {:else}
348
+ <span class="text_50">none (z.void)</span>
349
+ {/if}
350
+ </div>
351
+ <div>
352
+ <strong>output</strong>
353
+ <pre>{JSON.stringify(method.output_schema, null, 2)}</pre>
354
+ </div>
355
+ </div>
356
+ </td>
357
+ </tr>
358
+ {/if}
359
+ {/each}
360
+ </tbody>
361
+ </table>
362
+ </div>
363
+ {/if}
364
+ {/each}
365
+ {/if}
366
+
367
+ {#if surface.ws_endpoints.length}
368
+ <h3>websocket endpoints</h3>
369
+ {#each surface.ws_endpoints as endpoint (endpoint.path)}
370
+ <div
371
+ class="row"
372
+ style:gap="var(--space_sm)"
373
+ style:align-items="center"
374
+ style:flex-wrap="wrap"
375
+ >
376
+ <code>{endpoint.path}</code>
377
+ <span class="chip">{endpoint.methods.length} methods</span>
378
+ {#each endpoint.required_roles as role (role)}
379
+ <span class="chip color_d">role:{role}</span>
380
+ {/each}
381
+ {#each endpoint.allowed_origins as origin (origin)}
382
+ <span class="chip color_b"><code>{origin}</code></span>
383
+ {/each}
384
+ </div>
385
+ {#if endpoint.methods.length === 0}
386
+ <p class="text_50">no methods</p>
387
+ {:else}
388
+ <div style:overflow-x="auto">
389
+ <table>
390
+ <thead>
391
+ <tr>
392
+ <th>method</th>
393
+ <th>kind</th>
394
+ <th>auth</th>
395
+ <th>side effects</th>
396
+ <th>rate limit</th>
397
+ <th>description</th>
398
+ </tr>
399
+ </thead>
400
+ <tbody>
401
+ {#each endpoint.methods as method (method.name)}
402
+ {@const key = `${endpoint.path}|${method.name}`}
403
+ <tr onclick={() => toggle_ws_method(key)} style:cursor="pointer">
404
+ <td><code>{method.name}</code></td>
405
+ <td><span class="chip">{method.kind}</span></td>
406
+ <td>
407
+ {#if method.auth}
408
+ <span class={auth_chip_class(method.auth)}>{format_auth(method.auth)}</span>
409
+ {:else}
410
+ <span class="text_50">—</span>
411
+ {/if}
412
+ </td>
413
+ <td>{method.side_effects ? 'yes' : 'no'}</td>
414
+ <td class="text_50">{method.rate_limit_key ?? '-'}</td>
415
+ <td class="text_50">{method.description}</td>
416
+ </tr>
417
+ {#if expanded_ws_method === key}
418
+ <tr transition:slide>
419
+ <td colspan="6">
420
+ <div class="column" style:gap="var(--space_sm)">
421
+ <div>
422
+ <strong>input</strong>
423
+ {#if method.input_schema}
424
+ <pre>{JSON.stringify(method.input_schema, null, 2)}</pre>
425
+ {:else}
426
+ <span class="text_50">none (z.void)</span>
427
+ {/if}
428
+ </div>
429
+ <div>
430
+ <strong>output</strong>
431
+ <pre>{JSON.stringify(method.output_schema, null, 2)}</pre>
432
+ </div>
433
+ </div>
434
+ </td>
435
+ </tr>
436
+ {/if}
437
+ {/each}
438
+ </tbody>
439
+ </table>
440
+ </div>
441
+ {/if}
442
+ {/each}
443
+ {/if}
444
+
286
445
  {#if surface.diagnostics.length}
287
446
  <h3>diagnostics</h3>
288
447
  <div style:overflow-x="auto">
@@ -1 +1 @@
1
- {"version":3,"file":"SurfaceExplorer.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/SurfaceExplorer.svelte"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAC,UAAU,EAAwC,MAAM,oBAAoB,CAAC;AASzF,KAAK,gBAAgB,GAAI;IAAC,OAAO,EAAE,UAAU,CAAA;CAAC,CAAC;AAuShD,QAAA,MAAM,eAAe,sDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"SurfaceExplorer.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/SurfaceExplorer.svelte"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAC,UAAU,EAAwC,MAAM,oBAAoB,CAAC;AAUzF,KAAK,gBAAgB,GAAI;IAAC,OAAO,EAAE,UAAU,CAAA;CAAC,CAAC;AAgchD,QAAA,MAAM,eAAe,sDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.63.0",
3
+ "version": "0.64.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",