@stacksjs/newsletter 0.70.86 → 0.70.88

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/newsletter",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.86",
5
+ "version": "0.70.88",
6
6
  "description": "Promotional email & newsletter primitives for Stacks. Lists, campaigns, subscribers, and one-click unsubscribe — sent through @stacksjs/email.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -56,10 +56,10 @@
56
56
  "prepublishOnly": "bun run build"
57
57
  },
58
58
  "devDependencies": {
59
- "@stacksjs/cli": "0.70.86",
60
- "@stacksjs/config": "0.70.86",
59
+ "@stacksjs/cli": "0.70.88",
60
+ "@stacksjs/config": "0.70.88",
61
61
  "better-dx": "^0.2.16",
62
- "@stacksjs/error-handling": "0.70.86",
63
- "@stacksjs/types": "0.70.86"
62
+ "@stacksjs/error-handling": "0.70.88",
63
+ "@stacksjs/types": "0.70.88"
64
64
  }
65
65
  }
@@ -1,37 +0,0 @@
1
- /**
2
- * Campaign CRUD + dispatch.
3
- *
4
- * `sendNow()` and `schedule()` both end up dispatching the
5
- * `SendCampaignJob` queue handler — `schedule()` just adds a `delay()`
6
- * computed against the campaign's `scheduled_at`. The job is the one
7
- * source of truth for actually fanning out emails so that a manual
8
- * `sendNow()` from the dashboard and a cron-fired scheduled run go
9
- * through identical machinery.
10
- */
11
- declare function resolveListId(input: CreateCampaignInput): Promise<number>;
12
- /**
13
- * @defaultValue
14
- * ```ts
15
- * {
16
- * create: () => unknown,
17
- * find: () => unknown,
18
- * update: () => unknown,
19
- * sendNow: () => unknown,
20
- * schedule: () => unknown,
21
- * cancel: () => unknown
22
- * }
23
- * ```
24
- */
25
- export declare const campaigns: {
26
- create: (input: CreateCampaignInput) => Promise<void>;
27
- find: (id: number) => Promise<void>;
28
- update: (id: number, patch: Partial<CreateCampaignInput>) => Promise<void>;
29
- /** Move a draft straight into the queue. */
30
- sendNow: (id: number, options?: SendCampaignOptions) => Promise<void>;
31
- /**
32
- * Persist a `scheduled_at` and dispatch the job with a delay computed
33
- * from now. If the time is in the past, falls back to immediate send.
34
- */
35
- schedule: (id: number, scheduledAt: Date | string, options?: SendCampaignOptions) => Promise<void>;
36
- cancel: (id: number) => Promise<void>
37
- };
package/dist/headers.d.ts DELETED
@@ -1,25 +0,0 @@
1
- /**
2
- * Build the headers for an outbound campaign email.
3
- *
4
- * Returns a flat header map that drivers can copy into the outgoing
5
- * MIME envelope.
6
- */
7
- export declare function buildUnsubscribeHeaders(opts: UnsubscribeHeaderOptions): Record<string, string>;
8
- /**
9
- * RFC 8058 / RFC 2369 List-Unsubscribe header construction.
10
- *
11
- * Why both mailto: and https://? Some clients (older Outlook, some
12
- * webmails) only honor `mailto:`; modern clients prefer the one-click
13
- * HTTPS form. Including both lets every client unsubscribe without
14
- * forcing the user to look for a tiny in-body link.
15
- *
16
- * `List-Unsubscribe-Post: List-Unsubscribe=One-Click` is what tells
17
- * Gmail/Yahoo/etc. that POSTing to the https URL with no body is a
18
- * valid one-click unsubscribe. Without that header those mailbox
19
- * providers fall back to *clicking* the URL — which means a normal
20
- * GET handler. RFC 8058 clients will POST.
21
- */
22
- export declare interface UnsubscribeHeaderOptions {
23
- url: string
24
- mailto?: string
25
- }
package/dist/index.d.ts DELETED
@@ -1,6 +0,0 @@
1
- export * from './campaigns';
2
- export * from './headers';
3
- export * from './lists';
4
- export * from './newsletter';
5
- export * from './subscriptions';
6
- export * from './types';
package/dist/index.js DELETED
@@ -1,2 +0,0 @@
1
- // @bun
2
- var H=import.meta.require;function B(x){return x.toLowerCase().trim().replace(/[^\w\s-]+/g,"").replace(/\s+/g,"-").replace(/-+/g,"-")}var J={async create(x){let{EmailList:z}=await import("@stacksjs/orm"),D=x.slug??B(x.name);return z.create({name:x.name,slug:D,description:x.description??null,status:"active",isPublic:x.isPublic===!1?0:1,doubleOptIn:x.doubleOptIn===!1?0:1,subscriberCount:0,activeCount:0,unsubscribedCount:0,bouncedCount:0})},async find(x){let{EmailList:z}=await import("@stacksjs/orm");if(typeof x==="number")return z.find(x);return z.where("slug",x).first()},async all(){let{EmailList:x}=await import("@stacksjs/orm");return x.where("status","active").get()},async archive(x){let z=await J.find(x);if(!z)throw Error(`[newsletter] List '${String(x)}' not found`);return z.update({status:"archived"})}};async function K(x){if(x.emailListId)return x.emailListId;if(x.emailListSlug){let z=await J.find(x.emailListSlug);if(!z)throw Error(`[newsletter] List '${x.emailListSlug}' not found`);return z.id}throw Error("[newsletter] Campaign requires emailListId or emailListSlug")}var V={async create(x){let{Campaign:z}=await import("@stacksjs/orm"),D=await K(x),F=x.scheduledAt?"scheduled":"draft";return z.create({name:x.name,description:x.description??null,type:"email",status:F,subject:x.subject,template:x.template,text:x.text??null,from_name:x.fromName??null,from_address:x.fromAddress??null,email_list_id:D,scheduled_at:x.scheduledAt??null,sentCount:0})},async find(x){let{Campaign:z}=await import("@stacksjs/orm");return z.find(x)},async update(x,z){let D=await V.find(x);if(!D)throw Error(`[newsletter] Campaign ${x} not found`);if(D.status!=="draft"&&D.status!=="scheduled")throw Error(`[newsletter] Cannot edit campaign in status '${D.status}'`);return D.update(z)},async sendNow(x,z={}){let D=await V.find(x);if(!D)throw Error(`[newsletter] Campaign ${x} not found`);if(D.status==="sending"||D.status==="sent")throw Error(`[newsletter] Campaign ${x} already ${D.status}`);await D.update({status:"sending"});let{job:F}=await import("@stacksjs/queue");return await F("SendCampaign",{campaignId:x,chunkSize:z.chunkSize??50,dryRun:z.dryRun??!1}).onQueue("campaigns").dispatch(),{ok:!0,campaignId:x}},async schedule(x,z,D={}){let F=await V.find(x);if(!F)throw Error(`[newsletter] Campaign ${x} not found`);let G=z instanceof Date?z:new Date(z),M=Math.max(0,Math.floor((G.getTime()-Date.now())/1000));await F.update({status:M===0?"sending":"scheduled",scheduled_at:G.toISOString()});let{job:Q}=await import("@stacksjs/queue");return await Q("SendCampaign",{campaignId:x,chunkSize:D.chunkSize??50,dryRun:D.dryRun??!1}).onQueue("campaigns").delay(M).dispatch(),{ok:!0,campaignId:x,scheduledAt:G.toISOString()}},async cancel(x){let z=await V.find(x);if(!z)throw Error(`[newsletter] Campaign ${x} not found`);if(z.status==="sent")throw Error(`[newsletter] Campaign ${x} already sent \u2014 cannot cancel`);return z.update({status:"cancelled"})}};function U(x){if(!x.url||!/^https?:\/\//i.test(x.url))throw Error("[newsletter] List-Unsubscribe URL must be absolute (http/https)");let z=[];if(x.mailto)z.push(`<mailto:${x.mailto}>`);return z.push(`<${x.url}>`),{"List-Unsubscribe":z.join(", "),"List-Unsubscribe-Post":"List-Unsubscribe=One-Click"}}async function P(x){if(x==null){let D=await J.find("default");if(!D)D=await J.create({name:"Default",slug:"default"});return D.id}let z=await J.find(x);if(!z)throw Error(`[newsletter] List '${String(x)}' not found`);return z.id}async function X(x,z={}){if(!x||!x.includes("@"))throw Error("[newsletter] subscribe() requires a valid email address");let{Subscriber:D,EmailListSubscriber:F}=await import("@stacksjs/orm"),G=await P(z.list),M=z.source??"api",Q=await D.where("email",x).first();if(!Q)Q=await D.create({email:x,status:"subscribed",source:M});let W=await F.where("subscriber_id",Q.id).where("email_list_id",G).first();if(W){if(W.status==="unsubscribed")await W.update({status:"subscribed",unsubscribed_at:null});return{created:!1,email:x,listId:G,token:W.uuid}}let $=await F.create({subscriber_id:Q.id,email_list_id:G,status:"subscribed",source:M});return{created:!0,email:x,listId:G,token:$.uuid}}async function Y(x){if(!x)return{ok:!1};let{EmailListSubscriber:z,Subscriber:D}=await import("@stacksjs/orm"),F=await z.where("uuid",x).first();if(!F)return{ok:!1};if(F.status==="unsubscribed")return{ok:!0,alreadyUnsubscribed:!0,email:(await D.find(F.subscriber_id))?.email,listId:F.email_list_id};return await F.update({status:"unsubscribed",unsubscribed_at:new Date().toISOString()}),{ok:!0,email:(await D.find(F.subscriber_id))?.email,listId:F.email_list_id}}async function Z(x){let{Subscriber:z,EmailListSubscriber:D}=await import("@stacksjs/orm"),F=await z.where("email",x).first();if(!F)return 0;let G=await D.where("subscriber_id",F.id).where("status","subscribed").get();for(let M of G)await M.update({status:"unsubscribed",unsubscribed_at:new Date().toISOString()});return G.length}var I={lists:J,campaigns:V,subscribe:X,unsubscribe:Y,unsubscribeAll:Z};export{Z as unsubscribeAll,Y as unsubscribe,X as subscribe,I as newsletter,J as lists,V as campaigns,U as buildUnsubscribeHeaders};
package/dist/lists.d.ts DELETED
@@ -1,26 +0,0 @@
1
- /**
2
- * EmailList CRUD wrapper.
3
- *
4
- * The model lives in `defaults/app/Models/EmailList.ts`; here we expose
5
- * a friendlier surface for application code so callers don't depend on
6
- * raw ORM idioms (which changes across releases).
7
- */
8
- declare function slugify(input: string): string;
9
- /**
10
- * @defaultValue
11
- * ```ts
12
- * {
13
- * create: () => unknown,
14
- * find: () => unknown,
15
- * all: () => unknown,
16
- * archive: () => unknown
17
- * }
18
- * ```
19
- */
20
- export declare const lists: {
21
- create: (input: CreateListInput) => Promise<void>;
22
- /** Look up by slug first, then by id — slugs are the public-facing handle. */
23
- find: (idOrSlug: number | string) => Promise<void>;
24
- all: () => Promise<void>;
25
- archive: (idOrSlug: number | string) => Promise<void>
26
- };
@@ -1,17 +0,0 @@
1
- import { campaigns } from './campaigns';
2
- import { lists } from './lists';
3
- import { subscribe, unsubscribe, unsubscribeAll } from './subscriptions';
4
- /**
5
- * Top-level Newsletter facade.
6
- *
7
- * Keeps the import surface tiny — most call sites only need
8
- * `newsletter.subscribe()` or `newsletter.campaigns.sendNow()`.
9
- */
10
- export declare const newsletter: {
11
- lists: typeof lists
12
- campaigns: typeof campaigns
13
- subscribe: typeof subscribe
14
- unsubscribe: typeof unsubscribe
15
- unsubscribeAll: typeof unsubscribeAll
16
- };
17
- export { campaigns, lists, subscribe, unsubscribe, unsubscribeAll };
@@ -1,5 +0,0 @@
1
- import type { SubscribeOptions, SubscribeResult, UnsubscribeResult } from './types';
2
- export declare function subscribe(email: string, options?: SubscribeOptions): Promise<SubscribeResult>;
3
- export declare function unsubscribe(token: string): Promise<UnsubscribeResult>;
4
- /** Bulk unsubscribe by email — used by bounce/complaint handlers. */
5
- export declare function unsubscribeAll(email: string): Promise<number>;
package/dist/types.d.ts DELETED
@@ -1,64 +0,0 @@
1
- export declare interface CreateListInput {
2
- name: string
3
- slug?: string
4
- description?: string
5
- doubleOptIn?: boolean
6
- isPublic?: boolean
7
- }
8
- export declare interface CreateCampaignInput {
9
- name: string
10
- subject: string
11
- template: string
12
- text?: string
13
- emailListId?: number
14
- emailListSlug?: string
15
- fromName?: string
16
- fromAddress?: string
17
- scheduledAt?: string
18
- description?: string
19
- }
20
- export declare interface SubscribeOptions {
21
- list?: string | number
22
- source?: string
23
- upsert?: boolean
24
- }
25
- export declare interface SubscribeResult {
26
- created: boolean
27
- email: string
28
- listId: number
29
- token: string
30
- }
31
- export declare interface UnsubscribeResult {
32
- ok: boolean
33
- email?: string
34
- listId?: number
35
- alreadyUnsubscribed?: boolean
36
- }
37
- export declare interface SendCampaignOptions {
38
- chunkSize?: number
39
- dryRun?: boolean
40
- }
41
- /**
42
- * Public types for the @stacksjs/newsletter package.
43
- *
44
- * The shapes here describe the *facade input/output*, not the underlying
45
- * model rows — that lets us keep the public API stable even as model
46
- * attribute lists evolve.
47
- */
48
- export type EmailListStatus = 'active' | 'inactive' | 'archived';
49
- export type CampaignStatus = | 'draft'
50
- | 'scheduled'
51
- | 'sending'
52
- | 'sent'
53
- | 'paused'
54
- | 'cancelled'
55
- | 'failed';
56
- export type SubscriptionStatus = | 'subscribed'
57
- | 'unsubscribed'
58
- | 'pending'
59
- | 'bounced';
60
- export type CampaignSendStatus = | 'queued'
61
- | 'sent'
62
- | 'failed'
63
- | 'bounced'
64
- | 'complained';