@agent-native/core 0.177.0 → 0.177.1-nightly-20260908153602
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/templates/design/e2e/helpers.ts +66 -0
- package/dist/client/agent-page/AgentTabsPage.js +1 -1
- package/dist/client/settings/SettingsTabsPage.js +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/email-catalog/actions/list-email-log.d.ts +7 -0
- package/dist/email-catalog/actions/list-email-log.js +28 -2
- package/dist/email-catalog/log.d.ts +29 -3
- package/dist/email-catalog/log.js +59 -34
- package/dist/email-catalog/schema.d.ts +54 -1
- package/dist/email-catalog/schema.js +24 -1
- package/dist/email-catalog/system-emails.d.ts +1 -0
- package/dist/email-catalog/system-emails.js +16 -1
- package/dist/file-upload/actions/upload-image.d.ts +3 -3
- package/dist/localization/default-messages.d.ts +17 -0
- package/dist/localization/default-messages.js +17 -0
- package/dist/observability/routes.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/better-auth-instance.js +2 -1
- package/dist/server/email.js +123 -18
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-native.css +11 -0
- package/package.json +1 -1
|
@@ -555,3 +555,69 @@ export async function cdpScreenshot(
|
|
|
555
555
|
const { writeFile } = await import("node:fs/promises");
|
|
556
556
|
await writeFile(filePath, Buffer.from(data, "base64"));
|
|
557
557
|
}
|
|
558
|
+
|
|
559
|
+
/** Inner markup of one node, matched by walking tag depth from its open tag —
|
|
560
|
+
* a non-greedy regex would stop at the first `</div>` of a nested child. */
|
|
561
|
+
export function elementInner(html: string, nodeId: string): string {
|
|
562
|
+
const openIndex = html.indexOf(`data-agent-native-node-id="${nodeId}"`);
|
|
563
|
+
if (openIndex < 0) throw new Error(`node ${nodeId} not found`);
|
|
564
|
+
const tagStart = html.lastIndexOf("<", openIndex);
|
|
565
|
+
const tag = /^<([a-zA-Z0-9-]+)/.exec(html.slice(tagStart))?.[1];
|
|
566
|
+
if (!tag) throw new Error(`no tag for ${nodeId}`);
|
|
567
|
+
const contentStart = html.indexOf(">", openIndex) + 1;
|
|
568
|
+
const pattern = new RegExp(`</?${tag}\\b`, "g");
|
|
569
|
+
pattern.lastIndex = contentStart;
|
|
570
|
+
let depth = 1;
|
|
571
|
+
let match: RegExpExecArray | null;
|
|
572
|
+
while ((match = pattern.exec(html))) {
|
|
573
|
+
depth += match[0].startsWith("</") ? -1 : 1;
|
|
574
|
+
if (depth === 0) return html.slice(contentStart, match.index);
|
|
575
|
+
}
|
|
576
|
+
throw new Error(`unbalanced ${tag} for ${nodeId}`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const VOID_TAGS = new Set([
|
|
580
|
+
"area",
|
|
581
|
+
"base",
|
|
582
|
+
"br",
|
|
583
|
+
"col",
|
|
584
|
+
"embed",
|
|
585
|
+
"hr",
|
|
586
|
+
"img",
|
|
587
|
+
"input",
|
|
588
|
+
"link",
|
|
589
|
+
"meta",
|
|
590
|
+
"source",
|
|
591
|
+
"track",
|
|
592
|
+
"wbr",
|
|
593
|
+
]);
|
|
594
|
+
|
|
595
|
+
/** Node ids of one node's direct children, in DOM order. A subtree-wide scan
|
|
596
|
+
* also collects the generated `<span data-an-text>` ids inside painted or
|
|
597
|
+
* padded text leaves, which are not flow children of this node. */
|
|
598
|
+
export function childNodeIds(html: string, parentId: string): string[] {
|
|
599
|
+
const ids: string[] = [];
|
|
600
|
+
let depth = 0;
|
|
601
|
+
for (const tag of elementInner(html, parentId).matchAll(
|
|
602
|
+
/<(\/?)([a-zA-Z0-9-]+)([^>]*)>/g,
|
|
603
|
+
)) {
|
|
604
|
+
const [, slash, name, attrs] = tag as unknown as [
|
|
605
|
+
string,
|
|
606
|
+
string,
|
|
607
|
+
string,
|
|
608
|
+
string,
|
|
609
|
+
];
|
|
610
|
+
if (slash === "/") {
|
|
611
|
+
depth -= 1;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (depth === 0) {
|
|
615
|
+
const id = /data-agent-native-node-id="([^"]+)"/.exec(attrs)?.[1];
|
|
616
|
+
if (id) ids.push(id);
|
|
617
|
+
}
|
|
618
|
+
if (!attrs.trimEnd().endsWith("/") && !VOID_TAGS.has(name.toLowerCase())) {
|
|
619
|
+
depth += 1;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return ids;
|
|
623
|
+
}
|
|
@@ -407,7 +407,7 @@ export function AgentTabsPage({ appName, extraTabs = [], extraTabFactories = [],
|
|
|
407
407
|
event.preventDefault();
|
|
408
408
|
selectSearchResult(results[0]);
|
|
409
409
|
}
|
|
410
|
-
}, placeholder: searchPlaceholder, "aria-label": searchPlaceholder, className: "h-8 w-full rounded-md border border-border bg-background ps-8 pe-7 text-[13px] text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-foreground/30 focus:ring-2 focus:ring-accent/40" }), query && (_jsx("button", { type: "button", onClick: () => setQuery(""), "aria-label": "Clear search", className: "absolute end-1.5 top-1/2 flex size-5 -translate-y-1/2 cursor-pointer items-center justify-center rounded text-muted-foreground hover:bg-accent/60 hover:text-foreground", children: _jsx(IconX, { className: "size-3.5" }) }))] })) : null, query.trim() ? (_jsx("div", { role: "listbox", "aria-label": "Agent search results", className: "flex flex-col gap-0.5", children: results.length === 0 ? (_jsx("p", { className: "px-2 py-6 text-center text-xs text-muted-foreground", children: "No matching items" })) : (results.map((entry) => (_jsx("button", { type: "button", role: "option", onClick: () => selectSearchResult(entry), className: "flex cursor-pointer items-start gap-2 rounded-md px-2.5 py-2 text-start text-sm text-foreground hover:bg-accent/60", children: _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: entry.label }), _jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: entry.description ?? entry.tabId })] }) }, entry.id)))) })) : (_jsx("nav", { "aria-label": "Agent sections", role: "tablist", className: "flex gap-1 overflow-x-auto sm:flex-col sm:overflow-x-visible", children: tabGroups.map((group, groupIndex) => (_jsxs("div", { className: cn("contents sm:block", groupIndex > 0 &&
|
|
410
|
+
}, placeholder: searchPlaceholder, "aria-label": searchPlaceholder, className: "agent-native-search-input h-8 w-full rounded-md border border-border bg-background ps-8 pe-7 text-[13px] text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-foreground/30 focus:ring-2 focus:ring-accent/40" }), query && (_jsx("button", { type: "button", onClick: () => setQuery(""), "aria-label": "Clear search", className: "absolute end-1.5 top-1/2 flex size-5 -translate-y-1/2 cursor-pointer items-center justify-center rounded text-muted-foreground hover:bg-accent/60 hover:text-foreground", children: _jsx(IconX, { className: "size-3.5" }) }))] })) : null, query.trim() ? (_jsx("div", { role: "listbox", "aria-label": "Agent search results", className: "flex flex-col gap-0.5", children: results.length === 0 ? (_jsx("p", { className: "px-2 py-6 text-center text-xs text-muted-foreground", children: "No matching items" })) : (results.map((entry) => (_jsx("button", { type: "button", role: "option", onClick: () => selectSearchResult(entry), className: "flex cursor-pointer items-start gap-2 rounded-md px-2.5 py-2 text-start text-sm text-foreground hover:bg-accent/60", children: _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: entry.label }), _jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: entry.description ?? entry.tabId })] }) }, entry.id)))) })) : (_jsx("nav", { "aria-label": "Agent sections", role: "tablist", className: "flex gap-1 overflow-x-auto sm:flex-col sm:overflow-x-visible", children: tabGroups.map((group, groupIndex) => (_jsxs("div", { className: cn("contents sm:block", groupIndex > 0 &&
|
|
411
411
|
"sm:mt-2 sm:border-t sm:border-border/60 sm:pt-2"), children: [group.id === "resources" && (_jsx("div", { className: "hidden px-2.5 pb-1 pt-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground/50 sm:block", children: "Agent resources" })), group.id === "agent" && (_jsx("div", { className: "hidden px-2.5 pb-1 pt-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground/50 sm:block", children: "Agent operations" })), _jsx("div", { className: "contents sm:flex sm:flex-col sm:gap-1", children: group.tabs.map((tab) => {
|
|
412
412
|
const Icon = tab.icon;
|
|
413
413
|
const selected = tab.id === selectedTab?.id;
|
|
@@ -413,7 +413,7 @@ function SettingsTabsPageContent({ general, account, team, whatsNew, extraTabs =
|
|
|
413
413
|
event.preventDefault();
|
|
414
414
|
selectEntry(results[0]);
|
|
415
415
|
}
|
|
416
|
-
}, placeholder: searchPlaceholder, "aria-label": searchPlaceholder, className: "h-8 w-full rounded-md border border-border bg-background ps-8 pe-7 text-[13px] text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-foreground/30 focus:ring-2 focus:ring-accent/40" }), query ? (_jsx("button", { type: "button", onClick: () => setQuery(""), "aria-label": "Clear search", className: "absolute end-1.5 top-1/2 flex size-5 -translate-y-1/2 items-center justify-center rounded text-muted-foreground hover:bg-accent/60 hover:text-foreground", children: _jsx(IconX, { className: "size-3.5" }) })) : null] })) : null, searching ? (_jsx("div", { role: "listbox", "aria-label": "Settings search results", className: "flex flex-col gap-0.5", children: results.length === 0 ? (_jsx("p", { className: "px-2 py-6 text-center text-[12px] text-muted-foreground", children: "No matching settings" })) : (results.map((entry) => {
|
|
416
|
+
}, placeholder: searchPlaceholder, "aria-label": searchPlaceholder, className: "agent-native-search-input h-8 w-full rounded-md border border-border bg-background ps-8 pe-7 text-[13px] text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-foreground/30 focus:ring-2 focus:ring-accent/40" }), query ? (_jsx("button", { type: "button", onClick: () => setQuery(""), "aria-label": "Clear search", className: "absolute end-1.5 top-1/2 flex size-5 -translate-y-1/2 items-center justify-center rounded text-muted-foreground hover:bg-accent/60 hover:text-foreground", children: _jsx(IconX, { className: "size-3.5" }) })) : null] })) : null, searching ? (_jsx("div", { role: "listbox", "aria-label": "Settings search results", className: "flex flex-col gap-0.5", children: results.length === 0 ? (_jsx("p", { className: "px-2 py-6 text-center text-[12px] text-muted-foreground", children: "No matching settings" })) : (results.map((entry) => {
|
|
417
417
|
const Icon = entry.icon;
|
|
418
418
|
const tab = tabs.find((candidate) => candidate.id === entry.tabId);
|
|
419
419
|
const entryHash = entry.hash?.replace(/^#/, "");
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
|
|
|
26
26
|
* Body: { update: string (base64), requestSource?: string }
|
|
27
27
|
*/
|
|
28
28
|
export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
29
|
-
ok?: undefined;
|
|
30
29
|
error: string;
|
|
30
|
+
ok?: undefined;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
33
33
|
ok: boolean;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
declare const _default: import("../../action.js").ActionDefinition<{
|
|
2
2
|
templateId?: string;
|
|
3
|
+
to?: string;
|
|
4
|
+
from?: string;
|
|
5
|
+
status?: "failed" | "sent";
|
|
6
|
+
provider?: string;
|
|
7
|
+
sinceMs?: unknown;
|
|
8
|
+
untilMs?: unknown;
|
|
3
9
|
limit?: unknown;
|
|
10
|
+
offset?: unknown;
|
|
4
11
|
}, {
|
|
5
12
|
entries: import("../log.js").EmailLogEntry[];
|
|
6
13
|
}>;
|
|
@@ -5,19 +5,45 @@ import { getRequestOrgId } from "../../server/request-context.js";
|
|
|
5
5
|
import { authorizeTransactionalEmailRead } from "../authorize.js";
|
|
6
6
|
import { listEmailLog } from "../log.js";
|
|
7
7
|
export default defineAction({
|
|
8
|
-
description: "List recent transactional email sends from this app, newest first,
|
|
8
|
+
description: "List recent transactional email sends from this app, newest first — the audit trail of every attempted send, including the raw request sent to the mail provider and its raw response. Supports filtering by registered email id, recipient/sender substring, status, provider, and a date range. Use this to answer 'did this email go out' or 'why did this email go to the wrong person'.",
|
|
9
9
|
schema: z.object({
|
|
10
10
|
templateId: z.string().optional(),
|
|
11
|
+
to: z
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("Substring match against the recipient address."),
|
|
15
|
+
from: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Substring match against the resolved sender address."),
|
|
19
|
+
status: z.enum(["sent", "failed"]).optional(),
|
|
20
|
+
provider: z.string().optional(),
|
|
21
|
+
sinceMs: z.coerce
|
|
22
|
+
.number()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Only sends at or after this Unix epoch (ms)."),
|
|
25
|
+
untilMs: z.coerce
|
|
26
|
+
.number()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Only sends at or before this Unix epoch (ms)."),
|
|
11
29
|
limit: z.coerce.number().int().min(1).max(500).default(100),
|
|
30
|
+
offset: z.coerce.number().int().min(0).default(0),
|
|
12
31
|
}),
|
|
13
32
|
http: { method: "GET" },
|
|
14
33
|
authorize: ({ templateId }) => authorizeTransactionalEmailRead(templateId ? [templateId] : []),
|
|
15
|
-
run: async ({ templateId, limit }) => ({
|
|
34
|
+
run: async ({ templateId, to, from, status, provider, sinceMs, untilMs, limit, offset, }) => ({
|
|
16
35
|
entries: await listEmailLog({
|
|
17
36
|
orgId: getRequestOrgId() ?? "",
|
|
18
37
|
app: getAppConfig().app.slug ?? "unknown",
|
|
19
38
|
templateId,
|
|
39
|
+
to,
|
|
40
|
+
from,
|
|
41
|
+
status,
|
|
42
|
+
provider,
|
|
43
|
+
sinceMs,
|
|
44
|
+
untilMs,
|
|
20
45
|
limit,
|
|
46
|
+
offset,
|
|
21
47
|
}),
|
|
22
48
|
}),
|
|
23
49
|
});
|
|
@@ -14,8 +14,15 @@ export interface RecordEmailSendArgs {
|
|
|
14
14
|
sender: string;
|
|
15
15
|
subject: string;
|
|
16
16
|
status: "sent" | "failed";
|
|
17
|
+
/** Set when the call never reached the provider (threw before/without an HTTP response). */
|
|
17
18
|
error?: string;
|
|
18
19
|
provider: string;
|
|
20
|
+
/** Exact outbound JSON body sent to the provider, credential- and attachment-body-free. */
|
|
21
|
+
requestPayload?: string;
|
|
22
|
+
/** Raw HTTP status code from the provider, when a response was received. */
|
|
23
|
+
responseStatus?: number;
|
|
24
|
+
/** Raw HTTP response body text from the provider, when a response was received. */
|
|
25
|
+
responseBody?: string;
|
|
19
26
|
}
|
|
20
27
|
/**
|
|
21
28
|
* Append one send record.
|
|
@@ -48,14 +55,33 @@ export interface EmailLogEntry {
|
|
|
48
55
|
status: string;
|
|
49
56
|
error: string | null;
|
|
50
57
|
provider: string;
|
|
58
|
+
requestPayload: string | null;
|
|
59
|
+
responseStatus: number | null;
|
|
60
|
+
responseBody: string | null;
|
|
51
61
|
createdAt: number;
|
|
52
62
|
}
|
|
53
|
-
|
|
54
|
-
export declare function listEmailLog(options: {
|
|
63
|
+
export interface ListEmailLogFilters {
|
|
55
64
|
orgId: string;
|
|
56
65
|
app: string;
|
|
57
66
|
templateId?: string;
|
|
67
|
+
/** Substring match against the recipient address. */
|
|
68
|
+
to?: string;
|
|
69
|
+
/** Substring match against the resolved sender address. */
|
|
70
|
+
from?: string;
|
|
71
|
+
status?: "sent" | "failed";
|
|
72
|
+
provider?: string;
|
|
73
|
+
/** Only sends at or after this Unix epoch (ms). */
|
|
74
|
+
sinceMs?: number;
|
|
75
|
+
/** Only sends at or before this Unix epoch (ms). */
|
|
76
|
+
untilMs?: number;
|
|
58
77
|
limit?: number;
|
|
59
|
-
|
|
78
|
+
offset?: number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Most recent sends for one app, newest first, combinably filtered — modeled
|
|
82
|
+
* on `queryAuditEvents` so this admin-facing query builds the same way every
|
|
83
|
+
* other filterable log in the framework does.
|
|
84
|
+
*/
|
|
85
|
+
export declare function listEmailLog(options: ListEmailLogFilters): Promise<EmailLogEntry[]>;
|
|
60
86
|
/** Provider category that is safe to query for one organization only. */
|
|
61
87
|
export declare function getScopedEmailProviderCategory(templateId: string, orgId: string): string;
|
|
@@ -11,35 +11,25 @@ import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db
|
|
|
11
11
|
import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
|
|
12
12
|
import { getRequestOrgId } from "../server/request-context.js";
|
|
13
13
|
let _initPromise;
|
|
14
|
+
const ADDITIVE_TEXT_COLUMNS = ["request_payload", "response_body"];
|
|
14
15
|
export async function ensureTable() {
|
|
15
16
|
if (!_initPromise) {
|
|
16
17
|
_initPromise = (async () => {
|
|
17
|
-
const { EMAIL_LOG_CREATE_SQL, EMAIL_LOG_ORG_APP_INDEX_SQL, EMAIL_LOG_TEMPLATE_INDEX_SQL, } = await import("./schema.js");
|
|
18
|
-
const client = getDbExec();
|
|
18
|
+
const { EMAIL_LOG_CREATE_SQL, EMAIL_LOG_ORG_APP_INDEX_SQL, EMAIL_LOG_TEMPLATE_INDEX_SQL, EMAIL_LOG_ORG_STATUS_INDEX_SQL, EMAIL_LOG_ORG_PROVIDER_INDEX_SQL, } = await import("./schema.js");
|
|
19
19
|
// Generic INTEGER maps to BIGINT on Postgres, which millisecond
|
|
20
20
|
// timestamps need.
|
|
21
21
|
const createSql = EMAIL_LOG_CREATE_SQL.replace(/\bINTEGER\b/g, "BIGINT");
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
await
|
|
27
|
-
await ensureIndexExists("email_log_org_app_created_idx", EMAIL_LOG_ORG_APP_INDEX_SQL);
|
|
28
|
-
return;
|
|
22
|
+
await ensureTableExists("email_log", createSql);
|
|
23
|
+
await widenIntColumnsToBigInt("email_log", ["created_at"]);
|
|
24
|
+
await ensureColumnExists("email_log", "org_id", "ALTER TABLE email_log ADD COLUMN IF NOT EXISTS org_id TEXT");
|
|
25
|
+
for (const column of ADDITIVE_TEXT_COLUMNS) {
|
|
26
|
+
await ensureColumnExists("email_log", column, `ALTER TABLE email_log ADD COLUMN IF NOT EXISTS ${column} TEXT`);
|
|
29
27
|
}
|
|
30
|
-
await
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const message = String(error?.message ?? error);
|
|
36
|
-
if (!/already exists|duplicate column name/i.test(message)) {
|
|
37
|
-
throw error;
|
|
38
|
-
}
|
|
39
|
-
console.info("[agent-native:email] email_log.org_id already exists during local bootstrap");
|
|
40
|
-
}
|
|
41
|
-
await client.execute(EMAIL_LOG_TEMPLATE_INDEX_SQL);
|
|
42
|
-
await client.execute(EMAIL_LOG_ORG_APP_INDEX_SQL);
|
|
28
|
+
await ensureColumnExists("email_log", "response_status", "ALTER TABLE email_log ADD COLUMN IF NOT EXISTS response_status BIGINT");
|
|
29
|
+
await ensureIndexExists("email_log_template_created_idx", EMAIL_LOG_TEMPLATE_INDEX_SQL);
|
|
30
|
+
await ensureIndexExists("email_log_org_app_created_idx", EMAIL_LOG_ORG_APP_INDEX_SQL);
|
|
31
|
+
await ensureIndexExists("email_log_org_status_created_idx", EMAIL_LOG_ORG_STATUS_INDEX_SQL);
|
|
32
|
+
await ensureIndexExists("email_log_org_provider_created_idx", EMAIL_LOG_ORG_PROVIDER_INDEX_SQL);
|
|
43
33
|
})().catch((error) => {
|
|
44
34
|
// Don't memoize a failed bootstrap — the next send should retry rather
|
|
45
35
|
// than log nothing forever.
|
|
@@ -63,8 +53,8 @@ export async function recordEmailSend(args) {
|
|
|
63
53
|
const orgId = args.orgId ?? getRequestOrgId() ?? null;
|
|
64
54
|
await getDbExec().execute({
|
|
65
55
|
sql: `INSERT INTO email_log
|
|
66
|
-
(id, org_id, template_id, app, recipient, sender, subject, status, error, provider, created_at)
|
|
67
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
56
|
+
(id, org_id, template_id, app, recipient, sender, subject, status, error, provider, request_payload, response_status, response_body, created_at)
|
|
57
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
68
58
|
args: [
|
|
69
59
|
randomUUID(),
|
|
70
60
|
orgId,
|
|
@@ -76,6 +66,9 @@ export async function recordEmailSend(args) {
|
|
|
76
66
|
args.status,
|
|
77
67
|
args.error ?? null,
|
|
78
68
|
args.provider,
|
|
69
|
+
args.requestPayload ?? null,
|
|
70
|
+
args.responseStatus ?? null,
|
|
71
|
+
args.responseBody ?? null,
|
|
79
72
|
Date.now(),
|
|
80
73
|
],
|
|
81
74
|
});
|
|
@@ -108,20 +101,49 @@ export async function getEmailSendStats(since, app, orgId) {
|
|
|
108
101
|
lastSentAt: row.last_sent_at == null ? null : Number(row.last_sent_at),
|
|
109
102
|
}));
|
|
110
103
|
}
|
|
111
|
-
|
|
104
|
+
const LOG_COLUMNS = "id, template_id, app, recipient, sender, subject, status, error, provider, " +
|
|
105
|
+
"request_payload, response_status, response_body, created_at";
|
|
106
|
+
/**
|
|
107
|
+
* Most recent sends for one app, newest first, combinably filtered — modeled
|
|
108
|
+
* on `queryAuditEvents` so this admin-facing query builds the same way every
|
|
109
|
+
* other filterable log in the framework does.
|
|
110
|
+
*/
|
|
112
111
|
export async function listEmailLog(options) {
|
|
113
112
|
await ensureTable();
|
|
113
|
+
const where = ["org_id = ?", "app = ?"];
|
|
114
|
+
const args = [options.orgId, options.app];
|
|
115
|
+
const push = (clause, value) => {
|
|
116
|
+
where.push(clause);
|
|
117
|
+
args.push(value);
|
|
118
|
+
};
|
|
119
|
+
if (options.templateId)
|
|
120
|
+
push("template_id = ?", options.templateId);
|
|
121
|
+
if (options.status)
|
|
122
|
+
push("status = ?", options.status);
|
|
123
|
+
if (options.provider)
|
|
124
|
+
push("provider = ?", options.provider);
|
|
125
|
+
if (options.to)
|
|
126
|
+
push("recipient LIKE ?", `%${options.to}%`);
|
|
127
|
+
if (options.from)
|
|
128
|
+
push("sender LIKE ?", `%${options.from}%`);
|
|
129
|
+
if (typeof options.sinceMs === "number") {
|
|
130
|
+
push("created_at >= ?", Math.floor(options.sinceMs));
|
|
131
|
+
}
|
|
132
|
+
if (typeof options.untilMs === "number") {
|
|
133
|
+
push("created_at <= ?", Math.floor(options.untilMs));
|
|
134
|
+
}
|
|
114
135
|
const limit = Math.min(Math.max(options.limit ?? 100, 1), 500);
|
|
115
|
-
const
|
|
116
|
-
? `WHERE org_id = ? AND app = ? AND template_id = ?`
|
|
117
|
-
: `WHERE org_id = ? AND app = ?`;
|
|
118
|
-
const args = options.templateId
|
|
119
|
-
? [options.orgId, options.app, options.templateId, limit]
|
|
120
|
-
: [options.orgId, options.app, limit];
|
|
136
|
+
const offset = Math.max(0, Math.floor(options.offset ?? 0));
|
|
121
137
|
const { rows } = await getDbExec().execute({
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
// `id DESC` breaks ties on `created_at` (millisecond resolution, so
|
|
139
|
+
// concurrent/bulk sends can share a timestamp) — without it, tied rows
|
|
140
|
+
// can sort differently across page requests and the Send log UI would
|
|
141
|
+
// skip or duplicate entries when paging.
|
|
142
|
+
sql: `SELECT ${LOG_COLUMNS} FROM email_log
|
|
143
|
+
WHERE ${where.join(" AND ")}
|
|
144
|
+
ORDER BY created_at DESC, id DESC
|
|
145
|
+
LIMIT ? OFFSET ?`,
|
|
146
|
+
args: [...args, limit, offset],
|
|
125
147
|
});
|
|
126
148
|
return rows.map((row) => ({
|
|
127
149
|
id: String(row.id),
|
|
@@ -133,6 +155,9 @@ export async function listEmailLog(options) {
|
|
|
133
155
|
status: String(row.status),
|
|
134
156
|
error: row.error == null ? null : String(row.error),
|
|
135
157
|
provider: String(row.provider),
|
|
158
|
+
requestPayload: row.request_payload == null ? null : String(row.request_payload),
|
|
159
|
+
responseStatus: row.response_status == null ? null : Number(row.response_status),
|
|
160
|
+
responseBody: row.response_body == null ? null : String(row.response_body),
|
|
136
161
|
createdAt: Number(row.created_at),
|
|
137
162
|
}));
|
|
138
163
|
}
|
|
@@ -186,6 +186,57 @@ export declare const emailLog: import("drizzle-orm/pg-core").PgTableWithColumns<
|
|
|
186
186
|
identity: undefined;
|
|
187
187
|
generated: undefined;
|
|
188
188
|
}, {}, {}>;
|
|
189
|
+
requestPayload: import("drizzle-orm/pg-core").PgColumn<{
|
|
190
|
+
name: "request_payload";
|
|
191
|
+
tableName: "email_log";
|
|
192
|
+
dataType: "string";
|
|
193
|
+
columnType: "PgText";
|
|
194
|
+
data: string;
|
|
195
|
+
driverParam: string;
|
|
196
|
+
notNull: false;
|
|
197
|
+
hasDefault: false;
|
|
198
|
+
isPrimaryKey: false;
|
|
199
|
+
isAutoincrement: false;
|
|
200
|
+
hasRuntimeDefault: false;
|
|
201
|
+
enumValues: [string, ...string[]];
|
|
202
|
+
baseColumn: never;
|
|
203
|
+
identity: undefined;
|
|
204
|
+
generated: undefined;
|
|
205
|
+
}, {}, {}>;
|
|
206
|
+
responseStatus: import("drizzle-orm/pg-core").PgColumn<{
|
|
207
|
+
name: "response_status";
|
|
208
|
+
tableName: "email_log";
|
|
209
|
+
dataType: "number";
|
|
210
|
+
columnType: "PgBigInt53";
|
|
211
|
+
data: number;
|
|
212
|
+
driverParam: string | number;
|
|
213
|
+
notNull: false;
|
|
214
|
+
hasDefault: false;
|
|
215
|
+
isPrimaryKey: false;
|
|
216
|
+
isAutoincrement: false;
|
|
217
|
+
hasRuntimeDefault: false;
|
|
218
|
+
enumValues: undefined;
|
|
219
|
+
baseColumn: never;
|
|
220
|
+
identity: undefined;
|
|
221
|
+
generated: undefined;
|
|
222
|
+
}, {}, {}>;
|
|
223
|
+
responseBody: import("drizzle-orm/pg-core").PgColumn<{
|
|
224
|
+
name: "response_body";
|
|
225
|
+
tableName: "email_log";
|
|
226
|
+
dataType: "string";
|
|
227
|
+
columnType: "PgText";
|
|
228
|
+
data: string;
|
|
229
|
+
driverParam: string;
|
|
230
|
+
notNull: false;
|
|
231
|
+
hasDefault: false;
|
|
232
|
+
isPrimaryKey: false;
|
|
233
|
+
isAutoincrement: false;
|
|
234
|
+
hasRuntimeDefault: false;
|
|
235
|
+
enumValues: [string, ...string[]];
|
|
236
|
+
baseColumn: never;
|
|
237
|
+
identity: undefined;
|
|
238
|
+
generated: undefined;
|
|
239
|
+
}, {}, {}>;
|
|
189
240
|
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
190
241
|
name: "created_at";
|
|
191
242
|
tableName: "email_log";
|
|
@@ -206,6 +257,8 @@ export declare const emailLog: import("drizzle-orm/pg-core").PgTableWithColumns<
|
|
|
206
257
|
};
|
|
207
258
|
dialect: 'pg';
|
|
208
259
|
}>;
|
|
209
|
-
export declare const EMAIL_LOG_CREATE_SQL = "CREATE TABLE IF NOT EXISTS email_log (\n id TEXT PRIMARY KEY,\n org_id TEXT,\n template_id TEXT,\n app TEXT,\n recipient TEXT NOT NULL,\n sender TEXT NOT NULL,\n subject TEXT NOT NULL,\n status TEXT NOT NULL,\n error TEXT,\n provider TEXT NOT NULL,\n created_at INTEGER NOT NULL\n)";
|
|
260
|
+
export declare const EMAIL_LOG_CREATE_SQL = "CREATE TABLE IF NOT EXISTS email_log (\n id TEXT PRIMARY KEY,\n org_id TEXT,\n template_id TEXT,\n app TEXT,\n recipient TEXT NOT NULL,\n sender TEXT NOT NULL,\n subject TEXT NOT NULL,\n status TEXT NOT NULL,\n error TEXT,\n provider TEXT NOT NULL,\n request_payload TEXT,\n response_status INTEGER,\n response_body TEXT,\n created_at INTEGER NOT NULL\n)";
|
|
210
261
|
export declare const EMAIL_LOG_TEMPLATE_INDEX_SQL = "CREATE INDEX IF NOT EXISTS email_log_template_created_idx\n ON email_log (template_id, created_at)";
|
|
211
262
|
export declare const EMAIL_LOG_ORG_APP_INDEX_SQL = "CREATE INDEX IF NOT EXISTS email_log_org_app_created_idx\n ON email_log (org_id, app, created_at)";
|
|
263
|
+
export declare const EMAIL_LOG_ORG_STATUS_INDEX_SQL = "CREATE INDEX IF NOT EXISTS email_log_org_status_created_idx\n ON email_log (org_id, status, created_at)";
|
|
264
|
+
export declare const EMAIL_LOG_ORG_PROVIDER_INDEX_SQL = "CREATE INDEX IF NOT EXISTS email_log_org_provider_created_idx\n ON email_log (org_id, provider, created_at)";
|
|
@@ -28,10 +28,26 @@ export const emailLog = table("email_log", {
|
|
|
28
28
|
subject: text("subject").notNull(),
|
|
29
29
|
/** "sent" once the provider accepted it, or "failed". Never optimistic. */
|
|
30
30
|
status: text("status", { enum: ["sent", "failed"] }).notNull(),
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Error text when the call never reached the provider or threw before/
|
|
33
|
+
* outside getting an HTTP response (network error, timeout/abort, credential
|
|
34
|
+
* resolution failure). Distinct from `responseStatus`/`responseBody`, which
|
|
35
|
+
* capture a provider response the request DID reach, so "we never reached
|
|
36
|
+
* the provider" and "the provider rejected it" stay visibly different.
|
|
37
|
+
*/
|
|
32
38
|
error: text("error"),
|
|
33
39
|
/** "resend" | "sendgrid" | "dev". */
|
|
34
40
|
provider: text("provider").notNull(),
|
|
41
|
+
/**
|
|
42
|
+
* Exact outbound JSON body sent to the provider, minus the Authorization
|
|
43
|
+
* header (the only secret in the request) and any attachment `content`
|
|
44
|
+
* bytes (large, no diagnostic value for "who did this go to").
|
|
45
|
+
*/
|
|
46
|
+
requestPayload: text("request_payload"),
|
|
47
|
+
/** Raw HTTP status code from the provider, when a response was received. */
|
|
48
|
+
responseStatus: bigint("response_status", { mode: "number" }),
|
|
49
|
+
/** Raw HTTP response body text from the provider, when a response was received. */
|
|
50
|
+
responseBody: text("response_body"),
|
|
35
51
|
createdAt: bigint("created_at", { mode: "number" }).notNull(),
|
|
36
52
|
});
|
|
37
53
|
export const EMAIL_LOG_CREATE_SQL = `CREATE TABLE IF NOT EXISTS email_log (
|
|
@@ -45,9 +61,16 @@ export const EMAIL_LOG_CREATE_SQL = `CREATE TABLE IF NOT EXISTS email_log (
|
|
|
45
61
|
status TEXT NOT NULL,
|
|
46
62
|
error TEXT,
|
|
47
63
|
provider TEXT NOT NULL,
|
|
64
|
+
request_payload TEXT,
|
|
65
|
+
response_status INTEGER,
|
|
66
|
+
response_body TEXT,
|
|
48
67
|
created_at INTEGER NOT NULL
|
|
49
68
|
)`;
|
|
50
69
|
export const EMAIL_LOG_TEMPLATE_INDEX_SQL = `CREATE INDEX IF NOT EXISTS email_log_template_created_idx
|
|
51
70
|
ON email_log (template_id, created_at)`;
|
|
52
71
|
export const EMAIL_LOG_ORG_APP_INDEX_SQL = `CREATE INDEX IF NOT EXISTS email_log_org_app_created_idx
|
|
53
72
|
ON email_log (org_id, app, created_at)`;
|
|
73
|
+
export const EMAIL_LOG_ORG_STATUS_INDEX_SQL = `CREATE INDEX IF NOT EXISTS email_log_org_status_created_idx
|
|
74
|
+
ON email_log (org_id, status, created_at)`;
|
|
75
|
+
export const EMAIL_LOG_ORG_PROVIDER_INDEX_SQL = `CREATE INDEX IF NOT EXISTS email_log_org_provider_created_idx
|
|
76
|
+
ON email_log (org_id, provider, created_at)`;
|
|
@@ -10,4 +10,5 @@
|
|
|
10
10
|
export declare const CORE_INVITE_EMAIL_ID = "core.organization-invite";
|
|
11
11
|
export declare const CORE_VERIFY_SIGNUP_EMAIL_ID = "core.verify-signup";
|
|
12
12
|
export declare const CORE_RESET_PASSWORD_EMAIL_ID = "core.reset-password";
|
|
13
|
+
export declare const CORE_MAGIC_LINK_EMAIL_ID = "core.magic-link";
|
|
13
14
|
export declare function registerCoreSystemEmails(): void;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* site so the emails appear in every app's catalog without each template
|
|
8
8
|
* remembering to opt in.
|
|
9
9
|
*/
|
|
10
|
-
import { renderInviteEmail, renderResetPasswordEmail, renderVerifySignupEmail, } from "../server/email-templates.js";
|
|
10
|
+
import { renderInviteEmail, renderMagicLinkEmail, renderResetPasswordEmail, renderVerifySignupEmail, } from "../server/email-templates.js";
|
|
11
11
|
import { defineTransactionalEmail } from "./registry.js";
|
|
12
12
|
/** Obviously-fake sample data — these render in a preview pane, never send. */
|
|
13
13
|
const SAMPLE_URL = "https://example.com/accept/sample-token";
|
|
@@ -15,6 +15,7 @@ const SAMPLE_EMAIL = "sam.rivera@example.com";
|
|
|
15
15
|
export const CORE_INVITE_EMAIL_ID = "core.organization-invite";
|
|
16
16
|
export const CORE_VERIFY_SIGNUP_EMAIL_ID = "core.verify-signup";
|
|
17
17
|
export const CORE_RESET_PASSWORD_EMAIL_ID = "core.reset-password";
|
|
18
|
+
export const CORE_MAGIC_LINK_EMAIL_ID = "core.magic-link";
|
|
18
19
|
let registered = false;
|
|
19
20
|
export function registerCoreSystemEmails() {
|
|
20
21
|
if (registered)
|
|
@@ -64,4 +65,18 @@ export function registerCoreSystemEmails() {
|
|
|
64
65
|
resetUrl: SAMPLE_URL,
|
|
65
66
|
}),
|
|
66
67
|
});
|
|
68
|
+
defineTransactionalEmail({
|
|
69
|
+
id: CORE_MAGIC_LINK_EMAIL_ID,
|
|
70
|
+
app: "core",
|
|
71
|
+
name: "Magic link sign-in",
|
|
72
|
+
trigger: "A user submits their email on the sign-in screen while magic-link is the active login mode.",
|
|
73
|
+
recipientLabel: "Sign-in address",
|
|
74
|
+
recipient: "The address typed into the sign-in form.",
|
|
75
|
+
senderLabel: "Default, app-branded",
|
|
76
|
+
sender: "The configured EMAIL_FROM, branded with the app name the sign-in happened in.",
|
|
77
|
+
preview: () => renderMagicLinkEmail({
|
|
78
|
+
email: SAMPLE_EMAIL,
|
|
79
|
+
magicLinkUrl: SAMPLE_URL,
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
67
82
|
}
|
|
@@ -3,25 +3,25 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
3
3
|
url?: string;
|
|
4
4
|
filename?: string;
|
|
5
5
|
}, {
|
|
6
|
-
id?: undefined;
|
|
7
6
|
error: string;
|
|
8
7
|
configured?: undefined;
|
|
9
8
|
connectPath?: undefined;
|
|
10
9
|
url?: undefined;
|
|
10
|
+
id?: undefined;
|
|
11
11
|
provider?: undefined;
|
|
12
12
|
} | {
|
|
13
|
-
id?: undefined;
|
|
14
13
|
error: string;
|
|
15
14
|
configured: boolean;
|
|
16
15
|
connectPath: string;
|
|
17
16
|
url?: undefined;
|
|
17
|
+
id?: undefined;
|
|
18
18
|
provider?: undefined;
|
|
19
19
|
} | {
|
|
20
|
-
error?: undefined;
|
|
21
20
|
configured?: undefined;
|
|
22
21
|
connectPath?: undefined;
|
|
23
22
|
url: string;
|
|
24
23
|
id: string;
|
|
25
24
|
provider: string;
|
|
25
|
+
error?: undefined;
|
|
26
26
|
}>;
|
|
27
27
|
export default _default;
|
|
@@ -291,6 +291,23 @@ declare const messages: {
|
|
|
291
291
|
status: string;
|
|
292
292
|
opens: string;
|
|
293
293
|
lastEvent: string;
|
|
294
|
+
app: string;
|
|
295
|
+
sendLogTitle: string;
|
|
296
|
+
sendLogTemplate: string;
|
|
297
|
+
sendLogProvider: string;
|
|
298
|
+
sendLogResponseStatus: string;
|
|
299
|
+
sendLogError: string;
|
|
300
|
+
sendLogToFilter: string;
|
|
301
|
+
sendLogFromFilter: string;
|
|
302
|
+
sendLogAllStatuses: string;
|
|
303
|
+
sendLogSent: string;
|
|
304
|
+
sendLogFailed: string;
|
|
305
|
+
sendLogAllProviders: string;
|
|
306
|
+
sendLogClearFilters: string;
|
|
307
|
+
sendLogEmpty: string;
|
|
308
|
+
sendLogTimestamp: string;
|
|
309
|
+
sendLogPrevious: string;
|
|
310
|
+
sendLogNext: string;
|
|
294
311
|
};
|
|
295
312
|
pages: {
|
|
296
313
|
appsDescription: string;
|
|
@@ -297,6 +297,23 @@ const messages = {
|
|
|
297
297
|
status: "Status",
|
|
298
298
|
opens: "Opens",
|
|
299
299
|
lastEvent: "Last event",
|
|
300
|
+
app: "App",
|
|
301
|
+
sendLogTitle: "Send log",
|
|
302
|
+
sendLogTemplate: "Template",
|
|
303
|
+
sendLogProvider: "Provider",
|
|
304
|
+
sendLogResponseStatus: "Response status",
|
|
305
|
+
sendLogError: "Error",
|
|
306
|
+
sendLogToFilter: "To contains…",
|
|
307
|
+
sendLogFromFilter: "From contains…",
|
|
308
|
+
sendLogAllStatuses: "All statuses",
|
|
309
|
+
sendLogSent: "Sent",
|
|
310
|
+
sendLogFailed: "Failed",
|
|
311
|
+
sendLogAllProviders: "All providers",
|
|
312
|
+
sendLogClearFilters: "Clear filters",
|
|
313
|
+
sendLogEmpty: "No sends match these filters in this date range.",
|
|
314
|
+
sendLogTimestamp: "Timestamp",
|
|
315
|
+
sendLogPrevious: "Previous",
|
|
316
|
+
sendLogNext: "Next",
|
|
300
317
|
},
|
|
301
318
|
pages: {
|
|
302
319
|
appsDescription: "Open workspace apps and start new app creation from Dispatch.",
|
|
@@ -55,13 +55,13 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
55
55
|
} | {
|
|
56
56
|
summary?: undefined;
|
|
57
57
|
spans?: undefined;
|
|
58
|
-
id?: undefined;
|
|
59
58
|
error: any;
|
|
59
|
+
id?: undefined;
|
|
60
60
|
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
62
|
summary?: undefined;
|
|
63
63
|
spans?: undefined;
|
|
64
|
+
ok: boolean;
|
|
64
65
|
id?: undefined;
|
|
65
66
|
error?: undefined;
|
|
66
|
-
ok: boolean;
|
|
67
67
|
}>>;
|
|
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
48
48
|
}>;
|
|
49
49
|
/** DELETE /_agent-native/resources/:id — delete a resource */
|
|
50
50
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
51
|
-
ok?: undefined;
|
|
52
51
|
error: string;
|
|
52
|
+
ok?: undefined;
|
|
53
53
|
} | {
|
|
54
54
|
error?: undefined;
|
|
55
55
|
ok: boolean;
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -34,16 +34,16 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
|
|
|
34
34
|
/** POST /_agent-native/secrets/:key — write a secret. */
|
|
35
35
|
export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
36
36
|
error: string;
|
|
37
|
-
ok?: undefined;
|
|
38
37
|
status?: undefined;
|
|
38
|
+
ok?: undefined;
|
|
39
39
|
} | {
|
|
40
40
|
ok: boolean;
|
|
41
41
|
status: string;
|
|
42
42
|
error?: undefined;
|
|
43
43
|
} | {
|
|
44
|
-
ok?: undefined;
|
|
45
44
|
error: string;
|
|
46
45
|
removed?: undefined;
|
|
46
|
+
ok?: undefined;
|
|
47
47
|
} | {
|
|
48
48
|
ok: boolean;
|
|
49
49
|
removed: boolean;
|
|
@@ -54,9 +54,9 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
|
|
|
54
54
|
* or the current stored value without changing anything.
|
|
55
55
|
*/
|
|
56
56
|
export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
57
|
-
ok?: undefined;
|
|
58
57
|
error: string;
|
|
59
58
|
note?: undefined;
|
|
59
|
+
ok?: undefined;
|
|
60
60
|
} | {
|
|
61
61
|
ok: boolean;
|
|
62
62
|
note?: undefined;
|
|
@@ -24,7 +24,7 @@ import { getAppConfig } from "../app-config/index.js";
|
|
|
24
24
|
import { TEMPLATES } from "../cli/templates-meta.js";
|
|
25
25
|
import { getDbExec } from "../db/client.js";
|
|
26
26
|
import { getRuntimeDatabaseUrl, getPgliteClient, isPgliteUrl, loadPgliteDrizzle, pgPoolOptions, neonPoolOptions, guardNeonPool, sharedDbPool, onSharedDbPoolsClosed, onSharedDbPoolReplaced, } from "../db/client.js";
|
|
27
|
-
import { CORE_RESET_PASSWORD_EMAIL_ID, CORE_VERIFY_SIGNUP_EMAIL_ID, } from "../email-catalog/system-emails.js";
|
|
27
|
+
import { CORE_MAGIC_LINK_EMAIL_ID, CORE_RESET_PASSWORD_EMAIL_ID, CORE_VERIFY_SIGNUP_EMAIL_ID, } from "../email-catalog/system-emails.js";
|
|
28
28
|
import { saveOAuthTokens } from "../oauth-tokens/store.js";
|
|
29
29
|
import { acceptPendingInvitationsForEmail } from "../org/accept-pending.js";
|
|
30
30
|
import { getAuthEmailForUserId, getRequiredAuthProviderForEmail, } from "../org/auth-policy.js";
|
|
@@ -1137,6 +1137,7 @@ async function createBetterAuthInstance(config) {
|
|
|
1137
1137
|
text,
|
|
1138
1138
|
appSender,
|
|
1139
1139
|
disableClickTracking: true,
|
|
1140
|
+
templateId: CORE_MAGIC_LINK_EMAIL_ID,
|
|
1140
1141
|
});
|
|
1141
1142
|
},
|
|
1142
1143
|
});
|
package/dist/server/email.js
CHANGED
|
@@ -179,6 +179,75 @@ function resolveAppSender(configuredFrom, appSender) {
|
|
|
179
179
|
replyTo: appSender.replyTo,
|
|
180
180
|
};
|
|
181
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Thrown when a provider request completed (we got an HTTP response) but the
|
|
184
|
+
* status was not 2xx. Carries the raw request/response so the audit log can
|
|
185
|
+
* distinguish "the provider rejected it" from a thrown error that never
|
|
186
|
+
* reached the provider (network failure, timeout, credential resolution).
|
|
187
|
+
*/
|
|
188
|
+
class EmailProviderError extends Error {
|
|
189
|
+
provider;
|
|
190
|
+
from;
|
|
191
|
+
requestPayload;
|
|
192
|
+
responseStatus;
|
|
193
|
+
responseBody;
|
|
194
|
+
constructor(message, details) {
|
|
195
|
+
super(message);
|
|
196
|
+
this.name = "EmailProviderError";
|
|
197
|
+
this.provider = details.provider;
|
|
198
|
+
this.from = details.from;
|
|
199
|
+
this.requestPayload = details.requestPayload;
|
|
200
|
+
this.responseStatus = details.responseStatus;
|
|
201
|
+
this.responseBody = details.responseBody;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Serialize a provider payload for the audit log, stripping attachment bytes
|
|
206
|
+
* and message bodies. Attachment `content` is base64 file data with no
|
|
207
|
+
* diagnostic value for "who did this go to". The HTML/text body is omitted
|
|
208
|
+
* too: transactional emails routinely embed a one-time magic-link, password-
|
|
209
|
+
* reset, or verification URL, which is bearer-token-equivalent — logging it
|
|
210
|
+
* verbatim would let anyone with `email_log` read access sign in as the
|
|
211
|
+
* recipient. `subject` and `templateId` already identify what was sent.
|
|
212
|
+
*/
|
|
213
|
+
const MAX_LOGGED_TEXT_LENGTH = 8_000;
|
|
214
|
+
function truncateForLog(value) {
|
|
215
|
+
if (value.length <= MAX_LOGGED_TEXT_LENGTH)
|
|
216
|
+
return value;
|
|
217
|
+
const omitted = value.length - MAX_LOGGED_TEXT_LENGTH;
|
|
218
|
+
return `${value.slice(0, MAX_LOGGED_TEXT_LENGTH)}<truncated, ${omitted} more characters>`;
|
|
219
|
+
}
|
|
220
|
+
function omittedBodyMarker(value) {
|
|
221
|
+
return typeof value === "string" ? `<omitted, ${value.length} chars>` : value;
|
|
222
|
+
}
|
|
223
|
+
function redactPayloadForLog(payload) {
|
|
224
|
+
const loggable = { ...payload };
|
|
225
|
+
// Resend shape: top-level `html` / `text` fields.
|
|
226
|
+
if ("html" in loggable)
|
|
227
|
+
loggable.html = omittedBodyMarker(loggable.html);
|
|
228
|
+
if ("text" in loggable)
|
|
229
|
+
loggable.text = omittedBodyMarker(loggable.text);
|
|
230
|
+
// SendGrid shape: `content: [{ type, value }]`.
|
|
231
|
+
if (Array.isArray(loggable.content)) {
|
|
232
|
+
loggable.content = loggable.content.map((entry) => ({ ...entry, value: omittedBodyMarker(entry.value) }));
|
|
233
|
+
}
|
|
234
|
+
if (Array.isArray(loggable.attachments) && loggable.attachments.length) {
|
|
235
|
+
loggable.attachments = loggable.attachments.map(({ content: _content, ...rest }) => ({
|
|
236
|
+
...rest,
|
|
237
|
+
contentOmitted: true,
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
return truncateForLog(JSON.stringify(loggable));
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* A response body that failed to read is not the same as a genuinely empty
|
|
244
|
+
* one — collapsing both to "" would make a truncated/aborted read look like a
|
|
245
|
+
* provider that legitimately returned nothing.
|
|
246
|
+
*/
|
|
247
|
+
function unreadableResponseBody(error) {
|
|
248
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
249
|
+
return `<response body unreadable: ${message}>`;
|
|
250
|
+
}
|
|
182
251
|
async function deliverEmail(args, signal) {
|
|
183
252
|
const config = await resolveEmailTransport(args.useDeploymentCredentials);
|
|
184
253
|
signal?.throwIfAborted();
|
|
@@ -218,6 +287,7 @@ async function deliverEmail(args, signal) {
|
|
|
218
287
|
headers["References"] = args.references;
|
|
219
288
|
if (Object.keys(headers).length)
|
|
220
289
|
payload.headers = headers;
|
|
290
|
+
const requestPayload = redactPayloadForLog(payload);
|
|
221
291
|
const res = await fetch("https://api.resend.com/emails", {
|
|
222
292
|
method: "POST",
|
|
223
293
|
headers: {
|
|
@@ -227,11 +297,23 @@ async function deliverEmail(args, signal) {
|
|
|
227
297
|
body: JSON.stringify(payload),
|
|
228
298
|
signal,
|
|
229
299
|
});
|
|
300
|
+
const responseBody = truncateForLog(await res.text().catch(unreadableResponseBody));
|
|
230
301
|
if (!res.ok) {
|
|
231
|
-
|
|
232
|
-
|
|
302
|
+
throw new EmailProviderError(`Resend error ${res.status}: ${responseBody}`, {
|
|
303
|
+
provider,
|
|
304
|
+
from,
|
|
305
|
+
requestPayload,
|
|
306
|
+
responseStatus: res.status,
|
|
307
|
+
responseBody,
|
|
308
|
+
});
|
|
233
309
|
}
|
|
234
|
-
return {
|
|
310
|
+
return {
|
|
311
|
+
provider,
|
|
312
|
+
from,
|
|
313
|
+
requestPayload,
|
|
314
|
+
responseStatus: res.status,
|
|
315
|
+
responseBody,
|
|
316
|
+
};
|
|
235
317
|
}
|
|
236
318
|
if (provider === "sendgrid") {
|
|
237
319
|
const personalization = {
|
|
@@ -288,6 +370,7 @@ async function deliverEmail(args, signal) {
|
|
|
288
370
|
content_id: a.contentId,
|
|
289
371
|
}));
|
|
290
372
|
}
|
|
373
|
+
const requestPayload = redactPayloadForLog(sgPayload);
|
|
291
374
|
const res = await fetch("https://api.sendgrid.com/v3/mail/send", {
|
|
292
375
|
method: "POST",
|
|
293
376
|
headers: {
|
|
@@ -297,11 +380,23 @@ async function deliverEmail(args, signal) {
|
|
|
297
380
|
body: JSON.stringify(sgPayload),
|
|
298
381
|
signal,
|
|
299
382
|
});
|
|
383
|
+
const responseBody = truncateForLog(await res.text().catch(unreadableResponseBody));
|
|
300
384
|
if (!res.ok) {
|
|
301
|
-
|
|
302
|
-
|
|
385
|
+
throw new EmailProviderError(`SendGrid error ${res.status}: ${responseBody}`, {
|
|
386
|
+
provider,
|
|
387
|
+
from,
|
|
388
|
+
requestPayload,
|
|
389
|
+
responseStatus: res.status,
|
|
390
|
+
responseBody,
|
|
391
|
+
});
|
|
303
392
|
}
|
|
304
|
-
return {
|
|
393
|
+
return {
|
|
394
|
+
provider,
|
|
395
|
+
from,
|
|
396
|
+
requestPayload,
|
|
397
|
+
responseStatus: res.status,
|
|
398
|
+
responseBody,
|
|
399
|
+
};
|
|
305
400
|
}
|
|
306
401
|
// Dev fallback — no provider configured. Logging the full body exposes
|
|
307
402
|
// reset tokens, so only do it outside production. In production, refuse
|
|
@@ -320,33 +415,43 @@ async function deliverEmail(args, signal) {
|
|
|
320
415
|
* provider branch so a new transport cannot be added without being logged.
|
|
321
416
|
*/
|
|
322
417
|
async function sendEmailWithSignal(args, signal) {
|
|
418
|
+
const baseRecord = {
|
|
419
|
+
templateId: args.templateId,
|
|
420
|
+
app: args.app ?? getAppConfig().app.slug ?? "unknown",
|
|
421
|
+
orgId: args.orgId ?? getRequestOrgId(),
|
|
422
|
+
recipient: args.to,
|
|
423
|
+
subject: args.subject,
|
|
424
|
+
};
|
|
323
425
|
let outcome;
|
|
324
426
|
try {
|
|
325
427
|
outcome = await deliverEmail(args, signal);
|
|
326
428
|
}
|
|
327
429
|
catch (error) {
|
|
430
|
+
// A response was received but the provider rejected it: the error carries
|
|
431
|
+
// the raw request/response so "provider said no" stays distinguishable
|
|
432
|
+
// from "we never reached the provider" (network failure, timeout,
|
|
433
|
+
// credential resolution failure), which sets none of these three fields.
|
|
434
|
+
const providerError = error instanceof EmailProviderError ? error : undefined;
|
|
328
435
|
await recordEmailSend({
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
orgId: args.orgId ?? getRequestOrgId(),
|
|
332
|
-
recipient: args.to,
|
|
333
|
-
sender: outcome?.from ?? args.from ?? "unknown",
|
|
334
|
-
subject: args.subject,
|
|
436
|
+
...baseRecord,
|
|
437
|
+
sender: providerError?.from ?? args.from ?? "unknown",
|
|
335
438
|
status: "failed",
|
|
336
439
|
error: error instanceof Error ? error.message : String(error),
|
|
337
|
-
provider:
|
|
440
|
+
provider: providerError?.provider ?? "unknown",
|
|
441
|
+
requestPayload: providerError?.requestPayload,
|
|
442
|
+
responseStatus: providerError?.responseStatus,
|
|
443
|
+
responseBody: providerError?.responseBody,
|
|
338
444
|
});
|
|
339
445
|
throw error;
|
|
340
446
|
}
|
|
341
447
|
await recordEmailSend({
|
|
342
|
-
|
|
343
|
-
app: args.app ?? getAppConfig().app.slug ?? "unknown",
|
|
344
|
-
orgId: args.orgId ?? getRequestOrgId(),
|
|
345
|
-
recipient: args.to,
|
|
448
|
+
...baseRecord,
|
|
346
449
|
sender: outcome.from,
|
|
347
|
-
subject: args.subject,
|
|
348
450
|
status: "sent",
|
|
349
451
|
provider: outcome.provider,
|
|
452
|
+
requestPayload: outcome.requestPayload,
|
|
453
|
+
responseStatus: outcome.responseStatus,
|
|
454
|
+
responseBody: outcome.responseBody,
|
|
350
455
|
});
|
|
351
456
|
}
|
|
352
457
|
export async function sendEmail(args) {
|
|
@@ -1224,3 +1224,14 @@
|
|
|
1224
1224
|
}
|
|
1225
1225
|
}
|
|
1226
1226
|
}
|
|
1227
|
+
|
|
1228
|
+
/* Search inputs that render their own clear button must hide the
|
|
1229
|
+
browser's native WebKit cancel icon, or two clear controls stack on
|
|
1230
|
+
top of each other. Scoped to inputs that opt in via this class, since
|
|
1231
|
+
other type="search" inputs rely on the native control as their only
|
|
1232
|
+
clear affordance. Not motion-related, so it must not live inside the
|
|
1233
|
+
surrounding `prefers-reduced-motion: no-preference` block above. */
|
|
1234
|
+
input.agent-native-search-input::-webkit-search-cancel-button {
|
|
1235
|
+
-webkit-appearance: none;
|
|
1236
|
+
appearance: none;
|
|
1237
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.177.
|
|
3
|
+
"version": "0.177.1-nightly-20260908153602",
|
|
4
4
|
"description": "The agentic application framework for building autonomous agents with intuitive UIs",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|