pi-roundtable-webchat 0.8.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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/package.json +50 -0
- package/src/access.ts +99 -0
- package/src/budget.ts +74 -0
- package/src/chat.ts +661 -0
- package/src/connections.ts +99 -0
- package/src/index.ts +32 -0
- package/src/oidc.ts +237 -0
- package/src/plugin.ts +224 -0
- package/src/prompts.ts +198 -0
- package/src/protocol.ts +202 -0
- package/src/rest.ts +158 -0
- package/src/surface.ts +126 -0
- package/src/tickets.ts +87 -0
package/src/tickets.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { WebIdentity } from "./oidc.ts";
|
|
2
|
+
|
|
3
|
+
export interface TicketBookOptions {
|
|
4
|
+
/** How long a ticket may wait before it is spent. */
|
|
5
|
+
ttlMs: number;
|
|
6
|
+
/** The most tickets held at once, for everyone together; issuing one more drops the oldest. Default 10 000. */
|
|
7
|
+
max?: number;
|
|
8
|
+
/**
|
|
9
|
+
* The most tickets one person holds at once; issuing one more drops that person's oldest, so
|
|
10
|
+
* nobody can push out anyone else's. Default 5.
|
|
11
|
+
*/
|
|
12
|
+
perPrincipal?: number;
|
|
13
|
+
now?: () => number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Held {
|
|
17
|
+
identity: WebIdentity;
|
|
18
|
+
expiresAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One-time tickets: a REST call authenticated with a bearer token gets a ticket, which a browser
|
|
23
|
+
* spends once to open a WebSocket, because a browser cannot set a header on a WebSocket and the
|
|
24
|
+
* token must never sit in a URL. A ticket is random, short-lived, never outlives its token, and
|
|
25
|
+
* lives only in memory.
|
|
26
|
+
*/
|
|
27
|
+
export class TicketBook {
|
|
28
|
+
readonly #held = new Map<string, Held>();
|
|
29
|
+
/** Each person's tickets, oldest first. */
|
|
30
|
+
readonly #byPerson = new Map<string, Set<string>>();
|
|
31
|
+
readonly #ttlMs: number;
|
|
32
|
+
readonly #max: number;
|
|
33
|
+
readonly #perPrincipal: number;
|
|
34
|
+
readonly #now: () => number;
|
|
35
|
+
|
|
36
|
+
constructor(options: TicketBookOptions) {
|
|
37
|
+
this.#ttlMs = options.ttlMs;
|
|
38
|
+
this.#max = options.max ?? 10_000;
|
|
39
|
+
this.#perPrincipal = options.perPrincipal ?? 5;
|
|
40
|
+
this.#now = options.now ?? Date.now;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
issue(identity: WebIdentity): { ticket: string; expiresAt: Date } {
|
|
44
|
+
const now = this.#now();
|
|
45
|
+
this.#sweep(now);
|
|
46
|
+
const mine = this.#byPerson.get(identity.id) ?? new Set<string>();
|
|
47
|
+
for (const oldest of mine) {
|
|
48
|
+
if (mine.size < this.#perPrincipal) break;
|
|
49
|
+
this.#drop(oldest);
|
|
50
|
+
}
|
|
51
|
+
while (this.#held.size >= this.#max) {
|
|
52
|
+
const oldest = this.#held.keys().next().value;
|
|
53
|
+
if (oldest === undefined) break;
|
|
54
|
+
this.#drop(oldest);
|
|
55
|
+
}
|
|
56
|
+
const ticket = Buffer.from(
|
|
57
|
+
crypto.getRandomValues(new Uint8Array(32)),
|
|
58
|
+
).toString("base64url");
|
|
59
|
+
const expiresAt = Math.min(now + this.#ttlMs, identity.expiresAt.getTime());
|
|
60
|
+
this.#held.set(ticket, { identity, expiresAt });
|
|
61
|
+
mine.add(ticket);
|
|
62
|
+
this.#byPerson.set(identity.id, mine);
|
|
63
|
+
return { ticket, expiresAt: new Date(expiresAt) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The person the ticket was issued to, once; undefined for a spent, old, or unknown ticket. */
|
|
67
|
+
redeem(ticket: string): WebIdentity | undefined {
|
|
68
|
+
const held = this.#held.get(ticket);
|
|
69
|
+
if (!held) return undefined;
|
|
70
|
+
this.#drop(ticket);
|
|
71
|
+
return held.expiresAt >= this.#now() ? held.identity : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#drop(ticket: string): void {
|
|
75
|
+
const held = this.#held.get(ticket);
|
|
76
|
+
if (!held) return;
|
|
77
|
+
this.#held.delete(ticket);
|
|
78
|
+
const mine = this.#byPerson.get(held.identity.id);
|
|
79
|
+
mine?.delete(ticket);
|
|
80
|
+
if (mine?.size === 0) this.#byPerson.delete(held.identity.id);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#sweep(now: number): void {
|
|
84
|
+
for (const [ticket, held] of this.#held)
|
|
85
|
+
if (held.expiresAt < now) this.#drop(ticket);
|
|
86
|
+
}
|
|
87
|
+
}
|