Skip to content

Commit e406e65

Browse files
Merge pull request #12 from openfetch-js/features/security-url-redaction-cache-warn
Features/security url redaction cache warn
2 parents de60862 + 9235a78 commit e406e65

25 files changed

Lines changed: 400 additions & 54 deletions

CONTRIBUTING.md

Lines changed: 29 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,69 @@
1-
# Contributing to @hamdymohamedak/openfetch
1+
# Contributing on GitHub
22

3-
Thank you for taking the time to improve this project. The following guidelines keep reviews predictable and the package stable across runtimes.
3+
This document describes how to participate in **@hamdymohamedak/openfetch** through GitHub: issues, discussions, and pull requests.
44

5-
## Ground rules
5+
## What we look for in changes
6+
7+
These rules keep reviews predictable and the library safe across runtimes:
68

79
1. **Stay on `fetch`.** Do not add XMLHttpRequest, alternate fetch shims as required dependencies, or polyfills that assume a browser.
810
2. **Avoid browser-only globals.** Do not reference `window`, `document`, `localStorage`, `sessionStorage`, `WebSocket`, or `EventSource` in library code.
911
3. **Keep the public API small.** Prefer new behavior as optional middleware or clearly documented config rather than breaking existing callers.
10-
4. **TypeScript source of truth.** All implementation lives under `src/`; `dist/` is compiled output from `npm run build`.
11-
12-
## How to contribute
12+
4. **TypeScript source of truth.** Implementation lives under `src/`; `dist/` is build output from `npm run build`.
1313

14-
### Reporting issues
14+
## Reporting issues
1515

16-
Open an issue with:
16+
[Open an issue](https://github.com/openfetch-js/OpenFetch/issues) on this repository with:
1717

1818
- Runtime and version (Node, Bun, Deno, worker, browser).
1919
- Minimal code sample and expected vs actual behavior.
2020
- If relevant, the target URL shape (without secrets).
2121

22-
### Suggesting features
22+
## Suggesting features
2323

24-
Describe the use case and whether it can live in userland (middleware) vs core. Large features should be discussed in an issue before a heavy pull request.
24+
Open an issue first. Describe the use case and whether it can live in userland (middleware) vs core. Large features should be agreed in an issue before a large pull request.
2525

26-
### Pull requests
26+
## Pull requests
2727

28-
#### Branch naming
28+
### Branch naming
2929

30-
Use **one branch per concern**: a single feature **or** a single bug fix, not unrelated changes in the same branch.
30+
Use **one branch per concern**: one feature **or** one bug fix, not unrelated changes together.
3131

32-
- **Features:** `features/<short-description>`for example `features/add-retry`, `features/cache-key-override`.
33-
- **Bug fixes:** `bugs/<short-description>`for example `bugs/timeout-signal`, `bugs/cache-key-collision`.
32+
- **Features:** `features/<short-description>`e.g. `features/add-retry`, `features/cache-key-override`.
33+
- **Bug fixes:** `bugs/<short-description>`e.g. `bugs/timeout-signal`, `bugs/cache-key-collision`.
3434

3535
Use **kebab-case** after the prefix. Keep the slug short but specific enough that reviewers can tell what the branch is for.
3636

37-
1. **Fork** the repository and create a branch from `main` following the [branch naming](#branch-naming) rules above.
38-
2. **Implement** your change in `src/`. Match existing formatting and naming.
39-
3. **Build** locally: `npm run build` (must pass with no TypeScript errors).
37+
### Workflow
38+
39+
1. **Fork** this repository and create a branch from `main` using the [branch naming](#branch-naming) rules above.
40+
2. **Implement** in `src/`. Match existing formatting and naming.
41+
3. **Build** locally: `npm run build` (no TypeScript errors).
4042
4. **Document** user-visible behavior in `README.md` and, if structural, in `docs/PROJECT_FLOW.md`.
41-
5. **Open a PR** into `main` with:
43+
5. **Open a pull request** into `main` with:
4244
- A clear title and description.
43-
- What changed and why (motivation).
45+
- What changed and why.
4446
- Any breaking changes called out explicitly.
4547

46-
### Commit messages
47-
48-
Use short, imperative subjects (for example `Add cache key override option`). Optional body for context. Consistent history helps maintainers and consumers.
49-
50-
### Code review
51-
52-
Maintainers may request tests, naming tweaks, or doc updates. Keeping changes scoped to one concern per PR speeds up merge.
48+
### Before you push
5349

54-
## Local development
50+
From a clone of your fork:
5551

5652
```bash
5753
npm install
5854
npm run build
5955
npm run test:security
6056
```
6157

62-
`npm run test:security` runs the checks under `security-tests/`. `tsc` remains the compile gate for every change.
58+
`npm run test:security` runs `security-tests/`. The TypeScript compile step is the gate for every change.
6359

64-
## Publishing
65-
66-
Publishing to npm is reserved for maintainers after version bump and changelog review. Consumers should install from the registry or from a tagged release, not from unreviewed branches.
67-
68-
npm **requires two-factor authentication** (or a granular access token with publish rights) for `npm publish`. Logging in with the browser (`npm login`) is not enough by itself.
60+
### Commit messages
6961

70-
1. On [npmjs.com](https://www.npmjs.com/) go to **Account → Two-Factor Authentication** and enable **Authorization and writes** (authenticator app recommended).
71-
2. From the package root:
62+
Use short, imperative subjects (e.g. `Add cache key override option`). Add a body when extra context helps reviewers reading the PR on GitHub.
7263

73-
```bash
74-
npm publish --access public --otp=123456
75-
```
76-
77-
Use the current 6-digit code from your authenticator app. Do not commit or share OTPs.
64+
### Code review
7865

79-
**Alternative:** create a **Granular Access Token** at npm with permission to publish this package (and “bypass 2FA” only if your org policy allows it), then configure npm to use that token for `registry.npmjs.org`.
66+
Maintainers may ask for tests, naming tweaks, or doc updates on the PR. Smaller, single-concern PRs are easier to review and merge.
8067

8168
## Code of conduct
8269

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Register **`retry` before `timeout`** so retries wrap the full inner stack. Use
8585

8686
**Retry timing:** `retry.timeoutTotalMs` measures elapsed time with a monotonic clock (`performance.now()` when available), so the budget is not skewed by system clock changes. By default (`retry.enforceTotalTimeout !== false`), each attempt merges a deadline into the request `signal` so an in-flight `fetch` aborts when the budget runs out (`ERR_RETRY_TIMEOUT`). Set `retry.enforceTotalTimeout: false` to enforce the budget only between attempts. `retry.timeoutPerAttemptMs` sets `timeout` for every attempt inside the retry middleware. Each `dispatch` uses `clearTimeout` in a `finally` block so per-attempt timers are not left dangling.
8787

88-
**Debug:** Default logs omit request headers. Use `debug({ includeRequestHeaders: true, maskHeaders: ["authorization"], maskStrategy: "partial" })` for values like `Bearer ****abcd`, or `maskStrategy: "hash"` for a short fingerprint. **`maskHeaderValues`** supports the same strategies when building your own logs.
88+
**Debug:** Default logs omit request headers. Logged URLs **redact common sensitive query parameters** (`token`, `code`, `password`, …); set `maskUrlQuery: false` to log raw URLs (avoid in production). Use `debug({ includeRequestHeaders: true, maskHeaders: ["authorization"], maskStrategy: "partial" })` for values like `Bearer ****abcd`, or `maskStrategy: "hash"` for a short fingerprint. **`maskHeaderValues`** supports the same strategies when building your own logs.
8989

9090
### Execution model
9191

@@ -102,7 +102,7 @@ Understanding order helps avoid surprises with retries, timeouts, and escape hat
102102

103103
### Memory cache and authentication
104104

105-
The default cache key is ``METHOD fullUrl``. For **authenticated or per-user** GETs, also pass header names that affect the response so entries do not leak across users:
105+
The default cache key is ``METHOD fullUrl``. The first request with **`Authorization` or `Cookie`** and no `varyHeaderNames` / custom `key` triggers a **one-time `console.warn`** (suppress with `suppressAuthCacheKeyWarning: true` if you only cache public data). For **authenticated or per-user** GETs, also pass header names that affect the response so entries do not leak across users:
106106

107107
```ts
108108
createCacheMiddleware(store, {
@@ -129,7 +129,7 @@ For URLs influenced by untrusted input, call `assertSafeHttpUrl(url)` before req
129129

130130
### Errors and logging
131131

132-
`OpenFetchError.toShape()` omits `config.auth` but may still include **response `data` and `headers`**. For client-facing or shared logs, use `toShape({ includeResponseData: false, includeResponseHeaders: false })`. The error instance itself can still hold full `config`; do not expose it raw.
132+
`OpenFetchError.toShape()` omits `config.auth` and by default **redacts sensitive query parameters** in the `url` field; pass `redactSensitiveUrlQuery: false` only for trusted diagnostics. It may still include **response `data` and `headers`**. For client-facing or shared logs, use `toShape({ includeResponseData: false, includeResponseHeaders: false })`. The error instance itself can still hold full `config`; do not expose it raw.
133133

134134
## Documentation
135135

SECURITY.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ openfetch is a thin `fetch` wrapper. Callers supply URLs, headers, and bodies. T
77
- **Axios-class proxy CVEs (e.g. CVE-2025-62718 / `NO_PROXY` normalization)** — openfetch does **not** implement axios-style `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` matching. Outbound routing follows the host runtime’s `fetch` (and any platform proxy). Those CVEs therefore do **not** map to openfetch code paths; policy still belongs at the app, proxy, or mesh layer.
88

99
- **Network trust** — You choose endpoints. Blocking private IPs, metadata hosts, or open redirects is an **application** concern for partially trusted URLs.
10-
- **Secrets**`toShape()` on `OpenFetchError` avoids echoing `config.auth`, but the full `Error` object may still carry `config` (including credentials). Response bodies and headers in `toShape()` may still contain tokens or PII; use `toShape({ includeResponseData: false, includeResponseHeaders: false })` when serializing for untrusted clients or broad logs. Never send raw errors to untrusted clients without redaction.
10+
- **Secrets**`toShape()` on `OpenFetchError` avoids echoing `config.auth`, but the full `Error` object may still carry `config` (including credentials). Response bodies and headers in `toShape()` may still contain tokens or PII; use `toShape({ includeResponseData: false, includeResponseHeaders: false })` when serializing for untrusted clients or broad logs. By default, `toShape()` also **redacts common sensitive query parameters** in the serialized `url` (for example `token`, `code`, `password`); use `redactSensitiveUrlQuery: false` only for trusted diagnostics. The `debug()` plugin applies the same redaction to logged URLs. Never send raw errors to untrusted clients without redaction.
1111
- **Supply chain** — Install this package from npm or a verified Git tag; verify integrity with your package manager.
1212

1313
## Server-side usage and SSRF
@@ -34,6 +34,8 @@ Mitigations (combine as appropriate):
3434

3535
Unauthenticated, fully public GETs may keep the default key.
3636

37+
The middleware emits a **one-time `console.warn`** the first time it sees `Authorization` or `Cookie` on a cacheable request while `varyHeaderNames` is empty and no custom `key` is set. Suppress with `suppressAuthCacheKeyWarning: true` when you know the cache is safe (for example anonymous-only endpoints).
38+
3739
## Retry and non-idempotent methods
3840

3941
By default, `createRetryMiddleware` retries network/parse failures and configured HTTP error statuses **only** for `GET`, `HEAD`, `OPTIONS`, and `TRACE`, to reduce duplicate side effects (for example double charges on `POST`).

dist/core/cache.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ export type CacheMiddlewareOptions = {
3434
* Use for authenticated or personalized GETs, e.g. `["authorization", "cookie"]`, so entries do not leak across users.
3535
*/
3636
varyHeaderNames?: string[];
37+
/**
38+
* When false (default), the first cached GET/HEAD that includes `Authorization` or `Cookie`
39+
* without `varyHeaderNames` or a custom `key` triggers a one-time `console.warn` about
40+
* possible cross-user cache leakage. Set true if you intentionally cache anonymous responses
41+
* only or use another isolation mechanism.
42+
*/
43+
suppressAuthCacheKeyWarning?: boolean;
3744
};
3845
/** Append a stable suffix from header values so cache keys differ per auth/cookie (etc.). */
3946
export declare function appendCacheKeyVaryHeaders(baseKey: string, headers: Record<string, string> | undefined, headerNames: string[]): string;

dist/core/cache.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/core/cache.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ function shallowCloneResponse(r) {
3333
data: r.data,
3434
};
3535
}
36+
function headersHaveAuthOrCookie(headers) {
37+
if (!headers)
38+
return false;
39+
for (const k of Object.keys(headers)) {
40+
const l = k.toLowerCase();
41+
if (l === "authorization" || l === "cookie")
42+
return true;
43+
}
44+
return false;
45+
}
3646
/** Append a stable suffix from header values so cache keys differ per auth/cookie (etc.). */
3747
export function appendCacheKeyVaryHeaders(baseKey, headers, headerNames) {
3848
if (!headerNames.length)
@@ -87,6 +97,7 @@ export function createCacheMiddleware(store, options) {
8797
const varyHeaderNames = options?.varyHeaderNames ?? [];
8898
const methods = new Set((options?.methods ?? ["GET", "HEAD"]).map((m) => m.toUpperCase()));
8999
const inflight = new Map();
100+
let authCacheKeyWarningIssued = false;
90101
return async (ctx, next) => {
91102
if (ctx.request.memoryCache?.skip) {
92103
await next();
@@ -100,6 +111,16 @@ export function createCacheMiddleware(store, options) {
100111
const urlString = buildURL(ctx.request.url, ctx.request);
101112
const rawKey = options?.key?.({ request: ctx.request, url: urlString }) ??
102113
`${method} ${urlString}`;
114+
if (!authCacheKeyWarningIssued &&
115+
options?.suppressAuthCacheKeyWarning !== true &&
116+
options?.key === undefined &&
117+
varyHeaderNames.length === 0 &&
118+
headersHaveAuthOrCookie(ctx.request.headers)) {
119+
authCacheKeyWarningIssued = true;
120+
if (typeof console !== "undefined" && typeof console.warn === "function") {
121+
console.warn("[openfetch] createCacheMiddleware: request uses Authorization or Cookie but varyHeaderNames is empty and no custom key is set; cache entries may be shared across users. Use varyHeaderNames: [\"authorization\", \"cookie\"] or options.key, or set suppressAuthCacheKeyWarning: true if this is intentional.");
122+
}
123+
}
103124
const key = appendCacheKeyVaryHeaders(rawKey, ctx.request.headers, varyHeaderNames);
104125
const ttlMs = ctx.request.memoryCache?.ttlMs ?? defaultTtl;
105126
const swrMs = ctx.request.memoryCache?.staleWhileRevalidateMs ?? defaultSwr;

dist/core/error.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ export type OpenFetchErrorToShapeOptions = {
2323
* Default true (backward compatible).
2424
*/
2525
includeResponseHeaders?: boolean;
26+
/**
27+
* When true (default), replaces sensitive query parameter values in the serialized `url`
28+
* (e.g. `token`, `code`, `api_key`). Set false only for trusted internal diagnostics.
29+
*/
30+
redactSensitiveUrlQuery?: boolean;
31+
/** Extra query parameter names to redact (case-insensitive); merged with the built-in list. */
32+
sensitiveQueryParamNames?: string[];
2633
};
2734
export declare class OpenFetchError<T = unknown> extends Error {
2835
config?: OpenFetchConfig;

dist/core/error.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)