@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
- package/corpus/templates/analytics/server/lib/emails.ts +51 -0
- package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
- package/corpus/templates/calendar/server/lib/emails.ts +167 -0
- package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
- package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/clips/actions/invite-member.ts +39 -15
- package/corpus/templates/clips/server/lib/emails.ts +256 -0
- package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
- package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
- package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
- package/corpus/templates/content/server/lib/emails.ts +70 -0
- package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
- package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
- package/corpus/templates/forms/server/lib/emails.ts +52 -0
- package/corpus/templates/forms/server/lib/response-email.ts +2 -0
- package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
- package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
- package/corpus/templates/plan/server/lib/emails.ts +67 -0
- package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
- package/corpus/templates/slides/server/lib/emails.ts +40 -0
- package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/email-catalog/actions/list-email-activity.d.ts +6 -0
- package/dist/email-catalog/actions/list-email-activity.js +15 -0
- package/dist/email-catalog/actions/list-email-engagement.d.ts +6 -0
- package/dist/email-catalog/actions/list-email-engagement.js +15 -0
- package/dist/email-catalog/actions/list-email-log.d.ts +8 -0
- package/dist/email-catalog/actions/list-email-log.js +22 -0
- package/dist/email-catalog/actions/list-transactional-emails.d.ts +23 -0
- package/dist/email-catalog/actions/list-transactional-emails.js +61 -0
- package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +10 -0
- package/dist/email-catalog/actions/render-transactional-email-preview.js +24 -0
- package/dist/email-catalog/authorize.d.ts +2 -0
- package/dist/email-catalog/authorize.js +10 -0
- package/dist/email-catalog/log.d.ts +57 -0
- package/dist/email-catalog/log.js +117 -0
- package/dist/email-catalog/provider-metrics.d.ts +34 -0
- package/dist/email-catalog/provider-metrics.js +165 -0
- package/dist/email-catalog/registry.d.ts +79 -0
- package/dist/email-catalog/registry.js +71 -0
- package/dist/email-catalog/schema.d.ts +212 -0
- package/dist/email-catalog/schema.js +49 -0
- package/dist/email-catalog/system-emails.d.ts +14 -0
- package/dist/email-catalog/system-emails.js +68 -0
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/localization/default-messages.d.ts +45 -0
- package/dist/localization/default-messages.js +45 -0
- package/dist/notifications/routes.d.ts +5 -5
- package/dist/org/handlers.js +8 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
- package/dist/provider-api/actions/provider-api.d.ts +12 -12
- package/dist/server/action-discovery.js +22 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/better-auth-instance.js +17 -2
- package/dist/server/email.d.ts +9 -0
- package/dist/server/email.js +44 -3
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- 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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
export function renderPlanCommentEmail(input: {
|
|
211
|
+
actor: string;
|
|
212
|
+
app: string;
|
|
211
213
|
planTitle: string;
|
|
212
|
-
|
|
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.
|
|
220
|
+
input.reason === "mention"
|
|
222
221
|
? "mentioned you in a comment on"
|
|
223
|
-
: input.
|
|
222
|
+
: input.reason === "plan-owner"
|
|
224
223
|
? "left a comment on your plan"
|
|
225
224
|
: "replied in a comment thread you participated in";
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
: input.
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -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,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,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,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
|