@hotfusion/modeller 0.0.2 → 0.0.3

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/docs/ERRORS.md ADDED
@@ -0,0 +1,90 @@
1
+ # Errors
2
+
3
+ All errors thrown by Modeller are plain objects with a `code` field. They aren't `Error` instances — catch them as plain objects and branch on `code`.
4
+
5
+ > [← Back to README](../README.md)
6
+
7
+ ---
8
+
9
+ ## Error codes
10
+
11
+ | Code | Source | Extra fields |
12
+ |---------------------|-------------------------------------------------|-------------------------|
13
+ | `VALIDATION_ERROR` | AJV failed on `insert` / `update` / event args. | `errors` (AJV errors[]) |
14
+ | `UNIQUE_VIOLATION` | A `unique` field collided. | `field` |
15
+ | `METHOD_NOT_FOUND` | `call('id', ...)` with unregistered id. | `id` |
16
+ | `UPLOAD_NOT_FOUND` | `callUpload('id', ...)` with unregistered id. | `id` |
17
+ | `STREAM_NOT_FOUND` | `callStream('id', ...)` with unregistered id. | `id` |
18
+ | `EVENT_NOT_FOUND` | `subscription.dispatch('id', ...)` mismatch. | `id` |
19
+
20
+ ---
21
+
22
+ ## Catching
23
+
24
+ ```ts
25
+ try {
26
+ await users.insert({ email: 'bad' });
27
+ } catch (err) {
28
+ switch (err.code) {
29
+ case 'VALIDATION_ERROR':
30
+ console.log('AJV errors:', err.errors);
31
+ break;
32
+ case 'UNIQUE_VIOLATION':
33
+ console.log('duplicate field:', err.field);
34
+ break;
35
+ default:
36
+ throw err;
37
+ }
38
+ }
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Validation error shape
44
+
45
+ `VALIDATION_ERROR` carries the raw AJV error array on `errors`. Each entry has `instancePath`, `keyword`, `message`, `params`, and `schemaPath`.
46
+
47
+ ```ts
48
+ {
49
+ code: 'VALIDATION_ERROR',
50
+ errors: [
51
+ {
52
+ instancePath : '/email',
53
+ keyword : 'format',
54
+ message : 'must match format "email"',
55
+ params : { format: 'email' },
56
+ schemaPath : '#/properties/email/format'
57
+ }
58
+ ]
59
+ }
60
+ ```
61
+
62
+ `ajv-errors` is enabled, so per-field custom messages declared via `errorMessage` in the schema show up in `message`.
63
+
64
+ ---
65
+
66
+ ## Hooks and errors
67
+
68
+ A non-scheduled hook that throws aborts the operation. The thrown value is logged at `error` level via the [logger](./MODEL.md#logging) and re-thrown to the caller. Throw a plain object with a `code` to keep error handling consistent:
69
+
70
+ ```ts
71
+ users.hook({
72
+ id: 'check-quota',
73
+ on: 'before:insert',
74
+ callback: async ({ data }) => {
75
+ if (await overQuota(data)) throw { code: 'QUOTA_EXCEEDED' };
76
+ }
77
+ });
78
+ ```
79
+
80
+ Scheduled hooks (those with `schedule: <ms>`) are fire-and-forget. Errors from them are logged but not propagated.
81
+
82
+ ---
83
+
84
+ ## Workers and errors
85
+
86
+ Worker handler errors are caught and logged at `error` level. They never crash the interval — the worker keeps ticking. If you want a worker to stop on failure, call `model.stop('worker-id')` from inside the handler's catch block.
87
+
88
+ ---
89
+
90
+ > **Related:** [Core →](./CORE.md) · [Model →](./MODEL.md)
package/docs/MODEL.md ADDED
@@ -0,0 +1,296 @@
1
+ # Model Layer
2
+
3
+ `Model extends Core`. Everything from the [Core layer](./CORE.md) is available on a `Model` instance — `insert`, `update`, `delete`, `get`, `list`, `find`, `getSchema`, `pull`. On top of that, `Model` adds the behavior surface: hooks, workers, methods, uploads, streams, events, subscriptions, logging, and extensions.
4
+
5
+ > [← Back to README](../README.md)
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [Hooks](#hooks)
12
+ - [Workers](#workers)
13
+ - [Methods](#methods)
14
+ - [Uploads](#uploads)
15
+ - [Streams](#streams)
16
+ - [Events &amp; Subscriptions](#events--subscriptions)
17
+ - [Internal Event Emitter](#internal-event-emitter)
18
+ - [Extensions](#extensions)
19
+ - [Logging](#logging)
20
+
21
+ ---
22
+
23
+ ## Hooks
24
+
25
+ Hooks run before or after a CRUD operation. They can short-circuit by throwing, mutate the payload, or perform side effects. Both sync and async callbacks are supported.
26
+
27
+ ```ts
28
+ users.hook({
29
+ id : 'normalize-email',
30
+ on : 'before:insert',
31
+ callback : async ({ data }) => {
32
+ if (data?.email) data.email = data.email.toLowerCase();
33
+ }
34
+ });
35
+
36
+ users.hook({
37
+ id : 'audit',
38
+ on : ['after:insert', 'after:update', 'after:delete'],
39
+ callback : ({ operation, key, data }) => audit.log(operation, key, data)
40
+ });
41
+
42
+ users.unhook('normalize-email');
43
+ ```
44
+
45
+ ### Hook events
46
+
47
+ | Event | Payload fields |
48
+ |-------------------|-----------------------------------------------|
49
+ | `before:insert` | `operation`, `timing`, `data` |
50
+ | `after:insert` | `operation`, `timing`, `data`, `key: { _id }` |
51
+ | `before:update` | `operation`, `timing`, `key`, `data` |
52
+ | `after:update` | `operation`, `timing`, `key`, `data` |
53
+ | `before:delete` | `operation`, `timing`, `key` |
54
+ | `after:delete` | `operation`, `timing`, `key`, `data` |
55
+ | `before:list` | `operation`, `timing`, `data` (= query) |
56
+ | `after:list` | `operation`, `timing`, `data` (= query) |
57
+
58
+ `get` and `find` do **not** fire hooks.
59
+
60
+ ### Scheduled hooks
61
+
62
+ `schedule: <ms>` defers execution via `setTimeout`. Scheduled hooks are fire-and-forget — they don't block the operation and their errors aren't propagated.
63
+
64
+ ```ts
65
+ users.hook({
66
+ id : 'welcome-email',
67
+ on : 'after:insert',
68
+ schedule : 5_000,
69
+ callback : ({ data }) => mailer.send(data.email, 'welcome')
70
+ });
71
+ ```
72
+
73
+ ### Throwing from a hook
74
+
75
+ A non-scheduled hook that throws aborts the operation. The error is logged at `error` level and re-thrown to the caller.
76
+
77
+ ---
78
+
79
+ ## Workers
80
+
81
+ Workers are interval-based background tasks bound to the model. They can be **named** (managed individually) or **unsigned** (managed in bulk).
82
+
83
+ ```ts
84
+ users.worker({
85
+ id : 'sync',
86
+ interval : 30_000,
87
+ immediate : true, // run once on start, default true
88
+ handler : async (model) => { await model.pull(); }
89
+ });
90
+
91
+ users.worker({ interval: 60_000, handler: () => cleanup() }); // unsigned
92
+ ```
93
+
94
+ ### Lifecycle
95
+
96
+ ```ts
97
+ users.start(); // start all (named + unsigned)
98
+ users.start('sync'); // start a specific named worker
99
+ users.stop(); // stop all
100
+ users.stop('sync'); // stop a specific named worker
101
+ users.isRunning('sync'); // → boolean
102
+ ```
103
+
104
+ Errors thrown inside a worker handler are caught and logged — they never crash the interval.
105
+
106
+ ---
107
+
108
+ ## Methods
109
+
110
+ Methods are named, schemaless RPC-style operations. Use them for anything that doesn't fit the CRUD shape.
111
+
112
+ ```ts
113
+ users.method({
114
+ id : 'resetPassword',
115
+ handler : async ({ email, newPassword }, model, ctx) => {
116
+ const u = await model.get({ email });
117
+ if (!u) throw { code: 'NOT_FOUND' };
118
+ return model.update({ _id: u._id }, { password: newPassword });
119
+ }
120
+ });
121
+
122
+ await users.call('resetPassword', { email, newPassword }, ctx);
123
+ users.getMethods(); // → ['resetPassword']
124
+ ```
125
+
126
+ `call` throws `{ code: 'METHOD_NOT_FOUND', id }` if the method isn't registered.
127
+
128
+ ---
129
+
130
+ ## Uploads
131
+
132
+ Uploads receive a `Buffer` plus a `fields` map (typically multipart form fields).
133
+
134
+ ```ts
135
+ users.upload({
136
+ id : 'avatar',
137
+ handler : async (file, fields, model) => {
138
+ const path = await storage.put(file, fields.filename);
139
+ return model.update({ _id: fields.userId }, { avatar: path });
140
+ }
141
+ });
142
+
143
+ await users.callUpload('avatar', buffer, { userId, filename: 'me.png' });
144
+ users.getUploads(); // → ['avatar']
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Streams
150
+
151
+ Streams are similar to uploads but expect chunked `Buffer` data with arbitrary `meta`.
152
+
153
+ ```ts
154
+ users.stream({
155
+ id : 'import',
156
+ handler : async (chunk, meta, model) => {
157
+ for (const row of parseCsv(chunk)) await model.insert(row);
158
+ }
159
+ });
160
+
161
+ await users.callStream('import', chunk, { source: 'crm' });
162
+ users.getStreams(); // → ['import']
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Events & Subscriptions
168
+
169
+ Events are named, schema-validated handlers that clients **subscribe to**. Where `method` is one-shot RPC, events are designed for fan-out to multiple subscribers, scoped by query.
170
+
171
+ ### Defining an event
172
+
173
+ ```ts
174
+ users.event({
175
+ id : 'roleChanged',
176
+ schema : {
177
+ type : 'object',
178
+ required : ['_id', 'role'],
179
+ properties: {
180
+ _id : { type: 'string' },
181
+ role : { type: 'string', enum: ['user', 'admin'] }
182
+ }
183
+ },
184
+ handler: async ({ _id, role }, model, ctx) => {
185
+ return model.update({ _id }, { role });
186
+ }
187
+ });
188
+ ```
189
+
190
+ If `schema` is set, args are validated before the handler runs. Validation errors throw `{ code: 'VALIDATION_ERROR', errors }`.
191
+
192
+ ### Subscribing
193
+
194
+ A subscription is keyed by a normalized query — same query keys/values produce the same subscription instance.
195
+
196
+ ```ts
197
+ const { subscription, subId } = users.subscribe({ role: 'admin' });
198
+
199
+ subscription.on('publish', payload => {
200
+ console.log('admin update:', payload);
201
+ });
202
+
203
+ // shorthand — returns just the subscription
204
+ const sub = users.subscription({ role: 'admin' });
205
+ ```
206
+
207
+ ### Dispatching an event
208
+
209
+ `dispatch` validates args (if a schema is set) and runs the matching event handler.
210
+
211
+ ```ts
212
+ await sub.dispatch('roleChanged', { _id, role: 'admin' }, ctx);
213
+ ```
214
+
215
+ Throws `{ code: 'EVENT_NOT_FOUND', id }` if the event isn't registered.
216
+
217
+ ### Publishing to subscribers
218
+
219
+ `publish` is your fan-out hook — it emits to every listener attached via `on('publish', ...)` for that subscription. The transport layer ([Server](./SERVER.md)) wires this into a Socket.IO room so browser clients receive it automatically.
220
+
221
+ ```ts
222
+ sub.publish({ type: 'role-changed', _id, role: 'admin' });
223
+ ```
224
+
225
+ A common pattern is calling `publish` from inside an `after:update` hook to notify everyone watching that slice of the model. See [Patterns](./PATTERNS.md).
226
+
227
+ ### Inspection / cleanup
228
+
229
+ ```ts
230
+ sub.getEvents(); // → ['roleChanged']
231
+ users.unsubscribe({ role: 'admin' });
232
+ ```
233
+
234
+ ---
235
+
236
+ ## Internal Event Emitter
237
+
238
+ Separate from the subscription system, every model is also an `EventEmitter`. CRUD operations emit on this emitter directly — useful for in-process listeners that don't need query scoping.
239
+
240
+ ```ts
241
+ users.on('insert', doc => log('inserted', doc));
242
+ users.on('update', ({ key, data }) => log('updated', key, data));
243
+ users.on('delete', result => log('deleted', result));
244
+ users.on('log', entry => sink.write(entry));
245
+ ```
246
+
247
+ `on`, `off`, `once`, `emit` are all available. Built-in events: `insert`, `update`, `delete`, `log`.
248
+
249
+ ---
250
+
251
+ ## Extensions
252
+
253
+ Pass `extensions: [...]` in the constructor options to mount sub-models as named properties on the parent. The mount key is `extension.id`.
254
+
255
+ ```ts
256
+ const ssh = new Model('ssh', sshSchema);
257
+ const users = new Model('users', userSchema, { extensions: [ssh] });
258
+
259
+ users.ssh.insert({ host: '...', port: 22, username: 'root' });
260
+ users.getExtensions(); // → [Model('ssh')]
261
+ ```
262
+
263
+ This is how Modeller composes namespaces — a `system` model with `system.ssh`, `system.docker`, `system.firewall` extensions, all addressable as `system.ssh.insert(...)`.
264
+
265
+ ---
266
+
267
+ ## Logging
268
+
269
+ A built-in logger surfaces hook, worker, method, event, and CRUD activity.
270
+
271
+ ```ts
272
+ const users = new Model('users', schema, {
273
+ level : 'debug', // 'debug' | 'info' | 'warn' | 'error' | 'silent'
274
+ timestamp : true,
275
+ features : { hook: true, worker: true, method: true, event: false, crud: true },
276
+ onEntry : entry => myLogger.log(entry)
277
+ });
278
+ ```
279
+
280
+ Or supply a custom adapter:
281
+
282
+ ```ts
283
+ { logAdapter: { log: entry => myStore.append(entry) } }
284
+ ```
285
+
286
+ Listen on the model itself:
287
+
288
+ ```ts
289
+ users.on('log', entry => sink.write(entry));
290
+ ```
291
+
292
+ Each entry includes `level`, `feature`, `modelId`, `message`, `callSite`, `timestamp`, and optional `data`. `initial` is `true` for one-shot registration messages (e.g. "Hook registered"), `false` for runtime activity.
293
+
294
+ ---
295
+
296
+ > **Next:** [Server layer →](./SERVER.md)
@@ -0,0 +1,182 @@
1
+ # Common Patterns
2
+
3
+ Recipes that show the [Core → Model → Server](../README.md) chain in action. Each pattern is small, self-contained, and reflects how the framework is meant to be composed.
4
+
5
+ > [← Back to README](../README.md)
6
+
7
+ ---
8
+
9
+ ## Publish on update
10
+
11
+ Notify every client subscribed to a slice of a model whenever a document in that slice changes. The `after:update` hook is the natural seam.
12
+
13
+ ```ts
14
+ users.hook({
15
+ id: 'broadcast',
16
+ on: 'after:update',
17
+ callback: ({ key, data }) => {
18
+ users.subscription({ _id: key._id }).publish({ type: 'updated', data });
19
+ }
20
+ });
21
+ ```
22
+
23
+ The [Server layer](./SERVER.md) wires the subscription into a Socket.IO room, so every browser client subscribed to `{ _id }` receives the payload automatically.
24
+
25
+ ---
26
+
27
+ ## FK validation across models
28
+
29
+ Use the async `model` keyword to require that a field references an existing document in another model.
30
+
31
+ ```ts
32
+ const orders = new Model('orders', {
33
+ type: 'object',
34
+ properties: {
35
+ userId: { type: 'string', model: 'users.email' }
36
+ }
37
+ }, { schemes: [users.getSchema()] });
38
+ ```
39
+
40
+ `Validator` rejects an `insert` whose `userId` doesn't exist in the `users` model. See [Validator](./CORE.md#validator).
41
+
42
+ ---
43
+
44
+ ## Composing a system root via extensions
45
+
46
+ `extensions` mounts sub-models as named properties on the parent — addressable as `parent.childId.method(...)`.
47
+
48
+ ```ts
49
+ const ssh = new Model('ssh', sshSchema);
50
+ const docker = new Model('docker', dockerSchema);
51
+ const firewall = new Model('firewall', firewallSchema);
52
+
53
+ const system = new Model('system', systemSchema, {
54
+ extensions: [ssh, docker, firewall]
55
+ });
56
+
57
+ await system.ssh.insert({ host: '...', port: 22, username: 'root' });
58
+ await system.firewall.list();
59
+ ```
60
+
61
+ This pattern is how Modeller composes namespaces. Each extension keeps its own hooks, workers, methods, events, and schema — they aren't merged, just mounted.
62
+
63
+ See [Extensions](./MODEL.md#extensions).
64
+
65
+ ---
66
+
67
+ ## Background sync worker
68
+
69
+ Reconcile in-memory state with an external source on an interval.
70
+
71
+ ```ts
72
+ firewall.worker({
73
+ id : 'sync-rules',
74
+ interval : 30_000,
75
+ handler : async (model) => {
76
+ const live = await readLiveUFW();
77
+ for (const rule of live) {
78
+ const existing = await model.get({ id: rule.id });
79
+ if (!existing) await model.insert(rule);
80
+ }
81
+ }
82
+ });
83
+
84
+ firewall.start('sync-rules');
85
+ ```
86
+
87
+ Errors inside the handler are logged but never crash the interval. See [Workers](./MODEL.md#workers).
88
+
89
+ ---
90
+
91
+ ## Quota / authorization in `before:insert`
92
+
93
+ Throw from a `before:` hook to abort an operation.
94
+
95
+ ```ts
96
+ users.hook({
97
+ id: 'check-quota',
98
+ on: 'before:insert',
99
+ callback: async ({ data }) => {
100
+ if (await overQuota(data.tenantId)) {
101
+ throw { code: 'QUOTA_EXCEEDED' };
102
+ }
103
+ }
104
+ });
105
+ ```
106
+
107
+ The thrown object propagates back to the caller of `insert()`. See [Errors](./ERRORS.md).
108
+
109
+ ---
110
+
111
+ ## Schedule a side effect after insert
112
+
113
+ Send a welcome email five seconds after a user is created — without blocking the insert.
114
+
115
+ ```ts
116
+ users.hook({
117
+ id : 'welcome-email',
118
+ on : 'after:insert',
119
+ schedule : 5_000,
120
+ callback : ({ data }) => mailer.send(data.email, 'welcome')
121
+ });
122
+ ```
123
+
124
+ `schedule` makes the hook fire-and-forget via `setTimeout`. Errors from scheduled hooks are logged but don't propagate.
125
+
126
+ ---
127
+
128
+ ## Persist with an adapter, hydrate on boot
129
+
130
+ Wire an `Adapter` so writes are durable, then `pull()` once at startup.
131
+
132
+ ```ts
133
+ class FileAdapter extends Adapter {
134
+ async sync(id, doc) { await fs.writeFile(`./data/${id}.json`, JSON.stringify(doc)); }
135
+ async pull() { return JSON.parse(await fs.readFile('./data/users.json', 'utf8')); }
136
+ }
137
+
138
+ const users = new Model('users', schema, { adapter: FileAdapter });
139
+ await users.pull(); // hydrate before serving traffic
140
+ ```
141
+
142
+ See [Adapter](./CORE.md#adapter).
143
+
144
+ ---
145
+
146
+ ## Method as RPC, event as fan-out
147
+
148
+ Two different shapes for "do something":
149
+
150
+ - **Method** — one caller, one return value. Use for actions like `resetPassword`, `regenerateApiKey`.
151
+ - **Event** — one or many callers via a subscription, validated args, multi-subscriber fan-out. Use when the action should also notify watchers.
152
+
153
+ ```ts
154
+ // method — RPC
155
+ users.method({ id: 'resetPassword', handler: async (...) => { /* ... */ } });
156
+
157
+ // event — broadcasts via subscription
158
+ users.event({ id: 'roleChanged', schema, handler: async (...) => { /* ... */ } });
159
+ ```
160
+
161
+ If in doubt, start with a method. Promote to an event only when more than one consumer needs to know it happened.
162
+
163
+ ---
164
+
165
+ ## Listening in-process vs over the wire
166
+
167
+ Two emitters live on every model. Use the right one for the job:
168
+
169
+ - **`model.on('insert' | 'update' | 'delete' | 'log', ...)`** — in-process, no query scoping. Use for audit logs, metrics, internal indexers.
170
+ - **`model.subscription(query).on('publish', ...)`** — query-scoped fan-out, designed to be bridged to clients via the [Server layer](./SERVER.md).
171
+
172
+ ```ts
173
+ // in-process: every write
174
+ users.on('update', ({ key, data }) => metrics.increment('users.updated'));
175
+
176
+ // per-query: clients listening for admin changes
177
+ users.subscription({ role: 'admin' }).on('publish', payload => doSomething(payload));
178
+ ```
179
+
180
+ ---
181
+
182
+ > **Related:** [Model →](./MODEL.md) · [Errors →](./ERRORS.md)
package/docs/SERVER.md ADDED
@@ -0,0 +1,88 @@
1
+ # Server Layer
2
+
3
+ `Server` is a thin transport that exposes models over HTTP and WebSocket. The package abstracts the underlying Koa + Socket.IO + OIDC + busboy machinery — you don't interact with it directly.
4
+
5
+ > [← Back to README](../README.md)
6
+
7
+ ---
8
+
9
+ ## The mental model
10
+
11
+ - **Models in, HTTP + WS out.** Methods, uploads, streams, and events become endpoints.
12
+ - **Subscriptions become rooms.** `subscription.publish(payload)` reaches every connected client subscribed to that query.
13
+ - **A `/@metadata` endpoint** advertises every model, scope, operation, and event so clients can build typed proxies at runtime.
14
+ - **Auth is OIDC + JWT.** Routes can opt in with `protected: true`.
15
+
16
+ ---
17
+
18
+ ## Quick example
19
+
20
+ ```ts
21
+ import { Server, Model } from '@hotfusion/modeller';
22
+
23
+ const server = new Server(3030, { domain: 'api', secret: process.env.SECRET });
24
+
25
+ server.route({
26
+ method : 'post',
27
+ path : '/system/ping',
28
+ callback : async (ctx) => ({ ok: true, ts: Date.now() })
29
+ });
30
+
31
+ server.start();
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Public methods
37
+
38
+ | Method | Purpose |
39
+ |-------------------------------------------------|-------------------------------------------------------|
40
+ | `route({ method, path, callback, protected? })` | Register a custom HTTP route. |
41
+ | `upload(path, handler)` | Multipart form-data endpoint, parsed into a Buffer. |
42
+ | `stream(handler)` | Single global handler for chunked socket uploads with resume. |
43
+ | `setOIDCProviders(path, providers)` | Wire OIDC providers under a base path. |
44
+ | `start()` | Bind to the port, mount routes, start Socket.IO. |
45
+
46
+ The full HTTP/WS surface is generated from your models — you don't need to wire each operation manually.
47
+
48
+ ---
49
+
50
+ ## Server events
51
+
52
+ ```ts
53
+ server.on('mounted', (s) => console.log('listening'));
54
+ server.on('connection', ({ socket }) => /* socket.io connection */);
55
+ server.on('request', ({ ctx, flags }) => /* every HTTP request */);
56
+ server.on('authenticated', ({ profile, provider, ctx, middleware }) => /* OIDC success */);
57
+ server.on('provider', ({ token, sid, session }) => /* token issued */);
58
+ server.on('uploaded', ({ file, buffer, resolve }) => resolve({ /* meta */ }));
59
+ ```
60
+
61
+ | Event | When it fires |
62
+ |-----------------|------------------------------------------------------------|
63
+ | `mounted` | After `start()` has bound the port and mounted routes. |
64
+ | `connection` | A new Socket.IO client connects. |
65
+ | `request` | Every incoming HTTP request, with `protected` flag. |
66
+ | `authenticated` | OIDC profile successfully verified. |
67
+ | `provider` | A provider has issued a token; session is established. |
68
+ | `uploaded` | A streamed upload finished assembling. Resolve with meta. |
69
+
70
+ ---
71
+
72
+ ## Auth
73
+
74
+ `{ protected: true }` on a route requires a valid `Authorization: Bearer <jwt>` header. The token is verified against the secret passed to the constructor (or auto-generated via `KeyGen.getSystemSecret()` if omitted).
75
+
76
+ `setOIDCProviders` wires multiple OIDC providers under a base path. The server handles the redirect, callback, token exchange, and session creation. Listen to `authenticated` and `provider` to react to login.
77
+
78
+ ---
79
+
80
+ ## Scope
81
+
82
+ This package is meant to be used as an abstraction. The Server layer intentionally keeps a narrow surface so consumers don't need to know Koa, Socket.IO, OIDC client config, or busboy internals.
83
+
84
+ If you need to dig deeper — custom middleware, custom socket rooms, OIDC introspection — read `src/server.ts` directly. The public API documented here is the contract.
85
+
86
+ ---
87
+
88
+ > **Related:** [Utilities →](./UTILITIES.md) · [Errors →](./ERRORS.md)