@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.
Files changed (88) hide show
  1. package/dist/{esm/add-header.d.ts → add-header.d.ts} +0 -1
  2. package/dist/{esm/add-header.js → add-header.js} +0 -1
  3. package/dist/auth.d.ts +44 -0
  4. package/dist/auth.js +118 -0
  5. package/dist/{esm/cache-entry.d.ts → cache-entry.d.ts} +0 -1
  6. package/dist/{esm/cache-entry.js → cache-entry.js} +0 -1
  7. package/dist/{esm/cache-revalidate.d.ts → cache-revalidate.d.ts} +0 -1
  8. package/dist/{esm/cache-revalidate.js → cache-revalidate.js} +7 -17
  9. package/dist/{esm/delete-header.d.ts → delete-header.d.ts} +0 -1
  10. package/dist/{esm/delete-header.js → delete-header.js} +0 -1
  11. package/dist/{esm/env.d.ts → env.d.ts} +0 -1
  12. package/dist/{esm/env.js → env.js} +0 -1
  13. package/dist/{esm/get-header.d.ts → get-header.d.ts} +0 -1
  14. package/dist/{esm/get-header.js → get-header.js} +0 -1
  15. package/dist/handle-304-response.d.ts +3 -0
  16. package/dist/handle-304-response.js +8 -0
  17. package/dist/{esm/index.d.ts → index.d.ts} +4 -3
  18. package/dist/{esm/index.js → index.js} +37 -17
  19. package/dist/{esm/is-cacheable.d.ts → is-cacheable.d.ts} +0 -1
  20. package/dist/{esm/is-cacheable.js → is-cacheable.js} +0 -1
  21. package/dist/{esm/is-iterable.d.ts → is-iterable.d.ts} +0 -1
  22. package/dist/{esm/is-iterable.js → is-iterable.js} +0 -1
  23. package/dist/oidc.d.ts +17 -0
  24. package/dist/oidc.js +150 -0
  25. package/dist/{esm/otplease.d.ts → otplease.d.ts} +6 -2
  26. package/dist/{esm/otplease.js → otplease.js} +19 -8
  27. package/dist/{esm/raw-header.d.ts → raw-header.d.ts} +0 -1
  28. package/dist/{esm/raw-header.js → raw-header.js} +2 -5
  29. package/dist/{esm/redirect.d.ts → redirect.d.ts} +0 -1
  30. package/dist/{esm/redirect.js → redirect.js} +0 -1
  31. package/dist/{esm/revalidate.d.ts → revalidate.d.ts} +0 -1
  32. package/dist/{esm/revalidate.js → revalidate.js} +1 -6
  33. package/dist/{esm/set-cache-headers.d.ts → set-cache-headers.d.ts} +0 -1
  34. package/dist/{esm/set-cache-headers.js → set-cache-headers.js} +0 -1
  35. package/dist/{esm/set-raw-header.d.ts → set-raw-header.d.ts} +0 -1
  36. package/dist/{esm/set-raw-header.js → set-raw-header.js} +1 -3
  37. package/dist/{esm/string-encoding.d.ts → string-encoding.d.ts} +0 -1
  38. package/dist/{esm/string-encoding.js → string-encoding.js} +0 -1
  39. package/dist/{esm/token-response.d.ts → token-response.d.ts} +0 -1
  40. package/dist/{esm/token-response.js → token-response.js} +0 -1
  41. package/dist/{esm/web-auth-challenge.d.ts → web-auth-challenge.d.ts} +0 -1
  42. package/dist/{esm/web-auth-challenge.js → web-auth-challenge.js} +0 -1
  43. package/package.json +37 -48
  44. package/dist/esm/add-header.d.ts.map +0 -1
  45. package/dist/esm/add-header.js.map +0 -1
  46. package/dist/esm/auth.d.ts +0 -9
  47. package/dist/esm/auth.d.ts.map +0 -1
  48. package/dist/esm/auth.js +0 -39
  49. package/dist/esm/auth.js.map +0 -1
  50. package/dist/esm/cache-entry.d.ts.map +0 -1
  51. package/dist/esm/cache-entry.js.map +0 -1
  52. package/dist/esm/cache-revalidate.d.ts.map +0 -1
  53. package/dist/esm/cache-revalidate.js.map +0 -1
  54. package/dist/esm/delete-header.d.ts.map +0 -1
  55. package/dist/esm/delete-header.js.map +0 -1
  56. package/dist/esm/env.d.ts.map +0 -1
  57. package/dist/esm/env.js.map +0 -1
  58. package/dist/esm/get-header.d.ts.map +0 -1
  59. package/dist/esm/get-header.js.map +0 -1
  60. package/dist/esm/handle-304-response.d.ts +0 -4
  61. package/dist/esm/handle-304-response.d.ts.map +0 -1
  62. package/dist/esm/handle-304-response.js +0 -10
  63. package/dist/esm/handle-304-response.js.map +0 -1
  64. package/dist/esm/index.d.ts.map +0 -1
  65. package/dist/esm/index.js.map +0 -1
  66. package/dist/esm/is-cacheable.d.ts.map +0 -1
  67. package/dist/esm/is-cacheable.js.map +0 -1
  68. package/dist/esm/is-iterable.d.ts.map +0 -1
  69. package/dist/esm/is-iterable.js.map +0 -1
  70. package/dist/esm/otplease.d.ts.map +0 -1
  71. package/dist/esm/otplease.js.map +0 -1
  72. package/dist/esm/package.json +0 -3
  73. package/dist/esm/raw-header.d.ts.map +0 -1
  74. package/dist/esm/raw-header.js.map +0 -1
  75. package/dist/esm/redirect.d.ts.map +0 -1
  76. package/dist/esm/redirect.js.map +0 -1
  77. package/dist/esm/revalidate.d.ts.map +0 -1
  78. package/dist/esm/revalidate.js.map +0 -1
  79. package/dist/esm/set-cache-headers.d.ts.map +0 -1
  80. package/dist/esm/set-cache-headers.js.map +0 -1
  81. package/dist/esm/set-raw-header.d.ts.map +0 -1
  82. package/dist/esm/set-raw-header.js.map +0 -1
  83. package/dist/esm/string-encoding.d.ts.map +0 -1
  84. package/dist/esm/string-encoding.js.map +0 -1
  85. package/dist/esm/token-response.d.ts.map +0 -1
  86. package/dist/esm/token-response.js.map +0 -1
  87. package/dist/esm/web-auth-challenge.d.ts.map +0 -1
  88. 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
@@ -24,4 +24,3 @@ export const addHeader = (headers, key, value) => {
24
24
  return headers;
25
25
  }
26
26
  };
27
- //# sourceMappingURL=add-header.js.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
+ };
@@ -138,4 +138,3 @@ export declare class CacheEntry {
138
138
  encode(): Buffer;
139
139
  }
140
140
  export {};
141
- //# sourceMappingURL=cache-entry.d.ts.map
@@ -515,4 +515,3 @@ export class CacheEntry {
515
515
  }
516
516
  }
517
517
  const emptyCacheEntry = new CacheEntry(0, [], { contentLength: 0 });
518
- //# sourceMappingURL=cache-entry.js.map
@@ -1,2 +1 @@
1
1
  export declare const register: (path: string, method: "HEAD" | "GET", url: string | URL) => void;
2
- //# sourceMappingURL=cache-revalidate.d.ts.map
@@ -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
- // When compiled the script to be run is passed as an
26
- // environment variable and then routed by the main entry point
27
- if (process.env.__VLT_INTERNAL_COMPILED) {
28
- env.__VLT_INTERNAL_MAIN = pathToFileURL(__CODE_SPLIT_SCRIPT_NAME).toString();
29
- args.push(path);
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
@@ -29,4 +29,3 @@ export const deleteHeader = (headers, key) => {
29
29
  return headers;
30
30
  }
31
31
  };
32
- //# sourceMappingURL=delete-header.js.map
@@ -4,4 +4,3 @@ export declare const isNode: boolean;
4
4
  export declare const bun: string | undefined;
5
5
  export declare const deno: string | undefined;
6
6
  export declare const node: string | undefined;
7
- //# sourceMappingURL=env.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
@@ -1,2 +1 @@
1
1
  export declare const getHeader: (headers: Iterable<[string, string[] | string | undefined]> | Record<string, string[] | string | undefined> | string[] | null | undefined, key: string) => string[] | string | undefined;
2
- //# sourceMappingURL=get-header.d.ts.map
@@ -34,4 +34,3 @@ export const getHeader = (headers, key) => {
34
34
  }
35
35
  }
36
36
  };
37
- //# sourceMappingURL=get-header.js.map
@@ -0,0 +1,3 @@
1
+ import type { Dispatcher } from 'undici';
2
+ import type { CacheEntry } from './cache-entry.ts';
3
+ export declare const handleCacheHitResponse: (resp: Dispatcher.ResponseData, entry?: CacheEntry) => entry is CacheEntry;
@@ -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
- export { CacheEntry, deleteToken, getKC, isToken, keychains, setToken, type JSONObj, type Token, type TokenResponse, type WebAuthChallenge, };
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 { handle304Response } from "./handle-304-response.js";
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
- export { CacheEntry, deleteToken, getKC, isToken, keychains, setToken, };
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 = 60, } = options;
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 tokensUrl = new URL('-/npm/v1/tokens', registry);
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}`, registry), { useCache: false, method: 'DELETE' });
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 getToken(origin, this.identity));
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 (handle304Response(response, entry))
283
+ if (handleCacheHitResponse(response, entry))
282
284
  return entry;
285
+ let consumedBody;
283
286
  if (response.statusCode === 401) {
284
- const repeatRequest = await otplease(this, options, response);
285
- if (repeatRequest)
286
- return await this.request(url, repeatRequest);
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: response.headers['content-length'] ?
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
@@ -2,4 +2,3 @@
2
2
  * Determine whether we're allowed to cache it, based on status code
3
3
  */
4
4
  export declare const isCacheable: (statusCode: number) => boolean;
5
- //# sourceMappingURL=is-cacheable.d.ts.map
@@ -7,4 +7,3 @@ export const isCacheable = (statusCode) => statusCode < 200 ? false
7
7
  : statusCode === 308 ? true
8
8
  : statusCode === 410 ? true
9
9
  : false;
10
- //# sourceMappingURL=is-cacheable.js.map
@@ -1,2 +1 @@
1
1
  export declare const isIterable: <T>(o: unknown) => o is Iterable<T>;
2
- //# sourceMappingURL=is-iterable.d.ts.map
@@ -1,2 +1 @@
1
1
  export const isIterable = (o) => !!o && typeof o === 'object' && Symbol.iterator in o;
2
- //# sourceMappingURL=is-iterable.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 declare const otplease: (client: RegistryClient, options: RegistryClientRequestOptions, response: Dispatcher.ResponseData) => Promise<RegistryClientRequestOptions | undefined>;
4
- //# sourceMappingURL=otplease.d.ts.map
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>;