@mantajs/adapter-notification-resend 0.2.0-beta.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 +53 -0
- package/dist/adapter.d.ts +23 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +87 -0
- package/dist/adapter.js.map +1 -0
- package/dist/adapter.test.d.ts +2 -0
- package/dist/adapter.test.d.ts.map +1 -0
- package/dist/adapter.test.js +149 -0
- package/dist/adapter.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @mantajs/adapter-notification-resend
|
|
2
|
+
|
|
3
|
+
Resend adapter for Manta's `INotificationPort`. Sends transactional email through [Resend](https://resend.com/).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @mantajs/adapter-notification-resend
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
In `manta.config.ts`:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { ResendNotificationAdapter } from '@mantajs/adapter-notification-resend'
|
|
17
|
+
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
adapters: {
|
|
20
|
+
notification: () =>
|
|
21
|
+
new ResendNotificationAdapter({
|
|
22
|
+
apiKey: process.env.RESEND_API_KEY,
|
|
23
|
+
defaultFrom: process.env.RESEND_FROM_EMAIL, // "Brand <noreply@domain.com>"
|
|
24
|
+
defaultReplyTo: process.env.RESEND_REPLY_TO,
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then from any command/step:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
await ctx.app.notification.send({
|
|
34
|
+
channel: 'email',
|
|
35
|
+
to: 'customer@example.com',
|
|
36
|
+
subject: 'Your order is on its way',
|
|
37
|
+
html: renderedHtml,
|
|
38
|
+
text: renderedText,
|
|
39
|
+
tags: [{ name: 'category', value: 'shipping' }],
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Behavior
|
|
44
|
+
|
|
45
|
+
- **Email-only.** Sending other channels throws `MantaError('INVALID_DATA')`.
|
|
46
|
+
- **Validation.** Missing `subject`, body (`html` or `text`), or `from` → `INVALID_DATA`.
|
|
47
|
+
- **Resend 4xx errors** (validation/auth/quota) → returns `{ status: 'FAILURE', error }`.
|
|
48
|
+
- **Network/transport errors** → throws `MantaError('UNEXPECTED_STATE')` so the workflow runner can retry.
|
|
49
|
+
|
|
50
|
+
## See also
|
|
51
|
+
|
|
52
|
+
- [Resend SDK docs](https://resend.com/docs/send-with-nodejs)
|
|
53
|
+
- `INotificationPort` interface in `@mantajs/core/ports/notification`
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { INotificationPort } from '@mantajs/core';
|
|
2
|
+
export interface ResendNotificationAdapterOptions {
|
|
3
|
+
/** Resend API key. Defaults to `process.env.RESEND_API_KEY`. */
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
/**
|
|
6
|
+
* Default `from` address. Used when the per-call payload omits `from`.
|
|
7
|
+
* Format: `"Display Name <addr@domain>"` or just `"addr@domain"`.
|
|
8
|
+
*/
|
|
9
|
+
defaultFrom?: string;
|
|
10
|
+
/** Default reply-to address. Used when the per-call payload omits `replyTo`. */
|
|
11
|
+
defaultReplyTo?: string | string[];
|
|
12
|
+
}
|
|
13
|
+
type SendInput = Parameters<INotificationPort['send']>[0];
|
|
14
|
+
type SendResult = Awaited<ReturnType<INotificationPort['send']>>;
|
|
15
|
+
export declare class ResendNotificationAdapter implements INotificationPort {
|
|
16
|
+
private _client;
|
|
17
|
+
private _defaultFrom?;
|
|
18
|
+
private _defaultReplyTo?;
|
|
19
|
+
constructor(opts?: ResendNotificationAdapterOptions);
|
|
20
|
+
send(notification: SendInput): Promise<SendResult>;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAItD,MAAM,WAAW,gCAAgC;IAC/C,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CACnC;AAED,KAAK,SAAS,GAAG,UAAU,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACzD,KAAK,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;AAEhE,qBAAa,yBAA0B,YAAW,iBAAiB;IACjE,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,YAAY,CAAC,CAAQ;IAC7B,OAAO,CAAC,eAAe,CAAC,CAAmB;gBAE/B,IAAI,GAAE,gCAAqC;IAajD,IAAI,CAAC,YAAY,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;CAqEzD"}
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// SPEC-097 — ResendNotificationAdapter implements INotificationPort
|
|
2
|
+
//
|
|
3
|
+
// Maps the framework's provider-agnostic notification payload to the Resend
|
|
4
|
+
// SDK's `emails.send()` call. Email-only adapter — sending non-email channels
|
|
5
|
+
// throws INVALID_DATA.
|
|
6
|
+
//
|
|
7
|
+
// Failure modes:
|
|
8
|
+
// - Validation errors (missing subject/from/body) → throw MantaError('INVALID_DATA').
|
|
9
|
+
// - Resend client returns `{ error }` (4xx-style validation/auth/quota errors)
|
|
10
|
+
// → return `{ status: 'FAILURE', error }` so callers can mark/retry per-cart.
|
|
11
|
+
// - Network or transport errors thrown by the SDK → re-thrown as
|
|
12
|
+
// MantaError('UNEXPECTED_STATE') so the workflow runner decides retry policy.
|
|
13
|
+
import { MantaError } from '@mantajs/core';
|
|
14
|
+
import { Resend } from 'resend';
|
|
15
|
+
export class ResendNotificationAdapter {
|
|
16
|
+
_client;
|
|
17
|
+
_defaultFrom;
|
|
18
|
+
_defaultReplyTo;
|
|
19
|
+
constructor(opts = {}) {
|
|
20
|
+
const apiKey = opts.apiKey ?? process.env.RESEND_API_KEY;
|
|
21
|
+
if (!apiKey || apiKey.length === 0) {
|
|
22
|
+
throw new MantaError('INVALID_STATE', 'ResendNotificationAdapter: missing API key. Pass `apiKey` or set RESEND_API_KEY.');
|
|
23
|
+
}
|
|
24
|
+
this._client = new Resend(apiKey);
|
|
25
|
+
this._defaultFrom = opts.defaultFrom;
|
|
26
|
+
this._defaultReplyTo = opts.defaultReplyTo;
|
|
27
|
+
}
|
|
28
|
+
async send(notification) {
|
|
29
|
+
if (notification.channel !== 'email') {
|
|
30
|
+
throw new MantaError('INVALID_DATA', `ResendNotificationAdapter only supports channel="email" (got "${notification.channel}")`);
|
|
31
|
+
}
|
|
32
|
+
if (!notification.subject || notification.subject.length === 0) {
|
|
33
|
+
throw new MantaError('INVALID_DATA', 'Email notifications require a subject');
|
|
34
|
+
}
|
|
35
|
+
if (!notification.html && !notification.text) {
|
|
36
|
+
throw new MantaError('INVALID_DATA', 'Email notifications require html or text body');
|
|
37
|
+
}
|
|
38
|
+
const from = notification.from ?? this._defaultFrom;
|
|
39
|
+
if (!from) {
|
|
40
|
+
throw new MantaError('INVALID_DATA', 'No `from` address — pass `from` per call or set `defaultFrom` on the adapter.');
|
|
41
|
+
}
|
|
42
|
+
const replyTo = notification.replyTo ?? this._defaultReplyTo;
|
|
43
|
+
// Per the framework plan, idempotency_key is forwarded as the
|
|
44
|
+
// `Idempotency-Key` HTTP header. Resend's docs document that header
|
|
45
|
+
// name directly, so this is the canonical mapping.
|
|
46
|
+
const headers = { ...(notification.headers ?? {}) };
|
|
47
|
+
if (notification.idempotency_key) {
|
|
48
|
+
headers['Idempotency-Key'] = notification.idempotency_key;
|
|
49
|
+
}
|
|
50
|
+
// Build the SDK payload. The Resend SDK's CreateEmailOptions is a
|
|
51
|
+
// discriminated union (react|html|text RequireAtLeastOne) — we provide
|
|
52
|
+
// html and/or text, never react.
|
|
53
|
+
// biome-ignore lint/suspicious/noExplicitAny: Resend's discriminated union doesn't narrow well from optional fields
|
|
54
|
+
const payload = {
|
|
55
|
+
from,
|
|
56
|
+
to: notification.to,
|
|
57
|
+
subject: notification.subject,
|
|
58
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
59
|
+
tags: notification.tags,
|
|
60
|
+
replyTo,
|
|
61
|
+
};
|
|
62
|
+
if (notification.html)
|
|
63
|
+
payload.html = notification.html;
|
|
64
|
+
if (notification.text)
|
|
65
|
+
payload.text = notification.text;
|
|
66
|
+
let response;
|
|
67
|
+
try {
|
|
68
|
+
response = await this._client.emails.send(payload);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
// Network/transport errors — let the workflow runner decide retry.
|
|
72
|
+
throw new MantaError('UNEXPECTED_STATE', `Resend transport error: ${err.message}`);
|
|
73
|
+
}
|
|
74
|
+
if (response.error) {
|
|
75
|
+
// 4xx-class provider failures (invalid_from_address, validation_error,
|
|
76
|
+
// rate_limit_exceeded, ...). Surface as FAILURE so per-cart loops keep
|
|
77
|
+
// going; the caller decides whether to mark or retry.
|
|
78
|
+
const e = response.error;
|
|
79
|
+
return {
|
|
80
|
+
status: 'FAILURE',
|
|
81
|
+
error: new Error(`[${e.name ?? 'resend_error'}] ${e.message ?? 'Unknown Resend error'}`),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return { status: 'SUCCESS', id: response.data?.id };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.test.d.ts","sourceRoot":"","sources":["../src/adapter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Unit tests for ResendNotificationAdapter — mocks the Resend SDK so we can
|
|
2
|
+
// assert the payload mapping without hitting the network.
|
|
3
|
+
import { MantaError } from '@mantajs/core';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
const sendMock = vi.fn();
|
|
6
|
+
vi.mock('resend', () => ({
|
|
7
|
+
Resend: vi.fn().mockImplementation(() => ({
|
|
8
|
+
emails: { send: sendMock },
|
|
9
|
+
})),
|
|
10
|
+
}));
|
|
11
|
+
import { ResendNotificationAdapter } from './adapter';
|
|
12
|
+
describe('ResendNotificationAdapter', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
sendMock.mockReset();
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
delete process.env.RESEND_API_KEY;
|
|
18
|
+
});
|
|
19
|
+
it('throws if no API key is provided and env is empty', () => {
|
|
20
|
+
expect(() => new ResendNotificationAdapter({})).toThrow(MantaError);
|
|
21
|
+
});
|
|
22
|
+
it('uses RESEND_API_KEY env when apiKey option is omitted', () => {
|
|
23
|
+
process.env.RESEND_API_KEY = 'env-key';
|
|
24
|
+
expect(() => new ResendNotificationAdapter({})).not.toThrow();
|
|
25
|
+
});
|
|
26
|
+
it('throws INVALID_DATA when channel is not email', async () => {
|
|
27
|
+
const a = new ResendNotificationAdapter({ apiKey: 'k', defaultFrom: 'a@x.com' });
|
|
28
|
+
await expect(a.send({ to: 'b@x.com', channel: 'sms', text: 'hi' })).rejects.toThrow(MantaError);
|
|
29
|
+
});
|
|
30
|
+
it('throws INVALID_DATA when subject is missing', async () => {
|
|
31
|
+
const a = new ResendNotificationAdapter({ apiKey: 'k', defaultFrom: 'a@x.com' });
|
|
32
|
+
await expect(a.send({ to: 'b@x.com', channel: 'email', html: '<p>hi</p>' })).rejects.toThrow(/subject/i);
|
|
33
|
+
});
|
|
34
|
+
it('throws INVALID_DATA when both html and text are missing', async () => {
|
|
35
|
+
const a = new ResendNotificationAdapter({ apiKey: 'k', defaultFrom: 'a@x.com' });
|
|
36
|
+
await expect(a.send({ to: 'b@x.com', channel: 'email', subject: 'Hi' })).rejects.toThrow(/html or text/i);
|
|
37
|
+
});
|
|
38
|
+
it('throws INVALID_DATA when neither from nor defaultFrom is set', async () => {
|
|
39
|
+
const a = new ResendNotificationAdapter({ apiKey: 'k' });
|
|
40
|
+
await expect(a.send({ to: 'b@x.com', channel: 'email', subject: 'Hi', text: 'Hi' })).rejects.toThrow(/from/i);
|
|
41
|
+
});
|
|
42
|
+
it('uses defaultFrom when payload omits from', async () => {
|
|
43
|
+
sendMock.mockResolvedValueOnce({ data: { id: 'msg_1' }, error: null, headers: {} });
|
|
44
|
+
const a = new ResendNotificationAdapter({ apiKey: 'k', defaultFrom: 'PALAS <hello@palas.com>' });
|
|
45
|
+
await a.send({ to: 'user@x.com', channel: 'email', subject: 'Hi', html: '<p>x</p>' });
|
|
46
|
+
expect(sendMock).toHaveBeenCalledTimes(1);
|
|
47
|
+
expect(sendMock.mock.calls[0][0]).toMatchObject({ from: 'PALAS <hello@palas.com>' });
|
|
48
|
+
});
|
|
49
|
+
it('per-call from overrides defaultFrom', async () => {
|
|
50
|
+
sendMock.mockResolvedValueOnce({ data: { id: 'msg_2' }, error: null, headers: {} });
|
|
51
|
+
const a = new ResendNotificationAdapter({ apiKey: 'k', defaultFrom: 'a@x.com' });
|
|
52
|
+
await a.send({
|
|
53
|
+
to: 'user@x.com',
|
|
54
|
+
channel: 'email',
|
|
55
|
+
from: 'override@x.com',
|
|
56
|
+
subject: 'Hi',
|
|
57
|
+
text: 'hi',
|
|
58
|
+
});
|
|
59
|
+
expect(sendMock.mock.calls[0][0]).toMatchObject({ from: 'override@x.com' });
|
|
60
|
+
});
|
|
61
|
+
it('forwards idempotency_key as Idempotency-Key header', async () => {
|
|
62
|
+
sendMock.mockResolvedValueOnce({ data: { id: 'msg_3' }, error: null, headers: {} });
|
|
63
|
+
const a = new ResendNotificationAdapter({ apiKey: 'k', defaultFrom: 'a@x.com' });
|
|
64
|
+
await a.send({
|
|
65
|
+
to: 'user@x.com',
|
|
66
|
+
channel: 'email',
|
|
67
|
+
subject: 'Hi',
|
|
68
|
+
text: 'hi',
|
|
69
|
+
idempotency_key: 'cart:123:1',
|
|
70
|
+
});
|
|
71
|
+
expect(sendMock.mock.calls[0][0].headers).toMatchObject({ 'Idempotency-Key': 'cart:123:1' });
|
|
72
|
+
});
|
|
73
|
+
it('preserves caller headers and adds Idempotency-Key', async () => {
|
|
74
|
+
sendMock.mockResolvedValueOnce({ data: { id: 'msg_4' }, error: null, headers: {} });
|
|
75
|
+
const a = new ResendNotificationAdapter({ apiKey: 'k', defaultFrom: 'a@x.com' });
|
|
76
|
+
await a.send({
|
|
77
|
+
to: 'user@x.com',
|
|
78
|
+
channel: 'email',
|
|
79
|
+
subject: 'Hi',
|
|
80
|
+
text: 'hi',
|
|
81
|
+
headers: { 'List-Unsubscribe': '<https://x.com/u>' },
|
|
82
|
+
idempotency_key: 'k-1',
|
|
83
|
+
});
|
|
84
|
+
expect(sendMock.mock.calls[0][0].headers).toEqual({
|
|
85
|
+
'List-Unsubscribe': '<https://x.com/u>',
|
|
86
|
+
'Idempotency-Key': 'k-1',
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
it('passes tags through to the SDK', async () => {
|
|
90
|
+
sendMock.mockResolvedValueOnce({ data: { id: 'msg_5' }, error: null, headers: {} });
|
|
91
|
+
const a = new ResendNotificationAdapter({ apiKey: 'k', defaultFrom: 'a@x.com' });
|
|
92
|
+
const tags = [
|
|
93
|
+
{ name: 'category', value: 'abandoned-cart' },
|
|
94
|
+
{ name: 'cart_id', value: 'c-1' },
|
|
95
|
+
];
|
|
96
|
+
await a.send({
|
|
97
|
+
to: 'user@x.com',
|
|
98
|
+
channel: 'email',
|
|
99
|
+
subject: 'Hi',
|
|
100
|
+
text: 'hi',
|
|
101
|
+
tags,
|
|
102
|
+
});
|
|
103
|
+
expect(sendMock.mock.calls[0][0].tags).toEqual(tags);
|
|
104
|
+
});
|
|
105
|
+
it('returns SUCCESS with id when Resend succeeds', async () => {
|
|
106
|
+
sendMock.mockResolvedValueOnce({ data: { id: 'msg_ok' }, error: null, headers: {} });
|
|
107
|
+
const a = new ResendNotificationAdapter({ apiKey: 'k', defaultFrom: 'a@x.com' });
|
|
108
|
+
const out = await a.send({
|
|
109
|
+
to: 'user@x.com',
|
|
110
|
+
channel: 'email',
|
|
111
|
+
subject: 'Hi',
|
|
112
|
+
html: '<p>hi</p>',
|
|
113
|
+
});
|
|
114
|
+
expect(out).toEqual({ status: 'SUCCESS', id: 'msg_ok' });
|
|
115
|
+
});
|
|
116
|
+
it('returns FAILURE on Resend error response (4xx)', async () => {
|
|
117
|
+
sendMock.mockResolvedValueOnce({
|
|
118
|
+
data: null,
|
|
119
|
+
error: { name: 'invalid_from_address', message: 'Bad from', statusCode: 422 },
|
|
120
|
+
headers: {},
|
|
121
|
+
});
|
|
122
|
+
const a = new ResendNotificationAdapter({ apiKey: 'k', defaultFrom: 'bad@nowhere' });
|
|
123
|
+
const out = await a.send({
|
|
124
|
+
to: 'user@x.com',
|
|
125
|
+
channel: 'email',
|
|
126
|
+
subject: 'Hi',
|
|
127
|
+
text: 'hi',
|
|
128
|
+
});
|
|
129
|
+
expect(out.status).toBe('FAILURE');
|
|
130
|
+
expect(out.error).toBeDefined();
|
|
131
|
+
expect(out.error?.message).toMatch(/invalid_from_address|Bad from/);
|
|
132
|
+
});
|
|
133
|
+
it('throws when SDK throws (network/5xx surface as transport error)', async () => {
|
|
134
|
+
sendMock.mockRejectedValueOnce(new Error('fetch failed'));
|
|
135
|
+
const a = new ResendNotificationAdapter({ apiKey: 'k', defaultFrom: 'a@x.com' });
|
|
136
|
+
await expect(a.send({ to: 'user@x.com', channel: 'email', subject: 'Hi', text: 'hi' })).rejects.toThrow(/transport error|fetch failed/i);
|
|
137
|
+
});
|
|
138
|
+
it('uses defaultReplyTo when payload omits replyTo', async () => {
|
|
139
|
+
sendMock.mockResolvedValueOnce({ data: { id: 'msg_r' }, error: null, headers: {} });
|
|
140
|
+
const a = new ResendNotificationAdapter({
|
|
141
|
+
apiKey: 'k',
|
|
142
|
+
defaultFrom: 'a@x.com',
|
|
143
|
+
defaultReplyTo: 'reply@x.com',
|
|
144
|
+
});
|
|
145
|
+
await a.send({ to: 'user@x.com', channel: 'email', subject: 'Hi', text: 'hi' });
|
|
146
|
+
expect(sendMock.mock.calls[0][0]).toMatchObject({ replyTo: 'reply@x.com' });
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
//# sourceMappingURL=adapter.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.test.js","sourceRoot":"","sources":["../src/adapter.test.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,yBAAyB,EAAE,KAAK,gCAAgC,EAAE,MAAM,WAAW,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,yBAAyB,EAAyC,MAAM,WAAW,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mantajs/adapter-notification-resend",
|
|
3
|
+
"version": "0.2.0-beta.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"@mantajs/core": "0.2.0-beta.0"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"resend": "^6.12.3"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
]
|
|
23
|
+
}
|