feat(ai-visibility): dual-mode Semrush credential provider + PR-3a hardening (inert) - #3099
feat(ai-visibility): dual-mode Semrush credential provider + PR-3a hardening (inert)#3099cwjwisse wants to merge 1 commit into
Conversation
…rdening (inert) Makes the flag-gated per-brand Semrush auth seam from #3064 a seamless drop-in for the eventual S2S wiring without turning it on. The seam stays inert by default: no provider is registered, AI_VISIBILITY_PER_BRAND_AUTH_ENABLED defaults off, and the shared client_credentials path is byte-identical. Part A -- dual-mode credential provider (semrush-credential-provider.js, new): createDualModeSemrushCredentialProvider implements both contested upstream knobs behind config (spec adobe/mysticat-architecture#248 §6.2/§6.3) so the eventual decision is a config flip, not a rewrite: - Knob A -- credential granularity: one shared TA vs one TA per org (controls only the credential scope/cache key). - Knob B -- workspace selection: workspaceId supplied in the request vs ims_org_id-from-token -> workspace mapping (controls only the descriptor's workspaceHint). Default-to-code is per-org TA + ims_org_id mapping. The org resolver, request- workspace resolver, the ims_org_id -> workspace mapping (candidate source: serenity-docs INDEX.md), and the token minter are all injected seams; no brand/org/workspace value is hardcoded. The provider registers nothing and is installed only by a future PR (3b) via setSemrushCredentialProvider. Part B -- hardening deferred from the #3064 (PR-3a) review, safe to add while inert: 1. In-flight token coalescing: getCachedToken shares a single outstanding mint across concurrent cold/expired callers for one key, instead of each caller minting its own token. 2. Transport teardown on eviction: the per-credential client pool owns an Http2SessionManager per entry and aborts it on eviction/reset, closing the evicted transport's HTTP/2 session instead of leaking it. 3. TTL skew margin: getCachedToken refreshes a real (provider-supplied expiry) token a few seconds early to avoid a mid-flight 401; the conservative default TTL is unaffected and a real TTL shorter than the skew keeps its own expiry. No behavior change in any environment. The live flip is future work (PR-3b), gated on LLMO-6836 (upstream S2S) and the Knob A / Knob B decision. Refs: LLMO-7029, LLMO-6836, adobe/mysticat-architecture#248 §6.3 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
This PR will trigger a minor release when merged. |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Hey @cwjwisse,
Verdict: Request changes - two hardening gaps undermine the resilience this PR is building.
Complexity: HIGH - large diff (805 lines across 6 files).
Changes: Adds a dual-mode Semrush credential provider implementing both contested auth knobs behind config, plus three hardening items (in-flight token coalescing, HTTP/2 session teardown on eviction, TTL skew margin) to the flag-gated per-brand auth seam (6 files).
Must fix before merge
- [Important] Stale
nowMsused for expiry calculation after mint delay -src/support/ai-visibility/semrush-credential-resolver.js:160(details inline) - [Important]
teardownClientPoolEntrycan throw in forEach and prevent remaining entries from being torn down -src/support/ai-visibility/grpc-transport.js:149(details inline)
Non-blocking (4): minor issues and suggestions
- nit: Provider returns
nullon two distinct early-exit paths (missing org vs unmapped workspace) with no way for callers to distinguish the failure mode -src/support/ai-visibility/semrush-credential-provider.js:110- consider a debug-level log or typed result when this goes live. - nit:
TOKEN_TTL_SKEW_MS = 5000assumes gRPC calls complete within 5s but the assumption is not documented relative to transport timeout -src/support/ai-visibility/semrush-credential-resolver.js:56 - suggestion: Add a test verifying TTL skew applies to the
expiresAtMs(absolute) path, not just theexpiresInMs(relative) path - the branch setsproviderSuppliedExpiry = trueidentically but has no dedicated skew assertion. - suggestion: Add a test verifying
resetGrpcClientscallsabort()on the flag-OFF singletoncachedEntry- this is new production code on the default live path with no dedicated coverage.
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 2m 57s | Cost: $7.49 | Commit: 812bf358fa9420f82d73736703c263615a360de5
If this code review was useful, please react with 👍. Otherwise, react with 👎.
| } else if (typeof minted.expiresInMs === 'number') { | ||
| expiresAtMs = nowMs + minted.expiresInMs; | ||
| providerSuppliedExpiry = true; | ||
| } else { |
There was a problem hiding this comment.
issue (blocking): Stale nowMs undermines the TTL skew this PR introduces.
nowMs is captured at function entry (line 134) but reused inside the mint promise (here and line 153) after await mintFn(). If the mint call takes significant time (network round-trip to an S2S token endpoint), nowMs is stale by the time the token is cached, and the effective TTL is longer than intended.
For a 5-minute default TTL this is benign, but for a provider-supplied short-lived token (say 60s), a 3s mint latency means the cache overestimates remaining life by 3s - directly eroding the 5s skew margin this PR adds to prevent mid-flight 401s.
Fix: Capture Date.now() fresh inside the IIFE after the mint resolves:
const mintedAt = Date.now();
// then use mintedAt for relative expiry:
expiresAtMs = mintedAt + minted.expiresInMs;Keep the outer nowMs for the cache-hit check at the top.
|
|
||
| /** | ||
| * Tear down a pool entry's transport: abort its HTTP/2 session manager so the | ||
| * underlying connection is closed rather than leaked when the entry is evicted or the |
There was a problem hiding this comment.
issue (blocking): Unguarded abort() call can break the teardown loop.
teardownClientPoolEntry is called inside perCredentialEntries.forEach(teardownClientPoolEntry) in resetGrpcClients. If Http2SessionManager.abort() throws (e.g. the session is already in an error state, or a future library version changes behavior), the exception propagates out of the forEach and remaining entries are never torn down - leaking the connections this fix was designed to close.
Fix: Wrap the abort in a try/catch:
function teardownClientPoolEntry(entry) {
try {
entry?.sessionManager?.abort?.();
} catch { /* session already closed */ }
}
1. Abstract
Makes the flag-gated per-brand Semrush auth seam (shipped inert in #3064) a seamless drop-in for the eventual S2S wiring, without turning it on. Adds a decision-independent dual-mode credential provider and lands the three PR-3a hardening items deferred from that review. Still inert by default — no behavior change in any environment.
2. Reasoning
The seam from #3064 is provider-less and flag-off. The upstream technical-account (TA) decision is still contested along two axes (spec §6.2), and the S2S path itself is blocked (LLMO-6836). Rather than wait, this PR pre-builds both contested shapes behind config so the eventual decision is a config flip, not a rewrite, and closes the correctness gaps the #3064 review flagged as required-before-flip. Everything remains gated: no provider is registered, the flag defaults off, and the shared
client_credentialspath is byte-identical to today.3. High-level overview of the changes
Part A — dual-mode credential provider (
semrush-credential-provider.js, new)createDualModeSemrushCredentialProvider({ config, resolveOrgId, resolveRequestWorkspace, lookupWorkspaceForOrg, mintToken })returns a provider with the exact(brand, env) => SemrushCredential | nullshape the resolver expects.SHARED(one TA) vsPER_ORG(one TA per org). Controls only the credential scope/cache key.REQUEST(workspaceIdfrom the request) vsTOKEN_ORG_MAPPING(ims_org_id→ workspace via an injected lookup). Controls only the descriptor'sworkspaceHint.PER_ORG+TOKEN_ORG_MAPPING(spec §6.2 — most likely to survive a security review).ims_org_id → workspacemapping (candidate source: serenity-docsINDEX.md), and token minter are all injected seams. No brand/org/workspace value is hardcoded; the built-in extractors only read fields off the passed context.setSemrushCredentialProvider.Part B — hardening deferred from the #3064 (PR-3a) review (
semrush-credential-resolver.js,grpc-transport.js)getCachedTokennow shares a single outstanding mint across concurrent cold/expired callers for one key (in-flight Promise map), instead of each minting its own token. Directly addresses the thundering-herd suggestion in the feat(ai-visibility): flag-gated per-brand Semrush auth seam skeleton (inert) #3064 review.Http2SessionManagerper entry andabort()s it on eviction/reset, so the evicted transport's HTTP/2 session is closed rather than leaked.getCachedTokenrefreshes a real (provider-supplied expiry) token a few seconds before its true expiry, avoiding a mid-flight 401. The conservative default TTL is unaffected, and a real TTL shorter than the skew keeps its own expiry (never born-stale).getGrpcClientsreturns the same client set as before; the pool entry now internally carries its session manager, and the external return shape is unchanged.4. Required information
products/llmo/spec-brand-claims-semrush-ingestion-feed.md§6.2 / §6.3 (the two pluggable knobs; default-to-code)Related Issues
7. Test plan
(a) Local unit tests:
test/support/ai-visibility/semrush-credential-provider.test.js(new): inert-by-default (importing/building the provider registers nothing — resolver still returnsnull); config validation; default mode (per-org key + org→workspace mapping); Knob A (shared key, no org needed); Knob B (requestworkspaceIdvs mapping); both contested shapes from one seam set via config alone;getAuthTokenmints for the resolved scope; no hardcoded mapping; and an end-to-end install into the resolver seam that shares one mint per credential key viagetCachedToken.test/support/ai-visibility/semrush-credential-resolver.test.js: added B1 coalescing (single mint shared across concurrent callers; shared failure caches nothing) and B3 skew (early refresh for real tokens; default TTL unskewed; short-TTL guard).test/support/ai-visibility/grpc-transport-per-brand.test.js: added B2 teardown assertions (evicted entry and reset bothabort()the HTTP/2 session) and mockedHttp2SessionManager; the flag-off byte-identical path is unchanged.npm run lint,npm run type-check, and the fullnpm testsuite pass.(b) Per environment: nothing to verify — the flag defaults off everywhere, no provider is registered, and the shared
client_credentialspath is byte-identical. ExistingGET /llmo/ai-visibility/*responses are unchanged. EnablingAI_VISIBILITY_PER_BRAND_AUTH_ENABLEDwith a provider installed is the first live check, and that lands in PR-3b once the contested TA decision (Knob A + Knob B) and LLMO-6836 resolve.8. Deployment & merge order
main).🤖 Generated with Claude Code