@agent-native/core 0.128.0 → 0.128.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/core/CHANGELOG.md +21 -0
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/server/builder-design-systems.ts +299 -82
- package/corpus/core/src/server/index.ts +8 -0
- package/corpus/templates/analytics/_gitignore +2 -0
- package/corpus/templates/assets/_gitignore +2 -0
- package/corpus/templates/calendar/_gitignore +2 -0
- package/corpus/templates/clips/_gitignore +2 -0
- package/corpus/templates/content/_gitignore +2 -0
- package/corpus/templates/design/_gitignore +2 -0
- package/corpus/templates/design/app/i18n-data.ts +10 -0
- package/corpus/templates/design/app/lib/builder-design-system-upload.ts +280 -0
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +90 -54
- package/corpus/templates/design/changelog/2026-07-28-figma-fig-uploads-now-show-indexing-progress-and-an-open-in-.md +6 -0
- package/corpus/templates/design/server/handlers/design-system-decode-job-status.ts +49 -0
- package/corpus/templates/design/server/handlers/design-system-upload-start.ts +87 -0
- package/corpus/templates/design/server/handlers/index-design-system-sources.ts +82 -0
- package/corpus/templates/design/server/routes/api/design-system-decode-job-status.get.ts +1 -0
- package/corpus/templates/design/server/routes/api/design-system-upload-start.post.ts +1 -0
- package/corpus/templates/design/server/routes/api/index-design-system-sources.post.ts +1 -0
- package/corpus/templates/dispatch/_gitignore +2 -0
- package/corpus/templates/forms/_gitignore +2 -0
- package/corpus/templates/macros/_gitignore +2 -0
- package/corpus/templates/mail/_gitignore +2 -0
- package/corpus/templates/plan/_gitignore +2 -0
- package/corpus/templates/slides/_gitignore +2 -0
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +126 -53
- package/corpus/templates/slides/app/components/design-system/builder-design-system-upload.ts +271 -0
- package/corpus/templates/slides/app/components/design-system/builder-index-response.ts +1 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
- package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +49 -0
- package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +87 -0
- package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +79 -0
- package/corpus/templates/slides/server/routes/api/design-system-decode-job-status.get.ts +1 -0
- package/corpus/templates/slides/server/routes/api/design-system-upload-start.post.ts +1 -0
- package/corpus/templates/slides/server/routes/api/index-design-system-sources.post.ts +1 -0
- package/corpus/templates/tasks/_gitignore +2 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/builder-design-systems.d.ts +49 -0
- package/dist/server/builder-design-systems.d.ts.map +1 -1
- package/dist/server/builder-design-systems.js +221 -69
- package/dist/server/builder-design-systems.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/package.json +2 -1
- package/src/server/builder-design-systems.ts +299 -82
- package/src/server/index.ts +8 -0
- package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +0 -151
- package/corpus/templates/design/server/routes/api/index-design-system-with-builder.post.ts +0 -1
- package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +0 -119
- package/corpus/templates/slides/server/routes/api/index-design-system-with-builder.post.ts +0 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { appApiPath } from "@agent-native/core/client/api-path";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type BuilderIndexResult,
|
|
5
|
+
readBuilderIndexResponse,
|
|
6
|
+
} from "./builder-index-response";
|
|
7
|
+
|
|
8
|
+
// GCS resumable uploads require every chunk except the last to be a multiple
|
|
9
|
+
// of 256 KiB. 16 MiB is the recommended default and keeps very large `.fig`
|
|
10
|
+
// files off a single request body (the serverless host caps bodies well below
|
|
11
|
+
// Figma export sizes).
|
|
12
|
+
const GCS_CHUNK_SIZE = 16 * 1024 * 1024;
|
|
13
|
+
const MAX_CHUNK_RETRIES = 5;
|
|
14
|
+
|
|
15
|
+
interface UploadSlot {
|
|
16
|
+
idx: number;
|
|
17
|
+
uploadUrl: string;
|
|
18
|
+
uploadToken: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function readJson(res: Response): Promise<any> {
|
|
22
|
+
try {
|
|
23
|
+
return await res.json();
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function requestUploadSlots(files: File[]): Promise<UploadSlot[]> {
|
|
30
|
+
const res = await fetch(appApiPath("/api/design-system-upload-start"), {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: { "Content-Type": "application/json" },
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
attachments: files.map((file) => ({
|
|
35
|
+
name: file.name,
|
|
36
|
+
mimetype: file.type || "application/octet-stream",
|
|
37
|
+
declaredSize: file.size,
|
|
38
|
+
})),
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
const json = await readJson(res);
|
|
42
|
+
if (!res.ok || json?.error) {
|
|
43
|
+
throw new Error(json?.error || `Failed to start upload (${res.status})`);
|
|
44
|
+
}
|
|
45
|
+
const slots = Array.isArray(json?.uploads)
|
|
46
|
+
? ([...json.uploads] as UploadSlot[]).sort((a, b) => a.idx - b.idx)
|
|
47
|
+
: [];
|
|
48
|
+
if (slots.length !== files.length) {
|
|
49
|
+
throw new Error("Upload could not be started for all files.");
|
|
50
|
+
}
|
|
51
|
+
return slots;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function initiateResumableSession(
|
|
55
|
+
uploadUrl: string,
|
|
56
|
+
mimetype: string,
|
|
57
|
+
fileSize: number,
|
|
58
|
+
): Promise<string> {
|
|
59
|
+
const response = await fetch(uploadUrl, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
"x-goog-resumable": "start",
|
|
63
|
+
// The signed URL commits to the exact declared size; echo it back
|
|
64
|
+
// byte-for-byte or GCS rejects the session.
|
|
65
|
+
"x-goog-content-length-range": `0,${fileSize}`,
|
|
66
|
+
"Content-Type": mimetype,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new Error(`Failed to initiate upload session (${response.status})`);
|
|
71
|
+
}
|
|
72
|
+
const sessionUri = response.headers.get("Location");
|
|
73
|
+
if (!sessionUri) {
|
|
74
|
+
throw new Error("Upload session URI missing from storage response");
|
|
75
|
+
}
|
|
76
|
+
return sessionUri;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// GCS reports the highest committed byte in a `Range: bytes=0-<end>` header.
|
|
80
|
+
function committedOffsetFromRange(response: Response): number | null {
|
|
81
|
+
const match = response.headers.get("Range")?.match(/bytes=0-(\d+)/);
|
|
82
|
+
return match ? parseInt(match[1], 10) + 1 : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function queryCommittedOffset(
|
|
86
|
+
sessionUri: string,
|
|
87
|
+
total: number,
|
|
88
|
+
): Promise<number> {
|
|
89
|
+
const response = await fetch(sessionUri, {
|
|
90
|
+
method: "PUT",
|
|
91
|
+
headers: { "Content-Range": `bytes */${total}` },
|
|
92
|
+
});
|
|
93
|
+
if (response.status === 200 || response.status === 201) return total;
|
|
94
|
+
if (response.status === 308) return committedOffsetFromRange(response) ?? 0;
|
|
95
|
+
throw new Error(`Failed to query upload status (${response.status})`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
if (signal?.aborted) {
|
|
101
|
+
return reject(new DOMException("Aborted", "AbortError"));
|
|
102
|
+
}
|
|
103
|
+
const timer = setTimeout(resolve, ms);
|
|
104
|
+
signal?.addEventListener(
|
|
105
|
+
"abort",
|
|
106
|
+
() => {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
109
|
+
},
|
|
110
|
+
{ once: true },
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function streamFileToStorage(
|
|
116
|
+
slot: UploadSlot,
|
|
117
|
+
file: File,
|
|
118
|
+
onProgress: (uploadedBytes: number) => void,
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
const mimetype = file.type || "application/octet-stream";
|
|
121
|
+
const sessionUri = await initiateResumableSession(
|
|
122
|
+
slot.uploadUrl,
|
|
123
|
+
mimetype,
|
|
124
|
+
file.size,
|
|
125
|
+
);
|
|
126
|
+
const total = file.size;
|
|
127
|
+
|
|
128
|
+
if (total === 0) {
|
|
129
|
+
const response = await fetch(sessionUri, {
|
|
130
|
+
method: "PUT",
|
|
131
|
+
headers: { "Content-Range": "bytes */0" },
|
|
132
|
+
body: new Uint8Array(0),
|
|
133
|
+
});
|
|
134
|
+
if (response.status !== 200 && response.status !== 201) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Failed to finalize zero-byte upload (${response.status})`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
onProgress(0);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let offset = 0;
|
|
144
|
+
let retries = 0;
|
|
145
|
+
while (offset < total) {
|
|
146
|
+
const end = Math.min(offset + GCS_CHUNK_SIZE, total);
|
|
147
|
+
const isLast = end === total;
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(sessionUri, {
|
|
150
|
+
method: "PUT",
|
|
151
|
+
headers: { "Content-Range": `bytes ${offset}-${end - 1}/${total}` },
|
|
152
|
+
body: file.slice(offset, end),
|
|
153
|
+
});
|
|
154
|
+
if (response.status === 200 || response.status === 201) {
|
|
155
|
+
offset = total;
|
|
156
|
+
} else if (!isLast && response.status === 308) {
|
|
157
|
+
const nextOffset = committedOffsetFromRange(response) ?? offset;
|
|
158
|
+
if (nextOffset <= offset) {
|
|
159
|
+
throw new Error(`Upload stalled at byte ${offset}`);
|
|
160
|
+
}
|
|
161
|
+
offset = nextOffset;
|
|
162
|
+
} else {
|
|
163
|
+
throw new Error(`Unexpected upload status ${response.status}`);
|
|
164
|
+
}
|
|
165
|
+
retries = 0;
|
|
166
|
+
onProgress(offset);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (++retries > MAX_CHUNK_RETRIES) throw err;
|
|
169
|
+
await delay(500 * retries);
|
|
170
|
+
try {
|
|
171
|
+
offset = await queryCommittedOffset(sessionUri, total);
|
|
172
|
+
} catch {
|
|
173
|
+
// If the offset query also fails, retry from the last local offset —
|
|
174
|
+
// GCS's resumable PUT safely re-acknowledges bytes it already has.
|
|
175
|
+
}
|
|
176
|
+
onProgress(offset);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface UploadAndIndexOptions {
|
|
182
|
+
projectName?: string;
|
|
183
|
+
onProgress?: (fraction: number) => void;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Streams `.fig`/design files straight to storage in resumable chunks, then
|
|
188
|
+
* finalizes Builder DSI indexing with the resulting upload tokens. No file
|
|
189
|
+
* bytes pass through the app server, so arbitrarily large Figma files work.
|
|
190
|
+
*/
|
|
191
|
+
export async function uploadAndIndexFigmaFiles(
|
|
192
|
+
files: File[],
|
|
193
|
+
options: UploadAndIndexOptions = {},
|
|
194
|
+
): Promise<BuilderIndexResult> {
|
|
195
|
+
if (files.length === 0) throw new Error("No files to upload.");
|
|
196
|
+
|
|
197
|
+
const slots = await requestUploadSlots(files);
|
|
198
|
+
|
|
199
|
+
const totalBytes = files.reduce((sum, file) => sum + file.size, 0) || 1;
|
|
200
|
+
const uploadedPerFile = new Array<number>(files.length).fill(0);
|
|
201
|
+
for (let i = 0; i < files.length; i++) {
|
|
202
|
+
await streamFileToStorage(slots[i], files[i], (uploaded) => {
|
|
203
|
+
uploadedPerFile[i] = uploaded;
|
|
204
|
+
const done = uploadedPerFile.reduce((sum, n) => sum + n, 0);
|
|
205
|
+
options.onProgress?.(Math.min(done / totalBytes, 1));
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const res = await fetch(appApiPath("/api/index-design-system-sources"), {
|
|
210
|
+
method: "POST",
|
|
211
|
+
headers: { "Content-Type": "application/json" },
|
|
212
|
+
body: JSON.stringify({
|
|
213
|
+
projectName: options.projectName,
|
|
214
|
+
uploadTokens: slots.map((slot) => slot.uploadToken),
|
|
215
|
+
}),
|
|
216
|
+
});
|
|
217
|
+
return readBuilderIndexResponse(res);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface DecodeJobStatus {
|
|
221
|
+
status: "pending" | "processing" | "complete" | "error";
|
|
222
|
+
branchUrl: string | null;
|
|
223
|
+
error: string | null;
|
|
224
|
+
framesProcessed: number;
|
|
225
|
+
totalFrames: number;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const DECODE_JOB_POLL_INTERVAL_MS = 5_000;
|
|
229
|
+
const DECODE_JOB_MAX_POLLS = 120; // ~10 min at 5s, so a stuck job can't loop forever
|
|
230
|
+
|
|
231
|
+
export interface PollDecodeJobOptions {
|
|
232
|
+
signal?: AbortSignal;
|
|
233
|
+
onUpdate?: (status: DecodeJobStatus) => void;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* After indexing returns a jobId, the `.fig` decode job is still `pending` with
|
|
238
|
+
* no branchUrl. Poll until the branch appears or the job reaches a terminal
|
|
239
|
+
* state. A job that reports `status: "error"` resolves so the caller can read
|
|
240
|
+
* `status.error`; network failures, timeouts, and aborts reject.
|
|
241
|
+
*/
|
|
242
|
+
export async function pollDecodeJobStatus(
|
|
243
|
+
jobId: string,
|
|
244
|
+
options: PollDecodeJobOptions = {},
|
|
245
|
+
): Promise<DecodeJobStatus> {
|
|
246
|
+
const { signal, onUpdate } = options;
|
|
247
|
+
const path = appApiPath(
|
|
248
|
+
`/api/design-system-decode-job-status?jobId=${encodeURIComponent(jobId)}`,
|
|
249
|
+
);
|
|
250
|
+
for (let i = 0; i < DECODE_JOB_MAX_POLLS; i++) {
|
|
251
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
252
|
+
const res = await fetch(path, { signal });
|
|
253
|
+
const json = await readJson(res);
|
|
254
|
+
if (!res.ok || json?.error) {
|
|
255
|
+
throw new Error(json?.error || `Status check failed (${res.status})`);
|
|
256
|
+
}
|
|
257
|
+
const status = json as DecodeJobStatus;
|
|
258
|
+
onUpdate?.(status);
|
|
259
|
+
if (
|
|
260
|
+
status.branchUrl ||
|
|
261
|
+
status.status === "complete" ||
|
|
262
|
+
status.status === "error"
|
|
263
|
+
) {
|
|
264
|
+
return status;
|
|
265
|
+
}
|
|
266
|
+
await delay(DECODE_JOB_POLL_INTERVAL_MS, signal);
|
|
267
|
+
}
|
|
268
|
+
throw new Error(
|
|
269
|
+
"Timed out waiting for the design system to finish decoding.",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
@@ -13,7 +13,7 @@ export interface BuilderIndexResult {
|
|
|
13
13
|
builderConnectUrl?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export const MAX_BUILDER_INDEX_UPLOAD_BYTES =
|
|
16
|
+
export const MAX_BUILDER_INDEX_UPLOAD_BYTES = 512 * 1024 * 1024;
|
|
17
17
|
|
|
18
18
|
export function formatFileSize(bytes: number): string {
|
|
19
19
|
return `${Math.round(bytes / 1024 / 1024)} MB`;
|
|
@@ -537,6 +537,7 @@ const messages = {
|
|
|
537
537
|
builderDesignSystemId: "Design system",
|
|
538
538
|
builderJobId: "Job",
|
|
539
539
|
openInBuilder: "Open in Builder",
|
|
540
|
+
decodeFailed: "Decoding failed: {{error}}",
|
|
540
541
|
websiteUrl: "Website URL",
|
|
541
542
|
websitePlaceholder: "example.com or Nike",
|
|
542
543
|
add: "Add",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FeatureNotConfiguredError,
|
|
3
|
+
fetchBuilderDesignSystemDecodeJobStatus,
|
|
4
|
+
getSession,
|
|
5
|
+
} from "@agent-native/core/server";
|
|
6
|
+
import { defineEventHandler, getQuery, setResponseStatus } from "h3";
|
|
7
|
+
|
|
8
|
+
import { withSlidesRequestContext } from "./request-auth-context.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Reads a Builder design-system decode job's status. The UI polls this after
|
|
12
|
+
* `/api/index-design-system-sources` returns a jobId, until the `.fig` decode
|
|
13
|
+
* job leaves `pending` and exposes a `branchUrl` (or reports an `error`).
|
|
14
|
+
*/
|
|
15
|
+
export const designSystemDecodeJobStatus = defineEventHandler(async (event) => {
|
|
16
|
+
const session = await getSession(event).catch(() => null);
|
|
17
|
+
if (!session?.email) {
|
|
18
|
+
setResponseStatus(event, 401);
|
|
19
|
+
return { error: "Unauthorized" };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const jobId = getQuery(event).jobId;
|
|
23
|
+
if (typeof jobId !== "string" || !jobId.trim()) {
|
|
24
|
+
setResponseStatus(event, 400);
|
|
25
|
+
return { error: "jobId is required." };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
return await withSlidesRequestContext(event, () =>
|
|
30
|
+
fetchBuilderDesignSystemDecodeJobStatus(jobId.trim()),
|
|
31
|
+
);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (err instanceof FeatureNotConfiguredError) {
|
|
34
|
+
setResponseStatus(event, 412);
|
|
35
|
+
return {
|
|
36
|
+
error: err.message,
|
|
37
|
+
builderConnectUrl:
|
|
38
|
+
err.builderConnectUrl ?? "/_agent-native/builder/connect",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
setResponseStatus(event, 502);
|
|
42
|
+
return {
|
|
43
|
+
error:
|
|
44
|
+
err instanceof Error
|
|
45
|
+
? err.message
|
|
46
|
+
: "Failed to read decode job status.",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FeatureNotConfiguredError,
|
|
3
|
+
getSession,
|
|
4
|
+
startBuilderDesignSystemUpload,
|
|
5
|
+
} from "@agent-native/core/server";
|
|
6
|
+
import { defineEventHandler, readBody, setResponseStatus } from "h3";
|
|
7
|
+
|
|
8
|
+
import { withSlidesRequestContext } from "./request-auth-context.js";
|
|
9
|
+
|
|
10
|
+
const MAX_FIG_BYTES = 512 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
interface AttachmentInput {
|
|
13
|
+
name?: unknown;
|
|
14
|
+
mimetype?: unknown;
|
|
15
|
+
declaredSize?: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Opens signed resumable-upload slots so the browser can stream large `.fig`
|
|
20
|
+
* bytes straight to storage. Only small JSON metadata rides through the app
|
|
21
|
+
* server; the file bytes never do.
|
|
22
|
+
*/
|
|
23
|
+
export const designSystemUploadStart = defineEventHandler(async (event) => {
|
|
24
|
+
const session = await getSession(event).catch(() => null);
|
|
25
|
+
if (!session?.email) {
|
|
26
|
+
setResponseStatus(event, 401);
|
|
27
|
+
return { error: "Unauthorized" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const body = (await readBody(event).catch(() => null)) as {
|
|
31
|
+
attachments?: AttachmentInput[];
|
|
32
|
+
} | null;
|
|
33
|
+
const rawList = Array.isArray(body?.attachments) ? body.attachments : [];
|
|
34
|
+
if (rawList.length === 0) {
|
|
35
|
+
setResponseStatus(event, 400);
|
|
36
|
+
return { error: "No attachments provided." };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const attachments: {
|
|
40
|
+
name: string;
|
|
41
|
+
mimetype: string;
|
|
42
|
+
declaredSize: number;
|
|
43
|
+
}[] = [];
|
|
44
|
+
for (const raw of rawList) {
|
|
45
|
+
const name = typeof raw?.name === "string" ? raw.name.trim() : "";
|
|
46
|
+
const mimetype =
|
|
47
|
+
typeof raw?.mimetype === "string" && raw.mimetype.trim()
|
|
48
|
+
? raw.mimetype.trim()
|
|
49
|
+
: "application/octet-stream";
|
|
50
|
+
const declaredSize = Number(raw?.declaredSize);
|
|
51
|
+
if (!name) {
|
|
52
|
+
setResponseStatus(event, 400);
|
|
53
|
+
return { error: "Attachment name is required." };
|
|
54
|
+
}
|
|
55
|
+
if (!Number.isSafeInteger(declaredSize) || declaredSize <= 0) {
|
|
56
|
+
setResponseStatus(event, 400);
|
|
57
|
+
return { error: "Attachment declaredSize must be a positive integer." };
|
|
58
|
+
}
|
|
59
|
+
if (declaredSize > MAX_FIG_BYTES) {
|
|
60
|
+
setResponseStatus(event, 413);
|
|
61
|
+
return {
|
|
62
|
+
error: `File too large (max ${Math.round(MAX_FIG_BYTES / 1024 / 1024)} MB).`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
attachments.push({ name, mimetype, declaredSize });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const uploads = await withSlidesRequestContext(event, () =>
|
|
70
|
+
startBuilderDesignSystemUpload(attachments),
|
|
71
|
+
);
|
|
72
|
+
return { uploads };
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err instanceof FeatureNotConfiguredError) {
|
|
75
|
+
setResponseStatus(event, 412);
|
|
76
|
+
return {
|
|
77
|
+
error: err.message,
|
|
78
|
+
builderConnectUrl:
|
|
79
|
+
err.builderConnectUrl ?? "/_agent-native/builder/connect",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
setResponseStatus(event, 502);
|
|
83
|
+
return {
|
|
84
|
+
error: err instanceof Error ? err.message : "Failed to start upload.",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FeatureNotConfiguredError,
|
|
3
|
+
getSession,
|
|
4
|
+
indexBuilderDesignSystem,
|
|
5
|
+
} from "@agent-native/core/server";
|
|
6
|
+
import { defineEventHandler, readBody, setResponseStatus } from "h3";
|
|
7
|
+
|
|
8
|
+
import { upsertBuilderProxyDesignSystem } from "../lib/builder-design-system-proxy.js";
|
|
9
|
+
import { withSlidesRequestContext } from "./request-auth-context.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Finalizes Builder DSI indexing from upload tokens produced by the
|
|
13
|
+
* browser-streamed resumable upload. The file bytes were streamed straight to
|
|
14
|
+
* storage; this endpoint only forwards the opaque tokens.
|
|
15
|
+
*/
|
|
16
|
+
export const indexDesignSystemSources = defineEventHandler(async (event) => {
|
|
17
|
+
const session = await getSession(event).catch(() => null);
|
|
18
|
+
if (!session?.email) {
|
|
19
|
+
setResponseStatus(event, 401);
|
|
20
|
+
return { error: "Unauthorized" };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const body = (await readBody(event).catch(() => null)) as {
|
|
24
|
+
projectName?: unknown;
|
|
25
|
+
uploadTokens?: unknown;
|
|
26
|
+
} | null;
|
|
27
|
+
const uploadTokens = Array.isArray(body?.uploadTokens)
|
|
28
|
+
? body.uploadTokens.filter(
|
|
29
|
+
(token): token is string =>
|
|
30
|
+
typeof token === "string" && token.length > 0,
|
|
31
|
+
)
|
|
32
|
+
: [];
|
|
33
|
+
if (uploadTokens.length === 0) {
|
|
34
|
+
setResponseStatus(event, 400);
|
|
35
|
+
return { error: "No uploaded files to index." };
|
|
36
|
+
}
|
|
37
|
+
const projectName =
|
|
38
|
+
typeof body?.projectName === "string"
|
|
39
|
+
? body.projectName.trim() || undefined
|
|
40
|
+
: undefined;
|
|
41
|
+
|
|
42
|
+
const sources = uploadTokens.map((uploadToken) => ({
|
|
43
|
+
kind: "file" as const,
|
|
44
|
+
uploadToken,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
return await withSlidesRequestContext(event, async ({ email, orgId }) => {
|
|
49
|
+
const result = await indexBuilderDesignSystem({ sources, projectName });
|
|
50
|
+
const proxy = await upsertBuilderProxyDesignSystem({
|
|
51
|
+
result,
|
|
52
|
+
ownerEmail: email ?? session.email,
|
|
53
|
+
orgId: orgId ?? null,
|
|
54
|
+
projectName,
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
...result,
|
|
58
|
+
...proxy,
|
|
59
|
+
uploadedFileCount: uploadTokens.length,
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (err instanceof FeatureNotConfiguredError) {
|
|
64
|
+
setResponseStatus(event, 412);
|
|
65
|
+
return {
|
|
66
|
+
error: err.message,
|
|
67
|
+
builderConnectUrl:
|
|
68
|
+
err.builderConnectUrl ?? "/_agent-native/builder/connect",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
setResponseStatus(event, 502);
|
|
72
|
+
return {
|
|
73
|
+
error:
|
|
74
|
+
err instanceof Error
|
|
75
|
+
? err.message
|
|
76
|
+
: "Builder design-system indexing failed.",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { designSystemDecodeJobStatus as default } from "../../handlers/design-system-decode-job-status";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { designSystemUploadStart as default } from "../../handlers/design-system-upload-start";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { indexDesignSystemSources as default } from "../../handlers/index-design-system-sources";
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
|
|
|
26
26
|
* Body: { update: string (base64), requestSource?: string }
|
|
27
27
|
*/
|
|
28
28
|
export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
29
|
-
ok?: undefined;
|
|
30
29
|
error: string;
|
|
30
|
+
ok?: undefined;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
33
33
|
ok: boolean;
|
|
@@ -75,8 +75,9 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
75
75
|
user: "user";
|
|
76
76
|
}>>;
|
|
77
77
|
}, z.core.$strip>>, {
|
|
78
|
-
id?: undefined;
|
|
79
78
|
deleted?: undefined;
|
|
79
|
+
id?: undefined;
|
|
80
|
+
provider?: undefined;
|
|
80
81
|
found?: undefined;
|
|
81
82
|
message?: undefined;
|
|
82
83
|
providers: {
|
|
@@ -89,45 +90,44 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
89
90
|
updatedAt: number;
|
|
90
91
|
}[];
|
|
91
92
|
count: number;
|
|
92
|
-
provider?: undefined;
|
|
93
93
|
registered?: undefined;
|
|
94
94
|
label?: undefined;
|
|
95
95
|
} | {
|
|
96
|
-
id?: undefined;
|
|
97
96
|
deleted?: undefined;
|
|
97
|
+
count?: undefined;
|
|
98
|
+
id?: undefined;
|
|
98
99
|
message?: undefined;
|
|
99
100
|
providers?: undefined;
|
|
100
|
-
count?: undefined;
|
|
101
101
|
found: boolean;
|
|
102
102
|
provider: import("../custom-registry.js").CustomProviderConfig;
|
|
103
103
|
registered?: undefined;
|
|
104
104
|
label?: undefined;
|
|
105
105
|
} | {
|
|
106
106
|
deleted?: undefined;
|
|
107
|
-
message?: undefined;
|
|
108
|
-
providers?: undefined;
|
|
109
107
|
count?: undefined;
|
|
110
108
|
provider?: undefined;
|
|
109
|
+
message?: undefined;
|
|
110
|
+
providers?: undefined;
|
|
111
111
|
found: boolean;
|
|
112
112
|
id: string;
|
|
113
113
|
registered?: undefined;
|
|
114
114
|
label?: undefined;
|
|
115
115
|
} | {
|
|
116
|
+
count?: undefined;
|
|
117
|
+
provider?: undefined;
|
|
116
118
|
found?: undefined;
|
|
117
119
|
message?: undefined;
|
|
118
120
|
providers?: undefined;
|
|
119
|
-
count?: undefined;
|
|
120
|
-
provider?: undefined;
|
|
121
121
|
deleted: boolean;
|
|
122
122
|
id: string;
|
|
123
123
|
registered?: undefined;
|
|
124
124
|
label?: undefined;
|
|
125
125
|
} | {
|
|
126
126
|
deleted?: undefined;
|
|
127
|
-
found?: undefined;
|
|
128
|
-
providers?: undefined;
|
|
129
127
|
count?: undefined;
|
|
130
128
|
provider?: undefined;
|
|
129
|
+
found?: undefined;
|
|
130
|
+
providers?: undefined;
|
|
131
131
|
registered: boolean;
|
|
132
132
|
id: string;
|
|
133
133
|
label: string;
|
|
@@ -311,9 +311,8 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
311
311
|
notes?: string | undefined;
|
|
312
312
|
scope?: "org" | "user" | undefined;
|
|
313
313
|
}, {
|
|
314
|
-
deleted?: undefined;
|
|
315
314
|
id?: undefined;
|
|
316
|
-
|
|
315
|
+
deleted?: undefined;
|
|
317
316
|
found?: undefined;
|
|
318
317
|
providers: {
|
|
319
318
|
id: string;
|
|
@@ -325,13 +324,14 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
325
324
|
updatedAt: number;
|
|
326
325
|
}[];
|
|
327
326
|
count: number;
|
|
327
|
+
provider?: undefined;
|
|
328
328
|
registered?: undefined;
|
|
329
329
|
label?: undefined;
|
|
330
330
|
message?: undefined;
|
|
331
331
|
} | {
|
|
332
|
+
id?: undefined;
|
|
332
333
|
deleted?: undefined;
|
|
333
334
|
count?: undefined;
|
|
334
|
-
id?: undefined;
|
|
335
335
|
providers?: undefined;
|
|
336
336
|
found: boolean;
|
|
337
337
|
provider: import("../custom-registry.js").CustomProviderConfig;
|
|
@@ -341,18 +341,18 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
341
341
|
} | {
|
|
342
342
|
deleted?: undefined;
|
|
343
343
|
count?: undefined;
|
|
344
|
-
provider?: undefined;
|
|
345
344
|
providers?: undefined;
|
|
345
|
+
provider?: undefined;
|
|
346
346
|
found: boolean;
|
|
347
347
|
id: string;
|
|
348
348
|
registered?: undefined;
|
|
349
349
|
label?: undefined;
|
|
350
350
|
message?: undefined;
|
|
351
351
|
} | {
|
|
352
|
-
count?: undefined;
|
|
353
|
-
provider?: undefined;
|
|
354
352
|
found?: undefined;
|
|
353
|
+
count?: undefined;
|
|
355
354
|
providers?: undefined;
|
|
355
|
+
provider?: undefined;
|
|
356
356
|
deleted: boolean;
|
|
357
357
|
id: string;
|
|
358
358
|
registered?: undefined;
|
|
@@ -360,10 +360,10 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
360
360
|
message?: undefined;
|
|
361
361
|
} | {
|
|
362
362
|
deleted?: undefined;
|
|
363
|
-
count?: undefined;
|
|
364
|
-
provider?: undefined;
|
|
365
363
|
found?: undefined;
|
|
364
|
+
count?: undefined;
|
|
366
365
|
providers?: undefined;
|
|
366
|
+
provider?: undefined;
|
|
367
367
|
registered: boolean;
|
|
368
368
|
id: string;
|
|
369
369
|
label: string;
|
|
@@ -27,11 +27,11 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
27
27
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
28
28
|
error: any;
|
|
29
29
|
} | {
|
|
30
|
+
error?: undefined;
|
|
30
31
|
ok: boolean;
|
|
31
32
|
key: string;
|
|
32
33
|
baseUrlKey?: string;
|
|
33
34
|
scope: AgentEngineApiKeyScope;
|
|
34
|
-
error?: undefined;
|
|
35
35
|
}>>;
|
|
36
36
|
export {};
|
|
37
37
|
//# sourceMappingURL=agent-engine-api-key-route.d.ts.map
|