@studio-foundation/api 0.3.0-beta.1 → 0.3.0-beta.6

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 (62) hide show
  1. package/package.json +7 -4
  2. package/ARCHITECTURE.md +0 -52
  3. package/src/.gitkeep +0 -0
  4. package/src/api.ts +0 -8
  5. package/src/bootstrap.ts +0 -259
  6. package/src/event-bus.ts +0 -64
  7. package/src/index.ts +0 -58
  8. package/src/integration-runtime.ts +0 -180
  9. package/src/integration-store.ts +0 -125
  10. package/src/integrations/linear/failure-handler.ts +0 -93
  11. package/src/integrations/linear/webhook-handler.ts +0 -156
  12. package/src/integrations/registry.ts +0 -12
  13. package/src/integrations/types.ts +0 -37
  14. package/src/launcher.ts +0 -214
  15. package/src/logger.ts +0 -50
  16. package/src/plans.ts +0 -43
  17. package/src/routes/agents.ts +0 -131
  18. package/src/routes/config.ts +0 -154
  19. package/src/routes/contracts.ts +0 -205
  20. package/src/routes/pipelines.ts +0 -146
  21. package/src/routes/projects.ts +0 -237
  22. package/src/routes/runs.ts +0 -468
  23. package/src/routes/skills.ts +0 -136
  24. package/src/routes/tools.ts +0 -222
  25. package/src/routes/users.ts +0 -169
  26. package/src/routes/validate.ts +0 -196
  27. package/src/routes/webhooks.ts +0 -120
  28. package/src/server.ts +0 -155
  29. package/src/spawners/http-api-spawner.ts +0 -96
  30. package/src/user-store-pg.ts +0 -138
  31. package/src/user-store.ts +0 -125
  32. package/src/utils/repo-resolver.ts +0 -3
  33. package/src/webhook-dispatcher.ts +0 -142
  34. package/src/webhook-store.ts +0 -141
  35. package/tests/agents.test.ts +0 -164
  36. package/tests/cancel.test.ts +0 -120
  37. package/tests/config.test.ts +0 -196
  38. package/tests/contracts.test.ts +0 -302
  39. package/tests/event-bus.test.ts +0 -53
  40. package/tests/http-api-spawner.test.ts +0 -158
  41. package/tests/integration-runtime.test.ts +0 -199
  42. package/tests/integration-store.test.ts +0 -66
  43. package/tests/integrations/linear/failure-handler.test.ts +0 -113
  44. package/tests/integrations/linear/webhook-handler.test.ts +0 -191
  45. package/tests/launcher.test.ts +0 -380
  46. package/tests/linear-webhook.test.ts +0 -390
  47. package/tests/pipelines.test.ts +0 -166
  48. package/tests/projects.test.ts +0 -283
  49. package/tests/runs.test.ts +0 -379
  50. package/tests/server.test.ts +0 -208
  51. package/tests/skills.test.ts +0 -149
  52. package/tests/sse.test.ts +0 -129
  53. package/tests/tools.test.ts +0 -233
  54. package/tests/user-store.test.ts +0 -93
  55. package/tests/users.test.ts +0 -189
  56. package/tests/utils/repo-resolver.test.ts +0 -105
  57. package/tests/validate.test.ts +0 -233
  58. package/tests/webhook-dispatcher.test.ts +0 -214
  59. package/tests/webhook-store.test.ts +0 -98
  60. package/tests/webhooks.test.ts +0 -176
  61. package/tsconfig.json +0 -24
  62. package/vitest.config.ts +0 -7
@@ -1,208 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { mkdirSync } from 'node:fs';
3
- import { resolve } from 'node:path';
4
- import { buildServer } from '../src/server.js';
5
- import { InMemoryRunStore } from '@studio-foundation/engine';
6
- import type { RunLauncher } from '../src/launcher.js';
7
- import type { IntegrationRuntime } from '../src/integration-runtime.js';
8
- import type { IntegrationStore } from '../src/integration-store.js';
9
- import { UserStore, type User } from '../src/user-store.js';
10
- import { DEFAULT_PLANS } from '../src/plans.js';
11
- import type { WebhookStore } from '../src/webhook-store.js';
12
-
13
- function makeMockLauncher(): RunLauncher {
14
- return {
15
- launch: async () => ({ run_id: 'mock-run-id' }),
16
- cancel: async () => {},
17
- };
18
- }
19
-
20
- const nullIntegrationRuntime = { registerRoutes: () => {} } as unknown as IntegrationRuntime;
21
- const nullIntegrationStore = {} as unknown as IntegrationStore;
22
-
23
- function makeTempUserStore(): UserStore {
24
- const dir = resolve('/tmp', `.studio-auth-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
25
- mkdirSync(dir, { recursive: true });
26
- return new UserStore(resolve(dir, 'runs.db'));
27
- }
28
-
29
- const nullWebhookStore = {} as unknown as WebhookStore;
30
-
31
- const proUser: User = {
32
- id: 'user-pro-1',
33
- email: 'pro@example.com',
34
- plan: 'pro',
35
- api_key: 'sk-pro-key',
36
- created_at: '2026-01-01T00:00:00.000Z',
37
- };
38
-
39
- describe('buildServer — auth', () => {
40
- it('no api key configured → requests pass without Authorization', async () => {
41
- const server = buildServer({
42
- store: new InMemoryRunStore(),
43
- launcher: makeMockLauncher(),
44
- configsDir: '/tmp/.studio',
45
- projectName: 'test-project',
46
- apiConfig: {}, // no key
47
- integrationRuntime: nullIntegrationRuntime,
48
- integrationStore: nullIntegrationStore,
49
- });
50
-
51
- const res = await server.inject({ method: 'GET', url: '/api/projects' });
52
- expect(res.statusCode).not.toBe(401);
53
- });
54
-
55
- it('api key configured → missing Authorization → 401', async () => {
56
- const server = buildServer({
57
- store: new InMemoryRunStore(),
58
- launcher: makeMockLauncher(),
59
- configsDir: '/tmp/.studio',
60
- projectName: 'test-project',
61
- apiConfig: { key: 'sk-studio-secret' },
62
- integrationRuntime: nullIntegrationRuntime,
63
- integrationStore: nullIntegrationStore,
64
- });
65
-
66
- const res = await server.inject({ method: 'GET', url: '/api/projects' });
67
- expect(res.statusCode).toBe(401);
68
- expect(res.json()).toEqual({ error: 'Unauthorized' });
69
- });
70
-
71
- it('api key configured → wrong key → 401', async () => {
72
- const server = buildServer({
73
- store: new InMemoryRunStore(),
74
- launcher: makeMockLauncher(),
75
- configsDir: '/tmp/.studio',
76
- projectName: 'test-project',
77
- apiConfig: { key: 'sk-studio-secret' },
78
- integrationRuntime: nullIntegrationRuntime,
79
- integrationStore: nullIntegrationStore,
80
- });
81
-
82
- const res = await server.inject({
83
- method: 'GET',
84
- url: '/api/projects',
85
- headers: { Authorization: 'Bearer sk-studio-wrong' },
86
- });
87
- expect(res.statusCode).toBe(401);
88
- });
89
-
90
- it('api key configured → correct key → passes', async () => {
91
- const server = buildServer({
92
- store: new InMemoryRunStore(),
93
- launcher: makeMockLauncher(),
94
- configsDir: '/tmp/.studio',
95
- projectName: 'test-project',
96
- apiConfig: { key: 'sk-studio-secret' },
97
- integrationRuntime: nullIntegrationRuntime,
98
- integrationStore: nullIntegrationStore,
99
- });
100
-
101
- const res = await server.inject({
102
- method: 'GET',
103
- url: '/api/projects',
104
- headers: { Authorization: 'Bearer sk-studio-secret' },
105
- });
106
- expect(res.statusCode).not.toBe(401);
107
- });
108
- });
109
-
110
- describe('buildServer — multi-user auth', () => {
111
- it('user api_key → 200', async () => {
112
- const userStore = makeTempUserStore();
113
- userStore.saveUser(proUser);
114
-
115
- const server = buildServer({
116
- store: new InMemoryRunStore(),
117
- launcher: makeMockLauncher(),
118
- configsDir: '/tmp/.studio',
119
- projectName: 'test',
120
- apiConfig: {},
121
- integrationRuntime: nullIntegrationRuntime,
122
- integrationStore: nullIntegrationStore,
123
- userStore,
124
- plans: DEFAULT_PLANS,
125
- hasUsers: true,
126
- });
127
-
128
- const res = await server.inject({
129
- method: 'GET',
130
- url: '/api/projects',
131
- headers: { Authorization: 'Bearer sk-pro-key' },
132
- });
133
- userStore.close();
134
- expect(res.statusCode).not.toBe(401);
135
- });
136
-
137
- it('unknown api_key when users exist → 401', async () => {
138
- const userStore = makeTempUserStore();
139
- userStore.saveUser(proUser);
140
-
141
- const server = buildServer({
142
- store: new InMemoryRunStore(),
143
- launcher: makeMockLauncher(),
144
- configsDir: '/tmp/.studio',
145
- projectName: 'test',
146
- apiConfig: {},
147
- integrationRuntime: nullIntegrationRuntime,
148
- integrationStore: nullIntegrationStore,
149
- userStore,
150
- plans: DEFAULT_PLANS,
151
- hasUsers: true,
152
- });
153
-
154
- const res = await server.inject({
155
- method: 'GET',
156
- url: '/api/projects',
157
- headers: { Authorization: 'Bearer sk-unknown' },
158
- });
159
- userStore.close();
160
- expect(res.statusCode).toBe(401);
161
- });
162
-
163
- it('legacy api.key works when no users in DB (hasUsers=false)', async () => {
164
- const userStore = makeTempUserStore();
165
-
166
- const server = buildServer({
167
- store: new InMemoryRunStore(),
168
- launcher: makeMockLauncher(),
169
- configsDir: '/tmp/.studio',
170
- projectName: 'test',
171
- apiConfig: { key: 'sk-legacy-key' },
172
- integrationRuntime: nullIntegrationRuntime,
173
- integrationStore: nullIntegrationStore,
174
- userStore,
175
- plans: DEFAULT_PLANS,
176
- hasUsers: false,
177
- });
178
-
179
- const res = await server.inject({
180
- method: 'GET',
181
- url: '/api/projects',
182
- headers: { Authorization: 'Bearer sk-legacy-key' },
183
- });
184
- userStore.close();
185
- expect(res.statusCode).not.toBe(401);
186
- });
187
-
188
- it('no users, no api.key → open dev mode', async () => {
189
- const userStore = makeTempUserStore();
190
-
191
- const server = buildServer({
192
- store: new InMemoryRunStore(),
193
- launcher: makeMockLauncher(),
194
- configsDir: '/tmp/.studio',
195
- projectName: 'test',
196
- apiConfig: {},
197
- integrationRuntime: nullIntegrationRuntime,
198
- integrationStore: nullIntegrationStore,
199
- userStore,
200
- plans: DEFAULT_PLANS,
201
- hasUsers: false,
202
- });
203
-
204
- const res = await server.inject({ method: 'GET', url: '/api/projects' });
205
- userStore.close();
206
- expect(res.statusCode).not.toBe(401);
207
- });
208
- });
@@ -1,149 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
- import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
3
- import { resolve } from 'node:path';
4
- import { buildServer } from '../src/server.js';
5
- import { InMemoryRunStore } from '@studio-foundation/engine';
6
- import type { IntegrationRuntime } from '../src/integration-runtime.js';
7
- import type { IntegrationStore } from '../src/integration-store.js';
8
-
9
- const TMP = resolve('/tmp', `.studio-skills-test-${Date.now()}`);
10
- const SKILLS_DIR = resolve(TMP, 'skills');
11
-
12
- const nullIntegrationRuntime = { registerRoutes: () => {} } as unknown as IntegrationRuntime;
13
- const nullIntegrationStore = {} as unknown as IntegrationStore;
14
-
15
- function makeServer() {
16
- return buildServer({
17
- store: new InMemoryRunStore(),
18
- launcher: { launch: async () => ({ run_id: 'x' }), cancel: async () => {} },
19
- configsDir: TMP,
20
- projectName: 'test-project',
21
- apiConfig: {},
22
- studioVersion: '0.0.0-test',
23
- maskedConfig: { providers: [] },
24
- integrationRuntime: nullIntegrationRuntime,
25
- integrationStore: nullIntegrationStore,
26
- });
27
- }
28
-
29
- beforeAll(() => {
30
- mkdirSync(SKILLS_DIR, { recursive: true });
31
- writeFileSync(resolve(SKILLS_DIR, 'commit-conventions.skill.md'), '# Commit Conventions\nUse conventional commits.');
32
- writeFileSync(resolve(SKILLS_DIR, 'react-patterns.skill.md'), '# React Patterns\nPrefer hooks over HOCs.');
33
- writeFileSync(resolve(SKILLS_DIR, 'ignored.md'), ''); // must be ignored
34
- });
35
-
36
- afterAll(() => {
37
- rmSync(TMP, { recursive: true, force: true });
38
- });
39
-
40
- describe('GET /api/skills', () => {
41
- it('returns only *.skill.md files as skill names', async () => {
42
- const server = makeServer();
43
- const res = await server.inject({ method: 'GET', url: '/api/skills' });
44
- expect(res.statusCode).toBe(200);
45
- const { skills } = res.json() as { skills: string[] };
46
- expect(skills).toContain('commit-conventions');
47
- expect(skills).toContain('react-patterns');
48
- expect(skills).not.toContain('ignored');
49
- expect(skills).not.toContain('commit-conventions.skill.md');
50
- });
51
-
52
- it('returns empty array when skills dir is missing', async () => {
53
- const server = buildServer({
54
- store: new InMemoryRunStore(),
55
- launcher: { launch: async () => ({ run_id: 'x' }), cancel: async () => {} },
56
- configsDir: resolve('/tmp', `.studio-no-skills-${Date.now()}`),
57
- projectName: 'test-project',
58
- apiConfig: {},
59
- studioVersion: '0.0.0-test',
60
- maskedConfig: { providers: [] },
61
- integrationRuntime: nullIntegrationRuntime,
62
- integrationStore: nullIntegrationStore,
63
- });
64
- const res = await server.inject({ method: 'GET', url: '/api/skills' });
65
- expect(res.statusCode).toBe(200);
66
- expect((res.json() as { skills: string[] }).skills).toEqual([]);
67
- });
68
- });
69
-
70
- describe('GET /api/skills/:name', () => {
71
- it('returns skill content as JSON', async () => {
72
- const server = makeServer();
73
- const res = await server.inject({ method: 'GET', url: '/api/skills/commit-conventions' });
74
- expect(res.statusCode).toBe(200);
75
- const body = res.json() as { name: string; content: string };
76
- expect(body.name).toBe('commit-conventions');
77
- expect(body.content).toContain('conventional commits');
78
- });
79
-
80
- it('returns 404 for unknown skill', async () => {
81
- const server = makeServer();
82
- const res = await server.inject({ method: 'GET', url: '/api/skills/nonexistent' });
83
- expect(res.statusCode).toBe(404);
84
- expect((res.json() as { error: string }).error).toBe('Skill not found');
85
- });
86
- });
87
-
88
- describe('PUT /api/skills/:name', () => {
89
- it('creates a new skill file', async () => {
90
- const server = makeServer();
91
- const res = await server.inject({
92
- method: 'PUT',
93
- url: '/api/skills/new-skill',
94
- payload: { content: '# New Skill\nSome instructions.' },
95
- });
96
- expect(res.statusCode).toBe(200);
97
- const body = res.json() as { name: string; content: string };
98
- expect(body.name).toBe('new-skill');
99
- expect(body.content).toContain('New Skill');
100
- // Verify it can be read back
101
- const getRes = await server.inject({ method: 'GET', url: '/api/skills/new-skill' });
102
- expect(getRes.statusCode).toBe(200);
103
- expect((getRes.json() as { content: string }).content).toContain('New Skill');
104
- });
105
-
106
- it('updates an existing skill', async () => {
107
- const server = makeServer();
108
- const res = await server.inject({
109
- method: 'PUT',
110
- url: '/api/skills/react-patterns',
111
- payload: { content: '# React Patterns\nUpdated content.' },
112
- });
113
- expect(res.statusCode).toBe(200);
114
- const getRes = await server.inject({ method: 'GET', url: '/api/skills/react-patterns' });
115
- expect((getRes.json() as { content: string }).content).toContain('Updated content');
116
- });
117
-
118
- it('returns 400 when content field is missing', async () => {
119
- const server = makeServer();
120
- const res = await server.inject({
121
- method: 'PUT',
122
- url: '/api/skills/foo',
123
- payload: { name: 'foo' },
124
- });
125
- expect(res.statusCode).toBe(400);
126
- });
127
- });
128
-
129
- describe('DELETE /api/skills/:name', () => {
130
- it('deletes a skill and returns 204', async () => {
131
- const server = makeServer();
132
- await server.inject({
133
- method: 'PUT',
134
- url: '/api/skills/to-delete',
135
- payload: { content: '# To Delete' },
136
- });
137
- const res = await server.inject({ method: 'DELETE', url: '/api/skills/to-delete' });
138
- expect(res.statusCode).toBe(204);
139
- const getRes = await server.inject({ method: 'GET', url: '/api/skills/to-delete' });
140
- expect(getRes.statusCode).toBe(404);
141
- });
142
-
143
- it('returns 404 for nonexistent skill', async () => {
144
- const server = makeServer();
145
- const res = await server.inject({ method: 'DELETE', url: '/api/skills/nonexistent' });
146
- expect(res.statusCode).toBe(404);
147
- expect((res.json() as { error: string }).error).toBe('Skill not found');
148
- });
149
- });
package/tests/sse.test.ts DELETED
@@ -1,129 +0,0 @@
1
- import { describe, it, expect, vi, afterEach } from 'vitest';
2
- import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
3
- import { resolve } from 'node:path';
4
- import { buildServer } from '../src/server.js';
5
- import type { RunLauncher } from '../src/launcher.js';
6
- import type { IntegrationRuntime } from '../src/integration-runtime.js';
7
- import type { IntegrationStore } from '../src/integration-store.js';
8
-
9
- const TMP = resolve('/tmp', `studio-sse-test-${Date.now()}`);
10
- mkdirSync(TMP, { recursive: true });
11
-
12
- const nullIntegrationRuntime = { registerRoutes: () => {} } as unknown as IntegrationRuntime;
13
- const nullIntegrationStore = {} as unknown as IntegrationStore;
14
-
15
- afterEach(() => {
16
- rmSync(TMP, { recursive: true, force: true });
17
- mkdirSync(TMP, { recursive: true });
18
- });
19
-
20
- function makeDeps(overrides?: Partial<{
21
- runExists: boolean;
22
- runStatus: string;
23
- logPath: string | null;
24
- }>) {
25
- const { runExists = false, runStatus = 'running', logPath = null } = overrides ?? {};
26
-
27
- const mockStore = {
28
- getPipelineRun: vi.fn().mockReturnValue(
29
- runExists
30
- ? { id: 'run-1', pipeline_name: 'p', status: runStatus, started_at: '', stages: [] }
31
- : null
32
- ),
33
- getLogPath: vi.fn().mockReturnValue(logPath),
34
- saveLogPath: vi.fn(),
35
- listPipelineRuns: vi.fn().mockReturnValue([]),
36
- savePipelineRun: vi.fn(),
37
- updatePipelineRun: vi.fn(),
38
- saveStageRun: vi.fn(),
39
- saveTaskRun: vi.fn(),
40
- };
41
-
42
- const mockLauncher: RunLauncher = {
43
- launch: vi.fn().mockResolvedValue({ run_id: 'run-1' }),
44
- cancel: vi.fn().mockResolvedValue(undefined),
45
- subscribe: vi.fn().mockReturnValue(() => {}),
46
- };
47
-
48
- return { mockStore, mockLauncher };
49
- }
50
-
51
- describe('GET /api/runs/:id/stream', () => {
52
- it('returns 404 for unknown run', async () => {
53
- const { mockStore, mockLauncher } = makeDeps({ runExists: false });
54
- const fastify = buildServer({
55
- store: mockStore as never,
56
- launcher: mockLauncher,
57
- configsDir: TMP,
58
- projectName: 'test',
59
- apiConfig: {},
60
- integrationRuntime: nullIntegrationRuntime,
61
- integrationStore: nullIntegrationStore,
62
- });
63
-
64
- const res = await fastify.inject({ method: 'GET', url: '/api/runs/unknown-id/stream' });
65
- expect(res.statusCode).toBe(404);
66
- expect(JSON.parse(res.body)).toMatchObject({ error: 'Run not found' });
67
- });
68
-
69
- it('replays JSONL history and closes for a terminated run', async () => {
70
- const logFile = resolve(TMP, 'run.jsonl');
71
- writeFileSync(logFile, [
72
- JSON.stringify({ ts: '2026-01-01', run_id: 'r1', event: 'stage_complete', stage_name: 'brief-analysis', status: 'success' }),
73
- JSON.stringify({ ts: '2026-01-01', run_id: 'r1', event: 'pipeline_complete', status: 'success' }),
74
- ].join('\n') + '\n');
75
-
76
- const { mockStore, mockLauncher } = makeDeps({
77
- runExists: true,
78
- runStatus: 'success',
79
- logPath: logFile,
80
- });
81
-
82
- const fastify = buildServer({
83
- store: mockStore as never,
84
- launcher: mockLauncher,
85
- configsDir: TMP,
86
- projectName: 'test',
87
- apiConfig: {},
88
- integrationRuntime: nullIntegrationRuntime,
89
- integrationStore: nullIntegrationStore,
90
- });
91
-
92
- const res = await fastify.inject({ method: 'GET', url: '/api/runs/run-1/stream' });
93
- expect(res.headers['content-type']).toContain('text/event-stream');
94
- expect(res.body).toContain('event: stage_complete');
95
- expect(res.body).toContain('event: pipeline_complete');
96
- });
97
-
98
- it('filters events by ?events= query param', async () => {
99
- const logFile = resolve(TMP, 'run-filter.jsonl');
100
- writeFileSync(logFile, [
101
- JSON.stringify({ ts: '2026-01-01', run_id: 'r1', event: 'stage_complete', stage_name: 's1' }),
102
- JSON.stringify({ ts: '2026-01-01', run_id: 'r1', event: 'pipeline_complete', status: 'success' }),
103
- ].join('\n') + '\n');
104
-
105
- const { mockStore, mockLauncher } = makeDeps({
106
- runExists: true,
107
- runStatus: 'success',
108
- logPath: logFile,
109
- });
110
-
111
- const fastify = buildServer({
112
- store: mockStore as never,
113
- launcher: mockLauncher,
114
- configsDir: TMP,
115
- projectName: 'test',
116
- apiConfig: {},
117
- integrationRuntime: nullIntegrationRuntime,
118
- integrationStore: nullIntegrationStore,
119
- });
120
-
121
- const res = await fastify.inject({
122
- method: 'GET',
123
- url: '/api/runs/run-1/stream?events=pipeline_complete',
124
- });
125
-
126
- expect(res.body).not.toContain('event: stage_complete');
127
- expect(res.body).toContain('event: pipeline_complete');
128
- });
129
- });