@agent-native/core 0.138.0 → 0.139.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.
Files changed (66) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
  3. package/corpus/templates/analytics/server/lib/emails.ts +51 -0
  4. package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
  5. package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
  6. package/corpus/templates/calendar/server/lib/emails.ts +167 -0
  7. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
  8. package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
  9. package/corpus/templates/clips/actions/invite-member.ts +39 -15
  10. package/corpus/templates/clips/server/lib/emails.ts +256 -0
  11. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
  12. package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
  13. package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
  14. package/corpus/templates/content/server/lib/emails.ts +70 -0
  15. package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
  16. package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
  17. package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
  18. package/corpus/templates/forms/server/lib/emails.ts +52 -0
  19. package/corpus/templates/forms/server/lib/response-email.ts +2 -0
  20. package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
  21. package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
  22. package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
  23. package/corpus/templates/plan/server/lib/emails.ts +67 -0
  24. package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
  25. package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
  26. package/corpus/templates/slides/server/lib/emails.ts +40 -0
  27. package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
  28. package/dist/collab/routes.d.ts +1 -1
  29. package/dist/collab/struct-routes.d.ts +1 -1
  30. package/dist/email-catalog/actions/list-email-activity.d.ts +6 -0
  31. package/dist/email-catalog/actions/list-email-activity.js +15 -0
  32. package/dist/email-catalog/actions/list-email-engagement.d.ts +6 -0
  33. package/dist/email-catalog/actions/list-email-engagement.js +15 -0
  34. package/dist/email-catalog/actions/list-email-log.d.ts +8 -0
  35. package/dist/email-catalog/actions/list-email-log.js +22 -0
  36. package/dist/email-catalog/actions/list-transactional-emails.d.ts +23 -0
  37. package/dist/email-catalog/actions/list-transactional-emails.js +61 -0
  38. package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +10 -0
  39. package/dist/email-catalog/actions/render-transactional-email-preview.js +24 -0
  40. package/dist/email-catalog/authorize.d.ts +2 -0
  41. package/dist/email-catalog/authorize.js +10 -0
  42. package/dist/email-catalog/log.d.ts +57 -0
  43. package/dist/email-catalog/log.js +117 -0
  44. package/dist/email-catalog/provider-metrics.d.ts +34 -0
  45. package/dist/email-catalog/provider-metrics.js +165 -0
  46. package/dist/email-catalog/registry.d.ts +79 -0
  47. package/dist/email-catalog/registry.js +71 -0
  48. package/dist/email-catalog/schema.d.ts +212 -0
  49. package/dist/email-catalog/schema.js +49 -0
  50. package/dist/email-catalog/system-emails.d.ts +14 -0
  51. package/dist/email-catalog/system-emails.js +68 -0
  52. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  53. package/dist/localization/default-messages.d.ts +45 -0
  54. package/dist/localization/default-messages.js +45 -0
  55. package/dist/notifications/routes.d.ts +5 -5
  56. package/dist/org/handlers.js +8 -1
  57. package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
  58. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  59. package/dist/server/action-discovery.js +22 -0
  60. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  61. package/dist/server/better-auth-instance.js +17 -2
  62. package/dist/server/email.d.ts +9 -0
  63. package/dist/server/email.js +44 -3
  64. package/dist/server/index.d.ts +1 -0
  65. package/dist/server/index.js +1 -0
  66. package/package.json +2 -1
@@ -22,6 +22,8 @@ import {
22
22
  } from "../server/lib/local-identity.js";
23
23
  import { newId, nowIso, planPath, writeEvent } from "../server/plans.js";
24
24
 
25
+ export const PLAN_ACCESS_REQUEST_EMAIL_ID = "plan.access-request";
26
+
25
27
  function httpError(message: string, statusCode: number): Error {
26
28
  return Object.assign(new Error(message), { statusCode });
27
29
  }
@@ -49,6 +51,28 @@ function absolutePlanUrl(planId: string, kind: "plan" | "recap"): string {
49
51
  }
50
52
  }
51
53
 
54
+ export function renderPlanAccessRequestEmail(input: {
55
+ requesterName: string;
56
+ requesterEmail: string;
57
+ planTitle: string;
58
+ url: string;
59
+ }) {
60
+ const subject = `${input.requesterName} requested access to "${input.planTitle}"`;
61
+ return {
62
+ subject,
63
+ ...renderEmail({
64
+ preheader: subject,
65
+ heading: "Access request",
66
+ paragraphs: [
67
+ `${emailStrong(input.requesterName)} (${emailStrong(input.requesterEmail)}) requested access to ${emailStrong(input.planTitle)}.`,
68
+ "Open the plan and use Share to grant access if this request should be approved.",
69
+ ],
70
+ cta: { label: "Open plan", url: input.url },
71
+ footer: "You received this because you own this Agent-Native Plan.",
72
+ }),
73
+ };
74
+ }
75
+
52
76
  async function notifyOwner(input: {
53
77
  planId: string;
54
78
  planKind: "plan" | "recap";
@@ -62,19 +86,16 @@ async function notifyOwner(input: {
62
86
  return false;
63
87
  }
64
88
 
65
- const url = absolutePlanUrl(input.planId, input.planKind);
66
- const subject = `${input.requesterName} requested access to "${input.planTitle}"`;
67
- const { html, text } = renderEmail({
68
- preheader: subject,
69
- heading: "Access request",
70
- paragraphs: [
71
- `${emailStrong(input.requesterName)} (${emailStrong(input.requesterEmail)}) requested access to ${emailStrong(input.planTitle)}.`,
72
- "Open the plan and use Share to grant access if this request should be approved.",
73
- ],
74
- cta: { label: "Open plan", url },
75
- footer: "You received this because you own this Agent-Native Plan.",
89
+ await sendEmail({
90
+ ...renderPlanAccessRequestEmail({
91
+ requesterName: input.requesterName,
92
+ requesterEmail: input.requesterEmail,
93
+ planTitle: input.planTitle,
94
+ url: absolutePlanUrl(input.planId, input.planKind),
95
+ }),
96
+ to: input.ownerEmail,
97
+ templateId: PLAN_ACCESS_REQUEST_EMAIL_ID,
76
98
  });
77
- await sendEmail({ to: input.ownerEmail, subject, html, text });
78
99
  return true;
79
100
  }
80
101
 
@@ -14,6 +14,8 @@ import {
14
14
  import type { PlanBundle, PlanComment } from "../../shared/types.js";
15
15
  import { getDb, schema } from "../db/index.js";
16
16
 
17
+ export const PLAN_COMMENT_EMAIL_ID = "plan.plan-comment";
18
+
17
19
  type CommentNotificationInput = {
18
20
  bundle: PlanBundle;
19
21
  insertedCommentIds: string[];
@@ -205,40 +207,64 @@ export function planCommentNotificationRecipients(input: {
205
207
  return Array.from(recipients.values());
206
208
  }
207
209
 
208
- async function sendPlanCommentNotification(input: {
209
- recipient: NotificationRecipient;
210
- comment: PlanComment;
210
+ export function renderPlanCommentEmail(input: {
211
+ actor: string;
212
+ app: string;
211
213
  planTitle: string;
212
- planId: string;
214
+ planUrl: string;
215
+ message: string;
216
+ isReply: boolean;
217
+ reason: NotificationRecipient["reason"];
213
218
  }) {
214
- const actor = actorName(input.comment);
215
- const app = appName();
216
- const isReply = Boolean(input.comment.parentCommentId);
217
- const subject = isReply
218
- ? `${actor} replied to a comment on "${input.planTitle}"`
219
- : `${actor} commented on "${input.planTitle}"`;
220
219
  const actionText =
221
- input.recipient.reason === "mention"
220
+ input.reason === "mention"
222
221
  ? "mentioned you in a comment on"
223
- : input.recipient.reason === "plan-owner"
222
+ : input.reason === "plan-owner"
224
223
  ? "left a comment on your plan"
225
224
  : "replied in a comment thread you participated in";
226
- const { html, text } = renderEmail({
227
- preheader: `${actor} ${actionText} on ${app}.`,
228
- heading: isReply ? "New reply on your plan" : "New comment on your plan",
229
- paragraphs: [
230
- `${emailStrong(actor)} ${actionText} ${emailStrong(input.planTitle)}.`,
231
- `Comment: "${commentExcerpt(input.comment.message)}"`,
232
- ],
233
- cta: { label: "Open plan", url: planUrl(input.planId) },
234
- footer:
235
- input.recipient.reason === "plan-owner"
236
- ? "You received this because you own this plan."
237
- : input.recipient.reason === "mention"
238
- ? "You received this because you were mentioned in this comment."
239
- : "You received this because you participated in this comment thread.",
225
+ return {
226
+ subject: input.isReply
227
+ ? `${input.actor} replied to a comment on "${input.planTitle}"`
228
+ : `${input.actor} commented on "${input.planTitle}"`,
229
+ ...renderEmail({
230
+ preheader: `${input.actor} ${actionText} on ${input.app}.`,
231
+ heading: input.isReply
232
+ ? "New reply on your plan"
233
+ : "New comment on your plan",
234
+ paragraphs: [
235
+ `${emailStrong(input.actor)} ${actionText} ${emailStrong(input.planTitle)}.`,
236
+ `Comment: "${commentExcerpt(input.message)}"`,
237
+ ],
238
+ cta: { label: "Open plan", url: input.planUrl },
239
+ footer:
240
+ input.reason === "plan-owner"
241
+ ? "You received this because you own this plan."
242
+ : input.reason === "mention"
243
+ ? "You received this because you were mentioned in this comment."
244
+ : "You received this because you participated in this comment thread.",
245
+ }),
246
+ };
247
+ }
248
+
249
+ async function sendPlanCommentNotification(input: {
250
+ recipient: NotificationRecipient;
251
+ comment: PlanComment;
252
+ planTitle: string;
253
+ planId: string;
254
+ }) {
255
+ await sendEmail({
256
+ ...renderPlanCommentEmail({
257
+ actor: actorName(input.comment),
258
+ app: appName(),
259
+ planTitle: input.planTitle,
260
+ planUrl: planUrl(input.planId),
261
+ message: input.comment.message,
262
+ isReply: Boolean(input.comment.parentCommentId),
263
+ reason: input.recipient.reason,
264
+ }),
265
+ to: input.recipient.email,
266
+ templateId: PLAN_COMMENT_EMAIL_ID,
240
267
  });
241
- await sendEmail({ to: input.recipient.email, subject, html, text });
242
268
  }
243
269
 
244
270
  export async function notifyPlanCommentRecipients({
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Catalog entries for the transactional emails Plan sends.
3
+ *
4
+ * Registered from `server/plugins/transactional-emails.ts` so Dispatch can list
5
+ * and preview them without the app having sent anything yet.
6
+ */
7
+
8
+ import { defineTransactionalEmail } from "@agent-native/core/email-catalog";
9
+
10
+ import {
11
+ PLAN_ACCESS_REQUEST_EMAIL_ID,
12
+ renderPlanAccessRequestEmail,
13
+ } from "../../actions/request-plan-access.js";
14
+ import {
15
+ PLAN_COMMENT_EMAIL_ID,
16
+ renderPlanCommentEmail,
17
+ } from "./comment-notifications.js";
18
+
19
+ let registered = false;
20
+
21
+ export function registerPlanEmails(): void {
22
+ if (registered) return;
23
+ registered = true;
24
+
25
+ defineTransactionalEmail({
26
+ id: PLAN_COMMENT_EMAIL_ID,
27
+ name: "Plan comment",
28
+ trigger:
29
+ "A human (never the agent) posts a comment or reply on a plan or recap. The subject changes for a reply and the body changes with why the recipient was picked; all variants send under this id.",
30
+ recipientLabel: "Owner, mentions, authors",
31
+ recipient:
32
+ "The plan owner, everyone mentioned in the comment, and — for a reply — every other human author in the same thread. The comment's own author, synthetic QA addresses, and the owner when the comment mentions the source author are all dropped. One email per remaining address.",
33
+ senderLabel: "Default sender",
34
+ sender:
35
+ "The configured default sender. This call site sets no `from`, `fromName`, `replyTo`, or `appSender`.",
36
+ preview: () =>
37
+ renderPlanCommentEmail({
38
+ actor: "Sam Rivera",
39
+ app: "Agent-Native Plan",
40
+ planTitle: "Checkout rewrite",
41
+ planUrl: "https://example.com/plans/plan_sample",
42
+ message: "Can we split the migration into two deploys?",
43
+ isReply: false,
44
+ reason: "plan-owner",
45
+ }),
46
+ });
47
+
48
+ defineTransactionalEmail({
49
+ id: PLAN_ACCESS_REQUEST_EMAIL_ID,
50
+ name: "Plan access request",
51
+ trigger:
52
+ "A signed-in user who cannot resolve access calls `request-plan-access` on a private plan URL. Skipped when email is not configured or the requester is the owner.",
53
+ recipientLabel: "Plan owner",
54
+ recipient:
55
+ "The plan's `ownerEmail` column. Anonymous public viewers and guest author identities cannot reach this path.",
56
+ senderLabel: "Default sender",
57
+ sender:
58
+ "The configured default sender. This call site sets no `from`, `fromName`, `replyTo`, or `appSender`, so the owner cannot reply straight to the requester.",
59
+ preview: () =>
60
+ renderPlanAccessRequestEmail({
61
+ requesterName: "Sam Rivera",
62
+ requesterEmail: "sam.rivera@example.com",
63
+ planTitle: "Checkout rewrite",
64
+ url: "https://example.com/plans/plan_sample",
65
+ }),
66
+ });
67
+ }
@@ -0,0 +1,5 @@
1
+ import { registerPlanEmails } from "../lib/emails.js";
2
+
3
+ export default function registerTransactionalEmails(): void {
4
+ registerPlanEmails();
5
+ }
@@ -21,6 +21,7 @@ import { and, eq } from "drizzle-orm";
21
21
  import { getDeckUrl } from "../../actions/_app-url.js";
22
22
  import { SLIDES_USER_PREFS_KEY } from "../../shared/slides-user-prefs.js";
23
23
  import { getDb, schema } from "../db/index.js";
24
+ import { SLIDES_DECK_COMMENT_EMAIL_ID } from "./emails.js";
24
25
 
25
26
  /**
26
27
  * `deck-missing` stays distinct from `no-recipients`: one means the deck could
@@ -99,6 +100,41 @@ async function threadParticipants(
99
100
  return rows.map((row) => row.authorEmail);
100
101
  }
101
102
 
103
+ export function renderDeckCommentEmail({
104
+ actor,
105
+ title,
106
+ url,
107
+ content,
108
+ isReply,
109
+ }: {
110
+ actor: string;
111
+ title: string;
112
+ url: string;
113
+ content: string;
114
+ isReply: boolean;
115
+ }) {
116
+ return {
117
+ subject: isReply
118
+ ? `${actor} replied to a comment on "${title}"`
119
+ : `${actor} commented on "${title}"`,
120
+ ...renderEmail({
121
+ preheader: isReply
122
+ ? `${actor} replied to a comment on ${title}.`
123
+ : `${actor} commented on ${title}.`,
124
+ heading: isReply ? "New reply on your deck" : "New comment",
125
+ paragraphs: [
126
+ isReply
127
+ ? `${emailStrong(actor)} replied in a comment thread on ${emailStrong(title)}.`
128
+ : `${emailStrong(actor)} commented on ${emailStrong(title)}.`,
129
+ `"${excerpt(content)}"`,
130
+ ],
131
+ cta: { label: "Open deck", url },
132
+ footer:
133
+ "You received this because you own or participated in this thread. Turn these off in Slides settings.",
134
+ }),
135
+ };
136
+ }
137
+
102
138
  export async function notifyDeckComment(input: {
103
139
  deckId: string;
104
140
  slideId: string;
@@ -153,29 +189,16 @@ async function deliverDeckCommentEmails(input: {
153
189
  preferenceKey: SLIDES_USER_PREFS_KEY,
154
190
  logLabel: LOG_LABEL,
155
191
  send: async (to) => {
156
- const { html, text } = renderEmail({
157
- preheader: input.isReply
158
- ? `${actor} replied to a comment on ${deck.title}.`
159
- : `${actor} commented on ${deck.title}.`,
160
- heading: input.isReply ? "New reply on your deck" : "New comment",
161
- paragraphs: [
162
- input.isReply
163
- ? `${emailStrong(actor)} replied in a comment thread on ${emailStrong(deck.title)}.`
164
- : `${emailStrong(actor)} commented on ${emailStrong(deck.title)}.`,
165
- `"${excerpt(input.content)}"`,
166
- ],
167
- cta: { label: "Open deck", url },
168
- footer:
169
- "You received this because you own or participated in this thread. Turn these off in Slides settings.",
170
- });
171
-
172
192
  await sendEmail({
193
+ ...renderDeckCommentEmail({
194
+ actor,
195
+ title: deck.title,
196
+ url,
197
+ content: input.content,
198
+ isReply: input.isReply,
199
+ }),
173
200
  to,
174
- subject: input.isReply
175
- ? `${actor} replied to a comment on "${deck.title}"`
176
- : `${actor} commented on "${deck.title}"`,
177
- html,
178
- text,
201
+ templateId: SLIDES_DECK_COMMENT_EMAIL_ID,
179
202
  });
180
203
  },
181
204
  });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Catalog entries for the transactional emails Slides sends.
3
+ *
4
+ * Registered from `server/plugins/transactional-emails.ts` so Dispatch can list
5
+ * and preview them without the app having sent anything yet.
6
+ */
7
+
8
+ import { defineTransactionalEmail } from "@agent-native/core/email-catalog";
9
+
10
+ import { renderDeckCommentEmail } from "./comment-notifications.js";
11
+
12
+ export const SLIDES_DECK_COMMENT_EMAIL_ID = "slides.deck-comment";
13
+
14
+ let registered = false;
15
+
16
+ export function registerSlidesEmails(): void {
17
+ if (registered) return;
18
+ registered = true;
19
+
20
+ defineTransactionalEmail({
21
+ id: SLIDES_DECK_COMMENT_EMAIL_ID,
22
+ name: "Deck comment",
23
+ trigger:
24
+ "Someone posts a comment or a thread reply on a slide. Subject and copy differ slightly for a reply; both send under this id.",
25
+ recipientLabel: "Deck owner and authors",
26
+ recipient:
27
+ "The deck owner, plus every prior author in the thread when the new comment is a reply. The list is re-checked against the deck's live ACL and filtered by each user's `emailNotifications` preference; the comment's own author never receives it.",
28
+ senderLabel: "Default sender",
29
+ sender:
30
+ "The configured default sender. This call site sets no `from`, `fromName`, `replyTo`, or `appSender`.",
31
+ preview: () =>
32
+ renderDeckCommentEmail({
33
+ actor: "Sam Rivera",
34
+ title: "Series A narrative",
35
+ url: "https://example.com/decks/deck_sample?slide=3",
36
+ content: "Slide 3 needs the updated revenue chart before Thursday.",
37
+ isReply: false,
38
+ }),
39
+ });
40
+ }
@@ -0,0 +1,5 @@
1
+ import { registerSlidesEmails } from "../lib/emails.js";
2
+
3
+ export default function registerTransactionalEmails(): void {
4
+ registerSlidesEmails();
5
+ }
@@ -41,9 +41,9 @@ export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import
41
41
  * Body: { text: string, fieldName?: string, requestSource?: string }
42
42
  */
43
43
  export declare const postCollabText: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
44
- text?: undefined;
45
44
  ok?: undefined;
46
45
  error: string;
46
+ text?: undefined;
47
47
  } | {
48
48
  error?: undefined;
49
49
  ok: boolean;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- ok?: undefined;
17
16
  error: string;
17
+ ok?: undefined;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -0,0 +1,6 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ templateId: string;
3
+ limit?: unknown;
4
+ }, import("../provider-metrics.js").ProviderMetricsResult<import("../provider-metrics.js").EmailActivityEntry[]>>;
5
+ export default _default;
6
+ //# sourceMappingURL=list-email-activity.d.ts.map
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { authorizeTransactionalEmailRead } from "../authorize.js";
4
+ import { fetchEmailActivity } from "../provider-metrics.js";
5
+ export default defineAction({
6
+ description: "List recent provider activity for one registered transactional email in this app. Organization admin only.",
7
+ schema: z.object({
8
+ templateId: z.string().min(1),
9
+ limit: z.coerce.number().int().min(1).max(1000).default(50),
10
+ }),
11
+ http: { method: "GET" },
12
+ authorize: ({ templateId }) => authorizeTransactionalEmailRead([templateId]),
13
+ run: async ({ templateId, limit }) => fetchEmailActivity({ templateId, limit }),
14
+ });
15
+ //# sourceMappingURL=list-email-activity.js.map
@@ -0,0 +1,6 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ templateIds: string[];
3
+ windowDays?: unknown;
4
+ }, import("../provider-metrics.js").ProviderMetricsResult<import("../provider-metrics.js").EmailEngagement[]>>;
5
+ export default _default;
6
+ //# sourceMappingURL=list-email-engagement.d.ts.map
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { authorizeTransactionalEmailRead } from "../authorize.js";
4
+ import { fetchEmailEngagement } from "../provider-metrics.js";
5
+ export default defineAction({
6
+ description: "Read provider engagement metrics for registered transactional emails in this app. Organization admin only.",
7
+ schema: z.object({
8
+ templateIds: z.array(z.string().min(1)).max(100),
9
+ windowDays: z.coerce.number().int().min(1).max(365).default(30),
10
+ }),
11
+ http: { method: "POST" },
12
+ authorize: ({ templateIds }) => authorizeTransactionalEmailRead(templateIds),
13
+ run: async ({ templateIds, windowDays }) => fetchEmailEngagement(templateIds, windowDays),
14
+ });
15
+ //# sourceMappingURL=list-email-engagement.js.map
@@ -0,0 +1,8 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ templateId?: string;
3
+ limit?: unknown;
4
+ }, {
5
+ entries: import("../log.js").EmailLogEntry[];
6
+ }>;
7
+ export default _default;
8
+ //# sourceMappingURL=list-email-log.d.ts.map
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { getAppSlug } from "../../server/app-name.js";
4
+ import { authorizeTransactionalEmailRead } from "../authorize.js";
5
+ import { listEmailLog } from "../log.js";
6
+ export default defineAction({
7
+ description: "List recent transactional email sends from this app, newest first, optionally filtered to one registered email id.",
8
+ schema: z.object({
9
+ templateId: z.string().optional(),
10
+ limit: z.coerce.number().int().min(1).max(500).default(100),
11
+ }),
12
+ http: { method: "GET" },
13
+ authorize: ({ templateId }) => authorizeTransactionalEmailRead(templateId ? [templateId] : []),
14
+ run: async ({ templateId, limit }) => ({
15
+ entries: await listEmailLog({
16
+ app: getAppSlug() ?? "unknown",
17
+ templateId,
18
+ limit,
19
+ }),
20
+ }),
21
+ });
22
+ //# sourceMappingURL=list-email-log.js.map
@@ -0,0 +1,23 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ windowDays?: unknown;
3
+ }, {
4
+ app: string;
5
+ windowDays: number;
6
+ statsAvailable: boolean;
7
+ statsError: string;
8
+ emails: {
9
+ id: string;
10
+ app: string;
11
+ name: string;
12
+ trigger: string;
13
+ recipient: string;
14
+ recipientLabel: string;
15
+ sender: string;
16
+ senderLabel: string;
17
+ sent: number;
18
+ failed: number;
19
+ lastSentAt: number;
20
+ }[];
21
+ }>;
22
+ export default _default;
23
+ //# sourceMappingURL=list-transactional-emails.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { getAppSlug } from "../../server/app-name.js";
4
+ import { getEmailSendStats } from "../log.js";
5
+ import { listTransactionalEmails } from "../registry.js";
6
+ import { registerCoreSystemEmails } from "../system-emails.js";
7
+ const DEFAULT_WINDOW_DAYS = 30;
8
+ export default defineAction({
9
+ description: "List the transactional emails this app can send, with the trigger, recipient and sender logic for each, plus local send counts and last-sent. Engagement metrics such as open rate are not included here — they live at the email provider.",
10
+ schema: z.object({
11
+ windowDays: z.coerce
12
+ .number()
13
+ .int()
14
+ .min(1)
15
+ .max(365)
16
+ .default(DEFAULT_WINDOW_DAYS)
17
+ .describe("How many days of send history to summarize."),
18
+ }),
19
+ http: { method: "GET" },
20
+ run: async ({ windowDays }) => {
21
+ registerCoreSystemEmails();
22
+ const since = Date.now() - windowDays * 24 * 60 * 60 * 1000;
23
+ const app = getAppSlug() ?? "unknown";
24
+ const definitions = listTransactionalEmails();
25
+ // A failed stats read must not masquerade as "no email ever sent" — the
26
+ // catalog is still worth returning, but the caller has to be able to tell
27
+ // that the numbers are missing rather than zero.
28
+ let statsById = null;
29
+ let statsError = null;
30
+ try {
31
+ const stats = await getEmailSendStats(since, app);
32
+ statsById = new Map(stats.map((row) => [row.templateId, row]));
33
+ }
34
+ catch (error) {
35
+ statsError = error instanceof Error ? error.message : String(error);
36
+ }
37
+ return {
38
+ app,
39
+ windowDays,
40
+ statsAvailable: statsError === null,
41
+ statsError,
42
+ emails: definitions.map((definition) => {
43
+ const stats = statsById?.get(definition.id);
44
+ return {
45
+ id: definition.id,
46
+ app: definition.app,
47
+ name: definition.name,
48
+ trigger: definition.trigger,
49
+ recipient: definition.recipient,
50
+ recipientLabel: definition.recipientLabel,
51
+ sender: definition.sender,
52
+ senderLabel: definition.senderLabel,
53
+ sent: stats?.sent ?? (statsById ? 0 : null),
54
+ failed: stats?.failed ?? (statsById ? 0 : null),
55
+ lastSentAt: stats?.lastSentAt ?? null,
56
+ };
57
+ }),
58
+ };
59
+ },
60
+ });
61
+ //# sourceMappingURL=list-transactional-emails.js.map
@@ -0,0 +1,10 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ id: string;
3
+ }, {
4
+ id: string;
5
+ subject: string;
6
+ html: string;
7
+ text: string;
8
+ }>;
9
+ export default _default;
10
+ //# sourceMappingURL=render-transactional-email-preview.d.ts.map
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { renderTransactionalEmailPreview } from "../registry.js";
4
+ import { registerCoreSystemEmails } from "../system-emails.js";
5
+ export default defineAction({
6
+ description: "Render one of this app's transactional emails with representative dummy data, returning the subject, HTML and plain-text bodies for preview.",
7
+ schema: z.object({
8
+ id: z
9
+ .string()
10
+ .describe("Registered email id, e.g. calendar.booking-confirmed."),
11
+ }),
12
+ http: { method: "GET" },
13
+ run: async ({ id }) => {
14
+ registerCoreSystemEmails();
15
+ const rendered = renderTransactionalEmailPreview(id);
16
+ return {
17
+ id,
18
+ subject: rendered.subject,
19
+ html: rendered.html,
20
+ text: rendered.text,
21
+ };
22
+ },
23
+ });
24
+ //# sourceMappingURL=render-transactional-email-preview.js.map
@@ -0,0 +1,2 @@
1
+ export declare function authorizeTransactionalEmailRead(templateIds?: string[]): Promise<boolean>;
2
+ //# sourceMappingURL=authorize.d.ts.map
@@ -0,0 +1,10 @@
1
+ import { currentRequestUserIsOrgAdmin } from "../server/org-admin.js";
2
+ import { getTransactionalEmail } from "./registry.js";
3
+ import { registerCoreSystemEmails } from "./system-emails.js";
4
+ export async function authorizeTransactionalEmailRead(templateIds = []) {
5
+ if (!(await currentRequestUserIsOrgAdmin()))
6
+ return false;
7
+ registerCoreSystemEmails();
8
+ return templateIds.every((id) => Boolean(getTransactionalEmail(id)));
9
+ }
10
+ //# sourceMappingURL=authorize.js.map