@studio-foundation/api 0.3.0-beta.1

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 (188) hide show
  1. package/ARCHITECTURE.md +52 -0
  2. package/LICENSE +663 -0
  3. package/README.md +161 -0
  4. package/dist/api.d.ts +8 -0
  5. package/dist/api.d.ts.map +1 -0
  6. package/dist/api.js +6 -0
  7. package/dist/api.js.map +1 -0
  8. package/dist/bootstrap.d.ts +66 -0
  9. package/dist/bootstrap.d.ts.map +1 -0
  10. package/dist/bootstrap.js +193 -0
  11. package/dist/bootstrap.js.map +1 -0
  12. package/dist/event-bus.d.ts +16 -0
  13. package/dist/event-bus.d.ts.map +1 -0
  14. package/dist/event-bus.js +37 -0
  15. package/dist/event-bus.js.map +1 -0
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +52 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/integration-runtime.d.ts +22 -0
  21. package/dist/integration-runtime.d.ts.map +1 -0
  22. package/dist/integration-runtime.js +150 -0
  23. package/dist/integration-runtime.js.map +1 -0
  24. package/dist/integration-store.d.ts +26 -0
  25. package/dist/integration-store.d.ts.map +1 -0
  26. package/dist/integration-store.js +80 -0
  27. package/dist/integration-store.js.map +1 -0
  28. package/dist/integrations/linear/failure-handler.d.ts +5 -0
  29. package/dist/integrations/linear/failure-handler.d.ts.map +1 -0
  30. package/dist/integrations/linear/failure-handler.js +73 -0
  31. package/dist/integrations/linear/failure-handler.js.map +1 -0
  32. package/dist/integrations/linear/webhook-handler.d.ts +6 -0
  33. package/dist/integrations/linear/webhook-handler.d.ts.map +1 -0
  34. package/dist/integrations/linear/webhook-handler.js +131 -0
  35. package/dist/integrations/linear/webhook-handler.js.map +1 -0
  36. package/dist/integrations/registry.d.ts +4 -0
  37. package/dist/integrations/registry.d.ts.map +1 -0
  38. package/dist/integrations/registry.js +9 -0
  39. package/dist/integrations/registry.js.map +1 -0
  40. package/dist/integrations/types.d.ts +33 -0
  41. package/dist/integrations/types.d.ts.map +1 -0
  42. package/dist/integrations/types.js +2 -0
  43. package/dist/integrations/types.js.map +1 -0
  44. package/dist/launcher.d.ts +45 -0
  45. package/dist/launcher.d.ts.map +1 -0
  46. package/dist/launcher.js +149 -0
  47. package/dist/launcher.js.map +1 -0
  48. package/dist/logger.d.ts +7 -0
  49. package/dist/logger.d.ts.map +1 -0
  50. package/dist/logger.js +37 -0
  51. package/dist/logger.js.map +1 -0
  52. package/dist/plans.d.ts +13 -0
  53. package/dist/plans.d.ts.map +1 -0
  54. package/dist/plans.js +33 -0
  55. package/dist/plans.js.map +1 -0
  56. package/dist/routes/agents.d.ts +6 -0
  57. package/dist/routes/agents.d.ts.map +1 -0
  58. package/dist/routes/agents.js +118 -0
  59. package/dist/routes/agents.js.map +1 -0
  60. package/dist/routes/config.d.ts +6 -0
  61. package/dist/routes/config.d.ts.map +1 -0
  62. package/dist/routes/config.js +129 -0
  63. package/dist/routes/config.js.map +1 -0
  64. package/dist/routes/contracts.d.ts +6 -0
  65. package/dist/routes/contracts.d.ts.map +1 -0
  66. package/dist/routes/contracts.js +188 -0
  67. package/dist/routes/contracts.js.map +1 -0
  68. package/dist/routes/pipelines.d.ts +6 -0
  69. package/dist/routes/pipelines.d.ts.map +1 -0
  70. package/dist/routes/pipelines.js +133 -0
  71. package/dist/routes/pipelines.js.map +1 -0
  72. package/dist/routes/projects.d.ts +6 -0
  73. package/dist/routes/projects.d.ts.map +1 -0
  74. package/dist/routes/projects.js +220 -0
  75. package/dist/routes/projects.js.map +1 -0
  76. package/dist/routes/runs.d.ts +6 -0
  77. package/dist/routes/runs.d.ts.map +1 -0
  78. package/dist/routes/runs.js +427 -0
  79. package/dist/routes/runs.js.map +1 -0
  80. package/dist/routes/skills.d.ts +6 -0
  81. package/dist/routes/skills.d.ts.map +1 -0
  82. package/dist/routes/skills.js +123 -0
  83. package/dist/routes/skills.js.map +1 -0
  84. package/dist/routes/tools.d.ts +6 -0
  85. package/dist/routes/tools.d.ts.map +1 -0
  86. package/dist/routes/tools.js +193 -0
  87. package/dist/routes/tools.js.map +1 -0
  88. package/dist/routes/users.d.ts +6 -0
  89. package/dist/routes/users.d.ts.map +1 -0
  90. package/dist/routes/users.js +152 -0
  91. package/dist/routes/users.js.map +1 -0
  92. package/dist/routes/validate.d.ts +6 -0
  93. package/dist/routes/validate.d.ts.map +1 -0
  94. package/dist/routes/validate.js +171 -0
  95. package/dist/routes/validate.js.map +1 -0
  96. package/dist/routes/webhooks.d.ts +8 -0
  97. package/dist/routes/webhooks.d.ts.map +1 -0
  98. package/dist/routes/webhooks.js +102 -0
  99. package/dist/routes/webhooks.js.map +1 -0
  100. package/dist/server.d.ts +46 -0
  101. package/dist/server.d.ts.map +1 -0
  102. package/dist/server.js +101 -0
  103. package/dist/server.js.map +1 -0
  104. package/dist/spawners/http-api-spawner.d.ts +10 -0
  105. package/dist/spawners/http-api-spawner.d.ts.map +1 -0
  106. package/dist/spawners/http-api-spawner.js +88 -0
  107. package/dist/spawners/http-api-spawner.js.map +1 -0
  108. package/dist/user-store-pg.d.ts +21 -0
  109. package/dist/user-store-pg.d.ts.map +1 -0
  110. package/dist/user-store-pg.js +97 -0
  111. package/dist/user-store-pg.js.map +1 -0
  112. package/dist/user-store.d.ts +29 -0
  113. package/dist/user-store.d.ts.map +1 -0
  114. package/dist/user-store.js +97 -0
  115. package/dist/user-store.js.map +1 -0
  116. package/dist/utils/repo-resolver.d.ts +3 -0
  117. package/dist/utils/repo-resolver.d.ts.map +1 -0
  118. package/dist/utils/repo-resolver.js +3 -0
  119. package/dist/utils/repo-resolver.js.map +1 -0
  120. package/dist/webhook-dispatcher.d.ts +13 -0
  121. package/dist/webhook-dispatcher.d.ts.map +1 -0
  122. package/dist/webhook-dispatcher.js +108 -0
  123. package/dist/webhook-dispatcher.js.map +1 -0
  124. package/dist/webhook-store.d.ts +31 -0
  125. package/dist/webhook-store.d.ts.map +1 -0
  126. package/dist/webhook-store.js +91 -0
  127. package/dist/webhook-store.js.map +1 -0
  128. package/package.json +59 -0
  129. package/src/.gitkeep +0 -0
  130. package/src/api.ts +8 -0
  131. package/src/bootstrap.ts +259 -0
  132. package/src/event-bus.ts +64 -0
  133. package/src/index.ts +58 -0
  134. package/src/integration-runtime.ts +180 -0
  135. package/src/integration-store.ts +125 -0
  136. package/src/integrations/linear/failure-handler.ts +93 -0
  137. package/src/integrations/linear/webhook-handler.ts +156 -0
  138. package/src/integrations/registry.ts +12 -0
  139. package/src/integrations/types.ts +37 -0
  140. package/src/launcher.ts +214 -0
  141. package/src/logger.ts +50 -0
  142. package/src/plans.ts +43 -0
  143. package/src/routes/agents.ts +131 -0
  144. package/src/routes/config.ts +154 -0
  145. package/src/routes/contracts.ts +205 -0
  146. package/src/routes/pipelines.ts +146 -0
  147. package/src/routes/projects.ts +237 -0
  148. package/src/routes/runs.ts +468 -0
  149. package/src/routes/skills.ts +136 -0
  150. package/src/routes/tools.ts +222 -0
  151. package/src/routes/users.ts +169 -0
  152. package/src/routes/validate.ts +196 -0
  153. package/src/routes/webhooks.ts +120 -0
  154. package/src/server.ts +155 -0
  155. package/src/spawners/http-api-spawner.ts +96 -0
  156. package/src/user-store-pg.ts +138 -0
  157. package/src/user-store.ts +125 -0
  158. package/src/utils/repo-resolver.ts +3 -0
  159. package/src/webhook-dispatcher.ts +142 -0
  160. package/src/webhook-store.ts +141 -0
  161. package/tests/agents.test.ts +164 -0
  162. package/tests/cancel.test.ts +120 -0
  163. package/tests/config.test.ts +196 -0
  164. package/tests/contracts.test.ts +302 -0
  165. package/tests/event-bus.test.ts +53 -0
  166. package/tests/http-api-spawner.test.ts +158 -0
  167. package/tests/integration-runtime.test.ts +199 -0
  168. package/tests/integration-store.test.ts +66 -0
  169. package/tests/integrations/linear/failure-handler.test.ts +113 -0
  170. package/tests/integrations/linear/webhook-handler.test.ts +191 -0
  171. package/tests/launcher.test.ts +380 -0
  172. package/tests/linear-webhook.test.ts +390 -0
  173. package/tests/pipelines.test.ts +166 -0
  174. package/tests/projects.test.ts +283 -0
  175. package/tests/runs.test.ts +379 -0
  176. package/tests/server.test.ts +208 -0
  177. package/tests/skills.test.ts +149 -0
  178. package/tests/sse.test.ts +129 -0
  179. package/tests/tools.test.ts +233 -0
  180. package/tests/user-store.test.ts +93 -0
  181. package/tests/users.test.ts +189 -0
  182. package/tests/utils/repo-resolver.test.ts +105 -0
  183. package/tests/validate.test.ts +233 -0
  184. package/tests/webhook-dispatcher.test.ts +214 -0
  185. package/tests/webhook-store.test.ts +98 -0
  186. package/tests/webhooks.test.ts +176 -0
  187. package/tsconfig.json +24 -0
  188. package/vitest.config.ts +7 -0
@@ -0,0 +1,102 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ const VALID_EVENTS = [
3
+ 'pipeline_start',
4
+ 'pipeline_complete',
5
+ 'stage_complete',
6
+ 'stage_rejected',
7
+ 'stage_failed',
8
+ 'group_feedback',
9
+ ];
10
+ const errorSchema = { type: 'object', properties: { error: { type: 'string' } } };
11
+ const webhookSchema = {
12
+ type: 'object',
13
+ properties: {
14
+ id: { type: 'string' },
15
+ url: { type: 'string' },
16
+ events: { type: 'array', items: { type: 'string' } },
17
+ status: { type: 'string', enum: ['active', 'failed'] },
18
+ created_at: { type: 'string' },
19
+ },
20
+ };
21
+ export async function webhooksRoutes(fastify, options) {
22
+ const { webhookStore } = options.deps;
23
+ // POST /api/webhooks
24
+ fastify.post('/webhooks', {
25
+ schema: {
26
+ tags: ['webhooks'],
27
+ summary: 'Register a new webhook',
28
+ body: {
29
+ type: 'object',
30
+ required: ['url', 'events'],
31
+ properties: {
32
+ url: { type: 'string' },
33
+ events: { type: 'array', items: { type: 'string' }, minItems: 1 },
34
+ secret: { type: 'string' },
35
+ },
36
+ },
37
+ response: {
38
+ 201: webhookSchema,
39
+ 400: errorSchema,
40
+ },
41
+ },
42
+ }, async (request, reply) => {
43
+ const { url, events, secret } = request.body;
44
+ if (!events || events.length === 0) {
45
+ return reply.status(400).send({ error: 'events must not be empty' });
46
+ }
47
+ const id = randomUUID();
48
+ const created_at = new Date().toISOString();
49
+ webhookStore.saveWebhook({
50
+ id,
51
+ url,
52
+ events,
53
+ ...(secret != null ? { secret } : {}),
54
+ status: 'active',
55
+ created_at,
56
+ });
57
+ return reply.status(201).send({ id, url, events, status: 'active', created_at });
58
+ });
59
+ // GET /api/webhooks
60
+ fastify.get('/webhooks', {
61
+ schema: {
62
+ tags: ['webhooks'],
63
+ summary: 'List all registered webhooks',
64
+ response: {
65
+ 200: {
66
+ type: 'object',
67
+ properties: {
68
+ webhooks: { type: 'array', items: webhookSchema },
69
+ },
70
+ },
71
+ },
72
+ },
73
+ }, async (_request, reply) => {
74
+ const webhooks = webhookStore.listWebhooks().map(({ secret: _secret, ...w }) => w);
75
+ return reply.send({ webhooks });
76
+ });
77
+ // DELETE /api/webhooks/:id
78
+ fastify.delete('/webhooks/:id', {
79
+ schema: {
80
+ tags: ['webhooks'],
81
+ summary: 'Delete a webhook',
82
+ params: {
83
+ type: 'object',
84
+ properties: { id: { type: 'string' } },
85
+ required: ['id'],
86
+ },
87
+ response: {
88
+ 204: { type: 'null', description: 'No content' },
89
+ 404: errorSchema,
90
+ },
91
+ },
92
+ }, async (request, reply) => {
93
+ const deleted = webhookStore.deleteWebhook(request.params.id);
94
+ if (!deleted) {
95
+ return reply.status(404).send({ error: 'Webhook not found' });
96
+ }
97
+ return reply.status(204).send();
98
+ });
99
+ }
100
+ // Export VALID_EVENTS for documentation/validation use
101
+ export { VALID_EVENTS };
102
+ //# sourceMappingURL=webhooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../../src/routes/webhooks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { AnyRunStore } from '@studio-foundation/engine';
2
+ import type { RunLauncher } from './launcher.js';
3
+ import type { WebhookStore } from './webhook-store.js';
4
+ import type { IntegrationStore } from './integration-store.js';
5
+ import type { IntegrationRuntime } from './integration-runtime.js';
6
+ import type { UserStore } from './user-store.js';
7
+ import type { PgUserStore } from './user-store-pg.js';
8
+ import { type PlansConfig } from './plans.js';
9
+ export interface ApiConfig {
10
+ key?: string;
11
+ port?: number;
12
+ }
13
+ export type MaskedConfig = {
14
+ defaults?: {
15
+ provider?: string;
16
+ model?: string;
17
+ };
18
+ providers: string[];
19
+ };
20
+ export interface ServerDeps {
21
+ store: AnyRunStore;
22
+ launcher: RunLauncher;
23
+ configsDir: string;
24
+ /** Raw projects_dir from config (may contain ~). Used by route handlers for repo cloning. */
25
+ projectsDir?: string;
26
+ projectName: string;
27
+ apiConfig: ApiConfig;
28
+ studioVersion: string;
29
+ maskedConfig: MaskedConfig;
30
+ webhookStore: WebhookStore;
31
+ integrationStore: IntegrationStore;
32
+ integrationRuntime: IntegrationRuntime;
33
+ userStore?: UserStore | PgUserStore;
34
+ plans?: PlansConfig;
35
+ /** true when at least one user exists in the DB — computed at bootstrap time to avoid per-request DB calls */
36
+ hasUsers?: boolean;
37
+ }
38
+ declare module 'fastify' {
39
+ interface FastifyRequest {
40
+ user?: import('./user-store.js').User;
41
+ }
42
+ }
43
+ export declare function buildServer(deps: ServerDeps): import("fastify").FastifyInstance<import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>, import("node:http").IncomingMessage, import("node:http").ServerResponse<import("node:http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault> & PromiseLike<import("fastify").FastifyInstance<import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>, import("node:http").IncomingMessage, import("node:http").ServerResponse<import("node:http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>> & {
44
+ __linterBrands: "SafePromiseLike";
45
+ };
46
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAa5E,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAEF,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,6FAA6F;IAC7F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,SAAS,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,YAAY,CAAC;IAC3B,YAAY,EAAE,YAAY,CAAC;IAC3B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,SAAS,CAAC,EAAE,SAAS,GAAG,WAAW,CAAC;IACpC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,8GAA8G;IAC9G,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAGD,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,IAAI,CAAC,EAAE,OAAO,iBAAiB,EAAE,IAAI,CAAC;KACvC;CACF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU;;EA0F3C"}
package/dist/server.js ADDED
@@ -0,0 +1,101 @@
1
+ // Fastify server builder — takes deps via injection for testability
2
+ // buildServer(deps) → FastifyInstance, ready to listen
3
+ import Fastify from 'fastify';
4
+ import cors from '@fastify/cors';
5
+ import rateLimit from '@fastify/rate-limit';
6
+ import swagger from '@fastify/swagger';
7
+ import swaggerUi from '@fastify/swagger-ui';
8
+ import { getPlanLimits, DEFAULT_PLANS } from './plans.js';
9
+ import { runsRoutes } from './routes/runs.js';
10
+ import { projectsRoutes } from './routes/projects.js';
11
+ import { contractsRoutes } from './routes/contracts.js';
12
+ import { pipelinesRoutes } from './routes/pipelines.js';
13
+ import { toolsRoutes } from './routes/tools.js';
14
+ import { agentsRoutes } from './routes/agents.js';
15
+ import { configRoutes } from './routes/config.js';
16
+ import { skillsRoutes } from './routes/skills.js';
17
+ import { validateRoutes } from './routes/validate.js';
18
+ import { webhooksRoutes } from './routes/webhooks.js';
19
+ import { usersRoutes } from './routes/users.js';
20
+ export function buildServer(deps) {
21
+ const fastify = Fastify({ logger: false });
22
+ // Register text/plain parser so PUT routes that accept YAML bodies work correctly.
23
+ // Without this, Fastify v5 cannot parse text/plain and body validation fails with 400.
24
+ fastify.addContentTypeParser('text/plain', { parseAs: 'string' }, (_req, body, done) => {
25
+ done(null, body);
26
+ });
27
+ void fastify.register(cors, { origin: true });
28
+ void fastify.register(rateLimit, {
29
+ global: true,
30
+ max: (req) => {
31
+ const planName = req.user?.plan ?? 'free';
32
+ const plans = deps.plans ?? DEFAULT_PLANS;
33
+ return getPlanLimits(plans, planName).rate_limit_per_minute;
34
+ },
35
+ timeWindow: '1 minute',
36
+ keyGenerator: (req) => req.user?.id ?? req.ip ?? 'anonymous',
37
+ addHeaders: {
38
+ 'x-ratelimit-limit': true,
39
+ 'x-ratelimit-remaining': true,
40
+ 'x-ratelimit-reset': true,
41
+ },
42
+ allowList: (req) => req.url.startsWith('/api/integrations/'),
43
+ });
44
+ if (process.env['NODE_ENV'] !== 'production') {
45
+ void fastify.register(swagger, {
46
+ openapi: {
47
+ info: {
48
+ title: 'Studio API',
49
+ description: 'REST API for Studio pipeline orchestration',
50
+ version: '1.0.0',
51
+ },
52
+ },
53
+ });
54
+ void fastify.register(swaggerUi, {
55
+ routePrefix: '/api/docs',
56
+ });
57
+ }
58
+ // Auth hook — supports three modes:
59
+ // 1. Multi-user: userStore provided + hasUsers=true → lookup by api_key
60
+ // 2. Legacy single-key: hasUsers=false + api.key configured → Bearer check
61
+ // 3. Open/dev: no users, no api.key → allow all (local dev only)
62
+ fastify.addHook('onRequest', async (request, reply) => {
63
+ const path = request.url.split('?')[0];
64
+ if (path.startsWith('/api/integrations/'))
65
+ return;
66
+ const auth = request.headers['authorization'];
67
+ const token = auth?.startsWith('Bearer ') ? auth.slice(7) : undefined;
68
+ if (deps.userStore && deps.hasUsers) {
69
+ // Multi-user mode: look up user by api_key
70
+ const user = token ? await deps.userStore.getUserByApiKey(token) : null;
71
+ if (!user) {
72
+ await reply.status(401).send({ error: 'Unauthorized' });
73
+ return;
74
+ }
75
+ request.user = user;
76
+ return;
77
+ }
78
+ // Legacy single-key mode
79
+ if (deps.apiConfig.key) {
80
+ if (!auth || auth !== `Bearer ${deps.apiConfig.key}`) {
81
+ await reply.status(401).send({ error: 'Unauthorized' });
82
+ }
83
+ return;
84
+ }
85
+ // No userStore with users, no api.key → open (local dev)
86
+ });
87
+ void fastify.register(runsRoutes, { prefix: '/api', deps });
88
+ void fastify.register(projectsRoutes, { prefix: '/api', deps });
89
+ void fastify.register(contractsRoutes, { prefix: '/api', deps });
90
+ void fastify.register(pipelinesRoutes, { prefix: '/api', deps });
91
+ void fastify.register(toolsRoutes, { prefix: '/api', deps });
92
+ void fastify.register(agentsRoutes, { prefix: '/api', deps });
93
+ void fastify.register(configRoutes, { prefix: '/api', deps });
94
+ void fastify.register(skillsRoutes, { prefix: '/api', deps });
95
+ void fastify.register(validateRoutes, { prefix: '/api', deps });
96
+ void fastify.register(webhooksRoutes, { prefix: '/api', deps });
97
+ void fastify.register(usersRoutes, { prefix: '/api', deps });
98
+ deps.integrationRuntime.registerRoutes(fastify, '/api');
99
+ return fastify;
100
+ }
101
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { RunSpawner, SpawnConfig, SpawnResult } from '@studio-foundation/contracts';
2
+ export declare class HttpApiSpawner implements RunSpawner {
3
+ private apiUrl;
4
+ private apiKey?;
5
+ constructor(apiUrl: string, apiKey?: string | undefined);
6
+ private authHeaders;
7
+ spawnAndWait(config: SpawnConfig): Promise<SpawnResult>;
8
+ private waitForCompletion;
9
+ }
10
+ //# sourceMappingURL=http-api-spawner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-api-spawner.d.ts","sourceRoot":"","sources":["../../src/spawners/http-api-spawner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAe,MAAM,8BAA8B,CAAC;AAEtG,qBAAa,cAAe,YAAW,UAAU;IACnC,OAAO,CAAC,MAAM;IAAU,OAAO,CAAC,MAAM,CAAC;gBAA/B,MAAM,EAAE,MAAM,EAAU,MAAM,CAAC,EAAE,MAAM,YAAA;IAE3D,OAAO,CAAC,WAAW;IAIb,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IA0C7D,OAAO,CAAC,iBAAiB;CA4C1B"}
@@ -0,0 +1,88 @@
1
+ export class HttpApiSpawner {
2
+ apiUrl;
3
+ apiKey;
4
+ constructor(apiUrl, apiKey) {
5
+ this.apiUrl = apiUrl;
6
+ this.apiKey = apiKey;
7
+ }
8
+ authHeaders() {
9
+ return this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {};
10
+ }
11
+ async spawnAndWait(config) {
12
+ // 1. Launch the run
13
+ const postRes = await fetch(`${this.apiUrl}/api/runs`, {
14
+ method: 'POST',
15
+ headers: {
16
+ 'Content-Type': 'application/json',
17
+ 'X-Studio-Depth': String(config.depth),
18
+ 'X-Studio-Parent-Run-Id': config.parentRunId,
19
+ ...this.authHeaders(),
20
+ },
21
+ body: JSON.stringify({ pipeline: config.pipeline, input: config.input }),
22
+ });
23
+ if (!postRes.ok) {
24
+ const text = await postRes.text();
25
+ throw new Error(`Failed to launch child run: ${postRes.status} ${text}`);
26
+ }
27
+ const { run_id } = (await postRes.json());
28
+ // 2. Wait for pipeline_complete via SSE
29
+ await this.waitForCompletion(run_id);
30
+ // 3. Fetch full run result to get output
31
+ const getRes = await fetch(`${this.apiUrl}/api/runs/${run_id}`, {
32
+ headers: { ...this.authHeaders() },
33
+ });
34
+ if (!getRes.ok) {
35
+ throw new Error(`Failed to fetch child run result ${run_id}: ${getRes.status}`);
36
+ }
37
+ const run = (await getRes.json());
38
+ if (run.status === 'failed' || run.status === 'rejected' || run.status === 'cancelled') {
39
+ throw new Error(`Child run ${run_id} ${run.status}`);
40
+ }
41
+ const lastStage = [...run.stages].reverse().find(s => s.status === 'success');
42
+ const output = lastStage?.output ?? null;
43
+ return { run_id, status: run.status, output };
44
+ }
45
+ waitForCompletion(runId) {
46
+ return new Promise((resolve, reject) => {
47
+ fetch(`${this.apiUrl}/api/runs/${runId}/stream`, {
48
+ headers: { Accept: 'text/event-stream', ...this.authHeaders() },
49
+ })
50
+ .then(response => {
51
+ if (!response.ok || !response.body) {
52
+ reject(new Error(`SSE connection failed for run ${runId}: ${response.status}`));
53
+ return;
54
+ }
55
+ const reader = response.body.getReader();
56
+ const decoder = new TextDecoder();
57
+ let buffer = '';
58
+ let currentEventType = '';
59
+ const pump = () => reader.read().then(({ done, value }) => {
60
+ if (done) {
61
+ reject(new Error(`SSE stream ended without pipeline_complete for run ${runId}`));
62
+ return;
63
+ }
64
+ buffer += decoder.decode(value, { stream: true });
65
+ const lines = buffer.split('\n');
66
+ buffer = lines.pop() ?? '';
67
+ for (const line of lines) {
68
+ if (line.startsWith('event: ')) {
69
+ currentEventType = line.slice(7).trim();
70
+ }
71
+ else if (line.startsWith('data: ') && currentEventType === 'pipeline_complete') {
72
+ reader.cancel();
73
+ resolve();
74
+ return;
75
+ }
76
+ else if (line === '') {
77
+ currentEventType = '';
78
+ }
79
+ }
80
+ return pump();
81
+ });
82
+ pump().catch(reject);
83
+ })
84
+ .catch(reject);
85
+ });
86
+ }
87
+ }
88
+ //# sourceMappingURL=http-api-spawner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-api-spawner.js","sourceRoot":"","sources":["../../src/spawners/http-api-spawner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { User, DailyUsage } from './user-store.js';
2
+ export type { User, DailyUsage } from './user-store.js';
3
+ export declare class PgUserStore {
4
+ private pool;
5
+ private schemaReady;
6
+ constructor(connectionString: string);
7
+ private ensureSchema;
8
+ private initSchema;
9
+ getUserByApiKey(apiKey: string): Promise<User | null>;
10
+ getUserById(id: string): Promise<User | null>;
11
+ getUserByEmail(email: string): Promise<User | null>;
12
+ listUsers(): Promise<User[]>;
13
+ saveUser(user: User): Promise<void>;
14
+ deleteUser(id: string): Promise<void>;
15
+ getDailyUsage(userId: string, date: string): Promise<DailyUsage>;
16
+ incrementRuns(userId: string, date: string): Promise<void>;
17
+ incrementTokens(userId: string, date: string, tokens: number): Promise<void>;
18
+ close(): Promise<void>;
19
+ }
20
+ export type AnyUserStore = import('./user-store.js').UserStore | PgUserStore;
21
+ //# sourceMappingURL=user-store-pg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-store-pg.d.ts","sourceRoot":"","sources":["../src/user-store-pg.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACxD,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAExD,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAoB;IAChC,OAAO,CAAC,WAAW,CAA8B;gBAErC,gBAAgB,EAAE,MAAM;IAMpC,OAAO,CAAC,YAAY;YAON,UAAU;IAqBlB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IASrD,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAS7C,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IASnD,SAAS,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;IAQ5B,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAcnC,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKrC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAShE,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW1D,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW5E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B;AAED,MAAM,MAAM,YAAY,GAAG,OAAO,iBAAiB,EAAE,SAAS,GAAG,WAAW,CAAC"}
@@ -0,0 +1,97 @@
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
+ import { createRequire } from 'node:module';
6
+ export class PgUserStore {
7
+ pool;
8
+ schemaReady = null;
9
+ constructor(connectionString) {
10
+ const _require = createRequire(import.meta.url);
11
+ const { Pool } = _require('pg');
12
+ this.pool = new Pool({ connectionString });
13
+ }
14
+ ensureSchema() {
15
+ if (!this.schemaReady) {
16
+ this.schemaReady = this.initSchema();
17
+ }
18
+ return this.schemaReady;
19
+ }
20
+ async initSchema() {
21
+ await this.pool.query(`
22
+ CREATE TABLE IF NOT EXISTS studio_users (
23
+ id TEXT PRIMARY KEY,
24
+ email TEXT UNIQUE NOT NULL,
25
+ plan TEXT NOT NULL DEFAULT 'free',
26
+ api_key TEXT UNIQUE NOT NULL,
27
+ created_at TEXT NOT NULL
28
+ )
29
+ `);
30
+ await this.pool.query(`
31
+ CREATE TABLE IF NOT EXISTS studio_usage (
32
+ user_id TEXT NOT NULL,
33
+ date TEXT NOT NULL,
34
+ runs_count INTEGER NOT NULL DEFAULT 0,
35
+ tokens_used INTEGER NOT NULL DEFAULT 0,
36
+ PRIMARY KEY (user_id, date)
37
+ )
38
+ `);
39
+ }
40
+ async getUserByApiKey(apiKey) {
41
+ await this.ensureSchema();
42
+ const res = await this.pool.query('SELECT id, email, plan, api_key, created_at FROM studio_users WHERE api_key = $1', [apiKey]);
43
+ return res.rows[0] ?? null;
44
+ }
45
+ async getUserById(id) {
46
+ await this.ensureSchema();
47
+ const res = await this.pool.query('SELECT id, email, plan, api_key, created_at FROM studio_users WHERE id = $1', [id]);
48
+ return res.rows[0] ?? null;
49
+ }
50
+ async getUserByEmail(email) {
51
+ await this.ensureSchema();
52
+ const res = await this.pool.query('SELECT id, email, plan, api_key, created_at FROM studio_users WHERE email = $1', [email]);
53
+ return res.rows[0] ?? null;
54
+ }
55
+ async listUsers() {
56
+ await this.ensureSchema();
57
+ const res = await this.pool.query('SELECT id, email, plan, api_key, created_at FROM studio_users ORDER BY created_at ASC');
58
+ return res.rows;
59
+ }
60
+ async saveUser(user) {
61
+ await this.ensureSchema();
62
+ await this.pool.query(`INSERT INTO studio_users (id, email, plan, api_key, created_at)
63
+ VALUES ($1, $2, $3, $4, $5)
64
+ ON CONFLICT (id) DO UPDATE SET
65
+ email = EXCLUDED.email,
66
+ plan = EXCLUDED.plan,
67
+ api_key = EXCLUDED.api_key,
68
+ created_at = EXCLUDED.created_at`, [user.id, user.email, user.plan, user.api_key, user.created_at]);
69
+ }
70
+ async deleteUser(id) {
71
+ await this.ensureSchema();
72
+ await this.pool.query('DELETE FROM studio_users WHERE id = $1', [id]);
73
+ }
74
+ async getDailyUsage(userId, date) {
75
+ await this.ensureSchema();
76
+ const res = await this.pool.query('SELECT user_id, date, runs_count, tokens_used FROM studio_usage WHERE user_id = $1 AND date = $2', [userId, date]);
77
+ return res.rows[0] ?? { user_id: userId, date, runs_count: 0, tokens_used: 0 };
78
+ }
79
+ async incrementRuns(userId, date) {
80
+ await this.ensureSchema();
81
+ await this.pool.query(`INSERT INTO studio_usage (user_id, date, runs_count, tokens_used)
82
+ VALUES ($1, $2, 1, 0)
83
+ ON CONFLICT (user_id, date) DO UPDATE SET
84
+ runs_count = studio_usage.runs_count + 1`, [userId, date]);
85
+ }
86
+ async incrementTokens(userId, date, tokens) {
87
+ await this.ensureSchema();
88
+ await this.pool.query(`INSERT INTO studio_usage (user_id, date, runs_count, tokens_used)
89
+ VALUES ($1, $2, 0, $3)
90
+ ON CONFLICT (user_id, date) DO UPDATE SET
91
+ tokens_used = studio_usage.tokens_used + EXCLUDED.tokens_used`, [userId, date, tokens]);
92
+ }
93
+ async close() {
94
+ await this.pool.end();
95
+ }
96
+ }
97
+ //# sourceMappingURL=user-store-pg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-store-pg.js","sourceRoot":"","sources":["../src/user-store-pg.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ export interface User {
2
+ id: string;
3
+ email: string;
4
+ plan: string;
5
+ api_key: string;
6
+ created_at: string;
7
+ }
8
+ export interface DailyUsage {
9
+ user_id: string;
10
+ date: string;
11
+ runs_count: number;
12
+ tokens_used: number;
13
+ }
14
+ export declare class UserStore {
15
+ private db;
16
+ constructor(dbPath: string);
17
+ private initSchema;
18
+ getUserByApiKey(apiKey: string): User | null;
19
+ getUserById(id: string): User | null;
20
+ getUserByEmail(email: string): User | null;
21
+ listUsers(): User[];
22
+ saveUser(user: User): void;
23
+ deleteUser(id: string): void;
24
+ getDailyUsage(userId: string, date: string): DailyUsage;
25
+ incrementRuns(userId: string, date: string): void;
26
+ incrementTokens(userId: string, date: string, tokens: number): void;
27
+ close(): void;
28
+ }
29
+ //# sourceMappingURL=user-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-store.d.ts","sourceRoot":"","sources":["../src/user-store.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,EAAE,CAAoC;gBAElC,MAAM,EAAE,MAAM;IAQ1B,OAAO,CAAC,UAAU;IAoBlB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAO5C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAOpC,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAO1C,SAAS,IAAI,IAAI,EAAE;IAMnB,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAY1B,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAI5B,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU;IAOvD,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IASjD,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IASnE,KAAK,IAAI,IAAI;CAGd"}
@@ -0,0 +1,97 @@
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
+ import { createRequire } from 'node:module';
6
+ export class UserStore {
7
+ db;
8
+ constructor(dbPath) {
9
+ const _require = createRequire(import.meta.url);
10
+ const Database = _require('better-sqlite3');
11
+ this.db = new Database(dbPath);
12
+ this.db.pragma('journal_mode = WAL');
13
+ this.initSchema();
14
+ }
15
+ initSchema() {
16
+ this.db.exec(`
17
+ CREATE TABLE IF NOT EXISTS users (
18
+ id TEXT PRIMARY KEY,
19
+ email TEXT UNIQUE NOT NULL,
20
+ plan TEXT NOT NULL DEFAULT 'free',
21
+ api_key TEXT UNIQUE NOT NULL,
22
+ created_at TEXT NOT NULL
23
+ );
24
+
25
+ CREATE TABLE IF NOT EXISTS usage (
26
+ user_id TEXT NOT NULL,
27
+ date TEXT NOT NULL,
28
+ runs_count INTEGER NOT NULL DEFAULT 0,
29
+ tokens_used INTEGER NOT NULL DEFAULT 0,
30
+ PRIMARY KEY (user_id, date)
31
+ );
32
+ `);
33
+ }
34
+ getUserByApiKey(apiKey) {
35
+ const row = this.db
36
+ .prepare('SELECT id, email, plan, api_key, created_at FROM users WHERE api_key = ?')
37
+ .get(apiKey);
38
+ return row ?? null;
39
+ }
40
+ getUserById(id) {
41
+ const row = this.db
42
+ .prepare('SELECT id, email, plan, api_key, created_at FROM users WHERE id = ?')
43
+ .get(id);
44
+ return row ?? null;
45
+ }
46
+ getUserByEmail(email) {
47
+ const row = this.db
48
+ .prepare('SELECT id, email, plan, api_key, created_at FROM users WHERE email = ?')
49
+ .get(email);
50
+ return row ?? null;
51
+ }
52
+ listUsers() {
53
+ return this.db
54
+ .prepare('SELECT id, email, plan, api_key, created_at FROM users ORDER BY created_at ASC')
55
+ .all();
56
+ }
57
+ saveUser(user) {
58
+ this.db.prepare(`
59
+ INSERT INTO users (id, email, plan, api_key, created_at)
60
+ VALUES (?, ?, ?, ?, ?)
61
+ ON CONFLICT(id) DO UPDATE SET
62
+ email = excluded.email,
63
+ plan = excluded.plan,
64
+ api_key = excluded.api_key,
65
+ created_at = excluded.created_at
66
+ `).run(user.id, user.email, user.plan, user.api_key, user.created_at);
67
+ }
68
+ deleteUser(id) {
69
+ this.db.prepare('DELETE FROM users WHERE id = ?').run(id);
70
+ }
71
+ getDailyUsage(userId, date) {
72
+ const row = this.db
73
+ .prepare('SELECT user_id, date, runs_count, tokens_used FROM usage WHERE user_id = ? AND date = ?')
74
+ .get(userId, date);
75
+ return row ?? { user_id: userId, date, runs_count: 0, tokens_used: 0 };
76
+ }
77
+ incrementRuns(userId, date) {
78
+ this.db.prepare(`
79
+ INSERT INTO usage (user_id, date, runs_count, tokens_used)
80
+ VALUES (?, ?, 1, 0)
81
+ ON CONFLICT (user_id, date) DO UPDATE SET
82
+ runs_count = runs_count + 1
83
+ `).run(userId, date);
84
+ }
85
+ incrementTokens(userId, date, tokens) {
86
+ this.db.prepare(`
87
+ INSERT INTO usage (user_id, date, runs_count, tokens_used)
88
+ VALUES (?, ?, 0, ?)
89
+ ON CONFLICT (user_id, date) DO UPDATE SET
90
+ tokens_used = tokens_used + excluded.tokens_used
91
+ `).run(userId, date, tokens);
92
+ }
93
+ close() {
94
+ this.db.close();
95
+ }
96
+ }
97
+ //# sourceMappingURL=user-store.js.map