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/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(identity: WebIdentity): { ticket: string; expiresAt: Date } {
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(identity.id) ?? new Set<string>();
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(identity.id, mine);
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(ticket: string): WebIdentity | undefined {
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() ? held.identity : undefined;
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.identity.id);
86
+ const mine = this.#byPerson.get(held.principalId);
79
87
  mine?.delete(ticket);
80
- if (mine?.size === 0) this.#byPerson.delete(held.identity.id);
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
- }