Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions backend/src/api/public/v1/akrites-external/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,23 @@ import { getAkritesExternalContactDetailBatch } from '../packages/getAkritesExte
import { getAkritesExternalPackageDetail } from '../packages/getAkritesExternalPackageDetail'
import { getAkritesExternalPackageDetailBatch } from '../packages/getAkritesExternalPackageDetailBatch'
import { getBlastRadiusJob } from '../packages/getBlastRadiusJob'
import { getBlastRadiusJobBatch } from '../packages/getBlastRadiusJobBatch'
import { submitBlastRadiusJob } from '../packages/submitBlastRadiusJob'
import { submitBlastRadiusJobBatch } from '../packages/submitBlastRadiusJobBatch'

const rateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 })

// Blast-radius jobs kick off a Temporal workflow per request, so they get their own,
// much stricter limiter — configurable via env so it can be tuned without a redeploy.
// Defaults to 5 requests/hour.
// Defaults to 50 requests/hour.
const blastRadiusRateLimitMax = Number(process.env.AKRITES_BLAST_RADIUS_RATE_LIMIT_MAX)
const blastRadiusRateLimitWindowMs = Number(process.env.AKRITES_BLAST_RADIUS_RATE_LIMIT_WINDOW_MS)

const blastRadiusRateLimiter = createRateLimiter({
max:
Number.isSafeInteger(blastRadiusRateLimitMax) && blastRadiusRateLimitMax > 0
? blastRadiusRateLimitMax
: 5,
: 50,
windowMs:
Number.isSafeInteger(blastRadiusRateLimitWindowMs) && blastRadiusRateLimitWindowMs > 0
? blastRadiusRateLimitWindowMs
Expand Down Expand Up @@ -75,7 +77,22 @@ export function akritesExternalRouter(): Router {
const blastRadiusSubRouter = Router()
blastRadiusSubRouter.use(requireScopes([SCOPES.READ_PACKAGES]))
blastRadiusSubRouter.post('/jobs', blastRadiusRateLimiter, safeWrap(submitBlastRadiusJob))
// Bulk submit multiplies Temporal workflow starts per request (up to
// MAX_BLAST_RADIUS_JOBS_PER_BATCH), so it sits behind the same strict
// blastRadiusRateLimiter as the single-job route, not the regular one.
blastRadiusSubRouter.post(
/^\/jobs:batch\/?$/,
blastRadiusRateLimiter,
safeWrap(submitBlastRadiusJobBatch),
)
blastRadiusSubRouter.get('/jobs/:analysisId', rateLimiter, safeWrap(getBlastRadiusJob))
// Bulk poll is read-only, same cost profile as the other batch endpoints, so
// it uses the regular rateLimiter.
blastRadiusSubRouter.post(
/^\/jobs:batch\/poll\/?$/,
rateLimiter,
safeWrap(getBlastRadiusJobBatch),
)
router.use('/blast-radius', blastRadiusSubRouter)

return router
Expand Down
229 changes: 212 additions & 17 deletions backend/src/api/public/v1/akrites-external/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ info:
Packages, Advisories and Contacts endpoints are implemented. Blast Radius
submit (2a) and poll (2b) are both implemented, backed by a 4-stage
Temporal pipeline (intel, dependents, reachability, report) for npm
packages; other ecosystems fail fast with ECOSYSTEM_NOT_SUPPORTED. The
7-day result cache is specced separately and not yet built.
packages; other ecosystems fail fast with ECOSYSTEM_NOT_SUPPORTED. Submit
reuses a 'done' analysis for the same advisory/package/ecosystem if it
completed within the last day (configurable, see the `force` field below)
instead of starting a new Temporal workflow.


TODO: scopes below (read:packages, read:stewardships) are the existing
Expand Down Expand Up @@ -51,14 +53,17 @@ tags:
- name: Blast Radius
description: >
Advisory reachability analysis — submit (2a) and poll (2b) are both
implemented. Submitting kicks off a Temporal workflow that runs the
npm reachability pipeline (other ecosystems fail fast with
ECOSYSTEM_NOT_SUPPORTED); poll returns job status and, once done,
results. Rate-limited independently of the other akrites-external
endpoints (default 5 requests/hour, configurable via
AKRITES_BLAST_RADIUS_RATE_LIMIT_MAX / _WINDOW_MS). Same interim scope
note as Advisories applies (read:packages, pending a dedicated
read:advisories scope).
implemented, each with a bulk counterpart. Submitting kicks off a
Temporal workflow that runs the npm reachability pipeline (other
ecosystems fail fast with ECOSYSTEM_NOT_SUPPORTED); poll returns job
status and, once done, results. Bulk submit (jobs:batch) is capped at
20 jobs per request (10 recommended as the default batch size) — each
entry starts its own workflow — and stays behind the same strict rate
limiter as the single-job route, defaulting to 50 requests/hour
(configurable via AKRITES_BLAST_RADIUS_RATE_LIMIT_MAX / _WINDOW_MS);
Comment on lines 59 to +63
bulk poll (jobs:batch/poll) is read-only and capped at 100 like the
other batch endpoints. Same interim scope note as Advisories applies
(read:packages, pending a dedicated read:advisories scope).

components:
securitySchemes:
Expand Down Expand Up @@ -404,12 +409,18 @@ components:
force:
type: boolean
default: false
description: Bypasses the 7-day cache and always triggers a new run. Use sparingly.
description: >
Bypasses the advisory cache (a 'done' analysis for the same
advisory/package/ecosystem completed within the last day, by
default) and always triggers a new run. Use sparingly.

BlastRadiusJobEntry:
type: object
required: [analysisId, advisoryId, package, ecosystem, status]
description: Response body of 2a — one job per request, never wrapped in an array.
description: >
Response body of 2a for a single submitted job. Returned directly (not
wrapped in an array) by the single-job submit; the batch submit (2a bulk)
returns an array of these under `results` — see BlastRadiusJobBatchResponse.
properties:
analysisId:
type: string
Expand All @@ -427,7 +438,76 @@ components:
status:
type: string
enum: [pending, running, done, failed]
description: Always pending — the response is returned before the Temporal workflow runs.
description: >
Pending when a job is freshly submitted, returned before the Temporal
workflow runs. Done when the advisory cache is reused instead — see
force above — in which case analysisId is the cached analysis's own
id, already completed. In the batch submit response, a job whose
workflow failed to start comes back as failed instead — the rest of
the batch is unaffected.

BlastRadiusJobBatchRequest:
type: object
required: [jobs]
properties:
jobs:
type: array
minItems: 1
maxItems: 20
description: >
Capped much lower than the 100-item read batches — each entry
starts its own Temporal workflow, so the batch multiplies
workflow starts (and reachability-analysis cost) per request.
10 is the recommended default batch size; 20 is the hard limit.
items:
$ref: '#/components/schemas/BlastRadiusJobRequest'

BlastRadiusJobBatchResponse:
type: object
required: [results]
description: >
Plain array in request order, one entry per submitted job — unlike the
read batches there is no found/not-found case, every job is submitted.
properties:
results:
type: array
items:
$ref: '#/components/schemas/BlastRadiusJobEntry'

BlastRadiusJobPollBatchRequest:
type: object
required: [analysisIds]
properties:
analysisIds:
type: array
minItems: 1
maxItems: 100
items:
type: string
format: uuid
page:
type: integer
minimum: 1
default: 1
pageSize:
type: integer
minimum: 1
maximum: 100
default: 20

BlastRadiusAnalysisBulkEntry:
type: object
required: [requestedAnalysisId, found, analysis]
properties:
requestedAnalysisId:
type: string
found:
type: boolean
analysis:
type: object
nullable: true
allOf:
- $ref: '#/components/schemas/BlastRadiusAnalysis'

BlastRadiusResultConfidence:
type: string
Expand Down Expand Up @@ -995,10 +1075,9 @@ paths:
Starts a Temporal workflow running the 4-stage reachability pipeline
(intel, dependents, reachability, report) for npm; other ecosystems
fail fast with ECOSYSTEM_NOT_SUPPORTED. Poll status/results via
GET /jobs/{analysisId}.


Not yet implemented: the 7-day result cache and force-bypass semantics.
GET /jobs/{analysisId}. Reuses a 'done' analysis for the same
advisory/package/ecosystem completed within the last day (by
default) instead of starting a new workflow, unless force is true.
tags: [Blast Radius]
security:
- M2MBearer:
Expand Down Expand Up @@ -1045,6 +1124,122 @@ paths:
schema:
$ref: '#/components/schemas/Error'

/akrites-external/blast-radius/jobs:batch:
post:
operationId: submitBlastRadiusJobBatch
summary: 2a bulk — Submit multiple blast-radius analysis jobs
description: >
One job per array entry, same semantics as the single-job submit —
omit package for an advisory-wide analysis, provide it to narrow to a
single package. Each entry starts its own Temporal workflow, so the
batch is capped at 20 jobs (10 recommended as the default batch
size), much lower than the 100-item read batches, and stays behind
the same strict rate limiter as the single-job route.


A per-job failure does not fail the whole batch — that entry comes
back with status: 'failed' and the rest still submit.
tags: [Blast Radius]
security:
- M2MBearer:
- read:packages
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BlastRadiusJobBatchRequest'
responses:
'202':
description: Jobs accepted, in request order.
content:
application/json:
schema:
$ref: '#/components/schemas/BlastRadiusJobBatchResponse'
'400':
description: Validation error (empty array, >20 items, or an invalid job entry).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Missing or invalid bearer token.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: Token missing read:packages scope.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Too many requests — rate-limited independently of the other endpoints.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'

/akrites-external/blast-radius/jobs:batch/poll:
post:
operationId: getBlastRadiusJobBatch
summary: 2b bulk — Poll multiple blast-radius analysis jobs
description: >
Same found/not-found echo shape as the other batch endpoints: an
unknown analysisId comes back { found: false, analysis: null }
instead of 404ing the whole request. Read-only, so it is rate-limited
the same as the other non-blast-radius endpoints, not the strict
submit limiter.
tags: [Blast Radius]
security:
- M2MBearer:
- read:packages
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BlastRadiusJobPollBatchRequest'
responses:
'200':
description: One page of results, in request order.
content:
application/json:
schema:
type: object
required: [page, pageSize, total, results]
properties:
page:
type: integer
pageSize:
type: integer
total:
type: integer
description: Total number of requested analysisIds, across all pages.
results:
type: array
items:
$ref: '#/components/schemas/BlastRadiusAnalysisBulkEntry'
'400':
description: Validation error (empty array, >100 items, or a non-uuid analysisId).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Missing or invalid bearer token.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: Token missing read:packages scope.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'

/akrites-external/blast-radius/jobs/{analysisId}:
get:
operationId: getBlastRadiusJob
Expand Down
Loading
Loading