@atproto/lex-client 0.0.10 → 0.0.11
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 +15 -0
- package/dist/agent.d.ts +5 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +15 -1
- package/dist/agent.js.map +1 -1
- package/dist/client.d.ts +4 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +48 -47
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +83 -64
- package/dist/errors.js.map +1 -1
- package/dist/response.d.ts +3 -2
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +26 -23
- package/dist/response.js.map +1 -1
- package/dist/www-authenticate.d.ts +12 -0
- package/dist/www-authenticate.d.ts.map +1 -0
- package/dist/www-authenticate.js +57 -0
- package/dist/www-authenticate.js.map +1 -0
- package/dist/xrpc.d.ts +2 -10
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +15 -28
- package/dist/xrpc.js.map +1 -1
- package/package.json +6 -6
- package/src/agent.ts +34 -1
- package/src/client.ts +8 -8
- package/src/errors.ts +153 -118
- package/src/response.ts +46 -44
- package/src/www-authenticate.test.ts +227 -0
- package/src/www-authenticate.ts +77 -0
- package/src/xrpc.ts +17 -49
package/src/response.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ResultSuccess,
|
|
8
8
|
} from '@atproto/lex-schema'
|
|
9
9
|
import {
|
|
10
|
+
XrpcAuthenticationError,
|
|
10
11
|
XrpcResponseError,
|
|
11
12
|
XrpcUpstreamError,
|
|
12
13
|
isXrpcErrorPayload,
|
|
@@ -26,7 +27,7 @@ export type XrpcResponsePayload<M extends Procedure | Query> =
|
|
|
26
27
|
*
|
|
27
28
|
* @implements {ResultSuccess<XrpcResponse<M>>} for convenience in result handling contexts.
|
|
28
29
|
*/
|
|
29
|
-
export class XrpcResponse<
|
|
30
|
+
export class XrpcResponse<M extends Procedure | Query>
|
|
30
31
|
implements ResultSuccess<XrpcResponse<M>>
|
|
31
32
|
{
|
|
32
33
|
/** @see {@link ResultSuccess.success} */
|
|
@@ -63,7 +64,8 @@ export class XrpcResponse<const M extends Procedure | Query>
|
|
|
63
64
|
/**
|
|
64
65
|
* @throws {XrpcResponseError} in case of (valid) XRPC error responses. Use
|
|
65
66
|
* {@link XrpcResponseError.matchesSchema} to narrow the error type based on
|
|
66
|
-
* the method's declared error schema.
|
|
67
|
+
* the method's declared error schema. This can be narrowed further as a
|
|
68
|
+
* {@link XrpcAuthenticationError} if the error is an authentication error.
|
|
67
69
|
* @throws {XrpcUpstreamError} when the response is not a valid XRPC
|
|
68
70
|
* response, or if the response does not conform to the method's schema.
|
|
69
71
|
*/
|
|
@@ -79,39 +81,49 @@ export class XrpcResponse<const M extends Procedure | Query>
|
|
|
79
81
|
// @NOTE redirect is set to 'follow', so we shouldn't get 3xx responses here
|
|
80
82
|
if (response.status < 200 || response.status >= 300) {
|
|
81
83
|
// Always parse json for error responses
|
|
82
|
-
const payload = await readPayload(response, { parse: true })
|
|
84
|
+
const payload = await readPayload(response, { parse: true }).catch(
|
|
85
|
+
(cause) => {
|
|
86
|
+
throw new XrpcUpstreamError(
|
|
87
|
+
method,
|
|
88
|
+
response,
|
|
89
|
+
null,
|
|
90
|
+
'Unable to parse response payload',
|
|
91
|
+
{ cause },
|
|
92
|
+
)
|
|
93
|
+
},
|
|
94
|
+
)
|
|
83
95
|
|
|
96
|
+
// Properly formatted XRPC error response ?
|
|
84
97
|
if (response.status >= 400 && isXrpcErrorPayload(payload)) {
|
|
85
|
-
throw
|
|
86
|
-
method,
|
|
87
|
-
response
|
|
88
|
-
response.headers,
|
|
89
|
-
payload,
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (response.status >= 500) {
|
|
94
|
-
throw new XrpcUpstreamError(
|
|
95
|
-
'UpstreamFailure',
|
|
96
|
-
`Upstream server encountered an error`,
|
|
97
|
-
response,
|
|
98
|
-
payload,
|
|
99
|
-
)
|
|
98
|
+
throw response.status === 401
|
|
99
|
+
? new XrpcAuthenticationError<M>(method, response, payload)
|
|
100
|
+
: new XrpcResponseError<M>(method, response, payload)
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
// Invalid XRPC response (we probably did not hit an XRPC implementation)
|
|
102
104
|
throw new XrpcUpstreamError(
|
|
103
|
-
|
|
104
|
-
response.status >= 400
|
|
105
|
-
? `Upstream server returned an invalid response payload`
|
|
106
|
-
: `Upstream server returned an invalid status code`,
|
|
105
|
+
method,
|
|
107
106
|
response,
|
|
108
107
|
payload,
|
|
108
|
+
response.status >= 500
|
|
109
|
+
? 'Upstream server encountered an error'
|
|
110
|
+
: response.status >= 400
|
|
111
|
+
? 'Invalid response payload'
|
|
112
|
+
: 'Invalid response status code',
|
|
109
113
|
)
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
// Only parse json if the schema expects it
|
|
113
117
|
const payload = await readPayload(response, {
|
|
114
118
|
parse: shouldParse(method),
|
|
119
|
+
}).catch((cause) => {
|
|
120
|
+
throw new XrpcUpstreamError(
|
|
121
|
+
method,
|
|
122
|
+
response,
|
|
123
|
+
null,
|
|
124
|
+
'Unable to parse response payload',
|
|
125
|
+
{ cause },
|
|
126
|
+
)
|
|
115
127
|
})
|
|
116
128
|
|
|
117
129
|
// Response is successful (2xx). Validate payload (data and encoding) against schema.
|
|
@@ -119,22 +131,22 @@ export class XrpcResponse<const M extends Procedure | Query>
|
|
|
119
131
|
// Schema expects no payload
|
|
120
132
|
if (payload) {
|
|
121
133
|
throw new XrpcUpstreamError(
|
|
122
|
-
|
|
123
|
-
`Expected response with no body, got ${payload.encoding}`,
|
|
134
|
+
method,
|
|
124
135
|
response,
|
|
125
136
|
payload,
|
|
137
|
+
`Expected response with no body, got ${payload.encoding}`,
|
|
126
138
|
)
|
|
127
139
|
}
|
|
128
140
|
} else {
|
|
129
141
|
// Schema expects a payload
|
|
130
142
|
if (!payload || !method.output.matchesEncoding(payload.encoding)) {
|
|
131
143
|
throw new XrpcUpstreamError(
|
|
132
|
-
|
|
144
|
+
method,
|
|
145
|
+
response,
|
|
146
|
+
payload,
|
|
133
147
|
payload
|
|
134
148
|
? `Expected ${method.output.encoding} response, got ${payload.encoding}`
|
|
135
149
|
: `Expected non-empty response with content-type ${method.output.encoding}`,
|
|
136
|
-
response,
|
|
137
|
-
payload,
|
|
138
150
|
)
|
|
139
151
|
}
|
|
140
152
|
|
|
@@ -144,10 +156,10 @@ export class XrpcResponse<const M extends Procedure | Query>
|
|
|
144
156
|
|
|
145
157
|
if (!result.success) {
|
|
146
158
|
throw new XrpcUpstreamError(
|
|
147
|
-
|
|
148
|
-
`Response validation failed: ${result.reason.message}`,
|
|
159
|
+
method,
|
|
149
160
|
response,
|
|
150
161
|
payload,
|
|
162
|
+
`Response validation failed: ${result.reason.message}`,
|
|
151
163
|
{ cause: result.reason },
|
|
152
164
|
)
|
|
153
165
|
}
|
|
@@ -204,22 +216,12 @@ async function readPayload(
|
|
|
204
216
|
// to @ipld/dag-json)
|
|
205
217
|
const text = await response.text()
|
|
206
218
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// parsing to JSON then converting to Lex (?)
|
|
219
|
+
// @NOTE Using `lexParse(text)` (instead of `jsonToLex(json)`) here as
|
|
220
|
+
// using a reviver function during JSON.parse should be faster than
|
|
221
|
+
// parsing to JSON then converting to Lex (?)
|
|
211
222
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
} catch (cause) {
|
|
215
|
-
throw new XrpcUpstreamError(
|
|
216
|
-
'InvalidResponse',
|
|
217
|
-
'Invalid JSON response body',
|
|
218
|
-
response,
|
|
219
|
-
null,
|
|
220
|
-
{ cause },
|
|
221
|
-
)
|
|
222
|
-
}
|
|
223
|
+
// @TODO verify statement above
|
|
224
|
+
return { encoding, body: lexParse(text) }
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
return { encoding, body: new Uint8Array(await response.arrayBuffer()) }
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { parseWWWAuthenticateHeader } from './www-authenticate.js'
|
|
3
|
+
|
|
4
|
+
describe(parseWWWAuthenticateHeader, () => {
|
|
5
|
+
describe('auth-params', () => {
|
|
6
|
+
it('parses single unquoted auth param', () => {
|
|
7
|
+
expect(parseWWWAuthenticateHeader('Bearer realm=example.com')).toEqual({
|
|
8
|
+
Bearer: { realm: 'example.com' },
|
|
9
|
+
})
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('parses single quoted auth param', () => {
|
|
13
|
+
expect(parseWWWAuthenticateHeader('Bearer realm="example.com"')).toEqual({
|
|
14
|
+
Bearer: { realm: 'example.com' },
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('parses quoted values with spaces', () => {
|
|
19
|
+
expect(parseWWWAuthenticateHeader('Bearer realm="my realm"')).toEqual({
|
|
20
|
+
Bearer: { realm: 'my realm' },
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('parses quoted values with escaped double quotes', () => {
|
|
25
|
+
expect(
|
|
26
|
+
parseWWWAuthenticateHeader('Bearer realm="example\\"quoted\\""'),
|
|
27
|
+
).toEqual({
|
|
28
|
+
Bearer: { realm: 'example"quoted"' },
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('parses quoted values with escaped backslash', () => {
|
|
33
|
+
expect(
|
|
34
|
+
parseWWWAuthenticateHeader('Bearer realm="path\\\\to\\\\file"'),
|
|
35
|
+
).toEqual({
|
|
36
|
+
Bearer: { realm: 'path\\to\\file' },
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('parses param names with hyphens', () => {
|
|
41
|
+
expect(
|
|
42
|
+
parseWWWAuthenticateHeader('Bearer error-uri="https://example.com"'),
|
|
43
|
+
).toEqual({
|
|
44
|
+
Bearer: { 'error-uri': 'https://example.com' },
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('parses param names with underscores', () => {
|
|
49
|
+
expect(
|
|
50
|
+
parseWWWAuthenticateHeader('Bearer error_description="test"'),
|
|
51
|
+
).toEqual({
|
|
52
|
+
Bearer: { error_description: 'test' },
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('parses param with numeric value', () => {
|
|
57
|
+
expect(parseWWWAuthenticateHeader('Bearer max-age=3600')).toEqual({
|
|
58
|
+
Bearer: { 'max-age': '3600' },
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('parses empty quoted value', () => {
|
|
63
|
+
expect(parseWWWAuthenticateHeader('Bearer realm=""')).toEqual({
|
|
64
|
+
Bearer: { realm: '' },
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('parses Basic auth challenge', () => {
|
|
69
|
+
expect(
|
|
70
|
+
parseWWWAuthenticateHeader('Basic realm="Access to staging site"'),
|
|
71
|
+
).toEqual({
|
|
72
|
+
Basic: { realm: 'Access to staging site' },
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('parses Bearer with realm', () => {
|
|
77
|
+
expect(
|
|
78
|
+
parseWWWAuthenticateHeader('Bearer realm="https://auth.example.com"'),
|
|
79
|
+
).toEqual({
|
|
80
|
+
Bearer: { realm: 'https://auth.example.com' },
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('parses DPoP with algs param', () => {
|
|
85
|
+
expect(parseWWWAuthenticateHeader('DPoP algs="ES256 RS256"')).toEqual({
|
|
86
|
+
DPoP: { algs: 'ES256 RS256' },
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('parses Digest auth challenge', () => {
|
|
91
|
+
const result = parseWWWAuthenticateHeader(
|
|
92
|
+
'Digest realm="digest-realm", nonce="abc123"',
|
|
93
|
+
)
|
|
94
|
+
expect(result).toEqual({
|
|
95
|
+
Digest: { realm: 'digest-realm', nonce: 'abc123' },
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('handle empty unquoted params', () => {
|
|
100
|
+
const result = parseWWWAuthenticateHeader('Bearer realm=')
|
|
101
|
+
expect(result).toEqual({ Bearer: { realm: '' } })
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('handle empty params', () => {
|
|
105
|
+
const result = parseWWWAuthenticateHeader('Bearer realm=""')
|
|
106
|
+
expect(result).toEqual({ Bearer: { realm: '' } })
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('treats scheme-only header as scheme with itself as token68', () => {
|
|
110
|
+
const result = parseWWWAuthenticateHeader('Basic')
|
|
111
|
+
expect(result).toEqual({ Basic: {} })
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('parses multiple challenges with commas and escaped quotes', () => {
|
|
115
|
+
const result = parseWWWAuthenticateHeader(
|
|
116
|
+
`Newauth realm="apps", type=1,\n\t title="Login to \\"apps\\"", Basic realm="simple"`,
|
|
117
|
+
)
|
|
118
|
+
expect(result).toEqual({
|
|
119
|
+
Newauth: {
|
|
120
|
+
realm: 'apps',
|
|
121
|
+
type: '1',
|
|
122
|
+
title: 'Login to "apps"',
|
|
123
|
+
},
|
|
124
|
+
Basic: { realm: 'simple' },
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('parses first challenge before comma', () => {
|
|
129
|
+
const result = parseWWWAuthenticateHeader(
|
|
130
|
+
'Basic realm="foo", Bearer realm="bar"',
|
|
131
|
+
)
|
|
132
|
+
expect(result).toEqual({
|
|
133
|
+
Basic: { realm: 'foo' },
|
|
134
|
+
Bearer: { realm: 'bar' },
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('edge cases', () => {
|
|
140
|
+
it('handles empty string', () => {
|
|
141
|
+
expect(parseWWWAuthenticateHeader('')).toEqual({})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('handles whitespace-only string', () => {
|
|
145
|
+
expect(parseWWWAuthenticateHeader(' ')).toEqual({})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('trims whitespace from header', () => {
|
|
149
|
+
expect(parseWWWAuthenticateHeader(' Bearer realm="test" ')).toEqual({
|
|
150
|
+
Bearer: { realm: 'test' },
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('handles commas as quoted param value', () => {
|
|
155
|
+
expect(
|
|
156
|
+
parseWWWAuthenticateHeader('Bearer realm="example, with, commas"'),
|
|
157
|
+
).toEqual({ Bearer: { realm: 'example, with, commas' } })
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('handles multiple challenges with varying whitespace', () => {
|
|
161
|
+
expect(
|
|
162
|
+
parseWWWAuthenticateHeader(
|
|
163
|
+
' Bearer realm="test" , Basic rr= ',
|
|
164
|
+
),
|
|
165
|
+
).toEqual({
|
|
166
|
+
Bearer: { realm: 'test' },
|
|
167
|
+
Basic: { rr: '' },
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('invalid challenges', () => {
|
|
173
|
+
it('parses single challenge with no comma correctly', () => {
|
|
174
|
+
expect(
|
|
175
|
+
parseWWWAuthenticateHeader('Bearer realm="oauth" error="invalid"'),
|
|
176
|
+
).toEqual(undefined)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('ignores invalid challenges', () => {
|
|
180
|
+
expect(parseWWWAuthenticateHeader('Bearer realm="unclosed')).toEqual(
|
|
181
|
+
undefined,
|
|
182
|
+
)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('handles random text without equals sign as token68', () => {
|
|
186
|
+
expect(parseWWWAuthenticateHeader('Bearer sometoken')).toEqual({
|
|
187
|
+
Bearer: 'sometoken',
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('ignores trailing whitespace after scheme', () => {
|
|
192
|
+
expect(parseWWWAuthenticateHeader('Bearer ')).toEqual({ Bearer: {} })
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('handles duplicate params (last wins)', () => {
|
|
196
|
+
expect(
|
|
197
|
+
parseWWWAuthenticateHeader('Bearer realm="first", realm="second"'),
|
|
198
|
+
).toEqual({ Bearer: { realm: 'second' } })
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('extracts valid param after invalid characters', () => {
|
|
202
|
+
expect(parseWWWAuthenticateHeader('Bearer realm@foo="bar"')).toEqual({
|
|
203
|
+
Bearer: { 'realm@foo': 'bar' },
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('ignores param with empty name', () => {
|
|
208
|
+
expect(parseWWWAuthenticateHeader('Bearer ="value"')).toEqual(undefined)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('handles completely malformed input gracefully', () => {
|
|
212
|
+
expect(parseWWWAuthenticateHeader('!@#$%')).toEqual({ '!@#$%': {} })
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('handles duplicate schemes as invalid', () => {
|
|
216
|
+
expect(
|
|
217
|
+
parseWWWAuthenticateHeader('Basic realm="first", Basic realm="second"'),
|
|
218
|
+
).toEqual(undefined)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('handles params without scheme as invalid', () => {
|
|
222
|
+
expect(parseWWWAuthenticateHeader('Bearer, realm="foo"')).toEqual(
|
|
223
|
+
undefined,
|
|
224
|
+
)
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
type WWWAuthenticateParams = { [authParam in string]: string }
|
|
2
|
+
export type WWWAuthenticate = {
|
|
3
|
+
[authScheme in string]:
|
|
4
|
+
| string // token68
|
|
5
|
+
| WWWAuthenticateParams
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns `undefined` if the header is malformed.
|
|
10
|
+
*/
|
|
11
|
+
export function parseWWWAuthenticateHeader(
|
|
12
|
+
header?: unknown,
|
|
13
|
+
): undefined | WWWAuthenticate {
|
|
14
|
+
if (typeof header !== 'string') return undefined
|
|
15
|
+
|
|
16
|
+
const wwwAuthenticate: WWWAuthenticate = {}
|
|
17
|
+
|
|
18
|
+
// Split over commas, not within quoted strings
|
|
19
|
+
const trimmedHeader = header.trim()
|
|
20
|
+
if (!trimmedHeader) return wwwAuthenticate
|
|
21
|
+
|
|
22
|
+
const parts = trimmedHeader.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/)
|
|
23
|
+
|
|
24
|
+
let currentParams: WWWAuthenticateParams | null = null
|
|
25
|
+
|
|
26
|
+
for (let part of parts) {
|
|
27
|
+
// Check if the part starts with an auth scheme
|
|
28
|
+
const schemeMatch = part.trim().match(/^([^"=\s]+)(\s+.*)?$/)
|
|
29
|
+
if (schemeMatch) {
|
|
30
|
+
const scheme = schemeMatch[1]
|
|
31
|
+
|
|
32
|
+
// Duplicate scheme
|
|
33
|
+
if (Object.hasOwn(wwwAuthenticate, scheme)) return undefined
|
|
34
|
+
|
|
35
|
+
const rest = schemeMatch[2]?.trim()
|
|
36
|
+
if (!rest) {
|
|
37
|
+
// Scheme only (no params or token68)
|
|
38
|
+
currentParams = null
|
|
39
|
+
wwwAuthenticate[scheme] = Object.create(null)
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!rest.includes('=')) {
|
|
44
|
+
// Scheme with token68
|
|
45
|
+
currentParams = null
|
|
46
|
+
wwwAuthenticate[scheme] = rest
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Scheme with params
|
|
51
|
+
|
|
52
|
+
currentParams = Object.create(null) as WWWAuthenticateParams
|
|
53
|
+
wwwAuthenticate[scheme] = currentParams
|
|
54
|
+
|
|
55
|
+
// Fall through to parse params
|
|
56
|
+
part = rest
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Invalid header
|
|
60
|
+
if (!currentParams) return undefined
|
|
61
|
+
|
|
62
|
+
const param = part.match(
|
|
63
|
+
/^\s*([^"\s=]+)=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([^\s,"]*))\s*$/,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
// invalid param
|
|
67
|
+
if (!param) return undefined
|
|
68
|
+
|
|
69
|
+
const paramName = param[1]
|
|
70
|
+
const paramValue =
|
|
71
|
+
param[3] ?? param[2]!.slice(1, -1).replaceAll(/\\(.)/g, '$1')
|
|
72
|
+
|
|
73
|
+
currentParams[paramName] = paramValue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return wwwAuthenticate
|
|
77
|
+
}
|
package/src/xrpc.ts
CHANGED
|
@@ -13,11 +13,7 @@ import {
|
|
|
13
13
|
getMain,
|
|
14
14
|
} from '@atproto/lex-schema'
|
|
15
15
|
import { Agent } from './agent.js'
|
|
16
|
-
import {
|
|
17
|
-
XrpcResponseError,
|
|
18
|
-
XrpcUnexpectedError,
|
|
19
|
-
XrpcUpstreamError,
|
|
20
|
-
} from './errors.js'
|
|
16
|
+
import { XrpcFailure, asXrpcFailure } from './errors.js'
|
|
21
17
|
import { XrpcResponse } from './response.js'
|
|
22
18
|
import { BinaryBodyInit, CallOptions } from './types.js'
|
|
23
19
|
import {
|
|
@@ -49,32 +45,6 @@ export type XrpcOptions<M extends Procedure | Query = Procedure | Query> =
|
|
|
49
45
|
XrpcInputOptions<XrpcRequestPayload<M>> &
|
|
50
46
|
XrpcParamsOptions<XrpcRequestParams<M>>
|
|
51
47
|
|
|
52
|
-
export type XrpcFailure<M extends Procedure | Query> =
|
|
53
|
-
// The server returned a valid XRPC error response
|
|
54
|
-
| XrpcResponseError<M>
|
|
55
|
-
// The response was not a valid XRPC response, or it does not match the schema
|
|
56
|
-
| XrpcUpstreamError
|
|
57
|
-
// Something went wrong (network error, etc.)
|
|
58
|
-
| XrpcUnexpectedError
|
|
59
|
-
|
|
60
|
-
export type XrpcResult<M extends Procedure | Query> =
|
|
61
|
-
| XrpcResponse<M>
|
|
62
|
-
| XrpcFailure<M>
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Utility method to type cast the error thrown by {@link xrpc} to an
|
|
66
|
-
* {@link XrpcFailure} matching the provided method. Only use this function
|
|
67
|
-
* inside a catch block right after calling {@link xrpc}, and use the same
|
|
68
|
-
* method type parameter as used in the {@link xrpc} call.
|
|
69
|
-
*/
|
|
70
|
-
export function asXrpcFailure<M extends Procedure | Query = Procedure | Query>(
|
|
71
|
-
err: unknown,
|
|
72
|
-
): XrpcFailure<M> {
|
|
73
|
-
if (err instanceof XrpcResponseError) return err
|
|
74
|
-
if (err instanceof XrpcUpstreamError) return err
|
|
75
|
-
return XrpcUnexpectedError.from(err)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
48
|
/**
|
|
79
49
|
* @throws XrpcFailure<M>
|
|
80
50
|
*/
|
|
@@ -94,13 +64,15 @@ export async function xrpc<const M extends Query | Procedure>(
|
|
|
94
64
|
ns: Main<M>,
|
|
95
65
|
options: XrpcOptions<M> = {} as XrpcOptions<M>,
|
|
96
66
|
): Promise<XrpcResponse<M>> {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
throw asXrpcFailure<M>(err)
|
|
101
|
-
}
|
|
67
|
+
const response = await xrpcSafe<M>(agent, ns, options)
|
|
68
|
+
if (response.success) return response
|
|
69
|
+
else throw response
|
|
102
70
|
}
|
|
103
71
|
|
|
72
|
+
export type XrpcResult<M extends Procedure | Query> =
|
|
73
|
+
| XrpcResponse<M>
|
|
74
|
+
| XrpcFailure<M>
|
|
75
|
+
|
|
104
76
|
export async function xrpcSafe<const M extends Query | Procedure>(
|
|
105
77
|
agent: Agent,
|
|
106
78
|
ns: NonNullable<unknown> extends XrpcOptions<M>
|
|
@@ -117,20 +89,16 @@ export async function xrpcSafe<const M extends Query | Procedure>(
|
|
|
117
89
|
ns: Main<M>,
|
|
118
90
|
options: XrpcOptions<M> = {} as XrpcOptions<M>,
|
|
119
91
|
): Promise<XrpcResult<M>> {
|
|
120
|
-
return lexRpcRequest<M>(agent, ns, options).catch(asXrpcFailure<M>)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function lexRpcRequest<const M extends Query | Procedure>(
|
|
124
|
-
agent: Agent,
|
|
125
|
-
ns: Main<M>,
|
|
126
|
-
options: XrpcOptions<M> = {} as XrpcOptions<M>,
|
|
127
|
-
): Promise<XrpcResponse<M>> {
|
|
128
|
-
const method = getMain(ns)
|
|
129
92
|
options.signal?.throwIfAborted()
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
93
|
+
const method: M = getMain(ns)
|
|
94
|
+
try {
|
|
95
|
+
const url = xrpcRequestUrl(method, options)
|
|
96
|
+
const request = xrpcRequestInit(method, options)
|
|
97
|
+
const response = await agent.fetchHandler(url, request)
|
|
98
|
+
return await XrpcResponse.fromFetchResponse<M>(method, response, options)
|
|
99
|
+
} catch (cause) {
|
|
100
|
+
return asXrpcFailure(method, cause)
|
|
101
|
+
}
|
|
134
102
|
}
|
|
135
103
|
|
|
136
104
|
function xrpcRequestUrl<M extends Procedure | Query | Subscription>(
|