@frigear-nu/notify 0.0.8 → 0.0.10
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 +29 -26
- package/dist/module.d.mts +31 -22
- package/dist/module.json +1 -1
- package/dist/module.mjs +32 -67
- package/dist/runtime/drivers/email/cloudflare.d.ts +7 -0
- package/dist/runtime/drivers/email/cloudflare.js +20 -0
- package/dist/runtime/drivers/email/index.d.ts +9 -0
- package/dist/runtime/drivers/email/index.js +21 -0
- package/dist/runtime/drivers/email/resend.d.ts +6 -0
- package/dist/runtime/drivers/email/resend.js +19 -0
- package/dist/runtime/drivers/email/ses.d.ts +9 -0
- package/dist/runtime/drivers/email/ses.js +33 -0
- package/dist/runtime/drivers/email/shared.d.ts +11 -0
- package/dist/runtime/drivers/email/shared.js +14 -0
- package/dist/runtime/drivers/email/sigv4.d.ts +14 -0
- package/dist/runtime/drivers/email/sigv4.js +38 -0
- package/dist/runtime/drivers/log.d.ts +2 -9
- package/dist/runtime/drivers/log.js +0 -1
- package/dist/runtime/server/utils/definers.d.ts +1 -3
- package/dist/runtime/server/utils/definers.js +0 -2
- package/dist/types.d.mts +1 -1
- package/package.json +4 -5
- package/dist/runtime/app/composables/useNotify.d.ts +0 -11
- package/dist/runtime/app/composables/useNotify.js +0 -16
- package/dist/runtime/drivers/ui/nuxt.ui.d.ts +0 -2
- package/dist/runtime/drivers/ui/nuxt.ui.js +0 -20
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @frigear-nu/notify
|
|
2
2
|
|
|
3
3
|
Generic notification driver/dispatch Nuxt module. Lets feature modules (e.g. `@frigear-nu/users`)
|
|
4
|
-
dispatch email/SMS/simple notifications
|
|
4
|
+
dispatch email/SMS/simple notifications without ever deciding how they're
|
|
5
5
|
delivered - that choice (and configuration) belongs to the app/consuming layer.
|
|
6
6
|
|
|
7
7
|
## Quick Setup
|
|
@@ -13,8 +13,7 @@ want to configure it.
|
|
|
13
13
|
## Configuration
|
|
14
14
|
|
|
15
15
|
Configure the module via the `notify` key in `nuxt.config.ts`. Every notification type (`email`,
|
|
16
|
-
`sms`, `simple`) defaults to the built-in `log` driver (logs instead of sending)
|
|
17
|
-
`ui` driver has no default, since there's no sensible UI to push to unless you configure one:
|
|
16
|
+
`sms`, `simple`) defaults to the built-in `log` driver (logs instead of sending):
|
|
18
17
|
|
|
19
18
|
```ts
|
|
20
19
|
export default defineNuxtConfig({
|
|
@@ -33,19 +32,13 @@ export default defineNuxtConfig({
|
|
|
33
32
|
// Same idea, matching the `SimpleDriver` signature, for generic push notifications.
|
|
34
33
|
driver: 'log',
|
|
35
34
|
},
|
|
36
|
-
ui: {
|
|
37
|
-
// Built-in `'@nuxt/ui'` driver (forwards to `@nuxt/ui`'s `useToast`), or a path to a
|
|
38
|
-
// function (default export) matching the `UIDriver` signature - a composable factory
|
|
39
|
-
// invoked once, synchronously, during `useNotify()`'s own setup-time call.
|
|
40
|
-
driver: '@nuxt/ui',
|
|
41
|
-
},
|
|
42
35
|
},
|
|
43
36
|
})
|
|
44
37
|
```
|
|
45
38
|
|
|
46
|
-
Only the configured driver for each notification type is ever resolved/loaded
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
Only the configured driver for each notification type is ever resolved/loaded.
|
|
40
|
+
|
|
41
|
+
In-app toasts are not part of this module: call `useToast()` from Nuxt UI directly.
|
|
49
42
|
|
|
50
43
|
#### Example Custom Driver
|
|
51
44
|
|
|
@@ -86,21 +79,31 @@ await dispatchSimpleNotification({
|
|
|
86
79
|
})
|
|
87
80
|
```
|
|
88
81
|
|
|
89
|
-
|
|
82
|
+
## Built-in email drivers: `resend`, `ses`, `cloudflare`
|
|
90
83
|
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
Zero dependencies and zero config beyond environment variables - each driver talks to its provider with plain `fetch`
|
|
85
|
+
(SES requests are signed with AWS SigV4 via Web Crypto). The driver and its credentials are read at **runtime**, so
|
|
86
|
+
you can switch providers per environment without rebuilding:
|
|
93
87
|
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
```bash
|
|
89
|
+
NUXT_NOTIFY_EMAIL_DRIVER=resend # log (default) | resend | ses | cloudflare
|
|
90
|
+
NUXT_NOTIFY_EMAIL_FROM="Frigear <no-reply@frigear.nu>"
|
|
97
91
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
92
|
+
# resend
|
|
93
|
+
NUXT_NOTIFY_EMAIL_RESEND_API_TOKEN=...
|
|
94
|
+
|
|
95
|
+
# ses (v2 API)
|
|
96
|
+
NUXT_NOTIFY_EMAIL_SES_REGION=eu-west-1
|
|
97
|
+
NUXT_NOTIFY_EMAIL_SES_ACCESS_KEY_ID=...
|
|
98
|
+
NUXT_NOTIFY_EMAIL_SES_SECRET_ACCESS_KEY=...
|
|
99
|
+
NUXT_NOTIFY_EMAIL_SES_SESSION_TOKEN=... # optional, for temporary credentials
|
|
100
|
+
|
|
101
|
+
# cloudflare (Email Service)
|
|
102
|
+
NUXT_NOTIFY_EMAIL_CLOUDFLARE_ACCOUNT_ID=...
|
|
103
|
+
NUXT_NOTIFY_EMAIL_CLOUDFLARE_API_TOKEN=...
|
|
106
104
|
```
|
|
105
|
+
|
|
106
|
+
Setting `notify.email.driver` in `nuxt.config.ts` to one of these names only changes the default; the env var still
|
|
107
|
+
wins. A missing credential fails the send with an error naming the variable to set. Any other `driver` value is still
|
|
108
|
+
treated as a path to a custom `EmailDriver`.
|
|
109
|
+
|
package/dist/module.d.mts
CHANGED
|
@@ -15,15 +15,6 @@ interface SimpleNotification<M = Record<string, unknown>> {
|
|
|
15
15
|
url?: string;
|
|
16
16
|
meta?: M;
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
19
|
-
* A notification that is intended to be displayed in the UI, e.g. via a toast or snackbar.
|
|
20
|
-
* The `type` field can be used to determine the styling of the notification.
|
|
21
|
-
*/
|
|
22
|
-
interface UINotification<A = unknown, M = NotificationMeta> extends SimpleNotification<M> {
|
|
23
|
-
type?: 'success' | 'error' | 'info' | 'warning';
|
|
24
|
-
actions?: A[];
|
|
25
|
-
meta?: M;
|
|
26
|
-
}
|
|
27
18
|
/**
|
|
28
19
|
* Generic, transport-agnostic SMS notification. Concrete notifications (e.g. a password reset)
|
|
29
20
|
* build one of these and hand it to `dispatchSMSNotification`, similar in spirit to how Laravel
|
|
@@ -46,6 +37,8 @@ interface EmailNotification<M = NotificationMeta> {
|
|
|
46
37
|
body: string;
|
|
47
38
|
/** Optional plain-text body. */
|
|
48
39
|
text?: string;
|
|
40
|
+
/** Address replies should go to (the `Reply-To` header). */
|
|
41
|
+
replyTo?: string;
|
|
49
42
|
meta?: M;
|
|
50
43
|
}
|
|
51
44
|
/**
|
|
@@ -56,18 +49,9 @@ interface EmailNotification<M = NotificationMeta> {
|
|
|
56
49
|
type SimpleDriver = (notification: SimpleNotification) => Promise<void> | void;
|
|
57
50
|
type SMSDriver = (notification: SMSNotification) => Promise<void> | void;
|
|
58
51
|
type EmailDriver = (notification: EmailNotification) => Promise<void> | void;
|
|
59
|
-
/**
|
|
60
|
-
* Unlike the server-side drivers above, a UI driver is a *composable factory*: `useNotify()` calls
|
|
61
|
-
* it once, synchronously, during its own (setup-time) invocation, and uses the function it returns
|
|
62
|
-
* to actually dispatch notifications later. This is required because driver implementations that
|
|
63
|
-
* need Vue's `inject()` (e.g. the built-in `@nuxt/ui` driver's `useToast()`) only work when called
|
|
64
|
-
* during a component's `setup()` - resolving them lazily, when a notification is dispatched (e.g.
|
|
65
|
-
* from a `setInterval` callback), runs outside any component and breaks `inject()`.
|
|
66
|
-
*/
|
|
67
|
-
type UIDriver = () => (notification: UINotification) => Promise<void> | void;
|
|
68
52
|
type ModuleOptions = {
|
|
69
53
|
email?: {
|
|
70
|
-
driver?:
|
|
54
|
+
driver?: 'log' | 'resend' | 'ses' | 'cloudflare' | (string & {});
|
|
71
55
|
};
|
|
72
56
|
sms?: {
|
|
73
57
|
driver?: string | 'log';
|
|
@@ -78,8 +62,30 @@ type ModuleOptions = {
|
|
|
78
62
|
push?: {
|
|
79
63
|
driver?: string;
|
|
80
64
|
};
|
|
81
|
-
|
|
82
|
-
|
|
65
|
+
};
|
|
66
|
+
/** Private runtime config (`runtimeConfig.notify`), overridable through `NUXT_NOTIFY_*` env vars. */
|
|
67
|
+
type NotifyRuntimeConfig = {
|
|
68
|
+
email: {
|
|
69
|
+
/** `NUXT_NOTIFY_EMAIL_DRIVER`: `log` (default), `resend`, `ses` or `cloudflare`. */
|
|
70
|
+
driver: string;
|
|
71
|
+
/** `NUXT_NOTIFY_EMAIL_FROM`: sender address, e.g. `Frigear <no-reply@frigear.nu>`. */
|
|
72
|
+
from: string;
|
|
73
|
+
/** `NUXT_NOTIFY_EMAIL_RESEND_API_TOKEN` */
|
|
74
|
+
resend: {
|
|
75
|
+
apiToken: string;
|
|
76
|
+
};
|
|
77
|
+
/** `NUXT_NOTIFY_EMAIL_SES_REGION`, `_ACCESS_KEY_ID`, `_SECRET_ACCESS_KEY`, `_SESSION_TOKEN` (optional) */
|
|
78
|
+
ses: {
|
|
79
|
+
region: string;
|
|
80
|
+
accessKeyId: string;
|
|
81
|
+
secretAccessKey: string;
|
|
82
|
+
sessionToken: string;
|
|
83
|
+
};
|
|
84
|
+
/** `NUXT_NOTIFY_EMAIL_CLOUDFLARE_ACCOUNT_ID`, `_API_TOKEN` */
|
|
85
|
+
cloudflare: {
|
|
86
|
+
accountId: string;
|
|
87
|
+
apiToken: string;
|
|
88
|
+
};
|
|
83
89
|
};
|
|
84
90
|
};
|
|
85
91
|
|
|
@@ -87,8 +93,11 @@ declare module '@nuxt/schema' {
|
|
|
87
93
|
interface NuxtConfig {
|
|
88
94
|
notify?: ModuleOptions;
|
|
89
95
|
}
|
|
96
|
+
interface RuntimeConfig {
|
|
97
|
+
notify: NotifyRuntimeConfig;
|
|
98
|
+
}
|
|
90
99
|
}
|
|
91
100
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
92
101
|
|
|
93
102
|
export { _default as default };
|
|
94
|
-
export type { EmailDriver, EmailNotification, ModuleOptions, SMSDriver, SMSNotification, SimpleDriver, SimpleNotification
|
|
103
|
+
export type { EmailDriver, EmailNotification, ModuleOptions, SMSDriver, SMSNotification, SimpleDriver, SimpleNotification };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver,
|
|
1
|
+
import { defineNuxtModule, createResolver, addServerImportsDir, addServerTemplate, addTypeTemplate } from '@nuxt/kit';
|
|
2
|
+
import { defu } from 'defu';
|
|
2
3
|
import { joinURL } from 'ufo';
|
|
3
4
|
|
|
4
|
-
const BUILTIN_UI_DRIVERS = {
|
|
5
|
-
"log": { path: "./runtime/drivers/log", exportName: "logUIDriver" },
|
|
6
|
-
"@nuxt/ui": { path: "./runtime/drivers/ui/nuxt.ui", exportName: "default" }
|
|
7
|
-
};
|
|
8
5
|
const BUILTIN_SERVER_DRIVERS = {
|
|
9
6
|
log: { path: "./runtime/drivers/log", exportName: "logDriver" }
|
|
10
7
|
};
|
|
8
|
+
const BUILTIN_EMAIL_DRIVERS = ["log", "resend", "ses", "cloudflare"];
|
|
9
|
+
const AUTO_EMAIL_DRIVER = { path: "./runtime/drivers/email/index", exportName: "default" };
|
|
11
10
|
const module$1 = defineNuxtModule({
|
|
12
11
|
meta: { name: "@frigear-nu/notify", configKey: "notify" },
|
|
13
12
|
defaults: {
|
|
@@ -15,83 +14,49 @@ const module$1 = defineNuxtModule({
|
|
|
15
14
|
sms: { driver: "log" },
|
|
16
15
|
simple: { driver: "log" }
|
|
17
16
|
},
|
|
18
|
-
moduleDependencies: {
|
|
19
|
-
"@nuxt/ui": {
|
|
20
|
-
optional: true,
|
|
21
|
-
version: ">=4.10"
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
17
|
async setup(options, nuxt) {
|
|
25
18
|
const resolver = createResolver(import.meta.url);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
addImports({
|
|
31
|
-
name: "useNotify",
|
|
32
|
-
as: "useNotify",
|
|
33
|
-
from: resolver.resolve("./runtime/app/composables/useNotify")
|
|
34
|
-
});
|
|
35
|
-
addImports({
|
|
36
|
-
name: "defineUINotification",
|
|
37
|
-
as: "defineUINotification",
|
|
38
|
-
from: resolver.resolve("./runtime/server/utils/definers")
|
|
39
|
-
});
|
|
40
|
-
const uiDriverTemplate = addTemplate({
|
|
41
|
-
filename: "#notify/ui-driver.ts",
|
|
42
|
-
write: true,
|
|
19
|
+
const registerServerDriver = (name, driverType, driver, builtins = BUILTIN_SERVER_DRIVERS) => {
|
|
20
|
+
const filename = `#notify/${name}`;
|
|
21
|
+
const template = addServerTemplate({
|
|
22
|
+
filename,
|
|
43
23
|
getContents: async () => {
|
|
44
|
-
const builtin =
|
|
24
|
+
const builtin = builtins[driver];
|
|
45
25
|
if (builtin) {
|
|
46
26
|
return `export { ${builtin.exportName} as default } from ${JSON.stringify(await resolver.resolvePath(builtin.path))}`;
|
|
47
27
|
}
|
|
48
|
-
|
|
49
|
-
throw new Error(`Invalid UI driver: ${options.ui?.driver || "undefined"}`);
|
|
50
|
-
}
|
|
51
|
-
const driverPath = await resolver.resolvePath(options.ui.driver, { cwd: nuxt.options.rootDir });
|
|
28
|
+
const driverPath = await resolver.resolvePath(driver, { cwd: nuxt.options.rootDir });
|
|
52
29
|
return `export { default } from ${JSON.stringify(driverPath)}`;
|
|
53
30
|
}
|
|
54
31
|
});
|
|
55
|
-
nuxt.options.alias
|
|
32
|
+
nuxt.options.nitro.alias ||= {};
|
|
33
|
+
nuxt.options.nitro.alias[`#notify/${name}`] = joinURL(template.filename, ".ts");
|
|
56
34
|
addTypeTemplate({
|
|
57
|
-
filename:
|
|
58
|
-
getContents: () => `declare module '#notify
|
|
59
|
-
import type { UIDriver } from ${JSON.stringify(resolver.resolve("./types"))}
|
|
60
|
-
const driver: UIDriver
|
|
61
|
-
export default driver
|
|
62
|
-
}
|
|
63
|
-
`
|
|
64
|
-
});
|
|
65
|
-
const registerServerDriver = (name, driverType, driver) => {
|
|
66
|
-
const filename = `#notify/${name}`;
|
|
67
|
-
const template = addServerTemplate({
|
|
68
|
-
filename,
|
|
69
|
-
getContents: async () => {
|
|
70
|
-
const builtin = BUILTIN_SERVER_DRIVERS[driver];
|
|
71
|
-
if (builtin) {
|
|
72
|
-
return `export { ${builtin.exportName} as default } from ${JSON.stringify(await resolver.resolvePath(builtin.path))}`;
|
|
73
|
-
}
|
|
74
|
-
const driverPath = await resolver.resolvePath(driver, { cwd: nuxt.options.rootDir });
|
|
75
|
-
return `export { default } from ${JSON.stringify(driverPath)}`;
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
nuxt.options.nitro.alias ||= {};
|
|
79
|
-
nuxt.options.nitro.alias[`#notify/${name}`] = joinURL(template.filename, ".ts");
|
|
80
|
-
addTypeTemplate({
|
|
81
|
-
filename: `types/notify-${name}.d.ts`,
|
|
82
|
-
getContents: () => `declare module '#notify/${name}' {
|
|
35
|
+
filename: `types/notify-${name}.d.ts`,
|
|
36
|
+
getContents: () => `declare module '#notify/${name}' {
|
|
83
37
|
import type { ${driverType} } from ${JSON.stringify(resolver.resolve("./types"))}
|
|
84
38
|
const driver: ${driverType}
|
|
85
39
|
export default driver
|
|
86
40
|
}
|
|
87
41
|
`
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
42
|
+
}, { nitro: true, nuxt: true });
|
|
43
|
+
};
|
|
44
|
+
const emailDriver = options.email?.driver || "log";
|
|
45
|
+
const emailBuiltin = BUILTIN_EMAIL_DRIVERS.includes(emailDriver);
|
|
46
|
+
const runtimeConfig = nuxt.options.runtimeConfig;
|
|
47
|
+
runtimeConfig.notify = defu(runtimeConfig.notify, {
|
|
48
|
+
email: {
|
|
49
|
+
driver: emailBuiltin ? emailDriver : "",
|
|
50
|
+
from: "",
|
|
51
|
+
resend: { apiToken: "" },
|
|
52
|
+
ses: { region: "", accessKeyId: "", secretAccessKey: "", sessionToken: "" },
|
|
53
|
+
cloudflare: { accountId: "", apiToken: "" }
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
registerServerDriver("email-driver", "EmailDriver", emailBuiltin ? "auto" : emailDriver, { auto: AUTO_EMAIL_DRIVER });
|
|
57
|
+
registerServerDriver("sms-driver", "SMSDriver", options.sms?.driver ?? "log");
|
|
58
|
+
registerServerDriver("simple-driver", "SimpleDriver", options.simple?.driver ?? "log");
|
|
59
|
+
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
|
|
95
60
|
}
|
|
96
61
|
});
|
|
97
62
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { EmailDriver } from '../../../types.js';
|
|
2
|
+
import { type EmailConfig } from './shared.js';
|
|
3
|
+
export interface CloudflareConfig extends EmailConfig {
|
|
4
|
+
accountId?: string;
|
|
5
|
+
apiToken?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const createCloudflareDriver: (config: CloudflareConfig) => EmailDriver;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { assertOk, requireConfig, resolveFrom, toArray } from "./shared.js";
|
|
2
|
+
export const createCloudflareDriver = (config) => async (n) => {
|
|
3
|
+
const accountId = requireConfig(config.accountId, "NUXT_NOTIFY_EMAIL_CLOUDFLARE_ACCOUNT_ID");
|
|
4
|
+
const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/email/sending/send`, {
|
|
5
|
+
method: "POST",
|
|
6
|
+
headers: {
|
|
7
|
+
"Authorization": `Bearer ${requireConfig(config.apiToken, "NUXT_NOTIFY_EMAIL_CLOUDFLARE_API_TOKEN")}`,
|
|
8
|
+
"Content-Type": "application/json"
|
|
9
|
+
},
|
|
10
|
+
body: JSON.stringify({
|
|
11
|
+
from: resolveFrom(config),
|
|
12
|
+
to: toArray(n.to),
|
|
13
|
+
subject: n.subject,
|
|
14
|
+
html: n.body,
|
|
15
|
+
text: n.text,
|
|
16
|
+
reply_to: n.replyTo
|
|
17
|
+
})
|
|
18
|
+
});
|
|
19
|
+
await assertOk("Cloudflare", response);
|
|
20
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EmailDriver } from '../../../types.js';
|
|
2
|
+
export declare const EMAIL_DRIVER_NAMES: readonly ["log", "resend", "ses", "cloudflare"];
|
|
3
|
+
/**
|
|
4
|
+
* Default `#notify/email-driver`. Environment variables only exist at runtime, so the concrete
|
|
5
|
+
* driver is chosen per send from `runtimeConfig.notify.email` (`NUXT_NOTIFY_EMAIL_DRIVER`), and
|
|
6
|
+
* only that driver's code is loaded.
|
|
7
|
+
*/
|
|
8
|
+
declare const autoDriver: EmailDriver;
|
|
9
|
+
export default autoDriver;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useRuntimeConfig } from "#imports";
|
|
2
|
+
import { logDriver } from "../log.js";
|
|
3
|
+
export const EMAIL_DRIVER_NAMES = ["log", "resend", "ses", "cloudflare"];
|
|
4
|
+
const autoDriver = async (notification) => {
|
|
5
|
+
const config = useRuntimeConfig().notify?.email ?? {};
|
|
6
|
+
const name = config.driver || "log";
|
|
7
|
+
const base = { from: config.from };
|
|
8
|
+
switch (name) {
|
|
9
|
+
case "log":
|
|
10
|
+
return logDriver(notification);
|
|
11
|
+
case "resend":
|
|
12
|
+
return (await import("./resend.js")).createResendDriver({ ...base, ...config.resend })(notification);
|
|
13
|
+
case "ses":
|
|
14
|
+
return (await import("./ses.js")).createSesDriver({ ...base, ...config.ses })(notification);
|
|
15
|
+
case "cloudflare":
|
|
16
|
+
return (await import("./cloudflare.js")).createCloudflareDriver({ ...base, ...config.cloudflare })(notification);
|
|
17
|
+
default:
|
|
18
|
+
throw new Error(`[notify] Unknown email driver "${name}" (NUXT_NOTIFY_EMAIL_DRIVER). Expected one of: ${EMAIL_DRIVER_NAMES.join(", ")}`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export default autoDriver;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { assertOk, requireConfig, resolveFrom, toArray } from "./shared.js";
|
|
2
|
+
export const createResendDriver = (config) => async (n) => {
|
|
3
|
+
const response = await fetch("https://api.resend.com/emails", {
|
|
4
|
+
method: "POST",
|
|
5
|
+
headers: {
|
|
6
|
+
"Authorization": `Bearer ${requireConfig(config.apiToken, "NUXT_NOTIFY_EMAIL_RESEND_API_TOKEN")}`,
|
|
7
|
+
"Content-Type": "application/json"
|
|
8
|
+
},
|
|
9
|
+
body: JSON.stringify({
|
|
10
|
+
from: resolveFrom(config),
|
|
11
|
+
to: toArray(n.to),
|
|
12
|
+
subject: n.subject,
|
|
13
|
+
html: n.body,
|
|
14
|
+
text: n.text,
|
|
15
|
+
reply_to: n.replyTo
|
|
16
|
+
})
|
|
17
|
+
});
|
|
18
|
+
await assertOk("Resend", response);
|
|
19
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EmailDriver } from '../../../types.js';
|
|
2
|
+
import { type EmailConfig } from './shared.js';
|
|
3
|
+
export interface SesConfig extends EmailConfig {
|
|
4
|
+
region?: string;
|
|
5
|
+
accessKeyId?: string;
|
|
6
|
+
secretAccessKey?: string;
|
|
7
|
+
sessionToken?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const createSesDriver: (config: SesConfig) => EmailDriver;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { assertOk, requireConfig, resolveFrom, toArray } from "./shared.js";
|
|
2
|
+
import { signRequest } from "./sigv4.js";
|
|
3
|
+
export const createSesDriver = (config) => async (n) => {
|
|
4
|
+
const region = requireConfig(config.region, "NUXT_NOTIFY_EMAIL_SES_REGION");
|
|
5
|
+
const accessKeyId = requireConfig(config.accessKeyId, "NUXT_NOTIFY_EMAIL_SES_ACCESS_KEY_ID");
|
|
6
|
+
const secretAccessKey = requireConfig(config.secretAccessKey, "NUXT_NOTIFY_EMAIL_SES_SECRET_ACCESS_KEY");
|
|
7
|
+
const url = `https://email.${region}.amazonaws.com/v2/email/outbound-emails`;
|
|
8
|
+
const body = JSON.stringify({
|
|
9
|
+
FromEmailAddress: resolveFrom(config),
|
|
10
|
+
Destination: { ToAddresses: toArray(n.to) },
|
|
11
|
+
ReplyToAddresses: n.replyTo ? [n.replyTo] : void 0,
|
|
12
|
+
Content: {
|
|
13
|
+
Simple: {
|
|
14
|
+
Subject: { Data: n.subject, Charset: "UTF-8" },
|
|
15
|
+
Body: {
|
|
16
|
+
Html: { Data: n.body, Charset: "UTF-8" },
|
|
17
|
+
...n.text ? { Text: { Data: n.text, Charset: "UTF-8" } } : {}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const headers = await signRequest({
|
|
23
|
+
method: "POST",
|
|
24
|
+
url,
|
|
25
|
+
body,
|
|
26
|
+
region,
|
|
27
|
+
service: "ses",
|
|
28
|
+
accessKeyId,
|
|
29
|
+
secretAccessKey,
|
|
30
|
+
sessionToken: config.sessionToken || void 0
|
|
31
|
+
});
|
|
32
|
+
await assertOk("SES", await fetch(url, { method: "POST", headers, body }));
|
|
33
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EmailNotification } from '../../../types.js';
|
|
2
|
+
export interface EmailConfig {
|
|
3
|
+
from?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const toArray: (value: string | string[]) => string[];
|
|
6
|
+
/** Throws a config error naming the env var an operator needs to set. */
|
|
7
|
+
export declare function requireConfig(value: string | undefined, envVar: string): string;
|
|
8
|
+
export declare const resolveFrom: (config: EmailConfig) => string;
|
|
9
|
+
/** Throws with the provider's response body for any non-2xx response. */
|
|
10
|
+
export declare function assertOk(provider: string, response: Response): Promise<void>;
|
|
11
|
+
export type { EmailNotification };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const toArray = (value) => Array.isArray(value) ? value : [value];
|
|
2
|
+
export function requireConfig(value, envVar) {
|
|
3
|
+
if (!value) {
|
|
4
|
+
throw new Error(`[notify] Missing email configuration: set ${envVar}`);
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
export const resolveFrom = (config) => requireConfig(config.from, "NUXT_NOTIFY_EMAIL_FROM");
|
|
9
|
+
export async function assertOk(provider, response) {
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
const detail = await response.text().catch(() => "");
|
|
12
|
+
throw new Error(`[notify] ${provider} responded ${response.status}${detail ? `: ${detail}` : ""}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SignOptions {
|
|
2
|
+
method: string;
|
|
3
|
+
url: string;
|
|
4
|
+
body: string;
|
|
5
|
+
region: string;
|
|
6
|
+
service: string;
|
|
7
|
+
accessKeyId: string;
|
|
8
|
+
secretAccessKey: string;
|
|
9
|
+
sessionToken?: string;
|
|
10
|
+
/** Injectable for tests. */
|
|
11
|
+
now?: Date;
|
|
12
|
+
}
|
|
13
|
+
/** Returns the headers (including `Authorization`) to send with the request. */
|
|
14
|
+
export declare function signRequest(o: SignOptions): Promise<Record<string, string>>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const encoder = new TextEncoder();
|
|
2
|
+
const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
3
|
+
const sha256Hex = async (data) => hex(await crypto.subtle.digest("SHA-256", encoder.encode(data)));
|
|
4
|
+
async function hmac(key, data) {
|
|
5
|
+
const raw = typeof key === "string" ? encoder.encode(key) : key;
|
|
6
|
+
const cryptoKey = await crypto.subtle.importKey("raw", raw, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
7
|
+
return crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(data));
|
|
8
|
+
}
|
|
9
|
+
export async function signRequest(o) {
|
|
10
|
+
const url = new URL(o.url);
|
|
11
|
+
const amzDate = (o.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
12
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
13
|
+
const payloadHash = await sha256Hex(o.body);
|
|
14
|
+
const headers = {
|
|
15
|
+
"content-type": "application/json",
|
|
16
|
+
"host": url.host,
|
|
17
|
+
"x-amz-content-sha256": payloadHash,
|
|
18
|
+
"x-amz-date": amzDate
|
|
19
|
+
};
|
|
20
|
+
if (o.sessionToken) headers["x-amz-security-token"] = o.sessionToken;
|
|
21
|
+
const names = Object.keys(headers).sort();
|
|
22
|
+
const canonicalHeaders = names.map((n) => `${n}:${headers[n].trim()}
|
|
23
|
+
`).join("");
|
|
24
|
+
const signedHeaders = names.join(";");
|
|
25
|
+
const canonicalRequest = [o.method, url.pathname, url.searchParams.toString(), canonicalHeaders, signedHeaders, payloadHash].join("\n");
|
|
26
|
+
const scope = `${dateStamp}/${o.region}/${o.service}/aws4_request`;
|
|
27
|
+
const stringToSign = ["AWS4-HMAC-SHA256", amzDate, scope, await sha256Hex(canonicalRequest)].join("\n");
|
|
28
|
+
const kDate = await hmac(`AWS4${o.secretAccessKey}`, dateStamp);
|
|
29
|
+
const kRegion = await hmac(kDate, o.region);
|
|
30
|
+
const kService = await hmac(kRegion, o.service);
|
|
31
|
+
const kSigning = await hmac(kService, "aws4_request");
|
|
32
|
+
const signature = hex(await hmac(kSigning, stringToSign));
|
|
33
|
+
const { host: _host, ...sendable } = headers;
|
|
34
|
+
return {
|
|
35
|
+
...sendable,
|
|
36
|
+
Authorization: `AWS4-HMAC-SHA256 Credential=${o.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import type { SimpleNotification,
|
|
1
|
+
import type { SimpleNotification, SMSNotification, EmailNotification } from '../../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Logs the notification instead of sending it. Safe default for development/unconfigured setups,
|
|
4
4
|
* and a reference implementation for the `EmailDriver` signature.
|
|
5
5
|
*/
|
|
6
|
-
export declare const logDriver: (notification: SimpleNotification |
|
|
7
|
-
/**
|
|
8
|
-
* `UIDriver`-shaped version of `logDriver` - unlike the server-side drivers, UI drivers are
|
|
9
|
-
* composable factories (see `UIDriver`'s doc comment), so this just wraps `logDriver` in one to
|
|
10
|
-
* use as the built-in `'log'` UI driver. Doesn't call any Vue composables itself, so there's
|
|
11
|
-
* nothing `inject()`-sensitive here - it works the same called eagerly or lazily.
|
|
12
|
-
*/
|
|
13
|
-
export declare const logUIDriver: UIDriver;
|
|
6
|
+
export declare const logDriver: (notification: SimpleNotification | SMSNotification | EmailNotification) => void;
|
|
14
7
|
export default logDriver;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import type { SimpleDriver,
|
|
1
|
+
import type { SimpleDriver, SMSDriver, EmailDriver, SimpleNotification, SMSNotification, EmailNotification } from '../../../types.js';
|
|
2
2
|
export declare const defineSimpleNotification: <T extends SimpleNotification>(notification: T) => T;
|
|
3
|
-
export declare const defineUINotification: <T extends UINotification>(notification: T) => T;
|
|
4
3
|
export declare const defineEmailNotification: <T extends EmailNotification>(notification: T) => T;
|
|
5
4
|
export declare const defineSMSNotification: <T extends SMSNotification>(notification: T) => T;
|
|
6
5
|
export declare const defineSimpleDriver: (driver: SimpleDriver) => SimpleDriver;
|
|
7
|
-
export declare const defineUIDriver: (driver: UIDriver) => UIDriver;
|
|
8
6
|
export declare const defineSMSDriver: (driver: SMSDriver) => SMSDriver;
|
|
9
7
|
export declare const defineEmailDriver: (driver: EmailDriver) => EmailDriver;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
export const defineSimpleNotification = (notification) => notification;
|
|
2
|
-
export const defineUINotification = (notification) => notification;
|
|
3
2
|
export const defineEmailNotification = (notification) => notification;
|
|
4
3
|
export const defineSMSNotification = (notification) => notification;
|
|
5
4
|
export const defineSimpleDriver = (driver) => driver;
|
|
6
|
-
export const defineUIDriver = (driver) => driver;
|
|
7
5
|
export const defineSMSDriver = (driver) => driver;
|
|
8
6
|
export const defineEmailDriver = (driver) => driver;
|
package/dist/types.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { default } from './module.mjs'
|
|
2
2
|
|
|
3
|
-
export { type EmailDriver, type EmailNotification, type ModuleOptions, type SMSDriver, type SMSNotification, type SimpleDriver, type SimpleNotification
|
|
3
|
+
export { type EmailDriver, type EmailNotification, type ModuleOptions, type SMSDriver, type SMSNotification, type SimpleDriver, type SimpleNotification } from './module.mjs'
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frigear-nu/notify",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Email, SMS and generic notification dispatch (pluggable drivers) Nuxt module, shared across Frigear modules.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://docs.frigear.dev/modules/notify",
|
|
8
8
|
"repository": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@nuxt/kit": "^4.6.0",
|
|
25
|
+
"defu": "^6.1.7",
|
|
25
26
|
"ufo": "^1.6.4"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
@@ -36,15 +37,13 @@
|
|
|
36
37
|
"vitest": "^5.0.1",
|
|
37
38
|
"vue-tsc": "^3.3.11"
|
|
38
39
|
},
|
|
39
|
-
"optionalDependencies": {
|
|
40
|
-
"@nuxt/ui": ">=4.10.0"
|
|
41
|
-
},
|
|
42
40
|
"scripts": {
|
|
43
41
|
"postinstall": "nuxt-module-build build --stub && nuxt prepare",
|
|
44
42
|
"build": "nuxt-module-build build",
|
|
45
43
|
"dev": "npm run dev:prepare && nuxt dev playground",
|
|
46
44
|
"dev:build": "nuxt build playground",
|
|
47
45
|
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
|
|
46
|
+
"test": "vitest run",
|
|
48
47
|
"lint": "eslint .",
|
|
49
48
|
"lint:fix": "eslint . --fix"
|
|
50
49
|
},
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { UINotification } from '../../../types.js';
|
|
2
|
-
/**
|
|
3
|
-
* App-side composable for pushing `UINotification`s through the configured UI driver (e.g. the
|
|
4
|
-
* `@nuxt/ui` driver, which forwards them to `useToast`). Only registered when `notify.ui.driver`
|
|
5
|
-
* is configured - see `setup()` in the module.
|
|
6
|
-
*/
|
|
7
|
-
export declare const useNotify: () => {
|
|
8
|
-
notifications: import("vue").Ref<UINotification<unknown, import("../../../types.js").NotificationMeta<Record<string, unknown>>>[], UINotification<unknown, import("../../../types.js").NotificationMeta<Record<string, unknown>>>[]>;
|
|
9
|
-
addNotification: (notification: UINotification) => any;
|
|
10
|
-
add: (notification: UINotification) => any;
|
|
11
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { useState } from "#imports";
|
|
2
|
-
import uiDriver from "#notify/ui-driver";
|
|
3
|
-
export const useNotify = () => {
|
|
4
|
-
const notifications = useState("notify-notifications", () => []);
|
|
5
|
-
const dispatch = uiDriver();
|
|
6
|
-
const add = (notification) => {
|
|
7
|
-
notifications.value.push(notification);
|
|
8
|
-
return dispatch(notification);
|
|
9
|
-
};
|
|
10
|
-
return {
|
|
11
|
-
notifications,
|
|
12
|
-
// `addNotification` is an alias for `add` to make the API more explicit and self-documenting
|
|
13
|
-
addNotification: add,
|
|
14
|
-
add
|
|
15
|
-
};
|
|
16
|
-
};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { useToast } from "#imports";
|
|
2
|
-
import { defineUIDriver } from "../../server/utils/definers.js";
|
|
3
|
-
const COLOR_BY_TYPE = {
|
|
4
|
-
success: "success",
|
|
5
|
-
error: "error",
|
|
6
|
-
warning: "warning",
|
|
7
|
-
info: "info"
|
|
8
|
-
};
|
|
9
|
-
const nuxtUiDriver = defineUIDriver(() => {
|
|
10
|
-
const toast = useToast();
|
|
11
|
-
return (notification) => {
|
|
12
|
-
toast.add({
|
|
13
|
-
title: notification.title,
|
|
14
|
-
description: notification.text,
|
|
15
|
-
color: COLOR_BY_TYPE[notification?.type ?? "info"],
|
|
16
|
-
actions: notification?.actions
|
|
17
|
-
});
|
|
18
|
-
};
|
|
19
|
-
});
|
|
20
|
-
export default nuxtUiDriver;
|