@oscharko-dev/keiko-server 0.2.0

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 (509) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/assistant-response.d.ts +6 -0
  3. package/dist/assistant-response.d.ts.map +1 -0
  4. package/dist/assistant-response.js +12 -0
  5. package/dist/browser.d.ts +11 -0
  6. package/dist/browser.d.ts.map +1 -0
  7. package/dist/browser.js +245 -0
  8. package/dist/chat-handlers.d.ts +48 -0
  9. package/dist/chat-handlers.d.ts.map +1 -0
  10. package/dist/chat-handlers.js +821 -0
  11. package/dist/chat-stream-handlers.d.ts +4 -0
  12. package/dist/chat-stream-handlers.d.ts.map +1 -0
  13. package/dist/chat-stream-handlers.js +136 -0
  14. package/dist/conversation-prompt.d.ts +8 -0
  15. package/dist/conversation-prompt.d.ts.map +1 -0
  16. package/dist/conversation-prompt.js +36 -0
  17. package/dist/conversation-validation.d.ts +26 -0
  18. package/dist/conversation-validation.d.ts.map +1 -0
  19. package/dist/conversation-validation.js +125 -0
  20. package/dist/credentialPersistence.d.ts +23 -0
  21. package/dist/credentialPersistence.d.ts.map +1 -0
  22. package/dist/credentialPersistence.js +93 -0
  23. package/dist/credentialVault.d.ts +30 -0
  24. package/dist/credentialVault.d.ts.map +1 -0
  25. package/dist/credentialVault.js +206 -0
  26. package/dist/csp.d.ts +3 -0
  27. package/dist/csp.d.ts.map +1 -0
  28. package/dist/csp.js +75 -0
  29. package/dist/deps.d.ts +78 -0
  30. package/dist/deps.d.ts.map +1 -0
  31. package/dist/deps.js +457 -0
  32. package/dist/editor/agentRoutes.d.ts +7 -0
  33. package/dist/editor/agentRoutes.d.ts.map +1 -0
  34. package/dist/editor/agentRoutes.js +197 -0
  35. package/dist/editor/assuredGateRunner.d.ts +36 -0
  36. package/dist/editor/assuredGateRunner.d.ts.map +1 -0
  37. package/dist/editor/assuredGateRunner.js +100 -0
  38. package/dist/editor/assuredPreFilter.d.ts +34 -0
  39. package/dist/editor/assuredPreFilter.d.ts.map +1 -0
  40. package/dist/editor/assuredPreFilter.js +134 -0
  41. package/dist/editor/assuredPreFilterRunner.d.ts +31 -0
  42. package/dist/editor/assuredPreFilterRunner.d.ts.map +1 -0
  43. package/dist/editor/assuredPreFilterRunner.js +312 -0
  44. package/dist/editor/builtinLanguageProviders.d.ts +6 -0
  45. package/dist/editor/builtinLanguageProviders.d.ts.map +1 -0
  46. package/dist/editor/builtinLanguageProviders.js +221 -0
  47. package/dist/editor/codingContext.d.ts +12 -0
  48. package/dist/editor/codingContext.d.ts.map +1 -0
  49. package/dist/editor/codingContext.js +121 -0
  50. package/dist/editor/codingContextEvidence.d.ts +7 -0
  51. package/dist/editor/codingContextEvidence.d.ts.map +1 -0
  52. package/dist/editor/codingContextEvidence.js +52 -0
  53. package/dist/editor/codingContextProviders.d.ts +36 -0
  54. package/dist/editor/codingContextProviders.d.ts.map +1 -0
  55. package/dist/editor/codingContextProviders.js +348 -0
  56. package/dist/editor/completionModelEvidence.d.ts +16 -0
  57. package/dist/editor/completionModelEvidence.d.ts.map +1 -0
  58. package/dist/editor/completionModelEvidence.js +50 -0
  59. package/dist/editor/completionRoutes.d.ts +37 -0
  60. package/dist/editor/completionRoutes.d.ts.map +1 -0
  61. package/dist/editor/completionRoutes.js +411 -0
  62. package/dist/editor/contextRoutes.d.ts +6 -0
  63. package/dist/editor/contextRoutes.d.ts.map +1 -0
  64. package/dist/editor/contextRoutes.js +411 -0
  65. package/dist/editor/disposableAssuredExecution.d.ts +22 -0
  66. package/dist/editor/disposableAssuredExecution.d.ts.map +1 -0
  67. package/dist/editor/disposableAssuredExecution.js +57 -0
  68. package/dist/editor/editorCompletionModel.d.ts +47 -0
  69. package/dist/editor/editorCompletionModel.d.ts.map +1 -0
  70. package/dist/editor/editorCompletionModel.js +156 -0
  71. package/dist/editor/editorInlineCompletionModel.d.ts +34 -0
  72. package/dist/editor/editorInlineCompletionModel.d.ts.map +1 -0
  73. package/dist/editor/editorInlineCompletionModel.js +112 -0
  74. package/dist/editor/editorModelTokenBudget.d.ts +46 -0
  75. package/dist/editor/editorModelTokenBudget.d.ts.map +1 -0
  76. package/dist/editor/editorModelTokenBudget.js +121 -0
  77. package/dist/editor/inlineCompletionRateLimiter.d.ts +19 -0
  78. package/dist/editor/inlineCompletionRateLimiter.d.ts.map +1 -0
  79. package/dist/editor/inlineCompletionRateLimiter.js +46 -0
  80. package/dist/editor/inlineCompletionRoutes.d.ts +26 -0
  81. package/dist/editor/inlineCompletionRoutes.d.ts.map +1 -0
  82. package/dist/editor/inlineCompletionRoutes.js +404 -0
  83. package/dist/editor/inlineCompletionTelemetryEvidence.d.ts +5 -0
  84. package/dist/editor/inlineCompletionTelemetryEvidence.d.ts.map +1 -0
  85. package/dist/editor/inlineCompletionTelemetryEvidence.js +42 -0
  86. package/dist/editor/languageCancellation.d.ts +19 -0
  87. package/dist/editor/languageCancellation.d.ts.map +1 -0
  88. package/dist/editor/languageCancellation.js +48 -0
  89. package/dist/editor/languageProvider.d.ts +39 -0
  90. package/dist/editor/languageProvider.d.ts.map +1 -0
  91. package/dist/editor/languageProvider.js +11 -0
  92. package/dist/editor/languageRoutes.d.ts +15 -0
  93. package/dist/editor/languageRoutes.d.ts.map +1 -0
  94. package/dist/editor/languageRoutes.js +106 -0
  95. package/dist/editor/languageSanitize.d.ts +8 -0
  96. package/dist/editor/languageSanitize.d.ts.map +1 -0
  97. package/dist/editor/languageSanitize.js +101 -0
  98. package/dist/editor/languageService.d.ts +36 -0
  99. package/dist/editor/languageService.d.ts.map +1 -0
  100. package/dist/editor/languageService.js +93 -0
  101. package/dist/editor/languageServiceHost.d.ts +14 -0
  102. package/dist/editor/languageServiceHost.d.ts.map +1 -0
  103. package/dist/editor/languageServiceHost.js +242 -0
  104. package/dist/editor/localKnowledgeRetrieval.d.ts +21 -0
  105. package/dist/editor/localKnowledgeRetrieval.d.ts.map +1 -0
  106. package/dist/editor/localKnowledgeRetrieval.js +44 -0
  107. package/dist/editor/patchApplyEvidence.d.ts +21 -0
  108. package/dist/editor/patchApplyEvidence.d.ts.map +1 -0
  109. package/dist/editor/patchApplyEvidence.js +87 -0
  110. package/dist/editor/patchApplyRoutes.d.ts +16 -0
  111. package/dist/editor/patchApplyRoutes.d.ts.map +1 -0
  112. package/dist/editor/patchApplyRoutes.js +307 -0
  113. package/dist/editor/postApplyVerification.d.ts +42 -0
  114. package/dist/editor/postApplyVerification.d.ts.map +1 -0
  115. package/dist/editor/postApplyVerification.js +177 -0
  116. package/dist/editor/testGenerationEvidence.d.ts +6 -0
  117. package/dist/editor/testGenerationEvidence.d.ts.map +1 -0
  118. package/dist/editor/testGenerationEvidence.js +72 -0
  119. package/dist/editor/testGenerationPatch.d.ts +10 -0
  120. package/dist/editor/testGenerationPatch.d.ts.map +1 -0
  121. package/dist/editor/testGenerationPatch.js +66 -0
  122. package/dist/editor/testGenerationRoutes.d.ts +21 -0
  123. package/dist/editor/testGenerationRoutes.d.ts.map +1 -0
  124. package/dist/editor/testGenerationRoutes.js +254 -0
  125. package/dist/editor/testGenerationRunner.d.ts +23 -0
  126. package/dist/editor/testGenerationRunner.d.ts.map +1 -0
  127. package/dist/editor/testGenerationRunner.js +120 -0
  128. package/dist/editor/textOffsets.d.ts +6 -0
  129. package/dist/editor/textOffsets.d.ts.map +1 -0
  130. package/dist/editor/textOffsets.js +82 -0
  131. package/dist/editor/typescriptLanguageProvider.d.ts +3 -0
  132. package/dist/editor/typescriptLanguageProvider.d.ts.map +1 -0
  133. package/dist/editor/typescriptLanguageProvider.js +217 -0
  134. package/dist/evidence.d.ts +28 -0
  135. package/dist/evidence.d.ts.map +1 -0
  136. package/dist/evidence.js +145 -0
  137. package/dist/files-deny.d.ts +3 -0
  138. package/dist/files-deny.d.ts.map +1 -0
  139. package/dist/files-deny.js +12 -0
  140. package/dist/files.d.ts +97 -0
  141. package/dist/files.d.ts.map +1 -0
  142. package/dist/files.js +733 -0
  143. package/dist/gateway-setup.d.ts +10 -0
  144. package/dist/gateway-setup.d.ts.map +1 -0
  145. package/dist/gateway-setup.js +896 -0
  146. package/dist/governed-workflow.d.ts +17 -0
  147. package/dist/governed-workflow.d.ts.map +1 -0
  148. package/dist/governed-workflow.js +147 -0
  149. package/dist/grounded-answer.d.ts +12 -0
  150. package/dist/grounded-answer.d.ts.map +1 -0
  151. package/dist/grounded-answer.js +69 -0
  152. package/dist/grounded-context-index.d.ts +25 -0
  153. package/dist/grounded-context-index.d.ts.map +1 -0
  154. package/dist/grounded-context-index.js +169 -0
  155. package/dist/grounded-document-evidence.d.ts +28 -0
  156. package/dist/grounded-document-evidence.d.ts.map +1 -0
  157. package/dist/grounded-document-evidence.js +430 -0
  158. package/dist/grounded-handoff.d.ts +4 -0
  159. package/dist/grounded-handoff.d.ts.map +1 -0
  160. package/dist/grounded-handoff.js +445 -0
  161. package/dist/grounded-orchestrator.d.ts +43 -0
  162. package/dist/grounded-orchestrator.d.ts.map +1 -0
  163. package/dist/grounded-orchestrator.js +1445 -0
  164. package/dist/grounded-prompt.d.ts +2 -0
  165. package/dist/grounded-prompt.d.ts.map +1 -0
  166. package/dist/grounded-prompt.js +17 -0
  167. package/dist/grounded-qa-hybrid.d.ts +36 -0
  168. package/dist/grounded-qa-hybrid.d.ts.map +1 -0
  169. package/dist/grounded-qa-hybrid.js +762 -0
  170. package/dist/grounded-qa-multi-source.d.ts +38 -0
  171. package/dist/grounded-qa-multi-source.d.ts.map +1 -0
  172. package/dist/grounded-qa-multi-source.js +461 -0
  173. package/dist/grounded-qa.d.ts +45 -0
  174. package/dist/grounded-qa.d.ts.map +1 -0
  175. package/dist/grounded-qa.js +877 -0
  176. package/dist/grounded-rerank.d.ts +26 -0
  177. package/dist/grounded-rerank.d.ts.map +1 -0
  178. package/dist/grounded-rerank.js +72 -0
  179. package/dist/grounded-turn-registry.d.ts +23 -0
  180. package/dist/grounded-turn-registry.d.ts.map +1 -0
  181. package/dist/grounded-turn-registry.js +102 -0
  182. package/dist/headers.d.ts +3 -0
  183. package/dist/headers.d.ts.map +1 -0
  184. package/dist/headers.js +22 -0
  185. package/dist/host-check.d.ts +3 -0
  186. package/dist/host-check.d.ts.map +1 -0
  187. package/dist/host-check.js +58 -0
  188. package/dist/index.d.ts +26 -0
  189. package/dist/index.d.ts.map +1 -0
  190. package/dist/index.js +33 -0
  191. package/dist/load-csp.d.ts +3 -0
  192. package/dist/load-csp.d.ts.map +1 -0
  193. package/dist/load-csp.js +100 -0
  194. package/dist/local-knowledge-grounded-qa.d.ts +42 -0
  195. package/dist/local-knowledge-grounded-qa.d.ts.map +1 -0
  196. package/dist/local-knowledge-grounded-qa.js +678 -0
  197. package/dist/local-knowledge-handlers.d.ts +24 -0
  198. package/dist/local-knowledge-handlers.d.ts.map +1 -0
  199. package/dist/local-knowledge-handlers.js +1285 -0
  200. package/dist/local-knowledge-indexing-registry.d.ts +13 -0
  201. package/dist/local-knowledge-indexing-registry.d.ts.map +1 -0
  202. package/dist/local-knowledge-indexing-registry.js +53 -0
  203. package/dist/localKnowledgeKeyProvider.d.ts +11 -0
  204. package/dist/localKnowledgeKeyProvider.d.ts.map +1 -0
  205. package/dist/localKnowledgeKeyProvider.js +48 -0
  206. package/dist/memory-audit-event-builders.d.ts +21 -0
  207. package/dist/memory-audit-event-builders.d.ts.map +1 -0
  208. package/dist/memory-audit-event-builders.js +187 -0
  209. package/dist/memory-audit-handler.d.ts +23 -0
  210. package/dist/memory-audit-handler.d.ts.map +1 -0
  211. package/dist/memory-audit-handler.js +191 -0
  212. package/dist/memory-capture-policy.d.ts +10 -0
  213. package/dist/memory-capture-policy.d.ts.map +1 -0
  214. package/dist/memory-capture-policy.js +44 -0
  215. package/dist/memory-consolidation-handlers.d.ts +6 -0
  216. package/dist/memory-consolidation-handlers.d.ts.map +1 -0
  217. package/dist/memory-consolidation-handlers.js +491 -0
  218. package/dist/memory-consolidation-registry.d.ts +47 -0
  219. package/dist/memory-consolidation-registry.d.ts.map +1 -0
  220. package/dist/memory-consolidation-registry.js +106 -0
  221. package/dist/memory-conv-handlers.d.ts +8 -0
  222. package/dist/memory-conv-handlers.d.ts.map +1 -0
  223. package/dist/memory-conv-handlers.js +369 -0
  224. package/dist/memory-conversation-context.d.ts +13 -0
  225. package/dist/memory-conversation-context.d.ts.map +1 -0
  226. package/dist/memory-conversation-context.js +22 -0
  227. package/dist/memory-diagnostics.d.ts +29 -0
  228. package/dist/memory-diagnostics.d.ts.map +1 -0
  229. package/dist/memory-diagnostics.js +122 -0
  230. package/dist/memory-embedding.d.ts +21 -0
  231. package/dist/memory-embedding.d.ts.map +1 -0
  232. package/dist/memory-embedding.js +264 -0
  233. package/dist/memory-handlers.d.ts +19 -0
  234. package/dist/memory-handlers.d.ts.map +1 -0
  235. package/dist/memory-handlers.js +1204 -0
  236. package/dist/memory-maintenance-handlers.d.ts +35 -0
  237. package/dist/memory-maintenance-handlers.d.ts.map +1 -0
  238. package/dist/memory-maintenance-handlers.js +219 -0
  239. package/dist/memory-record-builders.d.ts +4 -0
  240. package/dist/memory-record-builders.d.ts.map +1 -0
  241. package/dist/memory-record-builders.js +19 -0
  242. package/dist/memory-retention.d.ts +31 -0
  243. package/dist/memory-retention.d.ts.map +1 -0
  244. package/dist/memory-retention.js +151 -0
  245. package/dist/memory-retrieval-signals.d.ts +12 -0
  246. package/dist/memory-retrieval-signals.d.ts.map +1 -0
  247. package/dist/memory-retrieval-signals.js +100 -0
  248. package/dist/memory-salience.d.ts +12 -0
  249. package/dist/memory-salience.d.ts.map +1 -0
  250. package/dist/memory-salience.js +154 -0
  251. package/dist/memory-scope-sanitizer.d.ts +6 -0
  252. package/dist/memory-scope-sanitizer.d.ts.map +1 -0
  253. package/dist/memory-scope-sanitizer.js +106 -0
  254. package/dist/memory-target-resolver.d.ts +4 -0
  255. package/dist/memory-target-resolver.d.ts.map +1 -0
  256. package/dist/memory-target-resolver.js +73 -0
  257. package/dist/memory-workflow-port.d.ts +14 -0
  258. package/dist/memory-workflow-port.d.ts.map +1 -0
  259. package/dist/memory-workflow-port.js +186 -0
  260. package/dist/private-json.d.ts +3 -0
  261. package/dist/private-json.d.ts.map +1 -0
  262. package/dist/private-json.js +62 -0
  263. package/dist/promptEnhancer/index.d.ts +3 -0
  264. package/dist/promptEnhancer/index.d.ts.map +1 -0
  265. package/dist/promptEnhancer/index.js +5 -0
  266. package/dist/promptEnhancer/orchestrate.d.ts +2 -0
  267. package/dist/promptEnhancer/orchestrate.d.ts.map +1 -0
  268. package/dist/promptEnhancer/orchestrate.js +5 -0
  269. package/dist/promptEnhancer/routes.d.ts +9 -0
  270. package/dist/promptEnhancer/routes.d.ts.map +1 -0
  271. package/dist/promptEnhancer/routes.js +205 -0
  272. package/dist/qualityIntelligence/capsuleAdapter.d.ts +27 -0
  273. package/dist/qualityIntelligence/capsuleAdapter.d.ts.map +1 -0
  274. package/dist/qualityIntelligence/capsuleAdapter.js +57 -0
  275. package/dist/qualityIntelligence/connectorAuthorization.d.ts +22 -0
  276. package/dist/qualityIntelligence/connectorAuthorization.d.ts.map +1 -0
  277. package/dist/qualityIntelligence/connectorAuthorization.js +35 -0
  278. package/dist/qualityIntelligence/connectorErrors.d.ts +16 -0
  279. package/dist/qualityIntelligence/connectorErrors.d.ts.map +1 -0
  280. package/dist/qualityIntelligence/connectorErrors.js +56 -0
  281. package/dist/qualityIntelligence/connectorRoutes.d.ts +7 -0
  282. package/dist/qualityIntelligence/connectorRoutes.d.ts.map +1 -0
  283. package/dist/qualityIntelligence/connectorRoutes.js +167 -0
  284. package/dist/qualityIntelligence/editRoutes.d.ts +5 -0
  285. package/dist/qualityIntelligence/editRoutes.d.ts.map +1 -0
  286. package/dist/qualityIntelligence/editRoutes.js +293 -0
  287. package/dist/qualityIntelligence/exportAssembly.d.ts +22 -0
  288. package/dist/qualityIntelligence/exportAssembly.d.ts.map +1 -0
  289. package/dist/qualityIntelligence/exportAssembly.js +352 -0
  290. package/dist/qualityIntelligence/exportRoutes.d.ts +5 -0
  291. package/dist/qualityIntelligence/exportRoutes.d.ts.map +1 -0
  292. package/dist/qualityIntelligence/exportRoutes.js +320 -0
  293. package/dist/qualityIntelligence/figma/figmaConcurrency.d.ts +8 -0
  294. package/dist/qualityIntelligence/figma/figmaConcurrency.d.ts.map +1 -0
  295. package/dist/qualityIntelligence/figma/figmaConcurrency.js +34 -0
  296. package/dist/qualityIntelligence/figma/figmaConnector.d.ts +65 -0
  297. package/dist/qualityIntelligence/figma/figmaConnector.d.ts.map +1 -0
  298. package/dist/qualityIntelligence/figma/figmaConnector.js +184 -0
  299. package/dist/qualityIntelligence/figma/figmaConnectorAudit.d.ts +52 -0
  300. package/dist/qualityIntelligence/figma/figmaConnectorAudit.d.ts.map +1 -0
  301. package/dist/qualityIntelligence/figma/figmaConnectorAudit.js +63 -0
  302. package/dist/qualityIntelligence/figma/figmaConnectorErrors.d.ts +31 -0
  303. package/dist/qualityIntelligence/figma/figmaConnectorErrors.d.ts.map +1 -0
  304. package/dist/qualityIntelligence/figma/figmaConnectorErrors.js +220 -0
  305. package/dist/qualityIntelligence/figma/figmaConnectorMetrics.d.ts +44 -0
  306. package/dist/qualityIntelligence/figma/figmaConnectorMetrics.d.ts.map +1 -0
  307. package/dist/qualityIntelligence/figma/figmaConnectorMetrics.js +49 -0
  308. package/dist/qualityIntelligence/figma/figmaConsent.d.ts +39 -0
  309. package/dist/qualityIntelligence/figma/figmaConsent.d.ts.map +1 -0
  310. package/dist/qualityIntelligence/figma/figmaConsent.js +62 -0
  311. package/dist/qualityIntelligence/figma/figmaHttpPort.d.ts +28 -0
  312. package/dist/qualityIntelligence/figma/figmaHttpPort.d.ts.map +1 -0
  313. package/dist/qualityIntelligence/figma/figmaHttpPort.js +70 -0
  314. package/dist/qualityIntelligence/figma/figmaObservedActions.d.ts +49 -0
  315. package/dist/qualityIntelligence/figma/figmaObservedActions.d.ts.map +1 -0
  316. package/dist/qualityIntelligence/figma/figmaObservedActions.js +89 -0
  317. package/dist/qualityIntelligence/figma/figmaReadiness.d.ts +32 -0
  318. package/dist/qualityIntelligence/figma/figmaReadiness.d.ts.map +1 -0
  319. package/dist/qualityIntelligence/figma/figmaReadiness.js +67 -0
  320. package/dist/qualityIntelligence/figma/figmaRenderPort.d.ts +29 -0
  321. package/dist/qualityIntelligence/figma/figmaRenderPort.d.ts.map +1 -0
  322. package/dist/qualityIntelligence/figma/figmaRenderPort.js +93 -0
  323. package/dist/qualityIntelligence/figma/figmaResnapshot.d.ts +28 -0
  324. package/dist/qualityIntelligence/figma/figmaResnapshot.d.ts.map +1 -0
  325. package/dist/qualityIntelligence/figma/figmaResnapshot.js +38 -0
  326. package/dist/qualityIntelligence/figma/figmaRetry.d.ts +31 -0
  327. package/dist/qualityIntelligence/figma/figmaRetry.d.ts.map +1 -0
  328. package/dist/qualityIntelligence/figma/figmaRetry.js +62 -0
  329. package/dist/qualityIntelligence/figma/figmaScopeRef.d.ts +9 -0
  330. package/dist/qualityIntelligence/figma/figmaScopeRef.d.ts.map +1 -0
  331. package/dist/qualityIntelligence/figma/figmaScopeRef.js +18 -0
  332. package/dist/qualityIntelligence/figma/figmaScopedPagination.d.ts +86 -0
  333. package/dist/qualityIntelligence/figma/figmaScopedPagination.d.ts.map +1 -0
  334. package/dist/qualityIntelligence/figma/figmaScopedPagination.js +308 -0
  335. package/dist/qualityIntelligence/figma/figmaSnapshotBuilder.d.ts +31 -0
  336. package/dist/qualityIntelligence/figma/figmaSnapshotBuilder.d.ts.map +1 -0
  337. package/dist/qualityIntelligence/figma/figmaSnapshotBuilder.js +314 -0
  338. package/dist/qualityIntelligence/figma/figmaSnapshotHash.d.ts +18 -0
  339. package/dist/qualityIntelligence/figma/figmaSnapshotHash.d.ts.map +1 -0
  340. package/dist/qualityIntelligence/figma/figmaSnapshotHash.js +63 -0
  341. package/dist/qualityIntelligence/figma/figmaSnapshotTypes.d.ts +65 -0
  342. package/dist/qualityIntelligence/figma/figmaSnapshotTypes.d.ts.map +1 -0
  343. package/dist/qualityIntelligence/figma/figmaSnapshotTypes.js +13 -0
  344. package/dist/qualityIntelligence/figma/figmaTokenSource.d.ts +9 -0
  345. package/dist/qualityIntelligence/figma/figmaTokenSource.d.ts.map +1 -0
  346. package/dist/qualityIntelligence/figma/figmaTokenSource.js +61 -0
  347. package/dist/qualityIntelligence/figma/figmaTokenStore.d.ts +19 -0
  348. package/dist/qualityIntelligence/figma/figmaTokenStore.d.ts.map +1 -0
  349. package/dist/qualityIntelligence/figma/figmaTokenStore.js +156 -0
  350. package/dist/qualityIntelligence/figma/figmaUrl.d.ts +6 -0
  351. package/dist/qualityIntelligence/figma/figmaUrl.d.ts.map +1 -0
  352. package/dist/qualityIntelligence/figma/figmaUrl.js +36 -0
  353. package/dist/qualityIntelligence/figma/index.d.ts +20 -0
  354. package/dist/qualityIntelligence/figma/index.d.ts.map +1 -0
  355. package/dist/qualityIntelligence/figma/index.js +26 -0
  356. package/dist/qualityIntelligence/figmaCodegenRoutes.d.ts +28 -0
  357. package/dist/qualityIntelligence/figmaCodegenRoutes.d.ts.map +1 -0
  358. package/dist/qualityIntelligence/figmaCodegenRoutes.js +165 -0
  359. package/dist/qualityIntelligence/figmaSnapshotAdapter.d.ts +55 -0
  360. package/dist/qualityIntelligence/figmaSnapshotAdapter.d.ts.map +1 -0
  361. package/dist/qualityIntelligence/figmaSnapshotAdapter.js +219 -0
  362. package/dist/qualityIntelligence/figmaSnapshotOrchestration.d.ts +64 -0
  363. package/dist/qualityIntelligence/figmaSnapshotOrchestration.d.ts.map +1 -0
  364. package/dist/qualityIntelligence/figmaSnapshotOrchestration.js +203 -0
  365. package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts +112 -0
  366. package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts.map +1 -0
  367. package/dist/qualityIntelligence/figmaSnapshotRoutes.js +1063 -0
  368. package/dist/qualityIntelligence/figmaSnapshotScreenIds.d.ts +19 -0
  369. package/dist/qualityIntelligence/figmaSnapshotScreenIds.d.ts.map +1 -0
  370. package/dist/qualityIntelligence/figmaSnapshotScreenIds.js +75 -0
  371. package/dist/qualityIntelligence/generationPort.d.ts +15 -0
  372. package/dist/qualityIntelligence/generationPort.d.ts.map +1 -0
  373. package/dist/qualityIntelligence/generationPort.js +185 -0
  374. package/dist/qualityIntelligence/handoffErrors.d.ts +9 -0
  375. package/dist/qualityIntelligence/handoffErrors.d.ts.map +1 -0
  376. package/dist/qualityIntelligence/handoffErrors.js +21 -0
  377. package/dist/qualityIntelligence/handoffRoutes.d.ts +15 -0
  378. package/dist/qualityIntelligence/handoffRoutes.d.ts.map +1 -0
  379. package/dist/qualityIntelligence/handoffRoutes.js +341 -0
  380. package/dist/qualityIntelligence/index.d.ts +17 -0
  381. package/dist/qualityIntelligence/index.d.ts.map +1 -0
  382. package/dist/qualityIntelligence/index.js +36 -0
  383. package/dist/qualityIntelligence/judgePort.d.ts +30 -0
  384. package/dist/qualityIntelligence/judgePort.d.ts.map +1 -0
  385. package/dist/qualityIntelligence/judgePort.js +326 -0
  386. package/dist/qualityIntelligence/modelSelection.d.ts +58 -0
  387. package/dist/qualityIntelligence/modelSelection.d.ts.map +1 -0
  388. package/dist/qualityIntelligence/modelSelection.js +148 -0
  389. package/dist/qualityIntelligence/reCheckRoutes.d.ts +6 -0
  390. package/dist/qualityIntelligence/reCheckRoutes.d.ts.map +1 -0
  391. package/dist/qualityIntelligence/reCheckRoutes.js +1157 -0
  392. package/dist/qualityIntelligence/retentionEnforcement.d.ts +13 -0
  393. package/dist/qualityIntelligence/retentionEnforcement.d.ts.map +1 -0
  394. package/dist/qualityIntelligence/retentionEnforcement.js +47 -0
  395. package/dist/qualityIntelligence/retentionRoutes.d.ts +8 -0
  396. package/dist/qualityIntelligence/retentionRoutes.d.ts.map +1 -0
  397. package/dist/qualityIntelligence/retentionRoutes.js +74 -0
  398. package/dist/qualityIntelligence/reviewRoutes.d.ts +5 -0
  399. package/dist/qualityIntelligence/reviewRoutes.d.ts.map +1 -0
  400. package/dist/qualityIntelligence/reviewRoutes.js +145 -0
  401. package/dist/qualityIntelligence/reviewStore.d.ts +75 -0
  402. package/dist/qualityIntelligence/reviewStore.d.ts.map +1 -0
  403. package/dist/qualityIntelligence/reviewStore.js +170 -0
  404. package/dist/qualityIntelligence/runExecution.d.ts +36 -0
  405. package/dist/qualityIntelligence/runExecution.d.ts.map +1 -0
  406. package/dist/qualityIntelligence/runExecution.js +180 -0
  407. package/dist/qualityIntelligence/runIngestion.d.ts +70 -0
  408. package/dist/qualityIntelligence/runIngestion.d.ts.map +1 -0
  409. package/dist/qualityIntelligence/runIngestion.js +1235 -0
  410. package/dist/qualityIntelligence/runRegistry.d.ts +31 -0
  411. package/dist/qualityIntelligence/runRegistry.d.ts.map +1 -0
  412. package/dist/qualityIntelligence/runRegistry.js +66 -0
  413. package/dist/qualityIntelligence/runRoutes.d.ts +16 -0
  414. package/dist/qualityIntelligence/runRoutes.d.ts.map +1 -0
  415. package/dist/qualityIntelligence/runRoutes.js +357 -0
  416. package/dist/qualityIntelligence/traceabilityRoutes.d.ts +5 -0
  417. package/dist/qualityIntelligence/traceabilityRoutes.d.ts.map +1 -0
  418. package/dist/qualityIntelligence/traceabilityRoutes.js +173 -0
  419. package/dist/qualityIntelligence/uiRoutes.d.ts +7 -0
  420. package/dist/qualityIntelligence/uiRoutes.d.ts.map +1 -0
  421. package/dist/qualityIntelligence/uiRoutes.js +336 -0
  422. package/dist/read-handlers.d.ts +9 -0
  423. package/dist/read-handlers.d.ts.map +1 -0
  424. package/dist/read-handlers.js +265 -0
  425. package/dist/relationship-handlers.d.ts +191 -0
  426. package/dist/relationship-handlers.d.ts.map +1 -0
  427. package/dist/relationship-handlers.js +0 -0
  428. package/dist/routes.d.ts +37 -0
  429. package/dist/routes.d.ts.map +1 -0
  430. package/dist/routes.js +507 -0
  431. package/dist/run-engine.d.ts +25 -0
  432. package/dist/run-engine.d.ts.map +1 -0
  433. package/dist/run-engine.js +385 -0
  434. package/dist/run-handlers.d.ts +9 -0
  435. package/dist/run-handlers.d.ts.map +1 -0
  436. package/dist/run-handlers.js +465 -0
  437. package/dist/run-request.d.ts +17 -0
  438. package/dist/run-request.d.ts.map +1 -0
  439. package/dist/run-request.js +219 -0
  440. package/dist/runs.d.ts +47 -0
  441. package/dist/runs.d.ts.map +1 -0
  442. package/dist/runs.js +100 -0
  443. package/dist/server.d.ts +13 -0
  444. package/dist/server.d.ts.map +1 -0
  445. package/dist/server.js +152 -0
  446. package/dist/sink.d.ts +28 -0
  447. package/dist/sink.d.ts.map +1 -0
  448. package/dist/sink.js +80 -0
  449. package/dist/sse-write.d.ts +9 -0
  450. package/dist/sse-write.d.ts.map +1 -0
  451. package/dist/sse-write.js +26 -0
  452. package/dist/sse.d.ts +8 -0
  453. package/dist/sse.d.ts.map +1 -0
  454. package/dist/sse.js +27 -0
  455. package/dist/static.d.ts +5 -0
  456. package/dist/static.d.ts.map +1 -0
  457. package/dist/static.js +76 -0
  458. package/dist/store/chats.d.ts +17 -0
  459. package/dist/store/chats.d.ts.map +1 -0
  460. package/dist/store/chats.js +624 -0
  461. package/dist/store/db.d.ts +11 -0
  462. package/dist/store/db.d.ts.map +1 -0
  463. package/dist/store/db.js +203 -0
  464. package/dist/store/errors.d.ts +13 -0
  465. package/dist/store/errors.d.ts.map +1 -0
  466. package/dist/store/errors.js +30 -0
  467. package/dist/store/index.d.ts +7 -0
  468. package/dist/store/index.d.ts.map +1 -0
  469. package/dist/store/index.js +6 -0
  470. package/dist/store/messages.d.ts +8 -0
  471. package/dist/store/messages.d.ts.map +1 -0
  472. package/dist/store/messages.js +149 -0
  473. package/dist/store/paths.d.ts +5 -0
  474. package/dist/store/paths.d.ts.map +1 -0
  475. package/dist/store/paths.js +84 -0
  476. package/dist/store/projects.d.ts +8 -0
  477. package/dist/store/projects.d.ts.map +1 -0
  478. package/dist/store/projects.js +59 -0
  479. package/dist/store/relationship-audit.d.ts +42 -0
  480. package/dist/store/relationship-audit.d.ts.map +1 -0
  481. package/dist/store/relationship-audit.js +155 -0
  482. package/dist/store/relationships.d.ts +191 -0
  483. package/dist/store/relationships.d.ts.map +1 -0
  484. package/dist/store/relationships.js +724 -0
  485. package/dist/store/schema.d.ts +4 -0
  486. package/dist/store/schema.d.ts.map +1 -0
  487. package/dist/store/schema.js +220 -0
  488. package/dist/store/types.d.ts +29 -0
  489. package/dist/store/types.d.ts.map +1 -0
  490. package/dist/store/types.js +8 -0
  491. package/dist/store/validation.d.ts +7 -0
  492. package/dist/store/validation.d.ts.map +1 -0
  493. package/dist/store/validation.js +117 -0
  494. package/dist/store-handlers.d.ts +17 -0
  495. package/dist/store-handlers.d.ts.map +1 -0
  496. package/dist/store-handlers.js +872 -0
  497. package/dist/terminal-errors.d.ts +22 -0
  498. package/dist/terminal-errors.d.ts.map +1 -0
  499. package/dist/terminal-errors.js +45 -0
  500. package/dist/terminal-evidence.d.ts +21 -0
  501. package/dist/terminal-evidence.d.ts.map +1 -0
  502. package/dist/terminal-evidence.js +65 -0
  503. package/dist/terminal-routes.d.ts +10 -0
  504. package/dist/terminal-routes.d.ts.map +1 -0
  505. package/dist/terminal-routes.js +219 -0
  506. package/dist/terminal.d.ts +68 -0
  507. package/dist/terminal.d.ts.map +1 -0
  508. package/dist/terminal.js +855 -0
  509. package/package.json +52 -0
@@ -0,0 +1,1063 @@
1
+ // Figma Snapshot BFF routes (Epic #750, Issue #756).
2
+ //
3
+ // Two thin UI-facing routes that sit between the browser surface and the server-side
4
+ // Figma connector + snapshot store. The PAT stays ENTIRELY server-side — nothing in
5
+ // the request or response carries the token.
6
+ //
7
+ // POST /api/figma/snapshots — trigger a bounded snapshot-build from a board link
8
+ // GET /api/figma/snapshots/:runId — load a stored snapshot summary for display
9
+ // GET /api/figma/snapshots/:runId/screens/:screenIndex/image
10
+ // — stream a stored, token-free PNG side-file
11
+ //
12
+ // Trigger route:
13
+ // 1. Parses board link → (fileKey, nodeId) — rejects malformed / missing node-id links.
14
+ // 2. Resolves the read-only PAT server-side (vault > config > FIGMA_ACCESS_TOKEN env).
15
+ // 3. Builds a runId, runs connector → cleanScopedNodesToScreenIr → buildFigmaSnapshot → store.
16
+ // 4. Returns a minimal summary (runId, screenCount, skippedCount, reduction hint).
17
+ // No token, no raw IR bytes, no render bytes are embedded in the JSON response.
18
+ //
19
+ // Load route reads the stored immutable evidence record and returns a browser-safe
20
+ // projection. No re-contact with Figma.
21
+ //
22
+ // Both routes honour the existing QI error-envelope convention:
23
+ // { error: { code: string; message: string } }
24
+ //
25
+ // In-flight coalescing (item 2):
26
+ // Concurrent POSTs for the same scope (fileKey:nodeId) share ONE build promise. The FIRST
27
+ // caller starts the governed build, mints the runId ONCE, persists ONCE (inside the build
28
+ // chain so a deadline-expired-but-completed build still persists and is recoverable via an
29
+ // immediate retry POST). Subsequent callers await the same promise and receive the same
30
+ // runId/response. The entry is removed on settle (finally). Failures propagate to all
31
+ // waiters with the same coded error.
32
+ //
33
+ // Build deadline (item 3):
34
+ // KEIKO_FIGMA_BUILD_DEADLINE_MS (default 600 000 ms). The awaiting handler races the
35
+ // coalesced build promise against a deadline rejection. On deadline the handler responds
36
+ // 504 FIGMA_BUILD_TIMEOUT WITHOUT a runId. The coalesced build itself continues (other
37
+ // waiters may exist); persist is INSIDE the build chain, so if/when the build completes
38
+ // a subsequent POST for the same scope finds the map empty and re-coalesces cheaply.
39
+ // The underlying build is bounded by per-fetch timeouts and pagination budgets.
40
+ //
41
+ // Client disconnect (item 4):
42
+ // The handler listens for the 'close' event on ctx.req. On disconnect the per-waiter
43
+ // race resolves promptly (504 FIGMA_BUILD_TIMEOUT body never sent over the wire).
44
+ // The coalesced build continues — other waiters (if any) are unaffected and will
45
+ // receive the result when the build settles.
46
+ import { randomUUID } from "node:crypto";
47
+ import { lstatSync, readdirSync, readFileSync } from "node:fs";
48
+ import { join } from "node:path";
49
+ import { STREAMING } from "../routes.js";
50
+ import { currentGatewayConfig, currentGatewayEgressConfig, currentRedactionSecrets, } from "../deps.js";
51
+ import { redact } from "@oscharko-dev/keiko-security";
52
+ import { appendFigmaConnectorAudit, parseFigmaTarget, deriveFigmaScopeRef, observeFigmaRevoke, EXPECTED_FIGMA_SCOPES, FigmaConnectorError, resolveScopedPaginationLimits, } from "./figma/index.js";
53
+ import { governedSnapshotBuild, figmaTokenStoreFor, } from "./figmaSnapshotOrchestration.js";
54
+ import { createNodeFigmaSnapshotStore, } from "@oscharko-dev/keiko-evidence";
55
+ // ─── Error helpers ─────────────────────────────────────────────────────────────
56
+ // Operator-facing scope hint, derived from the single source of truth (figmaConsent.EXPECTED_FIGMA_SCOPES)
57
+ // so the re-key guidance can never drift from the scopes the consent ledger records (#758 AC2).
58
+ const READ_ONLY_SCOPE_HINT = EXPECTED_FIGMA_SCOPES.join(", ");
59
+ const FIGMA_ROUTE_ERROR_MESSAGES = {
60
+ FIGMA_TOKEN_MISSING: `No Figma PAT is configured. Add one in Keiko config, vault, or FIGMA_ACCESS_TOKEN (read-only scopes: ${READ_ONLY_SCOPE_HINT}).`,
61
+ FIGMA_TOKEN_INVALID: "The configured Figma PAT is invalid. Please rotate the token.",
62
+ FIGMA_TOKEN_EXPIRED: "The configured Figma PAT has expired. Please rotate the token.",
63
+ FIGMA_TOKEN_REVOKED: "The configured Figma PAT has been revoked. Please mint a new token.",
64
+ FIGMA_INSUFFICIENT_SCOPE: `The configured Figma PAT lacks the required read-only scopes (${READ_ONLY_SCOPE_HINT}).`,
65
+ FIGMA_NOT_FOUND: "The Figma board was not found. Check the link and that the PAT can access this file.",
66
+ FIGMA_NOT_READY: "The selected Figma scope is not release-ready. Pin a Figma version, select a Release section, or mark the frame Ready for dev before snapshotting.",
67
+ FIGMA_UPSTREAM_UNAVAILABLE: "Figma API is temporarily unavailable. Please try again.",
68
+ FIGMA_PROXY_EGRESS_FAILED: "The forward proxy rejected the Figma egress request. Check proxy configuration.",
69
+ FIGMA_PROXY_UNREACHABLE: "The configured forward proxy is unreachable. Check proxy host and port settings.",
70
+ FIGMA_PROXY_AUTH_REQUIRED: "The forward proxy requires authentication. Configure proxy credentials or an allow rule.",
71
+ FIGMA_PROXY_BLOCKED_BY_POLICY: "The forward proxy blocked the Figma egress request. Ask the proxy operator to allow api.figma.com.",
72
+ FIGMA_TLS_CA_FAILURE: "The Figma egress TLS certificate could not be verified. Check the configured CA bundle.",
73
+ FIGMA_RATE_LIMITED: "Figma rate-limited the snapshot-build. Retry with a narrower Release section or lower the Figma fetch limits before re-running.",
74
+ FIGMA_OVERSIZED_SCOPE: "The selected Figma board section is too large. Select a smaller section (frame or page).",
75
+ FIGMA_RESPONSE_TOO_LARGE: "The Figma API response exceeded the size limit. Select a smaller section.",
76
+ FIGMA_NETWORK_UNREACHABLE: "The outbound request to Figma failed. Check network connectivity and egress policy.",
77
+ FIGMA_EGRESS_TIMEOUT: "The Figma request timed out. Retry or raise KEIKO_FIGMA_REQUEST_TIMEOUT_MS.",
78
+ FIGMA_EGRESS_FAILED: "The outbound request to Figma failed before a response was received.",
79
+ FIGMA_BUILD_TIMEOUT: "The snapshot build exceeded the configured deadline. No partial snapshot was stored.",
80
+ FIGMA_INTERNAL: "An unexpected error occurred during the snapshot-build.",
81
+ FIGMA_BAD_LINK: "The board link is not a valid Figma URL, or it is missing a node-id " +
82
+ "(section/frame anchor required).",
83
+ FIGMA_BAD_METADATA: "The snapshot metadata update is invalid.",
84
+ FIGMA_SNAPSHOT_NOT_FOUND: "No snapshot was found for this run id.",
85
+ FIGMA_SCREEN_NOT_FOUND: "No captured screen image was found for this snapshot.",
86
+ FIGMA_NO_EVIDENCE_DIR: "The evidence directory is not configured; snapshots cannot be stored.",
87
+ FIGMA_CONSENT_REQUIRED: "Acknowledge the read-only, least-privilege Figma scope before the first snapshot for this board.",
88
+ FIGMA_TOKEN_REVOKED_OK: "The stored Figma PAT was removed.",
89
+ };
90
+ function figmaErrorBody(code) {
91
+ const base = {
92
+ error: { code, message: FIGMA_ROUTE_ERROR_MESSAGES[code] ?? "An error occurred." },
93
+ };
94
+ // The consent-required response carries the display-only least-privilege scopes (#760) so the UI
95
+ // can show exactly what a read-only token covers before the operator acknowledges. No token, no
96
+ // board reference — only the static scope strings.
97
+ return code === "FIGMA_CONSENT_REQUIRED" ? { ...base, scopes: EXPECTED_FIGMA_SCOPES } : base;
98
+ }
99
+ // Codes that map to 502 (upstream/auth/egress problems, not client errors).
100
+ const FIGMA_502_CODES = new Set([
101
+ "FIGMA_TOKEN_MISSING",
102
+ "FIGMA_TOKEN_INVALID",
103
+ "FIGMA_TOKEN_EXPIRED",
104
+ "FIGMA_TOKEN_REVOKED",
105
+ "FIGMA_INSUFFICIENT_SCOPE",
106
+ "FIGMA_PROXY_EGRESS_FAILED",
107
+ "FIGMA_PROXY_UNREACHABLE",
108
+ "FIGMA_PROXY_AUTH_REQUIRED",
109
+ "FIGMA_PROXY_BLOCKED_BY_POLICY",
110
+ "FIGMA_TLS_CA_FAILURE",
111
+ "FIGMA_UPSTREAM_UNAVAILABLE",
112
+ "FIGMA_NETWORK_UNREACHABLE",
113
+ "FIGMA_EGRESS_TIMEOUT",
114
+ "FIGMA_EGRESS_FAILED",
115
+ "FIGMA_RESPONSE_TOO_LARGE",
116
+ ]);
117
+ function figmaStatusForCode(code) {
118
+ if (FIGMA_502_CODES.has(code))
119
+ return 502;
120
+ if (code === "FIGMA_NOT_FOUND")
121
+ return 404;
122
+ if (code === "FIGMA_NOT_READY")
123
+ return 412;
124
+ if (code === "FIGMA_RATE_LIMITED")
125
+ return 429;
126
+ if (code === "FIGMA_OVERSIZED_SCOPE")
127
+ return 422;
128
+ // Precondition Required: the operator must acknowledge the read-only scope before the build (#760).
129
+ if (code === "FIGMA_CONSENT_REQUIRED")
130
+ return 428;
131
+ if (code === "FIGMA_BUILD_TIMEOUT")
132
+ return 504;
133
+ return 500;
134
+ }
135
+ // ─── Body reader ───────────────────────────────────────────────────────────────
136
+ const MAX_BODY_BYTES = 8 * 1024;
137
+ function readBody(req) {
138
+ return new Promise((resolve, reject) => {
139
+ const chunks = [];
140
+ let total = 0;
141
+ let capped = false;
142
+ req.on("data", (chunk) => {
143
+ total += chunk.length;
144
+ if (total > MAX_BODY_BYTES) {
145
+ if (!capped) {
146
+ capped = true;
147
+ reject(new Error("body_too_large"));
148
+ req.resume();
149
+ }
150
+ return;
151
+ }
152
+ chunks.push(chunk);
153
+ });
154
+ req.on("end", () => {
155
+ if (!capped)
156
+ resolve(Buffer.concat(chunks).toString("utf8"));
157
+ });
158
+ req.on("error", reject);
159
+ });
160
+ }
161
+ function buildReductionHint(screenCount, skippedCount, structuralOnlyCount = 0) {
162
+ const total = screenCount + skippedCount;
163
+ if (skippedCount === 0) {
164
+ return `${screenCount.toString()} screen${screenCount !== 1 ? "s" : ""} from ${total.toString()} detected`;
165
+ }
166
+ const structuralClause = structuralOnlyCount > 0
167
+ ? `${structuralOnlyCount.toString()} structural-only`
168
+ : `${skippedCount.toString()} render${skippedCount !== 1 ? "s" : ""} skipped`;
169
+ const missingIrCount = Math.max(0, skippedCount - structuralOnlyCount);
170
+ const missingClause = structuralOnlyCount > 0 && missingIrCount > 0
171
+ ? `; ${missingIrCount.toString()} without structural IR`
172
+ : "";
173
+ return `${screenCount.toString()} rendered screen${screenCount !== 1 ? "s" : ""} from ${total.toString()} detected (${structuralClause}${missingClause})`;
174
+ }
175
+ // Counts interaction-hint roles over a stored ScreenIr node tree (`{ root: { interactionHint,
176
+ // children } }`). Duck-typed: it does NOT import the IR domain, only walks the serialised shape the
177
+ // snapshot persists. Bounded by the tree size already capped at fetch time.
178
+ function countRoles(irJson) {
179
+ const counts = { fields: 0, controls: 0, texts: 0 };
180
+ const visit = (node) => {
181
+ if (typeof node !== "object" || node === null)
182
+ return;
183
+ const n = node;
184
+ const hint = typeof n.interactionHint === "string" ? n.interactionHint : "";
185
+ if (hint === "input")
186
+ counts.fields += 1;
187
+ else if (hint === "button" || hint === "link")
188
+ counts.controls += 1;
189
+ else if (hint === "text")
190
+ counts.texts += 1;
191
+ if (Array.isArray(n.children))
192
+ for (const child of n.children)
193
+ visit(child);
194
+ };
195
+ const ir = typeof irJson === "object" && irJson !== null ? irJson : {};
196
+ visit(ir.root);
197
+ return counts;
198
+ }
199
+ // Produces a brief structural summary string from a ScreenIr value (duck-typed — keeps this
200
+ // module honest: it does NOT import the IR domain or depend on its internal shape). Walks the IR
201
+ // node tree (`root`) to count fields/controls/text, which is where the structure actually lives —
202
+ // the previous flat `ir.fields`/`ir.controls` lookup never matched the persisted shape and always
203
+ // returned the bare "screen" fallback.
204
+ function irSummaryFromJson(irJson) {
205
+ const { fields, controls, texts } = countRoles(irJson);
206
+ const parts = [];
207
+ if (fields > 0)
208
+ parts.push(`${fields.toString()} field${fields !== 1 ? "s" : ""}`);
209
+ if (controls > 0)
210
+ parts.push(`${controls.toString()} control${controls !== 1 ? "s" : ""}`);
211
+ if (texts > 0)
212
+ parts.push(`${texts.toString()} text${texts !== 1 ? "s" : ""}`);
213
+ return parts.length > 0 ? parts.join(", ") : "screen";
214
+ }
215
+ function screenNameFromIrJson(irJson) {
216
+ if (typeof irJson !== "object" || irJson === null)
217
+ return "Screen";
218
+ const ir = irJson;
219
+ const name = ir.name;
220
+ return typeof name === "string" && name.length > 0 ? name : "Screen";
221
+ }
222
+ function managementSummaryFromMetadata(metadata) {
223
+ return {
224
+ ...(metadata?.displayName !== undefined ? { displayName: metadata.displayName } : {}),
225
+ ...(metadata?.updatedAt !== undefined ? { updatedAt: metadata.updatedAt } : {}),
226
+ };
227
+ }
228
+ function recordToSummary(record, coverage, metrics, metadata) {
229
+ const screenCount = record.screens.length;
230
+ const skippedCount = record.skippedScreens.length;
231
+ const structuralOnlyCount = record.structuralScreens?.length ?? 0;
232
+ const management = managementSummaryFromMetadata(metadata);
233
+ const truncatedClause = coverage !== undefined && (coverage.screensTruncated > 0 || coverage.capped)
234
+ ? `; ${coverage.screensTruncated.toString()} partially captured (deep content bounded)`
235
+ : "";
236
+ return {
237
+ runId: record.runId,
238
+ ...(management.displayName !== undefined ? { displayName: management.displayName } : {}),
239
+ management,
240
+ fileKey: record.provenance.fileKey,
241
+ nodeId: record.provenance.nodeId,
242
+ version: record.provenance.version,
243
+ fetchedAt: record.provenance.fetchedAt,
244
+ screenCount,
245
+ skippedCount,
246
+ structuralOnlyCount,
247
+ reductionHint: `${buildReductionHint(screenCount, skippedCount, structuralOnlyCount)}${truncatedClause}`,
248
+ integrityHash: record.integrityHash,
249
+ ...(coverage !== undefined ? { coverage } : {}),
250
+ ...(metrics !== undefined ? { metrics } : {}),
251
+ screens: record.screens.map((s) => ({
252
+ screenId: s.screenId,
253
+ name: screenNameFromIrJson(s.irJson),
254
+ irSummary: irSummaryFromJson(s.irJson),
255
+ imageRelativePath: s.image.relativePath,
256
+ imageSha256: s.image.sha256,
257
+ imageByteLength: s.image.byteLength,
258
+ })),
259
+ structuralScreens: (record.structuralScreens ?? []).map((s) => ({
260
+ screenId: s.screenId,
261
+ name: screenNameFromIrJson(s.irJson),
262
+ irSummary: irSummaryFromJson(s.irJson),
263
+ reason: s.reason,
264
+ })),
265
+ };
266
+ }
267
+ function decodeRouteParam(raw) {
268
+ if (raw === undefined || raw.length === 0)
269
+ return "";
270
+ try {
271
+ return decodeURIComponent(raw);
272
+ }
273
+ catch {
274
+ return "";
275
+ }
276
+ }
277
+ function collectIrIds(value, out = new Set()) {
278
+ if (typeof value !== "object" || value === null)
279
+ return out;
280
+ if (Array.isArray(value)) {
281
+ for (const entry of value)
282
+ collectIrIds(entry, out);
283
+ return out;
284
+ }
285
+ const record = value;
286
+ if (typeof record.id === "string" && record.id.length > 0)
287
+ out.add(record.id);
288
+ for (const entry of Object.values(record))
289
+ collectIrIds(entry, out);
290
+ return out;
291
+ }
292
+ function relatedLinksForScreen(irJson, links) {
293
+ if (links === undefined || links.length === 0)
294
+ return [];
295
+ const ids = collectIrIds(irJson);
296
+ return links.filter((link) => ids.has(link.sourceNodeId) || ids.has(link.targetNodeId));
297
+ }
298
+ function screenJsonSnapshot(record) {
299
+ return {
300
+ screenCount: record.screens.length,
301
+ skippedCount: record.skippedScreens.length,
302
+ structuralOnlyCount: record.structuralScreens?.length ?? 0,
303
+ integrityHash: record.integrityHash,
304
+ redactionSummary: record.redactionSummary,
305
+ ...(record.metrics !== undefined ? { metrics: record.metrics } : {}),
306
+ ...(record.tokens !== undefined ? { tokens: record.tokens } : {}),
307
+ };
308
+ }
309
+ function renderedScreenJsonResponse(record, rendered) {
310
+ return {
311
+ runId: record.runId,
312
+ fileKey: record.provenance.fileKey,
313
+ nodeId: record.provenance.nodeId,
314
+ version: record.provenance.version,
315
+ fetchedAt: record.provenance.fetchedAt,
316
+ source: {
317
+ kind: "figma-snapshot",
318
+ snapshotRunId: record.runId,
319
+ screenIds: [rendered.screenId],
320
+ },
321
+ snapshot: screenJsonSnapshot(record),
322
+ screen: {
323
+ kind: "rendered",
324
+ screenId: rendered.screenId,
325
+ name: screenNameFromIrJson(rendered.irJson),
326
+ irSummary: irSummaryFromJson(rendered.irJson),
327
+ integrityHash: rendered.integrityHash,
328
+ irJson: rendered.irJson,
329
+ image: rendered.image,
330
+ },
331
+ relatedLinks: relatedLinksForScreen(rendered.irJson, record.links),
332
+ };
333
+ }
334
+ function structuralScreenJsonResponse(record, structural) {
335
+ return {
336
+ runId: record.runId,
337
+ fileKey: record.provenance.fileKey,
338
+ nodeId: record.provenance.nodeId,
339
+ version: record.provenance.version,
340
+ fetchedAt: record.provenance.fetchedAt,
341
+ source: {
342
+ kind: "figma-snapshot",
343
+ snapshotRunId: record.runId,
344
+ screenIds: [structural.screenId],
345
+ },
346
+ snapshot: screenJsonSnapshot(record),
347
+ screen: {
348
+ kind: "structural",
349
+ screenId: structural.screenId,
350
+ name: screenNameFromIrJson(structural.irJson),
351
+ irSummary: irSummaryFromJson(structural.irJson),
352
+ integrityHash: structural.integrityHash,
353
+ irJson: structural.irJson,
354
+ structuralReason: structural.reason,
355
+ },
356
+ relatedLinks: relatedLinksForScreen(structural.irJson, record.links),
357
+ };
358
+ }
359
+ function screenJsonResponse(record, screenId) {
360
+ const rendered = record.screens.find((screen) => screen.screenId === screenId);
361
+ if (rendered !== undefined)
362
+ return renderedScreenJsonResponse(record, rendered);
363
+ const structural = record.structuralScreens?.find((screen) => screen.screenId === screenId);
364
+ return structural === undefined ? undefined : structuralScreenJsonResponse(record, structural);
365
+ }
366
+ function recordToListEntry(record, metadata) {
367
+ const structuralOnlyCount = record.structuralScreens?.length ?? 0;
368
+ const management = managementSummaryFromMetadata(metadata);
369
+ return {
370
+ runId: record.runId,
371
+ ...(management.displayName !== undefined ? { displayName: management.displayName } : {}),
372
+ management,
373
+ fileKey: record.provenance.fileKey,
374
+ nodeId: record.provenance.nodeId,
375
+ version: record.provenance.version,
376
+ fetchedAt: record.provenance.fetchedAt,
377
+ screenCount: record.screens.length,
378
+ skippedCount: record.skippedScreens.length,
379
+ structuralOnlyCount,
380
+ reductionHint: buildReductionHint(record.screens.length, record.skippedScreens.length, structuralOnlyCount),
381
+ integrityHash: record.integrityHash,
382
+ };
383
+ }
384
+ function persistedAuditCounts(record, metrics) {
385
+ return {
386
+ screens: metrics.screenCount,
387
+ renders: metrics.renderCount,
388
+ skipped: record.skippedScreens.length,
389
+ designTokens: metrics.designTokenCount,
390
+ ...(metrics.navGraph !== undefined ? { navTransitions: metrics.navGraph.transitions } : {}),
391
+ };
392
+ }
393
+ function appendPersistedSnapshotAudit(evidenceDir, result, record, isResnapshot) {
394
+ appendFigmaConnectorAudit({
395
+ scopeRef: result.scopeRef,
396
+ evidenceDir,
397
+ action: isResnapshot ? "resnapshot" : "snapshot",
398
+ outcome: "ok",
399
+ counts: persistedAuditCounts(record, result.metrics),
400
+ now: result.provenance.fetchedAt,
401
+ });
402
+ }
403
+ function appendSnapshotRouteFailureAudit(evidenceDir, result, isResnapshot, errorCode) {
404
+ appendFigmaConnectorAudit({
405
+ scopeRef: result.scopeRef,
406
+ evidenceDir,
407
+ action: isResnapshot ? "resnapshot" : "snapshot",
408
+ outcome: "error",
409
+ errorCode,
410
+ now: result.provenance.fetchedAt,
411
+ });
412
+ }
413
+ // ─── POST /api/figma/snapshots — parse + validate ─────────────────────────────
414
+ function parseTriggerJson(raw) {
415
+ let parsed;
416
+ try {
417
+ parsed = JSON.parse(raw);
418
+ }
419
+ catch {
420
+ return undefined;
421
+ }
422
+ return typeof parsed === "object" && parsed !== null
423
+ ? parsed
424
+ : undefined;
425
+ }
426
+ function parseTriggerBoardLink(body) {
427
+ const boardLink = typeof body.boardLink === "string" ? body.boardLink.trim() : "";
428
+ return boardLink.length > 0 && parseFigmaTarget(boardLink) !== null ? boardLink : undefined;
429
+ }
430
+ /** Reads and validates the POST body, returning the board link or an error result. */
431
+ async function parseTriggerBody(req) {
432
+ let raw;
433
+ try {
434
+ raw = await readBody(req);
435
+ }
436
+ catch {
437
+ return { status: 400, body: figmaErrorBody("FIGMA_BAD_LINK") };
438
+ }
439
+ const body = parseTriggerJson(raw);
440
+ if (body === undefined) {
441
+ return { status: 400, body: figmaErrorBody("FIGMA_BAD_LINK") };
442
+ }
443
+ const boardLink = parseTriggerBoardLink(body);
444
+ if (boardLink === undefined) {
445
+ return { status: 400, body: figmaErrorBody("FIGMA_BAD_LINK") };
446
+ }
447
+ const requestedVersion = typeof body.version === "string" ? parseFigmaVersion(body.version) : undefined;
448
+ const linkVersion = parseFigmaVersionFromLink(boardLink);
449
+ const version = requestedVersion ?? linkVersion;
450
+ return {
451
+ boardLink,
452
+ ...(version !== undefined ? { version } : {}),
453
+ // Explicit read-only-scope acknowledgement (#760): records consent BEFORE the first fetch.
454
+ acknowledgeReadOnly: body.acknowledgeReadOnly === true,
455
+ // Audited as a re-snapshot (#759): a fresh, explicit, full scoped re-fetch — never a delta.
456
+ isResnapshot: body.isResnapshot === true,
457
+ };
458
+ }
459
+ const FIGMA_VERSION_PATTERN = /^[A-Za-z0-9._:-]{1,256}$/u;
460
+ function parseFigmaVersion(raw) {
461
+ const value = raw.trim();
462
+ return FIGMA_VERSION_PATTERN.test(value) ? value : undefined;
463
+ }
464
+ function parseFigmaVersionFromLink(boardLink) {
465
+ try {
466
+ const url = new URL(boardLink);
467
+ const raw = url.searchParams.get("version-id") ?? url.searchParams.get("version");
468
+ return raw === null ? undefined : parseFigmaVersion(raw);
469
+ }
470
+ catch {
471
+ return undefined;
472
+ }
473
+ }
474
+ // Deployment-overridable deep scoped-pagination budgets (#837). Operators on a tighter Figma plan can
475
+ // dial concurrency/depth/screen-count down (or up) via env without a code change; an unset or
476
+ // non-positive value falls back to the connector default. Mirrors the #532 KEIKO_GROUNDING_* pattern.
477
+ export function figmaPaginationFromEnv(env) {
478
+ const readPositiveInt = (raw) => {
479
+ if (raw === undefined)
480
+ return undefined;
481
+ const value = Number(raw);
482
+ return Number.isInteger(value) && value > 0 ? value : undefined;
483
+ };
484
+ const overrides = {};
485
+ const apply = (key, envName) => {
486
+ const value = readPositiveInt(env[envName]);
487
+ if (value !== undefined)
488
+ overrides[key] = value;
489
+ };
490
+ apply("pageDepth", "KEIKO_FIGMA_PAGE_DEPTH");
491
+ apply("maxNodesPerScreen", "KEIKO_FIGMA_MAX_NODES_PER_SCREEN");
492
+ apply("maxFetchesPerScreen", "KEIKO_FIGMA_MAX_FETCHES_PER_SCREEN");
493
+ apply("maxScreensDeep", "KEIKO_FIGMA_MAX_SCREENS_DEEP");
494
+ apply("fetchConcurrency", "KEIKO_FIGMA_FETCH_CONCURRENCY");
495
+ return resolveScopedPaginationLimits(overrides);
496
+ }
497
+ /** Default total snapshot-build deadline in milliseconds (10 minutes). */
498
+ const DEFAULT_BUILD_DEADLINE_MS = 600_000;
499
+ /** Default per-fetch request timeout in milliseconds (1 minute). */
500
+ const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
501
+ // Parses a positive-integer env var, returning the default when the value is absent or invalid.
502
+ function readPositiveIntEnv(raw, defaultValue) {
503
+ if (raw === undefined)
504
+ return defaultValue;
505
+ const value = Number(raw);
506
+ return Number.isInteger(value) && value > 0 ? value : defaultValue;
507
+ }
508
+ /** KEIKO_FIGMA_BUILD_DEADLINE_MS — total build deadline used by the coalesced promise race. */
509
+ function figmaBuildDeadlineMsFromEnv(env) {
510
+ return readPositiveIntEnv(env.KEIKO_FIGMA_BUILD_DEADLINE_MS, DEFAULT_BUILD_DEADLINE_MS);
511
+ }
512
+ /** KEIKO_FIGMA_REQUEST_TIMEOUT_MS — per-fetch timeout threaded into the transport ports. */
513
+ function figmaRequestTimeoutMsFromEnv(env) {
514
+ return readPositiveIntEnv(env.KEIKO_FIGMA_REQUEST_TIMEOUT_MS, DEFAULT_REQUEST_TIMEOUT_MS);
515
+ }
516
+ // F9 observability: a `FIGMA_INTERNAL` 500 is the catch-all for an UNEXPECTED build/persist failure;
517
+ // the coded body is content-free, so on its own an operator cannot tell a transient render-body
518
+ // malformation from a filesystem failure from a genuine bug. Log the redacted cause (class + message,
519
+ // secrets scrubbed) so the incident is diagnosable without ever leaking a token or provider body.
520
+ // Matches the redacted-console.error convention (memory-salience.ts). Only fires for FIGMA_INTERNAL —
521
+ // expected coded errors (consent/auth/rate-limit) stay quiet (they are already audited).
522
+ function logFigmaInternal(stage, err, deps) {
523
+ const name = err instanceof Error ? err.constructor.name : typeof err;
524
+ const message = err instanceof Error ? err.message : String(err);
525
+ // eslint-disable-next-line no-console
526
+ console.error(`figma snapshot-build failed (${stage}): ${name}`, redact(message, currentRedactionSecrets(deps)));
527
+ }
528
+ // Map a thrown error from the governed build to a coded route result: a coded connector error maps to
529
+ // its status (consent-required → 428, auth → 502, rate-limit → 429, …); anything else is a safe 500.
530
+ function figmaErrorResult(err, deps) {
531
+ if (err instanceof FigmaConnectorError) {
532
+ if (err.code === "FIGMA_INTERNAL")
533
+ logFigmaInternal("build", err, deps);
534
+ return { status: figmaStatusForCode(err.code), body: figmaErrorBody(err.code) };
535
+ }
536
+ logFigmaInternal("build", err, deps);
537
+ return { status: 500, body: figmaErrorBody("FIGMA_INTERNAL") };
538
+ }
539
+ /**
540
+ * Persist the governed snapshot to the evidence store. Now carries BOTH the inter-screen prototype
541
+ * transitions (#811 navigation graph) and the deterministic design-tokens artifact (#752, consumed by
542
+ * design-to-code #755) — both hash-neutral and both previously dropped by the route.
543
+ */
544
+ function persistSnapshot(evidenceDir, runId, result, deps) {
545
+ const store = createNodeFigmaSnapshotStore(evidenceDir);
546
+ try {
547
+ store.record({
548
+ runId,
549
+ provenance: result.provenance,
550
+ integrityHash: result.snapshot.integrityHash,
551
+ screens: result.snapshot.screens.map((s) => ({
552
+ screenId: s.screenId,
553
+ irJson: s.ir,
554
+ integrityHash: s.integrityHash,
555
+ image: { mimeType: "image/png", bytes: s.image.bytes },
556
+ })),
557
+ ...(result.snapshot.structuralScreens !== undefined
558
+ ? {
559
+ structuralScreens: result.snapshot.structuralScreens.map((s) => ({
560
+ screenId: s.screenId,
561
+ reason: s.reason,
562
+ irJson: s.ir,
563
+ integrityHash: s.integrityHash,
564
+ })),
565
+ }
566
+ : {}),
567
+ skippedScreens: result.snapshot.skippedScreens.map((ss) => ({
568
+ screenId: ss.screenId,
569
+ reason: ss.reason,
570
+ })),
571
+ ...(result.snapshot.links !== undefined ? { links: result.snapshot.links } : {}),
572
+ tokens: result.ir.tokens,
573
+ metrics: result.metrics,
574
+ });
575
+ }
576
+ catch (e) {
577
+ logFigmaInternal("persist.record", e, deps);
578
+ return { status: 500, body: figmaErrorBody("FIGMA_INTERNAL") };
579
+ }
580
+ let record;
581
+ try {
582
+ record = store.load(runId);
583
+ }
584
+ catch (e) {
585
+ logFigmaInternal("persist.load", e, deps);
586
+ return { status: 500, body: figmaErrorBody("FIGMA_INTERNAL") };
587
+ }
588
+ if (record === undefined)
589
+ return { status: 500, body: figmaErrorBody("FIGMA_INTERNAL") };
590
+ return record;
591
+ }
592
+ // The map is module-level so it is shared across concurrent requests on the same server instance.
593
+ // It is exposed via makeInFlightMap so tests can inject a fresh map (isolation without mocking).
594
+ let defaultInFlightMap = new Map();
595
+ /** Injectable for tests — returns the module-level map by default. */
596
+ export function makeInFlightMap() {
597
+ return defaultInFlightMap;
598
+ }
599
+ /** Reset the module-level coalescing map. Used by tests to ensure isolation. */
600
+ export function resetInFlightMap() {
601
+ defaultInFlightMap = new Map();
602
+ }
603
+ /** Derive the coalescing key (fileKey:nodeId:operation:consent) for the in-flight map. */
604
+ function coalescingKeyFor(boardLink, acknowledgeReadOnly, isResnapshot, version) {
605
+ const target = parseFigmaTarget(boardLink);
606
+ if (target === null)
607
+ return undefined;
608
+ return `${target.fileKey}:${target.nodeId}:${version ?? "latest"}:${isResnapshot ? "resnapshot" : "snapshot"}:${acknowledgeReadOnly ? "ack" : "noack"}`;
609
+ }
610
+ // Starts the governed build + persist and removes the map entry on settle.
611
+ // Persist is INSIDE this chain: if a caller's deadline fires but the build later completes,
612
+ // the record is stored and an immediate retry POST coalesces or finds it via the GET route.
613
+ function startCoalescedBuild(scopeKey, inFlight, boardLink, body, evidenceDir, deps) {
614
+ const buildAndPersist = async () => {
615
+ let result;
616
+ try {
617
+ // The governed build resolves the vault>config>env PAT (#758), gates on recorded read-only
618
+ // consent before any egress + audits the action + computes metrics (#760), and deep-fetches +
619
+ // renders within the snapshot boundary (#837/#759). Errors (incl. consent-required) are coded.
620
+ result = await governedSnapshotBuild(boardLink, {
621
+ evidenceDir,
622
+ env: deps.env,
623
+ now: new Date().toISOString(),
624
+ acknowledgeReadOnly: body.acknowledgeReadOnly,
625
+ version: body.version,
626
+ pagination: figmaPaginationFromEnv(deps.env),
627
+ egress: currentGatewayEgressConfig(deps),
628
+ configToken: currentGatewayConfig(deps)?.figma?.accessToken,
629
+ portOptions: { timeoutMs: figmaRequestTimeoutMsFromEnv(deps.env) },
630
+ deferSuccessAudit: true,
631
+ }, body.isResnapshot);
632
+ }
633
+ catch (err) {
634
+ return figmaErrorResult(err, deps);
635
+ }
636
+ const runId = `fs-${randomUUID()}`;
637
+ const stored = persistSnapshot(evidenceDir, runId, result, deps);
638
+ if ("status" in stored) {
639
+ appendSnapshotRouteFailureAudit(evidenceDir, result, body.isResnapshot, "FIGMA_INTERNAL");
640
+ return stored;
641
+ }
642
+ appendPersistedSnapshotAudit(evidenceDir, result, stored, body.isResnapshot);
643
+ return { status: 201, body: recordToSummary(stored, result.coverage, stored.metrics) };
644
+ };
645
+ const promise = buildAndPersist().finally(() => {
646
+ inFlight.delete(scopeKey);
647
+ });
648
+ inFlight.set(scopeKey, { promise });
649
+ return promise;
650
+ }
651
+ function makeDeadline(ms) {
652
+ let timerId;
653
+ const promise = new Promise((_resolve, reject) => {
654
+ timerId = setTimeout(() => {
655
+ reject(new FigmaConnectorError("FIGMA_BUILD_TIMEOUT"));
656
+ }, ms);
657
+ timerId.unref();
658
+ });
659
+ return {
660
+ promise,
661
+ clear: () => {
662
+ clearTimeout(timerId);
663
+ },
664
+ };
665
+ }
666
+ // ─── POST /api/figma/snapshots ─────────────────────────────────────────────────
667
+ export async function handleFigmaTriggerSnapshot(ctx, deps, inFlight = defaultInFlightMap) {
668
+ const evidenceDir = deps.evidenceDir;
669
+ if (evidenceDir === undefined || evidenceDir.length === 0) {
670
+ return { status: 503, body: figmaErrorBody("FIGMA_NO_EVIDENCE_DIR") };
671
+ }
672
+ const bodyResult = await parseTriggerBody(ctx.req);
673
+ if ("status" in bodyResult)
674
+ return bodyResult;
675
+ const body = bodyResult;
676
+ const scopeKey = coalescingKeyFor(body.boardLink, body.acknowledgeReadOnly, body.isResnapshot, body.version);
677
+ if (scopeKey === undefined) {
678
+ // parseTriggerBody already validates the board link; this is a belt-and-suspenders guard.
679
+ return { status: 400, body: figmaErrorBody("FIGMA_BAD_LINK") };
680
+ }
681
+ // Coalesce: join an existing build for this scope, or start a new one.
682
+ const existing = inFlight.get(scopeKey);
683
+ const buildPromise = existing !== undefined
684
+ ? existing.promise
685
+ : startCoalescedBuild(scopeKey, inFlight, body.boardLink, body, evidenceDir, deps);
686
+ const deadlineMs = figmaBuildDeadlineMsFromEnv(deps.env);
687
+ const deadline = makeDeadline(deadlineMs);
688
+ // Per-waiter race: deadline + client-disconnect both resolve this waiter promptly while the
689
+ // coalesced build (and its persist) continues uninterrupted for other waiters.
690
+ // Named handler so removeListener can target it precisely in the finally block.
691
+ let onClose;
692
+ const disconnectPromise = new Promise((_resolve, reject) => {
693
+ onClose = () => {
694
+ reject(new FigmaConnectorError("FIGMA_BUILD_TIMEOUT"));
695
+ };
696
+ ctx.req.once("close", onClose);
697
+ });
698
+ try {
699
+ return await Promise.race([buildPromise, deadline.promise, disconnectPromise]);
700
+ }
701
+ catch (err) {
702
+ return figmaErrorResult(err, deps);
703
+ }
704
+ finally {
705
+ deadline.clear();
706
+ ctx.req.removeListener("close", onClose);
707
+ }
708
+ }
709
+ // ─── DELETE /api/figma/token — revoke the stored PAT (#758 rotation/revocation, #760 audit) ───
710
+ export function handleFigmaRevokeToken(ctx, deps) {
711
+ const evidenceDir = deps.evidenceDir;
712
+ if (evidenceDir === undefined || evidenceDir.length === 0) {
713
+ return { status: 503, body: figmaErrorBody("FIGMA_NO_EVIDENCE_DIR") };
714
+ }
715
+ // Revoke is operator key removal (#758): delete the encrypted vault entry. Audited as a connector
716
+ // action (#760) via the observed wrapper. The env/config token (if any) is untouched — revocation
717
+ // only removes the highest-precedence vault key, so the operator can fall back or re-key.
718
+ const scopeRef = deriveFigmaScopeRef("vault", "token");
719
+ try {
720
+ const store = figmaTokenStoreFor({ env: deps.env, evidenceDir });
721
+ observeFigmaRevoke({
722
+ ctx: { evidenceDir, now: new Date().toISOString() },
723
+ scopeRef,
724
+ run: () => {
725
+ store.revoke();
726
+ },
727
+ });
728
+ }
729
+ catch (err) {
730
+ return figmaErrorResult(err, deps);
731
+ }
732
+ return {
733
+ status: 200,
734
+ body: {
735
+ code: "FIGMA_TOKEN_REVOKED_OK",
736
+ message: FIGMA_ROUTE_ERROR_MESSAGES.FIGMA_TOKEN_REVOKED_OK,
737
+ },
738
+ };
739
+ }
740
+ // ─── GET /api/figma/snapshots — list stored snapshots for dashboard/history ────────────────
741
+ const DEFAULT_FIGMA_SNAPSHOT_LIST_LIMIT = 12;
742
+ const MAX_FIGMA_SNAPSHOT_LIST_LIMIT = 50;
743
+ const FIGMA_SNAPSHOT_RECORD_SUFFIX = ".figma-snapshot.json";
744
+ const FIGMA_EVIDENCE_SUBDIR = "qi";
745
+ function parseSnapshotListLimit(url) {
746
+ const raw = url.searchParams.get("limit");
747
+ if (raw === null || raw.length === 0)
748
+ return DEFAULT_FIGMA_SNAPSHOT_LIST_LIMIT;
749
+ const parsed = Number(raw);
750
+ if (!Number.isInteger(parsed) || parsed <= 0)
751
+ return DEFAULT_FIGMA_SNAPSHOT_LIST_LIMIT;
752
+ return Math.min(parsed, MAX_FIGMA_SNAPSHOT_LIST_LIMIT);
753
+ }
754
+ function parseSnapshotListScope(url) {
755
+ const fileKey = (url.searchParams.get("fileKey") ?? "").trim();
756
+ const nodeId = (url.searchParams.get("nodeId") ?? "").trim();
757
+ return fileKey.length > 0 && nodeId.length > 0 ? { fileKey, nodeId } : undefined;
758
+ }
759
+ function loadSnapshotListEntries(store, runIds) {
760
+ const entries = [];
761
+ for (const runId of runIds) {
762
+ let record;
763
+ try {
764
+ record = store.loadMetadata(runId);
765
+ }
766
+ catch {
767
+ continue;
768
+ }
769
+ if (record !== undefined)
770
+ entries.push(recordToListEntry(record, store.loadUserMetadata(runId)));
771
+ }
772
+ return entries;
773
+ }
774
+ function readSnapshotFetchedAt(qiDir, fileName) {
775
+ try {
776
+ const parsed = JSON.parse(readFileSync(join(qiDir, fileName), "utf8"));
777
+ const provenance = typeof parsed.provenance === "object" && parsed.provenance !== null
778
+ ? parsed.provenance
779
+ : undefined;
780
+ const fetchedAt = provenance?.fetchedAt;
781
+ return typeof fetchedAt === "string" && fetchedAt.length > 0 ? fetchedAt : undefined;
782
+ }
783
+ catch {
784
+ return undefined;
785
+ }
786
+ }
787
+ function listRecentSnapshotRunIds(evidenceDir, limit) {
788
+ const qiDir = join(evidenceDir, FIGMA_EVIDENCE_SUBDIR);
789
+ const stat = lstatSync(qiDir, { throwIfNoEntry: false });
790
+ if (stat?.isDirectory() !== true)
791
+ return [];
792
+ const records = [];
793
+ for (const entry of readdirSync(qiDir, { withFileTypes: true })) {
794
+ if (!entry.isFile() || !entry.name.endsWith(FIGMA_SNAPSHOT_RECORD_SUFFIX))
795
+ continue;
796
+ const runId = entry.name.slice(0, -FIGMA_SNAPSHOT_RECORD_SUFFIX.length);
797
+ const fetchedAt = readSnapshotFetchedAt(qiDir, entry.name);
798
+ if (fetchedAt !== undefined)
799
+ records.push({ runId, fetchedAt });
800
+ }
801
+ records.sort((a, b) => (a.fetchedAt > b.fetchedAt ? -1 : a.fetchedAt < b.fetchedAt ? 1 : 0));
802
+ return records.slice(0, limit).map((record) => record.runId);
803
+ }
804
+ export function handleFigmaListSnapshots(ctx, deps) {
805
+ const evidenceDir = deps.evidenceDir;
806
+ if (evidenceDir === undefined || evidenceDir.length === 0) {
807
+ return { status: 503, body: figmaErrorBody("FIGMA_NO_EVIDENCE_DIR") };
808
+ }
809
+ const limit = parseSnapshotListLimit(ctx.url);
810
+ const scope = parseSnapshotListScope(ctx.url);
811
+ const store = createNodeFigmaSnapshotStore(evidenceDir);
812
+ try {
813
+ if (scope !== undefined) {
814
+ const snapshots = store
815
+ .listByScope(scope.fileKey, scope.nodeId)
816
+ .slice(0, limit)
817
+ .flatMap((entry) => {
818
+ const record = store.loadMetadata(entry.runId);
819
+ return record === undefined
820
+ ? []
821
+ : [recordToListEntry(record, store.loadUserMetadata(entry.runId))];
822
+ });
823
+ return { status: 200, body: { snapshots } };
824
+ }
825
+ const snapshots = loadSnapshotListEntries(store, listRecentSnapshotRunIds(evidenceDir, limit));
826
+ return { status: 200, body: { snapshots } };
827
+ }
828
+ catch {
829
+ return { status: 500, body: figmaErrorBody("FIGMA_INTERNAL") };
830
+ }
831
+ }
832
+ // ─── GET /api/figma/snapshots/:runId ──────────────────────────────────────────
833
+ export function handleFigmaLoadSnapshot(ctx, deps) {
834
+ const evidenceDir = deps.evidenceDir;
835
+ if (evidenceDir === undefined || evidenceDir.length === 0) {
836
+ return { status: 503, body: figmaErrorBody("FIGMA_NO_EVIDENCE_DIR") };
837
+ }
838
+ const runId = ctx.params.runId ?? "";
839
+ if (runId.length === 0) {
840
+ return { status: 400, body: figmaErrorBody("FIGMA_SNAPSHOT_NOT_FOUND") };
841
+ }
842
+ const store = createNodeFigmaSnapshotStore(evidenceDir);
843
+ let record;
844
+ try {
845
+ record = store.loadMetadata(runId);
846
+ }
847
+ catch {
848
+ return { status: 500, body: figmaErrorBody("FIGMA_INTERNAL") };
849
+ }
850
+ if (record === undefined) {
851
+ return { status: 404, body: figmaErrorBody("FIGMA_SNAPSHOT_NOT_FOUND") };
852
+ }
853
+ return {
854
+ status: 200,
855
+ body: recordToSummary(record, undefined, record.metrics, store.loadUserMetadata(runId)),
856
+ };
857
+ }
858
+ // ─── PATCH /api/figma/snapshots/:runId — mutable management metadata ────────
859
+ const MAX_SNAPSHOT_DISPLAY_NAME_LENGTH = 120;
860
+ function hasControlCharacter(value) {
861
+ for (let i = 0; i < value.length; i += 1) {
862
+ const code = value.charCodeAt(i);
863
+ if (code <= 0x1f || code === 0x7f)
864
+ return true;
865
+ }
866
+ return false;
867
+ }
868
+ function parseSnapshotMetadataJson(raw) {
869
+ let parsed;
870
+ try {
871
+ parsed = JSON.parse(raw);
872
+ }
873
+ catch {
874
+ return undefined;
875
+ }
876
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
877
+ ? parsed
878
+ : undefined;
879
+ }
880
+ function normalizePatchDisplayName(value) {
881
+ if (value === null)
882
+ return null;
883
+ if (typeof value !== "string")
884
+ return undefined;
885
+ const trimmed = value.trim().replace(/\s+/gu, " ");
886
+ if (trimmed.length === 0)
887
+ return null;
888
+ if (trimmed.length > MAX_SNAPSHOT_DISPLAY_NAME_LENGTH)
889
+ return undefined;
890
+ if (hasControlCharacter(trimmed))
891
+ return undefined;
892
+ return trimmed;
893
+ }
894
+ async function parseSnapshotMetadataPatch(req) {
895
+ let raw;
896
+ try {
897
+ raw = await readBody(req);
898
+ }
899
+ catch {
900
+ return { status: 400, body: figmaErrorBody("FIGMA_BAD_METADATA") };
901
+ }
902
+ const body = parseSnapshotMetadataJson(raw);
903
+ if (body === undefined || !Object.prototype.hasOwnProperty.call(body, "displayName")) {
904
+ return { status: 400, body: figmaErrorBody("FIGMA_BAD_METADATA") };
905
+ }
906
+ const displayName = normalizePatchDisplayName(body.displayName);
907
+ return displayName === undefined
908
+ ? { status: 400, body: figmaErrorBody("FIGMA_BAD_METADATA") }
909
+ : { displayName };
910
+ }
911
+ export async function handleFigmaUpdateSnapshotMetadata(ctx, deps) {
912
+ const evidenceDir = deps.evidenceDir;
913
+ if (evidenceDir === undefined || evidenceDir.length === 0) {
914
+ return { status: 503, body: figmaErrorBody("FIGMA_NO_EVIDENCE_DIR") };
915
+ }
916
+ const runId = ctx.params.runId ?? "";
917
+ if (runId.length === 0) {
918
+ return { status: 400, body: figmaErrorBody("FIGMA_SNAPSHOT_NOT_FOUND") };
919
+ }
920
+ const patch = await parseSnapshotMetadataPatch(ctx.req);
921
+ if ("status" in patch)
922
+ return patch;
923
+ const store = createNodeFigmaSnapshotStore(evidenceDir);
924
+ let record;
925
+ try {
926
+ record = store.loadMetadata(runId);
927
+ }
928
+ catch {
929
+ return { status: 500, body: figmaErrorBody("FIGMA_INTERNAL") };
930
+ }
931
+ if (record === undefined) {
932
+ return { status: 404, body: figmaErrorBody("FIGMA_SNAPSHOT_NOT_FOUND") };
933
+ }
934
+ let metadata;
935
+ try {
936
+ metadata = store.updateUserMetadata(runId, {
937
+ displayName: patch.displayName,
938
+ updatedAt: new Date().toISOString(),
939
+ });
940
+ }
941
+ catch {
942
+ return { status: 500, body: figmaErrorBody("FIGMA_INTERNAL") };
943
+ }
944
+ return {
945
+ status: 200,
946
+ body: recordToSummary(record, undefined, record.metrics, metadata),
947
+ };
948
+ }
949
+ // ─── DELETE /api/figma/snapshots/:runId — explicit snapshot deletion ─────────
950
+ export function handleFigmaDeleteSnapshot(ctx, deps) {
951
+ const evidenceDir = deps.evidenceDir;
952
+ if (evidenceDir === undefined || evidenceDir.length === 0) {
953
+ return { status: 503, body: figmaErrorBody("FIGMA_NO_EVIDENCE_DIR") };
954
+ }
955
+ const runId = ctx.params.runId ?? "";
956
+ if (runId.length === 0) {
957
+ return { status: 400, body: figmaErrorBody("FIGMA_SNAPSHOT_NOT_FOUND") };
958
+ }
959
+ const store = createNodeFigmaSnapshotStore(evidenceDir);
960
+ let record;
961
+ try {
962
+ record = store.loadMetadata(runId);
963
+ }
964
+ catch {
965
+ return { status: 500, body: figmaErrorBody("FIGMA_INTERNAL") };
966
+ }
967
+ if (record === undefined) {
968
+ return { status: 404, body: figmaErrorBody("FIGMA_SNAPSHOT_NOT_FOUND") };
969
+ }
970
+ try {
971
+ const deleted = store.deleteSnapshot(runId);
972
+ return {
973
+ status: 200,
974
+ body: {
975
+ runId,
976
+ deleted: deleted.recordDeleted,
977
+ sideFileDirDeleted: deleted.sideFileDirDeleted,
978
+ metadataDeleted: deleted.metadataDeleted,
979
+ },
980
+ };
981
+ }
982
+ catch {
983
+ return { status: 500, body: figmaErrorBody("FIGMA_INTERNAL") };
984
+ }
985
+ }
986
+ // ─── GET /api/figma/snapshots/:runId/screens/:screenId/json ───────────────────
987
+ export function handleFigmaInspectSnapshotScreenJson(ctx, deps) {
988
+ const evidenceDir = deps.evidenceDir;
989
+ if (evidenceDir === undefined || evidenceDir.length === 0) {
990
+ return { status: 503, body: figmaErrorBody("FIGMA_NO_EVIDENCE_DIR") };
991
+ }
992
+ const runId = decodeRouteParam(ctx.params.runId);
993
+ const screenId = decodeRouteParam(ctx.params.screenId);
994
+ if (runId.length === 0 || screenId.length === 0) {
995
+ return { status: 404, body: figmaErrorBody("FIGMA_SCREEN_NOT_FOUND") };
996
+ }
997
+ const store = createNodeFigmaSnapshotStore(evidenceDir);
998
+ let record;
999
+ try {
1000
+ record = store.loadMetadata(runId);
1001
+ }
1002
+ catch {
1003
+ return { status: 500, body: figmaErrorBody("FIGMA_INTERNAL") };
1004
+ }
1005
+ if (record === undefined) {
1006
+ return { status: 404, body: figmaErrorBody("FIGMA_SNAPSHOT_NOT_FOUND") };
1007
+ }
1008
+ const body = screenJsonResponse(record, screenId);
1009
+ return body === undefined
1010
+ ? { status: 404, body: figmaErrorBody("FIGMA_SCREEN_NOT_FOUND") }
1011
+ : { status: 200, body };
1012
+ }
1013
+ // ─── GET /api/figma/snapshots/:runId/screens/:screenIndex/image ──────────────
1014
+ function parseScreenIndex(raw) {
1015
+ if (raw === undefined || raw.length === 0)
1016
+ return undefined;
1017
+ const parsed = Number(raw);
1018
+ return Number.isInteger(parsed) && parsed >= 0 && String(parsed) === raw ? parsed : undefined;
1019
+ }
1020
+ function loadSnapshotRecordForImage(evidenceDir, runId) {
1021
+ const store = createNodeFigmaSnapshotStore(evidenceDir);
1022
+ let record;
1023
+ try {
1024
+ record = store.load(runId);
1025
+ }
1026
+ catch {
1027
+ return { status: 500, body: figmaErrorBody("FIGMA_INTERNAL") };
1028
+ }
1029
+ return record === undefined
1030
+ ? { status: 404, body: figmaErrorBody("FIGMA_SCREEN_NOT_FOUND") }
1031
+ : { store, record };
1032
+ }
1033
+ export function handleFigmaLoadSnapshotImage(ctx, deps) {
1034
+ const evidenceDir = deps.evidenceDir;
1035
+ if (evidenceDir === undefined || evidenceDir.length === 0) {
1036
+ return { status: 503, body: figmaErrorBody("FIGMA_NO_EVIDENCE_DIR") };
1037
+ }
1038
+ const runId = ctx.params.runId ?? "";
1039
+ const screenIndex = parseScreenIndex(ctx.params.screenIndex);
1040
+ if (runId.length === 0 || screenIndex === undefined) {
1041
+ return { status: 404, body: figmaErrorBody("FIGMA_SCREEN_NOT_FOUND") };
1042
+ }
1043
+ const loaded = loadSnapshotRecordForImage(evidenceDir, runId);
1044
+ if ("status" in loaded)
1045
+ return loaded;
1046
+ const screen = loaded.record.screens[screenIndex];
1047
+ if (screen === undefined) {
1048
+ return { status: 404, body: figmaErrorBody("FIGMA_SCREEN_NOT_FOUND") };
1049
+ }
1050
+ try {
1051
+ const image = loaded.store.loadImage(runId, screen.image);
1052
+ ctx.res.statusCode = 200;
1053
+ ctx.res.setHeader("Content-Type", image.mimeType);
1054
+ ctx.res.setHeader("Content-Length", String(image.byteLength));
1055
+ ctx.res.setHeader("Cache-Control", "no-store");
1056
+ ctx.res.setHeader("ETag", `"sha256-${image.sha256}"`);
1057
+ ctx.res.end(image.bytes);
1058
+ return STREAMING;
1059
+ }
1060
+ catch {
1061
+ return { status: 500, body: figmaErrorBody("FIGMA_INTERNAL") };
1062
+ }
1063
+ }