@standardagents/builder 0.10.1-dev.8f03957 → 0.10.1-dev.cea2b66
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/dist/built-in-routes.js +7 -7
- package/dist/built-in-routes.js.map +1 -1
- package/dist/image-processing.d.ts +10 -6
- package/dist/image-processing.js +11 -138
- package/dist/image-processing.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +48 -161
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +6 -8
- package/dist/plugin.js.map +1 -1
- package/package.json +2 -5
|
@@ -2,26 +2,30 @@
|
|
|
2
2
|
* Server-side image processing for Cloudflare Workers
|
|
3
3
|
*
|
|
4
4
|
* Uses @standardagents/sip for memory-efficient processing with scanline-based
|
|
5
|
-
* resize and streaming output.
|
|
5
|
+
* resize and streaming output. All images are converted to JPEG.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
8
8
|
* - Compress/resize images to fit under 1.5MB target
|
|
9
|
-
* - Convert AVIF/WebP/PNG/JPEG to JPEG
|
|
9
|
+
* - Convert AVIF/WebP/PNG/JPEG to JPEG
|
|
10
10
|
* - Memory-efficient scanline processing
|
|
11
11
|
* - Smart quality optimization: quality first, dimensions last
|
|
12
12
|
*/
|
|
13
13
|
interface ProcessedImage {
|
|
14
14
|
data: ArrayBuffer;
|
|
15
|
-
mimeType: "image/jpeg"
|
|
15
|
+
mimeType: "image/jpeg";
|
|
16
16
|
width: number;
|
|
17
17
|
height: number;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* Process an image to ensure it's under 1.5MB and in
|
|
20
|
+
* Process an image to ensure it's under 1.5MB and in JPEG format.
|
|
21
|
+
*
|
|
22
|
+
* All images (including transparent PNGs) are converted to JPEG.
|
|
23
|
+
* This trades transparency for reliability - SIP's WASM works correctly
|
|
24
|
+
* in Cloudflare Workers while @jsquash's WASM loading fails.
|
|
21
25
|
*
|
|
22
26
|
* @param input - Raw image data as ArrayBuffer
|
|
23
|
-
* @param inputMimeType - MIME type hint (
|
|
24
|
-
* @returns Processed image data
|
|
27
|
+
* @param inputMimeType - MIME type hint (unused, kept for API compatibility)
|
|
28
|
+
* @returns Processed image data as JPEG with dimensions
|
|
25
29
|
*/
|
|
26
30
|
declare function processImage(input: ArrayBuffer, inputMimeType: string): Promise<ProcessedImage>;
|
|
27
31
|
/**
|
package/dist/image-processing.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { decode as decode$1, encode } from '@jsquash/png';
|
|
3
|
-
import { decode } from '@jsquash/avif';
|
|
1
|
+
import { sip } from '@standardagents/sip';
|
|
4
2
|
|
|
5
3
|
// src/image-processing/index.ts
|
|
6
4
|
var MAX_SIZE = 1.5 * 1024 * 1024;
|
|
@@ -10,144 +8,19 @@ async function processImage(input, inputMimeType) {
|
|
|
10
8
|
if (input.byteLength > MAX_INPUT_SIZE) {
|
|
11
9
|
throw new Error(`Image too large: ${input.byteLength} bytes exceeds ${MAX_INPUT_SIZE} byte limit`);
|
|
12
10
|
}
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
try {
|
|
20
|
-
const result = await sip.process(input, {
|
|
21
|
-
maxWidth: MAX_DIMENSION,
|
|
22
|
-
maxHeight: MAX_DIMENSION,
|
|
23
|
-
maxBytes: MAX_SIZE,
|
|
24
|
-
quality: 85
|
|
25
|
-
});
|
|
26
|
-
return {
|
|
27
|
-
data: result.data,
|
|
28
|
-
mimeType: "image/jpeg",
|
|
29
|
-
width: result.width,
|
|
30
|
-
height: result.height
|
|
31
|
-
};
|
|
32
|
-
} catch (err) {
|
|
33
|
-
console.error("[sip] Processing failed, falling back:", err);
|
|
34
|
-
throw err;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
async function processPngWithAlpha(input, format) {
|
|
38
|
-
let imageData;
|
|
39
|
-
if (format === "avif") {
|
|
40
|
-
imageData = await decode(input);
|
|
41
|
-
} else {
|
|
42
|
-
imageData = await decode$1(input);
|
|
43
|
-
}
|
|
44
|
-
const originalWidth = imageData.width;
|
|
45
|
-
const originalHeight = imageData.height;
|
|
46
|
-
let encoded = await encode(imageData);
|
|
47
|
-
if (encoded.byteLength <= MAX_SIZE) {
|
|
48
|
-
return {
|
|
49
|
-
data: encoded,
|
|
50
|
-
mimeType: "image/png",
|
|
51
|
-
width: originalWidth,
|
|
52
|
-
height: originalHeight
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
let scale = 0.9;
|
|
56
|
-
while (encoded.byteLength > MAX_SIZE && scale > 0.15) {
|
|
57
|
-
const newWidth = Math.floor(originalWidth * scale);
|
|
58
|
-
const newHeight = Math.floor(originalHeight * scale);
|
|
59
|
-
if (newWidth < 100 || newHeight < 100) {
|
|
60
|
-
scale *= 0.9;
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
const resized = resizeRgba(
|
|
64
|
-
new Uint8ClampedArray(imageData.data),
|
|
65
|
-
originalWidth,
|
|
66
|
-
originalHeight,
|
|
67
|
-
newWidth,
|
|
68
|
-
newHeight
|
|
69
|
-
);
|
|
70
|
-
const resizedImageData = { data: resized, width: newWidth, height: newHeight };
|
|
71
|
-
encoded = await encode(resizedImageData);
|
|
72
|
-
if (encoded.byteLength <= MAX_SIZE) {
|
|
73
|
-
return {
|
|
74
|
-
data: encoded,
|
|
75
|
-
mimeType: "image/png",
|
|
76
|
-
width: newWidth,
|
|
77
|
-
height: newHeight
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
scale *= 0.9;
|
|
81
|
-
}
|
|
82
|
-
const finalWidth = Math.floor(originalWidth * 0.15);
|
|
83
|
-
const finalHeight = Math.floor(originalHeight * 0.15);
|
|
84
|
-
const finalResized = resizeRgba(
|
|
85
|
-
new Uint8ClampedArray(imageData.data),
|
|
86
|
-
originalWidth,
|
|
87
|
-
originalHeight,
|
|
88
|
-
finalWidth,
|
|
89
|
-
finalHeight
|
|
90
|
-
);
|
|
91
|
-
encoded = await encode({ data: finalResized, width: finalWidth, height: finalHeight });
|
|
11
|
+
const result = await sip.process(input, {
|
|
12
|
+
maxWidth: MAX_DIMENSION,
|
|
13
|
+
maxHeight: MAX_DIMENSION,
|
|
14
|
+
maxBytes: MAX_SIZE,
|
|
15
|
+
quality: 85
|
|
16
|
+
});
|
|
92
17
|
return {
|
|
93
|
-
data:
|
|
94
|
-
mimeType: "image/
|
|
95
|
-
width:
|
|
96
|
-
height:
|
|
18
|
+
data: result.data,
|
|
19
|
+
mimeType: "image/jpeg",
|
|
20
|
+
width: result.width,
|
|
21
|
+
height: result.height
|
|
97
22
|
};
|
|
98
23
|
}
|
|
99
|
-
function resizeRgba(src, srcWidth, srcHeight, dstWidth, dstHeight) {
|
|
100
|
-
const dst = new Uint8ClampedArray(dstWidth * dstHeight * 4);
|
|
101
|
-
const xScale = srcWidth / dstWidth;
|
|
102
|
-
const yScale = srcHeight / dstHeight;
|
|
103
|
-
for (let dstY = 0; dstY < dstHeight; dstY++) {
|
|
104
|
-
for (let dstX = 0; dstX < dstWidth; dstX++) {
|
|
105
|
-
const srcXFloat = dstX * xScale;
|
|
106
|
-
const srcYFloat = dstY * yScale;
|
|
107
|
-
const srcX0 = Math.floor(srcXFloat);
|
|
108
|
-
const srcY0 = Math.floor(srcYFloat);
|
|
109
|
-
const srcX1 = Math.min(srcX0 + 1, srcWidth - 1);
|
|
110
|
-
const srcY1 = Math.min(srcY0 + 1, srcHeight - 1);
|
|
111
|
-
const tx = srcXFloat - srcX0;
|
|
112
|
-
const ty = srcYFloat - srcY0;
|
|
113
|
-
const idx00 = (srcY0 * srcWidth + srcX0) * 4;
|
|
114
|
-
const idx10 = (srcY0 * srcWidth + srcX1) * 4;
|
|
115
|
-
const idx01 = (srcY1 * srcWidth + srcX0) * 4;
|
|
116
|
-
const idx11 = (srcY1 * srcWidth + srcX1) * 4;
|
|
117
|
-
const dstIdx = (dstY * dstWidth + dstX) * 4;
|
|
118
|
-
for (let c = 0; c < 4; c++) {
|
|
119
|
-
const v00 = src[idx00 + c];
|
|
120
|
-
const v10 = src[idx10 + c];
|
|
121
|
-
const v01 = src[idx01 + c];
|
|
122
|
-
const v11 = src[idx11 + c];
|
|
123
|
-
const top = v00 * (1 - tx) + v10 * tx;
|
|
124
|
-
const bottom = v01 * (1 - tx) + v11 * tx;
|
|
125
|
-
dst[dstIdx + c] = Math.round(top * (1 - ty) + bottom * ty);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return dst;
|
|
130
|
-
}
|
|
131
|
-
function detectFormat(data, mimeType) {
|
|
132
|
-
const bytes = new Uint8Array(data.slice(0, 12));
|
|
133
|
-
if (bytes[4] === 102 && bytes[5] === 116 && bytes[6] === 121 && bytes[7] === 112) {
|
|
134
|
-
const brand = String.fromCharCode(...bytes.slice(8, 12));
|
|
135
|
-
if (brand === "avif" || brand === "avis") return "avif";
|
|
136
|
-
}
|
|
137
|
-
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
|
|
138
|
-
return "png";
|
|
139
|
-
}
|
|
140
|
-
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
|
|
141
|
-
return "jpeg";
|
|
142
|
-
}
|
|
143
|
-
if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
|
|
144
|
-
return "webp";
|
|
145
|
-
}
|
|
146
|
-
if (mimeType.includes("png")) return "png";
|
|
147
|
-
if (mimeType.includes("webp")) return "webp";
|
|
148
|
-
if (mimeType.includes("avif")) return "avif";
|
|
149
|
-
return "jpeg";
|
|
150
|
-
}
|
|
151
24
|
function needsProcessing(data, mimeType) {
|
|
152
25
|
const binaryLength = Math.ceil(data.length * 3 / 4);
|
|
153
26
|
return binaryLength > MAX_SIZE || mimeType.includes("avif") || mimeType.includes("webp");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/image-processing/index.ts"],"names":["decodeAvif","decodePng","encodePng"],"mappings":"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","file":"image-processing.js","sourcesContent":["/**\n * Server-side image processing for Cloudflare Workers\n *\n * Uses @standardagents/sip for memory-efficient processing with scanline-based\n * resize and streaming output. Falls back to PNG encoding for transparent images.\n *\n * Features:\n * - Compress/resize images to fit under 1.5MB target\n * - Convert AVIF/WebP/PNG/JPEG to JPEG (default) or PNG (for transparency)\n * - Memory-efficient scanline processing\n * - Smart quality optimization: quality first, dimensions last\n */\n\nimport { sip, probe } from \"@standardagents/sip\";\nimport { encode as encodePng, decode as decodePng } from \"@jsquash/png\";\nimport { decode as decodeAvif } from \"@jsquash/avif\";\n\n// ImageData type for Workers (matches the structure from @jsquash)\ninterface ImageDataLike {\n data: Uint8ClampedArray;\n width: number;\n height: number;\n}\n\n// Target 1.5MB binary → ~2MB base64 for RPC transfer\n// SQLite limit is 2MB per row, but base64 overhead means we need headroom\nconst MAX_SIZE = 1.5 * 1024 * 1024;\n\n// Memory safety limit\nconst MAX_DIMENSION = 4096;\n\n// Reject images that would consume too much memory before decoding\nconst MAX_INPUT_SIZE = 20 * 1024 * 1024; // 20MB\n\nexport interface ProcessedImage {\n data: ArrayBuffer;\n mimeType: \"image/jpeg\" | \"image/png\";\n width: number;\n height: number;\n}\n\n/**\n * Process an image to ensure it's under 1.5MB and in a supported format.\n *\n * @param input - Raw image data as ArrayBuffer\n * @param inputMimeType - MIME type hint (used as fallback for format detection)\n * @returns Processed image data with updated mimeType and dimensions\n */\nexport async function processImage(\n input: ArrayBuffer,\n inputMimeType: string\n): Promise<ProcessedImage> {\n // Memory safety: reject very large images\n if (input.byteLength > MAX_INPUT_SIZE) {\n throw new Error(`Image too large: ${input.byteLength} bytes exceeds ${MAX_INPUT_SIZE} byte limit`);\n }\n\n // Probe for format and transparency\n const probeResult = probe(input);\n const format = probeResult.format !== 'unknown' ? probeResult.format : detectFormat(input, inputMimeType);\n const hasAlpha = probeResult.hasAlpha;\n\n // For transparent images, we need to preserve alpha with PNG\n if (hasAlpha && (format === 'png' || format === 'avif' || format === 'webp')) {\n return await processPngWithAlpha(input, format);\n }\n\n // Use sip for efficient JPEG processing\n try {\n const result = await sip.process(input, {\n maxWidth: MAX_DIMENSION,\n maxHeight: MAX_DIMENSION,\n maxBytes: MAX_SIZE,\n quality: 85,\n });\n\n return {\n data: result.data,\n mimeType: \"image/jpeg\",\n width: result.width,\n height: result.height,\n };\n } catch (err) {\n // Fallback: if sip fails, try original processing\n console.error('[sip] Processing failed, falling back:', err);\n throw err;\n }\n}\n\n/**\n * Process PNG/AVIF images with transparency preservation\n */\nasync function processPngWithAlpha(\n input: ArrayBuffer,\n format: string\n): Promise<ProcessedImage> {\n // Decode based on format\n let imageData: ImageDataLike;\n\n if (format === 'avif') {\n imageData = await decodeAvif(input);\n } else {\n imageData = await decodePng(input);\n }\n\n const originalWidth = imageData.width;\n const originalHeight = imageData.height;\n\n // First try encoding at original size\n let encoded = await encodePng(imageData);\n\n if (encoded.byteLength <= MAX_SIZE) {\n return {\n data: encoded,\n mimeType: \"image/png\",\n width: originalWidth,\n height: originalHeight,\n };\n }\n\n // PNG too large - resize using sip's scanline method and re-encode\n // We need to resize while preserving alpha, so we'll do a simple RGBA resize\n let scale = 0.9;\n while (encoded.byteLength > MAX_SIZE && scale > 0.15) {\n const newWidth = Math.floor(originalWidth * scale);\n const newHeight = Math.floor(originalHeight * scale);\n\n if (newWidth < 100 || newHeight < 100) {\n scale *= 0.9;\n continue;\n }\n\n // Simple bilinear resize for RGBA\n const resized = resizeRgba(\n new Uint8ClampedArray(imageData.data),\n originalWidth,\n originalHeight,\n newWidth,\n newHeight\n );\n\n const resizedImageData: ImageDataLike = { data: resized, width: newWidth, height: newHeight };\n encoded = await encodePng(resizedImageData);\n\n if (encoded.byteLength <= MAX_SIZE) {\n return {\n data: encoded,\n mimeType: \"image/png\",\n width: newWidth,\n height: newHeight,\n };\n }\n\n scale *= 0.9;\n }\n\n // Fallback: smallest PNG\n const finalWidth = Math.floor(originalWidth * 0.15);\n const finalHeight = Math.floor(originalHeight * 0.15);\n const finalResized = resizeRgba(\n new Uint8ClampedArray(imageData.data),\n originalWidth,\n originalHeight,\n finalWidth,\n finalHeight\n );\n encoded = await encodePng({ data: finalResized, width: finalWidth, height: finalHeight });\n\n return {\n data: encoded,\n mimeType: \"image/png\",\n width: finalWidth,\n height: finalHeight,\n };\n}\n\n/**\n * Simple bilinear RGBA resize\n */\nfunction resizeRgba(\n src: Uint8ClampedArray,\n srcWidth: number,\n srcHeight: number,\n dstWidth: number,\n dstHeight: number\n): Uint8ClampedArray {\n const dst = new Uint8ClampedArray(dstWidth * dstHeight * 4);\n const xScale = srcWidth / dstWidth;\n const yScale = srcHeight / dstHeight;\n\n for (let dstY = 0; dstY < dstHeight; dstY++) {\n for (let dstX = 0; dstX < dstWidth; dstX++) {\n const srcXFloat = dstX * xScale;\n const srcYFloat = dstY * yScale;\n const srcX0 = Math.floor(srcXFloat);\n const srcY0 = Math.floor(srcYFloat);\n const srcX1 = Math.min(srcX0 + 1, srcWidth - 1);\n const srcY1 = Math.min(srcY0 + 1, srcHeight - 1);\n const tx = srcXFloat - srcX0;\n const ty = srcYFloat - srcY0;\n\n const idx00 = (srcY0 * srcWidth + srcX0) * 4;\n const idx10 = (srcY0 * srcWidth + srcX1) * 4;\n const idx01 = (srcY1 * srcWidth + srcX0) * 4;\n const idx11 = (srcY1 * srcWidth + srcX1) * 4;\n const dstIdx = (dstY * dstWidth + dstX) * 4;\n\n for (let c = 0; c < 4; c++) {\n const v00 = src[idx00 + c];\n const v10 = src[idx10 + c];\n const v01 = src[idx01 + c];\n const v11 = src[idx11 + c];\n const top = v00 * (1 - tx) + v10 * tx;\n const bottom = v01 * (1 - tx) + v11 * tx;\n dst[dstIdx + c] = Math.round(top * (1 - ty) + bottom * ty);\n }\n }\n }\n\n return dst;\n}\n\n/**\n * Detect image format from magic bytes (fallback)\n */\nfunction detectFormat(data: ArrayBuffer, mimeType: string): string {\n const bytes = new Uint8Array(data.slice(0, 12));\n\n if (bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70) {\n const brand = String.fromCharCode(...bytes.slice(8, 12));\n if (brand === \"avif\" || brand === \"avis\") return \"avif\";\n }\n\n if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) {\n return \"png\";\n }\n\n if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) {\n return \"jpeg\";\n }\n\n if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&\n bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) {\n return \"webp\";\n }\n\n if (mimeType.includes(\"png\")) return \"png\";\n if (mimeType.includes(\"webp\")) return \"webp\";\n if (mimeType.includes(\"avif\")) return \"avif\";\n return \"jpeg\";\n}\n\n/**\n * Check if an image needs processing based on size and format.\n *\n * @param data - Base64-encoded image data\n * @param mimeType - MIME type of the image\n * @returns true if processing is needed\n */\nexport function needsProcessing(data: string, mimeType: string): boolean {\n // Decode base64 to get actual size\n const binaryLength = Math.ceil(data.length * 3 / 4);\n\n // Process if >2MB or unsupported format\n return (\n binaryLength > MAX_SIZE ||\n mimeType.includes(\"avif\") ||\n mimeType.includes(\"webp\")\n );\n}\n\n/**\n * Convert base64 string to ArrayBuffer\n */\nexport function base64ToArrayBuffer(base64: string): ArrayBuffer {\n const binaryString = atob(base64);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes.buffer;\n}\n\n/**\n * Convert ArrayBuffer to base64 string\n */\nexport function arrayBufferToBase64(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/image-processing/index.ts"],"names":[],"mappings":";;;AAiBA,IAAM,QAAA,GAAW,MAAM,IAAA,GAAO,IAAA;AAG9B,IAAM,aAAA,GAAgB,IAAA;AAGtB,IAAM,cAAA,GAAiB,KAAK,IAAA,GAAO,IAAA;AAoBnC,eAAsB,YAAA,CACpB,OACA,aAAA,EACyB;AAEzB,EAAA,IAAI,KAAA,CAAM,aAAa,cAAA,EAAgB;AACrC,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,iBAAA,EAAoB,MAAM,UAAU,CAAA,eAAA,EAAkB,cAAc,CAAA,WAAA,CAAa,CAAA;AAAA,EACnG;AAGA,EAAA,MAAM,MAAA,GAAS,MAAM,GAAA,CAAI,OAAA,CAAQ,KAAA,EAAO;AAAA,IACtC,QAAA,EAAU,aAAA;AAAA,IACV,SAAA,EAAW,aAAA;AAAA,IACX,QAAA,EAAU,QAAA;AAAA,IACV,OAAA,EAAS;AAAA,GACV,CAAA;AAED,EAAA,OAAO;AAAA,IACL,MAAM,MAAA,CAAO,IAAA;AAAA,IACb,QAAA,EAAU,YAAA;AAAA,IACV,OAAO,MAAA,CAAO,KAAA;AAAA,IACd,QAAQ,MAAA,CAAO;AAAA,GACjB;AACF;AAuCO,SAAS,eAAA,CAAgB,MAAc,QAAA,EAA2B;AAEvE,EAAA,MAAM,eAAe,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,MAAA,GAAS,IAAI,CAAC,CAAA;AAGlD,EAAA,OACE,YAAA,GAAe,YACf,QAAA,CAAS,QAAA,CAAS,MAAM,CAAA,IACxB,QAAA,CAAS,SAAS,MAAM,CAAA;AAE5B;AAKO,SAAS,oBAAoB,MAAA,EAA6B;AAC/D,EAAA,MAAM,YAAA,GAAe,KAAK,MAAM,CAAA;AAChC,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,YAAA,CAAa,MAAM,CAAA;AAChD,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,YAAA,CAAa,QAAQ,CAAA,EAAA,EAAK;AAC5C,IAAA,KAAA,CAAM,CAAC,CAAA,GAAI,YAAA,CAAa,UAAA,CAAW,CAAC,CAAA;AAAA,EACtC;AACA,EAAA,OAAO,KAAA,CAAM,MAAA;AACf;AAKO,SAAS,oBAAoB,MAAA,EAA6B;AAC/D,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,MAAM,CAAA;AACnC,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,MAAA,IAAU,MAAA,CAAO,YAAA,CAAa,KAAA,CAAM,CAAC,CAAC,CAAA;AAAA,EACxC;AACA,EAAA,OAAO,KAAK,MAAM,CAAA;AACpB","file":"image-processing.js","sourcesContent":["/**\n * Server-side image processing for Cloudflare Workers\n *\n * Uses @standardagents/sip for memory-efficient processing with scanline-based\n * resize and streaming output. All images are converted to JPEG.\n *\n * Features:\n * - Compress/resize images to fit under 1.5MB target\n * - Convert AVIF/WebP/PNG/JPEG to JPEG\n * - Memory-efficient scanline processing\n * - Smart quality optimization: quality first, dimensions last\n */\n\nimport { sip } from \"@standardagents/sip\";\n\n// Target 1.5MB binary → ~2MB base64 for RPC transfer\n// SQLite limit is 2MB per row, but base64 overhead means we need headroom\nconst MAX_SIZE = 1.5 * 1024 * 1024;\n\n// Memory safety limit\nconst MAX_DIMENSION = 4096;\n\n// Reject images that would consume too much memory before decoding\nconst MAX_INPUT_SIZE = 20 * 1024 * 1024; // 20MB\n\nexport interface ProcessedImage {\n data: ArrayBuffer;\n mimeType: \"image/jpeg\";\n width: number;\n height: number;\n}\n\n/**\n * Process an image to ensure it's under 1.5MB and in JPEG format.\n *\n * All images (including transparent PNGs) are converted to JPEG.\n * This trades transparency for reliability - SIP's WASM works correctly\n * in Cloudflare Workers while @jsquash's WASM loading fails.\n *\n * @param input - Raw image data as ArrayBuffer\n * @param inputMimeType - MIME type hint (unused, kept for API compatibility)\n * @returns Processed image data as JPEG with dimensions\n */\nexport async function processImage(\n input: ArrayBuffer,\n inputMimeType: string\n): Promise<ProcessedImage> {\n // Memory safety: reject very large images\n if (input.byteLength > MAX_INPUT_SIZE) {\n throw new Error(`Image too large: ${input.byteLength} bytes exceeds ${MAX_INPUT_SIZE} byte limit`);\n }\n\n // Use sip for ALL images - outputs JPEG\n const result = await sip.process(input, {\n maxWidth: MAX_DIMENSION,\n maxHeight: MAX_DIMENSION,\n maxBytes: MAX_SIZE,\n quality: 85,\n });\n\n return {\n data: result.data,\n mimeType: \"image/jpeg\",\n width: result.width,\n height: result.height,\n };\n}\n\n/**\n * Detect image format from magic bytes (fallback)\n */\nfunction detectFormat(data: ArrayBuffer, mimeType: string): string {\n const bytes = new Uint8Array(data.slice(0, 12));\n\n if (bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70) {\n const brand = String.fromCharCode(...bytes.slice(8, 12));\n if (brand === \"avif\" || brand === \"avis\") return \"avif\";\n }\n\n if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) {\n return \"png\";\n }\n\n if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) {\n return \"jpeg\";\n }\n\n if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&\n bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) {\n return \"webp\";\n }\n\n if (mimeType.includes(\"png\")) return \"png\";\n if (mimeType.includes(\"webp\")) return \"webp\";\n if (mimeType.includes(\"avif\")) return \"avif\";\n return \"jpeg\";\n}\n\n/**\n * Check if an image needs processing based on size and format.\n *\n * @param data - Base64-encoded image data\n * @param mimeType - MIME type of the image\n * @returns true if processing is needed\n */\nexport function needsProcessing(data: string, mimeType: string): boolean {\n // Decode base64 to get actual size\n const binaryLength = Math.ceil(data.length * 3 / 4);\n\n // Process if >2MB or unsupported format\n return (\n binaryLength > MAX_SIZE ||\n mimeType.includes(\"avif\") ||\n mimeType.includes(\"webp\")\n );\n}\n\n/**\n * Convert base64 string to ArrayBuffer\n */\nexport function base64ToArrayBuffer(base64: string): ArrayBuffer {\n const binaryString = atob(base64);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes.buffer;\n}\n\n/**\n * Convert ArrayBuffer to base64 string\n */\nexport function arrayBufferToBase64(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n}\n"]}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { decode as decode$1, encode } from '@jsquash/png';
|
|
3
|
-
import { decode } from '@jsquash/avif';
|
|
1
|
+
import { sip } from '@standardagents/sip';
|
|
4
2
|
import fs2 from 'fs';
|
|
5
3
|
import path3 from 'path';
|
|
6
4
|
import { fileURLToPath } from 'url';
|
|
@@ -4603,7 +4601,9 @@ function rowToFileRecord(row) {
|
|
|
4603
4601
|
size: row.size,
|
|
4604
4602
|
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
4605
4603
|
isDirectory: row.is_directory === 1,
|
|
4606
|
-
createdAt: row.created_at
|
|
4604
|
+
createdAt: row.created_at,
|
|
4605
|
+
width: row.width ?? null,
|
|
4606
|
+
height: row.height ?? null
|
|
4607
4607
|
};
|
|
4608
4608
|
}
|
|
4609
4609
|
function inferMimeType(filename) {
|
|
@@ -4674,8 +4674,8 @@ var init_files = __esm({
|
|
|
4674
4674
|
}
|
|
4675
4675
|
const metadataJson = options?.metadata ? JSON.stringify(options.metadata) : null;
|
|
4676
4676
|
await this.sql.exec(
|
|
4677
|
-
`INSERT OR REPLACE INTO files (path, name, mime_type, storage, location, data, content, size, metadata, thumbnail, is_directory, created_at)
|
|
4678
|
-
VALUES (?, ?, ?, 'local', NULL, ?, ?, ?, ?, ?, 0, ?)`,
|
|
4677
|
+
`INSERT OR REPLACE INTO files (path, name, mime_type, storage, location, data, content, size, metadata, thumbnail, is_directory, created_at, width, height)
|
|
4678
|
+
VALUES (?, ?, ?, 'local', NULL, ?, ?, ?, ?, ?, 0, ?, ?, ?)`,
|
|
4679
4679
|
normalizedPath,
|
|
4680
4680
|
name,
|
|
4681
4681
|
mimeType,
|
|
@@ -4684,7 +4684,9 @@ var init_files = __esm({
|
|
|
4684
4684
|
size,
|
|
4685
4685
|
metadataJson,
|
|
4686
4686
|
options?.thumbnail || null,
|
|
4687
|
-
now
|
|
4687
|
+
now,
|
|
4688
|
+
options?.width ?? null,
|
|
4689
|
+
options?.height ?? null
|
|
4688
4690
|
);
|
|
4689
4691
|
await this.updateStats();
|
|
4690
4692
|
return {
|
|
@@ -4696,7 +4698,9 @@ var init_files = __esm({
|
|
|
4696
4698
|
size,
|
|
4697
4699
|
metadata: options?.metadata || null,
|
|
4698
4700
|
isDirectory: false,
|
|
4699
|
-
createdAt: now
|
|
4701
|
+
createdAt: now,
|
|
4702
|
+
width: options?.width ?? null,
|
|
4703
|
+
height: options?.height ?? null
|
|
4700
4704
|
};
|
|
4701
4705
|
}
|
|
4702
4706
|
/**
|
|
@@ -4970,144 +4974,19 @@ async function processImage(input, inputMimeType) {
|
|
|
4970
4974
|
if (input.byteLength > MAX_INPUT_SIZE) {
|
|
4971
4975
|
throw new Error(`Image too large: ${input.byteLength} bytes exceeds ${MAX_INPUT_SIZE} byte limit`);
|
|
4972
4976
|
}
|
|
4973
|
-
const
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
}
|
|
4979
|
-
try {
|
|
4980
|
-
const result = await sip.process(input, {
|
|
4981
|
-
maxWidth: MAX_DIMENSION,
|
|
4982
|
-
maxHeight: MAX_DIMENSION,
|
|
4983
|
-
maxBytes: MAX_SIZE,
|
|
4984
|
-
quality: 85
|
|
4985
|
-
});
|
|
4986
|
-
return {
|
|
4987
|
-
data: result.data,
|
|
4988
|
-
mimeType: "image/jpeg",
|
|
4989
|
-
width: result.width,
|
|
4990
|
-
height: result.height
|
|
4991
|
-
};
|
|
4992
|
-
} catch (err) {
|
|
4993
|
-
console.error("[sip] Processing failed, falling back:", err);
|
|
4994
|
-
throw err;
|
|
4995
|
-
}
|
|
4996
|
-
}
|
|
4997
|
-
async function processPngWithAlpha(input, format) {
|
|
4998
|
-
let imageData;
|
|
4999
|
-
if (format === "avif") {
|
|
5000
|
-
imageData = await decode(input);
|
|
5001
|
-
} else {
|
|
5002
|
-
imageData = await decode$1(input);
|
|
5003
|
-
}
|
|
5004
|
-
const originalWidth = imageData.width;
|
|
5005
|
-
const originalHeight = imageData.height;
|
|
5006
|
-
let encoded = await encode(imageData);
|
|
5007
|
-
if (encoded.byteLength <= MAX_SIZE) {
|
|
5008
|
-
return {
|
|
5009
|
-
data: encoded,
|
|
5010
|
-
mimeType: "image/png",
|
|
5011
|
-
width: originalWidth,
|
|
5012
|
-
height: originalHeight
|
|
5013
|
-
};
|
|
5014
|
-
}
|
|
5015
|
-
let scale = 0.9;
|
|
5016
|
-
while (encoded.byteLength > MAX_SIZE && scale > 0.15) {
|
|
5017
|
-
const newWidth = Math.floor(originalWidth * scale);
|
|
5018
|
-
const newHeight = Math.floor(originalHeight * scale);
|
|
5019
|
-
if (newWidth < 100 || newHeight < 100) {
|
|
5020
|
-
scale *= 0.9;
|
|
5021
|
-
continue;
|
|
5022
|
-
}
|
|
5023
|
-
const resized = resizeRgba(
|
|
5024
|
-
new Uint8ClampedArray(imageData.data),
|
|
5025
|
-
originalWidth,
|
|
5026
|
-
originalHeight,
|
|
5027
|
-
newWidth,
|
|
5028
|
-
newHeight
|
|
5029
|
-
);
|
|
5030
|
-
const resizedImageData = { data: resized, width: newWidth, height: newHeight };
|
|
5031
|
-
encoded = await encode(resizedImageData);
|
|
5032
|
-
if (encoded.byteLength <= MAX_SIZE) {
|
|
5033
|
-
return {
|
|
5034
|
-
data: encoded,
|
|
5035
|
-
mimeType: "image/png",
|
|
5036
|
-
width: newWidth,
|
|
5037
|
-
height: newHeight
|
|
5038
|
-
};
|
|
5039
|
-
}
|
|
5040
|
-
scale *= 0.9;
|
|
5041
|
-
}
|
|
5042
|
-
const finalWidth = Math.floor(originalWidth * 0.15);
|
|
5043
|
-
const finalHeight = Math.floor(originalHeight * 0.15);
|
|
5044
|
-
const finalResized = resizeRgba(
|
|
5045
|
-
new Uint8ClampedArray(imageData.data),
|
|
5046
|
-
originalWidth,
|
|
5047
|
-
originalHeight,
|
|
5048
|
-
finalWidth,
|
|
5049
|
-
finalHeight
|
|
5050
|
-
);
|
|
5051
|
-
encoded = await encode({ data: finalResized, width: finalWidth, height: finalHeight });
|
|
4977
|
+
const result = await sip.process(input, {
|
|
4978
|
+
maxWidth: MAX_DIMENSION,
|
|
4979
|
+
maxHeight: MAX_DIMENSION,
|
|
4980
|
+
maxBytes: MAX_SIZE,
|
|
4981
|
+
quality: 85
|
|
4982
|
+
});
|
|
5052
4983
|
return {
|
|
5053
|
-
data:
|
|
5054
|
-
mimeType: "image/
|
|
5055
|
-
width:
|
|
5056
|
-
height:
|
|
4984
|
+
data: result.data,
|
|
4985
|
+
mimeType: "image/jpeg",
|
|
4986
|
+
width: result.width,
|
|
4987
|
+
height: result.height
|
|
5057
4988
|
};
|
|
5058
4989
|
}
|
|
5059
|
-
function resizeRgba(src, srcWidth, srcHeight, dstWidth, dstHeight) {
|
|
5060
|
-
const dst = new Uint8ClampedArray(dstWidth * dstHeight * 4);
|
|
5061
|
-
const xScale = srcWidth / dstWidth;
|
|
5062
|
-
const yScale = srcHeight / dstHeight;
|
|
5063
|
-
for (let dstY = 0; dstY < dstHeight; dstY++) {
|
|
5064
|
-
for (let dstX = 0; dstX < dstWidth; dstX++) {
|
|
5065
|
-
const srcXFloat = dstX * xScale;
|
|
5066
|
-
const srcYFloat = dstY * yScale;
|
|
5067
|
-
const srcX0 = Math.floor(srcXFloat);
|
|
5068
|
-
const srcY0 = Math.floor(srcYFloat);
|
|
5069
|
-
const srcX1 = Math.min(srcX0 + 1, srcWidth - 1);
|
|
5070
|
-
const srcY1 = Math.min(srcY0 + 1, srcHeight - 1);
|
|
5071
|
-
const tx = srcXFloat - srcX0;
|
|
5072
|
-
const ty = srcYFloat - srcY0;
|
|
5073
|
-
const idx00 = (srcY0 * srcWidth + srcX0) * 4;
|
|
5074
|
-
const idx10 = (srcY0 * srcWidth + srcX1) * 4;
|
|
5075
|
-
const idx01 = (srcY1 * srcWidth + srcX0) * 4;
|
|
5076
|
-
const idx11 = (srcY1 * srcWidth + srcX1) * 4;
|
|
5077
|
-
const dstIdx = (dstY * dstWidth + dstX) * 4;
|
|
5078
|
-
for (let c = 0; c < 4; c++) {
|
|
5079
|
-
const v00 = src[idx00 + c];
|
|
5080
|
-
const v10 = src[idx10 + c];
|
|
5081
|
-
const v01 = src[idx01 + c];
|
|
5082
|
-
const v11 = src[idx11 + c];
|
|
5083
|
-
const top = v00 * (1 - tx) + v10 * tx;
|
|
5084
|
-
const bottom = v01 * (1 - tx) + v11 * tx;
|
|
5085
|
-
dst[dstIdx + c] = Math.round(top * (1 - ty) + bottom * ty);
|
|
5086
|
-
}
|
|
5087
|
-
}
|
|
5088
|
-
}
|
|
5089
|
-
return dst;
|
|
5090
|
-
}
|
|
5091
|
-
function detectFormat(data, mimeType) {
|
|
5092
|
-
const bytes = new Uint8Array(data.slice(0, 12));
|
|
5093
|
-
if (bytes[4] === 102 && bytes[5] === 116 && bytes[6] === 121 && bytes[7] === 112) {
|
|
5094
|
-
const brand = String.fromCharCode(...bytes.slice(8, 12));
|
|
5095
|
-
if (brand === "avif" || brand === "avis") return "avif";
|
|
5096
|
-
}
|
|
5097
|
-
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
|
|
5098
|
-
return "png";
|
|
5099
|
-
}
|
|
5100
|
-
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
|
|
5101
|
-
return "jpeg";
|
|
5102
|
-
}
|
|
5103
|
-
if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
|
|
5104
|
-
return "webp";
|
|
5105
|
-
}
|
|
5106
|
-
if (mimeType.includes("png")) return "png";
|
|
5107
|
-
if (mimeType.includes("webp")) return "webp";
|
|
5108
|
-
if (mimeType.includes("avif")) return "avif";
|
|
5109
|
-
return "jpeg";
|
|
5110
|
-
}
|
|
5111
4990
|
function needsProcessing(data, mimeType) {
|
|
5112
4991
|
const binaryLength = Math.ceil(data.length * 3 / 4);
|
|
5113
4992
|
return binaryLength > MAX_SIZE || mimeType.includes("avif") || mimeType.includes("webp");
|
|
@@ -7195,11 +7074,10 @@ function agentbuilder(options = {}) {
|
|
|
7195
7074
|
// WASM image processing deps - must be excluded to avoid pre-bundle cache issues
|
|
7196
7075
|
"@cf-wasm/photon",
|
|
7197
7076
|
"@cf-wasm/photon/workerd",
|
|
7077
|
+
"@standardagents/sip",
|
|
7078
|
+
// sip's jsquash dependencies (WASM-based decoders)
|
|
7198
7079
|
"@jsquash/avif",
|
|
7199
|
-
"@jsquash/
|
|
7200
|
-
"@jsquash/png",
|
|
7201
|
-
"@jsquash/webp",
|
|
7202
|
-
"@standardagents/sip"
|
|
7080
|
+
"@jsquash/webp"
|
|
7203
7081
|
];
|
|
7204
7082
|
const depsToInclude = [
|
|
7205
7083
|
"zod",
|
|
@@ -7234,11 +7112,10 @@ function agentbuilder(options = {}) {
|
|
|
7234
7112
|
// WASM image processing deps
|
|
7235
7113
|
"@cf-wasm/photon",
|
|
7236
7114
|
"@cf-wasm/photon/workerd",
|
|
7115
|
+
"@standardagents/sip",
|
|
7116
|
+
// sip's jsquash dependencies (WASM-based decoders)
|
|
7237
7117
|
"@jsquash/avif",
|
|
7238
|
-
"@jsquash/
|
|
7239
|
-
"@jsquash/png",
|
|
7240
|
-
"@jsquash/webp",
|
|
7241
|
-
"@standardagents/sip"
|
|
7118
|
+
"@jsquash/webp"
|
|
7242
7119
|
];
|
|
7243
7120
|
const depsToInclude = [
|
|
7244
7121
|
"zod",
|
|
@@ -9229,8 +9106,18 @@ var migration17 = {
|
|
|
9229
9106
|
}
|
|
9230
9107
|
};
|
|
9231
9108
|
|
|
9109
|
+
// src/durable-objects/migrations/018_add_image_dimensions.ts
|
|
9110
|
+
var migration18 = {
|
|
9111
|
+
version: 18,
|
|
9112
|
+
async up(sql) {
|
|
9113
|
+
await sql.exec(`ALTER TABLE files ADD COLUMN width INTEGER`);
|
|
9114
|
+
await sql.exec(`ALTER TABLE files ADD COLUMN height INTEGER`);
|
|
9115
|
+
await sql.exec(`UPDATE _metadata SET value = '18' WHERE key = 'schema_version'`);
|
|
9116
|
+
}
|
|
9117
|
+
};
|
|
9118
|
+
|
|
9232
9119
|
// src/durable-objects/migrations/index.ts
|
|
9233
|
-
var migrations = [migration, migration2, migration3, migration4, migration5, migration6, migration7, migration8, migration9, migration10, migration11, migration12, migration13, migration14, migration15, migration16, migration17];
|
|
9120
|
+
var migrations = [migration, migration2, migration3, migration4, migration5, migration6, migration7, migration8, migration9, migration10, migration11, migration12, migration13, migration14, migration15, migration16, migration17, migration18];
|
|
9234
9121
|
var LATEST_SCHEMA_VERSION = migrations.length;
|
|
9235
9122
|
|
|
9236
9123
|
// src/durable-objects/DurableThread.ts
|
|
@@ -9631,9 +9518,9 @@ var DurableThread = class extends DurableObject {
|
|
|
9631
9518
|
* Each migration is run in order, starting from the current version + 1.
|
|
9632
9519
|
*/
|
|
9633
9520
|
async runMigrations(fromVersion) {
|
|
9634
|
-
for (const
|
|
9635
|
-
if (
|
|
9636
|
-
await
|
|
9521
|
+
for (const migration20 of migrations) {
|
|
9522
|
+
if (migration20.version > fromVersion) {
|
|
9523
|
+
await migration20.up(this.ctx.storage.sql);
|
|
9637
9524
|
}
|
|
9638
9525
|
}
|
|
9639
9526
|
}
|
|
@@ -10720,7 +10607,7 @@ var DurableThread = class extends DurableObject {
|
|
|
10720
10607
|
id: message.id,
|
|
10721
10608
|
role: message.role,
|
|
10722
10609
|
content: message.content,
|
|
10723
|
-
attachments: message.attachments,
|
|
10610
|
+
attachments: message.attachments ? JSON.parse(message.attachments) : null,
|
|
10724
10611
|
created_at: message.created_at
|
|
10725
10612
|
});
|
|
10726
10613
|
const nextSide = role === "assistant" ? "b" : "a";
|
|
@@ -11179,7 +11066,7 @@ var DurableThread = class extends DurableObject {
|
|
|
11179
11066
|
};
|
|
11180
11067
|
|
|
11181
11068
|
// src/durable-objects/agentbuilder-migrations/0001_initial.ts
|
|
11182
|
-
var
|
|
11069
|
+
var migration19 = {
|
|
11183
11070
|
version: 1,
|
|
11184
11071
|
async up(sql) {
|
|
11185
11072
|
sql.exec(`
|
|
@@ -11278,7 +11165,7 @@ var migration18 = {
|
|
|
11278
11165
|
};
|
|
11279
11166
|
|
|
11280
11167
|
// src/durable-objects/agentbuilder-migrations/index.ts
|
|
11281
|
-
var migrations2 = [
|
|
11168
|
+
var migrations2 = [migration19];
|
|
11282
11169
|
var LATEST_SCHEMA_VERSION2 = 1;
|
|
11283
11170
|
|
|
11284
11171
|
// src/durable-objects/DurableAgentBuilder.ts
|
|
@@ -11368,9 +11255,9 @@ var DurableAgentBuilder = class extends DurableObject {
|
|
|
11368
11255
|
}
|
|
11369
11256
|
}
|
|
11370
11257
|
async runMigrations(fromVersion) {
|
|
11371
|
-
for (const
|
|
11372
|
-
if (
|
|
11373
|
-
await
|
|
11258
|
+
for (const migration20 of migrations2) {
|
|
11259
|
+
if (migration20.version > fromVersion) {
|
|
11260
|
+
await migration20.up(this.ctx.storage.sql);
|
|
11374
11261
|
}
|
|
11375
11262
|
}
|
|
11376
11263
|
}
|