@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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +90 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +35 -35
- package/src/auth-token-interoperability.test.ts +5 -3
- package/src/modules.test.ts +32 -0
- package/test/published/_environment.mjs +61 -0
- package/test/published/composition.ts +19 -0
- package/test/published/development-conditions.development.mjs +3 -0
- package/test/published/login-methods-route.mjs +22 -0
- package/test/published/workflow-bundles.mjs +50 -0
- 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": "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": "
|
|
29
|
-
"@shipfox/annotations-dto": "
|
|
30
|
-
"@shipfox/api-agent": "
|
|
31
|
-
"@shipfox/api-agent-dto": "
|
|
32
|
-
"@shipfox/api-auth": "
|
|
33
|
-
"@shipfox/api-auth-dto": "
|
|
34
|
-
"@shipfox/api-definitions": "
|
|
35
|
-
"@shipfox/api-definitions-dto": "
|
|
36
|
-
"@shipfox/api-dispatcher": "
|
|
37
|
-
"@shipfox/api-email-challenges": "
|
|
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": "
|
|
44
|
-
"@shipfox/api-runners-dto": "
|
|
45
|
-
"@shipfox/api-secrets": "
|
|
46
|
-
"@shipfox/api-secrets-dto": "
|
|
47
|
-
"@shipfox/api-triggers": "
|
|
48
|
-
"@shipfox/api-workflows": "
|
|
49
|
-
"@shipfox/api-workflows-dto": "
|
|
50
|
-
"@shipfox/api-workspaces-dto": "
|
|
51
|
-
"@shipfox/api-workspaces": "
|
|
52
|
-
"@shipfox/config": "1.2.
|
|
53
|
-
"@shipfox/node-error-monitoring": "0.2.
|
|
54
|
-
"@shipfox/node-fastify": "0.3.
|
|
55
|
-
"@shipfox/node-jwt": "0.3.
|
|
56
|
-
"@shipfox/node-module": "0.
|
|
57
|
-
"@shipfox/node-opentelemetry": "0.6.
|
|
58
|
-
"@shipfox/node-postgres": "0.4.
|
|
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
|
-
|
|
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
|
});
|
package/src/modules.test.ts
CHANGED
|
@@ -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,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
|
+
}
|