@shipfox/api-logs-dto 10.2.0 → 16.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-logs-dto",
3
3
  "license": "MIT",
4
- "version": "10.2.0",
4
+ "version": "16.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -15,10 +15,15 @@
15
15
  ".": {
16
16
  "types": "./dist/index.d.ts",
17
17
  "default": "./dist/index.js"
18
+ },
19
+ "./inter-module": {
20
+ "types": "./dist/inter-module.d.ts",
21
+ "default": "./dist/inter-module.js"
18
22
  }
19
23
  },
20
24
  "dependencies": {
21
- "zod": "^4.4.3"
25
+ "zod": "^4.4.3",
26
+ "@shipfox/inter-module": "0.2.3"
22
27
  },
23
28
  "imports": {
24
29
  "#*": "./dist/*"
package/src/index.ts CHANGED
@@ -11,7 +11,9 @@ export {
11
11
  appendLogsQuerySchema,
12
12
  appendLogsResponseSchema,
13
13
  type LogRecord,
14
+ type LogWriterConflictResponseDto,
14
15
  logRecordSchema,
16
+ logWriterConflictResponseSchema,
15
17
  MAX_RECORD_DATA_BYTES,
16
18
  MAX_RECORD_NAME_BYTES,
17
19
  type OffsetGapResponseDto,
@@ -25,6 +27,7 @@ export {
25
27
  readLogsQuerySchema,
26
28
  readLogsResponseSchema,
27
29
  SESSION_VIEW_VERSION,
30
+ type ServerLogRecord,
28
31
  type SessionViewDto,
29
32
  type SessionViewLifecycleRow,
30
33
  type SessionViewMessageRow,
@@ -34,6 +37,7 @@ export {
34
37
  type SessionViewThinkingRow,
35
38
  type SessionViewToolCallRow,
36
39
  type SessionViewToolResultRow,
40
+ serverLogRecordSchema,
37
41
  sessionViewLifecycleRowSchema,
38
42
  sessionViewMessageRowSchema,
39
43
  sessionViewRawRowSchema,
@@ -0,0 +1,69 @@
1
+ import {logsInterModuleContract} from './inter-module.js';
2
+
3
+ describe('logsInterModuleContract', () => {
4
+ test('accepts server-origin append commands', () => {
5
+ const input = logsInterModuleContract.methods.appendServerRecords.input.parse({
6
+ jobId: '00000000-0000-4000-8000-000000000001',
7
+ workspaceId: '00000000-0000-4000-8000-000000000002',
8
+ projectId: '00000000-0000-4000-8000-000000000003',
9
+ workflowRunAttemptId: '00000000-0000-4000-8000-000000000004',
10
+ stepId: '00000000-0000-4000-8000-000000000005',
11
+ attempt: 1,
12
+ records: [
13
+ {v: 1, ts: 1, type: 'output', stream: 'stdout', data: 'hello'},
14
+ {v: 1, ts: 1, type: 'group_start', group_id: 'g1', parent_group_id: null, name: 'call'},
15
+ {v: 1, ts: 1, type: 'group_end', group_id: 'g1'},
16
+ ],
17
+ });
18
+
19
+ expect(input.attempt).toBe(1);
20
+ expect(input.records).toHaveLength(3);
21
+ expect(input.records[1]).toMatchObject({type: 'group_start'});
22
+ });
23
+
24
+ test('rejects records outside the server-writable stored union', () => {
25
+ const result = logsInterModuleContract.methods.appendServerRecords.input.safeParse({
26
+ jobId: '00000000-0000-4000-8000-000000000001',
27
+ workspaceId: '00000000-0000-4000-8000-000000000002',
28
+ projectId: '00000000-0000-4000-8000-000000000003',
29
+ workflowRunAttemptId: '00000000-0000-4000-8000-000000000004',
30
+ stepId: '00000000-0000-4000-8000-000000000005',
31
+ attempt: 1,
32
+ records: [{v: 1, ts: 1, type: 'agent_session', data: 'raw entry, not yet normalized'}],
33
+ });
34
+
35
+ expect(result.success).toBe(false);
36
+ });
37
+
38
+ test('rejects server-only tombstones', () => {
39
+ const result = logsInterModuleContract.methods.appendServerRecords.input.safeParse({
40
+ jobId: '00000000-0000-4000-8000-000000000001',
41
+ workspaceId: '00000000-0000-4000-8000-000000000002',
42
+ projectId: '00000000-0000-4000-8000-000000000003',
43
+ workflowRunAttemptId: '00000000-0000-4000-8000-000000000004',
44
+ stepId: '00000000-0000-4000-8000-000000000005',
45
+ attempt: 1,
46
+ records: [{v: 1, ts: 1, type: 'capped'}],
47
+ });
48
+
49
+ expect(result.success).toBe(false);
50
+ });
51
+
52
+ test('keeps identity, attempt, and output bounds enforced', () => {
53
+ const input = {
54
+ jobId: '00000000-0000-4000-8000-000000000001',
55
+ workspaceId: '00000000-0000-4000-8000-000000000002',
56
+ projectId: '00000000-0000-4000-8000-000000000003',
57
+ workflowRunAttemptId: '00000000-0000-4000-8000-000000000004',
58
+ stepId: '00000000-0000-4000-8000-000000000005',
59
+ attempt: 1,
60
+ records: [],
61
+ };
62
+ const schema = logsInterModuleContract.methods.appendServerRecords;
63
+
64
+ expect(schema.input.safeParse({...input, attempt: 0}).success).toBe(false);
65
+ expect(schema.input.safeParse({...input, attempt: 2_147_483_648}).success).toBe(false);
66
+ expect(schema.input.safeParse({...input, jobId: 'not-a-uuid'}).success).toBe(false);
67
+ expect(schema.output.safeParse({committedLength: 0, capped: true}).success).toBe(true);
68
+ });
69
+ });
@@ -0,0 +1,69 @@
1
+ import {defineInterModuleContract, type InterModuleClient} from '@shipfox/inter-module';
2
+ import {z} from 'zod';
3
+ import {serverLogRecordSchema} from './schemas/index.js';
4
+
5
+ const idSchema = z.string().uuid();
6
+
7
+ /**
8
+ * Producer-owned Logs commands used by synchronous callers. The only method
9
+ * today is the server-origin append for server-executed steps (the tool step
10
+ * executor): it writes already-normalized stored records through the same
11
+ * offset-CAS and budget pipeline as the lease-bound runner route, but with
12
+ * chunk `origin` `server` and a tail-derived CAS offset (the caller owns no
13
+ * spool cursor). This is a trusted internal boundary, not an authorization
14
+ * boundary: the caller must derive the identity fields from its execution
15
+ * context rather than pass through arbitrary external input.
16
+ */
17
+ export const logsInterModuleContract = defineInterModuleContract({
18
+ module: 'logs',
19
+ methods: {
20
+ appendServerRecords: {
21
+ input: z.object({
22
+ jobId: idSchema,
23
+ workspaceId: idSchema,
24
+ projectId: idSchema,
25
+ workflowRunAttemptId: idSchema,
26
+ stepId: idSchema,
27
+ attempt: z
28
+ .number()
29
+ .int()
30
+ .min(1)
31
+ .max(2_147_483_647)
32
+ .describe('Attempt number of the step this batch belongs to.'),
33
+ /**
34
+ * Already-normalized server-writable stored records (the read union
35
+ * without server-only tombstones), serialized to whole newline-terminated
36
+ * NDJSON lines on ingest. Server-origin records skip the raw-to-stored
37
+ * normalization the runner path applies: they are stored verbatim. Callers should
38
+ * coalesce records into batches up to (but never over) `LOG_APPEND_BODY_LIMIT_BYTES`;
39
+ * an empty batch is reserved for a heartbeat. A server-origin writer owns its stream;
40
+ * it cannot be mixed with a lease-bound runner writer because their byte cursors differ.
41
+ */
42
+ records: z.array(serverLogRecordSchema),
43
+ }),
44
+ output: z.object({
45
+ committedLength: z
46
+ .number()
47
+ .int()
48
+ .min(0)
49
+ .describe(
50
+ 'New server-held byte position of the stream after this batch was applied (the tail the next call continues from).',
51
+ ),
52
+ capped: z
53
+ .boolean()
54
+ .describe(
55
+ 'When true, the per-job log budget is exhausted and further output is dropped.',
56
+ ),
57
+ }),
58
+ errors: {
59
+ 'lease-stream-mismatch': z.object({}),
60
+ 'malformed-log-chunk': z.object({}),
61
+ 'append-body-too-large': z.object({maxBytes: z.number().int().positive()}),
62
+ 'runner-writer-active': z.object({}),
63
+ 'offset-gap': z.object({committedLength: z.number().int().nonnegative()}),
64
+ },
65
+ },
66
+ },
67
+ });
68
+
69
+ export type LogsModuleClient = InterModuleClient<typeof logsInterModuleContract>;
@@ -4,7 +4,7 @@ import {z} from 'zod';
4
4
  * Append endpoint contract: `POST .../steps/:stepId/logs?attempt=N&offset=B`.
5
5
  *
6
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
7
+ * Zod-validated object: it is parsed line by line against the raw log record
8
8
  * union (`rawLogRecordSchema`). `offset` is a position in the raw
9
9
  * NDJSON spool stream; both `offset` and the returned `committed_length` are
10
10
  * bounded far below 2^53 by the accrual budget, so JavaScript `number` is safe.
@@ -53,3 +53,10 @@ export const offsetGapResponseSchema = z.object({
53
53
  });
54
54
 
55
55
  export type OffsetGapResponseDto = z.infer<typeof offsetGapResponseSchema>;
56
+
57
+ /** A lease append was attempted on a stream owned by a server-origin writer. */
58
+ export const logWriterConflictResponseSchema = z.object({
59
+ code: z.literal('log-writer-conflict'),
60
+ });
61
+
62
+ export type LogWriterConflictResponseDto = z.infer<typeof logWriterConflictResponseSchema>;
@@ -3,6 +3,8 @@ export {
3
3
  type AppendLogsResponseDto,
4
4
  appendLogsQuerySchema,
5
5
  appendLogsResponseSchema,
6
+ type LogWriterConflictResponseDto,
7
+ logWriterConflictResponseSchema,
6
8
  type OffsetGapResponseDto,
7
9
  offsetGapResponseSchema,
8
10
  } from './append.js';
@@ -21,6 +23,8 @@ export {
21
23
  parseRawLogRecordLine,
22
24
  type RawLogRecord,
23
25
  rawLogRecordSchema,
26
+ type ServerLogRecord,
27
+ serverLogRecordSchema,
24
28
  } from './record.js';
25
29
  export {
26
30
  SESSION_VIEW_VERSION,
@@ -70,7 +70,9 @@ const readLogsPresignedSchema = z.object({
70
70
  .number()
71
71
  .int()
72
72
  .min(0)
73
- .describe('Committed append byte position for the attempt. This is the runner CAS axis.'),
73
+ .describe(
74
+ 'Committed append byte position for the attempt. This is the active writer CAS axis.',
75
+ ),
74
76
  truncated: z
75
77
  .boolean()
76
78
  .describe('True when the stream was force-closed because the runner stopped reporting.'),
@@ -2,10 +2,10 @@ import {z} from 'zod';
2
2
  import {sessionViewRowSchema} from './session-view.js';
3
3
 
4
4
  /**
5
- * NDJSON log record contract one JSON object per line, runner-framed.
5
+ * NDJSON log record contract: one JSON object per line, runner-framed.
6
6
  *
7
7
  * `offset` / `committed_length` are byte positions in the raw append NDJSON spool stream
8
- * (envelope included) the offset-CAS axis the runner tracks. The per-job accrual
8
+ * (envelope included): the offset-CAS axis the runner tracks. The per-job accrual
9
9
  * budget charges the normalized NDJSON bytes the server stores, so framing and control records
10
10
  * count against it too. The per-record byte caps below bound each record so a single
11
11
  * entry's overhead is known and a runner cannot grow storage without moving the
@@ -123,19 +123,27 @@ export const rawLogRecordSchema = z.discriminatedUnion('type', [
123
123
  ]);
124
124
 
125
125
  /** Stored/read records: regular records, normalized agent sessions, and tombstones. */
126
- export const logRecordSchema = z.discriminatedUnion('type', [
126
+ const storedLogRecordSchemas = [
127
127
  logOutput,
128
128
  logGroupStart,
129
129
  logGroupEnd,
130
130
  logEnd,
131
131
  logGap,
132
132
  agentSession,
133
+ ] as const;
134
+
135
+ export const logRecordSchema = z.discriminatedUnion('type', [
136
+ ...storedLogRecordSchemas,
133
137
  logCapped,
134
138
  logRunnerLost,
135
139
  ]);
136
140
 
141
+ /** Records a trusted server-origin writer may append: stored records without tombstones. */
142
+ export const serverLogRecordSchema = z.discriminatedUnion('type', storedLogRecordSchemas);
143
+
137
144
  export type RawLogRecord = z.infer<typeof rawLogRecordSchema>;
138
145
  export type LogRecord = z.infer<typeof logRecordSchema>;
146
+ export type ServerLogRecord = z.infer<typeof serverLogRecordSchema>;
139
147
 
140
148
  export function parseLogRecordLine(line: string): LogRecord {
141
149
  return logRecordSchema.parse(JSON.parse(line));
@@ -144,7 +152,7 @@ export function parseLogRecordLine(line: string): LogRecord {
144
152
  /**
145
153
  * Parses one NDJSON line against the raw write union. A forged
146
154
  * server-only `capped` / `runner_lost` record fails here even though it is valid
147
- * under the read union this is the write-path forgery guard.
155
+ * under the read union: this is the write-path forgery guard.
148
156
  */
149
157
  export function parseRawLogRecordLine(line: string): RawLogRecord {
150
158
  return rawLogRecordSchema.parse(JSON.parse(line));