@shipfox/api-server 2.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 (47) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +35 -0
  5. package/LICENSE +21 -0
  6. package/README.md +59 -0
  7. package/dist/config.d.ts +9 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +43 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/e2e.d.ts +10 -0
  12. package/dist/e2e.d.ts.map +1 -0
  13. package/dist/e2e.js +42 -0
  14. package/dist/e2e.js.map +1 -0
  15. package/dist/index.d.ts +4 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +4 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/instrumentation.d.ts +2 -0
  20. package/dist/instrumentation.d.ts.map +1 -0
  21. package/dist/instrumentation.js +13 -0
  22. package/dist/instrumentation.js.map +1 -0
  23. package/dist/modules.d.ts +3 -0
  24. package/dist/modules.d.ts.map +1 -0
  25. package/dist/modules.js +76 -0
  26. package/dist/modules.js.map +1 -0
  27. package/dist/server.d.ts +16 -0
  28. package/dist/server.d.ts.map +1 -0
  29. package/dist/server.js +215 -0
  30. package/dist/server.js.map +1 -0
  31. package/dist/tsconfig.test.tsbuildinfo +1 -0
  32. package/package.json +80 -0
  33. package/src/config.test.ts +33 -0
  34. package/src/config.ts +54 -0
  35. package/src/e2e.test.ts +101 -0
  36. package/src/e2e.ts +61 -0
  37. package/src/index.ts +3 -0
  38. package/src/instrumentation.ts +8 -0
  39. package/src/modules.test.ts +137 -0
  40. package/src/modules.ts +90 -0
  41. package/src/server.test.ts +539 -0
  42. package/src/server.ts +242 -0
  43. package/tsconfig.build.json +9 -0
  44. package/tsconfig.build.tsbuildinfo +1 -0
  45. package/tsconfig.json +4 -0
  46. package/tsconfig.test.json +8 -0
  47. package/vitest.config.ts +3 -0
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@shipfox/api-server",
3
+ "license": "MIT",
4
+ "private": false,
5
+ "version": "2.0.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
9
+ "directory": "libs/api/server"
10
+ },
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "imports": {
15
+ "#*": {
16
+ "workspace-source": "./src/*",
17
+ "development": "./src/*",
18
+ "default": "./dist/*"
19
+ }
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "development": {
24
+ "types": "./src/index.ts",
25
+ "default": "./src/index.ts"
26
+ },
27
+ "default": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ }
31
+ },
32
+ "./instrumentation": {
33
+ "development": {
34
+ "types": "./src/instrumentation.ts",
35
+ "default": "./src/instrumentation.ts"
36
+ },
37
+ "default": {
38
+ "types": "./dist/instrumentation.d.ts",
39
+ "default": "./dist/instrumentation.js"
40
+ }
41
+ }
42
+ },
43
+ "dependencies": {
44
+ "@shipfox/api-auth": "2.0.0",
45
+ "@shipfox/api-agent": "2.0.0",
46
+ "@shipfox/api-dispatcher": "2.0.0",
47
+ "@shipfox/annotations": "2.0.0",
48
+ "@shipfox/api-integration-core": "2.0.0",
49
+ "@shipfox/api-definitions": "2.0.0",
50
+ "@shipfox/api-logs": "2.0.0",
51
+ "@shipfox/api-projects": "2.0.0",
52
+ "@shipfox/api-workspaces": "2.0.0",
53
+ "@shipfox/api-triggers": "2.0.0",
54
+ "@shipfox/config": "1.2.1",
55
+ "@shipfox/node-fastify": "0.2.1",
56
+ "@shipfox/node-error-monitoring": "0.1.2",
57
+ "@shipfox/api-workflows": "2.0.0",
58
+ "@shipfox/node-module": "0.2.0",
59
+ "@shipfox/api-secrets": "2.0.0",
60
+ "@shipfox/node-postgres": "0.4.1",
61
+ "@shipfox/api-runners": "2.0.0",
62
+ "@shipfox/node-opentelemetry": "0.5.0"
63
+ },
64
+ "devDependencies": {
65
+ "@shipfox/ts-config": "1.3.8",
66
+ "@shipfox/biome": "1.8.1",
67
+ "@shipfox/swc": "1.2.5",
68
+ "@shipfox/vitest": "1.2.2",
69
+ "@shipfox/typescript": "1.1.6"
70
+ },
71
+ "scripts": {
72
+ "build": "shipfox-swc",
73
+ "check": "shipfox-biome-check",
74
+ "check:fix": "shipfox-biome-check --write",
75
+ "test": "shipfox-vitest-run",
76
+ "test:watch": "shipfox-vitest-watch",
77
+ "type": "shipfox-tsc-check",
78
+ "type:emit": "shipfox-tsc-emit"
79
+ }
80
+ }
@@ -0,0 +1,33 @@
1
+ import {parseApiTrustProxy} from './config.js';
2
+
3
+ describe('parseApiTrustProxy', () => {
4
+ it.each([
5
+ ['false', false],
6
+ [' true ', true],
7
+ ['1', 1],
8
+ ['3', 3],
9
+ ['127.0.0.1', '127.0.0.1'],
10
+ ['10.0.0.0/8', '10.0.0.0/8'],
11
+ ['2001:db8::/32', '2001:db8::/32'],
12
+ ])('parses %s', (value, expected) => {
13
+ const result = parseApiTrustProxy(value);
14
+
15
+ expect(result).toBe(expected);
16
+ });
17
+
18
+ it.each([
19
+ '',
20
+ '0',
21
+ '-1',
22
+ '1.5',
23
+ 'maybe',
24
+ '9007199254740992',
25
+ '1'.repeat(400),
26
+ '10.0.0.0/99',
27
+ '2001:db8::/129',
28
+ ])('rejects %s', (value) => {
29
+ const act = () => parseApiTrustProxy(value);
30
+
31
+ expect(act).toThrow('API_TRUST_PROXY');
32
+ });
33
+ });
package/src/config.ts ADDED
@@ -0,0 +1,54 @@
1
+ import {isIP} from 'node:net';
2
+ import {bool, createConfig, port, str} from '@shipfox/config';
3
+
4
+ export type ApiTrustProxy = false | true | number | string;
5
+
6
+ const INTEGER_PATTERN = /^\d+$/;
7
+
8
+ export const config = createConfig({
9
+ E2E_ENABLED: bool({
10
+ desc: 'Enables the end-to-end test routes under /__e2e. Keep it false in production.',
11
+ default: false,
12
+ }),
13
+ E2E_ADMIN_API_KEY: str({
14
+ desc: 'Bearer token that protects the E2E admin routes. Set it when E2E_ENABLED is true.',
15
+ default: undefined,
16
+ }),
17
+ API_PORT: port({
18
+ desc: 'Port the API HTTP server listens on. When unset, the shared PORT setting controls the server port.',
19
+ default: undefined,
20
+ }),
21
+ API_TRUST_PROXY: str({
22
+ desc: 'Controls how the API trusts proxy headers for client IP detection. Use false when clients connect directly, true only when every caller is a trusted proxy, a positive hop count such as 1, or a trusted proxy IP/CIDR such as 10.0.0.0/8.',
23
+ default: 'false',
24
+ }),
25
+ });
26
+
27
+ function parseCidr(value: string): boolean {
28
+ const [address, prefix, extra] = value.split('/');
29
+ if (!address || !prefix || extra !== undefined || isIP(address) === 0) return false;
30
+
31
+ const maxPrefix = isIP(address) === 4 ? 32 : 128;
32
+ if (!INTEGER_PATTERN.test(prefix)) return false;
33
+
34
+ const prefixNumber = Number(prefix);
35
+ return prefixNumber >= 0 && prefixNumber <= maxPrefix;
36
+ }
37
+
38
+ export function parseApiTrustProxy(value: string): ApiTrustProxy {
39
+ const trimmed = value.trim();
40
+ if (trimmed === 'false') return false;
41
+ if (trimmed === 'true') return true;
42
+
43
+ if (INTEGER_PATTERN.test(trimmed)) {
44
+ const hops = Number(trimmed);
45
+ if (Number.isSafeInteger(hops) && hops > 0) return hops;
46
+ throw new Error('API_TRUST_PROXY hop count must be a positive safe integer');
47
+ }
48
+
49
+ if (isIP(trimmed) !== 0 || parseCidr(trimmed)) return trimmed;
50
+
51
+ throw new Error(
52
+ 'API_TRUST_PROXY must be false, true, a positive hop count, or a trusted proxy IP/CIDR',
53
+ );
54
+ }
@@ -0,0 +1,101 @@
1
+ import {closeApp, createApp, defineRoute} from '@shipfox/node-fastify';
2
+ import {afterEach, describe, expect, it} from '@shipfox/vitest/vi';
3
+ import {createE2eAdminAuthMethod, createE2eRouteGroup} from './e2e.js';
4
+
5
+ const pingRoute = defineRoute({
6
+ method: 'POST',
7
+ path: '/ping',
8
+ description: 'E2E test route.',
9
+ handler: () => ({ok: true}),
10
+ });
11
+
12
+ describe('E2E route gating', () => {
13
+ afterEach(async () => {
14
+ await closeApp();
15
+ });
16
+
17
+ it('does not mount E2E routes when E2E is disabled', async () => {
18
+ const app = await createApp({
19
+ auth: [createE2eAdminAuthMethod({E2E_ENABLED: false, E2E_ADMIN_API_KEY: 'secret'})],
20
+ routes: createE2eRouteGroup([pingRoute], {E2E_ENABLED: false, E2E_ADMIN_API_KEY: 'secret'}),
21
+ swagger: false,
22
+ });
23
+
24
+ const res = await app.inject({method: 'POST', url: '/__e2e/ping'});
25
+
26
+ expect(res.statusCode).toBe(404);
27
+ });
28
+
29
+ it('does not mount E2E routes when the admin key is missing', async () => {
30
+ const app = await createApp({
31
+ auth: [createE2eAdminAuthMethod({E2E_ENABLED: true})],
32
+ routes: createE2eRouteGroup([pingRoute], {E2E_ENABLED: true}),
33
+ swagger: false,
34
+ });
35
+
36
+ const res = await app.inject({method: 'POST', url: '/__e2e/ping'});
37
+
38
+ expect(res.statusCode).toBe(404);
39
+ });
40
+
41
+ it('rejects missing E2E admin bearer tokens', async () => {
42
+ const app = await createApp({
43
+ auth: [createE2eAdminAuthMethod({E2E_ENABLED: true, E2E_ADMIN_API_KEY: 'secret'})],
44
+ routes: createE2eRouteGroup([pingRoute], {E2E_ENABLED: true, E2E_ADMIN_API_KEY: 'secret'}),
45
+ swagger: false,
46
+ });
47
+
48
+ const res = await app.inject({method: 'POST', url: '/__e2e/ping'});
49
+
50
+ expect(res.statusCode).toBe(401);
51
+ });
52
+
53
+ it('rejects invalid E2E admin bearer tokens', async () => {
54
+ const app = await createApp({
55
+ auth: [createE2eAdminAuthMethod({E2E_ENABLED: true, E2E_ADMIN_API_KEY: 'secret'})],
56
+ routes: createE2eRouteGroup([pingRoute], {E2E_ENABLED: true, E2E_ADMIN_API_KEY: 'secret'}),
57
+ swagger: false,
58
+ });
59
+
60
+ const res = await app.inject({
61
+ method: 'POST',
62
+ url: '/__e2e/ping',
63
+ headers: {authorization: 'Bearer wrong'},
64
+ });
65
+
66
+ expect(res.statusCode).toBe(401);
67
+ });
68
+
69
+ it('allows valid E2E admin bearer tokens', async () => {
70
+ const app = await createApp({
71
+ auth: [createE2eAdminAuthMethod({E2E_ENABLED: true, E2E_ADMIN_API_KEY: 'secret'})],
72
+ routes: createE2eRouteGroup([pingRoute], {E2E_ENABLED: true, E2E_ADMIN_API_KEY: 'secret'}),
73
+ swagger: false,
74
+ });
75
+
76
+ const res = await app.inject({
77
+ method: 'POST',
78
+ url: '/__e2e/ping',
79
+ headers: {authorization: 'Bearer secret'},
80
+ });
81
+
82
+ expect(res.statusCode).toBe(200);
83
+ expect(res.json()).toEqual({ok: true});
84
+ });
85
+
86
+ it('allows a mixed-case E2E admin bearer scheme', async () => {
87
+ const app = await createApp({
88
+ auth: [createE2eAdminAuthMethod({E2E_ENABLED: true, E2E_ADMIN_API_KEY: 'secret'})],
89
+ routes: createE2eRouteGroup([pingRoute], {E2E_ENABLED: true, E2E_ADMIN_API_KEY: 'secret'}),
90
+ swagger: false,
91
+ });
92
+
93
+ const res = await app.inject({
94
+ method: 'POST',
95
+ url: '/__e2e/ping',
96
+ headers: {authorization: 'bEaReR secret'},
97
+ });
98
+
99
+ expect(res.statusCode).toBe(200);
100
+ });
101
+ });
package/src/e2e.ts ADDED
@@ -0,0 +1,61 @@
1
+ import {timingSafeEqual} from 'node:crypto';
2
+ import {
3
+ type AuthMethod,
4
+ ClientError,
5
+ type RouteExport,
6
+ type RouteGroup,
7
+ } from '@shipfox/node-fastify';
8
+
9
+ export const AUTH_E2E_ADMIN = 'e2e-admin';
10
+
11
+ export interface E2eConfig {
12
+ E2E_ENABLED: boolean;
13
+ E2E_ADMIN_API_KEY?: string | undefined;
14
+ }
15
+
16
+ const BEARER_RE = /^Bearer /iu;
17
+
18
+ export function shouldMountE2eRoutes(config: E2eConfig): boolean {
19
+ return config.E2E_ENABLED && Boolean(config.E2E_ADMIN_API_KEY);
20
+ }
21
+
22
+ function extractBearerToken(header: string | undefined): string | undefined {
23
+ return header?.replace(BEARER_RE, '');
24
+ }
25
+
26
+ function tokensMatch(actual: string, expected: string): boolean {
27
+ const actualBuffer = Buffer.from(actual);
28
+ const expectedBuffer = Buffer.from(expected);
29
+ return (
30
+ actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer)
31
+ );
32
+ }
33
+
34
+ export function createE2eAdminAuthMethod(config: E2eConfig): AuthMethod {
35
+ return {
36
+ name: AUTH_E2E_ADMIN,
37
+ authenticate: async (request) => {
38
+ await Promise.resolve();
39
+ const expected = config.E2E_ADMIN_API_KEY;
40
+ const actual = extractBearerToken(request.headers.authorization);
41
+
42
+ if (!expected || !actual || !tokensMatch(actual, expected)) {
43
+ throw new ClientError('Missing or invalid E2E admin API key', 'unauthorized', {
44
+ status: 401,
45
+ });
46
+ }
47
+ },
48
+ };
49
+ }
50
+
51
+ export function createE2eRouteGroup(routes: RouteExport[], config: E2eConfig): RouteGroup[] {
52
+ if (!shouldMountE2eRoutes(config) || routes.length === 0) return [];
53
+
54
+ return [
55
+ {
56
+ prefix: '/__e2e',
57
+ auth: AUTH_E2E_ADMIN,
58
+ routes,
59
+ },
60
+ ];
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export {defaultModules} from './modules.js';
2
+ export type {CreateServerOptions, RunServerOptions, ServerHandle} from './server.js';
3
+ export {createServer, runServer} from './server.js';
@@ -0,0 +1,8 @@
1
+ import {startInstanceInstrumentation} from '@shipfox/node-opentelemetry';
2
+
3
+ // The metrics API has no proxy meter: instruments created before this preload
4
+ // completes bind to a no-op provider for the process lifetime.
5
+ await startInstanceInstrumentation({
6
+ serviceName: 'api',
7
+ instrumentations: {fastify: true, http: true, pg: true},
8
+ });
@@ -0,0 +1,137 @@
1
+ import {defaultModules} from './modules.js';
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ buildAgentToolCatalogs: vi.fn(),
5
+ buildAgentToolSelectionCatalogs: vi.fn(),
6
+ createDefinitionsModule: vi.fn(),
7
+ createIntegrationsContext: vi.fn(),
8
+ createProjectsModule: vi.fn(),
9
+ createWorkspaceConnectionSnapshotLoader: vi.fn(),
10
+ deleteSecrets: vi.fn(),
11
+ getIntegrationConnectionById: vi.fn(),
12
+ getSecret: vi.fn(),
13
+ loadRunningLeasedStep: vi.fn(),
14
+ setAgentToolMaterializationServices: vi.fn(),
15
+ setSecrets: vi.fn(),
16
+ setSourceControl: vi.fn(),
17
+ }));
18
+
19
+ vi.mock('@shipfox/api-agent', () => ({agentModule: {name: 'agent'}}));
20
+ vi.mock('@shipfox/annotations', () => ({annotationsModule: {name: 'annotations'}}));
21
+ vi.mock('@shipfox/api-auth', () => ({authModule: {name: 'auth'}}));
22
+ vi.mock('@shipfox/api-definitions', () => ({
23
+ createDefinitionsModule: mocks.createDefinitionsModule,
24
+ }));
25
+ vi.mock('@shipfox/api-dispatcher', () => ({dispatcherModule: {name: 'dispatcher'}}));
26
+ vi.mock('@shipfox/api-integration-core', () => ({
27
+ buildAgentToolCatalogs: mocks.buildAgentToolCatalogs,
28
+ buildAgentToolSelectionCatalogs: mocks.buildAgentToolSelectionCatalogs,
29
+ createIntegrationsContext: mocks.createIntegrationsContext,
30
+ createWorkspaceConnectionSnapshotLoader: mocks.createWorkspaceConnectionSnapshotLoader,
31
+ getIntegrationConnectionById: mocks.getIntegrationConnectionById,
32
+ }));
33
+ vi.mock('@shipfox/api-logs', () => ({logsModule: {name: 'logs'}}));
34
+ vi.mock('@shipfox/api-projects', () => ({createProjectsModule: mocks.createProjectsModule}));
35
+ vi.mock('@shipfox/api-runners', () => ({runnersModule: {name: 'runners'}}));
36
+ vi.mock('@shipfox/api-secrets', () => ({
37
+ deleteSecrets: mocks.deleteSecrets,
38
+ getSecret: mocks.getSecret,
39
+ secretsModule: {name: 'secrets'},
40
+ setSecrets: mocks.setSecrets,
41
+ }));
42
+ vi.mock('@shipfox/api-triggers', () => ({triggersModule: {name: 'triggers'}}));
43
+ vi.mock('@shipfox/api-workflows', () => ({
44
+ loadRunningLeasedStep: mocks.loadRunningLeasedStep,
45
+ setAgentToolMaterializationServices: mocks.setAgentToolMaterializationServices,
46
+ setSourceControl: mocks.setSourceControl,
47
+ workflowsModule: {name: 'workflows'},
48
+ }));
49
+ vi.mock('@shipfox/api-workspaces', () => ({workspacesModule: {name: 'workspaces'}}));
50
+
51
+ describe('defaultModules', () => {
52
+ beforeEach(() => {
53
+ mocks.buildAgentToolCatalogs.mockReset();
54
+ mocks.buildAgentToolSelectionCatalogs.mockReset();
55
+ mocks.createDefinitionsModule.mockReset();
56
+ mocks.createIntegrationsContext.mockReset();
57
+ mocks.createProjectsModule.mockReset();
58
+ mocks.createWorkspaceConnectionSnapshotLoader.mockReset();
59
+ mocks.deleteSecrets.mockReset();
60
+ mocks.getIntegrationConnectionById.mockReset();
61
+ mocks.getSecret.mockReset();
62
+ mocks.loadRunningLeasedStep.mockReset();
63
+ mocks.setAgentToolMaterializationServices.mockReset();
64
+ mocks.setSecrets.mockReset();
65
+ mocks.setSourceControl.mockReset();
66
+
67
+ mocks.createIntegrationsContext.mockResolvedValue({
68
+ module: {name: 'integrations'},
69
+ registry: {},
70
+ sourceControl: {provider: 'source-control'},
71
+ });
72
+ mocks.buildAgentToolCatalogs.mockResolvedValue(new Map());
73
+ mocks.buildAgentToolSelectionCatalogs.mockResolvedValue(new Map());
74
+ mocks.createWorkspaceConnectionSnapshotLoader.mockReturnValue(vi.fn());
75
+ mocks.createProjectsModule.mockReturnValue({name: 'projects'});
76
+ mocks.createDefinitionsModule.mockReturnValue({name: 'definitions'});
77
+ });
78
+
79
+ it('returns the API modules in lifecycle order', async () => {
80
+ const modules = await defaultModules();
81
+
82
+ expect(modules.map((module) => module.name)).toEqual([
83
+ 'auth',
84
+ 'workspaces',
85
+ 'secrets',
86
+ 'agent',
87
+ 'integrations',
88
+ 'projects',
89
+ 'definitions',
90
+ 'workflows',
91
+ 'annotations',
92
+ 'runners',
93
+ 'logs',
94
+ 'triggers',
95
+ 'dispatcher',
96
+ ]);
97
+ });
98
+
99
+ it('uses the leased-step loader and namespaced Linear secrets', async () => {
100
+ await defaultModules();
101
+
102
+ expect(mocks.createIntegrationsContext).toHaveBeenCalledWith({
103
+ secrets: {
104
+ deleteSecrets: mocks.deleteSecrets,
105
+ linear: {
106
+ deleteSecrets: expect.any(Function),
107
+ getSecret: expect.any(Function),
108
+ setSecrets: expect.any(Function),
109
+ },
110
+ },
111
+ agentTools: {loadLeasedAgentStep: mocks.loadRunningLeasedStep},
112
+ });
113
+
114
+ const integrationsOptions = mocks.createIntegrationsContext.mock.calls[0]?.[0] as {
115
+ secrets: {
116
+ linear: {
117
+ deleteSecrets: (params: {namespace: string}) => unknown;
118
+ getSecret: (params: {namespace: string}) => unknown;
119
+ setSecrets: (params: {namespace: string}) => unknown;
120
+ };
121
+ };
122
+ };
123
+ integrationsOptions.secrets.linear.getSecret({namespace: 'workspace'});
124
+ integrationsOptions.secrets.linear.setSecrets({namespace: 'workspace'});
125
+ integrationsOptions.secrets.linear.deleteSecrets({namespace: 'workspace'});
126
+
127
+ expect(mocks.getSecret).toHaveBeenCalledWith({
128
+ namespace: 'system/integrations/linear/workspace',
129
+ });
130
+ expect(mocks.setSecrets).toHaveBeenCalledWith({
131
+ namespace: 'system/integrations/linear/workspace',
132
+ });
133
+ expect(mocks.deleteSecrets).toHaveBeenCalledWith({
134
+ namespace: 'system/integrations/linear/workspace',
135
+ });
136
+ });
137
+ });
package/src/modules.ts ADDED
@@ -0,0 +1,90 @@
1
+ import {annotationsModule} from '@shipfox/annotations';
2
+ import {agentModule} from '@shipfox/api-agent';
3
+ import {authModule} from '@shipfox/api-auth';
4
+ import {createDefinitionsModule} from '@shipfox/api-definitions';
5
+ import {dispatcherModule} from '@shipfox/api-dispatcher';
6
+ import {
7
+ buildAgentToolCatalogs,
8
+ buildAgentToolSelectionCatalogs,
9
+ createIntegrationsContext,
10
+ createWorkspaceConnectionSnapshotLoader,
11
+ getIntegrationConnectionById,
12
+ } from '@shipfox/api-integration-core';
13
+ import {logsModule} from '@shipfox/api-logs';
14
+ import {createProjectsModule} from '@shipfox/api-projects';
15
+ import {runnersModule} from '@shipfox/api-runners';
16
+ import {deleteSecrets, getSecret, secretsModule, setSecrets} from '@shipfox/api-secrets';
17
+ import {triggersModule} from '@shipfox/api-triggers';
18
+ import {
19
+ loadRunningLeasedStep,
20
+ setAgentToolMaterializationServices,
21
+ setSourceControl,
22
+ workflowsModule,
23
+ } from '@shipfox/api-workflows';
24
+ import {workspacesModule} from '@shipfox/api-workspaces';
25
+ import type {ShipfoxModule} from '@shipfox/node-module';
26
+
27
+ export async function defaultModules(): Promise<ShipfoxModule[]> {
28
+ const integrations = await createIntegrationsContext({
29
+ secrets: {
30
+ deleteSecrets,
31
+ linear: {
32
+ getSecret: (params) =>
33
+ getSecret({
34
+ ...params,
35
+ namespace: `system/integrations/linear/${params.namespace}`,
36
+ }),
37
+ setSecrets: (params) =>
38
+ setSecrets({
39
+ ...params,
40
+ namespace: `system/integrations/linear/${params.namespace}`,
41
+ }),
42
+ deleteSecrets: (params) =>
43
+ deleteSecrets({
44
+ ...params,
45
+ namespace: `system/integrations/linear/${params.namespace}`,
46
+ }),
47
+ },
48
+ },
49
+ agentTools: {loadLeasedAgentStep: loadRunningLeasedStep},
50
+ });
51
+ const [agentToolSelectionCatalogs, agentToolCatalogs] = await Promise.all([
52
+ buildAgentToolSelectionCatalogs(integrations.registry),
53
+ buildAgentToolCatalogs(integrations.registry),
54
+ ]);
55
+ const loadWorkspaceConnectionSnapshot = createWorkspaceConnectionSnapshotLoader(
56
+ integrations.registry,
57
+ );
58
+
59
+ // The checkout-token route resolves intents and mints credentials through the
60
+ // source-control service; wire it into the workflows module before serving.
61
+ setSourceControl(integrations.sourceControl);
62
+ setAgentToolMaterializationServices({
63
+ catalogs: agentToolCatalogs,
64
+ loadWorkspaceConnectionSnapshot,
65
+ getIntegrationConnectionById,
66
+ });
67
+ const projectsModule = createProjectsModule({sourceControl: integrations.sourceControl});
68
+ const definitionsModule = createDefinitionsModule({
69
+ sourceControl: integrations.sourceControl,
70
+ agentToolSelectionCatalogs,
71
+ loadWorkspaceConnectionSnapshot,
72
+ getIntegrationConnectionById,
73
+ });
74
+
75
+ return [
76
+ authModule,
77
+ workspacesModule,
78
+ secretsModule,
79
+ agentModule,
80
+ integrations.module,
81
+ projectsModule,
82
+ definitionsModule,
83
+ workflowsModule,
84
+ annotationsModule,
85
+ runnersModule,
86
+ logsModule,
87
+ triggersModule,
88
+ dispatcherModule,
89
+ ];
90
+ }