@vltpkg/registry-client 0.0.0-2 → 0.0.0-21
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/README.md +50 -53
- package/dist/esm/cache-entry.d.ts +55 -9
- package/dist/esm/cache-entry.d.ts.map +1 -1
- package/dist/esm/cache-entry.js +152 -37
- package/dist/esm/cache-entry.js.map +1 -1
- package/dist/esm/cache-revalidate.d.ts +2 -0
- package/dist/esm/cache-revalidate.d.ts.map +1 -0
- package/dist/esm/cache-revalidate.js +66 -0
- package/dist/esm/cache-revalidate.js.map +1 -0
- package/dist/esm/index.d.ts +40 -9
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +87 -50
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/otplease.d.ts.map +1 -1
- package/dist/esm/otplease.js +33 -25
- package/dist/esm/otplease.js.map +1 -1
- package/dist/esm/revalidate.d.ts +5 -0
- package/dist/esm/revalidate.d.ts.map +1 -0
- package/dist/esm/revalidate.js +55 -0
- package/dist/esm/revalidate.js.map +1 -0
- package/dist/esm/token-response.d.ts +1 -1
- package/dist/esm/token-response.d.ts.map +1 -1
- package/dist/esm/token-response.js +5 -2
- package/dist/esm/token-response.js.map +1 -1
- package/dist/esm/web-auth-challenge.d.ts +2 -2
- package/dist/esm/web-auth-challenge.d.ts.map +1 -1
- package/dist/esm/web-auth-challenge.js +13 -6
- package/dist/esm/web-auth-challenge.js.map +1 -1
- package/package.json +19 -20
package/README.md
CHANGED
|
@@ -2,76 +2,73 @@
|
|
|
2
2
|
|
|
3
3
|
# @vltpkg/registry-client
|
|
4
4
|
|
|
5
|
-
This is a very light wrapper around undici, optimized for interfacing
|
|
5
|
+
This is a very light wrapper around undici, optimized for interfacing
|
|
6
|
+
with an npm registry.
|
|
6
7
|
|
|
7
|
-
**[Cache Unzipped](#cache-unzipped)**
|
|
8
|
-
·
|
|
9
|
-
**[Integrity Options](#integrity-options)**
|
|
10
|
-
·
|
|
11
|
-
**[Usage](#usage)**
|
|
8
|
+
**[Cache Unzipped](#cache-unzipped)** ·
|
|
9
|
+
**[Integrity Options](#integrity-options)** · **[Usage](#usage)**
|
|
12
10
|
|
|
13
11
|
## Overview
|
|
14
12
|
|
|
15
|
-
Any response with `immutable` in the `cache-control` header, or with a
|
|
13
|
+
Any response with `immutable` in the `cache-control` header, or with a
|
|
14
|
+
`content-type` of `application/octet-stream` or a path ending in
|
|
15
|
+
`.tgz`, will be cached forever and never requested again as long as
|
|
16
|
+
the cache survives.
|
|
16
17
|
|
|
17
18
|
If the request has a cached response:
|
|
18
19
|
|
|
19
|
-
- Cached responses with `immutable` in the `cache-control`
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
a network request, no matter what, because tarballs are
|
|
25
|
-
immutable.
|
|
20
|
+
- Cached responses with `immutable` in the `cache-control` header will
|
|
21
|
+
be returned from cache without a network request, no matter what.
|
|
22
|
+
- Cached responses with a `content-type` of `application/octet-stream`
|
|
23
|
+
will be returned from cache without a network request, no matter
|
|
24
|
+
what, because tarballs are immutable.
|
|
26
25
|
- Cached responses with `max-age=<n>` or `s-max-age=<n>` will be
|
|
27
|
-
served from cache without a network request if it's less than
|
|
28
|
-
|
|
26
|
+
served from cache without a network request if it's less than `<n>`
|
|
27
|
+
seconds old.
|
|
29
28
|
- Otherwise, a network request to the registry will be made
|
|
30
|
-
- if an `etag` is present in the cached response, it will be
|
|
31
|
-
|
|
32
|
-
- If a `last-modified` header is in the response, that will
|
|
33
|
-
|
|
34
|
-
- If there is no `last-modified` header, then use the `mtime`
|
|
35
|
-
|
|
29
|
+
- if an `etag` is present in the cached response, it will be used as
|
|
30
|
+
the `if-none-match` header.
|
|
31
|
+
- If a `last-modified` header is in the response, that will be used
|
|
32
|
+
as the `if-modified-since` request header.
|
|
33
|
+
- If there is no `last-modified` header, then use the `mtime` of the
|
|
34
|
+
cache file as the `if-modified-since` header.
|
|
36
35
|
|
|
37
36
|
This is the extent of the cache control logic. It is not a
|
|
38
|
-
full-featured spec-compliant caching HTTP client, because that is
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
full-featured spec-compliant caching HTTP client, because that is not
|
|
38
|
+
needed for this use case. Every response will be cached, even if the
|
|
39
|
+
registry headers don't technically allow it.
|
|
41
40
|
|
|
42
41
|
## Cache Unzipped
|
|
43
42
|
|
|
44
|
-
Client always sends `accept-encoding: gzip;q=1.0, *;q=0.5`
|
|
45
|
-
|
|
43
|
+
Client always sends `accept-encoding: gzip;q=1.0, *;q=0.5` header when
|
|
44
|
+
making requests, to save time on the wire.
|
|
46
45
|
|
|
47
|
-
If response has `content-encoding: gzip`, then we swap out the
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
gzipped response anyway.
|
|
46
|
+
If response has `content-encoding: gzip`, then we swap out the body
|
|
47
|
+
for the unzipped response body in the cache, as if it was not gzipped
|
|
48
|
+
in the first place. This _must_ be done before returning the response,
|
|
49
|
+
because you can't `JSON.parse()` a gzipped response anyway.
|
|
52
50
|
|
|
53
|
-
If the response is `content-type: application/octet-stream` and
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
If the response is `content-type: application/octet-stream` and starts
|
|
52
|
+
with the gzip header, then we return the raw body as we received it,
|
|
53
|
+
but as a best-effort background job, unzip it and update the cache
|
|
54
|
+
entry to be an unzipped response body. This is done in the
|
|
55
|
+
`@vltpkg/cache-unzip` worker.
|
|
58
56
|
|
|
59
57
|
So,
|
|
60
58
|
|
|
61
|
-
- json responses will always be un-zipped, in the response and in
|
|
62
|
-
|
|
63
|
-
- artifact responses _may_ be gzipped (and thus, have to be
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
- json responses will always be un-zipped, in the response and in the
|
|
60
|
+
cache.
|
|
61
|
+
- artifact responses _may_ be gzipped (and thus, have to be unzipped
|
|
62
|
+
by the unpack operation), but will eventually be cached as unzipped
|
|
63
|
+
tarballs.
|
|
66
64
|
|
|
67
|
-
Thus, the `content-length` response header will _usually_ not
|
|
68
|
-
|
|
65
|
+
Thus, the `content-length` response header will _usually_ not match
|
|
66
|
+
the actual byte length of the response body.
|
|
69
67
|
|
|
70
68
|
## Integrity Options
|
|
71
69
|
|
|
72
|
-
An `integrity` option may be specified in a
|
|
73
|
-
|
|
74
|
-
`cache.set()`. For example:
|
|
70
|
+
An `integrity` option may be specified in a `fetchOptions.context`
|
|
71
|
+
object, or in the options provided to `cache.set()`. For example:
|
|
75
72
|
|
|
76
73
|
```js
|
|
77
74
|
const integrity = `sha512-${base64hash}`
|
|
@@ -86,16 +83,16 @@ cache.set(key, value, { integrity })
|
|
|
86
83
|
const value = await cache.fetch(key, { context: { integrity } })
|
|
87
84
|
```
|
|
88
85
|
|
|
89
|
-
If the integrity provided is obviously not a valid sha512
|
|
90
|
-
|
|
86
|
+
If the integrity provided is obviously not a valid sha512 `Integrity`
|
|
87
|
+
string, then it is ignored.
|
|
91
88
|
|
|
92
|
-
Integrity values are not calculated or verified. The caller must do
|
|
93
|
-
check, if desired.
|
|
89
|
+
Integrity values are not calculated or verified. The caller must do
|
|
90
|
+
this check, if desired.
|
|
94
91
|
|
|
95
92
|
Note that the integrity provided to `cache.fetch()` or `cache.set()`
|
|
96
93
|
does _not_ typically match the calculated integrity of the object
|
|
97
|
-
being cached. Typically, the integrity is related to the body of
|
|
98
|
-
|
|
94
|
+
being cached. Typically, the integrity is related to the body of the
|
|
95
|
+
response that a `@vltpkg/registry-client.CacheEntry` object
|
|
99
96
|
represents.
|
|
100
97
|
|
|
101
98
|
## Usage
|
|
@@ -1,31 +1,76 @@
|
|
|
1
|
+
import type { ErrorCauseOptions } from '@vltpkg/error-cause';
|
|
1
2
|
import type { Integrity, JSONField } from '@vltpkg/types';
|
|
2
|
-
import
|
|
3
|
+
import ccp from 'cache-control-parser';
|
|
4
|
+
import type { InspectOptions } from 'node:util';
|
|
3
5
|
export type JSONObj = Record<string, JSONField>;
|
|
4
6
|
declare const kCustomInspect: unique symbol;
|
|
7
|
+
export type CacheEntryOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* The expected integrity value for this response body
|
|
10
|
+
*/
|
|
11
|
+
integrity?: Integrity;
|
|
12
|
+
/**
|
|
13
|
+
* Whether to trust the integrity, or calculate the actual value.
|
|
14
|
+
*
|
|
15
|
+
* This indicates that we just accept whatever the integrity is as the actual
|
|
16
|
+
* integrity for saving back to the cache, because it's coming directly from
|
|
17
|
+
* the registry that we fetched a packument from, and is an initial gzipped
|
|
18
|
+
* artifact request.
|
|
19
|
+
*/
|
|
20
|
+
trustIntegrity?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* If the server does not serve a `stale-while-revalidate` value in the
|
|
23
|
+
* `cache-control` header, then this multiplier is applied to the `max-age`
|
|
24
|
+
* or `s-maxage` values.
|
|
25
|
+
*
|
|
26
|
+
* By default, this is `60`, so for example a response that is cacheable for
|
|
27
|
+
* 5 minutes will allow a stale response while revalidating for up to 5
|
|
28
|
+
* hours.
|
|
29
|
+
*
|
|
30
|
+
* If the server *does* provide a `stale-while-revalidate` value, then that
|
|
31
|
+
* is always used.
|
|
32
|
+
*
|
|
33
|
+
* Set to 0 to prevent any `stale-while-revalidate` behavior unless
|
|
34
|
+
* explicitly allowed by the server's `cache-control` header.
|
|
35
|
+
*/
|
|
36
|
+
'stale-while-revalidate-factor'?: number;
|
|
37
|
+
};
|
|
5
38
|
export declare class CacheEntry {
|
|
6
39
|
#private;
|
|
7
|
-
constructor(statusCode: number, headers: Buffer[], integrity?:
|
|
40
|
+
constructor(statusCode: number, headers: Buffer[], { integrity, trustIntegrity, 'stale-while-revalidate-factor': staleWhileRevalidateFactor, }?: CacheEntryOptions);
|
|
41
|
+
toJSON(): {
|
|
42
|
+
[k: string]: string | number | boolean | [string, string][] | Date | ccp.CacheControl | undefined;
|
|
43
|
+
};
|
|
8
44
|
[kCustomInspect](depth: number, options: InspectOptions): string;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
45
|
+
get date(): Date | undefined;
|
|
46
|
+
get maxAge(): number;
|
|
47
|
+
get cacheControl(): ccp.CacheControl;
|
|
48
|
+
get staleWhileRevalidate(): boolean;
|
|
49
|
+
get contentType(): string;
|
|
13
50
|
get valid(): boolean;
|
|
14
51
|
addBody(b: Buffer): void;
|
|
15
52
|
get statusCode(): number;
|
|
16
53
|
get headers(): Buffer<ArrayBufferLike>[];
|
|
17
54
|
/**
|
|
18
|
-
*
|
|
55
|
+
* Check that the sri integrity string that was provided to the ctor
|
|
19
56
|
* matches the body that we actually received. This should only be called
|
|
20
57
|
* AFTER the entire body has been completely downloaded.
|
|
21
58
|
*
|
|
59
|
+
* This method **will throw** if the integrity values do not match.
|
|
60
|
+
*
|
|
22
61
|
* Note that this will *usually* not be true if the value is coming out of
|
|
23
62
|
* the cache, because the cache entries are un-gzipped in place. It should
|
|
24
63
|
* _only_ be called for artifacts that come from an actual http response.
|
|
64
|
+
*
|
|
65
|
+
* Returns true if anything was actually verified.
|
|
25
66
|
*/
|
|
26
|
-
checkIntegrity():
|
|
67
|
+
checkIntegrity(context?: ErrorCauseOptions): this is CacheEntry & {
|
|
68
|
+
integrity: Integrity;
|
|
69
|
+
};
|
|
27
70
|
get integrityActual(): Integrity;
|
|
28
|
-
|
|
71
|
+
set integrityActual(i: Integrity);
|
|
72
|
+
set integrity(i: Integrity | undefined);
|
|
73
|
+
get integrity(): Integrity | undefined;
|
|
29
74
|
/**
|
|
30
75
|
* Give it a key, and it'll return the buffer of that header value
|
|
31
76
|
*/
|
|
@@ -62,6 +107,7 @@ export declare class CacheEntry {
|
|
|
62
107
|
* the cached response.
|
|
63
108
|
*/
|
|
64
109
|
static decode(buffer: Buffer): CacheEntry;
|
|
110
|
+
static isGzipEntry(buffer: Buffer): boolean;
|
|
65
111
|
/**
|
|
66
112
|
* Encode the entry as a single Buffer for writing to the cache
|
|
67
113
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-entry.d.ts","sourceRoot":"","sources":["../../src/cache-entry.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cache-entry.d.ts","sourceRoot":"","sources":["../../src/cache-entry.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE5D,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AACzD,OAAO,GAAG,MAAM,sBAAsB,CAAA;AAEtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAK/C,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;AAyB/C,QAAA,MAAM,cAAc,eAA2C,CAAA;AAE/D,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;OAEG;IACH,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IAExB;;;;;;;;;;;;;;OAcG;IACH,+BAA+B,CAAC,EAAE,MAAM,CAAA;CACzC,CAAA;AAED,qBAAa,UAAU;;gBAYnB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EAAE,EACjB,EACE,SAAS,EACT,cAAsB,EACtB,+BAA+B,EAC7B,0BAA+B,GAClC,GAAE,iBAAsB;IAmB3B,MAAM;;;IAwCN,CAAC,cAAc,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM;IAShE,IAAI,IAAI,IAAI,IAAI,GAAG,SAAS,CAK3B;IAGD,IAAI,MAAM,IAAI,MAAM,CAQnB;IAGD,IAAI,YAAY,IAAI,GAAG,CAAC,YAAY,CAKnC;IAGD,IAAI,oBAAoB,IAAI,OAAO,CAWlC;IAGD,IAAI,WAAW,WAKd;IAOD,IAAI,KAAK,IAAI,OAAO,CAmBnB;IAED,OAAO,CAAC,CAAC,EAAE,MAAM;IAKjB,IAAI,UAAU,WAEb;IACD,IAAI,OAAO,8BAEV;IAED;;;;;;;;;;;;OAYG;IACH,cAAc,CACZ,OAAO,GAAE,iBAAsB,GAC9B,IAAI,IAAI,UAAU,GAAG;QAAE,SAAS,EAAE,SAAS,CAAA;KAAE;IAchD,IAAI,eAAe,IAAI,SAAS,CAO/B;IAED,IAAI,eAAe,CAAC,CAAC,EAAE,SAAS,EAG/B;IAED,IAAI,SAAS,CAAC,CAAC,EAAE,SAAS,GAAG,SAAS,EAKrC;IACD,IAAI,SAAS,IANI,SAAS,GAAG,SAAS,CAQrC;IAED;;OAEG;IACH,SAAS,CAAC,CAAC,EAAE,MAAM;IAInB;;OAEG;IACH,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAI3C;;OAEG;IACH,MAAM,IAAI,MAAM;IAWhB,IAAI,IAAI,IAAI,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAEvC;IAGD,IAAI,MAAM,IAAI,OAAO,CAYpB;IAGD,IAAI,MAAM,IAAI,OAAO,CAcpB;IAED;;;;OAIG;IACH,KAAK;IAmBL;;;OAGG;IACH,IAAI;IAKJ;;OAEG;IACH,IAAI,IAAI,OAAO;IAQf;;;;OAIG;IACH,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU;IAsDzC,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAO3C;;OAEG;IAGH,MAAM,IAAI,MAAM;CAmCjB"}
|
package/dist/esm/cache-entry.js
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
// of the file.
|
|
21
21
|
import { error } from '@vltpkg/error-cause';
|
|
22
22
|
import ccp from 'cache-control-parser';
|
|
23
|
-
import { createHash } from 'crypto';
|
|
24
|
-
import { inspect } from 'util';
|
|
25
|
-
import { gunzipSync } from 'zlib';
|
|
23
|
+
import { createHash } from 'node:crypto';
|
|
24
|
+
import { inspect } from 'node:util';
|
|
25
|
+
import { gunzipSync } from 'node:zlib';
|
|
26
26
|
import { getRawHeader, setRawHeader } from "./raw-header.js";
|
|
27
27
|
const readSize = (buf, offset) => {
|
|
28
28
|
const a = buf[offset];
|
|
@@ -51,10 +51,15 @@ export class CacheEntry {
|
|
|
51
51
|
#integrity;
|
|
52
52
|
#integrityActual;
|
|
53
53
|
#json;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
#trustIntegrity;
|
|
55
|
+
#staleWhileRevalidateFactor;
|
|
56
|
+
constructor(statusCode, headers, { integrity, trustIntegrity = false, 'stale-while-revalidate-factor': staleWhileRevalidateFactor = 60, } = {}) {
|
|
57
57
|
this.#headers = headers;
|
|
58
|
+
this.#statusCode = statusCode;
|
|
59
|
+
this.#trustIntegrity = trustIntegrity;
|
|
60
|
+
this.#staleWhileRevalidateFactor = staleWhileRevalidateFactor;
|
|
61
|
+
if (integrity)
|
|
62
|
+
this.integrity = integrity;
|
|
58
63
|
}
|
|
59
64
|
get #headersAsObject() {
|
|
60
65
|
const ret = [];
|
|
@@ -65,42 +70,110 @@ export class CacheEntry {
|
|
|
65
70
|
}
|
|
66
71
|
return ret;
|
|
67
72
|
}
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
73
|
+
toJSON() {
|
|
74
|
+
const { statusCode, valid, staleWhileRevalidate, cacheControl, date, contentType, integrity, maxAge, isGzip, isJSON, } = this;
|
|
75
|
+
/* c8 ignore start */
|
|
76
|
+
const age = date ?
|
|
77
|
+
Math.floor((Date.now() - date.getTime()) / 1000)
|
|
78
|
+
: undefined;
|
|
79
|
+
const expires = date ? new Date(date.getTime() + this.maxAge * 1000) : undefined;
|
|
80
|
+
/* c8 ignore end */
|
|
81
|
+
return Object.fromEntries(Object.entries({
|
|
82
|
+
statusCode,
|
|
71
83
|
headers: this.#headersAsObject,
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
contentType,
|
|
85
|
+
integrity,
|
|
86
|
+
date,
|
|
87
|
+
expires,
|
|
88
|
+
cacheControl,
|
|
89
|
+
valid,
|
|
90
|
+
staleWhileRevalidate,
|
|
91
|
+
age,
|
|
92
|
+
maxAge,
|
|
93
|
+
isGzip,
|
|
94
|
+
isJSON,
|
|
95
|
+
}).filter(([_, v]) => v !== undefined));
|
|
96
|
+
}
|
|
97
|
+
[kCustomInspect](depth, options) {
|
|
98
|
+
const str = inspect(this.toJSON(), {
|
|
74
99
|
depth,
|
|
75
100
|
...options,
|
|
76
101
|
});
|
|
77
102
|
return `@vltpkg/registry-client.CacheEntry ${str}`;
|
|
78
103
|
}
|
|
104
|
+
#date;
|
|
105
|
+
get date() {
|
|
106
|
+
if (this.#date)
|
|
107
|
+
return this.#date;
|
|
108
|
+
const dh = this.getHeader('date')?.toString();
|
|
109
|
+
if (dh)
|
|
110
|
+
this.#date = new Date(dh);
|
|
111
|
+
return this.#date;
|
|
112
|
+
}
|
|
113
|
+
#maxAge;
|
|
114
|
+
get maxAge() {
|
|
115
|
+
if (this.#maxAge !== undefined)
|
|
116
|
+
return this.#maxAge;
|
|
117
|
+
// see if the max-age has not yet been crossed
|
|
118
|
+
// default to 5m if maxage is not set, as some registries
|
|
119
|
+
// do not set a cache control header at all.
|
|
120
|
+
const cc = this.cacheControl;
|
|
121
|
+
this.#maxAge = cc['max-age'] || cc['s-maxage'] || 300;
|
|
122
|
+
return this.#maxAge;
|
|
123
|
+
}
|
|
124
|
+
#cacheControl;
|
|
125
|
+
get cacheControl() {
|
|
126
|
+
if (this.#cacheControl)
|
|
127
|
+
return this.#cacheControl;
|
|
128
|
+
const cc = this.getHeader('cache-control')?.toString();
|
|
129
|
+
this.#cacheControl = cc ? ccp.parse(cc) : {};
|
|
130
|
+
return this.#cacheControl;
|
|
131
|
+
}
|
|
132
|
+
#staleWhileRevalidate;
|
|
133
|
+
get staleWhileRevalidate() {
|
|
134
|
+
if (this.#staleWhileRevalidate !== undefined)
|
|
135
|
+
return this.#staleWhileRevalidate;
|
|
136
|
+
if (this.valid || !this.date)
|
|
137
|
+
return true;
|
|
138
|
+
const swv = this.cacheControl['stale-while-revalidate'] ??
|
|
139
|
+
this.maxAge * this.#staleWhileRevalidateFactor;
|
|
140
|
+
this.#staleWhileRevalidate =
|
|
141
|
+
this.date.getTime() + swv * 1000 > Date.now();
|
|
142
|
+
return this.#staleWhileRevalidate;
|
|
143
|
+
}
|
|
144
|
+
#contentType;
|
|
145
|
+
get contentType() {
|
|
146
|
+
if (this.#contentType !== undefined)
|
|
147
|
+
return this.#contentType;
|
|
148
|
+
this.#contentType =
|
|
149
|
+
this.getHeader('content-type')?.toString() ?? '';
|
|
150
|
+
return this.#contentType;
|
|
151
|
+
}
|
|
79
152
|
/**
|
|
80
153
|
* `true` if the entry represents a cached response that is still
|
|
81
154
|
* valid to use.
|
|
82
155
|
*/
|
|
156
|
+
#valid;
|
|
83
157
|
get valid() {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const ct = this.getHeader('content-type')?.toString() ?? '';
|
|
87
|
-
const dh = this.getHeader('date')?.toString();
|
|
158
|
+
if (this.#valid !== undefined)
|
|
159
|
+
return this.#valid;
|
|
88
160
|
// immutable = never changes
|
|
89
|
-
if (
|
|
90
|
-
return true;
|
|
161
|
+
if (this.cacheControl.immutable)
|
|
162
|
+
return (this.#valid = true);
|
|
91
163
|
// some registries do text/json, some do application/json,
|
|
92
164
|
// some do application/vnd.npm.install-v1+json
|
|
93
165
|
// If it's NOT json, it's an immutable tarball
|
|
94
|
-
|
|
95
|
-
|
|
166
|
+
const ct = this.contentType;
|
|
167
|
+
if (ct && !/\bjson\b/.test(ct))
|
|
168
|
+
return (this.#valid = true);
|
|
96
169
|
// see if the max-age has not yet been crossed
|
|
97
170
|
// default to 5m if maxage is not set, as some registries
|
|
98
171
|
// do not set a cache control header at all.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return
|
|
172
|
+
if (!this.date)
|
|
173
|
+
return (this.#valid = false);
|
|
174
|
+
this.#valid =
|
|
175
|
+
this.date.getTime() + this.maxAge * 1000 > Date.now();
|
|
176
|
+
return this.#valid;
|
|
104
177
|
}
|
|
105
178
|
addBody(b) {
|
|
106
179
|
this.#body.push(b);
|
|
@@ -113,18 +186,31 @@ export class CacheEntry {
|
|
|
113
186
|
return this.#headers;
|
|
114
187
|
}
|
|
115
188
|
/**
|
|
116
|
-
*
|
|
189
|
+
* Check that the sri integrity string that was provided to the ctor
|
|
117
190
|
* matches the body that we actually received. This should only be called
|
|
118
191
|
* AFTER the entire body has been completely downloaded.
|
|
119
192
|
*
|
|
193
|
+
* This method **will throw** if the integrity values do not match.
|
|
194
|
+
*
|
|
120
195
|
* Note that this will *usually* not be true if the value is coming out of
|
|
121
196
|
* the cache, because the cache entries are un-gzipped in place. It should
|
|
122
197
|
* _only_ be called for artifacts that come from an actual http response.
|
|
198
|
+
*
|
|
199
|
+
* Returns true if anything was actually verified.
|
|
123
200
|
*/
|
|
124
|
-
checkIntegrity() {
|
|
201
|
+
checkIntegrity(context = {}) {
|
|
125
202
|
if (!this.#integrity)
|
|
126
203
|
return false;
|
|
127
|
-
|
|
204
|
+
if (this.integrityActual !== this.#integrity) {
|
|
205
|
+
throw error('Integrity check failure', {
|
|
206
|
+
code: 'EINTEGRITY',
|
|
207
|
+
response: this,
|
|
208
|
+
wanted: this.#integrity,
|
|
209
|
+
found: this.integrityActual,
|
|
210
|
+
...context,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
128
214
|
}
|
|
129
215
|
get integrityActual() {
|
|
130
216
|
if (this.#integrityActual)
|
|
@@ -132,8 +218,20 @@ export class CacheEntry {
|
|
|
132
218
|
const hash = createHash('sha512');
|
|
133
219
|
for (const buf of this.#body)
|
|
134
220
|
hash.update(buf);
|
|
135
|
-
|
|
136
|
-
|
|
221
|
+
const i = `sha512-${hash.digest('base64')}`;
|
|
222
|
+
this.integrityActual = i;
|
|
223
|
+
return i;
|
|
224
|
+
}
|
|
225
|
+
set integrityActual(i) {
|
|
226
|
+
this.#integrityActual = i;
|
|
227
|
+
this.setHeader('integrity', i);
|
|
228
|
+
}
|
|
229
|
+
set integrity(i) {
|
|
230
|
+
if (!this.#integrity && i) {
|
|
231
|
+
this.#integrity = i;
|
|
232
|
+
if (this.#trustIntegrity)
|
|
233
|
+
this.integrityActual = i;
|
|
234
|
+
}
|
|
137
235
|
}
|
|
138
236
|
get integrity() {
|
|
139
237
|
return this.#integrity;
|
|
@@ -243,7 +341,8 @@ export class CacheEntry {
|
|
|
243
341
|
json() {
|
|
244
342
|
if (this.#json !== undefined)
|
|
245
343
|
return this.#json;
|
|
246
|
-
const
|
|
344
|
+
const text = this.text();
|
|
345
|
+
const obj = JSON.parse(text || '{}');
|
|
247
346
|
this.#json = obj;
|
|
248
347
|
return obj;
|
|
249
348
|
}
|
|
@@ -254,44 +353,59 @@ export class CacheEntry {
|
|
|
254
353
|
*/
|
|
255
354
|
static decode(buffer) {
|
|
256
355
|
if (buffer.length < 4) {
|
|
257
|
-
return
|
|
356
|
+
return emptyCacheEntry;
|
|
258
357
|
}
|
|
259
358
|
const headSize = readSize(buffer, 0);
|
|
260
359
|
if (buffer.length < headSize) {
|
|
261
|
-
return
|
|
360
|
+
return emptyCacheEntry;
|
|
262
361
|
}
|
|
263
362
|
const statusCode = Number(buffer.subarray(4, 7).toString());
|
|
264
363
|
const headersBuffer = buffer.subarray(7, headSize);
|
|
265
364
|
// walk through the headers array, building up the rawHeaders Buffer[]
|
|
266
365
|
const headers = [];
|
|
267
366
|
let i = 0;
|
|
367
|
+
let integrity = undefined;
|
|
268
368
|
while (i < headersBuffer.length - 4) {
|
|
269
369
|
const size = readSize(headersBuffer, i);
|
|
270
|
-
|
|
370
|
+
const val = headersBuffer.subarray(i + 4, i + size);
|
|
371
|
+
// if the last one was the key integrity, then this one is the value
|
|
372
|
+
if (headers.length % 2 === 1 &&
|
|
373
|
+
String(headers[headers.length - 1]) === 'integrity') {
|
|
374
|
+
integrity = String(val);
|
|
375
|
+
}
|
|
376
|
+
headers.push(val);
|
|
271
377
|
i += size;
|
|
272
378
|
}
|
|
273
|
-
const c = new CacheEntry(statusCode, headers);
|
|
274
379
|
const body = buffer.subarray(headSize);
|
|
380
|
+
const c = new CacheEntry(statusCode, setRawHeader(headers, 'content-length', String(body.byteLength)), {
|
|
381
|
+
integrity,
|
|
382
|
+
trustIntegrity: true,
|
|
383
|
+
});
|
|
275
384
|
c.#body = [body];
|
|
276
385
|
c.#bodyLength = body.byteLength;
|
|
277
|
-
c.setHeader('content-length', String(c.#bodyLength));
|
|
278
386
|
if (c.isJSON) {
|
|
279
387
|
try {
|
|
280
388
|
c.json();
|
|
281
389
|
}
|
|
282
390
|
catch {
|
|
283
|
-
return
|
|
391
|
+
return emptyCacheEntry;
|
|
284
392
|
}
|
|
285
393
|
}
|
|
286
394
|
return c;
|
|
287
395
|
}
|
|
396
|
+
static isGzipEntry(buffer) {
|
|
397
|
+
if (buffer.length < 4)
|
|
398
|
+
return false;
|
|
399
|
+
const headSize = readSize(buffer, 0);
|
|
400
|
+
const gzipBytes = buffer.subarray(headSize, headSize + 2);
|
|
401
|
+
return gzipBytes[0] === 0x1f && gzipBytes[1] === 0x8b;
|
|
402
|
+
}
|
|
288
403
|
/**
|
|
289
404
|
* Encode the entry as a single Buffer for writing to the cache
|
|
290
405
|
*/
|
|
291
406
|
// TODO: should this maybe not concat, and just return Buffer[]?
|
|
292
407
|
// Then we can writev it to the cache file and save the memory copy
|
|
293
408
|
encode() {
|
|
294
|
-
// store json results as a serialized object.
|
|
295
409
|
if (this.isJSON)
|
|
296
410
|
this.json();
|
|
297
411
|
const sb = Buffer.from(String(this.#statusCode));
|
|
@@ -321,4 +435,5 @@ export class CacheEntry {
|
|
|
321
435
|
return Buffer.concat(chunks, headLength + this.#bodyLength);
|
|
322
436
|
}
|
|
323
437
|
}
|
|
438
|
+
const emptyCacheEntry = new CacheEntry(0, []);
|
|
324
439
|
//# sourceMappingURL=cache-entry.js.map
|