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 29 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
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 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:
forceRefreshAuthHeaderslock coalesces concurrent callers behind the first caller's Promise, silently discarding the second caller'susedAuthHeaderparameter - add a JSDoc note documenting this coalescing behavior so future maintainers understand the contract -src/credentials/oauth-credential-manager.js:197 - suggestion: add a test verifying lock release after rejection (call
refreshAuthHeaderswhen Atlassian returns 503, assert the lock clears and the next call fires a fresh request) - the.finally()is correct but a regression test on the release path strengthens confidence -test/oauth-credential-manager.test.js - suggestion: assert
getAuthHeadersreceivestrueon the 401 retry path in#withAuthRetrytests - the behavior is tested but the specificbypassCacheargument is not, so a regression to the cached path would not be caught -test/jira-cloud-client.test.js - nit:
dueDatevalidation usesnew Date(y, m-1, d)which interprets in local timezone - safe in Lambda (UTC) but considerDate.UTC+getUTC*for local-dev parity -src/clients/jira-cloud-client.js:200 - nit:
listProjects()andlistIssueTypes()declared on bothBaseTicketClientandJiraCloudClientin TypeScript - the subclass redeclaration is redundant since it extends the base -src/index.d.ts:54
Previously flagged, now resolved
- TypeScript
BaseTicketClienttype declarations now includelistProjects()andlistIssueTypes()- factory consumers can call them without unsafe cast - In-process Promise locks (
#refreshLock/#forceRefreshLock) serialize concurrent same-Lambda callers, preventing double-consumption of single-use refresh tokens dueDatevalidation uses component round-trip to catch impossible calendar dates (e.g. Feb 31)#withAuthRetry401 path usesgetAuthHeaders(true)bypassing the 30s TTL cache.nycrc.jsonbranches threshold aligned to repo standard (97%)listProjectspagination test assertsstartAtquery parameter valuesc8 ignore start/endblock on markdownToAdf catch fallback with justification comment- Undocumented field caps (labels, components, issueType, priority) correctly reverted - Atlassian is the validator of record
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 18s | Cost: $6.18 | Commit: d4af7eab3cfe8ec64a76fa3a4de3bb9bbcf2f74e
If this code review was useful, please react with 👍. Otherwise, react with 👎.
…review - Use Date.UTC + getUTC* for dueDate validation (timezone-safe) - Add JSDoc concurrency note on forceRefreshAuthHeaders lock semantics - Add lock-release-after-rejection test for refreshAuthHeaders - Assert bypassCache=true on 401 retry in #withAuthRetry test - Remove redundant listProjects/listIssueTypes from JiraCloudClient TS declaration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Hey @prithipalpatwal,
Verdict: Approve - all five prior non-blocking suggestions are correctly implemented; only a trivial lint fix remains for CI.
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.
Note: CI checks are currently failing - the Test job fails due to lint violations introduced in this commit (line exceeding max-len at jira-cloud-client.js:205 and function-paren-newline in oauth-credential-manager.test.js:689). Trivial line-wrapping fix required.
Non-blocking (1): minor issues and suggestions
- nit: lint violations in this commit cause CI failure - wrap the long line in
jira-cloud-client.js:205and fixfunction-paren-newlineinoauth-credential-manager.test.js:689-packages/spacecat-shared-ticket-client/
Previously flagged, now resolved
- Date.UTC + getUTC* for dueDate validation eliminates local-timezone skew in non-Lambda environments
- JSDoc concurrency note on
forceRefreshAuthHeadersaccurately documents lock coalescing semantics - Lock-release-after-rejection test confirms
.finally()clears the refresh lock after 503 bypassCache=trueassertion pins the 401-retry cache-bypass contract- Redundant
listProjects/listIssueTypesremoved fromJiraCloudClientTS declarations (inherited fromBaseTicketClient)
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 0m 51s | Cost: $3.78 | Commit: e936d1ac71190fd222660a572b88238c1f867ca9
If this code review was useful, please react with 👍. Otherwise, react with 👎.
…line) - Wrap long dueDate component check in jira-cloud-client.js:205 - Flatten multi-line OAuthCredentialManager constructor call in test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… documented Atlassian's OpenAPI spec has no maxLength on summary; the 255-char limit is server-enforced (HTTP 400) not formally specified. Update comment accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nt methods POST/PUT/PATCH requests are not retried on 5xx or network errors because the server may have processed the request before failing — retrying would risk creating duplicate resources (e.g. duplicate Jira tickets). 429 rate-limit retries remain for ALL methods since Jira rejects the request outright and nothing is created. GET/HEAD/OPTIONS/DELETE retain full retry behaviour (idempotent). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…window Atlassian rotating refresh tokens have a 10-minute reuse leeway where the same token can be exchanged multiple times without triggering breach detection. Updated class and method JSDoc to reflect this — our concurrency guards (locks, pre-read SM, recovery) are defensive-in-depth, not correctness-critical for most concurrent refresh scenarios. Ref: https://developer.atlassian.com/cloud/jira/software/oauth-2-3lo-apps/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rent Lambda behaviour Updated class and method JSDoc with empirically verified Atlassian refresh token behaviour: - Same refresh token reusable within 10-minute leeway (verified: 5 calls, all 200; after 10 min, all return unauthorized_client) - Concurrent Lambdas both succeed — last writer to SM wins, no token loss - Guards (locks, pre-read SM, recovery) are defense-in-depth for efficiency, not correctness-critical - 401 on refresh only fires for genuine revocation, not concurrent use Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…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 👎.
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}