@atcute/xrpc-server 2.0.0 → 2.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/dist/auth/index.d.ts +0 -1
- package/dist/auth/index.js +0 -1
- package/dist/auth/jwt-creator.d.ts +0 -1
- package/dist/auth/jwt-creator.js +0 -1
- package/dist/auth/jwt-verifier.d.ts +24 -33
- package/dist/auth/jwt-verifier.js +8 -10
- package/dist/auth/jwt.d.ts +0 -1
- package/dist/auth/jwt.js +6 -3
- package/dist/auth/types.d.ts +0 -1
- package/dist/auth/types.js +0 -1
- package/dist/main/index.d.ts +0 -1
- package/dist/main/index.js +0 -1
- package/dist/main/response.d.ts +0 -1
- package/dist/main/response.js +0 -1
- package/dist/main/router.d.ts +7 -11
- package/dist/main/router.js +3 -1
- package/dist/main/types/operation.d.ts +3 -5
- package/dist/main/types/operation.js +1 -1
- package/dist/main/types/websocket.d.ts +3 -5
- package/dist/main/types/websocket.js +0 -1
- package/dist/main/utils/frames.d.ts +0 -1
- package/dist/main/utils/frames.js +0 -1
- package/dist/main/utils/middlewares.d.ts +0 -1
- package/dist/main/utils/middlewares.js +1 -1
- package/dist/main/utils/namespaced.d.ts +0 -1
- package/dist/main/utils/namespaced.js +0 -1
- package/dist/main/utils/request-input.d.ts +3 -5
- package/dist/main/utils/request-input.js +3 -5
- package/dist/main/utils/request-params.d.ts +0 -1
- package/dist/main/utils/request-params.js +4 -3
- package/dist/main/utils/response.d.ts +0 -1
- package/dist/main/utils/response.js +0 -1
- package/dist/main/utils/websocket-mock.d.ts +0 -1
- package/dist/main/utils/websocket-mock.js +0 -1
- package/dist/main/xrpc-error.d.ts +16 -18
- package/dist/main/xrpc-error.js +9 -10
- package/dist/main/xrpc-handler.d.ts +1 -3
- package/dist/main/xrpc-handler.js +0 -1
- package/dist/middlewares/cors.d.ts +0 -1
- package/dist/middlewares/cors.js +0 -1
- package/dist/types/misc.d.ts +0 -1
- package/dist/types/misc.js +0 -1
- package/package.json +16 -19
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/jwt-creator.d.ts.map +0 -1
- package/dist/auth/jwt-creator.js.map +0 -1
- package/dist/auth/jwt-verifier.d.ts.map +0 -1
- package/dist/auth/jwt-verifier.js.map +0 -1
- package/dist/auth/jwt.d.ts.map +0 -1
- package/dist/auth/jwt.js.map +0 -1
- package/dist/auth/types.d.ts.map +0 -1
- package/dist/auth/types.js.map +0 -1
- package/dist/main/index.d.ts.map +0 -1
- package/dist/main/index.js.map +0 -1
- package/dist/main/response.d.ts.map +0 -1
- package/dist/main/response.js.map +0 -1
- package/dist/main/router.d.ts.map +0 -1
- package/dist/main/router.js.map +0 -1
- package/dist/main/types/operation.d.ts.map +0 -1
- package/dist/main/types/operation.js.map +0 -1
- package/dist/main/types/websocket.d.ts.map +0 -1
- package/dist/main/types/websocket.js.map +0 -1
- package/dist/main/utils/frames.d.ts.map +0 -1
- package/dist/main/utils/frames.js.map +0 -1
- package/dist/main/utils/middlewares.d.ts.map +0 -1
- package/dist/main/utils/middlewares.js.map +0 -1
- package/dist/main/utils/namespaced.d.ts.map +0 -1
- package/dist/main/utils/namespaced.js.map +0 -1
- package/dist/main/utils/request-input.d.ts.map +0 -1
- package/dist/main/utils/request-input.js.map +0 -1
- package/dist/main/utils/request-params.d.ts.map +0 -1
- package/dist/main/utils/request-params.js.map +0 -1
- package/dist/main/utils/response.d.ts.map +0 -1
- package/dist/main/utils/response.js.map +0 -1
- package/dist/main/utils/websocket-mock.d.ts.map +0 -1
- package/dist/main/utils/websocket-mock.js.map +0 -1
- package/dist/main/xrpc-error.d.ts.map +0 -1
- package/dist/main/xrpc-error.js.map +0 -1
- package/dist/main/xrpc-handler.d.ts.map +0 -1
- package/dist/main/xrpc-handler.js.map +0 -1
- package/dist/middlewares/cors.d.ts.map +0 -1
- package/dist/middlewares/cors.js.map +0 -1
- package/dist/types/misc.d.ts.map +0 -1
- package/dist/types/misc.js.map +0 -1
- package/lib/auth/index.ts +0 -2
- package/lib/auth/jwt-creator.ts +0 -53
- package/lib/auth/jwt-verifier.ts +0 -395
- package/lib/auth/jwt.ts +0 -148
- package/lib/auth/types.ts +0 -4
- package/lib/main/index.ts +0 -17
- package/lib/main/response.ts +0 -9
- package/lib/main/router.ts +0 -436
- package/lib/main/types/operation.ts +0 -130
- package/lib/main/types/websocket.ts +0 -22
- package/lib/main/utils/frames.ts +0 -71
- package/lib/main/utils/middlewares.ts +0 -13
- package/lib/main/utils/namespaced.ts +0 -5
- package/lib/main/utils/request-input.ts +0 -89
- package/lib/main/utils/request-params.ts +0 -108
- package/lib/main/utils/response.ts +0 -14
- package/lib/main/utils/websocket-mock.ts +0 -118
- package/lib/main/xrpc-error.ts +0 -201
- package/lib/main/xrpc-handler.ts +0 -53
- package/lib/middlewares/cors.ts +0 -104
- package/lib/types/misc.ts +0 -4
package/lib/main/router.ts
DELETED
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
safeParse,
|
|
3
|
-
type XRPCProcedureMetadata,
|
|
4
|
-
type XRPCQueryMetadata,
|
|
5
|
-
type XRPCSubscriptionMetadata,
|
|
6
|
-
} from '@atcute/lexicons/validations';
|
|
7
|
-
|
|
8
|
-
import type { Literal, Promisable } from '../types/misc.ts';
|
|
9
|
-
|
|
10
|
-
import type {
|
|
11
|
-
ProcedureConfig,
|
|
12
|
-
QueryConfig,
|
|
13
|
-
SubscriptionConfig,
|
|
14
|
-
UnknownOperationContext,
|
|
15
|
-
UnknownSubscriptionContext,
|
|
16
|
-
} from './types/operation.ts';
|
|
17
|
-
import type { WebSocketAdapter } from './types/websocket.ts';
|
|
18
|
-
import { encodeErrorFrame, encodeMessageFrame, extractMessageType, omitMessageType } from './utils/frames.ts';
|
|
19
|
-
import { createAsyncMiddlewareRunner, type Middleware } from './utils/middlewares.ts';
|
|
20
|
-
import { unwrapLxm, type Namespaced } from './utils/namespaced.ts';
|
|
21
|
-
import { constructMimeValidator, hasRequestBody } from './utils/request-input.ts';
|
|
22
|
-
import { constructParamsHandler } from './utils/request-params.ts';
|
|
23
|
-
import { invalidRequest, validationError } from './utils/response.ts';
|
|
24
|
-
import { XRPCError, XRPCSubscriptionError } from './xrpc-error.ts';
|
|
25
|
-
|
|
26
|
-
type InternalRequestContext = {
|
|
27
|
-
url: URL;
|
|
28
|
-
request: Request;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
type InternalRequestHandler = (context: InternalRequestContext) => Promise<Response>;
|
|
32
|
-
|
|
33
|
-
type InternalRouteData = {
|
|
34
|
-
method: 'GET' | 'POST';
|
|
35
|
-
handler: InternalRequestHandler;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type FetchMiddleware = Middleware<[request: Request], Promise<Response>>;
|
|
39
|
-
|
|
40
|
-
export type NotFoundHandler = (request: Request) => Promisable<Response>;
|
|
41
|
-
export type HealthCheckHandler = (request: Request) => Promisable<Response>;
|
|
42
|
-
export type ExceptionHandler = (error: unknown, request: Request) => Promisable<Response>;
|
|
43
|
-
|
|
44
|
-
/** telemetry hook invoked for unexpected HTTP handler errors; fire-and-forget. */
|
|
45
|
-
export type ErrorObserver = (ctx: { error: unknown; request: Request }) => void;
|
|
46
|
-
/** telemetry hook invoked for unexpected subscription errors; fire-and-forget. */
|
|
47
|
-
export type SocketErrorObserver = (ctx: { error: unknown; request: Request }) => void;
|
|
48
|
-
|
|
49
|
-
export const defaultExceptionHandler: ExceptionHandler = (error: unknown) => {
|
|
50
|
-
if (error instanceof XRPCError) {
|
|
51
|
-
return error.toResponse();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (error instanceof Response) {
|
|
55
|
-
return error;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return Response.json(
|
|
59
|
-
{ error: 'InternalServerError', message: `an exception happened whilst processing this request` },
|
|
60
|
-
{ status: 500 },
|
|
61
|
-
);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export const defaultNotFoundHandler: NotFoundHandler = () => {
|
|
65
|
-
return new Response('Not Found', { status: 404 });
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export interface XRPCRouterOptions {
|
|
69
|
-
middlewares?: FetchMiddleware[];
|
|
70
|
-
handleNotFound?: NotFoundHandler;
|
|
71
|
-
/**
|
|
72
|
-
* optional handler for `/xrpc/_health`. when provided, the router answers
|
|
73
|
-
* health-check requests by invoking this handler; when absent, the path
|
|
74
|
-
* falls through to `handleNotFound`. `_health` is not part of the atproto
|
|
75
|
-
* XRPC spec, so callers opt in explicitly.
|
|
76
|
-
*/
|
|
77
|
-
handleHealthCheck?: HealthCheckHandler;
|
|
78
|
-
/** translates a thrown error into an HTTP response. */
|
|
79
|
-
handleException?: ExceptionHandler;
|
|
80
|
-
/**
|
|
81
|
-
* fire-and-forget telemetry hook for unexpected HTTP errors. not invoked for
|
|
82
|
-
* client-induced errors (aborted requests, `XRPCError` subclasses, thrown
|
|
83
|
-
* `Response` objects).
|
|
84
|
-
*/
|
|
85
|
-
onError?: ErrorObserver;
|
|
86
|
-
/**
|
|
87
|
-
* fire-and-forget telemetry hook for unexpected subscription errors. not
|
|
88
|
-
* invoked for aborted signals or `XRPCSubscriptionError` (which is
|
|
89
|
-
* translated to an error frame).
|
|
90
|
-
*/
|
|
91
|
-
onSocketError?: SocketErrorObserver;
|
|
92
|
-
websocket?: WebSocketAdapter;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export class XRPCRouter {
|
|
96
|
-
#handlers: Record<string, InternalRouteData> = {};
|
|
97
|
-
#handleNotFound: NotFoundHandler;
|
|
98
|
-
#handleHealthCheck?: HealthCheckHandler;
|
|
99
|
-
#handleException: ExceptionHandler;
|
|
100
|
-
#onError?: ErrorObserver;
|
|
101
|
-
#onSocketError?: SocketErrorObserver;
|
|
102
|
-
#websocket?: WebSocketAdapter;
|
|
103
|
-
|
|
104
|
-
fetch: (request: Request) => Promise<Response>;
|
|
105
|
-
|
|
106
|
-
constructor({
|
|
107
|
-
middlewares = [],
|
|
108
|
-
handleException = defaultExceptionHandler,
|
|
109
|
-
handleNotFound = defaultNotFoundHandler,
|
|
110
|
-
handleHealthCheck,
|
|
111
|
-
onError,
|
|
112
|
-
onSocketError,
|
|
113
|
-
websocket,
|
|
114
|
-
}: XRPCRouterOptions = {}) {
|
|
115
|
-
const runner = createAsyncMiddlewareRunner([...middlewares, (request) => this.#dispatch(request)]);
|
|
116
|
-
|
|
117
|
-
this.fetch = (request) => runner(request);
|
|
118
|
-
this.#handleException = handleException;
|
|
119
|
-
this.#handleNotFound = handleNotFound;
|
|
120
|
-
this.#handleHealthCheck = handleHealthCheck;
|
|
121
|
-
this.#onError = onError;
|
|
122
|
-
this.#onSocketError = onSocketError;
|
|
123
|
-
this.#websocket = websocket;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
#observeError(error: unknown, request: Request): void {
|
|
127
|
-
// client-induced errors are not bugs; skip telemetry
|
|
128
|
-
if (request.signal.aborted) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (error instanceof XRPCError) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (error instanceof Response) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
this.#onError?.({ error, request });
|
|
140
|
-
} catch {
|
|
141
|
-
// observer threw; swallow to keep response path deterministic
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
#observeSocketError(error: unknown, request: Request): void {
|
|
146
|
-
if (request.signal.aborted) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
if (error instanceof XRPCSubscriptionError) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
this.#onSocketError?.({ error, request });
|
|
155
|
-
} catch {
|
|
156
|
-
// observer threw; swallow to keep socket close path deterministic
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async #dispatch(request: Request): Promise<Response> {
|
|
161
|
-
const url = new URL(request.url);
|
|
162
|
-
const pathname = url.pathname;
|
|
163
|
-
|
|
164
|
-
if (!pathname.startsWith('/xrpc/')) {
|
|
165
|
-
return this.#handleNotFound(request);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const nsid = pathname.slice('/xrpc/'.length);
|
|
169
|
-
|
|
170
|
-
if (nsid === '_health' && this.#handleHealthCheck !== undefined) {
|
|
171
|
-
try {
|
|
172
|
-
return await this.#handleHealthCheck(request);
|
|
173
|
-
} catch (err) {
|
|
174
|
-
if (request.signal.aborted) {
|
|
175
|
-
return new Response(null, { status: 499 });
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
this.#observeError(err, request);
|
|
179
|
-
return this.#handleException(err, request);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const route = this.#handlers[nsid];
|
|
184
|
-
if (route === undefined) {
|
|
185
|
-
return this.#handleNotFound(request);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// allow HEAD alongside GET; the runtime is responsible for stripping the
|
|
189
|
-
// response body per the Fetch API.
|
|
190
|
-
const allowed = request.method === route.method || (route.method === 'GET' && request.method === 'HEAD');
|
|
191
|
-
if (!allowed) {
|
|
192
|
-
return Response.json(
|
|
193
|
-
{ error: 'InvalidRequest', message: `invalid http method (expected ${route.method})` },
|
|
194
|
-
{ status: 405, headers: { allow: route.method === 'GET' ? 'GET, HEAD' : route.method } },
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
const response = await route.handler({
|
|
200
|
-
request: request,
|
|
201
|
-
url: url,
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
return response;
|
|
205
|
-
} catch (err) {
|
|
206
|
-
if (request.signal.aborted) {
|
|
207
|
-
return new Response(null, { status: 499 });
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
this.#observeError(err, request);
|
|
211
|
-
return this.#handleException(err, request);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
addQuery<TQuery extends XRPCQueryMetadata, TConfig extends QueryConfig<TQuery>>(
|
|
216
|
-
query: TQuery | Namespaced<TQuery>,
|
|
217
|
-
config: TConfig,
|
|
218
|
-
): void {
|
|
219
|
-
const querySchema = unwrapLxm(query);
|
|
220
|
-
const handleParams = querySchema.params ? constructParamsHandler(querySchema.params) : null;
|
|
221
|
-
|
|
222
|
-
const handler = config.handler;
|
|
223
|
-
|
|
224
|
-
this.#handlers[querySchema.nsid] = {
|
|
225
|
-
method: 'GET',
|
|
226
|
-
handler: async ({ request, url }) => {
|
|
227
|
-
let params: Record<string, Literal | Literal[]>;
|
|
228
|
-
|
|
229
|
-
if (handleParams !== null) {
|
|
230
|
-
const result = handleParams(url.searchParams);
|
|
231
|
-
if (!result.ok) {
|
|
232
|
-
return validationError('params', result);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
params = result.value;
|
|
236
|
-
} else {
|
|
237
|
-
params = {};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const context: UnknownOperationContext = {
|
|
241
|
-
request: request,
|
|
242
|
-
signal: request.signal,
|
|
243
|
-
params: params,
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const output = await handler(context as any);
|
|
247
|
-
|
|
248
|
-
if (output instanceof Response) {
|
|
249
|
-
return output;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return new Response(null);
|
|
253
|
-
},
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
addProcedure<TProcedure extends XRPCProcedureMetadata, TConfig extends ProcedureConfig<TProcedure>>(
|
|
258
|
-
procedure: TProcedure | Namespaced<TProcedure>,
|
|
259
|
-
config: TConfig,
|
|
260
|
-
): void {
|
|
261
|
-
const procedureSchema = unwrapLxm(procedure);
|
|
262
|
-
const handleParams = procedureSchema.params ? constructParamsHandler(procedureSchema.params) : null;
|
|
263
|
-
const validateInputType = procedureSchema.input ? constructMimeValidator(procedureSchema.input) : null;
|
|
264
|
-
|
|
265
|
-
const requiresInput = procedureSchema.input !== null;
|
|
266
|
-
const inputSchema = procedureSchema.input?.type === 'lex' ? procedureSchema.input.schema : null;
|
|
267
|
-
|
|
268
|
-
const handler = config.handler;
|
|
269
|
-
|
|
270
|
-
this.#handlers[procedureSchema.nsid] = {
|
|
271
|
-
method: 'POST',
|
|
272
|
-
handler: async ({ request, url }) => {
|
|
273
|
-
let params: Record<string, Literal | Literal[]>;
|
|
274
|
-
let input: Record<string, unknown> | undefined;
|
|
275
|
-
|
|
276
|
-
if (handleParams !== null) {
|
|
277
|
-
const result = handleParams(url.searchParams);
|
|
278
|
-
if (!result.ok) {
|
|
279
|
-
return validationError('params', result);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
params = result.value;
|
|
283
|
-
} else {
|
|
284
|
-
params = {};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (requiresInput) {
|
|
288
|
-
if (!hasRequestBody(request)) {
|
|
289
|
-
return invalidRequest(`request body is expected but none was provided`);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (validateInputType !== null) {
|
|
293
|
-
const result = validateInputType(request);
|
|
294
|
-
if (!result.ok) {
|
|
295
|
-
return invalidRequest(result.error);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (inputSchema !== null) {
|
|
300
|
-
let raw: any;
|
|
301
|
-
try {
|
|
302
|
-
raw = await request.json();
|
|
303
|
-
} catch {
|
|
304
|
-
return invalidRequest(`invalid request body (failed to parse json)`);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const result = safeParse(inputSchema, raw);
|
|
308
|
-
if (!result.ok) {
|
|
309
|
-
return validationError('input', result);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
input = result.value;
|
|
313
|
-
}
|
|
314
|
-
} else {
|
|
315
|
-
if (hasRequestBody(request)) {
|
|
316
|
-
return invalidRequest(`request body is provided when none was expected`);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const context: UnknownOperationContext = {
|
|
321
|
-
request: request,
|
|
322
|
-
signal: request.signal,
|
|
323
|
-
params: params,
|
|
324
|
-
input: input,
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
const output = await handler(context as any);
|
|
328
|
-
|
|
329
|
-
if (output instanceof Response) {
|
|
330
|
-
return output;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return new Response(null);
|
|
334
|
-
},
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
addSubscription<
|
|
339
|
-
TSubscription extends XRPCSubscriptionMetadata,
|
|
340
|
-
TConfig extends SubscriptionConfig<TSubscription>,
|
|
341
|
-
>(subscription: TSubscription | Namespaced<TSubscription>, config: TConfig): void {
|
|
342
|
-
const websocket = this.#websocket;
|
|
343
|
-
if (websocket === undefined) {
|
|
344
|
-
throw new Error(`WebSocket adapter not configured`);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const subscriptionSchema = unwrapLxm(subscription);
|
|
348
|
-
const nsid = subscriptionSchema.nsid;
|
|
349
|
-
|
|
350
|
-
const handleParams = subscriptionSchema.params ? constructParamsHandler(subscriptionSchema.params) : null;
|
|
351
|
-
const handler = config.handler;
|
|
352
|
-
|
|
353
|
-
this.#handlers[nsid] = {
|
|
354
|
-
method: 'GET',
|
|
355
|
-
handler: async ({ request, url }) => {
|
|
356
|
-
{
|
|
357
|
-
const con = request.headers.get('connection');
|
|
358
|
-
const req = request.headers.get('upgrade');
|
|
359
|
-
if (
|
|
360
|
-
con === null ||
|
|
361
|
-
req === null ||
|
|
362
|
-
con.toLowerCase() !== 'upgrade' ||
|
|
363
|
-
req.toLowerCase() !== 'websocket'
|
|
364
|
-
) {
|
|
365
|
-
return invalidRequest(`invalid WebSocket upgrade`);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
let params: Record<string, Literal | Literal[]>;
|
|
370
|
-
|
|
371
|
-
if (handleParams !== null) {
|
|
372
|
-
const result = handleParams(url.searchParams);
|
|
373
|
-
if (!result.ok) {
|
|
374
|
-
return validationError('params', result);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
params = result.value;
|
|
378
|
-
} else {
|
|
379
|
-
params = {};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const upgrade = await websocket.upgrade(request, async (ws) => {
|
|
383
|
-
const signal = ws.signal;
|
|
384
|
-
|
|
385
|
-
const context: UnknownSubscriptionContext = {
|
|
386
|
-
request: request,
|
|
387
|
-
params: params,
|
|
388
|
-
signal: signal,
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
try {
|
|
392
|
-
for await (const message of handler(context)) {
|
|
393
|
-
if (signal.aborted) {
|
|
394
|
-
break;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const type = extractMessageType(message, nsid);
|
|
398
|
-
const body = omitMessageType(message);
|
|
399
|
-
|
|
400
|
-
const frame = encodeMessageFrame(body, type);
|
|
401
|
-
await ws.send(frame);
|
|
402
|
-
const drained = ws.drain();
|
|
403
|
-
if (drained) {
|
|
404
|
-
await drained;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
ws.close(1000);
|
|
409
|
-
} catch (err) {
|
|
410
|
-
if (err instanceof XRPCSubscriptionError) {
|
|
411
|
-
const frame = encodeErrorFrame(err.error, err.message || undefined);
|
|
412
|
-
|
|
413
|
-
try {
|
|
414
|
-
await ws.send(frame);
|
|
415
|
-
} catch {
|
|
416
|
-
// best-effort, socket may already be closed
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
ws.close(err.closeCode, err.error);
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
ws.close(1011, `internal server error`);
|
|
424
|
-
this.#observeSocketError(err, request);
|
|
425
|
-
}
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
if (upgrade !== undefined) {
|
|
429
|
-
return upgrade;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
return invalidRequest(`WebSocket upgrade failed`);
|
|
433
|
-
},
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
InferOutput,
|
|
3
|
-
ObjectSchema,
|
|
4
|
-
VariantSchema,
|
|
5
|
-
XRPCBlobBodyParam,
|
|
6
|
-
XRPCLexBodyParam,
|
|
7
|
-
XRPCProcedureMetadata,
|
|
8
|
-
XRPCQueryMetadata,
|
|
9
|
-
XRPCSubscriptionMetadata,
|
|
10
|
-
} from '@atcute/lexicons/validations';
|
|
11
|
-
|
|
12
|
-
import type { Literal, Promisable } from '../../types/misc.ts';
|
|
13
|
-
import type { JSONResponse } from '../response.ts';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* untyped variant of {@link QueryContext} / {@link ProcedureContext}.
|
|
17
|
-
*
|
|
18
|
-
* `input` is set only when the lexicon declares a `lex` input body and the
|
|
19
|
-
* request JSON parsed successfully. for blob inputs (and for procedures that
|
|
20
|
-
* declare no input at all) it is `undefined`; handlers that expect a blob
|
|
21
|
-
* should stream from `request.body` directly.
|
|
22
|
-
*/
|
|
23
|
-
export type UnknownOperationContext = {
|
|
24
|
-
request: Request;
|
|
25
|
-
signal: AbortSignal;
|
|
26
|
-
params: Record<string, Literal | Literal[]>;
|
|
27
|
-
input?: Record<string, unknown>;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// #region Query
|
|
31
|
-
|
|
32
|
-
export type QueryContext<TQuery extends XRPCQueryMetadata> = {
|
|
33
|
-
request: Request;
|
|
34
|
-
signal: AbortSignal;
|
|
35
|
-
} & (TQuery['params'] extends ObjectSchema
|
|
36
|
-
? {
|
|
37
|
-
params: InferOutput<TQuery['params']>;
|
|
38
|
-
}
|
|
39
|
-
: {
|
|
40
|
-
// params
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
export type QueryHandler<TQuery extends XRPCQueryMetadata> = (
|
|
44
|
-
context: QueryContext<TQuery>,
|
|
45
|
-
) => Promisable<
|
|
46
|
-
TQuery['output'] extends null
|
|
47
|
-
? Response | void
|
|
48
|
-
: TQuery['output'] extends XRPCLexBodyParam
|
|
49
|
-
? Response | JSONResponse<InferOutput<TQuery['output']['schema']>>
|
|
50
|
-
: Response
|
|
51
|
-
>;
|
|
52
|
-
|
|
53
|
-
export type QueryConfig<TQuery extends XRPCQueryMetadata = XRPCQueryMetadata> = {
|
|
54
|
-
handler: QueryHandler<TQuery>;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
// #region Procedure
|
|
58
|
-
|
|
59
|
-
export type ProcedureContext<TProcedure extends XRPCProcedureMetadata> = {
|
|
60
|
-
signal: AbortSignal;
|
|
61
|
-
} & (TProcedure['input'] extends XRPCBlobBodyParam
|
|
62
|
-
? {
|
|
63
|
-
request: Request & { readonly body: ReadableStream<Uint8Array> };
|
|
64
|
-
}
|
|
65
|
-
: TProcedure['input'] extends XRPCLexBodyParam
|
|
66
|
-
? {
|
|
67
|
-
request: Request & { readonly body: null };
|
|
68
|
-
}
|
|
69
|
-
: {
|
|
70
|
-
request: Request;
|
|
71
|
-
}) &
|
|
72
|
-
(TProcedure['params'] extends ObjectSchema
|
|
73
|
-
? {
|
|
74
|
-
params: InferOutput<TProcedure['params']>;
|
|
75
|
-
}
|
|
76
|
-
: {
|
|
77
|
-
// params
|
|
78
|
-
}) &
|
|
79
|
-
(TProcedure['input'] extends XRPCLexBodyParam
|
|
80
|
-
? {
|
|
81
|
-
input: InferOutput<TProcedure['input']['schema']>;
|
|
82
|
-
}
|
|
83
|
-
: {
|
|
84
|
-
// input
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
export type ProcedureHandler<TProcedure extends XRPCProcedureMetadata> = (
|
|
88
|
-
context: ProcedureContext<TProcedure>,
|
|
89
|
-
) => Promisable<
|
|
90
|
-
TProcedure['output'] extends null
|
|
91
|
-
? Response | void
|
|
92
|
-
: TProcedure['output'] extends XRPCLexBodyParam
|
|
93
|
-
? Response | JSONResponse<InferOutput<TProcedure['output']['schema']>>
|
|
94
|
-
: Response
|
|
95
|
-
>;
|
|
96
|
-
|
|
97
|
-
export type ProcedureConfig<TProcedure extends XRPCProcedureMetadata = XRPCProcedureMetadata> = {
|
|
98
|
-
handler: ProcedureHandler<TProcedure>;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
// #region Subscription
|
|
102
|
-
|
|
103
|
-
export interface UnknownSubscriptionContext {
|
|
104
|
-
request: Request;
|
|
105
|
-
signal: AbortSignal;
|
|
106
|
-
params: Record<string, Literal | Literal[]>;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export type SubscriptionContext<TSubscription extends XRPCSubscriptionMetadata> = {
|
|
110
|
-
request: Request;
|
|
111
|
-
signal: AbortSignal;
|
|
112
|
-
} & (TSubscription['params'] extends ObjectSchema
|
|
113
|
-
? {
|
|
114
|
-
params: InferOutput<TSubscription['params']>;
|
|
115
|
-
}
|
|
116
|
-
: {
|
|
117
|
-
// params
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
export type SubscriptionHandler<TSubscription extends XRPCSubscriptionMetadata> = (
|
|
121
|
-
context: SubscriptionContext<TSubscription>,
|
|
122
|
-
) => AsyncIterable<
|
|
123
|
-
TSubscription['message'] extends ObjectSchema | VariantSchema<any>
|
|
124
|
-
? InferOutput<TSubscription['message']>
|
|
125
|
-
: never
|
|
126
|
-
>;
|
|
127
|
-
|
|
128
|
-
export type SubscriptionConfig<TSubscription extends XRPCSubscriptionMetadata = XRPCSubscriptionMetadata> = {
|
|
129
|
-
handler: SubscriptionHandler<TSubscription>;
|
|
130
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { Promisable } from '../../types/misc.ts';
|
|
2
|
-
|
|
3
|
-
export interface WebSocketConnection {
|
|
4
|
-
signal: AbortSignal;
|
|
5
|
-
send(data: Uint8Array): void | Promise<void>;
|
|
6
|
-
/**
|
|
7
|
-
* backpressure hook invoked by the router after every frame it sends.
|
|
8
|
-
* adapters that can observe the outgoing send buffer (Node `ws`, Bun, Deno)
|
|
9
|
-
* should resolve only once the buffer has drained below a healthy threshold.
|
|
10
|
-
* adapters without that visibility (e.g. Cloudflare Workers) should return
|
|
11
|
-
* synchronously.
|
|
12
|
-
*/
|
|
13
|
-
drain(): void | Promise<void>;
|
|
14
|
-
close(code?: number, reason?: string): void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface WebSocketAdapter {
|
|
18
|
-
upgrade(
|
|
19
|
-
request: Request,
|
|
20
|
-
handler: (ws: WebSocketConnection) => Promisable<void>,
|
|
21
|
-
): Promisable<Response | undefined>;
|
|
22
|
-
}
|
package/lib/main/utils/frames.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { encode } from '@atcute/cbor';
|
|
2
|
-
import { concat } from '@atcute/uint8array';
|
|
3
|
-
|
|
4
|
-
interface MessageFrameHeader {
|
|
5
|
-
op: 1;
|
|
6
|
-
t?: string; // Type discriminator for union messages (relative to NSID)
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface ErrorFrameHeader {
|
|
10
|
-
op: -1;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface ErrorFrameBody {
|
|
14
|
-
error: string;
|
|
15
|
-
message?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const encodeMessageFrame = (body: unknown, type?: string): Uint8Array => {
|
|
19
|
-
const header: MessageFrameHeader = {
|
|
20
|
-
op: 1,
|
|
21
|
-
t: type,
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
return concat([encode(header), encode(body)]);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export const encodeErrorFrame = (error: string, message?: string): Uint8Array => {
|
|
28
|
-
const header: ErrorFrameHeader = {
|
|
29
|
-
op: -1,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const body: ErrorFrameBody = {
|
|
33
|
-
error: error,
|
|
34
|
-
message: message,
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
return concat([encode(header), encode(body)]);
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export const extractMessageType = (message: unknown, nsid: string): string | undefined => {
|
|
41
|
-
if (typeof message !== 'object' || message === null) {
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const obj = message as Record<string, unknown>;
|
|
46
|
-
const type = obj.$type;
|
|
47
|
-
|
|
48
|
-
if (typeof type !== 'string') {
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// If type starts with the subscription NSID, make it relative
|
|
53
|
-
// e.g., "com.atproto.sync.subscribeRepos#commit" → "#commit"
|
|
54
|
-
if (type.startsWith(nsid + '#')) {
|
|
55
|
-
return type.slice(nsid.length);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Otherwise return the full type
|
|
59
|
-
return type;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export const omitMessageType = (message: unknown): unknown => {
|
|
63
|
-
if (typeof message !== 'object' || message === null) {
|
|
64
|
-
return message;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const obj = message as Record<string, unknown>;
|
|
68
|
-
const { $type: _type, ...rest } = obj;
|
|
69
|
-
|
|
70
|
-
return rest;
|
|
71
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export type Middleware<TParams extends any[], TReturn> = (
|
|
2
|
-
...params: [...TParams, next: (...params: TParams) => TReturn]
|
|
3
|
-
) => TReturn;
|
|
4
|
-
|
|
5
|
-
export const createAsyncMiddlewareRunner = <TParams extends any[], TReturn>(
|
|
6
|
-
middlewares: [...Middleware<TParams, Promise<TReturn>>[], Middleware<TParams, Promise<TReturn>>],
|
|
7
|
-
) => {
|
|
8
|
-
// prettier-ignore
|
|
9
|
-
return middlewares.reduceRight<(...params: TParams) => Promise<TReturn>>(
|
|
10
|
-
(next, run) => (...args) => run(...args, next),
|
|
11
|
-
() => Promise.reject(new Error(`middleware chain exhausted`)),
|
|
12
|
-
);
|
|
13
|
-
};
|