@atproto/lex-server 0.1.3 → 0.1.5
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 +29 -0
- package/dist/errors.d.ts +3 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/lex-router.d.ts +2 -2
- package/dist/lex-router.d.ts.map +1 -1
- package/dist/lex-router.js +2 -2
- package/dist/lex-router.js.map +1 -1
- package/dist/nodejs.d.ts +3 -2
- package/dist/nodejs.d.ts.map +1 -1
- package/dist/nodejs.js +1 -1
- package/dist/nodejs.js.map +1 -1
- package/dist/service-auth.d.ts +3 -3
- package/dist/service-auth.d.ts.map +1 -1
- package/dist/service-auth.js +2 -2
- package/dist/service-auth.js.map +1 -1
- package/package.json +15 -20
- package/nodejs.cjs +0 -5
- package/src/errors.test.ts +0 -262
- package/src/errors.ts +0 -173
- package/src/index.ts +0 -3
- package/src/lex-router.test.ts +0 -2189
- package/src/lex-router.ts +0 -1219
- package/src/lib/drain-websocket.ts +0 -34
- package/src/lib/sleep.ts +0 -25
- package/src/lib/www-authenticate.test.ts +0 -134
- package/src/lib/www-authenticate.ts +0 -111
- package/src/nodejs.test.ts +0 -107
- package/src/nodejs.ts +0 -678
- package/src/service-auth.test.ts +0 -87
- package/src/service-auth.ts +0 -517
- package/tsconfig.build.json +0 -12
- package/tsconfig.json +0 -8
- package/tsconfig.tests.json +0 -8
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { abortableSleep } from './sleep.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Performs polling based backpressure management for a WebSocket connection. If
|
|
5
|
-
* the amount of buffered data exceeds the specified high water mark, this
|
|
6
|
-
* function will wait until the buffered amount drops below the low water mark
|
|
7
|
-
* before resolving. This is useful for preventing memory issues when sending
|
|
8
|
-
* large amounts of data over a WebSocket connection.
|
|
9
|
-
*/
|
|
10
|
-
export async function drainWebsocket(
|
|
11
|
-
socket: WebSocket,
|
|
12
|
-
signal: AbortSignal,
|
|
13
|
-
{
|
|
14
|
-
highWaterMark = 250_000, // 250 KB
|
|
15
|
-
lowWaterMark = 50_000, // 50 KB
|
|
16
|
-
}: {
|
|
17
|
-
highWaterMark?: number
|
|
18
|
-
lowWaterMark?: number
|
|
19
|
-
} = {},
|
|
20
|
-
): Promise<void> {
|
|
21
|
-
if (socket.bufferedAmount > highWaterMark) {
|
|
22
|
-
// Once we exceed the high water mark, we wait until the buffered amount
|
|
23
|
-
// drops below the low water mark before allowing more data to be sent. This
|
|
24
|
-
// creates a hysteresis effect that prevents rapid toggling around the
|
|
25
|
-
// threshold.
|
|
26
|
-
while (
|
|
27
|
-
socket.readyState === 1 &&
|
|
28
|
-
socket.bufferedAmount !== 0 &&
|
|
29
|
-
socket.bufferedAmount > lowWaterMark
|
|
30
|
-
) {
|
|
31
|
-
await abortableSleep(10, signal)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
package/src/lib/sleep.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export async function abortableSleep(
|
|
2
|
-
ms: number,
|
|
3
|
-
signal: AbortSignal,
|
|
4
|
-
): Promise<void> {
|
|
5
|
-
signal.throwIfAborted()
|
|
6
|
-
|
|
7
|
-
return new Promise((resolve, reject) => {
|
|
8
|
-
const cleanup = () => {
|
|
9
|
-
signal.removeEventListener('abort', onAbort)
|
|
10
|
-
clearTimeout(timeoutHandle)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const timeoutHandle = setTimeout(() => {
|
|
14
|
-
cleanup()
|
|
15
|
-
resolve()
|
|
16
|
-
}, ms)
|
|
17
|
-
|
|
18
|
-
const onAbort = () => {
|
|
19
|
-
cleanup()
|
|
20
|
-
reject(signal.reason)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
signal.addEventListener('abort', onAbort)
|
|
24
|
-
})
|
|
25
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { formatWWWAuthenticateHeader } from './www-authenticate.js'
|
|
3
|
-
|
|
4
|
-
describe(formatWWWAuthenticateHeader, () => {
|
|
5
|
-
describe('single scheme with params object', () => {
|
|
6
|
-
it('formats a Bearer challenge with params', () => {
|
|
7
|
-
const result = formatWWWAuthenticateHeader({
|
|
8
|
-
Bearer: { realm: 'api.example.com', error: 'InvalidToken' },
|
|
9
|
-
})
|
|
10
|
-
expect(result).toBe(
|
|
11
|
-
'Bearer realm="api.example.com", error="InvalidToken"',
|
|
12
|
-
)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('omits undefined param values', () => {
|
|
16
|
-
const result = formatWWWAuthenticateHeader({
|
|
17
|
-
Bearer: { realm: 'api', error: undefined },
|
|
18
|
-
})
|
|
19
|
-
expect(result).toBe('Bearer realm="api"')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('omits null param values', () => {
|
|
23
|
-
const result = formatWWWAuthenticateHeader({
|
|
24
|
-
Bearer: { realm: 'api', error: null as any },
|
|
25
|
-
})
|
|
26
|
-
expect(result).toBe('Bearer realm="api"')
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('outputs only the scheme when all params are undefined', () => {
|
|
30
|
-
const result = formatWWWAuthenticateHeader({
|
|
31
|
-
Bearer: {},
|
|
32
|
-
})
|
|
33
|
-
expect(result).toBe('Bearer')
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
describe('single scheme with token68 string', () => {
|
|
38
|
-
it('formats a token68 value', () => {
|
|
39
|
-
const result = formatWWWAuthenticateHeader({
|
|
40
|
-
Bearer: 'base64encodedvalue==',
|
|
41
|
-
})
|
|
42
|
-
expect(result).toBe('Bearer base64encodedvalue==')
|
|
43
|
-
})
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
describe('multiple schemes', () => {
|
|
47
|
-
it('joins multiple different schemes with a comma', () => {
|
|
48
|
-
const result = formatWWWAuthenticateHeader({
|
|
49
|
-
Bearer: { realm: 'api' },
|
|
50
|
-
Basic: { realm: 'api' },
|
|
51
|
-
})
|
|
52
|
-
expect(result).toBe('Bearer realm="api", Basic realm="api"')
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
describe('array of challenges for the same scheme (new feature)', () => {
|
|
57
|
-
it('emits one challenge per array element', () => {
|
|
58
|
-
const result = formatWWWAuthenticateHeader({
|
|
59
|
-
Bearer: [
|
|
60
|
-
{ realm: 'first', error: 'TokenExpired' },
|
|
61
|
-
{ realm: 'second', error: 'TokenRevoked' },
|
|
62
|
-
],
|
|
63
|
-
})
|
|
64
|
-
expect(result).toBe(
|
|
65
|
-
'Bearer realm="first", error="TokenExpired", Bearer realm="second", error="TokenRevoked"',
|
|
66
|
-
)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('handles an array mixing token68 strings and param objects', () => {
|
|
70
|
-
const result = formatWWWAuthenticateHeader({
|
|
71
|
-
Bearer: ['token68value', { realm: 'api', error: 'BadToken' }],
|
|
72
|
-
})
|
|
73
|
-
expect(result).toBe(
|
|
74
|
-
'Bearer token68value, Bearer realm="api", error="BadToken"',
|
|
75
|
-
)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('handles an array of token68 strings', () => {
|
|
79
|
-
const result = formatWWWAuthenticateHeader({
|
|
80
|
-
Bearer: ['firstToken', 'secondToken'],
|
|
81
|
-
})
|
|
82
|
-
expect(result).toBe('Bearer firstToken, Bearer secondToken')
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('handles an array with a single element', () => {
|
|
86
|
-
const result = formatWWWAuthenticateHeader({
|
|
87
|
-
Bearer: [{ realm: 'api' }],
|
|
88
|
-
})
|
|
89
|
-
expect(result).toBe('Bearer realm="api"')
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('handles array challenges alongside other schemes', () => {
|
|
93
|
-
const result = formatWWWAuthenticateHeader({
|
|
94
|
-
Bearer: [
|
|
95
|
-
{ realm: 'r1', error: 'Err1' },
|
|
96
|
-
{ realm: 'r2', error: 'Err2' },
|
|
97
|
-
],
|
|
98
|
-
Basic: { realm: 'fallback' },
|
|
99
|
-
})
|
|
100
|
-
expect(result).toBe(
|
|
101
|
-
'Bearer realm="r1", error="Err1", Bearer realm="r2", error="Err2", Basic realm="fallback"',
|
|
102
|
-
)
|
|
103
|
-
})
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
describe('null / undefined scheme values', () => {
|
|
107
|
-
it('skips schemes with null value', () => {
|
|
108
|
-
const result = formatWWWAuthenticateHeader({
|
|
109
|
-
Bearer: null as any,
|
|
110
|
-
Basic: { realm: 'api' },
|
|
111
|
-
})
|
|
112
|
-
expect(result).toBe('Basic realm="api"')
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('skips schemes with undefined value', () => {
|
|
116
|
-
const result = formatWWWAuthenticateHeader({
|
|
117
|
-
Bearer: undefined,
|
|
118
|
-
Basic: { realm: 'api' },
|
|
119
|
-
})
|
|
120
|
-
expect(result).toBe('Basic realm="api"')
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('returns an empty string when all schemes are null/undefined', () => {
|
|
124
|
-
const result = formatWWWAuthenticateHeader({
|
|
125
|
-
Bearer: undefined,
|
|
126
|
-
})
|
|
127
|
-
expect(result).toBe('')
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
it('returns an empty string for an empty object', () => {
|
|
132
|
-
expect(formatWWWAuthenticateHeader({})).toBe('')
|
|
133
|
-
})
|
|
134
|
-
})
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Type representing the value of a WWW-Authenticate HTTP header.
|
|
3
|
-
*
|
|
4
|
-
* Supports multiple authentication schemes, each with optional parameters.
|
|
5
|
-
* Parameters can be provided as a token68 string (for schemes like Bearer)
|
|
6
|
-
* or as key-value pairs.
|
|
7
|
-
*
|
|
8
|
-
* @see {@link https://datatracker.ietf.org/doc/html/rfc7235#section-4.1 | RFC 7235 Section 4.1}
|
|
9
|
-
*
|
|
10
|
-
* @example Bearer scheme with parameters
|
|
11
|
-
* ```typescript
|
|
12
|
-
* const auth: WWWAuthenticate = {
|
|
13
|
-
* Bearer: {
|
|
14
|
-
* realm: 'api.example.com',
|
|
15
|
-
* error: 'InvalidToken',
|
|
16
|
-
* error_description: 'The token has expired'
|
|
17
|
-
* }
|
|
18
|
-
* }
|
|
19
|
-
* // Formats to: Bearer realm="api.example.com", error="InvalidToken", error_description="The token has expired"
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* @example Multiple schemes
|
|
23
|
-
* ```typescript
|
|
24
|
-
* const auth: WWWAuthenticate = {
|
|
25
|
-
* Bearer: { realm: 'api' },
|
|
26
|
-
* Basic: { realm: 'api' }
|
|
27
|
-
* }
|
|
28
|
-
* // Formats to: Bearer realm="api", Basic realm="api"
|
|
29
|
-
* ```
|
|
30
|
-
*
|
|
31
|
-
* @example Token68 value (no parameters)
|
|
32
|
-
* ```typescript
|
|
33
|
-
* const auth: WWWAuthenticate = {
|
|
34
|
-
* Bearer: 'base64encodedvalue=='
|
|
35
|
-
* }
|
|
36
|
-
* // Formats to: Bearer base64encodedvalue==
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
39
|
-
export type WWWAuthenticate = {
|
|
40
|
-
[authScheme in string]?:
|
|
41
|
-
| string // token68
|
|
42
|
-
| WWWAuthenticateParams
|
|
43
|
-
| (string | WWWAuthenticateParams)[]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export type WWWAuthenticateParams = { [authParam in string]?: string }
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Formats a WWWAuthenticate object into an HTTP header string.
|
|
50
|
-
*
|
|
51
|
-
* Converts the structured authentication scheme and parameter data into
|
|
52
|
-
* the proper WWW-Authenticate header format per RFC 7235.
|
|
53
|
-
*
|
|
54
|
-
* @param wwwAuthenticate - The authentication schemes and parameters
|
|
55
|
-
* @returns Formatted header string ready for use in HTTP responses
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* ```typescript
|
|
59
|
-
* const header = formatWWWAuthenticateHeader({
|
|
60
|
-
* Bearer: {
|
|
61
|
-
* realm: 'api.example.com',
|
|
62
|
-
* error: 'MissingToken'
|
|
63
|
-
* }
|
|
64
|
-
* })
|
|
65
|
-
* // Returns: 'Bearer realm="api.example.com", error="MissingToken"'
|
|
66
|
-
* ```
|
|
67
|
-
*
|
|
68
|
-
* @example Empty or undefined values
|
|
69
|
-
* ```typescript
|
|
70
|
-
* const header = formatWWWAuthenticateHeader({
|
|
71
|
-
* Bearer: { realm: 'api', error: undefined }
|
|
72
|
-
* })
|
|
73
|
-
* // Returns: 'Bearer realm="api"' (undefined values are omitted)
|
|
74
|
-
* ```
|
|
75
|
-
*/
|
|
76
|
-
export function formatWWWAuthenticateHeader(
|
|
77
|
-
wwwAuthenticate: WWWAuthenticate,
|
|
78
|
-
): string {
|
|
79
|
-
const challenges: string[] = []
|
|
80
|
-
for (const [scheme, params] of Object.entries(wwwAuthenticate)) {
|
|
81
|
-
if (params == null) continue
|
|
82
|
-
|
|
83
|
-
if (typeof params === 'string') {
|
|
84
|
-
challenges.push(formatWWWAuthenticateChallenge(scheme, params))
|
|
85
|
-
} else if (Array.isArray(params)) {
|
|
86
|
-
for (const p of params) {
|
|
87
|
-
challenges.push(formatWWWAuthenticateChallenge(scheme, p))
|
|
88
|
-
}
|
|
89
|
-
} else {
|
|
90
|
-
challenges.push(formatWWWAuthenticateChallenge(scheme, params))
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return challenges.join(', ')
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function formatWWWAuthenticateChallenge(
|
|
97
|
-
scheme: string,
|
|
98
|
-
params: string | WWWAuthenticateParams,
|
|
99
|
-
): string {
|
|
100
|
-
const paramsStr =
|
|
101
|
-
typeof params === 'string' ? params : formatWWWAuthenticateParams(params)
|
|
102
|
-
return paramsStr?.length ? `${scheme} ${paramsStr}` : scheme
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function formatWWWAuthenticateParams(params: WWWAuthenticateParams): string {
|
|
106
|
-
const parts: string[] = []
|
|
107
|
-
for (const [name, val] of Object.entries(params)) {
|
|
108
|
-
if (val != null) parts.push(`${name}=${JSON.stringify(val)}`)
|
|
109
|
-
}
|
|
110
|
-
return parts.join(', ')
|
|
111
|
-
}
|
package/src/nodejs.test.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { AddressInfo } from 'node:net'
|
|
2
|
-
import { scheduler } from 'node:timers/promises'
|
|
3
|
-
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
4
|
-
import { Server, serve } from './nodejs.js'
|
|
5
|
-
|
|
6
|
-
describe('Node.js RequestListener', () => {
|
|
7
|
-
let server: Server
|
|
8
|
-
let address: string
|
|
9
|
-
|
|
10
|
-
beforeAll(async () => {
|
|
11
|
-
server = await serve(async (request) => {
|
|
12
|
-
const { pathname } = new URL(request.url)
|
|
13
|
-
if (pathname === '/hello') {
|
|
14
|
-
return new Response('Hello, world!', {
|
|
15
|
-
status: 200,
|
|
16
|
-
headers: { 'Content-Type': 'text/plain' },
|
|
17
|
-
})
|
|
18
|
-
} else if (pathname === '/throw') {
|
|
19
|
-
throw new Error('Test error')
|
|
20
|
-
} else if (pathname === '/echo') {
|
|
21
|
-
return new Response(request.body, {
|
|
22
|
-
status: 200,
|
|
23
|
-
headers: { 'Content-Type': 'application/octet-stream' },
|
|
24
|
-
})
|
|
25
|
-
}
|
|
26
|
-
return new Response('Not Found', { status: 404 })
|
|
27
|
-
})
|
|
28
|
-
const { port } = server.address() as AddressInfo
|
|
29
|
-
address = `http://localhost:${port}`
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
afterAll(async () => {
|
|
33
|
-
await server.terminate()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('should respond with Hello, world! on /hello', async () => {
|
|
37
|
-
const res = await fetch(new URL(`/hello`, address))
|
|
38
|
-
const text = await res.text()
|
|
39
|
-
expect(res.status).toBe(200)
|
|
40
|
-
expect(text).toBe('Hello, world!')
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('should respond with Not Found on unknown path', async () => {
|
|
44
|
-
const res = await fetch(new URL(`/unknown`, address))
|
|
45
|
-
const text = await res.text()
|
|
46
|
-
expect(res.status).toBe(404)
|
|
47
|
-
expect(text).toBe('Not Found')
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('should handle thrown errors and respond with 500', async () => {
|
|
51
|
-
const res = await fetch(new URL(`/throw`, address))
|
|
52
|
-
const text = await res.text()
|
|
53
|
-
expect(res.status).toBe(500)
|
|
54
|
-
expect(text).toBe('Internal Server Error')
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('should handle streaming bodies', async () => {
|
|
58
|
-
const totalSize = 1024 * 1024
|
|
59
|
-
const consumerSize = 42 * 1024
|
|
60
|
-
|
|
61
|
-
let sentBytes = 0
|
|
62
|
-
let receivedBytes = 0
|
|
63
|
-
|
|
64
|
-
const res = await fetch(new URL(`/echo`, address), {
|
|
65
|
-
method: 'POST',
|
|
66
|
-
// @ts-expect-error
|
|
67
|
-
duplex: 'half',
|
|
68
|
-
body: new ReadableStream({
|
|
69
|
-
async pull(controller) {
|
|
70
|
-
const chunkSize = Math.min(1024, totalSize - sentBytes)
|
|
71
|
-
controller.enqueue('A'.repeat(chunkSize))
|
|
72
|
-
sentBytes += chunkSize
|
|
73
|
-
await scheduler.wait(0) // Yield to event loop
|
|
74
|
-
if (sentBytes === totalSize) controller.close()
|
|
75
|
-
},
|
|
76
|
-
}),
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
const reader = res.body!.getReader()
|
|
80
|
-
|
|
81
|
-
// eslint-disable-next-line no-constant-condition
|
|
82
|
-
while (true) {
|
|
83
|
-
const result = await reader.read()
|
|
84
|
-
if (result.done) break
|
|
85
|
-
receivedBytes += Buffer.byteLength(result.value)
|
|
86
|
-
if (receivedBytes >= consumerSize) {
|
|
87
|
-
await reader.cancel()
|
|
88
|
-
break
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
expect(receivedBytes).toBeGreaterThanOrEqual(consumerSize)
|
|
93
|
-
expect(sentBytes).toBeGreaterThanOrEqual(consumerSize)
|
|
94
|
-
expect(sentBytes).toBeLessThan(totalSize)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('should echo back request body on /echo', async () => {
|
|
98
|
-
const body = `Echo this back`
|
|
99
|
-
const res = await fetch(new URL(`/echo`, address), {
|
|
100
|
-
method: 'POST',
|
|
101
|
-
body,
|
|
102
|
-
})
|
|
103
|
-
const text = await res.text()
|
|
104
|
-
expect(res.status).toBe(200)
|
|
105
|
-
expect(text).toBe(body)
|
|
106
|
-
})
|
|
107
|
-
})
|