@stacksjs/defaults 0.74.24 → 0.74.25

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.
@@ -80,7 +80,9 @@ The dev server watches these directories for changes and auto-reloads:
80
80
  - `app/Actions/` -- invalidates module cache, hot-reloads actions
81
81
  - `app/Controllers/` -- invalidates module cache, hot-reloads controllers
82
82
  - `app/Middleware/` -- invalidates module cache, hot-reloads middleware
83
- - `app/Models/` (user models) -- triggers `buddy generate:model-files`
83
+ - `app/Models/` (user models) -- regenerates the model artifacts (there is no
84
+ `generate:model-files` command; the dev server does this itself, and
85
+ `buddy generate:types` is the manual equivalent)
84
86
 
85
87
  Production build: `bun build.ts` bundles `dev.ts` to `dist/dev.js`.
86
88
 
@@ -805,7 +807,9 @@ the foreign-key attributes derived from `belongsTo`. For example,
805
807
  ## CLI Commands
806
808
 
807
809
  - `buddy dev` -- starts the API server with hot-reload
808
- - `buddy generate:api-types` -- regenerates API TypeScript types from OpenAPI spec
810
+ - `buddy generate:types` -- regenerates the API TypeScript types and server
811
+ declarations (there is no `generate:api-types`)
812
+ - `buddy generate:openapi-spec` -- regenerates the OpenAPI spec those types come from
809
813
 
810
814
  ## Gotchas
811
815
 
@@ -45,7 +45,7 @@ up.
45
45
  fixture payload.
46
46
  3. **CLI invocation**, for instance `buddy <command>` with a fixture input,
47
47
  diffing stdout against a known-good snapshot.
48
- 4. **REPL probe**. `buddy repl` reaches models, config and the query builder
48
+ 4. **REPL probe**. `buddy tinker` reaches models, config and the query builder
49
49
  directly, which is the fastest loop for an ORM or relationship bug.
50
50
  5. **Headless browser script**. `/stacks-browse` drives a real browser over CDP
51
51
  and asserts on DOM, console and network with nothing to install.
@@ -177,7 +177,7 @@ time.**
177
177
  Tool preference:
178
178
 
179
179
  1. **REPL or debugger inspection** where the environment supports it. One
180
- breakpoint beats ten logs, and `buddy repl` is usually reachable.
180
+ breakpoint beats ten logs, and `buddy tinker` is usually reachable.
181
181
  2. **Targeted logs** at the boundaries that distinguish the hypotheses, via
182
182
  `log.debug()` from `@stacksjs/logging`.
183
183
  3. Never "log everything and grep".
@@ -1,5 +1,4 @@
1
1
  import { randomBytes, timingSafeEqual } from 'node:crypto'
2
- import { Buffer } from 'node:buffer'
3
2
  import { HttpError } from '@stacksjs/error-handling'
4
3
  import type { EnhancedRequest } from '@stacksjs/bun-router'
5
4
  import { Middleware } from '@stacksjs/router'
@@ -62,6 +61,7 @@ import { Middleware } from '@stacksjs/router'
62
61
  export const CSRF_COOKIE_NAME = 'X-CSRF-Token'
63
62
  const CSRF_HEADER_NAME = 'x-csrf-token'
64
63
  const TOKEN_BYTES = 32
64
+ const TOKEN_ENCODER = new TextEncoder()
65
65
 
66
66
  /**
67
67
  * Generate a fresh CSRF token (hex-encoded, 32 random bytes → 64 chars).
@@ -168,21 +168,25 @@ export function seedCsrfCookieIfMissing(req: Request, response: Response, minted
168
168
  }
169
169
 
170
170
  /**
171
- * Parse the Cookie header into a key→value map.
172
- * Lenient: malformed pairs are skipped, not thrown.
171
+ * Read just the CSRF cookies without building a map of unrelated cookies.
172
+ * Last duplicate wins; the canonical name takes precedence over the legacy
173
+ * name unless its final value is empty. Malformed pairs are skipped.
173
174
  */
174
- function parseCookies(req: Request): Record<string, string> {
175
+ function csrfCookieToken(req: Request): string {
175
176
  const header = req.headers.get('cookie')
176
- if (!header) return {}
177
- const out: Record<string, string> = {}
177
+ if (!header) return ''
178
+ let canonical = ''
179
+ let legacy = ''
178
180
  for (const part of header.split(';')) {
179
181
  const idx = part.indexOf('=')
180
182
  if (idx === -1) continue
181
- const k = part.slice(0, idx).trim()
182
- const v = part.slice(idx + 1).trim()
183
- if (k) out[k] = v
183
+ const name = part.slice(0, idx).trim()
184
+ if (name === CSRF_COOKIE_NAME)
185
+ canonical = part.slice(idx + 1).trim()
186
+ else if (name === 'csrf-token')
187
+ legacy = part.slice(idx + 1).trim()
184
188
  }
185
- return out
189
+ return canonical || legacy
186
190
  }
187
191
 
188
192
  /**
@@ -193,7 +197,7 @@ function safeEqual(a: string, b: string): boolean {
193
197
  if (typeof a !== 'string' || typeof b !== 'string') return false
194
198
  if (a.length !== b.length) return false
195
199
  try {
196
- return timingSafeEqual(Buffer.from(a), Buffer.from(b))
200
+ return timingSafeEqual(TOKEN_ENCODER.encode(a), TOKEN_ENCODER.encode(b))
197
201
  }
198
202
  catch {
199
203
  return false
@@ -256,8 +260,7 @@ export async function validateCsrfRequest(request: Request | EnhancedRequest): P
256
260
  || (typeof bodyToken === 'string' && bodyToken)
257
261
  || ''
258
262
 
259
- const cookies = parseCookies(request)
260
- const cookieToken = cookies[CSRF_COOKIE_NAME] || cookies['csrf-token'] || ''
263
+ const cookieToken = csrfCookieToken(request)
261
264
 
262
265
  if (!submitted || !cookieToken || !safeEqual(submitted, cookieToken)) {
263
266
  // 419 is the convention Laravel popularized for "CSRF token
@@ -33,10 +33,23 @@ export default defineModel({
33
33
  useApi: {
34
34
  uri: 'boards',
35
35
  routes: ['index', 'store', 'show', 'update', 'destroy'],
36
- middleware: ['auth'],
36
+ // Team-owned since #2412, so the active-team guard applies - the
37
+ // contract in tests/unit/default-team-api-scope-contract.test.ts.
38
+ middleware: ['auth', 'team'],
37
39
  },
38
40
  },
39
41
 
42
+ /*
43
+ * A board belongs to a team, which is what makes it - and everything on it -
44
+ * scopable. Without this, `Board` had no foreign key at all, so row scoping
45
+ * had nothing to resolve and withheld `store`/`update`/`destroy`: a kanban
46
+ * board with no create endpoint (stacksjs/stacks#2412).
47
+ *
48
+ * `BoardColumn` and `Label` chain to here, so this one column scopes all
49
+ * three.
50
+ */
51
+ belongsTo: ['Team'],
52
+
40
53
  hasMany: ['BoardColumn', 'Label'],
41
54
 
42
55
  attributes: {
@@ -1,4 +1,4 @@
1
- import { defineModel } from '@stacksjs/orm'
1
+ import { defineModel, parentOwnership } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  /**
@@ -40,6 +40,9 @@ export default defineModel({
40
40
  },
41
41
 
42
42
  belongsTo: ['Board'],
43
+
44
+ // Owned by whoever owns the board it sits on (stacksjs/stacks#2412).
45
+ ownership: parentOwnership('Board', 'board_id'),
43
46
  hasMany: ['Card'],
44
47
 
45
48
  attributes: {
@@ -1,4 +1,4 @@
1
- import { defineModel } from '@stacksjs/orm'
1
+ import { defineModel, parentOwnership } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  /**
@@ -39,6 +39,9 @@ export default defineModel({
39
39
 
40
40
  belongsTo: ['Board'],
41
41
 
42
+ // Owned by whoever owns the board it sits on (stacksjs/stacks#2412).
43
+ ownership: parentOwnership('Board', 'board_id'),
44
+
42
45
  attributes: {
43
46
  boardId: {
44
47
  order: 1,
@@ -7,6 +7,17 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+
11
+ /*
12
+ * A delivery policy, not a delivery. Its columns are `name`,
13
+ * `downloadLimit`, `expiryDays`, `requiresLogin` and `automaticDelivery` -
14
+ * a reusable configuration attached to what is sold, with no customer, order
15
+ * or file on it. #2412 guessed this "almost certainly belongs to an order or
16
+ * a customer"; the attributes say otherwise, so it is declared a catalog
17
+ * record and its writes want an admin gate rather than row scoping.
18
+ */
19
+ ownership: false,
20
+
10
21
  traits: {
11
22
  useUuid: true,
12
23
  useTimestamps: true,
@@ -1,4 +1,4 @@
1
- import { defineModel } from '@stacksjs/orm'
1
+ import { customerOwnership, defineModel } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  export default defineModel({
@@ -7,6 +7,18 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+
11
+ /*
12
+ * Points belong to a customer. The model carried `walletId`, pointing at a
13
+ * `LoyaltyWallet` that was never built, so nothing resolved an owner and the
14
+ * writes stayed denied (stacksjs/stacks#2412). Scoping by customer is the
15
+ * additive half of that issue's second option; `walletId` is left alone
16
+ * because repointing an existing column is a data decision of its own.
17
+ */
18
+ belongsTo: ['Customer'],
19
+
20
+ ownership: customerOwnership(),
21
+
10
22
  traits: {
11
23
  useUuid: true,
12
24
  useTimestamps: true,
@@ -1,4 +1,4 @@
1
- import { defineModel } from '@stacksjs/orm'
1
+ import { customerOwnership, defineModel } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  /**
@@ -34,7 +34,14 @@ export default defineModel({
34
34
  observe: true,
35
35
  },
36
36
 
37
- belongsTo: ['Auction'],
37
+ /*
38
+ * A pledge is made BY someone. It chained only to `Auction`, an unscoped
39
+ * catalog, so there was no owner to resolve and its writes stayed denied
40
+ * (stacksjs/stacks#2412).
41
+ */
42
+ belongsTo: ['Auction', 'Customer'],
43
+
44
+ ownership: customerOwnership(),
38
45
 
39
46
  indexes: [
40
47
  { name: 'pledges_auction_id_index', columns: ['auction_id'] },
@@ -18,6 +18,14 @@ export default defineModel({
18
18
  primaryKey: 'id',
19
19
  autoIncrement: true,
20
20
  belongsTo: ['PrintDevice'],
21
+
22
+ /*
23
+ * A receipt is a record OF a device, not of a customer - it chains to
24
+ * `PrintDevice`, an unscoped catalog. Saying so explicitly is the honest
25
+ * declaration; its writes want an admin gate rather than row scoping
26
+ * (stacksjs/stacks#2412).
27
+ */
28
+ ownership: false,
21
29
  traits: {
22
30
  useUuid: true,
23
31
  useTimestamps: true,
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.74.24",
5
+ "version": "0.74.25",
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.24",
5
+ "version": "0.74.25",
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.24",
58
+ "@stacksjs/mobile": "^0.74.25",
59
59
  "@stacksjs/sanitizer": "^0.2.113",
60
60
  "ts-qr-codes": "^0.1.8"
61
61
  }