@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.
@@ -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(/^#/, "");
@@ -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, optionally filtered to one registered email id.",
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
- /** Most recent sends for one app, newest first, optionally filtered to one template. */
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
- }): Promise<EmailLogEntry[]>;
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
- await ensureTableExists("email_log", createSql);
24
- await widenIntColumnsToBigInt("email_log", ["created_at"]);
25
- await ensureColumnExists("email_log", "org_id", "ALTER TABLE email_log ADD COLUMN IF NOT EXISTS org_id TEXT");
26
- await ensureIndexExists("email_log_template_created_idx", EMAIL_LOG_TEMPLATE_INDEX_SQL);
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 client.execute(createSql);
31
- try {
32
- await client.execute("ALTER TABLE email_log ADD COLUMN org_id TEXT");
33
- }
34
- catch (error) {
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
- /** Most recent sends for one app, newest first, optionally filtered to one template. */
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 where = options.templateId
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
- sql: `SELECT id, template_id, app, recipient, sender, subject, status, error, provider, created_at
123
- FROM email_log ${where} ORDER BY created_at DESC LIMIT ?`,
124
- args,
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
- /** Provider error text when status is "failed". */
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;
@@ -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
  });
@@ -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
- const body = await res.text().catch(() => "");
232
- throw new Error(`Resend error ${res.status}: ${body}`);
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 { provider, from };
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
- const body = await res.text().catch(() => "");
302
- throw new Error(`SendGrid error ${res.status}: ${body}`);
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 { provider, from };
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
- templateId: args.templateId,
330
- app: args.app ?? getAppConfig().app.slug ?? "unknown",
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: outcome?.provider ?? "unknown",
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
- templateId: args.templateId,
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) {
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- error?: undefined;
24
23
  text: string;
24
+ error?: undefined;
25
25
  }>>;
@@ -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.0",
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": {