@vltpkg/registry-client 0.0.0-2 → 0.0.0-20

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 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 with an npm registry.
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 `content-type` of `application/octet-stream` or a path ending in `.tgz`, will be cached forever and never requested again as long as the cache survives.
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
- header will be returned from cache without a network request,
21
- no matter what.
22
- - Cached responses with a `content-type` of
23
- `application/octet-stream` will be returned from cache without
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
- `<n>` seconds old.
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
- used as the `if-none-match` header.
32
- - If a `last-modified` header is in the response, that will
33
- be used as the `if-modified-since` request header.
34
- - If there is no `last-modified` header, then use the `mtime`
35
- of the cache file as the `if-modified-since` header.
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
- not needed for this use case. Every response will be cached, even
40
- if the registry headers don't technically allow it.
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
- header when making requests, to save time on the wire.
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
- body for the unzipped response body in the cache, as if it was
49
- not gzipped in the first place. This _must_ be done before
50
- returning the response, because you can't `JSON.parse()` a
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
- starts with the gzip header, then we return the raw body as we
55
- received it, but as a best-effort background job, unzip it and
56
- update the cache entry to be an unzipped response body. This is
57
- done in the `@vltpkg/cache-unzip` worker.
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
- the cache.
63
- - artifact responses _may_ be gzipped (and thus, have to be
64
- unzipped by the unpack operation), but will eventually be
65
- cached as unzipped tarballs.
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
- match the actual byte length of the response body.
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
- `fetchOptions.context` object, or in the options provided to
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
- `Integrity` string, then it is ignored.
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 this
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
- the response that a `@vltpkg/registry-client.CacheEntry` object
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 type { InspectOptions } from 'util';
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?: 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
- * `true` if the entry represents a cached response that is still
11
- * valid to use.
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
- * check that the sri integrity string that was provided to the ctor
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(): boolean;
67
+ checkIntegrity(context?: ErrorCauseOptions): this is CacheEntry & {
68
+ integrity: Integrity;
69
+ };
27
70
  get integrityActual(): Integrity;
28
- get integrity(): `sha512-${string}` | undefined;
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":"AAsBA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAIzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,MAAM,CAAA;AAI1C,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;AAyB/C,QAAA,MAAM,cAAc,eAA2C,CAAA;AAE/D,qBAAa,UAAU;;gBAUnB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EAAE,EACjB,SAAS,CAAC,EAAE,SAAS;IAiBvB,CAAC,cAAc,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM;IAehE;;;OAGG;IACH,IAAI,KAAK,IAAI,OAAO,CAsBnB;IAED,OAAO,CAAC,CAAC,EAAE,MAAM;IAKjB,IAAI,UAAU,WAEb;IACD,IAAI,OAAO,8BAEV;IAED;;;;;;;;OAQG;IACH,cAAc,IAAI,OAAO;IAIzB,IAAI,eAAe,IAAI,SAAS,CAM/B;IACD,IAAI,SAAS,mCAEZ;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;IAOf;;;;OAIG;IACH,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU;IAiCzC;;OAEG;IAGH,MAAM,IAAI,MAAM;CAoCjB"}
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"}
@@ -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
- constructor(statusCode, headers, integrity) {
55
- this.#integrity = integrity;
56
- this.#statusCode = statusCode;
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
- [kCustomInspect](depth, options) {
69
- const str = inspect({
70
- statusCode: this.statusCode,
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
- text: this.text(),
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
- const cc_ = this.getHeader('cache-control')?.toString();
85
- const cc = cc_ ? ccp.parse(cc_) : {};
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 (cc.immutable)
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
- if (ct !== '' && !/\bjson\b/.test(ct))
95
- return true;
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
- const ma = cc['max-age'] || cc['s-maxage'] || 300;
100
- if (ma && dh) {
101
- return Date.parse(dh) + ma * 1000 > Date.now();
102
- }
103
- return false;
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
- * check that the sri integrity string that was provided to the ctor
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
- return this.integrityActual === this.#integrity;
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
- this.#integrityActual = `sha512-${hash.digest('base64')}`;
136
- return this.#integrityActual;
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 obj = JSON.parse(this.text());
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 new CacheEntry(0, []);
356
+ return emptyCacheEntry;
258
357
  }
259
358
  const headSize = readSize(buffer, 0);
260
359
  if (buffer.length < headSize) {
261
- return new CacheEntry(0, []);
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
- headers.push(headersBuffer.subarray(i + 4, i + size));
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 new CacheEntry(0, []);
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