Skip to content

Commit bad167d

Browse files
committed
fix: update the endpoint
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent 7009346 commit bad167d

4 files changed

Lines changed: 41 additions & 31 deletions

File tree

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

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ 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 { refreshAkritesExternalContactDetail } from '../packages/refreshAkritesExternalContactDetail'
15+
import { ingestAkritesExternalContactDetail } from '../packages/ingestAkritesExternalContactDetail'
1616
import { submitBlastRadiusJob } from '../packages/submitBlastRadiusJob'
1717

1818
const rateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 })
@@ -36,10 +36,10 @@ const blastRadiusRateLimiter = envTunableRateLimiter(
3636
60 * 60 * 1000,
3737
)
3838

39-
// /contacts/refresh blocks ~10-20s per request (vs. the read-only /contacts/detail
39+
// /contacts/ingest blocks ~10-20s per request (vs. the read-only /contacts/detail
4040
// endpoints), so it gets its own limiter. Defaults to 20 requests/hour.
41-
const contactRefreshRateLimiter = envTunableRateLimiter(
42-
'AKRITES_CONTACT_REFRESH_RATE_LIMIT',
41+
const contactIngestRateLimiter = envTunableRateLimiter(
42+
'AKRITES_CONTACT_INGEST_RATE_LIMIT',
4343
20,
4444
60 * 60 * 1000,
4545
)
@@ -74,19 +74,18 @@ export function akritesExternalRouter(): Router {
7474
// closest issued one — READ_MAINTAINER_ROLES (maintainer data) — NOT READ_PACKAGES.
7575
// TODO: swap for cdp:maintainers:read once issued.
7676
const contactsSubRouter = Router()
77+
// rateLimiter first so requests failing the scope check still count against the quota
78+
// (throttles repeated invalid-auth attempts, not just successful ones).
79+
contactsSubRouter.use(rateLimiter)
7780
contactsSubRouter.use(requireScopes([SCOPES.READ_MAINTAINER_ROLES]))
78-
contactsSubRouter.get('/detail', rateLimiter, safeWrap(getAkritesExternalContactDetail))
79-
contactsSubRouter.post(
80-
/^\/detail:batch\/?$/,
81-
rateLimiter,
82-
safeWrap(getAkritesExternalContactDetailBatch),
83-
)
81+
contactsSubRouter.get('/detail', safeWrap(getAkritesExternalContactDetail))
82+
contactsSubRouter.post(/^\/detail:batch\/?$/, safeWrap(getAkritesExternalContactDetailBatch))
8483
// Sync, single-purl on-demand ingest — starts a Temporal workflow and blocks ~10-20s,
85-
// so it gets the dedicated contactRefreshRateLimiter, not the shared rateLimiter above.
84+
// so it stacks the dedicated contactIngestRateLimiter on top of the shared rateLimiter above.
8685
contactsSubRouter.post(
87-
'/refresh',
88-
contactRefreshRateLimiter,
89-
safeWrap(refreshAkritesExternalContactDetail),
86+
'/ingest',
87+
contactIngestRateLimiter,
88+
safeWrap(ingestAkritesExternalContactDetail),
9089
)
9190
router.use('/contacts', contactsSubRouter)
9291

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,9 +1247,9 @@ paths:
12471247
schema:
12481248
$ref: '#/components/schemas/Error'
12491249

1250-
/akrites-external/contacts/refresh:
1250+
/akrites-external/contacts/ingest:
12511251
post:
1252-
operationId: refreshContactDetail
1252+
operationId: ingestContactDetail
12531253
summary: On-demand security-contacts ingest for a single PURL
12541254
description: >
12551255
Synchronous, single-purl only — no batch variant, since fanning this out over
@@ -1260,9 +1260,13 @@ paths:
12601260
same purl attach to the same running workflow instead of starting duplicates.
12611261
12621262
1263+
Note: the underlying workflow only ingests contacts it has never seen before —
1264+
it does not refresh contacts already ingested for a purl.
1265+
1266+
12631267
Rate limited independently of the other /contacts endpoints — default
1264-
20 requests/hour, tunable via AKRITES_CONTACT_REFRESH_RATE_LIMIT_MAX /
1265-
AKRITES_CONTACT_REFRESH_RATE_LIMIT_WINDOW_MS.
1268+
20 requests/hour, tunable via AKRITES_CONTACT_INGEST_RATE_LIMIT_MAX /
1269+
AKRITES_CONTACT_INGEST_RATE_LIMIT_WINDOW_MS.
12661270
12671271
12681272
Not yet implemented: this is a synchronous, blocking call. Future work should

backend/src/api/public/v1/packages/refreshAkritesExternalContactDetail.test.ts renamed to backend/src/api/public/v1/packages/ingestAkritesExternalContactDetail.test.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import type { Request, Response } from 'express'
22
import { describe, expect, it, vi } from 'vitest'
33

44
import type { AkritesExternalContactDetailRow } from '@crowd/data-access-layer'
5+
import { WorkflowIdConflictPolicy, WorkflowIdReusePolicy } from '@crowd/temporal'
56

6-
import { refreshAkritesExternalContactDetail } from './refreshAkritesExternalContactDetail'
7+
import { ingestAkritesExternalContactDetail } from './ingestAkritesExternalContactDetail'
78

89
const { execute, getContactDetailsByPurls } = vi.hoisted(() => ({
910
execute: vi.fn(),
@@ -54,20 +55,22 @@ function mockReqRes(body: unknown) {
5455
return { req, res, status, json }
5556
}
5657

57-
describe('refreshAkritesExternalContactDetail', () => {
58+
describe('ingestAkritesExternalContactDetail', () => {
5859
it('executes ingestSecurityContactsForPurlWorkflow and returns the re-read contact detail', async () => {
5960
execute.mockResolvedValue({ found: true, repoId: 'repo-1' })
6061
getContactDetailsByPurls.mockResolvedValue([baseRow()])
6162

6263
const { req, res, json } = mockReqRes({ purl: 'pkg:npm/lodash' })
6364

64-
await refreshAkritesExternalContactDetail(req, res)
65+
await ingestAkritesExternalContactDetail(req, res)
6566

6667
expect(execute).toHaveBeenCalledTimes(1)
6768
const [workflowType, options] = execute.mock.calls[0]
6869
expect(workflowType).toBe('ingestSecurityContactsForPurlWorkflow')
6970
expect(options.taskQueue).toBe('security-contacts-worker')
7071
expect(options.workflowId).toMatch(/^security-contacts-ondemand:[0-9a-f]{64}$/)
72+
expect(options.workflowIdConflictPolicy).toBe(WorkflowIdConflictPolicy.USE_EXISTING)
73+
expect(options.workflowIdReusePolicy).toBe(WorkflowIdReusePolicy.ALLOW_DUPLICATE)
7174
expect(options.args).toEqual(['pkg:npm/lodash'])
7275

7376
expect(getContactDetailsByPurls).toHaveBeenCalledWith(expect.anything(), ['pkg:npm/lodash'])
@@ -79,11 +82,11 @@ describe('refreshAkritesExternalContactDetail', () => {
7982
getContactDetailsByPurls.mockResolvedValue([baseRow()])
8083

8184
const { req: req1, res: res1 } = mockReqRes({ purl: 'pkg:npm/lodash' })
82-
await refreshAkritesExternalContactDetail(req1, res1)
85+
await ingestAkritesExternalContactDetail(req1, res1)
8386
const id1 = execute.mock.calls[0][1].workflowId
8487

8588
const { req: req2, res: res2 } = mockReqRes({ purl: 'pkg:npm/lodash' })
86-
await refreshAkritesExternalContactDetail(req2, res2)
89+
await ingestAkritesExternalContactDetail(req2, res2)
8790
const id2 = execute.mock.calls[0][1].workflowId
8891

8992
expect(id1).toBe(id2)
@@ -94,7 +97,7 @@ describe('refreshAkritesExternalContactDetail', () => {
9497

9598
const { req, res } = mockReqRes({ purl: 'pkg:npm/left-pad' })
9699

97-
await expect(refreshAkritesExternalContactDetail(req, res)).rejects.toThrow()
100+
await expect(ingestAkritesExternalContactDetail(req, res)).rejects.toThrow()
98101
expect(getContactDetailsByPurls).not.toHaveBeenCalled()
99102
})
100103

@@ -104,13 +107,13 @@ describe('refreshAkritesExternalContactDetail', () => {
104107

105108
const { req, res } = mockReqRes({ purl: 'pkg:npm/lodash' })
106109

107-
await expect(refreshAkritesExternalContactDetail(req, res)).rejects.toThrow()
110+
await expect(ingestAkritesExternalContactDetail(req, res)).rejects.toThrow()
108111
})
109112

110113
it('rejects a request missing purl without executing a workflow', async () => {
111114
const { req, res } = mockReqRes({})
112115

113-
await expect(refreshAkritesExternalContactDetail(req, res)).rejects.toThrow()
116+
await expect(ingestAkritesExternalContactDetail(req, res)).rejects.toThrow()
114117
expect(execute).not.toHaveBeenCalled()
115118
})
116119
})

backend/src/api/public/v1/packages/refreshAkritesExternalContactDetail.ts renamed to backend/src/api/public/v1/packages/ingestAkritesExternalContactDetail.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,18 @@ interface IngestSecurityContactsForPurlResult {
2222
// Deterministic, purl-derived workflowId: concurrent callers hitting the same purl attach
2323
// to the same running workflow (USE_EXISTING) instead of each starting their own ingest —
2424
// same pattern as integrationService.ts's github-nango-sync workflow start.
25-
function refreshWorkflowId(purl: string): string {
25+
function ingestWorkflowId(purl: string): string {
2626
return `${TemporalWorkflowId.SECURITY_CONTACTS_ONDEMAND}:${createHash('sha256').update(purl).digest('hex')}`
2727
}
2828

2929
// Sync, single-purl on-demand ingest. Blocks ~10-20s (the worker's single-repo bound —
3030
// see security-contacts/workflows.ts's singleActs timeout) — no batch variant, since
3131
// fanning this out over many purls would multiply concurrent Temporal workflow starts.
32-
export async function refreshAkritesExternalContactDetail(
32+
//
33+
// Note: the workflow only ingests contacts it has never seen before — it does not
34+
// refresh/update contacts already ingested for a purl. That's why this endpoint is
35+
// named "ingest", not "refresh".
36+
export async function ingestAkritesExternalContactDetail(
3337
req: Request,
3438
res: Response,
3539
): Promise<void> {
@@ -40,21 +44,21 @@ export async function refreshAkritesExternalContactDetail(
4044
(purl: string) => Promise<IngestSecurityContactsForPurlResult>
4145
>('ingestSecurityContactsForPurlWorkflow', {
4246
taskQueue: 'security-contacts-worker',
43-
workflowId: refreshWorkflowId(purl),
47+
workflowId: ingestWorkflowId(purl),
4448
workflowIdConflictPolicy: WorkflowIdConflictPolicy.USE_EXISTING,
4549
workflowIdReusePolicy: WorkflowIdReusePolicy.ALLOW_DUPLICATE,
4650
args: [purl],
4751
})
4852

4953
if (!result.found) {
50-
throw new NotFoundError()
54+
throw new NotFoundError('Purl has no linked repository to ingest security contacts from')
5155
}
5256

5357
const qx = await getPackagesQx()
5458
const [row] = await getContactDetailsByPurls(qx, [purl])
5559

5660
if (!row) {
57-
throw new NotFoundError()
61+
throw new NotFoundError('Contact detail not found after ingest')
5862
}
5963

6064
ok(res, toAkritesExternalContactDetail(row))

0 commit comments

Comments
 (0)