@brightbeamai/chap-coordinator 0.2.8 → 0.2.10
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 +11 -13
- package/dist/{api-C3nkEk-B.d.cts → api-CcT_8H5j.d.cts} +14 -0
- package/dist/{api-CmSvlIqO.d.ts → api-Cvkhcalw.d.ts} +14 -0
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/index.cjs +169 -12
- 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 +169 -19
- package/dist/index.js.map +1 -1
- package/dist/jsonrpc.cjs +2 -0
- package/dist/jsonrpc.cjs.map +1 -1
- package/dist/jsonrpc.d.cts +2 -0
- package/dist/jsonrpc.d.ts +2 -0
- package/dist/jsonrpc.js +2 -0
- package/dist/jsonrpc.js.map +1 -1
- package/dist/methods.cjs.map +1 -1
- package/dist/methods.d.cts +23 -2
- package/dist/methods.d.ts +23 -2
- package/dist/storage/sqlite.cjs +5 -2
- package/dist/storage/sqlite.cjs.map +1 -1
- package/dist/storage/sqlite.js +4 -9
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/storage/store.cjs.map +1 -1
- package/dist/storage/store.d.cts +26 -1
- package/dist/storage/store.d.ts +26 -1
- package/dist/storage/store.js.map +1 -1
- 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 +79 -27
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as CoordinatorOptions, a as Coordinator } from './api-
|
|
2
|
-
export { b as CoordinatorApi, c as CoordinatorError, d as CredentialVerifier, E as EscalationPolicyFn, H as Handler, I as IdFactory, P as PRIVILEGED_METHODS, R as ReviewDepthPolicyFn, e as RoutingPolicyFn, S as ScittReceiptVerifier, f as ScittSubmitter, T as TokenVerifier, g as call, m as makeApi } from './api-
|
|
1
|
+
import { C as CoordinatorOptions, a as Coordinator } from './api-CcT_8H5j.cjs';
|
|
2
|
+
export { b as CoordinatorApi, c as CoordinatorError, d as CredentialVerifier, E as EscalationPolicyFn, H as Handler, I as IdFactory, P as PRIVILEGED_METHODS, R as ReviewDepthPolicyFn, e as RoutingPolicyFn, S as ScittReceiptVerifier, f as ScittSubmitter, T as TokenVerifier, g as call, m as makeApi } from './api-CcT_8H5j.cjs';
|
|
3
3
|
export { MethodName, MethodParams, MethodResult, MethodTable } from './methods.cjs';
|
|
4
4
|
export { MemoryStore, Store, WorkspaceRecord } from './storage/store.cjs';
|
|
5
5
|
export { ArtefactId, ArtefactRoutingHints, AuditEntry, AuditListener, Deliberation, DeliberationVote, Envelope, Handoff, HandoffTaskItem, JsonPatchOp, Jwk, KeyRecord, Member, Mode, OverrideArtefact, ParticipantType, ParticipantUri, PauseScope, ReviewDecision, RouteDecisionArtefact, SnapshotArtefact, Task, TaskId, TaskRoutingHints, TaskState, WhisperPrompt, Workspace, WorkspaceId, WorkspaceState, modeLE } from './types.cjs';
|
|
@@ -107,7 +107,7 @@ declare function makeDefaultPolicy(seniorTarget: string): Pick<CoordinatorOption
|
|
|
107
107
|
* happened outside CHAP, wrap it so the audit log carries a faithful,
|
|
108
108
|
* citable record.
|
|
109
109
|
*
|
|
110
|
-
* Spec target: CHAP 0.2. MCP
|
|
110
|
+
* Spec target: CHAP 0.2. MCP 2026-07-28. A2A 1.0.
|
|
111
111
|
*/
|
|
112
112
|
|
|
113
113
|
interface WrapMcpToolCallOptions {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as CoordinatorOptions, a as Coordinator } from './api-
|
|
2
|
-
export { b as CoordinatorApi, c as CoordinatorError, d as CredentialVerifier, E as EscalationPolicyFn, H as Handler, I as IdFactory, P as PRIVILEGED_METHODS, R as ReviewDepthPolicyFn, e as RoutingPolicyFn, S as ScittReceiptVerifier, f as ScittSubmitter, T as TokenVerifier, g as call, m as makeApi } from './api-
|
|
1
|
+
import { C as CoordinatorOptions, a as Coordinator } from './api-Cvkhcalw.js';
|
|
2
|
+
export { b as CoordinatorApi, c as CoordinatorError, d as CredentialVerifier, E as EscalationPolicyFn, H as Handler, I as IdFactory, P as PRIVILEGED_METHODS, R as ReviewDepthPolicyFn, e as RoutingPolicyFn, S as ScittReceiptVerifier, f as ScittSubmitter, T as TokenVerifier, g as call, m as makeApi } from './api-Cvkhcalw.js';
|
|
3
3
|
export { MethodName, MethodParams, MethodResult, MethodTable } from './methods.js';
|
|
4
4
|
export { MemoryStore, Store, WorkspaceRecord } from './storage/store.js';
|
|
5
5
|
export { ArtefactId, ArtefactRoutingHints, AuditEntry, AuditListener, Deliberation, DeliberationVote, Envelope, Handoff, HandoffTaskItem, JsonPatchOp, Jwk, KeyRecord, Member, Mode, OverrideArtefact, ParticipantType, ParticipantUri, PauseScope, ReviewDecision, RouteDecisionArtefact, SnapshotArtefact, Task, TaskId, TaskRoutingHints, TaskState, WhisperPrompt, Workspace, WorkspaceId, WorkspaceState, modeLE } from './types.js';
|
|
@@ -107,7 +107,7 @@ declare function makeDefaultPolicy(seniorTarget: string): Pick<CoordinatorOption
|
|
|
107
107
|
* happened outside CHAP, wrap it so the audit log carries a faithful,
|
|
108
108
|
* citable record.
|
|
109
109
|
*
|
|
110
|
-
* Spec target: CHAP 0.2. MCP
|
|
110
|
+
* Spec target: CHAP 0.2. MCP 2026-07-28. A2A 1.0.
|
|
111
111
|
*/
|
|
112
112
|
|
|
113
113
|
interface WrapMcpToolCallOptions {
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
1
|
// src/canonical.ts
|
|
9
2
|
import { createHash } from "crypto";
|
|
10
3
|
var ZERO_HASH = "sha256:" + "0".repeat(64);
|
|
@@ -169,12 +162,7 @@ var IdFactory = class {
|
|
|
169
162
|
return this.counter * 0x9E3779B97F4A7C15n & (1n << 80n) - 1n;
|
|
170
163
|
}
|
|
171
164
|
const buf = new Uint8Array(10);
|
|
172
|
-
|
|
173
|
-
crypto.getRandomValues(buf);
|
|
174
|
-
} else {
|
|
175
|
-
const nodeCrypto = __require("crypto");
|
|
176
|
-
buf.set(nodeCrypto.randomBytes(10));
|
|
177
|
-
}
|
|
165
|
+
crypto.getRandomValues(buf);
|
|
178
166
|
let n = 0n;
|
|
179
167
|
for (const b of buf) n = n << 8n | BigInt(b);
|
|
180
168
|
return n;
|
|
@@ -223,6 +211,7 @@ var E = {
|
|
|
223
211
|
NOT_AUTHORISED: -32011,
|
|
224
212
|
PATCH_FAILED: -32012,
|
|
225
213
|
REVIEW_LAPSED: -32013,
|
|
214
|
+
REVIEW_ALREADY_OPEN: -32014,
|
|
226
215
|
// whisper/1.0 profile (profiles/whisper.md S6)
|
|
227
216
|
WHISPER_ALREADY_ANSWERED: -32020,
|
|
228
217
|
WHISPER_LAPSED: -32021,
|
|
@@ -249,6 +238,7 @@ var E = {
|
|
|
249
238
|
SIG_KEY_NOT_FOUND: -32071,
|
|
250
239
|
SIG_KEY_REVOKED: -32072,
|
|
251
240
|
SIG_ROTATION_KEY_MISMATCH: -32073,
|
|
241
|
+
SIG_ARTEFACT_DIGEST_MISMATCH: -32074,
|
|
252
242
|
// audit-scitt/1.0 profile (profiles/audit-scitt.md S8)
|
|
253
243
|
SCITT_UNREACHABLE: -32080,
|
|
254
244
|
SCITT_STATEMENT_REJECTED: -32081,
|
|
@@ -716,6 +706,12 @@ function registerAuditScitt(coord) {
|
|
|
716
706
|
if (!(ws.chain_enabled || coord.options.enableChain)) {
|
|
717
707
|
return { error: rpcError(E.PARAMS, "Chain not enabled for this workspace") };
|
|
718
708
|
}
|
|
709
|
+
if (p.from_seq !== void 0 || p.to_seq !== void 0) {
|
|
710
|
+
return { error: rpcError(
|
|
711
|
+
E.PARAMS,
|
|
712
|
+
"Range verification is not implemented; omit from_seq and to_seq to verify the whole log."
|
|
713
|
+
) };
|
|
714
|
+
}
|
|
719
715
|
let start = ws.audit.findIndex((e) => e.prev_hash != null);
|
|
720
716
|
if (start < 0) start = ws.audit.length;
|
|
721
717
|
const errors = [];
|
|
@@ -727,16 +723,35 @@ function registerAuditScitt(coord) {
|
|
|
727
723
|
}
|
|
728
724
|
prev = sha256Hex(Buffer.concat([canonicalize(e.envelope), Buffer.from(expectedPrev, "utf-8")]));
|
|
729
725
|
}
|
|
730
|
-
const storedHead = ws.chain_head
|
|
726
|
+
const storedHead = ws.chain_head === void 0 || ws.chain_head === null ? ZERO_HASH : ws.chain_head;
|
|
731
727
|
if (prev !== storedHead) {
|
|
732
728
|
errors.push("chain_head mismatch: replay does not match stored head");
|
|
733
729
|
}
|
|
734
730
|
if (errors.length) {
|
|
735
731
|
return { error: rpcError(E.PARAMS, errors.join("; ")) };
|
|
736
732
|
}
|
|
733
|
+
const entriesTotal = ws.audit.length;
|
|
734
|
+
const entriesChecked = entriesTotal - start;
|
|
735
|
+
const entriesUnchecked = start;
|
|
736
|
+
if (entriesUnchecked > 0) {
|
|
737
|
+
return { result: {
|
|
738
|
+
status: "not_evaluated",
|
|
739
|
+
ok: false,
|
|
740
|
+
reason: "unchained_prefix",
|
|
741
|
+
entries_total: entriesTotal,
|
|
742
|
+
entries_checked: entriesChecked,
|
|
743
|
+
entries_unchecked: entriesUnchecked,
|
|
744
|
+
checked_from_seq: entriesChecked > 0 ? ws.audit[start].seq : null,
|
|
745
|
+
chain_head: storedHead
|
|
746
|
+
} };
|
|
747
|
+
}
|
|
737
748
|
return { result: {
|
|
749
|
+
status: "verified",
|
|
738
750
|
ok: true,
|
|
739
|
-
|
|
751
|
+
entries_total: entriesTotal,
|
|
752
|
+
entries_checked: entriesChecked,
|
|
753
|
+
entries_unchecked: 0,
|
|
754
|
+
checked_from_seq: entriesChecked > 0 ? ws.audit[start].seq : null,
|
|
740
755
|
chain_head: storedHead
|
|
741
756
|
} };
|
|
742
757
|
});
|
|
@@ -1517,6 +1532,16 @@ function registerSecuritySigned(coord) {
|
|
|
1517
1532
|
for (const f of ["target_uri", "kid"]) {
|
|
1518
1533
|
if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
|
|
1519
1534
|
}
|
|
1535
|
+
const sender = p.from;
|
|
1536
|
+
if (sender !== p.target_uri) {
|
|
1537
|
+
const caller = sender ? ws.members.get(sender) : void 0;
|
|
1538
|
+
if (!caller || caller.role !== "admin") {
|
|
1539
|
+
return { error: rpcError(
|
|
1540
|
+
E.NOT_AUTHORISED,
|
|
1541
|
+
"Revoking another member's key requires the admin role"
|
|
1542
|
+
) };
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1520
1545
|
const member = ws.members.get(p.target_uri);
|
|
1521
1546
|
if (!member) return { error: rpcError(E.SIG_KEY_NOT_FOUND, `Unknown target: ${p.target_uri}`) };
|
|
1522
1547
|
const key = member.keys.find((k) => k.kid === p.kid);
|
|
@@ -1608,7 +1633,8 @@ function registerWhisper(coord) {
|
|
|
1608
1633
|
prompt.answer_option = answerOption;
|
|
1609
1634
|
prompt.answer_text = answerText;
|
|
1610
1635
|
prompt.comment = p.comment;
|
|
1611
|
-
|
|
1636
|
+
p.task_id = prompt.task_id;
|
|
1637
|
+
return { result: { answered: true, whisper_id: prompt.id, task_id: prompt.task_id } };
|
|
1612
1638
|
});
|
|
1613
1639
|
coord.checkWhisperLapses = (workspaceId, now) => {
|
|
1614
1640
|
const ws = coord.workspaces.get(workspaceId);
|
|
@@ -1632,6 +1658,11 @@ function registerWhisper(coord) {
|
|
|
1632
1658
|
ts: coord.now(),
|
|
1633
1659
|
kind: "whisper_lapsed",
|
|
1634
1660
|
whisper_id: prompt.id,
|
|
1661
|
+
// Carry the task so a task-filtered audit.read surfaces the
|
|
1662
|
+
// lapse. A lapsed whisper applies its default with no human
|
|
1663
|
+
// input, so it is the case most in need of being visible on
|
|
1664
|
+
// the task's thread.
|
|
1665
|
+
task_id: prompt.task_id,
|
|
1635
1666
|
default_applied: prompt.default_if_lapsed
|
|
1636
1667
|
}
|
|
1637
1668
|
};
|
|
@@ -1833,6 +1864,7 @@ var Coordinator = class {
|
|
|
1833
1864
|
mode_ceiling: ws.mode_ceiling,
|
|
1834
1865
|
routing_policy_uri: ws.routing_policy_uri,
|
|
1835
1866
|
step_up_window_sec: ws.step_up_window_sec,
|
|
1867
|
+
min_acr: ws.min_acr,
|
|
1836
1868
|
members: Array.from(ws.members.values()),
|
|
1837
1869
|
tasks: Array.from(ws.tasks.values()),
|
|
1838
1870
|
overrides: Array.from(ws.overrides.values()),
|
|
@@ -1841,6 +1873,7 @@ var Coordinator = class {
|
|
|
1841
1873
|
handoffs: Array.from(ws.handoffs.values()),
|
|
1842
1874
|
snapshots: Array.from(ws.snapshots.values()),
|
|
1843
1875
|
route_decisions: Array.from(ws.route_decisions.values()),
|
|
1876
|
+
idempotency_keys: ws.idempotency_keys,
|
|
1844
1877
|
audit: ws.audit,
|
|
1845
1878
|
chain_head: ws.chain_head,
|
|
1846
1879
|
chain_enabled: ws.chain_enabled
|
|
@@ -1863,6 +1896,7 @@ var Coordinator = class {
|
|
|
1863
1896
|
mode_ceiling: w.mode_ceiling ?? "production",
|
|
1864
1897
|
routing_policy_uri: w.routing_policy_uri,
|
|
1865
1898
|
step_up_window_sec: w.step_up_window_sec ?? 300,
|
|
1899
|
+
min_acr: w.min_acr,
|
|
1866
1900
|
members: /* @__PURE__ */ new Map(),
|
|
1867
1901
|
tasks: /* @__PURE__ */ new Map(),
|
|
1868
1902
|
overrides: /* @__PURE__ */ new Map(),
|
|
@@ -1871,6 +1905,7 @@ var Coordinator = class {
|
|
|
1871
1905
|
handoffs: /* @__PURE__ */ new Map(),
|
|
1872
1906
|
snapshots: /* @__PURE__ */ new Map(),
|
|
1873
1907
|
route_decisions: /* @__PURE__ */ new Map(),
|
|
1908
|
+
idempotency_keys: w.idempotency_keys ?? {},
|
|
1874
1909
|
audit: w.audit ?? [],
|
|
1875
1910
|
chain_head: w.chain_head,
|
|
1876
1911
|
chain_enabled: !!w.chain_enabled
|
|
@@ -1909,6 +1944,16 @@ var Coordinator = class {
|
|
|
1909
1944
|
if (this.options.requireSignatures && method !== "participant.join" && method !== "workspace.create") {
|
|
1910
1945
|
const sigErr = this.verifySignature(envelope);
|
|
1911
1946
|
if (sigErr) return reply(envelope, { error: sigErr });
|
|
1947
|
+
if (method === "participant.rotate_key") {
|
|
1948
|
+
const signingKid = envelope.sig.split(":")[1];
|
|
1949
|
+
const oldKid = params.old_kid;
|
|
1950
|
+
if (oldKid !== void 0 && signingKid !== oldKid) {
|
|
1951
|
+
return reply(envelope, { error: rpcError(
|
|
1952
|
+
E.SIG_ROTATION_KEY_MISMATCH,
|
|
1953
|
+
"Rotation must be signed with the old key"
|
|
1954
|
+
) });
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1912
1957
|
}
|
|
1913
1958
|
if (this.options.enforceStepUp && PRIVILEGED_METHODS.has(method)) {
|
|
1914
1959
|
const stale = this.checkStepUp(params);
|
|
@@ -1939,6 +1984,16 @@ var Coordinator = class {
|
|
|
1939
1984
|
}
|
|
1940
1985
|
}
|
|
1941
1986
|
}
|
|
1987
|
+
if (method === "task.create") {
|
|
1988
|
+
const wsId2 = params.workspace;
|
|
1989
|
+
const ws = typeof wsId2 === "string" ? this.workspaces.get(wsId2) : void 0;
|
|
1990
|
+
const key = params.idempotency_key;
|
|
1991
|
+
if (ws && typeof key === "string") {
|
|
1992
|
+
const existingId = ws.idempotency_keys[key];
|
|
1993
|
+
const task = existingId ? ws.tasks.get(existingId) : void 0;
|
|
1994
|
+
if (task) return reply(envelope, { result: { task_id: existingId, state: task.state } });
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1942
1997
|
const handler = this.handlers.get(method);
|
|
1943
1998
|
if (!handler) {
|
|
1944
1999
|
return reply(envelope, { error: rpcError(E.METHOD, `Unknown method: ${method}`) });
|
|
@@ -2052,7 +2107,13 @@ var Coordinator = class {
|
|
|
2052
2107
|
const ws = this.workspaces.get(wsId);
|
|
2053
2108
|
if (!ws) return null;
|
|
2054
2109
|
const member = ws.members.get(sender);
|
|
2055
|
-
if (!member
|
|
2110
|
+
if (!member) return null;
|
|
2111
|
+
if (member.type !== "human" && member.oidc_sub === void 0) return null;
|
|
2112
|
+
if (member.oidc_auth_time === void 0) {
|
|
2113
|
+
return rpcError(E.OIDC_STEP_UP_REQUIRED, "Step-up authentication required", {
|
|
2114
|
+
window_sec: ws.step_up_window_sec
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2056
2117
|
const nowSec = Math.floor(Date.now() / 1e3);
|
|
2057
2118
|
const age = nowSec - member.oidc_auth_time;
|
|
2058
2119
|
if (age > ws.step_up_window_sec) {
|
|
@@ -2061,6 +2122,12 @@ var Coordinator = class {
|
|
|
2061
2122
|
age_sec: age
|
|
2062
2123
|
});
|
|
2063
2124
|
}
|
|
2125
|
+
if (ws.min_acr !== void 0 && member.oidc_acr !== ws.min_acr) {
|
|
2126
|
+
return rpcError(E.OIDC_STEP_UP_REQUIRED, "Step-up required: acr below the workspace minimum", {
|
|
2127
|
+
required_acr: ws.min_acr,
|
|
2128
|
+
acr: member.oidc_acr
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2064
2131
|
return null;
|
|
2065
2132
|
}
|
|
2066
2133
|
// ==========================================================
|
|
@@ -2110,6 +2177,7 @@ var Coordinator = class {
|
|
|
2110
2177
|
mode_ceiling: p.mode_ceiling ?? "production",
|
|
2111
2178
|
routing_policy_uri: p.routing_policy_uri,
|
|
2112
2179
|
step_up_window_sec: typeof p.step_up_window_sec === "number" ? p.step_up_window_sec : 300,
|
|
2180
|
+
min_acr: p.min_acr,
|
|
2113
2181
|
members: /* @__PURE__ */ new Map(),
|
|
2114
2182
|
tasks: /* @__PURE__ */ new Map(),
|
|
2115
2183
|
overrides: /* @__PURE__ */ new Map(),
|
|
@@ -2118,6 +2186,7 @@ var Coordinator = class {
|
|
|
2118
2186
|
handoffs: /* @__PURE__ */ new Map(),
|
|
2119
2187
|
snapshots: /* @__PURE__ */ new Map(),
|
|
2120
2188
|
route_decisions: /* @__PURE__ */ new Map(),
|
|
2189
|
+
idempotency_keys: {},
|
|
2121
2190
|
audit: [],
|
|
2122
2191
|
chain_enabled: chainEnabled,
|
|
2123
2192
|
chain_head: chainEnabled ? ZERO_HASH : void 0
|
|
@@ -2130,6 +2199,10 @@ var Coordinator = class {
|
|
|
2130
2199
|
if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
|
|
2131
2200
|
const ws = this.workspaces.get(p.workspace);
|
|
2132
2201
|
if (!ws) return { error: rpcError(E.PARAMS, `Unknown workspace: ${p.workspace}`) };
|
|
2202
|
+
if (this.options.requireReadMembership) {
|
|
2203
|
+
const notMember = this.requireMember(ws, p.from);
|
|
2204
|
+
if (notMember) return notMember;
|
|
2205
|
+
}
|
|
2133
2206
|
return { result: {
|
|
2134
2207
|
id: ws.id,
|
|
2135
2208
|
created: ws.created,
|
|
@@ -2147,10 +2220,15 @@ var Coordinator = class {
|
|
|
2147
2220
|
} };
|
|
2148
2221
|
}
|
|
2149
2222
|
opWorkspaceSetProfiles(p) {
|
|
2150
|
-
const miss = missing(p, ["workspace", "profiles"]);
|
|
2223
|
+
const miss = missing(p, ["workspace", "from", "profiles"]);
|
|
2151
2224
|
if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
|
|
2152
2225
|
const ws = this.workspaces.get(p.workspace);
|
|
2153
2226
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
2227
|
+
const notMember = this.requireMember(ws, p.from);
|
|
2228
|
+
if (notMember) return notMember;
|
|
2229
|
+
if (ws.members.get(p.from).role !== "admin") {
|
|
2230
|
+
return { error: rpcError(E.NOT_AUTHORISED, "workspace.set_profiles requires the admin role") };
|
|
2231
|
+
}
|
|
2154
2232
|
const newProfiles = [...p.profiles];
|
|
2155
2233
|
if (!newProfiles.some((pr) => pr.startsWith("core/"))) newProfiles.push("core/1.0");
|
|
2156
2234
|
ws.profiles = newProfiles;
|
|
@@ -2188,6 +2266,7 @@ var Coordinator = class {
|
|
|
2188
2266
|
member.oidc_sub = claims.sub;
|
|
2189
2267
|
const at = claims.auth_time;
|
|
2190
2268
|
if (typeof at === "number") member.oidc_auth_time = at;
|
|
2269
|
+
member.oidc_acr = claims.acr;
|
|
2191
2270
|
const cnf = claims.cnf;
|
|
2192
2271
|
const cnfJwk = cnf?.jwk;
|
|
2193
2272
|
if (cnfJwk && typeof cnfJwk.kid === "string") {
|
|
@@ -2216,6 +2295,7 @@ var Coordinator = class {
|
|
|
2216
2295
|
if (existing) {
|
|
2217
2296
|
if (member.oidc_sub !== void 0) existing.oidc_sub = member.oidc_sub;
|
|
2218
2297
|
if (member.oidc_auth_time !== void 0) existing.oidc_auth_time = member.oidc_auth_time;
|
|
2298
|
+
if (member.oidc_acr !== void 0) existing.oidc_acr = member.oidc_acr;
|
|
2219
2299
|
if (member.vc_holder !== void 0) existing.vc_holder = member.vc_holder;
|
|
2220
2300
|
for (const k of attested) {
|
|
2221
2301
|
if (!existing.keys.some((x) => x.kid === k.kid)) existing.keys.push(k);
|
|
@@ -2236,6 +2316,8 @@ var Coordinator = class {
|
|
|
2236
2316
|
if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
|
|
2237
2317
|
const ws = this.workspaces.get(p.workspace);
|
|
2238
2318
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
2319
|
+
const notMember = this.requireMember(ws, p.from);
|
|
2320
|
+
if (notMember) return notMember;
|
|
2239
2321
|
const assignee = p.assignee || p.to;
|
|
2240
2322
|
if (!assignee || !ws.members.has(assignee)) {
|
|
2241
2323
|
return { error: rpcError(E.PARAMS, "Assignee not in workspace") };
|
|
@@ -2270,11 +2352,14 @@ var Coordinator = class {
|
|
|
2270
2352
|
if (task.mode === "trial") task.review_required = true;
|
|
2271
2353
|
else if ("review_required" in p) task.review_required = !!p.review_required;
|
|
2272
2354
|
ws.tasks.set(taskId, task);
|
|
2355
|
+
if (typeof p.idempotency_key === "string") ws.idempotency_keys[p.idempotency_key] = taskId;
|
|
2273
2356
|
return { result: { task_id: taskId, state: "created" } };
|
|
2274
2357
|
}
|
|
2275
2358
|
opTaskUpdate(p) {
|
|
2276
2359
|
const ws = this.workspaces.get(p.workspace);
|
|
2277
2360
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
2361
|
+
const notMember = this.requireMember(ws, p.from);
|
|
2362
|
+
if (notMember) return notMember;
|
|
2278
2363
|
const task = ws.tasks.get(p.task_id);
|
|
2279
2364
|
if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
2280
2365
|
const newState = p.state;
|
|
@@ -2317,6 +2402,10 @@ var Coordinator = class {
|
|
|
2317
2402
|
opAuditRead(p) {
|
|
2318
2403
|
const ws = this.workspaces.get(p.workspace);
|
|
2319
2404
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
2405
|
+
if (this.options.requireReadMembership) {
|
|
2406
|
+
const notMember = this.requireMember(ws, p.from);
|
|
2407
|
+
if (notMember) return notMember;
|
|
2408
|
+
}
|
|
2320
2409
|
const range = p.range ?? {};
|
|
2321
2410
|
const filter = p.filter ?? {};
|
|
2322
2411
|
const fromSeq = range.from_seq ?? 0;
|
|
@@ -2353,12 +2442,38 @@ var Coordinator = class {
|
|
|
2353
2442
|
const reviewers = Array.isArray(to) ? to : typeof to === "string" ? [to] : [];
|
|
2354
2443
|
if (!reviewers.length) return { error: rpcError(E.PARAMS, "review.request needs 'to'") };
|
|
2355
2444
|
const now = this.now();
|
|
2445
|
+
const rule = p.rule || "any_one_approves";
|
|
2446
|
+
if (task.state === "review_requested" && task.review) {
|
|
2447
|
+
if (contentHash(task.pending_artefact) !== contentHash(p.artefact)) {
|
|
2448
|
+
return { error: rpcError(
|
|
2449
|
+
E.REVIEW_ALREADY_OPEN,
|
|
2450
|
+
"A review is already open on this task with different content. Decide, abstain, escalate or cancel it before requesting review of a new artefact."
|
|
2451
|
+
) };
|
|
2452
|
+
}
|
|
2453
|
+
if (rule !== task.review.rule) {
|
|
2454
|
+
return { error: rpcError(
|
|
2455
|
+
E.REVIEW_ALREADY_OPEN,
|
|
2456
|
+
`Cannot change the decision rule of an open review (currently ${task.review.rule})`
|
|
2457
|
+
) };
|
|
2458
|
+
}
|
|
2459
|
+
for (const r of reviewers) {
|
|
2460
|
+
if (!task.review.requested_to.includes(r)) task.review.requested_to.push(r);
|
|
2461
|
+
}
|
|
2462
|
+
if (p.deadline !== void 0) task.review.deadline = p.deadline;
|
|
2463
|
+
task.updated_at = now;
|
|
2464
|
+
return { result: {
|
|
2465
|
+
state: "review_requested",
|
|
2466
|
+
review_id: task.id,
|
|
2467
|
+
amended: true,
|
|
2468
|
+
requested_to: [...task.review.requested_to]
|
|
2469
|
+
} };
|
|
2470
|
+
}
|
|
2356
2471
|
task.state = "review_requested";
|
|
2357
2472
|
task.updated_at = now;
|
|
2358
2473
|
task.review = {
|
|
2359
2474
|
requested_at: now,
|
|
2360
2475
|
requested_to: reviewers,
|
|
2361
|
-
rule
|
|
2476
|
+
rule,
|
|
2362
2477
|
deadline: p.deadline,
|
|
2363
2478
|
decisions: []
|
|
2364
2479
|
};
|
|
@@ -2366,6 +2481,37 @@ var Coordinator = class {
|
|
|
2366
2481
|
task.pending_artefact = p.artefact;
|
|
2367
2482
|
return { result: { state: "review_requested", review_id: task.id } };
|
|
2368
2483
|
}
|
|
2484
|
+
/**
|
|
2485
|
+
* Verify an optional `approved_artefact_digest` against the artefact under
|
|
2486
|
+
* review (review/1.0 S3.2).
|
|
2487
|
+
*
|
|
2488
|
+
* The digest sits in params, so it is inside whatever the envelope signature
|
|
2489
|
+
* covers: the decision then attests the content the reviewer saw rather than
|
|
2490
|
+
* a task reference, and a relying party can check it without trusting the
|
|
2491
|
+
* Coordinator that produced it. Absent, behaviour is exactly as before.
|
|
2492
|
+
*
|
|
2493
|
+
* On mismatch nothing is recorded and no state changes, so a client that
|
|
2494
|
+
* computes the wrong digest refuses only its own decision.
|
|
2495
|
+
*/
|
|
2496
|
+
checkArtefactDigest(p, task) {
|
|
2497
|
+
const declared = p.approved_artefact_digest;
|
|
2498
|
+
if (declared === void 0 || declared === null) return null;
|
|
2499
|
+
if (typeof declared !== "string") {
|
|
2500
|
+
return { error: rpcError(E.PARAMS, "approved_artefact_digest must be a string") };
|
|
2501
|
+
}
|
|
2502
|
+
if (task.pending_artefact === void 0) {
|
|
2503
|
+
return { error: rpcError(E.PARAMS, "No artefact under review to bind a digest to") };
|
|
2504
|
+
}
|
|
2505
|
+
const actual = contentHash(task.pending_artefact);
|
|
2506
|
+
if (declared !== actual) {
|
|
2507
|
+
return { error: rpcError(
|
|
2508
|
+
E.SIG_ARTEFACT_DIGEST_MISMATCH,
|
|
2509
|
+
"approved_artefact_digest does not match the artefact under review",
|
|
2510
|
+
{ declared, actual }
|
|
2511
|
+
) };
|
|
2512
|
+
}
|
|
2513
|
+
return null;
|
|
2514
|
+
}
|
|
2369
2515
|
opDecide(p, kind) {
|
|
2370
2516
|
const miss = missing(p, ["workspace", "from", "task_id"]);
|
|
2371
2517
|
if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
|
|
@@ -2382,6 +2528,8 @@ var Coordinator = class {
|
|
|
2382
2528
|
}
|
|
2383
2529
|
const notReviewer = this.requireReviewer(task, p.from);
|
|
2384
2530
|
if (notReviewer) return notReviewer;
|
|
2531
|
+
const digestError = this.checkArtefactDigest(p, task);
|
|
2532
|
+
if (digestError) return digestError;
|
|
2385
2533
|
const now = this.now();
|
|
2386
2534
|
task.review.decisions.push({
|
|
2387
2535
|
reviewer: p.from,
|
|
@@ -2416,6 +2564,8 @@ var Coordinator = class {
|
|
|
2416
2564
|
}
|
|
2417
2565
|
const notReviewer = this.requireReviewer(task, p.from);
|
|
2418
2566
|
if (notReviewer) return notReviewer;
|
|
2567
|
+
const digestError = this.checkArtefactDigest(p, task);
|
|
2568
|
+
if (digestError) return digestError;
|
|
2419
2569
|
const base = task.pending_artefact;
|
|
2420
2570
|
if (base === void 0) return { error: rpcError(E.PARAMS, "No base artefact for override") };
|
|
2421
2571
|
let applied;
|