@shipfox/api-server 13.1.0 → 15.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.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@shipfox/api-server",
3
3
  "license": "MIT",
4
4
  "private": false,
5
- "version": "13.1.0",
5
+ "version": "15.0.0",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -22,36 +22,36 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@shipfox/annotations": "12.3.0",
25
+ "@shipfox/annotations": "15.0.0",
26
26
  "@shipfox/annotations-dto": "12.3.0",
27
- "@shipfox/api-agent": "13.1.0",
28
- "@shipfox/api-agent-dto": "13.1.0",
29
- "@shipfox/api-auth": "13.1.0",
30
- "@shipfox/api-auth-dto": "12.0.0",
31
- "@shipfox/api-definitions": "13.1.0",
32
- "@shipfox/api-definitions-dto": "12.3.0",
33
- "@shipfox/api-dispatcher": "12.2.0",
34
- "@shipfox/api-email-challenges": "1.1.9",
35
- "@shipfox/api-integration-core": "13.1.0",
36
- "@shipfox/api-integration-core-dto": "12.2.0",
37
- "@shipfox/api-logs": "13.1.0",
38
- "@shipfox/api-projects": "12.2.0",
39
- "@shipfox/api-projects-dto": "12.2.0",
40
- "@shipfox/api-runners": "13.1.0",
41
- "@shipfox/api-runners-dto": "13.0.0",
42
- "@shipfox/api-secrets": "12.2.0",
27
+ "@shipfox/api-agent": "15.0.0",
28
+ "@shipfox/api-agent-dto": "15.0.0",
29
+ "@shipfox/api-auth": "15.0.0",
30
+ "@shipfox/api-auth-dto": "15.0.0",
31
+ "@shipfox/api-definitions": "15.0.0",
32
+ "@shipfox/api-definitions-dto": "15.0.0",
33
+ "@shipfox/api-dispatcher": "15.0.0",
34
+ "@shipfox/api-email-challenges": "1.1.10",
35
+ "@shipfox/api-integration-core": "15.0.0",
36
+ "@shipfox/api-integration-core-dto": "15.0.0",
37
+ "@shipfox/api-logs": "15.0.0",
38
+ "@shipfox/api-projects": "15.0.0",
39
+ "@shipfox/api-projects-dto": "15.0.0",
40
+ "@shipfox/api-runners": "15.0.0",
41
+ "@shipfox/api-runners-dto": "14.0.0",
42
+ "@shipfox/api-secrets": "15.0.0",
43
43
  "@shipfox/api-secrets-dto": "12.0.0",
44
- "@shipfox/api-triggers": "13.1.0",
45
- "@shipfox/api-workflows": "13.1.0",
46
- "@shipfox/api-workflows-dto": "13.1.0",
47
- "@shipfox/api-workspaces-dto": "12.0.0",
48
- "@shipfox/api-workspaces": "13.1.0",
44
+ "@shipfox/api-triggers": "15.0.0",
45
+ "@shipfox/api-workflows": "15.0.0",
46
+ "@shipfox/api-workflows-dto": "15.0.0",
47
+ "@shipfox/api-workspaces-dto": "15.0.0",
48
+ "@shipfox/api-workspaces": "15.0.0",
49
49
  "@shipfox/config": "1.2.4",
50
50
  "@shipfox/node-error-monitoring": "0.3.0",
51
- "@shipfox/node-fastify": "0.4.2",
51
+ "@shipfox/node-fastify": "0.4.3",
52
52
  "@shipfox/node-jwt": "0.4.0",
53
- "@shipfox/node-module": "1.0.6",
54
- "@shipfox/node-opentelemetry": "0.6.4",
53
+ "@shipfox/node-module": "1.0.7",
54
+ "@shipfox/node-opentelemetry": "0.6.5",
55
55
  "@shipfox/node-postgres": "0.5.0"
56
56
  },
57
57
  "imports": {
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export {
2
+ type DefaultAgentModuleFactory,
2
3
  type DefaultAuthModuleFactory,
3
4
  type DefaultModulesExtension,
4
5
  type DefaultModulesOptions,
@@ -14,7 +14,8 @@ import {
14
14
  type WorkspacesInterModuleClient,
15
15
  workspacesInterModuleContract,
16
16
  } from '@shipfox/api-workspaces-dto/inter-module';
17
- import {defineInterModulePresentation} from '@shipfox/inter-module';
17
+ import {defineInterModuleContract, defineInterModulePresentation} from '@shipfox/inter-module';
18
+ import type {DefaultAgentModuleFactory} from './modules.js';
18
19
  import {defaultModules} from './modules.js';
19
20
 
20
21
  const mocks = vi.hoisted(() => ({
@@ -35,6 +36,7 @@ const mocks = vi.hoisted(() => ({
35
36
  deleteSecrets: vi.fn(),
36
37
  getIntegrationConnectionById: vi.fn(),
37
38
  getSecret: vi.fn(),
39
+ getSecretsByNamespace: vi.fn(),
38
40
  getWorkspaceCreator: vi.fn(),
39
41
  getWorkspaceOperatingState: vi.fn(),
40
42
  listMembershipsForTokenClaims: vi.fn(),
@@ -109,6 +111,7 @@ describe('defaultModules', () => {
109
111
  mocks.deleteSecrets.mockReset();
110
112
  mocks.getIntegrationConnectionById.mockReset();
111
113
  mocks.getSecret.mockReset();
114
+ mocks.getSecretsByNamespace.mockReset();
112
115
  mocks.getWorkspaceCreator.mockReset();
113
116
  mocks.listMembershipsForTokenClaims.mockReset();
114
117
  mocks.setSecrets.mockReset();
@@ -123,6 +126,7 @@ describe('defaultModules', () => {
123
126
  getAgentToolsContext: vi.fn(),
124
127
  listSourceFiles: vi.fn(),
125
128
  resolveConnection: vi.fn(),
129
+ resolveSourceRef: vi.fn(),
126
130
  resolveSourceRepository: vi.fn(),
127
131
  resolveTriggerReference: vi.fn(),
128
132
  }),
@@ -151,6 +155,7 @@ describe('defaultModules', () => {
151
155
  });
152
156
  mocks.deleteSecrets.mockResolvedValue({deleted: 1});
153
157
  mocks.getSecret.mockResolvedValue({value: 'secret'});
158
+ mocks.getSecretsByNamespace.mockResolvedValue({values: {}});
154
159
  mocks.listMembershipsForTokenClaims.mockResolvedValue({memberships: []});
155
160
  mocks.getWorkspaceCreator.mockResolvedValue({creatorUserId: null});
156
161
  mocks.setSecrets.mockResolvedValue({});
@@ -185,7 +190,7 @@ describe('defaultModules', () => {
185
190
  defineInterModulePresentation(secretsInterModuleContract, {
186
191
  deleteSecrets: mocks.deleteSecrets,
187
192
  getSecret: mocks.getSecret,
188
- getSecretsByNamespace: vi.fn(),
193
+ getSecretsByNamespace: mocks.getSecretsByNamespace,
189
194
  getVariablesByNamespace: vi.fn(),
190
195
  setSecrets: mocks.setSecrets,
191
196
  }),
@@ -193,6 +198,7 @@ describe('defaultModules', () => {
193
198
  });
194
199
  mocks.createAgentModule.mockReturnValue({
195
200
  name: 'agent',
201
+ database: {db: () => undefined as never, migrationsPath: 'test', databaseNamespace: 'agent'},
196
202
  interModulePresentations: [
197
203
  {
198
204
  contract: agentInterModuleContract,
@@ -222,7 +228,11 @@ describe('defaultModules', () => {
222
228
  interModulePresentations: [
223
229
  {
224
230
  contract: definitionsInterModuleContract,
225
- handlers: {getDefinitionForWorkflowRun: vi.fn()},
231
+ handlers: {
232
+ getDefinitionForWorkflowRun: vi.fn(),
233
+ listDefinitionsAtRef: vi.fn(),
234
+ resolveDefinitionAtRef: vi.fn(),
235
+ },
226
236
  },
227
237
  ],
228
238
  });
@@ -236,6 +246,7 @@ describe('defaultModules', () => {
236
246
  getLeasedAgentToolContext: vi.fn(),
237
247
  getStepLogContext: vi.fn(),
238
248
  resolveWorkflowRunTriggerReference: vi.fn(),
249
+ startDevRun: vi.fn(),
239
250
  startRunFromTrigger: vi.fn(),
240
251
  },
241
252
  },
@@ -314,6 +325,129 @@ describe('defaultModules', () => {
314
325
  expect(mocks.createAuthModule).toHaveBeenCalledWith({workspaces: expect.any(Object)});
315
326
  });
316
327
 
328
+ it('uses the default Agent module factory when none is supplied', async () => {
329
+ await defaultModules();
330
+
331
+ expect(mocks.createAgentModule).toHaveBeenCalledWith({secrets: expect.any(Object)});
332
+ expect(mocks.createAgentModule.mock.calls[0]?.[0].secrets).not.toHaveProperty('getSecret');
333
+ });
334
+
335
+ it('composes Agent with the shared Secrets client and registers the supplied module', async () => {
336
+ const customAgentModule = mocks.createAgentModule();
337
+ mocks.createAgentModule.mockClear();
338
+ const agentModule = vi.fn<DefaultAgentModuleFactory>(({secrets}) =>
339
+ mocks.createAgentModule({secrets}),
340
+ );
341
+
342
+ const modules = await defaultModules({agentModule});
343
+ const agentFactoryCall = agentModule.mock.calls[0];
344
+ expect(agentFactoryCall).toBeDefined();
345
+ if (!agentFactoryCall) throw new Error('Agent module factory was not called.');
346
+ const agentSecrets = agentFactoryCall[0].secrets;
347
+ const scope = {
348
+ workspaceId: crypto.randomUUID(),
349
+ projectId: null,
350
+ namespace: 'agent',
351
+ };
352
+ const secretValues = await agentSecrets.getSecretsByNamespace(scope);
353
+
354
+ expect(agentModule).toHaveBeenCalledWith({secrets: expect.any(Object)});
355
+ expect(secretValues).toEqual({values: {}});
356
+ expect(mocks.getSecretsByNamespace).toHaveBeenCalledWith(
357
+ scope,
358
+ expect.objectContaining({signal: expect.any(AbortSignal)}),
359
+ );
360
+ expect(agentSecrets).not.toHaveProperty('getSecret');
361
+ expect(agentSecrets).not.toHaveProperty('getVariablesByNamespace');
362
+ expect(mocks.createAgentModule).toHaveBeenCalledWith({secrets: agentSecrets});
363
+ expect(mocks.createAgentModule).toHaveBeenCalledTimes(1);
364
+ expect(modules.filter((module) => module.name === 'agent')).toEqual([customAgentModule]);
365
+ expect(
366
+ modules
367
+ .flatMap((module) =>
368
+ (module.interModulePresentations ?? []).map(({contract}) => contract.module),
369
+ )
370
+ .filter((module) => module === agentInterModuleContract.module),
371
+ ).toEqual([agentInterModuleContract.module]);
372
+ expect(modules.map((module) => module.name)).toEqual([
373
+ 'email-challenges',
374
+ 'auth',
375
+ 'workspaces',
376
+ 'secrets',
377
+ 'agent',
378
+ 'integrations',
379
+ 'projects',
380
+ 'definitions',
381
+ 'workflows',
382
+ 'annotations',
383
+ 'runners',
384
+ 'logs',
385
+ 'triggers',
386
+ 'dispatcher',
387
+ ]);
388
+ });
389
+
390
+ it('rejects a custom Agent module without the Agent database namespace', async () => {
391
+ const customAgentModule = {
392
+ ...mocks.createAgentModule(),
393
+ database: {
394
+ db: () => undefined as never,
395
+ migrationsPath: 'test',
396
+ databaseNamespace: 'custom_agent',
397
+ },
398
+ };
399
+
400
+ await expect(defaultModules({agentModule: () => customAgentModule})).rejects.toThrow(
401
+ 'Custom agentModule must declare database namespace "agent"',
402
+ );
403
+ });
404
+
405
+ it('rejects a custom Agent module without the canonical Agent presentation', async () => {
406
+ const customAgentModule = {
407
+ ...mocks.createAgentModule(),
408
+ interModulePresentations: [],
409
+ };
410
+
411
+ await expect(defaultModules({agentModule: () => customAgentModule})).rejects.toThrow(
412
+ 'Custom agentModule must present the canonical "agent" inter-module contract',
413
+ );
414
+ });
415
+
416
+ it('rejects a custom Agent module with a mismatched Agent contract', async () => {
417
+ const defaultAgentModule = mocks.createAgentModule();
418
+ const defaultPresentation = defaultAgentModule.interModulePresentations?.[0];
419
+ if (!defaultPresentation) throw new Error('Default Agent presentation is not configured.');
420
+ const mismatchedAgentContract = defineInterModuleContract({
421
+ module: agentInterModuleContract.module,
422
+ methods: Object.fromEntries(
423
+ Object.entries(agentInterModuleContract.methods).map(([method, contract]) => [
424
+ method,
425
+ {input: contract.input, output: contract.output, errors: contract.errors},
426
+ ]),
427
+ ),
428
+ });
429
+ const customAgentModule = {
430
+ ...defaultAgentModule,
431
+ interModulePresentations: [{...defaultPresentation, contract: mismatchedAgentContract}],
432
+ };
433
+
434
+ await expect(defaultModules({agentModule: () => customAgentModule})).rejects.toThrow(
435
+ 'Custom agentModule must present the canonical "agent" inter-module contract',
436
+ );
437
+ });
438
+
439
+ it('rejects an extension that duplicates the Agent presentation', async () => {
440
+ const defaultAgentModule = mocks.createAgentModule();
441
+ const agentPresentation = defaultAgentModule.interModulePresentations?.[0];
442
+ if (!agentPresentation) throw new Error('Default Agent presentation is not configured.');
443
+
444
+ await expect(
445
+ defaultModules({
446
+ extension: () => [{name: 'duplicate-agent', interModulePresentations: [agentPresentation]}],
447
+ }),
448
+ ).rejects.toThrow('Module "agent" already has a registered presentation');
449
+ });
450
+
317
451
  it('composes Auth with the shared Workspaces client and registers the supplied module', async () => {
318
452
  const signupPolicy = {isSignupAllowed: vi.fn().mockResolvedValue({allowed: true})};
319
453
  const customAuthModule = mocks.createAuthModule();
package/src/modules.ts CHANGED
@@ -20,7 +20,10 @@ import {projectsInterModuleContract} from '@shipfox/api-projects-dto/inter-modul
20
20
  import {createRunnersModule} from '@shipfox/api-runners';
21
21
  import {runnersInterModuleContract} from '@shipfox/api-runners-dto/inter-module';
22
22
  import {createSecretsModule} from '@shipfox/api-secrets';
23
- import {secretsInterModuleContract} from '@shipfox/api-secrets-dto/inter-module';
23
+ import {
24
+ type SecretsInterModuleClient,
25
+ secretsInterModuleContract,
26
+ } from '@shipfox/api-secrets-dto/inter-module';
24
27
  import {createTriggersModule} from '@shipfox/api-triggers';
25
28
  import {createWorkflowsModule} from '@shipfox/api-workflows';
26
29
  import {workflowsInterModuleContract} from '@shipfox/api-workflows-dto/inter-module';
@@ -41,6 +44,7 @@ import {logger} from '@shipfox/node-opentelemetry';
41
44
  export interface DefaultModulesOptions {
42
45
  webhookDeliverySource?: WebhookDeliverySource | undefined;
43
46
  authModule?: DefaultAuthModuleFactory | undefined;
47
+ agentModule?: DefaultAgentModuleFactory | undefined;
44
48
  runnersModule?: DefaultRunnersModuleFactory | undefined;
45
49
  extension?: DefaultModulesExtension | undefined;
46
50
  }
@@ -48,6 +52,17 @@ export interface DefaultModulesOptions {
48
52
  export type DefaultAuthModuleFactory = (options: {
49
53
  workspaces: WorkspacesInterModuleClient;
50
54
  }) => ShipfoxModule;
55
+
56
+ /**
57
+ * Builds the Agent module in the standard composition slot. The default
58
+ * factory validates Agent configuration through `createAgentModule`; a custom
59
+ * factory owns equivalent validation when it does not delegate to that
60
+ * factory. Custom modules must preserve the `agent` database namespace and
61
+ * canonical Agent presentation required by the composed clients.
62
+ */
63
+ export type DefaultAgentModuleFactory = (options: {
64
+ secrets: Pick<SecretsInterModuleClient, 'deleteSecrets' | 'getSecretsByNamespace' | 'setSecrets'>;
65
+ }) => ShipfoxModule;
51
66
  export type DefaultRunnersModuleFactory = (options: {auth: AuthInterModuleClient}) => ShipfoxModule;
52
67
  export type DefaultModulesExtension = (options: {
53
68
  workspaces: WorkspacesInterModuleClient;
@@ -166,13 +181,17 @@ export async function defaultModules(
166
181
  integrations: integrationsClient,
167
182
  });
168
183
  const extensionModules = options.extension?.({workspaces: workspacesClient}) ?? [];
184
+ const agentModule = (options.agentModule ?? createAgentModule)({
185
+ secrets: createAgentSecretsClient(secretsClient),
186
+ });
187
+ if (options.agentModule) validateCustomAgentModule(agentModule);
169
188
 
170
189
  const modules = [
171
190
  emailChallengesModule,
172
191
  (options.authModule ?? createAuthModule)({workspaces: workspacesClient}),
173
192
  createWorkspacesModule({auth: authClient, projects: projectsClient, runners: runnersClient}),
174
193
  createSecretsModule(projectsClient),
175
- createAgentModule({secrets: secretsClient}),
194
+ agentModule,
176
195
  integrations.module,
177
196
  projectsModule,
178
197
  definitionsModule,
@@ -193,7 +212,11 @@ export async function defaultModules(
193
212
  workflows: workflowsClient,
194
213
  jobLeaseTokenTtlSeconds: durationToSeconds(authConfig.AUTH_JOB_LEASE_TOKEN_EXPIRES_IN),
195
214
  }),
196
- createTriggersModule({workflows: workflowsClient}),
215
+ createTriggersModule({
216
+ workflows: workflowsClient,
217
+ definitions: definitionsClient,
218
+ projects: projectsClient,
219
+ }),
197
220
  dispatcherModule,
198
221
  ...extensionModules,
199
222
  ];
@@ -201,3 +224,39 @@ export async function defaultModules(
201
224
  interModuleTransport.seal();
202
225
  return modules;
203
226
  }
227
+
228
+ type AgentModuleSecretsClient = Pick<
229
+ SecretsInterModuleClient,
230
+ 'deleteSecrets' | 'getSecretsByNamespace' | 'setSecrets'
231
+ >;
232
+
233
+ function createAgentSecretsClient(
234
+ secretsClient: SecretsInterModuleClient,
235
+ ): AgentModuleSecretsClient {
236
+ return {
237
+ deleteSecrets: (input, options) => secretsClient.deleteSecrets(input, options),
238
+ getSecretsByNamespace: (input, options) => secretsClient.getSecretsByNamespace(input, options),
239
+ setSecrets: (input, options) => secretsClient.setSecrets(input, options),
240
+ };
241
+ }
242
+
243
+ function validateCustomAgentModule(module: ShipfoxModule): void {
244
+ const databases = module.database
245
+ ? Array.isArray(module.database)
246
+ ? module.database
247
+ : [module.database]
248
+ : [];
249
+ if (
250
+ !databases.some(({databaseNamespace}) => databaseNamespace === agentInterModuleContract.module)
251
+ ) {
252
+ throw new Error(
253
+ 'Custom agentModule must declare database namespace "agent" for the Agent module.',
254
+ );
255
+ }
256
+
257
+ if (
258
+ !module.interModulePresentations?.some(({contract}) => contract === agentInterModuleContract)
259
+ ) {
260
+ throw new Error('Custom agentModule must present the canonical "agent" inter-module contract.');
261
+ }
262
+ }