Socket
Socket
Sign inDemoInstall

http-cache-semantics

Package Overview
Dependencies
0
Maintainers
1
Versions
26
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 4.0.2 to 4.0.3

282

index.js
'use strict';
// rfc7231 6.1
const statusCodeCacheableByDefault = [200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501];
const statusCodeCacheableByDefault = [
200,
203,
204,
206,
300,
301,
404,
405,
410,
414,
501,
];
// This implementation does not understand partial responses (206)
const understoodStatuses = [200, 203, 204, 300, 301, 302, 303, 307, 308, 404, 405, 410, 414, 501];
const understoodStatuses = [
200,
203,
204,
300,
301,
302,
303,
307,
308,
404,
405,
410,
414,
501,
];
const hopByHopHeaders = {
'date': true, // included, because we add Age update Date
'connection':true, 'keep-alive':true, 'proxy-authenticate':true, 'proxy-authorization':true, 'te':true, 'trailer':true, 'transfer-encoding':true, 'upgrade':true
date: true, // included, because we add Age update Date
connection: true,
'keep-alive': true,
'proxy-authenticate': true,
'proxy-authorization': true,
te: true,
trailer: true,
'transfer-encoding': true,
upgrade: true,
};
const excludedFromRevalidationUpdate = {
// Since the old body is reused, it doesn't make sense to change properties of the body
'content-length': true, 'content-encoding': true, 'transfer-encoding': true,
'content-length': true,
'content-encoding': true,
'transfer-encoding': true,
'content-range': true,

@@ -25,5 +61,5 @@ };

const parts = header.trim().split(/\s*,\s*/); // TODO: lame parsing
for(const part of parts) {
const [k,v] = part.split(/\s*=\s*/, 2);
cc[k] = (v === undefined) ? true : v.replace(/^"|"$/g, ''); // TODO: lame unquoting
for (const part of parts) {
const [k, v] = part.split(/\s*=\s*/, 2);
cc[k] = v === undefined ? true : v.replace(/^"|"$/g, ''); // TODO: lame unquoting
}

@@ -36,3 +72,3 @@

let parts = [];
for(const k in cc) {
for (const k in cc) {
const v = cc[k];

@@ -48,3 +84,14 @@ parts.push(v === true ? k : k + '=' + v);

module.exports = class CachePolicy {
constructor(req, res, {shared, cacheHeuristic, immutableMinTimeToLive, ignoreCargoCult, trustServerDate, _fromObject} = {}) {
constructor(
req,
res,
{
shared,
cacheHeuristic,
immutableMinTimeToLive,
ignoreCargoCult,
trustServerDate,
_fromObject,
} = {}
) {
if (_fromObject) {

@@ -56,3 +103,3 @@ this._fromObject(_fromObject);

if (!res || !res.headers) {
throw Error("Response headers missing");
throw Error('Response headers missing');
}

@@ -63,5 +110,10 @@ this._assertRequestHasHeaders(req);

this._isShared = shared !== false;
this._trustServerDate = undefined !== trustServerDate ? trustServerDate : true;
this._cacheHeuristic = undefined !== cacheHeuristic ? cacheHeuristic : 0.1; // 10% matches IE
this._immutableMinTtl = undefined !== immutableMinTimeToLive ? immutableMinTimeToLive : 24*3600*1000;
this._trustServerDate =
undefined !== trustServerDate ? trustServerDate : true;
this._cacheHeuristic =
undefined !== cacheHeuristic ? cacheHeuristic : 0.1; // 10% matches IE
this._immutableMinTtl =
undefined !== immutableMinTimeToLive
? immutableMinTimeToLive
: 24 * 3600 * 1000;

@@ -80,3 +132,7 @@ this._status = 'status' in res ? res.status : 200;

// so there's no point stricly adhering to the blindly copy&pasted directives.
if (ignoreCargoCult && "pre-check" in this._rescc && "post-check" in this._rescc) {
if (
ignoreCargoCult &&
'pre-check' in this._rescc &&
'post-check' in this._rescc
) {
delete this._rescc['pre-check'];

@@ -87,3 +143,5 @@ delete this._rescc['post-check'];

delete this._rescc['must-revalidate'];
this._resHeaders = Object.assign({}, this._resHeaders, {'cache-control': formatCacheControl(this._rescc)});
this._resHeaders = Object.assign({}, this._resHeaders, {
'cache-control': formatCacheControl(this._rescc),
});
delete this._resHeaders.expires;

@@ -95,3 +153,6 @@ delete this._resHeaders.pragma;

// as having the same effect as if "Cache-Control: no-cache" were present (see Section 5.2.1).
if (!res.headers['cache-control'] && /no-cache/.test(res.headers.pragma)) {
if (
res.headers['cache-control'] == null &&
/no-cache/.test(res.headers.pragma)
) {
this._rescc['no-cache'] = true;

@@ -107,6 +168,9 @@ }

// The "no-store" request directive indicates that a cache MUST NOT store any part of either this request or any response to it.
return !!(!this._reqcc['no-store'] &&
return !!(
!this._reqcc['no-store'] &&
// A cache MUST NOT store a response to any request, unless:
// The request method is understood by the cache and defined as being cacheable, and
('GET' === this._method || 'HEAD' === this._method || ('POST' === this._method && this._hasExplicitExpiration())) &&
('GET' === this._method ||
'HEAD' === this._method ||
('POST' === this._method && this._hasExplicitExpiration())) &&
// the response status code is understood by the cache, and

@@ -119,14 +183,17 @@ understoodStatuses.indexOf(this._status) !== -1 &&

// the Authorization header field does not appear in the request, if the cache is shared,
(!this._isShared || this._noAuthorization || this._allowsStoringAuthenticated()) &&
(!this._isShared ||
this._noAuthorization ||
this._allowsStoringAuthenticated()) &&
// the response either:
(
// contains an Expires header field, or
this._resHeaders.expires ||
// contains an Expires header field, or
(this._resHeaders.expires ||
// contains a max-age response directive, or
// contains a s-maxage response directive and the cache is shared, or
// contains a public response directive.
this._rescc.public || this._rescc['max-age'] || this._rescc['s-maxage'] ||
this._rescc.public ||
this._rescc['max-age'] ||
this._rescc['s-maxage'] ||
// has a status code that is defined as cacheable by default
statusCodeCacheableByDefault.indexOf(this._status) !== -1
));
statusCodeCacheableByDefault.indexOf(this._status) !== -1)
);
}

@@ -136,5 +203,7 @@

// 4.2.1 Calculating Freshness Lifetime
return (this._isShared && this._rescc['s-maxage']) ||
return (
(this._isShared && this._rescc['s-maxage']) ||
this._rescc['max-age'] ||
this._resHeaders.expires;
this._resHeaders.expires
);
}

@@ -144,3 +213,3 @@

if (!req || !req.headers) {
throw Error("Request headers missing");
throw Error('Request headers missing');
}

@@ -164,3 +233,6 @@ }

if (requestCC['min-fresh'] && this.timeToLive() < 1000*requestCC['min-fresh']) {
if (
requestCC['min-fresh'] &&
this.timeToLive() < 1000 * requestCC['min-fresh']
) {
return false;

@@ -172,3 +244,7 @@ }

if (this.stale()) {
const allowsStale = requestCC['max-stale'] && !this._rescc['must-revalidate'] && (true === requestCC['max-stale'] || requestCC['max-stale'] > this.age() - this.maxAge());
const allowsStale =
requestCC['max-stale'] &&
!this._rescc['must-revalidate'] &&
(true === requestCC['max-stale'] ||
requestCC['max-stale'] > this.age() - this.maxAge());
if (!allowsStale) {

@@ -184,8 +260,12 @@ return false;

// The presented effective request URI and that of the stored response match, and
return (!this._url || this._url === req.url) &&
(this._host === req.headers.host) &&
return (
(!this._url || this._url === req.url) &&
this._host === req.headers.host &&
// the request method associated with the stored response allows it to be used for the presented request, and
(!req.method || this._method === req.method || (allowHeadMethod && 'HEAD' === req.method)) &&
(!req.method ||
this._method === req.method ||
(allowHeadMethod && 'HEAD' === req.method)) &&
// selecting header fields nominated by the stored response (if any) match those presented, and
this._varyMatches(req);
this._varyMatches(req)
);
}

@@ -195,3 +275,7 @@

// following Cache-Control response directives (Section 5.2.2) have such an effect: must-revalidate, public, and s-maxage.
return this._rescc['must-revalidate'] || this._rescc.public || this._rescc['s-maxage'];
return (
this._rescc['must-revalidate'] ||
this._rescc.public ||
this._rescc['s-maxage']
);
}

@@ -209,4 +293,7 @@

const fields = this._resHeaders.vary.trim().toLowerCase().split(/\s*,\s*/);
for(const name of fields) {
const fields = this._resHeaders.vary
.trim()
.toLowerCase()
.split(/\s*,\s*/);
for (const name of fields) {
if (req.headers[name] !== this._reqHeaders[name]) return false;

@@ -219,3 +306,3 @@ }

const headers = {};
for(const name in inHeaders) {
for (const name in inHeaders) {
if (hopByHopHeaders[name]) continue;

@@ -227,3 +314,3 @@ headers[name] = inHeaders[name];

const tokens = inHeaders.connection.trim().split(/\s*,\s*/);
for(const name of tokens) {
for (const name of tokens) {
delete headers[name];

@@ -251,4 +338,10 @@ }

// lifetime greater than 24 hours and the response's age is greater than 24 hours.
if (age > 3600*24 && !this._hasExplicitExpiration() && this.maxAge() > 3600*24) {
headers.warning = (headers.warning ? `${headers.warning}, ` : '') + '113 - "rfc7234 5.5.4"';
if (
age > 3600 * 24 &&
!this._hasExplicitExpiration() &&
this.maxAge() > 3600 * 24
) {
headers.warning =
(headers.warning ? `${headers.warning}, ` : '') +
'113 - "rfc7234 5.5.4"';
}

@@ -272,5 +365,5 @@ headers.age = `${Math.round(age)}`;

_serverDate() {
const dateValue = Date.parse(this._resHeaders.date)
const dateValue = Date.parse(this._resHeaders.date);
if (isFinite(dateValue)) {
const maxClockDrift = 8*3600*1000;
const maxClockDrift = 8 * 3600 * 1000;
const clockDrift = Math.abs(this._responseTime - dateValue);

@@ -291,3 +384,3 @@ if (clockDrift < maxClockDrift) {

age() {
let age = Math.max(0, (this._responseTime - this.date())/1000);
let age = Math.max(0, (this._responseTime - this.date()) / 1000);
if (this._resHeaders.age) {

@@ -298,3 +391,3 @@ let ageValue = this._ageValue();

const residentTime = (this.now() - this._responseTime)/1000;
const residentTime = (this.now() - this._responseTime) / 1000;
return age + residentTime;

@@ -322,3 +415,8 @@ }

// so this implementation requires explicit opt-in via public header
if (this._isShared && (this._resHeaders['set-cookie'] && !this._rescc.public && !this._rescc.immutable)) {
if (
this._isShared &&
(this._resHeaders['set-cookie'] &&
!this._rescc.public &&
!this._rescc.immutable)
) {
return 0;

@@ -355,3 +453,3 @@ }

}
return Math.max(defaultMinTtl, (expires - dateValue)/1000);
return Math.max(defaultMinTtl, (expires - dateValue) / 1000);
}

@@ -362,3 +460,6 @@

if (isFinite(lastModified) && dateValue > lastModified) {
return Math.max(defaultMinTtl, (dateValue - lastModified)/1000 * this._cacheHeuristic);
return Math.max(
defaultMinTtl,
((dateValue - lastModified) / 1000) * this._cacheHeuristic
);
}

@@ -371,3 +472,3 @@ }

timeToLive() {
return Math.max(0, this.maxAge() - this.age())*1000;
return Math.max(0, this.maxAge() - this.age()) * 1000;
}

@@ -380,8 +481,8 @@

static fromObject(obj) {
return new this(undefined, undefined, {_fromObject:obj});
return new this(undefined, undefined, { _fromObject: obj });
}
_fromObject(obj) {
if (this._responseTime) throw Error("Reinitialized");
if (!obj || obj.v !== 1) throw Error("Invalid serialization");
if (this._responseTime) throw Error('Reinitialized');
if (!obj || obj.v !== 1) throw Error('Invalid serialization');

@@ -391,3 +492,4 @@ this._responseTime = obj.t;

this._cacheHeuristic = obj.ch;
this._immutableMinTtl = obj.imm !== undefined ? obj.imm : 24*3600*1000;
this._immutableMinTtl =
obj.imm !== undefined ? obj.imm : 24 * 3600 * 1000;
this._status = obj.st;

@@ -406,3 +508,3 @@ this._resHeaders = obj.resh;

return {
v:1,
v: 1,
t: this._responseTime,

@@ -438,3 +540,4 @@ sh: this._isShared,

if (!this._requestMatches(incomingReq, true) || !this.storable()) { // revalidation allowed via HEAD
if (!this._requestMatches(incomingReq, true) || !this.storable()) {
// revalidation allowed via HEAD
// not for the same resource, or wasn't allowed to be cached anyway

@@ -448,7 +551,13 @@ delete headers['if-none-match'];

if (this._resHeaders.etag) {
headers['if-none-match'] = headers['if-none-match'] ? `${headers['if-none-match']}, ${this._resHeaders.etag}` : this._resHeaders.etag;
headers['if-none-match'] = headers['if-none-match']
? `${headers['if-none-match']}, ${this._resHeaders.etag}`
: this._resHeaders.etag;
}
// Clients MAY issue simple (non-subrange) GET requests with either weak validators or strong validators. Clients MUST NOT use weak validators in other forms of request.
const forbidsWeakValidators = headers['accept-ranges'] || headers['if-match'] || headers['if-unmodified-since'] || (this._method && this._method != 'GET');
const forbidsWeakValidators =
headers['accept-ranges'] ||
headers['if-match'] ||
headers['if-unmodified-since'] ||
(this._method && this._method != 'GET');

@@ -461,5 +570,7 @@ /* SHOULD send the Last-Modified value in non-subrange cache validation requests (using If-Modified-Since) if only a Last-Modified value has been provided by the origin server.

if (headers['if-none-match']) {
const etags = headers['if-none-match'].split(/,/).filter(etag => {
return !/^\s*W\//.test(etag);
});
const etags = headers['if-none-match']
.split(/,/)
.filter(etag => {
return !/^\s*W\//.test(etag);
});
if (!etags.length) {

@@ -471,3 +582,6 @@ delete headers['if-none-match'];

}
} else if (this._resHeaders['last-modified'] && !headers['if-modified-since']) {
} else if (
this._resHeaders['last-modified'] &&
!headers['if-modified-since']
) {
headers['if-modified-since'] = this._resHeaders['last-modified'];

@@ -491,3 +605,3 @@ }

if (!response || !response.headers) {
throw Error("Response headers missing");
throw Error('Response headers missing');
}

@@ -500,7 +614,13 @@

matches = false;
} else if (response.headers.etag && !/^\s*W\//.test(response.headers.etag)) {
} else if (
response.headers.etag &&
!/^\s*W\//.test(response.headers.etag)
) {
// "All of the stored responses with the same strong validator are selected.
// If none of the stored responses contain the same strong validator,
// then the cache MUST NOT use the new response to update any stored responses."
matches = this._resHeaders.etag && this._resHeaders.etag.replace(/^\s*W\//,'') === response.headers.etag;
matches =
this._resHeaders.etag &&
this._resHeaders.etag.replace(/^\s*W\//, '') ===
response.headers.etag;
} else if (this._resHeaders.etag && response.headers.etag) {

@@ -510,5 +630,9 @@ // "If the new response contains a weak validator and that validator corresponds

// then the most recent of those matching stored responses is selected for update."
matches = this._resHeaders.etag.replace(/^\s*W\//,'') === response.headers.etag.replace(/^\s*W\//,'');
matches =
this._resHeaders.etag.replace(/^\s*W\//, '') ===
response.headers.etag.replace(/^\s*W\//, '');
} else if (this._resHeaders['last-modified']) {
matches = this._resHeaders['last-modified'] === response.headers['last-modified'];
matches =
this._resHeaders['last-modified'] ===
response.headers['last-modified'];
} else {

@@ -519,4 +643,8 @@ // If the new response does not include any form of validator (such as in the case where

// lacks a validator, then that stored response is selected for update.
if (!this._resHeaders.etag && !this._resHeaders['last-modified'] &&
!response.headers.etag && !response.headers['last-modified']) {
if (
!this._resHeaders.etag &&
!this._resHeaders['last-modified'] &&
!response.headers.etag &&
!response.headers['last-modified']
) {
matches = true;

@@ -534,3 +662,3 @@ }

matches: false,
}
};
}

@@ -541,4 +669,7 @@

const headers = {};
for(const k in this._resHeaders) {
headers[k] = k in response.headers && !excludedFromRevalidationUpdate[k] ? response.headers[k] : this._resHeaders[k];
for (const k in this._resHeaders) {
headers[k] =
k in response.headers && !excludedFromRevalidationUpdate[k]
? response.headers[k]
: this._resHeaders[k];
}

@@ -552,3 +683,8 @@

return {
policy: new this.constructor(request, newResponse, {shared: this._isShared, cacheHeuristic: this._cacheHeuristic, immutableMinTimeToLive: this._immutableMinTtl, trustServerDate: this._trustServerDate}),
policy: new this.constructor(request, newResponse, {
shared: this._isShared,
cacheHeuristic: this._cacheHeuristic,
immutableMinTimeToLive: this._immutableMinTtl,
trustServerDate: this._trustServerDate,
}),
modified: false,

@@ -555,0 +691,0 @@ matches: true,

{
"name": "http-cache-semantics",
"version": "4.0.2",
"description": "Parses Cache-Control and other headers. Helps building correct HTTP caches and proxies",
"repository": "https://github.com/kornelski/http-cache-semantics.git",
"main": "index.js",
"scripts": {
"test": "mocha"
},
"files": [
"index.js"
],
"author": "Kornel Lesiński <kornel@geekhood.net> (https://kornel.ski/)",
"license": "BSD-2-Clause",
"devDependencies": {
"mocha": "^5.1.0"
}
"name": "http-cache-semantics",
"version": "4.0.3",
"description": "Parses Cache-Control and other headers. Helps building correct HTTP caches and proxies",
"repository": "https://github.com/kornelski/http-cache-semantics.git",
"main": "index.js",
"scripts": {
"test": "mocha"
},
"files": [
"index.js"
],
"author": "Kornel Lesiński <kornel@geekhood.net> (https://kornel.ski/)",
"license": "BSD-2-Clause",
"devDependencies": {
"eslint": "^5.13.0",
"eslint-plugin-prettier": "^3.0.1",
"husky": "^0.14.3",
"lint-staged": "^8.1.3",
"mocha": "^5.1.0",
"prettier": "^1.14.3",
"prettier-eslint-cli": "^4.7.1"
}
}

@@ -19,3 +19,7 @@ # Can I cache this? [![Build Status](https://travis-ci.org/kornelski/http-cache-semantics.svg?branch=master)](https://travis-ci.org/kornelski/http-cache-semantics)

// (this is pseudocode, roll your own cache (lru-cache package works))
letsPretendThisIsSomeCache.set(request.url, {policy, response}, policy.timeToLive());
letsPretendThisIsSomeCache.set(
request.url,
{ policy, response },
policy.timeToLive()
);
```

@@ -25,3 +29,3 @@

// And later, when you receive a new request:
const {policy, response} = letsPretendThisIsSomeCache.get(newRequest.url);
const { policy, response } = letsPretendThisIsSomeCache.get(newRequest.url);

@@ -64,3 +68,3 @@ // It's not enough that it exists in the cache, it has to match the new request, too:

cacheHeuristic: 0.1,
immutableMinTimeToLive: 24*3600*1000, // 24h
immutableMinTimeToLive: 24 * 3600 * 1000, // 24h
ignoreCargoCult: false,

@@ -73,3 +77,3 @@ trustServerDate: true,

`options.cacheHeuristic` is a fraction of response's age that is used as a fallback cache duration. The default is 0.1 (10%), e.g. if a file hasn't been modified for 100 days, it'll be cached for 100*0.1 = 10 days.
`options.cacheHeuristic` is a fraction of response's age that is used as a fallback cache duration. The default is 0.1 (10%), e.g. if a file hasn't been modified for 100 days, it'll be cached for 100\*0.1 = 10 days.

@@ -104,3 +108,3 @@ `options.immutableMinTimeToLive` is a number of milliseconds to assume as the default time to cache responses with `Cache-Control: immutable`. Note that [per RFC](http://httpwg.org/http-extensions/immutable.html) these can become stale, so `max-age` still overrides the default.

Returns approximate time in *milliseconds* until the response becomes stale (i.e. not fresh).
Returns approximate time in _milliseconds_ until the response becomes stale (i.e. not fresh).

@@ -133,10 +137,12 @@ After that time (when `timeToLive() <= 0`) the response might not be usable without revalidation. However, there are exceptions, e.g. a client can explicitly allow stale responses, so always check with `satisfiesWithoutRevalidation()`.

* `policy` — A new `CachePolicy` with HTTP headers updated from `revalidationResponse`. You can always replace the old cached `CachePolicy` with the new one.
* `modified` — Boolean indicating whether the response body has changed.
* If `false`, then a valid 304 Not Modified response has been received, and you can reuse the old cached response body.
* If `true`, you should use new response's body (if present), or make another request to the origin server without any conditional headers (i.e. don't use `revalidationHeaders()` this time) to get the new resource.
- `policy` — A new `CachePolicy` with HTTP headers updated from `revalidationResponse`. You can always replace the old cached `CachePolicy` with the new one.
- `modified` — Boolean indicating whether the response body has changed.
- If `false`, then a valid 304 Not Modified response has been received, and you can reuse the old cached response body.
- If `true`, you should use new response's body (if present), or make another request to the origin server without any conditional headers (i.e. don't use `revalidationHeaders()` this time) to get the new resource.
```js
// When serving requests from cache:
const {oldPolicy, oldResponse} = letsPretendThisIsSomeCache.get(newRequest.url);
const { oldPolicy, oldResponse } = letsPretendThisIsSomeCache.get(
newRequest.url
);

@@ -151,7 +157,14 @@ if (!oldPolicy.satisfiesWithoutRevalidation(newRequest)) {

// Create updated policy and combined response from the old and new data
const {policy, modified} = oldPolicy.revalidatedPolicy(newRequest, newResponse);
const { policy, modified } = oldPolicy.revalidatedPolicy(
newRequest,
newResponse
);
const response = modified ? newResponse : oldResponse;
// Update the cache with the newer/fresher response
letsPretendThisIsSomeCache.set(newRequest.url, {policy, response}, policy.timeToLive());
letsPretendThisIsSomeCache.set(
newRequest.url,
{ policy, response },
policy.timeToLive()
);

@@ -170,19 +183,19 @@ // And proceed returning cached response as usual

* [ImageOptim API](https://imageoptim.com/api), [make-fetch-happen](https://github.com/zkat/make-fetch-happen), [cacheable-request](https://www.npmjs.com/package/cacheable-request) ([got](https://www.npmjs.com/package/got)), [npm/registry-fetch](https://github.com/npm/registry-fetch), [etc.](https://github.com/kornelski/http-cache-semantics/network/dependents)
- [ImageOptim API](https://imageoptim.com/api), [make-fetch-happen](https://github.com/zkat/make-fetch-happen), [cacheable-request](https://www.npmjs.com/package/cacheable-request) ([got](https://www.npmjs.com/package/got)), [npm/registry-fetch](https://github.com/npm/registry-fetch), [etc.](https://github.com/kornelski/http-cache-semantics/network/dependents)
## Implemented
* `Cache-Control` response header with all the quirks.
* `Expires` with check for bad clocks.
* `Pragma` response header.
* `Age` response header.
* `Vary` response header.
* Default cacheability of statuses and methods.
* Requests for stale data.
* Filtering of hop-by-hop headers.
* Basic revalidation request
- `Cache-Control` response header with all the quirks.
- `Expires` with check for bad clocks.
- `Pragma` response header.
- `Age` response header.
- `Vary` response header.
- Default cacheability of statuses and methods.
- Requests for stale data.
- Filtering of hop-by-hop headers.
- Basic revalidation request
## Unimplemented
* Merging of range requests, If-Range (but correctly supports them as non-cacheable)
* Revalidation of multiple representations
- Merging of range requests, If-Range (but correctly supports them as non-cacheable)
- Revalidation of multiple representations
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc