@shipfox/api-triggers 10.2.0 → 11.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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-triggers",
3
3
  "license": "MIT",
4
- "version": "10.2.0",
4
+ "version": "11.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -22,23 +22,23 @@
22
22
  "cron-parser": "^5.6.1",
23
23
  "drizzle-orm": "^0.45.2",
24
24
  "zod": "^4.4.3",
25
- "@shipfox/api-definitions-dto": "10.0.0",
26
- "@shipfox/api-auth-context": "10.2.0",
25
+ "@shipfox/api-auth-context": "11.0.0",
26
+ "@shipfox/api-definitions-dto": "11.0.0",
27
27
  "@shipfox/api-integration-core-dto": "9.0.2",
28
- "@shipfox/api-workflows-dto": "10.0.0",
29
28
  "@shipfox/api-triggers-dto": "9.0.2",
29
+ "@shipfox/api-workflows-dto": "10.0.0",
30
30
  "@shipfox/config": "1.2.4",
31
- "@shipfox/expression": "1.2.0",
31
+ "@shipfox/expression": "1.2.1",
32
32
  "@shipfox/inter-module": "0.2.2",
33
+ "@shipfox/node-drizzle": "0.3.4",
33
34
  "@shipfox/node-error-monitoring": "0.3.0",
34
35
  "@shipfox/node-fastify": "0.4.0",
35
- "@shipfox/node-drizzle": "0.3.4",
36
36
  "@shipfox/node-module": "1.0.4",
37
- "@shipfox/node-postgres": "0.4.4",
38
37
  "@shipfox/node-opentelemetry": "0.6.3",
39
38
  "@shipfox/node-outbox": "0.2.6",
40
- "@shipfox/workflow-document": "2.1.3",
41
- "@shipfox/node-temporal": "0.4.4"
39
+ "@shipfox/node-postgres": "0.4.4",
40
+ "@shipfox/node-temporal": "0.4.4",
41
+ "@shipfox/workflow-document": "2.1.3"
42
42
  },
43
43
  "imports": {
44
44
  "#*": "./dist/*"
@@ -22,7 +22,11 @@ const workflows = {} as WorkflowsModuleClient;
22
22
  describe('POST /:definitionId/fire-manual', () => {
23
23
  let app: FastifyInstance;
24
24
  let workspaceId: string;
25
- let memberships: Array<{workspaceId: string; role: 'admin'}>;
25
+ let memberships: Array<{
26
+ workspaceId: string;
27
+ role: 'admin';
28
+ workspaceStatus: 'active' | 'suspended' | 'deleted';
29
+ }>;
26
30
 
27
31
  beforeAll(async () => {
28
32
  app = Fastify();
@@ -41,7 +45,7 @@ describe('POST /:definitionId/fire-manual', () => {
41
45
 
42
46
  beforeEach(() => {
43
47
  workspaceId = crypto.randomUUID();
44
- memberships = [{workspaceId, role: 'admin'}];
48
+ memberships = [{workspaceId, role: 'admin', workspaceStatus: 'active'}];
45
49
  fireManualSubscriptionMock.mockReset();
46
50
  });
47
51
 
@@ -89,6 +93,22 @@ describe('POST /:definitionId/fire-manual', () => {
89
93
  });
90
94
  });
91
95
 
96
+ test('returns workspace-suspended for a suspended membership claim', async () => {
97
+ const definitionId = crypto.randomUUID();
98
+ await triggerSubscriptionFactory.create({workspaceId, workflowDefinitionId: definitionId});
99
+ memberships = [{workspaceId, role: 'admin', workspaceStatus: 'suspended'}];
100
+
101
+ const res = await app.inject({
102
+ method: 'POST',
103
+ url: `/${definitionId}/fire-manual`,
104
+ payload: {},
105
+ });
106
+
107
+ expect(res.statusCode).toBe(409);
108
+ expect(res.json().code).toBe('workspace-suspended');
109
+ expect(fireManualSubscriptionMock).not.toHaveBeenCalled();
110
+ });
111
+
92
112
  test('maps suspended workspace to 409', async () => {
93
113
  const definitionId = crypto.randomUUID();
94
114
  await triggerSubscriptionFactory.create({workspaceId, workflowDefinitionId: definitionId});
@@ -1,4 +1,4 @@
1
- import {requireUserContext} from '@shipfox/api-auth-context';
1
+ import {requireUserContext, requireWorkspaceResourceAccess} from '@shipfox/api-auth-context';
2
2
  import {
3
3
  fireManualTriggerBodySchema,
4
4
  fireManualTriggerResponseSchema,
@@ -81,10 +81,18 @@ export function createFireManualTriggerRoute(workflows: WorkflowsModuleClient) {
81
81
  const userContext = requireUserContext(request);
82
82
 
83
83
  const subscription = await getManualSubscriptionByDefinitionId(definitionId);
84
- // 404 covers both "no such manual trigger" and "not your workspace" to avoid leaking existence.
85
- if (!subscription || !userContext.canAccess(subscription.workspaceId)) {
84
+ // Missing triggers and memberships remain 404 to avoid leaking existence; lifecycle claims
85
+ // propagate their stable access errors.
86
+ if (!subscription) {
86
87
  throw new ManualTriggerNotFoundError(definitionId);
87
88
  }
89
+ requireWorkspaceResourceAccess({
90
+ request,
91
+ workspaceId: subscription.workspaceId,
92
+ notFoundError: new ClientError('Manual trigger not found', 'manual-trigger-not-found', {
93
+ status: 404,
94
+ }),
95
+ });
88
96
 
89
97
  const run = await fireManualSubscription({
90
98
  workflows,
@@ -10,7 +10,11 @@ import {getTriggerEventRoute} from './get-trigger-event.js';
10
10
  describe('GET /trigger-events/:id', () => {
11
11
  let app: FastifyInstance;
12
12
  let workspaceId: string;
13
- let memberships: Array<{workspaceId: string; role: 'admin'}>;
13
+ let memberships: Array<{
14
+ workspaceId: string;
15
+ role: 'admin';
16
+ workspaceStatus: 'active' | 'suspended' | 'deleted';
17
+ }>;
14
18
 
15
19
  beforeAll(async () => {
16
20
  app = Fastify();
@@ -29,7 +33,7 @@ describe('GET /trigger-events/:id', () => {
29
33
 
30
34
  beforeEach(() => {
31
35
  workspaceId = crypto.randomUUID();
32
- memberships = [{workspaceId, role: 'admin'}];
36
+ memberships = [{workspaceId, role: 'admin', workspaceStatus: 'active'}];
33
37
  });
34
38
 
35
39
  test('returns the event with its decisions and full payload', async () => {
@@ -160,6 +164,16 @@ describe('GET /trigger-events/:id', () => {
160
164
  expect(res.json().decisions).toEqual([]);
161
165
  });
162
166
 
167
+ test('returns workspace-suspended for a suspended membership claim', async () => {
168
+ const event = await receivedEventFactory.create({workspaceId});
169
+ memberships = [{workspaceId, role: 'admin', workspaceStatus: 'suspended'}];
170
+
171
+ const res = await app.inject({method: 'GET', url: `/trigger-events/${event.id}`});
172
+
173
+ expect(res.statusCode).toBe(409);
174
+ expect(res.json().code).toBe('workspace-suspended');
175
+ });
176
+
163
177
  test('returns 404 for an unknown event id', async () => {
164
178
  const res = await app.inject({method: 'GET', url: `/trigger-events/${crypto.randomUUID()}`});
165
179
 
@@ -1,4 +1,4 @@
1
- import {requireUserContext} from '@shipfox/api-auth-context';
1
+ import {requireWorkspaceResourceAccess} from '@shipfox/api-auth-context';
2
2
  import {triggerEventDetailResponseSchema} from '@shipfox/api-triggers-dto';
3
3
  import {ClientError, defineRoute} from '@shipfox/node-fastify';
4
4
  import {z} from 'zod';
@@ -19,13 +19,18 @@ export const getTriggerEventRoute = defineRoute({
19
19
  },
20
20
  handler: async (request) => {
21
21
  const {id} = request.params;
22
- const userContext = requireUserContext(request);
23
22
 
24
23
  const event = await getTriggerEventById(id);
25
- // 404 covers both "no such event" and "not your workspace" to avoid leaking existence.
26
- if (!event || !userContext.canAccess(event.workspaceId)) {
24
+ // Missing events and memberships remain 404 to avoid leaking existence; lifecycle claims
25
+ // propagate their stable access errors.
26
+ if (!event) {
27
27
  throw new ClientError('Trigger event not found', 'not-found', {status: 404});
28
28
  }
29
+ requireWorkspaceResourceAccess({
30
+ request,
31
+ workspaceId: event.workspaceId,
32
+ notFoundError: new ClientError('Trigger event not found', 'not-found', {status: 404}),
33
+ });
29
34
 
30
35
  const decisions = await listDecisionsByReceivedEventId(event.id);
31
36
 
@@ -13,7 +13,11 @@ const facets = (res: {
13
13
  describe('GET /trigger-events/facets', () => {
14
14
  let app: FastifyInstance;
15
15
  let workspaceId: string;
16
- let memberships: Array<{workspaceId: string; role: 'admin'}>;
16
+ let memberships: Array<{
17
+ workspaceId: string;
18
+ role: 'admin';
19
+ workspaceStatus: 'active' | 'suspended' | 'deleted';
20
+ }>;
17
21
 
18
22
  beforeAll(async () => {
19
23
  app = Fastify();
@@ -32,7 +36,7 @@ describe('GET /trigger-events/facets', () => {
32
36
 
33
37
  beforeEach(() => {
34
38
  workspaceId = crypto.randomUUID();
35
- memberships = [{workspaceId, role: 'admin'}];
39
+ memberships = [{workspaceId, role: 'admin', workspaceStatus: 'active'}];
36
40
  });
37
41
 
38
42
  test('returns distinct sources and events with counts, ordered by count desc', async () => {
@@ -93,6 +97,18 @@ describe('GET /trigger-events/facets', () => {
93
97
  expect(res.json()).toEqual({sources: [], events: []});
94
98
  });
95
99
 
100
+ test('returns workspace-suspended for a suspended membership claim', async () => {
101
+ memberships = [{workspaceId, role: 'admin', workspaceStatus: 'suspended'}];
102
+
103
+ const res = await app.inject({
104
+ method: 'GET',
105
+ url: `/trigger-events/facets?workspace_id=${workspaceId}`,
106
+ });
107
+
108
+ expect(res.statusCode).toBe(409);
109
+ expect(res.json().code).toBe('workspace-suspended');
110
+ });
111
+
96
112
  test('denies a workspace the caller does not belong to', async () => {
97
113
  const otherWorkspaceId = crypto.randomUUID();
98
114
 
@@ -1,9 +1,9 @@
1
- import {requireUserContext} from '@shipfox/api-auth-context';
1
+ import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
2
2
  import {
3
3
  triggerEventFacetsQuerySchema,
4
4
  triggerEventFacetsResponseSchema,
5
5
  } from '@shipfox/api-triggers-dto';
6
- import {ClientError, defineRoute} from '@shipfox/node-fastify';
6
+ import {defineRoute} from '@shipfox/node-fastify';
7
7
  import {listTriggerEventFacets} from '#db/index.js';
8
8
 
9
9
  export const listTriggerEventFacetsRoute = defineRoute({
@@ -19,10 +19,7 @@ export const listTriggerEventFacetsRoute = defineRoute({
19
19
  handler: async (request) => {
20
20
  const {workspace_id: workspaceId} = request.query;
21
21
 
22
- const userContext = requireUserContext(request);
23
- if (!userContext.canAccess(workspaceId)) {
24
- throw new ClientError('Not a member of this workspace', 'forbidden', {status: 403});
25
- }
22
+ requireWorkspaceAccess({request, workspaceId});
26
23
 
27
24
  return await listTriggerEventFacets({workspaceId});
28
25
  },
@@ -12,7 +12,11 @@ const eventIds = (res: {json: () => {trigger_events: Array<{id: string}>}}) =>
12
12
  describe('GET /trigger-events', () => {
13
13
  let app: FastifyInstance;
14
14
  let workspaceId: string;
15
- let memberships: Array<{workspaceId: string; role: 'admin'}>;
15
+ let memberships: Array<{
16
+ workspaceId: string;
17
+ role: 'admin';
18
+ workspaceStatus: 'active' | 'suspended' | 'deleted';
19
+ }>;
16
20
 
17
21
  beforeAll(async () => {
18
22
  app = Fastify();
@@ -31,7 +35,7 @@ describe('GET /trigger-events', () => {
31
35
 
32
36
  beforeEach(() => {
33
37
  workspaceId = crypto.randomUUID();
34
- memberships = [{workspaceId, role: 'admin'}];
38
+ memberships = [{workspaceId, role: 'admin', workspaceStatus: 'active'}];
35
39
  });
36
40
 
37
41
  test('returns a workspace events newest first, isolated and payload-free', async () => {
@@ -318,6 +322,18 @@ describe('GET /trigger-events', () => {
318
322
  expect(res.json()).toEqual({trigger_events: [], next_cursor: null});
319
323
  });
320
324
 
325
+ test('returns workspace-suspended for a suspended membership claim', async () => {
326
+ memberships = [{workspaceId, role: 'admin', workspaceStatus: 'suspended'}];
327
+
328
+ const res = await app.inject({
329
+ method: 'GET',
330
+ url: `/trigger-events?workspace_id=${workspaceId}`,
331
+ });
332
+
333
+ expect(res.statusCode).toBe(409);
334
+ expect(res.json().code).toBe('workspace-suspended');
335
+ });
336
+
321
337
  test('denies a workspace the caller does not belong to', async () => {
322
338
  const otherWorkspaceId = crypto.randomUUID();
323
339
 
@@ -1,4 +1,4 @@
1
- import {requireUserContext} from '@shipfox/api-auth-context';
1
+ import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
2
2
  import {
3
3
  triggerEventListQuerySchema,
4
4
  triggerEventListResponseSchema,
@@ -30,10 +30,7 @@ export const listTriggerEventsRoute = defineRoute({
30
30
  cursor,
31
31
  } = request.query;
32
32
 
33
- const userContext = requireUserContext(request);
34
- if (!userContext.canAccess(workspaceId)) {
35
- throw new ClientError('Not a member of this workspace', 'forbidden', {status: 403});
36
- }
33
+ requireWorkspaceAccess({request, workspaceId});
37
34
 
38
35
  const decodedCursor = decodeTimestampIdCursor(cursor);
39
36
  if (cursor && !decodedCursor) {