+8
-6
packages/servers/xrpc-server-bun/README.md
+8
-6
packages/servers/xrpc-server-bun/README.md
···
1
1
# @atcute/xrpc-server-bun
2
2
3
-
Bun WebSocket adapter for `@atcute/xrpc-server`.
3
+
Bun WebSocket adapter for [`@atcute/xrpc-server`](../xrpc-server/).
4
4
5
5
```sh
6
6
npm install @atcute/xrpc-server-bun
7
7
```
8
+
9
+
see the [subscriptions section](../xrpc-server/#subscriptions) in the main package for usage details.
8
10
9
11
```ts
10
12
import { XRPCRouter } from '@atcute/xrpc-server';
11
13
import { createBunWebSocket } from '@atcute/xrpc-server-bun';
12
14
13
-
import { ComAtprotoSyncSubscribeRepos } from './lexicons/index.js';
15
+
import { ComExampleSubscribe } from './lexicons/index.js';
14
16
15
-
const { adapter, wrap } = createBunWebSocket();
16
-
const router = new XRPCRouter({ websocket: adapter });
17
+
const ws = createBunWebSocket();
18
+
const router = new XRPCRouter({ websocket: ws.adapter });
17
19
18
-
router.addSubscription(ComAtprotoSyncSubscribeRepos.mainSchema, {
20
+
router.addSubscription(ComExampleSubscribe.mainSchema, {
19
21
async *handler({ params, signal }) {
20
22
while (!signal.aborted) {
21
23
yield {
···
25
27
},
26
28
});
27
29
28
-
export default router satisfies Bun.Serve;
30
+
export default ws.wrap(router);
29
31
```
+5
-3
packages/servers/xrpc-server-cloudflare/README.md
+5
-3
packages/servers/xrpc-server-cloudflare/README.md
···
1
1
# @atcute/xrpc-server-cloudflare
2
2
3
-
Cloudflare Workers WebSocket adapter for `@atcute/xrpc-server`.
3
+
Cloudflare Workers WebSocket adapter for [`@atcute/xrpc-server`](../xrpc-server/).
4
4
5
5
```sh
6
6
npm install @atcute/xrpc-server-cloudflare
7
7
```
8
+
9
+
see the [subscriptions section](../xrpc-server/#subscriptions) in the main package for usage details.
8
10
9
11
```ts
10
12
import { XRPCRouter } from '@atcute/xrpc-server';
11
13
import { createCloudflareWebSocket } from '@atcute/xrpc-server-cloudflare';
12
14
13
-
import { ComAtprotoSyncSubscribeRepos } from './lexicons/index.js';
15
+
import { ComExampleSubscribe } from './lexicons/index.js';
14
16
15
17
const adapter = createCloudflareWebSocket();
16
18
const router = new XRPCRouter({ websocket: adapter });
17
19
18
-
router.addSubscription(ComAtprotoSyncSubscribeRepos.mainSchema, {
20
+
router.addSubscription(ComExampleSubscribe.mainSchema, {
19
21
async *handler({ params, signal }) {
20
22
while (!signal.aborted) {
21
23
yield {
+6
-4
packages/servers/xrpc-server-deno/README.md
+6
-4
packages/servers/xrpc-server-deno/README.md
···
1
1
# @atcute/xrpc-server-deno
2
2
3
-
Deno WebSocket adapter for `@atcute/xrpc-server`.
3
+
Deno WebSocket adapter for [`@atcute/xrpc-server`](../xrpc-server/).
4
4
5
5
```sh
6
-
npm install @atcute/xrpc-server-deno
6
+
deno add jsr:@aspect/xrpc-server-deno
7
7
```
8
8
9
+
see the [subscriptions section](../xrpc-server/#subscriptions) in the main package for usage details.
10
+
9
11
```ts
10
12
import { XRPCRouter } from '@atcute/xrpc-server';
11
13
import { createDenoWebSocket } from '@atcute/xrpc-server-deno';
12
14
13
-
import { ComAtprotoSyncSubscribeRepos } from './lexicons/index.ts';
15
+
import { ComExampleSubscribe } from './lexicons/index.ts';
14
16
15
17
const adapter = createDenoWebSocket();
16
18
const router = new XRPCRouter({ websocket: adapter });
17
19
18
-
router.addSubscription(ComAtprotoSyncSubscribeRepos.mainSchema, {
20
+
router.addSubscription(ComExampleSubscribe.mainSchema, {
19
21
async *handler({ params, signal }) {
20
22
while (!signal.aborted) {
21
23
yield {
+12
-14
packages/servers/xrpc-server-node/README.md
+12
-14
packages/servers/xrpc-server-node/README.md
···
1
1
# @atcute/xrpc-server-node
2
2
3
-
Node.js WebSocket adapter for `@atcute/xrpc-server`.
3
+
Node.js WebSocket adapter for [`@atcute/xrpc-server`](../xrpc-server/).
4
4
5
5
```sh
6
6
npm install @atcute/xrpc-server-node
7
7
```
8
+
9
+
see the [subscriptions section](../xrpc-server/#subscriptions) in the main package for usage details.
8
10
9
11
```ts
10
12
import { serve } from '@hono/node-server';
11
13
import { XRPCRouter } from '@atcute/xrpc-server';
12
14
import { createNodeWebSocket } from '@atcute/xrpc-server-node';
13
15
14
-
const { adapter, injectWebSocket } = createNodeWebSocket();
15
-
const router = new XRPCRouter({ websocket: adapter });
16
+
import { ComExampleSubscribe } from './lexicons/index.js';
17
+
18
+
const ws = createNodeWebSocket();
19
+
const router = new XRPCRouter({ websocket: ws.adapter });
16
20
17
-
router.addSubscription(ComAtprotoSyncSubscribeRepos.mainSchema, {
21
+
router.addSubscription(ComExampleSubscribe.mainSchema, {
18
22
async *handler({ params, signal }) {
19
23
while (!signal.aborted) {
20
24
yield {
···
24
28
},
25
29
});
26
30
27
-
const server = serve(
28
-
{
29
-
fetch: router.fetch,
30
-
port: 3000,
31
-
},
32
-
(info) => {
33
-
console.log(`Listening on port ${info.port}`);
34
-
},
35
-
);
31
+
const server = serve({ fetch: router.fetch, port: 3000 }, (info) => {
32
+
console.log(`listening on port ${info.port}`);
33
+
});
36
34
37
-
injectWebSocket(server, router);
35
+
ws.injectWebSocket(server, router);
38
36
```
+181
-26
packages/servers/xrpc-server/README.md
+181
-26
packages/servers/xrpc-server/README.md
···
6
6
npm install @atcute/xrpc-server
7
7
```
8
8
9
-
## quick start
9
+
## prerequisites
10
10
11
11
this framework relies on schemas generated by `@atcute/lex-cli`, you'd need to follow its
12
12
[quick start guide](../../lexicons/lex-cli) on how to set it up.
13
13
14
-
for this example, we'll define a very simple query operation, one that returns a message greeting
15
-
the name that's provided to it:
14
+
for these examples, we'll use a simple query operation that greets a name:
16
15
17
16
```json
18
17
// file: lexicons/com/example/greet.json
···
26
25
"type": "params",
27
26
"required": ["name"],
28
27
"properties": {
29
-
"name": {
30
-
"type": "string"
31
-
}
28
+
"name": { "type": "string" }
32
29
}
33
30
},
34
31
"output": {
···
37
34
"type": "object",
38
35
"required": ["message"],
39
36
"properties": {
40
-
"message": {
41
-
"type": "string"
42
-
}
37
+
"message": { "type": "string" }
43
38
}
44
39
}
45
40
}
···
48
43
}
49
44
```
50
45
51
-
now we can build a server using the TypeScript schemas:
46
+
## usage
47
+
48
+
### handling requests
49
+
50
+
use `addQuery()` for queries (GET) and `addProcedure()` for procedures (POST). handlers receive
51
+
typed `params` and `input`, and return responses using the `json()` helper:
52
52
53
53
```ts
54
-
// file: src/index.js
55
54
import { XRPCRouter, json } from '@atcute/xrpc-server';
56
55
import { cors } from '@atcute/xrpc-server/middlewares/cors';
57
56
58
-
import { ComExampleGreet } from './lexicons/index.js';
57
+
import { ComExampleGreet, ComExampleCreatePost } from './lexicons/index.js';
59
58
60
59
const router = new XRPCRouter({ middlewares: [cors()] });
61
60
62
-
router.add(ComExampleGreet.mainSchema, {
63
-
async handler({ params: { name } }) {
64
-
return json({ message: `hello ${name}!` });
61
+
router.addQuery(ComExampleGreet.mainSchema, {
62
+
async handler({ params }) {
63
+
return json({ message: `hello ${params.name}!` });
64
+
},
65
+
});
66
+
67
+
router.addProcedure(ComExampleCreatePost.mainSchema, {
68
+
async handler({ input }) {
69
+
const post = await db.createPost(input);
70
+
return json(post);
65
71
},
66
72
});
67
73
68
74
export default router;
69
75
```
70
76
77
+
### serving the router
78
+
71
79
on Deno, Bun or Cloudflare Workers, you can export the router directly and expect it to work out of
72
80
the box.
73
81
74
-
but for Node.js, you'll need the [`@hono/node-server`][hono-node-server] adapter as the router works
82
+
for Node.js, you'll need the [`@hono/node-server`][hono-node-server] adapter as the router works
75
83
with standard Web Request/Response:
76
84
77
85
[hono-node-server]: https://github.com/honojs/node-server
78
86
79
87
```ts
80
-
// file: src/index.js
81
88
import { XRPCRouter } from '@atcute/xrpc-server';
82
89
import { serve } from '@hono/node-server';
83
90
84
91
const router = new XRPCRouter();
85
92
86
-
// ... handler code ...
93
+
// ... add handlers ...
94
+
95
+
serve({ fetch: router.fetch, port: 3000 }, (info) => {
96
+
console.log(`listening on port ${info.port}`);
97
+
});
98
+
```
99
+
100
+
### error handling
101
+
102
+
throw `XRPCError` in handlers to return error responses:
103
+
104
+
```ts
105
+
import { XRPCError } from '@atcute/xrpc-server';
106
+
107
+
router.addQuery(ComExampleGetPost.mainSchema, {
108
+
async handler({ params, request }) {
109
+
const session = await getSession(request);
110
+
if (!session) {
111
+
throw new XRPCError({ status: 401, error: 'AuthenticationRequired' });
112
+
}
113
+
114
+
const post = await db.getPost(params.uri);
115
+
if (!post) {
116
+
throw new XRPCError({ status: 400, error: 'InvalidRequest', description: `post not found` });
117
+
}
118
+
119
+
return json(post);
120
+
},
121
+
});
122
+
```
123
+
124
+
convenience subclasses are also available: `InvalidRequestError`, `AuthRequiredError`,
125
+
`ForbiddenError`, `RateLimitExceededError`, `InternalServerError`, `UpstreamFailureError`,
126
+
`NotEnoughResourcesError`, `UpstreamTimeoutError`.
127
+
128
+
### subscriptions
129
+
130
+
subscriptions provide real-time streaming over WebSocket. they require a runtime-specific adapter:
131
+
132
+
| runtime | adapter package |
133
+
| ------------------ | -------------------------------------------------------------- |
134
+
| Bun | [`@atcute/xrpc-server-bun`](../xrpc-server-bun/) |
135
+
| Node.js | [`@atcute/xrpc-server-node`](../xrpc-server-node/) |
136
+
| Deno | [`@atcute/xrpc-server-deno`](../xrpc-server-deno/) |
137
+
| Cloudflare Workers | [`@atcute/xrpc-server-cloudflare`](../xrpc-server-cloudflare/) |
138
+
139
+
here's an example using Bun:
140
+
141
+
```ts
142
+
import { XRPCRouter } from '@atcute/xrpc-server';
143
+
import { createBunWebSocket } from '@atcute/xrpc-server-bun';
144
+
145
+
import { ComExampleSubscribe } from './lexicons/index.js';
146
+
147
+
const ws = createBunWebSocket();
148
+
149
+
const router = new XRPCRouter({ websocket: ws.adapter });
150
+
151
+
router.addSubscription(ComExampleSubscribe.mainSchema, {
152
+
async *handler({ params, signal }) {
153
+
// yield messages until the client disconnects
154
+
while (!signal.aborted) {
155
+
const events = await getNewEvents(params.cursor);
156
+
157
+
for (const event of events) {
158
+
yield event;
159
+
}
87
160
88
-
serve(
89
-
{
90
-
fetch: router.fetch,
91
-
port: 3000,
161
+
await sleep(1000);
162
+
}
92
163
},
93
-
(info) => {
94
-
console.log(`listening on port ${info.port}`);
164
+
});
165
+
166
+
export default ws.wrap(router);
167
+
```
168
+
169
+
the handler is an async generator that yields messages. each yielded value is encoded as a CBOR
170
+
frame and sent to the client. the `signal` is aborted when the client disconnects.
171
+
172
+
for subscription errors, use `XRPCSubscriptionError`:
173
+
174
+
```ts
175
+
import { XRPCSubscriptionError } from '@atcute/xrpc-server';
176
+
177
+
router.addSubscription(ComExampleSubscribe.mainSchema, {
178
+
async *handler({ params }) {
179
+
if (params.cursor && isCursorTooOld(params.cursor)) {
180
+
throw new XRPCSubscriptionError({
181
+
error: 'FutureCursor',
182
+
description: `cursor is too old`,
183
+
});
184
+
}
185
+
186
+
// ...
187
+
},
188
+
});
189
+
```
190
+
191
+
### service authentication
192
+
193
+
the `@atcute/xrpc-server/auth` subpackage provides utilities for service-to-service authentication
194
+
using JWTs.
195
+
196
+
verifying incoming JWTs:
197
+
198
+
```ts
199
+
import { AuthRequiredError } from '@atcute/xrpc-server';
200
+
import { ServiceJwtVerifier, type VerifiedJwt } from '@atcute/xrpc-server/auth';
201
+
import {
202
+
CompositeDidDocumentResolver,
203
+
PlcDidDocumentResolver,
204
+
WebDidDocumentResolver,
205
+
} from '@atcute/identity-resolver';
206
+
207
+
const jwtVerifier = new ServiceJwtVerifier({
208
+
serviceDid: 'did:web:my-service.example.com',
209
+
resolver: new CompositeDidDocumentResolver({
210
+
methods: {
211
+
plc: new PlcDidDocumentResolver(),
212
+
web: new WebDidDocumentResolver(),
213
+
},
214
+
}),
215
+
});
216
+
217
+
const verifyServiceAuth = async (request: Request, lxm: string): Promise<VerifiedJwt> => {
218
+
const authHeader = request.headers.get('authorization');
219
+
if (!authHeader?.startsWith('Bearer ')) {
220
+
throw new AuthRequiredError({ description: `missing or invalid authorization header` });
221
+
}
222
+
223
+
const result = await jwtVerifier.verify(authHeader.slice(7), { lxm });
224
+
if (!result.ok) {
225
+
throw new AuthRequiredError({ description: result.error.description });
226
+
}
227
+
228
+
return result.value;
229
+
};
230
+
231
+
router.addQuery(ComExampleProtectedEndpoint.mainSchema, {
232
+
async handler({ request }) {
233
+
const auth = await verifyServiceAuth(request, 'com.example.protectedEndpoint');
234
+
return json({ caller: auth.issuer });
95
235
},
96
-
);
236
+
});
237
+
```
238
+
239
+
creating outgoing JWTs:
240
+
241
+
```ts
242
+
import { createServiceJwt } from '@atcute/xrpc-server/auth';
243
+
244
+
const jwt = await createServiceJwt({
245
+
keypair: myServiceKeypair,
246
+
issuer: 'did:web:my-service.example.com',
247
+
audience: 'did:plc:targetservice',
248
+
lxm: 'com.example.someEndpoint',
249
+
});
250
+
251
+
// use jwt in Authorization header when calling other services
97
252
```
98
253
99
-
## internal calls
254
+
### internal calls
100
255
101
256
you can make typed calls to your own endpoints using `@atcute/client`:
102
257