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

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,138 +0,0 @@
1
- // api/src/user-store-pg.ts
2
- // PgUserStore — PostgreSQL async variant of UserStore
3
- // Follows same pattern as PgRunStore in @studio/engine
4
- // Table prefix studio_ to avoid conflicts with user app tables
5
-
6
- import { createRequire } from 'node:module';
7
- import type { User, DailyUsage } from './user-store.js';
8
- export type { User, DailyUsage } from './user-store.js';
9
-
10
- export class PgUserStore {
11
- private pool: import('pg').Pool;
12
- private schemaReady: Promise<void> | null = null;
13
-
14
- constructor(connectionString: string) {
15
- const _require = createRequire(import.meta.url);
16
- const { Pool } = _require('pg') as typeof import('pg');
17
- this.pool = new Pool({ connectionString });
18
- }
19
-
20
- private ensureSchema(): Promise<void> {
21
- if (!this.schemaReady) {
22
- this.schemaReady = this.initSchema();
23
- }
24
- return this.schemaReady;
25
- }
26
-
27
- private async initSchema(): Promise<void> {
28
- await this.pool.query(`
29
- CREATE TABLE IF NOT EXISTS studio_users (
30
- id TEXT PRIMARY KEY,
31
- email TEXT UNIQUE NOT NULL,
32
- plan TEXT NOT NULL DEFAULT 'free',
33
- api_key TEXT UNIQUE NOT NULL,
34
- created_at TEXT NOT NULL
35
- )
36
- `);
37
- await this.pool.query(`
38
- CREATE TABLE IF NOT EXISTS studio_usage (
39
- user_id TEXT NOT NULL,
40
- date TEXT NOT NULL,
41
- runs_count INTEGER NOT NULL DEFAULT 0,
42
- tokens_used INTEGER NOT NULL DEFAULT 0,
43
- PRIMARY KEY (user_id, date)
44
- )
45
- `);
46
- }
47
-
48
- async getUserByApiKey(apiKey: string): Promise<User | null> {
49
- await this.ensureSchema();
50
- const res = await this.pool.query<User>(
51
- 'SELECT id, email, plan, api_key, created_at FROM studio_users WHERE api_key = $1',
52
- [apiKey]
53
- );
54
- return res.rows[0] ?? null;
55
- }
56
-
57
- async getUserById(id: string): Promise<User | null> {
58
- await this.ensureSchema();
59
- const res = await this.pool.query<User>(
60
- 'SELECT id, email, plan, api_key, created_at FROM studio_users WHERE id = $1',
61
- [id]
62
- );
63
- return res.rows[0] ?? null;
64
- }
65
-
66
- async getUserByEmail(email: string): Promise<User | null> {
67
- await this.ensureSchema();
68
- const res = await this.pool.query<User>(
69
- 'SELECT id, email, plan, api_key, created_at FROM studio_users WHERE email = $1',
70
- [email]
71
- );
72
- return res.rows[0] ?? null;
73
- }
74
-
75
- async listUsers(): Promise<User[]> {
76
- await this.ensureSchema();
77
- const res = await this.pool.query<User>(
78
- 'SELECT id, email, plan, api_key, created_at FROM studio_users ORDER BY created_at ASC'
79
- );
80
- return res.rows;
81
- }
82
-
83
- async saveUser(user: User): Promise<void> {
84
- await this.ensureSchema();
85
- await this.pool.query(
86
- `INSERT INTO studio_users (id, email, plan, api_key, created_at)
87
- VALUES ($1, $2, $3, $4, $5)
88
- ON CONFLICT (id) DO UPDATE SET
89
- email = EXCLUDED.email,
90
- plan = EXCLUDED.plan,
91
- api_key = EXCLUDED.api_key,
92
- created_at = EXCLUDED.created_at`,
93
- [user.id, user.email, user.plan, user.api_key, user.created_at]
94
- );
95
- }
96
-
97
- async deleteUser(id: string): Promise<void> {
98
- await this.ensureSchema();
99
- await this.pool.query('DELETE FROM studio_users WHERE id = $1', [id]);
100
- }
101
-
102
- async getDailyUsage(userId: string, date: string): Promise<DailyUsage> {
103
- await this.ensureSchema();
104
- const res = await this.pool.query<DailyUsage>(
105
- 'SELECT user_id, date, runs_count, tokens_used FROM studio_usage WHERE user_id = $1 AND date = $2',
106
- [userId, date]
107
- );
108
- return res.rows[0] ?? { user_id: userId, date, runs_count: 0, tokens_used: 0 };
109
- }
110
-
111
- async incrementRuns(userId: string, date: string): Promise<void> {
112
- await this.ensureSchema();
113
- await this.pool.query(
114
- `INSERT INTO studio_usage (user_id, date, runs_count, tokens_used)
115
- VALUES ($1, $2, 1, 0)
116
- ON CONFLICT (user_id, date) DO UPDATE SET
117
- runs_count = studio_usage.runs_count + 1`,
118
- [userId, date]
119
- );
120
- }
121
-
122
- async incrementTokens(userId: string, date: string, tokens: number): Promise<void> {
123
- await this.ensureSchema();
124
- await this.pool.query(
125
- `INSERT INTO studio_usage (user_id, date, runs_count, tokens_used)
126
- VALUES ($1, $2, 0, $3)
127
- ON CONFLICT (user_id, date) DO UPDATE SET
128
- tokens_used = studio_usage.tokens_used + EXCLUDED.tokens_used`,
129
- [userId, date, tokens]
130
- );
131
- }
132
-
133
- async close(): Promise<void> {
134
- await this.pool.end();
135
- }
136
- }
137
-
138
- export type AnyUserStore = import('./user-store.js').UserStore | PgUserStore;
package/src/user-store.ts DELETED
@@ -1,125 +0,0 @@
1
- // api/src/user-store.ts
2
- // UserStore — SQLite persistence for users and daily usage
3
- // Follows same pattern as WebhookStore and IntegrationStore
4
- // Uses the same DB file as the run store (.studio/runs/runs.db)
5
-
6
- import { createRequire } from 'node:module';
7
-
8
- export interface User {
9
- id: string;
10
- email: string;
11
- plan: string;
12
- api_key: string;
13
- created_at: string;
14
- }
15
-
16
- export interface DailyUsage {
17
- user_id: string;
18
- date: string; // YYYY-MM-DD
19
- runs_count: number;
20
- tokens_used: number;
21
- }
22
-
23
- export class UserStore {
24
- private db: import('better-sqlite3').Database;
25
-
26
- constructor(dbPath: string) {
27
- const _require = createRequire(import.meta.url);
28
- const Database = _require('better-sqlite3') as new (path: string) => import('better-sqlite3').Database;
29
- this.db = new Database(dbPath);
30
- this.db.pragma('journal_mode = WAL');
31
- this.initSchema();
32
- }
33
-
34
- private initSchema(): void {
35
- this.db.exec(`
36
- CREATE TABLE IF NOT EXISTS users (
37
- id TEXT PRIMARY KEY,
38
- email TEXT UNIQUE NOT NULL,
39
- plan TEXT NOT NULL DEFAULT 'free',
40
- api_key TEXT UNIQUE NOT NULL,
41
- created_at TEXT NOT NULL
42
- );
43
-
44
- CREATE TABLE IF NOT EXISTS usage (
45
- user_id TEXT NOT NULL,
46
- date TEXT NOT NULL,
47
- runs_count INTEGER NOT NULL DEFAULT 0,
48
- tokens_used INTEGER NOT NULL DEFAULT 0,
49
- PRIMARY KEY (user_id, date)
50
- );
51
- `);
52
- }
53
-
54
- getUserByApiKey(apiKey: string): User | null {
55
- const row = this.db
56
- .prepare('SELECT id, email, plan, api_key, created_at FROM users WHERE api_key = ?')
57
- .get(apiKey) as User | undefined;
58
- return row ?? null;
59
- }
60
-
61
- getUserById(id: string): User | null {
62
- const row = this.db
63
- .prepare('SELECT id, email, plan, api_key, created_at FROM users WHERE id = ?')
64
- .get(id) as User | undefined;
65
- return row ?? null;
66
- }
67
-
68
- getUserByEmail(email: string): User | null {
69
- const row = this.db
70
- .prepare('SELECT id, email, plan, api_key, created_at FROM users WHERE email = ?')
71
- .get(email) as User | undefined;
72
- return row ?? null;
73
- }
74
-
75
- listUsers(): User[] {
76
- return this.db
77
- .prepare('SELECT id, email, plan, api_key, created_at FROM users ORDER BY created_at ASC')
78
- .all() as User[];
79
- }
80
-
81
- saveUser(user: User): void {
82
- this.db.prepare(`
83
- INSERT INTO users (id, email, plan, api_key, created_at)
84
- VALUES (?, ?, ?, ?, ?)
85
- ON CONFLICT(id) DO UPDATE SET
86
- email = excluded.email,
87
- plan = excluded.plan,
88
- api_key = excluded.api_key,
89
- created_at = excluded.created_at
90
- `).run(user.id, user.email, user.plan, user.api_key, user.created_at);
91
- }
92
-
93
- deleteUser(id: string): void {
94
- this.db.prepare('DELETE FROM users WHERE id = ?').run(id);
95
- }
96
-
97
- getDailyUsage(userId: string, date: string): DailyUsage {
98
- const row = this.db
99
- .prepare('SELECT user_id, date, runs_count, tokens_used FROM usage WHERE user_id = ? AND date = ?')
100
- .get(userId, date) as DailyUsage | undefined;
101
- return row ?? { user_id: userId, date, runs_count: 0, tokens_used: 0 };
102
- }
103
-
104
- incrementRuns(userId: string, date: string): void {
105
- this.db.prepare(`
106
- INSERT INTO usage (user_id, date, runs_count, tokens_used)
107
- VALUES (?, ?, 1, 0)
108
- ON CONFLICT (user_id, date) DO UPDATE SET
109
- runs_count = runs_count + 1
110
- `).run(userId, date);
111
- }
112
-
113
- incrementTokens(userId: string, date: string, tokens: number): void {
114
- this.db.prepare(`
115
- INSERT INTO usage (user_id, date, runs_count, tokens_used)
116
- VALUES (?, ?, 0, ?)
117
- ON CONFLICT (user_id, date) DO UPDATE SET
118
- tokens_used = tokens_used + excluded.tokens_used
119
- `).run(userId, date, tokens);
120
- }
121
-
122
- close(): void {
123
- this.db.close();
124
- }
125
- }
@@ -1,3 +0,0 @@
1
- // Re-exported from @studio/engine — implementation lives there
2
- export { resolveRepoPath, cloneRepo } from '@studio-foundation/engine';
3
- export type { RepoResolveOptions } from '@studio-foundation/engine';
@@ -1,142 +0,0 @@
1
- // WebhookDispatcher — listens to pipeline events and delivers them to registered webhooks
2
- // Handles HMAC-SHA256 signing, retry logic (30s / 5min / 15min), and failure tracking
3
-
4
- import { createHmac, randomUUID } from 'node:crypto';
5
- import type { SseEventType } from './event-bus.js';
6
- import type { WebhookStore } from './webhook-store.js';
7
-
8
- export type WebhookEventType =
9
- | 'pipeline_start'
10
- | 'pipeline_complete'
11
- | 'stage_complete'
12
- | 'stage_rejected'
13
- | 'stage_failed'
14
- | 'group_feedback';
15
-
16
- // Retry delays in ms: attempt 1 failed → 30s, attempt 2 failed → 5min, attempt 3 failed → 15min
17
- const RETRY_DELAYS = [30_000, 5 * 60_000, 15 * 60_000];
18
-
19
- function mapToWebhookEvent(sseType: SseEventType, data: unknown): WebhookEventType | null {
20
- switch (sseType) {
21
- case 'pipeline_start':
22
- return 'pipeline_start';
23
- case 'pipeline_complete':
24
- return 'pipeline_complete';
25
- case 'stage_complete': {
26
- const status = (data as { status?: string }).status;
27
- if (status === 'rejected') return 'stage_rejected';
28
- if (status === 'failed') return 'stage_failed';
29
- return 'stage_complete';
30
- }
31
- case 'group_feedback':
32
- return 'group_feedback';
33
- default:
34
- return null;
35
- }
36
- }
37
-
38
- function sign(body: string, secret: string): string {
39
- const hmac = createHmac('sha256', secret);
40
- hmac.update(body, 'utf-8');
41
- return `sha256=${hmac.digest('hex')}`;
42
- }
43
-
44
- export class WebhookDispatcher {
45
- constructor(
46
- private readonly store: WebhookStore,
47
- private readonly projectName: string,
48
- private readonly fetcher: typeof fetch = fetch,
49
- ) {}
50
-
51
- async handleBusEvent(runId: string, sseType: SseEventType, data: unknown): Promise<void> {
52
- const webhookEvent = mapToWebhookEvent(sseType, data);
53
- if (!webhookEvent) return;
54
-
55
- const webhooks = this.store.listWebhooks().filter(
56
- w => w.status === 'active' && w.events.includes(webhookEvent),
57
- );
58
-
59
- await Promise.all(
60
- webhooks.map(webhook => this.dispatch(webhook.id, webhookEvent, runId, data, 1)),
61
- );
62
- }
63
-
64
- private async dispatch(
65
- webhookId: string,
66
- event: WebhookEventType,
67
- runId: string,
68
- sourceData: unknown,
69
- attempt: number,
70
- ): Promise<void> {
71
- const webhook = this.store.getWebhook(webhookId);
72
- if (!webhook || webhook.status === 'failed') return;
73
-
74
- const payload = {
75
- event,
76
- run_id: runId,
77
- project: this.projectName,
78
- ts: new Date().toISOString(),
79
- ...(sourceData as object),
80
- };
81
-
82
- const body = JSON.stringify(payload);
83
- const headers: Record<string, string> = {
84
- 'Content-Type': 'application/json',
85
- 'X-Studio-Event': event,
86
- };
87
- if (webhook.secret) {
88
- headers['X-Studio-Signature'] = sign(body, webhook.secret);
89
- }
90
-
91
- const deliveryId = randomUUID();
92
- this.store.saveDelivery({
93
- id: deliveryId,
94
- webhook_id: webhookId,
95
- event,
96
- run_id: runId,
97
- status: 'pending',
98
- attempt,
99
- created_at: new Date().toISOString(),
100
- });
101
-
102
- try {
103
- const response = await this.fetcher(webhook.url, {
104
- method: 'POST',
105
- headers,
106
- body,
107
- signal: AbortSignal.timeout(10_000),
108
- });
109
-
110
- if (response.ok) {
111
- this.store.updateDelivery(deliveryId, 'success');
112
- } else {
113
- this.handleFailure(webhookId, event, runId, sourceData, attempt, deliveryId);
114
- }
115
- } catch {
116
- this.handleFailure(webhookId, event, runId, sourceData, attempt, deliveryId);
117
- }
118
- }
119
-
120
- private handleFailure(
121
- webhookId: string,
122
- event: WebhookEventType,
123
- runId: string,
124
- sourceData: unknown,
125
- attempt: number,
126
- deliveryId: string,
127
- ): void {
128
- const maxAttempts = RETRY_DELAYS.length + 1; // 4 total (1 original + 3 retries)
129
-
130
- if (attempt >= maxAttempts) {
131
- this.store.updateDelivery(deliveryId, 'failed');
132
- this.store.markWebhookFailed(webhookId);
133
- return;
134
- }
135
-
136
- this.store.updateDelivery(deliveryId, 'retrying');
137
- const delay = RETRY_DELAYS[attempt - 1];
138
- setTimeout(() => {
139
- void this.dispatch(webhookId, event, runId, sourceData, attempt + 1);
140
- }, delay);
141
- }
142
- }
@@ -1,141 +0,0 @@
1
- // WebhookStore — SQLite persistence for webhook registrations and deliveries
2
- // Uses the same DB file as the run store (.studio/runs/runs.db)
3
-
4
- import { createRequire } from 'node:module';
5
-
6
- export interface WebhookRegistration {
7
- id: string;
8
- url: string;
9
- events: string[];
10
- secret?: string;
11
- status: 'active' | 'failed';
12
- created_at: string;
13
- }
14
-
15
- export interface WebhookDelivery {
16
- id: string;
17
- webhook_id: string;
18
- event: string;
19
- run_id: string;
20
- status: 'pending' | 'retrying' | 'success' | 'failed';
21
- attempt: number;
22
- created_at: string;
23
- }
24
-
25
- export class WebhookStore {
26
- private db: import('better-sqlite3').Database;
27
-
28
- constructor(dbPath: string) {
29
- const _require = createRequire(import.meta.url);
30
- const Database = _require('better-sqlite3') as new (path: string) => import('better-sqlite3').Database;
31
- this.db = new Database(dbPath);
32
- this.db.pragma('journal_mode = WAL');
33
- this.initSchema();
34
- }
35
-
36
- private initSchema(): void {
37
- this.db.exec(`
38
- CREATE TABLE IF NOT EXISTS webhooks (
39
- id TEXT PRIMARY KEY,
40
- url TEXT NOT NULL,
41
- events TEXT NOT NULL,
42
- secret TEXT,
43
- status TEXT NOT NULL DEFAULT 'active',
44
- created_at TEXT NOT NULL
45
- );
46
-
47
- CREATE TABLE IF NOT EXISTS webhook_deliveries (
48
- id TEXT PRIMARY KEY,
49
- webhook_id TEXT NOT NULL,
50
- event TEXT NOT NULL,
51
- run_id TEXT NOT NULL,
52
- status TEXT NOT NULL,
53
- attempt INTEGER NOT NULL,
54
- created_at TEXT NOT NULL,
55
- FOREIGN KEY (webhook_id) REFERENCES webhooks(id)
56
- );
57
- `);
58
- }
59
-
60
- saveWebhook(webhook: WebhookRegistration): void {
61
- this.db.prepare(`
62
- INSERT INTO webhooks (id, url, events, secret, status, created_at)
63
- VALUES (?, ?, ?, ?, ?, ?)
64
- ON CONFLICT(id) DO UPDATE SET
65
- url = excluded.url,
66
- events = excluded.events,
67
- secret = excluded.secret,
68
- status = excluded.status
69
- `).run(
70
- webhook.id,
71
- webhook.url,
72
- JSON.stringify(webhook.events),
73
- webhook.secret ?? null,
74
- webhook.status,
75
- webhook.created_at,
76
- );
77
- }
78
-
79
- getWebhook(id: string): WebhookRegistration | null {
80
- const row = this.db.prepare(
81
- 'SELECT id, url, events, secret, status, created_at FROM webhooks WHERE id = ?'
82
- ).get(id) as { id: string; url: string; events: string; secret: string | null; status: string; created_at: string } | undefined;
83
-
84
- if (!row) return null;
85
- return {
86
- id: row.id,
87
- url: row.url,
88
- events: JSON.parse(row.events) as string[],
89
- ...(row.secret != null ? { secret: row.secret } : {}),
90
- status: row.status as 'active' | 'failed',
91
- created_at: row.created_at,
92
- };
93
- }
94
-
95
- listWebhooks(): WebhookRegistration[] {
96
- const rows = this.db.prepare(
97
- 'SELECT id, url, events, secret, status, created_at FROM webhooks ORDER BY created_at DESC'
98
- ).all() as Array<{ id: string; url: string; events: string; secret: string | null; status: string; created_at: string }>;
99
-
100
- return rows.map(row => ({
101
- id: row.id,
102
- url: row.url,
103
- events: JSON.parse(row.events) as string[],
104
- ...(row.secret != null ? { secret: row.secret } : {}),
105
- status: row.status as 'active' | 'failed',
106
- created_at: row.created_at,
107
- }));
108
- }
109
-
110
- deleteWebhook(id: string): boolean {
111
- const result = this.db.prepare('DELETE FROM webhooks WHERE id = ?').run(id);
112
- return result.changes > 0;
113
- }
114
-
115
- markWebhookFailed(id: string): void {
116
- this.db.prepare("UPDATE webhooks SET status = 'failed' WHERE id = ?").run(id);
117
- }
118
-
119
- saveDelivery(delivery: WebhookDelivery): void {
120
- this.db.prepare(`
121
- INSERT INTO webhook_deliveries (id, webhook_id, event, run_id, status, attempt, created_at)
122
- VALUES (?, ?, ?, ?, ?, ?, ?)
123
- `).run(
124
- delivery.id,
125
- delivery.webhook_id,
126
- delivery.event,
127
- delivery.run_id,
128
- delivery.status,
129
- delivery.attempt,
130
- delivery.created_at,
131
- );
132
- }
133
-
134
- updateDelivery(id: string, status: WebhookDelivery['status']): void {
135
- this.db.prepare('UPDATE webhook_deliveries SET status = ? WHERE id = ?').run(status, id);
136
- }
137
-
138
- close(): void {
139
- this.db.close();
140
- }
141
- }