@fuzdev/fuz_app 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) 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/socket.svelte.d.ts +16 -16
  18. package/dist/actions/socket.svelte.d.ts.map +1 -1
  19. package/dist/actions/socket.svelte.js +15 -15
  20. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  21. package/dist/auth/CLAUDE.md +923 -0
  22. package/dist/auth/account_action_specs.d.ts +216 -0
  23. package/dist/auth/account_action_specs.d.ts.map +1 -0
  24. package/dist/auth/account_action_specs.js +159 -0
  25. package/dist/auth/account_actions.d.ts +51 -0
  26. package/dist/auth/account_actions.d.ts.map +1 -0
  27. package/dist/auth/account_actions.js +119 -0
  28. package/dist/auth/account_queries.d.ts +6 -2
  29. package/dist/auth/account_queries.d.ts.map +1 -1
  30. package/dist/auth/account_queries.js +40 -4
  31. package/dist/auth/account_routes.d.ts +94 -16
  32. package/dist/auth/account_routes.d.ts.map +1 -1
  33. package/dist/auth/account_routes.js +108 -180
  34. package/dist/auth/account_schema.d.ts +85 -30
  35. package/dist/auth/account_schema.d.ts.map +1 -1
  36. package/dist/auth/account_schema.js +40 -8
  37. package/dist/auth/admin_action_specs.d.ts +674 -0
  38. package/dist/auth/admin_action_specs.d.ts.map +1 -0
  39. package/dist/auth/admin_action_specs.js +287 -0
  40. package/dist/auth/admin_actions.d.ts +69 -0
  41. package/dist/auth/admin_actions.d.ts.map +1 -0
  42. package/dist/auth/admin_actions.js +256 -0
  43. package/dist/auth/api_token.d.ts +10 -0
  44. package/dist/auth/api_token.d.ts.map +1 -1
  45. package/dist/auth/api_token.js +9 -0
  46. package/dist/auth/api_token_queries.d.ts +3 -3
  47. package/dist/auth/api_token_queries.js +3 -3
  48. package/dist/auth/app_settings_schema.d.ts +4 -3
  49. package/dist/auth/app_settings_schema.d.ts.map +1 -1
  50. package/dist/auth/app_settings_schema.js +2 -1
  51. package/dist/auth/audit_log_routes.d.ts +14 -6
  52. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  53. package/dist/auth/audit_log_routes.js +22 -79
  54. package/dist/auth/audit_log_schema.d.ts +100 -29
  55. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  56. package/dist/auth/audit_log_schema.js +83 -11
  57. package/dist/auth/bootstrap_routes.d.ts +14 -0
  58. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  59. package/dist/auth/bootstrap_routes.js +10 -3
  60. package/dist/auth/cleanup.d.ts +63 -0
  61. package/dist/auth/cleanup.d.ts.map +1 -0
  62. package/dist/auth/cleanup.js +80 -0
  63. package/dist/auth/invite_schema.d.ts +11 -10
  64. package/dist/auth/invite_schema.d.ts.map +1 -1
  65. package/dist/auth/invite_schema.js +4 -3
  66. package/dist/auth/migrations.d.ts +6 -0
  67. package/dist/auth/migrations.d.ts.map +1 -1
  68. package/dist/auth/migrations.js +28 -0
  69. package/dist/auth/permit_offer_action_specs.d.ts +364 -0
  70. package/dist/auth/permit_offer_action_specs.d.ts.map +1 -0
  71. package/dist/auth/permit_offer_action_specs.js +216 -0
  72. package/dist/auth/permit_offer_actions.d.ts +96 -0
  73. package/dist/auth/permit_offer_actions.d.ts.map +1 -0
  74. package/dist/auth/permit_offer_actions.js +428 -0
  75. package/dist/auth/permit_offer_notifications.d.ts +361 -0
  76. package/dist/auth/permit_offer_notifications.d.ts.map +1 -0
  77. package/dist/auth/permit_offer_notifications.js +179 -0
  78. package/dist/auth/permit_offer_queries.d.ts +165 -0
  79. package/dist/auth/permit_offer_queries.d.ts.map +1 -0
  80. package/dist/auth/permit_offer_queries.js +390 -0
  81. package/dist/auth/permit_offer_schema.d.ts +103 -0
  82. package/dist/auth/permit_offer_schema.d.ts.map +1 -0
  83. package/dist/auth/permit_offer_schema.js +142 -0
  84. package/dist/auth/permit_queries.d.ts +77 -14
  85. package/dist/auth/permit_queries.d.ts.map +1 -1
  86. package/dist/auth/permit_queries.js +119 -24
  87. package/dist/auth/session_queries.d.ts +4 -2
  88. package/dist/auth/session_queries.d.ts.map +1 -1
  89. package/dist/auth/session_queries.js +4 -2
  90. package/dist/auth/signup_routes.d.ts +13 -0
  91. package/dist/auth/signup_routes.d.ts.map +1 -1
  92. package/dist/auth/signup_routes.js +14 -7
  93. package/dist/http/CLAUDE.md +584 -0
  94. package/dist/http/pending_effects.d.ts +29 -0
  95. package/dist/http/pending_effects.d.ts.map +1 -0
  96. package/dist/http/pending_effects.js +31 -0
  97. package/dist/http/route_spec.d.ts.map +1 -1
  98. package/dist/http/route_spec.js +4 -3
  99. package/dist/rate_limiter.d.ts +30 -0
  100. package/dist/rate_limiter.d.ts.map +1 -1
  101. package/dist/rate_limiter.js +25 -2
  102. package/dist/realtime/sse_auth_guard.d.ts +2 -0
  103. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  104. package/dist/realtime/sse_auth_guard.js +5 -3
  105. package/dist/testing/CLAUDE.md +668 -1
  106. package/dist/testing/admin_integration.d.ts +10 -7
  107. package/dist/testing/admin_integration.d.ts.map +1 -1
  108. package/dist/testing/admin_integration.js +382 -482
  109. package/dist/testing/app_server.d.ts +7 -6
  110. package/dist/testing/app_server.d.ts.map +1 -1
  111. package/dist/testing/attack_surface.d.ts +9 -3
  112. package/dist/testing/attack_surface.d.ts.map +1 -1
  113. package/dist/testing/attack_surface.js +4 -4
  114. package/dist/testing/audit_completeness.d.ts +6 -0
  115. package/dist/testing/audit_completeness.d.ts.map +1 -1
  116. package/dist/testing/audit_completeness.js +158 -134
  117. package/dist/testing/auth_apps.d.ts.map +1 -1
  118. package/dist/testing/auth_apps.js +4 -33
  119. package/dist/testing/db.d.ts +1 -1
  120. package/dist/testing/db.d.ts.map +1 -1
  121. package/dist/testing/db.js +2 -0
  122. package/dist/testing/entities.d.ts +35 -13
  123. package/dist/testing/entities.d.ts.map +1 -1
  124. package/dist/testing/entities.js +17 -0
  125. package/dist/testing/integration.d.ts +10 -0
  126. package/dist/testing/integration.d.ts.map +1 -1
  127. package/dist/testing/integration.js +352 -340
  128. package/dist/testing/integration_helpers.d.ts +16 -5
  129. package/dist/testing/integration_helpers.d.ts.map +1 -1
  130. package/dist/testing/integration_helpers.js +24 -4
  131. package/dist/testing/rate_limiting.d.ts +7 -0
  132. package/dist/testing/rate_limiting.d.ts.map +1 -1
  133. package/dist/testing/rate_limiting.js +41 -10
  134. package/dist/testing/rpc_helpers.d.ts +153 -1
  135. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  136. package/dist/testing/rpc_helpers.js +184 -8
  137. package/dist/testing/sse_round_trip.d.ts +8 -0
  138. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  139. package/dist/testing/sse_round_trip.js +10 -3
  140. package/dist/testing/standard.d.ts +9 -1
  141. package/dist/testing/standard.d.ts.map +1 -1
  142. package/dist/testing/standard.js +6 -2
  143. package/dist/testing/surface_invariants.d.ts +7 -3
  144. package/dist/testing/surface_invariants.d.ts.map +1 -1
  145. package/dist/testing/surface_invariants.js +5 -4
  146. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  147. package/dist/testing/ws_round_trip.js +9 -38
  148. package/dist/ui/AccountSessions.svelte +8 -4
  149. package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
  150. package/dist/ui/AdminAccounts.svelte +61 -33
  151. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  152. package/dist/ui/AdminAuditLog.svelte +3 -2
  153. package/dist/ui/AdminAuditLog.svelte.d.ts.map +1 -1
  154. package/dist/ui/AdminInvites.svelte +3 -2
  155. package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
  156. package/dist/ui/AdminOverview.svelte +14 -9
  157. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  158. package/dist/ui/AdminPermitHistory.svelte +3 -2
  159. package/dist/ui/AdminPermitHistory.svelte.d.ts.map +1 -1
  160. package/dist/ui/AdminSessions.svelte +29 -25
  161. package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
  162. package/dist/ui/CLAUDE.md +351 -0
  163. package/dist/ui/OpenSignupToggle.svelte +6 -3
  164. package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
  165. package/dist/ui/PermitOfferForm.svelte +141 -0
  166. package/dist/ui/PermitOfferForm.svelte.d.ts +14 -0
  167. package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -0
  168. package/dist/ui/PermitOfferHistory.svelte +109 -0
  169. package/dist/ui/PermitOfferHistory.svelte.d.ts +11 -0
  170. package/dist/ui/PermitOfferHistory.svelte.d.ts.map +1 -0
  171. package/dist/ui/PermitOfferInbox.svelte +121 -0
  172. package/dist/ui/PermitOfferInbox.svelte.d.ts +12 -0
  173. package/dist/ui/PermitOfferInbox.svelte.d.ts.map +1 -0
  174. package/dist/ui/account_sessions_state.svelte.d.ts +53 -3
  175. package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
  176. package/dist/ui/account_sessions_state.svelte.js +39 -16
  177. package/dist/ui/admin_accounts_state.svelte.d.ts +118 -2
  178. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  179. package/dist/ui/admin_accounts_state.svelte.js +99 -23
  180. package/dist/ui/admin_invites_state.svelte.d.ts +47 -1
  181. package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
  182. package/dist/ui/admin_invites_state.svelte.js +38 -26
  183. package/dist/ui/admin_sessions_state.svelte.d.ts +26 -0
  184. package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
  185. package/dist/ui/admin_sessions_state.svelte.js +35 -21
  186. package/dist/ui/app_settings_state.svelte.d.ts +39 -0
  187. package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
  188. package/dist/ui/app_settings_state.svelte.js +34 -18
  189. package/dist/ui/audit_log_state.svelte.d.ts +40 -3
  190. package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
  191. package/dist/ui/audit_log_state.svelte.js +36 -42
  192. package/dist/ui/auth_state.svelte.d.ts +4 -3
  193. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  194. package/dist/ui/auth_state.svelte.js +4 -1
  195. package/dist/ui/permit_offers_state.svelte.d.ts +125 -0
  196. package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -0
  197. package/dist/ui/permit_offers_state.svelte.js +197 -0
  198. package/package.json +3 -3
  199. package/dist/auth/admin_routes.d.ts +0 -29
  200. package/dist/auth/admin_routes.d.ts.map +0 -1
  201. package/dist/auth/admin_routes.js +0 -226
  202. package/dist/auth/app_settings_routes.d.ts +0 -27
  203. package/dist/auth/app_settings_routes.d.ts.map +0 -1
  204. package/dist/auth/app_settings_routes.js +0 -66
  205. package/dist/auth/invite_routes.d.ts +0 -18
  206. package/dist/auth/invite_routes.d.ts.map +0 -1
  207. package/dist/auth/invite_routes.js +0 -129
@@ -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,351 @@
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
+
270
+ ## RPC adapter contexts
271
+
272
+ All five RPC-carrying contexts have a `() => () => null` default and
273
+ share the same `has_rpc`-gated state-class shape; consumers wire a typed
274
+ RPC client to each narrow interface. See "Key patterns" above for the
275
+ provisioner pattern.
276
+
277
+ - `auth_state_context` — carries `AuthState` directly (not an RPC
278
+ accessor). Used by every auth form, `AdminOverview`,
279
+ `AdminSettings`, `AccountSessions`, `LogoutButton`.
280
+ - `admin_accounts_rpc_context` — `() => AdminAccountsRpc | null`.
281
+ Consumed by `AdminAccounts`, `AdminSessions`, `AdminOverview`.
282
+ - `admin_invites_rpc_context` — `() => AdminInvitesRpc | null`.
283
+ Consumed by `AdminInvites`, `AdminOverview`.
284
+ - `audit_log_rpc_context` — `() => AuditLogRpc | null`. Consumed by
285
+ `AdminAuditLog`, `AdminPermitHistory`, `AdminOverview`.
286
+ - `app_settings_rpc_context` — `() => AppSettingsRpc | null`.
287
+ Consumed by `OpenSignupToggle`, `AdminOverview`.
288
+ - `account_sessions_rpc_context` — `() => AccountSessionsRpc | null`.
289
+ Consumed by `AccountSessions`.
290
+ - `permit_offers_state_context` — carries `PermitOffersState`
291
+ directly. Consumed by `PermitOfferInbox`, `PermitOfferForm`,
292
+ `PermitOfferHistory`. Wiring is ctor-bound (RPC + account/actor
293
+ getters), so there's no separate `permit_offers_rpc_context`.
294
+ - `sidebar_state_context` — `() => SidebarState`. Provisioned by
295
+ `AppShell`.
296
+
297
+ ## Popovers
298
+
299
+ - `popover.svelte.ts` — `Popover` class. Owns `visible`, `position`,
300
+ `align`, `offset`, `popover_class`, `disable_outside_click` as
301
+ `$state.raw`. Three `Attachment` factories: `container`,
302
+ `trigger(params?)`, `content(params?)`. `show()` / `hide()` /
303
+ `toggle()`, plus `update(params)` to swap config. ARIA roles +
304
+ `aria-expanded` / `aria-controls` wired automatically.
305
+ - `position_helpers.ts` — `Position` / `Alignment` / `CardinalPosition`
306
+ types; `generate_position_styles(position, align, offset)` returns
307
+ CSS styles record for absolute positioning (left/right/top/bottom/
308
+ center/overlay).
309
+ - `PopoverButton.svelte` — button + popover composition. Required
310
+ `popover_content: Snippet<[Popover]>`. Either `children` (simple
311
+ content inside the default `<button>`) or `button: Snippet<[Popover]>`
312
+ (custom trigger) — logs in DEV if both or neither are supplied.
313
+ Auto-hides when `disabled`.
314
+ - `ConfirmButton.svelte` — wraps `PopoverButton` for destructive
315
+ actions. Required `onconfirm: (Popover) => void`. `hide_on_confirm`
316
+ default `true`. `position` default `'left'`. Three optional
317
+ snippets — `children`, `popover_content`, `popover_button_content` —
318
+ each receiving `(Popover, confirm)`. Falls back to a remove-glyph
319
+ button when no snippets are supplied.
320
+
321
+ ## Data
322
+
323
+ - `Datatable.svelte` — generic grid (`<script generics="T">`).
324
+ Props: `columns`, `rows`, `row_key = 'id'`, `height?`, optional
325
+ `header` / `cell` / `empty` snippets. Sticky header, CSS-subgrid
326
+ layout, pointer-based column resize (writes deltas to a keyed
327
+ record). Default cell renders `column.format(value, row)` or
328
+ `format_value(value)`.
329
+ - `datatable.ts` — `DatatableColumn<T>` interface (`key`, `label`,
330
+ `width?`, `min_width?`, `format?`), `DATATABLE_COLUMN_WIDTH_DEFAULT`
331
+ (120), `DATATABLE_MIN_COLUMN_WIDTH` (50).
332
+
333
+ ## Fetch + format
334
+
335
+ - `ui_fetch.ts` — `ui_fetch(input, init?)` wraps `fetch` with
336
+ `credentials: 'include'` for cookie-based session auth;
337
+ `parse_response_error(response, fallback?)` safely extracts
338
+ `body.error` even from non-JSON responses (HTML 404 pages, etc.).
339
+ - `ui_format.ts` — display helpers:
340
+ - `format_relative_time(timestamp, now?)` — "2m ago", "3h ago",
341
+ "5d ago", "2mo ago", "1y ago"; "just now" when under a minute;
342
+ bidirectional (future timestamps render as "in 5m" etc.).
343
+ - `format_uptime(ms)` — "45s", "12m", "3h 15m", "2d 5h".
344
+ - `truncate_middle(str, max_length, separator = '…')`.
345
+ - `truncate_uuid(uuid)` — 12-char middle-truncation.
346
+ - `format_datetime_local(timestamp)` — absolute UTC string for
347
+ `title` attributes.
348
+ - `format_value(value)` — table-cell stringifier (NULL / undefined /
349
+ JSON / primitive).
350
+ - `format_audit_metadata(event_type, metadata)` — event-type-
351
+ 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"}
@@ -0,0 +1,109 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Both-directions permit offer history table.
4
+ *
5
+ * Shows every offer involving the current account — recipient or grantor
6
+ * — including terminal rows (accepted, declined, retracted, superseded,
7
+ * expired). Backed by `permit_offer_history` (new RPC action); seeded
8
+ * via `PermitOffersState.fetch_history()`.
9
+ *
10
+ * Consumers plug in optional `format_actor` / `format_scope` callbacks
11
+ * for display names.
12
+ */
13
+
14
+ import {permit_offers_state_context} from './permit_offers_state.svelte.js';
15
+ import Datatable from './Datatable.svelte';
16
+ import type {DatatableColumn} from './datatable.js';
17
+ import {format_relative_time, format_datetime_local, truncate_uuid} from './ui_format.js';
18
+ import type {PermitOfferJson} from '../auth/permit_offer_schema.js';
19
+
20
+ const {
21
+ current_actor_id,
22
+ format_actor = truncate_uuid,
23
+ format_scope,
24
+ format_role = (role: string) => role,
25
+ }: {
26
+ /** Used to label a row as sent vs received. When `null`, direction shows as `-`. */
27
+ current_actor_id: string | null;
28
+ format_actor?: (from_actor_id: string) => string;
29
+ format_scope?: (scope_id: string | null, role: string) => string;
30
+ format_role?: (role: string) => string;
31
+ } = $props();
32
+
33
+ const permit_offers = permit_offers_state_context.get();
34
+
35
+ const now = $state.raw(Date.now());
36
+
37
+ const status_of = (offer: PermitOfferJson): string => {
38
+ if (offer.accepted_at) return 'accepted';
39
+ if (offer.declined_at) return 'declined';
40
+ if (offer.retracted_at) return 'retracted';
41
+ if (offer.superseded_at) return 'superseded';
42
+ if (Date.parse(offer.expires_at) <= now) return 'expired';
43
+ return 'pending';
44
+ };
45
+
46
+ const status_chip_class = (status: string): string => {
47
+ switch (status) {
48
+ case 'accepted':
49
+ return 'chip color_b';
50
+ case 'pending':
51
+ return 'chip color_a';
52
+ case 'declined':
53
+ case 'retracted':
54
+ case 'superseded':
55
+ case 'expired':
56
+ return 'chip color_c';
57
+ default:
58
+ return 'chip';
59
+ }
60
+ };
61
+
62
+ const scope_label = (scope_id: string | null, role: string): string => {
63
+ if (format_scope) return format_scope(scope_id, role);
64
+ return scope_id === null ? 'global' : truncate_uuid(scope_id);
65
+ };
66
+
67
+ const columns: Array<DatatableColumn<PermitOfferJson>> = [
68
+ {key: 'from_actor_id', label: 'direction', width: 110},
69
+ {key: 'role', label: 'role', width: 140},
70
+ {key: 'scope_id', label: 'scope', width: 160},
71
+ {key: 'created_at', label: 'status', width: 120},
72
+ {key: 'expires_at', label: 'time', width: 110},
73
+ ];
74
+ </script>
75
+
76
+ <section>
77
+ <h2>offer history</h2>
78
+
79
+ {#if permit_offers.loading}
80
+ <p class="text_50">loading history...</p>
81
+ {:else if permit_offers.error}
82
+ <p class="color_c_50">{permit_offers.error}</p>
83
+ {:else}
84
+ <Datatable {columns} rows={permit_offers.history} height="400px" row_key="id">
85
+ {#snippet cell(column, row)}
86
+ {#if column.key === 'from_actor_id'}
87
+ {#if current_actor_id && row.from_actor_id === current_actor_id}
88
+ <span class="chip">sent</span>
89
+ <span class="text_50 font_size_sm">to {truncate_uuid(row.to_account_id)}</span>
90
+ {:else}
91
+ <span class="chip">received</span>
92
+ <span class="text_50 font_size_sm">from {format_actor(row.from_actor_id)}</span>
93
+ {/if}
94
+ {:else if column.key === 'role'}
95
+ {format_role(row.role)}
96
+ {:else if column.key === 'scope_id'}
97
+ <span class="text_50">{scope_label(row.scope_id, row.role)}</span>
98
+ {:else if column.key === 'created_at'}
99
+ {@const status = status_of(row)}
100
+ <span class={status_chip_class(status)}>{status}</span>
101
+ {:else if column.key === 'expires_at'}
102
+ <span title={format_datetime_local(row.created_at)}>
103
+ {format_relative_time(row.created_at)}
104
+ </span>
105
+ {/if}
106
+ {/snippet}
107
+ </Datatable>
108
+ {/if}
109
+ </section>
@@ -0,0 +1,11 @@
1
+ type $$ComponentProps = {
2
+ /** Used to label a row as sent vs received. When `null`, direction shows as `-`. */
3
+ current_actor_id: string | null;
4
+ format_actor?: (from_actor_id: string) => string;
5
+ format_scope?: (scope_id: string | null, role: string) => string;
6
+ format_role?: (role: string) => string;
7
+ };
8
+ declare const PermitOfferHistory: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type PermitOfferHistory = ReturnType<typeof PermitOfferHistory>;
10
+ export default PermitOfferHistory;
11
+ //# sourceMappingURL=PermitOfferHistory.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PermitOfferHistory.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/PermitOfferHistory.svelte"],"names":[],"mappings":"AAoBC,KAAK,gBAAgB,GAAI;IACxB,oFAAoF;IACpF,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,YAAY,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,MAAM,CAAC;IACjD,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACjE,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC,CAAC;AAiGH,QAAA,MAAM,kBAAkB,sDAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}