@shipfox/api-integration-core-dto 12.0.0 → 14.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.
@@ -6,14 +6,14 @@ export declare const WEBHOOK_MAX_HEADER_BYTES: number;
6
6
  export declare const WEBHOOK_MAX_RAW_QUERY_STRING_LENGTH: number;
7
7
  export declare const webhookRouteIds: readonly ["github", "gitea", "linear", "sentry", "slack.event", "slack.command", "webhook.connection", "jira"];
8
8
  export declare const webhookRouteIdSchema: z.ZodEnum<{
9
- github: "github";
10
9
  gitea: "gitea";
10
+ github: "github";
11
+ jira: "jira";
11
12
  linear: "linear";
12
13
  sentry: "sentry";
13
- "slack.event": "slack.event";
14
14
  "slack.command": "slack.command";
15
+ "slack.event": "slack.event";
15
16
  "webhook.connection": "webhook.connection";
16
- jira: "jira";
17
17
  }>;
18
18
  export type WebhookRouteId = z.infer<typeof webhookRouteIdSchema>;
19
19
  declare const webhookBodySchema: z.ZodObject<{
@@ -32,12 +32,12 @@ export declare const storedWebhookRequestSchema: z.ZodDiscriminatedUnion<[z.ZodO
32
32
  data: z.ZodString;
33
33
  }, z.core.$strict>;
34
34
  route_id: z.ZodEnum<{
35
- github: "github";
36
35
  gitea: "gitea";
36
+ github: "github";
37
37
  linear: "linear";
38
38
  sentry: "sentry";
39
- "slack.event": "slack.event";
40
39
  "slack.command": "slack.command";
40
+ "slack.event": "slack.event";
41
41
  }>;
42
42
  path_parameters: z.ZodObject<{}, z.core.$strict>;
43
43
  }, z.core.$strict>, z.ZodObject<{
@@ -82,12 +82,12 @@ export declare const webhookProcessingResultSchema: z.ZodDiscriminatedUnion<[z.Z
82
82
  }, z.core.$strict>, z.ZodObject<{
83
83
  outcome: z.ZodLiteral<"discarded">;
84
84
  reason: z.ZodEnum<{
85
- missing_required_input: "missing_required_input";
85
+ connection_unavailable: "connection_unavailable";
86
86
  invalid_signature: "invalid_signature";
87
- stale_at_receipt: "stale_at_receipt";
88
87
  malformed_payload: "malformed_payload";
88
+ missing_required_input: "missing_required_input";
89
+ stale_at_receipt: "stale_at_receipt";
89
90
  unsupported_event: "unsupported_event";
90
- connection_unavailable: "connection_unavailable";
91
91
  }>;
92
92
  deliveryId: z.ZodOptional<z.ZodString>;
93
93
  }, z.core.$strict>], "outcome">;
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.0.0",
4
+ "version": "14.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -1,13 +1,23 @@
1
1
  import {
2
+ INTEGRATION_CONNECTION_AVAILABLE,
2
3
  INTEGRATION_EVENT_RECEIVED,
3
4
  INTEGRATION_SOURCE_COMMIT_PUSHED,
4
5
  INTEGRATION_SOURCE_REPOSITORY_UPDATED,
6
+ integrationConnectionAvailableSchema,
5
7
  integrationEventReceivedSchema,
6
8
  integrationSourceCommitPushedSchema,
7
9
  integrationSourceRepositoryUpdatedSchema,
8
10
  integrationsEventSchemas,
9
11
  } from './events.js';
10
12
 
13
+ const validConnectionAvailable = {
14
+ provider: 'linear',
15
+ workspaceId: 'ws-1',
16
+ connectionId: 'conn-1',
17
+ slug: 'linear_shipfox',
18
+ capabilities: ['agent_tools'],
19
+ };
20
+
11
21
  const validEventReceived = {
12
22
  provider: 'github',
13
23
  source: 'github',
@@ -49,6 +59,65 @@ const validRepositoryUpdated = {
49
59
  },
50
60
  };
51
61
 
62
+ describe('integrationConnectionAvailableSchema', () => {
63
+ it('parses a valid connection-available payload unchanged', () => {
64
+ expect(integrationConnectionAvailableSchema.parse(validConnectionAvailable)).toEqual(
65
+ validConnectionAvailable,
66
+ );
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
+
114
+ it('rejects a payload without a connection slug', () => {
115
+ const {slug: _slug, ...withoutSlug} = validConnectionAvailable;
116
+
117
+ expect(() => integrationConnectionAvailableSchema.parse(withoutSlug)).toThrow();
118
+ });
119
+ });
120
+
52
121
  describe('integrationSourceCommitPushedSchema', () => {
53
122
  it('parses a valid commit-pushed payload unchanged', () => {
54
123
  const result = integrationSourceCommitPushedSchema.parse(validCommitPushed);
@@ -136,6 +205,7 @@ describe('integrationsEventSchemas', () => {
136
205
 
137
206
  expect(registeredTypes).toEqual(
138
207
  [
208
+ INTEGRATION_CONNECTION_AVAILABLE,
139
209
  INTEGRATION_EVENT_RECEIVED,
140
210
  INTEGRATION_SOURCE_COMMIT_PUSHED,
141
211
  INTEGRATION_SOURCE_REPOSITORY_UPDATED,
package/src/events.ts CHANGED
@@ -1,6 +1,8 @@
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;
5
+ export const INTEGRATION_CONNECTION_AVAILABLE = 'integrations.connection.available' as const;
4
6
 
5
7
  const nonEmptyStringSchema = z.string().nonempty();
6
8
  const isoDateTimeSchema = z.string().datetime();
@@ -22,6 +24,21 @@ export const integrationEventReceivedSchema = z.object({
22
24
  });
23
25
  export type IntegrationEventReceivedEvent = z.infer<typeof integrationEventReceivedSchema>;
24
26
 
27
+ export const integrationConnectionAvailableSchema = z.object({
28
+ provider: nonEmptyStringSchema,
29
+ workspaceId: nonEmptyStringSchema,
30
+ connectionId: nonEmptyStringSchema,
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([]),
37
+ });
38
+ export type IntegrationConnectionAvailableEvent = z.infer<
39
+ typeof integrationConnectionAvailableSchema
40
+ >;
41
+
25
42
  // A source-control push, normalized by the producing provider and carried by
26
43
  // `INTEGRATION_SOURCE_COMMIT_PUSHED` for domain consumers.
27
44
  export const sourcePushSchema = z.object({
@@ -107,12 +124,14 @@ export const SENTRY_ISSUE_ACTIONS = [
107
124
  export type SentryIssueAction = (typeof SENTRY_ISSUE_ACTIONS)[number];
108
125
 
109
126
  export interface IntegrationsEventMap {
127
+ [INTEGRATION_CONNECTION_AVAILABLE]: IntegrationConnectionAvailableEvent;
110
128
  [INTEGRATION_EVENT_RECEIVED]: IntegrationEventReceivedEvent;
111
129
  [INTEGRATION_SOURCE_COMMIT_PUSHED]: IntegrationSourceCommitPushedEvent;
112
130
  [INTEGRATION_SOURCE_REPOSITORY_UPDATED]: IntegrationSourceRepositoryUpdatedEvent;
113
131
  }
114
132
 
115
133
  export const integrationsEventSchemas = {
134
+ [INTEGRATION_CONNECTION_AVAILABLE]: integrationConnectionAvailableSchema,
116
135
  [INTEGRATION_EVENT_RECEIVED]: integrationEventReceivedSchema,
117
136
  [INTEGRATION_SOURCE_COMMIT_PUSHED]: integrationSourceCommitPushedSchema,
118
137
  [INTEGRATION_SOURCE_REPOSITORY_UPDATED]: integrationSourceRepositoryUpdatedSchema,
@@ -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
  });
@@ -4,6 +4,7 @@ import {z} from 'zod';
4
4
  const id = z.string().uuid();
5
5
  const provider = z.string().min(1);
6
6
  const capability = z.enum(['source_control', 'agent_tools']);
7
+ const safeRef = z.string().refine(isSafeRefInput, 'Ref contains a control character');
7
8
  const connection = z.object({id, provider, slug: z.string().min(1)});
8
9
  const repository = z.object({
9
10
  externalRepositoryId: z.string(),
@@ -36,6 +37,12 @@ const sourceErrors = {
36
37
  'provider-failure': providerError,
37
38
  };
38
39
 
40
+ const refErrors = {
41
+ ...sourceErrors,
42
+ 'ref-not-found': z.object({ref: safeRef}),
43
+ 'ref-invalid': z.object({ref: safeRef}),
44
+ };
45
+
39
46
  /** Producer-owned synchronous operations for the Integrations bounded context. */
40
47
  export const integrationsInterModuleContract = defineInterModuleContract({
41
48
  module: 'integrations',
@@ -54,6 +61,11 @@ export const integrationsInterModuleContract = defineInterModuleContract({
54
61
  output: triggerReference.nullable(),
55
62
  errors: sourceErrors,
56
63
  },
64
+ resolveSourceRef: {
65
+ input: sourceInput.extend({ref: safeRef}),
66
+ output: z.object({ref: z.string(), commit: z.string()}),
67
+ errors: refErrors,
68
+ },
57
69
  listSourceFiles: {
58
70
  input: sourceInput.extend({
59
71
  ref: z.string(),
@@ -135,6 +147,8 @@ export const integrationsInterModuleContract = defineInterModuleContract({
135
147
  workspaceConnections: z.array(
136
148
  z.object({slug: z.string(), id, provider, capabilities: z.array(capability)}),
137
149
  ),
150
+ eventCatalogs: z.array(z.object({provider, events: z.array(z.string())})),
151
+ fixedEventProviders: z.array(provider),
138
152
  defaultConnection: z.object({id, slug: z.string(), provider}).nullable(),
139
153
  }),
140
154
  errors: sourceErrors,
@@ -142,4 +156,11 @@ export const integrationsInterModuleContract = defineInterModuleContract({
142
156
  },
143
157
  });
144
158
 
159
+ function isSafeRefInput(value: string): boolean {
160
+ return [...value].every((character) => {
161
+ const code = character.codePointAt(0) ?? 0;
162
+ return !(code < 0x20 || (code >= 0x7f && code <= 0x9f) || code === 0x2028 || code === 0x2029);
163
+ });
164
+ }
165
+
145
166
  export type IntegrationsModuleClient = InterModuleClient<typeof integrationsInterModuleContract>;
@@ -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)