@shipfox/api-integration-sentry 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +192 -0
  5. package/LICENSE +21 -0
  6. package/README.md +145 -0
  7. package/dist/api/client.d.ts +24 -0
  8. package/dist/api/client.d.ts.map +1 -0
  9. package/dist/api/client.js +101 -0
  10. package/dist/api/client.js.map +1 -0
  11. package/dist/config.d.ts +9 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +32 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/core/errors.d.ts +57 -0
  16. package/dist/core/errors.d.ts.map +1 -0
  17. package/dist/core/errors.js +67 -0
  18. package/dist/core/errors.js.map +1 -0
  19. package/dist/core/install.d.ts +63 -0
  20. package/dist/core/install.d.ts.map +1 -0
  21. package/dist/core/install.js +209 -0
  22. package/dist/core/install.js.map +1 -0
  23. package/dist/core/signature.d.ts +7 -0
  24. package/dist/core/signature.d.ts.map +1 -0
  25. package/dist/core/signature.js +6 -0
  26. package/dist/core/signature.js.map +1 -0
  27. package/dist/core/webhook.d.ts +49 -0
  28. package/dist/core/webhook.d.ts.map +1 -0
  29. package/dist/core/webhook.js +175 -0
  30. package/dist/core/webhook.js.map +1 -0
  31. package/dist/db/db.d.ts +327 -0
  32. package/dist/db/db.d.ts.map +1 -0
  33. package/dist/db/db.js +18 -0
  34. package/dist/db/db.js.map +1 -0
  35. package/dist/db/installations.d.ts +74 -0
  36. package/dist/db/installations.d.ts.map +1 -0
  37. package/dist/db/installations.js +120 -0
  38. package/dist/db/installations.js.map +1 -0
  39. package/dist/db/migrations.d.ts +2 -0
  40. package/dist/db/migrations.d.ts.map +1 -0
  41. package/dist/db/migrations.js +5 -0
  42. package/dist/db/migrations.js.map +1 -0
  43. package/dist/db/schema/common.d.ts +2 -0
  44. package/dist/db/schema/common.d.ts.map +1 -0
  45. package/dist/db/schema/common.js +4 -0
  46. package/dist/db/schema/common.js.map +1 -0
  47. package/dist/db/schema/installations.d.ts +165 -0
  48. package/dist/db/schema/installations.d.ts.map +1 -0
  49. package/dist/db/schema/installations.js +42 -0
  50. package/dist/db/schema/installations.js.map +1 -0
  51. package/dist/index.d.ts +36 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +64 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/presentation/dto/integrations.d.ts +15 -0
  56. package/dist/presentation/dto/integrations.d.ts.map +1 -0
  57. package/dist/presentation/dto/integrations.js +9 -0
  58. package/dist/presentation/dto/integrations.js.map +1 -0
  59. package/dist/presentation/routes/errors.d.ts +2 -0
  60. package/dist/presentation/routes/errors.d.ts.map +1 -0
  61. package/dist/presentation/routes/errors.js +51 -0
  62. package/dist/presentation/routes/errors.js.map +1 -0
  63. package/dist/presentation/routes/install.d.ts +16 -0
  64. package/dist/presentation/routes/install.d.ts.map +1 -0
  65. package/dist/presentation/routes/install.js +76 -0
  66. package/dist/presentation/routes/install.js.map +1 -0
  67. package/dist/presentation/routes/installation-handler.d.ts +9 -0
  68. package/dist/presentation/routes/installation-handler.d.ts.map +1 -0
  69. package/dist/presentation/routes/installation-handler.js +70 -0
  70. package/dist/presentation/routes/installation-handler.js.map +1 -0
  71. package/dist/presentation/routes/issue-handler.d.ts +9 -0
  72. package/dist/presentation/routes/issue-handler.d.ts.map +1 -0
  73. package/dist/presentation/routes/issue-handler.js +50 -0
  74. package/dist/presentation/routes/issue-handler.js.map +1 -0
  75. package/dist/presentation/routes/webhook-context.d.ts +20 -0
  76. package/dist/presentation/routes/webhook-context.d.ts.map +1 -0
  77. package/dist/presentation/routes/webhook-context.js +10 -0
  78. package/dist/presentation/routes/webhook-context.js.map +1 -0
  79. package/dist/presentation/routes/webhook-delivery.d.ts +28 -0
  80. package/dist/presentation/routes/webhook-delivery.d.ts.map +1 -0
  81. package/dist/presentation/routes/webhook-delivery.js +56 -0
  82. package/dist/presentation/routes/webhook-delivery.js.map +1 -0
  83. package/dist/presentation/routes/webhook-request.d.ts +10 -0
  84. package/dist/presentation/routes/webhook-request.d.ts.map +1 -0
  85. package/dist/presentation/routes/webhook-request.js +56 -0
  86. package/dist/presentation/routes/webhook-request.js.map +1 -0
  87. package/dist/presentation/routes/webhooks.d.ts +5 -0
  88. package/dist/presentation/routes/webhooks.d.ts.map +1 -0
  89. package/dist/presentation/routes/webhooks.js +71 -0
  90. package/dist/presentation/routes/webhooks.js.map +1 -0
  91. package/dist/temporal/activities/index.d.ts +5 -0
  92. package/dist/temporal/activities/index.d.ts.map +1 -0
  93. package/dist/temporal/activities/index.js +8 -0
  94. package/dist/temporal/activities/index.js.map +1 -0
  95. package/dist/temporal/activities/prune-unclaimed-installations.d.ts +4 -0
  96. package/dist/temporal/activities/prune-unclaimed-installations.d.ts.map +1 -0
  97. package/dist/temporal/activities/prune-unclaimed-installations.js +11 -0
  98. package/dist/temporal/activities/prune-unclaimed-installations.js.map +1 -0
  99. package/dist/temporal/constants.d.ts +2 -0
  100. package/dist/temporal/constants.d.ts.map +1 -0
  101. package/dist/temporal/constants.js +3 -0
  102. package/dist/temporal/constants.js.map +1 -0
  103. package/dist/temporal/workflows/index.d.ts +2 -0
  104. package/dist/temporal/workflows/index.d.ts.map +1 -0
  105. package/dist/temporal/workflows/index.js +3 -0
  106. package/dist/temporal/workflows/index.js.map +1 -0
  107. package/dist/temporal/workflows/prune-unclaimed-installations-cron.d.ts +2 -0
  108. package/dist/temporal/workflows/prune-unclaimed-installations-cron.d.ts.map +1 -0
  109. package/dist/temporal/workflows/prune-unclaimed-installations-cron.js +14 -0
  110. package/dist/temporal/workflows/prune-unclaimed-installations-cron.js.map +1 -0
  111. package/dist/tsconfig.test.tsbuildinfo +1 -0
  112. package/drizzle/0000_initial.sql +14 -0
  113. package/drizzle/meta/_journal.json +13 -0
  114. package/drizzle.config.ts +7 -0
  115. package/package.json +70 -0
  116. package/src/api/client.test.ts +147 -0
  117. package/src/api/client.ts +147 -0
  118. package/src/config.test.ts +16 -0
  119. package/src/config.ts +35 -0
  120. package/src/connection-external-url.test.ts +79 -0
  121. package/src/core/errors.ts +81 -0
  122. package/src/core/install.test.ts +343 -0
  123. package/src/core/install.ts +305 -0
  124. package/src/core/signature.test.ts +50 -0
  125. package/src/core/signature.ts +11 -0
  126. package/src/core/webhook.test.ts +118 -0
  127. package/src/core/webhook.ts +246 -0
  128. package/src/db/db.ts +18 -0
  129. package/src/db/installations.test.ts +224 -0
  130. package/src/db/installations.ts +207 -0
  131. package/src/db/migrations.ts +4 -0
  132. package/src/db/schema/common.ts +3 -0
  133. package/src/db/schema/installations.ts +46 -0
  134. package/src/index.test.ts +18 -0
  135. package/src/index.ts +127 -0
  136. package/src/presentation/dto/integrations.ts +9 -0
  137. package/src/presentation/routes/errors.ts +48 -0
  138. package/src/presentation/routes/install.test.ts +325 -0
  139. package/src/presentation/routes/install.ts +102 -0
  140. package/src/presentation/routes/installation-handler.ts +76 -0
  141. package/src/presentation/routes/issue-handler.ts +52 -0
  142. package/src/presentation/routes/webhook-context.ts +25 -0
  143. package/src/presentation/routes/webhook-delivery.ts +61 -0
  144. package/src/presentation/routes/webhook-request.ts +66 -0
  145. package/src/presentation/routes/webhooks.test.ts +773 -0
  146. package/src/presentation/routes/webhooks.ts +59 -0
  147. package/src/temporal/activities/index.ts +7 -0
  148. package/src/temporal/activities/prune-unclaimed-installations.test.ts +52 -0
  149. package/src/temporal/activities/prune-unclaimed-installations.ts +11 -0
  150. package/src/temporal/constants.ts +1 -0
  151. package/src/temporal/workflows/index.ts +1 -0
  152. package/src/temporal/workflows/prune-unclaimed-installations-cron.ts +15 -0
  153. package/test/env.ts +11 -0
  154. package/test/factories/index.ts +9 -0
  155. package/test/factories/sentry-installation-webhook.ts +35 -0
  156. package/test/factories/sentry-installation.ts +44 -0
  157. package/test/factories/sentry-issue-webhook.ts +34 -0
  158. package/test/globalSetup.ts +14 -0
  159. package/test/index.ts +9 -0
  160. package/test/setup.ts +17 -0
  161. package/tsconfig.build.json +9 -0
  162. package/tsconfig.build.tsbuildinfo +1 -0
  163. package/tsconfig.json +3 -0
  164. package/tsconfig.test.json +9 -0
  165. package/vitest.config.ts +12 -0
@@ -0,0 +1,2 @@
1
+ $ shipfox-swc
2
+ Successfully compiled: 26 files with swc (156.42ms)
@@ -0,0 +1 @@
1
+ $ shipfox-tsc-emit
@@ -0,0 +1 @@
1
+ $ shipfox-tsc-check
package/CHANGELOG.md ADDED
@@ -0,0 +1,192 @@
1
+ # @shipfox/api-integration-sentry
2
+
3
+ ## 2.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1b0d344: Publishes the complete API runtime closure with packed-consumer-safe internal imports and records its exact package set in application releases.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [0cd6dd4]
12
+ - Updated dependencies [1b0d344]
13
+ - Updated dependencies [521e006]
14
+ - @shipfox/node-module@0.2.0
15
+ - @shipfox/api-auth-context@2.0.0
16
+ - @shipfox/api-integration-core-dto@2.0.0
17
+ - @shipfox/api-integration-sentry-dto@2.0.0
18
+ - @shipfox/config@1.2.1
19
+ - @shipfox/node-drizzle@0.2.1
20
+ - @shipfox/node-fastify@0.2.1
21
+ - @shipfox/node-opentelemetry@0.5.0
22
+ - @shipfox/node-postgres@0.4.1
23
+
24
+ ## 0.1.2
25
+
26
+ ### Patch Changes
27
+
28
+ - @shipfox/node-module@0.1.2
29
+
30
+ ## 0.1.1
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies [ec75cd5]
35
+ - Updated dependencies [6a1fb54]
36
+ - @shipfox/node-drizzle@0.2.0
37
+ - @shipfox/node-postgres@0.4.0
38
+ - @shipfox/node-module@0.1.1
39
+
40
+ ## 0.1.0
41
+
42
+ ### Minor Changes
43
+
44
+ - d245be8: [api/integrations] Make the signed Sentry installation webhook authoritative.
45
+ - `@shipfox/api-integration-sentry-dto`: reshape `sentryInstallationWebhookSchema`
46
+ to read `data.installation.{uuid, organization.slug, status, code}` plus an
47
+ optional top-level `actor`. Only consumed fields are validated and the raw
48
+ `code` is never logged.
49
+ - `@shipfox/api-integration-sentry`: the signed `installation.created` webhook now
50
+ exchanges the single-use code and persists a verified-but-unclaimed installation
51
+ (`connection_id IS NULL`, `code_hash = sha256(code)`). The browser flow narrows
52
+ to a claim that binds a verified install to a workspace under unified claim auth
53
+ (exchange-success, same-code hash match, or a retryable
54
+ `verification-in-progress` while a concurrent webhook is mid-exchange), with a
55
+ proof-mismatch 403 closing the bare-uuid IDOR. The exchange/verify run outside
56
+ the DB transaction; a short transaction wraps persist + delivery record. Adds
57
+ `connection_id` nullable + `code_hash` to the installations table and a daily TTL
58
+ cron that tombstones never-claimed installs.
59
+ - `@shipfox/api-integration-core`: inject the Sentry client into the webhook
60
+ context, resolve a null `connection_id` to "no connection" for pre-claim issue
61
+ deliveries, and register the unclaimed-installation cleanup cron when Sentry is
62
+ enabled.
63
+ - `@shipfox/client-integrations`: treat the retryable `verification-in-progress`
64
+ response as a backoff-eligible failure on the connect callback.
65
+
66
+ - b8e49ff: Add the client-side Sentry install/connect flow and a workspace settings
67
+ integrations hub.
68
+ - `@shipfox/client-integrations`: shared `IntegrationGallerySection` (capability
69
+ filter, lifecycle pills, "Added" date, external link, connected-first
70
+ ordering, degraded status mode), shared `RedirectInstallPage` powering the
71
+ GitHub and new Sentry install pages, `SentryCallbackPage` with an explicit
72
+ workspace confirm (sessionStorage only pre-selects), two-tier retry, and the
73
+ Sentry hooks (`useCreateSentryInstallMutation`, `connectSentry`,
74
+ `useIntegrationConnectionsQuery`).
75
+ - `@shipfox/client-workspace-settings`: new `/workspaces/$wid/settings/integrations`
76
+ page and an Integrations entry in the settings nav.
77
+ - `@shipfox/client-router`: routes for the Sentry install page, the root-level
78
+ Sentry callback, and the settings integrations page.
79
+ - `@shipfox/react-ui`: `sentry` icon (monochrome, theme-aware).
80
+ - `@shipfox/api-integration-core-dto`: optional `external_url` on the connection
81
+ DTO and an optional `connectionExternalUrl` method on `IntegrationProvider`.
82
+ - `@shipfox/api-integration-core`: `GET /integration-connections` now returns
83
+ connections of every lifecycle status (the active-only filter prevented
84
+ clients from surfacing disabled/error state) and resolves `external_url`
85
+ per connection best-effort.
86
+ - `@shipfox/api-integration-sentry` / `@shipfox/api-integration-github`:
87
+ implement `connectionExternalUrl` (Sentry org URL via a new
88
+ by-connection-id installation lookup; GitHub installation settings URL).
89
+
90
+ - 5b8ed32: Add the Sentry install/connect flow so a workspace member can link a Sentry
91
+ installation to a Shipfox workspace, writing the connection + installation rows
92
+ that the webhook receiver looks up.
93
+ - `@shipfox/api-integration-sentry`: add a stateless `SentryApiClient`
94
+ (authorization-code exchange, org-slug derivation, optional verify-install),
95
+ `handleSentryConnect`, and two authenticated routes —
96
+ `POST /integrations/sentry/install` (returns the external-install URL) and
97
+ `POST /integrations/sentry/connect` (links the installation). Sentry has no
98
+ `state` param, so the workspace is taken from the request body and authorized
99
+ against the live session; the org slug is derived from Sentry, never trusted
100
+ from the body. The verify-install side effect runs after the row is persisted,
101
+ and no Sentry token is stored. Provider HTTP errors map to a typed
102
+ `SentryIntegrationProviderError` and never carry the token, code, or client
103
+ secret.
104
+ - `@shipfox/api-integration-sentry-dto`: add the install/connect request and
105
+ response schemas.
106
+ - `@shipfox/api-integration-core`: wire the `getExistingSentryConnection` and
107
+ `connectSentryInstallation` closures into the Sentry provider (internal
108
+ wiring, no public API change).
109
+
110
+ A concurrent same-install connect race (shared with GitHub) is tracked
111
+ separately in ENG-409.
112
+
113
+ - d6d4862: Add a Sentry integration provider that ingests issue webhooks.
114
+ - New `@shipfox/api-integration-sentry` + `-sentry-dto` packages: a webhook
115
+ receiver that verifies the HMAC-SHA256 signature (keyed with the app client
116
+ secret), dedups on `Request-ID`, normalizes the issue payload, and publishes
117
+ `integrations.event.received` with `source: 'sentry'` and `event:
118
+ issue.<action>`. A raw `ignored` action is normalized to `archived`. Malformed,
119
+ bad-JSON, unknown-action, and unknown-resource deliveries are recorded-and-dropped
120
+ with a 204 (deliberate deviation from GitHub's 400 to avoid Sentry disabling the
121
+ webhook).
122
+ - `@shipfox/api-integration-core-dto`: add the `SentryIssuePayload` contract.
123
+ - `@shipfox/api-integration-core`: register the Sentry provider behind
124
+ `INTEGRATIONS_ENABLE_SENTRY_PROVIDER`, add the
125
+ `updateIntegrationConnectionLifecycleStatus` helper, and pin stable
126
+ migration-tracking table names per provider database.
127
+ - `@shipfox/node-fastify`: add the shared `rawBodyPlugin` and `WEBHOOK_BODY_LIMIT`
128
+ exports for webhook receivers.
129
+ - `@shipfox/node-module`: add an optional `migrationsTableName` to `ModuleDatabase`
130
+ so conditionally-composed databases get a position-independent migration table.
131
+ - `@shipfox/api-integration-github`: consume the shared `rawBodyPlugin` instead of
132
+ a local copy (internal refactor, no behavior change).
133
+
134
+ Deploy note: environments with GitHub enabled must rename the existing
135
+ `__drizzle_migrations_integrations_1` table to
136
+ `__drizzle_migrations_integrations_github` as part of this release, or GitHub
137
+ migrations re-run against existing tables.
138
+
139
+ ### Patch Changes
140
+
141
+ - 9f1c0ef: Harden integration connect against a concurrent same-install race. The installation upsert now only (re)points an installation at the connection that already owns it: the `onConflictDoUpdate` carries a `setWhere(connection_id = this connection)` predicate and throws `*InstallationAlreadyLinkedError` when the conflicting row belongs to a different connection. Two concurrent connects of the same provider install to different workspaces no longer leave one workspace with an active orphan connection while the install's webhooks silently route to the other; the losing transaction rolls back and surfaces a 409.
142
+ - 857fd73: Receive Gitea push webhooks and trigger runs through the existing source-push pipeline.
143
+ - `@shipfox/api-integration-gitea-dto`: `giteaPushPayloadSchema` (`{ref, after, repository: {name, full_name, default_branch, owner: {username}}}`).
144
+ - `@shipfox/api-integration-gitea`: `POST /webhooks/integrations/gitea` (`auth: []`, `rawBodyPlugin`, standard webhook body limit). It verifies the `X-Gitea-Signature` header as hex HMAC-SHA256 of the raw body keyed by `GITEA_WEBHOOK_SECRET` (401 on mismatch), records non-`push` events for delivery dedup and returns 204, and on a `push` resolves the org (`repository.owner.username`, lower-cased to match the stored org) to its connection, drops branch deletions and unknown orgs, and publishes a normalized `SourcePushPayload` (`externalRepositoryId: gitea:<owner>/<repo>` built from `owner.username`/`repository.name` to match the source-control adapter, `refs/heads/` stripped, `headCommitSha: after`, `isDefaultBranch`). `getGiteaConnectionByOrg` gains an optional transaction executor so the lookup runs inside the publishing transaction. The webhook is registered out of band by the Gitea instance admin, so the connect flow registers nothing.
145
+ - `@shipfox/api-integration-core`: wire `publishSourcePush`, `recordDeliveryOnly`, `getIntegrationConnectionById`, and the core database into the Gitea provider, mirroring GitHub.
146
+ - `@shipfox/node-fastify`: add a shared `verifyHexHmacSignature` helper for hex HMAC-SHA256 webhook signatures.
147
+ - `@shipfox/api-integration-sentry`: `verifySentrySignature` now delegates to the shared helper.
148
+
149
+ A push to a connected org's repo creates a workflow run through the existing pipeline. Duplicate POSTs of the same delivery are deduped via `X-Gitea-Delivery`; note that a manual "Redeliver" in Gitea mints a new delivery id and intentionally produces a fresh run (unlike GitHub, whose redelivery reuses the original id and is suppressed).
150
+
151
+ - 417f128: Move the Sentry unclaimed-installation cleanup cron out of `@shipfox/api-integration-core` into `@shipfox/api-integration-sentry`, which now owns its own Temporal maintenance worker. `core` aggregates per-integration workers from enabled providers instead of special-casing Sentry, keeping the integration dependency graph a tree (providers depend only on `*-core-dto`, never on `core`). The unclaimed-installation retention window is now configurable via `SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS` (default 7, minimum 1; startup fails on a smaller value so a misconfiguration cannot tombstone freshly created installs).
152
+ - 82d22e4: Make the workspace-membership gate stateless. `requireWorkspaceAccess` now lives in `@shipfox/api-auth-context` and authorizes a request purely from the verified session-token claims, replacing the `requireMembership` gate in `@shipfox/api-workspaces` that read the workspace row from the database on every workspace-scoped request. Membership and role already travel in the token, so the check needs no database access.
153
+
154
+ This removes the per-request database read and severs the runtime dependency on `@shipfox/api-workspaces` from feature modules that only needed the membership gate (integration providers, secrets, projects, agent, runners). Workspace existence and `active`-status enforcement, which no code path currently exercises, moves off the hot path; enforce it at token issuance when workspace suspension is introduced.
155
+
156
+ - Updated dependencies [34ba284]
157
+ - Updated dependencies [5707d6d]
158
+ - Updated dependencies [b9c3f32]
159
+ - Updated dependencies [a81b68c]
160
+ - Updated dependencies [115655e]
161
+ - Updated dependencies [c0a883c]
162
+ - Updated dependencies [72ce351]
163
+ - Updated dependencies [e47f8da]
164
+ - Updated dependencies [ce062a9]
165
+ - Updated dependencies [7b175f5]
166
+ - Updated dependencies [f3614ae]
167
+ - Updated dependencies [ae7a63c]
168
+ - Updated dependencies [d245be8]
169
+ - Updated dependencies [f92122b]
170
+ - Updated dependencies [f8f339a]
171
+ - Updated dependencies [857fd73]
172
+ - Updated dependencies [75520ff]
173
+ - Updated dependencies [b8e49ff]
174
+ - Updated dependencies [5b8ed32]
175
+ - Updated dependencies [d6d4862]
176
+ - Updated dependencies [c0a883c]
177
+ - Updated dependencies [6077301]
178
+ - Updated dependencies [3bea87f]
179
+ - Updated dependencies [82d22e4]
180
+ - Updated dependencies [01be723]
181
+ - Updated dependencies [2933c33]
182
+ - Updated dependencies [6181819]
183
+ - Updated dependencies [9c149d1]
184
+ - @shipfox/node-fastify@0.2.0
185
+ - @shipfox/node-drizzle@0.1.0
186
+ - @shipfox/api-auth-context@0.1.0
187
+ - @shipfox/api-integration-core-dto@0.1.0
188
+ - @shipfox/node-opentelemetry@0.4.2
189
+ - @shipfox/node-postgres@0.3.2
190
+ - @shipfox/node-module@0.1.0
191
+ - @shipfox/api-integration-sentry-dto@0.1.0
192
+ - @shipfox/config@1.2.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shipfox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # Shipfox API Integration Sentry
2
+
3
+ Shipfox API Integration Sentry receives Sentry App webhooks and publishes
4
+ normalized integration events for downstream modules such as triggers and
5
+ projects. It also exposes the install/connect flow that links a Sentry
6
+ installation to a Shipfox workspace.
7
+
8
+ ## Setup
9
+
10
+ This package is published as part of the API runtime closure. Install it from
11
+ the registry:
12
+
13
+ ```sh
14
+ pnpm add @shipfox/api-integration-sentry
15
+ ```
16
+
17
+ Enable the provider from the integrations core package with:
18
+
19
+ ```sh
20
+ INTEGRATIONS_ENABLE_SENTRY_PROVIDER=true
21
+ ```
22
+
23
+ Configure the Sentry App credentials in the API environment:
24
+
25
+ ```sh
26
+ SENTRY_APP_CLIENT_ID=
27
+ SENTRY_APP_CLIENT_SECRET=
28
+ SENTRY_APP_SLUG=
29
+ SENTRY_APP_VERIFY_INSTALL=true
30
+ SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS=7
31
+ ```
32
+
33
+ `SENTRY_APP_CLIENT_SECRET` is the shared secret used to verify inbound webhooks with HMAC-SHA256 (Sentry signs deliveries with it, the provider verifies them).
34
+
35
+ `SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS` (default 7, minimum 1) is how many days a verified-but-unclaimed installation may sit before the daily cleanup cron tombstones it. Startup fails on a value below 1.
36
+
37
+ ## Webhook Route
38
+
39
+ The provider mounts this unauthenticated webhook route:
40
+
41
+ | Method | Path | Purpose |
42
+ | --- | --- | --- |
43
+ | `POST` | `/webhooks/integrations/sentry` | Receives Sentry App webhooks. |
44
+
45
+ The route verifies the request signature before parsing JSON. It deduplicates
46
+ deliveries by Sentry's `Request-ID` header. Malformed JSON, unknown actions,
47
+ unknown resources, and events for unknown installations are recorded and
48
+ acknowledged with `204` so Sentry does not disable the webhook after repeated
49
+ non-2xx responses.
50
+
51
+ ## Events
52
+
53
+ Issue webhooks publish `INTEGRATION_EVENT_RECEIVED` with the connection slug as `source`.
54
+ Downstream trigger payloads receive the normalized `SentryIssuePayload` in
55
+ `triggerPayload.data`.
56
+
57
+ | Event | Sentry issue action |
58
+ | --- | --- |
59
+ | `issue.created` | `created` |
60
+ | `issue.resolved` | `resolved` |
61
+ | `issue.assigned` | `assigned` |
62
+ | `issue.archived` | `archived` |
63
+ | `issue.unresolved` | `unresolved` |
64
+
65
+ A raw Sentry `ignored` action is normalized to `issue.archived` with
66
+ `payload.action: 'archived'`.
67
+
68
+ ## Install / Connect Routes
69
+
70
+ The provider mounts these authenticated routes (`AUTH_USER` bearer token):
71
+
72
+ | Method | Path | Purpose |
73
+ | --- | --- | --- |
74
+ | `POST` | `/integrations/sentry/install` | Returns the Sentry external-install URL for a workspace. |
75
+ | `POST` | `/integrations/sentry/connect` | Links a Sentry installation to a workspace after the install redirect. |
76
+
77
+ Sentry has no `state` parameter in its install redirect, so the workspace is
78
+ taken from the request body (`workspace_id`) and authorized against the live
79
+ session. `POST /connect` accepts `{workspace_id, code, installation_id}` only —
80
+ the organization slug is derived from Sentry after the code exchange, never
81
+ trusted from the client. The exchanged token is used in-memory for the optional
82
+ verify-install call and then discarded; **no Sentry token is persisted**.
83
+
84
+ ## Sentry App registration
85
+
86
+ Configure the Sentry App (Settings → Developer Settings → your app):
87
+
88
+ - **Redirect URL:** point it at the client callback route
89
+ `<client-origin>/integrations/sentry/callback` (for example
90
+ `https://app.shipfox.io/integrations/sentry/callback`, or
91
+ `http://localhost:5173/integrations/sentry/callback` for local development).
92
+ The page reads `code` and `installationId` from the redirect query, asks the
93
+ user to confirm the target workspace, and calls
94
+ `POST /integrations/sentry/connect` with `{workspace_id, code,
95
+ installation_id}`. A redirect `orgSlug`, if present, is used only as display
96
+ copy on the confirm screen; it is never sent to the API. The server derives
97
+ the authoritative org slug from Sentry after the code exchange (the
98
+ `get-installation` call), so a forged slug cannot influence the stored
99
+ connection. This is per-environment configuration:
100
+ each environment's Sentry App must point at that environment's client
101
+ origin, or installs end on sentry.io with nowhere to return to.
102
+ - **Webhooks:** enable the **Issue** resource so issue webhooks are delivered.
103
+ - **Verify Install:** when enabled, the connect flow issues a
104
+ `PUT /api/0/sentry-app-installations/{installation_id}/` to mark the
105
+ installation installed; mirror the toggle with `SENTRY_APP_VERIFY_INSTALL`.
106
+
107
+ ### Required permissions
108
+
109
+ Set these under the app's **Permissions** tab. The installation token bakes in
110
+ the app's permissions at install time, so **changing a permission requires
111
+ reinstalling the app** before the new scope takes effect.
112
+
113
+ | Permission | Level | Why |
114
+ | --- | --- | --- |
115
+ | **Organization** | Read | `POST /connect` reads the installation to derive the org slug. Without it, the org lookup returns `403 Forbidden` and connect fails with `access-denied` (422). |
116
+ | **Issue & Event** | Read | Required for Sentry to deliver issue webhooks against the installation. |
117
+
118
+ When a Sentry call is rejected, the API logs a `Sentry API request rejected`
119
+ warning with the failing `operation` and the upstream `status` (for example
120
+ `{operation: "get-installation", status: 403}`). The client-facing error stays
121
+ generic to avoid leaking provider internals, so this log is where you confirm a
122
+ missing-permission `403` versus a transient failure.
123
+
124
+ ## Development
125
+
126
+ Run checks for this package:
127
+
128
+ ```sh
129
+ turbo check --filter=@shipfox/api-integration-sentry
130
+ turbo type --filter=@shipfox/api-integration-sentry
131
+ turbo test --filter=@shipfox/api-integration-sentry
132
+ ```
133
+
134
+ Tests use Vitest and a real PostgreSQL database. Start local services before
135
+ running the test suite:
136
+
137
+ ```sh
138
+ docker compose up -d
139
+ ```
140
+
141
+ The test environment uses the `api_test` database, set in `test/env.ts`.
142
+
143
+ ## License
144
+
145
+ MIT
@@ -0,0 +1,24 @@
1
+ export interface SentryAuthorization {
2
+ token: string;
3
+ refreshToken: string;
4
+ expiresAt: string;
5
+ }
6
+ export interface SentryInstallationDetails {
7
+ orgSlug: string;
8
+ }
9
+ export interface SentryApiClient {
10
+ exchangeAuthorizationCode(input: {
11
+ installationUuid: string;
12
+ code: string;
13
+ }): Promise<SentryAuthorization>;
14
+ getInstallation(input: {
15
+ installationUuid: string;
16
+ token: string;
17
+ }): Promise<SentryInstallationDetails>;
18
+ verifyInstallation(input: {
19
+ installationUuid: string;
20
+ token: string;
21
+ }): Promise<void>;
22
+ }
23
+ export declare function createSentryApiClient(): SentryApiClient;
24
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,yBAAyB,CAAC,KAAK,EAAE;QAC/B,gBAAgB,EAAE,MAAM,CAAC;QACzB,IAAI,EAAE,MAAM,CAAC;KACd,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAEjC,eAAe,CAAC,KAAK,EAAE;QACrB,gBAAgB,EAAE,MAAM,CAAC;QACzB,KAAK,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;IACvC,kBAAkB,CAAC,KAAK,EAAE;QAAC,gBAAgB,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrF;AAED,wBAAgB,qBAAqB,IAAI,eAAe,CAuEvD"}
@@ -0,0 +1,101 @@
1
+ import { logger } from '@shipfox/node-opentelemetry';
2
+ import ky, { HTTPError, TimeoutError } from 'ky';
3
+ import { config } from '#config.js';
4
+ import { SentryIntegrationProviderError } from '#core/errors.js';
5
+ const SENTRY_API_BASE = 'https://sentry.io/api/0';
6
+ export function createSentryApiClient() {
7
+ return {
8
+ async exchangeAuthorizationCode (input) {
9
+ // Encode the uuid: it originates from the request body / webhook payload, so
10
+ // a path separator in it must not be able to alter the request path (the host
11
+ // is already pinned to SENTRY_API_BASE).
12
+ const body = await mapSentryError('exchange-authorization-code', ()=>ky.post(`${SENTRY_API_BASE}/sentry-app-installations/${encodeURIComponent(input.installationUuid)}/authorizations/`, {
13
+ json: {
14
+ grant_type: 'authorization_code',
15
+ client_id: config.SENTRY_APP_CLIENT_ID,
16
+ client_secret: config.SENTRY_APP_CLIENT_SECRET,
17
+ code: input.code
18
+ }
19
+ }).json());
20
+ if (typeof body.token !== 'string' || typeof body.refreshToken !== 'string' || typeof body.expiresAt !== 'string') {
21
+ throw new SentryIntegrationProviderError('malformed-provider-response', 'Sentry authorization response did not include a token');
22
+ }
23
+ return {
24
+ token: body.token,
25
+ refreshToken: body.refreshToken,
26
+ expiresAt: body.expiresAt
27
+ };
28
+ },
29
+ async getInstallation (input) {
30
+ const body = await mapSentryError('get-installation', ()=>ky.get(`${SENTRY_API_BASE}/sentry-app-installations/${encodeURIComponent(input.installationUuid)}/`, {
31
+ headers: {
32
+ authorization: `Bearer ${input.token}`
33
+ }
34
+ }).json());
35
+ const slug = body.organization?.slug;
36
+ if (typeof slug !== 'string' || slug.length === 0) {
37
+ throw new SentryIntegrationProviderError('malformed-provider-response', 'Sentry installation response did not include an organization slug');
38
+ }
39
+ return {
40
+ orgSlug: slug
41
+ };
42
+ },
43
+ async verifyInstallation (input) {
44
+ await mapSentryError('verify-installation', ()=>ky.put(`${SENTRY_API_BASE}/sentry-app-installations/${encodeURIComponent(input.installationUuid)}/`, {
45
+ headers: {
46
+ authorization: `Bearer ${input.token}`
47
+ },
48
+ json: {
49
+ status: 'installed'
50
+ }
51
+ }).json());
52
+ }
53
+ };
54
+ }
55
+ // The typed error deliberately drops Sentry's status and body so no token, code,
56
+ // or client secret can leak to the client or the logged error chain. That also
57
+ // strips the one detail a self-hoster needs to fix a misconfigured Sentry app —
58
+ // e.g. a 403 on `get-installation` means the app is missing "Organization: Read"
59
+ // (see README "Required permissions"). So we log the upstream status
60
+ // here, the only place it survives, keyed by the operation that failed.
61
+ async function mapSentryError(operation, request) {
62
+ try {
63
+ return await request();
64
+ } catch (error) {
65
+ if (error instanceof SentryIntegrationProviderError) throw error;
66
+ if (error instanceof HTTPError) {
67
+ const { status, statusText, headers } = error.response;
68
+ logger().warn({
69
+ operation,
70
+ status,
71
+ statusText
72
+ }, 'Sentry API request rejected');
73
+ if (status === 429) {
74
+ throw new SentryIntegrationProviderError('rate-limited', 'Sentry request was rate limited', retryAfterSeconds(headers));
75
+ }
76
+ if (status >= 500) {
77
+ throw new SentryIntegrationProviderError('provider-unavailable', 'Sentry request failed');
78
+ }
79
+ throw new SentryIntegrationProviderError('access-denied', 'Sentry request was rejected');
80
+ }
81
+ if (error instanceof TimeoutError) {
82
+ logger().warn({
83
+ operation
84
+ }, 'Sentry API request timed out');
85
+ throw new SentryIntegrationProviderError('timeout', 'Sentry request timed out');
86
+ }
87
+ logger().warn({
88
+ operation,
89
+ errName: error instanceof Error ? error.name : typeof error
90
+ }, 'Sentry API request failed');
91
+ throw new SentryIntegrationProviderError('provider-unavailable', 'Sentry request failed');
92
+ }
93
+ }
94
+ function retryAfterSeconds(headers) {
95
+ const retryAfter = headers.get('retry-after');
96
+ if (!retryAfter) return undefined;
97
+ const parsed = Number.parseInt(retryAfter, 10);
98
+ return Number.isNaN(parsed) ? undefined : parsed;
99
+ }
100
+
101
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/api/client.ts"],"sourcesContent":["import {logger} from '@shipfox/node-opentelemetry';\nimport ky, {HTTPError, TimeoutError} from 'ky';\nimport {config} from '#config.js';\nimport {SentryIntegrationProviderError} from '#core/errors.js';\n\nconst SENTRY_API_BASE = 'https://sentry.io/api/0';\n\nexport interface SentryAuthorization {\n token: string;\n refreshToken: string;\n expiresAt: string;\n}\n\nexport interface SentryInstallationDetails {\n orgSlug: string;\n}\n\nexport interface SentryApiClient {\n exchangeAuthorizationCode(input: {\n installationUuid: string;\n code: string;\n }): Promise<SentryAuthorization>;\n // Derived from Sentry so the org slug is never trusted from the client body.\n getInstallation(input: {\n installationUuid: string;\n token: string;\n }): Promise<SentryInstallationDetails>;\n verifyInstallation(input: {installationUuid: string; token: string}): Promise<void>;\n}\n\nexport function createSentryApiClient(): SentryApiClient {\n return {\n async exchangeAuthorizationCode(input) {\n // Encode the uuid: it originates from the request body / webhook payload, so\n // a path separator in it must not be able to alter the request path (the host\n // is already pinned to SENTRY_API_BASE).\n const body = await mapSentryError('exchange-authorization-code', () =>\n ky\n .post(\n `${SENTRY_API_BASE}/sentry-app-installations/${encodeURIComponent(input.installationUuid)}/authorizations/`,\n {\n json: {\n grant_type: 'authorization_code',\n client_id: config.SENTRY_APP_CLIENT_ID,\n client_secret: config.SENTRY_APP_CLIENT_SECRET,\n code: input.code,\n },\n },\n )\n .json<{token?: unknown; refreshToken?: unknown; expiresAt?: unknown}>(),\n );\n\n if (\n typeof body.token !== 'string' ||\n typeof body.refreshToken !== 'string' ||\n typeof body.expiresAt !== 'string'\n ) {\n throw new SentryIntegrationProviderError(\n 'malformed-provider-response',\n 'Sentry authorization response did not include a token',\n );\n }\n return {token: body.token, refreshToken: body.refreshToken, expiresAt: body.expiresAt};\n },\n\n async getInstallation(input) {\n const body = await mapSentryError('get-installation', () =>\n ky\n .get(\n `${SENTRY_API_BASE}/sentry-app-installations/${encodeURIComponent(input.installationUuid)}/`,\n {\n headers: {authorization: `Bearer ${input.token}`},\n },\n )\n .json<{organization?: {slug?: unknown}}>(),\n );\n\n const slug = body.organization?.slug;\n if (typeof slug !== 'string' || slug.length === 0) {\n throw new SentryIntegrationProviderError(\n 'malformed-provider-response',\n 'Sentry installation response did not include an organization slug',\n );\n }\n return {orgSlug: slug};\n },\n\n async verifyInstallation(input) {\n await mapSentryError('verify-installation', () =>\n ky\n .put(\n `${SENTRY_API_BASE}/sentry-app-installations/${encodeURIComponent(input.installationUuid)}/`,\n {\n headers: {authorization: `Bearer ${input.token}`},\n json: {status: 'installed'},\n },\n )\n .json<unknown>(),\n );\n },\n };\n}\n\n// The typed error deliberately drops Sentry's status and body so no token, code,\n// or client secret can leak to the client or the logged error chain. That also\n// strips the one detail a self-hoster needs to fix a misconfigured Sentry app —\n// e.g. a 403 on `get-installation` means the app is missing \"Organization: Read\"\n// (see README \"Required permissions\"). So we log the upstream status\n// here, the only place it survives, keyed by the operation that failed.\nasync function mapSentryError<T>(operation: string, request: () => Promise<T>): Promise<T> {\n try {\n return await request();\n } catch (error) {\n if (error instanceof SentryIntegrationProviderError) throw error;\n if (error instanceof HTTPError) {\n const {status, statusText, headers} = error.response;\n logger().warn({operation, status, statusText}, 'Sentry API request rejected');\n if (status === 429) {\n throw new SentryIntegrationProviderError(\n 'rate-limited',\n 'Sentry request was rate limited',\n retryAfterSeconds(headers),\n );\n }\n if (status >= 500) {\n throw new SentryIntegrationProviderError('provider-unavailable', 'Sentry request failed');\n }\n throw new SentryIntegrationProviderError('access-denied', 'Sentry request was rejected');\n }\n if (error instanceof TimeoutError) {\n logger().warn({operation}, 'Sentry API request timed out');\n throw new SentryIntegrationProviderError('timeout', 'Sentry request timed out');\n }\n logger().warn(\n {operation, errName: error instanceof Error ? error.name : typeof error},\n 'Sentry API request failed',\n );\n throw new SentryIntegrationProviderError('provider-unavailable', 'Sentry request failed');\n }\n}\n\nfunction retryAfterSeconds(headers: Headers): number | undefined {\n const retryAfter = headers.get('retry-after');\n if (!retryAfter) return undefined;\n const parsed = Number.parseInt(retryAfter, 10);\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n"],"names":["logger","ky","HTTPError","TimeoutError","config","SentryIntegrationProviderError","SENTRY_API_BASE","createSentryApiClient","exchangeAuthorizationCode","input","body","mapSentryError","post","encodeURIComponent","installationUuid","json","grant_type","client_id","SENTRY_APP_CLIENT_ID","client_secret","SENTRY_APP_CLIENT_SECRET","code","token","refreshToken","expiresAt","getInstallation","get","headers","authorization","slug","organization","length","orgSlug","verifyInstallation","put","status","operation","request","error","statusText","response","warn","retryAfterSeconds","errName","Error","name","retryAfter","undefined","parsed","Number","parseInt","isNaN"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export declare const config: Readonly<{
2
+ SENTRY_APP_CLIENT_ID: string;
3
+ SENTRY_APP_CLIENT_SECRET: string;
4
+ SENTRY_APP_SLUG: string;
5
+ SENTRY_APP_VERIFY_INSTALL: boolean;
6
+ SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS: number;
7
+ } & import("@shipfox/config").CleanedEnvAccessors>;
8
+ export declare function assertRetentionDaysWithinBounds(days: number): void;
9
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +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"}
package/dist/config.js ADDED
@@ -0,0 +1,32 @@
1
+ import { bool, createConfig, num, str } from '@shipfox/config';
2
+ export const config = createConfig({
3
+ SENTRY_APP_CLIENT_ID: str({
4
+ 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.'
5
+ }),
6
+ SENTRY_APP_CLIENT_SECRET: str({
7
+ desc: 'Shared secret used to verify the HMAC-SHA256 signature on inbound Sentry webhooks. Must match the value set on the Sentry app. Required.'
8
+ }),
9
+ SENTRY_APP_SLUG: str({
10
+ desc: 'URL slug of the Sentry app, used to build install and callback links. Required.'
11
+ }),
12
+ SENTRY_APP_VERIFY_INSTALL: bool({
13
+ desc: 'Verifies the signature on Sentry app installation requests. Keep it true; turn it off only for local testing.',
14
+ default: true
15
+ }),
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.',
18
+ default: 7
19
+ })
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.
25
+ export function assertRetentionDaysWithinBounds(days) {
26
+ if (!Number.isFinite(days) || days < 1) {
27
+ throw new Error(`SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS must be a finite number of at least 1, received ${days}.`);
28
+ }
29
+ }
30
+ assertRetentionDaysWithinBounds(config.SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS);
31
+
32
+ //# sourceMappingURL=config.js.map
@@ -0,0 +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"}
@@ -0,0 +1,57 @@
1
+ import { IntegrationProviderError } from '@shipfox/api-integration-core-dto';
2
+ export declare class SentryIntegrationProviderError extends IntegrationProviderError {
3
+ }
4
+ export declare class SentryInstallationAlreadyLinkedError extends Error {
5
+ readonly installationUuid: string;
6
+ constructor(installationUuid: string);
7
+ }
8
+ /**
9
+ * The claim could not prove control of the named install: the presented code
10
+ * neither exchanged successfully nor matched the stored `code_hash`. Surfaced as
11
+ * a 403 so a forged or leaked bare uuid cannot bind someone else's install.
12
+ */
13
+ export declare class SentryClaimProofMismatchError extends Error {
14
+ readonly installationUuid: string;
15
+ constructor(installationUuid: string);
16
+ }
17
+ /**
18
+ * A concurrent path (the signed webhook) is mid-exchange for this install, so the
19
+ * verified row is not visible to the claim yet. Retryable: the existing client
20
+ * backoff re-calls and finds the now-persisted row.
21
+ */
22
+ export declare class SentryVerificationInProgressError extends Error {
23
+ readonly installationUuid: string;
24
+ readonly retryAfterSeconds: number;
25
+ constructor(installationUuid: string, retryAfterSeconds?: number);
26
+ }
27
+ /**
28
+ * Base for issue deliveries we received and authenticated but deliberately do
29
+ * not publish. The webhook layer records them for dedup and acknowledges with a
30
+ * 204 rather than treating them as failures — a sustained non-2xx can make Sentry
31
+ * disable the webhook. Carries no HTTP concerns so workers/jobs can reuse it.
32
+ */
33
+ export declare class SentryIssueDroppedError extends Error {
34
+ }
35
+ export declare class SentryInstallationNotFoundError extends SentryIssueDroppedError {
36
+ readonly installationUuid: string;
37
+ constructor(installationUuid: string);
38
+ }
39
+ export declare class SentryInstallationDeletedError extends SentryIssueDroppedError {
40
+ readonly installationUuid: string;
41
+ constructor(installationUuid: string);
42
+ }
43
+ export declare class SentryConnectionNotFoundError extends SentryIssueDroppedError {
44
+ readonly connectionId: string;
45
+ constructor(connectionId: string);
46
+ }
47
+ /**
48
+ * A verified install that no workspace has claimed yet (`connection_id IS NULL`),
49
+ * so an issue delivery has no workspace to publish against. Dropped (204) like the
50
+ * other pre-publish cases; kept distinct from SentryConnectionNotFoundError so the
51
+ * carried identifier is unambiguously an installation uuid, not a connection id.
52
+ */
53
+ export declare class SentryInstallationUnclaimedError extends SentryIssueDroppedError {
54
+ readonly installationUuid: string;
55
+ constructor(installationUuid: string);
56
+ }
57
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,wBAAwB,EAAC,MAAM,mCAAmC,CAAC;AAE3E,qBAAa,8BAA+B,SAAQ,wBAAwB;CAAG;AAE/E,qBAAa,oCAAqC,SAAQ,KAAK;aACjC,gBAAgB,EAAE,MAAM;gBAAxB,gBAAgB,EAAE,MAAM;CAMrD;AAED;;;;GAIG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;aAC1B,gBAAgB,EAAE,MAAM;gBAAxB,gBAAgB,EAAE,MAAM;CAIrD;AAED;;;;GAIG;AACH,qBAAa,iCAAkC,SAAQ,KAAK;aAExC,gBAAgB,EAAE,MAAM;aACxB,iBAAiB;gBADjB,gBAAgB,EAAE,MAAM,EACxB,iBAAiB,SAAI;CAKxC;AAED;;;;;GAKG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;CAAG;AAErD,qBAAa,+BAAgC,SAAQ,uBAAuB;aAC9C,gBAAgB,EAAE,MAAM;gBAAxB,gBAAgB,EAAE,MAAM;CAIrD;AAED,qBAAa,8BAA+B,SAAQ,uBAAuB;aAC7C,gBAAgB,EAAE,MAAM;gBAAxB,gBAAgB,EAAE,MAAM;CAIrD;AAED,qBAAa,6BAA8B,SAAQ,uBAAuB;aAC5C,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM;CAIjD;AAED;;;;;GAKG;AACH,qBAAa,gCAAiC,SAAQ,uBAAuB;aAC/C,gBAAgB,EAAE,MAAM;gBAAxB,gBAAgB,EAAE,MAAM;CAIrD"}