-
Notifications
You must be signed in to change notification settings - Fork 0
token #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
token #34
Conversation
WalkthroughRequest signing moved from local HMAC to a remote signature service. ApiClient now requests a SignatureResponse from /generate-signature, appends X-API-Nonce, X-API-Timestamp, X-API-Intended-Origin and X-API-Signature headers, and throws HttpError on non-ok responses. Store calls removed nonce parameters. Added SignatureResponse interface and two devDependency bumps. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as Caller
participant C as ApiClient
participant S as Signature Service (/generate-signature)
participant A as API Server
U->>C: get(url) / post(url, data)
Note over C: create nonce, timestamp, origin
C->>S: POST /generate-signature { nonce, origin }
alt signature OK
S-->>C: SignatureResponse (signature, tries, cadence)
C->>A: GET/POST url\nHeaders: X-API-Nonce, X-API-Timestamp,\nX-API-Intended-Origin, X-API-Signature
alt 200 OK
A-->>C: 200 + body
C-->>U: parsed result
else 304 Not Modified
A-->>C: 304
C-->>U: serve cached
else Error
A-->>C: non-ok
C-->>U: throw HttpError
end
else signature error
S-->>C: non-ok
C-->>U: throw HttpError
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/stores/api/client.ts (2)
119-136: Fix body double-read error in POST method.The error handling reads the response body twice, causing the "Body is unusable" error seen in pipeline failures.
public async post<T>(url: string, nonce: string, data: object): Promise<T> { const headers = this.createHeaders(); const fullUrl = new URL(url, this.basedURL); await this.appendSignature(nonce, headers, fullUrl.href); const response = await fetch(fullUrl.href, { method: 'POST', headers: headers, body: JSON.stringify(data), }); if (!response.ok) { - throw new HttpError(response, await response.text()); + const errorBody = await response.text(); + throw new HttpError(response, errorBody); } return await response.json(); }
138-173: Fix body double-read error in GET method.Same issue as in the POST method - the error handling reads the response body twice.
public async get<T>(url: string, nonce: string): Promise<T> { const headers = this.createHeaders(); const cached = this.getFromCache<T>(url); const fullUrl = new URL(url, this.basedURL); if (cached) { headers.append('If-None-Match', cached.etag); } await this.appendSignature(nonce, headers, fullUrl.href); const response = await fetch(fullUrl.href, { method: 'GET', headers: headers, }); if (response.status === 304) { console.log(`%c[CACHE] 304 Not Modified for "${url}". Serving from cache.`, 'color: #007acc;'); // We can safely assert cached is not null here. return cached!.data; } if (!response.ok) { - throw new HttpError(response, await response.text()); + const errorBody = await response.text(); + throw new HttpError(response, errorBody); } const eTag = response.headers.get('ETag'); const payload = await response.json(); if (eTag) { this.setToCache(url, eTag, payload); } return payload; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (3)
package.json(1 hunks)src/stores/api/client.ts(4 hunks)src/stores/api/response/signature-response.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/stores/api/client.ts (2)
src/stores/api/response/signature-response.ts (1)
SignatureResponse(1-9)src/stores/api/http-error.ts (1)
HttpError(1-15)
🪛 GitHub Check: vitest
src/stores/api/client.ts
[failure] 105-105: tests/stores/api/client.test.ts > ApiClient > caches get requests and serves from cache
HttpError: API request failed with status 304:
❯ ApiClient.getSignature src/stores/api/client.ts:105:10
❯ ApiClient.appendSignature src/stores/api/client.ts:112:19
❯ ApiClient.get src/stores/api/client.ts:147:3
❯ tests/stores/api/client.test.ts:68:18
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { body: '', status: 304 }
🪛 GitHub Actions: Unit Tests
src/stores/api/client.ts
[error] 135-135: TypeError: Body is unusable: Body has already been read
[error] 105-105: HttpError: API request failed with status 304
[error] 166-166: TypeError: Body is unusable: Body has already been read
🔇 Additional comments (4)
package.json (1)
35-35: DevDependency updates look good.The version bumps for
@vitejs/plugin-vue(5.2.1 → 6.0.1) andeslint-config-prettier(10.1.2 → 10.1.8) are reasonable updates that shouldn't affect production code.Also applies to: 39-39
src/stores/api/response/signature-response.ts (1)
1-9: Interface definition looks good.The
SignatureResponseinterface properly defines the structure for the remote signature service response with appropriate types.src/stores/api/client.ts (2)
80-80: Confirm timestamp format compatibility (src/stores/api/client.ts:80): No tests or documentation specify the expected format—verify with the signature service that it accepts a stringified Unix‐second timestamp and consider adding corresponding tests or docs.
87-109: Multiple critical issues in getSignature method.
- Body double-read error: The response body is read twice (lines 105 and 108), causing the "Body is unusable" error
- HTTP 304 handling: The method throws an error for 304 status, but 304 is used for caching in the GET method
Fix the body double-read issue:
private async getSignature(nonce: string, origin: string): Promise<SignatureResponse> { const headers = this.createHeaders(); const fullUrl = new URL('generate-signature', this.basedURL); headers.append('X-API-Intended-Origin', origin); const response = await fetch(fullUrl.href, { method: 'POST', headers: headers, body: JSON.stringify({ nonce: nonce, public_key: this.apiKey, username: this.apiUsername, - timestamp: this.timestamp, + timestamp: this.getCurrentTimestamp(), }), }); if (!response.ok) { - throw new HttpError(response, await response.text()); + const errorBody = await response.text(); + throw new HttpError(response, errorBody); } return await response.json(); }Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/stores/api/client.ts (1)
143-161: Update tests to use new client.get/post signatures
In tests/stores/api/client.test.ts (lines 52–59, 67–90), replace
client.get('…', nonce)→client.get('…')client.post('…', nonce, data)→client.post('…', data)
♻️ Duplicate comments (1)
src/stores/api/client.ts (1)
89-92: Make signature endpoint configurable and enforce HTTPS (prod).Use env-configurable endpoint instead of hardcoded path; keep HTTPS guard.
- const fullUrl = new URL('generate-signature', this.basedURL); + const endpoint = import.meta.env.VITE_SIGNATURE_ENDPOINT || 'generate-signature'; + const fullUrl = new URL(endpoint, this.basedURL);
🧹 Nitpick comments (2)
src/stores/api/client.ts (2)
35-40: Optional: enforce HTTPS for base API in production.Guard against misconfiguration similar to the signature endpoint.
constructor(options: ApiClientOptions) { this.env = options.env; this.apiKey = options.apiKey; this.apiUsername = options.apiUsername; - this.basedURL = `${import.meta.env.VITE_API_URL}`; + this.basedURL = `${import.meta.env.VITE_API_URL}`; + if (this.isProd() && new URL(this.basedURL).protocol !== 'https:') { + throw new Error('API base URL must be HTTPS in production'); + } }
33-33: Nit: typo in property name.Consider renaming basedURL → baseURL for clarity (wide refactor).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/stores/api/client.ts(4 hunks)src/stores/api/store.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/stores/api/store.ts (2)
src/stores/api/client.ts (1)
ApiResponse(24-27)src/stores/api/http-error.ts (1)
parseError(17-27)
src/stores/api/client.ts (3)
src/stores/api/response/signature-response.ts (1)
SignatureResponse(1-9)src/stores/api/http-error.ts (1)
HttpError(1-15)tests/stores/api/client.test.ts (2)
cacheKey(52-61)it(30-82)
🪛 GitHub Check: vitest
src/stores/api/client.ts
[failure] 111-111: tests/stores/api/client.test.ts > ApiClient > caches get requests and serves from cache
HttpError: API request failed with status 304:
❯ ApiClient.getSignature src/stores/api/client.ts:111:10
❯ ApiClient.appendSignature src/stores/api/client.ts:123:21
❯ ApiClient.get src/stores/api/client.ts:173:3
❯ tests/stores/api/client.test.ts:68:18
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { body: '', status: 304 }
🪛 GitHub Actions: Unit Tests
src/stores/api/client.ts
[error] 160-160: npm test: ApiClient.post() attempted to read the response body as JSON after it had already been read (Body is unusable: Body has already been read).
[error] 111-111: npm test: HttpError thrown for non-OK response (304) by reading response.text().
[error] 192-192: npm test: ApiClient.get() attempted to read response.json() after the body had already been read.
🔇 Additional comments (4)
src/stores/api/store.ts (2)
46-46: LGTM: store calls updated to new client signature (no nonce).All invocations correctly drop the nonce and match the refactored client API.
Also applies to: 55-55, 64-64, 73-73, 82-82, 91-91, 100-100, 109-109, 118-118, 127-127
118-118: Confirm backend contract for posts now using POST. Ensure the/postsendpoint accepts POST withfilterspayload and that idempotency/caching (including any CDNs or proxies) behaves as expected; no otherclient.getcalls for this endpoint remain outside tests.src/stores/api/client.ts (2)
72-74: Good: per-request timestamp.Fresh timestamps avoid stale-request rejections.
97-97: Verify origin format used for signing.Confirm the signature service expects the full URL (href) vs just the origin (scheme+host). If origin-only is required, use new URL(origin).origin.
| private createHeaders(): Headers { | ||
| const headers = new Headers(); | ||
|
|
||
| headers.append('X-API-Key', this.apiKey); | ||
| headers.append('X-Request-ID', uuidv4()); | ||
| headers.append('User-Agent', 'oullin/web-app'); | ||
| headers.append('X-API-Timestamp', this.timestamp); | ||
| headers.append('X-API-Username', this.apiUsername); | ||
| headers.append('Content-Type', 'application/json'); | ||
| headers.append('X-API-Timestamp', this.getCurrentTimestamp().toString()); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Remove forbidden User-Agent header; set Content-Type only for POST.
Browsers block setting User-Agent; also avoid sending Content-Type on GET to prevent unnecessary preflights.
Apply:
private createHeaders(): Headers {
const headers = new Headers();
headers.append('X-API-Key', this.apiKey);
headers.append('X-Request-ID', uuidv4());
- headers.append('User-Agent', 'oullin/web-app');
headers.append('X-API-Username', this.apiUsername);
- headers.append('Content-Type', 'application/json');
headers.append('X-API-Timestamp', this.getCurrentTimestamp().toString());
return headers;
}And set Content-Type only in post():
public async post<T>(url: string, data: object): Promise<T> {
const nonce = this.createNonce();
const headers = this.createHeaders();
+ headers.set('Content-Type', 'application/json');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private createHeaders(): Headers { | |
| const headers = new Headers(); | |
| headers.append('X-API-Key', this.apiKey); | |
| headers.append('X-Request-ID', uuidv4()); | |
| headers.append('User-Agent', 'oullin/web-app'); | |
| headers.append('X-API-Timestamp', this.timestamp); | |
| headers.append('X-API-Username', this.apiUsername); | |
| headers.append('Content-Type', 'application/json'); | |
| headers.append('X-API-Timestamp', this.getCurrentTimestamp().toString()); | |
| private createHeaders(): Headers { | |
| const headers = new Headers(); | |
| headers.append('X-API-Key', this.apiKey); | |
| headers.append('X-Request-ID', uuidv4()); | |
| // Removed User-Agent | |
| headers.append('X-API-Username', this.apiUsername); | |
| // Content-Type moved to POST requests only | |
| headers.append('X-API-Timestamp', this.getCurrentTimestamp().toString()); | |
| return headers; | |
| } | |
| public async post<T>(url: string, data: object): Promise<T> { | |
| const nonce = this.createNonce(); | |
| const headers = this.createHeaders(); | |
| headers.set('Content-Type', 'application/json'); | |
| // ...rest of the POST logic... | |
| } |
🤖 Prompt for AI Agents
In src/stores/api/client.ts around lines 76 to 85, remove the
headers.append('User-Agent', ...) and stop adding 'Content-Type' in
createHeaders(); instead leave createHeaders() to add only safe headers (API
key, request id, username, timestamp). Add logic in the post() method to set
'Content-Type: application/json' just before sending the body (e.g., clone
headers and append Content-Type for POST requests) so GET requests never include
Content-Type and the forbidden User-Agent is never set.
| if (!response.ok) { | ||
| throw new HttpError(response, await response.text()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don’t read response body on non-OK; fixes test failures and double-read errors.
Reading response.text() here consumes a shared mocked Response, causing “Body has already been read” later.
- if (!response.ok) {
- throw new HttpError(response, await response.text());
- }
+ if (!response.ok) {
+ throw new HttpError(response, null);
+ }Apply the same change in post() and get() error paths.
Also applies to: 156-158, 187-189
🧰 Tools
🪛 GitHub Check: vitest
[failure] 111-111: tests/stores/api/client.test.ts > ApiClient > caches get requests and serves from cache
HttpError: API request failed with status 304:
❯ ApiClient.getSignature src/stores/api/client.ts:111:10
❯ ApiClient.appendSignature src/stores/api/client.ts:123:21
❯ ApiClient.get src/stores/api/client.ts:173:3
❯ tests/stores/api/client.test.ts:68:18
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { body: '', status: 304 }
🪛 GitHub Actions: Unit Tests
[error] 111-111: npm test: HttpError thrown for non-OK response (304) by reading response.text().
🤖 Prompt for AI Agents
In src/stores/api/client.ts around lines 110-112 (and similarly at 156-158 and
187-189), the code reads await response.text() on non-OK responses which
consumes the response body and causes "Body has already been read" errors in
tests; remove the body read and throw the HttpError with only the Response
object (e.g., throw new HttpError(response)) in each error path so the response
body is not consumed here and callers can decide whether to read it.
| private async appendSignature(nonce: string, headers: Headers, origin: string) { | ||
| const retries = 3; | ||
| let lastError: Error | undefined; | ||
|
|
||
| for (let i = 0; i < retries; i++) { | ||
| try { | ||
| const sigResp = await this.getSignature(nonce, origin); | ||
|
|
||
| headers.append('X-API-Nonce', nonce); | ||
| headers.append('X-API-Intended-Origin', origin); | ||
| headers.append('X-API-Signature', sigResp.signature); | ||
|
|
||
| const canonical = [method.toUpperCase(), uri.pathname || '/', this.getSortedQuery(uri.href), this.apiUsername, this.apiKey, this.timestamp, nonce, content].join('\n'); | ||
| return; | ||
| } catch (error) { | ||
| lastError = error as Error; | ||
|
|
||
| if (i < retries - 1) { | ||
| // Exponential backoff: waits 1 s, then 2 s. | ||
| await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 1000)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return await this.hmacSha256Hex(this.apiKey, canonical); | ||
| throw lastError || new Error('Failed to get signature after multiple retries.'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Harden appendSignature: dev bypass, dedupe headers, faster backoff.
- Use headers.set to avoid duplicates.
- Provide a dev/test bypass to avoid hitting the signature service (unblocks 304 cache test).
- Shorter backoff base to avoid long stalls in dev.
- private async appendSignature(nonce: string, headers: Headers, origin: string) {
- const retries = 3;
+ private async appendSignature(nonce: string, headers: Headers, origin: string) {
+ // Dev/test: skip remote signature to avoid brittle tests and speed local runs
+ if (this.isDev() && !import.meta.env.VITE_SIGNATURE_ENDPOINT) {
+ headers.set('X-API-Nonce', nonce);
+ headers.set('X-API-Intended-Origin', origin);
+ headers.set('X-API-Signature', 'dev'); // harmless placeholder
+ return;
+ }
+ const retries = 3;
let lastError: Error | undefined;
for (let i = 0; i < retries; i++) {
try {
const sigResp = await this.getSignature(nonce, origin);
- headers.append('X-API-Nonce', nonce);
- headers.append('X-API-Intended-Origin', origin);
- headers.append('X-API-Signature', sigResp.signature);
+ headers.set('X-API-Nonce', nonce);
+ headers.set('X-API-Intended-Origin', origin);
+ headers.set('X-API-Signature', sigResp.signature);
return;
} catch (error) {
lastError = error as Error;
if (i < retries - 1) {
- // Exponential backoff: waits 1 s, then 2 s.
- await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 1000));
+ // Exponential backoff: ~100ms, 200ms
+ await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i)));
}
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private async appendSignature(nonce: string, headers: Headers, origin: string) { | |
| const retries = 3; | |
| let lastError: Error | undefined; | |
| for (let i = 0; i < retries; i++) { | |
| try { | |
| const sigResp = await this.getSignature(nonce, origin); | |
| headers.append('X-API-Nonce', nonce); | |
| headers.append('X-API-Intended-Origin', origin); | |
| headers.append('X-API-Signature', sigResp.signature); | |
| const canonical = [method.toUpperCase(), uri.pathname || '/', this.getSortedQuery(uri.href), this.apiUsername, this.apiKey, this.timestamp, nonce, content].join('\n'); | |
| return; | |
| } catch (error) { | |
| lastError = error as Error; | |
| if (i < retries - 1) { | |
| // Exponential backoff: waits 1 s, then 2 s. | |
| await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 1000)); | |
| } | |
| } | |
| } | |
| return await this.hmacSha256Hex(this.apiKey, canonical); | |
| throw lastError || new Error('Failed to get signature after multiple retries.'); | |
| } | |
| private async appendSignature(nonce: string, headers: Headers, origin: string) { | |
| // Dev/test: skip remote signature to avoid brittle tests and speed local runs | |
| if (this.isDev() && !import.meta.env.VITE_SIGNATURE_ENDPOINT) { | |
| headers.set('X-API-Nonce', nonce); | |
| headers.set('X-API-Intended-Origin', origin); | |
| headers.set('X-API-Signature', 'dev'); // harmless placeholder | |
| return; | |
| } | |
| const retries = 3; | |
| let lastError: Error | undefined; | |
| for (let i = 0; i < retries; i++) { | |
| try { | |
| const sigResp = await this.getSignature(nonce, origin); | |
| headers.set('X-API-Nonce', nonce); | |
| headers.set('X-API-Intended-Origin', origin); | |
| headers.set('X-API-Signature', sigResp.signature); | |
| return; | |
| } catch (error) { | |
| lastError = error as Error; | |
| if (i < retries - 1) { | |
| // Exponential backoff: ~100ms, 200ms | |
| await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i))); | |
| } | |
| } | |
| } | |
| throw lastError || new Error('Failed to get signature after multiple retries.'); | |
| } |
🤖 Prompt for AI Agents
In src/stores/api/client.ts around lines 117 to 141, appendSignature currently
uses headers.append (causing duplicate headers), always calls getSignature
(breaking dev/test flows), and uses long 1s base backoff; change headers.append
to headers.set to dedupe header values, add a dev/test bypass (e.g. when
NODE_ENV === 'test' or a passed-in client option) that skips getSignature and
sets X-API-Nonce, X-API-Intended-Origin, and a deterministic X-API-Signature
value for tests, and shorten the exponential backoff base (use 100ms base so
retries wait ~100ms, ~200ms, etc.) while preserving the retry loop and throwing
the last error if all attempts fail.
Summary by CodeRabbit
Chores
Refactor
Bug Fixes