pi-roundtable-webchat 0.8.0 → 0.9.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 +18 -0
- package/README.md +55 -26
- package/package.json +3 -3
- package/src/chat.ts +169 -31
- package/src/connections.ts +4 -4
- package/src/direct-channel.ts +66 -0
- package/src/index.ts +2 -2
- package/src/notices.ts +131 -0
- package/src/oidc.ts +47 -4
- package/src/plugin.ts +25 -14
- package/src/prompts.ts +12 -12
- package/src/protocol.ts +4 -1
- package/src/rest.ts +30 -2
- package/src/surface.ts +11 -7
- package/src/tickets.ts +16 -8
- package/src/access.ts +0 -99
package/src/tickets.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface TicketBookOptions {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
interface Held {
|
|
17
|
+
principalId: string;
|
|
17
18
|
identity: WebIdentity;
|
|
18
19
|
expiresAt: number;
|
|
19
20
|
}
|
|
@@ -40,10 +41,13 @@ export class TicketBook {
|
|
|
40
41
|
this.#now = options.now ?? Date.now;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
issue(
|
|
44
|
+
issue(
|
|
45
|
+
identity: WebIdentity,
|
|
46
|
+
principalId: string,
|
|
47
|
+
): { ticket: string; expiresAt: Date } {
|
|
44
48
|
const now = this.#now();
|
|
45
49
|
this.#sweep(now);
|
|
46
|
-
const mine = this.#byPerson.get(
|
|
50
|
+
const mine = this.#byPerson.get(principalId) ?? new Set<string>();
|
|
47
51
|
for (const oldest of mine) {
|
|
48
52
|
if (mine.size < this.#perPrincipal) break;
|
|
49
53
|
this.#drop(oldest);
|
|
@@ -57,27 +61,31 @@ export class TicketBook {
|
|
|
57
61
|
crypto.getRandomValues(new Uint8Array(32)),
|
|
58
62
|
).toString("base64url");
|
|
59
63
|
const expiresAt = Math.min(now + this.#ttlMs, identity.expiresAt.getTime());
|
|
60
|
-
this.#held.set(ticket, { identity, expiresAt });
|
|
64
|
+
this.#held.set(ticket, { identity, principalId, expiresAt });
|
|
61
65
|
mine.add(ticket);
|
|
62
|
-
this.#byPerson.set(
|
|
66
|
+
this.#byPerson.set(principalId, mine);
|
|
63
67
|
return { ticket, expiresAt: new Date(expiresAt) };
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
/** The person the ticket was issued to, once; undefined for a spent, old, or unknown ticket. */
|
|
67
|
-
redeem(
|
|
71
|
+
redeem(
|
|
72
|
+
ticket: string,
|
|
73
|
+
): { identity: WebIdentity; principalId: string } | undefined {
|
|
68
74
|
const held = this.#held.get(ticket);
|
|
69
75
|
if (!held) return undefined;
|
|
70
76
|
this.#drop(ticket);
|
|
71
|
-
return held.expiresAt >= this.#now()
|
|
77
|
+
return held.expiresAt >= this.#now()
|
|
78
|
+
? { identity: held.identity, principalId: held.principalId }
|
|
79
|
+
: undefined;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
#drop(ticket: string): void {
|
|
75
83
|
const held = this.#held.get(ticket);
|
|
76
84
|
if (!held) return;
|
|
77
85
|
this.#held.delete(ticket);
|
|
78
|
-
const mine = this.#byPerson.get(held.
|
|
86
|
+
const mine = this.#byPerson.get(held.principalId);
|
|
79
87
|
mine?.delete(ticket);
|
|
80
|
-
if (mine?.size === 0) this.#byPerson.delete(held.
|
|
88
|
+
if (mine?.size === 0) this.#byPerson.delete(held.principalId);
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
#sweep(now: number): void {
|
package/src/access.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import type { Tier } from "pi-roundtable";
|
|
2
|
-
import type { WebIdentity } from "./oidc.ts";
|
|
3
|
-
|
|
4
|
-
/** Who holds a tier: speaker ids, role or group names from the token, or everyone the provider signs in. */
|
|
5
|
-
export interface WebTierMembers {
|
|
6
|
-
users?: readonly string[];
|
|
7
|
-
roles?: readonly string[];
|
|
8
|
-
everyone?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Who may chat, and at which tier. Owners are named only by speaker id, so no claim a provider
|
|
13
|
-
* issues can make someone the owner; admins and members may be named by role as well. A person
|
|
14
|
-
* the map gives no tier is not admitted: no connection, no conversation, no turn.
|
|
15
|
-
*/
|
|
16
|
-
export interface WebAccessMap {
|
|
17
|
-
/** Speaker ids, such as `oidcSpeakerId(issuer, subject)`; never roles. */
|
|
18
|
-
owners?: readonly string[];
|
|
19
|
-
admins?: WebTierMembers;
|
|
20
|
-
members?: WebTierMembers;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** The tier a verified person chats at, or undefined when they are not admitted. */
|
|
24
|
-
export interface WebAccess {
|
|
25
|
-
tierOf(identity: WebIdentity): Tier | undefined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const isStrings = (value: unknown): value is readonly string[] =>
|
|
29
|
-
Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Throws unless `tier` is a `WebTierMembers` whose `users` and `roles` are lists of strings and
|
|
33
|
-
* whose `everyone` is a boolean: a JavaScript configuration is not type-checked, and a string
|
|
34
|
-
* where a list belongs would match its substrings.
|
|
35
|
-
*/
|
|
36
|
-
function checkTier(name: string, tier: unknown): void {
|
|
37
|
-
if (tier === undefined) return;
|
|
38
|
-
if (typeof tier !== "object" || tier === null || Array.isArray(tier))
|
|
39
|
-
throw new Error(
|
|
40
|
-
`webAccess: ${name} is { users?, roles?, everyone? }; got ${JSON.stringify(tier)}`,
|
|
41
|
-
);
|
|
42
|
-
const { users, roles, everyone } = tier as Record<string, unknown>;
|
|
43
|
-
for (const [field, value] of [
|
|
44
|
-
["users", users],
|
|
45
|
-
["roles", roles],
|
|
46
|
-
] as const)
|
|
47
|
-
if (value !== undefined && !isStrings(value))
|
|
48
|
-
throw new Error(
|
|
49
|
-
`webAccess: ${name}.${field} must be a list of strings; got ${JSON.stringify(value)}`,
|
|
50
|
-
);
|
|
51
|
-
if (everyone !== undefined && typeof everyone !== "boolean")
|
|
52
|
-
throw new Error(
|
|
53
|
-
`webAccess: ${name}.everyone must be true or false; got ${JSON.stringify(everyone)}`,
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function admitsSomeone(tier: WebTierMembers | undefined): boolean {
|
|
58
|
-
return (
|
|
59
|
-
tier !== undefined &&
|
|
60
|
-
(tier.everyone === true ||
|
|
61
|
-
(tier.users?.length ?? 0) > 0 ||
|
|
62
|
-
(tier.roles?.length ?? 0) > 0)
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The policy of an access map: the highest tier a person qualifies for wins. A map that admits
|
|
68
|
-
* no one is a configuration mistake and throws.
|
|
69
|
-
*/
|
|
70
|
-
export function webAccess(map: WebAccessMap): WebAccess {
|
|
71
|
-
if (map.owners !== undefined && !isStrings(map.owners))
|
|
72
|
-
throw new Error(
|
|
73
|
-
"webAccess: owners is a list of speaker ids; a provider's roles cannot name an owner",
|
|
74
|
-
);
|
|
75
|
-
checkTier("admins", map.admins);
|
|
76
|
-
checkTier("members", map.members);
|
|
77
|
-
const owners = new Set(map.owners ?? []);
|
|
78
|
-
if (
|
|
79
|
-
owners.size === 0 &&
|
|
80
|
-
!admitsSomeone(map.admins) &&
|
|
81
|
-
!admitsSomeone(map.members)
|
|
82
|
-
)
|
|
83
|
-
throw new Error(
|
|
84
|
-
"webAccess: the map admits no one; name owners, or admins or members by user, role, or everyone",
|
|
85
|
-
);
|
|
86
|
-
const holds = (tier: WebTierMembers | undefined, identity: WebIdentity) =>
|
|
87
|
-
tier !== undefined &&
|
|
88
|
-
(tier.everyone === true ||
|
|
89
|
-
tier.users?.includes(identity.id) === true ||
|
|
90
|
-
identity.roles.some((role) => tier.roles?.includes(role)));
|
|
91
|
-
return {
|
|
92
|
-
tierOf(identity) {
|
|
93
|
-
if (owners.has(identity.id)) return "owner";
|
|
94
|
-
if (holds(map.admins, identity)) return "admin";
|
|
95
|
-
if (holds(map.members, identity)) return "member";
|
|
96
|
-
return undefined;
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
}
|