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,421 @@
1
+ import { apiOrigin, mojoCall, usingMockTransport } from './client';
2
+ import { mockUploadBytes } from './mock';
3
+
4
+ const INITIATE_PATH = '/api/fileman/upload/initiate';
5
+ const FILE_PATH = '/api/fileman/file';
6
+ const FALLBACK_MIME = 'application/octet-stream';
7
+
8
+ export type FileUploadPhase = 'initiating' | 'uploading' | 'reconciling' | 'completing'
9
+ | 'completed' | 'failed' | 'uncertain';
10
+ export type FileUploadFailureStage = 'initiate' | 'transfer' | 'reconcile' | 'complete';
11
+
12
+ export interface UploadedFileRef {
13
+ id: number;
14
+ filename: string;
15
+ contentType: string;
16
+ size: number | null;
17
+ category: string | null;
18
+ fileManagerId: number | null;
19
+ groupId: number | null;
20
+ }
21
+
22
+ export interface FileUploadFailure {
23
+ stage: FileUploadFailureStage;
24
+ code: 'request_failed' | 'cancelled_after_initiate' | 'remote_state_unknown' | 'destination_mismatch';
25
+ message: string;
26
+ retryable: boolean;
27
+ }
28
+
29
+ export type FileUploadOutcome =
30
+ | { status: 'completed'; file: UploadedFileRef }
31
+ | { status: 'failed'; fileId: number | null; failure: FileUploadFailure }
32
+ | { status: 'uncertain'; fileId: number | null; failure: FileUploadFailure };
33
+
34
+ export interface FileUploadSnapshot {
35
+ phase: FileUploadPhase;
36
+ generation: number;
37
+ fileId: number | null;
38
+ loadedBytes: number;
39
+ totalBytes: number | null;
40
+ outcome: FileUploadOutcome | null;
41
+ }
42
+
43
+ export interface StartFileUploadOptions {
44
+ fileManagerId?: number;
45
+ groupId?: number;
46
+ use?: string;
47
+ }
48
+
49
+ export interface FileUploadTask {
50
+ readonly result: Promise<FileUploadOutcome>;
51
+ getSnapshot(): FileUploadSnapshot;
52
+ subscribe(listener: (snapshot: FileUploadSnapshot) => void): () => void;
53
+ cancel(): void;
54
+ retry(): Promise<FileUploadOutcome>;
55
+ recover(): Promise<FileUploadOutcome>;
56
+ }
57
+
58
+ interface InitiatedUpload {
59
+ id: number;
60
+ filename: string;
61
+ contentType: string;
62
+ size: number;
63
+ capability: NormalizedCapability | null;
64
+ }
65
+
66
+ interface NormalizedCapability {
67
+ url: string;
68
+ method: 'PUT' | 'POST';
69
+ headers: Record<string, string>;
70
+ fields: Array<[string, string]>;
71
+ }
72
+
73
+ interface WireFile {
74
+ id?: unknown;
75
+ filename?: unknown;
76
+ content_type?: unknown;
77
+ file_size?: unknown;
78
+ category?: unknown;
79
+ upload_status?: unknown;
80
+ file_manager?: unknown;
81
+ file_manager_id?: unknown;
82
+ group?: unknown;
83
+ group_id?: unknown;
84
+ }
85
+
86
+ function newIdempotencyKey(): string {
87
+ const id = globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
88
+ return `portal-upload:${id}`.slice(0, 128);
89
+ }
90
+
91
+ function isRecord(value: unknown): value is Record<string, unknown> {
92
+ return value != null && typeof value === 'object' && !Array.isArray(value);
93
+ }
94
+
95
+ function integer(value: unknown): number | null {
96
+ return typeof value === 'number' && Number.isSafeInteger(value) ? value : null;
97
+ }
98
+
99
+ function relationId(value: unknown): number | null {
100
+ if (typeof value === 'number' && Number.isSafeInteger(value)) return value;
101
+ return isRecord(value) ? integer(value.id) : null;
102
+ }
103
+
104
+ /** Removes browser fake paths, control characters and path separators. */
105
+ export function sanitizeUploadBasename(name: string): string {
106
+ const basename = name.split(/[\\/]/).pop() ?? '';
107
+ const clean = basename.replace(/[\u0000-\u001f\u007f]/g, '').trim().replace(/^\.+$/, '');
108
+ return (clean || 'upload.bin').slice(0, 255);
109
+ }
110
+
111
+ function safeScalarEntries(value: unknown): Array<[string, string]> {
112
+ if (value == null) return [];
113
+ if (!isRecord(value)) throw new TypeError('Invalid upload provider fields');
114
+ return Object.entries(value).map(([key, item]) => {
115
+ if (!key || !['string', 'number', 'boolean'].includes(typeof item)) throw new TypeError('Invalid upload provider field');
116
+ return [key, String(item)] as [string, string];
117
+ });
118
+ }
119
+
120
+ // Non-Bearer Authorization may itself be a provider capability. Bearer is
121
+ // rejected because the API token must never cross into the byte plane.
122
+ const FORBIDDEN_PROVIDER_HEADERS = new Set(['cookie', 'host', 'content-length', 'x-mojo-uid']);
123
+
124
+ function safeHeaders(value: unknown): Record<string, string> {
125
+ const headers: Record<string, string> = {};
126
+ for (const [name, item] of safeScalarEntries(value)) {
127
+ const lower = name.toLowerCase();
128
+ if (lower === 'authorization' && /^Bearer\s/i.test(item)) throw new TypeError('Unsafe upload provider header');
129
+ if (FORBIDDEN_PROVIDER_HEADERS.has(lower) || lower === 'content-type') continue;
130
+ if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) throw new TypeError('Invalid upload provider header');
131
+ headers[name] = item;
132
+ }
133
+ return headers;
134
+ }
135
+
136
+ function safeCapabilityUrl(value: unknown): string {
137
+ if (typeof value !== 'string' || value.trim() !== value || !value) throw new TypeError('Invalid upload URL');
138
+ if (value.startsWith('/')) {
139
+ if (value.startsWith('//') || value.includes('\\') || /[\u0000-\u001f\u007f#]/.test(value)) throw new TypeError('Invalid upload URL');
140
+ return value === '/api' || value.startsWith('/api/') ? value : `/api${value}`;
141
+ }
142
+ const parsed = new URL(value);
143
+ if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || parsed.hash) throw new TypeError('Invalid upload URL');
144
+ return parsed.toString();
145
+ }
146
+
147
+ function normalizeUploadCapability(value: unknown): NormalizedCapability {
148
+ if (typeof value === 'string') return { url: safeCapabilityUrl(value), method: 'PUT', headers: {}, fields: [] };
149
+ if (!isRecord(value)) throw new TypeError('Invalid upload capability');
150
+ const url = safeCapabilityUrl(value.upload_url ?? value.url);
151
+ const method = String(value.method ?? 'PUT').toUpperCase();
152
+ if (method !== 'PUT' && method !== 'POST') throw new TypeError('Invalid upload method');
153
+ const fields = safeScalarEntries(value.fields);
154
+ if (method === 'PUT' && fields.length) throw new TypeError('PUT upload fields are unsupported');
155
+ return { url, method, headers: safeHeaders(value.headers), fields };
156
+ }
157
+
158
+ function publicUploadUrl(url: string): string {
159
+ if (!url.startsWith('/')) return url;
160
+ const origin = apiOrigin() || (typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
161
+ return new URL(url, origin).toString();
162
+ }
163
+
164
+ function xhrUpload(request: Parameters<typeof mockUploadBytes>[0]): Promise<void> {
165
+ return new Promise((resolve, reject) => {
166
+ const xhr = new XMLHttpRequest();
167
+ const abort = () => xhr.abort();
168
+ xhr.open(request.method, publicUploadUrl(request.url));
169
+ for (const [name, value] of Object.entries(request.headers)) xhr.setRequestHeader(name, value);
170
+ if (request.method === 'PUT') xhr.setRequestHeader('Content-Type', request.contentType);
171
+ xhr.upload.onprogress = (event) => request.onProgress(event.loaded, event.lengthComputable ? event.total : 0);
172
+ xhr.onerror = () => reject(new TypeError('Upload connection failed'));
173
+ xhr.onabort = () => reject(new DOMException('The operation was aborted', 'AbortError'));
174
+ xhr.onload = () => xhr.status >= 200 && xhr.status < 300
175
+ ? resolve()
176
+ : reject(new Error(`Upload provider rejected the request (${xhr.status})`));
177
+ request.signal.addEventListener('abort', abort, { once: true });
178
+ xhr.onloadend = () => request.signal.removeEventListener('abort', abort);
179
+ if (request.method === 'POST') {
180
+ const form = new FormData();
181
+ for (const [name, value] of request.fields) form.append(name, value);
182
+ form.append('file', request.file);
183
+ xhr.send(form);
184
+ } else xhr.send(request.file);
185
+ });
186
+ }
187
+
188
+ function safeFailure(stage: FileUploadFailureStage, code: FileUploadFailure['code'], retryable: boolean): FileUploadFailure {
189
+ const messages: Record<FileUploadFailure['code'], string> = {
190
+ request_failed: 'The upload request failed.',
191
+ cancelled_after_initiate: 'Cancellation was requested after the server created the upload.',
192
+ remote_state_unknown: 'The server could not confirm the upload state.',
193
+ destination_mismatch: 'The completed file does not match the selected destination.',
194
+ };
195
+ return { stage, code, message: messages[code], retryable };
196
+ }
197
+
198
+ function safeCompletedFile(value: unknown): UploadedFileRef {
199
+ if (!isRecord(value)) throw new TypeError('Invalid File response');
200
+ const row = value as WireFile;
201
+ const id = integer(row.id);
202
+ if (id == null || row.upload_status !== 'completed' || typeof row.filename !== 'string' || typeof row.content_type !== 'string') {
203
+ throw new TypeError('Invalid completed File response');
204
+ }
205
+ return {
206
+ id,
207
+ filename: row.filename,
208
+ contentType: row.content_type,
209
+ size: row.file_size == null ? null : integer(row.file_size),
210
+ category: typeof row.category === 'string' ? row.category : null,
211
+ fileManagerId: integer(row.file_manager_id) ?? relationId(row.file_manager),
212
+ groupId: integer(row.group_id) ?? relationId(row.group),
213
+ };
214
+ }
215
+
216
+ class UploadTask implements FileUploadTask {
217
+ readonly #file: File;
218
+ readonly #options: StartFileUploadOptions;
219
+ readonly #listeners = new Set<(snapshot: FileUploadSnapshot) => void>();
220
+ #snapshot: FileUploadSnapshot = { phase: 'initiating', generation: 0, fileId: null, loadedBytes: 0, totalBytes: null, outcome: null };
221
+ #initiated: InitiatedUpload | null = null;
222
+ #controller: AbortController | null = null;
223
+ #active: Promise<FileUploadOutcome> | null = null;
224
+ #result: Promise<FileUploadOutcome>;
225
+ #idempotencyKey = newIdempotencyKey();
226
+ #authoritativeTerminal = false;
227
+
228
+ constructor(file: File, options: StartFileUploadOptions) {
229
+ this.#file = file;
230
+ this.#options = options;
231
+ this.#result = this.#start('initial');
232
+ }
233
+
234
+ get result(): Promise<FileUploadOutcome> { return this.#result; }
235
+ getSnapshot(): FileUploadSnapshot { return { ...this.#snapshot, outcome: this.#snapshot.outcome && structuredClone(this.#snapshot.outcome) }; }
236
+ subscribe(listener: (snapshot: FileUploadSnapshot) => void): () => void {
237
+ this.#listeners.add(listener);
238
+ listener(this.getSnapshot());
239
+ return () => this.#listeners.delete(listener);
240
+ }
241
+ cancel(): void { this.#controller?.abort(); }
242
+ retry(): Promise<FileUploadOutcome> { return this.#start('retry'); }
243
+ recover(): Promise<FileUploadOutcome> { return this.#start('recover'); }
244
+
245
+ #publish(patch: Partial<FileUploadSnapshot>, generation: number): void {
246
+ if (generation !== this.#snapshot.generation) return;
247
+ this.#snapshot = { ...this.#snapshot, ...patch };
248
+ for (const listener of this.#listeners) {
249
+ try { listener(this.getSnapshot()); } catch { /* observers cannot change transfer truth */ }
250
+ }
251
+ }
252
+
253
+ #start(mode: 'initial' | 'retry' | 'recover'): Promise<FileUploadOutcome> {
254
+ if (this.#active) return this.#active;
255
+ if (mode !== 'initial' && !['failed', 'uncertain'].includes(this.#snapshot.phase)) return this.#result;
256
+ // Recovery is reconciliation-only. Without an id it must not create a
257
+ // possible duplicate; the caller must explicitly choose retry/restart.
258
+ if (mode === 'recover' && !this.#initiated) return this.#result;
259
+ // A server-proven failed/expired lifecycle is a closed attempt. The
260
+ // next explicit user retry creates a new File with a fresh key. An
261
+ // id-less ambiguous initiation retains its key so replay finds the
262
+ // first committed File instead of creating a duplicate.
263
+ if (mode === 'retry' && this.#authoritativeTerminal) {
264
+ this.#resetAttempt();
265
+ }
266
+ const generation = this.#snapshot.generation + 1;
267
+ this.#snapshot = { phase: this.#initiated ? 'reconciling' : 'initiating', generation, fileId: this.#initiated?.id ?? null, loadedBytes: 0, totalBytes: null, outcome: null };
268
+ this.#controller = new AbortController();
269
+ const running = this.#run(generation, mode, this.#controller.signal).finally(() => {
270
+ if (this.#snapshot.generation === generation) { this.#active = null; this.#controller = null; }
271
+ });
272
+ this.#active = running;
273
+ this.#result = running;
274
+ return running;
275
+ }
276
+
277
+ async #run(generation: number, mode: 'initial' | 'retry' | 'recover', signal: AbortSignal): Promise<FileUploadOutcome> {
278
+ let stage: FileUploadFailureStage = this.#initiated ? 'reconcile' : 'initiate';
279
+ try {
280
+ if (!this.#initiated) {
281
+ const row = await this.#initiate(generation, signal);
282
+ if (row.upload_status === 'completed') return this.#completeOutcome(safeCompletedFile(row), generation);
283
+ if (row.upload_status === 'failed' || row.upload_status === 'expired') return this.#failOutcome(stage, generation, true);
284
+ }
285
+ else {
286
+ const row = await this.#read(generation, signal);
287
+ if (row.upload_status === 'completed') return this.#completeOutcome(safeCompletedFile(row), generation);
288
+ if (row.upload_status !== 'uploading') {
289
+ if (mode !== 'retry') return this.#failOutcome(stage, generation, true);
290
+ // The explicit retry that discovers an authoritative
291
+ // terminal lifecycle immediately starts its fresh attempt.
292
+ this.#resetAttempt();
293
+ stage = 'initiate';
294
+ const fresh = await this.#initiate(generation, signal);
295
+ if (fresh.upload_status === 'completed') return this.#completeOutcome(safeCompletedFile(fresh), generation);
296
+ if (fresh.upload_status !== 'uploading') return this.#failOutcome(stage, generation, true);
297
+ }
298
+ if (mode === 'recover') return await this.#complete(generation, signal);
299
+ }
300
+ stage = 'transfer';
301
+ await this.#transfer(generation, signal);
302
+ stage = 'reconcile';
303
+ const row = await this.#read(generation, signal);
304
+ if (row.upload_status === 'completed') return this.#completeOutcome(safeCompletedFile(row), generation);
305
+ if (row.upload_status !== 'uploading') return this.#failOutcome(stage, generation, true);
306
+ stage = 'complete';
307
+ return await this.#complete(generation, signal);
308
+ } catch (error) {
309
+ if (generation !== this.#snapshot.generation) return this.#snapshot.outcome ?? this.#failOutcome(stage, generation);
310
+ const aborted = error instanceof DOMException && error.name === 'AbortError';
311
+ if (!this.#initiated) {
312
+ // The request may have committed before its response or abort
313
+ // became observable. Without an id there is nothing safe to
314
+ // reconcile, so absence of a File row must never be claimed.
315
+ const outcome: FileUploadOutcome = {
316
+ status: 'uncertain', fileId: null,
317
+ failure: safeFailure('initiate', 'remote_state_unknown', true),
318
+ };
319
+ this.#publish({ phase: 'uncertain', outcome }, generation);
320
+ return outcome;
321
+ }
322
+ if (!aborted && (stage === 'complete' || stage === 'reconcile')) {
323
+ try {
324
+ const row = await this.#read(generation, new AbortController().signal);
325
+ if (row.upload_status === 'completed') return this.#completeOutcome(safeCompletedFile(row), generation);
326
+ } catch { /* state remains ambiguous */ }
327
+ }
328
+ const outcome: FileUploadOutcome = {
329
+ status: 'uncertain', fileId: this.#initiated.id,
330
+ failure: safeFailure(stage, aborted ? 'cancelled_after_initiate' : 'remote_state_unknown', true),
331
+ };
332
+ this.#publish({ phase: 'uncertain', outcome }, generation);
333
+ return outcome;
334
+ }
335
+ }
336
+
337
+ async #initiate(generation: number, signal: AbortSignal): Promise<WireFile> {
338
+ this.#publish({ phase: 'initiating' }, generation);
339
+ const filename = sanitizeUploadBasename(this.#file.name);
340
+ const requestedType = this.#file.type || FALLBACK_MIME;
341
+ const body: Record<string, unknown> = { filename, content_type: requestedType, file_size: this.#file.size, idempotency_key: this.#idempotencyKey };
342
+ if (this.#options.fileManagerId != null) body.file_manager = this.#options.fileManagerId;
343
+ if (this.#options.groupId != null) body.group = this.#options.groupId;
344
+ if (this.#options.use?.trim()) body.use = this.#options.use.trim();
345
+ const envelope = await mojoCall(INITIATE_PATH, { method: 'POST', body, signal });
346
+ if (!isRecord(envelope.data)) throw new TypeError('Invalid upload initiation response');
347
+ const id = integer(envelope.data.id);
348
+ const contentType = typeof envelope.data.content_type === 'string' && envelope.data.content_type ? envelope.data.content_type : null;
349
+ const serverFilename = typeof envelope.data.filename === 'string' && envelope.data.filename ? envelope.data.filename : null;
350
+ const size = integer(envelope.data.file_size);
351
+ if (id == null || contentType == null || serverFilename == null || size == null) throw new TypeError('Invalid upload initiation response');
352
+ const initiated: InitiatedUpload = { id, filename: serverFilename, contentType, size, capability: null };
353
+ this.#initiated = initiated;
354
+ this.#publish({ fileId: id }, generation);
355
+ if (envelope.data.upload_status === 'uploading') initiated.capability = normalizeUploadCapability(envelope.data);
356
+ return envelope.data;
357
+ }
358
+
359
+ async #transfer(generation: number, signal: AbortSignal): Promise<void> {
360
+ const initiated = this.#initiated!;
361
+ if (!initiated.capability) throw new TypeError('Upload capability is unavailable');
362
+ this.#publish({ phase: 'uploading', loadedBytes: 0, totalBytes: null }, generation);
363
+ const request = {
364
+ ...initiated.capability,
365
+ file: this.#file,
366
+ contentType: initiated.contentType,
367
+ signal,
368
+ onProgress: (loaded: number, total: number) => this.#publish({ loadedBytes: loaded, totalBytes: total > 0 ? total : null }, generation),
369
+ };
370
+ await (usingMockTransport() ? mockUploadBytes(request) : xhrUpload(request));
371
+ }
372
+
373
+ async #read(generation: number, signal: AbortSignal): Promise<WireFile> {
374
+ this.#publish({ phase: 'reconciling' }, generation);
375
+ const envelope = await mojoCall(`${FILE_PATH}/${this.#initiated!.id}`, { signal });
376
+ if (!isRecord(envelope.data)) throw new TypeError('Invalid File response');
377
+ return envelope.data;
378
+ }
379
+
380
+ async #complete(generation: number, signal: AbortSignal): Promise<FileUploadOutcome> {
381
+ this.#publish({ phase: 'completing' }, generation);
382
+ await mojoCall(`${FILE_PATH}/${this.#initiated!.id}`, { method: 'POST', body: { action: 'mark_as_completed' }, signal });
383
+ const row = await this.#read(generation, signal);
384
+ return this.#completeOutcome(safeCompletedFile(row), generation);
385
+ }
386
+
387
+ #resetAttempt(): void {
388
+ this.#initiated = null;
389
+ this.#authoritativeTerminal = false;
390
+ this.#idempotencyKey = newIdempotencyKey();
391
+ }
392
+
393
+ #completeOutcome(file: UploadedFileRef, generation: number): FileUploadOutcome {
394
+ const expectedManager = this.#options.fileManagerId;
395
+ const expectedGroup = this.#options.groupId ?? null;
396
+ if ((expectedManager != null && (file.fileManagerId !== expectedManager || file.groupId !== expectedGroup))
397
+ || (expectedManager == null && this.#options.groupId != null && file.groupId !== expectedGroup)) {
398
+ const outcome: FileUploadOutcome = {
399
+ status: 'uncertain', fileId: file.id,
400
+ failure: safeFailure('reconcile', 'destination_mismatch', false),
401
+ };
402
+ this.#publish({ phase: 'uncertain', outcome }, generation);
403
+ return outcome;
404
+ }
405
+ const outcome: FileUploadOutcome = { status: 'completed', file };
406
+ this.#publish({ phase: 'completed', outcome }, generation);
407
+ return outcome;
408
+ }
409
+
410
+ #failOutcome(stage: FileUploadFailureStage, generation: number, terminal = false): FileUploadOutcome {
411
+ this.#authoritativeTerminal = terminal;
412
+ const outcome: FileUploadOutcome = { status: 'failed', fileId: this.#initiated?.id ?? null, failure: safeFailure(stage, 'request_failed', terminal) };
413
+ this.#publish({ phase: 'failed', outcome }, generation);
414
+ return outcome;
415
+ }
416
+ }
417
+
418
+ /** Starts the imperative three-stage django-mojo upload workflow immediately. */
419
+ export function startFileUpload(file: File, options: StartFileUploadOptions = {}): FileUploadTask {
420
+ return new UploadTask(file, options);
421
+ }