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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "1.1.7",
3
+ "version": "1.1.9",
4
4
  "description": "Local OAuth image generation studio with classic and node workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "start": "node bin/ima2.js serve",
11
11
  "dev": "node scripts/dev.mjs",
12
- "dev:server": "node --watch server.js",
12
+ "dev:server": "tsx watch server.ts",
13
13
  "ui:install": "cd ui && npm install",
14
14
  "ui:dev": "cd ui && npm run dev",
15
15
  "ui:build": "cd ui && npm run build",
@@ -17,11 +17,15 @@
17
17
  "test": "node scripts/run-tests.mjs",
18
18
  "test:package-install": "node --test tests/package-install-smoke.mjs",
19
19
  "setup": "node bin/ima2.js setup",
20
- "prepublishOnly": "npm test && npm run build && npm run test:package-install && npm run lint:pkg",
21
- "lint:pkg": "node -e \"const p=require('./package.json'); const req=['name','version','bin']; for(const k of req){if(!p[k])throw new Error('missing '+k)} const mustInclude=['bin/','lib/','routes/','integrations/','server.js']; for(const f of mustInclude){if(!p.files.includes(f))throw new Error('files[] must include '+f)}\"",
20
+ "prepack": "npm run ui:build && npm run build:server && npm run build:cli",
21
+ "prepublishOnly": "npm run typecheck && npm run ui:build && npm run build:server && npm run build:cli && npm run lint:pkg && npm run test:package-install",
22
+ "lint:pkg": "node -e \"const p=require('./package.json'); const req=['name','version','bin']; for(const k of req){if(!p[k])throw new Error('missing '+k)} const mustInclude=['bin/','lib/','routes/','integrations/comfyui/ima2_gen_bridge/__init__.py','integrations/comfyui/ima2_gen_bridge/nodes.py','integrations/comfyui/ima2_gen_bridge/README.md','assets/card-news/templates/','server.js']; for(const f of mustInclude){if(!p.files.includes(f))throw new Error('files[] must include '+f)}\"",
22
23
  "release:patch": "npm version patch && npm publish && git push origin main --tags",
23
24
  "release:minor": "npm version minor && npm publish && git push origin main --tags",
24
- "release:major": "npm version major && npm publish && git push origin main --tags"
25
+ "release:major": "npm version major && npm publish && git push origin main --tags",
26
+ "typecheck": "tsc --noEmit -p tsconfig.json",
27
+ "build:server": "tsc -p tsconfig.build.json",
28
+ "build:cli": "tsc -p tsconfig.bin.json && node scripts/fix-shebangs.mjs"
25
29
  },
26
30
  "keywords": [
27
31
  "openai",
@@ -39,14 +43,18 @@
39
43
  "bin/",
40
44
  "lib/",
41
45
  "routes/",
42
- "integrations/",
46
+ "integrations/comfyui/ima2_gen_bridge/__init__.py",
47
+ "integrations/comfyui/ima2_gen_bridge/nodes.py",
48
+ "integrations/comfyui/ima2_gen_bridge/README.md",
43
49
  "ui/dist/",
44
50
  "docs/",
45
- "assets/",
46
- "server.js",
47
- "config.js",
51
+ "assets/card-news/templates/",
52
+ "server.ts",
53
+ "config.ts",
48
54
  ".env.example",
49
- "README.md"
55
+ "README.md",
56
+ "server.js",
57
+ "config.js"
50
58
  ],
51
59
  "engines": {
52
60
  "node": ">=20"
@@ -58,6 +66,14 @@
58
66
  "openai": "^5.8.2",
59
67
  "openai-oauth": "^1.0.2",
60
68
  "sharp": "^0.34.5",
69
+ "trash": "^10.1.1",
61
70
  "ulid": "^3.0.2"
71
+ },
72
+ "devDependencies": {
73
+ "@types/better-sqlite3": "^7.6.13",
74
+ "@types/express": "^5.0.6",
75
+ "@types/node": "^22.19.17",
76
+ "tsx": "^4.21.0",
77
+ "typescript": "^5.9.3"
62
78
  }
63
79
  }
@@ -1,69 +1,64 @@
1
1
  import { getDb } from "../lib/db.js";
2
-
3
2
  const MAX_ANNOTATION_PAYLOAD_CHARS = 256 * 1024;
4
-
5
3
  function getBrowserId(req) {
6
- const browserId = req.headers["x-ima2-browser-id"];
7
- return typeof browserId === "string" && browserId.trim() ? browserId.trim() : null;
4
+ const browserId = req.headers["x-ima2-browser-id"];
5
+ return typeof browserId === "string" && browserId.trim() ? browserId.trim() : null;
8
6
  }
9
-
10
7
  function isSafeFilename(filename) {
11
- return (
12
- typeof filename === "string" &&
13
- filename.length > 0 &&
14
- filename.length <= 240 &&
15
- !filename.includes("..") &&
16
- !filename.startsWith("/") &&
17
- !filename.includes("\\")
18
- );
8
+ return (typeof filename === "string" &&
9
+ filename.length > 0 &&
10
+ filename.length <= 240 &&
11
+ !filename.includes("..") &&
12
+ !filename.startsWith("/") &&
13
+ !filename.includes("\\"));
19
14
  }
20
-
21
15
  function normalizePayload(value) {
22
- const payload = value?.annotations ?? value;
23
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
24
- return { error: "annotations payload is required" };
25
- }
26
- const paths = Array.isArray(payload.paths) ? payload.paths : [];
27
- const boxes = Array.isArray(payload.boxes) ? payload.boxes : [];
28
- const memos = Array.isArray(payload.memos) ? payload.memos : [];
29
- const normalized = { paths, boxes, memos };
30
- const text = JSON.stringify(normalized);
31
- if (text.length > MAX_ANNOTATION_PAYLOAD_CHARS) {
32
- return { error: "annotations payload is too large" };
33
- }
34
- return { payload: normalized, text };
35
- }
36
-
37
- export function registerAnnotationRoutes(app) {
38
- app.get("/api/annotations/:filename", (req, res) => {
39
- try {
40
- const browserId = getBrowserId(req);
41
- const filename = decodeURIComponent(req.params.filename);
42
- if (!browserId) return res.status(400).json({ error: "X-Ima2-Browser-Id header is required" });
43
- if (!isSafeFilename(filename)) return res.status(400).json({ error: "invalid filename" });
44
-
45
- const row = getDb()
46
- .prepare("SELECT payload FROM image_annotations WHERE browser_id = ? AND filename = ?")
47
- .get(browserId, filename);
48
- const annotations = row ? JSON.parse(row.payload) : null;
49
- res.json({ annotations });
50
- } catch (err) {
51
- res.status(500).json({ error: err.message });
16
+ const payload = value?.annotations ?? value;
17
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
18
+ return { error: "annotations payload is required" };
19
+ }
20
+ const paths = Array.isArray(payload.paths) ? payload.paths : [];
21
+ const boxes = Array.isArray(payload.boxes) ? payload.boxes : [];
22
+ const memos = Array.isArray(payload.memos) ? payload.memos : [];
23
+ const normalized = { paths, boxes, memos };
24
+ const text = JSON.stringify(normalized);
25
+ if (text.length > MAX_ANNOTATION_PAYLOAD_CHARS) {
26
+ return { error: "annotations payload is too large" };
52
27
  }
53
- });
54
-
55
- app.put("/api/annotations/:filename", (req, res) => {
56
- try {
57
- const browserId = getBrowserId(req);
58
- const filename = decodeURIComponent(req.params.filename);
59
- if (!browserId) return res.status(400).json({ error: "X-Ima2-Browser-Id header is required" });
60
- if (!isSafeFilename(filename)) return res.status(400).json({ error: "invalid filename" });
61
-
62
- const normalized = normalizePayload(req.body);
63
- if (normalized.error) return res.status(400).json({ error: normalized.error });
64
-
65
- const id = `${browserId}:${filename}`;
66
- getDb().prepare(`
28
+ return { payload: normalized, text };
29
+ }
30
+ export function registerAnnotationRoutes(app, _ctx) {
31
+ app.get("/api/annotations/:filename", (req, res) => {
32
+ try {
33
+ const browserId = getBrowserId(req);
34
+ const filename = decodeURIComponent(req.params.filename);
35
+ if (!browserId)
36
+ return res.status(400).json({ error: "X-Ima2-Browser-Id header is required" });
37
+ if (!isSafeFilename(filename))
38
+ return res.status(400).json({ error: "invalid filename" });
39
+ const row = getDb()
40
+ .prepare("SELECT payload FROM image_annotations WHERE browser_id = ? AND filename = ?")
41
+ .get(browserId, filename);
42
+ const annotations = row ? JSON.parse(row.payload) : null;
43
+ res.json({ annotations });
44
+ }
45
+ catch (err) {
46
+ res.status(500).json({ error: err.message });
47
+ }
48
+ });
49
+ app.put("/api/annotations/:filename", (req, res) => {
50
+ try {
51
+ const browserId = getBrowserId(req);
52
+ const filename = decodeURIComponent(req.params.filename);
53
+ if (!browserId)
54
+ return res.status(400).json({ error: "X-Ima2-Browser-Id header is required" });
55
+ if (!isSafeFilename(filename))
56
+ return res.status(400).json({ error: "invalid filename" });
57
+ const normalized = normalizePayload(req.body);
58
+ if (normalized.error)
59
+ return res.status(400).json({ error: normalized.error });
60
+ const id = `${browserId}:${filename}`;
61
+ getDb().prepare(`
67
62
  INSERT INTO image_annotations (id, browser_id, filename, payload, schema_version, updated_at)
68
63
  VALUES (?, ?, ?, ?, 1, unixepoch())
69
64
  ON CONFLICT(browser_id, filename) DO UPDATE SET
@@ -71,25 +66,27 @@ export function registerAnnotationRoutes(app) {
71
66
  schema_version = excluded.schema_version,
72
67
  updated_at = unixepoch()
73
68
  `).run(id, browserId, filename, normalized.text);
74
- res.json({ ok: true });
75
- } catch (err) {
76
- res.status(500).json({ error: err.message });
77
- }
78
- });
79
-
80
- app.delete("/api/annotations/:filename", (req, res) => {
81
- try {
82
- const browserId = getBrowserId(req);
83
- const filename = decodeURIComponent(req.params.filename);
84
- if (!browserId) return res.status(400).json({ error: "X-Ima2-Browser-Id header is required" });
85
- if (!isSafeFilename(filename)) return res.status(400).json({ error: "invalid filename" });
86
-
87
- getDb()
88
- .prepare("DELETE FROM image_annotations WHERE browser_id = ? AND filename = ?")
89
- .run(browserId, filename);
90
- res.json({ ok: true });
91
- } catch (err) {
92
- res.status(500).json({ error: err.message });
93
- }
94
- });
69
+ res.json({ ok: true });
70
+ }
71
+ catch (err) {
72
+ res.status(500).json({ error: err.message });
73
+ }
74
+ });
75
+ app.delete("/api/annotations/:filename", (req, res) => {
76
+ try {
77
+ const browserId = getBrowserId(req);
78
+ const filename = decodeURIComponent(req.params.filename);
79
+ if (!browserId)
80
+ return res.status(400).json({ error: "X-Ima2-Browser-Id header is required" });
81
+ if (!isSafeFilename(filename))
82
+ return res.status(400).json({ error: "invalid filename" });
83
+ getDb()
84
+ .prepare("DELETE FROM image_annotations WHERE browser_id = ? AND filename = ?")
85
+ .run(browserId, filename);
86
+ res.json({ ok: true });
87
+ }
88
+ catch (err) {
89
+ res.status(500).json({ error: err.message });
90
+ }
91
+ });
95
92
  }
@@ -0,0 +1,95 @@
1
+ import { getDb } from "../lib/db.js";
2
+
3
+ const MAX_ANNOTATION_PAYLOAD_CHARS = 256 * 1024;
4
+
5
+ function getBrowserId(req) {
6
+ const browserId = req.headers["x-ima2-browser-id"];
7
+ return typeof browserId === "string" && browserId.trim() ? browserId.trim() : null;
8
+ }
9
+
10
+ function isSafeFilename(filename) {
11
+ return (
12
+ typeof filename === "string" &&
13
+ filename.length > 0 &&
14
+ filename.length <= 240 &&
15
+ !filename.includes("..") &&
16
+ !filename.startsWith("/") &&
17
+ !filename.includes("\\")
18
+ );
19
+ }
20
+
21
+ function normalizePayload(value) {
22
+ const payload = value?.annotations ?? value;
23
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
24
+ return { error: "annotations payload is required" };
25
+ }
26
+ const paths = Array.isArray(payload.paths) ? payload.paths : [];
27
+ const boxes = Array.isArray(payload.boxes) ? payload.boxes : [];
28
+ const memos = Array.isArray(payload.memos) ? payload.memos : [];
29
+ const normalized = { paths, boxes, memos };
30
+ const text = JSON.stringify(normalized);
31
+ if (text.length > MAX_ANNOTATION_PAYLOAD_CHARS) {
32
+ return { error: "annotations payload is too large" };
33
+ }
34
+ return { payload: normalized, text };
35
+ }
36
+
37
+ export function registerAnnotationRoutes(app, _ctx?: any) {
38
+ app.get("/api/annotations/:filename", (req, res) => {
39
+ try {
40
+ const browserId = getBrowserId(req);
41
+ const filename = decodeURIComponent(req.params.filename);
42
+ if (!browserId) return res.status(400).json({ error: "X-Ima2-Browser-Id header is required" });
43
+ if (!isSafeFilename(filename)) return res.status(400).json({ error: "invalid filename" });
44
+
45
+ const row = getDb()
46
+ .prepare("SELECT payload FROM image_annotations WHERE browser_id = ? AND filename = ?")
47
+ .get(browserId, filename);
48
+ const annotations = row ? JSON.parse(row.payload) : null;
49
+ res.json({ annotations });
50
+ } catch (err) {
51
+ res.status(500).json({ error: err.message });
52
+ }
53
+ });
54
+
55
+ app.put("/api/annotations/:filename", (req, res) => {
56
+ try {
57
+ const browserId = getBrowserId(req);
58
+ const filename = decodeURIComponent(req.params.filename);
59
+ if (!browserId) return res.status(400).json({ error: "X-Ima2-Browser-Id header is required" });
60
+ if (!isSafeFilename(filename)) return res.status(400).json({ error: "invalid filename" });
61
+
62
+ const normalized = normalizePayload(req.body);
63
+ if (normalized.error) return res.status(400).json({ error: normalized.error });
64
+
65
+ const id = `${browserId}:${filename}`;
66
+ getDb().prepare(`
67
+ INSERT INTO image_annotations (id, browser_id, filename, payload, schema_version, updated_at)
68
+ VALUES (?, ?, ?, ?, 1, unixepoch())
69
+ ON CONFLICT(browser_id, filename) DO UPDATE SET
70
+ payload = excluded.payload,
71
+ schema_version = excluded.schema_version,
72
+ updated_at = unixepoch()
73
+ `).run(id, browserId, filename, normalized.text);
74
+ res.json({ ok: true });
75
+ } catch (err) {
76
+ res.status(500).json({ error: err.message });
77
+ }
78
+ });
79
+
80
+ app.delete("/api/annotations/:filename", (req, res) => {
81
+ try {
82
+ const browserId = getBrowserId(req);
83
+ const filename = decodeURIComponent(req.params.filename);
84
+ if (!browserId) return res.status(400).json({ error: "X-Ima2-Browser-Id header is required" });
85
+ if (!isSafeFilename(filename)) return res.status(400).json({ error: "invalid filename" });
86
+
87
+ getDb()
88
+ .prepare("DELETE FROM image_annotations WHERE browser_id = ? AND filename = ?")
89
+ .run(browserId, filename);
90
+ res.json({ ok: true });
91
+ } catch (err) {
92
+ res.status(500).json({ error: err.message });
93
+ }
94
+ });
95
+ }
@@ -1,64 +1,60 @@
1
1
  import express from "express";
2
2
  import { createCanvasVersion, updateCanvasVersion } from "../lib/canvasVersionStore.js";
3
-
4
3
  function decodeHeader(value) {
5
- if (typeof value !== "string" || !value) return null;
6
- try {
7
- return decodeURIComponent(value);
8
- } catch {
9
- return value;
10
- }
4
+ if (typeof value !== "string" || !value)
5
+ return null;
6
+ try {
7
+ return decodeURIComponent(value);
8
+ }
9
+ catch {
10
+ return value;
11
+ }
11
12
  }
12
-
13
13
  function getRequestBuffer(req) {
14
- return Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
14
+ return Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
15
15
  }
16
-
17
16
  function getPrompt(req) {
18
- return decodeHeader(req.headers["x-ima2-canvas-prompt"]);
17
+ return decodeHeader(req.headers["x-ima2-canvas-prompt"]);
19
18
  }
20
-
21
19
  export function registerCanvasVersionRoutes(app, ctx) {
22
- const rawPng = express.raw({ type: "image/png", limit: ctx.config.server.bodyLimit });
23
-
24
- app.post("/api/canvas-versions", rawPng, async (req, res) => {
25
- try {
26
- const sourceFilename =
27
- typeof req.query.sourceFilename === "string"
28
- ? req.query.sourceFilename
29
- : decodeHeader(req.headers["x-ima2-canvas-source-filename"]);
30
- const item = await createCanvasVersion(ctx, {
31
- sourceFilename,
32
- prompt: getPrompt(req),
33
- buffer: getRequestBuffer(req),
34
- });
35
- res.status(201).json({ item });
36
- } catch (err) {
37
- res.status(err.status || 500).json({
38
- error: err.message,
39
- code: err.code || "CANVAS_VERSION_SAVE_FAILED",
40
- });
41
- }
42
- });
43
-
44
- app.put("/api/canvas-versions/:filename", rawPng, async (req, res) => {
45
- try {
46
- const filename = decodeURIComponent(req.params.filename);
47
- const sourceFilename =
48
- typeof req.query.sourceFilename === "string"
49
- ? req.query.sourceFilename
50
- : decodeHeader(req.headers["x-ima2-canvas-source-filename"]);
51
- const item = await updateCanvasVersion(ctx, filename, {
52
- sourceFilename,
53
- prompt: getPrompt(req),
54
- buffer: getRequestBuffer(req),
55
- });
56
- res.json({ item });
57
- } catch (err) {
58
- res.status(err.status || 500).json({
59
- error: err.message,
60
- code: err.code || "CANVAS_VERSION_SAVE_FAILED",
61
- });
62
- }
63
- });
20
+ const rawPng = express.raw({ type: "image/png", limit: ctx.config.server.bodyLimit });
21
+ app.post("/api/canvas-versions", rawPng, async (req, res) => {
22
+ try {
23
+ const sourceFilename = typeof req.query.sourceFilename === "string"
24
+ ? req.query.sourceFilename
25
+ : decodeHeader(req.headers["x-ima2-canvas-source-filename"]);
26
+ const item = await createCanvasVersion(ctx, {
27
+ sourceFilename,
28
+ prompt: getPrompt(req),
29
+ buffer: getRequestBuffer(req),
30
+ });
31
+ res.status(201).json({ item });
32
+ }
33
+ catch (err) {
34
+ res.status(err.status || 500).json({
35
+ error: err.message,
36
+ code: err.code || "CANVAS_VERSION_SAVE_FAILED",
37
+ });
38
+ }
39
+ });
40
+ app.put("/api/canvas-versions/:filename", rawPng, async (req, res) => {
41
+ try {
42
+ const filename = decodeURIComponent(req.params.filename);
43
+ const sourceFilename = typeof req.query.sourceFilename === "string"
44
+ ? req.query.sourceFilename
45
+ : decodeHeader(req.headers["x-ima2-canvas-source-filename"]);
46
+ const item = await updateCanvasVersion(ctx, filename, {
47
+ sourceFilename,
48
+ prompt: getPrompt(req),
49
+ buffer: getRequestBuffer(req),
50
+ });
51
+ res.json({ item });
52
+ }
53
+ catch (err) {
54
+ res.status(err.status || 500).json({
55
+ error: err.message,
56
+ code: err.code || "CANVAS_VERSION_SAVE_FAILED",
57
+ });
58
+ }
59
+ });
64
60
  }
@@ -0,0 +1,64 @@
1
+ import express from "express";
2
+ import { createCanvasVersion, updateCanvasVersion } from "../lib/canvasVersionStore.js";
3
+
4
+ function decodeHeader(value) {
5
+ if (typeof value !== "string" || !value) return null;
6
+ try {
7
+ return decodeURIComponent(value);
8
+ } catch {
9
+ return value;
10
+ }
11
+ }
12
+
13
+ function getRequestBuffer(req) {
14
+ return Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
15
+ }
16
+
17
+ function getPrompt(req) {
18
+ return decodeHeader(req.headers["x-ima2-canvas-prompt"]);
19
+ }
20
+
21
+ export function registerCanvasVersionRoutes(app, ctx) {
22
+ const rawPng = express.raw({ type: "image/png", limit: ctx.config.server.bodyLimit });
23
+
24
+ app.post("/api/canvas-versions", rawPng, async (req, res) => {
25
+ try {
26
+ const sourceFilename =
27
+ typeof req.query.sourceFilename === "string"
28
+ ? req.query.sourceFilename
29
+ : decodeHeader(req.headers["x-ima2-canvas-source-filename"]);
30
+ const item = await createCanvasVersion(ctx, {
31
+ sourceFilename,
32
+ prompt: getPrompt(req),
33
+ buffer: getRequestBuffer(req),
34
+ });
35
+ res.status(201).json({ item });
36
+ } catch (err) {
37
+ res.status(err.status || 500).json({
38
+ error: err.message,
39
+ code: err.code || "CANVAS_VERSION_SAVE_FAILED",
40
+ });
41
+ }
42
+ });
43
+
44
+ app.put("/api/canvas-versions/:filename", rawPng, async (req, res) => {
45
+ try {
46
+ const filename = decodeURIComponent(req.params.filename);
47
+ const sourceFilename =
48
+ typeof req.query.sourceFilename === "string"
49
+ ? req.query.sourceFilename
50
+ : decodeHeader(req.headers["x-ima2-canvas-source-filename"]);
51
+ const item = await updateCanvasVersion(ctx, filename, {
52
+ sourceFilename,
53
+ prompt: getPrompt(req),
54
+ buffer: getRequestBuffer(req),
55
+ });
56
+ res.json({ item });
57
+ } catch (err) {
58
+ res.status(err.status || 500).json({
59
+ error: err.message,
60
+ code: err.code || "CANVAS_VERSION_SAVE_FAILED",
61
+ });
62
+ }
63
+ });
64
+ }