@shipfox/api-server 7.0.2 → 8.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/CHANGELOG.md +63 -0
- package/README.md +11 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/instrumentation.js +5 -1
- package/dist/instrumentation.js.map +1 -1
- package/dist/modules.d.ts +10 -0
- package/dist/modules.d.ts.map +1 -1
- package/dist/modules.js +30 -5
- package/dist/modules.js.map +1 -1
- package/dist/routes/login-methods.d.ts +5 -0
- package/dist/routes/login-methods.d.ts.map +1 -0
- package/dist/routes/login-methods.js +22 -0
- package/dist/routes/login-methods.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +68 -11
- package/dist/server.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +25 -25
- package/src/auth-token-interoperability.test.ts +52 -0
- package/src/index.ts +7 -1
- package/src/instrumentation.ts +5 -1
- package/src/modules.test.ts +70 -5
- package/src/modules.ts +36 -6
- package/src/routes/login-methods.test.ts +56 -0
- package/src/routes/login-methods.ts +21 -0
- package/src/server.test.ts +26 -6
- package/src/server.ts +52 -14
- 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": "8.0.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -25,37 +25,37 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@shipfox/annotations": "
|
|
28
|
+
"@shipfox/annotations": "8.0.0",
|
|
29
29
|
"@shipfox/annotations-dto": "6.0.0",
|
|
30
|
-
"@shipfox/api-agent": "
|
|
31
|
-
"@shipfox/api-agent-dto": "
|
|
32
|
-
"@shipfox/api-auth": "7.0
|
|
33
|
-
"@shipfox/api-auth-dto": "
|
|
34
|
-
"@shipfox/api-definitions": "
|
|
30
|
+
"@shipfox/api-agent": "8.0.0",
|
|
31
|
+
"@shipfox/api-agent-dto": "8.0.0",
|
|
32
|
+
"@shipfox/api-auth": "7.1.0",
|
|
33
|
+
"@shipfox/api-auth-dto": "7.1.0",
|
|
34
|
+
"@shipfox/api-definitions": "8.0.0",
|
|
35
35
|
"@shipfox/api-definitions-dto": "6.0.0",
|
|
36
|
-
"@shipfox/api-dispatcher": "
|
|
37
|
-
"@shipfox/api-email-challenges": "0.
|
|
38
|
-
"@shipfox/api-integration-core": "
|
|
39
|
-
"@shipfox/api-integration-core-dto": "
|
|
40
|
-
"@shipfox/api-logs": "
|
|
41
|
-
"@shipfox/api-projects": "
|
|
42
|
-
"@shipfox/api-projects-dto": "
|
|
43
|
-
"@shipfox/api-runners": "
|
|
36
|
+
"@shipfox/api-dispatcher": "7.1.0",
|
|
37
|
+
"@shipfox/api-email-challenges": "0.3.0",
|
|
38
|
+
"@shipfox/api-integration-core": "8.0.0",
|
|
39
|
+
"@shipfox/api-integration-core-dto": "8.0.0",
|
|
40
|
+
"@shipfox/api-logs": "8.0.0",
|
|
41
|
+
"@shipfox/api-projects": "8.0.0",
|
|
42
|
+
"@shipfox/api-projects-dto": "8.0.0",
|
|
43
|
+
"@shipfox/api-runners": "8.0.0",
|
|
44
44
|
"@shipfox/api-runners-dto": "7.0.1",
|
|
45
|
-
"@shipfox/api-secrets": "
|
|
45
|
+
"@shipfox/api-secrets": "8.0.0",
|
|
46
46
|
"@shipfox/api-secrets-dto": "6.0.0",
|
|
47
|
-
"@shipfox/api-triggers": "
|
|
48
|
-
"@shipfox/api-workflows": "
|
|
49
|
-
"@shipfox/api-workflows-dto": "
|
|
47
|
+
"@shipfox/api-triggers": "8.0.0",
|
|
48
|
+
"@shipfox/api-workflows": "8.0.0",
|
|
49
|
+
"@shipfox/api-workflows-dto": "8.0.0",
|
|
50
50
|
"@shipfox/api-workspaces-dto": "6.0.0",
|
|
51
|
-
"@shipfox/api-workspaces": "7.0
|
|
51
|
+
"@shipfox/api-workspaces": "7.1.0",
|
|
52
52
|
"@shipfox/config": "1.2.2",
|
|
53
|
-
"@shipfox/node-error-monitoring": "0.
|
|
54
|
-
"@shipfox/node-fastify": "0.
|
|
53
|
+
"@shipfox/node-error-monitoring": "0.2.0",
|
|
54
|
+
"@shipfox/node-fastify": "0.3.0",
|
|
55
55
|
"@shipfox/node-jwt": "0.3.0",
|
|
56
|
-
"@shipfox/node-
|
|
57
|
-
"@shipfox/node-
|
|
58
|
-
"@shipfox/node-
|
|
56
|
+
"@shipfox/node-module": "0.5.0",
|
|
57
|
+
"@shipfox/node-opentelemetry": "0.6.0",
|
|
58
|
+
"@shipfox/node-postgres": "0.4.2"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "shipfox-swc",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {AUTH_LEASED_JOB, requireLeasedJobContext} from '@shipfox/api-auth-context';
|
|
2
|
+
import {closeApp, createApp, defineRoute} from '@shipfox/node-fastify';
|
|
3
|
+
import {afterEach, describe, expect, it} from '@shipfox/vitest/vi';
|
|
4
|
+
|
|
5
|
+
process.env.AUTH_ROOT_KEY ??= 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=';
|
|
6
|
+
|
|
7
|
+
const leaseRoute = defineRoute({
|
|
8
|
+
method: 'GET',
|
|
9
|
+
path: '/lease',
|
|
10
|
+
description: 'Auth composition test route.',
|
|
11
|
+
handler: (request) => {
|
|
12
|
+
const lease = requireLeasedJobContext(request);
|
|
13
|
+
return {job_id: lease.jobId, step_id: lease.currentStepId};
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('Auth token interoperability', () => {
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await closeApp();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('accepts an Auth-issued step lease through the server route boundary', async () => {
|
|
23
|
+
const {createLeaseTokenAuthMethod, issueJobLeaseToken} = await import('@shipfox/api-auth');
|
|
24
|
+
const jobId = crypto.randomUUID();
|
|
25
|
+
const stepId = crypto.randomUUID();
|
|
26
|
+
const token = await issueJobLeaseToken({
|
|
27
|
+
jobId,
|
|
28
|
+
jobExecutionId: crypto.randomUUID(),
|
|
29
|
+
workflowRunId: crypto.randomUUID(),
|
|
30
|
+
workflowRunAttemptId: crypto.randomUUID(),
|
|
31
|
+
projectId: crypto.randomUUID(),
|
|
32
|
+
workspaceId: crypto.randomUUID(),
|
|
33
|
+
runnerSessionId: crypto.randomUUID(),
|
|
34
|
+
currentStepId: stepId,
|
|
35
|
+
currentStepAttempt: 1,
|
|
36
|
+
});
|
|
37
|
+
const app = await createApp({
|
|
38
|
+
auth: [createLeaseTokenAuthMethod()],
|
|
39
|
+
routes: [{prefix: '/runs/jobs/current', auth: AUTH_LEASED_JOB, routes: [leaseRoute]}],
|
|
40
|
+
swagger: false,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const res = await app.inject({
|
|
44
|
+
method: 'GET',
|
|
45
|
+
url: '/runs/jobs/current/lease',
|
|
46
|
+
headers: {authorization: `Bearer ${token}`},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(res.statusCode).toBe(200);
|
|
50
|
+
expect(res.json()).toEqual({job_id: jobId, step_id: stepId});
|
|
51
|
+
});
|
|
52
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
type DefaultModulesExtension,
|
|
3
|
+
type DefaultModulesOptions,
|
|
4
|
+
type DefaultRunnersModuleFactory,
|
|
5
|
+
defaultModules,
|
|
6
|
+
} from './modules.js';
|
|
7
|
+
export {createLoginMethodsRoute} from './routes/login-methods.js';
|
|
2
8
|
export type {CreateServerOptions, RunServerOptions, ServerHandle} from './server.js';
|
|
3
9
|
export {createServer, runServer} from './server.js';
|
package/src/instrumentation.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import {createRequire} from 'node:module';
|
|
1
2
|
import {startInstanceInstrumentation} from '@shipfox/node-opentelemetry';
|
|
2
3
|
|
|
4
|
+
const {version} = createRequire(import.meta.url)('../package.json') as {version: string};
|
|
5
|
+
|
|
3
6
|
// The metrics API has no proxy meter: instruments created before this preload
|
|
4
7
|
// completes bind to a no-op provider for the process lifetime.
|
|
5
8
|
await startInstanceInstrumentation({
|
|
6
9
|
serviceName: 'api',
|
|
7
|
-
|
|
10
|
+
serviceVersion: version,
|
|
11
|
+
instrumentations: {fastify: true, http: true, pg: true, awsSdk: true},
|
|
8
12
|
});
|
package/src/modules.test.ts
CHANGED
|
@@ -2,15 +2,18 @@ import {annotationsInterModuleContract} from '@shipfox/annotations-dto/inter-mod
|
|
|
2
2
|
import {agentInterModuleContract} from '@shipfox/api-agent-dto/inter-module';
|
|
3
3
|
import {authInterModuleContract} from '@shipfox/api-auth-dto/inter-module';
|
|
4
4
|
import {definitionsInterModuleContract} from '@shipfox/api-definitions-dto/inter-module';
|
|
5
|
-
import {integrationsInterModuleContract} from '@shipfox/api-integration-core-dto';
|
|
6
|
-
import {projectsInterModuleContract} from '@shipfox/api-projects-dto';
|
|
5
|
+
import {integrationsInterModuleContract} from '@shipfox/api-integration-core-dto/inter-module';
|
|
6
|
+
import {projectsInterModuleContract} from '@shipfox/api-projects-dto/inter-module';
|
|
7
7
|
import {runnersInterModuleContract} from '@shipfox/api-runners-dto/inter-module';
|
|
8
8
|
import {
|
|
9
9
|
type SecretsInterModuleClient,
|
|
10
10
|
secretsInterModuleContract,
|
|
11
11
|
} from '@shipfox/api-secrets-dto/inter-module';
|
|
12
12
|
import {workflowsInterModuleContract} from '@shipfox/api-workflows-dto/inter-module';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
type WorkspacesInterModuleClient,
|
|
15
|
+
workspacesInterModuleContract,
|
|
16
|
+
} from '@shipfox/api-workspaces-dto/inter-module';
|
|
14
17
|
import {defineInterModulePresentation} from '@shipfox/inter-module';
|
|
15
18
|
import {defaultModules} from './modules.js';
|
|
16
19
|
|
|
@@ -30,6 +33,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
30
33
|
deleteSecrets: vi.fn(),
|
|
31
34
|
getIntegrationConnectionById: vi.fn(),
|
|
32
35
|
getSecret: vi.fn(),
|
|
36
|
+
listMembershipsForTokenClaims: vi.fn(),
|
|
33
37
|
setSecrets: vi.fn(),
|
|
34
38
|
}));
|
|
35
39
|
|
|
@@ -96,7 +100,7 @@ vi.mock('@shipfox/api-workspaces', () => ({
|
|
|
96
100
|
{
|
|
97
101
|
contract: workspacesInterModuleContract,
|
|
98
102
|
handlers: {
|
|
99
|
-
listMembershipsForTokenClaims:
|
|
103
|
+
listMembershipsForTokenClaims: mocks.listMembershipsForTokenClaims,
|
|
100
104
|
preflightInvitationAcceptance: vi.fn(),
|
|
101
105
|
acceptInvitation: vi.fn(),
|
|
102
106
|
requireActiveMembership: vi.fn(),
|
|
@@ -123,6 +127,7 @@ describe('defaultModules', () => {
|
|
|
123
127
|
mocks.deleteSecrets.mockReset();
|
|
124
128
|
mocks.getIntegrationConnectionById.mockReset();
|
|
125
129
|
mocks.getSecret.mockReset();
|
|
130
|
+
mocks.listMembershipsForTokenClaims.mockReset();
|
|
126
131
|
mocks.setSecrets.mockReset();
|
|
127
132
|
|
|
128
133
|
mocks.createIntegrationsContext.mockResolvedValue({
|
|
@@ -147,6 +152,7 @@ describe('defaultModules', () => {
|
|
|
147
152
|
mocks.createWorkspaceConnectionSnapshotLoader.mockReturnValue(vi.fn());
|
|
148
153
|
mocks.deleteSecrets.mockResolvedValue({deleted: 1});
|
|
149
154
|
mocks.getSecret.mockResolvedValue({value: 'secret'});
|
|
155
|
+
mocks.listMembershipsForTokenClaims.mockResolvedValue({memberships: []});
|
|
150
156
|
mocks.setSecrets.mockResolvedValue({});
|
|
151
157
|
mocks.createProjectsModule.mockReturnValue({
|
|
152
158
|
name: 'projects',
|
|
@@ -182,7 +188,11 @@ describe('defaultModules', () => {
|
|
|
182
188
|
interModulePresentations: [
|
|
183
189
|
{
|
|
184
190
|
contract: agentInterModuleContract,
|
|
185
|
-
handlers: {
|
|
191
|
+
handlers: {
|
|
192
|
+
getValidationCatalog: vi.fn(),
|
|
193
|
+
resolveAgentConfig: vi.fn(),
|
|
194
|
+
resolveRuntimeCredentials: vi.fn(),
|
|
195
|
+
},
|
|
186
196
|
},
|
|
187
197
|
],
|
|
188
198
|
});
|
|
@@ -254,6 +264,61 @@ describe('defaultModules', () => {
|
|
|
254
264
|
expect(mocks.createRunnersModule).toHaveBeenCalledWith({auth: expect.any(Object)});
|
|
255
265
|
});
|
|
256
266
|
|
|
267
|
+
it('lets a host replace only the Runners module with the composed Auth client', async () => {
|
|
268
|
+
const policy = {filterEligibleWorkspaceIds: vi.fn().mockResolvedValue(new Set<string>())};
|
|
269
|
+
const runnersModule = mocks.createRunnersModule({});
|
|
270
|
+
const createHostRunnersModule = vi.fn(({auth}) =>
|
|
271
|
+
mocks.createRunnersModule({auth, installationProvisioning: {policy}}),
|
|
272
|
+
);
|
|
273
|
+
mocks.createRunnersModule.mockClear();
|
|
274
|
+
|
|
275
|
+
const modules = await defaultModules({runnersModule: createHostRunnersModule});
|
|
276
|
+
|
|
277
|
+
expect(createHostRunnersModule).toHaveBeenCalledWith({auth: expect.any(Object)});
|
|
278
|
+
expect(mocks.createRunnersModule).toHaveBeenCalledWith({
|
|
279
|
+
auth: createHostRunnersModule.mock.calls[0]?.[0].auth,
|
|
280
|
+
installationProvisioning: {policy},
|
|
281
|
+
});
|
|
282
|
+
expect(modules.filter((module) => module.name === 'runners')).toEqual([runnersModule]);
|
|
283
|
+
expect(modules.map((module) => module.name)).toEqual([
|
|
284
|
+
'email-challenges',
|
|
285
|
+
'auth',
|
|
286
|
+
'workspaces',
|
|
287
|
+
'secrets',
|
|
288
|
+
'agent',
|
|
289
|
+
'integrations',
|
|
290
|
+
'projects',
|
|
291
|
+
'definitions',
|
|
292
|
+
'workflows',
|
|
293
|
+
'annotations',
|
|
294
|
+
'runners',
|
|
295
|
+
'logs',
|
|
296
|
+
'triggers',
|
|
297
|
+
'dispatcher',
|
|
298
|
+
]);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('extends the default module list with the composed Workspaces client', async () => {
|
|
302
|
+
let workspaces: WorkspacesInterModuleClient | undefined;
|
|
303
|
+
const extensionModule = {name: 'cloud'};
|
|
304
|
+
const extension = vi.fn((options: {workspaces: WorkspacesInterModuleClient}) => {
|
|
305
|
+
workspaces = options.workspaces;
|
|
306
|
+
return [extensionModule];
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const modules = await defaultModules({extension});
|
|
310
|
+
const userId = crypto.randomUUID();
|
|
311
|
+
const memberships = await workspaces?.listMembershipsForTokenClaims({userId});
|
|
312
|
+
|
|
313
|
+
expect(extension).toHaveBeenCalledWith({workspaces: expect.any(Object)});
|
|
314
|
+
expect(memberships).toEqual({memberships: []});
|
|
315
|
+
expect(mocks.listMembershipsForTokenClaims).toHaveBeenCalledWith(
|
|
316
|
+
{userId},
|
|
317
|
+
expect.objectContaining({signal: expect.any(AbortSignal)}),
|
|
318
|
+
);
|
|
319
|
+
expect(modules.at(-1)).toBe(extensionModule);
|
|
320
|
+
});
|
|
321
|
+
|
|
257
322
|
it('injects Workflows into integrations and logs and namespaces provider secrets', async () => {
|
|
258
323
|
await defaultModules();
|
|
259
324
|
|
package/src/modules.ts
CHANGED
|
@@ -4,16 +4,19 @@ import {createAgentModule} from '@shipfox/api-agent';
|
|
|
4
4
|
import {agentInterModuleContract} from '@shipfox/api-agent-dto/inter-module';
|
|
5
5
|
import {createAuthModule} from '@shipfox/api-auth';
|
|
6
6
|
import {config as authConfig} from '@shipfox/api-auth/config';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type AuthInterModuleClient,
|
|
9
|
+
authInterModuleContract,
|
|
10
|
+
} from '@shipfox/api-auth-dto/inter-module';
|
|
8
11
|
import {createDefinitionsModule} from '@shipfox/api-definitions';
|
|
9
12
|
import {definitionsInterModuleContract} from '@shipfox/api-definitions-dto/inter-module';
|
|
10
13
|
import {dispatcherModule} from '@shipfox/api-dispatcher';
|
|
11
14
|
import {emailChallengesModule} from '@shipfox/api-email-challenges';
|
|
12
15
|
import {createIntegrationsContext, type WebhookDeliverySource} from '@shipfox/api-integration-core';
|
|
13
|
-
import {integrationsInterModuleContract} from '@shipfox/api-integration-core-dto';
|
|
16
|
+
import {integrationsInterModuleContract} from '@shipfox/api-integration-core-dto/inter-module';
|
|
14
17
|
import {createLogsModule} from '@shipfox/api-logs';
|
|
15
18
|
import {createProjectsModule} from '@shipfox/api-projects';
|
|
16
|
-
import {projectsInterModuleContract} from '@shipfox/api-projects-dto';
|
|
19
|
+
import {projectsInterModuleContract} from '@shipfox/api-projects-dto/inter-module';
|
|
17
20
|
import {createRunnersModule} from '@shipfox/api-runners';
|
|
18
21
|
import {runnersInterModuleContract} from '@shipfox/api-runners-dto/inter-module';
|
|
19
22
|
import {createSecretsModule} from '@shipfox/api-secrets';
|
|
@@ -22,22 +25,46 @@ import {createTriggersModule} from '@shipfox/api-triggers';
|
|
|
22
25
|
import {createWorkflowsModule} from '@shipfox/api-workflows';
|
|
23
26
|
import {workflowsInterModuleContract} from '@shipfox/api-workflows-dto/inter-module';
|
|
24
27
|
import {workspacesModule} from '@shipfox/api-workspaces';
|
|
25
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
type WorkspacesInterModuleClient,
|
|
30
|
+
workspacesInterModuleContract,
|
|
31
|
+
} from '@shipfox/api-workspaces-dto/inter-module';
|
|
32
|
+
import {reportError} from '@shipfox/node-error-monitoring';
|
|
26
33
|
import {durationToSeconds} from '@shipfox/node-jwt';
|
|
27
34
|
import type {ShipfoxModule} from '@shipfox/node-module';
|
|
28
35
|
import {
|
|
29
36
|
createInMemoryInterModuleTransport,
|
|
30
37
|
registerInterModulePresentations,
|
|
31
38
|
} from '@shipfox/node-module/inter-module';
|
|
39
|
+
import {logger} from '@shipfox/node-opentelemetry';
|
|
32
40
|
|
|
33
41
|
export interface DefaultModulesOptions {
|
|
34
42
|
webhookDeliverySource?: WebhookDeliverySource | undefined;
|
|
43
|
+
runnersModule?: DefaultRunnersModuleFactory | undefined;
|
|
44
|
+
extension?: DefaultModulesExtension | undefined;
|
|
35
45
|
}
|
|
36
46
|
|
|
47
|
+
export type DefaultRunnersModuleFactory = (options: {auth: AuthInterModuleClient}) => ShipfoxModule;
|
|
48
|
+
export type DefaultModulesExtension = (options: {
|
|
49
|
+
workspaces: WorkspacesInterModuleClient;
|
|
50
|
+
}) => ShipfoxModule[];
|
|
51
|
+
|
|
37
52
|
export async function defaultModules(
|
|
38
53
|
options: DefaultModulesOptions = {},
|
|
39
54
|
): Promise<ShipfoxModule[]> {
|
|
40
|
-
const interModuleTransport = createInMemoryInterModuleTransport(
|
|
55
|
+
const interModuleTransport = createInMemoryInterModuleTransport({
|
|
56
|
+
reportInternalError: (error, context) => {
|
|
57
|
+
logger().error(
|
|
58
|
+
{err: error, module: context.module, method: context.method, phase: context.phase},
|
|
59
|
+
'Inter-module call failed unexpectedly',
|
|
60
|
+
);
|
|
61
|
+
reportError(error, {
|
|
62
|
+
boundary: 'inter-module',
|
|
63
|
+
operation: `${context.module}.${context.method}`,
|
|
64
|
+
tags: {module: context.module, method: context.method, phase: context.phase},
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
});
|
|
41
68
|
const workflowsClient = interModuleTransport.createClient(workflowsInterModuleContract);
|
|
42
69
|
const authClient = interModuleTransport.createClient(authInterModuleContract);
|
|
43
70
|
const agentClient = interModuleTransport.createClient(agentInterModuleContract);
|
|
@@ -131,8 +158,10 @@ export async function defaultModules(
|
|
|
131
158
|
const projectsModule = createProjectsModule({integrations: integrationsClient});
|
|
132
159
|
const definitionsModule = createDefinitionsModule({
|
|
133
160
|
projects: projectsClient,
|
|
161
|
+
agent: agentClient,
|
|
134
162
|
integrations: integrationsClient,
|
|
135
163
|
});
|
|
164
|
+
const extensionModules = options.extension?.({workspaces: workspacesClient}) ?? [];
|
|
136
165
|
|
|
137
166
|
const modules = [
|
|
138
167
|
emailChallengesModule,
|
|
@@ -154,13 +183,14 @@ export async function defaultModules(
|
|
|
154
183
|
integrations: integrationsClient,
|
|
155
184
|
}),
|
|
156
185
|
annotationsModule,
|
|
157
|
-
createRunnersModule({auth: authClient}),
|
|
186
|
+
(options.runnersModule ?? createRunnersModule)({auth: authClient}),
|
|
158
187
|
createLogsModule({
|
|
159
188
|
workflows: workflowsClient,
|
|
160
189
|
jobLeaseTokenTtlSeconds: durationToSeconds(authConfig.AUTH_JOB_LEASE_TOKEN_EXPIRES_IN),
|
|
161
190
|
}),
|
|
162
191
|
createTriggersModule({workflows: workflowsClient}),
|
|
163
192
|
dispatcherModule,
|
|
193
|
+
...extensionModules,
|
|
164
194
|
];
|
|
165
195
|
registerInterModulePresentations({transport: interModuleTransport, modules});
|
|
166
196
|
interModuleTransport.seal();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {createApp} from '@shipfox/node-fastify';
|
|
2
|
+
import type {LoginMethod} from '@shipfox/node-module';
|
|
3
|
+
import {createLoginMethodsRoute} from './login-methods.js';
|
|
4
|
+
|
|
5
|
+
describe('createLoginMethodsRoute', () => {
|
|
6
|
+
it('lists registered methods exactly once, including unknown future IDs', async () => {
|
|
7
|
+
const app = await createApp({
|
|
8
|
+
auth: [],
|
|
9
|
+
routes: [
|
|
10
|
+
createLoginMethodsRoute({
|
|
11
|
+
loginMethods: [{id: 'password'}, {id: 'oauth-google'}, {id: 'future-provider'}],
|
|
12
|
+
}),
|
|
13
|
+
],
|
|
14
|
+
swagger: false,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const response = await app.inject({method: 'GET', url: '/auth/login-methods'});
|
|
19
|
+
|
|
20
|
+
expect(response.statusCode).toBe(200);
|
|
21
|
+
expect(response.json()).toEqual({
|
|
22
|
+
login_methods: [{id: 'password'}, {id: 'oauth-google'}, {id: 'future-provider'}],
|
|
23
|
+
});
|
|
24
|
+
} finally {
|
|
25
|
+
await app.close();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('does not serialize module implementation details', async () => {
|
|
30
|
+
const loginMethod = {
|
|
31
|
+
id: 'oauth-google',
|
|
32
|
+
clientSecret: 'not-for-clients',
|
|
33
|
+
callbackUrl: 'https://api.example.test/auth/callback',
|
|
34
|
+
} as LoginMethod;
|
|
35
|
+
const app = await createApp({
|
|
36
|
+
auth: [],
|
|
37
|
+
routes: [createLoginMethodsRoute({loginMethods: [loginMethod]})],
|
|
38
|
+
swagger: false,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const response = await app.inject({method: 'GET', url: '/auth/login-methods'});
|
|
43
|
+
|
|
44
|
+
expect(response.statusCode).toBe(200);
|
|
45
|
+
expect(response.json()).toEqual({login_methods: [{id: 'oauth-google'}]});
|
|
46
|
+
expect(response.body).not.toContain('not-for-clients');
|
|
47
|
+
expect(response.body).not.toContain('api.example.test');
|
|
48
|
+
} finally {
|
|
49
|
+
await app.close();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('rejects an out-of-contract login method ID before the server can start', () => {
|
|
54
|
+
expect(() => createLoginMethodsRoute({loginMethods: [{id: ''}]})).toThrow();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {loginMethodsResponseSchema} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {defineRoute} from '@shipfox/node-fastify';
|
|
3
|
+
import type {LoginMethod} from '@shipfox/node-module';
|
|
4
|
+
|
|
5
|
+
export function createLoginMethodsRoute({loginMethods}: {loginMethods: readonly LoginMethod[]}) {
|
|
6
|
+
const response = loginMethodsResponseSchema.parse({
|
|
7
|
+
login_methods: loginMethods.map(({id}) => ({id})),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
return defineRoute({
|
|
11
|
+
method: 'GET',
|
|
12
|
+
path: '/auth/login-methods',
|
|
13
|
+
description: 'List the login methods available in this server composition.',
|
|
14
|
+
schema: {
|
|
15
|
+
response: {
|
|
16
|
+
200: loginMethodsResponseSchema,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
handler: () => response,
|
|
20
|
+
});
|
|
21
|
+
}
|
package/src/server.test.ts
CHANGED
|
@@ -12,11 +12,13 @@ const mocks = vi.hoisted(() => {
|
|
|
12
12
|
const workersHandle = {stop: vi.fn()};
|
|
13
13
|
const servicesHandle = {stop: vi.fn()};
|
|
14
14
|
return {
|
|
15
|
-
|
|
15
|
+
markErrorReported: vi.fn(),
|
|
16
|
+
reportError: vi.fn(),
|
|
16
17
|
closeApp: vi.fn(),
|
|
17
18
|
closeErrorMonitoring: vi.fn(),
|
|
18
19
|
closePostgresClient: vi.fn(),
|
|
19
20
|
createApp: vi.fn(),
|
|
21
|
+
defineRoute: vi.fn((route) => route),
|
|
20
22
|
createE2eAdminAuthMethod: vi.fn(),
|
|
21
23
|
createE2eRouteGroup: vi.fn(),
|
|
22
24
|
createPostgresClient: vi.fn(),
|
|
@@ -42,12 +44,14 @@ const mocks = vi.hoisted(() => {
|
|
|
42
44
|
});
|
|
43
45
|
|
|
44
46
|
vi.mock('@shipfox/node-error-monitoring', () => ({
|
|
45
|
-
captureException: mocks.captureException,
|
|
46
47
|
closeErrorMonitoring: mocks.closeErrorMonitoring,
|
|
48
|
+
markErrorReported: mocks.markErrorReported,
|
|
49
|
+
reportError: mocks.reportError,
|
|
47
50
|
}));
|
|
48
51
|
vi.mock('@shipfox/node-fastify', () => ({
|
|
49
52
|
closeApp: mocks.closeApp,
|
|
50
53
|
createApp: mocks.createApp,
|
|
54
|
+
defineRoute: mocks.defineRoute,
|
|
51
55
|
listen: mocks.listen,
|
|
52
56
|
}));
|
|
53
57
|
vi.mock('@shipfox/node-module', () => ({
|
|
@@ -113,11 +117,13 @@ function lastStartModuleServicesOptions(): {
|
|
|
113
117
|
|
|
114
118
|
function resetMocks(): void {
|
|
115
119
|
vi.useRealTimers();
|
|
116
|
-
mocks.
|
|
120
|
+
mocks.markErrorReported.mockReset();
|
|
121
|
+
mocks.reportError.mockReset();
|
|
117
122
|
mocks.closeApp.mockReset();
|
|
118
123
|
mocks.closeErrorMonitoring.mockReset();
|
|
119
124
|
mocks.closePostgresClient.mockReset();
|
|
120
125
|
mocks.createApp.mockReset();
|
|
126
|
+
mocks.defineRoute.mockReset();
|
|
121
127
|
mocks.createE2eAdminAuthMethod.mockReset();
|
|
122
128
|
mocks.createE2eRouteGroup.mockReset();
|
|
123
129
|
mocks.createPostgresClient.mockReset();
|
|
@@ -143,6 +149,7 @@ function resetMocks(): void {
|
|
|
143
149
|
mocks.closeErrorMonitoring.mockResolvedValue(true);
|
|
144
150
|
mocks.closePostgresClient.mockResolvedValue(undefined);
|
|
145
151
|
mocks.createApp.mockResolvedValue({});
|
|
152
|
+
mocks.defineRoute.mockImplementation((route) => route);
|
|
146
153
|
mocks.aggregateLoginMethods.mockReturnValue([{id: 'test-login'}]);
|
|
147
154
|
mocks.createE2eRouteGroup.mockReturnValue([]);
|
|
148
155
|
mocks.initializeModules.mockResolvedValue({
|
|
@@ -204,9 +211,16 @@ describe('createServer', () => {
|
|
|
204
211
|
});
|
|
205
212
|
expect(mocks.createApp).toHaveBeenCalledWith({
|
|
206
213
|
auth: [],
|
|
207
|
-
routes: [
|
|
214
|
+
routes: [
|
|
215
|
+
expect.objectContaining({
|
|
216
|
+
method: 'GET',
|
|
217
|
+
path: '/auth/login-methods',
|
|
218
|
+
}),
|
|
219
|
+
],
|
|
208
220
|
fastifyOptions: {trustProxy: false},
|
|
209
221
|
});
|
|
222
|
+
const appConfig = mocks.createApp.mock.calls.at(0)?.[0];
|
|
223
|
+
expect(appConfig?.routes[0].handler()).toEqual({login_methods: [{id: 'test-login'}]});
|
|
210
224
|
});
|
|
211
225
|
|
|
212
226
|
it('fails before startup side effects when no login method is configured', async () => {
|
|
@@ -492,7 +506,10 @@ describe('createServer', () => {
|
|
|
492
506
|
{err: failure, taskQueue: 'runtime-queue'},
|
|
493
507
|
'Module worker stopped unexpectedly',
|
|
494
508
|
);
|
|
495
|
-
expect(mocks.
|
|
509
|
+
expect(mocks.reportError).toHaveBeenCalledWith(failure, {
|
|
510
|
+
boundary: 'api.runtime',
|
|
511
|
+
tags: {taskQueue: 'runtime-queue'},
|
|
512
|
+
});
|
|
496
513
|
expect(mocks.closeApp).toHaveBeenCalledOnce();
|
|
497
514
|
expect(mocks.closeErrorMonitoring).toHaveBeenCalledWith(2_000);
|
|
498
515
|
expect(mocks.closeErrorMonitoring.mock.invocationCallOrder[0]).toBeLessThan(
|
|
@@ -513,7 +530,10 @@ describe('createServer', () => {
|
|
|
513
530
|
{err: failure, service: 'runtime-service'},
|
|
514
531
|
'Module service stopped unexpectedly',
|
|
515
532
|
);
|
|
516
|
-
expect(mocks.
|
|
533
|
+
expect(mocks.reportError).toHaveBeenCalledWith(failure, {
|
|
534
|
+
boundary: 'api.runtime',
|
|
535
|
+
tags: {service: 'runtime-service'},
|
|
536
|
+
});
|
|
517
537
|
expect(mocks.closeApp).toHaveBeenCalledOnce();
|
|
518
538
|
expect(mocks.closeErrorMonitoring).toHaveBeenCalledWith(2_000);
|
|
519
539
|
expect(mocks.closeErrorMonitoring.mock.invocationCallOrder[0]).toBeLessThan(
|