@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
@@ -0,0 +1,2 @@
1
+ $ shipfox-swc
2
+ Successfully compiled: 59 files with swc (132.09ms)
@@ -0,0 +1 @@
1
+ $ shipfox-tsc-emit
@@ -0,0 +1 @@
1
+ $ shipfox-tsc-check
package/CHANGELOG.md ADDED
@@ -0,0 +1,179 @@
1
+ # @shipfox/api-logs
2
+
3
+ ## 2.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1b0d344: Publishes the complete API runtime closure with packed-consumer-safe internal imports and records its exact package set in application releases.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [0cd6dd4]
12
+ - Updated dependencies [a68458a]
13
+ - Updated dependencies [6eba800]
14
+ - Updated dependencies [1b0d344]
15
+ - Updated dependencies [521e006]
16
+ - @shipfox/node-module@0.2.0
17
+ - @shipfox/node-temporal@0.2.0
18
+ - @shipfox/api-agent-dto@2.0.0
19
+ - @shipfox/api-auth-context@2.0.0
20
+ - @shipfox/api-logs-dto@2.0.0
21
+ - @shipfox/api-workflows@2.0.0
22
+ - @shipfox/api-workflows-dto@2.0.0
23
+ - @shipfox/config@1.2.1
24
+ - @shipfox/node-drizzle@0.2.1
25
+ - @shipfox/node-fastify@0.2.1
26
+ - @shipfox/node-opentelemetry@0.5.0
27
+ - @shipfox/node-outbox@0.2.1
28
+ - @shipfox/node-postgres@0.4.1
29
+
30
+ ## 0.1.2
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies [705dd43]
35
+ - @shipfox/node-outbox@0.2.0
36
+ - @shipfox/api-workflows@0.1.2
37
+ - @shipfox/node-module@0.1.2
38
+
39
+ ## 0.1.1
40
+
41
+ ### Patch Changes
42
+
43
+ - Updated dependencies [ec75cd5]
44
+ - Updated dependencies [6a1fb54]
45
+ - @shipfox/node-drizzle@0.2.0
46
+ - @shipfox/node-postgres@0.4.0
47
+ - @shipfox/api-workflows@0.1.1
48
+ - @shipfox/node-module@0.1.1
49
+ - @shipfox/node-outbox@0.1.1
50
+
51
+ ## 0.1.0
52
+
53
+ ### Minor Changes
54
+
55
+ - a56748d: Adds ingestion-time agent session parsing with a stored canonical SessionView read endpoint and workflow harness lookup.
56
+ - f92122b: Adds the logs module foundation: a stateless monolith module with its own schema, the runner-facing offset-CAS append endpoint (job-lease authenticated, idempotent, multi-instance safe), a per-job accrual budget with a cap tombstone, and an S3-compatible client targeting Garage at startup. The NDJSON v1 record contract lives in the new `@shipfox/api-logs-dto` package, and `@shipfox/node-fastify` gains a `createRawBodyPlugin({contentType, bodyLimit})` factory for byte-exact request bodies.
57
+ - 4207772: Add the session-authenticated log read endpoint: `GET /steps/:stepId/attempts/:attempt/logs?cursor=N`. One cursor endpoint serves both the live tail and the full history of a step attempt, workspace-scoped through the stream row's denormalized `workspaceId` (a 404 covers both a missing stream and a cross-workspace step, so existence never leaks).
58
+ - Open or closed-but-uncompacted streams return inline NDJSON read from the hot Postgres chunks, walked by chunk `seq` so server-injected control tombstones (`capped`, `runner_lost`) interleave with runner bytes exactly as compaction concatenates them. The inline bytes are therefore byte-identical to the decompressed compacted object. Pages are bounded by `LOG_READ_INLINE_MAX_BYTES` (default 1 MiB), with a `has_more` flag and a `next_cursor` the client drains before it tails.
59
+ - Compacted streams (`object_key` set) return a presigned GET URL (`LOG_READ_URL_TTL_SECONDS`, default 3600) plus `total_bytes`, `expires_at`, and `truncated`, so the browser fetches the object directly and API egress is bypassed.
60
+ - `@shipfox/api-logs-dto` gains `readLogsQuerySchema` and the `readLogsResponseSchema` discriminated union (`inline` or `presigned`) so the backend, client, and E2E helpers share one contract. `@shipfox/api-logs` adds the `@aws-sdk/s3-request-presigner` dependency.
61
+
62
+ ### Patch Changes
63
+
64
+ - 68e4022: Collapse the log stream `kind` into one stream per `(job, step, attempt)`. `kind` was modeled at the wrong layer: the runner already funnels every record through one spool and one offset axis, so the per-kind split (separate stream rows, identity, and ingest dispatch) was unnecessary server-side modeling. A reader now pulls every record for a step attempt and filters by the record `type` at read time, without passing a kind.
65
+ - The append request drops the `kind` query param and `LogStreamClosedEvent` drops `kind`. `streamKind`/`StreamKind` and the `agent_session` line validator (`parseSessionLine`) are removed.
66
+ - `attempt_streams` identity becomes `(job, step, attempt)`; the `kind` and now-dead `capped` columns are dropped (budget cap is signaled in-band by a `capped` tombstone and tracked in `job_accounting`). The close path injects `capped`/`runner_lost` tombstones uniformly.
67
+ - `LOG_MAX_SESSION_LINE_BYTES` is removed and `LOG_APPEND_BODY_LIMIT_BYTES` is right-sized to 1 MiB. The runner's flush-window comment is corrected to match (the server body limit is now 1 MiB, no longer 8 MiB).
68
+
69
+ Agent-session capture (its own record `type`(s) and runner producer) is deferred to the agent-sessions feature, which will emit those records into this same stream.
70
+
71
+ - ae7a63c: Adds daily dispatched outbox row retention with bounded cleanup batches and retention indexes on module outbox tables.
72
+ - d49ee4c: Forward agent step session entries into the logs module as opaque `agent_session` records: the runner tails the pi session file and forwards each verbatim entry over a shared log-stream sink, and the write path stores them with a configurable per-line size cap sized for inline base64 content.
73
+ - 8cab4e7: Close a log stream so it becomes eligible for compaction. A committed `end` record declared-closes the stream inside the append transaction, and a `WORKFLOWS_JOB_TERMINATED` subscriber arms a grace-then-close Temporal workflow that force-closes any stream the runner never ended (appending a `runner_lost` tombstone and marking it truncated). Both paths route through one guarded close that writes a single `logs.stream.closed` outbox event, and a closed-stream guard drops later appends. Adds `closed_at` and three partial indexes to the stream table.
74
+ - b4103b5: Delete expired logs from both object storage and Postgres on an hourly cron, with a configurable horizon (`LOG_RETENTION_DAYS`, default 90), enforced by our own worker rather than bucket lifecycle rules so behavior is identical across object stores. A retention sweep drains closed streams past the horizon in batches, bounded by a self-imposed time budget so a timed-out run never overlaps the next; per stream it first hard-deletes the row (chunks cascading), guarded on the observed `object_key` so a concurrent compaction publish is left intact, then deletes the whole attempt object prefix (reclaiming orphan leaves left behind by a losing compaction attempt). Failed or raced rows are skipped for the rest of the run so a poison row cannot starve the streams behind it, and a `job_accounting` row is pruned only when its job has no remaining streams and no recent activity, so a live job's budget is never reset.
75
+ - b0a0e1a: Reap log streams that leak open after the one-shot job-terminated close. That sweep snapshots a job's open streams once, so a stream whose first append lands after it ran was never closed and leaked forever: open streams are invisible to compaction and retention (both keyed on `state = 'closed'`). A new `reapStaleOpenStreamsCron` (every 10 minutes, on the logs lifecycle queue, staggered off the retention sweep) force-closes any stream left open past the job-lease window, marking it truncated so it re-enters the compaction and retention lifecycle. Adds the `LOG_STREAM_REAP_AFTER_SECONDS` config (default 7200s; startup validates it exceeds the configured `AUTH_JOB_LEASE_TOKEN_EXPIRES_IN`, read from auth's `config`) and the `logs_attempt_streams_open_age_idx` partial index.
76
+ - 2883ab4: Add a stream `kind` to the logs contract and harden the write path. Streams are now identified by `(job, step, attempt, kind)` with two kinds: `log_stream` (process output) and `agent_session` (verbatim, format-agnostic agent JSONL).
77
+ - The record envelope drops `src` to `{v, ts}`; the stdout/stderr pipe moves to `stream` on output, and control records become a flat `type` discriminator with multi-level named groups (`group_id`/`parent_group_id`, capped depth with overflow flattening).
78
+ - The append validator is split into an appendable union and a read union, so a forged server-only `capped`/`runner_lost` tombstone is rejected. `agent_session` lines are stored verbatim after a whole-line, fatal-UTF-8, JSON-well-formedness check within a configurable line cap; the budget cap never injects an in-band tombstone into a session, and close derives its `capped`/`truncated` flags out of band.
79
+ - The append request carries `kind`; the shared body limit and session line cap are configurable with a startup invariant.
80
+
81
+ - 4c39cb5: Makes log retention delete object prefixes before stream rows so transient object-storage delete failures leave rows discoverable for a later sweep instead of permanently orphaning billed objects.
82
+ - 150f378: Compact a closed log stream's hot Postgres chunks into one gzip-compressed NDJSON object in object storage, record the object key on the stream row, and delete the chunk rows. Compaction is a Temporal workflow started from the `logs.stream.closed` event (deduped per stream) on a dedicated task queue, with a reconcile cron backstop that re-drives any closed stream whose compaction never started or permanently failed. Crash-safe and idempotent: each attempt uploads to its own per-attempt object key and a single-winner publish (atomic object-key set and chunk delete, guarded by `object_key IS NULL`) records exactly one, so a slow or retried attempt can never overwrite a published object; a streamed-vs-table integrity check over chunk count, last seq, and byte total guards the irreversible chunk delete. Keeps Postgres bounded by in-flight work rather than retention.
83
+ - 362b3eb: Scope runner log append authorization to the dispatched step attempt carried by the job lease token.
84
+ - 3bea87f: Adds a typed `subscriberFactory` that binds each outbox event name to its payload type at construction, so subscriber handlers receive a typed `(payload, event)` and the per-handler `event.payload as X` casts are gone; a private brand makes the factory the only way to build a module subscriber.
85
+ - 8ac88f3: Runs the API logs test suite without per-file Vitest module isolation by replacing cached module mocks with explicit test dependencies.
86
+ - 2933c33: Adds drain-boundary Zod validation for current outbox publisher event payloads.
87
+ - Updated dependencies [eb40964]
88
+ - Updated dependencies [5c18360]
89
+ - Updated dependencies [2c156d2]
90
+ - Updated dependencies [067a260]
91
+ - Updated dependencies [34ba284]
92
+ - Updated dependencies [a56748d]
93
+ - Updated dependencies [5707d6d]
94
+ - Updated dependencies [7a9943d]
95
+ - Updated dependencies [f788565]
96
+ - Updated dependencies [b9c3f32]
97
+ - Updated dependencies [c17dd6e]
98
+ - Updated dependencies [a81b68c]
99
+ - Updated dependencies [c0a883c]
100
+ - Updated dependencies [72ce351]
101
+ - Updated dependencies [e47f8da]
102
+ - Updated dependencies [736249b]
103
+ - Updated dependencies [2bc5595]
104
+ - Updated dependencies [b1f57d1]
105
+ - Updated dependencies [de54da2]
106
+ - Updated dependencies [7b175f5]
107
+ - Updated dependencies [7ca4c65]
108
+ - Updated dependencies [f104ff2]
109
+ - Updated dependencies [68e4022]
110
+ - Updated dependencies [5bcdbf4]
111
+ - Updated dependencies [97162dd]
112
+ - Updated dependencies [b694b09]
113
+ - Updated dependencies [c47be09]
114
+ - Updated dependencies [f9f059e]
115
+ - Updated dependencies [940696a]
116
+ - Updated dependencies [f98c2be]
117
+ - Updated dependencies [e9396c9]
118
+ - Updated dependencies [ae7a63c]
119
+ - Updated dependencies [5729548]
120
+ - Updated dependencies [f92122b]
121
+ - Updated dependencies [4207772]
122
+ - Updated dependencies [e250c4c]
123
+ - Updated dependencies [b525dcd]
124
+ - Updated dependencies [d49ee4c]
125
+ - Updated dependencies [2883ab4]
126
+ - Updated dependencies [857fd73]
127
+ - Updated dependencies [a982f20]
128
+ - Updated dependencies [aca162b]
129
+ - Updated dependencies [998eba3]
130
+ - Updated dependencies [5327934]
131
+ - Updated dependencies [314e84e]
132
+ - Updated dependencies [3afb7e3]
133
+ - Updated dependencies [139e3be]
134
+ - Updated dependencies [247cbd6]
135
+ - Updated dependencies [c652a68]
136
+ - Updated dependencies [121b42e]
137
+ - Updated dependencies [75520ff]
138
+ - Updated dependencies [bf8319f]
139
+ - Updated dependencies [795f440]
140
+ - Updated dependencies [3dcd751]
141
+ - Updated dependencies [c0a883c]
142
+ - Updated dependencies [362b3eb]
143
+ - Updated dependencies [d6d4862]
144
+ - Updated dependencies [c0a883c]
145
+ - Updated dependencies [6077301]
146
+ - Updated dependencies [9c1c947]
147
+ - Updated dependencies [d635979]
148
+ - Updated dependencies [3bea87f]
149
+ - Updated dependencies [82d22e4]
150
+ - Updated dependencies [d69b164]
151
+ - Updated dependencies [2fb3e87]
152
+ - Updated dependencies [b74f635]
153
+ - Updated dependencies [ef1e917]
154
+ - Updated dependencies [c6eb2ee]
155
+ - Updated dependencies [f9153e8]
156
+ - Updated dependencies [2933c33]
157
+ - Updated dependencies [6181819]
158
+ - Updated dependencies [a8905ed]
159
+ - Updated dependencies [e699508]
160
+ - Updated dependencies [282e66a]
161
+ - Updated dependencies [0dd23a7]
162
+ - Updated dependencies [9c149d1]
163
+ - Updated dependencies [e1d4972]
164
+ - Updated dependencies [f9bf446]
165
+ - Updated dependencies [8ecc121]
166
+ - Updated dependencies [d7b9596]
167
+ - @shipfox/api-workflows@0.1.0
168
+ - @shipfox/api-workflows-dto@0.1.0
169
+ - @shipfox/api-agent-dto@0.1.0
170
+ - @shipfox/node-fastify@0.2.0
171
+ - @shipfox/api-logs-dto@0.1.0
172
+ - @shipfox/node-drizzle@0.1.0
173
+ - @shipfox/api-auth-context@0.1.0
174
+ - @shipfox/node-opentelemetry@0.4.2
175
+ - @shipfox/node-postgres@0.3.2
176
+ - @shipfox/node-temporal@0.1.1
177
+ - @shipfox/node-module@0.1.0
178
+ - @shipfox/node-outbox@0.1.0
179
+ - @shipfox/config@1.2.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shipfox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # Shipfox API Logs
2
+
3
+ Shipfox API Logs is the server-side store for step logs. It accepts an append-only
4
+ byte stream from the runner under an offset protocol, enforces a per-job storage
5
+ budget, keeps the hot bytes in PostgreSQL, and closes streams when a step ends or its
6
+ runner is lost. It serves those logs back on a session-authed read endpoint, and owns its
7
+ tables and routes.
8
+
9
+ ## Example
10
+
11
+ Register the module with the API module runner:
12
+
13
+ ```ts
14
+ import {logsModule} from '@shipfox/api-logs';
15
+ import {createApp, listen} from '@shipfox/node-fastify';
16
+ import {initializeModules} from '@shipfox/node-module';
17
+
18
+ const {auth, routes} = await initializeModules({
19
+ modules: [logsModule],
20
+ });
21
+
22
+ await createApp({auth, routes});
23
+ await listen();
24
+ ```
25
+
26
+ This adds:
27
+
28
+ - log database migrations from `libs/api/logs/drizzle`
29
+ - the append route under `/runs/jobs/current/steps/:stepId/logs` (lease-authed)
30
+ - the read route under `/steps/:stepId/attempts/:attempt/logs` (session-authed): inline NDJSON
31
+ while the stream is hot, a presigned object URL once it is compacted, plus the canonical
32
+ `SessionView` parsed from stored agent-session rows
33
+ - the `logs.stream.closed` publisher and two close subscribers: the step-attempt-terminated
34
+ subscriber that closes each attempt's stream when the runner did not end it in-band, and the
35
+ job-terminated subscriber that force-closes any of a job's still-open streams
36
+ - the Temporal lifecycle workers that carry a closed stream through to deletion: compaction to a
37
+ gzip object, plus the compaction-reconcile, stale-open-stream reaper, and retention-sweep crons
38
+
39
+ ## Streams
40
+
41
+ One stream per `(job, step, attempt)`, scoped by `job_id` (from the lease), so a lease can
42
+ only ever reach its own job's streams. There is no per-producer `kind` on the stream: every
43
+ record a step emits lives in this one stream, and a reader filters by the record's `type`
44
+ (see below) at read time without needing to know anything about the producer.
45
+
46
+ ## The log record contract
47
+
48
+ One JSON object per line. The shared envelope is `{v, ts}`. The pipe rides on `stream` for
49
+ output; control records are flat by `type`:
50
+
51
+ ```ts
52
+ {v: 1, ts, type: 'output', stream: 'stdout' | 'stderr', data} // <= 16 KiB data
53
+ {v: 1, ts, type: 'group_start', group_id, parent_group_id, name} // name <= 1 KiB
54
+ {v: 1, ts, type: 'group_end', group_id}
55
+ {v: 1, ts, type: 'end', total_bytes}
56
+ {v: 1, ts, type: 'gap', dropped_bytes}
57
+ {v: 1, ts, type: 'agent_session', row} // normalized SessionView row
58
+ {v: 1, ts, type: 'capped'} // server-only
59
+ {v: 1, ts, type: 'runner_lost'} // server-only
60
+ ```
61
+
62
+ ### Write-path protection
63
+
64
+ Two layers stop a runner from writing what it should not:
65
+
66
+ 1. **Lease scope** — the lease binds writes to the job's own `(step, attempt)`, so cross-job
67
+ injection is structurally impossible.
68
+ 2. **Distinct raw/write and stored/read unions** — every line is validated against the **raw**
69
+ record union. The server-only `capped`/`runner_lost` tombstones are members of the read union
70
+ only, not the raw write union, so a forged tombstone append is rejected (400). A forged
71
+ tombstone that is otherwise a valid record is logged as a narrowed audit warning (no payload,
72
+ no token).
73
+
74
+ ### Multi-level named groups
75
+
76
+ `::group::<name>` / `::endgroup::` markers form a tree. The runner keeps a nesting stack: each
77
+ `group_start` gets a monotonic id (`g1`, `g2`, …) and a `parent_group_id` (the enclosing group,
78
+ or `null` at the root). Nesting is capped at depth 32; past the cap a group is flattened to plain
79
+ output and counted so its matching `::endgroup::` never pops a real parent. The tree is
80
+ recoverable from the ids, parent links, and the tombstone position at truncation.
81
+
82
+ ## Read path
83
+
84
+ A session-authed `GET /steps/:stepId/attempts/:attempt/logs?cursor=N` serves one endpoint for both
85
+ the live tail and the full history. It is workspace-scoped through the stream row's denormalized
86
+ `workspace_id`; a 404 covers both a missing stream and a cross-workspace step, so existence never
87
+ leaks.
88
+
89
+ - **Hot (open, or closed but not yet compacted)** — inline NDJSON read from the Postgres chunks,
90
+ walked by chunk `seq` so server control tombstones interleave with normalized runner records
91
+ exactly as compaction concatenates them, making the inline bytes byte-identical to the
92
+ decompressed object.
93
+ Pages are bounded by `LOG_READ_INLINE_MAX_BYTES`; the client follows `has_more`/`next_cursor` to
94
+ drain the backlog, then tails from the last cursor.
95
+ - **Cold (compacted, `object_key` set)** — a presigned GET URL (`LOG_READ_URL_TTL_SECONDS`) so the
96
+ browser fetches the object directly and API egress is bypassed.
97
+
98
+ ## Agent-session capture
99
+
100
+ An agent step forwards each agent session entry as one `agent_session` record on this **same**
101
+ append request, distinguished by its `type`. That append-side record carries the raw entry in
102
+ `data`; it is a write contract only. On ingestion, the API parses each accepted entry into one or
103
+ more canonical `SessionView` rows and writes those rows back as normal `agent_session` log records
104
+ in the same chunk stream (`{v, ts, type: 'agent_session', row}`). The parser is selected from the
105
+ step's `config.harness` (`pi` by default).
106
+
107
+ The transformed records use the same offset-CAS append protocol, chunk table, compaction object,
108
+ and `/logs` read endpoint as output records. Retries, gaps, closed-stream drops, and post-cap drops
109
+ therefore behave exactly like any other log append: if the chunk is not stored, no normalized
110
+ session records appear. A malformed or unsupported harness entry degrades to one `raw` row instead
111
+ of failing ingestion or read.
112
+
113
+ The `/logs` read endpoint returns one stream of log records. `state` and `truncated` come from the
114
+ stream row so clients know when polling can stop. The endpoint never parses session entries on
115
+ read; it serves the normalized records stored by ingestion.
116
+
117
+ The runner forwards entries opaquely and never splits one across records, so a record always
118
+ carries a whole entry. Per-entry size is bounded by `LOG_MAX_SESSION_LINE_BYTES` (a larger line is
119
+ rejected with 400, and the runner drops it with a `gap`); the request body limit
120
+ (`LOG_APPEND_BODY_LIMIT_BYTES`) must hold a full line plus framing, enforced by a startup invariant.
121
+
122
+ ## Budget and terminal state
123
+
124
+ One shared, job-wide, generous accrual budget covers a job's logs. When the job's stored bytes
125
+ cross the budget, the job is capped and further appends are dropped. The append that crosses the
126
+ budget is stored in full, then an in-band `capped` tombstone record is injected once; later appends
127
+ are accepted-and-dropped. The cap is a **job-level** signal — a stream can be byte-complete and
128
+ still sit under a capped job if a sibling step exhausted the shared budget.
129
+
130
+ A stream closes on one of four triggers, all converging on the same guarded, exactly-once close:
131
+ the runner declares its end in-band (an `end` record in an append), which closes the stream
132
+ `declared` the moment those bytes commit; the step-attempt-terminated subscriber closes an attempt
133
+ the runner never ended in-band, `declared` when the runner drained its spool or `timeout` when it
134
+ abandoned it (carried on the event's `logOutcome`); the job-terminated sweep force-closes any of a
135
+ job's still-open streams once the job goes terminal, after a grace period
136
+ (`LOG_STREAM_CLOSE_GRACE_SECONDS`); and the reaper cron force-closes any stream still open past the
137
+ lease window (`LOG_STREAM_REAP_AFTER_SECONDS`), the backstop for a stream the one-shot sweeps missed
138
+ (one whose first append landed after they ran). Every timeout close sets `truncated` and injects a
139
+ `runner_lost` tombstone; each close writes one `logs.stream.closed` event, which drives compaction.
140
+
141
+ ## Setup
142
+
143
+ Install the package from the registry:
144
+
145
+ ```sh
146
+ pnpm add @shipfox/api-logs
147
+ ```
148
+
149
+ Configuration lives in `src/config.ts` (object storage for compacted logs, the per-job budget,
150
+ the close grace period, the append body limit, and the read path's presigned-URL TTL and inline
151
+ page cap). See each variable's `desc` for what to set.
@@ -0,0 +1,69 @@
1
+ import type { Readable } from 'node:stream';
2
+ import { S3Client } from '@aws-sdk/client-s3';
3
+ import { type LogObjectKeyParams } from '#core/entities/log-object.js';
4
+ /** Lazily-built S3 client targeting the configured object store (Garage in dev). */
5
+ export declare function s3Client(): S3Client;
6
+ export declare function closeS3Client(): void;
7
+ /** Readiness probe: true when the configured bucket answers a HEAD. Used by the app readiness check, not at process boot. */
8
+ export declare function checkBucketReachable(): Promise<boolean>;
9
+ /**
10
+ * S3 client for streaming compaction uploads. `s3Client()` is tuned fail-fast for the
11
+ * readiness HEAD, but each multipart part upload can take as long as the link needs, so
12
+ * the per-request timeout is disabled here. Temporal owns the retry budget, so the SDK
13
+ * keeps only a small attempt count for transient connection blips.
14
+ */
15
+ export declare function uploadS3Client(): S3Client;
16
+ export declare function closeUploadS3Client(): void;
17
+ /**
18
+ * Per-attempt object key for a compacted stream: the stream's stable prefix
19
+ * (`logObjectKey` -> `{prefix}/{workspaceId}/{jobId}/{stepId}/{attempt}`, shared with retention
20
+ * and workspace prefix deletes) plus a unique `uploadToken` leaf. Each compaction attempt
21
+ * uploads to its own key and the winner records it atomically, so a slow or zombie attempt
22
+ * can never overwrite a published object. Losing and crashed attempts leave a leaf under the
23
+ * same prefix, reclaimed by the bucket's incomplete-multipart-abort rule and prefix-scoped
24
+ * retention.
25
+ */
26
+ export declare function compactedObjectKey(identity: LogObjectKeyParams, uploadToken: string): string;
27
+ export interface PutCompactedObjectParams {
28
+ key: string;
29
+ body: Readable;
30
+ metadata: Record<string, string>;
31
+ signal?: AbortSignal;
32
+ onProgress?: () => void;
33
+ }
34
+ /**
35
+ * Streams `body` (gzip-compressed NDJSON) to the compacted object via a multipart upload.
36
+ * `Content-Encoding: gzip` so a browser-direct presigned GET auto-decompresses. Aborts the
37
+ * multipart upload when `signal` fires, so a cancelled activity leaves no dangling parts.
38
+ *
39
+ * A hard crash (no graceful abort) can still strand incomplete multipart parts, which on
40
+ * pay-per-storage providers (S3, R2) bill until aborted. The worker never sets bucket
41
+ * policy, so the deployed bucket must carry an `AbortIncompleteMultipartUpload` lifecycle
42
+ * rule. `dev/garage/bootstrap.sh` provisions it for local Garage; self-hosters set the same
43
+ * rule on their bucket.
44
+ */
45
+ export declare function putCompactedObject(params: PutCompactedObjectParams): Promise<void>;
46
+ /**
47
+ * Deletes a compacted object. S3 `DeleteObject` is idempotent (a missing key is a success),
48
+ * so retention and the publish orphan guard can call it freely without a pre-check.
49
+ */
50
+ export declare function deleteObject(key: string): Promise<void>;
51
+ /**
52
+ * Presigns a GET for a compacted log object so the browser can fetch it directly,
53
+ * bypassing API egress. `getSignedUrl` only computes a signature locally (no network
54
+ * call), so the fail-fast `s3Client()` is fine here. `expiresAt` mirrors the URL's
55
+ * `LOG_READ_URL_TTL_SECONDS` lifetime so the caller can hand the client an absolute
56
+ * deadline without re-deriving it.
57
+ */
58
+ export declare function presignedGetUrl(objectKey: string): Promise<{
59
+ url: string;
60
+ expiresAt: Date;
61
+ }>;
62
+ /**
63
+ * Deletes all objects under a per-attempt prefix. Retention deletes by prefix, not only the
64
+ * recorded `object_key`, to reclaim orphan leaves from failed compaction attempts.
65
+ *
66
+ * Pass a trailing slash so attempt `1` never matches attempt `10`.
67
+ */
68
+ export declare function deleteObjectsByPrefix(prefix: string): Promise<void>;
69
+ //# sourceMappingURL=object-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-storage.d.ts","sourceRoot":"","sources":["../../src/api/object-storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,aAAa,CAAC;AAC1C,OAAO,EAML,QAAQ,EACT,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAAC,KAAK,kBAAkB,EAAe,MAAM,8BAA8B,CAAC;AAYnF,oFAAoF;AACpF,wBAAgB,QAAQ,IAAI,QAAQ,CAcnC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAGpC;AAED,6HAA6H;AAC7H,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO7D;AAID;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,QAAQ,CAYzC;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAG1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAE5F;AAOD,MAAM,WAAW,wBAAwB;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCxF;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7D;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAC,CAAC,CAYhG;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmCzE"}
@@ -0,0 +1,199 @@
1
+ import { DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand, HeadBucketCommand, ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3';
2
+ import { Upload } from '@aws-sdk/lib-storage';
3
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
4
+ import { config } from '#config.js';
5
+ import { logObjectKey } from '#core/entities/log-object.js';
6
+ const explicitS3Credentials = config.LOG_STORAGE_S3_ACCESS_KEY_ID && config.LOG_STORAGE_S3_SECRET_ACCESS_KEY ? {
7
+ accessKeyId: config.LOG_STORAGE_S3_ACCESS_KEY_ID,
8
+ secretAccessKey: config.LOG_STORAGE_S3_SECRET_ACCESS_KEY
9
+ } : undefined;
10
+ let _client;
11
+ /** Lazily-built S3 client targeting the configured object store (Garage in dev). */ export function s3Client() {
12
+ if (!_client) {
13
+ _client = new S3Client({
14
+ endpoint: config.LOG_STORAGE_S3_ENDPOINT,
15
+ region: config.LOG_STORAGE_S3_REGION,
16
+ forcePathStyle: config.LOG_STORAGE_S3_FORCE_PATH_STYLE,
17
+ ...explicitS3Credentials ? {
18
+ credentials: explicitS3Credentials
19
+ } : {},
20
+ // Fail fast: a slow or black-holed endpoint must not hang callers behind
21
+ // SDK backoff. One retry, short connect/request timeouts.
22
+ maxAttempts: 2,
23
+ requestHandler: {
24
+ connectionTimeout: 1_000,
25
+ requestTimeout: 3_000
26
+ }
27
+ });
28
+ }
29
+ return _client;
30
+ }
31
+ export function closeS3Client() {
32
+ _client?.destroy();
33
+ _client = undefined;
34
+ }
35
+ /** Readiness probe: true when the configured bucket answers a HEAD. Used by the app readiness check, not at process boot. */ export async function checkBucketReachable() {
36
+ try {
37
+ await s3Client().send(new HeadBucketCommand({
38
+ Bucket: config.LOG_STORAGE_S3_BUCKET
39
+ }));
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+ let _uploadClient;
46
+ /**
47
+ * S3 client for streaming compaction uploads. `s3Client()` is tuned fail-fast for the
48
+ * readiness HEAD, but each multipart part upload can take as long as the link needs, so
49
+ * the per-request timeout is disabled here. Temporal owns the retry budget, so the SDK
50
+ * keeps only a small attempt count for transient connection blips.
51
+ */ export function uploadS3Client() {
52
+ if (!_uploadClient) {
53
+ _uploadClient = new S3Client({
54
+ endpoint: config.LOG_STORAGE_S3_ENDPOINT,
55
+ region: config.LOG_STORAGE_S3_REGION,
56
+ forcePathStyle: config.LOG_STORAGE_S3_FORCE_PATH_STYLE,
57
+ ...explicitS3Credentials ? {
58
+ credentials: explicitS3Credentials
59
+ } : {},
60
+ maxAttempts: 3,
61
+ requestHandler: {
62
+ connectionTimeout: 5_000,
63
+ requestTimeout: 0
64
+ }
65
+ });
66
+ }
67
+ return _uploadClient;
68
+ }
69
+ export function closeUploadS3Client() {
70
+ _uploadClient?.destroy();
71
+ _uploadClient = undefined;
72
+ }
73
+ /**
74
+ * Per-attempt object key for a compacted stream: the stream's stable prefix
75
+ * (`logObjectKey` -> `{prefix}/{workspaceId}/{jobId}/{stepId}/{attempt}`, shared with retention
76
+ * and workspace prefix deletes) plus a unique `uploadToken` leaf. Each compaction attempt
77
+ * uploads to its own key and the winner records it atomically, so a slow or zombie attempt
78
+ * can never overwrite a published object. Losing and crashed attempts leave a leaf under the
79
+ * same prefix, reclaimed by the bucket's incomplete-multipart-abort rule and prefix-scoped
80
+ * retention.
81
+ */ export function compactedObjectKey(identity, uploadToken) {
82
+ return `${logObjectKey(config.LOG_STORAGE_S3_PREFIX, identity)}/${uploadToken}`;
83
+ }
84
+ // lib-storage defaults (5MB parts, queue of 4); we pin a small queue so peak buffer memory
85
+ // stays bounded under concurrent compactions (queueSize x partSize per active upload).
86
+ const UPLOAD_PART_SIZE = 5 * 1024 * 1024;
87
+ const UPLOAD_QUEUE_SIZE = 2;
88
+ /**
89
+ * Streams `body` (gzip-compressed NDJSON) to the compacted object via a multipart upload.
90
+ * `Content-Encoding: gzip` so a browser-direct presigned GET auto-decompresses. Aborts the
91
+ * multipart upload when `signal` fires, so a cancelled activity leaves no dangling parts.
92
+ *
93
+ * A hard crash (no graceful abort) can still strand incomplete multipart parts, which on
94
+ * pay-per-storage providers (S3, R2) bill until aborted. The worker never sets bucket
95
+ * policy, so the deployed bucket must carry an `AbortIncompleteMultipartUpload` lifecycle
96
+ * rule. `dev/garage/bootstrap.sh` provisions it for local Garage; self-hosters set the same
97
+ * rule on their bucket.
98
+ */ export async function putCompactedObject(params) {
99
+ const upload = new Upload({
100
+ client: uploadS3Client(),
101
+ partSize: UPLOAD_PART_SIZE,
102
+ queueSize: UPLOAD_QUEUE_SIZE,
103
+ params: {
104
+ Bucket: config.LOG_STORAGE_S3_BUCKET,
105
+ Key: params.key,
106
+ Body: params.body,
107
+ ContentType: 'application/x-ndjson',
108
+ ContentEncoding: 'gzip',
109
+ Metadata: params.metadata
110
+ }
111
+ });
112
+ if (params.onProgress) upload.on('httpUploadProgress', params.onProgress);
113
+ if (params.signal) {
114
+ // Subscribe before the aborted-check so a signal firing between them cannot be missed,
115
+ // then handle the already-aborted case eagerly (an already-aborted signal never fires 'abort').
116
+ params.signal.addEventListener('abort', ()=>{
117
+ void upload.abort().catch(()=>undefined);
118
+ }, {
119
+ once: true
120
+ });
121
+ if (params.signal.aborted) {
122
+ await upload.abort().catch(()=>undefined);
123
+ throw new Error('Compaction upload aborted before it started');
124
+ }
125
+ }
126
+ await upload.done();
127
+ }
128
+ /**
129
+ * Deletes a compacted object. S3 `DeleteObject` is idempotent (a missing key is a success),
130
+ * so retention and the publish orphan guard can call it freely without a pre-check.
131
+ */ export async function deleteObject(key) {
132
+ await s3Client().send(new DeleteObjectCommand({
133
+ Bucket: config.LOG_STORAGE_S3_BUCKET,
134
+ Key: key
135
+ }));
136
+ }
137
+ /**
138
+ * Presigns a GET for a compacted log object so the browser can fetch it directly,
139
+ * bypassing API egress. `getSignedUrl` only computes a signature locally (no network
140
+ * call), so the fail-fast `s3Client()` is fine here. `expiresAt` mirrors the URL's
141
+ * `LOG_READ_URL_TTL_SECONDS` lifetime so the caller can hand the client an absolute
142
+ * deadline without re-deriving it.
143
+ */ export async function presignedGetUrl(objectKey) {
144
+ const ttlSeconds = config.LOG_READ_URL_TTL_SECONDS;
145
+ // Stamp the deadline from before signing: getSignedUrl bakes its expiry from the clock at
146
+ // sign time, so reading Date.now() after the await could report a deadline slightly past the
147
+ // real one. Computing it first keeps expiresAt a conservative (never-late) bound.
148
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000);
149
+ const url = await getSignedUrl(s3Client(), new GetObjectCommand({
150
+ Bucket: config.LOG_STORAGE_S3_BUCKET,
151
+ Key: objectKey
152
+ }), {
153
+ expiresIn: ttlSeconds
154
+ });
155
+ return {
156
+ url,
157
+ expiresAt
158
+ };
159
+ }
160
+ /**
161
+ * Deletes all objects under a per-attempt prefix. Retention deletes by prefix, not only the
162
+ * recorded `object_key`, to reclaim orphan leaves from failed compaction attempts.
163
+ *
164
+ * Pass a trailing slash so attempt `1` never matches attempt `10`.
165
+ */ export async function deleteObjectsByPrefix(prefix) {
166
+ if (prefix === '') {
167
+ throw new Error('deleteObjectsByPrefix refuses an empty prefix (it would target the whole bucket)');
168
+ }
169
+ const client = s3Client();
170
+ let continuationToken;
171
+ do {
172
+ const listed = await client.send(new ListObjectsV2Command({
173
+ Bucket: config.LOG_STORAGE_S3_BUCKET,
174
+ Prefix: prefix,
175
+ ContinuationToken: continuationToken
176
+ }));
177
+ const objects = (listed.Contents ?? []).flatMap((object)=>object.Key ? [
178
+ {
179
+ Key: object.Key
180
+ }
181
+ ] : []);
182
+ if (objects.length > 0) {
183
+ const deleted = await client.send(new DeleteObjectsCommand({
184
+ Bucket: config.LOG_STORAGE_S3_BUCKET,
185
+ Delete: {
186
+ Objects: objects,
187
+ Quiet: true
188
+ }
189
+ }));
190
+ if (deleted.Errors && deleted.Errors.length > 0) {
191
+ const [first] = deleted.Errors;
192
+ throw new Error(`Failed to delete ${deleted.Errors.length} object(s) under ${prefix}: ${first?.Key} ${first?.Message}`);
193
+ }
194
+ }
195
+ continuationToken = listed.IsTruncated ? listed.NextContinuationToken : undefined;
196
+ }while (continuationToken)
197
+ }
198
+
199
+ //# sourceMappingURL=object-storage.js.map