@atproto-labs/xrpc-utils 0.0.1
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/CHANGELOG.md +11 -0
- package/LICENSE.txt +7 -0
- package/dist/accept.d.ts +13 -0
- package/dist/accept.d.ts.map +1 -0
- package/dist/accept.js +108 -0
- package/dist/accept.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
- package/src/accept.ts +143 -0
- package/src/index.ts +1 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +4 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @atproto-labs/xrpc-utils
|
|
2
|
+
|
|
3
|
+
## 0.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#3177](https://github.com/bluesky-social/atproto/pull/3177) [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99) Thanks [@matthieusieben](https://github.com/matthieusieben)! - New utility package to work with xrpc-server
|
|
8
|
+
|
|
9
|
+
- Updated dependencies []:
|
|
10
|
+
- @atproto/xrpc-server@0.7.5
|
|
11
|
+
- @atproto/xrpc@0.6.6
|
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Dual MIT/Apache-2.0 License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2024 Bluesky PBC, and Contributors
|
|
4
|
+
|
|
5
|
+
Except as otherwise noted in individual files, this software is licensed under the MIT license (<http://opensource.org/licenses/MIT>), or the Apache License, Version 2.0 (<http://www.apache.org/licenses/LICENSE-2.0>).
|
|
6
|
+
|
|
7
|
+
Downstream projects and end users may chose either license individually, or both together, at their discretion. The motivation for this dual-licensing is the additional software patent assurance provided by Apache 2.0.
|
package/dist/accept.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type AcceptFlags = {
|
|
2
|
+
q: number;
|
|
3
|
+
};
|
|
4
|
+
export type Accept = [name: string, flags: AcceptFlags];
|
|
5
|
+
export declare const ACCEPT_ENCODING_COMPRESSED: readonly [Accept, ...Accept[]];
|
|
6
|
+
export declare const ACCEPT_ENCODING_UNCOMPRESSED: readonly [Accept, ...Accept[]];
|
|
7
|
+
export declare function buildProxiedContentEncoding(acceptHeader: undefined | string | string[], preferCompressed: boolean): string;
|
|
8
|
+
export declare function negotiateContentEncoding(acceptHeader: undefined | string | string[], preferences: readonly Accept[]): string;
|
|
9
|
+
/**
|
|
10
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Glossary/Quality_values}
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatAcceptHeader(accept: readonly [Accept, ...Accept[]]): string;
|
|
13
|
+
//# sourceMappingURL=accept.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accept.d.ts","sourceRoot":"","sources":["../src/accept.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AACvC,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,CAAC,CAAA;AAEvD,eAAO,MAAM,0BAA0B,EAAE,SAAS,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAKrE,CAAA;AAED,eAAO,MAAM,4BAA4B,EAAE,SAAS,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAKvE,CAAA;AAMD,wBAAgB,2BAA2B,CACzC,YAAY,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,EAAE,EAC3C,gBAAgB,EAAE,OAAO,GACxB,MAAM,CAOR;AAED,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,EAAE,EAC3C,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,MAAM,CAoCR;AAUD;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,GACrC,MAAM,CAER"}
|
package/dist/accept.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ACCEPT_ENCODING_UNCOMPRESSED = exports.ACCEPT_ENCODING_COMPRESSED = void 0;
|
|
4
|
+
exports.buildProxiedContentEncoding = buildProxiedContentEncoding;
|
|
5
|
+
exports.negotiateContentEncoding = negotiateContentEncoding;
|
|
6
|
+
exports.formatAcceptHeader = formatAcceptHeader;
|
|
7
|
+
const xrpc_1 = require("@atproto/xrpc");
|
|
8
|
+
const xrpc_server_1 = require("@atproto/xrpc-server");
|
|
9
|
+
exports.ACCEPT_ENCODING_COMPRESSED = [
|
|
10
|
+
['gzip', { q: 1.0 }],
|
|
11
|
+
['deflate', { q: 0.9 }],
|
|
12
|
+
['br', { q: 0.8 }],
|
|
13
|
+
['identity', { q: 0.1 }],
|
|
14
|
+
];
|
|
15
|
+
exports.ACCEPT_ENCODING_UNCOMPRESSED = [
|
|
16
|
+
['identity', { q: 1.0 }],
|
|
17
|
+
['gzip', { q: 0.3 }],
|
|
18
|
+
['deflate', { q: 0.2 }],
|
|
19
|
+
['br', { q: 0.1 }],
|
|
20
|
+
];
|
|
21
|
+
// accept-encoding defaults to "identity with lowest priority"
|
|
22
|
+
const ACCEPT_ENC_DEFAULT = ['identity', { q: 0.001 }];
|
|
23
|
+
const ACCEPT_FORBID_STAR = ['*', { q: 0 }];
|
|
24
|
+
function buildProxiedContentEncoding(acceptHeader, preferCompressed) {
|
|
25
|
+
return negotiateContentEncoding(acceptHeader, preferCompressed
|
|
26
|
+
? exports.ACCEPT_ENCODING_COMPRESSED
|
|
27
|
+
: exports.ACCEPT_ENCODING_UNCOMPRESSED);
|
|
28
|
+
}
|
|
29
|
+
function negotiateContentEncoding(acceptHeader, preferences) {
|
|
30
|
+
const acceptMap = Object.fromEntries(parseAcceptEncoding(acceptHeader));
|
|
31
|
+
// Make sure the default (identity) is covered by the preferences
|
|
32
|
+
if (!preferences.some(coversIdentityAccept)) {
|
|
33
|
+
preferences = [...preferences, ACCEPT_ENC_DEFAULT];
|
|
34
|
+
}
|
|
35
|
+
const common = preferences.filter(([name]) => {
|
|
36
|
+
const acceptQ = (acceptMap[name] ?? acceptMap['*'])?.q;
|
|
37
|
+
// Per HTTP/1.1, "identity" is always acceptable unless explicitly rejected
|
|
38
|
+
if (name === 'identity') {
|
|
39
|
+
return acceptQ == null || acceptQ > 0;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
return acceptQ != null && acceptQ > 0;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
// Since "identity" was present in the preferences, a missing "identity" in
|
|
46
|
+
// the common array means that the client explicitly rejected it. Let's reflect
|
|
47
|
+
// this by adding it to the common array.
|
|
48
|
+
if (!common.some(coversIdentityAccept)) {
|
|
49
|
+
common.push(ACCEPT_FORBID_STAR);
|
|
50
|
+
}
|
|
51
|
+
// If no common encodings are acceptable, throw a 406 Not Acceptable error
|
|
52
|
+
if (!common.some(isAllowedAccept)) {
|
|
53
|
+
throw new xrpc_server_1.XRPCError(xrpc_1.ResponseType.NotAcceptable, 'this service does not support any of the requested encodings');
|
|
54
|
+
}
|
|
55
|
+
return formatAcceptHeader(common);
|
|
56
|
+
}
|
|
57
|
+
function coversIdentityAccept([name]) {
|
|
58
|
+
return name === 'identity' || name === '*';
|
|
59
|
+
}
|
|
60
|
+
function isAllowedAccept([, flags]) {
|
|
61
|
+
return flags.q > 0;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Glossary/Quality_values}
|
|
65
|
+
*/
|
|
66
|
+
function formatAcceptHeader(accept) {
|
|
67
|
+
return accept.map(formatAcceptPart).join(',');
|
|
68
|
+
}
|
|
69
|
+
function formatAcceptPart([name, flags]) {
|
|
70
|
+
return `${name};q=${flags.q}`;
|
|
71
|
+
}
|
|
72
|
+
function parseAcceptEncoding(acceptEncodings) {
|
|
73
|
+
if (!acceptEncodings?.length)
|
|
74
|
+
return [];
|
|
75
|
+
return Array.isArray(acceptEncodings)
|
|
76
|
+
? acceptEncodings.flatMap(parseAcceptEncoding)
|
|
77
|
+
: acceptEncodings.split(',').map(parseAcceptEncodingDefinition);
|
|
78
|
+
}
|
|
79
|
+
function parseAcceptEncodingDefinition(def) {
|
|
80
|
+
const { length, 0: encoding, 1: params } = def.trim().split(';', 3);
|
|
81
|
+
if (length > 2) {
|
|
82
|
+
throw new xrpc_server_1.InvalidRequestError(`Invalid accept-encoding: "${def}"`);
|
|
83
|
+
}
|
|
84
|
+
if (!encoding || encoding.includes('=')) {
|
|
85
|
+
throw new xrpc_server_1.InvalidRequestError(`Invalid accept-encoding: "${def}"`);
|
|
86
|
+
}
|
|
87
|
+
const flags = { q: 1 };
|
|
88
|
+
if (length === 2) {
|
|
89
|
+
const { length, 0: key, 1: value } = params.split('=', 3);
|
|
90
|
+
if (length !== 2) {
|
|
91
|
+
throw new xrpc_server_1.InvalidRequestError(`Invalid accept-encoding: "${def}"`);
|
|
92
|
+
}
|
|
93
|
+
if (key === 'q' || key === 'Q') {
|
|
94
|
+
const q = parseFloat(value);
|
|
95
|
+
if (q === 0 || (Number.isFinite(q) && q <= 1 && q >= 0.001)) {
|
|
96
|
+
flags.q = q;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
throw new xrpc_server_1.InvalidRequestError(`Invalid accept-encoding: "${def}"`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
throw new xrpc_server_1.InvalidRequestError(`Invalid accept-encoding: "${def}"`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return [encoding.toLowerCase(), flags];
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=accept.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accept.js","sourceRoot":"","sources":["../src/accept.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./accept.js"), exports);
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,8CAA2B"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atproto-labs/xrpc-utils",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "XRPC server utilities for Node.JS",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"atproto",
|
|
8
|
+
"node",
|
|
9
|
+
"xrpc",
|
|
10
|
+
"server",
|
|
11
|
+
"utilities",
|
|
12
|
+
"content",
|
|
13
|
+
"negotiation"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://atproto.com",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/bluesky-social/atproto",
|
|
19
|
+
"directory": "packages/internal/xrpc-utils"
|
|
20
|
+
},
|
|
21
|
+
"type": "commonjs",
|
|
22
|
+
"main": "dist/index.js",
|
|
23
|
+
"types": "dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./accept": {
|
|
30
|
+
"types": "./dist/accept.d.ts",
|
|
31
|
+
"default": "./dist/accept.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@atproto/xrpc": "^0.6.6",
|
|
36
|
+
"@atproto/xrpc-server": "^0.7.5"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"typescript": "^5.6.3"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc --build tsconfig.json"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/accept.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { ResponseType } from '@atproto/xrpc'
|
|
2
|
+
import {
|
|
3
|
+
InvalidRequestError,
|
|
4
|
+
XRPCError as XRPCServerError,
|
|
5
|
+
} from '@atproto/xrpc-server'
|
|
6
|
+
|
|
7
|
+
export type AcceptFlags = { q: number }
|
|
8
|
+
export type Accept = [name: string, flags: AcceptFlags]
|
|
9
|
+
|
|
10
|
+
export const ACCEPT_ENCODING_COMPRESSED: readonly [Accept, ...Accept[]] = [
|
|
11
|
+
['gzip', { q: 1.0 }],
|
|
12
|
+
['deflate', { q: 0.9 }],
|
|
13
|
+
['br', { q: 0.8 }],
|
|
14
|
+
['identity', { q: 0.1 }],
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export const ACCEPT_ENCODING_UNCOMPRESSED: readonly [Accept, ...Accept[]] = [
|
|
18
|
+
['identity', { q: 1.0 }],
|
|
19
|
+
['gzip', { q: 0.3 }],
|
|
20
|
+
['deflate', { q: 0.2 }],
|
|
21
|
+
['br', { q: 0.1 }],
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
// accept-encoding defaults to "identity with lowest priority"
|
|
25
|
+
const ACCEPT_ENC_DEFAULT = ['identity', { q: 0.001 }] as const satisfies Accept
|
|
26
|
+
const ACCEPT_FORBID_STAR = ['*', { q: 0 }] as const satisfies Accept
|
|
27
|
+
|
|
28
|
+
export function buildProxiedContentEncoding(
|
|
29
|
+
acceptHeader: undefined | string | string[],
|
|
30
|
+
preferCompressed: boolean,
|
|
31
|
+
): string {
|
|
32
|
+
return negotiateContentEncoding(
|
|
33
|
+
acceptHeader,
|
|
34
|
+
preferCompressed
|
|
35
|
+
? ACCEPT_ENCODING_COMPRESSED
|
|
36
|
+
: ACCEPT_ENCODING_UNCOMPRESSED,
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function negotiateContentEncoding(
|
|
41
|
+
acceptHeader: undefined | string | string[],
|
|
42
|
+
preferences: readonly Accept[],
|
|
43
|
+
): string {
|
|
44
|
+
const acceptMap = Object.fromEntries<undefined | AcceptFlags>(
|
|
45
|
+
parseAcceptEncoding(acceptHeader),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// Make sure the default (identity) is covered by the preferences
|
|
49
|
+
if (!preferences.some(coversIdentityAccept)) {
|
|
50
|
+
preferences = [...preferences, ACCEPT_ENC_DEFAULT]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const common = preferences.filter(([name]) => {
|
|
54
|
+
const acceptQ = (acceptMap[name] ?? acceptMap['*'])?.q
|
|
55
|
+
// Per HTTP/1.1, "identity" is always acceptable unless explicitly rejected
|
|
56
|
+
if (name === 'identity') {
|
|
57
|
+
return acceptQ == null || acceptQ > 0
|
|
58
|
+
} else {
|
|
59
|
+
return acceptQ != null && acceptQ > 0
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Since "identity" was present in the preferences, a missing "identity" in
|
|
64
|
+
// the common array means that the client explicitly rejected it. Let's reflect
|
|
65
|
+
// this by adding it to the common array.
|
|
66
|
+
if (!common.some(coversIdentityAccept)) {
|
|
67
|
+
common.push(ACCEPT_FORBID_STAR)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// If no common encodings are acceptable, throw a 406 Not Acceptable error
|
|
71
|
+
if (!common.some(isAllowedAccept)) {
|
|
72
|
+
throw new XRPCServerError(
|
|
73
|
+
ResponseType.NotAcceptable,
|
|
74
|
+
'this service does not support any of the requested encodings',
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return formatAcceptHeader(common as [Accept, ...Accept[]])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function coversIdentityAccept([name]: Accept): boolean {
|
|
82
|
+
return name === 'identity' || name === '*'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isAllowedAccept([, flags]: Accept): boolean {
|
|
86
|
+
return flags.q > 0
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Glossary/Quality_values}
|
|
91
|
+
*/
|
|
92
|
+
export function formatAcceptHeader(
|
|
93
|
+
accept: readonly [Accept, ...Accept[]],
|
|
94
|
+
): string {
|
|
95
|
+
return accept.map(formatAcceptPart).join(',')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatAcceptPart([name, flags]: Accept): string {
|
|
99
|
+
return `${name};q=${flags.q}`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseAcceptEncoding(
|
|
103
|
+
acceptEncodings: undefined | string | string[],
|
|
104
|
+
): Accept[] {
|
|
105
|
+
if (!acceptEncodings?.length) return []
|
|
106
|
+
|
|
107
|
+
return Array.isArray(acceptEncodings)
|
|
108
|
+
? acceptEncodings.flatMap(parseAcceptEncoding)
|
|
109
|
+
: acceptEncodings.split(',').map(parseAcceptEncodingDefinition)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseAcceptEncodingDefinition(def: string): Accept {
|
|
113
|
+
const { length, 0: encoding, 1: params } = def.trim().split(';', 3)
|
|
114
|
+
|
|
115
|
+
if (length > 2) {
|
|
116
|
+
throw new InvalidRequestError(`Invalid accept-encoding: "${def}"`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!encoding || encoding.includes('=')) {
|
|
120
|
+
throw new InvalidRequestError(`Invalid accept-encoding: "${def}"`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const flags = { q: 1 }
|
|
124
|
+
if (length === 2) {
|
|
125
|
+
const { length, 0: key, 1: value } = params.split('=', 3)
|
|
126
|
+
if (length !== 2) {
|
|
127
|
+
throw new InvalidRequestError(`Invalid accept-encoding: "${def}"`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (key === 'q' || key === 'Q') {
|
|
131
|
+
const q = parseFloat(value)
|
|
132
|
+
if (q === 0 || (Number.isFinite(q) && q <= 1 && q >= 0.001)) {
|
|
133
|
+
flags.q = q
|
|
134
|
+
} else {
|
|
135
|
+
throw new InvalidRequestError(`Invalid accept-encoding: "${def}"`)
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
throw new InvalidRequestError(`Invalid accept-encoding: "${def}"`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return [encoding.toLowerCase(), flags]
|
|
143
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './accept.js'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/accept.ts","./src/index.ts"],"version":"5.6.3"}
|
package/tsconfig.json
ADDED