@canonmsg/backend-contracts 8.4.0 → 8.5.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.
@@ -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
+ }
package/dist/cjs/index.js CHANGED
@@ -37,3 +37,4 @@ __exportStar(require("./selfContext.js"), exports);
37
37
  __exportStar(require("./replyAuthority.js"), exports);
38
38
  __exportStar(require("./runtimeDescriptor.js"), exports);
39
39
  __exportStar(require("./workSessions.js"), exports);
40
+ __exportStar(require("./conversationMembership.js"), exports);
@@ -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: ['name', 'memberIds'],
790
+ required: ['memberIds'],
791
791
  additionalProperties: false,
792
792
  properties: {
793
793
  name: { type: 'string', minLength: 1, maxLength: verbContract_js_1.VERB_LIMITS.groupNameChars },
@@ -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: string;
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
+ }
package/dist/index.d.ts CHANGED
@@ -21,3 +21,4 @@ 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';
package/dist/index.js CHANGED
@@ -21,3 +21,4 @@ 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';
@@ -9,4 +9,6 @@ export interface AgentReplyAuthorityV1 {
9
9
  sourceMessageId: string;
10
10
  token: string;
11
11
  expiresAt: string;
12
+ /** Agent membership admission at issuance; absent/null only for legacy membership. */
13
+ admissionId?: string | null;
12
14
  }
@@ -580,8 +580,8 @@ export interface ForwardResult {
580
580
  forwardedFrom?: unknown;
581
581
  }
582
582
  export interface CreateGroupInput {
583
- /** Group name (authoring cap VERB_LIMITS.groupNameChars). */
584
- name: string;
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: {
@@ -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 ["name", "memberIds"];
1301
+ readonly required: readonly ["memberIds"];
1302
1302
  readonly additionalProperties: false;
1303
1303
  readonly properties: {
1304
1304
  readonly name: {
@@ -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: ['name', 'memberIds'],
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.4.0",
3
+ "version": "8.5.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",