@stacksjs/defaults 0.70.353 → 0.70.355

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 (125) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +17 -2
  2. package/ai/skills/stacks-browse/scripts/browse.ts +207 -6
  3. package/ai/skills/stacks-dashboard/scripts/audit.ts +0 -1
  4. package/ai/skills/stacks-email/SKILL.md +35 -15
  5. package/app/Actions/Dashboard/Auth/MeAction.ts +8 -7
  6. package/app/Actions/Dashboard/Ci/RepoRunJobsAction.ts +10 -15
  7. package/app/Actions/Dashboard/Ci/RepoRunsAction.ts +12 -14
  8. package/app/Actions/Dashboard/Ci/RunnerHistoryAction.ts +11 -10
  9. package/app/Actions/Dashboard/Ci/StatusAction.ts +2 -11
  10. package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +1 -0
  11. package/app/Actions/Dashboard/Commerce/CommercePrintLogsAction.ts +3 -4
  12. package/app/Actions/Dashboard/Commerce/CommerceProductDetailAction.ts +1 -0
  13. package/app/Actions/Dashboard/Commerce/PaymentRefundAction.ts +1 -0
  14. package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +1 -0
  15. package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +1 -0
  16. package/app/Actions/Dashboard/Commerce/tax-rate-records.ts +9 -0
  17. package/app/Actions/Dashboard/Content/author-input.ts +1 -1
  18. package/app/Actions/Dashboard/Content/page-input.ts +1 -1
  19. package/app/Actions/Dashboard/DashboardStatsAction.ts +0 -1
  20. package/app/Actions/Dashboard/Deployments/CreateDeployment.ts +3 -2
  21. package/app/Actions/Dashboard/Deployments/GetDeployment.ts +1 -0
  22. package/app/Actions/Dashboard/Deployments/UpdateDeployScript.ts +3 -2
  23. package/app/Actions/Dashboard/Email/InboxActivityAction.ts +28 -16
  24. package/app/Actions/Dashboard/Email/InboxAttachmentDownloadAction.ts +45 -0
  25. package/app/Actions/Dashboard/Email/InboxDeleteAction.ts +11 -6
  26. package/app/Actions/Dashboard/Email/InboxIndexAction.ts +5 -7
  27. package/app/Actions/Dashboard/Email/InboxMarkReadAction.ts +11 -6
  28. package/app/Actions/Dashboard/Email/InboxMarkUnreadAction.ts +11 -6
  29. package/app/Actions/Dashboard/Email/InboxPreferenceShowAction.ts +10 -4
  30. package/app/Actions/Dashboard/Email/InboxPreferenceUpdateAction.ts +8 -9
  31. package/app/Actions/Dashboard/Email/InboxShowAction.ts +7 -7
  32. package/app/Actions/Dashboard/Email/InboxStatsAction.ts +5 -7
  33. package/app/Actions/Dashboard/Email/inbox-request.ts +33 -0
  34. package/app/Actions/Dashboard/Infrastructure/LogIndexAction.ts +3 -4
  35. package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +3 -6
  36. package/app/Actions/Dashboard/Jobs/JobIndexAction.ts +8 -15
  37. package/app/Actions/Dashboard/Jobs/JobRetryAction.ts +4 -3
  38. package/app/Actions/Dashboard/Jobs/JobShowAction.ts +3 -3
  39. package/app/Actions/Dashboard/Kanban/BoardDestroyAction.ts +5 -5
  40. package/app/Actions/Dashboard/Kanban/BoardShowAction.ts +9 -16
  41. package/app/Actions/Dashboard/Kanban/BoardStoreAction.ts +4 -3
  42. package/app/Actions/Dashboard/Kanban/BoardUpdateAction.ts +5 -5
  43. package/app/Actions/Dashboard/Kanban/BoardsIndexAction.ts +1 -2
  44. package/app/Actions/Dashboard/Kanban/BoardsReorderAction.ts +4 -3
  45. package/app/Actions/Dashboard/Kanban/CardAssigneesSyncAction.ts +11 -9
  46. package/app/Actions/Dashboard/Kanban/CardCommentDestroyAction.ts +10 -12
  47. package/app/Actions/Dashboard/Kanban/CardCommentStoreAction.ts +19 -43
  48. package/app/Actions/Dashboard/Kanban/CardCommentUpdateAction.ts +44 -0
  49. package/app/Actions/Dashboard/Kanban/CardDestroyAction.ts +5 -5
  50. package/app/Actions/Dashboard/Kanban/CardLabelsSyncAction.ts +6 -6
  51. package/app/Actions/Dashboard/Kanban/CardShowAction.ts +4 -4
  52. package/app/Actions/Dashboard/Kanban/CardStoreAction.ts +7 -6
  53. package/app/Actions/Dashboard/Kanban/CardUpdateAction.ts +5 -5
  54. package/app/Actions/Dashboard/Kanban/CardsReorderAction.ts +4 -3
  55. package/app/Actions/Dashboard/Kanban/ColumnDestroyAction.ts +5 -5
  56. package/app/Actions/Dashboard/Kanban/ColumnStoreAction.ts +4 -3
  57. package/app/Actions/Dashboard/Kanban/ColumnUpdateAction.ts +5 -5
  58. package/app/Actions/Dashboard/Kanban/ColumnsReorderAction.ts +4 -3
  59. package/app/Actions/Dashboard/Kanban/LabelDestroyAction.ts +4 -4
  60. package/app/Actions/Dashboard/Kanban/LabelStoreAction.ts +4 -3
  61. package/app/Actions/Dashboard/Kanban/LabelUpdateAction.ts +5 -5
  62. package/app/Actions/Dashboard/Kanban/UsersListAction.ts +1 -1
  63. package/app/Actions/Dashboard/Kanban/kanban-comment.ts +14 -0
  64. package/app/Actions/Dashboard/Kanban/kanban-model.test.ts +36 -0
  65. package/app/Actions/Dashboard/Kanban/kanban-model.ts +14 -7
  66. package/app/Actions/Dashboard/Models/ModelDestroyAction.ts +4 -3
  67. package/app/Actions/Dashboard/Models/ModelShowAction.ts +15 -18
  68. package/app/Actions/Dashboard/Models/ModelStoreAction.ts +8 -8
  69. package/app/Actions/Dashboard/Models/ModelUpdateAction.ts +9 -9
  70. package/app/Actions/Dashboard/Models/ModelsIndexAction.ts +4 -3
  71. package/app/Actions/Dashboard/Models/model-api.ts +27 -0
  72. package/app/Actions/Dashboard/Models/model-write.ts +2 -10
  73. package/app/Actions/Dashboard/Notifications/NotificationDeliveryHistoryAction.ts +3 -4
  74. package/app/Actions/Dashboard/Notifications/NotificationDeliveryIndexAction.ts +6 -4
  75. package/app/Actions/Dashboard/Queries/QueryShowAction.ts +1 -0
  76. package/app/Actions/Dashboard/Rbac/PermissionDestroyAction.ts +4 -4
  77. package/app/Actions/Dashboard/Rbac/PermissionStoreAction.ts +3 -2
  78. package/app/Actions/Dashboard/Rbac/RoleDestroyAction.ts +4 -4
  79. package/app/Actions/Dashboard/Rbac/RolePermissionsShowAction.ts +4 -4
  80. package/app/Actions/Dashboard/Rbac/RolePermissionsSyncAction.ts +4 -3
  81. package/app/Actions/Dashboard/Rbac/RoleStoreAction.ts +3 -2
  82. package/app/Actions/Dashboard/Rbac/UserRolesShowAction.ts +4 -5
  83. package/app/Actions/Dashboard/Rbac/UserRolesSyncAction.ts +4 -4
  84. package/app/Actions/Dashboard/Search/GlobalSearchAction.ts +1 -0
  85. package/app/Actions/Dashboard/Teams/TeamInviteAction.ts +3 -3
  86. package/app/Actions/Dashboard/Teams/TeamMemberUpdateAction.ts +2 -2
  87. package/app/Actions/Dashboard/dashboard-request.ts +13 -0
  88. package/app/Models/CardComment.ts +3 -4
  89. package/app/Models/commerce/TaxRate.ts +48 -0
  90. package/functions/dashboard-api.ts +51 -10
  91. package/functions/inbox.test.ts +2 -0
  92. package/functions/inbox.ts +27 -2
  93. package/functions/public-application-url.test.ts +13 -0
  94. package/functions/public-application-url.ts +27 -0
  95. package/ide/vscode/package.json +1 -1
  96. package/package.json +1 -1
  97. package/resources/components/Dashboard/Auth/AccessTokens.stx +15 -0
  98. package/resources/components/Dashboard/Ci/CiDashboard.stx +24 -6
  99. package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +23 -0
  100. package/resources/components/Dashboard/Commerce/CommerceTaxesDashboard.stx +4 -0
  101. package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +9 -1
  102. package/resources/components/Dashboard/Commerce/ProductWaitlistDashboard.stx +8 -7
  103. package/resources/components/Dashboard/Commerce/RestaurantWaitlistDashboard.stx +8 -7
  104. package/resources/components/Dashboard/Content/BlogDashboard.stx +14 -2
  105. package/resources/components/Dashboard/Content/PostsDashboard.stx +7 -1
  106. package/resources/components/Dashboard/Deployments/DeploymentDetail.stx +16 -2
  107. package/resources/components/Dashboard/Email/EmailNavigation.stx +5 -5
  108. package/resources/components/Dashboard/Email/EmailSidebar.stx +1 -1
  109. package/resources/components/Dashboard/Email/InboxAttachmentList.stx +53 -0
  110. package/resources/components/Dashboard/Email/InboxDashboard.stx +5 -4
  111. package/resources/components/Dashboard/Email/InboxMessageDetail.stx +6 -4
  112. package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +63 -6
  113. package/resources/components/Dashboard/Kanban/KanbanBoardsDashboard.stx +3 -6
  114. package/resources/components/Dashboard/Management/PermissionsDashboard.stx +4 -4
  115. package/resources/components/Dashboard/NotFoundDashboard.stx +2 -5
  116. package/resources/components/Dashboard/Queries/QueryDashboard.stx +6 -2
  117. package/resources/components/Dashboard/Settings/AppearanceSettingsDashboard.stx +4 -4
  118. package/resources/components/Dashboard/UI/EmptyState.stx +2 -2
  119. package/resources/components/Dashboard/UI/PageLayout.stx +3 -3
  120. package/resources/functions/dashboard/sidebar.ts +4 -4
  121. package/routes/dashboard-api.ts +8 -20
  122. package/views/dashboard/.discovered-models.json +24 -0
  123. package/views/dashboard/layouts/default.stx +19 -16
  124. package/views/dashboard/stores/ci.ts +13 -7
  125. package/views/dashboard/stores/kanban.ts +48 -11
@@ -1,6 +1,7 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { dashboard as dashboardConfig } from '@stacksjs/config'
3
3
  import { getDashboardData } from '@stacksjs/github'
4
+ import { response } from '@stacksjs/router'
4
5
  import { runCiFailureNotifier } from './failure-notifier'
5
6
  import { runRunnerPressureMonitor } from './runner-pressure-monitor'
6
7
 
@@ -57,17 +58,7 @@ export default new Action({
57
58
  // shape the page can render without leaking the env-var name to
58
59
  // every browser viewing the dashboard.
59
60
  console.error('[dashboard/ci] snapshot failed:', err)
60
- return {
61
- repos: [],
62
- fetchedAt: new Date().toISOString(),
63
- total: 0,
64
- passing: 0,
65
- failing: 0,
66
- pending: 0,
67
- noRuns: 0,
68
- runners: {},
69
- error: err instanceof Error ? err.message : 'unknown error',
70
- }
61
+ return response.json({ message: 'CI status could not be loaded.' }, 502)
71
62
  }
72
63
  },
73
64
  })
@@ -1,3 +1,4 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { randomUUIDv7 } from 'bun'
2
3
  import { Action } from '@stacksjs/actions'
3
4
  import { orders } from '@stacksjs/commerce'
@@ -2,7 +2,8 @@ import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { db } from '@stacksjs/database'
4
4
  import { formatDate, PrintDevice } from '@stacksjs/orm'
5
- import { request as routerRequest, response } from '@stacksjs/router'
5
+ import { response } from '@stacksjs/router'
6
+ import { dashboardRequestValue } from '../dashboard-request'
6
7
  import {
7
8
  commerceEnum,
8
9
  commerceIdentifier,
@@ -24,9 +25,7 @@ const SORTS = {
24
25
  type PrintLogSort = keyof typeof SORTS
25
26
 
26
27
  function queryValue(request: RequestInstance, key: string): string {
27
- const query = ((routerRequest as any).query || {}) as Record<string, string | string[] | undefined>
28
- const value = query[key]
29
- return String((Array.isArray(value) ? value[0] : value) || request.get(key) || '').trim()
28
+ return dashboardRequestValue(request, key)
30
29
  }
31
30
 
32
31
  function selectedSort(value: string): PrintLogSort {
@@ -1,3 +1,4 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { config } from '@stacksjs/config'
3
4
  import { Category, Manufacturer, Product, ProductUnit, ProductVariant, Review } from '@stacksjs/orm'
@@ -1,3 +1,4 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { payments } from '@stacksjs/commerce'
3
4
  import { log } from '@stacksjs/logging'
@@ -1,3 +1,4 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { products } from '@stacksjs/commerce'
3
4
  import { response } from '@stacksjs/router'
@@ -1,3 +1,4 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { tax } from '@stacksjs/commerce'
3
4
  import { response } from '@stacksjs/router'
@@ -19,6 +19,10 @@ export interface TaxRateRecord {
19
19
  region: TaxRateRegion
20
20
  status: 'active' | 'inactive'
21
21
  isDefault: boolean
22
+ /** Stable identifier application code matches on, independent of `name`. */
23
+ code: string
24
+ /** Whether a qualifying exemption stops this component being charged. */
25
+ exemptible: boolean
22
26
  createdAt: string
23
27
  }
24
28
 
@@ -50,6 +54,11 @@ export function normalizeTaxRateRecord(record: any): TaxRateRecord {
50
54
  ]),
51
55
  status: commerceEnum(commerceValue(record, 'status'), source, 'status', ['active', 'inactive']),
52
56
  isDefault: commerceBoolean(commerceValue(record, 'is_default', 'isDefault'), source, 'is_default'),
57
+ // Optional, both of them: a rate created before these columns existed has
58
+ // neither, and a dashboard that throws on an older row is worse than one
59
+ // that shows it without a badge.
60
+ code: String(commerceValue(record, 'code') ?? ''),
61
+ exemptible: Boolean(commerceValue(record, 'exemptible') ?? false),
53
62
  createdAt: commerceTimestamp(commerceValue(record, 'created_at', 'createdAt'), source),
54
63
  }
55
64
  }
@@ -9,7 +9,7 @@ export interface AuthorInput {
9
9
  avatar: string
10
10
  }
11
11
 
12
- // eslint-disable-next-line pickier/no-unused-vars
12
+ // eslint-disable-next-line
13
13
  export function parseAuthorInput(inputRequest: RequestInstance): { data: AuthorInput } | { message: string } {
14
14
  const data = {
15
15
  name: str(inputRequest.get('name')).trim(),
@@ -10,7 +10,7 @@ export function parsePublished(value: unknown): boolean {
10
10
  return value === true || value === 1 || value === '1' || value === 'true'
11
11
  }
12
12
 
13
- // eslint-disable-next-line pickier/no-unused-vars
13
+ // eslint-disable-next-line
14
14
  export function parsePageInput(inputRequest: RequestInstance): { data: PageInput } | { message: string } {
15
15
  const data = {
16
16
  title: str(inputRequest.get('title')).trim(),
@@ -58,7 +58,6 @@ export default new Action({
58
58
  const { count: _count, ...metadata } = definition
59
59
  return formatDashboardStat(metadata, results[index])
60
60
  })
61
- // eslint-disable-next-line pickier/no-unused-vars
62
61
  const issues = results.flatMap((result, index) => result.status === 'rejected'
63
62
  ? [{
64
63
  source: definitions[index].title,
@@ -1,8 +1,9 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { join } from 'node:path'
3
4
  import process from 'node:process'
4
5
  import { Deployment } from '@stacksjs/orm'
5
- import { request, response } from '@stacksjs/router'
6
+ import { response } from '@stacksjs/router'
6
7
  import { booleanValue, deploymentCommandArgs } from './deployment-input'
7
8
 
8
9
  function gitValue(args: string[]): string {
@@ -22,7 +23,7 @@ export default new Action({
22
23
  method: 'POST',
23
24
  apiResponse: true,
24
25
 
25
- async handle() {
26
+ async handle(request: RequestInstance) {
26
27
  const dryRun = booleanValue(request.get('dryRun') || request.get('dry_run'))
27
28
  const confirmed = booleanValue(request.get('confirmed'))
28
29
  if (!dryRun && !confirmed) {
@@ -1,3 +1,4 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { Deployment } from '@stacksjs/orm'
3
4
  import { response } from '@stacksjs/router'
@@ -1,8 +1,9 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { mkdir, rename, writeFile } from 'node:fs/promises'
3
4
  import { dirname, join } from 'node:path'
4
5
  import process from 'node:process'
5
- import { request, response } from '@stacksjs/router'
6
+ import { response } from '@stacksjs/router'
6
7
 
7
8
  export default new Action({
8
9
  name: 'UpdateDeployScript',
@@ -10,7 +11,7 @@ export default new Action({
10
11
  method: 'PUT',
11
12
  apiResponse: true,
12
13
 
13
- async handle() {
14
+ async handle(request: RequestInstance) {
14
15
  const content = String(request.get('content') || '')
15
16
  if (!content.trim())
16
17
  return response.json({ success: false, message: 'Deploy script content is required.' }, { status: 422 })
@@ -2,8 +2,9 @@ import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { db } from '@stacksjs/database'
4
4
  import { emailSDK } from '@stacksjs/email'
5
- import { request as routerRequest, response } from '@stacksjs/router'
6
- import { defaultMailbox } from './mail-preference'
5
+ import { response } from '@stacksjs/router'
6
+ import { dashboardRequestValue } from '../dashboard-request'
7
+ import { dashboardMailbox, inboxActionError } from './inbox-request'
7
8
 
8
9
  const RANGES = ['day', 'week', 'month', 'year'] as const
9
10
  type ActivityRange = typeof RANGES[number]
@@ -34,9 +35,7 @@ interface Bucket {
34
35
  }
35
36
 
36
37
  function queryValue(request: RequestInstance, key: string): string {
37
- const query = ((routerRequest as any).query || {}) as Record<string, string | string[] | undefined>
38
- const value = query[key]
39
- return String((Array.isArray(value) ? value[0] : value) || request.get(key) || '').trim()
38
+ return dashboardRequestValue(request, key)
40
39
  }
41
40
 
42
41
  function startOfHour(date: Date): Date {
@@ -144,23 +143,36 @@ export default new Action({
144
143
  const range = RANGES.includes(requestedRange as ActivityRange)
145
144
  ? requestedRange as ActivityRange
146
145
  : 'week'
147
- const mailbox = queryValue(request, 'mailbox') || defaultMailbox()
146
+ let mailbox: string
147
+ try {
148
+ mailbox = dashboardMailbox(request)
149
+ }
150
+ catch (error) {
151
+ return inboxActionError(error, 'Inbox activity could not be loaded.')
152
+ }
148
153
  const buckets = buildBuckets(range, new Date())
149
154
  const periodStart = buckets[0]!.start
150
155
  const periodEnd = buckets.at(-1)!.end
151
156
  const periodLength = periodEnd.getTime() - periodStart.getTime()
152
157
  const previousStart = new Date(periodStart.getTime() - periodLength)
153
158
 
154
- const [inbox, deliveryRows] = await Promise.all([
155
- emailSDK.getInbox(mailbox, { limit: 1000 }),
156
- db
157
- .selectFrom('notification_deliveries')
158
- .select(['id', 'recipient', 'subject', 'status', 'sent_at', 'created_at'])
159
- .where('channel', '=', 'email')
160
- .where('created_at', '>=', previousStart.toISOString())
161
- .orderBy('created_at', 'desc')
162
- .execute() as Promise<DeliveryRow[]>,
163
- ])
159
+ let inbox
160
+ let deliveryRows: DeliveryRow[]
161
+ try {
162
+ [inbox, deliveryRows] = await Promise.all([
163
+ emailSDK.getInbox(mailbox, { limit: 1000 }),
164
+ db
165
+ .selectFrom('notification_deliveries')
166
+ .select(['id', 'recipient', 'subject', 'status', 'sent_at', 'created_at'])
167
+ .where('channel', '=', 'email')
168
+ .where('created_at', '>=', previousStart.toISOString())
169
+ .orderBy('created_at', 'desc')
170
+ .execute() as Promise<DeliveryRow[]>,
171
+ ])
172
+ }
173
+ catch (error) {
174
+ return inboxActionError(error, 'Inbox activity could not be loaded.')
175
+ }
164
176
 
165
177
  const currentInbound = inbox.filter(email => inPeriod(email.date, periodStart, periodEnd))
166
178
  const previousInbound = inbox.filter(email => inPeriod(email.date, previousStart, periodStart))
@@ -0,0 +1,45 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { Action } from '@stacksjs/actions'
3
+ import {
4
+ emailSDK,
5
+ inboxAttachmentContentDisposition,
6
+ inboxAttachmentContentType,
7
+ } from '@stacksjs/email'
8
+ import { response } from '@stacksjs/router'
9
+ import { dashboardMailbox, inboxActionError } from './inbox-request'
10
+
11
+ export default new Action({
12
+ name: 'InboxAttachmentDownloadAction',
13
+ description: 'Downloads one attachment from a guarded inbound email.',
14
+ method: 'GET',
15
+ apiResponse: false,
16
+
17
+ async handle(request: RequestInstance) {
18
+ const messageId = String(request.getParam('id') || '')
19
+ const attachmentId = String(request.getParam('attachmentId') || '')
20
+
21
+ if (!messageId || !attachmentId)
22
+ return response.json({ message: 'A message ID and attachment ID are required.' }, 422)
23
+
24
+ try {
25
+ const mailbox = dashboardMailbox(request)
26
+ const result = await emailSDK.getAttachment(mailbox, messageId, attachmentId)
27
+ if (!result)
28
+ return response.json({ message: 'Attachment not found.' }, 404)
29
+
30
+ return new Response(result.body as BodyInit, {
31
+ status: 200,
32
+ headers: {
33
+ 'Cache-Control': 'private, no-store',
34
+ 'Content-Disposition': inboxAttachmentContentDisposition(result.attachment.name),
35
+ 'Content-Length': String(result.body.byteLength),
36
+ 'Content-Type': inboxAttachmentContentType(result.contentType),
37
+ 'X-Content-Type-Options': 'nosniff',
38
+ },
39
+ })
40
+ }
41
+ catch (error) {
42
+ return inboxActionError(error, 'The attachment could not be downloaded.')
43
+ }
44
+ },
45
+ })
@@ -2,7 +2,7 @@ import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { emailSDK } from '@stacksjs/email'
4
4
  import { response } from '@stacksjs/router'
5
- import { defaultMailbox } from './mail-preference'
5
+ import { dashboardMailbox, inboxActionError } from './inbox-request'
6
6
 
7
7
  export default new Action({
8
8
  name: 'InboxDeleteAction',
@@ -11,16 +11,21 @@ export default new Action({
11
11
  apiResponse: true,
12
12
 
13
13
  async handle(request: RequestInstance) {
14
- const mailbox = String(request.get('mailbox') || defaultMailbox())
15
14
  const messageId = String(request.getParam('id') || '')
16
15
 
17
16
  if (!messageId)
18
17
  return response.json({ message: 'A message ID is required.' }, 422)
19
18
 
20
- const success = await emailSDK.delete(mailbox, messageId)
21
- if (!success)
22
- return response.json({ message: 'Email not found or it could not be deleted.' }, 404)
19
+ try {
20
+ const mailbox = dashboardMailbox(request)
21
+ const success = await emailSDK.delete(mailbox, messageId)
22
+ if (!success)
23
+ return response.json({ message: 'Email not found or it could not be deleted.' }, 404)
23
24
 
24
- return response.json({ success: true, mailbox, messageId })
25
+ return response.json({ success: true, mailbox, messageId })
26
+ }
27
+ catch (error) {
28
+ return inboxActionError(error, 'The email could not be deleted.')
29
+ }
25
30
  },
26
31
  })
@@ -1,7 +1,7 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { emailSDK } from '@stacksjs/email'
3
- import { response } from '@stacksjs/router'
4
- import { defaultMailbox } from './mail-preference'
4
+ import { dashboardMailbox, inboxActionError } from './inbox-request'
5
5
 
6
6
  export interface InboxItem {
7
7
  messageId: string
@@ -22,9 +22,9 @@ export default new Action({
22
22
  method: 'GET',
23
23
  apiResponse: true,
24
24
 
25
- async handle(request: any) {
25
+ async handle(request: RequestInstance) {
26
26
  try {
27
- const mailbox = request?.query?.mailbox || defaultMailbox()
27
+ const mailbox = dashboardMailbox(request)
28
28
  const emails = await emailSDK.getInbox(mailbox, { limit: 1000 })
29
29
 
30
30
  return {
@@ -34,9 +34,7 @@ export default new Action({
34
34
  }
35
35
  }
36
36
  catch (err) {
37
- return response.json({
38
- message: err instanceof Error ? err.message : 'Inbox messages could not be loaded.',
39
- }, 503)
37
+ return inboxActionError(err, 'Inbox messages could not be loaded.')
40
38
  }
41
39
  },
42
40
  })
@@ -2,7 +2,7 @@ import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { emailSDK } from '@stacksjs/email'
4
4
  import { response } from '@stacksjs/router'
5
- import { defaultMailbox } from './mail-preference'
5
+ import { dashboardMailbox, inboxActionError } from './inbox-request'
6
6
 
7
7
  export default new Action({
8
8
  name: 'InboxMarkReadAction',
@@ -11,16 +11,21 @@ export default new Action({
11
11
  apiResponse: true,
12
12
 
13
13
  async handle(request: RequestInstance) {
14
- const mailbox = String(request.get('mailbox') || defaultMailbox())
15
14
  const messageId = String(request.get('messageId') || '')
16
15
 
17
16
  if (!messageId)
18
17
  return response.json({ message: 'messageId is required.' }, 422)
19
18
 
20
- const success = await emailSDK.markAsRead(mailbox, messageId)
21
- if (!success)
22
- return response.json({ message: 'Email not found or its read state could not be updated.' }, 404)
19
+ try {
20
+ const mailbox = dashboardMailbox(request)
21
+ const success = await emailSDK.markAsRead(mailbox, messageId)
22
+ if (!success)
23
+ return response.json({ message: 'Email not found or its read state could not be updated.' }, 404)
23
24
 
24
- return response.json({ success: true, mailbox, messageId })
25
+ return response.json({ success: true, mailbox, messageId })
26
+ }
27
+ catch (error) {
28
+ return inboxActionError(error, 'The email read state could not be updated.')
29
+ }
25
30
  },
26
31
  })
@@ -2,7 +2,7 @@ import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { emailSDK } from '@stacksjs/email'
4
4
  import { response } from '@stacksjs/router'
5
- import { defaultMailbox } from './mail-preference'
5
+ import { dashboardMailbox, inboxActionError } from './inbox-request'
6
6
 
7
7
  export default new Action({
8
8
  name: 'InboxMarkUnreadAction',
@@ -11,16 +11,21 @@ export default new Action({
11
11
  apiResponse: true,
12
12
 
13
13
  async handle(request: RequestInstance) {
14
- const mailbox = String(request.get('mailbox') || defaultMailbox())
15
14
  const messageId = String(request.get('messageId') || '')
16
15
 
17
16
  if (!messageId)
18
17
  return response.json({ message: 'messageId is required.' }, 422)
19
18
 
20
- const success = await emailSDK.markAsUnread(mailbox, messageId)
21
- if (!success)
22
- return response.json({ message: 'Email not found or its read state could not be updated.' }, 404)
19
+ try {
20
+ const mailbox = dashboardMailbox(request)
21
+ const success = await emailSDK.markAsUnread(mailbox, messageId)
22
+ if (!success)
23
+ return response.json({ message: 'Email not found or its read state could not be updated.' }, 404)
23
24
 
24
- return response.json({ success: true, mailbox, messageId })
25
+ return response.json({ success: true, mailbox, messageId })
26
+ }
27
+ catch (error) {
28
+ return inboxActionError(error, 'The email read state could not be updated.')
29
+ }
25
30
  },
26
31
  })
@@ -2,7 +2,8 @@ import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { MailPreference } from '@stacksjs/orm'
4
4
  import { response } from '@stacksjs/router'
5
- import { defaultMailbox, serializeMailPreference } from './mail-preference'
5
+ import { dashboardMailbox, inboxActionError } from './inbox-request'
6
+ import { serializeMailPreference } from './mail-preference'
6
7
 
7
8
  export default new Action({
8
9
  name: 'InboxPreferenceShowAction',
@@ -11,8 +12,13 @@ export default new Action({
11
12
  apiResponse: true,
12
13
 
13
14
  async handle(request: RequestInstance) {
14
- const mailbox = String(request.get('mailbox') || defaultMailbox()).trim().toLowerCase()
15
- const record = await MailPreference.where('mailbox', '=', mailbox).first()
16
- return response.json({ preference: serializeMailPreference(record, mailbox) })
15
+ try {
16
+ const mailbox = dashboardMailbox(request)
17
+ const record = await MailPreference.where('mailbox', '=', mailbox).first()
18
+ return response.json({ preference: serializeMailPreference(record, mailbox) })
19
+ }
20
+ catch (error) {
21
+ return inboxActionError(error, 'Mail settings could not be loaded.')
22
+ }
17
23
  },
18
24
  })
@@ -2,8 +2,8 @@ import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { MailPreference } from '@stacksjs/orm'
4
4
  import { response } from '@stacksjs/router'
5
+ import { dashboardMailbox, inboxActionError } from './inbox-request'
5
6
  import {
6
- defaultMailbox,
7
7
  type MailPreferenceInput,
8
8
  mailPreferenceAttributes,
9
9
  normalizeStringList,
@@ -132,14 +132,13 @@ export default new Action({
132
132
  apiResponse: true,
133
133
 
134
134
  async handle(request: RequestInstance) {
135
- const mailboxValue = request.get('mailbox')
136
- const mailbox = (mailboxValue === null || mailboxValue === undefined
137
- ? defaultMailbox()
138
- : typeof mailboxValue === 'string' ? mailboxValue : '')
139
- .trim()
140
- .toLowerCase()
141
- if (!/^[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+$/.test(mailbox) || mailbox.length > 320)
142
- return response.json({ message: 'The mail settings are invalid.', fields: { mailbox: 'Enter a valid mailbox.' } }, 422)
135
+ let mailbox: string
136
+ try {
137
+ mailbox = dashboardMailbox(request)
138
+ }
139
+ catch (error) {
140
+ return inboxActionError(error, 'Mail settings could not be updated.')
141
+ }
143
142
 
144
143
  const { fields, input } = inputFromRequest(request, mailbox)
145
144
  if (Object.keys(fields).length)
@@ -1,7 +1,8 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { emailSDK } from '@stacksjs/email'
3
4
  import { response } from '@stacksjs/router'
4
- import { defaultMailbox } from './mail-preference'
5
+ import { dashboardMailbox, inboxActionError } from './inbox-request'
5
6
  import { sanitizeInboxHtml } from './sanitize-inbox-html'
6
7
 
7
8
  export default new Action({
@@ -10,10 +11,10 @@ export default new Action({
10
11
  method: 'GET',
11
12
  apiResponse: true,
12
13
 
13
- async handle(request: any) {
14
+ async handle(request: RequestInstance) {
14
15
  try {
15
- const mailbox = request?.query?.mailbox || defaultMailbox()
16
- const messageId = request?.params?.id
16
+ const mailbox = dashboardMailbox(request)
17
+ const messageId = String(request.getParam('id') || '')
17
18
 
18
19
  if (!messageId)
19
20
  return response.json({ message: 'A message ID is required.' }, 422)
@@ -28,12 +29,11 @@ export default new Action({
28
29
  html: sanitizeInboxHtml(email.html ?? ''),
29
30
  text: email.text ?? '',
30
31
  metadata: email.metadata,
32
+ attachments: email.attachments,
31
33
  }
32
34
  }
33
35
  catch (err) {
34
- return response.json({
35
- message: err instanceof Error ? err.message : 'Email content could not be loaded.',
36
- }, 503)
36
+ return inboxActionError(err, 'Email content could not be loaded.')
37
37
  }
38
38
  },
39
39
  })
@@ -1,7 +1,7 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { emailSDK } from '@stacksjs/email'
3
- import { response } from '@stacksjs/router'
4
- import { defaultMailbox } from './mail-preference'
4
+ import { dashboardMailbox, inboxActionError } from './inbox-request'
5
5
 
6
6
  export default new Action({
7
7
  name: 'InboxStatsAction',
@@ -9,9 +9,9 @@ export default new Action({
9
9
  method: 'GET',
10
10
  apiResponse: true,
11
11
 
12
- async handle(request: any) {
12
+ async handle(request: RequestInstance) {
13
13
  try {
14
- const mailbox = request?.query?.mailbox || defaultMailbox()
14
+ const mailbox = dashboardMailbox(request)
15
15
  const stats = await emailSDK.getInboxStats(mailbox)
16
16
 
17
17
  return {
@@ -20,9 +20,7 @@ export default new Action({
20
20
  }
21
21
  }
22
22
  catch (err) {
23
- return response.json({
24
- message: err instanceof Error ? err.message : 'Inbox statistics could not be loaded.',
25
- }, 503)
23
+ return inboxActionError(err, 'Inbox statistics could not be loaded.')
26
24
  }
27
25
  },
28
26
  })
@@ -0,0 +1,33 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { config } from '@stacksjs/config'
3
+ import { inboxMailboxPath, InvalidInboxMailboxError } from '@stacksjs/email'
4
+ import { response } from '@stacksjs/router'
5
+ import { defaultMailbox } from './mail-preference'
6
+
7
+ function configuredMailDomain(): string {
8
+ const email = (config as any)?.email || {}
9
+ const fromAddress = String(email.from?.address || '').trim()
10
+ const fromDomain = fromAddress.includes('@') ? fromAddress.split('@').at(-1) : ''
11
+ return String(email.domain || fromDomain || 'stacksjs.com').trim().toLowerCase()
12
+ }
13
+
14
+ export function dashboardMailbox(request: RequestInstance): string {
15
+ const value = request.get('mailbox')
16
+ if (value !== null && value !== undefined && typeof value !== 'string')
17
+ throw new InvalidInboxMailboxError()
18
+
19
+ return inboxMailboxPath(value?.trim() || defaultMailbox(), configuredMailDomain()).address
20
+ }
21
+
22
+ export function inboxActionError(error: unknown, fallback: string): Response {
23
+ if (error instanceof InvalidInboxMailboxError) {
24
+ return response.json({
25
+ message: 'Enter a mailbox on the configured email domain.',
26
+ fields: { mailbox: 'Enter a mailbox on the configured email domain.' },
27
+ }, 422)
28
+ }
29
+
30
+ return response.json({
31
+ message: fallback,
32
+ }, 503)
33
+ }
@@ -1,15 +1,14 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { db } from '@stacksjs/database'
4
- import { request as routerRequest, response } from '@stacksjs/router'
4
+ import { response } from '@stacksjs/router'
5
+ import { dashboardRequestValue } from '../dashboard-request'
5
6
  import { DASHBOARD_LOG_TYPES, normalizeDashboardLog, summarizeDashboardLogTypes } from './log-dashboard'
6
7
 
7
8
  const RANGES = ['1', '7', '30', '90', 'all'] as const
8
9
 
9
10
  function queryValue(request: RequestInstance, key: string): string {
10
- const query = ((routerRequest as any).query || {}) as Record<string, string | string[] | undefined>
11
- const value = query[key]
12
- return String((Array.isArray(value) ? value[0] : value) || request.get(key) || '').trim()
11
+ return dashboardRequestValue(request, key)
13
12
  }
14
13
 
15
14
  export default new Action({
@@ -1,3 +1,4 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { response } from '@stacksjs/router'
3
4
  import { tsCloud } from '~/config/cloud'
@@ -9,12 +10,8 @@ export default new Action({
9
10
  description: 'Returns one configured server or persisted deployment.',
10
11
  method: 'GET',
11
12
  apiResponse: true,
12
- async handle(request) {
13
- const identifier = String(
14
- (request as any)?.params?.id
15
- ?? (request as any)?.param?.('id')
16
- ?? '',
17
- )
13
+ async handle(request: RequestInstance) {
14
+ const identifier = request.getParam('id')
18
15
  if (!identifier)
19
16
  return response.json({ error: 'A server identifier is required.' }, 400)
20
17