@agent-native/core 0.182.1-nightly-20260917183518 → 0.182.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 (56) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +6 -0
  3. package/corpus/templates/calendar/CHANGELOG.md +1 -0
  4. package/corpus/templates/clips/CHANGELOG.md +7 -0
  5. package/corpus/templates/clips/app/components/library/sort-menu.tsx +2 -2
  6. package/corpus/templates/clips/changelog/2026-09-17-clips-waits-for-authentication-before-showing-the-recording-.md +6 -0
  7. package/corpus/templates/clips/changelog/2026-09-17-the-clips-sort-control-keeps-its-keyboard-focus-ring-visible.md +6 -0
  8. package/corpus/templates/clips/chrome-extension/src/popup.html +1 -1
  9. package/corpus/templates/clips/chrome-extension/src/popup.ts +30 -21
  10. package/corpus/templates/content/CHANGELOG.md +7 -0
  11. package/corpus/templates/content/actions/resolve-preview-document-draft.ts +181 -16
  12. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +28 -15
  13. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +11 -0
  14. package/corpus/templates/content/app/components/editor/document-save-rebase.ts +13 -10
  15. package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +80 -0
  16. package/corpus/templates/content/changelog/2026-09-16-recovery-choices-stay-attached-to-the-draft-you-reviewed.md +6 -0
  17. package/corpus/templates/design/CHANGELOG.md +29 -0
  18. package/corpus/templates/design/actions/import-design-source.ts +42 -2
  19. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +26 -15
  20. package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +83 -1
  21. package/corpus/templates/design/app/i18n-data.ts +3 -1
  22. package/corpus/templates/design/app/lib/design-file-upload.ts +6 -4
  23. package/corpus/templates/design/app/lib/design-import.ts +2 -0
  24. package/corpus/templates/design/app/lib/fig-client-import.ts +207 -49
  25. package/corpus/templates/design/changelog/2026-09-17-large-fig-files-import-directly-in-the-browser-without-the-o.md +6 -0
  26. package/corpus/templates/design/server/lib/fig-file-decoder.ts +31 -8
  27. package/corpus/templates/design/server/lib/import-design-files.ts +188 -75
  28. package/corpus/templates/design/server/plugins/db.ts +16 -7
  29. package/corpus/templates/design/shared/fig-to-frames.ts +55 -25
  30. package/corpus/templates/factory/CHANGELOG.md +6 -0
  31. package/corpus/templates/mail/CHANGELOG.md +6 -0
  32. package/corpus/templates/slides/CHANGELOG.md +12 -0
  33. package/corpus/templates/slides/app/components/editor/AssetLibraryPanel.tsx +1 -1
  34. package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +1 -2
  35. package/corpus/templates/slides/app/hooks/use-agent-generating.ts +82 -2
  36. package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +9 -2
  37. package/corpus/templates/slides/app/lib/slide-image-replacement.ts +1 -1
  38. package/corpus/templates/slides/app/pages/DeckEditor.tsx +1 -1
  39. package/corpus/templates/slides/server/handlers/assets.ts +134 -8
  40. package/corpus/templates/slides/server/handlers/uploads.ts +23 -10
  41. package/corpus/templates/slides/shared/upload-types.ts +1 -0
  42. package/dist/application-state/store.d.ts +9 -0
  43. package/dist/application-state/store.js +18 -0
  44. package/dist/automation/index.d.ts +1 -1
  45. package/dist/collab/struct-routes.d.ts +1 -1
  46. package/dist/deploy/build.js +7 -0
  47. package/dist/file-upload/actions/upload-image.d.ts +41 -10
  48. package/dist/file-upload/actions/upload-image.js +399 -19
  49. package/dist/jobs/scheduler.js +11 -0
  50. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  51. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  52. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  53. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  54. package/dist/resources/handlers.d.ts +1 -1
  55. package/dist/secrets/routes.d.ts +3 -3
  56. package/package.json +4 -4
@@ -4,6 +4,7 @@ import {
4
4
  type AgentChatMessage,
5
5
  } from "@agent-native/core/client/agent-chat";
6
6
  import { useCallback, useEffect, useRef, useState } from "react";
7
+ import { toast } from "sonner";
7
8
 
8
9
  // This is only a lost-signal recovery guard. A long deck legitimately takes
9
10
  // several minutes because each slide is written and fit-checked separately.
@@ -12,6 +13,7 @@ const MAX_GENERATING_MS = 30 * 60 * 1000;
12
13
  // Gateway continuations can briefly report a stopped chat between model/tool
13
14
  // chunks. Keep generation UI and presence steady across that transport gap.
14
15
  export const CHAT_STOP_DEBOUNCE_MS = 4_000;
16
+ const CHAT_SUBMIT_TARGET_EVENT = "agentNative.chatSubmitTarget";
15
17
 
16
18
  type AgentGeneratingSubmitOptions = Pick<
17
19
  AgentChatMessage,
@@ -37,8 +39,13 @@ export function useAgentGenerating() {
37
39
  const engineConfigured = useAgentEngineConfigured();
38
40
  const [recentlyGenerating, setRecentlyGenerating] = useState(false);
39
41
  const [timedOut, setTimedOut] = useState(false);
42
+ const [runError, setRunError] = useState(false);
40
43
  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
41
44
  const stopDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
45
+ const activeSubmitRef = useRef<string | null>(null);
46
+ const activeTabRef = useRef<string | null>(null);
47
+ const generationActiveRef = useRef(false);
48
+ generationActiveRef.current = generating || recentlyGenerating;
42
49
 
43
50
  const clearWatchdog = useCallback(() => {
44
51
  if (timeoutRef.current !== null) {
@@ -62,9 +69,76 @@ export function useAgentGenerating() {
62
69
  clearWatchdog();
63
70
  setRecentlyGenerating(false);
64
71
  setTimedOut(false);
72
+ setRunError(false);
65
73
  }, [clearStopDebounce, clearWatchdog, providerMissing]);
66
74
 
67
75
  useEffect(() => {
76
+ const targetHandler = (event: Event) => {
77
+ const detail = (event as CustomEvent).detail;
78
+ if (
79
+ detail?.submitMessageId !== activeSubmitRef.current ||
80
+ typeof detail?.tabId !== "string"
81
+ ) {
82
+ return;
83
+ }
84
+ activeTabRef.current = detail.tabId;
85
+ };
86
+ const errorHandler = (event: Event) => {
87
+ const detail = (event as CustomEvent).detail;
88
+ const eventTabId =
89
+ typeof detail?.tabId === "string" ? detail.tabId : null;
90
+ if (
91
+ !generationActiveRef.current ||
92
+ !eventTabId ||
93
+ !activeTabRef.current ||
94
+ eventTabId !== activeTabRef.current ||
95
+ typeof detail?.message !== "string"
96
+ ) {
97
+ return;
98
+ }
99
+ activeSubmitRef.current = null;
100
+ activeTabRef.current = null;
101
+ clearStopDebounce();
102
+ clearWatchdog();
103
+ setRecentlyGenerating(false);
104
+ setTimedOut(false);
105
+ setRunError(true);
106
+ toast.error(detail.message, {
107
+ id:
108
+ typeof detail.runId === "string"
109
+ ? detail.runId
110
+ : `agent-run-error-${eventTabId}`,
111
+ });
112
+ };
113
+ const runningHandler = (event: Event) => {
114
+ const detail = (event as CustomEvent).detail;
115
+ if (
116
+ detail?.isRunning !== false ||
117
+ !activeTabRef.current ||
118
+ detail?.tabId !== activeTabRef.current
119
+ ) {
120
+ return;
121
+ }
122
+ activeSubmitRef.current = null;
123
+ activeTabRef.current = null;
124
+ };
125
+
126
+ window.addEventListener(CHAT_SUBMIT_TARGET_EVENT, targetHandler);
127
+ window.addEventListener("agent-chat:run-error", errorHandler);
128
+ window.addEventListener("agentNative.chatRunning", runningHandler);
129
+ return () => {
130
+ window.removeEventListener(CHAT_SUBMIT_TARGET_EVENT, targetHandler);
131
+ window.removeEventListener("agent-chat:run-error", errorHandler);
132
+ window.removeEventListener("agentNative.chatRunning", runningHandler);
133
+ };
134
+ }, [clearStopDebounce, clearWatchdog]);
135
+
136
+ useEffect(() => {
137
+ if (runError) {
138
+ clearStopDebounce();
139
+ clearWatchdog();
140
+ return clearStopDebounce;
141
+ }
68
142
  if (stopReason === "stopped") {
69
143
  clearStopDebounce();
70
144
  clearWatchdog();
@@ -93,6 +167,7 @@ export function useAgentGenerating() {
93
167
  generating,
94
168
  recentlyGenerating,
95
169
  stopReason,
170
+ runError,
96
171
  clearStopDebounce,
97
172
  clearWatchdog,
98
173
  ]);
@@ -111,16 +186,20 @@ export function useAgentGenerating() {
111
186
  context: string,
112
187
  options?: AgentGeneratingSubmitOptions,
113
188
  ) => {
189
+ const submitMessageId = `slides-submit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
114
190
  setTimedOut(false);
191
+ setRunError(false);
115
192
  clearWatchdog();
116
193
  timeoutRef.current = setTimeout(
117
194
  () => setTimedOut(true),
118
195
  MAX_GENERATING_MS,
119
196
  );
120
- send({
197
+ activeSubmitRef.current = submitMessageId;
198
+ activeTabRef.current = send({
121
199
  message,
122
200
  context,
123
201
  submit: true,
202
+ submitMessageId,
124
203
  ...options,
125
204
  } as AgentChatMessage & { attachments?: ReadonlyArray<unknown> });
126
205
  },
@@ -132,7 +211,8 @@ export function useAgentGenerating() {
132
211
  !providerMissing &&
133
212
  stopReason !== "stopped" &&
134
213
  (generating || recentlyGenerating) &&
135
- !timedOut,
214
+ !timedOut &&
215
+ !runError,
136
216
  submit,
137
217
  };
138
218
  }
@@ -121,6 +121,10 @@ export function buildImageDropAgentPayload(args: {
121
121
 
122
122
  export function canInlineImageFile(file: File): boolean {
123
123
  const mediaType = file.type || "application/octet-stream";
124
+ if (mediaType.split(";", 1)[0]?.trim().toLowerCase() === "image/svg+xml") {
125
+ return false;
126
+ }
127
+ if (/\.svg$/i.test(file.name)) return false;
124
128
  const encodedLength = Math.ceil(file.size / 3) * 4;
125
129
  return (
126
130
  encodedLength + `data:${mediaType};base64,`.length <=
@@ -129,10 +133,13 @@ export function canInlineImageFile(file: File): boolean {
129
133
  }
130
134
 
131
135
  export function canInlineImageDataUrl(dataUrl: string): boolean {
132
- const match = /^data:(image\/[^;]+);base64,(.*)$/s.exec(dataUrl);
136
+ const match = /^data:(image\/[^;]+);base64,(.*)$/is.exec(dataUrl);
137
+ const mediaType = match?.[1]?.trim().toLowerCase();
133
138
  return Boolean(
134
139
  match &&
135
- match[2].length + `data:${match[1]};base64,`.length <=
140
+ mediaType &&
141
+ mediaType !== "image/svg+xml" &&
142
+ match[2].length + `data:${mediaType};base64,`.length <=
136
143
  MAX_INLINE_IMAGE_BASE64_CHARS,
137
144
  );
138
145
  }
@@ -45,7 +45,7 @@ interface PlaceholderTarget {
45
45
  export function imageFileLooksSupported(file: File): boolean {
46
46
  return (
47
47
  file.type.startsWith("image/") ||
48
- /\.(?:png|jpe?g|gif|webp|avif|ico)$/i.test(file.name)
48
+ /\.(?:png|jpe?g|gif|webp|avif|ico|svg)$/i.test(file.name)
49
49
  );
50
50
  }
51
51
 
@@ -3054,7 +3054,7 @@ export default function DeckEditor() {
3054
3054
  <input
3055
3055
  ref={uploadInputRef}
3056
3056
  type="file"
3057
- accept="image/*"
3057
+ accept="image/*,.svg"
3058
3058
  onChange={handleDirectUpload}
3059
3059
  className="hidden"
3060
3060
  />
@@ -52,7 +52,7 @@ async function requireSession(
52
52
  return { session: auth.context as AuthedSlidesSession, error: null };
53
53
  }
54
54
 
55
- function isRasterAssetExtension(ext: string): boolean {
55
+ function isImageAssetExtension(ext: string): boolean {
56
56
  return new Set([
57
57
  ".jpg",
58
58
  ".jpeg",
@@ -61,6 +61,7 @@ function isRasterAssetExtension(ext: string): boolean {
61
61
  ".webp",
62
62
  ".avif",
63
63
  ".ico",
64
+ ".svg",
64
65
  ]).has(ext);
65
66
  }
66
67
 
@@ -68,6 +69,16 @@ function ascii(data: Uint8Array, start: number, end: number): string {
68
69
  return Buffer.from(data.subarray(start, end)).toString("ascii");
69
70
  }
70
71
 
72
+ export function hasExpectedSvgSignature(data: Uint8Array): boolean {
73
+ const head = Buffer.from(
74
+ data.subarray(0, Math.min(data.length, 8192)),
75
+ ).toString("utf8");
76
+ const normalized = head.replace(/^\uFEFF/, "").trimStart();
77
+ return /^(?:(?:\s|<!--[\s\S]*?-->|<\?xml\b[\s\S]*?\?>))*<svg(?:\s|\/?>)/i.test(
78
+ normalized,
79
+ );
80
+ }
81
+
71
82
  function hasExpectedImageSignature(ext: string, data: Uint8Array): boolean {
72
83
  if (ext === ".png") {
73
84
  return (
@@ -98,16 +109,127 @@ function hasExpectedImageSignature(ext: string, data: Uint8Array): boolean {
98
109
  if (ext === ".avif") {
99
110
  return ascii(data, 4, 12).includes("ftyp");
100
111
  }
112
+ if (ext === ".svg") {
113
+ return hasExpectedSvgSignature(data);
114
+ }
101
115
  return false;
102
116
  }
103
117
 
118
+ function decodeXmlReferences(source: string): string {
119
+ const namedReferences: Record<string, string> = {
120
+ amp: "&",
121
+ apos: "'",
122
+ gt: ">",
123
+ lt: "<",
124
+ quot: '"',
125
+ };
126
+ return source.replace(
127
+ /&#x([0-9a-f]+);|&#([0-9]+);|&([a-z]+);/gi,
128
+ (
129
+ match,
130
+ hex: string | undefined,
131
+ decimal: string | undefined,
132
+ named: string | undefined,
133
+ ) => {
134
+ const codePoint = hex
135
+ ? Number.parseInt(hex, 16)
136
+ : decimal
137
+ ? Number.parseInt(decimal, 10)
138
+ : undefined;
139
+ if (
140
+ codePoint !== undefined &&
141
+ codePoint <= 0x10ffff &&
142
+ !(codePoint >= 0xd800 && codePoint <= 0xdfff)
143
+ ) {
144
+ return String.fromCodePoint(codePoint);
145
+ }
146
+ return named ? (namedReferences[named.toLowerCase()] ?? match) : match;
147
+ },
148
+ );
149
+ }
150
+
151
+ function decodeCssEscapes(source: string): string {
152
+ return source
153
+ .replace(/\\(?:\r\n|[\r\n\f])/g, "")
154
+ .replace(
155
+ /\\([0-9a-f]{1,6})(?:[ \t\r\n\f])?|\\([^\r\n])/gi,
156
+ (match, hex: string | undefined, character: string | undefined) => {
157
+ if (!hex) return character ?? match;
158
+ const codePoint = Number.parseInt(hex, 16);
159
+ if (
160
+ codePoint > 0x10ffff ||
161
+ (codePoint >= 0xd800 && codePoint <= 0xdfff)
162
+ ) {
163
+ return match;
164
+ }
165
+ return String.fromCodePoint(codePoint);
166
+ },
167
+ );
168
+ }
169
+
170
+ export function isSafeSvg(data: Uint8Array): boolean {
171
+ let source: string;
172
+ try {
173
+ source = new TextDecoder("utf-8", { fatal: true }).decode(data);
174
+ } catch {
175
+ // coercion-ok: malformed UTF-8 must reject SVG validation.
176
+ return false;
177
+ }
178
+ source = source.replace(/^\uFEFF/, "").trim();
179
+ const normalizedSource = decodeCssEscapes(
180
+ decodeXmlReferences(source),
181
+ ).replace(/\/\*[\s\S]*?\*\//g, "");
182
+ const forbidden = [
183
+ /<\s*(?:script|foreignObject|iframe|object|embed|link|audio|video|animate(?:Transform|Motion|Color)?|set|discard)\b/i,
184
+ /<\s*\/?[a-z_][\w.-]*:[a-z_][\w.-]*\b/i,
185
+ /<!\s*(?:DOCTYPE|ENTITY)\b/i,
186
+ /<\?xml-stylesheet\b/i,
187
+ /\bxml:base\s*=/i,
188
+ /\son[a-z][a-z0-9:_-]*\s*=/i,
189
+ /\b(?:javascript|vbscript)\s*:/i,
190
+ /\b(?:expression|behavior|-moz-binding)\s*\(/i,
191
+ /@import\b/i,
192
+ /\b(?:image|(?:-webkit-)?image-set)\s*\(/i,
193
+ ];
194
+ if (forbidden.some((pattern) => pattern.test(normalizedSource))) return false;
195
+
196
+ for (const match of normalizedSource.matchAll(
197
+ /(?:href|xlink:href)\s*=\s*(?:(['"])(.*?)\1|([^\s>]+))/gi,
198
+ )) {
199
+ const target = (match[2] ?? match[3] ?? match[4] ?? "").trim();
200
+ if (
201
+ target &&
202
+ !target.startsWith("#") &&
203
+ !/^data:image\/(?:png|jpeg|gif|webp);base64,/i.test(target)
204
+ ) {
205
+ return false;
206
+ }
207
+ }
208
+ for (const match of normalizedSource.matchAll(
209
+ /url\(\s*(["']?)(.*?)\1\s*\)/gi,
210
+ )) {
211
+ const target = match[2]?.trim() ?? "";
212
+ if (
213
+ target &&
214
+ !target.startsWith("#") &&
215
+ !/^data:image\/(?:png|jpeg|gif|webp);base64,/i.test(target)
216
+ ) {
217
+ return false;
218
+ }
219
+ }
220
+ return true;
221
+ }
222
+
104
223
  export function canSaveAsUploadedAsset(args: {
105
224
  originalName: string;
106
225
  data: Uint8Array;
107
226
  }): boolean {
227
+ const ext = path.extname(args.originalName).toLowerCase();
108
228
  return (
109
229
  args.data.length <= MAX_ASSET_FILE_SIZE &&
110
- isRasterAssetExtension(path.extname(args.originalName).toLowerCase())
230
+ isImageAssetExtension(ext) &&
231
+ (ext !== ".svg" ||
232
+ (hasExpectedImageSignature(ext, args.data) && isSafeSvg(args.data)))
111
233
  );
112
234
  }
113
235
 
@@ -133,22 +255,26 @@ export async function uploadImageAsset(args: {
133
255
  }
134
256
 
135
257
  const ext = path.extname(args.originalName).toLowerCase();
136
- // SVG is excluded — it can embed <script> tags and execute when served
137
- // as image/svg+xml from the same origin.
138
- if (!isRasterAssetExtension(ext)) {
258
+ if (!isImageAssetExtension(ext)) {
139
259
  throw new Error(
140
- "Only raster image files are allowed (jpg, png, gif, webp, avif, ico)",
260
+ "Only image files are allowed (jpg, png, gif, webp, avif, ico, svg)",
141
261
  );
142
262
  }
143
263
  if (!hasExpectedImageSignature(ext, args.data)) {
144
264
  throw new Error("Uploaded image bytes do not match file extension");
145
265
  }
146
266
 
267
+ if (ext === ".svg" && !isSafeSvg(args.data)) {
268
+ throw new Error("SVG contains active content or external references");
269
+ }
270
+
271
+ const mimeType = ext === ".svg" ? "image/svg+xml" : args.type;
272
+
147
273
  const result = await runWithRequestContext({ userEmail: args.email }, () =>
148
274
  uploadFile({
149
275
  data: args.data,
150
276
  filename: args.originalName,
151
- mimeType: args.type,
277
+ mimeType,
152
278
  ownerEmail: args.email,
153
279
  }),
154
280
  );
@@ -164,7 +290,7 @@ export async function uploadImageAsset(args: {
164
290
  const asset: UploadedAsset = {
165
291
  url: result.url,
166
292
  filename: args.originalName,
167
- type: args.type || "application/octet-stream",
293
+ type: mimeType || "application/octet-stream",
168
294
  size: args.data.length,
169
295
  provider: result.provider,
170
296
  };
@@ -13,6 +13,7 @@ import {
13
13
  MAX_FIG_REFERENCE_FILE_BYTES,
14
14
  MAX_REFERENCE_FILE_BYTES,
15
15
  MAX_REFERENCE_FILES,
16
+ MAX_SVG_REFERENCE_FILE_BYTES,
16
17
  SLIDES_REFERENCE_FILE_ERROR_LABEL,
17
18
  isSlidesReferenceFileExtension,
18
19
  } from "../../shared/upload-types.js";
@@ -22,7 +23,12 @@ import {
22
23
  deleteUploadedReferenceBlob,
23
24
  storeUploadedReferenceBlob,
24
25
  } from "../lib/uploaded-reference-storage.js";
25
- import { canSaveAsUploadedAsset, uploadImageAsset } from "./assets.js";
26
+ import {
27
+ canSaveAsUploadedAsset,
28
+ hasExpectedSvgSignature,
29
+ isSafeSvg,
30
+ uploadImageAsset,
31
+ } from "./assets.js";
26
32
  import {
27
33
  resolveSlidesRequestAuth,
28
34
  withSlidesRequestContext,
@@ -31,6 +37,7 @@ import {
31
37
  export {
32
38
  MAX_FIG_REFERENCE_FILE_BYTES,
33
39
  MAX_REFERENCE_FILE_BYTES,
40
+ MAX_SVG_REFERENCE_FILE_BYTES,
34
41
  } from "../../shared/upload-types.js";
35
42
  const FIG_LOCAL_COPY_SIGNATURE = new Uint8Array([
36
43
  0x66, 0x69, 0x67, 0x2d, 0x6b, 0x69, 0x77, 0x69,
@@ -66,6 +73,9 @@ function ascii(data: Uint8Array, start: number, end: number): string {
66
73
  export function maxReferenceFileBytes(
67
74
  originalName: string | undefined,
68
75
  ): number {
76
+ if (path.extname(originalName ?? "").toLowerCase() === ".svg") {
77
+ return MAX_SVG_REFERENCE_FILE_BYTES;
78
+ }
69
79
  return path.extname(originalName ?? "").toLowerCase() === ".fig"
70
80
  ? MAX_FIG_REFERENCE_FILE_BYTES
71
81
  : MAX_REFERENCE_FILE_BYTES;
@@ -108,14 +118,7 @@ function hasExpectedSignature(ext: string, data: Uint8Array): boolean {
108
118
  return ascii(data, 0, 4) === "RIFF" && ascii(data, 8, 12) === "WEBP";
109
119
  }
110
120
  if (ext === ".svg") {
111
- const head = Buffer.from(
112
- data.subarray(0, Math.min(data.length, 8192)),
113
- ).toString("utf8");
114
- const normalized = head.replace(/^\uFEFF/, "").trimStart();
115
- return (
116
- /^<svg(?:\s|>)/i.test(normalized) ||
117
- /^<\?xml\b[\s\S]{0,4096}<svg(?:\s|>)/i.test(normalized)
118
- );
121
+ return hasExpectedSvgSignature(data);
119
122
  }
120
123
  return !data.subarray(0, 4096).includes(0);
121
124
  }
@@ -160,6 +163,10 @@ export async function saveUploadedReferenceFile(args: {
160
163
  `Unsupported file type. Allowed: ${SLIDES_REFERENCE_FILE_ERROR_LABEL}.`,
161
164
  );
162
165
  }
166
+ const maxBytes = maxReferenceFileBytes(args.originalName);
167
+ if (args.data.length > maxBytes) {
168
+ throw new Error(`File too large (max ${formatMaxFileSize(maxBytes)})`);
169
+ }
163
170
  const isDeclaredImage = [".png", ".jpg", ".jpeg", ".gif", ".webp"].includes(
164
171
  declaredExt,
165
172
  );
@@ -179,12 +186,18 @@ export async function saveUploadedReferenceFile(args: {
179
186
  if (!hasExpectedSignature(ext, args.data)) {
180
187
  throw new Error(`File contents do not match ${ext} upload type`);
181
188
  }
189
+ if (ext === ".svg" && !isSafeSvg(args.data)) {
190
+ throw new Error("SVG contains active content or external references");
191
+ }
182
192
  const assetOriginalName =
183
193
  ext === declaredExt
184
194
  ? args.originalName
185
195
  : `${path.basename(args.originalName, path.extname(args.originalName))}${ext}`;
186
196
  const resolvedType =
187
- detectedImage?.mimeType ?? (args.type || "application/octet-stream");
197
+ detectedImage?.mimeType ??
198
+ (declaredExt === ".svg"
199
+ ? "image/svg+xml"
200
+ : args.type || "application/octet-stream");
188
201
  let uploadedPath: string;
189
202
  if (isHostedSlidesRuntime()) {
190
203
  let reference: string | null;
@@ -27,6 +27,7 @@ export const SLIDES_REFERENCE_FILE_ERROR_LABEL =
27
27
 
28
28
  export const MAX_REFERENCE_FILES = 20;
29
29
  export const MAX_REFERENCE_FILE_BYTES = 50 * 1024 * 1024;
30
+ export const MAX_SVG_REFERENCE_FILE_BYTES = 10 * 1024 * 1024;
30
31
  export const MAX_INLINE_IMAGE_BASE64_CHARS = 1_000_000;
31
32
  export const MAX_INLINE_IMAGE_BYTES = 10 * 1024 * 1024;
32
33
  export const MAX_FIG_REFERENCE_FILE_BYTES = 200 * 1024 * 1024;
@@ -31,4 +31,13 @@ export declare function appStateList(sessionId: string, keyPrefix: string): Prom
31
31
  key: string;
32
32
  value: Record<string, unknown>;
33
33
  }>>;
34
+ /**
35
+ * List a bounded batch of keys across sessions for framework-owned cleanup.
36
+ * Callers must use the returned session id when conditionally deleting a row.
37
+ */
38
+ export declare function appStateListByKeyPrefix(keyPrefix: string, limit?: number): Promise<Array<{
39
+ sessionId: string;
40
+ key: string;
41
+ value: Record<string, unknown>;
42
+ }>>;
34
43
  export declare function appStateDeleteByPrefix(sessionId: string, keyPrefix: string, options?: StoreWriteOptions): Promise<number>;
@@ -217,6 +217,24 @@ export async function appStateList(sessionId, keyPrefix) {
217
217
  value: JSON.parse(row.value),
218
218
  }));
219
219
  }
220
+ /**
221
+ * List a bounded batch of keys across sessions for framework-owned cleanup.
222
+ * Callers must use the returned session id when conditionally deleting a row.
223
+ */
224
+ export async function appStateListByKeyPrefix(keyPrefix, limit = 100) {
225
+ await ensureTable();
226
+ const client = getDbExec();
227
+ const boundedLimit = Math.max(1, Math.min(Math.floor(limit), 2_048));
228
+ const { rows } = await client.execute({
229
+ sql: `SELECT session_id, key, value FROM application_state WHERE key LIKE ? ESCAPE '!' ORDER BY updated_at ASC LIMIT ?`,
230
+ args: [escapeLike(keyPrefix) + "%", boundedLimit],
231
+ });
232
+ return rows.map((row) => ({
233
+ sessionId: row.session_id,
234
+ key: row.key,
235
+ value: JSON.parse(row.value),
236
+ }));
237
+ }
220
238
  export async function appStateDeleteByPrefix(sessionId, keyPrefix, options) {
221
239
  await ensureTable();
222
240
  const client = getDbExec();
@@ -186,8 +186,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
186
186
  duplicate?: undefined;
187
187
  eventId?: undefined;
188
188
  } | {
189
+ error?: undefined;
189
190
  accepted: boolean;
190
191
  duplicate: boolean;
191
192
  eventId: string;
192
- error?: undefined;
193
193
  }>;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- ok?: undefined;
17
16
  error: string;
17
+ ok?: undefined;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -768,6 +768,12 @@ export const CLOUDFLARE_WORKER_NODE_BUILTIN_STUB_MODULES = {
768
768
  "moveCursor",
769
769
  ]),
770
770
  repl: cloudflareNodeBuiltinStubSource("repl", ["start"]),
771
+ sqlite: cloudflareNodeBuiltinStubSource("sqlite", [
772
+ "DatabaseSync",
773
+ "StatementSync",
774
+ "backup",
775
+ "constants",
776
+ ]),
771
777
  sys: cloudflareNodeBuiltinStubSource("sys", [
772
778
  "debug",
773
779
  "deprecate",
@@ -2443,6 +2449,7 @@ const NODE_BUILTINS = [
2443
2449
  "stream",
2444
2450
  "stream/web",
2445
2451
  "string_decoder",
2452
+ "sqlite",
2446
2453
  "sys",
2447
2454
  "timers",
2448
2455
  "tls",
@@ -1,27 +1,58 @@
1
+ export declare const UPLOAD_RECEIPT_PREFIX = "file-upload-receipt:";
2
+ /** Commit every image receipt for a completed browser import batch. */
3
+ export declare function commitUploadReceiptsForImport(importId: string): Promise<void>;
4
+ export declare function runUploadReceiptCleanupOnce(options?: {
5
+ force?: boolean;
6
+ now?: number;
7
+ limit?: number;
8
+ }): Promise<{
9
+ scanned: number;
10
+ deleted: number;
11
+ released: number;
12
+ failed: number;
13
+ skipped: boolean;
14
+ }>;
1
15
  declare const _default: import("../../action.js").ActionDefinition<{
2
16
  data?: string;
3
17
  url?: string;
4
18
  filename?: string;
19
+ idempotencyKey?: string;
20
+ cleanup?: "delete" | "release";
5
21
  }, {
6
- error: string;
7
- configured?: undefined;
8
- connectPath?: undefined;
9
- url?: undefined;
10
- id?: undefined;
11
- provider?: undefined;
22
+ idempotencyKey: string;
23
+ alreadyMissing: boolean;
24
+ released?: undefined;
25
+ deleted?: undefined;
26
+ } | {
27
+ alreadyMissing?: undefined;
28
+ idempotencyKey: string;
29
+ released: boolean;
30
+ deleted?: undefined;
31
+ } | {
32
+ alreadyMissing?: undefined;
33
+ released?: undefined;
34
+ idempotencyKey: string;
35
+ deleted: boolean;
12
36
  } | {
13
37
  error: string;
14
- configured: boolean;
15
- connectPath: string;
16
38
  url?: undefined;
17
39
  id?: undefined;
18
40
  provider?: undefined;
19
- } | {
20
- error?: undefined;
21
41
  configured?: undefined;
22
42
  connectPath?: undefined;
43
+ } | {
23
44
  url: string;
24
45
  id: string;
25
46
  provider: string;
47
+ configured?: undefined;
48
+ connectPath?: undefined;
49
+ error?: undefined;
50
+ } | {
51
+ url?: undefined;
52
+ id?: undefined;
53
+ provider?: undefined;
54
+ error: string;
55
+ configured: boolean;
56
+ connectPath: string;
26
57
  }>;
27
58
  export default _default;