@shipfox/api-logs 2.0.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 (342) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +179 -0
  5. package/LICENSE +21 -0
  6. package/README.md +151 -0
  7. package/dist/api/object-storage.d.ts +69 -0
  8. package/dist/api/object-storage.d.ts.map +1 -0
  9. package/dist/api/object-storage.js +199 -0
  10. package/dist/api/object-storage.js.map +1 -0
  11. package/dist/config.d.ts +20 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +108 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/core/append-logs.d.ts +28 -0
  16. package/dist/core/append-logs.d.ts.map +1 -0
  17. package/dist/core/append-logs.js +318 -0
  18. package/dist/core/append-logs.js.map +1 -0
  19. package/dist/core/budget.d.ts +12 -0
  20. package/dist/core/budget.d.ts.map +1 -0
  21. package/dist/core/budget.js +9 -0
  22. package/dist/core/budget.js.map +1 -0
  23. package/dist/core/close-stream.d.ts +27 -0
  24. package/dist/core/close-stream.d.ts.map +1 -0
  25. package/dist/core/close-stream.js +59 -0
  26. package/dist/core/close-stream.js.map +1 -0
  27. package/dist/core/compaction.d.ts +28 -0
  28. package/dist/core/compaction.d.ts.map +1 -0
  29. package/dist/core/compaction.js +49 -0
  30. package/dist/core/compaction.js.map +1 -0
  31. package/dist/core/entities/attempt-stream.d.ts +37 -0
  32. package/dist/core/entities/attempt-stream.d.ts.map +1 -0
  33. package/dist/core/entities/attempt-stream.js +15 -0
  34. package/dist/core/entities/attempt-stream.js.map +1 -0
  35. package/dist/core/entities/job-accounting.d.ts +12 -0
  36. package/dist/core/entities/job-accounting.d.ts.map +1 -0
  37. package/dist/core/entities/job-accounting.js +6 -0
  38. package/dist/core/entities/job-accounting.js.map +1 -0
  39. package/dist/core/entities/log-object.d.ts +13 -0
  40. package/dist/core/entities/log-object.d.ts.map +1 -0
  41. package/dist/core/entities/log-object.js +9 -0
  42. package/dist/core/entities/log-object.js.map +1 -0
  43. package/dist/core/errors.d.ts +26 -0
  44. package/dist/core/errors.d.ts.map +1 -0
  45. package/dist/core/errors.js +31 -0
  46. package/dist/core/errors.js.map +1 -0
  47. package/dist/core/finalize-attempt-stream.d.ts +22 -0
  48. package/dist/core/finalize-attempt-stream.d.ts.map +1 -0
  49. package/dist/core/finalize-attempt-stream.js +48 -0
  50. package/dist/core/finalize-attempt-stream.js.map +1 -0
  51. package/dist/core/read-logs.d.ts +33 -0
  52. package/dist/core/read-logs.d.ts.map +1 -0
  53. package/dist/core/read-logs.js +49 -0
  54. package/dist/core/read-logs.js.map +1 -0
  55. package/dist/core/reap-stale-open-streams.d.ts +27 -0
  56. package/dist/core/reap-stale-open-streams.d.ts.map +1 -0
  57. package/dist/core/reap-stale-open-streams.js +50 -0
  58. package/dist/core/reap-stale-open-streams.js.map +1 -0
  59. package/dist/core/retention.d.ts +32 -0
  60. package/dist/core/retention.d.ts.map +1 -0
  61. package/dist/core/retention.js +148 -0
  62. package/dist/core/retention.js.map +1 -0
  63. package/dist/core/session/claude/rows.d.ts +6 -0
  64. package/dist/core/session/claude/rows.d.ts.map +1 -0
  65. package/dist/core/session/claude/rows.js +145 -0
  66. package/dist/core/session/claude/rows.js.map +1 -0
  67. package/dist/core/session/claude-parser.d.ts +4 -0
  68. package/dist/core/session/claude-parser.d.ts.map +1 -0
  69. package/dist/core/session/claude-parser.js +42 -0
  70. package/dist/core/session/claude-parser.js.map +1 -0
  71. package/dist/core/session/entry-schema.d.ts +51 -0
  72. package/dist/core/session/entry-schema.d.ts.map +1 -0
  73. package/dist/core/session/entry-schema.js +61 -0
  74. package/dist/core/session/entry-schema.js.map +1 -0
  75. package/dist/core/session/object.d.ts +12 -0
  76. package/dist/core/session/object.d.ts.map +1 -0
  77. package/dist/core/session/object.js +50 -0
  78. package/dist/core/session/object.js.map +1 -0
  79. package/dist/core/session/parse-session.d.ts +5 -0
  80. package/dist/core/session/parse-session.d.ts.map +1 -0
  81. package/dist/core/session/parse-session.js +18 -0
  82. package/dist/core/session/parse-session.js.map +1 -0
  83. package/dist/core/session/pi/message-rows.d.ts +4 -0
  84. package/dist/core/session/pi/message-rows.d.ts.map +1 -0
  85. package/dist/core/session/pi/message-rows.js +113 -0
  86. package/dist/core/session/pi/message-rows.js.map +1 -0
  87. package/dist/core/session/pi/metadata.d.ts +18 -0
  88. package/dist/core/session/pi/metadata.d.ts.map +1 -0
  89. package/dist/core/session/pi/metadata.js +120 -0
  90. package/dist/core/session/pi/metadata.js.map +1 -0
  91. package/dist/core/session/pi/text.d.ts +12 -0
  92. package/dist/core/session/pi/text.d.ts.map +1 -0
  93. package/dist/core/session/pi/text.js +59 -0
  94. package/dist/core/session/pi/text.js.map +1 -0
  95. package/dist/core/session/pi-parser.d.ts +5 -0
  96. package/dist/core/session/pi-parser.d.ts.map +1 -0
  97. package/dist/core/session/pi-parser.js +73 -0
  98. package/dist/core/session/pi-parser.js.map +1 -0
  99. package/dist/core/session/rows.d.ts +7 -0
  100. package/dist/core/session/rows.d.ts.map +1 -0
  101. package/dist/core/session/rows.js +39 -0
  102. package/dist/core/session/rows.js.map +1 -0
  103. package/dist/core/session/session-record.d.ts +5 -0
  104. package/dist/core/session/session-record.d.ts.map +1 -0
  105. package/dist/core/session/session-record.js +3 -0
  106. package/dist/core/session/session-record.js.map +1 -0
  107. package/dist/db/accounting.d.ts +33 -0
  108. package/dist/db/accounting.d.ts.map +1 -0
  109. package/dist/db/accounting.js +52 -0
  110. package/dist/db/accounting.js.map +1 -0
  111. package/dist/db/chunks.d.ts +62 -0
  112. package/dist/db/chunks.d.ts.map +1 -0
  113. package/dist/db/chunks.js +88 -0
  114. package/dist/db/chunks.js.map +1 -0
  115. package/dist/db/db.d.ts +752 -0
  116. package/dist/db/db.d.ts.map +1 -0
  117. package/dist/db/db.js +24 -0
  118. package/dist/db/db.js.map +1 -0
  119. package/dist/db/index.d.ts +2 -0
  120. package/dist/db/index.d.ts.map +1 -0
  121. package/dist/db/index.js +5 -0
  122. package/dist/db/index.js.map +1 -0
  123. package/dist/db/schema/attempt-streams.d.ts +304 -0
  124. package/dist/db/schema/attempt-streams.d.ts.map +1 -0
  125. package/dist/db/schema/attempt-streams.js +100 -0
  126. package/dist/db/schema/attempt-streams.js.map +1 -0
  127. package/dist/db/schema/chunks.d.ts +166 -0
  128. package/dist/db/schema/chunks.d.ts.map +1 -0
  129. package/dist/db/schema/chunks.js +47 -0
  130. package/dist/db/schema/chunks.js.map +1 -0
  131. package/dist/db/schema/common.d.ts +29 -0
  132. package/dist/db/schema/common.d.ts.map +1 -0
  133. package/dist/db/schema/common.js +11 -0
  134. package/dist/db/schema/common.js.map +1 -0
  135. package/dist/db/schema/job-accounting.d.ts +143 -0
  136. package/dist/db/schema/job-accounting.d.ts.map +1 -0
  137. package/dist/db/schema/job-accounting.js +43 -0
  138. package/dist/db/schema/job-accounting.js.map +1 -0
  139. package/dist/db/schema/outbox.d.ts +195 -0
  140. package/dist/db/schema/outbox.d.ts.map +1 -0
  141. package/dist/db/schema/outbox.js +5 -0
  142. package/dist/db/schema/outbox.js.map +1 -0
  143. package/dist/db/streams.d.ts +141 -0
  144. package/dist/db/streams.d.ts.map +1 -0
  145. package/dist/db/streams.js +197 -0
  146. package/dist/db/streams.js.map +1 -0
  147. package/dist/index.d.ts +4 -0
  148. package/dist/index.d.ts.map +1 -0
  149. package/dist/index.js +81 -0
  150. package/dist/index.js.map +1 -0
  151. package/dist/metrics/index.d.ts +2 -0
  152. package/dist/metrics/index.d.ts.map +1 -0
  153. package/dist/metrics/index.js +3 -0
  154. package/dist/metrics/index.js.map +1 -0
  155. package/dist/metrics/instance.d.ts +14 -0
  156. package/dist/metrics/instance.d.ts.map +1 -0
  157. package/dist/metrics/instance.js +16 -0
  158. package/dist/metrics/instance.js.map +1 -0
  159. package/dist/metrics/service.d.ts +2 -0
  160. package/dist/metrics/service.d.ts.map +1 -0
  161. package/dist/metrics/service.js +20 -0
  162. package/dist/metrics/service.js.map +1 -0
  163. package/dist/presentation/dto/read-logs.d.ts +4 -0
  164. package/dist/presentation/dto/read-logs.d.ts.map +1 -0
  165. package/dist/presentation/dto/read-logs.js +22 -0
  166. package/dist/presentation/dto/read-logs.js.map +1 -0
  167. package/dist/presentation/routes/append-logs.d.ts +2 -0
  168. package/dist/presentation/routes/append-logs.d.ts.map +1 -0
  169. package/dist/presentation/routes/append-logs.js +71 -0
  170. package/dist/presentation/routes/append-logs.js.map +1 -0
  171. package/dist/presentation/routes/index.d.ts +3 -0
  172. package/dist/presentation/routes/index.d.ts.map +1 -0
  173. package/dist/presentation/routes/index.js +32 -0
  174. package/dist/presentation/routes/index.js.map +1 -0
  175. package/dist/presentation/routes/read-logs.d.ts +2 -0
  176. package/dist/presentation/routes/read-logs.d.ts.map +1 -0
  177. package/dist/presentation/routes/read-logs.js +41 -0
  178. package/dist/presentation/routes/read-logs.js.map +1 -0
  179. package/dist/presentation/subscribers/on-job-terminated.d.ts +8 -0
  180. package/dist/presentation/subscribers/on-job-terminated.d.ts.map +1 -0
  181. package/dist/presentation/subscribers/on-job-terminated.js +32 -0
  182. package/dist/presentation/subscribers/on-job-terminated.js.map +1 -0
  183. package/dist/presentation/subscribers/on-log-stream-closed.d.ts +9 -0
  184. package/dist/presentation/subscribers/on-log-stream-closed.d.ts.map +1 -0
  185. package/dist/presentation/subscribers/on-log-stream-closed.js +31 -0
  186. package/dist/presentation/subscribers/on-log-stream-closed.js.map +1 -0
  187. package/dist/presentation/subscribers/on-step-attempt-terminated.d.ts +3 -0
  188. package/dist/presentation/subscribers/on-step-attempt-terminated.d.ts.map +1 -0
  189. package/dist/presentation/subscribers/on-step-attempt-terminated.js +14 -0
  190. package/dist/presentation/subscribers/on-step-attempt-terminated.js.map +1 -0
  191. package/dist/temporal/activities/close-abandoned-streams.d.ts +12 -0
  192. package/dist/temporal/activities/close-abandoned-streams.d.ts.map +1 -0
  193. package/dist/temporal/activities/close-abandoned-streams.js +33 -0
  194. package/dist/temporal/activities/close-abandoned-streams.js.map +1 -0
  195. package/dist/temporal/activities/compact-stream.d.ts +33 -0
  196. package/dist/temporal/activities/compact-stream.d.ts.map +1 -0
  197. package/dist/temporal/activities/compact-stream.js +95 -0
  198. package/dist/temporal/activities/compact-stream.js.map +1 -0
  199. package/dist/temporal/activities/compaction-reconcile.d.ts +16 -0
  200. package/dist/temporal/activities/compaction-reconcile.d.ts.map +1 -0
  201. package/dist/temporal/activities/compaction-reconcile.js +52 -0
  202. package/dist/temporal/activities/compaction-reconcile.js.map +1 -0
  203. package/dist/temporal/activities/index.d.ts +14 -0
  204. package/dist/temporal/activities/index.d.ts.map +1 -0
  205. package/dist/temporal/activities/index.js +16 -0
  206. package/dist/temporal/activities/index.js.map +1 -0
  207. package/dist/temporal/activities/reap-stale-open-streams.d.ts +4 -0
  208. package/dist/temporal/activities/reap-stale-open-streams.d.ts.map +1 -0
  209. package/dist/temporal/activities/reap-stale-open-streams.js +11 -0
  210. package/dist/temporal/activities/reap-stale-open-streams.js.map +1 -0
  211. package/dist/temporal/activities/retention-sweep.d.ts +7 -0
  212. package/dist/temporal/activities/retention-sweep.d.ts.map +1 -0
  213. package/dist/temporal/activities/retention-sweep.js +19 -0
  214. package/dist/temporal/activities/retention-sweep.js.map +1 -0
  215. package/dist/temporal/constants.d.ts +7 -0
  216. package/dist/temporal/constants.d.ts.map +1 -0
  217. package/dist/temporal/constants.js +13 -0
  218. package/dist/temporal/constants.js.map +1 -0
  219. package/dist/temporal/workflows/close-abandoned-streams.d.ts +11 -0
  220. package/dist/temporal/workflows/close-abandoned-streams.d.ts.map +1 -0
  221. package/dist/temporal/workflows/close-abandoned-streams.js +22 -0
  222. package/dist/temporal/workflows/close-abandoned-streams.js.map +1 -0
  223. package/dist/temporal/workflows/compact-stream.d.ts +6 -0
  224. package/dist/temporal/workflows/compact-stream.d.ts.map +1 -0
  225. package/dist/temporal/workflows/compact-stream.js +25 -0
  226. package/dist/temporal/workflows/compact-stream.js.map +1 -0
  227. package/dist/temporal/workflows/compaction-reconcile-cron.d.ts +3 -0
  228. package/dist/temporal/workflows/compaction-reconcile-cron.d.ts.map +1 -0
  229. package/dist/temporal/workflows/compaction-reconcile-cron.js +15 -0
  230. package/dist/temporal/workflows/compaction-reconcile-cron.js.map +1 -0
  231. package/dist/temporal/workflows/index.d.ts +6 -0
  232. package/dist/temporal/workflows/index.d.ts.map +1 -0
  233. package/dist/temporal/workflows/index.js +7 -0
  234. package/dist/temporal/workflows/index.js.map +1 -0
  235. package/dist/temporal/workflows/reap-stale-open-streams-cron.d.ts +2 -0
  236. package/dist/temporal/workflows/reap-stale-open-streams-cron.d.ts.map +1 -0
  237. package/dist/temporal/workflows/reap-stale-open-streams-cron.js +15 -0
  238. package/dist/temporal/workflows/reap-stale-open-streams-cron.js.map +1 -0
  239. package/dist/temporal/workflows/retention-sweep-cron.d.ts +2 -0
  240. package/dist/temporal/workflows/retention-sweep-cron.d.ts.map +1 -0
  241. package/dist/temporal/workflows/retention-sweep-cron.js +23 -0
  242. package/dist/temporal/workflows/retention-sweep-cron.js.map +1 -0
  243. package/dist/tsconfig.test.tsbuildinfo +1 -0
  244. package/drizzle/0000_initial.sql +65 -0
  245. package/drizzle/meta/0000_snapshot.json +529 -0
  246. package/drizzle/meta/_journal.json +13 -0
  247. package/drizzle.config.ts +7 -0
  248. package/package.json +84 -0
  249. package/src/api/object-storage-credentials.test.ts +70 -0
  250. package/src/api/object-storage.test.ts +64 -0
  251. package/src/api/object-storage.ts +226 -0
  252. package/src/config.test.ts +118 -0
  253. package/src/config.ts +130 -0
  254. package/src/core/append-logs.test.ts +563 -0
  255. package/src/core/append-logs.ts +384 -0
  256. package/src/core/budget.test.ts +30 -0
  257. package/src/core/budget.ts +19 -0
  258. package/src/core/close-stream.ts +72 -0
  259. package/src/core/compaction.ts +68 -0
  260. package/src/core/entities/attempt-stream.ts +38 -0
  261. package/src/core/entities/job-accounting.ts +11 -0
  262. package/src/core/entities/log-object.test.ts +25 -0
  263. package/src/core/entities/log-object.ts +18 -0
  264. package/src/core/errors.ts +37 -0
  265. package/src/core/finalize-attempt-stream.test.ts +90 -0
  266. package/src/core/finalize-attempt-stream.ts +59 -0
  267. package/src/core/read-logs.ts +77 -0
  268. package/src/core/reap-stale-open-streams.test.ts +137 -0
  269. package/src/core/reap-stale-open-streams.ts +60 -0
  270. package/src/core/retention.test.ts +388 -0
  271. package/src/core/retention.ts +184 -0
  272. package/src/core/session/claude/rows.ts +223 -0
  273. package/src/core/session/claude-parser.test.ts +199 -0
  274. package/src/core/session/claude-parser.ts +38 -0
  275. package/src/core/session/entry-schema.ts +82 -0
  276. package/src/core/session/object.ts +57 -0
  277. package/src/core/session/parse-session.ts +23 -0
  278. package/src/core/session/pi/message-rows.ts +211 -0
  279. package/src/core/session/pi/metadata.ts +148 -0
  280. package/src/core/session/pi/text.ts +83 -0
  281. package/src/core/session/pi-parser.test.ts +451 -0
  282. package/src/core/session/pi-parser.ts +107 -0
  283. package/src/core/session/rows.ts +38 -0
  284. package/src/core/session/session-record.ts +4 -0
  285. package/src/db/accounting.ts +81 -0
  286. package/src/db/chunks.ts +143 -0
  287. package/src/db/db.ts +22 -0
  288. package/src/db/index.ts +4 -0
  289. package/src/db/schema/attempt-streams.ts +106 -0
  290. package/src/db/schema/chunks.ts +43 -0
  291. package/src/db/schema/common.ts +12 -0
  292. package/src/db/schema/job-accounting.ts +38 -0
  293. package/src/db/schema/outbox.ts +4 -0
  294. package/src/db/streams.test.ts +47 -0
  295. package/src/db/streams.ts +368 -0
  296. package/src/index.ts +77 -0
  297. package/src/metrics/index.ts +1 -0
  298. package/src/metrics/instance.ts +33 -0
  299. package/src/metrics/service.ts +23 -0
  300. package/src/presentation/dto/read-logs.ts +24 -0
  301. package/src/presentation/routes/append-logs.test.ts +279 -0
  302. package/src/presentation/routes/append-logs.ts +66 -0
  303. package/src/presentation/routes/index.ts +27 -0
  304. package/src/presentation/routes/read-logs.test.ts +504 -0
  305. package/src/presentation/routes/read-logs.ts +38 -0
  306. package/src/presentation/subscribers/on-job-terminated.test.ts +64 -0
  307. package/src/presentation/subscribers/on-job-terminated.ts +26 -0
  308. package/src/presentation/subscribers/on-log-stream-closed.test.ts +63 -0
  309. package/src/presentation/subscribers/on-log-stream-closed.ts +26 -0
  310. package/src/presentation/subscribers/on-step-attempt-terminated.ts +16 -0
  311. package/src/temporal/activities/close-abandoned-streams.test.ts +84 -0
  312. package/src/temporal/activities/close-abandoned-streams.ts +30 -0
  313. package/src/temporal/activities/compact-stream.test.ts +221 -0
  314. package/src/temporal/activities/compact-stream.ts +123 -0
  315. package/src/temporal/activities/compaction-reconcile.test.ts +143 -0
  316. package/src/temporal/activities/compaction-reconcile.ts +48 -0
  317. package/src/temporal/activities/index.ts +15 -0
  318. package/src/temporal/activities/reap-stale-open-streams.test.ts +48 -0
  319. package/src/temporal/activities/reap-stale-open-streams.ts +14 -0
  320. package/src/temporal/activities/retention-sweep.test.ts +38 -0
  321. package/src/temporal/activities/retention-sweep.ts +23 -0
  322. package/src/temporal/constants.ts +14 -0
  323. package/src/temporal/workflows/close-abandoned-streams.ts +25 -0
  324. package/src/temporal/workflows/compact-stream.ts +26 -0
  325. package/src/temporal/workflows/compaction-reconcile-cron.ts +14 -0
  326. package/src/temporal/workflows/index.ts +5 -0
  327. package/src/temporal/workflows/reap-stale-open-streams-cron.ts +13 -0
  328. package/src/temporal/workflows/retention-sweep-cron.ts +23 -0
  329. package/test/env.ts +37 -0
  330. package/test/factories/job-accounting.ts +38 -0
  331. package/test/fixtures/closed-stream.ts +53 -0
  332. package/test/fixtures/lease-token.ts +42 -0
  333. package/test/fixtures/ndjson.ts +34 -0
  334. package/test/globalSetup.ts +44 -0
  335. package/test/index.ts +9 -0
  336. package/test/queries.ts +54 -0
  337. package/test/setup.ts +17 -0
  338. package/tsconfig.build.json +9 -0
  339. package/tsconfig.build.tsbuildinfo +1 -0
  340. package/tsconfig.json +3 -0
  341. package/tsconfig.test.json +8 -0
  342. package/vitest.config.ts +13 -0
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@shipfox/api-logs",
3
+ "license": "MIT",
4
+ "version": "2.0.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
8
+ "directory": "libs/api/logs"
9
+ },
10
+ "private": false,
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "imports": {
15
+ "#test/*": "./test/*",
16
+ "#*": {
17
+ "workspace-source": "./src/*",
18
+ "development": "./src/*",
19
+ "default": "./dist/*"
20
+ }
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "development": {
25
+ "types": "./src/index.ts",
26
+ "default": "./src/index.ts"
27
+ },
28
+ "default": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ }
33
+ },
34
+ "dependencies": {
35
+ "@aws-sdk/client-s3": "^3.1068.0",
36
+ "@aws-sdk/lib-storage": "^3.1068.0",
37
+ "@aws-sdk/s3-request-presigner": "^3.1068.0",
38
+ "@temporalio/activity": "^1.16.1",
39
+ "@temporalio/workflow": "^1.16.1",
40
+ "drizzle-orm": "^0.45.2",
41
+ "zod": "^4.4.3",
42
+ "@shipfox/api-auth-context": "2.0.0",
43
+ "@shipfox/api-workflows-dto": "2.0.0",
44
+ "@shipfox/api-logs-dto": "2.0.0",
45
+ "@shipfox/config": "1.2.1",
46
+ "@shipfox/node-drizzle": "0.2.1",
47
+ "@shipfox/node-fastify": "0.2.1",
48
+ "@shipfox/api-agent-dto": "2.0.0",
49
+ "@shipfox/node-module": "0.2.0",
50
+ "@shipfox/node-opentelemetry": "0.5.0",
51
+ "@shipfox/node-postgres": "0.4.1",
52
+ "@shipfox/node-outbox": "0.2.1",
53
+ "@shipfox/node-temporal": "0.2.0",
54
+ "@shipfox/api-workflows": "2.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "@temporalio/client": "^1.16.1",
58
+ "@temporalio/common": "^1.16.1",
59
+ "@temporalio/testing": "^1.16.1",
60
+ "@temporalio/worker": "^1.16.1",
61
+ "@types/pg": "^8.15.5",
62
+ "drizzle-kit": "^0.31.10",
63
+ "fastify": "^5.3.3",
64
+ "fastify-type-provider-zod": "^6.0.0",
65
+ "fishery": "^2.2.2",
66
+ "@shipfox/api-auth-dto": "2.0.0",
67
+ "@shipfox/biome": "1.8.1",
68
+ "@shipfox/api-auth": "2.0.0",
69
+ "@shipfox/swc": "1.2.5",
70
+ "@shipfox/node-jwt": "0.2.0",
71
+ "@shipfox/ts-config": "1.3.8",
72
+ "@shipfox/vitest": "1.2.2",
73
+ "@shipfox/typescript": "1.1.6"
74
+ },
75
+ "scripts": {
76
+ "build": "shipfox-swc",
77
+ "check": "shipfox-biome-check",
78
+ "check:fix": "shipfox-biome-check --write",
79
+ "test": "shipfox-vitest-run",
80
+ "test:watch": "shipfox-vitest-watch",
81
+ "type": "shipfox-tsc-check",
82
+ "type:emit": "shipfox-tsc-emit"
83
+ }
84
+ }
@@ -0,0 +1,70 @@
1
+ afterEach(() => {
2
+ vi.unstubAllEnvs();
3
+ vi.resetModules();
4
+ });
5
+
6
+ function useAmbientCredentials(): void {
7
+ vi.stubEnv('LOG_STORAGE_S3_ACCESS_KEY_ID', undefined);
8
+ vi.stubEnv('LOG_STORAGE_S3_SECRET_ACCESS_KEY', undefined);
9
+ vi.stubEnv('AWS_PROFILE', undefined);
10
+ vi.stubEnv('AWS_ACCESS_KEY_ID', 'ambient-access-key');
11
+ vi.stubEnv('AWS_SECRET_ACCESS_KEY', 'ambient-secret-key');
12
+ vi.stubEnv('AWS_SESSION_TOKEN', 'ambient-session-token');
13
+ }
14
+
15
+ describe('S3 client credentials', () => {
16
+ it('uses one explicit credential pair for normal and multipart clients', async () => {
17
+ vi.stubEnv('LOG_STORAGE_S3_ACCESS_KEY_ID', 'explicit-access-key');
18
+ vi.stubEnv('LOG_STORAGE_S3_SECRET_ACCESS_KEY', 'explicit-secret-key');
19
+ vi.resetModules();
20
+ const {closeS3Client, closeUploadS3Client, s3Client, uploadS3Client} = await import(
21
+ './object-storage.js'
22
+ );
23
+
24
+ const credentials = await Promise.all([
25
+ s3Client().config.credentials(),
26
+ uploadS3Client().config.credentials(),
27
+ ]);
28
+
29
+ expect(credentials).toEqual([
30
+ expect.objectContaining({
31
+ accessKeyId: 'explicit-access-key',
32
+ secretAccessKey: 'explicit-secret-key',
33
+ }),
34
+ expect.objectContaining({
35
+ accessKeyId: 'explicit-access-key',
36
+ secretAccessKey: 'explicit-secret-key',
37
+ }),
38
+ ]);
39
+ closeS3Client();
40
+ closeUploadS3Client();
41
+ });
42
+
43
+ it('uses ambient AWS credentials for normal and multipart clients', async () => {
44
+ useAmbientCredentials();
45
+ vi.resetModules();
46
+ const {closeS3Client, closeUploadS3Client, s3Client, uploadS3Client} = await import(
47
+ './object-storage.js'
48
+ );
49
+
50
+ const credentials = await Promise.all([
51
+ s3Client().config.credentials(),
52
+ uploadS3Client().config.credentials(),
53
+ ]);
54
+
55
+ expect(credentials).toEqual([
56
+ expect.objectContaining({
57
+ accessKeyId: 'ambient-access-key',
58
+ secretAccessKey: 'ambient-secret-key',
59
+ sessionToken: 'ambient-session-token',
60
+ }),
61
+ expect.objectContaining({
62
+ accessKeyId: 'ambient-access-key',
63
+ secretAccessKey: 'ambient-secret-key',
64
+ sessionToken: 'ambient-session-token',
65
+ }),
66
+ ]);
67
+ closeS3Client();
68
+ closeUploadS3Client();
69
+ });
70
+ });
@@ -0,0 +1,64 @@
1
+ import {Buffer} from 'node:buffer';
2
+ import {Readable} from 'node:stream';
3
+ import {createGzip} from 'node:zlib';
4
+ import {HeadObjectCommand} from '@aws-sdk/client-s3';
5
+ import {config} from '#config.js';
6
+ import {deleteObject, putCompactedObject, s3Client} from './object-storage.js';
7
+
8
+ function testKey(): string {
9
+ return `logs/test/${crypto.randomUUID()}`;
10
+ }
11
+
12
+ async function objectExists(key: string): Promise<boolean> {
13
+ try {
14
+ await s3Client().send(new HeadObjectCommand({Bucket: config.LOG_STORAGE_S3_BUCKET, Key: key}));
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ describe('putCompactedObject', () => {
22
+ it('rejects without writing when the signal is already aborted', async () => {
23
+ const key = testKey();
24
+
25
+ const upload = putCompactedObject({
26
+ key,
27
+ body: Readable.from([Buffer.from('x')]).pipe(createGzip()),
28
+ metadata: {},
29
+ signal: AbortSignal.abort(),
30
+ });
31
+
32
+ await expect(upload).rejects.toThrow('aborted before it started');
33
+ expect(await objectExists(key)).toBe(false);
34
+ });
35
+
36
+ it('aborts the in-flight upload and rejects when the signal fires mid-stream', async () => {
37
+ const key = testKey();
38
+ const controller = new AbortController();
39
+ // Aborts as soon as the upload starts pulling, and never ends, so the upload is genuinely
40
+ // in flight (no complete part, no CompleteMultipartUpload) when the abort lands.
41
+ const body = new Readable({
42
+ read() {
43
+ this.push(Buffer.from('partial'));
44
+ controller.abort();
45
+ },
46
+ });
47
+
48
+ const upload = putCompactedObject({
49
+ key,
50
+ body: body.pipe(createGzip()),
51
+ metadata: {},
52
+ signal: controller.signal,
53
+ });
54
+
55
+ await expect(upload).rejects.toThrow();
56
+ expect(await objectExists(key)).toBe(false);
57
+ });
58
+ });
59
+
60
+ describe('deleteObject', () => {
61
+ it('resolves for a key that does not exist (idempotent)', async () => {
62
+ await expect(deleteObject(testKey())).resolves.toBeUndefined();
63
+ });
64
+ });
@@ -0,0 +1,226 @@
1
+ import type {Readable} from 'node:stream';
2
+ import {
3
+ DeleteObjectCommand,
4
+ DeleteObjectsCommand,
5
+ GetObjectCommand,
6
+ HeadBucketCommand,
7
+ ListObjectsV2Command,
8
+ S3Client,
9
+ } from '@aws-sdk/client-s3';
10
+ import {Upload} from '@aws-sdk/lib-storage';
11
+ import {getSignedUrl} from '@aws-sdk/s3-request-presigner';
12
+ import {config} from '#config.js';
13
+ import {type LogObjectKeyParams, logObjectKey} from '#core/entities/log-object.js';
14
+
15
+ const explicitS3Credentials =
16
+ config.LOG_STORAGE_S3_ACCESS_KEY_ID && config.LOG_STORAGE_S3_SECRET_ACCESS_KEY
17
+ ? {
18
+ accessKeyId: config.LOG_STORAGE_S3_ACCESS_KEY_ID,
19
+ secretAccessKey: config.LOG_STORAGE_S3_SECRET_ACCESS_KEY,
20
+ }
21
+ : undefined;
22
+
23
+ let _client: S3Client | undefined;
24
+
25
+ /** Lazily-built S3 client targeting the configured object store (Garage in dev). */
26
+ export function s3Client(): S3Client {
27
+ if (!_client) {
28
+ _client = new S3Client({
29
+ endpoint: config.LOG_STORAGE_S3_ENDPOINT,
30
+ region: config.LOG_STORAGE_S3_REGION,
31
+ forcePathStyle: config.LOG_STORAGE_S3_FORCE_PATH_STYLE,
32
+ ...(explicitS3Credentials ? {credentials: explicitS3Credentials} : {}),
33
+ // Fail fast: a slow or black-holed endpoint must not hang callers behind
34
+ // SDK backoff. One retry, short connect/request timeouts.
35
+ maxAttempts: 2,
36
+ requestHandler: {connectionTimeout: 1_000, requestTimeout: 3_000},
37
+ });
38
+ }
39
+ return _client;
40
+ }
41
+
42
+ export function closeS3Client(): void {
43
+ _client?.destroy();
44
+ _client = undefined;
45
+ }
46
+
47
+ /** Readiness probe: true when the configured bucket answers a HEAD. Used by the app readiness check, not at process boot. */
48
+ export async function checkBucketReachable(): Promise<boolean> {
49
+ try {
50
+ await s3Client().send(new HeadBucketCommand({Bucket: config.LOG_STORAGE_S3_BUCKET}));
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ let _uploadClient: S3Client | undefined;
58
+
59
+ /**
60
+ * S3 client for streaming compaction uploads. `s3Client()` is tuned fail-fast for the
61
+ * readiness HEAD, but each multipart part upload can take as long as the link needs, so
62
+ * the per-request timeout is disabled here. Temporal owns the retry budget, so the SDK
63
+ * keeps only a small attempt count for transient connection blips.
64
+ */
65
+ export function uploadS3Client(): S3Client {
66
+ if (!_uploadClient) {
67
+ _uploadClient = new S3Client({
68
+ endpoint: config.LOG_STORAGE_S3_ENDPOINT,
69
+ region: config.LOG_STORAGE_S3_REGION,
70
+ forcePathStyle: config.LOG_STORAGE_S3_FORCE_PATH_STYLE,
71
+ ...(explicitS3Credentials ? {credentials: explicitS3Credentials} : {}),
72
+ maxAttempts: 3,
73
+ requestHandler: {connectionTimeout: 5_000, requestTimeout: 0},
74
+ });
75
+ }
76
+ return _uploadClient;
77
+ }
78
+
79
+ export function closeUploadS3Client(): void {
80
+ _uploadClient?.destroy();
81
+ _uploadClient = undefined;
82
+ }
83
+
84
+ /**
85
+ * Per-attempt object key for a compacted stream: the stream's stable prefix
86
+ * (`logObjectKey` -> `{prefix}/{workspaceId}/{jobId}/{stepId}/{attempt}`, shared with retention
87
+ * and workspace prefix deletes) plus a unique `uploadToken` leaf. Each compaction attempt
88
+ * uploads to its own key and the winner records it atomically, so a slow or zombie attempt
89
+ * can never overwrite a published object. Losing and crashed attempts leave a leaf under the
90
+ * same prefix, reclaimed by the bucket's incomplete-multipart-abort rule and prefix-scoped
91
+ * retention.
92
+ */
93
+ export function compactedObjectKey(identity: LogObjectKeyParams, uploadToken: string): string {
94
+ return `${logObjectKey(config.LOG_STORAGE_S3_PREFIX, identity)}/${uploadToken}`;
95
+ }
96
+
97
+ // lib-storage defaults (5MB parts, queue of 4); we pin a small queue so peak buffer memory
98
+ // stays bounded under concurrent compactions (queueSize x partSize per active upload).
99
+ const UPLOAD_PART_SIZE = 5 * 1024 * 1024;
100
+ const UPLOAD_QUEUE_SIZE = 2;
101
+
102
+ export interface PutCompactedObjectParams {
103
+ key: string;
104
+ body: Readable;
105
+ metadata: Record<string, string>;
106
+ signal?: AbortSignal;
107
+ onProgress?: () => void;
108
+ }
109
+
110
+ /**
111
+ * Streams `body` (gzip-compressed NDJSON) to the compacted object via a multipart upload.
112
+ * `Content-Encoding: gzip` so a browser-direct presigned GET auto-decompresses. Aborts the
113
+ * multipart upload when `signal` fires, so a cancelled activity leaves no dangling parts.
114
+ *
115
+ * A hard crash (no graceful abort) can still strand incomplete multipart parts, which on
116
+ * pay-per-storage providers (S3, R2) bill until aborted. The worker never sets bucket
117
+ * policy, so the deployed bucket must carry an `AbortIncompleteMultipartUpload` lifecycle
118
+ * rule. `dev/garage/bootstrap.sh` provisions it for local Garage; self-hosters set the same
119
+ * rule on their bucket.
120
+ */
121
+ export async function putCompactedObject(params: PutCompactedObjectParams): Promise<void> {
122
+ const upload = new Upload({
123
+ client: uploadS3Client(),
124
+ partSize: UPLOAD_PART_SIZE,
125
+ queueSize: UPLOAD_QUEUE_SIZE,
126
+ params: {
127
+ Bucket: config.LOG_STORAGE_S3_BUCKET,
128
+ Key: params.key,
129
+ Body: params.body,
130
+ ContentType: 'application/x-ndjson',
131
+ ContentEncoding: 'gzip',
132
+ Metadata: params.metadata,
133
+ },
134
+ });
135
+
136
+ if (params.onProgress) upload.on('httpUploadProgress', params.onProgress);
137
+ if (params.signal) {
138
+ // Subscribe before the aborted-check so a signal firing between them cannot be missed,
139
+ // then handle the already-aborted case eagerly (an already-aborted signal never fires 'abort').
140
+ params.signal.addEventListener(
141
+ 'abort',
142
+ () => {
143
+ void upload.abort().catch(() => undefined);
144
+ },
145
+ {once: true},
146
+ );
147
+ if (params.signal.aborted) {
148
+ await upload.abort().catch(() => undefined);
149
+ throw new Error('Compaction upload aborted before it started');
150
+ }
151
+ }
152
+
153
+ await upload.done();
154
+ }
155
+
156
+ /**
157
+ * Deletes a compacted object. S3 `DeleteObject` is idempotent (a missing key is a success),
158
+ * so retention and the publish orphan guard can call it freely without a pre-check.
159
+ */
160
+ export async function deleteObject(key: string): Promise<void> {
161
+ await s3Client().send(new DeleteObjectCommand({Bucket: config.LOG_STORAGE_S3_BUCKET, Key: key}));
162
+ }
163
+
164
+ /**
165
+ * Presigns a GET for a compacted log object so the browser can fetch it directly,
166
+ * bypassing API egress. `getSignedUrl` only computes a signature locally (no network
167
+ * call), so the fail-fast `s3Client()` is fine here. `expiresAt` mirrors the URL's
168
+ * `LOG_READ_URL_TTL_SECONDS` lifetime so the caller can hand the client an absolute
169
+ * deadline without re-deriving it.
170
+ */
171
+ export async function presignedGetUrl(objectKey: string): Promise<{url: string; expiresAt: Date}> {
172
+ const ttlSeconds = config.LOG_READ_URL_TTL_SECONDS;
173
+ // Stamp the deadline from before signing: getSignedUrl bakes its expiry from the clock at
174
+ // sign time, so reading Date.now() after the await could report a deadline slightly past the
175
+ // real one. Computing it first keeps expiresAt a conservative (never-late) bound.
176
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000);
177
+ const url = await getSignedUrl(
178
+ s3Client(),
179
+ new GetObjectCommand({Bucket: config.LOG_STORAGE_S3_BUCKET, Key: objectKey}),
180
+ {expiresIn: ttlSeconds},
181
+ );
182
+ return {url, expiresAt};
183
+ }
184
+
185
+ /**
186
+ * Deletes all objects under a per-attempt prefix. Retention deletes by prefix, not only the
187
+ * recorded `object_key`, to reclaim orphan leaves from failed compaction attempts.
188
+ *
189
+ * Pass a trailing slash so attempt `1` never matches attempt `10`.
190
+ */
191
+ export async function deleteObjectsByPrefix(prefix: string): Promise<void> {
192
+ if (prefix === '') {
193
+ throw new Error(
194
+ 'deleteObjectsByPrefix refuses an empty prefix (it would target the whole bucket)',
195
+ );
196
+ }
197
+ const client = s3Client();
198
+ let continuationToken: string | undefined;
199
+ do {
200
+ const listed = await client.send(
201
+ new ListObjectsV2Command({
202
+ Bucket: config.LOG_STORAGE_S3_BUCKET,
203
+ Prefix: prefix,
204
+ ContinuationToken: continuationToken,
205
+ }),
206
+ );
207
+ const objects = (listed.Contents ?? []).flatMap((object) =>
208
+ object.Key ? [{Key: object.Key}] : [],
209
+ );
210
+ if (objects.length > 0) {
211
+ const deleted = await client.send(
212
+ new DeleteObjectsCommand({
213
+ Bucket: config.LOG_STORAGE_S3_BUCKET,
214
+ Delete: {Objects: objects, Quiet: true},
215
+ }),
216
+ );
217
+ if (deleted.Errors && deleted.Errors.length > 0) {
218
+ const [first] = deleted.Errors;
219
+ throw new Error(
220
+ `Failed to delete ${deleted.Errors.length} object(s) under ${prefix}: ${first?.Key} ${first?.Message}`,
221
+ );
222
+ }
223
+ }
224
+ continuationToken = listed.IsTruncated ? listed.NextContinuationToken : undefined;
225
+ } while (continuationToken);
226
+ }
@@ -0,0 +1,118 @@
1
+ describe('S3 credential validation', () => {
2
+ afterEach(() => {
3
+ vi.unstubAllEnvs();
4
+ vi.resetModules();
5
+ });
6
+
7
+ it.each([
8
+ ['explicit-access-key', undefined],
9
+ [undefined, 'explicit-secret-key'],
10
+ ])('rejects a partial explicit credential pair', async (accessKeyId, secretAccessKey) => {
11
+ vi.stubEnv('LOG_STORAGE_S3_ACCESS_KEY_ID', accessKeyId);
12
+ vi.stubEnv('LOG_STORAGE_S3_SECRET_ACCESS_KEY', secretAccessKey);
13
+ vi.resetModules();
14
+
15
+ await expect(import('#config.js')).rejects.toThrow(
16
+ 'LOG_STORAGE_S3_ACCESS_KEY_ID and LOG_STORAGE_S3_SECRET_ACCESS_KEY must be set together',
17
+ );
18
+ });
19
+ });
20
+
21
+ describe('LOG_RETENTION_DAYS validation', () => {
22
+ afterEach(() => {
23
+ vi.unstubAllEnvs();
24
+ vi.resetModules();
25
+ });
26
+
27
+ it.each(['0', '-5', '1.5'])('fails startup when LOG_RETENTION_DAYS is %s', async (value) => {
28
+ vi.stubEnv('LOG_RETENTION_DAYS', value);
29
+ vi.resetModules();
30
+
31
+ await expect(import('#config.js')).rejects.toThrow('LOG_RETENTION_DAYS');
32
+ });
33
+
34
+ it('accepts a whole-day value of 1 or greater', async () => {
35
+ vi.stubEnv('LOG_RETENTION_DAYS', '30');
36
+ vi.resetModules();
37
+
38
+ const {config} = await import('#config.js');
39
+
40
+ expect(config.LOG_RETENTION_DAYS).toBe(30);
41
+ });
42
+ });
43
+
44
+ describe('LOG_STREAM_REAP_AFTER_SECONDS validation', () => {
45
+ afterEach(() => {
46
+ vi.unstubAllEnvs();
47
+ vi.resetModules();
48
+ });
49
+
50
+ // Reaping a stream a still-valid lease is appending to silently truncates live logs, so a value
51
+ // at or below the job lease TTL (default 90m = 5400s) must fail startup.
52
+ it.each([
53
+ '0',
54
+ '-100',
55
+ '5400',
56
+ ])('fails startup when LOG_STREAM_REAP_AFTER_SECONDS is %s (at or below the default lease TTL)', async (value) => {
57
+ vi.stubEnv('LOG_STREAM_REAP_AFTER_SECONDS', value);
58
+ vi.resetModules();
59
+
60
+ await expect(import('#config.js')).rejects.toThrow('LOG_STREAM_REAP_AFTER_SECONDS');
61
+ });
62
+
63
+ it('accepts a value above the lease TTL', async () => {
64
+ vi.stubEnv('LOG_STREAM_REAP_AFTER_SECONDS', '9000');
65
+ vi.resetModules();
66
+
67
+ const {config} = await import('#config.js');
68
+
69
+ expect(config.LOG_STREAM_REAP_AFTER_SECONDS).toBe(9000);
70
+ });
71
+
72
+ // The check reads the actual configured lease TTL, not a hardcoded floor: raising the lease past
73
+ // the reaper window must fail startup even though the value clears the default lease.
74
+ it('fails when AUTH_JOB_LEASE_TOKEN_EXPIRES_IN is raised above the reaper window', async () => {
75
+ vi.stubEnv('AUTH_JOB_LEASE_TOKEN_EXPIRES_IN', '180m');
76
+ vi.stubEnv('LOG_STREAM_REAP_AFTER_SECONDS', '9000');
77
+ vi.resetModules();
78
+
79
+ await expect(import('#config.js')).rejects.toThrow('LOG_STREAM_REAP_AFTER_SECONDS');
80
+ });
81
+
82
+ // The converse: a small reaper window is fine when the lease is correspondingly small, proving
83
+ // the bound tracks the lease rather than a fixed 5400s floor.
84
+ it('accepts a small reaper window when the lease TTL is correspondingly small', async () => {
85
+ vi.stubEnv('AUTH_JOB_LEASE_TOKEN_EXPIRES_IN', '5m');
86
+ vi.stubEnv('LOG_STREAM_REAP_AFTER_SECONDS', '600');
87
+ vi.resetModules();
88
+
89
+ const {config} = await import('#config.js');
90
+
91
+ expect(config.LOG_STREAM_REAP_AFTER_SECONDS).toBe(600);
92
+ });
93
+ });
94
+
95
+ describe('LOG_MAX_SESSION_LINE_BYTES validation', () => {
96
+ afterEach(() => {
97
+ vi.unstubAllEnvs();
98
+ vi.resetModules();
99
+ });
100
+
101
+ it('fails startup when the session line cap exceeds the append body limit', async () => {
102
+ vi.stubEnv('LOG_APPEND_BODY_LIMIT_BYTES', '1024');
103
+ vi.stubEnv('LOG_MAX_SESSION_LINE_BYTES', '2048');
104
+ vi.resetModules();
105
+
106
+ await expect(import('#config.js')).rejects.toThrow('LOG_APPEND_BODY_LIMIT_BYTES');
107
+ });
108
+
109
+ it('accepts a session line cap equal to the append body limit', async () => {
110
+ vi.stubEnv('LOG_APPEND_BODY_LIMIT_BYTES', '2048');
111
+ vi.stubEnv('LOG_MAX_SESSION_LINE_BYTES', '2048');
112
+ vi.resetModules();
113
+
114
+ const {config} = await import('#config.js');
115
+
116
+ expect(config.LOG_MAX_SESSION_LINE_BYTES).toBe(2048);
117
+ });
118
+ });