@atproto/xrpc-server 0.0.1
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/README.md +42 -0
- package/babel.config.js +1 -0
- package/build.js +22 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +34790 -0
- package/dist/index.js.map +7 -0
- package/dist/logger.d.ts +2 -0
- package/dist/server.d.ts +19 -0
- package/dist/types.d.ts +115 -0
- package/dist/util.d.ts +10 -0
- package/jest.config.js +6 -0
- package/package.json +35 -0
- package/src/index.ts +2 -0
- package/src/logger.ts +5 -0
- package/src/server.ts +253 -0
- package/src/types.ts +159 -0
- package/src/util.ts +237 -0
- package/tests/_util.ts +20 -0
- package/tests/auth.test.ts +155 -0
- package/tests/bodies.test.ts +240 -0
- package/tests/errors.test.ts +214 -0
- package/tests/parameters.test.ts +189 -0
- package/tests/procedures.test.ts +165 -0
- package/tests/queries.test.ts +117 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +14 -0
- package/update-pkg.js +14 -0
package/dist/logger.d.ts
ADDED
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import express, { NextFunction, RequestHandler } from 'express';
|
|
2
|
+
import { Lexicons, LexXrpcProcedure, LexXrpcQuery } from '@atproto/lexicon';
|
|
3
|
+
import { XRPCHandler, XRPCHandlerConfig, Options } from './types';
|
|
4
|
+
export declare function createServer(lexicons?: unknown[], options?: Options): Server;
|
|
5
|
+
export declare class Server {
|
|
6
|
+
router: import("express-serve-static-core").Router;
|
|
7
|
+
routes: import("express-serve-static-core").Router;
|
|
8
|
+
lex: Lexicons;
|
|
9
|
+
options: Options;
|
|
10
|
+
middleware: Record<'json' | 'text', RequestHandler>;
|
|
11
|
+
constructor(lexicons?: unknown[], opts?: Options);
|
|
12
|
+
method(nsid: string, configOrFn: XRPCHandlerConfig | XRPCHandler): void;
|
|
13
|
+
addMethod(nsid: string, configOrFn: XRPCHandlerConfig | XRPCHandler): void;
|
|
14
|
+
addLexicon(doc: unknown): void;
|
|
15
|
+
addLexicons(docs: unknown[]): void;
|
|
16
|
+
protected addRoute(nsid: string, def: LexXrpcQuery | LexXrpcProcedure, config: XRPCHandlerConfig): Promise<void>;
|
|
17
|
+
catchall(req: express.Request, _res: express.Response, next: NextFunction): Promise<void>;
|
|
18
|
+
createHandler(nsid: string, def: LexXrpcQuery | LexXrpcProcedure, handler: XRPCHandler): RequestHandler;
|
|
19
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import zod from 'zod';
|
|
3
|
+
import { ResponseType } from '@atproto/xrpc';
|
|
4
|
+
export declare type Options = {
|
|
5
|
+
validateResponse?: boolean;
|
|
6
|
+
payload?: {
|
|
7
|
+
jsonLimit?: number;
|
|
8
|
+
blobLimit?: number;
|
|
9
|
+
textLimit?: number;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export declare type UndecodedParams = typeof express.request['query'];
|
|
13
|
+
export declare type Primitive = string | number | boolean;
|
|
14
|
+
export declare type Params = Record<string, Primitive | Primitive[] | undefined>;
|
|
15
|
+
export declare const handlerInput: zod.ZodObject<{
|
|
16
|
+
encoding: zod.ZodString;
|
|
17
|
+
body: zod.ZodAny;
|
|
18
|
+
}, "strip", zod.ZodTypeAny, {
|
|
19
|
+
body?: any;
|
|
20
|
+
encoding: string;
|
|
21
|
+
}, {
|
|
22
|
+
body?: any;
|
|
23
|
+
encoding: string;
|
|
24
|
+
}>;
|
|
25
|
+
export declare type HandlerInput = zod.infer<typeof handlerInput>;
|
|
26
|
+
export declare const handlerAuth: zod.ZodObject<{
|
|
27
|
+
credentials: zod.ZodAny;
|
|
28
|
+
artifacts: zod.ZodAny;
|
|
29
|
+
}, "strip", zod.ZodTypeAny, {
|
|
30
|
+
credentials?: any;
|
|
31
|
+
artifacts?: any;
|
|
32
|
+
}, {
|
|
33
|
+
credentials?: any;
|
|
34
|
+
artifacts?: any;
|
|
35
|
+
}>;
|
|
36
|
+
export declare type HandlerAuth = zod.infer<typeof handlerAuth>;
|
|
37
|
+
export declare const handlerSuccess: zod.ZodObject<{
|
|
38
|
+
encoding: zod.ZodString;
|
|
39
|
+
body: zod.ZodAny;
|
|
40
|
+
}, "strip", zod.ZodTypeAny, {
|
|
41
|
+
body?: any;
|
|
42
|
+
encoding: string;
|
|
43
|
+
}, {
|
|
44
|
+
body?: any;
|
|
45
|
+
encoding: string;
|
|
46
|
+
}>;
|
|
47
|
+
export declare type HandlerSuccess = zod.infer<typeof handlerSuccess>;
|
|
48
|
+
export declare const handlerError: zod.ZodObject<{
|
|
49
|
+
status: zod.ZodNumber;
|
|
50
|
+
error: zod.ZodOptional<zod.ZodString>;
|
|
51
|
+
message: zod.ZodOptional<zod.ZodString>;
|
|
52
|
+
}, "strip", zod.ZodTypeAny, {
|
|
53
|
+
error?: string | undefined;
|
|
54
|
+
message?: string | undefined;
|
|
55
|
+
status: number;
|
|
56
|
+
}, {
|
|
57
|
+
error?: string | undefined;
|
|
58
|
+
message?: string | undefined;
|
|
59
|
+
status: number;
|
|
60
|
+
}>;
|
|
61
|
+
export declare type HandlerError = zod.infer<typeof handlerError>;
|
|
62
|
+
export declare type HandlerOutput = HandlerSuccess | HandlerError;
|
|
63
|
+
export declare type XRPCHandler = (ctx: {
|
|
64
|
+
auth: HandlerAuth | undefined;
|
|
65
|
+
params: Params;
|
|
66
|
+
input: HandlerInput | undefined;
|
|
67
|
+
req: express.Request;
|
|
68
|
+
res: express.Response;
|
|
69
|
+
}) => Promise<HandlerOutput> | HandlerOutput | undefined;
|
|
70
|
+
export declare type AuthOutput = HandlerAuth | HandlerError;
|
|
71
|
+
export declare type AuthVerifier = (ctx: {
|
|
72
|
+
req: express.Request;
|
|
73
|
+
res: express.Response;
|
|
74
|
+
}) => Promise<AuthOutput> | AuthOutput;
|
|
75
|
+
export declare type XRPCHandlerConfig = {
|
|
76
|
+
auth?: AuthVerifier;
|
|
77
|
+
handler: XRPCHandler;
|
|
78
|
+
};
|
|
79
|
+
export declare class XRPCError extends Error {
|
|
80
|
+
type: ResponseType;
|
|
81
|
+
errorMessage?: string | undefined;
|
|
82
|
+
customErrorName?: string | undefined;
|
|
83
|
+
constructor(type: ResponseType, errorMessage?: string | undefined, customErrorName?: string | undefined);
|
|
84
|
+
get payload(): {
|
|
85
|
+
error: string | undefined;
|
|
86
|
+
message: string | undefined;
|
|
87
|
+
};
|
|
88
|
+
get typeStr(): string | undefined;
|
|
89
|
+
static fromError(error: unknown): XRPCError;
|
|
90
|
+
}
|
|
91
|
+
export declare function isHandlerError(v: unknown): v is HandlerError;
|
|
92
|
+
export declare class InvalidRequestError extends XRPCError {
|
|
93
|
+
constructor(errorMessage?: string, customErrorName?: string);
|
|
94
|
+
}
|
|
95
|
+
export declare class AuthRequiredError extends XRPCError {
|
|
96
|
+
constructor(errorMessage?: string, customErrorName?: string);
|
|
97
|
+
}
|
|
98
|
+
export declare class ForbiddenError extends XRPCError {
|
|
99
|
+
constructor(errorMessage?: string, customErrorName?: string);
|
|
100
|
+
}
|
|
101
|
+
export declare class InternalServerError extends XRPCError {
|
|
102
|
+
constructor(errorMessage?: string, customErrorName?: string);
|
|
103
|
+
}
|
|
104
|
+
export declare class UpstreamFailureError extends XRPCError {
|
|
105
|
+
constructor(errorMessage?: string, customErrorName?: string);
|
|
106
|
+
}
|
|
107
|
+
export declare class NotEnoughResoucesError extends XRPCError {
|
|
108
|
+
constructor(errorMessage?: string, customErrorName?: string);
|
|
109
|
+
}
|
|
110
|
+
export declare class UpstreamTimeoutError extends XRPCError {
|
|
111
|
+
constructor(errorMessage?: string, customErrorName?: string);
|
|
112
|
+
}
|
|
113
|
+
export declare class MethodNotImplementedError extends XRPCError {
|
|
114
|
+
constructor(errorMessage?: string, customErrorName?: string);
|
|
115
|
+
}
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { Lexicons, LexXrpcProcedure, LexXrpcQuery } from '@atproto/lexicon';
|
|
3
|
+
import { UndecodedParams, Params, HandlerInput, HandlerSuccess, Options } from './types';
|
|
4
|
+
export declare function decodeQueryParams(def: LexXrpcProcedure | LexXrpcQuery, params: UndecodedParams): Params;
|
|
5
|
+
export declare function decodeQueryParam(type: string, value: unknown): string | number | boolean | undefined;
|
|
6
|
+
export declare function validateInput(nsid: string, def: LexXrpcProcedure | LexXrpcQuery, req: express.Request, opts: Options, lexicons: Lexicons): HandlerInput | undefined;
|
|
7
|
+
export declare function validateOutput(nsid: string, def: LexXrpcProcedure | LexXrpcQuery, output: HandlerSuccess | undefined, lexicons: Lexicons): HandlerSuccess | undefined;
|
|
8
|
+
export declare function normalizeMime(v: string): any;
|
|
9
|
+
export declare function hasBody(req: express.Request): string | true | undefined;
|
|
10
|
+
export declare function processBodyAsBytes(req: express.Request): Promise<Uint8Array>;
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atproto/xrpc-server",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "jest",
|
|
7
|
+
"prettier": "prettier --check src/",
|
|
8
|
+
"prettier:fix": "prettier --write src/",
|
|
9
|
+
"lint": "eslint . --ext .ts,.tsx",
|
|
10
|
+
"lint:fix": "yarn lint --fix",
|
|
11
|
+
"verify": "run-p prettier lint",
|
|
12
|
+
"verify:fix": "yarn prettier:fix && yarn lint:fix",
|
|
13
|
+
"build": "node ./build.js",
|
|
14
|
+
"postbuild": "tsc --build tsconfig.build.json",
|
|
15
|
+
"update-main-to-dist": "node ./update-pkg.js --update-main-to-dist",
|
|
16
|
+
"update-main-to-src": "node ./update-pkg.js --update-main-to-src",
|
|
17
|
+
"prepublish": "npm run update-main-to-dist",
|
|
18
|
+
"postpublish": "npm run update-main-to-src"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@atproto/common": "*",
|
|
23
|
+
"@atproto/lexicon": "*",
|
|
24
|
+
"express": "^4.17.2",
|
|
25
|
+
"http-errors": "^2.0.0",
|
|
26
|
+
"mime-types": "^2.1.35",
|
|
27
|
+
"zod": "^3.14.2"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@atproto/crypto": "*",
|
|
31
|
+
"@atproto/xrpc": "*",
|
|
32
|
+
"@types/express": "^4.17.13",
|
|
33
|
+
"@types/http-errors": "^2.0.1"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.ts
ADDED
package/src/logger.ts
ADDED
package/src/server.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Readable } from 'stream'
|
|
2
|
+
import express, {
|
|
3
|
+
ErrorRequestHandler,
|
|
4
|
+
NextFunction,
|
|
5
|
+
RequestHandler,
|
|
6
|
+
} from 'express'
|
|
7
|
+
import { Lexicons, LexXrpcProcedure, LexXrpcQuery } from '@atproto/lexicon'
|
|
8
|
+
import {
|
|
9
|
+
XRPCHandler,
|
|
10
|
+
XRPCError,
|
|
11
|
+
InvalidRequestError,
|
|
12
|
+
HandlerOutput,
|
|
13
|
+
HandlerSuccess,
|
|
14
|
+
handlerSuccess,
|
|
15
|
+
XRPCHandlerConfig,
|
|
16
|
+
MethodNotImplementedError,
|
|
17
|
+
HandlerAuth,
|
|
18
|
+
AuthVerifier,
|
|
19
|
+
isHandlerError,
|
|
20
|
+
Options,
|
|
21
|
+
} from './types'
|
|
22
|
+
import { decodeQueryParams, validateInput, validateOutput } from './util'
|
|
23
|
+
import log from './logger'
|
|
24
|
+
|
|
25
|
+
export function createServer(lexicons?: unknown[], options?: Options) {
|
|
26
|
+
return new Server(lexicons, options)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class Server {
|
|
30
|
+
router = express.Router()
|
|
31
|
+
routes = express.Router()
|
|
32
|
+
lex = new Lexicons()
|
|
33
|
+
options: Options
|
|
34
|
+
middleware: Record<'json' | 'text', RequestHandler>
|
|
35
|
+
|
|
36
|
+
constructor(lexicons?: unknown[], opts?: Options) {
|
|
37
|
+
if (lexicons) {
|
|
38
|
+
this.addLexicons(lexicons)
|
|
39
|
+
}
|
|
40
|
+
this.router.use(this.routes)
|
|
41
|
+
this.router.use('/xrpc/:methodId', this.catchall.bind(this))
|
|
42
|
+
this.router.use(errorMiddleware)
|
|
43
|
+
this.options = opts ?? {}
|
|
44
|
+
this.middleware = {
|
|
45
|
+
json: express.json({ limit: opts?.payload?.jsonLimit }),
|
|
46
|
+
text: express.text({ limit: opts?.payload?.textLimit }),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// handlers
|
|
51
|
+
// =
|
|
52
|
+
|
|
53
|
+
method(nsid: string, configOrFn: XRPCHandlerConfig | XRPCHandler) {
|
|
54
|
+
this.addMethod(nsid, configOrFn)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
addMethod(nsid: string, configOrFn: XRPCHandlerConfig | XRPCHandler) {
|
|
58
|
+
const config =
|
|
59
|
+
typeof configOrFn === 'function' ? { handler: configOrFn } : configOrFn
|
|
60
|
+
const def = this.lex.getDef(nsid)
|
|
61
|
+
if (!def || (def.type !== 'query' && def.type !== 'procedure')) {
|
|
62
|
+
throw new Error(`Lex def for ${nsid} is not a query or a procedure`)
|
|
63
|
+
}
|
|
64
|
+
this.addRoute(nsid, def, config)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// schemas
|
|
68
|
+
// =
|
|
69
|
+
|
|
70
|
+
addLexicon(doc: unknown) {
|
|
71
|
+
this.lex.add(doc)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
addLexicons(docs: unknown[]) {
|
|
75
|
+
for (const doc of docs) {
|
|
76
|
+
this.addLexicon(doc)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// http
|
|
81
|
+
// =
|
|
82
|
+
|
|
83
|
+
protected async addRoute(
|
|
84
|
+
nsid: string,
|
|
85
|
+
def: LexXrpcQuery | LexXrpcProcedure,
|
|
86
|
+
config: XRPCHandlerConfig,
|
|
87
|
+
) {
|
|
88
|
+
const verb: 'post' | 'get' = def.type === 'procedure' ? 'post' : 'get'
|
|
89
|
+
const middleware: RequestHandler[] = []
|
|
90
|
+
middleware.push(createLocalsMiddleware(nsid))
|
|
91
|
+
if (config.auth) {
|
|
92
|
+
middleware.push(createAuthMiddleware(config.auth))
|
|
93
|
+
}
|
|
94
|
+
if (verb === 'post') {
|
|
95
|
+
middleware.push(this.middleware.json)
|
|
96
|
+
middleware.push(this.middleware.text)
|
|
97
|
+
}
|
|
98
|
+
this.routes[verb](
|
|
99
|
+
`/xrpc/${nsid}`,
|
|
100
|
+
...middleware,
|
|
101
|
+
this.createHandler(nsid, def, config.handler),
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async catchall(
|
|
106
|
+
req: express.Request,
|
|
107
|
+
_res: express.Response,
|
|
108
|
+
next: NextFunction,
|
|
109
|
+
) {
|
|
110
|
+
const def = this.lex.getDef(req.params.methodId)
|
|
111
|
+
if (!def) {
|
|
112
|
+
return next(new MethodNotImplementedError())
|
|
113
|
+
}
|
|
114
|
+
// validate method
|
|
115
|
+
if (def.type === 'query' && req.method !== 'GET') {
|
|
116
|
+
return next(
|
|
117
|
+
new InvalidRequestError(
|
|
118
|
+
`Incorrect HTTP method (${req.method}) expected GET`,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
} else if (def.type === 'procedure' && req.method !== 'POST') {
|
|
122
|
+
return next(
|
|
123
|
+
new InvalidRequestError(
|
|
124
|
+
`Incorrect HTTP method (${req.method}) expected POST`,
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
return next()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
createHandler(
|
|
132
|
+
nsid: string,
|
|
133
|
+
def: LexXrpcQuery | LexXrpcProcedure,
|
|
134
|
+
handler: XRPCHandler,
|
|
135
|
+
): RequestHandler {
|
|
136
|
+
const validateReqInput = (req: express.Request) =>
|
|
137
|
+
validateInput(nsid, def, req, this.options, this.lex)
|
|
138
|
+
const validateResOutput =
|
|
139
|
+
this.options.validateResponse === false
|
|
140
|
+
? (output?: HandlerSuccess) => output
|
|
141
|
+
: (output?: HandlerSuccess) =>
|
|
142
|
+
validateOutput(nsid, def, output, this.lex)
|
|
143
|
+
const assertValidXrpcParams = (params: unknown) =>
|
|
144
|
+
this.lex.assertValidXrpcParams(nsid, params)
|
|
145
|
+
return async function (req, res, next) {
|
|
146
|
+
try {
|
|
147
|
+
// validate request
|
|
148
|
+
const params = decodeQueryParams(def, req.query)
|
|
149
|
+
try {
|
|
150
|
+
assertValidXrpcParams(params)
|
|
151
|
+
} catch (e) {
|
|
152
|
+
throw new InvalidRequestError(String(e))
|
|
153
|
+
}
|
|
154
|
+
const input = validateReqInput(req)
|
|
155
|
+
|
|
156
|
+
if (input?.body instanceof Readable) {
|
|
157
|
+
// If the body stream errors at any time, abort the request
|
|
158
|
+
input.body.once('error', next)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const locals: RequestLocals = req[kRequestLocals]
|
|
162
|
+
|
|
163
|
+
// run the handler
|
|
164
|
+
const outputUnvalidated = await handler({
|
|
165
|
+
params,
|
|
166
|
+
input,
|
|
167
|
+
auth: locals.auth,
|
|
168
|
+
req,
|
|
169
|
+
res,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
if (isHandlerError(outputUnvalidated)) {
|
|
173
|
+
throw XRPCError.fromError(outputUnvalidated)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!outputUnvalidated || isHandlerSuccess(outputUnvalidated)) {
|
|
177
|
+
// validate response
|
|
178
|
+
const output = validateResOutput(outputUnvalidated)
|
|
179
|
+
// send response
|
|
180
|
+
if (
|
|
181
|
+
output?.encoding === 'application/json' ||
|
|
182
|
+
output?.encoding === 'json'
|
|
183
|
+
) {
|
|
184
|
+
res.status(200).json(output.body)
|
|
185
|
+
} else if (output) {
|
|
186
|
+
res.header('Content-Type', output.encoding)
|
|
187
|
+
res
|
|
188
|
+
.status(200)
|
|
189
|
+
.send(
|
|
190
|
+
output.body instanceof Uint8Array
|
|
191
|
+
? Buffer.from(output.body)
|
|
192
|
+
: output.body,
|
|
193
|
+
)
|
|
194
|
+
} else {
|
|
195
|
+
res.status(200).end()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch (err: unknown) {
|
|
199
|
+
next(err)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isHandlerSuccess(v: HandlerOutput): v is HandlerSuccess {
|
|
206
|
+
return handlerSuccess.safeParse(v).success
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const kRequestLocals = Symbol('requestLocals')
|
|
210
|
+
|
|
211
|
+
function createLocalsMiddleware(nsid: string): RequestHandler {
|
|
212
|
+
return function (req, _res, next) {
|
|
213
|
+
const locals: RequestLocals = { auth: undefined, nsid }
|
|
214
|
+
req[kRequestLocals] = locals
|
|
215
|
+
return next()
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
type RequestLocals = {
|
|
220
|
+
auth: HandlerAuth | undefined
|
|
221
|
+
nsid: string
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function createAuthMiddleware(verifier: AuthVerifier): RequestHandler {
|
|
225
|
+
return async function (req, res, next) {
|
|
226
|
+
try {
|
|
227
|
+
const result = await verifier({ req, res })
|
|
228
|
+
if (isHandlerError(result)) {
|
|
229
|
+
throw XRPCError.fromError(result)
|
|
230
|
+
}
|
|
231
|
+
const locals: RequestLocals = req[kRequestLocals]
|
|
232
|
+
locals.auth = result
|
|
233
|
+
next()
|
|
234
|
+
} catch (err: unknown) {
|
|
235
|
+
next(err)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const errorMiddleware: ErrorRequestHandler = function (err, req, res, next) {
|
|
241
|
+
const locals: RequestLocals | undefined = req[kRequestLocals]
|
|
242
|
+
const methodSuffix = locals ? ` method ${locals.nsid}` : ''
|
|
243
|
+
if (err instanceof XRPCError) {
|
|
244
|
+
log.error(err, `error in xrpc${methodSuffix}`)
|
|
245
|
+
} else {
|
|
246
|
+
log.error(err, `unhandled exception in xrpc${methodSuffix}`)
|
|
247
|
+
}
|
|
248
|
+
if (res.headersSent) {
|
|
249
|
+
return next(err)
|
|
250
|
+
}
|
|
251
|
+
const xrpcError = XRPCError.fromError(err)
|
|
252
|
+
return res.status(xrpcError.type).json(xrpcError.payload)
|
|
253
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { isHttpError } from 'http-errors'
|
|
3
|
+
import zod from 'zod'
|
|
4
|
+
import { ResponseType, ResponseTypeStrings } from '@atproto/xrpc'
|
|
5
|
+
|
|
6
|
+
export type Options = {
|
|
7
|
+
validateResponse?: boolean
|
|
8
|
+
payload?: {
|
|
9
|
+
jsonLimit?: number
|
|
10
|
+
blobLimit?: number
|
|
11
|
+
textLimit?: number
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type UndecodedParams = typeof express.request['query']
|
|
16
|
+
|
|
17
|
+
export type Primitive = string | number | boolean
|
|
18
|
+
export type Params = Record<string, Primitive | Primitive[] | undefined>
|
|
19
|
+
|
|
20
|
+
export const handlerInput = zod.object({
|
|
21
|
+
encoding: zod.string(),
|
|
22
|
+
body: zod.any(),
|
|
23
|
+
})
|
|
24
|
+
export type HandlerInput = zod.infer<typeof handlerInput>
|
|
25
|
+
|
|
26
|
+
export const handlerAuth = zod.object({
|
|
27
|
+
credentials: zod.any(),
|
|
28
|
+
artifacts: zod.any(),
|
|
29
|
+
})
|
|
30
|
+
export type HandlerAuth = zod.infer<typeof handlerAuth>
|
|
31
|
+
|
|
32
|
+
export const handlerSuccess = zod.object({
|
|
33
|
+
encoding: zod.string(),
|
|
34
|
+
body: zod.any(),
|
|
35
|
+
})
|
|
36
|
+
export type HandlerSuccess = zod.infer<typeof handlerSuccess>
|
|
37
|
+
|
|
38
|
+
export const handlerError = zod.object({
|
|
39
|
+
status: zod.number(),
|
|
40
|
+
error: zod.string().optional(),
|
|
41
|
+
message: zod.string().optional(),
|
|
42
|
+
})
|
|
43
|
+
export type HandlerError = zod.infer<typeof handlerError>
|
|
44
|
+
|
|
45
|
+
export type HandlerOutput = HandlerSuccess | HandlerError
|
|
46
|
+
|
|
47
|
+
export type XRPCHandler = (ctx: {
|
|
48
|
+
auth: HandlerAuth | undefined
|
|
49
|
+
params: Params
|
|
50
|
+
input: HandlerInput | undefined
|
|
51
|
+
req: express.Request
|
|
52
|
+
res: express.Response
|
|
53
|
+
}) => Promise<HandlerOutput> | HandlerOutput | undefined
|
|
54
|
+
|
|
55
|
+
export type AuthOutput = HandlerAuth | HandlerError
|
|
56
|
+
|
|
57
|
+
export type AuthVerifier = (ctx: {
|
|
58
|
+
req: express.Request
|
|
59
|
+
res: express.Response
|
|
60
|
+
}) => Promise<AuthOutput> | AuthOutput
|
|
61
|
+
|
|
62
|
+
export type XRPCHandlerConfig = {
|
|
63
|
+
auth?: AuthVerifier
|
|
64
|
+
handler: XRPCHandler
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class XRPCError extends Error {
|
|
68
|
+
constructor(
|
|
69
|
+
public type: ResponseType,
|
|
70
|
+
public errorMessage?: string,
|
|
71
|
+
public customErrorName?: string,
|
|
72
|
+
) {
|
|
73
|
+
super(errorMessage)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get payload() {
|
|
77
|
+
return {
|
|
78
|
+
error: this.customErrorName,
|
|
79
|
+
message:
|
|
80
|
+
this.type === ResponseType.InternalServerError
|
|
81
|
+
? this.typeStr // Do not respond with error details for 500s
|
|
82
|
+
: this.errorMessage || this.typeStr,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get typeStr(): string | undefined {
|
|
87
|
+
return ResponseTypeStrings[this.type]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static fromError(error: unknown) {
|
|
91
|
+
if (error instanceof XRPCError) {
|
|
92
|
+
return error
|
|
93
|
+
}
|
|
94
|
+
let resultErr: XRPCError
|
|
95
|
+
if (isHttpError(error)) {
|
|
96
|
+
resultErr = new XRPCError(error.status, error.message, error.name)
|
|
97
|
+
} else if (isHandlerError(error)) {
|
|
98
|
+
resultErr = new XRPCError(error.status, error.message, error.error)
|
|
99
|
+
} else if (error instanceof Error) {
|
|
100
|
+
resultErr = new InternalServerError(error.message)
|
|
101
|
+
} else {
|
|
102
|
+
resultErr = new InternalServerError('Unexpected internal server error')
|
|
103
|
+
}
|
|
104
|
+
resultErr.cause = error
|
|
105
|
+
return resultErr
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isHandlerError(v: unknown): v is HandlerError {
|
|
110
|
+
return handlerError.safeParse(v).success
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export class InvalidRequestError extends XRPCError {
|
|
114
|
+
constructor(errorMessage?: string, customErrorName?: string) {
|
|
115
|
+
super(ResponseType.InvalidRequest, errorMessage, customErrorName)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class AuthRequiredError extends XRPCError {
|
|
120
|
+
constructor(errorMessage?: string, customErrorName?: string) {
|
|
121
|
+
super(ResponseType.AuthRequired, errorMessage, customErrorName)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class ForbiddenError extends XRPCError {
|
|
126
|
+
constructor(errorMessage?: string, customErrorName?: string) {
|
|
127
|
+
super(ResponseType.Forbidden, errorMessage, customErrorName)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export class InternalServerError extends XRPCError {
|
|
132
|
+
constructor(errorMessage?: string, customErrorName?: string) {
|
|
133
|
+
super(ResponseType.InternalServerError, errorMessage, customErrorName)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class UpstreamFailureError extends XRPCError {
|
|
138
|
+
constructor(errorMessage?: string, customErrorName?: string) {
|
|
139
|
+
super(ResponseType.UpstreamFailure, errorMessage, customErrorName)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export class NotEnoughResoucesError extends XRPCError {
|
|
144
|
+
constructor(errorMessage?: string, customErrorName?: string) {
|
|
145
|
+
super(ResponseType.NotEnoughResouces, errorMessage, customErrorName)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export class UpstreamTimeoutError extends XRPCError {
|
|
150
|
+
constructor(errorMessage?: string, customErrorName?: string) {
|
|
151
|
+
super(ResponseType.UpstreamTimeout, errorMessage, customErrorName)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export class MethodNotImplementedError extends XRPCError {
|
|
156
|
+
constructor(errorMessage?: string, customErrorName?: string) {
|
|
157
|
+
super(ResponseType.MethodNotImplemented, errorMessage, customErrorName)
|
|
158
|
+
}
|
|
159
|
+
}
|