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,515 @@
1
+ import { useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query';
2
+ import {
3
+ mojoCall, mojoList, withFreshAuth, type MojoList, type Params,
4
+ } from '../../client';
5
+ import {
6
+ CertificateModel, DnsCredentialModel, DomainModel, sanitizeCertificateRow, sanitizeDnsCredentialRow, sanitizeDomainPurchaseRow, sanitizeDomainRow,
7
+ sanitizeRegistrarDiscoveryResponse,
8
+ type CertificateRow, type DnsCapabilities, type DnsCredentialRow,
9
+ type DnsGroupChoice, type DnsProviderCapability, type DnsRecordRow,
10
+ type DnsRecordSetResponse, type DomainRow, type DomainPurchaseRow, type RegistrarQuote, type RegistrarPurchaseResult,
11
+ type RegistrarDiscoveryResponse, type RegistrarSearchRow, type RegistrantContact, type RegistrantContactResponse,
12
+ type WhoisResponse,
13
+ } from './models';
14
+ import {
15
+ sanitizeAcmeDelegationStatus, sanitizeCertificateLastError,
16
+ type AcmeDelegationStatus,
17
+ } from './certificate-data';
18
+ import { recordKey, sameRecordOwnerSnapshot, snapshotRecordOwner, type RecordOwnerSnapshot } from './dns-data';
19
+ import { sanitizePurchaseResult } from './purchase-data';
20
+ import { sanitizeRegistrantResponse } from './registrant-data';
21
+
22
+ export const DNS_GROUP_CHOICE_ENDPOINT = '/api/dnsman/credential/group-choice';
23
+
24
+ export const dnsKeys = {
25
+ root: ['dnsman'] as const,
26
+ capabilities: (group?: number | null) => ['dnsman', 'config', group ?? 'global'] as const,
27
+ groupChoices: (params: Params) => ['dnsman', 'credential-group-choice', params] as const,
28
+ groupChoice: (id: number) => ['dnsman', 'credential-group-choice', 'one', id] as const,
29
+ records: (domainId: number) => ['dnsman', 'records', domainId] as const,
30
+ delegations: (domainId: number) => ['dnsman', 'delegations', domainId] as const,
31
+ };
32
+
33
+ function object(value: unknown, where: string): Record<string, unknown> {
34
+ if (value == null || typeof value !== 'object' || Array.isArray(value)) {
35
+ throw new Error(`DNS administration unavailable: malformed ${where}`);
36
+ }
37
+ return value as Record<string, unknown>;
38
+ }
39
+
40
+ function boolean(value: unknown, where: string): boolean {
41
+ if (typeof value !== 'boolean') throw new Error(`DNS administration unavailable: malformed ${where}`);
42
+ return value;
43
+ }
44
+
45
+ function string(value: unknown, where: string, nullable = false): string | null {
46
+ if (nullable && value === null) return null;
47
+ if (typeof value !== 'string') throw new Error(`DNS administration unavailable: malformed ${where}`);
48
+ return value;
49
+ }
50
+
51
+ function integer(value: unknown, where: string, min = 0): number {
52
+ if (!Number.isInteger(value) || Number(value) < min) throw new Error(`DNS administration unavailable: malformed ${where}`);
53
+ return Number(value);
54
+ }
55
+
56
+ export function parseDnsCapabilities(value: unknown): DnsCapabilities {
57
+ const raw = object(value, 'capability response');
58
+ const providersRaw = raw.providers;
59
+ if (!Array.isArray(providersRaw) || providersRaw.length === 0) {
60
+ throw new Error('DNS administration unavailable: malformed providers');
61
+ }
62
+ const providers: DnsProviderCapability[] = providersRaw.map((item, index) => {
63
+ const provider = object(item, `providers[${index}]`);
64
+ return {
65
+ name: string(provider.name, `providers[${index}].name`) as string,
66
+ purchase: boolean(provider.purchase, `providers[${index}].purchase`),
67
+ requires_credential: boolean(provider.requires_credential, `providers[${index}].requires_credential`),
68
+ };
69
+ });
70
+ const recordTypes = raw.allowed_record_types;
71
+ if (!Array.isArray(recordTypes) || recordTypes.some((type) => typeof type !== 'string' || !type)) {
72
+ throw new Error('DNS administration unavailable: malformed allowed_record_types');
73
+ }
74
+ const acme = object(raw.acme, 'acme');
75
+ const delegated = object(raw.delegated_acme, 'delegated_acme');
76
+ return {
77
+ purchase_enabled: boolean(raw.purchase_enabled, 'purchase_enabled'),
78
+ registrant_contact_configured: boolean(raw.registrant_contact_configured, 'registrant_contact_configured'),
79
+ max_domain_price: string(raw.max_domain_price, 'max_domain_price') as string,
80
+ currency: string(raw.currency, 'currency') as string,
81
+ quote_ttl_minutes: integer(raw.quote_ttl_minutes, 'quote_ttl_minutes', 1),
82
+ allowed_record_types: [...recordTypes] as string[],
83
+ search_batch_limit: integer(raw.search_batch_limit, 'search_batch_limit', 1),
84
+ suggestions_enabled: boolean(raw.suggestions_enabled, 'suggestions_enabled'),
85
+ providers,
86
+ acme: {
87
+ configured: boolean(acme.configured, 'acme.configured'),
88
+ staging: boolean(acme.staging, 'acme.staging'),
89
+ },
90
+ delegated_acme: {
91
+ available: boolean(delegated.available, 'delegated_acme.available'),
92
+ record_type: string(delegated.record_type, 'delegated_acme.record_type') as string,
93
+ target_suffix: string(delegated.target_suffix, 'delegated_acme.target_suffix', true),
94
+ profile: string(delegated.profile, 'delegated_acme.profile') as string,
95
+ requires_provider_credentials: boolean(
96
+ delegated.requires_provider_credentials,
97
+ 'delegated_acme.requires_provider_credentials',
98
+ ),
99
+ },
100
+ cert_renew_days: integer(raw.cert_renew_days, 'cert_renew_days', 1),
101
+ };
102
+ }
103
+
104
+ export async function fetchDnsCapabilities(group?: number | null): Promise<DnsCapabilities> {
105
+ const response = await mojoCall('/api/dnsman/config', {
106
+ ...(group == null ? {} : { params: { group } }),
107
+ });
108
+ return parseDnsCapabilities(response.data);
109
+ }
110
+
111
+ export function useDnsCapabilities(group?: number | null, opts: { enabled?: boolean } = {}) {
112
+ return useQuery({
113
+ queryKey: dnsKeys.capabilities(group),
114
+ queryFn: () => fetchDnsCapabilities(group),
115
+ enabled: opts.enabled !== false,
116
+ });
117
+ }
118
+
119
+ function groupChoiceInteger(value: unknown, fallback: number, minimum: number, maximum: number): number {
120
+ const input = value == null ? fallback : value;
121
+ if (typeof input === 'boolean' || (typeof input !== 'string' && typeof input !== 'number')) {
122
+ throw new Error('Invalid credential group-choice query');
123
+ }
124
+ if (typeof input === 'string' && !/^[0-9]+$/.test(input)) {
125
+ throw new Error('Invalid credential group-choice query');
126
+ }
127
+ const parsed = typeof input === 'number' ? input : Number(input);
128
+ if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
129
+ throw new Error('Invalid credential group-choice query');
130
+ }
131
+ return parsed;
132
+ }
133
+
134
+ export function normalizeGroupChoiceParams(params: Params): Params {
135
+ const keys = Object.keys(params).filter((key) => params[key] != null);
136
+ if (keys.some((key) => !['id', 'search', 'start', 'size'].includes(key))) throw new Error('Invalid credential group-choice query');
137
+ if (keys.includes('id')) {
138
+ if (keys.length !== 1) throw new Error('Invalid credential group-choice query');
139
+ const input = params.id;
140
+ if (typeof input === 'boolean' || (typeof input !== 'string' && typeof input !== 'number')) {
141
+ throw new Error('Invalid credential group-choice query');
142
+ }
143
+ const text = String(input);
144
+ if (!/^[0-9]+$/.test(text) || BigInt(text) < 1n || BigInt(text) > 9223372036854775807n) throw new Error('Invalid credential group-choice query');
145
+ return { id: text };
146
+ }
147
+ const searchInput = params.search;
148
+ if (searchInput != null && typeof searchInput !== 'string') throw new Error('Invalid credential group-choice query');
149
+ const search = (searchInput ?? '').trim();
150
+ const start = groupChoiceInteger(params.start, 0, 0, 100000);
151
+ const size = groupChoiceInteger(params.size, 25, 1, 50);
152
+ if (search.length > 100) {
153
+ throw new Error('Invalid credential group-choice query');
154
+ }
155
+ return { ...(search ? { search } : {}), start, size };
156
+ }
157
+
158
+ export async function fetchDnsGroupChoices(params: Params = {}): Promise<MojoList<DnsGroupChoice>> {
159
+ const safe = normalizeGroupChoiceParams(params);
160
+ return mojoList<DnsGroupChoice>(DNS_GROUP_CHOICE_ENDPOINT, safe);
161
+ }
162
+
163
+ export async function fetchDnsGroupChoice(id: number): Promise<DnsGroupChoice | null> {
164
+ const result = await fetchDnsGroupChoices({ id });
165
+ return result.rows[0] ?? null;
166
+ }
167
+
168
+ export function useDnsGroupChoice(id: number | null) {
169
+ return useQuery({
170
+ queryKey: dnsKeys.groupChoice(id ?? 0),
171
+ queryFn: () => fetchDnsGroupChoice(id!),
172
+ enabled: id != null,
173
+ });
174
+ }
175
+
176
+ export interface LinkDnsCredentialInput {
177
+ group: number;
178
+ provider: string;
179
+ name?: string;
180
+ api_key: string;
181
+ api_secret: string;
182
+ }
183
+
184
+ async function submitCredential(body: Record<string, unknown>): Promise<DnsCredentialRow> {
185
+ const response = await withFreshAuth(() => mojoCall('/api/dnsman/credential/link', {
186
+ method: 'POST', body,
187
+ }));
188
+ return sanitizeDnsCredentialRow(response.data as DnsCredentialRow);
189
+ }
190
+
191
+ export async function linkDnsCredential(input: LinkDnsCredentialInput): Promise<DnsCredentialRow> {
192
+ if (!Number.isInteger(input.group) || input.group < 1) throw new Error('Choose a group');
193
+ return submitCredential({ ...input });
194
+ }
195
+
196
+ export async function rotateDnsCredential(
197
+ row: DnsCredentialRow,
198
+ input: Pick<LinkDnsCredentialInput, 'name' | 'api_key' | 'api_secret'>,
199
+ ): Promise<DnsCredentialRow> {
200
+ const group = typeof row.group === 'number' ? row.group : row.group?.id;
201
+ if (!group) throw new Error('This credential has no valid group');
202
+ return submitCredential({
203
+ group, provider: row.provider, credential: row.id,
204
+ name: input.name ?? row.name, api_key: input.api_key, api_secret: input.api_secret,
205
+ });
206
+ }
207
+
208
+ async function postData<T>(path: string, body: Record<string, unknown>): Promise<T> {
209
+ return (await mojoCall(path, { method: 'POST', body })).data as T;
210
+ }
211
+
212
+ export function searchRegistrar(domain: string): Promise<RegistrarSearchRow> {
213
+ return postData('/api/dnsman/registrar/search', { domain });
214
+ }
215
+
216
+ export function searchRegistrarBatch(input: { domain?: string; domains?: string[]; tlds?: string[] }): Promise<{ results: RegistrarSearchRow[] }> {
217
+ return postData('/api/dnsman/registrar/search', input);
218
+ }
219
+
220
+ export function suggestRegistrar(input: { domain: string; count?: number; only_available?: boolean }): Promise<{ results: RegistrarSearchRow[] }> {
221
+ return postData('/api/dnsman/registrar/suggest', input);
222
+ }
223
+
224
+ /** Returns a one-use token; consumers must keep it transient and uncached. */
225
+ export function quoteDomain(input: { group: number; domain: string; years?: number }): Promise<RegistrarQuote> {
226
+ return postData('/api/dnsman/registrar/quote', input);
227
+ }
228
+
229
+ export async function purchaseDomain(input: { group: number; purchase: number; confirm_token: string }): Promise<RegistrarPurchaseResult> {
230
+ return sanitizePurchaseResult(await postData('/api/dnsman/registrar/purchase', input));
231
+ }
232
+
233
+ /** Detail and its error remain modal-local; neither enters Query cache. */
234
+ export async function fetchPurchaseDetail(id: number): Promise<DomainPurchaseRow> {
235
+ const response = await mojoCall(`/api/dnsman/purchase/${id}`, { params: { graph: 'default' } });
236
+ return sanitizeDomainPurchaseRow(response.data);
237
+ }
238
+
239
+ export async function fetchRegistrantContact(group: number | null): Promise<RegistrantContactResponse> {
240
+ const response = await mojoCall('/api/dnsman/registrant', { ...(group == null ? {} : { params: { group } }) });
241
+ return sanitizeRegistrantResponse(response.data);
242
+ }
243
+
244
+ export async function saveRegistrantContact(contact: RegistrantContact, group: number | null = null): Promise<RegistrantContactResponse> {
245
+ const response = await mojoCall('/api/dnsman/registrant', { method: 'POST', body: { ...(group == null ? {} : { group }), contact } });
246
+ return sanitizeRegistrantResponse(response.data);
247
+ }
248
+
249
+ export async function clearRegistrantContact(group: number | null): Promise<RegistrantContactResponse> {
250
+ const response = await mojoCall('/api/dnsman/registrant', { method: 'POST', body: { ...(group == null ? {} : { group }), clear: true } });
251
+ return sanitizeRegistrantResponse(response.data);
252
+ }
253
+
254
+ export async function registerExistingDomain(input: { group: number; domain: string; credential: number }): Promise<DomainRow> {
255
+ return sanitizeDomainRow(await postData<DomainRow>('/api/dnsman/registrar/register-existing', input));
256
+ }
257
+
258
+ export async function discoverHouseDomains(untracked = false): Promise<RegistrarDiscoveryResponse> {
259
+ const response = await mojoCall('/api/dnsman/registrar/discover', { params: { untracked } });
260
+ return sanitizeRegistrarDiscoveryResponse(response.data);
261
+ }
262
+
263
+ export async function adoptHouseDomain(input: { domain: string; group?: number; create_zone?: boolean }): Promise<DomainRow> {
264
+ return sanitizeDomainRow(await postData<DomainRow>('/api/dnsman/registrar/adopt', input));
265
+ }
266
+
267
+ export async function assignHouseDomain(input: { domain: number; group: number }): Promise<DomainRow> {
268
+ return sanitizeDomainRow(await postData<DomainRow>('/api/dnsman/registrar/assign-group', input));
269
+ }
270
+
271
+ export function parseDnsRecordSetResponse(value: unknown): DnsRecordSetResponse {
272
+ const payload = object(value, 'record response');
273
+ if (Object.keys(payload).some((key) => !['domain', 'provider', 'records'].includes(key))) throw new Error('DNS administration unavailable: malformed record response');
274
+ if (typeof payload.domain !== 'string' || !payload.domain || typeof payload.provider !== 'string' || !payload.provider || !Array.isArray(payload.records)) {
275
+ throw new Error('DNS administration unavailable: malformed record response');
276
+ }
277
+ const records = payload.records.map((value, index): DnsRecordRow => {
278
+ const record = object(value, `records[${index}]`);
279
+ if (Object.keys(record).some((key) => !['type', 'name', 'record_values', 'ttl'].includes(key))) throw new Error(`DNS administration unavailable: malformed records[${index}]`);
280
+ if (typeof record.type !== 'string' || !record.type || typeof record.name !== 'string' || !record.name
281
+ || !Array.isArray(record.record_values) || record.record_values.some((item) => typeof item !== 'string')
282
+ || !Number.isInteger(record.ttl) || Number(record.ttl) < 0 || 'id' in record) {
283
+ throw new Error(`DNS administration unavailable: malformed records[${index}]`);
284
+ }
285
+ return { type: record.type, name: record.name, record_values: [...record.record_values] as string[], ttl: Number(record.ttl) };
286
+ });
287
+ return { domain: payload.domain, provider: payload.provider, records };
288
+ }
289
+
290
+ /** Direct, uncached read used for confirmation-window preflight/reconciliation. */
291
+ export async function fetchDnsRecords(domain: number): Promise<DnsRecordSetResponse> {
292
+ const response = await mojoCall('/api/dnsman/dns', { params: { domain } });
293
+ return parseDnsRecordSetResponse(response.data);
294
+ }
295
+
296
+ /** Compatibility alias; unlike #1429 it remains strictly id-less. */
297
+ export const listDnsRecords = fetchDnsRecords;
298
+
299
+ export function useDnsRecords(domain: number | null, enabled = true) {
300
+ return useQuery({
301
+ queryKey: dnsKeys.records(domain ?? 0),
302
+ queryFn: () => fetchDnsRecords(domain!),
303
+ enabled: enabled && domain != null,
304
+ });
305
+ }
306
+
307
+ export async function fetchAcmeDelegations(domain: number): Promise<AcmeDelegationStatus[]> {
308
+ const response = await mojoCall('/api/dnsman/delegation', { params: { domain } });
309
+ if (!Array.isArray(response.data)) throw new Error('DNS administration unavailable: malformed delegation response');
310
+ return response.data.map(sanitizeAcmeDelegationStatus).filter((row) => row.id > 0 && row.state !== 'retired');
311
+ }
312
+
313
+ export function useAcmeDelegations(domain: number | null, enabled = true) {
314
+ return useQuery({
315
+ queryKey: dnsKeys.delegations(domain ?? 0),
316
+ queryFn: () => fetchAcmeDelegations(domain!),
317
+ enabled: enabled && domain != null,
318
+ });
319
+ }
320
+
321
+ export interface DnsWriteResponse { status: true; provider: string; change_id: string | null }
322
+ export function parseDnsWriteResponse(value: unknown): DnsWriteResponse {
323
+ const response = object(value, 'record write response');
324
+ if (Object.keys(response).some((key) => !['status', 'provider', 'change_id'].includes(key))
325
+ || response.status !== true || typeof response.provider !== 'string' || !response.provider
326
+ || !(typeof response.change_id === 'string' || response.change_id === null)) {
327
+ throw new Error('DNS administration unavailable: malformed record write response');
328
+ }
329
+ return { status: true, provider: response.provider, change_id: response.change_id };
330
+ }
331
+ export async function upsertDnsRecord(domain: number, record: DnsRecordRow): Promise<DnsWriteResponse> {
332
+ const response = await mojoCall('/api/dnsman/dns', { method: 'POST', body: {
333
+ domain, type: record.type, name: record.name,
334
+ record_values: record.record_values, ttl: record.ttl,
335
+ } });
336
+ return parseDnsWriteResponse(response);
337
+ }
338
+
339
+ export async function deleteDnsRecordSet(domain: number, record: Pick<DnsRecordRow, 'type' | 'name'>): Promise<DnsWriteResponse> {
340
+ const response = await mojoCall('/api/dnsman/dns/delete', { method: 'POST', body: { domain, type: record.type, name: record.name } });
341
+ return parseDnsWriteResponse(response);
342
+ }
343
+ export const deleteDnsRecord = deleteDnsRecordSet;
344
+
345
+ export class DnsStaleSnapshotError extends Error {
346
+ constructor() { super('The live record set changed after this editor opened. Refresh and review the new values before trying again.'); this.name = 'DnsStaleSnapshotError'; }
347
+ }
348
+ export interface DnsOperationResult<T> {
349
+ response?: T;
350
+ live: DnsRecordSetResponse | null;
351
+ applied: boolean;
352
+ ambiguousApplied: boolean;
353
+ }
354
+ export interface CoordinateDnsOperationOptions<T> {
355
+ opening: RecordOwnerSnapshot;
356
+ fetchFresh: () => Promise<DnsRecordSetResponse>;
357
+ write: () => Promise<T>;
358
+ intended: DnsRecordRow | null;
359
+ reconcile?: (live: DnsRecordSetResponse) => void | Promise<void>;
360
+ /** Required cache-safety boundary after every attempted write. */
361
+ invalidate: () => void | Promise<void>;
362
+ }
363
+ function intendedMatches(live: DnsRecordSetResponse, intended: DnsRecordRow | null, opening: RecordOwnerSnapshot): boolean {
364
+ const exact = live.records.find((record) => recordKey(record) === opening.key) ?? null;
365
+ if (intended == null) return exact == null;
366
+ return !!exact && exact.ttl === intended.ttl && exact.type === intended.type && exact.name.toLowerCase().replace(/\.+$/, '') === intended.name.toLowerCase().replace(/\.+$/, '')
367
+ && exact.record_values.length === intended.record_values.length && exact.record_values.every((value, index) => value === intended.record_values[index]);
368
+ }
369
+
370
+ /** Executable confirm -> fresh preflight -> immediate write -> finally reconcile sequence. */
371
+ export async function coordinateDnsRecordOperation<T>(options: CoordinateDnsOperationOptions<T>): Promise<DnsOperationResult<T>> {
372
+ const preflight = await options.fetchFresh();
373
+ const current = snapshotRecordOwner(preflight.records, options.opening.exact?.type ?? options.intended?.type ?? options.opening.key.split('|')[0]!, options.opening.owner);
374
+ if (!sameRecordOwnerSnapshot(options.opening, current)) {
375
+ await options.reconcile?.(preflight);
376
+ throw new DnsStaleSnapshotError();
377
+ }
378
+ let response: T | undefined;
379
+ let requestError: unknown;
380
+ let reconciliationError: unknown;
381
+ let invalidationError: unknown;
382
+ let live: DnsRecordSetResponse | null = null;
383
+ try {
384
+ response = await options.write();
385
+ } catch (error) {
386
+ requestError = error;
387
+ } finally {
388
+ try {
389
+ live = await options.fetchFresh();
390
+ await options.reconcile?.(live);
391
+ } catch (error) {
392
+ reconciliationError = error;
393
+ }
394
+ try {
395
+ await options.invalidate();
396
+ } catch (error) {
397
+ invalidationError = error;
398
+ }
399
+ }
400
+ const applied = live != null && intendedMatches(live, options.intended, options.opening);
401
+ if (requestError) {
402
+ if (applied) {
403
+ const error = requestError instanceof Error ? requestError : new Error(String(requestError));
404
+ Object.assign(error, { dnsLiveStateApplied: true });
405
+ }
406
+ throw requestError;
407
+ }
408
+ if (reconciliationError) throw reconciliationError;
409
+ if (invalidationError) throw invalidationError;
410
+ return { response, live, applied, ambiguousApplied: false };
411
+ }
412
+
413
+ export function useDnsRecordCoordinator(domain: number) {
414
+ const queryClient = useQueryClient();
415
+ return async <T>(options: Omit<CoordinateDnsOperationOptions<T>, 'fetchFresh' | 'reconcile' | 'invalidate'>) => coordinateDnsRecordOperation({
416
+ ...options,
417
+ fetchFresh: () => fetchDnsRecords(domain),
418
+ reconcile: async (live) => {
419
+ queryClient.setQueryData(dnsKeys.records(domain), live);
420
+ },
421
+ invalidate: async () => {
422
+ await queryClient.invalidateQueries({ queryKey: dnsKeys.records(domain) });
423
+ },
424
+ });
425
+ }
426
+
427
+ export async function resolveDnsDomainByName(normalizedName: string): Promise<number | null> {
428
+ const target = normalizedName.trim().toLowerCase().replace(/\.+$/, '');
429
+ if (!target) return null;
430
+ const page = await mojoList<DomainRow>(DomainModel.endpoint, DomainModel.normalizeListParams?.({ search: target, sort: 'name', size: 50 }) ?? { search: target, sort: 'name', size: 50 });
431
+ const exact = page.rows.map(sanitizeDomainRow).find((row) => row.name.toLowerCase().replace(/\.+$/, '') === target);
432
+ return exact && Number.isInteger(exact.id) && exact.id > 0 ? exact.id : null;
433
+ }
434
+
435
+ /** Legal-contact PII: imperative and deliberately outside Query. */
436
+ export async function getRegistrantContact(group?: number | null): Promise<RegistrantContactResponse> {
437
+ return fetchRegistrantContact(group ?? null);
438
+ }
439
+
440
+ /** Registrar PII: imperative and deliberately outside Query. */
441
+ export async function getWhois(domain: number): Promise<WhoisResponse> {
442
+ const response = await mojoCall('/api/dnsman/whois', { params: { domain } });
443
+ return response.data as WhoisResponse;
444
+ }
445
+
446
+ export function updateWhois(domain: number, contact: RegistrantContact): Promise<{ name: string; operation_id: string }> {
447
+ return postData('/api/dnsman/whois', { domain, contact });
448
+ }
449
+
450
+ export function setWhoisPrivacy(domain: number, enabled: boolean): Promise<{ name: string; privacy: boolean; operation_id: string }> {
451
+ return postData('/api/dnsman/whois/privacy', { domain, enabled });
452
+ }
453
+
454
+ const certificateFlights = new Map<string, Promise<CertificateRow>>();
455
+
456
+ async function reconcileCertificates(queryClient: QueryClient, row?: CertificateRow): Promise<void> {
457
+ if (row) queryClient.setQueryData(CertificateModel.keys.one(row.id), row);
458
+ await CertificateModel.invalidate(queryClient);
459
+ await queryClient.refetchQueries({
460
+ type: 'active',
461
+ predicate: (query) => query.queryKey[0] === CertificateModel.endpoint,
462
+ });
463
+ }
464
+
465
+ async function certificateOperation(
466
+ queryClient: QueryClient,
467
+ key: string,
468
+ path: string,
469
+ body: Record<string, unknown>,
470
+ ): Promise<CertificateRow> {
471
+ const existing = certificateFlights.get(key);
472
+ if (existing) return existing;
473
+ const flight = (async () => {
474
+ let row: CertificateRow | undefined;
475
+ let failure: unknown;
476
+ let reconcileFailure: unknown;
477
+ try {
478
+ // Deliberately imperative and non-retrying: both ACME operations
479
+ // can reach external state and must never live in MutationCache.
480
+ row = sanitizeCertificateRow(await postData<CertificateRow>(path, body));
481
+ } catch (error) {
482
+ failure = error;
483
+ } finally {
484
+ try {
485
+ await reconcileCertificates(queryClient, row);
486
+ } catch (error) {
487
+ reconcileFailure = error;
488
+ }
489
+ }
490
+ if (failure) {
491
+ const message = sanitizeCertificateLastError(failure instanceof Error ? failure.message : failure) ?? 'Certificate operation failed';
492
+ throw new Error(`${message}${reconcileFailure ? ' Authoritative refresh also failed; refresh before trying another certificate operation.' : ''}`);
493
+ }
494
+ if (!row || reconcileFailure) {
495
+ throw new Error('The certificate operation may have persisted, but authoritative refresh failed. Refresh before trying another certificate operation.');
496
+ }
497
+ return row;
498
+ })().finally(() => certificateFlights.delete(key));
499
+ certificateFlights.set(key, flight);
500
+ return flight;
501
+ }
502
+
503
+ export function requestCertificate(queryClient: QueryClient, domain: number, names?: string[]): Promise<CertificateRow> {
504
+ return certificateOperation(queryClient, `request:${domain}`, '/api/dnsman/certificate/request', {
505
+ domain, ...(names?.length ? { names } : {}),
506
+ });
507
+ }
508
+
509
+ export function revokeCertificate(queryClient: QueryClient, certificate: number): Promise<CertificateRow> {
510
+ return certificateOperation(queryClient, `revoke:${certificate}`, '/api/dnsman/certificate/revoke', { certificate });
511
+ }
512
+
513
+ export async function invalidateDnsCredentials(queryClient: QueryClient): Promise<void> {
514
+ await queryClient.invalidateQueries({ queryKey: DnsCredentialModel.keys.root });
515
+ }