@shipfox/api-server 13.0.0 → 14.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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-type.log +0 -1
- package/CHANGELOG.md +65 -0
- package/README.md +20 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modules.d.ts +12 -0
- package/dist/modules.d.ts.map +1 -1
- package/dist/modules.js +23 -3
- package/dist/modules.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +19 -19
- package/src/index.ts +1 -0
- package/src/modules.test.ts +136 -3
- package/src/modules.ts +57 -2
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@shipfox/api-server",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"private": false,
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "14.0.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -24,28 +24,28 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@shipfox/annotations": "12.3.0",
|
|
26
26
|
"@shipfox/annotations-dto": "12.3.0",
|
|
27
|
-
"@shipfox/api-agent": "
|
|
28
|
-
"@shipfox/api-agent-dto": "
|
|
29
|
-
"@shipfox/api-auth": "
|
|
27
|
+
"@shipfox/api-agent": "14.0.0",
|
|
28
|
+
"@shipfox/api-agent-dto": "14.0.0",
|
|
29
|
+
"@shipfox/api-auth": "13.1.0",
|
|
30
30
|
"@shipfox/api-auth-dto": "12.0.0",
|
|
31
|
-
"@shipfox/api-definitions": "
|
|
32
|
-
"@shipfox/api-definitions-dto": "
|
|
31
|
+
"@shipfox/api-definitions": "14.0.0",
|
|
32
|
+
"@shipfox/api-definitions-dto": "14.0.0",
|
|
33
33
|
"@shipfox/api-dispatcher": "12.2.0",
|
|
34
|
-
"@shipfox/api-email-challenges": "1.1.
|
|
35
|
-
"@shipfox/api-integration-core": "
|
|
36
|
-
"@shipfox/api-integration-core-dto": "
|
|
37
|
-
"@shipfox/api-logs": "
|
|
38
|
-
"@shipfox/api-projects": "
|
|
39
|
-
"@shipfox/api-projects-dto": "
|
|
40
|
-
"@shipfox/api-runners": "
|
|
41
|
-
"@shipfox/api-runners-dto": "
|
|
42
|
-
"@shipfox/api-secrets": "
|
|
34
|
+
"@shipfox/api-email-challenges": "1.1.9",
|
|
35
|
+
"@shipfox/api-integration-core": "14.0.0",
|
|
36
|
+
"@shipfox/api-integration-core-dto": "14.0.0",
|
|
37
|
+
"@shipfox/api-logs": "14.0.0",
|
|
38
|
+
"@shipfox/api-projects": "14.0.0",
|
|
39
|
+
"@shipfox/api-projects-dto": "14.0.0",
|
|
40
|
+
"@shipfox/api-runners": "14.0.0",
|
|
41
|
+
"@shipfox/api-runners-dto": "14.0.0",
|
|
42
|
+
"@shipfox/api-secrets": "14.0.0",
|
|
43
43
|
"@shipfox/api-secrets-dto": "12.0.0",
|
|
44
|
-
"@shipfox/api-triggers": "
|
|
45
|
-
"@shipfox/api-workflows": "
|
|
46
|
-
"@shipfox/api-workflows-dto": "
|
|
44
|
+
"@shipfox/api-triggers": "14.0.0",
|
|
45
|
+
"@shipfox/api-workflows": "14.0.0",
|
|
46
|
+
"@shipfox/api-workflows-dto": "14.0.0",
|
|
47
47
|
"@shipfox/api-workspaces-dto": "12.0.0",
|
|
48
|
-
"@shipfox/api-workspaces": "
|
|
48
|
+
"@shipfox/api-workspaces": "14.0.0",
|
|
49
49
|
"@shipfox/config": "1.2.4",
|
|
50
50
|
"@shipfox/node-error-monitoring": "0.3.0",
|
|
51
51
|
"@shipfox/node-fastify": "0.4.2",
|
package/src/index.ts
CHANGED
package/src/modules.test.ts
CHANGED
|
@@ -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:
|
|
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: {
|
|
231
|
+
handlers: {
|
|
232
|
+
getDefinitionForWorkflowRun: vi.fn(),
|
|
233
|
+
listDefinitionsAtRef: vi.fn(),
|
|
234
|
+
resolveDefinitionAtRef: vi.fn(),
|
|
235
|
+
},
|
|
226
236
|
},
|
|
227
237
|
],
|
|
228
238
|
});
|
|
@@ -314,6 +324,129 @@ describe('defaultModules', () => {
|
|
|
314
324
|
expect(mocks.createAuthModule).toHaveBeenCalledWith({workspaces: expect.any(Object)});
|
|
315
325
|
});
|
|
316
326
|
|
|
327
|
+
it('uses the default Agent module factory when none is supplied', async () => {
|
|
328
|
+
await defaultModules();
|
|
329
|
+
|
|
330
|
+
expect(mocks.createAgentModule).toHaveBeenCalledWith({secrets: expect.any(Object)});
|
|
331
|
+
expect(mocks.createAgentModule.mock.calls[0]?.[0].secrets).not.toHaveProperty('getSecret');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('composes Agent with the shared Secrets client and registers the supplied module', async () => {
|
|
335
|
+
const customAgentModule = mocks.createAgentModule();
|
|
336
|
+
mocks.createAgentModule.mockClear();
|
|
337
|
+
const agentModule = vi.fn<DefaultAgentModuleFactory>(({secrets}) =>
|
|
338
|
+
mocks.createAgentModule({secrets}),
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const modules = await defaultModules({agentModule});
|
|
342
|
+
const agentFactoryCall = agentModule.mock.calls[0];
|
|
343
|
+
expect(agentFactoryCall).toBeDefined();
|
|
344
|
+
if (!agentFactoryCall) throw new Error('Agent module factory was not called.');
|
|
345
|
+
const agentSecrets = agentFactoryCall[0].secrets;
|
|
346
|
+
const scope = {
|
|
347
|
+
workspaceId: crypto.randomUUID(),
|
|
348
|
+
projectId: null,
|
|
349
|
+
namespace: 'agent',
|
|
350
|
+
};
|
|
351
|
+
const secretValues = await agentSecrets.getSecretsByNamespace(scope);
|
|
352
|
+
|
|
353
|
+
expect(agentModule).toHaveBeenCalledWith({secrets: expect.any(Object)});
|
|
354
|
+
expect(secretValues).toEqual({values: {}});
|
|
355
|
+
expect(mocks.getSecretsByNamespace).toHaveBeenCalledWith(
|
|
356
|
+
scope,
|
|
357
|
+
expect.objectContaining({signal: expect.any(AbortSignal)}),
|
|
358
|
+
);
|
|
359
|
+
expect(agentSecrets).not.toHaveProperty('getSecret');
|
|
360
|
+
expect(agentSecrets).not.toHaveProperty('getVariablesByNamespace');
|
|
361
|
+
expect(mocks.createAgentModule).toHaveBeenCalledWith({secrets: agentSecrets});
|
|
362
|
+
expect(mocks.createAgentModule).toHaveBeenCalledTimes(1);
|
|
363
|
+
expect(modules.filter((module) => module.name === 'agent')).toEqual([customAgentModule]);
|
|
364
|
+
expect(
|
|
365
|
+
modules
|
|
366
|
+
.flatMap((module) =>
|
|
367
|
+
(module.interModulePresentations ?? []).map(({contract}) => contract.module),
|
|
368
|
+
)
|
|
369
|
+
.filter((module) => module === agentInterModuleContract.module),
|
|
370
|
+
).toEqual([agentInterModuleContract.module]);
|
|
371
|
+
expect(modules.map((module) => module.name)).toEqual([
|
|
372
|
+
'email-challenges',
|
|
373
|
+
'auth',
|
|
374
|
+
'workspaces',
|
|
375
|
+
'secrets',
|
|
376
|
+
'agent',
|
|
377
|
+
'integrations',
|
|
378
|
+
'projects',
|
|
379
|
+
'definitions',
|
|
380
|
+
'workflows',
|
|
381
|
+
'annotations',
|
|
382
|
+
'runners',
|
|
383
|
+
'logs',
|
|
384
|
+
'triggers',
|
|
385
|
+
'dispatcher',
|
|
386
|
+
]);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('rejects a custom Agent module without the Agent database namespace', async () => {
|
|
390
|
+
const customAgentModule = {
|
|
391
|
+
...mocks.createAgentModule(),
|
|
392
|
+
database: {
|
|
393
|
+
db: () => undefined as never,
|
|
394
|
+
migrationsPath: 'test',
|
|
395
|
+
databaseNamespace: 'custom_agent',
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
await expect(defaultModules({agentModule: () => customAgentModule})).rejects.toThrow(
|
|
400
|
+
'Custom agentModule must declare database namespace "agent"',
|
|
401
|
+
);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('rejects a custom Agent module without the canonical Agent presentation', async () => {
|
|
405
|
+
const customAgentModule = {
|
|
406
|
+
...mocks.createAgentModule(),
|
|
407
|
+
interModulePresentations: [],
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
await expect(defaultModules({agentModule: () => customAgentModule})).rejects.toThrow(
|
|
411
|
+
'Custom agentModule must present the canonical "agent" inter-module contract',
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('rejects a custom Agent module with a mismatched Agent contract', async () => {
|
|
416
|
+
const defaultAgentModule = mocks.createAgentModule();
|
|
417
|
+
const defaultPresentation = defaultAgentModule.interModulePresentations?.[0];
|
|
418
|
+
if (!defaultPresentation) throw new Error('Default Agent presentation is not configured.');
|
|
419
|
+
const mismatchedAgentContract = defineInterModuleContract({
|
|
420
|
+
module: agentInterModuleContract.module,
|
|
421
|
+
methods: Object.fromEntries(
|
|
422
|
+
Object.entries(agentInterModuleContract.methods).map(([method, contract]) => [
|
|
423
|
+
method,
|
|
424
|
+
{input: contract.input, output: contract.output, errors: contract.errors},
|
|
425
|
+
]),
|
|
426
|
+
),
|
|
427
|
+
});
|
|
428
|
+
const customAgentModule = {
|
|
429
|
+
...defaultAgentModule,
|
|
430
|
+
interModulePresentations: [{...defaultPresentation, contract: mismatchedAgentContract}],
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
await expect(defaultModules({agentModule: () => customAgentModule})).rejects.toThrow(
|
|
434
|
+
'Custom agentModule must present the canonical "agent" inter-module contract',
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('rejects an extension that duplicates the Agent presentation', async () => {
|
|
439
|
+
const defaultAgentModule = mocks.createAgentModule();
|
|
440
|
+
const agentPresentation = defaultAgentModule.interModulePresentations?.[0];
|
|
441
|
+
if (!agentPresentation) throw new Error('Default Agent presentation is not configured.');
|
|
442
|
+
|
|
443
|
+
await expect(
|
|
444
|
+
defaultModules({
|
|
445
|
+
extension: () => [{name: 'duplicate-agent', interModulePresentations: [agentPresentation]}],
|
|
446
|
+
}),
|
|
447
|
+
).rejects.toThrow('Module "agent" already has a registered presentation');
|
|
448
|
+
});
|
|
449
|
+
|
|
317
450
|
it('composes Auth with the shared Workspaces client and registers the supplied module', async () => {
|
|
318
451
|
const signupPolicy = {isSignupAllowed: vi.fn().mockResolvedValue({allowed: true})};
|
|
319
452
|
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 {
|
|
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
|
-
|
|
194
|
+
agentModule,
|
|
176
195
|
integrations.module,
|
|
177
196
|
projectsModule,
|
|
178
197
|
definitionsModule,
|
|
@@ -201,3 +220,39 @@ export async function defaultModules(
|
|
|
201
220
|
interModuleTransport.seal();
|
|
202
221
|
return modules;
|
|
203
222
|
}
|
|
223
|
+
|
|
224
|
+
type AgentModuleSecretsClient = Pick<
|
|
225
|
+
SecretsInterModuleClient,
|
|
226
|
+
'deleteSecrets' | 'getSecretsByNamespace' | 'setSecrets'
|
|
227
|
+
>;
|
|
228
|
+
|
|
229
|
+
function createAgentSecretsClient(
|
|
230
|
+
secretsClient: SecretsInterModuleClient,
|
|
231
|
+
): AgentModuleSecretsClient {
|
|
232
|
+
return {
|
|
233
|
+
deleteSecrets: (input, options) => secretsClient.deleteSecrets(input, options),
|
|
234
|
+
getSecretsByNamespace: (input, options) => secretsClient.getSecretsByNamespace(input, options),
|
|
235
|
+
setSecrets: (input, options) => secretsClient.setSecrets(input, options),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function validateCustomAgentModule(module: ShipfoxModule): void {
|
|
240
|
+
const databases = module.database
|
|
241
|
+
? Array.isArray(module.database)
|
|
242
|
+
? module.database
|
|
243
|
+
: [module.database]
|
|
244
|
+
: [];
|
|
245
|
+
if (
|
|
246
|
+
!databases.some(({databaseNamespace}) => databaseNamespace === agentInterModuleContract.module)
|
|
247
|
+
) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
'Custom agentModule must declare database namespace "agent" for the Agent module.',
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (
|
|
254
|
+
!module.interModulePresentations?.some(({contract}) => contract === agentInterModuleContract)
|
|
255
|
+
) {
|
|
256
|
+
throw new Error('Custom agentModule must present the canonical "agent" inter-module contract.');
|
|
257
|
+
}
|
|
258
|
+
}
|