@stacksjs/defaults 0.72.5 → 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
@@ -1,6 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { FailedJob, Job } from '@stacksjs/orm'
3
- import { getGlobalMetrics } from '@stacksjs/queue'
3
+ import { getGlobalMetrics, queuedJobState } from '@stacksjs/queue'
4
4
  import { dashboardOperationalError } from '../dashboard-response'
5
5
 
6
6
  interface QueueBucket {
@@ -11,9 +11,11 @@ interface QueueBucket {
11
11
  total: number
12
12
  }
13
13
 
14
- const isPending = (status: string) => status === 'pending' || status === 'waiting' || status === 'queued'
15
- const isActive = (status: string) => status === 'processing' || status === 'active'
16
- const isCompleted = (status: string) => status === 'completed' || status === 'done'
14
+ // A queued job has no `status` column - `queuedJobState` in @stacksjs/queue
15
+ // holds the rule (reserved_at means a worker has it; available_at in the
16
+ // future means it is not eligible yet), and the queue's own health check reads
17
+ // the same one. A row that finished is deleted by the worker, so 'completed'
18
+ // is not a state the jobs table can be in; failures live in `failed_jobs`.
17
19
 
18
20
  export default new Action({
19
21
  name: 'QueueStatsAction',
@@ -21,19 +23,29 @@ export default new Action({
21
23
  method: 'GET',
22
24
  async handle() {
23
25
  try {
24
- const [totalJobs, failedJobCount, allJobs] = await Promise.all([
26
+ const [totalJobs, failedJobCount, allJobs, allFailedJobs] = await Promise.all([
25
27
  Job.count(),
26
28
  FailedJob.count(),
27
29
  Job.all(),
30
+ FailedJob.all(),
28
31
  ])
29
32
 
30
33
  const queueMap: Record<string, QueueBucket> = {}
34
+
35
+ // Per-queue failure counts come from failed_jobs, which is where a
36
+ // failure actually lands. This column read `status === 'failed'` off the
37
+ // jobs table before, and so was always zero.
38
+ const failedByQueue = new Map<string, number>()
39
+ for (const f of allFailedJobs) {
40
+ const queueName = String(f.get('queue') || 'default')
41
+ failedByQueue.set(queueName, (failedByQueue.get(queueName) ?? 0) + 1)
42
+ }
31
43
  let active = 0
32
- let completed = 0
44
+ const completed = 0
33
45
 
34
46
  for (const j of allJobs) {
35
47
  const queueName = String(j.get('queue') || 'default')
36
- const status = String(j.get('status') || 'pending')
48
+ const status = queuedJobState({ reserved_at: j.get('reserved_at'), available_at: j.get('available_at') })
37
49
 
38
50
  if (!queueMap[queueName]) {
39
51
  queueMap[queueName] = { pending: 0, active: 0, completed: 0, failed: 0, total: 0 }
@@ -41,16 +53,13 @@ export default new Action({
41
53
  const bucket = queueMap[queueName]
42
54
  bucket.total++
43
55
 
44
- if (isPending(status)) bucket.pending++
45
- else if (isActive(status)) {
56
+ if (status === 'processing') {
46
57
  bucket.active++
47
58
  active++
48
59
  }
49
- else if (isCompleted(status)) {
50
- bucket.completed++
51
- completed++
60
+ else {
61
+ bucket.pending++
52
62
  }
53
- else if (status === 'failed') bucket.failed++
54
63
  }
55
64
 
56
65
  const queues = Object.entries(queueMap).map(([name, data]) => ({
@@ -59,7 +68,7 @@ export default new Action({
59
68
  pending: data.pending,
60
69
  active: data.active,
61
70
  completed: data.completed,
62
- failed: data.failed,
71
+ failed: failedByQueue.get(name) ?? data.failed,
63
72
  total: data.total,
64
73
  }))
65
74
 
@@ -131,7 +131,7 @@ async function loadSearchableModels(): Promise<SearchableModel[]> {
131
131
  const search = definition.traits?.useSearch
132
132
  if (!search || typeof search !== 'object' || !Array.isArray(search.searchable)) continue
133
133
 
134
- const fields = search.searchable
134
+ const fields: string[] = (search.searchable as unknown[])
135
135
  .map((field: unknown) => snakeCase(String(field)))
136
136
  .filter((field: string) => isSafeIdentifier(field) && !HIDDEN_FIELDS.has(field))
137
137
  const table = String(definition.table || pluralize(snakeCase(name)))
@@ -51,11 +51,19 @@ export interface MailSettingsState {
51
51
  }
52
52
  }
53
53
 
54
+ /**
55
+ * Whatever the settings form posted.
56
+ *
57
+ * Every field is optional AND `unknown`: this is the raw request body, and
58
+ * `updateMailSettings` runs each value through `stringValue` and validates it.
59
+ * Declaring the four core fields as required meant `request.all()` - the only
60
+ * thing any caller has - did not satisfy the type it was written for.
61
+ */
54
62
  export interface MailSettingsInput {
55
- revision: unknown
56
- driver: unknown
57
- fromName: unknown
58
- fromAddress: unknown
63
+ revision?: unknown
64
+ driver?: unknown
65
+ fromName?: unknown
66
+ fromAddress?: unknown
59
67
  smtp?: Record<string, unknown>
60
68
  ses?: Record<string, unknown>
61
69
  sendgrid?: Record<string, unknown>
@@ -50,8 +50,12 @@ export function parseCommandSource(
50
50
  modifiedAt = '',
51
51
  ): SourceInventoryItem {
52
52
  const command = source.match(/\.command\(\s*['"`]([^'"`]+)['"`]\s*(?:,\s*['"`]([^'"`]+)['"`])?/)
53
- const sourceAliases = [...source.matchAll(/\.alias\(\s*['"`]([^'"`]+)['"`]\s*\)/g)].map(match => match[1])
54
- const options = [...source.matchAll(/\.option\(\s*['"`]([^'"`]+)['"`]/g)].map(match => match[1])
53
+ const sourceAliases = [...source.matchAll(/\.alias\(\s*['"`]([^'"`]+)['"`]\s*\)/g)]
54
+ .map(match => match[1])
55
+ .filter((alias): alias is string => alias !== undefined)
56
+ const options = [...source.matchAll(/\.option\(\s*['"`]([^'"`]+)['"`]/g)]
57
+ .map(match => match[1])
58
+ .filter((option): option is string => option !== undefined)
55
59
 
56
60
  return {
57
61
  name: command?.[1] || registeredSignature,
@@ -1,10 +1,11 @@
1
+ import type { ResponseStatus } from '@stacksjs/bun-router'
1
2
  import { response } from '@stacksjs/router'
2
3
 
3
4
  export function dashboardOperationalError(
4
5
  error: unknown,
5
6
  message: string,
6
7
  action: string,
7
- status = 503,
8
+ status: ResponseStatus = 503,
8
9
  ): Response {
9
10
  console.error(`[dashboard/api] ${action} failed:`, error)
10
11
  return response.json({ message }, status)
@@ -1,7 +1,7 @@
1
1
  // Health check action with detailed service status
2
2
  // This overrides the default framework health check
3
3
 
4
- import { route } from '@stacksjs/router'
4
+ import { response, route } from '@stacksjs/router'
5
5
 
6
6
  export default {
7
7
  name: 'Health',
@@ -70,13 +70,17 @@ async function getServiceHealth() {
70
70
 
71
71
  // Queue Health
72
72
  try {
73
- const { Job } = await import('@stacksjs/orm')
73
+ // The queue package owns what "healthy" means for a queue - depth
74
+ // thresholds, stuck workers, jobs whose reserve has gone stale - and its
75
+ // own worker health endpoint reads the same function. This used to count
76
+ // rows where `status = 'pending'`, a column the jobs table does not have.
77
+ const { checkQueueHealth } = await import('@stacksjs/queue')
74
78
  const queueStart = Date.now()
75
- const pendingJobs = await Job.where('status', 'pending').count()
79
+ const queueHealth = await checkQueueHealth()
76
80
  const queueLatency = Date.now() - queueStart
77
81
  services.push({
78
82
  name: 'Queue',
79
- status: pendingJobs < 100 ? 'healthy' : pendingJobs < 1000 ? 'degraded' : 'critical',
83
+ status: queueHealth.status === 'unhealthy' ? 'critical' : queueHealth.status,
80
84
  latency: `${queueLatency}ms`,
81
85
  uptime: '99.9%',
82
86
  })
@@ -104,9 +108,10 @@ async function getServiceHealth() {
104
108
  // Also register as a route for redundancy
105
109
  route.get('/health', async () => {
106
110
  const services = await getServiceHealth()
107
- return {
111
+ // A route handler returns a Response; a bare object is not one.
112
+ return response.json({
108
113
  status: 'ok',
109
114
  timestamp: Date.now(),
110
115
  services,
111
- }
116
+ })
112
117
  })
@@ -45,7 +45,9 @@ export default new Action({
45
45
 
46
46
  return createLocaleSwitchResponse(request as unknown as Request, String(request.param('locale') ?? ''), {
47
47
  locales,
48
- defaultLocale: app?.locale ?? locales[0],
48
+ // `locales[0]` is `string | undefined` under noUncheckedIndexedAccess,
49
+ // and this list is never empty by construction - but say so.
50
+ defaultLocale: app?.locale ?? locales[0] ?? 'en',
49
51
  })
50
52
  },
51
53
  })
@@ -7,7 +7,12 @@ export default new Action({
7
7
  description: 'Fetch a single error by ID',
8
8
  method: 'GET',
9
9
  async handle(request: RequestInstance) {
10
+ // `getParamAsInt` returns null for a param that is not an integer, which
11
+ // is a bad request rather than a lookup for id `null`.
10
12
  const id = request.getParamAsInt('id')
13
+ if (id === null)
14
+ return response.json({ error: 'A numeric error id is required' }, 422)
15
+
11
16
  const result = await errors.fetchById(id)
12
17
 
13
18
  if (!result) {
@@ -21,7 +21,9 @@ export default new Action({
21
21
  }
22
22
 
23
23
  const paymentIntent = await user?.paymentIntent({
24
- amount: Number(product.unit_price),
24
+ // The Product model's column is `price`; `unit_price` has never been
25
+ // one, so this read was undefined and the intent was created for NaN.
26
+ amount: Number(product.get('price')),
25
27
  currency: 'usd',
26
28
  payment_method_types: ['card'],
27
29
  })
@@ -7,7 +7,9 @@ export interface WelcomeEmailParams {
7
7
  name?: string
8
8
  }
9
9
 
10
- export default new Action({
10
+ // Invoked by the event system with the `user:registered` payload rather than
11
+ // by the router with a request - see app/Events.ts.
12
+ export default new Action<string, undefined, '', WelcomeEmailParams>({
11
13
  name: 'SendWelcomeEmail',
12
14
  description: 'Sends a welcome email to newly registered users',
13
15
 
@@ -1,3 +1,4 @@
1
+ import type { ResponseStatus } from '@stacksjs/bun-router'
1
2
  import { response } from '@stacksjs/router'
2
3
 
3
4
  export class TeamStateConflictError extends Error {
@@ -11,7 +12,7 @@ export function teamOperationalError(
11
12
  error: unknown,
12
13
  message: string,
13
14
  action: string,
14
- status = 503,
15
+ status: ResponseStatus = 503,
15
16
  ): Response {
16
17
  console.error(`[teams/api] ${action} failed:`, error)
17
18
  return response.json({ message }, status)
@@ -5,7 +5,7 @@
5
5
  * Append ?type=<scenario> to trigger a specific exception.
6
6
  */
7
7
 
8
- import type { EnhancedRequest } from 'bun-router'
8
+ import type { EnhancedRequest } from '@stacksjs/bun-router'
9
9
  import { dirname, join } from 'node:path'
10
10
  import process from 'node:process'
11
11
  import { fileURLToPath } from 'node:url'
@@ -33,7 +33,7 @@ export default class PruneModelsJob {
33
33
  try {
34
34
  log.info('Running model pruning job')
35
35
 
36
- const { prunable } = await import('../../orm/src/utils/prunable')
36
+ const { prunable } = await import('@stacksjs/orm')
37
37
 
38
38
  const results: Array<{ table: string, pruned: number }> = []
39
39
  let total = 0
@@ -37,7 +37,9 @@ export default class PruneQueryLogsJob {
37
37
  }
38
38
 
39
39
  // Prune old query logs
40
- const result = await QueryController.pruneQueryLogs()
40
+ // `pruneQueryLogs` is an instance method - the routes reach it through
41
+ // 'Controllers/QueryController@method', which instantiates for them.
42
+ const result = await new QueryController().pruneQueryLogs()
41
43
 
42
44
  log.info(`Successfully pruned ${result.pruned} query logs older than ${result.retentionDays} days`)
43
45
 
@@ -1,5 +1,5 @@
1
1
  import { config } from '@stacksjs/config'
2
- import { mail, template } from '@stacksjs/email'
2
+ import { escapeHtml, mail, safe, template } from '@stacksjs/email'
3
3
 
4
4
  export interface OrderConfirmationItem {
5
5
  name: string
@@ -38,7 +38,10 @@ export async function sendOrderConfirmation(options: OrderConfirmationOptions):
38
38
  orderId: options.orderId,
39
39
  orderUrl: options.orderUrl,
40
40
  customerName: options.customerName || 'there',
41
- items: options.items,
41
+ // A template variable is a scalar - an array would be interpolated as
42
+ // "[object Object],[object Object]". The rows are rendered here and
43
+ // marked safe, with every value the customer supplied escaped first.
44
+ itemRows: safe(renderItemRows(options.items)),
42
45
  subtotal: options.subtotal,
43
46
  shipping: options.shipping,
44
47
  total: options.total,
@@ -57,4 +60,17 @@ export async function sendOrderConfirmation(options: OrderConfirmationOptions):
57
60
  })
58
61
  }
59
62
 
63
+ function formatMoney(amount: number): string {
64
+ return (amount / 100).toFixed(2)
65
+ }
66
+
67
+ function renderItemRows(items: OrderConfirmationItem[]): string {
68
+ return items
69
+ .map(item => `<tr>
70
+ <td style="padding: 12px 0; color: #d4d4d4; font-size: 15px;">${escapeHtml(item.name)} &times; ${Number(item.qty) || 0}</td>
71
+ <td style="padding: 12px 0; color: #ececec; font-size: 15px; text-align: right;">${formatMoney(Number(item.lineTotal) || 0)}</td>
72
+ </tr>`)
73
+ .join('\n')
74
+ }
75
+
60
76
  export default sendOrderConfirmation
@@ -36,7 +36,9 @@ export default new Middleware({
36
36
  throw new HttpError(401, 'Unauthenticated.')
37
37
  }
38
38
 
39
- const tokenAbilities: string[] = token.abilities || []
39
+ // `_currentAccessToken` is declared `unknown` (the token row is
40
+ // project-defined), so name the one field this middleware reads.
41
+ const tokenAbilities: string[] = (token as { abilities?: string[] }).abilities || []
40
42
 
41
43
  // Wildcard grants all permissions
42
44
  if (tokenAbilities.includes('*')) {
@@ -1,4 +1,4 @@
1
- import { AuthorizationException, authorize } from '@stacksjs/auth'
1
+ import { authenticatedUser, AuthorizationException, authorize } from '@stacksjs/auth'
2
2
  import { HttpError } from '@stacksjs/error-handling'
3
3
  import { Middleware, resolveRouteModel, setRouteModelFallback } from '@stacksjs/router'
4
4
 
@@ -76,7 +76,7 @@ export default new Middleware({
76
76
  }
77
77
 
78
78
  // Get the authenticated user
79
- const user = request.user || request._user || null
79
+ const user = await authenticatedUser(request)
80
80
 
81
81
  // Prepare arguments for the gate check
82
82
  const args: any[] = []
@@ -109,7 +109,7 @@ export default new Middleware({
109
109
 
110
110
  // Perform the authorization check
111
111
  try {
112
- await authorize(ability, user, ...args)
112
+ await authorize(ability, user ?? null, ...args)
113
113
  }
114
114
  catch (error) {
115
115
  if (error instanceof AuthorizationException) {
@@ -180,7 +180,9 @@ export async function applyCompression(request: Request, response: Response): Pr
180
180
  newHeaders.set('Vary', 'Accept-Encoding')
181
181
  }
182
182
 
183
- return new Response(compressed, {
183
+ // Node's Buffer satisfies BodyInit at runtime but not in the DOM lib's
184
+ // types; the underlying bytes are what Response wants either way.
185
+ return new Response(new Uint8Array(compressed), {
184
186
  status: response.status,
185
187
  statusText: response.statusText,
186
188
  headers: newHeaders,
@@ -1,6 +1,7 @@
1
1
  import { randomBytes, timingSafeEqual } from 'node:crypto'
2
2
  import { Buffer } from 'node:buffer'
3
3
  import { HttpError } from '@stacksjs/error-handling'
4
+ import type { EnhancedRequest } from '@stacksjs/bun-router'
4
5
  import { Middleware } from '@stacksjs/router'
5
6
 
6
7
  /**
@@ -218,7 +219,7 @@ const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS'])
218
219
  * the router, such as the local dashboard config editor, can enforce the same
219
220
  * double-submit and bearer-token rules without duplicating security logic.
220
221
  */
221
- export async function validateCsrfRequest(request: Request): Promise<void> {
222
+ export async function validateCsrfRequest(request: Request | EnhancedRequest): Promise<void> {
222
223
  const method = request.method.toUpperCase()
223
224
 
224
225
  // Safe methods don't mutate state — no token check needed.
@@ -236,15 +237,20 @@ export async function validateCsrfRequest(request: Request): Promise<void> {
236
237
  // (webhooks, third-party callbacks). The router stamps a hint on
237
238
  // the request when it resolves such an action; if that hint is
238
239
  // present, skip enforcement.
239
- if (request._skipCsrf === true) return
240
+ const enhanced = request as EnhancedRequest
241
+ if (enhanced._skipCsrf === true) return
240
242
 
241
243
  // Look up the submitted token. Header is the SPA path; body field
242
244
  // is the traditional form-post path. We accept either.
243
245
  const headerToken = request.headers.get(CSRF_HEADER_NAME)
244
246
  || request.headers.get('X-CSRF-Token')
245
247
  || request.headers.get('X-Csrf-Token')
246
- const body = request.jsonBody || request.formBody || {}
247
- const bodyToken: string | undefined = body?._token ?? body?.csrf_token
248
+ const body = enhanced.jsonBody || enhanced.formBody || {}
249
+ // A parsed body is `unknown`-valued: the token is validated as a string
250
+ // two lines down, so read it as one rather than asserting it is one.
251
+ const rawBodyToken = (body as Record<string, unknown> | undefined)?._token
252
+ ?? (body as Record<string, unknown> | undefined)?.csrf_token
253
+ const bodyToken: string | undefined = typeof rawBodyToken === 'string' ? rawBodyToken : undefined
248
254
 
249
255
  const submitted = (typeof headerToken === 'string' && headerToken)
250
256
  || (typeof bodyToken === 'string' && bodyToken)
@@ -1,3 +1,4 @@
1
+ import { authenticatedUser } from '@stacksjs/auth'
1
2
  import { HttpError } from '@stacksjs/error-handling'
2
3
  import { Middleware } from '@stacksjs/router'
3
4
 
@@ -15,7 +16,7 @@ export default new Middleware({
15
16
  priority: 4,
16
17
 
17
18
  async handle(request) {
18
- const user = request.user || request._user || null
19
+ const user = await authenticatedUser(request)
19
20
 
20
21
  if (!user) {
21
22
  throw new HttpError(401, 'Unauthenticated.')
@@ -1,3 +1,4 @@
1
+ import { authenticatedUser } from '@stacksjs/auth'
1
2
  import { HttpError } from '@stacksjs/error-handling'
2
3
  import { Middleware } from '@stacksjs/router'
3
4
 
@@ -25,7 +26,7 @@ export default new Middleware({
25
26
 
26
27
  if (requiredPermissions.length === 0) return
27
28
 
28
- const user = request.user || request._user || null
29
+ const user = await authenticatedUser(request)
29
30
 
30
31
  if (!user) {
31
32
  throw new HttpError(401, 'Unauthenticated.')
@@ -1,3 +1,4 @@
1
+ import { authenticatedUser } from '@stacksjs/auth'
1
2
  import { HttpError } from '@stacksjs/error-handling'
2
3
  import { Middleware } from '@stacksjs/router'
3
4
 
@@ -24,7 +25,7 @@ export default new Middleware({
24
25
 
25
26
  if (requiredRoles.length === 0) return
26
27
 
27
- const user = request.user || request._user || null
28
+ const user = await authenticatedUser(request)
28
29
 
29
30
  if (!user) {
30
31
  throw new HttpError(401, 'Unauthenticated.')
@@ -1,3 +1,4 @@
1
+ import { authenticatedUser } from '@stacksjs/auth'
1
2
  import { HttpError } from '@stacksjs/error-handling'
2
3
  import { Middleware } from '@stacksjs/router'
3
4
 
@@ -16,7 +17,7 @@ export default new Middleware({
16
17
  priority: 3,
17
18
 
18
19
  async handle(request) {
19
- const user = request.user || request._user || request._authenticatedUser
20
+ const user = await authenticatedUser(request)
20
21
 
21
22
  if (!user) {
22
23
  throw new HttpError(401, 'Unauthenticated.')
@@ -47,7 +47,7 @@ export default new Middleware({
47
47
  let limiter = limiterCache.get(params)
48
48
  if (!limiter) {
49
49
  try {
50
- const config = parseThrottleString(params)
50
+ const config = parseThrottleString(params as Parameters<typeof parseThrottleString>[0])
51
51
  limiter = createRateLimitMiddleware(config, `throttle:${params}`)
52
52
  limiterCache.set(params, limiter)
53
53
  }
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.72.5",
5
+ "version": "0.72.7",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.5",
5
+ "version": "0.72.7",
6
6
  "description": "The complete managed Stacks application scaffold, including runtime defaults, AI guidance, editor metadata, and npm-backed project support files.",
7
7
  "author": "Chris Breuer",
8
8
  "license": "MIT",
@@ -46,7 +46,7 @@
46
46
  "dependencies": {
47
47
  "@iconify-json/f7": "^1.2.2",
48
48
  "@iconify-json/hugeicons": "^1.2.27",
49
- "@stacksjs/mobile": "^0.72.5",
49
+ "@stacksjs/mobile": "^0.72.7",
50
50
  "@stacksjs/sanitizer": "^0.2.113"
51
51
  },
52
52
  "scripts": {
@@ -342,7 +342,7 @@ export function groupByDay(
342
342
  const d = new Date(raw)
343
343
  if (Number.isNaN(d.getTime())) continue
344
344
  const k = d.toISOString().slice(0, 10)
345
- if (k in buckets) buckets[k]++
345
+ if (k in buckets) buckets[k] = (buckets[k] ?? 0) + 1
346
346
  }
347
347
  return Object.entries(buckets).map(([date, count]) => ({ date, count }))
348
348
  }
@@ -372,7 +372,7 @@ export function sumByDay(
372
372
  if (Number.isNaN(d.getTime())) continue
373
373
  const k = d.toISOString().slice(0, 10)
374
374
  if (!(k in buckets)) continue
375
- buckets[k] += Number(safeGet(r, valueKey, 0)) || 0
375
+ buckets[k] = (buckets[k] ?? 0) + (Number(safeGet(r, valueKey, 0)) || 0)
376
376
  }
377
377
  return Object.entries(buckets).map(([date, total]) => ({ date, total }))
378
378
  }