@stacksjs/defaults 0.70.354 → 0.70.356

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 (214) 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-email/SKILL.md +73 -16
  4. package/app/Actions/ContactAction.ts +2 -2
  5. package/app/Actions/Dashboard/Analytics/EventAnalyticsAction.ts +2 -3
  6. package/app/Actions/Dashboard/Analytics/MarketingAnalyticsAction.ts +2 -3
  7. package/app/Actions/Dashboard/Analytics/SalesAnalyticsAction.ts +2 -3
  8. package/app/Actions/Dashboard/Analytics/WebAnalyticsAction.ts +9 -3
  9. package/app/Actions/Dashboard/Auth/MeAction.ts +12 -14
  10. package/app/Actions/Dashboard/Buddy/BuddyChatAction.ts +2 -3
  11. package/app/Actions/Dashboard/Ci/RepoRunJobsAction.ts +10 -15
  12. package/app/Actions/Dashboard/Ci/RepoRunsAction.ts +12 -14
  13. package/app/Actions/Dashboard/Ci/RunnerHistoryAction.ts +11 -10
  14. package/app/Actions/Dashboard/Ci/StatusAction.ts +2 -11
  15. package/app/Actions/Dashboard/Cloud/CloudIndexAction.ts +8 -1
  16. package/app/Actions/Dashboard/Cloud/ServerlessIndexAction.ts +15 -8
  17. package/app/Actions/Dashboard/Commerce/CommerceCategoriesAction.ts +2 -4
  18. package/app/Actions/Dashboard/Commerce/CommerceCouponsAction.ts +2 -4
  19. package/app/Actions/Dashboard/Commerce/CommerceCustomersAction.ts +2 -4
  20. package/app/Actions/Dashboard/Commerce/CommerceDashboardAction.ts +2 -4
  21. package/app/Actions/Dashboard/Commerce/CommerceDeliveryAction.ts +2 -4
  22. package/app/Actions/Dashboard/Commerce/CommerceGiftCardsAction.ts +2 -4
  23. package/app/Actions/Dashboard/Commerce/CommerceOrdersAction.ts +2 -4
  24. package/app/Actions/Dashboard/Commerce/CommercePaymentsAction.ts +2 -4
  25. package/app/Actions/Dashboard/Commerce/CommercePosAction.ts +2 -4
  26. package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +76 -61
  27. package/app/Actions/Dashboard/Commerce/CommercePrintDevicesAction.ts +2 -4
  28. package/app/Actions/Dashboard/Commerce/CommercePrintLogsAction.ts +4 -7
  29. package/app/Actions/Dashboard/Commerce/CommerceProductDetailAction.ts +4 -4
  30. package/app/Actions/Dashboard/Commerce/CommerceProductsAction.ts +2 -4
  31. package/app/Actions/Dashboard/Commerce/CommerceTaxesAction.ts +2 -4
  32. package/app/Actions/Dashboard/Commerce/DeliveryRouteIndexAction.ts +2 -4
  33. package/app/Actions/Dashboard/Commerce/DigitalDeliveryIndexAction.ts +2 -4
  34. package/app/Actions/Dashboard/Commerce/DriverIndexAction.ts +2 -4
  35. package/app/Actions/Dashboard/Commerce/LicenseKeyIndexAction.ts +2 -4
  36. package/app/Actions/Dashboard/Commerce/LicenseKeyOptionsAction.ts +2 -4
  37. package/app/Actions/Dashboard/Commerce/ManufacturerIndexAction.ts +2 -4
  38. package/app/Actions/Dashboard/Commerce/PaymentRefundAction.ts +1 -0
  39. package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +1 -0
  40. package/app/Actions/Dashboard/Commerce/ProductUnitIndexAction.ts +2 -4
  41. package/app/Actions/Dashboard/Commerce/ProductVariantIndexAction.ts +2 -4
  42. package/app/Actions/Dashboard/Commerce/ProductWaitlistIndexAction.ts +2 -4
  43. package/app/Actions/Dashboard/Commerce/RestaurantWaitlistIndexAction.ts +2 -4
  44. package/app/Actions/Dashboard/Commerce/ReviewIndexAction.ts +2 -4
  45. package/app/Actions/Dashboard/Commerce/ShippingMethodIndexAction.ts +2 -4
  46. package/app/Actions/Dashboard/Commerce/ShippingRateIndexAction.ts +2 -4
  47. package/app/Actions/Dashboard/Commerce/ShippingZoneIndexAction.ts +2 -4
  48. package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +1 -0
  49. package/app/Actions/Dashboard/DashboardActivityAction.ts +2 -5
  50. package/app/Actions/Dashboard/Data/ActivityIndexAction.ts +2 -5
  51. package/app/Actions/Dashboard/Data/SubscriberIndexAction.ts +2 -5
  52. package/app/Actions/Dashboard/Data/TeamIndexAction.ts +2 -5
  53. package/app/Actions/Dashboard/Data/UserIndexAction.ts +2 -5
  54. package/app/Actions/Dashboard/Deployments/CreateDeployment.ts +26 -21
  55. package/app/Actions/Dashboard/Deployments/GetAverageDeploymentTime.ts +8 -2
  56. package/app/Actions/Dashboard/Deployments/GetDeployScript.ts +3 -6
  57. package/app/Actions/Dashboard/Deployments/GetDeployment.ts +5 -5
  58. package/app/Actions/Dashboard/Deployments/GetDeploymentCount.ts +7 -1
  59. package/app/Actions/Dashboard/Deployments/GetDeploymentLiveTerminalOutput.ts +10 -9
  60. package/app/Actions/Dashboard/Deployments/GetDeployments.ts +2 -4
  61. package/app/Actions/Dashboard/Deployments/GetRecentDeployments.ts +8 -2
  62. package/app/Actions/Dashboard/Deployments/UpdateDeployScript.ts +21 -7
  63. package/app/Actions/Dashboard/Email/CapturedMailIndexAction.ts +2 -4
  64. package/app/Actions/Dashboard/Email/CapturedMailShowAction.ts +3 -4
  65. package/app/Actions/Dashboard/Email/InboxActivityAction.ts +28 -16
  66. package/app/Actions/Dashboard/Email/InboxAttachmentDownloadAction.ts +45 -0
  67. package/app/Actions/Dashboard/Email/InboxDeleteAction.ts +11 -6
  68. package/app/Actions/Dashboard/Email/InboxIndexAction.ts +5 -7
  69. package/app/Actions/Dashboard/Email/InboxMarkReadAction.ts +11 -6
  70. package/app/Actions/Dashboard/Email/InboxMarkUnreadAction.ts +11 -6
  71. package/app/Actions/Dashboard/Email/InboxPreferenceShowAction.ts +10 -4
  72. package/app/Actions/Dashboard/Email/InboxPreferenceUpdateAction.ts +8 -9
  73. package/app/Actions/Dashboard/Email/InboxShowAction.ts +7 -7
  74. package/app/Actions/Dashboard/Email/InboxStatsAction.ts +5 -7
  75. package/app/Actions/Dashboard/Email/inbox-request.ts +33 -0
  76. package/app/Actions/Dashboard/Infrastructure/InsightsAction.ts +15 -14
  77. package/app/Actions/Dashboard/Infrastructure/LogIndexAction.ts +4 -11
  78. package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +3 -6
  79. package/app/Actions/Dashboard/Jobs/JobIndexAction.ts +9 -18
  80. package/app/Actions/Dashboard/Jobs/JobRetryAction.ts +4 -3
  81. package/app/Actions/Dashboard/Jobs/JobShowAction.ts +16 -11
  82. package/app/Actions/Dashboard/Jobs/JobStatsAction.ts +2 -4
  83. package/app/Actions/Dashboard/Kanban/BoardDestroyAction.ts +7 -8
  84. package/app/Actions/Dashboard/Kanban/BoardShowAction.ts +11 -19
  85. package/app/Actions/Dashboard/Kanban/BoardStoreAction.ts +6 -6
  86. package/app/Actions/Dashboard/Kanban/BoardUpdateAction.ts +7 -8
  87. package/app/Actions/Dashboard/Kanban/BoardsIndexAction.ts +3 -5
  88. package/app/Actions/Dashboard/Kanban/BoardsReorderAction.ts +6 -6
  89. package/app/Actions/Dashboard/Kanban/CardAssigneesSyncAction.ts +13 -12
  90. package/app/Actions/Dashboard/Kanban/CardCommentDestroyAction.ts +12 -15
  91. package/app/Actions/Dashboard/Kanban/CardCommentStoreAction.ts +21 -46
  92. package/app/Actions/Dashboard/Kanban/CardCommentUpdateAction.ts +43 -0
  93. package/app/Actions/Dashboard/Kanban/CardDestroyAction.ts +7 -8
  94. package/app/Actions/Dashboard/Kanban/CardLabelsSyncAction.ts +8 -9
  95. package/app/Actions/Dashboard/Kanban/CardShowAction.ts +6 -7
  96. package/app/Actions/Dashboard/Kanban/CardStoreAction.ts +9 -9
  97. package/app/Actions/Dashboard/Kanban/CardUpdateAction.ts +7 -8
  98. package/app/Actions/Dashboard/Kanban/CardsReorderAction.ts +6 -6
  99. package/app/Actions/Dashboard/Kanban/ColumnDestroyAction.ts +7 -8
  100. package/app/Actions/Dashboard/Kanban/ColumnStoreAction.ts +6 -6
  101. package/app/Actions/Dashboard/Kanban/ColumnUpdateAction.ts +7 -8
  102. package/app/Actions/Dashboard/Kanban/ColumnsReorderAction.ts +6 -6
  103. package/app/Actions/Dashboard/Kanban/LabelDestroyAction.ts +6 -7
  104. package/app/Actions/Dashboard/Kanban/LabelStoreAction.ts +6 -6
  105. package/app/Actions/Dashboard/Kanban/LabelUpdateAction.ts +7 -8
  106. package/app/Actions/Dashboard/Kanban/UsersListAction.ts +3 -4
  107. package/app/Actions/Dashboard/Kanban/kanban-comment.ts +14 -0
  108. package/app/Actions/Dashboard/Kanban/kanban-model.test.ts +36 -0
  109. package/app/Actions/Dashboard/Kanban/kanban-model.ts +14 -7
  110. package/app/Actions/Dashboard/Kanban/kanban-response.ts +5 -0
  111. package/app/Actions/Dashboard/Library/CreateComponent.ts +15 -6
  112. package/app/Actions/Dashboard/Library/CreateFunction.ts +15 -6
  113. package/app/Actions/Dashboard/Library/DependencyIndexAction.ts +88 -77
  114. package/app/Actions/Dashboard/Library/GetAverageReleaseTime.ts +21 -15
  115. package/app/Actions/Dashboard/Library/GetComponents.ts +14 -7
  116. package/app/Actions/Dashboard/Library/GetDownloadCount.ts +14 -5
  117. package/app/Actions/Dashboard/Library/GetFunctions.ts +12 -5
  118. package/app/Actions/Dashboard/Library/GetReleaseCount.ts +14 -5
  119. package/app/Actions/Dashboard/Library/GetReleases.ts +17 -10
  120. package/app/Actions/Dashboard/Library/PackageIndexAction.ts +14 -7
  121. package/app/Actions/Dashboard/Models/ModelDestroyAction.ts +11 -8
  122. package/app/Actions/Dashboard/Models/ModelShowAction.ts +97 -192
  123. package/app/Actions/Dashboard/Models/ModelStoreAction.ts +14 -12
  124. package/app/Actions/Dashboard/Models/ModelUpdateAction.ts +18 -16
  125. package/app/Actions/Dashboard/Models/ModelsIndexAction.ts +50 -19
  126. package/app/Actions/Dashboard/Models/model-api.ts +27 -0
  127. package/app/Actions/Dashboard/Models/model-write.ts +22 -15
  128. package/app/Actions/Dashboard/Notifications/NotificationDeliveryHistoryAction.ts +49 -40
  129. package/app/Actions/Dashboard/Notifications/NotificationDeliveryIndexAction.ts +20 -10
  130. package/app/Actions/Dashboard/Notifications/NotificationDeliveryOverviewAction.ts +96 -84
  131. package/app/Actions/Dashboard/Notifications/NotificationDeliveryRetryAction.ts +34 -21
  132. package/app/Actions/Dashboard/Queries/QueryDashboardAction.ts +2 -4
  133. package/app/Actions/Dashboard/Queries/QueryShowAction.ts +3 -3
  134. package/app/Actions/Dashboard/Queue/QueueStatsAction.ts +2 -4
  135. package/app/Actions/Dashboard/Queue/QueueWorkersAction.ts +2 -4
  136. package/app/Actions/Dashboard/Rbac/PermissionDestroyAction.ts +6 -6
  137. package/app/Actions/Dashboard/Rbac/PermissionStoreAction.ts +5 -4
  138. package/app/Actions/Dashboard/Rbac/PermissionsIndexAction.ts +2 -3
  139. package/app/Actions/Dashboard/Rbac/RoleDestroyAction.ts +6 -6
  140. package/app/Actions/Dashboard/Rbac/RolePermissionsShowAction.ts +6 -6
  141. package/app/Actions/Dashboard/Rbac/RolePermissionsSyncAction.ts +6 -9
  142. package/app/Actions/Dashboard/Rbac/RoleStoreAction.ts +5 -4
  143. package/app/Actions/Dashboard/Rbac/RolesIndexAction.ts +4 -6
  144. package/app/Actions/Dashboard/Rbac/UserRolesShowAction.ts +6 -7
  145. package/app/Actions/Dashboard/Rbac/UserRolesSyncAction.ts +6 -13
  146. package/app/Actions/Dashboard/Rbac/UsersListAction.ts +2 -3
  147. package/app/Actions/Dashboard/Rbac/rbac-response.ts +10 -0
  148. package/app/Actions/Dashboard/Realtime/RealtimeStatsAction.ts +2 -4
  149. package/app/Actions/Dashboard/Search/GlobalSearchAction.ts +69 -55
  150. package/app/Actions/Dashboard/Teams/TeamInvitationDestroyAction.ts +34 -16
  151. package/app/Actions/Dashboard/Teams/TeamInvitationResendAction.ts +52 -29
  152. package/app/Actions/Dashboard/Teams/TeamInviteAction.ts +104 -73
  153. package/app/Actions/Dashboard/Teams/TeamMemberDestroyAction.ts +36 -17
  154. package/app/Actions/Dashboard/Teams/TeamMemberUpdateAction.ts +41 -28
  155. package/app/Actions/Dashboard/Teams/TeamPeopleIndexAction.ts +71 -58
  156. package/app/Actions/Dashboard/Teams/team-invitation-delivery.ts +29 -7
  157. package/app/Actions/Dashboard/Teams/team-member-count.ts +4 -2
  158. package/app/Actions/Dashboard/Teams/team-records.test.ts +2 -0
  159. package/app/Actions/Dashboard/Teams/team-records.ts +2 -0
  160. package/app/Actions/Dashboard/dashboard-request.ts +13 -0
  161. package/app/Actions/Dashboard/dashboard-response.ts +16 -0
  162. package/app/Actions/SendWelcomeEmail.ts +1 -1
  163. package/app/Actions/Teams/AcceptInvitationAction.ts +74 -51
  164. package/app/Actions/Teams/ShowInvitationAction.ts +21 -14
  165. package/app/Actions/Teams/team-response.ts +23 -0
  166. package/app/Mail/OrderConfirmation.ts +1 -1
  167. package/app/Mail/SubscriptionConfirmation.ts +1 -1
  168. package/app/Mail/TeamInvitation.ts +1 -1
  169. package/app/Models/CardComment.ts +3 -4
  170. package/app/Models/EmailIdempotency.ts +64 -0
  171. package/app/Models/EmailSuppression.ts +52 -0
  172. package/app/Models/EmailWebhookEvent.ts +51 -0
  173. package/app/Models/TeamInvitation.ts +13 -0
  174. package/functions/dashboard-api.ts +51 -10
  175. package/functions/data.ts +0 -1
  176. package/functions/inbox.test.ts +2 -0
  177. package/functions/inbox.ts +27 -2
  178. package/functions/jobs.ts +0 -1
  179. package/functions/public-application-url.test.ts +13 -0
  180. package/functions/public-application-url.ts +27 -0
  181. package/ide/vscode/package.json +1 -1
  182. package/package.json +1 -1
  183. package/resources/components/Dashboard/Auth/AccessTokens.stx +15 -0
  184. package/resources/components/Dashboard/Ci/CiDashboard.stx +24 -6
  185. package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +1 -1
  186. package/resources/components/Dashboard/Commerce/ProductWaitlistDashboard.stx +8 -7
  187. package/resources/components/Dashboard/Commerce/RestaurantWaitlistDashboard.stx +8 -7
  188. package/resources/components/Dashboard/Content/BlogDashboard.stx +14 -2
  189. package/resources/components/Dashboard/Content/PostsDashboard.stx +7 -1
  190. package/resources/components/Dashboard/Data/DataRecords.stx +0 -4
  191. package/resources/components/Dashboard/Deployments/DeploymentDetail.stx +16 -2
  192. package/resources/components/Dashboard/Email/EmailNavigation.stx +5 -5
  193. package/resources/components/Dashboard/Email/EmailSidebar.stx +1 -1
  194. package/resources/components/Dashboard/Email/InboxAttachmentList.stx +53 -0
  195. package/resources/components/Dashboard/Email/InboxDashboard.stx +5 -4
  196. package/resources/components/Dashboard/Email/InboxMessageDetail.stx +6 -4
  197. package/resources/components/Dashboard/Jobs/JobDetail.stx +3 -2
  198. package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +63 -6
  199. package/resources/components/Dashboard/Kanban/KanbanBoardsDashboard.stx +3 -6
  200. package/resources/components/Dashboard/Management/PermissionsDashboard.stx +4 -4
  201. package/resources/components/Dashboard/Models/ModelRecordsDashboard.stx +7 -13
  202. package/resources/components/Dashboard/Models/ModelsOverview.stx +1 -1
  203. package/resources/components/Dashboard/NotFoundDashboard.stx +2 -5
  204. package/resources/components/Dashboard/Queries/QueryDashboard.stx +6 -2
  205. package/resources/components/Dashboard/Settings/AppearanceSettingsDashboard.stx +4 -4
  206. package/resources/components/Dashboard/UI/EmptyState.stx +2 -2
  207. package/resources/components/Dashboard/UI/PageLayout.stx +3 -3
  208. package/resources/functions/dashboard/sidebar.ts +4 -4
  209. package/routes/dashboard-api.ts +8 -20
  210. package/views/dashboard/.discovered-models.json +54 -0
  211. package/views/dashboard/layouts/default.stx +19 -16
  212. package/views/dashboard/stores/auth.ts +15 -14
  213. package/views/dashboard/stores/ci.ts +13 -7
  214. package/views/dashboard/stores/kanban.ts +48 -11
@@ -1,7 +1,8 @@
1
1
  import { config } from '@stacksjs/config'
2
2
  import { db } from '@stacksjs/database'
3
3
  import { sendTeamInvitation } from '../../../Mail/TeamInvitation'
4
- import { invitationUrl, sqlTimestamp } from './team-records'
4
+ import { teamOperationalIssue } from '../../Teams/team-response'
5
+ import { changedRows, invitationUrl, sqlTimestamp } from './team-records'
5
6
 
6
7
  interface DeliveryInput {
7
8
  id: number
@@ -9,12 +10,22 @@ interface DeliveryInput {
9
10
  teamName: string
10
11
  role: string
11
12
  token: string
13
+ tokenHash: string
14
+ }
15
+
16
+ async function recordDeliveryFailure(input: DeliveryInput, message: string): Promise<void> {
17
+ try {
18
+ await markTeamInvitationDeliveryFailed(input.id, input.tokenHash, message)
19
+ }
20
+ catch (error) {
21
+ teamOperationalIssue(error, 'Invitation delivery failure could not be recorded.', 'deliverTeamInvitation.failureStatus')
22
+ }
12
23
  }
13
24
 
14
25
  export async function deliverTeamInvitation(input: DeliveryInput): Promise<void> {
15
26
  const baseUrl = String(process.env.APP_URL || config.app.url || '').trim()
16
27
  if (!baseUrl) {
17
- await markTeamInvitationDeliveryFailed(input.id, 'APP_URL is not configured.')
28
+ await recordDeliveryFailure(input, 'APP_URL is not configured.')
18
29
  throw new Error('APP_URL must be configured before invitations can be delivered.')
19
30
  }
20
31
 
@@ -26,7 +37,15 @@ export async function deliverTeamInvitation(input: DeliveryInput): Promise<void>
26
37
  invitationUrl: invitationUrl(baseUrl, input.token),
27
38
  expiresInDays: 7,
28
39
  })
29
- await (db as any)
40
+ }
41
+ catch (error) {
42
+ const message = teamOperationalIssue(error, 'Email delivery failed.', 'deliverTeamInvitation.provider')
43
+ await recordDeliveryFailure(input, message)
44
+ throw error
45
+ }
46
+
47
+ try {
48
+ const result = await (db as any)
30
49
  .updateTable('team_invitations')
31
50
  .set({
32
51
  delivery_status: 'sent',
@@ -35,16 +54,18 @@ export async function deliverTeamInvitation(input: DeliveryInput): Promise<void>
35
54
  updated_at: sqlTimestamp(),
36
55
  })
37
56
  .where('id', '=', input.id)
38
- .execute()
57
+ .where('token_hash', '=', input.tokenHash)
58
+ .executeTakeFirst()
59
+ if (changedRows(result) !== 1)
60
+ throw new Error('Invitation delivery status changed before it could be recorded.')
39
61
  }
40
62
  catch (error) {
41
- const message = error instanceof Error ? error.message : 'Email delivery failed.'
42
- await markTeamInvitationDeliveryFailed(input.id, message)
63
+ teamOperationalIssue(error, 'Invitation delivery success could not be recorded.', 'deliverTeamInvitation.successStatus')
43
64
  throw error
44
65
  }
45
66
  }
46
67
 
47
- export async function markTeamInvitationDeliveryFailed(id: number, error: string): Promise<void> {
68
+ export async function markTeamInvitationDeliveryFailed(id: number, tokenHash: string, error: string): Promise<void> {
48
69
  await (db as any)
49
70
  .updateTable('team_invitations')
50
71
  .set({
@@ -53,5 +74,6 @@ export async function markTeamInvitationDeliveryFailed(id: number, error: string
53
74
  updated_at: sqlTimestamp(),
54
75
  })
55
76
  .where('id', '=', id)
77
+ .where('token_hash', '=', tokenHash)
56
78
  .execute()
57
79
  }
@@ -1,4 +1,4 @@
1
- import { db } from '@stacksjs/database'
1
+ import { db, sql } from '@stacksjs/database'
2
2
  import { sqlTimestamp } from './team-records'
3
3
 
4
4
  export async function syncTeamMemberCount(teamId: number, connection: typeof db = db): Promise<number> {
@@ -6,9 +6,11 @@ export async function syncTeamMemberCount(teamId: number, connection: typeof db
6
6
  .selectFrom('team_members')
7
7
  .where('team_id', '=', teamId)
8
8
  .where('status', '=', 'active')
9
- .select(connection.fn.count('id').as('count'))
9
+ .select(sql`count(*) as count`)
10
10
  .executeTakeFirst()
11
11
  const count = Number(row?.count || 0)
12
+ if (!Number.isSafeInteger(count) || count < 0)
13
+ throw new TypeError('Team member count must be a non-negative integer.')
12
14
 
13
15
  await connection
14
16
  .updateTable('teams')
@@ -25,6 +25,8 @@ describe('team records', () => {
25
25
  expect(changedRows({ changes: 1 })).toBe(1)
26
26
  expect(changedRows({ affectedRows: 2 })).toBe(2)
27
27
  expect(changedRows([{ numUpdatedRows: 3n }])).toBe(3)
28
+ expect(changedRows({ numDeletedRows: 4n })).toBe(4)
29
+ expect(changedRows({ numInsertedRows: 5n })).toBe(5)
28
30
  expect(changedRows(null)).toBe(0)
29
31
  })
30
32
 
@@ -24,6 +24,8 @@ export function changedRows(result: unknown): number {
24
24
  ?? record.affectedRows
25
25
  ?? record.numAffectedRows
26
26
  ?? record.numUpdatedRows
27
+ ?? record.numDeletedRows
28
+ ?? record.numInsertedRows
27
29
  ?? 0,
28
30
  )
29
31
  }
@@ -0,0 +1,13 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+
3
+ /** Reads one scalar dashboard input value through the native request bag. */
4
+ export function dashboardRequestValue(
5
+ request: RequestInstance,
6
+ key: string,
7
+ fallback = '',
8
+ ): string {
9
+ const value = request.get<unknown>(key, fallback)
10
+ if (Array.isArray(value))
11
+ return value.length > 0 ? String(value[0]).trim() : fallback
12
+ return value == null ? fallback : String(value).trim()
13
+ }
@@ -0,0 +1,16 @@
1
+ import { response } from '@stacksjs/router'
2
+
3
+ export function dashboardOperationalError(
4
+ error: unknown,
5
+ message: string,
6
+ action: string,
7
+ status = 503,
8
+ ): Response {
9
+ console.error(`[dashboard/api] ${action} failed:`, error)
10
+ return response.json({ message }, status)
11
+ }
12
+
13
+ export function dashboardOperationalIssue(error: unknown, message: string, action: string): string {
14
+ console.error(`[dashboard/api] ${action} failed:`, error)
15
+ return message
16
+ }
@@ -15,7 +15,7 @@ export default new Action({
15
15
  const appName = config.app.name || 'our app'
16
16
  const url = config.app.url || 'https://localhost:5173'
17
17
 
18
- await mail.send({
18
+ await mail.sendOrFail({
19
19
  to,
20
20
  subject: `Welcome to ${appName}!`,
21
21
  text: `
@@ -13,6 +13,7 @@ import {
13
13
  sqlTimestamp,
14
14
  } from '../Dashboard/Teams/team-records'
15
15
  import { syncTeamMemberCount } from '../Dashboard/Teams/team-member-count'
16
+ import { teamOperationalError } from './team-response'
16
17
 
17
18
  export default new Action({
18
19
  name: 'Accept Team Invitation',
@@ -25,23 +26,38 @@ export default new Action({
25
26
  if (!token || token.length > 200)
26
27
  return response.json({ message: 'Invitation not found.' }, 404)
27
28
 
28
- const authenticated = await resolveAuthenticatedUser(request as any)
29
+ let authenticated
30
+ try {
31
+ authenticated = await resolveAuthenticatedUser(request as any)
32
+ }
33
+ catch (error) {
34
+ return teamOperationalError(error, 'Authentication could not be verified.', 'AcceptInvitationAction.auth')
35
+ }
29
36
  if (!authenticated?.id)
30
37
  return response.json({ message: 'Sign in to accept this invitation.' }, 401)
31
38
 
32
- const user = await db
33
- .selectFrom('users')
34
- .where('id', '=', authenticated.id)
35
- .select(['id', 'email'])
36
- .executeTakeFirst()
39
+ const tokenHash = hashInvitationToken(token)
40
+ let user
41
+ let invitation
42
+ try {
43
+ [user, invitation] = await Promise.all([
44
+ db
45
+ .selectFrom('users')
46
+ .where('id', '=', authenticated.id)
47
+ .select(['id', 'email'])
48
+ .executeTakeFirst(),
49
+ (db as any)
50
+ .selectFrom('team_invitations')
51
+ .where('token_hash', '=', tokenHash)
52
+ .select(['id', 'team_id', 'email', 'role', 'status', 'expires_at'])
53
+ .executeTakeFirst(),
54
+ ])
55
+ }
56
+ catch (error) {
57
+ return teamOperationalError(error, 'Invitation acceptance could not be prepared.', 'AcceptInvitationAction.lookup')
58
+ }
37
59
  if (!user)
38
60
  return response.json({ message: 'Authenticated user not found.' }, 401)
39
-
40
- const invitation = await (db as any)
41
- .selectFrom('team_invitations')
42
- .where('token_hash', '=', hashInvitationToken(token))
43
- .select(['id', 'team_id', 'email', 'role', 'status', 'expires_at'])
44
- .executeTakeFirst()
45
61
  if (!invitation)
46
62
  return response.json({ message: 'Invitation not found.' }, 404)
47
63
 
@@ -58,49 +74,56 @@ export default new Action({
58
74
  const userId = Number(user.id)
59
75
  const now = sqlTimestamp()
60
76
 
61
- const accepted = await db.transaction(async (rawTrx) => {
62
- const trx = rawTrx as unknown as typeof db
63
- const claim = await (trx as any)
64
- .updateTable('team_invitations')
65
- .set({
66
- status: 'accepted',
67
- token_hash: hashInvitationToken(generateInvitationToken()),
68
- accepted_by_user_id: userId,
69
- accepted_at: now,
70
- updated_at: now,
71
- })
72
- .where('id', '=', Number(invitation.id))
73
- .where('token_hash', '=', hashInvitationToken(token))
74
- .where('status', '=', 'pending')
75
- .executeTakeFirst()
77
+ let accepted
78
+ try {
79
+ accepted = await db.transaction(async (rawTrx) => {
80
+ const trx = rawTrx as unknown as typeof db
81
+ const claim = await (trx as any)
82
+ .updateTable('team_invitations')
83
+ .set({
84
+ status: 'accepted',
85
+ token_hash: hashInvitationToken(generateInvitationToken()),
86
+ pending_key: null,
87
+ accepted_by_user_id: userId,
88
+ accepted_at: now,
89
+ updated_at: now,
90
+ })
91
+ .where('id', '=', Number(invitation.id))
92
+ .where('token_hash', '=', tokenHash)
93
+ .where('status', '=', 'pending')
94
+ .executeTakeFirst()
76
95
 
77
- if (changedRows(claim) !== 1)
78
- return null
96
+ if (changedRows(claim) !== 1)
97
+ return null
79
98
 
80
- const existing = await (trx as any)
81
- .selectFrom('team_members')
82
- .where('team_id', '=', teamId)
83
- .where('user_id', '=', userId)
84
- .select(['id'])
85
- .executeTakeFirst()
99
+ const existing = await (trx as any)
100
+ .selectFrom('team_members')
101
+ .where('team_id', '=', teamId)
102
+ .where('user_id', '=', userId)
103
+ .select(['id'])
104
+ .executeTakeFirst()
86
105
 
87
- if (!existing) {
88
- await (trx as any)
89
- .insertInto('team_members')
90
- .values({
91
- team_id: teamId,
92
- user_id: userId,
93
- role: String(invitation.role),
94
- status: 'active',
95
- created_at: now,
96
- uuid: randomUUID(),
97
- })
98
- .execute()
99
- }
106
+ if (!existing) {
107
+ await (trx as any)
108
+ .insertInto('team_members')
109
+ .values({
110
+ team_id: teamId,
111
+ user_id: userId,
112
+ role: String(invitation.role),
113
+ status: 'active',
114
+ created_at: now,
115
+ uuid: randomUUID(),
116
+ })
117
+ .execute()
118
+ }
100
119
 
101
- await syncTeamMemberCount(teamId, trx)
102
- return { alreadyMember: Boolean(existing) }
103
- })
120
+ await syncTeamMemberCount(teamId, trx)
121
+ return { alreadyMember: Boolean(existing) }
122
+ })
123
+ }
124
+ catch (error) {
125
+ return teamOperationalError(error, 'The invitation could not be accepted.', 'AcceptInvitationAction.accept', 500)
126
+ }
104
127
 
105
128
  if (!accepted)
106
129
  return response.json({ message: 'This invitation is no longer available.' }, 409)
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
3
3
  import { db } from '@stacksjs/database'
4
4
  import { response } from '@stacksjs/router'
5
5
  import { hashInvitationToken, invitationStatus } from '../Dashboard/Teams/team-records'
6
+ import { teamOperationalError } from './team-response'
6
7
 
7
8
  export default new Action({
8
9
  name: 'Show Team Invitation',
@@ -15,20 +16,26 @@ export default new Action({
15
16
  if (!token || token.length > 200)
16
17
  return response.json({ message: 'Invitation not found.' }, 404)
17
18
 
18
- const invitation = await (db as any)
19
- .selectFrom('team_invitations')
20
- .innerJoin('teams', 'teams.id', '=', 'team_invitations.team_id')
21
- .where('team_invitations.token_hash', '=', hashInvitationToken(token))
22
- .select([
23
- 'team_invitations.id as id',
24
- 'team_invitations.team_id as team_id',
25
- 'team_invitations.email as email',
26
- 'team_invitations.role as role',
27
- 'team_invitations.status as status',
28
- 'team_invitations.expires_at as expires_at',
29
- 'teams.name as team_name',
30
- ])
31
- .executeTakeFirst()
19
+ let invitation
20
+ try {
21
+ invitation = await (db as any)
22
+ .selectFrom('team_invitations')
23
+ .innerJoin('teams', 'teams.id', '=', 'team_invitations.team_id')
24
+ .where('team_invitations.token_hash', '=', hashInvitationToken(token))
25
+ .select([
26
+ 'team_invitations.id as id',
27
+ 'team_invitations.team_id as team_id',
28
+ 'team_invitations.email as email',
29
+ 'team_invitations.role as role',
30
+ 'team_invitations.status as status',
31
+ 'team_invitations.expires_at as expires_at',
32
+ 'teams.name as team_name',
33
+ ])
34
+ .executeTakeFirst()
35
+ }
36
+ catch (error) {
37
+ return teamOperationalError(error, 'Invitation details could not be loaded.', 'ShowInvitationAction')
38
+ }
32
39
  if (!invitation)
33
40
  return response.json({ message: 'Invitation not found.' }, 404)
34
41
 
@@ -0,0 +1,23 @@
1
+ import { response } from '@stacksjs/router'
2
+
3
+ export class TeamStateConflictError extends Error {
4
+ constructor(message: string) {
5
+ super(message)
6
+ this.name = 'TeamStateConflictError'
7
+ }
8
+ }
9
+
10
+ export function teamOperationalError(
11
+ error: unknown,
12
+ message: string,
13
+ action: string,
14
+ status = 503,
15
+ ): Response {
16
+ console.error(`[teams/api] ${action} failed:`, error)
17
+ return response.json({ message }, status)
18
+ }
19
+
20
+ export function teamOperationalIssue(error: unknown, message: string, action: string): string {
21
+ console.error(`[teams/api] ${action} failed:`, error)
22
+ return message
23
+ }
@@ -48,7 +48,7 @@ export async function sendOrderConfirmation(options: OrderConfirmationOptions):
48
48
  subject: `Your ${appName} order #${options.orderId} is confirmed`,
49
49
  })
50
50
 
51
- await mail.send({
51
+ await mail.sendOrFail({
52
52
  to: [options.to],
53
53
  from: { name: appName, address: fromAddress },
54
54
  subject: `Your ${appName} order #${options.orderId} is confirmed`,
@@ -26,7 +26,7 @@ export async function sendSubscriptionConfirmation(options: SubscriptionConfirma
26
26
  subject: `Welcome to ${appName}!`,
27
27
  })
28
28
 
29
- await mail.send({
29
+ await mail.sendOrFail({
30
30
  to: [to],
31
31
  from: {
32
32
  name: config.email.from?.name || appName,
@@ -23,7 +23,7 @@ export async function sendTeamInvitation(options: TeamInvitationMailOptions): Pr
23
23
  subject,
24
24
  })
25
25
 
26
- await mail.send({
26
+ await mail.sendOrFail({
27
27
  to: [options.to],
28
28
  from: {
29
29
  name: config.email.from?.name || appName,
@@ -2,13 +2,12 @@ import { defineModel } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  /**
5
- * Card comment (stacksjs/stacks#1846 Phase 3).
5
+ * Card comment for the dashboard Kanban surface.
6
6
  *
7
7
  * Each comment belongs to a card and (optionally) to the user who
8
8
  * wrote it. Anonymous comments are allowed via nullable `user_id` —
9
9
  * makes the dev dashboard usable on localhost without an auth round-
10
- * trip, and supports system-generated activity entries
11
- * (eventually — Phase 4 could write status-change comments here).
10
+ * trip, and supports system-generated activity entries.
12
11
  *
13
12
  * Distinct from `Comment` (which is post-specific) so the schemas
14
13
  * don't fight: post comments carry `author_name`/`author_email` for
@@ -35,7 +34,7 @@ export default defineModel({
35
34
 
36
35
  useApi: {
37
36
  uri: 'card-comments',
38
- routes: ['index', 'store', 'show', 'destroy'],
37
+ routes: ['index', 'store', 'show', 'update', 'destroy'],
39
38
  middleware: ['auth'],
40
39
  },
41
40
  },
@@ -0,0 +1,64 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ export default defineModel({
5
+ name: 'EmailIdempotency',
6
+ table: 'email_idempotency',
7
+ primaryKey: 'id',
8
+ autoIncrement: true,
9
+
10
+ traits: {
11
+ useTimestamps: true,
12
+ useApi: {
13
+ uri: 'email-idempotency',
14
+ routes: ['index', 'show', 'destroy'],
15
+ middleware: ['auth'],
16
+ },
17
+ },
18
+
19
+ attributes: {
20
+ idempotencyKey: {
21
+ required: true,
22
+ hidden: true,
23
+ unique: true,
24
+ validation: {
25
+ rule: schema.string().required().max(255),
26
+ },
27
+ },
28
+ messageId: {
29
+ required: false,
30
+ validation: {
31
+ rule: schema.string().max(512),
32
+ },
33
+ },
34
+ recipient: {
35
+ required: false,
36
+ hidden: true,
37
+ validation: {
38
+ rule: schema.string().max(2000),
39
+ },
40
+ },
41
+ subject: {
42
+ required: false,
43
+ hidden: true,
44
+ validation: {
45
+ rule: schema.string().max(998),
46
+ },
47
+ },
48
+ provider: {
49
+ required: false,
50
+ validation: {
51
+ rule: schema.string().max(100),
52
+ },
53
+ },
54
+ success: {
55
+ required: true,
56
+ default: true,
57
+ validation: {
58
+ rule: schema.boolean(),
59
+ },
60
+ },
61
+ },
62
+
63
+ dashboard: { enabled: false },
64
+ } as const)
@@ -0,0 +1,52 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ export default defineModel({
5
+ name: 'EmailSuppression',
6
+ table: 'email_suppressions',
7
+ primaryKey: 'id',
8
+ autoIncrement: true,
9
+
10
+ indexes: [
11
+ {
12
+ name: 'email_suppressions_email_type_unique',
13
+ columns: ['email', 'type'],
14
+ unique: true,
15
+ },
16
+ ],
17
+
18
+ traits: {
19
+ useTimestamps: true,
20
+ useApi: {
21
+ uri: 'email-suppressions',
22
+ routes: ['index', 'show', 'destroy'],
23
+ middleware: ['auth'],
24
+ },
25
+ },
26
+
27
+ attributes: {
28
+ email: {
29
+ required: true,
30
+ fillable: true,
31
+ validation: {
32
+ rule: schema.string().email().required().max(320),
33
+ },
34
+ },
35
+ type: {
36
+ required: true,
37
+ fillable: true,
38
+ validation: {
39
+ rule: schema.enum(['bounce', 'complaint', 'unsubscribe', 'manual']),
40
+ },
41
+ },
42
+ reason: {
43
+ required: false,
44
+ fillable: true,
45
+ validation: {
46
+ rule: schema.string().max(2000),
47
+ },
48
+ },
49
+ },
50
+
51
+ dashboard: { enabled: false },
52
+ } as const)
@@ -0,0 +1,51 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ export default defineModel({
5
+ name: 'EmailWebhookEvent',
6
+ table: 'email_webhook_events',
7
+ primaryKey: 'id',
8
+ autoIncrement: true,
9
+
10
+ indexes: [
11
+ {
12
+ name: 'email_webhook_events_provider_event_unique',
13
+ columns: ['provider', 'event_id'],
14
+ unique: true,
15
+ },
16
+ ],
17
+
18
+ traits: {
19
+ useTimestamps: true,
20
+ useApi: {
21
+ uri: 'email-webhook-events',
22
+ routes: ['index', 'show', 'destroy'],
23
+ middleware: ['auth'],
24
+ },
25
+ },
26
+
27
+ attributes: {
28
+ provider: {
29
+ required: true,
30
+ fillable: true,
31
+ validation: {
32
+ rule: schema.enum(['mailgun', 'postmark', 'ses', 'sendgrid']),
33
+ },
34
+ },
35
+ eventId: {
36
+ required: true,
37
+ hidden: true,
38
+ validation: {
39
+ rule: schema.string().required().max(512),
40
+ },
41
+ },
42
+ processedAt: {
43
+ required: true,
44
+ validation: {
45
+ rule: schema.timestamp().required(),
46
+ },
47
+ },
48
+ },
49
+
50
+ dashboard: { enabled: false },
51
+ } as const)
@@ -20,6 +20,11 @@ export default defineModel({
20
20
  columns: ['token_hash'],
21
21
  unique: true,
22
22
  },
23
+ {
24
+ name: 'team_invitations_pending_key_unique',
25
+ columns: ['pending_key'],
26
+ unique: true,
27
+ },
23
28
  {
24
29
  name: 'team_invitations_team_email_status_index',
25
30
  columns: ['team_id', 'email', 'status'],
@@ -79,6 +84,14 @@ export default defineModel({
79
84
  },
80
85
  },
81
86
 
87
+ pendingKey: {
88
+ required: false,
89
+ hidden: true,
90
+ validation: {
91
+ rule: schema.string().max(384),
92
+ },
93
+ },
94
+
82
95
  invitedByUserId: {
83
96
  required: false,
84
97
  validation: {