@stacksjs/defaults 0.74.0 → 0.74.2

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 (50) hide show
  1. package/app/Models/AnalyticsEvent.ts +5 -0
  2. package/app/Models/Content/Menu.ts +5 -1
  3. package/app/Models/Content/MenuItem.ts +6 -1
  4. package/app/Models/Content/Page.ts +5 -1
  5. package/app/Models/Content/Post.ts +5 -1
  6. package/app/Models/Content/Redirect.ts +5 -1
  7. package/app/Models/EmailIdempotency.ts +5 -0
  8. package/app/Models/EmailList.ts +6 -0
  9. package/app/Models/EmailSuppression.ts +5 -0
  10. package/app/Models/EmailWebhookEvent.ts +5 -0
  11. package/app/Models/Forms/Form.ts +5 -1
  12. package/app/Models/Forms/FormField.ts +6 -1
  13. package/app/Models/MailPreference.ts +5 -0
  14. package/app/Models/Request.ts +5 -0
  15. package/app/Models/SiteDomain.ts +5 -1
  16. package/app/Models/Tag.ts +6 -0
  17. package/app/Models/Team.ts +6 -1
  18. package/app/Models/User.ts +6 -1
  19. package/app/Models/commerce/Auction.ts +6 -0
  20. package/app/Models/commerce/AuctionItem.ts +6 -0
  21. package/app/Models/commerce/Cart.ts +6 -1
  22. package/app/Models/commerce/CartItem.ts +6 -1
  23. package/app/Models/commerce/Category.ts +6 -0
  24. package/app/Models/commerce/Coupon.ts +6 -0
  25. package/app/Models/commerce/DeliveryRoute.ts +6 -1
  26. package/app/Models/commerce/DeliveryStop.ts +6 -1
  27. package/app/Models/commerce/GiftCard.ts +6 -1
  28. package/app/Models/commerce/LicenseKey.ts +6 -1
  29. package/app/Models/commerce/LoyaltyReward.ts +6 -0
  30. package/app/Models/commerce/Manufacturer.ts +6 -0
  31. package/app/Models/commerce/Order.ts +6 -1
  32. package/app/Models/commerce/Payment.ts +6 -1
  33. package/app/Models/commerce/PrintDevice.ts +6 -0
  34. package/app/Models/commerce/Product.ts +6 -0
  35. package/app/Models/commerce/ProductUnit.ts +6 -0
  36. package/app/Models/commerce/ProductVariant.ts +6 -0
  37. package/app/Models/commerce/Review.ts +6 -1
  38. package/app/Models/commerce/ShippingMethod.ts +6 -0
  39. package/app/Models/commerce/ShippingRate.ts +6 -0
  40. package/app/Models/commerce/ShippingZone.ts +6 -0
  41. package/app/Models/commerce/TaxRate.ts +6 -0
  42. package/app/Models/commerce/Transaction.ts +6 -1
  43. package/app/Models/commerce/WaitlistProduct.ts +6 -1
  44. package/app/Models/commerce/WaitlistRestaurant.ts +6 -1
  45. package/app/Models/realtime/Websocket.ts +5 -0
  46. package/ide/vscode/package.json +1 -1
  47. package/package.json +2 -2
  48. package/project/storage/framework/tsconfig.app.json +4 -1
  49. package/resources/views/cms/blocks/form.stx +92 -1
  50. package/routes/forms.ts +46 -0
@@ -7,6 +7,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // An infrastructure table: rows are written by the system, not on behalf of a
11
+ // caller, so no row has a per-caller owner to scope by. Writes are gated by
12
+ // `middleware` instead. Declared rather than left silent (stacksjs/stacks#2375).
13
+ ownership: false,
14
+
10
15
  traits: {
11
16
  useUuid: true,
12
17
  useTimestamps: true,
@@ -1,4 +1,4 @@
1
- import { defineModel } from '@stacksjs/orm'
1
+ import { defineModel, siteOwnership } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  /**
@@ -20,6 +20,10 @@ export default defineModel({
20
20
  },
21
21
  ],
22
22
 
23
+ // Owned through the site, which belongs to a team, so the owner is the set of
24
+ // site ids the caller's team owns rather than a single id (stacksjs/stacks#2375).
25
+ ownership: siteOwnership(),
26
+
23
27
  traits: {
24
28
  useTimestamps: true,
25
29
  useApi: {
@@ -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
  /**
@@ -11,6 +11,11 @@ export default defineModel({
11
11
  primaryKey: 'id',
12
12
  autoIncrement: true,
13
13
 
14
+ // No owner of its own: these rows are owned by whoever owns the menu's site, and so its team
15
+ // (stacksjs/stacks#2375). Resolved through the parent so it follows any change
16
+ // to how Menu decides ownership.
17
+ ownership: parentOwnership('Menu', 'menu_id'),
18
+
14
19
  traits: {
15
20
  useTimestamps: true,
16
21
  useApi: {
@@ -1,4 +1,4 @@
1
- import { defineModel } from '@stacksjs/orm'
1
+ import { defineModel, siteOwnership } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  /**
@@ -28,6 +28,10 @@ export default defineModel({
28
28
  },
29
29
  ],
30
30
 
31
+ // Owned through the site, which belongs to a team, so the owner is the set of
32
+ // site ids the caller's team owns rather than a single id (stacksjs/stacks#2375).
33
+ ownership: siteOwnership(),
34
+
31
35
  traits: {
32
36
  useUuid: true,
33
37
  useTimestamps: true,
@@ -1,4 +1,4 @@
1
- import { defineModel } from '@stacksjs/orm'
1
+ import { defineModel, siteOwnership } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  export default defineModel({
@@ -7,6 +7,10 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // Owned through the site, which belongs to a team, so the owner is the set of
11
+ // site ids the caller's team owns rather than a single id (stacksjs/stacks#2375).
12
+ ownership: siteOwnership(),
13
+
10
14
  traits: {
11
15
  useUuid: true,
12
16
  useTimestamps: true,
@@ -1,4 +1,4 @@
1
- import { defineModel } from '@stacksjs/orm'
1
+ import { defineModel, siteOwnership } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  /**
@@ -21,6 +21,10 @@ export default defineModel({
21
21
  },
22
22
  ],
23
23
 
24
+ // Owned through the site, which belongs to a team, so the owner is the set of
25
+ // site ids the caller's team owns rather than a single id (stacksjs/stacks#2375).
26
+ ownership: siteOwnership(),
27
+
24
28
  traits: {
25
29
  useTimestamps: true,
26
30
  useApi: {
@@ -7,6 +7,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // An infrastructure table: rows are written by the system, not on behalf of a
11
+ // caller, so no row has a per-caller owner to scope by. Writes are gated by
12
+ // `middleware` instead. Declared rather than left silent (stacksjs/stacks#2375).
13
+ ownership: false,
14
+
10
15
  traits: {
11
16
  useTimestamps: true,
12
17
  useApi: {
@@ -8,6 +8,12 @@ export default defineModel({
8
8
  autoIncrement: true,
9
9
  hasMany: ['EmailListSubscriber', 'Campaign', 'CampaignSend'],
10
10
 
11
+ // A reference table: no row here has a per-caller owner, so there is nothing
12
+ // to scope by and writes are an administrative concern gated by `middleware`.
13
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
14
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
15
+ ownership: false,
16
+
11
17
  traits: {
12
18
  useUuid: true,
13
19
  useTimestamps: true,
@@ -15,6 +15,11 @@ export default defineModel({
15
15
  },
16
16
  ],
17
17
 
18
+ // An infrastructure table: rows are written by the system, not on behalf of a
19
+ // caller, so no row has a per-caller owner to scope by. Writes are gated by
20
+ // `middleware` instead. Declared rather than left silent (stacksjs/stacks#2375).
21
+ ownership: false,
22
+
18
23
  traits: {
19
24
  useTimestamps: true,
20
25
  useApi: {
@@ -15,6 +15,11 @@ export default defineModel({
15
15
  },
16
16
  ],
17
17
 
18
+ // An infrastructure table: rows are written by the system, not on behalf of a
19
+ // caller, so no row has a per-caller owner to scope by. Writes are gated by
20
+ // `middleware` instead. Declared rather than left silent (stacksjs/stacks#2375).
21
+ ownership: false,
22
+
18
23
  traits: {
19
24
  useTimestamps: true,
20
25
  useApi: {
@@ -1,4 +1,4 @@
1
- import { defineModel } from '@stacksjs/orm'
1
+ import { defineModel, siteOwnership } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  /**
@@ -23,6 +23,10 @@ export default defineModel({
23
23
  },
24
24
  ],
25
25
 
26
+ // Owned through the site, which belongs to a team, so the owner is the set of
27
+ // site ids the caller's team owns rather than a single id (stacksjs/stacks#2375).
28
+ ownership: siteOwnership(),
29
+
26
30
  traits: {
27
31
  useUuid: true,
28
32
  useTimestamps: true,
@@ -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
  /**
@@ -21,6 +21,11 @@ export default defineModel({
21
21
  },
22
22
  ],
23
23
 
24
+ // No owner of its own: these rows are owned by whoever owns the form's site, and so its team
25
+ // (stacksjs/stacks#2375). Resolved through the parent so it follows any change
26
+ // to how Form decides ownership.
27
+ ownership: parentOwnership('Form', 'form_id'),
28
+
24
29
  traits: {
25
30
  useTimestamps: true,
26
31
  useApi: {
@@ -7,6 +7,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // An infrastructure table: rows are written by the system, not on behalf of a
11
+ // caller, so no row has a per-caller owner to scope by. Writes are gated by
12
+ // `middleware` instead. Declared rather than left silent (stacksjs/stacks#2375).
13
+ ownership: false,
14
+
10
15
  traits: {
11
16
  useTimestamps: true,
12
17
  useApi: {
@@ -22,6 +22,11 @@ export default defineModel({
22
22
  },
23
23
  ],
24
24
 
25
+ // An infrastructure table: rows are written by the system, not on behalf of a
26
+ // caller, so no row has a per-caller owner to scope by. Writes are gated by
27
+ // `middleware` instead. Declared rather than left silent (stacksjs/stacks#2375).
28
+ ownership: false,
29
+
25
30
  traits: {
26
31
  useTimestamps: true,
27
32
  useSoftDeletes: true,
@@ -1,4 +1,4 @@
1
- import { defineModel } from '@stacksjs/orm'
1
+ import { defineModel, siteOwnership } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  /**
@@ -14,6 +14,10 @@ export default defineModel({
14
14
  primaryKey: 'id',
15
15
  autoIncrement: true,
16
16
 
17
+ // Owned through the site, which belongs to a team, so the owner is the set of
18
+ // site ids the caller's team owns rather than a single id (stacksjs/stacks#2375).
19
+ ownership: siteOwnership(),
20
+
17
21
  traits: {
18
22
  useTimestamps: true,
19
23
  observe: true, // domain changes clear the host-resolution cache
package/app/Models/Tag.ts CHANGED
@@ -25,6 +25,12 @@ export default defineModel({
25
25
  primaryKey: 'id',
26
26
  autoIncrement: true,
27
27
 
28
+ // A reference table: no row here has a per-caller owner, so there is nothing
29
+ // to scope by and writes are an administrative concern gated by `middleware`.
30
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
31
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
32
+ ownership: false,
33
+
28
34
  traits: {
29
35
  useUuid: true,
30
36
  useTimestamps: true,
@@ -1,4 +1,4 @@
1
- import { defineModel } from '@stacksjs/orm'
1
+ import { defineModel, teamMembershipOwnership } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  export default defineModel({
@@ -7,6 +7,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // The owner column is the primary key: a caller may write the team they are
11
+ // actually a member of, resolved by the same auth path every other
12
+ // team-scoped model uses (stacksjs/stacks#2375).
13
+ ownership: teamMembershipOwnership(),
14
+
10
15
  traits: {
11
16
  useUuid: true,
12
17
  useTimestamps: true,
@@ -1,5 +1,5 @@
1
1
  import type { Attributes } from '@stacksjs/types'
2
- import { defineModel } from '@stacksjs/orm'
2
+ import { defineModel, selfOwnership } from '@stacksjs/orm'
3
3
  import { makeHash } from '@stacksjs/security'
4
4
  // soon, these will be auto-imported
5
5
  import { schema } from '@stacksjs/validation'
@@ -19,6 +19,11 @@ export default defineModel({
19
19
  },
20
20
  ],
21
21
 
22
+ // A caller may write their own row and no other (stacksjs/stacks#2375). The
23
+ // owner column IS the primary key here, so without this `User` is the worst
24
+ // case the issue describes: an authenticated caller able to PATCH any user.
25
+ ownership: selfOwnership(),
26
+
22
27
  traits: {
23
28
  useAuth: {
24
29
  usePasskey: true,
@@ -17,6 +17,12 @@ export default defineModel({
17
17
  primaryKey: 'id',
18
18
  autoIncrement: true,
19
19
 
20
+ // A reference table: no row here has a per-caller owner, so there is nothing
21
+ // to scope by and writes are an administrative concern gated by `middleware`.
22
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
23
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
24
+ ownership: false,
25
+
20
26
  traits: {
21
27
  useUuid: true,
22
28
  useTimestamps: true,
@@ -13,6 +13,12 @@ export default defineModel({
13
13
  primaryKey: 'id',
14
14
  autoIncrement: true,
15
15
 
16
+ // A reference table: no row here has a per-caller owner, so there is nothing
17
+ // to scope by and writes are an administrative concern gated by `middleware`.
18
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
19
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
20
+ ownership: false,
21
+
16
22
  traits: {
17
23
  useUuid: true,
18
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
  export default defineModel({
@@ -7,6 +7,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // Rows belong to the caller's customer record, one hop from the user
11
+ // (stacksjs/stacks#2375). Without this the generated writes are reachable by
12
+ // any authenticated caller for any row.
13
+ ownership: customerOwnership(),
14
+
10
15
  traits: {
11
16
  useUuid: true,
12
17
  useTimestamps: true,
@@ -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
  export default defineModel({
@@ -7,6 +7,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // No owner of its own: these rows are owned by whoever owns the cart's customer
11
+ // (stacksjs/stacks#2375). Resolved through the parent so it follows any change
12
+ // to how Cart decides ownership.
13
+ ownership: parentOwnership('Cart', 'cart_id'),
14
+
10
15
  traits: {
11
16
  useUuid: true,
12
17
  useTimestamps: true,
@@ -8,6 +8,12 @@ export default defineModel({
8
8
  primaryKey: 'id',
9
9
  autoIncrement: true,
10
10
 
11
+ // A reference table: no row here has a per-caller owner, so there is nothing
12
+ // to scope by and writes are an administrative concern gated by `middleware`.
13
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
14
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
15
+ ownership: false,
16
+
11
17
  traits: {
12
18
  useUuid: true,
13
19
  useTimestamps: true,
@@ -7,6 +7,12 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // A reference table: no row here has a per-caller owner, so there is nothing
11
+ // to scope by and writes are an administrative concern gated by `middleware`.
12
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
13
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
14
+ ownership: false,
15
+
10
16
  traits: {
11
17
  useUuid: true,
12
18
  useTimestamps: true,
@@ -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
  export default defineModel({
@@ -7,6 +7,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // No owner of its own: these rows are owned by whoever owns the courier, who is a user
11
+ // (stacksjs/stacks#2375). Resolved through the parent so it follows any change
12
+ // to how Courier decides ownership.
13
+ ownership: parentOwnership('Courier', 'courier_id'),
14
+
10
15
  traits: {
11
16
  useUuid: true,
12
17
  useTimestamps: true,
@@ -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
  /**
@@ -18,6 +18,11 @@ export default defineModel({
18
18
  primaryKey: 'id',
19
19
  autoIncrement: true,
20
20
 
21
+ // No owner of its own: these rows are owned by whoever owns the order's customer
22
+ // (stacksjs/stacks#2375). Resolved through the parent so it follows any change
23
+ // to how Order decides ownership.
24
+ ownership: parentOwnership('Order', 'order_id'),
25
+
21
26
  traits: {
22
27
  useUuid: true,
23
28
  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,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // Rows belong to the caller's customer record, one hop from the user
11
+ // (stacksjs/stacks#2375). Without this the generated writes are reachable by
12
+ // any authenticated caller for any row.
13
+ ownership: customerOwnership(),
14
+
10
15
  traits: {
11
16
  useUuid: true,
12
17
  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,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // Rows belong to the caller's customer record, one hop from the user
11
+ // (stacksjs/stacks#2375). Without this the generated writes are reachable by
12
+ // any authenticated caller for any row.
13
+ ownership: customerOwnership(),
14
+
10
15
  traits: {
11
16
  useUuid: true,
12
17
  useTimestamps: true,
@@ -7,6 +7,12 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // A reference table: no row here has a per-caller owner, so there is nothing
11
+ // to scope by and writes are an administrative concern gated by `middleware`.
12
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
13
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
14
+ ownership: false,
15
+
10
16
  traits: {
11
17
  useUuid: true,
12
18
  useTimestamps: true,
@@ -7,6 +7,12 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // A reference table: no row here has a per-caller owner, so there is nothing
11
+ // to scope by and writes are an administrative concern gated by `middleware`.
12
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
13
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
14
+ ownership: false,
15
+
10
16
  traits: {
11
17
  useUuid: true,
12
18
  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,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // Rows belong to the caller's customer record, one hop from the user
11
+ // (stacksjs/stacks#2375). Without this the generated writes are reachable by
12
+ // any authenticated caller for any row.
13
+ ownership: customerOwnership(),
14
+
10
15
  traits: {
11
16
  useUuid: true,
12
17
  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,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // Rows belong to the caller's customer record, one hop from the user
11
+ // (stacksjs/stacks#2375). Without this the generated writes are reachable by
12
+ // any authenticated caller for any row.
13
+ ownership: customerOwnership(),
14
+
10
15
  traits: {
11
16
  useUuid: true,
12
17
  useTimestamps: true,
@@ -7,6 +7,12 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
  hasMany: ['Receipt'],
10
+ // A reference table: no row here has a per-caller owner, so there is nothing
11
+ // to scope by and writes are an administrative concern gated by `middleware`.
12
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
13
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
14
+ ownership: false,
15
+
10
16
  traits: {
11
17
  useUuid: true,
12
18
  useTimestamps: true,
@@ -7,6 +7,12 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // A reference table: no row here has a per-caller owner, so there is nothing
11
+ // to scope by and writes are an administrative concern gated by `middleware`.
12
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
13
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
14
+ ownership: false,
15
+
10
16
  traits: {
11
17
  useUuid: true,
12
18
  useTimestamps: true,
@@ -7,6 +7,12 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // A reference table: no row here has a per-caller owner, so there is nothing
11
+ // to scope by and writes are an administrative concern gated by `middleware`.
12
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
13
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
14
+ ownership: false,
15
+
10
16
  traits: {
11
17
  useUuid: true,
12
18
  useTimestamps: true,
@@ -7,6 +7,12 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // A reference table: no row here has a per-caller owner, so there is nothing
11
+ // to scope by and writes are an administrative concern gated by `middleware`.
12
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
13
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
14
+ ownership: false,
15
+
10
16
  traits: {
11
17
  useUuid: true,
12
18
  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,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // Rows belong to the caller's customer record, one hop from the user
11
+ // (stacksjs/stacks#2375). Without this the generated writes are reachable by
12
+ // any authenticated caller for any row.
13
+ ownership: customerOwnership(),
14
+
10
15
  traits: {
11
16
  useUuid: true,
12
17
  useTimestamps: true,
@@ -7,6 +7,12 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // A reference table: no row here has a per-caller owner, so there is nothing
11
+ // to scope by and writes are an administrative concern gated by `middleware`.
12
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
13
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
14
+ ownership: false,
15
+
10
16
  traits: {
11
17
  useUuid: true,
12
18
  useTimestamps: true,
@@ -7,6 +7,12 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // A reference table: no row here has a per-caller owner, so there is nothing
11
+ // to scope by and writes are an administrative concern gated by `middleware`.
12
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
13
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
14
+ ownership: false,
15
+
10
16
  traits: {
11
17
  useUuid: true,
12
18
  useTimestamps: true,
@@ -7,6 +7,12 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // A reference table: no row here has a per-caller owner, so there is nothing
11
+ // to scope by and writes are an administrative concern gated by `middleware`.
12
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
13
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
14
+ ownership: false,
15
+
10
16
  traits: {
11
17
  useUuid: true,
12
18
  useTimestamps: true,
@@ -7,6 +7,12 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // A reference table: no row here has a per-caller owner, so there is nothing
11
+ // to scope by and writes are an administrative concern gated by `middleware`.
12
+ // Declared rather than left silent so `security.api.rowScoping: 'deny'` can
13
+ // tell "considered" from "nobody thought about it" (stacksjs/stacks#2375).
14
+ ownership: false,
15
+
10
16
  traits: {
11
17
  useUuid: true,
12
18
  useTimestamps: true,
@@ -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
  export default defineModel({
@@ -7,6 +7,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // No owner of its own: these rows are owned by whoever owns the order's customer
11
+ // (stacksjs/stacks#2375). Resolved through the parent so it follows any change
12
+ // to how Order decides ownership.
13
+ ownership: parentOwnership('Order', 'order_id'),
14
+
10
15
  traits: {
11
16
  useUuid: true,
12
17
  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,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
  belongsTo: ['Product', 'Customer'],
10
+ // Rows belong to the caller's customer record, one hop from the user
11
+ // (stacksjs/stacks#2375). Without this the generated writes are reachable by
12
+ // any authenticated caller for any row.
13
+ ownership: customerOwnership(),
14
+
10
15
  traits: {
11
16
  useUuid: true,
12
17
  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,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
  belongsTo: ['Customer'],
10
+ // Rows belong to the caller's customer record, one hop from the user
11
+ // (stacksjs/stacks#2375). Without this the generated writes are reachable by
12
+ // any authenticated caller for any row.
13
+ ownership: customerOwnership(),
14
+
10
15
  traits: {
11
16
  useUuid: true,
12
17
  useTimestamps: true,
@@ -7,6 +7,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // An infrastructure table: rows are written by the system, not on behalf of a
11
+ // caller, so no row has a per-caller owner to scope by. Writes are gated by
12
+ // `middleware` instead. Declared rather than left silent (stacksjs/stacks#2375).
13
+ ownership: false,
14
+
10
15
  traits: {
11
16
  useTimestamps: true,
12
17
  useSeeder: {
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.74.0",
5
+ "version": "0.74.2",
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.0",
5
+ "version": "0.74.2",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -54,7 +54,7 @@
54
54
  "dependencies": {
55
55
  "@iconify-json/f7": "^1.2.2",
56
56
  "@iconify-json/hugeicons": "^1.2.27",
57
- "@stacksjs/mobile": "^0.74.0",
57
+ "@stacksjs/mobile": "^0.74.2",
58
58
  "@stacksjs/sanitizer": "^0.2.113",
59
59
  "ts-qr-codes": "^0.1.8"
60
60
  }
@@ -6,7 +6,10 @@
6
6
  "compilerOptions": {
7
7
  "lib": ["esnext", "dom"],
8
8
  "rootDir": "../..",
9
- "tsBuildInfoFile": "./.cache/typescript/app.tsbuildinfo",
9
+ // Not incremental, for the reason spelled out in tsconfig.app.json: the
10
+ // cache reports a false green after the auto-import barrels are
11
+ // regenerated. stacksjs/stacks#2405.
12
+ "incremental": false,
10
13
  "paths": {
11
14
  "Actions/*": ["../../app/Actions/*"],
12
15
  "Commands/*": ["../../app/Commands/*"],
@@ -18,6 +18,12 @@
18
18
  const sending = state(false)
19
19
  const done = state('')
20
20
  const openedAt = Date.now()
21
+ // Field names with an upload in flight, so each file input can disable
22
+ // itself and the submit button can wait for all of them.
23
+ const uploading = reactive<Record<string, boolean>>({})
24
+ // The chosen file's name per field, so the person can see what was attached
25
+ // rather than only the storage path the server answered with.
26
+ const attached = reactive<Record<string, string>>({})
21
27
 
22
28
  effect(() => {
23
29
  if (!host() || definition())
@@ -52,10 +58,87 @@
52
58
  return conditions.action === 'show' ? matched : !matched
53
59
  }
54
60
 
61
+ /**
62
+ * Upload one file and record the storage path it returns.
63
+ *
64
+ * The submitted value for a `file` field is that path, not the file: the
65
+ * server checks it sits under this form's upload prefix before accepting the
66
+ * submission, so a path this endpoint did not issue is rejected.
67
+ *
68
+ * Size and type are enforced server-side from `config/forms.ts`; `accept` on
69
+ * the input is only a picker convenience, since a file dialog filter is a
70
+ * suggestion and not a check.
71
+ */
72
+ async function uploadFile(field: PublicFormField, event: Event): Promise<void> {
73
+ const input = event.target as HTMLInputElement
74
+ const file = input.files?.[0]
75
+ if (!file)
76
+ return
77
+
78
+ uploading[field.name] = true
79
+ errors.set({ ...errors(), [field.name]: '' })
80
+
81
+ try {
82
+ const uuid = host()?.dataset.formUuid
83
+ const payload = new FormData()
84
+ payload.append('field', field.name)
85
+ payload.append('file', file)
86
+
87
+ const reply = await fetch(`/api/forms/${uuid}/uploads`, {
88
+ method: 'POST',
89
+ credentials: 'same-origin',
90
+ body: payload,
91
+ })
92
+ const body = await reply.json().catch(() => ({}))
93
+
94
+ if (!reply.ok) {
95
+ // Clear the input as well as the value: leaving a rejected file
96
+ // showing next to its own error message reads as though it was taken.
97
+ input.value = ''
98
+ delete values[field.name]
99
+ delete attached[field.name]
100
+ errors.set({ ...errors(), [field.name]: body.message ?? 'That file could not be uploaded.' })
101
+ return
102
+ }
103
+
104
+ values[field.name] = body.path
105
+ attached[field.name] = file.name
106
+ }
107
+ catch {
108
+ input.value = ''
109
+ errors.set({ ...errors(), [field.name]: 'That file could not be uploaded.' })
110
+ }
111
+ finally {
112
+ uploading[field.name] = false
113
+ }
114
+ }
115
+
116
+ /**
117
+ * The `accept` attribute for a field's picker.
118
+ *
119
+ * A convenience only - a file dialog filter is a suggestion, and the server
120
+ * enforces the real allowlist from `config/forms.ts`. Built here rather than
121
+ * inline in the template so the extension parameter has a type.
122
+ */
123
+ function acceptAttr(field: PublicFormField): string {
124
+ return (field.options.accept ?? []).map((extension: string) => `.${extension}`).join(',')
125
+ }
126
+
127
+ /** True while any upload is still in flight. */
128
+ function anyUploading(): boolean {
129
+ return Object.values(uploading).some(Boolean)
130
+ }
131
+
55
132
  async function submit(event: Event): Promise<void> {
56
133
  event.preventDefault()
57
134
  if (sending())
58
135
  return
136
+ // A submission sent mid-upload would carry no path for that field and read
137
+ // as a missing required answer, which is not what happened.
138
+ if (anyUploading()) {
139
+ errors.set({ _form: 'Please wait for the upload to finish.' })
140
+ return
141
+ }
59
142
  sending.set(true)
60
143
  errors.set({})
61
144
 
@@ -119,6 +202,14 @@
119
202
  <input type="checkbox" @change="values[field.name] = $event.target.checked" class="h-4 w-4 rounded">
120
203
  {{ field.options.placeholder ?? 'Yes' }}
121
204
  </label>
205
+ {{-- A file field used to fall through to the text input below, so
206
+ a person typed prose into what should have been a picker and
207
+ the submission could never satisfy it. --}}
208
+ <div :else-if="field.type === 'file'" class="mt-1.5">
209
+ <input type="file" :required="field.required && !values[field.name]" :disabled="uploading[field.name]" accept="{{ acceptAttr(field) }}" @change="uploadFile(field, $event)" class="block w-full text-[15px] file:mr-3 file:rounded-lg file:border-0 file:bg-gray-100 file:px-3.5 file:py-2 file:text-[14px] file:font-medium">
210
+ <p :if="uploading[field.name]" class="mt-1.5 text-[13px] text-gray-500">Uploading...</p>
211
+ <p :else-if="attached[field.name]" class="mt-1.5 text-[13px] text-green-700">Attached {{ attached[field.name] }}</p>
212
+ </div>
122
213
  <input :else type="{{ field.type === 'email' ? 'email' : (field.type === 'phone' ? 'tel' : (field.type === 'date' ? 'date' : (field.type === 'currency' ? 'number' : 'text'))) }}" :required="field.required" placeholder="{{ field.options.placeholder ?? '' }}" @input="values[field.name] = field.type === 'currency' ? Math.round(Number($event.target.value) * 100) : $event.target.value" class="mt-1.5 w-full rounded-lg border border-gray-300 px-3.5 py-2.5 text-[15px]">
123
214
  <p :if="errors()[field.name]" class="mt-1.5 text-[13px] text-red-600">{{ errors()[field.name] }}</p>
124
215
  </template>
@@ -128,7 +219,7 @@
128
219
 
129
220
  <input type="text" name="website" tabindex="-1" autocomplete="off" aria-hidden="true" class="hidden" @input="values._hp = $event.target.value">
130
221
 
131
- <button type="submit" :disabled="sending()" class="mt-7 rounded-lg px-6 py-3 font-medium bg-gray-900 text-white transition-transform active:scale-[0.98] disabled:opacity-60">
222
+ <button type="submit" :disabled="sending() || anyUploading()" class="mt-7 rounded-lg px-6 py-3 font-medium bg-gray-900 text-white transition-transform active:scale-[0.98] disabled:opacity-60">
132
223
  {{ sending() ? 'Sending...' : (definition()?.submitLabel ?? 'Submit') }}
133
224
  </button>
134
225
  </form>
package/routes/forms.ts CHANGED
@@ -35,6 +35,52 @@ route.get('/api/forms/{uuid}', async (request: any) => {
35
35
  return response.json(publicDefinition(form))
36
36
  }).rateLimit(60, 'minute')
37
37
 
38
+ /**
39
+ * File uploads for a form's `file` fields.
40
+ *
41
+ * Proxied through the server rather than presigned, because this is where the
42
+ * ceilings in `config/forms.ts` can actually be applied: a presigned policy can
43
+ * express a size cap but not a real type check (S3 sees only the Content-Type
44
+ * the client declared), and a proxied upload works on a local disk too. A
45
+ * public form is not a bulk-upload surface. stacksjs/stacks#2406.
46
+ *
47
+ * The stored path is returned for the submit step, which checks it sits under
48
+ * this form's prefix before accepting it.
49
+ */
50
+ route.post('/api/forms/{uuid}/uploads', async (request: any) => {
51
+ const { checkUpload, fileFieldNamed, formUploadPrefix, loadFormByUuid, resolveUploadLimits } = await import('@stacksjs/forms')
52
+
53
+ const form = await loadFormByUuid(String(request.params?.uuid ?? request.param?.('uuid') ?? ''), await siteIdForRequest(request))
54
+ if (!form || form.status === 'draft')
55
+ return response.notFound('Form not found')
56
+
57
+ // Same gate the submit endpoint applies: a closed form does not take files
58
+ // either, and accepting them would leave orphans nothing ever references.
59
+ if (form.status !== 'active')
60
+ return response.json({ message: 'This form is not accepting responses.' }, { status: 409 })
61
+
62
+ const field = fileFieldNamed(form, request.get?.('field') ?? request.input?.('field'))
63
+ if (!field)
64
+ return response.json({ message: 'Unknown upload field.' }, { status: 422 })
65
+
66
+ const file = request.file?.('file')
67
+ if (!file)
68
+ return response.json({ message: `${field.label} is required.` }, { status: 422 })
69
+
70
+ const limits = resolveUploadLimits(field)
71
+ const rejection = checkUpload(field, { name: file.name, size: file.size }, limits)
72
+ if (rejection)
73
+ return response.json({ message: rejection.message }, { status: rejection.status })
74
+
75
+ const { Storage } = await import('@stacksjs/storage')
76
+ const stored = await Storage.put(file, {
77
+ ...(limits.disk ? { disk: limits.disk } : {}),
78
+ dir: formUploadPrefix(form),
79
+ })
80
+
81
+ return response.json({ path: stored.path }, { status: 201 })
82
+ }).rateLimit(20, 'minute')
83
+
38
84
  route.post('/api/forms/{uuid}/submissions', async (request: any) => {
39
85
  const { dispatchSubmissionNotifications, loadFormByUuid, submitForm } = await import('@stacksjs/forms')
40
86