@shipfox/api-server 7.1.0 → 9.0.1

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.1.0",
5
+ "version": "9.0.1",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -11,9 +11,6 @@
11
11
  "type": "module",
12
12
  "main": "dist/index.js",
13
13
  "types": "dist/index.d.ts",
14
- "imports": {
15
- "#*": "./dist/*"
16
- },
17
14
  "exports": {
18
15
  ".": {
19
16
  "types": "./dist/index.d.ts",
@@ -25,37 +22,40 @@
25
22
  }
26
23
  },
27
24
  "dependencies": {
28
- "@shipfox/annotations": "7.1.0",
29
- "@shipfox/api-agent": "7.1.0",
30
- "@shipfox/annotations-dto": "6.0.0",
31
- "@shipfox/api-agent-dto": "6.0.0",
32
- "@shipfox/api-auth": "7.1.0",
33
- "@shipfox/api-auth-dto": "7.1.0",
34
- "@shipfox/api-definitions-dto": "6.0.0",
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",
41
- "@shipfox/api-projects-dto": "6.0.0",
42
- "@shipfox/api-runners": "7.1.0",
43
- "@shipfox/api-runners-dto": "7.0.1",
44
- "@shipfox/api-secrets": "7.1.0",
45
- "@shipfox/api-integration-core-dto": "6.0.0",
46
- "@shipfox/api-workflows": "7.1.0",
47
- "@shipfox/api-secrets-dto": "6.0.0",
48
- "@shipfox/api-triggers": "7.1.0",
49
- "@shipfox/api-workspaces-dto": "6.0.0",
50
- "@shipfox/api-workflows-dto": "6.0.0",
51
- "@shipfox/api-workspaces": "7.1.0",
52
- "@shipfox/config": "1.2.2",
53
- "@shipfox/node-fastify": "0.3.0",
54
- "@shipfox/node-error-monitoring": "0.2.0",
55
- "@shipfox/node-jwt": "0.3.0",
56
- "@shipfox/node-postgres": "0.4.2",
57
- "@shipfox/node-module": "0.5.0",
58
- "@shipfox/node-opentelemetry": "0.6.0"
25
+ "@shipfox/annotations": "9.0.1",
26
+ "@shipfox/annotations-dto": "9.0.1",
27
+ "@shipfox/api-agent": "9.0.1",
28
+ "@shipfox/api-agent-dto": "9.0.1",
29
+ "@shipfox/api-auth": "9.0.1",
30
+ "@shipfox/api-auth-dto": "9.0.1",
31
+ "@shipfox/api-definitions": "9.0.1",
32
+ "@shipfox/api-definitions-dto": "9.0.1",
33
+ "@shipfox/api-dispatcher": "9.0.1",
34
+ "@shipfox/api-email-challenges": "1.1.0",
35
+ "@shipfox/api-integration-core": "9.0.1",
36
+ "@shipfox/api-integration-core-dto": "9.0.1",
37
+ "@shipfox/api-logs": "9.0.1",
38
+ "@shipfox/api-projects": "9.0.1",
39
+ "@shipfox/api-projects-dto": "9.0.1",
40
+ "@shipfox/api-runners": "9.0.1",
41
+ "@shipfox/api-runners-dto": "9.0.1",
42
+ "@shipfox/api-secrets": "9.0.1",
43
+ "@shipfox/api-secrets-dto": "9.0.1",
44
+ "@shipfox/api-triggers": "9.0.1",
45
+ "@shipfox/api-workflows": "9.0.1",
46
+ "@shipfox/api-workflows-dto": "9.0.1",
47
+ "@shipfox/api-workspaces-dto": "9.0.1",
48
+ "@shipfox/api-workspaces": "9.0.1",
49
+ "@shipfox/config": "1.2.3",
50
+ "@shipfox/node-error-monitoring": "0.2.1",
51
+ "@shipfox/node-fastify": "0.3.1",
52
+ "@shipfox/node-jwt": "0.3.1",
53
+ "@shipfox/node-module": "1.0.0",
54
+ "@shipfox/node-opentelemetry": "0.6.1",
55
+ "@shipfox/node-postgres": "0.4.3"
56
+ },
57
+ "imports": {
58
+ "#*": "./dist/*"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "shipfox-swc",
@@ -0,0 +1,54 @@
1
+ import {createLeaseTokenAuthMethod, issueJobLeaseToken} from '@shipfox/api-auth';
2
+ import {AUTH_LEASED_JOB, requireLeasedJobContext} from '@shipfox/api-auth-context';
3
+ import {closeApp, createApp, defineRoute} from '@shipfox/node-fastify';
4
+ import {afterEach, describe, expect, it} from '@shipfox/vitest/vi';
5
+
6
+ vi.hoisted(() => {
7
+ process.env.AUTH_ROOT_KEY ??= 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=';
8
+ });
9
+
10
+ const leaseRoute = defineRoute({
11
+ method: 'GET',
12
+ path: '/lease',
13
+ description: 'Auth composition test route.',
14
+ handler: (request) => {
15
+ const lease = requireLeasedJobContext(request);
16
+ return {job_id: lease.jobId, step_id: lease.currentStepId};
17
+ },
18
+ });
19
+
20
+ describe('Auth token interoperability', () => {
21
+ afterEach(async () => {
22
+ await closeApp();
23
+ });
24
+
25
+ it('accepts an Auth-issued step lease through the server route boundary', async () => {
26
+ const jobId = crypto.randomUUID();
27
+ const stepId = crypto.randomUUID();
28
+ const token = await issueJobLeaseToken({
29
+ jobId,
30
+ jobExecutionId: crypto.randomUUID(),
31
+ workflowRunId: crypto.randomUUID(),
32
+ workflowRunAttemptId: crypto.randomUUID(),
33
+ projectId: crypto.randomUUID(),
34
+ workspaceId: crypto.randomUUID(),
35
+ runnerSessionId: crypto.randomUUID(),
36
+ currentStepId: stepId,
37
+ currentStepAttempt: 1,
38
+ });
39
+ const app = await createApp({
40
+ auth: [createLeaseTokenAuthMethod()],
41
+ routes: [{prefix: '/runs/jobs/current', auth: AUTH_LEASED_JOB, routes: [leaseRoute]}],
42
+ swagger: false,
43
+ });
44
+
45
+ const res = await app.inject({
46
+ method: 'GET',
47
+ url: '/runs/jobs/current/lease',
48
+ headers: {authorization: `Bearer ${token}`},
49
+ });
50
+
51
+ expect(res.statusCode).toBe(200);
52
+ expect(res.json()).toEqual({job_id: jobId, step_id: stepId});
53
+ }, 15_000);
54
+ });
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export {
2
+ type DefaultModulesExtension,
2
3
  type DefaultModulesOptions,
3
4
  type DefaultRunnersModuleFactory,
4
5
  defaultModules,
@@ -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,8 @@ const mocks = vi.hoisted(() => ({
30
33
  deleteSecrets: vi.fn(),
31
34
  getIntegrationConnectionById: vi.fn(),
32
35
  getSecret: vi.fn(),
36
+ getWorkspaceCreator: vi.fn(),
37
+ listMembershipsForTokenClaims: vi.fn(),
33
38
  setSecrets: vi.fn(),
34
39
  }));
35
40
 
@@ -96,7 +101,8 @@ vi.mock('@shipfox/api-workspaces', () => ({
96
101
  {
97
102
  contract: workspacesInterModuleContract,
98
103
  handlers: {
99
- listMembershipsForTokenClaims: vi.fn(),
104
+ listMembershipsForTokenClaims: mocks.listMembershipsForTokenClaims,
105
+ getWorkspaceCreator: mocks.getWorkspaceCreator,
100
106
  preflightInvitationAcceptance: vi.fn(),
101
107
  acceptInvitation: vi.fn(),
102
108
  requireActiveMembership: vi.fn(),
@@ -123,6 +129,8 @@ describe('defaultModules', () => {
123
129
  mocks.deleteSecrets.mockReset();
124
130
  mocks.getIntegrationConnectionById.mockReset();
125
131
  mocks.getSecret.mockReset();
132
+ mocks.getWorkspaceCreator.mockReset();
133
+ mocks.listMembershipsForTokenClaims.mockReset();
126
134
  mocks.setSecrets.mockReset();
127
135
 
128
136
  mocks.createIntegrationsContext.mockResolvedValue({
@@ -147,6 +155,8 @@ describe('defaultModules', () => {
147
155
  mocks.createWorkspaceConnectionSnapshotLoader.mockReturnValue(vi.fn());
148
156
  mocks.deleteSecrets.mockResolvedValue({deleted: 1});
149
157
  mocks.getSecret.mockResolvedValue({value: 'secret'});
158
+ mocks.listMembershipsForTokenClaims.mockResolvedValue({memberships: []});
159
+ mocks.getWorkspaceCreator.mockResolvedValue({creatorUserId: null});
150
160
  mocks.setSecrets.mockResolvedValue({});
151
161
  mocks.createProjectsModule.mockReturnValue({
152
162
  name: 'projects',
@@ -182,7 +192,11 @@ describe('defaultModules', () => {
182
192
  interModulePresentations: [
183
193
  {
184
194
  contract: agentInterModuleContract,
185
- handlers: {resolveAgentConfig: vi.fn(), resolveRuntimeCredentials: vi.fn()},
195
+ handlers: {
196
+ getValidationCatalog: vi.fn(),
197
+ resolveAgentConfig: vi.fn(),
198
+ resolveRuntimeCredentials: vi.fn(),
199
+ },
186
200
  },
187
201
  ],
188
202
  });
@@ -248,6 +262,27 @@ describe('defaultModules', () => {
248
262
  ]);
249
263
  });
250
264
 
265
+ it('registers one presentation for every composed inter-module client before sealing', async () => {
266
+ const modules = await defaultModules();
267
+ const presentationModules = modules.flatMap((module) =>
268
+ (module.interModulePresentations ?? []).map(({contract}) => contract.module),
269
+ );
270
+ const composedClientModules = [
271
+ agentInterModuleContract,
272
+ annotationsInterModuleContract,
273
+ authInterModuleContract,
274
+ definitionsInterModuleContract,
275
+ integrationsInterModuleContract,
276
+ projectsInterModuleContract,
277
+ runnersInterModuleContract,
278
+ secretsInterModuleContract,
279
+ workflowsInterModuleContract,
280
+ workspacesInterModuleContract,
281
+ ].map((contract) => contract.module);
282
+
283
+ expect(presentationModules.sort()).toEqual(composedClientModules.sort());
284
+ });
285
+
251
286
  it('injects Auth into the Runners module', async () => {
252
287
  await defaultModules();
253
288
 
@@ -288,6 +323,34 @@ describe('defaultModules', () => {
288
323
  ]);
289
324
  });
290
325
 
326
+ it('extends the default module list with the composed Workspaces client', async () => {
327
+ let workspaces: WorkspacesInterModuleClient | undefined;
328
+ const extensionModule = {name: 'cloud'};
329
+ const extension = vi.fn((options: {workspaces: WorkspacesInterModuleClient}) => {
330
+ workspaces = options.workspaces;
331
+ return [extensionModule];
332
+ });
333
+
334
+ const modules = await defaultModules({extension});
335
+ const userId = crypto.randomUUID();
336
+ const memberships = await workspaces?.listMembershipsForTokenClaims({userId});
337
+ const workspaceId = crypto.randomUUID();
338
+ const creator = await workspaces?.getWorkspaceCreator({workspaceId});
339
+
340
+ expect(extension).toHaveBeenCalledWith({workspaces: expect.any(Object)});
341
+ expect(memberships).toEqual({memberships: []});
342
+ expect(creator).toEqual({creatorUserId: null});
343
+ expect(mocks.listMembershipsForTokenClaims).toHaveBeenCalledWith(
344
+ {userId},
345
+ expect.objectContaining({signal: expect.any(AbortSignal)}),
346
+ );
347
+ expect(mocks.getWorkspaceCreator).toHaveBeenCalledWith(
348
+ {workspaceId},
349
+ expect.objectContaining({signal: expect.any(AbortSignal)}),
350
+ );
351
+ expect(modules.at(-1)).toBe(extensionModule);
352
+ });
353
+
291
354
  it('injects Workflows into integrations and logs and namespaces provider secrets', async () => {
292
355
  await defaultModules();
293
356
 
package/src/modules.ts CHANGED
@@ -13,10 +13,10 @@ import {definitionsInterModuleContract} from '@shipfox/api-definitions-dto/inter
13
13
  import {dispatcherModule} from '@shipfox/api-dispatcher';
14
14
  import {emailChallengesModule} from '@shipfox/api-email-challenges';
15
15
  import {createIntegrationsContext, type WebhookDeliverySource} from '@shipfox/api-integration-core';
16
- import {integrationsInterModuleContract} from '@shipfox/api-integration-core-dto';
16
+ import {integrationsInterModuleContract} from '@shipfox/api-integration-core-dto/inter-module';
17
17
  import {createLogsModule} from '@shipfox/api-logs';
18
18
  import {createProjectsModule} from '@shipfox/api-projects';
19
- import {projectsInterModuleContract} from '@shipfox/api-projects-dto';
19
+ import {projectsInterModuleContract} from '@shipfox/api-projects-dto/inter-module';
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';
@@ -25,7 +25,10 @@ import {createTriggersModule} from '@shipfox/api-triggers';
25
25
  import {createWorkflowsModule} from '@shipfox/api-workflows';
26
26
  import {workflowsInterModuleContract} from '@shipfox/api-workflows-dto/inter-module';
27
27
  import {workspacesModule} from '@shipfox/api-workspaces';
28
- import {workspacesInterModuleContract} from '@shipfox/api-workspaces-dto/inter-module';
28
+ import {
29
+ type WorkspacesInterModuleClient,
30
+ workspacesInterModuleContract,
31
+ } from '@shipfox/api-workspaces-dto/inter-module';
29
32
  import {reportError} from '@shipfox/node-error-monitoring';
30
33
  import {durationToSeconds} from '@shipfox/node-jwt';
31
34
  import type {ShipfoxModule} from '@shipfox/node-module';
@@ -38,9 +41,13 @@ import {logger} from '@shipfox/node-opentelemetry';
38
41
  export interface DefaultModulesOptions {
39
42
  webhookDeliverySource?: WebhookDeliverySource | undefined;
40
43
  runnersModule?: DefaultRunnersModuleFactory | undefined;
44
+ extension?: DefaultModulesExtension | undefined;
41
45
  }
42
46
 
43
47
  export type DefaultRunnersModuleFactory = (options: {auth: AuthInterModuleClient}) => ShipfoxModule;
48
+ export type DefaultModulesExtension = (options: {
49
+ workspaces: WorkspacesInterModuleClient;
50
+ }) => ShipfoxModule[];
44
51
 
45
52
  export async function defaultModules(
46
53
  options: DefaultModulesOptions = {},
@@ -151,8 +158,10 @@ export async function defaultModules(
151
158
  const projectsModule = createProjectsModule({integrations: integrationsClient});
152
159
  const definitionsModule = createDefinitionsModule({
153
160
  projects: projectsClient,
161
+ agent: agentClient,
154
162
  integrations: integrationsClient,
155
163
  });
164
+ const extensionModules = options.extension?.({workspaces: workspacesClient}) ?? [];
156
165
 
157
166
  const modules = [
158
167
  emailChallengesModule,
@@ -181,6 +190,7 @@ export async function defaultModules(
181
190
  }),
182
191
  createTriggersModule({workflows: workflowsClient}),
183
192
  dispatcherModule,
193
+ ...extensionModules,
184
194
  ];
185
195
  registerInterModulePresentations({transport: interModuleTransport, modules});
186
196
  interModuleTransport.seal();
@@ -0,0 +1,61 @@
1
+ export function publishedTestEnvironment() {
2
+ // Use the active workspace's isolated Postgres instead of a fixed CI port, which
3
+ // could point at an unrelated local database when this contract runs in a worktree.
4
+ const postgresHost = process.env.POSTGRES_HOST ?? '127.0.0.1';
5
+ const postgresPort = process.env.POSTGRES_PORT ?? '5432';
6
+ const postgresUsername = process.env.POSTGRES_USERNAME ?? 'shipfox';
7
+ const postgresPassword = process.env.POSTGRES_PASSWORD ?? 'password';
8
+ const postgresDatabase = 'api_test';
9
+
10
+ return {
11
+ AUTH_ROOT_KEY: 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=',
12
+ DATABASE_URL: `postgres://${postgresUsername}:${postgresPassword}@${postgresHost}:${postgresPort}/${postgresDatabase}`,
13
+ GITEA_BASE_URL: 'https://gitea.example.com',
14
+ GITEA_SERVICE_TOKEN: 'external-consumer-token',
15
+ GITEA_SERVICE_USERNAME: 'shipfox-bot',
16
+ GITEA_WEBHOOK_SECRET: 'external-consumer-webhook-secret',
17
+ GITHUB_API_BASE_URL: 'https://api.github.com',
18
+ GITHUB_APP_CLIENT_ID: 'external-consumer-client-id',
19
+ GITHUB_APP_CLIENT_SECRET: 'external-consumer-client-secret',
20
+ GITHUB_APP_ID: '1',
21
+ GITHUB_APP_PRIVATE_KEY: 'external-consumer-private-key',
22
+ GITHUB_APP_SLUG: 'shipfox-external-consumer',
23
+ GITHUB_APP_USERNAME: 'shipfox-external-consumer',
24
+ GITHUB_APP_WEBHOOK_SECRET: 'external-consumer-webhook-secret',
25
+ GITHUB_INSTALL_STATE_SECRET: 'external-consumer-install-state-secret',
26
+ JIRA_OAUTH_CLIENT_ID: 'external-consumer-client-id',
27
+ JIRA_OAUTH_CLIENT_SECRET: 'external-consumer-client-secret',
28
+ JIRA_OAUTH_REDIRECT_URL: 'https://shipfox.example.com/integrations/jira/callback',
29
+ JIRA_WEBHOOK_BASE_URL: 'https://shipfox.example.com',
30
+ JIRA_WEBHOOK_SIGNING_SECRET: 'external-consumer-webhook-secret',
31
+ LINEAR_MCP_ENDPOINT: 'https://mcp.linear.app/mcp',
32
+ LINEAR_OAUTH_CLIENT_ID: 'external-consumer-client-id',
33
+ LINEAR_OAUTH_CLIENT_SECRET: 'external-consumer-client-secret',
34
+ LINEAR_OAUTH_REDIRECT_URL: 'https://shipfox.example.com/integrations/linear/callback',
35
+ LINEAR_WEBHOOK_SIGNING_SECRET: 'external-consumer-webhook-secret',
36
+ SLACK_API_BASE_URL: 'https://slack.example.com/api',
37
+ SLACK_OAUTH_CLIENT_ID: 'external-consumer-client-id',
38
+ SLACK_OAUTH_CLIENT_SECRET: 'external-consumer-client-secret',
39
+ SLACK_OAUTH_REDIRECT_URL: 'https://shipfox.example.com/integrations/slack/callback',
40
+ SLACK_SIGNING_SECRET: 'external-consumer-signing-secret',
41
+ LOG_STORAGE_S3_ACCESS_KEY_ID: 'external-consumer-access-key',
42
+ LOG_STORAGE_S3_BUCKET: 'shipfox-logs',
43
+ LOG_STORAGE_S3_ENDPOINT: 'http://127.0.0.1:3900',
44
+ LOG_STORAGE_S3_FORCE_PATH_STYLE: 'true',
45
+ LOG_STORAGE_S3_REGION: 'garage',
46
+ LOG_STORAGE_S3_SECRET_ACCESS_KEY: 'external-consumer-secret-key',
47
+ POSTGRES_DATABASE: postgresDatabase,
48
+ POSTGRES_HOST: postgresHost,
49
+ POSTGRES_MAX_CONNECTIONS: '5',
50
+ POSTGRES_PASSWORD: postgresPassword,
51
+ POSTGRES_PORT: postgresPort,
52
+ POSTGRES_USERNAME: postgresUsername,
53
+ SECRETS_ENCRYPTION_KEK: 'ZmVkY2JhOTg3NjU0MzIxMGZlZGNiYTk4NzY1NDMyMTA=',
54
+ SENTRY_APP_CLIENT_ID: 'external-consumer-client-id',
55
+ SENTRY_APP_CLIENT_SECRET: 'external-consumer-client-secret',
56
+ SENTRY_APP_SLUG: 'shipfox-external-consumer',
57
+ SENTRY_APP_VERIFY_INSTALL: 'true',
58
+ TEMPORAL_ADDRESS: '127.0.0.1:7233',
59
+ WORKSPACE_JWT_SECRET: 'external-consumer-workspace-secret',
60
+ };
61
+ }
@@ -0,0 +1,19 @@
1
+ import {createRunnersModule} from '@shipfox/api-runners';
2
+ import {createServer, defaultModules} from '@shipfox/api-server';
3
+
4
+ void createServer({
5
+ modules: [
6
+ ...(await defaultModules({
7
+ runnersModule: ({auth}) =>
8
+ createRunnersModule({
9
+ auth,
10
+ installationProvisioning: {
11
+ policy: {
12
+ filterEligibleWorkspaceIds: async (workspaceIds) => new Set(workspaceIds),
13
+ },
14
+ },
15
+ }),
16
+ })),
17
+ {name: 'external-dummy'},
18
+ ],
19
+ });
@@ -0,0 +1,3 @@
1
+ await import('@shipfox/api-server/instrumentation');
2
+
3
+ process.exit(0);
@@ -0,0 +1,22 @@
1
+ const {loginMethodsResponseSchema} = await import('@shipfox/api-auth-dto');
2
+ const {createLoginMethodsRoute} = await import('@shipfox/api-server');
3
+ const {createApp} = await import('@shipfox/node-fastify');
4
+
5
+ const app = await createApp({
6
+ auth: [],
7
+ routes: [createLoginMethodsRoute({loginMethods: [{id: 'external-provider'}]})],
8
+ swagger: false,
9
+ });
10
+
11
+ try {
12
+ const response = await app.inject({method: 'GET', url: '/auth/login-methods'});
13
+ if (response.statusCode !== 200) {
14
+ throw new Error('Packed login-method catalog route did not respond.');
15
+ }
16
+ const body = loginMethodsResponseSchema.parse(response.json());
17
+ if (body.login_methods[0]?.id !== 'external-provider') {
18
+ throw new Error('Packed login-method catalog route returned an unexpected contract.');
19
+ }
20
+ } finally {
21
+ await app.close();
22
+ }
@@ -0,0 +1,50 @@
1
+ process.env.INTEGRATIONS_ENABLE_SENTRY_PROVIDER = 'true';
2
+ process.env.NODE_ENV = 'production';
3
+
4
+ const {readdir, readFile} = await import('node:fs/promises');
5
+ const {dirname, join} = await import('node:path');
6
+ const {defaultModules} = await import('@shipfox/api-server');
7
+ const {loadProductionWorkflowBundle} = await import('@shipfox/node-temporal');
8
+ const modules = await defaultModules();
9
+ const workflowPaths = new Set(
10
+ modules.flatMap((module) => module.workers ?? []).map((worker) => worker.workflowsPath),
11
+ );
12
+
13
+ if (!workflowPaths.size) throw new Error('Packed API server declares no workflow entrypoints.');
14
+
15
+ const bundles = new Map();
16
+ for (const workflowsPath of workflowPaths) {
17
+ const workflowBundle = loadProductionWorkflowBundle(workflowsPath);
18
+ bundles.set(workflowsPath, workflowBundle);
19
+
20
+ const code = await readFile(workflowBundle.codePath, 'utf8');
21
+ if (/@shipfox[/\\][^/\\]+[/\\]src[/\\]/u.test(code)) {
22
+ throw new Error(`Workflow bundle for ${workflowsPath} resolved a first-party source path.`);
23
+ }
24
+ }
25
+
26
+ const declaredCodePaths = new Set([...bundles.values()].map(({codePath}) => codePath));
27
+ if (declaredCodePaths.size !== workflowPaths.size) {
28
+ throw new Error('Packed API workflow entrypoints do not map one-to-one to prebuilt bundles.');
29
+ }
30
+
31
+ const workflowDirectories = new Set(
32
+ [...workflowPaths].map((workflowsPath) => dirname(workflowsPath)),
33
+ );
34
+ const emittedBundles = (
35
+ await Promise.all(
36
+ [...workflowDirectories].map(async (directory) =>
37
+ (
38
+ await readdir(directory)
39
+ )
40
+ .filter((entry) => entry.endsWith('.bundle.js'))
41
+ .map((entry) => join(directory, entry)),
42
+ ),
43
+ )
44
+ ).flat();
45
+ const unreferencedBundles = emittedBundles.filter((codePath) => !declaredCodePaths.has(codePath));
46
+ if (unreferencedBundles.length) {
47
+ throw new Error(
48
+ `Packed API contains unreferenced workflow bundles: ${unreferencedBundles.join(', ')}`,
49
+ );
50
+ }