@stacksjs/defaults 0.75.41 → 0.75.43

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/ai/skills/stacks-actions/SKILL.md +1 -1
  2. package/ai/skills/stacks-auto-imports/SKILL.md +1 -1
  3. package/ai/skills/stacks-chat/SKILL.md +51 -1
  4. package/ai/skills/stacks-cloud/SKILL.md +36 -0
  5. package/ai/skills/stacks-dashboard/SKILL.md +16 -2
  6. package/ai/skills/stacks-database/SKILL.md +24 -3
  7. package/ai/skills/stacks-deploy/SKILL.md +7 -1
  8. package/ai/skills/stacks-desktop/SKILL.md +10 -0
  9. package/ai/skills/stacks-development/SKILL.md +1 -1
  10. package/ai/skills/stacks-migrations/SKILL.md +2 -2
  11. package/ai/skills/stacks-models/SKILL.md +38 -2
  12. package/ai/skills/stacks-orm/SKILL.md +2 -2
  13. package/ai/skills/stacks-security/SKILL.md +28 -1
  14. package/app/Actions/Auth/DataExportAction.test.ts +84 -0
  15. package/app/Actions/Auth/DataExportAction.ts +40 -0
  16. package/app/Actions/Dashboard/Content/FileReprocessAction.ts +1 -1
  17. package/app/Actions/Dashboard/Content/file-manager.ts +120 -8
  18. package/app/Actions/Dashboard/Content/file-metadata.ts +108 -12
  19. package/app/Actions/Dashboard/Content/file-pipeline.test.ts +13 -9
  20. package/app/Actions/Dashboard/Content/file-preview.test.ts +487 -0
  21. package/app/Actions/Dashboard/Content/file-preview.ts +548 -0
  22. package/app/Jobs/GenerateStoragePreviewJob.ts +47 -0
  23. package/app/Jobs/OptimizeStorageImageJob.ts +7 -3
  24. package/app/Jobs/PruneRetainedDataJob.ts +46 -0
  25. package/app/Models/Activity.ts +2 -0
  26. package/app/Models/CampaignSend.ts +2 -0
  27. package/app/Models/Card.ts +1 -0
  28. package/app/Models/CardComment.ts +1 -0
  29. package/app/Models/Comment.ts +5 -0
  30. package/app/Models/CommunicationSuppression.ts +2 -1
  31. package/app/Models/ConsentEvent.ts +4 -3
  32. package/app/Models/Content/Author.ts +5 -0
  33. package/app/Models/EmailListSubscriber.ts +1 -0
  34. package/app/Models/EmailSuppression.ts +2 -0
  35. package/app/Models/Forms/FormSubmission.ts +5 -0
  36. package/app/Models/GdprRequest.ts +77 -0
  37. package/app/Models/MagicLinkToken.ts +2 -0
  38. package/app/Models/Notification.ts +2 -0
  39. package/app/Models/NotificationDelivery.ts +4 -0
  40. package/app/Models/PaymentMethod.ts +5 -0
  41. package/app/Models/PaymentTransaction.ts +1 -0
  42. package/app/Models/PersonalAccessToken.ts +3 -0
  43. package/app/Models/Referral.ts +4 -1
  44. package/app/Models/ReferralCode.ts +4 -1
  45. package/app/Models/Request.ts +3 -0
  46. package/app/Models/SmsOptOut.ts +2 -0
  47. package/app/Models/SocialAccount.ts +3 -0
  48. package/app/Models/SocialPost.ts +2 -0
  49. package/app/Models/StorageItemTask.ts +14 -8
  50. package/app/Models/Subscriber.ts +2 -0
  51. package/app/Models/SubscriberEmail.ts +2 -0
  52. package/app/Models/Subscription.ts +1 -0
  53. package/app/Models/TeamInvitation.ts +2 -0
  54. package/app/Models/TeamMember.ts +1 -0
  55. package/app/Models/User.ts +5 -0
  56. package/app/Models/commerce/Bid.ts +3 -0
  57. package/app/Models/commerce/Cart.ts +2 -0
  58. package/app/Models/commerce/Courier.ts +7 -0
  59. package/app/Models/commerce/CourierPing.ts +3 -0
  60. package/app/Models/commerce/Customer.ts +5 -0
  61. package/app/Models/commerce/DeliveryStop.ts +7 -0
  62. package/app/Models/commerce/GiftCard.ts +4 -0
  63. package/app/Models/commerce/Order.ts +5 -0
  64. package/app/Models/commerce/OrderItem.ts +2 -0
  65. package/app/Models/commerce/Payment.ts +5 -0
  66. package/app/Models/commerce/Pledge.ts +3 -0
  67. package/app/Models/commerce/Transaction.ts +2 -0
  68. package/app/Models/commerce/WaitlistProduct.ts +5 -0
  69. package/app/Models/commerce/WaitlistRestaurant.ts +4 -0
  70. package/ide/vscode/.vscode/extensions.json +1 -1
  71. package/ide/vscode/.vscode/settings.json +8 -149
  72. package/ide/vscode/README.md +48 -23
  73. package/ide/vscode/build.ts +43 -0
  74. package/ide/vscode/package.json +305 -56
  75. package/ide/vscode/scripts/stx-contributes.ts +66 -0
  76. package/ide/vscode/src/env-file.ts +103 -0
  77. package/ide/vscode/src/env.ts +241 -0
  78. package/ide/vscode/src/extension.ts +120 -26
  79. package/ide/vscode/src/pickier-protocol.ts +51 -0
  80. package/ide/vscode/src/pickier-worker.ts +92 -0
  81. package/ide/vscode/src/pickier.ts +433 -0
  82. package/ide/vscode/src/project.ts +14 -4
  83. package/ide/vscode/src/stacks.ts +18 -11
  84. package/ide/vscode/tests/build.test.ts +58 -0
  85. package/ide/vscode/tests/env.test.ts +166 -0
  86. package/ide/vscode/tests/extension.test.ts +8 -0
  87. package/ide/vscode/tests/fake-vscode.ts +228 -0
  88. package/ide/vscode/tests/manifest.test.ts +88 -0
  89. package/ide/vscode/tests/pickier.test.ts +202 -0
  90. package/ide/vscode/tsconfig.json +2 -2
  91. package/package.json +2 -2
  92. package/resources/components/Dashboard/Content/FileManagerDashboard.stx +2 -0
  93. package/resources/components/Dashboard/FileViewer/MediaCard.stx +14 -3
  94. package/routes/auth.ts +4 -0
  95. package/vcs/github/workflows/buddy-bot.yml +2 -2
  96. package/vcs/github/workflows/ci.yml +3 -3
  97. package/vcs/github/workflows/release.yml +1 -1
  98. package/ide/vscode/src/vscode.d.ts +0 -181
@@ -17,6 +17,7 @@ export default defineModel({
17
17
  ],
18
18
 
19
19
  traits: {
20
+ gdpr: { subject: { via: 'Subscriber' }, erasure: 'anonymize', basis: 'legitimate_interests', purpose: 'Campaign delivery statistics' },
20
21
  useUuid: true,
21
22
  useTimestamps: true,
22
23
  useApi: {
@@ -81,6 +82,7 @@ export default defineModel({
81
82
  },
82
83
 
83
84
  recipient: {
85
+ personal: true,
84
86
  required: true,
85
87
  fillable: true,
86
88
  validation: {
@@ -21,6 +21,7 @@ export default defineModel({
21
21
  autoIncrement: true,
22
22
 
23
23
  traits: {
24
+ gdpr: { erasure: 'keep', basis: 'legitimate_interests', purpose: 'Shared board content, attributed to the (anonymized) author' },
24
25
  useUuid: true,
25
26
  useTimestamps: true,
26
27
  useSearch: {
@@ -21,6 +21,7 @@ export default defineModel({
21
21
  autoIncrement: true,
22
22
 
23
23
  traits: {
24
+ gdpr: { erasure: 'keep', basis: 'legitimate_interests', purpose: 'Shared board discussion, attributed to the (anonymized) author' },
24
25
  useUuid: true,
25
26
  useTimestamps: true,
26
27
  useSearch: {
@@ -8,6 +8,7 @@ export default defineModel({
8
8
  autoIncrement: true,
9
9
 
10
10
  traits: {
11
+ gdpr: { erasure: 'anonymize', basis: 'legitimate_interests', purpose: 'Comments on posts' },
11
12
  useUuid: true,
12
13
  useTimestamps: true,
13
14
  useSeeder: {
@@ -24,6 +25,7 @@ export default defineModel({
24
25
 
25
26
  attributes: {
26
27
  authorName: {
28
+ personal: true,
27
29
  required: true,
28
30
  fillable: true,
29
31
  validation: {
@@ -33,6 +35,7 @@ export default defineModel({
33
35
  },
34
36
 
35
37
  authorEmail: {
38
+ personal: true,
36
39
  required: true,
37
40
  fillable: true,
38
41
  validation: {
@@ -79,6 +82,7 @@ export default defineModel({
79
82
  },
80
83
 
81
84
  ipAddress: {
85
+ personal: true,
82
86
  required: false,
83
87
  fillable: true,
84
88
  validation: {
@@ -88,6 +92,7 @@ export default defineModel({
88
92
  },
89
93
 
90
94
  userAgent: {
95
+ personal: true,
91
96
  required: false,
92
97
  fillable: true,
93
98
  validation: {
@@ -6,13 +6,14 @@ export default defineModel({
6
6
  table: 'communication_suppressions',
7
7
  belongsTo: ['Team'],
8
8
  traits: {
9
+ gdpr: { subject: { email: 'recipient' }, erasure: 'keep', basis: 'legal_obligation', purpose: 'Honouring opt-outs, which needs the address it suppresses' },
9
10
  useUuid: true,
10
11
  useTimestamps: true,
11
12
  useApi: { uri: 'communication-suppressions', routes: ['index', 'store', 'show', 'destroy'], middleware: ['auth', 'team'] },
12
13
  },
13
14
  indexes: [{ name: 'communication_suppressions_unique', columns: ['team_id', 'channel', 'recipient'], unique: true }],
14
15
  attributes: {
15
- recipient: { required: true, fillable: true, validation: { rule: schema.string().max(255) }, factory: faker => faker.internet.email() },
16
+ recipient: { personal: true, required: true, fillable: true, validation: { rule: schema.string().max(255) }, factory: faker => faker.internet.email() },
16
17
  channel: { required: true, fillable: true, validation: { rule: schema.enum(['email', 'sms', 'push']) }, factory: () => 'email' },
17
18
  reason: { required: true, fillable: true, validation: { rule: schema.enum(['unsubscribe', 'bounce', 'complaint', 'carrier', 'manual', 'legal']) }, factory: () => 'unsubscribe' },
18
19
  source: { required: true, fillable: true, validation: { rule: schema.string().max(120) }, factory: () => 'preference_center' },
@@ -6,6 +6,7 @@ export default defineModel({
6
6
  table: 'consent_events',
7
7
  belongsTo: ['Team'],
8
8
  traits: {
9
+ gdpr: { subject: { email: 'recipient' }, erasure: 'keep', basis: 'legal_obligation', purpose: 'Proof of consent and its withdrawal (Article 7(1))' },
9
10
  useUuid: true,
10
11
  useTimestamps: true,
11
12
  useApi: { uri: 'consent-events', routes: ['index', 'show'], middleware: ['auth', 'team'] },
@@ -15,7 +16,7 @@ export default defineModel({
15
16
  { name: 'consent_events_lookup', columns: ['team_id', 'channel', 'recipient', 'occurred_at'] },
16
17
  ],
17
18
  attributes: {
18
- recipient: { required: true, fillable: true, validation: { rule: schema.string().max(255) }, factory: faker => faker.internet.email() },
19
+ recipient: { personal: true, required: true, fillable: true, validation: { rule: schema.string().max(255) }, factory: faker => faker.internet.email() },
19
20
  channel: { required: true, fillable: true, validation: { rule: schema.enum(['email', 'sms', 'push']) }, factory: () => 'email' },
20
21
  action: { required: true, fillable: true, validation: { rule: schema.enum(['requested', 'granted', 'revoked', 'confirmed', 'suppressed']) }, factory: () => 'granted' },
21
22
  purpose: { required: true, fillable: true, validation: { rule: schema.string().max(120) }, factory: () => 'marketing' },
@@ -23,8 +24,8 @@ export default defineModel({
23
24
  jurisdiction: { required: false, fillable: true, validation: { rule: schema.string().max(80) }, factory: () => null },
24
25
  policyVersion: { required: true, fillable: true, validation: { rule: schema.string().max(40) }, factory: () => '1.0' },
25
26
  idempotencyKey: { required: false, fillable: true, validation: { rule: schema.string().max(255) }, factory: faker => faker.string.uuid() },
26
- proof: { required: false, fillable: true, validation: { rule: schema.json() }, factory: () => JSON.stringify({}) },
27
- ipAddress: { required: false, fillable: true, validation: { rule: schema.string().max(45) }, factory: faker => faker.internet.ip() },
27
+ proof: { personal: true, required: false, fillable: true, validation: { rule: schema.json() }, factory: () => JSON.stringify({}) },
28
+ ipAddress: { personal: true, required: false, fillable: true, validation: { rule: schema.string().max(45) }, factory: faker => faker.internet.ip() },
28
29
  occurredAt: { required: true, fillable: true, validation: { rule: schema.timestamp() }, factory: () => new Date().toISOString() },
29
30
  },
30
31
  } as const)
@@ -16,6 +16,7 @@ export default defineModel({
16
16
  ],
17
17
 
18
18
  traits: {
19
+ gdpr: { erasure: 'anonymize', basis: 'legitimate_interests', purpose: 'Byline on published content' },
19
20
  useAuth: {
20
21
  usePasskey: true,
21
22
  },
@@ -51,6 +52,7 @@ export default defineModel({
51
52
 
52
53
  attributes: {
53
54
  name: {
55
+ personal: true,
54
56
  order: 1,
55
57
  fillable: true,
56
58
  validation: {
@@ -65,6 +67,7 @@ export default defineModel({
65
67
  },
66
68
 
67
69
  email: {
70
+ personal: true,
68
71
  unique: true,
69
72
  order: 2,
70
73
  fillable: true,
@@ -79,6 +82,7 @@ export default defineModel({
79
82
  },
80
83
 
81
84
  bio: {
85
+ personal: true,
82
86
  required: false,
83
87
  order: 3,
84
88
  fillable: true,
@@ -92,6 +96,7 @@ export default defineModel({
92
96
  },
93
97
 
94
98
  avatar: {
99
+ personal: true,
95
100
  required: false,
96
101
  order: 4,
97
102
  fillable: true,
@@ -18,6 +18,7 @@ export default defineModel({
18
18
  belongsTo: ['EmailList', 'Subscriber'],
19
19
 
20
20
  traits: {
21
+ gdpr: { subject: { via: 'Subscriber' }, erasure: 'delete', basis: 'consent', purpose: 'Mailing list membership' },
21
22
  useUuid: true,
22
23
  useTimestamps: true,
23
24
  useApi: {
@@ -21,6 +21,7 @@ export default defineModel({
21
21
  ownership: false,
22
22
 
23
23
  traits: {
24
+ gdpr: { subject: { email: 'email' }, erasure: 'keep', basis: 'legal_obligation', purpose: 'Honouring bounces and complaints, which needs the address it suppresses' },
24
25
  useTimestamps: true,
25
26
  useApi: {
26
27
  uri: 'email-suppressions',
@@ -34,6 +35,7 @@ export default defineModel({
34
35
 
35
36
  attributes: {
36
37
  email: {
38
+ personal: true,
37
39
  required: true,
38
40
  fillable: true,
39
41
  validation: {
@@ -18,6 +18,7 @@ export default defineModel({
18
18
  autoIncrement: true,
19
19
 
20
20
  traits: {
21
+ gdpr: { subject: { email: 'email' }, erasure: 'anonymize', basis: 'consent', purpose: 'Form submissions' },
21
22
  useUuid: true,
22
23
  useTimestamps: true,
23
24
  },
@@ -27,6 +28,7 @@ export default defineModel({
27
28
  attributes: {
28
29
  /** Validated { [fieldName]: value } JSON. Named data, not values - values is a SQL keyword. */
29
30
  data: {
31
+ personal: true,
30
32
  required: true,
31
33
  order: 1,
32
34
  fillable: true,
@@ -37,6 +39,7 @@ export default defineModel({
37
39
  },
38
40
 
39
41
  email: {
42
+ personal: true,
40
43
  required: false,
41
44
  order: 2,
42
45
  fillable: true,
@@ -47,6 +50,7 @@ export default defineModel({
47
50
  },
48
51
 
49
52
  name: {
53
+ personal: true,
50
54
  required: false,
51
55
  order: 3,
52
56
  fillable: true,
@@ -89,6 +93,7 @@ export default defineModel({
89
93
  },
90
94
 
91
95
  ip: {
96
+ personal: true,
92
97
  required: false,
93
98
  order: 7,
94
99
  fillable: true,
@@ -0,0 +1,77 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ /**
5
+ * The ledger of data-subject requests (stacksjs/stacks#365).
6
+ *
7
+ * One row per access export, per erasure, and per retention run that changed
8
+ * something - written by `exportSubjectData`, `eraseSubject` and
9
+ * `pruneRetainedData`, inside the erasure's own transaction, so an erasure is
10
+ * recorded exactly when it happened. It is the answer to "when did we act on
11
+ * this request, and what did we do", which a regulator can ask and which the
12
+ * data itself can no longer answer once it is erased.
13
+ *
14
+ * It holds no personal data of its own: `summary` carries per-model counts,
15
+ * never values. `subject_id` has no foreign key on purpose - the user row it
16
+ * names may be anonymized or gone, and the record of having erased it must
17
+ * outlive it.
18
+ */
19
+ export default defineModel({
20
+ name: 'GdprRequest',
21
+ table: 'gdpr_requests',
22
+ primaryKey: 'id',
23
+ autoIncrement: true,
24
+
25
+ indexes: [
26
+ { name: 'gdpr_requests_subject_lookup', columns: ['subject_id', 'type', 'occurred_at'] },
27
+ ],
28
+
29
+ // System-written; there is no caller whose rows these are, and no API.
30
+ ownership: false,
31
+
32
+ traits: {
33
+ useUuid: true,
34
+ useTimestamps: true,
35
+ gdpr: { subject: 'subject_id', erasure: 'keep', basis: 'legal_obligation', purpose: 'Record of the data-subject requests acted on' },
36
+ },
37
+
38
+ attributes: {
39
+ type: {
40
+ required: true,
41
+ fillable: true,
42
+ validation: { rule: schema.enum(['access', 'erasure', 'retention']) },
43
+ factory: () => 'access',
44
+ },
45
+ subjectId: {
46
+ required: false,
47
+ fillable: true,
48
+ validation: { rule: schema.number().integer().min(1) },
49
+ factory: () => 1,
50
+ },
51
+ actor: {
52
+ required: false,
53
+ fillable: true,
54
+ validation: { rule: schema.string().max(120) },
55
+ factory: () => 'cli',
56
+ },
57
+ status: {
58
+ required: true,
59
+ fillable: true,
60
+ default: 'completed',
61
+ validation: { rule: schema.enum(['completed', 'failed']) },
62
+ factory: () => 'completed',
63
+ },
64
+ summary: {
65
+ required: false,
66
+ fillable: true,
67
+ validation: { rule: schema.json() },
68
+ factory: () => JSON.stringify({}),
69
+ },
70
+ occurredAt: {
71
+ required: true,
72
+ fillable: true,
73
+ validation: { rule: schema.timestamp() },
74
+ factory: () => new Date().toISOString(),
75
+ },
76
+ },
77
+ } as const)
@@ -24,6 +24,7 @@ export default defineModel({
24
24
  ],
25
25
 
26
26
  traits: {
27
+ gdpr: { erasure: 'delete', basis: 'contract', purpose: 'Passwordless sign-in links' },
27
28
  useTimestamps: true,
28
29
  },
29
30
 
@@ -31,6 +32,7 @@ export default defineModel({
31
32
 
32
33
  attributes: {
33
34
  email: {
35
+ personal: true,
34
36
  required: true,
35
37
  order: 1,
36
38
  fillable: true,
@@ -8,6 +8,7 @@ export default defineModel({
8
8
  autoIncrement: true,
9
9
 
10
10
  traits: {
11
+ gdpr: { erasure: 'delete', basis: 'contract', purpose: 'In-app notifications' },
11
12
  useUuid: true,
12
13
  useTimestamps: true,
13
14
  useSeeder: {
@@ -38,6 +39,7 @@ export default defineModel({
38
39
  },
39
40
 
40
41
  data: {
42
+ personal: true,
41
43
  type: 'text',
42
44
  required: true,
43
45
  fillable: true,
@@ -8,6 +8,7 @@ export default defineModel({
8
8
  autoIncrement: true,
9
9
 
10
10
  traits: {
11
+ gdpr: { subject: 'user_id', erasure: 'delete', basis: 'contract', purpose: 'Notification delivery log' },
11
12
  useTimestamps: true,
12
13
  useSearch: {
13
14
  displayable: ['id', 'channel', 'recipient', 'subject', 'status', 'sentAt'],
@@ -41,6 +42,7 @@ export default defineModel({
41
42
  },
42
43
 
43
44
  recipient: {
45
+ personal: true,
44
46
  required: true,
45
47
  fillable: true,
46
48
  validation: {
@@ -49,6 +51,7 @@ export default defineModel({
49
51
  },
50
52
 
51
53
  subject: {
54
+ personal: true,
52
55
  required: false,
53
56
  fillable: true,
54
57
  validation: {
@@ -57,6 +60,7 @@ export default defineModel({
57
60
  },
58
61
 
59
62
  body: {
63
+ personal: true,
60
64
  required: true,
61
65
  fillable: true,
62
66
  validation: {
@@ -11,6 +11,7 @@ export default defineModel({
11
11
  belongsTo: ['User'],
12
12
  hasMany: ['PaymentTransaction'],
13
13
  traits: {
14
+ gdpr: { erasure: 'anonymize', basis: 'contract', purpose: 'Saved payment methods' },
14
15
  useSeeder: {
15
16
  count: 5,
16
17
  },
@@ -31,6 +32,7 @@ export default defineModel({
31
32
  },
32
33
 
33
34
  lastFour: {
35
+ personal: true,
34
36
  fillable: true,
35
37
  validation: {
36
38
  rule: schema.number().required(),
@@ -43,6 +45,7 @@ export default defineModel({
43
45
  },
44
46
 
45
47
  brand: {
48
+ personal: true,
46
49
  fillable: true,
47
50
  validation: {
48
51
  rule: schema.string().required().max(50),
@@ -55,6 +58,7 @@ export default defineModel({
55
58
  },
56
59
 
57
60
  expMonth: {
61
+ personal: true,
58
62
  fillable: true,
59
63
  validation: {
60
64
  rule: schema.number().required(),
@@ -67,6 +71,7 @@ export default defineModel({
67
71
  },
68
72
 
69
73
  expYear: {
74
+ personal: true,
70
75
  fillable: true,
71
76
  validation: {
72
77
  rule: schema.number().required(),
@@ -10,6 +10,7 @@ export default defineModel({
10
10
  autoIncrement: true, // defaults to true
11
11
  belongsTo: ['User', 'PaymentMethod'],
12
12
  traits: {
13
+ gdpr: { erasure: 'keep', basis: 'legal_obligation', purpose: 'Billing records' },
13
14
  useUuid: true,
14
15
  useSeeder: {
15
16
  count: 5,
@@ -27,6 +27,7 @@ export default defineModel({
27
27
  autoIncrement: true,
28
28
 
29
29
  traits: {
30
+ gdpr: { subject: { column: 'tokenable_id', where: { tokenable_type: 'users' } }, erasure: 'anonymize', basis: 'contract', purpose: 'API tokens, revoked on erasure' },
30
31
  useTimestamps: true,
31
32
  // Deliberately no `useApi`.
32
33
  //
@@ -136,6 +137,7 @@ export default defineModel({
136
137
  * nothing authorises on either.
137
138
  */
138
139
  userAgent: {
140
+ personal: true,
139
141
  order: 8,
140
142
  fillable: true,
141
143
  validation: {
@@ -144,6 +146,7 @@ export default defineModel({
144
146
  },
145
147
 
146
148
  ipAddress: {
149
+ personal: true,
147
150
  order: 9,
148
151
  fillable: true,
149
152
  validation: {
@@ -4,7 +4,10 @@ import { schema } from '@stacksjs/validation'
4
4
  export default defineModel({
5
5
  name: 'Referral',
6
6
  table: 'referrals',
7
- traits: { useTimestamps: true },
7
+ traits: {
8
+ useTimestamps: true,
9
+ gdpr: { subject: ['referrer_id', 'referred_user_id'], erasure: 'keep', basis: 'contract', purpose: 'Referral rewards owed' },
10
+ },
8
11
  indexes: [{ name: 'referrals_referrer_status', columns: ['referrer_id', 'status'] }],
9
12
  attributes: {
10
13
  referrerId: { required: true, validation: { rule: schema.number().integer().min(1) } },
@@ -4,7 +4,10 @@ import { schema } from '@stacksjs/validation'
4
4
  export default defineModel({
5
5
  name: 'ReferralCode',
6
6
  table: 'referral_codes',
7
- traits: { useTimestamps: true },
7
+ traits: {
8
+ useTimestamps: true,
9
+ gdpr: { subject: 'user_id', erasure: 'delete', basis: 'contract', purpose: 'Referral codes' },
10
+ },
8
11
  attributes: {
9
12
  userId: { required: true, unique: true, validation: { rule: schema.number().integer().min(1) } },
10
13
  code: { required: true, unique: true, validation: { rule: schema.string().max(24) } },
@@ -28,6 +28,7 @@ export default defineModel({
28
28
  ownership: false,
29
29
 
30
30
  traits: {
31
+ gdpr: { basis: 'legitimate_interests', purpose: 'Request log for debugging and abuse prevention' },
31
32
  useTimestamps: true,
32
33
  useSoftDeletes: true,
33
34
  useSeeder: {
@@ -93,6 +94,7 @@ export default defineModel({
93
94
  },
94
95
 
95
96
  ip_address: {
97
+ personal: true,
96
98
  fillable: true,
97
99
  validation: {
98
100
  rule: schema.string(),
@@ -117,6 +119,7 @@ export default defineModel({
117
119
  },
118
120
 
119
121
  user_agent: {
122
+ personal: true,
120
123
  fillable: true,
121
124
  validation: {
122
125
  rule: schema.string(),
@@ -22,12 +22,14 @@ export default defineModel({
22
22
  ],
23
23
 
24
24
  traits: {
25
+ gdpr: { erasure: 'keep', basis: 'legal_obligation', purpose: 'Honouring SMS STOP replies, keyed by phone number' },
25
26
  useTimestamps: true,
26
27
  },
27
28
 
28
29
  attributes: {
29
30
  /** E.164 where derivable (`+13105550199`). */
30
31
  phone: {
32
+ personal: true,
31
33
  required: true,
32
34
  order: 1,
33
35
  fillable: true,
@@ -33,6 +33,7 @@ export default defineModel({
33
33
  ],
34
34
 
35
35
  traits: {
36
+ gdpr: { erasure: 'delete', basis: 'contract', purpose: 'Linked sign-in providers' },
36
37
  useTimestamps: true,
37
38
  },
38
39
 
@@ -49,6 +50,7 @@ export default defineModel({
49
50
  },
50
51
 
51
52
  providerUserId: {
53
+ personal: true,
52
54
  required: true,
53
55
  fillable: true,
54
56
  validation: {
@@ -60,6 +62,7 @@ export default defineModel({
60
62
  // The address the provider reported at link time. Informational — sign-in
61
63
  // resolves through (provider, provider_user_id), never through this.
62
64
  providerEmail: {
65
+ personal: true,
63
66
  fillable: true,
64
67
  validation: {
65
68
  rule: schema.string().max(255),
@@ -8,6 +8,7 @@ export default defineModel({
8
8
  autoIncrement: true,
9
9
 
10
10
  traits: {
11
+ gdpr: { erasure: 'delete', basis: 'contract', purpose: 'Social posts the user scheduled' },
11
12
  useUuid: true,
12
13
  useTimestamps: true,
13
14
  useSeeder: {
@@ -31,6 +32,7 @@ export default defineModel({
31
32
 
32
33
  attributes: {
33
34
  content: {
35
+ personal: true,
34
36
  required: true,
35
37
  fillable: true,
36
38
  validation: {
@@ -5,7 +5,8 @@ import { schema } from '@stacksjs/validation'
5
5
  * One unit of background processing for one file (stacksjs/stacks#2578).
6
6
  *
7
7
  * The three things #245 asked for that could not happen inside a request -
8
- * image optimization, video transcode, AI tagging - share the same shape:
8
+ * image optimization, video transcode, AI tagging - and the preview #308 asked
9
+ * for share the same shape:
9
10
  * dispatch work and show what happened to it. Transcoding is minutes, not
10
11
  * milliseconds; a vision call is a round trip to a third party. An upload
11
12
  * handler that waits for either is an upload handler that times out.
@@ -81,10 +82,11 @@ export default defineModel({
81
82
  kind: {
82
83
  order: 3,
83
84
  fillable: true,
84
- // `optimize` (images), `transcode` (video), `tag` (AI). An enum rather
85
- // than free text so a typo in a dispatch is a validation error instead of
86
- // a task nothing will ever run.
87
- validation: { rule: schema.enum(['optimize', 'transcode', 'tag']) },
85
+ // `optimize` (images), `transcode` (video), `tag` (AI), `preview` (the
86
+ // file manager's thumbnail, stacksjs/stacks#308). An enum rather than
87
+ // free text so a typo in a dispatch is a validation error instead of a
88
+ // task nothing will ever run.
89
+ validation: { rule: schema.enum(['optimize', 'transcode', 'tag', 'preview']) },
88
90
  factory: () => 'optimize',
89
91
  },
90
92
 
@@ -92,7 +94,10 @@ export default defineModel({
92
94
  order: 4,
93
95
  fillable: true,
94
96
  default: 'queued',
95
- validation: { rule: schema.enum(['queued', 'running', 'done', 'failed']) },
97
+ // `skipped` is terminal and not a failure: the work does not apply to
98
+ // this file here - a PDF with no renderer to draw it - and `error`
99
+ // carries the reason, so the dashboard can say why.
100
+ validation: { rule: schema.enum(['queued', 'running', 'done', 'failed', 'skipped']) },
96
101
  factory: () => 'queued',
97
102
  },
98
103
 
@@ -109,8 +114,9 @@ export default defineModel({
109
114
  error: {
110
115
  order: 6,
111
116
  fillable: true,
112
- // The failure as the worker saw it. Truncated by the writer rather than
113
- // by the column, so a stack trace does not silently lose its first line.
117
+ // The failure as the worker saw it, or why a skipped task did not apply.
118
+ // Truncated by the writer rather than by the column, so a stack trace
119
+ // does not silently lose its first line.
114
120
  validation: { rule: schema.string().max(2000) },
115
121
  factory: () => '',
116
122
  },
@@ -10,6 +10,7 @@ export default defineModel({
10
10
  hasMany: ['SubscriberEmail', 'EmailListSubscriber', 'CampaignSend'],
11
11
 
12
12
  traits: {
13
+ gdpr: { subject: { email: 'email' }, erasure: 'anonymize', basis: 'consent', purpose: 'Newsletter subscription' },
13
14
  useUuid: true,
14
15
  useTimestamps: true,
15
16
  useSeeder: {
@@ -31,6 +32,7 @@ export default defineModel({
31
32
 
32
33
  attributes: {
33
34
  email: {
35
+ personal: true,
34
36
  unique: true,
35
37
  required: true,
36
38
  fillable: true,
@@ -9,6 +9,7 @@ export default defineModel({
9
9
  belongsTo: ['Subscriber'],
10
10
 
11
11
  traits: {
12
+ gdpr: { subject: { via: 'Subscriber' }, erasure: 'delete', basis: 'consent', purpose: 'Newsletter subscription addresses' },
12
13
  useUuid: true,
13
14
  useTimestamps: true,
14
15
  useApi: {
@@ -24,6 +25,7 @@ export default defineModel({
24
25
 
25
26
  attributes: {
26
27
  email: {
28
+ personal: true,
27
29
  required: true,
28
30
  fillable: true,
29
31
  validation: {
@@ -8,6 +8,7 @@ export default defineModel({
8
8
  autoIncrement: true, // defaults to true
9
9
  belongsTo: ['User'],
10
10
  traits: {
11
+ gdpr: { erasure: 'keep', basis: 'legal_obligation', purpose: 'Billing records' },
11
12
  useUuid: true,
12
13
  },
13
14
  attributes: {
@@ -34,6 +34,7 @@ export default defineModel({
34
34
  belongsTo: ['Team'],
35
35
 
36
36
  traits: {
37
+ gdpr: { subject: { email: 'email' }, erasure: 'delete', basis: 'contract', purpose: 'Team invitations' },
37
38
  useUuid: true,
38
39
  useTimestamps: true,
39
40
  useSeeder: { count: 0 },
@@ -60,6 +61,7 @@ export default defineModel({
60
61
  },
61
62
 
62
63
  email: {
64
+ personal: true,
63
65
  required: true,
64
66
  fillable: true,
65
67
  validation: {
@@ -30,6 +30,7 @@ export default defineModel({
30
30
  belongsTo: ['Team', 'User'],
31
31
 
32
32
  traits: {
33
+ gdpr: { erasure: 'delete', basis: 'contract', purpose: 'Team membership' },
33
34
  useUuid: true,
34
35
  useTimestamps: true,
35
36
  useSeeder: { count: 0 },