Skip to content

feat: add spacecat-shared-ticket-client package with Jira Cloud integration (SITES-44690)#1701

Open
prithipalpatwal wants to merge 29 commits into
mainfrom
feat/SITES-44690-ticket-client
Open

feat: add spacecat-shared-ticket-client package with Jira Cloud integration (SITES-44690)#1701
prithipalpatwal wants to merge 29 commits into
mainfrom
feat/SITES-44690-ticket-client

Conversation

@prithipalpatwal

@prithipalpatwal prithipalpatwal commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Jira: SITES-44690

Summary

Adds the spacecat-shared-ticket-client package — a provider-agnostic ticket management client with Jira Cloud as
the first implementation.

Package Structure

src/
├── adf/
│   └── markdown-to-adf.js              # Markdown → ADF conversion (marked.lexer)
├── clients/                                                                                                      
│   ├── base-ticket-client.js           # Abstract base (createTicket contract)
│   └── jira-cloud-client.js            # Jira REST API v3 provider                                               
├── credentials/                                                                                                
│   ├── credential-manager-factory.js   # Auth strategy dispatch
│   └── oauth-credential-manager.js     # OAuth 2.0 3LO token lifecycle                                           
├── http/
│   └── rate-limit-aware-http-client.js # 429 retry with backoff + quota fail-fast                                
├── ticket-client-factory.js            # Composition root (wires everything)                                     
├── index.js                            # Barrel exports
└── index.d.ts                          # TypeScript declarations                                                 

Key Capabilities

JiraCloudClient

  • Targets Jira REST API v3
  • Markdown-to-ADF description conversion (full Markdown — headings, lists, code blocks, bold/italic, links)
  • 255-character summary truncation (server-enforced; validated client-side)
  • Non-empty summary validation before API call
  • Label whitespace validation (Jira silently rejects labels with spaces)
  • dueDate calendar validity check via UTC-safe component round-trip (e.g. rejects 2026-02-31)
  • Code-point-safe truncation in sanitizeSummary and sanitizeFilename — avoids splitting surrogate pairs in
    emoji strings
  • uploadAttachment() with MIME type whitelist, magic-byte validation, filename sanitization
  • listProjects() and listIssueTypes() with pagination support

RateLimitAwareHttpClient

  • Automatic HTTP 429 retry with exponential backoff
  • Retry-After header support
  • Quota-exhaustion fail-fast (no retry on hour-long quota windows)
  • Non-idempotent safety: POST/PUT/PATCH skip 5xx and network-error retry — server may have processed before
    failing; retrying would risk duplicate ticket creation

OAuthCredentialManager

  • OAuth 2.0 3LO token lifecycle management
  • In-process #refreshLock / #forceRefreshLock Promise gates to serialise concurrent same-Lambda refresh calls
  • getAuthHeaders(bypassCache=true) to bypass the 5-min TTL cache on Jira API 401 retry
  • Concurrent-safe refresh via Secret Manager pre-read
  • GRANT_REVOKED recovery on Atlassian 403 — re-reads SM twice (rotation-race check), then writes requiresReauth: true if no fresh tokens found (see Grant Failure section below)
  • Secret Manager write retry with exponential backoff + idempotency tokens
  • SM secret accessToken structure validation — surfaces corrupt secrets early instead of sending Bearer undefined to Jira
  • Supports minimal { requiresReauth: true } SM payloads (e.g. written by admin tooling to force re-auth)
  • Refresh token reuse: Atlassian permits the same refresh token to be exchanged multiple times within a 10-minute
    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

  • SSRF protection: all requests routed through fixed Atlassian gateway (api.atlassian.com); cloudId validated
    as UUID; siteUrl validated as https://*.atlassian.net and used for display only, never as a request target
  • Path traversal protection: organizationId and connectionId validated as UUIDs before Secret Manager path
    interpolation; attachment filenames sanitized
  • DoS protection in markdownToAdf: 1 MB input cap (UTF-8 byte-accurate via Buffer.byteLength), depth limit 20,
    fallback to plain paragraph

Design

  • SOLID-compliant
    • SRP: ADF conversion in its own module; createTicket() delegates to #validateTicketInput() +
      #buildTicketBody() private methods
    • OCP: Map-based provider dispatch
    • ISP: Provider-specific methods implemented only on subclasses
    • DIP: All dependencies injected via constructors
  • Composition Root: TicketClientFactory.create() wires credential manager → rate-limited HTTP client →
    provider client; consumers use a single client.createTicket() entry point
  • Single-level inheritance: JiraCloudClient extends BaseTicketClient; no deep hierarchy

Grant Failure — Atlassian 403 + GRANT_REVOKED

Atlassian returns HTTP 403 for all refresh token failures. Two error codes are known:

Code Source Scenarios
invalid_grant [Official Atlassian
docs](https://developer.atlassian.com/cloud/oauth/getting-started/refresh-tokens/) Token expired (90-day
inactivity), token not rotated, password changed
unauthorized_client Empirically verified Token reused past 10-min window, app revoked, malformed/truncated
token, random/empty string

Both trigger code: 'GRANT_REVOKED' — callers check a semantic signal, not a raw HTTP status.
#recoverFromRevokedGrant re-reads SM twice (immediate + 200ms wait for rotation-race recovery). If no fresh
tokens found, writes requiresReauth: true to 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}

@github-actions

Copy link
Copy Markdown

This PR will trigger a minor release when merged.

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

  1. [Critical] Secret path injection - buildSecretPath interpolates organizationId/connectionId without validation; a malformed DB record with ../ can traverse the Secrets Manager namespace - src/ticket-client-factory.js:44 (details inline)
  2. [Important] Unused production dependencies - @adobe/fetch and @adobe/spacecat-shared-utils are declared in dependencies but never imported anywhere in src/; ships dead weight to consumers - package.json:41 (details inline)
  3. [Important] Uncapped Retry-After - #resolveWaitMs trusts the server-sent Retry-After header without an upper bound; a value of 86400 blocks the Lambda for a full day - src/http/rate-limit-aware-http-client.js:62 (details inline)
  4. [Important] ClientRequestToken misuse - 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?.length works for Buffer/Uint8Array but will silently under-count multi-byte strings if the type contract ever loosens - consider Buffer.byteLength or a type guard - src/clients/jira-cloud-client.js:203
  • nit: c8, mocha, mocha-multi-reporters are used in scripts/config but not in devDependencies - 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_REGEX greedily 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: sanitizeFilename strips ../ 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 👎.

Comment thread packages/spacecat-shared-ticket-client/src/ticket-client-factory.js
Comment thread packages/spacecat-shared-ticket-client/package.json

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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: buildSecretPath now validates both IDs as UUIDs before interpolation
  • Unused production dependencies: @adobe/fetch and @adobe/spacecat-shared-utils removed; c8, mocha, mocha-multi-reporters added to devDependencies
  • Uncapped Retry-After: MAX_WAIT_MS = 30_000 ceiling applied via Math.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 like report..final.pdf - stripping path separators (/ and \) instead would be more targeted - src/clients/jira-cloud-client.js:125
  • nit: UUID_REGEX comment says "Matches lowercase UUIDs" but the regex uses the /i flag accepting uppercase - align the comment with the code or remove the flag - src/ticket-client-factory.js:34
  • suggestion: validate process.env.NODE_ENV against an allowlist or /^[a-z]+$/ pattern in buildSecretPath for 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_REGEX lookbehind will strip trailing ) from legitimate URLs with balanced parens like Wikipedia links https://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 👎.

@prithipalpatwal prithipalpatwal force-pushed the feat/SITES-44690-ticket-client branch from 4a5adf2 to 4b583ae Compare June 23, 2026 15:52
@MysticatBot

Copy link
Copy Markdown

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

@prithipalpatwal prithipalpatwal requested review from MysticatBot and removed request for MysticatBot June 23, 2026 18:10

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

  1. [Important] sanitizeFilename strips all .. sequences and allows path separators through - src/clients/jira-cloud-client.js:127 (details inline)
  2. [Important] process.env.NODE_ENV interpolated into SM secret path without validation - src/ticket-client-factory.js:56 (details inline)
  3. [Important] Token refresh 401 from Atlassian never triggers setRequiresReauth - src/credentials/oauth-credential-manager.js:84 (details inline)
  4. [Important] siteUrl validation 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.js does not verify RateLimitAwareHttpClient export despite it being exported from src/index.js - test/index.test.js:22
  • nit: CLOUD_ID_REGEX is lowercase-only (no /i) while UUID_REGEX in the factory has /i - inconsistent casing contract - src/clients/jira-cloud-client.js:19 vs src/ticket-client-factory.js:34
  • nit: #buildAdfDescription JSDoc 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-client to the Package Catalog's API clients row in the root CLAUDE.md so the discovery index stays current
  • nit: test/setup-env.js copyright header says 2026, rest of files say 2025 - test/setup-env.js:2

Previously flagged, now resolved

  • Secret path injection: buildSecretPath now validates both IDs as UUIDs before interpolation
  • Unused production dependencies: removed from dependencies; test tooling moved to devDependencies
  • Uncapped Retry-After: MAX_WAIT_MS = 30_000 ceiling applied via Math.min
  • ClientRequestToken misuse: SHA-256 hash of full access token replaces naive .slice(0, 32)

Comment thread packages/spacecat-shared-ticket-client/src/ticket-client-factory.js
Comment thread packages/spacecat-shared-ticket-client/src/clients/jira-cloud-client.js Outdated
@MysticatBot MysticatBot added the complexity:high High complexity PR label Jun 23, 2026

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 setRequiresReauth via try/catch on #fetchNewTokens with .status property 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_REGEX comment says "Matches lowercase UUIDs" but the regex uses the /i flag accepting uppercase - align the comment or remove the flag for consistency with CLOUD_ID_REGEX - src/ticket-client-factory.js:34
  • nit: sanitizeFilename comment "Strip path separators" is on the line above the eslint-disable, making it visually ambiguous which .replace it 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 👎.

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

  1. [Important] TicketData interface missing priority, dueDate, components, parent fields - TypeScript consumers will get compile errors - src/index.d.ts:22 (details inline)
Non-blocking (6): minor issues and suggestions
  • suggestion: validate dueDate format (/^\d{4}-\d{2}-\d{2}$/) before passing to Jira - consistent with how cloudId, ticketKey, and organizationId are validated - src/clients/jira-cloud-client.js:160
  • suggestion: add a max-pages safety bound to listProjects pagination loop to prevent unbounded requests if Jira API misbehaves - src/clients/jira-cloud-client.js:112
  • nit: .nycrc.json sets branches: 100 but 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-client in the API clients row and increment the package count from 22 to 23 - both catalogs are now stale
  • nit: CLOUD_ID_REGEX uses /i (accepts uppercase) while UUID_REGEX in the factory does not - intentional per comments but a one-line note on CLOUD_ID_REGEX explaining why would prevent future fixes - src/clients/jira-cloud-client.js:20
  • nit: test/setup-env.js copyright header says 2026 while all other files say 2025 - test/setup-env.js:2

Previously flagged, now resolved

  • Secret path injection: buildSecretPath validates both IDs as UUIDs before interpolation
  • Unused production dependencies: removed; test tooling moved to devDependencies
  • Uncapped Retry-After: MAX_WAIT_MS = 30_000 ceiling applied via Math.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 👎.

Comment thread packages/spacecat-shared-ticket-client/src/index.d.ts
@prithipalpatwal

Copy link
Copy Markdown
Contributor Author

Addressed review 4572484771 in 73f62962:

  • Added priority, dueDate, components, parent to TicketData in src/index.d.ts
  • Made description optional (matches implementation)
  • Added dueDate YYYY-MM-DD validation + test
  • Added listProjects max-pages safety bound (100)
  • Documented CLOUD_ID_REGEX /i rationale

79 tests passing, 100% coverage. CI green.

@prithipalpatwal prithipalpatwal requested review from MysticatBot and removed request for MysticatBot June 25, 2026 17:38

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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-client from 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_PAGES safety bound has no dedicated test proving the loop terminates at 100 when isLast is perpetually false - a small stub test would prevent accidental removal - src/clients/jira-cloud-client.js:252

Previously flagged, now resolved

  • TypeScript TicketData interface now includes priority, dueDate, components, parent; description made optional
  • dueDate format validated via regex before passing to Jira
  • listProjects pagination bounded by MAX_PAGES = 100
  • CLOUD_ID_REGEX /i flag 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 👎.

@prithipalpatwal prithipalpatwal force-pushed the feat/SITES-44690-ticket-client branch from 661f5a7 to d7db958 Compare June 29, 2026 20:24

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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: forceRefreshAuthHeaders lock coalesces concurrent callers behind the first caller's Promise, silently discarding the second caller's usedAuthHeader parameter - 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 refreshAuthHeaders when 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 getAuthHeaders receives true on the 401 retry path in #withAuthRetry tests - the behavior is tested but the specific bypassCache argument is not, so a regression to the cached path would not be caught - test/jira-cloud-client.test.js
  • nit: dueDate validation uses new Date(y, m-1, d) which interprets in local timezone - safe in Lambda (UTC) but consider Date.UTC + getUTC* for local-dev parity - src/clients/jira-cloud-client.js:200
  • nit: listProjects() and listIssueTypes() declared on both BaseTicketClient and JiraCloudClient in TypeScript - the subclass redeclaration is redundant since it extends the base - src/index.d.ts:54

Previously flagged, now resolved

  • TypeScript BaseTicketClient type declarations now include listProjects() and listIssueTypes() - 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
  • dueDate validation uses component round-trip to catch impossible calendar dates (e.g. Feb 31)
  • #withAuthRetry 401 path uses getAuthHeaders(true) bypassing the 30s TTL cache
  • .nycrc.json branches threshold aligned to repo standard (97%)
  • listProjects pagination test asserts startAt query parameter values
  • c8 ignore start/end block 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>

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:205 and fix function-paren-newline in oauth-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 forceRefreshAuthHeaders accurately documents lock coalescing semantics
  • Lock-release-after-rejection test confirms .finally() clears the refresh lock after 503
  • bypassCache=true assertion pins the 401-retry cache-bypass contract
  • Redundant listProjects/listIssueTypes removed from JiraCloudClient TS declarations (inherited from BaseTicketClient)

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 👎.

prithipalpatwal and others added 9 commits July 2, 2026 20:51
…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>

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:47 vs CLAUDE.md:74
  • suggestion: siteUrl validation permits bare atlassian.net hostname (no subdomain required) - display-only so no security impact, but adding || siteUrlParsed.hostname === 'atlassian.net' to the rejection would match the documented *.atlassian.net contract - src/clients/jira-cloud-client.js:88

Previously flagged, now resolved

  • Secret path injection: buildSecretPath validates both IDs as UUIDs before interpolation
  • Unused production dependencies: removed; test tooling moved to devDependencies
  • Uncapped Retry-After: MAX_WAIT_MS = 30_000 ceiling applied via Math.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 with concurrent-race recovery
  • siteUrl validation: upgraded to new URL() hostname parsing
  • TypeScript TicketData interface: includes priority, dueDate, components, parent; description made optional
  • dueDate validation: format validated via regex + calendar round-trip (Date.UTC)
  • listProjects pagination: bounded by MAX_PAGES = 100 with dedicated test
  • AGENTS.md and CLAUDE.md: updated to 23 packages with ticket-client in API clients row
  • ADF link nodes validate href scheme via allowlist (https/http/mailto)
  • #writeReauthFlag re-reads secret inside retry loop; clock.restore() moved to afterEach
  • parent field validated against TICKET_KEY_REGEX
  • Unwrapped httpClient passed to CredentialManagerFactory (auth endpoint has different rate-limit semantics)
  • TypeScript declarations expose refreshAuthHeaders and forceRefreshAuthHeaders
  • redirect: 'error' on credential-bearing fetches
  • XSS link-scheme test covers javascript: and data: 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
  • #withAuthRetry 401 path uses getAuthHeaders(true) bypassing TTL cache
  • .nycrc.json branches 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 👎.

prithipalpatwal and others added 9 commits July 2, 2026 23:34
…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>

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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' : undefined attaches an explicit undefined-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 only invalid_grant and unauthorized_client trigger 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.json correctly 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: buildSecretPath validates both IDs as UUIDs before interpolation
  • Unused production dependencies: removed; test tooling moved to devDependencies
  • Uncapped Retry-After: MAX_WAIT_MS = 30_000 ceiling applied via Math.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 with concurrent-race recovery
  • siteUrl validation: upgraded to new URL() hostname parsing
  • TypeScript TicketData interface: includes priority, dueDate, components, parent; description made optional
  • dueDate validation: format validated via regex + calendar round-trip (Date.UTC)
  • listProjects pagination: bounded by MAX_PAGES = 100 with dedicated test
  • AGENTS.md and CLAUDE.md: updated to 23 packages with ticket-client in API clients row
  • ADF link nodes validate href scheme via allowlist (https/http/mailto)
  • #writeReauthFlag re-reads secret inside retry loop; clock.restore() moved to afterEach
  • parent field validated against TICKET_KEY_REGEX
  • Unwrapped httpClient passed to CredentialManagerFactory (auth endpoint has different rate-limit semantics)
  • TypeScript declarations expose refreshAuthHeaders and forceRefreshAuthHeaders
  • redirect: 'error' on credential-bearing fetches
  • XSS link-scheme test covers javascript: and data: 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
  • #withAuthRetry 401 path uses getAuthHeaders(true) bypassing TTL cache
  • .nycrc.json branches 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: createTicket split into #validateTicketInput + #buildTicketBody
  • Grant error handling: semantic err.code = 'GRANT_REVOKED' replaces synthetic status mapping; unauthorized_client added 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>

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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-valued code property on non-grant errors
  • Negative test added for unrecognized error codes (403 access_denied does 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 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants