@agent-native/core 0.118.1 → 0.119.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 +2 -2
- package/corpus/core/CHANGELOG.md +19 -0
- package/corpus/core/docs/content/a2a-protocol.mdx +15 -0
- package/corpus/core/docs/content/template-chat.mdx +1 -1
- package/corpus/core/docs/content/template-plan.mdx +3 -3
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/activity.ts +371 -0
- package/corpus/core/src/a2a/client.ts +3 -1
- package/corpus/core/src/a2a/index.ts +21 -0
- package/corpus/core/src/a2a/types.ts +8 -0
- package/corpus/core/src/agent/model-config.ts +19 -23
- package/corpus/core/src/agent/production-agent.ts +167 -100
- package/corpus/core/src/agent/types.ts +16 -1
- package/corpus/core/src/client/chat/tool-call-display.tsx +190 -35
- package/corpus/core/src/client/chat-model-groups.ts +67 -27
- package/corpus/core/src/client/sharing/ShareButton.tsx +1 -27
- package/corpus/core/src/client/sharing/ShareDialog.tsx +0 -16
- package/corpus/core/src/client/sharing/useShareButtonController.ts +0 -7
- package/corpus/core/src/client/sharing/useShareDialogController.ts +0 -2
- package/corpus/core/src/client/sse-event-processor.ts +85 -1
- package/corpus/core/src/ingestion/figma-node-to-html.ts +20 -99
- package/corpus/core/src/ingestion/figma-paint-math.ts +371 -0
- package/corpus/core/src/ingestion/index.ts +20 -0
- package/corpus/core/src/localization/default-messages.ts +6 -0
- package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +1 -18
- package/corpus/core/src/scripts/call-agent.ts +49 -6
- package/corpus/core/src/server/agent-chat-plugin.ts +50 -10
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +16 -0
- package/corpus/templates/assets/app/routes/library.tsx +17 -9
- package/corpus/templates/clips/changelog/2026-07-23-fixed-recordings-failing-to-save-with-a-cancelled-or-cleanup.md +6 -0
- package/corpus/templates/clips/server/plugins/db.ts +9 -1
- package/corpus/templates/design/AGENTS.md +18 -5
- package/corpus/templates/design/FIGMA_INTEROPERABILITY.md +25 -12
- package/corpus/templates/design/actions/hydrate-figma-paste-images.ts +115 -0
- package/corpus/templates/design/actions/import-figma-clipboard.ts +83 -18
- package/corpus/templates/design/actions/import-figma-frame.ts +34 -21
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +28 -1
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +106 -11
- package/corpus/templates/design/app/components/design/FigmaHydrationDialog.tsx +273 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +7 -7
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +57 -9
- package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +10 -0
- package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +8 -1
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -0
- package/corpus/templates/design/app/i18n-data.ts +150 -2
- package/corpus/templates/design/app/lib/design-file-upload.ts +85 -0
- package/corpus/templates/design/app/lib/design-import.ts +19 -1
- package/corpus/templates/design/app/lib/figma-clipboard.ts +110 -10
- package/corpus/templates/design/changelog/2026-07-20-paste-figma-frames-directly-onto-the-canvas-no-token-needed.md +6 -0
- package/corpus/templates/design/changelog/2026-07-21-fig-upload-frame-url-and-rate-limit-errors.md +5 -0
- package/corpus/templates/design/changelog/2026-07-22-connecting-a-figma-token-now-actually-fills-in-a-pasted-desi.md +6 -0
- package/corpus/templates/design/changelog/2026-07-22-figma-line-arrow-vectors-and-stroked-icons-now-render-instea.md +6 -0
- package/corpus/templates/design/changelog/2026-07-22-fill-a-no-token-figma-paste-s-images-by-dropping-the-origina.md +6 -0
- package/corpus/templates/design/changelog/2026-07-22-imported-and-pasted-figma-frames-now-render-faithfully-fixed.md +6 -0
- package/corpus/templates/design/server/handlers/import-design-file.ts +50 -0
- package/corpus/templates/design/server/lib/fig-file-decoder.ts +90 -49
- package/corpus/templates/design/server/lib/fig-file-import.ts +19 -5
- package/corpus/templates/design/server/lib/fig-file-to-html.ts +583 -78
- package/corpus/templates/design/server/lib/figma-clipboard-local-decode.ts +238 -0
- package/corpus/templates/design/server/lib/figma-image-hydration.ts +389 -0
- package/corpus/templates/design/server/lib/figma-node-import.ts +136 -36
- package/corpus/templates/design/server/register-secrets.ts +14 -2
- package/dist/a2a/activity.d.ts +50 -0
- package/dist/a2a/activity.d.ts.map +1 -0
- package/dist/a2a/activity.js +236 -0
- package/dist/a2a/activity.js.map +1 -0
- package/dist/a2a/client.d.ts +2 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +1 -1
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/index.d.ts +2 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +1 -0
- package/dist/a2a/index.js.map +1 -1
- package/dist/a2a/types.d.ts +1 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/agent/engine/anthropic-engine.d.ts +1 -1
- package/dist/agent/engine/builder-engine.d.ts +2 -2
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/model-config.d.ts +16 -16
- package/dist/agent/model-config.d.ts.map +1 -1
- package/dist/agent/model-config.js +19 -23
- package/dist/agent/model-config.js.map +1 -1
- package/dist/agent/production-agent.d.ts +26 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +137 -80
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/types.d.ts +10 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +63 -14
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/chat-model-groups.d.ts.map +1 -1
- package/dist/client/chat-model-groups.js +44 -21
- package/dist/client/chat-model-groups.js.map +1 -1
- package/dist/client/sharing/ShareButton.d.ts +1 -1
- package/dist/client/sharing/ShareButton.d.ts.map +1 -1
- package/dist/client/sharing/ShareButton.js +3 -4
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/client/sharing/ShareDialog.d.ts.map +1 -1
- package/dist/client/sharing/ShareDialog.js +1 -2
- package/dist/client/sharing/ShareDialog.js.map +1 -1
- package/dist/client/sharing/useShareButtonController.d.ts +0 -1
- package/dist/client/sharing/useShareButtonController.d.ts.map +1 -1
- package/dist/client/sharing/useShareButtonController.js +0 -6
- package/dist/client/sharing/useShareButtonController.js.map +1 -1
- package/dist/client/sharing/useShareDialogController.d.ts +0 -1
- package/dist/client/sharing/useShareDialogController.d.ts.map +1 -1
- package/dist/client/sharing/useShareDialogController.js +0 -1
- package/dist/client/sharing/useShareDialogController.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +12 -0
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +63 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/ingestion/figma-node-to-html.d.ts.map +1 -1
- package/dist/ingestion/figma-node-to-html.js +14 -88
- package/dist/ingestion/figma-node-to-html.js.map +1 -1
- package/dist/ingestion/figma-paint-math.d.ts +157 -0
- package/dist/ingestion/figma-paint-math.d.ts.map +1 -0
- package/dist/ingestion/figma-paint-math.js +274 -0
- package/dist/ingestion/figma-paint-math.js.map +1 -0
- package/dist/ingestion/index.d.ts +1 -0
- package/dist/ingestion/index.d.ts.map +1 -1
- package/dist/ingestion/index.js +1 -0
- package/dist/ingestion/index.js.map +1 -1
- package/dist/localization/default-messages.d.ts +6 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +6 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
- package/dist/scripts/agent-engines/list-agent-engines.js +1 -18
- package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +49 -6
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +38 -10
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +16 -0
- package/docs/content/a2a-protocol.mdx +15 -0
- package/docs/content/template-chat.mdx +1 -1
- package/docs/content/template-plan.mdx +3 -3
- package/package.json +1 -1
- package/src/a2a/activity.ts +371 -0
- package/src/a2a/client.ts +3 -1
- package/src/a2a/index.ts +21 -0
- package/src/a2a/types.ts +8 -0
- package/src/agent/model-config.ts +19 -23
- package/src/agent/production-agent.ts +167 -100
- package/src/agent/types.ts +16 -1
- package/src/client/chat/tool-call-display.tsx +190 -35
- package/src/client/chat-model-groups.ts +67 -27
- package/src/client/sharing/ShareButton.tsx +1 -27
- package/src/client/sharing/ShareDialog.tsx +0 -16
- package/src/client/sharing/useShareButtonController.ts +0 -7
- package/src/client/sharing/useShareDialogController.ts +0 -2
- package/src/client/sse-event-processor.ts +85 -1
- package/src/ingestion/figma-node-to-html.ts +20 -99
- package/src/ingestion/figma-paint-math.ts +371 -0
- package/src/ingestion/index.ts +20 -0
- package/src/localization/default-messages.ts +6 -0
- package/src/scripts/agent-engines/list-agent-engines.ts +1 -18
- package/src/scripts/call-agent.ts +49 -6
- package/src/server/agent-chat-plugin.ts +50 -10
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +16 -0
- package/corpus/core/src/observability/hosted-model-experiment.ts +0 -118
- package/dist/observability/hosted-model-experiment.d.ts +0 -39
- package/dist/observability/hosted-model-experiment.d.ts.map +0 -1
- package/dist/observability/hosted-model-experiment.js +0 -90
- package/dist/observability/hosted-model-experiment.js.map +0 -1
- package/src/observability/hosted-model-experiment.ts +0 -118
|
@@ -20,6 +20,10 @@ const MAX_HTML_BYTES = 2 * 1024 * 1024;
|
|
|
20
20
|
const MULTIPART_OVERHEAD_BYTES = 1024 * 1024;
|
|
21
21
|
const TOTAL_BODY_LIMIT = MAX_FIG_FILE_BYTES + MULTIPART_OVERHEAD_BYTES;
|
|
22
22
|
|
|
23
|
+
// Matches the local-kiwi clipboard frame cap so a token-free .fig hydration of
|
|
24
|
+
// a multi-frame paste can fill every imported screen in one upload.
|
|
25
|
+
const MAX_HYDRATE_FILES = 50;
|
|
26
|
+
|
|
23
27
|
function fieldText(
|
|
24
28
|
parts: Awaited<ReturnType<typeof readMultipartFormData>>,
|
|
25
29
|
name: string,
|
|
@@ -129,6 +133,51 @@ export const importDesignFile = defineEventHandler(async (event) => {
|
|
|
129
133
|
if (data.length > MAX_FIG_FILE_BYTES) {
|
|
130
134
|
throw new Error(".fig file is too large (max 50 MB).");
|
|
131
135
|
}
|
|
136
|
+
|
|
137
|
+
// Token-free hydration: fill the image placeholders left by a no-token
|
|
138
|
+
// clipboard paste using the SAME .fig's embedded image bytes. No Figma
|
|
139
|
+
// token, no REST call — the .fig `images/` entries are keyed by the
|
|
140
|
+
// same SHA-1 hash the paste stamped into data-figma-image-ref.
|
|
141
|
+
const hydrateFileIdsRaw = fieldText(parts, "hydrateFileIds");
|
|
142
|
+
if (hydrateFileIdsRaw) {
|
|
143
|
+
const fileIds = hydrateFileIdsRaw
|
|
144
|
+
.split(",")
|
|
145
|
+
.map((id) => id.trim())
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
if (fileIds.length === 0) {
|
|
148
|
+
throw new Error("No screen ids provided to hydrate.");
|
|
149
|
+
}
|
|
150
|
+
if (fileIds.length > MAX_HYDRATE_FILES) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Too many screens to hydrate at once (max ${MAX_HYDRATE_FILES}).`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const { hydrateFileImagesFromFig, indexFigImages } =
|
|
156
|
+
await import("../lib/figma-image-hydration.js");
|
|
157
|
+
// Decode + index the uploaded .fig once, then reuse across screens.
|
|
158
|
+
const figImages = indexFigImages(data);
|
|
159
|
+
const results = [];
|
|
160
|
+
let totalResolved = 0;
|
|
161
|
+
let totalMissing = 0;
|
|
162
|
+
for (const fileId of fileIds) {
|
|
163
|
+
const result = await hydrateFileImagesFromFig({
|
|
164
|
+
fileId,
|
|
165
|
+
figImages,
|
|
166
|
+
ownerEmail: session.email!,
|
|
167
|
+
});
|
|
168
|
+
results.push(result);
|
|
169
|
+
totalResolved += result.resolved;
|
|
170
|
+
totalMissing += result.missing;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
importKind: "fig-hydrate" as const,
|
|
174
|
+
designId,
|
|
175
|
+
results,
|
|
176
|
+
totalResolved,
|
|
177
|
+
totalMissing,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
132
181
|
// Keep Kiwi/Zstd and the sizeable editable renderer off the normal HTML
|
|
133
182
|
// upload path. They are loaded only for an actual `.fig` request.
|
|
134
183
|
const { importFigFileToEditableHtml } =
|
|
@@ -148,6 +197,7 @@ export const importDesignFile = defineEventHandler(async (event) => {
|
|
|
148
197
|
importKind: "fig",
|
|
149
198
|
...saved,
|
|
150
199
|
stats: converted.stats,
|
|
200
|
+
unresolvedImageRefCount: converted.stats.unresolvedImageRefCount,
|
|
151
201
|
};
|
|
152
202
|
}
|
|
153
203
|
|
|
@@ -21,16 +21,20 @@ const MAX_KIWI_CHUNKS = 4_096;
|
|
|
21
21
|
const MAX_ZIP_ENTRIES = 2_048;
|
|
22
22
|
const MAX_ZIP_NAME_BYTES = 512;
|
|
23
23
|
const MAX_COMPRESSION_RATIO = 1_000;
|
|
24
|
-
const MAX_DECODED_OBJECTS = 250_000;
|
|
25
24
|
const MAX_DECODE_DEPTH = 256;
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// Sized with ~15x headroom over a real 11 MB corpus .fig (~530k objects,
|
|
26
|
+
// ~1.5M items, longest single collection ~7.6k) so genuine files decode while a
|
|
27
|
+
// crafted document still hits a finite total-work ceiling.
|
|
28
|
+
const MAX_DECODED_OBJECTS = 8_000_000;
|
|
29
|
+
const MAX_COLLECTION_LENGTH = 2_000_000;
|
|
30
|
+
const MAX_COLLECTION_ITEMS = 24_000_000;
|
|
28
31
|
const MAX_DECODE_READS = 64 * 1024 * 1024;
|
|
29
32
|
const MAX_SANITIZED_BINARY_BYTES = 32 * 1024 * 1024;
|
|
30
33
|
const MAX_DECODED_STRING_BYTES = 4 * 1024 * 1024;
|
|
31
34
|
const MAX_TOTAL_STRING_BYTES = 32 * 1024 * 1024;
|
|
32
35
|
const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
|
|
33
36
|
const FIG_KIWI_MAGIC = Buffer.from("fig-kiwi", "utf8");
|
|
37
|
+
const FIGJAM_KIWI_MAGIC = Buffer.from("fig-jam.", "utf8");
|
|
34
38
|
const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
|
|
35
39
|
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
36
40
|
const JPEG_MAGIC = Buffer.from([0xff, 0xd8, 0xff]);
|
|
@@ -53,6 +57,7 @@ export interface DecodedFig {
|
|
|
53
57
|
format: "kiwi" | "zip";
|
|
54
58
|
version?: number;
|
|
55
59
|
document: unknown;
|
|
60
|
+
decodeError?: string;
|
|
56
61
|
images: DecodedFigImage[];
|
|
57
62
|
thumbnail: Buffer | null;
|
|
58
63
|
}
|
|
@@ -193,7 +198,10 @@ export function decodeKiwiContainer(file: Buffer): DecodedFigKiwi {
|
|
|
193
198
|
if (file.length > MAX_FIG_FILE_BYTES) {
|
|
194
199
|
throw new Error(".fig file is too large (max 50 MB).");
|
|
195
200
|
}
|
|
196
|
-
if (
|
|
201
|
+
if (
|
|
202
|
+
!file.subarray(0, 8).equals(FIG_KIWI_MAGIC) &&
|
|
203
|
+
!file.subarray(0, 8).equals(FIGJAM_KIWI_MAGIC)
|
|
204
|
+
) {
|
|
197
205
|
throw new Error("Not a fig-kiwi file (missing magic header)");
|
|
198
206
|
}
|
|
199
207
|
if (file.length < 12) {
|
|
@@ -476,7 +484,7 @@ function sanitizeForJson(
|
|
|
476
484
|
const entries = Object.entries(value);
|
|
477
485
|
budget.items += entries.length;
|
|
478
486
|
if (budget.items > MAX_COLLECTION_ITEMS) {
|
|
479
|
-
throw new Error("Decoded .fig document has too many
|
|
487
|
+
throw new Error("Decoded .fig document has too many fields.");
|
|
480
488
|
}
|
|
481
489
|
try {
|
|
482
490
|
for (const [k, v] of entries) {
|
|
@@ -625,23 +633,26 @@ function compileBudgetedSchema(schema: Schema): CompiledDecoder {
|
|
|
625
633
|
}
|
|
626
634
|
|
|
627
635
|
// Returns null on any decode failure so callers can still surface the raw
|
|
628
|
-
// document buffer.
|
|
636
|
+
// document buffer. Also returns an optional decodeError string so callers can
|
|
637
|
+
// surface the reason rather than falling back to a generic message.
|
|
629
638
|
function decodeKiwiDocument(
|
|
630
639
|
schemaBuf: Buffer,
|
|
631
640
|
documentBuf: Buffer,
|
|
632
|
-
): unknown | null {
|
|
641
|
+
): { document: unknown | null; decodeError?: string } {
|
|
633
642
|
let schema: Schema;
|
|
634
643
|
try {
|
|
635
644
|
assertSafeBinarySchemaShape(schemaBuf);
|
|
636
645
|
schema = decodeBinarySchema(schemaBuf);
|
|
637
|
-
} catch {
|
|
638
|
-
return
|
|
646
|
+
} catch (e) {
|
|
647
|
+
return {
|
|
648
|
+
document: null,
|
|
649
|
+
decodeError: `Schema parsing failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
650
|
+
};
|
|
639
651
|
}
|
|
640
652
|
// kiwi-schema compiles the schema with `new Function`, so never pass names
|
|
641
653
|
// from an untrusted binary schema to it without strict identifier and size
|
|
642
654
|
// validation. Real Figma schemas use ordinary identifiers and a `Message`
|
|
643
655
|
// root; anything else is an unsupported/probably hostile variant.
|
|
644
|
-
if (schema.definitions.length > 1_024) return null;
|
|
645
656
|
const definitionNames = new Set(schema.definitions.map((d) => d.name));
|
|
646
657
|
const safeIdentifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
647
658
|
const primitiveTypes = new Set([
|
|
@@ -654,40 +665,71 @@ function decodeKiwiDocument(
|
|
|
654
665
|
"int64",
|
|
655
666
|
"uint64",
|
|
656
667
|
]);
|
|
657
|
-
let fieldCount = 0;
|
|
658
668
|
for (const definition of schema.definitions) {
|
|
659
|
-
if (!safeIdentifier.test(definition.name))
|
|
660
|
-
|
|
661
|
-
|
|
669
|
+
if (!safeIdentifier.test(definition.name)) {
|
|
670
|
+
return {
|
|
671
|
+
document: null,
|
|
672
|
+
decodeError: `Unsafe schema identifier: "${definition.name}"`,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
if (!Array.isArray(definition.fields)) {
|
|
676
|
+
return {
|
|
677
|
+
document: null,
|
|
678
|
+
decodeError: `Definition "${definition.name}" has no fields array`,
|
|
679
|
+
};
|
|
662
680
|
}
|
|
663
|
-
fieldCount += definition.fields.length;
|
|
664
|
-
if (fieldCount > 20_000) return null;
|
|
665
681
|
for (const field of definition.fields) {
|
|
666
|
-
if (!safeIdentifier.test(field.name))
|
|
682
|
+
if (!safeIdentifier.test(field.name)) {
|
|
683
|
+
return {
|
|
684
|
+
document: null,
|
|
685
|
+
decodeError: `Unsafe field identifier: "${definition.name}.${field.name}"`,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
667
688
|
if (
|
|
668
689
|
field.type !== null &&
|
|
669
690
|
!primitiveTypes.has(field.type) &&
|
|
670
691
|
!definitionNames.has(field.type)
|
|
671
692
|
) {
|
|
672
|
-
return
|
|
693
|
+
return {
|
|
694
|
+
document: null,
|
|
695
|
+
decodeError: `Unknown field type "${field.type}" in "${definition.name}.${field.name}"`,
|
|
696
|
+
};
|
|
673
697
|
}
|
|
674
698
|
}
|
|
675
699
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
700
|
+
// Current Figma .fig files use "Message" as root; fall back to the first
|
|
701
|
+
// MESSAGE definition when the name differs (schema evolution resilience).
|
|
702
|
+
const rootMessage =
|
|
703
|
+
schema.definitions.find(
|
|
704
|
+
(d) => d.name === "Message" && d.kind === "MESSAGE",
|
|
705
|
+
) ?? schema.definitions.find((d) => d.kind === "MESSAGE");
|
|
706
|
+
if (!rootMessage) {
|
|
707
|
+
return {
|
|
708
|
+
document: null,
|
|
709
|
+
decodeError: `No MESSAGE definition found in schema (${schema.definitions.length} definitions: ${schema.definitions
|
|
710
|
+
.map((d) => d.name)
|
|
711
|
+
.slice(0, 10)
|
|
712
|
+
.join(", ")})`,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
681
715
|
|
|
682
716
|
let compiled: CompiledDecoder;
|
|
683
717
|
try {
|
|
684
718
|
compiled = compileBudgetedSchema(schema);
|
|
685
|
-
} catch {
|
|
686
|
-
return
|
|
719
|
+
} catch (e) {
|
|
720
|
+
return {
|
|
721
|
+
document: null,
|
|
722
|
+
decodeError: `Schema compilation failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
723
|
+
};
|
|
687
724
|
}
|
|
688
725
|
const decodeKey = `decode${rootMessage.name}`;
|
|
689
726
|
const decoder = compiled[decodeKey];
|
|
690
|
-
if (typeof decoder !== "function")
|
|
727
|
+
if (typeof decoder !== "function") {
|
|
728
|
+
return {
|
|
729
|
+
document: null,
|
|
730
|
+
decodeError: `Compiled decoder missing function "${decodeKey}"`,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
691
733
|
|
|
692
734
|
try {
|
|
693
735
|
const view = new Uint8Array(
|
|
@@ -697,15 +739,20 @@ function decodeKiwiDocument(
|
|
|
697
739
|
);
|
|
698
740
|
const bb = new BudgetByteBuffer(view);
|
|
699
741
|
const document = decoder.call(compiled, bb);
|
|
700
|
-
return
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
742
|
+
return {
|
|
743
|
+
document: sanitizeForJson(document, {
|
|
744
|
+
objects: 0,
|
|
745
|
+
items: 0,
|
|
746
|
+
binaryBytes: 0,
|
|
747
|
+
stringBytes: 0,
|
|
748
|
+
active: new WeakSet(),
|
|
749
|
+
}),
|
|
750
|
+
};
|
|
751
|
+
} catch (e) {
|
|
752
|
+
return {
|
|
753
|
+
document: null,
|
|
754
|
+
decodeError: `Document decoding failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
755
|
+
};
|
|
709
756
|
}
|
|
710
757
|
}
|
|
711
758
|
|
|
@@ -746,23 +793,12 @@ function assertSafeBinarySchemaShape(schemaBuf: Buffer): void {
|
|
|
746
793
|
),
|
|
747
794
|
);
|
|
748
795
|
const definitionCount = bb.readVarUint();
|
|
749
|
-
if (definitionCount > 1_024) {
|
|
750
|
-
throw new Error(".fig schema has too many definitions.");
|
|
751
|
-
}
|
|
752
|
-
let totalFields = 0;
|
|
753
796
|
for (let index = 0; index < definitionCount; index += 1) {
|
|
754
797
|
bb.readString();
|
|
755
798
|
const kind = bb.readByte();
|
|
756
799
|
if (kind > 2)
|
|
757
800
|
throw new Error(".fig schema has an invalid definition kind.");
|
|
758
801
|
const fields = bb.readVarUint();
|
|
759
|
-
if (fields > 1_024) {
|
|
760
|
-
throw new Error(".fig schema definition has too many fields.");
|
|
761
|
-
}
|
|
762
|
-
totalFields += fields;
|
|
763
|
-
if (totalFields > 20_000) {
|
|
764
|
-
throw new Error(".fig schema has too many fields.");
|
|
765
|
-
}
|
|
766
802
|
for (let fieldIndex = 0; fieldIndex < fields; fieldIndex += 1) {
|
|
767
803
|
bb.readString();
|
|
768
804
|
bb.readVarInt();
|
|
@@ -790,7 +826,8 @@ export function decodeFig(file: Buffer): DecodedFig {
|
|
|
790
826
|
const inner = decodeKiwiContainer(canvasEntry.data);
|
|
791
827
|
version = inner.version;
|
|
792
828
|
extraBlobs = inner.blobs;
|
|
793
|
-
|
|
829
|
+
const kiwiResult = decodeKiwiDocument(inner.schema, inner.document);
|
|
830
|
+
document = kiwiResult.document;
|
|
794
831
|
|
|
795
832
|
const images: DecodedFigImage[] = [];
|
|
796
833
|
const seen = new Set<string>();
|
|
@@ -813,19 +850,23 @@ export function decodeFig(file: Buffer): DecodedFig {
|
|
|
813
850
|
format: "zip",
|
|
814
851
|
version,
|
|
815
852
|
document,
|
|
853
|
+
...(kiwiResult.decodeError
|
|
854
|
+
? { decodeError: kiwiResult.decodeError }
|
|
855
|
+
: {}),
|
|
816
856
|
images,
|
|
817
857
|
thumbnail: thumbnailEntry?.data ?? null,
|
|
818
858
|
};
|
|
819
859
|
}
|
|
820
860
|
|
|
821
861
|
const decoded = decodeKiwiContainer(file);
|
|
822
|
-
const
|
|
862
|
+
const kiwiResult = decodeKiwiDocument(decoded.schema, decoded.document);
|
|
823
863
|
const images = collectImagesFromBlobs(decoded.blobs);
|
|
824
864
|
const thumbnail = findThumbnail(decoded.document, decoded.blobs);
|
|
825
865
|
return {
|
|
826
866
|
format: "kiwi",
|
|
827
867
|
version: decoded.version,
|
|
828
|
-
document,
|
|
868
|
+
document: kiwiResult.document,
|
|
869
|
+
...(kiwiResult.decodeError ? { decodeError: kiwiResult.decodeError } : {}),
|
|
829
870
|
images,
|
|
830
871
|
thumbnail,
|
|
831
872
|
};
|
|
@@ -32,6 +32,8 @@ export interface FigFileImportResult {
|
|
|
32
32
|
imageCount: number;
|
|
33
33
|
uploadedImageCount: number;
|
|
34
34
|
omittedImageCount: number;
|
|
35
|
+
approximatedNodeCount: number;
|
|
36
|
+
unresolvedImageRefCount: number;
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -45,16 +47,20 @@ function mimeTypeForImage(image: DecodedFigImage): string {
|
|
|
45
47
|
return "application/octet-stream";
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
function nodeChangesFromDocument(
|
|
50
|
+
function nodeChangesFromDocument(
|
|
51
|
+
document: unknown,
|
|
52
|
+
decodeError?: string,
|
|
53
|
+
): unknown[] {
|
|
49
54
|
if (!document || typeof document !== "object") {
|
|
55
|
+
const detail = decodeError ? ` Decode detail: ${decodeError}.` : "";
|
|
50
56
|
throw new Error(
|
|
51
|
-
|
|
57
|
+
`This .fig file could not be decoded.${detail} The schema format may have changed since this file was saved, or the file may be a newer Figma version. Try: (1) copy the frame in Figma and paste directly onto the Design canvas — no API quota needed, or (2) use a Figma frame link to import via the API.`,
|
|
52
58
|
);
|
|
53
59
|
}
|
|
54
60
|
const nodeChanges = (document as { nodeChanges?: unknown }).nodeChanges;
|
|
55
61
|
if (!Array.isArray(nodeChanges)) {
|
|
56
62
|
throw new Error(
|
|
57
|
-
"This .fig
|
|
63
|
+
"This .fig file decoded but does not contain editable node data. Copy the frame in Figma and paste onto the canvas, or use a Figma frame link to import via the API.",
|
|
58
64
|
);
|
|
59
65
|
}
|
|
60
66
|
if (nodeChanges.length > MAX_FIG_NODES) {
|
|
@@ -156,8 +162,12 @@ export async function convertDecodedFigToEditableHtml(
|
|
|
156
162
|
},
|
|
157
163
|
): Promise<FigFileImportResult> {
|
|
158
164
|
assertSafeDecodedFigDocument(decoded.document);
|
|
159
|
-
const nodeChanges = nodeChangesFromDocument(
|
|
165
|
+
const nodeChanges = nodeChangesFromDocument(
|
|
166
|
+
decoded.document,
|
|
167
|
+
decoded.decodeError,
|
|
168
|
+
);
|
|
160
169
|
assertEmbeddedImageBudget(decoded.images);
|
|
170
|
+
|
|
161
171
|
// Render and validate before uploading any extracted images so an invalid or
|
|
162
172
|
// excessively complex document cannot leave orphaned storage objects behind.
|
|
163
173
|
// The upload primitive has no cross-provider delete contract, so validate
|
|
@@ -171,6 +181,7 @@ export async function convertDecodedFigToEditableHtml(
|
|
|
171
181
|
const preliminary = renderHtmlTemplates(decoded.document, {
|
|
172
182
|
imageMap: worstCaseImageMap,
|
|
173
183
|
missingImageUrl: "about:blank",
|
|
184
|
+
trackUnresolvedImageRefs: true,
|
|
174
185
|
});
|
|
175
186
|
validateRenderedFrames(preliminary);
|
|
176
187
|
const images = await uploadEmbeddedImages(
|
|
@@ -186,6 +197,7 @@ export async function convertDecodedFigToEditableHtml(
|
|
|
186
197
|
// Never persist a data URL or a broken relative link when an image
|
|
187
198
|
// blob could not be uploaded. The warning makes the omission clear.
|
|
188
199
|
missingImageUrl: "about:blank",
|
|
200
|
+
trackUnresolvedImageRefs: true,
|
|
189
201
|
});
|
|
190
202
|
validateRenderedFrames(rendered);
|
|
191
203
|
|
|
@@ -244,6 +256,8 @@ export async function convertDecodedFigToEditableHtml(
|
|
|
244
256
|
imageCount: decoded.images.length,
|
|
245
257
|
uploadedImageCount: images.uploaded,
|
|
246
258
|
omittedImageCount: images.omitted,
|
|
259
|
+
approximatedNodeCount: rendered.approximatedNodes.length,
|
|
260
|
+
unresolvedImageRefCount: rendered.unresolvedImageRefs?.size ?? 0,
|
|
247
261
|
},
|
|
248
262
|
};
|
|
249
263
|
}
|
|
@@ -253,7 +267,7 @@ function validateRenderedFrames(
|
|
|
253
267
|
): void {
|
|
254
268
|
if (rendered.frames.length === 0) {
|
|
255
269
|
throw new Error(
|
|
256
|
-
"No editable top-level frames were found in this .fig file.
|
|
270
|
+
"No editable top-level frames were found in this .fig file.",
|
|
257
271
|
);
|
|
258
272
|
}
|
|
259
273
|
if (rendered.frames.length > MAX_FIG_FRAMES) {
|