@trstudios/discord-notify 1.1.0 → 1.2.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/README.md CHANGED
@@ -77,6 +77,36 @@ if (!result.ok) {
77
77
 
78
78
  Use `"none"` for messages that contain secrets, such as sign-in codes, password resets and invite links. Anyone who can read the alert channel can read whatever an alert includes.
79
79
 
80
+ ### Check delivery and alerts
81
+
82
+ `sendTestMail` sends a short test email through the same path as real mail. With `fail: true`, it uses a deliberately invalid token instead, so the send fails and the failure alert fires. That checks the alert path without changing any configuration.
83
+
84
+ `secretMatches` compares a request header with a configured secret in constant time. It returns `false` when either value is missing.
85
+
86
+ Here's an example route, shown with Next.js, protected by an admin secret. It sends only to an address from configuration:
87
+
88
+ ```ts
89
+ import { secretMatches, sendTestMail } from "@trstudios/discord-notify";
90
+
91
+ export async function POST(request: Request) {
92
+ if (!secretMatches(request.headers.get("x-admin-token"), process.env.ADMIN_TOKEN)) {
93
+ return Response.json({ error: "Not allowed" }, { status: 403 });
94
+ }
95
+ const to = process.env.TEST_EMAIL_TO;
96
+ if (!to) return Response.json({ error: "TEST_EMAIL_TO is not set" }, { status: 500 });
97
+
98
+ const result = await sendTestMail({
99
+ from: { address: "noreply@example.com", name: "Example" },
100
+ to: [{ address: to }],
101
+ source: "example.com",
102
+ fail: new URL(request.url).searchParams.get("fail") === "1",
103
+ });
104
+ return Response.json(result, { status: result.ok ? 200 : 502 });
105
+ }
106
+ ```
107
+
108
+ Never take the recipient from the request. Otherwise anyone holding the admin secret could use the route to send mail to arbitrary addresses.
109
+
80
110
  ## Safety notes
81
111
 
82
112
  - **No pings:** alerts are posted with mentions disabled, so text in a message can't ping `@everyone` or a role.
@@ -96,7 +126,7 @@ npm run typecheck
96
126
  1. Bump `version` in `package.json` and commit.
97
127
  2. Push a matching tag, for example `git tag v1.0.1 && git push origin v1.0.1`.
98
128
 
99
- GitHub Actions then stages the release on npm through Trusted Publishing, with provenance. A maintainer approves it on npmjs.com to publish it.
129
+ GitHub Actions then publishes it to npm through Trusted Publishing, with provenance.
100
130
 
101
131
  ## License
102
132
 
package/dist/notify.d.ts CHANGED
@@ -85,4 +85,24 @@ export declare function failureAlert(mail: Mail, error: string, level: AlertCont
85
85
  * still answer its own request.
86
86
  */
87
87
  export declare function sendMail(mail: Mail, options?: SendOptions): Promise<SendResult>;
88
+ /**
89
+ * Compares a secret from a request header with the expected value in constant
90
+ * time. Returns false when either is missing, so an unset secret never matches.
91
+ */
92
+ export declare function secretMatches(given: string | null | undefined, expected: string | undefined): boolean;
93
+ export interface TestMailOptions extends SendOptions {
94
+ from: Address;
95
+ /** Where the test goes. Take it from configuration, never from the request. */
96
+ to: Address[];
97
+ /**
98
+ * Send with a deliberately invalid token, so the send fails and the failure
99
+ * alert fires. Checks the alert path without changing any configuration.
100
+ */
101
+ fail?: boolean;
102
+ }
103
+ /**
104
+ * Sends a short test email through the same path as real mail, to check
105
+ * delivery (or, with `fail`, the failure alert). Never throws.
106
+ */
107
+ export declare function sendTestMail(options: TestMailOptions): Promise<SendResult>;
88
108
  export {};
package/dist/notify.js CHANGED
@@ -2,7 +2,8 @@
2
2
  // Server-side only: it reads secrets from the environment. Never import it from
3
3
  // browser or app bundles.
4
4
  //
5
- // No dependencies; needs a global fetch (Node 18+, Bun, Deno).
5
+ // No dependencies; needs a global fetch and node:crypto (Node 18+, Bun, Deno).
6
+ import { createHash, timingSafeEqual } from "node:crypto";
6
7
  // Discord's limits for one embed.
7
8
  const TITLE_MAX = 256;
8
9
  const FIELD_NAME_MAX = 256;
@@ -180,3 +181,38 @@ export async function sendMail(mail, options = {}) {
180
181
  });
181
182
  return { ok: false, error };
182
183
  }
184
+ // ---------------------------------------------------------------------------
185
+ // Delivery checks
186
+ // ---------------------------------------------------------------------------
187
+ /**
188
+ * Compares a secret from a request header with the expected value in constant
189
+ * time. Returns false when either is missing, so an unset secret never matches.
190
+ */
191
+ export function secretMatches(given, expected) {
192
+ if (!given || !expected)
193
+ return false;
194
+ const a = createHash("sha256").update(given).digest();
195
+ const b = createHash("sha256").update(expected).digest();
196
+ return timingSafeEqual(a, b);
197
+ }
198
+ /**
199
+ * Sends a short test email through the same path as real mail, to check
200
+ * delivery (or, with `fail`, the failure alert). Never throws.
201
+ */
202
+ export async function sendTestMail(options) {
203
+ const { from, to, fail, ...send } = options;
204
+ const when = new Date().toISOString();
205
+ const what = fail ? "Failure alert test" : "Delivery test";
206
+ return sendMail({
207
+ from,
208
+ to,
209
+ subject: `${what}${send.source ? ` (${send.source})` : ""}: ${when}`,
210
+ text: fail
211
+ ? `This send used a deliberately invalid token, so it should fail and raise an alert.\n\nSent ${when}.`
212
+ : `If you can read this, email from ${from.address} is being delivered.\n\nSent ${when}.`,
213
+ }, {
214
+ alertContent: "summary",
215
+ ...send,
216
+ ...(fail && { token: "invalid-token-for-failure-test" }),
217
+ });
218
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trstudios/discord-notify",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Discord alerts for server-side failures, with a ZeptoMail sender that reports failed sends. No dependencies.",
5
5
  "keywords": [
6
6
  "discord",