feat: add spacecat-shared-ticket-client package with Jira Cloud integration (SITES-44690)#1701
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
…bber - I1: #writeReauthFlag checks for fresh tokens before writing — prevents clobbering a concurrent Lambda's valid tokens with requiresReauth: true - M3: Validate summary is non-empty before API call — avoids round-trip with non-descriptive Jira 400 - M4: Validate labels contain no whitespace — Jira rejects labels with spaces but the error does not indicate which field failed - M7: Validate SM secret has accessToken string — surfaces corrupt secret early instead of sending 'Bearer undefined' to Jira Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nput cap
- #readSecret: allow requiresReauth secrets without accessToken — admin
tools may write { requiresReauth: true } as a minimal payload; the old
validation threw a confusing error instead of surfacing REQUIRES_REAUTH
- markdownToAdf: use Buffer.byteLength for the 1MB input cap instead of
String.length (UTF-16 code units) — emoji-dense input could exceed 1MB
in UTF-8 bytes while appearing under the cap in code units. Binary
search finds the code-point boundary that fits within the byte limit
without splitting surrogate pairs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spread to code points before slicing to avoid splitting surrogate pairs
in emoji filenames. Consistent with sanitizeSummary which already uses
the same [...spread].slice().join('') pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Hey @prithipalpatwal,
Verdict: Approve - no new issues since the last approval; the 12 intervening commits are merge-from-main only.
Complexity: HIGH - large diff; new package scaffolding across 28 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 (28 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: AGENTS.md Architecture Overview header still says "23 packages" but the CLAUDE.md catalog row lists more client names (mac-giver-client, project-engine-client, user-manager-client) that AGENTS.md omits - the two catalogs are diverging in which clients they enumerate -
AGENTS.md:47vsCLAUDE.md:74 - suggestion:
siteUrlvalidation permits bareatlassian.nethostname (no subdomain required) - display-only so no security impact, but adding|| siteUrlParsed.hostname === 'atlassian.net'to the rejection would match the documented*.atlassian.netcontract -src/clients/jira-cloud-client.js:88
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 + calendar round-trip (Date.UTC)
- listProjects pagination: bounded by
MAX_PAGES = 100with dedicated test - AGENTS.md and CLAUDE.md: updated to 23 packages with
ticket-clientin API clients row - ADF link nodes validate
hrefscheme via allowlist (https/http/mailto) #writeReauthFlagre-reads secret inside retry loop;clock.restore()moved toafterEachparentfield validated againstTICKET_KEY_REGEX- Unwrapped
httpClientpassed toCredentialManagerFactory(auth endpoint has different rate-limit semantics) - TypeScript declarations expose
refreshAuthHeadersandforceRefreshAuthHeaders redirect: 'error'on credential-bearing fetches- XSS link-scheme test covers
javascript:anddata:schemes with mixed-case coverage - ADF DoS: depth guard (MAX_DEPTH=20) + 1 MB input cap + try/catch fallback
- In-process Promise locks serialize concurrent same-Lambda callers
#withAuthRetry401 path usesgetAuthHeaders(true)bypassing TTL cache.nycrc.jsonbranches threshold aligned to repo standard (97%)- SM write failure no longer escalates to requiresReauth (returns valid token, logs warning)
- lint violations fixed (CI passing)
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 16s | Cost: $9.13 | Commit: c573580a1a3858d372d22f36e9cd4b266c771a4e
If this code review was useful, please react with 👍. Otherwise, react with 👎.
…tBody from createTicket SRP fix: createTicket() was mixing input validation, Jira field mapping, and HTTP orchestration in one 83-line method. Extract two private methods so each has a single reason to change. No behavior change. 198 tests pass, 100% line/statement coverage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Empirically verified: Atlassian returns HTTP 403 with error body
{"error":"unauthorized_client"} for both:
- Expired refresh token reuse window (>10 min)
- App revoked by the user
Neither scenario was in GRANT_ERRORS, so effectiveStatus stayed 403
and the err.status === 401 guard in #doForceRefreshAuthHeaders never
fired — reauth recovery was silently skipped.
Fix: add 'unauthorized_client' to GRANT_ERRORS so it maps to synthetic
401, triggering #recoverFromAtlassian401() and requiresReauth write.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… token behavior Update #recoverFromAtlassian401 and class-level JSDoc to reflect actual observed behavior from live testing: - HTTP status is 403 (not 400/401) with error unauthorized_client - "ALL tokens returned unauthorized_client" was imprecise: only the ORIGINAL token was tested post-window; derived tokens retain independent 90-day TTL - Document all four scenarios that produce 403 unauthorized_client Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…or code Previously #fetchNewTokens mapped grant-level Atlassian errors to a synthetic err.status=401 so callers could check err.status === 401. This was fragile — callers depended on a manufactured HTTP status rather than a meaningful semantic code. Replace with err.code = 'GRANT_REVOKED' thrown for all grant-level failures. Callers now check err.code === 'GRANT_REVOKED' which is: - explicit about the failure reason - independent of Atlassian's HTTP status (which varies: 400/403) - immune to future status code changes from Atlassian Rename #recoverFromAtlassian401 → #recoverFromRevokedGrant to match. Update tests to use realistic 403/unauthorized_client mocks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rationale Add empirically verified scenario table to class JSDoc: Token >10 min (stale SM) | 403 unauthorized_client | User must reconnect App revoked | 403 unauthorized_client | User must reconnect Clarify #recoverFromRevokedGrant: - Explain the rotation-race case (200ms wait IS needed for Lambda B to find Lambda A's fresh tokens after a concurrent forceRefresh) - Explain the genuinely-dead-grant case (both scenarios → requiresReauth) - Fix stale "Atlassian 401" log messages to say GRANT_REVOKED Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rically verified unauthorized_client at HTTP 403 — verified in live test today. invalid_grant, invalid_token, access_denied — assumed from RFC 6749 §5.2, not empirically tested. Note this distinction in the comment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… unauthorized_client empirically observed
Empirical testing confirmed Atlassian returns HTTP 403 {"error":"unauthorized_client"}
as a catch-all for ALL refresh token failures. RFC 6749 §5.2 codes invalid_grant,
invalid_token, and access_denied were never observed at the Atlassian token endpoint.
- Replace GRANT_ERRORS Set with direct `errorCode === 'unauthorized_client'` check
- Remove `invalid_grant` test (code path can never be reached)
- Expand scenario table in class JSDoc with all empirically verified failure cases
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… as GRANT_REVOKED Official Atlassian 3LO docs document 403 invalid_grant for expired/revoked tokens. Empirical testing returned 403 unauthorized_client for all manual test scenarios. Both codes now trigger GRANT_REVOKED → reauth recovery path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ios in JSDoc Replace vague "all manual test scenarios" with the specific four cases tested: token reused past 10-min window, app revoked, malformed token, random/empty string. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Hey @prithipalpatwal,
Verdict: Approve - all prior blocking findings are correctly fixed; only non-blocking nits remain after 9 new commits.
Complexity: HIGH - large diff; new package scaffolding across 28 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 (28 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 (4): minor issues and suggestions
- nit:
code: isGrantRevoked ? 'GRANT_REVOKED' : undefinedattaches an explicitundefined-valued property to non-grant errors; use conditional spread instead (...(isGrantRevoked && { code: 'GRANT_REVOKED' })) to avoid noise in structured logging -src/credentials/oauth-credential-manager.js:407 - nit: test descriptions still say "non-401" (e.g.
'propagates non-401 Atlassian errors...') but the production code no longer checks for 401 from the token endpoint; rename to "non-grant" for clarity -test/oauth-credential-manager.test.js:665 - suggestion: add a negative test for HTTP 403 with an unrecognized error code (e.g.
access_denied) to document that onlyinvalid_grantandunauthorized_clienttrigger GRANT_REVOKED - the code is correct but the contract is not pinned by a test -test/oauth-credential-manager.test.js - nit: README states "100% statement, branch, function, and line coverage is enforced" but
.nycrc.jsoncorrectly implements 97% branches per repo convention - align the README text -packages/spacecat-shared-ticket-client/README.md:395
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 + calendar round-trip (Date.UTC)
- listProjects pagination: bounded by
MAX_PAGES = 100with dedicated test - AGENTS.md and CLAUDE.md: updated to 23 packages with
ticket-clientin API clients row - ADF link nodes validate
hrefscheme via allowlist (https/http/mailto) #writeReauthFlagre-reads secret inside retry loop;clock.restore()moved toafterEachparentfield validated againstTICKET_KEY_REGEX- Unwrapped
httpClientpassed toCredentialManagerFactory(auth endpoint has different rate-limit semantics) - TypeScript declarations expose
refreshAuthHeadersandforceRefreshAuthHeaders redirect: 'error'on credential-bearing fetches- XSS link-scheme test covers
javascript:anddata:schemes with mixed-case coverage - ADF DoS: depth guard (MAX_DEPTH=20) + 1 MB input cap + try/catch fallback
- In-process Promise locks serialize concurrent same-Lambda callers
#withAuthRetry401 path usesgetAuthHeaders(true)bypassing TTL cache.nycrc.jsonbranches threshold aligned to repo standard (97%)- SM write failure no longer escalates to requiresReauth (returns valid token, logs warning)
- Lint violations fixed (CI passing)
- SRP extraction:
createTicketsplit into#validateTicketInput+#buildTicketBody - Grant error handling: semantic
err.code = 'GRANT_REVOKED'replaces synthetic status mapping;unauthorized_clientadded as recognized grant error
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 23s | Cost: $6.57 | Commit: 7f55dbb47118c5569034e9628c23771e9d85421e
If this code review was useful, please react with 👍. Otherwise, react with 👎.
…review 1. Use conditional spread instead of explicit undefined for non-grant errors (avoids noise in structured logging) 2. Rename test descriptions from "non-401" to "non-grant" — code no longer checks for 401 from the token endpoint 3. Add negative test for access_denied (403) to document that only invalid_grant and unauthorized_client trigger GRANT_REVOKED 4. Align README coverage claim with .nycrc.json (97% branches, not 100%) 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 nits are correctly addressed; no new issues introduced.
Complexity: HIGH - large diff; new package scaffolding across 28 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 (28 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 (0): no issues found
No new findings on this commit.
Previously flagged, now resolved
- README coverage statement corrected to match repo convention (97% branch coverage)
- Conditional spread replaces explicit
undefined-valuedcodeproperty on non-grant errors - Negative test added for unrecognized error codes (403
access_denieddoes not trigger GRANT_REVOKED) - Test descriptions renamed from "non-401" to "non-grant" for accuracy
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 0m 45s | Cost: $2.35 | Commit: 6a9f79b7eac636d95917c877bbf7f04965437db7
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,
Thanks for the thorough turnaround - this closes the loop on the prior review. I re-verified each earlier finding against the current code. Every blocking item is resolved, so this supersedes the earlier request for changes.
Previously flagged, now resolved
- Token expiry detection:
#isExpirednow treats a non-finiteexpiresAtas expired, and#readSecretvalidates the secret structure, so a corrupt/partial secret can no longer serve a stale token forever. - Rotated-token safety: an SM write failure now surfaces as
SM_WRITE_FAILEDand returns the freshly-issued Atlassian token instead of force-markingrequiresReauth, so a transient SM blip no longer bricks the connection. - ADF DoS: depth cap (20), 1 MB input cap, and a try/catch plain-text fallback make the converter crash-safe on untrusted input.
- Duplicate-ticket risk: non-idempotent methods are excluded from 5xx/network retry, and the non-idempotency is documented on
createTicket. - OAuth error handling: the token-endpoint error body is now parsed, and both
invalid_grantandunauthorized_client(403) route to the reauth path. - Empty code fence, multibyte summary truncation, redirect pinning (
redirect: 'error'), per-attempt fetch timeout, idempotent 5xx/network retry, SM read caching, failure-path logging, README backoff numbers,listProjectsreturn type, and summary-presence validation are all addressed.
I also checked the refactored concurrency and caching code (in-process refresh locks, secret cache) for regressions and found none - the locks clear in finally and the cache is invalidated before every write.
Non-blocking follow-ups
These are documentation/contract cleanups, not defects. Fine to address in a follow-up.
-
The
createTicketJSDoc still contradicts what the code does.jira-cloud-client.js:164- the doc says "Plain-text description (no ADF markup)... untrusted suggestion content is never rendered as structured markup (spec 13)," and the class doc at line 132 says "plain-text leaf nodes only." But the implementation runsmarkdownToAdf(description)and the README (lines 81-99) documents and encourages full markdown - bold, links, tables, code blocks. Now that raw HTML tokens are stripped and link schemes are allowlisted, this is no longer an injection risk, but the security contract stated in the JSDoc is simply false: structured markup is rendered. A consumer trusting that comment will misjudge what reaches Jira. Suggestion: reconcile the JSDoc and class doc with the README/code - either drop the "plain-text only / spec 13" language or state explicitly that markdown is parsed with HTML stripped and link schemes allowlisted. -
The token-refresh handshake is undocumented and not reachable through the factory.
getAuthHeadersis pure-read and throwsTOKEN_REFRESH_REQUIREDon expiry; the only methods that actually refresh (refreshAuthHeaders/forceRefreshAuthHeaders) live onOAuthCredentialManager, whichTicketClientFactory.create(ticket-client-factory.js:96) does not expose on the returned client. The README Error Handling section (lines 160-175) covers 429 andrequires re-authorizationbut never mentionsTOKEN_REFRESH_REQUIREDor how to complete the refresh-and-retry loop. If the intended model is that the consuming service constructs its ownOAuthCredentialManager(which is exported), document that explicitly with an end-to-end example; otherwise expose the credential manager on the factory-created client. -
MAX_WAIT_MSis applied only to theRetry-Afterbranch, not the exponential-backoff fallback (rate-limit-aware-http-client.js:231). It stays under the ceiling today only becauseMAX_RETRIES=4andBASE_BACKOFF_MS=2000; the "hard ceiling on any single retry wait" comment (lines 20-21) will silently stop holding if either constant grows. Wrap the fallback return inMath.min(..., MAX_WAIT_MS). There is also still no aggregate retry deadline across the nested#withAuthRetry+ 429 loop, though the new per-attempt 30 s timeout bounds the worst case considerably.
Assessment
Ready to merge? Yes. Every blocking item from the prior review is resolved and I found no regressions in the refactored code. The three items above are documentation-accuracy and defensive-hardening cleanups, not exploitable defects, and can be handled as follow-ups. Approving.
## @adobe/spacecat-shared-ticket-client-v1.0.0 (2026-07-06) ### Features * add spacecat-shared-ticket-client package with Jira Cloud integration (SITES-44690) ([#1701](#1701)) ([75e9204](75e9204)) ### Bug Fixes * **ticket-client:** mark package private + sync lockfile to unblock main release pipeline ([#1782](#1782)) ([b338c7e](b338c7e))
|
🎉 This PR is included in version @adobe/spacecat-shared-ticket-client-v1.0.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
…#1783) ## Summary - Adds `tokenRefreshedAt` (ISO 8601 timestamp) to the SM secret payload written by `OAuthCredentialManager#writeTokens()` on inline token refresh - Previously, auth-service wrote this field at OAuth callback time, but ticket-client's inline refresh overwrote the secret without it — the timestamp disappeared after the first inline refresh ## Context Found during alignment review of auth-service PR #595 against ticket-client PR #1701. The field is observability-only (not read by any code path), but ops tooling that inspects SM secrets to check token freshness loses the timestamp. ## Test plan - [x] Existing test updated to verify `tokenRefreshedAt` is present and recent in written SM payload - [x] 200 tests passing, 100% coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Jira: SITES-44690
Summary
Adds the
spacecat-shared-ticket-clientpackage — a provider-agnostic ticket management client with Jira Cloud asthe first implementation.
Package Structure
Key Capabilities
JiraCloudClient
dueDatecalendar validity check via UTC-safe component round-trip (e.g. rejects 2026-02-31)sanitizeSummaryandsanitizeFilename— avoids splitting surrogate pairs inemoji strings
uploadAttachment()with MIME type whitelist, magic-byte validation, filename sanitizationlistProjects()andlistIssueTypes()with pagination supportRateLimitAwareHttpClient
Retry-Afterheader supportfailing; retrying would risk duplicate ticket creation
OAuthCredentialManager
#refreshLock/#forceRefreshLockPromise gates to serialise concurrent same-Lambda refresh callsgetAuthHeaders(bypassCache=true)to bypass the 5-min TTL cache on Jira API 401 retryrequiresReauth: trueif no fresh tokens found (see Grant Failure section below)accessTokenstructure validation — surfaces corrupt secrets early instead of sendingBearer undefinedto Jira{ requiresReauth: true }SM payloads (e.g. written by admin tooling to force re-auth)window — concurrent Lambda refreshes both succeed (last writer to SM wins, no token loss); guards are
defense-in-depth for efficiency, not correctness-critical
Security
api.atlassian.com);cloudIdvalidatedas UUID;
siteUrlvalidated ashttps://*.atlassian.netand used for display only, never as a request targetorganizationIdandconnectionIdvalidated as UUIDs before Secret Manager pathinterpolation; attachment filenames sanitized
markdownToAdf: 1 MB input cap (UTF-8 byte-accurate viaBuffer.byteLength), depth limit 20,fallback to plain paragraph
Design
createTicket()delegates to#validateTicketInput()+#buildTicketBody()private methodsTicketClientFactory.create()wires credential manager → rate-limited HTTP client →provider client; consumers use a single
client.createTicket()entry pointJiraCloudClient extends BaseTicketClient; no deep hierarchyGrant Failure — Atlassian 403 + GRANT_REVOKED
Atlassian returns HTTP 403 for all refresh token failures. Two error codes are known:
invalid_grantunauthorized_clientBoth trigger
code: 'GRANT_REVOKED'— callers check a semantic signal, not a raw HTTP status.#recoverFromRevokedGrantre-reads SM twice (immediate + 200ms wait for rotation-race recovery). If no freshtokens found, writes
requiresReauth: trueto SM and throws.Alignment
Implements the v1 scope from mysticat-architecture PR
#150. Coverage includes all API endpoints, field
mappings, error handling, and retry flows.
Secret path pattern:
/mysticat/task-management/{orgId}/{connectionId}