@shipfox/api-integration-sentry 8.0.0 → 9.0.2

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 (52) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +49 -0
  3. package/README.md +3 -8
  4. package/dist/core/errors.d.ts +1 -1
  5. package/dist/core/errors.d.ts.map +1 -1
  6. package/dist/core/errors.js +1 -1
  7. package/dist/core/errors.js.map +1 -1
  8. package/dist/core/install.d.ts +1 -1
  9. package/dist/core/install.d.ts.map +1 -1
  10. package/dist/core/install.js.map +1 -1
  11. package/dist/core/webhook-processor.d.ts +1 -1
  12. package/dist/core/webhook-processor.d.ts.map +1 -1
  13. package/dist/core/webhook-processor.js +1 -1
  14. package/dist/core/webhook-processor.js.map +1 -1
  15. package/dist/core/webhook.d.ts +1 -1
  16. package/dist/core/webhook.d.ts.map +1 -1
  17. package/dist/core/webhook.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/presentation/dto/integrations.d.ts +6 -7
  22. package/dist/presentation/dto/integrations.d.ts.map +1 -1
  23. package/dist/presentation/dto/integrations.js +12 -4
  24. package/dist/presentation/dto/integrations.js.map +1 -1
  25. package/dist/presentation/routes/errors.js +1 -1
  26. package/dist/presentation/routes/errors.js.map +1 -1
  27. package/dist/presentation/routes/install.d.ts +1 -1
  28. package/dist/presentation/routes/install.d.ts.map +1 -1
  29. package/dist/presentation/routes/install.js.map +1 -1
  30. package/dist/presentation/routes/webhook-context.d.ts +1 -1
  31. package/dist/presentation/routes/webhook-context.d.ts.map +1 -1
  32. package/dist/presentation/routes/webhook-context.js.map +1 -1
  33. package/dist/presentation/routes/webhooks.d.ts.map +1 -1
  34. package/dist/presentation/routes/webhooks.js +1 -1
  35. package/dist/presentation/routes/webhooks.js.map +1 -1
  36. package/dist/tsconfig.test.tsbuildinfo +1 -1
  37. package/package.json +21 -14
  38. package/src/core/errors.ts +1 -1
  39. package/src/core/install.test.ts +1 -1
  40. package/src/core/install.ts +1 -1
  41. package/src/core/webhook-processor.test.ts +1 -4
  42. package/src/core/webhook-processor.ts +5 -5
  43. package/src/core/webhook.ts +2 -2
  44. package/src/index.ts +1 -1
  45. package/src/presentation/dto/integrations.ts +13 -5
  46. package/src/presentation/routes/errors.ts +1 -1
  47. package/src/presentation/routes/install.test.ts +1 -1
  48. package/src/presentation/routes/install.ts +1 -1
  49. package/src/presentation/routes/webhook-context.ts +1 -1
  50. package/src/presentation/routes/webhooks.test.ts +1 -1
  51. package/src/presentation/routes/webhooks.ts +2 -5
  52. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1 +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"}
1
+ {"version":3,"sources":["../../../src/presentation/routes/errors.ts"],"sourcesContent":["import {\n ConnectionSlugConflictError,\n type IntegrationProviderErrorReason,\n} from '@shipfox/api-integration-spi';\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,+BAA+B;AACtC,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"}
@@ -1,4 +1,4 @@
1
- import type { IntegrationConnection } from '@shipfox/api-integration-core-dto';
1
+ import type { IntegrationConnection } from '@shipfox/api-integration-spi';
2
2
  import { type RouteGroup } from '@shipfox/node-fastify';
3
3
  import type { SentryApiClient } from '#api/client.js';
4
4
  import { type ConnectSentryInstallationInput } from '#core/install.js';
@@ -1 +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"}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/install.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,8BAA8B,CAAC;AACxE,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"}
@@ -1 +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"}
1
+ {"version":3,"sources":["../../../src/presentation/routes/install.ts"],"sourcesContent":["import {AUTH_USER, requireUserContext, requireWorkspaceAccess} from '@shipfox/api-auth-context';\nimport {\n createSentryInstallBodySchema,\n createSentryInstallResponseSchema,\n sentryConnectBodySchema,\n sentryConnectResponseSchema,\n} from '@shipfox/api-integration-sentry-dto';\nimport type {IntegrationConnection} from '@shipfox/api-integration-spi';\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;AAChG,SACEC,6BAA6B,EAC7BC,iCAAiC,EACjCC,uBAAuB,EACvBC,2BAA2B,QACtB,sCAAsC;AAE7C,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"}
@@ -1,4 +1,4 @@
1
- import type { GetIntegrationConnectionByIdFn, PublishIntegrationEventReceivedFn, RecordDeliveryOnlyFn, UpdateIntegrationConnectionLifecycleStatusFn } from '@shipfox/api-integration-core-dto';
1
+ import type { GetIntegrationConnectionByIdFn, PublishIntegrationEventReceivedFn, RecordDeliveryOnlyFn, UpdateIntegrationConnectionLifecycleStatusFn } from '@shipfox/api-integration-spi';
2
2
  import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
3
3
  import type { SentryApiClient } from '#api/client.js';
4
4
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-context.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/webhook-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,8BAA8B,EAC9B,iCAAiC,EACjC,oBAAoB,EACpB,4CAA4C,EAC7C,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,MAAM,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,+BAA+B,EAAE,iCAAiC,CAAC;IACnE,kBAAkB,EAAE,oBAAoB,CAAC;IACzC,4BAA4B,EAAE,8BAA8B,CAAC;IAC7D,+BAA+B,EAAE,4CAA4C,CAAC;CAC/E"}
1
+ {"version":3,"file":"webhook-context.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/webhook-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,8BAA8B,EAC9B,iCAAiC,EACjC,oBAAoB,EACpB,4CAA4C,EAC7C,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,MAAM,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,+BAA+B,EAAE,iCAAiC,CAAC;IACnE,kBAAkB,EAAE,oBAAoB,CAAC;IACzC,4BAA4B,EAAE,8BAA8B,CAAC;IAC7D,+BAA+B,EAAE,4CAA4C,CAAC;CAC/E"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/presentation/routes/webhook-context.ts"],"sourcesContent":["import type {\n GetIntegrationConnectionByIdFn,\n PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n UpdateIntegrationConnectionLifecycleStatusFn,\n} from '@shipfox/api-integration-core-dto';\nimport type {NodePgDatabase} from 'drizzle-orm/node-postgres';\nimport type {SentryApiClient} from '#api/client.js';\n\n/**\n * The capabilities a Sentry webhook handler needs from the rest of the system.\n * `@shipfox/api-integration-core` owns and wires the core persistence in\n * production; tests fake them with spies. Passing them in keeps the route\n * decoupled from core's persistence and avoids a package dependency cycle. The\n * Sentry client is injected too so the authoritative `installation.created` path\n * can exchange the code, and tests can fake the exchange.\n */\nexport interface SentryWebhookContext {\n sentry: SentryApiClient;\n coreDb: () => NodePgDatabase<Record<string, unknown>>;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n updateConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;\n}\n"],"names":[],"mappings":"AASA;;;;;;;CAOC,GACD,WAOC"}
1
+ {"version":3,"sources":["../../../src/presentation/routes/webhook-context.ts"],"sourcesContent":["import type {\n GetIntegrationConnectionByIdFn,\n PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n UpdateIntegrationConnectionLifecycleStatusFn,\n} from '@shipfox/api-integration-spi';\nimport type {NodePgDatabase} from 'drizzle-orm/node-postgres';\nimport type {SentryApiClient} from '#api/client.js';\n\n/**\n * The capabilities a Sentry webhook handler needs from the rest of the system.\n * `@shipfox/api-integration-core` owns and wires the core persistence in\n * production; tests fake them with spies. Passing them in keeps the route\n * decoupled from core's persistence and avoids a package dependency cycle. The\n * Sentry client is injected too so the authoritative `installation.created` path\n * can exchange the code, and tests can fake the exchange.\n */\nexport interface SentryWebhookContext {\n sentry: SentryApiClient;\n coreDb: () => NodePgDatabase<Record<string, unknown>>;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n updateConnectionLifecycleStatus: UpdateIntegrationConnectionLifecycleStatusFn;\n}\n"],"names":[],"mappings":"AASA;;;;;;;CAOC,GACD,WAOC"}
@@ -1 +1 @@
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
+ {"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/webhooks.ts"],"names":[],"mappings":"AAEA,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,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { createStoredWebhookRequest, WEBHOOK_MAX_RAW_BODY_BYTES } from '@shipfox/api-integration-core-dto';
2
+ import { createStoredWebhookRequest, WEBHOOK_MAX_RAW_BODY_BYTES } from '@shipfox/api-integration-spi';
3
3
  import { ClientError, defineRoute, rawBodyPlugin, WEBHOOK_BODY_LIMIT } from '@shipfox/node-fastify';
4
4
  import { createSentryWebhookProcessor } from '#core/webhook-processor.js';
5
5
  export function createSentryWebhookRoutes(context) {
@@ -1 +1 @@
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
+ {"version":3,"sources":["../../../src/presentation/routes/webhooks.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport {createStoredWebhookRequest, WEBHOOK_MAX_RAW_BODY_BYTES} from '@shipfox/api-integration-spi';\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-spi').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,SAAQC,0BAA0B,EAAEC,0BAA0B,QAAO,+BAA+B;AACpG,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,MAAsE,EACtEG,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"}