@shipfox/api-integration-core-dto 12.2.0 → 15.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-integration-core-dto",
3
3
  "license": "MIT",
4
- "version": "12.2.0",
4
+ "version": "15.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -23,7 +23,8 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "zod": "^4.4.3",
26
- "@shipfox/inter-module": "0.2.3"
26
+ "@shipfox/inter-module": "0.2.3",
27
+ "@shipfox/regex": "0.3.0"
27
28
  },
28
29
  "imports": {
29
30
  "#*": "./dist/*"
@@ -15,6 +15,7 @@ const validConnectionAvailable = {
15
15
  workspaceId: 'ws-1',
16
16
  connectionId: 'conn-1',
17
17
  slug: 'linear_shipfox',
18
+ capabilities: ['agent_tools'],
18
19
  };
19
20
 
20
21
  const validEventReceived = {
@@ -65,6 +66,51 @@ describe('integrationConnectionAvailableSchema', () => {
65
66
  );
66
67
  });
67
68
 
69
+ it('carries both capabilities for a source-control and tool provider', () => {
70
+ const input = {...validConnectionAvailable, capabilities: ['source_control', 'agent_tools']};
71
+
72
+ expect(integrationConnectionAvailableSchema.parse(input)).toEqual(input);
73
+ });
74
+
75
+ it('carries an empty capabilities array for a connection without adapters', () => {
76
+ const input = {...validConnectionAvailable, capabilities: []};
77
+
78
+ expect(integrationConnectionAvailableSchema.parse(input)).toEqual(input);
79
+ });
80
+
81
+ it('defaults capabilities for an event written before the field existed', () => {
82
+ const {capabilities: _capabilities, ...withoutCapabilities} = validConnectionAvailable;
83
+
84
+ expect(integrationConnectionAvailableSchema.parse(withoutCapabilities)).toEqual({
85
+ ...withoutCapabilities,
86
+ capabilities: [],
87
+ });
88
+ });
89
+
90
+ it('rejects unknown or empty capability values', () => {
91
+ expect(() =>
92
+ integrationConnectionAvailableSchema.parse({
93
+ ...validConnectionAvailable,
94
+ capabilities: [''],
95
+ }),
96
+ ).toThrow();
97
+ expect(() =>
98
+ integrationConnectionAvailableSchema.parse({
99
+ ...validConnectionAvailable,
100
+ capabilities: ['agent-tools'],
101
+ }),
102
+ ).toThrow();
103
+ });
104
+
105
+ it('rejects capability arrays above the contract bound', () => {
106
+ expect(() =>
107
+ integrationConnectionAvailableSchema.parse({
108
+ ...validConnectionAvailable,
109
+ capabilities: Array.from({length: 17}, () => 'agent_tools'),
110
+ }),
111
+ ).toThrow();
112
+ });
113
+
68
114
  it('rejects a payload without a connection slug', () => {
69
115
  const {slug: _slug, ...withoutSlug} = validConnectionAvailable;
70
116
 
package/src/events.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {z} from 'zod';
2
+ import {integrationCapabilitySchema} from './schemas/integrations.js';
2
3
 
3
4
  export const INTEGRATION_EVENT_RECEIVED = 'integrations.event.received' as const;
4
5
  export const INTEGRATION_CONNECTION_AVAILABLE = 'integrations.connection.available' as const;
@@ -28,6 +29,11 @@ export const integrationConnectionAvailableSchema = z.object({
28
29
  workspaceId: nonEmptyStringSchema,
29
30
  connectionId: nonEmptyStringSchema,
30
31
  slug: nonEmptyStringSchema,
32
+ // Provider registry capabilities, so subscribers can tell a tool connection
33
+ // from a source-control connection without their own provider table.
34
+ // Defaulting keeps availability events written before this field existed
35
+ // deliverable while every new event is still constrained to known capabilities.
36
+ capabilities: z.array(integrationCapabilitySchema).max(16).default([]),
31
37
  });
32
38
  export type IntegrationConnectionAvailableEvent = z.infer<
33
39
  typeof integrationConnectionAvailableSchema
@@ -65,6 +65,37 @@ describe('integrationsInterModuleContract', () => {
65
65
  expect(integrationsInterModuleContract.methods.resolveConnection.output.parse(null)).toBeNull();
66
66
  });
67
67
 
68
+ test('accepts a resolved source ref through the producer contract', () => {
69
+ const result = integrationsInterModuleContract.methods.resolveSourceRef.output.parse({
70
+ ref: 'refs/heads/fix-triage-prompt',
71
+ commit: 'a'.repeat(40),
72
+ });
73
+
74
+ expect(result).toEqual({ref: 'refs/heads/fix-triage-prompt', commit: 'a'.repeat(40)});
75
+ });
76
+
77
+ test('parses a ref-bearing input through the producer contract', () => {
78
+ const input = integrationsInterModuleContract.methods.resolveSourceRef.input.parse({
79
+ workspaceId: '00000000-0000-4000-8000-000000000001',
80
+ connectionId: '00000000-0000-4000-8000-000000000002',
81
+ externalRepositoryId: 'github:42',
82
+ ref: 'refs/heads/main',
83
+ });
84
+
85
+ expect(input.ref).toBe('refs/heads/main');
86
+ });
87
+
88
+ test('rejects control characters in a ref-bearing input', () => {
89
+ expect(() =>
90
+ integrationsInterModuleContract.methods.resolveSourceRef.input.parse({
91
+ workspaceId: '00000000-0000-4000-8000-000000000001',
92
+ connectionId: '00000000-0000-4000-8000-000000000002',
93
+ externalRepositoryId: 'github:42',
94
+ ref: 'refs/heads/unsafe\nname',
95
+ }),
96
+ ).toThrow();
97
+ });
98
+
68
99
  test.each([
69
100
  ['connection-not-found', {connectionId: '00000000-0000-4000-8000-000000000001'}],
70
101
  ['provider-unavailable', {provider: 'github'}],
@@ -77,4 +108,49 @@ describe('integrationsInterModuleContract', () => {
77
108
 
78
109
  expect(schema.parse(details)).toEqual(details);
79
110
  });
111
+
112
+ test.each([
113
+ ['ref-not-found', {ref: 'refs/heads/missing'}],
114
+ ['ref-invalid', {ref: 'a'.repeat(40)}],
115
+ ] as const)('defines the %s ref failure', (code, details) => {
116
+ const schema =
117
+ integrationsInterModuleContract.methods.resolveSourceRef.errors[
118
+ code as keyof typeof integrationsInterModuleContract.methods.resolveSourceRef.errors
119
+ ];
120
+
121
+ expect(schema.parse(details)).toEqual(details);
122
+ });
123
+
124
+ test('accepts provider event catalogs and fixed event providers on the validation context', () => {
125
+ const output = integrationsInterModuleContract.methods.getAgentToolsContext.output.parse({
126
+ selectionCatalogs: [],
127
+ catalogs: [],
128
+ workspaceConnections: [],
129
+ eventCatalogs: [
130
+ {provider: 'github', events: ['push']},
131
+ {provider: 'webhook', events: ['received']},
132
+ ],
133
+ fixedEventProviders: ['webhook'],
134
+ defaultConnection: null,
135
+ });
136
+
137
+ expect(output.eventCatalogs).toEqual([
138
+ {provider: 'github', events: ['push']},
139
+ {provider: 'webhook', events: ['received']},
140
+ ]);
141
+ expect(output.fixedEventProviders).toEqual(['webhook']);
142
+ });
143
+
144
+ test('rejects empty fixed event provider identifiers', () => {
145
+ expect(() =>
146
+ integrationsInterModuleContract.methods.getAgentToolsContext.output.parse({
147
+ selectionCatalogs: [],
148
+ catalogs: [],
149
+ workspaceConnections: [],
150
+ eventCatalogs: [],
151
+ fixedEventProviders: [''],
152
+ defaultConnection: null,
153
+ }),
154
+ ).toThrow();
155
+ });
80
156
  });
@@ -1,9 +1,11 @@
1
1
  import {defineInterModuleContract, type InterModuleClient} from '@shipfox/inter-module';
2
+ import {isSafeRefInput} from '@shipfox/regex';
2
3
  import {z} from 'zod';
3
4
 
4
5
  const id = z.string().uuid();
5
6
  const provider = z.string().min(1);
6
7
  const capability = z.enum(['source_control', 'agent_tools']);
8
+ const safeRef = z.string().refine(isSafeRefInput, 'Ref contains a control character');
7
9
  const connection = z.object({id, provider, slug: z.string().min(1)});
8
10
  const repository = z.object({
9
11
  externalRepositoryId: z.string(),
@@ -36,6 +38,12 @@ const sourceErrors = {
36
38
  'provider-failure': providerError,
37
39
  };
38
40
 
41
+ const refErrors = {
42
+ ...sourceErrors,
43
+ 'ref-not-found': z.object({ref: safeRef}),
44
+ 'ref-invalid': z.object({ref: safeRef}),
45
+ };
46
+
39
47
  /** Producer-owned synchronous operations for the Integrations bounded context. */
40
48
  export const integrationsInterModuleContract = defineInterModuleContract({
41
49
  module: 'integrations',
@@ -54,6 +62,11 @@ export const integrationsInterModuleContract = defineInterModuleContract({
54
62
  output: triggerReference.nullable(),
55
63
  errors: sourceErrors,
56
64
  },
65
+ resolveSourceRef: {
66
+ input: sourceInput.extend({ref: safeRef}),
67
+ output: z.object({ref: z.string(), commit: z.string()}),
68
+ errors: refErrors,
69
+ },
57
70
  listSourceFiles: {
58
71
  input: sourceInput.extend({
59
72
  ref: z.string(),
@@ -135,6 +148,8 @@ export const integrationsInterModuleContract = defineInterModuleContract({
135
148
  workspaceConnections: z.array(
136
149
  z.object({slug: z.string(), id, provider, capabilities: z.array(capability)}),
137
150
  ),
151
+ eventCatalogs: z.array(z.object({provider, events: z.array(z.string())})),
152
+ fixedEventProviders: z.array(provider),
138
153
  defaultConnection: z.object({id, slug: z.string(), provider}).nullable(),
139
154
  }),
140
155
  errors: sourceErrors,
@@ -25,6 +25,7 @@ export {
25
25
  listRepositoriesParamsSchema,
26
26
  listRepositoriesQuerySchema,
27
27
  listRepositoriesResponseSchema,
28
+ RESERVED_CONNECTION_SLUGS,
28
29
  type RepositoryDto,
29
30
  type RepositoryVisibilityDto,
30
31
  repositoryDtoSchema,
@@ -2,6 +2,13 @@ import {z} from 'zod';
2
2
 
3
3
  export const CONNECTION_SLUG_MAX_LENGTH = 100;
4
4
 
5
+ /**
6
+ * Slugs sync classifies by literal: a source equal to one of these names is a
7
+ * built-in trigger source, never a connection. No provider may allocate a
8
+ * connection holding one of them.
9
+ */
10
+ export const RESERVED_CONNECTION_SLUGS = ['manual', 'cron'] as const;
11
+
5
12
  export const connectionSlugSchema = z
6
13
  .string()
7
14
  .min(1)