@stacksjs/defaults 0.74.51 → 0.74.53

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.
@@ -259,13 +259,18 @@ Entity-centric API for single-table design:
259
259
  migrationLocks: 'migration_locks',
260
260
  queryLogging: {
261
261
  // Defaults on outside production and off in production. Production also
262
- // skips query hooks unless persistent history is explicitly enabled.
262
+ // skips query hooks unless persistent history is explicitly enabled,
263
+ // except one onQueryError hook on PostgreSQL and MySQL that reports a
264
+ // pool broken by oven-sh/bun#42804.
263
265
  enabled: env.DB_QUERY_LOGGING_ENABLED ?? !['production', 'prod'].includes(env.APP_ENV || ''),
264
266
  captureAllTraces: false, // slow and failed queries always keep traces
265
267
  // Bound values in query_logs.bindings, credentials stored as `<redacted>`
266
268
  // (by name and shape, so not every secret); production keeps only each
267
269
  // value's type unless this is enabled. Env takes true/false, 1/0, yes/no, on/off.
268
270
  captureBindings: env.DB_QUERY_LOGGING_CAPTURE_BINDINGS ?? !['production', 'prod'].includes(env.APP_ENV || ''),
271
+ // More secret columns, on top of those names: 'code' in every table,
272
+ // 'gift_cards.code' in that one. Also taken out of a failed query's error.
273
+ sensitiveColumns: [],
269
274
  slowThreshold: 100, // ms
270
275
  retention: 7, // days
271
276
  pruneFrequency: 24, // hours
@@ -1,4 +1,4 @@
1
- import { Controller } from '@stacksjs/server'
1
+ import { Controller } from '@stacksjs/server/controllers/base'
2
2
  /**
3
3
  * Base Controller class providing Laravel-like functionality
4
4
  */
@@ -1,6 +1,6 @@
1
1
  import { config } from '@stacksjs/config'
2
2
  import { db, mutationCount, sql, sqlDateTime } from '@stacksjs/database/runtime'
3
- import { Controller } from '@stacksjs/server'
3
+ import { Controller } from '@stacksjs/server/controllers/base'
4
4
 
5
5
  const DAY_MS = 86_400_000
6
6
  /** `2026-09-10T11` - the stored timestamp truncated to its hour. */
@@ -60,6 +60,32 @@ function timingSafeEqual(a: string, b: string): boolean {
60
60
  return diff === 0
61
61
  }
62
62
 
63
+ /**
64
+ * The token a signed `<token>.<sig>` value carries, or null when the
65
+ * value is missing, unsigned or its signature does not match.
66
+ *
67
+ * The storefront pages read the cookie through this. They take no
68
+ * unsigned value: the actions accept one only to re-sign a cart that
69
+ * predates signing, and a page never writes the cookie.
70
+ */
71
+ export function verifyCartCookie(value: string | null | undefined): string | null {
72
+ if (!value || typeof value !== 'string') return null
73
+
74
+ const dot = value.lastIndexOf('.')
75
+ if (dot < 0) return null
76
+
77
+ const token = value.slice(0, dot)
78
+ const presented = value.slice(dot + 1)
79
+ if (!token || presented.length !== SIG_CHARS) return null
80
+
81
+ const expected = createHmac('sha256', getKey())
82
+ .update(token)
83
+ .digest('base64url')
84
+ .slice(0, SIG_CHARS)
85
+
86
+ return timingSafeEqual(expected, presented) ? token : null
87
+ }
88
+
63
89
  /**
64
90
  * Pull a verified cart token out of the request, or null if the
65
91
  * cookie is missing/malformed/forged. Legacy unsigned tokens
@@ -75,17 +101,7 @@ export function readCartCookie(request: any, cookieName: string): string | null
75
101
  // returns no row for unknown tokens — same fallback path.
76
102
  if (!raw.includes('.')) return raw
77
103
 
78
- const dot = raw.lastIndexOf('.')
79
- const token = raw.slice(0, dot)
80
- const presented = raw.slice(dot + 1)
81
- if (!token || presented.length !== SIG_CHARS) return null
82
-
83
- const expected = createHmac('sha256', getKey())
84
- .update(token)
85
- .digest('base64url')
86
- .slice(0, SIG_CHARS)
87
-
88
- return timingSafeEqual(expected, presented) ? token : null
104
+ return verifyCartCookie(raw)
89
105
  }
90
106
 
91
107
  /**
@@ -101,7 +117,7 @@ export function writeCartCookie(
101
117
  request.cookies?.set?.(cookieName, sign(token), opts)
102
118
  }
103
119
 
104
- export const cartCookie = { read: readCartCookie, write: writeCartCookie, sign }
120
+ export const cartCookie = { read: readCartCookie, write: writeCartCookie, sign, verify: verifyCartCookie }
105
121
  export default cartCookie
106
122
 
107
123
  // Re-export `SIG_BYTES` so tests can assert the byte budget without
@@ -0,0 +1,73 @@
1
+ /**
2
+ * How the storefront's server-rendered pages read the database.
3
+ *
4
+ * The cart, the three checkout steps and the storefront layout query
5
+ * SQLite directly from their `<script server>` blocks. Each used to open
6
+ * the file itself, and every way that could fail rendered an empty cart
7
+ * without a word in any log:
8
+ *
9
+ * - stx swallows an error thrown by a server script unless STX_DEBUG
10
+ * is set, and renders the page from the script's static values, so
11
+ * a missing column read as "Your cart is empty." No migration
12
+ * creates `carts.session_token`, which every one of these pages
13
+ * looks the cart up by.
14
+ * - The layout caught its own errors and said nothing, on purpose.
15
+ * - They opened the file read-only, and SQLite (as Bun 1.4.1 ships it)
16
+ * refuses a read-only connection to a WAL-mode database that has no
17
+ * `-shm` file beside it. The framework's query builder puts the
18
+ * database in WAL mode, so a page read worked only while a `-shm`
19
+ * file happened to exist.
20
+ * - Under a `DB_CONNECTION` other than sqlite they read whatever SQLite
21
+ * file `DB_DATABASE_PATH` or `database/stacks.sqlite` named, which
22
+ * is not the app's database.
23
+ *
24
+ * {@link readStorefront} is the one place those are handled: the page
25
+ * still degrades to its empty cart, and the server log says why.
26
+ */
27
+
28
+ import { Database } from 'bun:sqlite'
29
+ import { resolve } from 'node:path'
30
+ import process from 'node:process'
31
+
32
+ /** Each distinct warning once per process, so a broken schema is not one line per request. */
33
+ const warned = new Set<string>()
34
+
35
+ function warnOnce(message: string): void {
36
+ if (warned.has(message))
37
+ return
38
+
39
+ warned.add(message)
40
+ console.warn(message)
41
+ }
42
+
43
+ /**
44
+ * Run `read` against the app's SQLite database and return what it returns,
45
+ * or `fallback` when the database cannot be read, with a warning in the
46
+ * server log naming `page` and the reason.
47
+ *
48
+ * Opened read-write without `create`, with `query_only` on: it can make the
49
+ * `-shm` file a WAL-mode database needs, cannot create a database that is
50
+ * not there, and cannot write to one that is. Closed before returning.
51
+ */
52
+ export function readStorefront<T>(page: string, fallback: T, read: (db: Database) => T): T {
53
+ const connection = (process.env.DB_CONNECTION || 'sqlite').toLowerCase()
54
+ if (connection !== 'sqlite') {
55
+ warnOnce(`[storefront] ${page} shows no cart: it reads carts from SQLite, and DB_CONNECTION is ${connection}.`)
56
+ return fallback
57
+ }
58
+
59
+ const file = resolve(process.cwd(), process.env.DB_DATABASE_PATH || 'database/stacks.sqlite')
60
+ let db: Database | undefined
61
+ try {
62
+ db = new Database(file, { readwrite: true, create: false })
63
+ db.run('PRAGMA query_only = ON')
64
+ return read(db)
65
+ }
66
+ catch (error) {
67
+ warnOnce(`[storefront] ${page} shows no cart: reading ${file} failed: ${error instanceof Error ? error.message : String(error)}`)
68
+ return fallback
69
+ }
70
+ finally {
71
+ db?.close()
72
+ }
73
+ }
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.74.51",
5
+ "version": "0.74.53",
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.74.51",
5
+ "version": "0.74.53",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@iconify-json/f7": "^1.2.2",
57
57
  "@iconify-json/hugeicons": "^1.2.27",
58
- "@stacksjs/mobile": "^0.74.51",
58
+ "@stacksjs/mobile": "^0.74.53",
59
59
  "@stacksjs/sanitizer": "^0.2.113",
60
60
  "ts-qr-codes": "^0.1.8"
61
61
  }
@@ -19,27 +19,23 @@
19
19
  * the request handler.
20
20
  *
21
21
  * The cart count is read from the storefront's session cookie so the
22
- * badge is correct on first paint, before any client-side JS runs.
22
+ * badge is correct on first paint, before any client-side JS runs. A
23
+ * copy has to point the two requires below at the storefront helpers
24
+ * from wherever it sits.
23
25
  */
24
26
 
25
- const { cartTokenFromRequest } = require('../functions/storefront/cart-cookie')
26
- const Database = require('bun:sqlite').Database
27
- const path = require('path')
27
+ const { verifyCartCookie } = require('../../app/Storefront/CartCookie')
28
+ const { readStorefront } = require('../../app/Storefront/StorefrontDatabase')
28
29
 
29
- const dbPath = path.resolve(process.cwd(), process.env.DB_DATABASE_PATH || 'database/stacks.sqlite')
30
-
31
- let cartCount = 0
32
- try {
33
- const sessionToken = cartTokenFromRequest(request)
34
- if (sessionToken) {
35
- const db = new Database(dbPath, { readonly: true })
36
- const cart = db.query(`SELECT total_items FROM carts WHERE session_token = ?1 AND status = 'active' LIMIT 1`).get(sessionToken)
37
- if (cart && cart.total_items)
38
- cartCount = Number(cart.total_items) || 0
39
- db.close()
40
- }
41
- }
42
- catch { /* schema not migrated yet, or db missing — render an empty header */ }
30
+ // The same signed cookie the cart page reads. When the cart cannot be read
31
+ // the badge shows nothing, and the server log says why.
32
+ const token = verifyCartCookie(requestContext.cookie('stacks_cart'))
33
+ const cartCount = token
34
+ ? readStorefront('layouts/storefront.stx', 0, (db: import('bun:sqlite').Database) => {
35
+ const row = db.query<{ total_items: number | string | null }, [string]>(`SELECT total_items FROM carts WHERE session_token = ?1 AND status = 'active' LIMIT 1`).get(token)
36
+ return Number(row?.total_items) || 0
37
+ })
38
+ : 0
43
39
  </script>
44
40
 
45
41
  <!DOCTYPE html>
@@ -3,9 +3,7 @@
3
3
  @section('title', 'Your cart')
4
4
 
5
5
  <script server>
6
- const { cartTokenFromRequest } = require('../../functions/storefront/cart-cookie')
7
- // The columns this query selects. bun:sqlite returns `any`, so nothing
8
- // downstream of the map was checked either.
6
+ // The columns the line query selects.
9
7
  interface CartItemRow {
10
8
  id: number
11
9
  quantity: number | string
@@ -17,46 +15,56 @@ interface CartItemRow {
17
15
  weight_grams: number | null
18
16
  }
19
17
 
18
+ // The column of `carts` this page reads. The query selects `*`.
19
+ interface CartRow {
20
+ id: number
21
+ }
22
+
20
23
  interface StorefrontLineItem { lineTotal: number }
21
24
 
22
- const Database = require('bun:sqlite').Database
23
- const path = require('path')
24
-
25
- const dbPath = path.resolve(process.cwd(), process.env.DB_DATABASE_PATH || 'database/stacks.sqlite')
26
- const db = new Database(dbPath, { readonly: true })
27
-
28
- const token = cartTokenFromRequest(request)
29
- let items = []
30
- let cart = null
31
-
32
- if (token) {
33
- cart = db.query(`SELECT * FROM carts WHERE session_token = ?1 AND status = 'active' LIMIT 1`).get(token)
34
- if (cart) {
35
- items = db.query(`
36
- SELECT ci.id, ci.quantity, ci.unit_price, ci.total_price, ci.product_name,
37
- ci.product_image, ci.product_sku, p.weight_grams
38
- FROM cart_items ci
39
- LEFT JOIN products p ON p.slug = ci.product_sku
40
- WHERE ci.cart_id = ?1
41
- ORDER BY ci.id ASC
42
- `).all(cart.id).map((row: CartItemRow) => {
43
- const qty = Number(row.quantity)
44
- const price = Number(row.unit_price)
45
- const lineTotal = Number(row.total_price)
46
- return {
47
- id: row.id,
48
- slug: row.product_sku,
49
- name: row.product_name,
50
- weight: row.weight_grams ? `${row.weight_grams}g` : '',
51
- qty,
52
- priceLabel: `$${price.toFixed(2)}`,
53
- lineTotal,
54
- lineTotalLabel: `$${lineTotal.toFixed(2)}`,
55
- image: row.product_image,
56
- }
25
+ const { verifyCartCookie } = require('../../app/Storefront/CartCookie')
26
+ const { readStorefront } = require('../../app/Storefront/StorefrontDatabase')
27
+
28
+ // Signed by the storefront actions as the token, a dot and its HMAC. An
29
+ // unsigned or tampered cookie is no cart, and is never looked up.
30
+ const token = verifyCartCookie(requestContext.cookie('stacks_cart'))
31
+
32
+ const found = token
33
+ ? readStorefront('views/cart.stx', { cart: null, items: [] }, (db: import('bun:sqlite').Database) => {
34
+ const row = db.query<CartRow, [string]>(`SELECT * FROM carts WHERE session_token = ?1 AND status = 'active' LIMIT 1`).get(token)
35
+ if (!row)
36
+ return { cart: null, items: [] }
37
+
38
+ const lines = db.query<CartItemRow, [number]>(`
39
+ SELECT ci.id, ci.quantity, ci.unit_price, ci.total_price, ci.product_name,
40
+ ci.product_image, ci.product_sku, p.weight_grams
41
+ FROM cart_items ci
42
+ LEFT JOIN products p ON p.slug = ci.product_sku
43
+ WHERE ci.cart_id = ?1
44
+ ORDER BY ci.id ASC
45
+ `).all(row.id).map((line: CartItemRow) => {
46
+ const qty = Number(line.quantity)
47
+ const price = Number(line.unit_price)
48
+ const lineTotal = Number(line.total_price)
49
+ return {
50
+ id: line.id,
51
+ slug: line.product_sku,
52
+ name: line.product_name,
53
+ weight: line.weight_grams ? `${line.weight_grams}g` : '',
54
+ qty,
55
+ priceLabel: `$${price.toFixed(2)}`,
56
+ lineTotal,
57
+ lineTotalLabel: `$${lineTotal.toFixed(2)}`,
58
+ image: line.product_image,
59
+ }
60
+ })
61
+
62
+ return { cart: row, items: lines }
57
63
  })
58
- }
59
- }
64
+ : { cart: null, items: [] }
65
+
66
+ const cart = found.cart
67
+ const items = found.items
60
68
 
61
69
  // Mirror the rule used by PlaceOrderAction so the price the shopper
62
70
  // sees in their cart matches the total they're actually charged. Both
@@ -3,45 +3,55 @@
3
3
  @section('title', 'Checkout — Contact')
4
4
 
5
5
  <script server>
6
- const { cartTokenFromRequest } = require('../../../functions/storefront/cart-cookie')
7
- // The columns this query selects. bun:sqlite returns `any`, so nothing
8
- // downstream of the map was checked either.
6
+ // The columns the line query selects.
9
7
  interface CartSummaryRow {
10
8
  product_name: string
11
9
  quantity: number | string
12
10
  total_price: number | string
13
11
  }
14
12
 
13
+ // The columns of `carts` this page reads. The query selects `*`, and no
14
+ // migration creates any but `id` (see app/Storefront/StorefrontDatabase.ts).
15
+ interface CartRow {
16
+ id: number
17
+ email: string | null
18
+ }
19
+
15
20
  interface StorefrontLineItem { lineTotal: number }
16
21
 
17
- const Database = require('bun:sqlite').Database
18
- const path = require('path')
19
-
20
- const dbPath = path.resolve(process.cwd(), process.env.DB_DATABASE_PATH || 'database/stacks.sqlite')
21
- const db = new Database(dbPath, { readonly: true })
22
-
23
- const token = cartTokenFromRequest(request)
24
- let items = []
25
- let cart = null
26
-
27
- if (token) {
28
- cart = db.query(`SELECT * FROM carts WHERE session_token = ?1 AND status = 'active' LIMIT 1`).get(token)
29
- if (cart) {
30
- items = db.query(`
31
- SELECT product_name, quantity, total_price
32
- FROM cart_items WHERE cart_id = ?1 ORDER BY id ASC
33
- `).all(cart.id).map((row: CartSummaryRow) => {
34
- const qty = Number(row.quantity)
35
- const lineTotal = Number(row.total_price)
36
- return {
37
- name: row.product_name,
38
- qty,
39
- lineTotal,
40
- lineTotalLabel: `$${lineTotal.toFixed(2)}`,
41
- }
22
+ const { verifyCartCookie } = require('../../../app/Storefront/CartCookie')
23
+ const { readStorefront } = require('../../../app/Storefront/StorefrontDatabase')
24
+
25
+ // Signed by the storefront actions as the token, a dot and its HMAC. An
26
+ // unsigned or tampered cookie is no cart, and is never looked up.
27
+ const token = verifyCartCookie(requestContext.cookie('stacks_cart'))
28
+
29
+ const found = token
30
+ ? readStorefront('views/checkout/contact.stx', { cart: null, items: [] }, (db: import('bun:sqlite').Database) => {
31
+ const row = db.query<CartRow, [string]>(`SELECT * FROM carts WHERE session_token = ?1 AND status = 'active' LIMIT 1`).get(token)
32
+ if (!row)
33
+ return { cart: null, items: [] }
34
+
35
+ const lines = db.query<CartSummaryRow, [number]>(`
36
+ SELECT product_name, quantity, total_price
37
+ FROM cart_items WHERE cart_id = ?1 ORDER BY id ASC
38
+ `).all(row.id).map((line: CartSummaryRow) => {
39
+ const qty = Number(line.quantity)
40
+ const lineTotal = Number(line.total_price)
41
+ return {
42
+ name: line.product_name,
43
+ qty,
44
+ lineTotal,
45
+ lineTotalLabel: `$${lineTotal.toFixed(2)}`,
46
+ }
47
+ })
48
+
49
+ return { cart: row, items: lines }
42
50
  })
43
- }
44
- }
51
+ : { cart: null, items: [] }
52
+
53
+ const cart = found.cart
54
+ const items = found.items
45
55
 
46
56
  // Mirror the rule in PlaceOrderAction's _shipping helper so the running
47
57
  // totals on every checkout step match what gets charged.
@@ -3,45 +3,60 @@
3
3
  @section('title', 'Checkout — Payment')
4
4
 
5
5
  <script server>
6
- const { cartTokenFromRequest } = require('../../../functions/storefront/cart-cookie')
7
- // The columns this query selects. bun:sqlite returns `any`, so nothing
8
- // downstream of the map was checked either.
6
+ // The columns the line query selects.
9
7
  interface CartSummaryRow {
10
8
  product_name: string
11
9
  quantity: number | string
12
10
  total_price: number | string
13
11
  }
14
12
 
13
+ // The columns of `carts` this page reads. The query selects `*`, and no
14
+ // migration creates any but `id` (see app/Storefront/StorefrontDatabase.ts).
15
+ interface CartRow {
16
+ id: number
17
+ email: string | null
18
+ shipping_name: string | null
19
+ shipping_address: string | null
20
+ shipping_city: string | null
21
+ shipping_state: string | null
22
+ shipping_zip: string | null
23
+ }
24
+
15
25
  interface StorefrontLineItem { lineTotal: number }
16
26
 
17
- const Database = require('bun:sqlite').Database
18
- const path = require('path')
19
-
20
- const dbPath = path.resolve(process.cwd(), process.env.DB_DATABASE_PATH || 'database/stacks.sqlite')
21
- const db = new Database(dbPath, { readonly: true })
22
-
23
- const token = cartTokenFromRequest(request)
24
- let items = []
25
- let cart = null
26
-
27
- if (token) {
28
- cart = db.query(`SELECT * FROM carts WHERE session_token = ?1 AND status = 'active' LIMIT 1`).get(token)
29
- if (cart) {
30
- items = db.query(`
31
- SELECT product_name, quantity, total_price
32
- FROM cart_items WHERE cart_id = ?1 ORDER BY id ASC
33
- `).all(cart.id).map((row: CartSummaryRow) => {
34
- const qty = Number(row.quantity)
35
- const lineTotal = Number(row.total_price)
36
- return {
37
- name: row.product_name,
38
- qty,
39
- lineTotal,
40
- lineTotalLabel: `$${lineTotal.toFixed(2)}`,
41
- }
27
+ const { verifyCartCookie } = require('../../../app/Storefront/CartCookie')
28
+ const { readStorefront } = require('../../../app/Storefront/StorefrontDatabase')
29
+
30
+ // Signed by the storefront actions as the token, a dot and its HMAC. An
31
+ // unsigned or tampered cookie is no cart, and is never looked up.
32
+ const token = verifyCartCookie(requestContext.cookie('stacks_cart'))
33
+
34
+ const found = token
35
+ ? readStorefront('views/checkout/payment.stx', { cart: null, items: [] }, (db: import('bun:sqlite').Database) => {
36
+ const row = db.query<CartRow, [string]>(`SELECT * FROM carts WHERE session_token = ?1 AND status = 'active' LIMIT 1`).get(token)
37
+ if (!row)
38
+ return { cart: null, items: [] }
39
+
40
+ const lines = db.query<CartSummaryRow, [number]>(`
41
+ SELECT product_name, quantity, total_price
42
+ FROM cart_items WHERE cart_id = ?1 ORDER BY id ASC
43
+ `).all(row.id).map((line: CartSummaryRow) => {
44
+ const qty = Number(line.quantity)
45
+ const lineTotal = Number(line.total_price)
46
+ return {
47
+ name: line.product_name,
48
+ qty,
49
+ lineTotal,
50
+ lineTotalLabel: `$${lineTotal.toFixed(2)}`,
51
+ }
52
+ })
53
+
54
+ return { cart: row, items: lines }
42
55
  })
43
- }
44
- }
56
+ : { cart: null, items: [] }
57
+
58
+ const cart = found.cart
59
+ const items = found.items
45
60
 
46
61
  const FREE_SHIPPING_THRESHOLD = Number(process.env.STOREFRONT_FREE_SHIPPING_THRESHOLD ?? 40) || 40
47
62
  const FLAT_SHIPPING = Number(process.env.STOREFRONT_FLAT_SHIPPING ?? 5) || 5
@@ -3,45 +3,60 @@
3
3
  @section('title', 'Checkout — Shipping')
4
4
 
5
5
  <script server>
6
- const { cartTokenFromRequest } = require('../../../functions/storefront/cart-cookie')
7
- // The columns this query selects. bun:sqlite returns `any`, so nothing
8
- // downstream of the map was checked either.
6
+ // The columns the line query selects.
9
7
  interface CartSummaryRow {
10
8
  product_name: string
11
9
  quantity: number | string
12
10
  total_price: number | string
13
11
  }
14
12
 
13
+ // The columns of `carts` this page reads. The query selects `*`, and no
14
+ // migration creates any but `id` (see app/Storefront/StorefrontDatabase.ts).
15
+ interface CartRow {
16
+ id: number
17
+ email: string | null
18
+ shipping_name: string | null
19
+ shipping_address: string | null
20
+ shipping_city: string | null
21
+ shipping_state: string | null
22
+ shipping_zip: string | null
23
+ }
24
+
15
25
  interface StorefrontLineItem { lineTotal: number }
16
26
 
17
- const Database = require('bun:sqlite').Database
18
- const path = require('path')
19
-
20
- const dbPath = path.resolve(process.cwd(), process.env.DB_DATABASE_PATH || 'database/stacks.sqlite')
21
- const db = new Database(dbPath, { readonly: true })
22
-
23
- const token = cartTokenFromRequest(request)
24
- let items = []
25
- let cart = null
26
-
27
- if (token) {
28
- cart = db.query(`SELECT * FROM carts WHERE session_token = ?1 AND status = 'active' LIMIT 1`).get(token)
29
- if (cart) {
30
- items = db.query(`
31
- SELECT product_name, quantity, total_price
32
- FROM cart_items WHERE cart_id = ?1 ORDER BY id ASC
33
- `).all(cart.id).map((row: CartSummaryRow) => {
34
- const qty = Number(row.quantity)
35
- const lineTotal = Number(row.total_price)
36
- return {
37
- name: row.product_name,
38
- qty,
39
- lineTotal,
40
- lineTotalLabel: `$${lineTotal.toFixed(2)}`,
41
- }
27
+ const { verifyCartCookie } = require('../../../app/Storefront/CartCookie')
28
+ const { readStorefront } = require('../../../app/Storefront/StorefrontDatabase')
29
+
30
+ // Signed by the storefront actions as the token, a dot and its HMAC. An
31
+ // unsigned or tampered cookie is no cart, and is never looked up.
32
+ const token = verifyCartCookie(requestContext.cookie('stacks_cart'))
33
+
34
+ const found = token
35
+ ? readStorefront('views/checkout/shipping.stx', { cart: null, items: [] }, (db: import('bun:sqlite').Database) => {
36
+ const row = db.query<CartRow, [string]>(`SELECT * FROM carts WHERE session_token = ?1 AND status = 'active' LIMIT 1`).get(token)
37
+ if (!row)
38
+ return { cart: null, items: [] }
39
+
40
+ const lines = db.query<CartSummaryRow, [number]>(`
41
+ SELECT product_name, quantity, total_price
42
+ FROM cart_items WHERE cart_id = ?1 ORDER BY id ASC
43
+ `).all(row.id).map((line: CartSummaryRow) => {
44
+ const qty = Number(line.quantity)
45
+ const lineTotal = Number(line.total_price)
46
+ return {
47
+ name: line.product_name,
48
+ qty,
49
+ lineTotal,
50
+ lineTotalLabel: `$${lineTotal.toFixed(2)}`,
51
+ }
52
+ })
53
+
54
+ return { cart: row, items: lines }
42
55
  })
43
- }
44
- }
56
+ : { cart: null, items: [] }
57
+
58
+ const cart = found.cart
59
+ const items = found.items
45
60
 
46
61
  const FREE_SHIPPING_THRESHOLD = Number(process.env.STOREFRONT_FREE_SHIPPING_THRESHOLD ?? 40) || 40
47
62
  const FLAT_SHIPPING = Number(process.env.STOREFRONT_FLAT_SHIPPING ?? 5) || 5
@@ -1,58 +0,0 @@
1
- /**
2
- * Read the storefront cart token out of the request.
3
- *
4
- * The cart and the three checkout steps each opened with
5
- *
6
- * const token = (typeof requestContext !== 'undefined')
7
- * ? requestContext.cookie('stacks_cart')
8
- * : null
9
- *
10
- * and `requestContext` is not a binding stx has ever put in a server script's
11
- * scope. It is the NAME OF A PARAMETER inside the template hydrator, and the
12
- * page context a server assembles carries `params`, `request` and `method`.
13
- * So the `typeof` guard was always false, `token` was always null, and every
14
- * one of those pages rendered its empty-cart branch no matter what was in the
15
- * cart. The guard is what hid it: without it the pages would have thrown on
16
- * the first request instead of quietly showing nothing.
17
- *
18
- * `request` is really provided, so the cookie is read from its header.
19
- */
20
-
21
- /** The cookie the storefront stores its cart session token in. */
22
- export const CART_COOKIE = 'stacks_cart'
23
-
24
- /**
25
- * The value of `name` in a request's Cookie header, or null.
26
- *
27
- * Values are percent-encoded on the way in, so they are decoded here. A
28
- * malformed encoding yields the raw value rather than throwing, since a
29
- * cookie is attacker-supplied and a render must not fail on one.
30
- */
31
- export function cookieFromRequest(request: Request | undefined, name: string): string | null {
32
- const header = request?.headers?.get('cookie')
33
- if (!header)
34
- return null
35
-
36
- for (const part of header.split(';')) {
37
- const index = part.indexOf('=')
38
- if (index < 0)
39
- continue
40
- if (part.slice(0, index).trim() !== name)
41
- continue
42
-
43
- const raw = part.slice(index + 1).trim()
44
- try {
45
- return decodeURIComponent(raw)
46
- }
47
- catch {
48
- return raw
49
- }
50
- }
51
-
52
- return null
53
- }
54
-
55
- /** The storefront cart token carried by this request, or null. */
56
- export function cartTokenFromRequest(request: Request | undefined): string | null {
57
- return cookieFromRequest(request, CART_COOKIE)
58
- }