@dokebi/sse 0.4.4 → 0.4.5

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/AI.md CHANGED
@@ -135,7 +135,7 @@ events.close(); events.reconnect();
135
135
 
136
136
  ## 5a. Seal hosts (D-0062)
137
137
 
138
- On streams of a seal host (@dokebi/app `domains.<host>.seal: true`) or of a `seal: true` web route, **the data of every event** (including `dokebi-open`, `dokebi-reload` and `dokebi-restart`) is sealed with that stream's path (`E:<ts>:<b64>`, dokebi-web `sealOf`). Event names and ids stay plain. Plain streams share one buffer; only sealed streams seal per receiver. A failed seal ends that stream (`SEAL_ENCODE_FAILED`; never sent in plain text). Browser: `connectEvents({ ..., seal: currentSealBridge() })` (`@dokebi/seal/http`; undefined on plain pages, so the same code works). With a bridge, plain data is refused as `SEAL_FAILED`; without one, sealed data is refused as `SEAL_BRIDGE_MISSING` (neither reaches the handler).
138
+ On streams of a seal host (@dokebi/app `domains.<host>.seal: true`) or of a `seal: true` web route, **the data of every event** (including `dokebi-open`, `dokebi-reload` and `dokebi-restart`) is sealed with that stream's path (`E:<ts>:<b64>`, dokebi-web `sealOf`). Event names and ids stay plain. Plain streams share one buffer; only sealed streams seal per receiver. A failed seal ends that stream (`SEAL_ENCODE_FAILED`; never sent in plain text); when it is the first frame (`dokebi-open`, `dokebi-reload`) that cannot be sealed, the request is refused with 500 `SEAL_FAILED` before the stream head. Browser: `connectEvents({ ..., seal: currentSealBridge() })` (`@dokebi/seal/http`; undefined on plain pages, so the same code works). With a bridge, plain data is refused as `SEAL_FAILED`; without one, sealed data is refused as `SEAL_BRIDGE_MISSING` (neither reaches the handler).
139
139
 
140
140
  ## 6. Several workers and servers
141
141
 
package/dist/gateway.js CHANGED
@@ -341,6 +341,18 @@ export class Gateway {
341
341
  // D-0062: a stream of a seal host seals the data of every event, its first ones included.
342
342
  const seal = this.web.sealOf(request);
343
343
  const data = (json) => (seal ? seal.encode(json) : json);
344
+ // The first frame is made before the reply is hijacked: a seal that fails here still has a
345
+ // reply to refuse with. Made after the head (audit R9-g11-2), the exception left a hijacked
346
+ // response that was never ended: no stream entry, no ping, nothing drain could end.
347
+ const first = (event, value) => {
348
+ try {
349
+ return frame(undefined, event, data(JSON.stringify(value)));
350
+ }
351
+ catch (error) {
352
+ this.#throttle.log('error', 'SSE first frame seal failed; stream refused', 'SEAL_ENCODE_FAILED', error, { path: route.path, routeId: registered.id });
353
+ return undefined;
354
+ }
355
+ };
344
356
  if (route.version !== undefined) {
345
357
  const sent = single(query.dokebiVersion) ?? single(request.headers['x-dokebi-version']);
346
358
  if (sent !== route.version) {
@@ -349,12 +361,18 @@ export class Gateway {
349
361
  expected: route.version,
350
362
  received: typeof sent === 'string' ? sent.slice(0, 64) : null,
351
363
  });
364
+ const reload = first(RELOAD_EVENT, { version: route.version });
365
+ if (reload === undefined)
366
+ return refuse(500, { error: 'SEAL_FAILED' });
352
367
  reply.hijack();
353
368
  res.writeHead(200, headers);
354
- res.end(`retry: ${this.options.reloadRetryMs}\n${frame(undefined, RELOAD_EVENT, data(JSON.stringify({ version: route.version })))}`);
369
+ res.end(`retry: ${this.options.reloadRetryMs}\n${reload}`);
355
370
  return;
356
371
  }
357
372
  }
373
+ const opening = first(OPEN_EVENT, { retryMs: this.options.retryMs });
374
+ if (opening === undefined)
375
+ return refuse(500, { error: 'SEAL_FAILED' });
358
376
  const header = single(request.headers['last-event-id']);
359
377
  const asked = header ?? single(query.lastEventId);
360
378
  const lastEventId = typeof asked === 'string' && asked.length <= MAX_TEXT && isLine(asked) ? asked : undefined;
@@ -366,7 +384,7 @@ export class Gateway {
366
384
  res.writeHead(200, headers);
367
385
  // The reconnect hint first, and the same value as data for clients that manage reconnects
368
386
  // themselves (EventSource does not expose `retry:`).
369
- res.write(`retry: ${this.options.retryMs}\n${frame(undefined, OPEN_EVENT, data(JSON.stringify({ retryMs: this.options.retryMs })))}`);
387
+ res.write(`retry: ${this.options.retryMs}\n${opening}`);
370
388
  const entry = this.#add(res, identity, route, rooms, lastEventId, seal);
371
389
  res.once('close', () => this.#closed(entry));
372
390
  if (identity.authorization) {
package/dist/plugin.js CHANGED
@@ -109,7 +109,7 @@ class SseRuntime {
109
109
  /** Factory input: `{ app, options? }`. Options default to `app.config['dokebi-sse']`. */
110
110
  export const createPlugin = definePlugin({
111
111
  name: 'dokebi-sse',
112
- version: '0.4.4',
112
+ version: '0.4.5',
113
113
  core: '^0.4.2',
114
114
  // The declaration (not parseOptions) so core logs OPTION_DEPRECATED for old keys (bus, closeTimeoutMs).
115
115
  options: declaration,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dokebi/sse",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Server-sent events for @dokebi/web: authenticated streams, rooms, cross-worker fan-out over @dokebi/event-bus, and a browser client",
5
5
  "keywords": [
6
6
  "dokebi",
@@ -69,11 +69,11 @@
69
69
  },
70
70
  "devDependencies": {
71
71
  "@dokebi/cluster": "0.4.2",
72
- "@dokebi/config": "0.4.5",
73
- "@dokebi/event-bus": "0.4.2",
72
+ "@dokebi/config": "0.4.6",
73
+ "@dokebi/event-bus": "0.4.3",
74
74
  "@dokebi/health": "0.4.1",
75
- "@dokebi/nats": "0.4.1",
76
- "@dokebi/web": "0.4.4",
75
+ "@dokebi/nats": "0.4.2",
76
+ "@dokebi/web": "0.4.7",
77
77
  "react": "19.3.0",
78
78
  "react-test-renderer": "19.3.0",
79
79
  "vite": "8.3.4",