@byok-sdk/server 0.1.0
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 +10 -0
- package/dist/auth.d.ts +171 -0
- package/dist/blob-store.d.ts +77 -0
- package/dist/event-queue.d.ts +20 -0
- package/dist/heartbeat.d.ts +30 -0
- package/dist/http.d.ts +24 -0
- package/dist/hub.d.ts +861 -0
- package/dist/ids.d.ts +3 -0
- package/dist/index.d.ts +116 -0
- package/dist/index.js +2675 -0
- package/dist/index.js.map +1 -0
- package/dist/pairing.d.ts +58 -0
- package/dist/rate-limiter.d.ts +114 -0
- package/dist/sqlite-blob-store.d.ts +89 -0
- package/dist/sqlite-support.d.ts +84 -0
- package/dist/sqlite-task-store.d.ts +123 -0
- package/dist/task-store.d.ts +125 -0
- package/dist/types.d.ts +378 -0
- package/dist/ws-server.d.ts +20 -0
- package/package.json +56 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import type { AgentEventOrUnknown, BlobRef, PermissionPolicy, RuntimeCapabilities, RuntimeId, RuntimeInfo, TaskApprovalResolvedPayload, TaskArtifactPayload, TaskState } from '@byok-sdk/protocol';
|
|
2
|
+
import type { BlobStore } from './blob-store';
|
|
3
|
+
import type { RateLimiterOptions } from './rate-limiter';
|
|
4
|
+
import type { TaskStore } from './task-store';
|
|
5
|
+
import type { TokenSigner } from './auth';
|
|
6
|
+
/** Options for {@link createByokServer}. */
|
|
7
|
+
export interface CreateByokServerOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Identifies which product this server instance serves. Checked against the
|
|
10
|
+
* `productId` a daemon announces in `conn.hello` — one daemon process is
|
|
11
|
+
* always scoped to one product (see plan: "一产品一 daemon 进程"), so a
|
|
12
|
+
* mismatched daemon is rejected at handshake time.
|
|
13
|
+
*/
|
|
14
|
+
productId: string;
|
|
15
|
+
/** WS-native ping interval, ms (§ heartbeat). Default 30s. */
|
|
16
|
+
heartbeatIntervalMs?: number;
|
|
17
|
+
/** How long `GET /byok/events` holds an empty poll open before returning, ms (§8). Default ~50s; override for tests. */
|
|
18
|
+
longPollHoldMs?: number;
|
|
19
|
+
/** Per-product blob size ceiling in bytes (§7). Default 100MB. */
|
|
20
|
+
maxBlobSizeBytes?: number;
|
|
21
|
+
/** Override the reference {@link BlobStore} (e.g. a real object-store-backed implementation, or `sqlite-blob-store.ts`'s `SqliteBlobStore` for a persistent single-node deployment). */
|
|
22
|
+
blobStore?: BlobStore;
|
|
23
|
+
/** Override the reference {@link TaskStore} (e.g. `sqlite-task-store.ts`'s `SqliteTaskStore` for a persistent single-node deployment). Defaults to an in-memory store that loses all task state on restart. */
|
|
24
|
+
taskStore?: TaskStore;
|
|
25
|
+
/** Override the reference {@link TokenSigner} (e.g. an org-wide/KMS-backed signer). */
|
|
26
|
+
tokenSigner?: TokenSigner;
|
|
27
|
+
/**
|
|
28
|
+
* How long a `Claimed`/`Running`/`AwaitApproval` task may sit with no
|
|
29
|
+
* inbound `task.*` activity from its owning device while that device is
|
|
30
|
+
* dark (disconnected, or long-poll-silent) before the server reaps it to
|
|
31
|
+
* `Failed(retryable: true, reason: 'lease-expired')` — no new task state,
|
|
32
|
+
* no new wire message; the embedder is expected to re-dispatch as a
|
|
33
|
+
* brand-new task, same as any other retryable failure. Deliberately
|
|
34
|
+
* generous — it exists purely as a backstop for a device that never
|
|
35
|
+
* reconnects at all (M1's redelivery, docs/protocol.md §9, already covers
|
|
36
|
+
* "came back within the window"), so it must stay far larger than any
|
|
37
|
+
* realistic task duration or it will race and fail perfectly healthy
|
|
38
|
+
* long-running tasks. A task on a *connected*, actively-progressing
|
|
39
|
+
* device is never touched regardless of this value — see
|
|
40
|
+
* `ConnectionHub`'s lease-reaper doc comment (`hub.ts`) for the full
|
|
41
|
+
* design and its accepted residual risk. Default 30 minutes.
|
|
42
|
+
*/
|
|
43
|
+
taskLeaseMs?: number;
|
|
44
|
+
/**
|
|
45
|
+
* M4 Phase 4 (part A): per-device inbound-envelope token bucket, enforced
|
|
46
|
+
* by `ConnectionHub.handleInbound` (`hub.ts`) — the single choke point
|
|
47
|
+
* both WS (`ws-server.ts`) and long-poll (`POST /byok/messages`, `http.ts`)
|
|
48
|
+
* inbound traffic passes through. Defaults: 50 msg/s sustained, burst 100
|
|
49
|
+
* (see `rate-limiter.ts`'s own defaults). Exceeding it never drops
|
|
50
|
+
* silently: it counts in `ConnectionHub.stats()`'s `rateLimitEvents`, and
|
|
51
|
+
* emits a `device.rate_limited` {@link ByokServerEvent} — see that
|
|
52
|
+
* variant's own doc comment for the per-transport enforcement shape (WS
|
|
53
|
+
* close vs. long-poll 429). Blob upload/download routes (`http.ts`) are
|
|
54
|
+
* deliberately NOT covered by this same bucket — see that file's own
|
|
55
|
+
* comment on why a shared limiter didn't drop in cleanly there.
|
|
56
|
+
*
|
|
57
|
+
* Honest caveat (no code change changes this — it's an inherent property
|
|
58
|
+
* of an abrupt WS close, not something rate limiting adds): a
|
|
59
|
+
* flood-triggered 1008 close is not special. Envelopes the daemon's own
|
|
60
|
+
* WS transport already handed off to its socket write between the moment
|
|
61
|
+
* the device exceeded budget and the close actually landing share the
|
|
62
|
+
* ordinary at-most-once exposure of ANY abrupt WS disconnect (network
|
|
63
|
+
* blip, server restart, etc.) — the wire's at-least-once guarantee
|
|
64
|
+
* (docs/protocol.md §9) is specified for the server->daemon direction
|
|
65
|
+
* only; daemon->server has no redelivery cursor to begin with, so this
|
|
66
|
+
* was already true before rate limiting existed. A flood just makes that
|
|
67
|
+
* pre-existing window more likely to have something in flight at the
|
|
68
|
+
* exact moment of a close.
|
|
69
|
+
*/
|
|
70
|
+
rateLimit?: RateLimiterOptions;
|
|
71
|
+
/**
|
|
72
|
+
* M4 Phase 4 (part B.2): opt-in `GET /healthz` liveness route on the Hono
|
|
73
|
+
* app (`http.ts`) — deliberately unauthenticated (no bearer check) and
|
|
74
|
+
* carrying no sensitive data (no device ids, no counts), just
|
|
75
|
+
* `{ok:true, uptimeMs}`; see `http.ts`'s own comment on that route for the
|
|
76
|
+
* full auth-posture rationale. Default `false` (no route mounted at all).
|
|
77
|
+
* `ConnectionHub.stats()` (richer, in-process-only detail) is never
|
|
78
|
+
* exposed over HTTP by this SDK regardless of this flag — an embedder that
|
|
79
|
+
* wants that surfaced remotely builds its own authenticated route around
|
|
80
|
+
* `stats()`.
|
|
81
|
+
*/
|
|
82
|
+
healthzRoute?: boolean;
|
|
83
|
+
}
|
|
84
|
+
/** Input to {@link ByokServer.dispatch}. */
|
|
85
|
+
export interface DispatchInput {
|
|
86
|
+
instruction: string;
|
|
87
|
+
runtime?: RuntimeId;
|
|
88
|
+
policy?: PermissionPolicy;
|
|
89
|
+
deviceId?: string;
|
|
90
|
+
sessionRef?: string;
|
|
91
|
+
}
|
|
92
|
+
/** Outcome of a task that reached a terminal state. */
|
|
93
|
+
export interface TaskResult {
|
|
94
|
+
state: Extract<TaskState, 'Complete' | 'Failed' | 'Cancelled'>;
|
|
95
|
+
summary?: string;
|
|
96
|
+
sessionRef?: string;
|
|
97
|
+
artifactRefs?: BlobRef[];
|
|
98
|
+
reason?: string;
|
|
99
|
+
retryable?: boolean;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Normalized event stream for a dispatched task: incoming `task.progress`
|
|
103
|
+
* AgentEvents, state transitions, and artifacts, folded into one feed so a
|
|
104
|
+
* consumer only has to read one `events()` iterable per task.
|
|
105
|
+
*
|
|
106
|
+
* `event` is {@link AgentEventOrUnknown}, not the narrower `AgentEvent`
|
|
107
|
+
* (pre-freeze tolerance, `@byok-sdk/protocol`'s `agent-event.ts`): an
|
|
108
|
+
* unknown-type event — one a newer daemon/runtime-adapter minor version
|
|
109
|
+
* produced that this build doesn't recognize — is forwarded here as-is
|
|
110
|
+
* rather than dropped. It's still observability data a newer embedder UI
|
|
111
|
+
* may understand even if this server doesn't; the reference server's job is
|
|
112
|
+
* to tolerate and forward, not to decide what's renderable. Use the
|
|
113
|
+
* exported `isKnownAgentEvent`/`partitionAgentEvents` helpers if a consumer
|
|
114
|
+
* needs to distinguish the two.
|
|
115
|
+
*/
|
|
116
|
+
export type ServerTaskEvent = {
|
|
117
|
+
kind: 'state';
|
|
118
|
+
state: TaskState;
|
|
119
|
+
at: string;
|
|
120
|
+
} | {
|
|
121
|
+
kind: 'agent';
|
|
122
|
+
event: AgentEventOrUnknown;
|
|
123
|
+
} | {
|
|
124
|
+
kind: 'artifact';
|
|
125
|
+
artifact: TaskArtifactPayload;
|
|
126
|
+
} | {
|
|
127
|
+
kind: 'await_approval';
|
|
128
|
+
summary: string;
|
|
129
|
+
} | {
|
|
130
|
+
kind: 'error';
|
|
131
|
+
reason: string;
|
|
132
|
+
retryable?: boolean;
|
|
133
|
+
};
|
|
134
|
+
/** Handle returned by {@link ByokServer.dispatch} for one in-flight task. */
|
|
135
|
+
export interface TaskHandle {
|
|
136
|
+
readonly taskId: string;
|
|
137
|
+
events(): AsyncIterable<ServerTaskEvent>;
|
|
138
|
+
cancel(reason?: string): Promise<void>;
|
|
139
|
+
/**
|
|
140
|
+
* M5 (approval targeting, docs/protocol.md §5.3): `opts.approvalId`
|
|
141
|
+
* targets a SPECIFIC pending approval rather than "whichever one is
|
|
142
|
+
* currently pending" (the default when `opts` is omitted, unchanged from
|
|
143
|
+
* pre-M5). Thin wrapper over `ConnectionHub.approveTask` (`hub.ts`) — see
|
|
144
|
+
* that method's own doc comment for the full targeting/staleness
|
|
145
|
+
* semantics, including when this throws `StaleApprovalError` (exported
|
|
146
|
+
* from the package index for a caller to catch/inspect).
|
|
147
|
+
*/
|
|
148
|
+
approve(opts?: {
|
|
149
|
+
approvalId?: string;
|
|
150
|
+
}): Promise<void>;
|
|
151
|
+
/** M5: same `opts.approvalId` targeting semantics as {@link approve} above. */
|
|
152
|
+
reject(reason?: string, opts?: {
|
|
153
|
+
approvalId?: string;
|
|
154
|
+
}): Promise<void>;
|
|
155
|
+
steer(text: string): Promise<void>;
|
|
156
|
+
result(): Promise<TaskResult>;
|
|
157
|
+
}
|
|
158
|
+
/** A device known to this server, joined from pairing identity + live connection state. */
|
|
159
|
+
export interface MachineInfo {
|
|
160
|
+
deviceId: string;
|
|
161
|
+
deviceName: string;
|
|
162
|
+
connected: boolean;
|
|
163
|
+
lastSeen?: string;
|
|
164
|
+
/** Runtimes detected on this device, as reported in its last `conn.hello` (M1: typed, replaces the old untyped `agents`). */
|
|
165
|
+
runtimes?: RuntimeInfo[];
|
|
166
|
+
}
|
|
167
|
+
/** Snapshot of a task as tracked by the in-memory {@link TaskStore}. */
|
|
168
|
+
export interface TaskSnapshot {
|
|
169
|
+
taskId: string;
|
|
170
|
+
state: TaskState;
|
|
171
|
+
instruction: string;
|
|
172
|
+
/**
|
|
173
|
+
* The REQUESTED runtime — `DispatchInput.runtime`, forwarded verbatim into
|
|
174
|
+
* `task.offer.runtime`. Set once at `dispatch()` time and never touched
|
|
175
|
+
* again afterward, regardless of what the daemon actually ends up running.
|
|
176
|
+
* `undefined` means "no preference was expressed" (the daemon auto-selects,
|
|
177
|
+
* pi-first) — NOT "the daemon ran no runtime". Contrast with
|
|
178
|
+
* {@link claimedRuntime}, the ACTUAL runtime the daemon reports having
|
|
179
|
+
* picked; see that field's own doc comment for the full requested-vs-
|
|
180
|
+
* claimed distinction (docs/protocol.md §3.1).
|
|
181
|
+
*/
|
|
182
|
+
runtime?: RuntimeId;
|
|
183
|
+
policy: PermissionPolicy;
|
|
184
|
+
deviceId?: string;
|
|
185
|
+
sessionRef?: string;
|
|
186
|
+
createdAt: string;
|
|
187
|
+
updatedAt: string;
|
|
188
|
+
result?: TaskResult;
|
|
189
|
+
/**
|
|
190
|
+
* M5 (approval targeting, docs/protocol.md §5.3): the daemon-reported
|
|
191
|
+
* `approvalId` for the CURRENT `AwaitApproval` cycle, if this server has
|
|
192
|
+
* learned one (`ConnectionHub.onAwaitApproval`, `hub.ts`) — `undefined`
|
|
193
|
+
* whenever the task isn't currently awaiting approval, OR it is but no id
|
|
194
|
+
* was ever reported for it (a legacy daemon). Cleared centrally the
|
|
195
|
+
* instant the task LEAVES `AwaitApproval` (`ConnectionHub`'s
|
|
196
|
+
* `transitionTask`), so a later `AwaitApproval` cycle for the same task
|
|
197
|
+
* never inherits a stale id from a previous one. `approveTask`/
|
|
198
|
+
* `rejectTask` compare an operator-supplied target id against this field
|
|
199
|
+
* to decide whether a decision is stale (`StaleApprovalError`) — see
|
|
200
|
+
* `hub.ts` for the full mechanism.
|
|
201
|
+
*/
|
|
202
|
+
pendingApprovalId?: string;
|
|
203
|
+
/**
|
|
204
|
+
* M5 (claimed runtime, docs/protocol.md §3.1): the ACTUAL adapter the
|
|
205
|
+
* daemon reports having selected for this task (`task.claim.runtime`,
|
|
206
|
+
* `ConnectionHub.onClaim` — `hub.ts`) — covers both the explicit-runtime
|
|
207
|
+
* path (echoes {@link runtime}) and the auto-select/pi-first path (a value
|
|
208
|
+
* where {@link runtime} is `undefined`, since no preference was ever
|
|
209
|
+
* requested). `undefined` until the first `task.claim` for this task
|
|
210
|
+
* arrives, and forever after for a legacy daemon that predates this field
|
|
211
|
+
* (an old daemon's `task.claim` simply omits it). Set exactly once, at the
|
|
212
|
+
* `Offered -> Claimed` transition, and never modified again afterward — a
|
|
213
|
+
* retried/idempotent claim from the same device is a no-op that never
|
|
214
|
+
* reaches `onClaim`'s patch at all (see `onClaim`'s own doc comment), so
|
|
215
|
+
* this can never be silently overwritten by a redelivered claim.
|
|
216
|
+
*/
|
|
217
|
+
claimedRuntime?: RuntimeId;
|
|
218
|
+
/**
|
|
219
|
+
* S0/D-4 (runtime-honest control surface): the capability block the
|
|
220
|
+
* CLAIMING adapter reported for itself on its own `task.claim`
|
|
221
|
+
* (`TaskClaimPayload.capabilities`, `@byok-sdk/protocol`), snapshotted at the
|
|
222
|
+
* exact moment of the `Offered -> Claimed` transition
|
|
223
|
+
* (`ConnectionHub.onClaim`, `hub.ts`).
|
|
224
|
+
*
|
|
225
|
+
* Sourced from the claim and from nothing else. The connection-level
|
|
226
|
+
* `conn.hello.runtimes[].capabilities` is discovery data — it describes a
|
|
227
|
+
* device rather than a task, and a long-poll-only daemon never sends
|
|
228
|
+
* `conn.hello` at all — so it is never read here or by the gate; see
|
|
229
|
+
* `SteerRejectedError` (`hub.ts`) for the full argument.
|
|
230
|
+
*
|
|
231
|
+
* A SNAPSHOT, deliberately — not a live read of anything: the same device
|
|
232
|
+
* can reconnect later with a different adapter set (a runtime upgraded,
|
|
233
|
+
* removed, or newly installed mid-task), and a task that is already running
|
|
234
|
+
* must keep being judged against what was true when it was claimed.
|
|
235
|
+
* `ConnectionHub.steerTask` is the consumer: it fails closed with a
|
|
236
|
+
* `SteerRejectedError` (`hub.ts`) unless this snapshot says `steer === true`,
|
|
237
|
+
* BEFORE any `task.steer` envelope exists.
|
|
238
|
+
*
|
|
239
|
+
* `undefined` means "this server does not know" — never "supported" and
|
|
240
|
+
* never "unsupported as a fact". It stays `undefined` when the claim carried
|
|
241
|
+
* no `capabilities` (a pre-D-4 daemon; the wire field is optional) and for
|
|
242
|
+
* every task record written before S0 existed. Both are treated as a refusal
|
|
243
|
+
* by the steer gate rather than filled in with a guessed default.
|
|
244
|
+
*
|
|
245
|
+
* Written exactly once, alongside {@link claimedRuntime}, on the first
|
|
246
|
+
* real claim — a retried/idempotent claim returns from `onClaim` before
|
|
247
|
+
* the patch, so this can never be silently rewritten later.
|
|
248
|
+
*/
|
|
249
|
+
claimedRuntimeCapabilities?: RuntimeCapabilities;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Cross-cutting server event feed (device connects/disconnects, task
|
|
253
|
+
* creation/state changes) — the "event hub" from the plan's 服务端参考实现
|
|
254
|
+
* section, as opposed to `TaskHandle.events()` which is scoped to one task.
|
|
255
|
+
* Not part of the pinned wire contract; a server-embedder-facing convenience.
|
|
256
|
+
*/
|
|
257
|
+
export type ByokServerEvent = {
|
|
258
|
+
kind: 'device.connected';
|
|
259
|
+
deviceId: string;
|
|
260
|
+
at: string;
|
|
261
|
+
} | {
|
|
262
|
+
kind: 'device.disconnected';
|
|
263
|
+
deviceId: string;
|
|
264
|
+
at: string;
|
|
265
|
+
} | {
|
|
266
|
+
kind: 'task.created';
|
|
267
|
+
taskId: string;
|
|
268
|
+
at: string;
|
|
269
|
+
} | {
|
|
270
|
+
kind: 'task.state';
|
|
271
|
+
taskId: string;
|
|
272
|
+
state: TaskState;
|
|
273
|
+
at: string;
|
|
274
|
+
/**
|
|
275
|
+
* M5 (claimed runtime): mirrors {@link TaskSnapshot.claimedRuntime} at
|
|
276
|
+
* the moment of this transition — `undefined` until (and unless) the
|
|
277
|
+
* daemon's `task.claim` reported one, so it first appears on the
|
|
278
|
+
* `Offered -> Claimed` event and stays whatever value it had from then
|
|
279
|
+
* on for every later transition of the same task. See that field's own
|
|
280
|
+
* doc comment for the requested-vs-claimed distinction
|
|
281
|
+
* (docs/protocol.md §3.1).
|
|
282
|
+
*/
|
|
283
|
+
claimedRuntime?: RuntimeId;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* M4 Phase 3 hardening (orchestrator-directed): the daemon resolved a
|
|
287
|
+
* pending approval entirely locally (M4 Phase 3's local `approvals.resolve`
|
|
288
|
+
* control-socket path) — no wire `task.approve`/`task.reject` ever reached
|
|
289
|
+
* the server for it. This fires when daemon-originated task traffic
|
|
290
|
+
* (`task.progress`/`task.artifact`/`task.complete`) for a task the server's
|
|
291
|
+
* own record still has as `AwaitApproval` proves, after the fact, that the
|
|
292
|
+
* approval was resolved on the device — see `ConnectionHub`'s
|
|
293
|
+
* `resumeIfImplicitlyApproved` (hub.ts) for the state-machine side of this.
|
|
294
|
+
* Deliberately NOT a wire message (no `packages/protocol` change) — a
|
|
295
|
+
* first-class `task.approval_resolved` wire notification is a deferred
|
|
296
|
+
* v1.1 candidate; this is purely an embedder-facing observability signal
|
|
297
|
+
* so a SaaS UI can distinguish "approved server-side" from "the device
|
|
298
|
+
* says it was approved locally" if it cares to.
|
|
299
|
+
*/
|
|
300
|
+
| {
|
|
301
|
+
kind: 'task.approval_resolved_implicit';
|
|
302
|
+
taskId: string;
|
|
303
|
+
at: string;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* M4 (additive-minor): the EXPLICIT counterpart to
|
|
307
|
+
* `task.approval_resolved_implicit` above — fires when the daemon reports
|
|
308
|
+
* a locally-resolved approval via the wire `task.approval_resolved`
|
|
309
|
+
* message (`ConnectionHub.onApprovalResolved`, `hub.ts`) rather than the
|
|
310
|
+
* server having to infer it from later task traffic. Carries the same
|
|
311
|
+
* `approvalId`/`decision`/`resolvedBy` the daemon reported, so an embedder
|
|
312
|
+
* can render/audit exactly what was resolved and by which path, not just
|
|
313
|
+
* that a resolution happened. `resolvedBy` is currently always `'local'`
|
|
314
|
+
* (`@byok-sdk/protocol`'s `TaskApprovalResolvedPayloadSchema` — a single-value
|
|
315
|
+
* enum today, future-proofed for an additional value later without a
|
|
316
|
+
* version bump). Mutually exclusive with `task.approval_resolved_implicit`
|
|
317
|
+
* for the same resolution: whichever mechanism the server processes first
|
|
318
|
+
* performs the actual `AwaitApproval -> Running` transition, and the other
|
|
319
|
+
* is already a no-op by the time it would otherwise run — see
|
|
320
|
+
* `onApprovalResolved`'s own doc comment (`hub.ts`) for the full
|
|
321
|
+
* relationship.
|
|
322
|
+
*/
|
|
323
|
+
| ({
|
|
324
|
+
kind: 'task.approval_resolved';
|
|
325
|
+
taskId: string;
|
|
326
|
+
at: string;
|
|
327
|
+
/**
|
|
328
|
+
* M5 (hello-capability plumbing, docs/protocol.md §5.3): whether the
|
|
329
|
+
* REPORTING device advertised the `approval-targeting` capability flag
|
|
330
|
+
* (`version.ts`) in its `conn.hello` — an observability-only signal
|
|
331
|
+
* (see that flag's own doc comment: it never gates matching, which is
|
|
332
|
+
* always decided by field presence on the specific message). `false`
|
|
333
|
+
* for a legacy daemon, or one whose connection capabilities this hub
|
|
334
|
+
* never recorded (see `ConnectionHub.getDeviceCapabilities`).
|
|
335
|
+
*/
|
|
336
|
+
targeted: boolean;
|
|
337
|
+
} & Pick<TaskApprovalResolvedPayload, 'approvalId' | 'decision' | 'resolvedBy'>)
|
|
338
|
+
/**
|
|
339
|
+
* M4 Phase 4 (part A): `deviceId` exceeded its inbound-envelope rate limit
|
|
340
|
+
* (`CreateByokServerOptions.rateLimit`, enforced in
|
|
341
|
+
* `ConnectionHub.handleInbound`, `hub.ts`) — fired for every envelope that
|
|
342
|
+
* arrives once the bucket is empty, not just the first. Never a silent
|
|
343
|
+
* drop: this event fires AND the occurrence is counted in
|
|
344
|
+
* `ConnectionHub.stats()`'s `rateLimitEvents`. Per-transport enforcement
|
|
345
|
+
* differs (both still emit this same event): a WS connection is closed
|
|
346
|
+
* (policy-violation close code) right after, so the client's existing
|
|
347
|
+
* backoff+reconnect (protocol §9's redelivery covers the rest); a
|
|
348
|
+
* long-poll device has no live connection to close, so `POST
|
|
349
|
+
* /byok/messages` (`http.ts`) instead answers that request with HTTP 429.
|
|
350
|
+
*/
|
|
351
|
+
| {
|
|
352
|
+
kind: 'device.rate_limited';
|
|
353
|
+
deviceId: string;
|
|
354
|
+
at: string;
|
|
355
|
+
};
|
|
356
|
+
/**
|
|
357
|
+
* Plain, serializable in-process snapshot returned by
|
|
358
|
+
* `ConnectionHub.stats()` (`hub.ts`) — M4 Phase 4 (part B.1). Deliberately
|
|
359
|
+
* NOT exposed over HTTP by this SDK (see `CreateByokServerOptions.healthzRoute`'s
|
|
360
|
+
* doc comment): an embedder that wants any of this surfaced remotely builds
|
|
361
|
+
* its own authenticated route around `ByokServer.stats()`.
|
|
362
|
+
*/
|
|
363
|
+
export interface HubStats {
|
|
364
|
+
/** Devices with a currently-live WS or long-poll connection. */
|
|
365
|
+
connectedDeviceCount: number;
|
|
366
|
+
/** Every {@link TaskState} mapped to how many known tasks currently sit in it. */
|
|
367
|
+
taskCountsByState: Record<TaskState, number>;
|
|
368
|
+
/** Total inbound daemon->server envelopes {@link ConnectionHub.handleInbound} has ever been called with (every outcome, including rejected/rate-limited). */
|
|
369
|
+
envelopesIn: number;
|
|
370
|
+
/** Total server->daemon envelopes ever constructed via {@link ConnectionHub}'s single outbound choke point (`sendToDevice`), regardless of whether a live transport was available to flush them immediately. */
|
|
371
|
+
envelopesOut: number;
|
|
372
|
+
/** Inbound envelopes recognized as an already-seen `(deviceId, id)` pair (N3) — a no-op wire-level success, counted here for observability. */
|
|
373
|
+
dedupDrops: number;
|
|
374
|
+
/** Inbound envelopes rejected for exceeding a device's rate limit — see `device.rate_limited` on {@link ByokServerEvent}. */
|
|
375
|
+
rateLimitEvents: number;
|
|
376
|
+
/** Milliseconds since this `ConnectionHub` was constructed. */
|
|
377
|
+
uptimeMs: number;
|
|
378
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Server as HttpServer } from 'node:http';
|
|
2
|
+
import { type AuthDeps } from './auth';
|
|
3
|
+
import type { ConnectionHub } from './hub';
|
|
4
|
+
interface AttachDeps extends AuthDeps {
|
|
5
|
+
hub: ConnectionHub;
|
|
6
|
+
productId: string;
|
|
7
|
+
/** WS-native ping interval, ms. Defaults inside `heartbeat.ts` (30s) if omitted. */
|
|
8
|
+
heartbeatIntervalMs?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Wire up the `GET /byok/ws` upgrade on a raw Node HTTP server (the one
|
|
12
|
+
* `@hono/node-server`'s `serve()` returns). Auth happens on the upgrade
|
|
13
|
+
* request itself via `Authorization: Bearer <accessToken>` (a JWT minted by
|
|
14
|
+
* `/byok/pair` or `/byok/token` — Auth v2, §6); an invalid, expired, or
|
|
15
|
+
* revoked token gets a 401 and the socket is destroyed. Handshake
|
|
16
|
+
* (`conn.hello` -> `conn.ack`) happens on the first WS message once
|
|
17
|
+
* upgraded.
|
|
18
|
+
*/
|
|
19
|
+
export declare function attachWebSocket(server: HttpServer, deps: AttachDeps): void;
|
|
20
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@byok-sdk/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "BYOK SDK server: in-memory M0 reference implementation of the SaaS-side coordinator",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Ancienttwo/byok-sdk.git",
|
|
10
|
+
"directory": "packages/server"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Ancienttwo/byok-sdk/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/Ancienttwo/byok-sdk#readme",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@hono/node-server": "^2.0.10",
|
|
40
|
+
"hono": "^4.12.30",
|
|
41
|
+
"jose": "^6.2.3",
|
|
42
|
+
"ws": "^8.21.1",
|
|
43
|
+
"@byok-sdk/protocol": "0.1.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/ws": "^8.18.1"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup && tsc -p tsconfig.build.json",
|
|
50
|
+
"dev": "tsup --watch",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"clean": "rm -rf dist"
|
|
55
|
+
}
|
|
56
|
+
}
|