agent-protocols 0.2.3 → 0.3.1

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/dist/discourse.js CHANGED
@@ -1,8 +1,19 @@
1
+ /**
2
+ * Agent Discourse Protocol 1.0: kernel types, the room type system, and
3
+ * verification helpers.
4
+ *
5
+ * The protocol defines nine built-in event types. Every other event type is
6
+ * declared per room as a schema-validated type definition, either inline or
7
+ * imported from a type pack. Hosts validate structure and permissions; they
8
+ * never need to understand application semantics.
9
+ */
10
+ import { Validator } from "@cfworker/json-schema";
1
11
  import canonicalize from "canonicalize";
2
12
  import { createHash } from "node:crypto";
3
13
  import { protocolError } from "./errors.js";
4
14
  import { createEvent, verifyEnvelope, withRoomId, } from "./identity.js";
5
15
  export const DISCOURSE_PROTOCOL = "agent-discourse/1.0";
16
+ /** The nine built-in event types. All other types are room-defined. */
6
17
  export const eventType = {
7
18
  ROOM_CREATE: "room.create",
8
19
  ROOM_JOIN: "room.join",
@@ -11,147 +22,309 @@ export const eventType = {
11
22
  ROOM_MEMBER_ROLE_UPDATE: "room.member.role.update",
12
23
  ROOM_CLOSE: "room.close",
13
24
  ROOM_CANCEL: "room.cancel",
25
+ TYPE_DEFINE: "type.define",
14
26
  MESSAGE_CREATE: "message.create",
15
- REACTION_CREATE: "reaction.create",
16
- MESSAGE_PROPOSAL_CREATE: "message.proposal.create",
17
- MESSAGE_POLL_CREATE: "message.poll.create",
18
- MESSAGE_POLL_VOTE: "message.poll.vote",
19
- MESSAGE_RESOLUTION_CREATE: "message.resolution.create",
20
- SOURCE_ADD: "source.add",
21
- TURN_UPDATE: "turn.update",
22
- QUESTION_CREATE: "question.create",
23
- ROOM_STEER: "room.steer",
24
- MAP_UPDATE: "map.update",
25
- ARTIFACT_CREATE: "artifact.create",
26
- SESSION_OFFER: "session.offer",
27
- SESSION_ANSWER: "session.answer",
28
- SESSION_CANDIDATE: "session.candidate",
29
- SESSION_CLOSE: "session.close",
30
27
  };
31
- const sessionMediaKinds = new Set([
32
- "audio",
33
- "video",
34
- "screen",
35
- "data",
36
- "file",
37
- ]);
28
+ export const BUILTIN_EVENT_TYPES = Object.values(eventType);
29
+ /** Custom event types must not use these prefixes. */
30
+ export const RESERVED_TYPE_PREFIXES = ["room.", "type."];
31
+ /** Registered type packs defined by the specification in `1.0.packs.json`. */
32
+ export const packId = {
33
+ REACTIONS: "adp:reactions/1.0",
34
+ DELIBERATION: "adp:deliberation/1.0",
35
+ CURATION: "adp:curation/1.0",
36
+ MODERATION: "adp:moderation/1.0",
37
+ REALTIME: "adp:realtime/1.0",
38
+ };
39
+ export const REGISTERED_PACK_IDS = Object.values(packId);
40
+ /** Indexes the packs of a document by pack id for registry materialization. */
41
+ export function packMap(document) {
42
+ const packs = {};
43
+ for (const pack of document.packs)
44
+ packs[pack.id] = pack;
45
+ return packs;
46
+ }
38
47
  export function roomCreateEvent(actor, createdAt, nonce, payload) {
39
48
  return createEvent(DISCOURSE_PROTOCOL, eventType.ROOM_CREATE, actor, createdAt, nonce, payload);
40
49
  }
50
+ export function typeDefineEvent(actor, createdAt, nonce, roomId, declaration) {
51
+ return withRoomId(createEvent(DISCOURSE_PROTOCOL, eventType.TYPE_DEFINE, actor, createdAt, nonce, declaration), roomId);
52
+ }
41
53
  export function discourseEvent(type, actor, createdAt, nonce, roomId, payload) {
42
54
  return withRoomId(createEvent(DISCOURSE_PROTOCOL, type, actor, createdAt, nonce, payload), roomId);
43
55
  }
56
+ export function isBuiltinEventType(type) {
57
+ return BUILTIN_EVENT_TYPES.includes(type);
58
+ }
59
+ export function eventRequiresRoomId(type) {
60
+ return type !== eventType.ROOM_CREATE;
61
+ }
44
62
  export function validateDiscourseEnvelope(envelope) {
45
63
  verifyEnvelope(envelope);
46
64
  const protocol = envelope.event.protocol;
47
65
  if (protocol !== DISCOURSE_PROTOCOL) {
48
66
  throw protocolError("invalid_event_protocol", `expected ${DISCOURSE_PROTOCOL}, got ${protocol}`);
49
67
  }
50
- if (eventRequiresRoomId(envelope.event.type) &&
51
- envelope.event.room_id === undefined) {
68
+ if (envelope.event.type === eventType.ROOM_CREATE) {
69
+ if (envelope.event.room_id !== undefined) {
70
+ throw protocolError("invalid_event", "room.create must not include room_id");
71
+ }
72
+ }
73
+ else if (envelope.event.room_id === undefined) {
52
74
  throw protocolError("missing_room_id", "event requires a room_id");
53
75
  }
54
76
  }
55
77
  export function validateRoomPath(envelope, pathRoomId) {
56
78
  const actual = envelope.event.room_id;
57
- if (actual === undefined && envelope.event.type === eventType.ROOM_CREATE)
79
+ if (envelope.event.type === eventType.ROOM_CREATE) {
80
+ if (actual !== undefined) {
81
+ throw protocolError("invalid_event", "room.create must not include room_id");
82
+ }
58
83
  return;
84
+ }
59
85
  if (actual === undefined)
60
86
  throw protocolError("missing_room_id", "event requires a room_id");
61
87
  if (actual !== pathRoomId)
62
88
  throw protocolError("room_id_mismatch", `expected ${pathRoomId}, got ${actual}`);
63
89
  }
64
- export function eventRequiresRoomId(type) {
65
- return type !== eventType.ROOM_CREATE;
90
+ /**
91
+ * Checks the shape of a custom event type name: lowercase dot-separated, at
92
+ * least two segments, not built-in, not under a reserved prefix.
93
+ */
94
+ export function validateCustomEventTypeName(name) {
95
+ const segments = name.split(".");
96
+ const validShape = segments.length >= 2 &&
97
+ segments.every((segment) => /^[a-z0-9][a-z0-9_-]*$/.test(segment));
98
+ if (!validShape) {
99
+ throw protocolError("invalid_event", `invalid event type name: ${name}`);
100
+ }
101
+ if (isBuiltinEventType(name)) {
102
+ throw protocolError("invalid_event", `${name} is a built-in event type`);
103
+ }
104
+ if (RESERVED_TYPE_PREFIXES.some((prefix) => name.startsWith(prefix))) {
105
+ throw protocolError("invalid_event", `${name} uses a reserved type prefix`);
106
+ }
107
+ }
108
+ export function isPackImport(declaration) {
109
+ return (typeof declaration === "object" &&
110
+ declaration !== null &&
111
+ ("use" in declaration || "pack" in declaration || "digest" in declaration));
112
+ }
113
+ export function isTypeDef(declaration) {
114
+ return (typeof declaration === "object" &&
115
+ declaration !== null &&
116
+ !isPackImport(declaration) &&
117
+ "type" in declaration);
118
+ }
119
+ export function validateTypeDef(def) {
120
+ validateCustomEventTypeName(def.type);
121
+ if (!["message", "signal", "control"].includes(def.kind)) {
122
+ throw protocolError("invalid_event", `invalid type kind: ${def.kind}`);
123
+ }
124
+ if (typeof def.title !== "string" || def.title.trim() === "") {
125
+ throw protocolError("invalid_event", "type definition title must not be empty");
126
+ }
127
+ if (typeof def.schema !== "object" ||
128
+ def.schema === null ||
129
+ Array.isArray(def.schema)) {
130
+ throw protocolError("invalid_event", "type definition schema must be a JSON Schema object");
131
+ }
132
+ compileSchema(def.schema);
133
+ if (def.roles !== undefined && def.roles.length === 0) {
134
+ throw protocolError("invalid_event", "type definition roles must not be empty");
135
+ }
136
+ for (const hint of [def.rate_hint, def.max_payload_hint]) {
137
+ if (hint !== undefined && (!Number.isInteger(hint) || hint < 1)) {
138
+ throw protocolError("invalid_event", "type definition hints must be positive integers");
139
+ }
140
+ }
141
+ }
142
+ export function validatePackImport(declaration) {
143
+ const hasUse = declaration.use !== undefined;
144
+ const hasExternal = declaration.pack !== undefined && declaration.digest !== undefined;
145
+ if (hasUse) {
146
+ if (declaration.pack !== undefined || declaration.digest !== undefined) {
147
+ throw protocolError("invalid_event", "pack import requires either use, or pack with digest");
148
+ }
149
+ if (!isRegisteredPackId(declaration.use)) {
150
+ throw protocolError("invalid_event", `invalid registered pack id: ${declaration.use}`);
151
+ }
152
+ }
153
+ else if (hasExternal) {
154
+ if (declaration.digest.trim() === "") {
155
+ throw protocolError("invalid_event", "external pack digest must not be empty");
156
+ }
157
+ }
158
+ else {
159
+ throw protocolError("invalid_event", "pack import requires either use, or pack with digest");
160
+ }
161
+ if (declaration.types !== undefined && declaration.types.length === 0) {
162
+ throw protocolError("invalid_event", "pack import types subset must not be empty");
163
+ }
164
+ }
165
+ export function validateTypeDeclaration(declaration) {
166
+ if (isPackImport(declaration)) {
167
+ validatePackImport(declaration);
168
+ }
169
+ else if (isTypeDef(declaration)) {
170
+ validateTypeDef(declaration);
171
+ }
172
+ else {
173
+ throw protocolError("invalid_event", "type declaration must be an inline definition or a pack import");
174
+ }
175
+ }
176
+ function isRegisteredPackId(id) {
177
+ return /^adp:[a-z0-9-]+\/[0-9]+\.[0-9]+$/.test(id);
66
178
  }
67
179
  export function validateRoomCreatePayload(payload) {
68
180
  if (payload.topic.trim() === "") {
69
- throw protocolError("invalid_room", "room topic must not be empty");
181
+ throw protocolError("invalid_event", "room topic must not be empty");
70
182
  }
71
183
  if (payload.start_time >= payload.end_time) {
72
- throw protocolError("invalid_room", "start_time must be before end_time");
184
+ throw protocolError("invalid_event", "start_time must be before end_time");
185
+ }
186
+ const maxSpeakers = payload.policy?.max_speakers;
187
+ if (maxSpeakers !== undefined &&
188
+ (!Number.isInteger(maxSpeakers) || maxSpeakers < 1)) {
189
+ throw protocolError("invalid_event", "max_speakers must be a positive integer");
73
190
  }
74
- const maxParticipants = payload.policy?.max_participants;
75
- if (maxParticipants !== undefined &&
76
- (!Number.isInteger(maxParticipants) || maxParticipants < 1)) {
77
- throw protocolError("invalid_room", "max_participants must be a positive integer");
191
+ for (const declaration of payload.types ?? []) {
192
+ validateTypeDeclaration(declaration);
78
193
  }
79
194
  }
80
- export function validatePollCreatePayload(payload) {
81
- if (payload.poll_id.trim() === "" || payload.question.trim() === "") {
82
- throw protocolError("invalid_poll", "poll_id and question are required");
195
+ export function validateMessageCreatePayload(payload) {
196
+ if (payload.content_type.trim() === "") {
197
+ throw protocolError("invalid_event", "content_type must not be empty");
83
198
  }
84
- if (payload.options.length < 2) {
85
- throw protocolError("invalid_poll", "poll requires at least two options");
199
+ }
200
+ /** The effective set of type definitions active in a room. */
201
+ export class TypeRegistry {
202
+ types = new Map();
203
+ /**
204
+ * Materializes a registry from declarations, resolving pack imports from
205
+ * `packs`, keyed by registered pack id or external pack URI.
206
+ */
207
+ static fromDeclarations(declarations, packs = {}) {
208
+ const registry = new TypeRegistry();
209
+ for (const declaration of declarations) {
210
+ registry.apply(declaration, packs);
211
+ }
212
+ return registry;
86
213
  }
87
- const optionIds = new Set();
88
- for (const option of payload.options) {
89
- if (option.id.trim() === "" || option.label.trim() === "") {
90
- throw protocolError("invalid_poll", "option id and label are required");
214
+ /**
215
+ * Applies one declaration: an inline definition or a pack import.
216
+ * Redefining an existing type replaces it; the latest definition wins.
217
+ */
218
+ apply(declaration, packs = {}) {
219
+ if (isPackImport(declaration)) {
220
+ this.import(declaration, packs);
91
221
  }
92
- if (optionIds.has(option.id)) {
93
- throw protocolError("invalid_poll", "poll option ids must be unique");
222
+ else if (isTypeDef(declaration)) {
223
+ this.define(declaration);
224
+ }
225
+ else {
226
+ throw protocolError("invalid_event", "type declaration must be an inline definition or a pack import");
94
227
  }
95
- optionIds.add(option.id);
96
228
  }
97
- const minChoices = payload.min_choices ?? 1;
98
- const maxChoices = payload.max_choices ?? 1;
99
- if (minChoices < 1 || maxChoices < minChoices) {
100
- throw protocolError("invalid_poll", "invalid poll choice limits");
229
+ define(def) {
230
+ validateTypeDef(def);
231
+ this.types.set(def.type, def);
101
232
  }
102
- }
103
- export function validatePollVotePayload(payload, poll, nowMs) {
104
- if (poll.closes_at !== undefined && nowMs !== undefined && nowMs > poll.closes_at) {
105
- throw protocolError("poll_closed", "poll is closed");
106
- }
107
- const minChoices = poll.min_choices ?? 1;
108
- const maxChoices = poll.max_choices ?? 1;
109
- const optionIds = new Set(poll.options.map((option) => option.id));
110
- const selected = new Set(payload.option_ids);
111
- if (selected.size !== payload.option_ids.length) {
112
- throw protocolError("invalid_poll_vote", "duplicate poll options");
233
+ import(declaration, packs) {
234
+ validatePackImport(declaration);
235
+ const reference = declaration.use ?? declaration.pack;
236
+ const pack = packs[reference];
237
+ if (!pack) {
238
+ throw protocolError("pack_unavailable", `pack not available: ${reference}`);
239
+ }
240
+ const available = new Set(pack.types.map((def) => def.type));
241
+ for (const name of declaration.types ?? []) {
242
+ if (!available.has(name)) {
243
+ throw protocolError("pack_unavailable", `type ${name} is not in pack ${reference}`);
244
+ }
245
+ }
246
+ const subset = declaration.types !== undefined ? new Set(declaration.types) : undefined;
247
+ for (const name of Object.keys(declaration.overrides ?? {})) {
248
+ const imported = subset ? subset.has(name) : available.has(name);
249
+ if (!imported) {
250
+ throw protocolError("invalid_event", `override target ${name} is not imported from pack ${reference}`);
251
+ }
252
+ }
253
+ for (const def of pack.types) {
254
+ if (subset && !subset.has(def.type))
255
+ continue;
256
+ const override = declaration.overrides?.[def.type];
257
+ this.define(override ? { ...def, ...override } : { ...def });
258
+ }
113
259
  }
114
- if (selected.size < minChoices || selected.size > maxChoices) {
115
- throw protocolError("invalid_poll_vote", "invalid number of options");
260
+ get(type) {
261
+ return this.types.get(type);
116
262
  }
117
- for (const optionId of selected) {
118
- if (!optionIds.has(optionId)) {
119
- throw protocolError("invalid_poll_vote", "unknown poll option");
120
- }
263
+ has(type) {
264
+ return this.types.has(type);
121
265
  }
122
- }
123
- export function validateSessionOfferPayload(payload) {
124
- validateSessionId(payload.session_id);
125
- if (payload.session_type !== "webrtc") {
126
- throw protocolError("invalid_session", "session_type must be webrtc");
266
+ get size() {
267
+ return this.types.size;
127
268
  }
128
- if (!Array.isArray(payload.media) || payload.media.length === 0) {
129
- throw protocolError("invalid_session", "media must not be empty");
269
+ definitions() {
270
+ return [...this.types.values()];
130
271
  }
131
- for (const media of payload.media) {
132
- if (!sessionMediaKinds.has(media)) {
133
- throw protocolError("invalid_session", `unsupported media kind ${media}`);
272
+ /** Validates a custom event payload against the type's schema and status. */
273
+ validatePayload(type, payload) {
274
+ const def = this.types.get(type);
275
+ if (!def) {
276
+ throw protocolError("type_not_defined", type);
277
+ }
278
+ if ((def.status ?? "active") === "disabled") {
279
+ throw protocolError("type_disabled", type);
280
+ }
281
+ const validator = compileSchema(def.schema);
282
+ const result = validator.validate(payload);
283
+ if (!result.valid) {
284
+ const detail = result.errors
285
+ .slice(0, 3)
286
+ .map((error) => error.error)
287
+ .join("; ");
288
+ throw protocolError("payload_schema_violation", `${type}: ${detail}`);
134
289
  }
135
290
  }
136
- validateSessionDescription(payload.description, "offer");
137
- validateSessionTransfers(payload.transfers ?? []);
138
291
  }
139
- export function validateSessionAnswerPayload(payload) {
140
- validateSessionId(payload.session_id);
141
- if (payload.offer_event_id.trim() === "") {
142
- throw protocolError("invalid_session", "offer_event_id is required");
292
+ /**
293
+ * Validates an event payload: built-in payloads are accepted as-is (use the
294
+ * typed validators for them); custom payloads must satisfy the registry.
295
+ */
296
+ export function validateEventAgainstRegistry(type, payload, registry) {
297
+ if (isBuiltinEventType(type))
298
+ return;
299
+ registry.validatePayload(type, payload);
300
+ }
301
+ function compileSchema(schema) {
302
+ try {
303
+ return new Validator(schema, "2020-12", false);
304
+ }
305
+ catch (error) {
306
+ throw protocolError("invalid_event", `invalid type schema: ${error}`);
143
307
  }
144
- validateSessionDescription(payload.description, "answer");
145
- validateSessionTransfers(payload.transfers ?? []);
146
308
  }
147
- export function validateSessionCandidatePayload(payload) {
148
- validateSessionId(payload.session_id);
149
- if (payload.end_of_candidates)
150
- return;
151
- if (!payload.candidate ||
152
- typeof payload.candidate.candidate !== "string" ||
153
- payload.candidate.candidate.trim() === "") {
154
- throw protocolError("invalid_session", "candidate is required unless end_of_candidates is true");
309
+ /**
310
+ * Verifies a `<algorithm>:<base64url-digest>` content digest over raw bytes.
311
+ * Supports `sha256` and `sha3-256`.
312
+ */
313
+ export function verifyPackDigest(bytes, digest) {
314
+ const separator = digest.indexOf(":");
315
+ if (separator < 0) {
316
+ throw protocolError("pack_unavailable", `invalid digest format: ${digest}`);
317
+ }
318
+ const algorithm = digest.slice(0, separator);
319
+ const expected = digest.slice(separator + 1);
320
+ if (algorithm !== "sha256" && algorithm !== "sha3-256") {
321
+ throw protocolError("pack_unavailable", `unsupported digest algorithm: ${algorithm}`);
322
+ }
323
+ const actual = createHash(algorithm)
324
+ .update(bytes)
325
+ .digest("base64url");
326
+ if (actual !== expected) {
327
+ throw protocolError("pack_unavailable", "pack digest mismatch");
155
328
  }
156
329
  }
157
330
  export function serverRecordHashPayload(roomId, seq, preHash, envelopeHash, receivedAt) {
@@ -207,149 +380,76 @@ export function verifyServerRecordChain(records) {
207
380
  export function archiveEventsDigest(records) {
208
381
  return hashCanonicalJson(records);
209
382
  }
210
- export function canSubmitEvent(type, context) {
211
- if (type === eventType.ROOM_CREATE)
212
- return true;
213
- if (type === eventType.ROOM_JOIN)
214
- return Boolean(context.joinRequestApproved);
215
- if (context.isCreator)
216
- return isKnownEventType(type);
217
- switch (context.role) {
218
- case "moderator":
219
- return moderatorCanSubmit(type, context.moderatorAuthorized ?? false);
220
- case "expert":
221
- return speakerCanSubmit(type, context.expertPolicyAllowed ?? false);
222
- case "participant":
223
- return speakerCanSubmit(type, context.participantPolicyAllowed ?? false);
224
- case "observer":
225
- return observerCanSubmit(type, context);
226
- default:
227
- return false;
383
+ /** Default sender roles for each kind. The creator passes every role check. */
384
+ export function defaultKindRoles(kind) {
385
+ switch (kind) {
386
+ case "message":
387
+ return ["moderator", "speaker"];
388
+ case "signal":
389
+ return ["moderator", "speaker", "observer"];
390
+ case "control":
391
+ return ["moderator"];
392
+ }
393
+ }
394
+ /**
395
+ * Role check for one event type, using kind defaults and per-type overrides
396
+ * from the room's type registry. State checks are separate.
397
+ */
398
+ export function canSubmitEvent(type, context, registry = new TypeRegistry()) {
399
+ switch (type) {
400
+ case eventType.ROOM_CREATE:
401
+ return true;
402
+ case eventType.ROOM_JOIN:
403
+ return Boolean(context.joinRequestApproved);
404
+ case eventType.ROOM_LEAVE:
405
+ return Boolean(context.isCreator) || context.role !== undefined;
406
+ case eventType.ROOM_JOIN_REVIEW:
407
+ case eventType.ROOM_MEMBER_ROLE_UPDATE:
408
+ case eventType.ROOM_CLOSE:
409
+ case eventType.ROOM_CANCEL:
410
+ case eventType.TYPE_DEFINE:
411
+ return Boolean(context.isCreator) || context.role === "moderator";
412
+ case eventType.MESSAGE_CREATE:
413
+ return (Boolean(context.isCreator) ||
414
+ context.role === "moderator" ||
415
+ context.role === "speaker");
416
+ default: {
417
+ const def = registry.get(type);
418
+ if (!def || (def.status ?? "active") === "disabled")
419
+ return false;
420
+ if (context.isCreator)
421
+ return true;
422
+ if (context.role === undefined)
423
+ return false;
424
+ const roles = def.roles ?? defaultKindRoles(def.kind);
425
+ return roles.includes(context.role);
426
+ }
228
427
  }
229
428
  }
230
- export function canWriteInState(type, state, options = {}) {
429
+ export function canWriteInState(type, state) {
231
430
  switch (state) {
232
431
  case "scheduled":
233
- return eventTypeIn(type, [
234
- eventType.ROOM_JOIN,
235
- eventType.ROOM_JOIN_REVIEW,
236
- eventType.ROOM_CANCEL,
237
- ]);
432
+ return (type === eventType.ROOM_JOIN ||
433
+ type === eventType.ROOM_JOIN_REVIEW ||
434
+ type === eventType.ROOM_MEMBER_ROLE_UPDATE ||
435
+ type === eventType.ROOM_LEAVE ||
436
+ type === eventType.TYPE_DEFINE ||
437
+ type === eventType.ROOM_CANCEL);
238
438
  case "active":
239
439
  return type !== eventType.ROOM_CREATE && type !== eventType.ROOM_CANCEL;
240
440
  case "ended":
241
- return (Boolean(options.postEndReactionAllowed) &&
242
- type === eventType.REACTION_CREATE);
243
441
  case "cancelled":
244
442
  return false;
245
443
  }
246
444
  }
247
- export function canAcceptRoomWrite(type, state, permission, options = {}) {
248
- return (canSubmitEvent(type, permission) && canWriteInState(type, state, options));
445
+ export function canAcceptRoomWrite(type, state, permission, registry = new TypeRegistry()) {
446
+ return canSubmitEvent(type, permission, registry) && canWriteInState(type, state);
249
447
  }
250
- export function validateRoomWrite(type, state, permission, options = {}) {
251
- if (!canAcceptRoomWrite(type, state, permission, options)) {
448
+ export function validateRoomWrite(type, state, permission, registry = new TypeRegistry()) {
449
+ if (!canAcceptRoomWrite(type, state, permission, registry)) {
252
450
  throw protocolError("permission_denied", "actor lacks permission or state is not writable");
253
451
  }
254
452
  }
255
- function moderatorCanSubmit(type, moderatorAuthorized) {
256
- return (eventTypeIn(type, [
257
- eventType.ROOM_JOIN_REVIEW,
258
- eventType.ROOM_CLOSE,
259
- eventType.MESSAGE_CREATE,
260
- eventType.SOURCE_ADD,
261
- eventType.TURN_UPDATE,
262
- eventType.QUESTION_CREATE,
263
- eventType.ROOM_STEER,
264
- eventType.MAP_UPDATE,
265
- eventType.ARTIFACT_CREATE,
266
- eventType.SESSION_OFFER,
267
- eventType.SESSION_ANSWER,
268
- eventType.SESSION_CANDIDATE,
269
- eventType.SESSION_CLOSE,
270
- eventType.MESSAGE_PROPOSAL_CREATE,
271
- eventType.MESSAGE_POLL_CREATE,
272
- eventType.MESSAGE_POLL_VOTE,
273
- eventType.MESSAGE_RESOLUTION_CREATE,
274
- eventType.REACTION_CREATE,
275
- eventType.ROOM_LEAVE,
276
- ]) ||
277
- (moderatorAuthorized &&
278
- eventTypeIn(type, [
279
- eventType.ROOM_MEMBER_ROLE_UPDATE,
280
- eventType.ROOM_CANCEL,
281
- ])));
282
- }
283
- function speakerCanSubmit(type, policyAllowed) {
284
- return (eventTypeIn(type, [
285
- eventType.MESSAGE_CREATE,
286
- eventType.SOURCE_ADD,
287
- eventType.ROOM_STEER,
288
- eventType.MESSAGE_PROPOSAL_CREATE,
289
- eventType.MESSAGE_POLL_CREATE,
290
- eventType.MESSAGE_POLL_VOTE,
291
- eventType.SESSION_OFFER,
292
- eventType.SESSION_ANSWER,
293
- eventType.SESSION_CANDIDATE,
294
- eventType.SESSION_CLOSE,
295
- eventType.REACTION_CREATE,
296
- eventType.ROOM_LEAVE,
297
- ]) ||
298
- (policyAllowed &&
299
- eventTypeIn(type, [
300
- eventType.QUESTION_CREATE,
301
- eventType.MAP_UPDATE,
302
- eventType.ARTIFACT_CREATE,
303
- eventType.MESSAGE_RESOLUTION_CREATE,
304
- ])));
305
- }
306
- function observerCanSubmit(type, context) {
307
- return (eventTypeIn(type, [eventType.REACTION_CREATE, eventType.ROOM_LEAVE]) ||
308
- (Boolean(context.observerSteeringAllowed) &&
309
- type === eventType.ROOM_STEER) ||
310
- (Boolean(context.observerPollVoteAllowed) &&
311
- type === eventType.MESSAGE_POLL_VOTE));
312
- }
313
- function isKnownEventType(type) {
314
- return eventTypeIn(type, Object.values(eventType));
315
- }
316
- function eventTypeIn(type, values) {
317
- return values.includes(type);
318
- }
319
- function validateSessionId(sessionId) {
320
- if (sessionId.trim() === "") {
321
- throw protocolError("invalid_session", "session_id is required");
322
- }
323
- }
324
- function validateSessionDescription(description, expectedType) {
325
- if (!description ||
326
- typeof description.type !== "string" ||
327
- typeof description.sdp !== "string") {
328
- throw protocolError("invalid_session", "session description is required");
329
- }
330
- if (description.type !== expectedType) {
331
- throw protocolError("invalid_session", `session description type must be ${expectedType}`);
332
- }
333
- if (description.sdp.trim() === "") {
334
- throw protocolError("invalid_session", "session description sdp is required");
335
- }
336
- }
337
- function validateSessionTransfers(transfers) {
338
- if (!Array.isArray(transfers)) {
339
- throw protocolError("invalid_session", "transfers must be an array");
340
- }
341
- for (const transfer of transfers) {
342
- if (!transfer ||
343
- typeof transfer.transfer_id !== "string" ||
344
- transfer.transfer_id.trim() === "") {
345
- throw protocolError("invalid_session", "transfer_id is required");
346
- }
347
- if (transfer.size_bytes !== undefined &&
348
- (!Number.isInteger(transfer.size_bytes) || transfer.size_bytes < 0)) {
349
- throw protocolError("invalid_session", "size_bytes must be a non-negative integer");
350
- }
351
- }
352
- }
353
453
  function hashCanonicalJson(value) {
354
454
  const canonical = canonicalize(value);
355
455
  if (canonical === undefined) {