@atproto/lex-server 0.0.1 → 0.0.3
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 +20 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lex-server.d.ts +12 -9
- package/dist/lex-server.d.ts.map +1 -1
- package/dist/lex-server.js +35 -27
- package/dist/lex-server.js.map +1 -1
- package/dist/nodejs.d.ts +3 -3
- package/dist/nodejs.d.ts.map +1 -1
- package/dist/nodejs.js +49 -19
- package/dist/nodejs.js.map +1 -1
- package/dist/service-auth.d.ts +67 -0
- package/dist/service-auth.d.ts.map +1 -0
- package/dist/service-auth.js +191 -0
- package/dist/service-auth.js.map +1 -0
- package/package.json +10 -7
- package/src/index.ts +1 -0
- package/src/lex-server.test.ts +2 -2
- package/src/lex-server.ts +56 -45
- package/src/nodejs.ts +64 -30
- package/src/service-auth.ts +375 -0
- package/dist/example.d.ts +0 -2
- package/dist/example.d.ts.map +0 -1
- package/dist/example.js +0 -36
- package/dist/example.js.map +0 -1
- package/dist/lex-auth-error.d.ts +0 -15
- package/dist/lex-auth-error.d.ts.map +0 -1
- package/dist/lex-auth-error.js +0 -52
- package/dist/lex-auth-error.js.map +0 -1
- package/dist/subscripotion.d.ts +0 -2
- package/dist/subscripotion.d.ts.map +0 -1
- package/dist/subscripotion.js +0 -36
- package/dist/subscripotion.js.map +0 -1
- package/dist/test.d.mts +0 -2
- package/dist/test.d.mts.map +0 -1
- package/dist/test.mjs +0 -52
- package/dist/test.mjs.map +0 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { AtprotoDid, AtprotoDidDocument } from '@atproto/did';
|
|
2
|
+
import { DidString } from '@atproto/lex-schema';
|
|
3
|
+
import { CreateDidResolverOptions } from '@atproto-labs/did-resolver';
|
|
4
|
+
import { LexRouterAuth } from './lex-server.js';
|
|
5
|
+
/**
|
|
6
|
+
* A function to check and record nonce uniqueness.
|
|
7
|
+
*/
|
|
8
|
+
export type UniqueNonceChecker = (nonce: string) => Promise<boolean>;
|
|
9
|
+
export type ServiceAuthOptions = CreateDidResolverOptions & {
|
|
10
|
+
/**
|
|
11
|
+
* Expected audience ("aud") claim in the JWT token. Set to `null` to skip
|
|
12
|
+
* audience verification (not recommended).
|
|
13
|
+
*/
|
|
14
|
+
audience: null | DidString;
|
|
15
|
+
/**
|
|
16
|
+
* Function to check and record nonce uniqueness. The value checked here must
|
|
17
|
+
* be unique within {@link ServiceAuthOptions.maxAge} seconds before and after
|
|
18
|
+
* the current time.
|
|
19
|
+
*
|
|
20
|
+
* @param nonce - The nonce to check.
|
|
21
|
+
*/
|
|
22
|
+
unique: UniqueNonceChecker;
|
|
23
|
+
/**
|
|
24
|
+
* Maximum age of the JWT token in seconds.
|
|
25
|
+
*
|
|
26
|
+
* @default 300 (5 minutes)
|
|
27
|
+
*/
|
|
28
|
+
maxAge?: number;
|
|
29
|
+
};
|
|
30
|
+
export type ServiceAuthCredentials = {
|
|
31
|
+
did: AtprotoDid;
|
|
32
|
+
didDocument: AtprotoDidDocument;
|
|
33
|
+
jwt: ParsedJwt;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Creates an authentication handler for LexRouter that verifies AT protocol
|
|
37
|
+
* "service auth" JWT bearer tokens signed by decentralized identifiers (DIDs).
|
|
38
|
+
*/
|
|
39
|
+
export declare function serviceAuth({ audience, maxAge, unique, ...options }: ServiceAuthOptions): LexRouterAuth<ServiceAuthCredentials>;
|
|
40
|
+
export type ParseJwtOptions = {
|
|
41
|
+
maxAge: number;
|
|
42
|
+
audience: null | DidString;
|
|
43
|
+
unique: UniqueNonceChecker;
|
|
44
|
+
lxm: string;
|
|
45
|
+
};
|
|
46
|
+
export type ParsedJwt = {
|
|
47
|
+
header: HeaderObject;
|
|
48
|
+
payload: PayloadObject;
|
|
49
|
+
message: Uint8Array;
|
|
50
|
+
signature: Uint8Array;
|
|
51
|
+
};
|
|
52
|
+
type HeaderObject = {
|
|
53
|
+
alg: string;
|
|
54
|
+
typ?: string;
|
|
55
|
+
};
|
|
56
|
+
type PayloadObject = {
|
|
57
|
+
iss: DidString;
|
|
58
|
+
aud: DidString;
|
|
59
|
+
exp: number;
|
|
60
|
+
iat?: number;
|
|
61
|
+
nbf?: number;
|
|
62
|
+
lxm?: string;
|
|
63
|
+
nonce?: string;
|
|
64
|
+
};
|
|
65
|
+
export declare function isPayloadObject(obj: unknown): obj is PayloadObject;
|
|
66
|
+
export {};
|
|
67
|
+
//# sourceMappingURL=service-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-auth.d.ts","sourceRoot":"","sources":["../src/service-auth.ts"],"names":[],"mappings":"AACA,OAAO,EACL,UAAU,EACV,kBAAkB,EAGnB,MAAM,cAAc,CAAA;AAErB,OAAO,EAAE,SAAS,EAAe,MAAM,qBAAqB,CAAA;AAC5D,OAAO,EACL,wBAAwB,EAEzB,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAI/C;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AAEpE,MAAM,MAAM,kBAAkB,GAAG,wBAAwB,GAAG;IAC1D;;;OAGG;IACH,QAAQ,EAAE,IAAI,GAAG,SAAS,CAAA;IAC1B;;;;;;OAMG;IACH,MAAM,EAAE,kBAAkB,CAAA;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,GAAG,EAAE,UAAU,CAAA;IACf,WAAW,EAAE,kBAAkB,CAAA;IAC/B,GAAG,EAAE,SAAS,CAAA;CACf,CAAA;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,MAAe,EACf,MAAM,EACN,GAAG,OAAO,EACX,EAAE,kBAAkB,GAAG,aAAa,CAAC,sBAAsB,CAAC,CAyD5D;AA2ED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,IAAI,GAAG,SAAS,CAAA;IAC1B,MAAM,EAAE,kBAAkB,CAAA;IAC1B,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,YAAY,CAAA;IACpB,OAAO,EAAE,aAAa,CAAA;IACtB,OAAO,EAAE,UAAU,CAAA;IACnB,SAAS,EAAE,UAAU,CAAA;CACtB,CAAA;AA4HD,KAAK,YAAY,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AASjD,KAAK,aAAa,GAAG;IACnB,GAAG,EAAE,SAAS,CAAA;IACd,GAAG,EAAE,SAAS,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AACD,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,aAAa,CAalE"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.serviceAuth = serviceAuth;
|
|
4
|
+
exports.isPayloadObject = isPayloadObject;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const crypto = tslib_1.__importStar(require("@atproto/crypto"));
|
|
7
|
+
const did_1 = require("@atproto/did");
|
|
8
|
+
const lex_data_1 = require("@atproto/lex-data");
|
|
9
|
+
const lex_schema_1 = require("@atproto/lex-schema");
|
|
10
|
+
const did_resolver_1 = require("@atproto-labs/did-resolver");
|
|
11
|
+
const errors_js_1 = require("./errors.js");
|
|
12
|
+
const BEARER_PREFIX = 'Bearer ';
|
|
13
|
+
/**
|
|
14
|
+
* Creates an authentication handler for LexRouter that verifies AT protocol
|
|
15
|
+
* "service auth" JWT bearer tokens signed by decentralized identifiers (DIDs).
|
|
16
|
+
*/
|
|
17
|
+
function serviceAuth({ audience, maxAge = 5 * 60, unique, ...options }) {
|
|
18
|
+
const didResolver = (0, did_resolver_1.createDidResolver)(options);
|
|
19
|
+
return async ({ request, method }) => {
|
|
20
|
+
const { signal } = request;
|
|
21
|
+
const jwt = await parseJwtBearer(request, {
|
|
22
|
+
lxm: method.nsid,
|
|
23
|
+
maxAge,
|
|
24
|
+
audience,
|
|
25
|
+
unique,
|
|
26
|
+
});
|
|
27
|
+
let didDocument = await didResolver
|
|
28
|
+
.resolve(jwt.payload.iss, { signal })
|
|
29
|
+
.catch((cause) => {
|
|
30
|
+
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'Could not resolve DID document', { Bearer: { error: 'DidResolutionFailed' } }, { cause });
|
|
31
|
+
});
|
|
32
|
+
const key = getAtprotoSigningKey(didDocument);
|
|
33
|
+
if (!key || !(await verifyJwt(jwt, key))) {
|
|
34
|
+
signal.throwIfAborted();
|
|
35
|
+
// Try refreshing the DID document in case it was updated
|
|
36
|
+
didDocument = await didResolver
|
|
37
|
+
.resolve(jwt.payload.iss, { signal, noCache: true })
|
|
38
|
+
.catch((cause) => {
|
|
39
|
+
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'Could not resolve DID document', { Bearer: { error: 'DidResolutionFailed' } }, { cause });
|
|
40
|
+
});
|
|
41
|
+
// Verify again with the fresh key (if it changed)
|
|
42
|
+
const keyFresh = getAtprotoSigningKey(didDocument);
|
|
43
|
+
if (!keyFresh || key === keyFresh || !(await verifyJwt(jwt, keyFresh))) {
|
|
44
|
+
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'Invalid JWT signature', { Bearer: { error: 'BadJwtSignature' } });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
did: didDocument.id,
|
|
49
|
+
didDocument,
|
|
50
|
+
jwt,
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function verifyJwt(jwt, key) {
|
|
55
|
+
try {
|
|
56
|
+
return await crypto.verifySignature(key, jwt.message, jwt.signature, {
|
|
57
|
+
jwtAlg: jwt.header.alg,
|
|
58
|
+
allowMalleableSig: true,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (cause) {
|
|
62
|
+
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'Could not verify JWT signature', { Bearer: { error: 'BadJwtSignature' } }, { cause });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function getAtprotoSigningKey(didDocument) {
|
|
66
|
+
try {
|
|
67
|
+
const key = didDocument.verificationMethod?.find(isAtprotoVerificationMethod, didDocument);
|
|
68
|
+
if (key?.publicKeyMultibase) {
|
|
69
|
+
if (key.type === 'EcdsaSecp256r1VerificationKey2019') {
|
|
70
|
+
const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase);
|
|
71
|
+
return crypto.formatDidKey(crypto.P256_JWT_ALG, keyBytes);
|
|
72
|
+
}
|
|
73
|
+
else if (key.type === 'EcdsaSecp256k1VerificationKey2019') {
|
|
74
|
+
const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase);
|
|
75
|
+
return crypto.formatDidKey(crypto.SECP256K1_JWT_ALG, keyBytes);
|
|
76
|
+
}
|
|
77
|
+
else if (key.type === 'Multikey') {
|
|
78
|
+
const parsed = crypto.parseMultikey(key.publicKeyMultibase);
|
|
79
|
+
return crypto.formatDidKey(parsed.jwtAlg, parsed.keyBytes);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Invalid key, ignore
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
function isAtprotoVerificationMethod(vm) {
|
|
89
|
+
return typeof vm === 'object' && (0, did_1.matchesIdentifier)(this.id, 'atproto', vm.id);
|
|
90
|
+
}
|
|
91
|
+
async function parseJwtBearer(request, options) {
|
|
92
|
+
const authorization = request.headers.get('authorization');
|
|
93
|
+
if (!authorization?.startsWith(BEARER_PREFIX)) {
|
|
94
|
+
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'Bearer token required', { Bearer: { error: 'MissingBearer' } });
|
|
95
|
+
}
|
|
96
|
+
const token = authorization.slice(BEARER_PREFIX.length).trim();
|
|
97
|
+
return parseJwt(token, options);
|
|
98
|
+
}
|
|
99
|
+
async function parseJwt(token, options) {
|
|
100
|
+
const { length, 0: headerB64, 1: payloadB64, 2: signatureB64, } = token.split('.');
|
|
101
|
+
if (length !== 3) {
|
|
102
|
+
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'Invalid JWT token', { Bearer: { error: 'BadJwt' } });
|
|
103
|
+
}
|
|
104
|
+
let header;
|
|
105
|
+
try {
|
|
106
|
+
header = jsonFromBase64(headerB64, isHeaderObject);
|
|
107
|
+
}
|
|
108
|
+
catch (cause) {
|
|
109
|
+
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'Invalid JWT token', { Bearer: { error: 'BadJwt' } }, { cause });
|
|
110
|
+
}
|
|
111
|
+
if (header.alg === 'none' ||
|
|
112
|
+
// service tokens are not OAuth 2.0 access tokens
|
|
113
|
+
// https://datatracker.ietf.org/doc/html/rfc9068
|
|
114
|
+
header.typ === 'at+jwt' ||
|
|
115
|
+
// "refresh+jwt" is a non-standard type used by the @atproto packages
|
|
116
|
+
header.typ === 'refresh+jwt' ||
|
|
117
|
+
// "DPoP" proofs are not meant to be used as service tokens
|
|
118
|
+
// https://datatracker.ietf.org/doc/html/rfc9449
|
|
119
|
+
header.typ === 'dpop+jwt') {
|
|
120
|
+
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'Invalid JWT token', { Bearer: { error: 'BadJwt' } });
|
|
121
|
+
}
|
|
122
|
+
let payload;
|
|
123
|
+
try {
|
|
124
|
+
payload = jsonFromBase64(payloadB64, isPayloadObject);
|
|
125
|
+
}
|
|
126
|
+
catch (cause) {
|
|
127
|
+
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'Invalid JWT token', { Bearer: { error: 'BadJwt' } }, { cause });
|
|
128
|
+
}
|
|
129
|
+
if (options.audience !== null && options.audience !== payload.aud) {
|
|
130
|
+
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'Invalid audience', {
|
|
131
|
+
Bearer: { error: 'InvalidAudience' },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const now = Math.floor(Date.now() / 1000);
|
|
135
|
+
if (payload.nbf != null && now < payload.nbf) {
|
|
136
|
+
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'JWT token not yet valid', { Bearer: { error: 'JwtNotYetValid' } });
|
|
137
|
+
}
|
|
138
|
+
if (now > payload.exp) {
|
|
139
|
+
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'JWT token expired', { Bearer: { error: 'JwtExpired' } });
|
|
140
|
+
}
|
|
141
|
+
// Prevent issuer from generating very long-lived tokens
|
|
142
|
+
if (timeDiff(now, payload.exp) > options.maxAge ||
|
|
143
|
+
timeDiff(now, payload.iat) > options.maxAge) {
|
|
144
|
+
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'JWT token too old', { Bearer: { error: 'JwtTooOld' } });
|
|
145
|
+
}
|
|
146
|
+
if (payload.lxm != null && typeof payload.lxm !== options.lxm) {
|
|
147
|
+
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'Invalid JWT lexicon method ("lxm")', { Bearer: { error: 'BadJwtLexiconMethod' } });
|
|
148
|
+
}
|
|
149
|
+
if (payload.nonce != null && !(await (0, options.unique)(payload.nonce))) {
|
|
150
|
+
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'Replay attack detected: nonce is not unique', { Bearer: { error: 'NonceNotUnique' } });
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
header,
|
|
154
|
+
payload,
|
|
155
|
+
message: textEncoder.encode(`${headerB64}.${payloadB64}`),
|
|
156
|
+
signature: (0, lex_data_1.fromBase64)(signatureB64, 'base64url'),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const textEncoder = /*#__PURE__*/ new TextEncoder();
|
|
160
|
+
function isHeaderObject(obj) {
|
|
161
|
+
return ((0, lex_data_1.isPlainObject)(obj) &&
|
|
162
|
+
typeof obj.alg === 'string' &&
|
|
163
|
+
(obj.typ === undefined || typeof obj.typ === 'string'));
|
|
164
|
+
}
|
|
165
|
+
function isPayloadObject(obj) {
|
|
166
|
+
return ((0, lex_data_1.isPlainObject)(obj) &&
|
|
167
|
+
typeof obj.iss === 'string' &&
|
|
168
|
+
typeof obj.aud === 'string' &&
|
|
169
|
+
(obj.lxm === undefined || typeof obj.lxm === 'string') &&
|
|
170
|
+
(obj.nonce === undefined || typeof obj.nonce === 'string') &&
|
|
171
|
+
(obj.iat === undefined || isPositiveInt(obj.iat)) &&
|
|
172
|
+
(obj.nbf === undefined || isPositiveInt(obj.nbf)) &&
|
|
173
|
+
isPositiveInt(obj.exp) &&
|
|
174
|
+
(0, lex_schema_1.isDidString)(obj.iss) &&
|
|
175
|
+
(0, lex_schema_1.isDidString)(obj.aud));
|
|
176
|
+
}
|
|
177
|
+
function timeDiff(t1, t2) {
|
|
178
|
+
if (t2 === undefined)
|
|
179
|
+
return 0;
|
|
180
|
+
return Math.abs(t1 - t2);
|
|
181
|
+
}
|
|
182
|
+
function isPositiveInt(value) {
|
|
183
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0;
|
|
184
|
+
}
|
|
185
|
+
function jsonFromBase64(b64, isType) {
|
|
186
|
+
const obj = JSON.parse((0, lex_data_1.utf8FromBase64)(b64, 'base64url'));
|
|
187
|
+
if (isType(obj))
|
|
188
|
+
return obj;
|
|
189
|
+
throw new Error('Invalid type');
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=service-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-auth.js","sourceRoot":"","sources":["../src/service-auth.ts"],"names":[],"mappings":"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","sourcesContent":["import * as crypto from '@atproto/crypto'\nimport {\n AtprotoDid,\n AtprotoDidDocument,\n Did,\n matchesIdentifier,\n} from '@atproto/did'\nimport { fromBase64, isPlainObject, utf8FromBase64 } from '@atproto/lex-data'\nimport { DidString, isDidString } from '@atproto/lex-schema'\nimport {\n CreateDidResolverOptions,\n createDidResolver,\n} from '@atproto-labs/did-resolver'\nimport { LexServerAuthError } from './errors.js'\nimport { LexRouterAuth } from './lex-server.js'\n\nconst BEARER_PREFIX = 'Bearer '\n\n/**\n * A function to check and record nonce uniqueness.\n */\nexport type UniqueNonceChecker = (nonce: string) => Promise<boolean>\n\nexport type ServiceAuthOptions = CreateDidResolverOptions & {\n /**\n * Expected audience (\"aud\") claim in the JWT token. Set to `null` to skip\n * audience verification (not recommended).\n */\n audience: null | DidString\n /**\n * Function to check and record nonce uniqueness. The value checked here must\n * be unique within {@link ServiceAuthOptions.maxAge} seconds before and after\n * the current time.\n *\n * @param nonce - The nonce to check.\n */\n unique: UniqueNonceChecker\n /**\n * Maximum age of the JWT token in seconds.\n *\n * @default 300 (5 minutes)\n */\n maxAge?: number\n}\n\nexport type ServiceAuthCredentials = {\n did: AtprotoDid\n didDocument: AtprotoDidDocument\n jwt: ParsedJwt\n}\n\n/**\n * Creates an authentication handler for LexRouter that verifies AT protocol\n * \"service auth\" JWT bearer tokens signed by decentralized identifiers (DIDs).\n */\nexport function serviceAuth({\n audience,\n maxAge = 5 * 60,\n unique,\n ...options\n}: ServiceAuthOptions): LexRouterAuth<ServiceAuthCredentials> {\n const didResolver = createDidResolver(options)\n\n return async ({ request, method }) => {\n const { signal } = request\n const jwt = await parseJwtBearer(request, {\n lxm: method.nsid,\n maxAge,\n audience,\n unique,\n })\n\n let didDocument: AtprotoDidDocument = await didResolver\n .resolve(jwt.payload.iss, { signal })\n .catch((cause) => {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Could not resolve DID document',\n { Bearer: { error: 'DidResolutionFailed' } },\n { cause },\n )\n })\n\n const key = getAtprotoSigningKey(didDocument)\n\n if (!key || !(await verifyJwt(jwt, key))) {\n signal.throwIfAborted()\n\n // Try refreshing the DID document in case it was updated\n didDocument = await didResolver\n .resolve(jwt.payload.iss, { signal, noCache: true })\n .catch((cause) => {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Could not resolve DID document',\n { Bearer: { error: 'DidResolutionFailed' } },\n { cause },\n )\n })\n\n // Verify again with the fresh key (if it changed)\n const keyFresh = getAtprotoSigningKey(didDocument)\n if (!keyFresh || key === keyFresh || !(await verifyJwt(jwt, keyFresh))) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT signature',\n { Bearer: { error: 'BadJwtSignature' } },\n )\n }\n }\n\n return {\n did: didDocument.id,\n didDocument,\n jwt,\n }\n }\n}\n\nasync function verifyJwt(jwt: ParsedJwt, key: Did<'key'>) {\n try {\n return await crypto.verifySignature(key, jwt.message, jwt.signature, {\n jwtAlg: jwt.header.alg,\n allowMalleableSig: true,\n })\n } catch (cause) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Could not verify JWT signature',\n { Bearer: { error: 'BadJwtSignature' } },\n { cause },\n )\n }\n}\n\nfunction getAtprotoSigningKey(\n didDocument: AtprotoDidDocument,\n): null | Did<'key'> {\n try {\n const key = didDocument.verificationMethod?.find(\n isAtprotoVerificationMethod,\n didDocument,\n )\n\n if (key?.publicKeyMultibase) {\n if (key.type === 'EcdsaSecp256r1VerificationKey2019') {\n const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)\n return crypto.formatDidKey(crypto.P256_JWT_ALG, keyBytes)\n } else if (key.type === 'EcdsaSecp256k1VerificationKey2019') {\n const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)\n return crypto.formatDidKey(crypto.SECP256K1_JWT_ALG, keyBytes)\n } else if (key.type === 'Multikey') {\n const parsed = crypto.parseMultikey(key.publicKeyMultibase)\n return crypto.formatDidKey(parsed.jwtAlg, parsed.keyBytes)\n }\n }\n } catch {\n // Invalid key, ignore\n }\n\n return null\n}\n\nfunction isAtprotoVerificationMethod<\n V extends string | { id: string; type: string; publicKeyMultibase?: string },\n>(\n this: AtprotoDidDocument,\n vm: V,\n): vm is Exclude<V, string> & {\n id: `${string}#atproto`\n} {\n return typeof vm === 'object' && matchesIdentifier(this.id, 'atproto', vm.id)\n}\n\nasync function parseJwtBearer(\n request: Request,\n options: ParseJwtOptions,\n): Promise<ParsedJwt> {\n const authorization = request.headers.get('authorization')\n if (!authorization?.startsWith(BEARER_PREFIX)) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Bearer token required',\n { Bearer: { error: 'MissingBearer' } },\n )\n }\n\n const token = authorization.slice(BEARER_PREFIX.length).trim()\n\n return parseJwt(token, options)\n}\n\nexport type ParseJwtOptions = {\n maxAge: number\n audience: null | DidString\n unique: UniqueNonceChecker\n lxm: string\n}\n\nexport type ParsedJwt = {\n header: HeaderObject\n payload: PayloadObject\n message: Uint8Array\n signature: Uint8Array\n}\n\nasync function parseJwt(\n token: string,\n options: ParseJwtOptions,\n): Promise<ParsedJwt> {\n const {\n length,\n 0: headerB64,\n 1: payloadB64,\n 2: signatureB64,\n } = token.split('.')\n if (length !== 3) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n )\n }\n\n let header: HeaderObject\n try {\n header = jsonFromBase64(headerB64, isHeaderObject)\n } catch (cause) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n { cause },\n )\n }\n\n if (\n header.alg === 'none' ||\n // service tokens are not OAuth 2.0 access tokens\n // https://datatracker.ietf.org/doc/html/rfc9068\n header.typ === 'at+jwt' ||\n // \"refresh+jwt\" is a non-standard type used by the @atproto packages\n header.typ === 'refresh+jwt' ||\n // \"DPoP\" proofs are not meant to be used as service tokens\n // https://datatracker.ietf.org/doc/html/rfc9449\n header.typ === 'dpop+jwt'\n ) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n )\n }\n\n let payload: PayloadObject\n try {\n payload = jsonFromBase64(payloadB64, isPayloadObject)\n } catch (cause) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n { cause },\n )\n }\n\n if (options.audience !== null && options.audience !== payload.aud) {\n throw new LexServerAuthError('AuthenticationRequired', 'Invalid audience', {\n Bearer: { error: 'InvalidAudience' },\n })\n }\n\n const now = Math.floor(Date.now() / 1000)\n\n if (payload.nbf != null && now < payload.nbf) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'JWT token not yet valid',\n { Bearer: { error: 'JwtNotYetValid' } },\n )\n }\n\n if (now > payload.exp) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'JWT token expired',\n { Bearer: { error: 'JwtExpired' } },\n )\n }\n\n // Prevent issuer from generating very long-lived tokens\n if (\n timeDiff(now, payload.exp) > options.maxAge ||\n timeDiff(now, payload.iat) > options.maxAge\n ) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'JWT token too old',\n { Bearer: { error: 'JwtTooOld' } },\n )\n }\n\n if (payload.lxm != null && typeof payload.lxm !== options.lxm) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT lexicon method (\"lxm\")',\n { Bearer: { error: 'BadJwtLexiconMethod' } },\n )\n }\n\n if (payload.nonce != null && !(await (0, options.unique)(payload.nonce))) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Replay attack detected: nonce is not unique',\n { Bearer: { error: 'NonceNotUnique' } },\n )\n }\n\n return {\n header,\n payload,\n message: textEncoder.encode(`${headerB64}.${payloadB64}`),\n signature: fromBase64(signatureB64, 'base64url'),\n }\n}\n\nconst textEncoder = /*#__PURE__*/ new TextEncoder()\n\ntype HeaderObject = { alg: string; typ?: string }\nfunction isHeaderObject(obj: unknown): obj is HeaderObject {\n return (\n isPlainObject(obj) &&\n typeof obj.alg === 'string' &&\n (obj.typ === undefined || typeof obj.typ === 'string')\n )\n}\n\ntype PayloadObject = {\n iss: DidString\n aud: DidString\n exp: number\n iat?: number\n nbf?: number\n lxm?: string\n nonce?: string\n}\nexport function isPayloadObject(obj: unknown): obj is PayloadObject {\n return (\n isPlainObject(obj) &&\n typeof obj.iss === 'string' &&\n typeof obj.aud === 'string' &&\n (obj.lxm === undefined || typeof obj.lxm === 'string') &&\n (obj.nonce === undefined || typeof obj.nonce === 'string') &&\n (obj.iat === undefined || isPositiveInt(obj.iat)) &&\n (obj.nbf === undefined || isPositiveInt(obj.nbf)) &&\n isPositiveInt(obj.exp) &&\n isDidString(obj.iss) &&\n isDidString(obj.aud)\n )\n}\n\nfunction timeDiff(t1: number, t2?: number): number {\n if (t2 === undefined) return 0\n return Math.abs(t1 - t2)\n}\n\nfunction isPositiveInt(value: unknown): value is number {\n return typeof value === 'number' && Number.isInteger(value) && value > 0\n}\n\nfunction jsonFromBase64<T>(b64: string, isType: (obj: unknown) => obj is T): T {\n const obj = JSON.parse(utf8FromBase64(b64, 'base64url'))\n if (isType(obj)) return obj\n throw new Error('Invalid type')\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/lex-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Request router for Atproto Lexicon protocols and schemas",
|
|
6
6
|
"keywords": [
|
|
@@ -45,17 +45,20 @@
|
|
|
45
45
|
"http-terminator": "^3.2.0",
|
|
46
46
|
"tslib": "^2.8.1",
|
|
47
47
|
"ws": "^8.18.3",
|
|
48
|
-
"@atproto/
|
|
49
|
-
"@atproto/
|
|
50
|
-
"@atproto/
|
|
51
|
-
"@atproto/lex-
|
|
48
|
+
"@atproto-labs/did-resolver": "0.2.5",
|
|
49
|
+
"@atproto/crypto": "0.4.5",
|
|
50
|
+
"@atproto/did": "0.2.4",
|
|
51
|
+
"@atproto/lex-cbor": "0.0.6",
|
|
52
|
+
"@atproto/lex-data": "0.0.6",
|
|
53
|
+
"@atproto/lex-json": "0.0.6",
|
|
54
|
+
"@atproto/lex-schema": "0.0.7"
|
|
52
55
|
},
|
|
53
56
|
"devDependencies": {
|
|
54
57
|
"@types/ws": "^8.18.1",
|
|
55
58
|
"@vitest/coverage-v8": "4.0.16",
|
|
56
59
|
"vitest": "^4.0.16",
|
|
57
|
-
"@atproto/lex": "0.0.
|
|
58
|
-
"@atproto/lex-client": "0.0.
|
|
60
|
+
"@atproto/lex": "0.0.9",
|
|
61
|
+
"@atproto/lex-client": "0.0.7"
|
|
59
62
|
},
|
|
60
63
|
"scripts": {
|
|
61
64
|
"build": "tsc --build tsconfig.build.json",
|
package/src/index.ts
CHANGED
package/src/lex-server.test.ts
CHANGED
|
@@ -287,7 +287,7 @@ describe('Authentication', () => {
|
|
|
287
287
|
function createBasicAuth(allowed: {
|
|
288
288
|
username: string
|
|
289
289
|
password: string
|
|
290
|
-
}): LexRouterAuth<
|
|
290
|
+
}): LexRouterAuth<BasicAuthCredentials> {
|
|
291
291
|
return async ({ request }) => {
|
|
292
292
|
const header = request.headers.get('authorization') ?? ''
|
|
293
293
|
if (!header.startsWith('Basic ')) {
|
|
@@ -1514,7 +1514,7 @@ describe('Subscription', () => {
|
|
|
1514
1514
|
|
|
1515
1515
|
const router = new LexRouter({ upgradeWebSocket }).add(
|
|
1516
1516
|
io.example.subscribe,
|
|
1517
|
-
async function* ({ params: { message },
|
|
1517
|
+
async function* ({ params: { message }, signal }) {
|
|
1518
1518
|
try {
|
|
1519
1519
|
for (; sentCount < 10; ) {
|
|
1520
1520
|
await scheduler.wait(5, { signal })
|
package/src/lex-server.ts
CHANGED
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
} from '@atproto/lex-schema'
|
|
19
19
|
import { drainWebsocket } from './lib/drain-websocket.js'
|
|
20
20
|
|
|
21
|
-
type
|
|
21
|
+
type Awaitable<T> = T | Promise<T>
|
|
22
|
+
export type LexMethod = Query | Procedure | Subscription
|
|
22
23
|
|
|
23
24
|
export type NetAddr = {
|
|
24
25
|
hostname: string
|
|
@@ -31,11 +32,11 @@ export type UnixAddr = {
|
|
|
31
32
|
transport: 'unix' | 'unixpacket'
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
export type Addr = NetAddr | UnixAddr
|
|
35
|
+
export type Addr = NetAddr | UnixAddr | undefined
|
|
35
36
|
|
|
36
|
-
export type ConnectionInfo = {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
export type ConnectionInfo<A extends Addr = Addr> = {
|
|
38
|
+
remoteAddr: A
|
|
39
|
+
completed: Promise<void>
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
type Handler = (
|
|
@@ -48,6 +49,7 @@ export type LexRouterHandlerContext<Method extends LexMethod, Credentials> = {
|
|
|
48
49
|
input: InferMethodInput<Method, Body>
|
|
49
50
|
params: InferMethodParams<Method>
|
|
50
51
|
request: Request
|
|
52
|
+
signal: AbortSignal
|
|
51
53
|
connection?: ConnectionInfo
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -72,14 +74,14 @@ export type LexRouterMethodHandler<
|
|
|
72
74
|
Credentials = unknown,
|
|
73
75
|
> = (
|
|
74
76
|
ctx: LexRouterHandlerContext<Method, Credentials>,
|
|
75
|
-
) =>
|
|
77
|
+
) => Awaitable<LexRouterHandlerOutput<Method>>
|
|
76
78
|
|
|
77
79
|
export type LexRouterMethodConfig<
|
|
78
80
|
Method extends Query | Procedure = Query | Procedure,
|
|
79
81
|
Credentials = unknown,
|
|
80
82
|
> = {
|
|
81
83
|
handler: LexRouterMethodHandler<Method, Credentials>
|
|
82
|
-
auth: LexRouterAuth<
|
|
84
|
+
auth: LexRouterAuth<Credentials, Method>
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
export type LexRouterSubscriptionHandler<
|
|
@@ -94,18 +96,19 @@ export type LexRouterSubscriptionConfig<
|
|
|
94
96
|
Credentials = unknown,
|
|
95
97
|
> = {
|
|
96
98
|
handler: LexRouterSubscriptionHandler<Method, Credentials>
|
|
97
|
-
auth: LexRouterAuth<
|
|
99
|
+
auth: LexRouterAuth<Credentials, Method>
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
export type LexRouterAuthContext<Method extends LexMethod = LexMethod> = {
|
|
103
|
+
method: Method
|
|
101
104
|
params: InferMethodParams<Method>
|
|
102
105
|
request: Request
|
|
103
106
|
connection?: ConnectionInfo
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
export type LexRouterAuth<
|
|
107
|
-
Method extends LexMethod = LexMethod,
|
|
108
110
|
Credentials = unknown,
|
|
111
|
+
Method extends LexMethod = LexMethod,
|
|
109
112
|
> = (ctx: LexRouterAuthContext<Method>) => Credentials | Promise<Credentials>
|
|
110
113
|
|
|
111
114
|
export type LexErrorHandlerContext = {
|
|
@@ -185,7 +188,7 @@ export class LexRouter {
|
|
|
185
188
|
private buildMethodHandler<Method extends Query | Procedure, Credentials>(
|
|
186
189
|
method: Method,
|
|
187
190
|
methodHandler: LexRouterMethodHandler<Method, Credentials>,
|
|
188
|
-
auth?: LexRouterAuth<
|
|
191
|
+
auth?: LexRouterAuth<Credentials, Method>,
|
|
189
192
|
): Handler {
|
|
190
193
|
const getInput = (
|
|
191
194
|
method.type === 'procedure'
|
|
@@ -193,10 +196,7 @@ export class LexRouter {
|
|
|
193
196
|
: getQueryInput.bind(method)
|
|
194
197
|
) as (request: Request) => Promise<InferMethodInput<Method, Body>>
|
|
195
198
|
|
|
196
|
-
return async (
|
|
197
|
-
request: Request,
|
|
198
|
-
connection?: ConnectionInfo,
|
|
199
|
-
): Promise<Response> => {
|
|
199
|
+
return async (request, connection) => {
|
|
200
200
|
// @NOTE CORS requests should be handled by a middleware before reaching
|
|
201
201
|
// this point.
|
|
202
202
|
if (
|
|
@@ -216,7 +216,7 @@ export class LexRouter {
|
|
|
216
216
|
const params = method.parameters.fromURLSearchParams(url.searchParams)
|
|
217
217
|
|
|
218
218
|
const credentials = auth
|
|
219
|
-
? await auth({ params, request, connection })
|
|
219
|
+
? await auth({ method, params, request, connection })
|
|
220
220
|
: (undefined as Credentials)
|
|
221
221
|
|
|
222
222
|
const input = await getInput(request)
|
|
@@ -227,6 +227,7 @@ export class LexRouter {
|
|
|
227
227
|
input,
|
|
228
228
|
request,
|
|
229
229
|
connection,
|
|
230
|
+
signal: request.signal,
|
|
230
231
|
})
|
|
231
232
|
|
|
232
233
|
if (output instanceof Response) {
|
|
@@ -258,7 +259,7 @@ export class LexRouter {
|
|
|
258
259
|
private buildSubscriptionHandler<Method extends Subscription, Credentials>(
|
|
259
260
|
method: Method,
|
|
260
261
|
methodHandler: LexRouterSubscriptionHandler<Method, Credentials>,
|
|
261
|
-
auth?: LexRouterAuth<
|
|
262
|
+
auth?: LexRouterAuth<Credentials, Method>,
|
|
262
263
|
): Handler {
|
|
263
264
|
const {
|
|
264
265
|
onHandlerError,
|
|
@@ -272,10 +273,7 @@ export class LexRouter {
|
|
|
272
273
|
)
|
|
273
274
|
}
|
|
274
275
|
|
|
275
|
-
return async (
|
|
276
|
-
request: Request,
|
|
277
|
-
connection?: ConnectionInfo,
|
|
278
|
-
): Promise<Response> => {
|
|
276
|
+
return async (request, connection) => {
|
|
279
277
|
if (request.method !== 'GET') {
|
|
280
278
|
return Response.json(
|
|
281
279
|
{ error: 'InvalidRequest', message: 'Method not allowed' },
|
|
@@ -302,19 +300,34 @@ export class LexRouter {
|
|
|
302
300
|
)
|
|
303
301
|
}
|
|
304
302
|
|
|
303
|
+
if (request.signal.aborted) {
|
|
304
|
+
return Response.json(
|
|
305
|
+
{ error: 'RequestAborted', message: 'The request was aborted' },
|
|
306
|
+
{ status: 499 },
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
|
|
305
310
|
try {
|
|
306
311
|
const { response, socket } = upgradeWebSocket(request)
|
|
307
312
|
|
|
308
|
-
|
|
313
|
+
// @NOTE We are using a distinct signal than request.signal because that
|
|
314
|
+
// signal may get aborted before the WebSocket is closed (this is the
|
|
315
|
+
// case with Deno).
|
|
316
|
+
const abortController = new AbortController()
|
|
317
|
+
const { signal } = abortController
|
|
318
|
+
const abort = () => abortController.abort()
|
|
319
|
+
|
|
320
|
+
const onMessage = (event: unknown) => {
|
|
309
321
|
const error = new LexError(
|
|
310
322
|
'InvalidRequest',
|
|
311
323
|
'XRPC subscriptions do not accept messages',
|
|
324
|
+
{ cause: event },
|
|
312
325
|
)
|
|
313
326
|
socket.send(encodeErrorFrame(error))
|
|
314
327
|
socket.close(1008, error.error)
|
|
315
|
-
}
|
|
328
|
+
}
|
|
316
329
|
|
|
317
|
-
|
|
330
|
+
const onOpen = async () => {
|
|
318
331
|
try {
|
|
319
332
|
const url = new URL(request.url)
|
|
320
333
|
const params = method.parameters.fromURLSearchParams(
|
|
@@ -322,10 +335,10 @@ export class LexRouter {
|
|
|
322
335
|
)
|
|
323
336
|
|
|
324
337
|
const credentials: Credentials = auth
|
|
325
|
-
? await auth({ params, request, connection })
|
|
338
|
+
? await auth({ method, params, request, connection })
|
|
326
339
|
: (undefined as Credentials)
|
|
327
340
|
|
|
328
|
-
|
|
341
|
+
signal.throwIfAborted()
|
|
329
342
|
|
|
330
343
|
const iterable = methodHandler({
|
|
331
344
|
credentials,
|
|
@@ -333,31 +346,20 @@ export class LexRouter {
|
|
|
333
346
|
input: undefined as InferMethodInput<Method, Body>,
|
|
334
347
|
request,
|
|
335
348
|
connection,
|
|
349
|
+
signal,
|
|
336
350
|
})
|
|
337
351
|
|
|
338
352
|
const iterator = iterable[Symbol.asyncIterator]()
|
|
339
353
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
await iterator.return!()
|
|
346
|
-
} catch {
|
|
347
|
-
// Ignore
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
socket.addEventListener('error', abort)
|
|
351
|
-
socket.addEventListener('close', abort)
|
|
352
|
-
}
|
|
354
|
+
signal.addEventListener('abort', async () => {
|
|
355
|
+
// @NOTE will cause the process to crash if this throws
|
|
356
|
+
await iterator.return?.()
|
|
357
|
+
})
|
|
353
358
|
|
|
354
|
-
while (socket.readyState === 1) {
|
|
359
|
+
while (!signal.aborted && socket.readyState === 1) {
|
|
355
360
|
const result = await iterator.next()
|
|
356
361
|
if (result.done) break
|
|
357
362
|
|
|
358
|
-
// Should not be needed (socket would emit "close" event)
|
|
359
|
-
request.signal.throwIfAborted()
|
|
360
|
-
|
|
361
363
|
// @TODO add validation of output based on method.output.schema?
|
|
362
364
|
|
|
363
365
|
const data = encodeMessageFrame(method, result.value)
|
|
@@ -366,10 +368,12 @@ export class LexRouter {
|
|
|
366
368
|
|
|
367
369
|
// Apply backpressure by waiting for the buffered data to drain
|
|
368
370
|
// before generating the next message
|
|
369
|
-
await drainWebsocket(socket,
|
|
371
|
+
await drainWebsocket(socket, signal, this.options)
|
|
370
372
|
}
|
|
371
373
|
|
|
372
|
-
socket.
|
|
374
|
+
if (socket.readyState === 1) {
|
|
375
|
+
socket.close(1000)
|
|
376
|
+
}
|
|
373
377
|
} catch (error) {
|
|
374
378
|
// If the socket is still open, send an error frame before closing
|
|
375
379
|
if (socket.readyState === 1) {
|
|
@@ -391,8 +395,15 @@ export class LexRouter {
|
|
|
391
395
|
if (onHandlerError && !isAbortReason(request.signal, error)) {
|
|
392
396
|
await onHandlerError({ error, request, method })
|
|
393
397
|
}
|
|
398
|
+
} finally {
|
|
399
|
+
abortController.abort()
|
|
394
400
|
}
|
|
395
|
-
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
socket.addEventListener('error', abort)
|
|
404
|
+
socket.addEventListener('close', abort)
|
|
405
|
+
socket.addEventListener('open', onOpen)
|
|
406
|
+
socket.addEventListener('message', onMessage)
|
|
396
407
|
|
|
397
408
|
return response
|
|
398
409
|
} catch (error) {
|