@shipfox/api-integration-sentry 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 +192 -0
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/dist/api/client.d.ts +24 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +101 -0
- package/dist/api/client.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +32 -0
- package/dist/config.js.map +1 -0
- package/dist/core/errors.d.ts +57 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +67 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/install.d.ts +63 -0
- package/dist/core/install.d.ts.map +1 -0
- package/dist/core/install.js +209 -0
- package/dist/core/install.js.map +1 -0
- package/dist/core/signature.d.ts +7 -0
- package/dist/core/signature.d.ts.map +1 -0
- package/dist/core/signature.js +6 -0
- package/dist/core/signature.js.map +1 -0
- package/dist/core/webhook.d.ts +49 -0
- package/dist/core/webhook.d.ts.map +1 -0
- package/dist/core/webhook.js +175 -0
- package/dist/core/webhook.js.map +1 -0
- package/dist/db/db.d.ts +327 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +18 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/installations.d.ts +74 -0
- package/dist/db/installations.d.ts.map +1 -0
- package/dist/db/installations.js +120 -0
- package/dist/db/installations.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +5 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/schema/common.d.ts +2 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +4 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/installations.d.ts +165 -0
- package/dist/db/schema/installations.d.ts.map +1 -0
- package/dist/db/schema/installations.js +42 -0
- package/dist/db/schema/installations.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/presentation/dto/integrations.d.ts +15 -0
- package/dist/presentation/dto/integrations.d.ts.map +1 -0
- package/dist/presentation/dto/integrations.js +9 -0
- package/dist/presentation/dto/integrations.js.map +1 -0
- package/dist/presentation/routes/errors.d.ts +2 -0
- package/dist/presentation/routes/errors.d.ts.map +1 -0
- package/dist/presentation/routes/errors.js +51 -0
- package/dist/presentation/routes/errors.js.map +1 -0
- package/dist/presentation/routes/install.d.ts +16 -0
- package/dist/presentation/routes/install.d.ts.map +1 -0
- package/dist/presentation/routes/install.js +76 -0
- package/dist/presentation/routes/install.js.map +1 -0
- package/dist/presentation/routes/installation-handler.d.ts +9 -0
- package/dist/presentation/routes/installation-handler.d.ts.map +1 -0
- package/dist/presentation/routes/installation-handler.js +70 -0
- package/dist/presentation/routes/installation-handler.js.map +1 -0
- package/dist/presentation/routes/issue-handler.d.ts +9 -0
- package/dist/presentation/routes/issue-handler.d.ts.map +1 -0
- package/dist/presentation/routes/issue-handler.js +50 -0
- package/dist/presentation/routes/issue-handler.js.map +1 -0
- package/dist/presentation/routes/webhook-context.d.ts +20 -0
- package/dist/presentation/routes/webhook-context.d.ts.map +1 -0
- package/dist/presentation/routes/webhook-context.js +10 -0
- package/dist/presentation/routes/webhook-context.js.map +1 -0
- package/dist/presentation/routes/webhook-delivery.d.ts +28 -0
- package/dist/presentation/routes/webhook-delivery.d.ts.map +1 -0
- package/dist/presentation/routes/webhook-delivery.js +56 -0
- package/dist/presentation/routes/webhook-delivery.js.map +1 -0
- package/dist/presentation/routes/webhook-request.d.ts +10 -0
- package/dist/presentation/routes/webhook-request.d.ts.map +1 -0
- package/dist/presentation/routes/webhook-request.js +56 -0
- package/dist/presentation/routes/webhook-request.js.map +1 -0
- package/dist/presentation/routes/webhooks.d.ts +5 -0
- package/dist/presentation/routes/webhooks.d.ts.map +1 -0
- package/dist/presentation/routes/webhooks.js +71 -0
- package/dist/presentation/routes/webhooks.js.map +1 -0
- package/dist/temporal/activities/index.d.ts +5 -0
- package/dist/temporal/activities/index.d.ts.map +1 -0
- package/dist/temporal/activities/index.js +8 -0
- package/dist/temporal/activities/index.js.map +1 -0
- package/dist/temporal/activities/prune-unclaimed-installations.d.ts +4 -0
- package/dist/temporal/activities/prune-unclaimed-installations.d.ts.map +1 -0
- package/dist/temporal/activities/prune-unclaimed-installations.js +11 -0
- package/dist/temporal/activities/prune-unclaimed-installations.js.map +1 -0
- package/dist/temporal/constants.d.ts +2 -0
- package/dist/temporal/constants.d.ts.map +1 -0
- package/dist/temporal/constants.js +3 -0
- package/dist/temporal/constants.js.map +1 -0
- package/dist/temporal/workflows/index.d.ts +2 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -0
- package/dist/temporal/workflows/index.js +3 -0
- package/dist/temporal/workflows/index.js.map +1 -0
- package/dist/temporal/workflows/prune-unclaimed-installations-cron.d.ts +2 -0
- package/dist/temporal/workflows/prune-unclaimed-installations-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/prune-unclaimed-installations-cron.js +14 -0
- package/dist/temporal/workflows/prune-unclaimed-installations-cron.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_initial.sql +14 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/package.json +70 -0
- package/src/api/client.test.ts +147 -0
- package/src/api/client.ts +147 -0
- package/src/config.test.ts +16 -0
- package/src/config.ts +35 -0
- package/src/connection-external-url.test.ts +79 -0
- package/src/core/errors.ts +81 -0
- package/src/core/install.test.ts +343 -0
- package/src/core/install.ts +305 -0
- package/src/core/signature.test.ts +50 -0
- package/src/core/signature.ts +11 -0
- package/src/core/webhook.test.ts +118 -0
- package/src/core/webhook.ts +246 -0
- package/src/db/db.ts +18 -0
- package/src/db/installations.test.ts +224 -0
- package/src/db/installations.ts +207 -0
- package/src/db/migrations.ts +4 -0
- package/src/db/schema/common.ts +3 -0
- package/src/db/schema/installations.ts +46 -0
- package/src/index.test.ts +18 -0
- package/src/index.ts +127 -0
- package/src/presentation/dto/integrations.ts +9 -0
- package/src/presentation/routes/errors.ts +48 -0
- package/src/presentation/routes/install.test.ts +325 -0
- package/src/presentation/routes/install.ts +102 -0
- package/src/presentation/routes/installation-handler.ts +76 -0
- package/src/presentation/routes/issue-handler.ts +52 -0
- package/src/presentation/routes/webhook-context.ts +25 -0
- package/src/presentation/routes/webhook-delivery.ts +61 -0
- package/src/presentation/routes/webhook-request.ts +66 -0
- package/src/presentation/routes/webhooks.test.ts +773 -0
- package/src/presentation/routes/webhooks.ts +59 -0
- package/src/temporal/activities/index.ts +7 -0
- package/src/temporal/activities/prune-unclaimed-installations.test.ts +52 -0
- package/src/temporal/activities/prune-unclaimed-installations.ts +11 -0
- package/src/temporal/constants.ts +1 -0
- package/src/temporal/workflows/index.ts +1 -0
- package/src/temporal/workflows/prune-unclaimed-installations-cron.ts +15 -0
- package/test/env.ts +11 -0
- package/test/factories/index.ts +9 -0
- package/test/factories/sentry-installation-webhook.ts +35 -0
- package/test/factories/sentry-installation.ts +44 -0
- package/test/factories/sentry-issue-webhook.ts +34 -0
- package/test/globalSetup.ts +14 -0
- package/test/index.ts +9 -0
- package/test/setup.ts +17 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +9 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,70 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GetIntegrationConnectionByIdFn, PublishIntegrationEventReceivedFn, RecordDeliveryOnlyFn, UpdateIntegrationConnectionLifecycleStatusFn } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
3
|
+
import type { SentryApiClient } from '#api/client.js';
|
|
4
|
+
/**
|
|
5
|
+
* The capabilities a Sentry webhook handler needs from the rest of the system.
|
|
6
|
+
* `@shipfox/api-integration-core` owns and wires the core persistence in
|
|
7
|
+
* production; tests fake them with spies. Passing them in keeps the route
|
|
8
|
+
* decoupled from core's persistence and avoids a package dependency cycle. The
|
|
9
|
+
* Sentry client is injected too so the authoritative `installation.created` path
|
|
10
|
+
* can exchange the code, and tests can fake the exchange.
|
|
11
|
+
*/
|
|
12
|
+
export interface SentryWebhookContext {
|
|
13
|
+
sentry: SentryApiClient;
|
|
14
|
+
coreDb: () => NodePgDatabase<Record<string, unknown>>;
|
|
15
|
+
publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;
|
|
16
|
+
recordDeliveryOnly: RecordDeliveryOnlyFn;
|
|
17
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
18
|
+
updateConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=webhook-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-context.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/webhook-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,8BAA8B,EAC9B,iCAAiC,EACjC,oBAAoB,EACpB,4CAA4C,EAC7C,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,MAAM,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,+BAA+B,EAAE,iCAAiC,CAAC;IACnE,kBAAkB,EAAE,oBAAoB,CAAC;IACzC,4BAA4B,EAAE,8BAA8B,CAAC;IAC7D,+BAA+B,EAAE,4CAA4C,CAAC;CAC/E"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capabilities a Sentry webhook handler needs from the rest of the system.
|
|
3
|
+
* `@shipfox/api-integration-core` owns and wires the core persistence in
|
|
4
|
+
* production; tests fake them with spies. Passing them in keeps the route
|
|
5
|
+
* decoupled from core's persistence and avoids a package dependency cycle. The
|
|
6
|
+
* Sentry client is injected too so the authoritative `installation.created` path
|
|
7
|
+
* can exchange the code, and tests can fake the exchange.
|
|
8
|
+
*/ export { };
|
|
9
|
+
|
|
10
|
+
//# sourceMappingURL=webhook-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/webhook-context.ts"],"sourcesContent":["import type {\n GetIntegrationConnectionByIdFn,\n PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n UpdateIntegrationConnectionLifecycleStatusFn,\n} from '@shipfox/api-integration-core-dto';\nimport type {NodePgDatabase} from 'drizzle-orm/node-postgres';\nimport type {SentryApiClient} from '#api/client.js';\n\n/**\n * The capabilities a Sentry webhook handler needs from the rest of the system.\n * `@shipfox/api-integration-core` owns and wires the core persistence in\n * production; tests fake them with spies. Passing them in keeps the route\n * decoupled from core's persistence and avoids a package dependency cycle. The\n * Sentry client is injected too so the authoritative `installation.created` path\n * can exchange the code, and tests can fake the exchange.\n */\nexport interface SentryWebhookContext {\n sentry: SentryApiClient;\n coreDb: () => NodePgDatabase<Record<string, unknown>>;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n updateConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;\n}\n"],"names":[],"mappings":"AASA;;;;;;;CAOC,GACD,WAOC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type RouteGroup } from '@shipfox/node-fastify';
|
|
2
|
+
import type { SentryWebhookContext } from './webhook-context.js';
|
|
3
|
+
export type { SentryWebhookContext } from './webhook-context.js';
|
|
4
|
+
export declare function createSentryWebhookRoutes(context: SentryWebhookContext): RouteGroup;
|
|
5
|
+
//# sourceMappingURL=webhooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/webhooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,UAAU,EAGhB,MAAM,uBAAuB,CAAC;AAM/B,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,sBAAsB,CAAC;AAO/D,YAAY,EAAC,oBAAoB,EAAC,MAAM,sBAAsB,CAAC;AAE/D,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,oBAAoB,GAAG,UAAU,CAqCnF"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ClientError, defineRoute, rawBodyPlugin, WEBHOOK_BODY_LIMIT } from '@shipfox/node-fastify';
|
|
2
|
+
import { logger } from '@shipfox/node-opentelemetry';
|
|
3
|
+
import { config } from '#config.js';
|
|
4
|
+
import { verifySentrySignature } from '#core/signature.js';
|
|
5
|
+
import { handleInstallationResource } from './installation-handler.js';
|
|
6
|
+
import { handleIssueResource } from './issue-handler.js';
|
|
7
|
+
import { recordAndDrop } from './webhook-delivery.js';
|
|
8
|
+
import { parseSentryWebhookRequest } from './webhook-request.js';
|
|
9
|
+
const ISSUE_RESOURCE = 'issue';
|
|
10
|
+
const INSTALLATION_RESOURCE = 'installation';
|
|
11
|
+
export function createSentryWebhookRoutes(context) {
|
|
12
|
+
const webhookRoute = defineRoute({
|
|
13
|
+
method: 'POST',
|
|
14
|
+
path: '/',
|
|
15
|
+
auth: [],
|
|
16
|
+
description: 'Sentry integration webhook receiver.',
|
|
17
|
+
options: {
|
|
18
|
+
bodyLimit: WEBHOOK_BODY_LIMIT
|
|
19
|
+
},
|
|
20
|
+
handler: (request, reply)=>{
|
|
21
|
+
const { deliveryId, resource, signature, signatureHeaderName, rawBody } = parseSentryWebhookRequest(request);
|
|
22
|
+
if (!verifySentrySignature({
|
|
23
|
+
rawBody,
|
|
24
|
+
signature,
|
|
25
|
+
secret: config.SENTRY_APP_CLIENT_SECRET
|
|
26
|
+
})) {
|
|
27
|
+
throw new ClientError('invalid signature', 'invalid-signature', {
|
|
28
|
+
status: 401
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
logger().debug({
|
|
32
|
+
deliveryId,
|
|
33
|
+
signatureHeader: signatureHeaderName
|
|
34
|
+
}, 'sentry webhook: signature verified');
|
|
35
|
+
if (resource === ISSUE_RESOURCE) {
|
|
36
|
+
return handleIssueResource({
|
|
37
|
+
context,
|
|
38
|
+
reply,
|
|
39
|
+
deliveryId,
|
|
40
|
+
rawBody
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (resource === INSTALLATION_RESOURCE) {
|
|
44
|
+
return handleInstallationResource({
|
|
45
|
+
context,
|
|
46
|
+
reply,
|
|
47
|
+
deliveryId,
|
|
48
|
+
rawBody
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// Unsupported resources are acknowledged so Sentry does not retry or disable the app.
|
|
52
|
+
return recordAndDrop({
|
|
53
|
+
context,
|
|
54
|
+
reply,
|
|
55
|
+
deliveryId
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
prefix: '/webhooks/integrations/sentry',
|
|
61
|
+
auth: [],
|
|
62
|
+
plugins: [
|
|
63
|
+
rawBodyPlugin
|
|
64
|
+
],
|
|
65
|
+
routes: [
|
|
66
|
+
webhookRoute
|
|
67
|
+
]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
//# sourceMappingURL=webhooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/webhooks.ts"],"sourcesContent":["import {\n ClientError,\n defineRoute,\n type RouteGroup,\n rawBodyPlugin,\n WEBHOOK_BODY_LIMIT,\n} from '@shipfox/node-fastify';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {config} from '#config.js';\nimport {verifySentrySignature} from '#core/signature.js';\nimport {handleInstallationResource} from './installation-handler.js';\nimport {handleIssueResource} from './issue-handler.js';\nimport type {SentryWebhookContext} from './webhook-context.js';\nimport {recordAndDrop} from './webhook-delivery.js';\nimport {parseSentryWebhookRequest} from './webhook-request.js';\n\nconst ISSUE_RESOURCE = 'issue';\nconst INSTALLATION_RESOURCE = 'installation';\n\nexport type {SentryWebhookContext} from './webhook-context.js';\n\nexport function createSentryWebhookRoutes(context: SentryWebhookContext): RouteGroup {\n const webhookRoute = defineRoute({\n method: 'POST',\n path: '/',\n auth: [],\n description: 'Sentry integration webhook receiver.',\n options: {bodyLimit: WEBHOOK_BODY_LIMIT},\n handler: (request, reply) => {\n const {deliveryId, resource, signature, signatureHeaderName, rawBody} =\n parseSentryWebhookRequest(request);\n\n if (!verifySentrySignature({rawBody, signature, secret: config.SENTRY_APP_CLIENT_SECRET})) {\n throw new ClientError('invalid signature', 'invalid-signature', {status: 401});\n }\n logger().debug(\n {deliveryId, signatureHeader: signatureHeaderName},\n 'sentry webhook: signature verified',\n );\n\n if (resource === ISSUE_RESOURCE) {\n return handleIssueResource({context, reply, deliveryId, rawBody});\n }\n if (resource === INSTALLATION_RESOURCE) {\n return handleInstallationResource({context, reply, deliveryId, rawBody});\n }\n\n // Unsupported resources are acknowledged so Sentry does not retry or disable the app.\n return recordAndDrop({context, reply, deliveryId});\n },\n });\n\n return {\n prefix: '/webhooks/integrations/sentry',\n auth: [],\n plugins: [rawBodyPlugin],\n routes: [webhookRoute],\n };\n}\n"],"names":["ClientError","defineRoute","rawBodyPlugin","WEBHOOK_BODY_LIMIT","logger","config","verifySentrySignature","handleInstallationResource","handleIssueResource","recordAndDrop","parseSentryWebhookRequest","ISSUE_RESOURCE","INSTALLATION_RESOURCE","createSentryWebhookRoutes","context","webhookRoute","method","path","auth","description","options","bodyLimit","handler","request","reply","deliveryId","resource","signature","signatureHeaderName","rawBody","secret","SENTRY_APP_CLIENT_SECRET","status","debug","signatureHeader","prefix","plugins","routes"],"mappings":"AAAA,SACEA,WAAW,EACXC,WAAW,EAEXC,aAAa,EACbC,kBAAkB,QACb,wBAAwB;AAC/B,SAAQC,MAAM,QAAO,8BAA8B;AACnD,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,qBAAqB,QAAO,qBAAqB;AACzD,SAAQC,0BAA0B,QAAO,4BAA4B;AACrE,SAAQC,mBAAmB,QAAO,qBAAqB;AAEvD,SAAQC,aAAa,QAAO,wBAAwB;AACpD,SAAQC,yBAAyB,QAAO,uBAAuB;AAE/D,MAAMC,iBAAiB;AACvB,MAAMC,wBAAwB;AAI9B,OAAO,SAASC,0BAA0BC,OAA6B;IACrE,MAAMC,eAAed,YAAY;QAC/Be,QAAQ;QACRC,MAAM;QACNC,MAAM,EAAE;QACRC,aAAa;QACbC,SAAS;YAACC,WAAWlB;QAAkB;QACvCmB,SAAS,CAACC,SAASC;YACjB,MAAM,EAACC,UAAU,EAAEC,QAAQ,EAAEC,SAAS,EAAEC,mBAAmB,EAAEC,OAAO,EAAC,GACnEnB,0BAA0Ba;YAE5B,IAAI,CAACjB,sBAAsB;gBAACuB;gBAASF;gBAAWG,QAAQzB,OAAO0B,wBAAwB;YAAA,IAAI;gBACzF,MAAM,IAAI/B,YAAY,qBAAqB,qBAAqB;oBAACgC,QAAQ;gBAAG;YAC9E;YACA5B,SAAS6B,KAAK,CACZ;gBAACR;gBAAYS,iBAAiBN;YAAmB,GACjD;YAGF,IAAIF,aAAaf,gBAAgB;gBAC/B,OAAOH,oBAAoB;oBAACM;oBAASU;oBAAOC;oBAAYI;gBAAO;YACjE;YACA,IAAIH,aAAad,uBAAuB;gBACtC,OAAOL,2BAA2B;oBAACO;oBAASU;oBAAOC;oBAAYI;gBAAO;YACxE;YAEA,sFAAsF;YACtF,OAAOpB,cAAc;gBAACK;gBAASU;gBAAOC;YAAU;QAClD;IACF;IAEA,OAAO;QACLU,QAAQ;QACRjB,MAAM,EAAE;QACRkB,SAAS;YAAClC;SAAc;QACxBmC,QAAQ;YAACtB;SAAa;IACxB;AACF"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { pruneUnclaimedSentryInstallationsActivity } from './prune-unclaimed-installations.js';
|
|
2
|
+
export declare function createSentryMaintenanceActivities(): {
|
|
3
|
+
pruneUnclaimedSentryInstallationsActivity: typeof pruneUnclaimedSentryInstallationsActivity;
|
|
4
|
+
};
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/temporal/activities/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,yCAAyC,EAAC,MAAM,oCAAoC,CAAC;AAE7F,wBAAgB,iCAAiC;;EAIhD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/temporal/activities/index.ts"],"sourcesContent":["import {pruneUnclaimedSentryInstallationsActivity} from './prune-unclaimed-installations.js';\n\nexport function createSentryMaintenanceActivities() {\n return {\n pruneUnclaimedSentryInstallationsActivity,\n };\n}\n"],"names":["pruneUnclaimedSentryInstallationsActivity","createSentryMaintenanceActivities"],"mappings":"AAAA,SAAQA,yCAAyC,QAAO,qCAAqC;AAE7F,OAAO,SAASC;IACd,OAAO;QACLD;IACF;AACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prune-unclaimed-installations.d.ts","sourceRoot":"","sources":["../../../src/temporal/activities/prune-unclaimed-installations.ts"],"names":[],"mappings":"AAKA,wBAAsB,yCAAyC,IAAI,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAC,CAAC,CAK/F"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { config } from '#config.js';
|
|
2
|
+
import { pruneUnclaimedSentryInstallations } from '#db/installations.js';
|
|
3
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
4
|
+
export async function pruneUnclaimedSentryInstallationsActivity() {
|
|
5
|
+
const olderThan = new Date(Date.now() - config.SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS * MS_PER_DAY);
|
|
6
|
+
return await pruneUnclaimedSentryInstallations({
|
|
7
|
+
olderThan
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=prune-unclaimed-installations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/temporal/activities/prune-unclaimed-installations.ts"],"sourcesContent":["import {config} from '#config.js';\nimport {pruneUnclaimedSentryInstallations} from '#db/installations.js';\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\nexport async function pruneUnclaimedSentryInstallationsActivity(): Promise<{tombstoned: number}> {\n const olderThan = new Date(\n Date.now() - config.SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS * MS_PER_DAY,\n );\n return await pruneUnclaimedSentryInstallations({olderThan});\n}\n"],"names":["config","pruneUnclaimedSentryInstallations","MS_PER_DAY","pruneUnclaimedSentryInstallationsActivity","olderThan","Date","now","SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS"],"mappings":"AAAA,SAAQA,MAAM,QAAO,aAAa;AAClC,SAAQC,iCAAiC,QAAO,uBAAuB;AAEvE,MAAMC,aAAa,KAAK,KAAK,KAAK;AAElC,OAAO,eAAeC;IACpB,MAAMC,YAAY,IAAIC,KACpBA,KAAKC,GAAG,KAAKN,OAAOO,4CAA4C,GAAGL;IAErE,OAAO,MAAMD,kCAAkC;QAACG;IAAS;AAC3D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/temporal/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,6BAA6B,oCAAoC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/temporal/constants.ts"],"sourcesContent":["export const SENTRY_MAINTENANCE_TASK_QUEUE = 'integrations-sentry-maintenance';\n"],"names":["SENTRY_MAINTENANCE_TASK_QUEUE"],"mappings":"AAAA,OAAO,MAAMA,gCAAgC,kCAAkC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/temporal/workflows/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,qCAAqC,EAAC,MAAM,yCAAyC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/temporal/workflows/index.ts"],"sourcesContent":["export {pruneUnclaimedSentryInstallationsCron} from './prune-unclaimed-installations-cron.js';\n"],"names":["pruneUnclaimedSentryInstallationsCron"],"mappings":"AAAA,SAAQA,qCAAqC,QAAO,0CAA0C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prune-unclaimed-installations-cron.d.ts","sourceRoot":"","sources":["../../../src/temporal/workflows/prune-unclaimed-installations-cron.ts"],"names":[],"mappings":"AASA,wBAAsB,qCAAqC,IAAI,OAAO,CAAC,IAAI,CAAC,CAK3E"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { log, proxyActivities } from '@temporalio/workflow';
|
|
2
|
+
const { pruneUnclaimedSentryInstallationsActivity } = proxyActivities({
|
|
3
|
+
startToCloseTimeout: '5 minutes'
|
|
4
|
+
});
|
|
5
|
+
export async function pruneUnclaimedSentryInstallationsCron() {
|
|
6
|
+
const { tombstoned } = await pruneUnclaimedSentryInstallationsActivity();
|
|
7
|
+
if (tombstoned > 0) {
|
|
8
|
+
log.info('Tombstoned stale unclaimed Sentry installations', {
|
|
9
|
+
tombstoned
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=prune-unclaimed-installations-cron.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/temporal/workflows/prune-unclaimed-installations-cron.ts"],"sourcesContent":["import {log, proxyActivities} from '@temporalio/workflow';\nimport type {createSentryMaintenanceActivities} from '../activities/index.js';\n\nconst {pruneUnclaimedSentryInstallationsActivity} = proxyActivities<\n ReturnType<typeof createSentryMaintenanceActivities>\n>({\n startToCloseTimeout: '5 minutes',\n});\n\nexport async function pruneUnclaimedSentryInstallationsCron(): Promise<void> {\n const {tombstoned} = await pruneUnclaimedSentryInstallationsActivity();\n if (tombstoned > 0) {\n log.info('Tombstoned stale unclaimed Sentry installations', {tombstoned});\n }\n}\n"],"names":["log","proxyActivities","pruneUnclaimedSentryInstallationsActivity","startToCloseTimeout","pruneUnclaimedSentryInstallationsCron","tombstoned","info"],"mappings":"AAAA,SAAQA,GAAG,EAAEC,eAAe,QAAO,uBAAuB;AAG1D,MAAM,EAACC,yCAAyC,EAAC,GAAGD,gBAElD;IACAE,qBAAqB;AACvB;AAEA,OAAO,eAAeC;IACpB,MAAM,EAACC,UAAU,EAAC,GAAG,MAAMH;IAC3B,IAAIG,aAAa,GAAG;QAClBL,IAAIM,IAAI,CAAC,mDAAmD;YAACD;QAAU;IACzE;AACF"}
|