@sx3/ultra 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/LICENSE +21 -0
- package/README.md +512 -0
- package/dist/auth.d.mts +56 -0
- package/dist/auth.mjs +84 -0
- package/dist/client.d.mts +27 -0
- package/dist/client.mjs +131 -0
- package/dist/context-ChCsZh7S.d.mts +17 -0
- package/dist/context.d.mts +2 -0
- package/dist/context.mjs +10 -0
- package/dist/cors.d.mts +16 -0
- package/dist/cors.mjs +58 -0
- package/dist/crypto.d.mts +7 -0
- package/dist/crypto.mjs +44 -0
- package/dist/error-CII1zMOR.mjs +45 -0
- package/dist/error.d.mts +25 -0
- package/dist/error.mjs +3 -0
- package/dist/http-BqWCMASL.d.mts +10 -0
- package/dist/http.d.mts +3 -0
- package/dist/http.mjs +1 -0
- package/dist/middleware-COKreBGP.d.mts +43 -0
- package/dist/middleware.d.mts +4 -0
- package/dist/middleware.mjs +1 -0
- package/dist/procedure-BN1rLLRX.mjs +86 -0
- package/dist/procedure.d.mts +4 -0
- package/dist/procedure.mjs +3 -0
- package/dist/response-CNhIkAYG.mjs +59 -0
- package/dist/response.d.mts +5 -0
- package/dist/response.mjs +3 -0
- package/dist/rpc-Ch2UXReT.d.mts +23 -0
- package/dist/rpc-_rBI0z-9.mjs +7 -0
- package/dist/rpc.d.mts +2 -0
- package/dist/rpc.mjs +3 -0
- package/dist/session.d.mts +115 -0
- package/dist/session.mjs +181 -0
- package/dist/types-Cn69QrjS.d.mts +11 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +1 -0
- package/dist/ultra.d.mts +69 -0
- package/dist/ultra.mjs +273 -0
- package/dist/validation-CkRfxQJ_.d.mts +57 -0
- package/dist/validation-Cop5Wvlr.mjs +12 -0
- package/dist/validation.d.mts +2 -0
- package/dist/validation.mjs +3 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-PRESENT SX3 <https://github.com/SX-3>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
# Ultra
|
|
2
|
+
|
|
3
|
+
Type-safe and fast RPC over HTTP/WebSocket for [Bun](https://bun.sh).
|
|
4
|
+
|
|
5
|
+
## Оглавление
|
|
6
|
+
|
|
7
|
+
- [Install](#install)
|
|
8
|
+
- [Quick start](#quick-start)
|
|
9
|
+
- [Core concepts](#core-concepts)
|
|
10
|
+
- [Modules](#modules)
|
|
11
|
+
- [Type safety](#type-safety)
|
|
12
|
+
- [Protocol independence](#protocol-independence)
|
|
13
|
+
- [Middleware](#middleware)
|
|
14
|
+
- [Validation](#validation)
|
|
15
|
+
- [Context](#context)
|
|
16
|
+
- [Built-in features](#built-in-features)
|
|
17
|
+
- [CORS](#cors)
|
|
18
|
+
- [Sessions](#sessions)
|
|
19
|
+
- [Authentication](#authentication)
|
|
20
|
+
- [Crypto](#crypto)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bun add @sx3/ultra
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
Write a simple server with two modules: users and books.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// server.ts
|
|
36
|
+
import { Ultra } from '@sx3/ultra';
|
|
37
|
+
|
|
38
|
+
// User module
|
|
39
|
+
const users = new Ultra().routes(input => ({
|
|
40
|
+
users: {
|
|
41
|
+
list: input().http().handler(() => [
|
|
42
|
+
{ id: 1, name: 'Alice' },
|
|
43
|
+
{ id: 2, name: 'Bob' }
|
|
44
|
+
])
|
|
45
|
+
}
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// Book module
|
|
49
|
+
const books = new Ultra().routes(input => ({
|
|
50
|
+
books: {
|
|
51
|
+
list: input().http().handler(() => [
|
|
52
|
+
{ id: 1, title: 'TypeScript' },
|
|
53
|
+
{ id: 2, title: 'Brave New World' }
|
|
54
|
+
]),
|
|
55
|
+
}
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
// Root module
|
|
59
|
+
const server = new Ultra()
|
|
60
|
+
.use(users)
|
|
61
|
+
.use(books)
|
|
62
|
+
.on('server:started', (bunServer) => {
|
|
63
|
+
console.log(`Server started at ${bunServer.url}`);
|
|
64
|
+
})
|
|
65
|
+
.start();
|
|
66
|
+
|
|
67
|
+
// Type for client usage
|
|
68
|
+
export type Server = typeof server;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Create a client to call the server methods.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// clients.ts
|
|
75
|
+
import type { Server } from './server';
|
|
76
|
+
import { createHTTPClient } from '@sx3/ultra/client';
|
|
77
|
+
|
|
78
|
+
const http = createHTTPClient<Server>({
|
|
79
|
+
baseUrl: 'http://localhost:3000',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const users = await http.users.list(); // [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
|
|
83
|
+
const books = await http.books.list(); // [{ id: 1, title: 'TypeScript' }, { id: 2, title: 'Brave New World' }]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Create WebSocket client to call server methods over WebSocket.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// clients.ts
|
|
90
|
+
import type { Server } from './server';
|
|
91
|
+
import { createWebSocketClient } from '@sx3/ultra/client';
|
|
92
|
+
|
|
93
|
+
let socket = new WebSocket('ws://localhost:3000/ws');
|
|
94
|
+
const ws = createWebSocketClient<Server>({
|
|
95
|
+
// Socket getter function
|
|
96
|
+
socket: () => socket,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const users = await ws.users.list(); // [{ id: 1, name: 'Alice' } ...]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Or create [super client](#protocol-independence) for dynamic transport switching.
|
|
103
|
+
|
|
104
|
+
## Middleware
|
|
105
|
+
|
|
106
|
+
Middleware just functions that run before your route handlers. You can use them to add authentication, logging, error handling, etc.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { Ultra } from '@sx3/ultra';
|
|
110
|
+
import { UnauthorizedError } from '@sx3/ultra/error';
|
|
111
|
+
|
|
112
|
+
// Simple authentication middleware
|
|
113
|
+
async function isAuthenticated({ context, next }) {
|
|
114
|
+
if (!await context.auth.check()) return new UnauthorizedError();
|
|
115
|
+
return next();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const app = new Ultra()
|
|
119
|
+
.use(isAuthenticated) // Apply middleware globally
|
|
120
|
+
.routes(input => ({
|
|
121
|
+
profile: {
|
|
122
|
+
get: input()
|
|
123
|
+
.use(isAuthenticated) // Apply middleware to specific route
|
|
124
|
+
.http()
|
|
125
|
+
.handler(({ context }) => {
|
|
126
|
+
return context.auth.user; // Access authenticated user
|
|
127
|
+
}),
|
|
128
|
+
},
|
|
129
|
+
}));
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Validation
|
|
133
|
+
|
|
134
|
+
Ultra supports any library compatible with [Standard Schema](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec).
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { Ultra } from '@sx3/ultra';
|
|
138
|
+
import { createHTTPClient } from '@sx3/ultra/client';
|
|
139
|
+
// import * as z from 'zod';
|
|
140
|
+
import * as s from 'sury';
|
|
141
|
+
|
|
142
|
+
const LoginSchema = s.schema({
|
|
143
|
+
name: s.string(),
|
|
144
|
+
password: s.string(),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const UserSchema = s.schema({
|
|
148
|
+
id: s.number(),
|
|
149
|
+
name: s.string(),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const auth = new Ultra().routes(input => ({
|
|
153
|
+
auth: {
|
|
154
|
+
// Schema for runtime input validation
|
|
155
|
+
login: input(LoginSchema)
|
|
156
|
+
// Schema for runtime output validation
|
|
157
|
+
.output(UserSchema)
|
|
158
|
+
.http()
|
|
159
|
+
.handler(({ input }) => {
|
|
160
|
+
// input is typed and validated as { name: string; password: string }
|
|
161
|
+
const user = { id: 1, name: input.name };
|
|
162
|
+
|
|
163
|
+
return user;
|
|
164
|
+
}),
|
|
165
|
+
}
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
const client = createHTTPClient<typeof auth>({
|
|
169
|
+
baseUrl: 'http://localhost:3000',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const user = await client.auth.login({ name: 'Alice', password: 'secret' }); // user is typed as { id: number; name: string }
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Difference between runtime validation and TypeScript types:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import * as z from 'zod';
|
|
179
|
+
|
|
180
|
+
const api = new Ultra().routes(input => ({
|
|
181
|
+
// Ultra checks input and output data
|
|
182
|
+
validated: input(z.object({ a: z.number(), b: z.number() }))
|
|
183
|
+
.output(z.number())
|
|
184
|
+
.http()
|
|
185
|
+
.handler(({ input }) => {
|
|
186
|
+
// input is typed and validated as { a: number; b: number }
|
|
187
|
+
return input.a + input.b;
|
|
188
|
+
}),
|
|
189
|
+
|
|
190
|
+
// You are confident in your types and don't want to waste CPU/memory on validation.
|
|
191
|
+
typed: input<{ a: number; b: number }>()
|
|
192
|
+
.output<number>()
|
|
193
|
+
.http()
|
|
194
|
+
.handler(({ input }) => {
|
|
195
|
+
// input is typed as { a: number; b: number } but NOT validated
|
|
196
|
+
return input.a + input.b;
|
|
197
|
+
}),
|
|
198
|
+
}));
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Context
|
|
202
|
+
|
|
203
|
+
Ultra provides context system that allows you to share data across your application.
|
|
204
|
+
You can extend the context with a function or value. Example from session module:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// session.ts
|
|
208
|
+
|
|
209
|
+
export function createSessionModule<S extends Record<string, SessionStoreFactory>>(config: SessionConfig<S>) {
|
|
210
|
+
// Create module
|
|
211
|
+
return new Ultra()
|
|
212
|
+
// deriveWS to add sessionId to WebSocket data object | run each socket connection | use for store data in WS connection
|
|
213
|
+
.deriveWS((context: HTTPContext) => ({ sessionId: Session.getOrCreateId(context.request, config) }))
|
|
214
|
+
// derive function add session instance to context | run each request
|
|
215
|
+
.derive(context => ({ session: new Session(config, context) }))
|
|
216
|
+
// Middleware to initiate and commit session on each request
|
|
217
|
+
.use(async ({ context, next }) => {
|
|
218
|
+
await context.session.initiate();
|
|
219
|
+
const response = await next();
|
|
220
|
+
await context.session.commit();
|
|
221
|
+
return response;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
You can add a static value for each request:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const app = new Ultra().derive({ appName: 'My Ultra App' });
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Core concepts
|
|
233
|
+
|
|
234
|
+
### Modules
|
|
235
|
+
|
|
236
|
+
Each module is a self-contained application.
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
// auth.ts
|
|
240
|
+
|
|
241
|
+
// This is a self-contained application. It declares all its dependencies.
|
|
242
|
+
const auth = new Ultra()
|
|
243
|
+
.use(cors) // Use CORS middleware
|
|
244
|
+
.use(session) // Use session module
|
|
245
|
+
.routes(input => ({
|
|
246
|
+
auth: {
|
|
247
|
+
login: input(s.schema({ login: s.string(), password: s.string() }))
|
|
248
|
+
.http()
|
|
249
|
+
.handler(({ input }) => {
|
|
250
|
+
// Handle login
|
|
251
|
+
}),
|
|
252
|
+
}
|
|
253
|
+
}));
|
|
254
|
+
|
|
255
|
+
// You can run it independently
|
|
256
|
+
auth.start();
|
|
257
|
+
|
|
258
|
+
// Or use it as a module in another application
|
|
259
|
+
const main = new Ultra()
|
|
260
|
+
.use(auth)
|
|
261
|
+
.start();
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
You can use modules as many times as you like.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
const moduleA = new Ultra();
|
|
268
|
+
|
|
269
|
+
const moduleB = new Ultra()
|
|
270
|
+
.use(moduleA); // Use first time
|
|
271
|
+
|
|
272
|
+
const moduleC = new Ultra()
|
|
273
|
+
.use(moduleB)
|
|
274
|
+
.use(moduleA); // Use second time
|
|
275
|
+
|
|
276
|
+
const mainApp = new Ultra()
|
|
277
|
+
.use(moduleA)
|
|
278
|
+
.use(moduleB)
|
|
279
|
+
.use(moduleC)
|
|
280
|
+
.start();
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
It may seem like modules will be duplicated and cause conflicts, but Ultra, under the hood, deduplicates everything that is connected to it.
|
|
284
|
+
|
|
285
|
+
This applies not only to modules:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
// Derive function
|
|
289
|
+
function requestIdDerive(context) {
|
|
290
|
+
console.log('Deriving!');
|
|
291
|
+
return { requestId: crypto.randomUUID() };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Middleware
|
|
295
|
+
async function logger({ next }) {
|
|
296
|
+
console.log('Request!');
|
|
297
|
+
return next();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Routes
|
|
301
|
+
function routes(input) {
|
|
302
|
+
return {
|
|
303
|
+
ping: input().http().handler(() => {
|
|
304
|
+
console.log('Handling ping!');
|
|
305
|
+
return 'pong';
|
|
306
|
+
}),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const a = new Ultra()
|
|
311
|
+
.derive(requestIdDerive)
|
|
312
|
+
.use(logger)
|
|
313
|
+
.routes(routes);
|
|
314
|
+
|
|
315
|
+
const b = new Ultra()
|
|
316
|
+
.derive(requestIdDerive)
|
|
317
|
+
.use(logger)
|
|
318
|
+
.routes(routes);
|
|
319
|
+
|
|
320
|
+
const app = new Ultra()
|
|
321
|
+
.derive(requestIdDerive)
|
|
322
|
+
.use(logger)
|
|
323
|
+
.use(a)
|
|
324
|
+
.use(b)
|
|
325
|
+
.routes(routes)
|
|
326
|
+
.start();
|
|
327
|
+
|
|
328
|
+
fetch('http://localhost:3000/ping'); // Printed: Deriving!, Request!, Handling ping!
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Type safety
|
|
332
|
+
|
|
333
|
+
Ultra provides end-to-end type safety for your server and clients.
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
import { Ultra } from '@sx3/ultra';
|
|
337
|
+
import { createSuperClient } from '@sx3/ultra/client';
|
|
338
|
+
|
|
339
|
+
const math = new Ultra().routes(input => ({
|
|
340
|
+
math: {
|
|
341
|
+
add: input<{ a: number; b: number }>()
|
|
342
|
+
.http()
|
|
343
|
+
.handler(({ input }) => {
|
|
344
|
+
// input is typed as { a: number; b: number }
|
|
345
|
+
return input.a + input.b;
|
|
346
|
+
}),
|
|
347
|
+
}
|
|
348
|
+
}));
|
|
349
|
+
|
|
350
|
+
const client = createSuperClient<typeof math>({/** ... */});
|
|
351
|
+
|
|
352
|
+
const result = await client.math.add({ a: 1, b: 2 }); // the result is automatically inferred as a number
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Protocol independence
|
|
356
|
+
|
|
357
|
+
Ultra strives to be independent of specific protocols. You simply call functions and the application decides how to send the data.
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
// clients.ts
|
|
361
|
+
import type { Server } from './server';
|
|
362
|
+
import { createHTTPClient, createSuperClient, createWebSocketClient } from '@sx3/ultra/client';
|
|
363
|
+
|
|
364
|
+
const http = createHTTPClient<Server>({
|
|
365
|
+
baseUrl: 'http://localhost:3000',
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
let socket = new WebSocket('ws://localhost:3000/ws');
|
|
369
|
+
const ws = createWebSocketClient<Server>({
|
|
370
|
+
socket: () => socket,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const api = createSuperClient<Server>({
|
|
374
|
+
// Transport picker function | if WebSocket is open, use it; otherwise, use HTTP
|
|
375
|
+
pick: (method: string, params: unknown, options?: any) => {
|
|
376
|
+
if (socket.readyState === WebSocket.OPEN) return ws;
|
|
377
|
+
return http;
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const users = await api.users.list(); // [{ id: 1, name: 'Alice' } ...]
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Currently only HTTP and WebSockets are supported.
|
|
385
|
+
|
|
386
|
+
## Built-in features
|
|
387
|
+
|
|
388
|
+
Ultra has several built-in features to make your life easier.
|
|
389
|
+
|
|
390
|
+
### CORS
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
import { Ultra } from '@sx3/ultra';
|
|
394
|
+
import { createCORSMiddleware } from '@sx3/ultra/cors';
|
|
395
|
+
|
|
396
|
+
const cors = createCORSMiddleware({
|
|
397
|
+
origin: ['http://localhost:5173'],
|
|
398
|
+
credentials: true,
|
|
399
|
+
// methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
400
|
+
// allowedHeaders: ['Content-Type', 'Authorization'],
|
|
401
|
+
// exposedHeaders: ['X-Custom-Header'],
|
|
402
|
+
// maxAge: 3600,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const app = new Ultra().use(cors); // Apply CORS middleware globally
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Sessions
|
|
409
|
+
|
|
410
|
+
Multiple session stores are supported: in-memory, Redis, and custom stores.
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
// session.ts
|
|
414
|
+
import { env } from '#app/env';
|
|
415
|
+
import { createSessionModule, defineConfig, MemorySessionStore, RedisSessionStore } from '@/sx3/ultra/session';
|
|
416
|
+
|
|
417
|
+
export const config = defineConfig({
|
|
418
|
+
// Name for cookie or prefix for redis key
|
|
419
|
+
name: 'session',
|
|
420
|
+
ttl: 3600, // 1 hour
|
|
421
|
+
store: 'redis',
|
|
422
|
+
secret: env.APP_KEY,
|
|
423
|
+
cookie: {
|
|
424
|
+
path: '/',
|
|
425
|
+
httpOnly: true,
|
|
426
|
+
secure: env.NODE_ENV === 'production',
|
|
427
|
+
sameSite: 'lax',
|
|
428
|
+
},
|
|
429
|
+
stores: {
|
|
430
|
+
redis: config => new RedisSessionStore(config, redis),
|
|
431
|
+
memory: config => new MemorySessionStore(config),
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
export const session = createSessionModule(config);
|
|
436
|
+
|
|
437
|
+
// server.ts
|
|
438
|
+
import { Ultra } from '@sx3/ultra';
|
|
439
|
+
import { session } from './session';
|
|
440
|
+
|
|
441
|
+
const app = new Ultra().use(session).routes(input => ({
|
|
442
|
+
profile: {
|
|
443
|
+
get: input().http().handler(({ context: { session } }) => {
|
|
444
|
+
// Access session data
|
|
445
|
+
session.get('user');
|
|
446
|
+
session.set('user', { id: 1, name: 'Alice' });
|
|
447
|
+
}),
|
|
448
|
+
},
|
|
449
|
+
})).start();
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Authentication
|
|
453
|
+
|
|
454
|
+
Auth module requires [session module](#sessions).
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
// auth.ts
|
|
458
|
+
import { Ultra } from '@sx3/ultra';
|
|
459
|
+
import { createAuthModule, defineConfig, SessionAuthProvider } from '@sx3/ultra/auth';
|
|
460
|
+
|
|
461
|
+
interface User {
|
|
462
|
+
name: string;
|
|
463
|
+
age: number;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const config = defineConfig<User>({
|
|
467
|
+
provider: 'session',
|
|
468
|
+
providers: {
|
|
469
|
+
session: context => new SessionAuthProvider<User>(context),
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
export const auth = createAuthModule<User>(config);
|
|
474
|
+
|
|
475
|
+
// server.ts
|
|
476
|
+
import { auth } from './auth';
|
|
477
|
+
import { session } from './session';
|
|
478
|
+
import { isAuthenticated, isGuest } from '@sx3/ultra/auth';
|
|
479
|
+
|
|
480
|
+
const app = new Ultra()
|
|
481
|
+
.use(session)
|
|
482
|
+
.use(auth)
|
|
483
|
+
.routes(input => ({
|
|
484
|
+
// Just example
|
|
485
|
+
auth: {
|
|
486
|
+
login: input(LoginSchema)
|
|
487
|
+
.output(UserSchema)
|
|
488
|
+
.http()
|
|
489
|
+
.use(isGuest)
|
|
490
|
+
.handler(async ({ input, context }) => {
|
|
491
|
+
// ... check credentials logic
|
|
492
|
+
// then
|
|
493
|
+
await context.auth.login(user);
|
|
494
|
+
return user;
|
|
495
|
+
}),
|
|
496
|
+
|
|
497
|
+
logout: input()
|
|
498
|
+
.http()
|
|
499
|
+
.use(isAuthenticated)
|
|
500
|
+
.handler(({ context }) => context.auth.logout()),
|
|
501
|
+
|
|
502
|
+
profile: input().use(isAuthenticated).http().handler(({ context }) => context.auth.user!),
|
|
503
|
+
}
|
|
504
|
+
}))
|
|
505
|
+
.start();
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Crypto
|
|
509
|
+
|
|
510
|
+
Crypto functions are available [here](https://github.com/SX-3/ultra/blob/main/src/crypto.ts).
|
|
511
|
+
|
|
512
|
+
Inspired by [Elysia](https://elysiajs.com/) and [oRPC](https://orpc.dev/) powered by [Bun](https://bun.sh).
|
package/dist/auth.d.mts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { n as DefaultSocketData, t as BaseContext } from "./context-ChCsZh7S.mjs";
|
|
2
|
+
import { o as Promisable, r as JSONObject } from "./types-Cn69QrjS.mjs";
|
|
3
|
+
import "./http-BqWCMASL.mjs";
|
|
4
|
+
import { t as Middleware } from "./middleware-COKreBGP.mjs";
|
|
5
|
+
import { ProceduresMap, Ultra } from "./ultra.mjs";
|
|
6
|
+
import { Session } from "./session.mjs";
|
|
7
|
+
|
|
8
|
+
//#region src/auth.d.ts
|
|
9
|
+
interface AuthProvider<User> {
|
|
10
|
+
user: User | null;
|
|
11
|
+
check: () => Promisable<boolean>;
|
|
12
|
+
login: (user: User) => Promisable<void>;
|
|
13
|
+
logout: () => Promisable<void>;
|
|
14
|
+
setUser: (user: User) => Promisable<void>;
|
|
15
|
+
}
|
|
16
|
+
type AuthProviderFactory<User = any> = (context: BaseContext) => AuthProvider<User>;
|
|
17
|
+
interface AuthConfig<P extends Record<string, AuthProviderFactory> = Record<string, AuthProviderFactory>> {
|
|
18
|
+
provider: keyof P;
|
|
19
|
+
providers: P;
|
|
20
|
+
}
|
|
21
|
+
type AuthContext<User extends JSONObject> = BaseContext & {
|
|
22
|
+
session: Session;
|
|
23
|
+
auth: Auth<User>;
|
|
24
|
+
};
|
|
25
|
+
declare function defineConfig<User extends JSONObject, P extends Record<string, AuthProviderFactory<User>> = Record<string, AuthProviderFactory<User>>>(config: AuthConfig<P>): AuthConfig<P>;
|
|
26
|
+
declare function createAuthModule<User extends JSONObject, P extends Record<string, AuthProviderFactory<User>> = Record<string, AuthProviderFactory<User>>>(config: AuthConfig<P>): Ultra<ProceduresMap, BaseContext & {
|
|
27
|
+
auth: Auth<User, P>;
|
|
28
|
+
}, DefaultSocketData>;
|
|
29
|
+
declare const isAuthenticated: Middleware<unknown, unknown, AuthContext<JSONObject>>;
|
|
30
|
+
declare const isGuest: Middleware<unknown, unknown, AuthContext<JSONObject>>;
|
|
31
|
+
declare class Auth<User extends JSONObject, Providers extends Record<string, AuthProviderFactory<User>> = Record<string, AuthProviderFactory<User>>> {
|
|
32
|
+
protected readonly config: AuthConfig<Providers>;
|
|
33
|
+
protected readonly context: BaseContext;
|
|
34
|
+
protected readonly usingProvider: keyof Providers;
|
|
35
|
+
protected readonly providerCache: Map<keyof Providers, AuthProvider<User>>;
|
|
36
|
+
constructor(config: AuthConfig<Providers>, context: BaseContext, provider?: keyof Providers, providerCache?: Map<keyof Providers, AuthProvider<User>>);
|
|
37
|
+
use(provider: keyof Providers): Auth<User, Providers>;
|
|
38
|
+
get user(): User | null;
|
|
39
|
+
check(): Promisable<boolean>;
|
|
40
|
+
login(user: User): Promisable<void>;
|
|
41
|
+
logout(): Promisable<void>;
|
|
42
|
+
setUser(user: User): Promisable<void>;
|
|
43
|
+
protected get provider(): AuthProvider<User>;
|
|
44
|
+
}
|
|
45
|
+
declare class SessionAuthProvider<User extends JSONObject> implements AuthProvider<User> {
|
|
46
|
+
protected readonly context: AuthContext<User>;
|
|
47
|
+
protected readonly sessionKey: string;
|
|
48
|
+
constructor(context: AuthContext<User>, sessionKey?: string);
|
|
49
|
+
get user(): User;
|
|
50
|
+
check(): boolean;
|
|
51
|
+
login(user: User): void;
|
|
52
|
+
logout(): void;
|
|
53
|
+
setUser(user: User): void;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { Auth, AuthContext, AuthProvider, SessionAuthProvider, createAuthModule, defineConfig, isAuthenticated, isGuest };
|
package/dist/auth.mjs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { r as UnauthorizedError } from "./error-CII1zMOR.mjs";
|
|
2
|
+
import { Ultra } from "./ultra.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/auth.ts
|
|
5
|
+
function defineConfig(config) {
|
|
6
|
+
return config;
|
|
7
|
+
}
|
|
8
|
+
function createAuthModule(config) {
|
|
9
|
+
return new Ultra().derive((context) => ({ auth: new Auth(config, context) }));
|
|
10
|
+
}
|
|
11
|
+
const isAuthenticated = async (options) => {
|
|
12
|
+
if (!await options.context.auth.check()) return new UnauthorizedError();
|
|
13
|
+
return options.next();
|
|
14
|
+
};
|
|
15
|
+
const isGuest = async (options) => {
|
|
16
|
+
if (await options.context.auth.check()) return new UnauthorizedError();
|
|
17
|
+
return options.next();
|
|
18
|
+
};
|
|
19
|
+
var Auth = class Auth {
|
|
20
|
+
config;
|
|
21
|
+
context;
|
|
22
|
+
usingProvider;
|
|
23
|
+
providerCache;
|
|
24
|
+
constructor(config, context, provider = config.provider, providerCache = /* @__PURE__ */ new Map()) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.context = context;
|
|
27
|
+
this.usingProvider = provider;
|
|
28
|
+
this.providerCache = providerCache;
|
|
29
|
+
}
|
|
30
|
+
use(provider) {
|
|
31
|
+
return new Auth(this.config, this.context, provider, this.providerCache);
|
|
32
|
+
}
|
|
33
|
+
get user() {
|
|
34
|
+
return this.provider.user;
|
|
35
|
+
}
|
|
36
|
+
check() {
|
|
37
|
+
return this.provider.check();
|
|
38
|
+
}
|
|
39
|
+
login(user) {
|
|
40
|
+
return this.provider.login(user);
|
|
41
|
+
}
|
|
42
|
+
logout() {
|
|
43
|
+
return this.provider.logout();
|
|
44
|
+
}
|
|
45
|
+
setUser(user) {
|
|
46
|
+
return this.provider.setUser(user);
|
|
47
|
+
}
|
|
48
|
+
get provider() {
|
|
49
|
+
const cached = this.providerCache.get(this.usingProvider);
|
|
50
|
+
if (cached) return cached;
|
|
51
|
+
const providerFactory = this.config.providers[this.usingProvider];
|
|
52
|
+
if (!providerFactory) throw new Error(`Auth provider "${String(this.usingProvider)}" is not configured.`);
|
|
53
|
+
const instance = providerFactory(this.context);
|
|
54
|
+
this.providerCache.set(this.usingProvider, instance);
|
|
55
|
+
return instance;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var SessionAuthProvider = class {
|
|
59
|
+
context;
|
|
60
|
+
sessionKey;
|
|
61
|
+
constructor(context, sessionKey = "user") {
|
|
62
|
+
this.context = context;
|
|
63
|
+
this.sessionKey = sessionKey;
|
|
64
|
+
}
|
|
65
|
+
get user() {
|
|
66
|
+
return this.context.session.get(this.sessionKey) || null;
|
|
67
|
+
}
|
|
68
|
+
check() {
|
|
69
|
+
return this.user !== null;
|
|
70
|
+
}
|
|
71
|
+
login(user) {
|
|
72
|
+
this.setUser(user);
|
|
73
|
+
this.context.session.regenerate();
|
|
74
|
+
}
|
|
75
|
+
logout() {
|
|
76
|
+
this.context.session.delete(this.sessionKey);
|
|
77
|
+
}
|
|
78
|
+
setUser(user) {
|
|
79
|
+
this.context.session.set(this.sessionKey, user);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
export { Auth, SessionAuthProvider, createAuthModule, defineConfig, isAuthenticated, isGuest };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import "./context-ChCsZh7S.mjs";
|
|
2
|
+
import "./http-BqWCMASL.mjs";
|
|
3
|
+
import { i as Procedure } from "./middleware-COKreBGP.mjs";
|
|
4
|
+
import { ProceduresMap, Ultra } from "./ultra.mjs";
|
|
5
|
+
|
|
6
|
+
//#region src/client.d.ts
|
|
7
|
+
type GetProcedures<T> = T extends Ultra<infer P> ? P : never;
|
|
8
|
+
type BuildProcedure<P, CO> = P extends Procedure<infer I, infer O, any> ? (undefined extends I ? (input?: I, callOptions?: CO) => Promise<O> : (input: I, callOptions?: CO) => Promise<O>) : never;
|
|
9
|
+
type BuildClient<P extends ProceduresMap, CO> = { [K in keyof P]: P[K] extends Procedure<any, any, any> ? BuildProcedure<P[K], CO> : P[K] extends ProceduresMap ? BuildClient<P[K], CO> : never };
|
|
10
|
+
type Invoke<CO> = (method: string, params: unknown, callOptions?: CO) => Promise<unknown>;
|
|
11
|
+
interface HTTPClientOptions extends Omit<RequestInit, 'body'> {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
timeout?: number;
|
|
14
|
+
}
|
|
15
|
+
declare function createHTTPClient<B extends Ultra>(clientOptions: HTTPClientOptions): BuildClient<GetProcedures<B>, Partial<HTTPClientOptions>>;
|
|
16
|
+
interface WebSocketClientOptions {
|
|
17
|
+
socket: () => WebSocket;
|
|
18
|
+
timeout?: number;
|
|
19
|
+
}
|
|
20
|
+
declare function createWebSocketClient<B extends Ultra>(options: WebSocketClientOptions): BuildClient<GetProcedures<B>, Partial<WebSocketClientOptions>>;
|
|
21
|
+
type ClientsCallsParams = Partial<WebSocketClientOptions> | Partial<HTTPClientOptions>;
|
|
22
|
+
interface SuperClientOptions<B extends Ultra> {
|
|
23
|
+
pick: (...args: Parameters<Invoke<ClientsCallsParams>>) => BuildClient<GetProcedures<B>, ClientsCallsParams>;
|
|
24
|
+
}
|
|
25
|
+
declare function createSuperClient<B extends Ultra>(options: SuperClientOptions<B>): BuildClient<GetProcedures<B>, ClientsCallsParams>;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { createHTTPClient, createSuperClient, createWebSocketClient };
|