@shipfox/api-integration-spi 0.2.2 → 1.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 +33 -0
- package/dist/contracts.d.ts +17 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +36 -0
- package/dist/contracts.js.map +1 -1
- package/dist/ports.d.ts +16 -1
- package/dist/ports.d.ts.map +1 -1
- package/dist/ports.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/contracts.test.ts +49 -0
- package/src/contracts.ts +67 -0
- package/src/ports.ts +15 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-integration-spi",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@shipfox/api-integration-core-dto": "
|
|
21
|
+
"@shipfox/api-integration-core-dto": "12.2.0"
|
|
22
22
|
},
|
|
23
23
|
"imports": {
|
|
24
24
|
"#*": "./dist/*"
|
package/src/contracts.test.ts
CHANGED
|
@@ -1,9 +1,58 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildProviderRepositoryId,
|
|
3
3
|
IntegrationProviderError,
|
|
4
|
+
isValidGitObjectId,
|
|
5
|
+
isValidGitRefName,
|
|
6
|
+
isValidTriggerRef,
|
|
4
7
|
parseProviderRepositoryId,
|
|
5
8
|
} from './contracts.js';
|
|
6
9
|
|
|
10
|
+
describe('git ref names', () => {
|
|
11
|
+
it.each([
|
|
12
|
+
'refs/heads/main',
|
|
13
|
+
'refs/heads/feature/review',
|
|
14
|
+
'refs/heads/foo./bar',
|
|
15
|
+
'refs/heads/foo/-bar',
|
|
16
|
+
'refs/pull/17/head',
|
|
17
|
+
])('accepts %s', (ref) => {
|
|
18
|
+
expect(isValidGitRefName(ref)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it.each([
|
|
22
|
+
'',
|
|
23
|
+
'HEAD',
|
|
24
|
+
'main',
|
|
25
|
+
'-main',
|
|
26
|
+
'refs/heads/foo bar',
|
|
27
|
+
'refs/heads/foo..bar',
|
|
28
|
+
'refs/heads/foo.lock',
|
|
29
|
+
'refs/heads/.foo',
|
|
30
|
+
'refs/heads/foo.',
|
|
31
|
+
'refs/heads/foo@{bar',
|
|
32
|
+
])('rejects %s', (ref) => {
|
|
33
|
+
expect(isValidGitRefName(ref)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it.each(['refs/tags/-evil', 'refs/heads/feature/-evil'])('accepts safe trigger ref %s', (ref) => {
|
|
37
|
+
expect(isValidTriggerRef(ref)).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('git object ids', () => {
|
|
42
|
+
it.each(['a'.repeat(40), 'b'.repeat(64)])('accepts a full object id', (value) => {
|
|
43
|
+
expect(isValidGitObjectId(value)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it.each([
|
|
47
|
+
'a',
|
|
48
|
+
'abcdef1234567890',
|
|
49
|
+
'0'.repeat(40),
|
|
50
|
+
'g'.repeat(40),
|
|
51
|
+
])('rejects an invalid object id', (value) => {
|
|
52
|
+
expect(isValidGitObjectId(value)).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
7
56
|
describe('provider repository identifiers', () => {
|
|
8
57
|
it('prefixes provider-owned identifiers', () => {
|
|
9
58
|
const result = buildProviderRepositoryId('github', '42');
|
package/src/contracts.ts
CHANGED
|
@@ -2,6 +2,9 @@ export type IntegrationProviderKind = string;
|
|
|
2
2
|
export type IntegrationCapability = 'source_control' | 'agent_tools';
|
|
3
3
|
export type IntegrationConnectionLifecycleStatus = 'active' | 'disabled' | 'error';
|
|
4
4
|
|
|
5
|
+
const GIT_OBJECT_ID_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
6
|
+
const ZERO_GIT_OBJECT_ID_PATTERN = /^0+$/;
|
|
7
|
+
|
|
5
8
|
export interface IntegrationConnection<
|
|
6
9
|
ProviderKind extends IntegrationProviderKind = IntegrationProviderKind,
|
|
7
10
|
> {
|
|
@@ -110,6 +113,14 @@ export interface CreateCheckoutSpecInput<
|
|
|
110
113
|
permissions?: CheckoutPermissions | undefined;
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
export interface TriggerReference {
|
|
117
|
+
externalRepositoryId: string;
|
|
118
|
+
ref: string;
|
|
119
|
+
commit: string;
|
|
120
|
+
/** Provider handle of whoever caused the event, when the payload names one. */
|
|
121
|
+
actor: string | null;
|
|
122
|
+
}
|
|
123
|
+
|
|
113
124
|
export interface SourceControlProvider<
|
|
114
125
|
Connection extends IntegrationConnection = IntegrationConnection,
|
|
115
126
|
> {
|
|
@@ -117,6 +128,7 @@ export interface SourceControlProvider<
|
|
|
117
128
|
resolveRepository(input: ResolveRepositoryInput<Connection>): Promise<RepositorySnapshot>;
|
|
118
129
|
listFiles(input: ListFilesInput<Connection>): Promise<FilePage>;
|
|
119
130
|
fetchFile(input: FetchFileInput<Connection>): Promise<FileSnapshot>;
|
|
131
|
+
resolveTriggerReference(payload: unknown): TriggerReference | null;
|
|
120
132
|
createCheckoutSpec?(input: CreateCheckoutSpecInput<Connection>): Promise<CheckoutSpec>;
|
|
121
133
|
}
|
|
122
134
|
|
|
@@ -293,3 +305,58 @@ export function parseProviderRepositoryId(
|
|
|
293
305
|
}
|
|
294
306
|
return value;
|
|
295
307
|
}
|
|
308
|
+
|
|
309
|
+
/** Checks the constraints enforced by `git check-ref-format`. */
|
|
310
|
+
export function isValidGitRefName(ref: string): boolean {
|
|
311
|
+
if (!ref || ref === '@' || ref.startsWith('-')) return false;
|
|
312
|
+
if (
|
|
313
|
+
!ref.includes('/') ||
|
|
314
|
+
ref.startsWith('/') ||
|
|
315
|
+
ref.endsWith('/') ||
|
|
316
|
+
ref.includes('//') ||
|
|
317
|
+
ref.includes('..') ||
|
|
318
|
+
ref.includes('@{') ||
|
|
319
|
+
ref.endsWith('.')
|
|
320
|
+
) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
if (
|
|
324
|
+
[...ref].some((character) => {
|
|
325
|
+
const code = character.codePointAt(0) ?? 0;
|
|
326
|
+
return code <= 0x20 || code === 0x7f || '~^:?*[]\\'.includes(character);
|
|
327
|
+
})
|
|
328
|
+
) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const components = ref.split('/');
|
|
333
|
+
return components.every(
|
|
334
|
+
(component) =>
|
|
335
|
+
component.length > 0 && !component.startsWith('.') && !component.endsWith('.lock'),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
340
|
+
return typeof value === 'object' && value !== null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function asRecord(value: unknown): Record<string, unknown> | null {
|
|
344
|
+
return isRecord(value) ? value : null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function nonEmptyString(value: unknown): string | null {
|
|
348
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function positiveInteger(value: unknown): number | null {
|
|
352
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Validates a provider ref before it is passed to a git operation. */
|
|
356
|
+
export function isValidTriggerRef(ref: string): boolean {
|
|
357
|
+
return isValidGitRefName(ref);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function isValidGitObjectId(value: string): boolean {
|
|
361
|
+
return GIT_OBJECT_ID_PATTERN.test(value) && !ZERO_GIT_OBJECT_ID_PATTERN.test(value);
|
|
362
|
+
}
|
package/src/ports.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
IntegrationEventReceivedEvent,
|
|
3
3
|
SourcePushPayload,
|
|
4
|
+
SourceRepositoryIdentity,
|
|
4
5
|
} from '@shipfox/api-integration-core-dto';
|
|
5
6
|
import type {IntegrationConnection, IntegrationConnectionLifecycleStatus} from '#contracts.js';
|
|
6
7
|
|
|
@@ -37,6 +38,20 @@ export type PublishSourcePushFn = (params: {
|
|
|
37
38
|
push: SourcePushPayload;
|
|
38
39
|
}) => Promise<{published: boolean}>;
|
|
39
40
|
|
|
41
|
+
export type PublishSourceRepositoryUpdatedFn = (params: {
|
|
42
|
+
tx: IntegrationTx;
|
|
43
|
+
provider: string;
|
|
44
|
+
source: string;
|
|
45
|
+
workspaceId: string;
|
|
46
|
+
connectionId: string;
|
|
47
|
+
connectionName: string;
|
|
48
|
+
deliveryId: string;
|
|
49
|
+
receivedAt: string;
|
|
50
|
+
rawPayload: unknown;
|
|
51
|
+
event: string;
|
|
52
|
+
repositories: SourceRepositoryIdentity[];
|
|
53
|
+
}) => Promise<{published: boolean}>;
|
|
54
|
+
|
|
40
55
|
export type RecordDeliveryOnlyFn = (params: {
|
|
41
56
|
tx: IntegrationTx;
|
|
42
57
|
provider: string;
|