Skip to content

fix(audit): name the integration when a bulk sync pause or start targets nothing (NAN-6791) - #7304

Merged
pfreixes merged 3 commits into
masterfrom
pau/nan-6791-sync-target-fallback
Aug 31, 2026
Merged

fix(audit): name the integration when a bulk sync pause or start targets nothing (NAN-6791)#7304
pfreixes merged 3 commits into
masterfrom
pau/nan-6791-sync-target-fallback

Conversation

@pfreixes

@pfreixes pfreixes commented Aug 31, 2026

Copy link
Copy Markdown
Contributor
  • POST /sync/pause and POST /sync/start accept an empty syncs list to mean "every sync", and the controller expands it only after the audit resolver has run, so those events recorded no target at all. They now fall back to the integration, which is the widest scope the request itself names. Measured in production: 4 of the 5 no-target sync.started events in a 24h window were successful bulk calls, across 4 accounts.
  • The same resolver threw on a malformed member, losing the valid targets beside it, and turned a non-string name into a fabricated [object Object]::v2 id. Invalid members are now dropped and the rest survive.
  • The MCP syncs_set_state tool emits the same two events from its own copy of the resolver, with the same empty-list behaviour, so both now share one function.

A sync.paused row names the syncs when the caller named them and the integration when they did not, so the target type tells a reader which shape the call had. The connection is unchanged and stays in metadata, where both emitters already put it. Design and the reproduction matrix are on NAN-6791.

Test plan

  • Six unit cases: the integration fallback on both endpoints, an absent syncs field, valid syncs surviving a malformed neighbour, a non-string name, a non-string variant
  • Mutation-checked: breaking each guard fails a test, and each case is the only catcher of at least one break
  • ts-build, oxlint and prettier clean; 284 audit + MCP unit tests pass
  • After deploy: confirm no-target sync.paused / sync.started rows drop to zero in ClickHouse

…ets nothing

`POST /sync/pause` and `POST /sync/start` accept an empty `syncs` list to mean
every sync, and the controller expands it after the audit resolver has run, so
the event recorded no target at all. Fall back to the integration, which is the
widest scope the request itself names.

The same resolver also threw on a malformed member, discarding the valid targets
beside it, and turned a non-string name into a fabricated `[object Object]::` id.
Drop the invalid members instead.

The MCP `syncs_set_state` tool emits the same two events off its own copy of the
resolver with the same empty-list behaviour, so both now share one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 31, 2026

Copy link
Copy Markdown

NAN-6791

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files

Confidence score: 4/5

  • In packages/server/lib/middleware/audit/sync.middleware.ts, malformed or empty requests with a numeric provider_config_key can record an invalid integration target id such as "12345"; guard the raw value with nonEmptyString before passing it to makeTarget.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/server/lib/middleware/audit/sync.middleware.ts">

<violation number="1" location="packages/server/lib/middleware/audit/sync.middleware.ts:98">
P2: When an empty or malformed request supplies a numeric `provider_config_key`, this resolver records integration target id `"12345"` because `makeTarget` accepts numbers. Guard the raw value with `nonEmptyString` before the fallback so invalid requests are not attributed to a fabricated integration.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

.map(({ syncName, syncVariant }) => makeTarget('sync', syncTargetId(syncName, syncVariant)))
.filter((t): t is AuditTarget => Boolean(t));
return targets.length > 0 ? targets : undefined;
return targets.length > 0 ? targets : makeTarget('integration', providerConfigKey);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When an empty or malformed request supplies a numeric provider_config_key, this resolver records integration target id "12345" because makeTarget accepts numbers. Guard the raw value with nonEmptyString before the fallback so invalid requests are not attributed to a fabricated integration.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/server/lib/middleware/audit/sync.middleware.ts, line 98:

<comment>When an empty or malformed request supplies a numeric `provider_config_key`, this resolver records integration target id `"12345"` because `makeTarget` accepts numbers. Guard the raw value with `nonEmptyString` before the fallback so invalid requests are not attributed to a fabricated integration.</comment>

<file context>
@@ -90,14 +90,32 @@ function syncBaseMeta(providerConfigKey: unknown, connectionId?: unknown): Recor
         .map(({ syncName, syncVariant }) => makeTarget('sync', syncTargetId(syncName, syncVariant)))
         .filter((t): t is AuditTarget => Boolean(t));
-    return targets.length > 0 ? targets : undefined;
+    return targets.length > 0 ? targets : makeTarget('integration', providerConfigKey);
+}
+
</file context>
Suggested change
return targets.length > 0 ? targets : makeTarget('integration', providerConfigKey);
return targets.length > 0 ? targets : makeTarget('integration', nonEmptyString(providerConfigKey));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applying this one.

An empty string is already handled — toAuditId returns undefined for '', so the suggestion changes nothing there. The only input it changes is a numeric key, and toAuditId coerces numbers to strings deliberately (several targets are numeric ids). On top of that, providerConfigKeySchema is z.string().regex(...), so {"provider_config_key": 12345} is a 400 and the event already records outcome: 'failure'"12345" is verbatim what the caller sent, not a fabricated integration.

The convention is also one-sided: across the audit middleware there are 47 makeTarget( call sites and none wraps its value in nonEmptyString — ten pass a raw req.body/req.params value straight in (makeTarget('sync', req.body.scriptName), makeTarget('connection', req.params.connectionId)). toAuditId is the single place that decides what is a valid id, on purpose. Adding a guard here would make this the only exception.

The nonEmptyString four lines up isn't a general input guard either — it's there because a sync name gets concatenated into name::variant, where a non-string fabricates [object Object]::. A bare id has no such problem. I've reworded that comment to say so, since the asymmetry is a fair question to have.

pfreixes and others added 2 commits August 31, 2026 15:23
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The malformed-member test used a non-string name and a non-string variant at
once, so it passed on the variant normalisation alone and stayed green when the
name check was removed. Split it: one case per guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pfreixes
pfreixes marked this pull request as ready for review August 31, 2026 13:55
@pfreixes
pfreixes requested a review from a team August 31, 2026 13:55
@pfreixes pfreixes assigned marcindobry and unassigned marcindobry Aug 31, 2026
@pfreixes
pfreixes requested a review from marcindobry August 31, 2026 13:55
@pfreixes
pfreixes added this pull request to the merge queue Aug 31, 2026
Merged via the queue into master with commit b14e3aa Aug 31, 2026
37 checks passed
@pfreixes
pfreixes deleted the pau/nan-6791-sync-target-fallback branch August 31, 2026 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants