@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
package/src/nodejs.ts
CHANGED
|
@@ -66,6 +66,8 @@ export function upgradeWebSocket(request: Request): {
|
|
|
66
66
|
return { response, socket }
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
const kUpgradeEvent = Symbol.for('@atproto/lex-server:upgrade')
|
|
70
|
+
|
|
69
71
|
function handleWebSocketUpgrade(
|
|
70
72
|
req: IncomingMessage,
|
|
71
73
|
response: Response,
|
|
@@ -98,6 +100,8 @@ function handleWebSocketUpgrade(
|
|
|
98
100
|
// @TODO find a way to properly "close" the _socket when the server is
|
|
99
101
|
// shutting down (might require replacing http-terminator with a local
|
|
100
102
|
// implementation)
|
|
103
|
+
|
|
104
|
+
req.emit(kUpgradeEvent, ws)
|
|
101
105
|
})
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -138,26 +142,10 @@ function toRequest(req: IncomingMessage): Request {
|
|
|
138
142
|
const url = new URL(req.url ?? '/', `${protocol}://${host}`)
|
|
139
143
|
const headers = toHeaders(req.headers)
|
|
140
144
|
const body = toBody(req)
|
|
141
|
-
|
|
142
|
-
const abortController = new AbortController()
|
|
143
|
-
const abort = (err?: Error) => abortController.abort(err)
|
|
144
|
-
|
|
145
|
-
req.on('close', abort)
|
|
146
|
-
req.on('error', abort)
|
|
147
|
-
req.on('end', abort)
|
|
148
|
-
|
|
149
|
-
abortController.signal.addEventListener(
|
|
150
|
-
'abort',
|
|
151
|
-
() => {
|
|
152
|
-
req.off('close', abort)
|
|
153
|
-
req.off('error', abort)
|
|
154
|
-
req.off('end', abort)
|
|
155
|
-
},
|
|
156
|
-
{ once: true },
|
|
157
|
-
)
|
|
145
|
+
const signal = requestSignal(req)
|
|
158
146
|
|
|
159
147
|
return new Request(url, {
|
|
160
|
-
signal
|
|
148
|
+
signal,
|
|
161
149
|
method: req.method,
|
|
162
150
|
headers,
|
|
163
151
|
body,
|
|
@@ -168,6 +156,58 @@ function toRequest(req: IncomingMessage): Request {
|
|
|
168
156
|
})
|
|
169
157
|
}
|
|
170
158
|
|
|
159
|
+
function requestSignal(req: IncomingMessage): AbortSignal {
|
|
160
|
+
if (req.destroyed) return AbortSignal.abort()
|
|
161
|
+
|
|
162
|
+
const abortController = new AbortController()
|
|
163
|
+
|
|
164
|
+
const abort = (err?: Error | WebSocket) => {
|
|
165
|
+
abortController.abort(err instanceof Error ? err : undefined)
|
|
166
|
+
|
|
167
|
+
req.off('close', abort)
|
|
168
|
+
req.off('error', abort)
|
|
169
|
+
req.off('end', abort)
|
|
170
|
+
req.off(kUpgradeEvent, abort)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
req.on('close', abort)
|
|
174
|
+
req.on('error', abort)
|
|
175
|
+
req.on('end', abort)
|
|
176
|
+
req.on(kUpgradeEvent, abort)
|
|
177
|
+
|
|
178
|
+
return abortController.signal
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function requestCompletion(req: IncomingMessage): Promise<void> {
|
|
182
|
+
if (req.destroyed) return Promise.resolve()
|
|
183
|
+
|
|
184
|
+
// Unlike the abort signal, we complete the promise only when the request
|
|
185
|
+
// is fully done, accounting for websocket upgrade.
|
|
186
|
+
return new Promise((resolve) => {
|
|
187
|
+
const cleanup = () => {
|
|
188
|
+
req.off('close', done)
|
|
189
|
+
req.off('error', done)
|
|
190
|
+
req.off('end', done)
|
|
191
|
+
req.off(kUpgradeEvent, onUpgrade)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const onUpgrade = (ws: WebSocket) => {
|
|
195
|
+
cleanup()
|
|
196
|
+
ws.addEventListener('close', () => resolve())
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const done = () => {
|
|
200
|
+
resolve()
|
|
201
|
+
cleanup()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
req.on('close', done)
|
|
205
|
+
req.on('error', done)
|
|
206
|
+
req.on('end', done)
|
|
207
|
+
req.on(kUpgradeEvent, onUpgrade)
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
171
211
|
function toHeaders(headers: IncomingHttpHeaders): Headers {
|
|
172
212
|
const result = new Headers()
|
|
173
213
|
for (const [key, value] of Object.entries(headers)) {
|
|
@@ -202,14 +242,14 @@ function toBody(req: IncomingMessage): null | ReadableStream<Uint8Array> {
|
|
|
202
242
|
}
|
|
203
243
|
|
|
204
244
|
export type NetAddr = {
|
|
245
|
+
transport: 'tcp'
|
|
205
246
|
hostname: string
|
|
206
247
|
port: number
|
|
207
|
-
transport: 'tcp'
|
|
208
248
|
}
|
|
209
249
|
|
|
210
250
|
export type NodeConnectionInfo = {
|
|
211
|
-
|
|
212
|
-
remoteAddr
|
|
251
|
+
completed: Promise<void>
|
|
252
|
+
remoteAddr: NetAddr | undefined
|
|
213
253
|
}
|
|
214
254
|
|
|
215
255
|
export interface HandlerFunction {
|
|
@@ -233,21 +273,15 @@ async function handleRequest(
|
|
|
233
273
|
|
|
234
274
|
function toConnectionInfo(req: IncomingMessage): NodeConnectionInfo {
|
|
235
275
|
const { socket } = req
|
|
276
|
+
|
|
236
277
|
return {
|
|
237
|
-
|
|
238
|
-
socket.localAddress != null
|
|
239
|
-
? {
|
|
240
|
-
hostname: socket.localAddress,
|
|
241
|
-
port: socket.localPort!,
|
|
242
|
-
transport: 'tcp',
|
|
243
|
-
}
|
|
244
|
-
: undefined,
|
|
278
|
+
completed: requestCompletion(req),
|
|
245
279
|
remoteAddr:
|
|
246
280
|
socket.remoteAddress != null
|
|
247
281
|
? {
|
|
282
|
+
transport: 'tcp',
|
|
248
283
|
hostname: socket.remoteAddress,
|
|
249
284
|
port: socket.remotePort!,
|
|
250
|
-
transport: 'tcp',
|
|
251
285
|
}
|
|
252
286
|
: undefined,
|
|
253
287
|
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import * as crypto from '@atproto/crypto'
|
|
2
|
+
import {
|
|
3
|
+
AtprotoDid,
|
|
4
|
+
AtprotoDidDocument,
|
|
5
|
+
Did,
|
|
6
|
+
matchesIdentifier,
|
|
7
|
+
} from '@atproto/did'
|
|
8
|
+
import { fromBase64, isPlainObject, utf8FromBase64 } from '@atproto/lex-data'
|
|
9
|
+
import { DidString, isDidString } from '@atproto/lex-schema'
|
|
10
|
+
import {
|
|
11
|
+
CreateDidResolverOptions,
|
|
12
|
+
createDidResolver,
|
|
13
|
+
} from '@atproto-labs/did-resolver'
|
|
14
|
+
import { LexServerAuthError } from './errors.js'
|
|
15
|
+
import { LexRouterAuth } from './lex-server.js'
|
|
16
|
+
|
|
17
|
+
const BEARER_PREFIX = 'Bearer '
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A function to check and record nonce uniqueness.
|
|
21
|
+
*/
|
|
22
|
+
export type UniqueNonceChecker = (nonce: string) => Promise<boolean>
|
|
23
|
+
|
|
24
|
+
export type ServiceAuthOptions = CreateDidResolverOptions & {
|
|
25
|
+
/**
|
|
26
|
+
* Expected audience ("aud") claim in the JWT token. Set to `null` to skip
|
|
27
|
+
* audience verification (not recommended).
|
|
28
|
+
*/
|
|
29
|
+
audience: null | DidString
|
|
30
|
+
/**
|
|
31
|
+
* Function to check and record nonce uniqueness. The value checked here must
|
|
32
|
+
* be unique within {@link ServiceAuthOptions.maxAge} seconds before and after
|
|
33
|
+
* the current time.
|
|
34
|
+
*
|
|
35
|
+
* @param nonce - The nonce to check.
|
|
36
|
+
*/
|
|
37
|
+
unique: UniqueNonceChecker
|
|
38
|
+
/**
|
|
39
|
+
* Maximum age of the JWT token in seconds.
|
|
40
|
+
*
|
|
41
|
+
* @default 300 (5 minutes)
|
|
42
|
+
*/
|
|
43
|
+
maxAge?: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ServiceAuthCredentials = {
|
|
47
|
+
did: AtprotoDid
|
|
48
|
+
didDocument: AtprotoDidDocument
|
|
49
|
+
jwt: ParsedJwt
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates an authentication handler for LexRouter that verifies AT protocol
|
|
54
|
+
* "service auth" JWT bearer tokens signed by decentralized identifiers (DIDs).
|
|
55
|
+
*/
|
|
56
|
+
export function serviceAuth({
|
|
57
|
+
audience,
|
|
58
|
+
maxAge = 5 * 60,
|
|
59
|
+
unique,
|
|
60
|
+
...options
|
|
61
|
+
}: ServiceAuthOptions): LexRouterAuth<ServiceAuthCredentials> {
|
|
62
|
+
const didResolver = createDidResolver(options)
|
|
63
|
+
|
|
64
|
+
return async ({ request, method }) => {
|
|
65
|
+
const { signal } = request
|
|
66
|
+
const jwt = await parseJwtBearer(request, {
|
|
67
|
+
lxm: method.nsid,
|
|
68
|
+
maxAge,
|
|
69
|
+
audience,
|
|
70
|
+
unique,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
let didDocument: AtprotoDidDocument = await didResolver
|
|
74
|
+
.resolve(jwt.payload.iss, { signal })
|
|
75
|
+
.catch((cause) => {
|
|
76
|
+
throw new LexServerAuthError(
|
|
77
|
+
'AuthenticationRequired',
|
|
78
|
+
'Could not resolve DID document',
|
|
79
|
+
{ Bearer: { error: 'DidResolutionFailed' } },
|
|
80
|
+
{ cause },
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const key = getAtprotoSigningKey(didDocument)
|
|
85
|
+
|
|
86
|
+
if (!key || !(await verifyJwt(jwt, key))) {
|
|
87
|
+
signal.throwIfAborted()
|
|
88
|
+
|
|
89
|
+
// Try refreshing the DID document in case it was updated
|
|
90
|
+
didDocument = await didResolver
|
|
91
|
+
.resolve(jwt.payload.iss, { signal, noCache: true })
|
|
92
|
+
.catch((cause) => {
|
|
93
|
+
throw new LexServerAuthError(
|
|
94
|
+
'AuthenticationRequired',
|
|
95
|
+
'Could not resolve DID document',
|
|
96
|
+
{ Bearer: { error: 'DidResolutionFailed' } },
|
|
97
|
+
{ cause },
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Verify again with the fresh key (if it changed)
|
|
102
|
+
const keyFresh = getAtprotoSigningKey(didDocument)
|
|
103
|
+
if (!keyFresh || key === keyFresh || !(await verifyJwt(jwt, keyFresh))) {
|
|
104
|
+
throw new LexServerAuthError(
|
|
105
|
+
'AuthenticationRequired',
|
|
106
|
+
'Invalid JWT signature',
|
|
107
|
+
{ Bearer: { error: 'BadJwtSignature' } },
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
did: didDocument.id,
|
|
114
|
+
didDocument,
|
|
115
|
+
jwt,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function verifyJwt(jwt: ParsedJwt, key: Did<'key'>) {
|
|
121
|
+
try {
|
|
122
|
+
return await crypto.verifySignature(key, jwt.message, jwt.signature, {
|
|
123
|
+
jwtAlg: jwt.header.alg,
|
|
124
|
+
allowMalleableSig: true,
|
|
125
|
+
})
|
|
126
|
+
} catch (cause) {
|
|
127
|
+
throw new LexServerAuthError(
|
|
128
|
+
'AuthenticationRequired',
|
|
129
|
+
'Could not verify JWT signature',
|
|
130
|
+
{ Bearer: { error: 'BadJwtSignature' } },
|
|
131
|
+
{ cause },
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getAtprotoSigningKey(
|
|
137
|
+
didDocument: AtprotoDidDocument,
|
|
138
|
+
): null | Did<'key'> {
|
|
139
|
+
try {
|
|
140
|
+
const key = didDocument.verificationMethod?.find(
|
|
141
|
+
isAtprotoVerificationMethod,
|
|
142
|
+
didDocument,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if (key?.publicKeyMultibase) {
|
|
146
|
+
if (key.type === 'EcdsaSecp256r1VerificationKey2019') {
|
|
147
|
+
const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)
|
|
148
|
+
return crypto.formatDidKey(crypto.P256_JWT_ALG, keyBytes)
|
|
149
|
+
} else if (key.type === 'EcdsaSecp256k1VerificationKey2019') {
|
|
150
|
+
const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)
|
|
151
|
+
return crypto.formatDidKey(crypto.SECP256K1_JWT_ALG, keyBytes)
|
|
152
|
+
} else if (key.type === 'Multikey') {
|
|
153
|
+
const parsed = crypto.parseMultikey(key.publicKeyMultibase)
|
|
154
|
+
return crypto.formatDidKey(parsed.jwtAlg, parsed.keyBytes)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Invalid key, ignore
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isAtprotoVerificationMethod<
|
|
165
|
+
V extends string | { id: string; type: string; publicKeyMultibase?: string },
|
|
166
|
+
>(
|
|
167
|
+
this: AtprotoDidDocument,
|
|
168
|
+
vm: V,
|
|
169
|
+
): vm is Exclude<V, string> & {
|
|
170
|
+
id: `${string}#atproto`
|
|
171
|
+
} {
|
|
172
|
+
return typeof vm === 'object' && matchesIdentifier(this.id, 'atproto', vm.id)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function parseJwtBearer(
|
|
176
|
+
request: Request,
|
|
177
|
+
options: ParseJwtOptions,
|
|
178
|
+
): Promise<ParsedJwt> {
|
|
179
|
+
const authorization = request.headers.get('authorization')
|
|
180
|
+
if (!authorization?.startsWith(BEARER_PREFIX)) {
|
|
181
|
+
throw new LexServerAuthError(
|
|
182
|
+
'AuthenticationRequired',
|
|
183
|
+
'Bearer token required',
|
|
184
|
+
{ Bearer: { error: 'MissingBearer' } },
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const token = authorization.slice(BEARER_PREFIX.length).trim()
|
|
189
|
+
|
|
190
|
+
return parseJwt(token, options)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export type ParseJwtOptions = {
|
|
194
|
+
maxAge: number
|
|
195
|
+
audience: null | DidString
|
|
196
|
+
unique: UniqueNonceChecker
|
|
197
|
+
lxm: string
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export type ParsedJwt = {
|
|
201
|
+
header: HeaderObject
|
|
202
|
+
payload: PayloadObject
|
|
203
|
+
message: Uint8Array
|
|
204
|
+
signature: Uint8Array
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function parseJwt(
|
|
208
|
+
token: string,
|
|
209
|
+
options: ParseJwtOptions,
|
|
210
|
+
): Promise<ParsedJwt> {
|
|
211
|
+
const {
|
|
212
|
+
length,
|
|
213
|
+
0: headerB64,
|
|
214
|
+
1: payloadB64,
|
|
215
|
+
2: signatureB64,
|
|
216
|
+
} = token.split('.')
|
|
217
|
+
if (length !== 3) {
|
|
218
|
+
throw new LexServerAuthError(
|
|
219
|
+
'AuthenticationRequired',
|
|
220
|
+
'Invalid JWT token',
|
|
221
|
+
{ Bearer: { error: 'BadJwt' } },
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let header: HeaderObject
|
|
226
|
+
try {
|
|
227
|
+
header = jsonFromBase64(headerB64, isHeaderObject)
|
|
228
|
+
} catch (cause) {
|
|
229
|
+
throw new LexServerAuthError(
|
|
230
|
+
'AuthenticationRequired',
|
|
231
|
+
'Invalid JWT token',
|
|
232
|
+
{ Bearer: { error: 'BadJwt' } },
|
|
233
|
+
{ cause },
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (
|
|
238
|
+
header.alg === 'none' ||
|
|
239
|
+
// service tokens are not OAuth 2.0 access tokens
|
|
240
|
+
// https://datatracker.ietf.org/doc/html/rfc9068
|
|
241
|
+
header.typ === 'at+jwt' ||
|
|
242
|
+
// "refresh+jwt" is a non-standard type used by the @atproto packages
|
|
243
|
+
header.typ === 'refresh+jwt' ||
|
|
244
|
+
// "DPoP" proofs are not meant to be used as service tokens
|
|
245
|
+
// https://datatracker.ietf.org/doc/html/rfc9449
|
|
246
|
+
header.typ === 'dpop+jwt'
|
|
247
|
+
) {
|
|
248
|
+
throw new LexServerAuthError(
|
|
249
|
+
'AuthenticationRequired',
|
|
250
|
+
'Invalid JWT token',
|
|
251
|
+
{ Bearer: { error: 'BadJwt' } },
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let payload: PayloadObject
|
|
256
|
+
try {
|
|
257
|
+
payload = jsonFromBase64(payloadB64, isPayloadObject)
|
|
258
|
+
} catch (cause) {
|
|
259
|
+
throw new LexServerAuthError(
|
|
260
|
+
'AuthenticationRequired',
|
|
261
|
+
'Invalid JWT token',
|
|
262
|
+
{ Bearer: { error: 'BadJwt' } },
|
|
263
|
+
{ cause },
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (options.audience !== null && options.audience !== payload.aud) {
|
|
268
|
+
throw new LexServerAuthError('AuthenticationRequired', 'Invalid audience', {
|
|
269
|
+
Bearer: { error: 'InvalidAudience' },
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const now = Math.floor(Date.now() / 1000)
|
|
274
|
+
|
|
275
|
+
if (payload.nbf != null && now < payload.nbf) {
|
|
276
|
+
throw new LexServerAuthError(
|
|
277
|
+
'AuthenticationRequired',
|
|
278
|
+
'JWT token not yet valid',
|
|
279
|
+
{ Bearer: { error: 'JwtNotYetValid' } },
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (now > payload.exp) {
|
|
284
|
+
throw new LexServerAuthError(
|
|
285
|
+
'AuthenticationRequired',
|
|
286
|
+
'JWT token expired',
|
|
287
|
+
{ Bearer: { error: 'JwtExpired' } },
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Prevent issuer from generating very long-lived tokens
|
|
292
|
+
if (
|
|
293
|
+
timeDiff(now, payload.exp) > options.maxAge ||
|
|
294
|
+
timeDiff(now, payload.iat) > options.maxAge
|
|
295
|
+
) {
|
|
296
|
+
throw new LexServerAuthError(
|
|
297
|
+
'AuthenticationRequired',
|
|
298
|
+
'JWT token too old',
|
|
299
|
+
{ Bearer: { error: 'JwtTooOld' } },
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (payload.lxm != null && typeof payload.lxm !== options.lxm) {
|
|
304
|
+
throw new LexServerAuthError(
|
|
305
|
+
'AuthenticationRequired',
|
|
306
|
+
'Invalid JWT lexicon method ("lxm")',
|
|
307
|
+
{ Bearer: { error: 'BadJwtLexiconMethod' } },
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (payload.nonce != null && !(await (0, options.unique)(payload.nonce))) {
|
|
312
|
+
throw new LexServerAuthError(
|
|
313
|
+
'AuthenticationRequired',
|
|
314
|
+
'Replay attack detected: nonce is not unique',
|
|
315
|
+
{ Bearer: { error: 'NonceNotUnique' } },
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
header,
|
|
321
|
+
payload,
|
|
322
|
+
message: textEncoder.encode(`${headerB64}.${payloadB64}`),
|
|
323
|
+
signature: fromBase64(signatureB64, 'base64url'),
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const textEncoder = /*#__PURE__*/ new TextEncoder()
|
|
328
|
+
|
|
329
|
+
type HeaderObject = { alg: string; typ?: string }
|
|
330
|
+
function isHeaderObject(obj: unknown): obj is HeaderObject {
|
|
331
|
+
return (
|
|
332
|
+
isPlainObject(obj) &&
|
|
333
|
+
typeof obj.alg === 'string' &&
|
|
334
|
+
(obj.typ === undefined || typeof obj.typ === 'string')
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
type PayloadObject = {
|
|
339
|
+
iss: DidString
|
|
340
|
+
aud: DidString
|
|
341
|
+
exp: number
|
|
342
|
+
iat?: number
|
|
343
|
+
nbf?: number
|
|
344
|
+
lxm?: string
|
|
345
|
+
nonce?: string
|
|
346
|
+
}
|
|
347
|
+
export function isPayloadObject(obj: unknown): obj is PayloadObject {
|
|
348
|
+
return (
|
|
349
|
+
isPlainObject(obj) &&
|
|
350
|
+
typeof obj.iss === 'string' &&
|
|
351
|
+
typeof obj.aud === 'string' &&
|
|
352
|
+
(obj.lxm === undefined || typeof obj.lxm === 'string') &&
|
|
353
|
+
(obj.nonce === undefined || typeof obj.nonce === 'string') &&
|
|
354
|
+
(obj.iat === undefined || isPositiveInt(obj.iat)) &&
|
|
355
|
+
(obj.nbf === undefined || isPositiveInt(obj.nbf)) &&
|
|
356
|
+
isPositiveInt(obj.exp) &&
|
|
357
|
+
isDidString(obj.iss) &&
|
|
358
|
+
isDidString(obj.aud)
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function timeDiff(t1: number, t2?: number): number {
|
|
363
|
+
if (t2 === undefined) return 0
|
|
364
|
+
return Math.abs(t1 - t2)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isPositiveInt(value: unknown): value is number {
|
|
368
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function jsonFromBase64<T>(b64: string, isType: (obj: unknown) => obj is T): T {
|
|
372
|
+
const obj = JSON.parse(utf8FromBase64(b64, 'base64url'))
|
|
373
|
+
if (isType(obj)) return obj
|
|
374
|
+
throw new Error('Invalid type')
|
|
375
|
+
}
|
package/dist/example.d.ts
DELETED
package/dist/example.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"example.d.ts","sourceRoot":"","sources":["../src/example.ts"],"names":[],"mappings":""}
|
package/dist/example.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/* eslint-disable @typescript-eslint/no-namespace */
|
|
3
|
-
/* eslint-disable n/no-extraneous-import */
|
|
4
|
-
/* eslint-disable import/no-unresolved */
|
|
5
|
-
/* eslint-env node */
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
const lex_1 = require("@atproto/lex");
|
|
8
|
-
const index_js_1 = require("./index.js");
|
|
9
|
-
const nodejs_js_1 = require("./nodejs.js");
|
|
10
|
-
var com;
|
|
11
|
-
(function (com) {
|
|
12
|
-
let example;
|
|
13
|
-
(function (example) {
|
|
14
|
-
let echo;
|
|
15
|
-
(function (echo) {
|
|
16
|
-
echo.nsid = 'com.example.echo';
|
|
17
|
-
echo.message = lex_1.l.typedObject(echo.nsid, 'message', lex_1.l.object({
|
|
18
|
-
message: lex_1.l.string(),
|
|
19
|
-
}));
|
|
20
|
-
echo.main = lex_1.l.subscription(echo.nsid, lex_1.l.params({
|
|
21
|
-
message: lex_1.l.string({ minLength: 1 }),
|
|
22
|
-
interval: lex_1.l.optional(lex_1.l.integer({ minimum: 0, default: 500 })),
|
|
23
|
-
}), lex_1.l.typedUnion([lex_1.l.typedRef(() => echo.message)], false));
|
|
24
|
-
})(echo = example.echo || (example.echo = {}));
|
|
25
|
-
})(example = com.example || (com.example = {}));
|
|
26
|
-
})(com || (com = {}));
|
|
27
|
-
const router = new index_js_1.LexRouter({ upgradeWebSocket: nodejs_js_1.upgradeWebSocket })
|
|
28
|
-
//
|
|
29
|
-
.add(com.example.echo, async function* ({ params: { interval, message } }) {
|
|
30
|
-
while (true) {
|
|
31
|
-
yield com.example.echo.message.$build({ message });
|
|
32
|
-
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
(0, nodejs_js_1.serve)(router, { port: 8080, host: '0.0.0.0' });
|
|
36
|
-
//# sourceMappingURL=example.js.map
|
package/dist/example.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"example.js","sourceRoot":"","sources":["../src/example.ts"],"names":[],"mappings":";AAAA,oDAAoD;AACpD,2CAA2C;AAC3C,yCAAyC;AACzC,qBAAqB;;AAErB,sCAAgC;AAChC,yCAAsC;AACtC,2CAAqD;AAErD,IAAU,GAAG,CAuBZ;AAvBD,WAAU,GAAG;IACX,IAAiB,OAAO,CAqBvB;IArBD,WAAiB,OAAO;QACtB,IAAiB,IAAI,CAmBpB;QAnBD,WAAiB,IAAI;YACN,SAAI,GAAG,kBAAkB,CAAA;YAEzB,YAAO,GAAG,OAAC,CAAC,WAAW,CAClC,KAAA,IAAI,EACJ,SAAS,EACT,OAAC,CAAC,MAAM,CAAC;gBACP,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE;aACpB,CAAC,CACH,CAAA;YAEY,SAAI,GAAG,OAAC,CAAC,YAAY,CAChC,KAAA,IAAI,EACJ,OAAC,CAAC,MAAM,CAAC;gBACP,OAAO,EAAE,OAAC,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;gBACnC,QAAQ,EAAE,OAAC,CAAC,QAAQ,CAAC,OAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;aAC9D,CAAC,EACF,OAAC,CAAC,UAAU,CAAC,CAAC,OAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAA,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,CACjD,CAAA;QACH,CAAC,EAnBgB,IAAI,GAAJ,YAAI,KAAJ,YAAI,QAmBpB;IACH,CAAC,EArBgB,OAAO,GAAP,WAAO,KAAP,WAAO,QAqBvB;AACH,CAAC,EAvBS,GAAG,KAAH,GAAG,QAuBZ;AAED,MAAM,MAAM,GAAG,IAAI,oBAAS,CAAC,EAAE,gBAAgB,EAAhB,4BAAgB,EAAE,CAAC;IAChD,EAAE;KACD,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;IACvE,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAA;QAClD,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;IAC/D,CAAC;AACH,CAAC,CAAC,CAAA;AAEJ,IAAA,iBAAK,EAAC,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAA","sourcesContent":["/* eslint-disable @typescript-eslint/no-namespace */\n/* eslint-disable n/no-extraneous-import */\n/* eslint-disable import/no-unresolved */\n/* eslint-env node */\n\nimport { l } from '@atproto/lex'\nimport { LexRouter } from './index.js'\nimport { serve, upgradeWebSocket } from './nodejs.js'\n\nnamespace com {\n export namespace example {\n export namespace echo {\n export const nsid = 'com.example.echo'\n\n export const message = l.typedObject(\n nsid,\n 'message',\n l.object({\n message: l.string(),\n }),\n )\n\n export const main = l.subscription(\n nsid,\n l.params({\n message: l.string({ minLength: 1 }),\n interval: l.optional(l.integer({ minimum: 0, default: 500 })),\n }),\n l.typedUnion([l.typedRef(() => message)], false),\n )\n }\n }\n}\n\nconst router = new LexRouter({ upgradeWebSocket })\n //\n .add(com.example.echo, async function* ({ params: { interval, message } }) {\n while (true) {\n yield com.example.echo.message.$build({ message })\n await new Promise((resolve) => setTimeout(resolve, interval))\n }\n })\n\nserve(router, { port: 8080, host: '0.0.0.0' })\n"]}
|
package/dist/lex-auth-error.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { LexError, LexErrorCode } from '@atproto/lex-data';
|
|
2
|
-
export type WWWAuthenticate = {
|
|
3
|
-
[k: string]: Record<string, string>;
|
|
4
|
-
};
|
|
5
|
-
export declare function formatWWWAuthenticate(wwwAuthenticate: WWWAuthenticate): string;
|
|
6
|
-
export declare class LexServerAuthError<N extends LexErrorCode = LexErrorCode> extends LexError<N> {
|
|
7
|
-
readonly wwwAuthenticate?: WWWAuthenticate | undefined;
|
|
8
|
-
name: string;
|
|
9
|
-
constructor(error: N, message: string, wwwAuthenticate?: WWWAuthenticate | undefined, options?: ErrorOptions);
|
|
10
|
-
get wwwAuthenticateHeader(): string;
|
|
11
|
-
toJSON(): import("@atproto/lex-data").LexErrorData<any>;
|
|
12
|
-
toResponse(): Response;
|
|
13
|
-
static from(cause: LexError, wwwAuthenticate?: WWWAuthenticate): LexServerAuthError;
|
|
14
|
-
}
|
|
15
|
-
//# sourceMappingURL=lex-auth-error.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lex-auth-error.d.ts","sourceRoot":"","sources":["../src/lex-auth-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAE1D,MAAM,MAAM,eAAe,GAAG;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAA;AACrE,wBAAgB,qBAAqB,CACnC,eAAe,EAAE,eAAe,GAC/B,MAAM,CAWR;AAED,qBAAa,kBAAkB,CAC7B,CAAC,SAAS,YAAY,GAAG,YAAY,CACrC,SAAQ,QAAQ,CAAC,CAAC,CAAC;IAMjB,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe;IAL5C,IAAI,SAAuB;gBAGzB,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,MAAM,EACN,eAAe,CAAC,EAAE,eAAe,YAAA,EAC1C,OAAO,CAAC,EAAE,YAAY;IAKxB,IAAI,qBAAqB,IAAI,MAAM,CAElC;IAED,MAAM;IAKN,UAAU,IAAI,QAAQ;IAatB,MAAM,CAAC,IAAI,CACT,KAAK,EAAE,QAAQ,EACf,eAAe,CAAC,EAAE,eAAe,GAChC,kBAAkB;CAMtB"}
|
package/dist/lex-auth-error.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LexServerAuthError = void 0;
|
|
4
|
-
exports.formatWWWAuthenticate = formatWWWAuthenticate;
|
|
5
|
-
const lex_data_1 = require("@atproto/lex-data");
|
|
6
|
-
function formatWWWAuthenticate(wwwAuthenticate) {
|
|
7
|
-
return Object.entries(wwwAuthenticate)
|
|
8
|
-
.map(([type, params]) => {
|
|
9
|
-
if (!params)
|
|
10
|
-
return null;
|
|
11
|
-
const paramsEnc = Object.entries(params)
|
|
12
|
-
.filter(([_, val]) => val != null)
|
|
13
|
-
.map(([name, val]) => `${name}=${JSON.stringify(val)}`);
|
|
14
|
-
return paramsEnc?.length ? `${type} ${paramsEnc.join(', ')}` : type;
|
|
15
|
-
})
|
|
16
|
-
.filter(Boolean)
|
|
17
|
-
.join(', ');
|
|
18
|
-
}
|
|
19
|
-
class LexServerAuthError extends lex_data_1.LexError {
|
|
20
|
-
wwwAuthenticate;
|
|
21
|
-
name = 'LexServerAuthError';
|
|
22
|
-
constructor(error, message, wwwAuthenticate, options) {
|
|
23
|
-
super(error, message, options);
|
|
24
|
-
this.wwwAuthenticate = wwwAuthenticate;
|
|
25
|
-
}
|
|
26
|
-
get wwwAuthenticateHeader() {
|
|
27
|
-
return formatWWWAuthenticate(this.wwwAuthenticate ?? {});
|
|
28
|
-
}
|
|
29
|
-
toJSON() {
|
|
30
|
-
const { cause } = this;
|
|
31
|
-
return cause instanceof lex_data_1.LexError ? cause.toJSON() : super.toJSON();
|
|
32
|
-
}
|
|
33
|
-
toResponse() {
|
|
34
|
-
const { wwwAuthenticateHeader } = this;
|
|
35
|
-
const headers = wwwAuthenticateHeader
|
|
36
|
-
? new Headers({
|
|
37
|
-
'WWW-Authenticate': wwwAuthenticateHeader,
|
|
38
|
-
'Access-Control-Expose-Headers': 'WWW-Authenticate', // CORS
|
|
39
|
-
})
|
|
40
|
-
: undefined;
|
|
41
|
-
return Response.json(this.toJSON(), { status: 401, headers });
|
|
42
|
-
}
|
|
43
|
-
static from(cause, wwwAuthenticate) {
|
|
44
|
-
if (cause instanceof LexServerAuthError)
|
|
45
|
-
return cause;
|
|
46
|
-
return new LexServerAuthError(cause.error, cause.message, wwwAuthenticate, {
|
|
47
|
-
cause,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
exports.LexServerAuthError = LexServerAuthError;
|
|
52
|
-
//# sourceMappingURL=lex-auth-error.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lex-auth-error.js","sourceRoot":"","sources":["../src/lex-auth-error.ts"],"names":[],"mappings":";;;AAGA,sDAaC;AAhBD,gDAA0D;AAG1D,SAAgB,qBAAqB,CACnC,eAAgC;IAEhC,OAAO,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC;SACnC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE;QACtB,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAA;QACxB,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;aACrC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,IAAI,CAAC;aACjC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACzD,OAAO,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IACrE,CAAC,CAAC;SACD,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED,MAAa,kBAEX,SAAQ,mBAAW;IAMR;IALX,IAAI,GAAG,oBAAoB,CAAA;IAE3B,YACE,KAAQ,EACR,OAAe,EACN,eAAiC,EAC1C,OAAsB;QAEtB,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;QAHrB,oBAAe,GAAf,eAAe,CAAkB;IAI5C,CAAC;IAED,IAAI,qBAAqB;QACvB,OAAO,qBAAqB,CAAC,IAAI,CAAC,eAAe,IAAI,EAAE,CAAC,CAAA;IAC1D,CAAC;IAED,MAAM;QACJ,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAA;QACtB,OAAO,KAAK,YAAY,mBAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAA;IACpE,CAAC;IAED,UAAU;QACR,MAAM,EAAE,qBAAqB,EAAE,GAAG,IAAI,CAAA;QAEtC,MAAM,OAAO,GAAG,qBAAqB;YACnC,CAAC,CAAC,IAAI,OAAO,CAAC;gBACV,kBAAkB,EAAE,qBAAqB;gBACzC,+BAA+B,EAAE,kBAAkB,EAAE,OAAO;aAC7D,CAAC;YACJ,CAAC,CAAC,SAAS,CAAA;QAEb,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAA;IAC/D,CAAC;IAED,MAAM,CAAC,IAAI,CACT,KAAe,EACf,eAAiC;QAEjC,IAAI,KAAK,YAAY,kBAAkB;YAAE,OAAO,KAAK,CAAA;QACrD,OAAO,IAAI,kBAAkB,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,eAAe,EAAE;YACzE,KAAK;SACN,CAAC,CAAA;IACJ,CAAC;CACF;AA7CD,gDA6CC","sourcesContent":["import { LexError, LexErrorCode } from '@atproto/lex-data'\n\nexport type WWWAuthenticate = { [k: string]: Record<string, string> }\nexport function formatWWWAuthenticate(\n wwwAuthenticate: WWWAuthenticate,\n): string {\n return Object.entries(wwwAuthenticate)\n .map(([type, params]) => {\n if (!params) return null\n const paramsEnc = Object.entries(params)\n .filter(([_, val]) => val != null)\n .map(([name, val]) => `${name}=${JSON.stringify(val)}`)\n return paramsEnc?.length ? `${type} ${paramsEnc.join(', ')}` : type\n })\n .filter(Boolean)\n .join(', ')\n}\n\nexport class LexServerAuthError<\n N extends LexErrorCode = LexErrorCode,\n> extends LexError<N> {\n name = 'LexServerAuthError'\n\n constructor(\n error: N,\n message: string,\n readonly wwwAuthenticate?: WWWAuthenticate,\n options?: ErrorOptions,\n ) {\n super(error, message, options)\n }\n\n get wwwAuthenticateHeader(): string {\n return formatWWWAuthenticate(this.wwwAuthenticate ?? {})\n }\n\n toJSON() {\n const { cause } = this\n return cause instanceof LexError ? cause.toJSON() : super.toJSON()\n }\n\n toResponse(): Response {\n const { wwwAuthenticateHeader } = this\n\n const headers = wwwAuthenticateHeader\n ? new Headers({\n 'WWW-Authenticate': wwwAuthenticateHeader,\n 'Access-Control-Expose-Headers': 'WWW-Authenticate', // CORS\n })\n : undefined\n\n return Response.json(this.toJSON(), { status: 401, headers })\n }\n\n static from(\n cause: LexError,\n wwwAuthenticate?: WWWAuthenticate,\n ): LexServerAuthError {\n if (cause instanceof LexServerAuthError) return cause\n return new LexServerAuthError(cause.error, cause.message, wwwAuthenticate, {\n cause,\n })\n }\n}\n"]}
|
package/dist/subscripotion.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"subscripotion.d.ts","sourceRoot":"","sources":["../src/subscripotion.ts"],"names":[],"mappings":""}
|