@shipfox/api-logs-dto 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 (51) 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 +42 -0
  5. package/LICENSE +21 -0
  6. package/dist/events.d.ts +23 -0
  7. package/dist/events.d.ts.map +1 -0
  8. package/dist/events.js +15 -0
  9. package/dist/events.js.map +1 -0
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +4 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/schemas/append.d.ts +29 -0
  15. package/dist/schemas/append.d.ts.map +1 -0
  16. package/dist/schemas/append.js +25 -0
  17. package/dist/schemas/append.js.map +1 -0
  18. package/dist/schemas/index.d.ts +5 -0
  19. package/dist/schemas/index.d.ts.map +1 -0
  20. package/dist/schemas/index.js +6 -0
  21. package/dist/schemas/index.js.map +1 -0
  22. package/dist/schemas/read.d.ts +34 -0
  23. package/dist/schemas/read.d.ts.map +1 -0
  24. package/dist/schemas/read.js +48 -0
  25. package/dist/schemas/read.js.map +1 -0
  26. package/dist/schemas/record.d.ts +178 -0
  27. package/dist/schemas/record.d.ts.map +1 -0
  28. package/dist/schemas/record.js +136 -0
  29. package/dist/schemas/record.js.map +1 -0
  30. package/dist/schemas/session-view.d.ts +178 -0
  31. package/dist/schemas/session-view.d.ts.map +1 -0
  32. package/dist/schemas/session-view.js +73 -0
  33. package/dist/schemas/session-view.js.map +1 -0
  34. package/dist/tsconfig.test.tsbuildinfo +1 -0
  35. package/package.json +55 -0
  36. package/src/events.ts +22 -0
  37. package/src/index.ts +46 -0
  38. package/src/schemas/append.test.ts +50 -0
  39. package/src/schemas/append.ts +55 -0
  40. package/src/schemas/index.ts +45 -0
  41. package/src/schemas/read.test.ts +69 -0
  42. package/src/schemas/read.ts +84 -0
  43. package/src/schemas/record.test.ts +275 -0
  44. package/src/schemas/record.ts +151 -0
  45. package/src/schemas/session-view.test.ts +96 -0
  46. package/src/schemas/session-view.ts +89 -0
  47. package/tsconfig.build.json +9 -0
  48. package/tsconfig.build.tsbuildinfo +1 -0
  49. package/tsconfig.json +3 -0
  50. package/tsconfig.test.json +8 -0
  51. package/vitest.config.ts +3 -0
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@shipfox/api-logs-dto",
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-dto"
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
+ "zod": "^4.4.3"
36
+ },
37
+ "devDependencies": {
38
+ "@shipfox/biome": "1.8.1",
39
+ "@shipfox/depcruise": "1.0.1",
40
+ "@shipfox/ts-config": "1.3.8",
41
+ "@shipfox/vitest": "1.2.2",
42
+ "@shipfox/typescript": "1.1.6",
43
+ "@shipfox/swc": "1.2.5"
44
+ },
45
+ "scripts": {
46
+ "build": "shipfox-swc",
47
+ "check": "shipfox-biome-check",
48
+ "check:fix": "shipfox-biome-check --write",
49
+ "depcruise": "shipfox-depcruise",
50
+ "test": "shipfox-vitest-run",
51
+ "test:watch": "shipfox-vitest-watch",
52
+ "type": "shipfox-tsc-check",
53
+ "type:emit": "shipfox-tsc-emit"
54
+ }
55
+ }
package/src/events.ts ADDED
@@ -0,0 +1,22 @@
1
+ import {z} from 'zod';
2
+
3
+ const nonEmptyStringSchema = z.string().nonempty();
4
+
5
+ export const LOG_STREAM_CLOSED = 'logs.stream.closed' as const;
6
+
7
+ export const logStreamClosedEventSchema = z.object({
8
+ workspaceId: nonEmptyStringSchema,
9
+ jobId: nonEmptyStringSchema,
10
+ stepId: nonEmptyStringSchema,
11
+ attempt: z.number(),
12
+ streamId: nonEmptyStringSchema,
13
+ });
14
+ export type LogStreamClosedEvent = z.infer<typeof logStreamClosedEventSchema>;
15
+
16
+ export interface LogsEventMap {
17
+ [LOG_STREAM_CLOSED]: LogStreamClosedEvent;
18
+ }
19
+
20
+ export const logsEventSchemas = {
21
+ [LOG_STREAM_CLOSED]: logStreamClosedEventSchema,
22
+ } satisfies Record<keyof LogsEventMap, z.ZodType>;
package/src/index.ts ADDED
@@ -0,0 +1,46 @@
1
+ export {
2
+ LOG_STREAM_CLOSED,
3
+ type LogStreamClosedEvent,
4
+ type LogsEventMap,
5
+ logStreamClosedEventSchema,
6
+ logsEventSchemas,
7
+ } from './events.js';
8
+ export {
9
+ type AppendLogsQueryDto,
10
+ type AppendLogsResponseDto,
11
+ appendLogsQuerySchema,
12
+ appendLogsResponseSchema,
13
+ type LogRecord,
14
+ logRecordSchema,
15
+ MAX_RECORD_DATA_BYTES,
16
+ MAX_RECORD_NAME_BYTES,
17
+ type OffsetGapResponseDto,
18
+ offsetGapResponseSchema,
19
+ parseLogRecordLine,
20
+ parseRawLogRecordLine,
21
+ type RawLogRecord,
22
+ type ReadLogsQueryDto,
23
+ type ReadLogsResponseDto,
24
+ rawLogRecordSchema,
25
+ readLogsQuerySchema,
26
+ readLogsResponseSchema,
27
+ SESSION_VIEW_VERSION,
28
+ type SessionViewDto,
29
+ type SessionViewLifecycleRow,
30
+ type SessionViewMessageRow,
31
+ type SessionViewRawRow,
32
+ type SessionViewRow,
33
+ type SessionViewRowMeta,
34
+ type SessionViewThinkingRow,
35
+ type SessionViewToolCallRow,
36
+ type SessionViewToolResultRow,
37
+ sessionViewLifecycleRowSchema,
38
+ sessionViewMessageRowSchema,
39
+ sessionViewRawRowSchema,
40
+ sessionViewRowMetaSchema,
41
+ sessionViewRowSchema,
42
+ sessionViewSchema,
43
+ sessionViewThinkingRowSchema,
44
+ sessionViewToolCallRowSchema,
45
+ sessionViewToolResultRowSchema,
46
+ } from './schemas/index.js';
@@ -0,0 +1,50 @@
1
+ import {appendLogsQuerySchema, offsetGapResponseSchema} from './append.js';
2
+
3
+ describe('appendLogsQuerySchema', () => {
4
+ it('coerces string query params to integers', () => {
5
+ const parsed = appendLogsQuerySchema.parse({attempt: '1', offset: '0'});
6
+
7
+ expect(parsed).toEqual({attempt: 1, offset: 0});
8
+ });
9
+
10
+ it('accepts offset 0', () => {
11
+ const parsed = appendLogsQuerySchema.parse({attempt: '2', offset: '0'});
12
+
13
+ expect(parsed.offset).toBe(0);
14
+ });
15
+
16
+ it('rejects attempt below 1', () => {
17
+ const parse = () => appendLogsQuerySchema.parse({attempt: '0', offset: '0'});
18
+
19
+ expect(parse).toThrow();
20
+ });
21
+
22
+ it('rejects a negative offset', () => {
23
+ const parse = () => appendLogsQuerySchema.parse({attempt: '1', offset: '-5'});
24
+
25
+ expect(parse).toThrow();
26
+ });
27
+
28
+ it('rejects a non-integer offset', () => {
29
+ const parse = () => appendLogsQuerySchema.parse({attempt: '1', offset: '1.5'});
30
+
31
+ expect(parse).toThrow();
32
+ });
33
+
34
+ it('rejects an attempt beyond the Postgres integer range', () => {
35
+ const parse = () => appendLogsQuerySchema.parse({attempt: '2147483648', offset: '0'});
36
+
37
+ expect(parse).toThrow();
38
+ });
39
+ });
40
+
41
+ describe('offsetGapResponseSchema', () => {
42
+ it('matches the ClientError wire shape', () => {
43
+ const parsed = offsetGapResponseSchema.parse({
44
+ code: 'offset-gap',
45
+ details: {committed_length: 42},
46
+ });
47
+
48
+ expect(parsed).toEqual({code: 'offset-gap', details: {committed_length: 42}});
49
+ });
50
+ });
@@ -0,0 +1,55 @@
1
+ import {z} from 'zod';
2
+
3
+ /**
4
+ * Append endpoint contract: `POST .../steps/:stepId/logs?attempt=N&offset=B`.
5
+ *
6
+ * The body is raw NDJSON bytes (whole records, newline-terminated), not a
7
+ * Zod-validated object — it is parsed line by line against the raw log record
8
+ * union (`rawLogRecordSchema`). `offset` is a position in the raw
9
+ * NDJSON spool stream; both `offset` and the returned `committed_length` are
10
+ * bounded far below 2^53 by the accrual budget, so JavaScript `number` is safe.
11
+ */
12
+ export const appendLogsQuerySchema = z.object({
13
+ attempt: z.coerce
14
+ .number()
15
+ .int()
16
+ .min(1)
17
+ .max(2_147_483_647)
18
+ .describe('Attempt number of the step this chunk belongs to.'),
19
+ offset: z.coerce
20
+ .number()
21
+ .int()
22
+ .min(0)
23
+ .describe(
24
+ 'Byte position of this chunk in the raw NDJSON spool. Must equal the server-held committed length: an earlier offset is acknowledged as already applied, a later offset returns 409 so the runner rewinds.',
25
+ ),
26
+ });
27
+
28
+ export type AppendLogsQueryDto = z.infer<typeof appendLogsQuerySchema>;
29
+
30
+ export const appendLogsResponseSchema = z.object({
31
+ committed_length: z
32
+ .number()
33
+ .int()
34
+ .min(0)
35
+ .describe('New server-held byte position after this chunk was applied.'),
36
+ capped: z
37
+ .boolean()
38
+ .describe('When true, the per-job log budget is exhausted and further output is dropped.'),
39
+ });
40
+
41
+ export type AppendLogsResponseDto = z.infer<typeof appendLogsResponseSchema>;
42
+
43
+ /** Body of the 409 returned on an offset gap, so the runner rewinds its spool cursor. */
44
+ export const offsetGapResponseSchema = z.object({
45
+ code: z.literal('offset-gap'),
46
+ details: z.object({
47
+ committed_length: z
48
+ .number()
49
+ .int()
50
+ .min(0)
51
+ .describe('Current server-held committed length the runner should rewind to.'),
52
+ }),
53
+ });
54
+
55
+ export type OffsetGapResponseDto = z.infer<typeof offsetGapResponseSchema>;
@@ -0,0 +1,45 @@
1
+ export {
2
+ type AppendLogsQueryDto,
3
+ type AppendLogsResponseDto,
4
+ appendLogsQuerySchema,
5
+ appendLogsResponseSchema,
6
+ type OffsetGapResponseDto,
7
+ offsetGapResponseSchema,
8
+ } from './append.js';
9
+ export {
10
+ type ReadLogsQueryDto,
11
+ type ReadLogsResponseDto,
12
+ readLogsQuerySchema,
13
+ readLogsResponseSchema,
14
+ } from './read.js';
15
+ export {
16
+ type LogRecord,
17
+ logRecordSchema,
18
+ MAX_RECORD_DATA_BYTES,
19
+ MAX_RECORD_NAME_BYTES,
20
+ parseLogRecordLine,
21
+ parseRawLogRecordLine,
22
+ type RawLogRecord,
23
+ rawLogRecordSchema,
24
+ } from './record.js';
25
+ export {
26
+ SESSION_VIEW_VERSION,
27
+ type SessionViewDto,
28
+ type SessionViewLifecycleRow,
29
+ type SessionViewMessageRow,
30
+ type SessionViewRawRow,
31
+ type SessionViewRow,
32
+ type SessionViewRowMeta,
33
+ type SessionViewThinkingRow,
34
+ type SessionViewToolCallRow,
35
+ type SessionViewToolResultRow,
36
+ sessionViewLifecycleRowSchema,
37
+ sessionViewMessageRowSchema,
38
+ sessionViewRawRowSchema,
39
+ sessionViewRowMetaSchema,
40
+ sessionViewRowSchema,
41
+ sessionViewSchema,
42
+ sessionViewThinkingRowSchema,
43
+ sessionViewToolCallRowSchema,
44
+ sessionViewToolResultRowSchema,
45
+ } from './session-view.js';
@@ -0,0 +1,69 @@
1
+ import {readLogsQuerySchema, readLogsResponseSchema} from './read.js';
2
+
3
+ describe('readLogsQuerySchema', () => {
4
+ it('defaults the cursor to 0 when omitted', () => {
5
+ const parsed = readLogsQuerySchema.parse({});
6
+
7
+ expect(parsed.cursor).toBe(0);
8
+ });
9
+
10
+ it('coerces a string cursor to an integer', () => {
11
+ const parsed = readLogsQuerySchema.parse({cursor: '42'});
12
+
13
+ expect(parsed.cursor).toBe(42);
14
+ });
15
+
16
+ it('rejects a negative cursor', () => {
17
+ const parse = () => readLogsQuerySchema.parse({cursor: '-1'});
18
+
19
+ expect(parse).toThrow();
20
+ });
21
+ });
22
+
23
+ describe('readLogsResponseSchema', () => {
24
+ it('parses the inline variant', () => {
25
+ const parsed = readLogsResponseSchema.parse({
26
+ mode: 'inline',
27
+ ndjson: '{"v":1,"ts":1,"type":"output","stream":"stdout","data":"hi\\n"}\n',
28
+ next_cursor: 7,
29
+ has_more: true,
30
+ state: 'open',
31
+ truncated: false,
32
+ });
33
+
34
+ expect(parsed.mode).toBe('inline');
35
+ });
36
+
37
+ it('parses the presigned variant', () => {
38
+ const parsed = readLogsResponseSchema.parse({
39
+ mode: 'presigned',
40
+ url: 'https://storage.example/logs/object?sig=abc',
41
+ state: 'closed',
42
+ expires_at: new Date().toISOString(),
43
+ total_bytes: 1024,
44
+ truncated: true,
45
+ });
46
+
47
+ expect(parsed.mode).toBe('presigned');
48
+ });
49
+
50
+ it('rejects an unknown mode', () => {
51
+ const parse = () => readLogsResponseSchema.parse({mode: 'proxy'});
52
+
53
+ expect(parse).toThrow();
54
+ });
55
+
56
+ it('rejects a non-URL presigned url', () => {
57
+ const parse = () =>
58
+ readLogsResponseSchema.parse({
59
+ mode: 'presigned',
60
+ url: 'not a url',
61
+ state: 'closed',
62
+ expires_at: new Date().toISOString(),
63
+ total_bytes: 1,
64
+ truncated: false,
65
+ });
66
+
67
+ expect(parse).toThrow();
68
+ });
69
+ });
@@ -0,0 +1,84 @@
1
+ import {z} from 'zod';
2
+
3
+ /**
4
+ * Read endpoint contract: `GET .../steps/:stepId/attempts/:attempt/logs?cursor=N`.
5
+ *
6
+ * `cursor` is an opaque chunk-sequence position, not a byte offset. Server-injected
7
+ * control tombstones (`capped`, `runner_lost`) do not advance the runner byte axis, so
8
+ * the read walks chunks by insertion `seq` to keep every record in stream order. The
9
+ * same `seq` walk backs compaction, so the inline `ndjson` is byte-identical to the
10
+ * decompressed compacted object. Start at 0 and echo back `next_cursor` to page forward.
11
+ */
12
+ export const readLogsQuerySchema = z.object({
13
+ cursor: z.coerce
14
+ .number()
15
+ .int()
16
+ .min(0)
17
+ .default(0)
18
+ .describe(
19
+ 'Opaque chunk-sequence cursor. Start at 0; echo back the previous response next_cursor to page forward. Not a byte offset.',
20
+ ),
21
+ });
22
+
23
+ export type ReadLogsQueryDto = z.infer<typeof readLogsQuerySchema>;
24
+
25
+ /**
26
+ * Inline shape: stored NDJSON bytes from `cursor`, parsed client-side with
27
+ * `parseLogRecordLine`. Served while the stream is open or closed but not yet compacted.
28
+ * The bytes carry every record of every type for the `(step, attempt)`; the client
29
+ * filters by record type for display.
30
+ */
31
+ const readLogsInlineSchema = z.object({
32
+ mode: z.literal('inline'),
33
+ ndjson: z
34
+ .string()
35
+ .describe('Stored NDJSON: every record for this (step, attempt) from cursor, in stream order.'),
36
+ next_cursor: z
37
+ .number()
38
+ .int()
39
+ .min(0)
40
+ .describe('Cursor to pass on the next poll to fetch records after this page.'),
41
+ has_more: z
42
+ .boolean()
43
+ .describe(
44
+ 'True when more buffered records remain past this page; re-poll immediately to drain before tailing at the refresh interval.',
45
+ ),
46
+ state: z
47
+ .enum(['open', 'closed'])
48
+ .describe('Stream lifecycle: open still accepts appends, closed is terminal.'),
49
+ truncated: z
50
+ .boolean()
51
+ .describe('True when the stream was force-closed because the runner stopped reporting.'),
52
+ });
53
+
54
+ /**
55
+ * Presigned shape: a short-lived GET URL to the compacted object, fetched directly by
56
+ * the browser (API egress bypassed). Served once the stream is compacted (`object_key`
57
+ * set). The object is the same NDJSON the inline shape streams, gzip-compressed.
58
+ */
59
+ const readLogsPresignedSchema = z.object({
60
+ mode: z.literal('presigned'),
61
+ url: z.string().url().describe('Presigned GET URL for the compacted NDJSON object.'),
62
+ state: z
63
+ .literal('closed')
64
+ .describe('Compacted streams are closed before they are served by URL.'),
65
+ expires_at: z
66
+ .string()
67
+ .datetime({offset: true})
68
+ .describe('ISO 8601 instant after which the presigned URL stops working.'),
69
+ total_bytes: z
70
+ .number()
71
+ .int()
72
+ .min(0)
73
+ .describe('Committed append byte position for the attempt. This is the runner CAS axis.'),
74
+ truncated: z
75
+ .boolean()
76
+ .describe('True when the stream was force-closed because the runner stopped reporting.'),
77
+ });
78
+
79
+ export const readLogsResponseSchema = z.discriminatedUnion('mode', [
80
+ readLogsInlineSchema,
81
+ readLogsPresignedSchema,
82
+ ]);
83
+
84
+ export type ReadLogsResponseDto = z.infer<typeof readLogsResponseSchema>;