@agent-native/core 0.101.3 → 0.101.4
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/core/CHANGELOG.md +7 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/RunStuckBanner.tsx +12 -5
- package/corpus/core/src/client/sse-event-processor.ts +36 -1
- package/corpus/core/src/client/use-run-stuck-detection.ts +162 -25
- package/corpus/core/src/deploy/build.ts +78 -25
- package/corpus/templates/chat/changelog/2026-07-14-chat-opens-reliably-on-hosted-deployments-instead-of-failing.md +6 -0
- package/corpus/templates/clips/app/components/player/media-duration.ts +24 -0
- package/corpus/templates/clips/app/components/player/video-player.tsx +9 -18
- package/corpus/templates/clips/changelog/2026-07-14-paused-time-no-longer-counts-toward-chrome-extension-recordi.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +30 -4
- package/corpus/templates/clips/chrome-extension/src/recording-duration.ts +32 -0
- package/corpus/templates/slides/actions/_uploaded-files.ts +13 -2
- package/corpus/templates/slides/actions/import-docx.ts +3 -9
- package/corpus/templates/slides/actions/import-file.ts +11 -14
- package/corpus/templates/slides/actions/import-pptx.ts +3 -9
- package/corpus/templates/slides/changelog/2026-07-14-powerpoint-template-uploads-now-work-in-hosted-slides-deploy.md +6 -0
- package/corpus/templates/slides/server/handlers/uploads.ts +100 -50
- package/corpus/templates/slides/server/lib/tenant-files.ts +38 -11
- package/corpus/templates/slides/server/lib/uploaded-reference-storage.ts +108 -0
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +9 -5
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +33 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts +4 -4
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +104 -19
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/deploy/build.d.ts +15 -14
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +64 -23
- package/dist/deploy/build.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
|
@@ -36,6 +36,13 @@ import {
|
|
|
36
36
|
import { MAX_UPLOAD_BYTES } from "@shared/upload-limits";
|
|
37
37
|
|
|
38
38
|
import { waitForReadyRecordingAfterFinalizeError } from "./finalize-recovery";
|
|
39
|
+
import {
|
|
40
|
+
createRecordingDurationState,
|
|
41
|
+
pauseRecordingDuration,
|
|
42
|
+
resumeRecordingDuration,
|
|
43
|
+
startRecordingDuration,
|
|
44
|
+
type RecordingDurationState,
|
|
45
|
+
} from "./recording-duration";
|
|
39
46
|
import { captureExtensionError, initExtensionSentry } from "./sentry";
|
|
40
47
|
|
|
41
48
|
initExtensionSentry("offscreen");
|
|
@@ -187,6 +194,7 @@ type ActiveRecording = {
|
|
|
187
194
|
authToken: string | null;
|
|
188
195
|
mode: CaptureMode;
|
|
189
196
|
startedAtMs: number;
|
|
197
|
+
duration: RecordingDurationState;
|
|
190
198
|
mimeType: string;
|
|
191
199
|
recorder: MediaRecorder;
|
|
192
200
|
outputStream: MediaStream;
|
|
@@ -1218,6 +1226,7 @@ async function begin(message: BeginMessage): Promise<{
|
|
|
1218
1226
|
authToken: message.authToken ?? null,
|
|
1219
1227
|
mode: ready.mode,
|
|
1220
1228
|
startedAtMs: 0,
|
|
1229
|
+
duration: createRecordingDurationState(),
|
|
1221
1230
|
mimeType,
|
|
1222
1231
|
recorder,
|
|
1223
1232
|
outputStream,
|
|
@@ -1350,6 +1359,7 @@ function startRecorderNow(recording: ActiveRecording): void {
|
|
|
1350
1359
|
playStartChime();
|
|
1351
1360
|
recording.recorder.start(2000);
|
|
1352
1361
|
recording.startedAtMs = Date.now();
|
|
1362
|
+
recording.duration = startRecordingDuration(recording.startedAtMs);
|
|
1353
1363
|
console.log("[clips-offscreen] recorder.start ok");
|
|
1354
1364
|
reportStatus(recording.sessionId, "recording", {
|
|
1355
1365
|
recordingId: recording.recordingId,
|
|
@@ -1446,6 +1456,11 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
|
|
|
1446
1456
|
recording.resolveStopped({ ok: true, status: "cancelled" });
|
|
1447
1457
|
return;
|
|
1448
1458
|
}
|
|
1459
|
+
// Freeze the media duration as soon as MediaRecorder stops. Waiting for
|
|
1460
|
+
// outstanding uploads below must not make the clip appear longer, and time
|
|
1461
|
+
// spent paused is excluded by pauseRecordingDuration().
|
|
1462
|
+
recording.duration = pauseRecordingDuration(recording.duration, Date.now());
|
|
1463
|
+
const durationMs = recording.duration.elapsedMs;
|
|
1449
1464
|
reportStatus(recording.sessionId, "uploading", {
|
|
1450
1465
|
recordingId: recording.recordingId,
|
|
1451
1466
|
});
|
|
@@ -1460,7 +1475,6 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
|
|
|
1460
1475
|
? rejected.reason
|
|
1461
1476
|
: new Error(String(rejected.reason));
|
|
1462
1477
|
}
|
|
1463
|
-
const durationMs = Math.max(0, Date.now() - recording.startedAtMs);
|
|
1464
1478
|
// Surface WHY a finalize might fail before the server's cryptic "No chunks
|
|
1465
1479
|
// found": 0 chunks means the recorder emitted no non-empty data (empty
|
|
1466
1480
|
// capture / never started), which is a different problem than an auth 401.
|
|
@@ -1549,7 +1563,7 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
|
|
|
1549
1563
|
recordingId: recording.recordingId,
|
|
1550
1564
|
chunkCount: recording.chunkIndex,
|
|
1551
1565
|
mimeType: recording.mimeType,
|
|
1552
|
-
durationMs
|
|
1566
|
+
durationMs,
|
|
1553
1567
|
},
|
|
1554
1568
|
});
|
|
1555
1569
|
// The upload failed — save the buffered recording to disk so it isn't lost.
|
|
@@ -1574,7 +1588,13 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
|
|
|
1574
1588
|
function pause(message: SimpleMessage): { ok: boolean } {
|
|
1575
1589
|
const recording = activeRecording;
|
|
1576
1590
|
if (recording && recording.sessionId === message.sessionId) {
|
|
1577
|
-
if (recording.recorder.state === "recording")
|
|
1591
|
+
if (recording.recorder.state === "recording") {
|
|
1592
|
+
recording.recorder.pause();
|
|
1593
|
+
recording.duration = pauseRecordingDuration(
|
|
1594
|
+
recording.duration,
|
|
1595
|
+
Date.now(),
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1578
1598
|
reportStatus(recording.sessionId, "paused", {
|
|
1579
1599
|
recordingId: recording.recordingId,
|
|
1580
1600
|
});
|
|
@@ -1585,7 +1605,13 @@ function pause(message: SimpleMessage): { ok: boolean } {
|
|
|
1585
1605
|
function resume(message: SimpleMessage): { ok: boolean } {
|
|
1586
1606
|
const recording = activeRecording;
|
|
1587
1607
|
if (recording && recording.sessionId === message.sessionId) {
|
|
1588
|
-
if (recording.recorder.state === "paused")
|
|
1608
|
+
if (recording.recorder.state === "paused") {
|
|
1609
|
+
recording.recorder.resume();
|
|
1610
|
+
recording.duration = resumeRecordingDuration(
|
|
1611
|
+
recording.duration,
|
|
1612
|
+
Date.now(),
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1589
1615
|
reportStatus(recording.sessionId, "recording", {
|
|
1590
1616
|
recordingId: recording.recordingId,
|
|
1591
1617
|
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type RecordingDurationState = {
|
|
2
|
+
elapsedMs: number;
|
|
3
|
+
activeSinceMs: number | null;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function createRecordingDurationState(): RecordingDurationState {
|
|
7
|
+
return { elapsedMs: 0, activeSinceMs: null };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function startRecordingDuration(nowMs: number): RecordingDurationState {
|
|
11
|
+
return { elapsedMs: 0, activeSinceMs: nowMs };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function pauseRecordingDuration(
|
|
15
|
+
state: RecordingDurationState,
|
|
16
|
+
nowMs: number,
|
|
17
|
+
): RecordingDurationState {
|
|
18
|
+
if (state.activeSinceMs === null) return state;
|
|
19
|
+
return {
|
|
20
|
+
elapsedMs:
|
|
21
|
+
state.elapsedMs + Math.max(0, Math.round(nowMs - state.activeSinceMs)),
|
|
22
|
+
activeSinceMs: null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resumeRecordingDuration(
|
|
27
|
+
state: RecordingDurationState,
|
|
28
|
+
nowMs: number,
|
|
29
|
+
): RecordingDurationState {
|
|
30
|
+
if (state.activeSinceMs !== null) return state;
|
|
31
|
+
return { ...state, activeSinceMs: nowMs };
|
|
32
|
+
}
|
|
@@ -4,11 +4,19 @@ import path from "path";
|
|
|
4
4
|
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
5
5
|
|
|
6
6
|
import { tenantUploadDir } from "../server/lib/tenant-files.js";
|
|
7
|
+
import { readUploadedReferenceBlob } from "../server/lib/uploaded-reference-storage.js";
|
|
7
8
|
|
|
8
|
-
export function
|
|
9
|
+
export async function readUserUploadedFile(
|
|
10
|
+
filePath: string,
|
|
11
|
+
): Promise<{ data: Buffer; filename: string }> {
|
|
9
12
|
const email = getRequestUserEmail();
|
|
10
13
|
if (!email) throw new Error("no authenticated user");
|
|
11
14
|
|
|
15
|
+
const privateUpload = await readUploadedReferenceBlob(filePath, email);
|
|
16
|
+
if (privateUpload) {
|
|
17
|
+
return privateUpload;
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
const allowedDir = tenantUploadDir(email);
|
|
13
21
|
const absPath = path.isAbsolute(filePath)
|
|
14
22
|
? filePath
|
|
@@ -23,5 +31,8 @@ export function resolveUserUploadedFile(filePath: string): string {
|
|
|
23
31
|
if (!fs.existsSync(resolved)) {
|
|
24
32
|
throw new Error(`File not found: ${filePath}`);
|
|
25
33
|
}
|
|
26
|
-
return
|
|
34
|
+
return {
|
|
35
|
+
data: await fs.promises.readFile(resolved),
|
|
36
|
+
filename: path.basename(resolved),
|
|
37
|
+
};
|
|
27
38
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
|
|
3
1
|
import { defineAction } from "@agent-native/core";
|
|
4
2
|
import { z } from "zod";
|
|
5
3
|
|
|
6
4
|
import { parseDocx } from "../server/handlers/import/docx-parser.js";
|
|
7
5
|
import { convertSectionsToSlides } from "../server/handlers/import/html-converter.js";
|
|
8
|
-
import {
|
|
6
|
+
import { readUserUploadedFile } from "./_uploaded-files.js";
|
|
9
7
|
|
|
10
8
|
export default defineAction({
|
|
11
9
|
description:
|
|
@@ -16,9 +14,7 @@ export default defineAction({
|
|
|
16
14
|
schema: z.object({
|
|
17
15
|
filePath: z
|
|
18
16
|
.string()
|
|
19
|
-
.describe(
|
|
20
|
-
"Server path to the uploaded DOCX file (e.g. data/uploads/document.docx)",
|
|
21
|
-
),
|
|
17
|
+
.describe("Uploaded DOCX path or opaque hosted upload reference"),
|
|
22
18
|
convertToSlides: z
|
|
23
19
|
.boolean()
|
|
24
20
|
.optional()
|
|
@@ -28,9 +24,7 @@ export default defineAction({
|
|
|
28
24
|
),
|
|
29
25
|
}),
|
|
30
26
|
run: async ({ filePath, convertToSlides }) => {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const fileBuffer = await fs.promises.readFile(absPath);
|
|
27
|
+
const { data: fileBuffer } = await readUserUploadedFile(filePath);
|
|
34
28
|
const doc = await parseDocx(fileBuffer);
|
|
35
29
|
|
|
36
30
|
const result: Record<string, unknown> = {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
1
|
import path from "path";
|
|
3
2
|
|
|
4
3
|
import { defineAction } from "@agent-native/core";
|
|
@@ -15,7 +14,7 @@ import { z } from "zod";
|
|
|
15
14
|
import { getDb, schema } from "../server/db/index.js";
|
|
16
15
|
import { notifyClients } from "../server/handlers/decks.js";
|
|
17
16
|
import { upsertBuilderProxyDesignSystem } from "../server/lib/builder-design-system-proxy.js";
|
|
18
|
-
import {
|
|
17
|
+
import { readUserUploadedFile } from "./_uploaded-files.js";
|
|
19
18
|
|
|
20
19
|
const DEFAULT_MAX_SOURCE_CHARS = 60_000;
|
|
21
20
|
|
|
@@ -30,9 +29,7 @@ export default defineAction({
|
|
|
30
29
|
schema: z.object({
|
|
31
30
|
filePath: z
|
|
32
31
|
.string()
|
|
33
|
-
.describe(
|
|
34
|
-
"Server path to the uploaded file (e.g. data/uploads/file.pptx)",
|
|
35
|
-
),
|
|
32
|
+
.describe("Uploaded file path or opaque hosted upload reference"),
|
|
36
33
|
format: z
|
|
37
34
|
.enum(["pptx", "docx", "pdf", "fig", "auto"])
|
|
38
35
|
.optional()
|
|
@@ -60,15 +57,15 @@ export default defineAction({
|
|
|
60
57
|
),
|
|
61
58
|
}),
|
|
62
59
|
run: async ({ filePath, format, deckId, importIntoDeck, maxChars }) => {
|
|
63
|
-
const
|
|
60
|
+
const uploaded = await readUserUploadedFile(filePath);
|
|
64
61
|
const sourceLimit = maxChars ?? DEFAULT_MAX_SOURCE_CHARS;
|
|
65
|
-
|
|
66
|
-
const
|
|
62
|
+
const fileBuffer = uploaded.data;
|
|
63
|
+
const filename = uploaded.filename;
|
|
67
64
|
|
|
68
65
|
// Detect format from extension if auto
|
|
69
66
|
let detectedFormat = format;
|
|
70
67
|
if (detectedFormat === "auto") {
|
|
71
|
-
const ext = path.extname(
|
|
68
|
+
const ext = path.extname(filename).toLowerCase();
|
|
72
69
|
if (ext === ".pptx") detectedFormat = "pptx";
|
|
73
70
|
else if (ext === ".docx") detectedFormat = "docx";
|
|
74
71
|
else if (ext === ".pdf") detectedFormat = "pdf";
|
|
@@ -86,12 +83,12 @@ export default defineAction({
|
|
|
86
83
|
"Figma .fig imports start Builder design-system indexing, not slide replacements. Re-run without importIntoDeck.",
|
|
87
84
|
);
|
|
88
85
|
}
|
|
89
|
-
const title = titleFromPath(
|
|
86
|
+
const title = titleFromPath(filename);
|
|
90
87
|
const result = await startBuilderDesignSystemIndex({
|
|
91
88
|
projectName: title,
|
|
92
89
|
files: [
|
|
93
90
|
{
|
|
94
|
-
name: path.basename(
|
|
91
|
+
name: path.basename(filename),
|
|
95
92
|
data: fileBuffer,
|
|
96
93
|
mimeType: "application/octet-stream",
|
|
97
94
|
},
|
|
@@ -126,7 +123,7 @@ export default defineAction({
|
|
|
126
123
|
const { convertToSlideHtml } =
|
|
127
124
|
await import("../server/handlers/import/html-converter.js");
|
|
128
125
|
const presentation = await parsePptx(fileBuffer);
|
|
129
|
-
const title = presentation.title || titleFromPath(
|
|
126
|
+
const title = presentation.title || titleFromPath(filename);
|
|
130
127
|
|
|
131
128
|
if (importIntoDeck) {
|
|
132
129
|
if (!deckId) throw new Error("deckId is required to import into deck");
|
|
@@ -172,7 +169,7 @@ export default defineAction({
|
|
|
172
169
|
await import("../server/handlers/import/html-converter.js");
|
|
173
170
|
const doc = await parseDocx(fileBuffer);
|
|
174
171
|
const slideHtmlArray = convertSectionsToSlides(doc.sections);
|
|
175
|
-
const title = doc.title || titleFromPath(
|
|
172
|
+
const title = doc.title || titleFromPath(filename);
|
|
176
173
|
|
|
177
174
|
if (importIntoDeck) {
|
|
178
175
|
if (!deckId) throw new Error("deckId is required to import into deck");
|
|
@@ -221,7 +218,7 @@ export default defineAction({
|
|
|
221
218
|
const result = await pdf.getText();
|
|
222
219
|
const pages = normalizePdfPages(result);
|
|
223
220
|
const textPages = pages.filter((p) => p.text.trim());
|
|
224
|
-
const title = titleFromPath(
|
|
221
|
+
const title = titleFromPath(filename);
|
|
225
222
|
|
|
226
223
|
if (textPages.length === 0) {
|
|
227
224
|
throw new Error(
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
|
|
3
1
|
import { defineAction } from "@agent-native/core";
|
|
4
2
|
import { writeAppState } from "@agent-native/core/application-state";
|
|
5
3
|
import {
|
|
@@ -15,7 +13,7 @@ import { notifyClients } from "../server/handlers/decks.js";
|
|
|
15
13
|
import { convertToSlideHtml } from "../server/handlers/import/html-converter.js";
|
|
16
14
|
import { parsePptx } from "../server/handlers/import/pptx-parser.js";
|
|
17
15
|
import { getDeckUrl } from "./_app-url.js";
|
|
18
|
-
import {
|
|
16
|
+
import { readUserUploadedFile } from "./_uploaded-files.js";
|
|
19
17
|
|
|
20
18
|
export default defineAction({
|
|
21
19
|
description:
|
|
@@ -26,9 +24,7 @@ export default defineAction({
|
|
|
26
24
|
schema: z.object({
|
|
27
25
|
filePath: z
|
|
28
26
|
.string()
|
|
29
|
-
.describe(
|
|
30
|
-
"Server path to the uploaded PPTX file (e.g. data/uploads/presentation.pptx)",
|
|
31
|
-
),
|
|
27
|
+
.describe("Uploaded PPTX path or opaque hosted upload reference"),
|
|
32
28
|
deckId: z
|
|
33
29
|
.string()
|
|
34
30
|
.optional()
|
|
@@ -43,9 +39,7 @@ export default defineAction({
|
|
|
43
39
|
),
|
|
44
40
|
}),
|
|
45
41
|
run: async ({ filePath, deckId, title }) => {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const fileBuffer = await fs.promises.readFile(absPath);
|
|
42
|
+
const { data: fileBuffer } = await readUserUploadedFile(filePath);
|
|
49
43
|
const presentation = await parsePptx(fileBuffer);
|
|
50
44
|
|
|
51
45
|
const deckTitle = title || presentation.title || "Imported Presentation";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
|
|
4
|
-
import { getSession } from "@agent-native/core/server";
|
|
5
4
|
import {
|
|
6
5
|
defineEventHandler,
|
|
7
6
|
setResponseStatus,
|
|
@@ -14,7 +13,15 @@ import {
|
|
|
14
13
|
isSlidesReferenceFileExtension,
|
|
15
14
|
} from "../../shared/upload-types";
|
|
16
15
|
import { tenantUploadDir } from "../lib/tenant-files.js";
|
|
16
|
+
import {
|
|
17
|
+
isHostedSlidesRuntime,
|
|
18
|
+
storeUploadedReferenceBlob,
|
|
19
|
+
} from "../lib/uploaded-reference-storage.js";
|
|
17
20
|
import { canSaveAsUploadedAsset, uploadImageAsset } from "./assets.js";
|
|
21
|
+
import {
|
|
22
|
+
resolveSlidesRequestAuthContext,
|
|
23
|
+
withSlidesRequestContext,
|
|
24
|
+
} from "./request-auth-context.js";
|
|
18
25
|
|
|
19
26
|
export const MAX_REFERENCE_FILE_BYTES = 50 * 1024 * 1024;
|
|
20
27
|
export const MAX_FIG_REFERENCE_FILE_BYTES = 200 * 1024 * 1024;
|
|
@@ -113,6 +120,7 @@ function pathForAgent(absPath: string): string {
|
|
|
113
120
|
|
|
114
121
|
export async function saveUploadedReferenceFile(args: {
|
|
115
122
|
email: string;
|
|
123
|
+
orgId?: string | null;
|
|
116
124
|
originalName: string;
|
|
117
125
|
data: Uint8Array;
|
|
118
126
|
type?: string;
|
|
@@ -127,14 +135,43 @@ export async function saveUploadedReferenceFile(args: {
|
|
|
127
135
|
if (!hasExpectedSignature(ext, args.data)) {
|
|
128
136
|
throw new Error(`File contents do not match ${ext} upload type`);
|
|
129
137
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
let uploadedPath: string;
|
|
139
|
+
if (isHostedSlidesRuntime()) {
|
|
140
|
+
let reference: string | null;
|
|
141
|
+
try {
|
|
142
|
+
reference = await storeUploadedReferenceBlob({
|
|
143
|
+
email: args.email,
|
|
144
|
+
orgId: args.orgId,
|
|
145
|
+
data: args.data,
|
|
146
|
+
filename,
|
|
147
|
+
mimeType: args.type || "application/octet-stream",
|
|
148
|
+
});
|
|
149
|
+
} catch {
|
|
150
|
+
throw Object.assign(
|
|
151
|
+
new Error("Private file storage failed while saving the upload."),
|
|
152
|
+
{ statusCode: 503 },
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (!reference) {
|
|
156
|
+
throw Object.assign(
|
|
157
|
+
new Error(
|
|
158
|
+
"Private file storage is not configured. Connect Builder.io or another file provider before uploading reference files in a hosted Slides deployment.",
|
|
159
|
+
),
|
|
160
|
+
{ statusCode: 503 },
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
uploadedPath = reference;
|
|
164
|
+
} else {
|
|
165
|
+
const uploadDir = tenantUploadDir(args.email);
|
|
166
|
+
await fs.promises.mkdir(uploadDir, { recursive: true });
|
|
167
|
+
const destPath = path.join(uploadDir, filename);
|
|
168
|
+
await fs.promises.writeFile(destPath, args.data);
|
|
169
|
+
uploadedPath = pathForAgent(destPath);
|
|
170
|
+
}
|
|
171
|
+
// For images, also push to the public file-upload provider so the agent can
|
|
172
|
+
// embed a hosted URL (in slide HTML, chat replies, etc.). The `path` above
|
|
173
|
+
// remains the private import source: a tenant path locally and an encrypted,
|
|
174
|
+
// owner-scoped blob reference in hosted deployments.
|
|
138
175
|
let url: string | undefined;
|
|
139
176
|
if (
|
|
140
177
|
canSaveAsUploadedAsset({
|
|
@@ -159,7 +196,7 @@ export async function saveUploadedReferenceFile(args: {
|
|
|
159
196
|
}
|
|
160
197
|
}
|
|
161
198
|
return {
|
|
162
|
-
path:
|
|
199
|
+
path: uploadedPath,
|
|
163
200
|
url,
|
|
164
201
|
originalName: args.originalName,
|
|
165
202
|
filename,
|
|
@@ -170,54 +207,67 @@ export async function saveUploadedReferenceFile(args: {
|
|
|
170
207
|
|
|
171
208
|
// Upload one or more files
|
|
172
209
|
export const uploadFiles = defineEventHandler(async (event) => {
|
|
173
|
-
const
|
|
174
|
-
|
|
210
|
+
const authContext = await resolveSlidesRequestAuthContext(event);
|
|
211
|
+
const email = authContext.email;
|
|
212
|
+
if (!email) {
|
|
175
213
|
setResponseStatus(event, 401);
|
|
176
214
|
return { error: "Unauthorized" };
|
|
177
215
|
}
|
|
178
216
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
217
|
+
return withSlidesRequestContext(
|
|
218
|
+
event,
|
|
219
|
+
async ({ orgId }) => {
|
|
220
|
+
const parts = await readMultipartFormData(event);
|
|
221
|
+
const fileParts =
|
|
222
|
+
parts?.filter(
|
|
223
|
+
(p) => (p.name === "files" || p.name === "file") && p.data,
|
|
224
|
+
) ?? [];
|
|
183
225
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
226
|
+
if (fileParts.length === 0) {
|
|
227
|
+
setResponseStatus(event, 400);
|
|
228
|
+
return { error: "No files uploaded" };
|
|
229
|
+
}
|
|
188
230
|
|
|
189
|
-
|
|
231
|
+
const MAX_FILES = 20;
|
|
190
232
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
233
|
+
if (fileParts.length > MAX_FILES) {
|
|
234
|
+
setResponseStatus(event, 413);
|
|
235
|
+
return { error: `Too many files (max ${MAX_FILES})` };
|
|
236
|
+
}
|
|
195
237
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
238
|
+
const oversized = fileParts.find(
|
|
239
|
+
(p) => p.data.length > maxReferenceFileBytes(p.filename),
|
|
240
|
+
);
|
|
241
|
+
if (oversized) {
|
|
242
|
+
const limit = maxReferenceFileBytes(oversized.filename);
|
|
243
|
+
setResponseStatus(event, 413);
|
|
244
|
+
return { error: `File too large (max ${formatMaxFileSize(limit)})` };
|
|
245
|
+
}
|
|
204
246
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
247
|
+
let results;
|
|
248
|
+
try {
|
|
249
|
+
results = await Promise.all(
|
|
250
|
+
fileParts.map(async (part) => {
|
|
251
|
+
return saveUploadedReferenceFile({
|
|
252
|
+
email,
|
|
253
|
+
orgId,
|
|
254
|
+
originalName: part.filename || "upload",
|
|
255
|
+
data: part.data,
|
|
256
|
+
type: part.type,
|
|
257
|
+
});
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
const statusCode =
|
|
262
|
+
typeof (err as { statusCode?: unknown })?.statusCode === "number"
|
|
263
|
+
? (err as { statusCode: number }).statusCode
|
|
264
|
+
: 400;
|
|
265
|
+
setResponseStatus(event, statusCode);
|
|
266
|
+
return { error: err instanceof Error ? err.message : "Invalid upload" };
|
|
267
|
+
}
|
|
221
268
|
|
|
222
|
-
|
|
269
|
+
return results;
|
|
270
|
+
},
|
|
271
|
+
authContext,
|
|
272
|
+
);
|
|
223
273
|
});
|
|
@@ -10,26 +10,53 @@ export function tenantFileKey(email: string): string {
|
|
|
10
10
|
.slice(0, 24);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function tenantUploadDir(email: string): string {
|
|
14
|
-
return path.join(
|
|
13
|
+
export function tenantUploadDir(email: string, cwd = process.cwd()): string {
|
|
14
|
+
return path.join(cwd, "data", "uploads", tenantFileKey(email));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function
|
|
17
|
+
export function isHostedSlidesRuntime(
|
|
18
|
+
cwd = process.cwd(),
|
|
19
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
20
|
+
): boolean {
|
|
21
|
+
if (env.NETLIFY && env.NETLIFY !== "false" && env.NETLIFY_LOCAL !== "true") {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
18
24
|
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
(env.AWS_LAMBDA_FUNCTION_NAME ||
|
|
26
|
+
env.LAMBDA_TASK_ROOT ||
|
|
27
|
+
cwd === "/var/task" ||
|
|
28
|
+
cwd.startsWith("/var/task/")) &&
|
|
29
|
+
env.NETLIFY_LOCAL !== "true"
|
|
24
30
|
) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return Boolean(
|
|
34
|
+
env.VERCEL ||
|
|
35
|
+
env.VERCEL_ENV ||
|
|
36
|
+
env.CF_PAGES ||
|
|
37
|
+
env.RENDER ||
|
|
38
|
+
env.FLY_APP_NAME ||
|
|
39
|
+
env.K_SERVICE,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function exportRootDir(
|
|
44
|
+
cwd = process.cwd(),
|
|
45
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
46
|
+
): string {
|
|
47
|
+
if (isHostedSlidesRuntime(cwd, env)) {
|
|
25
48
|
return path.join(os.tmpdir(), "agent-native-slides", "exports");
|
|
26
49
|
}
|
|
27
50
|
|
|
28
|
-
return path.join(
|
|
51
|
+
return path.join(cwd, "data", "exports");
|
|
29
52
|
}
|
|
30
53
|
|
|
31
|
-
export function tenantExportDir(
|
|
32
|
-
|
|
54
|
+
export function tenantExportDir(
|
|
55
|
+
email: string,
|
|
56
|
+
cwd = process.cwd(),
|
|
57
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
58
|
+
): string {
|
|
59
|
+
return path.join(exportRootDir(cwd, env), tenantFileKey(email));
|
|
33
60
|
}
|
|
34
61
|
|
|
35
62
|
export function safeGeneratedFilename(title: string, ext: ".html" | ".pptx") {
|