@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.
Files changed (41) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +7 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/RunStuckBanner.tsx +12 -5
  5. package/corpus/core/src/client/sse-event-processor.ts +36 -1
  6. package/corpus/core/src/client/use-run-stuck-detection.ts +162 -25
  7. package/corpus/core/src/deploy/build.ts +78 -25
  8. package/corpus/templates/chat/changelog/2026-07-14-chat-opens-reliably-on-hosted-deployments-instead-of-failing.md +6 -0
  9. package/corpus/templates/clips/app/components/player/media-duration.ts +24 -0
  10. package/corpus/templates/clips/app/components/player/video-player.tsx +9 -18
  11. package/corpus/templates/clips/changelog/2026-07-14-paused-time-no-longer-counts-toward-chrome-extension-recordi.md +6 -0
  12. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +30 -4
  13. package/corpus/templates/clips/chrome-extension/src/recording-duration.ts +32 -0
  14. package/corpus/templates/slides/actions/_uploaded-files.ts +13 -2
  15. package/corpus/templates/slides/actions/import-docx.ts +3 -9
  16. package/corpus/templates/slides/actions/import-file.ts +11 -14
  17. package/corpus/templates/slides/actions/import-pptx.ts +3 -9
  18. package/corpus/templates/slides/changelog/2026-07-14-powerpoint-template-uploads-now-work-in-hosted-slides-deploy.md +6 -0
  19. package/corpus/templates/slides/server/handlers/uploads.ts +100 -50
  20. package/corpus/templates/slides/server/lib/tenant-files.ts +38 -11
  21. package/corpus/templates/slides/server/lib/uploaded-reference-storage.ts +108 -0
  22. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  23. package/dist/client/RunStuckBanner.js +9 -5
  24. package/dist/client/RunStuckBanner.js.map +1 -1
  25. package/dist/client/sse-event-processor.d.ts.map +1 -1
  26. package/dist/client/sse-event-processor.js +33 -1
  27. package/dist/client/sse-event-processor.js.map +1 -1
  28. package/dist/client/use-run-stuck-detection.d.ts +4 -4
  29. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  30. package/dist/client/use-run-stuck-detection.js +104 -19
  31. package/dist/client/use-run-stuck-detection.js.map +1 -1
  32. package/dist/collab/awareness.d.ts +2 -2
  33. package/dist/collab/awareness.d.ts.map +1 -1
  34. package/dist/deploy/build.d.ts +15 -14
  35. package/dist/deploy/build.d.ts.map +1 -1
  36. package/dist/deploy/build.js +64 -23
  37. package/dist/deploy/build.js.map +1 -1
  38. package/dist/notifications/routes.d.ts +3 -3
  39. package/dist/progress/routes.d.ts +1 -1
  40. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  41. 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: Math.max(0, Date.now() - recording.startedAtMs),
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") recording.recorder.pause();
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") recording.recorder.resume();
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 resolveUserUploadedFile(filePath: string): string {
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 resolved;
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 { resolveUserUploadedFile } from "./_uploaded-files.js";
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 absPath = resolveUserUploadedFile(filePath);
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 { resolveUserUploadedFile } from "./_uploaded-files.js";
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 absPath = resolveUserUploadedFile(filePath);
60
+ const uploaded = await readUserUploadedFile(filePath);
64
61
  const sourceLimit = maxChars ?? DEFAULT_MAX_SOURCE_CHARS;
65
-
66
- const fileBuffer = await fs.promises.readFile(absPath);
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(absPath).toLowerCase();
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(absPath);
86
+ const title = titleFromPath(filename);
90
87
  const result = await startBuilderDesignSystemIndex({
91
88
  projectName: title,
92
89
  files: [
93
90
  {
94
- name: path.basename(absPath),
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(absPath);
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(absPath);
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(absPath);
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 { resolveUserUploadedFile } from "./_uploaded-files.js";
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 absPath = resolveUserUploadedFile(filePath);
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";
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-14
4
+ ---
5
+
6
+ PowerPoint template uploads now work in hosted Slides deployments.
@@ -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
- const uploadDir = tenantUploadDir(args.email);
131
- await fs.promises.mkdir(uploadDir, { recursive: true });
132
- const destPath = path.join(uploadDir, filename);
133
- await fs.promises.writeFile(destPath, args.data);
134
- // For images, also push to the file-upload provider so the agent can embed
135
- // a hosted URL (in slide HTML, chat replies, etc.). For non-image reference
136
- // files (PPTX, DOCX, PDF), the on-disk path is what the agent uses via
137
- // shell — no provider URL is required.
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: pathForAgent(destPath),
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 session = await getSession(event).catch(() => null);
174
- if (!session?.email) {
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
- const parts = await readMultipartFormData(event);
180
- const fileParts =
181
- parts?.filter((p) => (p.name === "files" || p.name === "file") && p.data) ??
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
- if (fileParts.length === 0) {
185
- setResponseStatus(event, 400);
186
- return { error: "No files uploaded" };
187
- }
226
+ if (fileParts.length === 0) {
227
+ setResponseStatus(event, 400);
228
+ return { error: "No files uploaded" };
229
+ }
188
230
 
189
- const MAX_FILES = 20;
231
+ const MAX_FILES = 20;
190
232
 
191
- if (fileParts.length > MAX_FILES) {
192
- setResponseStatus(event, 413);
193
- return { error: `Too many files (max ${MAX_FILES})` };
194
- }
233
+ if (fileParts.length > MAX_FILES) {
234
+ setResponseStatus(event, 413);
235
+ return { error: `Too many files (max ${MAX_FILES})` };
236
+ }
195
237
 
196
- const oversized = fileParts.find(
197
- (p) => p.data.length > maxReferenceFileBytes(p.filename),
198
- );
199
- if (oversized) {
200
- const limit = maxReferenceFileBytes(oversized.filename);
201
- setResponseStatus(event, 413);
202
- return { error: `File too large (max ${formatMaxFileSize(limit)})` };
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
- let results;
206
- try {
207
- results = await Promise.all(
208
- fileParts.map(async (part) => {
209
- return saveUploadedReferenceFile({
210
- email: session.email,
211
- originalName: part.filename || "upload",
212
- data: part.data,
213
- type: part.type,
214
- });
215
- }),
216
- );
217
- } catch (err) {
218
- setResponseStatus(event, 400);
219
- return { error: err instanceof Error ? err.message : "Invalid upload" };
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
- return results;
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(process.cwd(), "data", "uploads", tenantFileKey(email));
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 exportRootDir(): string {
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
- process.env.NETLIFY ||
20
- process.env.VERCEL ||
21
- process.env.AWS_LAMBDA_FUNCTION_NAME ||
22
- process.cwd() === "/var/task" ||
23
- process.cwd().startsWith("/var/task/")
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(process.cwd(), "data", "exports");
51
+ return path.join(cwd, "data", "exports");
29
52
  }
30
53
 
31
- export function tenantExportDir(email: string): string {
32
- return path.join(exportRootDir(), tenantFileKey(email));
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") {