@canonmsg/backend-contracts 2.0.0 → 2.2.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.
@@ -0,0 +1,634 @@
1
+ "use strict";
2
+ /**
3
+ * Canon verb WIRE contract — `canon.verb-wire.v1`.
4
+ *
5
+ * The verb INTENT contract (`canon.verbs.v1`, verbContract.ts) is the
6
+ * plaintext semantic input used locally by tools, MCP, SDK, and adapters. It
7
+ * is NOT the remote wire schema. What crosses the network is this contract:
8
+ * an authenticated public ENVELOPE (the routing/policy metadata the server
9
+ * may read, authorize, and enforce on) plus a BODY that is either plaintext
10
+ * JSON (temporary/plaintext conversations) or MLS ciphertext (encrypted
11
+ * conversations). See docs/design/e2ee-mls.md §1-2.
12
+ *
13
+ * one developer-facing API ≠ one plaintext wire schema
14
+ *
15
+ * Server endpoints (/agent/verbs/*) implement THIS contract with the `json`
16
+ * codec initially; MLS lands later as a codec swap, not an API change. In
17
+ * `json` mode the server may additionally read `body.value` to execute
18
+ * legacy behavior (validation, snapshot building); in `mls` mode the
19
+ * envelope is everything it will ever see.
20
+ *
21
+ * The intent→wire projection (and its inverse, used by the server executor
22
+ * in json mode) is defined here so every binding and the server agree on
23
+ * exactly which intent fields are envelope (server-visible) vs body
24
+ * (content). The split follows the 2026-07-12 plaintext audit: closed
25
+ * enums, ids, deadlines, and turn-protocol keys are envelope; free text,
26
+ * cards, prompts, questions, tool names/summaries, details, diffs, and
27
+ * self-contexts are body.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.CANON_VERB_WIRE_JSON_SCHEMA = exports.VERB_WIRE_ENVELOPE_FIELDS = exports.CANON_VERB_WIRE_SCHEMA_ID = exports.CANON_VERB_WIRE_SCHEMA_VERSION = void 0;
31
+ exports.projectVerbIntentToWire = projectVerbIntentToWire;
32
+ exports.mergeVerbWireToIntent = mergeVerbWireToIntent;
33
+ exports.findVerbWireEnvelopeViolations = findVerbWireEnvelopeViolations;
34
+ const verbContract_js_1 = require("./verbContract.js");
35
+ exports.CANON_VERB_WIRE_SCHEMA_VERSION = 'canon.verb-wire.v1';
36
+ exports.CANON_VERB_WIRE_SCHEMA_ID = 'https://canonmsg.com/schemas/canon.verb-wire.v1.json';
37
+ // ---------------------------------------------------------------------------
38
+ // Which envelope fields each verb uses (documentation + validation aid)
39
+ // ---------------------------------------------------------------------------
40
+ exports.VERB_WIRE_ENVELOPE_FIELDS = {
41
+ send_to: {
42
+ required: [],
43
+ optional: [
44
+ 'conversationId',
45
+ 'targetUserId',
46
+ 'canonContactId',
47
+ 'sourceConversationId',
48
+ 'sessionSelection',
49
+ 'sessionConfig',
50
+ 'idempotencyKey',
51
+ 'replyTo',
52
+ 'replyToPosition',
53
+ 'mentions',
54
+ 'turn',
55
+ ],
56
+ },
57
+ request_input: {
58
+ required: ['conversationId'],
59
+ optional: ['requestId', 'kind', 'sensitive', 'responseUserId', 'expiresAt', 'native', 'turn'],
60
+ },
61
+ request_approval: {
62
+ required: ['conversationId'],
63
+ optional: [
64
+ 'requestId',
65
+ 'mode',
66
+ 'riskLevel',
67
+ 'risk',
68
+ 'category',
69
+ 'allowSessionRule',
70
+ 'responseUserId',
71
+ 'expiresAt',
72
+ 'native',
73
+ 'runtimeId',
74
+ 'turn',
75
+ ],
76
+ },
77
+ check_approval: {
78
+ required: ['requestId'],
79
+ optional: ['conversationId'],
80
+ },
81
+ send_card: {
82
+ required: ['conversationId'],
83
+ optional: ['requestId', 'native', 'runtimeId', 'turn'],
84
+ },
85
+ request_card: {
86
+ required: ['conversationId'],
87
+ optional: ['requestId', 'responseUserId', 'expiresAt', 'native', 'runtimeId', 'turn'],
88
+ },
89
+ share_contact: {
90
+ required: ['conversationId', 'contactUserId'],
91
+ optional: ['idempotencyKey'],
92
+ },
93
+ react: {
94
+ required: ['conversationId', 'messageId'],
95
+ optional: [],
96
+ },
97
+ forward: {
98
+ required: ['sourceConversationId', 'conversationId', 'messageId'],
99
+ optional: [],
100
+ },
101
+ create_group: {
102
+ required: ['memberIds'],
103
+ optional: [],
104
+ },
105
+ add_member: {
106
+ required: ['conversationId', 'targetUserId'],
107
+ optional: [],
108
+ },
109
+ remove_member: {
110
+ required: ['conversationId', 'targetUserId'],
111
+ optional: [],
112
+ },
113
+ leave_conversation: {
114
+ required: ['conversationId'],
115
+ optional: [],
116
+ },
117
+ list_contacts: { required: [], optional: [] },
118
+ list_contact_requests: { required: [], optional: [] },
119
+ list_conversations: { required: [], optional: ['limit'] },
120
+ };
121
+ // ---------------------------------------------------------------------------
122
+ // JSON Schema
123
+ // ---------------------------------------------------------------------------
124
+ const wireBodyDef = {
125
+ description: 'The verb body: plaintext JSON for temporary/plaintext conversations, MLS '
126
+ + 'ciphertext for encrypted ones. In json mode the server may read value to '
127
+ + 'execute legacy behavior; in mls mode it is opaque.',
128
+ oneOf: [
129
+ {
130
+ type: 'object',
131
+ required: ['encoding', 'value'],
132
+ additionalProperties: false,
133
+ properties: {
134
+ encoding: { const: 'json' },
135
+ value: { type: 'object' },
136
+ },
137
+ },
138
+ {
139
+ type: 'object',
140
+ required: ['encoding', 'epoch', 'ciphertext', 'aadHash'],
141
+ additionalProperties: false,
142
+ properties: {
143
+ encoding: { const: 'mls' },
144
+ epoch: { type: 'integer', minimum: 0 },
145
+ ciphertext: { type: 'string', minLength: 1 },
146
+ aadHash: {
147
+ type: 'string',
148
+ minLength: 1,
149
+ description: 'Hash binding the envelope (AAD) to the ciphertext.',
150
+ },
151
+ },
152
+ },
153
+ ],
154
+ };
155
+ const wireTurnDef = {
156
+ type: 'object',
157
+ additionalProperties: false,
158
+ properties: {
159
+ turnId: { type: ['string', 'null'] },
160
+ turnSemantics: { enum: ['progress', 'turn_complete', 'control'] },
161
+ deliveryIntent: { enum: ['queue', 'interrupt', 'interleave', 'stop'] },
162
+ replyBehavior: { enum: ['allow_auto_reply', 'suppress_auto_reply'] },
163
+ },
164
+ };
165
+ const wireEnvelopeDef = {
166
+ type: 'object',
167
+ description: 'Authenticated public envelope: routing/policy metadata only, never '
168
+ + 'content. Per-verb field discipline: VERB_WIRE_ENVELOPE_FIELDS.',
169
+ additionalProperties: false,
170
+ properties: {
171
+ conversationId: { type: 'string', pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeId },
172
+ targetUserId: { type: 'string', minLength: 1 },
173
+ canonContactId: { type: 'string', minLength: 1 },
174
+ sourceConversationId: { type: 'string', pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeId },
175
+ contactUserId: { type: 'string', minLength: 1 },
176
+ requestId: { type: 'string', pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeId },
177
+ responseUserId: { type: 'string', pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeId },
178
+ expiresAt: { type: 'integer', minimum: 0 },
179
+ mode: { enum: ['blocking', 'detached'] },
180
+ kind: { enum: ['clarify', 'sudo', 'secret'] },
181
+ sensitive: { type: 'boolean' },
182
+ allowSessionRule: { type: 'boolean' },
183
+ riskLevel: { enum: ['normal', 'destructive'] },
184
+ risk: { enum: ['low', 'normal', 'high', 'destructive'] },
185
+ category: {
186
+ enum: ['command', 'file', 'network', 'browser', 'mcp', 'plugin', 'canon', 'tool'],
187
+ },
188
+ sessionSelection: {
189
+ oneOf: [
190
+ {
191
+ type: 'object',
192
+ required: ['mode'],
193
+ additionalProperties: false,
194
+ properties: { mode: { enum: ['new', 'continue_latest', 'continue_or_create'] } },
195
+ },
196
+ {
197
+ type: 'object',
198
+ required: ['mode', 'conversationId'],
199
+ additionalProperties: false,
200
+ properties: {
201
+ mode: { const: 'specific' },
202
+ conversationId: { type: 'string', pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeId },
203
+ },
204
+ },
205
+ ],
206
+ },
207
+ sessionConfig: { type: ['object', 'null'] },
208
+ idempotencyKey: {
209
+ type: 'string',
210
+ pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeId,
211
+ not: { pattern: '^(\\.{1,2}|__.*__)$' },
212
+ },
213
+ messageId: { type: 'string', minLength: 1 },
214
+ replyTo: { type: 'string' },
215
+ replyToPosition: { type: 'integer' },
216
+ mentions: {
217
+ type: 'array',
218
+ items: { type: 'string' },
219
+ description: 'Plaintext by decision D3 (routing + mention-piercing pushes).',
220
+ },
221
+ memberIds: {
222
+ type: 'array',
223
+ items: { type: 'string' },
224
+ minItems: 1,
225
+ description: 'Group-membership routing ids (create_group). Plaintext like mentions '
226
+ + '(D3 rationale) but a distinct field — membership operations must not '
227
+ + 'overload the mention slot.',
228
+ },
229
+ turn: { $ref: '#/$defs/turn' },
230
+ native: { type: 'object' },
231
+ runtimeId: { type: 'string', maxLength: verbContract_js_1.VERB_LIMITS.turnIdChars },
232
+ limit: { type: 'integer', minimum: 1 },
233
+ },
234
+ };
235
+ exports.CANON_VERB_WIRE_JSON_SCHEMA = {
236
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
237
+ $id: exports.CANON_VERB_WIRE_SCHEMA_ID,
238
+ title: 'Canon Verb Wire v1',
239
+ description: 'The remote wire contract for agent verbs: authenticated envelope + '
240
+ + 'json-or-mls body. The semantic intent contract (canon.verbs.v1) is '
241
+ + 'local; this is what crosses the network.',
242
+ type: 'object',
243
+ required: ['wire', 'verb', 'envelope', 'body'],
244
+ additionalProperties: false,
245
+ properties: {
246
+ wire: { const: exports.CANON_VERB_WIRE_SCHEMA_VERSION },
247
+ verb: { enum: [...verbContract_js_1.CANON_VERB_NAMES] },
248
+ envelope: { $ref: '#/$defs/envelope' },
249
+ body: { $ref: '#/$defs/body' },
250
+ },
251
+ $defs: {
252
+ body: wireBodyDef,
253
+ envelope: wireEnvelopeDef,
254
+ turn: wireTurnDef,
255
+ accepted: {
256
+ type: 'object',
257
+ required: ['status', 'requestId'],
258
+ additionalProperties: true,
259
+ properties: {
260
+ status: { const: 'accepted' },
261
+ requestId: { type: 'string' },
262
+ expiresAt: { type: 'integer' },
263
+ messageId: { type: 'string' },
264
+ responseUserId: { type: 'string' },
265
+ interactive: { type: 'boolean' },
266
+ },
267
+ },
268
+ response: {
269
+ type: 'object',
270
+ required: ['wire', 'verb', 'result'],
271
+ additionalProperties: true,
272
+ properties: {
273
+ wire: { const: exports.CANON_VERB_WIRE_SCHEMA_VERSION },
274
+ verb: { enum: [...verbContract_js_1.CANON_VERB_NAMES] },
275
+ result: { $ref: '#/$defs/body' },
276
+ },
277
+ },
278
+ },
279
+ };
280
+ // ---------------------------------------------------------------------------
281
+ // Intent <-> wire projection (json codec)
282
+ // ---------------------------------------------------------------------------
283
+ const TURN_KEYS = ['turnId', 'turnSemantics', 'deliveryIntent', 'replyBehavior'];
284
+ function splitTurnMetadata(metadata) {
285
+ if (!metadata || typeof metadata !== 'object')
286
+ return { turn: undefined, rest: undefined };
287
+ const turn = {};
288
+ const rest = {};
289
+ for (const [key, value] of Object.entries(metadata)) {
290
+ if (TURN_KEYS.includes(key))
291
+ turn[key] = value;
292
+ else
293
+ rest[key] = value;
294
+ }
295
+ return {
296
+ turn: Object.keys(turn).length ? turn : undefined,
297
+ rest: Object.keys(rest).length ? rest : undefined,
298
+ };
299
+ }
300
+ function compact(record) {
301
+ const out = {};
302
+ for (const [key, value] of Object.entries(record)) {
303
+ if (value !== undefined)
304
+ out[key] = value;
305
+ }
306
+ return out;
307
+ }
308
+ /**
309
+ * Resolve a relative deadline to the absolute epoch-ms the wire carries.
310
+ * Deterministic: callers pass `now` (the runtime library uses Date.now()).
311
+ */
312
+ function resolveExpiresAt(intent, now) {
313
+ if (typeof intent.expiresAt === 'number')
314
+ return intent.expiresAt;
315
+ if (typeof intent.timeoutMs === 'number')
316
+ return now + intent.timeoutMs;
317
+ return undefined;
318
+ }
319
+ /**
320
+ * Project a plaintext verb intent (canon.verbs.v1 input) into its wire form
321
+ * with the `json` codec: envelope = the routing/policy fields the server may
322
+ * see; body.value = the content fields (what MLS later encrypts).
323
+ *
324
+ * The inverse is mergeVerbWireToIntent; round-tripping normalizes deadlines
325
+ * to absolute expiresAt but is otherwise lossless.
326
+ */
327
+ function projectVerbIntentToWire(verb, intent, options) {
328
+ const input = intent;
329
+ let envelope = {};
330
+ let value = {};
331
+ switch (verb) {
332
+ case 'send_to': {
333
+ const messageOptions = (input.messageOptions ?? {});
334
+ const { turn, rest } = splitTurnMetadata(messageOptions.metadata);
335
+ envelope = compact({
336
+ conversationId: input.targetConversationId,
337
+ targetUserId: input.targetUserId,
338
+ canonContactId: input.canonContactId,
339
+ sourceConversationId: input.sourceConversationId,
340
+ sessionSelection: input.sessionSelection,
341
+ sessionConfig: input.sessionConfig,
342
+ idempotencyKey: messageOptions.messageId,
343
+ replyTo: messageOptions.replyTo,
344
+ replyToPosition: messageOptions.replyToPosition,
345
+ mentions: messageOptions.mentions,
346
+ turn,
347
+ });
348
+ const contentOptions = compact({
349
+ contentType: messageOptions.contentType,
350
+ attachments: messageOptions.attachments,
351
+ metadata: rest,
352
+ });
353
+ value = compact({
354
+ text: input.text,
355
+ selfContext: input.selfContext,
356
+ requestMessage: input.requestMessage,
357
+ messageOptions: Object.keys(contentOptions).length ? contentOptions : undefined,
358
+ });
359
+ break;
360
+ }
361
+ case 'request_input': {
362
+ envelope = compact({
363
+ conversationId: input.conversationId,
364
+ requestId: input.inputId,
365
+ kind: input.kind,
366
+ sensitive: input.sensitive,
367
+ responseUserId: input.responseUserId,
368
+ expiresAt: resolveExpiresAt(input, options.now),
369
+ native: input.native,
370
+ turn: input.turnId !== undefined ? { turnId: input.turnId } : undefined,
371
+ });
372
+ value = compact({
373
+ title: input.title,
374
+ prompt: input.prompt,
375
+ choices: input.choices,
376
+ questions: input.questions,
377
+ secretName: input.secretName,
378
+ });
379
+ break;
380
+ }
381
+ case 'request_approval': {
382
+ envelope = compact({
383
+ conversationId: input.conversationId,
384
+ requestId: input.approvalId,
385
+ mode: input.mode,
386
+ riskLevel: input.riskLevel,
387
+ risk: input.risk,
388
+ category: input.category,
389
+ allowSessionRule: input.allowSessionRule,
390
+ responseUserId: input.responseUserId,
391
+ expiresAt: resolveExpiresAt(input, options.now),
392
+ native: input.native,
393
+ runtimeId: input.runtimeId,
394
+ turn: input.turnId !== undefined ? { turnId: input.turnId } : undefined,
395
+ });
396
+ value = compact({
397
+ toolName: input.toolName,
398
+ toolSummary: input.toolSummary,
399
+ details: input.details,
400
+ diff: input.diff,
401
+ });
402
+ break;
403
+ }
404
+ case 'check_approval': {
405
+ envelope = compact({
406
+ requestId: input.approvalId,
407
+ conversationId: input.conversationId,
408
+ });
409
+ break;
410
+ }
411
+ case 'send_card':
412
+ case 'request_card': {
413
+ envelope = compact({
414
+ conversationId: input.conversationId,
415
+ requestId: input.cardId,
416
+ responseUserId: verb === 'request_card' ? input.responseUserId : undefined,
417
+ expiresAt: verb === 'request_card' ? resolveExpiresAt(input, options.now) : undefined,
418
+ native: input.native,
419
+ runtimeId: input.runtimeId,
420
+ turn: input.turnId !== undefined ? { turnId: input.turnId } : undefined,
421
+ });
422
+ value = compact({ card: input.card });
423
+ break;
424
+ }
425
+ case 'share_contact': {
426
+ envelope = compact({
427
+ conversationId: input.conversationId,
428
+ contactUserId: input.contactUserId,
429
+ idempotencyKey: input.messageId,
430
+ });
431
+ value = compact({ text: input.text });
432
+ break;
433
+ }
434
+ case 'react': {
435
+ envelope = compact({
436
+ conversationId: input.conversationId,
437
+ messageId: input.messageId,
438
+ });
439
+ value = compact({ emoji: input.emoji });
440
+ break;
441
+ }
442
+ case 'forward': {
443
+ envelope = compact({
444
+ sourceConversationId: input.sourceConversationId,
445
+ conversationId: input.targetConversationId,
446
+ messageId: input.messageId,
447
+ });
448
+ value = compact({ text: input.text });
449
+ break;
450
+ }
451
+ case 'create_group': {
452
+ envelope = compact({ memberIds: input.memberIds });
453
+ value = compact({ name: input.name });
454
+ break;
455
+ }
456
+ case 'add_member':
457
+ case 'remove_member': {
458
+ envelope = compact({
459
+ conversationId: input.conversationId,
460
+ targetUserId: input.userId,
461
+ });
462
+ break;
463
+ }
464
+ case 'leave_conversation': {
465
+ envelope = compact({ conversationId: input.conversationId });
466
+ break;
467
+ }
468
+ case 'list_contacts':
469
+ case 'list_contact_requests': {
470
+ break;
471
+ }
472
+ case 'list_conversations': {
473
+ envelope = compact({ limit: input.limit });
474
+ break;
475
+ }
476
+ }
477
+ return {
478
+ wire: exports.CANON_VERB_WIRE_SCHEMA_VERSION,
479
+ verb,
480
+ envelope,
481
+ body: { encoding: 'json', value },
482
+ };
483
+ }
484
+ /**
485
+ * Reconstruct the plaintext verb intent from a json-codec wire request — the
486
+ * server executor's half of the seam. Throws if the body is not json.
487
+ */
488
+ function mergeVerbWireToIntent(request) {
489
+ if (request.body.encoding !== 'json') {
490
+ throw new Error(`cannot merge encoding '${request.body.encoding}' to a plaintext intent — mls bodies are opaque to the server`);
491
+ }
492
+ const envelope = request.envelope;
493
+ const value = request.body.value;
494
+ switch (request.verb) {
495
+ case 'send_to': {
496
+ const contentOptions = (value.messageOptions ?? {});
497
+ const metadata = compact({
498
+ ...(contentOptions.metadata ?? {}),
499
+ ...(envelope.turn ?? {}),
500
+ });
501
+ const messageOptions = compact({
502
+ messageId: envelope.idempotencyKey,
503
+ contentType: contentOptions.contentType,
504
+ attachments: contentOptions.attachments,
505
+ mentions: envelope.mentions,
506
+ replyTo: envelope.replyTo,
507
+ replyToPosition: envelope.replyToPosition,
508
+ metadata: Object.keys(metadata).length ? metadata : undefined,
509
+ });
510
+ return compact({
511
+ targetConversationId: envelope.conversationId,
512
+ targetUserId: envelope.targetUserId,
513
+ canonContactId: envelope.canonContactId,
514
+ sourceConversationId: envelope.sourceConversationId,
515
+ sessionSelection: envelope.sessionSelection,
516
+ sessionConfig: envelope.sessionConfig,
517
+ text: value.text,
518
+ selfContext: value.selfContext,
519
+ requestMessage: value.requestMessage,
520
+ messageOptions: Object.keys(messageOptions).length ? messageOptions : undefined,
521
+ });
522
+ }
523
+ case 'request_input':
524
+ return compact({
525
+ conversationId: envelope.conversationId,
526
+ inputId: envelope.requestId,
527
+ kind: envelope.kind,
528
+ sensitive: envelope.sensitive,
529
+ responseUserId: envelope.responseUserId,
530
+ expiresAt: envelope.expiresAt,
531
+ native: envelope.native,
532
+ turnId: envelope.turn?.turnId ?? undefined,
533
+ title: value.title,
534
+ prompt: value.prompt,
535
+ choices: value.choices,
536
+ questions: value.questions,
537
+ secretName: value.secretName,
538
+ });
539
+ case 'request_approval':
540
+ return compact({
541
+ conversationId: envelope.conversationId,
542
+ approvalId: envelope.requestId,
543
+ mode: envelope.mode,
544
+ riskLevel: envelope.riskLevel,
545
+ risk: envelope.risk,
546
+ category: envelope.category,
547
+ allowSessionRule: envelope.allowSessionRule,
548
+ responseUserId: envelope.responseUserId,
549
+ expiresAt: envelope.expiresAt,
550
+ native: envelope.native,
551
+ runtimeId: envelope.runtimeId,
552
+ turnId: envelope.turn?.turnId ?? undefined,
553
+ toolName: value.toolName,
554
+ toolSummary: value.toolSummary,
555
+ details: value.details,
556
+ diff: value.diff,
557
+ });
558
+ case 'check_approval':
559
+ return compact({
560
+ approvalId: envelope.requestId,
561
+ conversationId: envelope.conversationId,
562
+ });
563
+ case 'send_card':
564
+ case 'request_card':
565
+ return compact({
566
+ conversationId: envelope.conversationId,
567
+ cardId: envelope.requestId,
568
+ responseUserId: envelope.responseUserId,
569
+ expiresAt: envelope.expiresAt,
570
+ native: envelope.native,
571
+ runtimeId: envelope.runtimeId,
572
+ turnId: envelope.turn?.turnId ?? undefined,
573
+ card: value.card,
574
+ });
575
+ case 'share_contact':
576
+ return compact({
577
+ conversationId: envelope.conversationId,
578
+ contactUserId: envelope.contactUserId,
579
+ messageId: envelope.idempotencyKey,
580
+ text: value.text,
581
+ });
582
+ case 'react':
583
+ return compact({
584
+ conversationId: envelope.conversationId,
585
+ messageId: envelope.messageId,
586
+ emoji: value.emoji,
587
+ });
588
+ case 'forward':
589
+ return compact({
590
+ sourceConversationId: envelope.sourceConversationId,
591
+ targetConversationId: envelope.conversationId,
592
+ messageId: envelope.messageId,
593
+ text: value.text,
594
+ });
595
+ case 'create_group':
596
+ return compact({
597
+ name: value.name,
598
+ memberIds: envelope.memberIds,
599
+ });
600
+ case 'add_member':
601
+ case 'remove_member':
602
+ return compact({
603
+ conversationId: envelope.conversationId,
604
+ userId: envelope.targetUserId,
605
+ });
606
+ case 'leave_conversation':
607
+ return compact({ conversationId: envelope.conversationId });
608
+ case 'list_contacts':
609
+ case 'list_contact_requests':
610
+ return {};
611
+ case 'list_conversations':
612
+ return compact({ limit: envelope.limit });
613
+ }
614
+ }
615
+ /**
616
+ * Envelope discipline check: no field outside the verb's allowed set, all
617
+ * required fields present. Schema validation catches shape; this catches
618
+ * per-verb field misuse (a strict-envelope server rejects on it).
619
+ */
620
+ function findVerbWireEnvelopeViolations(verb, envelope) {
621
+ const spec = exports.VERB_WIRE_ENVELOPE_FIELDS[verb];
622
+ const allowed = new Set([...spec.required, ...spec.optional]);
623
+ const violations = [];
624
+ for (const key of Object.keys(envelope)) {
625
+ if (!allowed.has(key))
626
+ violations.push(`envelope.${key} is not used by verb '${verb}'`);
627
+ }
628
+ for (const key of spec.required) {
629
+ if (envelope[key] === undefined) {
630
+ violations.push(`envelope.${key} is required for verb '${verb}'`);
631
+ }
632
+ }
633
+ return violations;
634
+ }
@@ -0,0 +1,6 @@
1
+ /** Matches the header line of a PEM private-key block. */
2
+ export declare const PRIVATE_KEY_BLOCK: RegExp;
3
+ /** True when a diff file path (or rename source) matches the sensitive list. */
4
+ export declare function isSensitiveDiffPath(path: string): boolean;
5
+ /** Replace high-confidence credential shapes inside hunk text with `[redacted]`. */
6
+ export declare function redactSecretTokens(text: string): string;