@shipfox/api-integration-sentry 7.1.0 → 9.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +41 -0
- package/README.md +3 -8
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +1 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/install.d.ts +1 -1
- package/dist/core/install.d.ts.map +1 -1
- package/dist/core/install.js.map +1 -1
- package/dist/core/webhook-processor.d.ts +1 -1
- package/dist/core/webhook-processor.d.ts.map +1 -1
- package/dist/core/webhook-processor.js +1 -1
- package/dist/core/webhook-processor.js.map +1 -1
- package/dist/core/webhook.d.ts +1 -1
- package/dist/core/webhook.d.ts.map +1 -1
- package/dist/core/webhook.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/dto/integrations.d.ts +6 -7
- package/dist/presentation/dto/integrations.d.ts.map +1 -1
- package/dist/presentation/dto/integrations.js +12 -4
- package/dist/presentation/dto/integrations.js.map +1 -1
- package/dist/presentation/routes/errors.js +1 -1
- package/dist/presentation/routes/errors.js.map +1 -1
- package/dist/presentation/routes/install.d.ts +1 -1
- package/dist/presentation/routes/install.d.ts.map +1 -1
- package/dist/presentation/routes/install.js.map +1 -1
- package/dist/presentation/routes/webhook-context.d.ts +1 -1
- package/dist/presentation/routes/webhook-context.d.ts.map +1 -1
- package/dist/presentation/routes/webhook-context.js.map +1 -1
- package/dist/presentation/routes/webhooks.d.ts.map +1 -1
- package/dist/presentation/routes/webhooks.js +1 -1
- package/dist/presentation/routes/webhooks.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +13 -14
- package/src/core/errors.ts +1 -1
- package/src/core/install.test.ts +1 -1
- package/src/core/install.ts +1 -1
- package/src/core/webhook-processor.test.ts +1 -4
- package/src/core/webhook-processor.ts +5 -5
- package/src/core/webhook.ts +2 -2
- package/src/index.ts +1 -1
- package/src/presentation/dto/integrations.ts +13 -5
- package/src/presentation/routes/errors.ts +1 -1
- package/src/presentation/routes/install.test.ts +1 -1
- package/src/presentation/routes/install.ts +1 -1
- package/src/presentation/routes/webhook-context.ts +1 -1
- package/src/presentation/routes/webhooks.test.ts +1 -1
- package/src/presentation/routes/webhooks.ts +2 -5
- package/tsconfig.build.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
$ shipfox-swc && shipfox-temporal-bundle dist/temporal/workflows/index.js
|
|
2
|
-
Successfully compiled: 23 files with swc (
|
|
3
|
-
2026-07-
|
|
2
|
+
Successfully compiled: 23 files with swc (293.38ms)
|
|
3
|
+
2026-07-23T14:12:11.275Z [INFO] asset workflow-bundle-46314ad53629a6893a6d.js 3.17 MiB [emitted] [immutable] (name: main)
|
|
4
4
|
orphan modules 33.5 KiB [orphan] 21 modules
|
|
5
5
|
runtime modules 1.13 KiB 5 modules
|
|
6
6
|
modules by path ../../../../node_modules/.pnpm/ 970 KiB 196 modules
|
|
@@ -12,6 +12,6 @@ optional modules 30 bytes [optional]
|
|
|
12
12
|
__temporal_custom_payload_converter (ignored) 15 bytes [optional] [built] [code generated]
|
|
13
13
|
__temporal_custom_failure_converter (ignored) 15 bytes [optional] [built] [code generated]
|
|
14
14
|
../../../shared/node/temporal/dist/workflow-error-interceptor.js 1.21 KiB [built] [code generated]
|
|
15
|
-
webpack 5.107.2 compiled successfully in
|
|
16
|
-
2026-07-
|
|
15
|
+
webpack 5.107.2 compiled successfully in 3023 ms
|
|
16
|
+
2026-07-23T14:12:11.280Z [INFO] Workflow bundle created { size: '3.17MB' }
|
|
17
17
|
/home/runner/work/shipfox/shipfox/libs/api/integration/sentry/dist/temporal/workflows/index.js: 3323961 bytes
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# @shipfox/api-integration-sentry
|
|
2
2
|
|
|
3
|
+
## 9.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 475ce59: Republishes all public packages after restoring release authorization.
|
|
8
|
+
- Updated dependencies [475ce59]
|
|
9
|
+
- Updated dependencies [154e03f]
|
|
10
|
+
- @shipfox/api-auth-context@9.0.1
|
|
11
|
+
- @shipfox/api-integration-sentry-dto@9.0.1
|
|
12
|
+
- @shipfox/api-integration-spi@0.2.1
|
|
13
|
+
- @shipfox/config@1.2.3
|
|
14
|
+
- @shipfox/node-drizzle@0.3.3
|
|
15
|
+
- @shipfox/node-fastify@0.3.1
|
|
16
|
+
- @shipfox/node-module@1.0.0
|
|
17
|
+
- @shipfox/node-opentelemetry@0.6.1
|
|
18
|
+
- @shipfox/node-postgres@0.4.3
|
|
19
|
+
|
|
20
|
+
## 9.0.0
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- 4a6d124: Separates Integrations provider SPI contracts from the public DTO surface.
|
|
25
|
+
- Updated dependencies [4a6d124]
|
|
26
|
+
- @shipfox/api-integration-spi@0.2.0
|
|
27
|
+
- @shipfox/api-auth-context@9.0.0
|
|
28
|
+
- @shipfox/api-integration-sentry-dto@9.0.0
|
|
29
|
+
- @shipfox/config@1.2.2
|
|
30
|
+
- @shipfox/node-drizzle@0.3.2
|
|
31
|
+
- @shipfox/node-fastify@0.3.0
|
|
32
|
+
- @shipfox/node-module@0.5.0
|
|
33
|
+
- @shipfox/node-opentelemetry@0.6.0
|
|
34
|
+
- @shipfox/node-postgres@0.4.2
|
|
35
|
+
|
|
36
|
+
## 8.0.0
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- Updated dependencies [7f227c6]
|
|
41
|
+
- @shipfox/api-integration-core-dto@8.0.0
|
|
42
|
+
- @shipfox/api-integration-sentry-dto@8.0.0
|
|
43
|
+
|
|
3
44
|
## 7.1.0
|
|
4
45
|
|
|
5
46
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -131,14 +131,9 @@ turbo type --filter=@shipfox/api-integration-sentry
|
|
|
131
131
|
turbo test --filter=@shipfox/api-integration-sentry
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
```sh
|
|
138
|
-
docker compose up -d
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
The test environment uses the `api_test` database, set in `test/env.ts`.
|
|
134
|
+
For repository test conventions, read the
|
|
135
|
+
[testing guide](../../../../docs/guides/testing.md). This package uses the
|
|
136
|
+
`api_test` database, set in `test/env.ts`.
|
|
142
137
|
|
|
143
138
|
## License
|
|
144
139
|
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IntegrationProviderError } from '@shipfox/api-integration-
|
|
1
|
+
import { IntegrationProviderError } from '@shipfox/api-integration-spi';
|
|
2
2
|
export declare class SentryIntegrationProviderError extends IntegrationProviderError {
|
|
3
3
|
}
|
|
4
4
|
export declare class SentryInstallationAlreadyLinkedError extends Error {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,wBAAwB,EAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,wBAAwB,EAAC,MAAM,8BAA8B,CAAC;AAEtE,qBAAa,8BAA+B,SAAQ,wBAAwB;CAAG;AAE/E,qBAAa,oCAAqC,SAAQ,KAAK;aACjC,gBAAgB,EAAE,MAAM;gBAAxB,gBAAgB,EAAE,MAAM;CAMrD;AAED;;;;GAIG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;aAC1B,gBAAgB,EAAE,MAAM;gBAAxB,gBAAgB,EAAE,MAAM;CAIrD;AAED;;;;GAIG;AACH,qBAAa,iCAAkC,SAAQ,KAAK;aAExC,gBAAgB,EAAE,MAAM;aACxB,iBAAiB;gBADjB,gBAAgB,EAAE,MAAM,EACxB,iBAAiB,SAAI;CAKxC;AAED;;;;;GAKG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;CAAG;AAErD,qBAAa,+BAAgC,SAAQ,uBAAuB;aAC9C,gBAAgB,EAAE,MAAM;gBAAxB,gBAAgB,EAAE,MAAM;CAIrD;AAED,qBAAa,8BAA+B,SAAQ,uBAAuB;aAC7C,gBAAgB,EAAE,MAAM;gBAAxB,gBAAgB,EAAE,MAAM;CAIrD;AAED,qBAAa,6BAA8B,SAAQ,uBAAuB;aAC5C,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM;CAIjD;AAED;;;;;GAKG;AACH,qBAAa,gCAAiC,SAAQ,uBAAuB;aAC/C,gBAAgB,EAAE,MAAM;gBAAxB,gBAAgB,EAAE,MAAM;CAIrD"}
|
package/dist/core/errors.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IntegrationProviderError } from '@shipfox/api-integration-
|
|
1
|
+
import { IntegrationProviderError } from '@shipfox/api-integration-spi';
|
|
2
2
|
export class SentryIntegrationProviderError extends IntegrationProviderError {
|
|
3
3
|
}
|
|
4
4
|
export class SentryInstallationAlreadyLinkedError extends Error {
|
package/dist/core/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/errors.ts"],"sourcesContent":["import {IntegrationProviderError} from '@shipfox/api-integration-
|
|
1
|
+
{"version":3,"sources":["../../src/core/errors.ts"],"sourcesContent":["import {IntegrationProviderError} from '@shipfox/api-integration-spi';\n\nexport class SentryIntegrationProviderError extends IntegrationProviderError {}\n\nexport class SentryInstallationAlreadyLinkedError extends Error {\n constructor(public readonly installationUuid: string) {\n super(\n `Sentry installation is already linked to another Shipfox workspace: ${installationUuid}`,\n );\n this.name = 'SentryInstallationAlreadyLinkedError';\n }\n}\n\n/**\n * The claim could not prove control of the named install: the presented code\n * neither exchanged successfully nor matched the stored `code_hash`. Surfaced as\n * a 403 so a forged or leaked bare uuid cannot bind someone else's install.\n */\nexport class SentryClaimProofMismatchError extends Error {\n constructor(public readonly installationUuid: string) {\n super(`Sentry claim could not be verified for installation: ${installationUuid}`);\n this.name = 'SentryClaimProofMismatchError';\n }\n}\n\n/**\n * A concurrent path (the signed webhook) is mid-exchange for this install, so the\n * verified row is not visible to the claim yet. Retryable: the existing client\n * backoff re-calls and finds the now-persisted row.\n */\nexport class SentryVerificationInProgressError extends Error {\n constructor(\n public readonly installationUuid: string,\n public readonly retryAfterSeconds = 2,\n ) {\n super(`Sentry installation verification is still in progress: ${installationUuid}`);\n this.name = 'SentryVerificationInProgressError';\n }\n}\n\n/**\n * Base for issue deliveries we received and authenticated but deliberately do\n * not publish. The webhook layer records them for dedup and acknowledges with a\n * 204 rather than treating them as failures — a sustained non-2xx can make Sentry\n * disable the webhook. Carries no HTTP concerns so workers/jobs can reuse it.\n */\nexport class SentryIssueDroppedError extends Error {}\n\nexport class SentryInstallationNotFoundError extends SentryIssueDroppedError {\n constructor(public readonly installationUuid: string) {\n super(`Sentry installation not found: ${installationUuid}`);\n this.name = 'SentryInstallationNotFoundError';\n }\n}\n\nexport class SentryInstallationDeletedError extends SentryIssueDroppedError {\n constructor(public readonly installationUuid: string) {\n super(`Sentry installation is deleted: ${installationUuid}`);\n this.name = 'SentryInstallationDeletedError';\n }\n}\n\nexport class SentryConnectionNotFoundError extends SentryIssueDroppedError {\n constructor(public readonly connectionId: string) {\n super(`Sentry installation has no connection: ${connectionId}`);\n this.name = 'SentryConnectionNotFoundError';\n }\n}\n\n/**\n * A verified install that no workspace has claimed yet (`connection_id IS NULL`),\n * so an issue delivery has no workspace to publish against. Dropped (204) like the\n * other pre-publish cases; kept distinct from SentryConnectionNotFoundError so the\n * carried identifier is unambiguously an installation uuid, not a connection id.\n */\nexport class SentryInstallationUnclaimedError extends SentryIssueDroppedError {\n constructor(public readonly installationUuid: string) {\n super(`Sentry installation is verified but not yet claimed: ${installationUuid}`);\n this.name = 'SentryInstallationUnclaimedError';\n }\n}\n"],"names":["IntegrationProviderError","SentryIntegrationProviderError","SentryInstallationAlreadyLinkedError","Error","installationUuid","name","SentryClaimProofMismatchError","SentryVerificationInProgressError","retryAfterSeconds","SentryIssueDroppedError","SentryInstallationNotFoundError","SentryInstallationDeletedError","SentryConnectionNotFoundError","connectionId","SentryInstallationUnclaimedError"],"mappings":"AAAA,SAAQA,wBAAwB,QAAO,+BAA+B;AAEtE,OAAO,MAAMC,uCAAuCD;AAA0B;AAE9E,OAAO,MAAME,6CAA6CC;IACxD,YAAY,AAAgBC,gBAAwB,CAAE;QACpD,KAAK,CACH,CAAC,oEAAoE,EAAEA,kBAAkB,QAFjEA,mBAAAA;QAI1B,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA;;;;CAIC,GACD,OAAO,MAAMC,sCAAsCH;IACjD,YAAY,AAAgBC,gBAAwB,CAAE;QACpD,KAAK,CAAC,CAAC,qDAAqD,EAAEA,kBAAkB,QADtDA,mBAAAA;QAE1B,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA;;;;CAIC,GACD,OAAO,MAAME,0CAA0CJ;IACrD,YACE,AAAgBC,gBAAwB,EACxC,AAAgBI,oBAAoB,CAAC,CACrC;QACA,KAAK,CAAC,CAAC,uDAAuD,EAAEJ,kBAAkB,QAHlEA,mBAAAA,uBACAI,oBAAAA;QAGhB,IAAI,CAACH,IAAI,GAAG;IACd;AACF;AAEA;;;;;CAKC,GACD,OAAO,MAAMI,gCAAgCN;AAAO;AAEpD,OAAO,MAAMO,wCAAwCD;IACnD,YAAY,AAAgBL,gBAAwB,CAAE;QACpD,KAAK,CAAC,CAAC,+BAA+B,EAAEA,kBAAkB,QADhCA,mBAAAA;QAE1B,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAMM,uCAAuCF;IAClD,YAAY,AAAgBL,gBAAwB,CAAE;QACpD,KAAK,CAAC,CAAC,gCAAgC,EAAEA,kBAAkB,QADjCA,mBAAAA;QAE1B,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAMO,sCAAsCH;IACjD,YAAY,AAAgBI,YAAoB,CAAE;QAChD,KAAK,CAAC,CAAC,uCAAuC,EAAEA,cAAc,QADpCA,eAAAA;QAE1B,IAAI,CAACR,IAAI,GAAG;IACd;AACF;AAEA;;;;;CAKC,GACD,OAAO,MAAMS,yCAAyCL;IACpD,YAAY,AAAgBL,gBAAwB,CAAE;QACpD,KAAK,CAAC,CAAC,qDAAqD,EAAEA,kBAAkB,QADtDA,mBAAAA;QAE1B,IAAI,CAACC,IAAI,GAAG;IACd;AACF"}
|
package/dist/core/install.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IntegrationConnection } from '@shipfox/api-integration-
|
|
1
|
+
import type { IntegrationConnection } from '@shipfox/api-integration-spi';
|
|
2
2
|
import type { SentryApiClient, SentryAuthorization } from '#api/client.js';
|
|
3
3
|
import type { PersistVerifiedUnclaimedInstallationParams, SentryInstallation } from '#db/installations.js';
|
|
4
4
|
export interface ConnectSentryInstallationInput {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/core/install.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/core/install.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,8BAA8B,CAAC;AAExE,OAAO,KAAK,EAAC,eAAe,EAAE,mBAAmB,EAAC,MAAM,gBAAgB,CAAC;AAQzE,OAAO,KAAK,EACV,0CAA0C,EAC1C,kBAAkB,EACnB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,8BAA8B;IAC7C,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAKD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAUD,MAAM,WAAW,2CAA2C;IAC1D,MAAM,EAAE,eAAe,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IAGb,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,aAAa,EAAE,OAAO,CAAC;IACvB,oCAAoC,EAAE,CACpC,KAAK,EAAE,0CAA0C,KAC9C,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,2CAA2C;IAC1D,YAAY,EAAE,kBAAkB,CAAC;IACjC,aAAa,EAAE,mBAAmB,CAAC;CACpC;AAED;;;;;;;;GAQG;AACH,wBAAsB,qCAAqC,CACzD,MAAM,EAAE,2CAA2C,GAClD,OAAO,CAAC,2CAA2C,CAAC,CA8BtD;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,eAAe,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,OAAO,CAAC;IACvB,qBAAqB,EAAE,CAAC,KAAK,EAAE;QAC7B,gBAAgB,EAAE,MAAM,CAAC;KAC1B,KAAK,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC;IAC9C,iBAAiB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC,CAAC;IACxF,yBAAyB,EAAE,CACzB,KAAK,EAAE,8BAA8B,KAClC,OAAO,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC9C,oCAAoC,EAAE,CACpC,KAAK,EAAE,0CAA0C,KAC9C,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAClC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,yBAAyB,GAChC,OAAO,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAC,CAiB1C;AAyID,wBAAsB,kCAAkC,CAAC,KAAK,EAAE;IAC9D,MAAM,EAAE,eAAe,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAehB"}
|
package/dist/core/install.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/install.ts"],"sourcesContent":["import {createHash, timingSafeEqual} from 'node:crypto';\nimport type {IntegrationConnection} from '@shipfox/api-integration-core-dto';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport type {SentryApiClient, SentryAuthorization} from '#api/client.js';\nimport {\n SentryClaimProofMismatchError,\n SentryInstallationAlreadyLinkedError,\n SentryInstallationDeletedError,\n SentryIntegrationProviderError,\n SentryVerificationInProgressError,\n} from '#core/errors.js';\nimport type {\n PersistVerifiedUnclaimedInstallationParams,\n SentryInstallation,\n} from '#db/installations.js';\n\nexport interface ConnectSentryInstallationInput {\n workspaceId: string;\n installationUuid: string;\n orgSlug: string;\n displayName: string;\n installerUserId: string;\n codeHash: string;\n}\n\n// sha256 of the single-use authorization code. Stored on the install row so the\n// claim can prove the claimant holds the same code Sentry issued, without ever\n// persisting a live credential (the code is dead once exchanged).\nexport function hashAuthorizationCode(code: string): string {\n return createHash('sha256').update(code).digest('hex');\n}\n\n// Constant-time compare of two sha256 hex digests, matching the HMAC check in\n// signature.ts. The length guard runs first because timingSafeEqual throws on a\n// length mismatch, and a digest's length is not secret.\nfunction codeHashesEqual(presented: string, stored: string): boolean {\n if (presented.length !== stored.length) return false;\n return timingSafeEqual(Buffer.from(presented), Buffer.from(stored));\n}\n\nexport interface VerifyAndPersistUnclaimedInstallationParams {\n sentry: SentryApiClient;\n installationUuid: string;\n code: string;\n // Known from the signed webhook payload; omit to derive it from Sentry after\n // the exchange (the browser-first claim carries no payload).\n orgSlug?: string | undefined;\n verifyInstall: boolean;\n persistVerifiedUnclaimedInstallation: (\n input: PersistVerifiedUnclaimedInstallationParams,\n ) => Promise<SentryInstallation>;\n}\n\nexport interface VerifyAndPersistUnclaimedInstallationResult {\n installation: SentryInstallation;\n authorization: SentryAuthorization;\n}\n\n/**\n * Security-critical exchange → persist → best-effort verify, shared by the signed\n * webhook and the browser-first claim. The exchange is the authenticity check and\n * spends the single-use code, so it runs OUTSIDE any DB transaction; the caller's\n * `persistVerifiedUnclaimedInstallation` owns the short transaction. The verify\n * runs AFTER the row is durably persisted, so a verify failure leaves a claimable\n * row rather than a Sentry-side \"installed\" state pointing at a row that was never\n * written. Never logs the raw code.\n */\nexport async function verifyAndPersistUnclaimedInstallation(\n params: VerifyAndPersistUnclaimedInstallationParams,\n): Promise<VerifyAndPersistUnclaimedInstallationResult> {\n const authorization = await params.sentry.exchangeAuthorizationCode({\n installationUuid: params.installationUuid,\n code: params.code,\n });\n\n const orgSlug =\n params.orgSlug ??\n (\n await params.sentry.getInstallation({\n installationUuid: params.installationUuid,\n token: authorization.token,\n })\n ).orgSlug;\n\n const installation = await params.persistVerifiedUnclaimedInstallation({\n installationUuid: params.installationUuid,\n orgSlug,\n codeHash: hashAuthorizationCode(params.code),\n });\n\n if (params.verifyInstall) {\n await verifySentryInstallationBestEffort({\n sentry: params.sentry,\n installationUuid: params.installationUuid,\n token: authorization.token,\n });\n }\n\n return {installation, authorization};\n}\n\nexport interface HandleSentryConnectParams {\n sentry: SentryApiClient;\n workspaceId: string;\n code: string;\n installationUuid: string;\n installerUserId: string;\n verifyInstall: boolean;\n getSentryInstallation: (input: {\n installationUuid: string;\n }) => Promise<SentryInstallation | undefined>;\n getConnectionById: (id: string) => Promise<IntegrationConnection<'sentry'> | undefined>;\n connectSentryInstallation: (\n input: ConnectSentryInstallationInput,\n ) => Promise<IntegrationConnection<'sentry'>>;\n persistVerifiedUnclaimedInstallation: (\n input: PersistVerifiedUnclaimedInstallationParams,\n ) => Promise<SentryInstallation>;\n}\n\n/**\n * Binds a verified Sentry installation to a workspace (the claim half of the\n * webhook-authoritative flow). The webhook persists the verified-unclaimed row;\n * this proves the claimant controls the install and sets `connection_id`.\n *\n * Proof rules (unified claim auth):\n * - exchange succeeds → browser-first winner or a re-entry with a fresh code.\n * - exchange \"already used\" + the presented code hashes to the stored hash →\n * the same code the webhook spent, so the claimant holds it (same-code race).\n * - exchange \"already used\" + the verified row is not visible yet → a concurrent\n * webhook is mid-exchange; retryable so the client backoff reconciles.\n * - anything else on an existing unclaimed row → proof mismatch (403, IDOR gate).\n */\nexport async function handleSentryConnect(\n params: HandleSentryConnectParams,\n): Promise<IntegrationConnection<'sentry'>> {\n const install = await params.getSentryInstallation({installationUuid: params.installationUuid});\n\n if (install) {\n if (install.status === 'deleted') {\n throw new SentryInstallationDeletedError(params.installationUuid);\n }\n if (install.connectionId) {\n return resolveClaimedInstall(params, install.connectionId);\n }\n if (!isVerifiedUnclaimed(install)) {\n throw new SentryVerificationInProgressError(params.installationUuid);\n }\n return claimVerifiedInstall(params, install);\n }\n\n return claimBrowserFirst(params);\n}\n\nasync function resolveClaimedInstall(\n params: HandleSentryConnectParams,\n connectionId: string,\n): Promise<IntegrationConnection<'sentry'>> {\n const connection = await params.getConnectionById(connectionId);\n if (connection && connection.workspaceId === params.workspaceId) {\n return connection;\n }\n throw new SentryInstallationAlreadyLinkedError(params.installationUuid);\n}\n\nasync function claimVerifiedInstall(\n params: HandleSentryConnectParams,\n install: VerifiedUnclaimedSentryInstallation,\n): Promise<IntegrationConnection<'sentry'>> {\n let authorization: SentryAuthorization;\n try {\n authorization = await params.sentry.exchangeAuthorizationCode({\n installationUuid: params.installationUuid,\n code: params.code,\n });\n } catch (error) {\n if (isCodeAlreadyUsed(error)) {\n if (\n install.codeHash &&\n codeHashesEqual(hashAuthorizationCode(params.code), install.codeHash)\n ) {\n return bindClaim(params, {orgSlug: install.orgSlug, codeHash: install.codeHash});\n }\n throw new SentryClaimProofMismatchError(params.installationUuid);\n }\n throw error;\n }\n\n const connection = await bindClaim(params, {\n orgSlug: install.orgSlug,\n codeHash: hashAuthorizationCode(params.code),\n });\n if (params.verifyInstall) {\n await verifySentryInstallationBestEffort({\n sentry: params.sentry,\n installationUuid: params.installationUuid,\n token: authorization.token,\n connectionId: connection.id,\n });\n }\n return connection;\n}\n\nasync function claimBrowserFirst(\n params: HandleSentryConnectParams,\n): Promise<IntegrationConnection<'sentry'>> {\n let result: VerifyAndPersistUnclaimedInstallationResult;\n try {\n result = await verifyAndPersistUnclaimedInstallation({\n sentry: params.sentry,\n installationUuid: params.installationUuid,\n code: params.code,\n verifyInstall: false,\n persistVerifiedUnclaimedInstallation: params.persistVerifiedUnclaimedInstallation,\n });\n } catch (error) {\n if (isCodeAlreadyUsed(error)) {\n return reconcileConcurrentClaim(params);\n }\n throw error;\n }\n\n const connection = await bindClaim(params, {\n orgSlug: result.installation.orgSlug,\n codeHash: hashAuthorizationCode(params.code),\n });\n if (params.verifyInstall) {\n await verifySentryInstallationBestEffort({\n sentry: params.sentry,\n installationUuid: params.installationUuid,\n token: result.authorization.token,\n connectionId: connection.id,\n });\n }\n return connection;\n}\n\n// The browser-first exchange got \"already used\" with no row visible at lookup: a\n// concurrent webhook won the exchange. Re-read once — if its verified row is now\n// visible we reconcile through the same proof rules; if it got claimed we resolve\n// it; if it was tombstoned we surface that terminally (matching the top-level\n// check); otherwise it is still mid-flight, so the claim is retryable.\nasync function reconcileConcurrentClaim(\n params: HandleSentryConnectParams,\n): Promise<IntegrationConnection<'sentry'>> {\n const reread = await params.getSentryInstallation({installationUuid: params.installationUuid});\n if (reread?.status === 'deleted') {\n throw new SentryInstallationDeletedError(params.installationUuid);\n }\n if (reread) {\n if (reread.connectionId) {\n return resolveClaimedInstall(params, reread.connectionId);\n }\n if (isVerifiedUnclaimed(reread)) {\n return claimVerifiedInstall(params, reread);\n }\n }\n throw new SentryVerificationInProgressError(params.installationUuid);\n}\n\ntype VerifiedUnclaimedSentryInstallation = SentryInstallation & {\n connectionId: null;\n status: 'installed';\n orgSlug: string;\n codeHash: string;\n};\n\nfunction isVerifiedUnclaimed(\n install: SentryInstallation,\n): install is VerifiedUnclaimedSentryInstallation {\n return (\n install.connectionId === null && install.status === 'installed' && install.codeHash !== null\n );\n}\n\nfunction bindClaim(\n params: HandleSentryConnectParams,\n binding: {orgSlug: string; codeHash: string},\n): Promise<IntegrationConnection<'sentry'>> {\n return params.connectSentryInstallation({\n workspaceId: params.workspaceId,\n installationUuid: params.installationUuid,\n orgSlug: binding.orgSlug,\n displayName: `Sentry ${binding.orgSlug}`,\n installerUserId: params.installerUserId,\n codeHash: binding.codeHash,\n });\n}\n\nexport async function verifySentryInstallationBestEffort(input: {\n sentry: SentryApiClient;\n installationUuid: string;\n token: string;\n connectionId?: string;\n}): Promise<void> {\n // The row is already persisted and receiving webhooks, so a verify failure only\n // leaves the install pending on Sentry's side (re-verifying needs a fresh token\n // we cannot mint here). Log it rather than failing a working claim.\n try {\n await input.sentry.verifyInstallation({\n installationUuid: input.installationUuid,\n token: input.token,\n });\n } catch (error) {\n logger().warn(\n {installationUuid: input.installationUuid, connectionId: input.connectionId, err: error},\n 'sentry connect: verify-install failed after persistence',\n );\n }\n}\n\n// The Sentry client collapses a reused, expired, or forged code to a single\n// `access-denied` provider error; we cannot tell them apart from the response.\n// On an install we already know was verified, the most likely cause is the code\n// having been spent already, so the hash check disambiguates rather than the\n// error itself.\nfunction isCodeAlreadyUsed(error: unknown): boolean {\n return error instanceof SentryIntegrationProviderError && error.reason === 'access-denied';\n}\n"],"names":["createHash","timingSafeEqual","logger","SentryClaimProofMismatchError","SentryInstallationAlreadyLinkedError","SentryInstallationDeletedError","SentryIntegrationProviderError","SentryVerificationInProgressError","hashAuthorizationCode","code","update","digest","codeHashesEqual","presented","stored","length","Buffer","from","verifyAndPersistUnclaimedInstallation","params","authorization","sentry","exchangeAuthorizationCode","installationUuid","orgSlug","getInstallation","token","installation","persistVerifiedUnclaimedInstallation","codeHash","verifyInstall","verifySentryInstallationBestEffort","handleSentryConnect","install","getSentryInstallation","status","connectionId","resolveClaimedInstall","isVerifiedUnclaimed","claimVerifiedInstall","claimBrowserFirst","connection","getConnectionById","workspaceId","error","isCodeAlreadyUsed","bindClaim","id","result","reconcileConcurrentClaim","reread","binding","connectSentryInstallation","displayName","installerUserId","input","verifyInstallation","warn","err","reason"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/core/install.ts"],"sourcesContent":["import {createHash, timingSafeEqual} from 'node:crypto';\nimport type {IntegrationConnection} from '@shipfox/api-integration-spi';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport type {SentryApiClient, SentryAuthorization} from '#api/client.js';\nimport {\n SentryClaimProofMismatchError,\n SentryInstallationAlreadyLinkedError,\n SentryInstallationDeletedError,\n SentryIntegrationProviderError,\n SentryVerificationInProgressError,\n} from '#core/errors.js';\nimport type {\n PersistVerifiedUnclaimedInstallationParams,\n SentryInstallation,\n} from '#db/installations.js';\n\nexport interface ConnectSentryInstallationInput {\n workspaceId: string;\n installationUuid: string;\n orgSlug: string;\n displayName: string;\n installerUserId: string;\n codeHash: string;\n}\n\n// sha256 of the single-use authorization code. Stored on the install row so the\n// claim can prove the claimant holds the same code Sentry issued, without ever\n// persisting a live credential (the code is dead once exchanged).\nexport function hashAuthorizationCode(code: string): string {\n return createHash('sha256').update(code).digest('hex');\n}\n\n// Constant-time compare of two sha256 hex digests, matching the HMAC check in\n// signature.ts. The length guard runs first because timingSafeEqual throws on a\n// length mismatch, and a digest's length is not secret.\nfunction codeHashesEqual(presented: string, stored: string): boolean {\n if (presented.length !== stored.length) return false;\n return timingSafeEqual(Buffer.from(presented), Buffer.from(stored));\n}\n\nexport interface VerifyAndPersistUnclaimedInstallationParams {\n sentry: SentryApiClient;\n installationUuid: string;\n code: string;\n // Known from the signed webhook payload; omit to derive it from Sentry after\n // the exchange (the browser-first claim carries no payload).\n orgSlug?: string | undefined;\n verifyInstall: boolean;\n persistVerifiedUnclaimedInstallation: (\n input: PersistVerifiedUnclaimedInstallationParams,\n ) => Promise<SentryInstallation>;\n}\n\nexport interface VerifyAndPersistUnclaimedInstallationResult {\n installation: SentryInstallation;\n authorization: SentryAuthorization;\n}\n\n/**\n * Security-critical exchange → persist → best-effort verify, shared by the signed\n * webhook and the browser-first claim. The exchange is the authenticity check and\n * spends the single-use code, so it runs OUTSIDE any DB transaction; the caller's\n * `persistVerifiedUnclaimedInstallation` owns the short transaction. The verify\n * runs AFTER the row is durably persisted, so a verify failure leaves a claimable\n * row rather than a Sentry-side \"installed\" state pointing at a row that was never\n * written. Never logs the raw code.\n */\nexport async function verifyAndPersistUnclaimedInstallation(\n params: VerifyAndPersistUnclaimedInstallationParams,\n): Promise<VerifyAndPersistUnclaimedInstallationResult> {\n const authorization = await params.sentry.exchangeAuthorizationCode({\n installationUuid: params.installationUuid,\n code: params.code,\n });\n\n const orgSlug =\n params.orgSlug ??\n (\n await params.sentry.getInstallation({\n installationUuid: params.installationUuid,\n token: authorization.token,\n })\n ).orgSlug;\n\n const installation = await params.persistVerifiedUnclaimedInstallation({\n installationUuid: params.installationUuid,\n orgSlug,\n codeHash: hashAuthorizationCode(params.code),\n });\n\n if (params.verifyInstall) {\n await verifySentryInstallationBestEffort({\n sentry: params.sentry,\n installationUuid: params.installationUuid,\n token: authorization.token,\n });\n }\n\n return {installation, authorization};\n}\n\nexport interface HandleSentryConnectParams {\n sentry: SentryApiClient;\n workspaceId: string;\n code: string;\n installationUuid: string;\n installerUserId: string;\n verifyInstall: boolean;\n getSentryInstallation: (input: {\n installationUuid: string;\n }) => Promise<SentryInstallation | undefined>;\n getConnectionById: (id: string) => Promise<IntegrationConnection<'sentry'> | undefined>;\n connectSentryInstallation: (\n input: ConnectSentryInstallationInput,\n ) => Promise<IntegrationConnection<'sentry'>>;\n persistVerifiedUnclaimedInstallation: (\n input: PersistVerifiedUnclaimedInstallationParams,\n ) => Promise<SentryInstallation>;\n}\n\n/**\n * Binds a verified Sentry installation to a workspace (the claim half of the\n * webhook-authoritative flow). The webhook persists the verified-unclaimed row;\n * this proves the claimant controls the install and sets `connection_id`.\n *\n * Proof rules (unified claim auth):\n * - exchange succeeds → browser-first winner or a re-entry with a fresh code.\n * - exchange \"already used\" + the presented code hashes to the stored hash →\n * the same code the webhook spent, so the claimant holds it (same-code race).\n * - exchange \"already used\" + the verified row is not visible yet → a concurrent\n * webhook is mid-exchange; retryable so the client backoff reconciles.\n * - anything else on an existing unclaimed row → proof mismatch (403, IDOR gate).\n */\nexport async function handleSentryConnect(\n params: HandleSentryConnectParams,\n): Promise<IntegrationConnection<'sentry'>> {\n const install = await params.getSentryInstallation({installationUuid: params.installationUuid});\n\n if (install) {\n if (install.status === 'deleted') {\n throw new SentryInstallationDeletedError(params.installationUuid);\n }\n if (install.connectionId) {\n return resolveClaimedInstall(params, install.connectionId);\n }\n if (!isVerifiedUnclaimed(install)) {\n throw new SentryVerificationInProgressError(params.installationUuid);\n }\n return claimVerifiedInstall(params, install);\n }\n\n return claimBrowserFirst(params);\n}\n\nasync function resolveClaimedInstall(\n params: HandleSentryConnectParams,\n connectionId: string,\n): Promise<IntegrationConnection<'sentry'>> {\n const connection = await params.getConnectionById(connectionId);\n if (connection && connection.workspaceId === params.workspaceId) {\n return connection;\n }\n throw new SentryInstallationAlreadyLinkedError(params.installationUuid);\n}\n\nasync function claimVerifiedInstall(\n params: HandleSentryConnectParams,\n install: VerifiedUnclaimedSentryInstallation,\n): Promise<IntegrationConnection<'sentry'>> {\n let authorization: SentryAuthorization;\n try {\n authorization = await params.sentry.exchangeAuthorizationCode({\n installationUuid: params.installationUuid,\n code: params.code,\n });\n } catch (error) {\n if (isCodeAlreadyUsed(error)) {\n if (\n install.codeHash &&\n codeHashesEqual(hashAuthorizationCode(params.code), install.codeHash)\n ) {\n return bindClaim(params, {orgSlug: install.orgSlug, codeHash: install.codeHash});\n }\n throw new SentryClaimProofMismatchError(params.installationUuid);\n }\n throw error;\n }\n\n const connection = await bindClaim(params, {\n orgSlug: install.orgSlug,\n codeHash: hashAuthorizationCode(params.code),\n });\n if (params.verifyInstall) {\n await verifySentryInstallationBestEffort({\n sentry: params.sentry,\n installationUuid: params.installationUuid,\n token: authorization.token,\n connectionId: connection.id,\n });\n }\n return connection;\n}\n\nasync function claimBrowserFirst(\n params: HandleSentryConnectParams,\n): Promise<IntegrationConnection<'sentry'>> {\n let result: VerifyAndPersistUnclaimedInstallationResult;\n try {\n result = await verifyAndPersistUnclaimedInstallation({\n sentry: params.sentry,\n installationUuid: params.installationUuid,\n code: params.code,\n verifyInstall: false,\n persistVerifiedUnclaimedInstallation: params.persistVerifiedUnclaimedInstallation,\n });\n } catch (error) {\n if (isCodeAlreadyUsed(error)) {\n return reconcileConcurrentClaim(params);\n }\n throw error;\n }\n\n const connection = await bindClaim(params, {\n orgSlug: result.installation.orgSlug,\n codeHash: hashAuthorizationCode(params.code),\n });\n if (params.verifyInstall) {\n await verifySentryInstallationBestEffort({\n sentry: params.sentry,\n installationUuid: params.installationUuid,\n token: result.authorization.token,\n connectionId: connection.id,\n });\n }\n return connection;\n}\n\n// The browser-first exchange got \"already used\" with no row visible at lookup: a\n// concurrent webhook won the exchange. Re-read once — if its verified row is now\n// visible we reconcile through the same proof rules; if it got claimed we resolve\n// it; if it was tombstoned we surface that terminally (matching the top-level\n// check); otherwise it is still mid-flight, so the claim is retryable.\nasync function reconcileConcurrentClaim(\n params: HandleSentryConnectParams,\n): Promise<IntegrationConnection<'sentry'>> {\n const reread = await params.getSentryInstallation({installationUuid: params.installationUuid});\n if (reread?.status === 'deleted') {\n throw new SentryInstallationDeletedError(params.installationUuid);\n }\n if (reread) {\n if (reread.connectionId) {\n return resolveClaimedInstall(params, reread.connectionId);\n }\n if (isVerifiedUnclaimed(reread)) {\n return claimVerifiedInstall(params, reread);\n }\n }\n throw new SentryVerificationInProgressError(params.installationUuid);\n}\n\ntype VerifiedUnclaimedSentryInstallation = SentryInstallation & {\n connectionId: null;\n status: 'installed';\n orgSlug: string;\n codeHash: string;\n};\n\nfunction isVerifiedUnclaimed(\n install: SentryInstallation,\n): install is VerifiedUnclaimedSentryInstallation {\n return (\n install.connectionId === null && install.status === 'installed' && install.codeHash !== null\n );\n}\n\nfunction bindClaim(\n params: HandleSentryConnectParams,\n binding: {orgSlug: string; codeHash: string},\n): Promise<IntegrationConnection<'sentry'>> {\n return params.connectSentryInstallation({\n workspaceId: params.workspaceId,\n installationUuid: params.installationUuid,\n orgSlug: binding.orgSlug,\n displayName: `Sentry ${binding.orgSlug}`,\n installerUserId: params.installerUserId,\n codeHash: binding.codeHash,\n });\n}\n\nexport async function verifySentryInstallationBestEffort(input: {\n sentry: SentryApiClient;\n installationUuid: string;\n token: string;\n connectionId?: string;\n}): Promise<void> {\n // The row is already persisted and receiving webhooks, so a verify failure only\n // leaves the install pending on Sentry's side (re-verifying needs a fresh token\n // we cannot mint here). Log it rather than failing a working claim.\n try {\n await input.sentry.verifyInstallation({\n installationUuid: input.installationUuid,\n token: input.token,\n });\n } catch (error) {\n logger().warn(\n {installationUuid: input.installationUuid, connectionId: input.connectionId, err: error},\n 'sentry connect: verify-install failed after persistence',\n );\n }\n}\n\n// The Sentry client collapses a reused, expired, or forged code to a single\n// `access-denied` provider error; we cannot tell them apart from the response.\n// On an install we already know was verified, the most likely cause is the code\n// having been spent already, so the hash check disambiguates rather than the\n// error itself.\nfunction isCodeAlreadyUsed(error: unknown): boolean {\n return error instanceof SentryIntegrationProviderError && error.reason === 'access-denied';\n}\n"],"names":["createHash","timingSafeEqual","logger","SentryClaimProofMismatchError","SentryInstallationAlreadyLinkedError","SentryInstallationDeletedError","SentryIntegrationProviderError","SentryVerificationInProgressError","hashAuthorizationCode","code","update","digest","codeHashesEqual","presented","stored","length","Buffer","from","verifyAndPersistUnclaimedInstallation","params","authorization","sentry","exchangeAuthorizationCode","installationUuid","orgSlug","getInstallation","token","installation","persistVerifiedUnclaimedInstallation","codeHash","verifyInstall","verifySentryInstallationBestEffort","handleSentryConnect","install","getSentryInstallation","status","connectionId","resolveClaimedInstall","isVerifiedUnclaimed","claimVerifiedInstall","claimBrowserFirst","connection","getConnectionById","workspaceId","error","isCodeAlreadyUsed","bindClaim","id","result","reconcileConcurrentClaim","reread","binding","connectSentryInstallation","displayName","installerUserId","input","verifyInstallation","warn","err","reason"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type GetIntegrationConnectionByIdFn, type PublishIntegrationEventReceivedFn, type RecordDeliveryOnlyFn, type StoredWebhookRequest, type UpdateIntegrationConnectionLifecycleStatusFn, type WebhookProcessingResult } from '@shipfox/api-integration-
|
|
1
|
+
import { type GetIntegrationConnectionByIdFn, type PublishIntegrationEventReceivedFn, type RecordDeliveryOnlyFn, type StoredWebhookRequest, type UpdateIntegrationConnectionLifecycleStatusFn, type WebhookProcessingResult } from '@shipfox/api-integration-spi';
|
|
2
2
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
3
3
|
import type { SentryApiClient } from '#api/client.js';
|
|
4
4
|
export interface SentryWebhookProcessor {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook-processor.d.ts","sourceRoot":"","sources":["../../src/core/webhook-processor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"webhook-processor.d.ts","sourceRoot":"","sources":["../../src/core/webhook-processor.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,8BAA8B,EACnC,KAAK,iCAAiC,EACtC,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,4CAA4C,EACjD,KAAK,uBAAuB,EAC7B,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAuBpD,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;CAC1E;AAED,MAAM,WAAW,mCAAmC;IAClD,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;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,mCAAmC,GAC3C,sBAAsB,CAExB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import { decodeWebhookBody } from '@shipfox/api-integration-core-dto';
|
|
3
2
|
import { sentryInstallationWebhookSchema, sentryIssueWebhookSchema } from '@shipfox/api-integration-sentry-dto';
|
|
3
|
+
import { decodeWebhookBody } from '@shipfox/api-integration-spi';
|
|
4
4
|
import { logger } from '@shipfox/node-opentelemetry';
|
|
5
5
|
import { config } from '#config.js';
|
|
6
6
|
import { SentryIssueDroppedError } from '#core/errors.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/webhook-processor.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {\n decodeWebhookBody,\n type GetIntegrationConnectionByIdFn,\n type PublishIntegrationEventReceivedFn,\n type RecordDeliveryOnlyFn,\n type StoredWebhookRequest,\n type UpdateIntegrationConnectionLifecycleStatusFn,\n type WebhookProcessingResult,\n} from '@shipfox/api-integration-core-dto';\nimport {\n sentryInstallationWebhookSchema,\n sentryIssueWebhookSchema,\n} from '@shipfox/api-integration-sentry-dto';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport type {NodePgDatabase} from 'drizzle-orm/node-postgres';\nimport type {SentryApiClient} from '#api/client.js';\nimport {config} from '#config.js';\nimport {SentryIssueDroppedError} from '#core/errors.js';\nimport {\n handleSentryInstallationCreated,\n handleSentryInstallationDeleted,\n handleSentryIssueEvent,\n normalizeSentryIssueAction,\n} from '#core/webhook.js';\nimport {\n completeSentryInstallationVerification,\n getSentryInstallationByInstallationUuid,\n} from '#db/installations.js';\nimport {verifySentrySignature} from './signature.js';\n\nconst DELIVERY_ID_HEADER = 'request-id';\nconst RESOURCE_HEADER = 'sentry-hook-resource';\nconst SIGNATURE_HEADER = 'sentry-hook-signature';\nconst LEGACY_SIGNATURE_HEADER = 'sentry-app-signature';\nconst ISSUE_RESOURCE = 'issue';\nconst INSTALLATION_RESOURCE = 'installation';\nconst SENTRY_PROVIDER = 'sentry';\n\nexport interface SentryWebhookProcessor {\n process(request: StoredWebhookRequest): Promise<WebhookProcessingResult>;\n}\n\nexport interface CreateSentryWebhookProcessorOptions {\n sentry: SentryApiClient;\n coreDb: () => NodePgDatabase<Record<string, unknown>>;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n updateConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;\n}\n\nexport function createSentryWebhookProcessor(\n context: CreateSentryWebhookProcessorOptions,\n): SentryWebhookProcessor {\n return {process: (request) => processSentryWebhookRequest(context, request)};\n}\n\nasync function processSentryWebhookRequest(\n context: CreateSentryWebhookProcessorOptions,\n request: StoredWebhookRequest,\n): Promise<WebhookProcessingResult> {\n if (request.route_id !== 'sentry') {\n throw new Error(`Sentry processor cannot process ${request.route_id} requests`);\n }\n\n const deliveryId = request.headers[DELIVERY_ID_HEADER];\n const resource = request.headers[RESOURCE_HEADER];\n const signature = request.headers[SIGNATURE_HEADER] ?? request.headers[LEGACY_SIGNATURE_HEADER];\n if (!deliveryId || !resource || !signature) {\n return {outcome: 'discarded', reason: 'missing_required_input'};\n }\n\n const rawBody = Buffer.from(decodeWebhookBody(request.body));\n if (!verifySentrySignature({rawBody, signature, secret: config.SENTRY_APP_CLIENT_SECRET})) {\n return {outcome: 'discarded', reason: 'invalid_signature', deliveryId};\n }\n\n logger().debug(\n {\n deliveryId,\n signatureHeader: request.headers[SIGNATURE_HEADER]\n ? SIGNATURE_HEADER\n : LEGACY_SIGNATURE_HEADER,\n },\n 'sentry webhook: signature verified',\n );\n\n if (resource === ISSUE_RESOURCE) return processIssueResource(context, deliveryId, rawBody);\n if (resource === INSTALLATION_RESOURCE)\n return processInstallationResource(context, deliveryId, rawBody);\n\n await recordDeliveryOnly(context, deliveryId);\n return {outcome: 'discarded', reason: 'unsupported_event', deliveryId};\n}\n\nasync function processIssueResource(\n context: CreateSentryWebhookProcessorOptions,\n deliveryId: string,\n rawBody: Uint8Array,\n): Promise<WebhookProcessingResult> {\n const parsed = parsePayload(\n sentryIssueWebhookSchema,\n rawBody,\n deliveryId,\n normalizeSentryIssueAction,\n );\n if (!parsed.success) {\n await recordDeliveryOnly(context, deliveryId);\n return {outcome: 'discarded', reason: parsed.reason, deliveryId};\n }\n const payload = parsed.data;\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)) throw error;\n\n logger().warn({deliveryId, err: error}, `sentry webhook: ${error.message}, dropping`);\n await recordDeliveryOnly(context, deliveryId);\n return {outcome: 'discarded', reason: 'connection_unavailable', deliveryId};\n }\n\n return {outcome: 'processed', deliveryId};\n}\n\nasync function processInstallationResource(\n context: CreateSentryWebhookProcessorOptions,\n deliveryId: string,\n rawBody: Uint8Array,\n): Promise<WebhookProcessingResult> {\n const parsed = parsePayload(sentryInstallationWebhookSchema, rawBody, deliveryId);\n if (!parsed.success) {\n await recordDeliveryOnly(context, deliveryId);\n return {outcome: 'discarded', reason: parsed.reason, deliveryId};\n }\n const payload = parsed.data;\n\n const installation = payload.data.installation;\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 return {outcome: 'processed', deliveryId};\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, codeHash, deliveryId: id}) =>\n context.coreDb().transaction(async (tx) => {\n const completed = await completeSentryInstallationVerification(\n {installationUuid, codeHash},\n {tx},\n );\n await context.recordDeliveryOnly({tx, provider: SENTRY_PROVIDER, deliveryId: id});\n if (completed) return completed;\n\n const current = await getSentryInstallationByInstallationUuid(installationUuid, {tx});\n if (!current) throw new Error('Sentry installation verification lost its claim');\n return current;\n }),\n recordDelivery: (id) => recordDeliveryOnly(context, id),\n });\n\n return {outcome: 'processed', deliveryId};\n}\n\ntype ParsePayloadResult<T> =\n | {success: true; data: T}\n | {success: false; reason: 'malformed_payload' | 'unsupported_event'};\n\nfunction parsePayload<T>(\n schema: {safeParse(value: unknown): {success: true; data: T} | {success: false; error: unknown}},\n rawBody: Uint8Array,\n deliveryId: string,\n normalize: (payload: unknown) => unknown = (payload) => payload,\n): ParsePayloadResult<T> {\n let parsedJson: unknown;\n try {\n parsedJson = JSON.parse(Buffer.from(rawBody).toString('utf8'));\n } catch (error) {\n logger().warn({deliveryId, err: error}, 'sentry webhook: payload JSON parse failed, dropping');\n return {success: false, reason: 'malformed_payload'};\n }\n\n const payload = schema.safeParse(normalize(parsedJson));\n if (payload.success) return {success: true, data: payload.data};\n\n logger().warn(\n {deliveryId, issues: payload.error},\n 'sentry webhook: payload failed validation, dropping',\n );\n return {success: false, reason: 'unsupported_event'};\n}\n\nasync function recordDeliveryOnly(\n context: CreateSentryWebhookProcessorOptions,\n deliveryId: string,\n): Promise<void> {\n await context.coreDb().transaction(async (tx) => {\n await context.recordDeliveryOnly({tx, provider: SENTRY_PROVIDER, deliveryId});\n });\n}\n"],"names":["Buffer","decodeWebhookBody","sentryInstallationWebhookSchema","sentryIssueWebhookSchema","logger","config","SentryIssueDroppedError","handleSentryInstallationCreated","handleSentryInstallationDeleted","handleSentryIssueEvent","normalizeSentryIssueAction","completeSentryInstallationVerification","getSentryInstallationByInstallationUuid","verifySentrySignature","DELIVERY_ID_HEADER","RESOURCE_HEADER","SIGNATURE_HEADER","LEGACY_SIGNATURE_HEADER","ISSUE_RESOURCE","INSTALLATION_RESOURCE","SENTRY_PROVIDER","createSentryWebhookProcessor","context","process","request","processSentryWebhookRequest","route_id","Error","deliveryId","headers","resource","signature","outcome","reason","rawBody","from","body","secret","SENTRY_APP_CLIENT_SECRET","debug","signatureHeader","processIssueResource","processInstallationResource","recordDeliveryOnly","parsed","parsePayload","success","payload","data","coreDb","transaction","tx","publishIntegrationEventReceived","getIntegrationConnectionById","error","warn","err","message","installation","action","installationUuid","uuid","updateConnectionLifecycleStatus","orgSlug","organization","slug","code","sentry","verifyInstall","SENTRY_APP_VERIFY_INSTALL","getSentryInstallation","persistUnclaimedAndRecordDelivery","codeHash","id","completed","provider","current","recordDelivery","schema","normalize","parsedJson","JSON","parse","toString","safeParse","issues"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/core/webhook-processor.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {\n sentryInstallationWebhookSchema,\n sentryIssueWebhookSchema,\n} from '@shipfox/api-integration-sentry-dto';\nimport {\n decodeWebhookBody,\n type GetIntegrationConnectionByIdFn,\n type PublishIntegrationEventReceivedFn,\n type RecordDeliveryOnlyFn,\n type StoredWebhookRequest,\n type UpdateIntegrationConnectionLifecycleStatusFn,\n type WebhookProcessingResult,\n} from '@shipfox/api-integration-spi';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport type {NodePgDatabase} from 'drizzle-orm/node-postgres';\nimport type {SentryApiClient} from '#api/client.js';\nimport {config} from '#config.js';\nimport {SentryIssueDroppedError} from '#core/errors.js';\nimport {\n handleSentryInstallationCreated,\n handleSentryInstallationDeleted,\n handleSentryIssueEvent,\n normalizeSentryIssueAction,\n} from '#core/webhook.js';\nimport {\n completeSentryInstallationVerification,\n getSentryInstallationByInstallationUuid,\n} from '#db/installations.js';\nimport {verifySentrySignature} from './signature.js';\n\nconst DELIVERY_ID_HEADER = 'request-id';\nconst RESOURCE_HEADER = 'sentry-hook-resource';\nconst SIGNATURE_HEADER = 'sentry-hook-signature';\nconst LEGACY_SIGNATURE_HEADER = 'sentry-app-signature';\nconst ISSUE_RESOURCE = 'issue';\nconst INSTALLATION_RESOURCE = 'installation';\nconst SENTRY_PROVIDER = 'sentry';\n\nexport interface SentryWebhookProcessor {\n process(request: StoredWebhookRequest): Promise<WebhookProcessingResult>;\n}\n\nexport interface CreateSentryWebhookProcessorOptions {\n sentry: SentryApiClient;\n coreDb: () => NodePgDatabase<Record<string, unknown>>;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n updateConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;\n}\n\nexport function createSentryWebhookProcessor(\n context: CreateSentryWebhookProcessorOptions,\n): SentryWebhookProcessor {\n return {process: (request) => processSentryWebhookRequest(context, request)};\n}\n\nasync function processSentryWebhookRequest(\n context: CreateSentryWebhookProcessorOptions,\n request: StoredWebhookRequest,\n): Promise<WebhookProcessingResult> {\n if (request.route_id !== 'sentry') {\n throw new Error(`Sentry processor cannot process ${request.route_id} requests`);\n }\n\n const deliveryId = request.headers[DELIVERY_ID_HEADER];\n const resource = request.headers[RESOURCE_HEADER];\n const signature = request.headers[SIGNATURE_HEADER] ?? request.headers[LEGACY_SIGNATURE_HEADER];\n if (!deliveryId || !resource || !signature) {\n return {outcome: 'discarded', reason: 'missing_required_input'};\n }\n\n const rawBody = Buffer.from(decodeWebhookBody(request.body));\n if (!verifySentrySignature({rawBody, signature, secret: config.SENTRY_APP_CLIENT_SECRET})) {\n return {outcome: 'discarded', reason: 'invalid_signature', deliveryId};\n }\n\n logger().debug(\n {\n deliveryId,\n signatureHeader: request.headers[SIGNATURE_HEADER]\n ? SIGNATURE_HEADER\n : LEGACY_SIGNATURE_HEADER,\n },\n 'sentry webhook: signature verified',\n );\n\n if (resource === ISSUE_RESOURCE) return processIssueResource(context, deliveryId, rawBody);\n if (resource === INSTALLATION_RESOURCE)\n return processInstallationResource(context, deliveryId, rawBody);\n\n await recordDeliveryOnly(context, deliveryId);\n return {outcome: 'discarded', reason: 'unsupported_event', deliveryId};\n}\n\nasync function processIssueResource(\n context: CreateSentryWebhookProcessorOptions,\n deliveryId: string,\n rawBody: Uint8Array,\n): Promise<WebhookProcessingResult> {\n const parsed = parsePayload(\n sentryIssueWebhookSchema,\n rawBody,\n deliveryId,\n normalizeSentryIssueAction,\n );\n if (!parsed.success) {\n await recordDeliveryOnly(context, deliveryId);\n return {outcome: 'discarded', reason: parsed.reason, deliveryId};\n }\n const payload = parsed.data;\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)) throw error;\n\n logger().warn({deliveryId, err: error}, `sentry webhook: ${error.message}, dropping`);\n await recordDeliveryOnly(context, deliveryId);\n return {outcome: 'discarded', reason: 'connection_unavailable', deliveryId};\n }\n\n return {outcome: 'processed', deliveryId};\n}\n\nasync function processInstallationResource(\n context: CreateSentryWebhookProcessorOptions,\n deliveryId: string,\n rawBody: Uint8Array,\n): Promise<WebhookProcessingResult> {\n const parsed = parsePayload(sentryInstallationWebhookSchema, rawBody, deliveryId);\n if (!parsed.success) {\n await recordDeliveryOnly(context, deliveryId);\n return {outcome: 'discarded', reason: parsed.reason, deliveryId};\n }\n const payload = parsed.data;\n\n const installation = payload.data.installation;\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 return {outcome: 'processed', deliveryId};\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, codeHash, deliveryId: id}) =>\n context.coreDb().transaction(async (tx) => {\n const completed = await completeSentryInstallationVerification(\n {installationUuid, codeHash},\n {tx},\n );\n await context.recordDeliveryOnly({tx, provider: SENTRY_PROVIDER, deliveryId: id});\n if (completed) return completed;\n\n const current = await getSentryInstallationByInstallationUuid(installationUuid, {tx});\n if (!current) throw new Error('Sentry installation verification lost its claim');\n return current;\n }),\n recordDelivery: (id) => recordDeliveryOnly(context, id),\n });\n\n return {outcome: 'processed', deliveryId};\n}\n\ntype ParsePayloadResult<T> =\n | {success: true; data: T}\n | {success: false; reason: 'malformed_payload' | 'unsupported_event'};\n\nfunction parsePayload<T>(\n schema: {safeParse(value: unknown): {success: true; data: T} | {success: false; error: unknown}},\n rawBody: Uint8Array,\n deliveryId: string,\n normalize: (payload: unknown) => unknown = (payload) => payload,\n): ParsePayloadResult<T> {\n let parsedJson: unknown;\n try {\n parsedJson = JSON.parse(Buffer.from(rawBody).toString('utf8'));\n } catch (error) {\n logger().warn({deliveryId, err: error}, 'sentry webhook: payload JSON parse failed, dropping');\n return {success: false, reason: 'malformed_payload'};\n }\n\n const payload = schema.safeParse(normalize(parsedJson));\n if (payload.success) return {success: true, data: payload.data};\n\n logger().warn(\n {deliveryId, issues: payload.error},\n 'sentry webhook: payload failed validation, dropping',\n );\n return {success: false, reason: 'unsupported_event'};\n}\n\nasync function recordDeliveryOnly(\n context: CreateSentryWebhookProcessorOptions,\n deliveryId: string,\n): Promise<void> {\n await context.coreDb().transaction(async (tx) => {\n await context.recordDeliveryOnly({tx, provider: SENTRY_PROVIDER, deliveryId});\n });\n}\n"],"names":["Buffer","sentryInstallationWebhookSchema","sentryIssueWebhookSchema","decodeWebhookBody","logger","config","SentryIssueDroppedError","handleSentryInstallationCreated","handleSentryInstallationDeleted","handleSentryIssueEvent","normalizeSentryIssueAction","completeSentryInstallationVerification","getSentryInstallationByInstallationUuid","verifySentrySignature","DELIVERY_ID_HEADER","RESOURCE_HEADER","SIGNATURE_HEADER","LEGACY_SIGNATURE_HEADER","ISSUE_RESOURCE","INSTALLATION_RESOURCE","SENTRY_PROVIDER","createSentryWebhookProcessor","context","process","request","processSentryWebhookRequest","route_id","Error","deliveryId","headers","resource","signature","outcome","reason","rawBody","from","body","secret","SENTRY_APP_CLIENT_SECRET","debug","signatureHeader","processIssueResource","processInstallationResource","recordDeliveryOnly","parsed","parsePayload","success","payload","data","coreDb","transaction","tx","publishIntegrationEventReceived","getIntegrationConnectionById","error","warn","err","message","installation","action","installationUuid","uuid","updateConnectionLifecycleStatus","orgSlug","organization","slug","code","sentry","verifyInstall","SENTRY_APP_VERIFY_INSTALL","getSentryInstallation","persistUnclaimedAndRecordDelivery","codeHash","id","completed","provider","current","recordDelivery","schema","normalize","parsedJson","JSON","parse","toString","safeParse","issues"],"mappings":"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"}
|
package/dist/core/webhook.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { GetIntegrationConnectionByIdFn, IntegrationTx, PublishIntegrationEventReceivedFn, RecordDeliveryOnlyFn, UpdateIntegrationConnectionLifecycleStatusFn } from '@shipfox/api-integration-core-dto';
|
|
2
1
|
import type { SentryIssueWebhookDto } from '@shipfox/api-integration-sentry-dto';
|
|
2
|
+
import type { GetIntegrationConnectionByIdFn, IntegrationTx, PublishIntegrationEventReceivedFn, RecordDeliveryOnlyFn, UpdateIntegrationConnectionLifecycleStatusFn } from '@shipfox/api-integration-spi';
|
|
3
3
|
import type { SentryApiClient } from '#api/client.js';
|
|
4
4
|
import { type SentryInstallation } from '#db/installations.js';
|
|
5
5
|
export interface HandleSentryIssueEventParams {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/core/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,8BAA8B,EAC9B,aAAa,EACb,iCAAiC,EACjC,oBAAoB,EAEpB,4CAA4C,EAC7C,MAAM,
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/core/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,qCAAqC,CAAC;AAC/E,OAAO,KAAK,EACV,8BAA8B,EAC9B,aAAa,EACb,iCAAiC,EACjC,oBAAoB,EAEpB,4CAA4C,EAC7C,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AASpD,OAAO,EAKL,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAM9B,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,aAAa,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,qBAAqB,CAAC;IAC/B,+BAA+B,EAAE,iCAAiC,CAAC;IACnE,4BAA4B,EAAE,8BAA8B,CAAC;CAC9D;AAOD,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,4BAA4B,GAAG,OAAO,CAAC,IAAI,CAAC,CAuChG;AAED,MAAM,WAAW,qCAAqC;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IAGzB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,MAAM,EAAE,eAAe,CAAC;IACxB,aAAa,EAAE,OAAO,CAAC;IACvB,qBAAqB,EAAE,CAAC,KAAK,EAAE;QAC7B,gBAAgB,EAAE,MAAM,CAAC;KAC1B,KAAK,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC;IAG9C,iCAAiC,EAAE,CAAC,KAAK,EAAE;QACzC,gBAAgB,EAAE,MAAM,CAAC;QACzB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;KACpB,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAGlC,cAAc,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvD;AAED;;;;;;;GAOG;AACH,wBAAsB,+BAA+B,CACnD,MAAM,EAAE,qCAAqC,GAC5C,OAAO,CAAC,IAAI,CAAC,CAoFf;AAED,MAAM,WAAW,qCAAqC;IACpD,EAAE,EAAE,aAAa,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,oBAAoB,CAAC;IACzC,+BAA+B,EAAE,4CAA4C,CAAC;CAC/E;AAKD,wBAAsB,+BAA+B,CACnD,MAAM,EAAE,qCAAqC,GAC5C,OAAO,CAAC,IAAI,CAAC,CAiBf;AAID,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,OAAO,GAAG,OAAO,CAOvE"}
|
package/dist/core/webhook.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/webhook.ts"],"sourcesContent":["import type {\n GetIntegrationConnectionByIdFn,\n IntegrationTx,\n PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n SentryIssuePayload,\n UpdateIntegrationConnectionLifecycleStatusFn,\n} from '@shipfox/api-integration-core-dto';\nimport type {SentryIssueWebhookDto} from '@shipfox/api-integration-sentry-dto';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport type {SentryApiClient} from '#api/client.js';\nimport {\n SentryConnectionNotFoundError,\n SentryInstallationDeletedError,\n SentryInstallationNotFoundError,\n SentryInstallationUnclaimedError,\n SentryIntegrationProviderError,\n} from '#core/errors.js';\nimport {hashAuthorizationCode, verifySentryInstallationBestEffort} from '#core/install.js';\nimport {\n claimSentryInstallationVerification,\n getSentryInstallationByInstallationUuid,\n markSentryInstallationDeleted,\n markSentryInstallationExchangeSucceeded,\n type SentryInstallation,\n} from '#db/installations.js';\n\nconst SENTRY_SOURCE = 'sentry';\nconst DEFAULT_ISSUE_TITLE = 'Sentry issue';\nconst DELETED_STATUS = 'deleted';\n\nexport interface HandleSentryIssueEventParams {\n tx: IntegrationTx;\n deliveryId: string;\n payload: SentryIssueWebhookDto;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n}\n\n// Publishes the mapped event for a verified issue delivery. Throws a typed\n// SentryIssueDroppedError subclass when the delivery references state we cannot\n// publish against (unknown/deleted installation, or an install not yet claimed\n// into a workspace); the webhook layer records-and-drops those. Dedup of an\n// already-seen delivery is handled inside publishIntegrationEventReceived.\nexport async function handleSentryIssueEvent(params: HandleSentryIssueEventParams): Promise<void> {\n const installationUuid = params.payload.installation.uuid;\n\n const installation = await getSentryInstallationByInstallationUuid(installationUuid, {\n tx: params.tx,\n });\n if (!installation) {\n throw new SentryInstallationNotFoundError(installationUuid);\n }\n if (installation.status === DELETED_STATUS) {\n throw new SentryInstallationDeletedError(installationUuid);\n }\n // A verified-but-unclaimed install has no workspace yet, so there is nothing to\n // publish an event against. This is the pre-claim drop window (counted/logged).\n if (!installation.connectionId) {\n throw new SentryInstallationUnclaimedError(installationUuid);\n }\n\n const connection = await params.getIntegrationConnectionById(installation.connectionId, {\n tx: params.tx,\n });\n if (!connection) {\n throw new SentryConnectionNotFoundError(installation.connectionId);\n }\n\n await params.publishIntegrationEventReceived({\n tx: params.tx,\n event: {\n provider: SENTRY_SOURCE,\n source: connection.slug,\n event: `issue.${params.payload.action}`,\n workspaceId: connection.workspaceId,\n connectionId: connection.id,\n connectionName: connection.displayName,\n deliveryId: params.deliveryId,\n receivedAt: new Date().toISOString(),\n payload: normalizeIssuePayload(params.payload),\n },\n });\n}\n\nexport interface HandleSentryInstallationCreatedParams {\n deliveryId: string;\n installationUuid: string;\n // From the signed payload; the webhook is authoritative so the slug is trusted\n // without a getInstallation round-trip. The code is the single-use grant.\n orgSlug: string | undefined;\n code: string | undefined;\n sentry: SentryApiClient;\n verifyInstall: boolean;\n getSentryInstallation: (input: {\n installationUuid: string;\n }) => Promise<SentryInstallation | undefined>;\n // Persists the verified-unclaimed row and records the delivery in one short\n // transaction, returning the persisted row.\n persistUnclaimedAndRecordDelivery: (input: {\n installationUuid: string;\n orgSlug: string;\n codeHash: string;\n deliveryId: string;\n }) => Promise<SentryInstallation>;\n // Records the delivery for dedup without persisting an install (reconcile/no-op\n // or missing claim input).\n recordDelivery: (deliveryId: string) => Promise<void>;\n}\n\n/**\n * The authoritative `installation.created` path. Whichever of webhook or browser\n * arrives first exchanges the single-use code and persists a verified-unclaimed\n * row; the other reconciles. The webhook writes a pending claim before the\n * exchange and a durable checkpoint after it succeeds. Only that checkpoint can\n * bypass the single-use exchange on retry. The installed transition and delivery\n * record commit in one transaction. Never logs the raw code.\n */\nexport async function handleSentryInstallationCreated(\n params: HandleSentryInstallationCreatedParams,\n): Promise<void> {\n const existing = await params.getSentryInstallation({installationUuid: params.installationUuid});\n if (existing && (existing.status === 'installed' || existing.status === 'deleted')) {\n logger().debug(\n {deliveryId: params.deliveryId, installationUuid: params.installationUuid},\n 'sentry webhook: installation.created for an existing row, reconciling',\n );\n await params.recordDelivery(params.deliveryId);\n return;\n }\n\n if (!params.code || !params.orgSlug) {\n logger().warn(\n {deliveryId: params.deliveryId, installationUuid: params.installationUuid},\n 'sentry webhook: installation.created without claim input, dropping',\n );\n await params.recordDelivery(params.deliveryId);\n return;\n }\n\n const codeHash = hashAuthorizationCode(params.code);\n let claimed =\n existing ??\n (await claimSentryInstallationVerification({\n installationUuid: params.installationUuid,\n orgSlug: params.orgSlug,\n codeHash,\n }));\n if (claimed.status === 'installed' || claimed.status === 'deleted') {\n await params.recordDelivery(params.deliveryId);\n return;\n }\n if (claimed.codeHash !== codeHash) {\n logger().warn(\n {deliveryId: params.deliveryId, installationUuid: params.installationUuid},\n 'sentry webhook: installation.created does not match the pending claim, dropping',\n );\n await params.recordDelivery(params.deliveryId);\n return;\n }\n\n let authorization: Awaited<ReturnType<SentryApiClient['exchangeAuthorizationCode']>> | undefined;\n if (claimed.status === 'pending') {\n try {\n authorization = await params.sentry.exchangeAuthorizationCode({\n installationUuid: params.installationUuid,\n code: params.code,\n });\n } catch (error) {\n if (!(error instanceof SentryIntegrationProviderError) || error.reason !== 'access-denied') {\n throw error;\n }\n\n const current = await params.getSentryInstallation({\n installationUuid: params.installationUuid,\n });\n const concurrentAttemptSucceeded =\n current?.codeHash === codeHash &&\n (current.status === 'exchange-succeeded' || current.status === 'installed');\n if (!current || !concurrentAttemptSucceeded) throw error;\n claimed = current;\n }\n\n if (authorization) {\n await markSentryInstallationExchangeSucceeded({\n installationUuid: params.installationUuid,\n codeHash,\n });\n }\n }\n\n const completed = await params.persistUnclaimedAndRecordDelivery({\n installationUuid: params.installationUuid,\n orgSlug: claimed.orgSlug,\n codeHash,\n deliveryId: params.deliveryId,\n });\n if (authorization && completed.status === 'installed' && params.verifyInstall) {\n await verifySentryInstallationBestEffort({\n sentry: params.sentry,\n installationUuid: params.installationUuid,\n token: authorization.token,\n });\n }\n}\n\nexport interface HandleSentryInstallationDeletedParams {\n tx: IntegrationTx;\n deliveryId: string;\n installationUuid: string;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n updateConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;\n}\n\n// Tombstones the install and disables its connection if one exists. An unknown\n// uuid also gets a tombstone, so a reordered creation cannot restore it. The\n// state change and delivery record share the caller's transaction.\nexport async function handleSentryInstallationDeleted(\n params: HandleSentryInstallationDeletedParams,\n): Promise<void> {\n const installation = await markSentryInstallationDeleted(\n {installationUuid: params.installationUuid},\n {tx: params.tx},\n );\n if (installation?.connectionId) {\n await params.updateConnectionLifecycleStatus(\n {id: installation.connectionId, lifecycleStatus: 'disabled'},\n {tx: params.tx},\n );\n }\n\n await params.recordDeliveryOnly({\n tx: params.tx,\n provider: SENTRY_SOURCE,\n deliveryId: params.deliveryId,\n });\n}\n\n// A raw Sentry `ignored` action is normalized to `archived` before validation,\n// so legacy ignore events still fire `issue.archived` workflows.\nexport function normalizeSentryIssueAction(parsedJson: unknown): unknown {\n if (typeof parsedJson !== 'object' || parsedJson === null) return parsedJson;\n const obj = parsedJson as {action?: unknown};\n if (obj.action === 'ignored') {\n return {...obj, action: 'archived'};\n }\n return parsedJson;\n}\n\nfunction normalizeIssuePayload(payload: SentryIssueWebhookDto): SentryIssuePayload {\n const issue = payload.data.issue;\n return {\n action: payload.action,\n issueId: issue.id,\n shortId: issue.shortId ?? null,\n title: issue.title ?? DEFAULT_ISSUE_TITLE,\n culprit: issue.culprit ?? null,\n level: issue.level ?? null,\n status: issue.status ?? null,\n platform: issue.platform ?? null,\n webUrl: issue.web_url ?? null,\n issueUrl: issue.url ?? null,\n projectUrl: issue.project_url ?? null,\n firstSeenAt: issue.firstSeen ?? null,\n lastSeenAt: issue.lastSeen ?? null,\n };\n}\n"],"names":["logger","SentryConnectionNotFoundError","SentryInstallationDeletedError","SentryInstallationNotFoundError","SentryInstallationUnclaimedError","SentryIntegrationProviderError","hashAuthorizationCode","verifySentryInstallationBestEffort","claimSentryInstallationVerification","getSentryInstallationByInstallationUuid","markSentryInstallationDeleted","markSentryInstallationExchangeSucceeded","SENTRY_SOURCE","DEFAULT_ISSUE_TITLE","DELETED_STATUS","handleSentryIssueEvent","params","installationUuid","payload","installation","uuid","tx","status","connectionId","connection","getIntegrationConnectionById","publishIntegrationEventReceived","event","provider","source","slug","action","workspaceId","id","connectionName","displayName","deliveryId","receivedAt","Date","toISOString","normalizeIssuePayload","handleSentryInstallationCreated","existing","getSentryInstallation","debug","recordDelivery","code","orgSlug","warn","codeHash","claimed","authorization","sentry","exchangeAuthorizationCode","error","reason","current","concurrentAttemptSucceeded","completed","persistUnclaimedAndRecordDelivery","verifyInstall","token","handleSentryInstallationDeleted","updateConnectionLifecycleStatus","lifecycleStatus","recordDeliveryOnly","normalizeSentryIssueAction","parsedJson","obj","issue","data","issueId","shortId","title","culprit","level","platform","webUrl","web_url","issueUrl","url","projectUrl","project_url","firstSeenAt","firstSeen","lastSeenAt","lastSeen"],"mappings":"AASA,SAAQA,MAAM,QAAO,8BAA8B;AAEnD,SACEC,6BAA6B,EAC7BC,8BAA8B,EAC9BC,+BAA+B,EAC/BC,gCAAgC,EAChCC,8BAA8B,QACzB,kBAAkB;AACzB,SAAQC,qBAAqB,EAAEC,kCAAkC,QAAO,mBAAmB;AAC3F,SACEC,mCAAmC,EACnCC,uCAAuC,EACvCC,6BAA6B,EAC7BC,uCAAuC,QAElC,uBAAuB;AAE9B,MAAMC,gBAAgB;AACtB,MAAMC,sBAAsB;AAC5B,MAAMC,iBAAiB;AAUvB,2EAA2E;AAC3E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,2EAA2E;AAC3E,OAAO,eAAeC,uBAAuBC,MAAoC;IAC/E,MAAMC,mBAAmBD,OAAOE,OAAO,CAACC,YAAY,CAACC,IAAI;IAEzD,MAAMD,eAAe,MAAMV,wCAAwCQ,kBAAkB;QACnFI,IAAIL,OAAOK,EAAE;IACf;IACA,IAAI,CAACF,cAAc;QACjB,MAAM,IAAIhB,gCAAgCc;IAC5C;IACA,IAAIE,aAAaG,MAAM,KAAKR,gBAAgB;QAC1C,MAAM,IAAIZ,+BAA+Be;IAC3C;IACA,gFAAgF;IAChF,gFAAgF;IAChF,IAAI,CAACE,aAAaI,YAAY,EAAE;QAC9B,MAAM,IAAInB,iCAAiCa;IAC7C;IAEA,MAAMO,aAAa,MAAMR,OAAOS,4BAA4B,CAACN,aAAaI,YAAY,EAAE;QACtFF,IAAIL,OAAOK,EAAE;IACf;IACA,IAAI,CAACG,YAAY;QACf,MAAM,IAAIvB,8BAA8BkB,aAAaI,YAAY;IACnE;IAEA,MAAMP,OAAOU,+BAA+B,CAAC;QAC3CL,IAAIL,OAAOK,EAAE;QACbM,OAAO;YACLC,UAAUhB;YACViB,QAAQL,WAAWM,IAAI;YACvBH,OAAO,CAAC,MAAM,EAAEX,OAAOE,OAAO,CAACa,MAAM,EAAE;YACvCC,aAAaR,WAAWQ,WAAW;YACnCT,cAAcC,WAAWS,EAAE;YAC3BC,gBAAgBV,WAAWW,WAAW;YACtCC,YAAYpB,OAAOoB,UAAU;YAC7BC,YAAY,IAAIC,OAAOC,WAAW;YAClCrB,SAASsB,sBAAsBxB,OAAOE,OAAO;QAC/C;IACF;AACF;AA2BA;;;;;;;CAOC,GACD,OAAO,eAAeuB,gCACpBzB,MAA6C;IAE7C,MAAM0B,WAAW,MAAM1B,OAAO2B,qBAAqB,CAAC;QAAC1B,kBAAkBD,OAAOC,gBAAgB;IAAA;IAC9F,IAAIyB,YAAaA,CAAAA,SAASpB,MAAM,KAAK,eAAeoB,SAASpB,MAAM,KAAK,SAAQ,GAAI;QAClFtB,SAAS4C,KAAK,CACZ;YAACR,YAAYpB,OAAOoB,UAAU;YAAEnB,kBAAkBD,OAAOC,gBAAgB;QAAA,GACzE;QAEF,MAAMD,OAAO6B,cAAc,CAAC7B,OAAOoB,UAAU;QAC7C;IACF;IAEA,IAAI,CAACpB,OAAO8B,IAAI,IAAI,CAAC9B,OAAO+B,OAAO,EAAE;QACnC/C,SAASgD,IAAI,CACX;YAACZ,YAAYpB,OAAOoB,UAAU;YAAEnB,kBAAkBD,OAAOC,gBAAgB;QAAA,GACzE;QAEF,MAAMD,OAAO6B,cAAc,CAAC7B,OAAOoB,UAAU;QAC7C;IACF;IAEA,MAAMa,WAAW3C,sBAAsBU,OAAO8B,IAAI;IAClD,IAAII,UACFR,YACC,MAAMlC,oCAAoC;QACzCS,kBAAkBD,OAAOC,gBAAgB;QACzC8B,SAAS/B,OAAO+B,OAAO;QACvBE;IACF;IACF,IAAIC,QAAQ5B,MAAM,KAAK,eAAe4B,QAAQ5B,MAAM,KAAK,WAAW;QAClE,MAAMN,OAAO6B,cAAc,CAAC7B,OAAOoB,UAAU;QAC7C;IACF;IACA,IAAIc,QAAQD,QAAQ,KAAKA,UAAU;QACjCjD,SAASgD,IAAI,CACX;YAACZ,YAAYpB,OAAOoB,UAAU;YAAEnB,kBAAkBD,OAAOC,gBAAgB;QAAA,GACzE;QAEF,MAAMD,OAAO6B,cAAc,CAAC7B,OAAOoB,UAAU;QAC7C;IACF;IAEA,IAAIe;IACJ,IAAID,QAAQ5B,MAAM,KAAK,WAAW;QAChC,IAAI;YACF6B,gBAAgB,MAAMnC,OAAOoC,MAAM,CAACC,yBAAyB,CAAC;gBAC5DpC,kBAAkBD,OAAOC,gBAAgB;gBACzC6B,MAAM9B,OAAO8B,IAAI;YACnB;QACF,EAAE,OAAOQ,OAAO;YACd,IAAI,CAAEA,CAAAA,iBAAiBjD,8BAA6B,KAAMiD,MAAMC,MAAM,KAAK,iBAAiB;gBAC1F,MAAMD;YACR;YAEA,MAAME,UAAU,MAAMxC,OAAO2B,qBAAqB,CAAC;gBACjD1B,kBAAkBD,OAAOC,gBAAgB;YAC3C;YACA,MAAMwC,6BACJD,SAASP,aAAaA,YACrBO,CAAAA,QAAQlC,MAAM,KAAK,wBAAwBkC,QAAQlC,MAAM,KAAK,WAAU;YAC3E,IAAI,CAACkC,WAAW,CAACC,4BAA4B,MAAMH;YACnDJ,UAAUM;QACZ;QAEA,IAAIL,eAAe;YACjB,MAAMxC,wCAAwC;gBAC5CM,kBAAkBD,OAAOC,gBAAgB;gBACzCgC;YACF;QACF;IACF;IAEA,MAAMS,YAAY,MAAM1C,OAAO2C,iCAAiC,CAAC;QAC/D1C,kBAAkBD,OAAOC,gBAAgB;QACzC8B,SAASG,QAAQH,OAAO;QACxBE;QACAb,YAAYpB,OAAOoB,UAAU;IAC/B;IACA,IAAIe,iBAAiBO,UAAUpC,MAAM,KAAK,eAAeN,OAAO4C,aAAa,EAAE;QAC7E,MAAMrD,mCAAmC;YACvC6C,QAAQpC,OAAOoC,MAAM;YACrBnC,kBAAkBD,OAAOC,gBAAgB;YACzC4C,OAAOV,cAAcU,KAAK;QAC5B;IACF;AACF;AAUA,+EAA+E;AAC/E,6EAA6E;AAC7E,mEAAmE;AACnE,OAAO,eAAeC,gCACpB9C,MAA6C;IAE7C,MAAMG,eAAe,MAAMT,8BACzB;QAACO,kBAAkBD,OAAOC,gBAAgB;IAAA,GAC1C;QAACI,IAAIL,OAAOK,EAAE;IAAA;IAEhB,IAAIF,cAAcI,cAAc;QAC9B,MAAMP,OAAO+C,+BAA+B,CAC1C;YAAC9B,IAAId,aAAaI,YAAY;YAAEyC,iBAAiB;QAAU,GAC3D;YAAC3C,IAAIL,OAAOK,EAAE;QAAA;IAElB;IAEA,MAAML,OAAOiD,kBAAkB,CAAC;QAC9B5C,IAAIL,OAAOK,EAAE;QACbO,UAAUhB;QACVwB,YAAYpB,OAAOoB,UAAU;IAC/B;AACF;AAEA,+EAA+E;AAC/E,iEAAiE;AACjE,OAAO,SAAS8B,2BAA2BC,UAAmB;IAC5D,IAAI,OAAOA,eAAe,YAAYA,eAAe,MAAM,OAAOA;IAClE,MAAMC,MAAMD;IACZ,IAAIC,IAAIrC,MAAM,KAAK,WAAW;QAC5B,OAAO;YAAC,GAAGqC,GAAG;YAAErC,QAAQ;QAAU;IACpC;IACA,OAAOoC;AACT;AAEA,SAAS3B,sBAAsBtB,OAA8B;IAC3D,MAAMmD,QAAQnD,QAAQoD,IAAI,CAACD,KAAK;IAChC,OAAO;QACLtC,QAAQb,QAAQa,MAAM;QACtBwC,SAASF,MAAMpC,EAAE;QACjBuC,SAASH,MAAMG,OAAO,IAAI;QAC1BC,OAAOJ,MAAMI,KAAK,IAAI5D;QACtB6D,SAASL,MAAMK,OAAO,IAAI;QAC1BC,OAAON,MAAMM,KAAK,IAAI;QACtBrD,QAAQ+C,MAAM/C,MAAM,IAAI;QACxBsD,UAAUP,MAAMO,QAAQ,IAAI;QAC5BC,QAAQR,MAAMS,OAAO,IAAI;QACzBC,UAAUV,MAAMW,GAAG,IAAI;QACvBC,YAAYZ,MAAMa,WAAW,IAAI;QACjCC,aAAad,MAAMe,SAAS,IAAI;QAChCC,YAAYhB,MAAMiB,QAAQ,IAAI;IAChC;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../src/core/webhook.ts"],"sourcesContent":["import type {SentryIssueWebhookDto} from '@shipfox/api-integration-sentry-dto';\nimport type {\n GetIntegrationConnectionByIdFn,\n IntegrationTx,\n PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n SentryIssuePayload,\n UpdateIntegrationConnectionLifecycleStatusFn,\n} from '@shipfox/api-integration-spi';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport type {SentryApiClient} from '#api/client.js';\nimport {\n SentryConnectionNotFoundError,\n SentryInstallationDeletedError,\n SentryInstallationNotFoundError,\n SentryInstallationUnclaimedError,\n SentryIntegrationProviderError,\n} from '#core/errors.js';\nimport {hashAuthorizationCode, verifySentryInstallationBestEffort} from '#core/install.js';\nimport {\n claimSentryInstallationVerification,\n getSentryInstallationByInstallationUuid,\n markSentryInstallationDeleted,\n markSentryInstallationExchangeSucceeded,\n type SentryInstallation,\n} from '#db/installations.js';\n\nconst SENTRY_SOURCE = 'sentry';\nconst DEFAULT_ISSUE_TITLE = 'Sentry issue';\nconst DELETED_STATUS = 'deleted';\n\nexport interface HandleSentryIssueEventParams {\n tx: IntegrationTx;\n deliveryId: string;\n payload: SentryIssueWebhookDto;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n}\n\n// Publishes the mapped event for a verified issue delivery. Throws a typed\n// SentryIssueDroppedError subclass when the delivery references state we cannot\n// publish against (unknown/deleted installation, or an install not yet claimed\n// into a workspace); the webhook layer records-and-drops those. Dedup of an\n// already-seen delivery is handled inside publishIntegrationEventReceived.\nexport async function handleSentryIssueEvent(params: HandleSentryIssueEventParams): Promise<void> {\n const installationUuid = params.payload.installation.uuid;\n\n const installation = await getSentryInstallationByInstallationUuid(installationUuid, {\n tx: params.tx,\n });\n if (!installation) {\n throw new SentryInstallationNotFoundError(installationUuid);\n }\n if (installation.status === DELETED_STATUS) {\n throw new SentryInstallationDeletedError(installationUuid);\n }\n // A verified-but-unclaimed install has no workspace yet, so there is nothing to\n // publish an event against. This is the pre-claim drop window (counted/logged).\n if (!installation.connectionId) {\n throw new SentryInstallationUnclaimedError(installationUuid);\n }\n\n const connection = await params.getIntegrationConnectionById(installation.connectionId, {\n tx: params.tx,\n });\n if (!connection) {\n throw new SentryConnectionNotFoundError(installation.connectionId);\n }\n\n await params.publishIntegrationEventReceived({\n tx: params.tx,\n event: {\n provider: SENTRY_SOURCE,\n source: connection.slug,\n event: `issue.${params.payload.action}`,\n workspaceId: connection.workspaceId,\n connectionId: connection.id,\n connectionName: connection.displayName,\n deliveryId: params.deliveryId,\n receivedAt: new Date().toISOString(),\n payload: normalizeIssuePayload(params.payload),\n },\n });\n}\n\nexport interface HandleSentryInstallationCreatedParams {\n deliveryId: string;\n installationUuid: string;\n // From the signed payload; the webhook is authoritative so the slug is trusted\n // without a getInstallation round-trip. The code is the single-use grant.\n orgSlug: string | undefined;\n code: string | undefined;\n sentry: SentryApiClient;\n verifyInstall: boolean;\n getSentryInstallation: (input: {\n installationUuid: string;\n }) => Promise<SentryInstallation | undefined>;\n // Persists the verified-unclaimed row and records the delivery in one short\n // transaction, returning the persisted row.\n persistUnclaimedAndRecordDelivery: (input: {\n installationUuid: string;\n orgSlug: string;\n codeHash: string;\n deliveryId: string;\n }) => Promise<SentryInstallation>;\n // Records the delivery for dedup without persisting an install (reconcile/no-op\n // or missing claim input).\n recordDelivery: (deliveryId: string) => Promise<void>;\n}\n\n/**\n * The authoritative `installation.created` path. Whichever of webhook or browser\n * arrives first exchanges the single-use code and persists a verified-unclaimed\n * row; the other reconciles. The webhook writes a pending claim before the\n * exchange and a durable checkpoint after it succeeds. Only that checkpoint can\n * bypass the single-use exchange on retry. The installed transition and delivery\n * record commit in one transaction. Never logs the raw code.\n */\nexport async function handleSentryInstallationCreated(\n params: HandleSentryInstallationCreatedParams,\n): Promise<void> {\n const existing = await params.getSentryInstallation({installationUuid: params.installationUuid});\n if (existing && (existing.status === 'installed' || existing.status === 'deleted')) {\n logger().debug(\n {deliveryId: params.deliveryId, installationUuid: params.installationUuid},\n 'sentry webhook: installation.created for an existing row, reconciling',\n );\n await params.recordDelivery(params.deliveryId);\n return;\n }\n\n if (!params.code || !params.orgSlug) {\n logger().warn(\n {deliveryId: params.deliveryId, installationUuid: params.installationUuid},\n 'sentry webhook: installation.created without claim input, dropping',\n );\n await params.recordDelivery(params.deliveryId);\n return;\n }\n\n const codeHash = hashAuthorizationCode(params.code);\n let claimed =\n existing ??\n (await claimSentryInstallationVerification({\n installationUuid: params.installationUuid,\n orgSlug: params.orgSlug,\n codeHash,\n }));\n if (claimed.status === 'installed' || claimed.status === 'deleted') {\n await params.recordDelivery(params.deliveryId);\n return;\n }\n if (claimed.codeHash !== codeHash) {\n logger().warn(\n {deliveryId: params.deliveryId, installationUuid: params.installationUuid},\n 'sentry webhook: installation.created does not match the pending claim, dropping',\n );\n await params.recordDelivery(params.deliveryId);\n return;\n }\n\n let authorization: Awaited<ReturnType<SentryApiClient['exchangeAuthorizationCode']>> | undefined;\n if (claimed.status === 'pending') {\n try {\n authorization = await params.sentry.exchangeAuthorizationCode({\n installationUuid: params.installationUuid,\n code: params.code,\n });\n } catch (error) {\n if (!(error instanceof SentryIntegrationProviderError) || error.reason !== 'access-denied') {\n throw error;\n }\n\n const current = await params.getSentryInstallation({\n installationUuid: params.installationUuid,\n });\n const concurrentAttemptSucceeded =\n current?.codeHash === codeHash &&\n (current.status === 'exchange-succeeded' || current.status === 'installed');\n if (!current || !concurrentAttemptSucceeded) throw error;\n claimed = current;\n }\n\n if (authorization) {\n await markSentryInstallationExchangeSucceeded({\n installationUuid: params.installationUuid,\n codeHash,\n });\n }\n }\n\n const completed = await params.persistUnclaimedAndRecordDelivery({\n installationUuid: params.installationUuid,\n orgSlug: claimed.orgSlug,\n codeHash,\n deliveryId: params.deliveryId,\n });\n if (authorization && completed.status === 'installed' && params.verifyInstall) {\n await verifySentryInstallationBestEffort({\n sentry: params.sentry,\n installationUuid: params.installationUuid,\n token: authorization.token,\n });\n }\n}\n\nexport interface HandleSentryInstallationDeletedParams {\n tx: IntegrationTx;\n deliveryId: string;\n installationUuid: string;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n updateConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;\n}\n\n// Tombstones the install and disables its connection if one exists. An unknown\n// uuid also gets a tombstone, so a reordered creation cannot restore it. The\n// state change and delivery record share the caller's transaction.\nexport async function handleSentryInstallationDeleted(\n params: HandleSentryInstallationDeletedParams,\n): Promise<void> {\n const installation = await markSentryInstallationDeleted(\n {installationUuid: params.installationUuid},\n {tx: params.tx},\n );\n if (installation?.connectionId) {\n await params.updateConnectionLifecycleStatus(\n {id: installation.connectionId, lifecycleStatus: 'disabled'},\n {tx: params.tx},\n );\n }\n\n await params.recordDeliveryOnly({\n tx: params.tx,\n provider: SENTRY_SOURCE,\n deliveryId: params.deliveryId,\n });\n}\n\n// A raw Sentry `ignored` action is normalized to `archived` before validation,\n// so legacy ignore events still fire `issue.archived` workflows.\nexport function normalizeSentryIssueAction(parsedJson: unknown): unknown {\n if (typeof parsedJson !== 'object' || parsedJson === null) return parsedJson;\n const obj = parsedJson as {action?: unknown};\n if (obj.action === 'ignored') {\n return {...obj, action: 'archived'};\n }\n return parsedJson;\n}\n\nfunction normalizeIssuePayload(payload: SentryIssueWebhookDto): SentryIssuePayload {\n const issue = payload.data.issue;\n return {\n action: payload.action,\n issueId: issue.id,\n shortId: issue.shortId ?? null,\n title: issue.title ?? DEFAULT_ISSUE_TITLE,\n culprit: issue.culprit ?? null,\n level: issue.level ?? null,\n status: issue.status ?? null,\n platform: issue.platform ?? null,\n webUrl: issue.web_url ?? null,\n issueUrl: issue.url ?? null,\n projectUrl: issue.project_url ?? null,\n firstSeenAt: issue.firstSeen ?? null,\n lastSeenAt: issue.lastSeen ?? null,\n };\n}\n"],"names":["logger","SentryConnectionNotFoundError","SentryInstallationDeletedError","SentryInstallationNotFoundError","SentryInstallationUnclaimedError","SentryIntegrationProviderError","hashAuthorizationCode","verifySentryInstallationBestEffort","claimSentryInstallationVerification","getSentryInstallationByInstallationUuid","markSentryInstallationDeleted","markSentryInstallationExchangeSucceeded","SENTRY_SOURCE","DEFAULT_ISSUE_TITLE","DELETED_STATUS","handleSentryIssueEvent","params","installationUuid","payload","installation","uuid","tx","status","connectionId","connection","getIntegrationConnectionById","publishIntegrationEventReceived","event","provider","source","slug","action","workspaceId","id","connectionName","displayName","deliveryId","receivedAt","Date","toISOString","normalizeIssuePayload","handleSentryInstallationCreated","existing","getSentryInstallation","debug","recordDelivery","code","orgSlug","warn","codeHash","claimed","authorization","sentry","exchangeAuthorizationCode","error","reason","current","concurrentAttemptSucceeded","completed","persistUnclaimedAndRecordDelivery","verifyInstall","token","handleSentryInstallationDeleted","updateConnectionLifecycleStatus","lifecycleStatus","recordDeliveryOnly","normalizeSentryIssueAction","parsedJson","obj","issue","data","issueId","shortId","title","culprit","level","platform","webUrl","web_url","issueUrl","url","projectUrl","project_url","firstSeenAt","firstSeen","lastSeenAt","lastSeen"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GetIntegrationConnectionByIdFn, PublishIntegrationEventReceivedFn, RecordDeliveryOnlyFn, UpdateIntegrationConnectionLifecycleStatusFn } from '@shipfox/api-integration-
|
|
1
|
+
import type { GetIntegrationConnectionByIdFn, PublishIntegrationEventReceivedFn, RecordDeliveryOnlyFn, UpdateIntegrationConnectionLifecycleStatusFn } from '@shipfox/api-integration-spi';
|
|
2
2
|
import type { ModuleWorker } from '@shipfox/node-module';
|
|
3
3
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
4
4
|
import { type SentryApiClient } from '#api/client.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,8BAA8B,EAC9B,iCAAiC,EACjC,oBAAoB,EACpB,4CAA4C,EAC7C,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,8BAA8B,EAC9B,iCAAiC,EACjC,oBAAoB,EACpB,4CAA4C,EAC7C,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAwB,KAAK,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAE3E,OAAO,EAAC,OAAO,EAAE,EAAE,EAAC,MAAM,WAAW,CAAC;AACtC,OAAO,EACL,mCAAmC,EACnC,oCAAoC,EACrC,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAC,cAAc,EAAC,MAAM,mBAAmB,CAAC;AACjD,OAAO,EACL,KAAK,oCAAoC,EAE1C,MAAM,iCAAiC,CAAC;AAKzC,YAAY,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AACpD,OAAO,EACL,6BAA6B,EAC7B,oCAAoC,EACpC,8BAA8B,EAC9B,8BAA8B,EAC9B,iCAAiC,GAClC,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,8BAA8B,EAC9B,2CAA2C,GAC5C,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,qCAAqC,GACtC,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,+BAA+B,EAC/B,+BAA+B,EAC/B,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,mCAAmC,EACnC,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAC,4BAA4B,EAAC,MAAM,4BAA4B,CAAC;AACxE,YAAY,EACV,0CAA0C,EAC1C,kBAAkB,EAClB,wBAAwB,EACxB,8BAA8B,GAC/B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,uCAAuC,EACvC,oCAAoC,EACpC,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAC,OAAO,EAAE,EAAE,EAAE,cAAc,EAAC,CAAC;AAErC,MAAM,WAAW,sCACf,SAAQ,IAAI,CACV,oCAAoC,EACpC,QAAQ,GAAG,sCAAsC,CAClD;IACD,MAAM,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;IACrC,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;IAC9E,mCAAmC,CAAC,EAAE,OAAO,mCAAmC,GAAG,SAAS,CAAC;IAC7F,oCAAoC,CAAC,EAAE,OAAO,oCAAoC,GAAG,SAAS,CAAC;CAChG;AAED,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,sCAAsC;;;sCAkBnD;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;;;;;;EAyBrF;AAKD,wBAAgB,6BAA6B,IAAI,YAAY,CAa5D"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {dirname, resolve} from 'node:path';\nimport {fileURLToPath} from 'node:url';\nimport type {\n GetIntegrationConnectionByIdFn,\n PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n UpdateIntegrationConnectionLifecycleStatusFn,\n} from '@shipfox/api-integration-
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {dirname, resolve} from 'node:path';\nimport {fileURLToPath} from 'node:url';\nimport type {\n GetIntegrationConnectionByIdFn,\n PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n UpdateIntegrationConnectionLifecycleStatusFn,\n} from '@shipfox/api-integration-spi';\nimport type {ModuleWorker} from '@shipfox/node-module';\nimport type {NodePgDatabase} from 'drizzle-orm/node-postgres';\nimport {createSentryApiClient, type SentryApiClient} from '#api/client.js';\nimport {createSentryWebhookProcessor} from '#core/webhook-processor.js';\nimport {closeDb, db} from '#db/db.js';\nimport {\n getSentryInstallationByConnectionId,\n persistVerifiedUnclaimedInstallation,\n} from '#db/installations.js';\nimport {migrationsPath} from '#db/migrations.js';\nimport {\n type CreateSentryIntegrationRoutesOptions,\n createSentryIntegrationRoutes,\n} from '#presentation/routes/install.js';\nimport {createSentryWebhookRoutes} from '#presentation/routes/webhooks.js';\nimport {createSentryMaintenanceActivities} from '#temporal/activities/index.js';\nimport {SENTRY_MAINTENANCE_TASK_QUEUE} from '#temporal/constants.js';\n\nexport type {SentryApiClient} from '#api/client.js';\nexport {\n SentryClaimProofMismatchError,\n SentryInstallationAlreadyLinkedError,\n SentryInstallationDeletedError,\n SentryIntegrationProviderError,\n SentryVerificationInProgressError,\n} from '#core/errors.js';\nexport type {\n ConnectSentryInstallationInput,\n VerifyAndPersistUnclaimedInstallationParams,\n} from '#core/install.js';\nexport {\n handleSentryConnect,\n hashAuthorizationCode,\n verifyAndPersistUnclaimedInstallation,\n} from '#core/install.js';\nexport {\n handleSentryInstallationCreated,\n handleSentryInstallationDeleted,\n handleSentryIssueEvent,\n} from '#core/webhook.js';\nexport type {\n CreateSentryWebhookProcessorOptions,\n SentryWebhookProcessor,\n} from '#core/webhook-processor.js';\nexport {createSentryWebhookProcessor} from '#core/webhook-processor.js';\nexport type {\n PersistVerifiedUnclaimedInstallationParams,\n SentryInstallation,\n SentryInstallationStatus,\n UpsertSentryInstallationParams,\n} from '#db/installations.js';\nexport {\n getSentryInstallationByInstallationUuid,\n persistVerifiedUnclaimedInstallation,\n upsertSentryInstallation,\n} from '#db/installations.js';\nexport {closeDb, db, migrationsPath};\n\nexport interface CreateSentryIntegrationProviderOptions\n extends Omit<\n CreateSentryIntegrationRoutesOptions,\n 'sentry' | 'persistVerifiedUnclaimedInstallation'\n > {\n sentry?: SentryApiClient | undefined;\n coreDb: () => NodePgDatabase<Record<string, unknown>>;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n updateConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;\n getSentryInstallationByConnectionId?: typeof getSentryInstallationByConnectionId | undefined;\n persistVerifiedUnclaimedInstallation?: typeof persistVerifiedUnclaimedInstallation | undefined;\n}\n\nexport function createSentryIntegrationProvider(options: CreateSentryIntegrationProviderOptions) {\n const sentry = options.sentry ?? createSentryApiClient();\n const getInstallationByConnectionId =\n options.getSentryInstallationByConnectionId ?? getSentryInstallationByConnectionId;\n const persistUnclaimed =\n options.persistVerifiedUnclaimedInstallation ?? persistVerifiedUnclaimedInstallation;\n const webhookProcessor = createSentryWebhookProcessor({\n sentry,\n coreDb: options.coreDb,\n publishIntegrationEventReceived: options.publishIntegrationEventReceived,\n recordDeliveryOnly: options.recordDeliveryOnly,\n getIntegrationConnectionById: options.getIntegrationConnectionById,\n updateConnectionLifecycleStatus: options.updateConnectionLifecycleStatus,\n });\n\n return {\n provider: 'sentry' as const,\n displayName: 'Sentry',\n async connectionExternalUrl(connection: {id: string}): Promise<string | undefined> {\n const installation = await getInstallationByConnectionId(connection.id);\n if (!installation?.orgSlug) return undefined;\n return `https://sentry.io/organizations/${encodeURIComponent(installation.orgSlug)}/`;\n },\n routes: [\n createSentryIntegrationRoutes({\n sentry,\n getSentryInstallation: options.getSentryInstallation,\n getConnectionById: options.getConnectionById,\n connectSentryInstallation: options.connectSentryInstallation,\n persistVerifiedUnclaimedInstallation: persistUnclaimed,\n }),\n createSentryWebhookRoutes({\n sentry,\n coreDb: options.coreDb,\n publishIntegrationEventReceived: options.publishIntegrationEventReceived,\n recordDeliveryOnly: options.recordDeliveryOnly,\n getIntegrationConnectionById: options.getIntegrationConnectionById,\n updateConnectionLifecycleStatus: options.updateConnectionLifecycleStatus,\n processor: webhookProcessor,\n }),\n ],\n webhookProcessors: [{routeIds: ['sentry'] as const, processor: webhookProcessor}],\n };\n}\n\nconst packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');\nconst maintenanceWorkflowsPath = resolve(packageRoot, 'dist/temporal/workflows/index.js');\n\nexport function createSentryMaintenanceWorker(): ModuleWorker {\n return {\n taskQueue: SENTRY_MAINTENANCE_TASK_QUEUE,\n workflowsPath: maintenanceWorkflowsPath,\n activities: createSentryMaintenanceActivities,\n workflows: [\n {\n name: 'pruneUnclaimedSentryInstallationsCron',\n id: 'sentry-prune-unclaimed-installations',\n cronSchedule: '0 4 * * *',\n },\n ],\n };\n}\n"],"names":["dirname","resolve","fileURLToPath","createSentryApiClient","createSentryWebhookProcessor","closeDb","db","getSentryInstallationByConnectionId","persistVerifiedUnclaimedInstallation","migrationsPath","createSentryIntegrationRoutes","createSentryWebhookRoutes","createSentryMaintenanceActivities","SENTRY_MAINTENANCE_TASK_QUEUE","SentryClaimProofMismatchError","SentryInstallationAlreadyLinkedError","SentryInstallationDeletedError","SentryIntegrationProviderError","SentryVerificationInProgressError","handleSentryConnect","hashAuthorizationCode","verifyAndPersistUnclaimedInstallation","handleSentryInstallationCreated","handleSentryInstallationDeleted","handleSentryIssueEvent","getSentryInstallationByInstallationUuid","upsertSentryInstallation","createSentryIntegrationProvider","options","sentry","getInstallationByConnectionId","persistUnclaimed","webhookProcessor","coreDb","publishIntegrationEventReceived","recordDeliveryOnly","getIntegrationConnectionById","updateConnectionLifecycleStatus","provider","displayName","connectionExternalUrl","connection","installation","id","orgSlug","undefined","encodeURIComponent","routes","getSentryInstallation","getConnectionById","connectSentryInstallation","processor","webhookProcessors","routeIds","packageRoot","url","maintenanceWorkflowsPath","createSentryMaintenanceWorker","taskQueue","workflowsPath","activities","workflows","name","cronSchedule"],"mappings":"AAAA,SAAQA,OAAO,EAAEC,OAAO,QAAO,YAAY;AAC3C,SAAQC,aAAa,QAAO,WAAW;AASvC,SAAQC,qBAAqB,QAA6B,iBAAiB;AAC3E,SAAQC,4BAA4B,QAAO,6BAA6B;AACxE,SAAQC,OAAO,EAAEC,EAAE,QAAO,YAAY;AACtC,SACEC,mCAAmC,EACnCC,oCAAoC,QAC/B,uBAAuB;AAC9B,SAAQC,cAAc,QAAO,oBAAoB;AACjD,SAEEC,6BAA6B,QACxB,kCAAkC;AACzC,SAAQC,yBAAyB,QAAO,mCAAmC;AAC3E,SAAQC,iCAAiC,QAAO,gCAAgC;AAChF,SAAQC,6BAA6B,QAAO,yBAAyB;AAGrE,SACEC,6BAA6B,EAC7BC,oCAAoC,EACpCC,8BAA8B,EAC9BC,8BAA8B,EAC9BC,iCAAiC,QAC5B,kBAAkB;AAKzB,SACEC,mBAAmB,EACnBC,qBAAqB,EACrBC,qCAAqC,QAChC,mBAAmB;AAC1B,SACEC,+BAA+B,EAC/BC,+BAA+B,EAC/BC,sBAAsB,QACjB,mBAAmB;AAK1B,SAAQpB,4BAA4B,QAAO,6BAA6B;AAOxE,SACEqB,uCAAuC,EACvCjB,oCAAoC,EACpCkB,wBAAwB,QACnB,uBAAuB;AAC9B,SAAQrB,OAAO,EAAEC,EAAE,EAAEG,cAAc,GAAE;AAiBrC,OAAO,SAASkB,gCAAgCC,OAA+C;IAC7F,MAAMC,SAASD,QAAQC,MAAM,IAAI1B;IACjC,MAAM2B,gCACJF,QAAQrB,mCAAmC,IAAIA;IACjD,MAAMwB,mBACJH,QAAQpB,oCAAoC,IAAIA;IAClD,MAAMwB,mBAAmB5B,6BAA6B;QACpDyB;QACAI,QAAQL,QAAQK,MAAM;QACtBC,iCAAiCN,QAAQM,+BAA+B;QACxEC,oBAAoBP,QAAQO,kBAAkB;QAC9CC,8BAA8BR,QAAQQ,4BAA4B;QAClEC,iCAAiCT,QAAQS,+BAA+B;IAC1E;IAEA,OAAO;QACLC,UAAU;QACVC,aAAa;QACb,MAAMC,uBAAsBC,UAAwB;YAClD,MAAMC,eAAe,MAAMZ,8BAA8BW,WAAWE,EAAE;YACtE,IAAI,CAACD,cAAcE,SAAS,OAAOC;YACnC,OAAO,CAAC,gCAAgC,EAAEC,mBAAmBJ,aAAaE,OAAO,EAAE,CAAC,CAAC;QACvF;QACAG,QAAQ;YACNrC,8BAA8B;gBAC5BmB;gBACAmB,uBAAuBpB,QAAQoB,qBAAqB;gBACpDC,mBAAmBrB,QAAQqB,iBAAiB;gBAC5CC,2BAA2BtB,QAAQsB,yBAAyB;gBAC5D1C,sCAAsCuB;YACxC;YACApB,0BAA0B;gBACxBkB;gBACAI,QAAQL,QAAQK,MAAM;gBACtBC,iCAAiCN,QAAQM,+BAA+B;gBACxEC,oBAAoBP,QAAQO,kBAAkB;gBAC9CC,8BAA8BR,QAAQQ,4BAA4B;gBAClEC,iCAAiCT,QAAQS,+BAA+B;gBACxEc,WAAWnB;YACb;SACD;QACDoB,mBAAmB;YAAC;gBAACC,UAAU;oBAAC;iBAAS;gBAAWF,WAAWnB;YAAgB;SAAE;IACnF;AACF;AAEA,MAAMsB,cAAcrD,QAAQD,QAAQE,cAAc,YAAYqD,GAAG,IAAI;AACrE,MAAMC,2BAA2BvD,QAAQqD,aAAa;AAEtD,OAAO,SAASG;IACd,OAAO;QACLC,WAAW7C;QACX8C,eAAeH;QACfI,YAAYhD;QACZiD,WAAW;YACT;gBACEC,MAAM;gBACNnB,IAAI;gBACJoB,cAAc;YAChB;SACD;IACH;AACF"}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { IntegrationConnection } from '@shipfox/api-integration-spi';
|
|
2
2
|
export declare function toIntegrationConnectionDto(connection: IntegrationConnection<'sentry'>): {
|
|
3
|
-
created_at: string;
|
|
4
|
-
updated_at: string;
|
|
5
|
-
external_url?: string;
|
|
6
3
|
id: string;
|
|
7
4
|
workspace_id: string;
|
|
8
|
-
provider:
|
|
5
|
+
provider: "sentry";
|
|
9
6
|
external_account_id: string;
|
|
10
7
|
slug: string;
|
|
11
8
|
display_name: string;
|
|
12
|
-
lifecycle_status: import("@shipfox/api-integration-
|
|
13
|
-
capabilities:
|
|
9
|
+
lifecycle_status: import("@shipfox/api-integration-spi").IntegrationConnectionLifecycleStatus;
|
|
10
|
+
capabilities: never[];
|
|
11
|
+
created_at: string;
|
|
12
|
+
updated_at: string;
|
|
14
13
|
};
|
|
15
14
|
//# sourceMappingURL=integrations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integrations.d.ts","sourceRoot":"","sources":["../../../src/presentation/dto/integrations.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"integrations.d.ts","sourceRoot":"","sources":["../../../src/presentation/dto/integrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,8BAA8B,CAAC;AAGxE,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,qBAAqB,CAAC,QAAQ,CAAC;;;;;;;;;;;EAarF"}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import { toIntegrationConnectionDto as toCoreIntegrationConnectionDto } from '@shipfox/api-integration-core-dto';
|
|
2
1
|
// Sentry exposes no adapters, so its connections carry no capabilities.
|
|
3
2
|
export function toIntegrationConnectionDto(connection) {
|
|
4
|
-
return
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
return {
|
|
4
|
+
id: connection.id,
|
|
5
|
+
workspace_id: connection.workspaceId,
|
|
6
|
+
provider: connection.provider,
|
|
7
|
+
external_account_id: connection.externalAccountId,
|
|
8
|
+
slug: connection.slug,
|
|
9
|
+
display_name: connection.displayName,
|
|
10
|
+
lifecycle_status: connection.lifecycleStatus,
|
|
11
|
+
capabilities: [],
|
|
12
|
+
created_at: connection.createdAt.toISOString(),
|
|
13
|
+
updated_at: connection.updatedAt.toISOString()
|
|
14
|
+
};
|
|
7
15
|
}
|
|
8
16
|
|
|
9
17
|
//# sourceMappingURL=integrations.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/presentation/dto/integrations.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/dto/integrations.ts"],"sourcesContent":["import type {IntegrationConnection} from '@shipfox/api-integration-spi';\n\n// Sentry exposes no adapters, so its connections carry no capabilities.\nexport function toIntegrationConnectionDto(connection: IntegrationConnection<'sentry'>) {\n return {\n id: connection.id,\n workspace_id: connection.workspaceId,\n provider: connection.provider,\n external_account_id: connection.externalAccountId,\n slug: connection.slug,\n display_name: connection.displayName,\n lifecycle_status: connection.lifecycleStatus,\n capabilities: [],\n created_at: connection.createdAt.toISOString(),\n updated_at: connection.updatedAt.toISOString(),\n };\n}\n"],"names":["toIntegrationConnectionDto","connection","id","workspace_id","workspaceId","provider","external_account_id","externalAccountId","slug","display_name","displayName","lifecycle_status","lifecycleStatus","capabilities","created_at","createdAt","toISOString","updated_at","updatedAt"],"mappings":"AAEA,wEAAwE;AACxE,OAAO,SAASA,2BAA2BC,UAA2C;IACpF,OAAO;QACLC,IAAID,WAAWC,EAAE;QACjBC,cAAcF,WAAWG,WAAW;QACpCC,UAAUJ,WAAWI,QAAQ;QAC7BC,qBAAqBL,WAAWM,iBAAiB;QACjDC,MAAMP,WAAWO,IAAI;QACrBC,cAAcR,WAAWS,WAAW;QACpCC,kBAAkBV,WAAWW,eAAe;QAC5CC,cAAc,EAAE;QAChBC,YAAYb,WAAWc,SAAS,CAACC,WAAW;QAC5CC,YAAYhB,WAAWiB,SAAS,CAACF,WAAW;IAC9C;AACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConnectionSlugConflictError } from '@shipfox/api-integration-
|
|
1
|
+
import { ConnectionSlugConflictError } from '@shipfox/api-integration-spi';
|
|
2
2
|
import { ClientError } from '@shipfox/node-fastify';
|
|
3
3
|
import { SentryClaimProofMismatchError, SentryInstallationAlreadyLinkedError, SentryInstallationDeletedError, SentryIntegrationProviderError, SentryVerificationInProgressError } from '#core/errors.js';
|
|
4
4
|
function providerStatus(reason) {
|