feat: add spacecat-shared-ticket-client package with Jira Cloud integration (SITES-44690)#1701
feat: add spacecat-shared-ticket-client package with Jira Cloud integration (SITES-44690)#1701prithipalpatwal wants to merge 7 commits into
Conversation
|
This PR will trigger a minor release when merged. |
There was a problem hiding this comment.
Hey @prithipalpatwal,
Verdict: Request changes - solid scaffolding with a few security and correctness gaps to close before merge.
Changes: adds the spacecat-shared-ticket-client package with Jira Cloud integration via OAuth 2.0 3LO, including rate-limit-aware HTTP, credential management, ADF formatting, and attachment upload (23 files).
Must fix before merge
- [Critical] Secret path injection -
buildSecretPathinterpolatesorganizationId/connectionIdwithout validation; a malformed DB record with../can traverse the Secrets Manager namespace -src/ticket-client-factory.js:44(details inline) - [Important] Unused production dependencies -
@adobe/fetchand@adobe/spacecat-shared-utilsare declared independenciesbut never imported anywhere insrc/; ships dead weight to consumers -package.json:41(details inline) - [Important] Uncapped
Retry-After-#resolveWaitMstrusts the server-sentRetry-Afterheader without an upper bound; a value of86400blocks the Lambda for a full day -src/http/rate-limit-aware-http-client.js:62(details inline) - [Important]
ClientRequestTokenmisuse -access_token.slice(0, 32)may collide across refreshes or contain invalid characters for SM's idempotency token; use a proper UUID or deterministic hash instead -src/credentials/oauth-credential-manager.js:90(details inline)
Non-blocking (5): minor issues and suggestions
- nit:
content?.lengthworks for Buffer/Uint8Array but will silently under-count multi-byte strings if the type contract ever loosens - considerBuffer.byteLengthor a type guard -src/clients/jira-cloud-client.js:203 - nit:
c8,mocha,mocha-multi-reportersare used in scripts/config but not indevDependencies- works via workspace hoisting but explicit is better -package.json:44 - suggestion: add jitter to exponential backoff (
waitMs * (0.5 + Math.random() * 0.5)) to avoid thundering herd on shared rate limits -src/http/rate-limit-aware-http-client.js:60 - nit:
URL_REGEXgreedily captures trailing punctuation (e.g.)or.at end of sentence) - consider a negative lookbehind or trimming trailing non-URL chars -src/clients/jira-cloud-client.js:32 - nit:
sanitizeFilenamestrips../sequences but not standalone..(no trailing slash) - edge case for directory traversal on Windows-style paths -src/clients/jira-cloud-client.js:124
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 10m 30s | Cost: $8.16 | Commit: 2541c41333939018ad13d14c8cdba405e63ee6a8
If this code review was useful, please react with 👍. Otherwise, react with 👎.
There was a problem hiding this comment.
Hey @prithipalpatwal,
Verdict: Approve - all four prior blocking findings are correctly fixed with adequate tests.
Changes: adds the spacecat-shared-ticket-client package with Jira Cloud integration via OAuth 2.0 3LO, including rate-limit-aware HTTP, credential management, ADF formatting, and attachment upload (23 files).
Previously flagged, now resolved
- Secret path injection:
buildSecretPathnow validates both IDs as UUIDs before interpolation - Unused production dependencies:
@adobe/fetchand@adobe/spacecat-shared-utilsremoved;c8,mocha,mocha-multi-reportersadded to devDependencies - Uncapped Retry-After:
MAX_WAIT_MS = 30_000ceiling applied viaMath.min - ClientRequestToken misuse: SHA-256 hash of full access token (64-char hex) replaces naive
.slice(0, 32)
Non-blocking (4): minor issues and suggestions
- nit:
sanitizeFilename.replace(/\.\./g, '')strips ALL..sequences including legitimate ones in filenames likereport..final.pdf- stripping path separators (/and\) instead would be more targeted -src/clients/jira-cloud-client.js:125 - nit:
UUID_REGEXcomment says "Matches lowercase UUIDs" but the regex uses the/iflag accepting uppercase - align the comment with the code or remove the flag -src/ticket-client-factory.js:34 - suggestion: validate
process.env.NODE_ENVagainst an allowlist or/^[a-z]+$/pattern inbuildSecretPathfor defense-in-depth - the other two path segments are UUID-validated but the env segment is not -src/ticket-client-factory.js:56 - nit:
URL_REGEXlookbehind will strip trailing)from legitimate URLs with balanced parens like Wikipedia linkshttps://en.wikipedia.org/wiki/Foo_(bar)- acceptable tradeoff for the Jira description use case but worth a code comment -src/clients/jira-cloud-client.js:30
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 5s | Cost: $6.08 | Commit: 0303efc11adae7029511b667a03e529fcdd7d5dc
If this code review was useful, please react with 👍. Otherwise, react with 👎.
4a5adf2 to
4b583ae
Compare
|
Mysticat review failed: Claude CLI crashed (exit 1): stderr= stdout={"type":"result","subtype":"error_max_budget_usd","duration_ms":6,"duration_api_ms":1560718,"is_error":true,"num_turns":1,"stop_reason":null,"session_id":"b18831f0-22a7-4862-a39b-cdbc7351939d","total_cost_usd":10.4506784,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tok |
MysticatBot
left a comment
There was a problem hiding this comment.
Hey @prithipalpatwal,
Verdict: Request changes - prior blocking findings are fixed; a few remaining correctness issues in filename sanitization, environment validation, and token lifecycle handling.
Complexity: HIGH - large diff, new package scaffolding across 24 files.
Changes: adds the spacecat-shared-ticket-client package with Jira Cloud integration via OAuth 2.0 3LO, including rate-limit-aware HTTP, credential management, ADF formatting, and attachment upload (24 files).
Must fix before merge
- [Important]
sanitizeFilenamestrips all..sequences and allows path separators through -src/clients/jira-cloud-client.js:127(details inline) - [Important]
process.env.NODE_ENVinterpolated into SM secret path without validation -src/ticket-client-factory.js:56(details inline) - [Important] Token refresh 401 from Atlassian never triggers
setRequiresReauth-src/credentials/oauth-credential-manager.js:84(details inline) - [Important]
siteUrlvalidation uses string suffix matching instead of hostname parsing -src/clients/jira-cloud-client.js:97(details inline)
Non-blocking (5): minor issues and suggestions
- nit:
index.test.jsdoes not verifyRateLimitAwareHttpClientexport despite it being exported fromsrc/index.js-test/index.test.js:22 - nit:
CLOUD_ID_REGEXis lowercase-only (no/i) whileUUID_REGEXin the factory has/i- inconsistent casing contract -src/clients/jira-cloud-client.js:19vssrc/ticket-client-factory.js:34 - nit:
#buildAdfDescriptionJSDoc says "URLs are converted to clickable link nodes" but implementation emits plain-text nodes only -src/clients/jira-cloud-client.js:282 - suggestion: add
ticket-clientto the Package Catalog's API clients row in the root CLAUDE.md so the discovery index stays current - nit:
test/setup-env.jscopyright header says 2026, rest of files say 2025 -test/setup-env.js:2
Previously flagged, now resolved
- Secret path injection:
buildSecretPathnow validates both IDs as UUIDs before interpolation - Unused production dependencies: removed from
dependencies; test tooling moved todevDependencies - Uncapped Retry-After:
MAX_WAIT_MS = 30_000ceiling applied viaMath.min - ClientRequestToken misuse: SHA-256 hash of full access token replaces naive
.slice(0, 32)
There was a problem hiding this comment.
Hey @prithipalpatwal,
Verdict: Approve - all four prior blocking findings are correctly fixed with appropriate test coverage.
Complexity: HIGH - large diff; new package scaffolding with auth + secrets management.
Changes: adds the spacecat-shared-ticket-client package with Jira Cloud integration via OAuth 2.0 3LO, including rate-limit-aware HTTP, credential management, ADF formatting, and attachment upload (24 files).
Previously flagged, now resolved
- sanitizeFilename now strips path separators (
/,\) instead of all..sequences - preserves legitimate filenames while neutralizing traversal - NODE_ENV removed from SM secret path entirely - account boundary provides env isolation, eliminating the unvalidated interpolation
- Token refresh 401 now triggers
setRequiresReauthvia try/catch on#fetchNewTokenswith.statusproperty on the thrown error - siteUrl validation upgraded to
new URL()hostname parsing - blocks path-based and subdomain-based spoofing attempts
Non-blocking (2): minor issues and suggestions
- nit:
UUID_REGEXcomment says "Matches lowercase UUIDs" but the regex uses the/iflag accepting uppercase - align the comment or remove the flag for consistency withCLOUD_ID_REGEX-src/ticket-client-factory.js:34 - nit:
sanitizeFilenamecomment "Strip path separators" is on the line above the eslint-disable, making it visually ambiguous which.replaceit describes - consider moving it directly above the regex -src/clients/jira-cloud-client.js:120
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 1s | Cost: $3.07 | Commit: 15094660ec79f5d55cbd5652798e3ee31763514f
If this code review was useful, please react with 👍. Otherwise, react with 👎.
There was a problem hiding this comment.
Hey @prithipalpatwal,
Verdict: Request changes - TypeScript declarations are out of sync with the implementation after the optional-fields commit.
Complexity: HIGH - large diff; new package scaffolding across 24 files.
Changes: adds the spacecat-shared-ticket-client package with Jira Cloud integration via OAuth 2.0 3LO, including rate-limit-aware HTTP, credential management, ADF formatting, and attachment upload (24 files).
Must fix before merge
- [Important]
TicketDatainterface missingpriority,dueDate,components,parentfields - TypeScript consumers will get compile errors -src/index.d.ts:22(details inline)
Non-blocking (6): minor issues and suggestions
- suggestion: validate
dueDateformat (/^\d{4}-\d{2}-\d{2}$/) before passing to Jira - consistent with howcloudId,ticketKey, andorganizationIdare validated -src/clients/jira-cloud-client.js:160 - suggestion: add a max-pages safety bound to
listProjectspagination loop to prevent unbounded requests if Jira API misbehaves -src/clients/jira-cloud-client.js:112 - nit:
.nycrc.jsonsetsbranches: 100but project standard is 97% branches per CLAUDE.md - stricter than required but diverges from other packages -packages/spacecat-shared-ticket-client/.nycrc.json:8 - suggestion: update root CLAUDE.md and AGENTS.md Package Catalog to include
ticket-clientin the API clients row and increment the package count from 22 to 23 - both catalogs are now stale - nit:
CLOUD_ID_REGEXuses/i(accepts uppercase) whileUUID_REGEXin the factory does not - intentional per comments but a one-line note onCLOUD_ID_REGEXexplaining why would prevent future fixes -src/clients/jira-cloud-client.js:20 - nit:
test/setup-env.jscopyright header says 2026 while all other files say 2025 -test/setup-env.js:2
Previously flagged, now resolved
- Secret path injection:
buildSecretPathvalidates both IDs as UUIDs before interpolation - Unused production dependencies: removed; test tooling moved to devDependencies
- Uncapped Retry-After:
MAX_WAIT_MS = 30_000ceiling applied viaMath.min - ClientRequestToken misuse: SHA-256 hash of full access token replaces naive
.slice(0, 32) - sanitizeFilename: now strips path separators instead of all
..sequences - NODE_ENV: removed from SM secret path entirely
- Token refresh 401: now triggers
setRequiresReauth - siteUrl validation: upgraded to
new URL()hostname parsing
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 2m 6s | Cost: $9.66 | Commit: 0eb2ca8c8d6e49ca99458f3aeb10a92a97ad9c09
If this code review was useful, please react with 👍. Otherwise, react with 👎.
|
Addressed review 4572484771 in
79 tests passing, 100% coverage. CI green. |
There was a problem hiding this comment.
Hey @prithipalpatwal,
Verdict: Approve - all prior blocking findings are correctly fixed; only non-blocking nits remain.
Complexity: HIGH - large diff; new package scaffolding across 24 files.
Changes: adds the spacecat-shared-ticket-client package with Jira Cloud integration via OAuth 2.0 3LO, including rate-limit-aware HTTP, credential management, ADF formatting, and attachment upload (24 files).
Non-blocking (3): minor issues and suggestions
- suggestion: root CLAUDE.md and AGENTS.md Package Catalog still say "22 packages" and omit
ticket-clientfrom the API clients row - both are now factually stale after this PR -AGENTS.md:47 - nit: dueDate validation uses an IIFE-inside-spread pattern that is inconsistent with the other optional fields (priority, components, parent all use plain conditional spread) - hoisting the guard before body construction would be more readable -
src/clients/jira-cloud-client.js:214 - suggestion:
MAX_PAGESsafety bound has no dedicated test proving the loop terminates at 100 whenisLastis perpetually false - a small stub test would prevent accidental removal -src/clients/jira-cloud-client.js:252
Previously flagged, now resolved
- TypeScript
TicketDatainterface now includespriority,dueDate,components,parent;descriptionmade optional dueDateformat validated via regex before passing to JiralistProjectspagination bounded byMAX_PAGES = 100CLOUD_ID_REGEX/iflag rationale documented in comment
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 19s | Cost: $4.23 | Commit: 73f6296206f4286f1d8a3838fa33be2019ef5f66
If this code review was useful, please react with 👍. Otherwise, react with 👎.
661f5a7 to
d7db958
Compare
10bd050 to
6c58d5d
Compare
There was a problem hiding this comment.
Hey @prithipalpatwal,
Verdict: Approve - all prior blocking findings are correctly fixed; only non-blocking suggestions remain.
Complexity: HIGH - large diff; new package scaffolding across 25 files with auth + secrets management.
Changes: adds the spacecat-shared-ticket-client package with Jira Cloud integration via OAuth 2.0 3LO, including rate-limit-aware HTTP, credential management, ADF formatting, and attachment upload (25 files).
Note: Recommend a human read before merge - this change modifies the root AGENTS.md (architectural significance). The bot review is a complement to, not a replacement for, a human read here.
Non-blocking (5): minor issues and suggestions
- suggestion: validate
hrefscheme in ADF link nodes (rejectjavascript:,data:) for defense-in-depth - Jira sanitizes server-side but this hardens against re-rendering in other contexts -src/adf/markdown-to-adf.js:523 - suggestion: tighten
siteUrlvalidation to reject bareatlassian.net(require subdomain:hostname !== 'atlassian.net') - display-only field but tighter matches the*.atlassian.netcontract -src/clients/jira-cloud-client.js:97 - suggestion: pass the unwrapped
httpClienttoCredentialManagerFactory.create()instead of the rate-limited wrapper - auth endpoint has different rate-limit semantics than the Jira API and does not emitRateLimit-Reasonheaders -src/ticket-client-factory.js:87 - nit: validate
projectKeyis truthy increateTicketbefore sending to Jira, consistent with theprojectIdcheck inlistIssueTypes-src/clients/jira-cloud-client.js:925 - nit: TypeScript declarations do not expose
refreshAuthHeadersorforceRefreshAuthHeaders- if consuming services call these directly, they will lack type support -src/index.d.ts:55
Previously flagged, now resolved
- Secret path injection:
buildSecretPathvalidates both IDs as UUIDs before interpolation - Unused production dependencies: removed; test tooling moved to devDependencies
- Uncapped Retry-After:
MAX_WAIT_MS = 30_000ceiling applied viaMath.min - ClientRequestToken misuse: SHA-256 hash of full access token replaces naive
.slice(0, 32) - sanitizeFilename: now strips path separators instead of all
..sequences - NODE_ENV: removed from SM secret path entirely
- Token refresh 401: now triggers
setRequiresReauthwith concurrent-race recovery - siteUrl validation: upgraded to
new URL()hostname parsing - TypeScript
TicketDatainterface: includespriority,dueDate,components,parent;descriptionmade optional - dueDate validation: format validated via regex before passing to Jira
- listProjects pagination: bounded by
MAX_PAGES = 100 - AGENTS.md: updated to 23 packages with
ticket-clientin API clients row
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 14s | Cost: $7.69 | Commit: 6c58d5d92c5867cc9e6d0d3e40cfc102cd525196
If this code review was useful, please react with 👍. Otherwise, react with 👎.
6c58d5d to
c735603
Compare
There was a problem hiding this comment.
Hey @prithipalpatwal,
Verdict: Approve - well-engineered package with solid security posture; only non-blocking suggestions remain after 7+ review rounds.
Complexity: HIGH - large diff; new package scaffolding across 26 files with auth + secrets management.
Changes: adds the spacecat-shared-ticket-client package with Jira Cloud integration via OAuth 2.0 3LO, including rate-limit-aware HTTP, credential management, ADF formatting, and attachment upload (26 files).
Note: Recommend a human read before merge - this change modifies the root AGENTS.md (architectural significance). The bot review is a complement to, not a replacement for, a human read here.
Non-blocking (7): minor issues and suggestions
- suggestion: TypeScript declarations do not expose
refreshAuthHeadersorforceRefreshAuthHeadersonOAuthCredentialManager- consuming services calling these directly will lack type support -src/index.d.ts:57 - suggestion: pass the unwrapped
httpClienttoCredentialManagerFactory.create()instead of the rate-limited wrapper - the auth endpoint (auth.atlassian.com) has different rate-limit semantics than the Jira API and does not emitRateLimit-Reasonheaders -src/ticket-client-factory.js:87 - nit:
siteUrlvalidation permits bareatlassian.net(no subdomain) - display-only so no security impact, but tighter matches the*.atlassian.netcontract: add|| siteUrlParsed.hostname === 'atlassian.net'rejection -src/clients/jira-cloud-client.js:97 - suggestion:
setRequiresReauthand#writeReauthFlagperform a bare SMputSecretValuewithout retry - if the write fails transiently the connection is left in a zombie state unable to refresh but not flagged for reauth -src/credentials/oauth-credential-manager.js:135 - nit: validate
parentfield againstTICKET_KEY_REGEXbefore sending to Jira, consistent with the defensive validation onticketKeyinuploadAttachment-src/clients/jira-cloud-client.js:220 - nit:
listProjectspagination safety boundMAX_PAGES = 100has no test exercising the break condition - a loop-logic refactor could silently remove the safety bound -src/clients/jira-cloud-client.js:252 - nit: concurrent refresh 200ms wait in
#recoverFromAtlassian401is not tested with fake timers - removing the setTimeout would not break the current test suite -src/credentials/oauth-credential-manager.js:176
Previously flagged, now resolved
- Secret path injection:
buildSecretPathvalidates both IDs as UUIDs before interpolation - Unused production dependencies: removed; test tooling moved to devDependencies
- Uncapped Retry-After:
MAX_WAIT_MS = 30_000ceiling applied viaMath.min - ClientRequestToken misuse: SHA-256 hash of full access token replaces naive
.slice(0, 32) - sanitizeFilename: now strips path separators instead of all
..sequences - NODE_ENV: removed from SM secret path entirely
- Token refresh 401: now triggers
setRequiresReauthwith concurrent-race recovery - siteUrl validation: upgraded to
new URL()hostname parsing - TypeScript
TicketDatainterface: includespriority,dueDate,components,parent;descriptionmade optional - dueDate validation: format validated via regex before passing to Jira
- listProjects pagination: bounded by
MAX_PAGES = 100 - AGENTS.md: updated to 23 packages with
ticket-clientin API clients row
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 24s | Cost: $8.24 | Commit: f6f3daff53742e15a103f61db234593a3f40010b
If this code review was useful, please react with 👍. Otherwise, react with 👎.
- Add missing refreshAuthHeaders/forceRefreshAuthHeaders to OAuthCredentialManager TypeScript declarations - Pass unwrapped httpClient to CredentialManagerFactory — auth.atlassian.com does not emit Jira rate-limit headers so the rate-limited wrapper adds no value for token refresh calls - Add 3-attempt SM write retry with exponential backoff to #writeReauthFlag; setRequiresReauth now delegates to it (eliminates duplication, both paths get retry) - Validate parent field against TICKET_KEY_REGEX in createTicket, consistent with uploadAttachment - Add test for MAX_PAGES=100 pagination safety bound in listProjects - Use sinon fake timers (toFake: ['setTimeout']) for #recoverFromAtlassian401 tests to avoid real 200ms wall-clock delays; add retry coverage for setRequiresReauth Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Hey @prithipalpatwal,
Verdict: Approve - all prior non-blocking suggestions are correctly addressed; only minor nits remain.
Complexity: HIGH - large diff; new package scaffolding across 26 files with auth + secrets management.
Changes: adds the spacecat-shared-ticket-client package with Jira Cloud integration via OAuth 2.0 3LO, including rate-limit-aware HTTP, credential management, ADF formatting, and attachment upload (26 files).
Note: Recommend a human read before merge - this change modifies the root AGENTS.md (architectural significance). The bot review is a complement to, not a replacement for, a human read here.
Non-blocking (5): minor issues and suggestions
- suggestion: CLAUDE.md Package Catalog is still missing
ticket-clientin the API clients row - AGENTS.md was updated but CLAUDE.md is now inconsistent -CLAUDE.md - nit:
#writeReauthFlagreads the secret once outside the retry loop - a concurrent writer between retries could have its tokens clobbered with the stale snapshot plusrequiresReauth: true; re-reading inside the loop would be strictly more correct as a read-modify-write pattern -src/credentials/oauth-credential-manager.js:370 - nit:
clock.restore()calls in oauth-credential-manager tests are in the test body rather thanafterEach- if an assertion fails before restore, fake timer leaks into subsequent tests -test/oauth-credential-manager.test.js - nit:
setRequiresReauthtest usesclock.tickAsync(500)which assumes the total backoff is under 500ms - if constants change this test could silently hang; consider calculating from the constants -test/oauth-credential-manager.test.js - suggestion: validate
hrefscheme in ADF link nodes (rejectjavascript:,data:) for defense-in-depth - Jira sanitizes server-side but this hardens against re-rendering in other contexts -src/adf/markdown-to-adf.js:523
Previously flagged, now resolved
- TypeScript declarations now expose
refreshAuthHeadersandforceRefreshAuthHeadersonOAuthCredentialManager - Unwrapped
httpClientpassed toCredentialManagerFactory- auth.atlassian.com does not emit Jira rate-limit headers #writeReauthFlagnow has 3-attempt SM write retry with exponential backoff;setRequiresReauthdelegates to it (eliminates duplication)parentfield validated againstTICKET_KEY_REGEXincreateTicket, consistent withuploadAttachmentMAX_PAGES = 100pagination safety bound now has a dedicated test#recoverFromAtlassian401tests use sinon fake timers - no more real 200ms wall-clock delays
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 4m 4s | Cost: $5.68 | Commit: 7dd234120b92241488aa20751e87b18879b8262b
If this code review was useful, please react with 👍. Otherwise, react with 👎.
There was a problem hiding this comment.
Hey @prithipalpatwal,
Verdict: Approve - all prior non-blocking suggestions are correctly addressed; no new issues introduced.
Complexity: HIGH - large diff; new package scaffolding across 26 files with auth + secrets management.
Changes: adds the spacecat-shared-ticket-client package with Jira Cloud integration via OAuth 2.0 3LO, including rate-limit-aware HTTP, credential management, ADF formatting, and attachment upload (26 files).
Note: Recommend a human read before merge - this change modifies the root AGENTS.md (architectural significance). The bot review is a complement to, not a replacement for, a human read here.
Non-blocking (1): minor issues and suggestions
- nit: XSS link-scheme test covers only
javascript:- a second case (e.g.data:text/html,...) would document that the stripping logic is scheme-agnostic rather than specifically targeting one scheme -test/jira-cloud-client.test.js:677
Previously flagged, now resolved
- CLAUDE.md Package Catalog now includes
ticket-clientin the API clients row #writeReauthFlagre-reads secret inside retry loop on every attempt, avoiding stale-snapshot clobberingclock.restore()moved toafterEachin bothrefreshAuthHeadersandsetRequiresReauthdescribe blockstickAsyncuses calculated value (300ms) with explanatory comment documenting the backoff math- ADF link nodes validate
hrefscheme via allowlist, rendering unsafe schemes as plain text
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 0m 56s | Cost: $3.89 | Commit: 7d3975ed288d129271ee52cdbc423b9230985572
If this code review was useful, please react with 👍. Otherwise, react with 👎.
There was a problem hiding this comment.
Hey @prithipalpatwal,
Verdict: Approve - all prior findings addressed; only non-blocking nits remain after 8+ review rounds.
Complexity: HIGH - large diff; new package scaffolding across 26 files with auth + secrets management.
Changes: adds the spacecat-shared-ticket-client package with Jira Cloud integration via OAuth 2.0 3LO, including rate-limit-aware HTTP, credential management, ADF formatting, and attachment upload (26 files).
Note: Recommend a human read before merge - this change modifies the root AGENTS.md (architectural significance). The bot review is a complement to, not a replacement for, a human read here.
Non-blocking (2): minor issues and suggestions
- nit: XSS test uses a for-loop inside a single
it()- a failure on the first scheme short-circuits evaluation of the second; separateit()blocks would give independent pass/fail, though the assertion labels make diagnosis trivial as-is -test/jira-cloud-client.test.js:679 - suggestion: consider adding a mixed-case scheme test (
JaVaScRiPt:) to confirm case-insensitive stripping - the implementation's regex already handles this via theiflag on the allowlist check, but a test would document the contract -test/jira-cloud-client.test.js:679
Previously flagged, now resolved
- XSS link-scheme test now covers both
javascript:anddata:URI schemes (addresses prior nit about single-scheme coverage) - Secret path injection:
buildSecretPathvalidates both IDs as UUIDs before interpolation - Unused production dependencies: removed; test tooling moved to devDependencies
- Uncapped Retry-After:
MAX_WAIT_MS = 30_000ceiling applied viaMath.min - ClientRequestToken misuse: SHA-256 hash of full access token replaces naive
.slice(0, 32) - sanitizeFilename: now strips path separators instead of all
..sequences - NODE_ENV: removed from SM secret path entirely
- Token refresh 401: now triggers
setRequiresReauthwith concurrent-race recovery - siteUrl validation: upgraded to
new URL()hostname parsing - TypeScript
TicketDatainterface: includespriority,dueDate,components,parent;descriptionmade optional - dueDate validation: format validated via regex before passing to Jira
- listProjects pagination: bounded by
MAX_PAGES = 100 - AGENTS.md and CLAUDE.md: updated to 23 packages with
ticket-clientin API clients row - ADF link nodes validate
hrefscheme via allowlist, rendering unsafe schemes as plain text #writeReauthFlagre-reads secret inside retry loop;clock.restore()moved toafterEachparentfield validated againstTICKET_KEY_REGEX;MAX_PAGEShas a dedicated test- Unwrapped
httpClientpassed toCredentialManagerFactory(auth endpoint has different rate-limit semantics)
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 0m 59s | Cost: $3.31 | Commit: cc0295c7071159272771db75f67440ca2de8f409
If this code review was useful, please react with 👍. Otherwise, react with 👎.
…-44690) Scaffolds the spacecat-shared-ticket-client package implementing BaseTicketClient, JiraCloudClient, OAuthCredentialManager, RateLimitAwareHttpClient, TicketClientFactory, and CredentialManagerFactory. - JiraCloudClient targets Jira REST API v3 with markdown-to-ADF conversion, 255-char summary truncation, uploadAttachment with MIME whitelist + magic bytes validation + filename sanitization - RateLimitAwareHttpClient wraps HTTP with automatic 429 retry, exponential backoff, Retry-After header support, and quota-exhaustion fail-fast - OAuthCredentialManager handles OAuth 2.0 3LO token lifecycle with concurrent-safe refresh via SM pre-read and race recovery - SSRF protection: all calls route through fixed Atlassian gateway; cloudId validated as UUID; siteUrl validated as https://*.atlassian.net 144 tests, 100% coverage across all metrics. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add missing refreshAuthHeaders/forceRefreshAuthHeaders to OAuthCredentialManager TypeScript declarations - Pass unwrapped httpClient to CredentialManagerFactory — auth.atlassian.com does not emit Jira rate-limit headers so the rate-limited wrapper adds no value for token refresh calls - Add 3-attempt SM write retry with exponential backoff to #writeReauthFlag; setRequiresReauth now delegates to it (eliminates duplication, both paths get retry) - Validate parent field against TICKET_KEY_REGEX in createTicket, consistent with uploadAttachment - Add test for MAX_PAGES=100 pagination safety bound in listProjects - Use sinon fake timers (toFake: ['setTimeout']) for #recoverFromAtlassian401 tests to avoid real 200ms wall-clock delays; add retry coverage for setRequiresReauth Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ticket-client to CLAUDE.md Package Catalog (was missing after AGENTS.md update) - Re-read SM secret inside #writeReauthFlag retry loop — prevents clobbering concurrent winner's tokens with stale snapshot on retry - Move clock.restore() to afterEach in affected test suites — prevents fake timer leak on assertion failure - Replace hardcoded tickAsync(500) with computed 300ms (SM_WRITE_BASE_DELAY_MS * (2^0 + 2^1)) — test stays correct if constants change - Validate href scheme in ADF link nodes (allow only https/http/mailto, render unsafe schemes as plain text); add tests for javascript: XSS and empty href coverage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents that href scheme validation is scheme-agnostic by covering both javascript: and data: alongside the existing empty-href case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Split for-loop XSS test into independent it() blocks (javascript:, data:) so each scheme gets its own pass/fail signal - Add mixed-case scheme test (JaVaScRiPt:) to document case-insensitive allowlist check via /i flag in markdownToAdf link validation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cc0295c to
14ef435
Compare
There was a problem hiding this comment.
Hey @prithipalpatwal,
Verdict: Approve - merge from main only; no source changes to the ticket-client package since the last approval.
Complexity: HIGH - large diff; new package scaffolding across 26 files with auth + secrets management.
Changes: adds the spacecat-shared-ticket-client package with Jira Cloud integration via OAuth 2.0 3LO, including rate-limit-aware HTTP, credential management, ADF formatting, and attachment upload (26 files).
Note: Recommend a human read before merge - this change modifies the root AGENTS.md (architectural significance). The bot review is a complement to, not a replacement for, a human read here.
Non-blocking (1): minor issues and suggestions
- nit: AGENTS.md Package Catalog still says "22 packages" in the Architecture Overview header while the catalog table and CLAUDE.md both say 23 -
AGENTS.md:44
Previously flagged, now resolved
- Secret path injection:
buildSecretPathvalidates both IDs as UUIDs before interpolation - Unused production dependencies: removed; test tooling moved to devDependencies
- Uncapped Retry-After:
MAX_WAIT_MS = 30_000ceiling applied viaMath.min - ClientRequestToken misuse: SHA-256 hash of full access token replaces naive
.slice(0, 32) - sanitizeFilename: now strips path separators instead of all
..sequences - NODE_ENV: removed from SM secret path entirely
- Token refresh 401: now triggers
setRequiresReauthwith concurrent-race recovery - siteUrl validation: upgraded to
new URL()hostname parsing - TypeScript
TicketDatainterface: includespriority,dueDate,components,parent;descriptionmade optional - dueDate validation: format validated via regex before passing to Jira
- listProjects pagination: bounded by
MAX_PAGES = 100 - AGENTS.md and CLAUDE.md: updated to 23 packages with
ticket-clientin API clients row - ADF link nodes validate
hrefscheme via allowlist, rendering unsafe schemes as plain text #writeReauthFlagre-reads secret inside retry loop;clock.restore()moved toafterEachparentfield validated againstTICKET_KEY_REGEX;MAX_PAGEShas a dedicated test- Unwrapped
httpClientpassed toCredentialManagerFactory(auth endpoint has different rate-limit semantics) - TypeScript declarations expose
refreshAuthHeadersandforceRefreshAuthHeaders - XSS link-scheme test covers
javascript:anddata:schemes with mixed-case coverage
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 9m 37s | Cost: $2.48 | Commit: 7a8433cf82773ef4ac93b754d7aac149ac80437b
If this code review was useful, please react with 👍. Otherwise, react with 👎.
ravverma
left a comment
There was a problem hiding this comment.
Hey @prithipalpatwal,
This is a follow-up independent review after the automated review rounds. The security-critical work already done (SSRF gateway pinning, secret-path UUID validation, Retry-After capping, secret hygiene) holds up well. The findings below are gaps that the prior rounds did not surface, several of which are attacker-triggerable via the untrusted description field or affect token-lifecycle availability. Note: the CI Test check is currently pending.
Strengths
- The concurrent-refresh design for Atlassian's rotating single-use refresh tokens (pre-read collapse +
#recoverFromAtlassian401two-phase re-read) is the correct model for a stateless multi-Lambda deployment.oauth-credential-manager.js #fetchNewTokensvalidates the shape of a 200 token response before persisting, preventing a malformed success from corrupting Secret Manager.oauth-credential-manager.js- No secret/token leakage into logs, thrown errors, or
ticketUrl- confirmed across all operations.#requireOknever logs response bodies.jira-cloud-client.js:520 markedis pinned to 18.0.5 with an integrity hash and adds zero transitive prod dependencies. Clean supply-chain surface.- 429 handling is well-built: quota-vs-burst branching, jitter in [0.7, 1.3) to spread thundering herds, Retry-After honored.
rate-limit-aware-http-client.js:150
Issues
Critical (Must Fix)
-
#isExpiredtreats a missing or NaNexpiresAtas never-expiring.oauth-credential-manager.js:218-Date.now() >= (secret.expiresAt - BUFFER)evaluates tofalsewhenexpiresAtis undefined/null/non-numeric (comparison against NaN). A partial write, manual SM edit, or schema drift then silently disables all refresh logic:getAuthHeadersserves a possibly-dead Bearer token forever, and the refresh/recovery paths all early-exit treating the stale secret as valid.#fetchNewTokensvalidates Atlassian's response shape but nothing validates SM's own shape on read. Fix: in#readSecret, requireNumber.isFinite(secret.expiresAt)and presence ofaccessToken/refreshToken; treat missing/NaN as expired (fail-safe). -
A live rotated refresh token is discarded if the SM write fails, bricking the connection.
oauth-credential-manager.js:129- Atlassian refresh tokens are single-use: once#fetchNewTokensreturns 200, the old token in SM is dead and only the newrefreshed.refresh_tokencan refresh again. If#writeTokensthen exhausts its retries, it re-reads SM (dead old token), sees it expired, and force-marks the connectionrequires_reauth, throwing away the still-valid new token it held microseconds earlier. A transient SM blip becomes a mandatory human re-authorization, and under SM throttling this fires for every connection mid-refresh. Fix: persist the in-hand new token bundle more aggressively before falling to reauth; at minimum log at error that a live rotated token was dropped. -
Unbounded recursion in the ADF converter crashes on untrusted input (DoS).
markdown-to-adf.js-tokensToAdf/inlineToAdfrecurse per nesting level with no depth guard, andmarkdownToAdfwrapslexer()+ conversion in no try/catch. A description of roughly 3,000>characters (or deeply nested lists) overflows the stack with an uncaughtRangeError(verified locally against marked 18.0.5 - the conversion recursion overflows below marked's own lexer limit). Sincedescriptionis untrusted suggestion-derived content, a ~3 KB payload reliably crashescreateTicketas an uncategorized 500. Fix: cap input length, thread a depth counter through the recursion, and wrap conversion in try/catch that falls back to a single plain-text paragraph rather than throwing.
Important (Should Fix)
-
createTicketis a non-idempotent POST sitting under two retry layers - duplicate-ticket risk.jira-cloud-client.js- the create POST has no dedup key. If Jira commits the issue but the response is lost (connection reset, or Lambda timeout per item 5), the caller retries and files a second identical ticket. Jira Cloud offers no create-issue idempotency token, so mitigation is application-level (caller-supplied correlation key stored in a label, checked via JQL before create) or, at minimum, document loudly thatcreateTicketis at-least-once, not exactly-once. Today nothing warns the caller. -
No overall retry deadline, and the auth-retry layer nests a full 429 backoff loop inside itself.
jira-cloud-client.js#withAuthRetrywrapsrequestFn, which is theRateLimitAwareHttpClient.fetchcontaining the 429 loop. A 401 re-invokes the entire MAX_RETRIES-deep 429 backoff again, so a singlecreateTicketcan accumulate ~75-80s of backoff sleep before the status fetch even runs. There is no aggregate deadline or Lambda-time awareness, so a caller on a common 30-60s timeout is killed mid-backoff (and per item 4, may have already created the ticket). Relatedly,MAX_WAIT_MSis applied only to the Retry-After branch, not the exponential fallback (rate-limit-aware-http-client.js:153), so the "hard ceiling" silently stops holding ifMAX_RETRIES/BASE_BACKOFF_MSgrow. Fix: thread a single deadline/AbortSignal through both layers and clamp the exponential fallback withMath.min(..., MAX_WAIT_MS). -
Plain-text-only security contract is contradicted by the code, and HTML tokens break the documented invariant.
jira-cloud-client.jscreateTicketJSDoc says "Plain-text description (no ADF markup)... untrusted content is never rendered as structured markup (spec 13)," but the implementation callsmarkdownToAdf(description)and the README encourages full markdown. Separately,markdown-to-adf.jsnever handles marked'shtmltokens, so<script>/<img onerror=...>fall through thedefaultbranch and are emitted verbatim into an ADF text node. Jira renders these as literal text (not stored XSS in Jira itself), but the file's own comment claims defense-in-depth "if ADF is consumed by other renderers" - which is only true for links, not raw HTML. Fix: reconcile the JSDoc/README/code on whether markdown is intended, and add an explicitcase 'html':in both switch functions that strips tags so the "no active markup leaves this function" invariant is real. -
Empty fenced code block produces ADF that Jira rejects, failing ticket creation.
markdown-to-adf.jscode branch buildscontent: [{ type: 'text', text: '' }]for an empty code fence. The file's own comment notes Jira rejects empty text nodes, andinlineToAdffilters them, but the code branch bypasses that filter. Attacker-influenced description content can deterministically breakcreateTicket. Fix:content: token.text ? [{ type: 'text', text: token.text }] : []. -
Atlassian OAuth failures are collapsed to
{ status }and only 401 triggers reauth.oauth-credential-manager.js:248- the error body is never inspected, soinvalid_client(misconfigured app credentials, a fleet-wide operational issue) is indistinguishable frominvalid_grant(this user's token revoked). A client-credential rotation would stampede every connection intorequires_reauth. Also, RFC 6749 5.2 mandates400 invalid_grantfor a bad refresh token and Atlassian often returns403, but the code only branches on status401, so the reauth path may rarely fire in practice. Fix: parse the JSON error body, route onlyinvalid_grantto reauth, and trigger on 400/403 as well as 401. -
Summary truncation splits multibyte characters.
jira-cloud-client.js:337-plain.slice(0, 255)operates on UTF-16 code units; a 255-unit slice of emoji/multibyte content can end on a lone high surrogate that JSON-serializes to a replacement char. The only truncation test uses'A'.repeat(300)(pure ASCII) so it structurally cannot catch this. Fix: make truncation code-point aware and add boundary tests at 255/256 with multibyte content. -
Only 429 is retried; 5xx, network errors, and timeouts are not, and no fetch timeout is enforced.
rate-limit-aware-http-client.js:66returns immediately for any non-429. Transient 502/503/504 (common during Atlassian incidents) fail the user request, and since the underlying fetch has no timeout, a hung Atlassian connection hangs the Lambda until it is killed. Fix: add bounded retry for 5xx and network errors (respecting the item 5 deadline, and not on the non-idempotent POST without the item 4 guard), and enforce or document a required per-request timeout. -
The re-auth handshake is undocumented and unreachable for factory consumers.
refreshAuthHeaders/forceRefreshAuthHeadersare the only methods that actually consume a refresh token, but nothing in the package calls them -#withAuthRetryonly throwsTOKEN_REFRESH_REQUIRED. YetTicketClientFactory.createreturns aBaseTicketClientwith no accessor to itscredentialManager, so a factory-based consumer literally cannot call the refresh methods this package exports, and the README's Error Handling section never explains the loop. Fix: document the catch-TOKEN_REFRESH_REQUIREDthen refresh then retry handshake, and either expose the manager or move the reactive refresh inside the client. -
Thin observability on the failure paths that matter. There is no log when all retries are exhausted and the final 429 is returned (
rate-limit-aware-http-client.js:79returns silently), reauth/refresh events are only atdebug, and there are no metrics for retry-exhausted / quota-exhausted / reauth-triggered. In prod these failure modes are a black box. Fix: warn/error on retry exhaustion and reauth transitions, and emit structured metric fields. -
Secret Manager read amplification:
getAuthHeadersreads SM on every API call.oauth-credential-manager.js- a single user action (paginated list + list types + create + status) is easily 6-10+GetSecretValuecalls, and paginatedlistProjectsreads per page. At scale this contends against account-wide SM throttling and manifests as auth failures unrelated to the tokens. Fix: cache the decoded secret in-memory with a short TTL on the read path (keep race-recovery reads uncached). -
Composition and shared-primitive divergence from the rest of the monorepo. Every other client exposes
static createFrom(context)and pullsenv/log/fetchfrom context; this package requires callers to hand-assemblesmClient/httpClient/log. It also hand-rolls UUID/URL validation twice instead of using@adobe/spacecat-shared-utils(isValidUUID,isValidUrl,tracingFetch), andindex.d.tstypessmClientas bareobjectwhile the code assumes the AWS SDK v2getSecretValue()/putSecretValue()call shape (v3 usesclient.send(command)). Fix: addcreateFrom(context), adopt the shared utils, and define a minimalSecretsManagerClientinterface in the types (or file tracked fast-follows). -
Field/payload mappings are exercised but never asserted, so regressions stay green under 100% coverage.
labels.map(String)output (jira-cloud-client.js:448), the attachment Blob's MIME type and byte content (597), andsanitizeFilename's control-char/null-byte/over-length handling (350) are all covered by tests that assert only call counts or partial results. Add assertions on the serialized request payloads.
Minor (Nice to Have)
- README states 429 fallback backoff is "1s, 2s, 4s, 8s" but the code uses
BASE_BACKOFF_MS = 2000producing 2s, 4s, 8s, 16s. On-call responders reading this during an incident will trust wrong numbers.README.md - Set
redirect: 'error'(or'manual') on credential-bearing fetches in#fetchNewTokensand Jira calls. The SSRF fix pins the initial URL but the injected fetch follows redirects by default, so a redirect could carry the Bearer token off the pinned host.oauth-credential-manager.js - Magic-byte validation is signature-presence only:
text/*types skip checks entirely, and the WebP check is satisfiable by any RIFF container. Consider aligning the filename extension to the validated MIME type.jira-cloud-client.js:292 listProjectsreturn type disagrees across JSDoc ({key, name}), code ({id, key, name}), and README - andidis exactly whatlistIssueTypes(projectId)needs, so dropping it misleads consumers on how to chain the calls.jira-cloud-client.js:239#buildTicketResulthost-mismatch guard is a security invariant markedc8 ignorewith zero coverage; make it testable rather than ignored.jira-cloud-client.js:743- Inline code inside
**bold**silently drops thestrongmark (hard-codedcodeMarks), and nested lists / ordered-liststartvalues are untested.markdown-to-adf.js:50 summaryis marked required in the types but never validated present, so an omitted summary is sent as""and fails opaquely at Jira;labels/componentsthrow a rawTypeErroron non-array input. Fail fast locally likeprojectKeydoes.jira-cloud-client.js
Recommendations
- The two highest-leverage fixes are: (a) validate the SM secret schema on read with the same rigor already applied to Atlassian's response, and (b) thread a single deadline/AbortSignal through both retry layers. Together these resolve items 1, 5, and 10 at the root.
- Add a targeted test that feeds
<script>,<img onerror>, deeply nested>, and empty code fences tomarkdownToAdfand asserts either a clean result or a categorized error - the current Jira test file does not cover the html-token or deep-nesting paths, which is why they were missed.
Assessment
Ready to merge? No, with fixes. The prior review rounds handled the classic security checklist well, but the approval rests on invariants (never-crashing plain-text conversion, non-expiring-token detection, retry ceilings) that are not actually enforced. Items 1-3 are attacker-triggerable or availability-critical and no existing test exercises them, so they should block merge. Items 4-15 are important robustness and contract gaps that are far cheaper to address now than after consumers wire against the current shape.
Next Steps
- Fix the 3 critical items first (SM secret-shape validation, rotated-token-drop, ADF recursion/crash guard).
- Address the important items - the idempotency/retry-deadline pair (items 4, 5, 10) and the plain-text-contract reconciliation (items 6, 7) are the most consequential.
- Minor items are optional polish.
Out of scope, worth tracking: the DB task_management_provider enum, connection-validation schemas, and the Vault-to-Lambda env-var injection for the OAuth client credentials all live outside this repo but are hard dependencies of this package's correctness.
Jira: SITES-44690
Summary
Adds the
spacecat-shared-ticket-clientpackage — a provider-agnostic ticket management client with Jira Cloud as the first implementation.Package Structure
Key Capabilities
JiraCloudClient
uploadAttachment()with:listProjects()andlistIssueTypes()with pagination supportRateLimitAwareHttpClient
Retry-Afterheader supportOAuthCredentialManager
Security
api.atlassian.com)cloudIdvalidated as a UUIDsiteUrlvalidated ashttps://*.atlassian.netsiteUrlused for display only, never as a request targetorganizationIdandconnectionIdvalidated as UUIDs before Secret Manager path interpolationDesign
SOLID-compliant
Composition Root
TicketClientFactory.create()wires together:client.createTicket()entry pointSingle-level inheritance
JiraCloudClientextendsBaseTicketClientfor shared constructor logicAlignment
Implements the v1 scope from mysticat-architecture PR #150.
Coverage includes:
Secret path pattern:
Test Plan
Covered Scenarios
createTicket()listProjects()listIssueTypes()uploadAttachment()Retry-Afterhandling#withAuthRetry)REQUIRES_REAUTHpropagation