@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.
- package/ai/skills/stacks-api/SKILL.md +6 -2
- package/ai/skills/stacks-investigate/SKILL.md +2 -2
- package/app/Middleware/Csrf.ts +16 -13
- package/app/Models/Board.ts +14 -1
- package/app/Models/BoardColumn.ts +4 -1
- package/app/Models/Label.ts +4 -1
- package/app/Models/commerce/DigitalDelivery.ts +11 -0
- package/app/Models/commerce/LoyaltyPoint.ts +13 -1
- package/app/Models/commerce/Pledge.ts +9 -2
- package/app/Models/commerce/Receipt.ts +8 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
|
@@ -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) --
|
|
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:
|
|
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
|
|
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
|
|
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".
|
package/app/Middleware/Csrf.ts
CHANGED
|
@@ -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
|
-
*
|
|
172
|
-
*
|
|
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
|
|
175
|
+
function csrfCookieToken(req: Request): string {
|
|
175
176
|
const header = req.headers.get('cookie')
|
|
176
|
-
if (!header) return
|
|
177
|
-
|
|
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
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
package/app/Models/Board.ts
CHANGED
|
@@ -33,10 +33,23 @@ export default defineModel({
|
|
|
33
33
|
useApi: {
|
|
34
34
|
uri: 'boards',
|
|
35
35
|
routes: ['index', 'store', 'show', 'update', 'destroy'],
|
|
36
|
-
|
|
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: {
|
package/app/Models/Label.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/ide/vscode/package.json
CHANGED
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.
|
|
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.
|
|
58
|
+
"@stacksjs/mobile": "^0.74.25",
|
|
59
59
|
"@stacksjs/sanitizer": "^0.2.113",
|
|
60
60
|
"ts-qr-codes": "^0.1.8"
|
|
61
61
|
}
|