@vltpkg/registry-client 1.0.0-rc.29 → 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/auth.d.ts CHANGED
@@ -11,6 +11,14 @@ export type Token = `Bearer ${string}` | `Basic ${string}`;
11
11
  * `new URL(url).origin` behaviour (e.g. `https://registry.npmjs.org`).
12
12
  */
13
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;
14
22
  export declare const keychains: Map<string, Keychain<Token>>;
15
23
  /**
16
24
  * In-memory token store for OIDC-exchanged tokens.
package/dist/auth.js CHANGED
@@ -13,6 +13,14 @@ export const normalizeRegistryKey = (url) => {
13
13
  const u = new URL(url);
14
14
  return (u.origin + u.pathname).replace(/\/+$/, '');
15
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 + '/';
16
24
  // just exported for testing
17
25
  export const keychains = new Map();
18
26
  /**
@@ -45,7 +53,9 @@ export const deleteToken = async (registry, identity) => {
45
53
  };
46
54
  export const setToken = async (registry, token, identity) => {
47
55
  const kc = getKC(identity);
48
- return kc.set(normalizeRegistryKey(registry), token);
56
+ await kc.load();
57
+ kc.set(normalizeRegistryKey(registry), token);
58
+ await kc.save();
49
59
  };
50
60
  export const getToken = async (registry, identity) => {
51
61
  const kc = getKC(identity);
package/dist/index.d.ts CHANGED
@@ -3,14 +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 { clearRuntimeTokens, deleteToken, getKC, getTokenByURL, isToken, keychains, normalizeRegistryKey, runtimeTokens, setRuntimeToken, 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
12
  import type { OidcOptions } from './oidc.ts';
13
- export { CacheEntry, clearRuntimeTokens, deleteToken, getKC, getTokenByURL, isToken, keychains, normalizeRegistryKey, oidc, runtimeTokens, setRuntimeToken, setToken, type JSONObj, type OidcOptions, type Token, type TokenResponse, type WebAuthChallenge, };
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, };
14
14
  export type CacheableMethod = 'GET' | 'HEAD';
15
15
  export declare const isCacheableMethod: (m: unknown) => m is CacheableMethod;
16
16
  export type RegistryClientOptions = {
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ 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 { clearRuntimeTokens, deleteToken, getKC, getToken, getTokenByURL, isToken, keychains, normalizeRegistryKey, runtimeTokens, setRuntimeToken, 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";
@@ -22,7 +22,7 @@ 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, getTokenByURL, isToken, keychains, normalizeRegistryKey, oidc, runtimeTokens, setRuntimeToken, setToken, };
25
+ export { CacheEntry, clearRuntimeTokens, deleteToken, getKC, getToken, getTokenByURL, isToken, keychains, normalizeRegistryKey, oidc, registryBase, runtimeTokens, setRuntimeToken, setToken, };
26
26
  export const isCacheableMethod = (m) => m === 'GET' || m === 'HEAD';
27
27
  const { version } = loadPackageJson(import.meta.filename, process.env.__VLT_INTERNAL_REGISTRY_CLIENT_PACKAGE_JSON);
28
28
  const nua = globalThis.navigator?.userAgent ??
@@ -112,13 +112,14 @@ export class RegistryClient {
112
112
  if (!tok)
113
113
  return;
114
114
  const s = tok.replace(/^(Bearer|Basic) /i, '');
115
- const tokensUrl = new URL('-/npm/v1/tokens', registry);
115
+ const base = registryBase(registry);
116
+ const tokensUrl = new URL('-/npm/v1/tokens', base);
116
117
  const record = await this.seek(tokensUrl, ({ token }) => s.startsWith(token), {
117
118
  useCache: false,
118
119
  }).catch(() => undefined);
119
120
  if (record) {
120
121
  const { key } = record;
121
- 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' });
122
123
  }
123
124
  await deleteToken(registry, this.identity);
124
125
  }
@@ -136,7 +137,7 @@ export class RegistryClient {
136
137
  // - hang on the doneUrl until done
137
138
  //
138
139
  // if that fails: fall back to couchdb login
139
- const webLoginURL = new URL('-/v1/login', registry);
140
+ const webLoginURL = new URL('-/v1/login', registryBase(registry));
140
141
  const response = await this.request(webLoginURL, {
141
142
  method: 'POST',
142
143
  useCache: false,
@@ -281,10 +282,15 @@ export class RegistryClient {
281
282
  async #handleResponse(url, options, response, entry) {
282
283
  if (handleCacheHitResponse(response, entry))
283
284
  return entry;
285
+ let consumedBody;
284
286
  if (response.statusCode === 401) {
285
- const repeatRequest = await otplease(this, options, response);
286
- if (repeatRequest)
287
- 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
+ }
288
294
  }
289
295
  const h = [];
290
296
  for (const [key, value] of Object.entries(response.headers)) {
@@ -298,15 +304,20 @@ export class RegistryClient {
298
304
  }
299
305
  }
300
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;
301
314
  const result = new CacheEntry(
302
315
  /* c8 ignore next - should always have a status code */
303
316
  response.statusCode || 200, h, {
304
317
  integrity,
305
318
  trustIntegrity,
306
319
  'stale-while-revalidate-factor': this.staleWhileRevalidateFactor,
307
- contentLength: response.headers['content-length'] ?
308
- Number(response.headers['content-length'])
309
- : /* c8 ignore next */ undefined,
320
+ contentLength,
310
321
  });
311
322
  if (isRedirect(result)) {
312
323
  response.body.resume();
@@ -316,6 +327,15 @@ export class RegistryClient {
316
327
  }
317
328
  return result;
318
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
+ }
319
339
  response.body.on('data', (chunk) => result.addBody(chunk));
320
340
  return await new Promise((res, rej) => {
321
341
  response.body.on('error', rej);
package/dist/oidc.js CHANGED
@@ -47,11 +47,10 @@ const _oidc = async ({ packageName, registry, }) => {
47
47
  const isGitHub = process.env.GITHUB_ACTIONS === 'true';
48
48
  const isGitLab = process.env.GITLAB_CI === 'true';
49
49
  const isCircle = process.env.CIRCLECI === 'true';
50
- log(`CI detected: github=${isGitHub} gitlab=${isGitLab} circle=${isCircle}`);
51
50
  if (!isGitHub && !isGitLab && !isCircle) {
52
- log('Not in a supported CI environment — skipping');
53
51
  return undefined;
54
52
  }
53
+ log(`CI detected: github=${isGitHub} gitlab=${isGitLab} circle=${isCircle}`);
55
54
  // NPM_ID_TOKEN is supported as an override in all CI environments
56
55
  let idToken = process.env.NPM_ID_TOKEN;
57
56
  log(`NPM_ID_TOKEN: ${idToken ? `set (length=${idToken.length})` : 'not set'}`);
@@ -1,3 +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>;
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>;
package/dist/otplease.js CHANGED
@@ -27,8 +27,10 @@ export const otplease = async (client, options, response) => {
27
27
  const challenge = getWebAuthChallenge(await response.body.json().catch(() => null));
28
28
  if (challenge) {
29
29
  return {
30
- ...options,
31
- otp: (await client.webAuthOpener(challenge)).token,
30
+ retry: {
31
+ ...options,
32
+ otp: (await client.webAuthOpener(challenge)).token,
33
+ },
32
34
  };
33
35
  }
34
36
  const { 'npm-notice': npmNotice } = response.headers;
@@ -39,8 +41,10 @@ export const otplease = async (client, options, response) => {
39
41
  await urlOpen(match[1]);
40
42
  log(notice);
41
43
  return {
42
- ...options,
43
- otp: await question('OTP: '),
44
+ retry: {
45
+ ...options,
46
+ otp: await question('OTP: '),
47
+ },
44
48
  };
45
49
  }
46
50
  }
@@ -48,15 +52,23 @@ export const otplease = async (client, options, response) => {
48
52
  response,
49
53
  });
50
54
  }
55
+ if (wwwAuth.has('bearer')) {
56
+ throw error('Missing or invalid authentication token. Run `vlt login` or `vlt token add` to authenticate.', { response });
57
+ }
51
58
  if (wwwAuth.size) {
52
59
  throw error('Unknown authentication challenge', { response });
53
60
  }
54
- // see if the body is prompting for otp
61
+ // Consume the body to check if it's prompting for OTP.
62
+ // We must return the consumed text so the caller doesn't try to
63
+ // re-read from the already-drained stream.
55
64
  const text = await response.body.text().catch(() => '');
56
65
  if (text.toLowerCase().includes('one-time pass')) {
57
66
  return {
58
- ...options,
59
- otp: await question(text),
67
+ retry: {
68
+ ...options,
69
+ otp: await question(text),
70
+ },
60
71
  };
61
72
  }
73
+ return { bodyConsumed: text };
62
74
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/registry-client",
3
3
  "description": "Fetch package artifacts and metadata from registries",
4
- "version": "1.0.0-rc.29",
4
+ "version": "1.0.0-rc.30",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -13,14 +13,14 @@
13
13
  "url": "http://vlt.sh"
14
14
  },
15
15
  "dependencies": {
16
- "@vltpkg/cache": "1.0.0-rc.29",
17
- "@vltpkg/cache-unzip": "1.0.0-rc.29",
18
- "@vltpkg/error-cause": "1.0.0-rc.29",
19
- "@vltpkg/keychain": "1.0.0-rc.29",
20
- "@vltpkg/output": "1.0.0-rc.29",
21
- "@vltpkg/types": "1.0.0-rc.29",
22
- "@vltpkg/url-open": "1.0.0-rc.29",
23
- "@vltpkg/xdg": "1.0.0-rc.29",
16
+ "@vltpkg/cache": "1.0.0-rc.30",
17
+ "@vltpkg/cache-unzip": "1.0.0-rc.30",
18
+ "@vltpkg/error-cause": "1.0.0-rc.30",
19
+ "@vltpkg/keychain": "1.0.0-rc.30",
20
+ "@vltpkg/output": "1.0.0-rc.30",
21
+ "@vltpkg/types": "1.0.0-rc.30",
22
+ "@vltpkg/url-open": "1.0.0-rc.30",
23
+ "@vltpkg/xdg": "1.0.0-rc.30",
24
24
  "cache-control-parser": "^2.0.6",
25
25
  "package-json-from-dist": "^1.0.1",
26
26
  "undici": "^7.16.0"