@shipfox/api-integration-core-dto 2.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/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +70 -0
- package/LICENSE +21 -0
- package/dist/contracts/index.d.ts +3 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +3 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/integrations.d.ts +191 -0
- package/dist/contracts/integrations.d.ts.map +1 -0
- package/dist/contracts/integrations.js +44 -0
- package/dist/contracts/integrations.js.map +1 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +87 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +56 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/ports.d.ts +59 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +3 -0
- package/dist/ports.js.map +1 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +3 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/integrations.d.ts +162 -0
- package/dist/schemas/integrations.d.ts.map +1 -0
- package/dist/schemas/integrations.js +82 -0
- package/dist/schemas/integrations.js.map +1 -0
- package/dist/slug.d.ts +7 -0
- package/dist/slug.d.ts.map +1 -0
- package/dist/slug.js +14 -0
- package/dist/slug.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/package.json +55 -0
- package/src/contracts/index.ts +43 -0
- package/src/contracts/integrations.test.ts +64 -0
- package/src/contracts/integrations.ts +312 -0
- package/src/errors.ts +6 -0
- package/src/events.test.ts +109 -0
- package/src/events.ts +91 -0
- package/src/index.ts +6 -0
- package/src/ports.ts +73 -0
- package/src/schemas/index.ts +35 -0
- package/src/schemas/integrations.ts +109 -0
- package/src/slug.test.ts +32 -0
- package/src/slug.ts +26 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +3 -0
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shipfox/api-integration-core-dto",
|
|
3
|
+
"license": "MIT",
|
|
4
|
+
"version": "2.0.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
8
|
+
"directory": "libs/api/integration/core-dto"
|
|
9
|
+
},
|
|
10
|
+
"private": false,
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"imports": {
|
|
15
|
+
"#test/*": "./test/*",
|
|
16
|
+
"#*": {
|
|
17
|
+
"workspace-source": "./src/*",
|
|
18
|
+
"development": "./src/*",
|
|
19
|
+
"default": "./dist/*"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"development": {
|
|
25
|
+
"types": "./src/index.ts",
|
|
26
|
+
"default": "./src/index.ts"
|
|
27
|
+
},
|
|
28
|
+
"default": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"zod": "^4.4.3"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@shipfox/biome": "1.8.1",
|
|
39
|
+
"@shipfox/depcruise": "1.0.1",
|
|
40
|
+
"@shipfox/swc": "1.2.5",
|
|
41
|
+
"@shipfox/typescript": "1.1.6",
|
|
42
|
+
"@shipfox/vitest": "1.2.2",
|
|
43
|
+
"@shipfox/ts-config": "1.3.8"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "shipfox-swc",
|
|
47
|
+
"check": "shipfox-biome-check",
|
|
48
|
+
"check:fix": "shipfox-biome-check --write",
|
|
49
|
+
"depcruise": "shipfox-depcruise",
|
|
50
|
+
"test": "shipfox-vitest-run",
|
|
51
|
+
"test:watch": "shipfox-vitest-watch",
|
|
52
|
+
"type": "shipfox-tsc-check",
|
|
53
|
+
"type:emit": "shipfox-tsc-emit"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
AgentToolCallInput,
|
|
3
|
+
AgentToolCatalogEntry,
|
|
4
|
+
AgentToolCatalogMethod,
|
|
5
|
+
AgentToolJsonSchema,
|
|
6
|
+
AgentToolSelectionCatalog,
|
|
7
|
+
AgentToolSelector,
|
|
8
|
+
AgentToolSelectorKind,
|
|
9
|
+
AgentToolSensitivity,
|
|
10
|
+
AgentToolSession,
|
|
11
|
+
AgentToolsProvider,
|
|
12
|
+
CheckoutCredentials,
|
|
13
|
+
CheckoutPermissions,
|
|
14
|
+
CheckoutSpec,
|
|
15
|
+
CreateCheckoutSpecInput,
|
|
16
|
+
FetchFileInput,
|
|
17
|
+
FileEntry,
|
|
18
|
+
FilePage,
|
|
19
|
+
FileSnapshot,
|
|
20
|
+
IntegrationCapability,
|
|
21
|
+
IntegrationConnection,
|
|
22
|
+
IntegrationConnectionLifecycleStatus,
|
|
23
|
+
IntegrationProvider,
|
|
24
|
+
IntegrationProviderAdapters,
|
|
25
|
+
IntegrationProviderErrorReason,
|
|
26
|
+
IntegrationProviderKind,
|
|
27
|
+
ListFilesInput,
|
|
28
|
+
ListRepositoriesInput,
|
|
29
|
+
OpenAgentToolsSessionInput,
|
|
30
|
+
RegisteredIntegrationProvider,
|
|
31
|
+
RepositoryPage,
|
|
32
|
+
RepositorySnapshot,
|
|
33
|
+
RepositoryVisibility,
|
|
34
|
+
ResolveRepositoryInput,
|
|
35
|
+
SourceControlProvider,
|
|
36
|
+
} from './integrations.js';
|
|
37
|
+
export {
|
|
38
|
+
buildProviderRepositoryId,
|
|
39
|
+
IntegrationProviderError,
|
|
40
|
+
MAX_REPOSITORY_FILE_BYTES,
|
|
41
|
+
parseProviderRepositoryId,
|
|
42
|
+
toIntegrationConnectionDto,
|
|
43
|
+
} from './integrations.js';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildProviderRepositoryId,
|
|
3
|
+
IntegrationProviderError,
|
|
4
|
+
parseProviderRepositoryId,
|
|
5
|
+
} from './integrations.js';
|
|
6
|
+
|
|
7
|
+
const MISSING_PREFIX = /missing a provider prefix/;
|
|
8
|
+
const WRONG_PROVIDER = /not owned by provider github/;
|
|
9
|
+
const MISSING_VALUE = /missing a provider-owned value/;
|
|
10
|
+
|
|
11
|
+
describe('buildProviderRepositoryId', () => {
|
|
12
|
+
it('joins the provider and value with a colon', () => {
|
|
13
|
+
const result = buildProviderRepositoryId('github', '42');
|
|
14
|
+
|
|
15
|
+
expect(result).toBe('github:42');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('parseProviderRepositoryId', () => {
|
|
20
|
+
it('returns the provider-owned value for a well-formed id', () => {
|
|
21
|
+
const result = parseProviderRepositoryId('github:42', 'github');
|
|
22
|
+
|
|
23
|
+
expect(result).toBe('42');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('keeps colons inside the value (splits only on the first separator)', () => {
|
|
27
|
+
const result = parseProviderRepositoryId('github:org/repo:extra', 'github');
|
|
28
|
+
|
|
29
|
+
expect(result).toBe('org/repo:extra');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it.each([
|
|
33
|
+
['42', 'no provider prefix'],
|
|
34
|
+
[':42', 'an empty provider prefix'],
|
|
35
|
+
])('rejects %s (%s)', (externalRepositoryId) => {
|
|
36
|
+
const parse = () => parseProviderRepositoryId(externalRepositoryId, 'github');
|
|
37
|
+
|
|
38
|
+
expect(parse).toThrow(IntegrationProviderError);
|
|
39
|
+
expect(parse).toThrow(MISSING_PREFIX);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('rejects an id owned by a different provider', () => {
|
|
43
|
+
const parse = () => parseProviderRepositoryId('gitlab:42', 'github');
|
|
44
|
+
|
|
45
|
+
expect(parse).toThrow(IntegrationProviderError);
|
|
46
|
+
expect(parse).toThrow(WRONG_PROVIDER);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('rejects an id with a prefix but no value', () => {
|
|
50
|
+
const parse = () => parseProviderRepositoryId('github:', 'github');
|
|
51
|
+
|
|
52
|
+
expect(parse).toThrow(IntegrationProviderError);
|
|
53
|
+
expect(parse).toThrow(MISSING_VALUE);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('surfaces the repository-not-found reason on the thrown error', () => {
|
|
57
|
+
expect.assertions(1);
|
|
58
|
+
try {
|
|
59
|
+
parseProviderRepositoryId('42', 'github');
|
|
60
|
+
} catch (error) {
|
|
61
|
+
expect((error as IntegrationProviderError).reason).toBe('repository-not-found');
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
export type IntegrationProviderKind = string;
|
|
2
|
+
export type IntegrationCapability = 'source_control' | 'agent_tools';
|
|
3
|
+
export type IntegrationConnectionLifecycleStatus = 'active' | 'disabled' | 'error';
|
|
4
|
+
|
|
5
|
+
export interface IntegrationConnection<
|
|
6
|
+
ProviderKind extends IntegrationProviderKind = IntegrationProviderKind,
|
|
7
|
+
> {
|
|
8
|
+
id: string;
|
|
9
|
+
workspaceId: string;
|
|
10
|
+
provider: ProviderKind;
|
|
11
|
+
externalAccountId: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
displayName: string;
|
|
14
|
+
lifecycleStatus: IntegrationConnectionLifecycleStatus;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
updatedAt: Date;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type RepositoryVisibility = 'public' | 'private' | 'internal' | 'unknown';
|
|
20
|
+
|
|
21
|
+
export interface RepositorySnapshot {
|
|
22
|
+
externalRepositoryId: string;
|
|
23
|
+
owner: string;
|
|
24
|
+
name: string;
|
|
25
|
+
fullName: string;
|
|
26
|
+
defaultBranch: string;
|
|
27
|
+
visibility: RepositoryVisibility;
|
|
28
|
+
cloneUrl: string;
|
|
29
|
+
htmlUrl: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RepositoryPage {
|
|
33
|
+
repositories: RepositorySnapshot[];
|
|
34
|
+
nextCursor: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ListRepositoriesInput<
|
|
38
|
+
Connection extends IntegrationConnection = IntegrationConnection,
|
|
39
|
+
> {
|
|
40
|
+
connection: Connection;
|
|
41
|
+
limit: number;
|
|
42
|
+
cursor?: string | undefined;
|
|
43
|
+
search?: string | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ResolveRepositoryInput<
|
|
47
|
+
Connection extends IntegrationConnection = IntegrationConnection,
|
|
48
|
+
> {
|
|
49
|
+
connection: Connection;
|
|
50
|
+
externalRepositoryId: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface FileSnapshot {
|
|
54
|
+
path: string;
|
|
55
|
+
ref: string;
|
|
56
|
+
content: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface FileEntry {
|
|
60
|
+
path: string;
|
|
61
|
+
type: 'file';
|
|
62
|
+
size: number | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface FilePage {
|
|
66
|
+
files: FileEntry[];
|
|
67
|
+
nextCursor: string | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ListFilesInput<Connection extends IntegrationConnection = IntegrationConnection>
|
|
71
|
+
extends ResolveRepositoryInput<Connection> {
|
|
72
|
+
ref: string;
|
|
73
|
+
prefix: string;
|
|
74
|
+
limit: number;
|
|
75
|
+
cursor?: string | undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface FetchFileInput<Connection extends IntegrationConnection = IntegrationConnection>
|
|
79
|
+
extends ResolveRepositoryInput<Connection> {
|
|
80
|
+
ref: string;
|
|
81
|
+
path: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface CheckoutCredentials {
|
|
85
|
+
username: string;
|
|
86
|
+
token: string;
|
|
87
|
+
expiresAt: Date;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface CheckoutGitAuthor {
|
|
91
|
+
name: string;
|
|
92
|
+
email: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface CheckoutPermissions {
|
|
96
|
+
contents: 'read' | 'write';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface CheckoutSpec {
|
|
100
|
+
/**
|
|
101
|
+
* Clone URL that must never embed authentication material. Credentials live
|
|
102
|
+
* only in `credentials` so a redaction helper can mask them; a provider that
|
|
103
|
+
* embeds a token in this URL would bypass redaction and leak it into logs,
|
|
104
|
+
* `git remote -v`, and persisted job rows.
|
|
105
|
+
*/
|
|
106
|
+
repositoryUrl: string;
|
|
107
|
+
ref: string;
|
|
108
|
+
credentials?: CheckoutCredentials | undefined;
|
|
109
|
+
gitAuthor?: CheckoutGitAuthor | undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface CreateCheckoutSpecInput<
|
|
113
|
+
Connection extends IntegrationConnection = IntegrationConnection,
|
|
114
|
+
> extends ResolveRepositoryInput<Connection> {
|
|
115
|
+
ref?: string | undefined;
|
|
116
|
+
permissions?: CheckoutPermissions | undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface SourceControlProvider<
|
|
120
|
+
Connection extends IntegrationConnection = IntegrationConnection,
|
|
121
|
+
> {
|
|
122
|
+
listRepositories(input: ListRepositoriesInput<Connection>): Promise<RepositoryPage>;
|
|
123
|
+
resolveRepository(input: ResolveRepositoryInput<Connection>): Promise<RepositorySnapshot>;
|
|
124
|
+
listFiles(input: ListFilesInput<Connection>): Promise<FilePage>;
|
|
125
|
+
fetchFile(input: FetchFileInput<Connection>): Promise<FileSnapshot>;
|
|
126
|
+
createCheckoutSpec?(input: CreateCheckoutSpecInput<Connection>): Promise<CheckoutSpec>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type AgentToolSensitivity = 'read' | 'write';
|
|
130
|
+
export type AgentToolJsonSchema = Record<string, unknown>;
|
|
131
|
+
|
|
132
|
+
export interface AgentToolCatalogMethod<RequiredScope = unknown> {
|
|
133
|
+
id: string;
|
|
134
|
+
description: string;
|
|
135
|
+
sensitivity: AgentToolSensitivity;
|
|
136
|
+
sensitive: boolean;
|
|
137
|
+
requiredScope: RequiredScope;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface AgentToolCatalogEntry<RequiredScope = unknown> {
|
|
141
|
+
id: string;
|
|
142
|
+
description: string;
|
|
143
|
+
sensitivity: AgentToolSensitivity;
|
|
144
|
+
sensitive: boolean;
|
|
145
|
+
requiredScope: RequiredScope;
|
|
146
|
+
inputSchema: AgentToolJsonSchema;
|
|
147
|
+
outputSchema?: AgentToolJsonSchema | undefined;
|
|
148
|
+
methods?: readonly AgentToolCatalogMethod<RequiredScope>[] | undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type AgentToolSelectorKind = 'family' | 'family_wildcard' | 'method' | 'standalone';
|
|
152
|
+
|
|
153
|
+
export interface AgentToolSelector {
|
|
154
|
+
readonly token: string;
|
|
155
|
+
readonly kind: AgentToolSelectorKind;
|
|
156
|
+
readonly sensitivity: AgentToolSensitivity;
|
|
157
|
+
readonly sensitive: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface AgentToolSelectionCatalog {
|
|
161
|
+
readonly selectors: readonly AgentToolSelector[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface AgentToolCallInput {
|
|
165
|
+
toolId: string;
|
|
166
|
+
arguments: Record<string, unknown>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface AgentToolSession<CallResult = unknown> {
|
|
170
|
+
call(input: AgentToolCallInput): Promise<CallResult>;
|
|
171
|
+
close?(): Promise<void>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface OpenAgentToolsSessionInput<
|
|
175
|
+
Connection extends IntegrationConnection = IntegrationConnection,
|
|
176
|
+
RequiredScope = unknown,
|
|
177
|
+
ProviderScope = unknown,
|
|
178
|
+
> {
|
|
179
|
+
connection: Connection;
|
|
180
|
+
tools: readonly AgentToolCatalogEntry<RequiredScope>[];
|
|
181
|
+
scope: ProviderScope;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface AgentToolsProvider<
|
|
185
|
+
Connection extends IntegrationConnection = IntegrationConnection,
|
|
186
|
+
RequiredScope = unknown,
|
|
187
|
+
ProviderScope = unknown,
|
|
188
|
+
CallResult = unknown,
|
|
189
|
+
> {
|
|
190
|
+
catalog():
|
|
191
|
+
| readonly AgentToolCatalogEntry<RequiredScope>[]
|
|
192
|
+
| Promise<readonly AgentToolCatalogEntry<RequiredScope>[]>;
|
|
193
|
+
selectionCatalog(): AgentToolSelectionCatalog | Promise<AgentToolSelectionCatalog>;
|
|
194
|
+
openSession(
|
|
195
|
+
input: OpenAgentToolsSessionInput<Connection, RequiredScope, ProviderScope>,
|
|
196
|
+
): Promise<AgentToolSession<CallResult>>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface IntegrationProviderAdapters<
|
|
200
|
+
Connection extends IntegrationConnection = IntegrationConnection,
|
|
201
|
+
> {
|
|
202
|
+
source_control?: SourceControlProvider<Connection> | undefined;
|
|
203
|
+
agent_tools?: AgentToolsProvider<Connection> | undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface IntegrationProvider<
|
|
207
|
+
ProviderKind extends IntegrationProviderKind = IntegrationProviderKind,
|
|
208
|
+
Route = unknown,
|
|
209
|
+
Connection extends IntegrationConnection<ProviderKind> = IntegrationConnection<ProviderKind>,
|
|
210
|
+
> {
|
|
211
|
+
provider: ProviderKind;
|
|
212
|
+
displayName: string;
|
|
213
|
+
adapters?: IntegrationProviderAdapters<Connection> | undefined;
|
|
214
|
+
routes?: Route[] | undefined;
|
|
215
|
+
/**
|
|
216
|
+
* Resolves the provider-side home of a connection (e.g. the Sentry org or the
|
|
217
|
+
* GitHub installation settings page). Returns undefined when the connection has
|
|
218
|
+
* no external home or the provider-side record is missing.
|
|
219
|
+
*/
|
|
220
|
+
connectionExternalUrl?(connection: Connection): Promise<string | undefined>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface RegisteredIntegrationProvider<
|
|
224
|
+
ProviderKind extends IntegrationProviderKind = IntegrationProviderKind,
|
|
225
|
+
Route = unknown,
|
|
226
|
+
Connection extends IntegrationConnection<ProviderKind> = IntegrationConnection<ProviderKind>,
|
|
227
|
+
> extends IntegrationProvider<ProviderKind, Route, Connection> {
|
|
228
|
+
adapters: IntegrationProviderAdapters<Connection>;
|
|
229
|
+
capabilities: IntegrationCapability[];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export type IntegrationProviderErrorReason =
|
|
233
|
+
| 'repository-not-found'
|
|
234
|
+
| 'installation-not-found'
|
|
235
|
+
| 'file-not-found'
|
|
236
|
+
| 'access-denied'
|
|
237
|
+
| 'rate-limited'
|
|
238
|
+
| 'timeout'
|
|
239
|
+
| 'provider-unavailable'
|
|
240
|
+
| 'malformed-provider-response'
|
|
241
|
+
| 'content-too-large'
|
|
242
|
+
| 'too-many-files';
|
|
243
|
+
|
|
244
|
+
export class IntegrationProviderError extends Error {
|
|
245
|
+
constructor(
|
|
246
|
+
public readonly reason: IntegrationProviderErrorReason,
|
|
247
|
+
message: string,
|
|
248
|
+
public readonly retryAfterSeconds?: number | undefined,
|
|
249
|
+
) {
|
|
250
|
+
super(message);
|
|
251
|
+
this.name = 'IntegrationProviderError';
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export const MAX_REPOSITORY_FILE_BYTES = 1_000_000;
|
|
256
|
+
|
|
257
|
+
export function toIntegrationConnectionDto(
|
|
258
|
+
connection: IntegrationConnection,
|
|
259
|
+
options: {
|
|
260
|
+
capabilities: IntegrationCapability[];
|
|
261
|
+
externalUrl?: string | undefined;
|
|
262
|
+
},
|
|
263
|
+
) {
|
|
264
|
+
return {
|
|
265
|
+
id: connection.id,
|
|
266
|
+
workspace_id: connection.workspaceId,
|
|
267
|
+
provider: connection.provider,
|
|
268
|
+
external_account_id: connection.externalAccountId,
|
|
269
|
+
slug: connection.slug,
|
|
270
|
+
display_name: connection.displayName,
|
|
271
|
+
lifecycle_status: connection.lifecycleStatus,
|
|
272
|
+
capabilities: options.capabilities,
|
|
273
|
+
...(options.externalUrl ? {external_url: options.externalUrl} : {}),
|
|
274
|
+
created_at: connection.createdAt.toISOString(),
|
|
275
|
+
updated_at: connection.updatedAt.toISOString(),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function buildProviderRepositoryId(
|
|
280
|
+
provider: IntegrationProviderKind,
|
|
281
|
+
value: string,
|
|
282
|
+
): string {
|
|
283
|
+
return `${provider}:${value}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function parseProviderRepositoryId(
|
|
287
|
+
externalRepositoryId: string,
|
|
288
|
+
expectedProvider: IntegrationProviderKind,
|
|
289
|
+
): string {
|
|
290
|
+
const separatorIndex = externalRepositoryId.indexOf(':');
|
|
291
|
+
if (separatorIndex <= 0) {
|
|
292
|
+
throw new IntegrationProviderError(
|
|
293
|
+
'repository-not-found',
|
|
294
|
+
`External repository id is missing a provider prefix: ${externalRepositoryId}`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const prefix = externalRepositoryId.slice(0, separatorIndex);
|
|
298
|
+
const value = externalRepositoryId.slice(separatorIndex + 1);
|
|
299
|
+
if (prefix !== expectedProvider) {
|
|
300
|
+
throw new IntegrationProviderError(
|
|
301
|
+
'repository-not-found',
|
|
302
|
+
`External repository id ${externalRepositoryId} is not owned by provider ${expectedProvider}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (!value) {
|
|
306
|
+
throw new IntegrationProviderError(
|
|
307
|
+
'repository-not-found',
|
|
308
|
+
`External repository id ${externalRepositoryId} is missing a provider-owned value`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
return value;
|
|
312
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INTEGRATION_EVENT_RECEIVED,
|
|
3
|
+
INTEGRATION_SOURCE_COMMIT_PUSHED,
|
|
4
|
+
integrationEventReceivedSchema,
|
|
5
|
+
integrationSourceCommitPushedSchema,
|
|
6
|
+
integrationsEventSchemas,
|
|
7
|
+
} from './events.js';
|
|
8
|
+
|
|
9
|
+
const validEventReceived = {
|
|
10
|
+
provider: 'github',
|
|
11
|
+
source: 'github',
|
|
12
|
+
event: 'push',
|
|
13
|
+
workspaceId: 'ws-1',
|
|
14
|
+
connectionId: 'conn-1',
|
|
15
|
+
connectionName: 'Acme Production',
|
|
16
|
+
deliveryId: 'delivery-1',
|
|
17
|
+
receivedAt: '2026-06-21T00:00:00.000Z',
|
|
18
|
+
payload: {opaque: true},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const validCommitPushed = {
|
|
22
|
+
provider: 'github',
|
|
23
|
+
workspaceId: 'ws-1',
|
|
24
|
+
connectionId: 'conn-1',
|
|
25
|
+
deliveryId: 'delivery-1',
|
|
26
|
+
receivedAt: '2026-06-21T00:00:00.000Z',
|
|
27
|
+
push: {
|
|
28
|
+
externalRepositoryId: 'acme/repo',
|
|
29
|
+
ref: 'refs/heads/main',
|
|
30
|
+
headCommitSha: 'abc123',
|
|
31
|
+
defaultBranch: 'main',
|
|
32
|
+
isDefaultBranch: true,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('integrationSourceCommitPushedSchema', () => {
|
|
37
|
+
it('parses a valid commit-pushed payload unchanged', () => {
|
|
38
|
+
const result = integrationSourceCommitPushedSchema.parse(validCommitPushed);
|
|
39
|
+
|
|
40
|
+
expect(result).toEqual(validCommitPushed);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('rejects a payload missing a top-level field', () => {
|
|
44
|
+
const {provider: _provider, ...withoutProvider} = validCommitPushed;
|
|
45
|
+
|
|
46
|
+
const parse = () => integrationSourceCommitPushedSchema.parse(withoutProvider);
|
|
47
|
+
|
|
48
|
+
expect(parse).toThrow();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('rejects a payload missing a nested push field', () => {
|
|
52
|
+
const {headCommitSha: _headCommitSha, ...pushWithoutSha} = validCommitPushed.push;
|
|
53
|
+
const input = {...validCommitPushed, push: pushWithoutSha};
|
|
54
|
+
|
|
55
|
+
const parse = () => integrationSourceCommitPushedSchema.parse(input);
|
|
56
|
+
|
|
57
|
+
expect(parse).toThrow();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('strips unknown keys (tolerant of forward-compatible producer additions)', () => {
|
|
61
|
+
const input = {...validCommitPushed, addedLater: 'ignored'};
|
|
62
|
+
|
|
63
|
+
const result = integrationSourceCommitPushedSchema.parse(input);
|
|
64
|
+
|
|
65
|
+
expect(result).toEqual(validCommitPushed);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('integrationEventReceivedSchema', () => {
|
|
70
|
+
it('parses a valid generic integration event envelope unchanged', () => {
|
|
71
|
+
const result = integrationEventReceivedSchema.parse(validEventReceived);
|
|
72
|
+
|
|
73
|
+
expect(result).toEqual(validEventReceived);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('defaults missing connection names to null for queued events', () => {
|
|
77
|
+
const {connectionName: _connectionName, ...withoutConnectionName} = validEventReceived;
|
|
78
|
+
|
|
79
|
+
const result = integrationEventReceivedSchema.parse(withoutConnectionName);
|
|
80
|
+
|
|
81
|
+
expect(result).toEqual({...withoutConnectionName, connectionName: null});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('rejects a payload missing the opaque provider payload key', () => {
|
|
85
|
+
const {payload: _payload, ...withoutPayload} = validEventReceived;
|
|
86
|
+
|
|
87
|
+
const parse = () => integrationEventReceivedSchema.parse(withoutPayload);
|
|
88
|
+
|
|
89
|
+
expect(parse).toThrow();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('strips unknown envelope keys', () => {
|
|
93
|
+
const input = {...validEventReceived, addedLater: 'ignored'};
|
|
94
|
+
|
|
95
|
+
const result = integrationEventReceivedSchema.parse(input);
|
|
96
|
+
|
|
97
|
+
expect(result).toEqual(validEventReceived);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('integrationsEventSchemas', () => {
|
|
102
|
+
it('registers every integration publisher event type', () => {
|
|
103
|
+
const registeredTypes = Object.keys(integrationsEventSchemas).sort();
|
|
104
|
+
|
|
105
|
+
expect(registeredTypes).toEqual(
|
|
106
|
+
[INTEGRATION_EVENT_RECEIVED, INTEGRATION_SOURCE_COMMIT_PUSHED].sort(),
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
});
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
|
|
3
|
+
export const INTEGRATION_EVENT_RECEIVED = 'integrations.event.received' as const;
|
|
4
|
+
|
|
5
|
+
const nonEmptyStringSchema = z.string().nonempty();
|
|
6
|
+
const isoDateTimeSchema = z.string().datetime();
|
|
7
|
+
const requiredUnknownSchema = z.custom<unknown>((value) => value !== undefined);
|
|
8
|
+
const nullableConnectionNameSchema = nonEmptyStringSchema.nullish().default(null);
|
|
9
|
+
|
|
10
|
+
export const integrationEventReceivedSchema = z.object({
|
|
11
|
+
provider: nonEmptyStringSchema,
|
|
12
|
+
source: nonEmptyStringSchema,
|
|
13
|
+
event: nonEmptyStringSchema,
|
|
14
|
+
workspaceId: nonEmptyStringSchema,
|
|
15
|
+
connectionId: nonEmptyStringSchema,
|
|
16
|
+
// Denormalized so trigger consumers can render the connection name without a
|
|
17
|
+
// synchronous callback into the integrations module.
|
|
18
|
+
connectionName: nullableConnectionNameSchema,
|
|
19
|
+
deliveryId: nonEmptyStringSchema,
|
|
20
|
+
receivedAt: isoDateTimeSchema,
|
|
21
|
+
payload: requiredUnknownSchema,
|
|
22
|
+
});
|
|
23
|
+
export type IntegrationEventReceivedEvent = z.infer<typeof integrationEventReceivedSchema>;
|
|
24
|
+
|
|
25
|
+
// A source-control push, normalized by the producing provider and carried by
|
|
26
|
+
// `INTEGRATION_SOURCE_COMMIT_PUSHED` for domain consumers.
|
|
27
|
+
export const sourcePushSchema = z.object({
|
|
28
|
+
externalRepositoryId: nonEmptyStringSchema,
|
|
29
|
+
ref: nonEmptyStringSchema,
|
|
30
|
+
headCommitSha: nonEmptyStringSchema,
|
|
31
|
+
defaultBranch: nonEmptyStringSchema,
|
|
32
|
+
isDefaultBranch: z.boolean(),
|
|
33
|
+
});
|
|
34
|
+
export type SourcePushPayload = z.infer<typeof sourcePushSchema>;
|
|
35
|
+
|
|
36
|
+
export const INTEGRATION_SOURCE_COMMIT_PUSHED =
|
|
37
|
+
'integrations.source_control.commit_pushed' as const;
|
|
38
|
+
|
|
39
|
+
// Typed, provider-agnostic source-control event. The producing provider owns the
|
|
40
|
+
// translation from its raw webhook into this shape, so domain consumers never decode
|
|
41
|
+
// provider payloads. `isDefaultBranch` is a fact; the branch policy lives in the consumer.
|
|
42
|
+
export const integrationSourceCommitPushedSchema = z.object({
|
|
43
|
+
provider: nonEmptyStringSchema,
|
|
44
|
+
workspaceId: nonEmptyStringSchema,
|
|
45
|
+
connectionId: nonEmptyStringSchema,
|
|
46
|
+
deliveryId: nonEmptyStringSchema,
|
|
47
|
+
receivedAt: isoDateTimeSchema,
|
|
48
|
+
push: sourcePushSchema,
|
|
49
|
+
});
|
|
50
|
+
export type IntegrationSourceCommitPushedEvent = z.infer<
|
|
51
|
+
typeof integrationSourceCommitPushedSchema
|
|
52
|
+
>;
|
|
53
|
+
|
|
54
|
+
export interface SentryIssuePayload {
|
|
55
|
+
action: SentryIssueAction;
|
|
56
|
+
issueId: string;
|
|
57
|
+
shortId: string | null;
|
|
58
|
+
title: string;
|
|
59
|
+
culprit: string | null;
|
|
60
|
+
level: string | null;
|
|
61
|
+
status: string | null;
|
|
62
|
+
platform: string | null;
|
|
63
|
+
webUrl: string | null;
|
|
64
|
+
issueUrl: string | null;
|
|
65
|
+
projectUrl: string | null;
|
|
66
|
+
firstSeenAt: string | null;
|
|
67
|
+
lastSeenAt: string | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Single source of truth for Sentry issue actions: the sentry-dto webhook schema
|
|
71
|
+
// builds its z.enum from this tuple, so accepted webhook actions and the published
|
|
72
|
+
// SentryIssuePayload contract cannot drift.
|
|
73
|
+
export const SENTRY_ISSUE_ACTIONS = [
|
|
74
|
+
'created',
|
|
75
|
+
'resolved',
|
|
76
|
+
'assigned',
|
|
77
|
+
'archived',
|
|
78
|
+
'unresolved',
|
|
79
|
+
] as const;
|
|
80
|
+
|
|
81
|
+
export type SentryIssueAction = (typeof SENTRY_ISSUE_ACTIONS)[number];
|
|
82
|
+
|
|
83
|
+
export interface IntegrationsEventMap {
|
|
84
|
+
[INTEGRATION_EVENT_RECEIVED]: IntegrationEventReceivedEvent;
|
|
85
|
+
[INTEGRATION_SOURCE_COMMIT_PUSHED]: IntegrationSourceCommitPushedEvent;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const integrationsEventSchemas = {
|
|
89
|
+
[INTEGRATION_EVENT_RECEIVED]: integrationEventReceivedSchema,
|
|
90
|
+
[INTEGRATION_SOURCE_COMMIT_PUSHED]: integrationSourceCommitPushedSchema,
|
|
91
|
+
} satisfies Record<keyof IntegrationsEventMap, z.ZodType>;
|