@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 +5 -12
- package/dist/{api-CmSvlIqO.d.ts → api-CPEk0V6i.d.ts} +1 -0
- package/dist/{api-C3nkEk-B.d.cts → api-DE78DA0o.d.cts} +1 -0
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/index.cjs +78 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +78 -16
- package/dist/index.js.map +1 -1
- package/dist/methods.cjs.map +1 -1
- package/dist/methods.d.cts +1 -0
- package/dist/methods.d.ts +1 -0
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +3 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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;
|