@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +6 -0
- package/corpus/templates/calendar/CHANGELOG.md +1 -0
- package/corpus/templates/clips/CHANGELOG.md +7 -0
- package/corpus/templates/clips/app/components/library/sort-menu.tsx +2 -2
- package/corpus/templates/clips/changelog/2026-09-17-clips-waits-for-authentication-before-showing-the-recording-.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-17-the-clips-sort-control-keeps-its-keyboard-focus-ring-visible.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/popup.html +1 -1
- package/corpus/templates/clips/chrome-extension/src/popup.ts +30 -21
- package/corpus/templates/content/CHANGELOG.md +7 -0
- package/corpus/templates/content/actions/resolve-preview-document-draft.ts +181 -16
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +28 -15
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +11 -0
- package/corpus/templates/content/app/components/editor/document-save-rebase.ts +13 -10
- package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +80 -0
- package/corpus/templates/content/changelog/2026-09-16-recovery-choices-stay-attached-to-the-draft-you-reviewed.md +6 -0
- package/corpus/templates/design/CHANGELOG.md +29 -0
- package/corpus/templates/design/actions/import-design-source.ts +42 -2
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +26 -15
- package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +83 -1
- package/corpus/templates/design/app/i18n-data.ts +3 -1
- package/corpus/templates/design/app/lib/design-file-upload.ts +6 -4
- package/corpus/templates/design/app/lib/design-import.ts +2 -0
- package/corpus/templates/design/app/lib/fig-client-import.ts +207 -49
- package/corpus/templates/design/changelog/2026-09-17-large-fig-files-import-directly-in-the-browser-without-the-o.md +6 -0
- package/corpus/templates/design/server/lib/fig-file-decoder.ts +31 -8
- package/corpus/templates/design/server/lib/import-design-files.ts +188 -75
- package/corpus/templates/design/server/plugins/db.ts +16 -7
- package/corpus/templates/design/shared/fig-to-frames.ts +55 -25
- package/corpus/templates/factory/CHANGELOG.md +6 -0
- package/corpus/templates/mail/CHANGELOG.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +12 -0
- package/corpus/templates/slides/app/components/editor/AssetLibraryPanel.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +1 -2
- package/corpus/templates/slides/app/hooks/use-agent-generating.ts +82 -2
- package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +9 -2
- package/corpus/templates/slides/app/lib/slide-image-replacement.ts +1 -1
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +1 -1
- package/corpus/templates/slides/server/handlers/assets.ts +134 -8
- package/corpus/templates/slides/server/handlers/uploads.ts +23 -10
- package/corpus/templates/slides/shared/upload-types.ts +1 -0
- package/dist/application-state/store.d.ts +9 -0
- package/dist/application-state/store.js +18 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +7 -0
- package/dist/file-upload/actions/upload-image.d.ts +41 -10
- package/dist/file-upload/actions/upload-image.js +399 -19
- package/dist/jobs/scheduler.js +11 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- 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
|
-
|
|
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,(.*)$/
|
|
136
|
+
const match = /^data:(image\/[^;]+);base64,(.*)$/is.exec(dataUrl);
|
|
137
|
+
const mediaType = match?.[1]?.trim().toLowerCase();
|
|
133
138
|
return Boolean(
|
|
134
139
|
match &&
|
|
135
|
-
|
|
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
|
|
|
@@ -52,7 +52,7 @@ async function requireSession(
|
|
|
52
52
|
return { session: auth.context as AuthedSlidesSession, error: null };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
// as image/svg+xml from the same origin.
|
|
138
|
-
if (!isRasterAssetExtension(ext)) {
|
|
258
|
+
if (!isImageAssetExtension(ext)) {
|
|
139
259
|
throw new Error(
|
|
140
|
-
"Only
|
|
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
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
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 ??
|
|
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;
|
package/dist/deploy/build.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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;
|