@agent-native/core 0.168.4 → 0.168.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8492
34
+ - template files: 8496
@@ -14,7 +14,10 @@ import { useState, useEffect, useCallback, useRef } from "react";
14
14
  import { createPortal } from "react-dom";
15
15
  import { toast } from "sonner";
16
16
 
17
- import { MAX_REFERENCE_FILE_BYTES } from "../../../shared/upload-types";
17
+ import {
18
+ MAX_REFERENCE_FILE_BYTES,
19
+ MAX_REFERENCE_FILES,
20
+ } from "../../../shared/upload-types";
18
21
  import { Button } from "../ui/button";
19
22
  import { Input } from "../ui/input";
20
23
  import { GoogleDocImportHint } from "./GoogleDocImportHint";
@@ -29,39 +32,159 @@ export interface UploadedFile {
29
32
  size: number;
30
33
  }
31
34
 
32
- export async function uploadPromptFiles(
33
- files: File[],
34
- ): Promise<UploadedFile[]> {
35
- if (files.length === 0) return [];
35
+ // Netlify functions cap request bodies well under what a real PPTX/PDF
36
+ // needs, so any file above this size streams through the chunked upload
37
+ // endpoints (sub-4 MB slices, reassembled server-side) instead of one
38
+ // multipart POST.
39
+ const CHUNK_UPLOAD_THRESHOLD_BYTES = 4 * 1024 * 1024;
40
+ const CHUNK_SIZE_BYTES = 4 * 1024 * 1024;
41
+
42
+ async function readUploadJson(response: Response): Promise<unknown> {
43
+ try {
44
+ return await response.json();
45
+ } catch (error) {
46
+ throw new Error(`Upload returned invalid JSON (${response.status})`, {
47
+ cause: error,
48
+ });
49
+ }
50
+ }
51
+
52
+ function extractErrorMessage(data: unknown): string | null {
53
+ if (
54
+ data &&
55
+ typeof data === "object" &&
56
+ "error" in data &&
57
+ typeof (data as { error: unknown }).error === "string" &&
58
+ (data as { error: string }).error.trim()
59
+ ) {
60
+ return (data as { error: string }).error;
61
+ }
62
+ return null;
63
+ }
64
+
65
+ async function uploadFilesMultipart(files: File[]): Promise<UploadedFile[]> {
36
66
  const formData = new FormData();
37
67
  files.forEach((file) => formData.append("files", file));
38
- ensureEmbedAuthFetchInterceptor();
39
68
  const response = await fetch(`${appBasePath()}/api/uploads`, {
40
69
  method: "POST",
41
70
  body: formData,
42
71
  credentials: "include",
43
72
  });
73
+ const data = await readUploadJson(response);
44
74
  if (!response.ok) {
45
- let message = "Upload failed";
46
- try {
47
- const data: unknown = await response.json();
48
- if (
49
- data &&
50
- typeof data === "object" &&
51
- "error" in data &&
52
- typeof data.error === "string" &&
53
- data.error.trim()
54
- ) {
55
- message = data.error;
56
- }
57
- } catch (error) {
58
- const uploadError = new Error(`Upload failed (${response.status})`);
59
- (uploadError as Error & { cause?: unknown }).cause = error;
60
- throw uploadError;
75
+ throw new Error(
76
+ extractErrorMessage(data) || `Upload failed (${response.status})`,
77
+ );
78
+ }
79
+ if (!Array.isArray(data)) {
80
+ throw new Error("Upload failed: invalid response");
81
+ }
82
+ return data as UploadedFile[];
83
+ }
84
+
85
+ async function uploadFileChunked(file: File): Promise<UploadedFile> {
86
+ const startResponse = await fetch(
87
+ `${appBasePath()}/api/uploads-chunked/start`,
88
+ {
89
+ method: "POST",
90
+ headers: { "Content-Type": "application/json" },
91
+ credentials: "include",
92
+ body: JSON.stringify({
93
+ filename: file.name,
94
+ mimetype: file.type || "application/octet-stream",
95
+ declaredSize: file.size,
96
+ }),
97
+ },
98
+ );
99
+ const startData = await readUploadJson(startResponse);
100
+ if (!startResponse.ok) {
101
+ throw new Error(
102
+ extractErrorMessage(startData) ||
103
+ `Upload failed (${startResponse.status})`,
104
+ );
105
+ }
106
+ if (
107
+ startData &&
108
+ typeof startData === "object" &&
109
+ (startData as { uploadMode?: unknown }).uploadMode === "multipart"
110
+ ) {
111
+ const [uploaded] = await uploadFilesMultipart([file]);
112
+ if (!uploaded) throw new Error("Upload failed: no file returned");
113
+ return uploaded;
114
+ }
115
+ const sessionId =
116
+ startData && typeof startData === "object"
117
+ ? (startData as { sessionId?: unknown }).sessionId
118
+ : undefined;
119
+ if (typeof sessionId !== "string" || !sessionId) {
120
+ throw new Error("Upload failed: session ID missing");
121
+ }
122
+
123
+ const totalChunks = Math.max(1, Math.ceil(file.size / CHUNK_SIZE_BYTES));
124
+ for (let index = 0; index < totalChunks; index++) {
125
+ const start = index * CHUNK_SIZE_BYTES;
126
+ const end = Math.min(start + CHUNK_SIZE_BYTES, file.size);
127
+ const isFinal = index === totalChunks - 1;
128
+ const chunkResponse = await fetch(
129
+ `${appBasePath()}/api/uploads-chunked/${sessionId}/chunk?index=${index}&isFinal=${
130
+ isFinal ? "1" : "0"
131
+ }`,
132
+ {
133
+ method: "POST",
134
+ credentials: "include",
135
+ headers: { "Content-Type": "application/octet-stream" },
136
+ body: file.slice(start, end),
137
+ },
138
+ );
139
+ const chunkData = await readUploadJson(chunkResponse);
140
+ if (!chunkResponse.ok) {
141
+ throw new Error(
142
+ extractErrorMessage(chunkData) ||
143
+ `Upload failed (${chunkResponse.status})`,
144
+ );
145
+ }
146
+ if (isFinal) {
147
+ const result = Array.isArray(chunkData)
148
+ ? (chunkData[0] as UploadedFile)
149
+ : undefined;
150
+ if (!result) throw new Error("Upload failed: no file returned");
151
+ return result;
61
152
  }
62
- throw new Error(message);
63
153
  }
64
- return (await response.json()) as UploadedFile[];
154
+ throw new Error("Upload failed: no final chunk response");
155
+ }
156
+
157
+ export async function uploadPromptFiles(
158
+ files: File[],
159
+ ): Promise<UploadedFile[]> {
160
+ if (files.length === 0) return [];
161
+ if (files.length > MAX_REFERENCE_FILES) {
162
+ throw new Error(`Too many files (max ${MAX_REFERENCE_FILES})`);
163
+ }
164
+ ensureEmbedAuthFetchInterceptor();
165
+ const smallIndices = files.flatMap((file, index) =>
166
+ file.size <= CHUNK_UPLOAD_THRESHOLD_BYTES ? [index] : [],
167
+ );
168
+ const largeIndices = files.flatMap((file, index) =>
169
+ file.size > CHUNK_UPLOAD_THRESHOLD_BYTES ? [index] : [],
170
+ );
171
+ const [smallUploads, largeUploads] = await Promise.all([
172
+ smallIndices.length > 0
173
+ ? uploadFilesMultipart(smallIndices.map((index) => files[index]))
174
+ : [],
175
+ Promise.all(largeIndices.map((index) => uploadFileChunked(files[index]))),
176
+ ]);
177
+ if (smallUploads.length !== smallIndices.length) {
178
+ throw new Error("Upload failed: response file count did not match request");
179
+ }
180
+ const uploads = new Array<UploadedFile>(files.length);
181
+ smallIndices.forEach((fileIndex, resultIndex) => {
182
+ uploads[fileIndex] = smallUploads[resultIndex];
183
+ });
184
+ largeIndices.forEach((fileIndex, resultIndex) => {
185
+ uploads[fileIndex] = largeUploads[resultIndex];
186
+ });
187
+ return uploads;
65
188
  }
66
189
 
67
190
  /**
@@ -0,0 +1,317 @@
1
+ import {
2
+ deletePrivateBlob,
3
+ putPrivateBlob,
4
+ readPrivateBlob,
5
+ } from "@agent-native/core/private-blob";
6
+ import {
7
+ defineEventHandler,
8
+ getHeader,
9
+ getQuery,
10
+ getRouterParam,
11
+ readBody,
12
+ readRawBody,
13
+ setResponseStatus,
14
+ } from "h3";
15
+ import { nanoid } from "nanoid";
16
+
17
+ import {
18
+ createChunkedUploadSession,
19
+ deleteChunkedUploadSession,
20
+ getChunkedUploadSession,
21
+ listChunkedUploadSessions,
22
+ type ChunkedUploadSession,
23
+ } from "../lib/chunked-upload-session.js";
24
+ import { isHostedSlidesRuntime } from "../lib/tenant-files.js";
25
+ import {
26
+ resolveSlidesRequestAuth,
27
+ withSlidesRequestContext,
28
+ } from "./request-auth-context.js";
29
+ import { maxReferenceFileBytes, saveUploadedReferenceFile } from "./uploads.js";
30
+
31
+ const MAX_CHUNK_BYTES = 4 * 1024 * 1024;
32
+ const MAX_CHUNKS = 128;
33
+ const SESSION_TTL_MS = 60 * 60 * 1000;
34
+
35
+ interface StartBody {
36
+ filename?: unknown;
37
+ mimetype?: unknown;
38
+ declaredSize?: unknown;
39
+ }
40
+
41
+ async function deleteChunk(handle: ChunkedUploadSession["chunks"][string]) {
42
+ return (await deletePrivateBlob(handle)).deleted;
43
+ }
44
+
45
+ async function cleanupChunks(session: ChunkedUploadSession): Promise<boolean> {
46
+ const results = await Promise.all(
47
+ Object.values(session.chunks).map(deleteChunk),
48
+ );
49
+ return results.every(Boolean);
50
+ }
51
+
52
+ async function discardSession(
53
+ sessionId: string,
54
+ session: ChunkedUploadSession,
55
+ ): Promise<boolean> {
56
+ const cleaned = await cleanupChunks(session);
57
+ if (cleaned) await deleteChunkedUploadSession(sessionId);
58
+ return cleaned;
59
+ }
60
+
61
+ async function reapExpiredChunkedUploads(): Promise<void> {
62
+ const now = Date.now();
63
+ const sessions = await listChunkedUploadSessions();
64
+ await Promise.all(
65
+ sessions.map(async ({ sessionId, session }) => {
66
+ const expiresAt = Date.parse(session.expiresAt);
67
+ if (Number.isFinite(expiresAt) && expiresAt > now) return;
68
+ try {
69
+ const cleaned = await discardSession(sessionId, session);
70
+ if (!cleaned) {
71
+ console.warn("[slides-upload] expired session cleanup incomplete", {
72
+ sessionId,
73
+ });
74
+ }
75
+ } catch (error) {
76
+ console.warn("[slides-upload] expired session cleanup failed", {
77
+ sessionId,
78
+ error: error instanceof Error ? error.message : String(error),
79
+ });
80
+ }
81
+ }),
82
+ );
83
+ }
84
+
85
+ async function cleanupCommittedSession(
86
+ sessionId: string,
87
+ session: ChunkedUploadSession,
88
+ ): Promise<void> {
89
+ try {
90
+ const cleaned = await discardSession(sessionId, session);
91
+ if (!cleaned) {
92
+ console.warn("[slides-upload] committed session cleanup incomplete", {
93
+ sessionId,
94
+ });
95
+ }
96
+ } catch (error) {
97
+ console.warn("[slides-upload] committed session cleanup failed", {
98
+ sessionId,
99
+ error: error instanceof Error ? error.message : String(error),
100
+ });
101
+ }
102
+ }
103
+
104
+ export const startChunkedUpload = defineEventHandler(async (event) => {
105
+ const auth = await resolveSlidesRequestAuth(event);
106
+ if (!auth.ok) {
107
+ setResponseStatus(event, auth.statusCode);
108
+ return { error: auth.error };
109
+ }
110
+ const authContext = auth.context;
111
+ if (!authContext.email) {
112
+ setResponseStatus(event, 401);
113
+ return { error: "Unauthorized" };
114
+ }
115
+
116
+ return withSlidesRequestContext(
117
+ event,
118
+ async () => {
119
+ if (!isHostedSlidesRuntime()) {
120
+ return { uploadMode: "multipart" as const };
121
+ }
122
+ await reapExpiredChunkedUploads();
123
+ const body = (await readBody(event).catch(
124
+ () => null,
125
+ )) as StartBody | null;
126
+ const filename =
127
+ typeof body?.filename === "string" ? body.filename.trim() : "";
128
+ const mimetype =
129
+ typeof body?.mimetype === "string" && body.mimetype.trim()
130
+ ? body.mimetype.trim()
131
+ : "application/octet-stream";
132
+ const declaredSize = Number(body?.declaredSize);
133
+ if (!filename) {
134
+ setResponseStatus(event, 400);
135
+ return { error: "filename is required" };
136
+ }
137
+ if (!Number.isSafeInteger(declaredSize) || declaredSize <= 0) {
138
+ setResponseStatus(event, 400);
139
+ return { error: "declaredSize must be a positive integer" };
140
+ }
141
+ const limit = maxReferenceFileBytes(filename);
142
+ if (declaredSize > limit) {
143
+ setResponseStatus(event, 413);
144
+ return {
145
+ error: `File too large (max ${Math.round(limit / 1024 / 1024)} MB)`,
146
+ };
147
+ }
148
+
149
+ const sessionId = nanoid();
150
+ const now = Date.now();
151
+ await createChunkedUploadSession(sessionId, {
152
+ filename,
153
+ mimeType: mimetype,
154
+ declaredSize,
155
+ chunks: {},
156
+ chunkSizes: {},
157
+ createdAt: new Date(now).toISOString(),
158
+ expiresAt: new Date(now + SESSION_TTL_MS).toISOString(),
159
+ });
160
+ return { sessionId, maxChunkBytes: MAX_CHUNK_BYTES };
161
+ },
162
+ authContext,
163
+ );
164
+ });
165
+
166
+ export const uploadChunkedChunk = defineEventHandler(async (event) => {
167
+ const auth = await resolveSlidesRequestAuth(event);
168
+ if (!auth.ok) {
169
+ setResponseStatus(event, auth.statusCode);
170
+ return { error: auth.error };
171
+ }
172
+ const authContext = auth.context;
173
+ const email = authContext.email;
174
+ if (!email) {
175
+ setResponseStatus(event, 401);
176
+ return { error: "Unauthorized" };
177
+ }
178
+
179
+ return withSlidesRequestContext(
180
+ event,
181
+ async ({ orgId }) => {
182
+ const sessionId = getRouterParam(event, "sessionId");
183
+ if (!sessionId) {
184
+ setResponseStatus(event, 400);
185
+ return { error: "Missing sessionId" };
186
+ }
187
+ const session = await getChunkedUploadSession(sessionId);
188
+ if (!session) {
189
+ setResponseStatus(event, 404);
190
+ return { error: "Upload session not found or expired" };
191
+ }
192
+ if (Date.parse(session.expiresAt) <= Date.now()) {
193
+ await discardSession(sessionId, session);
194
+ setResponseStatus(event, 410);
195
+ return { error: "Upload session expired" };
196
+ }
197
+
198
+ const query = getQuery(event);
199
+ const index = Number(query.index ?? 0);
200
+ const isFinal = query.isFinal === "1" || query.isFinal === "true";
201
+ if (!Number.isInteger(index) || index < 0 || index >= MAX_CHUNKS) {
202
+ setResponseStatus(event, 400);
203
+ return { error: "Invalid chunk index" };
204
+ }
205
+
206
+ const contentLengthHeader = getHeader(event, "content-length");
207
+ if (!contentLengthHeader || !/^\d+$/.test(contentLengthHeader)) {
208
+ setResponseStatus(event, 411);
209
+ return { error: "Valid Content-Length header required" };
210
+ }
211
+ const contentLength = Number(contentLengthHeader);
212
+ if (contentLength <= 0) {
213
+ setResponseStatus(event, 400);
214
+ return { error: "Empty chunk body" };
215
+ }
216
+ if (contentLength > MAX_CHUNK_BYTES) {
217
+ setResponseStatus(event, 413);
218
+ return { error: "Chunk too large" };
219
+ }
220
+
221
+ const chunkKey = String(index);
222
+ const previousSize = session.chunkSizes[chunkKey] ?? 0;
223
+ const receivedBefore = Object.values(session.chunkSizes).reduce(
224
+ (total, size) => total + size,
225
+ 0,
226
+ );
227
+ const nextSize = receivedBefore - previousSize + contentLength;
228
+ const fileLimit = maxReferenceFileBytes(session.filename);
229
+ if (nextSize > session.declaredSize || nextSize > fileLimit) {
230
+ await discardSession(sessionId, session);
231
+ setResponseStatus(event, 413);
232
+ return { error: "Uploaded bytes exceed the declared file size" };
233
+ }
234
+
235
+ const raw = await readRawBody(event, false);
236
+ const bytes = raw ?? new Uint8Array(0);
237
+ if (bytes.byteLength !== contentLength) {
238
+ setResponseStatus(event, 400);
239
+ return { error: "Chunk size does not match Content-Length" };
240
+ }
241
+
242
+ const previousHandle = session.chunks[chunkKey];
243
+ if (previousHandle && !(await deleteChunk(previousHandle))) {
244
+ setResponseStatus(event, 503);
245
+ return { error: "Could not replace the previously uploaded chunk" };
246
+ }
247
+
248
+ const handle = await putPrivateBlob({
249
+ data: bytes,
250
+ filename: `${sessionId}-${index}`,
251
+ mimeType: "application/octet-stream",
252
+ ownerEmail: email,
253
+ });
254
+ if (!handle) {
255
+ setResponseStatus(event, 503);
256
+ return { error: "Upload storage is not available" };
257
+ }
258
+ session.chunks[chunkKey] = handle;
259
+ session.chunkSizes[chunkKey] = bytes.byteLength;
260
+ await createChunkedUploadSession(sessionId, session);
261
+
262
+ if (!isFinal) return { ok: true };
263
+
264
+ const orderedIndices = Object.keys(session.chunks)
265
+ .map(Number)
266
+ .sort((a, b) => a - b);
267
+ const missing = orderedIndices.some((value, i) => value !== i);
268
+ const receivedSize = Object.values(session.chunkSizes).reduce(
269
+ (total, size) => total + size,
270
+ 0,
271
+ );
272
+ if (
273
+ missing ||
274
+ orderedIndices.length === 0 ||
275
+ receivedSize !== session.declaredSize
276
+ ) {
277
+ await discardSession(sessionId, session);
278
+ setResponseStatus(event, 400);
279
+ return { error: "Upload is incomplete or has an invalid size" };
280
+ }
281
+
282
+ let result;
283
+ try {
284
+ const parts = await Promise.all(
285
+ orderedIndices.map(async (chunkIndex) => {
286
+ const chunkHandle = session.chunks[String(chunkIndex)];
287
+ const read = await readPrivateBlob(chunkHandle);
288
+ return Buffer.from(read.data);
289
+ }),
290
+ );
291
+ const combined = Buffer.concat(parts);
292
+ if (combined.byteLength !== session.declaredSize) {
293
+ throw new Error("Assembled upload size does not match declaredSize");
294
+ }
295
+ result = await saveUploadedReferenceFile({
296
+ email,
297
+ orgId,
298
+ originalName: session.filename,
299
+ data: combined,
300
+ type: session.mimeType,
301
+ });
302
+ } catch (err) {
303
+ await discardSession(sessionId, session);
304
+ const statusCode =
305
+ typeof (err as { statusCode?: unknown })?.statusCode === "number"
306
+ ? (err as { statusCode: number }).statusCode
307
+ : 400;
308
+ setResponseStatus(event, statusCode);
309
+ return { error: err instanceof Error ? err.message : "Invalid upload" };
310
+ }
311
+
312
+ await cleanupCommittedSession(sessionId, session);
313
+ return [result];
314
+ },
315
+ authContext,
316
+ );
317
+ });
@@ -11,6 +11,7 @@ import { nanoid } from "nanoid";
11
11
  import {
12
12
  MAX_FIG_REFERENCE_FILE_BYTES,
13
13
  MAX_REFERENCE_FILE_BYTES,
14
+ MAX_REFERENCE_FILES,
14
15
  SLIDES_REFERENCE_FILE_ERROR_LABEL,
15
16
  isSlidesReferenceFileExtension,
16
17
  } from "../../shared/upload-types.js";
@@ -237,11 +238,9 @@ export const uploadFiles = defineEventHandler(async (event) => {
237
238
  return { error: "No files uploaded" };
238
239
  }
239
240
 
240
- const MAX_FILES = 20;
241
-
242
- if (fileParts.length > MAX_FILES) {
241
+ if (fileParts.length > MAX_REFERENCE_FILES) {
243
242
  setResponseStatus(event, 413);
244
- return { error: `Too many files (max ${MAX_FILES})` };
243
+ return { error: `Too many files (max ${MAX_REFERENCE_FILES})` };
245
244
  }
246
245
 
247
246
  const oversized = fileParts.find(
@@ -0,0 +1,54 @@
1
+ import {
2
+ deleteAppState,
3
+ listAppState,
4
+ readAppState,
5
+ writeAppState,
6
+ } from "@agent-native/core/application-state";
7
+ import type { PrivateBlobHandle } from "@agent-native/core/private-blob";
8
+
9
+ export interface ChunkedUploadSession {
10
+ filename: string;
11
+ mimeType: string;
12
+ declaredSize: number;
13
+ chunks: Record<string, PrivateBlobHandle>;
14
+ chunkSizes: Record<string, number>;
15
+ createdAt: string;
16
+ expiresAt: string;
17
+ }
18
+
19
+ const PREFIX = "slides-upload-chunks-";
20
+ const key = (sessionId: string) => `${PREFIX}${sessionId}`;
21
+
22
+ export async function createChunkedUploadSession(
23
+ sessionId: string,
24
+ session: ChunkedUploadSession,
25
+ ): Promise<void> {
26
+ await writeAppState(
27
+ key(sessionId),
28
+ session as unknown as Record<string, unknown>,
29
+ );
30
+ }
31
+
32
+ export async function getChunkedUploadSession(
33
+ sessionId: string,
34
+ ): Promise<ChunkedUploadSession | null> {
35
+ const raw = await readAppState(key(sessionId));
36
+ if (!raw || typeof raw !== "object") return null;
37
+ return raw as unknown as ChunkedUploadSession;
38
+ }
39
+
40
+ export async function listChunkedUploadSessions(): Promise<
41
+ Array<{ sessionId: string; session: ChunkedUploadSession }>
42
+ > {
43
+ const entries = await listAppState(PREFIX);
44
+ return entries.map(({ key: entryKey, value }) => ({
45
+ sessionId: entryKey.slice(PREFIX.length),
46
+ session: value as unknown as ChunkedUploadSession,
47
+ }));
48
+ }
49
+
50
+ export async function deleteChunkedUploadSession(
51
+ sessionId: string,
52
+ ): Promise<void> {
53
+ await deleteAppState(key(sessionId));
54
+ }
@@ -0,0 +1 @@
1
+ export { uploadChunkedChunk as default } from "../../../../handlers/uploads-chunked";
@@ -0,0 +1 @@
1
+ export { startChunkedUpload as default } from "../../../handlers/uploads-chunked";
@@ -25,6 +25,7 @@ export const SLIDES_REFERENCE_FILE_LABEL =
25
25
  export const SLIDES_REFERENCE_FILE_ERROR_LABEL =
26
26
  "pptx, docx, pdf, fig, text, Markdown, JSON, CSV, and images including SVG";
27
27
 
28
+ export const MAX_REFERENCE_FILES = 20;
28
29
  export const MAX_REFERENCE_FILE_BYTES = 50 * 1024 * 1024;
29
30
  export const MAX_FIG_REFERENCE_FILE_BYTES = 200 * 1024 * 1024;
30
31
 
@@ -161,6 +161,8 @@ export declare function shouldAllowAgentChatSurfaceSettingsMode(mode: AgentChatS
161
161
  export declare function AgentChatSurface({ mode, className, defaultMode, showHeader, showTabBar, isFullscreen, style, chatViewTransition, showPageNewChatButton, ...props }: AgentChatSurfaceProps): React.JSX.Element;
162
162
  export interface AgentSidebarProps {
163
163
  children: React.ReactNode;
164
+ /** Keep the app surface mounted while temporarily disabling the chat panel. */
165
+ enabled?: boolean;
164
166
  /** Placeholder text for the empty chat state */
165
167
  emptyStateText?: string;
166
168
  /** Suggestion prompts shown when no messages */
@@ -260,7 +262,7 @@ export interface AgentSidebarProps {
260
262
  * Wraps app content with a toggleable agent sidebar.
261
263
  * Use AgentToggleButton in your header to open/close it.
262
264
  */
263
- export declare function AgentSidebar({ children, emptyStateText, suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, availableModels, modelListLoading, selectedAgent, onAgentChange, onConnectLocalRuntime, onConnectProvider, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position, defaultOpen, animateMobile, animateDesktop, chatViewTransition, chatViewTransitionHandoff, storageKey, openStorageKey, apiUrl, agentChatSurface, desktopIdentityUnauthenticated, desktopIdentityAuthenticated, showTabBar, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning, onFullscreenRequest, scope, isolateHistoryByScope, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding, }: AgentSidebarProps): React.JSX.Element;
265
+ export declare function AgentSidebar({ children, enabled, emptyStateText, suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, availableModels, modelListLoading, selectedAgent, onAgentChange, onConnectLocalRuntime, onConnectProvider, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position, defaultOpen, animateMobile, animateDesktop, chatViewTransition, chatViewTransitionHandoff, storageKey, openStorageKey, apiUrl, agentChatSurface, desktopIdentityUnauthenticated, desktopIdentityAuthenticated, showTabBar, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning, onFullscreenRequest, scope, isolateHistoryByScope, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding, }: AgentSidebarProps): React.JSX.Element;
264
266
  /**
265
267
  * Focus the agent chat composer input.
266
268
  * Opens the sidebar if closed, then focuses the text input.
@@ -1418,7 +1418,7 @@ export function AgentChatSurface({ mode = "panel", className, defaultMode = "cha
1418
1418
  * Wraps app content with a toggleable agent sidebar.
1419
1419
  * Use AgentToggleButton in your header to open/close it.
1420
1420
  */
1421
- export function AgentSidebar({ children, emptyStateText = "How can I help you?", suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, availableModels, modelListLoading, selectedAgent, onAgentChange, onConnectLocalRuntime, onConnectProvider, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position = "right", defaultOpen = false, animateMobile = true, animateDesktop = true, chatViewTransition = false, chatViewTransitionHandoff = false, storageKey, openStorageKey, apiUrl, agentChatSurface, desktopIdentityUnauthenticated, desktopIdentityAuthenticated, showTabBar = true, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning = false, onFullscreenRequest, scope, isolateHistoryByScope = false, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding = false, }) {
1421
+ export function AgentSidebar({ children, enabled = true, emptyStateText = "How can I help you?", suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, availableModels, modelListLoading, selectedAgent, onAgentChange, onConnectLocalRuntime, onConnectProvider, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position = "right", defaultOpen = false, animateMobile = true, animateDesktop = true, chatViewTransition = false, chatViewTransitionHandoff = false, storageKey, openStorageKey, apiUrl, agentChatSurface, desktopIdentityUnauthenticated, desktopIdentityAuthenticated, showTabBar = true, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning = false, onFullscreenRequest, scope, isolateHistoryByScope = false, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding = false, }) {
1422
1422
  const staticHostedHarnessEnabled = isHostedHarnessConfigured(injectedAgentNativeConfig().harness);
1423
1423
  const hostedHarnessQuery = useActionQuery("get-hosted-harness-config", undefined, { enabled: staticHostedHarnessEnabled });
1424
1424
  const hostedHarnessStatus = hostedHarnessQuery.data;
@@ -1596,7 +1596,8 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1596
1596
  const [hasFrameSidebarState, setHasFrameSidebarState] = useState(false);
1597
1597
  const [backgroundPanelActive, setBackgroundPanelActive] = useState(false);
1598
1598
  const [runningTabIds, setRunningTabIds] = useState(() => new Set());
1599
- const shouldMountPanel = !isPerAppChatHosted &&
1599
+ const shouldMountPanel = enabled &&
1600
+ !isPerAppChatHosted &&
1600
1601
  !presentationMode &&
1601
1602
  (!frameCodeMode || !shouldParentFrameOwnAgentPanel()) &&
1602
1603
  (open || backgroundPanelActive || runningTabIds.size > 0);
@@ -1612,9 +1613,10 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1612
1613
  if (frameOwned && !hasFrameSidebarState && !isPerAppChatHosted)
1613
1614
  return;
1614
1615
  dispatchAgentSidebarStateChange({
1615
- open: isPerAppChatHosted
1616
- ? perAppChatState.open
1617
- : !presentationMode && (frameOwned ? frameSidebarOpen : open),
1616
+ open: enabled &&
1617
+ (isPerAppChatHosted
1618
+ ? perAppChatState.open
1619
+ : !presentationMode && (frameOwned ? frameSidebarOpen : open)),
1618
1620
  source: frameOwned ? "frame" : "app",
1619
1621
  mode: frameOwned ? "code" : "app",
1620
1622
  });
@@ -1626,6 +1628,7 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1626
1628
  hasFrameSidebarState,
1627
1629
  isPerAppChatHosted,
1628
1630
  perAppChatState.open,
1631
+ enabled,
1629
1632
  ]);
1630
1633
  useEffect(() => {
1631
1634
  if (!isPerAppChatSidebar)
@@ -1633,7 +1636,7 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1633
1636
  const frameOwned = frameCodeMode && shouldParentFrameOwnAgentPanel();
1634
1637
  if (frameOwned && !hasFrameSidebarState)
1635
1638
  return;
1636
- const openState = !presentationMode && (frameOwned ? frameSidebarOpen : open);
1639
+ const openState = enabled && !presentationMode && (frameOwned ? frameSidebarOpen : open);
1637
1640
  const message = buildAppChatSidebarStateMessage(openState);
1638
1641
  window.dispatchEvent(new CustomEvent(APP_CHAT_SIDEBAR_STATE_EVENT, {
1639
1642
  detail: message.data,
@@ -1656,6 +1659,7 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1656
1659
  isPerAppChatSidebar,
1657
1660
  open,
1658
1661
  presentationMode,
1662
+ enabled,
1659
1663
  ]);
1660
1664
  useEffect(() => {
1661
1665
  const preparePanel = () => setBackgroundPanelActive(true);
@@ -1968,10 +1972,9 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1968
1972
  }
1969
1973
  };
1970
1974
  }, [shouldMountPanel, sidebarAnimationEnabled]);
1971
- const shouldRenderPanel = sidebarAnimationEnabled
1972
- ? renderAnimatedPanel
1973
- : shouldMountPanel;
1974
- const panelOpen = open && shouldMountPanel;
1975
+ const shouldRenderPanel = enabled &&
1976
+ (sidebarAnimationEnabled ? renderAnimatedPanel : shouldMountPanel);
1977
+ const panelOpen = enabled && open && shouldMountPanel;
1975
1978
  const panelLayout = isMobile
1976
1979
  ? "mobile"
1977
1980
  : wideDrawerEnabled
@@ -2064,6 +2067,7 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
2064
2067
  return (_jsx(AgentSidebarOnboardingContext.Provider, { value: true, children: _jsxs(RealtimeVoiceModeProvider, { browserTabId: browserTabId, children: [showFirstRunOnboarding && (_jsx(Suspense, { fallback: null, children: _jsx(FirstRunOnboarding, {}) })), _jsxs("div", { className: "agent-sidebar-shell flex min-w-0 flex-1 h-screen overflow-hidden", "data-agent-sidebar-position": effectivePosition, "data-agent-native-hosted-harness-ui": hostedHarnessUi ? "desktop" : undefined, "data-agent-native-hosted-chat": isPerAppChatHosted ? "true" : undefined, "data-agent-sidebar-resizing": isResizing ? "true" : undefined, children: [isMobile &&
2065
2068
  !isPerAppChatHosted &&
2066
2069
  !presentationMode &&
2070
+ enabled &&
2067
2071
  (mobileAnimationEnabled ? shouldRenderPanel : open) && (_jsx("div", { className: cn("agent-sidebar-backdrop fixed inset-0 bg-foreground/40", mobileAnimationEnabled && !panelOpen && "pointer-events-none"), "data-agent-sidebar-animation": mobileAnimationEnabled ? "mobile" : undefined, "data-agent-sidebar-state": panelOpen ? "open" : "closed", style: { zIndex: SIDEBAR_OVERLAY_Z_INDEX - 1 }, onClick: () => setOpenPersisted(false) })), shouldMountPanel ? _jsx(URLSync, { browserTabId: browserTabId }) : null, isResizing ? (_jsx("div", { "aria-hidden": "true", className: "agent-sidebar-resize-overlay" })) : null, isLeft && !presentationMode ? sidebar : null, isLeft && !presentationMode ? drawerPlaceholder : null, _jsx("div", { className: "agent-sidebar-main-surface flex flex-1 flex-col overflow-auto min-w-0", "data-agent-sidebar-main-position": position, "data-agent-sidebar-main-state": !isMobile && !presentationMode && panelOpen ? "open" : "closed", "data-agent-sidebar-resizing": isResizing ? "true" : undefined, children: _jsx(ScreenRefreshBoundary, { children: children }) }), !isLeft && !presentationMode ? drawerPlaceholder : null, !isLeft && !presentationMode ? sidebar : null] })] }) }));
2068
2072
  }
2069
2073
  /**
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
62
62
  error: string;
63
63
  states?: undefined;
64
64
  } | {
65
- error?: undefined;
66
65
  states: {
67
66
  clientId: number;
68
67
  state: string;
69
68
  }[];
69
+ error?: undefined;
70
70
  }>>;
71
71
  /**
72
72
  * GET /_agent-native/collab/:docId/users
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
77
77
  error: string;
78
78
  users?: undefined;
79
79
  } | {
80
- error?: undefined;
81
80
  users: {
82
81
  clientId: number;
83
82
  lastSeen: number;
84
83
  }[];
84
+ error?: undefined;
85
85
  }>>;
@@ -199,6 +199,30 @@ export const builderFileUploadProvider = {
199
199
  console.log(`[builder-upload] done: ${json.url}`);
200
200
  return { url: json.url, id: json.id, provider: "builder" };
201
201
  },
202
+ delete: async ({ url }) => {
203
+ const assetUrl = new URL(url);
204
+ if (assetUrl.hostname !== "cdn.builder.io")
205
+ return false;
206
+ assetUrl.search = "";
207
+ assetUrl.hash = "";
208
+ const { resolveBuilderCredentials } = await import("../server/credential-provider.js");
209
+ const credentials = await resolveBuilderCredentials();
210
+ if (!credentials.privateKey || !credentials.publicKey)
211
+ return false;
212
+ const deleteUrl = new URL("/api/v1/assets/by-url", "https://cdn.builder.io");
213
+ deleteUrl.searchParams.set("url", assetUrl.toString());
214
+ deleteUrl.searchParams.set("apiKey", credentials.publicKey);
215
+ const response = await fetchWithTimeout(deleteUrl.toString(), {
216
+ method: "DELETE",
217
+ headers: { Authorization: `Bearer ${credentials.privateKey}` },
218
+ });
219
+ if (response.ok)
220
+ return true;
221
+ if (response.status === 404)
222
+ return false;
223
+ await assertOk(response, "Builder.io asset delete failed");
224
+ return false;
225
+ },
202
226
  resumable: {
203
227
  async startSession(filename, mimeType, maxBytes) {
204
228
  const { resolveBuilderPrivateKey } = await import("../server/credential-provider.js");
@@ -1,4 +1,4 @@
1
- export type { FileUploadInput, FileUploadProvider, FileUploadResult, ResumableUploadSession, ResumableChunkResult, } from "./types.js";
2
- export { registerFileUploadProvider, unregisterFileUploadProvider, listFileUploadProviders, getActiveFileUploadProvider, getActiveFileUploadProviderForRequest, uploadFile, } from "./registry.js";
1
+ export type { FileUploadDeleteInput, FileUploadInput, FileUploadProvider, FileUploadResult, ResumableUploadSession, ResumableChunkResult, } from "./types.js";
2
+ export { registerFileUploadProvider, unregisterFileUploadProvider, listFileUploadProviders, getActiveFileUploadProvider, getActiveFileUploadProviderForRequest, deleteUploadedFile, uploadFile, } from "./registry.js";
3
3
  export { builderFileUploadProvider } from "./builder.js";
4
4
  export { preUploadImageAttachments, preUploadAttachments, isFileUploadProviderConfigured, type PreUploadAttachmentsResult, type PreUploadedImageAttachment, type PreUploadedFileAttachment, } from "./pre-upload-attachments.js";
@@ -1,3 +1,3 @@
1
- export { registerFileUploadProvider, unregisterFileUploadProvider, listFileUploadProviders, getActiveFileUploadProvider, getActiveFileUploadProviderForRequest, uploadFile, } from "./registry.js";
1
+ export { registerFileUploadProvider, unregisterFileUploadProvider, listFileUploadProviders, getActiveFileUploadProvider, getActiveFileUploadProviderForRequest, deleteUploadedFile, uploadFile, } from "./registry.js";
2
2
  export { builderFileUploadProvider } from "./builder.js";
3
3
  export { preUploadImageAttachments, preUploadAttachments, isFileUploadProviderConfigured, } from "./pre-upload-attachments.js";
@@ -1,4 +1,4 @@
1
- import type { FileUploadInput, FileUploadProvider, FileUploadResult } from "./types.js";
1
+ import type { FileUploadDeleteInput, FileUploadInput, FileUploadProvider, FileUploadResult } from "./types.js";
2
2
  /**
3
3
  * Register a file upload provider. Call from a server plugin or app
4
4
  * bootstrap. Idempotent per id — later calls with the same id replace.
@@ -19,4 +19,5 @@ export declare function getActiveFileUploadProviderForRequest(): Promise<FileUpl
19
19
  * configured. `null` is an explicit storage-setup state: callers must not
20
20
  * turn the input into a base64 SQL fallback.
21
21
  */
22
+ export declare function deleteUploadedFile(providerId: string, input: FileUploadDeleteInput): Promise<boolean>;
22
23
  export declare function uploadFile(input: FileUploadInput): Promise<FileUploadResult | null>;
@@ -59,6 +59,14 @@ export async function getActiveFileUploadProviderForRequest() {
59
59
  * configured. `null` is an explicit storage-setup state: callers must not
60
60
  * turn the input into a base64 SQL fallback.
61
61
  */
62
+ export async function deleteUploadedFile(providerId, input) {
63
+ const provider = providerId === builderFileUploadProvider.id
64
+ ? builderFileUploadProvider
65
+ : providers.get(providerId);
66
+ if (!provider?.delete)
67
+ return false;
68
+ return provider.delete(input);
69
+ }
62
70
  export async function uploadFile(input) {
63
71
  const provider = await getActiveFileUploadProviderForRequest();
64
72
  // User-registered providers (S3, etc.) may be configured by sync runtime
@@ -146,6 +146,56 @@ async function putObject(config, key, data, contentType) {
146
146
  }
147
147
  return `${config.publicBaseUrl}/${key.split("/").map(encodePathSegment).join("/")}`;
148
148
  }
149
+ async function deleteObject(config, key) {
150
+ const now = new Date();
151
+ const amzDate = now
152
+ .toISOString()
153
+ .replace(/[:-]|\.\d{3}/g, "")
154
+ .slice(0, 15) + "Z";
155
+ const dateStamp = amzDate.slice(0, 8);
156
+ const credentialScope = `${dateStamp}/${config.region}/s3/aws4_request`;
157
+ const host = new URL(config.endpoint).host;
158
+ const canonicalUri = objectPath(config, key);
159
+ const payloadHash = await sha256(new Uint8Array(0));
160
+ const headers = {
161
+ host,
162
+ "x-amz-content-sha256": payloadHash,
163
+ "x-amz-date": amzDate,
164
+ };
165
+ const signedHeaderKeys = Object.keys(headers).sort();
166
+ const signedHeaders = signedHeaderKeys.join(";");
167
+ const canonicalHeaders = signedHeaderKeys
168
+ .map((header) => `${header}:${headers[header]}`)
169
+ .join("\n") + "\n";
170
+ const canonicalRequest = [
171
+ "DELETE",
172
+ canonicalUri,
173
+ "",
174
+ canonicalHeaders,
175
+ signedHeaders,
176
+ payloadHash,
177
+ ].join("\n");
178
+ const requestHash = await sha256(new TextEncoder().encode(canonicalRequest));
179
+ const stringToSign = [
180
+ "AWS4-HMAC-SHA256",
181
+ amzDate,
182
+ credentialScope,
183
+ requestHash,
184
+ ].join("\n");
185
+ const signature = toHex(await hmac(await signingKey(config.secretAccessKey, dateStamp, config.region), stringToSign));
186
+ const authorization = `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${credentialScope}, ` +
187
+ `SignedHeaders=${signedHeaders}, Signature=${signature}`;
188
+ const response = await fetch(`${config.endpoint}${canonicalUri}`, {
189
+ method: "DELETE",
190
+ headers: { ...headers, Authorization: authorization },
191
+ });
192
+ if (response.ok)
193
+ return true;
194
+ if (response.status === 404)
195
+ return false;
196
+ const detail = await response.text();
197
+ throw new Error(`S3 DeleteObject failed (${response.status}): ${detail || response.statusText}`);
198
+ }
149
199
  function safeFilename(filename) {
150
200
  const basename = filename?.split(/[\\/]/).pop()?.trim() || "attachment";
151
201
  return (basename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 160) || "attachment");
@@ -163,6 +213,14 @@ export const s3FileUploadProvider = {
163
213
  const key = `uploads/${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${safeFilename(filename)}`;
164
214
  const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
165
215
  const url = await putObject(config, key, bytes, mimeType || "application/octet-stream");
166
- return { url, provider: "s3" };
216
+ return { url, id: key, provider: "s3" };
217
+ },
218
+ delete: async ({ id }) => {
219
+ if (!id)
220
+ return false;
221
+ const config = await readRequestConfig();
222
+ if (!config)
223
+ return false;
224
+ return deleteObject(config, id);
167
225
  },
168
226
  };
@@ -28,6 +28,10 @@ export interface FileUploadResult {
28
28
  /** The provider that handled the upload. */
29
29
  provider: string;
30
30
  }
31
+ export interface FileUploadDeleteInput {
32
+ url: string;
33
+ id?: string;
34
+ }
31
35
  /** Opaque session handle returned by {@link FileUploadProvider.resumable.startSession}.
32
36
  * `sessionId` is provider-specific (GCS Location URI, S3 UploadId, etc.).
33
37
  * `meta` holds any provider state needed for subsequent relay and complete calls. */
@@ -56,6 +60,8 @@ export interface FileUploadProvider {
56
60
  isConfiguredForRequest?: () => Promise<boolean>;
57
61
  /** Upload a file and return a URL. Throw on failure. */
58
62
  upload: (input: FileUploadInput) => Promise<FileUploadResult>;
63
+ /** Delete a previously uploaded file when the provider supports it. */
64
+ delete?: (input: FileUploadDeleteInput) => Promise<boolean>;
59
65
  /**
60
66
  * Optional resumable/streaming upload capability.
61
67
  * When present, create-recording will initialise a session and stream chunks
@@ -11,14 +11,14 @@
11
11
  * DELETE /_agent-native/notifications/:id — delete
12
12
  */
13
13
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
14
- error?: undefined;
15
14
  count: number;
16
15
  updated?: undefined;
16
+ error?: undefined;
17
17
  ok?: undefined;
18
18
  } | {
19
- error?: undefined;
20
19
  count?: undefined;
21
20
  updated: number;
21
+ error?: undefined;
22
22
  ok?: undefined;
23
23
  } | {
24
24
  count?: undefined;
@@ -26,8 +26,8 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
26
26
  error: string;
27
27
  ok?: undefined;
28
28
  } | {
29
- error?: undefined;
30
29
  count?: undefined;
31
30
  updated?: undefined;
32
31
  ok: boolean;
32
+ error?: undefined;
33
33
  }>>;
@@ -3,11 +3,6 @@ export declare function registerPrivateBlobProvider(provider: PrivateBlobProvide
3
3
  export declare function unregisterPrivateBlobProvider(id: string): void;
4
4
  export declare function listPrivateBlobProviders(): PrivateBlobProvider[];
5
5
  export declare function getActivePrivateBlobProvider(): PrivateBlobProvider | null;
6
- /**
7
- * @deprecated Use `defineAppConfig({ privateBlob: { publicUploadFallback } })`
8
- * instead. This writes the same value into the deprecated layer of the config
9
- * ladder, so an explicit `defineAppConfig` call now wins over it.
10
- */
11
6
  export declare function setPrivateBlobPublicUploadFallbackEnabled(enabled: boolean): void;
12
7
  export declare function putPrivateBlob(input: PrivateBlobPutInput): Promise<PrivateBlobHandle | null>;
13
8
  export declare function readPrivateBlob(handle: PrivateBlobHandle): Promise<PrivateBlobReadResult>;
@@ -1,12 +1,14 @@
1
1
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
2
  import { getAppConfig } from "../app-config/index.js";
3
- import { setAppConfigLayer } from "../app-config/store.js";
4
- import { uploadFile } from "../file-upload/index.js";
3
+ import { deleteUploadedFile, uploadFile } from "../file-upload/index.js";
5
4
  import { decryptSecretValue, encryptSecretValue, getSecretEncryptionKey, } from "../secrets/crypto.js";
6
5
  const PUBLIC_UPLOAD_HANDLE_PREFIX = "public-upload:v1:";
7
6
  const PUBLIC_UPLOAD_READ_RETRY_DELAYS_MS = [100, 250, 500];
8
7
  const globals = globalThis;
9
8
  const providers = (globals.__agentNativePrivateBlobProviders ??= new Map());
9
+ const publicUploadFallbackRef = (globals.__agentNativePrivateBlobPublicUploadFallback ??= {
10
+ enabled: true,
11
+ });
10
12
  function toBytes(data) {
11
13
  return data instanceof Uint8Array ? data : new Uint8Array(data);
12
14
  }
@@ -170,13 +172,16 @@ export function listPrivateBlobProviders() {
170
172
  return [...providers.values()];
171
173
  }
172
174
  export function getActivePrivateBlobProvider() {
173
- const selected = getAppConfig().privateBlob.provider;
174
- if (selected) {
175
- const provider = providers.get(selected);
176
- if (!provider) {
177
- throw new Error(`Private blob provider "${selected}" is selected in app config but no provider with that id is registered`);
175
+ const selectedId = getAppConfig().privateBlob.provider;
176
+ if (selectedId) {
177
+ const selected = providers.get(selectedId);
178
+ if (!selected) {
179
+ throw new Error(`Private blob config selects '${selectedId}', but no provider with that id is registered`);
178
180
  }
179
- return provider;
181
+ if (!selected.isConfigured()) {
182
+ throw new Error(`Private blob provider '${selectedId}' is selected but not configured`);
183
+ }
184
+ return selected;
180
185
  }
181
186
  for (const provider of providers.values()) {
182
187
  if (provider.isConfigured())
@@ -184,20 +189,15 @@ export function getActivePrivateBlobProvider() {
184
189
  }
185
190
  return null;
186
191
  }
187
- /**
188
- * @deprecated Use `defineAppConfig({ privateBlob: { publicUploadFallback } })`
189
- * instead. This writes the same value into the deprecated layer of the config
190
- * ladder, so an explicit `defineAppConfig` call now wins over it.
191
- */
192
192
  export function setPrivateBlobPublicUploadFallbackEnabled(enabled) {
193
- setAppConfigLayer("legacy", {
194
- privateBlob: { publicUploadFallback: enabled },
195
- });
193
+ publicUploadFallbackRef.enabled = enabled;
196
194
  }
197
195
  export async function putPrivateBlob(input) {
198
196
  const provider = getActivePrivateBlobProvider();
199
197
  if (provider)
200
198
  return provider.put(input);
199
+ if (!publicUploadFallbackRef.enabled)
200
+ return null;
201
201
  if (!getAppConfig().privateBlob.publicUploadFallback)
202
202
  return null;
203
203
  return putViaEncryptedPublicUpload(input);
@@ -216,10 +216,17 @@ export async function deletePrivateBlob(handle) {
216
216
  if (provider)
217
217
  return provider.delete(handle);
218
218
  if (isPublicUploadFallbackHandle(handle)) {
219
+ const descriptor = decodePublicUploadDescriptor(handle.id);
220
+ const deleted = await deleteUploadedFile(descriptor.uploadProvider, {
221
+ url: descriptor.url,
222
+ id: descriptor.uploadId,
223
+ });
219
224
  return {
220
- deleted: false,
225
+ deleted,
221
226
  provider: handle.provider,
222
- reason: "delete is not supported by the encrypted public-upload fallback provider",
227
+ ...(deleted
228
+ ? {}
229
+ : { reason: "backing upload provider could not delete the asset" }),
223
230
  };
224
231
  }
225
232
  throw new Error(`No private blob provider registered for ${handle.provider}`);
@@ -15,6 +15,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- error?: undefined;
19
18
  ok: boolean;
19
+ error?: undefined;
20
20
  }>>;
@@ -51,8 +51,8 @@ export declare function handleDeleteResource(event: any): Promise<{
51
51
  error: string;
52
52
  ok?: undefined;
53
53
  } | {
54
- error?: undefined;
55
54
  ok: boolean;
55
+ error?: undefined;
56
56
  }>;
57
57
  /** POST /_agent-native/resources/upload — upload a file as a resource */
58
58
  export declare function handleUploadResource(event: any): Promise<import("./store.js").Resource | {
@@ -73,10 +73,10 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
73
73
  runId: string | null;
74
74
  expiresAt: number | null;
75
75
  metadata: string | null;
76
- error?: undefined;
77
76
  url: string;
78
77
  provider: string;
79
78
  storageSetupRequired?: undefined;
79
+ error?: undefined;
80
80
  } | {
81
81
  error: string;
82
82
  storageSetupRequired: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.168.4",
3
+ "version": "0.168.6",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -428,8 +428,8 @@
428
428
  "y-protocols": "^1.0.7",
429
429
  "yjs": "^13.6.32",
430
430
  "zod": "^4.3.6",
431
- "@agent-native/toolkit": "^0.16.9",
432
- "@agent-native/recap-cli": "0.5.7"
431
+ "@agent-native/recap-cli": "0.5.7",
432
+ "@agent-native/toolkit": "^0.16.9"
433
433
  },
434
434
  "devDependencies": {
435
435
  "@ai-sdk/anthropic": "^3.0.71",