@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 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.
@@ -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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"}
@@ -0,0 +1,2 @@
1
+ export * from './accept.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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,8 @@
1
+ {
2
+ "extends": ["../../../tsconfig/node.json"],
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/accept.ts","./src/index.ts"],"version":"5.6.3"}
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "include": [],
3
+ "references": [{ "path": "./tsconfig.build.json" }]
4
+ }