@shipfox/api-logs 10.1.0 → 11.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 (52) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +26 -0
  3. package/dist/core/append-logs.d.ts.map +1 -1
  4. package/dist/core/append-logs.js +13 -5
  5. package/dist/core/append-logs.js.map +1 -1
  6. package/dist/core/close-stream.d.ts.map +1 -1
  7. package/dist/core/close-stream.js +16 -1
  8. package/dist/core/close-stream.js.map +1 -1
  9. package/dist/core/entities/attempt-stream.d.ts +2 -1
  10. package/dist/core/entities/attempt-stream.d.ts.map +1 -1
  11. package/dist/core/entities/attempt-stream.js.map +1 -1
  12. package/dist/core/session/claude/rows.d.ts +15 -3
  13. package/dist/core/session/claude/rows.d.ts.map +1 -1
  14. package/dist/core/session/claude/rows.js +127 -13
  15. package/dist/core/session/claude/rows.js.map +1 -1
  16. package/dist/core/session/claude-parser.d.ts +4 -2
  17. package/dist/core/session/claude-parser.d.ts.map +1 -1
  18. package/dist/core/session/claude-parser.js +56 -10
  19. package/dist/core/session/claude-parser.js.map +1 -1
  20. package/dist/db/db.d.ts +113 -0
  21. package/dist/db/db.d.ts.map +1 -1
  22. package/dist/db/schema/attempt-streams.d.ts +113 -0
  23. package/dist/db/schema/attempt-streams.d.ts.map +1 -1
  24. package/dist/db/schema/attempt-streams.js +2 -0
  25. package/dist/db/schema/attempt-streams.js.map +1 -1
  26. package/dist/db/streams.d.ts +3 -1
  27. package/dist/db/streams.d.ts.map +1 -1
  28. package/dist/db/streams.js +6 -1
  29. package/dist/db/streams.js.map +1 -1
  30. package/dist/presentation/routes/read-logs.d.ts.map +1 -1
  31. package/dist/presentation/routes/read-logs.js +11 -5
  32. package/dist/presentation/routes/read-logs.js.map +1 -1
  33. package/dist/tsconfig.test.tsbuildinfo +1 -1
  34. package/drizzle/0002_wise_dorian_gray.sql +1 -0
  35. package/drizzle/meta/0002_snapshot.json +561 -0
  36. package/drizzle/meta/_journal.json +7 -0
  37. package/package.json +3 -3
  38. package/src/core/append-logs.test.ts +57 -0
  39. package/src/core/append-logs.ts +15 -3
  40. package/src/core/close-stream.ts +17 -1
  41. package/src/core/entities/attempt-stream.ts +2 -1
  42. package/src/core/session/claude/rows.ts +172 -9
  43. package/src/core/session/claude-parser.test.ts +453 -4
  44. package/src/core/session/claude-parser.ts +59 -10
  45. package/src/db/schema/attempt-streams.ts +3 -1
  46. package/src/db/streams.ts +10 -2
  47. package/src/presentation/routes/read-logs.test.ts +18 -4
  48. package/src/presentation/routes/read-logs.ts +9 -5
  49. package/test/fixtures/claude-session/agent-step.jsonl +12 -0
  50. package/test/fixtures/claude-session/sdk-system-subtypes.json +32 -0
  51. package/test/fixtures/claude-session.ts +45 -0
  52. package/tsconfig.build.tsbuildinfo +1 -1
@@ -50,13 +50,15 @@ const fakeUserAuth: AuthMethod = {
50
50
  throw new ClientError('Invalid user token', 'unauthorized', {status: 401});
51
51
  }
52
52
  const header = request.headers['x-test-workspace'];
53
- const workspaceId = Array.isArray(header) ? header[0] : header;
53
+ const value = Array.isArray(header) ? header[0] : header;
54
+ const [workspaceId, status] = value?.split('|') ?? [];
55
+ const workspaceStatus = status === 'suspended' || status === 'deleted' ? status : 'active';
54
56
  setUserContext(
55
57
  request,
56
58
  buildUserContext({
57
59
  userId: 'user-1',
58
60
  email: 'user@example.com',
59
- memberships: workspaceId ? [{workspaceId, role: 'admin'}] : [],
61
+ memberships: workspaceId ? [{workspaceId, role: 'admin', workspaceStatus}] : [],
60
62
  }),
61
63
  );
62
64
  return Promise.resolve();
@@ -174,11 +176,11 @@ describe('GET /steps/:stepId/attempts/:attempt/logs', () => {
174
176
  return `/steps/${stepId}/attempts/${attempt}/logs?cursor=${cursor}`;
175
177
  }
176
178
 
177
- function authedGet(stream: AttemptStream, cursor: number, workspaceId = stream.workspaceId) {
179
+ function authedGet(stream: AttemptStream, cursor: number, workspaceClaim = stream.workspaceId) {
178
180
  return app.inject({
179
181
  method: 'GET',
180
182
  url: readUrl(stream.stepId, stream.attempt, cursor),
181
- headers: {authorization: 'Bearer user', 'x-test-workspace': workspaceId},
183
+ headers: {authorization: 'Bearer user', 'x-test-workspace': workspaceClaim},
182
184
  });
183
185
  }
184
186
 
@@ -288,6 +290,18 @@ describe('GET /steps/:stepId/attempts/:attempt/logs', () => {
288
290
  expect(res.json().code).toBe('not-found');
289
291
  });
290
292
 
293
+ it('returns workspace-suspended for a suspended membership claim', async () => {
294
+ const stream = await arrangeStream({
295
+ workspaceId: crypto.randomUUID(),
296
+ chunks: [ndjsonBody(outputLine('secret\n'))],
297
+ });
298
+
299
+ const res = await authedGet(stream, 0, `${stream.workspaceId}|suspended`);
300
+
301
+ expect(res.statusCode).toBe(409);
302
+ expect(res.json().code).toBe('workspace-suspended');
303
+ });
304
+
291
305
  it('serves an open stream inline with the next cursor and stream state', async () => {
292
306
  const workspaceId = crypto.randomUUID();
293
307
  const lineA = outputLine('alpha\n');
@@ -1,4 +1,4 @@
1
- import {requireUserContext} from '@shipfox/api-auth-context';
1
+ import {requireWorkspaceResourceAccess} from '@shipfox/api-auth-context';
2
2
  import {readLogsQuerySchema, readLogsResponseSchema} from '@shipfox/api-logs-dto';
3
3
  import {ClientError, defineRoute} from '@shipfox/node-fastify';
4
4
  import {z} from 'zod';
@@ -22,16 +22,20 @@ export const readLogsRoute = defineRoute({
22
22
  },
23
23
  },
24
24
  handler: async (request) => {
25
- const user = requireUserContext(request);
26
25
  const {stepId, attempt} = request.params;
27
26
  const {cursor} = request.query;
28
27
 
29
28
  const stream = await getStreamByStepAttempt({stepId, attempt});
30
- // 404 covers both "no such stream" and "not your workspace" so the endpoint never
31
- // leaks the existence of another workspace's step.
32
- if (!stream || !user.canAccess(stream.workspaceId)) {
29
+ // Missing streams and memberships remain 404 so the endpoint never leaks another
30
+ // workspace's step; lifecycle claims propagate their stable access errors.
31
+ if (!stream) {
33
32
  throw new ClientError('Logs not found', 'not-found', {status: 404});
34
33
  }
34
+ requireWorkspaceResourceAccess({
35
+ request,
36
+ workspaceId: stream.workspaceId,
37
+ notFoundError: new ClientError('Logs not found', 'not-found', {status: 404}),
38
+ });
35
39
 
36
40
  return toReadLogsDto(await buildLogReadResult(stream, cursor));
37
41
  },
@@ -0,0 +1,12 @@
1
+ {"type":"system","subtype":"init","apiKeySource":"user","claude_code_version":"1.0.0","cwd":"/workspace","tools":["Read","Bash"],"mcp_servers":[],"model":"claude-sonnet-4-20250514","permissionMode":"default","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"uuid":"init-1","session_id":"session-1"}
2
+ {"type":"assistant","message":{"id":"msg-1","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"I will inspect the repository."},{"type":"tool_use","id":"toolu-1","name":"Read","input":{"file_path":"README.md"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":100,"output_tokens":20}},"parent_tool_use_id":null,"uuid":"assistant-1","session_id":"session-1"}
3
+ {"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu-1","content":[{"type":"text","text":"# Shipfox\n"}],"is_error":false}]},"parent_tool_use_id":null,"uuid":"user-1","session_id":"session-1"}
4
+ {"type":"system","subtype":"permission_denied","tool_name":"mcp__shipfox_outputs__set_output","tool_use_id":"toolu-2","decision_reason_type":"mode","decision_reason":"Permission mode","message":"Permission denied","uuid":"permission-1","session_id":"session-1"}
5
+ {"type":"system","subtype":"api_retry","attempt":2,"max_retries":3,"retry_delay_ms":1500,"error_status":429,"error":"rate_limit","uuid":"retry-1","session_id":"session-1"}
6
+ {"type":"system","subtype":"hook_response","hook_id":"hook-1","hook_name":"PreToolUse","hook_event":"PreToolUse","output":"Hook completed","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"hook-1","session_id":"session-1"}
7
+ {"type":"system","subtype":"compact_boundary","compact_metadata":{"trigger":"auto","pre_tokens":12000,"post_tokens":4000,"duration_ms":250},"uuid":"compact-1","session_id":"session-1"}
8
+ {"type":"result","subtype":"success","is_error":false,"result":"First turn response","duration_ms":1200,"duration_api_ms":900,"num_turns":1,"total_cost_usd":0.0042,"uuid":"result-1","session_id":"session-1"}
9
+ {"type":"system","subtype":"init","apiKeySource":"user","claude_code_version":"1.0.0","cwd":"/workspace","tools":["Read","Bash"],"mcp_servers":[],"model":"claude-sonnet-4-20250514","permissionMode":"default","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"uuid":"init-2","session_id":"session-1"}
10
+ {"type":"user","message":{"role":"user","content":"The previous turn ended without setting required workflow outputs: answer. Call set_output for each missing key, then provide your final response."},"parent_tool_use_id":null,"isSynthetic":true,"uuid":"user-2","session_id":"session-1"}
11
+ {"type":"assistant","message":{"id":"msg-2","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"The answer is ready."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":120,"output_tokens":8}},"parent_tool_use_id":null,"uuid":"assistant-2","session_id":"session-1"}
12
+ {"type":"result","subtype":"success","is_error":false,"result":"Final response","duration_ms":800,"duration_api_ms":600,"num_turns":1,"total_cost_usd":0.0021,"uuid":"result-2","session_id":"session-1"}
@@ -0,0 +1,32 @@
1
+ {
2
+ "package": "@anthropic-ai/claude-agent-sdk",
3
+ "version": "0.3.200",
4
+ "subtypes": [
5
+ "api_retry",
6
+ "commands_changed",
7
+ "compact_boundary",
8
+ "elicitation_complete",
9
+ "files_persisted",
10
+ "hook_progress",
11
+ "hook_response",
12
+ "hook_started",
13
+ "informational",
14
+ "init",
15
+ "local_command_output",
16
+ "memory_recall",
17
+ "mirror_error",
18
+ "model_refusal_fallback",
19
+ "model_refusal_no_fallback",
20
+ "notification",
21
+ "permission_denied",
22
+ "plugin_install",
23
+ "session_state_changed",
24
+ "status",
25
+ "task_notification",
26
+ "task_progress",
27
+ "task_started",
28
+ "task_updated",
29
+ "thinking_tokens",
30
+ "worker_shutting_down"
31
+ ]
32
+ }
@@ -0,0 +1,45 @@
1
+ import {readFileSync} from 'node:fs';
2
+ import type {AgentSessionRecord} from '#core/session/session-record.js';
3
+
4
+ export interface ClaudeSystemSubtypeFixture {
5
+ package: string;
6
+ version: string;
7
+ subtypes: string[];
8
+ }
9
+
10
+ const SDK_ENTRY_URL = import.meta.resolve('@anthropic-ai/claude-agent-sdk');
11
+ const SDK_TYPES_URL = new URL('./sdk.d.ts', SDK_ENTRY_URL);
12
+
13
+ export function readClaudeSessionFixture(name: string): AgentSessionRecord[] {
14
+ const contents = readFileSync(
15
+ new URL(`./claude-session/${name}`, import.meta.url),
16
+ 'utf8',
17
+ ).trim();
18
+
19
+ return contents.split('\n').map((data, index) => ({data, ts: index + 1}));
20
+ }
21
+
22
+ export function readClaudeSystemSubtypeFixture(): ClaudeSystemSubtypeFixture {
23
+ const contents = readFileSync(
24
+ new URL('./claude-session/sdk-system-subtypes.json', import.meta.url),
25
+ 'utf8',
26
+ );
27
+ return JSON.parse(contents) as ClaudeSystemSubtypeFixture;
28
+ }
29
+
30
+ export function readInstalledClaudeSdkVersion(): string {
31
+ const contents = readFileSync(new URL('./package.json', SDK_TYPES_URL), 'utf8');
32
+ const packageJson = JSON.parse(contents) as {version?: unknown};
33
+ if (typeof packageJson.version !== 'string') {
34
+ throw new Error('The installed Claude SDK package has no version');
35
+ }
36
+ return packageJson.version;
37
+ }
38
+
39
+ export function readInstalledClaudeSystemSubtypes(): string[] {
40
+ const declaration = readFileSync(SDK_TYPES_URL, 'utf8');
41
+ return [...declaration.matchAll(/type: 'system';\s+subtype: '([^']+)'/g)]
42
+ .map((match) => match[1])
43
+ .filter((subtype): subtype is string => subtype !== undefined)
44
+ .sort();
45
+ }