@shipfox/api-server 8.0.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": "8.0.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": "8.0.0",
29
- "@shipfox/annotations-dto": "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
- "@shipfox/api-definitions-dto": "6.0.0",
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
- "@shipfox/api-runners-dto": "7.0.1",
45
- "@shipfox/api-secrets": "8.0.0",
46
- "@shipfox/api-secrets-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
- "@shipfox/api-workspaces-dto": "6.0.0",
51
- "@shipfox/api-workspaces": "7.1.0",
52
- "@shipfox/config": "1.2.2",
53
- "@shipfox/node-error-monitoring": "0.2.0",
54
- "@shipfox/node-fastify": "0.3.0",
55
- "@shipfox/node-jwt": "0.3.0",
56
- "@shipfox/node-module": "0.5.0",
57
- "@shipfox/node-opentelemetry": "0.6.0",
58
- "@shipfox/node-postgres": "0.4.2"
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",
@@ -1,8 +1,11 @@
1
+ import {createLeaseTokenAuthMethod, issueJobLeaseToken} from '@shipfox/api-auth';
1
2
  import {AUTH_LEASED_JOB, requireLeasedJobContext} from '@shipfox/api-auth-context';
2
3
  import {closeApp, createApp, defineRoute} from '@shipfox/node-fastify';
3
4
  import {afterEach, describe, expect, it} from '@shipfox/vitest/vi';
4
5
 
5
- process.env.AUTH_ROOT_KEY ??= 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=';
6
+ vi.hoisted(() => {
7
+ process.env.AUTH_ROOT_KEY ??= 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=';
8
+ });
6
9
 
7
10
  const leaseRoute = defineRoute({
8
11
  method: 'GET',
@@ -20,7 +23,6 @@ describe('Auth token interoperability', () => {
20
23
  });
21
24
 
22
25
  it('accepts an Auth-issued step lease through the server route boundary', async () => {
23
- const {createLeaseTokenAuthMethod, issueJobLeaseToken} = await import('@shipfox/api-auth');
24
26
  const jobId = crypto.randomUUID();
25
27
  const stepId = crypto.randomUUID();
26
28
  const token = await issueJobLeaseToken({
@@ -48,5 +50,5 @@ describe('Auth token interoperability', () => {
48
50
 
49
51
  expect(res.statusCode).toBe(200);
50
52
  expect(res.json()).toEqual({job_id: jobId, step_id: stepId});
51
- });
53
+ }, 15_000);
52
54
  });
@@ -33,6 +33,7 @@ const mocks = vi.hoisted(() => ({
33
33
  deleteSecrets: vi.fn(),
34
34
  getIntegrationConnectionById: vi.fn(),
35
35
  getSecret: vi.fn(),
36
+ getWorkspaceCreator: vi.fn(),
36
37
  listMembershipsForTokenClaims: vi.fn(),
37
38
  setSecrets: vi.fn(),
38
39
  }));
@@ -101,6 +102,7 @@ vi.mock('@shipfox/api-workspaces', () => ({
101
102
  contract: workspacesInterModuleContract,
102
103
  handlers: {
103
104
  listMembershipsForTokenClaims: mocks.listMembershipsForTokenClaims,
105
+ getWorkspaceCreator: mocks.getWorkspaceCreator,
104
106
  preflightInvitationAcceptance: vi.fn(),
105
107
  acceptInvitation: vi.fn(),
106
108
  requireActiveMembership: vi.fn(),
@@ -127,6 +129,7 @@ describe('defaultModules', () => {
127
129
  mocks.deleteSecrets.mockReset();
128
130
  mocks.getIntegrationConnectionById.mockReset();
129
131
  mocks.getSecret.mockReset();
132
+ mocks.getWorkspaceCreator.mockReset();
130
133
  mocks.listMembershipsForTokenClaims.mockReset();
131
134
  mocks.setSecrets.mockReset();
132
135
 
@@ -153,6 +156,7 @@ describe('defaultModules', () => {
153
156
  mocks.deleteSecrets.mockResolvedValue({deleted: 1});
154
157
  mocks.getSecret.mockResolvedValue({value: 'secret'});
155
158
  mocks.listMembershipsForTokenClaims.mockResolvedValue({memberships: []});
159
+ mocks.getWorkspaceCreator.mockResolvedValue({creatorUserId: null});
156
160
  mocks.setSecrets.mockResolvedValue({});
157
161
  mocks.createProjectsModule.mockReturnValue({
158
162
  name: 'projects',
@@ -258,6 +262,27 @@ describe('defaultModules', () => {
258
262
  ]);
259
263
  });
260
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
+
261
286
  it('injects Auth into the Runners module', async () => {
262
287
  await defaultModules();
263
288
 
@@ -309,13 +334,20 @@ describe('defaultModules', () => {
309
334
  const modules = await defaultModules({extension});
310
335
  const userId = crypto.randomUUID();
311
336
  const memberships = await workspaces?.listMembershipsForTokenClaims({userId});
337
+ const workspaceId = crypto.randomUUID();
338
+ const creator = await workspaces?.getWorkspaceCreator({workspaceId});
312
339
 
313
340
  expect(extension).toHaveBeenCalledWith({workspaces: expect.any(Object)});
314
341
  expect(memberships).toEqual({memberships: []});
342
+ expect(creator).toEqual({creatorUserId: null});
315
343
  expect(mocks.listMembershipsForTokenClaims).toHaveBeenCalledWith(
316
344
  {userId},
317
345
  expect.objectContaining({signal: expect.any(AbortSignal)}),
318
346
  );
347
+ expect(mocks.getWorkspaceCreator).toHaveBeenCalledWith(
348
+ {workspaceId},
349
+ expect.objectContaining({signal: expect.any(AbortSignal)}),
350
+ );
319
351
  expect(modules.at(-1)).toBe(extensionModule);
320
352
  });
321
353
 
@@ -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
+ }