@atproto/xrpc-server 0.4.2 → 0.4.4-next.0

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE.txt +1 -1
  3. package/dist/auth.d.ts +3 -2
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +124 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +25 -53377
  10. package/dist/index.js.map +1 -7
  11. package/dist/logger.d.ts +1 -0
  12. package/dist/logger.d.ts.map +1 -0
  13. package/dist/logger.js +7 -0
  14. package/dist/logger.js.map +1 -0
  15. package/dist/rate-limiter.d.ts +2 -1
  16. package/dist/rate-limiter.d.ts.map +1 -0
  17. package/dist/rate-limiter.js +166 -0
  18. package/dist/rate-limiter.js.map +1 -0
  19. package/dist/server.d.ts +6 -5
  20. package/dist/server.d.ts.map +1 -0
  21. package/dist/server.js +472 -0
  22. package/dist/server.js.map +1 -0
  23. package/dist/stream/frames.d.ts +2 -1
  24. package/dist/stream/frames.d.ts.map +1 -0
  25. package/dist/stream/frames.js +141 -0
  26. package/dist/stream/frames.js.map +1 -0
  27. package/dist/stream/index.d.ts +1 -0
  28. package/dist/stream/index.d.ts.map +1 -0
  29. package/dist/stream/index.js +22 -0
  30. package/dist/stream/index.js.map +1 -0
  31. package/dist/stream/logger.d.ts +1 -0
  32. package/dist/stream/logger.d.ts.map +1 -0
  33. package/dist/stream/logger.js +7 -0
  34. package/dist/stream/logger.js.map +1 -0
  35. package/dist/stream/server.d.ts +3 -1
  36. package/dist/stream/server.d.ts.map +1 -0
  37. package/dist/stream/server.js +70 -0
  38. package/dist/stream/server.js.map +1 -0
  39. package/dist/stream/stream.d.ts +1 -0
  40. package/dist/stream/stream.d.ts.map +1 -0
  41. package/dist/stream/stream.js +44 -0
  42. package/dist/stream/stream.js.map +1 -0
  43. package/dist/stream/subscription.d.ts +2 -0
  44. package/dist/stream/subscription.d.ts.map +1 -0
  45. package/dist/stream/subscription.js +80 -0
  46. package/dist/stream/subscription.js.map +1 -0
  47. package/dist/stream/types.d.ts +5 -4
  48. package/dist/stream/types.d.ts.map +1 -0
  49. package/dist/stream/types.js +47 -0
  50. package/dist/stream/types.js.map +1 -0
  51. package/dist/stream/websocket-keepalive.d.ts +2 -0
  52. package/dist/stream/websocket-keepalive.d.ts.map +1 -0
  53. package/dist/stream/websocket-keepalive.js +160 -0
  54. package/dist/stream/websocket-keepalive.js.map +1 -0
  55. package/dist/types.d.ts +54 -34
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +163 -0
  58. package/dist/types.js.map +1 -0
  59. package/dist/util.d.ts +3 -2
  60. package/dist/util.d.ts.map +1 -0
  61. package/dist/util.js +263 -0
  62. package/dist/util.js.map +1 -0
  63. package/jest.config.js +4 -3
  64. package/package.json +10 -11
  65. package/src/rate-limiter.ts +3 -0
  66. package/src/server.ts +53 -14
  67. package/src/stream/frames.ts +1 -1
  68. package/src/stream/websocket-keepalive.ts +2 -1
  69. package/src/types.ts +22 -10
  70. package/src/util.ts +3 -3
  71. package/tests/bodies.test.ts +4 -4
  72. package/tests/errors.test.ts +1 -1
  73. package/tsconfig.build.json +6 -2
  74. package/tsconfig.json +3 -11
  75. package/tsconfig.tests.json +7 -0
  76. package/babel.config.js +0 -1
  77. package/build.js +0 -14
package/jest.config.js CHANGED
@@ -1,6 +1,7 @@
1
- const base = require('../../jest.config.base.js')
2
-
1
+ /** @type {import('jest').Config} */
3
2
  module.exports = {
4
- ...base,
5
3
  displayName: 'XRPC Server',
4
+ transform: { '^.+\\.(t|j)s$': '@swc/jest' },
5
+ transformIgnorePatterns: [`<rootDir>/node_modules/(?!get-port)`],
6
+ setupFiles: ['<rootDir>/../../jest.setup.ts'],
6
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/xrpc-server",
3
- "version": "0.4.2",
3
+ "version": "0.4.4-next.0",
4
4
  "license": "MIT",
5
5
  "description": "atproto HTTP API (XRPC) server library",
6
6
  "keywords": [
@@ -14,6 +14,7 @@
14
14
  "directory": "packages/xrpc-server"
15
15
  },
16
16
  "main": "dist/index.js",
17
+ "types": "dist/index.d.ts",
17
18
  "dependencies": {
18
19
  "cbor-x": "^1.5.1",
19
20
  "express": "^4.17.2",
@@ -23,9 +24,9 @@
23
24
  "uint8arrays": "3.0.0",
24
25
  "ws": "^8.12.0",
25
26
  "zod": "^3.21.4",
26
- "@atproto/common": "^0.3.3",
27
- "@atproto/crypto": "^0.3.0",
28
- "@atproto/lexicon": "^0.3.1"
27
+ "@atproto/common": "^0.3.4-next.0",
28
+ "@atproto/lexicon": "^0.3.3-next.0",
29
+ "@atproto/crypto": "^0.3.1-next.0"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@types/express": "^4.17.13",
@@ -34,16 +35,14 @@
34
35
  "@types/ws": "^8.5.4",
35
36
  "get-port": "^6.1.2",
36
37
  "jose": "^4.15.4",
38
+ "jest": "^28.1.2",
37
39
  "key-encoder": "^2.0.3",
38
40
  "multiformats": "^9.9.0",
39
- "@atproto/crypto": "^0.3.0",
40
- "@atproto/xrpc": "^0.4.1"
41
+ "@atproto/crypto": "^0.3.1-next.0",
42
+ "@atproto/xrpc": "^0.4.3-next.0"
41
43
  },
42
44
  "scripts": {
43
45
  "test": "jest",
44
- "build": "node ./build.js",
45
- "postbuild": "tsc --build tsconfig.build.json",
46
- "update-main-to-dist": "node ../../update-main-to-dist.js packages/xrpc-server"
47
- },
48
- "types": "dist/index.d.ts"
46
+ "build": "tsc --build tsconfig.build.json"
47
+ }
49
48
  }
@@ -75,6 +75,9 @@ export class RateLimiter implements RateLimiterI {
75
75
  return null
76
76
  }
77
77
  const key = opts?.calcKey ? opts.calcKey(ctx) : this.calcKey(ctx)
78
+ if (key === null) {
79
+ return null
80
+ }
78
81
  const points = opts?.calcPoints
79
82
  ? opts.calcPoints(ctx)
80
83
  : this.calcPoints(ctx)
package/src/server.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { Readable } from 'stream'
2
2
  import express, {
3
+ Application,
4
+ Express,
5
+ Router,
6
+ Request,
7
+ Response,
3
8
  ErrorRequestHandler,
4
9
  NextFunction,
5
10
  RequestHandler,
@@ -36,6 +41,7 @@ import {
36
41
  RateLimiterConsume,
37
42
  isShared,
38
43
  RateLimitExceededError,
44
+ HandlerPipeThrough,
39
45
  } from './types'
40
46
  import {
41
47
  decodeQueryParams,
@@ -51,8 +57,8 @@ export function createServer(lexicons?: LexiconDoc[], options?: Options) {
51
57
  }
52
58
 
53
59
  export class Server {
54
- router = express()
55
- routes = express.Router()
60
+ router: Express = express()
61
+ routes: Router = express.Router()
56
62
  subscriptions = new Map<string, XrpcStreamServer>()
57
63
  lex = new Lexicons()
58
64
  options: Options
@@ -68,7 +74,7 @@ export class Server {
68
74
  this.router.use(this.routes)
69
75
  this.router.use('/xrpc/:methodId', this.catchall.bind(this))
70
76
  this.router.use(errorMiddleware)
71
- this.router.once('mount', (app: express.Application) => {
77
+ this.router.once('mount', (app: Application) => {
72
78
  this.enableStreamingOnListen(app)
73
79
  })
74
80
  this.options = opts ?? {}
@@ -173,15 +179,11 @@ export class Server {
173
179
  this.routes[verb](
174
180
  `/xrpc/${nsid}`,
175
181
  ...middleware,
176
- this.createHandler(nsid, def, config.handler),
182
+ this.createHandler(nsid, def, config),
177
183
  )
178
184
  }
179
185
 
180
- async catchall(
181
- req: express.Request,
182
- _res: express.Response,
183
- next: NextFunction,
184
- ) {
186
+ async catchall(req: Request, _res: Response, next: NextFunction) {
185
187
  const def = this.lex.getDef(req.params.methodId)
186
188
  if (!def) {
187
189
  return next(new MethodNotImplementedError())
@@ -206,10 +208,13 @@ export class Server {
206
208
  createHandler(
207
209
  nsid: string,
208
210
  def: LexXrpcQuery | LexXrpcProcedure,
209
- handler: XRPCHandler,
211
+ routeCfg: XRPCHandlerConfig,
210
212
  ): RequestHandler {
211
- const validateReqInput = (req: express.Request) =>
212
- validateInput(nsid, def, req, this.options, this.lex)
213
+ const routeOpts = {
214
+ blobLimit: routeCfg.opts?.blobLimit ?? this.options.payload?.blobLimit,
215
+ }
216
+ const validateReqInput = (req: Request) =>
217
+ validateInput(nsid, def, req, routeOpts, this.lex)
213
218
  const validateResOutput =
214
219
  this.options.validateResponse === false
215
220
  ? (output?: HandlerSuccess) => output
@@ -254,12 +259,26 @@ export class Server {
254
259
  }
255
260
 
256
261
  // run the handler
257
- const outputUnvalidated = await handler(reqCtx)
262
+ const outputUnvalidated = await routeCfg.handler(reqCtx)
258
263
 
259
264
  if (isHandlerError(outputUnvalidated)) {
260
265
  throw XRPCError.fromError(outputUnvalidated)
261
266
  }
262
267
 
268
+ if (outputUnvalidated && isHandlerPipeThrough(outputUnvalidated)) {
269
+ // set headers
270
+ if (outputUnvalidated?.headers) {
271
+ Object.entries(outputUnvalidated.headers).forEach(([name, val]) => {
272
+ res.header(name, val)
273
+ })
274
+ }
275
+ res
276
+ .header('Content-Type', outputUnvalidated.encoding)
277
+ .status(200)
278
+ .send(Buffer.from(outputUnvalidated.buffer))
279
+ return
280
+ }
281
+
263
282
  if (!outputUnvalidated || isHandlerSuccess(outputUnvalidated)) {
264
283
  // validate response
265
284
  const output = validateResOutput(outputUnvalidated)
@@ -371,7 +390,7 @@ export class Server {
371
390
  )
372
391
  }
373
392
 
374
- private enableStreamingOnListen(app: express.Application) {
393
+ private enableStreamingOnListen(app: Application) {
375
394
  const _listen = app.listen
376
395
  app.listen = (...args) => {
377
396
  // @ts-ignore the args spread
@@ -445,6 +464,26 @@ function isHandlerSuccess(v: HandlerOutput): v is HandlerSuccess {
445
464
  return handlerSuccess.safeParse(v).success
446
465
  }
447
466
 
467
+ function isHandlerPipeThrough(v: HandlerOutput): v is HandlerPipeThrough {
468
+ if (v === null || typeof v !== 'object') {
469
+ return false
470
+ }
471
+ if (!isString(v['encoding']) || !(v['buffer'] instanceof ArrayBuffer)) {
472
+ return false
473
+ }
474
+ if (v['headers'] !== undefined) {
475
+ if (v['headers'] === null || typeof v['headers'] !== 'object') {
476
+ return false
477
+ }
478
+ if (!Object.values(v['headers']).every(isString)) {
479
+ return false
480
+ }
481
+ }
482
+ return true
483
+ }
484
+
485
+ const isString = (val: unknown): val is string => typeof val === 'string'
486
+
448
487
  const kRequestLocals = Symbol('requestLocals')
449
488
 
450
489
  function createLocalsMiddleware(nsid: string): RequestHandler {
@@ -11,7 +11,7 @@ import {
11
11
  } from './types'
12
12
 
13
13
  export abstract class Frame {
14
- header: FrameHeader
14
+ abstract header: FrameHeader
15
15
  body: unknown
16
16
  get op(): FrameType {
17
17
  return this.header.op
@@ -80,7 +80,7 @@ export class WebSocketKeepAlive {
80
80
 
81
81
  startHeartbeat(ws: WebSocket) {
82
82
  let isAlive = true
83
- let heartbeatInterval: NodeJS.Timer | null = null
83
+ let heartbeatInterval: NodeJS.Timeout | null = null
84
84
 
85
85
  const checkAlive = () => {
86
86
  if (!isAlive) {
@@ -145,6 +145,7 @@ function forwardSignal(signal: AbortSignal, ac: AbortController) {
145
145
  return ac.abort(signal.reason)
146
146
  } else {
147
147
  signal.addEventListener('abort', () => ac.abort(signal.reason), {
148
+ // @ts-ignore https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68625
148
149
  signal: ac.signal,
149
150
  })
150
151
  }
package/src/types.ts CHANGED
@@ -46,6 +46,13 @@ export const handlerSuccess = zod.object({
46
46
  })
47
47
  export type HandlerSuccess = zod.infer<typeof handlerSuccess>
48
48
 
49
+ export const handlerPipeThrough = zod.object({
50
+ encoding: zod.string(),
51
+ buffer: zod.instanceof(ArrayBuffer),
52
+ headers: zod.record(zod.string()).optional(),
53
+ })
54
+ export type HandlerPipeThrough = zod.infer<typeof handlerPipeThrough>
55
+
49
56
  export const handlerError = zod.object({
50
57
  status: zod.number(),
51
58
  error: zod.string().optional(),
@@ -53,7 +60,7 @@ export const handlerError = zod.object({
53
60
  })
54
61
  export type HandlerError = zod.infer<typeof handlerError>
55
62
 
56
- export type HandlerOutput = HandlerSuccess | HandlerError
63
+ export type HandlerOutput = HandlerSuccess | HandlerPipeThrough | HandlerError
57
64
 
58
65
  export type XRPCReqContext = {
59
66
  auth: HandlerAuth | undefined
@@ -85,7 +92,7 @@ export type StreamAuthVerifier = (ctx: {
85
92
  req: IncomingMessage
86
93
  }) => Promise<AuthOutput> | AuthOutput
87
94
 
88
- export type CalcKeyFn = (ctx: XRPCReqContext) => string
95
+ export type CalcKeyFn = (ctx: XRPCReqContext) => string | null
89
96
  export type CalcPointsFn = (ctx: XRPCReqContext) => number
90
97
 
91
98
  export interface RateLimiterI {
@@ -101,29 +108,29 @@ export type RateLimiterCreator = (opts: {
101
108
  keyPrefix: string
102
109
  durationMs: number
103
110
  points: number
104
- calcKey?: (ctx: XRPCReqContext) => string
105
- calcPoints?: (ctx: XRPCReqContext) => number
111
+ calcKey?: CalcKeyFn
112
+ calcPoints?: CalcPointsFn
106
113
  }) => RateLimiterI
107
114
 
108
115
  export type ServerRateLimitDescription = {
109
116
  name: string
110
117
  durationMs: number
111
118
  points: number
112
- calcKey?: (ctx: XRPCReqContext) => string
113
- calcPoints?: (ctx: XRPCReqContext) => number
119
+ calcKey?: CalcKeyFn
120
+ calcPoints?: CalcPointsFn
114
121
  }
115
122
 
116
123
  export type SharedRateLimitOpts = {
117
124
  name: string
118
- calcKey?: (ctx: XRPCReqContext) => string
119
- calcPoints?: (ctx: XRPCReqContext) => number
125
+ calcKey?: CalcKeyFn
126
+ calcPoints?: CalcPointsFn
120
127
  }
121
128
 
122
129
  export type RouteRateLimitOpts = {
123
130
  durationMs: number
124
131
  points: number
125
- calcKey?: (ctx: XRPCReqContext) => string
126
- calcPoints?: (ctx: XRPCReqContext) => number
132
+ calcKey?: CalcKeyFn
133
+ calcPoints?: CalcPointsFn
127
134
  }
128
135
 
129
136
  export type HandlerRateLimitOpts = SharedRateLimitOpts | RouteRateLimitOpts
@@ -143,7 +150,12 @@ export type RateLimiterStatus = {
143
150
  isFirstInDuration: boolean
144
151
  }
145
152
 
153
+ export type RouteOpts = {
154
+ blobLimit?: number
155
+ }
156
+
146
157
  export type XRPCHandlerConfig = {
158
+ opts?: RouteOpts
147
159
  rateLimit?: HandlerRateLimitOpts | HandlerRateLimitOpts[]
148
160
  auth?: AuthVerifier
149
161
  handler: XRPCHandler
package/src/util.ts CHANGED
@@ -19,8 +19,8 @@ import {
19
19
  handlerSuccess,
20
20
  InvalidRequestError,
21
21
  InternalServerError,
22
- Options,
23
22
  XRPCError,
23
+ RouteOpts,
24
24
  } from './types'
25
25
 
26
26
  export function decodeQueryParams(
@@ -82,7 +82,7 @@ export function validateInput(
82
82
  nsid: string,
83
83
  def: LexXrpcProcedure | LexXrpcQuery,
84
84
  req: express.Request,
85
- opts: Options,
85
+ opts: RouteOpts,
86
86
  lexicons: Lexicons,
87
87
  ): HandlerInput | undefined {
88
88
  // request expectation
@@ -139,7 +139,7 @@ export function validateInput(
139
139
  if (req.readableEnded) {
140
140
  body = req.body
141
141
  } else {
142
- body = decodeBodyStream(req, opts.payload?.blobLimit)
142
+ body = decodeBodyStream(req, opts.blobLimit)
143
143
  }
144
144
 
145
145
  return {
@@ -274,9 +274,9 @@ describe('Bodies', () => {
274
274
  const resBody = await res.json()
275
275
  const status = res.status
276
276
  expect(status).toBe(400)
277
- expect(resBody.error).toBe('InvalidRequest')
278
- expect(resBody.message).toBe(
279
- 'Request encoding (Content-Type) required but not provided',
280
- )
277
+ expect(resBody).toMatchObject({
278
+ error: 'InvalidRequest',
279
+ message: 'Request encoding (Content-Type) required but not provided',
280
+ })
281
281
  })
282
282
  })
@@ -75,7 +75,7 @@ const LEXICONS: LexiconDoc[] = [
75
75
  },
76
76
  ]
77
77
 
78
- const MISMATCHED_LEXICONS = [
78
+ const MISMATCHED_LEXICONS: LexiconDoc[] = [
79
79
  {
80
80
  lexicon: 1,
81
81
  id: 'io.example.query',
@@ -1,4 +1,8 @@
1
1
  {
2
- "extends": "./tsconfig.json",
3
- "exclude": ["**/*.spec.ts", "**/*.test.ts"]
2
+ "extends": "../../tsconfig/node.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./dist"
6
+ },
7
+ "include": ["./src"]
4
8
  }
package/tsconfig.json CHANGED
@@ -1,15 +1,7 @@
1
1
  {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "outDir": "./dist", // Your outDir,
6
- "emitDeclarationOnly": true
7
- },
8
- "include": ["./src", "__tests__/**/**.ts"],
2
+ "include": [],
9
3
  "references": [
10
- { "path": "../common/tsconfig.build.json" },
11
- { "path": "../crypto/tsconfig.build.json" },
12
- { "path": "../lexicon/tsconfig.build.json" },
13
- { "path": "../xrpc/tsconfig.build.json" }
4
+ { "path": "./tsconfig.build.json" },
5
+ { "path": "./tsconfig.tests.json" }
14
6
  ]
15
7
  }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig/tests.json",
3
+ "compilerOptions": {
4
+ "rootDir": "."
5
+ },
6
+ "include": ["./tests"]
7
+ }
package/babel.config.js DELETED
@@ -1 +0,0 @@
1
- module.exports = require('../../babel.config.js')
package/build.js DELETED
@@ -1,14 +0,0 @@
1
- const { nodeExternalsPlugin } = require('esbuild-node-externals')
2
-
3
- const buildShallow =
4
- process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true'
5
-
6
- require('esbuild').build({
7
- logLevel: 'info',
8
- entryPoints: ['src/index.ts'],
9
- bundle: true,
10
- sourcemap: true,
11
- outdir: 'dist',
12
- platform: 'node',
13
- plugins: buildShallow ? [nodeExternalsPlugin()] : [],
14
- })