@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.
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +179 -0
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/api/object-storage.d.ts +69 -0
- package/dist/api/object-storage.d.ts.map +1 -0
- package/dist/api/object-storage.js +199 -0
- package/dist/api/object-storage.js.map +1 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +108 -0
- package/dist/config.js.map +1 -0
- package/dist/core/append-logs.d.ts +28 -0
- package/dist/core/append-logs.d.ts.map +1 -0
- package/dist/core/append-logs.js +318 -0
- package/dist/core/append-logs.js.map +1 -0
- package/dist/core/budget.d.ts +12 -0
- package/dist/core/budget.d.ts.map +1 -0
- package/dist/core/budget.js +9 -0
- package/dist/core/budget.js.map +1 -0
- package/dist/core/close-stream.d.ts +27 -0
- package/dist/core/close-stream.d.ts.map +1 -0
- package/dist/core/close-stream.js +59 -0
- package/dist/core/close-stream.js.map +1 -0
- package/dist/core/compaction.d.ts +28 -0
- package/dist/core/compaction.d.ts.map +1 -0
- package/dist/core/compaction.js +49 -0
- package/dist/core/compaction.js.map +1 -0
- package/dist/core/entities/attempt-stream.d.ts +37 -0
- package/dist/core/entities/attempt-stream.d.ts.map +1 -0
- package/dist/core/entities/attempt-stream.js +15 -0
- package/dist/core/entities/attempt-stream.js.map +1 -0
- package/dist/core/entities/job-accounting.d.ts +12 -0
- package/dist/core/entities/job-accounting.d.ts.map +1 -0
- package/dist/core/entities/job-accounting.js +6 -0
- package/dist/core/entities/job-accounting.js.map +1 -0
- package/dist/core/entities/log-object.d.ts +13 -0
- package/dist/core/entities/log-object.d.ts.map +1 -0
- package/dist/core/entities/log-object.js +9 -0
- package/dist/core/entities/log-object.js.map +1 -0
- package/dist/core/errors.d.ts +26 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +31 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/finalize-attempt-stream.d.ts +22 -0
- package/dist/core/finalize-attempt-stream.d.ts.map +1 -0
- package/dist/core/finalize-attempt-stream.js +48 -0
- package/dist/core/finalize-attempt-stream.js.map +1 -0
- package/dist/core/read-logs.d.ts +33 -0
- package/dist/core/read-logs.d.ts.map +1 -0
- package/dist/core/read-logs.js +49 -0
- package/dist/core/read-logs.js.map +1 -0
- package/dist/core/reap-stale-open-streams.d.ts +27 -0
- package/dist/core/reap-stale-open-streams.d.ts.map +1 -0
- package/dist/core/reap-stale-open-streams.js +50 -0
- package/dist/core/reap-stale-open-streams.js.map +1 -0
- package/dist/core/retention.d.ts +32 -0
- package/dist/core/retention.d.ts.map +1 -0
- package/dist/core/retention.js +148 -0
- package/dist/core/retention.js.map +1 -0
- package/dist/core/session/claude/rows.d.ts +6 -0
- package/dist/core/session/claude/rows.d.ts.map +1 -0
- package/dist/core/session/claude/rows.js +145 -0
- package/dist/core/session/claude/rows.js.map +1 -0
- package/dist/core/session/claude-parser.d.ts +4 -0
- package/dist/core/session/claude-parser.d.ts.map +1 -0
- package/dist/core/session/claude-parser.js +42 -0
- package/dist/core/session/claude-parser.js.map +1 -0
- package/dist/core/session/entry-schema.d.ts +51 -0
- package/dist/core/session/entry-schema.d.ts.map +1 -0
- package/dist/core/session/entry-schema.js +61 -0
- package/dist/core/session/entry-schema.js.map +1 -0
- package/dist/core/session/object.d.ts +12 -0
- package/dist/core/session/object.d.ts.map +1 -0
- package/dist/core/session/object.js +50 -0
- package/dist/core/session/object.js.map +1 -0
- package/dist/core/session/parse-session.d.ts +5 -0
- package/dist/core/session/parse-session.d.ts.map +1 -0
- package/dist/core/session/parse-session.js +18 -0
- package/dist/core/session/parse-session.js.map +1 -0
- package/dist/core/session/pi/message-rows.d.ts +4 -0
- package/dist/core/session/pi/message-rows.d.ts.map +1 -0
- package/dist/core/session/pi/message-rows.js +113 -0
- package/dist/core/session/pi/message-rows.js.map +1 -0
- package/dist/core/session/pi/metadata.d.ts +18 -0
- package/dist/core/session/pi/metadata.d.ts.map +1 -0
- package/dist/core/session/pi/metadata.js +120 -0
- package/dist/core/session/pi/metadata.js.map +1 -0
- package/dist/core/session/pi/text.d.ts +12 -0
- package/dist/core/session/pi/text.d.ts.map +1 -0
- package/dist/core/session/pi/text.js +59 -0
- package/dist/core/session/pi/text.js.map +1 -0
- package/dist/core/session/pi-parser.d.ts +5 -0
- package/dist/core/session/pi-parser.d.ts.map +1 -0
- package/dist/core/session/pi-parser.js +73 -0
- package/dist/core/session/pi-parser.js.map +1 -0
- package/dist/core/session/rows.d.ts +7 -0
- package/dist/core/session/rows.d.ts.map +1 -0
- package/dist/core/session/rows.js +39 -0
- package/dist/core/session/rows.js.map +1 -0
- package/dist/core/session/session-record.d.ts +5 -0
- package/dist/core/session/session-record.d.ts.map +1 -0
- package/dist/core/session/session-record.js +3 -0
- package/dist/core/session/session-record.js.map +1 -0
- package/dist/db/accounting.d.ts +33 -0
- package/dist/db/accounting.d.ts.map +1 -0
- package/dist/db/accounting.js +52 -0
- package/dist/db/accounting.js.map +1 -0
- package/dist/db/chunks.d.ts +62 -0
- package/dist/db/chunks.d.ts.map +1 -0
- package/dist/db/chunks.js +88 -0
- package/dist/db/chunks.js.map +1 -0
- package/dist/db/db.d.ts +752 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +24 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +5 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema/attempt-streams.d.ts +304 -0
- package/dist/db/schema/attempt-streams.d.ts.map +1 -0
- package/dist/db/schema/attempt-streams.js +100 -0
- package/dist/db/schema/attempt-streams.js.map +1 -0
- package/dist/db/schema/chunks.d.ts +166 -0
- package/dist/db/schema/chunks.d.ts.map +1 -0
- package/dist/db/schema/chunks.js +47 -0
- package/dist/db/schema/chunks.js.map +1 -0
- package/dist/db/schema/common.d.ts +29 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +11 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/job-accounting.d.ts +143 -0
- package/dist/db/schema/job-accounting.d.ts.map +1 -0
- package/dist/db/schema/job-accounting.js +43 -0
- package/dist/db/schema/job-accounting.js.map +1 -0
- package/dist/db/schema/outbox.d.ts +195 -0
- package/dist/db/schema/outbox.d.ts.map +1 -0
- package/dist/db/schema/outbox.js +5 -0
- package/dist/db/schema/outbox.js.map +1 -0
- package/dist/db/streams.d.ts +141 -0
- package/dist/db/streams.d.ts.map +1 -0
- package/dist/db/streams.js +197 -0
- package/dist/db/streams.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/index.d.ts +2 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +3 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instance.d.ts +14 -0
- package/dist/metrics/instance.d.ts.map +1 -0
- package/dist/metrics/instance.js +16 -0
- package/dist/metrics/instance.js.map +1 -0
- package/dist/metrics/service.d.ts +2 -0
- package/dist/metrics/service.d.ts.map +1 -0
- package/dist/metrics/service.js +20 -0
- package/dist/metrics/service.js.map +1 -0
- package/dist/presentation/dto/read-logs.d.ts +4 -0
- package/dist/presentation/dto/read-logs.d.ts.map +1 -0
- package/dist/presentation/dto/read-logs.js +22 -0
- package/dist/presentation/dto/read-logs.js.map +1 -0
- package/dist/presentation/routes/append-logs.d.ts +2 -0
- package/dist/presentation/routes/append-logs.d.ts.map +1 -0
- package/dist/presentation/routes/append-logs.js +71 -0
- package/dist/presentation/routes/append-logs.js.map +1 -0
- package/dist/presentation/routes/index.d.ts +3 -0
- package/dist/presentation/routes/index.d.ts.map +1 -0
- package/dist/presentation/routes/index.js +32 -0
- package/dist/presentation/routes/index.js.map +1 -0
- package/dist/presentation/routes/read-logs.d.ts +2 -0
- package/dist/presentation/routes/read-logs.d.ts.map +1 -0
- package/dist/presentation/routes/read-logs.js +41 -0
- package/dist/presentation/routes/read-logs.js.map +1 -0
- package/dist/presentation/subscribers/on-job-terminated.d.ts +8 -0
- package/dist/presentation/subscribers/on-job-terminated.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-job-terminated.js +32 -0
- package/dist/presentation/subscribers/on-job-terminated.js.map +1 -0
- package/dist/presentation/subscribers/on-log-stream-closed.d.ts +9 -0
- package/dist/presentation/subscribers/on-log-stream-closed.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-log-stream-closed.js +31 -0
- package/dist/presentation/subscribers/on-log-stream-closed.js.map +1 -0
- package/dist/presentation/subscribers/on-step-attempt-terminated.d.ts +3 -0
- package/dist/presentation/subscribers/on-step-attempt-terminated.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-step-attempt-terminated.js +14 -0
- package/dist/presentation/subscribers/on-step-attempt-terminated.js.map +1 -0
- package/dist/temporal/activities/close-abandoned-streams.d.ts +12 -0
- package/dist/temporal/activities/close-abandoned-streams.d.ts.map +1 -0
- package/dist/temporal/activities/close-abandoned-streams.js +33 -0
- package/dist/temporal/activities/close-abandoned-streams.js.map +1 -0
- package/dist/temporal/activities/compact-stream.d.ts +33 -0
- package/dist/temporal/activities/compact-stream.d.ts.map +1 -0
- package/dist/temporal/activities/compact-stream.js +95 -0
- package/dist/temporal/activities/compact-stream.js.map +1 -0
- package/dist/temporal/activities/compaction-reconcile.d.ts +16 -0
- package/dist/temporal/activities/compaction-reconcile.d.ts.map +1 -0
- package/dist/temporal/activities/compaction-reconcile.js +52 -0
- package/dist/temporal/activities/compaction-reconcile.js.map +1 -0
- package/dist/temporal/activities/index.d.ts +14 -0
- package/dist/temporal/activities/index.d.ts.map +1 -0
- package/dist/temporal/activities/index.js +16 -0
- package/dist/temporal/activities/index.js.map +1 -0
- package/dist/temporal/activities/reap-stale-open-streams.d.ts +4 -0
- package/dist/temporal/activities/reap-stale-open-streams.d.ts.map +1 -0
- package/dist/temporal/activities/reap-stale-open-streams.js +11 -0
- package/dist/temporal/activities/reap-stale-open-streams.js.map +1 -0
- package/dist/temporal/activities/retention-sweep.d.ts +7 -0
- package/dist/temporal/activities/retention-sweep.d.ts.map +1 -0
- package/dist/temporal/activities/retention-sweep.js +19 -0
- package/dist/temporal/activities/retention-sweep.js.map +1 -0
- package/dist/temporal/constants.d.ts +7 -0
- package/dist/temporal/constants.d.ts.map +1 -0
- package/dist/temporal/constants.js +13 -0
- package/dist/temporal/constants.js.map +1 -0
- package/dist/temporal/workflows/close-abandoned-streams.d.ts +11 -0
- package/dist/temporal/workflows/close-abandoned-streams.d.ts.map +1 -0
- package/dist/temporal/workflows/close-abandoned-streams.js +22 -0
- package/dist/temporal/workflows/close-abandoned-streams.js.map +1 -0
- package/dist/temporal/workflows/compact-stream.d.ts +6 -0
- package/dist/temporal/workflows/compact-stream.d.ts.map +1 -0
- package/dist/temporal/workflows/compact-stream.js +25 -0
- package/dist/temporal/workflows/compact-stream.js.map +1 -0
- package/dist/temporal/workflows/compaction-reconcile-cron.d.ts +3 -0
- package/dist/temporal/workflows/compaction-reconcile-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/compaction-reconcile-cron.js +15 -0
- package/dist/temporal/workflows/compaction-reconcile-cron.js.map +1 -0
- package/dist/temporal/workflows/index.d.ts +6 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -0
- package/dist/temporal/workflows/index.js +7 -0
- package/dist/temporal/workflows/index.js.map +1 -0
- package/dist/temporal/workflows/reap-stale-open-streams-cron.d.ts +2 -0
- package/dist/temporal/workflows/reap-stale-open-streams-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/reap-stale-open-streams-cron.js +15 -0
- package/dist/temporal/workflows/reap-stale-open-streams-cron.js.map +1 -0
- package/dist/temporal/workflows/retention-sweep-cron.d.ts +2 -0
- package/dist/temporal/workflows/retention-sweep-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/retention-sweep-cron.js +23 -0
- package/dist/temporal/workflows/retention-sweep-cron.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_initial.sql +65 -0
- package/drizzle/meta/0000_snapshot.json +529 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/package.json +84 -0
- package/src/api/object-storage-credentials.test.ts +70 -0
- package/src/api/object-storage.test.ts +64 -0
- package/src/api/object-storage.ts +226 -0
- package/src/config.test.ts +118 -0
- package/src/config.ts +130 -0
- package/src/core/append-logs.test.ts +563 -0
- package/src/core/append-logs.ts +384 -0
- package/src/core/budget.test.ts +30 -0
- package/src/core/budget.ts +19 -0
- package/src/core/close-stream.ts +72 -0
- package/src/core/compaction.ts +68 -0
- package/src/core/entities/attempt-stream.ts +38 -0
- package/src/core/entities/job-accounting.ts +11 -0
- package/src/core/entities/log-object.test.ts +25 -0
- package/src/core/entities/log-object.ts +18 -0
- package/src/core/errors.ts +37 -0
- package/src/core/finalize-attempt-stream.test.ts +90 -0
- package/src/core/finalize-attempt-stream.ts +59 -0
- package/src/core/read-logs.ts +77 -0
- package/src/core/reap-stale-open-streams.test.ts +137 -0
- package/src/core/reap-stale-open-streams.ts +60 -0
- package/src/core/retention.test.ts +388 -0
- package/src/core/retention.ts +184 -0
- package/src/core/session/claude/rows.ts +223 -0
- package/src/core/session/claude-parser.test.ts +199 -0
- package/src/core/session/claude-parser.ts +38 -0
- package/src/core/session/entry-schema.ts +82 -0
- package/src/core/session/object.ts +57 -0
- package/src/core/session/parse-session.ts +23 -0
- package/src/core/session/pi/message-rows.ts +211 -0
- package/src/core/session/pi/metadata.ts +148 -0
- package/src/core/session/pi/text.ts +83 -0
- package/src/core/session/pi-parser.test.ts +451 -0
- package/src/core/session/pi-parser.ts +107 -0
- package/src/core/session/rows.ts +38 -0
- package/src/core/session/session-record.ts +4 -0
- package/src/db/accounting.ts +81 -0
- package/src/db/chunks.ts +143 -0
- package/src/db/db.ts +22 -0
- package/src/db/index.ts +4 -0
- package/src/db/schema/attempt-streams.ts +106 -0
- package/src/db/schema/chunks.ts +43 -0
- package/src/db/schema/common.ts +12 -0
- package/src/db/schema/job-accounting.ts +38 -0
- package/src/db/schema/outbox.ts +4 -0
- package/src/db/streams.test.ts +47 -0
- package/src/db/streams.ts +368 -0
- package/src/index.ts +77 -0
- package/src/metrics/index.ts +1 -0
- package/src/metrics/instance.ts +33 -0
- package/src/metrics/service.ts +23 -0
- package/src/presentation/dto/read-logs.ts +24 -0
- package/src/presentation/routes/append-logs.test.ts +279 -0
- package/src/presentation/routes/append-logs.ts +66 -0
- package/src/presentation/routes/index.ts +27 -0
- package/src/presentation/routes/read-logs.test.ts +504 -0
- package/src/presentation/routes/read-logs.ts +38 -0
- package/src/presentation/subscribers/on-job-terminated.test.ts +64 -0
- package/src/presentation/subscribers/on-job-terminated.ts +26 -0
- package/src/presentation/subscribers/on-log-stream-closed.test.ts +63 -0
- package/src/presentation/subscribers/on-log-stream-closed.ts +26 -0
- package/src/presentation/subscribers/on-step-attempt-terminated.ts +16 -0
- package/src/temporal/activities/close-abandoned-streams.test.ts +84 -0
- package/src/temporal/activities/close-abandoned-streams.ts +30 -0
- package/src/temporal/activities/compact-stream.test.ts +221 -0
- package/src/temporal/activities/compact-stream.ts +123 -0
- package/src/temporal/activities/compaction-reconcile.test.ts +143 -0
- package/src/temporal/activities/compaction-reconcile.ts +48 -0
- package/src/temporal/activities/index.ts +15 -0
- package/src/temporal/activities/reap-stale-open-streams.test.ts +48 -0
- package/src/temporal/activities/reap-stale-open-streams.ts +14 -0
- package/src/temporal/activities/retention-sweep.test.ts +38 -0
- package/src/temporal/activities/retention-sweep.ts +23 -0
- package/src/temporal/constants.ts +14 -0
- package/src/temporal/workflows/close-abandoned-streams.ts +25 -0
- package/src/temporal/workflows/compact-stream.ts +26 -0
- package/src/temporal/workflows/compaction-reconcile-cron.ts +14 -0
- package/src/temporal/workflows/index.ts +5 -0
- package/src/temporal/workflows/reap-stale-open-streams-cron.ts +13 -0
- package/src/temporal/workflows/retention-sweep-cron.ts +23 -0
- package/test/env.ts +37 -0
- package/test/factories/job-accounting.ts +38 -0
- package/test/fixtures/closed-stream.ts +53 -0
- package/test/fixtures/lease-token.ts +42 -0
- package/test/fixtures/ndjson.ts +34 -0
- package/test/globalSetup.ts +44 -0
- package/test/index.ts +9 -0
- package/test/queries.ts +54 -0
- package/test/setup.ts +17 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- 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
|
+
});
|