@brightbeamai/chap-coordinator 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,19 +9,11 @@ surface, so the two interoperate on the same JSON-RPC 2.0 wire.
9
9
 
10
10
  ## Install
11
11
 
12
- This package is currently distributed alongside the spec repo rather
13
- than published to npm. To use it directly from source in another
14
- TypeScript project:
15
-
16
12
  ```bash
17
- # From the chap repo root
18
- cd packages/coordinator
19
- npm pack
20
- # In your project
21
- npm install ./brightbeamai-chap-coordinator-*.tgz # the .tgz that npm pack just produced
13
+ npm install @brightbeamai/chap-coordinator
22
14
  ```
23
15
 
24
- Node 18+ required. Zero external runtime dependencies; uses Node's
16
+ Node 20+ required. Zero external runtime dependencies; uses Node's
25
17
  built-in `crypto` for Ed25519 and JCS.
26
18
 
27
19
  ## Companion packages
@@ -33,7 +25,8 @@ A2A ecosystems:
33
25
  - [`@brightbeamai/chap-coordinator-mcp`](https://github.com/BrightbeamAI/chap/tree/main/packages/coordinator-mcp) wraps a Coordinator
34
26
  as an MCP server. Every CHAP method becomes an MCP tool named
35
27
  `chap.<method>`. Reference stdio server at
36
- `reference/mcp-server-ts/`. Spec target: MCP 2025-11-25.
28
+ `reference/mcp-server-ts/`. Spec target: MCP 2026-07-28, serving
29
+ 2025-11-25 clients as well.
37
30
  - [`@brightbeamai/chap-coordinator-a2a`](https://github.com/BrightbeamAI/chap/tree/main/packages/coordinator-a2a) wraps a Coordinator
38
31
  as an A2A agent. Every CHAP method becomes an `AgentSkill` on the
39
32
  Agent Card. Reference Express server at `reference/a2a-server-ts/`.
@@ -247,7 +240,7 @@ statements so the caller can submit out-of-band.
247
240
  npm test
248
241
  ```
249
242
 
250
- 72 tests as of this release, covering Core, every profile, JCS and
243
+ The suite covers Core, every profile, JCS and
251
244
  Ed25519 conformance vectors, signed-envelope verification, OIDC/VC
252
245
  binding, the inward wrap helpers, and an end-to-end composition test
253
246
  exercising every method handler in one workspace.
@@ -74,6 +74,7 @@ interface CoordinatorOptions {
74
74
  enableChain?: boolean;
75
75
  requireSignatures?: boolean;
76
76
  enforceStepUp?: boolean;
77
+ requireReadMembership?: boolean;
77
78
  onAudit?: AuditListener;
78
79
  onAutoEscalate?: (task: Task, to: ParticipantUri) => void;
79
80
  verifyOidcToken?: TokenVerifier;
@@ -74,6 +74,7 @@ interface CoordinatorOptions {
74
74
  enableChain?: boolean;
75
75
  requireSignatures?: boolean;
76
76
  enforceStepUp?: boolean;
77
+ requireReadMembership?: boolean;
77
78
  onAudit?: AuditListener;
78
79
  onAutoEscalate?: (task: Task, to: ParticipantUri) => void;
79
80
  verifyOidcToken?: TokenVerifier;
package/dist/api.d.cts CHANGED
@@ -1,4 +1,4 @@
1
1
  import './methods.cjs';
2
- export { b as CoordinatorApi, c as CoordinatorError, g as call, m as makeApi } from './api-C3nkEk-B.cjs';
2
+ export { b as CoordinatorApi, c as CoordinatorError, g as call, m as makeApi } from './api-DE78DA0o.cjs';
3
3
  import './types.cjs';
4
4
  import './storage/store.cjs';
package/dist/api.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import './methods.js';
2
- export { b as CoordinatorApi, c as CoordinatorError, g as call, m as makeApi } from './api-CmSvlIqO.js';
2
+ export { b as CoordinatorApi, c as CoordinatorError, g as call, m as makeApi } from './api-CPEk0V6i.js';
3
3
  import './types.js';
4
4
  import './storage/store.js';
package/dist/index.cjs CHANGED
@@ -210,12 +210,7 @@ var IdFactory = class {
210
210
  return this.counter * 0x9E3779B97F4A7C15n & (1n << 80n) - 1n;
211
211
  }
212
212
  const buf = new Uint8Array(10);
213
- if (typeof crypto !== "undefined" && crypto.getRandomValues) {
214
- crypto.getRandomValues(buf);
215
- } else {
216
- const nodeCrypto = require("crypto");
217
- buf.set(nodeCrypto.randomBytes(10));
218
- }
213
+ crypto.getRandomValues(buf);
219
214
  let n = 0n;
220
215
  for (const b of buf) n = n << 8n | BigInt(b);
221
216
  return n;
@@ -1558,6 +1553,16 @@ function registerSecuritySigned(coord) {
1558
1553
  for (const f of ["target_uri", "kid"]) {
1559
1554
  if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
1560
1555
  }
1556
+ const sender = p.from;
1557
+ if (sender !== p.target_uri) {
1558
+ const caller = sender ? ws.members.get(sender) : void 0;
1559
+ if (!caller || caller.role !== "admin") {
1560
+ return { error: rpcError(
1561
+ E.NOT_AUTHORISED,
1562
+ "Revoking another member's key requires the admin role"
1563
+ ) };
1564
+ }
1565
+ }
1561
1566
  const member = ws.members.get(p.target_uri);
1562
1567
  if (!member) return { error: rpcError(E.SIG_KEY_NOT_FOUND, `Unknown target: ${p.target_uri}`) };
1563
1568
  const key = member.keys.find((k) => k.kid === p.kid);
@@ -1649,7 +1654,8 @@ function registerWhisper(coord) {
1649
1654
  prompt.answer_option = answerOption;
1650
1655
  prompt.answer_text = answerText;
1651
1656
  prompt.comment = p.comment;
1652
- return { result: { answered: true, whisper_id: prompt.id } };
1657
+ p.task_id = prompt.task_id;
1658
+ return { result: { answered: true, whisper_id: prompt.id, task_id: prompt.task_id } };
1653
1659
  });
1654
1660
  coord.checkWhisperLapses = (workspaceId, now) => {
1655
1661
  const ws = coord.workspaces.get(workspaceId);
@@ -1673,6 +1679,11 @@ function registerWhisper(coord) {
1673
1679
  ts: coord.now(),
1674
1680
  kind: "whisper_lapsed",
1675
1681
  whisper_id: prompt.id,
1682
+ // Carry the task so a task-filtered audit.read surfaces the
1683
+ // lapse. A lapsed whisper applies its default with no human
1684
+ // input, so it is the case most in need of being visible on
1685
+ // the task's thread.
1686
+ task_id: prompt.task_id,
1676
1687
  default_applied: prompt.default_if_lapsed
1677
1688
  }
1678
1689
  };
@@ -1874,6 +1885,7 @@ var Coordinator = class {
1874
1885
  mode_ceiling: ws.mode_ceiling,
1875
1886
  routing_policy_uri: ws.routing_policy_uri,
1876
1887
  step_up_window_sec: ws.step_up_window_sec,
1888
+ min_acr: ws.min_acr,
1877
1889
  members: Array.from(ws.members.values()),
1878
1890
  tasks: Array.from(ws.tasks.values()),
1879
1891
  overrides: Array.from(ws.overrides.values()),
@@ -1882,6 +1894,7 @@ var Coordinator = class {
1882
1894
  handoffs: Array.from(ws.handoffs.values()),
1883
1895
  snapshots: Array.from(ws.snapshots.values()),
1884
1896
  route_decisions: Array.from(ws.route_decisions.values()),
1897
+ idempotency_keys: ws.idempotency_keys,
1885
1898
  audit: ws.audit,
1886
1899
  chain_head: ws.chain_head,
1887
1900
  chain_enabled: ws.chain_enabled
@@ -1904,6 +1917,7 @@ var Coordinator = class {
1904
1917
  mode_ceiling: w.mode_ceiling ?? "production",
1905
1918
  routing_policy_uri: w.routing_policy_uri,
1906
1919
  step_up_window_sec: w.step_up_window_sec ?? 300,
1920
+ min_acr: w.min_acr,
1907
1921
  members: /* @__PURE__ */ new Map(),
1908
1922
  tasks: /* @__PURE__ */ new Map(),
1909
1923
  overrides: /* @__PURE__ */ new Map(),
@@ -1912,6 +1926,7 @@ var Coordinator = class {
1912
1926
  handoffs: /* @__PURE__ */ new Map(),
1913
1927
  snapshots: /* @__PURE__ */ new Map(),
1914
1928
  route_decisions: /* @__PURE__ */ new Map(),
1929
+ idempotency_keys: w.idempotency_keys ?? {},
1915
1930
  audit: w.audit ?? [],
1916
1931
  chain_head: w.chain_head,
1917
1932
  chain_enabled: !!w.chain_enabled
@@ -1950,6 +1965,16 @@ var Coordinator = class {
1950
1965
  if (this.options.requireSignatures && method !== "participant.join" && method !== "workspace.create") {
1951
1966
  const sigErr = this.verifySignature(envelope);
1952
1967
  if (sigErr) return reply(envelope, { error: sigErr });
1968
+ if (method === "participant.rotate_key") {
1969
+ const signingKid = envelope.sig.split(":")[1];
1970
+ const oldKid = params.old_kid;
1971
+ if (oldKid !== void 0 && signingKid !== oldKid) {
1972
+ return reply(envelope, { error: rpcError(
1973
+ E.SIG_ROTATION_KEY_MISMATCH,
1974
+ "Rotation must be signed with the old key"
1975
+ ) });
1976
+ }
1977
+ }
1953
1978
  }
1954
1979
  if (this.options.enforceStepUp && PRIVILEGED_METHODS.has(method)) {
1955
1980
  const stale = this.checkStepUp(params);
@@ -1980,6 +2005,16 @@ var Coordinator = class {
1980
2005
  }
1981
2006
  }
1982
2007
  }
2008
+ if (method === "task.create") {
2009
+ const wsId2 = params.workspace;
2010
+ const ws = typeof wsId2 === "string" ? this.workspaces.get(wsId2) : void 0;
2011
+ const key = params.idempotency_key;
2012
+ if (ws && typeof key === "string") {
2013
+ const existingId = ws.idempotency_keys[key];
2014
+ const task = existingId ? ws.tasks.get(existingId) : void 0;
2015
+ if (task) return reply(envelope, { result: { task_id: existingId, state: task.state } });
2016
+ }
2017
+ }
1983
2018
  const handler = this.handlers.get(method);
1984
2019
  if (!handler) {
1985
2020
  return reply(envelope, { error: rpcError(E.METHOD, `Unknown method: ${method}`) });
@@ -2093,7 +2128,13 @@ var Coordinator = class {
2093
2128
  const ws = this.workspaces.get(wsId);
2094
2129
  if (!ws) return null;
2095
2130
  const member = ws.members.get(sender);
2096
- if (!member || member.oidc_auth_time === void 0) return null;
2131
+ if (!member) return null;
2132
+ if (member.type !== "human" && member.oidc_sub === void 0) return null;
2133
+ if (member.oidc_auth_time === void 0) {
2134
+ return rpcError(E.OIDC_STEP_UP_REQUIRED, "Step-up authentication required", {
2135
+ window_sec: ws.step_up_window_sec
2136
+ });
2137
+ }
2097
2138
  const nowSec = Math.floor(Date.now() / 1e3);
2098
2139
  const age = nowSec - member.oidc_auth_time;
2099
2140
  if (age > ws.step_up_window_sec) {
@@ -2102,6 +2143,12 @@ var Coordinator = class {
2102
2143
  age_sec: age
2103
2144
  });
2104
2145
  }
2146
+ if (ws.min_acr !== void 0 && member.oidc_acr !== ws.min_acr) {
2147
+ return rpcError(E.OIDC_STEP_UP_REQUIRED, "Step-up required: acr below the workspace minimum", {
2148
+ required_acr: ws.min_acr,
2149
+ acr: member.oidc_acr
2150
+ });
2151
+ }
2105
2152
  return null;
2106
2153
  }
2107
2154
  // ==========================================================
@@ -2151,6 +2198,7 @@ var Coordinator = class {
2151
2198
  mode_ceiling: p.mode_ceiling ?? "production",
2152
2199
  routing_policy_uri: p.routing_policy_uri,
2153
2200
  step_up_window_sec: typeof p.step_up_window_sec === "number" ? p.step_up_window_sec : 300,
2201
+ min_acr: p.min_acr,
2154
2202
  members: /* @__PURE__ */ new Map(),
2155
2203
  tasks: /* @__PURE__ */ new Map(),
2156
2204
  overrides: /* @__PURE__ */ new Map(),
@@ -2159,6 +2207,7 @@ var Coordinator = class {
2159
2207
  handoffs: /* @__PURE__ */ new Map(),
2160
2208
  snapshots: /* @__PURE__ */ new Map(),
2161
2209
  route_decisions: /* @__PURE__ */ new Map(),
2210
+ idempotency_keys: {},
2162
2211
  audit: [],
2163
2212
  chain_enabled: chainEnabled,
2164
2213
  chain_head: chainEnabled ? ZERO_HASH : void 0
@@ -2171,6 +2220,10 @@ var Coordinator = class {
2171
2220
  if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
2172
2221
  const ws = this.workspaces.get(p.workspace);
2173
2222
  if (!ws) return { error: rpcError(E.PARAMS, `Unknown workspace: ${p.workspace}`) };
2223
+ if (this.options.requireReadMembership) {
2224
+ const notMember = this.requireMember(ws, p.from);
2225
+ if (notMember) return notMember;
2226
+ }
2174
2227
  return { result: {
2175
2228
  id: ws.id,
2176
2229
  created: ws.created,
@@ -2188,10 +2241,15 @@ var Coordinator = class {
2188
2241
  } };
2189
2242
  }
2190
2243
  opWorkspaceSetProfiles(p) {
2191
- const miss = missing(p, ["workspace", "profiles"]);
2244
+ const miss = missing(p, ["workspace", "from", "profiles"]);
2192
2245
  if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
2193
2246
  const ws = this.workspaces.get(p.workspace);
2194
2247
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2248
+ const notMember = this.requireMember(ws, p.from);
2249
+ if (notMember) return notMember;
2250
+ if (ws.members.get(p.from).role !== "admin") {
2251
+ return { error: rpcError(E.NOT_AUTHORISED, "workspace.set_profiles requires the admin role") };
2252
+ }
2195
2253
  const newProfiles = [...p.profiles];
2196
2254
  if (!newProfiles.some((pr) => pr.startsWith("core/"))) newProfiles.push("core/1.0");
2197
2255
  ws.profiles = newProfiles;
@@ -2229,6 +2287,7 @@ var Coordinator = class {
2229
2287
  member.oidc_sub = claims.sub;
2230
2288
  const at = claims.auth_time;
2231
2289
  if (typeof at === "number") member.oidc_auth_time = at;
2290
+ member.oidc_acr = claims.acr;
2232
2291
  const cnf = claims.cnf;
2233
2292
  const cnfJwk = cnf?.jwk;
2234
2293
  if (cnfJwk && typeof cnfJwk.kid === "string") {
@@ -2257,6 +2316,7 @@ var Coordinator = class {
2257
2316
  if (existing) {
2258
2317
  if (member.oidc_sub !== void 0) existing.oidc_sub = member.oidc_sub;
2259
2318
  if (member.oidc_auth_time !== void 0) existing.oidc_auth_time = member.oidc_auth_time;
2319
+ if (member.oidc_acr !== void 0) existing.oidc_acr = member.oidc_acr;
2260
2320
  if (member.vc_holder !== void 0) existing.vc_holder = member.vc_holder;
2261
2321
  for (const k of attested) {
2262
2322
  if (!existing.keys.some((x) => x.kid === k.kid)) existing.keys.push(k);
@@ -2277,6 +2337,8 @@ var Coordinator = class {
2277
2337
  if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
2278
2338
  const ws = this.workspaces.get(p.workspace);
2279
2339
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2340
+ const notMember = this.requireMember(ws, p.from);
2341
+ if (notMember) return notMember;
2280
2342
  const assignee = p.assignee || p.to;
2281
2343
  if (!assignee || !ws.members.has(assignee)) {
2282
2344
  return { error: rpcError(E.PARAMS, "Assignee not in workspace") };
@@ -2311,11 +2373,14 @@ var Coordinator = class {
2311
2373
  if (task.mode === "trial") task.review_required = true;
2312
2374
  else if ("review_required" in p) task.review_required = !!p.review_required;
2313
2375
  ws.tasks.set(taskId, task);
2376
+ if (typeof p.idempotency_key === "string") ws.idempotency_keys[p.idempotency_key] = taskId;
2314
2377
  return { result: { task_id: taskId, state: "created" } };
2315
2378
  }
2316
2379
  opTaskUpdate(p) {
2317
2380
  const ws = this.workspaces.get(p.workspace);
2318
2381
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2382
+ const notMember = this.requireMember(ws, p.from);
2383
+ if (notMember) return notMember;
2319
2384
  const task = ws.tasks.get(p.task_id);
2320
2385
  if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
2321
2386
  const newState = p.state;
@@ -2358,6 +2423,10 @@ var Coordinator = class {
2358
2423
  opAuditRead(p) {
2359
2424
  const ws = this.workspaces.get(p.workspace);
2360
2425
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2426
+ if (this.options.requireReadMembership) {
2427
+ const notMember = this.requireMember(ws, p.from);
2428
+ if (notMember) return notMember;
2429
+ }
2361
2430
  const range = p.range ?? {};
2362
2431
  const filter = p.filter ?? {};
2363
2432
  const fromSeq = range.from_seq ?? 0;