@shipfox/api-integration-webhook 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 +50 -0
- package/LICENSE +21 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +18 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/presentation/dto/connections.d.ts +4 -0
- package/dist/presentation/dto/connections.d.ts.map +1 -0
- package/dist/presentation/dto/connections.js +15 -0
- package/dist/presentation/dto/connections.js.map +1 -0
- package/dist/presentation/routes/connections.d.ts +14 -0
- package/dist/presentation/routes/connections.d.ts.map +1 -0
- package/dist/presentation/routes/connections.js +153 -0
- package/dist/presentation/routes/connections.js.map +1 -0
- package/dist/presentation/routes/inbound.d.ts +11 -0
- package/dist/presentation/routes/inbound.d.ts.map +1 -0
- package/dist/presentation/routes/inbound.js +117 -0
- package/dist/presentation/routes/inbound.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/package.json +62 -0
- package/src/config.ts +8 -0
- package/src/constants.test.ts +23 -0
- package/src/constants.ts +21 -0
- package/src/index.ts +52 -0
- package/src/presentation/dto/connections.ts +20 -0
- package/src/presentation/routes/connections.test.ts +374 -0
- package/src/presentation/routes/connections.ts +184 -0
- package/src/presentation/routes/inbound.test.ts +210 -0
- package/src/presentation/routes/inbound.ts +132 -0
- package/test/env.ts +7 -0
- package/test/globalSetup.ts +5 -0
- package/test/setup.ts +6 -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 +12 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$ shipfox-tsc-emit
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$ shipfox-tsc-check
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @shipfox/api-integration-webhook
|
|
2
|
+
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1b0d344: Publishes the complete API runtime closure with packed-consumer-safe internal imports and records its exact package set in application releases.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [1b0d344]
|
|
12
|
+
- @shipfox/api-auth-context@2.0.0
|
|
13
|
+
- @shipfox/api-integration-core-dto@2.0.0
|
|
14
|
+
- @shipfox/api-integration-webhook-dto@2.0.0
|
|
15
|
+
- @shipfox/config@1.2.1
|
|
16
|
+
- @shipfox/node-fastify@0.2.1
|
|
17
|
+
|
|
18
|
+
## 0.0.1
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- 82d22e4: Make the workspace-membership gate stateless. `requireWorkspaceAccess` now lives in `@shipfox/api-auth-context` and authorizes a request purely from the verified session-token claims, replacing the `requireMembership` gate in `@shipfox/api-workspaces` that read the workspace row from the database on every workspace-scoped request. Membership and role already travel in the token, so the check needs no database access.
|
|
23
|
+
|
|
24
|
+
This removes the per-request database read and severs the runtime dependency on `@shipfox/api-workspaces` from feature modules that only needed the membership gate (integration providers, secrets, projects, agent, runners). Workspace existence and `active`-status enforcement, which no code path currently exercises, moves off the hot path; enforce it at token issuance when workspace suspension is introduced.
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [34ba284]
|
|
27
|
+
- Updated dependencies [b9c3f32]
|
|
28
|
+
- Updated dependencies [861091c]
|
|
29
|
+
- Updated dependencies [a81b68c]
|
|
30
|
+
- Updated dependencies [115655e]
|
|
31
|
+
- Updated dependencies [c0a883c]
|
|
32
|
+
- Updated dependencies [72ce351]
|
|
33
|
+
- Updated dependencies [e47f8da]
|
|
34
|
+
- Updated dependencies [ce062a9]
|
|
35
|
+
- Updated dependencies [f3614ae]
|
|
36
|
+
- Updated dependencies [f92122b]
|
|
37
|
+
- Updated dependencies [f8f339a]
|
|
38
|
+
- Updated dependencies [857fd73]
|
|
39
|
+
- Updated dependencies [b8e49ff]
|
|
40
|
+
- Updated dependencies [d6d4862]
|
|
41
|
+
- Updated dependencies [c0a883c]
|
|
42
|
+
- Updated dependencies [82d22e4]
|
|
43
|
+
- Updated dependencies [01be723]
|
|
44
|
+
- Updated dependencies [2933c33]
|
|
45
|
+
- Updated dependencies [6181819]
|
|
46
|
+
- @shipfox/node-fastify@0.2.0
|
|
47
|
+
- @shipfox/api-integration-webhook-dto@0.0.1
|
|
48
|
+
- @shipfox/api-auth-context@0.1.0
|
|
49
|
+
- @shipfox/api-integration-core-dto@0.1.0
|
|
50
|
+
- @shipfox/config@1.2.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shipfox
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;;kDAKjB,CAAC"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createConfig, url } from '@shipfox/config';
|
|
2
|
+
export const config = createConfig({
|
|
3
|
+
WEBHOOK_PUBLIC_URL: url({
|
|
4
|
+
desc: 'Public origin of the API used to build webhook inbound URLs returned to users. Set it to the externally reachable API URL, including the scheme.',
|
|
5
|
+
default: 'http://localhost:3000'
|
|
6
|
+
})
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {createConfig, url} from '@shipfox/config';\n\nexport const config = createConfig({\n WEBHOOK_PUBLIC_URL: url({\n desc: 'Public origin of the API used to build webhook inbound URLs returned to users. Set it to the externally reachable API URL, including the scheme.',\n default: 'http://localhost:3000',\n }),\n});\n"],"names":["createConfig","url","config","WEBHOOK_PUBLIC_URL","desc","default"],"mappings":"AAAA,SAAQA,YAAY,EAAEC,GAAG,QAAO,kBAAkB;AAElD,OAAO,MAAMC,SAASF,aAAa;IACjCG,oBAAoBF,IAAI;QACtBG,MAAM;QACNC,SAAS;IACX;AACF,GAAG"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const WEBHOOK_INBOUND_BODY_LIMIT: number;
|
|
2
|
+
export declare const WEBHOOK_FORWARDED_HEADERS: readonly ["content-type", "user-agent", "x-delivery-id", "x-request-id"];
|
|
3
|
+
export declare function redactHeaders(headers: Record<string, string | string[] | undefined>): Record<string, string | string[]>;
|
|
4
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,0BAA0B,QAAkB,CAAC;AAE1D,eAAO,MAAM,yBAAyB,0EAK5B,CAAC;AAIX,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GACrD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAOnC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const WEBHOOK_INBOUND_BODY_LIMIT = 1 * 1024 * 1024;
|
|
2
|
+
export const WEBHOOK_FORWARDED_HEADERS = [
|
|
3
|
+
'content-type',
|
|
4
|
+
'user-agent',
|
|
5
|
+
'x-delivery-id',
|
|
6
|
+
'x-request-id'
|
|
7
|
+
];
|
|
8
|
+
const forwardedHeaderSet = new Set(WEBHOOK_FORWARDED_HEADERS);
|
|
9
|
+
export function redactHeaders(headers) {
|
|
10
|
+
const redacted = {};
|
|
11
|
+
for (const [name, value] of Object.entries(headers)){
|
|
12
|
+
if (value === undefined) continue;
|
|
13
|
+
redacted[name] = forwardedHeaderSet.has(name.toLowerCase()) ? value : '[redacted]';
|
|
14
|
+
}
|
|
15
|
+
return redacted;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts"],"sourcesContent":["export const WEBHOOK_INBOUND_BODY_LIMIT = 1 * 1024 * 1024;\n\nexport const WEBHOOK_FORWARDED_HEADERS = [\n 'content-type',\n 'user-agent',\n 'x-delivery-id',\n 'x-request-id',\n] as const;\n\nconst forwardedHeaderSet = new Set<string>(WEBHOOK_FORWARDED_HEADERS);\n\nexport function redactHeaders(\n headers: Record<string, string | string[] | undefined>,\n): Record<string, string | string[]> {\n const redacted: Record<string, string | string[]> = {};\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined) continue;\n redacted[name] = forwardedHeaderSet.has(name.toLowerCase()) ? value : '[redacted]';\n }\n return redacted;\n}\n"],"names":["WEBHOOK_INBOUND_BODY_LIMIT","WEBHOOK_FORWARDED_HEADERS","forwardedHeaderSet","Set","redactHeaders","headers","redacted","name","value","Object","entries","undefined","has","toLowerCase"],"mappings":"AAAA,OAAO,MAAMA,6BAA6B,IAAI,OAAO,KAAK;AAE1D,OAAO,MAAMC,4BAA4B;IACvC;IACA;IACA;IACA;CACD,CAAU;AAEX,MAAMC,qBAAqB,IAAIC,IAAYF;AAE3C,OAAO,SAASG,cACdC,OAAsD;IAEtD,MAAMC,WAA8C,CAAC;IACrD,KAAK,MAAM,CAACC,MAAMC,MAAM,IAAIC,OAAOC,OAAO,CAACL,SAAU;QACnD,IAAIG,UAAUG,WAAW;QACzBL,QAAQ,CAACC,KAAK,GAAGL,mBAAmBU,GAAG,CAACL,KAAKM,WAAW,MAAML,QAAQ;IACxE;IACA,OAAOF;AACT"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CreateIntegrationConnectionFn, DeleteIntegrationConnectionFn, GetIntegrationConnectionByIdFn, IntegrationConnection, IntegrationTx, PublishIntegrationEventReceivedFn, UpdateIntegrationConnectionLifecycleStatusFn } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
export { redactHeaders, WEBHOOK_FORWARDED_HEADERS, WEBHOOK_INBOUND_BODY_LIMIT } from '#constants.js';
|
|
3
|
+
export interface CreateWebhookIntegrationProviderOptions {
|
|
4
|
+
coreDb: () => {
|
|
5
|
+
transaction<T>(callback: (tx: IntegrationTx) => Promise<T>): Promise<T>;
|
|
6
|
+
};
|
|
7
|
+
createIntegrationConnection: CreateIntegrationConnectionFn;
|
|
8
|
+
listIntegrationConnections: (params: {
|
|
9
|
+
workspaceId: string;
|
|
10
|
+
}) => Promise<IntegrationConnection[]>;
|
|
11
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
12
|
+
updateIntegrationConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;
|
|
13
|
+
deleteIntegrationConnection: DeleteIntegrationConnectionFn;
|
|
14
|
+
publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;
|
|
15
|
+
baseUrl?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
export declare function createWebhookIntegrationProvider(options: CreateWebhookIntegrationProviderOptions): {
|
|
18
|
+
provider: "webhook";
|
|
19
|
+
displayName: string;
|
|
20
|
+
routes: import("@shipfox/node-fastify").RouteGroup[];
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,6BAA6B,EAC7B,6BAA6B,EAC7B,8BAA8B,EAC9B,qBAAqB,EACrB,aAAa,EACb,iCAAiC,EACjC,4CAA4C,EAC7C,MAAM,mCAAmC,CAAC;AAM3C,OAAO,EAAC,aAAa,EAAE,yBAAyB,EAAE,0BAA0B,EAAC,MAAM,eAAe,CAAC;AAEnG,MAAM,WAAW,uCAAuC;IACtD,MAAM,EAAE,MAAM;QACZ,WAAW,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;KACzE,CAAC;IACF,2BAA2B,EAAE,6BAA6B,CAAC;IAC3D,0BAA0B,EAAE,CAAC,MAAM,EAAE;QAAC,WAAW,EAAE,MAAM,CAAA;KAAC,KAAK,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAAC;IAChG,4BAA4B,EAAE,8BAA8B,CAAC;IAC7D,0CAA0C,EAAE,4CAA4C,CAAC;IACzF,2BAA2B,EAAE,6BAA6B,CAAC;IAC3D,+BAA+B,EAAE,iCAAiC,CAAC;IACnE,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,wBAAgB,gCAAgC,CAAC,OAAO,EAAE,uCAAuC;;;;EAsBhG"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { WEBHOOK_PROVIDER } from '@shipfox/api-integration-webhook-dto';
|
|
2
|
+
import { config } from '#config.js';
|
|
3
|
+
import { createWebhookConnectionRoutes } from '#presentation/routes/connections.js';
|
|
4
|
+
import { createWebhookInboundRoutes } from '#presentation/routes/inbound.js';
|
|
5
|
+
export { redactHeaders, WEBHOOK_FORWARDED_HEADERS, WEBHOOK_INBOUND_BODY_LIMIT } from '#constants.js';
|
|
6
|
+
export function createWebhookIntegrationProvider(options) {
|
|
7
|
+
const baseUrl = options.baseUrl ?? config.WEBHOOK_PUBLIC_URL;
|
|
8
|
+
return {
|
|
9
|
+
provider: WEBHOOK_PROVIDER,
|
|
10
|
+
displayName: 'Webhook',
|
|
11
|
+
routes: [
|
|
12
|
+
createWebhookConnectionRoutes({
|
|
13
|
+
baseUrl,
|
|
14
|
+
createIntegrationConnection: options.createIntegrationConnection,
|
|
15
|
+
listIntegrationConnections: options.listIntegrationConnections,
|
|
16
|
+
getIntegrationConnectionById: options.getIntegrationConnectionById,
|
|
17
|
+
updateIntegrationConnectionLifecycleStatus: options.updateIntegrationConnectionLifecycleStatus,
|
|
18
|
+
deleteIntegrationConnection: options.deleteIntegrationConnection
|
|
19
|
+
}),
|
|
20
|
+
createWebhookInboundRoutes({
|
|
21
|
+
coreDb: options.coreDb,
|
|
22
|
+
getIntegrationConnectionById: options.getIntegrationConnectionById,
|
|
23
|
+
publishIntegrationEventReceived: options.publishIntegrationEventReceived
|
|
24
|
+
})
|
|
25
|
+
]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type {\n CreateIntegrationConnectionFn,\n DeleteIntegrationConnectionFn,\n GetIntegrationConnectionByIdFn,\n IntegrationConnection,\n IntegrationTx,\n PublishIntegrationEventReceivedFn,\n UpdateIntegrationConnectionLifecycleStatusFn,\n} from '@shipfox/api-integration-core-dto';\nimport {WEBHOOK_PROVIDER} from '@shipfox/api-integration-webhook-dto';\nimport {config} from '#config.js';\nimport {createWebhookConnectionRoutes} from '#presentation/routes/connections.js';\nimport {createWebhookInboundRoutes} from '#presentation/routes/inbound.js';\n\nexport {redactHeaders, WEBHOOK_FORWARDED_HEADERS, WEBHOOK_INBOUND_BODY_LIMIT} from '#constants.js';\n\nexport interface CreateWebhookIntegrationProviderOptions {\n coreDb: () => {\n transaction<T>(callback: (tx: IntegrationTx) => Promise<T>): Promise<T>;\n };\n createIntegrationConnection: CreateIntegrationConnectionFn;\n listIntegrationConnections: (params: {workspaceId: string}) => Promise<IntegrationConnection[]>;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n updateIntegrationConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;\n deleteIntegrationConnection: DeleteIntegrationConnectionFn;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n baseUrl?: string | undefined;\n}\n\nexport function createWebhookIntegrationProvider(options: CreateWebhookIntegrationProviderOptions) {\n const baseUrl = options.baseUrl ?? config.WEBHOOK_PUBLIC_URL;\n return {\n provider: WEBHOOK_PROVIDER,\n displayName: 'Webhook',\n routes: [\n createWebhookConnectionRoutes({\n baseUrl,\n createIntegrationConnection: options.createIntegrationConnection,\n listIntegrationConnections: options.listIntegrationConnections,\n getIntegrationConnectionById: options.getIntegrationConnectionById,\n updateIntegrationConnectionLifecycleStatus:\n options.updateIntegrationConnectionLifecycleStatus,\n deleteIntegrationConnection: options.deleteIntegrationConnection,\n }),\n createWebhookInboundRoutes({\n coreDb: options.coreDb,\n getIntegrationConnectionById: options.getIntegrationConnectionById,\n publishIntegrationEventReceived: options.publishIntegrationEventReceived,\n }),\n ],\n };\n}\n"],"names":["WEBHOOK_PROVIDER","config","createWebhookConnectionRoutes","createWebhookInboundRoutes","redactHeaders","WEBHOOK_FORWARDED_HEADERS","WEBHOOK_INBOUND_BODY_LIMIT","createWebhookIntegrationProvider","options","baseUrl","WEBHOOK_PUBLIC_URL","provider","displayName","routes","createIntegrationConnection","listIntegrationConnections","getIntegrationConnectionById","updateIntegrationConnectionLifecycleStatus","deleteIntegrationConnection","coreDb","publishIntegrationEventReceived"],"mappings":"AASA,SAAQA,gBAAgB,QAAO,uCAAuC;AACtE,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,6BAA6B,QAAO,sCAAsC;AAClF,SAAQC,0BAA0B,QAAO,kCAAkC;AAE3E,SAAQC,aAAa,EAAEC,yBAAyB,EAAEC,0BAA0B,QAAO,gBAAgB;AAenG,OAAO,SAASC,iCAAiCC,OAAgD;IAC/F,MAAMC,UAAUD,QAAQC,OAAO,IAAIR,OAAOS,kBAAkB;IAC5D,OAAO;QACLC,UAAUX;QACVY,aAAa;QACbC,QAAQ;YACNX,8BAA8B;gBAC5BO;gBACAK,6BAA6BN,QAAQM,2BAA2B;gBAChEC,4BAA4BP,QAAQO,0BAA0B;gBAC9DC,8BAA8BR,QAAQQ,4BAA4B;gBAClEC,4CACET,QAAQS,0CAA0C;gBACpDC,6BAA6BV,QAAQU,2BAA2B;YAClE;YACAf,2BAA2B;gBACzBgB,QAAQX,QAAQW,MAAM;gBACtBH,8BAA8BR,QAAQQ,4BAA4B;gBAClEI,iCAAiCZ,QAAQY,+BAA+B;YAC1E;SACD;IACH;AACF"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { IntegrationConnection } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import type { WebhookConnectionDto } from '@shipfox/api-integration-webhook-dto';
|
|
3
|
+
export declare function toWebhookConnectionDto(connection: IntegrationConnection, baseUrl: string): WebhookConnectionDto;
|
|
4
|
+
//# sourceMappingURL=connections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../../../src/presentation/dto/connections.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,mCAAmC,CAAC;AAC7E,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,sCAAsC,CAAC;AAI/E,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,qBAAqB,EACjC,OAAO,EAAE,MAAM,GACd,oBAAoB,CAWtB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const TRAILING_SLASHES_RE = /\/+$/;
|
|
2
|
+
export function toWebhookConnectionDto(connection, baseUrl) {
|
|
3
|
+
return {
|
|
4
|
+
id: connection.id,
|
|
5
|
+
workspace_id: connection.workspaceId,
|
|
6
|
+
name: connection.displayName,
|
|
7
|
+
slug: connection.slug,
|
|
8
|
+
lifecycle_status: connection.lifecycleStatus,
|
|
9
|
+
inbound_url: `${baseUrl.replace(TRAILING_SLASHES_RE, '')}/webhook/${connection.id}`,
|
|
10
|
+
created_at: connection.createdAt.toISOString(),
|
|
11
|
+
updated_at: connection.updatedAt.toISOString()
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//# sourceMappingURL=connections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/dto/connections.ts"],"sourcesContent":["import type {IntegrationConnection} from '@shipfox/api-integration-core-dto';\nimport type {WebhookConnectionDto} from '@shipfox/api-integration-webhook-dto';\n\nconst TRAILING_SLASHES_RE = /\\/+$/;\n\nexport function toWebhookConnectionDto(\n connection: IntegrationConnection,\n baseUrl: string,\n): WebhookConnectionDto {\n return {\n id: connection.id,\n workspace_id: connection.workspaceId,\n name: connection.displayName,\n slug: connection.slug,\n lifecycle_status: connection.lifecycleStatus,\n inbound_url: `${baseUrl.replace(TRAILING_SLASHES_RE, '')}/webhook/${connection.id}`,\n created_at: connection.createdAt.toISOString(),\n updated_at: connection.updatedAt.toISOString(),\n };\n}\n"],"names":["TRAILING_SLASHES_RE","toWebhookConnectionDto","connection","baseUrl","id","workspace_id","workspaceId","name","displayName","slug","lifecycle_status","lifecycleStatus","inbound_url","replace","created_at","createdAt","toISOString","updated_at","updatedAt"],"mappings":"AAGA,MAAMA,sBAAsB;AAE5B,OAAO,SAASC,uBACdC,UAAiC,EACjCC,OAAe;IAEf,OAAO;QACLC,IAAIF,WAAWE,EAAE;QACjBC,cAAcH,WAAWI,WAAW;QACpCC,MAAML,WAAWM,WAAW;QAC5BC,MAAMP,WAAWO,IAAI;QACrBC,kBAAkBR,WAAWS,eAAe;QAC5CC,aAAa,GAAGT,QAAQU,OAAO,CAACb,qBAAqB,IAAI,SAAS,EAAEE,WAAWE,EAAE,EAAE;QACnFU,YAAYZ,WAAWa,SAAS,CAACC,WAAW;QAC5CC,YAAYf,WAAWgB,SAAS,CAACF,WAAW;IAC9C;AACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type CreateIntegrationConnectionFn, type DeleteIntegrationConnectionFn, type GetIntegrationConnectionByIdFn, type IntegrationConnection, type UpdateIntegrationConnectionLifecycleStatusFn } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import { type RouteGroup } from '@shipfox/node-fastify';
|
|
3
|
+
export interface CreateWebhookConnectionRoutesOptions {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
createIntegrationConnection: CreateIntegrationConnectionFn;
|
|
6
|
+
listIntegrationConnections: (params: {
|
|
7
|
+
workspaceId: string;
|
|
8
|
+
}) => Promise<IntegrationConnection[]>;
|
|
9
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
10
|
+
updateIntegrationConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;
|
|
11
|
+
deleteIntegrationConnection: DeleteIntegrationConnectionFn;
|
|
12
|
+
}
|
|
13
|
+
export declare function createWebhookConnectionRoutes(options: CreateWebhookConnectionRoutesOptions): RouteGroup;
|
|
14
|
+
//# sourceMappingURL=connections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/connections.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,6BAA6B,EAClC,KAAK,6BAA6B,EAClC,KAAK,8BAA8B,EACnC,KAAK,qBAAqB,EAC1B,KAAK,4CAA4C,EAClD,MAAM,mCAAmC,CAAC;AAS3C,OAAO,EAA2B,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AAKhF,MAAM,WAAW,oCAAoC;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,2BAA2B,EAAE,6BAA6B,CAAC;IAC3D,0BAA0B,EAAE,CAAC,MAAM,EAAE;QAAC,WAAW,EAAE,MAAM,CAAA;KAAC,KAAK,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAAC;IAChG,4BAA4B,EAAE,8BAA8B,CAAC;IAC7D,0CAA0C,EAAE,4CAA4C,CAAC;IACzF,2BAA2B,EAAE,6BAA6B,CAAC;CAC5D;AA6BD,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,oCAAoC,GAC5C,UAAU,CA2HZ"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { AUTH_USER, requireWorkspaceAccess } from '@shipfox/api-auth-context';
|
|
2
|
+
import { ConnectionSlugConflictError } from '@shipfox/api-integration-core-dto';
|
|
3
|
+
import { createWebhookConnectionBodySchema, listWebhookConnectionsQuerySchema, listWebhookConnectionsResponseSchema, updateWebhookConnectionBodySchema, WEBHOOK_PROVIDER, webhookConnectionDtoSchema } from '@shipfox/api-integration-webhook-dto';
|
|
4
|
+
import { ClientError, defineRoute } from '@shipfox/node-fastify';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { toWebhookConnectionDto } from '#presentation/dto/connections.js';
|
|
7
|
+
const connectionParamsSchema = z.object({
|
|
8
|
+
connectionId: z.string().uuid()
|
|
9
|
+
});
|
|
10
|
+
function isConnectionAlreadyExistsError(error) {
|
|
11
|
+
return typeof error === 'object' && error !== null && (error.name === 'IntegrationConnectionAlreadyExistsError' || error instanceof ConnectionSlugConflictError);
|
|
12
|
+
}
|
|
13
|
+
async function requireWebhookConnection(params) {
|
|
14
|
+
const connection = await params.getIntegrationConnectionById(params.connectionId);
|
|
15
|
+
if (!connection || connection.provider !== WEBHOOK_PROVIDER) {
|
|
16
|
+
throw new ClientError('Webhook connection not found', 'not-found', {
|
|
17
|
+
status: 404
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
requireWorkspaceAccess({
|
|
21
|
+
request: params.request,
|
|
22
|
+
workspaceId: connection.workspaceId
|
|
23
|
+
});
|
|
24
|
+
return connection;
|
|
25
|
+
}
|
|
26
|
+
export function createWebhookConnectionRoutes(options) {
|
|
27
|
+
const createConnectionRoute = defineRoute({
|
|
28
|
+
method: 'POST',
|
|
29
|
+
path: '/connections',
|
|
30
|
+
auth: AUTH_USER,
|
|
31
|
+
description: 'Create a generic webhook connection.',
|
|
32
|
+
schema: {
|
|
33
|
+
body: createWebhookConnectionBodySchema,
|
|
34
|
+
response: {
|
|
35
|
+
201: webhookConnectionDtoSchema
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
errorHandler: (error)=>{
|
|
39
|
+
if (isConnectionAlreadyExistsError(error)) {
|
|
40
|
+
throw new ClientError('Webhook slug already exists', 'slug-already-exists', {
|
|
41
|
+
status: 409
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
},
|
|
46
|
+
handler: async (request, reply)=>{
|
|
47
|
+
const { workspace_id: workspaceId, name, slug } = request.body;
|
|
48
|
+
requireWorkspaceAccess({
|
|
49
|
+
request,
|
|
50
|
+
workspaceId
|
|
51
|
+
});
|
|
52
|
+
const connection = await options.createIntegrationConnection({
|
|
53
|
+
workspaceId,
|
|
54
|
+
provider: WEBHOOK_PROVIDER,
|
|
55
|
+
// The generic webhook provider uses the human slug as its provider-side account id.
|
|
56
|
+
externalAccountId: slug,
|
|
57
|
+
slug,
|
|
58
|
+
displayName: name
|
|
59
|
+
});
|
|
60
|
+
reply.status(201);
|
|
61
|
+
return toWebhookConnectionDto(connection, options.baseUrl);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const listConnectionsRoute = defineRoute({
|
|
65
|
+
method: 'GET',
|
|
66
|
+
path: '/connections',
|
|
67
|
+
auth: AUTH_USER,
|
|
68
|
+
description: 'List generic webhook connections for a workspace.',
|
|
69
|
+
schema: {
|
|
70
|
+
querystring: listWebhookConnectionsQuerySchema,
|
|
71
|
+
response: {
|
|
72
|
+
200: listWebhookConnectionsResponseSchema
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
handler: async (request)=>{
|
|
76
|
+
const { workspace_id: workspaceId } = request.query;
|
|
77
|
+
requireWorkspaceAccess({
|
|
78
|
+
request,
|
|
79
|
+
workspaceId
|
|
80
|
+
});
|
|
81
|
+
const connections = (await options.listIntegrationConnections({
|
|
82
|
+
workspaceId
|
|
83
|
+
})).filter((connection)=>connection.provider === WEBHOOK_PROVIDER);
|
|
84
|
+
return {
|
|
85
|
+
connections: connections.map((connection)=>toWebhookConnectionDto(connection, options.baseUrl))
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const updateConnectionRoute = defineRoute({
|
|
90
|
+
method: 'PATCH',
|
|
91
|
+
path: '/connections/:connectionId',
|
|
92
|
+
auth: AUTH_USER,
|
|
93
|
+
description: 'Update a generic webhook connection.',
|
|
94
|
+
schema: {
|
|
95
|
+
params: connectionParamsSchema,
|
|
96
|
+
body: updateWebhookConnectionBodySchema,
|
|
97
|
+
response: {
|
|
98
|
+
200: webhookConnectionDtoSchema
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
handler: async (request)=>{
|
|
102
|
+
const connection = await requireWebhookConnection({
|
|
103
|
+
request,
|
|
104
|
+
connectionId: request.params.connectionId,
|
|
105
|
+
getIntegrationConnectionById: options.getIntegrationConnectionById
|
|
106
|
+
});
|
|
107
|
+
const updated = await options.updateIntegrationConnectionLifecycleStatus({
|
|
108
|
+
id: connection.id,
|
|
109
|
+
lifecycleStatus: request.body.lifecycle_status
|
|
110
|
+
});
|
|
111
|
+
if (!updated) {
|
|
112
|
+
throw new ClientError('Webhook connection not found', 'not-found', {
|
|
113
|
+
status: 404
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return toWebhookConnectionDto(updated, options.baseUrl);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
const deleteConnectionRoute = defineRoute({
|
|
120
|
+
method: 'DELETE',
|
|
121
|
+
path: '/connections/:connectionId',
|
|
122
|
+
auth: AUTH_USER,
|
|
123
|
+
description: 'Delete a generic webhook connection.',
|
|
124
|
+
schema: {
|
|
125
|
+
params: connectionParamsSchema,
|
|
126
|
+
response: {
|
|
127
|
+
204: z.void()
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
handler: async (request, reply)=>{
|
|
131
|
+
const connection = await requireWebhookConnection({
|
|
132
|
+
request,
|
|
133
|
+
connectionId: request.params.connectionId,
|
|
134
|
+
getIntegrationConnectionById: options.getIntegrationConnectionById
|
|
135
|
+
});
|
|
136
|
+
await options.deleteIntegrationConnection({
|
|
137
|
+
id: connection.id
|
|
138
|
+
});
|
|
139
|
+
reply.code(204);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
prefix: '/integrations/webhook',
|
|
144
|
+
routes: [
|
|
145
|
+
createConnectionRoute,
|
|
146
|
+
listConnectionsRoute,
|
|
147
|
+
updateConnectionRoute,
|
|
148
|
+
deleteConnectionRoute
|
|
149
|
+
]
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
//# sourceMappingURL=connections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/connections.ts"],"sourcesContent":["import {AUTH_USER, requireWorkspaceAccess} from '@shipfox/api-auth-context';\nimport {\n ConnectionSlugConflictError,\n type CreateIntegrationConnectionFn,\n type DeleteIntegrationConnectionFn,\n type GetIntegrationConnectionByIdFn,\n type IntegrationConnection,\n type UpdateIntegrationConnectionLifecycleStatusFn,\n} from '@shipfox/api-integration-core-dto';\nimport {\n createWebhookConnectionBodySchema,\n listWebhookConnectionsQuerySchema,\n listWebhookConnectionsResponseSchema,\n updateWebhookConnectionBodySchema,\n WEBHOOK_PROVIDER,\n webhookConnectionDtoSchema,\n} from '@shipfox/api-integration-webhook-dto';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {FastifyRequest} from 'fastify';\nimport {z} from 'zod';\nimport {toWebhookConnectionDto} from '#presentation/dto/connections.js';\n\nexport interface CreateWebhookConnectionRoutesOptions {\n baseUrl: string;\n createIntegrationConnection: CreateIntegrationConnectionFn;\n listIntegrationConnections: (params: {workspaceId: string}) => Promise<IntegrationConnection[]>;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n updateIntegrationConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;\n deleteIntegrationConnection: DeleteIntegrationConnectionFn;\n}\n\nconst connectionParamsSchema = z.object({\n connectionId: z.string().uuid(),\n});\n\nfunction isConnectionAlreadyExistsError(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n ((error as {name?: unknown}).name === 'IntegrationConnectionAlreadyExistsError' ||\n error instanceof ConnectionSlugConflictError)\n );\n}\n\nasync function requireWebhookConnection(params: {\n request: FastifyRequest;\n connectionId: string;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n}): Promise<IntegrationConnection> {\n const connection = await params.getIntegrationConnectionById(params.connectionId);\n if (!connection || connection.provider !== WEBHOOK_PROVIDER) {\n throw new ClientError('Webhook connection not found', 'not-found', {status: 404});\n }\n\n requireWorkspaceAccess({request: params.request, workspaceId: connection.workspaceId});\n return connection;\n}\n\nexport function createWebhookConnectionRoutes(\n options: CreateWebhookConnectionRoutesOptions,\n): RouteGroup {\n const createConnectionRoute = defineRoute({\n method: 'POST',\n path: '/connections',\n auth: AUTH_USER,\n description: 'Create a generic webhook connection.',\n schema: {\n body: createWebhookConnectionBodySchema,\n response: {\n 201: webhookConnectionDtoSchema,\n },\n },\n errorHandler: (error) => {\n if (isConnectionAlreadyExistsError(error)) {\n throw new ClientError('Webhook slug already exists', 'slug-already-exists', {status: 409});\n }\n throw error;\n },\n handler: async (request, reply) => {\n const {workspace_id: workspaceId, name, slug} = request.body;\n\n requireWorkspaceAccess({request, workspaceId});\n const connection = await options.createIntegrationConnection({\n workspaceId,\n provider: WEBHOOK_PROVIDER,\n // The generic webhook provider uses the human slug as its provider-side account id.\n externalAccountId: slug,\n slug,\n displayName: name,\n });\n\n reply.status(201);\n return toWebhookConnectionDto(connection, options.baseUrl);\n },\n });\n\n const listConnectionsRoute = defineRoute({\n method: 'GET',\n path: '/connections',\n auth: AUTH_USER,\n description: 'List generic webhook connections for a workspace.',\n schema: {\n querystring: listWebhookConnectionsQuerySchema,\n response: {\n 200: listWebhookConnectionsResponseSchema,\n },\n },\n handler: async (request) => {\n const {workspace_id: workspaceId} = request.query;\n\n requireWorkspaceAccess({request, workspaceId});\n const connections = (await options.listIntegrationConnections({workspaceId})).filter(\n (connection) => connection.provider === WEBHOOK_PROVIDER,\n );\n return {\n connections: connections.map((connection) =>\n toWebhookConnectionDto(connection, options.baseUrl),\n ),\n };\n },\n });\n\n const updateConnectionRoute = defineRoute({\n method: 'PATCH',\n path: '/connections/:connectionId',\n auth: AUTH_USER,\n description: 'Update a generic webhook connection.',\n schema: {\n params: connectionParamsSchema,\n body: updateWebhookConnectionBodySchema,\n response: {\n 200: webhookConnectionDtoSchema,\n },\n },\n handler: async (request) => {\n const connection = await requireWebhookConnection({\n request,\n connectionId: request.params.connectionId,\n getIntegrationConnectionById: options.getIntegrationConnectionById,\n });\n const updated = await options.updateIntegrationConnectionLifecycleStatus({\n id: connection.id,\n lifecycleStatus: request.body.lifecycle_status,\n });\n if (!updated) {\n throw new ClientError('Webhook connection not found', 'not-found', {status: 404});\n }\n return toWebhookConnectionDto(updated, options.baseUrl);\n },\n });\n\n const deleteConnectionRoute = defineRoute({\n method: 'DELETE',\n path: '/connections/:connectionId',\n auth: AUTH_USER,\n description: 'Delete a generic webhook connection.',\n schema: {\n params: connectionParamsSchema,\n response: {\n 204: z.void(),\n },\n },\n handler: async (request, reply) => {\n const connection = await requireWebhookConnection({\n request,\n connectionId: request.params.connectionId,\n getIntegrationConnectionById: options.getIntegrationConnectionById,\n });\n\n await options.deleteIntegrationConnection({id: connection.id});\n reply.code(204);\n },\n });\n\n return {\n prefix: '/integrations/webhook',\n routes: [\n createConnectionRoute,\n listConnectionsRoute,\n updateConnectionRoute,\n deleteConnectionRoute,\n ],\n };\n}\n"],"names":["AUTH_USER","requireWorkspaceAccess","ConnectionSlugConflictError","createWebhookConnectionBodySchema","listWebhookConnectionsQuerySchema","listWebhookConnectionsResponseSchema","updateWebhookConnectionBodySchema","WEBHOOK_PROVIDER","webhookConnectionDtoSchema","ClientError","defineRoute","z","toWebhookConnectionDto","connectionParamsSchema","object","connectionId","string","uuid","isConnectionAlreadyExistsError","error","name","requireWebhookConnection","params","connection","getIntegrationConnectionById","provider","status","request","workspaceId","createWebhookConnectionRoutes","options","createConnectionRoute","method","path","auth","description","schema","body","response","errorHandler","handler","reply","workspace_id","slug","createIntegrationConnection","externalAccountId","displayName","baseUrl","listConnectionsRoute","querystring","query","connections","listIntegrationConnections","filter","map","updateConnectionRoute","updated","updateIntegrationConnectionLifecycleStatus","id","lifecycleStatus","lifecycle_status","deleteConnectionRoute","void","deleteIntegrationConnection","code","prefix","routes"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { GetIntegrationConnectionByIdFn, IntegrationTx, PublishIntegrationEventReceivedFn } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import { type RouteGroup } from '@shipfox/node-fastify';
|
|
3
|
+
export interface CreateWebhookInboundRoutesOptions {
|
|
4
|
+
coreDb: () => {
|
|
5
|
+
transaction<T>(callback: (tx: IntegrationTx) => Promise<T>): Promise<T>;
|
|
6
|
+
};
|
|
7
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
8
|
+
publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;
|
|
9
|
+
}
|
|
10
|
+
export declare function createWebhookInboundRoutes(options: CreateWebhookInboundRoutesOptions): RouteGroup;
|
|
11
|
+
//# sourceMappingURL=inbound.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/inbound.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,8BAA8B,EAC9B,aAAa,EACb,iCAAiC,EAClC,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EAA2B,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AA0BhF,MAAM,WAAW,iCAAiC;IAChD,MAAM,EAAE,MAAM;QACZ,WAAW,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;KACzE,CAAC;IACF,4BAA4B,EAAE,8BAA8B,CAAC;IAC7D,+BAA+B,EAAE,iCAAiC,CAAC;CACpE;AAyBD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,iCAAiC,GAAG,UAAU,CAiEjG"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import formBodyPlugin from '@fastify/formbody';
|
|
3
|
+
import { WEBHOOK_PROVIDER, WEBHOOK_RECEIVED_EVENT } from '@shipfox/api-integration-webhook-dto';
|
|
4
|
+
import { ClientError, defineRoute } from '@shipfox/node-fastify';
|
|
5
|
+
import fp from 'fastify-plugin';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { redactHeaders, WEBHOOK_INBOUND_BODY_LIMIT } from '#constants.js';
|
|
8
|
+
const MAX_DELIVERY_ID_HEADER_LENGTH = 200;
|
|
9
|
+
const acceptedContentTypes = new Set([
|
|
10
|
+
'application/json',
|
|
11
|
+
'application/x-www-form-urlencoded',
|
|
12
|
+
'text/plain'
|
|
13
|
+
]);
|
|
14
|
+
const FORM_URLENCODED_CONTENT_TYPE_RE = /^application\/x-www-form-urlencoded(?:;.*)?$/i;
|
|
15
|
+
const formBodyRoutePlugin = fp(async (app)=>{
|
|
16
|
+
await app.register(formBodyPlugin, {
|
|
17
|
+
bodyLimit: WEBHOOK_INBOUND_BODY_LIMIT
|
|
18
|
+
});
|
|
19
|
+
app.addContentTypeParser(FORM_URLENCODED_CONTENT_TYPE_RE, {
|
|
20
|
+
parseAs: 'buffer',
|
|
21
|
+
bodyLimit: WEBHOOK_INBOUND_BODY_LIMIT
|
|
22
|
+
}, (_request, body, done)=>{
|
|
23
|
+
done(null, Object.fromEntries(new URLSearchParams(body.toString('utf8'))));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
const inboundParamsSchema = z.object({
|
|
27
|
+
connectionId: z.string().uuid()
|
|
28
|
+
});
|
|
29
|
+
const inboundQuerySchema = z.record(z.string(), z.unknown());
|
|
30
|
+
const inboundAcceptedResponseSchema = z.object({
|
|
31
|
+
delivery_id: z.string()
|
|
32
|
+
});
|
|
33
|
+
function contentType(requestContentType) {
|
|
34
|
+
return requestContentType?.split(';', 1)[0]?.trim().toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
function deliveryIdFor(header) {
|
|
37
|
+
const value = Array.isArray(header) ? header[0] : header;
|
|
38
|
+
if (!value) return randomUUID();
|
|
39
|
+
if (value.length > MAX_DELIVERY_ID_HEADER_LENGTH) {
|
|
40
|
+
throw new ClientError('Delivery ID header is too long', 'delivery-id-too-long', {
|
|
41
|
+
status: 400
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
export function createWebhookInboundRoutes(options) {
|
|
47
|
+
const inboundRoute = defineRoute({
|
|
48
|
+
method: 'POST',
|
|
49
|
+
path: '/:connectionId',
|
|
50
|
+
auth: [],
|
|
51
|
+
description: 'Generic webhook receiver.',
|
|
52
|
+
options: {
|
|
53
|
+
bodyLimit: WEBHOOK_INBOUND_BODY_LIMIT
|
|
54
|
+
},
|
|
55
|
+
schema: {
|
|
56
|
+
params: inboundParamsSchema,
|
|
57
|
+
querystring: inboundQuerySchema,
|
|
58
|
+
response: {
|
|
59
|
+
202: inboundAcceptedResponseSchema
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
handler: async (request, reply)=>{
|
|
63
|
+
const mediaType = contentType(request.headers['content-type']);
|
|
64
|
+
if (!mediaType || !acceptedContentTypes.has(mediaType)) {
|
|
65
|
+
throw new ClientError('Unsupported content type', 'unsupported-media-type', {
|
|
66
|
+
status: 415
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const { connectionId } = request.params;
|
|
70
|
+
const connection = await options.getIntegrationConnectionById(connectionId);
|
|
71
|
+
if (!connection || connection.provider !== WEBHOOK_PROVIDER || connection.lifecycleStatus !== 'active') {
|
|
72
|
+
throw new ClientError('Webhook connection not found', 'not-found', {
|
|
73
|
+
status: 404
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const deliveryId = deliveryIdFor(request.headers['x-delivery-id']);
|
|
77
|
+
const receivedAt = new Date().toISOString();
|
|
78
|
+
await options.coreDb().transaction(async (tx)=>{
|
|
79
|
+
await options.publishIntegrationEventReceived({
|
|
80
|
+
tx,
|
|
81
|
+
event: {
|
|
82
|
+
provider: WEBHOOK_PROVIDER,
|
|
83
|
+
source: connection.slug,
|
|
84
|
+
event: WEBHOOK_RECEIVED_EVENT,
|
|
85
|
+
workspaceId: connection.workspaceId,
|
|
86
|
+
connectionId: connection.id,
|
|
87
|
+
connectionName: connection.displayName,
|
|
88
|
+
deliveryId,
|
|
89
|
+
receivedAt,
|
|
90
|
+
payload: {
|
|
91
|
+
method: request.method,
|
|
92
|
+
headers: redactHeaders(request.headers),
|
|
93
|
+
query: request.query,
|
|
94
|
+
body: request.body
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
reply.code(202);
|
|
100
|
+
return {
|
|
101
|
+
delivery_id: deliveryId
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
prefix: '/webhook',
|
|
107
|
+
auth: [],
|
|
108
|
+
plugins: [
|
|
109
|
+
formBodyRoutePlugin
|
|
110
|
+
],
|
|
111
|
+
routes: [
|
|
112
|
+
inboundRoute
|
|
113
|
+
]
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
//# sourceMappingURL=inbound.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/inbound.ts"],"sourcesContent":["import type {Buffer} from 'node:buffer';\nimport {randomUUID} from 'node:crypto';\nimport formBodyPlugin from '@fastify/formbody';\nimport type {\n GetIntegrationConnectionByIdFn,\n IntegrationTx,\n PublishIntegrationEventReceivedFn,\n} from '@shipfox/api-integration-core-dto';\nimport {WEBHOOK_PROVIDER, WEBHOOK_RECEIVED_EVENT} from '@shipfox/api-integration-webhook-dto';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {FastifyPluginAsync} from 'fastify';\nimport fp from 'fastify-plugin';\nimport {z} from 'zod';\nimport {redactHeaders, WEBHOOK_INBOUND_BODY_LIMIT} from '#constants.js';\n\nconst MAX_DELIVERY_ID_HEADER_LENGTH = 200;\nconst acceptedContentTypes = new Set([\n 'application/json',\n 'application/x-www-form-urlencoded',\n 'text/plain',\n]);\nconst FORM_URLENCODED_CONTENT_TYPE_RE = /^application\\/x-www-form-urlencoded(?:;.*)?$/i;\n\nconst formBodyRoutePlugin: FastifyPluginAsync = fp(async (app) => {\n await app.register(formBodyPlugin, {bodyLimit: WEBHOOK_INBOUND_BODY_LIMIT});\n\n app.addContentTypeParser(\n FORM_URLENCODED_CONTENT_TYPE_RE,\n {parseAs: 'buffer', bodyLimit: WEBHOOK_INBOUND_BODY_LIMIT},\n (_request, body: Buffer, done) => {\n done(null, Object.fromEntries(new URLSearchParams(body.toString('utf8'))));\n },\n );\n});\n\nexport interface CreateWebhookInboundRoutesOptions {\n coreDb: () => {\n transaction<T>(callback: (tx: IntegrationTx) => Promise<T>): Promise<T>;\n };\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n}\n\nconst inboundParamsSchema = z.object({\n connectionId: z.string().uuid(),\n});\n\nconst inboundQuerySchema = z.record(z.string(), z.unknown());\n\nconst inboundAcceptedResponseSchema = z.object({\n delivery_id: z.string(),\n});\n\nfunction contentType(requestContentType: string | undefined): string | undefined {\n return requestContentType?.split(';', 1)[0]?.trim().toLowerCase();\n}\n\nfunction deliveryIdFor(header: string | string[] | undefined): string {\n const value = Array.isArray(header) ? header[0] : header;\n if (!value) return randomUUID();\n if (value.length > MAX_DELIVERY_ID_HEADER_LENGTH) {\n throw new ClientError('Delivery ID header is too long', 'delivery-id-too-long', {status: 400});\n }\n return value;\n}\n\nexport function createWebhookInboundRoutes(options: CreateWebhookInboundRoutesOptions): RouteGroup {\n const inboundRoute = defineRoute({\n method: 'POST',\n path: '/:connectionId',\n auth: [],\n description: 'Generic webhook receiver.',\n options: {bodyLimit: WEBHOOK_INBOUND_BODY_LIMIT},\n schema: {\n params: inboundParamsSchema,\n querystring: inboundQuerySchema,\n response: {\n 202: inboundAcceptedResponseSchema,\n },\n },\n handler: async (request, reply) => {\n const mediaType = contentType(request.headers['content-type']);\n if (!mediaType || !acceptedContentTypes.has(mediaType)) {\n throw new ClientError('Unsupported content type', 'unsupported-media-type', {status: 415});\n }\n\n const {connectionId} = request.params;\n const connection = await options.getIntegrationConnectionById(connectionId);\n if (\n !connection ||\n connection.provider !== WEBHOOK_PROVIDER ||\n connection.lifecycleStatus !== 'active'\n ) {\n throw new ClientError('Webhook connection not found', 'not-found', {status: 404});\n }\n\n const deliveryId = deliveryIdFor(request.headers['x-delivery-id']);\n const receivedAt = new Date().toISOString();\n await options.coreDb().transaction(async (tx) => {\n await options.publishIntegrationEventReceived({\n tx,\n event: {\n provider: WEBHOOK_PROVIDER,\n source: connection.slug,\n event: WEBHOOK_RECEIVED_EVENT,\n workspaceId: connection.workspaceId,\n connectionId: connection.id,\n connectionName: connection.displayName,\n deliveryId,\n receivedAt,\n payload: {\n method: request.method,\n headers: redactHeaders(request.headers),\n query: request.query,\n body: request.body,\n },\n },\n });\n });\n\n reply.code(202);\n return {delivery_id: deliveryId};\n },\n });\n\n return {\n prefix: '/webhook',\n auth: [],\n plugins: [formBodyRoutePlugin],\n routes: [inboundRoute],\n };\n}\n"],"names":["randomUUID","formBodyPlugin","WEBHOOK_PROVIDER","WEBHOOK_RECEIVED_EVENT","ClientError","defineRoute","fp","z","redactHeaders","WEBHOOK_INBOUND_BODY_LIMIT","MAX_DELIVERY_ID_HEADER_LENGTH","acceptedContentTypes","Set","FORM_URLENCODED_CONTENT_TYPE_RE","formBodyRoutePlugin","app","register","bodyLimit","addContentTypeParser","parseAs","_request","body","done","Object","fromEntries","URLSearchParams","toString","inboundParamsSchema","object","connectionId","string","uuid","inboundQuerySchema","record","unknown","inboundAcceptedResponseSchema","delivery_id","contentType","requestContentType","split","trim","toLowerCase","deliveryIdFor","header","value","Array","isArray","length","status","createWebhookInboundRoutes","options","inboundRoute","method","path","auth","description","schema","params","querystring","response","handler","request","reply","mediaType","headers","has","connection","getIntegrationConnectionById","provider","lifecycleStatus","deliveryId","receivedAt","Date","toISOString","coreDb","transaction","tx","publishIntegrationEventReceived","event","source","slug","workspaceId","id","connectionName","displayName","payload","query","code","prefix","plugins","routes"],"mappings":"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"}
|