@shipfox/api-integration-sentry 4.0.0 → 6.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 +7 -7
- package/CHANGELOG.md +49 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -5
- package/dist/config.js.map +1 -1
- package/dist/core/install.d.ts +6 -0
- package/dist/core/install.d.ts.map +1 -1
- package/dist/core/install.js +8 -5
- package/dist/core/install.js.map +1 -1
- package/dist/core/signature.d.ts +1 -1
- package/dist/core/signature.d.ts.map +1 -1
- package/dist/core/signature.js.map +1 -1
- package/dist/core/webhook-processor.d.ts +16 -0
- package/dist/core/webhook-processor.d.ts.map +1 -0
- package/dist/core/webhook-processor.js +198 -0
- package/dist/core/webhook-processor.js.map +1 -0
- package/dist/core/webhook.d.ts +4 -4
- package/dist/core/webhook.d.ts.map +1 -1
- package/dist/core/webhook.js +63 -49
- package/dist/core/webhook.js.map +1 -1
- package/dist/db/db.d.ts +8 -4
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/installations.d.ts +35 -13
- package/dist/db/installations.d.ts.map +1 -1
- package/dist/db/installations.js +89 -31
- package/dist/db/installations.js.map +1 -1
- package/dist/db/schema/installations.d.ts +5 -3
- package/dist/db/schema/installations.d.ts.map +1 -1
- package/dist/db/schema/installations.js +6 -6
- package/dist/db/schema/installations.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/webhooks.d.ts +4 -1
- package/dist/presentation/routes/webhooks.d.ts.map +1 -1
- package/dist/presentation/routes/webhooks.js +69 -43
- package/dist/presentation/routes/webhooks.js.map +1 -1
- package/dist/temporal/activities/prune-unclaimed-installations.d.ts +1 -0
- package/dist/temporal/activities/prune-unclaimed-installations.d.ts.map +1 -1
- package/dist/temporal/activities/prune-unclaimed-installations.js.map +1 -1
- package/dist/temporal/workflows/index.bundle.js +7 -2
- package/dist/temporal/workflows/prune-unclaimed-installations-cron.d.ts.map +1 -1
- package/dist/temporal/workflows/prune-unclaimed-installations-cron.js +6 -1
- package/dist/temporal/workflows/prune-unclaimed-installations-cron.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +20 -28
- package/src/config.ts +4 -5
- package/src/core/install.ts +21 -7
- package/src/core/signature.ts +1 -1
- package/src/core/webhook-processor.test.ts +304 -0
- package/src/core/webhook-processor.ts +224 -0
- package/src/core/webhook.ts +74 -53
- package/src/db/installations.test.ts +163 -17
- package/src/db/installations.ts +136 -35
- package/src/db/schema/installations.ts +7 -7
- package/src/index.test.ts +4 -1
- package/src/index.ts +16 -0
- package/src/presentation/routes/webhooks.test.ts +6 -5
- package/src/presentation/routes/webhooks.ts +88 -29
- package/src/temporal/activities/prune-unclaimed-installations.test.ts +24 -2
- package/src/temporal/activities/prune-unclaimed-installations.ts +4 -1
- package/src/temporal/workflows/prune-unclaimed-installations-cron.ts +4 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/presentation/routes/installation-handler.d.ts +0 -9
- package/dist/presentation/routes/installation-handler.d.ts.map +0 -1
- package/dist/presentation/routes/installation-handler.js +0 -70
- package/dist/presentation/routes/installation-handler.js.map +0 -1
- package/dist/presentation/routes/issue-handler.d.ts +0 -9
- package/dist/presentation/routes/issue-handler.d.ts.map +0 -1
- package/dist/presentation/routes/issue-handler.js +0 -50
- package/dist/presentation/routes/issue-handler.js.map +0 -1
- package/dist/presentation/routes/webhook-delivery.d.ts +0 -28
- package/dist/presentation/routes/webhook-delivery.d.ts.map +0 -1
- package/dist/presentation/routes/webhook-delivery.js +0 -56
- package/dist/presentation/routes/webhook-delivery.js.map +0 -1
- package/dist/presentation/routes/webhook-request.d.ts +0 -10
- package/dist/presentation/routes/webhook-request.d.ts.map +0 -1
- package/dist/presentation/routes/webhook-request.js +0 -56
- package/dist/presentation/routes/webhook-request.js.map +0 -1
- package/src/presentation/routes/installation-handler.ts +0 -76
- package/src/presentation/routes/issue-handler.ts +0 -52
- package/src/presentation/routes/webhook-delivery.ts +0 -61
- package/src/presentation/routes/webhook-request.ts +0 -66
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { FastifyReply } from 'fastify';
|
|
2
|
-
import type { SentryWebhookContext } from './webhook-context.js';
|
|
3
|
-
export declare function handleInstallationResource(args: {
|
|
4
|
-
context: SentryWebhookContext;
|
|
5
|
-
reply: FastifyReply;
|
|
6
|
-
deliveryId: string;
|
|
7
|
-
rawBody: string;
|
|
8
|
-
}): Promise<null>;
|
|
9
|
-
//# sourceMappingURL=installation-handler.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"installation-handler.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/installation-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,SAAS,CAAC;AAO1C,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,sBAAsB,CAAC;AAQ/D,wBAAsB,0BAA0B,CAAC,IAAI,EAAE;IACrD,OAAO,EAAE,oBAAoB,CAAC;IAC9B,KAAK,EAAE,YAAY,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDhB"}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { sentryInstallationWebhookSchema } from '@shipfox/api-integration-sentry-dto';
|
|
2
|
-
import { config } from '#config.js';
|
|
3
|
-
import { handleSentryInstallationCreated, handleSentryInstallationDeleted } from '#core/webhook.js';
|
|
4
|
-
import { getSentryInstallationByInstallationUuid, persistVerifiedUnclaimedInstallation } from '#db/installations.js';
|
|
5
|
-
import { parseAndValidateOrDrop, SENTRY_PROVIDER } from './webhook-delivery.js';
|
|
6
|
-
// Validates an installation lifecycle delivery, then applies it. `created` is the
|
|
7
|
-
// authoritative install signal: it exchanges the single-use code and persists a
|
|
8
|
-
// verified-unclaimed row (exchange outside any transaction; persist + delivery
|
|
9
|
-
// record in one short transaction). `deleted` tombstones the install and disables
|
|
10
|
-
// the connection in a single transaction.
|
|
11
|
-
export async function handleInstallationResource(args) {
|
|
12
|
-
const { context, reply, deliveryId, rawBody } = args;
|
|
13
|
-
const payload = await parseAndValidateOrDrop({
|
|
14
|
-
schema: sentryInstallationWebhookSchema,
|
|
15
|
-
rawBody,
|
|
16
|
-
deliveryId,
|
|
17
|
-
context,
|
|
18
|
-
reply
|
|
19
|
-
});
|
|
20
|
-
if (!payload) return null;
|
|
21
|
-
const installation = payload.data.installation;
|
|
22
|
-
if (payload.action === 'deleted') {
|
|
23
|
-
await context.coreDb().transaction(async (tx)=>{
|
|
24
|
-
await handleSentryInstallationDeleted({
|
|
25
|
-
tx,
|
|
26
|
-
deliveryId,
|
|
27
|
-
installationUuid: installation.uuid,
|
|
28
|
-
recordDeliveryOnly: context.recordDeliveryOnly,
|
|
29
|
-
updateConnectionLifecycleStatus: context.updateConnectionLifecycleStatus
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
reply.code(204);
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
await handleSentryInstallationCreated({
|
|
36
|
-
deliveryId,
|
|
37
|
-
installationUuid: installation.uuid,
|
|
38
|
-
orgSlug: installation.organization?.slug,
|
|
39
|
-
code: installation.code,
|
|
40
|
-
sentry: context.sentry,
|
|
41
|
-
verifyInstall: config.SENTRY_APP_VERIFY_INSTALL,
|
|
42
|
-
getSentryInstallation: ({ installationUuid })=>getSentryInstallationByInstallationUuid(installationUuid),
|
|
43
|
-
persistUnclaimedAndRecordDelivery: ({ installationUuid, orgSlug, codeHash, deliveryId: id })=>context.coreDb().transaction(async (tx)=>{
|
|
44
|
-
const persisted = await persistVerifiedUnclaimedInstallation({
|
|
45
|
-
installationUuid,
|
|
46
|
-
orgSlug,
|
|
47
|
-
codeHash
|
|
48
|
-
}, {
|
|
49
|
-
tx
|
|
50
|
-
});
|
|
51
|
-
await context.recordDeliveryOnly({
|
|
52
|
-
tx,
|
|
53
|
-
provider: SENTRY_PROVIDER,
|
|
54
|
-
deliveryId: id
|
|
55
|
-
});
|
|
56
|
-
return persisted;
|
|
57
|
-
}),
|
|
58
|
-
recordDelivery: (id)=>context.coreDb().transaction(async (tx)=>{
|
|
59
|
-
await context.recordDeliveryOnly({
|
|
60
|
-
tx,
|
|
61
|
-
provider: SENTRY_PROVIDER,
|
|
62
|
-
deliveryId: id
|
|
63
|
-
});
|
|
64
|
-
})
|
|
65
|
-
});
|
|
66
|
-
reply.code(204);
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
//# sourceMappingURL=installation-handler.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/presentation/routes/installation-handler.ts"],"sourcesContent":["import {sentryInstallationWebhookSchema} from '@shipfox/api-integration-sentry-dto';\nimport type {FastifyReply} from 'fastify';\nimport {config} from '#config.js';\nimport {handleSentryInstallationCreated, handleSentryInstallationDeleted} from '#core/webhook.js';\nimport {\n getSentryInstallationByInstallationUuid,\n persistVerifiedUnclaimedInstallation,\n} from '#db/installations.js';\nimport type {SentryWebhookContext} from './webhook-context.js';\nimport {parseAndValidateOrDrop, SENTRY_PROVIDER} from './webhook-delivery.js';\n\n// Validates an installation lifecycle delivery, then applies it. `created` is the\n// authoritative install signal: it exchanges the single-use code and persists a\n// verified-unclaimed row (exchange outside any transaction; persist + delivery\n// record in one short transaction). `deleted` tombstones the install and disables\n// the connection in a single transaction.\nexport async function handleInstallationResource(args: {\n context: SentryWebhookContext;\n reply: FastifyReply;\n deliveryId: string;\n rawBody: string;\n}): Promise<null> {\n const {context, reply, deliveryId, rawBody} = args;\n\n const payload = await parseAndValidateOrDrop({\n schema: sentryInstallationWebhookSchema,\n rawBody,\n deliveryId,\n context,\n reply,\n });\n if (!payload) return null;\n\n const installation = payload.data.installation;\n\n if (payload.action === 'deleted') {\n await context.coreDb().transaction(async (tx) => {\n await handleSentryInstallationDeleted({\n tx,\n deliveryId,\n installationUuid: installation.uuid,\n recordDeliveryOnly: context.recordDeliveryOnly,\n updateConnectionLifecycleStatus: context.updateConnectionLifecycleStatus,\n });\n });\n reply.code(204);\n return null;\n }\n\n await handleSentryInstallationCreated({\n deliveryId,\n installationUuid: installation.uuid,\n orgSlug: installation.organization?.slug,\n code: installation.code,\n sentry: context.sentry,\n verifyInstall: config.SENTRY_APP_VERIFY_INSTALL,\n getSentryInstallation: ({installationUuid}) =>\n getSentryInstallationByInstallationUuid(installationUuid),\n persistUnclaimedAndRecordDelivery: ({installationUuid, orgSlug, codeHash, deliveryId: id}) =>\n context.coreDb().transaction(async (tx) => {\n const persisted = await persistVerifiedUnclaimedInstallation(\n {installationUuid, orgSlug, codeHash},\n {tx},\n );\n await context.recordDeliveryOnly({tx, provider: SENTRY_PROVIDER, deliveryId: id});\n return persisted;\n }),\n recordDelivery: (id) =>\n context.coreDb().transaction(async (tx) => {\n await context.recordDeliveryOnly({tx, provider: SENTRY_PROVIDER, deliveryId: id});\n }),\n });\n\n reply.code(204);\n return null;\n}\n"],"names":["sentryInstallationWebhookSchema","config","handleSentryInstallationCreated","handleSentryInstallationDeleted","getSentryInstallationByInstallationUuid","persistVerifiedUnclaimedInstallation","parseAndValidateOrDrop","SENTRY_PROVIDER","handleInstallationResource","args","context","reply","deliveryId","rawBody","payload","schema","installation","data","action","coreDb","transaction","tx","installationUuid","uuid","recordDeliveryOnly","updateConnectionLifecycleStatus","code","orgSlug","organization","slug","sentry","verifyInstall","SENTRY_APP_VERIFY_INSTALL","getSentryInstallation","persistUnclaimedAndRecordDelivery","codeHash","id","persisted","provider","recordDelivery"],"mappings":"AAAA,SAAQA,+BAA+B,QAAO,sCAAsC;AAEpF,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,+BAA+B,EAAEC,+BAA+B,QAAO,mBAAmB;AAClG,SACEC,uCAAuC,EACvCC,oCAAoC,QAC/B,uBAAuB;AAE9B,SAAQC,sBAAsB,EAAEC,eAAe,QAAO,wBAAwB;AAE9E,kFAAkF;AAClF,gFAAgF;AAChF,+EAA+E;AAC/E,kFAAkF;AAClF,0CAA0C;AAC1C,OAAO,eAAeC,2BAA2BC,IAKhD;IACC,MAAM,EAACC,OAAO,EAAEC,KAAK,EAAEC,UAAU,EAAEC,OAAO,EAAC,GAAGJ;IAE9C,MAAMK,UAAU,MAAMR,uBAAuB;QAC3CS,QAAQf;QACRa;QACAD;QACAF;QACAC;IACF;IACA,IAAI,CAACG,SAAS,OAAO;IAErB,MAAME,eAAeF,QAAQG,IAAI,CAACD,YAAY;IAE9C,IAAIF,QAAQI,MAAM,KAAK,WAAW;QAChC,MAAMR,QAAQS,MAAM,GAAGC,WAAW,CAAC,OAAOC;YACxC,MAAMlB,gCAAgC;gBACpCkB;gBACAT;gBACAU,kBAAkBN,aAAaO,IAAI;gBACnCC,oBAAoBd,QAAQc,kBAAkB;gBAC9CC,iCAAiCf,QAAQe,+BAA+B;YAC1E;QACF;QACAd,MAAMe,IAAI,CAAC;QACX,OAAO;IACT;IAEA,MAAMxB,gCAAgC;QACpCU;QACAU,kBAAkBN,aAAaO,IAAI;QACnCI,SAASX,aAAaY,YAAY,EAAEC;QACpCH,MAAMV,aAAaU,IAAI;QACvBI,QAAQpB,QAAQoB,MAAM;QACtBC,eAAe9B,OAAO+B,yBAAyB;QAC/CC,uBAAuB,CAAC,EAACX,gBAAgB,EAAC,GACxClB,wCAAwCkB;QAC1CY,mCAAmC,CAAC,EAACZ,gBAAgB,EAAEK,OAAO,EAAEQ,QAAQ,EAAEvB,YAAYwB,EAAE,EAAC,GACvF1B,QAAQS,MAAM,GAAGC,WAAW,CAAC,OAAOC;gBAClC,MAAMgB,YAAY,MAAMhC,qCACtB;oBAACiB;oBAAkBK;oBAASQ;gBAAQ,GACpC;oBAACd;gBAAE;gBAEL,MAAMX,QAAQc,kBAAkB,CAAC;oBAACH;oBAAIiB,UAAU/B;oBAAiBK,YAAYwB;gBAAE;gBAC/E,OAAOC;YACT;QACFE,gBAAgB,CAACH,KACf1B,QAAQS,MAAM,GAAGC,WAAW,CAAC,OAAOC;gBAClC,MAAMX,QAAQc,kBAAkB,CAAC;oBAACH;oBAAIiB,UAAU/B;oBAAiBK,YAAYwB;gBAAE;YACjF;IACJ;IAEAzB,MAAMe,IAAI,CAAC;IACX,OAAO;AACT"}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { FastifyReply } from 'fastify';
|
|
2
|
-
import type { SentryWebhookContext } from './webhook-context.js';
|
|
3
|
-
export declare function handleIssueResource(args: {
|
|
4
|
-
context: SentryWebhookContext;
|
|
5
|
-
reply: FastifyReply;
|
|
6
|
-
deliveryId: string;
|
|
7
|
-
rawBody: string;
|
|
8
|
-
}): Promise<null>;
|
|
9
|
-
//# sourceMappingURL=issue-handler.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"issue-handler.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/issue-handler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,SAAS,CAAC;AAG1C,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,sBAAsB,CAAC;AAQ/D,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,OAAO,EAAE,oBAAoB,CAAC;IAC9B,KAAK,EAAE,YAAY,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiChB"}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { sentryIssueWebhookSchema } from '@shipfox/api-integration-sentry-dto';
|
|
2
|
-
import { logger } from '@shipfox/node-opentelemetry';
|
|
3
|
-
import { SentryIssueDroppedError } from '#core/errors.js';
|
|
4
|
-
import { handleSentryIssueEvent, normalizeSentryIssueAction } from '#core/webhook.js';
|
|
5
|
-
import { parseAndValidateOrDrop, recordAndDrop } from './webhook-delivery.js';
|
|
6
|
-
// Validates an issue delivery, then publishes the mapped event in a transaction.
|
|
7
|
-
// A malformed or unknown-action payload is recorded-and-dropped (204) before we
|
|
8
|
-
// reach core. Core throws a SentryIssueDroppedError when the delivery references
|
|
9
|
-
// state we cannot publish against (unknown/deleted installation, missing
|
|
10
|
-
// connection); we record-and-drop those too. Any other error bubbles up.
|
|
11
|
-
export async function handleIssueResource(args) {
|
|
12
|
-
const { context, reply, deliveryId, rawBody } = args;
|
|
13
|
-
const payload = await parseAndValidateOrDrop({
|
|
14
|
-
schema: sentryIssueWebhookSchema,
|
|
15
|
-
normalize: normalizeSentryIssueAction,
|
|
16
|
-
rawBody,
|
|
17
|
-
deliveryId,
|
|
18
|
-
context,
|
|
19
|
-
reply
|
|
20
|
-
});
|
|
21
|
-
if (!payload) return null;
|
|
22
|
-
try {
|
|
23
|
-
await context.coreDb().transaction(async (tx)=>{
|
|
24
|
-
await handleSentryIssueEvent({
|
|
25
|
-
tx,
|
|
26
|
-
deliveryId,
|
|
27
|
-
payload,
|
|
28
|
-
publishIntegrationEventReceived: context.publishIntegrationEventReceived,
|
|
29
|
-
getIntegrationConnectionById: context.getIntegrationConnectionById
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
} catch (error) {
|
|
33
|
-
if (error instanceof SentryIssueDroppedError) {
|
|
34
|
-
logger().warn({
|
|
35
|
-
deliveryId,
|
|
36
|
-
err: error
|
|
37
|
-
}, `sentry webhook: ${error.message}, dropping`);
|
|
38
|
-
return recordAndDrop({
|
|
39
|
-
context,
|
|
40
|
-
reply,
|
|
41
|
-
deliveryId
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
throw error;
|
|
45
|
-
}
|
|
46
|
-
reply.code(204);
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
//# sourceMappingURL=issue-handler.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/presentation/routes/issue-handler.ts"],"sourcesContent":["import {sentryIssueWebhookSchema} from '@shipfox/api-integration-sentry-dto';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport type {FastifyReply} from 'fastify';\nimport {SentryIssueDroppedError} from '#core/errors.js';\nimport {handleSentryIssueEvent, normalizeSentryIssueAction} from '#core/webhook.js';\nimport type {SentryWebhookContext} from './webhook-context.js';\nimport {parseAndValidateOrDrop, recordAndDrop} from './webhook-delivery.js';\n\n// Validates an issue delivery, then publishes the mapped event in a transaction.\n// A malformed or unknown-action payload is recorded-and-dropped (204) before we\n// reach core. Core throws a SentryIssueDroppedError when the delivery references\n// state we cannot publish against (unknown/deleted installation, missing\n// connection); we record-and-drop those too. Any other error bubbles up.\nexport async function handleIssueResource(args: {\n context: SentryWebhookContext;\n reply: FastifyReply;\n deliveryId: string;\n rawBody: string;\n}): Promise<null> {\n const {context, reply, deliveryId, rawBody} = args;\n\n const payload = await parseAndValidateOrDrop({\n schema: sentryIssueWebhookSchema,\n normalize: normalizeSentryIssueAction,\n rawBody,\n deliveryId,\n context,\n reply,\n });\n if (!payload) return null;\n\n try {\n await context.coreDb().transaction(async (tx) => {\n await handleSentryIssueEvent({\n tx,\n deliveryId,\n payload,\n publishIntegrationEventReceived: context.publishIntegrationEventReceived,\n getIntegrationConnectionById: context.getIntegrationConnectionById,\n });\n });\n } catch (error) {\n if (error instanceof SentryIssueDroppedError) {\n logger().warn({deliveryId, err: error}, `sentry webhook: ${error.message}, dropping`);\n return recordAndDrop({context, reply, deliveryId});\n }\n throw error;\n }\n\n reply.code(204);\n return null;\n}\n"],"names":["sentryIssueWebhookSchema","logger","SentryIssueDroppedError","handleSentryIssueEvent","normalizeSentryIssueAction","parseAndValidateOrDrop","recordAndDrop","handleIssueResource","args","context","reply","deliveryId","rawBody","payload","schema","normalize","coreDb","transaction","tx","publishIntegrationEventReceived","getIntegrationConnectionById","error","warn","err","message","code"],"mappings":"AAAA,SAAQA,wBAAwB,QAAO,sCAAsC;AAC7E,SAAQC,MAAM,QAAO,8BAA8B;AAEnD,SAAQC,uBAAuB,QAAO,kBAAkB;AACxD,SAAQC,sBAAsB,EAAEC,0BAA0B,QAAO,mBAAmB;AAEpF,SAAQC,sBAAsB,EAAEC,aAAa,QAAO,wBAAwB;AAE5E,iFAAiF;AACjF,gFAAgF;AAChF,iFAAiF;AACjF,yEAAyE;AACzE,yEAAyE;AACzE,OAAO,eAAeC,oBAAoBC,IAKzC;IACC,MAAM,EAACC,OAAO,EAAEC,KAAK,EAAEC,UAAU,EAAEC,OAAO,EAAC,GAAGJ;IAE9C,MAAMK,UAAU,MAAMR,uBAAuB;QAC3CS,QAAQd;QACRe,WAAWX;QACXQ;QACAD;QACAF;QACAC;IACF;IACA,IAAI,CAACG,SAAS,OAAO;IAErB,IAAI;QACF,MAAMJ,QAAQO,MAAM,GAAGC,WAAW,CAAC,OAAOC;YACxC,MAAMf,uBAAuB;gBAC3Be;gBACAP;gBACAE;gBACAM,iCAAiCV,QAAQU,+BAA+B;gBACxEC,8BAA8BX,QAAQW,4BAA4B;YACpE;QACF;IACF,EAAE,OAAOC,OAAO;QACd,IAAIA,iBAAiBnB,yBAAyB;YAC5CD,SAASqB,IAAI,CAAC;gBAACX;gBAAYY,KAAKF;YAAK,GAAG,CAAC,gBAAgB,EAAEA,MAAMG,OAAO,CAAC,UAAU,CAAC;YACpF,OAAOlB,cAAc;gBAACG;gBAASC;gBAAOC;YAAU;QAClD;QACA,MAAMU;IACR;IAEAX,MAAMe,IAAI,CAAC;IACX,OAAO;AACT"}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { FastifyReply } from 'fastify';
|
|
2
|
-
import type { z } from 'zod';
|
|
3
|
-
import type { SentryWebhookContext } from './webhook-context.js';
|
|
4
|
-
export declare const SENTRY_PROVIDER = "sentry";
|
|
5
|
-
/**
|
|
6
|
-
* Parses and validates a delivery body, recording-and-dropping it (HTTP 204) when
|
|
7
|
-
* the JSON is malformed or fails the schema (an unknown action falls here too).
|
|
8
|
-
* Returns the validated payload, or null when the delivery was dropped.
|
|
9
|
-
*/
|
|
10
|
-
export declare function parseAndValidateOrDrop<TSchema extends z.ZodType>(args: {
|
|
11
|
-
schema: TSchema;
|
|
12
|
-
rawBody: string;
|
|
13
|
-
deliveryId: string;
|
|
14
|
-
context: SentryWebhookContext;
|
|
15
|
-
reply: FastifyReply;
|
|
16
|
-
normalize?: (parsedJson: unknown) => unknown;
|
|
17
|
-
}): Promise<z.infer<TSchema> | null>;
|
|
18
|
-
/**
|
|
19
|
-
* Records the delivery for dedup and replies 204 without acting on it. A sustained
|
|
20
|
-
* non-2xx can degrade or disable the webhook on Sentry's side (a deliberate
|
|
21
|
-
* deviation from GitHub's 400), so deliveries we cannot use are acknowledged.
|
|
22
|
-
*/
|
|
23
|
-
export declare function recordAndDrop(args: {
|
|
24
|
-
context: SentryWebhookContext;
|
|
25
|
-
reply: FastifyReply;
|
|
26
|
-
deliveryId: string;
|
|
27
|
-
}): Promise<null>;
|
|
28
|
-
//# sourceMappingURL=webhook-delivery.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"webhook-delivery.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/webhook-delivery.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAC3B,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,sBAAsB,CAAC;AAE/D,eAAO,MAAM,eAAe,WAAW,CAAC;AAExC;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,SAAS,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE;IAC5E,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,oBAAoB,CAAC;IAC9B,KAAK,EAAE,YAAY,CAAC;IACpB,SAAS,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,OAAO,CAAC;CAC9C,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,CAuBnC;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,OAAO,EAAE,oBAAoB,CAAC;IAC9B,KAAK,EAAE,YAAY,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB"}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { logger } from '@shipfox/node-opentelemetry';
|
|
2
|
-
export const SENTRY_PROVIDER = 'sentry';
|
|
3
|
-
/**
|
|
4
|
-
* Parses and validates a delivery body, recording-and-dropping it (HTTP 204) when
|
|
5
|
-
* the JSON is malformed or fails the schema (an unknown action falls here too).
|
|
6
|
-
* Returns the validated payload, or null when the delivery was dropped.
|
|
7
|
-
*/ export async function parseAndValidateOrDrop(args) {
|
|
8
|
-
const { schema, rawBody, deliveryId, context, reply, normalize } = args;
|
|
9
|
-
let parsedJson;
|
|
10
|
-
try {
|
|
11
|
-
parsedJson = JSON.parse(rawBody);
|
|
12
|
-
} catch (error) {
|
|
13
|
-
logger().warn({
|
|
14
|
-
deliveryId,
|
|
15
|
-
err: error
|
|
16
|
-
}, 'sentry webhook: payload JSON parse failed, dropping');
|
|
17
|
-
await recordAndDrop({
|
|
18
|
-
context,
|
|
19
|
-
reply,
|
|
20
|
-
deliveryId
|
|
21
|
-
});
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
const validated = schema.safeParse(normalize ? normalize(parsedJson) : parsedJson);
|
|
25
|
-
if (!validated.success) {
|
|
26
|
-
logger().warn({
|
|
27
|
-
deliveryId,
|
|
28
|
-
issues: validated.error.issues
|
|
29
|
-
}, 'sentry webhook: payload failed validation (or unknown action), dropping');
|
|
30
|
-
await recordAndDrop({
|
|
31
|
-
context,
|
|
32
|
-
reply,
|
|
33
|
-
deliveryId
|
|
34
|
-
});
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
return validated.data;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Records the delivery for dedup and replies 204 without acting on it. A sustained
|
|
41
|
-
* non-2xx can degrade or disable the webhook on Sentry's side (a deliberate
|
|
42
|
-
* deviation from GitHub's 400), so deliveries we cannot use are acknowledged.
|
|
43
|
-
*/ export async function recordAndDrop(args) {
|
|
44
|
-
const { context, reply, deliveryId } = args;
|
|
45
|
-
await context.coreDb().transaction(async (tx)=>{
|
|
46
|
-
await context.recordDeliveryOnly({
|
|
47
|
-
tx,
|
|
48
|
-
provider: SENTRY_PROVIDER,
|
|
49
|
-
deliveryId
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
reply.code(204);
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
//# sourceMappingURL=webhook-delivery.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/presentation/routes/webhook-delivery.ts"],"sourcesContent":["import {logger} from '@shipfox/node-opentelemetry';\nimport type {FastifyReply} from 'fastify';\nimport type {z} from 'zod';\nimport type {SentryWebhookContext} from './webhook-context.js';\n\nexport const SENTRY_PROVIDER = 'sentry';\n\n/**\n * Parses and validates a delivery body, recording-and-dropping it (HTTP 204) when\n * the JSON is malformed or fails the schema (an unknown action falls here too).\n * Returns the validated payload, or null when the delivery was dropped.\n */\nexport async function parseAndValidateOrDrop<TSchema extends z.ZodType>(args: {\n schema: TSchema;\n rawBody: string;\n deliveryId: string;\n context: SentryWebhookContext;\n reply: FastifyReply;\n normalize?: (parsedJson: unknown) => unknown;\n}): Promise<z.infer<TSchema> | null> {\n const {schema, rawBody, deliveryId, context, reply, normalize} = args;\n\n let parsedJson: unknown;\n try {\n parsedJson = JSON.parse(rawBody);\n } catch (error) {\n logger().warn({deliveryId, err: error}, 'sentry webhook: payload JSON parse failed, dropping');\n await recordAndDrop({context, reply, deliveryId});\n return null;\n }\n\n const validated = schema.safeParse(normalize ? normalize(parsedJson) : parsedJson);\n if (!validated.success) {\n logger().warn(\n {deliveryId, issues: validated.error.issues},\n 'sentry webhook: payload failed validation (or unknown action), dropping',\n );\n await recordAndDrop({context, reply, deliveryId});\n return null;\n }\n\n return validated.data;\n}\n\n/**\n * Records the delivery for dedup and replies 204 without acting on it. A sustained\n * non-2xx can degrade or disable the webhook on Sentry's side (a deliberate\n * deviation from GitHub's 400), so deliveries we cannot use are acknowledged.\n */\nexport async function recordAndDrop(args: {\n context: SentryWebhookContext;\n reply: FastifyReply;\n deliveryId: string;\n}): Promise<null> {\n const {context, reply, deliveryId} = args;\n await context.coreDb().transaction(async (tx) => {\n await context.recordDeliveryOnly({tx, provider: SENTRY_PROVIDER, deliveryId});\n });\n reply.code(204);\n return null;\n}\n"],"names":["logger","SENTRY_PROVIDER","parseAndValidateOrDrop","args","schema","rawBody","deliveryId","context","reply","normalize","parsedJson","JSON","parse","error","warn","err","recordAndDrop","validated","safeParse","success","issues","data","coreDb","transaction","tx","recordDeliveryOnly","provider","code"],"mappings":"AAAA,SAAQA,MAAM,QAAO,8BAA8B;AAKnD,OAAO,MAAMC,kBAAkB,SAAS;AAExC;;;;CAIC,GACD,OAAO,eAAeC,uBAAkDC,IAOvE;IACC,MAAM,EAACC,MAAM,EAAEC,OAAO,EAAEC,UAAU,EAAEC,OAAO,EAAEC,KAAK,EAAEC,SAAS,EAAC,GAAGN;IAEjE,IAAIO;IACJ,IAAI;QACFA,aAAaC,KAAKC,KAAK,CAACP;IAC1B,EAAE,OAAOQ,OAAO;QACdb,SAASc,IAAI,CAAC;YAACR;YAAYS,KAAKF;QAAK,GAAG;QACxC,MAAMG,cAAc;YAACT;YAASC;YAAOF;QAAU;QAC/C,OAAO;IACT;IAEA,MAAMW,YAAYb,OAAOc,SAAS,CAACT,YAAYA,UAAUC,cAAcA;IACvE,IAAI,CAACO,UAAUE,OAAO,EAAE;QACtBnB,SAASc,IAAI,CACX;YAACR;YAAYc,QAAQH,UAAUJ,KAAK,CAACO,MAAM;QAAA,GAC3C;QAEF,MAAMJ,cAAc;YAACT;YAASC;YAAOF;QAAU;QAC/C,OAAO;IACT;IAEA,OAAOW,UAAUI,IAAI;AACvB;AAEA;;;;CAIC,GACD,OAAO,eAAeL,cAAcb,IAInC;IACC,MAAM,EAACI,OAAO,EAAEC,KAAK,EAAEF,UAAU,EAAC,GAAGH;IACrC,MAAMI,QAAQe,MAAM,GAAGC,WAAW,CAAC,OAAOC;QACxC,MAAMjB,QAAQkB,kBAAkB,CAAC;YAACD;YAAIE,UAAUzB;YAAiBK;QAAU;IAC7E;IACAE,MAAMmB,IAAI,CAAC;IACX,OAAO;AACT"}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { FastifyRequest } from 'fastify';
|
|
2
|
-
export interface SentryWebhookRequest {
|
|
3
|
-
deliveryId: string;
|
|
4
|
-
resource: string;
|
|
5
|
-
signature: string;
|
|
6
|
-
signatureHeaderName: string;
|
|
7
|
-
rawBody: string;
|
|
8
|
-
}
|
|
9
|
-
export declare function parseSentryWebhookRequest(request: FastifyRequest): SentryWebhookRequest;
|
|
10
|
-
//# sourceMappingURL=webhook-request.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"webhook-request.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/webhook-request.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,SAAS,CAAC;AAQ5C,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;CACjB;AAKD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,cAAc,GAAG,oBAAoB,CA8BvF"}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { Buffer } from 'node:buffer';
|
|
2
|
-
import { ClientError } from '@shipfox/node-fastify';
|
|
3
|
-
const REQUEST_ID_HEADER = 'request-id';
|
|
4
|
-
const RESOURCE_HEADER = 'sentry-hook-resource';
|
|
5
|
-
const SIGNATURE_HEADER = 'sentry-hook-signature';
|
|
6
|
-
// Sentry has used both header names; accepting both keeps older deliveries verifiable.
|
|
7
|
-
const LEGACY_SIGNATURE_HEADER = 'sentry-app-signature';
|
|
8
|
-
// Extracts and structurally validates the parts every Sentry delivery must carry.
|
|
9
|
-
// Throws ClientError for anything a genuine signed delivery would never omit; the
|
|
10
|
-
// cryptographic signature check happens in the route once the secret is in hand.
|
|
11
|
-
export function parseSentryWebhookRequest(request) {
|
|
12
|
-
const deliveryId = request.headers[REQUEST_ID_HEADER];
|
|
13
|
-
if (typeof deliveryId !== 'string' || !deliveryId) {
|
|
14
|
-
throw new ClientError('missing Request-ID header', 'missing-request-id');
|
|
15
|
-
}
|
|
16
|
-
const resource = request.headers[RESOURCE_HEADER];
|
|
17
|
-
if (typeof resource !== 'string' || !resource) {
|
|
18
|
-
throw new ClientError('missing Sentry-Hook-Resource header', 'missing-resource');
|
|
19
|
-
}
|
|
20
|
-
const signature = resolveSignature(request);
|
|
21
|
-
if (!signature) {
|
|
22
|
-
throw new ClientError('missing Sentry-Hook-Signature header', 'missing-signature', {
|
|
23
|
-
status: 401
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
const body = request.body;
|
|
27
|
-
if (!Buffer.isBuffer(body)) {
|
|
28
|
-
throw new ClientError('expected raw JSON body', 'invalid-body');
|
|
29
|
-
}
|
|
30
|
-
return {
|
|
31
|
-
deliveryId,
|
|
32
|
-
resource,
|
|
33
|
-
signature: signature.value,
|
|
34
|
-
signatureHeaderName: signature.headerName,
|
|
35
|
-
rawBody: body.toString('utf8')
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
function resolveSignature(request) {
|
|
39
|
-
const header = request.headers[SIGNATURE_HEADER];
|
|
40
|
-
if (typeof header === 'string' && header) {
|
|
41
|
-
return {
|
|
42
|
-
value: header,
|
|
43
|
-
headerName: SIGNATURE_HEADER
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
const legacyHeader = request.headers[LEGACY_SIGNATURE_HEADER];
|
|
47
|
-
if (typeof legacyHeader === 'string' && legacyHeader) {
|
|
48
|
-
return {
|
|
49
|
-
value: legacyHeader,
|
|
50
|
-
headerName: LEGACY_SIGNATURE_HEADER
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
//# sourceMappingURL=webhook-request.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/presentation/routes/webhook-request.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {ClientError} from '@shipfox/node-fastify';\nimport type {FastifyRequest} from 'fastify';\n\nconst REQUEST_ID_HEADER = 'request-id';\nconst RESOURCE_HEADER = 'sentry-hook-resource';\nconst SIGNATURE_HEADER = 'sentry-hook-signature';\n// Sentry has used both header names; accepting both keeps older deliveries verifiable.\nconst LEGACY_SIGNATURE_HEADER = 'sentry-app-signature';\n\nexport interface SentryWebhookRequest {\n deliveryId: string;\n resource: string;\n signature: string;\n signatureHeaderName: string;\n rawBody: string;\n}\n\n// Extracts and structurally validates the parts every Sentry delivery must carry.\n// Throws ClientError for anything a genuine signed delivery would never omit; the\n// cryptographic signature check happens in the route once the secret is in hand.\nexport function parseSentryWebhookRequest(request: FastifyRequest): SentryWebhookRequest {\n const deliveryId = request.headers[REQUEST_ID_HEADER];\n if (typeof deliveryId !== 'string' || !deliveryId) {\n throw new ClientError('missing Request-ID header', 'missing-request-id');\n }\n\n const resource = request.headers[RESOURCE_HEADER];\n if (typeof resource !== 'string' || !resource) {\n throw new ClientError('missing Sentry-Hook-Resource header', 'missing-resource');\n }\n\n const signature = resolveSignature(request);\n if (!signature) {\n throw new ClientError('missing Sentry-Hook-Signature header', 'missing-signature', {\n status: 401,\n });\n }\n\n const body = request.body;\n if (!Buffer.isBuffer(body)) {\n throw new ClientError('expected raw JSON body', 'invalid-body');\n }\n\n return {\n deliveryId,\n resource,\n signature: signature.value,\n signatureHeaderName: signature.headerName,\n rawBody: body.toString('utf8'),\n };\n}\n\nfunction resolveSignature(\n request: FastifyRequest,\n): {value: string; headerName: string} | undefined {\n const header = request.headers[SIGNATURE_HEADER];\n if (typeof header === 'string' && header) {\n return {value: header, headerName: SIGNATURE_HEADER};\n }\n const legacyHeader = request.headers[LEGACY_SIGNATURE_HEADER];\n if (typeof legacyHeader === 'string' && legacyHeader) {\n return {value: legacyHeader, headerName: LEGACY_SIGNATURE_HEADER};\n }\n return undefined;\n}\n"],"names":["Buffer","ClientError","REQUEST_ID_HEADER","RESOURCE_HEADER","SIGNATURE_HEADER","LEGACY_SIGNATURE_HEADER","parseSentryWebhookRequest","request","deliveryId","headers","resource","signature","resolveSignature","status","body","isBuffer","value","signatureHeaderName","headerName","rawBody","toString","header","legacyHeader","undefined"],"mappings":"AAAA,SAAQA,MAAM,QAAO,cAAc;AACnC,SAAQC,WAAW,QAAO,wBAAwB;AAGlD,MAAMC,oBAAoB;AAC1B,MAAMC,kBAAkB;AACxB,MAAMC,mBAAmB;AACzB,uFAAuF;AACvF,MAAMC,0BAA0B;AAUhC,kFAAkF;AAClF,kFAAkF;AAClF,iFAAiF;AACjF,OAAO,SAASC,0BAA0BC,OAAuB;IAC/D,MAAMC,aAAaD,QAAQE,OAAO,CAACP,kBAAkB;IACrD,IAAI,OAAOM,eAAe,YAAY,CAACA,YAAY;QACjD,MAAM,IAAIP,YAAY,6BAA6B;IACrD;IAEA,MAAMS,WAAWH,QAAQE,OAAO,CAACN,gBAAgB;IACjD,IAAI,OAAOO,aAAa,YAAY,CAACA,UAAU;QAC7C,MAAM,IAAIT,YAAY,uCAAuC;IAC/D;IAEA,MAAMU,YAAYC,iBAAiBL;IACnC,IAAI,CAACI,WAAW;QACd,MAAM,IAAIV,YAAY,wCAAwC,qBAAqB;YACjFY,QAAQ;QACV;IACF;IAEA,MAAMC,OAAOP,QAAQO,IAAI;IACzB,IAAI,CAACd,OAAOe,QAAQ,CAACD,OAAO;QAC1B,MAAM,IAAIb,YAAY,0BAA0B;IAClD;IAEA,OAAO;QACLO;QACAE;QACAC,WAAWA,UAAUK,KAAK;QAC1BC,qBAAqBN,UAAUO,UAAU;QACzCC,SAASL,KAAKM,QAAQ,CAAC;IACzB;AACF;AAEA,SAASR,iBACPL,OAAuB;IAEvB,MAAMc,SAASd,QAAQE,OAAO,CAACL,iBAAiB;IAChD,IAAI,OAAOiB,WAAW,YAAYA,QAAQ;QACxC,OAAO;YAACL,OAAOK;YAAQH,YAAYd;QAAgB;IACrD;IACA,MAAMkB,eAAef,QAAQE,OAAO,CAACJ,wBAAwB;IAC7D,IAAI,OAAOiB,iBAAiB,YAAYA,cAAc;QACpD,OAAO;YAACN,OAAOM;YAAcJ,YAAYb;QAAuB;IAClE;IACA,OAAOkB;AACT"}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import {sentryInstallationWebhookSchema} from '@shipfox/api-integration-sentry-dto';
|
|
2
|
-
import type {FastifyReply} from 'fastify';
|
|
3
|
-
import {config} from '#config.js';
|
|
4
|
-
import {handleSentryInstallationCreated, handleSentryInstallationDeleted} from '#core/webhook.js';
|
|
5
|
-
import {
|
|
6
|
-
getSentryInstallationByInstallationUuid,
|
|
7
|
-
persistVerifiedUnclaimedInstallation,
|
|
8
|
-
} from '#db/installations.js';
|
|
9
|
-
import type {SentryWebhookContext} from './webhook-context.js';
|
|
10
|
-
import {parseAndValidateOrDrop, SENTRY_PROVIDER} from './webhook-delivery.js';
|
|
11
|
-
|
|
12
|
-
// Validates an installation lifecycle delivery, then applies it. `created` is the
|
|
13
|
-
// authoritative install signal: it exchanges the single-use code and persists a
|
|
14
|
-
// verified-unclaimed row (exchange outside any transaction; persist + delivery
|
|
15
|
-
// record in one short transaction). `deleted` tombstones the install and disables
|
|
16
|
-
// the connection in a single transaction.
|
|
17
|
-
export async function handleInstallationResource(args: {
|
|
18
|
-
context: SentryWebhookContext;
|
|
19
|
-
reply: FastifyReply;
|
|
20
|
-
deliveryId: string;
|
|
21
|
-
rawBody: string;
|
|
22
|
-
}): Promise<null> {
|
|
23
|
-
const {context, reply, deliveryId, rawBody} = args;
|
|
24
|
-
|
|
25
|
-
const payload = await parseAndValidateOrDrop({
|
|
26
|
-
schema: sentryInstallationWebhookSchema,
|
|
27
|
-
rawBody,
|
|
28
|
-
deliveryId,
|
|
29
|
-
context,
|
|
30
|
-
reply,
|
|
31
|
-
});
|
|
32
|
-
if (!payload) return null;
|
|
33
|
-
|
|
34
|
-
const installation = payload.data.installation;
|
|
35
|
-
|
|
36
|
-
if (payload.action === 'deleted') {
|
|
37
|
-
await context.coreDb().transaction(async (tx) => {
|
|
38
|
-
await handleSentryInstallationDeleted({
|
|
39
|
-
tx,
|
|
40
|
-
deliveryId,
|
|
41
|
-
installationUuid: installation.uuid,
|
|
42
|
-
recordDeliveryOnly: context.recordDeliveryOnly,
|
|
43
|
-
updateConnectionLifecycleStatus: context.updateConnectionLifecycleStatus,
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
reply.code(204);
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
await handleSentryInstallationCreated({
|
|
51
|
-
deliveryId,
|
|
52
|
-
installationUuid: installation.uuid,
|
|
53
|
-
orgSlug: installation.organization?.slug,
|
|
54
|
-
code: installation.code,
|
|
55
|
-
sentry: context.sentry,
|
|
56
|
-
verifyInstall: config.SENTRY_APP_VERIFY_INSTALL,
|
|
57
|
-
getSentryInstallation: ({installationUuid}) =>
|
|
58
|
-
getSentryInstallationByInstallationUuid(installationUuid),
|
|
59
|
-
persistUnclaimedAndRecordDelivery: ({installationUuid, orgSlug, codeHash, deliveryId: id}) =>
|
|
60
|
-
context.coreDb().transaction(async (tx) => {
|
|
61
|
-
const persisted = await persistVerifiedUnclaimedInstallation(
|
|
62
|
-
{installationUuid, orgSlug, codeHash},
|
|
63
|
-
{tx},
|
|
64
|
-
);
|
|
65
|
-
await context.recordDeliveryOnly({tx, provider: SENTRY_PROVIDER, deliveryId: id});
|
|
66
|
-
return persisted;
|
|
67
|
-
}),
|
|
68
|
-
recordDelivery: (id) =>
|
|
69
|
-
context.coreDb().transaction(async (tx) => {
|
|
70
|
-
await context.recordDeliveryOnly({tx, provider: SENTRY_PROVIDER, deliveryId: id});
|
|
71
|
-
}),
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
reply.code(204);
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import {sentryIssueWebhookSchema} from '@shipfox/api-integration-sentry-dto';
|
|
2
|
-
import {logger} from '@shipfox/node-opentelemetry';
|
|
3
|
-
import type {FastifyReply} from 'fastify';
|
|
4
|
-
import {SentryIssueDroppedError} from '#core/errors.js';
|
|
5
|
-
import {handleSentryIssueEvent, normalizeSentryIssueAction} from '#core/webhook.js';
|
|
6
|
-
import type {SentryWebhookContext} from './webhook-context.js';
|
|
7
|
-
import {parseAndValidateOrDrop, recordAndDrop} from './webhook-delivery.js';
|
|
8
|
-
|
|
9
|
-
// Validates an issue delivery, then publishes the mapped event in a transaction.
|
|
10
|
-
// A malformed or unknown-action payload is recorded-and-dropped (204) before we
|
|
11
|
-
// reach core. Core throws a SentryIssueDroppedError when the delivery references
|
|
12
|
-
// state we cannot publish against (unknown/deleted installation, missing
|
|
13
|
-
// connection); we record-and-drop those too. Any other error bubbles up.
|
|
14
|
-
export async function handleIssueResource(args: {
|
|
15
|
-
context: SentryWebhookContext;
|
|
16
|
-
reply: FastifyReply;
|
|
17
|
-
deliveryId: string;
|
|
18
|
-
rawBody: string;
|
|
19
|
-
}): Promise<null> {
|
|
20
|
-
const {context, reply, deliveryId, rawBody} = args;
|
|
21
|
-
|
|
22
|
-
const payload = await parseAndValidateOrDrop({
|
|
23
|
-
schema: sentryIssueWebhookSchema,
|
|
24
|
-
normalize: normalizeSentryIssueAction,
|
|
25
|
-
rawBody,
|
|
26
|
-
deliveryId,
|
|
27
|
-
context,
|
|
28
|
-
reply,
|
|
29
|
-
});
|
|
30
|
-
if (!payload) return null;
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
await context.coreDb().transaction(async (tx) => {
|
|
34
|
-
await handleSentryIssueEvent({
|
|
35
|
-
tx,
|
|
36
|
-
deliveryId,
|
|
37
|
-
payload,
|
|
38
|
-
publishIntegrationEventReceived: context.publishIntegrationEventReceived,
|
|
39
|
-
getIntegrationConnectionById: context.getIntegrationConnectionById,
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
} catch (error) {
|
|
43
|
-
if (error instanceof SentryIssueDroppedError) {
|
|
44
|
-
logger().warn({deliveryId, err: error}, `sentry webhook: ${error.message}, dropping`);
|
|
45
|
-
return recordAndDrop({context, reply, deliveryId});
|
|
46
|
-
}
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
reply.code(204);
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import {logger} from '@shipfox/node-opentelemetry';
|
|
2
|
-
import type {FastifyReply} from 'fastify';
|
|
3
|
-
import type {z} from 'zod';
|
|
4
|
-
import type {SentryWebhookContext} from './webhook-context.js';
|
|
5
|
-
|
|
6
|
-
export const SENTRY_PROVIDER = 'sentry';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Parses and validates a delivery body, recording-and-dropping it (HTTP 204) when
|
|
10
|
-
* the JSON is malformed or fails the schema (an unknown action falls here too).
|
|
11
|
-
* Returns the validated payload, or null when the delivery was dropped.
|
|
12
|
-
*/
|
|
13
|
-
export async function parseAndValidateOrDrop<TSchema extends z.ZodType>(args: {
|
|
14
|
-
schema: TSchema;
|
|
15
|
-
rawBody: string;
|
|
16
|
-
deliveryId: string;
|
|
17
|
-
context: SentryWebhookContext;
|
|
18
|
-
reply: FastifyReply;
|
|
19
|
-
normalize?: (parsedJson: unknown) => unknown;
|
|
20
|
-
}): Promise<z.infer<TSchema> | null> {
|
|
21
|
-
const {schema, rawBody, deliveryId, context, reply, normalize} = args;
|
|
22
|
-
|
|
23
|
-
let parsedJson: unknown;
|
|
24
|
-
try {
|
|
25
|
-
parsedJson = JSON.parse(rawBody);
|
|
26
|
-
} catch (error) {
|
|
27
|
-
logger().warn({deliveryId, err: error}, 'sentry webhook: payload JSON parse failed, dropping');
|
|
28
|
-
await recordAndDrop({context, reply, deliveryId});
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const validated = schema.safeParse(normalize ? normalize(parsedJson) : parsedJson);
|
|
33
|
-
if (!validated.success) {
|
|
34
|
-
logger().warn(
|
|
35
|
-
{deliveryId, issues: validated.error.issues},
|
|
36
|
-
'sentry webhook: payload failed validation (or unknown action), dropping',
|
|
37
|
-
);
|
|
38
|
-
await recordAndDrop({context, reply, deliveryId});
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return validated.data;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Records the delivery for dedup and replies 204 without acting on it. A sustained
|
|
47
|
-
* non-2xx can degrade or disable the webhook on Sentry's side (a deliberate
|
|
48
|
-
* deviation from GitHub's 400), so deliveries we cannot use are acknowledged.
|
|
49
|
-
*/
|
|
50
|
-
export async function recordAndDrop(args: {
|
|
51
|
-
context: SentryWebhookContext;
|
|
52
|
-
reply: FastifyReply;
|
|
53
|
-
deliveryId: string;
|
|
54
|
-
}): Promise<null> {
|
|
55
|
-
const {context, reply, deliveryId} = args;
|
|
56
|
-
await context.coreDb().transaction(async (tx) => {
|
|
57
|
-
await context.recordDeliveryOnly({tx, provider: SENTRY_PROVIDER, deliveryId});
|
|
58
|
-
});
|
|
59
|
-
reply.code(204);
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import {Buffer} from 'node:buffer';
|
|
2
|
-
import {ClientError} from '@shipfox/node-fastify';
|
|
3
|
-
import type {FastifyRequest} from 'fastify';
|
|
4
|
-
|
|
5
|
-
const REQUEST_ID_HEADER = 'request-id';
|
|
6
|
-
const RESOURCE_HEADER = 'sentry-hook-resource';
|
|
7
|
-
const SIGNATURE_HEADER = 'sentry-hook-signature';
|
|
8
|
-
// Sentry has used both header names; accepting both keeps older deliveries verifiable.
|
|
9
|
-
const LEGACY_SIGNATURE_HEADER = 'sentry-app-signature';
|
|
10
|
-
|
|
11
|
-
export interface SentryWebhookRequest {
|
|
12
|
-
deliveryId: string;
|
|
13
|
-
resource: string;
|
|
14
|
-
signature: string;
|
|
15
|
-
signatureHeaderName: string;
|
|
16
|
-
rawBody: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Extracts and structurally validates the parts every Sentry delivery must carry.
|
|
20
|
-
// Throws ClientError for anything a genuine signed delivery would never omit; the
|
|
21
|
-
// cryptographic signature check happens in the route once the secret is in hand.
|
|
22
|
-
export function parseSentryWebhookRequest(request: FastifyRequest): SentryWebhookRequest {
|
|
23
|
-
const deliveryId = request.headers[REQUEST_ID_HEADER];
|
|
24
|
-
if (typeof deliveryId !== 'string' || !deliveryId) {
|
|
25
|
-
throw new ClientError('missing Request-ID header', 'missing-request-id');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const resource = request.headers[RESOURCE_HEADER];
|
|
29
|
-
if (typeof resource !== 'string' || !resource) {
|
|
30
|
-
throw new ClientError('missing Sentry-Hook-Resource header', 'missing-resource');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const signature = resolveSignature(request);
|
|
34
|
-
if (!signature) {
|
|
35
|
-
throw new ClientError('missing Sentry-Hook-Signature header', 'missing-signature', {
|
|
36
|
-
status: 401,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const body = request.body;
|
|
41
|
-
if (!Buffer.isBuffer(body)) {
|
|
42
|
-
throw new ClientError('expected raw JSON body', 'invalid-body');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
deliveryId,
|
|
47
|
-
resource,
|
|
48
|
-
signature: signature.value,
|
|
49
|
-
signatureHeaderName: signature.headerName,
|
|
50
|
-
rawBody: body.toString('utf8'),
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function resolveSignature(
|
|
55
|
-
request: FastifyRequest,
|
|
56
|
-
): {value: string; headerName: string} | undefined {
|
|
57
|
-
const header = request.headers[SIGNATURE_HEADER];
|
|
58
|
-
if (typeof header === 'string' && header) {
|
|
59
|
-
return {value: header, headerName: SIGNATURE_HEADER};
|
|
60
|
-
}
|
|
61
|
-
const legacyHeader = request.headers[LEGACY_SIGNATURE_HEADER];
|
|
62
|
-
if (typeof legacyHeader === 'string' && legacyHeader) {
|
|
63
|
-
return {value: legacyHeader, headerName: LEGACY_SIGNATURE_HEADER};
|
|
64
|
-
}
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|