@owlmeans/server-socket 0.1.18-rc.26 → 0.1.18-rc.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -42
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-socket/SKILL.md +3 -3
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -1,74 +1,205 @@
|
|
|
1
1
|
# @owlmeans/server-socket
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- The `Connection` interface (from `@owlmeans/socket`) is the runtime object passed to your handler
|
|
3
|
+
The server WebSocket carrier for OwlMeans backends: binds socket protocol declarations to connection
|
|
4
|
+
handlers on the Fastify API server. An application uses `connection(protocol, callback)` for push
|
|
5
|
+
and streaming channels — live notifications, file or job progress, RPC over one long-lived
|
|
6
|
+
connection. `@owlmeans/server-app`'s `makeContext` already registers the socket service and its
|
|
7
|
+
middleware. Request/response HTTP routes use `handlers<Context>()` from `@owlmeans/server-api`, the
|
|
8
|
+
browser side is `@owlmeans/client-socket`, and background work that must survive a restart belongs
|
|
9
|
+
in a queue (`@owlmeans/server-job`).
|
|
11
10
|
|
|
12
11
|
## Installation
|
|
13
12
|
|
|
14
13
|
```bash
|
|
15
|
-
bun add @owlmeans/server-socket@^0.1.18-rc.
|
|
14
|
+
bun add @owlmeans/server-socket@^0.1.18-rc.27
|
|
16
15
|
```
|
|
17
16
|
|
|
17
|
+
## Concepts
|
|
18
|
+
|
|
19
|
+
- **Socket protocol** — a declaration whose route is built with `socket()` from `@owlmeans/route`.
|
|
20
|
+
The HTTP server skips it; the socket service mounts it as a `GET` WebSocket route.
|
|
21
|
+
- **Connection** — the `@owlmeans/socket` `Connection` model. This package assigns its `send`,
|
|
22
|
+
`close`, `authenticate` and `prepare`; the handler uses `notify`, `listen`, `observe`, `perform`,
|
|
23
|
+
`call` and `close`.
|
|
24
|
+
- **Upgrade-time guards** — the protocol's guards and gates run before the upgrade with the same
|
|
25
|
+
pipeline as HTTP, including organization-entity resolution, so a guarded socket arrives with
|
|
26
|
+
`request.auth` and `request.entity` set.
|
|
27
|
+
- **In-band authentication** — a socket that receives its token after the connection is open
|
|
28
|
+
assigns `conn.authenticate`; that code path establishes authentication and must resolve the entity
|
|
29
|
+
itself.
|
|
30
|
+
- **System frames** — on close, listeners receive `{ type: MessageType.System, event: 'close',
|
|
31
|
+
payload: { code } }`. A thrown callback closes the socket with code 1011; server shutdown closes
|
|
32
|
+
open sockets with 1001.
|
|
33
|
+
|
|
18
34
|
## Usage
|
|
19
35
|
|
|
20
|
-
|
|
36
|
+
### Declare and bind
|
|
21
37
|
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
26
|
-
import { appEntrypoints as protocols } from 'my-app-common'
|
|
38
|
+
```ts
|
|
39
|
+
// my-app-common
|
|
40
|
+
import { contract, protocol, typed } from '@owlmeans/entrypoint'
|
|
41
|
+
import { route, socket } from '@owlmeans/route'
|
|
27
42
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
export const projectProtocols = {
|
|
44
|
+
// ...base, list, create
|
|
45
|
+
events: protocol(
|
|
46
|
+
route('my-app:api:project:events', '/:id/events', socket({ parent: projectBase })),
|
|
47
|
+
contract.request({ params: typed<ProjectParams>(ProjectParamsSchema) }, typed<void>()),
|
|
48
|
+
),
|
|
49
|
+
}
|
|
50
|
+
```
|
|
31
51
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
52
|
+
```ts
|
|
53
|
+
// my-app-api
|
|
54
|
+
import { bind } from '@owlmeans/server-app'
|
|
55
|
+
import { connection } from '@owlmeans/server-socket'
|
|
56
|
+
import { projectProtocols } from 'my-app-common'
|
|
57
|
+
import type { Context } from './context.js'
|
|
36
58
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
conn.notify('
|
|
59
|
+
const events = connection<typeof projectProtocols.events, Context>(projectProtocols.events,
|
|
60
|
+
async (conn, context, request) => {
|
|
61
|
+
await conn.notify('project:ready', { id: request.params.id })
|
|
40
62
|
})
|
|
41
|
-
})
|
|
42
63
|
|
|
43
|
-
export const
|
|
64
|
+
export const appEntrypoints = [
|
|
65
|
+
bind(projectProtocols.events, events),
|
|
66
|
+
]
|
|
44
67
|
```
|
|
45
68
|
|
|
46
|
-
|
|
69
|
+
### Push organization-scoped events and clean up
|
|
70
|
+
|
|
71
|
+
The parent declares `guards`, so authentication and the entity are resolved before the upgrade.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { requireEntityKey } from '@owlmeans/auth-common'
|
|
75
|
+
import { MessageType } from '@owlmeans/socket'
|
|
76
|
+
import type { EventMessage } from '@owlmeans/socket'
|
|
77
|
+
|
|
78
|
+
export const events = connection<typeof projectProtocols.events, Context>(projectProtocols.events,
|
|
79
|
+
async (conn, context, request) => {
|
|
80
|
+
const entityId = requireEntityKey(request)
|
|
81
|
+
|
|
82
|
+
// app-local pub/sub resource, one channel per organization
|
|
83
|
+
const unsubscribe = await context.projectEvents().subscribe(async event => {
|
|
84
|
+
if (event.projectId === request.params.id) {
|
|
85
|
+
await conn.notify('project:event', event)
|
|
86
|
+
}
|
|
87
|
+
}, { channel: entityId })
|
|
88
|
+
|
|
89
|
+
conn.listen(async raw => {
|
|
90
|
+
if (typeof raw !== 'object') return
|
|
91
|
+
const message = raw as EventMessage<void>
|
|
92
|
+
if (message.type === MessageType.System && message.event === 'close') {
|
|
93
|
+
await unsubscribe()
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Authenticate in-band
|
|
100
|
+
|
|
101
|
+
When the client cannot send a bearer on the upgrade request, it sends the token as the first auth
|
|
102
|
+
frame. The handler runs the guard itself and must call `attachEntity`.
|
|
47
103
|
|
|
48
|
-
|
|
104
|
+
```ts
|
|
105
|
+
import { AUTH_HEADER, AuthenFailed, AuthenticationStage, AuthUnknown } from '@owlmeans/auth'
|
|
106
|
+
import type { Auth, AuthToken } from '@owlmeans/auth'
|
|
107
|
+
import { attachEntity, DEFAULT_GUARD, requireEntityKey } from '@owlmeans/auth-common'
|
|
108
|
+
import { provideResponse } from '@owlmeans/entrypoint'
|
|
109
|
+
import type { AbstractResponse, GuardService } from '@owlmeans/entrypoint'
|
|
110
|
+
import type { AuthenticateMethod } from '@owlmeans/socket'
|
|
49
111
|
|
|
50
|
-
|
|
51
|
-
|
|
112
|
+
export const watch = connection<typeof projectProtocols.watch, Context>(projectProtocols.watch,
|
|
113
|
+
async (conn, context, request) => {
|
|
114
|
+
const timeout = setTimeout(() => void conn.close(), 5_000)
|
|
52
115
|
|
|
53
|
-
|
|
54
|
-
|
|
116
|
+
conn.authenticate = (async (stage: AuthenticationStage, token: AuthToken) => {
|
|
117
|
+
if (stage !== AuthenticationStage.Authenticate) {
|
|
118
|
+
throw new AuthUnknown('stage')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
request.headers[AUTH_HEADER] ??= token.token
|
|
122
|
+
const response: AbstractResponse<Auth> = provideResponse({ header: () => void 0 })
|
|
123
|
+
const guard = context.service<GuardService>(DEFAULT_GUARD)
|
|
124
|
+
if (!await guard.handle(request, response) || response.value == null) {
|
|
125
|
+
throw new AuthenFailed('guard')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
request.auth = response.value
|
|
129
|
+
// This path establishes authentication, so it resolves the organization entity itself.
|
|
130
|
+
await attachEntity(context, request)
|
|
131
|
+
clearTimeout(timeout)
|
|
132
|
+
|
|
133
|
+
await startWatching(conn, context, requireEntityKey(request))
|
|
134
|
+
|
|
135
|
+
return [AuthenticationStage.Authenticated, true]
|
|
136
|
+
}) as AuthenticateMethod
|
|
137
|
+
})
|
|
55
138
|
```
|
|
56
139
|
|
|
57
|
-
|
|
140
|
+
### RPC over a connection
|
|
58
141
|
|
|
59
|
-
|
|
142
|
+
```ts
|
|
143
|
+
export const terminal = connection<typeof projectProtocols.terminal, Context>(projectProtocols.terminal,
|
|
144
|
+
async (conn, context, request) => {
|
|
145
|
+
const entityId = requireEntityKey(request)
|
|
60
146
|
|
|
61
|
-
|
|
147
|
+
conn.perform('project:rename', async (id: string, name: string) => {
|
|
148
|
+
const project = await context.project().load({ id, entityId })
|
|
149
|
+
return project == null ? null : context.project().update({ ...project, name })
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The browser side calls it with `conn.call('project:rename', id, name)` on a
|
|
155
|
+
`@owlmeans/client-socket` connection.
|
|
62
156
|
|
|
63
|
-
###
|
|
157
|
+
### A context without `@owlmeans/server-app`
|
|
64
158
|
|
|
65
|
-
|
|
159
|
+
```ts
|
|
160
|
+
import { appendApiServer } from '@owlmeans/server-api'
|
|
161
|
+
import { appendSocketService, createSocketMiddleware } from '@owlmeans/server-socket'
|
|
66
162
|
|
|
67
|
-
|
|
163
|
+
appendApiServer(context)
|
|
164
|
+
appendSocketService(context)
|
|
165
|
+
context.registerMiddleware(createSocketMiddleware())
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## API
|
|
68
169
|
|
|
69
|
-
|
|
170
|
+
| Symbol | Kind | Purpose |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `connection<Protocol, SocketContext, SocketConnection>(protocol, handler)` | function | Bind a socket protocol to `(connection, context, request, response) => Promise<void>`; returns `BoundEntrypointHandler<Protocol>` |
|
|
173
|
+
| `handleConnection(handler)` | function | Unbound compatibility wrapper returning a `RefedEntrypointHandler` |
|
|
174
|
+
| `appendSocketService(ctx, alias = DEFAULT_ALIAS)` | function | Register the socket service on a context that already has the API server |
|
|
175
|
+
| `createSocketService(alias?)` | function | Create the socket service; `update(apiServer)` registers `@fastify/websocket` and the socket routes |
|
|
176
|
+
| `createSocketMiddleware(web = 'api-server', socket = DEFAULT_ALIAS)` | function | Loading-stage context middleware that attaches the socket service to the API server once |
|
|
177
|
+
| `DEFAULT_ALIAS` | const | `'socket-server'` |
|
|
178
|
+
| `SocketService` | type | `InitializedService` with `update(api: ApiServer)` |
|
|
179
|
+
| `Config`, `Context` | type | `ServerConfig` and `ServerContext & ApiServerAppend` |
|
|
180
|
+
|
|
181
|
+
## Common pitfalls
|
|
182
|
+
|
|
183
|
+
- Declare the route with `socket()`; without it the declaration is mounted as a plain HTTP `GET`.
|
|
184
|
+
- Bind with the same protocol object on both sides: `bind(p, connection(p, callback))`.
|
|
185
|
+
- Guards and gates are inherited from the protocol tree and enforced before the handler runs; do not
|
|
186
|
+
re-implement them inside the callback.
|
|
187
|
+
- A socket that authenticates on its own must call `attachEntity(context, request)`, or
|
|
188
|
+
`request.entity` stays empty and entity-keyed lookups silently find nothing.
|
|
189
|
+
- The token query parameter is used only to stamp `sender` / `recipient` on frames. It is not
|
|
190
|
+
verified — never treat it as authentication.
|
|
191
|
+
- Release subscriptions and watchers on the System `close` frame. Listeners also receive heartbeat
|
|
192
|
+
frames, so check `type` and `event` before acting.
|
|
193
|
+
- Do not expose Fastify request or socket objects through a shared contract.
|
|
194
|
+
|
|
195
|
+
## Related packages
|
|
196
|
+
|
|
197
|
+
- [`@owlmeans/socket`](../socket) — `Connection`, `MessageType`, `EventMessage`
|
|
198
|
+
- [`@owlmeans/client-socket`](../client-socket) — the browser and native carrier
|
|
199
|
+
- [`@owlmeans/server-api`](../server-api) — the API server this service attaches to
|
|
200
|
+
- [`@owlmeans/server-entrypoint`](../server-entrypoint) — `bind()`
|
|
70
201
|
- [`@owlmeans/server-app`](../server-app) — calls `appendSocketService` in `makeContext`
|
|
71
|
-
- [`@owlmeans/
|
|
202
|
+
- [`@owlmeans/auth-common`](../auth-common) — `attachEntity`, `requireEntityKey`, `DEFAULT_GUARD`
|
|
72
203
|
|
|
73
204
|
<!-- owlmeans:agent-guidance:start -->
|
|
74
205
|
## Agent guidance
|
|
@@ -78,7 +209,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
78
209
|
your project's skill store (`.agents/skills/`):
|
|
79
210
|
|
|
80
211
|
```sh
|
|
81
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
212
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.21
|
|
82
213
|
```
|
|
83
214
|
|
|
84
215
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/server-socket",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.28",
|
|
5
|
+
"generatedAt": "2026-09-15T12:21:31.933Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -6,7 +6,7 @@ description: Bind OwlMeans socket protocol declarations to Fastify WebSocket han
|
|
|
6
6
|
|
|
7
7
|
# Socket protocol handlers
|
|
8
8
|
|
|
9
|
-
**Install:** `bun add @owlmeans/server-socket@^0.1.18-rc.
|
|
9
|
+
**Install:** `bun add @owlmeans/server-socket@^0.1.18-rc.27`
|
|
10
10
|
|
|
11
11
|
Declare a socket route and its contract in the shared protocol package. Bind it on the server with
|
|
12
12
|
`connection()` and `bind()`.
|
|
@@ -15,8 +15,8 @@ Declare a socket route and its contract in the shared protocol package. Bind it
|
|
|
15
15
|
import { bind } from '@owlmeans/server-entrypoint'
|
|
16
16
|
import { connection } from '@owlmeans/server-socket'
|
|
17
17
|
|
|
18
|
-
export const
|
|
19
|
-
bind(
|
|
18
|
+
export const serverBindings = [
|
|
19
|
+
bind(streamProtocols.watch, connection(streamProtocols.watch,
|
|
20
20
|
async (connection, context, request) => {
|
|
21
21
|
await connection.send({ type: 'ready' })
|
|
22
22
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/server-socket",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.28",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -31,15 +31,15 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@fastify/websocket": "^11.0.1",
|
|
34
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
35
|
-
"@owlmeans/basic-envelope": "^0.1.18-rc.
|
|
36
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
37
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
38
|
-
"@owlmeans/server-api": "^0.1.18-rc.
|
|
39
|
-
"@owlmeans/server-auth": "^0.1.18-rc.
|
|
40
|
-
"@owlmeans/server-context": "^0.1.18-rc.
|
|
41
|
-
"@owlmeans/server-entrypoint": "^0.1.18-rc.
|
|
42
|
-
"@owlmeans/socket": "^0.1.18-rc.
|
|
34
|
+
"@owlmeans/auth": "^0.1.18-rc.18",
|
|
35
|
+
"@owlmeans/basic-envelope": "^0.1.18-rc.21",
|
|
36
|
+
"@owlmeans/context": "^0.1.18-rc.17",
|
|
37
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.20",
|
|
38
|
+
"@owlmeans/server-api": "^0.1.18-rc.26",
|
|
39
|
+
"@owlmeans/server-auth": "^0.1.18-rc.28",
|
|
40
|
+
"@owlmeans/server-context": "^0.1.18-rc.21",
|
|
41
|
+
"@owlmeans/server-entrypoint": "^0.1.18-rc.20",
|
|
42
|
+
"@owlmeans/socket": "^0.1.18-rc.18"
|
|
43
43
|
},
|
|
44
44
|
"publishConfig": {
|
|
45
45
|
"access": "public"
|