@brightbeamai/chap-coordinator 0.3.3 → 0.3.5

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.
Files changed (47) hide show
  1. package/README.md +54 -8
  2. package/dist/{api-59aU01Bs.d.cts → api-CjZQnj63.d.cts} +128 -14
  3. package/dist/{api-CxciGFHA.d.ts → api-DG0VFGVr.d.ts} +128 -14
  4. package/dist/api.cjs +3 -1
  5. package/dist/api.cjs.map +1 -1
  6. package/dist/api.d.cts +1 -1
  7. package/dist/api.d.ts +1 -1
  8. package/dist/api.js +3 -1
  9. package/dist/api.js.map +1 -1
  10. package/dist/index.cjs +1578 -276
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +20 -6
  13. package/dist/index.d.ts +20 -6
  14. package/dist/index.js +1577 -276
  15. package/dist/index.js.map +1 -1
  16. package/dist/jsonrpc.cjs +1 -0
  17. package/dist/jsonrpc.cjs.map +1 -1
  18. package/dist/jsonrpc.d.cts +1 -0
  19. package/dist/jsonrpc.d.ts +1 -0
  20. package/dist/jsonrpc.js +1 -0
  21. package/dist/jsonrpc.js.map +1 -1
  22. package/dist/methods.cjs.map +1 -1
  23. package/dist/methods.d.cts +36 -3
  24. package/dist/methods.d.ts +36 -3
  25. package/dist/patch.cjs +195 -61
  26. package/dist/patch.cjs.map +1 -1
  27. package/dist/patch.d.cts +16 -2
  28. package/dist/patch.d.ts +16 -2
  29. package/dist/patch.js +194 -61
  30. package/dist/patch.js.map +1 -1
  31. package/dist/storage/sqlite.cjs +81 -19
  32. package/dist/storage/sqlite.cjs.map +1 -1
  33. package/dist/storage/sqlite.d.cts +19 -9
  34. package/dist/storage/sqlite.d.ts +19 -9
  35. package/dist/storage/sqlite.js +81 -19
  36. package/dist/storage/sqlite.js.map +1 -1
  37. package/dist/storage/store.cjs +20 -15
  38. package/dist/storage/store.cjs.map +1 -1
  39. package/dist/storage/store.d.cts +24 -6
  40. package/dist/storage/store.d.ts +24 -6
  41. package/dist/storage/store.js +20 -15
  42. package/dist/storage/store.js.map +1 -1
  43. package/dist/types.cjs.map +1 -1
  44. package/dist/types.d.cts +56 -1
  45. package/dist/types.d.ts +56 -1
  46. package/dist/types.js.map +1 -1
  47. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createHash } from "crypto";
3
3
  var ZERO_HASH = "sha256:" + "0".repeat(64);
4
4
  var NON_INTEGER_ERROR = 'CHAP canonical numbers must be integers; represent decimals as strings (e.g. "8.2") so the hash is deterministic across implementations.';
5
- var NUMBER_RANGE_ERROR = "CHAP canonical integers must be within the safe-integer range (abs value <= Number.MAX_SAFE_INTEGER); represent larger numbers as strings.";
5
+ var NUMBER_RANGE_ERROR = "CHAP canonical integers must be within the safe-integer range (abs value <= 2^53 - 1); represent larger numbers as strings.";
6
6
  var LONE_SURROGATE_ERROR = "CHAP canonical strings must be valid Unicode (I-JSON / RFC 7493); unpaired UTF-16 surrogates are not permitted.";
7
7
  function assertNoLoneSurrogate(s) {
8
8
  for (let i = 0; i < s.length; i++) {
@@ -23,7 +23,7 @@ function canonicalString(s) {
23
23
  function canonicalNumber(n) {
24
24
  if (!Number.isFinite(n)) {
25
25
  throw new Error(
26
- "Non-finite numbers are not permitted in a CHAP canonical value"
26
+ "Non-finite numbers are not permitted in a CHAP canonical value."
27
27
  );
28
28
  }
29
29
  if (!Number.isInteger(n)) {
@@ -96,6 +96,7 @@ var OWNING_PROFILE = Object.freeze({
96
96
  "participant.leave": "core/1.0",
97
97
  "participant.revoke_key": "core/1.0",
98
98
  "participant.rotate_key": "core/1.0",
99
+ "review.acknowledge": "review/1.0",
99
100
  "review.depth": "routing/1.0",
100
101
  "review.request": "review/1.0",
101
102
  "task.complete": "core/1.0",
@@ -106,6 +107,7 @@ var OWNING_PROFILE = Object.freeze({
106
107
  "whisper.ask": "whisper/1.0",
107
108
  "workspace.create": "core/1.0",
108
109
  "workspace.describe": "core/1.0",
110
+ "workspace.invite": "core/1.0",
109
111
  "workspace.set_profiles": "core/1.0"
110
112
  });
111
113
  var ALWAYS_AVAILABLE = /* @__PURE__ */ new Set([
@@ -114,6 +116,20 @@ var ALWAYS_AVAILABLE = /* @__PURE__ */ new Set([
114
116
  "audit.verify_chain",
115
117
  "audit.verify_receipt"
116
118
  ]);
119
+ var PROFILE_VERSIONS = Object.freeze({
120
+ "audit-scitt": "1.0",
121
+ "control": "1.0",
122
+ "core": "1.0",
123
+ "deliberation": "1.0",
124
+ "handoff": "1.0",
125
+ "identity-oidc": "1.0",
126
+ "identity-vc": "1.0",
127
+ "modes": "1.0",
128
+ "review": "1.0",
129
+ "routing": "1.0",
130
+ "security-signed": "1.0",
131
+ "whisper": "1.0"
132
+ });
117
133
 
118
134
  // src/crypto.ts
119
135
  import {
@@ -171,21 +187,132 @@ function signEnvelope(canonical, privateKey, kid) {
171
187
  const sig = nodeSign(null, canonical, privateKey);
172
188
  return `ed25519:${kid}:${sig.toString("base64")}`;
173
189
  }
190
+ function strictBase64(s) {
191
+ const bytes = Buffer.from(s, "base64");
192
+ return bytes.toString("base64") === s ? bytes : void 0;
193
+ }
174
194
  function verifyEnvelope(canonical, sigTag, publicKey) {
175
195
  if (!sigTag.startsWith("ed25519:")) return false;
176
196
  const parts = sigTag.split(":");
177
197
  if (parts.length < 2) return false;
178
- const sigB64 = parts[parts.length - 1];
198
+ const sig = strictBase64(parts[parts.length - 1]);
199
+ if (sig === void 0) return false;
179
200
  try {
180
- const sig = Buffer.from(sigB64, "base64");
181
201
  return nodeVerify(null, canonical, publicKey, sig);
182
202
  } catch {
183
203
  return false;
184
204
  }
185
205
  }
206
+ function isEd25519Key(jwk) {
207
+ if (jwk === null || typeof jwk !== "object" || Array.isArray(jwk)) return false;
208
+ const key = jwk;
209
+ return key.kty === "OKP" && key.crv === "Ed25519" && typeof key.x === "string" && COORDINATE.test(key.x);
210
+ }
186
211
  function publicKeyFromJwk(jwk) {
187
212
  return publicKeyFromRaw(b64urlDecode(jwk.x));
188
213
  }
214
+ function jwkThumbprint(jwk) {
215
+ if (jwk === null || typeof jwk !== "object" || Array.isArray(jwk)) return void 0;
216
+ const key = jwk;
217
+ const names = key.kty === "OKP" ? ["crv", "kty", "x"] : key.kty === "EC" ? ["crv", "kty", "x", "y"] : void 0;
218
+ if (!names || names.some((n) => typeof key[n] !== "string")) return void 0;
219
+ const members = Object.fromEntries(names.map((n) => [n, key[n]]));
220
+ return b64urlNoPad(createHash2("sha256").update(canonicalize(members)).digest());
221
+ }
222
+ var B64URL = /^[A-Za-z0-9_-]+$/;
223
+ function parseWebAuthnAssertion(part) {
224
+ const pieces = part.split(".");
225
+ if (pieces.length !== 3 || pieces.some((p) => !B64URL.test(p) || p.length % 4 === 1)) return void 0;
226
+ const [authenticatorData, clientDataJSON, signature] = pieces.map((p) => Buffer.from(p, "base64url"));
227
+ return { authenticatorData, clientDataJSON, signature };
228
+ }
229
+ var COORDINATE = /^[A-Za-z0-9_-]{43}$/;
230
+ function isWebAuthnKey(jwk) {
231
+ if (jwk === null || typeof jwk !== "object" || Array.isArray(jwk)) return false;
232
+ const key = jwk;
233
+ const w = key.webauthn;
234
+ return key.kty === "EC" && key.crv === "P-256" && typeof key.x === "string" && COORDINATE.test(key.x) && typeof key.y === "string" && COORDINATE.test(key.y) && (key.alg === void 0 || key.alg === "ES256") && w !== null && typeof w === "object" && !Array.isArray(w) && typeof w.rp_id === "string" && w.rp_id !== "" && (w.origins === void 0 || Array.isArray(w.origins) && w.origins.every((o) => typeof o === "string"));
235
+ }
236
+ var UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
237
+ var MAX_CLIENT_DATA_NESTING = 64;
238
+ function jsonDepth(text) {
239
+ let depth = 0;
240
+ let deepest = 0;
241
+ let inString = false;
242
+ for (let i = 0; i < text.length; i++) {
243
+ const ch = text[i];
244
+ if (inString) {
245
+ if (ch === "\\") i++;
246
+ else if (ch === '"') inString = false;
247
+ } else if (ch === '"') {
248
+ inString = true;
249
+ } else if (ch === "{" || ch === "[") {
250
+ deepest = Math.max(deepest, ++depth);
251
+ } else if (ch === "}" || ch === "]") {
252
+ depth--;
253
+ }
254
+ }
255
+ return deepest;
256
+ }
257
+ var UP = 1;
258
+ var UV = 4;
259
+ var BE = 8;
260
+ var BS = 16;
261
+ var AT = 64;
262
+ var ED = 128;
263
+ function webAuthnFailure(canonical, assertion, jwk) {
264
+ const w = jwk.webauthn;
265
+ if (!Array.isArray(w.origins) || w.origins.length === 0) return "WebAuthn key lists no origin";
266
+ let text;
267
+ try {
268
+ text = UTF8.decode(assertion.clientDataJSON);
269
+ } catch {
270
+ text = void 0;
271
+ }
272
+ if (text !== void 0 && jsonDepth(text) > MAX_CLIENT_DATA_NESTING) {
273
+ return `WebAuthn clientDataJSON nests deeper than ${MAX_CLIENT_DATA_NESTING} levels`;
274
+ }
275
+ let client;
276
+ try {
277
+ client = text === void 0 ? void 0 : JSON.parse(text);
278
+ } catch {
279
+ client = void 0;
280
+ }
281
+ if (client === null || typeof client !== "object" || Array.isArray(client)) {
282
+ return "WebAuthn clientDataJSON is not a JSON object";
283
+ }
284
+ const data = client;
285
+ if (data.type !== "webauthn.get") return "WebAuthn clientDataJSON type is not webauthn.get";
286
+ if (data.challenge !== b64urlNoPad(createHash2("sha256").update(canonical).digest())) {
287
+ return "WebAuthn challenge is not the digest of this request";
288
+ }
289
+ if (!w.origins.includes(data.origin)) return "WebAuthn origin is not one the key allows";
290
+ if (data.crossOrigin === true || data.topOrigin !== void 0) {
291
+ return "WebAuthn assertion was made in a cross-origin frame";
292
+ }
293
+ const auth = assertion.authenticatorData;
294
+ if (auth.length < 37) return "WebAuthn authenticatorData is too short";
295
+ if (!auth.subarray(0, 32).equals(createHash2("sha256").update(w.rp_id, "utf-8").digest())) {
296
+ return "WebAuthn rpIdHash does not match the key's rp_id";
297
+ }
298
+ const flags = auth[32];
299
+ if ((flags & (UP | UV)) !== (UP | UV)) return "WebAuthn assertion lacks user presence or verification";
300
+ if (flags & BS && !(flags & BE)) return "WebAuthn assertion is backed up but not backup eligible";
301
+ if (flags & AT) return "WebAuthn assertion carries attested credential data";
302
+ if (flags & ED ? auth.length === 37 : auth.length !== 37) {
303
+ return "WebAuthn authenticatorData does not end where its flags say";
304
+ }
305
+ try {
306
+ const key = createPublicKey({
307
+ key: { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y },
308
+ format: "jwk"
309
+ });
310
+ const signed = Buffer.concat([auth, createHash2("sha256").update(assertion.clientDataJSON).digest()]);
311
+ return nodeVerify("sha256", signed, { key, dsaEncoding: "der" }, assertion.signature) ? null : "Signature failed verification";
312
+ } catch {
313
+ return "Signature failed verification";
314
+ }
315
+ }
189
316
  function deriveKeypair(uri) {
190
317
  const seed = deriveSeed(uri);
191
318
  const privateKey = privateKeyFromSeed(seed);
@@ -302,6 +429,7 @@ var E = {
302
429
  SIG_KEY_REVOKED: -32072,
303
430
  SIG_ROTATION_KEY_MISMATCH: -32073,
304
431
  SIG_ARTEFACT_DIGEST_MISMATCH: -32074,
432
+ SIG_ALREADY_ACCEPTED: -32075,
305
433
  // audit-scitt/1.0 profile (profiles/audit-scitt.md S8)
306
434
  SCITT_UNREACHABLE: -32080,
307
435
  SCITT_STATEMENT_REJECTED: -32081,
@@ -334,6 +462,44 @@ function isValidEnvelope(env) {
334
462
  return e.jsonrpc === "2.0";
335
463
  }
336
464
 
465
+ // src/params.ts
466
+ var NAMES = {
467
+ string: "a string",
468
+ strings: "a list of strings",
469
+ boolean: "a boolean",
470
+ object: "an object",
471
+ list: "a list",
472
+ integer: "an integer"
473
+ };
474
+ function isKind(value, kind) {
475
+ switch (kind) {
476
+ case "string":
477
+ return typeof value === "string";
478
+ case "strings":
479
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
480
+ case "boolean":
481
+ return typeof value === "boolean";
482
+ case "object":
483
+ return typeof value === "object" && value !== null && !Array.isArray(value);
484
+ case "list":
485
+ return Array.isArray(value);
486
+ case "integer":
487
+ return typeof value === "number" && Number.isInteger(value);
488
+ }
489
+ }
490
+ function paramsError(params, checks) {
491
+ for (const [name, kind] of checks) {
492
+ let value = params;
493
+ for (const part of name.split(".")) {
494
+ value = typeof value === "object" && value !== null && !Array.isArray(value) ? value[part] : void 0;
495
+ }
496
+ if (value !== void 0 && value !== null && !isKind(value, kind)) {
497
+ return { error: rpcError(E.PARAMS, `${name} must be ${NAMES[kind]}`) };
498
+ }
499
+ }
500
+ return null;
501
+ }
502
+
337
503
  // src/audit.ts
338
504
  function entryRecord(entry) {
339
505
  if (entry.outcome != null) return { outcome: entry.outcome, request: entry.request };
@@ -377,7 +543,8 @@ var UNRECORDED_CODES = /* @__PURE__ */ new Set([
377
543
  E.SIG_VERIFY_FAILED,
378
544
  E.SIG_KEY_NOT_FOUND,
379
545
  E.SIG_KEY_REVOKED,
380
- E.SIG_ROTATION_KEY_MISMATCH
546
+ E.SIG_ROTATION_KEY_MISMATCH,
547
+ E.SIG_ALREADY_ACCEPTED
381
548
  ]);
382
549
  var UNRECORDED_METHODS = /* @__PURE__ */ new Set(["workspace.create", "participant.join"]);
383
550
  function refusalIsRecorded(method, error, privileged) {
@@ -500,77 +667,116 @@ function deepEqual(a, b) {
500
667
  }
501
668
  return true;
502
669
  }
503
- function applyOne(doc, op) {
670
+ function addAt(doc, path, value, size, copied = false, movedFrom) {
671
+ const parts = splitPath(path);
672
+ if (parts.length === 0) {
673
+ if (size) {
674
+ size.reset(value);
675
+ if (copied || movedFrom !== void 0) size.carry(size);
676
+ }
677
+ return value;
678
+ }
679
+ const [parent, key] = navigate(doc, parts);
680
+ let newKey;
681
+ if (Array.isArray(parent)) {
682
+ if (key === "-") {
683
+ parent.push(value);
684
+ } else {
685
+ const idx = key;
686
+ if (idx < 0 || idx > parent.length) {
687
+ throw new PatchError(`Index out of range for add: ${path}`);
688
+ }
689
+ parent.splice(idx, 0, value);
690
+ }
691
+ } else if (parent !== null && typeof parent === "object") {
692
+ if (Object.prototype.hasOwnProperty.call(parent, key)) size?.take(parent[key]);
693
+ else newKey = key;
694
+ parent[key] = value;
695
+ } else {
696
+ throw new PatchError(`Cannot add into ${jsonType(parent)}`);
697
+ }
698
+ if (size) {
699
+ if (movedFrom === void 0) {
700
+ const inserted = size.insert(value, parts.length, newKey);
701
+ if (copied) size.carry(inserted);
702
+ } else {
703
+ if (newKey !== void 0) size.bytes += utf8Length(newKey);
704
+ if (parts.length > movedFrom) {
705
+ const moved = sizeOf(value);
706
+ size.depth = Math.max(size.depth, parts.length + moved.depth);
707
+ size.carry(moved);
708
+ }
709
+ }
710
+ }
711
+ return doc;
712
+ }
713
+ function removeAt(doc, path, size, moved = false) {
714
+ const parts = splitPath(path);
715
+ if (parts.length === 0) throw new PatchError("Cannot remove root.");
716
+ const [parent, key] = navigate(doc, parts);
717
+ if (Array.isArray(parent)) {
718
+ const idx = key === "-" ? -1 : key;
719
+ if (idx < 0 || idx >= parent.length) {
720
+ throw new PatchError(`Index out of range for remove: ${path}`);
721
+ }
722
+ if (size && !moved) size.take(parent[idx]);
723
+ parent.splice(idx, 1);
724
+ } else if (parent !== null && typeof parent === "object") {
725
+ if (!Object.prototype.hasOwnProperty.call(parent, key)) {
726
+ throw new PatchError(`Path not found for remove: ${path}`);
727
+ }
728
+ if (size) {
729
+ if (moved) size.bytes -= utf8Length(key);
730
+ else size.take(parent[key], key);
731
+ }
732
+ delete parent[key];
733
+ } else {
734
+ throw new PatchError(`Cannot remove from ${jsonType(parent)}`);
735
+ }
736
+ return doc;
737
+ }
738
+ function applyOne(doc, op, size) {
504
739
  const kind = op.op;
505
740
  const path = op.path ?? "";
506
741
  if (kind === "add") {
507
742
  if (!("value" in op)) throw new PatchError("'add' requires 'value'");
508
- const parts = splitPath(path);
509
- if (parts.length === 0) return op.value;
510
- const [parent, key] = navigate(doc, parts);
511
- if (Array.isArray(parent)) {
512
- if (key === "-") {
513
- parent.push(op.value);
514
- } else {
515
- const idx = key;
516
- if (idx < 0 || idx > parent.length) {
517
- throw new PatchError(`Index out of range for add: ${path}`);
518
- }
519
- parent.splice(idx, 0, op.value);
520
- }
521
- } else if (parent !== null && typeof parent === "object") {
522
- parent[key] = op.value;
523
- } else {
524
- throw new PatchError(`Cannot add into ${jsonType(parent)}`);
525
- }
526
- return doc;
743
+ return addAt(doc, path, clone(op.value), size);
527
744
  }
528
745
  if (kind === "replace") {
529
746
  if (!("value" in op)) throw new PatchError("'replace' requires 'value'");
530
747
  const parts = splitPath(path);
531
- if (parts.length === 0) return op.value;
748
+ const value = clone(op.value);
749
+ if (parts.length === 0) {
750
+ size?.reset(value);
751
+ return value;
752
+ }
532
753
  const [parent, key] = navigate(doc, parts);
533
754
  if (Array.isArray(parent)) {
534
755
  const idx = key === "-" ? -1 : key;
535
756
  if (idx < 0 || idx >= parent.length) {
536
757
  throw new PatchError(`Path not found for replace: ${path}`);
537
758
  }
538
- parent[idx] = op.value;
759
+ size?.take(parent[idx]);
760
+ parent[idx] = value;
539
761
  } else if (parent !== null && typeof parent === "object") {
540
762
  if (!Object.prototype.hasOwnProperty.call(parent, key)) {
541
763
  throw new PatchError(`Path not found for replace: ${path}`);
542
764
  }
543
- parent[key] = op.value;
765
+ size?.take(parent[key]);
766
+ parent[key] = value;
544
767
  } else {
545
768
  throw new PatchError(`Cannot replace in ${jsonType(parent)}`);
546
769
  }
770
+ size?.insert(value, parts.length);
547
771
  return doc;
548
772
  }
549
773
  if (kind === "remove") {
550
- const parts = splitPath(path);
551
- if (parts.length === 0) throw new PatchError("Cannot remove root.");
552
- const [parent, key] = navigate(doc, parts);
553
- if (Array.isArray(parent)) {
554
- const idx = key === "-" ? -1 : key;
555
- if (idx < 0 || idx >= parent.length) {
556
- throw new PatchError(`Index out of range for remove: ${path}`);
557
- }
558
- parent.splice(idx, 1);
559
- } else if (parent !== null && typeof parent === "object") {
560
- if (!Object.prototype.hasOwnProperty.call(parent, key)) {
561
- throw new PatchError(`Path not found for remove: ${path}`);
562
- }
563
- delete parent[key];
564
- } else {
565
- throw new PatchError(`Cannot remove from ${jsonType(parent)}`);
566
- }
567
- return doc;
774
+ return removeAt(doc, path, size);
568
775
  }
569
776
  if (kind === "copy") {
570
777
  const src = op.from;
571
778
  if (src === void 0) throw new PatchError("'copy' requires 'from'");
572
- const value = clone(getAt(doc, src));
573
- return applyOne(doc, { op: "add", path, value });
779
+ return addAt(doc, path, clone(getAt(doc, src)), size, true);
574
780
  }
575
781
  if (kind === "move") {
576
782
  const src = op.from;
@@ -578,9 +784,9 @@ function applyOne(doc, op) {
578
784
  if (path.startsWith(src + "/")) {
579
785
  throw new PatchError("'move' cannot move a location into its own child.");
580
786
  }
581
- const value = clone(getAt(doc, src));
582
- doc = applyOne(doc, { op: "remove", path: src });
583
- return applyOne(doc, { op: "add", path, value });
787
+ const value = getAt(doc, src);
788
+ doc = removeAt(doc, src, size, true);
789
+ return addAt(doc, path, value, size, false, splitPath(src).length);
584
790
  }
585
791
  if (kind === "test") {
586
792
  if (!("value" in op)) throw new PatchError("'test' requires 'value'");
@@ -592,29 +798,122 @@ function applyOne(doc, op) {
592
798
  }
593
799
  var MAX_PATCH_OPS = 1e3;
594
800
  var MAX_DOCUMENT_NODES = 1e5;
595
- function nodeCount(value) {
596
- if (Array.isArray(value)) {
597
- let n = 1;
598
- for (const v of value) n += nodeCount(v);
599
- return n;
600
- }
601
- if (value !== null && typeof value === "object") {
602
- let n = 1;
603
- for (const v of Object.values(value)) n += nodeCount(v);
604
- return n;
801
+ var MAX_DOCUMENT_DEPTH = 64;
802
+ var MAX_CARRIED_TIMES = 3;
803
+ var DEFAULT_MAX_PATCHED_BYTES = 1048576;
804
+ function utf8Length(s) {
805
+ const buffer = globalThis.Buffer;
806
+ if (typeof buffer === "function") {
807
+ return buffer.byteLength(s, "utf8");
808
+ }
809
+ let n = 0;
810
+ for (let i = 0; i < s.length; i++) {
811
+ const c = s.charCodeAt(i);
812
+ if (c < 128) n += 1;
813
+ else if (c < 2048) n += 2;
814
+ else if (c >= 55296 && c <= 56319 && i + 1 < s.length) {
815
+ const d = s.charCodeAt(i + 1);
816
+ if (d >= 56320 && d <= 57343) {
817
+ n += 4;
818
+ i++;
819
+ } else n += 3;
820
+ } else n += 3;
821
+ }
822
+ return n;
823
+ }
824
+ function sizeOf(value) {
825
+ if (value === null || typeof value !== "object") {
826
+ return { nodes: 1, bytes: typeof value === "string" ? utf8Length(value) : 0, depth: 1 };
827
+ }
828
+ let nodes = 0;
829
+ let bytes = 0;
830
+ let depth = 0;
831
+ const values = [value];
832
+ const levels = [1];
833
+ const visit = (x, level) => {
834
+ if (x !== null && typeof x === "object") {
835
+ values.push(x);
836
+ levels.push(level);
837
+ return;
838
+ }
839
+ nodes += 1;
840
+ if (level > depth) depth = level;
841
+ if (typeof x === "string") bytes += utf8Length(x);
842
+ };
843
+ while (values.length > 0) {
844
+ const v = values.pop();
845
+ const level = levels.pop();
846
+ nodes += 1;
847
+ if (level > depth) depth = level;
848
+ if (Array.isArray(v)) {
849
+ for (const x of v) visit(x, level + 1);
850
+ } else {
851
+ const o = v;
852
+ for (const k of Object.keys(o)) {
853
+ bytes += utf8Length(k);
854
+ visit(o[k], level + 1);
855
+ }
856
+ }
605
857
  }
606
- return 1;
858
+ return { nodes, bytes, depth };
607
859
  }
608
- function applyJsonPatch(doc, ops) {
860
+ var Size = class {
861
+ nodes = 0;
862
+ bytes = 0;
863
+ depth = 0;
864
+ /** What copies, and moves to a deeper place or onto the whole document, have carried. */
865
+ carriedNodes = 0;
866
+ carriedBytes = 0;
867
+ constructor(doc) {
868
+ this.reset(doc);
869
+ }
870
+ reset(doc) {
871
+ const s = sizeOf(doc);
872
+ this.nodes = s.nodes;
873
+ this.bytes = s.bytes;
874
+ this.depth = s.depth;
875
+ }
876
+ /** `value` goes in `segments` levels below the top, under a new `key`. */
877
+ insert(value, segments, key) {
878
+ const s = sizeOf(value);
879
+ this.nodes += s.nodes;
880
+ this.bytes += s.bytes + (key === void 0 ? 0 : utf8Length(key));
881
+ this.depth = Math.max(this.depth, segments + s.depth);
882
+ return s;
883
+ }
884
+ carry(s) {
885
+ this.carriedNodes += s.nodes;
886
+ this.carriedBytes += s.bytes;
887
+ }
888
+ /** `value` comes out, with its `key`. */
889
+ take(value, key) {
890
+ const s = sizeOf(value);
891
+ this.nodes -= s.nodes;
892
+ this.bytes -= s.bytes + (key === void 0 ? 0 : utf8Length(key));
893
+ }
894
+ };
895
+ function applyJsonPatch(doc, ops, limits = {}) {
896
+ const maxBytes = limits.maxStringBytes ?? DEFAULT_MAX_PATCHED_BYTES;
609
897
  if (ops.length > MAX_PATCH_OPS) {
610
898
  throw new PatchError(`Patch has too many operations (${ops.length} > ${MAX_PATCH_OPS})`);
611
899
  }
612
900
  let out = JSON.parse(JSON.stringify(doc));
901
+ const size = new Size(out);
613
902
  for (const op of ops) {
614
- out = applyOne(out, op);
615
- if (nodeCount(out) > MAX_DOCUMENT_NODES) {
903
+ out = applyOne(out, op, size);
904
+ if (size.depth > MAX_DOCUMENT_DEPTH) size.depth = sizeOf(out).depth;
905
+ if (size.nodes > MAX_DOCUMENT_NODES) {
616
906
  throw new PatchError(`Patched document exceeds the node limit (${MAX_DOCUMENT_NODES})`);
617
907
  }
908
+ if (size.bytes > maxBytes) {
909
+ throw new PatchError(`Patched document exceeds the size limit (${maxBytes} bytes of strings and keys)`);
910
+ }
911
+ if (size.depth > MAX_DOCUMENT_DEPTH) {
912
+ throw new PatchError(`Patched document nests deeper than ${MAX_DOCUMENT_DEPTH} levels`);
913
+ }
914
+ if (size.carriedNodes > MAX_CARRIED_TIMES * MAX_DOCUMENT_NODES || size.carriedBytes > MAX_CARRIED_TIMES * maxBytes) {
915
+ throw new PatchError(`Patch copies or moves more than its limit (${MAX_CARRIED_TIMES * MAX_DOCUMENT_NODES} values or ${MAX_CARRIED_TIMES * maxBytes} bytes of strings and keys)`);
916
+ }
618
917
  }
619
918
  return out;
620
919
  }
@@ -662,7 +961,8 @@ var COLLECTIONS = [
662
961
  "deliberations",
663
962
  "handoffs",
664
963
  "snapshots",
665
- "route_decisions"
964
+ "route_decisions",
965
+ "invitations"
666
966
  ];
667
967
  var IDEMPOTENCY_KEYS = "idempotency_keys";
668
968
  var StoreConflictError = class extends Error {
@@ -673,7 +973,7 @@ var StoreConflictError = class extends Error {
673
973
  base;
674
974
  constructor(workspace, held, base) {
675
975
  super(
676
- `The store holds ${held} audit entries for workspace ${workspace} and this write expected ${base}, so nothing was written. Another coordinator may be writing to this workspace, the store may have been changed outside this coordinator, or this coordinator could not load the workspace when it started. Restart this coordinator to reload.`
976
+ `The store holds ${held} audit entries for workspace ${workspace} and this write expected ${base}, so nothing was written. Another coordinator may be writing to this workspace, or the store may have been changed outside this coordinator. A coordinator reloads the workspace and answers the call as failed, and the call may be sent again.`
677
977
  );
678
978
  this.name = "StoreConflictError";
679
979
  this.workspace = workspace;
@@ -750,20 +1050,24 @@ function assembleSnapshot(meta, entries, objects) {
750
1050
  var MemoryStore = class {
751
1051
  held = /* @__PURE__ */ new Map();
752
1052
  load() {
753
- const out = [];
754
- for (const [id, h] of this.held) {
755
- const objects = [];
756
- for (const [collection, items] of h.objects) {
757
- for (const [key, value] of items) objects.push([collection, key, JSON.parse(value)]);
758
- }
759
- out.push({
760
- id,
761
- data: assembleSnapshot(JSON.parse(h.meta), h.entries.map((e) => JSON.parse(e)), objects),
762
- version: h.version,
763
- updated_at: h.updated_at
764
- });
1053
+ return [...this.held.keys()].map((id) => this.loadOne(id));
1054
+ }
1055
+ loadOne(id) {
1056
+ const h = this.held.get(id);
1057
+ if (!h) return void 0;
1058
+ const objects = [];
1059
+ for (const [collection, items] of h.objects) {
1060
+ for (const [key, value] of items) objects.push([collection, key, JSON.parse(value)]);
765
1061
  }
766
- return out;
1062
+ return {
1063
+ id,
1064
+ data: assembleSnapshot(JSON.parse(h.meta), h.entries.map((e) => JSON.parse(e)), objects),
1065
+ version: h.version,
1066
+ updated_at: h.updated_at
1067
+ };
1068
+ }
1069
+ version(id) {
1070
+ return this.held.get(id)?.entries.length;
767
1071
  }
768
1072
  save(record) {
769
1073
  this.apply(changeFromRecord(record));
@@ -890,7 +1194,8 @@ function makeApi(coord) {
890
1194
  workspace: {
891
1195
  create: (p) => c("workspace.create", p),
892
1196
  describe: (p) => c("workspace.describe", p),
893
- set_profiles: (p) => c("workspace.set_profiles", p)
1197
+ set_profiles: (p) => c("workspace.set_profiles", p),
1198
+ invite: (p) => c("workspace.invite", p)
894
1199
  },
895
1200
  participant: {
896
1201
  join: (p) => c("participant.join", p),
@@ -906,6 +1211,7 @@ function makeApi(coord) {
906
1211
  },
907
1212
  review: {
908
1213
  request: (p) => c("review.request", p),
1214
+ acknowledge: (p) => c("review.acknowledge", p),
909
1215
  depth: (p) => c("review.depth", p)
910
1216
  },
911
1217
  decide: {
@@ -979,10 +1285,17 @@ function registerAuditScitt(coord) {
979
1285
  coord.handlers.set("audit.submit_to_scitt", (p) => {
980
1286
  const ws = coord.workspaces.get(p.workspace);
981
1287
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1288
+ const bad = paramsError(p, [
1289
+ ["range", "object"],
1290
+ ["range.from_seq", "integer"],
1291
+ ["range.to_seq", "integer"],
1292
+ ["issuer", "string"]
1293
+ ]);
1294
+ if (bad) return bad;
982
1295
  const range = p.range ?? {};
983
1296
  const fromSeq = range.from_seq ?? 0;
984
1297
  const toSeq = range.to_seq ?? ws.audit.length;
985
- const issuer = p.issuer ?? "service:coordinator";
1298
+ const issuer = p.issuer || "service:coordinator";
986
1299
  if (!coord.options.scittSubmitter) {
987
1300
  const statements = ws.audit.slice(fromSeq, toSeq).map((e) => buildStatement(ws.id, entryRecord(e), issuer));
988
1301
  return { result: {
@@ -1012,7 +1325,7 @@ function registerAuditScitt(coord) {
1012
1325
  });
1013
1326
  coord.handlers.set("audit.verify_receipt", (p) => {
1014
1327
  const receipt = p.receipt;
1015
- if (typeof receipt !== "object" || receipt === null) {
1328
+ if (typeof receipt !== "object" || receipt === null || Array.isArray(receipt)) {
1016
1329
  return { error: rpcError(E.PARAMS, "receipt must be an object") };
1017
1330
  }
1018
1331
  if (coord.options.verifyScittReceipt) {
@@ -1026,7 +1339,7 @@ function registerAuditScitt(coord) {
1026
1339
  if (!ok) return { error: rpcError(E.SCITT_RECEIPT_INVALID, "Receipt did not verify") };
1027
1340
  return { result: { verified: true } };
1028
1341
  }
1029
- return { error: rpcError(E.SCITT_RECEIPT_INVALID, "No verifyScittReceipt hook configured; receipt not verified") };
1342
+ return { error: rpcError(E.SCITT_RECEIPT_INVALID, "No receipt verifier is configured; receipt not verified") };
1030
1343
  });
1031
1344
  coord.handlers.set("audit.verify_chain", (p) => {
1032
1345
  const ws = coord.workspaces.get(p.workspace);
@@ -1034,7 +1347,7 @@ function registerAuditScitt(coord) {
1034
1347
  if (!(ws.chain_enabled || coord.options.enableChain)) {
1035
1348
  return { error: rpcError(E.PARAMS, "Chain not enabled for this workspace") };
1036
1349
  }
1037
- if (p.from_seq !== void 0 || p.to_seq !== void 0) {
1350
+ if (p.from_seq != null || p.to_seq != null) {
1038
1351
  return { error: rpcError(
1039
1352
  E.PARAMS,
1040
1353
  "Range verification is not implemented; omit from_seq and to_seq to verify the whole log."
@@ -1096,13 +1409,21 @@ function registerAuditScitt(coord) {
1096
1409
  // src/profiles/control.ts
1097
1410
  var VALID_MODES = /* @__PURE__ */ new Set(["shadow", "trial", "production"]);
1098
1411
  var VALID_SCOPES = /* @__PURE__ */ new Set(["task", "participant", "workspace"]);
1412
+ var SCOPE_ERROR = "scope must be task, participant or workspace";
1099
1413
  function registerControl(coord) {
1100
1414
  coord.handlers.set("control.pause", (p) => {
1101
1415
  const ws = coord.workspaces.get(p.workspace);
1102
1416
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1417
+ const bad = paramsError(p, [
1418
+ ["scope", "string"],
1419
+ ["participant_uri", "string"],
1420
+ ["uri", "string"],
1421
+ ["in_flight_policy", "string"]
1422
+ ]);
1423
+ if (bad) return bad;
1103
1424
  const scope = p.scope || "task";
1104
1425
  if (!VALID_SCOPES.has(scope)) {
1105
- return { error: rpcError(E.PARAMS, `scope must be task/participant/workspace`) };
1426
+ return { error: rpcError(E.PARAMS, SCOPE_ERROR) };
1106
1427
  }
1107
1428
  if (scope === "task") {
1108
1429
  const task = ws.tasks.get(p.task_id);
@@ -1120,7 +1441,8 @@ function registerControl(coord) {
1120
1441
  }
1121
1442
  if (scope === "participant") {
1122
1443
  const uri = p.participant_uri || p.uri;
1123
- if (!uri || !ws.members.has(uri)) return { error: rpcError(E.PARAMS, `Unknown participant: ${uri}`) };
1444
+ if (!uri) return { error: rpcError(E.PARAMS, "participant_uri is required") };
1445
+ if (!ws.members.has(uri)) return { error: rpcError(E.PARAMS, `Unknown participant: ${uri}`) };
1124
1446
  ws.members.get(uri).paused = true;
1125
1447
  return { result: {
1126
1448
  scope: "participant",
@@ -1135,9 +1457,11 @@ function registerControl(coord) {
1135
1457
  coord.handlers.set("control.resume", (p) => {
1136
1458
  const ws = coord.workspaces.get(p.workspace);
1137
1459
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1460
+ const bad = paramsError(p, [["scope", "string"], ["participant_uri", "string"], ["uri", "string"]]);
1461
+ if (bad) return bad;
1138
1462
  const scope = p.scope || "task";
1139
1463
  if (!VALID_SCOPES.has(scope)) {
1140
- return { error: rpcError(E.PARAMS, `scope must be task/participant/workspace`) };
1464
+ return { error: rpcError(E.PARAMS, SCOPE_ERROR) };
1141
1465
  }
1142
1466
  if (scope === "task") {
1143
1467
  const task = ws.tasks.get(p.task_id);
@@ -1155,7 +1479,9 @@ function registerControl(coord) {
1155
1479
  }
1156
1480
  if (scope === "participant") {
1157
1481
  const uri = p.participant_uri || p.uri;
1158
- if (!uri || !ws.members.has(uri)) return { error: rpcError(E.PARAMS, `Unknown participant: ${uri}`) };
1482
+ if (!uri) return { error: rpcError(E.PARAMS, "participant_uri is required") };
1483
+ if (!ws.members.has(uri)) return { error: rpcError(E.PARAMS, `Unknown participant: ${uri}`) };
1484
+ if (uri === p.from) return { error: rpcError(E.NOT_AUTHORISED, "A paused participant does not lift its own pause") };
1159
1485
  ws.members.get(uri).paused = false;
1160
1486
  return { result: { scope: "participant", participant_uri: uri, paused: false } };
1161
1487
  }
@@ -1170,6 +1496,12 @@ function registerControl(coord) {
1170
1496
  if (task.state === "completed" || task.state === "declined" || task.state === "cancelled" || task.state === "superseded") {
1171
1497
  return { error: rpcError(E.CONTROL_NOT_AUTHORISED, `Cannot cancel ${task.state} task`) };
1172
1498
  }
1499
+ if (openRequiredReviewAuthors(ws, task).includes(p.from)) {
1500
+ return { error: rpcError(
1501
+ E.NOT_AUTHORISED,
1502
+ "The authors of the work under review cannot end its review without a decision"
1503
+ ) };
1504
+ }
1173
1505
  task.state = "cancelled";
1174
1506
  task.updated_at = coord.now();
1175
1507
  task.history.push({ ts: task.updated_at, from: p.from, state: "cancelled", note: p.reason });
@@ -1178,6 +1510,8 @@ function registerControl(coord) {
1178
1510
  coord.handlers.set("control.snapshot", (p) => {
1179
1511
  const ws = coord.workspaces.get(p.workspace);
1180
1512
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1513
+ const bad = paramsError(p, [["include", "strings"], ["label", "string"]]);
1514
+ if (bad) return bad;
1181
1515
  if (Array.isArray(p.include) && p.include.length === 0) {
1182
1516
  return { error: rpcError(E.PARAMS, "include must not be empty") };
1183
1517
  }
@@ -1232,6 +1566,14 @@ function registerControl(coord) {
1232
1566
  coord.handlers.set("control.rollback", (p) => {
1233
1567
  const ws = coord.workspaces.get(p.workspace);
1234
1568
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1569
+ const bad = paramsError(p, [
1570
+ ["to_snapshot_artefact_id", "string"],
1571
+ ["snapshot_artefact_id", "string"],
1572
+ ["snapshot_id", "string"],
1573
+ ["what_to_restore", "strings"],
1574
+ ["reason", "string"]
1575
+ ]);
1576
+ if (bad) return bad;
1235
1577
  const snapId = p.to_snapshot_artefact_id || p.snapshot_artefact_id || p.snapshot_id;
1236
1578
  if (!snapId) return { error: rpcError(E.PARAMS, "to_snapshot_artefact_id is required") };
1237
1579
  const snap = ws.snapshots.get(snapId);
@@ -1253,7 +1595,8 @@ function registerControl(coord) {
1253
1595
  for (const [uri, snapM] of snapByUri) {
1254
1596
  if (ws.members.has(uri)) {
1255
1597
  const cur = ws.members.get(uri);
1256
- cur.role = snapM.role ?? cur.role;
1598
+ const role = snapM.role ?? cur.role;
1599
+ if (role !== "admin" && cur.role !== "admin") cur.role = role;
1257
1600
  cur.scopes = Array.isArray(snapM.scopes) ? [...snapM.scopes] : void 0;
1258
1601
  }
1259
1602
  }
@@ -1272,10 +1615,18 @@ function registerControl(coord) {
1272
1615
  const oldId = p.task_id;
1273
1616
  const old = ws.tasks.get(oldId);
1274
1617
  if (!old) return { error: rpcError(E.PARAMS, "Unknown task") };
1618
+ if (openRequiredReviewAuthors(ws, old).includes(p.from)) {
1619
+ return { error: rpcError(
1620
+ E.NOT_AUTHORISED,
1621
+ "The authors of the work under review cannot end its review without a decision"
1622
+ ) };
1623
+ }
1275
1624
  const newSpec = p.successor_task ?? {};
1276
- if (!newSpec.kind) {
1625
+ if (typeof newSpec.kind !== "string" || !newSpec.kind) {
1277
1626
  return { error: rpcError(E.PARAMS, "successor_task must include kind") };
1278
1627
  }
1628
+ const bad = paramsError(p, [["successor_task.assignee", "string"], ["reason", "string"]]);
1629
+ if (bad) return bad;
1279
1630
  const assignee = newSpec.assignee || old.assignee;
1280
1631
  if (typeof assignee !== "string" || !ws.members.has(assignee)) {
1281
1632
  return { error: rpcError(E.PARAMS, "successor assignee not in workspace") };
@@ -1322,6 +1673,7 @@ function registerControl(coord) {
1322
1673
  } else if ("review_required" in newSpec) {
1323
1674
  newTask.review_required = !!newSpec.review_required;
1324
1675
  }
1676
+ newTask.prior_authors = successorAuthors(old, p.from);
1325
1677
  ws.tasks.set(newId, newTask);
1326
1678
  old.state = "superseded";
1327
1679
  old.superseded_by = newId;
@@ -1332,26 +1684,37 @@ function registerControl(coord) {
1332
1684
  state: "superseded",
1333
1685
  note: `-> ${newId}: ${p.reason || ""}`
1334
1686
  });
1335
- return { result: { superseded_task_id: old.id, new_task_id: newId, reason: p.reason } };
1687
+ return { result: {
1688
+ superseded_task_id: old.id,
1689
+ new_task_id: newId,
1690
+ ...p.reason != null ? { reason: p.reason } : {}
1691
+ } };
1336
1692
  });
1337
1693
  coord.handlers.set("control.set_mode_ceiling", (p) => {
1338
1694
  const ws = coord.workspaces.get(p.workspace);
1339
1695
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1696
+ const bad = paramsError(p, [["reason", "string"]]);
1697
+ if (bad) return bad;
1340
1698
  const m = p.new_ceiling ?? p.mode_ceiling;
1341
1699
  if (!VALID_MODES.has(m)) {
1342
- return { error: rpcError(E.PARAMS, `new_ceiling must be shadow/trial/production`) };
1700
+ return { error: rpcError(E.PARAMS, "new_ceiling must be shadow, trial or production") };
1343
1701
  }
1344
1702
  ws.mode_ceiling = m;
1345
- return { result: { mode_ceiling: ws.mode_ceiling, reason: p.reason } };
1703
+ return { result: { mode_ceiling: ws.mode_ceiling, ...p.reason != null ? { reason: p.reason } : {} } };
1346
1704
  });
1347
1705
  }
1348
1706
 
1349
1707
  // src/profiles/deliberation.ts
1708
+ var RESERVED_IDS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1709
+ var QUORUM_ARG = /^[0-9]+$/;
1710
+ var THRESHOLD_ARG = /^[0-9]+(\.[0-9]+)?$/;
1350
1711
  function parseRule(rule) {
1351
- if (rule.includes(":")) {
1352
- const [kind, arg] = rule.split(":", 2);
1353
- if (kind === "quorum") return { kind, params: { n: parseInt(arg, 10) } };
1354
- if (kind === "weighted_vote" || kind === "weighted_vote_with_veto") {
1712
+ const colon = rule.indexOf(":");
1713
+ if (colon >= 0) {
1714
+ const kind = rule.slice(0, colon);
1715
+ const arg = rule.slice(colon + 1);
1716
+ if (kind === "quorum" && QUORUM_ARG.test(arg)) return { kind, params: { n: parseInt(arg, 10) } };
1717
+ if ((kind === "weighted_vote" || kind === "weighted_vote_with_veto") && THRESHOLD_ARG.test(arg)) {
1355
1718
  return { kind, params: { threshold: parseFloat(arg) } };
1356
1719
  }
1357
1720
  }
@@ -1436,6 +1799,23 @@ function registerDeliberation(coord) {
1436
1799
  for (const f of ["from", "rule"]) {
1437
1800
  if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
1438
1801
  }
1802
+ for (const name of ["to", "participants"]) {
1803
+ const v = p[name];
1804
+ if (v != null && typeof v !== "string" && !(Array.isArray(v) && v.every((x) => typeof x === "string"))) {
1805
+ return { error: rpcError(E.PARAMS, `${name} must be a string or a list of strings`) };
1806
+ }
1807
+ }
1808
+ if (typeof p.rule !== "string") return { error: rpcError(E.PARAMS, "rule must be a string") };
1809
+ const bad = paramsError(p, [["weights", "object"], ["veto", "object"]]);
1810
+ if (bad) return bad;
1811
+ const weights = p.weights ?? {};
1812
+ if (!Object.values(weights).every((w) => typeof w === "number" && Number.isInteger(w))) {
1813
+ return { error: rpcError(E.PARAMS, "weights must map participants to integers") };
1814
+ }
1815
+ const veto = p.veto ?? {};
1816
+ if (!Object.values(veto).every((v) => typeof v === "boolean")) {
1817
+ return { error: rpcError(E.PARAMS, "veto must map participants to booleans") };
1818
+ }
1439
1819
  const to = p.to ?? p.participants;
1440
1820
  const participants = Array.isArray(to) ? to : typeof to === "string" ? [to] : [];
1441
1821
  if (!participants.length) {
@@ -1447,6 +1827,18 @@ function registerDeliberation(coord) {
1447
1827
  const msg = e instanceof Error ? e.message : String(e);
1448
1828
  return { error: rpcError(E.DELIB_UNKNOWN_RULE, msg) };
1449
1829
  }
1830
+ const suppliedDeliberationId = p.deliberation_id;
1831
+ if (suppliedDeliberationId !== void 0 && suppliedDeliberationId !== null && suppliedDeliberationId !== "") {
1832
+ if (typeof suppliedDeliberationId !== "string") {
1833
+ return { error: rpcError(E.PARAMS, "deliberation_id must be a string") };
1834
+ }
1835
+ if (RESERVED_IDS.has(suppliedDeliberationId)) {
1836
+ return { error: rpcError(E.PARAMS, `deliberation_id cannot be ${JSON.stringify(suppliedDeliberationId)}`) };
1837
+ }
1838
+ if (ws.deliberations.has(suppliedDeliberationId)) {
1839
+ return { error: rpcError(E.PARAMS, `deliberation_id already in use: ${suppliedDeliberationId}`) };
1840
+ }
1841
+ }
1450
1842
  const id = p.deliberation_id || coord.ids.deliberationId();
1451
1843
  const delib = {
1452
1844
  id,
@@ -1474,6 +1866,8 @@ function registerDeliberation(coord) {
1474
1866
  if (delib.state !== "open") {
1475
1867
  return { error: rpcError(E.DELIB_CLOSED_OR_LAPSED, "Deliberation not open") };
1476
1868
  }
1869
+ const bad = paramsError(p, [["comment", "string"], ["text", "string"]]);
1870
+ if (bad) return bad;
1477
1871
  delib.comments.push({
1478
1872
  ts: coord.now(),
1479
1873
  from: p.from,
@@ -1496,6 +1890,8 @@ function registerDeliberation(coord) {
1496
1890
  if (v !== "yea" && v !== "nay" && v !== "abstain") {
1497
1891
  return { error: rpcError(E.PARAMS, "vote must be yea/nay/abstain") };
1498
1892
  }
1893
+ const bad = paramsError(p, [["veto_invoked", "boolean"]]);
1894
+ if (bad) return bad;
1499
1895
  const voter = p.from;
1500
1896
  if (!delib.participants.includes(voter)) {
1501
1897
  return { error: rpcError(
@@ -1529,6 +1925,7 @@ function registerDeliberation(coord) {
1529
1925
  }
1530
1926
 
1531
1927
  // src/profiles/handoff.ts
1928
+ var RESERVED_IDS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1532
1929
  function registerHandoff(coord) {
1533
1930
  coord.handlers.set("handoff.propose", (p) => {
1534
1931
  const ws = coord.workspaces.get(p.workspace);
@@ -1536,6 +1933,9 @@ function registerHandoff(coord) {
1536
1933
  for (const f of ["from", "to", "tasks"]) {
1537
1934
  if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
1538
1935
  }
1936
+ if (typeof p.to !== "string") return { error: rpcError(E.PARAMS, "to must be a string") };
1937
+ const bad = paramsError(p, [["context_links", "strings"]]);
1938
+ if (bad) return bad;
1539
1939
  const proposer = p.from;
1540
1940
  const recipient = p.to;
1541
1941
  if (typeof recipient === "string" && !recipient.startsWith("group:")) {
@@ -1552,6 +1952,11 @@ function registerHandoff(coord) {
1552
1952
  }
1553
1953
  const proposed = [];
1554
1954
  for (const t of tasksIn) {
1955
+ if (t === null || typeof t !== "object" || Array.isArray(t) || typeof t.task_id !== "string") {
1956
+ return { error: rpcError(E.PARAMS, "each entry in tasks must be an object with a string task_id") };
1957
+ }
1958
+ const badItem = paramsError(t, [["blockers", "strings"]]);
1959
+ if (badItem) return badItem;
1555
1960
  const tid = t.task_id;
1556
1961
  if (!tid || !ws.tasks.has(tid)) {
1557
1962
  return { error: rpcError(E.PARAMS, `Unknown task in handoff: ${tid}`) };
@@ -1571,6 +1976,18 @@ function registerHandoff(coord) {
1571
1976
  blockers: t.blockers ?? []
1572
1977
  });
1573
1978
  }
1979
+ const suppliedHandoffId = p.handoff_id;
1980
+ if (suppliedHandoffId !== void 0 && suppliedHandoffId !== null && suppliedHandoffId !== "") {
1981
+ if (typeof suppliedHandoffId !== "string") {
1982
+ return { error: rpcError(E.PARAMS, "handoff_id must be a string") };
1983
+ }
1984
+ if (RESERVED_IDS2.has(suppliedHandoffId)) {
1985
+ return { error: rpcError(E.PARAMS, `handoff_id cannot be ${JSON.stringify(suppliedHandoffId)}`) };
1986
+ }
1987
+ if (ws.handoffs.has(suppliedHandoffId)) {
1988
+ return { error: rpcError(E.PARAMS, `handoff_id already in use: ${suppliedHandoffId}`) };
1989
+ }
1990
+ }
1574
1991
  const id = p.handoff_id || coord.ids.handoffId();
1575
1992
  const ho = {
1576
1993
  id,
@@ -1621,6 +2038,8 @@ function registerHandoff(coord) {
1621
2038
  if (ws.members.get(acceptor).paused) {
1622
2039
  return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Acceptor ${acceptor} is paused`) };
1623
2040
  }
2041
+ const bad = paramsError(p, [["accepted_task_ids", "strings"]]);
2042
+ if (bad) return bad;
1624
2043
  if (Array.isArray(p.accepted_task_ids) && p.accepted_task_ids.length === 0) {
1625
2044
  return { error: rpcError(E.PARAMS, "accepted_task_ids must not be empty") };
1626
2045
  }
@@ -1631,9 +2050,18 @@ function registerHandoff(coord) {
1631
2050
  return { error: rpcError(E.PARAMS, `Task ${tid} not in this handoff`) };
1632
2051
  }
1633
2052
  }
2053
+ for (const tid of acceptedIds) {
2054
+ if (ws.tasks.get(tid)?.assignee !== ho.proposer) {
2055
+ return { error: rpcError(
2056
+ E.HANDOFF_TASKS_NOT_ASSIGNED_TO_PROPOSER,
2057
+ `Task ${tid} is not currently assigned to ${ho.proposer}`
2058
+ ) };
2059
+ }
2060
+ }
1634
2061
  const now = coord.now();
1635
2062
  for (const tid of acceptedIds) {
1636
2063
  const task = ws.tasks.get(tid);
2064
+ if (acceptor !== task.assignee) task.prior_authors = unionOf(task.prior_authors ?? [], heldBy(task, ho.proposer));
1637
2065
  task.assignee = acceptor;
1638
2066
  task.updated_at = now;
1639
2067
  task.history.push({
@@ -1674,16 +2102,20 @@ function registerHandoff(coord) {
1674
2102
 
1675
2103
  // src/profiles/routing.ts
1676
2104
  var CRIT = { low: 0, medium: 1, high: 2, critical: 3 };
1677
- function defaultDepth(hints) {
2105
+ var DECIMAL = /^-?[0-9]+(\.[0-9]+)?$/;
2106
+ function confidenceOf(hints) {
2107
+ const v = hints.confidence;
2108
+ if (typeof v === "number" && Number.isInteger(v)) return v;
2109
+ if (typeof v === "string" && DECIMAL.test(v)) return Number(v);
2110
+ return 1;
2111
+ }
2112
+ function criticalityOf(hints) {
1678
2113
  const crit = hints.criticality ?? "medium";
1679
- let conf = 1;
1680
- try {
1681
- conf = Number(hints.confidence ?? 1);
1682
- } catch {
1683
- conf = 1;
1684
- }
1685
- if (isNaN(conf)) conf = 1;
1686
- const r = CRIT[crit] ?? 1;
2114
+ return typeof crit === "string" && Object.prototype.hasOwnProperty.call(CRIT, crit) ? CRIT[crit] : 1;
2115
+ }
2116
+ function defaultDepth(hints) {
2117
+ const conf = confidenceOf(hints);
2118
+ const r = criticalityOf(hints);
1687
2119
  if (r >= 3) return { depth: "full", summary: "criticality=critical: full review" };
1688
2120
  if (r === 2 && conf < 0.7) return { depth: "full", summary: "criticality=high and confidence<0.7: full review" };
1689
2121
  if (r === 0 && conf >= 0.95) return { depth: "skip", summary: "very low criticality + very high confidence: skip" };
@@ -1749,6 +2181,8 @@ function registerRouting(coord) {
1749
2181
  if (ws.members.get(selected).paused) {
1750
2182
  return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Assignee ${selected} is paused`) };
1751
2183
  }
2184
+ const leaving = heldBy(task, p.from);
2185
+ if (selected !== task.assignee && leaving.length) task.prior_authors = unionOf(task.prior_authors ?? [], leaving);
1752
2186
  task.assignee = selected;
1753
2187
  task.updated_at = coord.now();
1754
2188
  task.history.push({
@@ -1779,6 +2213,8 @@ function registerRouting(coord) {
1779
2213
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1780
2214
  const task = ws.tasks.get(p.task_id);
1781
2215
  if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
2216
+ const bad = paramsError(p, [["artefact_routing_hints", "object"]]);
2217
+ if (bad) return bad;
1782
2218
  const hints = {
1783
2219
  ...task.routing_hints ?? {},
1784
2220
  ...p.artefact_routing_hints ?? {}
@@ -1836,6 +2272,8 @@ function registerRouting(coord) {
1836
2272
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1837
2273
  const task = ws.tasks.get(p.task_id);
1838
2274
  if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
2275
+ const bad = paramsError(p, [["default_escalation_target", "string"]]);
2276
+ if (bad) return bad;
1839
2277
  const hints = { ...task.routing_hints ?? {} };
1840
2278
  let escalate, to, rule;
1841
2279
  if (coord.options.escalationPolicy) {
@@ -1849,15 +2287,8 @@ function registerRouting(coord) {
1849
2287
  return { error: rpcError(E.ROUTING_POLICY_UNREACHABLE, `escalation policy error: ${msg}`) };
1850
2288
  }
1851
2289
  } else {
1852
- const crit = hints.criticality ?? "medium";
1853
- let conf = 1;
1854
- try {
1855
- conf = Number(hints.confidence ?? 1);
1856
- } catch {
1857
- conf = 1;
1858
- }
1859
- if (isNaN(conf)) conf = 1;
1860
- const r = CRIT[crit] ?? 1;
2290
+ const conf = confidenceOf(hints);
2291
+ const r = criticalityOf(hints);
1861
2292
  escalate = r >= 3 || r >= 2 && conf < 0.6;
1862
2293
  to = p.default_escalation_target;
1863
2294
  rule = {
@@ -1914,6 +2345,9 @@ function registerSecuritySigned(coord) {
1914
2345
  for (const f of ["from", "old_kid", "new_jwk"]) {
1915
2346
  if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
1916
2347
  }
2348
+ for (const f of ["from", "old_kid"]) {
2349
+ if (typeof p[f] !== "string") return { error: rpcError(E.PARAMS, `${f} must be a string`) };
2350
+ }
1917
2351
  const member = ws.members.get(p.from);
1918
2352
  if (!member) return { error: rpcError(E.SIG_KEY_NOT_FOUND, `Unknown participant: ${p.from}`) };
1919
2353
  const oldKid = p.old_kid;
@@ -1924,6 +2358,9 @@ function registerSecuritySigned(coord) {
1924
2358
  if (!newJwk || typeof newJwk.kid !== "string") {
1925
2359
  return { error: rpcError(E.PARAMS, "new_jwk must include kid") };
1926
2360
  }
2361
+ if (member.keys.some((k) => k.kid === newJwk.kid)) {
2362
+ return { error: rpcError(E.PARAMS, `${p.from} already holds a key ${newJwk.kid}; a new key takes a kid of its own`) };
2363
+ }
1927
2364
  const now = coord.now();
1928
2365
  oldKey.valid_until = now;
1929
2366
  member.keys.push({
@@ -1939,6 +2376,11 @@ function registerSecuritySigned(coord) {
1939
2376
  for (const f of ["target_uri", "kid"]) {
1940
2377
  if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
1941
2378
  }
2379
+ for (const f of ["target_uri", "kid"]) {
2380
+ if (typeof p[f] !== "string") return { error: rpcError(E.PARAMS, `${f} must be a string`) };
2381
+ }
2382
+ const bad = paramsError(p, [["from", "string"], ["reason", "string"]]);
2383
+ if (bad) return bad;
1942
2384
  const sender = p.from;
1943
2385
  if (sender !== p.target_uri) {
1944
2386
  const caller = sender ? ws.members.get(sender) : void 0;
@@ -1961,6 +2403,7 @@ function registerSecuritySigned(coord) {
1961
2403
  }
1962
2404
 
1963
2405
  // src/profiles/whisper.ts
2406
+ var RESERVED_IDS3 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1964
2407
  function parseIso(ts) {
1965
2408
  return new Date(ts).getTime();
1966
2409
  }
@@ -1980,11 +2423,25 @@ function registerWhisper(coord) {
1980
2423
  if (!ws.tasks.has(p.task_id)) {
1981
2424
  return { error: rpcError(E.PARAMS, "Unknown task") };
1982
2425
  }
1983
- if (p.options !== void 0 && p.options !== null && !Array.isArray(p.options)) {
1984
- return { error: rpcError(E.PARAMS, "options must be a list") };
2426
+ const bad = paramsError(p, [["options", "list"], ["urgency", "string"]]);
2427
+ if (bad) return bad;
2428
+ if (typeof p.deadline_ms !== "number" || !Number.isInteger(p.deadline_ms)) {
2429
+ return { error: rpcError(E.PARAMS, "deadline_ms must be an integer") };
1985
2430
  }
1986
2431
  const to = p.to;
1987
2432
  const askee = Array.isArray(to) ? to : [to];
2433
+ const suppliedWhisperId = p.whisper_id;
2434
+ if (suppliedWhisperId !== void 0 && suppliedWhisperId !== null && suppliedWhisperId !== "") {
2435
+ if (typeof suppliedWhisperId !== "string") {
2436
+ return { error: rpcError(E.PARAMS, "whisper_id must be a string") };
2437
+ }
2438
+ if (RESERVED_IDS3.has(suppliedWhisperId)) {
2439
+ return { error: rpcError(E.PARAMS, `whisper_id cannot be ${JSON.stringify(suppliedWhisperId)}`) };
2440
+ }
2441
+ if (ws.whispers.has(suppliedWhisperId)) {
2442
+ return { error: rpcError(E.PARAMS, `whisper_id already in use: ${suppliedWhisperId}`) };
2443
+ }
2444
+ }
1988
2445
  const id = p.whisper_id || coord.ids.artefactId();
1989
2446
  const prompt = {
1990
2447
  id,
@@ -2032,7 +2489,7 @@ function registerWhisper(coord) {
2032
2489
  if (!hasOption) {
2033
2490
  return { error: rpcError(E.PARAMS, "answer_option is required when options are defined") };
2034
2491
  }
2035
- const valid = new Set(prompt.options.map((o) => o.id));
2492
+ const valid = new Set(prompt.options.filter((o) => o !== null && typeof o === "object" && !Array.isArray(o)).map((o) => o.id));
2036
2493
  if (!valid.has(answerOption)) {
2037
2494
  return { error: rpcError(
2038
2495
  E.WHISPER_OPTION_NOT_IN_SET,
@@ -2050,12 +2507,13 @@ function registerWhisper(coord) {
2050
2507
  prompt.comment = p.comment;
2051
2508
  return { result: { answered: true, whisper_id: prompt.id, task_id: prompt.task_id } };
2052
2509
  });
2053
- coord.checkWhisperLapses = (workspaceId, now) => {
2510
+ coord.checkWhisperLapses = (workspaceId, now) => coord.outsideCall(workspaceId, () => lapse(workspaceId, now));
2511
+ function lapse(workspaceId, now) {
2054
2512
  const ws = coord.workspaces.get(workspaceId);
2055
2513
  if (!ws) return [];
2056
2514
  const cutoff = parseIso(now ?? coord.now());
2057
2515
  const emitted = [];
2058
- for (const prompt of ws.whispers.values()) {
2516
+ for (const prompt of rawValues(ws.whispers)) {
2059
2517
  if (prompt.state !== "pending") continue;
2060
2518
  const asked = parseIso(prompt.asked_at);
2061
2519
  const deadline = asked + prompt.deadline_ms;
@@ -2085,7 +2543,7 @@ function registerWhisper(coord) {
2085
2543
  emitted.push(notify);
2086
2544
  }
2087
2545
  return emitted;
2088
- };
2546
+ }
2089
2547
  }
2090
2548
 
2091
2549
  // src/coordinator.ts
@@ -2098,6 +2556,7 @@ var PRIVILEGED_METHODS = /* @__PURE__ */ new Set([
2098
2556
  "control.rollback",
2099
2557
  "control.set_mode_ceiling",
2100
2558
  "workspace.set_profiles",
2559
+ "workspace.invite",
2101
2560
  "participant.rotate_key",
2102
2561
  "participant.revoke_key"
2103
2562
  ]);
@@ -2141,12 +2600,9 @@ function missing(params, fields) {
2141
2600
  for (const f of fields) if (!(f in params)) return f;
2142
2601
  return null;
2143
2602
  }
2144
- function tagsError(p) {
2145
- const tags = p.tags;
2146
- if (tags !== void 0 && (!Array.isArray(tags) || !tags.every((t) => typeof t === "string"))) {
2147
- return { error: rpcError(E.PARAMS, "tags must be a list of strings") };
2148
- }
2149
- return null;
2603
+ var MODES = /* @__PURE__ */ new Set(["shadow", "trial", "production"]);
2604
+ function isObject(value) {
2605
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2150
2606
  }
2151
2607
  function reviewRuleSupported(rule) {
2152
2608
  if (rule === "any_one_approves" || rule === "all_approve") return true;
@@ -2167,8 +2623,214 @@ function reviewSatisfied(review) {
2167
2623
  if (rule.startsWith("quorum:")) return approvers.size >= parseInt(rule.slice("quorum:".length), 10);
2168
2624
  return approvers.size >= 1;
2169
2625
  }
2626
+ function advertises(ws, name) {
2627
+ return ws.profiles.some((p) => p === name || p.startsWith(name + "/"));
2628
+ }
2629
+ function knownProfile(id) {
2630
+ const m = /^([a-z][a-z0-9-]*)\/(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.exec(id);
2631
+ if (!m || !Object.prototype.hasOwnProperty.call(PROFILE_VERSIONS, m[1])) return false;
2632
+ return PROFILE_VERSIONS[m[1]].split(".")[0] === m[2];
2633
+ }
2634
+ var LASTING_PROFILES = ["security-signed", "identity-oidc", "identity-vc"];
2635
+ function profilesError(profiles) {
2636
+ const unknown = profiles.find((x) => !knownProfile(x));
2637
+ if (unknown === void 0) return null;
2638
+ return { error: rpcError(
2639
+ E.PARAMS,
2640
+ `Unknown profile ${JSON.stringify(unknown)}: a profile id is <name>/<major>.<minor> for a profile the catalogue lists`
2641
+ ) };
2642
+ }
2643
+ var RESERVED_KEYS = ["__proto__", "constructor", "prototype"];
2644
+ var PARTICIPANT_TYPES = ["human", "agent", "service", "group", "workspace"];
2645
+ var ADMIN_TYPES = ["human", "service"];
2646
+ function participantError(type, uri, field) {
2647
+ if (!PARTICIPANT_TYPES.includes(type)) {
2648
+ return { error: rpcError(E.PARAMS, "type must be human, agent, service, group or workspace") };
2649
+ }
2650
+ if (!uri.startsWith(`${type}:`)) {
2651
+ return { error: rpcError(E.PARAMS, `${field} must start with "${type}:" for type ${type}`) };
2652
+ }
2653
+ if (type === "group" || type === "workspace") {
2654
+ return { error: rpcError(E.PARAMS, "group: and workspace: URIs name address scopes, which cannot join") };
2655
+ }
2656
+ return null;
2657
+ }
2658
+ function inForce(key, now) {
2659
+ return !(key.revoked_at !== void 0 && now >= key.revoked_at) && !(key.valid_until !== void 0 && now >= key.valid_until);
2660
+ }
2661
+ function sameKey(a, b) {
2662
+ const material = (k) => jwkThumbprint(k) ?? canonicalize(k).toString("utf-8");
2663
+ return a.kid === b.kid && material(a) === material(b);
2664
+ }
2665
+ function credentialKeys(cnf, nonce, joinKeys) {
2666
+ if (isObject(cnf)) {
2667
+ const kid = typeof cnf.kid === "string" && cnf.kid !== "" ? cnf.kid : jwkThumbprint(cnf);
2668
+ if (kid === void 0) return null;
2669
+ const key = { ...cnf, kid };
2670
+ return joinKeys.every((j) => sameKey(key, j)) ? [key] : null;
2671
+ }
2672
+ if (typeof nonce === "string") return joinKeys.every((j) => jwkThumbprint(j) === nonce) ? joinKeys : null;
2673
+ return void 0;
2674
+ }
2675
+ function rawInvitation(ws, uri) {
2676
+ return Map.prototype.get.call(ws.invitations, uri);
2677
+ }
2678
+ function awaitsFirstMember(ws) {
2679
+ if (ws.members.size > 0) return false;
2680
+ return !ws.audit.some((e) => e.outcome == null && entryCall(e)?.method === "participant.join");
2681
+ }
2682
+ function signingKeys(jwks) {
2683
+ const keys = isObject(jwks) && Array.isArray(jwks.keys) ? jwks.keys : [];
2684
+ return keys.filter((j) => isObject(j) && typeof j.kid === "string" && j.kid !== "");
2685
+ }
2686
+ function recordedForm(envelope) {
2687
+ if (envelope.method !== "participant.join" || !isObject(envelope.params)) return envelope;
2688
+ const params = { ...envelope.params };
2689
+ if (typeof params.oidc_token === "string") params.oidc_token = sha256Hex(params.oidc_token);
2690
+ if (isObject(params.vc_presentation)) params.vc_presentation = contentHash(params.vc_presentation);
2691
+ return { ...envelope, params };
2692
+ }
2693
+ function storeFailed() {
2694
+ return rpcError(E.INTERNAL, "Internal error", { reason: "store" });
2695
+ }
2696
+ function targetOf(envelope) {
2697
+ const params = isObject(envelope) ? envelope.params : void 0;
2698
+ const id = isObject(params) ? params.workspace : void 0;
2699
+ return typeof id === "string" && id !== "" ? id : void 0;
2700
+ }
2701
+ var KEPT_NULLS = { whispers: ["default_if_lapsed"] };
2702
+ function dropNulls(o, keep = []) {
2703
+ for (const key of Object.keys(o)) if (o[key] === null && !keep.includes(key)) delete o[key];
2704
+ }
2705
+ function shapeObject(name, item) {
2706
+ if (name === "route_decisions") {
2707
+ const extra = item.extra;
2708
+ delete item.extra;
2709
+ if (isObject(extra)) Object.assign(item, extra);
2710
+ item.kind = "route_decision";
2711
+ }
2712
+ dropNulls(item, KEPT_NULLS[name]);
2713
+ const each = (o, field, f) => {
2714
+ if (!Array.isArray(o[field])) return;
2715
+ for (const x of o[field]) {
2716
+ if (isObject(x)) {
2717
+ dropNulls(x);
2718
+ f?.(x);
2719
+ }
2720
+ }
2721
+ };
2722
+ if (name === "members") each(item, "keys");
2723
+ if (name === "tasks") {
2724
+ each(item, "history", (h) => {
2725
+ if (h.from === void 0 && typeof h.from_ === "string") h.from = h.from_;
2726
+ delete h.from_;
2727
+ });
2728
+ if (isObject(item.review)) {
2729
+ dropNulls(item.review);
2730
+ each(item.review, "decisions");
2731
+ }
2732
+ }
2733
+ if (name === "deliberations") each(item, "votes");
2734
+ if (name === "handoffs") {
2735
+ each(item, "tasks");
2736
+ if (Array.isArray(item.accepted_task_ids) && item.accepted_task_ids.length === 0) delete item.accepted_task_ids;
2737
+ }
2738
+ return item;
2739
+ }
2740
+ function storedCopy(name, item) {
2741
+ return shapeObject(name, structuredClone(item));
2742
+ }
2743
+ function shapeSnapshot(w) {
2744
+ const out = { ...w };
2745
+ dropNulls(out);
2746
+ for (const name of COLLECTIONS) {
2747
+ if (Array.isArray(out[name])) {
2748
+ out[name] = out[name].map((x) => isObject(x) ? storedCopy(name, x) : x);
2749
+ }
2750
+ }
2751
+ return out;
2752
+ }
2753
+ function reviewRequired(ws, task) {
2754
+ return task.review_required === true || task.mode === "trial" && advertises(ws, "modes");
2755
+ }
2756
+ function rawMember(ws, uri) {
2757
+ return typeof uri === "string" ? Map.prototype.get.call(ws.members, uri) : void 0;
2758
+ }
2759
+ function unionOf(...lists) {
2760
+ const out = [];
2761
+ for (const list of lists) for (const uri of list) if (!out.includes(uri)) out.push(uri);
2762
+ return out;
2763
+ }
2764
+ function authorsOf(task, completer) {
2765
+ const authors = [task.assignee];
2766
+ if (typeof completer === "string" && completer !== task.assignee) authors.push(completer);
2767
+ return unionOf(authors, Array.isArray(task.prior_authors) ? task.prior_authors : []);
2768
+ }
2769
+ function stateBefore(history, i) {
2770
+ for (let j = i - 1; j >= 0; j--) {
2771
+ if (history[j].state !== "paused") return history[j].state;
2772
+ }
2773
+ return void 0;
2774
+ }
2775
+ function workCompletion(history, i) {
2776
+ if (history[i].state !== "completed") return false;
2777
+ const before = stateBefore(history, i);
2778
+ return before !== "review_requested" && before !== "completed";
2779
+ }
2780
+ function lastCompleter(task) {
2781
+ for (let i = task.history.length - 1; i >= 0; i--) {
2782
+ if (workCompletion(task.history, i)) return task.history[i].from;
2783
+ }
2784
+ return void 0;
2785
+ }
2786
+ var OPENED_ON_COMPLETE = "review required; opened on task.complete";
2787
+ var NEW_ROUND_ON_COMPLETE = "a new round of the review; opened on task.complete";
2788
+ function storedAuthors(task) {
2789
+ let requester;
2790
+ const withRequester = (authors) => unionOf(authors, requester !== void 0 ? [requester] : []);
2791
+ for (let i = task.history.length - 1; i >= 0; i--) {
2792
+ const h = task.history[i];
2793
+ const opens = h.state === "review_requested" && stateBefore(task.history, i) !== "review_requested";
2794
+ if (opens && (h.note === OPENED_ON_COMPLETE || h.note === NEW_ROUND_ON_COMPLETE)) return withRequester(authorsOf(task, h.from));
2795
+ if (opens && requester === void 0) requester = h.from;
2796
+ if (workCompletion(task.history, i)) return withRequester(authorsOf(task, h.from));
2797
+ }
2798
+ return withRequester(authorsOf(task, void 0));
2799
+ }
2800
+ var ENDED = ["completed", "declined", "cancelled", "superseded", "escalated"];
2801
+ function reviewHolds(ws, task) {
2802
+ const seen = /* @__PURE__ */ new Set();
2803
+ for (let t = task; t && !seen.has(t.id); t = typeof t.supersedes === "string" ? ws.tasks.get(t.supersedes) : void 0) {
2804
+ seen.add(t.id);
2805
+ if (!reviewRequired(ws, t)) return [];
2806
+ if (t.review) {
2807
+ const decided = t.review.decisions.some((d) => d.kind === "reject" || d.kind === "override") || reviewSatisfied(t.review);
2808
+ return decided ? [] : t.review.authors;
2809
+ }
2810
+ }
2811
+ return [];
2812
+ }
2813
+ function openRequiredReviewAuthors(ws, task) {
2814
+ return ENDED.includes(task.state) ? [] : reviewHolds(ws, task);
2815
+ }
2816
+ function heldBy(task, mover) {
2817
+ return mover === task.assignee ? [task.assignee] : [];
2818
+ }
2819
+ function successorAuthors(old, mover) {
2820
+ const completer = lastCompleter(old);
2821
+ return unionOf(
2822
+ old.review ? old.review.authors : [],
2823
+ heldBy(old, mover),
2824
+ completer !== void 0 ? [completer] : [],
2825
+ Array.isArray(old.prior_authors) ? old.prior_authors : []
2826
+ );
2827
+ }
2828
+ function isScope(uri) {
2829
+ return typeof uri === "string" && (uri.startsWith("workspace:") || uri.startsWith("group:"));
2830
+ }
2170
2831
  function reply(env, body) {
2171
- const out = { jsonrpc: "2.0", id: env.id };
2832
+ const sent = env !== null && typeof env === "object" && !Array.isArray(env) ? env.id : void 0;
2833
+ const out = { jsonrpc: "2.0", id: sent ?? null };
2172
2834
  if (body.error !== void 0) out.error = body.error;
2173
2835
  else out.result = body.result;
2174
2836
  return out;
@@ -2183,7 +2845,7 @@ function indexSigned(index, entry) {
2183
2845
  return;
2184
2846
  }
2185
2847
  if (entry.outcome != null) index.refused.set(digest, { seq: entry.seq, code: entry.outcome.code });
2186
- else index.accepted.add(digest);
2848
+ else if (!index.accepted.has(digest)) index.accepted.set(digest, entry.seq);
2187
2849
  }
2188
2850
  function isThenable(value) {
2189
2851
  return typeof value?.then === "function";
@@ -2211,13 +2873,21 @@ var Coordinator = class {
2211
2873
  unflushed = /* @__PURE__ */ new Set();
2212
2874
  /** Workspaces with a write queued behind an async write in flight. */
2213
2875
  flushQueued = /* @__PURE__ */ new Set();
2876
+ /**
2877
+ * Workspaces whose store copy may differ from memory after a synchronous
2878
+ * write failed or conflicted. Each is reloaded from the store before the
2879
+ * next call on it, which is refused while the reload fails.
2880
+ */
2881
+ held = /* @__PURE__ */ new Set();
2882
+ /** Entries appended and not yet written, which the audit listeners hear of once they are. */
2883
+ unannounced = /* @__PURE__ */ new WeakMap();
2214
2884
  clockMs;
2215
2885
  frozenNow;
2216
2886
  auditListeners = [];
2217
2887
  /**
2218
2888
  * The signed calls on each workspace's log, by what their senders signed:
2219
- * recorded refusals with their seq and code, and accepted calls. Rebuilt
2220
- * from the log when missing, so a snapshot never carries it.
2889
+ * recorded refusals with their seq and code, and accepted calls with their
2890
+ * seq. Rebuilt from the log when missing, so a snapshot never carries it.
2221
2891
  */
2222
2892
  signedIndexes = /* @__PURE__ */ new WeakMap();
2223
2893
  /** Method handler registry; profiles plug into this. */
@@ -2353,6 +3023,7 @@ var Coordinator = class {
2353
3023
  change = this.takeChange(ws, p);
2354
3024
  } catch (err) {
2355
3025
  p.replace = true;
3026
+ if (this.options.storeErrors !== "log") this.held.add(ws.id);
2356
3027
  this.reportStoreError(err, { id: ws.id, version: ws.audit.length });
2357
3028
  return;
2358
3029
  }
@@ -2361,6 +3032,7 @@ var Coordinator = class {
2361
3032
  const done = () => {
2362
3033
  p.seq = taken.version;
2363
3034
  if (taken.replace) p.replace = false;
3035
+ this.announce(ws, taken.version);
2364
3036
  };
2365
3037
  const failed = (err) => {
2366
3038
  this.giveBack(ws, p, taken);
@@ -2370,13 +3042,34 @@ var Coordinator = class {
2370
3042
  try {
2371
3043
  result = this.store.apply(taken);
2372
3044
  } catch (err) {
3045
+ if (err instanceof StoreConflictError || this.options.storeErrors !== "log") this.held.add(ws.id);
2373
3046
  failed(err);
2374
3047
  return;
2375
3048
  }
2376
3049
  if (isThenable(result)) return Promise.resolve(result).then(done, failed);
2377
3050
  done();
2378
3051
  }
2379
- /** The change since the last write, taking the recorded keys with it. */
3052
+ /** Tell the audit listeners of the entries now written, up to `upTo`. */
3053
+ announce(ws, upTo) {
3054
+ const queued = this.unannounced.get(ws);
3055
+ if (!queued?.length) return;
3056
+ const now = queued.filter((e) => e.seq < upTo);
3057
+ this.unannounced.set(ws, queued.filter((e) => e.seq >= upTo));
3058
+ for (const entry of now) {
3059
+ for (const l of this.auditListeners) {
3060
+ try {
3061
+ l(ws, entry);
3062
+ } catch {
3063
+ }
3064
+ }
3065
+ }
3066
+ }
3067
+ /**
3068
+ * The change since the last write, taking the recorded keys with it. Each
3069
+ * object is written as a copy in the shape a restore gives back, so a
3070
+ * coordinator restarted on the store holds what this one holds, and the
3071
+ * workspace's own objects are left as they are.
3072
+ */
2380
3073
  takeChange(ws, p) {
2381
3074
  const replace = p.replace;
2382
3075
  const objects = {};
@@ -2385,12 +3078,15 @@ var Coordinator = class {
2385
3078
  if (replace) {
2386
3079
  const all = {};
2387
3080
  for (const [key, value] of Map.prototype.entries.call(map)) {
2388
- all[key] = value;
3081
+ all[key] = storedCopy(name, value);
2389
3082
  }
2390
3083
  objects[name] = all;
2391
3084
  } else if (map.dirty.size) {
2392
3085
  const changed = {};
2393
- for (const key of map.dirty) changed[key] = map.peek(key) ?? null;
3086
+ for (const key of map.dirty) {
3087
+ const value = map.peek(key);
3088
+ changed[key] = value === void 0 ? null : storedCopy(name, value);
3089
+ }
2394
3090
  objects[name] = changed;
2395
3091
  }
2396
3092
  map.dirty.clear();
@@ -2442,24 +3138,28 @@ var Coordinator = class {
2442
3138
  this.reportStoreError(err, { id: ws.id, version: next });
2443
3139
  return;
2444
3140
  }
3141
+ const upTo = ws.audit.length;
2445
3142
  const pending = this.saveChains.get(ws.id);
2446
3143
  if (pending) {
2447
- this.trackSave(pending.then(() => this.store.save(record)), record);
3144
+ this.trackSave(pending.then(() => this.store.save(record)), record, () => this.announce(ws, upTo));
2448
3145
  return;
2449
3146
  }
2450
3147
  let result;
2451
3148
  try {
2452
3149
  result = this.store.save(record);
2453
3150
  } catch (err) {
3151
+ if (this.options.storeErrors !== "log") this.held.add(ws.id);
2454
3152
  this.reportStoreError(err, record);
2455
3153
  return;
2456
3154
  }
2457
3155
  if (isThenable(result)) {
2458
- this.trackSave(Promise.resolve(result), record);
3156
+ this.trackSave(Promise.resolve(result), record, () => this.announce(ws, upTo));
3157
+ return;
2459
3158
  }
3159
+ this.announce(ws, upTo);
2460
3160
  }
2461
- trackSave(save, record) {
2462
- const guarded = save.catch((err) => this.reportStoreError(err, record));
3161
+ trackSave(save, record, saved) {
3162
+ const guarded = save.then(saved, (err) => this.reportStoreError(err, record));
2463
3163
  this.saveChains.set(record.id, guarded);
2464
3164
  void guarded.then(() => {
2465
3165
  if (this.saveChains.get(record.id) === guarded) {
@@ -2467,6 +3167,17 @@ var Coordinator = class {
2467
3167
  }
2468
3168
  });
2469
3169
  }
3170
+ reportInternalError(err, method, workspace) {
3171
+ const call2 = { method, ...workspace !== void 0 ? { workspace } : {} };
3172
+ if (this.options.onInternalError) {
3173
+ try {
3174
+ this.options.onInternalError(err, call2);
3175
+ } catch {
3176
+ }
3177
+ return;
3178
+ }
3179
+ console.error(`chap coordinator: ${method} failed with an internal error:`, err);
3180
+ }
2470
3181
  reportStoreError(err, record) {
2471
3182
  if (this.options.onStoreError) {
2472
3183
  try {
@@ -2476,7 +3187,9 @@ var Coordinator = class {
2476
3187
  return;
2477
3188
  }
2478
3189
  const message = err instanceof Error ? err.message : String(err);
2479
- console.error(`chap coordinator: writing workspace ${record.id} to the store failed: ${message}`);
3190
+ const line = `chap coordinator: writing workspace ${record.id} to the store failed: ${message}`;
3191
+ if (err instanceof StoreConflictError) console.warn(line);
3192
+ else console.error(line);
2480
3193
  }
2481
3194
  async drainSaves() {
2482
3195
  while (this.saveChains.size) {
@@ -2527,16 +3240,25 @@ var Coordinator = class {
2527
3240
  return null;
2528
3241
  }
2529
3242
  /**
2530
- * Assert that `sender` was addressed in the task's review. Applied to
2531
- * review-decision methods (decide.* and abstain.declare): to act on a
2532
- * review you must be one of the reviewers it was addressed to (the
2533
- * `to` set on review.request). Membership is the floor; this is the
2534
- * eligibility ceiling for decisions. Returns an error fragment if not
2535
- * eligible, else null.
3243
+ * Assert that `sender` may decide the task's open review. Applied to
3244
+ * review-decision methods (decide.* and abstain.declare) and to
3245
+ * review.acknowledge: to act on a review you must not be an author of the
3246
+ * work under review, must be a human member where the task requires
3247
+ * review, and must be one of the reviewers it was addressed to (the `to`
3248
+ * set on review.request).
3249
+ * Membership is the floor; this is the eligibility ceiling for decisions.
3250
+ * Returns an error fragment if not eligible, else null.
2536
3251
  */
2537
- requireReviewer(task, sender) {
3252
+ requireReviewer(ws, task, sender) {
2538
3253
  const review = task.review;
2539
- if (!review || !review.requested_to || review.requested_to.length === 0) {
3254
+ if (!review) return null;
3255
+ if (typeof sender === "string" && review.authors.includes(sender)) {
3256
+ return { error: rpcError(E.NOT_AUTHORISED, "The author of the work under review cannot decide it") };
3257
+ }
3258
+ if (reviewRequired(ws, task) && rawMember(ws, sender)?.type !== "human") {
3259
+ return { error: rpcError(E.NOT_AUTHORISED, `This task requires a person's review: ${String(sender)} is not a human member`) };
3260
+ }
3261
+ if (!review.requested_to || review.requested_to.length === 0) {
2540
3262
  return null;
2541
3263
  }
2542
3264
  if (review.requested_to.some((r) => typeof r === "string" && (r.startsWith("workspace:") || r.startsWith("group:")))) {
@@ -2565,23 +3287,31 @@ var Coordinator = class {
2565
3287
  routing_policy_uri: ws.routing_policy_uri,
2566
3288
  step_up_window_sec: ws.step_up_window_sec,
2567
3289
  min_acr: ws.min_acr,
3290
+ signature_policy: ws.signature_policy,
3291
+ join_policy: ws.join_policy,
3292
+ review_rule: ws.review_rule,
2568
3293
  chain_head: ws.chain_head,
2569
3294
  chain_enabled: ws.chain_enabled
2570
3295
  };
2571
3296
  }
2572
- /** One workspace as `snapshot()` holds it. Reads nothing as a change. */
3297
+ /**
3298
+ * One workspace as `snapshot()` holds it: each object of its collections a
3299
+ * copy in the shape the store keeps. Reads nothing as a change.
3300
+ */
2573
3301
  snapshotOf(ws) {
2574
3302
  const { chain_head, chain_enabled, ...own } = this.metaOf(ws);
3303
+ const stored = (name) => Array.from(rawValues(ws[name])).map((v) => storedCopy(name, v));
2575
3304
  return {
2576
3305
  ...own,
2577
- members: Array.from(rawValues(ws.members)),
2578
- tasks: Array.from(rawValues(ws.tasks)),
2579
- overrides: Array.from(rawValues(ws.overrides)),
2580
- whispers: Array.from(rawValues(ws.whispers)),
2581
- deliberations: Array.from(rawValues(ws.deliberations)),
2582
- handoffs: Array.from(rawValues(ws.handoffs)),
2583
- snapshots: Array.from(rawValues(ws.snapshots)),
2584
- route_decisions: Array.from(rawValues(ws.route_decisions)),
3306
+ members: stored("members"),
3307
+ tasks: stored("tasks"),
3308
+ overrides: stored("overrides"),
3309
+ whispers: stored("whispers"),
3310
+ deliberations: stored("deliberations"),
3311
+ handoffs: stored("handoffs"),
3312
+ snapshots: stored("snapshots"),
3313
+ route_decisions: stored("route_decisions"),
3314
+ invitations: stored("invitations"),
2585
3315
  idempotency_keys: { ...ws.idempotency_keys },
2586
3316
  audit: ws.audit,
2587
3317
  chain_head,
@@ -2609,7 +3339,8 @@ var Coordinator = class {
2609
3339
  }
2610
3340
  }
2611
3341
  /** One workspace from a snapshot entry, its maps tracked. */
2612
- rebuild(w) {
3342
+ rebuild(snapshot) {
3343
+ const w = shapeSnapshot(snapshot);
2613
3344
  const ws = {
2614
3345
  id: w.id,
2615
3346
  created: w.created,
@@ -2620,6 +3351,11 @@ var Coordinator = class {
2620
3351
  routing_policy_uri: w.routing_policy_uri,
2621
3352
  step_up_window_sec: w.step_up_window_sec ?? 300,
2622
3353
  min_acr: w.min_acr,
3354
+ // A workspace written before the policy existed verified every call,
3355
+ // and one written before the join policy existed admitted anyone.
3356
+ signature_policy: w.signature_policy === "humans" ? "humans" : "all",
3357
+ join_policy: w.join_policy === "invite" ? "invite" : "open",
3358
+ review_rule: typeof w.review_rule === "string" && reviewRuleSupported(w.review_rule) ? w.review_rule : void 0,
2623
3359
  members: this.newMap(),
2624
3360
  tasks: this.newMap(),
2625
3361
  overrides: this.newMap(),
@@ -2628,6 +3364,7 @@ var Coordinator = class {
2628
3364
  handoffs: this.newMap(),
2629
3365
  snapshots: this.newMap(),
2630
3366
  route_decisions: this.newMap(),
3367
+ invitations: this.newMap(),
2631
3368
  idempotency_keys: w.idempotency_keys ?? {},
2632
3369
  audit: w.audit ?? [],
2633
3370
  chain_head: w.chain_head,
@@ -2636,9 +3373,16 @@ var Coordinator = class {
2636
3373
  for (const m of w.members ?? []) {
2637
3374
  if (!Array.isArray(m.keys)) m.keys = [];
2638
3375
  if (m.paused === void 0) m.paused = false;
3376
+ const scheme = typeof m.uri === "string" ? m.uri.split(":")[0] : "";
3377
+ if (PARTICIPANT_TYPES.includes(scheme)) m.type = scheme;
3378
+ if (m.role === "admin" && !ADMIN_TYPES.includes(m.type)) m.role = "participant";
2639
3379
  ws.members.set(m.uri, m);
2640
3380
  }
2641
- for (const t of w.tasks ?? []) ws.tasks.set(t.id, t);
3381
+ for (const t of w.tasks ?? []) {
3382
+ if (t.review && !Array.isArray(t.review.authors)) t.review.authors = storedAuthors(t);
3383
+ if (t.review && !Array.isArray(t.review.acknowledgements)) t.review.acknowledgements = [];
3384
+ ws.tasks.set(t.id, t);
3385
+ }
2642
3386
  for (const o of w.overrides ?? []) ws.overrides.set(o.id, o);
2643
3387
  for (const x of w.whispers ?? [])
2644
3388
  ws.whispers.set(x.id, x);
@@ -2682,20 +3426,119 @@ var Coordinator = class {
2682
3426
  }
2683
3427
  for (const x of w.route_decisions ?? [])
2684
3428
  ws.route_decisions.set(x.id, x);
3429
+ for (const x of w.invitations ?? []) ws.invitations.set(x.uri, x);
2685
3430
  return ws;
2686
3431
  }
2687
3432
  // -- dispatch ------------------------------------------------------
2688
3433
  dispatch(envelope) {
2689
3434
  this.frozenNow = this.advanceClock();
2690
3435
  this.tracker.depth++;
3436
+ let response;
3437
+ let target;
3438
+ let ran = false;
2691
3439
  try {
2692
- const response = this._dispatch(envelope);
2693
- if (response.error) this.recordRefusal(envelope, response.error);
2694
- return response;
3440
+ target = targetOf(envelope);
3441
+ const refused = target === void 0 ? null : this.keepUp(target);
3442
+ if (refused) {
3443
+ response = reply(envelope, { error: refused });
3444
+ } else {
3445
+ ran = true;
3446
+ response = this._dispatch(envelope);
3447
+ if (response.error) this.recordRefusal(envelope, response.error);
3448
+ else if (target === void 0 && envelope.method === "workspace.create") {
3449
+ target = response.result?.workspace;
3450
+ }
3451
+ }
2695
3452
  } finally {
2696
3453
  if (--this.tracker.depth === 0) this.flushUnflushed();
2697
3454
  this.frozenNow = void 0;
2698
3455
  }
3456
+ if (ran && target !== void 0 && this.tracker.depth === 0 && this.held.has(target)) {
3457
+ this.reload(target);
3458
+ return reply(envelope, { error: storeFailed() });
3459
+ }
3460
+ return response;
3461
+ }
3462
+ /**
3463
+ * Work that changes a workspace outside any call, a whisper lapse check
3464
+ * for one, kept to the rules a call keeps (SPECIFICATION 10.3): the
3465
+ * workspace is first brought up to date with the store, and left alone
3466
+ * as long as it is held; what the work changed is written once, when it ends;
3467
+ * and where that write fails, the workspace goes back to what the store
3468
+ * holds and the work's results are dropped, since none took effect.
3469
+ */
3470
+ outsideCall(id, work) {
3471
+ if (this.tracker.depth > 0) return work();
3472
+ if (this.keepUp(id)) return [];
3473
+ this.tracker.depth++;
3474
+ let out;
3475
+ try {
3476
+ out = work();
3477
+ } finally {
3478
+ if (--this.tracker.depth === 0) this.flushUnflushed();
3479
+ }
3480
+ if (this.held.has(id)) {
3481
+ this.reload(id);
3482
+ return [];
3483
+ }
3484
+ return out;
3485
+ }
3486
+ /**
3487
+ * Before a call on a workspace: where the store holds a different number of
3488
+ * audit entries for it than this instance last wrote, another writer has
3489
+ * written it, and it is reloaded. A workspace held after a failed write is
3490
+ * reloaded as well, and the call refused while that fails.
3491
+ */
3492
+ keepUp(id) {
3493
+ if (this.held.has(id)) return this.reload(id) ? null : storeFailed();
3494
+ if (typeof this.store.version !== "function" || this.saveChains.has(id)) return null;
3495
+ let stored;
3496
+ try {
3497
+ stored = this.store.version(id);
3498
+ } catch {
3499
+ return null;
3500
+ }
3501
+ if (isThenable(stored)) return null;
3502
+ const ws = this.workspaces.get(id);
3503
+ const p = ws ? this.persisted.get(ws) : void 0;
3504
+ if (ws && (!p || p.replace)) return null;
3505
+ if (stored === p?.seq) return null;
3506
+ return this.reload(id) ? null : storeFailed();
3507
+ }
3508
+ /** Replace a workspace with what the store holds of it. False when it cannot be read. */
3509
+ reload(id) {
3510
+ let record;
3511
+ try {
3512
+ if (typeof this.store.loadOne === "function") {
3513
+ record = this.store.loadOne(id);
3514
+ } else {
3515
+ const all = this.store.load();
3516
+ if (isThenable(all)) return false;
3517
+ record = all.find((r) => r.id === id);
3518
+ }
3519
+ if (isThenable(record)) return false;
3520
+ } catch (err) {
3521
+ this.reportStoreError(err, { id, version: this.workspaces.get(id)?.audit.length ?? 0 });
3522
+ return false;
3523
+ }
3524
+ const old = this.workspaces.get(id);
3525
+ if (old) this.unflushed.delete(old);
3526
+ if (record) {
3527
+ let ws;
3528
+ try {
3529
+ ws = this.rebuild(record.data);
3530
+ } catch (err) {
3531
+ this.reportStoreError(err, { id, version: record.version });
3532
+ return false;
3533
+ }
3534
+ this.workspaces.set(id, ws);
3535
+ this.markWritten(ws);
3536
+ this.wsVersions.set(id, record.version);
3537
+ } else {
3538
+ this.workspaces.delete(id);
3539
+ }
3540
+ this.held.delete(id);
3541
+ return true;
2699
3542
  }
2700
3543
  _dispatch(envelope) {
2701
3544
  if (!isValidEnvelope(envelope) || typeof envelope.method !== "string" || !envelope.method) {
@@ -2732,11 +3575,16 @@ var Coordinator = class {
2732
3575
  const msg = canonicalError instanceof Error ? canonicalError.message : String(canonicalError);
2733
3576
  return reply(envelope, { error: rpcError(E.PARAMS, msg) });
2734
3577
  }
3578
+ if (params.workspace !== void 0 && (typeof params.workspace !== "string" || !params.workspace)) {
3579
+ return reply(envelope, { error: rpcError(E.PARAMS, "workspace must be a non-empty string") });
3580
+ }
2735
3581
  if (isSigned(envelope)) {
2736
3582
  const wsId2 = params.workspace;
2737
3583
  const ws = typeof wsId2 === "string" ? this.workspaces.get(wsId2) : void 0;
2738
3584
  if (ws) {
2739
- const prior = this.signedIndex(ws).refused.get(signedDigest(envelope));
3585
+ const index = this.signedIndex(ws);
3586
+ const digest = signedDigest(recordedForm(envelope));
3587
+ const prior = index.refused.get(digest);
2740
3588
  if (prior) {
2741
3589
  return reply(envelope, { error: rpcError(
2742
3590
  prior.code,
@@ -2744,15 +3592,24 @@ var Coordinator = class {
2744
3592
  { refused_at_seq: prior.seq }
2745
3593
  ) });
2746
3594
  }
3595
+ const accepted = index.accepted.get(digest);
3596
+ if (accepted !== void 0) {
3597
+ return reply(envelope, { error: rpcError(
3598
+ E.SIG_ALREADY_ACCEPTED,
3599
+ `Accepted at seq ${accepted}; a signed request takes effect once`,
3600
+ { accepted_at_seq: accepted }
3601
+ ) });
3602
+ }
2747
3603
  }
2748
3604
  }
2749
- if (this.options.requireSignatures && method !== "participant.join" && method !== "workspace.create") {
3605
+ const target = typeof params.workspace === "string" ? this.workspaces.get(params.workspace) : void 0;
3606
+ if (this.requiresSignatures(target) && method !== "participant.join" && method !== "workspace.create" && !(!isSigned(envelope) && target !== void 0 && !this.mustSign(target, rawMember(target, params.from)?.type))) {
2750
3607
  const sigErr = this.verifySignature(envelope);
2751
3608
  if (sigErr) return reply(envelope, { error: sigErr });
2752
3609
  if (method === "participant.rotate_key") {
2753
3610
  const signingKid = envelope.sig.split(":")[1];
2754
3611
  const oldKid = params.old_kid;
2755
- if (oldKid !== void 0 && signingKid !== oldKid) {
3612
+ if (oldKid != null && signingKid !== oldKid) {
2756
3613
  return reply(envelope, { error: rpcError(
2757
3614
  E.SIG_ROTATION_KEY_MISMATCH,
2758
3615
  "Rotation must be signed with the old key"
@@ -2807,14 +3664,14 @@ var Coordinator = class {
2807
3664
  try {
2808
3665
  out = handler(ownCopy(params));
2809
3666
  } catch (e) {
2810
- const msg = e instanceof Error ? e.message : String(e);
2811
- return reply(envelope, { error: rpcError(E.INTERNAL, "Internal error", { detail: msg }) });
3667
+ this.reportInternalError(e, method, params.workspace);
3668
+ return reply(envelope, { error: rpcError(E.INTERNAL, "Internal error") });
2812
3669
  }
2813
3670
  if (out.error) return reply(envelope, { error: out.error });
2814
- const wsId = params.workspace;
3671
+ const wsId = method === "workspace.create" ? out.result.workspace : params.workspace;
2815
3672
  if (typeof wsId === "string" && !READ_ONLY_METHODS.has(method)) {
2816
3673
  const ws = this.workspaces.get(wsId);
2817
- if (ws) this.recordAudit(ws, envelope);
3674
+ if (ws) this.recordAudit(ws, recordedForm(envelope));
2818
3675
  }
2819
3676
  return reply(envelope, { result: ownCopy(out.result) });
2820
3677
  }
@@ -2854,7 +3711,7 @@ var Coordinator = class {
2854
3711
  signedIndex(ws) {
2855
3712
  let index = this.signedIndexes.get(ws);
2856
3713
  if (!index) {
2857
- index = { refused: /* @__PURE__ */ new Map(), accepted: /* @__PURE__ */ new Set() };
3714
+ index = { refused: /* @__PURE__ */ new Map(), accepted: /* @__PURE__ */ new Map() };
2858
3715
  for (const entry of ws.audit) indexSigned(index, entry);
2859
3716
  this.signedIndexes.set(ws, index);
2860
3717
  }
@@ -2870,12 +3727,9 @@ var Coordinator = class {
2870
3727
  ws.audit.push(entry);
2871
3728
  const index = this.signedIndexes.get(ws);
2872
3729
  if (index) indexSigned(index, entry);
2873
- for (const l of this.auditListeners) {
2874
- try {
2875
- l(ws, entry);
2876
- } catch {
2877
- }
2878
- }
3730
+ const queued = this.unannounced.get(ws);
3731
+ if (queued) queued.push(entry);
3732
+ else this.unannounced.set(ws, [entry]);
2879
3733
  this.persist(ws);
2880
3734
  }
2881
3735
  // -- signature verification (security-signed/1.0) -----------------
@@ -2885,7 +3739,12 @@ var Coordinator = class {
2885
3739
  return rpcError(E.SIG_VERIFY_FAILED, "Missing top-level `sig` field");
2886
3740
  }
2887
3741
  const parts = sig.split(":");
2888
- if (parts.length !== 3 || parts[0] !== "ed25519") {
3742
+ const scheme = parts[0];
3743
+ if (parts.length !== 3 || scheme !== "ed25519" && scheme !== "webauthn") {
3744
+ return rpcError(E.SIG_VERIFY_FAILED, "Malformed signature tag");
3745
+ }
3746
+ const assertion = scheme === "webauthn" ? parseWebAuthnAssertion(parts[2]) : void 0;
3747
+ if (scheme === "webauthn" && !assertion) {
2889
3748
  return rpcError(E.SIG_VERIFY_FAILED, "Malformed signature tag");
2890
3749
  }
2891
3750
  const kid = parts[1];
@@ -2919,6 +3778,14 @@ var Coordinator = class {
2919
3778
  if (revoked) return rpcError(E.SIG_KEY_REVOKED, `Key ${kid} is revoked`);
2920
3779
  return rpcError(E.SIG_KEY_NOT_FOUND, `No key ${kid} valid at ${ts} for ${sender}`);
2921
3780
  }
3781
+ if (assertion) {
3782
+ if (!isWebAuthnKey(key.jwk)) return rpcError(E.SIG_VERIFY_FAILED, `Key ${kid} is not a WebAuthn key`);
3783
+ const stripped = JSON.parse(JSON.stringify(envelope));
3784
+ delete stripped.sig;
3785
+ const failure = webAuthnFailure(canonicalize(stripped), assertion, key.jwk);
3786
+ return failure === null ? null : rpcError(E.SIG_VERIFY_FAILED, failure);
3787
+ }
3788
+ if (!isEd25519Key(key.jwk)) return rpcError(E.SIG_VERIFY_FAILED, `Key ${kid} is not an Ed25519 key`);
2922
3789
  try {
2923
3790
  const stripped = JSON.parse(JSON.stringify(envelope));
2924
3791
  delete stripped.sig;
@@ -2932,6 +3799,39 @@ var Coordinator = class {
2932
3799
  return rpcError(E.SIG_VERIFY_FAILED, "Signature failed verification");
2933
3800
  }
2934
3801
  }
3802
+ /**
3803
+ * SPECIFICATION 6.5: an identity profile states that a verifier checks
3804
+ * people's credentials, so advertising one this coordinator has no
3805
+ * verifier for describes a guarantee it does not give.
3806
+ */
3807
+ unverifiedIdentity(profiles) {
3808
+ const named = (name) => profiles.some((x) => x === name || x.startsWith(name + "/"));
3809
+ if (named("identity-oidc") && this.options.verifyOidcToken === void 0) {
3810
+ return rpcError(
3811
+ E.PARAMS,
3812
+ "identity-oidc/1.0 is advertised but no token verifier is configured. A workspace that advertises it and does not enforce it describes a guarantee it does not give."
3813
+ );
3814
+ }
3815
+ if (named("identity-vc") && this.options.verifyVc === void 0) {
3816
+ return rpcError(
3817
+ E.PARAMS,
3818
+ "identity-vc/1.0 is advertised but no presentation verifier is configured. A workspace that advertises it and does not enforce it describes a guarantee it does not give."
3819
+ );
3820
+ }
3821
+ return null;
3822
+ }
3823
+ /** Whether a workspace requires signatures: it advertises security-signed/1.0, or this coordinator is configured to. */
3824
+ requiresSignatures(ws) {
3825
+ return !!this.options.requireSignatures || ws !== void 0 && advertises(ws, "security-signed");
3826
+ }
3827
+ /**
3828
+ * Whether a participant of this type signs its calls in a workspace that
3829
+ * requires signatures: every participant under "all", and every one but an
3830
+ * agent under "humans". A non-member signs.
3831
+ */
3832
+ mustSign(ws, type) {
3833
+ return ws.signature_policy === "all" || type !== "agent";
3834
+ }
2935
3835
  /** Lookup the key valid for a given participant at a given time. */
2936
3836
  lookupKey(member, kid, ts) {
2937
3837
  for (const k of member.keys) {
@@ -3005,6 +3905,7 @@ var Coordinator = class {
3005
3905
  this.handlers.set("workspace.create", (p) => this.opWorkspaceCreate(p));
3006
3906
  this.handlers.set("workspace.describe", (p) => this.opWorkspaceDescribe(p));
3007
3907
  this.handlers.set("workspace.set_profiles", (p) => this.opWorkspaceSetProfiles(p));
3908
+ this.handlers.set("workspace.invite", (p) => this.opWorkspaceInvite(p));
3008
3909
  this.handlers.set("participant.join", (p) => this.opParticipantJoin(p));
3009
3910
  this.handlers.set("participant.leave", (p) => this.opParticipantLeave(p));
3010
3911
  this.handlers.set("task.create", (p) => this.opTaskCreate(p));
@@ -3012,6 +3913,7 @@ var Coordinator = class {
3012
3913
  this.handlers.set("task.complete", (p) => this.opTaskComplete(p));
3013
3914
  this.handlers.set("audit.read", (p) => this.opAuditRead(p));
3014
3915
  this.handlers.set("review.request", (p) => this.opReviewRequest(p));
3916
+ this.handlers.set("review.acknowledge", (p) => this.opReviewAcknowledge(p));
3015
3917
  this.handlers.set("decide.approve", (p) => this.opDecide(p, "approve"));
3016
3918
  this.handlers.set("decide.reject", (p) => this.opDecide(p, "reject"));
3017
3919
  this.handlers.set("decide.override", (p) => this.opDecideOverride(p));
@@ -3031,24 +3933,59 @@ var Coordinator = class {
3031
3933
  // Core method handlers
3032
3934
  // ==========================================================
3033
3935
  opWorkspaceCreate(p) {
3936
+ const built = this.buildWorkspace(p);
3937
+ if (built.error) return { error: built.error };
3938
+ this.addWorkspace(built.ws);
3939
+ return { result: { workspace: built.ws.id, created: built.ws.created } };
3940
+ }
3941
+ /**
3942
+ * The workspace a workspace.create with these params makes, or why it
3943
+ * cannot be made. Nothing is registered, so participant.join can judge a
3944
+ * join by the workspace it would make and make it only once the join
3945
+ * passes.
3946
+ */
3947
+ buildWorkspace(p) {
3948
+ const bad = paramsError(p, [
3949
+ ["profiles", "strings"],
3950
+ ["mode", "string"],
3951
+ ["mode_ceiling", "string"],
3952
+ ["routing_policy_uri", "string"],
3953
+ ["step_up_window_sec", "integer"],
3954
+ ["min_acr", "string"]
3955
+ ]);
3956
+ if (bad) return bad;
3957
+ for (const name of ["mode", "mode_ceiling"]) {
3958
+ if (p[name] != null && !MODES.has(p[name])) {
3959
+ return { error: rpcError(E.PARAMS, `${name} must be shadow, trial or production`) };
3960
+ }
3961
+ }
3962
+ const policy = p.signature_policy ?? this.options.signaturePolicy ?? "all";
3963
+ if (policy !== "all" && policy !== "humans") {
3964
+ return { error: rpcError(E.PARAMS, "signature_policy must be all or humans") };
3965
+ }
3966
+ const joinPolicy = p.join_policy ?? this.options.joinPolicy ?? "open";
3967
+ if (joinPolicy !== "open" && joinPolicy !== "invite") {
3968
+ return { error: rpcError(E.PARAMS, "join_policy must be open or invite") };
3969
+ }
3970
+ const reviewRule = p.review_rule ?? void 0;
3971
+ if (reviewRule !== void 0 && (typeof reviewRule !== "string" || !reviewRuleSupported(reviewRule))) {
3972
+ return { error: rpcError(E.PARAMS, "review_rule must be any_one_approves, all_approve or quorum:<n>") };
3973
+ }
3034
3974
  const id = p.workspace || this.ids.workspaceId();
3035
- if (typeof id !== "string") return { error: rpcError(E.PARAMS, "workspace must be a string id") };
3036
3975
  if (this.workspaces.has(id)) return { error: rpcError(E.PARAMS, `workspace already exists: ${id}`) };
3037
- const profiles = Array.isArray(p.profiles) ? [...p.profiles] : [...this.options.defaultProfiles ?? ["core/1.0", "review/1.0"]];
3038
- for (const [profile, enforced, option] of [
3039
- ["security-signed/1.0", !!this.options.requireSignatures, "requireSignatures"],
3040
- ["identity-oidc/1.0", this.options.verifyOidcToken !== void 0, "verifyOidcToken"]
3041
- ]) {
3042
- const name = profile.split("/")[0];
3043
- const advertised = profiles.some((x) => x === name || x.startsWith(name + "/"));
3044
- if (enforced && !advertised) profiles.push(profile);
3045
- else if (advertised && !enforced) {
3046
- return { error: rpcError(
3047
- E.PARAMS,
3048
- `${profile} is advertised but ${option} is not configured. A workspace that advertises it and does not enforce it describes a guarantee it does not give.`
3049
- ) };
3050
- }
3976
+ if (Array.isArray(p.profiles)) {
3977
+ const unknown = profilesError(p.profiles);
3978
+ if (unknown) return unknown;
3051
3979
  }
3980
+ const profiles = Array.isArray(p.profiles) ? [...p.profiles] : [...this.options.defaultProfiles ?? ["core/1.0", "review/1.0"]];
3981
+ const signed = profiles.some((x) => x === "security-signed" || x.startsWith("security-signed/"));
3982
+ if (this.options.requireSignatures && !signed) profiles.push("security-signed/1.0");
3983
+ const oidc = profiles.some((x) => x === "identity-oidc" || x.startsWith("identity-oidc/"));
3984
+ if (this.options.verifyOidcToken !== void 0 && !oidc) profiles.push("identity-oidc/1.0");
3985
+ const vc = profiles.some((x) => x === "identity-vc" || x.startsWith("identity-vc/"));
3986
+ if (this.options.verifyVc !== void 0 && !vc) profiles.push("identity-vc/1.0");
3987
+ const unverified = this.unverifiedIdentity(profiles);
3988
+ if (unverified) return { error: unverified };
3052
3989
  const chainEnabled = profiles.includes("audit-scitt/1.0") || !!this.options.enableChain;
3053
3990
  const ws = {
3054
3991
  id,
@@ -3057,9 +3994,14 @@ var Coordinator = class {
3057
3994
  profiles,
3058
3995
  mode: p.mode ?? "trial",
3059
3996
  mode_ceiling: p.mode_ceiling ?? "production",
3060
- routing_policy_uri: p.routing_policy_uri,
3061
- step_up_window_sec: typeof p.step_up_window_sec === "number" ? p.step_up_window_sec : 300,
3062
- min_acr: p.min_acr,
3997
+ // A param given as null gives no value, as in the Python reference: a
3998
+ // null min_acr sets no minimum.
3999
+ routing_policy_uri: p.routing_policy_uri ?? void 0,
4000
+ step_up_window_sec: p.step_up_window_sec ?? 300,
4001
+ min_acr: p.min_acr ?? void 0,
4002
+ signature_policy: policy,
4003
+ join_policy: joinPolicy,
4004
+ ...reviewRule !== void 0 ? { review_rule: reviewRule } : {},
3063
4005
  members: this.newMap(),
3064
4006
  tasks: this.newMap(),
3065
4007
  overrides: this.newMap(),
@@ -3068,14 +4010,17 @@ var Coordinator = class {
3068
4010
  handoffs: this.newMap(),
3069
4011
  snapshots: this.newMap(),
3070
4012
  route_decisions: this.newMap(),
4013
+ invitations: this.newMap(),
3071
4014
  idempotency_keys: {},
3072
4015
  audit: [],
3073
4016
  chain_enabled: chainEnabled,
3074
4017
  chain_head: chainEnabled ? ZERO_HASH : void 0
3075
4018
  };
3076
- this.workspaces.set(id, ws);
4019
+ return { ws };
4020
+ }
4021
+ addWorkspace(ws) {
4022
+ this.workspaces.set(ws.id, ws);
3077
4023
  this.persisted.set(ws, { seq: 0, replace: false, keys: /* @__PURE__ */ new Set() });
3078
- return { result: { workspace: id, created: ws.created } };
3079
4024
  }
3080
4025
  opWorkspaceDescribe(p) {
3081
4026
  const miss = missing(p, ["workspace"]);
@@ -3094,6 +4039,9 @@ var Coordinator = class {
3094
4039
  mode_ceiling: ws.mode_ceiling,
3095
4040
  max_envelope_bytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES,
3096
4041
  step_up_window_sec: ws.step_up_window_sec,
4042
+ signature_policy: ws.signature_policy,
4043
+ join_policy: ws.join_policy,
4044
+ review_rule: ws.review_rule ?? "any_one_approves",
3097
4045
  profiles: ws.profiles,
3098
4046
  members: Array.from(rawValues(ws.members)).map(memberToDict),
3099
4047
  audit_count: ws.audit.length,
@@ -3109,6 +4057,11 @@ var Coordinator = class {
3109
4057
  opWorkspaceSetProfiles(p) {
3110
4058
  const miss = missing(p, ["workspace", "from", "profiles"]);
3111
4059
  if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
4060
+ if (!Array.isArray(p.profiles) || !p.profiles.every((x) => typeof x === "string")) {
4061
+ return { error: rpcError(E.PARAMS, "profiles must be a list of strings") };
4062
+ }
4063
+ const unknown = profilesError(p.profiles);
4064
+ if (unknown) return unknown;
3112
4065
  const ws = this.workspaces.get(p.workspace);
3113
4066
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
3114
4067
  const notMember = this.requireMember(ws, p.from);
@@ -3117,6 +4070,14 @@ var Coordinator = class {
3117
4070
  return { error: rpcError(E.NOT_AUTHORISED, "workspace.set_profiles requires the admin role") };
3118
4071
  }
3119
4072
  const newProfiles = [...p.profiles];
4073
+ for (const name of LASTING_PROFILES) {
4074
+ const held = ws.profiles.find((x) => x === name || x.startsWith(name + "/"));
4075
+ if (held !== void 0 && !newProfiles.some((x) => x.startsWith(name + "/"))) {
4076
+ return { error: rpcError(E.NOT_AUTHORISED, `${held} cannot be removed once advertised`) };
4077
+ }
4078
+ }
4079
+ const unverified = this.unverifiedIdentity(newProfiles);
4080
+ if (unverified) return { error: unverified };
3120
4081
  if (!newProfiles.some((pr) => pr.startsWith("core/"))) newProfiles.push("core/1.0");
3121
4082
  ws.profiles = newProfiles;
3122
4083
  if (newProfiles.includes("audit-scitt/1.0") && !ws.chain_enabled) {
@@ -3128,20 +4089,76 @@ var Coordinator = class {
3128
4089
  opParticipantJoin(p) {
3129
4090
  const miss = missing(p, ["workspace", "from", "type"]);
3130
4091
  if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
4092
+ for (const f of ["from", "type"]) {
4093
+ if (typeof p[f] !== "string") return { error: rpcError(E.PARAMS, `${f} must be a string`) };
4094
+ }
4095
+ const type = p.type;
4096
+ const named = participantError(type, p.from, "from");
4097
+ if (named) return named;
4098
+ const bad = paramsError(p, [
4099
+ ["role", "string"],
4100
+ ["display_name", "string"],
4101
+ ["scopes", "strings"],
4102
+ ["capabilities", "object"],
4103
+ ["jwks", "object"],
4104
+ ["jwks.keys", "list"],
4105
+ ["oidc_token", "string"],
4106
+ ["vc_presentation", "object"]
4107
+ ]);
4108
+ if (bad) return bad;
4109
+ if (p.role === "admin" && !ADMIN_TYPES.includes(type)) {
4110
+ return { error: rpcError(E.PARAMS, "The admin role is held by a human or a service") };
4111
+ }
3131
4112
  let ws = this.workspaces.get(p.workspace);
4113
+ const made = ws === void 0;
3132
4114
  if (!ws) {
3133
- const create = this.opWorkspaceCreate({ workspace: p.workspace });
3134
- if (create.error) return create;
3135
- ws = this.workspaces.get(p.workspace);
4115
+ const built = this.buildWorkspace({ workspace: p.workspace });
4116
+ if (built.error) return { error: built.error };
4117
+ ws = built.ws;
3136
4118
  }
3137
4119
  const uri = p.from;
3138
4120
  const now = this.now();
3139
4121
  const existing = ws.members.get(uri);
3140
4122
  let tokenVerified = false;
4123
+ let holderVerified = false;
4124
+ let credentialBound = false;
4125
+ const invitation = existing ? void 0 : rawInvitation(ws, uri);
4126
+ const asked = typeof p.role === "string" && p.role !== "" ? p.role : void 0;
4127
+ let role = asked ?? "participant";
4128
+ if (!existing) {
4129
+ const first = awaitsFirstMember(ws);
4130
+ if (ws.join_policy === "invite" && !invitation && !(first && ADMIN_TYPES.includes(type))) {
4131
+ return { error: rpcError(E.NOT_AUTHORISED, `${uri} has no invitation to this workspace`) };
4132
+ }
4133
+ if (invitation?.role !== void 0) {
4134
+ if (asked === "admin" && invitation.role !== "admin") {
4135
+ return { error: rpcError(E.NOT_AUTHORISED, "The admin role is granted by an admin") };
4136
+ }
4137
+ role = invitation.role;
4138
+ } else if (first && ws.join_policy === "invite") {
4139
+ if (asked !== void 0 && asked !== "admin") {
4140
+ return { error: rpcError(
4141
+ E.PARAMS,
4142
+ "The first member of a workspace that admits by invitation is its admin"
4143
+ ) };
4144
+ }
4145
+ role = "admin";
4146
+ } else if (asked === "admin" && !first) {
4147
+ return { error: rpcError(E.NOT_AUTHORISED, "The admin role is granted by an admin") };
4148
+ }
4149
+ }
4150
+ if (type === "human") {
4151
+ const oidc = this.options.verifyOidcToken !== void 0 && advertises(ws, "identity-oidc");
4152
+ const vc = this.options.verifyVc !== void 0 && advertises(ws, "identity-vc");
4153
+ const presented = oidc && typeof p.oidc_token === "string" || vc && isObject(p.vc_presentation);
4154
+ if ((oidc || vc) && !presented) {
4155
+ return { error: oidc ? rpcError(E.OIDC_TOKEN_INVALID, vc ? "An oidc_token or a vc_presentation is required to join as a human participant" : "An oidc_token is required to join as a human participant") : rpcError(E.VC_VP_INVALID, "A vc_presentation is required to join as a human participant") };
4156
+ }
4157
+ }
3141
4158
  const member = {
3142
4159
  uri,
3143
- type: p.type,
3144
- role: p.role || "participant",
4160
+ type,
4161
+ role,
3145
4162
  joined: now,
3146
4163
  display_name: p.display_name,
3147
4164
  capabilities: p.capabilities,
@@ -3149,6 +4166,9 @@ var Coordinator = class {
3149
4166
  keys: [],
3150
4167
  paused: false
3151
4168
  };
4169
+ if (invitation?.oidc_sub !== void 0 && (this.options.verifyOidcToken === void 0 || typeof p.oidc_token !== "string")) {
4170
+ return { error: rpcError(E.OIDC_TOKEN_INVALID, "An oidc_token is required: the invitation names a token subject") };
4171
+ }
3152
4172
  if (this.options.verifyOidcToken && typeof p.oidc_token === "string") {
3153
4173
  const claims = this.options.verifyOidcToken(p.oidc_token);
3154
4174
  if (claims === null) return { error: rpcError(E.OIDC_TOKEN_INVALID, "OIDC token invalid") };
@@ -3157,6 +4177,9 @@ var Coordinator = class {
3157
4177
  if (boundUri != null && boundUri !== uri) {
3158
4178
  return { error: rpcError(E.OIDC_CNF_MISMATCH, "OIDC token is bound to another participant") };
3159
4179
  }
4180
+ if (invitation?.oidc_sub !== void 0 && sub !== invitation.oidc_sub) {
4181
+ return { error: rpcError(E.OIDC_CNF_MISMATCH, "OIDC token subject does not match the invitation") };
4182
+ }
3160
4183
  if (existing) {
3161
4184
  if (existing.oidc_sub != null) {
3162
4185
  if (sub !== existing.oidc_sub) {
@@ -3165,19 +4188,23 @@ var Coordinator = class {
3165
4188
  } else if (boundUri !== uri) {
3166
4189
  return { error: rpcError(E.OIDC_CNF_MISMATCH, "OIDC token does not name this member") };
3167
4190
  }
4191
+ } else if (type === "human" && boundUri !== uri && invitation?.oidc_sub === void 0) {
4192
+ return { error: rpcError(E.OIDC_CNF_MISMATCH, "OIDC token does not name this participant") };
3168
4193
  }
3169
4194
  member.oidc_sub = sub;
3170
4195
  tokenVerified = true;
3171
4196
  const at = claims.auth_time;
3172
- if (typeof at === "number") member.oidc_auth_time = at;
4197
+ if (typeof at === "number" && Number.isFinite(at) && Math.abs(at) <= Number.MAX_SAFE_INTEGER) member.oidc_auth_time = Math.floor(at);
3173
4198
  member.oidc_acr = typeof claims.acr === "string" ? claims.acr : void 0;
3174
- const cnf = claims.cnf;
3175
- const cnfJwk = cnf?.jwk;
3176
- if (cnfJwk && typeof cnfJwk.kid === "string") {
3177
- member.keys.push({ jwk: cnfJwk, kid: cnfJwk.kid, valid_from: now });
4199
+ const cnf = isObject(claims.cnf) ? claims.cnf.jwk : void 0;
4200
+ const bound = credentialKeys(cnf, cnf == null ? claims.nonce : void 0, signingKeys(p.jwks));
4201
+ if (bound === null) return { error: rpcError(E.OIDC_CNF_MISMATCH, "The join's keys are not the key the token binds") };
4202
+ if (bound !== void 0) {
4203
+ credentialBound = true;
4204
+ for (const j of bound) member.keys.push({ jwk: j, kid: j.kid, valid_from: now });
3178
4205
  }
3179
4206
  }
3180
- if (this.options.verifyVc && typeof p.vc_presentation === "object" && p.vc_presentation !== null && !Array.isArray(p.vc_presentation)) {
4207
+ if (this.options.verifyVc && isObject(p.vc_presentation)) {
3181
4208
  const subject = this.options.verifyVc(p.vc_presentation);
3182
4209
  if (subject === null) return { error: rpcError(E.VC_VP_INVALID, "VC presentation invalid") };
3183
4210
  const holder = [subject.holder, subject.id].find(
@@ -3187,21 +4214,54 @@ var Coordinator = class {
3187
4214
  return { error: rpcError(E.VC_HOLDER_BINDING_INVALID, "Presentation holder does not match the member") };
3188
4215
  }
3189
4216
  member.vc_holder = holder;
3190
- const vpJwk = subject.cnf_jwk;
3191
- if (vpJwk && typeof vpJwk.kid === "string") {
3192
- member.keys.push({ jwk: vpJwk, kid: vpJwk.kid, valid_from: now });
4217
+ holderVerified = true;
4218
+ const bound = credentialKeys(subject.cnf_jwk, void 0, signingKeys(p.jwks));
4219
+ if (bound === null) {
4220
+ return { error: rpcError(E.VC_HOLDER_BINDING_INVALID, "The join's keys are not the key the presentation binds") };
4221
+ }
4222
+ if (bound !== void 0) {
4223
+ credentialBound = true;
4224
+ for (const j of bound) member.keys.push({ jwk: j, kid: j.kid, valid_from: now });
4225
+ }
4226
+ }
4227
+ if (!existing && type === "human" && (tokenVerified || holderVerified) && !credentialBound && !invitation?.jwks && signingKeys(p.jwks).length > 0) {
4228
+ return { error: tokenVerified ? rpcError(E.OIDC_CNF_MISMATCH, "The token binds no key: a person it verifies signs with a key it binds") : rpcError(
4229
+ E.VC_HOLDER_BINDING_INVALID,
4230
+ "The presentation binds no key: a person it verifies signs with a key it binds"
4231
+ ) };
4232
+ }
4233
+ const pinned = invitation?.jwks?.keys;
4234
+ if (pinned) {
4235
+ const strays = [...signingKeys(p.jwks), ...member.keys.map((k) => k.jwk)].filter((j) => !pinned.some((k) => sameKey(k, j)));
4236
+ if (strays.length) {
4237
+ return { error: rpcError(
4238
+ E.NOT_AUTHORISED,
4239
+ `${uri} joins with keys its invitation does not name`,
4240
+ { kids: [...new Set(strays.map((j) => j.kid))] }
4241
+ ) };
3193
4242
  }
4243
+ member.keys = pinned.map((j) => ({ jwk: j, kid: j.kid, valid_from: now }));
3194
4244
  }
3195
4245
  const attested = [...member.keys];
3196
- const jwks = p.jwks;
3197
- if (jwks?.keys && attested.length === 0) {
3198
- for (const j of jwks.keys) {
3199
- if (j.kid && !member.keys.some((k) => k.kid === j.kid)) {
4246
+ if (attested.length === 0) {
4247
+ for (const j of signingKeys(p.jwks)) {
4248
+ if (!member.keys.some((k) => k.kid === j.kid)) {
3200
4249
  member.keys.push({ jwk: j, kid: j.kid, valid_from: now });
3201
4250
  }
3202
4251
  }
3203
4252
  }
3204
4253
  if (existing) {
4254
+ if (attested.length === 0) {
4255
+ const keys = signingKeys(p.jwks);
4256
+ const held = keys.some((j) => existing.keys.some((k) => sameKey(k.jwk, j)));
4257
+ if (keys.length > 0 && !held) {
4258
+ return { error: rpcError(
4259
+ E.NOT_AUTHORISED,
4260
+ `${uri} is already a member, with other keys; a new key arrives through participant.rotate_key`,
4261
+ { kids: [...new Set(keys.map((j) => j.kid))] }
4262
+ ) };
4263
+ }
4264
+ }
3205
4265
  if (member.oidc_sub !== void 0) existing.oidc_sub = member.oidc_sub;
3206
4266
  if (member.oidc_auth_time !== void 0) existing.oidc_auth_time = member.oidc_auth_time;
3207
4267
  if (member.oidc_acr !== void 0) existing.oidc_acr = member.oidc_acr;
@@ -3212,18 +4272,126 @@ var Coordinator = class {
3212
4272
  }
3213
4273
  return { result: { joined: true, as: uri } };
3214
4274
  }
4275
+ if (this.requiresSignatures(ws) && this.mustSign(ws, type) && member.keys.length === 0) {
4276
+ return { error: rpcError(E.PARAMS, `${uri} must sign in this workspace, and joins with no key`) };
4277
+ }
4278
+ if (made) this.addWorkspace(ws);
3215
4279
  ws.members.set(uri, member);
4280
+ if (invitation) ws.invitations.delete(uri);
3216
4281
  return { result: { joined: true, as: uri } };
3217
4282
  }
4283
+ /**
4284
+ * SPECIFICATION 6.3.3: an admin invites a participant. For a URI that is
4285
+ * not a member it records an invitation, which the invitee's join takes
4286
+ * up. For a member that holds no key in force it registers the
4287
+ * invitation's keys: the way back for a member whose keys were all revoked,
4288
+ * or who joined a workspace that requires signatures with none.
4289
+ */
4290
+ opWorkspaceInvite(p) {
4291
+ const miss = missing(p, ["workspace", "from", "uri", "type"]);
4292
+ if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
4293
+ for (const f of ["uri", "type"]) {
4294
+ if (typeof p[f] !== "string") return { error: rpcError(E.PARAMS, `${f} must be a string`) };
4295
+ }
4296
+ const bad = paramsError(p, [
4297
+ ["role", "string"],
4298
+ ["jwks", "object"],
4299
+ ["jwks.keys", "list"],
4300
+ ["oidc_sub", "string"]
4301
+ ]);
4302
+ if (bad) return bad;
4303
+ const ws = this.workspaces.get(p.workspace);
4304
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
4305
+ const notMember = this.requireMember(ws, p.from);
4306
+ if (notMember) return notMember;
4307
+ if (rawMember(ws, p.from).role !== "admin") {
4308
+ return { error: rpcError(E.NOT_AUTHORISED, "workspace.invite requires the admin role") };
4309
+ }
4310
+ const uri = p.uri;
4311
+ const type = p.type;
4312
+ const named = participantError(type, uri, "uri");
4313
+ if (named) return named;
4314
+ const role = typeof p.role === "string" && p.role !== "" ? p.role : void 0;
4315
+ if (role === "admin" && !ADMIN_TYPES.includes(type)) {
4316
+ return { error: rpcError(E.PARAMS, "The admin role is held by a human or a service") };
4317
+ }
4318
+ const sub = typeof p.oidc_sub === "string" ? p.oidc_sub : void 0;
4319
+ if (sub !== void 0 && this.options.verifyOidcToken === void 0) {
4320
+ return { error: rpcError(E.PARAMS, "oidc_sub names a token subject, and this Coordinator has no token verifier") };
4321
+ }
4322
+ let pinned;
4323
+ if (p.jwks != null) {
4324
+ pinned = signingKeys(p.jwks);
4325
+ if (pinned.length === 0) return { error: rpcError(E.PARAMS, "jwks names no key: each key needs a kid") };
4326
+ }
4327
+ const now = this.now();
4328
+ const member = ws.members.get(uri);
4329
+ if (member) {
4330
+ if (member.keys.some((k) => inForce(k, now))) {
4331
+ return { error: rpcError(
4332
+ E.PARAMS,
4333
+ `${uri} is a member and holds a key in force; its keys change through participant.rotate_key`
4334
+ ) };
4335
+ }
4336
+ if (!pinned) {
4337
+ return { error: rpcError(E.PARAMS, `${uri} is a member; an invitation for a member registers the keys its jwks names`) };
4338
+ }
4339
+ if (this.options.verifyOidcToken !== void 0 && (member.type === "human" || member.oidc_sub !== void 0) || this.options.verifyVc !== void 0 && (member.type === "human" || member.vc_holder !== void 0)) {
4340
+ return { error: rpcError(
4341
+ E.PARAMS,
4342
+ `${uri} registers a new key by joining again with a token that binds it; an invitation does not register a person's keys here`
4343
+ ) };
4344
+ }
4345
+ if (role !== void 0 && role !== member.role || sub !== void 0 && sub !== member.oidc_sub) {
4346
+ return { error: rpcError(E.PARAMS, `${uri} is a member; an invitation for a member registers keys and changes nothing else`) };
4347
+ }
4348
+ const reused = pinned.find((j) => member.keys.some((k) => k.kid === j.kid));
4349
+ if (reused) return { error: rpcError(E.PARAMS, `Key ${reused.kid} was ${uri}'s; a new key takes a new kid`) };
4350
+ for (const j of pinned) member.keys.push({ jwk: j, kid: j.kid, valid_from: now });
4351
+ return { result: { invited: true, uri, registered: pinned.map((j) => j.kid) } };
4352
+ }
4353
+ if (pinned && (type === "human" || sub !== void 0) && (this.options.verifyOidcToken !== void 0 || this.options.verifyVc !== void 0)) {
4354
+ return { error: rpcError(
4355
+ E.PARAMS,
4356
+ `${uri} joins with a token that binds its key; an invitation does not register a person's keys here`
4357
+ ) };
4358
+ }
4359
+ const invitation = {
4360
+ uri,
4361
+ type,
4362
+ invited_by: p.from,
4363
+ invited_at: now
4364
+ };
4365
+ if (role !== void 0) invitation.role = role;
4366
+ if (pinned) invitation.jwks = { keys: pinned };
4367
+ if (sub !== void 0) invitation.oidc_sub = sub;
4368
+ ws.invitations.set(uri, invitation);
4369
+ return { result: { invited: true, uri } };
4370
+ }
3218
4371
  opParticipantLeave(p) {
3219
4372
  const ws = this.workspaces.get(p.workspace);
3220
4373
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
4374
+ if (rawMember(ws, p.from)?.paused) {
4375
+ return { error: rpcError(E.NOT_AUTHORISED, "A paused participant does not leave until another member lifts its pause") };
4376
+ }
3221
4377
  ws.members.delete(p.from);
3222
4378
  return { result: { left: true } };
3223
4379
  }
3224
4380
  opTaskCreate(p) {
3225
4381
  const miss = missing(p, ["workspace", "from", "kind", "input"]);
3226
4382
  if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
4383
+ const bad = paramsError(p, [
4384
+ ["assignee", "string"],
4385
+ ["to", "string"],
4386
+ ["mode", "string"],
4387
+ ["review_required", "boolean"],
4388
+ ["routing_hints", "object"],
4389
+ ["idempotency_key", "string"]
4390
+ ]);
4391
+ if (bad) return bad;
4392
+ if (RESERVED_KEYS.includes(p.idempotency_key)) {
4393
+ return { error: rpcError(E.PARAMS, "idempotency_key must not be __proto__, constructor or prototype") };
4394
+ }
3227
4395
  const ws = this.workspaces.get(p.workspace);
3228
4396
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
3229
4397
  const notMember = this.requireMember(ws, p.from);
@@ -3235,9 +4403,6 @@ var Coordinator = class {
3235
4403
  if (ws.members.get(assignee).paused) {
3236
4404
  return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Assignee ${assignee} is paused`) };
3237
4405
  }
3238
- if (p.mode !== void 0 && p.mode !== null && typeof p.mode !== "string") {
3239
- return { error: rpcError(E.PARAMS, "mode must be a string") };
3240
- }
3241
4406
  const requestedMode = p.mode || ws.mode;
3242
4407
  if (!modeLE(requestedMode, ws.mode_ceiling)) {
3243
4408
  return { error: rpcError(
@@ -3245,9 +4410,6 @@ var Coordinator = class {
3245
4410
  `Requested mode ${requestedMode} exceeds ceiling ${ws.mode_ceiling}`
3246
4411
  ) };
3247
4412
  }
3248
- if (p.review_required !== void 0 && p.review_required !== null && typeof p.review_required !== "boolean") {
3249
- return { error: rpcError(E.PARAMS, "review_required must be a boolean") };
3250
- }
3251
4413
  const taskId = this.ids.taskId();
3252
4414
  const now = this.now();
3253
4415
  const task = {
@@ -3286,6 +4448,7 @@ var Coordinator = class {
3286
4448
  if (notMember) return notMember;
3287
4449
  const task = ws.tasks.get(p.task_id);
3288
4450
  if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
4451
+ if (typeof p.state !== "string") return { error: rpcError(E.PARAMS, "state must be a string") };
3289
4452
  const newState = p.state;
3290
4453
  const legal = {
3291
4454
  created: ["in_progress", "declined"],
@@ -3296,12 +4459,42 @@ var Coordinator = class {
3296
4459
  if (!legal[task.state]?.includes(newState)) {
3297
4460
  return { error: rpcError(E.PARAMS, `Illegal transition ${task.state} -> ${newState}`) };
3298
4461
  }
3299
- if (newState === "completed" && task.review_required) {
4462
+ if (newState === "completed" && reviewRequired(ws, task)) {
3300
4463
  return { error: rpcError(
3301
4464
  E.PARAMS,
3302
4465
  "This task requires review. Submit the output with task.complete, which opens the review; a reviewer decision completes it."
3303
4466
  ) };
3304
4467
  }
4468
+ if (newState === "completed" && task.review) {
4469
+ return { error: rpcError(
4470
+ E.PARAMS,
4471
+ "This task holds a review. Submit the output with task.complete, which opens a new round; a reviewer decision completes it."
4472
+ ) };
4473
+ }
4474
+ if (task.state === "review_requested" && !task.review.authors.includes(p.from)) {
4475
+ return { error: rpcError(
4476
+ E.NOT_AUTHORISED,
4477
+ "Only the authors of the work under review take it back from review"
4478
+ ) };
4479
+ }
4480
+ if (newState === "cancelled" && openRequiredReviewAuthors(ws, task).includes(p.from)) {
4481
+ return { error: rpcError(
4482
+ E.NOT_AUTHORISED,
4483
+ "The authors of the work under review cannot end its review without a decision"
4484
+ ) };
4485
+ }
4486
+ if (newState === "declined" && task.review && !task.review.decisions.some((d) => d.kind === "reject")) {
4487
+ return { error: rpcError(
4488
+ E.PARAMS,
4489
+ "This task holds a review no decision has closed; a reviewer's decision ends it"
4490
+ ) };
4491
+ }
4492
+ if (newState === "declined" && !task.review && openRequiredReviewAuthors(ws, task).length > 0) {
4493
+ return { error: rpcError(
4494
+ E.PARAMS,
4495
+ "This task carries on a review no decision has closed; completing it opens the review a reviewer decides"
4496
+ ) };
4497
+ }
3305
4498
  task.state = newState;
3306
4499
  task.updated_at = this.now();
3307
4500
  task.history.push({
@@ -3322,32 +4515,60 @@ var Coordinator = class {
3322
4515
  if (task.state !== "created" && task.state !== "in_progress") {
3323
4516
  return { error: rpcError(E.PARAMS, `Cannot complete task in state: ${task.state}`) };
3324
4517
  }
3325
- if (task.review_required) {
4518
+ if (p.confidence != null && typeof p.confidence !== "number" && typeof p.confidence !== "string") {
4519
+ return { error: rpcError(E.PARAMS, "confidence must be a number or a string") };
4520
+ }
4521
+ const required = reviewRequired(ws, task);
4522
+ if (required && p.from !== task.assignee) {
4523
+ return { error: rpcError(E.NOT_AUTHORISED, "Only the assignee completes a task that requires review") };
4524
+ }
4525
+ if (required || task.review) {
3326
4526
  const now = this.now();
4527
+ const authors = authorsOf(task, p.from);
4528
+ let note = OPENED_ON_COMPLETE;
3327
4529
  if (!task.review) {
3328
- const producer = p.from;
3329
- const eligible = [...rawEntries(ws.members)].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
4530
+ const eligible = [...rawEntries(ws.members)].filter(([uri, member]) => !authors.includes(uri) && member.type === "human").map(([uri]) => uri);
3330
4531
  if (eligible.length === 0) {
3331
4532
  return { error: rpcError(
3332
4533
  E.NOT_AUTHORISED,
3333
4534
  "Task requires review but has no eligible human reviewer (needs a human member other than its assignee and completer)"
3334
4535
  ) };
3335
4536
  }
3336
- task.review = { requested_at: now, requested_to: eligible, rule: "any_one_approves", decisions: [] };
4537
+ task.review = {
4538
+ requested_at: now,
4539
+ requested_to: eligible,
4540
+ rule: ws.review_rule ?? "any_one_approves",
4541
+ decisions: [],
4542
+ authors,
4543
+ acknowledgements: []
4544
+ };
4545
+ } else {
4546
+ const roundAuthors = unionOf(task.review.authors, authors);
4547
+ const reviewers = task.review.requested_to.filter((r) => !roundAuthors.includes(r) && !(required && rawMember(ws, r) !== void 0 && rawMember(ws, r).type !== "human"));
4548
+ if (reviewers.length === 0) {
4549
+ return { error: rpcError(
4550
+ E.NOT_AUTHORISED,
4551
+ "No reviewer of this task remains once the authors of the work are left out"
4552
+ ) };
4553
+ }
4554
+ task.review = {
4555
+ ...task.review,
4556
+ requested_at: now,
4557
+ requested_to: reviewers,
4558
+ decisions: [],
4559
+ authors: roundAuthors,
4560
+ acknowledgements: []
4561
+ };
4562
+ note = NEW_ROUND_ON_COMPLETE;
3337
4563
  }
3338
4564
  task.pending_artefact = p.output;
3339
4565
  task.state = "review_requested";
3340
4566
  task.updated_at = now;
3341
- task.history.push({
3342
- ts: now,
3343
- from: p.from,
3344
- state: "review_requested",
3345
- note: "review required; opened on task.complete"
3346
- });
4567
+ task.history.push({ ts: now, from: p.from, state: "review_requested", note });
3347
4568
  return { result: { state: "review_requested", review_id: task.id } };
3348
4569
  }
3349
4570
  task.output = p.output;
3350
- if (typeof p.confidence === "number" || typeof p.confidence === "string") task.confidence = p.confidence;
4571
+ if (p.confidence != null) task.confidence = p.confidence;
3351
4572
  task.state = "completed";
3352
4573
  task.updated_at = this.now();
3353
4574
  task.history.push({ ts: task.updated_at, from: p.from, state: "completed" });
@@ -3360,8 +4581,16 @@ var Coordinator = class {
3360
4581
  const notMember = this.requireMember(ws, p.from);
3361
4582
  if (notMember) return notMember;
3362
4583
  }
3363
- const range = p.range ?? {};
3364
- const filter = p.filter ?? {};
4584
+ const bad = paramsError(p, [
4585
+ ["range.from_seq", "integer"],
4586
+ ["range.to_seq", "integer"],
4587
+ ["filter.method", "string"],
4588
+ ["filter.from", "string"],
4589
+ ["filter.task_id", "string"]
4590
+ ]);
4591
+ if (bad) return bad;
4592
+ const range = isObject(p.range) ? p.range : {};
4593
+ const filter = isObject(p.filter) ? p.filter : {};
3365
4594
  if (filter.outcome != null && filter.outcome !== "accepted" && filter.outcome !== "refused") {
3366
4595
  return { error: rpcError(E.PARAMS, "filter.outcome must be 'accepted' or 'refused'") };
3367
4596
  }
@@ -3412,37 +4641,73 @@ var Coordinator = class {
3412
4641
  return { error: rpcError(E.NOT_REVIEWABLE, `Cannot request review for task in state: ${task.state}`) };
3413
4642
  }
3414
4643
  const to = p.to;
4644
+ if (to != null && typeof to !== "string" && !(Array.isArray(to) && to.every((r) => typeof r === "string"))) {
4645
+ return { error: rpcError(E.PARAMS, "to must be a string or a list of strings") };
4646
+ }
3415
4647
  const reviewers = Array.isArray(to) ? to : typeof to === "string" ? [to] : [];
3416
4648
  if (!reviewers.length) return { error: rpcError(E.PARAMS, "review.request needs 'to'") };
4649
+ const bad = paramsError(p, [["rule", "string"]]);
4650
+ if (bad) return bad;
3417
4651
  const now = this.now();
3418
4652
  const declaredRule = p.rule;
3419
4653
  const rule = declaredRule || "any_one_approves";
3420
4654
  if (!reviewRuleSupported(rule)) {
3421
4655
  return { error: rpcError(E.PARAMS, `Unsupported review rule ${JSON.stringify(rule)}: review/1.0 supports any_one_approves, all_approve, quorum:<n>`) };
3422
4656
  }
3423
- if (task.state === "review_requested" && task.review) {
3424
- if (contentHash(task.pending_artefact) !== contentHash(p.artefact)) {
4657
+ const open = task.state === "review_requested" ? task.review : void 0;
4658
+ if (open) {
4659
+ if (contentHash(task.pending_artefact ?? null) !== contentHash(p.artefact ?? null)) {
3425
4660
  return { error: rpcError(
3426
4661
  E.REVIEW_ALREADY_OPEN,
3427
4662
  "A review is already open on this task with different content. Decide, abstain, escalate or cancel it before requesting review of a new artefact."
3428
4663
  ) };
3429
4664
  }
3430
- if (declaredRule !== void 0 && declaredRule !== task.review.rule) {
4665
+ if (declaredRule != null && declaredRule !== open.rule) {
3431
4666
  return { error: rpcError(
3432
4667
  E.REVIEW_ALREADY_OPEN,
3433
- `Cannot change the decision rule of an open review (currently ${task.review.rule})`
4668
+ `Cannot change the decision rule of an open review (currently ${open.rule})`
4669
+ ) };
4670
+ }
4671
+ }
4672
+ const authors = open ? open.authors : unionOf(task.review ? task.review.authors : [], authorsOf(task, lastCompleter(task)));
4673
+ if (reviewRequired(ws, task)) {
4674
+ if (!open) {
4675
+ return { error: rpcError(
4676
+ E.NOT_AUTHORISED,
4677
+ "This task requires a person's review, which opens on task.complete; review.request only adds reviewers to it once it is open"
4678
+ ) };
4679
+ }
4680
+ if (authors.includes(p.from)) {
4681
+ return { error: rpcError(E.NOT_AUTHORISED, "The author of the work under review cannot request its review") };
4682
+ }
4683
+ const scope = reviewers.find(isScope);
4684
+ if (scope !== void 0) {
4685
+ return { error: rpcError(
4686
+ E.PARAMS,
4687
+ `A review that requires a person names its reviewers, and ${scope} is an address scope`
3434
4688
  ) };
3435
4689
  }
3436
4690
  for (const r of reviewers) {
3437
- if (!task.review.requested_to.includes(r)) task.review.requested_to.push(r);
4691
+ if (authors.includes(r)) {
4692
+ return { error: rpcError(E.NOT_AUTHORISED, `The author of the work under review cannot review it: ${r}`) };
4693
+ }
4694
+ const member = rawMember(ws, r);
4695
+ if (member && member.type !== "human") {
4696
+ return { error: rpcError(E.NOT_AUTHORISED, `This task requires a person's review: ${r} is not a human member`) };
4697
+ }
4698
+ }
4699
+ }
4700
+ if (open) {
4701
+ for (const r of reviewers) {
4702
+ if (!open.requested_to.includes(r)) open.requested_to.push(r);
3438
4703
  }
3439
- if (p.deadline !== void 0) task.review.deadline = p.deadline;
4704
+ if (p.deadline != null) open.deadline = p.deadline;
3440
4705
  task.updated_at = now;
3441
4706
  return { result: {
3442
4707
  state: "review_requested",
3443
4708
  review_id: task.id,
3444
4709
  amended: true,
3445
- requested_to: [...task.review.requested_to]
4710
+ requested_to: [...open.requested_to]
3446
4711
  } };
3447
4712
  }
3448
4713
  task.state = "review_requested";
@@ -3452,12 +4717,41 @@ var Coordinator = class {
3452
4717
  requested_to: reviewers,
3453
4718
  rule,
3454
4719
  deadline: p.deadline,
3455
- decisions: []
4720
+ decisions: [],
4721
+ authors,
4722
+ acknowledgements: []
3456
4723
  };
3457
4724
  task.history.push({ ts: now, from: p.from, state: "review_requested" });
3458
4725
  task.pending_artefact = p.artefact;
3459
4726
  return { result: { state: "review_requested", review_id: task.id } };
3460
4727
  }
4728
+ /**
4729
+ * review/1.0 S3.6: a reviewer says they have begun the open review. Only
4730
+ * someone who may decide it can, so the log's record of who took a review
4731
+ * up, and when, names reviewers alone. It changes nothing else: the review
4732
+ * stays open and the acknowledgement counts towards no rule.
4733
+ */
4734
+ opReviewAcknowledge(p) {
4735
+ const miss = missing(p, ["workspace", "from", "task_id"]);
4736
+ if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
4737
+ const ws = this.workspaces.get(p.workspace);
4738
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
4739
+ const notMember = this.requireMember(ws, p.from);
4740
+ if (notMember) return notMember;
4741
+ const task = ws.tasks.get(p.task_id);
4742
+ if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
4743
+ if (task.state !== "review_requested") {
4744
+ return { error: rpcError(E.NOT_REVIEWABLE, `Task not awaiting review: ${task.state}`) };
4745
+ }
4746
+ const notReviewer = this.requireReviewer(ws, task, p.from);
4747
+ if (notReviewer) return notReviewer;
4748
+ const review = task.review;
4749
+ if (review.acknowledgements.some((a) => a.reviewer === p.from)) {
4750
+ return { error: rpcError(E.PARAMS, "Already acknowledged in this round") };
4751
+ }
4752
+ review.acknowledgements.push({ reviewer: p.from, ts: this.now() });
4753
+ return { result: { acknowledged: true, requested_at: review.requested_at } };
4754
+ }
3461
4755
  /**
3462
4756
  * Verify an optional `approved_artefact_digest` against the artefact under
3463
4757
  * review (review/1.0 S3.2).
@@ -3476,7 +4770,7 @@ var Coordinator = class {
3476
4770
  if (typeof declared !== "string") {
3477
4771
  return { error: rpcError(E.PARAMS, "approved_artefact_digest must be a string") };
3478
4772
  }
3479
- if (task.pending_artefact === void 0) {
4773
+ if (task.pending_artefact === void 0 || task.pending_artefact === null) {
3480
4774
  return { error: rpcError(E.PARAMS, "No artefact under review to bind a digest to") };
3481
4775
  }
3482
4776
  const actual = contentHash(task.pending_artefact);
@@ -3492,8 +4786,8 @@ var Coordinator = class {
3492
4786
  opDecide(p, kind) {
3493
4787
  const miss = missing(p, ["workspace", "from", "task_id"]);
3494
4788
  if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
3495
- const badTags = tagsError(p);
3496
- if (badTags) return badTags;
4789
+ const bad = paramsError(p, [["tags", "strings"], ["request_revision", "boolean"]]);
4790
+ if (bad) return bad;
3497
4791
  const ws = this.workspaces.get(p.workspace);
3498
4792
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
3499
4793
  const notMember = this.requireMember(ws, p.from);
@@ -3503,7 +4797,7 @@ var Coordinator = class {
3503
4797
  if (task.state !== "review_requested") {
3504
4798
  return { error: rpcError(E.NOT_REVIEWABLE, `Task not awaiting review: ${task.state}`) };
3505
4799
  }
3506
- const notReviewer = this.requireReviewer(task, p.from);
4800
+ const notReviewer = this.requireReviewer(ws, task, p.from);
3507
4801
  if (notReviewer) return notReviewer;
3508
4802
  const digestError = this.checkArtefactDigest(p, task);
3509
4803
  if (digestError) return digestError;
@@ -3530,8 +4824,8 @@ var Coordinator = class {
3530
4824
  opDecideOverride(p) {
3531
4825
  const miss = missing(p, ["workspace", "from", "task_id", "diff", "rationale"]);
3532
4826
  if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
3533
- const badTags = tagsError(p);
3534
- if (badTags) return badTags;
4827
+ const bad = paramsError(p, [["tags", "strings"], ["policy_refs", "strings"]]);
4828
+ if (bad) return bad;
3535
4829
  const ws = this.workspaces.get(p.workspace);
3536
4830
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
3537
4831
  const notMember = this.requireMember(ws, p.from);
@@ -3541,15 +4835,17 @@ var Coordinator = class {
3541
4835
  if (task.state !== "review_requested") {
3542
4836
  return { error: rpcError(E.NOT_REVIEWABLE, `Task not awaiting review: ${task.state}`) };
3543
4837
  }
3544
- const notReviewer = this.requireReviewer(task, p.from);
4838
+ const notReviewer = this.requireReviewer(ws, task, p.from);
3545
4839
  if (notReviewer) return notReviewer;
3546
4840
  const digestError = this.checkArtefactDigest(p, task);
3547
4841
  if (digestError) return digestError;
3548
4842
  const base = task.pending_artefact;
3549
- if (base === void 0) return { error: rpcError(E.PARAMS, "No base artefact for override") };
4843
+ if (base === void 0 || base === null) return { error: rpcError(E.PARAMS, "No base artefact for override") };
3550
4844
  let applied;
3551
4845
  try {
3552
- applied = applyJsonPatch(base, p.diff);
4846
+ applied = applyJsonPatch(base, p.diff, {
4847
+ maxStringBytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES
4848
+ });
3553
4849
  } catch (e) {
3554
4850
  const msg = e instanceof Error ? e.message : String(e);
3555
4851
  return { error: rpcError(E.PATCH_FAILED, msg) };
@@ -3588,6 +4884,8 @@ var Coordinator = class {
3588
4884
  opAbstainDeclare(p) {
3589
4885
  const miss = missing(p, ["workspace", "from", "task_id", "reason"]);
3590
4886
  if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
4887
+ const bad = paramsError(p, [["category", "string"]]);
4888
+ if (bad) return bad;
3591
4889
  const ws = this.workspaces.get(p.workspace);
3592
4890
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
3593
4891
  const notMember = this.requireMember(ws, p.from);
@@ -3597,7 +4895,7 @@ var Coordinator = class {
3597
4895
  if (task.state !== "review_requested") {
3598
4896
  return { error: rpcError(E.NOT_REVIEWABLE, `Task not awaiting review: ${task.state}`) };
3599
4897
  }
3600
- const notReviewer = this.requireReviewer(task, p.from);
4898
+ const notReviewer = this.requireReviewer(ws, task, p.from);
3601
4899
  if (notReviewer) return notReviewer;
3602
4900
  const now = this.now();
3603
4901
  task.review.decisions.push({
@@ -3629,6 +4927,8 @@ var Coordinator = class {
3629
4927
  return { error: rpcError(E.PARAMS, "new_task must be an object") };
3630
4928
  }
3631
4929
  const nt = p.new_task;
4930
+ const bad = paramsError(p, [["new_task.kind", "string"]]);
4931
+ if (bad) return bad;
3632
4932
  const assignee = nt.assignee;
3633
4933
  if (typeof assignee !== "string" || !ws.members.has(assignee)) {
3634
4934
  return { error: rpcError(E.PARAMS, "Escalation assignee not in workspace") };
@@ -3663,9 +4963,10 @@ var Coordinator = class {
3663
4963
  history: [{ ts: now, from: p.from, state: "created", note: `escalated from ${orig.id}` }],
3664
4964
  paused: false
3665
4965
  };
3666
- if (orig.review_required || ws.profiles.some((pr) => pr.startsWith("modes/")) && newTask.mode === "trial") {
4966
+ if (orig.review_required || orig.review !== void 0 || ws.profiles.some((pr) => pr.startsWith("modes/")) && newTask.mode === "trial") {
3667
4967
  newTask.review_required = true;
3668
4968
  }
4969
+ newTask.prior_authors = successorAuthors(orig, p.from);
3669
4970
  ws.tasks.set(newId, newTask);
3670
4971
  orig.state = "escalated";
3671
4972
  orig.updated_at = now;
@@ -3682,8 +4983,8 @@ function memberToDict(m) {
3682
4983
  joined: m.joined
3683
4984
  };
3684
4985
  if (m.display_name) out.display_name = m.display_name;
3685
- if (m.capabilities) out.capabilities = m.capabilities;
3686
- if (m.scopes) out.scopes = m.scopes;
4986
+ if (m.capabilities && Object.keys(m.capabilities).length) out.capabilities = m.capabilities;
4987
+ if (m.scopes?.length) out.scopes = m.scopes;
3687
4988
  if (m.keys.length) {
3688
4989
  out.jwks = { keys: m.keys.filter((k) => !k.revoked_at && !k.valid_until).map((k) => k.jwk) };
3689
4990
  out.key_history = m.keys;
@@ -3695,7 +4996,6 @@ function memberToDict(m) {
3695
4996
  }
3696
4997
 
3697
4998
  // src/policy.ts
3698
- var CRIT2 = { low: 0, medium: 1, high: 2, critical: 3 };
3699
4999
  function makeDefaultPolicy(seniorTarget) {
3700
5000
  const routingPolicy = (task, candidates) => {
3701
5001
  const eligible = candidates;
@@ -3713,15 +5013,8 @@ function makeDefaultPolicy(seniorTarget) {
3713
5013
  };
3714
5014
  };
3715
5015
  const reviewDepthPolicy = (_task, hints) => {
3716
- const crit = hints.criticality ?? "medium";
3717
- let conf = 1;
3718
- try {
3719
- conf = Number(hints.confidence ?? 1);
3720
- } catch {
3721
- conf = 1;
3722
- }
3723
- if (isNaN(conf)) conf = 1;
3724
- const r = CRIT2[crit] ?? 1;
5016
+ const conf = confidenceOf(hints);
5017
+ const r = criticalityOf(hints);
3725
5018
  if (r >= 3) return {
3726
5019
  depth: "full",
3727
5020
  rationale: { policy_id: "default", summary: "criticality=critical: full review" }
@@ -3746,15 +5039,8 @@ function makeDefaultPolicy(seniorTarget) {
3746
5039
  };
3747
5040
  };
3748
5041
  const escalationPolicy = (_task, hints) => {
3749
- const crit = hints.criticality ?? "medium";
3750
- let conf = 1;
3751
- try {
3752
- conf = Number(hints.confidence ?? 1);
3753
- } catch {
3754
- conf = 1;
3755
- }
3756
- if (isNaN(conf)) conf = 1;
3757
- const r = CRIT2[crit] ?? 1;
5042
+ const conf = confidenceOf(hints);
5043
+ const r = criticalityOf(hints);
3758
5044
  const escalate = r >= 3 || r >= 2 && conf < 0.6;
3759
5045
  return {
3760
5046
  escalate,
@@ -3774,6 +5060,19 @@ function wrapMcpToolCall(coord, workspace, options) {
3774
5060
  if (!workspace) throw new Error("workspace is required");
3775
5061
  if (!options.caller) throw new Error("caller is required");
3776
5062
  if (!options.tool) throw new Error("tool is required");
5063
+ if (options.reviewRequired) {
5064
+ const described = coord.dispatch({
5065
+ jsonrpc: "2.0",
5066
+ id: `wrap-${options.tool}-describe`,
5067
+ method: "workspace.describe",
5068
+ params: { workspace, from: options.caller }
5069
+ });
5070
+ if (described.error) throw new Error(`workspace.describe failed: ${JSON.stringify(described.error)}`);
5071
+ const members = described.result.members;
5072
+ if (!members.some((m) => m.type === "human" && m.uri !== options.caller)) {
5073
+ throw new Error("review_required needs a human member other than the caller to review the result; nothing was recorded");
5074
+ }
5075
+ }
3777
5076
  const inputHash = contentHash(options.args);
3778
5077
  const outputHash = contentHash(options.result);
3779
5078
  const kind = options.taskKind ?? `mcp_call:${options.tool}`;
@@ -3792,6 +5091,7 @@ function wrapMcpToolCall(coord, workspace, options) {
3792
5091
  input: options.args
3793
5092
  };
3794
5093
  if (options.routingHints) createParams.routing_hints = { ...options.routingHints };
5094
+ if (options.reviewRequired) createParams.review_required = true;
3795
5095
  const createResp = coord.dispatch({
3796
5096
  jsonrpc: "2.0",
3797
5097
  id: `wrap-${options.tool}-create`,
@@ -3821,7 +5121,7 @@ function wrapMcpToolCall(coord, workspace, options) {
3821
5121
  method: "task.complete",
3822
5122
  params: completeParams
3823
5123
  });
3824
- if (completeResp.error) throw new Error(`task.complete failed: ${JSON.stringify(completeResp.error)}`);
5124
+ if (completeResp.error) throw new Error(`task.complete failed for ${taskId}: ${JSON.stringify(completeResp.error)}`);
3825
5125
  return { task_id: taskId, input_hash: inputHash, output_hash: outputHash };
3826
5126
  }
3827
5127
  function wrapA2aMessageExchange(coord, workspace, options) {
@@ -3885,6 +5185,7 @@ function wrapA2aMessageExchange(coord, workspace, options) {
3885
5185
  export {
3886
5186
  Coordinator,
3887
5187
  CoordinatorError,
5188
+ DEFAULT_MAX_PATCHED_BYTES,
3888
5189
  E,
3889
5190
  IdFactory,
3890
5191
  MemoryStore,