@fuzdev/fuz_app 0.30.0 → 0.32.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 (222) hide show
  1. package/dist/actions/CLAUDE.md +630 -0
  2. package/dist/actions/action_rpc.d.ts +29 -0
  3. package/dist/actions/action_rpc.d.ts.map +1 -1
  4. package/dist/actions/action_rpc.js +42 -6
  5. package/dist/actions/action_types.d.ts +2 -2
  6. package/dist/actions/cancel.d.ts +12 -13
  7. package/dist/actions/cancel.d.ts.map +1 -1
  8. package/dist/actions/cancel.js +10 -13
  9. package/dist/actions/heartbeat.d.ts +8 -13
  10. package/dist/actions/heartbeat.d.ts.map +1 -1
  11. package/dist/actions/heartbeat.js +5 -8
  12. package/dist/actions/register_action_ws.d.ts +3 -3
  13. package/dist/actions/register_action_ws.js +2 -2
  14. package/dist/actions/register_ws_endpoint.d.ts +4 -4
  15. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  16. package/dist/actions/register_ws_endpoint.js +3 -3
  17. package/dist/actions/rpc_client.d.ts +29 -0
  18. package/dist/actions/rpc_client.d.ts.map +1 -1
  19. package/dist/actions/rpc_client.js +31 -0
  20. package/dist/actions/socket.svelte.d.ts +16 -16
  21. package/dist/actions/socket.svelte.d.ts.map +1 -1
  22. package/dist/actions/socket.svelte.js +15 -15
  23. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  24. package/dist/auth/CLAUDE.md +945 -0
  25. package/dist/auth/account_action_specs.d.ts +216 -0
  26. package/dist/auth/account_action_specs.d.ts.map +1 -0
  27. package/dist/auth/account_action_specs.js +159 -0
  28. package/dist/auth/account_actions.d.ts +51 -0
  29. package/dist/auth/account_actions.d.ts.map +1 -0
  30. package/dist/auth/account_actions.js +119 -0
  31. package/dist/auth/account_queries.d.ts +6 -2
  32. package/dist/auth/account_queries.d.ts.map +1 -1
  33. package/dist/auth/account_queries.js +40 -4
  34. package/dist/auth/account_routes.d.ts +94 -16
  35. package/dist/auth/account_routes.d.ts.map +1 -1
  36. package/dist/auth/account_routes.js +108 -180
  37. package/dist/auth/account_schema.d.ts +85 -30
  38. package/dist/auth/account_schema.d.ts.map +1 -1
  39. package/dist/auth/account_schema.js +40 -8
  40. package/dist/auth/admin_action_specs.d.ts +674 -0
  41. package/dist/auth/admin_action_specs.d.ts.map +1 -0
  42. package/dist/auth/admin_action_specs.js +287 -0
  43. package/dist/auth/admin_actions.d.ts +69 -0
  44. package/dist/auth/admin_actions.d.ts.map +1 -0
  45. package/dist/auth/admin_actions.js +256 -0
  46. package/dist/auth/admin_rpc_actions.d.ts +49 -0
  47. package/dist/auth/admin_rpc_actions.d.ts.map +1 -0
  48. package/dist/auth/admin_rpc_actions.js +32 -0
  49. package/dist/auth/api_token.d.ts +10 -0
  50. package/dist/auth/api_token.d.ts.map +1 -1
  51. package/dist/auth/api_token.js +9 -0
  52. package/dist/auth/api_token_queries.d.ts +3 -3
  53. package/dist/auth/api_token_queries.js +3 -3
  54. package/dist/auth/app_settings_schema.d.ts +4 -3
  55. package/dist/auth/app_settings_schema.d.ts.map +1 -1
  56. package/dist/auth/app_settings_schema.js +2 -1
  57. package/dist/auth/audit_log_routes.d.ts +14 -6
  58. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  59. package/dist/auth/audit_log_routes.js +22 -79
  60. package/dist/auth/audit_log_schema.d.ts +100 -29
  61. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  62. package/dist/auth/audit_log_schema.js +83 -11
  63. package/dist/auth/bootstrap_routes.d.ts +14 -0
  64. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  65. package/dist/auth/bootstrap_routes.js +10 -3
  66. package/dist/auth/cleanup.d.ts +63 -0
  67. package/dist/auth/cleanup.d.ts.map +1 -0
  68. package/dist/auth/cleanup.js +80 -0
  69. package/dist/auth/invite_schema.d.ts +11 -10
  70. package/dist/auth/invite_schema.d.ts.map +1 -1
  71. package/dist/auth/invite_schema.js +4 -3
  72. package/dist/auth/migrations.d.ts +6 -0
  73. package/dist/auth/migrations.d.ts.map +1 -1
  74. package/dist/auth/migrations.js +28 -0
  75. package/dist/auth/permit_offer_action_specs.d.ts +364 -0
  76. package/dist/auth/permit_offer_action_specs.d.ts.map +1 -0
  77. package/dist/auth/permit_offer_action_specs.js +216 -0
  78. package/dist/auth/permit_offer_actions.d.ts +96 -0
  79. package/dist/auth/permit_offer_actions.d.ts.map +1 -0
  80. package/dist/auth/permit_offer_actions.js +428 -0
  81. package/dist/auth/permit_offer_notifications.d.ts +361 -0
  82. package/dist/auth/permit_offer_notifications.d.ts.map +1 -0
  83. package/dist/auth/permit_offer_notifications.js +179 -0
  84. package/dist/auth/permit_offer_queries.d.ts +165 -0
  85. package/dist/auth/permit_offer_queries.d.ts.map +1 -0
  86. package/dist/auth/permit_offer_queries.js +390 -0
  87. package/dist/auth/permit_offer_schema.d.ts +103 -0
  88. package/dist/auth/permit_offer_schema.d.ts.map +1 -0
  89. package/dist/auth/permit_offer_schema.js +142 -0
  90. package/dist/auth/permit_queries.d.ts +77 -14
  91. package/dist/auth/permit_queries.d.ts.map +1 -1
  92. package/dist/auth/permit_queries.js +119 -24
  93. package/dist/auth/session_queries.d.ts +4 -2
  94. package/dist/auth/session_queries.d.ts.map +1 -1
  95. package/dist/auth/session_queries.js +4 -2
  96. package/dist/auth/signup_routes.d.ts +13 -0
  97. package/dist/auth/signup_routes.d.ts.map +1 -1
  98. package/dist/auth/signup_routes.js +14 -7
  99. package/dist/http/CLAUDE.md +584 -0
  100. package/dist/http/pending_effects.d.ts +29 -0
  101. package/dist/http/pending_effects.d.ts.map +1 -0
  102. package/dist/http/pending_effects.js +31 -0
  103. package/dist/http/route_spec.d.ts.map +1 -1
  104. package/dist/http/route_spec.js +4 -3
  105. package/dist/rate_limiter.d.ts +30 -0
  106. package/dist/rate_limiter.d.ts.map +1 -1
  107. package/dist/rate_limiter.js +25 -2
  108. package/dist/realtime/sse_auth_guard.d.ts +2 -0
  109. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  110. package/dist/realtime/sse_auth_guard.js +5 -3
  111. package/dist/server/app_server.d.ts +13 -2
  112. package/dist/server/app_server.d.ts.map +1 -1
  113. package/dist/server/app_server.js +12 -1
  114. package/dist/testing/CLAUDE.md +668 -1
  115. package/dist/testing/admin_integration.d.ts +10 -7
  116. package/dist/testing/admin_integration.d.ts.map +1 -1
  117. package/dist/testing/admin_integration.js +382 -482
  118. package/dist/testing/app_server.d.ts +7 -6
  119. package/dist/testing/app_server.d.ts.map +1 -1
  120. package/dist/testing/attack_surface.d.ts +9 -3
  121. package/dist/testing/attack_surface.d.ts.map +1 -1
  122. package/dist/testing/attack_surface.js +4 -4
  123. package/dist/testing/audit_completeness.d.ts +11 -0
  124. package/dist/testing/audit_completeness.d.ts.map +1 -1
  125. package/dist/testing/audit_completeness.js +169 -134
  126. package/dist/testing/auth_apps.d.ts.map +1 -1
  127. package/dist/testing/auth_apps.js +4 -33
  128. package/dist/testing/db.d.ts +1 -1
  129. package/dist/testing/db.d.ts.map +1 -1
  130. package/dist/testing/db.js +2 -0
  131. package/dist/testing/entities.d.ts +35 -13
  132. package/dist/testing/entities.d.ts.map +1 -1
  133. package/dist/testing/entities.js +17 -0
  134. package/dist/testing/integration.d.ts +10 -0
  135. package/dist/testing/integration.d.ts.map +1 -1
  136. package/dist/testing/integration.js +352 -340
  137. package/dist/testing/integration_helpers.d.ts +16 -5
  138. package/dist/testing/integration_helpers.d.ts.map +1 -1
  139. package/dist/testing/integration_helpers.js +24 -4
  140. package/dist/testing/rate_limiting.d.ts +7 -0
  141. package/dist/testing/rate_limiting.d.ts.map +1 -1
  142. package/dist/testing/rate_limiting.js +41 -10
  143. package/dist/testing/rpc_helpers.d.ts +153 -1
  144. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  145. package/dist/testing/rpc_helpers.js +184 -8
  146. package/dist/testing/sse_round_trip.d.ts +8 -0
  147. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  148. package/dist/testing/sse_round_trip.js +10 -3
  149. package/dist/testing/standard.d.ts +9 -1
  150. package/dist/testing/standard.d.ts.map +1 -1
  151. package/dist/testing/standard.js +6 -2
  152. package/dist/testing/stubs.d.ts +10 -2
  153. package/dist/testing/stubs.d.ts.map +1 -1
  154. package/dist/testing/stubs.js +17 -2
  155. package/dist/testing/surface_invariants.d.ts +7 -3
  156. package/dist/testing/surface_invariants.d.ts.map +1 -1
  157. package/dist/testing/surface_invariants.js +5 -4
  158. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  159. package/dist/testing/ws_round_trip.js +9 -38
  160. package/dist/ui/AccountSessions.svelte +8 -4
  161. package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
  162. package/dist/ui/AdminAccounts.svelte +61 -33
  163. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  164. package/dist/ui/AdminAuditLog.svelte +3 -2
  165. package/dist/ui/AdminAuditLog.svelte.d.ts.map +1 -1
  166. package/dist/ui/AdminInvites.svelte +3 -2
  167. package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
  168. package/dist/ui/AdminOverview.svelte +14 -9
  169. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  170. package/dist/ui/AdminPermitHistory.svelte +3 -2
  171. package/dist/ui/AdminPermitHistory.svelte.d.ts.map +1 -1
  172. package/dist/ui/AdminSessions.svelte +29 -25
  173. package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
  174. package/dist/ui/CLAUDE.md +363 -0
  175. package/dist/ui/OpenSignupToggle.svelte +6 -3
  176. package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
  177. package/dist/ui/PermitOfferForm.svelte +141 -0
  178. package/dist/ui/PermitOfferForm.svelte.d.ts +14 -0
  179. package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -0
  180. package/dist/ui/PermitOfferHistory.svelte +109 -0
  181. package/dist/ui/PermitOfferHistory.svelte.d.ts +11 -0
  182. package/dist/ui/PermitOfferHistory.svelte.d.ts.map +1 -0
  183. package/dist/ui/PermitOfferInbox.svelte +121 -0
  184. package/dist/ui/PermitOfferInbox.svelte.d.ts +12 -0
  185. package/dist/ui/PermitOfferInbox.svelte.d.ts.map +1 -0
  186. package/dist/ui/account_sessions_state.svelte.d.ts +53 -3
  187. package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
  188. package/dist/ui/account_sessions_state.svelte.js +39 -16
  189. package/dist/ui/admin_accounts_state.svelte.d.ts +118 -2
  190. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  191. package/dist/ui/admin_accounts_state.svelte.js +99 -23
  192. package/dist/ui/admin_invites_state.svelte.d.ts +47 -1
  193. package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
  194. package/dist/ui/admin_invites_state.svelte.js +38 -26
  195. package/dist/ui/admin_rpc_adapters.d.ts +94 -0
  196. package/dist/ui/admin_rpc_adapters.d.ts.map +1 -0
  197. package/dist/ui/admin_rpc_adapters.js +100 -0
  198. package/dist/ui/admin_sessions_state.svelte.d.ts +26 -0
  199. package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
  200. package/dist/ui/admin_sessions_state.svelte.js +35 -21
  201. package/dist/ui/app_settings_state.svelte.d.ts +39 -0
  202. package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
  203. package/dist/ui/app_settings_state.svelte.js +34 -18
  204. package/dist/ui/audit_log_state.svelte.d.ts +40 -3
  205. package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
  206. package/dist/ui/audit_log_state.svelte.js +36 -42
  207. package/dist/ui/auth_state.svelte.d.ts +4 -3
  208. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  209. package/dist/ui/auth_state.svelte.js +4 -1
  210. package/dist/ui/permit_offers_state.svelte.d.ts +125 -0
  211. package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -0
  212. package/dist/ui/permit_offers_state.svelte.js +197 -0
  213. package/package.json +3 -3
  214. package/dist/auth/admin_routes.d.ts +0 -29
  215. package/dist/auth/admin_routes.d.ts.map +0 -1
  216. package/dist/auth/admin_routes.js +0 -226
  217. package/dist/auth/app_settings_routes.d.ts +0 -27
  218. package/dist/auth/app_settings_routes.d.ts.map +0 -1
  219. package/dist/auth/app_settings_routes.js +0 -66
  220. package/dist/auth/invite_routes.d.ts +0 -18
  221. package/dist/auth/invite_routes.d.ts.map +0 -1
  222. package/dist/auth/invite_routes.js +0 -129
@@ -1,12 +1,14 @@
1
1
  <script lang="ts">
2
2
  import {AdminSessionsState} from './admin_sessions_state.svelte.js';
3
+ import {admin_accounts_rpc_context} from './admin_accounts_state.svelte.js';
3
4
  import {format_relative_time, format_datetime_local, truncate_uuid} from './ui_format.js';
4
5
  import ConfirmButton from './ConfirmButton.svelte';
5
6
  import Datatable from './Datatable.svelte';
6
7
  import type {DatatableColumn} from './datatable.js';
7
8
  import type {AdminSessionJson} from '../auth/audit_log_schema.js';
8
9
 
9
- const admin_sessions = new AdminSessionsState();
10
+ const get_rpc = admin_accounts_rpc_context.get();
11
+ const admin_sessions = new AdminSessionsState({get_rpc});
10
12
 
11
13
  void admin_sessions.fetch();
12
14
 
@@ -50,30 +52,32 @@
50
52
  {format_relative_time(row.expires_at)}
51
53
  </span>
52
54
  {:else if column.key === 'account_id'}
53
- <ConfirmButton
54
- onconfirm={() => admin_sessions.revoke_all_for_account(row.account_id)}
55
- title="revoke all sessions for {row.username}"
56
- class="sm"
57
- disabled={admin_sessions.revoking_account_ids.has(row.account_id)}
58
- >
59
- {#snippet children(_popover, _confirm)}
60
- {admin_sessions.revoking_account_ids.has(row.account_id)
61
- ? 'revoking…'
62
- : 'revoke sessions'}
63
- {/snippet}
64
- </ConfirmButton>
65
- <ConfirmButton
66
- onconfirm={() => admin_sessions.revoke_all_tokens_for_account(row.account_id)}
67
- title="revoke all tokens for {row.username}"
68
- class="sm"
69
- disabled={admin_sessions.revoking_token_account_ids.has(row.account_id)}
70
- >
71
- {#snippet children(_popover, _confirm)}
72
- {admin_sessions.revoking_token_account_ids.has(row.account_id)
73
- ? 'revoking…'
74
- : 'revoke tokens'}
75
- {/snippet}
76
- </ConfirmButton>
55
+ {#if admin_sessions.has_rpc}
56
+ <ConfirmButton
57
+ onconfirm={() => admin_sessions.revoke_all_for_account(row.account_id)}
58
+ title="revoke all sessions for {row.username}"
59
+ class="sm"
60
+ disabled={admin_sessions.revoking_account_ids.has(row.account_id)}
61
+ >
62
+ {#snippet children(_popover, _confirm)}
63
+ {admin_sessions.revoking_account_ids.has(row.account_id)
64
+ ? 'revoking…'
65
+ : 'revoke sessions'}
66
+ {/snippet}
67
+ </ConfirmButton>
68
+ <ConfirmButton
69
+ onconfirm={() => admin_sessions.revoke_all_tokens_for_account(row.account_id)}
70
+ title="revoke all tokens for {row.username}"
71
+ class="sm"
72
+ disabled={admin_sessions.revoking_token_account_ids.has(row.account_id)}
73
+ >
74
+ {#snippet children(_popover, _confirm)}
75
+ {admin_sessions.revoking_token_account_ids.has(row.account_id)
76
+ ? 'revoking…'
77
+ : 'revoke tokens'}
78
+ {/snippet}
79
+ </ConfirmButton>
80
+ {/if}
77
81
  {:else if column.format}
78
82
  {column.format(row[column.key], row)}
79
83
  {:else}
@@ -1 +1 @@
1
- {"version":3,"file":"AdminSessions.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminSessions.svelte"],"names":[],"mappings":"AAwFA,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IACtG,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,aAAa;;kBAA+E,CAAC;AACjF,KAAK,aAAa,GAAG,YAAY,CAAC,OAAO,aAAa,CAAC,CAAC;AAC1D,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"AdminSessions.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminSessions.svelte"],"names":[],"mappings":"AA6FA,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IACtG,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,aAAa;;kBAA+E,CAAC;AACjF,KAAK,aAAa,GAAG,YAAY,CAAC,OAAO,aAAa,CAAC,CAAC;AAC1D,eAAe,aAAa,CAAC"}
@@ -0,0 +1,363 @@
1
+ # ui/
2
+
3
+ Frontend subsystem — Svelte 5 components, reactive state classes, and DOM
4
+ utilities. Cookie-based SPA auth; prerendered static HTML served by Hono
5
+ (no SvelteKit SSR for sessions). State classes extend `Loadable` and
6
+ hold `$state` fields exclusively via runes. Shared dependencies flow
7
+ through Svelte context, never through props — RPC adapters in particular
8
+ are provisioned once at the admin shell and read by every `Admin*.svelte`.
9
+
10
+ See ../../docs/usage.md for end-to-end wiring examples (sections "Permit
11
+ offer UI" and "Admin UI"). This file is a reference, not a tutorial.
12
+
13
+ ## Key patterns
14
+
15
+ ### RPC adapter contexts with `() => null` fallback
16
+
17
+ Five narrow RPC adapter contexts — `admin_accounts_rpc_context`,
18
+ `admin_invites_rpc_context`, `audit_log_rpc_context`,
19
+ `app_settings_rpc_context`, `account_sessions_rpc_context` — carry a
20
+ reactive `() => Rpc | null` accessor. All five declare a `() => () => null`
21
+ default so components mounted without a provisioner render the "rpc adapter
22
+ not wired" state instead of crashing. (`permit_offers_state_context` carries
23
+ a `PermitOffersState` directly, not an RPC accessor, and isn't counted
24
+ here.) The standard consumer shape:
25
+
26
+ ```ts
27
+ const get_rpc = admin_accounts_rpc_context.get();
28
+ const admin_accounts = new AdminAccountsState({get_rpc});
29
+ ```
30
+
31
+ or for direct calls:
32
+
33
+ ```ts
34
+ const get_rpc = admin_accounts_rpc_context.get();
35
+ const rpc = $derived(get_rpc());
36
+ ```
37
+
38
+ The provisioner calls `context.set(() => rpc)` once at the admin route
39
+ shell. Every admin component plus `OpenSignupToggle.svelte` consumes the
40
+ context — RPC adapters are never threaded through props.
41
+
42
+ ### `has_rpc` gates fetch and mutations
43
+
44
+ Every state class backed by a narrow RPC interface exposes a `has_rpc`
45
+ getter. When `false`, `fetch()`, mutations, and `subscribe` no-op and
46
+ set `error` to `'rpc adapter not wired'`. Post-2026-04-23 RPC migration
47
+ this applies uniformly — `AdminSessionsState`'s listing + mutations all
48
+ run through the shared `AdminAccountsRpc`, so `has_rpc` gates the whole
49
+ surface.
50
+
51
+ ### `$state.raw` Map keyed by id + `$derived` views
52
+
53
+ `PermitOffersState` maintains a single `Map<string, PermitOfferJson>` in
54
+ `$state.raw`, keyed by offer id, and exposes `incoming` / `outgoing` /
55
+ `history` as `$derived.by` arrays. Writes go through `#merge_offers`
56
+ (clone-and-replace) / `#remove_offer` — never mutate the Map in place
57
+ because `$state.raw` expects reference swaps.
58
+
59
+ ### Reducer pattern for WS notifications
60
+
61
+ `PermitOffersState.apply_notification(notification)` is the single
62
+ reducer — `subscribe(subscribe_fn)` is a thin subscription adapter over
63
+ it. Six methods land on the reducer: `permit_offer_received` /
64
+ `_retracted` / `_accepted` / `_declined` / `_supersede` all merge a
65
+ `{offer}` payload; `permit_revoke` is ignored at this layer (permit
66
+ lifecycle lives in auth/permits state). The six notification specs and
67
+ their payload shapes are defined in `../auth/permit_offer_notifications.ts`
68
+ (see `../auth/CLAUDE.md` §WS notifications).
69
+
70
+ ### Svelte 5 inline `$props` shape
71
+
72
+ Always `const {...}: {...} = $props()` — never `interface Props`.
73
+ Destructure defaults in the binding list; put the type literal inline.
74
+ This matches the user-memory Svelte props rule and the existing file
75
+ conventions.
76
+
77
+ ### Context over props for shared deps
78
+
79
+ Auth, RPC adapters, sidebar, and permit offers all flow through
80
+ `create_context` from `@fuzdev/fuz_ui/context_helpers.js`. Components
81
+ consume with `const x = x_context.get()` (or a `get_rpc`/`$derived`
82
+ pair when the value may change reactively). New shared state joins the
83
+ pattern rather than reintroducing prop-drilling.
84
+
85
+ ## Shell + layout
86
+
87
+ - `AppShell.svelte` — sidebar-and-main shell. Props: `children`,
88
+ `sidebar` (Snippet), `sidebar_width = 180`, `sidebar_state?`,
89
+ `keyboard_shortcut?`, `show_toggle?`, `toggle_button?`.
90
+ Provisions `sidebar_state_context` internally (creates a fresh
91
+ `SidebarState` if `sidebar_state` prop is not supplied).
92
+ - `ColumnLayout.svelte` — fixed `aside` column + fluid `children`
93
+ column; `column_width = '280px'`.
94
+ - `MenuLink.svelte` — SvelteKit `<a>` with `selected`/`highlighted`
95
+ derived from `page.url.pathname`. Takes `path` (resolved via
96
+ `resolve` from `$app/paths`).
97
+ - `sidebar_state.svelte.ts` — `SidebarState` (with `activate()` cleanup
98
+ pattern, optional reactive `enabled` getter), `sidebar_state_context`.
99
+
100
+ ## Auth forms
101
+
102
+ All four consume `auth_state_context.get()`; all three form-driven ones
103
+ attach a `FormState` for Enter-advance + blur-touched validation.
104
+
105
+ - `LoginForm.svelte` — props `username_label = 'username or email'`,
106
+ `redirect_on_login`. Clears `auth_state.verify_error` on input.
107
+ - `BootstrapForm.svelte` — token + username + password + confirm;
108
+ validates `Username` schema and `PASSWORD_LENGTH_MIN`; focuses the
109
+ first invalid field on submit.
110
+ - `SignupForm.svelte` — username + optional email + password + confirm;
111
+ calls `auth_state.signup(username, password, email?)`.
112
+ - `LogoutButton.svelte` — wraps `PendingButton`; calls
113
+ `auth_state.logout()` when `onclick` doesn't `preventDefault()`.
114
+
115
+ ## Account
116
+
117
+ - `AccountSessions.svelte` — self-serve session list for the logged-in
118
+ account. Instantiates `AccountSessionsState`, renders a `Datatable`
119
+ with per-row `revoke` and an optional `revoke all`. Calling
120
+ `revoke_all` clears `auth_state.verified` so the UI falls back to
121
+ the login page.
122
+
123
+ ## Admin
124
+
125
+ Every admin component below consumes its RPC adapter via the matching
126
+ context and delegates rendering to `Datatable` + `ConfirmButton` for
127
+ destructive actions.
128
+
129
+ - `AdminAccounts.svelte` — accounts + permits + pending offers.
130
+ Consumes `admin_accounts_rpc_context`. Per-row actions: grant (+role
131
+ chip with `ConfirmButton`), revoke (`actor_id` + `permit_id`),
132
+ retract pending offer. Tracks `granting_keys` / `revoking_ids` /
133
+ `retracting_ids` for per-action spinners.
134
+ - `AdminAuditLog.svelte` — audit event stream. Consumes
135
+ `audit_log_rpc_context`. Filter by `event_type`, manual refresh,
136
+ toggle SSE streaming (via `EventSource` — not RPC).
137
+ - `AdminInvites.svelte` — invite CRUD + embeds `OpenSignupToggle`.
138
+ Consumes `admin_invites_rpc_context`. Tracks `creating` +
139
+ `deleting_ids`.
140
+ - `AdminOverview.svelte` — dashboard panels (accounts / sessions /
141
+ invites / recent activity / security / system). Consumes all four
142
+ RPC contexts plus `auth_state_context`; fetches in parallel on mount.
143
+ Derives `role_counts`, `failed_logins`, `permit_changes` from
144
+ the audit log.
145
+ - `AdminPermitHistory.svelte` — permit-grant/revoke history table.
146
+ Consumes `audit_log_rpc_context`, calls
147
+ `audit_log.fetch_permit_history()` once on mount.
148
+ - `AdminSessions.svelte` — cross-account active sessions.
149
+ Both listing (`admin_session_list` RPC) and the two revoke-all
150
+ mutations go through `admin_accounts_rpc_context` (reused).
151
+ Per-row: revoke sessions, revoke tokens — both `ConfirmButton`.
152
+ - `AdminSettings.svelte` — shell for `OpenSignupToggle` + the logged-in
153
+ account line + logout `ConfirmButton`. No direct RPC calls.
154
+ - `AdminSurface.svelte` — attack-surface viewer. Fetches
155
+ `/api/surface` (REST) and delegates to `SurfaceExplorer`.
156
+ - `OpenSignupToggle.svelte` — single checkbox bound to
157
+ `AppSettingsState.settings.open_signup`. Consumes
158
+ `app_settings_rpc_context`; hides gracefully when `has_rpc` is `false`.
159
+ - `SurfaceExplorer.svelte` — reads-only `AppSurface` renderer. Props:
160
+ `surface: AppSurface`. Filter routes by auth type; expand a row to
161
+ dump `params`/`query`/`input`/`output`/`errors` schemas as JSON.
162
+ Also tables middleware, env, events, and diagnostics.
163
+
164
+ ## Permit offers
165
+
166
+ - `PermitOfferInbox.svelte` — recipient-side pending inbox; renders
167
+ `PermitOffersState.incoming`. Props: `format_actor?`, `format_scope?`,
168
+ `format_role?` — consumers plug in display names for actor/scope ids.
169
+ Accept is a `PendingButton`; decline is a `ConfirmButton` whose
170
+ popover contains a textarea (max `PERMIT_OFFER_MESSAGE_LENGTH_MAX`).
171
+ - `PermitOfferForm.svelte` — grantor-side create form. Props:
172
+ `to_account_id`, `roles: Array<string>` (pre-filtered upstream by
173
+ `web_grantable`), `scope_id = null`, `on_created?`, `format_role?`.
174
+ Surfaces three reason codes with friendly copy:
175
+ `ERROR_OFFER_SELF_TARGET`, `ERROR_OFFER_ROLE_NOT_GRANTABLE`,
176
+ `ERROR_OFFER_NOT_AUTHORIZED` — imported from `../auth/permit_offer_action_specs.js`
177
+ (see `../auth/CLAUDE.md` for `permit_offer_action_specs.ts` + `permit_offer_actions.ts`).
178
+ - `PermitOfferHistory.svelte` — both-directions history (recipient +
179
+ grantor, including terminal). Props: `current_actor_id: string | null`
180
+ (classifies row as "sent" vs "received"), `format_actor?`,
181
+ `format_scope?`, `format_role?`. Consumes
182
+ `permit_offers_state_context`; caller seeds via
183
+ `PermitOffersState.fetch_history()`.
184
+ - `permit_offers_state.svelte.ts` — `PermitOffersState` (extends
185
+ `Loadable`) + `permit_offers_state_context`. Options:
186
+ `rpc: PermitOffersRpc`, `account_id: () => string | null`,
187
+ `actor_id: () => string | null`. The narrow `PermitOffersRpc`
188
+ interface has six methods: `list`, `history`, `create`, `accept`,
189
+ `decline`, `retract`. `$state.raw` Map keyed by offer id;
190
+ `$derived.by` views: `incoming` (recipient-side pending, soonest-
191
+ expiry first), `outgoing` (grantor-side pending, newest-created
192
+ first), `history` (all known, newest-created first). Reducer
193
+ `apply_notification` handles the six permit-offer notification
194
+ methods; `permit_revoke` is deliberately ignored here (auth/permits
195
+ concern). `reset()` clears the Map.
196
+
197
+ ## State primitives
198
+
199
+ - `loadable.svelte.ts` — `Loadable<TError = string>` base class.
200
+ `loading`, `error`, `error_data` (raw caught value for programmatic
201
+ inspection). Protected `run(fn, map_error?)` wraps async operations
202
+ with loading + error handling; subclasses add `$state` fields and
203
+ call `run`. `reset()` clears state; subclasses override to clear
204
+ domain data.
205
+ - `auth_state.svelte.ts` — `AuthState`, `auth_state_context`.
206
+ Fields: `verifying`, `verified`, `verify_error`, `account`, `actor`
207
+ (the caller's own `ActorSummaryJson` — surfaced directly so consumers
208
+ don't derive `actor_id` from the permit list), `permits`,
209
+ `active_permits` (derived via `is_permit_active`), `roles` (derived),
210
+ `needs_bootstrap`. Methods: `check_session()`
211
+ (GET `/api/account/status`), `login`, `bootstrap`, `signup`,
212
+ `logout`. Handles 401/403/409/429 translations inline.
213
+ - `table_state.svelte.ts` — `TableState` extends `Loadable`.
214
+ Paginated DB browser state: `table_name`, `columns`, `rows`,
215
+ `total`, `offset`, `limit` (capped by `TABLE_LIMIT_MAX = 1000`),
216
+ `primary_key`. Derived `showing_start`/`showing_end`/`has_prev`/
217
+ `has_next`. Methods: `fetch`, `go_prev`/`go_next`, `delete_row`.
218
+ - `form_state.svelte.ts` — `FormState`. Enter-advance between
219
+ focusable elements via `keydown`; per-field `touched` set via
220
+ delegated `focusout`; form-level `attempted` set on submit attempt.
221
+ Methods: `form()` (returns a Svelte `Attachment` for the form
222
+ element), `show(field)` (touched OR attempted), `is_touched(field)`,
223
+ `touch(field)` (programmatic), `focus(field)` (queries by `name`),
224
+ `attempt()`, `reset()`. In DEV throws if an input loses focus
225
+ without a `name` attribute — all tracked inputs must be named.
226
+ - `sidebar_state.svelte.ts` — see Shell + layout above.
227
+
228
+ ## Per-domain state modules
229
+
230
+ - `account_sessions_state.svelte.ts` — `AccountSessionsState` extends
231
+ `Loadable` + `account_sessions_rpc_context` + narrow
232
+ `AccountSessionsRpc` (`list`, `revoke`, `revoke_all`). Wraps the
233
+ `account_session_list` / `account_session_revoke` /
234
+ `account_session_revoke_all` RPC actions. Derived `active_count`.
235
+ - `audit_log_state.svelte.ts` — `AuditLogState` extends `Loadable`
236
+ - `audit_log_rpc_context` + narrow `AuditLogRpc` (`list` +
237
+ `permit_history`). Fields: `events`, `permit_history_events`,
238
+ `connected`. Internal `#last_seq` for SSE gap fill on reconnect.
239
+ Methods: `fetch(options?)` (RPC), `fetch_permit_history`,
240
+ `subscribe()` (opens `EventSource` at `#stream_url`, default
241
+ `/api/admin/audit-log/stream`; prepends new events; refills gap
242
+ via `since_seq`), `disconnect()`. SSE stays on `EventSource` —
243
+ streaming is not an RPC concern.
244
+ - `admin_accounts_state.svelte.ts` — `AdminAccountsState` extends
245
+ `Loadable` + `admin_accounts_rpc_context` + narrow
246
+ `AdminAccountsRpc` (six methods: `list_accounts`, `grant_permit`,
247
+ `revoke_permit`, `retract_offer`, `session_revoke_all`,
248
+ `token_revoke_all` — the last two are also reused by
249
+ `AdminSessionsState`). `SvelteSet`s for in-flight tracking:
250
+ `granting_keys` (`${account_id}:${role}`), `revoking_ids`
251
+ (permit id), `retracting_ids` (offer id). `revoke_permit` keys on
252
+ `actor_id` (permits are actor-scoped — matches `row.actor.id`
253
+ straight from the listing) with optional `reason`.
254
+ - `admin_invites_state.svelte.ts` — `AdminInvitesState` extends
255
+ `Loadable` + `admin_invites_rpc_context` + narrow
256
+ `AdminInvitesRpc` (`list`, `create`, `delete`). Fields:
257
+ `invites`, `creating`, `deleting_ids`; derived `invite_count`,
258
+ `unclaimed_count`.
259
+ - `admin_sessions_state.svelte.ts` — `AdminSessionsState` extends
260
+ `Loadable`. **Reuses** `admin_accounts_rpc_context` /
261
+ `AdminAccountsRpc` for the listing (`list_sessions` wraps
262
+ `admin_session_list`) and the two revoke-all mutations. `SvelteSet`s:
263
+ `revoking_account_ids`, `revoking_token_account_ids`. `has_rpc`
264
+ gates the listing + both revoke controls.
265
+ - `app_settings_state.svelte.ts` — `AppSettingsState` extends
266
+ `Loadable` + `app_settings_rpc_context` + narrow `AppSettingsRpc`
267
+ (`get`, `update`). Fields: `settings`, `updating`. Single mutation
268
+ `update_open_signup(boolean)`.
269
+ - `admin_rpc_adapters.ts` (plain `.ts`, no reactive state) — bundled
270
+ wiring for the four admin RPC contexts. `create_admin_rpc_adapters(rpc_call)`
271
+ takes a single `AdminRpcCall` closure (alias of `ThrowingRpcCall` from
272
+ `../actions/rpc_client.ts`) and returns `{admin_accounts, admin_invites,
273
+ audit_log, app_settings}` adapter objects. `provide_admin_rpc_contexts(adapters)`
274
+ calls `set` on all four contexts in one shot. Pair with
275
+ `create_throwing_rpc_call(api)` from `../actions/rpc_client.ts` to turn a
276
+ typed `create_rpc_client` Proxy into the throw-on-error `rpc_call`
277
+ signature — two lines at the admin shell layout. Method-name mapping is
278
+ in the module TSDoc (`grant_permit` → `permit_offer_create`,
279
+ `retract_offer` → `permit_offer_retract`, etc.) and the
280
+ `admin_rpc_adapters.test.ts` fixtures.
281
+
282
+ ## RPC adapter contexts
283
+
284
+ All five RPC-carrying contexts have a `() => () => null` default and
285
+ share the same `has_rpc`-gated state-class shape; consumers wire a typed
286
+ RPC client to each narrow interface. See "Key patterns" above for the
287
+ provisioner pattern.
288
+
289
+ - `auth_state_context` — carries `AuthState` directly (not an RPC
290
+ accessor). Used by every auth form, `AdminOverview`,
291
+ `AdminSettings`, `AccountSessions`, `LogoutButton`.
292
+ - `admin_accounts_rpc_context` — `() => AdminAccountsRpc | null`.
293
+ Consumed by `AdminAccounts`, `AdminSessions`, `AdminOverview`.
294
+ - `admin_invites_rpc_context` — `() => AdminInvitesRpc | null`.
295
+ Consumed by `AdminInvites`, `AdminOverview`.
296
+ - `audit_log_rpc_context` — `() => AuditLogRpc | null`. Consumed by
297
+ `AdminAuditLog`, `AdminPermitHistory`, `AdminOverview`.
298
+ - `app_settings_rpc_context` — `() => AppSettingsRpc | null`.
299
+ Consumed by `OpenSignupToggle`, `AdminOverview`.
300
+ - `account_sessions_rpc_context` — `() => AccountSessionsRpc | null`.
301
+ Consumed by `AccountSessions`.
302
+ - `permit_offers_state_context` — carries `PermitOffersState`
303
+ directly. Consumed by `PermitOfferInbox`, `PermitOfferForm`,
304
+ `PermitOfferHistory`. Wiring is ctor-bound (RPC + account/actor
305
+ getters), so there's no separate `permit_offers_rpc_context`.
306
+ - `sidebar_state_context` — `() => SidebarState`. Provisioned by
307
+ `AppShell`.
308
+
309
+ ## Popovers
310
+
311
+ - `popover.svelte.ts` — `Popover` class. Owns `visible`, `position`,
312
+ `align`, `offset`, `popover_class`, `disable_outside_click` as
313
+ `$state.raw`. Three `Attachment` factories: `container`,
314
+ `trigger(params?)`, `content(params?)`. `show()` / `hide()` /
315
+ `toggle()`, plus `update(params)` to swap config. ARIA roles +
316
+ `aria-expanded` / `aria-controls` wired automatically.
317
+ - `position_helpers.ts` — `Position` / `Alignment` / `CardinalPosition`
318
+ types; `generate_position_styles(position, align, offset)` returns
319
+ CSS styles record for absolute positioning (left/right/top/bottom/
320
+ center/overlay).
321
+ - `PopoverButton.svelte` — button + popover composition. Required
322
+ `popover_content: Snippet<[Popover]>`. Either `children` (simple
323
+ content inside the default `<button>`) or `button: Snippet<[Popover]>`
324
+ (custom trigger) — logs in DEV if both or neither are supplied.
325
+ Auto-hides when `disabled`.
326
+ - `ConfirmButton.svelte` — wraps `PopoverButton` for destructive
327
+ actions. Required `onconfirm: (Popover) => void`. `hide_on_confirm`
328
+ default `true`. `position` default `'left'`. Three optional
329
+ snippets — `children`, `popover_content`, `popover_button_content` —
330
+ each receiving `(Popover, confirm)`. Falls back to a remove-glyph
331
+ button when no snippets are supplied.
332
+
333
+ ## Data
334
+
335
+ - `Datatable.svelte` — generic grid (`<script generics="T">`).
336
+ Props: `columns`, `rows`, `row_key = 'id'`, `height?`, optional
337
+ `header` / `cell` / `empty` snippets. Sticky header, CSS-subgrid
338
+ layout, pointer-based column resize (writes deltas to a keyed
339
+ record). Default cell renders `column.format(value, row)` or
340
+ `format_value(value)`.
341
+ - `datatable.ts` — `DatatableColumn<T>` interface (`key`, `label`,
342
+ `width?`, `min_width?`, `format?`), `DATATABLE_COLUMN_WIDTH_DEFAULT`
343
+ (120), `DATATABLE_MIN_COLUMN_WIDTH` (50).
344
+
345
+ ## Fetch + format
346
+
347
+ - `ui_fetch.ts` — `ui_fetch(input, init?)` wraps `fetch` with
348
+ `credentials: 'include'` for cookie-based session auth;
349
+ `parse_response_error(response, fallback?)` safely extracts
350
+ `body.error` even from non-JSON responses (HTML 404 pages, etc.).
351
+ - `ui_format.ts` — display helpers:
352
+ - `format_relative_time(timestamp, now?)` — "2m ago", "3h ago",
353
+ "5d ago", "2mo ago", "1y ago"; "just now" when under a minute;
354
+ bidirectional (future timestamps render as "in 5m" etc.).
355
+ - `format_uptime(ms)` — "45s", "12m", "3h 15m", "2d 5h".
356
+ - `truncate_middle(str, max_length, separator = '…')`.
357
+ - `truncate_uuid(uuid)` — 12-char middle-truncation.
358
+ - `format_datetime_local(timestamp)` — absolute UTC string for
359
+ `title` attributes.
360
+ - `format_value(value)` — table-cell stringifier (NULL / undefined /
361
+ JSON / primitive).
362
+ - `format_audit_metadata(event_type, metadata)` — event-type-
363
+ specific metadata summary (switch across every `AuditEventType`).
@@ -1,13 +1,16 @@
1
1
  <script lang="ts">
2
- import {AppSettingsState} from './app_settings_state.svelte.js';
2
+ import {AppSettingsState, app_settings_rpc_context} from './app_settings_state.svelte.js';
3
3
 
4
- const app_settings = new AppSettingsState();
4
+ const get_rpc = app_settings_rpc_context.get();
5
+ const app_settings = new AppSettingsState({get_rpc});
5
6
 
6
7
  void app_settings.fetch();
7
8
  </script>
8
9
 
9
10
  <div class="open-signup-toggle">
10
- {#if app_settings.loading}
11
+ {#if !app_settings.has_rpc}
12
+ <p class="text_50">rpc adapter not wired</p>
13
+ {:else if app_settings.loading}
11
14
  <p class="text_50">loading settings...</p>
12
15
  {:else if app_settings.settings}
13
16
  <label class="row">
@@ -1 +1 @@
1
- {"version":3,"file":"OpenSignupToggle.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/OpenSignupToggle.svelte"],"names":[],"mappings":"AAkCA,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IACtG,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,gBAAgB;;kBAA+E,CAAC;AACpF,KAAK,gBAAgB,GAAG,YAAY,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAChE,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"OpenSignupToggle.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/OpenSignupToggle.svelte"],"names":[],"mappings":"AAqCA,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IACtG,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,gBAAgB;;kBAA+E,CAAC;AACpF,KAAK,gBAAgB,GAAG,YAAY,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAChE,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,141 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Grantor-side permit offer form.
4
+ *
5
+ * Caller supplies `to_account_id`, the subset of roles the grantor may
6
+ * offer (typically filtered by `web_grantable`), an optional `scope_id`,
7
+ * and an optional `on_created` callback for post-submit UX. Errors from
8
+ * the RPC surface the three distinct reason codes — self-target,
9
+ * role-not-grantable, not-authorized — so consumers can render them
10
+ * appropriately.
11
+ */
12
+
13
+ import PendingButton from '@fuzdev/fuz_ui/PendingButton.svelte';
14
+
15
+ import {permit_offers_state_context} from './permit_offers_state.svelte.js';
16
+ import {FormState} from './form_state.svelte.js';
17
+ import {
18
+ PERMIT_OFFER_MESSAGE_LENGTH_MAX,
19
+ type PermitOfferJson,
20
+ } from '../auth/permit_offer_schema.js';
21
+ import {
22
+ ERROR_OFFER_NOT_AUTHORIZED,
23
+ ERROR_OFFER_ROLE_NOT_GRANTABLE,
24
+ ERROR_OFFER_SELF_TARGET,
25
+ } from '../auth/permit_offer_action_specs.js';
26
+
27
+ const {
28
+ to_account_id,
29
+ roles,
30
+ scope_id = null,
31
+ on_created,
32
+ format_role = (role: string) => role,
33
+ }: {
34
+ to_account_id: string;
35
+ /** Roles the caller may offer — caller filters by `web_grantable` upstream. */
36
+ roles: Array<string>;
37
+ /** Resource scope for the offer; `null` (default) yields a global offer. */
38
+ scope_id?: string | null;
39
+ on_created?: (offer: PermitOfferJson) => void;
40
+ format_role?: (role: string) => string;
41
+ } = $props();
42
+
43
+ const permit_offers = permit_offers_state_context.get();
44
+ const form_state = new FormState();
45
+
46
+ let role: string | undefined = $state.raw();
47
+ const selected_role = $derived(role ?? roles[0] ?? '');
48
+ let message = $state.raw('');
49
+ let local_error: string | null = $state.raw(null);
50
+
51
+ const submitting = $derived(permit_offers.loading);
52
+
53
+ const surface_error = (reason: string | null): string | null => {
54
+ switch (reason) {
55
+ case ERROR_OFFER_SELF_TARGET:
56
+ return 'You cannot offer a permit to yourself.';
57
+ case ERROR_OFFER_ROLE_NOT_GRANTABLE:
58
+ return 'That role cannot be offered through this form.';
59
+ case ERROR_OFFER_NOT_AUTHORIZED:
60
+ return 'You are not authorized to offer that role.';
61
+ default:
62
+ return null;
63
+ }
64
+ };
65
+
66
+ const handle_submit = async (): Promise<void> => {
67
+ form_state.attempt();
68
+ local_error = null;
69
+ if (!selected_role) {
70
+ form_state.focus('role');
71
+ return;
72
+ }
73
+ const offer = await permit_offers.create({
74
+ to_account_id,
75
+ role: selected_role,
76
+ scope_id,
77
+ message: message.trim() || null,
78
+ });
79
+ if (offer) {
80
+ message = '';
81
+ form_state.reset();
82
+ on_created?.(offer);
83
+ return;
84
+ }
85
+ // Structured error data carries the reason; fall back to raw error string.
86
+ const data = permit_offers.error_data as
87
+ | {data?: {reason?: string}; reason?: string}
88
+ | null
89
+ | undefined;
90
+ const reason = data?.data?.reason ?? data?.reason ?? null;
91
+ local_error = surface_error(reason) ?? permit_offers.error;
92
+ };
93
+ </script>
94
+
95
+ <form
96
+ class="width_atmost_md column gap_sm"
97
+ onsubmit={(e) => {
98
+ e.preventDefault();
99
+ void handle_submit();
100
+ }}
101
+ {@attach form_state.form()}
102
+ >
103
+ <label>
104
+ <div class="title">role</div>
105
+ <select
106
+ name="role"
107
+ value={selected_role}
108
+ onchange={(e) => (role = e.currentTarget.value)}
109
+ disabled={submitting}
110
+ >
111
+ {#each roles as role_option (role_option)}
112
+ <option value={role_option}>{format_role(role_option)}</option>
113
+ {/each}
114
+ </select>
115
+ </label>
116
+
117
+ <label>
118
+ <div class="title">message (optional)</div>
119
+ <textarea
120
+ name="message"
121
+ bind:value={message}
122
+ maxlength={PERMIT_OFFER_MESSAGE_LENGTH_MAX}
123
+ placeholder="optional note for the recipient"
124
+ disabled={submitting}
125
+ ></textarea>
126
+ </label>
127
+
128
+ <div class="row gap_sm">
129
+ <PendingButton
130
+ pending={submitting}
131
+ disabled={submitting || !selected_role}
132
+ onclick={handle_submit}
133
+ >
134
+ send offer
135
+ </PendingButton>
136
+ </div>
137
+
138
+ {#if local_error}
139
+ <p class="color_c_50 font_size_sm mt_xs mb_0">{local_error}</p>
140
+ {/if}
141
+ </form>
@@ -0,0 +1,14 @@
1
+ import { type PermitOfferJson } from '../auth/permit_offer_schema.js';
2
+ type $$ComponentProps = {
3
+ to_account_id: string;
4
+ /** Roles the caller may offer — caller filters by `web_grantable` upstream. */
5
+ roles: Array<string>;
6
+ /** Resource scope for the offer; `null` (default) yields a global offer. */
7
+ scope_id?: string | null;
8
+ on_created?: (offer: PermitOfferJson) => void;
9
+ format_role?: (role: string) => string;
10
+ };
11
+ declare const PermitOfferForm: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type PermitOfferForm = ReturnType<typeof PermitOfferForm>;
13
+ export default PermitOfferForm;
14
+ //# sourceMappingURL=PermitOfferForm.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PermitOfferForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/PermitOfferForm.svelte"],"names":[],"mappings":"AAiBA,OAAO,EAEL,KAAK,eAAe,EACpB,MAAM,gCAAgC,CAAC;AAOxC,KAAK,gBAAgB,GAAI;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC9C,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC,CAAC;AAsGH,QAAA,MAAM,eAAe,sDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}