@agent-native/core 0.86.0 → 0.86.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +9 -0
  3. package/corpus/core/package.json +2 -1
  4. package/corpus/core/src/client/AgentPanel.tsx +10 -0
  5. package/corpus/core/src/client/AssistantChat.tsx +91 -27
  6. package/corpus/core/src/client/ErrorBoundary.tsx +67 -12
  7. package/corpus/core/src/client/ErrorReportActions.tsx +69 -0
  8. package/corpus/core/src/client/FeedbackButton.tsx +8 -4
  9. package/corpus/core/src/client/db-admin/DbAdminPage.tsx +41 -13
  10. package/corpus/core/src/client/db-admin/SqlEditor.tsx +10 -3
  11. package/corpus/core/src/client/db-admin/TableEditor.tsx +12 -5
  12. package/corpus/core/src/client/db-admin/useAgentSync.ts +10 -3
  13. package/corpus/core/src/client/db-admin/useDbAdmin.ts +65 -16
  14. package/corpus/core/src/client/error-reporting.ts +100 -0
  15. package/corpus/core/src/client/index.ts +11 -0
  16. package/corpus/core/src/client/org/RequireActiveOrg.tsx +12 -0
  17. package/corpus/core/src/client/visual-style-controls.tsx +615 -2
  18. package/corpus/core/src/db-admin/index.ts +26 -0
  19. package/corpus/core/src/db-admin/operations.ts +87 -49
  20. package/corpus/core/src/localization/default-messages.ts +4 -0
  21. package/corpus/core/src/server/builder-design-systems.ts +328 -9
  22. package/corpus/core/src/server/index.ts +15 -0
  23. package/corpus/core/src/templates/default/app/root.tsx +47 -0
  24. package/corpus/templates/analytics/AGENTS.md +6 -4
  25. package/corpus/templates/analytics/actions/delete-db-admin-connection.ts +19 -0
  26. package/corpus/templates/analytics/actions/list-db-admin-connections.ts +18 -0
  27. package/corpus/templates/analytics/actions/navigate.ts +17 -3
  28. package/corpus/templates/analytics/actions/save-db-admin-connection.ts +33 -0
  29. package/corpus/templates/analytics/actions/view-screen.ts +11 -3
  30. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +11 -2
  31. package/corpus/templates/analytics/app/i18n/zh-TW.ts +23 -2
  32. package/corpus/templates/analytics/app/i18n-data.ts +230 -20
  33. package/corpus/templates/analytics/app/pages/Agents.tsx +398 -4
  34. package/corpus/templates/analytics/changelog/2026-07-03-connect-other-app-databases-from-analytics.md +6 -0
  35. package/corpus/templates/analytics/server/db/schema.ts +27 -0
  36. package/corpus/templates/analytics/server/lib/db-admin-connections.ts +458 -0
  37. package/corpus/templates/analytics/server/plugins/db.ts +35 -0
  38. package/corpus/templates/analytics/server/routes/[base]/_agent-native/analytics-db-admin/[...path].ts +1 -0
  39. package/corpus/templates/analytics/server/routes/_agent-native/analytics-db-admin/[...path].ts +160 -0
  40. package/corpus/templates/content/app/root.tsx +9 -0
  41. package/corpus/templates/content/changelog/2026-07-03-error-screens-now-include-a-feedback-button-with-debug-info.md +6 -0
  42. package/corpus/templates/design/AGENTS.md +13 -7
  43. package/corpus/templates/design/actions/get-design-system.ts +21 -0
  44. package/corpus/templates/design/actions/index-design-system-with-builder.ts +14 -37
  45. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +3 -3
  46. package/corpus/templates/design/app/components/design/code-workbench/SideBar.tsx +70 -9
  47. package/corpus/templates/design/app/components/design/code-workbench/editor/EditorTabs.tsx +11 -2
  48. package/corpus/templates/design/app/components/design/code-workbench/editor/MonacoHost.tsx +1 -1
  49. package/corpus/templates/design/app/components/design/code-workbench/editor/StatusBar.tsx +1 -1
  50. package/corpus/templates/design/app/components/design/code-workbench/explorer/ExplorerView.tsx +8 -2
  51. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +7 -3
  52. package/corpus/templates/design/app/components/design/code-workbench/store.tsx +48 -0
  53. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +6 -0
  54. package/corpus/templates/design/app/i18n/zh-TW.ts +15 -14
  55. package/corpus/templates/design/app/i18n-data.ts +154 -142
  56. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +32 -14
  57. package/corpus/templates/design/changelog/2026-07-03-design-system-setup-now-indexes-figma-code-and-design-md-sou.md +6 -0
  58. package/corpus/templates/design/changelog/2026-07-03-the-code-editor-is-easier-to-use-with-top-sidebar-tabs-clear.md +6 -0
  59. package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +16 -106
  60. package/corpus/templates/mail/app/root.tsx +60 -1
  61. package/corpus/templates/mail/changelog/2026-07-03-error-screens-now-include-a-feedback-button-with-debug-info.md +6 -0
  62. package/corpus/templates/plan/app/i18n/ar-SA.ts +3 -0
  63. package/corpus/templates/plan/app/i18n/de-DE.ts +4 -0
  64. package/corpus/templates/plan/app/i18n/en-US.ts +4 -0
  65. package/corpus/templates/plan/app/i18n/es-ES.ts +4 -0
  66. package/corpus/templates/plan/app/i18n/fr-FR.ts +4 -0
  67. package/corpus/templates/plan/app/i18n/hi-IN.ts +3 -0
  68. package/corpus/templates/plan/app/i18n/ja-JP.ts +4 -0
  69. package/corpus/templates/plan/app/i18n/ko-KR.ts +4 -0
  70. package/corpus/templates/plan/app/i18n/pt-BR.ts +4 -0
  71. package/corpus/templates/plan/app/i18n/zh-CN.ts +3 -0
  72. package/corpus/templates/plan/app/i18n/zh-TW.ts +3 -0
  73. package/corpus/templates/plan/app/pages/PlansPage.tsx +74 -3
  74. package/corpus/templates/plan/changelog/2026-07-03-error-screens-now-include-a-feedback-button-with-debug-info.md +6 -0
  75. package/corpus/templates/slides/AGENTS.md +7 -3
  76. package/corpus/templates/slides/actions/get-design-system.ts +21 -0
  77. package/corpus/templates/slides/actions/import-file.ts +15 -7
  78. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +14 -37
  79. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +32 -8
  80. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +2 -2
  81. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +2 -4
  82. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +163 -154
  83. package/corpus/templates/slides/app/components/editor/SlideStyleInspector.tsx +19 -15
  84. package/corpus/templates/slides/app/components/editor/SpeakerNotesPanel.tsx +56 -6
  85. package/corpus/templates/slides/app/components/layout/Layout.tsx +6 -1
  86. package/corpus/templates/slides/app/i18n/ar-SA.ts +8 -7
  87. package/corpus/templates/slides/app/i18n/de-DE.ts +7 -8
  88. package/corpus/templates/slides/app/i18n/en-US.ts +8 -7
  89. package/corpus/templates/slides/app/i18n/es-ES.ts +7 -8
  90. package/corpus/templates/slides/app/i18n/fr-FR.ts +7 -8
  91. package/corpus/templates/slides/app/i18n/hi-IN.ts +8 -7
  92. package/corpus/templates/slides/app/i18n/ja-JP.ts +7 -8
  93. package/corpus/templates/slides/app/i18n/ko-KR.ts +7 -7
  94. package/corpus/templates/slides/app/i18n/pt-BR.ts +7 -8
  95. package/corpus/templates/slides/app/i18n/zh-CN.ts +8 -7
  96. package/corpus/templates/slides/app/i18n/zh-TW.ts +8 -7
  97. package/corpus/templates/slides/app/lib/canvas-zoom.ts +16 -1
  98. package/corpus/templates/slides/app/pages/DeckEditor.tsx +3 -3
  99. package/corpus/templates/slides/changelog/2026-07-03-design-system-setup-now-indexes-figma-code-and-design-md-sou.md +6 -0
  100. package/corpus/templates/slides/changelog/2026-07-03-slide-editing-now-keeps-thumbnails-speaker-notes-styling-and.md +6 -0
  101. package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +16 -104
  102. package/dist/client/AgentPanel.d.ts.map +1 -1
  103. package/dist/client/AgentPanel.js +2 -1
  104. package/dist/client/AgentPanel.js.map +1 -1
  105. package/dist/client/AssistantChat.d.ts.map +1 -1
  106. package/dist/client/AssistantChat.js +77 -25
  107. package/dist/client/AssistantChat.js.map +1 -1
  108. package/dist/client/ErrorBoundary.d.ts.map +1 -1
  109. package/dist/client/ErrorBoundary.js +37 -2
  110. package/dist/client/ErrorBoundary.js.map +1 -1
  111. package/dist/client/ErrorReportActions.d.ts +13 -0
  112. package/dist/client/ErrorReportActions.d.ts.map +1 -0
  113. package/dist/client/ErrorReportActions.js +14 -0
  114. package/dist/client/ErrorReportActions.js.map +1 -0
  115. package/dist/client/FeedbackButton.d.ts +3 -1
  116. package/dist/client/FeedbackButton.d.ts.map +1 -1
  117. package/dist/client/FeedbackButton.js +6 -5
  118. package/dist/client/FeedbackButton.js.map +1 -1
  119. package/dist/client/db-admin/DbAdminPage.d.ts +9 -1
  120. package/dist/client/db-admin/DbAdminPage.d.ts.map +1 -1
  121. package/dist/client/db-admin/DbAdminPage.js +17 -13
  122. package/dist/client/db-admin/DbAdminPage.js.map +1 -1
  123. package/dist/client/db-admin/SqlEditor.d.ts +3 -1
  124. package/dist/client/db-admin/SqlEditor.d.ts.map +1 -1
  125. package/dist/client/db-admin/SqlEditor.js +3 -3
  126. package/dist/client/db-admin/SqlEditor.js.map +1 -1
  127. package/dist/client/db-admin/TableEditor.d.ts +3 -1
  128. package/dist/client/db-admin/TableEditor.d.ts.map +1 -1
  129. package/dist/client/db-admin/TableEditor.js +6 -6
  130. package/dist/client/db-admin/TableEditor.js.map +1 -1
  131. package/dist/client/db-admin/useAgentSync.d.ts +3 -2
  132. package/dist/client/db-admin/useAgentSync.d.ts.map +1 -1
  133. package/dist/client/db-admin/useAgentSync.js +8 -4
  134. package/dist/client/db-admin/useAgentSync.js.map +1 -1
  135. package/dist/client/db-admin/useDbAdmin.d.ts +11 -7
  136. package/dist/client/db-admin/useDbAdmin.d.ts.map +1 -1
  137. package/dist/client/db-admin/useDbAdmin.js +45 -20
  138. package/dist/client/db-admin/useDbAdmin.js.map +1 -1
  139. package/dist/client/error-reporting.d.ts +19 -0
  140. package/dist/client/error-reporting.d.ts.map +1 -0
  141. package/dist/client/error-reporting.js +71 -0
  142. package/dist/client/error-reporting.js.map +1 -0
  143. package/dist/client/index.d.ts +3 -1
  144. package/dist/client/index.d.ts.map +1 -1
  145. package/dist/client/index.js +3 -1
  146. package/dist/client/index.js.map +1 -1
  147. package/dist/client/org/RequireActiveOrg.d.ts.map +1 -1
  148. package/dist/client/org/RequireActiveOrg.js +2 -1
  149. package/dist/client/org/RequireActiveOrg.js.map +1 -1
  150. package/dist/client/visual-style-controls.d.ts +11 -0
  151. package/dist/client/visual-style-controls.d.ts.map +1 -1
  152. package/dist/client/visual-style-controls.js +350 -1
  153. package/dist/client/visual-style-controls.js.map +1 -1
  154. package/dist/collab/routes.d.ts +1 -1
  155. package/dist/collab/struct-routes.d.ts +1 -1
  156. package/dist/db-admin/index.d.ts +3 -0
  157. package/dist/db-admin/index.d.ts.map +1 -0
  158. package/dist/db-admin/index.js +2 -0
  159. package/dist/db-admin/index.js.map +1 -0
  160. package/dist/db-admin/operations.d.ts +26 -5
  161. package/dist/db-admin/operations.d.ts.map +1 -1
  162. package/dist/db-admin/operations.js +66 -55
  163. package/dist/db-admin/operations.js.map +1 -1
  164. package/dist/localization/default-messages.d.ts +3 -0
  165. package/dist/localization/default-messages.d.ts.map +1 -1
  166. package/dist/localization/default-messages.js +3 -0
  167. package/dist/localization/default-messages.js.map +1 -1
  168. package/dist/notifications/routes.d.ts +1 -1
  169. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  170. package/dist/resources/handlers.d.ts +1 -1
  171. package/dist/secrets/routes.d.ts +9 -9
  172. package/dist/server/builder-design-systems.d.ts +61 -0
  173. package/dist/server/builder-design-systems.d.ts.map +1 -1
  174. package/dist/server/builder-design-systems.js +219 -14
  175. package/dist/server/builder-design-systems.js.map +1 -1
  176. package/dist/server/index.d.ts +1 -1
  177. package/dist/server/index.d.ts.map +1 -1
  178. package/dist/server/index.js +1 -1
  179. package/dist/server/index.js.map +1 -1
  180. package/dist/server/transcribe-voice.d.ts +1 -1
  181. package/dist/templates/default/app/root.tsx +47 -0
  182. package/package.json +2 -1
  183. package/src/templates/default/app/root.tsx +47 -0
@@ -0,0 +1,458 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import { createDbExec, getDbExec, type Dialect } from "@agent-native/core/db";
4
+ import type { DbAdminRuntime } from "@agent-native/core/db-admin";
5
+ import { getOrgContext } from "@agent-native/core/org";
6
+ import {
7
+ deleteAppSecret,
8
+ getAppSecretMeta,
9
+ readAppSecret,
10
+ writeAppSecret,
11
+ } from "@agent-native/core/secrets";
12
+ import {
13
+ getRequestOrgId,
14
+ getRequestUserEmail,
15
+ getSession,
16
+ runWithRequestContext,
17
+ } from "@agent-native/core/server";
18
+ import type { H3Event } from "h3";
19
+
20
+ const CONNECTIONS_TABLE = "analytics_db_admin_connections";
21
+ const SECRET_PREFIX = "analytics-db-admin";
22
+
23
+ export interface DbAdminConnection {
24
+ id: string;
25
+ name: string;
26
+ appId: string | null;
27
+ appUrl: string | null;
28
+ createdBy: string;
29
+ createdAt: string;
30
+ updatedAt: string;
31
+ orgId: string;
32
+ databaseUrlLast4: string | null;
33
+ hasDatabaseAuthToken: boolean;
34
+ databaseAuthTokenLast4: string | null;
35
+ }
36
+
37
+ export interface DbAdminAdminContext {
38
+ userEmail: string;
39
+ orgId: string;
40
+ role: "owner" | "admin";
41
+ }
42
+
43
+ export class DbAdminConnectionError extends Error {
44
+ constructor(
45
+ readonly statusCode: number,
46
+ message: string,
47
+ ) {
48
+ super(message);
49
+ this.name = "DbAdminConnectionError";
50
+ }
51
+ }
52
+
53
+ function isAdminRole(value: unknown): value is "owner" | "admin" {
54
+ return value === "owner" || value === "admin";
55
+ }
56
+
57
+ function readString(
58
+ row: Record<string, unknown>,
59
+ ...keys: string[]
60
+ ): string | null {
61
+ for (const key of keys) {
62
+ const value = row[key];
63
+ if (typeof value === "string" && value.trim()) return value;
64
+ }
65
+ return null;
66
+ }
67
+
68
+ async function resolveOrgRole(
69
+ userEmail: string,
70
+ orgId: string,
71
+ ): Promise<string | null> {
72
+ try {
73
+ const { rows } = await getDbExec().execute({
74
+ sql: `SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,
75
+ args: [orgId, userEmail.toLowerCase()],
76
+ });
77
+ return readString((rows[0] ?? {}) as Record<string, unknown>, "role");
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ export async function requireDbAdminContextFromRequest(input?: {
84
+ userEmail?: string;
85
+ orgId?: string | null;
86
+ }): Promise<DbAdminAdminContext> {
87
+ const userEmail = input?.userEmail ?? getRequestUserEmail();
88
+ const orgId = input?.orgId ?? getRequestOrgId() ?? null;
89
+ if (!userEmail) {
90
+ throw new DbAdminConnectionError(401, "Sign in to manage app databases.");
91
+ }
92
+ if (!orgId) {
93
+ throw new DbAdminConnectionError(
94
+ 403,
95
+ "An active organization is required to manage app databases.",
96
+ );
97
+ }
98
+ const role = await resolveOrgRole(userEmail, orgId);
99
+ if (!isAdminRole(role)) {
100
+ throw new DbAdminConnectionError(
101
+ 403,
102
+ "Only organization owners and admins can access connected app databases.",
103
+ );
104
+ }
105
+ return { userEmail, orgId, role };
106
+ }
107
+
108
+ export async function requireDbAdminContextFromEvent(
109
+ event: H3Event,
110
+ ): Promise<DbAdminAdminContext> {
111
+ const session = await getSession(event).catch(() => null);
112
+ if (!session?.email) {
113
+ throw new DbAdminConnectionError(401, "Sign in to manage app databases.");
114
+ }
115
+ const org = await getOrgContext(event).catch(() => null);
116
+ if (!org?.orgId) {
117
+ throw new DbAdminConnectionError(
118
+ 403,
119
+ "An active organization is required to manage app databases.",
120
+ );
121
+ }
122
+ if (!isAdminRole(org.role)) {
123
+ throw new DbAdminConnectionError(
124
+ 403,
125
+ "Only organization owners and admins can access connected app databases.",
126
+ );
127
+ }
128
+ return { userEmail: session.email, orgId: org.orgId, role: org.role };
129
+ }
130
+
131
+ export async function runWithDbAdminEventContext<T>(
132
+ event: H3Event,
133
+ fn: (ctx: DbAdminAdminContext) => Promise<T>,
134
+ ): Promise<T> {
135
+ const ctx = await requireDbAdminContextFromEvent(event);
136
+ return runWithRequestContext(
137
+ { userEmail: ctx.userEmail, orgId: ctx.orgId },
138
+ () => fn(ctx),
139
+ );
140
+ }
141
+
142
+ function secretKey(connectionId: string, kind: "url" | "auth-token"): string {
143
+ return `${SECRET_PREFIX}:${connectionId}:${kind}`;
144
+ }
145
+
146
+ function normalizeOptionalString(value: unknown): string | null {
147
+ return typeof value === "string" && value.trim() ? value.trim() : null;
148
+ }
149
+
150
+ function normalizeDatabaseUrl(value: unknown): string {
151
+ const raw = normalizeOptionalString(value);
152
+ if (!raw) {
153
+ throw new DbAdminConnectionError(400, "Database URL is required.");
154
+ }
155
+ const lower = raw.toLowerCase();
156
+ if (
157
+ !lower.startsWith("postgres://") &&
158
+ !lower.startsWith("postgresql://") &&
159
+ !lower.startsWith("libsql://")
160
+ ) {
161
+ throw new DbAdminConnectionError(
162
+ 400,
163
+ "Use a Postgres, PostgreSQL, or libSQL database URL.",
164
+ );
165
+ }
166
+ return raw;
167
+ }
168
+
169
+ function dialectForDatabaseUrl(url: string): Dialect {
170
+ const lower = url.toLowerCase();
171
+ if (lower.startsWith("postgres://") || lower.startsWith("postgresql://")) {
172
+ return "postgres";
173
+ }
174
+ return "sqlite";
175
+ }
176
+
177
+ function rowToConnection(
178
+ row: Record<string, unknown>,
179
+ secretMeta: {
180
+ databaseUrlLast4: string | null;
181
+ hasDatabaseAuthToken: boolean;
182
+ databaseAuthTokenLast4: string | null;
183
+ },
184
+ ): DbAdminConnection {
185
+ return {
186
+ id: readString(row, "id") ?? "",
187
+ name: readString(row, "name") ?? "Connected app",
188
+ appId: readString(row, "appId", "app_id"),
189
+ appUrl: readString(row, "appUrl", "app_url"),
190
+ createdBy: readString(row, "createdBy", "created_by") ?? "",
191
+ createdAt: readString(row, "createdAt", "created_at") ?? "",
192
+ updatedAt: readString(row, "updatedAt", "updated_at") ?? "",
193
+ orgId: readString(row, "orgId", "org_id") ?? "",
194
+ ...secretMeta,
195
+ };
196
+ }
197
+
198
+ async function secretMetadata(
199
+ ctx: DbAdminAdminContext,
200
+ row: Record<string, unknown>,
201
+ ) {
202
+ const urlSecretKey = readString(
203
+ row,
204
+ "databaseUrlSecretKey",
205
+ "database_url_secret_key",
206
+ );
207
+ const authSecretKey = readString(
208
+ row,
209
+ "databaseAuthTokenSecretKey",
210
+ "database_auth_token_secret_key",
211
+ );
212
+ const databaseUrlMeta = urlSecretKey
213
+ ? await getAppSecretMeta({
214
+ key: urlSecretKey,
215
+ scope: "org",
216
+ scopeId: ctx.orgId,
217
+ })
218
+ : null;
219
+ const databaseAuthTokenMeta = authSecretKey
220
+ ? await getAppSecretMeta({
221
+ key: authSecretKey,
222
+ scope: "org",
223
+ scopeId: ctx.orgId,
224
+ })
225
+ : null;
226
+ return {
227
+ databaseUrlLast4: databaseUrlMeta?.last4 ?? null,
228
+ hasDatabaseAuthToken: Boolean(databaseAuthTokenMeta),
229
+ databaseAuthTokenLast4: databaseAuthTokenMeta?.last4 ?? null,
230
+ };
231
+ }
232
+
233
+ export async function listDbAdminConnections(
234
+ ctx: DbAdminAdminContext,
235
+ ): Promise<DbAdminConnection[]> {
236
+ const { rows } = await getDbExec().execute({
237
+ sql: `SELECT id, name, app_id AS "appId", app_url AS "appUrl",
238
+ database_url_secret_key AS "databaseUrlSecretKey",
239
+ database_auth_token_secret_key AS "databaseAuthTokenSecretKey",
240
+ created_by AS "createdBy", created_at AS "createdAt",
241
+ updated_at AS "updatedAt", org_id AS "orgId"
242
+ FROM ${CONNECTIONS_TABLE}
243
+ WHERE org_id = ?
244
+ ORDER BY updated_at DESC, name ASC`,
245
+ args: [ctx.orgId],
246
+ });
247
+ return Promise.all(
248
+ rows.map(async (row) =>
249
+ rowToConnection(
250
+ row as Record<string, unknown>,
251
+ await secretMetadata(ctx, row as Record<string, unknown>),
252
+ ),
253
+ ),
254
+ );
255
+ }
256
+
257
+ export async function saveDbAdminConnection(
258
+ ctx: DbAdminAdminContext,
259
+ input: {
260
+ id?: string | null;
261
+ name: string;
262
+ appId?: string | null;
263
+ appUrl?: string | null;
264
+ databaseUrl: string;
265
+ databaseAuthToken?: string | null;
266
+ },
267
+ ): Promise<DbAdminConnection> {
268
+ const name = normalizeOptionalString(input.name);
269
+ if (!name)
270
+ throw new DbAdminConnectionError(400, "Connection name is required.");
271
+ const databaseUrl = normalizeDatabaseUrl(input.databaseUrl);
272
+ const authToken = normalizeOptionalString(input.databaseAuthToken);
273
+ const id = normalizeOptionalString(input.id) ?? randomUUID();
274
+ const databaseUrlSecretKey = secretKey(id, "url");
275
+ const databaseAuthTokenSecretKey = authToken
276
+ ? secretKey(id, "auth-token")
277
+ : null;
278
+ const now = new Date().toISOString();
279
+
280
+ await writeAppSecret({
281
+ key: databaseUrlSecretKey,
282
+ scope: "org",
283
+ scopeId: ctx.orgId,
284
+ value: databaseUrl,
285
+ description: `${name} database URL`,
286
+ });
287
+ if (authToken) {
288
+ await writeAppSecret({
289
+ key: databaseAuthTokenSecretKey!,
290
+ scope: "org",
291
+ scopeId: ctx.orgId,
292
+ value: authToken,
293
+ description: `${name} database auth token`,
294
+ });
295
+ } else {
296
+ await deleteAppSecret({
297
+ key: secretKey(id, "auth-token"),
298
+ scope: "org",
299
+ scopeId: ctx.orgId,
300
+ }).catch(() => false);
301
+ }
302
+
303
+ await getDbExec().execute({
304
+ sql: `INSERT INTO ${CONNECTIONS_TABLE}
305
+ (id, name, app_id, app_url, database_url_secret_key,
306
+ database_auth_token_secret_key, created_by, created_at, updated_at, org_id)
307
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
308
+ ON CONFLICT(id) DO UPDATE SET
309
+ name = excluded.name,
310
+ app_id = excluded.app_id,
311
+ app_url = excluded.app_url,
312
+ database_url_secret_key = excluded.database_url_secret_key,
313
+ database_auth_token_secret_key = excluded.database_auth_token_secret_key,
314
+ updated_at = excluded.updated_at
315
+ WHERE ${CONNECTIONS_TABLE}.org_id = excluded.org_id`,
316
+ args: [
317
+ id,
318
+ name,
319
+ normalizeOptionalString(input.appId),
320
+ normalizeOptionalString(input.appUrl),
321
+ databaseUrlSecretKey,
322
+ databaseAuthTokenSecretKey,
323
+ ctx.userEmail,
324
+ now,
325
+ now,
326
+ ctx.orgId,
327
+ ],
328
+ });
329
+
330
+ const connection = await getDbAdminConnection(ctx, id);
331
+ if (!connection) {
332
+ throw new DbAdminConnectionError(404, "Connection not found.");
333
+ }
334
+ return connection;
335
+ }
336
+
337
+ export async function deleteDbAdminConnection(
338
+ ctx: DbAdminAdminContext,
339
+ id: string,
340
+ ): Promise<{ deleted: boolean }> {
341
+ const target = await getConnectionRow(ctx, id);
342
+ if (!target) return { deleted: false };
343
+
344
+ await getDbExec().execute({
345
+ sql: `DELETE FROM ${CONNECTIONS_TABLE} WHERE id = ? AND org_id = ?`,
346
+ args: [id, ctx.orgId],
347
+ });
348
+ await deleteAppSecret({
349
+ key: readString(target, "databaseUrlSecretKey", "database_url_secret_key")!,
350
+ scope: "org",
351
+ scopeId: ctx.orgId,
352
+ }).catch(() => false);
353
+ const authKey = readString(
354
+ target,
355
+ "databaseAuthTokenSecretKey",
356
+ "database_auth_token_secret_key",
357
+ );
358
+ if (authKey) {
359
+ await deleteAppSecret({
360
+ key: authKey,
361
+ scope: "org",
362
+ scopeId: ctx.orgId,
363
+ }).catch(() => false);
364
+ }
365
+ return { deleted: true };
366
+ }
367
+
368
+ async function getConnectionRow(
369
+ ctx: DbAdminAdminContext,
370
+ id: string,
371
+ ): Promise<Record<string, unknown> | null> {
372
+ const { rows } = await getDbExec().execute({
373
+ sql: `SELECT id, name, app_id AS "appId", app_url AS "appUrl",
374
+ database_url_secret_key AS "databaseUrlSecretKey",
375
+ database_auth_token_secret_key AS "databaseAuthTokenSecretKey",
376
+ created_by AS "createdBy", created_at AS "createdAt",
377
+ updated_at AS "updatedAt", org_id AS "orgId"
378
+ FROM ${CONNECTIONS_TABLE}
379
+ WHERE id = ? AND org_id = ?
380
+ LIMIT 1`,
381
+ args: [id, ctx.orgId],
382
+ });
383
+ return (rows[0] as Record<string, unknown> | undefined) ?? null;
384
+ }
385
+
386
+ export async function getDbAdminConnection(
387
+ ctx: DbAdminAdminContext,
388
+ id: string,
389
+ ): Promise<DbAdminConnection | null> {
390
+ const row = await getConnectionRow(ctx, id);
391
+ if (!row) return null;
392
+ return rowToConnection(row, await secretMetadata(ctx, row));
393
+ }
394
+
395
+ export async function withDbAdminConnectionRuntime<T>(
396
+ ctx: DbAdminAdminContext,
397
+ id: string,
398
+ fn: (runtime: DbAdminRuntime, connection: DbAdminConnection) => Promise<T>,
399
+ ): Promise<T> {
400
+ const row = await getConnectionRow(ctx, id);
401
+ if (!row) throw new DbAdminConnectionError(404, "Connection not found.");
402
+
403
+ const databaseUrlSecretKey = readString(
404
+ row,
405
+ "databaseUrlSecretKey",
406
+ "database_url_secret_key",
407
+ );
408
+ if (!databaseUrlSecretKey) {
409
+ throw new DbAdminConnectionError(
410
+ 500,
411
+ "Connection is missing database URL metadata.",
412
+ );
413
+ }
414
+ const databaseUrl = await readAppSecret({
415
+ key: databaseUrlSecretKey,
416
+ scope: "org",
417
+ scopeId: ctx.orgId,
418
+ });
419
+ if (!databaseUrl?.value) {
420
+ throw new DbAdminConnectionError(400, "Database URL secret is missing.");
421
+ }
422
+
423
+ const authSecretKey = readString(
424
+ row,
425
+ "databaseAuthTokenSecretKey",
426
+ "database_auth_token_secret_key",
427
+ );
428
+ const authToken = authSecretKey
429
+ ? await readAppSecret({
430
+ key: authSecretKey,
431
+ scope: "org",
432
+ scopeId: ctx.orgId,
433
+ })
434
+ : null;
435
+
436
+ const exec = await createDbExec({
437
+ url: databaseUrl.value,
438
+ authToken: authToken?.value,
439
+ });
440
+ try {
441
+ return await fn(
442
+ {
443
+ db: exec,
444
+ dialect: dialectForDatabaseUrl(databaseUrl.value),
445
+ },
446
+ rowToConnection(row, await secretMetadata(ctx, row)),
447
+ );
448
+ } finally {
449
+ await exec.close?.().catch(() => {});
450
+ }
451
+ }
452
+
453
+ export function redactDbAdminError(err: unknown): string {
454
+ const message = err instanceof Error ? err.message : String(err);
455
+ return message
456
+ .replace(/(postgres(?:ql)?:\/\/[^:\s/]+:)[^@\s]+@/gi, "$1[redacted]@")
457
+ .replace(/(libsql:\/\/[^?\s]+[?&]authToken=)[^&\s]+/gi, "$1[redacted]");
458
+ }
@@ -671,6 +671,41 @@ const runAnalyticsMigrations = runMigrations(
671
671
  name: "analytics-alert-rules-enabled-eval-idx",
672
672
  sql: `CREATE INDEX IF NOT EXISTS analytics_alert_rules_enabled_eval_idx ON analytics_alert_rules (enabled, last_status, last_evaluated_at, created_at)`,
673
673
  },
674
+ {
675
+ version: 81,
676
+ name: "analytics-db-admin-connections-table",
677
+ sql: {
678
+ postgres: `CREATE TABLE IF NOT EXISTS analytics_db_admin_connections (
679
+ id TEXT PRIMARY KEY,
680
+ name TEXT NOT NULL,
681
+ app_id TEXT,
682
+ app_url TEXT,
683
+ database_url_secret_key TEXT NOT NULL,
684
+ database_auth_token_secret_key TEXT,
685
+ created_by TEXT NOT NULL,
686
+ created_at TEXT NOT NULL DEFAULT (now()::text),
687
+ updated_at TEXT NOT NULL DEFAULT (now()::text),
688
+ org_id TEXT NOT NULL
689
+ )`,
690
+ sqlite: `CREATE TABLE IF NOT EXISTS analytics_db_admin_connections (
691
+ id TEXT PRIMARY KEY,
692
+ name TEXT NOT NULL,
693
+ app_id TEXT,
694
+ app_url TEXT,
695
+ database_url_secret_key TEXT NOT NULL,
696
+ database_auth_token_secret_key TEXT,
697
+ created_by TEXT NOT NULL,
698
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
699
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
700
+ org_id TEXT NOT NULL
701
+ )`,
702
+ },
703
+ },
704
+ {
705
+ version: 82,
706
+ name: "analytics-db-admin-connections-org-updated-idx",
707
+ sql: `CREATE INDEX IF NOT EXISTS analytics_db_admin_connections_org_updated_idx ON analytics_db_admin_connections (org_id, updated_at)`,
708
+ },
674
709
  ],
675
710
  { table: "analytics_migrations" },
676
711
  );
@@ -0,0 +1 @@
1
+ export { default } from "../../../_agent-native/analytics-db-admin/[...path].js";
@@ -0,0 +1,160 @@
1
+ import {
2
+ applyMutations,
3
+ DbAdminConfirmRequiredError,
4
+ getRows,
5
+ getTableSchema,
6
+ listTables,
7
+ runSql,
8
+ type DbAdminMutation,
9
+ type DbAdminRowsRequest,
10
+ } from "@agent-native/core/db-admin";
11
+ import { readBody } from "@agent-native/core/server";
12
+ import {
13
+ defineEventHandler,
14
+ getMethod,
15
+ getRequestURL,
16
+ setResponseHeader,
17
+ setResponseStatus,
18
+ type H3Event,
19
+ } from "h3";
20
+
21
+ import {
22
+ DbAdminConnectionError,
23
+ redactDbAdminError,
24
+ runWithDbAdminEventContext,
25
+ withDbAdminConnectionRuntime,
26
+ } from "../../../lib/db-admin-connections";
27
+
28
+ function decodeSegment(value: string | undefined): string | undefined {
29
+ if (!value) return undefined;
30
+ try {
31
+ return decodeURIComponent(value);
32
+ } catch {
33
+ return value;
34
+ }
35
+ }
36
+
37
+ function pathSegments(event: H3Event): string[] {
38
+ const url = getRequestURL(event);
39
+ const marker = "/_agent-native/analytics-db-admin";
40
+ const markerIndex = url.pathname.indexOf(marker);
41
+ const suffix =
42
+ markerIndex === -1 ? "" : url.pathname.slice(markerIndex + marker.length);
43
+ return suffix
44
+ .replace(/^\/+/, "")
45
+ .split("/")
46
+ .filter(Boolean)
47
+ .map(decodeSegment)
48
+ .filter((segment): segment is string => Boolean(segment));
49
+ }
50
+
51
+ function methodNotAllowed(event: H3Event) {
52
+ setResponseStatus(event, 405);
53
+ return { ok: false, error: "Method not allowed" };
54
+ }
55
+
56
+ function badRequest(event: H3Event, error: string) {
57
+ setResponseStatus(event, 400);
58
+ return { ok: false, error };
59
+ }
60
+
61
+ function notFound(event: H3Event, error: string) {
62
+ setResponseStatus(event, 404);
63
+ return { ok: false, error };
64
+ }
65
+
66
+ export default defineEventHandler(async (event) => {
67
+ setResponseHeader(event, "Cache-Control", "no-store");
68
+
69
+ const method = getMethod(event);
70
+ const [connectionId, ...segments] = pathSegments(event);
71
+ if (!connectionId) {
72
+ return badRequest(event, "Connection id is required");
73
+ }
74
+
75
+ try {
76
+ return await runWithDbAdminEventContext(event, async (admin) =>
77
+ withDbAdminConnectionRuntime(admin, connectionId, async (runtime) => {
78
+ if (segments[0] === "overview" && segments.length === 1) {
79
+ if (method !== "GET") return methodNotAllowed(event);
80
+ const result = await listTables(runtime);
81
+ return { ok: true, ...result };
82
+ }
83
+
84
+ if (segments[0] === "table") {
85
+ const name = segments[1];
86
+ if (!name) return badRequest(event, "Table name is required");
87
+ const sub = segments[2];
88
+
89
+ if (sub === "schema" && segments.length === 3) {
90
+ if (method !== "GET") return methodNotAllowed(event);
91
+ const table = await getTableSchema(name, runtime);
92
+ return { ok: true, table };
93
+ }
94
+
95
+ if (sub === "rows" && segments.length === 3) {
96
+ if (method !== "POST") return methodNotAllowed(event);
97
+ const body = await readBody<Partial<DbAdminRowsRequest>>(event);
98
+ const result = await getRows(
99
+ name,
100
+ {
101
+ page: Number(body.page) || 1,
102
+ pageSize: Number(body.pageSize) || 50,
103
+ sort: body.sort,
104
+ filters: body.filters,
105
+ },
106
+ runtime,
107
+ );
108
+ return { ok: true, ...result };
109
+ }
110
+
111
+ if (sub === "mutate" && segments.length === 3) {
112
+ if (method !== "POST") return methodNotAllowed(event);
113
+ const body = await readBody<DbAdminMutation>(event);
114
+ const result = await applyMutations(name, body ?? {}, runtime);
115
+ return { ok: true, ...result };
116
+ }
117
+
118
+ return notFound(event, "Unknown analytics db-admin table route");
119
+ }
120
+
121
+ if (segments[0] === "query" && segments.length === 1) {
122
+ if (method !== "POST") return methodNotAllowed(event);
123
+ const body = await readBody<{
124
+ sql?: string;
125
+ params?: unknown[];
126
+ confirmDestructive?: boolean;
127
+ }>(event);
128
+ try {
129
+ const result = await runSql(
130
+ String(body.sql ?? ""),
131
+ Array.isArray(body.params) ? body.params : undefined,
132
+ { confirmDestructive: body.confirmDestructive === true },
133
+ runtime,
134
+ );
135
+ return { ok: true, ...result };
136
+ } catch (err) {
137
+ if (err instanceof DbAdminConfirmRequiredError) {
138
+ setResponseStatus(event, 400);
139
+ return {
140
+ ok: false,
141
+ error: err.message,
142
+ needsConfirm: true,
143
+ };
144
+ }
145
+ throw err;
146
+ }
147
+ }
148
+
149
+ return notFound(event, "Unknown analytics db-admin route");
150
+ }),
151
+ );
152
+ } catch (err) {
153
+ if (err instanceof DbAdminConnectionError) {
154
+ setResponseStatus(event, err.statusCode);
155
+ return { ok: false, error: err.message };
156
+ }
157
+ setResponseStatus(event, 500);
158
+ return { ok: false, error: redactDbAdminError(err) };
159
+ }
160
+ });
@@ -4,6 +4,7 @@ import {
4
4
  appPath,
5
5
  CommandMenu,
6
6
  createAgentNativeQueryClient,
7
+ ErrorReportActions,
7
8
  getLocaleInitScript,
8
9
  getThemeInitScript,
9
10
  type LocaleCode,
@@ -627,6 +628,14 @@ function ContentErrorBoundaryBody() {
627
628
  >
628
629
  Reload
629
630
  </button>
631
+ <ErrorReportActions
632
+ appName="Content"
633
+ title={title}
634
+ details={details}
635
+ issueTitle={`Content error: ${title}`}
636
+ className="mt-4"
637
+ align="center"
638
+ />
630
639
  </div>
631
640
  </main>
632
641
  );
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-03
4
+ ---
5
+
6
+ Content error screens now include a feedback button with debug context and a prefilled GitHub issue fallback.