@shipfox/api-integration-core-dto 5.0.0 → 8.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 +1 -1
- package/CHANGELOG.md +20 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/inter-module.d.ts +303 -0
- package/dist/inter-module.d.ts.map +1 -0
- package/dist/inter-module.js +189 -0
- package/dist/inter-module.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/dist/webhooks.d.ts +97 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +151 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +7 -10
- package/src/index.ts +1 -0
- package/src/inter-module.test.ts +38 -0
- package/src/inter-module.ts +129 -0
- package/src/webhooks.test.ts +92 -0
- package/src/webhooks.ts +228 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const WEBHOOK_REQUEST_SCHEMA_VERSION: 1;
|
|
3
|
+
export declare const WEBHOOK_MAX_RAW_BODY_BYTES: number;
|
|
4
|
+
export declare const WEBHOOK_MAX_SERIALIZED_REQUEST_BYTES: number;
|
|
5
|
+
export declare const WEBHOOK_MAX_HEADER_BYTES: number;
|
|
6
|
+
export declare const WEBHOOK_MAX_RAW_QUERY_STRING_LENGTH: number;
|
|
7
|
+
export declare const webhookRouteIds: readonly ["github", "gitea", "linear", "sentry", "slack.event", "slack.command", "webhook.connection"];
|
|
8
|
+
export declare const webhookRouteIdSchema: z.ZodEnum<{
|
|
9
|
+
github: "github";
|
|
10
|
+
gitea: "gitea";
|
|
11
|
+
linear: "linear";
|
|
12
|
+
sentry: "sentry";
|
|
13
|
+
"slack.event": "slack.event";
|
|
14
|
+
"slack.command": "slack.command";
|
|
15
|
+
"webhook.connection": "webhook.connection";
|
|
16
|
+
}>;
|
|
17
|
+
export type WebhookRouteId = z.infer<typeof webhookRouteIdSchema>;
|
|
18
|
+
declare const webhookBodySchema: z.ZodObject<{
|
|
19
|
+
encoding: z.ZodLiteral<"base64">;
|
|
20
|
+
data: z.ZodString;
|
|
21
|
+
}, z.core.$strict>;
|
|
22
|
+
export declare const storedWebhookRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
23
|
+
schema_version: z.ZodLiteral<1>;
|
|
24
|
+
request_id: z.ZodString;
|
|
25
|
+
received_at: z.ZodString;
|
|
26
|
+
method: z.ZodLiteral<"POST">;
|
|
27
|
+
raw_query_string: z.ZodString;
|
|
28
|
+
headers: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
29
|
+
body: z.ZodObject<{
|
|
30
|
+
encoding: z.ZodLiteral<"base64">;
|
|
31
|
+
data: z.ZodString;
|
|
32
|
+
}, z.core.$strict>;
|
|
33
|
+
route_id: z.ZodEnum<{
|
|
34
|
+
github: "github";
|
|
35
|
+
gitea: "gitea";
|
|
36
|
+
linear: "linear";
|
|
37
|
+
sentry: "sentry";
|
|
38
|
+
"slack.event": "slack.event";
|
|
39
|
+
"slack.command": "slack.command";
|
|
40
|
+
}>;
|
|
41
|
+
path_parameters: z.ZodObject<{}, z.core.$strict>;
|
|
42
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
43
|
+
schema_version: z.ZodLiteral<1>;
|
|
44
|
+
request_id: z.ZodString;
|
|
45
|
+
received_at: z.ZodString;
|
|
46
|
+
method: z.ZodLiteral<"POST">;
|
|
47
|
+
raw_query_string: z.ZodString;
|
|
48
|
+
headers: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
49
|
+
body: z.ZodObject<{
|
|
50
|
+
encoding: z.ZodLiteral<"base64">;
|
|
51
|
+
data: z.ZodString;
|
|
52
|
+
}, z.core.$strict>;
|
|
53
|
+
route_id: z.ZodLiteral<"webhook.connection">;
|
|
54
|
+
path_parameters: z.ZodObject<{
|
|
55
|
+
connection_id: z.ZodString;
|
|
56
|
+
}, z.core.$strict>;
|
|
57
|
+
}, z.core.$strict>], "route_id">;
|
|
58
|
+
export type StoredWebhookRequest = z.infer<typeof storedWebhookRequestSchema>;
|
|
59
|
+
export declare const webhookProcessingResultSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
60
|
+
outcome: z.ZodLiteral<"processed">;
|
|
61
|
+
deliveryId: z.ZodOptional<z.ZodString>;
|
|
62
|
+
challenge: z.ZodOptional<z.ZodString>;
|
|
63
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
64
|
+
outcome: z.ZodLiteral<"duplicate">;
|
|
65
|
+
deliveryId: z.ZodString;
|
|
66
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
67
|
+
outcome: z.ZodLiteral<"discarded">;
|
|
68
|
+
reason: z.ZodEnum<{
|
|
69
|
+
missing_required_input: "missing_required_input";
|
|
70
|
+
invalid_signature: "invalid_signature";
|
|
71
|
+
stale_at_receipt: "stale_at_receipt";
|
|
72
|
+
malformed_payload: "malformed_payload";
|
|
73
|
+
unsupported_event: "unsupported_event";
|
|
74
|
+
connection_unavailable: "connection_unavailable";
|
|
75
|
+
}>;
|
|
76
|
+
deliveryId: z.ZodOptional<z.ZodString>;
|
|
77
|
+
}, z.core.$strict>], "outcome">;
|
|
78
|
+
export type WebhookProcessingResult = z.infer<typeof webhookProcessingResultSchema>;
|
|
79
|
+
/** Processes one provider-neutral inbound webhook request. */
|
|
80
|
+
export interface WebhookRequestProcessor {
|
|
81
|
+
process(request: StoredWebhookRequest): Promise<WebhookProcessingResult>;
|
|
82
|
+
}
|
|
83
|
+
export interface CreateStoredWebhookRequestInput {
|
|
84
|
+
requestId: string;
|
|
85
|
+
routeId: WebhookRouteId;
|
|
86
|
+
receivedAt: string;
|
|
87
|
+
rawQueryString: string;
|
|
88
|
+
headers: Record<string, string>;
|
|
89
|
+
body: Uint8Array;
|
|
90
|
+
connectionId?: string | undefined;
|
|
91
|
+
}
|
|
92
|
+
export declare function encodeWebhookBody(body: Uint8Array): string;
|
|
93
|
+
export declare function decodeWebhookBody(body: z.infer<typeof webhookBodySchema>): Uint8Array;
|
|
94
|
+
export declare function createStoredWebhookRequest(input: CreateStoredWebhookRequestInput): StoredWebhookRequest;
|
|
95
|
+
export declare function createMaximumSizeStoredWebhookRequestFixture(): StoredWebhookRequest;
|
|
96
|
+
export {};
|
|
97
|
+
//# sourceMappingURL=webhooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,eAAO,MAAM,8BAA8B,EAAG,CAAU,CAAC;AACzD,eAAO,MAAM,0BAA0B,QAAa,CAAC;AACrD,eAAO,MAAM,oCAAoC,QAAc,CAAC;AAChE,eAAO,MAAM,wBAAwB,QAAY,CAAC;AAClD,eAAO,MAAM,mCAAmC,QAAW,CAAC;AAW5D,eAAO,MAAM,eAAe,wGAA8D,CAAC;AAE3F,eAAO,MAAM,oBAAoB;;;;;;;;EAA0B,CAAC;AAC5D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AA4BlE,QAAA,MAAM,iBAAiB;;;kBAYpB,CAAC;AA6BJ,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAGrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;+BAuBxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,8DAA8D;AAC9D,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;CAC1E;AAED,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,UAAU,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA0B1D;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,GAAG,UAAU,CAyBrF;AAED,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,+BAA+B,GACrC,oBAAoB,CAetB;AAED,wBAAgB,4CAA4C,IAAI,oBAAoB,CAiBnF"}
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const WEBHOOK_REQUEST_SCHEMA_VERSION = 1;
|
|
3
|
+
export const WEBHOOK_MAX_RAW_BODY_BYTES = 512 * 1024;
|
|
4
|
+
export const WEBHOOK_MAX_SERIALIZED_REQUEST_BYTES = 1024 * 1024;
|
|
5
|
+
export const WEBHOOK_MAX_HEADER_BYTES = 64 * 1024;
|
|
6
|
+
export const WEBHOOK_MAX_RAW_QUERY_STRING_LENGTH = 8 * 1024;
|
|
7
|
+
const standardWebhookRouteIds = [
|
|
8
|
+
'github',
|
|
9
|
+
'gitea',
|
|
10
|
+
'linear',
|
|
11
|
+
'sentry',
|
|
12
|
+
'slack.event',
|
|
13
|
+
'slack.command'
|
|
14
|
+
];
|
|
15
|
+
export const webhookRouteIds = [
|
|
16
|
+
...standardWebhookRouteIds,
|
|
17
|
+
'webhook.connection'
|
|
18
|
+
];
|
|
19
|
+
export const webhookRouteIdSchema = z.enum(webhookRouteIds);
|
|
20
|
+
const base64Pattern = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
21
|
+
function decodedBase64Length(value) {
|
|
22
|
+
const paddingLength = value.endsWith('==') ? 2 : value.endsWith('=') ? 1 : 0;
|
|
23
|
+
return value.length / 4 * 3 - paddingLength;
|
|
24
|
+
}
|
|
25
|
+
function headersByteLength(headers) {
|
|
26
|
+
return new TextEncoder().encode(JSON.stringify(headers)).byteLength;
|
|
27
|
+
}
|
|
28
|
+
const webhookHeadersSchema = z.record(z.string(), z.string().max(8 * 1024)).refine((headers)=>Object.keys(headers).every((name)=>name === name.toLowerCase() && name.length > 0 && name.length <= 128), 'Webhook header names must be lowercase and at most 128 characters').refine((headers)=>headersByteLength(headers) <= WEBHOOK_MAX_HEADER_BYTES, `Webhook headers must serialize to at most ${WEBHOOK_MAX_HEADER_BYTES} bytes`);
|
|
29
|
+
const webhookBodySchema = z.object({
|
|
30
|
+
encoding: z.literal('base64'),
|
|
31
|
+
data: z.string().max(Math.ceil(WEBHOOK_MAX_RAW_BODY_BYTES / 3) * 4).regex(base64Pattern)
|
|
32
|
+
}).strict().refine((body)=>decodedBase64Length(body.data) <= WEBHOOK_MAX_RAW_BODY_BYTES, `Webhook bodies must decode to at most ${WEBHOOK_MAX_RAW_BODY_BYTES} bytes`);
|
|
33
|
+
const storedWebhookRequestBaseSchema = z.object({
|
|
34
|
+
schema_version: z.literal(WEBHOOK_REQUEST_SCHEMA_VERSION),
|
|
35
|
+
request_id: z.string().uuid(),
|
|
36
|
+
received_at: z.string().datetime({
|
|
37
|
+
offset: true
|
|
38
|
+
}),
|
|
39
|
+
method: z.literal('POST'),
|
|
40
|
+
raw_query_string: z.string().max(WEBHOOK_MAX_RAW_QUERY_STRING_LENGTH),
|
|
41
|
+
headers: webhookHeadersSchema,
|
|
42
|
+
body: webhookBodySchema
|
|
43
|
+
});
|
|
44
|
+
const emptyPathParametersSchema = z.object({}).strict();
|
|
45
|
+
const connectionPathParametersSchema = z.object({
|
|
46
|
+
connection_id: z.string().uuid()
|
|
47
|
+
}).strict();
|
|
48
|
+
const standardWebhookRouteRequestSchema = storedWebhookRequestBaseSchema.extend({
|
|
49
|
+
route_id: z.enum(standardWebhookRouteIds),
|
|
50
|
+
path_parameters: emptyPathParametersSchema
|
|
51
|
+
}).strict();
|
|
52
|
+
const genericWebhookRouteRequestSchema = storedWebhookRequestBaseSchema.extend({
|
|
53
|
+
route_id: z.literal('webhook.connection'),
|
|
54
|
+
path_parameters: connectionPathParametersSchema
|
|
55
|
+
}).strict();
|
|
56
|
+
export const storedWebhookRequestSchema = z.discriminatedUnion('route_id', [
|
|
57
|
+
standardWebhookRouteRequestSchema,
|
|
58
|
+
genericWebhookRouteRequestSchema
|
|
59
|
+
]);
|
|
60
|
+
export const webhookProcessingResultSchema = z.discriminatedUnion('outcome', [
|
|
61
|
+
z.object({
|
|
62
|
+
outcome: z.literal('processed'),
|
|
63
|
+
deliveryId: z.string().min(1).optional(),
|
|
64
|
+
challenge: z.string().min(1).optional()
|
|
65
|
+
}).strict(),
|
|
66
|
+
z.object({
|
|
67
|
+
outcome: z.literal('duplicate'),
|
|
68
|
+
deliveryId: z.string().min(1)
|
|
69
|
+
}).strict(),
|
|
70
|
+
z.object({
|
|
71
|
+
outcome: z.literal('discarded'),
|
|
72
|
+
reason: z.enum([
|
|
73
|
+
'missing_required_input',
|
|
74
|
+
'invalid_signature',
|
|
75
|
+
'stale_at_receipt',
|
|
76
|
+
'malformed_payload',
|
|
77
|
+
'unsupported_event',
|
|
78
|
+
'connection_unavailable'
|
|
79
|
+
]),
|
|
80
|
+
deliveryId: z.string().min(1).optional()
|
|
81
|
+
}).strict()
|
|
82
|
+
]);
|
|
83
|
+
export function encodeWebhookBody(body) {
|
|
84
|
+
let encoded = '';
|
|
85
|
+
for(let index = 0; index < body.length; index += 3){
|
|
86
|
+
const first = body[index] ?? 0;
|
|
87
|
+
const second = body[index + 1];
|
|
88
|
+
const third = body[index + 2];
|
|
89
|
+
const combined = first << 16 | (second ?? 0) << 8 | (third ?? 0);
|
|
90
|
+
encoded += 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'[combined >> 18 & 63];
|
|
91
|
+
encoded += 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'[combined >> 12 & 63];
|
|
92
|
+
encoded += second === undefined ? '=' : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'[combined >> 6 & 63];
|
|
93
|
+
encoded += third === undefined ? '=' : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'[combined & 63];
|
|
94
|
+
}
|
|
95
|
+
return encoded;
|
|
96
|
+
}
|
|
97
|
+
export function decodeWebhookBody(body) {
|
|
98
|
+
const output = new Uint8Array(decodedBase64Length(body.data));
|
|
99
|
+
let outputIndex = 0;
|
|
100
|
+
for(let index = 0; index < body.data.length; index += 4){
|
|
101
|
+
const chunk = body.data.slice(index, index + 4);
|
|
102
|
+
const values = chunk.split('').map((character)=>character === '=' ? 0 : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.indexOf(character));
|
|
103
|
+
const first = values[0] ?? 0;
|
|
104
|
+
const second = values[1] ?? 0;
|
|
105
|
+
const third = values[2] ?? 0;
|
|
106
|
+
const fourth = values[3] ?? 0;
|
|
107
|
+
const combined = first << 18 | second << 12 | third << 6 | fourth;
|
|
108
|
+
output[outputIndex++] = combined >> 16 & 255;
|
|
109
|
+
if (chunk[2] !== '=') output[outputIndex++] = combined >> 8 & 255;
|
|
110
|
+
if (chunk[3] !== '=') output[outputIndex++] = combined & 255;
|
|
111
|
+
}
|
|
112
|
+
return output;
|
|
113
|
+
}
|
|
114
|
+
export function createStoredWebhookRequest(input) {
|
|
115
|
+
const pathParameters = input.routeId === 'webhook.connection' ? {
|
|
116
|
+
connection_id: input.connectionId
|
|
117
|
+
} : {};
|
|
118
|
+
return storedWebhookRequestSchema.parse({
|
|
119
|
+
schema_version: WEBHOOK_REQUEST_SCHEMA_VERSION,
|
|
120
|
+
request_id: input.requestId,
|
|
121
|
+
route_id: input.routeId,
|
|
122
|
+
received_at: input.receivedAt,
|
|
123
|
+
method: 'POST',
|
|
124
|
+
path_parameters: pathParameters,
|
|
125
|
+
raw_query_string: input.rawQueryString,
|
|
126
|
+
headers: input.headers,
|
|
127
|
+
body: {
|
|
128
|
+
encoding: 'base64',
|
|
129
|
+
data: encodeWebhookBody(input.body)
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
export function createMaximumSizeStoredWebhookRequestFixture() {
|
|
134
|
+
const headers = Object.fromEntries(Array.from({
|
|
135
|
+
length: 8
|
|
136
|
+
}, (_, index)=>[
|
|
137
|
+
`x-fixture-${index}`,
|
|
138
|
+
'a'.repeat(index === 7 ? 8_000 : 8 * 1024)
|
|
139
|
+
]));
|
|
140
|
+
return createStoredWebhookRequest({
|
|
141
|
+
requestId: '9b11d65a-f7e7-40ea-b421-06af012a9be5',
|
|
142
|
+
routeId: 'webhook.connection',
|
|
143
|
+
receivedAt: '2026-07-20T10:30:00.123Z',
|
|
144
|
+
rawQueryString: 'q='.padEnd(WEBHOOK_MAX_RAW_QUERY_STRING_LENGTH, 'q'),
|
|
145
|
+
headers,
|
|
146
|
+
body: new Uint8Array(WEBHOOK_MAX_RAW_BODY_BYTES),
|
|
147
|
+
connectionId: 'c0a8012e-0b6d-4d8f-8d5c-6d74102602b0'
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//# sourceMappingURL=webhooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/webhooks.ts"],"sourcesContent":["import {z} from 'zod';\n\nexport const WEBHOOK_REQUEST_SCHEMA_VERSION = 1 as const;\nexport const WEBHOOK_MAX_RAW_BODY_BYTES = 512 * 1024;\nexport const WEBHOOK_MAX_SERIALIZED_REQUEST_BYTES = 1024 * 1024;\nexport const WEBHOOK_MAX_HEADER_BYTES = 64 * 1024;\nexport const WEBHOOK_MAX_RAW_QUERY_STRING_LENGTH = 8 * 1024;\n\nconst standardWebhookRouteIds = [\n 'github',\n 'gitea',\n 'linear',\n 'sentry',\n 'slack.event',\n 'slack.command',\n] as const;\n\nexport const webhookRouteIds = [...standardWebhookRouteIds, 'webhook.connection'] as const;\n\nexport const webhookRouteIdSchema = z.enum(webhookRouteIds);\nexport type WebhookRouteId = z.infer<typeof webhookRouteIdSchema>;\n\nconst base64Pattern = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;\n\nfunction decodedBase64Length(value: string): number {\n const paddingLength = value.endsWith('==') ? 2 : value.endsWith('=') ? 1 : 0;\n\n return (value.length / 4) * 3 - paddingLength;\n}\n\nfunction headersByteLength(headers: Record<string, string>): number {\n return new TextEncoder().encode(JSON.stringify(headers)).byteLength;\n}\n\nconst webhookHeadersSchema = z\n .record(z.string(), z.string().max(8 * 1024))\n .refine(\n (headers) =>\n Object.keys(headers).every(\n (name) => name === name.toLowerCase() && name.length > 0 && name.length <= 128,\n ),\n 'Webhook header names must be lowercase and at most 128 characters',\n )\n .refine(\n (headers) => headersByteLength(headers) <= WEBHOOK_MAX_HEADER_BYTES,\n `Webhook headers must serialize to at most ${WEBHOOK_MAX_HEADER_BYTES} bytes`,\n );\n\nconst webhookBodySchema = z\n .object({\n encoding: z.literal('base64'),\n data: z\n .string()\n .max(Math.ceil(WEBHOOK_MAX_RAW_BODY_BYTES / 3) * 4)\n .regex(base64Pattern),\n })\n .strict()\n .refine(\n (body) => decodedBase64Length(body.data) <= WEBHOOK_MAX_RAW_BODY_BYTES,\n `Webhook bodies must decode to at most ${WEBHOOK_MAX_RAW_BODY_BYTES} bytes`,\n );\n\nconst storedWebhookRequestBaseSchema = z.object({\n schema_version: z.literal(WEBHOOK_REQUEST_SCHEMA_VERSION),\n request_id: z.string().uuid(),\n received_at: z.string().datetime({offset: true}),\n method: z.literal('POST'),\n raw_query_string: z.string().max(WEBHOOK_MAX_RAW_QUERY_STRING_LENGTH),\n headers: webhookHeadersSchema,\n body: webhookBodySchema,\n});\n\nconst emptyPathParametersSchema = z.object({}).strict();\nconst connectionPathParametersSchema = z.object({connection_id: z.string().uuid()}).strict();\n\nconst standardWebhookRouteRequestSchema = storedWebhookRequestBaseSchema\n .extend({\n route_id: z.enum(standardWebhookRouteIds),\n path_parameters: emptyPathParametersSchema,\n })\n .strict();\n\nconst genericWebhookRouteRequestSchema = storedWebhookRequestBaseSchema\n .extend({\n route_id: z.literal('webhook.connection'),\n path_parameters: connectionPathParametersSchema,\n })\n .strict();\n\nexport const storedWebhookRequestSchema = z.discriminatedUnion('route_id', [\n standardWebhookRouteRequestSchema,\n genericWebhookRouteRequestSchema,\n]);\nexport type StoredWebhookRequest = z.infer<typeof storedWebhookRequestSchema>;\n\nexport const webhookProcessingResultSchema = z.discriminatedUnion('outcome', [\n z\n .object({\n outcome: z.literal('processed'),\n deliveryId: z.string().min(1).optional(),\n challenge: z.string().min(1).optional(),\n })\n .strict(),\n z.object({outcome: z.literal('duplicate'), deliveryId: z.string().min(1)}).strict(),\n z\n .object({\n outcome: z.literal('discarded'),\n reason: z.enum([\n 'missing_required_input',\n 'invalid_signature',\n 'stale_at_receipt',\n 'malformed_payload',\n 'unsupported_event',\n 'connection_unavailable',\n ]),\n deliveryId: z.string().min(1).optional(),\n })\n .strict(),\n]);\nexport type WebhookProcessingResult = z.infer<typeof webhookProcessingResultSchema>;\n\n/** Processes one provider-neutral inbound webhook request. */\nexport interface WebhookRequestProcessor {\n process(request: StoredWebhookRequest): Promise<WebhookProcessingResult>;\n}\n\nexport interface CreateStoredWebhookRequestInput {\n requestId: string;\n routeId: WebhookRouteId;\n receivedAt: string;\n rawQueryString: string;\n headers: Record<string, string>;\n body: Uint8Array;\n connectionId?: string | undefined;\n}\n\nexport function encodeWebhookBody(body: Uint8Array): string {\n let encoded = '';\n\n for (let index = 0; index < body.length; index += 3) {\n const first = body[index] ?? 0;\n const second = body[index + 1];\n const third = body[index + 2];\n const combined = (first << 16) | ((second ?? 0) << 8) | (third ?? 0);\n\n encoded += 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'[\n (combined >> 18) & 63\n ];\n encoded += 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'[\n (combined >> 12) & 63\n ];\n encoded +=\n second === undefined\n ? '='\n : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'[(combined >> 6) & 63];\n encoded +=\n third === undefined\n ? '='\n : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'[combined & 63];\n }\n\n return encoded;\n}\n\nexport function decodeWebhookBody(body: z.infer<typeof webhookBodySchema>): Uint8Array {\n const output = new Uint8Array(decodedBase64Length(body.data));\n let outputIndex = 0;\n\n for (let index = 0; index < body.data.length; index += 4) {\n const chunk = body.data.slice(index, index + 4);\n const values = chunk\n .split('')\n .map((character) =>\n character === '='\n ? 0\n : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.indexOf(character),\n );\n const first = values[0] ?? 0;\n const second = values[1] ?? 0;\n const third = values[2] ?? 0;\n const fourth = values[3] ?? 0;\n const combined = (first << 18) | (second << 12) | (third << 6) | fourth;\n\n output[outputIndex++] = (combined >> 16) & 255;\n if (chunk[2] !== '=') output[outputIndex++] = (combined >> 8) & 255;\n if (chunk[3] !== '=') output[outputIndex++] = combined & 255;\n }\n\n return output;\n}\n\nexport function createStoredWebhookRequest(\n input: CreateStoredWebhookRequestInput,\n): StoredWebhookRequest {\n const pathParameters =\n input.routeId === 'webhook.connection' ? {connection_id: input.connectionId} : {};\n\n return storedWebhookRequestSchema.parse({\n schema_version: WEBHOOK_REQUEST_SCHEMA_VERSION,\n request_id: input.requestId,\n route_id: input.routeId,\n received_at: input.receivedAt,\n method: 'POST',\n path_parameters: pathParameters,\n raw_query_string: input.rawQueryString,\n headers: input.headers,\n body: {encoding: 'base64', data: encodeWebhookBody(input.body)},\n });\n}\n\nexport function createMaximumSizeStoredWebhookRequestFixture(): StoredWebhookRequest {\n const headers = Object.fromEntries(\n Array.from({length: 8}, (_, index) => [\n `x-fixture-${index}`,\n 'a'.repeat(index === 7 ? 8_000 : 8 * 1024),\n ]),\n );\n\n return createStoredWebhookRequest({\n requestId: '9b11d65a-f7e7-40ea-b421-06af012a9be5',\n routeId: 'webhook.connection',\n receivedAt: '2026-07-20T10:30:00.123Z',\n rawQueryString: 'q='.padEnd(WEBHOOK_MAX_RAW_QUERY_STRING_LENGTH, 'q'),\n headers,\n body: new Uint8Array(WEBHOOK_MAX_RAW_BODY_BYTES),\n connectionId: 'c0a8012e-0b6d-4d8f-8d5c-6d74102602b0',\n });\n}\n"],"names":["z","WEBHOOK_REQUEST_SCHEMA_VERSION","WEBHOOK_MAX_RAW_BODY_BYTES","WEBHOOK_MAX_SERIALIZED_REQUEST_BYTES","WEBHOOK_MAX_HEADER_BYTES","WEBHOOK_MAX_RAW_QUERY_STRING_LENGTH","standardWebhookRouteIds","webhookRouteIds","webhookRouteIdSchema","enum","base64Pattern","decodedBase64Length","value","paddingLength","endsWith","length","headersByteLength","headers","TextEncoder","encode","JSON","stringify","byteLength","webhookHeadersSchema","record","string","max","refine","Object","keys","every","name","toLowerCase","webhookBodySchema","object","encoding","literal","data","Math","ceil","regex","strict","body","storedWebhookRequestBaseSchema","schema_version","request_id","uuid","received_at","datetime","offset","method","raw_query_string","emptyPathParametersSchema","connectionPathParametersSchema","connection_id","standardWebhookRouteRequestSchema","extend","route_id","path_parameters","genericWebhookRouteRequestSchema","storedWebhookRequestSchema","discriminatedUnion","webhookProcessingResultSchema","outcome","deliveryId","min","optional","challenge","reason","encodeWebhookBody","encoded","index","first","second","third","combined","undefined","decodeWebhookBody","output","Uint8Array","outputIndex","chunk","slice","values","split","map","character","indexOf","fourth","createStoredWebhookRequest","input","pathParameters","routeId","connectionId","parse","requestId","receivedAt","rawQueryString","createMaximumSizeStoredWebhookRequestFixture","fromEntries","Array","from","_","repeat","padEnd"],"mappings":"AAAA,SAAQA,CAAC,QAAO,MAAM;AAEtB,OAAO,MAAMC,iCAAiC,EAAW;AACzD,OAAO,MAAMC,6BAA6B,MAAM,KAAK;AACrD,OAAO,MAAMC,uCAAuC,OAAO,KAAK;AAChE,OAAO,MAAMC,2BAA2B,KAAK,KAAK;AAClD,OAAO,MAAMC,sCAAsC,IAAI,KAAK;AAE5D,MAAMC,0BAA0B;IAC9B;IACA;IACA;IACA;IACA;IACA;CACD;AAED,OAAO,MAAMC,kBAAkB;OAAID;IAAyB;CAAqB,CAAU;AAE3F,OAAO,MAAME,uBAAuBR,EAAES,IAAI,CAACF,iBAAiB;AAG5D,MAAMG,gBAAgB;AAEtB,SAASC,oBAAoBC,KAAa;IACxC,MAAMC,gBAAgBD,MAAME,QAAQ,CAAC,QAAQ,IAAIF,MAAME,QAAQ,CAAC,OAAO,IAAI;IAE3E,OAAO,AAACF,MAAMG,MAAM,GAAG,IAAK,IAAIF;AAClC;AAEA,SAASG,kBAAkBC,OAA+B;IACxD,OAAO,IAAIC,cAAcC,MAAM,CAACC,KAAKC,SAAS,CAACJ,UAAUK,UAAU;AACrE;AAEA,MAAMC,uBAAuBvB,EAC1BwB,MAAM,CAACxB,EAAEyB,MAAM,IAAIzB,EAAEyB,MAAM,GAAGC,GAAG,CAAC,IAAI,OACtCC,MAAM,CACL,CAACV,UACCW,OAAOC,IAAI,CAACZ,SAASa,KAAK,CACxB,CAACC,OAASA,SAASA,KAAKC,WAAW,MAAMD,KAAKhB,MAAM,GAAG,KAAKgB,KAAKhB,MAAM,IAAI,MAE/E,qEAEDY,MAAM,CACL,CAACV,UAAYD,kBAAkBC,YAAYb,0BAC3C,CAAC,0CAA0C,EAAEA,yBAAyB,MAAM,CAAC;AAGjF,MAAM6B,oBAAoBjC,EACvBkC,MAAM,CAAC;IACNC,UAAUnC,EAAEoC,OAAO,CAAC;IACpBC,MAAMrC,EACHyB,MAAM,GACNC,GAAG,CAACY,KAAKC,IAAI,CAACrC,6BAA6B,KAAK,GAChDsC,KAAK,CAAC9B;AACX,GACC+B,MAAM,GACNd,MAAM,CACL,CAACe,OAAS/B,oBAAoB+B,KAAKL,IAAI,KAAKnC,4BAC5C,CAAC,sCAAsC,EAAEA,2BAA2B,MAAM,CAAC;AAG/E,MAAMyC,iCAAiC3C,EAAEkC,MAAM,CAAC;IAC9CU,gBAAgB5C,EAAEoC,OAAO,CAACnC;IAC1B4C,YAAY7C,EAAEyB,MAAM,GAAGqB,IAAI;IAC3BC,aAAa/C,EAAEyB,MAAM,GAAGuB,QAAQ,CAAC;QAACC,QAAQ;IAAI;IAC9CC,QAAQlD,EAAEoC,OAAO,CAAC;IAClBe,kBAAkBnD,EAAEyB,MAAM,GAAGC,GAAG,CAACrB;IACjCY,SAASM;IACTmB,MAAMT;AACR;AAEA,MAAMmB,4BAA4BpD,EAAEkC,MAAM,CAAC,CAAC,GAAGO,MAAM;AACrD,MAAMY,iCAAiCrD,EAAEkC,MAAM,CAAC;IAACoB,eAAetD,EAAEyB,MAAM,GAAGqB,IAAI;AAAE,GAAGL,MAAM;AAE1F,MAAMc,oCAAoCZ,+BACvCa,MAAM,CAAC;IACNC,UAAUzD,EAAES,IAAI,CAACH;IACjBoD,iBAAiBN;AACnB,GACCX,MAAM;AAET,MAAMkB,mCAAmChB,+BACtCa,MAAM,CAAC;IACNC,UAAUzD,EAAEoC,OAAO,CAAC;IACpBsB,iBAAiBL;AACnB,GACCZ,MAAM;AAET,OAAO,MAAMmB,6BAA6B5D,EAAE6D,kBAAkB,CAAC,YAAY;IACzEN;IACAI;CACD,EAAE;AAGH,OAAO,MAAMG,gCAAgC9D,EAAE6D,kBAAkB,CAAC,WAAW;IAC3E7D,EACGkC,MAAM,CAAC;QACN6B,SAAS/D,EAAEoC,OAAO,CAAC;QACnB4B,YAAYhE,EAAEyB,MAAM,GAAGwC,GAAG,CAAC,GAAGC,QAAQ;QACtCC,WAAWnE,EAAEyB,MAAM,GAAGwC,GAAG,CAAC,GAAGC,QAAQ;IACvC,GACCzB,MAAM;IACTzC,EAAEkC,MAAM,CAAC;QAAC6B,SAAS/D,EAAEoC,OAAO,CAAC;QAAc4B,YAAYhE,EAAEyB,MAAM,GAAGwC,GAAG,CAAC;IAAE,GAAGxB,MAAM;IACjFzC,EACGkC,MAAM,CAAC;QACN6B,SAAS/D,EAAEoC,OAAO,CAAC;QACnBgC,QAAQpE,EAAES,IAAI,CAAC;YACb;YACA;YACA;YACA;YACA;YACA;SACD;QACDuD,YAAYhE,EAAEyB,MAAM,GAAGwC,GAAG,CAAC,GAAGC,QAAQ;IACxC,GACCzB,MAAM;CACV,EAAE;AAkBH,OAAO,SAAS4B,kBAAkB3B,IAAgB;IAChD,IAAI4B,UAAU;IAEd,IAAK,IAAIC,QAAQ,GAAGA,QAAQ7B,KAAK3B,MAAM,EAAEwD,SAAS,EAAG;QACnD,MAAMC,QAAQ9B,IAAI,CAAC6B,MAAM,IAAI;QAC7B,MAAME,SAAS/B,IAAI,CAAC6B,QAAQ,EAAE;QAC9B,MAAMG,QAAQhC,IAAI,CAAC6B,QAAQ,EAAE;QAC7B,MAAMI,WAAW,AAACH,SAAS,KAAO,AAACC,CAAAA,UAAU,CAAA,KAAM,IAAMC,CAAAA,SAAS,CAAA;QAElEJ,WAAW,kEAAkE,CAC3E,AAACK,YAAY,KAAM,GACpB;QACDL,WAAW,kEAAkE,CAC3E,AAACK,YAAY,KAAM,GACpB;QACDL,WACEG,WAAWG,YACP,MACA,kEAAkE,CAAC,AAACD,YAAY,IAAK,GAAG;QAC9FL,WACEI,UAAUE,YACN,MACA,kEAAkE,CAACD,WAAW,GAAG;IACzF;IAEA,OAAOL;AACT;AAEA,OAAO,SAASO,kBAAkBnC,IAAuC;IACvE,MAAMoC,SAAS,IAAIC,WAAWpE,oBAAoB+B,KAAKL,IAAI;IAC3D,IAAI2C,cAAc;IAElB,IAAK,IAAIT,QAAQ,GAAGA,QAAQ7B,KAAKL,IAAI,CAACtB,MAAM,EAAEwD,SAAS,EAAG;QACxD,MAAMU,QAAQvC,KAAKL,IAAI,CAAC6C,KAAK,CAACX,OAAOA,QAAQ;QAC7C,MAAMY,SAASF,MACZG,KAAK,CAAC,IACNC,GAAG,CAAC,CAACC,YACJA,cAAc,MACV,IACA,mEAAmEC,OAAO,CAACD;QAEnF,MAAMd,QAAQW,MAAM,CAAC,EAAE,IAAI;QAC3B,MAAMV,SAASU,MAAM,CAAC,EAAE,IAAI;QAC5B,MAAMT,QAAQS,MAAM,CAAC,EAAE,IAAI;QAC3B,MAAMK,SAASL,MAAM,CAAC,EAAE,IAAI;QAC5B,MAAMR,WAAW,AAACH,SAAS,KAAOC,UAAU,KAAOC,SAAS,IAAKc;QAEjEV,MAAM,CAACE,cAAc,GAAG,AAACL,YAAY,KAAM;QAC3C,IAAIM,KAAK,CAAC,EAAE,KAAK,KAAKH,MAAM,CAACE,cAAc,GAAG,AAACL,YAAY,IAAK;QAChE,IAAIM,KAAK,CAAC,EAAE,KAAK,KAAKH,MAAM,CAACE,cAAc,GAAGL,WAAW;IAC3D;IAEA,OAAOG;AACT;AAEA,OAAO,SAASW,2BACdC,KAAsC;IAEtC,MAAMC,iBACJD,MAAME,OAAO,KAAK,uBAAuB;QAACtC,eAAeoC,MAAMG,YAAY;IAAA,IAAI,CAAC;IAElF,OAAOjC,2BAA2BkC,KAAK,CAAC;QACtClD,gBAAgB3C;QAChB4C,YAAY6C,MAAMK,SAAS;QAC3BtC,UAAUiC,MAAME,OAAO;QACvB7C,aAAa2C,MAAMM,UAAU;QAC7B9C,QAAQ;QACRQ,iBAAiBiC;QACjBxC,kBAAkBuC,MAAMO,cAAc;QACtChF,SAASyE,MAAMzE,OAAO;QACtByB,MAAM;YAACP,UAAU;YAAUE,MAAMgC,kBAAkBqB,MAAMhD,IAAI;QAAC;IAChE;AACF;AAEA,OAAO,SAASwD;IACd,MAAMjF,UAAUW,OAAOuE,WAAW,CAChCC,MAAMC,IAAI,CAAC;QAACtF,QAAQ;IAAC,GAAG,CAACuF,GAAG/B,QAAU;YACpC,CAAC,UAAU,EAAEA,OAAO;YACpB,IAAIgC,MAAM,CAAChC,UAAU,IAAI,QAAQ,IAAI;SACtC;IAGH,OAAOkB,2BAA2B;QAChCM,WAAW;QACXH,SAAS;QACTI,YAAY;QACZC,gBAAgB,KAAKO,MAAM,CAACnG,qCAAqC;QACjEY;QACAyB,MAAM,IAAIqC,WAAW7E;QACrB2F,cAAc;IAChB;AACF"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-integration-core-dto",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "8.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -19,18 +19,15 @@
|
|
|
19
19
|
".": {
|
|
20
20
|
"types": "./dist/index.d.ts",
|
|
21
21
|
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./inter-module": {
|
|
24
|
+
"types": "./dist/inter-module.d.ts",
|
|
25
|
+
"default": "./dist/inter-module.js"
|
|
22
26
|
}
|
|
23
27
|
},
|
|
24
28
|
"dependencies": {
|
|
25
|
-
"zod": "^4.4.3"
|
|
26
|
-
|
|
27
|
-
"devDependencies": {
|
|
28
|
-
"@shipfox/biome": "1.8.2",
|
|
29
|
-
"@shipfox/depcruise": "1.0.2",
|
|
30
|
-
"@shipfox/swc": "1.2.6",
|
|
31
|
-
"@shipfox/ts-config": "1.3.8",
|
|
32
|
-
"@shipfox/typescript": "1.1.7",
|
|
33
|
-
"@shipfox/vitest": "1.2.3"
|
|
29
|
+
"zod": "^4.4.3",
|
|
30
|
+
"@shipfox/inter-module": "0.2.0"
|
|
34
31
|
},
|
|
35
32
|
"scripts": {
|
|
36
33
|
"build": "shipfox-swc",
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {integrationsInterModuleContract} from './inter-module.js';
|
|
2
|
+
|
|
3
|
+
describe('integrationsInterModuleContract', () => {
|
|
4
|
+
test('accepts a source repository lookup through the producer contract', () => {
|
|
5
|
+
const result = integrationsInterModuleContract.methods.resolveSourceRepository.output.parse({
|
|
6
|
+
connection: {
|
|
7
|
+
id: '00000000-0000-4000-8000-000000000001',
|
|
8
|
+
provider: 'github',
|
|
9
|
+
slug: 'github-main',
|
|
10
|
+
},
|
|
11
|
+
repository: {
|
|
12
|
+
externalRepositoryId: 'shipfox/project',
|
|
13
|
+
owner: 'shipfox',
|
|
14
|
+
name: 'project',
|
|
15
|
+
fullName: 'shipfox/project',
|
|
16
|
+
defaultBranch: 'main',
|
|
17
|
+
visibility: 'private',
|
|
18
|
+
cloneUrl: 'https://github.com/shipfox/project.git',
|
|
19
|
+
htmlUrl: 'https://github.com/shipfox/project',
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(result.repository.fullName).toBe('shipfox/project');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test.each([
|
|
27
|
+
['connection-not-found', {connectionId: '00000000-0000-4000-8000-000000000001'}],
|
|
28
|
+
['provider-unavailable', {provider: 'github'}],
|
|
29
|
+
['provider-failure', {reason: 'rate-limited', retryAfterSeconds: 30}],
|
|
30
|
+
] as const)('defines the %s source failure', (code, details) => {
|
|
31
|
+
const schema =
|
|
32
|
+
integrationsInterModuleContract.methods.resolveSourceRepository.errors[
|
|
33
|
+
code as keyof typeof integrationsInterModuleContract.methods.resolveSourceRepository.errors
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
expect(schema.parse(details)).toEqual(details);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {defineInterModuleContract, type InterModuleClient} from '@shipfox/inter-module';
|
|
2
|
+
import {z} from 'zod';
|
|
3
|
+
|
|
4
|
+
const id = z.string().uuid();
|
|
5
|
+
const provider = z.string().min(1);
|
|
6
|
+
const capability = z.enum(['source_control', 'agent_tools']);
|
|
7
|
+
const repository = z.object({
|
|
8
|
+
externalRepositoryId: z.string(),
|
|
9
|
+
owner: z.string(),
|
|
10
|
+
name: z.string(),
|
|
11
|
+
fullName: z.string(),
|
|
12
|
+
defaultBranch: z.string(),
|
|
13
|
+
visibility: z.enum(['public', 'private', 'internal', 'unknown']),
|
|
14
|
+
cloneUrl: z.string(),
|
|
15
|
+
htmlUrl: z.string(),
|
|
16
|
+
});
|
|
17
|
+
const sourceInput = z.object({workspaceId: id, connectionId: id, externalRepositoryId: z.string()});
|
|
18
|
+
const providerError = z.object({
|
|
19
|
+
reason: z.string(),
|
|
20
|
+
retryAfterSeconds: z.number().int().positive().optional(),
|
|
21
|
+
});
|
|
22
|
+
const sourceErrors = {
|
|
23
|
+
'connection-not-found': z.object({connectionId: id}),
|
|
24
|
+
'connection-inactive': z.object({connectionId: id}),
|
|
25
|
+
'connection-workspace-mismatch': z.object({connectionId: id}),
|
|
26
|
+
'provider-unavailable': z.object({provider}),
|
|
27
|
+
'capability-unavailable': z.object({provider, capability}),
|
|
28
|
+
'checkout-unsupported': z.object({provider}),
|
|
29
|
+
'provider-failure': providerError,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Producer-owned synchronous operations for the Integrations bounded context. */
|
|
33
|
+
export const integrationsInterModuleContract = defineInterModuleContract({
|
|
34
|
+
module: 'integrations',
|
|
35
|
+
methods: {
|
|
36
|
+
resolveSourceRepository: {
|
|
37
|
+
input: sourceInput,
|
|
38
|
+
output: z.object({connection: z.object({id, provider, slug: z.string()}), repository}),
|
|
39
|
+
errors: sourceErrors,
|
|
40
|
+
},
|
|
41
|
+
listSourceFiles: {
|
|
42
|
+
input: sourceInput.extend({
|
|
43
|
+
ref: z.string(),
|
|
44
|
+
prefix: z.string(),
|
|
45
|
+
limit: z.number().int().positive(),
|
|
46
|
+
cursor: z.string().optional(),
|
|
47
|
+
}),
|
|
48
|
+
output: z.object({
|
|
49
|
+
files: z.array(
|
|
50
|
+
z.object({path: z.string(), type: z.literal('file'), size: z.number().int().nullable()}),
|
|
51
|
+
),
|
|
52
|
+
nextCursor: z.string().nullable(),
|
|
53
|
+
}),
|
|
54
|
+
errors: sourceErrors,
|
|
55
|
+
},
|
|
56
|
+
fetchSourceFile: {
|
|
57
|
+
input: sourceInput.extend({ref: z.string(), path: z.string()}),
|
|
58
|
+
output: z.object({path: z.string(), ref: z.string(), content: z.string()}),
|
|
59
|
+
errors: sourceErrors,
|
|
60
|
+
},
|
|
61
|
+
createCheckoutSpec: {
|
|
62
|
+
input: sourceInput.extend({
|
|
63
|
+
ref: z.string().optional(),
|
|
64
|
+
permissions: z.object({contents: z.enum(['read', 'write'])}).optional(),
|
|
65
|
+
}),
|
|
66
|
+
output: z.object({
|
|
67
|
+
repositoryUrl: z.string(),
|
|
68
|
+
ref: z.string(),
|
|
69
|
+
credentials: z
|
|
70
|
+
.object({username: z.string(), token: z.string(), expiresAt: z.string().datetime()})
|
|
71
|
+
.optional(),
|
|
72
|
+
gitAuthor: z.object({name: z.string(), email: z.string()}).optional(),
|
|
73
|
+
}),
|
|
74
|
+
errors: sourceErrors,
|
|
75
|
+
},
|
|
76
|
+
getAgentToolsContext: {
|
|
77
|
+
input: z.object({workspaceId: id, defaultConnectionId: id}),
|
|
78
|
+
output: z.object({
|
|
79
|
+
selectionCatalogs: z.array(
|
|
80
|
+
z.object({
|
|
81
|
+
provider,
|
|
82
|
+
selectors: z.array(
|
|
83
|
+
z.object({
|
|
84
|
+
token: z.string(),
|
|
85
|
+
kind: z.enum(['family', 'family_wildcard', 'method', 'standalone']),
|
|
86
|
+
sensitivity: z.enum(['read', 'write']),
|
|
87
|
+
sensitive: z.boolean(),
|
|
88
|
+
}),
|
|
89
|
+
),
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
catalogs: z.array(
|
|
93
|
+
z.object({
|
|
94
|
+
provider,
|
|
95
|
+
tools: z.array(
|
|
96
|
+
z.object({
|
|
97
|
+
id: z.string(),
|
|
98
|
+
description: z.string(),
|
|
99
|
+
sensitivity: z.enum(['read', 'write']),
|
|
100
|
+
sensitive: z.boolean(),
|
|
101
|
+
requiredScope: z.unknown(),
|
|
102
|
+
inputSchema: z.record(z.string(), z.unknown()),
|
|
103
|
+
outputSchema: z.record(z.string(), z.unknown()).optional(),
|
|
104
|
+
methods: z
|
|
105
|
+
.array(
|
|
106
|
+
z.object({
|
|
107
|
+
id: z.string(),
|
|
108
|
+
description: z.string(),
|
|
109
|
+
sensitivity: z.enum(['read', 'write']),
|
|
110
|
+
sensitive: z.boolean(),
|
|
111
|
+
requiredScope: z.unknown(),
|
|
112
|
+
}),
|
|
113
|
+
)
|
|
114
|
+
.optional(),
|
|
115
|
+
}),
|
|
116
|
+
),
|
|
117
|
+
}),
|
|
118
|
+
),
|
|
119
|
+
workspaceConnections: z.array(
|
|
120
|
+
z.object({slug: z.string(), id, provider, capabilities: z.array(capability)}),
|
|
121
|
+
),
|
|
122
|
+
defaultConnection: z.object({id, slug: z.string(), provider}).nullable(),
|
|
123
|
+
}),
|
|
124
|
+
errors: sourceErrors,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export type IntegrationsModuleClient = InterModuleClient<typeof integrationsInterModuleContract>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createMaximumSizeStoredWebhookRequestFixture,
|
|
3
|
+
createStoredWebhookRequest,
|
|
4
|
+
decodeWebhookBody,
|
|
5
|
+
storedWebhookRequestSchema,
|
|
6
|
+
WEBHOOK_MAX_RAW_BODY_BYTES,
|
|
7
|
+
WEBHOOK_MAX_SERIALIZED_REQUEST_BYTES,
|
|
8
|
+
webhookProcessingResultSchema,
|
|
9
|
+
} from './webhooks.js';
|
|
10
|
+
|
|
11
|
+
const requestId = '9b11d65a-f7e7-40ea-b421-06af012a9be5';
|
|
12
|
+
const receivedAt = '2026-07-20T10:30:00.123Z';
|
|
13
|
+
|
|
14
|
+
describe('storedWebhookRequestSchema', () => {
|
|
15
|
+
it('round-trips exact body bytes from a direct adapter through the stored contract', () => {
|
|
16
|
+
const body = new Uint8Array([0, 255, 5, 128, 42]);
|
|
17
|
+
const directRequest = createStoredWebhookRequest({
|
|
18
|
+
requestId,
|
|
19
|
+
routeId: 'slack.command',
|
|
20
|
+
receivedAt,
|
|
21
|
+
rawQueryString: '',
|
|
22
|
+
headers: {'content-type': 'application/x-www-form-urlencoded'},
|
|
23
|
+
body,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const queuedRequest = storedWebhookRequestSchema.parse(
|
|
27
|
+
JSON.parse(JSON.stringify(directRequest)),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(queuedRequest.received_at).toBe(receivedAt);
|
|
31
|
+
expect(decodeWebhookBody(queuedRequest.body)).toEqual(body);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('rejects unknown fields, routes, and schema versions', () => {
|
|
35
|
+
const request = createStoredWebhookRequest({
|
|
36
|
+
requestId,
|
|
37
|
+
routeId: 'github',
|
|
38
|
+
receivedAt,
|
|
39
|
+
rawQueryString: '',
|
|
40
|
+
headers: {'content-type': 'application/json'},
|
|
41
|
+
body: new Uint8Array(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(storedWebhookRequestSchema.safeParse({...request, added_later: true}).success).toBe(
|
|
45
|
+
false,
|
|
46
|
+
);
|
|
47
|
+
expect(storedWebhookRequestSchema.safeParse({...request, schema_version: 2}).success).toBe(
|
|
48
|
+
false,
|
|
49
|
+
);
|
|
50
|
+
expect(storedWebhookRequestSchema.safeParse({...request, route_id: 'stripe'}).success).toBe(
|
|
51
|
+
false,
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('requires a generic connection identifier only for the generic route', () => {
|
|
56
|
+
const genericRequest = createStoredWebhookRequest({
|
|
57
|
+
requestId,
|
|
58
|
+
routeId: 'webhook.connection',
|
|
59
|
+
receivedAt,
|
|
60
|
+
rawQueryString: '',
|
|
61
|
+
headers: {'content-type': 'application/json'},
|
|
62
|
+
body: new Uint8Array(),
|
|
63
|
+
connectionId: 'c0a8012e-0b6d-4d8f-8d5c-6d74102602b0',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const withoutConnectionId = {
|
|
67
|
+
...genericRequest,
|
|
68
|
+
path_parameters: {},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
expect(storedWebhookRequestSchema.safeParse(withoutConnectionId).success).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('keeps the maximum-size fixture below the SQS message limit', () => {
|
|
75
|
+
const fixture = createMaximumSizeStoredWebhookRequestFixture();
|
|
76
|
+
const serializedSize = new TextEncoder().encode(JSON.stringify(fixture)).byteLength;
|
|
77
|
+
|
|
78
|
+
expect(decodeWebhookBody(fixture.body)).toHaveLength(WEBHOOK_MAX_RAW_BODY_BYTES);
|
|
79
|
+
expect(serializedSize).toBeLessThan(WEBHOOK_MAX_SERIALIZED_REQUEST_BYTES);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('webhookProcessingResultSchema', () => {
|
|
84
|
+
it.each([
|
|
85
|
+
{outcome: 'processed'},
|
|
86
|
+
{outcome: 'processed', challenge: 'slack-url-verification'},
|
|
87
|
+
{outcome: 'duplicate', deliveryId: 'delivery-1'},
|
|
88
|
+
{outcome: 'discarded', reason: 'invalid_signature'},
|
|
89
|
+
])('accepts the %s processing outcome', (result) => {
|
|
90
|
+
expect(webhookProcessingResultSchema.safeParse(result).success).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
});
|