@fonderie/courier 1.0.0 → 1.0.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.
- package/brain/outcomes.md +55 -0
- package/brain/signatures.md +136 -0
- package/dist/migrations/index.d.ts +3 -0
- package/package.json +9 -8
- package/dist/migrations/sql/001_courier.sql +0 -34
- package/dist/migrations/sql/002_seed_templates.sql +0 -49
- package/dist/migrations/sql/003_message_log_delivery.sql +0 -12
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<!-- GENERATED — do not edit. Regenerate with: npm run docs:signatures -->
|
|
2
|
+
|
|
3
|
+
# @fonderie/courier — outcomes
|
|
4
|
+
|
|
5
|
+
What this package does to a running app: tables its migrations create,
|
|
6
|
+
rows it seeds, routes it registers. Generated from the migration SQL and
|
|
7
|
+
route tables in source — trust this file instead of reading `dist/` or
|
|
8
|
+
downloading tarballs.
|
|
9
|
+
|
|
10
|
+
## Database tables (after all migrations)
|
|
11
|
+
|
|
12
|
+
### `fonderie_courier_templates`
|
|
13
|
+
|
|
14
|
+
```sql
|
|
15
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
|
|
16
|
+
type TEXT NOT NULL
|
|
17
|
+
locale TEXT
|
|
18
|
+
subject TEXT
|
|
19
|
+
html TEXT
|
|
20
|
+
text TEXT NOT NULL
|
|
21
|
+
active BOOLEAN NOT NULL DEFAULT true
|
|
22
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
23
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
24
|
+
-- UNIQUE (type, locale)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### `fonderie_message_log`
|
|
28
|
+
|
|
29
|
+
```sql
|
|
30
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
|
|
31
|
+
message_type TEXT NOT NULL
|
|
32
|
+
channel TEXT NOT NULL
|
|
33
|
+
recipient TEXT NOT NULL
|
|
34
|
+
locale TEXT
|
|
35
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
36
|
+
error TEXT
|
|
37
|
+
attempts INT NOT NULL DEFAULT 0
|
|
38
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
39
|
+
sent_at TIMESTAMPTZ
|
|
40
|
+
provider TEXT
|
|
41
|
+
provider_message_id TEXT
|
|
42
|
+
opened_at TIMESTAMPTZ
|
|
43
|
+
clicked_at TIMESTAMPTZ
|
|
44
|
+
bounced_at TIMESTAMPTZ
|
|
45
|
+
bounce_reason TEXT
|
|
46
|
+
-- INDEX idx_fml_created (created_at DESC)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Raw SQL ships in `node_modules/@fonderie/courier/dist/migrations/sql/` — read it there if you must; never download tarballs.
|
|
50
|
+
|
|
51
|
+
## Migration statements not replayed (verify in raw SQL)
|
|
52
|
+
|
|
53
|
+
- `ELSE`
|
|
54
|
+
- `END IF`
|
|
55
|
+
- `END $$`
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<!-- GENERATED — do not edit. Regenerate with: npm run docs:signatures -->
|
|
2
|
+
|
|
3
|
+
# @fonderie/courier — signatures
|
|
4
|
+
|
|
5
|
+
## @fonderie/courier
|
|
6
|
+
|
|
7
|
+
Subpath exports: `@fonderie/courier/types`, `@fonderie/courier/migrations`
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
new CourierModule(config: ICourierConfig, store?: IStoreAdapter | undefined, bus?: EventBus | undefined): CourierModule
|
|
11
|
+
.name: "@fonderie/courier"
|
|
12
|
+
.deps: string[]
|
|
13
|
+
.dispatcher: Dispatcher
|
|
14
|
+
.install(app: IFonderieApp): void
|
|
15
|
+
|
|
16
|
+
function handleSendGridDelivery(req: Request, store: IStoreAdapter, webhookSecret?: string | undefined): Promise<Response>
|
|
17
|
+
|
|
18
|
+
function handleMailgunDelivery(req: Request, store: IStoreAdapter, signingKey?: string | undefined): Promise<Response>
|
|
19
|
+
|
|
20
|
+
function handleMailtrapDelivery(req: Request, store: IStoreAdapter): Promise<Response>
|
|
21
|
+
|
|
22
|
+
new Dispatcher(config: ICourierConfig, resolver: ITemplateResolver, store?: IStoreAdapter | undefined): Dispatcher
|
|
23
|
+
.registerChannel(channel: ICourierChannel): Dispatcher
|
|
24
|
+
.dispatch(message: ICourierMessage): Promise<void>
|
|
25
|
+
|
|
26
|
+
new SmsChannel(config: ISmsChannelConfig): SmsChannel
|
|
27
|
+
.name: "sms"
|
|
28
|
+
.send(message: ICourierMessage, template: IRenderedTemplate): Promise<void>
|
|
29
|
+
|
|
30
|
+
new PushChannel(config: IPushChannelConfig): PushChannel
|
|
31
|
+
.name: "push"
|
|
32
|
+
.send(message: ICourierMessage, template: IRenderedTemplate): Promise<void>
|
|
33
|
+
|
|
34
|
+
new EmailChannel(config: IEmailChannelConfig): EmailChannel
|
|
35
|
+
.name: "email"
|
|
36
|
+
.send(message: ICourierMessage, template: IRenderedTemplate): Promise<void>
|
|
37
|
+
|
|
38
|
+
new DBTemplateResolver(store: IStoreAdapter): DBTemplateResolver
|
|
39
|
+
.resolve(type: string, data: Record<string, unknown>, locale?: string | undefined): Promise<IRenderedTemplate>
|
|
40
|
+
|
|
41
|
+
new FSTemplateResolver(directory: string): FSTemplateResolver
|
|
42
|
+
.resolve(type: string, data: Record<string, unknown>, locale?: string | undefined): Promise<IRenderedTemplate>
|
|
43
|
+
|
|
44
|
+
interface IMessageLog {
|
|
45
|
+
id: string;
|
|
46
|
+
messageType: string;
|
|
47
|
+
channel: string;
|
|
48
|
+
recipient: string;
|
|
49
|
+
locale: string | null;
|
|
50
|
+
status: MessageLogStatus;
|
|
51
|
+
error: string | null;
|
|
52
|
+
attempts: number;
|
|
53
|
+
provider: string | null;
|
|
54
|
+
providerMessageId: string | null;
|
|
55
|
+
openedAt: string | null;
|
|
56
|
+
clickedAt: string | null;
|
|
57
|
+
bouncedAt: string | null;
|
|
58
|
+
bounceReason: string | null;
|
|
59
|
+
createdAt: string;
|
|
60
|
+
sentAt: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type MessageLogStatus = 'pending' | 'sent' | 'failed' | 'delivered' | 'opened' | 'clicked' | 'bounced' | 'spam';
|
|
64
|
+
|
|
65
|
+
interface ICourierMessage {
|
|
66
|
+
type: string;
|
|
67
|
+
locale?: string;
|
|
68
|
+
recipient: {
|
|
69
|
+
email: string | null;
|
|
70
|
+
phone: string | null;
|
|
71
|
+
deviceToken: string | null;
|
|
72
|
+
};
|
|
73
|
+
data: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface ICourierChannel {
|
|
77
|
+
name: string;
|
|
78
|
+
send(message: ICourierMessage, template: IRenderedTemplate): Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface IRenderedTemplate {
|
|
82
|
+
subject?: string;
|
|
83
|
+
html?: string;
|
|
84
|
+
text: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface ITemplateResolver {
|
|
88
|
+
resolve(type: string, data: Record<string, unknown>, locale?: string): Promise<IRenderedTemplate>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const Channel: { readonly EMAIL: "email"; readonly SMS: "sms"; readonly PUSH: "push"; }
|
|
92
|
+
|
|
93
|
+
interface ICourierConfig {
|
|
94
|
+
channels: Record<string, Array<'email' | 'sms' | 'push'>>;
|
|
95
|
+
sms?: ISmsChannelConfig;
|
|
96
|
+
push?: IPushChannelConfig;
|
|
97
|
+
email?: IEmailChannelConfig;
|
|
98
|
+
templates?: {
|
|
99
|
+
source: 'db' | 'fs';
|
|
100
|
+
directory?: string;
|
|
101
|
+
};
|
|
102
|
+
delivery?: {
|
|
103
|
+
signingKeys?: {
|
|
104
|
+
sendgrid?: string;
|
|
105
|
+
mailgun?: string;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface IEmailChannelConfig {
|
|
111
|
+
provider: 'resend' | 'ses' | 'smtp';
|
|
112
|
+
from: string;
|
|
113
|
+
apiKey?: string;
|
|
114
|
+
smtp?: {
|
|
115
|
+
host: string;
|
|
116
|
+
port: number;
|
|
117
|
+
secure: boolean;
|
|
118
|
+
user: string;
|
|
119
|
+
pass: string;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface ISmsChannelConfig {
|
|
124
|
+
provider: 'twilio' | 'vonage';
|
|
125
|
+
from: string;
|
|
126
|
+
accountSid?: string;
|
|
127
|
+
authToken?: string;
|
|
128
|
+
apiKey?: string;
|
|
129
|
+
apiSecret?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface IPushChannelConfig {
|
|
133
|
+
provider: 'fcm';
|
|
134
|
+
serviceAccount: Record<string, unknown>;
|
|
135
|
+
}
|
|
136
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonderie/courier",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Transactional messaging for SaaS — multi-channel delivery (email, SMS, push) with FS or DB templates, per-message-type channel routing, and a persistent message log.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fonderie-js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"module": "./dist/index.js",
|
|
40
40
|
"types": "./dist/index.d.ts",
|
|
41
41
|
"scripts": {
|
|
42
|
-
"build": "tsup &&
|
|
42
|
+
"build": "tsup && tsup --config tsup.migrations.ts",
|
|
43
43
|
"dev": "tsup --watch",
|
|
44
44
|
"typecheck": "tsc --noEmit",
|
|
45
45
|
"test": "tsx --test src/__tests__/*.test.ts",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"nodemailer": "^8.0.7"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@fonderie/core": "^0.1.
|
|
55
|
-
"@fonderie/store": "^0.1.
|
|
56
|
-
"@fonderie/events": "^1.0.
|
|
54
|
+
"@fonderie/core": "^0.1.1",
|
|
55
|
+
"@fonderie/store": "^0.1.1",
|
|
56
|
+
"@fonderie/events": "^1.0.1"
|
|
57
57
|
},
|
|
58
58
|
"peerDependenciesMeta": {
|
|
59
59
|
"@fonderie/store": {
|
|
@@ -78,16 +78,17 @@
|
|
|
78
78
|
},
|
|
79
79
|
"files": [
|
|
80
80
|
"dist",
|
|
81
|
+
"brain",
|
|
81
82
|
"LICENSE",
|
|
82
83
|
"README.md"
|
|
83
84
|
],
|
|
84
85
|
"repository": {
|
|
85
86
|
"type": "git",
|
|
86
|
-
"url": "git+https://github.com/
|
|
87
|
+
"url": "git+https://github.com/fonderiejs/sdk.git",
|
|
87
88
|
"directory": "packages/courier"
|
|
88
89
|
},
|
|
89
|
-
"homepage": "https://github.com/
|
|
90
|
+
"homepage": "https://github.com/fonderiejs/sdk/tree/main/packages/courier#readme",
|
|
90
91
|
"bugs": {
|
|
91
|
-
"url": "https://github.com/
|
|
92
|
+
"url": "https://github.com/fonderiejs/sdk/issues"
|
|
92
93
|
}
|
|
93
94
|
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
-- fonderie_courier_templates: per-type, per-locale message templates
|
|
2
|
-
CREATE TABLE IF NOT EXISTS fonderie_courier_templates (
|
|
3
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4
|
-
type TEXT NOT NULL,
|
|
5
|
-
locale TEXT, -- NULL = default / catch-all locale
|
|
6
|
-
subject TEXT,
|
|
7
|
-
html TEXT,
|
|
8
|
-
text TEXT NOT NULL,
|
|
9
|
-
active BOOLEAN NOT NULL DEFAULT true,
|
|
10
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
11
|
-
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
12
|
-
UNIQUE (type, locale)
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
CREATE INDEX IF NOT EXISTS idx_fct_type
|
|
16
|
-
ON fonderie_courier_templates (type) WHERE active = true;
|
|
17
|
-
|
|
18
|
-
-- fonderie_message_log: audit trail of every dispatch attempt
|
|
19
|
-
CREATE TABLE IF NOT EXISTS fonderie_message_log (
|
|
20
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
21
|
-
message_type TEXT NOT NULL,
|
|
22
|
-
channel TEXT NOT NULL,
|
|
23
|
-
recipient TEXT NOT NULL,
|
|
24
|
-
locale TEXT,
|
|
25
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
26
|
-
error TEXT,
|
|
27
|
-
attempts INT NOT NULL DEFAULT 0,
|
|
28
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
29
|
-
sent_at TIMESTAMPTZ
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
CREATE INDEX IF NOT EXISTS idx_fml_type ON fonderie_message_log (message_type);
|
|
33
|
-
CREATE INDEX IF NOT EXISTS idx_fml_status ON fonderie_message_log (status);
|
|
34
|
-
CREATE INDEX IF NOT EXISTS idx_fml_created ON fonderie_message_log (created_at DESC);
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
-- Seed default email templates
|
|
2
|
-
-- Uses UPDATE + conditional INSERT to safely handle NULL locale (ON CONFLICT
|
|
3
|
-
-- does not fire for NULL columns in standard Postgres unique indexes).
|
|
4
|
-
|
|
5
|
-
-- email-verification — variables: pin, firstName
|
|
6
|
-
DO $$
|
|
7
|
-
BEGIN
|
|
8
|
-
IF EXISTS (
|
|
9
|
-
SELECT 1 FROM fonderie_courier_templates
|
|
10
|
-
WHERE type = 'email-verification' AND locale IS NULL
|
|
11
|
-
) THEN
|
|
12
|
-
UPDATE fonderie_courier_templates
|
|
13
|
-
SET
|
|
14
|
-
subject = 'Your verification code',
|
|
15
|
-
html = '<p>Hi {{firstName}},</p>
|
|
16
|
-
<p>Your email verification code is:</p>
|
|
17
|
-
<h2 style="letter-spacing:0.2em;">{{pin}}</h2>
|
|
18
|
-
<p>Enter this code to complete your registration. It expires in 24 hours.</p>
|
|
19
|
-
<p>If you did not create an account, you can safely ignore this email.</p>',
|
|
20
|
-
text = 'Hi {{firstName}},
|
|
21
|
-
|
|
22
|
-
Your email verification code is: {{pin}}
|
|
23
|
-
|
|
24
|
-
Enter this code to complete your registration. It expires in 24 hours.
|
|
25
|
-
|
|
26
|
-
If you did not create an account, you can safely ignore this email.',
|
|
27
|
-
updated_at = now()
|
|
28
|
-
WHERE type = 'email-verification' AND locale IS NULL;
|
|
29
|
-
ELSE
|
|
30
|
-
INSERT INTO fonderie_courier_templates (type, locale, subject, html, text)
|
|
31
|
-
VALUES (
|
|
32
|
-
'email-verification',
|
|
33
|
-
NULL,
|
|
34
|
-
'Your verification code',
|
|
35
|
-
'<p>Hi {{firstName}},</p>
|
|
36
|
-
<p>Your email verification code is:</p>
|
|
37
|
-
<h2 style="letter-spacing:0.2em;">{{pin}}</h2>
|
|
38
|
-
<p>Enter this code to complete your registration. It expires in 24 hours.</p>
|
|
39
|
-
<p>If you did not create an account, you can safely ignore this email.</p>',
|
|
40
|
-
'Hi {{firstName}},
|
|
41
|
-
|
|
42
|
-
Your email verification code is: {{pin}}
|
|
43
|
-
|
|
44
|
-
Enter this code to complete your registration. It expires in 24 hours.
|
|
45
|
-
|
|
46
|
-
If you did not create an account, you can safely ignore this email.'
|
|
47
|
-
);
|
|
48
|
-
END IF;
|
|
49
|
-
END $$;
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
-- Extend fonderie_message_log for delivery event correlation
|
|
2
|
-
ALTER TABLE fonderie_message_log
|
|
3
|
-
ADD COLUMN IF NOT EXISTS provider TEXT,
|
|
4
|
-
ADD COLUMN IF NOT EXISTS provider_message_id TEXT,
|
|
5
|
-
ADD COLUMN IF NOT EXISTS opened_at TIMESTAMPTZ,
|
|
6
|
-
ADD COLUMN IF NOT EXISTS clicked_at TIMESTAMPTZ,
|
|
7
|
-
ADD COLUMN IF NOT EXISTS bounced_at TIMESTAMPTZ,
|
|
8
|
-
ADD COLUMN IF NOT EXISTS bounce_reason TEXT;
|
|
9
|
-
|
|
10
|
-
CREATE INDEX IF NOT EXISTS idx_fml_provider_msg_id
|
|
11
|
-
ON fonderie_message_log (provider_message_id)
|
|
12
|
-
WHERE provider_message_id IS NOT NULL;
|