@vltpkg/registry-client 1.0.0-rc.3 → 1.0.0-rc.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{esm/add-header.d.ts → add-header.d.ts} +0 -1
- package/dist/{esm/add-header.js → add-header.js} +0 -1
- package/dist/auth.d.ts +44 -0
- package/dist/auth.js +118 -0
- package/dist/{esm/cache-entry.d.ts → cache-entry.d.ts} +0 -1
- package/dist/{esm/cache-entry.js → cache-entry.js} +0 -1
- package/dist/{esm/cache-revalidate.d.ts → cache-revalidate.d.ts} +0 -1
- package/dist/{esm/cache-revalidate.js → cache-revalidate.js} +7 -17
- package/dist/{esm/delete-header.d.ts → delete-header.d.ts} +0 -1
- package/dist/{esm/delete-header.js → delete-header.js} +0 -1
- package/dist/{esm/env.d.ts → env.d.ts} +0 -1
- package/dist/{esm/env.js → env.js} +0 -1
- package/dist/{esm/get-header.d.ts → get-header.d.ts} +0 -1
- package/dist/{esm/get-header.js → get-header.js} +0 -1
- package/dist/handle-304-response.d.ts +3 -0
- package/dist/handle-304-response.js +8 -0
- package/dist/{esm/index.d.ts → index.d.ts} +4 -3
- package/dist/{esm/index.js → index.js} +37 -17
- package/dist/{esm/is-cacheable.d.ts → is-cacheable.d.ts} +0 -1
- package/dist/{esm/is-cacheable.js → is-cacheable.js} +0 -1
- package/dist/{esm/is-iterable.d.ts → is-iterable.d.ts} +0 -1
- package/dist/{esm/is-iterable.js → is-iterable.js} +0 -1
- package/dist/oidc.d.ts +17 -0
- package/dist/oidc.js +150 -0
- package/dist/{esm/otplease.d.ts → otplease.d.ts} +6 -2
- package/dist/{esm/otplease.js → otplease.js} +19 -8
- package/dist/{esm/raw-header.d.ts → raw-header.d.ts} +0 -1
- package/dist/{esm/raw-header.js → raw-header.js} +2 -5
- package/dist/{esm/redirect.d.ts → redirect.d.ts} +0 -1
- package/dist/{esm/redirect.js → redirect.js} +0 -1
- package/dist/{esm/revalidate.d.ts → revalidate.d.ts} +0 -1
- package/dist/{esm/revalidate.js → revalidate.js} +1 -6
- package/dist/{esm/set-cache-headers.d.ts → set-cache-headers.d.ts} +0 -1
- package/dist/{esm/set-cache-headers.js → set-cache-headers.js} +0 -1
- package/dist/{esm/set-raw-header.d.ts → set-raw-header.d.ts} +0 -1
- package/dist/{esm/set-raw-header.js → set-raw-header.js} +1 -3
- package/dist/{esm/string-encoding.d.ts → string-encoding.d.ts} +0 -1
- package/dist/{esm/string-encoding.js → string-encoding.js} +0 -1
- package/dist/{esm/token-response.d.ts → token-response.d.ts} +0 -1
- package/dist/{esm/token-response.js → token-response.js} +0 -1
- package/dist/{esm/web-auth-challenge.d.ts → web-auth-challenge.d.ts} +0 -1
- package/dist/{esm/web-auth-challenge.js → web-auth-challenge.js} +0 -1
- package/package.json +37 -48
- package/dist/esm/add-header.d.ts.map +0 -1
- package/dist/esm/add-header.js.map +0 -1
- package/dist/esm/auth.d.ts +0 -9
- package/dist/esm/auth.d.ts.map +0 -1
- package/dist/esm/auth.js +0 -39
- package/dist/esm/auth.js.map +0 -1
- package/dist/esm/cache-entry.d.ts.map +0 -1
- package/dist/esm/cache-entry.js.map +0 -1
- package/dist/esm/cache-revalidate.d.ts.map +0 -1
- package/dist/esm/cache-revalidate.js.map +0 -1
- package/dist/esm/delete-header.d.ts.map +0 -1
- package/dist/esm/delete-header.js.map +0 -1
- package/dist/esm/env.d.ts.map +0 -1
- package/dist/esm/env.js.map +0 -1
- package/dist/esm/get-header.d.ts.map +0 -1
- package/dist/esm/get-header.js.map +0 -1
- package/dist/esm/handle-304-response.d.ts +0 -4
- package/dist/esm/handle-304-response.d.ts.map +0 -1
- package/dist/esm/handle-304-response.js +0 -10
- package/dist/esm/handle-304-response.js.map +0 -1
- package/dist/esm/index.d.ts.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/is-cacheable.d.ts.map +0 -1
- package/dist/esm/is-cacheable.js.map +0 -1
- package/dist/esm/is-iterable.d.ts.map +0 -1
- package/dist/esm/is-iterable.js.map +0 -1
- package/dist/esm/otplease.d.ts.map +0 -1
- package/dist/esm/otplease.js.map +0 -1
- package/dist/esm/package.json +0 -3
- package/dist/esm/raw-header.d.ts.map +0 -1
- package/dist/esm/raw-header.js.map +0 -1
- package/dist/esm/redirect.d.ts.map +0 -1
- package/dist/esm/redirect.js.map +0 -1
- package/dist/esm/revalidate.d.ts.map +0 -1
- package/dist/esm/revalidate.js.map +0 -1
- package/dist/esm/set-cache-headers.d.ts.map +0 -1
- package/dist/esm/set-cache-headers.js.map +0 -1
- package/dist/esm/set-raw-header.d.ts.map +0 -1
- package/dist/esm/set-raw-header.js.map +0 -1
- package/dist/esm/string-encoding.d.ts.map +0 -1
- package/dist/esm/string-encoding.js.map +0 -1
- package/dist/esm/token-response.d.ts.map +0 -1
- package/dist/esm/token-response.js.map +0 -1
- package/dist/esm/web-auth-challenge.d.ts.map +0 -1
- package/dist/esm/web-auth-challenge.js.map +0 -1
|
@@ -1,2 +1 @@
|
|
|
1
1
|
export declare const addHeader: <H extends [string, string[] | string][] | Iterable<[string, string[] | string | undefined]> | Record<string, string[] | string | undefined> | string[]>(headers: H | null | undefined, key: string, value?: string) => H;
|
|
2
|
-
//# sourceMappingURL=add-header.d.ts.map
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Keychain } from '@vltpkg/keychain';
|
|
2
|
+
export type Token = `Bearer ${string}` | `Basic ${string}`;
|
|
3
|
+
/**
|
|
4
|
+
* Normalize a registry URL into a stable key that preserves the
|
|
5
|
+
* path prefix. The result is `origin + pathname` with trailing
|
|
6
|
+
* slashes stripped so that
|
|
7
|
+
* `https://r.io/luke/` and `https://r.io/luke`
|
|
8
|
+
* both produce the same key.
|
|
9
|
+
*
|
|
10
|
+
* For plain-origin registries the result is identical to the old
|
|
11
|
+
* `new URL(url).origin` behaviour (e.g. `https://registry.npmjs.org`).
|
|
12
|
+
*/
|
|
13
|
+
export declare const normalizeRegistryKey: (url: string) => string;
|
|
14
|
+
/**
|
|
15
|
+
* Ensure a registry URL ends with `/` so that `new URL(path, base)`
|
|
16
|
+
* appends under the full path instead of replacing the last segment.
|
|
17
|
+
*
|
|
18
|
+
* registryBase('https://r.io/scope/name')
|
|
19
|
+
* // → 'https://r.io/scope/name/'
|
|
20
|
+
*/
|
|
21
|
+
export declare const registryBase: (url: string) => string;
|
|
22
|
+
export declare const keychains: Map<string, Keychain<Token>>;
|
|
23
|
+
/**
|
|
24
|
+
* In-memory token store for OIDC-exchanged tokens.
|
|
25
|
+
* These take precedence over env vars and keychain.
|
|
26
|
+
*/
|
|
27
|
+
export declare const runtimeTokens: Map<string, Token>;
|
|
28
|
+
export declare const setRuntimeToken: (registry: string, token: Token) => void;
|
|
29
|
+
export declare const clearRuntimeTokens: () => void;
|
|
30
|
+
export declare const getKC: (identity: string) => Keychain<Token>;
|
|
31
|
+
export declare const isToken: (t: any) => t is Token;
|
|
32
|
+
export declare const deleteToken: (registry: string, identity: string) => Promise<void>;
|
|
33
|
+
export declare const setToken: (registry: string, token: Token, identity: string) => Promise<void>;
|
|
34
|
+
export declare const getToken: (registry: string, identity: string) => Promise<Token | undefined>;
|
|
35
|
+
/**
|
|
36
|
+
* Find the best matching token for a request URL by performing a
|
|
37
|
+
* longest-prefix match against all known registry keys (runtime
|
|
38
|
+
* tokens, env-var registries, and keychain entries).
|
|
39
|
+
*
|
|
40
|
+
* This is used by `RegistryClient.request()` which only has the
|
|
41
|
+
* full request URL — not the configured registry URL that was used
|
|
42
|
+
* to construct it.
|
|
43
|
+
*/
|
|
44
|
+
export declare const getTokenByURL: (requestUrl: string, identity: string) => Promise<Token | undefined>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Keychain } from '@vltpkg/keychain';
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a registry URL into a stable key that preserves the
|
|
4
|
+
* path prefix. The result is `origin + pathname` with trailing
|
|
5
|
+
* slashes stripped so that
|
|
6
|
+
* `https://r.io/luke/` and `https://r.io/luke`
|
|
7
|
+
* both produce the same key.
|
|
8
|
+
*
|
|
9
|
+
* For plain-origin registries the result is identical to the old
|
|
10
|
+
* `new URL(url).origin` behaviour (e.g. `https://registry.npmjs.org`).
|
|
11
|
+
*/
|
|
12
|
+
export const normalizeRegistryKey = (url) => {
|
|
13
|
+
const u = new URL(url);
|
|
14
|
+
return (u.origin + u.pathname).replace(/\/+$/, '');
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Ensure a registry URL ends with `/` so that `new URL(path, base)`
|
|
18
|
+
* appends under the full path instead of replacing the last segment.
|
|
19
|
+
*
|
|
20
|
+
* registryBase('https://r.io/scope/name')
|
|
21
|
+
* // → 'https://r.io/scope/name/'
|
|
22
|
+
*/
|
|
23
|
+
export const registryBase = (url) => url.endsWith('/') ? url : url + '/';
|
|
24
|
+
// just exported for testing
|
|
25
|
+
export const keychains = new Map();
|
|
26
|
+
/**
|
|
27
|
+
* In-memory token store for OIDC-exchanged tokens.
|
|
28
|
+
* These take precedence over env vars and keychain.
|
|
29
|
+
*/
|
|
30
|
+
export const runtimeTokens = new Map();
|
|
31
|
+
export const setRuntimeToken = (registry, token) => {
|
|
32
|
+
runtimeTokens.set(normalizeRegistryKey(registry), token);
|
|
33
|
+
};
|
|
34
|
+
export const clearRuntimeTokens = () => {
|
|
35
|
+
runtimeTokens.clear();
|
|
36
|
+
};
|
|
37
|
+
export const getKC = (identity) => {
|
|
38
|
+
const kc = keychains.get(identity);
|
|
39
|
+
if (kc)
|
|
40
|
+
return kc;
|
|
41
|
+
const i = identity ? `vlt/auth/${identity}` : 'vlt/auth';
|
|
42
|
+
const nkc = new Keychain(i);
|
|
43
|
+
keychains.set(identity, nkc);
|
|
44
|
+
return nkc;
|
|
45
|
+
};
|
|
46
|
+
export const isToken = (t) => typeof t === 'string' &&
|
|
47
|
+
(t.startsWith('Bearer ') || t.startsWith('Basic '));
|
|
48
|
+
export const deleteToken = async (registry, identity) => {
|
|
49
|
+
const kc = getKC(identity);
|
|
50
|
+
await kc.load();
|
|
51
|
+
kc.delete(normalizeRegistryKey(registry));
|
|
52
|
+
await kc.save();
|
|
53
|
+
};
|
|
54
|
+
export const setToken = async (registry, token, identity) => {
|
|
55
|
+
const kc = getKC(identity);
|
|
56
|
+
await kc.load();
|
|
57
|
+
kc.set(normalizeRegistryKey(registry), token);
|
|
58
|
+
await kc.save();
|
|
59
|
+
};
|
|
60
|
+
export const getToken = async (registry, identity) => {
|
|
61
|
+
const kc = getKC(identity);
|
|
62
|
+
const key = normalizeRegistryKey(registry);
|
|
63
|
+
// Runtime tokens (e.g. from OIDC exchange) take precedence
|
|
64
|
+
const rt = runtimeTokens.get(key);
|
|
65
|
+
if (rt)
|
|
66
|
+
return rt;
|
|
67
|
+
const envReg = process.env.VLT_REGISTRY;
|
|
68
|
+
if (envReg && key === normalizeRegistryKey(envReg)) {
|
|
69
|
+
const envTok = process.env.VLT_TOKEN;
|
|
70
|
+
if (envTok)
|
|
71
|
+
return `Bearer ${envTok}`;
|
|
72
|
+
}
|
|
73
|
+
const tok = process.env[`VLT_TOKEN_${key.replace(/[^a-zA-Z0-9]+/g, '_')}`];
|
|
74
|
+
if (tok)
|
|
75
|
+
return `Bearer ${tok}`;
|
|
76
|
+
return kc.get(key);
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Find the best matching token for a request URL by performing a
|
|
80
|
+
* longest-prefix match against all known registry keys (runtime
|
|
81
|
+
* tokens, env-var registries, and keychain entries).
|
|
82
|
+
*
|
|
83
|
+
* This is used by `RegistryClient.request()` which only has the
|
|
84
|
+
* full request URL — not the configured registry URL that was used
|
|
85
|
+
* to construct it.
|
|
86
|
+
*/
|
|
87
|
+
export const getTokenByURL = async (requestUrl, identity) => {
|
|
88
|
+
const normalized = normalizeRegistryKey(requestUrl);
|
|
89
|
+
// Collect all known registry keys.
|
|
90
|
+
const candidates = [...runtimeTokens.keys()];
|
|
91
|
+
const envReg = process.env.VLT_REGISTRY;
|
|
92
|
+
if (envReg) {
|
|
93
|
+
candidates.push(normalizeRegistryKey(envReg));
|
|
94
|
+
}
|
|
95
|
+
const kc = getKC(identity);
|
|
96
|
+
// Keychain entries
|
|
97
|
+
for (const k of await kc.keys()) {
|
|
98
|
+
candidates.push(k);
|
|
99
|
+
}
|
|
100
|
+
// Find the longest candidate key that is a prefix of the
|
|
101
|
+
// normalized request URL.
|
|
102
|
+
let bestKey;
|
|
103
|
+
let bestLen = 0;
|
|
104
|
+
for (const candidate of candidates) {
|
|
105
|
+
if (candidate.length > bestLen &&
|
|
106
|
+
(normalized === candidate ||
|
|
107
|
+
normalized.startsWith(candidate + '/'))) {
|
|
108
|
+
bestKey = candidate;
|
|
109
|
+
bestLen = candidate.length;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (bestKey) {
|
|
113
|
+
return getToken(bestKey, identity);
|
|
114
|
+
}
|
|
115
|
+
// Fall back to origin-only match (handles VLT_TOKEN_* env vars
|
|
116
|
+
// which we can't enumerate by URL).
|
|
117
|
+
return getToken(requestUrl, identity);
|
|
118
|
+
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { __CODE_SPLIT_SCRIPT_NAME } from "./revalidate.js";
|
|
3
|
-
import { pathToFileURL } from 'node:url';
|
|
4
3
|
const isDeno = globalThis.Deno != undefined;
|
|
5
4
|
let didProcessBeforeExitHook = false;
|
|
6
5
|
const registered = new Map();
|
|
@@ -22,22 +21,14 @@ const handleBeforeExit = () => {
|
|
|
22
21
|
const env = { ...process.env };
|
|
23
22
|
const args = [];
|
|
24
23
|
/* c8 ignore start */
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
args.push(
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
// If we are running deno from source we need to add the
|
|
33
|
-
// unstable flags we need. The '-A' flag does not need
|
|
34
|
-
// to be passed in as Deno supplies that automatically.
|
|
35
|
-
if (isDeno) {
|
|
36
|
-
args.push('--unstable-node-globals', '--unstable-bare-node-builtins');
|
|
37
|
-
}
|
|
38
|
-
/* c8 ignore stop */
|
|
39
|
-
args.push(__CODE_SPLIT_SCRIPT_NAME, path);
|
|
24
|
+
// If we are running deno from source we need to add the
|
|
25
|
+
// unstable flags we need. The '-A' flag does not need
|
|
26
|
+
// to be passed in as Deno supplies that automatically.
|
|
27
|
+
if (isDeno) {
|
|
28
|
+
args.push('--unstable-node-globals', '--unstable-bare-node-builtins');
|
|
40
29
|
}
|
|
30
|
+
/* c8 ignore stop */
|
|
31
|
+
args.push(__CODE_SPLIT_SCRIPT_NAME, path);
|
|
41
32
|
registered.delete(path);
|
|
42
33
|
// Deno on Windows does not support detached processes
|
|
43
34
|
// https://github.com/denoland/deno/issues/25867
|
|
@@ -63,4 +54,3 @@ const handleBeforeExit = () => {
|
|
|
63
54
|
}
|
|
64
55
|
}
|
|
65
56
|
};
|
|
66
|
-
//# sourceMappingURL=cache-revalidate.js.map
|
|
@@ -1,2 +1 @@
|
|
|
1
1
|
export declare const deleteHeader: <H extends [string, string[] | string][] | Iterable<[string, string[] | string | undefined]> | Record<string, string[] | string | undefined> | string[]>(headers: H | null | undefined, key: string) => H;
|
|
2
|
-
//# sourceMappingURL=delete-header.d.ts.map
|
|
@@ -10,4 +10,3 @@ export const isNode = !isDeno && !isBun && 'node' in proc.versions;
|
|
|
10
10
|
export const bun = isBun ? proc.versions.bun : undefined;
|
|
11
11
|
export const deno = isDeno ? proc.versions.deno : undefined;
|
|
12
12
|
export const node = isNode ? proc.versions.node : undefined;
|
|
13
|
-
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const handleCacheHitResponse = (resp, entry) => {
|
|
2
|
+
if ((resp.statusCode !== 304 && resp.statusCode !== 412) || !entry)
|
|
3
|
+
return false;
|
|
4
|
+
const d = String(resp.headers.date ?? '') || new Date().toUTCString();
|
|
5
|
+
entry.setHeader('date', d);
|
|
6
|
+
resp.body.resume();
|
|
7
|
+
return true;
|
|
8
|
+
};
|
|
@@ -3,12 +3,14 @@ import type { Integrity } from '@vltpkg/types';
|
|
|
3
3
|
import type { Dispatcher } from 'undici';
|
|
4
4
|
import { RetryAgent } from 'undici';
|
|
5
5
|
import type { Token } from './auth.ts';
|
|
6
|
-
import { deleteToken, getKC, isToken, keychains, setToken } from './auth.ts';
|
|
6
|
+
import { clearRuntimeTokens, deleteToken, getKC, getToken, getTokenByURL, isToken, keychains, normalizeRegistryKey, registryBase, runtimeTokens, setRuntimeToken, setToken } from './auth.ts';
|
|
7
7
|
import type { JSONObj } from './cache-entry.ts';
|
|
8
8
|
import { CacheEntry } from './cache-entry.ts';
|
|
9
9
|
import type { TokenResponse } from './token-response.ts';
|
|
10
10
|
import type { WebAuthChallenge } from './web-auth-challenge.ts';
|
|
11
|
-
|
|
11
|
+
import { oidc } from './oidc.ts';
|
|
12
|
+
import type { OidcOptions } from './oidc.ts';
|
|
13
|
+
export { CacheEntry, clearRuntimeTokens, deleteToken, getKC, getToken, getTokenByURL, isToken, keychains, normalizeRegistryKey, oidc, registryBase, runtimeTokens, setRuntimeToken, setToken, type JSONObj, type OidcOptions, type Token, type TokenResponse, type WebAuthChallenge, };
|
|
12
14
|
export type CacheableMethod = 'GET' | 'HEAD';
|
|
13
15
|
export declare const isCacheableMethod: (m: unknown) => m is CacheableMethod;
|
|
14
16
|
export type RegistryClientOptions = {
|
|
@@ -141,4 +143,3 @@ export declare class RegistryClient {
|
|
|
141
143
|
webAuthOpener({ doneUrl, authUrl }: WebAuthChallenge): Promise<TokenResponse>;
|
|
142
144
|
request(url: URL | string, options?: RegistryClientRequestOptions): Promise<CacheEntry>;
|
|
143
145
|
}
|
|
144
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -10,18 +10,19 @@ import { setTimeout } from 'node:timers/promises';
|
|
|
10
10
|
import { loadPackageJson } from 'package-json-from-dist';
|
|
11
11
|
import { Agent, RetryAgent } from 'undici';
|
|
12
12
|
import { addHeader } from "./add-header.js";
|
|
13
|
-
import { deleteToken, getKC, getToken, isToken, keychains, setToken, } from "./auth.js";
|
|
13
|
+
import { clearRuntimeTokens, deleteToken, getKC, getToken, getTokenByURL, isToken, keychains, normalizeRegistryKey, registryBase, runtimeTokens, setRuntimeToken, setToken, } from "./auth.js";
|
|
14
14
|
import { CacheEntry } from "./cache-entry.js";
|
|
15
15
|
import { register } from "./cache-revalidate.js";
|
|
16
16
|
import { bun, deno, node } from "./env.js";
|
|
17
|
-
import {
|
|
17
|
+
import { handleCacheHitResponse } from "./handle-304-response.js";
|
|
18
18
|
import { otplease } from "./otplease.js";
|
|
19
19
|
import { isRedirect, redirect } from "./redirect.js";
|
|
20
20
|
import { setCacheHeaders } from "./set-cache-headers.js";
|
|
21
21
|
import { getTokenResponse } from "./token-response.js";
|
|
22
22
|
import { getWebAuthChallenge } from "./web-auth-challenge.js";
|
|
23
23
|
import { getEncondedValue } from "./string-encoding.js";
|
|
24
|
-
|
|
24
|
+
import { oidc } from "./oidc.js";
|
|
25
|
+
export { CacheEntry, clearRuntimeTokens, deleteToken, getKC, getToken, getTokenByURL, isToken, keychains, normalizeRegistryKey, oidc, registryBase, runtimeTokens, setRuntimeToken, setToken, };
|
|
25
26
|
export const isCacheableMethod = (m) => m === 'GET' || m === 'HEAD';
|
|
26
27
|
const { version } = loadPackageJson(import.meta.filename, process.env.__VLT_INTERNAL_REGISTRY_CLIENT_PACKAGE_JSON);
|
|
27
28
|
const nua = globalThis.navigator?.userAgent ??
|
|
@@ -52,7 +53,8 @@ export class RegistryClient {
|
|
|
52
53
|
identity;
|
|
53
54
|
staleWhileRevalidateFactor;
|
|
54
55
|
constructor(options) {
|
|
55
|
-
const { cache = xdg.cache(), 'fetch-retry-factor': timeoutFactor = 2, 'fetch-retry-mintimeout': minTimeout = 0, 'fetch-retry-maxtimeout': maxTimeout = 30_000, 'fetch-retries': maxRetries = 3, identity = '', 'stale-while-revalidate-factor': staleWhileRevalidateFactor =
|
|
56
|
+
const { cache = xdg.cache(), 'fetch-retry-factor': timeoutFactor = 2, 'fetch-retry-mintimeout': minTimeout = 0, 'fetch-retry-maxtimeout': maxTimeout = 30_000, 'fetch-retries': maxRetries = 3, identity = '', 'stale-while-revalidate-factor': staleWhileRevalidateFactor = 576, // 48h for a 5min cache
|
|
57
|
+
} = options;
|
|
56
58
|
this.identity = identity;
|
|
57
59
|
this.staleWhileRevalidateFactor = staleWhileRevalidateFactor;
|
|
58
60
|
const path = resolve(cache, 'registry-client');
|
|
@@ -110,13 +112,14 @@ export class RegistryClient {
|
|
|
110
112
|
if (!tok)
|
|
111
113
|
return;
|
|
112
114
|
const s = tok.replace(/^(Bearer|Basic) /i, '');
|
|
113
|
-
const
|
|
115
|
+
const base = registryBase(registry);
|
|
116
|
+
const tokensUrl = new URL('-/npm/v1/tokens', base);
|
|
114
117
|
const record = await this.seek(tokensUrl, ({ token }) => s.startsWith(token), {
|
|
115
118
|
useCache: false,
|
|
116
119
|
}).catch(() => undefined);
|
|
117
120
|
if (record) {
|
|
118
121
|
const { key } = record;
|
|
119
|
-
await this.request(new URL(`-/npm/v1/tokens/token/${key}`,
|
|
122
|
+
await this.request(new URL(`-/npm/v1/tokens/token/${key}`, base), { useCache: false, method: 'DELETE' });
|
|
120
123
|
}
|
|
121
124
|
await deleteToken(registry, this.identity);
|
|
122
125
|
}
|
|
@@ -134,7 +137,7 @@ export class RegistryClient {
|
|
|
134
137
|
// - hang on the doneUrl until done
|
|
135
138
|
//
|
|
136
139
|
// if that fails: fall back to couchdb login
|
|
137
|
-
const webLoginURL = new URL('-/v1/login', registry);
|
|
140
|
+
const webLoginURL = new URL('-/v1/login', registryBase(registry));
|
|
138
141
|
const response = await this.request(webLoginURL, {
|
|
139
142
|
method: 'POST',
|
|
140
143
|
useCache: false,
|
|
@@ -211,7 +214,6 @@ export class RegistryClient {
|
|
|
211
214
|
const { useCache = !!m } = options;
|
|
212
215
|
signal?.throwIfAborted();
|
|
213
216
|
// first, try to get from the cache before making any request.
|
|
214
|
-
const { origin } = u;
|
|
215
217
|
const key = `${method !== 'GET' ? method + ' ' : ''}${u}`;
|
|
216
218
|
const buffer = useCache ?
|
|
217
219
|
await this.cache.fetch(key, { context: { integrity } })
|
|
@@ -245,7 +247,7 @@ export class RegistryClient {
|
|
|
245
247
|
}
|
|
246
248
|
options.method = options.method ?? 'GET';
|
|
247
249
|
// will remove if we don't have a token.
|
|
248
|
-
options.headers = addHeader(options.headers, 'authorization', await
|
|
250
|
+
options.headers = addHeader(options.headers, 'authorization', await getTokenByURL(String(u), this.identity));
|
|
249
251
|
let response = null;
|
|
250
252
|
try {
|
|
251
253
|
response = await this.agent.request(options);
|
|
@@ -278,12 +280,17 @@ export class RegistryClient {
|
|
|
278
280
|
return result;
|
|
279
281
|
}
|
|
280
282
|
async #handleResponse(url, options, response, entry) {
|
|
281
|
-
if (
|
|
283
|
+
if (handleCacheHitResponse(response, entry))
|
|
282
284
|
return entry;
|
|
285
|
+
let consumedBody;
|
|
283
286
|
if (response.statusCode === 401) {
|
|
284
|
-
const
|
|
285
|
-
if (
|
|
286
|
-
return await this.request(url,
|
|
287
|
+
const otpResult = await otplease(this, options, response);
|
|
288
|
+
if (otpResult && 'retry' in otpResult) {
|
|
289
|
+
return await this.request(url, otpResult.retry);
|
|
290
|
+
}
|
|
291
|
+
if (otpResult && 'bodyConsumed' in otpResult) {
|
|
292
|
+
consumedBody = otpResult.bodyConsumed;
|
|
293
|
+
}
|
|
287
294
|
}
|
|
288
295
|
const h = [];
|
|
289
296
|
for (const [key, value] of Object.entries(response.headers)) {
|
|
@@ -297,15 +304,20 @@ export class RegistryClient {
|
|
|
297
304
|
}
|
|
298
305
|
}
|
|
299
306
|
const { integrity, trustIntegrity } = options;
|
|
307
|
+
// When otplease already consumed the body, use its length
|
|
308
|
+
// instead of the Content-Length header to size the CacheEntry
|
|
309
|
+
// buffer correctly.
|
|
310
|
+
const contentLength = consumedBody !== undefined ? consumedBody.length
|
|
311
|
+
: response.headers['content-length'] ?
|
|
312
|
+
Number(response.headers['content-length'])
|
|
313
|
+
: /* c8 ignore next */ undefined;
|
|
300
314
|
const result = new CacheEntry(
|
|
301
315
|
/* c8 ignore next - should always have a status code */
|
|
302
316
|
response.statusCode || 200, h, {
|
|
303
317
|
integrity,
|
|
304
318
|
trustIntegrity,
|
|
305
319
|
'stale-while-revalidate-factor': this.staleWhileRevalidateFactor,
|
|
306
|
-
contentLength
|
|
307
|
-
Number(response.headers['content-length'])
|
|
308
|
-
: /* c8 ignore next */ undefined,
|
|
320
|
+
contentLength,
|
|
309
321
|
});
|
|
310
322
|
if (isRedirect(result)) {
|
|
311
323
|
response.body.resume();
|
|
@@ -315,6 +327,15 @@ export class RegistryClient {
|
|
|
315
327
|
}
|
|
316
328
|
return result;
|
|
317
329
|
}
|
|
330
|
+
// If otplease already consumed the body (e.g. checking for OTP
|
|
331
|
+
// prompt on a plain 401), use the text it read rather than trying
|
|
332
|
+
// to re-read from the already-drained stream.
|
|
333
|
+
if (consumedBody !== undefined) {
|
|
334
|
+
if (consumedBody.length > 0) {
|
|
335
|
+
result.addBody(new TextEncoder().encode(consumedBody));
|
|
336
|
+
}
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
318
339
|
response.body.on('data', (chunk) => result.addBody(chunk));
|
|
319
340
|
return await new Promise((res, rej) => {
|
|
320
341
|
response.body.on('error', rej);
|
|
@@ -322,4 +343,3 @@ export class RegistryClient {
|
|
|
322
343
|
});
|
|
323
344
|
}
|
|
324
345
|
}
|
|
325
|
-
//# sourceMappingURL=index.js.map
|
package/dist/oidc.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Token } from './auth.ts';
|
|
2
|
+
export type OidcOptions = {
|
|
3
|
+
/** The package name being published, e.g. `@scope/name` */
|
|
4
|
+
packageName: string;
|
|
5
|
+
/** The full registry URL, e.g. `https://registry.npmjs.org/` */
|
|
6
|
+
registry: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Detect CI environment and exchange an OIDC ID token for a
|
|
10
|
+
* registry auth token. Sets the token into the runtime token
|
|
11
|
+
* store so subsequent requests are authenticated.
|
|
12
|
+
*
|
|
13
|
+
* This function **never throws**. OIDC is always optional — if
|
|
14
|
+
* it is unavailable or any step fails the function returns
|
|
15
|
+
* `undefined` silently.
|
|
16
|
+
*/
|
|
17
|
+
export declare const oidc: (opts: OidcOptions) => Promise<Token | undefined>;
|
package/dist/oidc.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { request } from 'undici';
|
|
2
|
+
import { setRuntimeToken } from "./auth.js";
|
|
3
|
+
const log = (...args) =>
|
|
4
|
+
// eslint-disable-next-line no-console
|
|
5
|
+
console.error('[vlt:oidc]', ...args);
|
|
6
|
+
/**
|
|
7
|
+
* Safely parse a JSON response body, returning undefined on failure.
|
|
8
|
+
*/
|
|
9
|
+
const safeJson = async (res) => {
|
|
10
|
+
try {
|
|
11
|
+
return (await res.body.json());
|
|
12
|
+
/* c8 ignore next 3 - defensive: non-JSON response body */
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Keys that are always replaced with '[REDACTED]' before logging.
|
|
20
|
+
*/
|
|
21
|
+
const SENSITIVE_KEYS = new Set(['token', 'value', 'secret', 'key']);
|
|
22
|
+
/**
|
|
23
|
+
* Recursively redact sensitive fields from a value before logging.
|
|
24
|
+
* Works as a JSON.stringify replacer so nested objects are handled
|
|
25
|
+
* automatically — no sensitive value is ever serialised.
|
|
26
|
+
*/
|
|
27
|
+
const redact = (obj) => JSON.parse(JSON.stringify(obj, (k, v) => SENSITIVE_KEYS.has(k) ? '[REDACTED]' : v));
|
|
28
|
+
/**
|
|
29
|
+
* Detect CI environment and exchange an OIDC ID token for a
|
|
30
|
+
* registry auth token. Sets the token into the runtime token
|
|
31
|
+
* store so subsequent requests are authenticated.
|
|
32
|
+
*
|
|
33
|
+
* This function **never throws**. OIDC is always optional — if
|
|
34
|
+
* it is unavailable or any step fails the function returns
|
|
35
|
+
* `undefined` silently.
|
|
36
|
+
*/
|
|
37
|
+
export const oidc = async (opts) => {
|
|
38
|
+
try {
|
|
39
|
+
return await _oidc(opts);
|
|
40
|
+
}
|
|
41
|
+
catch (err) /* c8 ignore start - defensive */ {
|
|
42
|
+
log('Unexpected error:', err instanceof Error ? err.message : String(err));
|
|
43
|
+
return undefined;
|
|
44
|
+
} /* c8 ignore stop */
|
|
45
|
+
};
|
|
46
|
+
const _oidc = async ({ packageName, registry, }) => {
|
|
47
|
+
const isGitHub = process.env.GITHUB_ACTIONS === 'true';
|
|
48
|
+
const isGitLab = process.env.GITLAB_CI === 'true';
|
|
49
|
+
const isCircle = process.env.CIRCLECI === 'true';
|
|
50
|
+
if (!isGitHub && !isGitLab && !isCircle) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
log(`CI detected: github=${isGitHub} gitlab=${isGitLab} circle=${isCircle}`);
|
|
54
|
+
// NPM_ID_TOKEN is supported as an override in all CI environments
|
|
55
|
+
let idToken = process.env.NPM_ID_TOKEN;
|
|
56
|
+
log(`NPM_ID_TOKEN: ${idToken ? `set (length=${idToken.length})` : 'not set'}`);
|
|
57
|
+
if (!idToken && isGitHub) {
|
|
58
|
+
idToken = await fetchGitHubIdToken(registry);
|
|
59
|
+
}
|
|
60
|
+
if (!idToken) {
|
|
61
|
+
log('No ID token available — skipping');
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const token = await exchangeToken(idToken, packageName, registry);
|
|
65
|
+
if (token) {
|
|
66
|
+
setRuntimeToken(registry, token);
|
|
67
|
+
log(`Token set for registry ${registry}`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
log('Token exchange did not return a token');
|
|
71
|
+
}
|
|
72
|
+
return token;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Fetch an OIDC ID token from GitHub Actions.
|
|
76
|
+
* Requires `id-token: write` permission in the workflow.
|
|
77
|
+
*/
|
|
78
|
+
const fetchGitHubIdToken = async (registry) => {
|
|
79
|
+
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
80
|
+
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
81
|
+
log(`ACTIONS_ID_TOKEN_REQUEST_URL: ${requestUrl ? `set (length=${requestUrl.length})` : 'not set — skipping (missing id-token permissions?)'}`);
|
|
82
|
+
log(`ACTIONS_ID_TOKEN_REQUEST_TOKEN: ${requestToken ? 'set' : 'not set'}`);
|
|
83
|
+
if (!requestUrl || !requestToken) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
const audience = `npm:${new URL(registry).hostname}`;
|
|
87
|
+
const url = new URL(requestUrl);
|
|
88
|
+
url.searchParams.append('audience', audience);
|
|
89
|
+
log(`Fetching GitHub ID token with audience ${audience}`);
|
|
90
|
+
try {
|
|
91
|
+
const res = await request(url, {
|
|
92
|
+
method: 'GET',
|
|
93
|
+
headers: {
|
|
94
|
+
accept: 'application/json',
|
|
95
|
+
authorization: `Bearer ${requestToken}`,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
log(`GitHub ID token response: status=${res.statusCode}`);
|
|
99
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
100
|
+
const body = await safeJson(res);
|
|
101
|
+
log(`GitHub ID token error body: ${body ? JSON.stringify(redact(body)) : '(non-JSON response)'}`);
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const json = await safeJson(res);
|
|
105
|
+
const value = json?.value;
|
|
106
|
+
log(`GitHub ID token: hasValue=${!!value}`);
|
|
107
|
+
return value || undefined;
|
|
108
|
+
}
|
|
109
|
+
catch (err) /* c8 ignore start - network error */ {
|
|
110
|
+
log('GitHub ID token fetch error:', err instanceof Error ? err.message : String(err));
|
|
111
|
+
return undefined;
|
|
112
|
+
} /* c8 ignore stop */
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Exchange an OIDC ID token for an npm auth token via the
|
|
116
|
+
* registry token exchange endpoint.
|
|
117
|
+
*/
|
|
118
|
+
const exchangeToken = async (idToken, packageName, registry) => {
|
|
119
|
+
// npm escaping: @scope/name → @scope%2Fname
|
|
120
|
+
const escapedName = packageName.replace('/', '%2F');
|
|
121
|
+
const exchangeUrl = new URL(`/-/npm/v1/oidc/token/exchange/package/${escapedName}`, registry);
|
|
122
|
+
log(`Exchanging token for package: ${packageName}`);
|
|
123
|
+
log(`Exchange URL: ${exchangeUrl.href}`);
|
|
124
|
+
try {
|
|
125
|
+
const res = await request(exchangeUrl, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
'content-type': 'application/json',
|
|
129
|
+
authorization: `Bearer ${idToken}`,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
log(`Exchange response: status=${res.statusCode}`);
|
|
133
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
134
|
+
const body = await safeJson(res);
|
|
135
|
+
log(`Exchange error body: ${body ? JSON.stringify(redact(body)) : '(non-JSON response)'}`);
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
const json = await safeJson(res);
|
|
139
|
+
const hasToken = !!json?.token;
|
|
140
|
+
log(`Exchange result: hasToken=${hasToken}`);
|
|
141
|
+
if (!json?.token || typeof json.token !== 'string') {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
return `Bearer ${json.token}`;
|
|
145
|
+
}
|
|
146
|
+
catch (err) /* c8 ignore start - network error */ {
|
|
147
|
+
log('Exchange request error:', err instanceof Error ? err.message : String(err));
|
|
148
|
+
return undefined;
|
|
149
|
+
} /* c8 ignore stop */
|
|
150
|
+
};
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { Dispatcher } from 'undici';
|
|
2
2
|
import type { RegistryClient, RegistryClientRequestOptions } from './index.ts';
|
|
3
|
-
export
|
|
4
|
-
|
|
3
|
+
export type OtpResult = {
|
|
4
|
+
retry: RegistryClientRequestOptions;
|
|
5
|
+
} | {
|
|
6
|
+
bodyConsumed: string;
|
|
7
|
+
} | undefined;
|
|
8
|
+
export declare const otplease: (client: RegistryClient, options: RegistryClientRequestOptions, response: Dispatcher.ResponseData) => Promise<OtpResult>;
|