ima2-gen 1.1.7 → 1.1.9
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/README.md +56 -27
- package/bin/commands/annotate.js +137 -0
- package/bin/commands/annotate.ts +118 -0
- package/bin/commands/cancel.js +37 -33
- package/bin/commands/cancel.ts +45 -0
- package/bin/commands/canvas-versions.js +91 -0
- package/bin/commands/canvas-versions.ts +80 -0
- package/bin/commands/cardnews.js +293 -0
- package/bin/commands/cardnews.ts +248 -0
- package/bin/commands/comfy.js +63 -0
- package/bin/commands/comfy.ts +54 -0
- package/bin/commands/config.js +270 -0
- package/bin/commands/config.ts +265 -0
- package/bin/commands/edit.js +97 -72
- package/bin/commands/edit.ts +116 -0
- package/bin/commands/gen.js +140 -118
- package/bin/commands/gen.ts +176 -0
- package/bin/commands/history.js +164 -0
- package/bin/commands/history.ts +145 -0
- package/bin/commands/ls.js +60 -42
- package/bin/commands/ls.ts +60 -0
- package/bin/commands/metadata.js +45 -0
- package/bin/commands/metadata.ts +36 -0
- package/bin/commands/multimode.js +159 -0
- package/bin/commands/multimode.ts +146 -0
- package/bin/commands/node.js +176 -0
- package/bin/commands/node.ts +157 -0
- package/bin/commands/observability.js +201 -0
- package/bin/commands/observability.ts +176 -0
- package/bin/commands/ping.js +26 -20
- package/bin/commands/ping.ts +29 -0
- package/bin/commands/prompt.js +506 -0
- package/bin/commands/prompt.ts +421 -0
- package/bin/commands/ps.js +78 -71
- package/bin/commands/ps.ts +78 -0
- package/bin/commands/session.js +308 -0
- package/bin/commands/session.ts +265 -0
- package/bin/commands/show.js +75 -40
- package/bin/commands/show.ts +69 -0
- package/bin/ima2.js +324 -310
- package/bin/ima2.ts +444 -0
- package/bin/lib/args.js +75 -66
- package/bin/lib/args.ts +73 -0
- package/bin/lib/browser-id.js +15 -0
- package/bin/lib/browser-id.ts +16 -0
- package/bin/lib/client.js +91 -83
- package/bin/lib/client.ts +109 -0
- package/bin/lib/error-hints.js +14 -17
- package/bin/lib/error-hints.ts +23 -0
- package/bin/lib/files.js +26 -28
- package/bin/lib/files.ts +39 -0
- package/bin/lib/output.js +44 -42
- package/bin/lib/output.ts +58 -0
- package/bin/lib/platform.js +60 -56
- package/bin/lib/platform.ts +97 -0
- package/bin/lib/sse.js +73 -0
- package/bin/lib/sse.ts +73 -0
- package/bin/lib/star-prompt.js +69 -76
- package/bin/lib/star-prompt.ts +97 -0
- package/bin/lib/storage-doctor.js +34 -35
- package/bin/lib/storage-doctor.ts +38 -0
- package/config.js +147 -190
- package/config.ts +331 -0
- package/docs/API.md +48 -8
- package/docs/CLI.md +190 -0
- package/docs/FAQ.ko.md +5 -5
- package/docs/FAQ.md +5 -5
- package/docs/README.ja.md +71 -25
- package/docs/README.ko.md +61 -24
- package/docs/README.zh-CN.md +73 -27
- package/lib/assetLifecycle.js +130 -130
- package/lib/assetLifecycle.ts +142 -0
- package/lib/canvasVersionStore.js +135 -153
- package/lib/canvasVersionStore.ts +181 -0
- package/lib/cardNewsGenerator.js +127 -142
- package/lib/cardNewsGenerator.ts +162 -0
- package/lib/cardNewsJobStore.js +78 -84
- package/lib/cardNewsJobStore.ts +107 -0
- package/lib/cardNewsManifestStore.js +88 -93
- package/lib/cardNewsManifestStore.ts +112 -0
- package/lib/cardNewsPlanner.js +157 -152
- package/lib/cardNewsPlanner.ts +180 -0
- package/lib/cardNewsPlannerClient.js +101 -98
- package/lib/cardNewsPlannerClient.ts +114 -0
- package/lib/cardNewsPlannerPrompt.js +56 -56
- package/lib/cardNewsPlannerPrompt.ts +60 -0
- package/lib/cardNewsPlannerSchema.js +231 -223
- package/lib/cardNewsPlannerSchema.ts +259 -0
- package/lib/cardNewsRoleTemplateStore.js +39 -41
- package/lib/cardNewsRoleTemplateStore.ts +47 -0
- package/lib/cardNewsTemplateStore.js +171 -175
- package/lib/cardNewsTemplateStore.ts +210 -0
- package/lib/codexDetect.js +44 -47
- package/lib/codexDetect.ts +69 -0
- package/lib/comfyBridge.js +164 -184
- package/lib/comfyBridge.ts +214 -0
- package/lib/db.js +41 -51
- package/lib/db.ts +166 -0
- package/lib/errorClassify.js +62 -78
- package/lib/errorClassify.ts +100 -0
- package/lib/generationErrors.js +140 -103
- package/lib/generationErrors.ts +125 -0
- package/lib/historyList.js +149 -147
- package/lib/historyList.ts +164 -0
- package/lib/imageMetadata.js +86 -89
- package/lib/imageMetadata.ts +111 -0
- package/lib/imageMetadataStore.js +46 -51
- package/lib/imageMetadataStore.ts +67 -0
- package/lib/imageModels.js +38 -45
- package/lib/imageModels.ts +52 -0
- package/lib/inflight.js +131 -150
- package/lib/inflight.ts +204 -0
- package/lib/localImportStore.js +105 -0
- package/lib/localImportStore.ts +111 -0
- package/lib/logger.js +105 -112
- package/lib/logger.ts +150 -0
- package/lib/nodeStore.js +65 -64
- package/lib/nodeStore.ts +81 -0
- package/lib/oauthLauncher.js +61 -59
- package/lib/oauthLauncher.ts +64 -0
- package/lib/oauthNormalize.js +15 -19
- package/lib/oauthNormalize.ts +30 -0
- package/lib/oauthProxy.js +834 -832
- package/lib/oauthProxy.ts +995 -0
- package/lib/openDirectory.js +41 -40
- package/lib/openDirectory.ts +45 -0
- package/lib/pngInfo.js +18 -20
- package/lib/pngInfo.ts +26 -0
- package/lib/promptImport/curatedSources.js +135 -0
- package/lib/promptImport/curatedSources.ts +139 -0
- package/lib/promptImport/discoveryRegistry.js +218 -0
- package/lib/promptImport/discoveryRegistry.ts +236 -0
- package/lib/promptImport/errors.js +10 -10
- package/lib/promptImport/errors.ts +18 -0
- package/lib/promptImport/githubDiscovery.js +238 -0
- package/lib/promptImport/githubDiscovery.ts +248 -0
- package/lib/promptImport/githubFolder.js +302 -0
- package/lib/promptImport/githubFolder.ts +308 -0
- package/lib/promptImport/githubSource.js +194 -171
- package/lib/promptImport/githubSource.ts +239 -0
- package/lib/promptImport/gptImageHints.js +61 -0
- package/lib/promptImport/gptImageHints.ts +68 -0
- package/lib/promptImport/parsePromptCandidates.js +110 -112
- package/lib/promptImport/parsePromptCandidates.ts +153 -0
- package/lib/promptImport/promptIndex.js +230 -0
- package/lib/promptImport/promptIndex.ts +248 -0
- package/lib/promptImport/rankPromptCandidates.js +52 -0
- package/lib/promptImport/rankPromptCandidates.ts +49 -0
- package/lib/providerOptions.js +31 -0
- package/lib/providerOptions.ts +41 -0
- package/lib/referenceImageCompress.js +51 -62
- package/lib/referenceImageCompress.ts +75 -0
- package/lib/refs.js +93 -81
- package/lib/refs.ts +117 -0
- package/lib/requestLogger.js +32 -38
- package/lib/requestLogger.ts +48 -0
- package/lib/responsesImageAdapter.js +351 -0
- package/lib/responsesImageAdapter.ts +352 -0
- package/lib/runtimePorts.js +71 -73
- package/lib/runtimePorts.ts +93 -0
- package/lib/sessionStore.js +179 -230
- package/lib/sessionStore.ts +272 -0
- package/lib/storageMigration.js +247 -245
- package/lib/storageMigration.ts +284 -0
- package/lib/styleSheet.js +86 -90
- package/lib/styleSheet.ts +128 -0
- package/lib/systemTrash.js +18 -0
- package/lib/systemTrash.ts +20 -0
- package/package.json +26 -10
- package/routes/annotations.js +76 -79
- package/routes/annotations.ts +95 -0
- package/routes/canvasVersions.js +50 -54
- package/routes/canvasVersions.ts +64 -0
- package/routes/cardNews.js +158 -171
- package/routes/cardNews.ts +183 -0
- package/routes/comfy.js +23 -31
- package/routes/comfy.ts +39 -0
- package/routes/edit.js +183 -214
- package/routes/edit.ts +230 -0
- package/routes/generate.js +269 -291
- package/routes/generate.ts +309 -0
- package/routes/health.js +102 -107
- package/routes/health.ts +114 -0
- package/routes/history.js +136 -144
- package/routes/history.ts +153 -0
- package/routes/imageImport.js +33 -0
- package/routes/imageImport.ts +33 -0
- package/routes/index.js +18 -16
- package/routes/index.ts +35 -0
- package/routes/metadata.js +60 -64
- package/routes/metadata.ts +71 -0
- package/routes/multimode.js +228 -263
- package/routes/multimode.ts +280 -0
- package/routes/nodes.js +378 -424
- package/routes/nodes.ts +455 -0
- package/routes/promptImport.js +291 -152
- package/routes/promptImport.ts +354 -0
- package/routes/prompts.js +333 -360
- package/routes/prompts.ts +379 -0
- package/routes/sessions.js +277 -285
- package/routes/sessions.ts +292 -0
- package/routes/storage.js +29 -31
- package/routes/storage.ts +39 -0
- package/server.js +189 -196
- package/server.ts +235 -0
- package/ui/dist/.vite/manifest.json +101 -0
- package/ui/dist/assets/CardNewsWorkspace-BJOCey7Z.js +2 -0
- package/ui/dist/assets/NodeCanvas-BZV40eAE.css +1 -0
- package/ui/dist/assets/NodeCanvas-C3dzYNsk.js +7 -0
- package/ui/dist/assets/PromptImportDialog-Dqu1VpUh.js +2 -0
- package/ui/dist/assets/PromptImportDiscoverySection-Dg8T9X0L.js +1 -0
- package/ui/dist/assets/PromptImportFolderSection-DBaqsFO4.js +1 -0
- package/ui/dist/assets/PromptLibraryPanel-p5QqR97M.js +2 -0
- package/ui/dist/assets/SettingsWorkspace-B5bSAZ6u.js +1 -0
- package/ui/dist/assets/index-C9cXwiWE.js +25 -0
- package/ui/dist/assets/index-CGMIkZXn.css +1 -0
- package/ui/dist/assets/index-Cvld7dUZ.js +1 -0
- package/ui/dist/index.html +6 -3
- package/assets/screenshot.png +0 -0
- package/assets/screenshots/classic-generate-light.png +0 -0
- package/assets/screenshots/node-graph-branching.png +0 -0
- package/assets/screenshots/settings-oauth-generation.png +0 -0
- package/assets/screenshots/settings-workspace.png +0 -0
- package/assets/screenshots/style-sheet-editor.png +0 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
- package/ui/dist/assets/index-DARPdT4Q.css +0 -1
- package/ui/dist/assets/index-ht80GMq4.js +0 -31
- package/ui/dist/assets/index-ht80GMq4.js.map +0 -1
|
@@ -1,75 +1,64 @@
|
|
|
1
1
|
import sharp from "sharp";
|
|
2
|
-
|
|
3
2
|
const DEFAULT_MAX_B64_BYTES = 6 * 1024 * 1024;
|
|
4
3
|
const DEFAULT_MAX_EDGE = 3840;
|
|
5
4
|
const DEFAULT_QUALITY_LADDER = [85, 75, 65, 55];
|
|
6
5
|
const FALLBACK_MAX_EDGE = 2048;
|
|
7
6
|
const FALLBACK_QUALITY_LADDER = [75, 65, 55];
|
|
8
|
-
|
|
9
7
|
function stripDataUrlPrefix(value) {
|
|
10
|
-
|
|
8
|
+
return String(value || "").replace(/^data:[^;]+;base64,/, "");
|
|
11
9
|
}
|
|
12
|
-
|
|
13
10
|
function toBase64(buffer) {
|
|
14
|
-
|
|
11
|
+
return buffer.toString("base64");
|
|
15
12
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const b64 = toBase64(out);
|
|
35
|
-
if (b64.length <= maxB64Bytes) return { b64, compressed: true, quality, maxEdge };
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
13
|
+
async function encodeJpegWithinBudget(input, { maxB64Bytes, maxEdge, qualityLadder, }) {
|
|
14
|
+
for (const quality of qualityLadder) {
|
|
15
|
+
const out = await sharp(input, { failOn: "none" })
|
|
16
|
+
.rotate()
|
|
17
|
+
.resize({
|
|
18
|
+
width: maxEdge,
|
|
19
|
+
height: maxEdge,
|
|
20
|
+
fit: "inside",
|
|
21
|
+
withoutEnlargement: true,
|
|
22
|
+
})
|
|
23
|
+
.flatten({ background: "#ffffff" })
|
|
24
|
+
.jpeg({ quality, progressive: true })
|
|
25
|
+
.toBuffer();
|
|
26
|
+
const b64 = toBase64(out);
|
|
27
|
+
if (b64.length <= maxB64Bytes)
|
|
28
|
+
return { b64, compressed: true, quality, maxEdge };
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
38
31
|
}
|
|
39
|
-
|
|
40
32
|
export async function compressReferenceB64ForOAuth(imageB64, options = {}) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
err.code = "REF_TOO_LARGE";
|
|
73
|
-
err.status = 400;
|
|
74
|
-
throw err;
|
|
33
|
+
const rawB64 = stripDataUrlPrefix(imageB64);
|
|
34
|
+
const maxB64Bytes = options.maxB64Bytes ?? DEFAULT_MAX_B64_BYTES;
|
|
35
|
+
const maxEdge = options.maxEdge ?? DEFAULT_MAX_EDGE;
|
|
36
|
+
const qualityLadder = options.qualityLadder ?? DEFAULT_QUALITY_LADDER;
|
|
37
|
+
if (!rawB64)
|
|
38
|
+
return { b64: rawB64, compressed: false, inputBytes: 0, outputBytes: 0 };
|
|
39
|
+
const input = Buffer.from(rawB64, "base64");
|
|
40
|
+
const inputBytes = rawB64.length;
|
|
41
|
+
if (!options.force && inputBytes <= maxB64Bytes) {
|
|
42
|
+
return { b64: rawB64, compressed: false, inputBytes, outputBytes: inputBytes };
|
|
43
|
+
}
|
|
44
|
+
const primary = await encodeJpegWithinBudget(input, {
|
|
45
|
+
maxB64Bytes,
|
|
46
|
+
maxEdge,
|
|
47
|
+
qualityLadder,
|
|
48
|
+
});
|
|
49
|
+
if (primary) {
|
|
50
|
+
return { ...primary, inputBytes, outputBytes: primary.b64.length };
|
|
51
|
+
}
|
|
52
|
+
const fallback = await encodeJpegWithinBudget(input, {
|
|
53
|
+
maxB64Bytes,
|
|
54
|
+
maxEdge: options.fallbackMaxEdge ?? FALLBACK_MAX_EDGE,
|
|
55
|
+
qualityLadder: options.fallbackQualityLadder ?? FALLBACK_QUALITY_LADDER,
|
|
56
|
+
});
|
|
57
|
+
if (fallback) {
|
|
58
|
+
return { ...fallback, inputBytes, outputBytes: fallback.b64.length };
|
|
59
|
+
}
|
|
60
|
+
const err = new Error(`Reference image remains above ${maxB64Bytes} base64 bytes after compression`);
|
|
61
|
+
err.code = "REF_TOO_LARGE";
|
|
62
|
+
err.status = 400;
|
|
63
|
+
throw err;
|
|
75
64
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import sharp from "sharp";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_B64_BYTES = 6 * 1024 * 1024;
|
|
4
|
+
const DEFAULT_MAX_EDGE = 3840;
|
|
5
|
+
const DEFAULT_QUALITY_LADDER = [85, 75, 65, 55];
|
|
6
|
+
const FALLBACK_MAX_EDGE = 2048;
|
|
7
|
+
const FALLBACK_QUALITY_LADDER = [75, 65, 55];
|
|
8
|
+
|
|
9
|
+
function stripDataUrlPrefix(value) {
|
|
10
|
+
return String(value || "").replace(/^data:[^;]+;base64,/, "");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toBase64(buffer) {
|
|
14
|
+
return buffer.toString("base64");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function encodeJpegWithinBudget(input, {
|
|
18
|
+
maxB64Bytes,
|
|
19
|
+
maxEdge,
|
|
20
|
+
qualityLadder,
|
|
21
|
+
}) {
|
|
22
|
+
for (const quality of qualityLadder) {
|
|
23
|
+
const out = await sharp(input, { failOn: "none" })
|
|
24
|
+
.rotate()
|
|
25
|
+
.resize({
|
|
26
|
+
width: maxEdge,
|
|
27
|
+
height: maxEdge,
|
|
28
|
+
fit: "inside",
|
|
29
|
+
withoutEnlargement: true,
|
|
30
|
+
})
|
|
31
|
+
.flatten({ background: "#ffffff" })
|
|
32
|
+
.jpeg({ quality, progressive: true })
|
|
33
|
+
.toBuffer();
|
|
34
|
+
const b64 = toBase64(out);
|
|
35
|
+
if (b64.length <= maxB64Bytes) return { b64, compressed: true, quality, maxEdge };
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function compressReferenceB64ForOAuth(imageB64, options: any = {}) {
|
|
41
|
+
const rawB64 = stripDataUrlPrefix(imageB64);
|
|
42
|
+
const maxB64Bytes = options.maxB64Bytes ?? DEFAULT_MAX_B64_BYTES;
|
|
43
|
+
const maxEdge = options.maxEdge ?? DEFAULT_MAX_EDGE;
|
|
44
|
+
const qualityLadder = options.qualityLadder ?? DEFAULT_QUALITY_LADDER;
|
|
45
|
+
if (!rawB64) return { b64: rawB64, compressed: false, inputBytes: 0, outputBytes: 0 };
|
|
46
|
+
|
|
47
|
+
const input = Buffer.from(rawB64, "base64");
|
|
48
|
+
const inputBytes = rawB64.length;
|
|
49
|
+
if (!options.force && inputBytes <= maxB64Bytes) {
|
|
50
|
+
return { b64: rawB64, compressed: false, inputBytes, outputBytes: inputBytes };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const primary = await encodeJpegWithinBudget(input, {
|
|
54
|
+
maxB64Bytes,
|
|
55
|
+
maxEdge,
|
|
56
|
+
qualityLadder,
|
|
57
|
+
});
|
|
58
|
+
if (primary) {
|
|
59
|
+
return { ...primary, inputBytes, outputBytes: primary.b64.length };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const fallback = await encodeJpegWithinBudget(input, {
|
|
63
|
+
maxB64Bytes,
|
|
64
|
+
maxEdge: options.fallbackMaxEdge ?? FALLBACK_MAX_EDGE,
|
|
65
|
+
qualityLadder: options.fallbackQualityLadder ?? FALLBACK_QUALITY_LADDER,
|
|
66
|
+
});
|
|
67
|
+
if (fallback) {
|
|
68
|
+
return { ...fallback, inputBytes, outputBytes: fallback.b64.length };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const err: any = new Error(`Reference image remains above ${maxB64Bytes} base64 bytes after compression`);
|
|
72
|
+
err.code = "REF_TOO_LARGE";
|
|
73
|
+
err.status = 400;
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
package/lib/refs.js
CHANGED
|
@@ -1,98 +1,110 @@
|
|
|
1
1
|
// lib/refs.js — reference-image validator (0.09.7).
|
|
2
2
|
// Extracted from server.js so unit tests can import without booting the app.
|
|
3
|
-
|
|
4
3
|
import { config } from "../config.js";
|
|
5
|
-
|
|
6
4
|
const BASE64_RE = /^[A-Za-z0-9+/]+=*$/;
|
|
7
5
|
const DATA_URL_RE = /^data:([^;,]+);base64,/i;
|
|
8
|
-
|
|
9
6
|
function approxBase64Bytes(b64) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
try {
|
|
8
|
+
return Buffer.from(b64, "base64").length;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return Math.floor((b64.length * 3) / 4);
|
|
12
|
+
}
|
|
15
13
|
}
|
|
16
|
-
|
|
17
14
|
export function detectImageMimeFromB64(b64) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
let buf;
|
|
16
|
+
try {
|
|
17
|
+
buf = Buffer.from(b64, "base64");
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (buf.length >= 4 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
|
|
23
|
+
return "image/png";
|
|
24
|
+
}
|
|
25
|
+
if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
|
|
26
|
+
return "image/jpeg";
|
|
27
|
+
}
|
|
28
|
+
if (buf.length >= 12 &&
|
|
29
|
+
buf.toString("ascii", 0, 4) === "RIFF" &&
|
|
30
|
+
buf.toString("ascii", 8, 12) === "WEBP") {
|
|
31
|
+
return "image/webp";
|
|
32
|
+
}
|
|
22
33
|
return null;
|
|
23
|
-
}
|
|
24
|
-
if (buf.length >= 4 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
|
|
25
|
-
return "image/png";
|
|
26
|
-
}
|
|
27
|
-
if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
|
|
28
|
-
return "image/jpeg";
|
|
29
|
-
}
|
|
30
|
-
if (
|
|
31
|
-
buf.length >= 12 &&
|
|
32
|
-
buf.toString("ascii", 0, 4) === "RIFF" &&
|
|
33
|
-
buf.toString("ascii", 8, 12) === "WEBP"
|
|
34
|
-
) {
|
|
35
|
-
return "image/webp";
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
34
|
}
|
|
39
|
-
|
|
40
35
|
export function safeReferenceDiagnostics(refDetails = []) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
36
|
+
if (!Array.isArray(refDetails))
|
|
37
|
+
return [];
|
|
38
|
+
return refDetails.map((ref) => ({
|
|
39
|
+
index: ref.index,
|
|
40
|
+
declaredMime: ref.declaredMime || null,
|
|
41
|
+
detectedMime: ref.detectedMime || null,
|
|
42
|
+
b64Chars: ref.b64Chars,
|
|
43
|
+
approxBytes: ref.approxBytes,
|
|
44
|
+
source: ref.source,
|
|
45
|
+
warnings: ref.warnings || [],
|
|
46
|
+
}));
|
|
51
47
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
maxB64Bytes = config.limits.maxRefB64Bytes,
|
|
56
|
-
} = {}) {
|
|
57
|
-
if (!Array.isArray(references)) {
|
|
58
|
-
return { error: "references must be an array", code: "REF_NOT_ARRAY" };
|
|
59
|
-
}
|
|
60
|
-
if (references.length > maxCount) {
|
|
61
|
-
return { error: `references may not exceed ${maxCount} items`, code: "REF_TOO_MANY" };
|
|
62
|
-
}
|
|
63
|
-
const out = [];
|
|
64
|
-
const refDetails = [];
|
|
65
|
-
for (let i = 0; i < references.length; i++) {
|
|
66
|
-
const r = references[i];
|
|
67
|
-
if (typeof r !== "string") {
|
|
68
|
-
return { error: `references[${i}] must be a string`, code: "REF_NOT_STRING" };
|
|
48
|
+
export function summarizeReferencePayload(references) {
|
|
49
|
+
if (!Array.isArray(references)) {
|
|
50
|
+
return { refsCount: 0, referenceBytes: 0, referenceB64Chars: 0 };
|
|
69
51
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
52
|
+
let referenceBytes = 0;
|
|
53
|
+
let referenceB64Chars = 0;
|
|
54
|
+
for (const ref of references) {
|
|
55
|
+
if (typeof ref !== "string")
|
|
56
|
+
continue;
|
|
57
|
+
const b64 = ref.replace(DATA_URL_RE, "");
|
|
58
|
+
referenceB64Chars += b64.length;
|
|
59
|
+
referenceBytes += approxBase64Bytes(b64);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
refsCount: references.length,
|
|
63
|
+
referenceBytes,
|
|
64
|
+
referenceB64Chars,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function validateAndNormalizeRefs(references, { maxCount = config.limits.maxRefCount, maxB64Bytes = config.limits.maxRefB64Bytes, } = {}) {
|
|
68
|
+
if (!Array.isArray(references)) {
|
|
69
|
+
return { error: "references must be an array", code: "REF_NOT_ARRAY" };
|
|
76
70
|
}
|
|
77
|
-
if (
|
|
78
|
-
|
|
71
|
+
if (references.length > maxCount) {
|
|
72
|
+
return { error: `references may not exceed ${maxCount} items`, code: "REF_TOO_MANY" };
|
|
79
73
|
}
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
const out = [];
|
|
75
|
+
const refDetails = [];
|
|
76
|
+
for (let i = 0; i < references.length; i++) {
|
|
77
|
+
const r = references[i];
|
|
78
|
+
if (typeof r !== "string") {
|
|
79
|
+
return { error: `references[${i}] must be a string`, code: "REF_NOT_STRING" };
|
|
80
|
+
}
|
|
81
|
+
const dataUrlMatch = r.match(DATA_URL_RE);
|
|
82
|
+
const declaredMime = dataUrlMatch?.[1]?.toLowerCase() || null;
|
|
83
|
+
const b64 = r.replace(DATA_URL_RE, "");
|
|
84
|
+
if (!b64)
|
|
85
|
+
return { error: `references[${i}] is empty`, code: "REF_EMPTY" };
|
|
86
|
+
if (b64.length > maxB64Bytes) {
|
|
87
|
+
return { error: `references[${i}] exceeds ${maxB64Bytes} bytes`, code: "REF_TOO_LARGE" };
|
|
88
|
+
}
|
|
89
|
+
if (!BASE64_RE.test(b64)) {
|
|
90
|
+
return { error: `references[${i}] is not valid base64`, code: "REF_NOT_BASE64" };
|
|
91
|
+
}
|
|
92
|
+
const detectedMime = detectImageMimeFromB64(b64);
|
|
93
|
+
const warnings = [];
|
|
94
|
+
if (declaredMime && detectedMime && declaredMime !== detectedMime) {
|
|
95
|
+
warnings.push("mime_mismatch");
|
|
96
|
+
}
|
|
97
|
+
out.push(b64);
|
|
98
|
+
refDetails.push({
|
|
99
|
+
index: i,
|
|
100
|
+
b64,
|
|
101
|
+
declaredMime,
|
|
102
|
+
detectedMime,
|
|
103
|
+
b64Chars: b64.length,
|
|
104
|
+
approxBytes: approxBase64Bytes(b64),
|
|
105
|
+
source: declaredMime ? "dataUrl" : "rawBase64",
|
|
106
|
+
warnings,
|
|
107
|
+
});
|
|
84
108
|
}
|
|
85
|
-
out
|
|
86
|
-
refDetails.push({
|
|
87
|
-
index: i,
|
|
88
|
-
b64,
|
|
89
|
-
declaredMime,
|
|
90
|
-
detectedMime,
|
|
91
|
-
b64Chars: b64.length,
|
|
92
|
-
approxBytes: approxBase64Bytes(b64),
|
|
93
|
-
source: declaredMime ? "dataUrl" : "rawBase64",
|
|
94
|
-
warnings,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
return { refs: out, refDetails, referenceDiagnostics: safeReferenceDiagnostics(refDetails) };
|
|
109
|
+
return { refs: out, refDetails, referenceDiagnostics: safeReferenceDiagnostics(refDetails) };
|
|
98
110
|
}
|
package/lib/refs.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// lib/refs.js — reference-image validator (0.09.7).
|
|
2
|
+
// Extracted from server.js so unit tests can import without booting the app.
|
|
3
|
+
|
|
4
|
+
import { config } from "../config.js";
|
|
5
|
+
|
|
6
|
+
const BASE64_RE = /^[A-Za-z0-9+/]+=*$/;
|
|
7
|
+
const DATA_URL_RE = /^data:([^;,]+);base64,/i;
|
|
8
|
+
|
|
9
|
+
function approxBase64Bytes(b64) {
|
|
10
|
+
try {
|
|
11
|
+
return Buffer.from(b64, "base64").length;
|
|
12
|
+
} catch {
|
|
13
|
+
return Math.floor((b64.length * 3) / 4);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function detectImageMimeFromB64(b64) {
|
|
18
|
+
let buf;
|
|
19
|
+
try {
|
|
20
|
+
buf = Buffer.from(b64, "base64");
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (buf.length >= 4 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
|
|
25
|
+
return "image/png";
|
|
26
|
+
}
|
|
27
|
+
if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
|
|
28
|
+
return "image/jpeg";
|
|
29
|
+
}
|
|
30
|
+
if (
|
|
31
|
+
buf.length >= 12 &&
|
|
32
|
+
buf.toString("ascii", 0, 4) === "RIFF" &&
|
|
33
|
+
buf.toString("ascii", 8, 12) === "WEBP"
|
|
34
|
+
) {
|
|
35
|
+
return "image/webp";
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function safeReferenceDiagnostics(refDetails = []) {
|
|
41
|
+
if (!Array.isArray(refDetails)) return [];
|
|
42
|
+
return refDetails.map((ref) => ({
|
|
43
|
+
index: ref.index,
|
|
44
|
+
declaredMime: ref.declaredMime || null,
|
|
45
|
+
detectedMime: ref.detectedMime || null,
|
|
46
|
+
b64Chars: ref.b64Chars,
|
|
47
|
+
approxBytes: ref.approxBytes,
|
|
48
|
+
source: ref.source,
|
|
49
|
+
warnings: ref.warnings || [],
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function summarizeReferencePayload(references) {
|
|
54
|
+
if (!Array.isArray(references)) {
|
|
55
|
+
return { refsCount: 0, referenceBytes: 0, referenceB64Chars: 0 };
|
|
56
|
+
}
|
|
57
|
+
let referenceBytes = 0;
|
|
58
|
+
let referenceB64Chars = 0;
|
|
59
|
+
for (const ref of references) {
|
|
60
|
+
if (typeof ref !== "string") continue;
|
|
61
|
+
const b64 = ref.replace(DATA_URL_RE, "");
|
|
62
|
+
referenceB64Chars += b64.length;
|
|
63
|
+
referenceBytes += approxBase64Bytes(b64);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
refsCount: references.length,
|
|
67
|
+
referenceBytes,
|
|
68
|
+
referenceB64Chars,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function validateAndNormalizeRefs(references, {
|
|
73
|
+
maxCount = config.limits.maxRefCount,
|
|
74
|
+
maxB64Bytes = config.limits.maxRefB64Bytes,
|
|
75
|
+
} = {}) {
|
|
76
|
+
if (!Array.isArray(references)) {
|
|
77
|
+
return { error: "references must be an array", code: "REF_NOT_ARRAY" };
|
|
78
|
+
}
|
|
79
|
+
if (references.length > maxCount) {
|
|
80
|
+
return { error: `references may not exceed ${maxCount} items`, code: "REF_TOO_MANY" };
|
|
81
|
+
}
|
|
82
|
+
const out = [];
|
|
83
|
+
const refDetails = [];
|
|
84
|
+
for (let i = 0; i < references.length; i++) {
|
|
85
|
+
const r = references[i];
|
|
86
|
+
if (typeof r !== "string") {
|
|
87
|
+
return { error: `references[${i}] must be a string`, code: "REF_NOT_STRING" };
|
|
88
|
+
}
|
|
89
|
+
const dataUrlMatch = r.match(DATA_URL_RE);
|
|
90
|
+
const declaredMime = dataUrlMatch?.[1]?.toLowerCase() || null;
|
|
91
|
+
const b64 = r.replace(DATA_URL_RE, "");
|
|
92
|
+
if (!b64) return { error: `references[${i}] is empty`, code: "REF_EMPTY" };
|
|
93
|
+
if (b64.length > maxB64Bytes) {
|
|
94
|
+
return { error: `references[${i}] exceeds ${maxB64Bytes} bytes`, code: "REF_TOO_LARGE" };
|
|
95
|
+
}
|
|
96
|
+
if (!BASE64_RE.test(b64)) {
|
|
97
|
+
return { error: `references[${i}] is not valid base64`, code: "REF_NOT_BASE64" };
|
|
98
|
+
}
|
|
99
|
+
const detectedMime = detectImageMimeFromB64(b64);
|
|
100
|
+
const warnings = [];
|
|
101
|
+
if (declaredMime && detectedMime && declaredMime !== detectedMime) {
|
|
102
|
+
warnings.push("mime_mismatch");
|
|
103
|
+
}
|
|
104
|
+
out.push(b64);
|
|
105
|
+
refDetails.push({
|
|
106
|
+
index: i,
|
|
107
|
+
b64,
|
|
108
|
+
declaredMime,
|
|
109
|
+
detectedMime,
|
|
110
|
+
b64Chars: b64.length,
|
|
111
|
+
approxBytes: approxBase64Bytes(b64),
|
|
112
|
+
source: declaredMime ? "dataUrl" : "rawBase64",
|
|
113
|
+
warnings,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return { refs: out, refDetails, referenceDiagnostics: safeReferenceDiagnostics(refDetails) };
|
|
117
|
+
}
|
package/lib/requestLogger.js
CHANGED
|
@@ -1,48 +1,42 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import { logEvent } from "./logger.js";
|
|
3
|
-
|
|
4
3
|
const REQUEST_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
5
4
|
const IGNORED_LOG_PATHS = new Set(["/api/health", "/api/inflight"]);
|
|
6
|
-
|
|
7
5
|
export function normalizeRequestId(value) {
|
|
8
|
-
|
|
6
|
+
return typeof value === "string" && REQUEST_ID_RE.test(value) ? value : `req_${randomUUID()}`;
|
|
9
7
|
}
|
|
10
|
-
|
|
11
8
|
function requestPath(req) {
|
|
12
|
-
|
|
9
|
+
return String(req.originalUrl || req.url || "").split("?")[0] || "/";
|
|
13
10
|
}
|
|
14
|
-
|
|
15
11
|
export function createRequestLogger() {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
next();
|
|
47
|
-
};
|
|
12
|
+
return function requestLogger(req, res, next) {
|
|
13
|
+
const path = requestPath(req);
|
|
14
|
+
if (!path.startsWith("/api/"))
|
|
15
|
+
return next();
|
|
16
|
+
const requestId = normalizeRequestId(req.get("x-request-id"));
|
|
17
|
+
const startedAt = Date.now();
|
|
18
|
+
req.id = requestId;
|
|
19
|
+
res.setHeader("X-Request-Id", requestId);
|
|
20
|
+
const ignoreLog = IGNORED_LOG_PATHS.has(path);
|
|
21
|
+
if (!ignoreLog) {
|
|
22
|
+
logEvent("http", "request", {
|
|
23
|
+
requestId,
|
|
24
|
+
method: req.method,
|
|
25
|
+
path,
|
|
26
|
+
client: req.get("x-ima2-client") || "ui",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
res.on("finish", () => {
|
|
30
|
+
if (ignoreLog)
|
|
31
|
+
return;
|
|
32
|
+
logEvent("http", "response", {
|
|
33
|
+
requestId,
|
|
34
|
+
method: req.method,
|
|
35
|
+
path,
|
|
36
|
+
status: res.statusCode,
|
|
37
|
+
durationMs: Date.now() - startedAt,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
next();
|
|
41
|
+
};
|
|
48
42
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { logEvent } from "./logger.js";
|
|
3
|
+
|
|
4
|
+
const REQUEST_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
5
|
+
const IGNORED_LOG_PATHS = new Set(["/api/health", "/api/inflight"]);
|
|
6
|
+
|
|
7
|
+
export function normalizeRequestId(value) {
|
|
8
|
+
return typeof value === "string" && REQUEST_ID_RE.test(value) ? value : `req_${randomUUID()}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function requestPath(req) {
|
|
12
|
+
return String(req.originalUrl || req.url || "").split("?")[0] || "/";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createRequestLogger() {
|
|
16
|
+
return function requestLogger(req, res, next) {
|
|
17
|
+
const path = requestPath(req);
|
|
18
|
+
if (!path.startsWith("/api/")) return next();
|
|
19
|
+
|
|
20
|
+
const requestId = normalizeRequestId(req.get("x-request-id"));
|
|
21
|
+
const startedAt = Date.now();
|
|
22
|
+
req.id = requestId;
|
|
23
|
+
res.setHeader("X-Request-Id", requestId);
|
|
24
|
+
|
|
25
|
+
const ignoreLog = IGNORED_LOG_PATHS.has(path);
|
|
26
|
+
if (!ignoreLog) {
|
|
27
|
+
logEvent("http", "request", {
|
|
28
|
+
requestId,
|
|
29
|
+
method: req.method,
|
|
30
|
+
path,
|
|
31
|
+
client: req.get("x-ima2-client") || "ui",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
res.on("finish", () => {
|
|
36
|
+
if (ignoreLog) return;
|
|
37
|
+
logEvent("http", "response", {
|
|
38
|
+
requestId,
|
|
39
|
+
method: req.method,
|
|
40
|
+
path,
|
|
41
|
+
status: res.statusCode,
|
|
42
|
+
durationMs: Date.now() - startedAt,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
next();
|
|
47
|
+
};
|
|
48
|
+
}
|