@corgelabs/corge 0.0.1 → 1.0.1

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 (341) hide show
  1. package/dist/ai/index.d.ts +3 -0
  2. package/dist/ai/index.d.ts.map +1 -0
  3. package/dist/ai/index.js +3 -0
  4. package/dist/ai/index.js.map +1 -0
  5. package/dist/ai/schema.d.ts +83 -0
  6. package/dist/ai/schema.d.ts.map +1 -0
  7. package/dist/ai/schema.js +19 -0
  8. package/dist/ai/schema.js.map +1 -0
  9. package/dist/ai/service.d.ts +14 -0
  10. package/dist/ai/service.d.ts.map +1 -0
  11. package/dist/ai/service.js +266 -0
  12. package/dist/ai/service.js.map +1 -0
  13. package/dist/ai/utils.d.ts +10 -0
  14. package/dist/ai/utils.d.ts.map +1 -0
  15. package/dist/ai/utils.js +27 -0
  16. package/dist/ai/utils.js.map +1 -0
  17. package/dist/attachments/index.d.ts +4 -0
  18. package/dist/attachments/index.d.ts.map +1 -0
  19. package/dist/attachments/index.js +4 -0
  20. package/dist/attachments/index.js.map +1 -0
  21. package/dist/attachments/routes.d.ts +75 -0
  22. package/dist/attachments/routes.d.ts.map +1 -0
  23. package/dist/attachments/routes.js +18 -0
  24. package/dist/attachments/routes.js.map +1 -0
  25. package/dist/attachments/schema.d.ts +61 -0
  26. package/dist/attachments/schema.d.ts.map +1 -0
  27. package/dist/attachments/schema.js +30 -0
  28. package/dist/attachments/schema.js.map +1 -0
  29. package/dist/attachments/service.d.ts +27 -0
  30. package/dist/attachments/service.d.ts.map +1 -0
  31. package/dist/attachments/service.js +224 -0
  32. package/dist/attachments/service.js.map +1 -0
  33. package/dist/attachments/utils.d.ts +9 -0
  34. package/dist/attachments/utils.d.ts.map +1 -0
  35. package/dist/attachments/utils.js +80 -0
  36. package/dist/attachments/utils.js.map +1 -0
  37. package/dist/auth/index.d.ts +4 -0
  38. package/dist/auth/index.d.ts.map +1 -0
  39. package/dist/auth/index.js +4 -0
  40. package/dist/auth/index.js.map +1 -0
  41. package/dist/auth/oidc.d.ts +25 -0
  42. package/dist/auth/oidc.d.ts.map +1 -0
  43. package/dist/auth/oidc.js +298 -0
  44. package/dist/auth/oidc.js.map +1 -0
  45. package/dist/auth/recovery.d.ts +41 -0
  46. package/dist/auth/recovery.d.ts.map +1 -0
  47. package/dist/auth/recovery.js +250 -0
  48. package/dist/auth/recovery.js.map +1 -0
  49. package/dist/auth/routes.d.ts +226 -0
  50. package/dist/auth/routes.d.ts.map +1 -0
  51. package/dist/auth/routes.js +96 -0
  52. package/dist/auth/routes.js.map +1 -0
  53. package/dist/auth/schema.d.ts +158 -0
  54. package/dist/auth/schema.d.ts.map +1 -0
  55. package/dist/auth/schema.js +82 -0
  56. package/dist/auth/schema.js.map +1 -0
  57. package/dist/auth/service.d.ts +96 -0
  58. package/dist/auth/service.d.ts.map +1 -0
  59. package/dist/auth/service.js +129 -0
  60. package/dist/auth/service.js.map +1 -0
  61. package/dist/auth/session.d.ts +51 -0
  62. package/dist/auth/session.d.ts.map +1 -0
  63. package/dist/auth/session.js +344 -0
  64. package/dist/auth/session.js.map +1 -0
  65. package/dist/auth/utils.d.ts +17 -0
  66. package/dist/auth/utils.d.ts.map +1 -0
  67. package/dist/auth/utils.js +96 -0
  68. package/dist/auth/utils.js.map +1 -0
  69. package/dist/bin/corge.js +542 -0
  70. package/dist/chat/context.d.ts +10 -0
  71. package/dist/chat/context.d.ts.map +1 -0
  72. package/dist/chat/context.js +221 -0
  73. package/dist/chat/context.js.map +1 -0
  74. package/dist/chat/index.d.ts +4 -0
  75. package/dist/chat/index.d.ts.map +1 -0
  76. package/dist/chat/index.js +4 -0
  77. package/dist/chat/index.js.map +1 -0
  78. package/dist/chat/routes.d.ts +160 -0
  79. package/dist/chat/routes.d.ts.map +1 -0
  80. package/dist/chat/routes.js +210 -0
  81. package/dist/chat/routes.js.map +1 -0
  82. package/dist/chat/schema.d.ts +285 -0
  83. package/dist/chat/schema.d.ts.map +1 -0
  84. package/dist/chat/schema.js +74 -0
  85. package/dist/chat/schema.js.map +1 -0
  86. package/dist/chat/service.d.ts +64 -0
  87. package/dist/chat/service.d.ts.map +1 -0
  88. package/dist/chat/service.js +279 -0
  89. package/dist/chat/service.js.map +1 -0
  90. package/dist/chat/stream.d.ts +53 -0
  91. package/dist/chat/stream.d.ts.map +1 -0
  92. package/dist/chat/stream.js +339 -0
  93. package/dist/chat/stream.js.map +1 -0
  94. package/dist/chat/utils.d.ts +61 -0
  95. package/dist/chat/utils.d.ts.map +1 -0
  96. package/dist/chat/utils.js +285 -0
  97. package/dist/chat/utils.js.map +1 -0
  98. package/dist/cli/builder.d.ts +6 -0
  99. package/dist/cli/builder.d.ts.map +1 -0
  100. package/dist/cli/builder.js +91 -0
  101. package/dist/cli/builder.js.map +1 -0
  102. package/dist/cli/deployer.d.ts +2 -0
  103. package/dist/cli/deployer.d.ts.map +1 -0
  104. package/dist/cli/deployer.js +152 -0
  105. package/dist/cli/deployer.js.map +1 -0
  106. package/dist/cli/dev-server.d.ts +2 -0
  107. package/dist/cli/dev-server.d.ts.map +1 -0
  108. package/dist/cli/dev-server.js +70 -0
  109. package/dist/cli/dev-server.js.map +1 -0
  110. package/dist/cli/env.d.ts +4 -0
  111. package/dist/cli/env.d.ts.map +1 -0
  112. package/dist/cli/env.js +68 -0
  113. package/dist/cli/env.js.map +1 -0
  114. package/dist/cli/init.d.ts +6 -0
  115. package/dist/cli/init.d.ts.map +1 -0
  116. package/dist/cli/init.js +97 -0
  117. package/dist/cli/init.js.map +1 -0
  118. package/dist/config.d.ts +262 -0
  119. package/dist/config.d.ts.map +1 -0
  120. package/dist/config.js +212 -0
  121. package/dist/config.js.map +1 -0
  122. package/dist/db/index.d.ts +3 -0
  123. package/dist/db/index.d.ts.map +1 -0
  124. package/dist/db/index.js +3 -0
  125. package/dist/db/index.js.map +1 -0
  126. package/dist/db/schema.d.ts +410 -0
  127. package/dist/db/schema.d.ts.map +1 -0
  128. package/dist/db/schema.js +294 -0
  129. package/dist/db/schema.js.map +1 -0
  130. package/dist/db/service.d.ts +42 -0
  131. package/dist/db/service.d.ts.map +1 -0
  132. package/dist/db/service.js +372 -0
  133. package/dist/db/service.js.map +1 -0
  134. package/dist/db/utils.d.ts +25 -0
  135. package/dist/db/utils.d.ts.map +1 -0
  136. package/dist/db/utils.js +188 -0
  137. package/dist/db/utils.js.map +1 -0
  138. package/dist/emails/index.d.ts +4 -0
  139. package/dist/emails/index.d.ts.map +1 -0
  140. package/dist/emails/index.js +4 -0
  141. package/dist/emails/index.js.map +1 -0
  142. package/dist/emails/routes.d.ts +191 -0
  143. package/dist/emails/routes.d.ts.map +1 -0
  144. package/dist/emails/routes.js +39 -0
  145. package/dist/emails/routes.js.map +1 -0
  146. package/dist/emails/schema.d.ts +237 -0
  147. package/dist/emails/schema.d.ts.map +1 -0
  148. package/dist/emails/schema.js +98 -0
  149. package/dist/emails/schema.js.map +1 -0
  150. package/dist/emails/service.d.ts +22 -0
  151. package/dist/emails/service.d.ts.map +1 -0
  152. package/dist/emails/service.js +337 -0
  153. package/dist/emails/service.js.map +1 -0
  154. package/dist/emails/utils.d.ts +49 -0
  155. package/dist/emails/utils.d.ts.map +1 -0
  156. package/dist/emails/utils.js +341 -0
  157. package/dist/emails/utils.js.map +1 -0
  158. package/dist/files/index.d.ts +4 -0
  159. package/dist/files/index.d.ts.map +1 -0
  160. package/dist/files/index.js +4 -0
  161. package/dist/files/index.js.map +1 -0
  162. package/dist/files/routes.d.ts +72 -0
  163. package/dist/files/routes.d.ts.map +1 -0
  164. package/dist/files/routes.js +38 -0
  165. package/dist/files/routes.js.map +1 -0
  166. package/dist/files/schema.d.ts +57 -0
  167. package/dist/files/schema.d.ts.map +1 -0
  168. package/dist/files/schema.js +39 -0
  169. package/dist/files/schema.js.map +1 -0
  170. package/dist/files/service.d.ts +27 -0
  171. package/dist/files/service.d.ts.map +1 -0
  172. package/dist/files/service.js +175 -0
  173. package/dist/files/service.js.map +1 -0
  174. package/dist/files/utils.d.ts +3 -0
  175. package/dist/files/utils.d.ts.map +1 -0
  176. package/dist/files/utils.js +63 -0
  177. package/dist/files/utils.js.map +1 -0
  178. package/dist/index.d.ts +875 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +9919 -0
  181. package/dist/index.js.map +7 -0
  182. package/dist/payments/billing.d.ts +29 -0
  183. package/dist/payments/billing.d.ts.map +1 -0
  184. package/dist/payments/billing.js +378 -0
  185. package/dist/payments/billing.js.map +1 -0
  186. package/dist/payments/index.d.ts +4 -0
  187. package/dist/payments/index.d.ts.map +1 -0
  188. package/dist/payments/index.js +4 -0
  189. package/dist/payments/index.js.map +1 -0
  190. package/dist/payments/routes.d.ts +340 -0
  191. package/dist/payments/routes.d.ts.map +1 -0
  192. package/dist/payments/routes.js +56 -0
  193. package/dist/payments/routes.js.map +1 -0
  194. package/dist/payments/schema.d.ts +412 -0
  195. package/dist/payments/schema.d.ts.map +1 -0
  196. package/dist/payments/schema.js +260 -0
  197. package/dist/payments/schema.js.map +1 -0
  198. package/dist/payments/service.d.ts +31 -0
  199. package/dist/payments/service.d.ts.map +1 -0
  200. package/dist/payments/service.js +322 -0
  201. package/dist/payments/service.js.map +1 -0
  202. package/dist/payments/utils.d.ts +61 -0
  203. package/dist/payments/utils.d.ts.map +1 -0
  204. package/dist/payments/utils.js +369 -0
  205. package/dist/payments/utils.js.map +1 -0
  206. package/dist/router/guard.d.ts +19 -0
  207. package/dist/router/guard.d.ts.map +1 -0
  208. package/dist/router/guard.js +243 -0
  209. package/dist/router/guard.js.map +1 -0
  210. package/dist/router/index.d.ts +3 -0
  211. package/dist/router/index.d.ts.map +1 -0
  212. package/dist/router/index.js +3 -0
  213. package/dist/router/index.js.map +1 -0
  214. package/dist/router/schema.d.ts +103 -0
  215. package/dist/router/schema.d.ts.map +1 -0
  216. package/dist/router/schema.js +282 -0
  217. package/dist/router/schema.js.map +1 -0
  218. package/dist/router/service.d.ts +25 -0
  219. package/dist/router/service.d.ts.map +1 -0
  220. package/dist/router/service.js +339 -0
  221. package/dist/router/service.js.map +1 -0
  222. package/dist/tasks/index.d.ts +4 -0
  223. package/dist/tasks/index.d.ts.map +1 -0
  224. package/dist/tasks/index.js +4 -0
  225. package/dist/tasks/index.js.map +1 -0
  226. package/dist/tasks/routes.d.ts +54 -0
  227. package/dist/tasks/routes.d.ts.map +1 -0
  228. package/dist/tasks/routes.js +195 -0
  229. package/dist/tasks/routes.js.map +1 -0
  230. package/dist/tasks/schema.d.ts +261 -0
  231. package/dist/tasks/schema.d.ts.map +1 -0
  232. package/dist/tasks/schema.js +35 -0
  233. package/dist/tasks/schema.js.map +1 -0
  234. package/dist/tasks/service.d.ts +32 -0
  235. package/dist/tasks/service.d.ts.map +1 -0
  236. package/dist/tasks/service.js +201 -0
  237. package/dist/tasks/service.js.map +1 -0
  238. package/dist/tasks/steps.d.ts +18 -0
  239. package/dist/tasks/steps.d.ts.map +1 -0
  240. package/dist/tasks/steps.js +365 -0
  241. package/dist/tasks/steps.js.map +1 -0
  242. package/dist/tasks/utils.d.ts +75 -0
  243. package/dist/tasks/utils.d.ts.map +1 -0
  244. package/dist/tasks/utils.js +311 -0
  245. package/dist/tasks/utils.js.map +1 -0
  246. package/dist/tasks/watcher.d.ts +13 -0
  247. package/dist/tasks/watcher.d.ts.map +1 -0
  248. package/dist/tasks/watcher.js +255 -0
  249. package/dist/tasks/watcher.js.map +1 -0
  250. package/dist/tasks/worker.d.ts +28 -0
  251. package/dist/tasks/worker.d.ts.map +1 -0
  252. package/dist/tasks/worker.js +326 -0
  253. package/dist/tasks/worker.js.map +1 -0
  254. package/dist/utils/index.d.ts +3 -0
  255. package/dist/utils/index.d.ts.map +1 -0
  256. package/dist/utils/index.js +3 -0
  257. package/dist/utils/index.js.map +1 -0
  258. package/dist/utils/schema.d.ts +79 -0
  259. package/dist/utils/schema.d.ts.map +1 -0
  260. package/dist/utils/schema.js +41 -0
  261. package/dist/utils/schema.js.map +1 -0
  262. package/dist/utils/service.d.ts +32 -0
  263. package/dist/utils/service.d.ts.map +1 -0
  264. package/dist/utils/service.js +384 -0
  265. package/dist/utils/service.js.map +1 -0
  266. package/docs/SDK_SPEC.md +1645 -0
  267. package/infra/firestore.base.indexes.json +138 -0
  268. package/infra/firestore.base.rules +152 -0
  269. package/infra/storage.base.rules +8 -0
  270. package/package.json +58 -13
  271. package/src/ai/index.ts +2 -0
  272. package/src/ai/schema.ts +64 -0
  273. package/src/ai/service.ts +317 -0
  274. package/src/ai/utils.ts +38 -0
  275. package/src/attachments/index.ts +3 -0
  276. package/src/attachments/routes.ts +28 -0
  277. package/src/attachments/schema.ts +45 -0
  278. package/src/attachments/service.ts +315 -0
  279. package/src/attachments/utils.ts +115 -0
  280. package/src/auth/index.ts +3 -0
  281. package/src/auth/oidc.ts +419 -0
  282. package/src/auth/recovery.ts +365 -0
  283. package/src/auth/routes.ts +142 -0
  284. package/src/auth/schema.ts +181 -0
  285. package/src/auth/service.ts +183 -0
  286. package/src/auth/session.ts +414 -0
  287. package/src/auth/utils.ts +106 -0
  288. package/src/chat/context.ts +310 -0
  289. package/src/chat/index.ts +3 -0
  290. package/src/chat/routes.ts +264 -0
  291. package/src/chat/schema.ts +295 -0
  292. package/src/chat/service.ts +360 -0
  293. package/src/chat/stream.ts +481 -0
  294. package/src/chat/utils.ts +370 -0
  295. package/src/cli/builder.ts +93 -0
  296. package/src/cli/deployer.ts +184 -0
  297. package/src/cli/dev-server.ts +77 -0
  298. package/src/cli/env.ts +89 -0
  299. package/src/cli/init.ts +121 -0
  300. package/src/config.ts +447 -0
  301. package/src/db/index.ts +2 -0
  302. package/src/db/schema.ts +409 -0
  303. package/src/db/service.ts +477 -0
  304. package/src/db/utils.ts +250 -0
  305. package/src/emails/index.ts +3 -0
  306. package/src/emails/routes.ts +64 -0
  307. package/src/emails/schema.ts +239 -0
  308. package/src/emails/service.ts +488 -0
  309. package/src/emails/utils.ts +449 -0
  310. package/src/files/index.ts +3 -0
  311. package/src/files/routes.ts +44 -0
  312. package/src/files/schema.ts +76 -0
  313. package/src/files/service.ts +238 -0
  314. package/src/files/utils.ts +77 -0
  315. package/src/index.ts +425 -0
  316. package/src/payments/billing.ts +488 -0
  317. package/src/payments/index.ts +3 -0
  318. package/src/payments/routes.ts +92 -0
  319. package/src/payments/schema.ts +401 -0
  320. package/src/payments/service.ts +445 -0
  321. package/src/payments/utils.ts +472 -0
  322. package/src/router/guard.ts +319 -0
  323. package/src/router/index.ts +2 -0
  324. package/src/router/schema.ts +458 -0
  325. package/src/router/service.ts +453 -0
  326. package/src/tasks/index.ts +11 -0
  327. package/src/tasks/routes.ts +230 -0
  328. package/src/tasks/schema.ts +310 -0
  329. package/src/tasks/service.ts +254 -0
  330. package/src/tasks/steps.ts +472 -0
  331. package/src/tasks/utils.ts +445 -0
  332. package/src/tasks/watcher.ts +307 -0
  333. package/src/tasks/worker.ts +461 -0
  334. package/src/utils/index.ts +2 -0
  335. package/src/utils/schema.ts +69 -0
  336. package/src/utils/service.ts +461 -0
  337. package/tests/e2e.test.ts +2531 -0
  338. package/tsconfig.base.json +12 -0
  339. package/README.md +0 -2
  340. package/index.d.ts +0 -1
  341. package/index.js +0 -1
@@ -0,0 +1,315 @@
1
+ import type { JevQuestion } from '../ai'
2
+ import type { SelectModel } from '../db'
3
+ import { fileScan, fileScanError, type FileRef, type FileScan, type FileScanError } from '../files'
4
+ import type { Core } from '../index'
5
+ import type { StepRunner } from '../tasks'
6
+ import {
7
+ getRegisteredAttachmentTypes,
8
+ processAttachmentsOutput,
9
+ scanAttachmentInput,
10
+ scanAttachmentOutput,
11
+ type AttachmentTypesMap,
12
+ type ProcessedAttachment,
13
+ type ProcessAttachmentsInput,
14
+ type ProcessAttachmentsOutput,
15
+ type ScanAttachmentInput,
16
+ type ScanAttachmentOutput
17
+ } from './schema'
18
+ import {
19
+ buildScanError,
20
+ classifyPermanentScanDefect,
21
+ extractAttachmentTasks,
22
+ parseFields,
23
+ schemaFor
24
+ } from './utils'
25
+
26
+ export class AttachmentsService {
27
+ readonly #core: Core
28
+
29
+ constructor(coreClient: Core) {
30
+ this.#core = coreClient
31
+ }
32
+
33
+ #wrapScanStep = async <T>(fn: () => Promise<T>, onDefect: (defect: FileScanError) => void): Promise<T> => {
34
+ try {
35
+ return await fn()
36
+ } catch (err) {
37
+ const defect = classifyPermanentScanDefect(err)
38
+ if (defect) {
39
+ onDefect(defect)
40
+ return this.#core.errors.nonRetryableError(defect.agentHint, defect.reason)
41
+ }
42
+ throw err
43
+ }
44
+ }
45
+
46
+ #findCrossUserByShaSum = async (
47
+ ref: FileRef,
48
+ shaSum: string,
49
+ requestedType?: string
50
+ ): Promise<{ scan?: FileScan; scanError?: FileScanError } | undefined> => {
51
+ if (!shaSum || shaSum.length < 24) return undefined
52
+ try {
53
+ await this.#core.internals.resilience.acquireLimit('firestore')
54
+ const snap = await this.#core.db.firestore().collection('file').where('shaSum', '==', shaSum).limit(5).get()
55
+ for (const doc of snap.docs) {
56
+ if (doc.id === ref) continue
57
+ const data = doc.data()
58
+ if (data.deleted === true) continue
59
+ if (data.scan) {
60
+ const parsedScan = fileScan.safeParse(data.scan)
61
+ if (parsedScan.success && (!requestedType || parsedScan.data.type === requestedType)) {
62
+ return { scan: parsedScan.data }
63
+ }
64
+ }
65
+ if (data.scanError) {
66
+ const parsedErr = fileScanError.safeParse(data.scanError)
67
+ if (parsedErr.success) {
68
+ return { scanError: parsedErr.data }
69
+ }
70
+ }
71
+ }
72
+ } catch {}
73
+ return undefined
74
+ }
75
+
76
+ classify = async (
77
+ attachmentContent: string,
78
+ types: AttachmentTypesMap = getRegisteredAttachmentTypes()
79
+ ): Promise<{ attachmentType: string; confidence: number }> => {
80
+ const entries = Object.entries(types)
81
+ if (!entries.length) return { attachmentType: 'other', confidence: 1 }
82
+ const questions: Record<string, JevQuestion> = {
83
+ documentType: {
84
+ type: 'choice',
85
+ instructions:
86
+ 'Classify the attached document (its full text is the state) into exactly one type. Choose "other" when it matches none of the specific types.',
87
+ criteria: {
88
+ ...Object.fromEntries(entries.map(([k, v]) => [k, v.description])),
89
+ other: 'Unrecognized document, plain image/diagram, or none of the specific attachment types'
90
+ }
91
+ }
92
+ }
93
+ const { answers } = await this.#core.ai.decide({ document: attachmentContent.slice(0, 80_000) }, questions)
94
+ const ans = answers.documentType
95
+ return ans?.type === 'choice'
96
+ ? { attachmentType: ans.choice, confidence: ans.confidence }
97
+ : { attachmentType: 'other', confidence: 0 }
98
+ }
99
+
100
+ #executeScanOcrAndClassify = async (opts: {
101
+ ref: FileRef
102
+ contentType: string
103
+ initialType?: string
104
+ types: AttachmentTypesMap
105
+ step: StepRunner
106
+ onDefect: (d: FileScanError) => void
107
+ }): Promise<FileScan> => {
108
+ const { ref, contentType, initialType, types, step, onDefect } = opts
109
+ const isImage = contentType.startsWith('image/')
110
+ let dataUrlP: Promise<string> | undefined
111
+ const getDataUrl = () =>
112
+ (dataUrlP ??= (async () =>
113
+ `data:${contentType};base64,${(await this.#core.files.download(ref)).toString('base64')}`)())
114
+
115
+ const hasTypes = Object.keys(types).length > 0
116
+ let resolvedType: string | undefined = hasTypes ? initialType : 'other'
117
+ let confidence = 1
118
+ let markdown = ''
119
+
120
+ if (!resolvedType || resolvedType === 'other') {
121
+ const ocrRes = await step('ocr', ({ signal }) =>
122
+ this.#wrapScanStep(async () => this.#core.ai.vision(await getDataUrl(), { signal }), onDefect)
123
+ )
124
+ markdown = ocrRes.markdown
125
+ if (!markdown) {
126
+ markdown = await step('vision', ({ signal }) =>
127
+ this.#wrapScanStep(async () => this.#core.ai.describeImage(await getDataUrl(), signal), onDefect)
128
+ )
129
+ resolvedType = 'other'
130
+ } else {
131
+ if (!resolvedType) {
132
+ const classified = await step('classify', () =>
133
+ this.#wrapScanStep(() => this.classify(markdown, types), onDefect)
134
+ )
135
+ resolvedType = classified.attachmentType
136
+ confidence = classified.confidence
137
+ }
138
+ if (hasTypes && resolvedType === 'other' && isImage) {
139
+ const visionDesc = await step('vision', ({ signal }) =>
140
+ this.#wrapScanStep(async () => this.#core.ai.describeImage(await getDataUrl(), signal), onDefect)
141
+ )
142
+ markdown = `${markdown}\n\n${visionDesc}`.trim()
143
+ }
144
+ }
145
+ }
146
+
147
+ let fields: Record<string, unknown> = {}
148
+ const schema = resolvedType && resolvedType !== 'other' ? schemaFor(resolvedType, types) : undefined
149
+ if (schema && resolvedType) {
150
+ const extract = await step('extract', ({ signal }) =>
151
+ this.#wrapScanStep(async () => this.#core.ai.vision(await getDataUrl(), { schema, signal }), onDefect)
152
+ )
153
+ fields = parseFields(resolvedType, extract.annotation, types)
154
+ if (!markdown) markdown = extract.markdown
155
+ }
156
+
157
+ if (markdown.trim().length < 5 && Object.keys(fields).length === 0) {
158
+ const defect = buildScanError(
159
+ 'EMPTY_OR_UNREADABLE',
160
+ 'The uploaded document appears blank or unreadable (fewer than 5 characters detected). Please upload a clear, legible scan or photo of the document.'
161
+ )
162
+ onDefect(defect)
163
+ return this.#core.errors.nonRetryableError(defect.agentHint, defect.reason)
164
+ }
165
+
166
+ return { type: resolvedType ?? 'other', confidence, fields, markdown }
167
+ }
168
+
169
+ scan = async (
170
+ raw: ScanAttachmentInput,
171
+ opts?: { types?: AttachmentTypesMap; step?: StepRunner; preloadedFile?: SelectModel<'file'> }
172
+ ): Promise<ScanAttachmentOutput> => {
173
+ const step = opts?.step ?? this.#core.step
174
+ const types = opts?.types ?? getRegisteredAttachmentTypes()
175
+ const preloadedFile = opts?.preloadedFile
176
+ const { ref, type } = scanAttachmentInput.parse(raw)
177
+ if (type && type !== 'other' && Object.keys(types).length > 0 && !(type in types)) {
178
+ throw new Error(`Invalid attachment type: ${type}`)
179
+ }
180
+
181
+ return this.#core.internals.resilience.singleflight(
182
+ `scan:${this.#core.env.isDev ? 'dev' : 'prod'}:${ref}:${type ?? ''}`,
183
+ async () => {
184
+ const entry =
185
+ preloadedFile ?? (await step('read-meta', () => this.#core.db.read('file', ref, false, { bypassRbac: true })))
186
+ if (!entry) throw new Error(`File not found: ${ref}`)
187
+ if (entry.scan) return scanAttachmentOutput.parse(entry.scan)
188
+ if (entry.scanError) {
189
+ return this.#core.errors.nonRetryableError(entry.scanError.agentHint, entry.scanError.reason)
190
+ }
191
+
192
+ const shaSum = entry.shaSum || ref.slice(-24)
193
+ const crossUserMatch = await this.#findCrossUserByShaSum(ref, shaSum, type)
194
+ if (crossUserMatch?.scan) {
195
+ await step('persist', () =>
196
+ this.#core.db.update('file', ref, { scan: crossUserMatch.scan!, scanError: null }, { bypassRbac: true })
197
+ )
198
+ return scanAttachmentOutput.parse(crossUserMatch.scan)
199
+ }
200
+ if (crossUserMatch?.scanError) {
201
+ await this.#core.db
202
+ .update('file', ref, { scanError: crossUserMatch.scanError, scan: null }, { bypassRbac: true })
203
+ .catch(() => {})
204
+ return this.#core.errors.nonRetryableError(
205
+ crossUserMatch.scanError.agentHint,
206
+ crossUserMatch.scanError.reason
207
+ )
208
+ }
209
+
210
+ let capturedDefect: FileScanError | undefined
211
+ try {
212
+ const scan = await this.#executeScanOcrAndClassify({
213
+ ref,
214
+ contentType: entry.contentType || 'application/pdf',
215
+ initialType: type,
216
+ types,
217
+ step,
218
+ onDefect: (d) => {
219
+ capturedDefect = d
220
+ }
221
+ })
222
+ await step('persist', () =>
223
+ this.#core.db.update('file', ref, { scan, scanError: null }, { bypassRbac: true })
224
+ )
225
+ return scanAttachmentOutput.parse(scan)
226
+ } catch (err) {
227
+ const defect = capturedDefect ?? classifyPermanentScanDefect(err)
228
+ if (defect) {
229
+ await this.#core.db
230
+ .update('file', ref, { scanError: defect, scan: null }, { bypassRbac: true })
231
+ .catch(() => {})
232
+ if (err instanceof this.#core.errors.NonRetryableError) throw err
233
+ return this.#core.errors.nonRetryableError(defect.agentHint, defect.reason)
234
+ }
235
+ throw err
236
+ }
237
+ }
238
+ )
239
+ }
240
+
241
+ #scanShortCircuit = async ({ input }: { input: ScanAttachmentInput }): Promise<ScanAttachmentOutput | undefined> => {
242
+ const entry = await this.#core.db.read('file', input.ref, false, { bypassRbac: true })
243
+ if (entry?.scan) return scanAttachmentOutput.parse(entry.scan)
244
+ if (entry?.scanError) {
245
+ return this.#core.errors.nonRetryableError(entry.scanError.agentHint, entry.scanError.reason)
246
+ }
247
+ return undefined
248
+ }
249
+
250
+ #scanTask = async ({
251
+ input,
252
+ step,
253
+ types,
254
+ preloadedFile
255
+ }: {
256
+ input: ScanAttachmentInput
257
+ step: StepRunner
258
+ types?: AttachmentTypesMap
259
+ preloadedFile?: SelectModel<'file'>
260
+ }): Promise<ProcessedAttachment> => {
261
+ try {
262
+ return { ref: input.ref, scan: await this.scan(input, { step, types, preloadedFile }) }
263
+ } catch (err) {
264
+ if (this.#core.errors.isNonRetryableError(err)) {
265
+ return { ref: input.ref, error: err instanceof Error ? err.message : String(err) }
266
+ }
267
+ return { ref: input.ref, error: 'Temporary scanning service unavailability. Please retry.' }
268
+ }
269
+ }
270
+
271
+ process = async (
272
+ raw: ProcessAttachmentsInput,
273
+ opts?: { types?: AttachmentTypesMap; step?: StepRunner }
274
+ ): Promise<ProcessAttachmentsOutput> => {
275
+ const tasks = extractAttachmentTasks(raw)
276
+ const step = opts?.step ?? this.#core.step
277
+
278
+ const uniqueRefs = [...new Set(tasks.map((t) => t.ref))]
279
+ const existingFiles = await this.#core.db.list('file', { ids: uniqueRefs }, { bypassRbac: true })
280
+ const fileMap = new Map<FileRef, SelectModel<'file'>>(existingFiles.map((f) => [f.id, f]))
281
+
282
+ const attachments = await step.map(
283
+ 'process-attachments',
284
+ tasks,
285
+ (taskInput) =>
286
+ this.#scanTask({
287
+ input: taskInput,
288
+ step: this.#core.internals.workflows.createStepRunner('workflow'),
289
+ types: opts?.types,
290
+ preloadedFile: fileMap.get(taskInput.ref)
291
+ }),
292
+ {
293
+ timeout: `${this.#core.config.tasks.scanDeadlineSeconds}s`,
294
+ shortCircuit: (taskInput) => {
295
+ const fileDoc = fileMap.get(taskInput.ref)
296
+ if (fileDoc?.scan) {
297
+ return { ref: taskInput.ref, scan: scanAttachmentOutput.parse(fileDoc.scan) }
298
+ }
299
+ if (fileDoc?.scanError) {
300
+ return { ref: taskInput.ref, error: fileDoc.scanError.agentHint }
301
+ }
302
+ return undefined
303
+ }
304
+ }
305
+ )
306
+
307
+ return processAttachmentsOutput.parse({ attachments })
308
+ }
309
+
310
+ get internals() {
311
+ return {
312
+ scanShortCircuit: this.#scanShortCircuit
313
+ }
314
+ }
315
+ }
@@ -0,0 +1,115 @@
1
+ import { z } from 'zod'
2
+ import type { FileScanError, UnparseableReason } from '../files'
3
+ import {
4
+ getRegisteredAttachmentTypes,
5
+ processAttachmentsInput,
6
+ type AttachmentTypesMap,
7
+ type ProcessAttachmentsInput,
8
+ type ScanAttachmentInput
9
+ } from './schema'
10
+
11
+ export const schemaFor = (
12
+ type: string,
13
+ types: AttachmentTypesMap = getRegisteredAttachmentTypes()
14
+ ): Record<string, unknown> | undefined =>
15
+ type in types ? (z.toJSONSchema(types[type]!.schema, { io: 'output' }) as Record<string, unknown>) : undefined
16
+
17
+ export const parseFields = (
18
+ type: string,
19
+ fields: Record<string, unknown>,
20
+ types: AttachmentTypesMap = getRegisteredAttachmentTypes()
21
+ ): Record<string, unknown> => (type in types ? (types[type]!.schema.parse(fields) as Record<string, unknown>) : {})
22
+
23
+ export const extractAttachmentTasks = (input: ProcessAttachmentsInput): ScanAttachmentInput[] =>
24
+ processAttachmentsInput.parse(input).refs.map((item) => (typeof item === 'string' ? { ref: item } : item))
25
+
26
+ export const buildScanError = (
27
+ reason: UnparseableReason,
28
+ agentHint: string,
29
+ diagnosticMessage?: string
30
+ ): FileScanError => ({
31
+ status: 'unparseable',
32
+ reason,
33
+ agentHint,
34
+ ...(diagnosticMessage ? { diagnosticMessage: diagnosticMessage.slice(0, 500) } : {}),
35
+ failedAt: new Date().toISOString()
36
+ })
37
+
38
+ export const extractHttpStatus = (err: unknown): number | undefined => {
39
+ if (!err || typeof err !== 'object') return undefined
40
+ const rec = err as Record<string, unknown>
41
+ if (typeof rec.statusCode === 'number') return rec.statusCode
42
+ if (typeof rec.status === 'number') return rec.status
43
+ const msg = err instanceof Error ? err.message : String(err)
44
+ const match = /\b(?:status(?:\s+code)?|http)\s*[:=]?\s*(4\d\d|5\d\d)\b/i.exec(msg)
45
+ return match?.[1] ? Number(match[1]) : undefined
46
+ }
47
+
48
+ const TRANSIENT_TOKENS = ['aborted', 'timed out', 'econnreset', 'etimedout', 'socket hang up', 'rate limit']
49
+
50
+ const DEFECT_RULES: ReadonlyArray<{
51
+ reason: UnparseableReason
52
+ hint: string
53
+ match: (status: number | undefined, lower: string) => boolean
54
+ }> = [
55
+ {
56
+ reason: 'PASSWORD_PROTECTED',
57
+ hint: 'This document is password-protected or encrypted and cannot be read. Please upload an unlocked copy of the document.',
58
+ match: (_, lower) => lower.includes('password') || lower.includes('encrypted')
59
+ },
60
+ {
61
+ reason: 'UNSUPPORTED_FORMAT',
62
+ hint: 'This file format is not supported by the document scanner. Please upload a valid PDF or standard image (JPEG, PNG, WebP).',
63
+ match: (status, lower) =>
64
+ status === 415 || lower.includes('unsupported media') || lower.includes('unsupported format')
65
+ },
66
+ {
67
+ reason: 'EMPTY_OR_UNREADABLE',
68
+ hint: 'The uploaded document appears blank or unreadable. Please upload a clear, legible scan or photo of the document.',
69
+ match: (_, lower) =>
70
+ lower.includes('empty description') ||
71
+ lower.includes('empty_or_unreadable') ||
72
+ lower.includes('no readable text')
73
+ },
74
+ {
75
+ reason: 'CLASSIFICATION_REJECTED',
76
+ hint: 'The document could not be classified into a valid document type. Please upload a clear, complete document.',
77
+ match: (_, lower) => lower.includes('classification_rejected')
78
+ },
79
+ {
80
+ reason: 'CORRUPTED_FILE',
81
+ hint: 'This file appears corrupted or malformed and could not be parsed. Please export or re-upload a valid PDF or image file.',
82
+ match: (status, lower) =>
83
+ status === 400 ||
84
+ status === 422 ||
85
+ lower.includes('corrupt') ||
86
+ lower.includes('malformed') ||
87
+ lower.includes('invalid pdf') ||
88
+ lower.includes('invalid image') ||
89
+ lower.includes('failed to process document') ||
90
+ lower.includes('could not parse')
91
+ }
92
+ ]
93
+
94
+ export const classifyPermanentScanDefect = (err: unknown): FileScanError | null => {
95
+ if (!err) return null
96
+ const bodyStr =
97
+ typeof err === 'object' && 'body' in err && typeof (err as { body?: unknown }).body === 'string'
98
+ ? (err as { body: string }).body
99
+ : ''
100
+ const msg = `${err instanceof Error ? err.message : String(err)} ${bodyStr}`.trim()
101
+ const lower = msg.toLowerCase()
102
+ const status = extractHttpStatus(err)
103
+
104
+ if (
105
+ status === 408 ||
106
+ status === 429 ||
107
+ (status !== undefined && status >= 500) ||
108
+ TRANSIENT_TOKENS.some((t) => lower.includes(t))
109
+ ) {
110
+ return null
111
+ }
112
+
113
+ const matched = DEFECT_RULES.find((rule) => rule.match(status, lower))
114
+ return matched ? buildScanError(matched.reason, matched.hint, msg) : null
115
+ }
@@ -0,0 +1,3 @@
1
+ export * from './schema'
2
+ export { AuthService } from './service'
3
+ export { coreAuthRoutes } from './routes'