@zerotal/notifications 1.7.0 → 1.7.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/notifications",
3
- "version": "1.7.0",
3
+ "version": "1.7.3",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -30,12 +30,12 @@
30
30
  "typecheck": "tsc --noEmit"
31
31
  },
32
32
  "dependencies": {
33
- "@zerotal/core": "1.7.0",
34
- "@zerotal/orm": "1.7.0",
35
- "@zerotal/queue": "1.7.0"
33
+ "@zerotal/core": "1.7.3",
34
+ "@zerotal/orm": "1.7.3",
35
+ "@zerotal/queue": "1.7.3"
36
36
  },
37
37
  "devDependencies": {
38
- "@zerotal/broadcasting": "1.7.0",
38
+ "@zerotal/broadcasting": "1.7.3",
39
39
  "typescript": "^5.8.0"
40
40
  },
41
41
  "description": "Multi-channel notifications for Zerotal — mail (SMTP/Resend), database, broadcast, Slack, and SMS.",
@@ -1,3 +1,4 @@
1
+ import { tryCurrentApp } from "@zerotal/core";
1
2
  import {
2
3
  resolveAddress,
3
4
  type AddressInput,
@@ -6,6 +7,35 @@ import {
6
7
  type MailPayload,
7
8
  } from "../drivers/MailDriver.ts";
8
9
 
10
+ /**
11
+ * Make an action URL absolute.
12
+ *
13
+ * A browser resolves `/projects/4` against the page it is on. An email client
14
+ * has no page, so a root-relative href in a delivered message is simply dead —
15
+ * and this is the one place the base URL cannot be left to the caller, because
16
+ * the caller is usually a queue job with no request to read an origin from.
17
+ *
18
+ * `config.app.url` is the origin the app already declares for exactly this.
19
+ * Resolution happens at render rather than in `action()` so a message can still
20
+ * be constructed outside an application — a unit test, or a driver being
21
+ * exercised on its own — where it is returned untouched.
22
+ *
23
+ * Anything already absolute is left alone, `mailto:` and `tel:` included.
24
+ */
25
+ function _absoluteUrl(url: string): string {
26
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith("//")) return url;
27
+
28
+ const base = tryCurrentApp()?.container.tryMake("config")?.get<string>("app.url");
29
+ if (!base) return url;
30
+
31
+ try {
32
+ return new URL(url, base).toString();
33
+ } catch {
34
+ // A base that is not a URL is a config error, not this message's problem.
35
+ return url;
36
+ }
37
+ }
38
+
9
39
  /** Inline text styling applied to a whole line or to a single run within a line. */
10
40
  export interface TextStyle {
11
41
  bold?: boolean;
@@ -302,7 +332,7 @@ export class MailMessage {
302
332
  for (const l of this._introLines) parts.push(this._paragraph(l));
303
333
  if (this._actionText && this._actionUrl) {
304
334
  parts.push(
305
- `<p style="margin:24px 0"><a href="${escapeHtml(this._actionUrl)}" ` +
335
+ `<p style="margin:24px 0"><a href="${escapeHtml(_absoluteUrl(this._actionUrl))}" ` +
306
336
  `style="display:inline-block;background:#4f46e5;color:#fff;text-decoration:none;` +
307
337
  `padding:12px 20px;border-radius:8px;font-size:14px;font-weight:600">${escapeHtml(this._actionText)}</a></p>`,
308
338
  );
@@ -330,7 +360,8 @@ export class MailMessage {
330
360
  private _renderText(): string {
331
361
  const parts: string[] = [this._lineText(this._greeting ?? [{ text: "Hello," }])];
332
362
  for (const l of this._introLines) parts.push(this._lineText(l));
333
- if (this._actionText && this._actionUrl) parts.push(`${this._actionText}: ${this._actionUrl}`);
363
+ if (this._actionText && this._actionUrl)
364
+ parts.push(`${this._actionText}: ${_absoluteUrl(this._actionUrl)}`);
334
365
  for (const l of this._outroLines) parts.push(this._lineText(l));
335
366
  parts.push(this._lineText(this._salutation ?? [{ text: "Regards," }]));
336
367
  return parts.join("\n\n");