-
Notifications
You must be signed in to change notification settings - Fork 0
fix(dos-id): consume unknown-entity webhook events as idempotent no-ops #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,6 +49,14 @@ export const handleDosWebhookEvent = async ( | |
| // ========================================== | ||
| // ORGANISATION EVENTS | ||
| // ========================================== | ||
| // DOS ID broadcasts events for the whole ecosystem, including | ||
| // organisations absent from the sign schema (created before webhook | ||
| // integration, failed provisioning, or deleted upstream). An | ||
| // entity-not-found lookup is permanent, so those events are consumed as | ||
| // idempotent no-ops instead of failures: retrying never succeeds and the | ||
| // retry storm once produced ~1.4k failed jobs per hour. Malformed | ||
| // payloads (missing required fields) keep failing so contract breaks | ||
| // stay loud. | ||
| case 'organization.created': | ||
| case 'org.created': { | ||
| const orgId = (data.org_id || data.id) as string | undefined; | ||
|
|
@@ -95,14 +103,20 @@ export const handleDosWebhookEvent = async ( | |
| const slug = data.slug as string | undefined; | ||
| const name = data.name as string | undefined; | ||
|
|
||
| // An empty where clause must never reach Prisma: a missing id AND slug | ||
| // is a malformed payload, not an entity to resolve. | ||
| if (!orgId && !slug) { | ||
| return { success: false, message: 'Missing org_id or slug in org.updated' }; | ||
| } | ||
|
|
||
| const org = await prisma.organisation.findFirst({ | ||
| where: { | ||
| OR: [...(orgId ? [{ id: orgId }] : []), ...(slug ? [{ url: slug }] : [])], | ||
| }, | ||
| }); | ||
|
|
||
| if (!org) { | ||
| return { success: false, message: 'Organization not found' }; | ||
| return { success: true, message: 'Organization not found in sign schema, nothing to update' }; | ||
| } | ||
|
Comment on lines
118
to
120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If both if (!org) {
if (!orgId && !slug) {
return { success: false, message: 'Missing organization identifier (org_id or slug)' };
}
return { success: true, message: 'Organization not found in sign schema, nothing to update' };
} |
||
|
|
||
| await prisma.organisation.update({ | ||
|
|
@@ -121,6 +135,12 @@ export const handleDosWebhookEvent = async ( | |
| const orgId = (data.org_id || data.id) as string | undefined; | ||
| const slug = data.slug as string | undefined; | ||
|
|
||
| // Same malformed-payload guard as org.updated: an empty OR clause must | ||
| // never reach Prisma on a destructive path. | ||
| if (!orgId && !slug) { | ||
| return { success: false, message: 'Missing org_id or slug in org.deleted' }; | ||
| } | ||
|
|
||
| const org = await prisma.organisation.findFirst({ | ||
| where: { | ||
| OR: [...(orgId ? [{ id: orgId }] : []), ...(slug ? [{ url: slug }] : [])], | ||
|
|
@@ -132,7 +152,7 @@ export const handleDosWebhookEvent = async ( | |
| }); | ||
|
|
||
| if (!org) { | ||
| return { success: false, message: 'Organization not found for deletion' }; | ||
| return { success: true, message: 'Organization not found in sign schema, nothing to delete' }; | ||
| } | ||
|
Comment on lines
154
to
156
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If both if (!org) {
if (!orgId && !slug) {
return { success: false, message: 'Missing organization identifier (org_id or slug)' };
}
return { success: true, message: 'Organization not found in sign schema, nothing to delete' };
} |
||
|
|
||
| await deleteOrganisation({ | ||
|
|
@@ -164,7 +184,7 @@ export const handleDosWebhookEvent = async ( | |
| }); | ||
|
|
||
| if (!org) { | ||
| return { success: false, message: 'Organization not found' }; | ||
| return { success: true, message: 'Organization not found in sign schema, nothing to update' }; | ||
| } | ||
|
|
||
| let user = await prisma.user.findFirst({ | ||
|
|
@@ -269,7 +289,7 @@ export const handleDosWebhookEvent = async ( | |
| }); | ||
|
|
||
| if (!org) { | ||
| return { success: false, message: `Organisation ${orgId} not found for team.created` }; | ||
| return { success: true, message: `Organisation ${orgId} not found in sign schema, nothing to create` }; | ||
| } | ||
|
|
||
| // Check if team already exists | ||
|
|
@@ -318,7 +338,7 @@ export const handleDosWebhookEvent = async ( | |
| }); | ||
|
|
||
| if (!team) { | ||
| return { success: false, message: 'Team not found for update' }; | ||
| return { success: true, message: 'Team not found in sign schema, nothing to update' }; | ||
| } | ||
|
Comment on lines
340
to
342
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If both if (!team) {
if (!teamId && !teamSlug) {
return { success: false, message: 'Missing team identifier (team_id or slug)' };
}
return { success: true, message: 'Team not found in sign schema, nothing to update' };
} |
||
|
|
||
| await prisma.team.update({ | ||
|
|
@@ -354,7 +374,7 @@ export const handleDosWebhookEvent = async ( | |
| }); | ||
|
|
||
| if (!team) { | ||
| return { success: false, message: 'Team not found for deletion' }; | ||
| return { success: true, message: 'Team not found in sign schema, nothing to delete' }; | ||
| } | ||
|
Comment on lines
376
to
378
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If both if (!team) {
if (!teamId && !teamSlug) {
return { success: false, message: 'Missing team identifier (team_id or slug)' };
}
return { success: true, message: 'Team not found in sign schema, nothing to delete' };
} |
||
|
|
||
| await prisma.$transaction(async (tx) => { | ||
|
|
@@ -413,7 +433,7 @@ export const handleDosWebhookEvent = async ( | |
| }); | ||
|
|
||
| if (!targetOrg) { | ||
| return { success: false, message: 'Target organisation not found' }; | ||
| return { success: true, message: 'Target organisation not found in sign schema, nothing to add' }; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| const finalOrgId = targetOrg.id; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: DOS/Crove-Sign
Length of output: 10427
Validate lookup identifiers before returning an idempotent no-op.
The organization branches do not validate
org_id/idorslug. The team branches validate only the organization identifier. With a valid organization scope but no accepted team identifier, the team lookup also buildsOR: []. Prisma returns no rows for an emptyOR, so each branch reaches its success return for a malformed payload.Add validation for the effective identifiers before each lookup.
Suggested fix
@@ organization.updated const slug = data.slug as string | undefined; const name = data.name as string | undefined; + if (!orgId && !slug) { + return { success: false, message: 'Missing organization identifier in organization.updated' }; + } + const org = await prisma.organisation.findFirst({ @@ organization.deleted const orgId = (data.org_id || data.id) as string | undefined; const slug = data.slug as string | undefined; + if (!orgId && !slug) { + return { success: false, message: 'Missing organization identifier in organization.deleted' }; + } + const org = await prisma.organisation.findFirst({ @@ team.updated const teamSlug = (data.slug || data.team_slug) as string | undefined; const teamName = (data.name || data.team_name) as string | undefined; + const hasTeamId = Boolean(teamId && !Number.isNaN(Number(teamId))); if (!orgId) { return { success: false, message: 'Missing org_id in team.updated' }; } + if (!hasTeamId && !teamSlug) { + return { success: false, message: 'Missing team identifier in team.updated' }; + } const team = await prisma.team.findFirst({ @@ - ...(teamId && !Number.isNaN(Number(teamId)) ? [{ id: Number(teamId) }] : []), + ...(hasTeamId ? [{ id: Number(teamId) }] : []), @@ team.deleted const teamId = (data.team_id || data.id) as string | undefined; const teamSlug = (data.slug || data.team_slug) as string | undefined; + const hasTeamId = Boolean(teamId && !Number.isNaN(Number(teamId))); if (!orgId) { return { success: false, message: 'Missing org_id in team.deleted' }; } + if (!hasTeamId && !teamSlug) { + return { success: false, message: 'Missing team identifier in team.deleted' }; + } const team = await prisma.team.findFirst({ @@ - ...(teamId && !Number.isNaN(Number(teamId)) ? [{ id: Number(teamId) }] : []), + ...(hasTeamId ? [{ id: Number(teamId) }] : []),🤖 Prompt for AI Agents