@vltpkg/registry-client 0.0.0-0.1730239248325 → 0.0.0-10

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 (69) hide show
  1. package/README.md +57 -54
  2. package/dist/esm/add-header.d.ts +1 -1
  3. package/dist/esm/add-header.d.ts.map +1 -1
  4. package/dist/esm/add-header.js +4 -1
  5. package/dist/esm/add-header.js.map +1 -1
  6. package/dist/esm/auth.d.ts +9 -0
  7. package/dist/esm/auth.d.ts.map +1 -0
  8. package/dist/esm/auth.js +39 -0
  9. package/dist/esm/auth.js.map +1 -0
  10. package/dist/esm/cache-entry.d.ts +62 -14
  11. package/dist/esm/cache-entry.d.ts.map +1 -1
  12. package/dist/esm/cache-entry.js +159 -55
  13. package/dist/esm/cache-entry.js.map +1 -1
  14. package/dist/esm/cache-revalidate.d.ts +2 -0
  15. package/dist/esm/cache-revalidate.d.ts.map +1 -0
  16. package/dist/esm/cache-revalidate.js +66 -0
  17. package/dist/esm/cache-revalidate.js.map +1 -0
  18. package/dist/esm/delete-header.d.ts +2 -0
  19. package/dist/esm/delete-header.d.ts.map +1 -0
  20. package/dist/esm/delete-header.js +32 -0
  21. package/dist/esm/delete-header.js.map +1 -0
  22. package/dist/esm/env.d.ts +0 -4
  23. package/dist/esm/env.d.ts.map +1 -1
  24. package/dist/esm/env.js +0 -3
  25. package/dist/esm/env.js.map +1 -1
  26. package/dist/esm/get-header.js +1 -1
  27. package/dist/esm/get-header.js.map +1 -1
  28. package/dist/esm/handle-304-response.d.ts +2 -2
  29. package/dist/esm/handle-304-response.d.ts.map +1 -1
  30. package/dist/esm/handle-304-response.js.map +1 -1
  31. package/dist/esm/index.d.ts +98 -9
  32. package/dist/esm/index.d.ts.map +1 -1
  33. package/dist/esm/index.js +265 -69
  34. package/dist/esm/index.js.map +1 -1
  35. package/dist/esm/is-iterable.d.ts +2 -0
  36. package/dist/esm/is-iterable.d.ts.map +1 -0
  37. package/dist/esm/is-iterable.js +2 -0
  38. package/dist/esm/is-iterable.js.map +1 -0
  39. package/dist/esm/otplease.d.ts +4 -0
  40. package/dist/esm/otplease.d.ts.map +1 -0
  41. package/dist/esm/otplease.js +55 -0
  42. package/dist/esm/otplease.js.map +1 -0
  43. package/dist/esm/raw-header.d.ts +1 -1
  44. package/dist/esm/raw-header.d.ts.map +1 -1
  45. package/dist/esm/redirect.d.ts +2 -2
  46. package/dist/esm/redirect.d.ts.map +1 -1
  47. package/dist/esm/redirect.js +1 -0
  48. package/dist/esm/redirect.js.map +1 -1
  49. package/dist/esm/revalidate.d.ts +5 -0
  50. package/dist/esm/revalidate.d.ts.map +1 -0
  51. package/dist/esm/revalidate.js +49 -0
  52. package/dist/esm/revalidate.js.map +1 -0
  53. package/dist/esm/set-cache-headers.d.ts +2 -2
  54. package/dist/esm/set-cache-headers.d.ts.map +1 -1
  55. package/dist/esm/set-cache-headers.js +1 -1
  56. package/dist/esm/set-cache-headers.js.map +1 -1
  57. package/dist/esm/token-response.d.ts +5 -0
  58. package/dist/esm/token-response.d.ts.map +1 -0
  59. package/dist/esm/token-response.js +5 -0
  60. package/dist/esm/token-response.js.map +1 -0
  61. package/dist/esm/web-auth-challenge.d.ts +6 -0
  62. package/dist/esm/web-auth-challenge.d.ts.map +1 -0
  63. package/dist/esm/web-auth-challenge.js +7 -0
  64. package/dist/esm/web-auth-challenge.js.map +1 -0
  65. package/package.json +30 -21
  66. package/dist/esm/serdes.d.ts +0 -15
  67. package/dist/esm/serdes.d.ts.map +0 -1
  68. package/dist/esm/serdes.js +0 -19
  69. package/dist/esm/serdes.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"get-header.js","sourceRoot":"","sources":["../../src/get-header.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG,CAAI,CAAM,EAAoB,EAAE,CACjD,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;AAE7B,MAAM,CAAC,MAAM,SAAS,GAAG,CACvB,OAKa,EACb,GAAW,EACoB,EAAE;IACjC,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAA;IAC9B,GAAG,GAAG,GAAG,CAAC,WAAW,EAAE,CAAA;IACvB,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,IAAI,CAAC,OAAO,CAAC,MAAM;YAAE,OAAO,SAAS,CAAA;QACrC,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9B,yBAAyB;YACzB,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAGlB,EAAE,CAAC;gBACJ,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,GAAG;oBAAE,OAAO,CAAC,CAAA;YACvC,CAAC;QACH,CAAC;aAAM,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACpC,sBAAsB;YACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC3C,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,GAAG;oBAAE,OAAO,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;YAC9D,CAAC;QACH,CAAC;IACH,CAAC;SAAM,IACL,UAAU,CAA0C,OAAO,CAAC,EAC5D,CAAC;QACD,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC;YAC7B,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,GAAG;gBAAE,OAAO,CAAC,CAAA;QACvC,CAAC;IACH,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YAC7C,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,GAAG;gBAAE,OAAO,CAAC,CAAA;QACvC,CAAC;IACH,CAAC;AACH,CAAC,CAAA","sourcesContent":["const isIterable = <T>(o: any): o is Iterable<T> =>\n !!o && !!o[Symbol.iterator]\n\nexport const getHeader = (\n headers:\n | Iterable<[string, string[] | string | undefined]>\n | Record<string, string[] | string | undefined>\n | string[]\n | null\n | undefined,\n key: string,\n): string[] | string | undefined => {\n if (!headers) return undefined\n key = key.toLowerCase()\n if (Array.isArray(headers)) {\n if (!headers.length) return undefined\n if (Array.isArray(headers[0])) {\n // [string,HeaderValue][]\n for (const [k, v] of headers as unknown as [\n string,\n string[] | string,\n ][]) {\n if (k.toLowerCase() === key) return v\n }\n } else if (headers.length % 2 === 0) {\n // [k, v, k2, v2, ...]\n for (let i = 0; i < headers.length; i += 2) {\n if (headers[i]?.toLowerCase() === key) return headers[i + 1]\n }\n }\n } else if (\n isIterable<[string, string[] | string | undefined]>(headers)\n ) {\n for (const [k, v] of headers) {\n if (k.toLowerCase() === key) return v\n }\n } else {\n for (const [k, v] of Object.entries(headers)) {\n if (k.toLowerCase() === key) return v\n }\n }\n}\n"]}
1
+ {"version":3,"file":"get-header.js","sourceRoot":"","sources":["../../src/get-header.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG,CAAI,CAAU,EAAoB,EAAE,CACrD,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAA;AAEtD,MAAM,CAAC,MAAM,SAAS,GAAG,CACvB,OAKa,EACb,GAAW,EACoB,EAAE;IACjC,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAA;IAC9B,GAAG,GAAG,GAAG,CAAC,WAAW,EAAE,CAAA;IACvB,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,IAAI,CAAC,OAAO,CAAC,MAAM;YAAE,OAAO,SAAS,CAAA;QACrC,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9B,yBAAyB;YACzB,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAGlB,EAAE,CAAC;gBACJ,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,GAAG;oBAAE,OAAO,CAAC,CAAA;YACvC,CAAC;QACH,CAAC;aAAM,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACpC,sBAAsB;YACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC3C,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,GAAG;oBAAE,OAAO,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;YAC9D,CAAC;QACH,CAAC;IACH,CAAC;SAAM,IACL,UAAU,CAA0C,OAAO,CAAC,EAC5D,CAAC;QACD,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC;YAC7B,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,GAAG;gBAAE,OAAO,CAAC,CAAA;QACvC,CAAC;IACH,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YAC7C,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,GAAG;gBAAE,OAAO,CAAC,CAAA;QACvC,CAAC;IACH,CAAC;AACH,CAAC,CAAA","sourcesContent":["const isIterable = <T>(o: unknown): o is Iterable<T> =>\n !!o && typeof o === 'object' && Symbol.iterator in o\n\nexport const getHeader = (\n headers:\n | Iterable<[string, string[] | string | undefined]>\n | Record<string, string[] | string | undefined>\n | string[]\n | null\n | undefined,\n key: string,\n): string[] | string | undefined => {\n if (!headers) return undefined\n key = key.toLowerCase()\n if (Array.isArray(headers)) {\n if (!headers.length) return undefined\n if (Array.isArray(headers[0])) {\n // [string,HeaderValue][]\n for (const [k, v] of headers as unknown as [\n string,\n string[] | string,\n ][]) {\n if (k.toLowerCase() === key) return v\n }\n } else if (headers.length % 2 === 0) {\n // [k, v, k2, v2, ...]\n for (let i = 0; i < headers.length; i += 2) {\n if (headers[i]?.toLowerCase() === key) return headers[i + 1]\n }\n }\n } else if (\n isIterable<[string, string[] | string | undefined]>(headers)\n ) {\n for (const [k, v] of headers) {\n if (k.toLowerCase() === key) return v\n }\n } else {\n for (const [k, v] of Object.entries(headers)) {\n if (k.toLowerCase() === key) return v\n }\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { Dispatcher } from 'undici';
2
- import { CacheEntry } from './cache-entry.js';
1
+ import type { Dispatcher } from 'undici';
2
+ import type { CacheEntry } from './cache-entry.ts';
3
3
  export declare const handle304Response: (resp: Dispatcher.ResponseData, entry?: CacheEntry) => entry is CacheEntry;
4
4
  //# sourceMappingURL=handle-304-response.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handle-304-response.d.ts","sourceRoot":"","sources":["../../src/handle-304-response.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE7C,eAAO,MAAM,iBAAiB,SACtB,UAAU,CAAC,YAAY,UACrB,UAAU,KACjB,KAAK,IAAI,UASX,CAAA"}
1
+ {"version":3,"file":"handle-304-response.d.ts","sourceRoot":"","sources":["../../src/handle-304-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD,eAAO,MAAM,iBAAiB,SACtB,UAAU,CAAC,YAAY,UACrB,UAAU,KACjB,KAAK,IAAI,UASX,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"handle-304-response.js","sourceRoot":"","sources":["../../src/handle-304-response.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,IAA6B,EAC7B,KAAkB,EACG,EAAE;IACvB,IAAI,IAAI,CAAC,UAAU,KAAK,GAAG,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAA;IAEnD,MAAM,CAAC,GACL,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IAC7D,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IACvC,2CAA2C;IAC3C,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAA;IAClB,OAAO,IAAI,CAAA;AACb,CAAC,CAAA","sourcesContent":["import { Dispatcher } from 'undici'\nimport { CacheEntry } from './cache-entry.js'\n\nexport const handle304Response = (\n resp: Dispatcher.ResponseData,\n entry?: CacheEntry,\n): entry is CacheEntry => {\n if (resp.statusCode !== 304 || !entry) return false\n\n const d =\n String(resp.headers.date ?? '') || new Date().toUTCString()\n entry.setHeader('date', Buffer.from(d))\n // shouldn't have a body, but just in case.\n resp.body.resume()\n return true\n}\n"]}
1
+ {"version":3,"file":"handle-304-response.js","sourceRoot":"","sources":["../../src/handle-304-response.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,IAA6B,EAC7B,KAAkB,EACG,EAAE;IACvB,IAAI,IAAI,CAAC,UAAU,KAAK,GAAG,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAA;IAEnD,MAAM,CAAC,GACL,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IAC7D,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IACvC,2CAA2C;IAC3C,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAA;IAClB,OAAO,IAAI,CAAA;AACb,CAAC,CAAA","sourcesContent":["import type { Dispatcher } from 'undici'\nimport type { CacheEntry } from './cache-entry.ts'\n\nexport const handle304Response = (\n resp: Dispatcher.ResponseData,\n entry?: CacheEntry,\n): entry is CacheEntry => {\n if (resp.statusCode !== 304 || !entry) return false\n\n const d =\n String(resp.headers.date ?? '') || new Date().toUTCString()\n entry.setHeader('date', Buffer.from(d))\n // shouldn't have a body, but just in case.\n resp.body.resume()\n return true\n}\n"]}
@@ -1,17 +1,54 @@
1
1
  import { Cache } from '@vltpkg/cache';
2
- import { Integrity } from '@vltpkg/types';
3
- import { Agent, Dispatcher } from 'undici';
4
- import { CacheEntry } from './cache-entry.js';
5
- export { type CacheEntry };
2
+ import type { Integrity } from '@vltpkg/types';
3
+ import type { Dispatcher } from 'undici';
4
+ import { RetryAgent } from 'undici';
5
+ import type { Token } from './auth.ts';
6
+ import { deleteToken, getKC, isToken, keychains, setToken } from './auth.ts';
7
+ import type { JSONObj } from './cache-entry.ts';
8
+ import { CacheEntry } from './cache-entry.ts';
9
+ import type { TokenResponse } from './token-response.ts';
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, };
12
+ export type CacheableMethod = 'GET' | 'HEAD';
13
+ export declare const isCacheableMethod: (m: unknown) => m is CacheableMethod;
6
14
  export type RegistryClientOptions = {
7
15
  /**
8
16
  * Path on disk where the cache should be stored
9
17
  *
10
- * @default `$HOME/.config/vlt/cache`
18
+ * Defaults to the XDG cache folder for `vlt/registry-client`
11
19
  */
12
20
  cache?: string;
21
+ /**
22
+ * Number of retries to perform when encountering network errors or
23
+ * likely-transient errors from git hosts.
24
+ */
25
+ 'fetch-retries'?: number;
26
+ /** The exponential backoff factor to use when retrying git hosts */
27
+ 'fetch-retry-factor'?: number;
28
+ /** Number of milliseconds before starting first retry */
29
+ 'fetch-retry-mintimeout'?: number;
30
+ /** Maximum number of milliseconds between two retries */
31
+ 'fetch-retry-maxtimeout'?: number;
32
+ /** the identity to use for storing auth tokens */
33
+ identity?: string;
34
+ /**
35
+ * If the server does not serve a `stale-while-revalidate` value in the
36
+ * `cache-control` header, then this multiplier is applied to the `max-age`
37
+ * or `s-maxage` values.
38
+ *
39
+ * By default, this is `60`, so for example a response that is cacheable for
40
+ * 5 minutes will allow a stale response while revalidating for up to 5
41
+ * hours.
42
+ *
43
+ * If the server *does* provide a `stale-while-revalidate` value, then that
44
+ * is always used.
45
+ *
46
+ * Set to 0 to prevent any `stale-while-revalidate` behavior unless
47
+ * explicitly allowed by the server's `cache-control` header.
48
+ */
49
+ 'stale-while-revalidate-factor'?: number;
13
50
  };
14
- export type RegistryClientRequestOptions = Omit<Dispatcher.DispatchOptions, 'method' | 'path'> & {
51
+ export type RegistryClientRequestOptions = Omit<Dispatcher.RequestOptions, 'method' | 'path'> & {
15
52
  /**
16
53
  * `path` should not be set when using the RegistryClient.
17
54
  * It will be overwritten with the path on the URL being requested.
@@ -30,6 +67,12 @@ export type RegistryClientRequestOptions = Omit<Dispatcher.DispatchOptions, 'met
30
67
  * the local disk cache, items are assumed to be trustworthy.
31
68
  */
32
69
  integrity?: Integrity;
70
+ /**
71
+ * Set to true if the integrity should be trusted implicitly without
72
+ * a recalculation, for example if it comes from a trusted registry that
73
+ * also serves the tarball itself.
74
+ */
75
+ trustIntegrity?: boolean;
33
76
  /**
34
77
  * Follow up to 10 redirections by default. Set this to 0 to just return
35
78
  * the 3xx response. If the max redirections are expired, and we still get
@@ -40,16 +83,62 @@ export type RegistryClientRequestOptions = Omit<Dispatcher.DispatchOptions, 'met
40
83
  /**
41
84
  * the number of redirections that have already been seen. This is used
42
85
  * internally, and should always start at 0.
43
- *
44
86
  * @internal
45
87
  */
46
88
  redirections?: Set<string>;
89
+ /**
90
+ * Set to `false` to suppress ANY lookups from cache. This will also
91
+ * prevent storing the result to the cache.
92
+ */
93
+ useCache?: false;
94
+ /**
95
+ * Set to pass an `npm-otp` header on the request.
96
+ *
97
+ * This should not be set except by the RegistryClient itself, when
98
+ * we receive a 401 response with an OTP challenge.
99
+ * @internal
100
+ */
101
+ otp?: string;
102
+ /**
103
+ * Set to false to explicitly prevent `stale-while-revalidate` behavior,
104
+ * for use in revalidating while stale.
105
+ * @internal
106
+ */
107
+ staleWhileRevalidate?: false;
47
108
  };
48
109
  export declare const userAgent: string;
49
110
  export declare class RegistryClient {
50
- agent: Agent;
111
+ #private;
112
+ agent: RetryAgent;
51
113
  cache: Cache;
52
- constructor({ cache, }: RegistryClientOptions);
114
+ identity: string;
115
+ staleWhileRevalidateFactor: number;
116
+ constructor(options: RegistryClientOptions);
117
+ /**
118
+ * Fetch the entire set of a paginated list of objects
119
+ */
120
+ scroll<T>(url: URL | string, options?: RegistryClientRequestOptions, seek?: (obj: T) => boolean): Promise<T[]>;
121
+ /**
122
+ * find a given item in a paginated set
123
+ */
124
+ seek<T>(url: URL | string, seek: (obj: T) => boolean, options?: RegistryClientRequestOptions): Promise<T | undefined>;
125
+ /**
126
+ * Log out from the registry specified, attempting to destroy the
127
+ * token if the registry supports that endpoint.
128
+ */
129
+ logout(registry: string): Promise<void>;
130
+ /**
131
+ * Log into the registry specified
132
+ *
133
+ * Does not return the token or expose it, just saves to the auth keychain
134
+ * and returns void if it worked. Otherwise, error is raised.
135
+ */
136
+ login(registry: string): Promise<void>;
137
+ /**
138
+ * Given a {@link WebAuthChallenge}, open the `loginUrl` in a browser and
139
+ * hang on the `doneUrl` until it returns a {@link TokenResponse} object.
140
+ */
141
+ webAuthOpener({ doneUrl, loginUrl }: WebAuthChallenge): Promise<TokenResponse>;
53
142
  request(url: URL | string, options?: RegistryClientRequestOptions): Promise<CacheEntry>;
54
143
  }
55
144
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAA;AAErC,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAGzC,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAE1C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAM7C,OAAO,EAAE,KAAK,UAAU,EAAE,CAAA;AAE1B,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,4BAA4B,GAAG,IAAI,CAC7C,UAAU,CAAC,eAAe,EAC1B,QAAQ,GAAG,MAAM,CAClB,GAAG;IACF;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb;;OAEG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAA;IAC7C;;;;;OAKG;IACH,SAAS,CAAC,EAAE,SAAS,CAAA;IAErB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IAExB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAC3B,CAAA;AASD,eAAO,MAAM,SAAS,QAA8C,CAAA;AAoBpE,qBAAa,cAAc;IACzB,KAAK,EAAE,KAAK,CAAA;IACZ,KAAK,EAAE,KAAK,CAAA;gBAEA,EACV,KAAoC,GACrC,EAAE,qBAAqB;IAUlB,OAAO,CACX,GAAG,EAAE,GAAG,GAAG,MAAM,EACjB,OAAO,GAAE,4BAAiC,GACzC,OAAO,CAAC,UAAU,CAAC;CAmGvB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
package/dist/esm/index.js CHANGED
@@ -1,20 +1,34 @@
1
1
  import { Cache } from '@vltpkg/cache';
2
- import { register } from '@vltpkg/cache-unzip';
2
+ import { register as cacheUnzipRegister } from '@vltpkg/cache-unzip';
3
+ import { asError, error } from '@vltpkg/error-cause';
4
+ import { logRequest } from '@vltpkg/output';
5
+ import { urlOpen } from '@vltpkg/url-open';
3
6
  import { XDG } from '@vltpkg/xdg';
7
+ import { dirname, resolve } from 'node:path';
8
+ import { setTimeout } from 'node:timers/promises';
4
9
  import { loadPackageJson } from 'package-json-from-dist';
5
- import { Agent } from 'undici';
6
- import { addHeader } from './add-header.js';
7
- import { CacheEntry } from './cache-entry.js';
8
- import { bun, deno, node } from './env.js';
9
- import { handle304Response } from './handle-304-response.js';
10
- import { isRedirect, redirect } from './redirect.js';
11
- import { setCacheHeaders } from './set-cache-headers.js';
12
- const { version } = loadPackageJson(import.meta.filename);
10
+ import { Agent, RetryAgent } from 'undici';
11
+ import { addHeader } from "./add-header.js";
12
+ import { deleteToken, getKC, getToken, isToken, keychains, setToken, } from "./auth.js";
13
+ import { CacheEntry } from "./cache-entry.js";
14
+ import { register } from "./cache-revalidate.js";
15
+ import { bun, deno, node } from "./env.js";
16
+ import { handle304Response } from "./handle-304-response.js";
17
+ import { otplease } from "./otplease.js";
18
+ import { isRedirect, redirect } from "./redirect.js";
19
+ import { setCacheHeaders } from "./set-cache-headers.js";
20
+ import { isTokenResponse } from "./token-response.js";
21
+ import { isWebAuthChallenge } from "./web-auth-challenge.js";
22
+ export { CacheEntry, deleteToken, getKC, isToken, keychains, setToken, };
23
+ export const isCacheableMethod = (m) => m === 'GET' || m === 'HEAD';
24
+ const { version } = loadPackageJson(import.meta.filename, process.env.__VLT_INTERNAL_REGISTRY_CLIENT_PACKAGE_JSON);
25
+ /* c8 ignore start - we do test this, but coverage fails */
13
26
  const nua = globalThis.navigator?.userAgent ??
14
27
  (bun ? `Bun/${bun}`
15
28
  : deno ? `Deno/${deno}`
16
29
  : node ? `Node.js/${node}`
17
30
  : '(unknown platform)');
31
+ /* c8 ignore stop */
18
32
  export const userAgent = `@vltpkg/registry-client/${version} ${nua}`;
19
33
  const agentOptions = {
20
34
  bodyTimeout: 600_000,
@@ -35,32 +49,184 @@ const xdg = new XDG('vlt');
35
49
  export class RegistryClient {
36
50
  agent;
37
51
  cache;
38
- constructor({ cache = xdg.cache('registry-client'), }) {
52
+ identity;
53
+ staleWhileRevalidateFactor;
54
+ 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
+ this.identity = identity;
57
+ this.staleWhileRevalidateFactor = staleWhileRevalidateFactor;
58
+ const path = resolve(cache, 'registry-client');
39
59
  this.cache = new Cache({
40
- path: cache,
60
+ path,
41
61
  onDiskWrite(_path, key, data) {
42
- if (CacheEntry.decode(data).isGzip)
43
- register(cache, key);
62
+ if (CacheEntry.isGzipEntry(data)) {
63
+ cacheUnzipRegister(path, key);
64
+ }
65
+ },
66
+ });
67
+ const dispatch = new Agent(agentOptions);
68
+ this.agent = new RetryAgent(dispatch, {
69
+ maxRetries,
70
+ timeoutFactor,
71
+ minTimeout,
72
+ maxTimeout,
73
+ retryAfter: true,
74
+ errorCodes: [
75
+ 'ECONNREFUSED',
76
+ 'ECONNRESET',
77
+ 'EHOSTDOWN',
78
+ 'ENETDOWN',
79
+ 'ENETUNREACH',
80
+ 'ENOTFOUND',
81
+ 'EPIPE',
82
+ 'UND_ERR_SOCKET',
83
+ ],
84
+ });
85
+ }
86
+ /**
87
+ * Fetch the entire set of a paginated list of objects
88
+ */
89
+ async scroll(url, options = {}, seek) {
90
+ const resp = await this.request(url, options);
91
+ const { objects, urls } = resp.json();
92
+ // if we have more, and haven't found our target, fetch more
93
+ return urls.next && !(seek && objects.some(seek)) ?
94
+ objects.concat(await this.scroll(urls.next, options, seek))
95
+ : objects;
96
+ }
97
+ /**
98
+ * find a given item in a paginated set
99
+ */
100
+ async seek(url, seek, options = {}) {
101
+ return (await this.scroll(url, options, seek)).find(seek);
102
+ }
103
+ /**
104
+ * Log out from the registry specified, attempting to destroy the
105
+ * token if the registry supports that endpoint.
106
+ */
107
+ async logout(registry) {
108
+ // if we have no token for that registry, nothing to do
109
+ const tok = await getToken(registry, this.identity);
110
+ if (!tok)
111
+ return;
112
+ const s = tok.replace(/^(Bearer|Basic) /i, '');
113
+ const tokensUrl = new URL('-/npm/v1/tokens', registry);
114
+ const record = await this.seek(tokensUrl, ({ token }) => s.startsWith(token), {
115
+ useCache: false,
116
+ }).catch(() => undefined);
117
+ if (record) {
118
+ const { key } = record;
119
+ await this.request(new URL(`-/npm/v1/tokens/token/${key}`, registry), { useCache: false, method: 'DELETE' });
120
+ }
121
+ await deleteToken(registry, this.identity);
122
+ }
123
+ /**
124
+ * Log into the registry specified
125
+ *
126
+ * Does not return the token or expose it, just saves to the auth keychain
127
+ * and returns void if it worked. Otherwise, error is raised.
128
+ */
129
+ async login(registry) {
130
+ // - make POST to '/-/v1/login'
131
+ // - include a body of {} and npm-auth-type:web
132
+ // - get a {doneUrl, loginUrl}
133
+ // - open the loginUrl
134
+ // - hang on the doneUrl until done
135
+ //
136
+ // if that fails: fall back to couchdb login
137
+ const webLoginURL = new URL('-/v1/login', registry);
138
+ const response = await this.request(webLoginURL, {
139
+ method: 'POST',
140
+ useCache: false,
141
+ headers: {
142
+ 'content-type': 'application/json',
143
+ 'npm-auth-type': 'web',
44
144
  },
145
+ body: '{}',
146
+ });
147
+ if (response.statusCode === 200) {
148
+ const challenge = response.json();
149
+ if (isWebAuthChallenge(challenge)) {
150
+ const result = await this.webAuthOpener(challenge);
151
+ await setToken(registry, `Bearer ${result.token}`, this.identity);
152
+ return;
153
+ }
154
+ }
155
+ /* c8 ignore start */
156
+ // TODO: fall back to username/password login, and/or couchdb PUT login
157
+ throw error('Failed to perform web login', { response });
158
+ }
159
+ /* c8 ignore stop */
160
+ /**
161
+ * Given a {@link WebAuthChallenge}, open the `loginUrl` in a browser and
162
+ * hang on the `doneUrl` until it returns a {@link TokenResponse} object.
163
+ */
164
+ async webAuthOpener({ doneUrl, loginUrl }) {
165
+ const ac = new AbortController();
166
+ const { signal } = ac;
167
+ /* c8 ignore start - race condition */
168
+ const [result] = await Promise.all([
169
+ this.#checkLogin(doneUrl, { signal }).then(result => {
170
+ ac.abort();
171
+ return result;
172
+ }),
173
+ urlOpen(loginUrl, { signal }).catch((er) => {
174
+ if (asError(er).name === 'AbortError')
175
+ return;
176
+ ac.abort();
177
+ throw er;
178
+ }),
179
+ ]);
180
+ /* c8 ignore stop */
181
+ return result;
182
+ }
183
+ async #checkLogin(url, options = {}) {
184
+ const response = await this.request(url, {
185
+ ...options,
186
+ useCache: false,
187
+ });
188
+ const { signal } = options;
189
+ if (response.statusCode === 202) {
190
+ const rt = response.getHeader('retry-after');
191
+ const retryAfter = rt ? Number(rt.toString()) : -1;
192
+ if (retryAfter > 0) {
193
+ await setTimeout(retryAfter * 1000, null, { signal });
194
+ }
195
+ return await this.#checkLogin(url, options);
196
+ }
197
+ if (response.statusCode === 200) {
198
+ const body = response.json();
199
+ if (isTokenResponse(body))
200
+ return body;
201
+ }
202
+ throw error('Invalid response from web login endpoint', {
203
+ response,
45
204
  });
46
- this.agent = new Agent(agentOptions);
47
205
  }
48
206
  async request(url, options = {}) {
207
+ logRequest(url, 'start');
49
208
  const u = typeof url === 'string' ? new URL(url) : url;
50
- const { method = 'GET', integrity, redirections = new Set(), } = options;
209
+ const { method = 'GET', integrity, redirections = new Set(), signal, otp = (process.env.VLT_OTP ?? '').trim(), staleWhileRevalidate = true, } = options;
210
+ let { trustIntegrity } = options;
211
+ const m = isCacheableMethod(method) ? method : undefined;
212
+ const { useCache = !!m } = options;
213
+ signal?.throwIfAborted();
51
214
  // first, try to get from the cache before making any request.
52
- const { origin, pathname } = u;
53
- const key = JSON.stringify([origin, method, pathname]);
54
- const buffer = await this.cache.fetch(key, {
55
- context: { integrity },
56
- });
215
+ const { origin } = u;
216
+ const key = `${method !== 'GET' ? method + ' ' : ''}${u}`;
217
+ const buffer = useCache ?
218
+ await this.cache.fetch(key, { context: { integrity } })
219
+ : undefined;
57
220
  const entry = buffer ? CacheEntry.decode(buffer) : undefined;
58
- if (entry?.valid)
221
+ if (entry?.valid) {
222
+ return entry;
223
+ }
224
+ if (staleWhileRevalidate && entry?.staleWhileRevalidate && m) {
225
+ // revalidate while returning the stale entry
226
+ register(dirname(this.cache.path()), m, url);
59
227
  return entry;
60
- // TODO: stale-while-revalidate timeout, say 1 day, where we'll
61
- // use the cached response even if it's invalid, and validate
62
- // in the background without waiting for it.
63
- // either no cache entry, or need to revalidate it.
228
+ }
229
+ // either no cache entry, or need to revalidate before use.
64
230
  setCacheHeaders(options, entry);
65
231
  redirections.add(String(url));
66
232
  Object.assign(options, {
@@ -69,54 +235,84 @@ export class RegistryClient {
69
235
  });
70
236
  options.origin = u.origin;
71
237
  options.headers = addHeader(addHeader(options.headers, 'accept-encoding', 'gzip;q=1.0, identity;q=0.5'), 'user-agent', userAgent);
238
+ if (otp) {
239
+ options.headers = addHeader(options.headers, 'npm-otp', otp);
240
+ }
241
+ if (integrity) {
242
+ options.headers = addHeader(options.headers, 'accept-integrity', integrity);
243
+ }
72
244
  options.method = options.method ?? 'GET';
73
- const result = await new Promise((res, rej) => {
74
- /* c8 ignore start - excessive type setting for eslint */
75
- this.agent
76
- .request(options)
77
- .then(res)
78
- .catch((er) => rej(er));
79
- /* c8 ignore stop */
80
- }).then(resp => {
81
- if (handle304Response(resp, entry))
82
- return entry;
83
- const h = [];
84
- for (const [key, value] of Object.entries(resp.headers)) {
85
- /* c8 ignore start - theoretical */
86
- if (Array.isArray(value)) {
87
- h.push(Buffer.from(key), Buffer.from(value.join(', ')));
88
- /* c8 ignore stop */
89
- }
90
- else if (typeof value === 'string') {
91
- h.push(Buffer.from(key), Buffer.from(value));
92
- }
245
+ // will remove if we don't have a token.
246
+ options.headers = addHeader(options.headers, 'authorization', await getToken(origin, this.identity));
247
+ let response = null;
248
+ try {
249
+ response = await this.agent.request(options);
250
+ /* c8 ignore start */
251
+ }
252
+ catch (er) {
253
+ // Rethrow so we get a better stack trace
254
+ throw error('Request failed', {
255
+ code: 'EREQUEST',
256
+ cause: er,
257
+ url,
258
+ method,
259
+ });
260
+ }
261
+ /* c8 ignore stop */
262
+ const result = await this.#handleResponse(u, options, response, entry);
263
+ if (result.getHeader('integrity')) {
264
+ trustIntegrity = true;
265
+ }
266
+ if (result.isGzip && !trustIntegrity) {
267
+ result.checkIntegrity({ url });
268
+ }
269
+ if (useCache) {
270
+ this.cache.set(key, result.encode(), {
271
+ integrity: result.integrity,
272
+ });
273
+ }
274
+ return result;
275
+ }
276
+ async #handleResponse(url, options, response, entry) {
277
+ if (handle304Response(response, entry))
278
+ return entry;
279
+ if (response.statusCode === 401) {
280
+ const repeatRequest = await otplease(this, options, response);
281
+ if (repeatRequest)
282
+ return await this.request(url, repeatRequest);
283
+ }
284
+ const h = [];
285
+ for (const [key, value] of Object.entries(response.headers)) {
286
+ /* c8 ignore start - theoretical */
287
+ if (Array.isArray(value)) {
288
+ h.push(Buffer.from(key), Buffer.from(value.join(', ')));
289
+ /* c8 ignore stop */
93
290
  }
94
- const result = new CacheEntry(
95
- /* c8 ignore next - should always have a status code */
96
- resp.statusCode || 200, h, options.integrity);
97
- if (isRedirect(result)) {
98
- resp.body.resume();
99
- try {
100
- const [nextURL, nextOptions] = redirect(options, result, u);
101
- if (nextOptions && nextURL) {
102
- return this.request(nextURL, nextOptions);
103
- }
104
- return result;
105
- }
106
- catch (er) {
107
- /* c8 ignore start */
108
- throw er instanceof Error ? er : (new Error(typeof er === 'string' ? er : 'Unknown error'));
109
- /* c8 ignore stop */
110
- }
291
+ else if (typeof value === 'string') {
292
+ h.push(Buffer.from(key), Buffer.from(value));
111
293
  }
112
- resp.body.on('data', (chunk) => result.addBody(chunk));
113
- return new Promise((res, rej) => {
114
- resp.body.on('error', rej);
115
- resp.body.on('end', () => res(result));
116
- });
294
+ }
295
+ const { integrity, trustIntegrity } = options;
296
+ const result = new CacheEntry(
297
+ /* c8 ignore next - should always have a status code */
298
+ response.statusCode || 200, h, {
299
+ integrity,
300
+ trustIntegrity,
301
+ 'stale-while-revalidate-factor': this.staleWhileRevalidateFactor,
302
+ });
303
+ if (isRedirect(result)) {
304
+ response.body.resume();
305
+ const [nextURL, nextOptions] = redirect(options, result, url);
306
+ if (nextOptions && nextURL) {
307
+ return await this.request(nextURL, nextOptions);
308
+ }
309
+ return result;
310
+ }
311
+ response.body.on('data', (chunk) => result.addBody(chunk));
312
+ return await new Promise((res, rej) => {
313
+ response.body.on('error', rej);
314
+ response.body.on('end', () => res(result));
117
315
  });
118
- this.cache.set(key, result.encode());
119
- return result;
120
316
  }
121
317
  }
122
318
  //# sourceMappingURL=index.js.map