@canonmsg/backend-contracts 0.2.4 → 1.1.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.
@@ -33,6 +33,19 @@ function serializeContactRequest(requestId, data) {
33
33
  if (typeof data.targetId !== 'string' || data.targetId.length === 0) {
34
34
  return null;
35
35
  }
36
+ if (typeof data.targetName !== 'string' || data.targetName.trim().length === 0) {
37
+ return null;
38
+ }
39
+ if (data.targetAvatarUrl !== null && data.targetAvatarUrl !== undefined && typeof data.targetAvatarUrl !== 'string') {
40
+ return null;
41
+ }
42
+ if (data.targetUserType !== 'human' && data.targetUserType !== 'ai_agent') {
43
+ return null;
44
+ }
45
+ if (data.targetUserType === 'ai_agent'
46
+ && (typeof data.targetOwnerId !== 'string' || data.targetOwnerId.length === 0)) {
47
+ return null;
48
+ }
36
49
  const kindValue = typeof data.kind === 'string' ? data.kind : null;
37
50
  if (kindValue !== 'dm' && kindValue !== 'group_invite') {
38
51
  return null;
@@ -57,11 +70,9 @@ function serializeContactRequest(requestId, data) {
57
70
  requesterName: typeof data.requesterName === 'string' ? data.requesterName : 'Unknown',
58
71
  requesterAvatarUrl: typeof data.requesterAvatarUrl === 'string' ? data.requesterAvatarUrl : null,
59
72
  targetId: data.targetId,
60
- targetName: typeof data.targetName === 'string' ? data.targetName : null,
73
+ targetName: data.targetName.trim(),
61
74
  targetAvatarUrl: typeof data.targetAvatarUrl === 'string' ? data.targetAvatarUrl : null,
62
- targetUserType: data.targetUserType === 'human' || data.targetUserType === 'ai_agent'
63
- ? data.targetUserType
64
- : null,
75
+ targetUserType: data.targetUserType,
65
76
  approverId: data.approverId,
66
77
  message: typeof data.message === 'string' ? data.message : null,
67
78
  status: normalizeStatus(data.status),
@@ -73,7 +84,7 @@ function serializeContactRequest(requestId, data) {
73
84
  if (groupContext) {
74
85
  payload.groupContext = groupContext;
75
86
  }
76
- if (typeof data.targetOwnerId === 'string' && data.targetOwnerId.length > 0) {
87
+ if (data.targetUserType === 'ai_agent') {
77
88
  payload.targetOwnerId = data.targetOwnerId;
78
89
  }
79
90
  return payload;
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.serializeStoredMessage = serializeStoredMessage;
4
4
  const media_js_1 = require("./media.js");
5
- const MISSING_CREATED_AT_ISO = new Date(0).toISOString();
6
5
  function isRecord(value) {
7
6
  return typeof value === 'object' && value !== null && !Array.isArray(value);
8
7
  }
@@ -21,14 +20,17 @@ function normalizeTimestamp(value) {
21
20
  }
22
21
  return null;
23
22
  }
24
- function normalizeCreatedAt(value, fallback) {
25
- const timestamp = normalizeTimestamp(value) ?? normalizeTimestamp(fallback);
26
- return timestamp ? timestamp.toISOString() : MISSING_CREATED_AT_ISO;
23
+ function requireCreatedAt(value) {
24
+ const timestamp = normalizeTimestamp(value);
25
+ if (!timestamp) {
26
+ throw new Error('Message is missing canonical createdAt');
27
+ }
28
+ return timestamp.toISOString();
27
29
  }
28
- function normalizeSenderType(value, fallback) {
29
- if (value === 'ai_agent' || fallback === 'ai_agent')
30
- return 'ai_agent';
31
- return 'human';
30
+ function requireSenderType(value) {
31
+ if (value === 'human' || value === 'ai_agent')
32
+ return value;
33
+ throw new Error('Message is missing canonical senderType');
32
34
  }
33
35
  function normalizeAgentClientType(value) {
34
36
  if (value === 'claude-code'
@@ -40,7 +42,7 @@ function normalizeAgentClientType(value) {
40
42
  }
41
43
  return undefined;
42
44
  }
43
- function normalizeContentType(value, attachments) {
45
+ function normalizeContentType(value) {
44
46
  if (value === 'text'
45
47
  || value === 'image'
46
48
  || value === 'audio'
@@ -48,10 +50,7 @@ function normalizeContentType(value, attachments) {
48
50
  || value === 'contact_card') {
49
51
  return value;
50
52
  }
51
- const primary = attachments[0]?.kind;
52
- if (primary === 'image' || primary === 'audio' || primary === 'file')
53
- return primary;
54
- return 'text';
53
+ throw new Error('Message is missing canonical contentType');
55
54
  }
56
55
  function normalizeForwardedFrom(value) {
57
56
  if (!isRecord(value))
@@ -96,22 +95,25 @@ function normalizeContactCard(value) {
96
95
  }
97
96
  function serializeStoredMessage(input) {
98
97
  const { data } = input;
99
- const attachments = (0, media_js_1.getMessageAttachments)({
100
- attachments: data.attachments,
101
- });
98
+ const attachments = data.attachments === undefined
99
+ ? []
100
+ : (0, media_js_1.normalizeStoredAttachments)(data.attachments);
101
+ if (!attachments) {
102
+ throw new Error('Message has malformed attachments');
103
+ }
102
104
  const result = {
103
105
  id: input.id,
104
106
  senderId: typeof data.senderId === 'string' ? data.senderId : '',
105
- senderType: normalizeSenderType(data.senderType, input.senderTypeFallback),
107
+ senderType: requireSenderType(data.senderType),
106
108
  isOwner: input.isOwner,
107
- contentType: normalizeContentType(data.contentType, attachments),
109
+ contentType: normalizeContentType(data.contentType),
108
110
  text: typeof data.text === 'string' ? data.text : null,
109
111
  attachments,
110
112
  mentions: normalizeStringArray(data.mentions),
111
113
  replyTo: typeof data.replyTo === 'string' ? data.replyTo : null,
112
114
  replyToPosition: typeof data.replyToPosition === 'number' ? data.replyToPosition : null,
113
115
  status: data.status === 'read' ? 'read' : 'sent',
114
- createdAt: normalizeCreatedAt(data.createdAt, input.createdAtFallback),
116
+ createdAt: requireCreatedAt(data.createdAt),
115
117
  };
116
118
  const forwardedFrom = normalizeForwardedFrom(data.forwardedFrom);
117
119
  if (data.forwarded === true || forwardedFrom) {
@@ -130,5 +132,8 @@ function serializeStoredMessage(input) {
130
132
  if (contactCard) {
131
133
  result.contactCard = contactCard;
132
134
  }
135
+ if (isRecord(data.runtimeCard)) {
136
+ result.runtimeCard = data.runtimeCard;
137
+ }
133
138
  return result;
134
139
  }
@@ -22,7 +22,9 @@ function isHiddenRuntimeCardMetadata(metadata) {
22
22
  || metadata.type === 'question_reply'
23
23
  || metadata.type === 'plan_approval_reply'
24
24
  || metadata.type === 'runtime_input_reply'
25
- || metadata.type === 'runtime_input_outcome';
25
+ || metadata.type === 'runtime_input_outcome'
26
+ || metadata.type === 'runtime_card_reply'
27
+ || metadata.type === 'runtime_card_outcome';
26
28
  }
27
29
  function normalizeTurnMetadata(metadata) {
28
30
  if (!isRecord(metadata))
@@ -36,12 +38,11 @@ function normalizeTurnMetadata(metadata) {
36
38
  || metadata.replyBehavior === 'suppress_auto_reply'
37
39
  ? metadata.replyBehavior
38
40
  : undefined;
39
- if (!turnSemantics && typeof metadata.turnComplete !== 'boolean' && !replyBehavior) {
41
+ if (!turnSemantics && !replyBehavior) {
40
42
  return null;
41
43
  }
42
44
  return {
43
45
  ...(turnSemantics ? { turnSemantics } : {}),
44
- ...(typeof metadata.turnComplete === 'boolean' ? { turnComplete: metadata.turnComplete } : {}),
45
46
  ...(replyBehavior ? { replyBehavior } : {}),
46
47
  };
47
48
  }
@@ -80,11 +81,9 @@ function resolveTurnMessageSemantics(input) {
80
81
  const turnMetadata = normalizeTurnMetadata(input.metadata);
81
82
  if (turnMetadata?.turnSemantics)
82
83
  return turnMetadata.turnSemantics;
83
- if (turnMetadata?.turnComplete === true)
84
- return 'turn_complete';
85
84
  if (input.senderType === 'human')
86
85
  return 'turn_complete';
87
- return isTurnOpen(input.senderTurnState) ? 'progress' : 'turn_complete';
86
+ return 'progress';
88
87
  }
89
88
  function shouldPromoteConversationMessage(input) {
90
89
  if (isHiddenRuntimeCardMetadata(input.metadata)) {
@@ -5,9 +5,9 @@ export interface SerializedContactRequest {
5
5
  requesterName: string;
6
6
  requesterAvatarUrl: string | null;
7
7
  targetId: string;
8
- targetName: string | null;
8
+ targetName: string;
9
9
  targetAvatarUrl: string | null;
10
- targetUserType: 'human' | 'ai_agent' | null;
10
+ targetUserType: 'human' | 'ai_agent';
11
11
  targetOwnerId?: string | null;
12
12
  approverId: string;
13
13
  message: string | null;
@@ -30,6 +30,19 @@ export function serializeContactRequest(requestId, data) {
30
30
  if (typeof data.targetId !== 'string' || data.targetId.length === 0) {
31
31
  return null;
32
32
  }
33
+ if (typeof data.targetName !== 'string' || data.targetName.trim().length === 0) {
34
+ return null;
35
+ }
36
+ if (data.targetAvatarUrl !== null && data.targetAvatarUrl !== undefined && typeof data.targetAvatarUrl !== 'string') {
37
+ return null;
38
+ }
39
+ if (data.targetUserType !== 'human' && data.targetUserType !== 'ai_agent') {
40
+ return null;
41
+ }
42
+ if (data.targetUserType === 'ai_agent'
43
+ && (typeof data.targetOwnerId !== 'string' || data.targetOwnerId.length === 0)) {
44
+ return null;
45
+ }
33
46
  const kindValue = typeof data.kind === 'string' ? data.kind : null;
34
47
  if (kindValue !== 'dm' && kindValue !== 'group_invite') {
35
48
  return null;
@@ -54,11 +67,9 @@ export function serializeContactRequest(requestId, data) {
54
67
  requesterName: typeof data.requesterName === 'string' ? data.requesterName : 'Unknown',
55
68
  requesterAvatarUrl: typeof data.requesterAvatarUrl === 'string' ? data.requesterAvatarUrl : null,
56
69
  targetId: data.targetId,
57
- targetName: typeof data.targetName === 'string' ? data.targetName : null,
70
+ targetName: data.targetName.trim(),
58
71
  targetAvatarUrl: typeof data.targetAvatarUrl === 'string' ? data.targetAvatarUrl : null,
59
- targetUserType: data.targetUserType === 'human' || data.targetUserType === 'ai_agent'
60
- ? data.targetUserType
61
- : null,
72
+ targetUserType: data.targetUserType,
62
73
  approverId: data.approverId,
63
74
  message: typeof data.message === 'string' ? data.message : null,
64
75
  status: normalizeStatus(data.status),
@@ -70,7 +81,7 @@ export function serializeContactRequest(requestId, data) {
70
81
  if (groupContext) {
71
82
  payload.groupContext = groupContext;
72
83
  }
73
- if (typeof data.targetOwnerId === 'string' && data.targetOwnerId.length > 0) {
84
+ if (data.targetUserType === 'ai_agent') {
74
85
  payload.targetOwnerId = data.targetOwnerId;
75
86
  }
76
87
  return payload;
package/dist/message.d.ts CHANGED
@@ -36,12 +36,12 @@ export interface SerializedMessage {
36
36
  createdAt: string;
37
37
  metadata?: Record<string, unknown>;
38
38
  contactCard?: SerializedContactCard;
39
+ /** Durable canon.card.v1 runtime card content, passed through opaquely. */
40
+ runtimeCard?: Record<string, unknown>;
39
41
  }
40
42
  export interface SerializeMessageInput {
41
43
  id: string;
42
44
  data: Record<string, unknown>;
43
- createdAtFallback?: unknown;
44
- senderTypeFallback?: unknown;
45
45
  senderName?: string;
46
46
  isOwner: boolean;
47
47
  }
package/dist/message.js CHANGED
@@ -1,5 +1,4 @@
1
- import { getMessageAttachments } from './media.js';
2
- const MISSING_CREATED_AT_ISO = new Date(0).toISOString();
1
+ import { normalizeStoredAttachments } from './media.js';
3
2
  function isRecord(value) {
4
3
  return typeof value === 'object' && value !== null && !Array.isArray(value);
5
4
  }
@@ -18,14 +17,17 @@ function normalizeTimestamp(value) {
18
17
  }
19
18
  return null;
20
19
  }
21
- function normalizeCreatedAt(value, fallback) {
22
- const timestamp = normalizeTimestamp(value) ?? normalizeTimestamp(fallback);
23
- return timestamp ? timestamp.toISOString() : MISSING_CREATED_AT_ISO;
20
+ function requireCreatedAt(value) {
21
+ const timestamp = normalizeTimestamp(value);
22
+ if (!timestamp) {
23
+ throw new Error('Message is missing canonical createdAt');
24
+ }
25
+ return timestamp.toISOString();
24
26
  }
25
- function normalizeSenderType(value, fallback) {
26
- if (value === 'ai_agent' || fallback === 'ai_agent')
27
- return 'ai_agent';
28
- return 'human';
27
+ function requireSenderType(value) {
28
+ if (value === 'human' || value === 'ai_agent')
29
+ return value;
30
+ throw new Error('Message is missing canonical senderType');
29
31
  }
30
32
  function normalizeAgentClientType(value) {
31
33
  if (value === 'claude-code'
@@ -37,7 +39,7 @@ function normalizeAgentClientType(value) {
37
39
  }
38
40
  return undefined;
39
41
  }
40
- function normalizeContentType(value, attachments) {
42
+ function normalizeContentType(value) {
41
43
  if (value === 'text'
42
44
  || value === 'image'
43
45
  || value === 'audio'
@@ -45,10 +47,7 @@ function normalizeContentType(value, attachments) {
45
47
  || value === 'contact_card') {
46
48
  return value;
47
49
  }
48
- const primary = attachments[0]?.kind;
49
- if (primary === 'image' || primary === 'audio' || primary === 'file')
50
- return primary;
51
- return 'text';
50
+ throw new Error('Message is missing canonical contentType');
52
51
  }
53
52
  function normalizeForwardedFrom(value) {
54
53
  if (!isRecord(value))
@@ -93,22 +92,25 @@ function normalizeContactCard(value) {
93
92
  }
94
93
  export function serializeStoredMessage(input) {
95
94
  const { data } = input;
96
- const attachments = getMessageAttachments({
97
- attachments: data.attachments,
98
- });
95
+ const attachments = data.attachments === undefined
96
+ ? []
97
+ : normalizeStoredAttachments(data.attachments);
98
+ if (!attachments) {
99
+ throw new Error('Message has malformed attachments');
100
+ }
99
101
  const result = {
100
102
  id: input.id,
101
103
  senderId: typeof data.senderId === 'string' ? data.senderId : '',
102
- senderType: normalizeSenderType(data.senderType, input.senderTypeFallback),
104
+ senderType: requireSenderType(data.senderType),
103
105
  isOwner: input.isOwner,
104
- contentType: normalizeContentType(data.contentType, attachments),
106
+ contentType: normalizeContentType(data.contentType),
105
107
  text: typeof data.text === 'string' ? data.text : null,
106
108
  attachments,
107
109
  mentions: normalizeStringArray(data.mentions),
108
110
  replyTo: typeof data.replyTo === 'string' ? data.replyTo : null,
109
111
  replyToPosition: typeof data.replyToPosition === 'number' ? data.replyToPosition : null,
110
112
  status: data.status === 'read' ? 'read' : 'sent',
111
- createdAt: normalizeCreatedAt(data.createdAt, input.createdAtFallback),
113
+ createdAt: requireCreatedAt(data.createdAt),
112
114
  };
113
115
  const forwardedFrom = normalizeForwardedFrom(data.forwardedFrom);
114
116
  if (data.forwarded === true || forwardedFrom) {
@@ -127,5 +129,8 @@ export function serializeStoredMessage(input) {
127
129
  if (contactCard) {
128
130
  result.contactCard = contactCard;
129
131
  }
132
+ if (isRecord(data.runtimeCard)) {
133
+ result.runtimeCard = data.runtimeCard;
134
+ }
130
135
  return result;
131
136
  }
@@ -3,7 +3,6 @@ export type TurnLifecycleState = 'idle' | 'thinking' | 'streaming' | 'tool' | 'w
3
3
  export type TurnMessageSemantics = 'progress' | 'turn_complete' | 'control';
4
4
  export interface TurnMetadata {
5
5
  turnSemantics?: TurnMessageSemantics;
6
- turnComplete?: boolean;
7
6
  replyBehavior?: 'allow_auto_reply' | 'suppress_auto_reply';
8
7
  }
9
8
  export interface TurnStateLike {
@@ -21,17 +20,14 @@ export declare function isTurnStateStale(turnState: TurnStateLike | null | undef
21
20
  export declare function resolveTurnMessageSemantics(input: {
22
21
  senderType: SenderType;
23
22
  metadata?: unknown;
24
- senderTurnState?: TurnStateLike | null;
25
23
  }): TurnMessageSemantics;
26
24
  export declare function shouldPromoteConversationMessage(input: {
27
25
  senderType: SenderType;
28
26
  metadata?: unknown;
29
- senderTurnState?: TurnStateLike | null;
30
27
  }): boolean;
31
28
  export declare function shouldTriggerAgentTurn(input: {
32
29
  senderType: SenderType;
33
30
  metadata?: unknown;
34
- senderTurnState?: TurnStateLike | null;
35
31
  }): {
36
32
  allow: boolean;
37
33
  semantics: TurnMessageSemantics;
@@ -11,7 +11,9 @@ function isHiddenRuntimeCardMetadata(metadata) {
11
11
  || metadata.type === 'question_reply'
12
12
  || metadata.type === 'plan_approval_reply'
13
13
  || metadata.type === 'runtime_input_reply'
14
- || metadata.type === 'runtime_input_outcome';
14
+ || metadata.type === 'runtime_input_outcome'
15
+ || metadata.type === 'runtime_card_reply'
16
+ || metadata.type === 'runtime_card_outcome';
15
17
  }
16
18
  export function normalizeTurnMetadata(metadata) {
17
19
  if (!isRecord(metadata))
@@ -25,12 +27,11 @@ export function normalizeTurnMetadata(metadata) {
25
27
  || metadata.replyBehavior === 'suppress_auto_reply'
26
28
  ? metadata.replyBehavior
27
29
  : undefined;
28
- if (!turnSemantics && typeof metadata.turnComplete !== 'boolean' && !replyBehavior) {
30
+ if (!turnSemantics && !replyBehavior) {
29
31
  return null;
30
32
  }
31
33
  return {
32
34
  ...(turnSemantics ? { turnSemantics } : {}),
33
- ...(typeof metadata.turnComplete === 'boolean' ? { turnComplete: metadata.turnComplete } : {}),
34
35
  ...(replyBehavior ? { replyBehavior } : {}),
35
36
  };
36
37
  }
@@ -69,11 +70,9 @@ export function resolveTurnMessageSemantics(input) {
69
70
  const turnMetadata = normalizeTurnMetadata(input.metadata);
70
71
  if (turnMetadata?.turnSemantics)
71
72
  return turnMetadata.turnSemantics;
72
- if (turnMetadata?.turnComplete === true)
73
- return 'turn_complete';
74
73
  if (input.senderType === 'human')
75
74
  return 'turn_complete';
76
- return isTurnOpen(input.senderTurnState) ? 'progress' : 'turn_complete';
75
+ return 'progress';
77
76
  }
78
77
  export function shouldPromoteConversationMessage(input) {
79
78
  if (isHiddenRuntimeCardMetadata(input.metadata)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/backend-contracts",
3
- "version": "0.2.4",
3
+ "version": "1.1.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",