@airhornjs/azure 5.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Jared Wray
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,254 @@
1
+ ![Airhorn](https://airhorn.org/logo.svg "Airhorn")
2
+
3
+ ---
4
+
5
+ [![tests](https://github.com/jaredwray/airhorn/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/airhorn/actions/workflows/tests.yml)
6
+ [![codecov](https://codecov.io/gh/jaredwray/airhorn/branch/main/graph/badge.svg?token=4OJEEB67Q5)](https://codecov.io/gh/jaredwray/airhorn)
7
+ [![license](https://img.shields.io/github/license/jaredwray/airhorn)](https://github.com/jaredwray/airhorn/blob/master/LICENSE)
8
+ [![npm](https://img.shields.io/npm/dm/@airhorn/azure)](https://npmjs.com/package/@airhorn/azure)
9
+ [![npm](https://img.shields.io/npm/v/@airhorn/azure)](https://npmjs.com/package/@airhorn/azure)
10
+
11
+ # @airhorn/azure
12
+
13
+ Azure Communication Services and Notification Hubs provider for Airhorn.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install airhorn @airhorn/azure
19
+ ```
20
+
21
+ ## Features
22
+
23
+ - SMS sending via Azure Communication Services
24
+ - Email sending via Azure Communication Services
25
+ - Mobile push notifications to iOS and Android devices via Azure Notification Hubs
26
+ - Automatic error handling and retry support
27
+ - Integration with Airhorn's notification system
28
+ - Support for multiple connection strings
29
+
30
+ ## Usage
31
+
32
+ ### SMS with Azure Communication Services
33
+
34
+ ```typescript
35
+ import { Airhorn } from 'airhorn';
36
+ import { AirhornAzure } from '@airhorn/azure';
37
+
38
+ // Create Azure provider for SMS
39
+ const azureProvider = new AirhornAzure({
40
+ connectionString: 'endpoint=https://your-service.communication.azure.com/;accesskey=your-key',
41
+ });
42
+
43
+ // Create Airhorn instance with Azure provider
44
+ const airhorn = new Airhorn({
45
+ providers: [azureProvider],
46
+ });
47
+
48
+ const data = {
49
+ orderId: '12345',
50
+ customerName: 'John',
51
+ };
52
+
53
+ // Send SMS
54
+ const template = {
55
+ from: '+1234567890', // Your sender phone number (must be provisioned in Azure)
56
+ content: 'Hello <%= customerName %>!, your order #<%= orderId %> has been shipped!',
57
+ type: AirhornSendType.SMS,
58
+ };
59
+
60
+ const result = await airhorn.send(
61
+ '+0987654321', // to
62
+ template,
63
+ data,
64
+ AirhornSendType.SMS
65
+ );
66
+ ```
67
+
68
+ ### Email with Azure Communication Services
69
+
70
+ ```typescript
71
+ import { Airhorn } from 'airhorn';
72
+ import { AirhornAzure } from '@airhorn/azure';
73
+
74
+ // Create Azure provider with email support
75
+ const azureProvider = new AirhornAzure({
76
+ connectionString: 'endpoint=https://your-service.communication.azure.com/;accesskey=your-key',
77
+ });
78
+
79
+ // Create Airhorn instance
80
+ const airhorn = new Airhorn({
81
+ providers: [azureProvider],
82
+ });
83
+
84
+ const data = {
85
+ orderId: '656565',
86
+ customerName: 'John',
87
+ };
88
+
89
+ // Send Email
90
+ const template = {
91
+ from: 'DoNotReply@yourdomain.com', // Must be verified domain in Azure
92
+ subject: 'Order Confirmation: <%= orderId %>',
93
+ content: 'Hi <%= customerName %>, your order #<%= orderId %> has been confirmed!',
94
+ };
95
+
96
+ const result = await airhorn.send(
97
+ 'recipient@example.com', // to
98
+ template,
99
+ data,
100
+ AirhornSendType.Email
101
+ );
102
+ ```
103
+
104
+ ### Mobile Push Notifications with Azure Notification Hubs
105
+
106
+ ```typescript
107
+ import { Airhorn } from 'airhorn';
108
+ import { AirhornAzure } from '@airhorn/azure';
109
+
110
+ const azureProvider = new AirhornAzure({
111
+ notificationHubConnectionString: 'Endpoint=sb://your-namespace.servicebus.windows.net/;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=your-key',
112
+ notificationHubName: 'your-hub-name',
113
+ });
114
+
115
+ const airhorn = new Airhorn({
116
+ providers: [azureProvider],
117
+ });
118
+
119
+ const data = {
120
+ orderId: '12345',
121
+ customerName: 'John',
122
+ };
123
+
124
+ // Send to iOS device via APNs
125
+ const template = {
126
+ from: 'YourApp',
127
+ content: JSON.stringify({
128
+ aps: {
129
+ alert: {
130
+ title: 'New Order',
131
+ body: 'Hi <%= customerName %>You have a new order #<%= orderId %>',
132
+ },
133
+ badge: 1,
134
+ sound: 'default',
135
+ },
136
+ // Custom data
137
+ orderId: '12345',
138
+ }),
139
+ };
140
+
141
+ // Send using tag expression
142
+ await airhorn.send(
143
+ 'user:john-doe', // Tag expression
144
+ template,
145
+ data,
146
+ AirhornSendType.MobilePush,
147
+ {
148
+ platform: 'apple',
149
+ tags: 'user:john-doe && ios',
150
+ },
151
+ );
152
+ ```
153
+
154
+ ### Custom Capabilities
155
+
156
+ You can specify which services to enable using the `capabilities` option:
157
+
158
+ ```typescript
159
+ // SMS only
160
+ const smsProvider = new AirhornAzure({
161
+ connectionString: 'your-connection-string',
162
+ capabilities: [AirhornSendType.SMS],
163
+ });
164
+
165
+ // Email only
166
+ const emailProvider = new AirhornAzure({
167
+ connectionString: 'your-connection-string',
168
+ capabilities: [AirhornSendType.Email],
169
+ });
170
+
171
+ // Mobile Push only
172
+ const pushProvider = new AirhornAzure({
173
+ notificationHubConnectionString: 'your-hub-connection-string',
174
+ notificationHubName: 'your-hub-name',
175
+ capabilities: [AirhornSendType.MobilePush],
176
+ });
177
+
178
+ // All capabilities (explicit)
179
+ const allProvider = new AirhornAzure({
180
+ connectionString: 'your-communication-services-connection-string',
181
+ notificationHubConnectionString: 'your-hub-connection-string',
182
+ notificationHubName: 'your-hub-name',
183
+ capabilities: [AirhornSendType.SMS, AirhornSendType.Email, AirhornSendType.MobilePush],
184
+ });
185
+ ```
186
+
187
+ ## Configuration
188
+
189
+ ### AirhornAzureOptions
190
+
191
+ - `connectionString` (optional): Azure Communication Services connection string (used for both SMS and Email if specific strings not provided)
192
+ - `emailConnectionString` (optional): Specific connection string for Email service
193
+ - `smsConnectionString` (optional): Specific connection string for SMS service
194
+ - `notificationHubConnectionString` (optional): Azure Notification Hub connection string
195
+ - `notificationHubName` (optional): Azure Notification Hub name
196
+ - `capabilities` (optional): Array of `AirhornSendType` values to specify which services to enable (defaults to SMS, MobilePush, and Email)
197
+
198
+ ## Additional Options
199
+
200
+ ### SMS Options
201
+
202
+ ```typescript
203
+ await airhorn.send(to, message, {
204
+ // Additional SMS options from Azure Communication Services
205
+ deliveryReportTimeoutInSeconds: 300,
206
+ tag: 'custom-tag',
207
+ });
208
+ ```
209
+
210
+ ## Prerequisites
211
+
212
+ ### Azure Communication Services
213
+
214
+ 1. Create an Azure Communication Services resource in Azure Portal
215
+ 2. Get your connection string from the resource
216
+ 3. For SMS: Provision a phone number through the Azure Portal
217
+ 4. For Email: Verify your sending domain
218
+
219
+ ### Azure Notification Hubs
220
+
221
+ 1. Create a Notification Hub namespace and hub in Azure Portal
222
+ 2. Configure platform credentials (APNs for iOS, FCM for Android)
223
+ 3. Get your connection string and hub name
224
+ 4. Implement device registration in your mobile apps
225
+
226
+ ## Azure RBAC Permissions
227
+
228
+ Minimum required permissions for the service principal or managed identity:
229
+
230
+ ### For Communication Services:
231
+ - `Azure Communication Services Contributor` role
232
+ - Or specific permissions:
233
+ - `Microsoft.Communication/CommunicationServices/read`
234
+ - `Microsoft.Communication/CommunicationServices/write`
235
+
236
+ ### For Notification Hubs:
237
+ - `Azure Notification Hubs Contributor` role
238
+ - Or specific permissions:
239
+ - `Microsoft.NotificationHubs/Namespaces/NotificationHubs/read`
240
+ - `Microsoft.NotificationHubs/Namespaces/NotificationHubs/write`
241
+
242
+ ## Testing
243
+
244
+ ```bash
245
+ pnpm test
246
+ ```
247
+
248
+ # How to Contribute
249
+
250
+ Now that you've set up your workspace, you're ready to contribute changes to the `airhorn` repository you can refer to the [CONTRIBUTING](../../CONTRIBUTING.md) guide. If you have any questions please feel free to ask by creating an issue and label it `question`.
251
+
252
+ # Licensing and Copyright
253
+
254
+ This project is [MIT License © Jared Wray](LICENSE)
@@ -0,0 +1,24 @@
1
+ import { AirhornSendType, AirhornProvider, AirhornProviderMessage, AirhornProviderSendResult } from 'airhorn';
2
+
3
+ type AirhornAzureOptions = {
4
+ connectionString?: string;
5
+ emailConnectionString?: string;
6
+ smsConnectionString?: string;
7
+ notificationHubConnectionString?: string;
8
+ notificationHubName?: string;
9
+ capabilities?: AirhornSendType[];
10
+ };
11
+ declare class AirhornAzure implements AirhornProvider {
12
+ name: string;
13
+ capabilities: AirhornSendType[];
14
+ private emailClient?;
15
+ private smsClient?;
16
+ private notificationHubClient?;
17
+ constructor(options: AirhornAzureOptions);
18
+ send(message: AirhornProviderMessage, options?: any): Promise<AirhornProviderSendResult>;
19
+ private sendSMS;
20
+ private sendEmail;
21
+ private sendMobilePush;
22
+ }
23
+
24
+ export { AirhornAzure, type AirhornAzureOptions };
package/dist/index.js ADDED
@@ -0,0 +1,253 @@
1
+ // src/index.ts
2
+ import {
3
+ EmailClient
4
+ } from "@azure/communication-email";
5
+ import { SmsClient } from "@azure/communication-sms";
6
+ import {
7
+ createAppleNotification,
8
+ createFcmLegacyNotification,
9
+ NotificationHubsClient
10
+ } from "@azure/notification-hubs";
11
+ import {
12
+ AirhornSendType
13
+ } from "airhorn";
14
+ var AirhornAzure = class {
15
+ name = "azure";
16
+ capabilities;
17
+ emailClient;
18
+ smsClient;
19
+ notificationHubClient;
20
+ constructor(options) {
21
+ this.capabilities = options.capabilities || [
22
+ AirhornSendType.SMS,
23
+ AirhornSendType.MobilePush,
24
+ AirhornSendType.Email
25
+ ];
26
+ if (this.capabilities.includes(AirhornSendType.Email)) {
27
+ const emailConnStr = options.emailConnectionString || options.connectionString;
28
+ if (emailConnStr) {
29
+ this.emailClient = new EmailClient(emailConnStr);
30
+ }
31
+ }
32
+ if (this.capabilities.includes(AirhornSendType.SMS)) {
33
+ const smsConnStr = options.smsConnectionString || options.connectionString;
34
+ if (smsConnStr) {
35
+ this.smsClient = new SmsClient(smsConnStr);
36
+ }
37
+ }
38
+ if (this.capabilities.includes(AirhornSendType.MobilePush)) {
39
+ if (options.notificationHubConnectionString && options.notificationHubName) {
40
+ this.notificationHubClient = new NotificationHubsClient(
41
+ options.notificationHubConnectionString,
42
+ options.notificationHubName
43
+ );
44
+ }
45
+ }
46
+ }
47
+ async send(message, options) {
48
+ const result = {
49
+ success: false,
50
+ response: null,
51
+ errors: []
52
+ };
53
+ try {
54
+ if (message.type === AirhornSendType.SMS) {
55
+ return this.sendSMS(message, options);
56
+ }
57
+ if (message.type === AirhornSendType.Email) {
58
+ return this.sendEmail(message, options);
59
+ }
60
+ if (message.type === AirhornSendType.MobilePush) {
61
+ return this.sendMobilePush(message, options);
62
+ }
63
+ throw new Error(
64
+ `AirhornAzure does not support message type: ${message.type}`
65
+ );
66
+ } catch (error) {
67
+ const err = error instanceof Error ? error : new Error(String(error));
68
+ result.errors.push(err);
69
+ result.response = {
70
+ error: err.message,
71
+ details: error
72
+ };
73
+ }
74
+ return result;
75
+ }
76
+ async sendSMS(message, options) {
77
+ const result = {
78
+ success: false,
79
+ response: null,
80
+ errors: []
81
+ };
82
+ try {
83
+ if (!this.smsClient) {
84
+ throw new Error("SMS client is not configured");
85
+ }
86
+ if (!message.from) {
87
+ throw new Error("From phone number is required for SMS messages");
88
+ }
89
+ const sendResult = await this.smsClient.send({
90
+ from: message.from,
91
+ to: [message.to],
92
+ message: message.content,
93
+ ...options
94
+ });
95
+ const successfulMessages = sendResult.filter(
96
+ // biome-ignore lint/suspicious/noExplicitAny: Azure SDK type
97
+ (msg) => msg.successful
98
+ );
99
+ const failedMessages = sendResult.filter(
100
+ // biome-ignore lint/suspicious/noExplicitAny: Azure SDK type
101
+ (msg) => !msg.successful
102
+ );
103
+ if (failedMessages.length > 0) {
104
+ for (const failed of failedMessages) {
105
+ result.errors.push(
106
+ new Error(
107
+ `Failed to send SMS to ${failed.to}: ${failed.errorMessage}`
108
+ )
109
+ );
110
+ }
111
+ }
112
+ result.success = successfulMessages.length > 0;
113
+ result.response = {
114
+ successful: successfulMessages.length,
115
+ failed: failedMessages.length,
116
+ results: sendResult
117
+ };
118
+ } catch (error) {
119
+ const err = error instanceof Error ? error : new Error(String(error));
120
+ result.errors.push(err);
121
+ result.response = {
122
+ error: err.message,
123
+ details: error
124
+ };
125
+ }
126
+ return result;
127
+ }
128
+ async sendEmail(message, options) {
129
+ const result = {
130
+ success: false,
131
+ response: null,
132
+ errors: []
133
+ };
134
+ try {
135
+ if (!this.emailClient) {
136
+ throw new Error("Email client is not configured");
137
+ }
138
+ if (!message.from) {
139
+ throw new Error("From email address is required for email messages");
140
+ }
141
+ const emailMessage = {
142
+ senderAddress: message.from,
143
+ recipients: {
144
+ to: [{ address: message.to }],
145
+ cc: options?.cc?.map((email) => ({ address: email })),
146
+ bcc: options?.bcc?.map((email) => ({ address: email }))
147
+ },
148
+ content: {
149
+ subject: message.subject || "Notification",
150
+ plainText: message.content,
151
+ html: options?.html || message.content
152
+ },
153
+ attachments: options?.attachments,
154
+ replyTo: options?.replyTo ? [{ address: options.replyTo }] : void 0,
155
+ headers: options?.headers
156
+ };
157
+ const poller = await this.emailClient.beginSend(emailMessage);
158
+ const sendResult = await poller.pollUntilDone();
159
+ result.success = sendResult.status === "Succeeded";
160
+ result.response = {
161
+ id: sendResult.id,
162
+ status: sendResult.status,
163
+ error: sendResult.error
164
+ };
165
+ if (sendResult.error) {
166
+ result.errors.push(
167
+ new Error(`Email send failed: ${sendResult.error.message}`)
168
+ );
169
+ }
170
+ } catch (error) {
171
+ const err = error instanceof Error ? error : new Error(String(error));
172
+ result.errors.push(err);
173
+ result.response = {
174
+ error: err.message,
175
+ details: error
176
+ };
177
+ }
178
+ return result;
179
+ }
180
+ async sendMobilePush(message, options) {
181
+ const result = {
182
+ success: false,
183
+ response: null,
184
+ errors: []
185
+ };
186
+ try {
187
+ if (!this.notificationHubClient) {
188
+ throw new Error("Notification Hub client is not configured");
189
+ }
190
+ if (!message.from) {
191
+ throw new Error("From identifier is required for mobile push messages");
192
+ }
193
+ let notification;
194
+ const sendOptions = {
195
+ tagExpression: options?.tags
196
+ };
197
+ if (options?.platform === "apple" || message.to.includes("apple")) {
198
+ const apnsPayload = typeof message.content === "string" ? JSON.parse(message.content) : message.content;
199
+ notification = createAppleNotification({
200
+ body: JSON.stringify(apnsPayload),
201
+ headers: options?.apnsHeaders
202
+ });
203
+ } else if (options?.platform === "android" || message.to.includes("android") || message.to.includes("fcm")) {
204
+ const fcmPayload = typeof message.content === "string" ? JSON.parse(message.content) : message.content;
205
+ notification = createFcmLegacyNotification({
206
+ body: JSON.stringify(fcmPayload)
207
+ });
208
+ } else {
209
+ sendOptions.tagExpression = message.to;
210
+ const payload = typeof message.content === "string" ? JSON.parse(message.content) : message.content;
211
+ if (payload.aps) {
212
+ notification = createAppleNotification({
213
+ body: JSON.stringify(payload)
214
+ });
215
+ } else if (payload.notification || payload.data) {
216
+ notification = createFcmLegacyNotification({
217
+ body: JSON.stringify(payload)
218
+ });
219
+ } else {
220
+ throw new Error("Invalid notification payload structure");
221
+ }
222
+ }
223
+ const notificationResult = await this.notificationHubClient.sendNotification(
224
+ notification,
225
+ sendOptions
226
+ );
227
+ result.success = notificationResult.state === "Enqueued";
228
+ result.response = {
229
+ notificationId: notificationResult.notificationId,
230
+ state: notificationResult.state,
231
+ correlationId: notificationResult.correlationId
232
+ };
233
+ if (notificationResult.state !== "Enqueued") {
234
+ result.errors.push(
235
+ new Error(
236
+ `Notification failed with state: ${notificationResult.state}`
237
+ )
238
+ );
239
+ }
240
+ } catch (error) {
241
+ const err = error instanceof Error ? error : new Error(String(error));
242
+ result.errors.push(err);
243
+ result.response = {
244
+ error: err.message,
245
+ details: error
246
+ };
247
+ }
248
+ return result;
249
+ }
250
+ };
251
+ export {
252
+ AirhornAzure
253
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@airhornjs/azure",
3
+ "version": "5.0.1",
4
+ "description": "Azure provider for Airhorn",
5
+ "license": "MIT",
6
+ "author": "Jared Wray <me@jaredwray.com>",
7
+ "homepage": "https://github.com/jaredwray/airhorn/tree/main/packages/azure#readme",
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "dependencies": {
18
+ "@azure/communication-email": "^1.0.0",
19
+ "@azure/communication-sms": "^1.1.0",
20
+ "@azure/notification-hubs": "^2.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@biomejs/biome": "^2.2.2",
24
+ "@types/node": "^24.3.0",
25
+ "@vitest/coverage-v8": "^3.2.4",
26
+ "rimraf": "^6.0.1",
27
+ "tsup": "^8.5.0",
28
+ "typescript": "^5.9.2",
29
+ "vite": "^7.1.3",
30
+ "vitest": "^3.2.4"
31
+ },
32
+ "peerDependencies": {
33
+ "airhorn": "5.0.1"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/jaredwray/airhorn.git",
38
+ "directory": "packages/azure"
39
+ },
40
+ "keywords": [
41
+ "airhorn",
42
+ "notifications",
43
+ "azure",
44
+ "communication-services",
45
+ "notification-hubs",
46
+ "sms",
47
+ "email",
48
+ "push",
49
+ "mobile"
50
+ ],
51
+ "files": [
52
+ "dist",
53
+ "LICENSE"
54
+ ],
55
+ "scripts": {
56
+ "lint": "biome check --write --error-on-warnings",
57
+ "test": "pnpm lint && vitest run --coverage",
58
+ "test:ci": "biome check --error-on-warnings && vitest run --coverage",
59
+ "clean": "rimraf ./dist ./coverage ./node_modules ./package-lock.json ./pnpm-lock.yaml",
60
+ "build:publish": "pnpm build && pnpm publish --access public",
61
+ "build": "rimraf ./dist && tsup src/index.ts --format esm --dts --clean"
62
+ }
63
+ }