@stacksjs/defaults 0.72.6 → 0.72.7

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 (98) hide show
  1. package/app/Actions/Auth/GenerateAuthenticationAction.ts +6 -6
  2. package/app/Actions/Auth/GenerateRegistrationAction.ts +10 -5
  3. package/app/Actions/Auth/RegisterAction.ts +5 -2
  4. package/app/Actions/Auth/VerifyAuthenticationAction.ts +5 -3
  5. package/app/Actions/Auth/VerifyRegistrationAction.ts +7 -3
  6. package/app/Actions/Billable/CheckoutAction.ts +14 -1
  7. package/app/Actions/Cms/AuthorDestroyAction.ts +1 -1
  8. package/app/Actions/Cms/AuthorShowAction.ts +1 -1
  9. package/app/Actions/Cms/AuthorUpdateAction.ts +1 -1
  10. package/app/Actions/Cms/CategorizableDestroyAction.ts +1 -1
  11. package/app/Actions/Cms/CategorizableShowAction.ts +1 -1
  12. package/app/Actions/Cms/CategorizableUpdateAction.ts +2 -3
  13. package/app/Actions/Cms/CommentDestroyAction.ts +1 -1
  14. package/app/Actions/Cms/CommentShowAction.ts +1 -1
  15. package/app/Actions/Cms/CommentUpdateAction.ts +1 -1
  16. package/app/Actions/Cms/PageDestroyAction.ts +1 -1
  17. package/app/Actions/Cms/PageStoreAction.ts +13 -13
  18. package/app/Actions/Cms/PageUpdateAction.ts +11 -8
  19. package/app/Actions/Cms/PostDestroyAction.ts +1 -1
  20. package/app/Actions/Cms/PostShowAction.ts +1 -1
  21. package/app/Actions/Cms/PostUpdateAction.ts +1 -1
  22. package/app/Actions/Cms/TaggableDestroyAction.ts +1 -1
  23. package/app/Actions/Cms/TaggableShowAction.ts +1 -1
  24. package/app/Actions/Cms/TaggableUpdateAction.ts +2 -2
  25. package/app/Actions/Commerce/GiftCardUpdateBalanceAction.ts +1 -1
  26. package/app/Actions/Commerce/WaitlistProductAnalyticsAction.ts +6 -2
  27. package/app/Actions/Commerce/WaitlistProductQuantityDistributionAction.ts +6 -2
  28. package/app/Actions/Commerce/WaitlistProductStatusAction.ts +8 -2
  29. package/app/Actions/Commerce/WaitlistProductTimeSeriesAction.ts +6 -2
  30. package/app/Actions/Dashboard/Analytics/campaign-analytics.ts +6 -1
  31. package/app/Actions/Dashboard/Analytics/request-analytics.ts +3 -3
  32. package/app/Actions/Dashboard/Buddy/buddy-chat.ts +8 -0
  33. package/app/Actions/Dashboard/Ci/runner-pressure-monitor.ts +2 -1
  34. package/app/Actions/Dashboard/Cloud/cloud-overview.ts +3 -1
  35. package/app/Actions/Dashboard/Commerce/CommerceDashboardAction.ts +12 -4
  36. package/app/Actions/Dashboard/Commerce/CommerceOrdersAction.ts +1 -1
  37. package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +2 -2
  38. package/app/Actions/Dashboard/Commerce/CommercePrintLogsAction.ts +1 -1
  39. package/app/Actions/Dashboard/Commerce/CommerceProductDetailAction.ts +2 -2
  40. package/app/Actions/Dashboard/Commerce/CommerceProductsAction.ts +5 -3
  41. package/app/Actions/Dashboard/Commerce/DeliveryRouteIndexAction.ts +1 -1
  42. package/app/Actions/Dashboard/Commerce/DriverIndexAction.ts +1 -1
  43. package/app/Actions/Dashboard/Commerce/LicenseKeyIndexAction.ts +3 -3
  44. package/app/Actions/Dashboard/Commerce/PaymentRefundAction.ts +4 -4
  45. package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +1 -1
  46. package/app/Actions/Dashboard/Commerce/ShippingMethodIndexAction.ts +1 -1
  47. package/app/Actions/Dashboard/Commerce/ShippingRateIndexAction.ts +2 -2
  48. package/app/Actions/Dashboard/Commerce/ShippingZoneIndexAction.ts +1 -1
  49. package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +1 -1
  50. package/app/Actions/Dashboard/Commerce/commerce-dashboard.ts +20 -9
  51. package/app/Actions/Dashboard/Content/file-manager.ts +3 -2
  52. package/app/Actions/Dashboard/DashboardHomeAction.ts +1 -1
  53. package/app/Actions/Dashboard/DashboardStatsAction.ts +7 -4
  54. package/app/Actions/Dashboard/Deployments/CreateDeploymentRollback.ts +2 -2
  55. package/app/Actions/Dashboard/Deployments/deployment-input.ts +3 -3
  56. package/app/Actions/Dashboard/Deployments/deployment-rollback.ts +12 -0
  57. package/app/Actions/Dashboard/Infrastructure/InsightsAction.ts +3 -3
  58. package/app/Actions/Dashboard/Jobs/job-records.ts +3 -3
  59. package/app/Actions/Dashboard/Kanban/BoardStoreAction.ts +5 -3
  60. package/app/Actions/Dashboard/Kanban/CardStoreAction.ts +5 -3
  61. package/app/Actions/Dashboard/Kanban/CardsReorderAction.ts +1 -1
  62. package/app/Actions/Dashboard/Kanban/kanban-model.ts +3 -3
  63. package/app/Actions/Dashboard/Kanban/kanban-response.ts +2 -1
  64. package/app/Actions/Dashboard/Library/DependencyIndexAction.ts +1 -0
  65. package/app/Actions/Dashboard/Library/GetReleases.ts +8 -3
  66. package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +3 -3
  67. package/app/Actions/Dashboard/Marketing/SocialPostStoreAction.ts +7 -0
  68. package/app/Actions/Dashboard/Marketing/SocialPostUpdateAction.ts +5 -1
  69. package/app/Actions/Dashboard/Models/ModelsIndexAction.ts +12 -9
  70. package/app/Actions/Dashboard/Operations/control-plane.ts +16 -4
  71. package/app/Actions/Dashboard/Queries/QueryDashboardAction.ts +1 -1
  72. package/app/Actions/Dashboard/Queue/QueueStatsAction.ts +23 -14
  73. package/app/Actions/Dashboard/Search/GlobalSearchAction.ts +1 -1
  74. package/app/Actions/Dashboard/Settings/mail-settings.ts +12 -4
  75. package/app/Actions/Dashboard/Source/source-inventory.ts +6 -2
  76. package/app/Actions/Dashboard/dashboard-response.ts +2 -1
  77. package/app/Actions/HealthAction.ts +11 -6
  78. package/app/Actions/Locale/SetLocaleAction.ts +3 -1
  79. package/app/Actions/Monitoring/ErrorShowAction.ts +5 -0
  80. package/app/Actions/Payment/CreatePaymentIntentAction.ts +3 -1
  81. package/app/Actions/SendWelcomeEmail.ts +3 -1
  82. package/app/Actions/Teams/team-response.ts +2 -1
  83. package/app/Actions/TestErrorAction.ts +1 -1
  84. package/app/Jobs/PruneModelsJob.ts +1 -1
  85. package/app/Jobs/PruneQueryLogsJob.ts +3 -1
  86. package/app/Mail/OrderConfirmation.ts +18 -2
  87. package/app/Middleware/Abilities.ts +3 -1
  88. package/app/Middleware/Can.ts +3 -3
  89. package/app/Middleware/Compress.ts +3 -1
  90. package/app/Middleware/Csrf.ts +10 -4
  91. package/app/Middleware/EnsureEmailIsVerified.ts +2 -1
  92. package/app/Middleware/Permission.ts +2 -1
  93. package/app/Middleware/Role.ts +2 -1
  94. package/app/Middleware/Team.ts +2 -1
  95. package/app/Middleware/Throttle.ts +1 -1
  96. package/ide/vscode/package.json +1 -1
  97. package/package.json +2 -2
  98. package/resources/functions/dashboard/data.ts +2 -2
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
3
3
  import {
4
4
  generateAuthenticationOptions,
5
5
  getUserPasskeys,
6
+ passkeyDescriptors,
6
7
  storeWebAuthnChallenge,
7
8
  } from '@stacksjs/auth'
8
9
  import { config } from '@stacksjs/config'
@@ -31,13 +32,12 @@ export default new Action({
31
32
  const appUrl = config.app?.url || 'localhost'
32
33
  const rpID = new URL(appUrl.startsWith('http') ? appUrl : `https://${appUrl}`).hostname
33
34
 
34
- const options: PublicKeyCredentialRequestOptionsJSON = await generateAuthenticationOptions({
35
+ const options = await generateAuthenticationOptions({
35
36
  rpID,
36
- allowCredentials: userPasskeys.map(passkey => ({
37
- id: passkey.id,
38
- transports: ['internal'],
39
- })),
40
- })
37
+ // The JSON-vs-ArrayBuffer boundary lives in `passkeyDescriptors`; the
38
+ // narrowing goes away when ts-auth b59ad36 ships.
39
+ allowCredentials: passkeyDescriptors(userPasskeys) as unknown as Parameters<typeof generateAuthenticationOptions>[0]['allowCredentials'],
40
+ }) as unknown as PublicKeyCredentialRequestOptionsJSON
41
41
 
42
42
  // Persist the challenge server-side so `VerifyAuthenticationAction`
43
43
  // can consume it from the DB instead of trusting `body.challenge`.
@@ -1,5 +1,5 @@
1
1
  import { Action } from '@stacksjs/actions'
2
- import { generateRegistrationOptions, getUserPasskeys, storeWebAuthnChallenge } from '@stacksjs/auth'
2
+ import { generateRegistrationOptions, getUserPasskeys, passkeyDescriptors, storeWebAuthnChallenge } from '@stacksjs/auth'
3
3
  import { config } from '@stacksjs/config'
4
4
 
5
5
  export default new Action({
@@ -30,12 +30,17 @@ export default new Action({
30
30
  const options = await generateRegistrationOptions({
31
31
  rpName,
32
32
  rpID,
33
+ // The WebAuthn user handle. It was omitted entirely, and the generator
34
+ // does `new TextEncoder().encode(options.userID)` - so every passkey was
35
+ // registered against the handle "undefined", the same one for every
36
+ // user. `storePasskey` records the email as `webauthn_user_id`, so the
37
+ // email is the handle the two halves have to agree on.
38
+ userID: userEmail,
33
39
  userName: userEmail,
34
40
  attestationType: 'none',
35
- excludeCredentials: userPasskeys.map(passkey => ({
36
- id: passkey.id,
37
- transports: ['internal'],
38
- })),
41
+ // See `passkeyDescriptors` for the JSON-vs-ArrayBuffer boundary; the
42
+ // narrowing goes away when ts-auth b59ad36 ships.
43
+ excludeCredentials: passkeyDescriptors(userPasskeys) as unknown as Parameters<typeof generateRegistrationOptions>[0]['excludeCredentials'],
39
44
  authenticatorSelection: {
40
45
  residentKey: 'preferred',
41
46
  userVerification: 'preferred',
@@ -40,11 +40,14 @@ export default new Action({
40
40
  // — listener errors are caught by the wildcard handler so a flaky
41
41
  // welcome email doesn't fail registration. The `to` alias matches
42
42
  // the contract SendWelcomeEmail expects.
43
+ // `to` is what SendWelcomeEmail addresses the mail to, so it has to be
44
+ // a string; the registering address is the honest fallback if the
45
+ // freshly-created user could not be read back.
43
46
  dispatch('user:registered', {
44
47
  id: user?.id,
45
- email: user?.email,
48
+ email: user?.email ?? email,
46
49
  name: user?.name,
47
- to: user?.email,
50
+ to: user?.email ?? email,
48
51
  })
49
52
 
50
53
  // Same OAuth2-compatible payload LoginAction returns, so a client can
@@ -48,9 +48,11 @@ export default new Action({
48
48
  }
49
49
  const publicKey = new Uint8Array(Object.values(jsonParse)).buffer
50
50
 
51
- // Convert challenge string to Uint8Array. The challenge is stored
52
- // as base64url (the same form generateAuthenticationOptions emits).
53
- const challengeBytes = Uint8Array.from(atob(expectedChallenge), c => c.charCodeAt(0))
51
+ // `consumeWebAuthnChallenge` already returns the bytes it stored. This
52
+ // used to run them through `atob()`, which stringifies a Uint8Array to
53
+ // "12,34,56..." before decoding it - so the challenge never matched and
54
+ // passkey authentication could not succeed.
55
+ const challengeBytes = expectedChallenge
54
56
 
55
57
  // Derive origin and rpID from app config instead of hardcoding localhost
56
58
  const appUrl = config.app?.url || 'http://localhost:3333'
@@ -40,12 +40,16 @@ export default new Action({
40
40
  const expectedRPID = new URL(expectedOrigin).hostname
41
41
 
42
42
  try {
43
- const verification = await verifyRegistrationResponse({
44
- response: body.attResp,
43
+ // Positional, as the function is declared. Passing one options object
44
+ // meant `credential.response` was undefined, the read threw, the catch
45
+ // swallowed it and every registration came back `verified: false` -
46
+ // passkey registration could never succeed through this action.
47
+ const verification = await verifyRegistrationResponse(
48
+ body.attResp,
45
49
  expectedChallenge,
46
50
  expectedOrigin,
47
51
  expectedRPID,
48
- })
52
+ )
49
53
 
50
54
  await setCurrentRegistrationOptions(user, verification)
51
55
 
@@ -1,3 +1,4 @@
1
+ import type { UserModel } from '@stacksjs/orm'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { manageCheckout } from '@stacksjs/payments'
3
4
  import { response } from '@stacksjs/router'
@@ -18,7 +19,19 @@ export default new Action({
18
19
  cancel_url: 'https://localhost:3000/cancel',
19
20
  }
20
21
 
21
- const session = await manageCheckout.create(user, params)
22
+ // Checkout needs the billable trait, which the default User model leaves
23
+ // OFF ("not every app bills through the User model"). Without it the
24
+ // methods manageCheckout calls - hasStripeId() and friends - are not on
25
+ // the record at all, so say which switch is missing instead of failing
26
+ // deeper in with "user.hasStripeId is not a function".
27
+ if (typeof (user as unknown as Partial<UserModel>).hasStripeId !== 'function') {
28
+ return response.json(
29
+ { message: 'Billing is not enabled. Set `traits.billable` on the User model to use checkout.' },
30
+ 503,
31
+ )
32
+ }
33
+
34
+ const session = await manageCheckout.create(user as unknown as UserModel, params)
22
35
 
23
36
  return response.json({ url: session.url })
24
37
  },
@@ -7,7 +7,7 @@ export default new Action({
7
7
  description: 'Delete an author',
8
8
  method: 'DELETE',
9
9
  async handle(request: RequestInstance) {
10
- const id = request.getParam('id')
10
+ const id = Number(request.getParam('id'))
11
11
  const author = await Author.find(Number(id))
12
12
 
13
13
  if (!author) {
@@ -7,7 +7,7 @@ export default new Action({
7
7
  description: 'Fetch a single author by ID',
8
8
  method: 'GET',
9
9
  async handle(request: RequestInstance) {
10
- const id = request.getParam('id')
10
+ const id = Number(request.getParam('id'))
11
11
  const result = await Author.find(Number(id))
12
12
 
13
13
  if (!result) {
@@ -7,7 +7,7 @@ export default new Action({
7
7
  description: 'Update an existing author',
8
8
  method: 'PATCH',
9
9
  async handle(request: RequestInstance) {
10
- const id = request.getParam('id')
10
+ const id = Number(request.getParam('id'))
11
11
  const author = await Author.find(Number(id))
12
12
 
13
13
  if (!author) {
@@ -7,7 +7,7 @@ export default new Action({
7
7
  description: 'Category Destroy ORM Action',
8
8
  method: 'DELETE',
9
9
  async handle(request: RequestInstance) {
10
- const id = request.getParam('id')
10
+ const id = Number(request.getParam('id'))
11
11
 
12
12
  await categorizable.destroy(id)
13
13
 
@@ -7,7 +7,7 @@ export default new Action({
7
7
  description: 'Category Show ORM Action',
8
8
  method: 'GET',
9
9
  async handle(request: RequestInstance) {
10
- const id = request.getParam('id')
10
+ const id = Number(request.getParam('id'))
11
11
 
12
12
  const model = await categorizable.fetchById(id)
13
13
 
@@ -1,4 +1,3 @@
1
- import type { CategorizableRequestType } from '@stacksjs/types'
2
1
  import { Action } from '@stacksjs/actions'
3
2
  import { categorizable } from '@stacksjs/cms'
4
3
  import { response } from '@stacksjs/router'
@@ -24,14 +23,14 @@ export default new Action({
24
23
  },
25
24
  })
26
25
 
27
- const id = request.getParam('id')
26
+ const id = Number(request.getParam('id'))
28
27
 
29
28
  const data = {
30
29
  name: request.get('name'),
31
30
  description: request.get('description'),
32
31
  }
33
32
 
34
- const model = await categorizable.update(id, data)
33
+ const model = await categorizable.update({ id, ...data })
35
34
 
36
35
  return response.json(model)
37
36
  },
@@ -7,7 +7,7 @@ export default new Action({
7
7
  description: 'Comment Destroy ORM Action',
8
8
  method: 'DELETE',
9
9
  async handle(request: RequestInstance) {
10
- const id = request.getParam('id')
10
+ const id = Number(request.getParam('id'))
11
11
 
12
12
  await comments.destroy(id)
13
13
 
@@ -7,7 +7,7 @@ export default new Action({
7
7
  description: 'Comment Show ORM Action',
8
8
  method: 'GET',
9
9
  async handle(request: RequestInstance) {
10
- const id = request.getParam('id')
10
+ const id = Number(request.getParam('id'))
11
11
 
12
12
  const model = await comments.fetchCommentById(id)
13
13
 
@@ -22,7 +22,7 @@ export default new Action({
22
22
  },
23
23
  },
24
24
  })
25
- const id = request.getParam('id')
25
+ const id = Number(request.getParam('id'))
26
26
 
27
27
  const data = {
28
28
  title: request.get('title'),
@@ -7,7 +7,7 @@ export default new Action({
7
7
  description: 'Page Destroy ORM Action',
8
8
  method: 'DELETE',
9
9
  async handle(request: RequestInstance) {
10
- const id = request.getParam('id')
10
+ const id = Number(request.getParam('id'))
11
11
 
12
12
  await pages.destroy(id)
13
13
 
@@ -1,6 +1,7 @@
1
1
  import { Action } from '@stacksjs/actions'
2
- import { authors, pages } from '@stacksjs/cms'
2
+ import { pages } from '@stacksjs/cms'
3
3
  import { response } from '@stacksjs/router'
4
+ import { requireSite } from '@stacksjs/sites'
4
5
 
5
6
  export default new Action({
6
7
  name: 'Page Store',
@@ -10,21 +11,20 @@ export default new Action({
10
11
  async handle(request) {
11
12
  await request.validate()
12
13
 
13
- const author = await authors.findOrCreate({
14
- name: 'Current User',
15
- email: 'current@user.com',
16
- })
14
+ // A page belongs to a site, and the site comes from the resolved request
15
+ // context rather than from the body: letting a caller name the site would
16
+ // let them write into somebody else's website.
17
+ const site = requireSite()
17
18
 
18
- const data = {
19
- author_id: author.id,
19
+ const model = await pages.store({
20
+ site_id: site.id,
20
21
  title: request.get('title'),
22
+ slug: request.get('slug'),
21
23
  template: request.get('template'),
22
- content: request.get('content'),
23
- views: 0,
24
- published_at: Date.now(),
25
- }
26
-
27
- const model = await pages.store(data)
24
+ meta_description: request.get('meta_description'),
25
+ status: request.get('status') ?? 'draft',
26
+ blocks: request.get('blocks'),
27
+ } as Parameters<typeof pages.store>[0])
28
28
 
29
29
  return response.json(model)
30
30
  },
@@ -1,6 +1,5 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { pages } from '@stacksjs/cms'
3
- import { formatDate } from '@stacksjs/orm'
4
3
  import { response } from '@stacksjs/router'
5
4
 
6
5
  export default new Action({
@@ -11,16 +10,20 @@ export default new Action({
11
10
  async handle(request) {
12
11
  await request.validate()
13
12
 
14
- const id = request.getParam('id')
13
+ const id = Number(request.getParam('id'))
15
14
 
16
- const data = {
17
- title: request.get('title'),
18
- template: request.get('template'),
19
- content: request.get('content'),
20
- updated_at: formatDate(new Date()),
15
+ // Only what the request actually sent. Spreading every field would write
16
+ // `undefined` over a title the caller never mentioned, and a slug or
17
+ // blocks value that is present goes through the document path in
18
+ // `pages.update`, which re-derives the path and snapshots a revision.
19
+ const data: Record<string, unknown> = {}
20
+ for (const field of ['title', 'slug', 'template', 'meta_description', 'status', 'blocks'] as const) {
21
+ const value = request.get(field)
22
+ if (value !== undefined && value !== null)
23
+ data[field] = value
21
24
  }
22
25
 
23
- const model = await pages.update(id, data)
26
+ const model = await pages.update(id, data as Parameters<typeof pages.update>[1])
24
27
 
25
28
  return response.json(model)
26
29
  },
@@ -8,7 +8,7 @@ export default new Action({
8
8
  description: 'Post Destroy ORM Action',
9
9
  method: 'DELETE',
10
10
  async handle(request: RequestInstance) {
11
- const id = request.getParam('id')
11
+ const id = Number(request.getParam('id'))
12
12
 
13
13
  await posts.destroy(id)
14
14
 
@@ -7,7 +7,7 @@ export default new Action({
7
7
  description: 'Post Show ORM Action',
8
8
  method: 'GET',
9
9
  async handle(request: RequestInstance) {
10
- const id = request.getParam('id')
10
+ const id = Number(request.getParam('id'))
11
11
 
12
12
  const model = await posts.fetchById(id)
13
13
 
@@ -14,7 +14,7 @@ export default new Action({
14
14
  async handle(request) {
15
15
  await request.validate()
16
16
 
17
- const id = request.getParam('id')
17
+ const id = Number(request.getParam('id'))
18
18
 
19
19
  const data = {
20
20
  title: request.get('title'),
@@ -7,7 +7,7 @@ export default new Action({
7
7
  description: 'Tag Destroy ORM Action',
8
8
  method: 'DELETE',
9
9
  async handle(request: RequestInstance) {
10
- const id = request.getParam('id')
10
+ const id = Number(request.getParam('id'))
11
11
 
12
12
  const model = await tags.destroy(id)
13
13
 
@@ -7,7 +7,7 @@ export default new Action({
7
7
  description: 'Tag Show ORM Action',
8
8
  method: 'GET',
9
9
  async handle(request: RequestInstance) {
10
- const id = request.getParam('id')
10
+ const id = Number(request.getParam('id'))
11
11
 
12
12
  const model = await tags.fetchTagById(id)
13
13
 
@@ -23,14 +23,14 @@ export default new Action({
23
23
  },
24
24
  })
25
25
 
26
- const id = request.getParam('id')
26
+ const id = Number(request.getParam('id'))
27
27
  const data = {
28
28
  id,
29
29
  name: request.get('name'),
30
30
  description: request.get('description'),
31
31
  }
32
32
 
33
- const model = await tags.update(id, data)
33
+ const model = await tags.update(data)
34
34
 
35
35
  return response.json(model)
36
36
  },
@@ -7,7 +7,7 @@ export default new Action({
7
7
  description: 'Update the balance of a gift card',
8
8
  method: 'POST',
9
9
 
10
- async handle({ request }) {
10
+ async handle(request) {
11
11
  const { id, amount } = request.all()
12
12
 
13
13
  if (!id || !amount) {
@@ -7,8 +7,12 @@ export default new Action({
7
7
  description: 'WaitlistProduct Analytics Action',
8
8
  method: 'GET',
9
9
  async handle(request: RequestInstance) {
10
- const startDate = request.getParam<Date>('startDate')
11
- const endDate = request.getParam<Date>('endDate')
10
+ // Params arrive as strings; `getParam` has no type parameter to change that.
11
+ const startDate = new Date(String(request.getParam('startDate')))
12
+ const endDate = new Date(String(request.getParam('endDate')))
13
+
14
+ if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()))
15
+ return response.json({ message: 'startDate and endDate must be valid dates.' }, 422)
12
16
 
13
17
  const [
14
18
  countBySource,
@@ -7,8 +7,12 @@ export default new Action({
7
7
  description: 'WaitlistProduct Quantity Distribution Action',
8
8
  method: 'GET',
9
9
  async handle(request: RequestInstance) {
10
- const startDate = request.getParam<Date>('startDate')
11
- const endDate = request.getParam<Date>('endDate')
10
+ // Params arrive as strings; `getParam` has no type parameter to change that.
11
+ const startDate = new Date(String(request.getParam('startDate')))
12
+ const endDate = new Date(String(request.getParam('endDate')))
13
+
14
+ if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()))
15
+ return response.json({ message: 'startDate and endDate must be valid dates.' }, 422)
12
16
 
13
17
  const quantityDistribution = await waitlists.products.fetchCountByAllQuantities(startDate, endDate)
14
18
 
@@ -7,8 +7,14 @@ export default new Action({
7
7
  description: 'WaitlistProduct Status Statistics Action',
8
8
  method: 'GET',
9
9
  async handle(request: RequestInstance) {
10
- const startDate = request.getParam<Date>('startDate')
11
- const endDate = request.getParam<Date>('endDate')
10
+ // Params arrive as strings; `getParam` has no type parameter to change
11
+ // that, and asking for `<Date>` only made the file stop typechecking
12
+ // while still handing the query layer a string.
13
+ const startDate = new Date(String(request.getParam('startDate')))
14
+ const endDate = new Date(String(request.getParam('endDate')))
15
+
16
+ if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()))
17
+ return response.json({ message: 'startDate and endDate must be valid dates.' }, 422)
12
18
 
13
19
  const [
14
20
  totalCount,
@@ -7,8 +7,12 @@ export default new Action({
7
7
  description: 'WaitlistProduct Time Series Action',
8
8
  method: 'GET',
9
9
  async handle(request: RequestInstance) {
10
- const startDate = request.getParam<Date>('startDate')
11
- const endDate = request.getParam<Date>('endDate')
10
+ // Params arrive as strings; `getParam` has no type parameter to change that.
11
+ const startDate = new Date(String(request.getParam('startDate')))
12
+ const endDate = new Date(String(request.getParam('endDate')))
13
+
14
+ if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()))
15
+ return response.json({ message: 'startDate and endDate must be valid dates.' }, 422)
12
16
 
13
17
  const timeSeriesData = await waitlists.products.fetchCountByDateGrouped(startDate, endDate)
14
18
 
@@ -115,7 +115,12 @@ export function buildCampaignAnalytics(
115
115
  }
116
116
  const channels = [...channelMap.entries()]
117
117
  .map(([key, rows]) => {
118
- const [type, currency] = key.split(':')
118
+ // `split` gives `string | undefined` per element; the keys are built as
119
+ // `type:currency` a few lines up, so an empty string is the honest
120
+ // fallback for a malformed one rather than a crash in `titleCase`.
121
+ const [rawType, rawCurrency] = key.split(':')
122
+ const type = rawType ?? ''
123
+ const currency = rawCurrency ?? ''
119
124
  return {
120
125
  type,
121
126
  name: titleCase(type),
@@ -1,3 +1,4 @@
1
+ import type { ReadableRecord } from '@stacksjs/orm'
1
2
  export type AnalyticsRange = 'day' | 'week' | 'month' | 'year'
2
3
  export type AnalyticsScope = 'all' | 'blog' | 'commerce'
3
4
 
@@ -11,9 +12,8 @@ export interface RequestAnalyticsRow {
11
12
  createdAt: string
12
13
  }
13
14
 
14
- export interface AnalyticsModelRecord {
15
- get: (key: string) => unknown
16
- }
15
+ /** The shared shape, kept under this name for the helpers below. */
16
+ export type AnalyticsModelRecord = ReadableRecord
17
17
 
18
18
  interface TrafficBucket {
19
19
  date: string
@@ -1,8 +1,16 @@
1
1
  import type { AIMessage, AIProvider, ConfiguredAIOptions } from '@stacksjs/ai'
2
2
 
3
+ /**
4
+ * The two keys this check reads out of the environment.
5
+ *
6
+ * The index signature is what makes `process.env` assignable: `ProcessEnv`
7
+ * declares no named members, so a bare two-key interface shares nothing with
8
+ * it and TypeScript rejects the call - the one thing every caller passes.
9
+ */
3
10
  export interface BuddyProviderEnvironment {
4
11
  ANTHROPIC_API_KEY?: string
5
12
  OPENAI_API_KEY?: string
13
+ [key: string]: string | undefined
6
14
  }
7
15
 
8
16
  export function resolveBuddyProvider(config: ConfiguredAIOptions): AIProvider {
@@ -53,7 +53,8 @@ async function appendSamples(snapshot: DashboardData, nowIso: string): Promise<v
53
53
  // Insert one row per org. Cross-dialect multi-row INSERT keeps it
54
54
  // a single round-trip even for the typical 5-org fleet.
55
55
  const rows = orgs.map((org) => {
56
- const r = runners[org]
56
+ // An org with no sample yet reads as zero pressure rather than throwing.
57
+ const r = runners[org] ?? { running: 0, queued: 0, cap: 0 }
57
58
  return { org, running: r.running, queued: r.queued, cap: r.cap, sampled_at: nowIso }
58
59
  })
59
60
  await db.insertInto('ci_runner_samples').values(rows as any).execute()
@@ -420,7 +420,9 @@ function serverlessOverview(config: CloudRecord): {
420
420
  serviceDetails: DashboardCloudResourceDetail[] = [],
421
421
  ) => {
422
422
  const id = `serverless:${environmentName}:${suffix}`
423
- const presentation = servicePresentation[type]
423
+ // `Record<string, …>` makes every lookup optional; an unknown service
424
+ // type gets a neutral chip rather than crashing the whole overview.
425
+ const presentation = servicePresentation[type] ?? { color: '#64748B', icon: 'i-hugeicons-cube' }
424
426
  services.push({
425
427
  id,
426
428
  name,
@@ -35,15 +35,23 @@ export default new Action({
35
35
  const customerIds = [...new Set(orderRows
36
36
  .map(order => order.customerId)
37
37
  .filter(Boolean))]
38
- const orderItems = orderIds.length > 0
39
- ? await OrderItem.where('order_id', 'in', orderIds).get()
38
+
39
+ // The normalizers hand back identifiers as strings (a record may key on
40
+ // a uuid), but these columns are numeric, so they go back to numbers at
41
+ // the query boundary - the same conversion CommerceOrdersAction does.
42
+ const numericOrderIds = orderIds.map(Number).filter(Number.isSafeInteger)
43
+ const numericCustomerIds = customerIds.map(Number).filter(Number.isSafeInteger)
44
+
45
+ const orderItems = numericOrderIds.length > 0
46
+ ? await OrderItem.whereIn('order_id', numericOrderIds).get()
40
47
  : []
41
48
  const orderItemRows = orderItems.map(normalizeCommerceDashboardOrderItem)
42
49
  const productIds = [...new Set(orderItemRows.map(item => item.productId))]
50
+ const numericProductIds = productIds.map(Number).filter(Number.isSafeInteger)
43
51
 
44
52
  const [products, customers] = await Promise.all([
45
- productIds.length > 0 ? Product.where('id', 'in', productIds).get() : [],
46
- customerIds.length > 0 ? Customer.where('id', 'in', customerIds).get() : [],
53
+ numericProductIds.length > 0 ? Product.whereIn('id', numericProductIds).get() : [],
54
+ numericCustomerIds.length > 0 ? Customer.whereIn('id', numericCustomerIds).get() : [],
47
55
  ])
48
56
 
49
57
  return buildCommerceDashboard(
@@ -28,7 +28,7 @@ export default new Action({
28
28
  const [customers, coupons, items] = await Promise.all([
29
29
  Customer.orderBy('name', 'asc').limit(500).get(),
30
30
  Coupon.orderBy('id', 'asc').limit(500).get(),
31
- numericOrderIds.length > 0 ? OrderItem.where('order_id', 'in', numericOrderIds).get() : [],
31
+ numericOrderIds.length > 0 ? OrderItem.whereIn('order_id', numericOrderIds).get() : [],
32
32
  ])
33
33
  const customerContexts = customers.map(normalizeOrderCustomerContext)
34
34
  const customerMap = new Map(customerContexts.map(customer => [customer.id, customer.context]))
@@ -103,7 +103,7 @@ async function existingReceipt(order: any) {
103
103
  receiptId(field(item, 'product_id', 'productId'), `OrderItem ${index + 1}`, 'product_id'),
104
104
  )
105
105
  const [products, payments] = await Promise.all([
106
- productIds.length > 0 ? Product.where('id', 'in', productIds).get() : [],
106
+ productIds.length > 0 ? Product.whereIn('id', productIds).get() : [],
107
107
  Payment.where('order_id', orderId).get(),
108
108
  ])
109
109
  const names = new Map(products.map((product) => {
@@ -242,7 +242,7 @@ export default new Action({
242
242
  let taxRates
243
243
  try {
244
244
  const [productRows, categories, manufacturers, persistedTaxRates] = await Promise.all([
245
- Product.where('id', 'in', productIds).get(),
245
+ Product.whereIn('id', productIds).get(),
246
246
  Category.orderBy('name', 'asc').limit(500).get(),
247
247
  Manufacturer.orderBy('manufacturer', 'asc').limit(500).get(),
248
248
  TaxRate.orderBy('id', 'asc').limit(500).get(),
@@ -93,7 +93,7 @@ export default new Action({
93
93
  const printDeviceIds = new Set(printDevices.map(device =>
94
94
  commerceIdentifier(commerceValue(device, 'id', 'uuid'), 'PrintDevice'),
95
95
  ))
96
- const records = rows.map(row => normalizePrintLogRecord(row, printDeviceIds))
96
+ const records = rows.map((row: Record<string, unknown>) => normalizePrintLogRecord(row, printDeviceIds))
97
97
 
98
98
  return {
99
99
  records,