@shipfox/api-server 7.0.2 → 7.1.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 +36 -0
- package/README.md +2 -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 +5 -0
- package/dist/modules.d.ts.map +1 -1
- package/dist/modules.js +22 -2
- 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 +22 -22
- package/src/index.ts +6 -1
- package/src/instrumentation.ts +5 -1
- package/src/modules.test.ts +34 -0
- package/src/modules.ts +23 -3
- 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": "7.0
|
|
5
|
+
"version": "7.1.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": "7.1.0",
|
|
29
|
+
"@shipfox/api-agent": "7.1.0",
|
|
29
30
|
"@shipfox/annotations-dto": "6.0.0",
|
|
30
|
-
"@shipfox/api-agent": "6.0.0",
|
|
31
31
|
"@shipfox/api-agent-dto": "6.0.0",
|
|
32
|
-
"@shipfox/api-auth": "7.0
|
|
33
|
-
"@shipfox/api-auth-dto": "
|
|
34
|
-
"@shipfox/api-definitions": "6.0.0",
|
|
32
|
+
"@shipfox/api-auth": "7.1.0",
|
|
33
|
+
"@shipfox/api-auth-dto": "7.1.0",
|
|
35
34
|
"@shipfox/api-definitions-dto": "6.0.0",
|
|
36
|
-
"@shipfox/api-dispatcher": "
|
|
37
|
-
"@shipfox/api-email-challenges": "0.
|
|
38
|
-
"@shipfox/api-
|
|
39
|
-
"@shipfox/api-integration-core
|
|
40
|
-
"@shipfox/api-
|
|
41
|
-
"@shipfox/api-
|
|
35
|
+
"@shipfox/api-dispatcher": "7.1.0",
|
|
36
|
+
"@shipfox/api-email-challenges": "0.3.0",
|
|
37
|
+
"@shipfox/api-definitions": "7.1.0",
|
|
38
|
+
"@shipfox/api-integration-core": "7.1.0",
|
|
39
|
+
"@shipfox/api-projects": "7.1.0",
|
|
40
|
+
"@shipfox/api-logs": "7.1.0",
|
|
42
41
|
"@shipfox/api-projects-dto": "6.0.0",
|
|
43
|
-
"@shipfox/api-runners": "7.0
|
|
42
|
+
"@shipfox/api-runners": "7.1.0",
|
|
44
43
|
"@shipfox/api-runners-dto": "7.0.1",
|
|
45
|
-
"@shipfox/api-secrets": "
|
|
44
|
+
"@shipfox/api-secrets": "7.1.0",
|
|
45
|
+
"@shipfox/api-integration-core-dto": "6.0.0",
|
|
46
|
+
"@shipfox/api-workflows": "7.1.0",
|
|
46
47
|
"@shipfox/api-secrets-dto": "6.0.0",
|
|
47
|
-
"@shipfox/api-triggers": "7.0
|
|
48
|
-
"@shipfox/api-workflows": "7.0.2",
|
|
49
|
-
"@shipfox/api-workflows-dto": "6.0.0",
|
|
48
|
+
"@shipfox/api-triggers": "7.1.0",
|
|
50
49
|
"@shipfox/api-workspaces-dto": "6.0.0",
|
|
51
|
-
"@shipfox/api-
|
|
50
|
+
"@shipfox/api-workflows-dto": "6.0.0",
|
|
51
|
+
"@shipfox/api-workspaces": "7.1.0",
|
|
52
52
|
"@shipfox/config": "1.2.2",
|
|
53
|
-
"@shipfox/node-
|
|
54
|
-
"@shipfox/node-
|
|
53
|
+
"@shipfox/node-fastify": "0.3.0",
|
|
54
|
+
"@shipfox/node-error-monitoring": "0.2.0",
|
|
55
55
|
"@shipfox/node-jwt": "0.3.0",
|
|
56
|
-
"@shipfox/node-opentelemetry": "0.5.2",
|
|
57
56
|
"@shipfox/node-postgres": "0.4.2",
|
|
58
|
-
"@shipfox/node-module": "0.
|
|
57
|
+
"@shipfox/node-module": "0.5.0",
|
|
58
|
+
"@shipfox/node-opentelemetry": "0.6.0"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "shipfox-swc",
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
type DefaultModulesOptions,
|
|
3
|
+
type DefaultRunnersModuleFactory,
|
|
4
|
+
defaultModules,
|
|
5
|
+
} from './modules.js';
|
|
6
|
+
export {createLoginMethodsRoute} from './routes/login-methods.js';
|
|
2
7
|
export type {CreateServerOptions, RunServerOptions, ServerHandle} from './server.js';
|
|
3
8
|
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
|
@@ -254,6 +254,40 @@ describe('defaultModules', () => {
|
|
|
254
254
|
expect(mocks.createRunnersModule).toHaveBeenCalledWith({auth: expect.any(Object)});
|
|
255
255
|
});
|
|
256
256
|
|
|
257
|
+
it('lets a host replace only the Runners module with the composed Auth client', async () => {
|
|
258
|
+
const policy = {filterEligibleWorkspaceIds: vi.fn().mockResolvedValue(new Set<string>())};
|
|
259
|
+
const runnersModule = mocks.createRunnersModule({});
|
|
260
|
+
const createHostRunnersModule = vi.fn(({auth}) =>
|
|
261
|
+
mocks.createRunnersModule({auth, installationProvisioning: {policy}}),
|
|
262
|
+
);
|
|
263
|
+
mocks.createRunnersModule.mockClear();
|
|
264
|
+
|
|
265
|
+
const modules = await defaultModules({runnersModule: createHostRunnersModule});
|
|
266
|
+
|
|
267
|
+
expect(createHostRunnersModule).toHaveBeenCalledWith({auth: expect.any(Object)});
|
|
268
|
+
expect(mocks.createRunnersModule).toHaveBeenCalledWith({
|
|
269
|
+
auth: createHostRunnersModule.mock.calls[0]?.[0].auth,
|
|
270
|
+
installationProvisioning: {policy},
|
|
271
|
+
});
|
|
272
|
+
expect(modules.filter((module) => module.name === 'runners')).toEqual([runnersModule]);
|
|
273
|
+
expect(modules.map((module) => module.name)).toEqual([
|
|
274
|
+
'email-challenges',
|
|
275
|
+
'auth',
|
|
276
|
+
'workspaces',
|
|
277
|
+
'secrets',
|
|
278
|
+
'agent',
|
|
279
|
+
'integrations',
|
|
280
|
+
'projects',
|
|
281
|
+
'definitions',
|
|
282
|
+
'workflows',
|
|
283
|
+
'annotations',
|
|
284
|
+
'runners',
|
|
285
|
+
'logs',
|
|
286
|
+
'triggers',
|
|
287
|
+
'dispatcher',
|
|
288
|
+
]);
|
|
289
|
+
});
|
|
290
|
+
|
|
257
291
|
it('injects Workflows into integrations and logs and namespaces provider secrets', async () => {
|
|
258
292
|
await defaultModules();
|
|
259
293
|
|
package/src/modules.ts
CHANGED
|
@@ -4,7 +4,10 @@ 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';
|
|
@@ -23,21 +26,38 @@ 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
28
|
import {workspacesInterModuleContract} from '@shipfox/api-workspaces-dto/inter-module';
|
|
29
|
+
import {reportError} from '@shipfox/node-error-monitoring';
|
|
26
30
|
import {durationToSeconds} from '@shipfox/node-jwt';
|
|
27
31
|
import type {ShipfoxModule} from '@shipfox/node-module';
|
|
28
32
|
import {
|
|
29
33
|
createInMemoryInterModuleTransport,
|
|
30
34
|
registerInterModulePresentations,
|
|
31
35
|
} from '@shipfox/node-module/inter-module';
|
|
36
|
+
import {logger} from '@shipfox/node-opentelemetry';
|
|
32
37
|
|
|
33
38
|
export interface DefaultModulesOptions {
|
|
34
39
|
webhookDeliverySource?: WebhookDeliverySource | undefined;
|
|
40
|
+
runnersModule?: DefaultRunnersModuleFactory | undefined;
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
export type DefaultRunnersModuleFactory = (options: {auth: AuthInterModuleClient}) => ShipfoxModule;
|
|
44
|
+
|
|
37
45
|
export async function defaultModules(
|
|
38
46
|
options: DefaultModulesOptions = {},
|
|
39
47
|
): Promise<ShipfoxModule[]> {
|
|
40
|
-
const interModuleTransport = createInMemoryInterModuleTransport(
|
|
48
|
+
const interModuleTransport = createInMemoryInterModuleTransport({
|
|
49
|
+
reportInternalError: (error, context) => {
|
|
50
|
+
logger().error(
|
|
51
|
+
{err: error, module: context.module, method: context.method, phase: context.phase},
|
|
52
|
+
'Inter-module call failed unexpectedly',
|
|
53
|
+
);
|
|
54
|
+
reportError(error, {
|
|
55
|
+
boundary: 'inter-module',
|
|
56
|
+
operation: `${context.module}.${context.method}`,
|
|
57
|
+
tags: {module: context.module, method: context.method, phase: context.phase},
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
});
|
|
41
61
|
const workflowsClient = interModuleTransport.createClient(workflowsInterModuleContract);
|
|
42
62
|
const authClient = interModuleTransport.createClient(authInterModuleContract);
|
|
43
63
|
const agentClient = interModuleTransport.createClient(agentInterModuleContract);
|
|
@@ -154,7 +174,7 @@ export async function defaultModules(
|
|
|
154
174
|
integrations: integrationsClient,
|
|
155
175
|
}),
|
|
156
176
|
annotationsModule,
|
|
157
|
-
createRunnersModule({auth: authClient}),
|
|
177
|
+
(options.runnersModule ?? createRunnersModule)({auth: authClient}),
|
|
158
178
|
createLogsModule({
|
|
159
179
|
workflows: workflowsClient,
|
|
160
180
|
jobLeaseTokenTtlSeconds: durationToSeconds(authConfig.AUTH_JOB_LEASE_TOKEN_EXPIRES_IN),
|
|
@@ -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(
|
package/src/server.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {closeErrorMonitoring, markErrorReported, reportError} from '@shipfox/node-error-monitoring';
|
|
2
2
|
import {closeApp, createApp, listen} from '@shipfox/node-fastify';
|
|
3
3
|
import {
|
|
4
4
|
aggregateLoginMethods,
|
|
@@ -17,6 +17,7 @@ import {logger, shutdownServiceMetrics, startServiceMetrics} from '@shipfox/node
|
|
|
17
17
|
import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres';
|
|
18
18
|
import {config, parseApiTrustProxy} from './config.js';
|
|
19
19
|
import {createE2eAdminAuthMethod, createE2eRouteGroup} from './e2e.js';
|
|
20
|
+
import {createLoginMethodsRoute} from './routes/login-methods.js';
|
|
20
21
|
|
|
21
22
|
const RUNTIME_FAILURE_HTTP_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
22
23
|
const ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS = 2_000;
|
|
@@ -46,7 +47,7 @@ export async function createServer(options: CreateServerOptions): Promise<Server
|
|
|
46
47
|
hasActiveServer = true;
|
|
47
48
|
|
|
48
49
|
try {
|
|
49
|
-
aggregateLoginMethods({modules: options.modules});
|
|
50
|
+
const loginMethods = aggregateLoginMethods({modules: options.modules});
|
|
50
51
|
startServiceMetrics({serviceName: 'api'});
|
|
51
52
|
createPostgresClient();
|
|
52
53
|
|
|
@@ -62,7 +63,7 @@ export async function createServer(options: CreateServerOptions): Promise<Server
|
|
|
62
63
|
logger().info('Creating HTTP server');
|
|
63
64
|
await createApp({
|
|
64
65
|
auth: [...auth, ...e2eAuth],
|
|
65
|
-
routes: [...routes, ...mountedE2eRoutes],
|
|
66
|
+
routes: [createLoginMethodsRoute({loginMethods}), ...routes, ...mountedE2eRoutes],
|
|
66
67
|
fastifyOptions: {trustProxy: parseApiTrustProxy(config.API_TRUST_PROXY)},
|
|
67
68
|
});
|
|
68
69
|
|
|
@@ -116,10 +117,24 @@ export async function createServer(options: CreateServerOptions): Promise<Server
|
|
|
116
117
|
() => workersHandle?.stop(),
|
|
117
118
|
() => shutdownServiceMetrics(),
|
|
118
119
|
() => closePostgresClient(),
|
|
119
|
-
async () => {
|
|
120
|
-
await closeErrorMonitoring(ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS);
|
|
121
|
-
},
|
|
122
120
|
]);
|
|
121
|
+
for (const cleanupError of cleanupErrors) {
|
|
122
|
+
logger().error({err: cleanupError}, 'Failed to clean up API server during shutdown');
|
|
123
|
+
reportError(cleanupError, {boundary: 'api.shutdown', operation: 'cleanup'});
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const errorMonitoringClosed = await closeErrorMonitoring(
|
|
127
|
+
ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS,
|
|
128
|
+
);
|
|
129
|
+
if (!errorMonitoringClosed) {
|
|
130
|
+
logger().error('Timed out closing error monitoring during API shutdown');
|
|
131
|
+
cleanupErrors.push(
|
|
132
|
+
new Error('Timed out closing error monitoring during API shutdown'),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
cleanupErrors.push(error);
|
|
137
|
+
}
|
|
123
138
|
throwCleanupErrors(cleanupErrors, 'Failed to stop API server');
|
|
124
139
|
stopped = true;
|
|
125
140
|
hasActiveServer = false;
|
|
@@ -138,6 +153,7 @@ export async function createServer(options: CreateServerOptions): Promise<Server
|
|
|
138
153
|
]);
|
|
139
154
|
for (const cleanupError of cleanupErrors) {
|
|
140
155
|
logger().error({err: cleanupError, bootError: error}, 'Failed to clean up API server boot');
|
|
156
|
+
reportError(cleanupError, {boundary: 'api.startup', operation: 'cleanup'});
|
|
141
157
|
}
|
|
142
158
|
hasActiveServer = false;
|
|
143
159
|
throw error;
|
|
@@ -168,11 +184,23 @@ export async function runServer(options: RunServerOptions): Promise<ServerHandle
|
|
|
168
184
|
{err: cleanupError, startError: error},
|
|
169
185
|
'Failed to clean up API server startup',
|
|
170
186
|
);
|
|
187
|
+
reportError(cleanupError, {
|
|
188
|
+
boundary: 'api.startup',
|
|
189
|
+
operation: 'cleanup-after-failed-start',
|
|
190
|
+
});
|
|
171
191
|
}
|
|
172
192
|
throw error;
|
|
173
193
|
}
|
|
174
194
|
|
|
175
|
-
const stopAndExit = () =>
|
|
195
|
+
const stopAndExit = () =>
|
|
196
|
+
void handle.stop().then(
|
|
197
|
+
() => process.exit(0),
|
|
198
|
+
(error) => {
|
|
199
|
+
logger().error({err: error}, 'Failed to stop API server after shutdown signal');
|
|
200
|
+
reportError(error, {boundary: 'api.shutdown', operation: 'signal-stop'});
|
|
201
|
+
process.exit(1);
|
|
202
|
+
},
|
|
203
|
+
);
|
|
176
204
|
process.once('SIGTERM', stopAndExit);
|
|
177
205
|
process.once('SIGINT', stopAndExit);
|
|
178
206
|
|
|
@@ -210,7 +238,9 @@ async function runCleanupSteps(steps: CleanupStep[]): Promise<unknown[]> {
|
|
|
210
238
|
function throwCleanupErrors(cleanupErrors: unknown[], message: string): void {
|
|
211
239
|
if (cleanupErrors.length === 0) return;
|
|
212
240
|
if (cleanupErrors.length === 1) throw cleanupErrors[0];
|
|
213
|
-
|
|
241
|
+
const aggregate = new AggregateError(cleanupErrors, message);
|
|
242
|
+
markErrorReported(aggregate);
|
|
243
|
+
throw aggregate;
|
|
214
244
|
}
|
|
215
245
|
|
|
216
246
|
async function handleModuleWorkerFailure(
|
|
@@ -246,11 +276,19 @@ async function handleModuleRuntimeFailure(options: {
|
|
|
246
276
|
onFailure(): void | Promise<void>;
|
|
247
277
|
}): Promise<void> {
|
|
248
278
|
logger().error({err: options.error, ...options.fields}, options.message);
|
|
249
|
-
|
|
279
|
+
reportError(options.error, {
|
|
280
|
+
boundary: 'api.runtime',
|
|
281
|
+
tags: options.fields,
|
|
282
|
+
});
|
|
250
283
|
|
|
251
284
|
try {
|
|
252
285
|
await closeHttpServerAfterRuntimeFailure();
|
|
253
|
-
await closeErrorMonitoring(ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS);
|
|
286
|
+
const errorMonitoringClosed = await closeErrorMonitoring(ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS);
|
|
287
|
+
if (!errorMonitoringClosed) {
|
|
288
|
+
logger().error('Timed out closing error monitoring after module runtime failure');
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
logger().error({err: error}, 'Failed to close error monitoring after module runtime failure');
|
|
254
292
|
} finally {
|
|
255
293
|
await options.onFailure();
|
|
256
294
|
}
|
|
@@ -270,13 +308,13 @@ async function closeHttpServerAfterRuntimeFailure(): Promise<void> {
|
|
|
270
308
|
},
|
|
271
309
|
);
|
|
272
310
|
if (result === 'timeout') {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
);
|
|
311
|
+
const error = new Error('Timed out closing HTTP server after module runtime failure');
|
|
312
|
+
logger().error({timeoutMs: RUNTIME_FAILURE_HTTP_SHUTDOWN_TIMEOUT_MS}, error.message);
|
|
313
|
+
reportError(error, {boundary: 'api.runtime', operation: 'close-http-timeout'});
|
|
277
314
|
}
|
|
278
315
|
} catch (error) {
|
|
279
316
|
logger().error({err: error}, 'Failed to close HTTP server after module runtime failure');
|
|
317
|
+
reportError(error, {boundary: 'api.runtime', operation: 'close-http'});
|
|
280
318
|
} finally {
|
|
281
319
|
if (timeoutId) {
|
|
282
320
|
clearTimeout(timeoutId);
|