@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 CHANGED
@@ -1,3 +1,207 @@
1
- # Temporary Holding Version
1
+ # @awsmbiz/inbis-notifications
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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)`.
@@ -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