@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/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.2",
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": "6.0.0",
28
+ "@shipfox/annotations": "8.0.0",
29
29
  "@shipfox/annotations-dto": "6.0.0",
30
- "@shipfox/api-agent": "6.0.0",
31
- "@shipfox/api-agent-dto": "6.0.0",
32
- "@shipfox/api-auth": "7.0.2",
33
- "@shipfox/api-auth-dto": "6.0.0",
34
- "@shipfox/api-definitions": "6.0.0",
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": "6.0.0",
37
- "@shipfox/api-email-challenges": "0.2.3",
38
- "@shipfox/api-integration-core": "7.0.1",
39
- "@shipfox/api-integration-core-dto": "6.0.0",
40
- "@shipfox/api-logs": "6.0.0",
41
- "@shipfox/api-projects": "6.0.0",
42
- "@shipfox/api-projects-dto": "6.0.0",
43
- "@shipfox/api-runners": "7.0.2",
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": "6.0.0",
45
+ "@shipfox/api-secrets": "8.0.0",
46
46
  "@shipfox/api-secrets-dto": "6.0.0",
47
- "@shipfox/api-triggers": "7.0.1",
48
- "@shipfox/api-workflows": "7.0.2",
49
- "@shipfox/api-workflows-dto": "6.0.0",
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.1",
51
+ "@shipfox/api-workspaces": "7.1.0",
52
52
  "@shipfox/config": "1.2.2",
53
- "@shipfox/node-error-monitoring": "0.1.3",
54
- "@shipfox/node-fastify": "0.2.4",
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-opentelemetry": "0.5.2",
57
- "@shipfox/node-postgres": "0.4.2",
58
- "@shipfox/node-module": "0.4.0"
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 {type DefaultModulesOptions, defaultModules} from './modules.js';
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';
@@ -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
- instrumentations: {fastify: true, http: true, pg: true},
10
+ serviceVersion: version,
11
+ instrumentations: {fastify: true, http: true, pg: true, awsSdk: true},
8
12
  });
@@ -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 {workspacesInterModuleContract} from '@shipfox/api-workspaces-dto/inter-module';
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: vi.fn(),
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: {resolveAgentConfig: vi.fn(), resolveRuntimeCredentials: vi.fn()},
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 {authInterModuleContract} from '@shipfox/api-auth-dto/inter-module';
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 {workspacesInterModuleContract} from '@shipfox/api-workspaces-dto/inter-module';
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
+ }
@@ -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
- captureException: vi.fn(),
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.captureException.mockReset();
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.captureException).toHaveBeenCalledWith(failure);
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.captureException).toHaveBeenCalledWith(failure);
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(