@awsmbiz/inbis-notifications 0.0.0-stage → 0.2.0
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/README.md +206 -2
- package/dist/index.d.ts +354 -0
- package/dist/index.js +644 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -3
package/README.md
CHANGED
|
@@ -1,3 +1,207 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @awsmbiz/inbis-notifications
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Delivery adapters and templates for Inbis apps. They implement the `EmailSender`, `SmsSender`
|
|
4
|
+
and `AuthEmailTemplates` interfaces from `@awsmbiz/inbis-core`, so other packages (such as
|
|
5
|
+
`@awsmbiz/inbis-auth`) never depend on a provider.
|
|
6
|
+
|
|
7
|
+
All real email and SMS go through **AWSM's shared communication service**
|
|
8
|
+
(`awsmbiz/biz-cloud-communication-service`), reached over a Cloudflare service binding. The
|
|
9
|
+
service owns quotas, suspension, provider credentials (Cloudflare Email Sending, Chatico) and
|
|
10
|
+
retries; this package does not duplicate them. Log adapters cover local development.
|
|
11
|
+
|
|
12
|
+
| Export | Contents |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `createCommunicationServiceEmailSender({ binding, appId, from?, replyTo?, smtp? })` | `EmailSender` over the service's `sendEmail`. Returns `{ messageId }` when the service gives one. |
|
|
15
|
+
| `createCommunicationServiceSmsSender({ binding, appId, smsHeader?, templates, requiredTemplates?, formatRecipient? })` | `SmsSender` over the service's `sendSms`. Returns `{ messageId }` (Chatico's message ID, else the submission ID). |
|
|
16
|
+
| `CommunicationServiceBinding`, `communicationServiceBinding(fetcher)` | The service's RPC surface as Inbis uses it (typed structurally; no dependency on the service repo), and a helper that types a `Fetcher` binding as it. |
|
|
17
|
+
| `SmsTemplateConfig`, `SmtpCredentials` | Per-message-type DLT settings; per-app SMTP credentials. |
|
|
18
|
+
| `e164DigitsRecipient` (default), `indianTenDigitRecipient` | Phone number formatters for the service (see below). |
|
|
19
|
+
| `createLogEmailSender({ print?, from?, keep? })` | Development `EmailSender`. Prints every email in full and keeps the most recent `keep` (default 50) in memory for `recent()`. |
|
|
20
|
+
| `createLogSmsSender({ print?, keep? })` | Development `SmsSender` (options type `LogSmsSenderOptions`). Prints every SMS and keeps the most recent `keep` (default 50) for `recent()`. |
|
|
21
|
+
| `EmailDeliveryError`, `SmsDeliveryError` | `AppError`s thrown by the adapters, with `code`, `retryable` (and `providerCode` / `providerStatus`) and the service error as `cause`. |
|
|
22
|
+
| `authEmailTemplates`, `createAuthEmailTemplates(messages?)` | The verification, password-reset and invitation emails, as HTML + plain text. |
|
|
23
|
+
| `renderBrandedEmail(content)` | The branded layout behind those templates, for other emails that have one action button (`content`: a `BrandedEmailContent`). |
|
|
24
|
+
| `emailMessages`, `EmailMessageKey`, `EmailMessages` | Every template string by message key (English only for now). |
|
|
25
|
+
|
|
26
|
+
Depends only on `@awsmbiz/inbis-core`. Tested in workerd, against a fake binding (the real service is
|
|
27
|
+
never called).
|
|
28
|
+
|
|
29
|
+
## Setting up the communication service
|
|
30
|
+
|
|
31
|
+
1. **Bind the service** in each deployed environment of `wrangler.jsonc` (service bindings and
|
|
32
|
+
vars are not inherited by `env.*`, so repeat them per environment):
|
|
33
|
+
|
|
34
|
+
```jsonc
|
|
35
|
+
"services": [{ "binding": "COMMUNICATION_SERVICE", "service": "biz-cloud-communication-service" }]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The calling Worker must be in AWSM's Cloudflare account. Keep local development on the log
|
|
39
|
+
adapters; there is nothing to bind locally.
|
|
40
|
+
2. **`APP_ID`** is set by the platform on every deployed app. Always pass `env.APP_ID` as
|
|
41
|
+
`appId`; never hardcode it or take it from user input. Both factories throw at startup when
|
|
42
|
+
it is missing or blank.
|
|
43
|
+
3. **Register the app** with the service before its first send (an AWSM admin, from Command
|
|
44
|
+
Center): `registerApp(appId, { emailMonthlyQuota, smsMonthlyQuota })`. Until then every send
|
|
45
|
+
fails with `EMAIL_APP_NOT_REGISTERED` / `SMS_APP_NOT_REGISTERED`.
|
|
46
|
+
4. **Email:** the `from` domain must be onboarded and verified in Cloudflare Email Sending on
|
|
47
|
+
AWSM's account (else `EMAIL_SENDER_NOT_VERIFIED`).
|
|
48
|
+
5. **SMS:** the SMS quota defaults to `0`, which means SMS is off; ask for it to be raised
|
|
49
|
+
(`setAppLimits(appId, { smsMonthlyQuota })`). Register the sender header and every SMS
|
|
50
|
+
template on DLT under AWSM (the registered DLT entity) and configure them per message type
|
|
51
|
+
(below).
|
|
52
|
+
|
|
53
|
+
`wrangler types` types a service binding as a plain `Fetcher` unless it can see the service's
|
|
54
|
+
source; wrap it with `communicationServiceBinding(...)`:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { requireBinding } from "@awsmbiz/inbis-cloudflare";
|
|
58
|
+
import { communicationServiceBinding } from "@awsmbiz/inbis-notifications";
|
|
59
|
+
|
|
60
|
+
const binding = communicationServiceBinding(requireBinding(env, "COMMUNICATION_SERVICE"));
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Email
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { createCommunicationServiceEmailSender } from "@awsmbiz/inbis-notifications";
|
|
67
|
+
|
|
68
|
+
const email = createCommunicationServiceEmailSender({
|
|
69
|
+
binding,
|
|
70
|
+
appId: env.APP_ID,
|
|
71
|
+
from: "no-reply@billing.example.com", // default; a message's own `from` wins
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- **Metered path (default):** AWSM's Cloudflare Email Sending, counted against the app's
|
|
76
|
+
monthly email quota and retried by the service on delivery failure. It takes only plain-string
|
|
77
|
+
addresses: `from`, one recipient, an optional reply-to (sent as `reply`), `subject`, `text`
|
|
78
|
+
and `html`. A display name (`Billing <no-reply@…>`) is dropped to the bare address on this
|
|
79
|
+
path. Nothing else is ever sent.
|
|
80
|
+
- **SMTP path:** pass the app's own SMTP server as `smtp: { host, port, secure?, username,
|
|
81
|
+
password }`, read from Worker secrets (`requireSecret`). Every email then goes through it,
|
|
82
|
+
unmetered, with the display name kept. The service never stores the credentials, never retries
|
|
83
|
+
SMTP failures, and neither the service nor this adapter logs them.
|
|
84
|
+
- **Placeholder addresses** of phone-only users (any reserved `.invalid` domain, see
|
|
85
|
+
`isPlaceholderEmail` in `@awsmbiz/inbis-core`) are refused by both email adapters with
|
|
86
|
+
`EMAIL_RECIPIENT_PLACEHOLDER` (400, not retryable); nothing is sent or logged.
|
|
87
|
+
- `EmailMessage.idempotencyKey` is accepted but not deduplicated.
|
|
88
|
+
|
|
89
|
+
## SMS
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { requiredAuthSmsTemplates } from "@awsmbiz/inbis-auth";
|
|
93
|
+
import { createCommunicationServiceSmsSender } from "@awsmbiz/inbis-notifications";
|
|
94
|
+
|
|
95
|
+
const sms = createCommunicationServiceSmsSender({
|
|
96
|
+
binding,
|
|
97
|
+
appId: env.APP_ID,
|
|
98
|
+
smsHeader: "AWSMIN", // default DLT sender header
|
|
99
|
+
templates: {
|
|
100
|
+
// Message template key (from @awsmbiz/inbis-auth) -> the app's DLT registration.
|
|
101
|
+
"auth.phone-otp": {
|
|
102
|
+
templateId: "1107000000000000001",
|
|
103
|
+
text: "{code} is your {appName} sign-in code. It expires in {minutes} minutes.",
|
|
104
|
+
},
|
|
105
|
+
"auth.invitation": {
|
|
106
|
+
smsHeader: "AWSMIV", // optional per-type header
|
|
107
|
+
templateId: "1107000000000000002",
|
|
108
|
+
text: "{inviter} invited you to {appName}. Sign in with this phone number to join: {url}",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
requiredTemplates: requiredAuthSmsTemplates(config), // startup check
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- **The text must match the registered DLT template exactly**, so it is the app's to configure.
|
|
116
|
+
`text` is the registered template with `{name}` placeholders where DLT has `{#var#}`; the
|
|
117
|
+
sender fills them from the message's `template.variables` (`SmsMessage.template.id` picks the
|
|
118
|
+
entry). The message's own `text` is not sent. Chatico rejects a mismatch
|
|
119
|
+
(`SMS_TEMPLATE_MISMATCH`). Keep each variable within DLT's limit (commonly 30 characters per
|
|
120
|
+
`{#var#}`; check your registration), and remember a variable can be blank (e.g. an invitation
|
|
121
|
+
without an inviter name) when wording the template.
|
|
122
|
+
- `@awsmbiz/inbis-auth` sends `auth.phone-otp` (`code`, `appName`, `minutes`) and `auth.invitation`
|
|
123
|
+
(`appName`, `inviter`, `url`); `AUTH_SMS_TEMPLATES` names them and
|
|
124
|
+
`requiredAuthSmsTemplates(config)` lists those a config needs (both, when phone OTP is on).
|
|
125
|
+
- **Startup checks** (throw, naming the template, never content): `appId` present; every
|
|
126
|
+
`requiredTemplates` key configured; every template has a sender header (its own or the
|
|
127
|
+
default), a template ID and text.
|
|
128
|
+
- **Phone numbers:** messages carry E.164 (`+919876543210`); anything else is
|
|
129
|
+
`SMS_RECIPIENT_INVALID` and nothing is sent. The service passes numbers to Chatico unchanged,
|
|
130
|
+
so `formatRecipient` converts them: `e164DigitsRecipient` (default, `919876543210`, country
|
|
131
|
+
code first) or `indianTenDigitRecipient` (`9876543210` for `+91` numbers). The AWSM Chatico
|
|
132
|
+
account expects the default, country code first without `+` (confirmed by the platform
|
|
133
|
+
owner); keep `e164DigitsRecipient` unless that account changes.
|
|
134
|
+
- **DLT (India):** AWSM is the registered DLT entity. The app registers its sender header(s)
|
|
135
|
+
and content templates under AWSM's entity, then configures the header, template ID and exact
|
|
136
|
+
text here. The service does not check them.
|
|
137
|
+
- The service bills one unit per message part per recipient (GSM-7: 160 characters, then 153
|
|
138
|
+
per part; Unicode: 70, then 67), so keep texts short and in GSM-7 where possible.
|
|
139
|
+
|
|
140
|
+
## Errors
|
|
141
|
+
|
|
142
|
+
Nothing is swallowed: every failure is thrown as an `EmailDeliveryError` or `SmsDeliveryError`.
|
|
143
|
+
Their messages are caller-safe (no addresses, numbers, content, codes or credentials); the
|
|
144
|
+
service's error is kept as `cause` for logs. The service's errors cross RPC as plain `Error`s, so
|
|
145
|
+
they are mapped by message:
|
|
146
|
+
|
|
147
|
+
| Service error (message) | Email `code` | SMS `code` | `retryable` |
|
|
148
|
+
|---|---|---|---|
|
|
149
|
+
| `No app registered with app_id "…"` | `EMAIL_APP_NOT_REGISTERED` | `SMS_APP_NOT_REGISTERED` | no |
|
|
150
|
+
| `App "…" is suspended` | `EMAIL_APP_SUSPENDED` | `SMS_APP_SUSPENDED` | no |
|
|
151
|
+
| `App "…" has exceeded its monthly quota` | `EMAIL_QUOTA_EXCEEDED` | `SMS_QUOTA_EXCEEDED` | yes (after a reset) |
|
|
152
|
+
| `The Cloudflare send path does not support "…"` | `EMAIL_UNSUPPORTED_FIELD` | | no |
|
|
153
|
+
| message with `E_SENDER_NOT_VERIFIED` / `E_RECIPIENT_NOT_ALLOWED` | `EMAIL_SENDER_NOT_VERIFIED` / `EMAIL_RECIPIENT_NOT_ALLOWED` | | no |
|
|
154
|
+
| message with `E_RATE_LIMIT_EXCEEDED` / `E_DAILY_LIMIT_EXCEEDED` | `EMAIL_RATE_LIMITED` / `EMAIL_DAILY_LIMIT_EXCEEDED` | | yes |
|
|
155
|
+
| `InvalidSms…` (`sendSms needs at least one recipient…`) | | `SMS_INVALID` | no |
|
|
156
|
+
| `Chatico send failed (409): …` | | `SMS_TEMPLATE_MISMATCH` | no |
|
|
157
|
+
| `Chatico send failed (5xx / 402 / 408 / 413): …` | | `SMS_PROVIDER_UNAVAILABLE` | yes |
|
|
158
|
+
| other `Chatico send failed (…)` | | `SMS_DELIVERY_FAILED` | no |
|
|
159
|
+
| anything else | `EMAIL_DELIVERY_FAILED` | `SMS_DELIVERY_FAILED` | no |
|
|
160
|
+
|
|
161
|
+
Refused before the service is called: `EMAIL_RECIPIENT_PLACEHOLDER`, `EMAIL_SENDER_MISSING`,
|
|
162
|
+
`SMS_TEMPLATE_NOT_CONFIGURED`, `SMS_TEMPLATE_VARIABLE_MISSING`, `SMS_RECIPIENT_INVALID`.
|
|
163
|
+
|
|
164
|
+
`retryable` means a later attempt may succeed; it does not mean the caller should resend. The
|
|
165
|
+
service already retries Cloudflare and Chatico delivery failures on its own cron (Chatico has no
|
|
166
|
+
idempotency key, so resending may deliver twice).
|
|
167
|
+
|
|
168
|
+
## Log adapters (local development)
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const email = createLogEmailSender(); // prints to console.info
|
|
172
|
+
email.recent(); // LoggedEmail[]: { messageId, sentAt, to, from, replyTo, subject, text, html, links }
|
|
173
|
+
|
|
174
|
+
const sms = createLogSmsSender(); // prints to console.info
|
|
175
|
+
sms.recent(); // LoggedSms[]: { messageId, sentAt, to, text, templateId }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Each email is printed with its sender, recipient, reply-to, subject, the full text body and
|
|
179
|
+
every http(s) link from both parts; each SMS with its recipient, template key and text. Nothing
|
|
180
|
+
is discarded silently. Use them only where nobody expects real delivery: their output contains
|
|
181
|
+
live verification and reset links and sign-in codes. The playground serves `recent()` as local
|
|
182
|
+
outboxes (`GET /api/dev/outbox`, `GET /api/dev/sms-outbox`, on localhost only).
|
|
183
|
+
|
|
184
|
+
## Templates
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { authEmailTemplates } from "@awsmbiz/inbis-notifications";
|
|
188
|
+
|
|
189
|
+
createInbisAuth({ config, env, db, email, emailTemplates: authEmailTemplates, ... });
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`@awsmbiz/inbis-auth` renders its emails with the injected `AuthEmailTemplates` and the brand from its
|
|
193
|
+
config (`emails.brand`). Each email has a subject, a plain-text part and an HTML part:
|
|
194
|
+
|
|
195
|
+
- **Layout:** table-based with inline styles, so it works in most email clients. It shows the
|
|
196
|
+
brand name or logo, a heading, a greeting, an action button, the link again as text, a note on
|
|
197
|
+
how long the link works, and an optional footer.
|
|
198
|
+
- **Branding (`EmailBrand`):** `name`, `logoUrl` (shown only if it is https), `accentColor`
|
|
199
|
+
(used only if it matches `#rrggbb`; otherwise a neutral colour) and `footer`. Every value is
|
|
200
|
+
HTML-escaped, so branding and user names cannot inject markup or CSS.
|
|
201
|
+
- **Message keys:** every string comes from `emailMessages` (`auth.verifyEmail.*`,
|
|
202
|
+
`auth.resetPassword.*`, `auth.invitation.*`, `duration.*`, `email.*`), with `{appName}`,
|
|
203
|
+
`{name}`, `{minutes}`, `{inviter}` and `{duration}` placeholders.
|
|
204
|
+
- **Invitation email** (`invitation({ brand, recipientName, url, expiresInMinutes, inviterName, role })`):
|
|
205
|
+
"{inviter} invited you to {appName}", a note when the role is `admin`, and how long the link
|
|
206
|
+
works in days, hours or minutes. A blank inviter name gets a neutral wording.
|
|
207
|
+
To add a locale, pass a complete catalogue: `createAuthEmailTemplates(frenchMessages)`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { AppError, AuthEmailTemplates, EmailBrand, EmailSender, RenderedEmail, SmsSender } from "@awsmbiz/inbis-core";
|
|
2
|
+
/**
|
|
3
|
+
* Every string of the email templates, by message key. English only in the
|
|
4
|
+
* baseline; another locale is another catalogue passed to
|
|
5
|
+
* `createAuthEmailTemplates`. `{name}` placeholders are filled at render time.
|
|
6
|
+
*/
|
|
7
|
+
export declare const emailMessages: {
|
|
8
|
+
readonly "email.greeting": "Hi {name},";
|
|
9
|
+
readonly "email.greetingNoName": "Hi,";
|
|
10
|
+
readonly "email.linkFallback": "If the button does not work, copy this link into your browser:";
|
|
11
|
+
readonly "email.signOff": "— The {appName} team";
|
|
12
|
+
readonly "auth.verifyEmail.subject": "Verify your email address for {appName}";
|
|
13
|
+
readonly "auth.verifyEmail.heading": "Verify your email address";
|
|
14
|
+
readonly "auth.verifyEmail.intro": "Confirm that this is your email address to finish setting up your {appName} account.";
|
|
15
|
+
readonly "auth.verifyEmail.action": "Verify email address";
|
|
16
|
+
readonly "auth.verifyEmail.expiry": "This link works for {minutes} minutes.";
|
|
17
|
+
readonly "auth.verifyEmail.ignore": "If you did not create an account, you can ignore this email.";
|
|
18
|
+
readonly "auth.resetPassword.subject": "Reset your {appName} password";
|
|
19
|
+
readonly "auth.resetPassword.heading": "Reset your password";
|
|
20
|
+
readonly "auth.resetPassword.intro": "Someone asked to reset the password of your {appName} account.";
|
|
21
|
+
readonly "auth.resetPassword.action": "Reset password";
|
|
22
|
+
readonly "auth.resetPassword.expiry": "This link works for {minutes} minutes and can be used once.";
|
|
23
|
+
readonly "auth.resetPassword.ignore": "If you did not ask for this, ignore this email: your password stays the same.";
|
|
24
|
+
readonly "auth.invitation.subject": "{inviter} invited you to {appName}";
|
|
25
|
+
readonly "auth.invitation.subjectNoInviter": "You are invited to {appName}";
|
|
26
|
+
readonly "auth.invitation.heading": "You are invited to {appName}";
|
|
27
|
+
readonly "auth.invitation.intro": "{inviter} invited you to join {appName}.";
|
|
28
|
+
readonly "auth.invitation.introNoInviter": "You have been invited to join {appName}.";
|
|
29
|
+
readonly "auth.invitation.introAdmin": "You will join as an administrator.";
|
|
30
|
+
readonly "auth.invitation.action": "Accept invitation";
|
|
31
|
+
readonly "auth.invitation.expiry": "This invitation works for {duration} and can be used once.";
|
|
32
|
+
readonly "auth.invitation.ignore": "If you were not expecting this invitation, you can ignore this email.";
|
|
33
|
+
readonly "duration.minutes": "{count} minutes";
|
|
34
|
+
readonly "duration.hours": "{count} hours";
|
|
35
|
+
readonly "duration.days": "{count} days";
|
|
36
|
+
};
|
|
37
|
+
type EmailMessageKey = keyof typeof emailMessages;
|
|
38
|
+
/** A complete message catalogue (one locale). */
|
|
39
|
+
type EmailMessages = Record<EmailMessageKey, string>;
|
|
40
|
+
/** Auth email templates over a message catalogue (one locale). */
|
|
41
|
+
export declare function createAuthEmailTemplates(messages?: EmailMessages): AuthEmailTemplates;
|
|
42
|
+
/** The English auth email templates; pass to `createInbisAuth({ emailTemplates })`. */
|
|
43
|
+
export declare const authEmailTemplates: AuthEmailTemplates;
|
|
44
|
+
/** Content of a branded transactional email with one call to action. */
|
|
45
|
+
interface BrandedEmailContent {
|
|
46
|
+
brand: EmailBrand;
|
|
47
|
+
subject: string;
|
|
48
|
+
/** Greeting line, e.g. "Hi Ada,". */
|
|
49
|
+
greeting: string;
|
|
50
|
+
heading: string;
|
|
51
|
+
/** Paragraphs before the button. */
|
|
52
|
+
intro: readonly string[];
|
|
53
|
+
action: {
|
|
54
|
+
label: string;
|
|
55
|
+
url: string;
|
|
56
|
+
};
|
|
57
|
+
/** Line under the button explaining how to copy the link. */
|
|
58
|
+
linkFallback: string;
|
|
59
|
+
/** Paragraphs after the button (expiry, "ignore if not you"). */
|
|
60
|
+
outro: readonly string[];
|
|
61
|
+
signOff: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Renders a branded email as plain text and HTML. All text is escaped; the
|
|
65
|
+
* accent colour must be `#rrggbb` and the logo an https URL, otherwise they
|
|
66
|
+
* are left out. Table-based, inline-styled HTML for broad client support.
|
|
67
|
+
*/
|
|
68
|
+
export declare function renderBrandedEmail(content: BrandedEmailContent): RenderedEmail;
|
|
69
|
+
/**
|
|
70
|
+
* The RPC surface of AWSM's shared communication service
|
|
71
|
+
* (`biz-cloud-communication-service`, entrypoint `CommunicationService`) that
|
|
72
|
+
* Inbis apps use, typed structurally so nothing depends on the private
|
|
73
|
+
* service repo. Apps reach it over a service binding, conventionally named
|
|
74
|
+
* `COMMUNICATION_SERVICE`, and always pass their platform-set `APP_ID`.
|
|
75
|
+
*/
|
|
76
|
+
/** A mailbox on the SMTP path; the metered Cloudflare path takes plain strings only. */
|
|
77
|
+
interface CommunicationServiceMailbox {
|
|
78
|
+
name?: string;
|
|
79
|
+
email: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The subset of the service's email message (`@workermailer/smtp`
|
|
83
|
+
* `EmailOptions`) that Inbis sends: one recipient, optional reply-to.
|
|
84
|
+
*/
|
|
85
|
+
interface CommunicationServiceEmail {
|
|
86
|
+
from: string | CommunicationServiceMailbox;
|
|
87
|
+
to: string;
|
|
88
|
+
reply?: string;
|
|
89
|
+
subject: string;
|
|
90
|
+
text?: string;
|
|
91
|
+
html?: string;
|
|
92
|
+
}
|
|
93
|
+
/** Per-app SMTP credentials. Pass them from Worker secrets; they are never stored or logged. */
|
|
94
|
+
interface SmtpCredentials {
|
|
95
|
+
host: string;
|
|
96
|
+
port: number;
|
|
97
|
+
/** Implicit TLS (port 465). Omit for STARTTLS (port 587). */
|
|
98
|
+
secure?: boolean;
|
|
99
|
+
username: string;
|
|
100
|
+
password: string;
|
|
101
|
+
authType?: "plain" | "login" | "cram-md5" | ("plain" | "login" | "cram-md5")[];
|
|
102
|
+
}
|
|
103
|
+
/** One SMS to send through AWSM's Chatico account. */
|
|
104
|
+
interface CommunicationServiceSms {
|
|
105
|
+
/** DLT sender header, registered by the app. */
|
|
106
|
+
smsHeader: string;
|
|
107
|
+
/** DLT template ID for that header. */
|
|
108
|
+
templateId: string;
|
|
109
|
+
/** Numbers in the format the Chatico account expects. */
|
|
110
|
+
recipients: string[];
|
|
111
|
+
/** Must match the registered DLT template text. */
|
|
112
|
+
message: string;
|
|
113
|
+
}
|
|
114
|
+
interface CommunicationServiceSmsResult {
|
|
115
|
+
submissionId: string;
|
|
116
|
+
messageIds: string[];
|
|
117
|
+
smsCount: number;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The service binding as Inbis uses it. A `Service<CommunicationService>`
|
|
121
|
+
* stub satisfies it; a binding typed as a plain `Fetcher` (no service types)
|
|
122
|
+
* needs a cast to this interface.
|
|
123
|
+
*/
|
|
124
|
+
interface CommunicationServiceBinding {
|
|
125
|
+
sendEmail(appId: string, message: CommunicationServiceEmail, smtp?: SmtpCredentials): Promise<unknown>;
|
|
126
|
+
sendSms(appId: string, message: CommunicationServiceSms): Promise<CommunicationServiceSmsResult>;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Types a service binding as the communication service. `wrangler types`
|
|
130
|
+
* types a service binding without the service's source as a plain `Fetcher`;
|
|
131
|
+
* at runtime its RPC methods are callable all the same.
|
|
132
|
+
*/
|
|
133
|
+
export declare function communicationServiceBinding(binding: Fetcher | CommunicationServiceBinding): CommunicationServiceBinding;
|
|
134
|
+
interface CommunicationServiceEmailSenderOptions {
|
|
135
|
+
/** The `COMMUNICATION_SERVICE` service binding. */
|
|
136
|
+
binding: CommunicationServiceBinding;
|
|
137
|
+
/** The app's platform-set `APP_ID` (`env.APP_ID`). Never user input. */
|
|
138
|
+
appId: string;
|
|
139
|
+
/** Default sender, e.g. `Billing <no-reply@billing.example.com>`. A message's own `from` wins. */
|
|
140
|
+
from?: string;
|
|
141
|
+
/** Default reply-to address. A message's own `replyTo` wins. */
|
|
142
|
+
replyTo?: string;
|
|
143
|
+
/**
|
|
144
|
+
* The app's own SMTP server (from Worker secrets). When set, every email takes
|
|
145
|
+
* the unmetered SMTP path; otherwise AWSM's metered Cloudflare path.
|
|
146
|
+
*/
|
|
147
|
+
smtp?: SmtpCredentials;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* {@link EmailSender} over AWSM's shared communication service. Without `smtp`
|
|
151
|
+
* it uses the metered Cloudflare path, which takes only plain-string addresses:
|
|
152
|
+
* a display name in `from` is dropped there (kept on the SMTP path). Quota,
|
|
153
|
+
* suspension and retries belong to the service. Every failure is thrown as an
|
|
154
|
+
* {@link EmailDeliveryError}; nothing is swallowed or logged.
|
|
155
|
+
*/
|
|
156
|
+
export declare function createCommunicationServiceEmailSender(options: CommunicationServiceEmailSenderOptions): EmailSender;
|
|
157
|
+
/**
|
|
158
|
+
* One SMS type (e.g. `auth.phone-otp`) as registered on DLT. The rendered text
|
|
159
|
+
* must match the registered template exactly, or Chatico rejects it.
|
|
160
|
+
*/
|
|
161
|
+
interface SmsTemplateConfig {
|
|
162
|
+
/** DLT sender header for this type. Defaults to the sender's `smsHeader`. */
|
|
163
|
+
smsHeader?: string;
|
|
164
|
+
/** The DLT template ID. */
|
|
165
|
+
templateId: string;
|
|
166
|
+
/**
|
|
167
|
+
* The registered template text with `{name}` placeholders for the message's
|
|
168
|
+
* variables, e.g. `"{code} is your {appName} sign-in code. Valid for {minutes} minutes."`.
|
|
169
|
+
* Each placeholder fills one `{#var#}` slot of the DLT template.
|
|
170
|
+
*/
|
|
171
|
+
text: string;
|
|
172
|
+
}
|
|
173
|
+
interface CommunicationServiceSmsSenderOptions {
|
|
174
|
+
/** The `COMMUNICATION_SERVICE` service binding. */
|
|
175
|
+
binding: CommunicationServiceBinding;
|
|
176
|
+
/** The app's platform-set `APP_ID` (`env.APP_ID`). Never user input. */
|
|
177
|
+
appId: string;
|
|
178
|
+
/** Default DLT sender header for templates that do not set their own. */
|
|
179
|
+
smsHeader?: string;
|
|
180
|
+
/** Message template key (`SmsMessage.template.id`) to its DLT configuration. */
|
|
181
|
+
templates: Record<string, SmsTemplateConfig>;
|
|
182
|
+
/**
|
|
183
|
+
* Template keys that must be configured, checked at startup, e.g.
|
|
184
|
+
* `requiredAuthSmsTemplates(config)` from `@awsmbiz/inbis-auth`.
|
|
185
|
+
*/
|
|
186
|
+
requiredTemplates?: readonly string[];
|
|
187
|
+
/**
|
|
188
|
+
* Converts an E.164 number (`+919876543210`) to the format the service's
|
|
189
|
+
* Chatico account expects. Default: {@link e164DigitsRecipient} (`919876543210`).
|
|
190
|
+
*/
|
|
191
|
+
formatRecipient?: (e164: string) => string;
|
|
192
|
+
}
|
|
193
|
+
/** `+919876543210` → `919876543210` (country code first, no `+`). The default. */
|
|
194
|
+
export declare function e164DigitsRecipient(e164: string): string;
|
|
195
|
+
/** `+919876543210` → `9876543210` (Indian numbers without the country code); other numbers as {@link e164DigitsRecipient}. */
|
|
196
|
+
export declare function indianTenDigitRecipient(e164: string): string;
|
|
197
|
+
/**
|
|
198
|
+
* {@link SmsSender} over AWSM's shared communication service (Chatico, AWSM as
|
|
199
|
+
* the DLT entity). The message's `template.id` picks the app's DLT
|
|
200
|
+
* configuration; the text sent is that template's text with the message's
|
|
201
|
+
* variables filled in (the message's own `text` is not sent). Quota,
|
|
202
|
+
* suspension and retries belong to the service. Every failure is thrown as an
|
|
203
|
+
* {@link SmsDeliveryError}; nothing is swallowed or logged.
|
|
204
|
+
*/
|
|
205
|
+
export declare function createCommunicationServiceSmsSender(options: CommunicationServiceSmsSenderOptions): SmsSender;
|
|
206
|
+
/** Stable codes of {@link EmailDeliveryError}. */
|
|
207
|
+
type EmailDeliveryErrorCode =
|
|
208
|
+
/** The communication service has no app registered under this `APP_ID`. */
|
|
209
|
+
"EMAIL_APP_NOT_REGISTERED" |
|
|
210
|
+
/** The app's email channel is suspended in the communication service. */
|
|
211
|
+
"EMAIL_APP_SUSPENDED" |
|
|
212
|
+
/** The app has used its monthly email quota; sending works again once it is reset or raised. */
|
|
213
|
+
"EMAIL_QUOTA_EXCEEDED" |
|
|
214
|
+
/** The metered (Cloudflare) path cannot send this message; it needs the SMTP path. */
|
|
215
|
+
"EMAIL_UNSUPPORTED_FIELD" |
|
|
216
|
+
/** The sending address/domain is not onboarded or verified (`E_SENDER_NOT_VERIFIED`). */
|
|
217
|
+
"EMAIL_SENDER_NOT_VERIFIED" |
|
|
218
|
+
/** The recipient is not allowed (`E_RECIPIENT_NOT_ALLOWED`). */
|
|
219
|
+
"EMAIL_RECIPIENT_NOT_ALLOWED" |
|
|
220
|
+
/** Short-term rate limit hit (`E_RATE_LIMIT_EXCEEDED`); the service retries it. */
|
|
221
|
+
"EMAIL_RATE_LIMITED" |
|
|
222
|
+
/** The sending account's daily quota is used up (`E_DAILY_LIMIT_EXCEEDED`); the service retries it. */
|
|
223
|
+
"EMAIL_DAILY_LIMIT_EXCEEDED" |
|
|
224
|
+
/** No `from` address on the message or the sender. */
|
|
225
|
+
"EMAIL_SENDER_MISSING" |
|
|
226
|
+
/** The recipient is a phone-only user's placeholder (`.invalid`) address; nothing is sent. */
|
|
227
|
+
"EMAIL_RECIPIENT_PLACEHOLDER" |
|
|
228
|
+
/** Anything else, including unknown service or provider errors. */
|
|
229
|
+
"EMAIL_DELIVERY_FAILED";
|
|
230
|
+
interface EmailDeliveryErrorOptions {
|
|
231
|
+
code: EmailDeliveryErrorCode;
|
|
232
|
+
/** The provider's own code (e.g. `E_RATE_LIMIT_EXCEEDED`), when the service error carried one. */
|
|
233
|
+
providerCode?: string;
|
|
234
|
+
cause?: unknown;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* An email that was not accepted for delivery. Never swallowed: adapters throw it.
|
|
238
|
+
* The message is caller-safe (no addresses or content); the service's error
|
|
239
|
+
* is kept as `cause` for logs.
|
|
240
|
+
*/
|
|
241
|
+
export declare class EmailDeliveryError extends AppError {
|
|
242
|
+
readonly code: EmailDeliveryErrorCode;
|
|
243
|
+
readonly providerCode: string | undefined;
|
|
244
|
+
/** True when sending the same message later may succeed (rate, daily or monthly limits). */
|
|
245
|
+
readonly retryable: boolean;
|
|
246
|
+
constructor(options: EmailDeliveryErrorOptions);
|
|
247
|
+
}
|
|
248
|
+
/** Stable codes of {@link SmsDeliveryError}. */
|
|
249
|
+
type SmsDeliveryErrorCode =
|
|
250
|
+
/** The message has no template, or its template key is not configured. */
|
|
251
|
+
"SMS_TEMPLATE_NOT_CONFIGURED" |
|
|
252
|
+
/** The template text needs a variable the message does not carry. */
|
|
253
|
+
"SMS_TEMPLATE_VARIABLE_MISSING" |
|
|
254
|
+
/** The recipient is not an E.164 number. */
|
|
255
|
+
"SMS_RECIPIENT_INVALID" |
|
|
256
|
+
/** The communication service has no app registered under this `APP_ID`. */
|
|
257
|
+
"SMS_APP_NOT_REGISTERED" |
|
|
258
|
+
/** The app's SMS channel is suspended in the communication service. */
|
|
259
|
+
"SMS_APP_SUSPENDED" |
|
|
260
|
+
/** The app has used its monthly SMS quota (a quota of 0 means SMS is off). */
|
|
261
|
+
"SMS_QUOTA_EXCEEDED" |
|
|
262
|
+
/** The service refused the message as empty or without recipients (`InvalidSmsError`). */
|
|
263
|
+
"SMS_INVALID" |
|
|
264
|
+
/** Chatico rejected the text as not matching the DLT template (HTTP 409). */
|
|
265
|
+
"SMS_TEMPLATE_MISMATCH" |
|
|
266
|
+
/** Chatico failed in a way the service retries on its own (5xx, 408, 402, 413). */
|
|
267
|
+
"SMS_PROVIDER_UNAVAILABLE" |
|
|
268
|
+
/** Anything else: another Chatico rejection, or an unknown service error. */
|
|
269
|
+
"SMS_DELIVERY_FAILED";
|
|
270
|
+
interface SmsDeliveryErrorOptions {
|
|
271
|
+
code: SmsDeliveryErrorCode;
|
|
272
|
+
/** Chatico's HTTP status, when the service relayed one. */
|
|
273
|
+
providerStatus?: number;
|
|
274
|
+
cause?: unknown;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* An SMS that was not accepted for delivery, or that could not be sent as configured.
|
|
278
|
+
* Never swallowed: adapters throw it. The message is caller-safe (no numbers or
|
|
279
|
+
* content); the service's error is kept as `cause` for logs.
|
|
280
|
+
*/
|
|
281
|
+
export declare class SmsDeliveryError extends AppError {
|
|
282
|
+
readonly code: SmsDeliveryErrorCode;
|
|
283
|
+
readonly providerStatus: number | undefined;
|
|
284
|
+
/** True when sending the same message later may succeed. */
|
|
285
|
+
readonly retryable: boolean;
|
|
286
|
+
constructor(options: SmsDeliveryErrorOptions);
|
|
287
|
+
}
|
|
288
|
+
/** An email as the log adapter recorded it. */
|
|
289
|
+
interface LoggedEmail {
|
|
290
|
+
messageId: string;
|
|
291
|
+
/** ISO timestamp. */
|
|
292
|
+
sentAt: string;
|
|
293
|
+
to: string;
|
|
294
|
+
from: string | undefined;
|
|
295
|
+
replyTo: string | undefined;
|
|
296
|
+
subject: string;
|
|
297
|
+
text: string;
|
|
298
|
+
html: string | undefined;
|
|
299
|
+
/** Every http(s) link in the text and HTML parts, in order, each once. */
|
|
300
|
+
links: string[];
|
|
301
|
+
}
|
|
302
|
+
interface LogEmailSenderOptions {
|
|
303
|
+
/** Where the rendered email goes. Default `console.info`. */
|
|
304
|
+
print?: (text: string) => void;
|
|
305
|
+
/** Default sender address, as a real adapter would use. */
|
|
306
|
+
from?: string;
|
|
307
|
+
/** How many recent emails {@link LogEmailSender.recent} keeps. Default 50. */
|
|
308
|
+
keep?: number;
|
|
309
|
+
}
|
|
310
|
+
interface LogEmailSender extends EmailSender {
|
|
311
|
+
/** The most recent emails, newest last (for a local outbox viewer). */
|
|
312
|
+
recent(): readonly LoggedEmail[];
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Development {@link EmailSender} that prints every email in full (addresses,
|
|
316
|
+
* subject, text body and links) instead of delivering it, and keeps the recent
|
|
317
|
+
* ones for an outbox viewer. Nothing is silently discarded.
|
|
318
|
+
*
|
|
319
|
+
* Never use it where real users expect email: it delivers nothing, and its
|
|
320
|
+
* output (and outbox) contains live verification and reset links.
|
|
321
|
+
*/
|
|
322
|
+
export declare function createLogEmailSender(options?: LogEmailSenderOptions): LogEmailSender;
|
|
323
|
+
/** An SMS as the log adapter recorded it. */
|
|
324
|
+
interface LoggedSms {
|
|
325
|
+
messageId: string;
|
|
326
|
+
/** ISO timestamp. */
|
|
327
|
+
sentAt: string;
|
|
328
|
+
/** E.164 recipient. */
|
|
329
|
+
to: string;
|
|
330
|
+
text: string;
|
|
331
|
+
/** The message's template key (e.g. `auth.phone-otp`), when it has one. */
|
|
332
|
+
templateId: string | undefined;
|
|
333
|
+
}
|
|
334
|
+
interface LogSmsSenderOptions {
|
|
335
|
+
/** Where the message goes. Default `console.info`. */
|
|
336
|
+
print?: (text: string) => void;
|
|
337
|
+
/** How many recent messages {@link LogSmsSender.recent} keeps. Default 50. */
|
|
338
|
+
keep?: number;
|
|
339
|
+
}
|
|
340
|
+
interface LogSmsSender extends SmsSender {
|
|
341
|
+
/** The most recent messages, newest last (for a local outbox viewer). */
|
|
342
|
+
recent(): readonly LoggedSms[];
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Development {@link SmsSender} that prints every message in full instead of
|
|
346
|
+
* delivering it, and keeps the recent ones for an outbox viewer. Nothing is
|
|
347
|
+
* silently discarded.
|
|
348
|
+
*
|
|
349
|
+
* Never use it where real users expect SMS: it delivers nothing, and its
|
|
350
|
+
* output (and outbox) contains live sign-in codes.
|
|
351
|
+
*/
|
|
352
|
+
export declare function createLogSmsSender(options?: LogSmsSenderOptions): LogSmsSender;
|
|
353
|
+
export type { BrandedEmailContent, CommunicationServiceBinding, CommunicationServiceEmail, CommunicationServiceEmailSenderOptions, CommunicationServiceMailbox, CommunicationServiceSms, CommunicationServiceSmsResult, CommunicationServiceSmsSenderOptions, EmailDeliveryErrorCode, EmailDeliveryErrorOptions, EmailMessageKey, EmailMessages, LogEmailSender, LogEmailSenderOptions, LogSmsSender, LogSmsSenderOptions, LoggedEmail, LoggedSms, SmsDeliveryErrorCode, SmsDeliveryErrorOptions, SmsTemplateConfig, SmtpCredentials };
|
|
354
|
+
//# sourceMappingURL=index.d.ts.map
|