@canonmsg/core 0.15.2 → 0.15.3

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.
@@ -82,7 +82,7 @@ function describeContactCard(card) {
82
82
  ? 'reject pending requests'
83
83
  : null,
84
84
  ].filter(Boolean).join(', ');
85
- const hint = `This host can inspect the card, but Canon admission actions are missing here. Missing capabilities: ${missingCapabilities}. Use another Canon surface for userId ${card.userId}.`;
85
+ const hint = `This host can inspect the card, but this wrapper does not expose callable Canon admission actions yet. Missing capabilities here: ${missingCapabilities}. Use an SDK/OpenClaw reach-out surface or another Canon client for userId ${card.userId}.`;
86
86
  return `${identity}\n${hint}`;
87
87
  }
88
88
  function describeAttachment(attachment, materialized) {
package/dist/index.d.ts CHANGED
@@ -37,6 +37,8 @@ export type { HostInboundParticipantContext, } from './host-runtime.js';
37
37
  export { createRuntimeStatePublisher, } from './runtime-state-publisher.js';
38
38
  export type { RuntimeStatePublisher, RuntimeStatePublisherOptions, RuntimeStreamingPayload, } from './runtime-state-publisher.js';
39
39
  export { formatCanonMessageAsText } from './message-format.js';
40
+ export { reachOutToCanonContact } from './reach-out.js';
41
+ export type { CanonReachOutClient, CanonReachOutOptions, CanonReachOutResult, } from './reach-out.js';
40
42
  export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
41
43
  export { resolveCanonBaseUrl } from './base-url.js';
42
44
  export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from './cli-metadata.js';
package/dist/index.js CHANGED
@@ -35,6 +35,8 @@ export { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMe
35
35
  export { createRuntimeStatePublisher, } from './runtime-state-publisher.js';
36
36
  // Message formatting (LLM-facing text projection)
37
37
  export { formatCanonMessageAsText } from './message-format.js';
38
+ // Admission-aware reach-out helpers for runtime clients
39
+ export { reachOutToCanonContact } from './reach-out.js';
38
40
  // Constants
39
41
  export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
40
42
  // Base URL resolver
@@ -10,11 +10,12 @@
10
10
  * regression in every consumer plugin.
11
11
  */
12
12
  export function formatCanonMessageAsText(message) {
13
+ const trimmedText = typeof message.text === 'string' ? message.text.trim() : '';
13
14
  if (message.contentType === 'contact_card' && message.contactCard) {
14
- return formatContactCard(message.contactCard);
15
+ const cardText = formatContactCard(message.contactCard);
16
+ return trimmedText ? `${cardText}\n${trimmedText}` : cardText;
15
17
  }
16
18
  const attachment = pickPrimaryAttachment(message.attachments);
17
- const trimmedText = typeof message.text === 'string' ? message.text.trim() : '';
18
19
  if (attachment?.kind === 'image') {
19
20
  return trimmedText ? `[image] ${trimmedText}` : '[image]';
20
21
  }
@@ -0,0 +1,37 @@
1
+ import type { CanonResolveAdmissionResult, CreateContactRequestResult, CreateConversationOptions, SendMessageOptions } from './types.js';
2
+ export interface CanonReachOutClient {
3
+ resolveAdmission(targetUserId: string): Promise<CanonResolveAdmissionResult>;
4
+ createConversation(options: CreateConversationOptions): Promise<{
5
+ conversationId: string;
6
+ }>;
7
+ sendMessage(conversationId: string, text: string, options?: SendMessageOptions): Promise<{
8
+ messageId: string;
9
+ }>;
10
+ createContactRequest(targetUserId: string, message?: string | null): Promise<CreateContactRequestResult>;
11
+ }
12
+ export interface CanonReachOutOptions {
13
+ targetUserId: string;
14
+ text?: string | null;
15
+ requestMessage?: string | null;
16
+ sendMessageOptions?: SendMessageOptions;
17
+ }
18
+ export type CanonReachOutResult = {
19
+ status: 'messaged';
20
+ conversationId: string;
21
+ messageId?: string;
22
+ } | {
23
+ status: 'requested';
24
+ requestId: string | null;
25
+ } | {
26
+ status: 'pending';
27
+ requestId: string | null;
28
+ } | {
29
+ status: 'blocked' | 'unavailable';
30
+ reason: string;
31
+ };
32
+ /**
33
+ * Admission-aware direct reach-out for runtime clients. It deliberately keeps
34
+ * backend conversation creation fail-closed; callers choose this helper when
35
+ * their product behavior is "message if reachable, otherwise request access."
36
+ */
37
+ export declare function reachOutToCanonContact(client: CanonReachOutClient, options: CanonReachOutOptions): Promise<CanonReachOutResult>;
@@ -0,0 +1,73 @@
1
+ const CONTACT_REQUEST_MESSAGE_LIMIT = 500;
2
+ function normalizeOptionalText(value) {
3
+ const trimmed = typeof value === 'string' ? value.trim() : '';
4
+ return trimmed.length > 0 ? trimmed : null;
5
+ }
6
+ function normalizeContactRequestMessage(requestMessage, fallbackText) {
7
+ const text = normalizeOptionalText(requestMessage) ?? normalizeOptionalText(fallbackText);
8
+ if (!text)
9
+ return null;
10
+ if (text.length <= CONTACT_REQUEST_MESSAGE_LIMIT)
11
+ return text;
12
+ return `${text.slice(0, CONTACT_REQUEST_MESSAGE_LIMIT - 3)}...`;
13
+ }
14
+ function isConnectionRequiredError(error) {
15
+ const status = error && typeof error === 'object' && 'status' in error
16
+ ? Number(error.status)
17
+ : null;
18
+ const message = error instanceof Error ? error.message : String(error ?? '');
19
+ return status === 403 && /CONNECTION_REQUIRED|connection required/i.test(message);
20
+ }
21
+ async function openConversationAndMaybeMessage(client, options) {
22
+ const { conversationId } = await client.createConversation({
23
+ type: 'direct',
24
+ targetUserId: options.targetUserId,
25
+ });
26
+ const text = normalizeOptionalText(options.text);
27
+ if (text) {
28
+ const { messageId } = options.sendMessageOptions
29
+ ? await client.sendMessage(conversationId, text, options.sendMessageOptions)
30
+ : await client.sendMessage(conversationId, text);
31
+ return { status: 'messaged', conversationId, messageId };
32
+ }
33
+ return { status: 'messaged', conversationId };
34
+ }
35
+ async function requestContact(client, options) {
36
+ const result = await client.createContactRequest(options.targetUserId, normalizeContactRequestMessage(options.requestMessage, options.text));
37
+ if (result.status === 'open') {
38
+ return openConversationAndMaybeMessage(client, { ...options, requestMessage: null });
39
+ }
40
+ if (result.status === 'duplicate') {
41
+ return { status: 'pending', requestId: result.requestId };
42
+ }
43
+ return { status: 'requested', requestId: result.requestId };
44
+ }
45
+ /**
46
+ * Admission-aware direct reach-out for runtime clients. It deliberately keeps
47
+ * backend conversation creation fail-closed; callers choose this helper when
48
+ * their product behavior is "message if reachable, otherwise request access."
49
+ */
50
+ export async function reachOutToCanonContact(client, options) {
51
+ const { admission } = await client.resolveAdmission(options.targetUserId);
52
+ if (admission.state === 'allowed' && admission.canMessage) {
53
+ try {
54
+ return await openConversationAndMaybeMessage(client, options);
55
+ }
56
+ catch (error) {
57
+ if (isConnectionRequiredError(error)) {
58
+ return requestContact(client, options);
59
+ }
60
+ throw error;
61
+ }
62
+ }
63
+ if (admission.state === 'pending-outbound') {
64
+ return { status: 'pending', requestId: admission.pendingRequestId ?? null };
65
+ }
66
+ if (admission.state === 'request-required' && admission.canRequestContact) {
67
+ return requestContact(client, options);
68
+ }
69
+ if (admission.state === 'blocked') {
70
+ return { status: 'blocked', reason: 'blocked' };
71
+ }
72
+ return { status: 'unavailable', reason: admission.state };
73
+ }
package/dist/types.d.ts CHANGED
@@ -21,9 +21,9 @@ export interface ForwardedFrom {
21
21
  /**
22
22
  * Server-serialized contact-card payload. Emitted on messages with
23
23
  * `contentType: 'contact_card'` so agents receive the referenced user's
24
- * identity alongside the card. Agents use the referenced `userId` with the
25
- * standard send-message path; if the target's inbound policy blocks cold
26
- * contact, they first send a contact request and retry after approval.
24
+ * identity alongside the card. Agents use the referenced `userId` with an
25
+ * admission-aware reach-out path; if the target's inbound policy blocks cold
26
+ * contact, they create a contact request and retry after approval.
27
27
  */
28
28
  export interface ContactCardPayload {
29
29
  userId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/core",
3
- "version": "0.15.2",
3
+ "version": "0.15.3",
4
4
  "description": "Canon core — shared types, REST client, SSE stream, and registration for Canon messaging",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",