@shipfox/api-integration-sentry 4.0.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +49 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -5
- package/dist/config.js.map +1 -1
- package/dist/core/install.d.ts +6 -0
- package/dist/core/install.d.ts.map +1 -1
- package/dist/core/install.js +8 -5
- package/dist/core/install.js.map +1 -1
- package/dist/core/signature.d.ts +1 -1
- package/dist/core/signature.d.ts.map +1 -1
- package/dist/core/signature.js.map +1 -1
- package/dist/core/webhook-processor.d.ts +16 -0
- package/dist/core/webhook-processor.d.ts.map +1 -0
- package/dist/core/webhook-processor.js +198 -0
- package/dist/core/webhook-processor.js.map +1 -0
- package/dist/core/webhook.d.ts +4 -4
- package/dist/core/webhook.d.ts.map +1 -1
- package/dist/core/webhook.js +63 -49
- package/dist/core/webhook.js.map +1 -1
- package/dist/db/db.d.ts +8 -4
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/installations.d.ts +35 -13
- package/dist/db/installations.d.ts.map +1 -1
- package/dist/db/installations.js +89 -31
- package/dist/db/installations.js.map +1 -1
- package/dist/db/schema/installations.d.ts +5 -3
- package/dist/db/schema/installations.d.ts.map +1 -1
- package/dist/db/schema/installations.js +6 -6
- package/dist/db/schema/installations.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/webhooks.d.ts +4 -1
- package/dist/presentation/routes/webhooks.d.ts.map +1 -1
- package/dist/presentation/routes/webhooks.js +69 -43
- package/dist/presentation/routes/webhooks.js.map +1 -1
- package/dist/temporal/activities/prune-unclaimed-installations.d.ts +1 -0
- package/dist/temporal/activities/prune-unclaimed-installations.d.ts.map +1 -1
- package/dist/temporal/activities/prune-unclaimed-installations.js.map +1 -1
- package/dist/temporal/workflows/index.bundle.js +7 -2
- package/dist/temporal/workflows/prune-unclaimed-installations-cron.d.ts.map +1 -1
- package/dist/temporal/workflows/prune-unclaimed-installations-cron.js +6 -1
- package/dist/temporal/workflows/prune-unclaimed-installations-cron.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +20 -28
- package/src/config.ts +4 -5
- package/src/core/install.ts +21 -7
- package/src/core/signature.ts +1 -1
- package/src/core/webhook-processor.test.ts +304 -0
- package/src/core/webhook-processor.ts +224 -0
- package/src/core/webhook.ts +74 -53
- package/src/db/installations.test.ts +163 -17
- package/src/db/installations.ts +136 -35
- package/src/db/schema/installations.ts +7 -7
- package/src/index.test.ts +4 -1
- package/src/index.ts +16 -0
- package/src/presentation/routes/webhooks.test.ts +6 -5
- package/src/presentation/routes/webhooks.ts +88 -29
- package/src/temporal/activities/prune-unclaimed-installations.test.ts +24 -2
- package/src/temporal/activities/prune-unclaimed-installations.ts +4 -1
- package/src/temporal/workflows/prune-unclaimed-installations-cron.ts +4 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/presentation/routes/installation-handler.d.ts +0 -9
- package/dist/presentation/routes/installation-handler.d.ts.map +0 -1
- package/dist/presentation/routes/installation-handler.js +0 -70
- package/dist/presentation/routes/installation-handler.js.map +0 -1
- package/dist/presentation/routes/issue-handler.d.ts +0 -9
- package/dist/presentation/routes/issue-handler.d.ts.map +0 -1
- package/dist/presentation/routes/issue-handler.js +0 -50
- package/dist/presentation/routes/issue-handler.js.map +0 -1
- package/dist/presentation/routes/webhook-delivery.d.ts +0 -28
- package/dist/presentation/routes/webhook-delivery.d.ts.map +0 -1
- package/dist/presentation/routes/webhook-delivery.js +0 -56
- package/dist/presentation/routes/webhook-delivery.js.map +0 -1
- package/dist/presentation/routes/webhook-request.d.ts +0 -10
- package/dist/presentation/routes/webhook-request.d.ts.map +0 -1
- package/dist/presentation/routes/webhook-request.js +0 -56
- package/dist/presentation/routes/webhook-request.js.map +0 -1
- package/src/presentation/routes/installation-handler.ts +0 -76
- package/src/presentation/routes/issue-handler.ts +0 -52
- package/src/presentation/routes/webhook-delivery.ts +0 -61
- package/src/presentation/routes/webhook-request.ts +0 -66
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:
|
|
3
|
-
2026-07-
|
|
2
|
+
Successfully compiled: 23 files with swc (345.22ms)
|
|
3
|
+
2026-07-21T08:09:54.975Z [INFO] asset workflow-bundle-3e5133a469aebb0f8908.js 1.45 MiB [emitted] [immutable] (name: main)
|
|
4
4
|
runtime modules 670 bytes 3 modules
|
|
5
5
|
modules by path ../../../../node_modules/.pnpm/ 424 KiB
|
|
6
6
|
modules by path ../../../../node_modules/.pnpm/@temporalio+common@1.18.1/node_modules/@temporal...(truncated) 155 KiB 34 modules
|
|
@@ -8,13 +8,13 @@ modules by path ../../../../node_modules/.pnpm/ 424 KiB
|
|
|
8
8
|
modules by path ../../../../node_modules/.pnpm/nexus-rpc@0.0.2/node_modules/nexus-rpc/lib/ 22.3 KiB 13 modules
|
|
9
9
|
../../../../node_modules/.pnpm/long@5.3.2/node_modules/long/index.js 43.2 KiB [built] [code generated]
|
|
10
10
|
../../../../node_modules/.pnpm/ms@3.0.0-canary.1/node_modules/ms/dist/index.mjs 3.26 KiB [built] [code generated]
|
|
11
|
-
modules by path ./dist/temporal/workflows/ 1.
|
|
11
|
+
modules by path ./dist/temporal/workflows/ 1.23 KiB
|
|
12
12
|
./dist/temporal/workflows/index-autogenerated-entrypoint.cjs 537 bytes [built] [code generated]
|
|
13
13
|
./dist/temporal/workflows/index.js 98 bytes [built] [code generated]
|
|
14
|
-
./dist/temporal/workflows/prune-unclaimed-installations-cron.js
|
|
14
|
+
./dist/temporal/workflows/prune-unclaimed-installations-cron.js 622 bytes [built] [code generated]
|
|
15
15
|
optional modules 30 bytes [optional]
|
|
16
16
|
__temporal_custom_payload_converter (ignored) 15 bytes [optional] [built] [code generated]
|
|
17
17
|
__temporal_custom_failure_converter (ignored) 15 bytes [optional] [built] [code generated]
|
|
18
|
-
webpack 5.107.2 compiled successfully in
|
|
19
|
-
2026-07-
|
|
20
|
-
/home/runner/work/shipfox/shipfox/libs/api/integration/sentry/dist/temporal/workflows/index.js:
|
|
18
|
+
webpack 5.107.2 compiled successfully in 3680 ms
|
|
19
|
+
2026-07-21T08:09:54.988Z [INFO] Workflow bundle created { size: '1.45MB' }
|
|
20
|
+
/home/runner/work/shipfox/shipfox/libs/api/integration/sentry/dist/temporal/workflows/index.js: 1517067 bytes
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# @shipfox/api-integration-sentry
|
|
2
2
|
|
|
3
|
+
## 6.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f262539: Adds a composed webhook processor and optional provider-neutral delivery source for hosted API runtimes.
|
|
8
|
+
- 8390468: Adds a shared Sentry webhook processor for direct and durable delivery adapters.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- f73da5d: Enforces bounded API context imports and routes inter-module consumers through producer contracts.
|
|
13
|
+
- 0649d62: Keeps GitHub credential cleanup and Sentry installation lifecycle handling safe across duplicate, concurrent, and reordered webhook delivery.
|
|
14
|
+
- Updated dependencies [0bb82a4]
|
|
15
|
+
- Updated dependencies [54ce48b]
|
|
16
|
+
- Updated dependencies [c0162b0]
|
|
17
|
+
- Updated dependencies [7ac43a4]
|
|
18
|
+
- Updated dependencies [f262539]
|
|
19
|
+
- Updated dependencies [a01e917]
|
|
20
|
+
- Updated dependencies [3bb4e26]
|
|
21
|
+
- Updated dependencies [8bdc149]
|
|
22
|
+
- Updated dependencies [3810996]
|
|
23
|
+
- Updated dependencies [b00ed29]
|
|
24
|
+
- Updated dependencies [8aa7cd3]
|
|
25
|
+
- Updated dependencies [81f9544]
|
|
26
|
+
- Updated dependencies [4604a06]
|
|
27
|
+
- @shipfox/api-integration-core-dto@6.0.0
|
|
28
|
+
- @shipfox/node-module@0.4.0
|
|
29
|
+
- @shipfox/node-drizzle@0.3.2
|
|
30
|
+
- @shipfox/api-auth-context@6.0.0
|
|
31
|
+
- @shipfox/node-fastify@0.2.4
|
|
32
|
+
- @shipfox/api-integration-sentry-dto@6.0.0
|
|
33
|
+
|
|
34
|
+
## 5.0.0
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- bb037af: Resolves workspace packages from source during development while published consumers continue to use compiled output.
|
|
39
|
+
- Updated dependencies [2875241]
|
|
40
|
+
- Updated dependencies [bb037af]
|
|
41
|
+
- Updated dependencies [fb70438]
|
|
42
|
+
- @shipfox/api-integration-core-dto@5.0.0
|
|
43
|
+
- @shipfox/api-auth-context@5.0.0
|
|
44
|
+
- @shipfox/api-integration-sentry-dto@5.0.0
|
|
45
|
+
- @shipfox/config@1.2.2
|
|
46
|
+
- @shipfox/node-drizzle@0.3.1
|
|
47
|
+
- @shipfox/node-fastify@0.2.3
|
|
48
|
+
- @shipfox/node-module@0.3.2
|
|
49
|
+
- @shipfox/node-opentelemetry@0.5.2
|
|
50
|
+
- @shipfox/node-postgres@0.4.2
|
|
51
|
+
|
|
3
52
|
## 4.0.0
|
|
4
53
|
|
|
5
54
|
### Patch Changes
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;;;;;;kDAkBjB,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;;;;;;kDAkBjB,CAAC;AAKH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAMlE"}
|
package/dist/config.js
CHANGED
|
@@ -14,14 +14,13 @@ export const config = createConfig({
|
|
|
14
14
|
default: true
|
|
15
15
|
}),
|
|
16
16
|
SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS: num({
|
|
17
|
-
desc: 'How many days
|
|
17
|
+
desc: 'How many days an unclaimed Sentry installation or incomplete exchange may sit without a state transition. The daily cleanup releases pending claims and tombstones verified installs after this window. Must be at least 1. Defaults to 7.',
|
|
18
18
|
default: 7
|
|
19
19
|
})
|
|
20
20
|
});
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// claimed yet. Reject it at startup rather than let the 04:00 cron fail silently.
|
|
21
|
+
// A value below 1 moves the cutoff to now or the future, which would expire fresh
|
|
22
|
+
// claims before a browser can bind them. Reject it at startup rather than let the
|
|
23
|
+
// 04:00 cron fail silently.
|
|
25
24
|
export function assertRetentionDaysWithinBounds(days) {
|
|
26
25
|
if (!Number.isFinite(days) || days < 1) {
|
|
27
26
|
throw new Error(`SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS must be a finite number of at least 1, received ${days}.`);
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {bool, createConfig, num, str} from '@shipfox/config';\n\nexport const config = createConfig({\n SENTRY_APP_CLIENT_ID: str({\n desc: 'OAuth client ID of the Sentry app. Reserved for the install and app-token-exchange flow; the webhook receiver does not read it yet. Required.',\n }),\n SENTRY_APP_CLIENT_SECRET: str({\n desc: 'Shared secret used to verify the HMAC-SHA256 signature on inbound Sentry webhooks. Must match the value set on the Sentry app. Required.',\n }),\n SENTRY_APP_SLUG: str({\n desc: 'URL slug of the Sentry app, used to build install and callback links. Required.',\n }),\n SENTRY_APP_VERIFY_INSTALL: bool({\n desc: 'Verifies the signature on Sentry app installation requests. Keep it true; turn it off only for local testing.',\n default: true,\n }),\n SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS: num({\n desc: 'How many days
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {bool, createConfig, num, str} from '@shipfox/config';\n\nexport const config = createConfig({\n SENTRY_APP_CLIENT_ID: str({\n desc: 'OAuth client ID of the Sentry app. Reserved for the install and app-token-exchange flow; the webhook receiver does not read it yet. Required.',\n }),\n SENTRY_APP_CLIENT_SECRET: str({\n desc: 'Shared secret used to verify the HMAC-SHA256 signature on inbound Sentry webhooks. Must match the value set on the Sentry app. Required.',\n }),\n SENTRY_APP_SLUG: str({\n desc: 'URL slug of the Sentry app, used to build install and callback links. Required.',\n }),\n SENTRY_APP_VERIFY_INSTALL: bool({\n desc: 'Verifies the signature on Sentry app installation requests. Keep it true; turn it off only for local testing.',\n default: true,\n }),\n SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS: num({\n desc: 'How many days an unclaimed Sentry installation or incomplete exchange may sit without a state transition. The daily cleanup releases pending claims and tombstones verified installs after this window. Must be at least 1. Defaults to 7.',\n default: 7,\n }),\n});\n\n// A value below 1 moves the cutoff to now or the future, which would expire fresh\n// claims before a browser can bind them. Reject it at startup rather than let the\n// 04:00 cron fail silently.\nexport function assertRetentionDaysWithinBounds(days: number): void {\n if (!Number.isFinite(days) || days < 1) {\n throw new Error(\n `SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS must be a finite number of at least 1, received ${days}.`,\n );\n }\n}\n\nassertRetentionDaysWithinBounds(config.SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS);\n"],"names":["bool","createConfig","num","str","config","SENTRY_APP_CLIENT_ID","desc","SENTRY_APP_CLIENT_SECRET","SENTRY_APP_SLUG","SENTRY_APP_VERIFY_INSTALL","default","SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS","assertRetentionDaysWithinBounds","days","Number","isFinite","Error"],"mappings":"AAAA,SAAQA,IAAI,EAAEC,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAE7D,OAAO,MAAMC,SAASH,aAAa;IACjCI,sBAAsBF,IAAI;QACxBG,MAAM;IACR;IACAC,0BAA0BJ,IAAI;QAC5BG,MAAM;IACR;IACAE,iBAAiBL,IAAI;QACnBG,MAAM;IACR;IACAG,2BAA2BT,KAAK;QAC9BM,MAAM;QACNI,SAAS;IACX;IACAC,8CAA8CT,IAAI;QAChDI,MAAM;QACNI,SAAS;IACX;AACF,GAAG;AAEH,kFAAkF;AAClF,kFAAkF;AAClF,4BAA4B;AAC5B,OAAO,SAASE,gCAAgCC,IAAY;IAC1D,IAAI,CAACC,OAAOC,QAAQ,CAACF,SAASA,OAAO,GAAG;QACtC,MAAM,IAAIG,MACR,CAAC,6FAA6F,EAAEH,KAAK,CAAC,CAAC;IAE3G;AACF;AAEAD,gCAAgCR,OAAOO,4CAA4C"}
|
package/dist/core/install.d.ts
CHANGED
|
@@ -60,4 +60,10 @@ export interface HandleSentryConnectParams {
|
|
|
60
60
|
* - anything else on an existing unclaimed row → proof mismatch (403, IDOR gate).
|
|
61
61
|
*/
|
|
62
62
|
export declare function handleSentryConnect(params: HandleSentryConnectParams): Promise<IntegrationConnection<'sentry'>>;
|
|
63
|
+
export declare function verifySentryInstallationBestEffort(input: {
|
|
64
|
+
sentry: SentryApiClient;
|
|
65
|
+
installationUuid: string;
|
|
66
|
+
token: string;
|
|
67
|
+
connectionId?: string;
|
|
68
|
+
}): Promise<void>;
|
|
63
69
|
//# sourceMappingURL=install.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/core/install.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,mCAAmC,CAAC;AAE7E,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,
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/core/install.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,mCAAmC,CAAC;AAE7E,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
CHANGED
|
@@ -37,7 +37,7 @@ function codeHashesEqual(presented, stored) {
|
|
|
37
37
|
codeHash: hashAuthorizationCode(params.code)
|
|
38
38
|
});
|
|
39
39
|
if (params.verifyInstall) {
|
|
40
|
-
await
|
|
40
|
+
await verifySentryInstallationBestEffort({
|
|
41
41
|
sentry: params.sentry,
|
|
42
42
|
installationUuid: params.installationUuid,
|
|
43
43
|
token: authorization.token
|
|
@@ -71,6 +71,9 @@ function codeHashesEqual(presented, stored) {
|
|
|
71
71
|
if (install.connectionId) {
|
|
72
72
|
return resolveClaimedInstall(params, install.connectionId);
|
|
73
73
|
}
|
|
74
|
+
if (!isVerifiedUnclaimed(install)) {
|
|
75
|
+
throw new SentryVerificationInProgressError(params.installationUuid);
|
|
76
|
+
}
|
|
74
77
|
return claimVerifiedInstall(params, install);
|
|
75
78
|
}
|
|
76
79
|
return claimBrowserFirst(params);
|
|
@@ -106,7 +109,7 @@ async function claimVerifiedInstall(params, install) {
|
|
|
106
109
|
codeHash: hashAuthorizationCode(params.code)
|
|
107
110
|
});
|
|
108
111
|
if (params.verifyInstall) {
|
|
109
|
-
await
|
|
112
|
+
await verifySentryInstallationBestEffort({
|
|
110
113
|
sentry: params.sentry,
|
|
111
114
|
installationUuid: params.installationUuid,
|
|
112
115
|
token: authorization.token,
|
|
@@ -136,7 +139,7 @@ async function claimBrowserFirst(params) {
|
|
|
136
139
|
codeHash: hashAuthorizationCode(params.code)
|
|
137
140
|
});
|
|
138
141
|
if (params.verifyInstall) {
|
|
139
|
-
await
|
|
142
|
+
await verifySentryInstallationBestEffort({
|
|
140
143
|
sentry: params.sentry,
|
|
141
144
|
installationUuid: params.installationUuid,
|
|
142
145
|
token: result.authorization.token,
|
|
@@ -168,7 +171,7 @@ async function reconcileConcurrentClaim(params) {
|
|
|
168
171
|
throw new SentryVerificationInProgressError(params.installationUuid);
|
|
169
172
|
}
|
|
170
173
|
function isVerifiedUnclaimed(install) {
|
|
171
|
-
return install.connectionId === null && install.status === 'installed';
|
|
174
|
+
return install.connectionId === null && install.status === 'installed' && install.codeHash !== null;
|
|
172
175
|
}
|
|
173
176
|
function bindClaim(params, binding) {
|
|
174
177
|
return params.connectSentryInstallation({
|
|
@@ -180,7 +183,7 @@ function bindClaim(params, binding) {
|
|
|
180
183
|
codeHash: binding.codeHash
|
|
181
184
|
});
|
|
182
185
|
}
|
|
183
|
-
async function
|
|
186
|
+
export async function verifySentryInstallationBestEffort(input) {
|
|
184
187
|
// The row is already persisted and receiving webhooks, so a verify failure only
|
|
185
188
|
// leaves the install pending on Sentry's side (re-verifying needs a fresh token
|
|
186
189
|
// we cannot mint here). Log it rather than failing a working claim.
|
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 bestEffortVerifyInstallation({\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 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: SentryInstallation,\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 bestEffortVerifyInstallation({\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 bestEffortVerifyInstallation({\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\nfunction isVerifiedUnclaimed(install: SentryInstallation): boolean {\n return install.connectionId === null && install.status === 'installed';\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\nasync function bestEffortVerifyInstallation(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","bestEffortVerifyInstallation","handleSentryConnect","install","getSentryInstallation","status","connectionId","resolveClaimedInstall","claimVerifiedInstall","claimBrowserFirst","connection","getConnectionById","workspaceId","error","isCodeAlreadyUsed","bindClaim","id","result","reconcileConcurrentClaim","reread","isVerifiedUnclaimed","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-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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"}
|
package/dist/core/signature.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../../src/core/signature.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../../src/core/signature.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,2BAA2B,GAAG,OAAO,CAElF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/signature.ts"],"sourcesContent":["import {verifyHexHmacSignature} from '@shipfox/node-fastify';\n\nexport interface VerifySentrySignatureParams {\n rawBody: string;\n signature: string;\n secret: string;\n}\n\nexport function verifySentrySignature(params: VerifySentrySignatureParams): boolean {\n return verifyHexHmacSignature(params);\n}\n"],"names":["verifyHexHmacSignature","verifySentrySignature","params"],"mappings":"AAAA,SAAQA,sBAAsB,QAAO,wBAAwB;AAQ7D,OAAO,SAASC,sBAAsBC,MAAmC;IACvE,OAAOF,uBAAuBE;AAChC"}
|
|
1
|
+
{"version":3,"sources":["../../src/core/signature.ts"],"sourcesContent":["import {verifyHexHmacSignature} from '@shipfox/node-fastify';\n\nexport interface VerifySentrySignatureParams {\n rawBody: string | Uint8Array;\n signature: string;\n secret: string;\n}\n\nexport function verifySentrySignature(params: VerifySentrySignatureParams): boolean {\n return verifyHexHmacSignature(params);\n}\n"],"names":["verifyHexHmacSignature","verifySentrySignature","params"],"mappings":"AAAA,SAAQA,sBAAsB,QAAO,wBAAwB;AAQ7D,OAAO,SAASC,sBAAsBC,MAAmC;IACvE,OAAOF,uBAAuBE;AAChC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type GetIntegrationConnectionByIdFn, type PublishIntegrationEventReceivedFn, type RecordDeliveryOnlyFn, type StoredWebhookRequest, type UpdateIntegrationConnectionLifecycleStatusFn, type WebhookProcessingResult } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
3
|
+
import type { SentryApiClient } from '#api/client.js';
|
|
4
|
+
export interface SentryWebhookProcessor {
|
|
5
|
+
process(request: StoredWebhookRequest): Promise<WebhookProcessingResult>;
|
|
6
|
+
}
|
|
7
|
+
export interface CreateSentryWebhookProcessorOptions {
|
|
8
|
+
sentry: SentryApiClient;
|
|
9
|
+
coreDb: () => NodePgDatabase<Record<string, unknown>>;
|
|
10
|
+
publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;
|
|
11
|
+
recordDeliveryOnly: RecordDeliveryOnlyFn;
|
|
12
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
13
|
+
updateConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;
|
|
14
|
+
}
|
|
15
|
+
export declare function createSentryWebhookProcessor(context: CreateSentryWebhookProcessorOptions): SentryWebhookProcessor;
|
|
16
|
+
//# sourceMappingURL=webhook-processor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-processor.d.ts","sourceRoot":"","sources":["../../src/core/webhook-processor.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,8BAA8B,EACnC,KAAK,iCAAiC,EACtC,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,4CAA4C,EACjD,KAAK,uBAAuB,EAC7B,MAAM,mCAAmC,CAAC;AAM3C,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"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { decodeWebhookBody } from '@shipfox/api-integration-core-dto';
|
|
3
|
+
import { sentryInstallationWebhookSchema, sentryIssueWebhookSchema } from '@shipfox/api-integration-sentry-dto';
|
|
4
|
+
import { logger } from '@shipfox/node-opentelemetry';
|
|
5
|
+
import { config } from '#config.js';
|
|
6
|
+
import { SentryIssueDroppedError } from '#core/errors.js';
|
|
7
|
+
import { handleSentryInstallationCreated, handleSentryInstallationDeleted, handleSentryIssueEvent, normalizeSentryIssueAction } from '#core/webhook.js';
|
|
8
|
+
import { completeSentryInstallationVerification, getSentryInstallationByInstallationUuid } from '#db/installations.js';
|
|
9
|
+
import { verifySentrySignature } from './signature.js';
|
|
10
|
+
const DELIVERY_ID_HEADER = 'request-id';
|
|
11
|
+
const RESOURCE_HEADER = 'sentry-hook-resource';
|
|
12
|
+
const SIGNATURE_HEADER = 'sentry-hook-signature';
|
|
13
|
+
const LEGACY_SIGNATURE_HEADER = 'sentry-app-signature';
|
|
14
|
+
const ISSUE_RESOURCE = 'issue';
|
|
15
|
+
const INSTALLATION_RESOURCE = 'installation';
|
|
16
|
+
const SENTRY_PROVIDER = 'sentry';
|
|
17
|
+
export function createSentryWebhookProcessor(context) {
|
|
18
|
+
return {
|
|
19
|
+
process: (request)=>processSentryWebhookRequest(context, request)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function processSentryWebhookRequest(context, request) {
|
|
23
|
+
if (request.route_id !== 'sentry') {
|
|
24
|
+
throw new Error(`Sentry processor cannot process ${request.route_id} requests`);
|
|
25
|
+
}
|
|
26
|
+
const deliveryId = request.headers[DELIVERY_ID_HEADER];
|
|
27
|
+
const resource = request.headers[RESOURCE_HEADER];
|
|
28
|
+
const signature = request.headers[SIGNATURE_HEADER] ?? request.headers[LEGACY_SIGNATURE_HEADER];
|
|
29
|
+
if (!deliveryId || !resource || !signature) {
|
|
30
|
+
return {
|
|
31
|
+
outcome: 'discarded',
|
|
32
|
+
reason: 'missing_required_input'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const rawBody = Buffer.from(decodeWebhookBody(request.body));
|
|
36
|
+
if (!verifySentrySignature({
|
|
37
|
+
rawBody,
|
|
38
|
+
signature,
|
|
39
|
+
secret: config.SENTRY_APP_CLIENT_SECRET
|
|
40
|
+
})) {
|
|
41
|
+
return {
|
|
42
|
+
outcome: 'discarded',
|
|
43
|
+
reason: 'invalid_signature',
|
|
44
|
+
deliveryId
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
logger().debug({
|
|
48
|
+
deliveryId,
|
|
49
|
+
signatureHeader: request.headers[SIGNATURE_HEADER] ? SIGNATURE_HEADER : LEGACY_SIGNATURE_HEADER
|
|
50
|
+
}, 'sentry webhook: signature verified');
|
|
51
|
+
if (resource === ISSUE_RESOURCE) return processIssueResource(context, deliveryId, rawBody);
|
|
52
|
+
if (resource === INSTALLATION_RESOURCE) return processInstallationResource(context, deliveryId, rawBody);
|
|
53
|
+
await recordDeliveryOnly(context, deliveryId);
|
|
54
|
+
return {
|
|
55
|
+
outcome: 'discarded',
|
|
56
|
+
reason: 'unsupported_event',
|
|
57
|
+
deliveryId
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function processIssueResource(context, deliveryId, rawBody) {
|
|
61
|
+
const parsed = parsePayload(sentryIssueWebhookSchema, rawBody, deliveryId, normalizeSentryIssueAction);
|
|
62
|
+
if (!parsed.success) {
|
|
63
|
+
await recordDeliveryOnly(context, deliveryId);
|
|
64
|
+
return {
|
|
65
|
+
outcome: 'discarded',
|
|
66
|
+
reason: parsed.reason,
|
|
67
|
+
deliveryId
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const payload = parsed.data;
|
|
71
|
+
try {
|
|
72
|
+
await context.coreDb().transaction(async (tx)=>{
|
|
73
|
+
await handleSentryIssueEvent({
|
|
74
|
+
tx,
|
|
75
|
+
deliveryId,
|
|
76
|
+
payload,
|
|
77
|
+
publishIntegrationEventReceived: context.publishIntegrationEventReceived,
|
|
78
|
+
getIntegrationConnectionById: context.getIntegrationConnectionById
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (!(error instanceof SentryIssueDroppedError)) throw error;
|
|
83
|
+
logger().warn({
|
|
84
|
+
deliveryId,
|
|
85
|
+
err: error
|
|
86
|
+
}, `sentry webhook: ${error.message}, dropping`);
|
|
87
|
+
await recordDeliveryOnly(context, deliveryId);
|
|
88
|
+
return {
|
|
89
|
+
outcome: 'discarded',
|
|
90
|
+
reason: 'connection_unavailable',
|
|
91
|
+
deliveryId
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
outcome: 'processed',
|
|
96
|
+
deliveryId
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function processInstallationResource(context, deliveryId, rawBody) {
|
|
100
|
+
const parsed = parsePayload(sentryInstallationWebhookSchema, rawBody, deliveryId);
|
|
101
|
+
if (!parsed.success) {
|
|
102
|
+
await recordDeliveryOnly(context, deliveryId);
|
|
103
|
+
return {
|
|
104
|
+
outcome: 'discarded',
|
|
105
|
+
reason: parsed.reason,
|
|
106
|
+
deliveryId
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const payload = parsed.data;
|
|
110
|
+
const installation = payload.data.installation;
|
|
111
|
+
if (payload.action === 'deleted') {
|
|
112
|
+
await context.coreDb().transaction(async (tx)=>{
|
|
113
|
+
await handleSentryInstallationDeleted({
|
|
114
|
+
tx,
|
|
115
|
+
deliveryId,
|
|
116
|
+
installationUuid: installation.uuid,
|
|
117
|
+
recordDeliveryOnly: context.recordDeliveryOnly,
|
|
118
|
+
updateConnectionLifecycleStatus: context.updateConnectionLifecycleStatus
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
outcome: 'processed',
|
|
123
|
+
deliveryId
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
await handleSentryInstallationCreated({
|
|
127
|
+
deliveryId,
|
|
128
|
+
installationUuid: installation.uuid,
|
|
129
|
+
orgSlug: installation.organization?.slug,
|
|
130
|
+
code: installation.code,
|
|
131
|
+
sentry: context.sentry,
|
|
132
|
+
verifyInstall: config.SENTRY_APP_VERIFY_INSTALL,
|
|
133
|
+
getSentryInstallation: ({ installationUuid })=>getSentryInstallationByInstallationUuid(installationUuid),
|
|
134
|
+
persistUnclaimedAndRecordDelivery: ({ installationUuid, codeHash, deliveryId: id })=>context.coreDb().transaction(async (tx)=>{
|
|
135
|
+
const completed = await completeSentryInstallationVerification({
|
|
136
|
+
installationUuid,
|
|
137
|
+
codeHash
|
|
138
|
+
}, {
|
|
139
|
+
tx
|
|
140
|
+
});
|
|
141
|
+
await context.recordDeliveryOnly({
|
|
142
|
+
tx,
|
|
143
|
+
provider: SENTRY_PROVIDER,
|
|
144
|
+
deliveryId: id
|
|
145
|
+
});
|
|
146
|
+
if (completed) return completed;
|
|
147
|
+
const current = await getSentryInstallationByInstallationUuid(installationUuid, {
|
|
148
|
+
tx
|
|
149
|
+
});
|
|
150
|
+
if (!current) throw new Error('Sentry installation verification lost its claim');
|
|
151
|
+
return current;
|
|
152
|
+
}),
|
|
153
|
+
recordDelivery: (id)=>recordDeliveryOnly(context, id)
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
outcome: 'processed',
|
|
157
|
+
deliveryId
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function parsePayload(schema, rawBody, deliveryId, normalize = (payload)=>payload) {
|
|
161
|
+
let parsedJson;
|
|
162
|
+
try {
|
|
163
|
+
parsedJson = JSON.parse(Buffer.from(rawBody).toString('utf8'));
|
|
164
|
+
} catch (error) {
|
|
165
|
+
logger().warn({
|
|
166
|
+
deliveryId,
|
|
167
|
+
err: error
|
|
168
|
+
}, 'sentry webhook: payload JSON parse failed, dropping');
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
reason: 'malformed_payload'
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const payload = schema.safeParse(normalize(parsedJson));
|
|
175
|
+
if (payload.success) return {
|
|
176
|
+
success: true,
|
|
177
|
+
data: payload.data
|
|
178
|
+
};
|
|
179
|
+
logger().warn({
|
|
180
|
+
deliveryId,
|
|
181
|
+
issues: payload.error
|
|
182
|
+
}, 'sentry webhook: payload failed validation, dropping');
|
|
183
|
+
return {
|
|
184
|
+
success: false,
|
|
185
|
+
reason: 'unsupported_event'
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async function recordDeliveryOnly(context, deliveryId) {
|
|
189
|
+
await context.coreDb().transaction(async (tx)=>{
|
|
190
|
+
await context.recordDeliveryOnly({
|
|
191
|
+
tx,
|
|
192
|
+
provider: SENTRY_PROVIDER,
|
|
193
|
+
deliveryId
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
//# sourceMappingURL=webhook-processor.js.map
|
|
@@ -0,0 +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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"}
|
package/dist/core/webhook.d.ts
CHANGED
|
@@ -31,10 +31,10 @@ export interface HandleSentryInstallationCreatedParams {
|
|
|
31
31
|
/**
|
|
32
32
|
* The authoritative `installation.created` path. Whichever of webhook or browser
|
|
33
33
|
* arrives first exchanges the single-use code and persists a verified-unclaimed
|
|
34
|
-
* row; the other reconciles.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
34
|
+
* row; the other reconciles. The webhook writes a pending claim before the
|
|
35
|
+
* exchange and a durable checkpoint after it succeeds. Only that checkpoint can
|
|
36
|
+
* bypass the single-use exchange on retry. The installed transition and delivery
|
|
37
|
+
* record commit in one transaction. Never logs the raw code.
|
|
38
38
|
*/
|
|
39
39
|
export declare function handleSentryInstallationCreated(params: HandleSentryInstallationCreatedParams): Promise<void>;
|
|
40
40
|
export interface HandleSentryInstallationDeletedParams {
|
|
@@ -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,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,qCAAqC,CAAC;AAE/E,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AASpD,OAAO,
|
|
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,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,qCAAqC,CAAC;AAE/E,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"}
|