@pmate/chat-sdk 0.1.7 → 0.1.8

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.
Files changed (54) hide show
  1. package/lib/components/ChatInputDesktop.js +7 -2
  2. package/lib/components/icons.d.ts +1 -0
  3. package/lib/components/icons.js +3 -0
  4. package/lib/hooks/useVoiceInputController.js +21 -2
  5. package/lib/index.d.ts +4 -0
  6. package/lib/index.js +4 -0
  7. package/lib/runtime/botAuth.d.ts +32 -0
  8. package/lib/runtime/botAuth.js +71 -0
  9. package/lib/runtime/botKey.d.ts +14 -0
  10. package/lib/runtime/botKey.js +25 -0
  11. package/lib/runtime/chatClient.d.ts +62 -0
  12. package/lib/runtime/chatClient.js +209 -0
  13. package/lib/runtime/index.d.ts +5 -0
  14. package/lib/runtime/index.js +5 -0
  15. package/lib/runtime/protocol.d.ts +83 -0
  16. package/lib/runtime/protocol.js +113 -0
  17. package/lib/runtime/thread.d.ts +3 -0
  18. package/lib/runtime/thread.js +18 -0
  19. package/lib/types/voice.d.ts +1 -0
  20. package/package.json +7 -1
  21. package/dist/assets/ChatInput.stories-BABdkDrt.js +0 -1545
  22. package/dist/assets/Color-AVL7NMMY-D33umUzR.js +0 -1
  23. package/dist/assets/DocsRenderer-PQXLIZUC-BUYhEk48.js +0 -1243
  24. package/dist/assets/client-De2ACSn_.js +0 -9
  25. package/dist/assets/iframe-DVJDoCiO.js +0 -1071
  26. package/dist/assets/iframe-q28pAgAa.css +0 -1
  27. package/dist/assets/index-CkuJGuix.js +0 -1
  28. package/dist/assets/preload-helper-PPVm8Dsz.js +0 -1
  29. package/dist/assets/react-18-DfLUnvSL.js +0 -1
  30. package/dist/favicon-wrapper.svg +0 -46
  31. package/dist/favicon.svg +0 -1
  32. package/dist/iframe.html +0 -687
  33. package/dist/index.html +0 -148
  34. package/dist/index.json +0 -1
  35. package/dist/nunito-sans-bold-italic.woff2 +0 -0
  36. package/dist/nunito-sans-bold.woff2 +0 -0
  37. package/dist/nunito-sans-italic.woff2 +0 -0
  38. package/dist/nunito-sans-regular.woff2 +0 -0
  39. package/dist/project.json +0 -1
  40. package/dist/sb-addons/docs-1/manager-bundle.js +0 -151
  41. package/dist/sb-addons/docs-1/manager-bundle.js.LEGAL.txt +0 -0
  42. package/dist/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js +0 -971
  43. package/dist/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js.LEGAL.txt +0 -0
  44. package/dist/sb-common-assets/favicon-wrapper.svg +0 -46
  45. package/dist/sb-common-assets/favicon.svg +0 -1
  46. package/dist/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
  47. package/dist/sb-common-assets/nunito-sans-bold.woff2 +0 -0
  48. package/dist/sb-common-assets/nunito-sans-italic.woff2 +0 -0
  49. package/dist/sb-common-assets/nunito-sans-regular.woff2 +0 -0
  50. package/dist/sb-manager/globals-module-info.js +0 -799
  51. package/dist/sb-manager/globals-runtime.js +0 -69791
  52. package/dist/sb-manager/globals.js +0 -34
  53. package/dist/sb-manager/runtime.js +0 -13198
  54. package/dist/vite-inject-mocker-entry.js +0 -2
@@ -16,7 +16,7 @@ import { InputSurface } from "./InputSurface";
16
16
  import { StatusHint } from "./StatusHint";
17
17
  import { TextComposer } from "./TextComposer";
18
18
  import { WaveformFeedback } from "./WaveformFeedback";
19
- import { FileOutlineIcon, ImageOutlineIcon, KeyboardOutlineIcon, MicOutlineIcon, } from "./icons";
19
+ import { FileOutlineIcon, ImageOutlineIcon, MicOutlineIcon, StopCircleOutlineIcon, } from "./icons";
20
20
  function DesktopInner(props) {
21
21
  const tokens = mergeChatInputTokens(props.tokens);
22
22
  const state = useChatInputState();
@@ -204,6 +204,9 @@ function DesktopInner(props) {
204
204
  voice.stopRecording("transcribe");
205
205
  return;
206
206
  }
207
+ if (voice.status === "transcribing") {
208
+ return;
209
+ }
207
210
  voice.beginVoiceMode();
208
211
  voice.startRecording();
209
212
  }, style: {
@@ -215,7 +218,9 @@ function DesktopInner(props) {
215
218
  opacity: props.disabled || props.enableVoice === false ? 0.4 : 1,
216
219
  }, "aria-label": voice.status === "recording"
217
220
  ? "停止录音并转文字"
218
- : "开始语音输入", children: voice.status === "recording" ? (_jsx(KeyboardOutlineIcon, { size: 27 })) : (_jsx(MicOutlineIcon, { size: 24, strokeWidth: 2 })) }), _jsx(InputSurface, { tokens: tokens, status: state.status, styleOverride: {
221
+ : voice.status === "transcribing"
222
+ ? "正在转写语音"
223
+ : "开始语音输入", children: voice.status === "recording" ? (_jsx(StopCircleOutlineIcon, { size: 27 })) : (_jsx(MicOutlineIcon, { size: 24, strokeWidth: 2 })) }), _jsx(InputSurface, { tokens: tokens, status: state.status, styleOverride: {
219
224
  minHeight: 52,
220
225
  padding: "0 14px",
221
226
  display: "flex",
@@ -6,6 +6,7 @@ type IconProps = {
6
6
  export declare function MicOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
7
7
  export declare function VoiceInputOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
8
8
  export declare function KeyboardOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function StopCircleOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
9
10
  export declare function SmileOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
10
11
  export declare function PlusOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
11
12
  export declare function ImageOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
@@ -21,6 +21,9 @@ export function KeyboardOutlineIcon({ size, stroke = "currentColor", strokeWidth
21
21
  [17, 12.5],
22
22
  ].map(([cx, cy], index) => (_jsx("circle", { cx: cx, cy: cy, r: "0.8", fill: stroke }, index))), _jsx("path", { d: "M7 16.2H17", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" })] }));
23
23
  }
24
+ export function StopCircleOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.8, }) {
25
+ return (_jsxs(BaseIcon, { size: size, children: [_jsx("circle", { cx: "12", cy: "12", r: "9", stroke: stroke, strokeWidth: strokeWidth }), _jsx("rect", { x: "8.4", y: "8.4", width: "7.2", height: "7.2", rx: "1.4", stroke: stroke, strokeWidth: strokeWidth, strokeLinejoin: "round" })] }));
26
+ }
24
27
  export function SmileOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.7, }) {
25
28
  return (_jsxs(BaseIcon, { size: size, children: [_jsx("circle", { cx: "12", cy: "12", r: "9", stroke: stroke, strokeWidth: strokeWidth }), _jsx("circle", { cx: "9", cy: "10", r: "1", fill: stroke }), _jsx("circle", { cx: "15", cy: "10", r: "1", fill: stroke }), _jsx("path", { d: "M8.2 14.2C9.15 15.55 10.42 16.2 12 16.2C13.58 16.2 14.85 15.55 15.8 14.2", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" })] }));
26
29
  }
@@ -7,6 +7,7 @@ import { MicEvents, MicStateManager } from "../voice/MicStateManager";
7
7
  const DEFAULT_WAVEFORM = [
8
8
  0.18, 0.28, 0.42, 0.58, 0.74, 0.9, 0.74, 0.58, 0.42, 0.28, 0.18,
9
9
  ];
10
+ const DEFAULT_TRANSCRIBE_TIMEOUT_MS = 30_000;
10
11
  export function useVoiceInputController(config) {
11
12
  const managerRef = useRef(null);
12
13
  const stopActionRef = useRef("send");
@@ -68,10 +69,11 @@ export function useVoiceInputController(config) {
68
69
  }, []);
69
70
  async function transcribeBlob(blob) {
70
71
  try {
72
+ const transcribeTimeoutMs = configRef.current?.transcribeTimeoutMs ?? DEFAULT_TRANSCRIBE_TIMEOUT_MS;
71
73
  const nextTranscript = configRef.current?.asrClient
72
- ? (await configRef.current.asrClient.transcribe(blob, {
74
+ ? (await withTimeout(configRef.current.asrClient.transcribe(blob, {
73
75
  lang: configRef.current.lang,
74
- })).text.trim()
76
+ }), transcribeTimeoutMs, "Voice transcription timed out. Please try again.")).text.trim()
75
77
  : (configRef.current?.simulatedTranscript ?? "hello from transcript").trim();
76
78
  if (!nextTranscript) {
77
79
  setStatus("error");
@@ -131,3 +133,20 @@ export function useVoiceInputController(config) {
131
133
  stopRecording,
132
134
  };
133
135
  }
136
+ function withTimeout(promise, timeoutMs, message) {
137
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
138
+ return promise;
139
+ }
140
+ return new Promise((resolve, reject) => {
141
+ const timeout = globalThis.setTimeout(() => {
142
+ reject(new Error(message));
143
+ }, timeoutMs);
144
+ promise.then((value) => {
145
+ globalThis.clearTimeout(timeout);
146
+ resolve(value);
147
+ }, (error) => {
148
+ globalThis.clearTimeout(timeout);
149
+ reject(error);
150
+ });
151
+ });
152
+ }
package/lib/index.d.ts CHANGED
@@ -16,6 +16,10 @@ export * from "./hooks/useTranscriptComposer";
16
16
  export * from "./hooks/useVoiceInputController";
17
17
  export * from "./services/agentSdkASRClient";
18
18
  export * from "./services/authTokenResolver";
19
+ export * from "./runtime/botAuth";
20
+ export * from "./runtime/botKey";
21
+ export * from "./runtime/chatClient";
22
+ export * from "./runtime/thread";
19
23
  export * from "./tokens/defaultTokens";
20
24
  export * from "./tokens/chatInputThemes";
21
25
  export * from "./types/attachment";
package/lib/index.js CHANGED
@@ -16,6 +16,10 @@ export * from "./hooks/useTranscriptComposer";
16
16
  export * from "./hooks/useVoiceInputController";
17
17
  export * from "./services/agentSdkASRClient";
18
18
  export * from "./services/authTokenResolver";
19
+ export * from "./runtime/botAuth";
20
+ export * from "./runtime/botKey";
21
+ export * from "./runtime/chatClient";
22
+ export * from "./runtime/thread";
19
23
  export * from "./tokens/defaultTokens";
20
24
  export * from "./tokens/chatInputThemes";
21
25
  export * from "./types/attachment";
@@ -0,0 +1,32 @@
1
+ import { type PmateBotKeyFile } from "./botKey.js";
2
+ export type BotLoginChallenge = {
3
+ id: string;
4
+ app: string;
5
+ name: string;
6
+ accountId: string;
7
+ credentialId: string;
8
+ challenge: string;
9
+ issuedAt: string;
10
+ expiresAt: string;
11
+ };
12
+ export type BotLoginSession = {
13
+ token: string;
14
+ identity?: {
15
+ accountId?: string;
16
+ app?: string;
17
+ kind?: string;
18
+ };
19
+ session?: {
20
+ identity?: {
21
+ accountId?: string;
22
+ app?: string;
23
+ kind?: string;
24
+ };
25
+ };
26
+ };
27
+ export declare function loginPmateBot(input: {
28
+ botKey: PmateBotKeyFile;
29
+ authBaseUrl?: string;
30
+ fetchImpl?: typeof fetch;
31
+ }): Promise<BotLoginSession>;
32
+ export declare function canonicalBotChallenge(challenge: BotLoginChallenge): string;
@@ -0,0 +1,71 @@
1
+ import { botCredentialId } from "./botKey.js";
2
+ const DEFAULT_AUTH_API_BASE_URL = "https://auth-api-v2.pmate.chat";
3
+ export async function loginPmateBot(input) {
4
+ const fetcher = input.fetchImpl ?? fetch;
5
+ const baseUrl = (input.authBaseUrl ?? DEFAULT_AUTH_API_BASE_URL).replace(/\/+$/, "");
6
+ const credentialId = botCredentialId(input.botKey);
7
+ const challenge = await post(fetcher, baseUrl, "/accounts/bots/session/challenge", {
8
+ app: input.botKey.app,
9
+ name: input.botKey.name,
10
+ credentialId,
11
+ });
12
+ const signature = await signBotChallenge(challenge, input.botKey.privateKey);
13
+ return post(fetcher, baseUrl, "/accounts/bots/session", {
14
+ app: challenge.app,
15
+ name: challenge.name,
16
+ credentialId: challenge.credentialId,
17
+ challengeId: challenge.id,
18
+ challenge: challenge.challenge,
19
+ signature,
20
+ });
21
+ }
22
+ export function canonicalBotChallenge(challenge) {
23
+ return JSON.stringify({
24
+ id: challenge.id,
25
+ app: challenge.app,
26
+ name: challenge.name,
27
+ accountId: challenge.accountId,
28
+ credentialId: challenge.credentialId,
29
+ challenge: challenge.challenge,
30
+ issuedAt: challenge.issuedAt,
31
+ expiresAt: challenge.expiresAt,
32
+ });
33
+ }
34
+ async function post(fetcher, baseUrl, path, body) {
35
+ const response = await fetcher(`${baseUrl}${path}?app=${encodeURIComponent(String(body.app ?? ""))}`, {
36
+ method: "POST",
37
+ headers: { "Content-Type": "application/json" },
38
+ body: JSON.stringify(body),
39
+ });
40
+ const json = (await response.json());
41
+ if (!response.ok || !json.success || !json.data) {
42
+ throw new Error(json.message || `Bot auth request failed: ${path}`);
43
+ }
44
+ return json.data;
45
+ }
46
+ async function signBotChallenge(challenge, privateKey) {
47
+ const subtle = globalThis.crypto?.subtle;
48
+ if (!subtle) {
49
+ throw new Error("WebCrypto is required for PMate bot login");
50
+ }
51
+ const key = await subtle.importKey("pkcs8", base64UrlToBytes(privateKey), "Ed25519", false, ["sign"]);
52
+ const signature = await subtle.sign("Ed25519", key, new TextEncoder().encode(canonicalBotChallenge(challenge)));
53
+ return bytesToBase64Url(new Uint8Array(signature));
54
+ }
55
+ function base64UrlToBytes(value) {
56
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
57
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
58
+ const binary = atob(padded);
59
+ const bytes = new Uint8Array(binary.length);
60
+ for (let index = 0; index < binary.length; index += 1) {
61
+ bytes[index] = binary.charCodeAt(index);
62
+ }
63
+ return bytes;
64
+ }
65
+ function bytesToBase64Url(bytes) {
66
+ let binary = "";
67
+ for (const byte of bytes) {
68
+ binary += String.fromCharCode(byte);
69
+ }
70
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
71
+ }
@@ -0,0 +1,14 @@
1
+ export type PmateBotKeyFile = {
2
+ type: "pmate-bot-key";
3
+ version: 1;
4
+ algorithm: "ed25519";
5
+ app: string;
6
+ name: string;
7
+ publicKey: string;
8
+ privateKey: string;
9
+ createdAt: string;
10
+ credentialId?: string;
11
+ publicKeyId?: string;
12
+ };
13
+ export declare function parsePmateBotKeyJson(input: string | unknown): PmateBotKeyFile;
14
+ export declare function botCredentialId(key: PmateBotKeyFile): string;
@@ -0,0 +1,25 @@
1
+ export function parsePmateBotKeyJson(input) {
2
+ const value = typeof input === "string" ? JSON.parse(input) : input;
3
+ if (!value || typeof value !== "object") {
4
+ throw new Error("Invalid PMate bot key JSON");
5
+ }
6
+ const candidate = value;
7
+ if (candidate.type !== "pmate-bot-key") {
8
+ throw new Error('Invalid PMate bot key JSON: expected type "pmate-bot-key"');
9
+ }
10
+ if (candidate.version !== 1) {
11
+ throw new Error("Invalid PMate bot key JSON: expected version 1");
12
+ }
13
+ if (candidate.algorithm !== "ed25519") {
14
+ throw new Error('Invalid PMate bot key JSON: expected algorithm "ed25519"');
15
+ }
16
+ for (const key of ["app", "name", "publicKey", "privateKey", "createdAt"]) {
17
+ if (!candidate[key]?.trim()) {
18
+ throw new Error(`Invalid PMate bot key JSON: missing ${key}`);
19
+ }
20
+ }
21
+ return candidate;
22
+ }
23
+ export function botCredentialId(key) {
24
+ return key.credentialId || key.publicKeyId || key.publicKey;
25
+ }
@@ -0,0 +1,62 @@
1
+ import { type BotLoginSession } from "./botAuth.js";
2
+ import { type PmateBotKeyFile } from "./botKey.js";
3
+ import { type ChatBody, type ChatRuntimeMessage, type RealtimeClient } from "./protocol.js";
4
+ export type ChatClientOptions = {
5
+ appId: string;
6
+ botKey?: PmateBotKeyFile | string | unknown;
7
+ session?: BotLoginSession;
8
+ hubUrl?: string;
9
+ authBaseUrl?: string;
10
+ realtimeClient?: RealtimeClient;
11
+ fetchImpl?: typeof fetch;
12
+ ackTimeoutMs?: number;
13
+ };
14
+ export declare class ChatClient {
15
+ private readonly options;
16
+ private client;
17
+ private session;
18
+ private botKey;
19
+ private readonly pendingAcks;
20
+ private readonly messageHandlers;
21
+ constructor(options: ChatClientOptions);
22
+ connect(): Promise<this>;
23
+ close(): void;
24
+ watchMessages(handler: (message: ChatRuntimeMessage) => void): () => boolean;
25
+ requestFriend(input: {
26
+ to: string;
27
+ message?: string;
28
+ }): Promise<ChatRuntimeMessage>;
29
+ acceptFriend(input: {
30
+ to: string;
31
+ requestId?: string;
32
+ message?: string;
33
+ }): Promise<ChatRuntimeMessage>;
34
+ rejectFriend(input: {
35
+ to: string;
36
+ requestId?: string;
37
+ reason?: string;
38
+ }): Promise<ChatRuntimeMessage>;
39
+ sendDm(input: {
40
+ to: string;
41
+ text: string;
42
+ }): Promise<ChatRuntimeMessage>;
43
+ createGroup(input: {
44
+ groupId: string;
45
+ members: string[];
46
+ title?: string;
47
+ }): Promise<ChatRuntimeMessage>;
48
+ sendGroup(input: {
49
+ groupId: string;
50
+ text: string;
51
+ recipients?: string[];
52
+ }): Promise<ChatRuntimeMessage>;
53
+ sendChat(to: string, body: ChatBody, recipients?: string[]): Promise<ChatRuntimeMessage>;
54
+ private sendContactAction;
55
+ private sendAuth;
56
+ private sendAndWaitForAck;
57
+ private handleIncoming;
58
+ private waitUntilConnected;
59
+ private accountId;
60
+ private requireClient;
61
+ }
62
+ export declare function createChatClient(options: ChatClientOptions): ChatClient;
@@ -0,0 +1,209 @@
1
+ import { loginPmateBot } from "./botAuth.js";
2
+ import { parsePmateBotKeyJson } from "./botKey.js";
3
+ import { ChatMsgKind, ChatMsgOp, ChatRealtimeEvents, RuntimeWebsocketClient, createRuntimeMsg, } from "./protocol.js";
4
+ const DEFAULT_HUB_URL = "wss://hub.pmate.chat";
5
+ const DEFAULT_ACK_TIMEOUT_MS = 5_000;
6
+ export class ChatClient {
7
+ options;
8
+ client = null;
9
+ session = null;
10
+ botKey = null;
11
+ pendingAcks = new Map();
12
+ messageHandlers = new Set();
13
+ constructor(options) {
14
+ this.options = options;
15
+ if (options.botKey) {
16
+ this.botKey = parsePmateBotKeyJson(options.botKey);
17
+ }
18
+ this.session = options.session ?? null;
19
+ this.client = options.realtimeClient ?? null;
20
+ }
21
+ async connect() {
22
+ if (!this.session) {
23
+ if (!this.botKey) {
24
+ throw new Error("ChatClient requires either session or PMate bot key JSON");
25
+ }
26
+ this.session = await loginPmateBot({
27
+ botKey: this.botKey,
28
+ authBaseUrl: this.options.authBaseUrl,
29
+ fetchImpl: this.options.fetchImpl,
30
+ });
31
+ }
32
+ const accountId = this.accountId();
33
+ if (!this.client) {
34
+ const hubUrl = normalizeWsUrl(this.options.hubUrl ?? DEFAULT_HUB_URL, accountId);
35
+ this.client = new RuntimeWebsocketClient(hubUrl);
36
+ }
37
+ this.client.on(ChatRealtimeEvents.Message, (message) => {
38
+ this.handleIncoming(message);
39
+ });
40
+ await this.waitUntilConnected();
41
+ await this.sendAuth();
42
+ return this;
43
+ }
44
+ close() {
45
+ this.client?.close();
46
+ this.client = null;
47
+ for (const pending of this.pendingAcks.values()) {
48
+ pending.reject(new Error("ChatClient closed"));
49
+ clearTimeout(pending.timer);
50
+ }
51
+ this.pendingAcks.clear();
52
+ }
53
+ watchMessages(handler) {
54
+ this.messageHandlers.add(handler);
55
+ return () => this.messageHandlers.delete(handler);
56
+ }
57
+ async requestFriend(input) {
58
+ return this.sendContactAction(input.to, "friendRequest", {
59
+ message: input.message,
60
+ });
61
+ }
62
+ async acceptFriend(input) {
63
+ return this.sendContactAction(input.to, "friendAccept", {
64
+ requestId: input.requestId,
65
+ message: input.message,
66
+ });
67
+ }
68
+ async rejectFriend(input) {
69
+ return this.sendContactAction(input.to, "friendReject", {
70
+ requestId: input.requestId,
71
+ reason: input.reason,
72
+ });
73
+ }
74
+ async sendDm(input) {
75
+ return this.sendChat(input.to, {
76
+ kind: "message",
77
+ threadKind: "dm",
78
+ contentKind: "text",
79
+ content: { appId: this.options.appId, text: input.text },
80
+ });
81
+ }
82
+ async createGroup(input) {
83
+ return this.sendChat(input.groupId, {
84
+ kind: "group",
85
+ threadKind: "group",
86
+ action: "groupCreate",
87
+ payload: { appId: this.options.appId, groupId: input.groupId, members: input.members, title: input.title },
88
+ }, input.members);
89
+ }
90
+ async sendGroup(input) {
91
+ return this.sendChat(input.groupId, {
92
+ kind: "message",
93
+ threadKind: "group",
94
+ contentKind: "text",
95
+ content: { appId: this.options.appId, groupId: input.groupId, text: input.text },
96
+ }, input.recipients);
97
+ }
98
+ async sendChat(to, body, recipients) {
99
+ const msg = createRuntimeMsg(this.accountId(), to, ChatMsgOp.CHAT, body, ChatMsgKind.DM);
100
+ if (recipients) {
101
+ msg.recipients = recipients;
102
+ }
103
+ await this.sendAndWaitForAck(msg);
104
+ return msg;
105
+ }
106
+ async sendContactAction(to, action, payload) {
107
+ return this.sendChat(to, {
108
+ kind: "contact",
109
+ threadKind: "dm",
110
+ action,
111
+ payload: cleanObject({ appId: this.options.appId, ...payload }),
112
+ });
113
+ }
114
+ async sendAuth() {
115
+ const token = this.session?.token;
116
+ if (!token) {
117
+ throw new Error("Missing PMate session token");
118
+ }
119
+ const auth = createRuntimeMsg(this.accountId(), "@hub", ChatMsgOp.AUTH, {
120
+ token,
121
+ as: "user",
122
+ }, ChatMsgKind.DM);
123
+ await this.sendAndWaitForAck(auth);
124
+ }
125
+ async sendAndWaitForAck(msg) {
126
+ const client = this.requireClient();
127
+ const ack = new Promise((resolve, reject) => {
128
+ const timer = setTimeout(() => {
129
+ this.pendingAcks.delete(msg.hash);
130
+ reject(new Error(`Timed out waiting for ack of ${msg.hash}`));
131
+ }, this.options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS);
132
+ this.pendingAcks.set(msg.hash, { resolve, reject, timer });
133
+ });
134
+ await client.sendMsg(msg);
135
+ return ack;
136
+ }
137
+ handleIncoming(message) {
138
+ if (message.opcode === ChatMsgOp.ACK) {
139
+ const ack = message.body;
140
+ const pending = this.pendingAcks.get(ack.ackOf);
141
+ if (pending) {
142
+ this.pendingAcks.delete(ack.ackOf);
143
+ clearTimeout(pending.timer);
144
+ if (ack.status === "ok") {
145
+ pending.resolve(ack);
146
+ }
147
+ else {
148
+ pending.reject(new Error(ack.message || ack.code || "Ack failed"));
149
+ }
150
+ }
151
+ return;
152
+ }
153
+ if (message.opcode === ChatMsgOp.CHAT) {
154
+ for (const handler of this.messageHandlers) {
155
+ handler(message);
156
+ }
157
+ }
158
+ }
159
+ waitUntilConnected() {
160
+ const client = this.requireClient();
161
+ if (client.isConnected()) {
162
+ return Promise.resolve();
163
+ }
164
+ return new Promise((resolve, reject) => {
165
+ const offConnected = client.on(ChatRealtimeEvents.Connected, (connected) => {
166
+ if (connected) {
167
+ offConnected();
168
+ offError();
169
+ resolve();
170
+ }
171
+ });
172
+ const offError = client.on(ChatRealtimeEvents.Error, (error) => {
173
+ offConnected();
174
+ offError();
175
+ reject(error instanceof Error ? error : new Error("WebSocket connection failed"));
176
+ });
177
+ });
178
+ }
179
+ accountId() {
180
+ const accountId = this.session?.session?.identity?.accountId ||
181
+ this.session?.identity?.accountId ||
182
+ this.botKey?.name;
183
+ if (!accountId) {
184
+ throw new Error("Missing account id");
185
+ }
186
+ return accountId;
187
+ }
188
+ requireClient() {
189
+ if (!this.client) {
190
+ throw new Error("ChatClient is not connected");
191
+ }
192
+ return this.client;
193
+ }
194
+ }
195
+ export function createChatClient(options) {
196
+ return new ChatClient(options);
197
+ }
198
+ function normalizeWsUrl(baseUrl, userId) {
199
+ const url = new URL(baseUrl);
200
+ if (url.protocol === "http:")
201
+ url.protocol = "ws:";
202
+ if (url.protocol === "https:")
203
+ url.protocol = "wss:";
204
+ url.searchParams.set("userId", userId);
205
+ return url.toString();
206
+ }
207
+ function cleanObject(input) {
208
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== null));
209
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./botAuth.js";
2
+ export * from "./botKey.js";
3
+ export * from "./chatClient.js";
4
+ export * from "./protocol.js";
5
+ export * from "./thread.js";
@@ -0,0 +1,5 @@
1
+ export * from "./botAuth.js";
2
+ export * from "./botKey.js";
3
+ export * from "./chatClient.js";
4
+ export * from "./protocol.js";
5
+ export * from "./thread.js";
@@ -0,0 +1,83 @@
1
+ export declare const ChatMsgOp: {
2
+ readonly CHAT: 1;
3
+ readonly ACK: 3;
4
+ readonly AUTH: 4;
5
+ };
6
+ export declare const ChatMsgKind: {
7
+ readonly DM: 1;
8
+ readonly GROUP: 2;
9
+ };
10
+ export declare const ChatRealtimeEvents: {
11
+ readonly Connected: "connected";
12
+ readonly Error: "error";
13
+ readonly Message: "message";
14
+ };
15
+ export type ChatBody = {
16
+ kind: "message";
17
+ threadKind: "dm" | "group";
18
+ contentKind: "text" | "image" | "emoji";
19
+ content: Record<string, unknown>;
20
+ } | {
21
+ kind: "pending";
22
+ threadKind: "dm" | "group";
23
+ pendingId: string;
24
+ expire?: number;
25
+ content?: Record<string, unknown>;
26
+ } | {
27
+ kind: "quote";
28
+ threadKind: "dm" | "group";
29
+ quotedMessageId: string;
30
+ content: Record<string, unknown>;
31
+ } | {
32
+ kind: "contact" | "group";
33
+ threadKind: "dm" | "group";
34
+ action: string;
35
+ payload?: Record<string, unknown>;
36
+ } | {
37
+ kind: "recall" | "edit" | "delete" | "hello";
38
+ threadKind: "dm" | "group";
39
+ [key: string]: unknown;
40
+ };
41
+ export type AckBody = {
42
+ ackOf: string;
43
+ status: "ok" | "error";
44
+ code?: string;
45
+ message?: string;
46
+ data?: unknown;
47
+ };
48
+ export type ChatRuntimeMessage = {
49
+ hash: string;
50
+ t: number;
51
+ from: string;
52
+ to: string;
53
+ body: unknown;
54
+ kind: number;
55
+ opcode: number;
56
+ pending?: {
57
+ expire: number;
58
+ };
59
+ quote?: string;
60
+ recipients?: string[];
61
+ };
62
+ export interface RealtimeClient {
63
+ on<T>(topic: string, handler: (value: T) => void): () => void;
64
+ isConnected(): boolean;
65
+ close(manual?: boolean): void;
66
+ sendMsg(msg: ChatRuntimeMessage): Promise<void>;
67
+ getConnectionType(): "websocket";
68
+ }
69
+ export declare function createRuntimeMsg(from: string, to: string, opcode: number, body: unknown, kind?: number): ChatRuntimeMessage;
70
+ export declare class RuntimeWebsocketClient implements RealtimeClient {
71
+ private readonly wsUrl;
72
+ private ws;
73
+ private connected;
74
+ private readonly listeners;
75
+ constructor(wsUrl: string);
76
+ on<T>(topic: string, handler: (value: T) => void): () => boolean;
77
+ isConnected(): boolean;
78
+ close(): void;
79
+ sendMsg(msg: ChatRuntimeMessage): Promise<void>;
80
+ getConnectionType(): "websocket";
81
+ private connect;
82
+ private emit;
83
+ }