@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,165 @@
1
+ import type { SentryInstallation } from '#db/installations.js';
2
+ export declare const sentryInstallations: import("drizzle-orm/pg-core").PgTableWithColumns<{
3
+ name: "installations";
4
+ schema: undefined;
5
+ columns: {
6
+ id: import("drizzle-orm/pg-core").PgColumn<{
7
+ name: "id";
8
+ tableName: "installations";
9
+ dataType: "string";
10
+ columnType: "PgUUID";
11
+ data: string;
12
+ driverParam: string;
13
+ notNull: true;
14
+ hasDefault: true;
15
+ isPrimaryKey: true;
16
+ isAutoincrement: false;
17
+ hasRuntimeDefault: false;
18
+ enumValues: undefined;
19
+ baseColumn: never;
20
+ identity: undefined;
21
+ generated: undefined;
22
+ }, {}, {}>;
23
+ connectionId: import("drizzle-orm/pg-core").PgColumn<{
24
+ name: "connection_id";
25
+ tableName: "installations";
26
+ dataType: "string";
27
+ columnType: "PgUUID";
28
+ data: string;
29
+ driverParam: string;
30
+ notNull: false;
31
+ hasDefault: false;
32
+ isPrimaryKey: false;
33
+ isAutoincrement: false;
34
+ hasRuntimeDefault: false;
35
+ enumValues: undefined;
36
+ baseColumn: never;
37
+ identity: undefined;
38
+ generated: undefined;
39
+ }, {}, {}>;
40
+ installationUuid: import("drizzle-orm/pg-core").PgColumn<{
41
+ name: "installation_uuid";
42
+ tableName: "installations";
43
+ dataType: "string";
44
+ columnType: "PgText";
45
+ data: string;
46
+ driverParam: string;
47
+ notNull: true;
48
+ hasDefault: false;
49
+ isPrimaryKey: false;
50
+ isAutoincrement: false;
51
+ hasRuntimeDefault: false;
52
+ enumValues: [string, ...string[]];
53
+ baseColumn: never;
54
+ identity: undefined;
55
+ generated: undefined;
56
+ }, {}, {}>;
57
+ orgSlug: import("drizzle-orm/pg-core").PgColumn<{
58
+ name: "org_slug";
59
+ tableName: "installations";
60
+ dataType: "string";
61
+ columnType: "PgText";
62
+ data: string;
63
+ driverParam: string;
64
+ notNull: true;
65
+ hasDefault: false;
66
+ isPrimaryKey: false;
67
+ isAutoincrement: false;
68
+ hasRuntimeDefault: false;
69
+ enumValues: [string, ...string[]];
70
+ baseColumn: never;
71
+ identity: undefined;
72
+ generated: undefined;
73
+ }, {}, {}>;
74
+ status: import("drizzle-orm/pg-core").PgColumn<{
75
+ name: "status";
76
+ tableName: "installations";
77
+ dataType: "string";
78
+ columnType: "PgText";
79
+ data: string;
80
+ driverParam: string;
81
+ notNull: true;
82
+ hasDefault: false;
83
+ isPrimaryKey: false;
84
+ isAutoincrement: false;
85
+ hasRuntimeDefault: false;
86
+ enumValues: [string, ...string[]];
87
+ baseColumn: never;
88
+ identity: undefined;
89
+ generated: undefined;
90
+ }, {}, {}>;
91
+ codeHash: import("drizzle-orm/pg-core").PgColumn<{
92
+ name: "code_hash";
93
+ tableName: "installations";
94
+ dataType: "string";
95
+ columnType: "PgText";
96
+ data: string;
97
+ driverParam: string;
98
+ notNull: false;
99
+ hasDefault: false;
100
+ isPrimaryKey: false;
101
+ isAutoincrement: false;
102
+ hasRuntimeDefault: false;
103
+ enumValues: [string, ...string[]];
104
+ baseColumn: never;
105
+ identity: undefined;
106
+ generated: undefined;
107
+ }, {}, {}>;
108
+ installerUserId: import("drizzle-orm/pg-core").PgColumn<{
109
+ name: "installer_user_id";
110
+ tableName: "installations";
111
+ dataType: "string";
112
+ columnType: "PgUUID";
113
+ data: string;
114
+ driverParam: string;
115
+ notNull: false;
116
+ hasDefault: false;
117
+ isPrimaryKey: false;
118
+ isAutoincrement: false;
119
+ hasRuntimeDefault: false;
120
+ enumValues: undefined;
121
+ baseColumn: never;
122
+ identity: undefined;
123
+ generated: undefined;
124
+ }, {}, {}>;
125
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
126
+ name: "created_at";
127
+ tableName: "installations";
128
+ dataType: "date";
129
+ columnType: "PgTimestamp";
130
+ data: Date;
131
+ driverParam: string;
132
+ notNull: true;
133
+ hasDefault: true;
134
+ isPrimaryKey: false;
135
+ isAutoincrement: false;
136
+ hasRuntimeDefault: false;
137
+ enumValues: undefined;
138
+ baseColumn: never;
139
+ identity: undefined;
140
+ generated: undefined;
141
+ }, {}, {}>;
142
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
143
+ name: "updated_at";
144
+ tableName: "installations";
145
+ dataType: "date";
146
+ columnType: "PgTimestamp";
147
+ data: Date;
148
+ driverParam: string;
149
+ notNull: true;
150
+ hasDefault: true;
151
+ isPrimaryKey: false;
152
+ isAutoincrement: false;
153
+ hasRuntimeDefault: false;
154
+ enumValues: undefined;
155
+ baseColumn: never;
156
+ identity: undefined;
157
+ generated: undefined;
158
+ }, {}, {}>;
159
+ };
160
+ dialect: "pg";
161
+ }>;
162
+ export type SentryInstallationDb = typeof sentryInstallations.$inferSelect;
163
+ export type SentryInstallationCreateDb = typeof sentryInstallations.$inferInsert;
164
+ export declare function toSentryInstallation(row: SentryInstallationDb): SentryInstallation;
165
+ //# sourceMappingURL=installations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installations.d.ts","sourceRoot":"","sources":["../../../src/db/schema/installations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,sBAAsB,CAAC;AAG7D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuB/B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,OAAO,mBAAmB,CAAC,YAAY,CAAC;AAC3E,MAAM,MAAM,0BAA0B,GAAG,OAAO,mBAAmB,CAAC,YAAY,CAAC;AAEjF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,oBAAoB,GAAG,kBAAkB,CAYlF"}
@@ -0,0 +1,42 @@
1
+ import { uuidv7PrimaryKey } from '@shipfox/node-drizzle';
2
+ import { text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core';
3
+ import { pgTable } from './common.js';
4
+ export const sentryInstallations = pgTable('installations', {
5
+ id: uuidv7PrimaryKey(),
6
+ // Null until a logged-in user claims the verified install into a workspace.
7
+ // An unclaimed install (`connection_id IS NULL`, `status='installed'`) is
8
+ // persisted by the authoritative webhook before any browser claim arrives.
9
+ connectionId: uuid('connection_id'),
10
+ installationUuid: text('installation_uuid').notNull(),
11
+ orgSlug: text('org_slug').notNull(),
12
+ status: text('status').notNull(),
13
+ // sha256(authorization code) of the exchange that verified this install.
14
+ // The claim presents the code and we match the hash, so a bare uuid alone
15
+ // cannot bind the install (IDOR guard) without storing a live credential.
16
+ codeHash: text('code_hash'),
17
+ installerUserId: uuid('installer_user_id'),
18
+ createdAt: timestamp('created_at', {
19
+ withTimezone: true
20
+ }).notNull().defaultNow(),
21
+ updatedAt: timestamp('updated_at', {
22
+ withTimezone: true
23
+ }).notNull().defaultNow()
24
+ }, (table)=>[
25
+ uniqueIndex('integrations_sentry_installations_connection_unique').on(table.connectionId),
26
+ uniqueIndex('integrations_sentry_installations_installation_unique').on(table.installationUuid)
27
+ ]);
28
+ export function toSentryInstallation(row) {
29
+ return {
30
+ id: row.id,
31
+ connectionId: row.connectionId,
32
+ installationUuid: row.installationUuid,
33
+ orgSlug: row.orgSlug,
34
+ status: row.status,
35
+ codeHash: row.codeHash,
36
+ installerUserId: row.installerUserId,
37
+ createdAt: row.createdAt,
38
+ updatedAt: row.updatedAt
39
+ };
40
+ }
41
+
42
+ //# sourceMappingURL=installations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/db/schema/installations.ts"],"sourcesContent":["import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';\nimport {text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';\nimport type {SentryInstallation} from '#db/installations.js';\nimport {pgTable} from './common.js';\n\nexport const sentryInstallations = pgTable(\n 'installations',\n {\n id: uuidv7PrimaryKey(),\n // Null until a logged-in user claims the verified install into a workspace.\n // An unclaimed install (`connection_id IS NULL`, `status='installed'`) is\n // persisted by the authoritative webhook before any browser claim arrives.\n connectionId: uuid('connection_id'),\n installationUuid: text('installation_uuid').notNull(),\n orgSlug: text('org_slug').notNull(),\n status: text('status').notNull(),\n // sha256(authorization code) of the exchange that verified this install.\n // The claim presents the code and we match the hash, so a bare uuid alone\n // cannot bind the install (IDOR guard) without storing a live credential.\n codeHash: text('code_hash'),\n installerUserId: uuid('installer_user_id'),\n createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),\n },\n (table) => [\n uniqueIndex('integrations_sentry_installations_connection_unique').on(table.connectionId),\n uniqueIndex('integrations_sentry_installations_installation_unique').on(table.installationUuid),\n ],\n);\n\nexport type SentryInstallationDb = typeof sentryInstallations.$inferSelect;\nexport type SentryInstallationCreateDb = typeof sentryInstallations.$inferInsert;\n\nexport function toSentryInstallation(row: SentryInstallationDb): SentryInstallation {\n return {\n id: row.id,\n connectionId: row.connectionId,\n installationUuid: row.installationUuid,\n orgSlug: row.orgSlug,\n status: row.status,\n codeHash: row.codeHash,\n installerUserId: row.installerUserId,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n"],"names":["uuidv7PrimaryKey","text","timestamp","uniqueIndex","uuid","pgTable","sentryInstallations","id","connectionId","installationUuid","notNull","orgSlug","status","codeHash","installerUserId","createdAt","withTimezone","defaultNow","updatedAt","table","on","toSentryInstallation","row"],"mappings":"AAAA,SAAQA,gBAAgB,QAAO,wBAAwB;AACvD,SAAQC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,IAAI,QAAO,sBAAsB;AAEvE,SAAQC,OAAO,QAAO,cAAc;AAEpC,OAAO,MAAMC,sBAAsBD,QACjC,iBACA;IACEE,IAAIP;IACJ,4EAA4E;IAC5E,0EAA0E;IAC1E,2EAA2E;IAC3EQ,cAAcJ,KAAK;IACnBK,kBAAkBR,KAAK,qBAAqBS,OAAO;IACnDC,SAASV,KAAK,YAAYS,OAAO;IACjCE,QAAQX,KAAK,UAAUS,OAAO;IAC9B,yEAAyE;IACzE,0EAA0E;IAC1E,0EAA0E;IAC1EG,UAAUZ,KAAK;IACfa,iBAAiBV,KAAK;IACtBW,WAAWb,UAAU,cAAc;QAACc,cAAc;IAAI,GAAGN,OAAO,GAAGO,UAAU;IAC7EC,WAAWhB,UAAU,cAAc;QAACc,cAAc;IAAI,GAAGN,OAAO,GAAGO,UAAU;AAC/E,GACA,CAACE,QAAU;QACThB,YAAY,uDAAuDiB,EAAE,CAACD,MAAMX,YAAY;QACxFL,YAAY,yDAAyDiB,EAAE,CAACD,MAAMV,gBAAgB;KAC/F,EACD;AAKF,OAAO,SAASY,qBAAqBC,GAAyB;IAC5D,OAAO;QACLf,IAAIe,IAAIf,EAAE;QACVC,cAAcc,IAAId,YAAY;QAC9BC,kBAAkBa,IAAIb,gBAAgB;QACtCE,SAASW,IAAIX,OAAO;QACpBC,QAAQU,IAAIV,MAAM;QAClBC,UAAUS,IAAIT,QAAQ;QACtBC,iBAAiBQ,IAAIR,eAAe;QACpCC,WAAWO,IAAIP,SAAS;QACxBG,WAAWI,IAAIJ,SAAS;IAC1B;AACF"}
@@ -0,0 +1,36 @@
1
+ import type { GetIntegrationConnectionByIdFn, PublishIntegrationEventReceivedFn, RecordDeliveryOnlyFn, UpdateIntegrationConnectionLifecycleStatusFn } from '@shipfox/api-integration-core-dto';
2
+ import type { ModuleWorker } from '@shipfox/node-module';
3
+ import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
4
+ import { type SentryApiClient } from '#api/client.js';
5
+ import { closeDb, db } from '#db/db.js';
6
+ import { getSentryInstallationByConnectionId, persistVerifiedUnclaimedInstallation } from '#db/installations.js';
7
+ import { migrationsPath } from '#db/migrations.js';
8
+ import { type CreateSentryIntegrationRoutesOptions } from '#presentation/routes/install.js';
9
+ export type { SentryApiClient } from '#api/client.js';
10
+ export { SentryClaimProofMismatchError, SentryInstallationAlreadyLinkedError, SentryInstallationDeletedError, SentryIntegrationProviderError, SentryVerificationInProgressError, } from '#core/errors.js';
11
+ export type { ConnectSentryInstallationInput, VerifyAndPersistUnclaimedInstallationParams, } from '#core/install.js';
12
+ export { handleSentryConnect, hashAuthorizationCode, verifyAndPersistUnclaimedInstallation, } from '#core/install.js';
13
+ export { handleSentryInstallationCreated, handleSentryInstallationDeleted, handleSentryIssueEvent, } from '#core/webhook.js';
14
+ export type { PersistVerifiedUnclaimedInstallationParams, SentryInstallation, SentryInstallationStatus, UpsertSentryInstallationParams, } from '#db/installations.js';
15
+ export { getSentryInstallationByInstallationUuid, persistVerifiedUnclaimedInstallation, upsertSentryInstallation, } from '#db/installations.js';
16
+ export { closeDb, db, migrationsPath };
17
+ export interface CreateSentryIntegrationProviderOptions extends Omit<CreateSentryIntegrationRoutesOptions, 'sentry' | 'persistVerifiedUnclaimedInstallation'> {
18
+ sentry?: SentryApiClient | undefined;
19
+ coreDb: () => NodePgDatabase<Record<string, unknown>>;
20
+ publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;
21
+ recordDeliveryOnly: RecordDeliveryOnlyFn;
22
+ getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
23
+ updateConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;
24
+ getSentryInstallationByConnectionId?: typeof getSentryInstallationByConnectionId | undefined;
25
+ persistVerifiedUnclaimedInstallation?: typeof persistVerifiedUnclaimedInstallation | undefined;
26
+ }
27
+ export declare function createSentryIntegrationProvider(options: CreateSentryIntegrationProviderOptions): {
28
+ provider: "sentry";
29
+ displayName: string;
30
+ connectionExternalUrl(connection: {
31
+ id: string;
32
+ }): Promise<string | undefined>;
33
+ routes: import("@shipfox/node-fastify").RouteGroup[];
34
+ };
35
+ export declare function createSentryMaintenanceWorker(): ModuleWorker;
36
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,8BAA8B,EAC9B,iCAAiC,EACjC,oBAAoB,EACpB,4CAA4C,EAC7C,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAwB,KAAK,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAC,OAAO,EAAE,EAAE,EAAC,MAAM,WAAW,CAAC;AACtC,OAAO,EACL,mCAAmC,EACnC,oCAAoC,EACrC,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAC,cAAc,EAAC,MAAM,mBAAmB,CAAC;AACjD,OAAO,EACL,KAAK,oCAAoC,EAE1C,MAAM,iCAAiC,CAAC;AAKzC,YAAY,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AACpD,OAAO,EACL,6BAA6B,EAC7B,oCAAoC,EACpC,8BAA8B,EAC9B,8BAA8B,EAC9B,iCAAiC,GAClC,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,8BAA8B,EAC9B,2CAA2C,GAC5C,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,qCAAqC,GACtC,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,+BAA+B,EAC/B,+BAA+B,EAC/B,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,0CAA0C,EAC1C,kBAAkB,EAClB,wBAAwB,EACxB,8BAA8B,GAC/B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,uCAAuC,EACvC,oCAAoC,EACpC,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAC,OAAO,EAAE,EAAE,EAAE,cAAc,EAAC,CAAC;AAErC,MAAM,WAAW,sCACf,SAAQ,IAAI,CACV,oCAAoC,EACpC,QAAQ,GAAG,sCAAsC,CAClD;IACD,MAAM,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;IACrC,MAAM,EAAE,MAAM,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,+BAA+B,EAAE,iCAAiC,CAAC;IACnE,kBAAkB,EAAE,oBAAoB,CAAC;IACzC,4BAA4B,EAAE,8BAA8B,CAAC;IAC7D,+BAA+B,EAAE,4CAA4C,CAAC;IAC9E,mCAAmC,CAAC,EAAE,OAAO,mCAAmC,GAAG,SAAS,CAAC;IAC7F,oCAAoC,CAAC,EAAE,OAAO,oCAAoC,GAAG,SAAS,CAAC;CAChG;AAED,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,sCAAsC;;;sCAUnD;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;;EAuBrF;AAKD,wBAAgB,6BAA6B,IAAI,YAAY,CAa5D"}
package/dist/index.js ADDED
@@ -0,0 +1,64 @@
1
+ import { dirname, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { createSentryApiClient } from '#api/client.js';
4
+ import { closeDb, db } from '#db/db.js';
5
+ import { getSentryInstallationByConnectionId, persistVerifiedUnclaimedInstallation } from '#db/installations.js';
6
+ import { migrationsPath } from '#db/migrations.js';
7
+ import { createSentryIntegrationRoutes } from '#presentation/routes/install.js';
8
+ import { createSentryWebhookRoutes } from '#presentation/routes/webhooks.js';
9
+ import { createSentryMaintenanceActivities } from '#temporal/activities/index.js';
10
+ import { SENTRY_MAINTENANCE_TASK_QUEUE } from '#temporal/constants.js';
11
+ export { SentryClaimProofMismatchError, SentryInstallationAlreadyLinkedError, SentryInstallationDeletedError, SentryIntegrationProviderError, SentryVerificationInProgressError } from '#core/errors.js';
12
+ export { handleSentryConnect, hashAuthorizationCode, verifyAndPersistUnclaimedInstallation } from '#core/install.js';
13
+ export { handleSentryInstallationCreated, handleSentryInstallationDeleted, handleSentryIssueEvent } from '#core/webhook.js';
14
+ export { getSentryInstallationByInstallationUuid, persistVerifiedUnclaimedInstallation, upsertSentryInstallation } from '#db/installations.js';
15
+ export { closeDb, db, migrationsPath };
16
+ export function createSentryIntegrationProvider(options) {
17
+ const sentry = options.sentry ?? createSentryApiClient();
18
+ const getInstallationByConnectionId = options.getSentryInstallationByConnectionId ?? getSentryInstallationByConnectionId;
19
+ const persistUnclaimed = options.persistVerifiedUnclaimedInstallation ?? persistVerifiedUnclaimedInstallation;
20
+ return {
21
+ provider: 'sentry',
22
+ displayName: 'Sentry',
23
+ async connectionExternalUrl (connection) {
24
+ const installation = await getInstallationByConnectionId(connection.id);
25
+ if (!installation?.orgSlug) return undefined;
26
+ return `https://sentry.io/organizations/${encodeURIComponent(installation.orgSlug)}/`;
27
+ },
28
+ routes: [
29
+ createSentryIntegrationRoutes({
30
+ sentry,
31
+ getSentryInstallation: options.getSentryInstallation,
32
+ getConnectionById: options.getConnectionById,
33
+ connectSentryInstallation: options.connectSentryInstallation,
34
+ persistVerifiedUnclaimedInstallation: persistUnclaimed
35
+ }),
36
+ createSentryWebhookRoutes({
37
+ sentry,
38
+ coreDb: options.coreDb,
39
+ publishIntegrationEventReceived: options.publishIntegrationEventReceived,
40
+ recordDeliveryOnly: options.recordDeliveryOnly,
41
+ getIntegrationConnectionById: options.getIntegrationConnectionById,
42
+ updateConnectionLifecycleStatus: options.updateConnectionLifecycleStatus
43
+ })
44
+ ]
45
+ };
46
+ }
47
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
48
+ const maintenanceWorkflowsPath = resolve(packageRoot, 'dist/temporal/workflows/index.js');
49
+ export function createSentryMaintenanceWorker() {
50
+ return {
51
+ taskQueue: SENTRY_MAINTENANCE_TASK_QUEUE,
52
+ workflowsPath: maintenanceWorkflowsPath,
53
+ activities: createSentryMaintenanceActivities,
54
+ workflows: [
55
+ {
56
+ name: 'pruneUnclaimedSentryInstallationsCron',
57
+ id: 'sentry-prune-unclaimed-installations',
58
+ cronSchedule: '0 4 * * *'
59
+ }
60
+ ]
61
+ };
62
+ }
63
+
64
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {dirname, resolve} from 'node:path';\nimport {fileURLToPath} from 'node:url';\nimport type {\n GetIntegrationConnectionByIdFn,\n PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n UpdateIntegrationConnectionLifecycleStatusFn,\n} from '@shipfox/api-integration-core-dto';\nimport type {ModuleWorker} from '@shipfox/node-module';\nimport type {NodePgDatabase} from 'drizzle-orm/node-postgres';\nimport {createSentryApiClient, type SentryApiClient} from '#api/client.js';\nimport {closeDb, db} from '#db/db.js';\nimport {\n getSentryInstallationByConnectionId,\n persistVerifiedUnclaimedInstallation,\n} from '#db/installations.js';\nimport {migrationsPath} from '#db/migrations.js';\nimport {\n type CreateSentryIntegrationRoutesOptions,\n createSentryIntegrationRoutes,\n} from '#presentation/routes/install.js';\nimport {createSentryWebhookRoutes} from '#presentation/routes/webhooks.js';\nimport {createSentryMaintenanceActivities} from '#temporal/activities/index.js';\nimport {SENTRY_MAINTENANCE_TASK_QUEUE} from '#temporal/constants.js';\n\nexport type {SentryApiClient} from '#api/client.js';\nexport {\n SentryClaimProofMismatchError,\n SentryInstallationAlreadyLinkedError,\n SentryInstallationDeletedError,\n SentryIntegrationProviderError,\n SentryVerificationInProgressError,\n} from '#core/errors.js';\nexport type {\n ConnectSentryInstallationInput,\n VerifyAndPersistUnclaimedInstallationParams,\n} from '#core/install.js';\nexport {\n handleSentryConnect,\n hashAuthorizationCode,\n verifyAndPersistUnclaimedInstallation,\n} from '#core/install.js';\nexport {\n handleSentryInstallationCreated,\n handleSentryInstallationDeleted,\n handleSentryIssueEvent,\n} from '#core/webhook.js';\nexport type {\n PersistVerifiedUnclaimedInstallationParams,\n SentryInstallation,\n SentryInstallationStatus,\n UpsertSentryInstallationParams,\n} from '#db/installations.js';\nexport {\n getSentryInstallationByInstallationUuid,\n persistVerifiedUnclaimedInstallation,\n upsertSentryInstallation,\n} from '#db/installations.js';\nexport {closeDb, db, migrationsPath};\n\nexport interface CreateSentryIntegrationProviderOptions\n extends Omit<\n CreateSentryIntegrationRoutesOptions,\n 'sentry' | 'persistVerifiedUnclaimedInstallation'\n > {\n sentry?: SentryApiClient | undefined;\n coreDb: () => NodePgDatabase<Record<string, unknown>>;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n updateConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;\n getSentryInstallationByConnectionId?: typeof getSentryInstallationByConnectionId | undefined;\n persistVerifiedUnclaimedInstallation?: typeof persistVerifiedUnclaimedInstallation | undefined;\n}\n\nexport function createSentryIntegrationProvider(options: CreateSentryIntegrationProviderOptions) {\n const sentry = options.sentry ?? createSentryApiClient();\n const getInstallationByConnectionId =\n options.getSentryInstallationByConnectionId ?? getSentryInstallationByConnectionId;\n const persistUnclaimed =\n options.persistVerifiedUnclaimedInstallation ?? persistVerifiedUnclaimedInstallation;\n\n return {\n provider: 'sentry' as const,\n displayName: 'Sentry',\n async connectionExternalUrl(connection: {id: string}): Promise<string | undefined> {\n const installation = await getInstallationByConnectionId(connection.id);\n if (!installation?.orgSlug) return undefined;\n return `https://sentry.io/organizations/${encodeURIComponent(installation.orgSlug)}/`;\n },\n routes: [\n createSentryIntegrationRoutes({\n sentry,\n getSentryInstallation: options.getSentryInstallation,\n getConnectionById: options.getConnectionById,\n connectSentryInstallation: options.connectSentryInstallation,\n persistVerifiedUnclaimedInstallation: persistUnclaimed,\n }),\n createSentryWebhookRoutes({\n sentry,\n coreDb: options.coreDb,\n publishIntegrationEventReceived: options.publishIntegrationEventReceived,\n recordDeliveryOnly: options.recordDeliveryOnly,\n getIntegrationConnectionById: options.getIntegrationConnectionById,\n updateConnectionLifecycleStatus: options.updateConnectionLifecycleStatus,\n }),\n ],\n };\n}\n\nconst packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');\nconst maintenanceWorkflowsPath = resolve(packageRoot, 'dist/temporal/workflows/index.js');\n\nexport function createSentryMaintenanceWorker(): ModuleWorker {\n return {\n taskQueue: SENTRY_MAINTENANCE_TASK_QUEUE,\n workflowsPath: maintenanceWorkflowsPath,\n activities: createSentryMaintenanceActivities,\n workflows: [\n {\n name: 'pruneUnclaimedSentryInstallationsCron',\n id: 'sentry-prune-unclaimed-installations',\n cronSchedule: '0 4 * * *',\n },\n ],\n };\n}\n"],"names":["dirname","resolve","fileURLToPath","createSentryApiClient","closeDb","db","getSentryInstallationByConnectionId","persistVerifiedUnclaimedInstallation","migrationsPath","createSentryIntegrationRoutes","createSentryWebhookRoutes","createSentryMaintenanceActivities","SENTRY_MAINTENANCE_TASK_QUEUE","SentryClaimProofMismatchError","SentryInstallationAlreadyLinkedError","SentryInstallationDeletedError","SentryIntegrationProviderError","SentryVerificationInProgressError","handleSentryConnect","hashAuthorizationCode","verifyAndPersistUnclaimedInstallation","handleSentryInstallationCreated","handleSentryInstallationDeleted","handleSentryIssueEvent","getSentryInstallationByInstallationUuid","upsertSentryInstallation","createSentryIntegrationProvider","options","sentry","getInstallationByConnectionId","persistUnclaimed","provider","displayName","connectionExternalUrl","connection","installation","id","orgSlug","undefined","encodeURIComponent","routes","getSentryInstallation","getConnectionById","connectSentryInstallation","coreDb","publishIntegrationEventReceived","recordDeliveryOnly","getIntegrationConnectionById","updateConnectionLifecycleStatus","packageRoot","url","maintenanceWorkflowsPath","createSentryMaintenanceWorker","taskQueue","workflowsPath","activities","workflows","name","cronSchedule"],"mappings":"AAAA,SAAQA,OAAO,EAAEC,OAAO,QAAO,YAAY;AAC3C,SAAQC,aAAa,QAAO,WAAW;AASvC,SAAQC,qBAAqB,QAA6B,iBAAiB;AAC3E,SAAQC,OAAO,EAAEC,EAAE,QAAO,YAAY;AACtC,SACEC,mCAAmC,EACnCC,oCAAoC,QAC/B,uBAAuB;AAC9B,SAAQC,cAAc,QAAO,oBAAoB;AACjD,SAEEC,6BAA6B,QACxB,kCAAkC;AACzC,SAAQC,yBAAyB,QAAO,mCAAmC;AAC3E,SAAQC,iCAAiC,QAAO,gCAAgC;AAChF,SAAQC,6BAA6B,QAAO,yBAAyB;AAGrE,SACEC,6BAA6B,EAC7BC,oCAAoC,EACpCC,8BAA8B,EAC9BC,8BAA8B,EAC9BC,iCAAiC,QAC5B,kBAAkB;AAKzB,SACEC,mBAAmB,EACnBC,qBAAqB,EACrBC,qCAAqC,QAChC,mBAAmB;AAC1B,SACEC,+BAA+B,EAC/BC,+BAA+B,EAC/BC,sBAAsB,QACjB,mBAAmB;AAO1B,SACEC,uCAAuC,EACvCjB,oCAAoC,EACpCkB,wBAAwB,QACnB,uBAAuB;AAC9B,SAAQrB,OAAO,EAAEC,EAAE,EAAEG,cAAc,GAAE;AAiBrC,OAAO,SAASkB,gCAAgCC,OAA+C;IAC7F,MAAMC,SAASD,QAAQC,MAAM,IAAIzB;IACjC,MAAM0B,gCACJF,QAAQrB,mCAAmC,IAAIA;IACjD,MAAMwB,mBACJH,QAAQpB,oCAAoC,IAAIA;IAElD,OAAO;QACLwB,UAAU;QACVC,aAAa;QACb,MAAMC,uBAAsBC,UAAwB;YAClD,MAAMC,eAAe,MAAMN,8BAA8BK,WAAWE,EAAE;YACtE,IAAI,CAACD,cAAcE,SAAS,OAAOC;YACnC,OAAO,CAAC,gCAAgC,EAAEC,mBAAmBJ,aAAaE,OAAO,EAAE,CAAC,CAAC;QACvF;QACAG,QAAQ;YACN/B,8BAA8B;gBAC5BmB;gBACAa,uBAAuBd,QAAQc,qBAAqB;gBACpDC,mBAAmBf,QAAQe,iBAAiB;gBAC5CC,2BAA2BhB,QAAQgB,yBAAyB;gBAC5DpC,sCAAsCuB;YACxC;YACApB,0BAA0B;gBACxBkB;gBACAgB,QAAQjB,QAAQiB,MAAM;gBACtBC,iCAAiClB,QAAQkB,+BAA+B;gBACxEC,oBAAoBnB,QAAQmB,kBAAkB;gBAC9CC,8BAA8BpB,QAAQoB,4BAA4B;gBAClEC,iCAAiCrB,QAAQqB,+BAA+B;YAC1E;SACD;IACH;AACF;AAEA,MAAMC,cAAchD,QAAQD,QAAQE,cAAc,YAAYgD,GAAG,IAAI;AACrE,MAAMC,2BAA2BlD,QAAQgD,aAAa;AAEtD,OAAO,SAASG;IACd,OAAO;QACLC,WAAWzC;QACX0C,eAAeH;QACfI,YAAY5C;QACZ6C,WAAW;YACT;gBACEC,MAAM;gBACNrB,IAAI;gBACJsB,cAAc;YAChB;SACD;IACH;AACF"}
@@ -0,0 +1,15 @@
1
+ import { type IntegrationConnection } from '@shipfox/api-integration-core-dto';
2
+ export declare function toIntegrationConnectionDto(connection: IntegrationConnection<'sentry'>): {
3
+ created_at: string;
4
+ updated_at: string;
5
+ external_url?: string;
6
+ id: string;
7
+ workspace_id: string;
8
+ provider: string;
9
+ external_account_id: string;
10
+ slug: string;
11
+ display_name: string;
12
+ lifecycle_status: import("@shipfox/api-integration-core-dto").IntegrationConnectionLifecycleStatus;
13
+ capabilities: import("@shipfox/api-integration-core-dto").IntegrationCapability[];
14
+ };
15
+ //# sourceMappingURL=integrations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integrations.d.ts","sourceRoot":"","sources":["../../../src/presentation/dto/integrations.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,qBAAqB,EAE3B,MAAM,mCAAmC,CAAC;AAG3C,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,qBAAqB,CAAC,QAAQ,CAAC;;;;;;;;;;;;EAErF"}
@@ -0,0 +1,9 @@
1
+ import { toIntegrationConnectionDto as toCoreIntegrationConnectionDto } from '@shipfox/api-integration-core-dto';
2
+ // Sentry exposes no adapters, so its connections carry no capabilities.
3
+ export function toIntegrationConnectionDto(connection) {
4
+ return toCoreIntegrationConnectionDto(connection, {
5
+ capabilities: []
6
+ });
7
+ }
8
+
9
+ //# sourceMappingURL=integrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/presentation/dto/integrations.ts"],"sourcesContent":["import {\n type IntegrationConnection,\n toIntegrationConnectionDto as toCoreIntegrationConnectionDto,\n} from '@shipfox/api-integration-core-dto';\n\n// Sentry exposes no adapters, so its connections carry no capabilities.\nexport function toIntegrationConnectionDto(connection: IntegrationConnection<'sentry'>) {\n return toCoreIntegrationConnectionDto(connection, {capabilities: []});\n}\n"],"names":["toIntegrationConnectionDto","toCoreIntegrationConnectionDto","connection","capabilities"],"mappings":"AAAA,SAEEA,8BAA8BC,8BAA8B,QACvD,oCAAoC;AAE3C,wEAAwE;AACxE,OAAO,SAASD,2BAA2BE,UAA2C;IACpF,OAAOD,+BAA+BC,YAAY;QAACC,cAAc,EAAE;IAAA;AACrE"}
@@ -0,0 +1,2 @@
1
+ export declare function sentryRouteErrorHandler(error: unknown): never;
2
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/errors.ts"],"names":[],"mappings":"AAmBA,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CA4B7D"}
@@ -0,0 +1,51 @@
1
+ import { ConnectionSlugConflictError } from '@shipfox/api-integration-core-dto';
2
+ import { ClientError } from '@shipfox/node-fastify';
3
+ import { SentryClaimProofMismatchError, SentryInstallationAlreadyLinkedError, SentryInstallationDeletedError, SentryIntegrationProviderError, SentryVerificationInProgressError } from '#core/errors.js';
4
+ function providerStatus(reason) {
5
+ if (reason === 'rate-limited') return 429;
6
+ if (reason === 'timeout' || reason === 'provider-unavailable') return 503;
7
+ return 422;
8
+ }
9
+ export function sentryRouteErrorHandler(error) {
10
+ if (error instanceof SentryInstallationAlreadyLinkedError) {
11
+ throw new ClientError(error.message, 'sentry-installation-already-linked', {
12
+ status: 409
13
+ });
14
+ }
15
+ if (error instanceof SentryInstallationDeletedError) {
16
+ throw new ClientError(error.message, 'sentry-installation-deleted', {
17
+ status: 409
18
+ });
19
+ }
20
+ if (error instanceof SentryClaimProofMismatchError) {
21
+ throw new ClientError(error.message, 'sentry-claim-proof-mismatch', {
22
+ status: 403
23
+ });
24
+ }
25
+ if (error instanceof ConnectionSlugConflictError) {
26
+ throw new ClientError(error.message, 'slug-conflict', {
27
+ status: 409
28
+ });
29
+ }
30
+ if (error instanceof SentryVerificationInProgressError) {
31
+ // Retryable: a concurrent webhook is mid-exchange. 503 + retry_after lands in
32
+ // the client backoff that already treats >= 500 as retryable.
33
+ throw new ClientError(error.message, 'sentry-verification-in-progress', {
34
+ status: 503,
35
+ details: {
36
+ retry_after_seconds: error.retryAfterSeconds
37
+ }
38
+ });
39
+ }
40
+ if (error instanceof SentryIntegrationProviderError) {
41
+ throw new ClientError(error.message, error.reason, {
42
+ details: {
43
+ retry_after_seconds: error.retryAfterSeconds
44
+ },
45
+ status: providerStatus(error.reason)
46
+ });
47
+ }
48
+ throw error;
49
+ }
50
+
51
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/presentation/routes/errors.ts"],"sourcesContent":["import {\n ConnectionSlugConflictError,\n type IntegrationProviderErrorReason,\n} from '@shipfox/api-integration-core-dto';\nimport {ClientError} from '@shipfox/node-fastify';\nimport {\n SentryClaimProofMismatchError,\n SentryInstallationAlreadyLinkedError,\n SentryInstallationDeletedError,\n SentryIntegrationProviderError,\n SentryVerificationInProgressError,\n} from '#core/errors.js';\n\nfunction providerStatus(reason: IntegrationProviderErrorReason): number {\n if (reason === 'rate-limited') return 429;\n if (reason === 'timeout' || reason === 'provider-unavailable') return 503;\n return 422;\n}\n\nexport function sentryRouteErrorHandler(error: unknown): never {\n if (error instanceof SentryInstallationAlreadyLinkedError) {\n throw new ClientError(error.message, 'sentry-installation-already-linked', {status: 409});\n }\n if (error instanceof SentryInstallationDeletedError) {\n throw new ClientError(error.message, 'sentry-installation-deleted', {status: 409});\n }\n if (error instanceof SentryClaimProofMismatchError) {\n throw new ClientError(error.message, 'sentry-claim-proof-mismatch', {status: 403});\n }\n if (error instanceof ConnectionSlugConflictError) {\n throw new ClientError(error.message, 'slug-conflict', {status: 409});\n }\n if (error instanceof SentryVerificationInProgressError) {\n // Retryable: a concurrent webhook is mid-exchange. 503 + retry_after lands in\n // the client backoff that already treats >= 500 as retryable.\n throw new ClientError(error.message, 'sentry-verification-in-progress', {\n status: 503,\n details: {retry_after_seconds: error.retryAfterSeconds},\n });\n }\n if (error instanceof SentryIntegrationProviderError) {\n throw new ClientError(error.message, error.reason, {\n details: {retry_after_seconds: error.retryAfterSeconds},\n status: providerStatus(error.reason),\n });\n }\n throw error;\n}\n"],"names":["ConnectionSlugConflictError","ClientError","SentryClaimProofMismatchError","SentryInstallationAlreadyLinkedError","SentryInstallationDeletedError","SentryIntegrationProviderError","SentryVerificationInProgressError","providerStatus","reason","sentryRouteErrorHandler","error","message","status","details","retry_after_seconds","retryAfterSeconds"],"mappings":"AAAA,SACEA,2BAA2B,QAEtB,oCAAoC;AAC3C,SAAQC,WAAW,QAAO,wBAAwB;AAClD,SACEC,6BAA6B,EAC7BC,oCAAoC,EACpCC,8BAA8B,EAC9BC,8BAA8B,EAC9BC,iCAAiC,QAC5B,kBAAkB;AAEzB,SAASC,eAAeC,MAAsC;IAC5D,IAAIA,WAAW,gBAAgB,OAAO;IACtC,IAAIA,WAAW,aAAaA,WAAW,wBAAwB,OAAO;IACtE,OAAO;AACT;AAEA,OAAO,SAASC,wBAAwBC,KAAc;IACpD,IAAIA,iBAAiBP,sCAAsC;QACzD,MAAM,IAAIF,YAAYS,MAAMC,OAAO,EAAE,sCAAsC;YAACC,QAAQ;QAAG;IACzF;IACA,IAAIF,iBAAiBN,gCAAgC;QACnD,MAAM,IAAIH,YAAYS,MAAMC,OAAO,EAAE,+BAA+B;YAACC,QAAQ;QAAG;IAClF;IACA,IAAIF,iBAAiBR,+BAA+B;QAClD,MAAM,IAAID,YAAYS,MAAMC,OAAO,EAAE,+BAA+B;YAACC,QAAQ;QAAG;IAClF;IACA,IAAIF,iBAAiBV,6BAA6B;QAChD,MAAM,IAAIC,YAAYS,MAAMC,OAAO,EAAE,iBAAiB;YAACC,QAAQ;QAAG;IACpE;IACA,IAAIF,iBAAiBJ,mCAAmC;QACtD,8EAA8E;QAC9E,8DAA8D;QAC9D,MAAM,IAAIL,YAAYS,MAAMC,OAAO,EAAE,mCAAmC;YACtEC,QAAQ;YACRC,SAAS;gBAACC,qBAAqBJ,MAAMK,iBAAiB;YAAA;QACxD;IACF;IACA,IAAIL,iBAAiBL,gCAAgC;QACnD,MAAM,IAAIJ,YAAYS,MAAMC,OAAO,EAAED,MAAMF,MAAM,EAAE;YACjDK,SAAS;gBAACC,qBAAqBJ,MAAMK,iBAAiB;YAAA;YACtDH,QAAQL,eAAeG,MAAMF,MAAM;QACrC;IACF;IACA,MAAME;AACR"}
@@ -0,0 +1,16 @@
1
+ import type { IntegrationConnection } from '@shipfox/api-integration-core-dto';
2
+ import { type RouteGroup } from '@shipfox/node-fastify';
3
+ import type { SentryApiClient } from '#api/client.js';
4
+ import { type ConnectSentryInstallationInput } from '#core/install.js';
5
+ import type { PersistVerifiedUnclaimedInstallationParams, SentryInstallation } from '#db/installations.js';
6
+ export interface CreateSentryIntegrationRoutesOptions {
7
+ sentry: SentryApiClient;
8
+ getSentryInstallation: (input: {
9
+ installationUuid: string;
10
+ }) => Promise<SentryInstallation | undefined>;
11
+ getConnectionById: (id: string) => Promise<IntegrationConnection<'sentry'> | undefined>;
12
+ connectSentryInstallation: (input: ConnectSentryInstallationInput) => Promise<IntegrationConnection<'sentry'>>;
13
+ persistVerifiedUnclaimedInstallation: (input: PersistVerifiedUnclaimedInstallationParams) => Promise<SentryInstallation>;
14
+ }
15
+ export declare function createSentryIntegrationRoutes({ sentry, getSentryInstallation, getConnectionById, connectSentryInstallation, persistVerifiedUnclaimedInstallation, }: CreateSentryIntegrationRoutesOptions): RouteGroup;
16
+ //# sourceMappingURL=install.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/install.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,mCAAmC,CAAC;AAO7E,OAAO,EAAc,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AACnE,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAEpD,OAAO,EAAC,KAAK,8BAA8B,EAAsB,MAAM,kBAAkB,CAAC;AAC1F,OAAO,KAAK,EACV,0CAA0C,EAC1C,kBAAkB,EACnB,MAAM,sBAAsB,CAAC;AAI9B,MAAM,WAAW,oCAAoC;IACnD,MAAM,EAAE,eAAe,CAAC;IACxB,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,wBAAgB,6BAA6B,CAAC,EAC5C,MAAM,EACN,qBAAqB,EACrB,iBAAiB,EACjB,yBAAyB,EACzB,oCAAoC,GACrC,EAAE,oCAAoC,GAAG,UAAU,CA8DnD"}
@@ -0,0 +1,76 @@
1
+ import { AUTH_USER, requireUserContext, requireWorkspaceAccess } from '@shipfox/api-auth-context';
2
+ import { createSentryInstallBodySchema, createSentryInstallResponseSchema, sentryConnectBodySchema, sentryConnectResponseSchema } from '@shipfox/api-integration-sentry-dto';
3
+ import { defineRoute } from '@shipfox/node-fastify';
4
+ import { config } from '#config.js';
5
+ import { handleSentryConnect } from '#core/install.js';
6
+ import { toIntegrationConnectionDto } from '#presentation/dto/integrations.js';
7
+ import { sentryRouteErrorHandler } from './errors.js';
8
+ export function createSentryIntegrationRoutes({ sentry, getSentryInstallation, getConnectionById, connectSentryInstallation, persistVerifiedUnclaimedInstallation }) {
9
+ const createInstallRoute = defineRoute({
10
+ method: 'POST',
11
+ path: '/install',
12
+ auth: AUTH_USER,
13
+ description: 'Create a Sentry app external-install URL for a workspace.',
14
+ schema: {
15
+ body: createSentryInstallBodySchema,
16
+ response: {
17
+ 200: createSentryInstallResponseSchema
18
+ }
19
+ },
20
+ handler: (request)=>{
21
+ const { workspace_id: workspaceId } = request.body;
22
+ requireWorkspaceAccess({
23
+ request,
24
+ workspaceId
25
+ });
26
+ // Sentry has no state param to embed; the server owns the slug.
27
+ const installUrl = `https://sentry.io/sentry-apps/${config.SENTRY_APP_SLUG}/external-install/`;
28
+ return {
29
+ install_url: installUrl
30
+ };
31
+ }
32
+ });
33
+ const connectRoute = defineRoute({
34
+ method: 'POST',
35
+ path: '/connect',
36
+ auth: AUTH_USER,
37
+ description: 'Link a Sentry installation to a workspace after the install redirect.',
38
+ schema: {
39
+ body: sentryConnectBodySchema,
40
+ response: {
41
+ 200: sentryConnectResponseSchema
42
+ }
43
+ },
44
+ errorHandler: sentryRouteErrorHandler,
45
+ handler: async (request)=>{
46
+ const { workspace_id: workspaceId, code, installation_id: installationUuid } = request.body;
47
+ const actor = requireUserContext(request);
48
+ requireWorkspaceAccess({
49
+ request,
50
+ workspaceId
51
+ });
52
+ const connection = await handleSentryConnect({
53
+ sentry,
54
+ workspaceId,
55
+ code,
56
+ installationUuid,
57
+ installerUserId: actor.userId,
58
+ verifyInstall: config.SENTRY_APP_VERIFY_INSTALL,
59
+ getSentryInstallation,
60
+ getConnectionById,
61
+ connectSentryInstallation,
62
+ persistVerifiedUnclaimedInstallation
63
+ });
64
+ return toIntegrationConnectionDto(connection);
65
+ }
66
+ });
67
+ return {
68
+ prefix: '/integrations/sentry',
69
+ routes: [
70
+ createInstallRoute,
71
+ connectRoute
72
+ ]
73
+ };
74
+ }
75
+
76
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/presentation/routes/install.ts"],"sourcesContent":["import {AUTH_USER, requireUserContext, requireWorkspaceAccess} from '@shipfox/api-auth-context';\nimport type {IntegrationConnection} from '@shipfox/api-integration-core-dto';\nimport {\n createSentryInstallBodySchema,\n createSentryInstallResponseSchema,\n sentryConnectBodySchema,\n sentryConnectResponseSchema,\n} from '@shipfox/api-integration-sentry-dto';\nimport {defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {SentryApiClient} from '#api/client.js';\nimport {config} from '#config.js';\nimport {type ConnectSentryInstallationInput, handleSentryConnect} from '#core/install.js';\nimport type {\n PersistVerifiedUnclaimedInstallationParams,\n SentryInstallation,\n} from '#db/installations.js';\nimport {toIntegrationConnectionDto} from '#presentation/dto/integrations.js';\nimport {sentryRouteErrorHandler} from './errors.js';\n\nexport interface CreateSentryIntegrationRoutesOptions {\n sentry: SentryApiClient;\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\nexport function createSentryIntegrationRoutes({\n sentry,\n getSentryInstallation,\n getConnectionById,\n connectSentryInstallation,\n persistVerifiedUnclaimedInstallation,\n}: CreateSentryIntegrationRoutesOptions): RouteGroup {\n const createInstallRoute = defineRoute({\n method: 'POST',\n path: '/install',\n auth: AUTH_USER,\n description: 'Create a Sentry app external-install URL for a workspace.',\n schema: {\n body: createSentryInstallBodySchema,\n response: {\n 200: createSentryInstallResponseSchema,\n },\n },\n handler: (request) => {\n const {workspace_id: workspaceId} = request.body;\n\n requireWorkspaceAccess({request, workspaceId});\n\n // Sentry has no state param to embed; the server owns the slug.\n const installUrl = `https://sentry.io/sentry-apps/${config.SENTRY_APP_SLUG}/external-install/`;\n return {install_url: installUrl};\n },\n });\n\n const connectRoute = defineRoute({\n method: 'POST',\n path: '/connect',\n auth: AUTH_USER,\n description: 'Link a Sentry installation to a workspace after the install redirect.',\n schema: {\n body: sentryConnectBodySchema,\n response: {\n 200: sentryConnectResponseSchema,\n },\n },\n errorHandler: sentryRouteErrorHandler,\n handler: async (request) => {\n const {workspace_id: workspaceId, code, installation_id: installationUuid} = request.body;\n const actor = requireUserContext(request);\n\n requireWorkspaceAccess({request, workspaceId});\n\n const connection = await handleSentryConnect({\n sentry,\n workspaceId,\n code,\n installationUuid,\n installerUserId: actor.userId,\n verifyInstall: config.SENTRY_APP_VERIFY_INSTALL,\n getSentryInstallation,\n getConnectionById,\n connectSentryInstallation,\n persistVerifiedUnclaimedInstallation,\n });\n\n return toIntegrationConnectionDto(connection);\n },\n });\n\n return {\n prefix: '/integrations/sentry',\n routes: [createInstallRoute, connectRoute],\n };\n}\n"],"names":["AUTH_USER","requireUserContext","requireWorkspaceAccess","createSentryInstallBodySchema","createSentryInstallResponseSchema","sentryConnectBodySchema","sentryConnectResponseSchema","defineRoute","config","handleSentryConnect","toIntegrationConnectionDto","sentryRouteErrorHandler","createSentryIntegrationRoutes","sentry","getSentryInstallation","getConnectionById","connectSentryInstallation","persistVerifiedUnclaimedInstallation","createInstallRoute","method","path","auth","description","schema","body","response","handler","request","workspace_id","workspaceId","installUrl","SENTRY_APP_SLUG","install_url","connectRoute","errorHandler","code","installation_id","installationUuid","actor","connection","installerUserId","userId","verifyInstall","SENTRY_APP_VERIFY_INSTALL","prefix","routes"],"mappings":"AAAA,SAAQA,SAAS,EAAEC,kBAAkB,EAAEC,sBAAsB,QAAO,4BAA4B;AAEhG,SACEC,6BAA6B,EAC7BC,iCAAiC,EACjCC,uBAAuB,EACvBC,2BAA2B,QACtB,sCAAsC;AAC7C,SAAQC,WAAW,QAAwB,wBAAwB;AAEnE,SAAQC,MAAM,QAAO,aAAa;AAClC,SAA6CC,mBAAmB,QAAO,mBAAmB;AAK1F,SAAQC,0BAA0B,QAAO,oCAAoC;AAC7E,SAAQC,uBAAuB,QAAO,cAAc;AAgBpD,OAAO,SAASC,8BAA8B,EAC5CC,MAAM,EACNC,qBAAqB,EACrBC,iBAAiB,EACjBC,yBAAyB,EACzBC,oCAAoC,EACC;IACrC,MAAMC,qBAAqBX,YAAY;QACrCY,QAAQ;QACRC,MAAM;QACNC,MAAMrB;QACNsB,aAAa;QACbC,QAAQ;YACNC,MAAMrB;YACNsB,UAAU;gBACR,KAAKrB;YACP;QACF;QACAsB,SAAS,CAACC;YACR,MAAM,EAACC,cAAcC,WAAW,EAAC,GAAGF,QAAQH,IAAI;YAEhDtB,uBAAuB;gBAACyB;gBAASE;YAAW;YAE5C,gEAAgE;YAChE,MAAMC,aAAa,CAAC,8BAA8B,EAAEtB,OAAOuB,eAAe,CAAC,kBAAkB,CAAC;YAC9F,OAAO;gBAACC,aAAaF;YAAU;QACjC;IACF;IAEA,MAAMG,eAAe1B,YAAY;QAC/BY,QAAQ;QACRC,MAAM;QACNC,MAAMrB;QACNsB,aAAa;QACbC,QAAQ;YACNC,MAAMnB;YACNoB,UAAU;gBACR,KAAKnB;YACP;QACF;QACA4B,cAAcvB;QACde,SAAS,OAAOC;YACd,MAAM,EAACC,cAAcC,WAAW,EAAEM,IAAI,EAAEC,iBAAiBC,gBAAgB,EAAC,GAAGV,QAAQH,IAAI;YACzF,MAAMc,QAAQrC,mBAAmB0B;YAEjCzB,uBAAuB;gBAACyB;gBAASE;YAAW;YAE5C,MAAMU,aAAa,MAAM9B,oBAAoB;gBAC3CI;gBACAgB;gBACAM;gBACAE;gBACAG,iBAAiBF,MAAMG,MAAM;gBAC7BC,eAAelC,OAAOmC,yBAAyB;gBAC/C7B;gBACAC;gBACAC;gBACAC;YACF;YAEA,OAAOP,2BAA2B6B;QACpC;IACF;IAEA,OAAO;QACLK,QAAQ;QACRC,QAAQ;YAAC3B;YAAoBe;SAAa;IAC5C;AACF"}
@@ -0,0 +1,9 @@
1
+ import type { FastifyReply } from 'fastify';
2
+ import type { SentryWebhookContext } from './webhook-context.js';
3
+ export declare function handleInstallationResource(args: {
4
+ context: SentryWebhookContext;
5
+ reply: FastifyReply;
6
+ deliveryId: string;
7
+ rawBody: string;
8
+ }): Promise<null>;
9
+ //# sourceMappingURL=installation-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installation-handler.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/installation-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,SAAS,CAAC;AAO1C,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,sBAAsB,CAAC;AAQ/D,wBAAsB,0BAA0B,CAAC,IAAI,EAAE;IACrD,OAAO,EAAE,oBAAoB,CAAC;IAC9B,KAAK,EAAE,YAAY,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDhB"}