portal-mojo 0.1.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 (369) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +55 -0
  3. package/docs/DetailView.md +173 -0
  4. package/docs/ModelTable.md +128 -0
  5. package/docs/README.md +98 -0
  6. package/docs/admin-assistant.md +72 -0
  7. package/docs/admin-bouncer.md +120 -0
  8. package/docs/admin-cloudwatch.md +38 -0
  9. package/docs/admin-credentials.md +116 -0
  10. package/docs/admin-dashboard.md +15 -0
  11. package/docs/admin-devices-geoip.md +284 -0
  12. package/docs/admin-dns-certificates.md +122 -0
  13. package/docs/admin-dns-records.md +65 -0
  14. package/docs/admin-dns-registrar.md +33 -0
  15. package/docs/admin-dns.md +80 -0
  16. package/docs/admin-identity-users.md +103 -0
  17. package/docs/admin-incidents.md +71 -0
  18. package/docs/admin-jobs.md +426 -0
  19. package/docs/admin-members.md +112 -0
  20. package/docs/admin-messaging.md +56 -0
  21. package/docs/admin-metrics-explorer.md +63 -0
  22. package/docs/admin-monitoring.md +99 -0
  23. package/docs/admin-network-security.md +393 -0
  24. package/docs/admin-phonehub.md +27 -0
  25. package/docs/admin-push.md +36 -0
  26. package/docs/admin-rules.md +87 -0
  27. package/docs/admin-security-tickets.md +40 -0
  28. package/docs/admin-settings.md +78 -0
  29. package/docs/admin-shortlinks.md +32 -0
  30. package/docs/admin-storage.md +141 -0
  31. package/docs/auth-pages.md +212 -0
  32. package/docs/auth.md +58 -0
  33. package/docs/calendar.md +122 -0
  34. package/docs/charts.md +303 -0
  35. package/docs/client.md +206 -0
  36. package/docs/collection-multiselect.md +117 -0
  37. package/docs/collection-select.md +132 -0
  38. package/docs/combobox.md +105 -0
  39. package/docs/dataview.md +226 -0
  40. package/docs/datepicker.md +151 -0
  41. package/docs/daterange.md +170 -0
  42. package/docs/datetimepicker.md +249 -0
  43. package/docs/defineModel.md +72 -0
  44. package/docs/detail-primitives.md +174 -0
  45. package/docs/feedback.md +85 -0
  46. package/docs/file-upload-ux.md +146 -0
  47. package/docs/forms.md +435 -0
  48. package/docs/grouping-and-fmt.md +98 -0
  49. package/docs/idioms.md +112 -0
  50. package/docs/image-editor.md +101 -0
  51. package/docs/loading.md +242 -0
  52. package/docs/location-address.md +76 -0
  53. package/docs/markdown.md +203 -0
  54. package/docs/menus-and-access.md +89 -0
  55. package/docs/multiselect-dropdown.md +153 -0
  56. package/docs/params.md +73 -0
  57. package/docs/popover.md +130 -0
  58. package/docs/realtime.md +61 -0
  59. package/docs/record-feed.md +238 -0
  60. package/docs/right-panel.md +42 -0
  61. package/docs/tabs-and-form-wizard.md +60 -0
  62. package/docs/taginput.md +120 -0
  63. package/docs/timepicker.md +173 -0
  64. package/docs/timezone-select.md +147 -0
  65. package/docs/worldmap.md +302 -0
  66. package/package.json +55 -0
  67. package/src/admin/assistant/AssistantFeed.tsx +29 -0
  68. package/src/admin/assistant/AssistantPanel.tsx +65 -0
  69. package/src/admin/assistant/api.ts +17 -0
  70. package/src/admin/assistant/data.ts +63 -0
  71. package/src/admin/assistant/index.ts +21 -0
  72. package/src/admin/assistant/launchers.tsx +33 -0
  73. package/src/admin/assistant/pages.tsx +78 -0
  74. package/src/admin/assistant/streaming.ts +238 -0
  75. package/src/admin/assistant/types.ts +30 -0
  76. package/src/admin/bouncer/devices.tsx +230 -0
  77. package/src/admin/bouncer/index.ts +34 -0
  78. package/src/admin/bouncer/models.ts +199 -0
  79. package/src/admin/bouncer/signals.tsx +182 -0
  80. package/src/admin/bouncer/signatures.tsx +220 -0
  81. package/src/admin/cloudwatch/CloudWatchChart.tsx +61 -0
  82. package/src/admin/cloudwatch/CloudWatchDashboardPage.tsx +52 -0
  83. package/src/admin/cloudwatch/CloudWatchResourceDetail.tsx +33 -0
  84. package/src/admin/cloudwatch/data.ts +163 -0
  85. package/src/admin/cloudwatch/index.ts +16 -0
  86. package/src/admin/credentials/group-api-keys.tsx +433 -0
  87. package/src/admin/credentials/index.ts +4 -0
  88. package/src/admin/credentials/models.ts +244 -0
  89. package/src/admin/credentials/secret-dialog.tsx +77 -0
  90. package/src/admin/credentials/webhook-subscriptions.tsx +423 -0
  91. package/src/admin/dashboard/AdminDashboardPage.tsx +101 -0
  92. package/src/admin/dashboard/data.ts +60 -0
  93. package/src/admin/dashboard/index.ts +2 -0
  94. package/src/admin/dns/CertificateDetail.tsx +115 -0
  95. package/src/admin/dns/CertificateLifecyclePoller.tsx +141 -0
  96. package/src/admin/dns/CertificateRequestDialog.tsx +103 -0
  97. package/src/admin/dns/CertificatesPage.tsx +85 -0
  98. package/src/admin/dns/DnsRecordEditor.tsx +110 -0
  99. package/src/admin/dns/DnsRecordsPage.tsx +36 -0
  100. package/src/admin/dns/DnsRecordsPanel.tsx +71 -0
  101. package/src/admin/dns/DomainCertificatesSection.tsx +40 -0
  102. package/src/admin/dns/DomainDetail.tsx +41 -0
  103. package/src/admin/dns/DomainOnboardingDialog.tsx +27 -0
  104. package/src/admin/dns/DomainPurchaseDetail.tsx +11 -0
  105. package/src/admin/dns/DomainPurchaseWizard.tsx +112 -0
  106. package/src/admin/dns/DomainPurchasesPage.tsx +16 -0
  107. package/src/admin/dns/DomainsPage.tsx +27 -0
  108. package/src/admin/dns/ProviderCredentialsPage.tsx +304 -0
  109. package/src/admin/dns/RegistrantContactPage.tsx +42 -0
  110. package/src/admin/dns/api.ts +515 -0
  111. package/src/admin/dns/certificate-data.ts +221 -0
  112. package/src/admin/dns/data.ts +32 -0
  113. package/src/admin/dns/dns-data.ts +266 -0
  114. package/src/admin/dns/dns-integration.ts +24 -0
  115. package/src/admin/dns/domain-links.ts +37 -0
  116. package/src/admin/dns/index.ts +86 -0
  117. package/src/admin/dns/models.ts +418 -0
  118. package/src/admin/dns/purchase-data.ts +67 -0
  119. package/src/admin/dns/registrant-data.ts +52 -0
  120. package/src/admin/identity/index.ts +5 -0
  121. package/src/admin/identity/members/GroupMembersPanel.tsx +146 -0
  122. package/src/admin/identity/members/MemberDetail.tsx +226 -0
  123. package/src/admin/identity/members/MembersPage.tsx +144 -0
  124. package/src/admin/identity/members/index.ts +24 -0
  125. package/src/admin/identity/members/member-flows.tsx +132 -0
  126. package/src/admin/identity/members/models.ts +204 -0
  127. package/src/admin/identity/users/UserDetail.tsx +257 -0
  128. package/src/admin/identity/users/UsersPage.tsx +163 -0
  129. package/src/admin/identity/users/index.ts +27 -0
  130. package/src/admin/identity/users/models.ts +232 -0
  131. package/src/admin/identity/users/sections/ApiKeysSection.tsx +119 -0
  132. package/src/admin/identity/users/sections/AuditSection.tsx +190 -0
  133. package/src/admin/identity/users/sections/DevicesSection.tsx +140 -0
  134. package/src/admin/identity/users/sections/GroupsSection.tsx +64 -0
  135. package/src/admin/identity/users/sections/LoginsSection.tsx +106 -0
  136. package/src/admin/identity/users/sections/MetadataPanel.tsx +36 -0
  137. package/src/admin/identity/users/sections/NotificationsSection.tsx +117 -0
  138. package/src/admin/identity/users/sections/OAuthSection.tsx +72 -0
  139. package/src/admin/identity/users/sections/OverviewSection.tsx +109 -0
  140. package/src/admin/identity/users/sections/PermissionsSection.tsx +35 -0
  141. package/src/admin/identity/users/sections/PersonalSection.tsx +73 -0
  142. package/src/admin/identity/users/sections/ProfileSection.tsx +175 -0
  143. package/src/admin/identity/users/sections/SecuritySection.tsx +123 -0
  144. package/src/admin/identity/users/sections/actions.tsx +752 -0
  145. package/src/admin/identity/users/sections/permission-catalog.ts +138 -0
  146. package/src/admin/identity/users/sections/queries.ts +74 -0
  147. package/src/admin/identity/users/sections/shared.tsx +299 -0
  148. package/src/admin/incidents/EventDetail.tsx +43 -0
  149. package/src/admin/incidents/EventsPage.tsx +52 -0
  150. package/src/admin/incidents/IncidentDetail.tsx +68 -0
  151. package/src/admin/incidents/IncidentsPage.tsx +102 -0
  152. package/src/admin/incidents/forensics.tsx +73 -0
  153. package/src/admin/incidents/index.ts +16 -0
  154. package/src/admin/incidents/models.ts +113 -0
  155. package/src/admin/incidents/sanitize.ts +71 -0
  156. package/src/admin/index.ts +383 -0
  157. package/src/admin/jobs/JobDashboardPage.tsx +111 -0
  158. package/src/admin/jobs/JobDetail.tsx +721 -0
  159. package/src/admin/jobs/JobRunnersPage.tsx +216 -0
  160. package/src/admin/jobs/JobsTablePage.tsx +122 -0
  161. package/src/admin/jobs/RunnerDetail.tsx +576 -0
  162. package/src/admin/jobs/ScheduledTaskDetail.tsx +421 -0
  163. package/src/admin/jobs/ScheduledTasksPage.tsx +167 -0
  164. package/src/admin/jobs/columns.tsx +297 -0
  165. package/src/admin/jobs/control.ts +488 -0
  166. package/src/admin/jobs/index.ts +85 -0
  167. package/src/admin/jobs/models.ts +599 -0
  168. package/src/admin/jobs/queries.ts +272 -0
  169. package/src/admin/jobs/scheduled-task-form.tsx +493 -0
  170. package/src/admin/jobs/sections/JobChannelsPanel.tsx +142 -0
  171. package/src/admin/jobs/sections/JobOperationsSection.tsx +423 -0
  172. package/src/admin/jobs/sections/JobRunnersStrip.tsx +82 -0
  173. package/src/admin/jobs/sections/JobThroughputSection.tsx +118 -0
  174. package/src/admin/messaging/EmailDomainWizard.tsx +24 -0
  175. package/src/admin/messaging/EmailDomainsPage.tsx +17 -0
  176. package/src/admin/messaging/EmailTemplatesPage.tsx +13 -0
  177. package/src/admin/messaging/MailboxesPage.tsx +15 -0
  178. package/src/admin/messaging/PublicMessagesPage.tsx +13 -0
  179. package/src/admin/messaging/SandboxedEmailPreview.tsx +23 -0
  180. package/src/admin/messaging/SentMessagesPage.tsx +11 -0
  181. package/src/admin/messaging/api.ts +17 -0
  182. package/src/admin/messaging/data.ts +10 -0
  183. package/src/admin/messaging/index.ts +23 -0
  184. package/src/admin/messaging/models.ts +76 -0
  185. package/src/admin/messaging/push/PushPage.tsx +44 -0
  186. package/src/admin/messaging/push/api.ts +17 -0
  187. package/src/admin/messaging/push/index.ts +9 -0
  188. package/src/admin/messaging/push/models.ts +43 -0
  189. package/src/admin/messaging/sanitize.ts +44 -0
  190. package/src/admin/monitoring/LogInspector.tsx +160 -0
  191. package/src/admin/monitoring/LogsPage.tsx +135 -0
  192. package/src/admin/monitoring/MetricsExplorerPage.tsx +360 -0
  193. package/src/admin/monitoring/MetricsPermissionsPage.tsx +166 -0
  194. package/src/admin/monitoring/MetricsSourcePicker.tsx +136 -0
  195. package/src/admin/monitoring/index.ts +55 -0
  196. package/src/admin/monitoring/metrics-explorer-client.ts +229 -0
  197. package/src/admin/monitoring/metrics-explorer-data.ts +158 -0
  198. package/src/admin/monitoring/models.ts +155 -0
  199. package/src/admin/network/BlockedIPsPage.tsx +194 -0
  200. package/src/admin/network/BlockedIpDetail.tsx +262 -0
  201. package/src/admin/network/FirewallLogPage.tsx +115 -0
  202. package/src/admin/network/IPSetDetail.tsx +280 -0
  203. package/src/admin/network/IPSetEditor.tsx +191 -0
  204. package/src/admin/network/IPSetsPage.tsx +177 -0
  205. package/src/admin/network/geofence/BlocksTab.tsx +289 -0
  206. package/src/admin/network/geofence/ExemptionsTab.tsx +349 -0
  207. package/src/admin/network/geofence/GeofencingPage.tsx +119 -0
  208. package/src/admin/network/geofence/PostureHeader.tsx +184 -0
  209. package/src/admin/network/geofence/RuleEditor.tsx +176 -0
  210. package/src/admin/network/geofence/RulesTab.tsx +254 -0
  211. package/src/admin/network/geofence/SimulatorTab.tsx +281 -0
  212. package/src/admin/network/geofence/geofence-data.ts +674 -0
  213. package/src/admin/network/index.ts +68 -0
  214. package/src/admin/network/models.ts +607 -0
  215. package/src/admin/phonehub/PhoneHubPage.tsx +57 -0
  216. package/src/admin/phonehub/api.ts +47 -0
  217. package/src/admin/phonehub/data.ts +6 -0
  218. package/src/admin/phonehub/index.ts +10 -0
  219. package/src/admin/phonehub/models.ts +68 -0
  220. package/src/admin/rules/HandlerChainBuilder.tsx +51 -0
  221. package/src/admin/rules/RuleSetDetailPage.tsx +33 -0
  222. package/src/admin/rules/RuleSetsPage.tsx +33 -0
  223. package/src/admin/rules/editors.tsx +54 -0
  224. package/src/admin/rules/handler-dsl.ts +213 -0
  225. package/src/admin/rules/index.ts +13 -0
  226. package/src/admin/rules/models.ts +112 -0
  227. package/src/admin/security/devices/LoginEventDetail.tsx +123 -0
  228. package/src/admin/security/devices/LoginLocationMap.tsx +321 -0
  229. package/src/admin/security/devices/LoginLocationsPage.tsx +162 -0
  230. package/src/admin/security/devices/UserDeviceDetail.tsx +546 -0
  231. package/src/admin/security/devices/UserDeviceLocationDetail.tsx +272 -0
  232. package/src/admin/security/devices/UserDevicesPage.tsx +84 -0
  233. package/src/admin/security/devices/index.ts +40 -0
  234. package/src/admin/security/devices/models.ts +470 -0
  235. package/src/admin/security/geoip/GeoIpCachePage.tsx +135 -0
  236. package/src/admin/security/geoip/GeoIpDossier.tsx +804 -0
  237. package/src/admin/security/geoip/geoip-forms.ts +102 -0
  238. package/src/admin/security/geoip/index.ts +27 -0
  239. package/src/admin/security/geoip/models.ts +397 -0
  240. package/src/admin/security/index.ts +31 -0
  241. package/src/admin/security/models.ts +137 -0
  242. package/src/admin/security/tickets.tsx +562 -0
  243. package/src/admin/security-permissions.ts +5 -0
  244. package/src/admin/settings/SettingDetail.tsx +57 -0
  245. package/src/admin/settings/SettingEditor.tsx +157 -0
  246. package/src/admin/settings/SettingsPage.tsx +72 -0
  247. package/src/admin/settings/index.ts +19 -0
  248. package/src/admin/settings/model.ts +143 -0
  249. package/src/admin/shortlinks/ShortlinkDetail.tsx +55 -0
  250. package/src/admin/shortlinks/ShortlinkHistoryPage.tsx +18 -0
  251. package/src/admin/shortlinks/ShortlinksPage.tsx +59 -0
  252. package/src/admin/shortlinks/index.ts +18 -0
  253. package/src/admin/shortlinks/models.ts +246 -0
  254. package/src/admin/storage/BackendsPage.tsx +43 -0
  255. package/src/admin/storage/BucketDetail.tsx +77 -0
  256. package/src/admin/storage/BucketsPage.tsx +64 -0
  257. package/src/admin/storage/FileManagerDetail.tsx +36 -0
  258. package/src/admin/storage/FilePreview.tsx +54 -0
  259. package/src/admin/storage/FileUploadSurface.tsx +250 -0
  260. package/src/admin/storage/FileView.tsx +109 -0
  261. package/src/admin/storage/FilesPage.tsx +53 -0
  262. package/src/admin/storage/file-renditions.ts +99 -0
  263. package/src/admin/storage/index.ts +31 -0
  264. package/src/admin/storage/models.ts +585 -0
  265. package/src/admin/storage/storage-dialogs.tsx +80 -0
  266. package/src/charts/CircularProgress.tsx +281 -0
  267. package/src/charts/KPIStrip.tsx +162 -0
  268. package/src/charts/KPITile.tsx +125 -0
  269. package/src/charts/MetricsChart.tsx +459 -0
  270. package/src/charts/MetricsMiniWidget.tsx +588 -0
  271. package/src/charts/MiniChart.tsx +283 -0
  272. package/src/charts/PieChart.tsx +354 -0
  273. package/src/charts/SeriesChart.tsx +378 -0
  274. package/src/charts/chart-dialogs.tsx +149 -0
  275. package/src/charts/exportChart.ts +144 -0
  276. package/src/charts/index.ts +28 -0
  277. package/src/charts/pie-math.ts +127 -0
  278. package/src/charts/stats.ts +265 -0
  279. package/src/charts/worldmap/WorldMap.tsx +649 -0
  280. package/src/charts/worldmap/countryCentroids.ts +304 -0
  281. package/src/charts/worldmap/geo.ts +293 -0
  282. package/src/charts/worldmap/use-world-land.ts +47 -0
  283. package/src/charts/worldmap/world-land.ts +18 -0
  284. package/src/charts/worldmap/worldmap-data.ts +285 -0
  285. package/src/client/auth.ts +737 -0
  286. package/src/client/client.ts +268 -0
  287. package/src/client/duid.ts +31 -0
  288. package/src/client/errors.ts +33 -0
  289. package/src/client/group-context.ts +28 -0
  290. package/src/client/group.tsx +126 -0
  291. package/src/client/hooks.ts +50 -0
  292. package/src/client/index.ts +37 -0
  293. package/src/client/jwt.ts +75 -0
  294. package/src/client/location.ts +285 -0
  295. package/src/client/lookups.ts +41 -0
  296. package/src/client/markdown-fallback.tsx +115 -0
  297. package/src/client/markdown-parse.ts +386 -0
  298. package/src/client/markdown-sanitize.ts +217 -0
  299. package/src/client/markdown.ts +122 -0
  300. package/src/client/me.ts +188 -0
  301. package/src/client/mock.ts +9984 -0
  302. package/src/client/model.ts +227 -0
  303. package/src/client/params.ts +292 -0
  304. package/src/client/realtime-mock.ts +221 -0
  305. package/src/client/realtime.ts +561 -0
  306. package/src/client/record-feed.ts +329 -0
  307. package/src/client/safe-export.ts +71 -0
  308. package/src/client/types.ts +278 -0
  309. package/src/client/upload.ts +421 -0
  310. package/src/ui/AddressField.tsx +273 -0
  311. package/src/ui/AttachmentQueue.tsx +92 -0
  312. package/src/ui/CollectionMultiSelect.tsx +622 -0
  313. package/src/ui/CollectionSelect.tsx +517 -0
  314. package/src/ui/ComboBox.tsx +336 -0
  315. package/src/ui/DetailView.tsx +438 -0
  316. package/src/ui/FileDrop.tsx +194 -0
  317. package/src/ui/FileField.tsx +369 -0
  318. package/src/ui/FilterBar.tsx +269 -0
  319. package/src/ui/FormFields.tsx +77 -0
  320. package/src/ui/FormView.tsx +360 -0
  321. package/src/ui/FormWizard.tsx +336 -0
  322. package/src/ui/GroupSwitcher.tsx +202 -0
  323. package/src/ui/Guarded.tsx +16 -0
  324. package/src/ui/MarkdownView.tsx +89 -0
  325. package/src/ui/ModelTable.tsx +848 -0
  326. package/src/ui/MultiSelectDropdown.tsx +390 -0
  327. package/src/ui/Popover.tsx +204 -0
  328. package/src/ui/RecordFeed.tsx +627 -0
  329. package/src/ui/RequiresGroup.tsx +32 -0
  330. package/src/ui/RightPanel.tsx +107 -0
  331. package/src/ui/SidebarNav.tsx +330 -0
  332. package/src/ui/Tabs.tsx +161 -0
  333. package/src/ui/TagInput.tsx +381 -0
  334. package/src/ui/ThemeProvider.tsx +52 -0
  335. package/src/ui/UploadQueue.tsx +603 -0
  336. package/src/ui/armed-button.tsx +91 -0
  337. package/src/ui/date/Calendar.tsx +498 -0
  338. package/src/ui/date/DatePicker.tsx +289 -0
  339. package/src/ui/date/DateRangePicker.tsx +280 -0
  340. package/src/ui/date/DateTimePicker.tsx +587 -0
  341. package/src/ui/date/PresetRail.tsx +165 -0
  342. package/src/ui/date/TimePicker.tsx +624 -0
  343. package/src/ui/date/TimezoneSelect.tsx +267 -0
  344. package/src/ui/date/fns.ts +679 -0
  345. package/src/ui/detail/DataView.tsx +692 -0
  346. package/src/ui/detail/FlowStrip.tsx +80 -0
  347. package/src/ui/detail/KnownFieldsCard.tsx +121 -0
  348. package/src/ui/detail/MetadataSection.tsx +219 -0
  349. package/src/ui/detail/StackTraceView.tsx +150 -0
  350. package/src/ui/detail/StatusPanel.tsx +73 -0
  351. package/src/ui/detail/Timeline.tsx +52 -0
  352. package/src/ui/detail/index.ts +12 -0
  353. package/src/ui/field-registry.tsx +428 -0
  354. package/src/ui/field-wire.ts +322 -0
  355. package/src/ui/form-autosave.ts +583 -0
  356. package/src/ui/format.ts +444 -0
  357. package/src/ui/grouping.ts +150 -0
  358. package/src/ui/image-editor/ImageEditor.tsx +484 -0
  359. package/src/ui/image-editor/canvas.ts +254 -0
  360. package/src/ui/image-editor/index.ts +3 -0
  361. package/src/ui/image-editor/math.ts +416 -0
  362. package/src/ui/index.ts +58 -0
  363. package/src/ui/loading.tsx +604 -0
  364. package/src/ui/menu-registry.ts +213 -0
  365. package/src/ui/modal.tsx +297 -0
  366. package/src/ui/password.tsx +302 -0
  367. package/src/ui/schema-form-core.tsx +315 -0
  368. package/src/ui/toast.tsx +219 -0
  369. package/src/ui/ui.tsx +45 -0
@@ -0,0 +1,78 @@
1
+ # Admin settings
2
+
3
+ `portal-mojo/admin` provides a real django-mojo runtime-settings administrator
4
+ for standalone and embedded portals. It replaces static settings-demo data with
5
+ the `/api/settings` model while keeping write-only values out of client caches.
6
+
7
+ ## Routes and access
8
+
9
+ Register `SettingsPage` at the section index. Selecting a row opens
10
+ `SettingDetail` through the shared KISS modal; there is no `:id` child route.
11
+ The route and sidebar contribution require either `sys.manage_settings`
12
+ or `sys.groups`, mirroring `Setting.RestMeta` while ensuring active-group member
13
+ permissions cannot open a fleet-wide settings surface.
14
+
15
+ ```tsx
16
+ {
17
+ id: 'settings',
18
+ title: 'Settings',
19
+ icon: 'bi-gear',
20
+ permissions: SETTINGS_PERMISSIONS,
21
+ routes: [
22
+ { path: '', component: SettingsPage, permissions: SETTINGS_PERMISSIONS },
23
+ ],
24
+ }
25
+ ```
26
+
27
+ `SettingsPage` searches only the key (the backend's sole `SEARCH_FIELDS`
28
+ entry), and offers server filters for `is_secret` and `group__isnull`. Keys are
29
+ immutable after creation. Operators with `sys.groups` may change scope, sending
30
+ a group id or explicit `null` for Global. A `sys.manage_settings`-only operator
31
+ sees the current scope read-only; the editor never mounts the group picker or
32
+ queries `/api/group`, and preserves that scope on save. New settings default to
33
+ Global. Delete is intentionally absent because live `Setting.RestMeta` does not
34
+ declare deletion.
35
+
36
+ ## Atomic write contract
37
+
38
+ Use `saveSettingAtomic()` / `buildSettingPayload()` rather than a generic
39
+ autosave. django-mojo processes fields against the model's current secrecy
40
+ state in request insertion order. Create-secret remains `is_secret → value`.
41
+ An existing Plain→Secret transition must be `value → is_secret`, so the current
42
+ plain row still holds the replacement when the flag flips and the pre-save hook
43
+ encrypts it. Secret→Plain is the reverse: `is_secret → value`, so the explicit
44
+ replacement lands as plain text. Same-secret replacement sends only `value`.
45
+
46
+ Both transitions require an explicit replacement; a new secret or a
47
+ Plain→Secret transition requires a non-empty secret, while Secret→Plain may
48
+ explicitly replace it with an empty plain value. An unchanged secret with a
49
+ blank replacement omits `value`; an unchanged plain setting may intentionally
50
+ save an empty string. A no-op makes no request.
51
+
52
+ The custom save bypasses TanStack MutationCache because mutation variables
53
+ retain submitted bodies. Its response passes through `SettingModel` sanitation
54
+ before it may enter Query cache. For secret rows, the cache contains only the
55
+ masked `display_value`; the `value` property is removed. Success toasts are
56
+ generic and backend failures stay in the form; neither interpolates a submitted
57
+ value.
58
+
59
+ ## Editor behavior
60
+
61
+ The replacement textarea is deliberately uncontrolled. Existing secret rows
62
+ seed it with an empty string—never `******`—and switching secret status clears
63
+ the control and marks it as needing an explicit replacement. Backend collision
64
+ and registered-key validation messages stay visible in the modal.
65
+
66
+ ## Exports
67
+
68
+ ```ts
69
+ import {
70
+ SettingModel,
71
+ SettingsPage,
72
+ SettingDetail,
73
+ SETTINGS_PERMISSIONS,
74
+ SETTINGS_ADMIN_SECTION,
75
+ buildSettingPayload,
76
+ saveSettingAtomic,
77
+ } from 'portal-mojo/admin';
78
+ ```
@@ -0,0 +1,32 @@
1
+ # Shortlinks Admin
2
+
3
+ Import from `portal-mojo/admin`. `SHORTLINKS_ADMIN_SECTION` contributes
4
+ `shortlinks/links` and `shortlinks/history` to the global, no-group
5
+ Communications navigation. Both routes require `sys.manage_shortlinks`.
6
+
7
+ ## Privacy boundary
8
+
9
+ django-mojo's `graph=list` includes the destination `url`. `ShortlinkModel`
10
+ therefore applies a positive row projection inside the query function before
11
+ TanStack Query receives a row. Destinations and metadata are omitted.
12
+
13
+ Click history is manage-only and projects raw records into: click id, shortlink
14
+ id/code, bot classification, timestamp, a bounded browser/bot summary, and an
15
+ HTTP(S) referrer origin. IP addresses, raw user-agent strings, referrer
16
+ path/query/fragment, metadata, and nested `shortlink.url` are never cached.
17
+ Neither table supports search or export, and list parameters are allowlisted.
18
+
19
+ ## Mutations and short URLs
20
+
21
+ Create, active-state changes, and delete are imperative one-shot requests. They
22
+ do not claim fresh-auth protection the backend does not provide and do not use
23
+ retrying mutation hooks. Every outcome runs an authoritative reconciliation in
24
+ `finally` semantics. If reconciliation itself fails, the UI warns that the
25
+ change may have persisted and blocks retry until a successful refresh.
26
+
27
+ Short URLs are `${apiOrigin()}/s/{code}` (or `/s/{code}` for the same-origin
28
+ mock). The package makes no canonical-host claim.
29
+
30
+ Human and bot totals are counts of retained tracking records. The displayed
31
+ remainder is `max(0, hit_count - human - bot)`; `hit_count` also includes hits
32
+ made while tracking was disabled.
@@ -0,0 +1,141 @@
1
+ # Storage Admin
2
+
3
+ ```ts
4
+ import {
5
+ STORAGE_ADMIN_SECTION, BucketsPage, BackendsPage, FilesPage, FileView,
6
+ FileUploadSurface, FileManagerModel, FileManagerUploadPolicyModel, FileModel,
7
+ } from 'portal-mojo/admin';
8
+ ```
9
+
10
+ `STORAGE_ADMIN_SECTION` contributes global/no-group Infrastructure routes at
11
+ `storage/buckets`, `storage/backends`, and `storage/files` (or the same paths
12
+ under an embedded `/system` mount). There are no record routes: buckets,
13
+ backends, nested files, and files use `modal.detail`.
14
+
15
+ ## Permissions
16
+
17
+ - Buckets: `sys.manage_aws | sys.files`.
18
+ - Backends/Files view: `sys.view_fileman | sys.manage_files | sys.files`.
19
+ - Backend/File mutations: `sys.manage_files | sys.files`.
20
+ - Add File, its whole-page drop surface, and upload destination requests mount
21
+ only with the File mutation grant.
22
+ - Group and user selectors mount only with their respective system-pinned
23
+ directory grants. Admin never reads an active group.
24
+
25
+ The section audience is the union of child audiences; each route retains its
26
+ exact clause. A member-only grant cannot satisfy any Storage route.
27
+
28
+ ## Buckets
29
+
30
+ `GET /api/aws/s3/bucket` is one complete account inventory, rendered with
31
+ explicitly local name search/order and no paging claims. Failed inventory is
32
+ an Error/Retry state, never “No buckets.”
33
+
34
+ All writes are `POST /api/aws/s3/bucket/<encoded-name>`:
35
+
36
+ - create: `{}` → `{id,name,created_new:true|false|null}`;
37
+ - access: `{set_public:boolean}` → verified `is_public`, `complete:true`,
38
+ `mutation_state:'complete'`;
39
+ - empty: `{empty:{confirm_name:name}}` under `withFreshAuth`, after an
40
+ `ArmedButton` and exact case-sensitive UI confirmation.
41
+
42
+ Every POST invalidates and actively refetches in `finally`. A primary failure
43
+ is preserved if refresh also fails. HTTP 409
44
+ `s3_operation_incomplete` retains finite `none|partial|unknown` evidence,
45
+ acknowledged counts, classified failures, nullable remaining values, and the
46
+ access safety-lock posture without fabricating zeros or atomic completion.
47
+ Bucket DELETE is absent.
48
+
49
+ ## Backends
50
+
51
+ `FileManagerModel` forces `graph=list` and allowlists graph fields before any
52
+ cache write. Readable credentials are masks only. Credential inputs begin
53
+ blank; blank means preserve. `saveFileManagerAtomic` runs imperatively outside
54
+ MutationCache, strips masks and blank secrets, fresh-auth retries the same
55
+ closure, refetches authoritatively, and compares requested owner FKs.
56
+
57
+ Creation supports runtime backends `file` and `s3`, explicitly starts private,
58
+ and requires an authorized group/user owner unless the caller is superuser and
59
+ chooses System. Supported payload actions are `test_connection`, `check_cors`,
60
+ `fix_cors`, and `clone`; inner `{status:false}` rejects. CORS is S3-only.
61
+ FileManager DELETE and `check_public_access` are absent.
62
+
63
+ ## Files and capability URLs
64
+
65
+ `FileModel` forces the list graph and normal saves to `filename`, `is_public`,
66
+ and explicit `group`. Group moves refetch and compare the authoritative FK.
67
+ Selection supports public/private, explicit prepared group move, safe
68
+ same-gesture download, and all-settled confirmed delete. Authorized operators
69
+ also get **Add File** and a drag-only whole-page drop overlay. Both open one
70
+ configuration modal; the bounded queue lives above it, so closing the modal
71
+ does not cancel work or discard outcomes.
72
+
73
+ Every batch chooses an explicit manager. System choices request only active
74
+ `graph=upload_policy` rows with null group/user owners. Group choices first use
75
+ the independently gated active-group directory, then request null-user policy
76
+ rows for that exact group. User-owned and dual-owned managers never enter these
77
+ Admin lists. The safe policy projection contains only name/use, size,
78
+ extension/MIME guidance, and transfer-route support. A false
79
+ `supports_direct_upload` means a local API target, not an unavailable manager.
80
+
81
+ The queue caps at six files and three concurrent transfers. It reports actual
82
+ byte progress, truthful cancel/retry/recovery, partial acceptance, and uncertain
83
+ remote state. Permission loss closes the destination modal, stops its queries,
84
+ and best-effort cancels queued transfers. Completion compares authoritative
85
+ scalar `file_manager_id`/`group_id` with the immutable destination. Terminal
86
+ changes coalesce into one authoritative active Files refetch plus one trailing
87
+ pass; refresh failure remains visible and current filters may hide the new row.
88
+
89
+ Each task owns a private strict `idempotency_key`. A lost initiation response
90
+ reuses it to recover the same File. A server-proven failed/expired lifecycle
91
+ rotates it and creates a fresh attempt on the same Retry click. Keys, browser
92
+ `File` objects, targets/tokens, and completion payloads never enter Query or
93
+ persistent storage.
94
+
95
+ File, thumbnail, rendition, and share URLs are bearer capabilities. The shared
96
+ validator accepts ordinary relative paths or explicit `http:`/`https:` URLs
97
+ without userinfo, and rejects protocol-relative, malformed, `javascript:`, and
98
+ `data:` values. Open/Download uses an anchor with `rel=noreferrer`. Safe exports
99
+ omit every URL, storage path, mask, metadata object, token, and transient.
100
+
101
+ `FileView` sections are Overview, Preview, Details, Renditions, Shares, and
102
+ Metadata. Category precedes MIME fallback: image inline; native audio/video;
103
+ PDF open; office-like documents use the best safe image rendition; archive
104
+ downloads; text/CSV/unknown remain generic. No returned HTML is embedded.
105
+ `StableMediaPreview` captures its safe source for the mounted File id so normal
106
+ query refreshes do not replace the media node; reopening refreshes policy.
107
+
108
+ ## Renditions and shares
109
+
110
+ Role-keyed rendition objects normalize to stable rows. Signatures include
111
+ role, rendition id, status, modified, width, height, and size. Recursive
112
+ non-overlapping polling waits five seconds, makes at most 12 fetch attempts,
113
+ and stops on signature change/arrival, failed or expired upload, close/id
114
+ change, or timeout. Timeout leaves manual Refresh.
115
+
116
+ Regeneration sends `{regenerate_renditions:true|string[]}` with deduped roles
117
+ capped at 20. Share creation sends `{share:true|options}` imperatively; options
118
+ and the newly returned capability URL stay in one modal-local result and never
119
+ enter Query/Mutation caches. The response parser uses the collision-safe
120
+ `shortlink_code` key and validates the ISO `expires_at`. Separately authorized
121
+ rows are labeled “Visible shares”; the backend owner fallback remains available
122
+ without granting global Shortlinks Admin. Safe `track_clicks` and bounded note
123
+ values are retained, and operators can use the live active toggle or permanent
124
+ DELETE contract.
125
+
126
+ ## Mock and verification
127
+
128
+ The central mock owns bucket, FileManager, File, upload attempts/byte sessions,
129
+ canonical rendition, and shortlink rows. It implements the safe policy graph,
130
+ flat lifecycle/target response, idempotent replay, terminal retry, and local or
131
+ provider paths. Credential writes derive masks and discard raw values. Stable
132
+ personas are `storage.viewer@nativemojo.com`,
133
+ `storage.manager@nativemojo.com`, `bucket.manager@nativemojo.com`, and
134
+ `storage.member@nativemojo.com` (password `mojo`); the showcase operator has
135
+ the broad demo grants.
136
+
137
+ Run `npm run verify:admin-storage`, `npm run verify:file-upload`, and
138
+ `npm run verify:upload-ux` for the focused route, permission, upload,
139
+ cache-safety, URL, FK-reconciliation, and bounded-polling contracts. Browser
140
+ evidence belongs in the Portal mock for viewer/member gates and in Showcase
141
+ for the exported surface, in both themes.
@@ -0,0 +1,212 @@
1
+ # auth-pages — in-app sign-in surfaces, step-up challenges, password tools
2
+
3
+ The C3 layer over [auth.md](auth.md)'s client: the portal's own login /
4
+ forgot / reset / magic-link pages (for deployments serving the portal
5
+ same-origin with the API), the fresh-auth (step-up) modal, and the password
6
+ strength/generator pieces. Board #1259.
7
+
8
+ ```ts
9
+ // The package pieces
10
+ import {
11
+ // MFA completion (consumes the mfa_token from login()'s 'mfa' result)
12
+ completeMfaTotp, completeMfaRecovery, sendMfaSms, completeMfaSms,
13
+ // Fresh-auth (step-up) challenge surface
14
+ REAUTH_STATUS, isReauthRequired, setFreshAuthHandler, requestFreshAuth,
15
+ withFreshAuth, sessionIsPersistent,
16
+ type MfaChallenge, type FreshAuthHandler, type MfaCompletionOptions,
17
+ } from 'portal-mojo/client';
18
+
19
+ import {
20
+ checkPasswordStrength, generatePassword, PasswordStrengthMeter,
21
+ type PasswordStrength, type PasswordStrengthClass, type GeneratePasswordOptions,
22
+ } from 'portal-mojo/ui';
23
+ ```
24
+
25
+ The pages themselves are APP surfaces (`apps/portal/src/pages/auth/`):
26
+ `LoginPage`, `ForgotPage`, `ResetPage`, `MagicPage`, `MfaPanel`,
27
+ `FreshAuthHost`, `AuthLayout` — wired through `routes.tsx`, which exports
28
+ `authRoutes`, `RequireAuth`, and `handleAuthTokenLanding()`.
29
+
30
+ ## Config switch
31
+
32
+ `VITE_MOJO_AUTH` (env, same idiom as `VITE_MOJO_API`):
33
+
34
+ | Value | Behavior |
35
+ |---|---|
36
+ | `inapp` | Unauthenticated visits route to `#/auth/login` (these pages) |
37
+ | `hosted` | Unauthenticated visits bounce to the django-mojo hosted `/auth` pages (bouncer pages, incl. the bot-check interstitial — which exists on the hosted path ONLY) |
38
+ | unset | **hosted when `VITE_MOJO_API` is set** (the default for real deployments), in-app under the mock (there is no hosted page to bounce to) |
39
+
40
+ Unknown values fall back to the unset default WITH a `console.warn`.
41
+ `hosted` + mock also warns and falls back to in-app. Resolution lives in
42
+ `pages/auth/config.ts` (`authMode()` / `inAppAuthEnabled()`).
43
+
44
+ ## Route guard
45
+
46
+ `RequireAuth` wraps the App route element (`<RequireAuth><App/></RequireAuth>`;
47
+ `authRoutes` mount as top-level SIBLINGS — auth pages are full-screen, no app
48
+ chrome). Unauthenticated (per the A1 snapshot: no valid access OR refresh
49
+ token) → in-app login (intended route stashed in
50
+ `sessionStorage['portal:auth-return']`, restored after any successful flow)
51
+ or the hosted redirect (which keeps auth.ts's own stash). Authenticated →
52
+ children plus `<FreshAuthHost/>`. A signed-in visit to `#/auth/login`
53
+ bounces to `/` (web-mojo AuthApp guard parity); the forgot/reset/magic pages
54
+ stay reachable signed-in — completing them re-logs-in by design (the server
55
+ answers with a full TokenGrant).
56
+
57
+ ## Flows (all on the A1 client — no new wire code in pages)
58
+
59
+ - **Password**: `login()` → tokens stored, or `{kind:'mfa'}` → `MfaPanel`.
60
+ Remember-me checkbox maps to `LoginOptions.remember` (localStorage vs
61
+ sessionStorage).
62
+ - **MFA step** (`MfaPanel`, shared with the fresh-auth modal). Methods from
63
+ the server: `totp` (code → `completeMfaTotp`; recovery-code toggle →
64
+ `completeMfaRecovery`), `sms` (`sendMfaSms` **consumes and re-issues the
65
+ mfa_token — always replace your stored token with the returned one**, then
66
+ `completeMfaSms`), `passkey` (a full `loginWithPasskey()` ceremony IS the
67
+ second factor — django-mojo's passkey login never routes through the MFA
68
+ gate). Unknown methods are dropped with a `console.warn`. The challenge
69
+ expiry (`expiresIn`, default 300s) is counted down visibly.
70
+ - **Forgot/reset**: `forgotPassword(email, 'code'|'link')`; code → ResetPage
71
+ code mode (email carried via router state + a sessionStorage stash so
72
+ reloads survive); link → lands on ResetPage token mode. Both resets log
73
+ the user in on success. The reset form carries the live
74
+ `PasswordStrengthMeter` + a generator affordance (fills both fields,
75
+ reveals the value). Strength is ADVISORY — submission is never blocked on
76
+ it; the server owns password policy.
77
+ - **Magic link**: request via `sendMagicLink`; the landing auto-exchanges
78
+ `loginWithMagicToken` ONCE (StrictMode-proof ref guard — `ml:` tokens are
79
+ single-use) with progress/error UI.
80
+ - **Passkey**: `loginWithPasskey(username?, {remember})` behind `isPasskeySupported()`;
81
+ no username → discoverable-credential prompt. The ceremony is fully built;
82
+ REAL-authenticator verification is deferred (mock validates shape only).
83
+ The remember choice is explicit because MFA and fresh-auth passkey paths
84
+ must not promote a sessionStorage session into localStorage.
85
+
86
+ ## Token-landing URL shapes
87
+
88
+ django-mojo's `build_token_url` produces
89
+ `{WEBAPP_BASE_URL}{WEBAPP_AUTH_PATH}?flow=<flow>&token=<tok>` with `pr:`
90
+ (password_reset) / `ml:` (magic_login) token prefixes. Both landing shapes
91
+ work:
92
+
93
+ - **Real search** (`/?flow=password_reset&token=pr:…` — WEBAPP_AUTH_PATH
94
+ points at the portal root): `handleAuthTokenLanding()` at boot scrubs
95
+ flow/token from the real URL **synchronously, before any network call**
96
+ (the auth_code discipline) and hash-routes to the landing page.
97
+ - **Hash query** (`/#/auth?flow=…&token=…` — WEBAPP_AUTH_PATH points at
98
+ `/#/auth`): the `/auth` index route dispatches by token prefix, then flow.
99
+
100
+ The landing pages capture the token once at mount and scrub it from the
101
+ hash query before exchanging.
102
+
103
+ ## Fresh-auth (step-up) challenges
104
+
105
+ django-mojo's freshness gate (`account/services/fresh_auth.py`,
106
+ `FRESH_AUTH_WINDOW`, default off): sensitive operations answer **HTTP 440 /
107
+ `reauth_required`** when the JWT's `auth_time` is too old. A token refresh
108
+ can NEVER satisfy it — refreshes carry `auth_time` forward unchanged; only
109
+ a genuine re-login mints a new one. That is why 440 is not 401: it must not
110
+ trigger the refresh path.
111
+
112
+ Client surface (`portal-mojo/client`):
113
+
114
+ - `isReauthRequired(error)` — MojoError with status/`error_code` 440 or
115
+ message `reauth_required`.
116
+ - `setFreshAuthHandler(handler)` — the app registers ONE UI handler;
117
+ `FreshAuthHost` (mounted by `RequireAuth`) does this.
118
+ - `requestFreshAuth()` — single-flight prompt: a burst of gated calls
119
+ shares one modal. No handler → `console.warn` + resolve false.
120
+ - `withFreshAuth(fn)` — run fn; on a 440 prompt, then retry **once** after a
121
+ successful re-login; a dismissed prompt rejects with the ORIGINAL error.
122
+
123
+ The modal re-prompts credentials OVER the current screen — page state
124
+ survives. It pins the identity to the current session's email (step-up is
125
+ "prove you are still you"), offers password + passkey + the MFA step, and
126
+ keeps the session in its original storage (`sessionIsPersistent()` →
127
+ `remember`). Dev affordance: `__mojo.requestFreshAuth()` in the console
128
+ exercises the modal without a 440.
129
+
130
+ Call-site pattern:
131
+
132
+ ```ts
133
+ await withFreshAuth(() => save(changes)); // a 440 prompts + retries once
134
+ ```
135
+
136
+ `defineModel().useSave()` and `.useAction()` wrap their mutation functions in
137
+ this flow globally. Mutation variables are closed over unchanged and the
138
+ normal success/cache path runs only after the retry succeeds. `useDelete()`
139
+ and direct `mojoCall()` calls remain explicit opt-ins: their sensitivity and
140
+ retry safety are endpoint-specific.
141
+
142
+ Deployment note: a deployment enforcing `BOUNCER_REQUIRE_TOKEN` gates
143
+ `/api/login` behind the bouncer token, which in-app surfaces do not carry
144
+ (`LoginOptions.extra.bouncer_token` is the seam) — the modal will surface
145
+ the server's 403 verbatim there; hosted mode is the supported posture for
146
+ bouncer-enforcing deployments.
147
+
148
+ ## Password tools (`portal-mojo/ui`)
149
+
150
+ - `checkPasswordStrength(password)` → `{score, strength, feedback, details}`
151
+ — web-mojo MOJOUtils scoring, EXACT: length bands (+1/+3/+4), variety
152
+ (+1 lower, +1 upper, +1 digits, +2 special), one -1 common-pattern
153
+ penalty, common-password `max(0, score-3)`, thresholds <2/<4/<6/<8 →
154
+ very-weak/weak/fair/good/strong, same feedback strings. 22 headless
155
+ assertion groups verified the port (entropy classes, floors, praise
156
+ lines, edge inputs).
157
+ - `generatePassword(options?)` — same option surface and guarantees
158
+ (defaults incl. length 12, one char per included class, `customChars`
159
+ override skips the guarantee, `excludeAmbiguous` strips i/l · I/O/L ·
160
+ 0/1 · |, length < 4 throws, empty pool throws). ONE documented deviation:
161
+ crypto.getRandomValues + Fisher–Yates replaces web-mojo's Math.random and
162
+ biased sort-shuffle — a non-crypto RNG in a password generator is a trap,
163
+ not a feature.
164
+ - `<PasswordStrengthMeter password showFeedback? />` — 5-segment bar +
165
+ label + feedback, colored via tokens through `data-strength`
166
+ (bad/warn/info/ok), both themes free. Empty input renders an idle unlit
167
+ state instead of the scorer's non-empty-string complaint.
168
+
169
+ ## Mock affordances
170
+
171
+ Under the mock transport the pages show inline dev hints: password `mojo`
172
+ for any seeded user (e.g. `ian@mojoverify.com`); Maya (seeded user 2) requires
173
+ MFA; TOTP `123456`, recovery `mock-recovery`, and SMS `654321`; reset code
174
+ `123456`; token formats `pr:mock-<id>` / `ml:mock-<id>`. MFA tokens are
175
+ expiring and single-use. The mock follows the measured runtime behavior:
176
+ TOTP/recovery/SMS attempts burn the token before code validation, and
177
+ `sms/send` burns then re-issues it (older web docs claiming otherwise are
178
+ stale). Login's mock-only `__mock_mfa_ttl` can make expiry immediate.
179
+
180
+ Arm a real one-shot 440 path from the dev console with
181
+ `__mojo.armMockReauth('POST', '/api/user/1')`. Matching includes method and
182
+ exact path, runs after bearer authentication, and is consumed only by the
183
+ first authenticated match. This makes anonymous 401, unrelated requests,
184
+ the first matching 440, retry success, and later normal calls reproducible.
185
+
186
+ ## Deferred (explicitly)
187
+
188
+ - **OAuth redirect flows** — re-deferred with a code-level seam
189
+ (`TODO(C3-deferred)` in auth.ts documents the full verified wire:
190
+ `GET /api/auth/oauth/<provider>/begin` → `{auth_url, state}` → provider →
191
+ backend callback → 302 to the frontend with `?code=&state=` →
192
+ `POST …/complete`). Blocked on: server-side redirect-uri allowlisting,
193
+ deployment-specific providers (`GET /api/auth/config` → `login.methods`),
194
+ and no way to exercise the round-trip against the mock or without real
195
+ provider credentials. Hosted pages cover OAuth deployments today.
196
+ - **Passkey real-authenticator verification** — the ceremony is fully
197
+ built on auth.ts; verifying against a real authenticator + a live
198
+ django-mojo passkey registration is Ian's post-merge pass.
199
+
200
+ ## Pitfalls
201
+
202
+ - `auth/sms/send` BURNS the mfa_token it received and returns a new one —
203
+ verifying with the old token fails. MfaPanel handles this; any new UI
204
+ must too.
205
+ - Never route a 440 into the token-refresh path; the server chose 440
206
+ precisely so 401-refresh machinery ignores it.
207
+ - `ml:`/`pr:` tokens are single-use: exchange exactly once (StrictMode
208
+ double-effects will burn them — use a ref guard, as MagicPage does).
209
+ - The auth pages must stay OUTSIDE `RequireAuth` and OUTSIDE the App shell
210
+ (they are route siblings, not children).
211
+ - Keep the `?auth_code=` scrub-before-network behavior in auth.ts intact —
212
+ `handleAuthTokenLanding()` follows the same discipline for `?token=`.
package/docs/auth.md ADDED
@@ -0,0 +1,58 @@
1
+ # auth — session client + hosted-auth bridge
2
+
3
+ ```ts
4
+ import {
5
+ initAuth, login, logout, onAuth, useAuthSnapshot,
6
+ handleAuthCodeFromURL, hostedAuthUrl, redirectToHostedAuth,
7
+ sendMagicLink, loginWithMagicToken, handleMagicTokenFromURL,
8
+ forgotPassword, resetPasswordWithCode, resetPasswordWithToken,
9
+ loginWithPasskey, isPasskeySupported,
10
+ } from 'portal-mojo/client';
11
+ ```
12
+
13
+ ## Boot (once, before render)
14
+
15
+ ```ts
16
+ initAuth(); // installs the pre-request gate + Authorization header + refresh watcher
17
+ void handleAuthCodeFromURL(); // returning from the hosted /auth pages (no-op otherwise)
18
+ onAuth('login', () => queryClient.invalidateQueries()); // identity changed →
19
+ onAuth('logout', () => queryClient.invalidateQueries()); // every cached answer is suspect
20
+ ```
21
+
22
+ ## Session mechanics
23
+
24
+ - JWT pair (`access_token`/`refresh_token`); single-flight refresh (3
25
+ concurrent 401s → ONE refresh POST); pre-request gate throws a synthetic
26
+ `AuthRequiredError` instead of firing doomed requests.
27
+ - `useAuthSnapshot()` → `{authenticated, uid, email}` — live, cross-tab.
28
+ - `logout()` is client-side (django-mojo has no logout endpoint) and emits
29
+ `'logout'`.
30
+ - Events for `onAuth`: `'login' | 'logout' | 'refreshed' | 'refresh-failed'
31
+ | 'unauthorized'`.
32
+
33
+ ## Hosted auth pages (the django-mojo "bouncer" pages)
34
+
35
+ The backend serves themed login/register pages at `<origin>/auth` (behind
36
+ its bot-check interstitial). Flow:
37
+
38
+ 1. `redirectToHostedAuth()` — stashes the current hash route in
39
+ sessionStorage, then navigates to
40
+ `<origin>/auth?redirect=<HASH-FREE current url>`. Hash-free is
41
+ load-bearing: the page string-appends `?auth_code=<code>` to the
42
+ redirect, and a `#` in it would swallow the code.
43
+ 2. User signs in on the hosted page; it mints a single-use 60s handoff code
44
+ (`POST /api/auth/handoff`) and navigates back with `?auth_code=`.
45
+ 3. `handleAuthCodeFromURL()` (boot) exchanges it (`POST /api/auth/exchange`),
46
+ scrubs the code from the URL synchronously, restores the stashed route.
47
+ Handles a hash-embedded `#/route?auth_code=` landing too.
48
+
49
+ `hostedAuthUrl()` returns null under the mock transport — there is no page
50
+ to host; the UI should fall back to a dev affordance (`__mojo.login(...)`).
51
+
52
+ ## Direct flows (for in-app auth pages)
53
+
54
+ `login(username, password)` → `{kind: 'authenticated', user}` or an MFA challenge;
55
+ magic-link (`sendMagicLink` → `loginWithMagicToken`), password reset (code
56
+ and token variants), passkeys (`loginWithPasskey`; ceremony fully ported,
57
+ mock validates shape only). All reject with the server's message on
58
+ failure.
@@ -0,0 +1,122 @@
1
+ # Calendar engine + date fns
2
+
3
+ The picker core ported from web-mojo's dependency-free calendar
4
+ (`calendar/Calendar.js` + `utils/dateFns.js`): ONE engine covering
5
+ day/month/year precisions, single + range selection, drill-down zoom, and
6
+ 1–2 day panes. Popover, trigger inputs, and preset rails are separate
7
+ components built ON this — the engine renders inline and owns no
8
+ positioning.
9
+
10
+ ```ts
11
+ import { Calendar } from 'portal-mojo/ui';
12
+ import { dateFns } from 'portal-mojo/ui'; // namespace, like fmt
13
+ ```
14
+
15
+ ## Value shapes (the contract)
16
+
17
+ - Canonical strings at each precision: `YYYY-MM-DD` / `YYYY-MM` / `YYYY`.
18
+ Everything the engine emits and accepts (`value`, `startValue`, `min`,
19
+ `disabledDates`, callback payloads) speaks these.
20
+ - Parsed shapes are plain int objects — `{y}`, `{y,m}`, `{y,m,d}`
21
+ (`ParsedYear/ParsedYm/ParsedYmd`, union `ParsedDate`). `Date` objects
22
+ never leak out of the fns; all math is browser-local (DST-safe), never
23
+ UTC.
24
+
25
+ ## `<Calendar>` API
26
+
27
+ | Prop | Notes |
28
+ |---|---|
29
+ | `precision` | `'day' \| 'month' \| 'year'` (default `day`) — the grid that COMMITS. Unknown values warn + fall back to `day`. |
30
+ | `mode` | `'single' \| 'range'` (default `single`) |
31
+ | `months` | `1 \| 2` day-view panes (default 1; clamped) |
32
+ | `value` | single mode — controlled canonical string (or null) |
33
+ | `startValue` / `endValue` | range mode — controlled canonical strings |
34
+ | `min` / `max` | bounds, compared at the PANE precision being painted: with `min="2026-03-15"` the March-2026 month tile and 2026 year tile stay enabled; only cells wholly out of bounds disable |
35
+ | `disabledDates` | `YYYY-MM-DD[]`, day grids only |
36
+ | `firstDay` | 0=Sun … (default 1=Mon); reorders weekday header + grid offsets |
37
+ | `locale` | BCP-47 for month/weekday names (default `en-US`) |
38
+ | `year` / `month` | initial page override (otherwise the selection, else today) |
39
+ | `onSelect` | `{value, parsed}` — single-mode commit at precision |
40
+ | `onRangeSelect` | `{start, end}` — ordered (ends swap if picked backwards) |
41
+ | `onRangeStart` | `{anchor}` — first range click; nothing committed yet |
42
+ | `onRangeCancel` | Escape pressed with an anchor in progress |
43
+ | `onViewChange` | `{view}` — zoom changed (drill-down or header click) |
44
+ | `onNavigate` | `{delta}` — page changed (buttons or PageUp/PageDown) |
45
+
46
+ **Selection is controlled.** A commit fires the callback; the cell paints
47
+ selected only after the owner writes the prop back. Prop writes carry the
48
+ source's programmatic semantics: a new `value` re-pages the calendar to it;
49
+ a new `startValue`/`endValue` pair cancels any in-progress anchor
50
+ (`setRange`). View, page, anchor and hover are internal state — paging and
51
+ zooming never round-trip through the owner.
52
+
53
+ ## Behavior invariants (from source, keep them true)
54
+
55
+ - **Drill-down zoom:** header label zooms out day→month→year (a `<button>`;
56
+ inert at the year grid). A tile ABOVE precision drills down
57
+ (year→month→day); a tile AT precision commits. Reaching day view is only
58
+ possible when `precision='day'`, so range/selected tint on month/year
59
+ grids paints only at matching precision.
60
+ - **Range machine:** first commit places the anchor (`onRangeStart`),
61
+ second commits the ordered pair (`onRangeSelect`). The anchor SURVIVES
62
+ paging/zooming — commit a start, page forward, commit the end elsewhere.
63
+ Hover paints a live preview from anchor to cursor; preview cells use
64
+ `-in-preview` classes, DISTINCT from committed `-in-range`, with
65
+ `-anchor-start/-anchor-end/-anchor-solo` end caps (day grids add
66
+ directional chevrons; solo hides them).
67
+ - While an anchor is active the committed range is suppressed from painting
68
+ (source parity). **Deviation from source:** after Escape the committed
69
+ props paint again — web-mojo left the calendar blank until the next
70
+ commit (nothing consumed `range:cancel`). Corollary: do NOT write range
71
+ props from `onRangeStart` — a range prop write means `setRange` and kills
72
+ the anchor.
73
+ - **Keyboard:** every cell is a real `<button>` (focusable, Enter/Space
74
+ commit). With focus anywhere inside: PageUp/PageDown page, Escape cancels
75
+ an in-progress anchor. There is no roving arrow-key focus — that was not
76
+ in the source engine either.
77
+ - **Today** wears `-today` (red, day grids only). `disabledDates` are
78
+ unclickable and skipped by hover preview painting (the span still covers
79
+ them — `excludeDisabledFromRange` was accepted-but-unused in source and
80
+ is not carried).
81
+ - Blank leading cells are inert `<div>`s, not buttons.
82
+
83
+ ## dateFns (`dateFns.*`)
84
+
85
+ Pure, framework-free, `Date`-free shapes. The tuned edge cases are the
86
+ point — do not "simplify":
87
+
88
+ | Group | Functions |
89
+ |---|---|
90
+ | parse/format | `parseYmd/parseYm/parseYear` (+`parseByPrecision`), `formatYmd/formatYm/formatYear` (+`formatByPrecision`), `formatForDisplay(parsed, 'MMM D, YYYY')` |
91
+ | calendar math | `daysInMonth(y,m)` (1-indexed), `weekdayWithFirstDay(y,m,d,firstDay)`, `addMonths/addYears` (shape-preserving, day CLAMPS: Jan 31 +1mo → Feb 28/29) |
92
+ | compare | `compareYmd/Ym/Year/ByPrecision` (−1/0/1; cross-precision compares shared parts — how min/max pane-gating works), `isSameDay/Month/Year` |
93
+ | counts | `daysBetweenInclusive` (1 for same day), `monthsBetweenInclusive`, `yearsBetweenInclusive`, `unitsBetweenInclusive` |
94
+ | time | `parseTime` ("14:30", "2:30 pm", seconds/TZ tails dropped; strict 12h: hour 1–12 with am/pm, else null), `formatTime(t, '24h'\|'12h')`, `compareTime`, `addMinutes` (wraps 24h) |
95
+ | datetime | `parseDateTime` (ISO±offset / "date time IANA" / date-only→00:00), `formatDateTime` ("YYYY-MM-DD HH:MM[ TZ]"), `formatDateTimeForDisplay`, `ianaOffset(zone, refDate)` (DST-aware "+05:30", null on bad zone) |
96
+ | names/now | `monthNames(locale, 'short'\|'long')`, `weekdayNames(locale, firstDay, len)` (Intl-backed, cached, static-English fast path), `today()`, `_setFrozenToday(p)` test hook |
97
+
98
+ Parse fns return `null` on invalid input, never throw; format fns return
99
+ `''` for null. `parseYmd` range-checks day 1–31 but not month length
100
+ (source behavior; the engine never emits an out-of-month day).
101
+
102
+ ## Styling
103
+
104
+ `apps/portal/src/theme/calendar.css` — engine anatomy only
105
+ (`mojo-calendar-*`: pane/head/nav/grid/weekday/cell + state classes), all
106
+ colors via theme tokens; component tokens `--mojo-cal-anchor-fill`,
107
+ `--mojo-cal-range-fill`, `--mojo-cal-range-fill-hover` (accent tints:
108
+ 10%/18% light, 18%/30% dark). The calendar fills its container's width —
109
+ give it a sized wrapper (~300px per pane; the popover shells do this for
110
+ you later).
111
+
112
+ ## Pitfalls
113
+
114
+ - Don't parse engine output with `new Date('YYYY-MM-DD')` — that's UTC
115
+ midnight and shifts a day in western zones. Use `dateFns.parseYmd`.
116
+ - `min`/`max` are compared at pane precision — a `min` mid-month still
117
+ allows that month's tile; check day-level rules at day level.
118
+ - Escape/PageUp/PageDown require focus inside the calendar (click one cell
119
+ first); the demo copy says so too.
120
+ - The wire's daterange triple (`dr_field/dr_start/dr_end`) and epoch-second
121
+ datetimes are PICKER-shell concerns — the engine only speaks canonical
122
+ local strings.