|
| 1 | +import { Router } from 'express' |
| 2 | + |
| 3 | +import { createRateLimiter } from '@/api/apiRateLimiter' |
| 4 | +import { requireScopes } from '@/api/public/middlewares/requireScopes' |
| 5 | +import { safeWrap } from '@/middlewares/errorMiddleware' |
| 6 | +import { SCOPES } from '@/security/scopes' |
| 7 | + |
| 8 | +import { getAkritesExternalAdvisoryDetail } from '../packages/getAkritesExternalAdvisoryDetail' |
| 9 | +import { getAkritesExternalAdvisoryDetailBatch } from '../packages/getAkritesExternalAdvisoryDetailBatch' |
| 10 | +import { getAkritesExternalContactDetail } from '../packages/getAkritesExternalContactDetail' |
| 11 | +import { getAkritesExternalContactDetailBatch } from '../packages/getAkritesExternalContactDetailBatch' |
| 12 | +import { getAkritesExternalPackageDetail } from '../packages/getAkritesExternalPackageDetail' |
| 13 | +import { getAkritesExternalPackageDetailBatch } from '../packages/getAkritesExternalPackageDetailBatch' |
| 14 | +import { submitBlastRadiusJob } from '../packages/submitBlastRadiusJob' |
| 15 | + |
| 16 | +const rateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 }) |
| 17 | + |
| 18 | +// Blast-radius jobs kick off a Temporal workflow per request, so they get their own, |
| 19 | +// much stricter limiter — configurable via env so it can be tuned without a redeploy |
| 20 | +// while the pipeline is still a no-op stub. Defaults to 5 requests/hour. |
| 21 | +const blastRadiusRateLimitMax = Number(process.env.AKRITES_BLAST_RADIUS_RATE_LIMIT_MAX) |
| 22 | +const blastRadiusRateLimitWindowMs = Number(process.env.AKRITES_BLAST_RADIUS_RATE_LIMIT_WINDOW_MS) |
| 23 | + |
| 24 | +const blastRadiusRateLimiter = createRateLimiter({ |
| 25 | + max: |
| 26 | + Number.isSafeInteger(blastRadiusRateLimitMax) && blastRadiusRateLimitMax > 0 |
| 27 | + ? blastRadiusRateLimitMax |
| 28 | + : 5, |
| 29 | + windowMs: |
| 30 | + Number.isSafeInteger(blastRadiusRateLimitWindowMs) && blastRadiusRateLimitWindowMs > 0 |
| 31 | + ? blastRadiusRateLimitWindowMs |
| 32 | + : 60 * 60 * 1000, |
| 33 | +}) |
| 34 | + |
| 35 | +export function akritesExternalRouter(): Router { |
| 36 | + const router = Router() |
| 37 | + |
| 38 | + // TODO: swap for a dedicated cdp:packages:read scope once Akrites gets its own |
| 39 | + // Auth0 M2M scopes (per the akrites-external draft contract) — reusing the |
| 40 | + // internal CDP UI scopes for now since that's what's actually issued today. |
| 41 | + const packagesSubRouter = Router() |
| 42 | + packagesSubRouter.use(rateLimiter) |
| 43 | + packagesSubRouter.use(requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all')) |
| 44 | + packagesSubRouter.get('/detail', safeWrap(getAkritesExternalPackageDetail)) |
| 45 | + packagesSubRouter.post(/^\/detail:batch\/?$/, safeWrap(getAkritesExternalPackageDetailBatch)) |
| 46 | + router.use('/packages', packagesSubRouter) |
| 47 | + |
| 48 | + // TODO: the contract gates advisories behind a dedicated read:advisories scope |
| 49 | + // (see the scope-naming note in the akrites-external OpenAPI). That scope isn't |
| 50 | + // issued by Auth0 yet, so reuse READ_PACKAGES for now — advisories are package |
| 51 | + // security data and, unlike the packages endpoints above, need no stewardship read. |
| 52 | + const advisoriesSubRouter = Router() |
| 53 | + advisoriesSubRouter.use(rateLimiter) |
| 54 | + advisoriesSubRouter.use(requireScopes([SCOPES.READ_PACKAGES])) |
| 55 | + advisoriesSubRouter.get('/detail', safeWrap(getAkritesExternalAdvisoryDetail)) |
| 56 | + advisoriesSubRouter.post(/^\/detail:batch\/?$/, safeWrap(getAkritesExternalAdvisoryDetailBatch)) |
| 57 | + router.use('/advisories', advisoriesSubRouter) |
| 58 | + |
| 59 | + // Security contacts expose contact PII (e.g. reporter emails), so the contract gates |
| 60 | + // them behind a dedicated cdp:maintainers:read scope and explicitly forbids reaching |
| 61 | + // them via the packages scope. That scope isn't issued by Auth0 yet, so reuse the |
| 62 | + // closest issued one — READ_MAINTAINER_ROLES (maintainer data) — NOT READ_PACKAGES. |
| 63 | + // TODO: swap for cdp:maintainers:read once issued. |
| 64 | + const contactsSubRouter = Router() |
| 65 | + contactsSubRouter.use(rateLimiter) |
| 66 | + contactsSubRouter.use(requireScopes([SCOPES.READ_MAINTAINER_ROLES])) |
| 67 | + contactsSubRouter.get('/detail', safeWrap(getAkritesExternalContactDetail)) |
| 68 | + contactsSubRouter.post(/^\/detail:batch\/?$/, safeWrap(getAkritesExternalContactDetailBatch)) |
| 69 | + router.use('/contacts', contactsSubRouter) |
| 70 | + |
| 71 | + // TODO: the contract gates blast-radius behind a dedicated read:advisories scope |
| 72 | + // (same as advisories above — see the scope-naming note in the akrites-external |
| 73 | + // OpenAPI). Not issued by Auth0 yet, so reuse READ_PACKAGES for now. |
| 74 | + const blastRadiusSubRouter = Router() |
| 75 | + blastRadiusSubRouter.use(blastRadiusRateLimiter) |
| 76 | + blastRadiusSubRouter.use(requireScopes([SCOPES.READ_PACKAGES])) |
| 77 | + blastRadiusSubRouter.post('/jobs', safeWrap(submitBlastRadiusJob)) |
| 78 | + router.use('/blast-radius', blastRadiusSubRouter) |
| 79 | + |
| 80 | + return router |
| 81 | +} |
0 commit comments