@shipfox/api-integration-sentry 5.0.0 → 7.1.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.
Files changed (84) hide show
  1. package/.turbo/turbo-build.log +12 -15
  2. package/CHANGELOG.md +42 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +4 -5
  5. package/dist/config.js.map +1 -1
  6. package/dist/core/install.d.ts +6 -0
  7. package/dist/core/install.d.ts.map +1 -1
  8. package/dist/core/install.js +8 -5
  9. package/dist/core/install.js.map +1 -1
  10. package/dist/core/signature.d.ts +1 -1
  11. package/dist/core/signature.d.ts.map +1 -1
  12. package/dist/core/signature.js.map +1 -1
  13. package/dist/core/webhook-processor.d.ts +16 -0
  14. package/dist/core/webhook-processor.d.ts.map +1 -0
  15. package/dist/core/webhook-processor.js +198 -0
  16. package/dist/core/webhook-processor.js.map +1 -0
  17. package/dist/core/webhook.d.ts +4 -4
  18. package/dist/core/webhook.d.ts.map +1 -1
  19. package/dist/core/webhook.js +63 -49
  20. package/dist/core/webhook.js.map +1 -1
  21. package/dist/db/db.d.ts +8 -4
  22. package/dist/db/db.d.ts.map +1 -1
  23. package/dist/db/installations.d.ts +35 -13
  24. package/dist/db/installations.d.ts.map +1 -1
  25. package/dist/db/installations.js +89 -31
  26. package/dist/db/installations.js.map +1 -1
  27. package/dist/db/schema/installations.d.ts +5 -3
  28. package/dist/db/schema/installations.d.ts.map +1 -1
  29. package/dist/db/schema/installations.js +6 -6
  30. package/dist/db/schema/installations.js.map +1 -1
  31. package/dist/index.d.ts +6 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +20 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/presentation/routes/webhooks.d.ts +4 -1
  36. package/dist/presentation/routes/webhooks.d.ts.map +1 -1
  37. package/dist/presentation/routes/webhooks.js +69 -43
  38. package/dist/presentation/routes/webhooks.js.map +1 -1
  39. package/dist/temporal/activities/prune-unclaimed-installations.d.ts +1 -0
  40. package/dist/temporal/activities/prune-unclaimed-installations.d.ts.map +1 -1
  41. package/dist/temporal/activities/prune-unclaimed-installations.js.map +1 -1
  42. package/dist/temporal/workflows/index.bundle.js +17169 -3306
  43. package/dist/temporal/workflows/prune-unclaimed-installations-cron.d.ts.map +1 -1
  44. package/dist/temporal/workflows/prune-unclaimed-installations-cron.js +6 -1
  45. package/dist/temporal/workflows/prune-unclaimed-installations-cron.js.map +1 -1
  46. package/dist/tsconfig.test.tsbuildinfo +1 -1
  47. package/package.json +9 -21
  48. package/src/config.ts +4 -5
  49. package/src/core/install.ts +21 -7
  50. package/src/core/signature.ts +1 -1
  51. package/src/core/webhook-processor.test.ts +304 -0
  52. package/src/core/webhook-processor.ts +224 -0
  53. package/src/core/webhook.ts +74 -53
  54. package/src/db/installations.test.ts +163 -17
  55. package/src/db/installations.ts +136 -35
  56. package/src/db/schema/installations.ts +7 -7
  57. package/src/index.test.ts +4 -1
  58. package/src/index.ts +16 -0
  59. package/src/presentation/routes/webhooks.test.ts +6 -5
  60. package/src/presentation/routes/webhooks.ts +88 -29
  61. package/src/temporal/activities/prune-unclaimed-installations.test.ts +24 -2
  62. package/src/temporal/activities/prune-unclaimed-installations.ts +4 -1
  63. package/src/temporal/workflows/prune-unclaimed-installations-cron.ts +4 -1
  64. package/tsconfig.build.tsbuildinfo +1 -1
  65. package/dist/presentation/routes/installation-handler.d.ts +0 -9
  66. package/dist/presentation/routes/installation-handler.d.ts.map +0 -1
  67. package/dist/presentation/routes/installation-handler.js +0 -70
  68. package/dist/presentation/routes/installation-handler.js.map +0 -1
  69. package/dist/presentation/routes/issue-handler.d.ts +0 -9
  70. package/dist/presentation/routes/issue-handler.d.ts.map +0 -1
  71. package/dist/presentation/routes/issue-handler.js +0 -50
  72. package/dist/presentation/routes/issue-handler.js.map +0 -1
  73. package/dist/presentation/routes/webhook-delivery.d.ts +0 -28
  74. package/dist/presentation/routes/webhook-delivery.d.ts.map +0 -1
  75. package/dist/presentation/routes/webhook-delivery.js +0 -56
  76. package/dist/presentation/routes/webhook-delivery.js.map +0 -1
  77. package/dist/presentation/routes/webhook-request.d.ts +0 -10
  78. package/dist/presentation/routes/webhook-request.d.ts.map +0 -1
  79. package/dist/presentation/routes/webhook-request.js +0 -56
  80. package/dist/presentation/routes/webhook-request.js.map +0 -1
  81. package/src/presentation/routes/installation-handler.ts +0 -76
  82. package/src/presentation/routes/issue-handler.ts +0 -52
  83. package/src/presentation/routes/webhook-delivery.ts +0 -61
  84. package/src/presentation/routes/webhook-request.ts +0 -66
@@ -1,20 +1,17 @@
1
1
  $ shipfox-swc && shipfox-temporal-bundle dist/temporal/workflows/index.js
2
- Successfully compiled: 26 files with swc (187.64ms)
3
- 2026-07-19T19:41:11.653Z [INFO] asset workflow-bundle-2feea38d2d234342d1e2.js 1.45 MiB [emitted] [immutable] (name: main)
4
- runtime modules 670 bytes 3 modules
5
- modules by path ../../../../node_modules/.pnpm/ 424 KiB
6
- modules by path ../../../../node_modules/.pnpm/@temporalio+common@1.18.1/node_modules/@temporal...(truncated) 155 KiB 34 modules
7
- modules by path ../../../../node_modules/.pnpm/@temporalio+workflow@1.18.1/node_modules/@tempor...(truncated) 201 KiB 26 modules
8
- modules by path ../../../../node_modules/.pnpm/nexus-rpc@0.0.2/node_modules/nexus-rpc/lib/ 22.3 KiB 13 modules
9
- ../../../../node_modules/.pnpm/long@5.3.2/node_modules/long/index.js 43.2 KiB [built] [code generated]
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.07 KiB
12
- ./dist/temporal/workflows/index-autogenerated-entrypoint.cjs 537 bytes [built] [code generated]
2
+ Successfully compiled: 23 files with swc (535.59ms)
3
+ 2026-07-22T13:11:18.120Z [INFO] asset workflow-bundle-46314ad53629a6893a6d.js 3.17 MiB [emitted] [immutable] (name: main)
4
+ orphan modules 33.5 KiB [orphan] 21 modules
5
+ runtime modules 1.13 KiB 5 modules
6
+ modules by path ../../../../node_modules/.pnpm/ 970 KiB 196 modules
7
+ modules by path ./dist/temporal/workflows/ 1.62 KiB
8
+ ./dist/temporal/workflows/index-autogenerated-entrypoint.cjs 943 bytes [built] [code generated]
13
9
  ./dist/temporal/workflows/index.js 98 bytes [built] [code generated]
14
- ./dist/temporal/workflows/prune-unclaimed-installations-cron.js 456 bytes [built] [code generated]
10
+ ./dist/temporal/workflows/prune-unclaimed-installations-cron.js 622 bytes [built] [code generated]
15
11
  optional modules 30 bytes [optional]
16
12
  __temporal_custom_payload_converter (ignored) 15 bytes [optional] [built] [code generated]
17
13
  __temporal_custom_failure_converter (ignored) 15 bytes [optional] [built] [code generated]
18
- webpack 5.107.2 compiled successfully in 2811 ms
19
- 2026-07-19T19:41:11.662Z [INFO] Workflow bundle created { size: '1.45MB' }
20
- /home/runner/work/shipfox/shipfox/libs/api/integration/sentry/dist/temporal/workflows/index.js: 1516523 bytes
14
+ ../../../shared/node/temporal/dist/workflow-error-interceptor.js 1.21 KiB [built] [code generated]
15
+ webpack 5.107.2 compiled successfully in 9053 ms
16
+ 2026-07-22T13:11:18.142Z [INFO] Workflow bundle created { size: '3.17MB' }
17
+ /home/runner/work/shipfox/shipfox/libs/api/integration/sentry/dist/temporal/workflows/index.js: 3323961 bytes
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # @shipfox/api-integration-sentry
2
2
 
3
+ ## 7.1.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [ac42c96]
8
+ - Updated dependencies [6ce08c0]
9
+ - @shipfox/node-fastify@0.3.0
10
+ - @shipfox/node-module@0.5.0
11
+ - @shipfox/node-opentelemetry@0.6.0
12
+ - @shipfox/api-auth-context@7.1.0
13
+
14
+ ## 6.0.0
15
+
16
+ ### Minor Changes
17
+
18
+ - f262539: Adds a composed webhook processor and optional provider-neutral delivery source for hosted API runtimes.
19
+ - 8390468: Adds a shared Sentry webhook processor for direct and durable delivery adapters.
20
+
21
+ ### Patch Changes
22
+
23
+ - f73da5d: Enforces bounded API context imports and routes inter-module consumers through producer contracts.
24
+ - 0649d62: Keeps GitHub credential cleanup and Sentry installation lifecycle handling safe across duplicate, concurrent, and reordered webhook delivery.
25
+ - Updated dependencies [0bb82a4]
26
+ - Updated dependencies [54ce48b]
27
+ - Updated dependencies [c0162b0]
28
+ - Updated dependencies [7ac43a4]
29
+ - Updated dependencies [f262539]
30
+ - Updated dependencies [a01e917]
31
+ - Updated dependencies [3bb4e26]
32
+ - Updated dependencies [8bdc149]
33
+ - Updated dependencies [3810996]
34
+ - Updated dependencies [b00ed29]
35
+ - Updated dependencies [8aa7cd3]
36
+ - Updated dependencies [81f9544]
37
+ - Updated dependencies [4604a06]
38
+ - @shipfox/api-integration-core-dto@6.0.0
39
+ - @shipfox/node-module@0.4.0
40
+ - @shipfox/node-drizzle@0.3.2
41
+ - @shipfox/api-auth-context@6.0.0
42
+ - @shipfox/node-fastify@0.2.4
43
+ - @shipfox/api-integration-sentry-dto@6.0.0
44
+
3
45
  ## 5.0.0
4
46
 
5
47
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;;;;;;kDAkBjB,CAAC;AAMH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAMlE"}
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 a verified-but-unclaimed Sentry installation may sit before the daily cleanup cron tombstones it. Must be at least 1; a smaller value moves the cutoff to now or the future and would tombstone freshly created, still-unclaimed installs. A reinstall always mints a fresh uuid, so a tombstone is never revived. Defaults to 7.',
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
- // The daily cleanup cron tombstones every unclaimed install older than
22
- // `now - SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS`. A value below 1 moves that
23
- // cutoff to now or the future, which would wipe freshly created installs nobody has
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}.`);
@@ -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 a verified-but-unclaimed Sentry installation may sit before the daily cleanup cron tombstones it. Must be at least 1; a smaller value moves the cutoff to now or the future and would tombstone freshly created, still-unclaimed installs. A reinstall always mints a fresh uuid, so a tombstone is never revived. Defaults to 7.',\n default: 7,\n }),\n});\n\n// The daily cleanup cron tombstones every unclaimed install older than\n// `now - SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS`. A value below 1 moves that\n// cutoff to now or the future, which would wipe freshly created installs nobody has\n// claimed yet. Reject it at startup rather than let the 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,uEAAuE;AACvE,mFAAmF;AACnF,oFAAoF;AACpF,kFAAkF;AAClF,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"}
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"}
@@ -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,CAc1C"}
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"}
@@ -37,7 +37,7 @@ function codeHashesEqual(presented, stored) {
37
37
  codeHash: hashAuthorizationCode(params.code)
38
38
  });
39
39
  if (params.verifyInstall) {
40
- await bestEffortVerifyInstallation({
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 bestEffortVerifyInstallation({
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 bestEffortVerifyInstallation({
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 bestEffortVerifyInstallation(input) {
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.
@@ -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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"}
@@ -1,5 +1,5 @@
1
1
  export interface VerifySentrySignatureParams {
2
- rawBody: string;
2
+ rawBody: string | Uint8Array;
3
3
  signature: string;
4
4
  secret: string;
5
5
  }
@@ -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;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,2BAA2B,GAAG,OAAO,CAElF"}
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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"}
@@ -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. Idempotent on the installation uuid. The exchange
35
- * runs outside any DB transaction; a short transaction wraps only persist +
36
- * delivery record. On an exchange failure the delivery is record-and-dropped
37
- * (204) the browser may still win, or the code expired. Never logs the raw code.
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,EAGL,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,CAgEf;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;AAMD,wBAAsB,+BAA+B,CACnD,MAAM,EAAE,qCAAqC,GAC5C,OAAO,CAAC,IAAI,CAAC,CAiBf;AAID,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,OAAO,GAAG,OAAO,CAOvE"}
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"}