@frigear-nu/notify 0.0.7 → 0.0.9

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,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, and push UI toasts, without ever deciding how they're
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) - only the
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 - unconfigured
47
- drivers (e.g. the `ui` driver when `notify.ui` isn't set) are never bundled, to keep the client
48
- bundle small.
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
- ### UI notifications
82
+ ## Built-in email drivers: `resend`, `ses`, `cloudflare`
90
83
 
91
- When `notify.ui.driver` is configured, `useNotify` is auto-imported client-side to push
92
- `UINotification`s through the configured driver (e.g. `@nuxt/ui`'s `useToast`):
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
- ```vue
95
- <script setup lang="ts">
96
- const notify = useNotify()
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
- function onSuccess() {
99
- notify.addNotification(defineUINotification({
100
- title: 'Saved',
101
- text: 'Your changes have been saved.',
102
- type: 'success',
103
- }))
104
- }
105
- </script>
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?: string | 'log';
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
- ui?: {
82
- driver?: string | '@nuxt/ui';
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, UIDriver, UINotification };
103
+ export type { EmailDriver, EmailNotification, ModuleOptions, SMSDriver, SMSNotification, SimpleDriver, SimpleNotification };
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frigear-nu/notify",
3
3
  "configKey": "notify",
4
- "version": "0.0.7",
4
+ "version": "0.0.9",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,13 +1,12 @@
1
- import { defineNuxtModule, createResolver, hasNuxtModule, addImports, addTemplate, addTypeTemplate, addServerImportsDir, addServerTemplate } from '@nuxt/kit';
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
- if (options.ui?.driver) {
27
- if (options.ui.driver === "@nuxt/ui" && !hasNuxtModule("@nuxt/ui")) {
28
- throw new Error(`@nuxt/ui driver is configured but @nuxt/ui module is not installed. Please install @nuxt/ui or configure a different UI driver.`);
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 = BUILTIN_UI_DRIVERS[options.ui?.driver];
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
- if (!options.ui?.driver) {
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["#notify/ui-driver"] = uiDriverTemplate.dst;
32
+ nuxt.options.nitro.alias ||= {};
33
+ nuxt.options.nitro.alias[`#notify/${name}`] = joinURL(template.filename, ".ts");
56
34
  addTypeTemplate({
57
- filename: "types/notify-ui-driver.d.ts",
58
- getContents: () => `declare module '#notify/ui-driver' {
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
- }, { nitro: true, nuxt: true });
89
- };
90
- registerServerDriver("email-driver", "EmailDriver", options.email?.driver ?? "log");
91
- registerServerDriver("sms-driver", "SMSDriver", options.sms?.driver ?? "log");
92
- registerServerDriver("simple-driver", "SimpleDriver", options.simple?.driver ?? "log");
93
- addServerImportsDir(resolver.resolve("./runtime/server/utils"));
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,6 @@
1
+ import type { EmailDriver } from '../../../types.js';
2
+ import { type EmailConfig } from './shared.js';
3
+ export interface ResendConfig extends EmailConfig {
4
+ apiToken?: string;
5
+ }
6
+ export declare const createResendDriver: (config: ResendConfig) => EmailDriver;
@@ -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, UINotification, SMSNotification, EmailNotification, UIDriver } from '../../types.js';
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 | UINotification | SMSNotification | EmailNotification) => void;
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,5 +1,4 @@
1
1
  export const logDriver = (notification) => {
2
2
  console.log("[notify] Dispatching notification:", notification);
3
3
  };
4
- export const logUIDriver = () => (notification) => logDriver(notification);
5
4
  export default logDriver;
@@ -1,9 +1,7 @@
1
- import type { SimpleDriver, UIDriver, SMSDriver, EmailDriver, SimpleNotification, UINotification, SMSNotification, EmailNotification } from '../../../types.js';
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, type UIDriver, type UINotification } from './module.mjs'
3
+ export { type EmailDriver, type EmailNotification, type ModuleOptions, type SMSDriver, type SMSNotification, type SimpleDriver, type SimpleNotification } from './module.mjs'
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@frigear-nu/notify",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
- "description": "Framework-agnostic email notification driver/dispatch Nuxt module, shared across Frigear modules.",
5
+ "description": "Email, SMS and generic notification dispatch (pluggable drivers) Nuxt module, shared across Frigear modules.",
6
6
  "license": "MIT",
7
+ "homepage": "https://docs.frigear.dev/modules/notify",
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "git+https://github.com/frigear-nu/frigear-nu.git",
@@ -21,6 +22,7 @@
21
22
  ],
22
23
  "dependencies": {
23
24
  "@nuxt/kit": "^4.6.0",
25
+ "defu": "^6.1.7",
24
26
  "ufo": "^1.6.4"
25
27
  },
26
28
  "devDependencies": {
@@ -35,15 +37,13 @@
35
37
  "vitest": "^5.0.1",
36
38
  "vue-tsc": "^3.3.11"
37
39
  },
38
- "optionalDependencies": {
39
- "@nuxt/ui": ">=4.10.0"
40
- },
41
40
  "scripts": {
42
41
  "postinstall": "nuxt-module-build build --stub && nuxt prepare",
43
42
  "build": "nuxt-module-build build",
44
43
  "dev": "npm run dev:prepare && nuxt dev playground",
45
44
  "dev:build": "nuxt build playground",
46
45
  "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
46
+ "test": "vitest run",
47
47
  "lint": "eslint .",
48
48
  "lint:fix": "eslint . --fix"
49
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,2 +0,0 @@
1
- declare const nuxtUiDriver: import("../../../types.js").UIDriver;
2
- export default nuxtUiDriver;
@@ -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;