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.
Files changed (229) hide show
  1. package/README.md +56 -27
  2. package/bin/commands/annotate.js +137 -0
  3. package/bin/commands/annotate.ts +118 -0
  4. package/bin/commands/cancel.js +37 -33
  5. package/bin/commands/cancel.ts +45 -0
  6. package/bin/commands/canvas-versions.js +91 -0
  7. package/bin/commands/canvas-versions.ts +80 -0
  8. package/bin/commands/cardnews.js +293 -0
  9. package/bin/commands/cardnews.ts +248 -0
  10. package/bin/commands/comfy.js +63 -0
  11. package/bin/commands/comfy.ts +54 -0
  12. package/bin/commands/config.js +270 -0
  13. package/bin/commands/config.ts +265 -0
  14. package/bin/commands/edit.js +97 -72
  15. package/bin/commands/edit.ts +116 -0
  16. package/bin/commands/gen.js +140 -118
  17. package/bin/commands/gen.ts +176 -0
  18. package/bin/commands/history.js +164 -0
  19. package/bin/commands/history.ts +145 -0
  20. package/bin/commands/ls.js +60 -42
  21. package/bin/commands/ls.ts +60 -0
  22. package/bin/commands/metadata.js +45 -0
  23. package/bin/commands/metadata.ts +36 -0
  24. package/bin/commands/multimode.js +159 -0
  25. package/bin/commands/multimode.ts +146 -0
  26. package/bin/commands/node.js +176 -0
  27. package/bin/commands/node.ts +157 -0
  28. package/bin/commands/observability.js +201 -0
  29. package/bin/commands/observability.ts +176 -0
  30. package/bin/commands/ping.js +26 -20
  31. package/bin/commands/ping.ts +29 -0
  32. package/bin/commands/prompt.js +506 -0
  33. package/bin/commands/prompt.ts +421 -0
  34. package/bin/commands/ps.js +78 -71
  35. package/bin/commands/ps.ts +78 -0
  36. package/bin/commands/session.js +308 -0
  37. package/bin/commands/session.ts +265 -0
  38. package/bin/commands/show.js +75 -40
  39. package/bin/commands/show.ts +69 -0
  40. package/bin/ima2.js +324 -310
  41. package/bin/ima2.ts +444 -0
  42. package/bin/lib/args.js +75 -66
  43. package/bin/lib/args.ts +73 -0
  44. package/bin/lib/browser-id.js +15 -0
  45. package/bin/lib/browser-id.ts +16 -0
  46. package/bin/lib/client.js +91 -83
  47. package/bin/lib/client.ts +109 -0
  48. package/bin/lib/error-hints.js +14 -17
  49. package/bin/lib/error-hints.ts +23 -0
  50. package/bin/lib/files.js +26 -28
  51. package/bin/lib/files.ts +39 -0
  52. package/bin/lib/output.js +44 -42
  53. package/bin/lib/output.ts +58 -0
  54. package/bin/lib/platform.js +60 -56
  55. package/bin/lib/platform.ts +97 -0
  56. package/bin/lib/sse.js +73 -0
  57. package/bin/lib/sse.ts +73 -0
  58. package/bin/lib/star-prompt.js +69 -76
  59. package/bin/lib/star-prompt.ts +97 -0
  60. package/bin/lib/storage-doctor.js +34 -35
  61. package/bin/lib/storage-doctor.ts +38 -0
  62. package/config.js +147 -190
  63. package/config.ts +331 -0
  64. package/docs/API.md +48 -8
  65. package/docs/CLI.md +190 -0
  66. package/docs/FAQ.ko.md +5 -5
  67. package/docs/FAQ.md +5 -5
  68. package/docs/README.ja.md +71 -25
  69. package/docs/README.ko.md +61 -24
  70. package/docs/README.zh-CN.md +73 -27
  71. package/lib/assetLifecycle.js +130 -130
  72. package/lib/assetLifecycle.ts +142 -0
  73. package/lib/canvasVersionStore.js +135 -153
  74. package/lib/canvasVersionStore.ts +181 -0
  75. package/lib/cardNewsGenerator.js +127 -142
  76. package/lib/cardNewsGenerator.ts +162 -0
  77. package/lib/cardNewsJobStore.js +78 -84
  78. package/lib/cardNewsJobStore.ts +107 -0
  79. package/lib/cardNewsManifestStore.js +88 -93
  80. package/lib/cardNewsManifestStore.ts +112 -0
  81. package/lib/cardNewsPlanner.js +157 -152
  82. package/lib/cardNewsPlanner.ts +180 -0
  83. package/lib/cardNewsPlannerClient.js +101 -98
  84. package/lib/cardNewsPlannerClient.ts +114 -0
  85. package/lib/cardNewsPlannerPrompt.js +56 -56
  86. package/lib/cardNewsPlannerPrompt.ts +60 -0
  87. package/lib/cardNewsPlannerSchema.js +231 -223
  88. package/lib/cardNewsPlannerSchema.ts +259 -0
  89. package/lib/cardNewsRoleTemplateStore.js +39 -41
  90. package/lib/cardNewsRoleTemplateStore.ts +47 -0
  91. package/lib/cardNewsTemplateStore.js +171 -175
  92. package/lib/cardNewsTemplateStore.ts +210 -0
  93. package/lib/codexDetect.js +44 -47
  94. package/lib/codexDetect.ts +69 -0
  95. package/lib/comfyBridge.js +164 -184
  96. package/lib/comfyBridge.ts +214 -0
  97. package/lib/db.js +41 -51
  98. package/lib/db.ts +166 -0
  99. package/lib/errorClassify.js +62 -78
  100. package/lib/errorClassify.ts +100 -0
  101. package/lib/generationErrors.js +140 -103
  102. package/lib/generationErrors.ts +125 -0
  103. package/lib/historyList.js +149 -147
  104. package/lib/historyList.ts +164 -0
  105. package/lib/imageMetadata.js +86 -89
  106. package/lib/imageMetadata.ts +111 -0
  107. package/lib/imageMetadataStore.js +46 -51
  108. package/lib/imageMetadataStore.ts +67 -0
  109. package/lib/imageModels.js +38 -45
  110. package/lib/imageModels.ts +52 -0
  111. package/lib/inflight.js +131 -150
  112. package/lib/inflight.ts +204 -0
  113. package/lib/localImportStore.js +105 -0
  114. package/lib/localImportStore.ts +111 -0
  115. package/lib/logger.js +105 -112
  116. package/lib/logger.ts +150 -0
  117. package/lib/nodeStore.js +65 -64
  118. package/lib/nodeStore.ts +81 -0
  119. package/lib/oauthLauncher.js +61 -59
  120. package/lib/oauthLauncher.ts +64 -0
  121. package/lib/oauthNormalize.js +15 -19
  122. package/lib/oauthNormalize.ts +30 -0
  123. package/lib/oauthProxy.js +834 -832
  124. package/lib/oauthProxy.ts +995 -0
  125. package/lib/openDirectory.js +41 -40
  126. package/lib/openDirectory.ts +45 -0
  127. package/lib/pngInfo.js +18 -20
  128. package/lib/pngInfo.ts +26 -0
  129. package/lib/promptImport/curatedSources.js +135 -0
  130. package/lib/promptImport/curatedSources.ts +139 -0
  131. package/lib/promptImport/discoveryRegistry.js +218 -0
  132. package/lib/promptImport/discoveryRegistry.ts +236 -0
  133. package/lib/promptImport/errors.js +10 -10
  134. package/lib/promptImport/errors.ts +18 -0
  135. package/lib/promptImport/githubDiscovery.js +238 -0
  136. package/lib/promptImport/githubDiscovery.ts +248 -0
  137. package/lib/promptImport/githubFolder.js +302 -0
  138. package/lib/promptImport/githubFolder.ts +308 -0
  139. package/lib/promptImport/githubSource.js +194 -171
  140. package/lib/promptImport/githubSource.ts +239 -0
  141. package/lib/promptImport/gptImageHints.js +61 -0
  142. package/lib/promptImport/gptImageHints.ts +68 -0
  143. package/lib/promptImport/parsePromptCandidates.js +110 -112
  144. package/lib/promptImport/parsePromptCandidates.ts +153 -0
  145. package/lib/promptImport/promptIndex.js +230 -0
  146. package/lib/promptImport/promptIndex.ts +248 -0
  147. package/lib/promptImport/rankPromptCandidates.js +52 -0
  148. package/lib/promptImport/rankPromptCandidates.ts +49 -0
  149. package/lib/providerOptions.js +31 -0
  150. package/lib/providerOptions.ts +41 -0
  151. package/lib/referenceImageCompress.js +51 -62
  152. package/lib/referenceImageCompress.ts +75 -0
  153. package/lib/refs.js +93 -81
  154. package/lib/refs.ts +117 -0
  155. package/lib/requestLogger.js +32 -38
  156. package/lib/requestLogger.ts +48 -0
  157. package/lib/responsesImageAdapter.js +351 -0
  158. package/lib/responsesImageAdapter.ts +352 -0
  159. package/lib/runtimePorts.js +71 -73
  160. package/lib/runtimePorts.ts +93 -0
  161. package/lib/sessionStore.js +179 -230
  162. package/lib/sessionStore.ts +272 -0
  163. package/lib/storageMigration.js +247 -245
  164. package/lib/storageMigration.ts +284 -0
  165. package/lib/styleSheet.js +86 -90
  166. package/lib/styleSheet.ts +128 -0
  167. package/lib/systemTrash.js +18 -0
  168. package/lib/systemTrash.ts +20 -0
  169. package/package.json +26 -10
  170. package/routes/annotations.js +76 -79
  171. package/routes/annotations.ts +95 -0
  172. package/routes/canvasVersions.js +50 -54
  173. package/routes/canvasVersions.ts +64 -0
  174. package/routes/cardNews.js +158 -171
  175. package/routes/cardNews.ts +183 -0
  176. package/routes/comfy.js +23 -31
  177. package/routes/comfy.ts +39 -0
  178. package/routes/edit.js +183 -214
  179. package/routes/edit.ts +230 -0
  180. package/routes/generate.js +269 -291
  181. package/routes/generate.ts +309 -0
  182. package/routes/health.js +102 -107
  183. package/routes/health.ts +114 -0
  184. package/routes/history.js +136 -144
  185. package/routes/history.ts +153 -0
  186. package/routes/imageImport.js +33 -0
  187. package/routes/imageImport.ts +33 -0
  188. package/routes/index.js +18 -16
  189. package/routes/index.ts +35 -0
  190. package/routes/metadata.js +60 -64
  191. package/routes/metadata.ts +71 -0
  192. package/routes/multimode.js +228 -263
  193. package/routes/multimode.ts +280 -0
  194. package/routes/nodes.js +378 -424
  195. package/routes/nodes.ts +455 -0
  196. package/routes/promptImport.js +291 -152
  197. package/routes/promptImport.ts +354 -0
  198. package/routes/prompts.js +333 -360
  199. package/routes/prompts.ts +379 -0
  200. package/routes/sessions.js +277 -285
  201. package/routes/sessions.ts +292 -0
  202. package/routes/storage.js +29 -31
  203. package/routes/storage.ts +39 -0
  204. package/server.js +189 -196
  205. package/server.ts +235 -0
  206. package/ui/dist/.vite/manifest.json +101 -0
  207. package/ui/dist/assets/CardNewsWorkspace-BJOCey7Z.js +2 -0
  208. package/ui/dist/assets/NodeCanvas-BZV40eAE.css +1 -0
  209. package/ui/dist/assets/NodeCanvas-C3dzYNsk.js +7 -0
  210. package/ui/dist/assets/PromptImportDialog-Dqu1VpUh.js +2 -0
  211. package/ui/dist/assets/PromptImportDiscoverySection-Dg8T9X0L.js +1 -0
  212. package/ui/dist/assets/PromptImportFolderSection-DBaqsFO4.js +1 -0
  213. package/ui/dist/assets/PromptLibraryPanel-p5QqR97M.js +2 -0
  214. package/ui/dist/assets/SettingsWorkspace-B5bSAZ6u.js +1 -0
  215. package/ui/dist/assets/index-C9cXwiWE.js +25 -0
  216. package/ui/dist/assets/index-CGMIkZXn.css +1 -0
  217. package/ui/dist/assets/index-Cvld7dUZ.js +1 -0
  218. package/ui/dist/index.html +6 -3
  219. package/assets/screenshot.png +0 -0
  220. package/assets/screenshots/classic-generate-light.png +0 -0
  221. package/assets/screenshots/node-graph-branching.png +0 -0
  222. package/assets/screenshots/settings-oauth-generation.png +0 -0
  223. package/assets/screenshots/settings-workspace.png +0 -0
  224. package/assets/screenshots/style-sheet-editor.png +0 -0
  225. package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  226. package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
  227. package/ui/dist/assets/index-DARPdT4Q.css +0 -1
  228. package/ui/dist/assets/index-ht80GMq4.js +0 -31
  229. 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
- return String(value || "").replace(/^data:[^;]+;base64,/, "");
8
+ return String(value || "").replace(/^data:[^;]+;base64,/, "");
11
9
  }
12
-
13
10
  function toBase64(buffer) {
14
- return buffer.toString("base64");
11
+ return buffer.toString("base64");
15
12
  }
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;
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
- 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 = new Error(`Reference image remains above ${maxB64Bytes} base64 bytes after compression`);
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
- try {
11
- return Buffer.from(b64, "base64").length;
12
- } catch {
13
- return Math.floor((b64.length * 3) / 4);
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
- let buf;
19
- try {
20
- buf = Buffer.from(b64, "base64");
21
- } catch {
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
- 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
- }));
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
- export function validateAndNormalizeRefs(references, {
54
- maxCount = config.limits.maxRefCount,
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
- const dataUrlMatch = r.match(DATA_URL_RE);
71
- const declaredMime = dataUrlMatch?.[1]?.toLowerCase() || null;
72
- const b64 = r.replace(DATA_URL_RE, "");
73
- if (!b64) return { error: `references[${i}] is empty`, code: "REF_EMPTY" };
74
- if (b64.length > maxB64Bytes) {
75
- return { error: `references[${i}] exceeds ${maxB64Bytes} bytes`, code: "REF_TOO_LARGE" };
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 (!BASE64_RE.test(b64)) {
78
- return { error: `references[${i}] is not valid base64`, code: "REF_NOT_BASE64" };
71
+ if (references.length > maxCount) {
72
+ return { error: `references may not exceed ${maxCount} items`, code: "REF_TOO_MANY" };
79
73
  }
80
- const detectedMime = detectImageMimeFromB64(b64);
81
- const warnings = [];
82
- if (declaredMime && detectedMime && declaredMime !== detectedMime) {
83
- warnings.push("mime_mismatch");
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.push(b64);
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
+ }
@@ -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
- return typeof value === "string" && REQUEST_ID_RE.test(value) ? value : `req_${randomUUID()}`;
6
+ return typeof value === "string" && REQUEST_ID_RE.test(value) ? value : `req_${randomUUID()}`;
9
7
  }
10
-
11
8
  function requestPath(req) {
12
- return String(req.originalUrl || req.url || "").split("?")[0] || "/";
9
+ return String(req.originalUrl || req.url || "").split("?")[0] || "/";
13
10
  }
14
-
15
11
  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
- };
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
+ }