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
@@ -3,179 +3,161 @@ import { constants } from "fs";
3
3
  import { basename, join, normalize, parse } from "path";
4
4
  import { randomBytes } from "crypto";
5
5
  import { embedImageMetadataBestEffort } from "./imageMetadataStore.js";
6
-
7
6
  const PNG_SIGNATURE = "89504e470d0a1a0a";
8
-
9
7
  function assertPngBuffer(buffer) {
10
- if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
11
- const err = new Error("PNG body is required");
12
- err.status = 400;
13
- err.code = "EMPTY_CANVAS_VERSION";
14
- throw err;
15
- }
16
- if (buffer.subarray(0, 8).toString("hex") !== PNG_SIGNATURE) {
17
- const err = new Error("Canvas version body must be a PNG image");
18
- err.status = 400;
19
- err.code = "CANVAS_VERSION_NOT_PNG";
20
- throw err;
21
- }
8
+ if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
9
+ const err = new Error("PNG body is required");
10
+ err.status = 400;
11
+ err.code = "EMPTY_CANVAS_VERSION";
12
+ throw err;
13
+ }
14
+ if (buffer.subarray(0, 8).toString("hex") !== PNG_SIGNATURE) {
15
+ const err = new Error("Canvas version body must be a PNG image");
16
+ err.status = 400;
17
+ err.code = "CANVAS_VERSION_NOT_PNG";
18
+ throw err;
19
+ }
22
20
  }
23
-
24
21
  function assertSafeFilename(filename) {
25
- if (
26
- typeof filename !== "string" ||
27
- filename.length === 0 ||
28
- filename !== basename(filename) ||
29
- filename.includes("..") ||
30
- !/^canvas-[a-zA-Z0-9._-]+\.png$/.test(filename)
31
- ) {
32
- const err = new Error("Invalid canvas version filename");
33
- err.status = 400;
34
- err.code = "INVALID_CANVAS_VERSION_FILENAME";
35
- throw err;
36
- }
22
+ if (typeof filename !== "string" ||
23
+ filename.length === 0 ||
24
+ filename !== basename(filename) ||
25
+ filename.includes("..") ||
26
+ !/^canvas-[a-zA-Z0-9._-]+\.png$/.test(filename)) {
27
+ const err = new Error("Invalid canvas version filename");
28
+ err.status = 400;
29
+ err.code = "INVALID_CANVAS_VERSION_FILENAME";
30
+ throw err;
31
+ }
37
32
  }
38
-
39
33
  function safeSourceBase(sourceFilename) {
40
- const parsed = parse(basename(String(sourceFilename || "image")));
41
- return parsed.name.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "image";
34
+ const parsed = parse(basename(String(sourceFilename || "image")));
35
+ return parsed.name.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "image";
42
36
  }
43
-
44
37
  function ensureInsideGeneratedDir(generatedDir, filename) {
45
- const full = normalize(join(generatedDir, filename));
46
- const root = normalize(generatedDir);
47
- if (!full.startsWith(root)) {
48
- const err = new Error("Canvas version path escapes generated directory");
49
- err.status = 400;
50
- err.code = "CANVAS_VERSION_PATH_ESCAPE";
51
- throw err;
52
- }
53
- return full;
38
+ const full = normalize(join(generatedDir, filename));
39
+ const root = normalize(generatedDir);
40
+ if (!full.startsWith(root)) {
41
+ const err = new Error("Canvas version path escapes generated directory");
42
+ err.status = 400;
43
+ err.code = "CANVAS_VERSION_PATH_ESCAPE";
44
+ throw err;
45
+ }
46
+ return full;
54
47
  }
55
-
56
48
  function makeCanvasFilename(sourceFilename) {
57
- const stamp = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "");
58
- const rand = randomBytes(3).toString("hex");
59
- return `canvas-${safeSourceBase(sourceFilename)}-${stamp}-${rand}.png`;
49
+ const stamp = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "");
50
+ const rand = randomBytes(3).toString("hex");
51
+ return `canvas-${safeSourceBase(sourceFilename)}-${stamp}-${rand}.png`;
60
52
  }
61
-
62
53
  async function writeCanvasPng(ctx, filename, buffer, meta) {
63
- await mkdir(ctx.config.storage.generatedDir, { recursive: true });
64
- const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, filename);
65
- const embedded = await embedImageMetadataBestEffort(buffer, "png", meta, {
66
- version: ctx.packageVersion,
67
- });
68
- await writeFile(full, embedded.buffer);
69
- await writeFile(`${full}.json`, JSON.stringify(meta)).catch(() => {});
54
+ await mkdir(ctx.config.storage.generatedDir, { recursive: true });
55
+ const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, filename);
56
+ const embedded = await embedImageMetadataBestEffort(buffer, "png", meta, {
57
+ version: ctx.packageVersion,
58
+ });
59
+ await writeFile(full, embedded.buffer);
60
+ await writeFile(`${full}.json`, JSON.stringify(meta)).catch(() => { });
70
61
  }
71
-
72
62
  async function readGeneratedMetadata(ctx, filename) {
73
- if (!filename) return null;
74
- try {
75
- const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, basename(filename));
76
- return JSON.parse(await readFile(`${full}.json`, "utf8"));
77
- } catch {
78
- return null;
79
- }
63
+ if (!filename)
64
+ return null;
65
+ try {
66
+ const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, basename(filename));
67
+ return JSON.parse(await readFile(`${full}.json`, "utf8"));
68
+ }
69
+ catch {
70
+ return null;
71
+ }
80
72
  }
81
-
82
73
  function firstString(...values) {
83
- return values.find((value) => typeof value === "string" && value.trim().length > 0) ?? null;
74
+ return values.find((value) => typeof value === "string" && value.trim().length > 0) ?? null;
84
75
  }
85
-
86
76
  function toGenerateItem(filename, meta) {
87
- const url = `/generated/${encodeURIComponent(filename)}`;
88
- return {
89
- image: url,
90
- url,
91
- thumb: url,
92
- filename,
93
- prompt: meta.prompt || undefined,
94
- userPrompt: meta.userPrompt || meta.prompt || null,
95
- revisedPrompt: null,
96
- promptMode: meta.promptMode || "direct",
97
- provider: meta.provider || "canvas",
98
- quality: meta.quality || null,
99
- size: meta.size || null,
100
- format: "png",
101
- moderation: meta.moderation || null,
102
- model: meta.model || null,
103
- usage: null,
104
- createdAt: meta.createdAt,
105
- kind: "edit",
106
- canvasMergedAt: meta.canvasMergedAt,
107
- canvasVersion: true,
108
- canvasSourceFilename: meta.canvasSourceFilename || null,
109
- canvasEditableFilename: filename,
110
- };
77
+ const url = `/generated/${encodeURIComponent(filename)}`;
78
+ return {
79
+ image: url,
80
+ url,
81
+ thumb: url,
82
+ filename,
83
+ prompt: meta.prompt || undefined,
84
+ userPrompt: meta.userPrompt || meta.prompt || null,
85
+ revisedPrompt: null,
86
+ promptMode: meta.promptMode || "direct",
87
+ provider: meta.provider || "canvas",
88
+ quality: meta.quality || null,
89
+ size: meta.size || null,
90
+ format: "png",
91
+ moderation: meta.moderation || null,
92
+ model: meta.model || null,
93
+ usage: null,
94
+ createdAt: meta.createdAt,
95
+ kind: "edit",
96
+ canvasMergedAt: meta.canvasMergedAt,
97
+ canvasVersion: true,
98
+ canvasSourceFilename: meta.canvasSourceFilename || null,
99
+ canvasEditableFilename: filename,
100
+ };
111
101
  }
112
-
113
102
  export async function createCanvasVersion(ctx, input) {
114
- assertPngBuffer(input.buffer);
115
- const sourceFilename = basename(String(input.sourceFilename || ""));
116
- if (!sourceFilename) {
117
- const err = new Error("sourceFilename is required");
118
- err.status = 400;
119
- err.code = "CANVAS_SOURCE_REQUIRED";
120
- throw err;
121
- }
122
- const filename = makeCanvasFilename(sourceFilename);
123
- const now = Date.now();
124
- const sourceMeta = await readGeneratedMetadata(ctx, sourceFilename);
125
- const prompt = firstString(input.prompt, sourceMeta?.userPrompt, sourceMeta?.prompt);
126
- const meta = {
127
- kind: "edit",
128
- provider: "canvas",
129
- format: "png",
130
- prompt,
131
- userPrompt: prompt,
132
- promptMode: sourceMeta?.promptMode || "direct",
133
- createdAt: now,
134
- canvasMergedAt: now,
135
- canvasVersion: true,
136
- canvasSourceFilename: sourceFilename,
137
- canvasEditableFilename: filename,
138
- };
139
- await writeCanvasPng(ctx, filename, input.buffer, meta);
140
- return toGenerateItem(filename, meta);
103
+ assertPngBuffer(input.buffer);
104
+ const sourceFilename = basename(String(input.sourceFilename || ""));
105
+ if (!sourceFilename) {
106
+ const err = new Error("sourceFilename is required");
107
+ err.status = 400;
108
+ err.code = "CANVAS_SOURCE_REQUIRED";
109
+ throw err;
110
+ }
111
+ const filename = makeCanvasFilename(sourceFilename);
112
+ const now = Date.now();
113
+ const sourceMeta = await readGeneratedMetadata(ctx, sourceFilename);
114
+ const prompt = firstString(input.prompt, sourceMeta?.userPrompt, sourceMeta?.prompt);
115
+ const meta = {
116
+ kind: "edit",
117
+ provider: "canvas",
118
+ format: "png",
119
+ prompt,
120
+ userPrompt: prompt,
121
+ promptMode: sourceMeta?.promptMode || "direct",
122
+ createdAt: now,
123
+ canvasMergedAt: now,
124
+ canvasVersion: true,
125
+ canvasSourceFilename: sourceFilename,
126
+ canvasEditableFilename: filename,
127
+ };
128
+ await writeCanvasPng(ctx, filename, input.buffer, meta);
129
+ return toGenerateItem(filename, meta);
141
130
  }
142
-
143
131
  export async function updateCanvasVersion(ctx, filename, input) {
144
- assertSafeFilename(filename);
145
- assertPngBuffer(input.buffer);
146
- const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, filename);
147
- await access(full, constants.F_OK).catch(() => {
148
- const err = new Error("Canvas version not found");
149
- err.status = 404;
150
- err.code = "CANVAS_VERSION_NOT_FOUND";
151
- throw err;
152
- });
153
- const now = Date.now();
154
- const sourceFilename = typeof input.sourceFilename === "string"
155
- ? basename(input.sourceFilename)
156
- : null;
157
- const sourceMeta = await readGeneratedMetadata(ctx, sourceFilename);
158
- const previousMeta = await readGeneratedMetadata(ctx, filename);
159
- const prompt = firstString(
160
- input.prompt,
161
- sourceMeta?.userPrompt,
162
- sourceMeta?.prompt,
163
- previousMeta?.userPrompt,
164
- previousMeta?.prompt,
165
- );
166
- const meta = {
167
- kind: "edit",
168
- provider: "canvas",
169
- format: "png",
170
- prompt,
171
- userPrompt: prompt,
172
- promptMode: sourceMeta?.promptMode || previousMeta?.promptMode || "direct",
173
- createdAt: now,
174
- canvasMergedAt: now,
175
- canvasVersion: true,
176
- canvasSourceFilename: sourceFilename,
177
- canvasEditableFilename: filename,
178
- };
179
- await writeCanvasPng(ctx, filename, input.buffer, meta);
180
- return toGenerateItem(filename, meta);
132
+ assertSafeFilename(filename);
133
+ assertPngBuffer(input.buffer);
134
+ const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, filename);
135
+ await access(full, constants.F_OK).catch(() => {
136
+ const err = new Error("Canvas version not found");
137
+ err.status = 404;
138
+ err.code = "CANVAS_VERSION_NOT_FOUND";
139
+ throw err;
140
+ });
141
+ const now = Date.now();
142
+ const sourceFilename = typeof input.sourceFilename === "string"
143
+ ? basename(input.sourceFilename)
144
+ : null;
145
+ const sourceMeta = await readGeneratedMetadata(ctx, sourceFilename);
146
+ const previousMeta = await readGeneratedMetadata(ctx, filename);
147
+ const prompt = firstString(input.prompt, sourceMeta?.userPrompt, sourceMeta?.prompt, previousMeta?.userPrompt, previousMeta?.prompt);
148
+ const meta = {
149
+ kind: "edit",
150
+ provider: "canvas",
151
+ format: "png",
152
+ prompt,
153
+ userPrompt: prompt,
154
+ promptMode: sourceMeta?.promptMode || previousMeta?.promptMode || "direct",
155
+ createdAt: now,
156
+ canvasMergedAt: now,
157
+ canvasVersion: true,
158
+ canvasSourceFilename: sourceFilename,
159
+ canvasEditableFilename: filename,
160
+ };
161
+ await writeCanvasPng(ctx, filename, input.buffer, meta);
162
+ return toGenerateItem(filename, meta);
181
163
  }
@@ -0,0 +1,181 @@
1
+ import { mkdir, writeFile, access, readFile } from "fs/promises";
2
+ import { constants } from "fs";
3
+ import { basename, join, normalize, parse } from "path";
4
+ import { randomBytes } from "crypto";
5
+ import { embedImageMetadataBestEffort } from "./imageMetadataStore.js";
6
+
7
+ const PNG_SIGNATURE = "89504e470d0a1a0a";
8
+
9
+ function assertPngBuffer(buffer) {
10
+ if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
11
+ const err: any = new Error("PNG body is required");
12
+ err.status = 400;
13
+ err.code = "EMPTY_CANVAS_VERSION";
14
+ throw err;
15
+ }
16
+ if (buffer.subarray(0, 8).toString("hex") !== PNG_SIGNATURE) {
17
+ const err: any = new Error("Canvas version body must be a PNG image");
18
+ err.status = 400;
19
+ err.code = "CANVAS_VERSION_NOT_PNG";
20
+ throw err;
21
+ }
22
+ }
23
+
24
+ function assertSafeFilename(filename) {
25
+ if (
26
+ typeof filename !== "string" ||
27
+ filename.length === 0 ||
28
+ filename !== basename(filename) ||
29
+ filename.includes("..") ||
30
+ !/^canvas-[a-zA-Z0-9._-]+\.png$/.test(filename)
31
+ ) {
32
+ const err: any = new Error("Invalid canvas version filename");
33
+ err.status = 400;
34
+ err.code = "INVALID_CANVAS_VERSION_FILENAME";
35
+ throw err;
36
+ }
37
+ }
38
+
39
+ function safeSourceBase(sourceFilename) {
40
+ const parsed = parse(basename(String(sourceFilename || "image")));
41
+ return parsed.name.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "image";
42
+ }
43
+
44
+ function ensureInsideGeneratedDir(generatedDir, filename) {
45
+ const full = normalize(join(generatedDir, filename));
46
+ const root = normalize(generatedDir);
47
+ if (!full.startsWith(root)) {
48
+ const err: any = new Error("Canvas version path escapes generated directory");
49
+ err.status = 400;
50
+ err.code = "CANVAS_VERSION_PATH_ESCAPE";
51
+ throw err;
52
+ }
53
+ return full;
54
+ }
55
+
56
+ function makeCanvasFilename(sourceFilename) {
57
+ const stamp = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "");
58
+ const rand = randomBytes(3).toString("hex");
59
+ return `canvas-${safeSourceBase(sourceFilename)}-${stamp}-${rand}.png`;
60
+ }
61
+
62
+ async function writeCanvasPng(ctx, filename, buffer, meta) {
63
+ await mkdir(ctx.config.storage.generatedDir, { recursive: true });
64
+ const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, filename);
65
+ const embedded = await embedImageMetadataBestEffort(buffer, "png", meta, {
66
+ version: ctx.packageVersion,
67
+ });
68
+ await writeFile(full, embedded.buffer);
69
+ await writeFile(`${full}.json`, JSON.stringify(meta)).catch(() => {});
70
+ }
71
+
72
+ async function readGeneratedMetadata(ctx, filename) {
73
+ if (!filename) return null;
74
+ try {
75
+ const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, basename(filename));
76
+ return JSON.parse(await readFile(`${full}.json`, "utf8"));
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ function firstString(...values) {
83
+ return values.find((value) => typeof value === "string" && value.trim().length > 0) ?? null;
84
+ }
85
+
86
+ function toGenerateItem(filename, meta) {
87
+ const url = `/generated/${encodeURIComponent(filename)}`;
88
+ return {
89
+ image: url,
90
+ url,
91
+ thumb: url,
92
+ filename,
93
+ prompt: meta.prompt || undefined,
94
+ userPrompt: meta.userPrompt || meta.prompt || null,
95
+ revisedPrompt: null,
96
+ promptMode: meta.promptMode || "direct",
97
+ provider: meta.provider || "canvas",
98
+ quality: meta.quality || null,
99
+ size: meta.size || null,
100
+ format: "png",
101
+ moderation: meta.moderation || null,
102
+ model: meta.model || null,
103
+ usage: null,
104
+ createdAt: meta.createdAt,
105
+ kind: "edit",
106
+ canvasMergedAt: meta.canvasMergedAt,
107
+ canvasVersion: true,
108
+ canvasSourceFilename: meta.canvasSourceFilename || null,
109
+ canvasEditableFilename: filename,
110
+ };
111
+ }
112
+
113
+ export async function createCanvasVersion(ctx, input) {
114
+ assertPngBuffer(input.buffer);
115
+ const sourceFilename = basename(String(input.sourceFilename || ""));
116
+ if (!sourceFilename) {
117
+ const err: any = new Error("sourceFilename is required");
118
+ err.status = 400;
119
+ err.code = "CANVAS_SOURCE_REQUIRED";
120
+ throw err;
121
+ }
122
+ const filename = makeCanvasFilename(sourceFilename);
123
+ const now = Date.now();
124
+ const sourceMeta = await readGeneratedMetadata(ctx, sourceFilename);
125
+ const prompt = firstString(input.prompt, sourceMeta?.userPrompt, sourceMeta?.prompt);
126
+ const meta = {
127
+ kind: "edit",
128
+ provider: "canvas",
129
+ format: "png",
130
+ prompt,
131
+ userPrompt: prompt,
132
+ promptMode: sourceMeta?.promptMode || "direct",
133
+ createdAt: now,
134
+ canvasMergedAt: now,
135
+ canvasVersion: true,
136
+ canvasSourceFilename: sourceFilename,
137
+ canvasEditableFilename: filename,
138
+ };
139
+ await writeCanvasPng(ctx, filename, input.buffer, meta);
140
+ return toGenerateItem(filename, meta);
141
+ }
142
+
143
+ export async function updateCanvasVersion(ctx, filename, input) {
144
+ assertSafeFilename(filename);
145
+ assertPngBuffer(input.buffer);
146
+ const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, filename);
147
+ await access(full, constants.F_OK).catch(() => {
148
+ const err: any = new Error("Canvas version not found");
149
+ err.status = 404;
150
+ err.code = "CANVAS_VERSION_NOT_FOUND";
151
+ throw err;
152
+ });
153
+ const now = Date.now();
154
+ const sourceFilename = typeof input.sourceFilename === "string"
155
+ ? basename(input.sourceFilename)
156
+ : null;
157
+ const sourceMeta = await readGeneratedMetadata(ctx, sourceFilename);
158
+ const previousMeta = await readGeneratedMetadata(ctx, filename);
159
+ const prompt = firstString(
160
+ input.prompt,
161
+ sourceMeta?.userPrompt,
162
+ sourceMeta?.prompt,
163
+ previousMeta?.userPrompt,
164
+ previousMeta?.prompt,
165
+ );
166
+ const meta = {
167
+ kind: "edit",
168
+ provider: "canvas",
169
+ format: "png",
170
+ prompt,
171
+ userPrompt: prompt,
172
+ promptMode: sourceMeta?.promptMode || previousMeta?.promptMode || "direct",
173
+ createdAt: now,
174
+ canvasMergedAt: now,
175
+ canvasVersion: true,
176
+ canvasSourceFilename: sourceFilename,
177
+ canvasEditableFilename: filename,
178
+ };
179
+ await writeCanvasPng(ctx, filename, input.buffer, meta);
180
+ return toGenerateItem(filename, meta);
181
+ }