macp-sdk-typescript 0.11.0 → 0.12.0
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/CHANGELOG.md +371 -0
- package/README.md +1 -1
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +2 -1
- package/dist/agent/participant.d.ts +47 -4
- package/dist/agent/participant.js +88 -10
- package/dist/agent/strategies.js +23 -7
- package/dist/agent/transports.d.ts +19 -1
- package/dist/agent/transports.js +108 -66
- package/dist/auth.d.ts +15 -4
- package/dist/auth.js +23 -7
- package/dist/base-session.js +13 -3
- package/dist/client.js +58 -15
- package/dist/commitment-hash.d.ts +33 -3
- package/dist/commitment-hash.js +39 -5
- package/dist/decision.d.ts +1 -0
- package/dist/decision.js +11 -9
- package/dist/envelope.js +9 -1
- package/dist/handoff.d.ts +1 -0
- package/dist/handoff.js +19 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -7
- package/dist/projections/base.d.ts +71 -14
- package/dist/projections/base.js +83 -12
- package/dist/projections/decision.js +23 -3
- package/dist/projections/handoff.js +39 -16
- package/dist/projections/proposal.js +16 -2
- package/dist/projections/quorum.js +13 -0
- package/dist/projections/task.d.ts +27 -4
- package/dist/projections/task.js +44 -13
- package/dist/proposal.d.ts +1 -0
- package/dist/proposal.js +2 -2
- package/dist/proto-registry.d.ts +69 -3
- package/dist/proto-registry.js +83 -5
- package/dist/quorum.d.ts +1 -0
- package/dist/quorum.js +4 -4
- package/dist/task.d.ts +1 -0
- package/dist/task.js +24 -11
- package/dist/types.d.ts +14 -7
- package/dist/validation.d.ts +14 -0
- package/dist/validation.js +39 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
package/dist/proto-registry.js
CHANGED
|
@@ -170,7 +170,13 @@ class ProtoRegistry {
|
|
|
170
170
|
* Decode an `ext.multi_round.v1` `Contribute` payload, accepting both wire
|
|
171
171
|
* formats: legacy JSON (`{"value":"..."}`, still replayed verbatim from
|
|
172
172
|
* pre-proto histories) and canonical protobuf (`ContributePayload`). Both
|
|
173
|
-
* normalize to `{ value
|
|
173
|
+
* normalize to `{ value }`, but only the protobuf path's `value` is always
|
|
174
|
+
* a `string` — the legacy-JSON path passes `parsed.value` through exactly
|
|
175
|
+
* as `JSON.parse` produced it (issue #124: no `String(...)` coercion, no
|
|
176
|
+
* `?? ''` fallback for an absent key), so `value` may be `undefined`,
|
|
177
|
+
* `null`, a number, a boolean, or an object/array for malformed-but-valid
|
|
178
|
+
* legacy JSON. This method's own contract is observational, not a gate —
|
|
179
|
+
* see the `contribute_acceptance` note below.
|
|
174
180
|
*
|
|
175
181
|
* Parse-then-fallback (issue #93), not a first-byte shortcut: JSON is always
|
|
176
182
|
* attempted first, and only a `JSON.parse` failure falls through to
|
|
@@ -178,8 +184,27 @@ class ProtoRegistry {
|
|
|
178
184
|
* not generalize — e.g. legacy JSON with leading whitespace has neither byte
|
|
179
185
|
* first, and used to reach `decodeMessage` and throw. Parity with
|
|
180
186
|
* `macp-sdk-python`'s `_decode_json_first_then_proto`, which has always
|
|
181
|
-
* worked this way.
|
|
182
|
-
*
|
|
187
|
+
* worked this way.
|
|
188
|
+
*
|
|
189
|
+
* **A successful `JSON.parse` is not proof the bytes are legacy JSON**
|
|
190
|
+
* (issue #104 — this method's prior "essentially never also syntactically
|
|
191
|
+
* valid JSON" claim was false, not merely imprecise): the canonical proto
|
|
192
|
+
* tag byte for field 1 (`0x0A`) is itself insignificant JSON whitespace, so
|
|
193
|
+
* for specific value byte-lengths the length varint — or, when the varint
|
|
194
|
+
* is itself whitespace, the value's own leading byte — becomes the first
|
|
195
|
+
* significant character `JSON.parse` sees, and a genuine `ContributePayload`
|
|
196
|
+
* silently misreads as a JSON number/string/object. Verified directly
|
|
197
|
+
* (encode-then-decode sweep, value lengths 1-127, seven value shapes): every
|
|
198
|
+
* one of lengths 9, 10, 13, 32, 34, 45, 48, 49-57, and 123 corrupted before
|
|
199
|
+
* `isCanonicalProto` below, most to total silent data loss
|
|
200
|
+
* (`String(undefined ?? '') === ''`), zero corrupt after.
|
|
201
|
+
*
|
|
202
|
+
* `isCanonicalProto` resolves the ambiguity by tie-break, not by refusing
|
|
203
|
+
* to try JSON first: bytes that parse as JSON *and* are the exact canonical
|
|
204
|
+
* proto encoding are treated as proto. This has a narrow, priced cost on
|
|
205
|
+
* the reverse direction (legacy JSON misread as proto) that it introduces
|
|
206
|
+
* rather than merely fails to close — see `isCanonicalProto`'s own
|
|
207
|
+
* docblock.
|
|
183
208
|
*
|
|
184
209
|
* An empty payload falls through the same path: `JSON.parse('')` throws, so
|
|
185
210
|
* it reaches `decodeMessage` on zero bytes, which yields `{}` (proto3
|
|
@@ -192,10 +217,63 @@ class ProtoRegistry {
|
|
|
192
217
|
decodeMultiRoundContribute(payload) {
|
|
193
218
|
try {
|
|
194
219
|
const parsed = JSON.parse(payload.toString('utf8'));
|
|
195
|
-
|
|
220
|
+
if (!this.isCanonicalProto(MULTI_ROUND_CONTRIBUTE, payload)) {
|
|
221
|
+
return { value: parsed.value };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Not JSON at all — fall through to the protobuf decode below.
|
|
226
|
+
}
|
|
227
|
+
return this.decodeMessage(MULTI_ROUND_CONTRIBUTE, payload);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* True iff `payload` is the exact canonical protobuf encoding of
|
|
231
|
+
* `typeName` — used to break the JSON/proto ambiguity in
|
|
232
|
+
* `decodeMultiRoundContribute` (issue #104): bytes that both parse as JSON
|
|
233
|
+
* *and* round-trip byte-identically through the proto message are treated
|
|
234
|
+
* as proto, not JSON.
|
|
235
|
+
*
|
|
236
|
+
* `macp-sdk-python`'s equivalent (`_is_canonical_proto`) needs an explicit
|
|
237
|
+
* `DiscardUnknownFields()` step before comparing, because Python's protobuf
|
|
238
|
+
* runtime preserves unknown fields verbatim through
|
|
239
|
+
* `ParseFromString`/`SerializeToString` by default — without it, a plain
|
|
240
|
+
* round-trip check misclassified several whitespace-prefixed legacy-JSON
|
|
241
|
+
* payloads as canonical proto. **`protobufjs` needs no equivalent step**:
|
|
242
|
+
* verified directly (a synthetic payload with a declared field-1 string
|
|
243
|
+
* plus an undeclared field 2) that `Type.decode()` silently drops fields
|
|
244
|
+
* not in the schema rather than preserving them, so `Type.encode()` can
|
|
245
|
+
* never reproduce bytes it never captured — a byte-exact round-trip here
|
|
246
|
+
* is already proof the payload is *exactly* the single-field encoding, no
|
|
247
|
+
* unknown fields possible. Confirmed this closes the same
|
|
248
|
+
* whitespace-prefixed false-positive class `DiscardUnknownFields` closes
|
|
249
|
+
* in Python (tab/space-prefixed legacy JSON at several lengths).
|
|
250
|
+
*
|
|
251
|
+
* One narrow, symmetric residual survives regardless — genuinely
|
|
252
|
+
* irreducible, not a gap this check merely fails to close: a payload whose
|
|
253
|
+
* first byte is a literal `0x0A` (`\n`) and whose remainder forms a
|
|
254
|
+
* complete, well-formed proto field-1 string with nothing left over has no
|
|
255
|
+
* unknown field for either implementation to expose a mismatch on, because
|
|
256
|
+
* it *is*, byte for byte, both a legal JSON reading and the canonical
|
|
257
|
+
* proto encoding of some (possibly nonsensical) string. Verified instances
|
|
258
|
+
* (this SDK's own `JSON.stringify`, which — unlike Python's `json.dumps`
|
|
259
|
+
* — omits the space after `:`, shifting the colliding lengths by one byte
|
|
260
|
+
* from `macp-sdk-python`'s pinned cases): `'\n' + JSON.stringify({value:
|
|
261
|
+
* 'z'.repeat(112)})`, and with one extra whitespace byte inserted after the
|
|
262
|
+
* `\n`, `'\r'` at value length 1 and `' '` at value length 20. A real
|
|
263
|
+
* legacy-JSON sender would need to deliberately prefix with a literal
|
|
264
|
+
* newline byte for this to matter — this SDK's own JSON producers never
|
|
265
|
+
* emit one — so the trade (fixing the ordinary, naturally-reachable
|
|
266
|
+
* forward-direction corruption above at the cost of this adversarial,
|
|
267
|
+
* unreachable-by-any-known-encoder reverse case) is the right one.
|
|
268
|
+
*/
|
|
269
|
+
isCanonicalProto(typeName, payload) {
|
|
270
|
+
try {
|
|
271
|
+
const type = this.lookupType(typeName);
|
|
272
|
+
const decoded = type.decode(payload);
|
|
273
|
+
return Buffer.from(type.encode(decoded).finish()).equals(payload);
|
|
196
274
|
}
|
|
197
275
|
catch {
|
|
198
|
-
return
|
|
276
|
+
return false;
|
|
199
277
|
}
|
|
200
278
|
}
|
|
201
279
|
lookupType(typeName) {
|
package/dist/quorum.d.ts
CHANGED
package/dist/quorum.js
CHANGED
|
@@ -60,15 +60,15 @@ class QuorumSession {
|
|
|
60
60
|
mode: constants_1.MODE_QUORUM,
|
|
61
61
|
messageType: 'SessionStart',
|
|
62
62
|
sessionId: this.sessionId,
|
|
63
|
-
sender: this.senderFor(input.sender),
|
|
63
|
+
sender: this.senderFor(input.sender, input.auth),
|
|
64
64
|
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_QUORUM, 'SessionStart', (0, envelope_1.toProtoPayload)(payload)),
|
|
65
65
|
});
|
|
66
|
-
return this.sendAndTrack(envelope, this.auth);
|
|
66
|
+
return this.sendAndTrack(envelope, input.auth ?? this.auth);
|
|
67
67
|
}
|
|
68
68
|
async requestApproval(input) {
|
|
69
69
|
(0, validation_1.validateRequiredField)('requestId', input.requestId);
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// `action`/`summary` are proto3 singular string fields (implicit
|
|
71
|
+
// presence) -- no client-side non-empty check here (issue #124 item 3).
|
|
72
72
|
const envelope = (0, envelope_1.buildEnvelope)({
|
|
73
73
|
mode: constants_1.MODE_QUORUM,
|
|
74
74
|
messageType: 'ApprovalRequest',
|
package/dist/task.d.ts
CHANGED
package/dist/task.js
CHANGED
|
@@ -60,15 +60,16 @@ class TaskSession {
|
|
|
60
60
|
mode: constants_1.MODE_TASK,
|
|
61
61
|
messageType: 'SessionStart',
|
|
62
62
|
sessionId: this.sessionId,
|
|
63
|
-
sender: this.senderFor(input.sender),
|
|
63
|
+
sender: this.senderFor(input.sender, input.auth),
|
|
64
64
|
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_TASK, 'SessionStart', (0, envelope_1.toProtoPayload)(payload)),
|
|
65
65
|
});
|
|
66
|
-
return this.sendAndTrack(envelope, this.auth);
|
|
66
|
+
return this.sendAndTrack(envelope, input.auth ?? this.auth);
|
|
67
67
|
}
|
|
68
68
|
async requestTask(input) {
|
|
69
69
|
(0, validation_1.validateRequiredField)('taskId', input.taskId);
|
|
70
70
|
(0, validation_1.validateRequiredField)('title', input.title);
|
|
71
|
-
(
|
|
71
|
+
// `instructions` is a proto3 singular string field (implicit presence) --
|
|
72
|
+
// no client-side non-empty check here (issue #124 item 3).
|
|
72
73
|
const envelope = (0, envelope_1.buildEnvelope)({
|
|
73
74
|
mode: constants_1.MODE_TASK,
|
|
74
75
|
messageType: 'TaskRequest',
|
|
@@ -80,23 +81,31 @@ class TaskSession {
|
|
|
80
81
|
}
|
|
81
82
|
async acceptTask(input) {
|
|
82
83
|
(0, validation_1.validateRequiredField)('taskId', input.taskId);
|
|
84
|
+
const sender = this.senderFor(input.sender, input.auth);
|
|
85
|
+
// Issue #108.5/4e: default assignee to the resolved sender, matching
|
|
86
|
+
// Python (`task.py:299,323,391,430`). `||` not `??` — an explicit empty
|
|
87
|
+
// string is never a meaningful assignee, and the reported gap includes
|
|
88
|
+
// that case, which `??` would let through unchanged.
|
|
89
|
+
const assignee = input.assignee || sender;
|
|
83
90
|
const envelope = (0, envelope_1.buildEnvelope)({
|
|
84
91
|
mode: constants_1.MODE_TASK,
|
|
85
92
|
messageType: 'TaskAccept',
|
|
86
93
|
sessionId: this.sessionId,
|
|
87
|
-
sender
|
|
88
|
-
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_TASK, 'TaskAccept', (0, envelope_1.toProtoPayload)(input)),
|
|
94
|
+
sender,
|
|
95
|
+
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_TASK, 'TaskAccept', (0, envelope_1.toProtoPayload)({ ...input, assignee })),
|
|
89
96
|
});
|
|
90
97
|
return this.sendAndTrack(envelope, input.auth);
|
|
91
98
|
}
|
|
92
99
|
async rejectTask(input) {
|
|
93
100
|
(0, validation_1.validateRequiredField)('taskId', input.taskId);
|
|
101
|
+
const sender = this.senderFor(input.sender, input.auth);
|
|
102
|
+
const assignee = input.assignee || sender;
|
|
94
103
|
const envelope = (0, envelope_1.buildEnvelope)({
|
|
95
104
|
mode: constants_1.MODE_TASK,
|
|
96
105
|
messageType: 'TaskReject',
|
|
97
106
|
sessionId: this.sessionId,
|
|
98
|
-
sender
|
|
99
|
-
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_TASK, 'TaskReject', (0, envelope_1.toProtoPayload)(input)),
|
|
107
|
+
sender,
|
|
108
|
+
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_TASK, 'TaskReject', (0, envelope_1.toProtoPayload)({ ...input, assignee })),
|
|
100
109
|
});
|
|
101
110
|
return this.sendAndTrack(envelope, input.auth);
|
|
102
111
|
}
|
|
@@ -113,23 +122,27 @@ class TaskSession {
|
|
|
113
122
|
}
|
|
114
123
|
async completeTask(input) {
|
|
115
124
|
(0, validation_1.validateRequiredField)('taskId', input.taskId);
|
|
125
|
+
const sender = this.senderFor(input.sender, input.auth);
|
|
126
|
+
const assignee = input.assignee || sender;
|
|
116
127
|
const envelope = (0, envelope_1.buildEnvelope)({
|
|
117
128
|
mode: constants_1.MODE_TASK,
|
|
118
129
|
messageType: 'TaskComplete',
|
|
119
130
|
sessionId: this.sessionId,
|
|
120
|
-
sender
|
|
121
|
-
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_TASK, 'TaskComplete', (0, envelope_1.toProtoPayload)(input)),
|
|
131
|
+
sender,
|
|
132
|
+
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_TASK, 'TaskComplete', (0, envelope_1.toProtoPayload)({ ...input, assignee })),
|
|
122
133
|
});
|
|
123
134
|
return this.sendAndTrack(envelope, input.auth);
|
|
124
135
|
}
|
|
125
136
|
async failTask(input) {
|
|
126
137
|
(0, validation_1.validateRequiredField)('taskId', input.taskId);
|
|
138
|
+
const sender = this.senderFor(input.sender, input.auth);
|
|
139
|
+
const assignee = input.assignee || sender;
|
|
127
140
|
const envelope = (0, envelope_1.buildEnvelope)({
|
|
128
141
|
mode: constants_1.MODE_TASK,
|
|
129
142
|
messageType: 'TaskFail',
|
|
130
143
|
sessionId: this.sessionId,
|
|
131
|
-
sender
|
|
132
|
-
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_TASK, 'TaskFail', (0, envelope_1.toProtoPayload)(input)),
|
|
144
|
+
sender,
|
|
145
|
+
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_TASK, 'TaskFail', (0, envelope_1.toProtoPayload)({ ...input, assignee })),
|
|
133
146
|
});
|
|
134
147
|
return this.sendAndTrack(envelope, input.auth);
|
|
135
148
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -217,12 +217,14 @@ export interface TaskRequestPayload {
|
|
|
217
217
|
}
|
|
218
218
|
export interface TaskAcceptPayload {
|
|
219
219
|
taskId: string;
|
|
220
|
-
|
|
220
|
+
/** Defaults to the resolved sender when omitted or empty (issue #108.5). */
|
|
221
|
+
assignee?: string;
|
|
221
222
|
reason?: string;
|
|
222
223
|
}
|
|
223
224
|
export interface TaskRejectPayload {
|
|
224
225
|
taskId: string;
|
|
225
|
-
|
|
226
|
+
/** Defaults to the resolved sender when omitted or empty (issue #108.5). */
|
|
227
|
+
assignee?: string;
|
|
226
228
|
reason?: string;
|
|
227
229
|
}
|
|
228
230
|
export interface TaskUpdatePayload {
|
|
@@ -234,13 +236,15 @@ export interface TaskUpdatePayload {
|
|
|
234
236
|
}
|
|
235
237
|
export interface TaskCompletePayload {
|
|
236
238
|
taskId: string;
|
|
237
|
-
|
|
239
|
+
/** Defaults to the resolved sender when omitted or empty (issue #108.5). */
|
|
240
|
+
assignee?: string;
|
|
238
241
|
output?: Buffer;
|
|
239
242
|
summary?: string;
|
|
240
243
|
}
|
|
241
244
|
export interface TaskFailPayload {
|
|
242
245
|
taskId: string;
|
|
243
|
-
|
|
246
|
+
/** Defaults to the resolved sender when omitted or empty (issue #108.5). */
|
|
247
|
+
assignee?: string;
|
|
244
248
|
errorCode?: string;
|
|
245
249
|
reason?: string;
|
|
246
250
|
retryable?: boolean;
|
|
@@ -253,12 +257,14 @@ export interface HandoffOfferPayload {
|
|
|
253
257
|
}
|
|
254
258
|
export interface HandoffContextPayload {
|
|
255
259
|
handoffId: string;
|
|
256
|
-
|
|
260
|
+
/** Defaults to 'application/octet-stream' when omitted (issue #108.6). */
|
|
261
|
+
contentType?: string;
|
|
257
262
|
context?: Buffer;
|
|
258
263
|
}
|
|
259
264
|
export interface HandoffAcceptPayload {
|
|
260
265
|
handoffId: string;
|
|
261
|
-
|
|
266
|
+
/** Defaults to the resolved sender when omitted or empty (issue #108.5). */
|
|
267
|
+
acceptedBy?: string;
|
|
262
268
|
reason?: string;
|
|
263
269
|
/**
|
|
264
270
|
* Runtime-emitted synthetic accepts ONLY (RFC-MACP-0010 §5.1): `true` on the
|
|
@@ -272,7 +278,8 @@ export interface HandoffAcceptPayload {
|
|
|
272
278
|
}
|
|
273
279
|
export interface HandoffDeclinePayload {
|
|
274
280
|
handoffId: string;
|
|
275
|
-
|
|
281
|
+
/** Defaults to the resolved sender when omitted or empty (issue #108.5). */
|
|
282
|
+
declinedBy?: string;
|
|
276
283
|
reason?: string;
|
|
277
284
|
}
|
|
278
285
|
export interface ApprovalRequestPayload {
|
package/dist/validation.d.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate that `sid` is a lowercase UUID v4/v7 or base64url (22+ chars).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the runtime's **no-fall-through** rule (issue #108.1, and
|
|
5
|
+
* `macp-sdk-python`'s `validate_session_id`, `validation.py:33-58`, which
|
|
6
|
+
* this ports verbatim in shape): a string with the *structural* shape of a
|
|
7
|
+
* UUID (36 chars, hyphens at 8-13-18-23, hex-only) is validated strictly as
|
|
8
|
+
* a lowercase v4/v7 UUID — it is never reinterpreted as base64url, which
|
|
9
|
+
* would otherwise let an uppercase, wrong-version, or bad-variant UUID slip
|
|
10
|
+
* through since it also happens to satisfy the base64url charset. A single
|
|
11
|
+
* combined regex cannot express this: the absence of fall-through is itself
|
|
12
|
+
* the rule. Only a string that is not UUID-shaped at all reaches the
|
|
13
|
+
* base64url branch.
|
|
14
|
+
*/
|
|
1
15
|
export declare function validateSessionId(sid: string): void;
|
|
2
16
|
export declare function validateVote(value: string): string;
|
|
3
17
|
export declare function validateRecommendation(value: string): string;
|
package/dist/validation.js
CHANGED
|
@@ -15,12 +15,36 @@ exports.validateRequiredField = validateRequiredField;
|
|
|
15
15
|
exports.validateCommitmentHash = validateCommitmentHash;
|
|
16
16
|
exports.validateSessionStart = validateSessionStart;
|
|
17
17
|
const errors_1 = require("./errors");
|
|
18
|
-
|
|
18
|
+
// A string with the structural shape of a UUID (36 chars, hyphens at
|
|
19
|
+
// 8-13-18-23, hex-only otherwise) — case-insensitive, any version/variant.
|
|
20
|
+
const UUID_SHAPE_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
21
|
+
// Strict lowercase UUID v4/v7 (version nibble 4 or 7, RFC 9562 variant 8/9/a/b).
|
|
22
|
+
const UUID_V4V7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[47][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
19
23
|
const BASE64URL_RE = /^[A-Za-z0-9_-]{22,}$/;
|
|
24
|
+
/**
|
|
25
|
+
* Validate that `sid` is a lowercase UUID v4/v7 or base64url (22+ chars).
|
|
26
|
+
*
|
|
27
|
+
* Mirrors the runtime's **no-fall-through** rule (issue #108.1, and
|
|
28
|
+
* `macp-sdk-python`'s `validate_session_id`, `validation.py:33-58`, which
|
|
29
|
+
* this ports verbatim in shape): a string with the *structural* shape of a
|
|
30
|
+
* UUID (36 chars, hyphens at 8-13-18-23, hex-only) is validated strictly as
|
|
31
|
+
* a lowercase v4/v7 UUID — it is never reinterpreted as base64url, which
|
|
32
|
+
* would otherwise let an uppercase, wrong-version, or bad-variant UUID slip
|
|
33
|
+
* through since it also happens to satisfy the base64url charset. A single
|
|
34
|
+
* combined regex cannot express this: the absence of fall-through is itself
|
|
35
|
+
* the rule. Only a string that is not UUID-shaped at all reaches the
|
|
36
|
+
* base64url branch.
|
|
37
|
+
*/
|
|
20
38
|
function validateSessionId(sid) {
|
|
21
|
-
if (
|
|
22
|
-
|
|
39
|
+
if (UUID_SHAPE_RE.test(sid)) {
|
|
40
|
+
if (!UUID_V4V7_RE.test(sid)) {
|
|
41
|
+
throw new errors_1.MacpSessionError(`session_id is UUID-shaped but not a lowercase v4/v7 UUID (no fall-through to base64url), got: ${sid}`);
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
23
44
|
}
|
|
45
|
+
if (BASE64URL_RE.test(sid))
|
|
46
|
+
return;
|
|
47
|
+
throw new errors_1.MacpSessionError(`session_id must be a lowercase UUID v4/v7 or base64url (22+ chars), got: ${sid}`);
|
|
24
48
|
}
|
|
25
49
|
const VALID_VOTES = new Set(['APPROVE', 'REJECT', 'ABSTAIN']);
|
|
26
50
|
function validateVote(value) {
|
|
@@ -39,7 +63,13 @@ function validateRecommendation(value) {
|
|
|
39
63
|
return normalized;
|
|
40
64
|
}
|
|
41
65
|
function validateConfidence(value) {
|
|
42
|
-
|
|
66
|
+
// `Number.isFinite` (matching this file's own `ttlMs`/`maxSuspendMs`
|
|
67
|
+
// idiom) rather than two range comparisons alone: `NaN < 0` and
|
|
68
|
+
// `NaN > 1.0` are both `false`, so a bare range check silently *accepts*
|
|
69
|
+
// `NaN` (issue #108.2) — verified by execution before this fix. `Infinity`
|
|
70
|
+
// was already rejected incidentally (`Infinity > 1.0`); this also rejects
|
|
71
|
+
// it explicitly rather than by accident.
|
|
72
|
+
if (!Number.isFinite(value) || value < 0.0 || value > 1.0) {
|
|
43
73
|
throw new errors_1.MacpSessionError(`confidence must be in [0.0, 1.0], got ${value}`);
|
|
44
74
|
}
|
|
45
75
|
}
|
|
@@ -131,7 +161,11 @@ function validateCommitmentHash(value, field = 'commitmentHash') {
|
|
|
131
161
|
}
|
|
132
162
|
}
|
|
133
163
|
function validateSessionStart(input) {
|
|
134
|
-
|
|
164
|
+
// `intent` is a proto3 singular string field (implicit presence -- an
|
|
165
|
+
// omitted field and an explicit "" serialize identically on the wire), and
|
|
166
|
+
// RFC-MACP-0001 §7.1 states a runtime "MUST NOT reject a SessionStart
|
|
167
|
+
// solely because intent is empty" -- no client-side non-empty check here
|
|
168
|
+
// (issue #124 item 3).
|
|
135
169
|
validateParticipants(input.participants);
|
|
136
170
|
validateTtlMs(input.ttlMs);
|
|
137
171
|
if (input.maxSuspendMs !== undefined)
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "macp-sdk-typescript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "TypeScript SDK for the MACP Rust runtime",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "tsc -p tsconfig.json",
|
|
23
|
-
"check": "tsc -p tsconfig.json --noEmit",
|
|
23
|
+
"check": "tsc -p tsconfig.json --noEmit && npm run check:examples",
|
|
24
|
+
"check:examples": "tsc -p tsconfig.examples.json",
|
|
24
25
|
"lint": "eslint src/",
|
|
25
26
|
"lint:fix": "eslint src/ --fix",
|
|
26
27
|
"format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts' --ignore-unknown",
|