@stacksjs/defaults 0.75.137 → 0.75.138

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.
@@ -63,7 +63,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
63
63
  | `@stacksjs/mobile` | [`stacks-mobile`](../stacks-mobile/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/mobile/src/index.ts) | 12 |
64
64
  | `@stacksjs/model-meta` | [`stacks-models`](../stacks-models/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/model-meta/src/index.ts) | 1 |
65
65
  | `@stacksjs/newsletter` | [`stacks-newsletter`](../stacks-newsletter/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/newsletter/src/index.ts) | 7 |
66
- | `@stacksjs/notifications` | [`stacks-notifications`](../stacks-notifications/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/index.ts) | 11 |
66
+ | `@stacksjs/notifications` | [`stacks-notifications`](../stacks-notifications/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/index.ts) | 12 |
67
67
  | `@stacksjs/objects` | [`stacks-objects`](../stacks-objects/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/objects/src/index.ts) | 4 |
68
68
  | `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) | 60 |
69
69
  | `@stacksjs/pagination` | [`stacks-pagination`](../stacks-pagination/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/pagination/src/index.ts) | 0 |
@@ -607,11 +607,11 @@ Evidence directory: [7 retained tests](https://github.com/stacksjs/stacks/blob/m
607
607
 
608
608
  Guidance: [`stacks-notifications`](../stacks-notifications/SKILL.md). Source: [storage/framework/core/notifications/src/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/index.ts).
609
609
 
610
- Declared runtime exports: `BroadcastNotificationDriver`, `DatabaseNotificationDriver`, `bulkSetPreferences`, `emailParagraphs`, `ensureSuccessfulEmailResult`, `ensureSuccessfulNotificationResults`, `filterChannelsByPreferences`, `getNotificationPreferences`, `makeDeliveryRecord`, `notification`, `notificationChannel`, `notificationEmailHtml`, `notificationEmailText`, `notify`, `recordNotificationDelivery`, `resolveDeliveryRecipient`, `setNotificationPreference`, `useBroadcast`, `useChat`, `useDatabase`, `useEmail`, `useNotification`, `usePush`, `useSMS`, `wherePreferenceCategory`.
610
+ Declared runtime exports: `BroadcastNotificationDriver`, `DEFAULT_SMS_OPT_OUT`, `DEFAULT_UNSUBSCRIBE_NOTICE`, `DatabaseNotificationDriver`, `bulkSetPreferences`, `emailParagraphs`, `ensureSuccessfulEmailResult`, `ensureSuccessfulNotificationResults`, `filterChannelsByPreferences`, `getNotificationPreferences`, `listUnsubscribeHeaders`, `makeDeliveryRecord`, `notification`, `notificationChannel`, `notificationEmailHtml`, `notificationEmailMessage`, `notificationEmailText`, `notificationSmsBody`, `notificationUnsubscribeLinks`, `notificationUnsubscribeToken`, `notify`, `parseNotificationUnsubscribeToken`, `recordNotificationDelivery`, `resolveDeliveryRecipient`, `setNotificationPreference`, `unsubscribeTokenFromRequest`, `unsubscribeWithToken`, `useBroadcast`, `useChat`, `useDatabase`, `useEmail`, `useNotification`, `usePush`, `useSMS`, `wherePreferenceCategory`.
611
611
 
612
- Local source modules: [src/delivery.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/delivery.ts), [src/drivers/broadcast.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/drivers/broadcast.ts), [src/drivers/database.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/drivers/database.ts), [src/drivers/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/drivers/index.ts), [src/email-body.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/email-body.ts), [src/preferences.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/preferences.ts).
612
+ Local source modules: [src/delivery.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/delivery.ts), [src/drivers/broadcast.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/drivers/broadcast.ts), [src/drivers/database.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/drivers/database.ts), [src/drivers/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/drivers/index.ts), [src/email-body.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/email-body.ts), [src/preferences.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/preferences.ts), [src/unsubscribe.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/unsubscribe.ts).
613
613
 
614
- Evidence directory: [11 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
614
+ Evidence directory: [12 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
615
615
 
616
616
  ### @stacksjs/objects
617
617
 
@@ -78,6 +78,44 @@ missing optional schema compatibility from real persistence failures. Apply
78
78
  model-driven migrations before relying on persistence. Inspect channel results
79
79
  and transport results rather than assuming a delivery log is provider proof.
80
80
 
81
+ ## Marketing: unsubscribe links and opt-out lines
82
+
83
+ Give a marketing notification `payload.unsubscribe`, built per person with
84
+ `notificationUnsubscribeLinks(userId, { notice })`:
85
+
86
+ ```ts
87
+ await notify(
88
+ { userId, email },
89
+ { subject, body, unsubscribe: notificationUnsubscribeLinks(userId, { notice: 'You get these because marketing email is on.' }) },
90
+ ['email'],
91
+ { category: 'marketing' },
92
+ )
93
+ ```
94
+
95
+ The email's footer then shows the notice, **Unsubscribe** (`url`, default
96
+ `{APP_URL}/unsubscribe?token=`) and **Email preferences** (`/settings`) instead
97
+ of "do not reply"; the text part spells the link out; and the message carries
98
+ `List-Unsubscribe: <oneClickUrl>` plus `List-Unsubscribe-Post:
99
+ List-Unsubscribe=One-Click` (RFC 8058, default
100
+ `{APP_URL}/api/notifications/unsubscribe?token=`). `payload.headers` adds
101
+ other headers. The pure builders are `notificationEmailMessage`,
102
+ `notificationEmailHtml`, `notificationEmailText` and `listUnsubscribeHeaders`.
103
+
104
+ The token (`notificationUnsubscribeToken`, `parseNotificationUnsubscribeToken`)
105
+ is `base64url("{userId}:{channel}:{category}:{hmac}")` signed with `APP_KEY`,
106
+ which must be set in production. It does not expire. It turns off one
107
+ preference (channel + category), unlike `@stacksjs/email`'s tokens, which
108
+ suppress an address for good. The app owns the page and the route; the route's
109
+ handler is `unsubscribeWithToken(await unsubscribeTokenFromRequest(request))`,
110
+ which reads the query string (one-click form POSTs) before a JSON body, and
111
+ answers 422 when it returns null. Ask before unsubscribing on the page, so a
112
+ link scanner opening the URL changes nothing.
113
+
114
+ A `marketing` text ends with "Reply STOP to opt out." (`notificationSmsBody`).
115
+ `payload.smsOptOut` replaces the line (and adds it to other categories), and
116
+ `false` leaves it out. STOP only becomes an opt-out the app sees when inbound
117
+ SMS is wired up; see `stacks-sms`.
118
+
81
119
  ## Channel factories
82
120
 
83
121
  - `useEmail(driver?)` returns a sendable transport. Without a driver it uses
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.137",
5
+ "version": "0.75.138",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
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.75.137",
5
+ "version": "0.75.138",
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.35",
58
- "@stacksjs/mobile": "0.75.137",
58
+ "@stacksjs/mobile": "0.75.138",
59
59
  "@stacksjs/sanitizer": "^0.2.422",
60
60
  "@xterm/addon-fit": "^0.11.0",
61
61
  "@xterm/xterm": "^6.0.0",
@@ -4,7 +4,8 @@
4
4
  // breaks kept, addresses as links) and, when the notification names one, a
5
5
  // button to where it happened. Same <EmailLayout> as the other prebaked
6
6
  // mailers (stacksjs/stacks#1901); an app's own resources/emails/notification
7
- // wins over this one.
7
+ // wins over this one. A notification with `unsubscribe` (marketing) says why
8
+ // it came and links to Unsubscribe in the footer instead of "do not reply".
8
9
  const appName = props.appName || 'Stacks'
9
10
  const primaryColor = props.primaryColor || '#3451b2'
10
11
  const subject = props.subject || appName
@@ -12,6 +13,9 @@ const paragraphs = Array.isArray(props.paragraphs) ? props.paragraphs : []
12
13
  const actionUrl = props.actionUrl || ''
13
14
  const actionLabel = props.actionLabel || 'Open'
14
15
  const preheader = props.preheader || ''
16
+ const unsubscribeUrl = props.unsubscribeUrl || ''
17
+ const unsubscribeNotice = props.unsubscribeNotice || 'You are receiving this because these emails are turned on in your settings.'
18
+ const preferencesUrl = props.preferencesUrl || ''
15
19
  const year = props.year || new Date().getFullYear()
16
20
  </script>
17
21
 
@@ -36,7 +40,12 @@ const year = props.year || new Date().getFullYear()
36
40
  <template #footer>
37
41
  <tr>
38
42
  <td style="padding:20px 40px;background-color:#f9fafb;border-top:1px solid #e5e7eb;">
39
- <p style="margin:0;color:#6b7280;font-size:12px;text-align:center;">This is an automated message from {{ appName }}. Please do not reply to this email.</p>
43
+ @if(unsubscribeUrl)
44
+ <p style="margin:0;color:#6b7280;font-size:12px;text-align:center;">{{ unsubscribeNotice }}</p>
45
+ <p style="margin:8px 0 0 0;color:#6b7280;font-size:12px;text-align:center;"><a href="{{ unsubscribeUrl }}" style="color:#6b7280;text-decoration:underline;">Unsubscribe</a>@if(preferencesUrl) &middot; <a href="{{ preferencesUrl }}" style="color:#6b7280;text-decoration:underline;">Email preferences</a>@endif</p>
46
+ @else
47
+ <p style="margin:0;color:#6b7280;font-size:12px;text-align:center;">This is an automated message from {{ appName }}. Please do not reply to this email.</p>
48
+ @endif
40
49
  <p style="margin:10px 0 0 0;color:#9ca3af;font-size:11px;text-align:center;">&copy; {{ year }} {{ appName }}. All rights reserved.</p>
41
50
  </td>
42
51
  </tr>