@canonmsg/backend-contracts 2.0.0 → 2.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.
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ // Sensitive-content protection for approval diffs.
3
+ //
4
+ // Approval diffs include PRE-IMAGE context lines and ride on a message readable
5
+ // by every conversation member, so a secret-bearing file would leak existing
6
+ // secrets. The emitting host applies this before sending (it owns the file
7
+ // context); the server (@canonmsg/functions) mirrors the same suppression
8
+ // defensively. Single-sourced here in the zero-dependency backend-contracts
9
+ // leaf so @canonmsg/core (client-side sanitizer) and Functions (server mirror)
10
+ // share one implementation — Functions deliberately does not depend on core.
11
+ //
12
+ // Two deliberately narrow mechanisms so ordinary code diffs stay readable:
13
+ // 1. Path suppression — files whose path screams "secret store" lose their
14
+ // hunk text entirely; entry/status/counts survive.
15
+ // 2. High-confidence token redaction — prefix-anchored credential shapes
16
+ // (AWS/GitHub/Slack/Google/OpenAI-style keys, JWTs) are replaced with
17
+ // `[redacted]` inside remaining hunks. Private-key blocks suppress the
18
+ // whole file (redacting a PEM body line-by-line is fragile).
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.PRIVATE_KEY_BLOCK = void 0;
21
+ exports.isSensitiveDiffPath = isSensitiveDiffPath;
22
+ exports.redactSecretTokens = redactSecretTokens;
23
+ const SENSITIVE_DIFF_BASENAME_PATTERNS = [
24
+ /^\.env(\..+)?$/i,
25
+ /^\.npmrc$/i,
26
+ /^\.netrc$/i,
27
+ /^\.pgpass$/i,
28
+ /^\.git-credentials$/i,
29
+ /^\.htpasswd$/i,
30
+ /\.(pem|key|p12|pfx|keystore|jks|tfvars)$/i,
31
+ /^id_(rsa|dsa|ecdsa|ed25519)(\..+)?$/i,
32
+ // Word-bounded so "secrets.json" and "client_secret.yaml" match but
33
+ // "secretary.ts" does not.
34
+ /(^|[-_.])(credentials?|secrets?)([-_.]|$)/i,
35
+ /^serviceaccount.*\.json$/i,
36
+ /^kubeconfig(\..+)?$/i,
37
+ ];
38
+ // Directory components that mark everything beneath them sensitive
39
+ // (~/.ssh/config, .aws/credentials, infra/secrets/db.yaml, …).
40
+ const SENSITIVE_DIFF_SEGMENT_PATTERNS = [
41
+ /^\.ssh$/i,
42
+ /^\.aws$/i,
43
+ /^\.gnupg$/i,
44
+ /^\.kube$/i,
45
+ /^secrets?$/i,
46
+ /^credentials?$/i,
47
+ ];
48
+ const SECRET_TOKEN_PATTERNS = [
49
+ /AKIA[0-9A-Z]{16}/g,
50
+ /gh[pousr]_[A-Za-z0-9]{36,255}/g,
51
+ /github_pat_[A-Za-z0-9_]{22,255}/g,
52
+ /xox[baprs]-[A-Za-z0-9-]{10,250}/g,
53
+ /AIza[0-9A-Za-z_-]{35}/g,
54
+ /sk-[A-Za-z0-9_-]{20,250}/g,
55
+ /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
56
+ ];
57
+ /** Matches the header line of a PEM private-key block. */
58
+ exports.PRIVATE_KEY_BLOCK = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY( BLOCK)?-----/;
59
+ /** True when a diff file path (or rename source) matches the sensitive list. */
60
+ function isSensitiveDiffPath(path) {
61
+ const segments = path.split('/').filter(Boolean);
62
+ const basename = segments.pop() ?? path;
63
+ return SENSITIVE_DIFF_BASENAME_PATTERNS.some((pattern) => pattern.test(basename))
64
+ || segments.some((segment) => SENSITIVE_DIFF_SEGMENT_PATTERNS.some((pattern) => pattern.test(segment)));
65
+ }
66
+ /** Replace high-confidence credential shapes inside hunk text with `[redacted]`. */
67
+ function redactSecretTokens(text) {
68
+ let redacted = text;
69
+ for (const pattern of SECRET_TOKEN_PATTERNS) {
70
+ redacted = redacted.replace(pattern, '[redacted]');
71
+ }
72
+ return redacted;
73
+ }
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
+ __exportStar(require("./diffRedaction.js"), exports);
17
18
  __exportStar(require("./media.js"), exports);
18
19
  __exportStar(require("./message.js"), exports);
19
20
  __exportStar(require("./runtimeCardFields.js"), exports);
@@ -61,7 +61,8 @@ function normalizeContentType(value) {
61
61
  || value === 'audio'
62
62
  || value === 'video'
63
63
  || value === 'file'
64
- || value === 'contact_card') {
64
+ || value === 'contact_card'
65
+ || value === 'interaction') {
65
66
  return value;
66
67
  }
67
68
  throw new Error('Message is missing canonical contentType');
@@ -110,6 +111,41 @@ function normalizeContactCard(value) {
110
111
  card.lifecycleState = value.lifecycleState;
111
112
  return card;
112
113
  }
114
+ const INTERACTION_KINDS = [
115
+ 'contact', 'approval', 'input', 'plan', 'card',
116
+ ];
117
+ function normalizeInteraction(value) {
118
+ if (!isRecord(value))
119
+ return undefined;
120
+ if (!INTERACTION_KINDS.includes(value.kind))
121
+ return undefined;
122
+ if (typeof value.requestId !== 'string' || value.requestId.length === 0)
123
+ return undefined;
124
+ const envelope = {
125
+ kind: value.kind,
126
+ requestId: value.requestId,
127
+ interactive: value.interactive === true,
128
+ };
129
+ if (typeof value.responseUserId === 'string')
130
+ envelope.responseUserId = value.responseUserId;
131
+ if (typeof value.expiresAt === 'string')
132
+ envelope.expiresAt = value.expiresAt;
133
+ if (typeof value.schemaVersion === 'string')
134
+ envelope.schemaVersion = value.schemaVersion;
135
+ if (isRecord(value.preview)) {
136
+ const preview = {};
137
+ if (typeof value.preview.title === 'string')
138
+ preview.title = value.preview.title;
139
+ if (Array.isArray(value.preview.blockKinds)) {
140
+ preview.blockKinds = value.preview.blockKinds.filter((entry) => typeof entry === 'string');
141
+ }
142
+ envelope.preview = preview;
143
+ }
144
+ if (isRecord(value.response)) {
145
+ envelope.response = value.response;
146
+ }
147
+ return envelope;
148
+ }
113
149
  function serializeStoredMessage(input) {
114
150
  const { data } = input;
115
151
  const attachments = data.attachments === undefined
@@ -153,6 +189,13 @@ function serializeStoredMessage(input) {
153
189
  if (runtimeCard) {
154
190
  result.runtimeCard = runtimeCard;
155
191
  }
192
+ const interaction = normalizeInteraction(data.interaction);
193
+ if (interaction) {
194
+ result.interaction = interaction;
195
+ }
196
+ if (typeof data.body === 'string') {
197
+ result.body = data.body;
198
+ }
156
199
  const reactions = normalizeReactions(data.reactions);
157
200
  if (reactions) {
158
201
  result.reactions = reactions;
@@ -0,0 +1,6 @@
1
+ /** Matches the header line of a PEM private-key block. */
2
+ export declare const PRIVATE_KEY_BLOCK: RegExp;
3
+ /** True when a diff file path (or rename source) matches the sensitive list. */
4
+ export declare function isSensitiveDiffPath(path: string): boolean;
5
+ /** Replace high-confidence credential shapes inside hunk text with `[redacted]`. */
6
+ export declare function redactSecretTokens(text: string): string;
@@ -0,0 +1,68 @@
1
+ // Sensitive-content protection for approval diffs.
2
+ //
3
+ // Approval diffs include PRE-IMAGE context lines and ride on a message readable
4
+ // by every conversation member, so a secret-bearing file would leak existing
5
+ // secrets. The emitting host applies this before sending (it owns the file
6
+ // context); the server (@canonmsg/functions) mirrors the same suppression
7
+ // defensively. Single-sourced here in the zero-dependency backend-contracts
8
+ // leaf so @canonmsg/core (client-side sanitizer) and Functions (server mirror)
9
+ // share one implementation — Functions deliberately does not depend on core.
10
+ //
11
+ // Two deliberately narrow mechanisms so ordinary code diffs stay readable:
12
+ // 1. Path suppression — files whose path screams "secret store" lose their
13
+ // hunk text entirely; entry/status/counts survive.
14
+ // 2. High-confidence token redaction — prefix-anchored credential shapes
15
+ // (AWS/GitHub/Slack/Google/OpenAI-style keys, JWTs) are replaced with
16
+ // `[redacted]` inside remaining hunks. Private-key blocks suppress the
17
+ // whole file (redacting a PEM body line-by-line is fragile).
18
+ const SENSITIVE_DIFF_BASENAME_PATTERNS = [
19
+ /^\.env(\..+)?$/i,
20
+ /^\.npmrc$/i,
21
+ /^\.netrc$/i,
22
+ /^\.pgpass$/i,
23
+ /^\.git-credentials$/i,
24
+ /^\.htpasswd$/i,
25
+ /\.(pem|key|p12|pfx|keystore|jks|tfvars)$/i,
26
+ /^id_(rsa|dsa|ecdsa|ed25519)(\..+)?$/i,
27
+ // Word-bounded so "secrets.json" and "client_secret.yaml" match but
28
+ // "secretary.ts" does not.
29
+ /(^|[-_.])(credentials?|secrets?)([-_.]|$)/i,
30
+ /^serviceaccount.*\.json$/i,
31
+ /^kubeconfig(\..+)?$/i,
32
+ ];
33
+ // Directory components that mark everything beneath them sensitive
34
+ // (~/.ssh/config, .aws/credentials, infra/secrets/db.yaml, …).
35
+ const SENSITIVE_DIFF_SEGMENT_PATTERNS = [
36
+ /^\.ssh$/i,
37
+ /^\.aws$/i,
38
+ /^\.gnupg$/i,
39
+ /^\.kube$/i,
40
+ /^secrets?$/i,
41
+ /^credentials?$/i,
42
+ ];
43
+ const SECRET_TOKEN_PATTERNS = [
44
+ /AKIA[0-9A-Z]{16}/g,
45
+ /gh[pousr]_[A-Za-z0-9]{36,255}/g,
46
+ /github_pat_[A-Za-z0-9_]{22,255}/g,
47
+ /xox[baprs]-[A-Za-z0-9-]{10,250}/g,
48
+ /AIza[0-9A-Za-z_-]{35}/g,
49
+ /sk-[A-Za-z0-9_-]{20,250}/g,
50
+ /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
51
+ ];
52
+ /** Matches the header line of a PEM private-key block. */
53
+ export const PRIVATE_KEY_BLOCK = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY( BLOCK)?-----/;
54
+ /** True when a diff file path (or rename source) matches the sensitive list. */
55
+ export function isSensitiveDiffPath(path) {
56
+ const segments = path.split('/').filter(Boolean);
57
+ const basename = segments.pop() ?? path;
58
+ return SENSITIVE_DIFF_BASENAME_PATTERNS.some((pattern) => pattern.test(basename))
59
+ || segments.some((segment) => SENSITIVE_DIFF_SEGMENT_PATTERNS.some((pattern) => pattern.test(segment)));
60
+ }
61
+ /** Replace high-confidence credential shapes inside hunk text with `[redacted]`. */
62
+ export function redactSecretTokens(text) {
63
+ let redacted = text;
64
+ for (const pattern of SECRET_TOKEN_PATTERNS) {
65
+ redacted = redacted.replace(pattern, '[redacted]');
66
+ }
67
+ return redacted;
68
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './diffRedaction.js';
1
2
  export * from './media.js';
2
3
  export * from './message.js';
3
4
  export * from './runtimeCardFields.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './diffRedaction.js';
1
2
  export * from './media.js';
2
3
  export * from './message.js';
3
4
  export * from './runtimeCardFields.js';
package/dist/message.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { MediaAttachment } from './media.js';
2
2
  export type SerializedSenderType = 'human' | 'ai_agent';
3
3
  export type SerializedAgentClientType = 'claude-code' | 'openclaw' | 'codex' | 'hermes' | 'generic';
4
- export type SerializedContentType = 'text' | 'image' | 'audio' | 'video' | 'file' | 'contact_card';
4
+ export type SerializedContentType = 'text' | 'image' | 'audio' | 'video' | 'file' | 'contact_card' | 'interaction';
5
5
  export interface ForwardedFrom {
6
6
  sourceConversationId: string;
7
7
  messageId: string;
@@ -40,8 +40,38 @@ export interface SerializedMessage {
40
40
  contactCard?: SerializedContactCard;
41
41
  /** Durable canon.card.v1 runtime card content, passed through opaquely. */
42
42
  runtimeCard?: Record<string, unknown>;
43
+ /**
44
+ * Plaintext, server-authorized interaction envelope for `contentType:
45
+ * 'interaction'` messages (card / input / approval / plan / contact). The
46
+ * authorization contract (`response`) and preview live here; the opaque
47
+ * request payload rides in `body`.
48
+ */
49
+ interaction?: SerializedInteraction;
50
+ /** Opaque interaction payload (JSON string or ciphertext); never parsed here. */
51
+ body?: string;
43
52
  reactions?: SerializedReactions;
44
53
  }
54
+ export type SerializedInteractionKind = 'contact' | 'approval' | 'input' | 'plan' | 'card';
55
+ export interface SerializedInteraction {
56
+ kind: SerializedInteractionKind;
57
+ requestId: string;
58
+ interactive: boolean;
59
+ responseUserId?: string;
60
+ expiresAt?: string;
61
+ schemaVersion?: string;
62
+ preview?: {
63
+ title?: string;
64
+ blockKinds?: string[];
65
+ };
66
+ response?: {
67
+ actionIds?: string[];
68
+ actionFields?: Record<string, unknown>;
69
+ questions?: Array<{
70
+ id: string;
71
+ sensitive?: boolean;
72
+ }>;
73
+ };
74
+ }
45
75
  export interface SerializeMessageInput {
46
76
  id: string;
47
77
  data: Record<string, unknown>;
package/dist/message.js CHANGED
@@ -58,7 +58,8 @@ function normalizeContentType(value) {
58
58
  || value === 'audio'
59
59
  || value === 'video'
60
60
  || value === 'file'
61
- || value === 'contact_card') {
61
+ || value === 'contact_card'
62
+ || value === 'interaction') {
62
63
  return value;
63
64
  }
64
65
  throw new Error('Message is missing canonical contentType');
@@ -107,6 +108,41 @@ function normalizeContactCard(value) {
107
108
  card.lifecycleState = value.lifecycleState;
108
109
  return card;
109
110
  }
111
+ const INTERACTION_KINDS = [
112
+ 'contact', 'approval', 'input', 'plan', 'card',
113
+ ];
114
+ function normalizeInteraction(value) {
115
+ if (!isRecord(value))
116
+ return undefined;
117
+ if (!INTERACTION_KINDS.includes(value.kind))
118
+ return undefined;
119
+ if (typeof value.requestId !== 'string' || value.requestId.length === 0)
120
+ return undefined;
121
+ const envelope = {
122
+ kind: value.kind,
123
+ requestId: value.requestId,
124
+ interactive: value.interactive === true,
125
+ };
126
+ if (typeof value.responseUserId === 'string')
127
+ envelope.responseUserId = value.responseUserId;
128
+ if (typeof value.expiresAt === 'string')
129
+ envelope.expiresAt = value.expiresAt;
130
+ if (typeof value.schemaVersion === 'string')
131
+ envelope.schemaVersion = value.schemaVersion;
132
+ if (isRecord(value.preview)) {
133
+ const preview = {};
134
+ if (typeof value.preview.title === 'string')
135
+ preview.title = value.preview.title;
136
+ if (Array.isArray(value.preview.blockKinds)) {
137
+ preview.blockKinds = value.preview.blockKinds.filter((entry) => typeof entry === 'string');
138
+ }
139
+ envelope.preview = preview;
140
+ }
141
+ if (isRecord(value.response)) {
142
+ envelope.response = value.response;
143
+ }
144
+ return envelope;
145
+ }
110
146
  export function serializeStoredMessage(input) {
111
147
  const { data } = input;
112
148
  const attachments = data.attachments === undefined
@@ -150,6 +186,13 @@ export function serializeStoredMessage(input) {
150
186
  if (runtimeCard) {
151
187
  result.runtimeCard = runtimeCard;
152
188
  }
189
+ const interaction = normalizeInteraction(data.interaction);
190
+ if (interaction) {
191
+ result.interaction = interaction;
192
+ }
193
+ if (typeof data.body === 'string') {
194
+ result.body = data.body;
195
+ }
153
196
  const reactions = normalizeReactions(data.reactions);
154
197
  if (reactions) {
155
198
  result.reactions = reactions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/backend-contracts",
3
- "version": "2.0.0",
3
+ "version": "2.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",