Skip to content

Commit 285e71e

Browse files
committed
Merge branch 'main' into CM-1349-dedupe-public-api-conflict-alerts
2 parents 92e0dc0 + e8081d8 commit 285e71e

130 files changed

Lines changed: 5755 additions & 723 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/src/api/public/v1/akrites-external/index.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import { getAkritesExternalContactDetailBatch } from '../packages/getAkritesExte
1212
import { getAkritesExternalPackageDetail } from '../packages/getAkritesExternalPackageDetail'
1313
import { getAkritesExternalPackageDetailBatch } from '../packages/getAkritesExternalPackageDetailBatch'
1414
import { getBlastRadiusJob } from '../packages/getBlastRadiusJob'
15+
import { getBlastRadiusJobBatch } from '../packages/getBlastRadiusJobBatch'
1516
import { ingestAkritesExternalContactDetail } from '../packages/ingestAkritesExternalContactDetail'
1617
import { submitBlastRadiusJob } from '../packages/submitBlastRadiusJob'
18+
import { submitBlastRadiusJobBatch } from '../packages/submitBlastRadiusJobBatch'
1719

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

@@ -29,7 +31,7 @@ function envTunableRateLimiter(envPrefix: string, defaultMax: number, defaultWin
2931
})
3032
}
3133

32-
// Blast-radius jobs default to 5 requests/hour.
34+
// Blast-radius jobs default to 50 requests/hour.
3335
const blastRadiusRateLimiter = envTunableRateLimiter(
3436
'AKRITES_BLAST_RADIUS_RATE_LIMIT',
3537
5,
@@ -110,7 +112,22 @@ export function akritesExternalRouter(): Router {
110112
const blastRadiusSubRouter = Router()
111113
blastRadiusSubRouter.use(requireScopes([SCOPES.READ_PACKAGES]))
112114
blastRadiusSubRouter.post('/jobs', blastRadiusRateLimiter, safeWrap(submitBlastRadiusJob))
115+
// Bulk submit multiplies Temporal workflow starts per request (up to
116+
// MAX_BLAST_RADIUS_JOBS_PER_BATCH), so it sits behind the same strict
117+
// blastRadiusRateLimiter as the single-job route, not the regular one.
118+
blastRadiusSubRouter.post(
119+
/^\/jobs:batch\/?$/,
120+
blastRadiusRateLimiter,
121+
safeWrap(submitBlastRadiusJobBatch),
122+
)
113123
blastRadiusSubRouter.get('/jobs/:analysisId', rateLimiter, safeWrap(getBlastRadiusJob))
124+
// Bulk poll is read-only, same cost profile as the other batch endpoints, so
125+
// it uses the regular rateLimiter.
126+
blastRadiusSubRouter.post(
127+
/^\/jobs:batch\/poll\/?$/,
128+
rateLimiter,
129+
safeWrap(getBlastRadiusJobBatch),
130+
)
114131
router.use('/blast-radius', blastRadiusSubRouter)
115132

116133
return router

backend/src/api/public/v1/akrites-external/openapi.yaml

Lines changed: 199 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,16 @@ tags:
5151
- name: Blast Radius
5252
description: >
5353
Advisory reachability analysis — submit (2a) and poll (2b) are both
54-
implemented. Submitting kicks off a Temporal workflow that runs the
55-
npm reachability pipeline (other ecosystems fail fast with
56-
ECOSYSTEM_NOT_SUPPORTED); poll returns job status and, once done,
57-
results. Rate-limited independently of the other akrites-external
58-
endpoints (default 5 requests/hour, configurable via
59-
AKRITES_BLAST_RADIUS_RATE_LIMIT_MAX / _WINDOW_MS). Same interim scope
60-
note as Advisories applies (read:packages, pending a dedicated
61-
read:advisories scope).
54+
implemented, each with a bulk counterpart. Submitting kicks off a
55+
Temporal workflow that runs the npm reachability pipeline (other
56+
ecosystems fail fast with ECOSYSTEM_NOT_SUPPORTED); poll returns job
57+
status and, once done, results. Bulk submit (jobs:batch) is capped at
58+
20 jobs per request (10 recommended as the default batch size) — each
59+
entry starts its own workflow — and stays behind the same strict rate
60+
limiter as the single-job route; bulk poll
61+
(jobs:batch/poll) is read-only and capped at 100 like the other batch
62+
endpoints. Same interim scope note as Advisories applies (read:packages,
63+
pending a dedicated read:advisories scope).
6264
6365
components:
6466
securitySchemes:
@@ -409,7 +411,10 @@ components:
409411
BlastRadiusJobEntry:
410412
type: object
411413
required: [analysisId, advisoryId, package, ecosystem, status]
412-
description: Response body of 2a — one job per request, never wrapped in an array.
414+
description: >
415+
Response body of 2a for a single submitted job. Returned directly (not
416+
wrapped in an array) by the single-job submit; the batch submit (2a bulk)
417+
returns an array of these under `results` — see BlastRadiusJobBatchResponse.
413418
properties:
414419
analysisId:
415420
type: string
@@ -427,7 +432,75 @@ components:
427432
status:
428433
type: string
429434
enum: [pending, running, done, failed]
430-
description: Always pending — the response is returned before the Temporal workflow runs.
435+
description: >
436+
Pending in the single-job submit response, always returned before the
437+
Temporal workflow runs. In the batch submit response, a job whose
438+
workflow failed to start comes back as failed instead — the rest of
439+
the batch is unaffected.
440+
441+
BlastRadiusJobBatchRequest:
442+
type: object
443+
required: [jobs]
444+
properties:
445+
jobs:
446+
type: array
447+
minItems: 1
448+
maxItems: 20
449+
description: >
450+
Capped much lower than the 100-item read batches — each entry
451+
starts its own Temporal workflow, so the batch multiplies
452+
workflow starts (and reachability-analysis cost) per request.
453+
10 is the recommended default batch size; 20 is the hard limit.
454+
items:
455+
$ref: '#/components/schemas/BlastRadiusJobRequest'
456+
457+
BlastRadiusJobBatchResponse:
458+
type: object
459+
required: [results]
460+
description: >
461+
Plain array in request order, one entry per requested job — unlike the
462+
read batches there is no found/not-found case, but a job can still come
463+
back with status: failed if row creation or the workflow start failed.
464+
properties:
465+
results:
466+
type: array
467+
items:
468+
$ref: '#/components/schemas/BlastRadiusJobEntry'
469+
470+
BlastRadiusJobPollBatchRequest:
471+
type: object
472+
required: [analysisIds]
473+
properties:
474+
analysisIds:
475+
type: array
476+
minItems: 1
477+
maxItems: 100
478+
items:
479+
type: string
480+
format: uuid
481+
page:
482+
type: integer
483+
minimum: 1
484+
default: 1
485+
pageSize:
486+
type: integer
487+
minimum: 1
488+
maximum: 100
489+
default: 20
490+
491+
BlastRadiusAnalysisBulkEntry:
492+
type: object
493+
required: [requestedAnalysisId, found, analysis]
494+
properties:
495+
requestedAnalysisId:
496+
type: string
497+
found:
498+
type: boolean
499+
analysis:
500+
type: object
501+
nullable: true
502+
allOf:
503+
- $ref: '#/components/schemas/BlastRadiusAnalysis'
431504

432505
BlastRadiusResultConfidence:
433506
type: string
@@ -1070,6 +1143,122 @@ paths:
10701143
schema:
10711144
$ref: '#/components/schemas/Error'
10721145

1146+
/akrites-external/blast-radius/jobs:batch:
1147+
post:
1148+
operationId: submitBlastRadiusJobBatch
1149+
summary: 2a bulk — Submit multiple blast-radius analysis jobs
1150+
description: >
1151+
One job per array entry, same semantics as the single-job submit —
1152+
omit package for an advisory-wide analysis, provide it to narrow to a
1153+
single package. Each entry starts its own Temporal workflow, so the
1154+
batch is capped at 20 jobs (10 recommended as the default batch
1155+
size), much lower than the 100-item read batches, and stays behind
1156+
the same strict rate limiter as the single-job route.
1157+
1158+
1159+
A per-job failure does not fail the whole batch — that entry comes
1160+
back with status: 'failed' and the rest still submit.
1161+
tags: [Blast Radius]
1162+
security:
1163+
- M2MBearer:
1164+
- read:packages
1165+
requestBody:
1166+
required: true
1167+
content:
1168+
application/json:
1169+
schema:
1170+
$ref: '#/components/schemas/BlastRadiusJobBatchRequest'
1171+
responses:
1172+
'202':
1173+
description: Jobs accepted, in request order.
1174+
content:
1175+
application/json:
1176+
schema:
1177+
$ref: '#/components/schemas/BlastRadiusJobBatchResponse'
1178+
'400':
1179+
description: Validation error (empty array, >20 items, or an invalid job entry).
1180+
content:
1181+
application/json:
1182+
schema:
1183+
$ref: '#/components/schemas/Error'
1184+
'401':
1185+
description: Missing or invalid bearer token.
1186+
content:
1187+
application/json:
1188+
schema:
1189+
$ref: '#/components/schemas/Error'
1190+
'403':
1191+
description: Token missing read:packages scope.
1192+
content:
1193+
application/json:
1194+
schema:
1195+
$ref: '#/components/schemas/Error'
1196+
'429':
1197+
description: Too many requests — rate-limited independently of the other endpoints.
1198+
content:
1199+
application/json:
1200+
schema:
1201+
$ref: '#/components/schemas/Error'
1202+
1203+
/akrites-external/blast-radius/jobs:batch/poll:
1204+
post:
1205+
operationId: getBlastRadiusJobBatch
1206+
summary: 2b bulk — Poll multiple blast-radius analysis jobs
1207+
description: >
1208+
Same found/not-found echo shape as the other batch endpoints: an
1209+
unknown analysisId comes back { found: false, analysis: null }
1210+
instead of 404ing the whole request. Read-only, so it is rate-limited
1211+
the same as the other non-blast-radius endpoints, not the strict
1212+
submit limiter.
1213+
tags: [Blast Radius]
1214+
security:
1215+
- M2MBearer:
1216+
- read:packages
1217+
requestBody:
1218+
required: true
1219+
content:
1220+
application/json:
1221+
schema:
1222+
$ref: '#/components/schemas/BlastRadiusJobPollBatchRequest'
1223+
responses:
1224+
'200':
1225+
description: One page of results, in request order.
1226+
content:
1227+
application/json:
1228+
schema:
1229+
type: object
1230+
required: [page, pageSize, total, results]
1231+
properties:
1232+
page:
1233+
type: integer
1234+
pageSize:
1235+
type: integer
1236+
total:
1237+
type: integer
1238+
description: Total number of requested analysisIds, across all pages.
1239+
results:
1240+
type: array
1241+
items:
1242+
$ref: '#/components/schemas/BlastRadiusAnalysisBulkEntry'
1243+
'400':
1244+
description: Validation error (empty array, >100 items, or a non-uuid analysisId).
1245+
content:
1246+
application/json:
1247+
schema:
1248+
$ref: '#/components/schemas/Error'
1249+
'401':
1250+
description: Missing or invalid bearer token.
1251+
content:
1252+
application/json:
1253+
schema:
1254+
$ref: '#/components/schemas/Error'
1255+
'403':
1256+
description: Token missing read:packages scope.
1257+
content:
1258+
application/json:
1259+
schema:
1260+
$ref: '#/components/schemas/Error'
1261+
10731262
/akrites-external/blast-radius/jobs/{analysisId}:
10741263
get:
10751264
operationId: getBlastRadiusJob

backend/src/api/public/v1/members/createMember.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,6 @@ export async function createMember(req: Request, res: Response): Promise<void> {
5555
identities.map((identity) => ({
5656
...identity,
5757
memberId: dbMember.id,
58-
value:
59-
identity.type === MemberIdentityType.EMAIL
60-
? identity.value.trim().toLowerCase()
61-
: identity.value.trim(),
6258
})),
6359
true,
6460
true,

backend/src/api/public/v1/members/identities/createMemberIdentity.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,6 @@ export async function createMemberIdentity(req: Request, res: Response): Promise
4646
throw new NotFoundError('Member not found')
4747
}
4848

49-
// Normalize emails to lowercase; keep username preferred casing from the caller.
50-
const normalizedValue =
51-
data.type === MemberIdentityType.EMAIL ? data.value.trim().toLowerCase() : data.value.trim()
52-
5349
let result!: IMemberIdentity
5450
let alreadyExisted = false
5551

@@ -59,7 +55,7 @@ export async function createMemberIdentity(req: Request, res: Response): Promise
5955
captureOldState({})
6056

6157
await qx.tx(async (tx) => {
62-
const existing = await findMemberIdentitiesByValue(tx, memberId, normalizedValue, {
58+
const existing = await findMemberIdentitiesByValue(tx, memberId, data.value, {
6359
type: data.type,
6460
})
6561
const exactMatch = existing.find((i) => i.platform === data.platform)
@@ -75,7 +71,7 @@ export async function createMemberIdentity(req: Request, res: Response): Promise
7571
{
7672
memberId,
7773
platform: data.platform,
78-
value: normalizedValue,
74+
value: data.value,
7975
type: data.type,
8076
source: data.source,
8177
verified: data.verified,
@@ -108,7 +104,7 @@ export async function createMemberIdentity(req: Request, res: Response): Promise
108104
rethrowIdentityConflict(req, error, {
109105
memberId,
110106
platform: data.platform,
111-
value: normalizedValue,
107+
value: data.value,
112108
type: data.type,
113109
})
114110
}

0 commit comments

Comments
 (0)