pi-roundtable-webchat 0.9.8 → 0.9.10
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/CHANGELOG.md +12 -0
- package/README.md +10 -1
- package/package.json +2 -2
- package/src/chat.ts +4 -0
- package/src/connections.ts +6 -0
- package/src/origin.ts +17 -0
- package/src/plugin.ts +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.9.10] - 2026-10-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- A turn run for a message carries the `Origin` header its connection's upgrade was accepted with, as `origin` on `context.turns.run`, so a tool reads it as `ToolTurn.origin`. It is trimmed and lower-cased and kept only when it is a serialized `scheme://host[:port]` of at most 256 characters; a connection that sent none, or an `Origin` of another shape, leaves it undefined. It is read from the transport at the upgrade, never from a frame or the message text. Needs pi-roundtable 0.9.10 or later to reach tools; on an older core the extra field is ignored.
|
|
13
|
+
|
|
14
|
+
## [0.9.9] - 2026-10-10
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- A privacy note in the README for the core's `attachments.retention`: a host can have the files messages used removed after a period, which also gives the person's `usedAttachmentBytesPerPrincipal` allowance back. The plugin needs no setting of its own for it. `memoryAttachments` (the test double) implements the port's new optional `expireUsed`.
|
|
19
|
+
|
|
8
20
|
## [0.9.8] - 2026-10-10
|
|
9
21
|
|
|
10
22
|
- Release in lockstep with pi-roundtable 0.9.8; no package-specific behavior changes.
|
package/README.md
CHANGED
|
@@ -169,6 +169,9 @@ The token is never read from a URL.
|
|
|
169
169
|
- **Another client**, such as a service, sends `Authorization: Bearer <token>` on the upgrade and offers `roundtable.webchat.v1`.
|
|
170
170
|
|
|
171
171
|
The server echoes `roundtable.webchat.v1`.
|
|
172
|
+
The `Origin` an accepted upgrade carried is kept on the connection, trimmed and lower-cased, and each message's turn passes it to the core as `origin`, so a tool reads it as `turn.origin` (for example to write where an issue was filed from).
|
|
173
|
+
It is read from the transport only, never from a frame or the message text.
|
|
174
|
+
A client that sent none, which only a route with `origins: "any"` admits, or a header that is not a serialized `scheme://host[:port]`, leaves `turn.origin` undefined; a tool that needs it should fail closed then.
|
|
172
175
|
An upgrade from an origin not in `origins`, or without one, is refused with 403; without a valid ticket or token with 401; for a person the core policy does not admit with 403; past the person's connection limit with 429.
|
|
173
176
|
|
|
174
177
|
```js
|
|
@@ -297,6 +300,12 @@ An upload no message used is deleted after `unsentUploadTtlMs` (24 hours): the p
|
|
|
297
300
|
Files a message used stay with their conversation, in the host's data directory, as long as the conversation does: when the conversation is deleted, the core removes them, the uploads waiting for it, and their share of the person's allowance.
|
|
298
301
|
This plugin has no way to delete a conversation itself; a host surface that deletes one does it through the core.
|
|
299
302
|
|
|
303
|
+
**Privacy: removing used files after a period.** A host that must not keep uploads sets the core's `attachments.retention` (`{ maxAgeMs, sweepEveryMs? }` in the host configuration; see the core's [plugin guide](https://github.com/wayne930242/pi-roundtable/blob/master/docs/plugins.md)), for example `{ maxAgeMs: 7 * 24 * 60 * 60_000 }` to keep no upload longer than seven days after the message that used it.
|
|
304
|
+
The host then deletes the files and their records, and takes their bytes off the person's `usedAttachmentBytesPerPrincipal` allowance, so a person who sends files for months is not refused with `attachment_quota` for good.
|
|
305
|
+
A model or tool that asks for a removed file is told it was removed after its retention period.
|
|
306
|
+
The transcript is a separate record: this removes the files, not the messages that named them or what the model said about them. Uploads that no message used are deleted after `unsentUploadTtlMs` as before, whatever the retention.
|
|
307
|
+
This needs a pi-roundtable release that has `attachments.retention`; on an earlier core the plugin keeps working and files simply live as long as their conversation.
|
|
308
|
+
|
|
300
309
|
Uploads are as private as the transcript: files sit in the host's data directory under the private conversation, where the owner and the operator can read them, as they can the transcript.
|
|
301
310
|
Only the person who uploaded a file can use it, and only in the conversation it was uploaded to; a shared conversation (one recorded for no one in particular) takes no uploads.
|
|
302
311
|
|
|
@@ -343,7 +352,7 @@ Only the person who uploaded a file can use it, and only in the conversation it
|
|
|
343
352
|
| `attachmentsPerMessage` | 8 files named by one message |
|
|
344
353
|
| `uploadsPerHour` | 60 uploads per person in any hour |
|
|
345
354
|
| `unsentUploadBytesPerPrincipal` | 64 MiB per person uploaded and not yet used by a message; uploads arriving together are counted per web chat plugin instance, so two web chats on one host each hold a person to it separately |
|
|
346
|
-
| `usedAttachmentBytesPerPrincipal` | 1 GiB per person kept by their messages across all conversations until a conversation is deleted; needs pi-roundtable 0.9.3 to be enforced |
|
|
355
|
+
| `usedAttachmentBytesPerPrincipal` | 1 GiB per person kept by their messages across all conversations until a conversation is deleted or the host's `attachments.retention` removes the files; needs pi-roundtable 0.9.3 to be enforced |
|
|
347
356
|
| `attachmentTypes` | `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `application/json`, `text/plain`, `application/pdf` |
|
|
348
357
|
| `unsentUploadTtlMs` | 24 hours before an unused upload is deleted |
|
|
349
358
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-roundtable-webchat",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.10",
|
|
4
4
|
"description": "A WebSocket chat adapter for pi-roundtable: private web conversations for people an OpenID Connect provider signs in, with live progress and approval cards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@earendil-works/pi-ai": ">=1.0.0 <2",
|
|
44
44
|
"@earendil-works/pi-coding-agent": ">=1.0.0 <2",
|
|
45
45
|
"@types/bun": "1.4.2",
|
|
46
|
-
"pi-roundtable": "0.9.
|
|
46
|
+
"pi-roundtable": "0.9.10",
|
|
47
47
|
"typebox": "1.3.34",
|
|
48
48
|
"typescript": "7.0.2"
|
|
49
49
|
}
|
package/src/chat.ts
CHANGED
|
@@ -112,6 +112,8 @@ interface Minted {
|
|
|
112
112
|
|
|
113
113
|
interface Pending {
|
|
114
114
|
speaker: Speaker;
|
|
115
|
+
/** The origin of the connection the message arrived on. */
|
|
116
|
+
origin?: string;
|
|
115
117
|
persona: WebPersona;
|
|
116
118
|
title: string;
|
|
117
119
|
/** The files the message references, moved into the conversation when it was accepted. */
|
|
@@ -600,6 +602,7 @@ export class WebChat {
|
|
|
600
602
|
const messageId = crypto.randomUUID();
|
|
601
603
|
this.#pending.set(messageId, {
|
|
602
604
|
speaker,
|
|
605
|
+
...(connection.origin ? { origin: connection.origin } : {}),
|
|
603
606
|
persona,
|
|
604
607
|
title: minted?.title ?? titleOf(text),
|
|
605
608
|
fresh: record === undefined,
|
|
@@ -805,6 +808,7 @@ export class WebChat {
|
|
|
805
808
|
kind: persona.kind,
|
|
806
809
|
text,
|
|
807
810
|
speaker,
|
|
811
|
+
...(pending.origin ? { origin: pending.origin } : {}),
|
|
808
812
|
interactive: true,
|
|
809
813
|
...(pending.attachments ? { attachments: pending.attachments } : {}),
|
|
810
814
|
...(persona.selection
|
package/src/connections.ts
CHANGED
|
@@ -6,6 +6,12 @@ import type { ServerFrame } from "./protocol.ts";
|
|
|
6
6
|
export interface Connection {
|
|
7
7
|
identity: WebIdentity;
|
|
8
8
|
speaker: Speaker;
|
|
9
|
+
/**
|
|
10
|
+
* The normalized `Origin` the upgrade carried and the route accepted; undefined when it sent none
|
|
11
|
+
* (a client that is not a browser, on a route that admits `"any"` origin). Fixed for the
|
|
12
|
+
* connection's life: it comes from the transport, never from a frame.
|
|
13
|
+
*/
|
|
14
|
+
origin?: string;
|
|
9
15
|
socket?: RouteSocket<Connection>;
|
|
10
16
|
/** Asks for a fresh token, then closes the socket when the token expires. */
|
|
11
17
|
timers: ReturnType<typeof setTimeout>[];
|
package/src/origin.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The longest origin kept; a longer header is not a browser's. */
|
|
2
|
+
const MAX_ORIGIN_LENGTH = 256;
|
|
3
|
+
|
|
4
|
+
/** A serialized origin: a scheme, `://`, and a host with an optional port, with no path, query or space. */
|
|
5
|
+
const ORIGIN = /^[a-z][a-z0-9+.-]*:\/\/[^\s/?#@]+$/;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The connection's `Origin` header as a tool may label a record with it: trimmed and lower-cased,
|
|
9
|
+
* or undefined when the header is absent, the opaque `null`, over-long, or not a serialized origin.
|
|
10
|
+
* Browsers always send one; a client that is not a browser sends any text it likes, so the shape
|
|
11
|
+
* is checked here and nothing else is trusted about it.
|
|
12
|
+
*/
|
|
13
|
+
export function normalizeOrigin(header: string | null): string | undefined {
|
|
14
|
+
const value = header?.trim().toLowerCase();
|
|
15
|
+
if (!value || value.length > MAX_ORIGIN_LENGTH) return undefined;
|
|
16
|
+
return ORIGIN.test(value) ? value : undefined;
|
|
17
|
+
}
|
package/src/plugin.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { Connection } from "./connections.ts";
|
|
|
21
21
|
import { webDirectChannel } from "./direct-channel.ts";
|
|
22
22
|
import { PgNotices } from "./notices.ts";
|
|
23
23
|
import { TokenRefused, type TokenVerifier } from "./oidc.ts";
|
|
24
|
+
import { normalizeOrigin } from "./origin.ts";
|
|
24
25
|
import { TICKET_PROTOCOL_PREFIX, WEBCHAT_PROTOCOL } from "./protocol.ts";
|
|
25
26
|
import { restHandler } from "./rest.ts";
|
|
26
27
|
import { TicketBook } from "./tickets.ts";
|
|
@@ -240,7 +241,12 @@ export function webChat(options: WebChatOptions): RoundtablePlugin {
|
|
|
240
241
|
if (error instanceof Refusal) return refuse(403, "Forbidden");
|
|
241
242
|
throw error;
|
|
242
243
|
}
|
|
243
|
-
const
|
|
244
|
+
const origin = normalizeOrigin(request.headers.get("origin"));
|
|
245
|
+
const connection: Connection = {
|
|
246
|
+
...admitted,
|
|
247
|
+
...(origin ? { origin } : {}),
|
|
248
|
+
timers: [],
|
|
249
|
+
};
|
|
244
250
|
if (!chat.connections.reserve(connection))
|
|
245
251
|
return refuse(429, "Too Many Connections");
|
|
246
252
|
return {
|