@shipfox/api-integration-sentry 5.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +12 -15
- package/CHANGELOG.md +42 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -5
- package/dist/config.js.map +1 -1
- package/dist/core/install.d.ts +6 -0
- package/dist/core/install.d.ts.map +1 -1
- package/dist/core/install.js +8 -5
- package/dist/core/install.js.map +1 -1
- package/dist/core/signature.d.ts +1 -1
- package/dist/core/signature.d.ts.map +1 -1
- package/dist/core/signature.js.map +1 -1
- package/dist/core/webhook-processor.d.ts +16 -0
- package/dist/core/webhook-processor.d.ts.map +1 -0
- package/dist/core/webhook-processor.js +198 -0
- package/dist/core/webhook-processor.js.map +1 -0
- package/dist/core/webhook.d.ts +4 -4
- package/dist/core/webhook.d.ts.map +1 -1
- package/dist/core/webhook.js +63 -49
- package/dist/core/webhook.js.map +1 -1
- package/dist/db/db.d.ts +8 -4
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/installations.d.ts +35 -13
- package/dist/db/installations.d.ts.map +1 -1
- package/dist/db/installations.js +89 -31
- package/dist/db/installations.js.map +1 -1
- package/dist/db/schema/installations.d.ts +5 -3
- package/dist/db/schema/installations.d.ts.map +1 -1
- package/dist/db/schema/installations.js +6 -6
- package/dist/db/schema/installations.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/webhooks.d.ts +4 -1
- package/dist/presentation/routes/webhooks.d.ts.map +1 -1
- package/dist/presentation/routes/webhooks.js +69 -43
- package/dist/presentation/routes/webhooks.js.map +1 -1
- package/dist/temporal/activities/prune-unclaimed-installations.d.ts +1 -0
- package/dist/temporal/activities/prune-unclaimed-installations.d.ts.map +1 -1
- package/dist/temporal/activities/prune-unclaimed-installations.js.map +1 -1
- package/dist/temporal/workflows/index.bundle.js +17169 -3306
- package/dist/temporal/workflows/prune-unclaimed-installations-cron.d.ts.map +1 -1
- package/dist/temporal/workflows/prune-unclaimed-installations-cron.js +6 -1
- package/dist/temporal/workflows/prune-unclaimed-installations-cron.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +9 -21
- package/src/config.ts +4 -5
- package/src/core/install.ts +21 -7
- package/src/core/signature.ts +1 -1
- package/src/core/webhook-processor.test.ts +304 -0
- package/src/core/webhook-processor.ts +224 -0
- package/src/core/webhook.ts +74 -53
- package/src/db/installations.test.ts +163 -17
- package/src/db/installations.ts +136 -35
- package/src/db/schema/installations.ts +7 -7
- package/src/index.test.ts +4 -1
- package/src/index.ts +16 -0
- package/src/presentation/routes/webhooks.test.ts +6 -5
- package/src/presentation/routes/webhooks.ts +88 -29
- package/src/temporal/activities/prune-unclaimed-installations.test.ts +24 -2
- package/src/temporal/activities/prune-unclaimed-installations.ts +4 -1
- package/src/temporal/workflows/prune-unclaimed-installations-cron.ts +4 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/presentation/routes/installation-handler.d.ts +0 -9
- package/dist/presentation/routes/installation-handler.d.ts.map +0 -1
- package/dist/presentation/routes/installation-handler.js +0 -70
- package/dist/presentation/routes/installation-handler.js.map +0 -1
- package/dist/presentation/routes/issue-handler.d.ts +0 -9
- package/dist/presentation/routes/issue-handler.d.ts.map +0 -1
- package/dist/presentation/routes/issue-handler.js +0 -50
- package/dist/presentation/routes/issue-handler.js.map +0 -1
- package/dist/presentation/routes/webhook-delivery.d.ts +0 -28
- package/dist/presentation/routes/webhook-delivery.d.ts.map +0 -1
- package/dist/presentation/routes/webhook-delivery.js +0 -56
- package/dist/presentation/routes/webhook-delivery.js.map +0 -1
- package/dist/presentation/routes/webhook-request.d.ts +0 -10
- package/dist/presentation/routes/webhook-request.d.ts.map +0 -1
- package/dist/presentation/routes/webhook-request.js +0 -56
- package/dist/presentation/routes/webhook-request.js.map +0 -1
- package/src/presentation/routes/installation-handler.ts +0 -76
- package/src/presentation/routes/issue-handler.ts +0 -52
- package/src/presentation/routes/webhook-delivery.ts +0 -61
- package/src/presentation/routes/webhook-request.ts +0 -66
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/db/installations.ts"],"sourcesContent":["import {and, eq, isNull, lt, sql} from 'drizzle-orm';\nimport {SentryInstallationAlreadyLinkedError} from '#core/errors.js';\nimport {db} from './db.js';\nimport {sentryInstallations, toSentryInstallation} from './schema/installations.js';\n\nexport type SentryInstallationStatus = 'installed' | 'deleted';\n\nexport interface SentryInstallation {\n id: string;\n connectionId: string | null;\n installationUuid: string;\n orgSlug: string;\n status: string;\n codeHash: string | null;\n installerUserId: string | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface UpsertSentryInstallationParams {\n connectionId: string;\n installationUuid: string;\n orgSlug: string;\n status: SentryInstallationStatus;\n codeHash?: string | null | undefined;\n installerUserId?: string | null | undefined;\n}\n\nexport interface PersistVerifiedUnclaimedInstallationParams {\n installationUuid: string;\n orgSlug: string;\n codeHash: string;\n}\n\ntype SentryDb = ReturnType<typeof db>;\ntype SentryTx = Parameters<Parameters<SentryDb['transaction']>[0]>[0];\n\nexport async function upsertSentryInstallation(\n params: UpsertSentryInstallationParams,\n options: {tx?: unknown} = {},\n): Promise<SentryInstallation> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const now = new Date();\n const [row] = await executor\n .insert(sentryInstallations)\n .values({\n connectionId: params.connectionId,\n installationUuid: params.installationUuid,\n orgSlug: params.orgSlug,\n status: params.status,\n codeHash: params.codeHash ?? null,\n installerUserId: params.installerUserId ?? null,\n })\n .onConflictDoUpdate({\n target: sentryInstallations.installationUuid,\n // TOCTOU guard: allow the update only when this installation is unclaimed\n // (connection_id IS NULL, the first claim of a verified-unclaimed row) or\n // already owned by this same connection (idempotent reconnect). A concurrent\n // claim of the same installation to a different workspace commits its own\n // connection_id first; this losing transaction then sees a different non-null\n // connection_id, the predicate is false, Postgres updates nothing, and the\n // empty RETURNING below rolls it back instead of silently repointing the\n // installation (cross-tenant event misroute).\n setWhere: sql`${sentryInstallations.connectionId} is null or ${sentryInstallations.connectionId} = ${params.connectionId}`,\n set: {\n connectionId: params.connectionId,\n orgSlug: params.orgSlug,\n status: params.status,\n codeHash: params.codeHash ?? null,\n installerUserId: params.installerUserId ?? null,\n updatedAt: now,\n },\n })\n .returning();\n\n if (!row) throw new SentryInstallationAlreadyLinkedError(params.installationUuid);\n return toSentryInstallation(row);\n}\n\n/**\n * Inserts the verified-but-unclaimed install half (`connection_id = NULL`,\n * `status = 'installed'`) after a successful code exchange. Idempotent on the\n * installation uuid: a conflicting row refreshes only `code_hash`/`org_slug`/\n * `updated_at`, so a webhook that lands after a claim never clobbers the\n * `connection_id` it set nor downgrades a `deleted` tombstone back to installed.\n */\nexport async function persistVerifiedUnclaimedInstallation(\n params: PersistVerifiedUnclaimedInstallationParams,\n options: {tx?: unknown} = {},\n): Promise<SentryInstallation> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const now = new Date();\n const [row] = await executor\n .insert(sentryInstallations)\n .values({\n connectionId: null,\n installationUuid: params.installationUuid,\n orgSlug: params.orgSlug,\n status: 'installed',\n codeHash: params.codeHash,\n })\n .onConflictDoUpdate({\n target: sentryInstallations.installationUuid,\n set: {\n orgSlug: params.orgSlug,\n codeHash: params.codeHash,\n updatedAt: now,\n },\n })\n .returning();\n\n if (!row) throw new Error('Sentry installation persist returned no rows');\n return toSentryInstallation(row);\n}\n\n/**\n * Verified installs no user has claimed yet (`connection_id IS NULL`,\n * `status='installed'`). Tombstoned rows are excluded so the TTL cron stays\n * idempotent and the unclaimed metric stays accurate. Pass `olderThan` to scope\n * to stale rows for the cron.\n */\nexport async function listUnclaimedSentryInstallations(\n params: {olderThan?: Date} = {},\n options: {tx?: unknown} = {},\n): Promise<SentryInstallation[]> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const conditions = [\n isNull(sentryInstallations.connectionId),\n eq(sentryInstallations.status, 'installed'),\n ];\n if (params.olderThan) {\n conditions.push(lt(sentryInstallations.createdAt, params.olderThan));\n }\n const rows = await executor\n .select()\n .from(sentryInstallations)\n .where(and(...conditions));\n return rows.map(toSentryInstallation);\n}\n\nexport async function getSentryInstallationByInstallationUuid(\n installationUuid: string,\n options: {tx?: unknown} = {},\n): Promise<SentryInstallation | undefined> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const rows = await executor\n .select()\n .from(sentryInstallations)\n .where(eq(sentryInstallations.installationUuid, installationUuid))\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toSentryInstallation(row);\n}\n\nexport async function getSentryInstallationByConnectionId(\n connectionId: string,\n options: {tx?: unknown} = {},\n): Promise<SentryInstallation | undefined> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const rows = await executor\n .select()\n .from(sentryInstallations)\n .where(eq(sentryInstallations.connectionId, connectionId))\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toSentryInstallation(row);\n}\n\n/**\n * Tombstones verified-unclaimed installs older than `olderThan` (the TTL cleanup\n * cron). Bounds a never-claimed install rather than leaving it pending forever.\n * Returns how many rows were tombstoned. We hold no Sentry token for these rows\n * (tokens are never persisted), so the Sentry-side uninstall is out of scope here.\n */\nexport async function pruneUnclaimedSentryInstallations(\n params: {olderThan: Date},\n options: {tx?: unknown} = {},\n): Promise<{tombstoned: number}> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const result = await executor\n .update(sentryInstallations)\n .set({status: 'deleted', updatedAt: new Date()})\n .where(\n and(\n isNull(sentryInstallations.connectionId),\n eq(sentryInstallations.status, 'installed'),\n lt(sentryInstallations.createdAt, params.olderThan),\n ),\n );\n return {tombstoned: result.rowCount ?? 0};\n}\n\nexport async function markSentryInstallationDeleted(\n params: {installationUuid: string},\n options: {tx?: unknown} = {},\n): Promise<SentryInstallation | undefined> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const [row] = await executor\n .update(sentryInstallations)\n .set({status: 'deleted', updatedAt: new Date()})\n .where(eq(sentryInstallations.installationUuid, params.installationUuid))\n .returning();\n if (!row) return undefined;\n return toSentryInstallation(row);\n}\n"],"names":["and","eq","isNull","lt","sql","SentryInstallationAlreadyLinkedError","db","sentryInstallations","toSentryInstallation","upsertSentryInstallation","params","options","executor","tx","now","Date","row","insert","values","connectionId","installationUuid","orgSlug","status","codeHash","installerUserId","onConflictDoUpdate","target","setWhere","set","updatedAt","returning","persistVerifiedUnclaimedInstallation","Error","listUnclaimedSentryInstallations","conditions","olderThan","push","createdAt","rows","select","from","where","map","getSentryInstallationByInstallationUuid","limit","undefined","getSentryInstallationByConnectionId","pruneUnclaimedSentryInstallations","result","update","tombstoned","rowCount","markSentryInstallationDeleted"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/db/installations.ts"],"sourcesContent":["import {and, eq, inArray, isNull, lt, ne, sql} from 'drizzle-orm';\nimport {SentryInstallationAlreadyLinkedError} from '#core/errors.js';\nimport {db} from './db.js';\nimport {sentryInstallations, toSentryInstallation} from './schema/installations.js';\n\nexport type SentryInstallationStatus = 'installed' | 'deleted';\nexport type SentryInstallationRowStatus =\n | 'pending'\n | 'exchange-succeeded'\n | SentryInstallationStatus;\n\nexport interface SentryInstallation {\n id: string;\n connectionId: string | null;\n installationUuid: string;\n orgSlug: string;\n status: SentryInstallationRowStatus;\n codeHash: string | null;\n installerUserId: string | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface UpsertSentryInstallationParams {\n connectionId: string;\n installationUuid: string;\n orgSlug: string;\n status: SentryInstallationStatus;\n codeHash?: string | null | undefined;\n installerUserId?: string | null | undefined;\n}\n\nexport interface PersistVerifiedUnclaimedInstallationParams {\n installationUuid: string;\n orgSlug: string;\n codeHash: string;\n}\n\nexport interface ClaimSentryInstallationVerificationParams {\n installationUuid: string;\n orgSlug: string;\n codeHash: string;\n}\n\ntype SentryDb = ReturnType<typeof db>;\ntype SentryTx = Parameters<Parameters<SentryDb['transaction']>[0]>[0];\n\nexport async function upsertSentryInstallation(\n params: UpsertSentryInstallationParams,\n options: {tx?: unknown} = {},\n): Promise<SentryInstallation> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const now = new Date();\n const [row] = await executor\n .insert(sentryInstallations)\n .values({\n connectionId: params.connectionId,\n installationUuid: params.installationUuid,\n orgSlug: params.orgSlug,\n status: params.status,\n codeHash: params.codeHash ?? null,\n installerUserId: params.installerUserId ?? null,\n })\n .onConflictDoUpdate({\n target: sentryInstallations.installationUuid,\n // A deleted UUID is terminal. Otherwise, allow the first claim of an\n // unclaimed row or an idempotent reconnect by its current connection. This\n // blocks both a concurrent cross-workspace claim and a claim racing deletion.\n setWhere: sql`${sentryInstallations.status} <> 'deleted' and (${sentryInstallations.connectionId} is null or ${sentryInstallations.connectionId} = ${params.connectionId})`,\n set: {\n connectionId: params.connectionId,\n orgSlug: params.orgSlug,\n status: params.status,\n codeHash: params.codeHash ?? null,\n installerUserId: params.installerUserId ?? null,\n updatedAt: now,\n },\n })\n .returning();\n\n if (!row) throw new SentryInstallationAlreadyLinkedError(params.installationUuid);\n return toSentryInstallation(row);\n}\n\n/**\n * Inserts the verified-but-unclaimed install half (`connection_id = NULL`,\n * `status = 'installed'`) after a successful code exchange. Idempotent on the\n * installation uuid: a pending row becomes installed, a claimed row keeps its\n * `connection_id`, and a deleted tombstone never becomes installed again.\n */\nexport async function persistVerifiedUnclaimedInstallation(\n params: PersistVerifiedUnclaimedInstallationParams,\n options: {tx?: unknown} = {},\n): Promise<SentryInstallation> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const now = new Date();\n const [row] = await executor\n .insert(sentryInstallations)\n .values({\n connectionId: null,\n installationUuid: params.installationUuid,\n orgSlug: params.orgSlug,\n status: 'installed',\n codeHash: params.codeHash,\n })\n .onConflictDoUpdate({\n target: sentryInstallations.installationUuid,\n setWhere: ne(sentryInstallations.status, 'deleted'),\n set: {\n orgSlug: params.orgSlug,\n status: 'installed',\n codeHash: params.codeHash,\n updatedAt: now,\n },\n })\n .returning();\n\n if (!row) {\n const existing = await getSentryInstallationByInstallationUuid(params.installationUuid, {\n tx: executor,\n });\n if (existing) return existing;\n throw new Error('Sentry installation persist returned no rows');\n }\n return toSentryInstallation(row);\n}\n\nexport async function claimSentryInstallationVerification(\n params: ClaimSentryInstallationVerificationParams,\n options: {tx?: unknown} = {},\n): Promise<SentryInstallation> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const [inserted] = await executor\n .insert(sentryInstallations)\n .values({\n connectionId: null,\n installationUuid: params.installationUuid,\n orgSlug: params.orgSlug,\n status: 'pending',\n codeHash: params.codeHash,\n })\n .onConflictDoNothing({target: sentryInstallations.installationUuid})\n .returning();\n\n if (inserted) return toSentryInstallation(inserted);\n\n const existing = await getSentryInstallationByInstallationUuid(params.installationUuid, {\n tx: executor,\n });\n if (!existing) throw new Error('Sentry installation verification claim returned no rows');\n return existing;\n}\n\nexport async function completeSentryInstallationVerification(\n params: {installationUuid: string; codeHash: string},\n options: {tx?: unknown} = {},\n): Promise<SentryInstallation | undefined> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const [row] = await executor\n .update(sentryInstallations)\n .set({status: 'installed', updatedAt: new Date()})\n .where(\n and(\n eq(sentryInstallations.installationUuid, params.installationUuid),\n eq(sentryInstallations.status, 'exchange-succeeded'),\n eq(sentryInstallations.codeHash, params.codeHash),\n ),\n )\n .returning();\n if (!row) return undefined;\n return toSentryInstallation(row);\n}\n\nexport async function markSentryInstallationExchangeSucceeded(\n params: {installationUuid: string; codeHash: string},\n options: {tx?: unknown} = {},\n): Promise<SentryInstallation | undefined> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const [row] = await executor\n .update(sentryInstallations)\n .set({status: 'exchange-succeeded', updatedAt: new Date()})\n .where(\n and(\n eq(sentryInstallations.installationUuid, params.installationUuid),\n eq(sentryInstallations.status, 'pending'),\n eq(sentryInstallations.codeHash, params.codeHash),\n ),\n )\n .returning();\n if (!row) return undefined;\n return toSentryInstallation(row);\n}\n\n/**\n * Install claims no user has bound to a connection yet. This includes pending\n * and exchanged claims so abandoned verification work remains observable.\n * Tombstoned rows are excluded. Pass `olderThan` to scope by the most recent\n * state transition for retention work.\n */\nexport async function listUnclaimedSentryInstallations(\n params: {olderThan?: Date} = {},\n options: {tx?: unknown} = {},\n): Promise<SentryInstallation[]> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const conditions = [\n isNull(sentryInstallations.connectionId),\n inArray(sentryInstallations.status, ['pending', 'exchange-succeeded', 'installed']),\n ];\n if (params.olderThan) {\n conditions.push(lt(sentryInstallations.updatedAt, params.olderThan));\n }\n const rows = await executor\n .select()\n .from(sentryInstallations)\n .where(and(...conditions));\n return rows.map(toSentryInstallation);\n}\n\nexport async function getSentryInstallationByInstallationUuid(\n installationUuid: string,\n options: {tx?: unknown} = {},\n): Promise<SentryInstallation | undefined> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const rows = await executor\n .select()\n .from(sentryInstallations)\n .where(eq(sentryInstallations.installationUuid, installationUuid))\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toSentryInstallation(row);\n}\n\nexport async function getSentryInstallationByConnectionId(\n connectionId: string,\n options: {tx?: unknown} = {},\n): Promise<SentryInstallation | undefined> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const rows = await executor\n .select()\n .from(sentryInstallations)\n .where(eq(sentryInstallations.connectionId, connectionId))\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toSentryInstallation(row);\n}\n\n/**\n * Releases abandoned pending claims and tombstones verified-unclaimed installs\n * older than `olderThan`. Pending claims have not durably completed an exchange,\n * so deleting them allows a later signed creation to try again. Exchanged and\n * installed rows stay terminal after expiry. We hold no Sentry token for these\n * rows, so the Sentry-side uninstall is out of scope here.\n */\nexport async function pruneUnclaimedSentryInstallations(\n params: {olderThan: Date},\n options: {tx?: unknown} = {},\n): Promise<{releasedPending: number; tombstoned: number}> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const releasedPending = await executor\n .delete(sentryInstallations)\n .where(\n and(\n isNull(sentryInstallations.connectionId),\n eq(sentryInstallations.status, 'pending'),\n lt(sentryInstallations.updatedAt, params.olderThan),\n ),\n );\n const tombstoned = await executor\n .update(sentryInstallations)\n .set({status: 'deleted', updatedAt: new Date()})\n .where(\n and(\n isNull(sentryInstallations.connectionId),\n inArray(sentryInstallations.status, ['exchange-succeeded', 'installed']),\n lt(sentryInstallations.updatedAt, params.olderThan),\n ),\n );\n return {\n releasedPending: releasedPending.rowCount ?? 0,\n tombstoned: tombstoned.rowCount ?? 0,\n };\n}\n\nexport async function markSentryInstallationDeleted(\n params: {installationUuid: string},\n options: {tx?: unknown} = {},\n): Promise<SentryInstallation> {\n const executor = (options.tx ?? db()) as SentryDb | SentryTx;\n const [row] = await executor\n .insert(sentryInstallations)\n .values({\n connectionId: null,\n installationUuid: params.installationUuid,\n // A reordered delete can arrive without organization data. Deleted rows never expose an organization URL.\n orgSlug: '',\n status: 'deleted',\n codeHash: null,\n })\n .onConflictDoUpdate({\n target: sentryInstallations.installationUuid,\n set: {status: 'deleted', updatedAt: new Date()},\n })\n .returning();\n if (!row) throw new Error('Sentry installation deletion returned no rows');\n return toSentryInstallation(row);\n}\n"],"names":["and","eq","inArray","isNull","lt","ne","sql","SentryInstallationAlreadyLinkedError","db","sentryInstallations","toSentryInstallation","upsertSentryInstallation","params","options","executor","tx","now","Date","row","insert","values","connectionId","installationUuid","orgSlug","status","codeHash","installerUserId","onConflictDoUpdate","target","setWhere","set","updatedAt","returning","persistVerifiedUnclaimedInstallation","existing","getSentryInstallationByInstallationUuid","Error","claimSentryInstallationVerification","inserted","onConflictDoNothing","completeSentryInstallationVerification","update","where","undefined","markSentryInstallationExchangeSucceeded","listUnclaimedSentryInstallations","conditions","olderThan","push","rows","select","from","map","limit","getSentryInstallationByConnectionId","pruneUnclaimedSentryInstallations","releasedPending","delete","tombstoned","rowCount","markSentryInstallationDeleted"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SentryInstallation } from '#db/installations.js';
|
|
1
|
+
import type { SentryInstallation, SentryInstallationRowStatus } from '#db/installations.js';
|
|
2
2
|
export declare const sentryInstallations: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
3
3
|
name: "installations";
|
|
4
4
|
schema: undefined;
|
|
@@ -76,7 +76,7 @@ export declare const sentryInstallations: import("drizzle-orm/pg-core").PgTableW
|
|
|
76
76
|
tableName: "installations";
|
|
77
77
|
dataType: "string";
|
|
78
78
|
columnType: "PgText";
|
|
79
|
-
data:
|
|
79
|
+
data: SentryInstallationRowStatus;
|
|
80
80
|
driverParam: string;
|
|
81
81
|
notNull: true;
|
|
82
82
|
hasDefault: false;
|
|
@@ -87,7 +87,9 @@ export declare const sentryInstallations: import("drizzle-orm/pg-core").PgTableW
|
|
|
87
87
|
baseColumn: never;
|
|
88
88
|
identity: undefined;
|
|
89
89
|
generated: undefined;
|
|
90
|
-
}, {}, {
|
|
90
|
+
}, {}, {
|
|
91
|
+
$type: SentryInstallationRowStatus;
|
|
92
|
+
}>;
|
|
91
93
|
codeHash: import("drizzle-orm/pg-core").PgColumn<{
|
|
92
94
|
name: "code_hash";
|
|
93
95
|
tableName: "installations";
|
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"installations.d.ts","sourceRoot":"","sources":["../../../src/db/schema/installations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,kBAAkB,EAAE,2BAA2B,EAAC,MAAM,sBAAsB,CAAC;AAG1F,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"}
|
|
@@ -4,15 +4,15 @@ import { pgTable } from './common.js';
|
|
|
4
4
|
export const sentryInstallations = pgTable('installations', {
|
|
5
5
|
id: uuidv7PrimaryKey(),
|
|
6
6
|
// Null until a logged-in user claims the verified install into a workspace.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// Pending and verified installs remain unclaimed until the browser binds a
|
|
8
|
+
// connection. Deleted rows are terminal tombstones.
|
|
9
9
|
connectionId: uuid('connection_id'),
|
|
10
10
|
installationUuid: text('installation_uuid').notNull(),
|
|
11
11
|
orgSlug: text('org_slug').notNull(),
|
|
12
|
-
status: text('status').notNull(),
|
|
13
|
-
// sha256(authorization code)
|
|
14
|
-
//
|
|
15
|
-
//
|
|
12
|
+
status: text('status').notNull().$type(),
|
|
13
|
+
// sha256(authorization code) claimed for this install. The hash identifies
|
|
14
|
+
// which pending exchange reached the durable success checkpoint and keeps a
|
|
15
|
+
// bare uuid from binding an install (IDOR guard) without storing a credential.
|
|
16
16
|
codeHash: text('code_hash'),
|
|
17
17
|
installerUserId: uuid('installer_user_id'),
|
|
18
18
|
createdAt: timestamp('created_at', {
|
|
@@ -1 +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 //
|
|
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, SentryInstallationRowStatus} 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 // Pending and verified installs remain unclaimed until the browser binds a\n // connection. Deleted rows are terminal tombstones.\n connectionId: uuid('connection_id'),\n installationUuid: text('installation_uuid').notNull(),\n orgSlug: text('org_slug').notNull(),\n status: text('status').notNull().$type<SentryInstallationRowStatus>(),\n // sha256(authorization code) claimed for this install. The hash identifies\n // which pending exchange reached the durable success checkpoint and keeps a\n // bare uuid from binding an install (IDOR guard) without storing a 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","$type","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,2EAA2E;IAC3E,oDAAoD;IACpDQ,cAAcJ,KAAK;IACnBK,kBAAkBR,KAAK,qBAAqBS,OAAO;IACnDC,SAASV,KAAK,YAAYS,OAAO;IACjCE,QAAQX,KAAK,UAAUS,OAAO,GAAGG,KAAK;IACtC,2EAA2E;IAC3E,4EAA4E;IAC5E,+EAA+E;IAC/EC,UAAUb,KAAK;IACfc,iBAAiBX,KAAK;IACtBY,WAAWd,UAAU,cAAc;QAACe,cAAc;IAAI,GAAGP,OAAO,GAAGQ,UAAU;IAC7EC,WAAWjB,UAAU,cAAc;QAACe,cAAc;IAAI,GAAGP,OAAO,GAAGQ,UAAU;AAC/E,GACA,CAACE,QAAU;QACTjB,YAAY,uDAAuDkB,EAAE,CAACD,MAAMZ,YAAY;QACxFL,YAAY,yDAAyDkB,EAAE,CAACD,MAAMX,gBAAgB;KAC/F,EACD;AAKF,OAAO,SAASa,qBAAqBC,GAAyB;IAC5D,OAAO;QACLhB,IAAIgB,IAAIhB,EAAE;QACVC,cAAce,IAAIf,YAAY;QAC9BC,kBAAkBc,IAAId,gBAAgB;QACtCE,SAASY,IAAIZ,OAAO;QACpBC,QAAQW,IAAIX,MAAM;QAClBE,UAAUS,IAAIT,QAAQ;QACtBC,iBAAiBQ,IAAIR,eAAe;QACpCC,WAAWO,IAAIP,SAAS;QACxBG,WAAWI,IAAIJ,SAAS;IAC1B;AACF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export { SentryClaimProofMismatchError, SentryInstallationAlreadyLinkedError, Se
|
|
|
11
11
|
export type { ConnectSentryInstallationInput, VerifyAndPersistUnclaimedInstallationParams, } from '#core/install.js';
|
|
12
12
|
export { handleSentryConnect, hashAuthorizationCode, verifyAndPersistUnclaimedInstallation, } from '#core/install.js';
|
|
13
13
|
export { handleSentryInstallationCreated, handleSentryInstallationDeleted, handleSentryIssueEvent, } from '#core/webhook.js';
|
|
14
|
+
export type { CreateSentryWebhookProcessorOptions, SentryWebhookProcessor, } from '#core/webhook-processor.js';
|
|
15
|
+
export { createSentryWebhookProcessor } from '#core/webhook-processor.js';
|
|
14
16
|
export type { PersistVerifiedUnclaimedInstallationParams, SentryInstallation, SentryInstallationStatus, UpsertSentryInstallationParams, } from '#db/installations.js';
|
|
15
17
|
export { getSentryInstallationByInstallationUuid, persistVerifiedUnclaimedInstallation, upsertSentryInstallation, } from '#db/installations.js';
|
|
16
18
|
export { closeDb, db, migrationsPath };
|
|
@@ -31,6 +33,10 @@ export declare function createSentryIntegrationProvider(options: CreateSentryInt
|
|
|
31
33
|
id: string;
|
|
32
34
|
}): Promise<string | undefined>;
|
|
33
35
|
routes: import("@shipfox/node-fastify").RouteGroup[];
|
|
36
|
+
webhookProcessors: {
|
|
37
|
+
routeIds: readonly ["sentry"];
|
|
38
|
+
processor: import("#core/webhook-processor.js").SentryWebhookProcessor;
|
|
39
|
+
}[];
|
|
34
40
|
};
|
|
35
41
|
export declare function createSentryMaintenanceWorker(): ModuleWorker;
|
|
36
42
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,8BAA8B,EAC9B,iCAAiC,EACjC,oBAAoB,EACpB,4CAA4C,EAC7C,MAAM,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;
|
|
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;AAE3E,OAAO,EAAC,OAAO,EAAE,EAAE,EAAC,MAAM,WAAW,CAAC;AACtC,OAAO,EACL,mCAAmC,EACnC,oCAAoC,EACrC,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAC,cAAc,EAAC,MAAM,mBAAmB,CAAC;AACjD,OAAO,EACL,KAAK,oCAAoC,EAE1C,MAAM,iCAAiC,CAAC;AAKzC,YAAY,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AACpD,OAAO,EACL,6BAA6B,EAC7B,oCAAoC,EACpC,8BAA8B,EAC9B,8BAA8B,EAC9B,iCAAiC,GAClC,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,8BAA8B,EAC9B,2CAA2C,GAC5C,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,qCAAqC,GACtC,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,+BAA+B,EAC/B,+BAA+B,EAC/B,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,mCAAmC,EACnC,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAC,4BAA4B,EAAC,MAAM,4BAA4B,CAAC;AACxE,YAAY,EACV,0CAA0C,EAC1C,kBAAkB,EAClB,wBAAwB,EACxB,8BAA8B,GAC/B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,uCAAuC,EACvC,oCAAoC,EACpC,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAC,OAAO,EAAE,EAAE,EAAE,cAAc,EAAC,CAAC;AAErC,MAAM,WAAW,sCACf,SAAQ,IAAI,CACV,oCAAoC,EACpC,QAAQ,GAAG,sCAAsC,CAClD;IACD,MAAM,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;IACrC,MAAM,EAAE,MAAM,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,+BAA+B,EAAE,iCAAiC,CAAC;IACnE,kBAAkB,EAAE,oBAAoB,CAAC;IACzC,4BAA4B,EAAE,8BAA8B,CAAC;IAC7D,+BAA+B,EAAE,4CAA4C,CAAC;IAC9E,mCAAmC,CAAC,EAAE,OAAO,mCAAmC,GAAG,SAAS,CAAC;IAC7F,oCAAoC,CAAC,EAAE,OAAO,oCAAoC,GAAG,SAAS,CAAC;CAChG;AAED,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,sCAAsC;;;sCAkBnD;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;;;;;;EAyBrF;AAKD,wBAAgB,6BAA6B,IAAI,YAAY,CAa5D"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dirname, resolve } from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { createSentryApiClient } from '#api/client.js';
|
|
4
|
+
import { createSentryWebhookProcessor } from '#core/webhook-processor.js';
|
|
4
5
|
import { closeDb, db } from '#db/db.js';
|
|
5
6
|
import { getSentryInstallationByConnectionId, persistVerifiedUnclaimedInstallation } from '#db/installations.js';
|
|
6
7
|
import { migrationsPath } from '#db/migrations.js';
|
|
@@ -11,12 +12,21 @@ import { SENTRY_MAINTENANCE_TASK_QUEUE } from '#temporal/constants.js';
|
|
|
11
12
|
export { SentryClaimProofMismatchError, SentryInstallationAlreadyLinkedError, SentryInstallationDeletedError, SentryIntegrationProviderError, SentryVerificationInProgressError } from '#core/errors.js';
|
|
12
13
|
export { handleSentryConnect, hashAuthorizationCode, verifyAndPersistUnclaimedInstallation } from '#core/install.js';
|
|
13
14
|
export { handleSentryInstallationCreated, handleSentryInstallationDeleted, handleSentryIssueEvent } from '#core/webhook.js';
|
|
15
|
+
export { createSentryWebhookProcessor } from '#core/webhook-processor.js';
|
|
14
16
|
export { getSentryInstallationByInstallationUuid, persistVerifiedUnclaimedInstallation, upsertSentryInstallation } from '#db/installations.js';
|
|
15
17
|
export { closeDb, db, migrationsPath };
|
|
16
18
|
export function createSentryIntegrationProvider(options) {
|
|
17
19
|
const sentry = options.sentry ?? createSentryApiClient();
|
|
18
20
|
const getInstallationByConnectionId = options.getSentryInstallationByConnectionId ?? getSentryInstallationByConnectionId;
|
|
19
21
|
const persistUnclaimed = options.persistVerifiedUnclaimedInstallation ?? persistVerifiedUnclaimedInstallation;
|
|
22
|
+
const webhookProcessor = createSentryWebhookProcessor({
|
|
23
|
+
sentry,
|
|
24
|
+
coreDb: options.coreDb,
|
|
25
|
+
publishIntegrationEventReceived: options.publishIntegrationEventReceived,
|
|
26
|
+
recordDeliveryOnly: options.recordDeliveryOnly,
|
|
27
|
+
getIntegrationConnectionById: options.getIntegrationConnectionById,
|
|
28
|
+
updateConnectionLifecycleStatus: options.updateConnectionLifecycleStatus
|
|
29
|
+
});
|
|
20
30
|
return {
|
|
21
31
|
provider: 'sentry',
|
|
22
32
|
displayName: 'Sentry',
|
|
@@ -39,8 +49,17 @@ export function createSentryIntegrationProvider(options) {
|
|
|
39
49
|
publishIntegrationEventReceived: options.publishIntegrationEventReceived,
|
|
40
50
|
recordDeliveryOnly: options.recordDeliveryOnly,
|
|
41
51
|
getIntegrationConnectionById: options.getIntegrationConnectionById,
|
|
42
|
-
updateConnectionLifecycleStatus: options.updateConnectionLifecycleStatus
|
|
52
|
+
updateConnectionLifecycleStatus: options.updateConnectionLifecycleStatus,
|
|
53
|
+
processor: webhookProcessor
|
|
43
54
|
})
|
|
55
|
+
],
|
|
56
|
+
webhookProcessors: [
|
|
57
|
+
{
|
|
58
|
+
routeIds: [
|
|
59
|
+
'sentry'
|
|
60
|
+
],
|
|
61
|
+
processor: webhookProcessor
|
|
62
|
+
}
|
|
44
63
|
]
|
|
45
64
|
};
|
|
46
65
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {dirname, resolve} from 'node:path';\nimport {fileURLToPath} from 'node:url';\nimport type {\n GetIntegrationConnectionByIdFn,\n PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n UpdateIntegrationConnectionLifecycleStatusFn,\n} from '@shipfox/api-integration-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","
|
|
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 {createSentryWebhookProcessor} from '#core/webhook-processor.js';\nimport {closeDb, db} from '#db/db.js';\nimport {\n getSentryInstallationByConnectionId,\n persistVerifiedUnclaimedInstallation,\n} from '#db/installations.js';\nimport {migrationsPath} from '#db/migrations.js';\nimport {\n type CreateSentryIntegrationRoutesOptions,\n createSentryIntegrationRoutes,\n} from '#presentation/routes/install.js';\nimport {createSentryWebhookRoutes} from '#presentation/routes/webhooks.js';\nimport {createSentryMaintenanceActivities} from '#temporal/activities/index.js';\nimport {SENTRY_MAINTENANCE_TASK_QUEUE} from '#temporal/constants.js';\n\nexport type {SentryApiClient} from '#api/client.js';\nexport {\n SentryClaimProofMismatchError,\n SentryInstallationAlreadyLinkedError,\n SentryInstallationDeletedError,\n SentryIntegrationProviderError,\n SentryVerificationInProgressError,\n} from '#core/errors.js';\nexport type {\n ConnectSentryInstallationInput,\n VerifyAndPersistUnclaimedInstallationParams,\n} from '#core/install.js';\nexport {\n handleSentryConnect,\n hashAuthorizationCode,\n verifyAndPersistUnclaimedInstallation,\n} from '#core/install.js';\nexport {\n handleSentryInstallationCreated,\n handleSentryInstallationDeleted,\n handleSentryIssueEvent,\n} from '#core/webhook.js';\nexport type {\n CreateSentryWebhookProcessorOptions,\n SentryWebhookProcessor,\n} from '#core/webhook-processor.js';\nexport {createSentryWebhookProcessor} from '#core/webhook-processor.js';\nexport type {\n PersistVerifiedUnclaimedInstallationParams,\n SentryInstallation,\n SentryInstallationStatus,\n UpsertSentryInstallationParams,\n} from '#db/installations.js';\nexport {\n getSentryInstallationByInstallationUuid,\n persistVerifiedUnclaimedInstallation,\n upsertSentryInstallation,\n} from '#db/installations.js';\nexport {closeDb, db, migrationsPath};\n\nexport interface CreateSentryIntegrationProviderOptions\n extends Omit<\n CreateSentryIntegrationRoutesOptions,\n 'sentry' | 'persistVerifiedUnclaimedInstallation'\n > {\n sentry?: SentryApiClient | undefined;\n coreDb: () => NodePgDatabase<Record<string, unknown>>;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n updateConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;\n getSentryInstallationByConnectionId?: typeof getSentryInstallationByConnectionId | undefined;\n persistVerifiedUnclaimedInstallation?: typeof persistVerifiedUnclaimedInstallation | undefined;\n}\n\nexport function createSentryIntegrationProvider(options: CreateSentryIntegrationProviderOptions) {\n const sentry = options.sentry ?? createSentryApiClient();\n const getInstallationByConnectionId =\n options.getSentryInstallationByConnectionId ?? getSentryInstallationByConnectionId;\n const persistUnclaimed =\n options.persistVerifiedUnclaimedInstallation ?? persistVerifiedUnclaimedInstallation;\n const webhookProcessor = createSentryWebhookProcessor({\n sentry,\n coreDb: options.coreDb,\n publishIntegrationEventReceived: options.publishIntegrationEventReceived,\n recordDeliveryOnly: options.recordDeliveryOnly,\n getIntegrationConnectionById: options.getIntegrationConnectionById,\n updateConnectionLifecycleStatus: options.updateConnectionLifecycleStatus,\n });\n\n return {\n provider: 'sentry' as const,\n displayName: 'Sentry',\n async connectionExternalUrl(connection: {id: string}): Promise<string | undefined> {\n const installation = await getInstallationByConnectionId(connection.id);\n if (!installation?.orgSlug) return undefined;\n return `https://sentry.io/organizations/${encodeURIComponent(installation.orgSlug)}/`;\n },\n routes: [\n createSentryIntegrationRoutes({\n sentry,\n getSentryInstallation: options.getSentryInstallation,\n getConnectionById: options.getConnectionById,\n connectSentryInstallation: options.connectSentryInstallation,\n persistVerifiedUnclaimedInstallation: persistUnclaimed,\n }),\n createSentryWebhookRoutes({\n sentry,\n coreDb: options.coreDb,\n publishIntegrationEventReceived: options.publishIntegrationEventReceived,\n recordDeliveryOnly: options.recordDeliveryOnly,\n getIntegrationConnectionById: options.getIntegrationConnectionById,\n updateConnectionLifecycleStatus: options.updateConnectionLifecycleStatus,\n processor: webhookProcessor,\n }),\n ],\n webhookProcessors: [{routeIds: ['sentry'] as const, processor: webhookProcessor}],\n };\n}\n\nconst packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');\nconst maintenanceWorkflowsPath = resolve(packageRoot, 'dist/temporal/workflows/index.js');\n\nexport function createSentryMaintenanceWorker(): ModuleWorker {\n return {\n taskQueue: SENTRY_MAINTENANCE_TASK_QUEUE,\n workflowsPath: maintenanceWorkflowsPath,\n activities: createSentryMaintenanceActivities,\n workflows: [\n {\n name: 'pruneUnclaimedSentryInstallationsCron',\n id: 'sentry-prune-unclaimed-installations',\n cronSchedule: '0 4 * * *',\n },\n ],\n };\n}\n"],"names":["dirname","resolve","fileURLToPath","createSentryApiClient","createSentryWebhookProcessor","closeDb","db","getSentryInstallationByConnectionId","persistVerifiedUnclaimedInstallation","migrationsPath","createSentryIntegrationRoutes","createSentryWebhookRoutes","createSentryMaintenanceActivities","SENTRY_MAINTENANCE_TASK_QUEUE","SentryClaimProofMismatchError","SentryInstallationAlreadyLinkedError","SentryInstallationDeletedError","SentryIntegrationProviderError","SentryVerificationInProgressError","handleSentryConnect","hashAuthorizationCode","verifyAndPersistUnclaimedInstallation","handleSentryInstallationCreated","handleSentryInstallationDeleted","handleSentryIssueEvent","getSentryInstallationByInstallationUuid","upsertSentryInstallation","createSentryIntegrationProvider","options","sentry","getInstallationByConnectionId","persistUnclaimed","webhookProcessor","coreDb","publishIntegrationEventReceived","recordDeliveryOnly","getIntegrationConnectionById","updateConnectionLifecycleStatus","provider","displayName","connectionExternalUrl","connection","installation","id","orgSlug","undefined","encodeURIComponent","routes","getSentryInstallation","getConnectionById","connectSentryInstallation","processor","webhookProcessors","routeIds","packageRoot","url","maintenanceWorkflowsPath","createSentryMaintenanceWorker","taskQueue","workflowsPath","activities","workflows","name","cronSchedule"],"mappings":"AAAA,SAAQA,OAAO,EAAEC,OAAO,QAAO,YAAY;AAC3C,SAAQC,aAAa,QAAO,WAAW;AASvC,SAAQC,qBAAqB,QAA6B,iBAAiB;AAC3E,SAAQC,4BAA4B,QAAO,6BAA6B;AACxE,SAAQC,OAAO,EAAEC,EAAE,QAAO,YAAY;AACtC,SACEC,mCAAmC,EACnCC,oCAAoC,QAC/B,uBAAuB;AAC9B,SAAQC,cAAc,QAAO,oBAAoB;AACjD,SAEEC,6BAA6B,QACxB,kCAAkC;AACzC,SAAQC,yBAAyB,QAAO,mCAAmC;AAC3E,SAAQC,iCAAiC,QAAO,gCAAgC;AAChF,SAAQC,6BAA6B,QAAO,yBAAyB;AAGrE,SACEC,6BAA6B,EAC7BC,oCAAoC,EACpCC,8BAA8B,EAC9BC,8BAA8B,EAC9BC,iCAAiC,QAC5B,kBAAkB;AAKzB,SACEC,mBAAmB,EACnBC,qBAAqB,EACrBC,qCAAqC,QAChC,mBAAmB;AAC1B,SACEC,+BAA+B,EAC/BC,+BAA+B,EAC/BC,sBAAsB,QACjB,mBAAmB;AAK1B,SAAQpB,4BAA4B,QAAO,6BAA6B;AAOxE,SACEqB,uCAAuC,EACvCjB,oCAAoC,EACpCkB,wBAAwB,QACnB,uBAAuB;AAC9B,SAAQrB,OAAO,EAAEC,EAAE,EAAEG,cAAc,GAAE;AAiBrC,OAAO,SAASkB,gCAAgCC,OAA+C;IAC7F,MAAMC,SAASD,QAAQC,MAAM,IAAI1B;IACjC,MAAM2B,gCACJF,QAAQrB,mCAAmC,IAAIA;IACjD,MAAMwB,mBACJH,QAAQpB,oCAAoC,IAAIA;IAClD,MAAMwB,mBAAmB5B,6BAA6B;QACpDyB;QACAI,QAAQL,QAAQK,MAAM;QACtBC,iCAAiCN,QAAQM,+BAA+B;QACxEC,oBAAoBP,QAAQO,kBAAkB;QAC9CC,8BAA8BR,QAAQQ,4BAA4B;QAClEC,iCAAiCT,QAAQS,+BAA+B;IAC1E;IAEA,OAAO;QACLC,UAAU;QACVC,aAAa;QACb,MAAMC,uBAAsBC,UAAwB;YAClD,MAAMC,eAAe,MAAMZ,8BAA8BW,WAAWE,EAAE;YACtE,IAAI,CAACD,cAAcE,SAAS,OAAOC;YACnC,OAAO,CAAC,gCAAgC,EAAEC,mBAAmBJ,aAAaE,OAAO,EAAE,CAAC,CAAC;QACvF;QACAG,QAAQ;YACNrC,8BAA8B;gBAC5BmB;gBACAmB,uBAAuBpB,QAAQoB,qBAAqB;gBACpDC,mBAAmBrB,QAAQqB,iBAAiB;gBAC5CC,2BAA2BtB,QAAQsB,yBAAyB;gBAC5D1C,sCAAsCuB;YACxC;YACApB,0BAA0B;gBACxBkB;gBACAI,QAAQL,QAAQK,MAAM;gBACtBC,iCAAiCN,QAAQM,+BAA+B;gBACxEC,oBAAoBP,QAAQO,kBAAkB;gBAC9CC,8BAA8BR,QAAQQ,4BAA4B;gBAClEC,iCAAiCT,QAAQS,+BAA+B;gBACxEc,WAAWnB;YACb;SACD;QACDoB,mBAAmB;YAAC;gBAACC,UAAU;oBAAC;iBAAS;gBAAWF,WAAWnB;YAAgB;SAAE;IACnF;AACF;AAEA,MAAMsB,cAAcrD,QAAQD,QAAQE,cAAc,YAAYqD,GAAG,IAAI;AACrE,MAAMC,2BAA2BvD,QAAQqD,aAAa;AAEtD,OAAO,SAASG;IACd,OAAO;QACLC,WAAW7C;QACX8C,eAAeH;QACfI,YAAYhD;QACZiD,WAAW;YACT;gBACEC,MAAM;gBACNnB,IAAI;gBACJoB,cAAc;YAChB;SACD;IACH;AACF"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type RouteGroup } from '@shipfox/node-fastify';
|
|
2
|
+
import { type SentryWebhookProcessor } from '#core/webhook-processor.js';
|
|
2
3
|
import type { SentryWebhookContext } from './webhook-context.js';
|
|
3
4
|
export type { SentryWebhookContext } from './webhook-context.js';
|
|
4
|
-
export declare function createSentryWebhookRoutes(context: SentryWebhookContext
|
|
5
|
+
export declare function createSentryWebhookRoutes(context: SentryWebhookContext & {
|
|
6
|
+
processor?: SentryWebhookProcessor | undefined;
|
|
7
|
+
}): RouteGroup;
|
|
5
8
|
//# sourceMappingURL=webhooks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/webhooks.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/webhooks.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,KAAK,UAAU,EAGhB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,sBAAsB,CAAC;AAE/D,YAAY,EAAC,oBAAoB,EAAC,MAAM,sBAAsB,CAAC;AAE/D,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,oBAAoB,GAAG;IAAC,SAAS,CAAC,EAAE,sBAAsB,GAAG,SAAS,CAAA;CAAC,GAC/E,UAAU,CAkCZ"}
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createStoredWebhookRequest, WEBHOOK_MAX_RAW_BODY_BYTES } from '@shipfox/api-integration-core-dto';
|
|
1
3
|
import { ClientError, defineRoute, rawBodyPlugin, WEBHOOK_BODY_LIMIT } from '@shipfox/node-fastify';
|
|
2
|
-
import {
|
|
3
|
-
import { config } from '#config.js';
|
|
4
|
-
import { verifySentrySignature } from '#core/signature.js';
|
|
5
|
-
import { handleInstallationResource } from './installation-handler.js';
|
|
6
|
-
import { handleIssueResource } from './issue-handler.js';
|
|
7
|
-
import { recordAndDrop } from './webhook-delivery.js';
|
|
8
|
-
import { parseSentryWebhookRequest } from './webhook-request.js';
|
|
9
|
-
const ISSUE_RESOURCE = 'issue';
|
|
10
|
-
const INSTALLATION_RESOURCE = 'installation';
|
|
4
|
+
import { createSentryWebhookProcessor } from '#core/webhook-processor.js';
|
|
11
5
|
export function createSentryWebhookRoutes(context) {
|
|
6
|
+
const processor = context.processor ?? createSentryWebhookProcessor(context);
|
|
12
7
|
const webhookRoute = defineRoute({
|
|
13
8
|
method: 'POST',
|
|
14
9
|
path: '/',
|
|
@@ -17,43 +12,18 @@ export function createSentryWebhookRoutes(context) {
|
|
|
17
12
|
options: {
|
|
18
13
|
bodyLimit: WEBHOOK_BODY_LIMIT
|
|
19
14
|
},
|
|
20
|
-
handler: (request, reply)=>{
|
|
21
|
-
const
|
|
22
|
-
if (!
|
|
23
|
-
|
|
24
|
-
signature,
|
|
25
|
-
secret: config.SENTRY_APP_CLIENT_SECRET
|
|
26
|
-
})) {
|
|
27
|
-
throw new ClientError('invalid signature', 'invalid-signature', {
|
|
28
|
-
status: 401
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
logger().debug({
|
|
32
|
-
deliveryId,
|
|
33
|
-
signatureHeader: signatureHeaderName
|
|
34
|
-
}, 'sentry webhook: signature verified');
|
|
35
|
-
if (resource === ISSUE_RESOURCE) {
|
|
36
|
-
return handleIssueResource({
|
|
37
|
-
context,
|
|
38
|
-
reply,
|
|
39
|
-
deliveryId,
|
|
40
|
-
rawBody
|
|
41
|
-
});
|
|
15
|
+
handler: async (request, reply)=>{
|
|
16
|
+
const body = request.body;
|
|
17
|
+
if (!(body instanceof Uint8Array)) {
|
|
18
|
+
throw new ClientError('expected raw JSON body', 'invalid-body');
|
|
42
19
|
}
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
reply,
|
|
47
|
-
deliveryId,
|
|
48
|
-
rawBody
|
|
20
|
+
if (body.byteLength > WEBHOOK_MAX_RAW_BODY_BYTES) {
|
|
21
|
+
throw new ClientError('Webhook request body is too large', 'body-too-large', {
|
|
22
|
+
status: 413
|
|
49
23
|
});
|
|
50
24
|
}
|
|
51
|
-
|
|
52
|
-
return
|
|
53
|
-
context,
|
|
54
|
-
reply,
|
|
55
|
-
deliveryId
|
|
56
|
-
});
|
|
25
|
+
const result = await processor.process(createSentryStoredWebhookRequest(body, request.headers, request.raw.url?.split('?')[1] ?? ''));
|
|
26
|
+
return sendSentryWebhookResponse(reply, result, request.headers);
|
|
57
27
|
}
|
|
58
28
|
});
|
|
59
29
|
return {
|
|
@@ -67,5 +37,61 @@ export function createSentryWebhookRoutes(context) {
|
|
|
67
37
|
]
|
|
68
38
|
};
|
|
69
39
|
}
|
|
40
|
+
function createSentryStoredWebhookRequest(body, headers, rawQueryString) {
|
|
41
|
+
try {
|
|
42
|
+
return createStoredWebhookRequest({
|
|
43
|
+
requestId: randomUUID(),
|
|
44
|
+
routeId: 'sentry',
|
|
45
|
+
receivedAt: new Date().toISOString(),
|
|
46
|
+
rawQueryString,
|
|
47
|
+
headers: sentryWebhookHeaders(headers),
|
|
48
|
+
body
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new ClientError('Webhook request metadata is invalid', 'invalid-webhook-request', {
|
|
52
|
+
cause: error
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function sentryWebhookHeaders(headers) {
|
|
57
|
+
return Object.fromEntries([
|
|
58
|
+
'content-type',
|
|
59
|
+
'request-id',
|
|
60
|
+
'sentry-hook-resource',
|
|
61
|
+
'sentry-hook-signature',
|
|
62
|
+
'sentry-app-signature'
|
|
63
|
+
].flatMap((name)=>{
|
|
64
|
+
const value = headers[name];
|
|
65
|
+
return typeof value === 'string' ? [
|
|
66
|
+
[
|
|
67
|
+
name,
|
|
68
|
+
value
|
|
69
|
+
]
|
|
70
|
+
] : [];
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
function sendSentryWebhookResponse(reply, result, headers) {
|
|
74
|
+
if (result.outcome === 'discarded' && result.reason === 'invalid_signature') {
|
|
75
|
+
reply.code(401);
|
|
76
|
+
return {
|
|
77
|
+
error: 'invalid signature'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (result.outcome === 'discarded' && result.reason === 'missing_required_input') {
|
|
81
|
+
const hasSignature = typeof headers['sentry-hook-signature'] === 'string' || typeof headers['sentry-app-signature'] === 'string';
|
|
82
|
+
if (!hasSignature) {
|
|
83
|
+
reply.code(401);
|
|
84
|
+
return {
|
|
85
|
+
error: 'missing signature'
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
reply.code(400);
|
|
89
|
+
return {
|
|
90
|
+
error: 'missing required input'
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
reply.code(204);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
70
96
|
|
|
71
97
|
//# sourceMappingURL=webhooks.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/presentation/routes/webhooks.ts"],"sourcesContent":["import {\n ClientError,\n defineRoute,\n type RouteGroup,\n rawBodyPlugin,\n WEBHOOK_BODY_LIMIT,\n} from '@shipfox/node-fastify';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/webhooks.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport {\n createStoredWebhookRequest,\n WEBHOOK_MAX_RAW_BODY_BYTES,\n} from '@shipfox/api-integration-core-dto';\nimport {\n ClientError,\n defineRoute,\n type RouteGroup,\n rawBodyPlugin,\n WEBHOOK_BODY_LIMIT,\n} from '@shipfox/node-fastify';\nimport {\n createSentryWebhookProcessor,\n type SentryWebhookProcessor,\n} from '#core/webhook-processor.js';\nimport type {SentryWebhookContext} from './webhook-context.js';\n\nexport type {SentryWebhookContext} from './webhook-context.js';\n\nexport function createSentryWebhookRoutes(\n context: SentryWebhookContext & {processor?: SentryWebhookProcessor | undefined},\n): RouteGroup {\n const processor = context.processor ?? createSentryWebhookProcessor(context);\n const webhookRoute = defineRoute({\n method: 'POST',\n path: '/',\n auth: [],\n description: 'Sentry integration webhook receiver.',\n options: {bodyLimit: WEBHOOK_BODY_LIMIT},\n handler: async (request, reply) => {\n const body = request.body;\n if (!(body instanceof Uint8Array)) {\n throw new ClientError('expected raw JSON body', 'invalid-body');\n }\n if (body.byteLength > WEBHOOK_MAX_RAW_BODY_BYTES) {\n throw new ClientError('Webhook request body is too large', 'body-too-large', {status: 413});\n }\n\n const result = await processor.process(\n createSentryStoredWebhookRequest(\n body,\n request.headers,\n request.raw.url?.split('?')[1] ?? '',\n ),\n );\n return sendSentryWebhookResponse(reply, result, request.headers);\n },\n });\n\n return {\n prefix: '/webhooks/integrations/sentry',\n auth: [],\n plugins: [rawBodyPlugin],\n routes: [webhookRoute],\n };\n}\n\nfunction createSentryStoredWebhookRequest(\n body: Uint8Array,\n headers: Record<string, string | string[] | undefined>,\n rawQueryString: string,\n) {\n try {\n return createStoredWebhookRequest({\n requestId: randomUUID(),\n routeId: 'sentry',\n receivedAt: new Date().toISOString(),\n rawQueryString,\n headers: sentryWebhookHeaders(headers),\n body,\n });\n } catch (error) {\n throw new ClientError('Webhook request metadata is invalid', 'invalid-webhook-request', {\n cause: error,\n });\n }\n}\n\nfunction sentryWebhookHeaders(headers: Record<string, string | string[] | undefined>) {\n return Object.fromEntries(\n [\n 'content-type',\n 'request-id',\n 'sentry-hook-resource',\n 'sentry-hook-signature',\n 'sentry-app-signature',\n ].flatMap((name) => {\n const value = headers[name];\n return typeof value === 'string' ? [[name, value]] : [];\n }),\n );\n}\n\nfunction sendSentryWebhookResponse(\n reply: {code(statusCode: number): void},\n result: import('@shipfox/api-integration-core-dto').WebhookProcessingResult,\n headers: Record<string, string | string[] | undefined>,\n) {\n if (result.outcome === 'discarded' && result.reason === 'invalid_signature') {\n reply.code(401);\n return {error: 'invalid signature'};\n }\n if (result.outcome === 'discarded' && result.reason === 'missing_required_input') {\n const hasSignature =\n typeof headers['sentry-hook-signature'] === 'string' ||\n typeof headers['sentry-app-signature'] === 'string';\n if (!hasSignature) {\n reply.code(401);\n return {error: 'missing signature'};\n }\n reply.code(400);\n return {error: 'missing required input'};\n }\n\n reply.code(204);\n return null;\n}\n"],"names":["randomUUID","createStoredWebhookRequest","WEBHOOK_MAX_RAW_BODY_BYTES","ClientError","defineRoute","rawBodyPlugin","WEBHOOK_BODY_LIMIT","createSentryWebhookProcessor","createSentryWebhookRoutes","context","processor","webhookRoute","method","path","auth","description","options","bodyLimit","handler","request","reply","body","Uint8Array","byteLength","status","result","process","createSentryStoredWebhookRequest","headers","raw","url","split","sendSentryWebhookResponse","prefix","plugins","routes","rawQueryString","requestId","routeId","receivedAt","Date","toISOString","sentryWebhookHeaders","error","cause","Object","fromEntries","flatMap","name","value","outcome","reason","code","hasSignature"],"mappings":"AAAA,SAAQA,UAAU,QAAO,cAAc;AACvC,SACEC,0BAA0B,EAC1BC,0BAA0B,QACrB,oCAAoC;AAC3C,SACEC,WAAW,EACXC,WAAW,EAEXC,aAAa,EACbC,kBAAkB,QACb,wBAAwB;AAC/B,SACEC,4BAA4B,QAEvB,6BAA6B;AAKpC,OAAO,SAASC,0BACdC,OAAgF;IAEhF,MAAMC,YAAYD,QAAQC,SAAS,IAAIH,6BAA6BE;IACpE,MAAME,eAAeP,YAAY;QAC/BQ,QAAQ;QACRC,MAAM;QACNC,MAAM,EAAE;QACRC,aAAa;QACbC,SAAS;YAACC,WAAWX;QAAkB;QACvCY,SAAS,OAAOC,SAASC;YACvB,MAAMC,OAAOF,QAAQE,IAAI;YACzB,IAAI,CAAEA,CAAAA,gBAAgBC,UAAS,GAAI;gBACjC,MAAM,IAAInB,YAAY,0BAA0B;YAClD;YACA,IAAIkB,KAAKE,UAAU,GAAGrB,4BAA4B;gBAChD,MAAM,IAAIC,YAAY,qCAAqC,kBAAkB;oBAACqB,QAAQ;gBAAG;YAC3F;YAEA,MAAMC,SAAS,MAAMf,UAAUgB,OAAO,CACpCC,iCACEN,MACAF,QAAQS,OAAO,EACfT,QAAQU,GAAG,CAACC,GAAG,EAAEC,MAAM,IAAI,CAAC,EAAE,IAAI;YAGtC,OAAOC,0BAA0BZ,OAAOK,QAAQN,QAAQS,OAAO;QACjE;IACF;IAEA,OAAO;QACLK,QAAQ;QACRnB,MAAM,EAAE;QACRoB,SAAS;YAAC7B;SAAc;QACxB8B,QAAQ;YAACxB;SAAa;IACxB;AACF;AAEA,SAASgB,iCACPN,IAAgB,EAChBO,OAAsD,EACtDQ,cAAsB;IAEtB,IAAI;QACF,OAAOnC,2BAA2B;YAChCoC,WAAWrC;YACXsC,SAAS;YACTC,YAAY,IAAIC,OAAOC,WAAW;YAClCL;YACAR,SAASc,qBAAqBd;YAC9BP;QACF;IACF,EAAE,OAAOsB,OAAO;QACd,MAAM,IAAIxC,YAAY,uCAAuC,2BAA2B;YACtFyC,OAAOD;QACT;IACF;AACF;AAEA,SAASD,qBAAqBd,OAAsD;IAClF,OAAOiB,OAAOC,WAAW,CACvB;QACE;QACA;QACA;QACA;QACA;KACD,CAACC,OAAO,CAAC,CAACC;QACT,MAAMC,QAAQrB,OAAO,CAACoB,KAAK;QAC3B,OAAO,OAAOC,UAAU,WAAW;YAAC;gBAACD;gBAAMC;aAAM;SAAC,GAAG,EAAE;IACzD;AAEJ;AAEA,SAASjB,0BACPZ,KAAuC,EACvCK,MAA2E,EAC3EG,OAAsD;IAEtD,IAAIH,OAAOyB,OAAO,KAAK,eAAezB,OAAO0B,MAAM,KAAK,qBAAqB;QAC3E/B,MAAMgC,IAAI,CAAC;QACX,OAAO;YAACT,OAAO;QAAmB;IACpC;IACA,IAAIlB,OAAOyB,OAAO,KAAK,eAAezB,OAAO0B,MAAM,KAAK,0BAA0B;QAChF,MAAME,eACJ,OAAOzB,OAAO,CAAC,wBAAwB,KAAK,YAC5C,OAAOA,OAAO,CAAC,uBAAuB,KAAK;QAC7C,IAAI,CAACyB,cAAc;YACjBjC,MAAMgC,IAAI,CAAC;YACX,OAAO;gBAACT,OAAO;YAAmB;QACpC;QACAvB,MAAMgC,IAAI,CAAC;QACX,OAAO;YAACT,OAAO;QAAwB;IACzC;IAEAvB,MAAMgC,IAAI,CAAC;IACX,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prune-unclaimed-installations.d.ts","sourceRoot":"","sources":["../../../src/temporal/activities/prune-unclaimed-installations.ts"],"names":[],"mappings":"AAKA,wBAAsB,yCAAyC,IAAI,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"prune-unclaimed-installations.d.ts","sourceRoot":"","sources":["../../../src/temporal/activities/prune-unclaimed-installations.ts"],"names":[],"mappings":"AAKA,wBAAsB,yCAAyC,IAAI,OAAO,CAAC;IACzE,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC,CAKD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/temporal/activities/prune-unclaimed-installations.ts"],"sourcesContent":["import {config} from '#config.js';\nimport {pruneUnclaimedSentryInstallations} from '#db/installations.js';\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\nexport async function pruneUnclaimedSentryInstallationsActivity(): Promise<{tombstoned: number}> {\n const olderThan = new Date(\n Date.now() - config.SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS * MS_PER_DAY,\n );\n return await pruneUnclaimedSentryInstallations({olderThan});\n}\n"],"names":["config","pruneUnclaimedSentryInstallations","MS_PER_DAY","pruneUnclaimedSentryInstallationsActivity","olderThan","Date","now","SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS"],"mappings":"AAAA,SAAQA,MAAM,QAAO,aAAa;AAClC,SAAQC,iCAAiC,QAAO,uBAAuB;AAEvE,MAAMC,aAAa,KAAK,KAAK,KAAK;AAElC,OAAO,eAAeC;
|
|
1
|
+
{"version":3,"sources":["../../../src/temporal/activities/prune-unclaimed-installations.ts"],"sourcesContent":["import {config} from '#config.js';\nimport {pruneUnclaimedSentryInstallations} from '#db/installations.js';\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\nexport async function pruneUnclaimedSentryInstallationsActivity(): Promise<{\n releasedPending: number;\n tombstoned: number;\n}> {\n const olderThan = new Date(\n Date.now() - config.SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS * MS_PER_DAY,\n );\n return await pruneUnclaimedSentryInstallations({olderThan});\n}\n"],"names":["config","pruneUnclaimedSentryInstallations","MS_PER_DAY","pruneUnclaimedSentryInstallationsActivity","olderThan","Date","now","SENTRY_UNCLAIMED_INSTALLATION_RETENTION_DAYS"],"mappings":"AAAA,SAAQA,MAAM,QAAO,aAAa;AAClC,SAAQC,iCAAiC,QAAO,uBAAuB;AAEvE,MAAMC,aAAa,KAAK,KAAK,KAAK;AAElC,OAAO,eAAeC;IAIpB,MAAMC,YAAY,IAAIC,KACpBA,KAAKC,GAAG,KAAKN,OAAOO,4CAA4C,GAAGL;IAErE,OAAO,MAAMD,kCAAkC;QAACG;IAAS;AAC3D"}
|