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,284 @@
1
+ # Admin: devices, login locations & GeoIP
2
+
3
+ ```ts
4
+ import {
5
+ // sections (registered in ADMIN_SECTIONS — no app wiring needed)
6
+ DEVICE_INTEL_ADMIN_SECTION, GEOIP_ADMIN_SECTION,
7
+ // pages
8
+ UserDevicesPage, LoginLocationsPage, GeoIpCachePage,
9
+ // the map binding
10
+ LoginLocationMap,
11
+ // KISS detail modals (#1425)
12
+ showUserDeviceDetail, showUserDeviceDetailByDuid,
13
+ showUserDeviceLocationDetail, showLoginEventDetail,
14
+ showGeoIpDossier, showGeoIpDossierForAddress,
15
+ // models — ONE defineModel per endpoint, package-wide
16
+ UserDeviceModel, UserDeviceLocationModel, LoginEventModel, GeoLocatedIPModel,
17
+ // permission clauses
18
+ USER_DEVICE_VIEW_PERMS, DEVICE_LOCATION_VIEW_PERMS,
19
+ LOGIN_EVENT_VIEW_PERMS, LOGIN_SUMMARY_PERMS,
20
+ GEOIP_VIEW_PERMS, GEOIP_MANAGE_PERMS,
21
+ // enforcement + presentation helpers
22
+ blockActive, whitelistActive, blockExpired, whitelistExpired,
23
+ threatTone, riskScoreOf, countryFlag, THREAT_LEVEL_ORDER,
24
+ loginRiskTone, LOGIN_RISK_LEGEND,
25
+ deviceIcon, browserLabel, osLabel, deviceLabel, daysActive, presenceOf,
26
+ // data hooks
27
+ useLoginLocationSummary, useLoginLocationList,
28
+ useUserDeviceSessions, useDeviceSiblings,
29
+ useGeoIpByAddress, useGeoIpRawRecord, lookupGeoIp,
30
+ geoIpSafeExporter, sanitizeGeoIpRow,
31
+ } from 'portal-mojo/admin';
32
+ ```
33
+
34
+ Account-security triage: who logged in from a new country, what is this IP,
35
+ and is this device shared across accounts. Demo: showcase → Admin →
36
+ **Devices, logins & GeoIP**. Styles:
37
+ `apps/{portal,showcase}/src/theme/admin-devices.css` (byte-identical).
38
+
39
+ ## Sections
40
+
41
+ Two siblings under the **Security** navigation group, beside Bouncer — so the
42
+ PRE-auth (`BouncerDevice.muid`) and POST-auth (`UserDevice.muid`) views of the
43
+ same physical browser sit next to each other.
44
+
45
+ | Section | id / basePath | Routes |
46
+ |---|---|---|
47
+ | Devices & Logins | `device-intel` / `security/devices` | `user-devices` · `login-locations` |
48
+ | IP Intelligence | `geoip` / `security/geoip` | `cache` |
49
+
50
+ All three surfaces are fleet-wide and **groupless** — `GeoLocatedIP` has no
51
+ group FK at all, and the device/login lists are only global with a `sys.`
52
+ grant. The *user-scoped* views stay inside `UserDetail`'s Devices and Logins
53
+ sections, which this module extends rather than forks.
54
+
55
+ `#1287`'s network-security pages (Blocked IPs, firewall log, IP sets,
56
+ geofencing) live in their OWN section at `security/network`; this module
57
+ reserves no space for them and exposes no extension seam. It does export
58
+ everything they import.
59
+
60
+ ## Permissions
61
+
62
+ Every clause is `sys.`-pinned, so an active-member grant can never satisfy it.
63
+ A permission-disabled query passes `enabled: false` — a denied surface issues
64
+ **no** request.
65
+
66
+ | Constant | Value | Backend source |
67
+ |---|---|---|
68
+ | `USER_DEVICE_VIEW_PERMS` | `sys.manage_users`, `sys.users` | `UserDevice.VIEW_PERMS` minus `owner` |
69
+ | `DEVICE_LOCATION_VIEW_PERMS` | `sys.manage_users`, `sys.users` | URL decorator ∩ `UserDeviceLocation.VIEW_PERMS` |
70
+ | `LOGIN_EVENT_VIEW_PERMS` | `sys.manage_users`, `sys.security`, `sys.users` | `UserLoginEvent.VIEW_PERMS` minus `owner` |
71
+ | `LOGIN_SUMMARY_PERMS` | `sys.manage_users`, `sys.security`, `sys.users` | `@requires_global_perms` on `/summary` + `/user` |
72
+ | `GEOIP_VIEW_PERMS` | `sys.manage_users`, `sys.view_security`, `sys.manage_security`, `sys.security`, `sys.users` | `GeoLocatedIP.VIEW_PERMS` |
73
+ | `GEOIP_MANAGE_PERMS` | `sys.manage_users`, `sys.manage_security`, `sys.security` | `GeoLocatedIP.SAVE_PERMS` |
74
+
75
+ **`sys.manage_devices` is deliberately absent everywhere.** It passes the
76
+ `@requires_global_perms` decorator on `/user/device/location` and
77
+ `/user/device/lookup`, then fails the *model's* `VIEW_PERMS`
78
+ (`manage_users | users`). Alone it opens nothing, so offering it as an
79
+ affordance would promise access the server refuses.
80
+
81
+ `UserDevice` and `UserLoginEvent` both carry `owner` in `VIEW_PERMS`: without
82
+ a global grant those endpoints return only the caller's own rows. The admin
83
+ pages are gated so that is never what an operator sees; the mock reproduces it
84
+ so the verifier can assert it.
85
+
86
+ ## Endpoints
87
+
88
+ | Endpoint | Gate | Notes |
89
+ |---|---|---|
90
+ | `GET /api/user/device` `(+/<pk>)` | model `VIEW_PERMS` incl. `owner` | graphs `default` · `basic` · `locations` · `sessions` |
91
+ | `GET /api/user/device/lookup?duid=` | `@requires_global_perms(manage_users, manage_devices, users)` | cross-tenant; `on_rest_get` does not re-gate |
92
+ | `GET /api/user/device/location` `(+/<pk>)` | URL decorator **and** model `VIEW_PERMS` | |
93
+ | `GET /api/account/logins` `(+/<pk>)` | model `VIEW_PERMS` incl. `owner` | `CAN_CREATE/UPDATE/DELETE` all False |
94
+ | `GET /api/account/logins/summary` | `@requires_global_perms(manage_users, security, users)` | country aggregation |
95
+ | `GET /api/account/logins/user?user_id=` | same, `@requires_params('user_id')` | per-user aggregation |
96
+ | `GET /api/system/geoip` `(+/<pk>)` | `GeoLocatedIP.VIEW_PERMS` / `SAVE_PERMS` | graphs `default` · `basic` · `detailed` · `federation` |
97
+ | `GET /api/system/geoip/lookup?ip=` | `@requires_auth()` only, rate-limited 30/ip | see the graph downgrade below |
98
+
99
+ **No model here declares `CAN_DELETE`,** and `rest.py` defaults it to False.
100
+ There is no delete path for a device, a device location, a login event or a
101
+ GeoIP record — the source's "Forget device", "Delete record" and "Delete
102
+ Location Record" items could never have worked.
103
+
104
+ ## Model field shapes
105
+
106
+ `UserDeviceRow` is the `default` graph. It has **no** `is_trusted` and **no**
107
+ `is_blocked` — web-mojo's active toggle, "Mark trusted/untrusted" kebab item,
108
+ Trusted/Blocked chips and trust timeline row all wrote a field the model never
109
+ had.
110
+
111
+ `UserDeviceSessionsRow` is the `sessions` graph, keyed separately in the query
112
+ cache (`[endpoint, 'one', id, {graph:'sessions'}]`) so a sparse `default` row
113
+ cannot satisfy it. Its three `extra` properties are real:
114
+
115
+ - `bouncer_device` — the pre-auth `BouncerDevice` for this `muid`.
116
+ - `active_sessions[]` — 24h of `BouncerSignal` grouped by `msid`, each with a
117
+ per-`mtab` breakdown.
118
+ - `recent_locations[]` — the last ten `UserDeviceLocation` rows, flattened.
119
+
120
+ `LoginEventRow` has **no** `event_type`. `GeoLocatedIPRow` has **no**
121
+ `reverse_dns`, **no** `ip_version` and **no** `metadata`, and its `provider`
122
+ and `data` fields are excluded from the `default` graph.
123
+
124
+ ## `LoginLocationMap`
125
+
126
+ ```tsx
127
+ <LoginLocationMap
128
+ userId={user.id} // scoped → /account/logins/user; omit → /summary
129
+ height={360}
130
+ drStart="2026-07-07" drEnd="2026-08-06" // YYYY-MM-DD, see the two wires
131
+ defaultMode="summary" // 'summary' | 'list'
132
+ showModeToggle
133
+ enabled={can} // false ⇒ no request at all
134
+ onCountrySelect={(cc) => …}
135
+ onOpenLogin={(id) => …}
136
+ onOpenUser={(id) => …} // list mode, global scope only
137
+ />
138
+ ```
139
+
140
+ Rendering belongs entirely to [`WorldMap`](worldmap.md) (#1426). One import
141
+ line — `from 'portal-mojo/charts'` — is the whole coupling surface, so
142
+ reconciling with the map component touches exactly one file. No projection,
143
+ SVG, marker geometry or colour ramp lives here.
144
+
145
+ - **Summary mode** — one sized marker per country, or per region after a
146
+ drill-down. Position comes from `COUNTRY_CENTROIDS` first and the server's
147
+ `Avg(latitude)`/`Avg(longitude)` only as a fallback: for a country spanning
148
+ the antimeridian that average lands in the wrong ocean. Region rows have no
149
+ centroid and always use the average.
150
+ - **List mode** — one dot per login, bounded at 500, coloured by
151
+ `loginRiskTone(row)`.
152
+ - **Drill-down** is *double-click*, on country markers, in summary mode only.
153
+ A single click fires `onCountrySelect` after WorldMap's 250 ms
154
+ double-click-cancellation window — that quarter-second delay is by design.
155
+ - **Coastlines come from `useWorldLand()`.** The checked-in
156
+ public-domain Natural Earth 110m outlines, no CDN and no tile server (see
157
+ `worldmap.md` → "The `land` seam"). The hook loads them in a separate chunk,
158
+ so pages without a map never carry the geometry. Rendering
159
+ correctly *without* it stays a hard requirement: drop the prop and the ocean +
160
+ graticule fallback still carries the markers, legend, tooltips, drill bar and
161
+ status line.
162
+
163
+ ### `loginRiskTone` — and the helper that was deleted
164
+
165
+ `loginRiskTone(row)` reads fields the wire actually sends: `is_new_country` →
166
+ `bad`, `is_new_region` → `warn`, otherwise `ok`.
167
+
168
+ It replaced `loginEventTone(eventType)`, which `portal-mojo/charts` briefly
169
+ exported after being ported from web-mojo. That helper mapped a login
170
+ event-type vocabulary (`success_login`, `failed_login`, `suspicious`, …) — and
171
+ **`UserLoginEvent` has never had an `event_type` field**, so a real row returned
172
+ `'mute'` every time. That is precisely the web-mojo bug where every dot on the
173
+ login map rendered grey.
174
+
175
+ It was **removed from the package 2026-08-06** rather than kept beside this one:
176
+ a plausible-looking name that silently greys out every marker is a trap, and
177
+ there is no series in django-mojo that carries an event type for it to be
178
+ correct about. `verify-worldmap.mjs` asserts both old names stay gone.
179
+
180
+ ## The two different date wires
181
+
182
+ They are not interchangeable, and mixing them silently returns everything.
183
+
184
+ | Endpoint | Wire | Format |
185
+ |---|---|---|
186
+ | `/api/account/logins` (list) | `dr_field` + `dr_start` + `dr_end` | always name `dr_field` explicitly |
187
+ | `/summary` and `/user` | `dr_start` + `dr_end` only | `YYYY-MM-DD`, parsed by `dates.parse` |
188
+
189
+ ## Backend traps encoded here
190
+
191
+ - **`region=false` is truthy.** `login_event.py` reads
192
+ `request.DATA.get('region')` with no string coercion, so the *string*
193
+ `"false"` drills. `region=1` is sent only while drilling, never `region=false`.
194
+ - **`user_id` must parse as an int** on `/logins/user`, or the endpoint
195
+ answers `{status: false, code: 400}`.
196
+ - **`country_code` must match `^[A-Z]{2,3}$`** uppercase or it is silently
197
+ ignored — an invalid code falls back to the country branch rather than erroring.
198
+ - **The region drill caps at 500 rows** (`MAX_REGION_RESULTS`).
199
+ - **`/summary` excludes null and empty `country_code`,** and private-range
200
+ logins carry no coordinates — so those logins are invisible on the map *by
201
+ construction*. The status line says so; the Logins tab is where they appear.
202
+ - **The embedded `user_device` graph is `basic` and carries no `id`.**
203
+ Cross-links from a location or login row resolve the device by `duid`
204
+ (`showUserDeviceDetailByDuid`).
205
+ - **`_param_is_true`**: query params arrive verbatim, so `auto_refresh=false`
206
+ would be truthy read naively. `lookupGeoIp` sends `1`/`0`.
207
+
208
+ ## Enforcement expiry is STATE
209
+
210
+ `GeoLocatedIP` exposes `block_active` and `whitelist_active` as Python
211
+ properties, but they are serialized only on the `basic` graph — the `default`
212
+ graph carries the raw booleans. `blockActive(row)` / `whitelistActive(row)`
213
+ mirror the model exactly:
214
+
215
+ - a whitelist **wins** over a block;
216
+ - a `blocked_until` in the past means **not blocked**;
217
+ - a `whitelisted_until` in the past means **not whitelisted**.
218
+
219
+ `blockExpired` / `whitelistExpired` surface the lapsed-but-still-flagged state
220
+ as its own badge. web-mojo rendered raw `is_blocked`, so an expired block
221
+ still read "Blocked" and a whitelisted-but-flagged IP read blocked when it was
222
+ not.
223
+
224
+ `block()` on the backend always escalates `threat_level` to at least `high`
225
+ and increments `block_count`; whitelisting clears an active block. The UI
226
+ re-reads the row from the action response rather than optimistically toggling,
227
+ because the backend block is idempotent and race-safe.
228
+
229
+ ## Scrubbing rules
230
+
231
+ 1. **The raw provider blob never enters the Query cache.** `sanitizeGeoIpRow`
232
+ deletes `data` before any list/detail/save write and runs the shared
233
+ `sanitizeSecurityValue` over the rest.
234
+ 2. **The only reader of `graph=detailed`** is `useGeoIpRawRecord` — an
235
+ explicit, `GEOIP_MANAGE_PERMS`-gated, `gcTime: 0` / `staleTime: 0` fetch
236
+ whose payload is sanitized and length-bounded before it is returned. It is
237
+ behind a "Show raw provider record" reveal that is collapsed by default.
238
+ 3. **Exports go through `geoIpSafeExporter`** (`createSafeExporter`) with a
239
+ declared field projection, so a crafted `graph` param cannot smuggle
240
+ `data` or `provider` into a CSV.
241
+ 4. **Inspecting an IP costs no provider call.** `showGeoIpDossierForAddress`
242
+ resolves through `?ip_address=` on the cache list — read-only, unmetered.
243
+ `/system/geoip/lookup` CREATES and REFRESHES records and is rate-limited
244
+ 30/ip, so it runs only on the explicit "Look up IP" action; a 429 surfaces
245
+ as a toast, never a silent no-op.
246
+ 5. **`/geoip/lookup` downgrades the graph, not the access.** The endpoint is
247
+ open to any authenticated identity (that is how a downstream instance uses
248
+ this one as its GeoIP provider), but a caller without `VIEW_PERMS` is
249
+ served the `federation` graph whatever it asked for — no enforcement state,
250
+ no raw blob.
251
+
252
+ ## Absent by design
253
+
254
+ Do not add these back; each was verified against the backend.
255
+
256
+ - The **trust toggle** and "Mark trusted/untrusted" — `is_trusted` does not exist.
257
+ - **"Forget device"**, "Delete GeoIP record", "Delete location record" — no
258
+ `CAN_DELETE` anywhere.
259
+ - Any **write** to a login event — `CAN_CREATE/UPDATE/DELETE` are all False.
260
+ - The six phantom KPI fields (`event_count`, `incident_count`,
261
+ `login_attempts`, `login_count`, `session_count`, `location_count`), which
262
+ existed on no model and rendered "—" forever. They are replaced with
263
+ `size: 0` count queries against endpoints that do exist.
264
+ - The `reverse_dns` / `ip_version` Network rows and the `provider` row on the
265
+ default graph — all three were permanently blank.
266
+ - A `user` **select** filter on either table: neither endpoint can enumerate
267
+ the directory, so the filter is the FK id.
268
+ - A search placeholder promising user or IP search on `/api/user/device` —
269
+ that model declares no `SEARCH_FIELDS` at all.
270
+
271
+ ## Verification
272
+
273
+ ```bash
274
+ node scripts/verify-admin-devices.mjs # or: npm run verify:admin-devices
275
+ ```
276
+
277
+ Headless assertions over the six permission clauses (`sys.`-pinning,
278
+ member-grant rejection, `manage_devices` absence), route generation for both
279
+ mounts, expiry semantics, the risk tone, the scrubber and the export
280
+ projection, one-`defineModel`-per-endpoint package-wide, the absent-by-design
281
+ list, theme byte-identity and tokens-only CSS, and the full mock wire:
282
+ owner scoping, anonymous denial, all four GeoIP graphs, the `/lookup` graph
283
+ downgrade, every POST_SAVE_ACTION, the verb refusals, the aggregation shape,
284
+ the region drill, the 500 cap, the `user_id` 400 and the `region=false` trap.
@@ -0,0 +1,122 @@
1
+ # Admin DNS certificates and ACME lifecycle
2
+
3
+ Import from `portal-mojo/admin`:
4
+
5
+ ```tsx
6
+ import {
7
+ CertificatesPage, CertificateDetail, CertificateRequestDialog,
8
+ DomainCertificatesSection, CertificateLifecyclePoller,
9
+ CertificateModel, deriveCertificateRenewalHealth,
10
+ deriveCertificateReadiness,
11
+ } from 'portal-mojo/admin';
12
+ ```
13
+
14
+ ## Route, gates, and presentation
15
+
16
+ `DNS_ADMIN_SECTION` contributes `dns/certificates` between DNS Records and
17
+ Provider Credentials. Like the rest of Admin it is global and no-group:
18
+ `sys.view_dns | sys.manage_dns | sys.security` opens the route and
19
+ `sys.manage_dns | sys.security` offers request/revoke. Tables own search,
20
+ status/domain filters, paging, and sort in the URL.
21
+
22
+ Row inspection uses `modal.detail`; requesting and confirming revocation use
23
+ small native KISS modals. `DomainDetail` composes the bounded
24
+ `DomainCertificatesSection` rather than adding a child route.
25
+
26
+ Certificate detail and revoke have an extra backend rule: when the hydrated
27
+ owning Domain has `group=null`, only a literal interactive superuser may open
28
+ or revoke it. A global `manage_dns` grant is not enough. The client hydrates
29
+ the Domain before fetching certificate detail, checks `me.is_superuser ===
30
+ true`, and gives a generic unavailable answer on denial so it does not become
31
+ an additional house-inventory oracle. The server remains authoritative.
32
+
33
+ ## Wire contract and cache boundary
34
+
35
+ ```text
36
+ GET /api/dnsman/certificate?graph=default
37
+ GET /api/dnsman/certificate/<id>
38
+ POST /api/dnsman/certificate/request {domain, names?}
39
+ POST /api/dnsman/certificate/revoke {certificate}
40
+ GET /api/dnsman/delegation?domain=<id>
41
+ ```
42
+
43
+ Only `graph=default` is accepted. Certificate export/download params are
44
+ rejected, not silently forwarded. The graph carries lifecycle metadata and a
45
+ basic Domain relation; it carries no `cert_pem`, `chain_pem`, private key,
46
+ ACME order URL, secret container, or arbitrary metadata. There is deliberately
47
+ no client helper or mock handler for `certificate/material/<id>`.
48
+
49
+ All list/detail rows pass through `sanitizeCertificateRow` in the Query
50
+ function, before TanStack Query stores them. It normalizes lifecycle status,
51
+ lowercase/trailing-dot-free SANs, finite timestamps/counts, a small Domain
52
+ projection, and a bounded/redacted `last_error`. Unknown/equivalent private
53
+ containers drop by construction. Delegation payloads retain only the backend's
54
+ public status shape; credential readiness uses the already-sanitized masked
55
+ credential model.
56
+
57
+ Request and revoke are imperative, single-flight, and non-retrying so an
58
+ ambiguous transport cannot duplicate an external CA operation. They do not use
59
+ MutationCache. A `finally` reconciliation invalidates/refetches active
60
+ certificate queries after success or failure. If authoritative refresh also
61
+ fails, repeat is blocked by an explicit “may have persisted; refresh first”
62
+ error.
63
+
64
+ ## Names and readiness
65
+
66
+ The request dialog normalizes names to lowercase ASCII without trailing dots,
67
+ deduplicates them, permits a wildcard only as the leading `*.`, and requires
68
+ every SAN to be the selected zone or a descendant. Empty names intentionally
69
+ omit `names`, letting the backend request the apex plus wildcard.
70
+
71
+ Readiness is backend-authoritative:
72
+
73
+ - ACME must be configured and the Domain active.
74
+ - Route 53 uses direct DNS-01.
75
+ - GoDaddy direct DNS-01 also needs its active, verified masked credential.
76
+ - `provider="mojo"` needs a verified delegated-ACME row.
77
+ - Once a delegation has `verified_at`, it is sticky. A later `broken` row
78
+ blocks issuance; the UI never claims direct-provider fallback.
79
+
80
+ `config.acme.staging` describes the deployment's current directory only. The
81
+ certificate model stores no historical staging provenance, so existing rows
82
+ are never labeled trusted or staging from that flag. New staging requests get
83
+ an explicit “not publicly trusted” warning.
84
+
85
+ ## Renewal and polling
86
+
87
+ `renew_after` is the authoritative renewal trigger. `days_remaining` is only
88
+ display. `CertificateLifecyclePoller` owns one timer per mounted certificate
89
+ surface, at 10 seconds and at most 36 ticks:
90
+
91
+ - new issuance follows `pending | issuing` to active/failed/revoked;
92
+ - a due active renewal records attempts/validity/error baseline, waits through
93
+ the initial active state and optional `issuing`, then stops on an observable
94
+ success or failure;
95
+ - an already-active row with `last_error` is a terminal renewal error and does
96
+ not start ordinary polling;
97
+ - timeout, unmount, and terminal state clear the timer.
98
+
99
+ This avoids one interval per row and avoids an endless poll on the backend's
100
+ valid “renewal failed but still-valid certificate remains active” behavior.
101
+
102
+ ## Mock, themes, and verification
103
+
104
+ The central mock contains pending, issuing, healthy active, due renewal,
105
+ active renewal-error, failed, revoked, house, verified delegation, broken
106
+ sticky delegation, staging, unconfigured, and malformed-response evidence.
107
+ Its only custody fixture is a private `material_present` boolean; no PEM is
108
+ stored or serialized.
109
+
110
+ Portal and Showcase both import `theme/admin-dns-certificates.css`, which uses
111
+ semantic tokens only. The executable demo is **Develop → Components → DNS
112
+ certificates & ACME** with manager, viewer, platform-house, and unconfigured
113
+ legs. Run:
114
+
115
+ ```bash
116
+ npm run verify:admin-dns-certificates
117
+ npm run verify:admin-dns
118
+ npm run verify:admin-dns-records
119
+ ```
120
+
121
+ Live verification is read-only. Do not request or revoke a real certificate
122
+ without separate operator authorization.
@@ -0,0 +1,65 @@
1
+ # DNS domains and live records
2
+
3
+ Import from `portal-mojo/admin`:
4
+
5
+ ```tsx
6
+ import {
7
+ DNS_ADMIN_SECTION, DomainsPage, DnsRecordsPage, DnsRecordsPanel,
8
+ DnsRecordEditor, DNS_VIEW_PERMISSIONS, DNS_MANAGE_PERMISSIONS,
9
+ registerDnsAdminIntegration, registerDnsDomainLinks,
10
+ } from 'portal-mojo/admin';
11
+ ```
12
+
13
+ ## Routes and permissions
14
+
15
+ The one canonical DNS section is `id: "dns"`, `basePath: "dns"`, in Infrastructure. Its routes are ordered `domains`, `records`, `certificates`, `purchases`, `registrant`, `credentials`. The read surfaces accept the global, system-pinned any-of view clause `sys.view_dns | sys.manage_dns | sys.security`; registrant and record mutation controls use `sys.manage_dns | sys.security`. Group/member grants never open Admin.
16
+
17
+ Domains uses the server-backed `DomainModel` list and `modal.detail`. Detail composes Overview, DNS Records, and the bounded Certificates section; Records is the same `DnsRecordsPanel` used by the standalone page. That page owns only `domain` and `record_type` in the URL. It may select the first active domain only when `domain` is absent. Invalid, denied, missing, and inactive deep links are preserved and shown as errors.
18
+
19
+ ## Wire contract
20
+
21
+ Domain list/default reads are standard model envelopes. List asks only for paging, search, sort, provider, status, group and the `list` graph. Live records are deliberately not a model:
22
+
23
+ ```text
24
+ GET /api/dnsman/dns?domain=8201
25
+ {status:true,data:{domain:"acme.example",provider:"route53",
26
+ records:[{type:"A",name:"acme.example",record_values:["192.0.2.10"],ttl:300}]}}
27
+ ```
28
+
29
+ Records have no database id, paging, search, or graph. The parser fails closed on malformed payloads or an invented `id`. Local identity is `TYPE|normalized-fqdn`.
30
+
31
+ Upsert replaces the complete ordered set and sends exactly
32
+ `{domain,type,name,record_values,ttl}`. Whole-set deletion sends only
33
+ `{domain,type,name}`. Removing some values is an upsert of all survivors,
34
+ never a partial delete.
35
+
36
+ Both write endpoints answer at the top level as
37
+ `{status:true,change_id:string|null,provider:string}`. Route 53 supplies a
38
+ change id; GoDaddy may return `null`. There is no nested `data` write payload.
39
+
40
+ ## Editor invariants
41
+
42
+ A, AAAA, CNAME, TXT, MX, SRV, CAA, and delegated non-apex NS use controlled structured rows, intersected with `allowed_record_types`. Unknown types remain readable and have no editor. Existing type/name are immutable.
43
+
44
+ Paste and blur may correct invisible characters, NBSP, curly quotes, URL/path/trailing dots, hostname case, bracketed IPs, and surrounding TXT/CAA quotes. Every correction records field, before, after, and message. Existing TXT values, including meaningful leading, trailing, and repeated whitespace, parse, format, edit, and submit losslessly unless the operator explicitly triggers and acknowledges a paste/blur correction. Submit never trims, dequotes, lowercases, normalizes, or deduplicates unacknowledged state. Blank/duplicate values, incomplete structured rows, invalid IPv4/full IPv6, hostname/range/TTL errors, out-of-zone or malformed names, invalid wildcards, apex NS/SOA/CNAME, and CNAME coexistence fail before the request. Server refusal remains authoritative.
45
+
46
+ Confirmation shows identity, TTL, unchanged/removed/added values, and contextual warnings from the editor opening snapshot. After confirmation the coordinator performs an uncached read, compares the exact set and every same-owner record (including CNAME coexistence), and writes immediately only when unchanged. A `finally` read reconciles cache after success, rejection, or an ambiguous response, and the zone query is invalidated unconditionally even when that read fails so stale cache is never authoritative. The draft and original error survive. This is best-effort preflight: no ETag/CAS exists, so a narrow GET-to-POST race remains.
47
+
48
+ ## Provider behavior
49
+
50
+ - Route 53 supports complete-set upsert and separately confirmed whole-set delete.
51
+ - GoDaddy manages DNS here; registrar operations stay in its account. The server clamps refreshed TTLs to 600 and cannot express whole-set deletion. Exact `_acme-challenge.* TXT ["retired"]` rows are spent/inert only for GoDaddy.
52
+ - `provider="mojo"` is certificate-only and fails closed for general DNS.
53
+ - Pending/registering domains, unknown providers, and missing/inactive/unverified credentials render explicit blocked states.
54
+
55
+ ## Composition seams
56
+
57
+ `dns-integration.ts` is dependency-free and HMR-safe. Partial registrations merge: production supplies exact-name resolution and `dns/records?domain=<id>`; the central mock supplies `applyManagedDnsRecords` against its sole DNS store. Absence is `null`, so permission is never mistaken for feature availability.
58
+
59
+ `registerDnsDomainLinks` is the reverse registry for certificate, WHOIS, or purchase children. Stable keys replace on HMR. Each entry owns label/icon, mount-relative route, optional system permission, capability/domain predicate, and optional order. Built-in related links are deterministically ordered Domains, DNS Records, Certificates, Provider Credentials.
60
+
61
+ ## Mock and verification
62
+
63
+ The central mock is the only DNS state owner and mirrors id-less envelopes, global-versus-tenant authorization, active/provider/credential gates, complete replacement, Route 53 delete, GoDaddy floor/refusal/spent placeholders, Mojo, unknown records, and structured types. Mutation verification is mock-only. Live verification may read a zone and open/cancel an editor, but must never POST or DELETE DNS records.
64
+
65
+ Both Portal and Showcase import `theme/admin-dns.css`; all colors use theme tokens. Run `npm run verify:admin-dns-records` with the orchestration-owned typecheck/build/browser pass.
@@ -0,0 +1,33 @@
1
+ # Admin DNS registrar and registrant contact
2
+
3
+ Import `DomainPurchasesPage`, `DomainPurchaseWizard`, `showDomainOnboarding`, and `RegistrantContactPage` from `portal-mojo/admin`.
4
+
5
+ The global, no-active-group DNS workspace mounts `purchases` and `registrant` between Certificates and Provider Credentials. Purchase-ledger viewing uses the global DNS view clause. Search is read-only; quote, purchase, adoption, and registrant writes require DNS manage authority. House contact and manual House adoption additionally require the literal interactive `is_superuser` identity.
6
+
7
+ ## Search, quote, and purchase
8
+
9
+ The wizard asks for one explicit group through bounded `/api/dnsman/credential/group-choice` search. It never reads `/api/group`, active-group state, or free-form group ids. TLDs are operator-typed and bounded by `config.search_batch_limit`; the client has no invented supported-TLD catalog. Search availability stays tri-state: `true`, `false`, or `null` (registry did not answer). Only literal `true` advances.
10
+
11
+ `POST /registrar/quote` returns `{purchase,name,price,currency,years,token,expires,privacy_supported}`. The raw token lives only in an immutable ref in the mounted wizard. It is never state, URL data, Query/Mutation cache data, mock request history, or durable mock data. The exact tuple binds purchase id, token, group, normalized domain, years, decimal price, currency, and expiry. Any upstream change or expiry clears it.
12
+
13
+ The operator types the exact normalized domain before the single-flight, nonretry `POST /registrar/purchase`. The token is removed from the ref before the request. Success and every exception reconcile through an imperative `GET /purchase/<id>?graph=default`; one non-overlapping bounded-backoff loop follows `submitted` to a durable terminal row. Timeout hands the operator to the ledger.
14
+
15
+ After purchase is attempted, the wizard cannot retry or requote. A failed, ambiguous, or timed-out result always says that money may have moved and directs the operator to the AWS Route 53 Domains operation console and durable ledger first.
16
+
17
+ ## Purchase ledger
18
+
19
+ `DomainPurchaseModel` lists only `graph=basic`: id, domain, status, price, and currency. Export/download and alternate graphs are rejected. Clicking a row performs an imperative default-graph read into a KISS modal; the detail response and redacted error are modal-local and never Query-cached.
20
+
21
+ ## Registrant PII
22
+
23
+ The contact page starts with no scope and no contact. Operators explicitly choose one authorized group, or literal superusers may choose House. Reads and writes are imperative; generation guards discard late responses after scope changes/unmount, and local contact state is cleared immediately.
24
+
25
+ An inherited group response renders a blank form and only the backend-authored boolean status; inherited values are never disclosed. Normal editable values may seed from that scope's database row or the House settings-file response. `Fax` and opaque `ExtraParams` are preserved only when the original response is a direct database row for the exact same scope. They are never carried across scope, inheritance, or deployment-file override boundaries.
26
+
27
+ ## House adoption
28
+
29
+ House adoption is a separate, literal-superuser-only menu option. The operator types one exact existing name and submits `/registrar/adopt` without `group`. The UI never calls `/registrar/discover`, auto-discovers account domains, or silently assigns the adopted asset to a tenant.
30
+
31
+ ## Themes, showcase, and verification
32
+
33
+ Both themes import the same token-only `admin-dns-registrar.css`. The showcase has manager-ready, missing-contact, viewer-ledger, and platform-House legs. `npm run verify:admin-dns-registrar` is mock-only and performs no real registrar quote or purchase.
@@ -0,0 +1,80 @@
1
+ # Admin DNS
2
+
3
+ Import from `portal-mojo/admin`.
4
+
5
+ `DNS_ADMIN_SECTION` mounts Domains, DNS Records, Certificates, Domain Purchases,
6
+ Registrant Contact, and Provider Credentials under `dns/` (or `system/dns/` embedded). Its global view gate is the ANY-of
7
+ clause `sys.view_dns | sys.manage_dns | sys.security`; mutations use
8
+ `sys.manage_dns | sys.security`. Member grants cannot satisfy either clause.
9
+
10
+ ## Capability gate
11
+
12
+ `useDnsCapabilities(group?)` reads `/api/dnsman/config`. The parser is strict:
13
+ every flag, limit, provider capability, ACME field, and delegated-ACME field
14
+ must be present with the correct type. There are no provider or feature
15
+ defaults. Until that contract succeeds, dependent controls do not render and
16
+ the page shows **DNS administration unavailable**.
17
+
18
+ Provider controls are derived only from the response. A provider is offered
19
+ for credential linking only when it reports `requires_credential: true`.
20
+
21
+ ## Safe models and operations
22
+
23
+ The package defines `DomainModel`, `DnsCredentialModel`,
24
+ `DomainPurchaseModel`, and `CertificateModel`. Their endpoints and graphs are
25
+ pinned and list params are allowlisted; export params are not accepted.
26
+ Defence-in-depth sanitizers are explicit safe-graph projectors. Only approved
27
+ scalar fields and approved sub-fields of group, user, credential, and domain
28
+ relations survive; unknown or equivalent secret containers drop by default
29
+ before data reaches TanStack Query.
30
+
31
+ DNS records, registrar search/suggest/quote/purchase, house-domain adoption,
32
+ registrant contact, WHOIS, and certificate request/revoke use typed imperative
33
+ helpers. There is deliberately no certificate-material helper.
34
+
35
+ House discovery returns the live `{count,truncated,domains}` contract. Each
36
+ domain includes registration/hosted-zone presence, safe zone metadata,
37
+ tracking/adoption state, and a reason. `untracked=true` returns only untracked
38
+ rows; discovery and assignment remain interactive-superuser operations.
39
+
40
+ The operator registrar workflow is documented in [admin-dns-registrar.md](admin-dns-registrar.md). It does not use discovery: House adoption is manual exact-name only. Purchase tokens remain transient, all post-confirmation outcomes reconcile through the read-only ledger, and contact PII remains imperative/local.
41
+
42
+ These responses require local call-flow handling and must never enter Query or
43
+ Mutation cache, logs, URL state, or mock state:
44
+
45
+ - provider API keys and secrets;
46
+ - registrar confirmation tokens;
47
+ - registrant and WHOIS contact PII;
48
+ - certificate PEM, chains, and private keys.
49
+
50
+ ## Provider credentials
51
+
52
+ `ProviderCredentialsPage` is a server-driven `ModelTable`. Search, filters,
53
+ sorting, and paging go to django-mojo. It has no export and no detail route.
54
+ Clicking a row opens a compact `modal.detail`-style KISS surface showing only
55
+ masked key/secret values and verification health.
56
+
57
+ Link and rotation submit raw values through a fresh-auth imperative POST to
58
+ `/api/dnsman/credential/link`. They are held in uncontrolled password refs,
59
+ cleared after every attempt, sanitized before any explicit cache write, and
60
+ the credential queries are invalidated in `finally`.
61
+
62
+ The link picker uses only `/api/dnsman/credential/group-choice` with bounded
63
+ `search/start/size` listing. A stored ID is hydrated through the exact
64
+ `?id=<id>` form. The full selected object is held by `CollectionSelect`, so it
65
+ never invents `/group-choice/<id>`, reads an active group context, calls
66
+ `/api/group`, or accepts a free-form ID.
67
+
68
+ Verification is transactional at the UI boundary: a failed first link adds
69
+ nothing; a failed rotation rejects, preserves the previous masks/encrypted
70
+ pair, and refetches the now-unverified row with its provider error. Successful
71
+ verification replaces the masks and marks the row verified. Viewer-only users
72
+ can inspect safe rows; managers can link, rotate, activate/retire, and arm
73
+ deletion.
74
+
75
+ ## Themes and showcase
76
+
77
+ The page composes existing portal primitives and semantic tokens; it adds no
78
+ fixed colors or theme-specific stylesheet. The executable three-leg showcase
79
+ is **Develop → Components → Admin → DNS**: live manager lifecycle,
80
+ viewer-only contract, and malformed-capability unavailable state.