Skip to content

Commit 8b91977

Browse files
pr/supply-chain-metadata
Pr/supply chain metadata
2 parents bf86721 + 314a02d commit 8b91977

16 files changed

Lines changed: 352 additions & 52 deletions

.github/SECURITY.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Security policy
2+
3+
The full threat model, SSRF/cache/retry guidance, and local security checks live in [`SECURITY.md`](../SECURITY.md) at the repository root (also shipped on npm).
4+
5+
## Reporting a vulnerability
6+
7+
Please do **not** open a public issue for undisclosed security defects.
8+
9+
- Prefer a [GitHub private security advisory](https://github.com/openfetch-js/OpenFetch/security/advisories/new) for this repository, or
10+
- Contact the maintainer privately if you cannot use GitHub advisories.
11+
12+
Include enough detail to reproduce or reason about impact. We aim to acknowledge valid reports and coordinate disclosure after a fix is available.

.github/workflows/publish.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Publishes to npm using trusted publishing (OIDC). On npmjs.com, trusted publisher must
2+
# reference this repo: https://github.com/openfetch-js/OpenFetch and workflow file: publish.yml
3+
# Requires: npm CLI >= 11.5.1, Node >= 22.14 (see https://docs.npmjs.com/trusted-publishers )
4+
name: Publish to npm
5+
6+
on:
7+
push:
8+
tags:
9+
- 'v*'
10+
11+
permissions:
12+
contents: read
13+
id-token: write
14+
15+
jobs:
16+
publish:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- uses: actions/setup-node@v4
22+
with:
23+
node-version: '22'
24+
registry-url: 'https://registry.npmjs.org'
25+
26+
- name: Upgrade npm for trusted publishing
27+
run: npm install -g npm@^11.5.1
28+
29+
- run: npm ci
30+
- run: npm test
31+
32+
# No NODE_AUTH_TOKEN: authentication is OIDC when trusted publishing is enabled.
33+
# Provenance is generated automatically for trusted publishing from GitHub Actions.
34+
- run: npm publish

dist/core/error.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export type OpenFetchErrorToShapeOptions = {
3030
redactSensitiveUrlQuery?: boolean;
3131
/** Extra query parameter names to redact (case-insensitive); merged with the built-in list. */
3232
sensitiveQueryParamNames?: string[];
33+
/** Replacement string for redacted query values (default `"[REDACTED]"`). */
34+
sensitiveQueryParamReplacement?: string;
3335
};
3436
export declare class OpenFetchError<T = unknown> extends Error {
3537
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.

dist/core/error.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export class OpenFetchError extends Error {
3939
const redactOpts = {
4040
enabled: options?.redactSensitiveUrlQuery !== false,
4141
paramNames: options?.sensitiveQueryParamNames,
42+
replacement: options?.sensitiveQueryParamReplacement,
4243
};
4344
url = redactSensitiveUrlQuery(url, redactOpts);
4445
const method = (this.config?.method ?? "GET").toUpperCase();

dist/helpers/redactUrlQuery.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ export type RedactUrlQueryOptions = {
55
paramNames?: string[];
66
/** When false, returns `url` unchanged. Default true. */
77
enabled?: boolean;
8+
/** Value substituted for sensitive query parameters (default `"[REDACTED]"`). */
9+
replacement?: string;
810
};
911
/**
1012
* Replaces values of sensitive query parameters for safe logging or serialization.
11-
* Invalid or non-absolute URLs are returned unchanged.
13+
* Supports absolute URLs and common relative forms (`/path?…`, `?only=query`, `api/x?…`).
14+
* Strings that are not valid URLs and do not look like path/query requests are returned unchanged.
1215
*/
1316
export declare function redactSensitiveUrlQuery(url: string, options?: RedactUrlQueryOptions): string;
1417
//# sourceMappingURL=redactUrlQuery.d.ts.map

dist/helpers/redactUrlQuery.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/helpers/redactUrlQuery.js

Lines changed: 96 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,40 +18,112 @@ export const DEFAULT_SENSITIVE_QUERY_PARAM_NAMES = [
1818
"csrf",
1919
"nonce",
2020
];
21+
const DEFAULT_REDACTION = "[REDACTED]";
2122
const DEFAULT_SET = new Set(DEFAULT_SENSITIVE_QUERY_PARAM_NAMES.map((n) => n.toLowerCase()));
23+
function normalizeKey(k) {
24+
return k.toLowerCase().replace(/[-_]/g, "");
25+
}
26+
function buildPartialFragments(sensitive) {
27+
const out = [];
28+
for (const s of sensitive) {
29+
const n = normalizeKey(s);
30+
if (n !== "")
31+
out.push(n);
32+
}
33+
return out;
34+
}
35+
const DEFAULT_PARTIAL_FRAGMENTS = buildPartialFragments(DEFAULT_SET);
36+
/**
37+
* Path- or query-shaped strings that are safe to resolve with a dummy base.
38+
* Avoids turning arbitrary tokens like `not-a-url` into `http://localhost/not-a-url`.
39+
*/
40+
function mayBeRelativeRequestUrl(url) {
41+
return (url.startsWith("/") ||
42+
url.startsWith("./") ||
43+
url.startsWith("../") ||
44+
url.startsWith("?") ||
45+
url.includes("/") ||
46+
url.includes("?"));
47+
}
48+
function parseUrlForRedaction(url) {
49+
try {
50+
const u = new URL(url);
51+
return { u, relativeInput: false, queryOnlyInput: false };
52+
}
53+
catch {
54+
if (!mayBeRelativeRequestUrl(url))
55+
return null;
56+
try {
57+
const u = new URL(url, "http://localhost");
58+
return {
59+
u,
60+
relativeInput: true,
61+
queryOnlyInput: url.startsWith("?"),
62+
};
63+
}
64+
catch {
65+
return null;
66+
}
67+
}
68+
}
69+
function isSensitiveName(nameLower, sensitive, partialFragments) {
70+
if (sensitive.has(nameLower))
71+
return true;
72+
const kn = normalizeKey(nameLower);
73+
if (kn === "")
74+
return false;
75+
for (const frag of partialFragments) {
76+
if (frag !== "" && kn.includes(frag))
77+
return true;
78+
}
79+
return false;
80+
}
81+
function serializeAfterRedaction(parsed) {
82+
const { u, relativeInput, queryOnlyInput } = parsed;
83+
if (!relativeInput)
84+
return u.toString();
85+
if (queryOnlyInput)
86+
return `${u.search}${u.hash}`;
87+
return `${u.pathname}${u.search}${u.hash}`;
88+
}
2289
/**
2390
* Replaces values of sensitive query parameters for safe logging or serialization.
24-
* Invalid or non-absolute URLs are returned unchanged.
91+
* Supports absolute URLs and common relative forms (`/path?…`, `?only=query`, `api/x?…`).
92+
* Strings that are not valid URLs and do not look like path/query requests are returned unchanged.
2593
*/
2694
export function redactSensitiveUrlQuery(url, options) {
2795
if (options?.enabled === false || url === "")
2896
return url;
2997
const extra = options?.paramNames ?? [];
3098
const sensitive = extra.length === 0
3199
? DEFAULT_SET
32-
: new Set([
33-
...DEFAULT_SET,
34-
...extra.map((n) => n.toLowerCase()),
35-
]);
36-
try {
37-
const u = new URL(url);
38-
if (u.search === "")
39-
return url;
40-
const params = u.searchParams;
41-
let changed = false;
42-
const names = new Set();
43-
params.forEach((_v, name) => {
44-
names.add(name);
45-
});
46-
for (const name of names) {
47-
if (sensitive.has(name.toLowerCase())) {
48-
params.set(name, "[REDACTED]");
49-
changed = true;
50-
}
51-
}
52-
return changed ? u.toString() : url;
53-
}
54-
catch {
100+
: new Set([...DEFAULT_SET, ...extra.map((n) => n.toLowerCase())]);
101+
const partialFragments = extra.length === 0
102+
? DEFAULT_PARTIAL_FRAGMENTS
103+
: buildPartialFragments(sensitive);
104+
const replacement = options?.replacement ?? DEFAULT_REDACTION;
105+
const parsed = parseUrlForRedaction(url);
106+
if (parsed === null)
107+
return url;
108+
const { u } = parsed;
109+
if (u.search === "")
55110
return url;
111+
const params = u.searchParams;
112+
let changed = false;
113+
const names = new Set();
114+
params.forEach((_v, name) => {
115+
names.add(name);
116+
});
117+
for (const name of names) {
118+
const lower = name.toLowerCase();
119+
if (!isSensitiveName(lower, sensitive, partialFragments))
120+
continue;
121+
const n = params.getAll(name).length;
122+
params.delete(name);
123+
for (let i = 0; i < n; i++) {
124+
params.append(name, replacement);
125+
}
126+
changed = true;
56127
}
128+
return changed ? serializeAfterRedaction(parsed) : url;
57129
}

dist/plugins/debug.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export type DebugPluginOptions = {
3636
maskUrlQuery?: boolean;
3737
/** Extra query parameter names to redact in the logged URL (case-insensitive). */
3838
sensitiveQueryParamNames?: string[];
39+
/** Replacement string for redacted query values in the logged URL (default `"[REDACTED]"`). */
40+
sensitiveQueryParamReplacement?: string;
3941
};
4042
/**
4143
* Development-oriented logging middleware. Omit from production bundles if unused.

dist/plugins/debug.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)