@canonmsg/backend-contracts 8.4.0 → 8.6.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/dist/canon-verbs.schema.json +0 -1
- package/dist/cjs/conversationMembership.js +48 -0
- package/dist/cjs/endpoint-unread.js +34 -0
- package/dist/cjs/endpoint.js +33 -0
- package/dist/cjs/endpointContentPolicy.js +269 -0
- package/dist/cjs/index.js +7 -0
- package/dist/cjs/plaintextProcessors.js +29 -0
- package/dist/cjs/verbSchemas.js +1 -1
- package/dist/communication.d.ts +16 -4
- package/dist/conversationMembership.d.ts +28 -0
- package/dist/conversationMembership.js +40 -0
- package/dist/endpoint-unread.d.ts +4 -0
- package/dist/endpoint-unread.js +31 -0
- package/dist/endpoint.d.ts +88 -0
- package/dist/endpoint.js +28 -0
- package/dist/endpointContentPolicy.d.ts +33 -0
- package/dist/endpointContentPolicy.js +263 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/plaintextProcessors.d.ts +55 -0
- package/dist/plaintextProcessors.js +25 -0
- package/dist/replyAuthority.d.ts +2 -0
- package/dist/verbContract.d.ts +13 -2
- package/dist/verbSchemas.d.ts +1 -1
- package/dist/verbSchemas.js +1 -1
- package/package.json +2 -2
|
@@ -1427,7 +1427,6 @@
|
|
|
1427
1427
|
"type": "object",
|
|
1428
1428
|
"description": "Create a group conversation. Each target's groupJoinPolicy is enforced server-side with staged admission: directly-addable members join at creation, approval-required members become pending group_invite requests; hard-denied members are skipped (see the result). A creator-only group is valid when at least one requested member has a pending invite. Under MLS, membership changes are Add/Remove proposals + Commit — a group operation is a cryptographic state change, not a codec swap.",
|
|
1429
1429
|
"required": [
|
|
1430
|
-
"name",
|
|
1431
1430
|
"memberIds"
|
|
1432
1431
|
],
|
|
1433
1432
|
"additionalProperties": false,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeConversationRole = normalizeConversationRole;
|
|
4
|
+
exports.isConversationAdminRole = isConversationAdminRole;
|
|
5
|
+
exports.readMembershipRevision = readMembershipRevision;
|
|
6
|
+
exports.resolveConversationPolicyScope = resolveConversationPolicyScope;
|
|
7
|
+
exports.buildConversationParticipantSummary = buildConversationParticipantSummary;
|
|
8
|
+
exports.readConversationInvitationPolicy = readConversationInvitationPolicy;
|
|
9
|
+
const communication_js_1 = require("./communication.js");
|
|
10
|
+
/** `owner` is a read-only legacy spelling, never superior to another admin. */
|
|
11
|
+
function normalizeConversationRole(value) {
|
|
12
|
+
return value === 'admin' || value === 'owner' ? 'admin' : value === 'member' ? 'member' : null;
|
|
13
|
+
}
|
|
14
|
+
function isConversationAdminRole(value) {
|
|
15
|
+
return normalizeConversationRole(value) === 'admin';
|
|
16
|
+
}
|
|
17
|
+
function readMembershipRevision(value) {
|
|
18
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
19
|
+
}
|
|
20
|
+
/** Behavioral scope follows the current roster, never the legacy shape label. */
|
|
21
|
+
function resolveConversationPolicyScope(memberIdsOrCount) {
|
|
22
|
+
const count = Array.isArray(memberIdsOrCount)
|
|
23
|
+
? new Set(memberIdsOrCount.filter((id) => typeof id === 'string' && id.length > 0)).size
|
|
24
|
+
: memberIdsOrCount;
|
|
25
|
+
if (typeof count !== 'number' || !Number.isSafeInteger(count))
|
|
26
|
+
return 'unknown';
|
|
27
|
+
return count === 2 ? 'direct' : count > 2 ? 'group' : 'unknown';
|
|
28
|
+
}
|
|
29
|
+
function buildConversationParticipantSummary(participants) {
|
|
30
|
+
const participantTypes = Object.fromEntries(participants.map(({ userId, userType }) => [userId, userType === 'ai_agent' ? 'ai_agent' : 'human']));
|
|
31
|
+
const types = Object.values(participantTypes);
|
|
32
|
+
const agentCount = types.filter((type) => type === 'ai_agent').length;
|
|
33
|
+
return {
|
|
34
|
+
participantTypes,
|
|
35
|
+
participantSummary: { humanCount: types.length - agentCount, agentCount, totalCount: types.length },
|
|
36
|
+
isAgentChat: agentCount > 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Legacy fields are compatibility inputs to one conservative invitation policy. */
|
|
40
|
+
function readConversationInvitationPolicy(data) {
|
|
41
|
+
const inbound = (0, communication_js_1.readCommunicationRule)(data?.inboundPolicy);
|
|
42
|
+
const group = (0, communication_js_1.readCommunicationRule)(data?.groupJoinPolicy);
|
|
43
|
+
if (inbound === 'closed' || group === 'closed')
|
|
44
|
+
return 'closed';
|
|
45
|
+
if (inbound === 'approval-required' || group === 'approval-required')
|
|
46
|
+
return 'approval-required';
|
|
47
|
+
return 'open';
|
|
48
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeEndpointUnreadProjection = computeEndpointUnreadProjection;
|
|
4
|
+
/** Metadata-only unread projection, shared by serving and the offline migration. */
|
|
5
|
+
function computeEndpointUnreadProjection(principalId, conversation, member) {
|
|
6
|
+
if (!conversation || !member || !Array.isArray(conversation.memberIds) || !conversation.memberIds.includes(principalId))
|
|
7
|
+
return null;
|
|
8
|
+
const message = conversation.lastMessage;
|
|
9
|
+
if (!message || typeof message.senderId !== 'string' || message.senderId === principalId)
|
|
10
|
+
return null;
|
|
11
|
+
const at = time(message.timestamp);
|
|
12
|
+
if (at === null)
|
|
13
|
+
return null;
|
|
14
|
+
const floor = time(member.historyStartAt), read = time(member.lastReadTimestamp), deleted = time(member.deletedAt);
|
|
15
|
+
if ((floor !== null && at < floor) || (read !== null && at <= read) || (deleted !== null && at <= deleted))
|
|
16
|
+
return null;
|
|
17
|
+
return { unmutedAtMs: time(member.mutedUntil) ?? 0 };
|
|
18
|
+
}
|
|
19
|
+
function time(value) {
|
|
20
|
+
if (value === null || value === undefined)
|
|
21
|
+
return null;
|
|
22
|
+
if (typeof value === 'string') {
|
|
23
|
+
const ms = Date.parse(value);
|
|
24
|
+
return Number.isFinite(ms) ? ms : null;
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === 'number')
|
|
27
|
+
return Number.isFinite(value) ? value : null;
|
|
28
|
+
if (typeof value !== 'object')
|
|
29
|
+
return null;
|
|
30
|
+
const timestamp = value;
|
|
31
|
+
const ms = typeof timestamp.toMillis === 'function' ? timestamp.toMillis() : typeof timestamp.toDate === 'function' ? timestamp.toDate().getTime()
|
|
32
|
+
: typeof timestamp.seconds === 'number' ? timestamp.seconds * 1000 + (timestamp.nanoseconds ?? 0) / 1e6 : NaN;
|
|
33
|
+
return Number.isFinite(ms) ? ms : null;
|
|
34
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ENDPOINT_ACTION_PATTERN = exports.ENDPOINT_ID_PATTERN = exports.CANON_ENDPOINT_PROTOCOL = void 0;
|
|
4
|
+
exports.isEndpointOperationRequest = isEndpointOperationRequest;
|
|
5
|
+
exports.isEndpointOperationReceipt = isEndpointOperationReceipt;
|
|
6
|
+
/** Plaintext endpoint protocol. Identity and authority come from authenticated transport. */
|
|
7
|
+
exports.CANON_ENDPOINT_PROTOCOL = 'canon.endpoint.v1';
|
|
8
|
+
exports.ENDPOINT_ID_PATTERN = /^[A-Za-z0-9_.:~-]{1,200}$/;
|
|
9
|
+
exports.ENDPOINT_ACTION_PATTERN = /^[a-z][a-z0-9_.-]{0,95}$/;
|
|
10
|
+
function isEndpointOperationRequest(value) {
|
|
11
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
12
|
+
return false;
|
|
13
|
+
const request = value;
|
|
14
|
+
const body = request.body;
|
|
15
|
+
return request.protocol === exports.CANON_ENDPOINT_PROTOCOL
|
|
16
|
+
&& typeof request.operationId === 'string' && exports.ENDPOINT_ID_PATTERN.test(request.operationId)
|
|
17
|
+
&& typeof request.installationId === 'string' && exports.ENDPOINT_ID_PATTERN.test(request.installationId)
|
|
18
|
+
&& typeof request.action === 'string' && exports.ENDPOINT_ACTION_PATTERN.test(request.action)
|
|
19
|
+
&& (request.ownershipVersion === undefined || Number.isSafeInteger(request.ownershipVersion) && request.ownershipVersion >= 0)
|
|
20
|
+
&& !!body && body.encoding === 'json'
|
|
21
|
+
&& !!body.value && typeof body.value === 'object' && !Array.isArray(body.value);
|
|
22
|
+
}
|
|
23
|
+
function isEndpointOperationReceipt(value) {
|
|
24
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
25
|
+
return false;
|
|
26
|
+
const receipt = value;
|
|
27
|
+
return receipt.protocol === exports.CANON_ENDPOINT_PROTOCOL
|
|
28
|
+
&& typeof receipt.operationId === 'string' && exports.ENDPOINT_ID_PATTERN.test(receipt.operationId)
|
|
29
|
+
&& typeof receipt.installationId === 'string' && exports.ENDPOINT_ID_PATTERN.test(receipt.installationId)
|
|
30
|
+
&& typeof receipt.action === 'string' && exports.ENDPOINT_ACTION_PATTERN.test(receipt.action)
|
|
31
|
+
&& ['pending', 'succeeded', 'rejected', 'uncertain'].includes(String(receipt.status))
|
|
32
|
+
&& typeof receipt.createdAt === 'string' && typeof receipt.updatedAt === 'string';
|
|
33
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EndpointContentPolicyError = exports.ENDPOINT_CONTENT_EVENTS = exports.ENDPOINT_CONTENT_EPHEMERAL_FAMILIES = exports.ENDPOINT_CONTENT_EPHEMERALS = exports.ENDPOINT_CONTENT_QUERIES = exports.ENDPOINT_CONTENT_FUNCTION_QUERIES = exports.ENDPOINT_CONTENT_ACTIONS = void 0;
|
|
4
|
+
exports.endpointOperationRequestRetention = endpointOperationRequestRetention;
|
|
5
|
+
exports.endpointContentBoundary = endpointContentBoundary;
|
|
6
|
+
const verbWire_js_1 = require("./verbWire.js");
|
|
7
|
+
exports.ENDPOINT_CONTENT_ACTIONS = [
|
|
8
|
+
'initialize_endpoint',
|
|
9
|
+
'sync_phone_contacts', 'request_work_session', 'resolve_work_session_request',
|
|
10
|
+
'update_agent_behavior_policy', 'update_conversation_agent_behavior_policy',
|
|
11
|
+
'send_message', 'delete_message', 'forward_message', 'react', 'verb_execute',
|
|
12
|
+
'send_contextual_message', 'create_interaction', 'consume_interaction',
|
|
13
|
+
'start_conversation', 'create_conversation', 'add_member', 'remove_member',
|
|
14
|
+
'leave_conversation', 'update_member_role', 'update_conversation', 'rename_conversation',
|
|
15
|
+
'update_topic', 'update_group_avatar', 'mark_read', 'cancel_queued_message',
|
|
16
|
+
'set_conversation_preferences', 'set_block', 'save_contact', 'remove_contact',
|
|
17
|
+
'respond_interaction', 'session_control', 'approve_contact_request',
|
|
18
|
+
'reject_contact_request', 'cancel_direct_request', 'update_message_disposition',
|
|
19
|
+
'create_upload', 'finalize_upload', 'delete_upload', 'retry_video_processing',
|
|
20
|
+
'finalize_memo_stream', 'abort_memo_stream', 'create_voice_session',
|
|
21
|
+
'join_voice_session', 'decline_voice_session', 'end_voice_session',
|
|
22
|
+
'consume_runtime_signal', 'generate_image',
|
|
23
|
+
];
|
|
24
|
+
exports.ENDPOINT_CONTENT_FUNCTION_QUERIES = [
|
|
25
|
+
'discover_agents', 'get_unread_summary', 'peek_runtime_signal',
|
|
26
|
+
'get_work_session_catalog', 'get_work_session_request', 'get_agent_behavior_policy',
|
|
27
|
+
'get_conversation_agent_behavior_policy', 'get_ephemeral', 'list_voice_sessions',
|
|
28
|
+
'list_contact_requests', 'list_conversations', 'list_messages', 'get_messages',
|
|
29
|
+
'admission_fences', 'list_contacts', 'get_contact', 'get_members', 'resolve_admission',
|
|
30
|
+
'resolve_group_admission', 'get_runtime_card_state', 'conversation',
|
|
31
|
+
'conversation_changes', 'get_upload', 'get_voice_session', 'get_image_generation',
|
|
32
|
+
'get_image_generation_status',
|
|
33
|
+
];
|
|
34
|
+
exports.ENDPOINT_CONTENT_QUERIES = [...exports.ENDPOINT_CONTENT_FUNCTION_QUERIES, 'admit_messages'];
|
|
35
|
+
exports.ENDPOINT_CONTENT_EPHEMERALS = [
|
|
36
|
+
'create_memo_stream', 'publish_presence', 'publish_typing', 'publish_streaming',
|
|
37
|
+
'publish_turn', 'publish_runtime_status', 'append_memo_chunk',
|
|
38
|
+
];
|
|
39
|
+
exports.ENDPOINT_CONTENT_EPHEMERAL_FAMILIES = [
|
|
40
|
+
'typing', 'streaming', 'turn-state', 'agent-session', 'runtime-card-state',
|
|
41
|
+
'memo-streams', 'runtime-info', 'runtime-activity', 'runtime-suppression',
|
|
42
|
+
'runtime-silence', 'runtime-attention', 'call-attention', 'agent-runtime', 'presence',
|
|
43
|
+
];
|
|
44
|
+
exports.ENDPOINT_CONTENT_EVENTS = [
|
|
45
|
+
'connected', 'heartbeat', 'agent.context', 'replay.expired',
|
|
46
|
+
'message.created', 'message.snapshot', 'message.updated', 'message.deleted',
|
|
47
|
+
'message.media_terminal', 'conversation.updated', 'conversation.changed',
|
|
48
|
+
'conversation.removed', 'conversations.changed', 'contacts.changed',
|
|
49
|
+
'contact.added', 'contact.updated', 'contact.removed', 'presence', 'typing',
|
|
50
|
+
'runtime.updated', 'runtime.control', 'turn.updated',
|
|
51
|
+
'voice.session.started', 'voice.session.ended',
|
|
52
|
+
...exports.ENDPOINT_CONTENT_EPHEMERAL_FAMILIES.map((family) => `ephemeral.${family}`),
|
|
53
|
+
];
|
|
54
|
+
class EndpointContentPolicyError extends Error {
|
|
55
|
+
code = 'ENDPOINT_CONTENT_UNCLASSIFIED';
|
|
56
|
+
constructor(lane, name) {
|
|
57
|
+
super(`Unclassified endpoint lane: ${lane}/${name}`);
|
|
58
|
+
this.name = 'EndpointContentPolicyError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.EndpointContentPolicyError = EndpointContentPolicyError;
|
|
62
|
+
const namesForLane = {
|
|
63
|
+
'operation.request': exports.ENDPOINT_CONTENT_ACTIONS,
|
|
64
|
+
'operation.receipt': exports.ENDPOINT_CONTENT_ACTIONS,
|
|
65
|
+
'query.request': exports.ENDPOINT_CONTENT_QUERIES,
|
|
66
|
+
'query.result': exports.ENDPOINT_CONTENT_QUERIES,
|
|
67
|
+
'ephemeral.request': exports.ENDPOINT_CONTENT_EPHEMERALS,
|
|
68
|
+
'ephemeral.result': exports.ENDPOINT_CONTENT_EPHEMERALS,
|
|
69
|
+
event: exports.ENDPOINT_CONTENT_EVENTS,
|
|
70
|
+
};
|
|
71
|
+
/** An explicit allowlist of routing/authorization fields, not a rule that all
|
|
72
|
+
* fields named `id` are public. Free text, previews, labels, media descriptors,
|
|
73
|
+
* paths, provider configuration, native snapshots and arbitrary metadata stay
|
|
74
|
+
* private. IDs/cursors/timing still reveal a communication graph. */
|
|
75
|
+
const protocolPaths = [
|
|
76
|
+
'protocol', 'operationId', 'installationId', 'action', 'ownershipVersion',
|
|
77
|
+
'preconditions.conversationId', 'preconditions.membershipRevision',
|
|
78
|
+
'preconditions.admissionId', 'preconditions.targetAdmissionId', 'body.encoding',
|
|
79
|
+
];
|
|
80
|
+
// Fields are reviewed for a particular lane: e.g. an unknown `contactId` added
|
|
81
|
+
// to send_message is still private, despite being a routing ID elsewhere.
|
|
82
|
+
const actionRouting = {
|
|
83
|
+
send_message: ['conversationId', 'messageId', 'replyTo', 'mentions.*'],
|
|
84
|
+
delete_message: ['conversationId', 'messageId'], forward_message: ['sourceConversationId', 'targetConversationId', 'messageId'],
|
|
85
|
+
react: ['conversationId', 'messageId'], send_contextual_message: ['conversationId'],
|
|
86
|
+
create_interaction: ['conversationId', 'requestId', 'inputId', 'approvalId', 'cardId', 'planId', 'responseUserId'],
|
|
87
|
+
consume_interaction: ['conversationId', 'inputId', 'approvalId', 'cardId', 'planId'],
|
|
88
|
+
respond_interaction: ['conversationId', 'requestId'],
|
|
89
|
+
start_conversation: ['targetUserId'], create_conversation: ['memberIds.*'],
|
|
90
|
+
add_member: ['conversationId', 'userId', 'agentId'], remove_member: ['conversationId', 'userId', 'agentId', 'targetUserId'],
|
|
91
|
+
leave_conversation: ['conversationId'], update_member_role: ['conversationId', 'userId', 'targetUserId'],
|
|
92
|
+
update_conversation: ['conversationId'], rename_conversation: ['conversationId'], update_topic: ['conversationId'],
|
|
93
|
+
update_group_avatar: ['conversationId'], mark_read: ['conversationId'], cancel_queued_message: ['conversationId', 'messageId'],
|
|
94
|
+
set_conversation_preferences: ['conversationId'], set_block: ['targetUserId'], save_contact: ['contactUserId'], remove_contact: ['contactId', 'contactUserId'],
|
|
95
|
+
session_control: ['conversationId', 'agentId'], approve_contact_request: ['requestId'], reject_contact_request: ['requestId'], cancel_direct_request: ['requestId'],
|
|
96
|
+
update_message_disposition: ['conversationId', 'messageId'], create_upload: ['conversationId'], finalize_upload: ['uploadId'], delete_upload: ['uploadId'], retry_video_processing: ['uploadId'],
|
|
97
|
+
finalize_memo_stream: ['conversationId', 'streamId'], abort_memo_stream: ['streamId'],
|
|
98
|
+
create_voice_session: ['conversationId'], join_voice_session: ['conversationId', 'sessionId'], decline_voice_session: ['conversationId', 'sessionId'], end_voice_session: ['conversationId', 'sessionId'],
|
|
99
|
+
consume_runtime_signal: ['conversationId', 'expectedSignalId'], generate_image: ['conversationId', 'requestId'],
|
|
100
|
+
request_work_session: ['conversationId', 'agentId', 'requestId'], resolve_work_session_request: ['requestId'],
|
|
101
|
+
update_agent_behavior_policy: ['agentId'], update_conversation_agent_behavior_policy: ['conversationId', 'agentId'],
|
|
102
|
+
};
|
|
103
|
+
const queryRouting = {
|
|
104
|
+
admit_messages: ['conversationId', 'messageIds.*', 'admissionId'], conversation_changes: ['conversationId', 'cursor', 'limit'],
|
|
105
|
+
conversation: ['conversationId'], list_conversations: ['limit', 'before'], list_messages: ['conversationId', 'before', 'limit'], get_messages: ['conversationId', 'messageIds.*'],
|
|
106
|
+
list_contacts: ['limit', 'before'], get_contact: ['contactId', 'contactUserId', 'userId'], get_members: ['conversationId'],
|
|
107
|
+
list_contact_requests: ['direction', 'limit', 'before'], admission_fences: ['conversationId', 'targetUserId'],
|
|
108
|
+
resolve_admission: ['targetUserId'], resolve_group_admission: ['conversationId', 'targetUserId'], get_runtime_card_state: ['conversationId', 'cardId'],
|
|
109
|
+
get_ephemeral: ['family', 'scopeId'], peek_runtime_signal: ['conversationId'], list_voice_sessions: ['conversationId', 'limit'],
|
|
110
|
+
get_upload: ['uploadId'], get_voice_session: ['conversationId', 'sessionId'], get_image_generation: ['requestId'],
|
|
111
|
+
get_work_session_catalog: ['agentId'], get_work_session_request: ['requestId'], get_agent_behavior_policy: ['agentId'], get_conversation_agent_behavior_policy: ['conversationId', 'agentId'],
|
|
112
|
+
};
|
|
113
|
+
const transientRouting = {
|
|
114
|
+
create_memo_stream: ['conversationId', 'streamId'], append_memo_chunk: ['streamId'],
|
|
115
|
+
publish_presence: [], publish_typing: ['conversationId'], publish_streaming: ['conversationId'], publish_turn: ['conversationId'], publish_runtime_status: ['conversationId'],
|
|
116
|
+
};
|
|
117
|
+
function eventRouting(name) {
|
|
118
|
+
if (name.startsWith('ephemeral.'))
|
|
119
|
+
return ['conversationId', 'scopeId'];
|
|
120
|
+
if (name === 'connected')
|
|
121
|
+
return ['protocol', 'connectionId', 'agentId', 'principalId', 'principalType'];
|
|
122
|
+
if (name === 'heartbeat')
|
|
123
|
+
return ['timestamp'];
|
|
124
|
+
if (name === 'agent.context')
|
|
125
|
+
return ['agentId', 'ownerId', 'ownershipVersion'];
|
|
126
|
+
if (name === 'conversations.changed')
|
|
127
|
+
return ['conversationIds.*', 'removedConversationIds.*'];
|
|
128
|
+
if (name.startsWith('message.'))
|
|
129
|
+
return ['conversationId', 'messageId', 'eventId', 'id', 'message.id', 'message.conversationId', 'message.senderId', 'message.senderType', 'message.createdAt', 'message.replyTo', 'message.mentions.*'];
|
|
130
|
+
if (name.startsWith('conversation.'))
|
|
131
|
+
return ['conversationId'];
|
|
132
|
+
if (name.startsWith('contact.'))
|
|
133
|
+
return ['contactId'];
|
|
134
|
+
if (name.startsWith('voice.session.'))
|
|
135
|
+
return ['conversationId', 'sessionId'];
|
|
136
|
+
if (/^(runtime\.|turn\.|typing$|presence$)/.test(name))
|
|
137
|
+
return ['conversationId', 'agentId', 'userId', 'memberId'];
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const secretSegments = new Set(['token', 'accessToken', 'refreshToken', 'uploadUrl', 'signedUrl', 'livekitToken', 'replyAuthority', 'credentials', 'password', 'secret', 'authorization', 'answer', 'answers', 'response', 'submittedValue']);
|
|
141
|
+
const matches = (path, pattern) => {
|
|
142
|
+
const parts = pattern.split('.');
|
|
143
|
+
return parts.length === path.length && parts.every((part, index) => part === path[index] || part === '*' && /^\d+$/.test(path[index]));
|
|
144
|
+
};
|
|
145
|
+
const publicMatch = (path, patterns) => patterns.some((pattern) => matches(path, pattern));
|
|
146
|
+
/** Local journal policy is separate from wire classification. Input answers and
|
|
147
|
+
* card values can carry passwords even when the response omits the original
|
|
148
|
+
* request's secret/sudo/field metadata. Only reviewed response shapes without
|
|
149
|
+
* those value bags may be retried from durable storage. Unknown response fields
|
|
150
|
+
* fail closed; a caller's `sensitive: false` cannot weaken this policy. */
|
|
151
|
+
function endpointOperationRequestRetention(action, body, envelope) {
|
|
152
|
+
if (action !== 'respond_interaction')
|
|
153
|
+
return 'journal';
|
|
154
|
+
// Interaction services use the body exclusively. Unknown envelope additions
|
|
155
|
+
// must not bypass retention, including conflicting fields rejected by server.
|
|
156
|
+
if (envelope && Object.keys(envelope).length > 0)
|
|
157
|
+
return 'memory';
|
|
158
|
+
const routing = ['kind', 'conversationId', 'agentId', 'requestId'];
|
|
159
|
+
const hasOnly = (fields) => Object.keys(body).every((field) => fields.includes(field));
|
|
160
|
+
if (routing.some((field) => body[field] !== undefined && typeof body[field] !== 'string'))
|
|
161
|
+
return 'memory';
|
|
162
|
+
if ((body.kind === 'input' || body.kind === 'card') && hasOnly([...routing, 'status', 'actionId'])) {
|
|
163
|
+
if (!['submitted', 'cancelled'].includes(String(body.status)) || (body.actionId !== undefined && typeof body.actionId !== 'string'))
|
|
164
|
+
return 'memory';
|
|
165
|
+
// A card button or cancellation with no submitted values contains no answer
|
|
166
|
+
// plaintext. Input/card requests with value, answers or values stay in RAM,
|
|
167
|
+
// including non-secret clarification: that distinction is absent on wire.
|
|
168
|
+
return 'journal';
|
|
169
|
+
}
|
|
170
|
+
if (body.kind === 'approval' && hasOnly([...routing, 'decision', 'sessionRule'])) {
|
|
171
|
+
if (body.decision !== 'allow' && body.decision !== 'deny')
|
|
172
|
+
return 'memory';
|
|
173
|
+
const rule = body.sessionRule;
|
|
174
|
+
if (rule !== undefined && (!rule || typeof rule !== 'object' || Array.isArray(rule)
|
|
175
|
+
|| !Object.entries(rule).every(([field, value]) => ['type', 'toolPattern', 'expiresAt'].includes(field)
|
|
176
|
+
&& (typeof value === 'string' || field === 'expiresAt' && value === null))))
|
|
177
|
+
return 'memory';
|
|
178
|
+
return 'journal';
|
|
179
|
+
}
|
|
180
|
+
if (body.kind === 'plan' && hasOnly([...routing, 'decision', 'feedback', 'grantedPrompts'])) {
|
|
181
|
+
if (!['approve', 'revise', 'reject'].includes(String(body.decision)) || body.feedback !== undefined && typeof body.feedback !== 'string')
|
|
182
|
+
return 'memory';
|
|
183
|
+
// Plan feedback and granted tool prompts are ordinary authored instructions,
|
|
184
|
+
// not secret-entry fields. Preserve their established offline semantics.
|
|
185
|
+
const prompts = body.grantedPrompts;
|
|
186
|
+
if (prompts !== undefined && (!Array.isArray(prompts) || prompts.some((prompt) => !prompt || typeof prompt !== 'object'
|
|
187
|
+
|| Array.isArray(prompt) || !Object.entries(prompt).every(([field, value]) => ['tool', 'prompt'].includes(field) && typeof value === 'string'))))
|
|
188
|
+
return 'memory';
|
|
189
|
+
return 'journal';
|
|
190
|
+
}
|
|
191
|
+
return 'memory';
|
|
192
|
+
}
|
|
193
|
+
/** Validate the lane before accepting/transmitting its payload. No body values
|
|
194
|
+
* are logged, copied into metadata, or claimed to be encrypted by this API. */
|
|
195
|
+
function endpointContentBoundary(lane, name, payload) {
|
|
196
|
+
if (!namesForLane[lane]?.includes(name))
|
|
197
|
+
throw new EndpointContentPolicyError(lane, name);
|
|
198
|
+
const record = payload && typeof payload === 'object' ? payload : undefined;
|
|
199
|
+
if (lane === 'query.request' && name === 'get_ephemeral' && record && !exports.ENDPOINT_CONTENT_EPHEMERAL_FAMILIES.includes(record.family)) {
|
|
200
|
+
throw new EndpointContentPolicyError(lane, `${name}/unknown-family`);
|
|
201
|
+
}
|
|
202
|
+
const requestRoutingPaths = (actionRouting[name] ?? []).flatMap((field) => [`body.value.${field}`, `envelope.${field}`]);
|
|
203
|
+
const classify = (path) => {
|
|
204
|
+
if (path.some((part) => secretSegments.has(part)))
|
|
205
|
+
return 'restricted-secret';
|
|
206
|
+
if (lane === 'operation.request' && ['respond_interaction', 'sync_phone_contacts'].includes(name) && path[0] === 'body' && path[1] === 'value' && !publicMatch(path, requestRoutingPaths))
|
|
207
|
+
return 'restricted-secret';
|
|
208
|
+
if (lane === 'operation.receipt' && path[0] === 'result' && (['consume_interaction', 'respond_interaction'].includes(name) || record?.resultReference?.kind === 'interaction'))
|
|
209
|
+
return 'restricted-secret';
|
|
210
|
+
let selected = payload;
|
|
211
|
+
for (const part of path)
|
|
212
|
+
selected = selected && typeof selected === 'object' && Object.hasOwn(selected, part) ? selected[part] : undefined;
|
|
213
|
+
// An allowed scalar path cannot make arbitrary nested content public.
|
|
214
|
+
if (selected !== null && typeof selected === 'object')
|
|
215
|
+
return 'private-content';
|
|
216
|
+
if (lane === 'operation.request') {
|
|
217
|
+
if (publicMatch(path, protocolPaths) || publicMatch(path, requestRoutingPaths))
|
|
218
|
+
return 'public-metadata';
|
|
219
|
+
// All response content is treated as restricted, even for a non-secret
|
|
220
|
+
// interaction; classification cannot depend on a stale local kind flag.
|
|
221
|
+
if (name === 'respond_interaction' && path[0] === 'body' && path[1] === 'value')
|
|
222
|
+
return 'restricted-secret';
|
|
223
|
+
if (name === 'sync_phone_contacts' && path[0] === 'body' && path[1] === 'value')
|
|
224
|
+
return 'restricted-secret';
|
|
225
|
+
if (name === 'verb_execute') {
|
|
226
|
+
const wire = record?.body?.value;
|
|
227
|
+
const spec = verbWire_js_1.VERB_WIRE_ENVELOPE_FIELDS[wire?.verb];
|
|
228
|
+
if (path[0] === 'body' && path[1] === 'value') {
|
|
229
|
+
if (path.length === 3 && ['wire', 'verb'].includes(path[2]))
|
|
230
|
+
return 'public-metadata';
|
|
231
|
+
if (path[2] === 'envelope' && spec) {
|
|
232
|
+
const field = path[3];
|
|
233
|
+
const allowed = [...spec.required, ...spec.optional];
|
|
234
|
+
// Reuse verbWire's existing split. Nested metadata is deliberately
|
|
235
|
+
// conservative: only declared scalar fields and numeric array IDs.
|
|
236
|
+
if (field && allowed.includes(field) && (path.length === 4 || path.length === 5 && ['mentions', 'memberIds'].includes(field) && /^\d+$/.test(path[4])))
|
|
237
|
+
return 'public-metadata';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else if (lane === 'operation.receipt') {
|
|
243
|
+
if (publicMatch(path, [...protocolPaths, 'status', 'createdAt', 'updatedAt', 'error.code', 'error.status', 'error.retryable', 'error.retryAfterMs', 'resultError.code', 'resultError.status', 'resultReference.kind', 'resultReference.conversationId', 'resultReference.uploadId', 'resultReference.sessionId', 'resultReference.requestId']))
|
|
244
|
+
return 'public-metadata';
|
|
245
|
+
if (path[0] === 'result' && (name === 'consume_interaction' || name === 'respond_interaction' || record?.resultReference?.kind === 'interaction'))
|
|
246
|
+
return 'restricted-secret';
|
|
247
|
+
}
|
|
248
|
+
else if (lane === 'query.request') {
|
|
249
|
+
if (publicMatch(path, queryRouting[name] ?? []))
|
|
250
|
+
return 'public-metadata';
|
|
251
|
+
}
|
|
252
|
+
else if (lane === 'query.result' || lane === 'ephemeral.result') {
|
|
253
|
+
if (name === 'conversation_changes' && publicMatch(path, ['cursor', 'hasMore', 'reset', 'admissionId']))
|
|
254
|
+
return 'public-metadata';
|
|
255
|
+
if (name === 'get_ephemeral' && path[0] === 'value')
|
|
256
|
+
return 'private-content';
|
|
257
|
+
}
|
|
258
|
+
else if (lane === 'ephemeral.request') {
|
|
259
|
+
if (publicMatch(path, transientRouting[name] ?? []))
|
|
260
|
+
return 'public-metadata';
|
|
261
|
+
}
|
|
262
|
+
else if (lane === 'event') {
|
|
263
|
+
if (publicMatch(path, eventRouting(name)))
|
|
264
|
+
return 'public-metadata';
|
|
265
|
+
}
|
|
266
|
+
return 'private-content';
|
|
267
|
+
};
|
|
268
|
+
return Object.freeze({ lane, name, defaultClass: 'private-content', classify });
|
|
269
|
+
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.computeEndpointUnreadProjection = void 0;
|
|
17
18
|
__exportStar(require("./diffRedaction.js"), exports);
|
|
18
19
|
__exportStar(require("./environment.js"), exports);
|
|
19
20
|
__exportStar(require("./media.js"), exports);
|
|
@@ -37,3 +38,9 @@ __exportStar(require("./selfContext.js"), exports);
|
|
|
37
38
|
__exportStar(require("./replyAuthority.js"), exports);
|
|
38
39
|
__exportStar(require("./runtimeDescriptor.js"), exports);
|
|
39
40
|
__exportStar(require("./workSessions.js"), exports);
|
|
41
|
+
__exportStar(require("./conversationMembership.js"), exports);
|
|
42
|
+
__exportStar(require("./endpoint.js"), exports);
|
|
43
|
+
__exportStar(require("./endpointContentPolicy.js"), exports);
|
|
44
|
+
__exportStar(require("./plaintextProcessors.js"), exports);
|
|
45
|
+
var endpoint_unread_js_1 = require("./endpoint-unread.js");
|
|
46
|
+
Object.defineProperty(exports, "computeEndpointUnreadProjection", { enumerable: true, get: function () { return endpoint_unread_js_1.computeEndpointUnreadProjection; } });
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PLAINTEXT_PROCESSORS = void 0;
|
|
4
|
+
exports.runPlaintextProcessor = runPlaintextProcessor;
|
|
5
|
+
/** Existing server content readers. Classification is not encryption: these
|
|
6
|
+
* processors intentionally inspect plaintext in the current JSON release.
|
|
7
|
+
* Their input boundary must be redesigned before enabling encrypted rooms. */
|
|
8
|
+
exports.PLAINTEXT_PROCESSORS = {
|
|
9
|
+
preview: { current: 'Read message/display text to derive conversation and notification previews.', beforeMls: 'Derive on authorized endpoints; clear/reconcile deleted-message previews there.' },
|
|
10
|
+
push: { current: 'Send visible titles and message previews to FCM/APNs.', beforeMls: 'Use generic wake notifications or endpoint-decrypted notification extensions.' },
|
|
11
|
+
topic: { current: 'Derive the initial topic from message text.', beforeMls: 'Derive on an authorized endpoint and publish as encrypted conversation content.' },
|
|
12
|
+
forward: { current: 'Read source content and copy plaintext attachments into the target room.', beforeMls: 'Decrypt and re-encrypt on an authorized endpoint; keep server target admission checks.' },
|
|
13
|
+
message_validation: { current: 'Inspect text, attachment descriptors and rich content before storing messages.', beforeMls: 'Validate decrypted content on endpoints; retain bounded ciphertext/envelope checks on the server.' },
|
|
14
|
+
interaction_validation: { current: 'Validate card/input/approval/plan render contracts and submitted responses.', beforeMls: 'Validate content on authorized requesting/responding endpoints; retain public admission and lifecycle checks.' },
|
|
15
|
+
image_generation: { current: 'Send prompts/source image bytes to Vertex and persist generated output.', beforeMls: 'Require explicit provider disclosure and an authorized endpoint/processing participant; no silent server decryption.' },
|
|
16
|
+
video_processing: { current: 'Download plaintext video, normalize it and create a poster.', beforeMls: 'Process on an endpoint or an explicitly authorized processing participant before encrypted upload.' },
|
|
17
|
+
owner_observation: { current: 'Allow authenticated owners to inspect managed agent conversations without room membership.', beforeMls: 'Explicitly enroll authorized owner keyholders or retire content observation; ownership alone supplies no decryption keys.' },
|
|
18
|
+
moderation_evidence: { current: 'Accept user-selected plaintext message evidence for moderation.', beforeMls: 'Design explicit selective disclosure/authenticity (including franking policy); never infer access to room keys.' },
|
|
19
|
+
};
|
|
20
|
+
/** The callback is the actual content processor, not a telemetry hook. Keeping
|
|
21
|
+
* the codec explicit ensures an opaque body cannot accidentally be processed by
|
|
22
|
+
* the JSON path. This is not authorization; callers still enforce their domain
|
|
23
|
+
* policy and every enabled transport currently requires JSON. */
|
|
24
|
+
function runPlaintextProcessor(name, input, process) {
|
|
25
|
+
if (!Object.hasOwn(exports.PLAINTEXT_PROCESSORS, name) || input.encoding !== 'json') {
|
|
26
|
+
throw new Error('PLAINTEXT_PROCESSOR_CODEC_REQUIRED');
|
|
27
|
+
}
|
|
28
|
+
return process(input.value);
|
|
29
|
+
}
|
package/dist/cjs/verbSchemas.js
CHANGED
|
@@ -787,7 +787,7 @@ const create_group_input = {
|
|
|
787
787
|
+ 'pending invite. Under MLS, membership '
|
|
788
788
|
+ 'changes are Add/Remove proposals + Commit — a group operation is a '
|
|
789
789
|
+ 'cryptographic state change, not a codec swap.',
|
|
790
|
-
required: ['
|
|
790
|
+
required: ['memberIds'],
|
|
791
791
|
additionalProperties: false,
|
|
792
792
|
properties: {
|
|
793
793
|
name: { type: 'string', minLength: 1, maxLength: verbContract_js_1.VERB_LIMITS.groupNameChars },
|
package/dist/communication.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ListConversationsResult } from './verbContract.js';
|
|
1
2
|
import type { DiscoverAgentsInput, DiscoverAgentsResult } from './agentDirectory.js';
|
|
2
3
|
/** The deliberately small policy vocabulary for principal communication. */
|
|
3
4
|
export type CommunicationRule = 'open' | 'approval-required' | 'closed';
|
|
@@ -20,14 +21,14 @@ export type CommunicateInput = ({
|
|
|
20
21
|
text: string;
|
|
21
22
|
messageId?: string;
|
|
22
23
|
} | {
|
|
23
|
-
action: 'start_direct';
|
|
24
|
+
action: 'start_direct' | 'start_conversation';
|
|
24
25
|
principalId: string;
|
|
25
26
|
text: string;
|
|
26
27
|
selection?: DirectConversationSelection;
|
|
27
28
|
messageId?: string;
|
|
28
29
|
} | {
|
|
29
|
-
action: 'create_group';
|
|
30
|
-
name
|
|
30
|
+
action: 'create_group' | 'create_conversation';
|
|
31
|
+
name?: string;
|
|
31
32
|
memberIds: string[];
|
|
32
33
|
} | {
|
|
33
34
|
action: 'forward_message';
|
|
@@ -42,12 +43,23 @@ export type CommunicateInput = ({
|
|
|
42
43
|
text?: string;
|
|
43
44
|
messageId?: string;
|
|
44
45
|
} | {
|
|
45
|
-
action: 'manage_group_members';
|
|
46
|
+
action: 'manage_group_members' | 'manage_participants';
|
|
46
47
|
conversationId: string;
|
|
47
48
|
userId: string;
|
|
48
49
|
operation: 'add' | 'remove';
|
|
50
|
+
} | {
|
|
51
|
+
action: 'list_conversations';
|
|
52
|
+
limit?: number;
|
|
53
|
+
} | {
|
|
54
|
+
action: 'leave_conversation';
|
|
55
|
+
conversationId: string;
|
|
49
56
|
};
|
|
50
57
|
export type CommunicateResult = ({
|
|
58
|
+
status: 'listed';
|
|
59
|
+
} & ListConversationsResult) | {
|
|
60
|
+
status: 'left';
|
|
61
|
+
conversationId: string;
|
|
62
|
+
} | ({
|
|
51
63
|
status: 'discovered';
|
|
52
64
|
} & DiscoverAgentsResult) | {
|
|
53
65
|
status: 'messaged';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type CommunicationRule } from './communication.js';
|
|
2
|
+
/** Shared social membership semantics; presentation and runtime ownership are separate. */
|
|
3
|
+
export type ConversationRole = 'admin' | 'member';
|
|
4
|
+
export type ConversationParticipantType = 'human' | 'ai_agent';
|
|
5
|
+
export interface ConversationParticipantSummary {
|
|
6
|
+
humanCount: number;
|
|
7
|
+
agentCount: number;
|
|
8
|
+
totalCount: number;
|
|
9
|
+
}
|
|
10
|
+
/** `owner` is a read-only legacy spelling, never superior to another admin. */
|
|
11
|
+
export declare function normalizeConversationRole(value: unknown): ConversationRole | null;
|
|
12
|
+
export declare function isConversationAdminRole(value: unknown): boolean;
|
|
13
|
+
export declare function readMembershipRevision(value: unknown): number;
|
|
14
|
+
/** Behavioral scope follows the current roster, never the legacy shape label. */
|
|
15
|
+
export declare function resolveConversationPolicyScope(memberIdsOrCount: unknown): 'direct' | 'group' | 'unknown';
|
|
16
|
+
export declare function buildConversationParticipantSummary(participants: readonly {
|
|
17
|
+
userId: string;
|
|
18
|
+
userType: unknown;
|
|
19
|
+
}[]): {
|
|
20
|
+
participantTypes: Record<string, ConversationParticipantType>;
|
|
21
|
+
participantSummary: ConversationParticipantSummary;
|
|
22
|
+
isAgentChat: boolean;
|
|
23
|
+
};
|
|
24
|
+
/** Legacy fields are compatibility inputs to one conservative invitation policy. */
|
|
25
|
+
export declare function readConversationInvitationPolicy(data: {
|
|
26
|
+
inboundPolicy?: unknown;
|
|
27
|
+
groupJoinPolicy?: unknown;
|
|
28
|
+
} | null | undefined): CommunicationRule;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readCommunicationRule } from './communication.js';
|
|
2
|
+
/** `owner` is a read-only legacy spelling, never superior to another admin. */
|
|
3
|
+
export function normalizeConversationRole(value) {
|
|
4
|
+
return value === 'admin' || value === 'owner' ? 'admin' : value === 'member' ? 'member' : null;
|
|
5
|
+
}
|
|
6
|
+
export function isConversationAdminRole(value) {
|
|
7
|
+
return normalizeConversationRole(value) === 'admin';
|
|
8
|
+
}
|
|
9
|
+
export function readMembershipRevision(value) {
|
|
10
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
11
|
+
}
|
|
12
|
+
/** Behavioral scope follows the current roster, never the legacy shape label. */
|
|
13
|
+
export function resolveConversationPolicyScope(memberIdsOrCount) {
|
|
14
|
+
const count = Array.isArray(memberIdsOrCount)
|
|
15
|
+
? new Set(memberIdsOrCount.filter((id) => typeof id === 'string' && id.length > 0)).size
|
|
16
|
+
: memberIdsOrCount;
|
|
17
|
+
if (typeof count !== 'number' || !Number.isSafeInteger(count))
|
|
18
|
+
return 'unknown';
|
|
19
|
+
return count === 2 ? 'direct' : count > 2 ? 'group' : 'unknown';
|
|
20
|
+
}
|
|
21
|
+
export function buildConversationParticipantSummary(participants) {
|
|
22
|
+
const participantTypes = Object.fromEntries(participants.map(({ userId, userType }) => [userId, userType === 'ai_agent' ? 'ai_agent' : 'human']));
|
|
23
|
+
const types = Object.values(participantTypes);
|
|
24
|
+
const agentCount = types.filter((type) => type === 'ai_agent').length;
|
|
25
|
+
return {
|
|
26
|
+
participantTypes,
|
|
27
|
+
participantSummary: { humanCount: types.length - agentCount, agentCount, totalCount: types.length },
|
|
28
|
+
isAgentChat: agentCount > 0,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Legacy fields are compatibility inputs to one conservative invitation policy. */
|
|
32
|
+
export function readConversationInvitationPolicy(data) {
|
|
33
|
+
const inbound = readCommunicationRule(data?.inboundPolicy);
|
|
34
|
+
const group = readCommunicationRule(data?.groupJoinPolicy);
|
|
35
|
+
if (inbound === 'closed' || group === 'closed')
|
|
36
|
+
return 'closed';
|
|
37
|
+
if (inbound === 'approval-required' || group === 'approval-required')
|
|
38
|
+
return 'approval-required';
|
|
39
|
+
return 'open';
|
|
40
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Metadata-only unread projection, shared by serving and the offline migration. */
|
|
2
|
+
export declare function computeEndpointUnreadProjection(principalId: string, conversation: Record<string, unknown> | undefined, member: Record<string, unknown> | undefined): {
|
|
3
|
+
unmutedAtMs: number;
|
|
4
|
+
} | null;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Metadata-only unread projection, shared by serving and the offline migration. */
|
|
2
|
+
export function computeEndpointUnreadProjection(principalId, conversation, member) {
|
|
3
|
+
if (!conversation || !member || !Array.isArray(conversation.memberIds) || !conversation.memberIds.includes(principalId))
|
|
4
|
+
return null;
|
|
5
|
+
const message = conversation.lastMessage;
|
|
6
|
+
if (!message || typeof message.senderId !== 'string' || message.senderId === principalId)
|
|
7
|
+
return null;
|
|
8
|
+
const at = time(message.timestamp);
|
|
9
|
+
if (at === null)
|
|
10
|
+
return null;
|
|
11
|
+
const floor = time(member.historyStartAt), read = time(member.lastReadTimestamp), deleted = time(member.deletedAt);
|
|
12
|
+
if ((floor !== null && at < floor) || (read !== null && at <= read) || (deleted !== null && at <= deleted))
|
|
13
|
+
return null;
|
|
14
|
+
return { unmutedAtMs: time(member.mutedUntil) ?? 0 };
|
|
15
|
+
}
|
|
16
|
+
function time(value) {
|
|
17
|
+
if (value === null || value === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
const ms = Date.parse(value);
|
|
21
|
+
return Number.isFinite(ms) ? ms : null;
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === 'number')
|
|
24
|
+
return Number.isFinite(value) ? value : null;
|
|
25
|
+
if (typeof value !== 'object')
|
|
26
|
+
return null;
|
|
27
|
+
const timestamp = value;
|
|
28
|
+
const ms = typeof timestamp.toMillis === 'function' ? timestamp.toMillis() : typeof timestamp.toDate === 'function' ? timestamp.toDate().getTime()
|
|
29
|
+
: typeof timestamp.seconds === 'number' ? timestamp.seconds * 1000 + (timestamp.nanoseconds ?? 0) / 1e6 : NaN;
|
|
30
|
+
return Number.isFinite(ms) ? ms : null;
|
|
31
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Plaintext endpoint protocol. Identity and authority come from authenticated transport. */
|
|
2
|
+
export declare const CANON_ENDPOINT_PROTOCOL: "canon.endpoint.v1";
|
|
3
|
+
export interface EndpointPreconditions {
|
|
4
|
+
conversationId?: string;
|
|
5
|
+
membershipRevision?: number;
|
|
6
|
+
admissionId?: string;
|
|
7
|
+
targetAdmissionId?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface EndpointOperationRequest {
|
|
10
|
+
protocol: typeof CANON_ENDPOINT_PROTOCOL;
|
|
11
|
+
operationId: string;
|
|
12
|
+
installationId: string;
|
|
13
|
+
action: string;
|
|
14
|
+
/** Agent ownership generation selected when authored; absent for humans. */
|
|
15
|
+
ownershipVersion?: number;
|
|
16
|
+
envelope?: Record<string, unknown>;
|
|
17
|
+
preconditions?: EndpointPreconditions;
|
|
18
|
+
body: {
|
|
19
|
+
encoding: 'json';
|
|
20
|
+
value: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface EndpointOperationError {
|
|
24
|
+
code: string;
|
|
25
|
+
message: string;
|
|
26
|
+
status?: number;
|
|
27
|
+
retryable?: boolean;
|
|
28
|
+
retryAfterMs?: number;
|
|
29
|
+
}
|
|
30
|
+
export type EndpointReceiptStatus = 'pending' | 'succeeded' | 'rejected' | 'uncertain';
|
|
31
|
+
export type EndpointResultReference = {
|
|
32
|
+
kind: 'interaction';
|
|
33
|
+
conversationId: string;
|
|
34
|
+
interactionKind: 'input' | 'approval' | 'card' | 'plan';
|
|
35
|
+
requestId: string;
|
|
36
|
+
operationKey: string;
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'upload';
|
|
39
|
+
uploadId: string;
|
|
40
|
+
} | {
|
|
41
|
+
kind: 'voice';
|
|
42
|
+
conversationId: string;
|
|
43
|
+
sessionId: string;
|
|
44
|
+
};
|
|
45
|
+
export interface EndpointOperationReceipt<T = unknown> {
|
|
46
|
+
protocol: typeof CANON_ENDPOINT_PROTOCOL;
|
|
47
|
+
operationId: string;
|
|
48
|
+
installationId: string;
|
|
49
|
+
action: string;
|
|
50
|
+
status: EndpointReceiptStatus;
|
|
51
|
+
result?: T;
|
|
52
|
+
/** Sensitive/bearer results are hydrated for the response, never journaled. */
|
|
53
|
+
resultReference?: EndpointResultReference;
|
|
54
|
+
/** Transient hydration failure: the durable mutation remains succeeded. */
|
|
55
|
+
resultError?: EndpointOperationError;
|
|
56
|
+
error?: EndpointOperationError;
|
|
57
|
+
createdAt: string;
|
|
58
|
+
updatedAt: string;
|
|
59
|
+
}
|
|
60
|
+
export interface EndpointQueryRequest {
|
|
61
|
+
protocol: typeof CANON_ENDPOINT_PROTOCOL;
|
|
62
|
+
query: string;
|
|
63
|
+
parameters: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
export interface EndpointQueryResponse<T = unknown> {
|
|
66
|
+
protocol: typeof CANON_ENDPOINT_PROTOCOL;
|
|
67
|
+
result: T;
|
|
68
|
+
}
|
|
69
|
+
/** Durable application cursors differ from the live transport's reconnect token. */
|
|
70
|
+
export interface EndpointEvent {
|
|
71
|
+
id: string;
|
|
72
|
+
kind: string;
|
|
73
|
+
conversationId?: string;
|
|
74
|
+
revision?: number;
|
|
75
|
+
cursor?: string;
|
|
76
|
+
durable: boolean;
|
|
77
|
+
data: Record<string, unknown>;
|
|
78
|
+
}
|
|
79
|
+
export interface EndpointCapabilities {
|
|
80
|
+
protocol: typeof CANON_ENDPOINT_PROTOCOL;
|
|
81
|
+
actions: string[];
|
|
82
|
+
queries: string[];
|
|
83
|
+
maintenance: boolean;
|
|
84
|
+
}
|
|
85
|
+
export declare const ENDPOINT_ID_PATTERN: RegExp;
|
|
86
|
+
export declare const ENDPOINT_ACTION_PATTERN: RegExp;
|
|
87
|
+
export declare function isEndpointOperationRequest(value: unknown): value is EndpointOperationRequest;
|
|
88
|
+
export declare function isEndpointOperationReceipt(value: unknown): value is EndpointOperationReceipt;
|
package/dist/endpoint.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Plaintext endpoint protocol. Identity and authority come from authenticated transport. */
|
|
2
|
+
export const CANON_ENDPOINT_PROTOCOL = 'canon.endpoint.v1';
|
|
3
|
+
export const ENDPOINT_ID_PATTERN = /^[A-Za-z0-9_.:~-]{1,200}$/;
|
|
4
|
+
export const ENDPOINT_ACTION_PATTERN = /^[a-z][a-z0-9_.-]{0,95}$/;
|
|
5
|
+
export function isEndpointOperationRequest(value) {
|
|
6
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
7
|
+
return false;
|
|
8
|
+
const request = value;
|
|
9
|
+
const body = request.body;
|
|
10
|
+
return request.protocol === CANON_ENDPOINT_PROTOCOL
|
|
11
|
+
&& typeof request.operationId === 'string' && ENDPOINT_ID_PATTERN.test(request.operationId)
|
|
12
|
+
&& typeof request.installationId === 'string' && ENDPOINT_ID_PATTERN.test(request.installationId)
|
|
13
|
+
&& typeof request.action === 'string' && ENDPOINT_ACTION_PATTERN.test(request.action)
|
|
14
|
+
&& (request.ownershipVersion === undefined || Number.isSafeInteger(request.ownershipVersion) && request.ownershipVersion >= 0)
|
|
15
|
+
&& !!body && body.encoding === 'json'
|
|
16
|
+
&& !!body.value && typeof body.value === 'object' && !Array.isArray(body.value);
|
|
17
|
+
}
|
|
18
|
+
export function isEndpointOperationReceipt(value) {
|
|
19
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
20
|
+
return false;
|
|
21
|
+
const receipt = value;
|
|
22
|
+
return receipt.protocol === CANON_ENDPOINT_PROTOCOL
|
|
23
|
+
&& typeof receipt.operationId === 'string' && ENDPOINT_ID_PATTERN.test(receipt.operationId)
|
|
24
|
+
&& typeof receipt.installationId === 'string' && ENDPOINT_ID_PATTERN.test(receipt.installationId)
|
|
25
|
+
&& typeof receipt.action === 'string' && ENDPOINT_ACTION_PATTERN.test(receipt.action)
|
|
26
|
+
&& ['pending', 'succeeded', 'rejected', 'uncertain'].includes(String(receipt.status))
|
|
27
|
+
&& typeof receipt.createdAt === 'string' && typeof receipt.updatedAt === 'string';
|
|
28
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Classification, not confidentiality: every enabled endpoint codec is JSON.
|
|
2
|
+
* Unknown fields are private content, including inside an `envelope`. Adding a
|
|
3
|
+
* lane requires review; adding content to an existing schema does not make it
|
|
4
|
+
* public. These definitions refer to existing payload schemas, not copies. */
|
|
5
|
+
export type EndpointContentClass = 'public-metadata' | 'private-content' | 'restricted-secret';
|
|
6
|
+
export type EndpointContentLane = 'operation.request' | 'operation.receipt' | 'query.request' | 'query.result' | 'ephemeral.request' | 'ephemeral.result' | 'event';
|
|
7
|
+
export declare const ENDPOINT_CONTENT_ACTIONS: readonly ["initialize_endpoint", "sync_phone_contacts", "request_work_session", "resolve_work_session_request", "update_agent_behavior_policy", "update_conversation_agent_behavior_policy", "send_message", "delete_message", "forward_message", "react", "verb_execute", "send_contextual_message", "create_interaction", "consume_interaction", "start_conversation", "create_conversation", "add_member", "remove_member", "leave_conversation", "update_member_role", "update_conversation", "rename_conversation", "update_topic", "update_group_avatar", "mark_read", "cancel_queued_message", "set_conversation_preferences", "set_block", "save_contact", "remove_contact", "respond_interaction", "session_control", "approve_contact_request", "reject_contact_request", "cancel_direct_request", "update_message_disposition", "create_upload", "finalize_upload", "delete_upload", "retry_video_processing", "finalize_memo_stream", "abort_memo_stream", "create_voice_session", "join_voice_session", "decline_voice_session", "end_voice_session", "consume_runtime_signal", "generate_image"];
|
|
8
|
+
export declare const ENDPOINT_CONTENT_FUNCTION_QUERIES: readonly ["discover_agents", "get_unread_summary", "peek_runtime_signal", "get_work_session_catalog", "get_work_session_request", "get_agent_behavior_policy", "get_conversation_agent_behavior_policy", "get_ephemeral", "list_voice_sessions", "list_contact_requests", "list_conversations", "list_messages", "get_messages", "admission_fences", "list_contacts", "get_contact", "get_members", "resolve_admission", "resolve_group_admission", "get_runtime_card_state", "conversation", "conversation_changes", "get_upload", "get_voice_session", "get_image_generation", "get_image_generation_status"];
|
|
9
|
+
export declare const ENDPOINT_CONTENT_QUERIES: readonly ["discover_agents", "get_unread_summary", "peek_runtime_signal", "get_work_session_catalog", "get_work_session_request", "get_agent_behavior_policy", "get_conversation_agent_behavior_policy", "get_ephemeral", "list_voice_sessions", "list_contact_requests", "list_conversations", "list_messages", "get_messages", "admission_fences", "list_contacts", "get_contact", "get_members", "resolve_admission", "resolve_group_admission", "get_runtime_card_state", "conversation", "conversation_changes", "get_upload", "get_voice_session", "get_image_generation", "get_image_generation_status", "admit_messages"];
|
|
10
|
+
export declare const ENDPOINT_CONTENT_EPHEMERALS: readonly ["create_memo_stream", "publish_presence", "publish_typing", "publish_streaming", "publish_turn", "publish_runtime_status", "append_memo_chunk"];
|
|
11
|
+
export declare const ENDPOINT_CONTENT_EPHEMERAL_FAMILIES: readonly ["typing", "streaming", "turn-state", "agent-session", "runtime-card-state", "memo-streams", "runtime-info", "runtime-activity", "runtime-suppression", "runtime-silence", "runtime-attention", "call-attention", "agent-runtime", "presence"];
|
|
12
|
+
export declare const ENDPOINT_CONTENT_EVENTS: readonly ["connected", "heartbeat", "agent.context", "replay.expired", "message.created", "message.snapshot", "message.updated", "message.deleted", "message.media_terminal", "conversation.updated", "conversation.changed", "conversation.removed", "conversations.changed", "contacts.changed", "contact.added", "contact.updated", "contact.removed", "presence", "typing", "runtime.updated", "runtime.control", "turn.updated", "voice.session.started", "voice.session.ended", ...string[]];
|
|
13
|
+
export declare class EndpointContentPolicyError extends Error {
|
|
14
|
+
readonly code = "ENDPOINT_CONTENT_UNCLASSIFIED";
|
|
15
|
+
constructor(lane: string, name: string);
|
|
16
|
+
}
|
|
17
|
+
export interface EndpointContentBoundary {
|
|
18
|
+
readonly lane: EndpointContentLane;
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly defaultClass: 'private-content';
|
|
21
|
+
/** Paths address the actual payload. Query/result/event paths are relative to
|
|
22
|
+
* their parameters/result/data; operation paths include body.value. */
|
|
23
|
+
classify(path: readonly string[]): EndpointContentClass;
|
|
24
|
+
}
|
|
25
|
+
/** Local journal policy is separate from wire classification. Input answers and
|
|
26
|
+
* card values can carry passwords even when the response omits the original
|
|
27
|
+
* request's secret/sudo/field metadata. Only reviewed response shapes without
|
|
28
|
+
* those value bags may be retried from durable storage. Unknown response fields
|
|
29
|
+
* fail closed; a caller's `sensitive: false` cannot weaken this policy. */
|
|
30
|
+
export declare function endpointOperationRequestRetention(action: string, body: Record<string, unknown>, envelope?: Record<string, unknown>): 'journal' | 'memory';
|
|
31
|
+
/** Validate the lane before accepting/transmitting its payload. No body values
|
|
32
|
+
* are logged, copied into metadata, or claimed to be encrypted by this API. */
|
|
33
|
+
export declare function endpointContentBoundary(lane: EndpointContentLane, name: string, payload?: unknown): EndpointContentBoundary;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { VERB_WIRE_ENVELOPE_FIELDS } from './verbWire.js';
|
|
2
|
+
export const ENDPOINT_CONTENT_ACTIONS = [
|
|
3
|
+
'initialize_endpoint',
|
|
4
|
+
'sync_phone_contacts', 'request_work_session', 'resolve_work_session_request',
|
|
5
|
+
'update_agent_behavior_policy', 'update_conversation_agent_behavior_policy',
|
|
6
|
+
'send_message', 'delete_message', 'forward_message', 'react', 'verb_execute',
|
|
7
|
+
'send_contextual_message', 'create_interaction', 'consume_interaction',
|
|
8
|
+
'start_conversation', 'create_conversation', 'add_member', 'remove_member',
|
|
9
|
+
'leave_conversation', 'update_member_role', 'update_conversation', 'rename_conversation',
|
|
10
|
+
'update_topic', 'update_group_avatar', 'mark_read', 'cancel_queued_message',
|
|
11
|
+
'set_conversation_preferences', 'set_block', 'save_contact', 'remove_contact',
|
|
12
|
+
'respond_interaction', 'session_control', 'approve_contact_request',
|
|
13
|
+
'reject_contact_request', 'cancel_direct_request', 'update_message_disposition',
|
|
14
|
+
'create_upload', 'finalize_upload', 'delete_upload', 'retry_video_processing',
|
|
15
|
+
'finalize_memo_stream', 'abort_memo_stream', 'create_voice_session',
|
|
16
|
+
'join_voice_session', 'decline_voice_session', 'end_voice_session',
|
|
17
|
+
'consume_runtime_signal', 'generate_image',
|
|
18
|
+
];
|
|
19
|
+
export const ENDPOINT_CONTENT_FUNCTION_QUERIES = [
|
|
20
|
+
'discover_agents', 'get_unread_summary', 'peek_runtime_signal',
|
|
21
|
+
'get_work_session_catalog', 'get_work_session_request', 'get_agent_behavior_policy',
|
|
22
|
+
'get_conversation_agent_behavior_policy', 'get_ephemeral', 'list_voice_sessions',
|
|
23
|
+
'list_contact_requests', 'list_conversations', 'list_messages', 'get_messages',
|
|
24
|
+
'admission_fences', 'list_contacts', 'get_contact', 'get_members', 'resolve_admission',
|
|
25
|
+
'resolve_group_admission', 'get_runtime_card_state', 'conversation',
|
|
26
|
+
'conversation_changes', 'get_upload', 'get_voice_session', 'get_image_generation',
|
|
27
|
+
'get_image_generation_status',
|
|
28
|
+
];
|
|
29
|
+
export const ENDPOINT_CONTENT_QUERIES = [...ENDPOINT_CONTENT_FUNCTION_QUERIES, 'admit_messages'];
|
|
30
|
+
export const ENDPOINT_CONTENT_EPHEMERALS = [
|
|
31
|
+
'create_memo_stream', 'publish_presence', 'publish_typing', 'publish_streaming',
|
|
32
|
+
'publish_turn', 'publish_runtime_status', 'append_memo_chunk',
|
|
33
|
+
];
|
|
34
|
+
export const ENDPOINT_CONTENT_EPHEMERAL_FAMILIES = [
|
|
35
|
+
'typing', 'streaming', 'turn-state', 'agent-session', 'runtime-card-state',
|
|
36
|
+
'memo-streams', 'runtime-info', 'runtime-activity', 'runtime-suppression',
|
|
37
|
+
'runtime-silence', 'runtime-attention', 'call-attention', 'agent-runtime', 'presence',
|
|
38
|
+
];
|
|
39
|
+
export const ENDPOINT_CONTENT_EVENTS = [
|
|
40
|
+
'connected', 'heartbeat', 'agent.context', 'replay.expired',
|
|
41
|
+
'message.created', 'message.snapshot', 'message.updated', 'message.deleted',
|
|
42
|
+
'message.media_terminal', 'conversation.updated', 'conversation.changed',
|
|
43
|
+
'conversation.removed', 'conversations.changed', 'contacts.changed',
|
|
44
|
+
'contact.added', 'contact.updated', 'contact.removed', 'presence', 'typing',
|
|
45
|
+
'runtime.updated', 'runtime.control', 'turn.updated',
|
|
46
|
+
'voice.session.started', 'voice.session.ended',
|
|
47
|
+
...ENDPOINT_CONTENT_EPHEMERAL_FAMILIES.map((family) => `ephemeral.${family}`),
|
|
48
|
+
];
|
|
49
|
+
export class EndpointContentPolicyError extends Error {
|
|
50
|
+
code = 'ENDPOINT_CONTENT_UNCLASSIFIED';
|
|
51
|
+
constructor(lane, name) {
|
|
52
|
+
super(`Unclassified endpoint lane: ${lane}/${name}`);
|
|
53
|
+
this.name = 'EndpointContentPolicyError';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const namesForLane = {
|
|
57
|
+
'operation.request': ENDPOINT_CONTENT_ACTIONS,
|
|
58
|
+
'operation.receipt': ENDPOINT_CONTENT_ACTIONS,
|
|
59
|
+
'query.request': ENDPOINT_CONTENT_QUERIES,
|
|
60
|
+
'query.result': ENDPOINT_CONTENT_QUERIES,
|
|
61
|
+
'ephemeral.request': ENDPOINT_CONTENT_EPHEMERALS,
|
|
62
|
+
'ephemeral.result': ENDPOINT_CONTENT_EPHEMERALS,
|
|
63
|
+
event: ENDPOINT_CONTENT_EVENTS,
|
|
64
|
+
};
|
|
65
|
+
/** An explicit allowlist of routing/authorization fields, not a rule that all
|
|
66
|
+
* fields named `id` are public. Free text, previews, labels, media descriptors,
|
|
67
|
+
* paths, provider configuration, native snapshots and arbitrary metadata stay
|
|
68
|
+
* private. IDs/cursors/timing still reveal a communication graph. */
|
|
69
|
+
const protocolPaths = [
|
|
70
|
+
'protocol', 'operationId', 'installationId', 'action', 'ownershipVersion',
|
|
71
|
+
'preconditions.conversationId', 'preconditions.membershipRevision',
|
|
72
|
+
'preconditions.admissionId', 'preconditions.targetAdmissionId', 'body.encoding',
|
|
73
|
+
];
|
|
74
|
+
// Fields are reviewed for a particular lane: e.g. an unknown `contactId` added
|
|
75
|
+
// to send_message is still private, despite being a routing ID elsewhere.
|
|
76
|
+
const actionRouting = {
|
|
77
|
+
send_message: ['conversationId', 'messageId', 'replyTo', 'mentions.*'],
|
|
78
|
+
delete_message: ['conversationId', 'messageId'], forward_message: ['sourceConversationId', 'targetConversationId', 'messageId'],
|
|
79
|
+
react: ['conversationId', 'messageId'], send_contextual_message: ['conversationId'],
|
|
80
|
+
create_interaction: ['conversationId', 'requestId', 'inputId', 'approvalId', 'cardId', 'planId', 'responseUserId'],
|
|
81
|
+
consume_interaction: ['conversationId', 'inputId', 'approvalId', 'cardId', 'planId'],
|
|
82
|
+
respond_interaction: ['conversationId', 'requestId'],
|
|
83
|
+
start_conversation: ['targetUserId'], create_conversation: ['memberIds.*'],
|
|
84
|
+
add_member: ['conversationId', 'userId', 'agentId'], remove_member: ['conversationId', 'userId', 'agentId', 'targetUserId'],
|
|
85
|
+
leave_conversation: ['conversationId'], update_member_role: ['conversationId', 'userId', 'targetUserId'],
|
|
86
|
+
update_conversation: ['conversationId'], rename_conversation: ['conversationId'], update_topic: ['conversationId'],
|
|
87
|
+
update_group_avatar: ['conversationId'], mark_read: ['conversationId'], cancel_queued_message: ['conversationId', 'messageId'],
|
|
88
|
+
set_conversation_preferences: ['conversationId'], set_block: ['targetUserId'], save_contact: ['contactUserId'], remove_contact: ['contactId', 'contactUserId'],
|
|
89
|
+
session_control: ['conversationId', 'agentId'], approve_contact_request: ['requestId'], reject_contact_request: ['requestId'], cancel_direct_request: ['requestId'],
|
|
90
|
+
update_message_disposition: ['conversationId', 'messageId'], create_upload: ['conversationId'], finalize_upload: ['uploadId'], delete_upload: ['uploadId'], retry_video_processing: ['uploadId'],
|
|
91
|
+
finalize_memo_stream: ['conversationId', 'streamId'], abort_memo_stream: ['streamId'],
|
|
92
|
+
create_voice_session: ['conversationId'], join_voice_session: ['conversationId', 'sessionId'], decline_voice_session: ['conversationId', 'sessionId'], end_voice_session: ['conversationId', 'sessionId'],
|
|
93
|
+
consume_runtime_signal: ['conversationId', 'expectedSignalId'], generate_image: ['conversationId', 'requestId'],
|
|
94
|
+
request_work_session: ['conversationId', 'agentId', 'requestId'], resolve_work_session_request: ['requestId'],
|
|
95
|
+
update_agent_behavior_policy: ['agentId'], update_conversation_agent_behavior_policy: ['conversationId', 'agentId'],
|
|
96
|
+
};
|
|
97
|
+
const queryRouting = {
|
|
98
|
+
admit_messages: ['conversationId', 'messageIds.*', 'admissionId'], conversation_changes: ['conversationId', 'cursor', 'limit'],
|
|
99
|
+
conversation: ['conversationId'], list_conversations: ['limit', 'before'], list_messages: ['conversationId', 'before', 'limit'], get_messages: ['conversationId', 'messageIds.*'],
|
|
100
|
+
list_contacts: ['limit', 'before'], get_contact: ['contactId', 'contactUserId', 'userId'], get_members: ['conversationId'],
|
|
101
|
+
list_contact_requests: ['direction', 'limit', 'before'], admission_fences: ['conversationId', 'targetUserId'],
|
|
102
|
+
resolve_admission: ['targetUserId'], resolve_group_admission: ['conversationId', 'targetUserId'], get_runtime_card_state: ['conversationId', 'cardId'],
|
|
103
|
+
get_ephemeral: ['family', 'scopeId'], peek_runtime_signal: ['conversationId'], list_voice_sessions: ['conversationId', 'limit'],
|
|
104
|
+
get_upload: ['uploadId'], get_voice_session: ['conversationId', 'sessionId'], get_image_generation: ['requestId'],
|
|
105
|
+
get_work_session_catalog: ['agentId'], get_work_session_request: ['requestId'], get_agent_behavior_policy: ['agentId'], get_conversation_agent_behavior_policy: ['conversationId', 'agentId'],
|
|
106
|
+
};
|
|
107
|
+
const transientRouting = {
|
|
108
|
+
create_memo_stream: ['conversationId', 'streamId'], append_memo_chunk: ['streamId'],
|
|
109
|
+
publish_presence: [], publish_typing: ['conversationId'], publish_streaming: ['conversationId'], publish_turn: ['conversationId'], publish_runtime_status: ['conversationId'],
|
|
110
|
+
};
|
|
111
|
+
function eventRouting(name) {
|
|
112
|
+
if (name.startsWith('ephemeral.'))
|
|
113
|
+
return ['conversationId', 'scopeId'];
|
|
114
|
+
if (name === 'connected')
|
|
115
|
+
return ['protocol', 'connectionId', 'agentId', 'principalId', 'principalType'];
|
|
116
|
+
if (name === 'heartbeat')
|
|
117
|
+
return ['timestamp'];
|
|
118
|
+
if (name === 'agent.context')
|
|
119
|
+
return ['agentId', 'ownerId', 'ownershipVersion'];
|
|
120
|
+
if (name === 'conversations.changed')
|
|
121
|
+
return ['conversationIds.*', 'removedConversationIds.*'];
|
|
122
|
+
if (name.startsWith('message.'))
|
|
123
|
+
return ['conversationId', 'messageId', 'eventId', 'id', 'message.id', 'message.conversationId', 'message.senderId', 'message.senderType', 'message.createdAt', 'message.replyTo', 'message.mentions.*'];
|
|
124
|
+
if (name.startsWith('conversation.'))
|
|
125
|
+
return ['conversationId'];
|
|
126
|
+
if (name.startsWith('contact.'))
|
|
127
|
+
return ['contactId'];
|
|
128
|
+
if (name.startsWith('voice.session.'))
|
|
129
|
+
return ['conversationId', 'sessionId'];
|
|
130
|
+
if (/^(runtime\.|turn\.|typing$|presence$)/.test(name))
|
|
131
|
+
return ['conversationId', 'agentId', 'userId', 'memberId'];
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const secretSegments = new Set(['token', 'accessToken', 'refreshToken', 'uploadUrl', 'signedUrl', 'livekitToken', 'replyAuthority', 'credentials', 'password', 'secret', 'authorization', 'answer', 'answers', 'response', 'submittedValue']);
|
|
135
|
+
const matches = (path, pattern) => {
|
|
136
|
+
const parts = pattern.split('.');
|
|
137
|
+
return parts.length === path.length && parts.every((part, index) => part === path[index] || part === '*' && /^\d+$/.test(path[index]));
|
|
138
|
+
};
|
|
139
|
+
const publicMatch = (path, patterns) => patterns.some((pattern) => matches(path, pattern));
|
|
140
|
+
/** Local journal policy is separate from wire classification. Input answers and
|
|
141
|
+
* card values can carry passwords even when the response omits the original
|
|
142
|
+
* request's secret/sudo/field metadata. Only reviewed response shapes without
|
|
143
|
+
* those value bags may be retried from durable storage. Unknown response fields
|
|
144
|
+
* fail closed; a caller's `sensitive: false` cannot weaken this policy. */
|
|
145
|
+
export function endpointOperationRequestRetention(action, body, envelope) {
|
|
146
|
+
if (action !== 'respond_interaction')
|
|
147
|
+
return 'journal';
|
|
148
|
+
// Interaction services use the body exclusively. Unknown envelope additions
|
|
149
|
+
// must not bypass retention, including conflicting fields rejected by server.
|
|
150
|
+
if (envelope && Object.keys(envelope).length > 0)
|
|
151
|
+
return 'memory';
|
|
152
|
+
const routing = ['kind', 'conversationId', 'agentId', 'requestId'];
|
|
153
|
+
const hasOnly = (fields) => Object.keys(body).every((field) => fields.includes(field));
|
|
154
|
+
if (routing.some((field) => body[field] !== undefined && typeof body[field] !== 'string'))
|
|
155
|
+
return 'memory';
|
|
156
|
+
if ((body.kind === 'input' || body.kind === 'card') && hasOnly([...routing, 'status', 'actionId'])) {
|
|
157
|
+
if (!['submitted', 'cancelled'].includes(String(body.status)) || (body.actionId !== undefined && typeof body.actionId !== 'string'))
|
|
158
|
+
return 'memory';
|
|
159
|
+
// A card button or cancellation with no submitted values contains no answer
|
|
160
|
+
// plaintext. Input/card requests with value, answers or values stay in RAM,
|
|
161
|
+
// including non-secret clarification: that distinction is absent on wire.
|
|
162
|
+
return 'journal';
|
|
163
|
+
}
|
|
164
|
+
if (body.kind === 'approval' && hasOnly([...routing, 'decision', 'sessionRule'])) {
|
|
165
|
+
if (body.decision !== 'allow' && body.decision !== 'deny')
|
|
166
|
+
return 'memory';
|
|
167
|
+
const rule = body.sessionRule;
|
|
168
|
+
if (rule !== undefined && (!rule || typeof rule !== 'object' || Array.isArray(rule)
|
|
169
|
+
|| !Object.entries(rule).every(([field, value]) => ['type', 'toolPattern', 'expiresAt'].includes(field)
|
|
170
|
+
&& (typeof value === 'string' || field === 'expiresAt' && value === null))))
|
|
171
|
+
return 'memory';
|
|
172
|
+
return 'journal';
|
|
173
|
+
}
|
|
174
|
+
if (body.kind === 'plan' && hasOnly([...routing, 'decision', 'feedback', 'grantedPrompts'])) {
|
|
175
|
+
if (!['approve', 'revise', 'reject'].includes(String(body.decision)) || body.feedback !== undefined && typeof body.feedback !== 'string')
|
|
176
|
+
return 'memory';
|
|
177
|
+
// Plan feedback and granted tool prompts are ordinary authored instructions,
|
|
178
|
+
// not secret-entry fields. Preserve their established offline semantics.
|
|
179
|
+
const prompts = body.grantedPrompts;
|
|
180
|
+
if (prompts !== undefined && (!Array.isArray(prompts) || prompts.some((prompt) => !prompt || typeof prompt !== 'object'
|
|
181
|
+
|| Array.isArray(prompt) || !Object.entries(prompt).every(([field, value]) => ['tool', 'prompt'].includes(field) && typeof value === 'string'))))
|
|
182
|
+
return 'memory';
|
|
183
|
+
return 'journal';
|
|
184
|
+
}
|
|
185
|
+
return 'memory';
|
|
186
|
+
}
|
|
187
|
+
/** Validate the lane before accepting/transmitting its payload. No body values
|
|
188
|
+
* are logged, copied into metadata, or claimed to be encrypted by this API. */
|
|
189
|
+
export function endpointContentBoundary(lane, name, payload) {
|
|
190
|
+
if (!namesForLane[lane]?.includes(name))
|
|
191
|
+
throw new EndpointContentPolicyError(lane, name);
|
|
192
|
+
const record = payload && typeof payload === 'object' ? payload : undefined;
|
|
193
|
+
if (lane === 'query.request' && name === 'get_ephemeral' && record && !ENDPOINT_CONTENT_EPHEMERAL_FAMILIES.includes(record.family)) {
|
|
194
|
+
throw new EndpointContentPolicyError(lane, `${name}/unknown-family`);
|
|
195
|
+
}
|
|
196
|
+
const requestRoutingPaths = (actionRouting[name] ?? []).flatMap((field) => [`body.value.${field}`, `envelope.${field}`]);
|
|
197
|
+
const classify = (path) => {
|
|
198
|
+
if (path.some((part) => secretSegments.has(part)))
|
|
199
|
+
return 'restricted-secret';
|
|
200
|
+
if (lane === 'operation.request' && ['respond_interaction', 'sync_phone_contacts'].includes(name) && path[0] === 'body' && path[1] === 'value' && !publicMatch(path, requestRoutingPaths))
|
|
201
|
+
return 'restricted-secret';
|
|
202
|
+
if (lane === 'operation.receipt' && path[0] === 'result' && (['consume_interaction', 'respond_interaction'].includes(name) || record?.resultReference?.kind === 'interaction'))
|
|
203
|
+
return 'restricted-secret';
|
|
204
|
+
let selected = payload;
|
|
205
|
+
for (const part of path)
|
|
206
|
+
selected = selected && typeof selected === 'object' && Object.hasOwn(selected, part) ? selected[part] : undefined;
|
|
207
|
+
// An allowed scalar path cannot make arbitrary nested content public.
|
|
208
|
+
if (selected !== null && typeof selected === 'object')
|
|
209
|
+
return 'private-content';
|
|
210
|
+
if (lane === 'operation.request') {
|
|
211
|
+
if (publicMatch(path, protocolPaths) || publicMatch(path, requestRoutingPaths))
|
|
212
|
+
return 'public-metadata';
|
|
213
|
+
// All response content is treated as restricted, even for a non-secret
|
|
214
|
+
// interaction; classification cannot depend on a stale local kind flag.
|
|
215
|
+
if (name === 'respond_interaction' && path[0] === 'body' && path[1] === 'value')
|
|
216
|
+
return 'restricted-secret';
|
|
217
|
+
if (name === 'sync_phone_contacts' && path[0] === 'body' && path[1] === 'value')
|
|
218
|
+
return 'restricted-secret';
|
|
219
|
+
if (name === 'verb_execute') {
|
|
220
|
+
const wire = record?.body?.value;
|
|
221
|
+
const spec = VERB_WIRE_ENVELOPE_FIELDS[wire?.verb];
|
|
222
|
+
if (path[0] === 'body' && path[1] === 'value') {
|
|
223
|
+
if (path.length === 3 && ['wire', 'verb'].includes(path[2]))
|
|
224
|
+
return 'public-metadata';
|
|
225
|
+
if (path[2] === 'envelope' && spec) {
|
|
226
|
+
const field = path[3];
|
|
227
|
+
const allowed = [...spec.required, ...spec.optional];
|
|
228
|
+
// Reuse verbWire's existing split. Nested metadata is deliberately
|
|
229
|
+
// conservative: only declared scalar fields and numeric array IDs.
|
|
230
|
+
if (field && allowed.includes(field) && (path.length === 4 || path.length === 5 && ['mentions', 'memberIds'].includes(field) && /^\d+$/.test(path[4])))
|
|
231
|
+
return 'public-metadata';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else if (lane === 'operation.receipt') {
|
|
237
|
+
if (publicMatch(path, [...protocolPaths, 'status', 'createdAt', 'updatedAt', 'error.code', 'error.status', 'error.retryable', 'error.retryAfterMs', 'resultError.code', 'resultError.status', 'resultReference.kind', 'resultReference.conversationId', 'resultReference.uploadId', 'resultReference.sessionId', 'resultReference.requestId']))
|
|
238
|
+
return 'public-metadata';
|
|
239
|
+
if (path[0] === 'result' && (name === 'consume_interaction' || name === 'respond_interaction' || record?.resultReference?.kind === 'interaction'))
|
|
240
|
+
return 'restricted-secret';
|
|
241
|
+
}
|
|
242
|
+
else if (lane === 'query.request') {
|
|
243
|
+
if (publicMatch(path, queryRouting[name] ?? []))
|
|
244
|
+
return 'public-metadata';
|
|
245
|
+
}
|
|
246
|
+
else if (lane === 'query.result' || lane === 'ephemeral.result') {
|
|
247
|
+
if (name === 'conversation_changes' && publicMatch(path, ['cursor', 'hasMore', 'reset', 'admissionId']))
|
|
248
|
+
return 'public-metadata';
|
|
249
|
+
if (name === 'get_ephemeral' && path[0] === 'value')
|
|
250
|
+
return 'private-content';
|
|
251
|
+
}
|
|
252
|
+
else if (lane === 'ephemeral.request') {
|
|
253
|
+
if (publicMatch(path, transientRouting[name] ?? []))
|
|
254
|
+
return 'public-metadata';
|
|
255
|
+
}
|
|
256
|
+
else if (lane === 'event') {
|
|
257
|
+
if (publicMatch(path, eventRouting(name)))
|
|
258
|
+
return 'public-metadata';
|
|
259
|
+
}
|
|
260
|
+
return 'private-content';
|
|
261
|
+
};
|
|
262
|
+
return Object.freeze({ lane, name, defaultClass: 'private-content', classify });
|
|
263
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -21,3 +21,8 @@ export * from './selfContext.js';
|
|
|
21
21
|
export * from './replyAuthority.js';
|
|
22
22
|
export * from './runtimeDescriptor.js';
|
|
23
23
|
export * from './workSessions.js';
|
|
24
|
+
export * from './conversationMembership.js';
|
|
25
|
+
export * from './endpoint.js';
|
|
26
|
+
export * from './endpointContentPolicy.js';
|
|
27
|
+
export * from './plaintextProcessors.js';
|
|
28
|
+
export { computeEndpointUnreadProjection } from './endpoint-unread.js';
|
package/dist/index.js
CHANGED
|
@@ -21,3 +21,8 @@ export * from './selfContext.js';
|
|
|
21
21
|
export * from './replyAuthority.js';
|
|
22
22
|
export * from './runtimeDescriptor.js';
|
|
23
23
|
export * from './workSessions.js';
|
|
24
|
+
export * from './conversationMembership.js';
|
|
25
|
+
export * from './endpoint.js';
|
|
26
|
+
export * from './endpointContentPolicy.js';
|
|
27
|
+
export * from './plaintextProcessors.js';
|
|
28
|
+
export { computeEndpointUnreadProjection } from './endpoint-unread.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Existing server content readers. Classification is not encryption: these
|
|
2
|
+
* processors intentionally inspect plaintext in the current JSON release.
|
|
3
|
+
* Their input boundary must be redesigned before enabling encrypted rooms. */
|
|
4
|
+
export declare const PLAINTEXT_PROCESSORS: {
|
|
5
|
+
readonly preview: {
|
|
6
|
+
readonly current: "Read message/display text to derive conversation and notification previews.";
|
|
7
|
+
readonly beforeMls: "Derive on authorized endpoints; clear/reconcile deleted-message previews there.";
|
|
8
|
+
};
|
|
9
|
+
readonly push: {
|
|
10
|
+
readonly current: "Send visible titles and message previews to FCM/APNs.";
|
|
11
|
+
readonly beforeMls: "Use generic wake notifications or endpoint-decrypted notification extensions.";
|
|
12
|
+
};
|
|
13
|
+
readonly topic: {
|
|
14
|
+
readonly current: "Derive the initial topic from message text.";
|
|
15
|
+
readonly beforeMls: "Derive on an authorized endpoint and publish as encrypted conversation content.";
|
|
16
|
+
};
|
|
17
|
+
readonly forward: {
|
|
18
|
+
readonly current: "Read source content and copy plaintext attachments into the target room.";
|
|
19
|
+
readonly beforeMls: "Decrypt and re-encrypt on an authorized endpoint; keep server target admission checks.";
|
|
20
|
+
};
|
|
21
|
+
readonly message_validation: {
|
|
22
|
+
readonly current: "Inspect text, attachment descriptors and rich content before storing messages.";
|
|
23
|
+
readonly beforeMls: "Validate decrypted content on endpoints; retain bounded ciphertext/envelope checks on the server.";
|
|
24
|
+
};
|
|
25
|
+
readonly interaction_validation: {
|
|
26
|
+
readonly current: "Validate card/input/approval/plan render contracts and submitted responses.";
|
|
27
|
+
readonly beforeMls: "Validate content on authorized requesting/responding endpoints; retain public admission and lifecycle checks.";
|
|
28
|
+
};
|
|
29
|
+
readonly image_generation: {
|
|
30
|
+
readonly current: "Send prompts/source image bytes to Vertex and persist generated output.";
|
|
31
|
+
readonly beforeMls: "Require explicit provider disclosure and an authorized endpoint/processing participant; no silent server decryption.";
|
|
32
|
+
};
|
|
33
|
+
readonly video_processing: {
|
|
34
|
+
readonly current: "Download plaintext video, normalize it and create a poster.";
|
|
35
|
+
readonly beforeMls: "Process on an endpoint or an explicitly authorized processing participant before encrypted upload.";
|
|
36
|
+
};
|
|
37
|
+
readonly owner_observation: {
|
|
38
|
+
readonly current: "Allow authenticated owners to inspect managed agent conversations without room membership.";
|
|
39
|
+
readonly beforeMls: "Explicitly enroll authorized owner keyholders or retire content observation; ownership alone supplies no decryption keys.";
|
|
40
|
+
};
|
|
41
|
+
readonly moderation_evidence: {
|
|
42
|
+
readonly current: "Accept user-selected plaintext message evidence for moderation.";
|
|
43
|
+
readonly beforeMls: "Design explicit selective disclosure/authenticity (including franking policy); never infer access to room keys.";
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export type PlaintextProcessorName = keyof typeof PLAINTEXT_PROCESSORS;
|
|
47
|
+
export interface PlaintextProcessorInput<T> {
|
|
48
|
+
encoding: 'json';
|
|
49
|
+
value: T;
|
|
50
|
+
}
|
|
51
|
+
/** The callback is the actual content processor, not a telemetry hook. Keeping
|
|
52
|
+
* the codec explicit ensures an opaque body cannot accidentally be processed by
|
|
53
|
+
* the JSON path. This is not authorization; callers still enforce their domain
|
|
54
|
+
* policy and every enabled transport currently requires JSON. */
|
|
55
|
+
export declare function runPlaintextProcessor<T, R>(name: PlaintextProcessorName, input: PlaintextProcessorInput<T>, process: (value: T) => R): R;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Existing server content readers. Classification is not encryption: these
|
|
2
|
+
* processors intentionally inspect plaintext in the current JSON release.
|
|
3
|
+
* Their input boundary must be redesigned before enabling encrypted rooms. */
|
|
4
|
+
export const PLAINTEXT_PROCESSORS = {
|
|
5
|
+
preview: { current: 'Read message/display text to derive conversation and notification previews.', beforeMls: 'Derive on authorized endpoints; clear/reconcile deleted-message previews there.' },
|
|
6
|
+
push: { current: 'Send visible titles and message previews to FCM/APNs.', beforeMls: 'Use generic wake notifications or endpoint-decrypted notification extensions.' },
|
|
7
|
+
topic: { current: 'Derive the initial topic from message text.', beforeMls: 'Derive on an authorized endpoint and publish as encrypted conversation content.' },
|
|
8
|
+
forward: { current: 'Read source content and copy plaintext attachments into the target room.', beforeMls: 'Decrypt and re-encrypt on an authorized endpoint; keep server target admission checks.' },
|
|
9
|
+
message_validation: { current: 'Inspect text, attachment descriptors and rich content before storing messages.', beforeMls: 'Validate decrypted content on endpoints; retain bounded ciphertext/envelope checks on the server.' },
|
|
10
|
+
interaction_validation: { current: 'Validate card/input/approval/plan render contracts and submitted responses.', beforeMls: 'Validate content on authorized requesting/responding endpoints; retain public admission and lifecycle checks.' },
|
|
11
|
+
image_generation: { current: 'Send prompts/source image bytes to Vertex and persist generated output.', beforeMls: 'Require explicit provider disclosure and an authorized endpoint/processing participant; no silent server decryption.' },
|
|
12
|
+
video_processing: { current: 'Download plaintext video, normalize it and create a poster.', beforeMls: 'Process on an endpoint or an explicitly authorized processing participant before encrypted upload.' },
|
|
13
|
+
owner_observation: { current: 'Allow authenticated owners to inspect managed agent conversations without room membership.', beforeMls: 'Explicitly enroll authorized owner keyholders or retire content observation; ownership alone supplies no decryption keys.' },
|
|
14
|
+
moderation_evidence: { current: 'Accept user-selected plaintext message evidence for moderation.', beforeMls: 'Design explicit selective disclosure/authenticity (including franking policy); never infer access to room keys.' },
|
|
15
|
+
};
|
|
16
|
+
/** The callback is the actual content processor, not a telemetry hook. Keeping
|
|
17
|
+
* the codec explicit ensures an opaque body cannot accidentally be processed by
|
|
18
|
+
* the JSON path. This is not authorization; callers still enforce their domain
|
|
19
|
+
* policy and every enabled transport currently requires JSON. */
|
|
20
|
+
export function runPlaintextProcessor(name, input, process) {
|
|
21
|
+
if (!Object.hasOwn(PLAINTEXT_PROCESSORS, name) || input.encoding !== 'json') {
|
|
22
|
+
throw new Error('PLAINTEXT_PROCESSOR_CODEC_REQUIRED');
|
|
23
|
+
}
|
|
24
|
+
return process(input.value);
|
|
25
|
+
}
|
package/dist/replyAuthority.d.ts
CHANGED
package/dist/verbContract.d.ts
CHANGED
|
@@ -580,8 +580,8 @@ export interface ForwardResult {
|
|
|
580
580
|
forwardedFrom?: unknown;
|
|
581
581
|
}
|
|
582
582
|
export interface CreateGroupInput {
|
|
583
|
-
/**
|
|
584
|
-
name
|
|
583
|
+
/** Optional conversation title (authoring cap VERB_LIMITS.groupNameChars). */
|
|
584
|
+
name?: string;
|
|
585
585
|
/** Other members; the caller is added automatically. Cap
|
|
586
586
|
* VERB_LIMITS.groupMembers including the creator. Each target's
|
|
587
587
|
* groupJoinPolicy is enforced server-side. */
|
|
@@ -658,6 +658,17 @@ export interface VerbConversationSummary {
|
|
|
658
658
|
name?: string | null;
|
|
659
659
|
topic: string | null;
|
|
660
660
|
memberIds: string[];
|
|
661
|
+
membershipModel?: 'unified';
|
|
662
|
+
membershipRevision?: number;
|
|
663
|
+
participantTypes?: Record<string, 'human' | 'ai_agent'>;
|
|
664
|
+
participantSummary?: {
|
|
665
|
+
humanCount: number;
|
|
666
|
+
agentCount: number;
|
|
667
|
+
totalCount: number;
|
|
668
|
+
};
|
|
669
|
+
runtimeSessionKind?: 'direct' | 'group';
|
|
670
|
+
admissionId?: string | null;
|
|
671
|
+
historyStartAt?: string | null;
|
|
661
672
|
isAgentChat: boolean;
|
|
662
673
|
hasUnread?: boolean;
|
|
663
674
|
lastMessage: {
|
package/dist/verbSchemas.d.ts
CHANGED
|
@@ -1298,7 +1298,7 @@ export declare const CANON_VERBS_JSON_SCHEMA: {
|
|
|
1298
1298
|
readonly create_group_input: {
|
|
1299
1299
|
readonly type: "object";
|
|
1300
1300
|
readonly description: string;
|
|
1301
|
-
readonly required: readonly ["
|
|
1301
|
+
readonly required: readonly ["memberIds"];
|
|
1302
1302
|
readonly additionalProperties: false;
|
|
1303
1303
|
readonly properties: {
|
|
1304
1304
|
readonly name: {
|
package/dist/verbSchemas.js
CHANGED
|
@@ -782,7 +782,7 @@ const create_group_input = {
|
|
|
782
782
|
+ 'pending invite. Under MLS, membership '
|
|
783
783
|
+ 'changes are Add/Remove proposals + Commit — a group operation is a '
|
|
784
784
|
+ 'cryptographic state change, not a codec swap.',
|
|
785
|
-
required: ['
|
|
785
|
+
required: ['memberIds'],
|
|
786
786
|
additionalProperties: false,
|
|
787
787
|
properties: {
|
|
788
788
|
name: { type: 'string', minLength: 1, maxLength: VERB_LIMITS.groupNameChars },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/backend-contracts",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.6.0",
|
|
4
4
|
"description": "Canon backend contract helpers shared by Functions and stream-service",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@canonmsg/rich-cards": "^0.10.
|
|
41
|
+
"@canonmsg/rich-cards": "^0.10.6",
|
|
42
42
|
"@types/node": "^22.0.0",
|
|
43
43
|
"ajv": "^8.20.0",
|
|
44
44
|
"typescript": "~5.7.0",
|