@soyeht/soyeht 0.1.1

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,209 @@
1
+ /**
2
+ * Minimal type declarations for openclaw/plugin-sdk.
3
+ * Only the types used by the soyeht plugin are declared here.
4
+ * The full types live in the OpenClaw monorepo.
5
+ */
6
+ declare module "openclaw/plugin-sdk" {
7
+ // ---- Config ----
8
+ export type OpenClawConfig = Record<string, unknown>;
9
+
10
+ // ---- Channel types ----
11
+ export type ChannelId = string;
12
+
13
+ export type ChannelMeta = {
14
+ id: ChannelId;
15
+ label: string;
16
+ selectionLabel: string;
17
+ docsPath: string;
18
+ blurb: string;
19
+ order?: number;
20
+ aliases?: string[];
21
+ systemImage?: string;
22
+ [key: string]: unknown;
23
+ };
24
+
25
+ export type ChatType = "direct" | "group" | "thread";
26
+
27
+ export type ChannelCapabilities = {
28
+ chatTypes: Array<ChatType | "thread">;
29
+ media?: boolean;
30
+ polls?: boolean;
31
+ reactions?: boolean;
32
+ edit?: boolean;
33
+ unsend?: boolean;
34
+ reply?: boolean;
35
+ threads?: boolean;
36
+ [key: string]: unknown;
37
+ };
38
+
39
+ export type ChannelConfigAdapter<ResolvedAccount> = {
40
+ listAccountIds: (cfg: OpenClawConfig) => string[];
41
+ resolveAccount: (
42
+ cfg: OpenClawConfig,
43
+ accountId?: string | null,
44
+ ) => ResolvedAccount;
45
+ defaultAccountId?: (cfg: OpenClawConfig) => string;
46
+ isEnabled?: (account: ResolvedAccount, cfg: OpenClawConfig) => boolean;
47
+ isConfigured?: (
48
+ account: ResolvedAccount,
49
+ cfg: OpenClawConfig,
50
+ ) => boolean | Promise<boolean>;
51
+ describeAccount?: (account: ResolvedAccount, cfg: OpenClawConfig) => Record<string, unknown>;
52
+ [key: string]: unknown;
53
+ };
54
+
55
+ export type OutboundDeliveryResult = {
56
+ channel: string;
57
+ messageId: string;
58
+ chatId?: string;
59
+ meta?: Record<string, unknown>;
60
+ };
61
+
62
+ export type ChannelOutboundContext = {
63
+ cfg: OpenClawConfig;
64
+ to: string;
65
+ text: string;
66
+ mediaUrl?: string;
67
+ accountId?: string | null;
68
+ identity?: unknown;
69
+ deps?: unknown;
70
+ silent?: boolean;
71
+ [key: string]: unknown;
72
+ };
73
+
74
+ export type ChannelOutboundAdapter = {
75
+ deliveryMode: "direct" | "gateway" | "hybrid";
76
+ sendText?: (ctx: ChannelOutboundContext) => Promise<OutboundDeliveryResult>;
77
+ sendMedia?: (ctx: ChannelOutboundContext) => Promise<OutboundDeliveryResult>;
78
+ [key: string]: unknown;
79
+ };
80
+
81
+ export type ChannelPlugin<ResolvedAccount = unknown> = {
82
+ id: ChannelId;
83
+ meta: ChannelMeta;
84
+ capabilities: ChannelCapabilities;
85
+ configSchema?: { schema: Record<string, unknown> };
86
+ reload?: { configPrefixes?: string[] };
87
+ config: ChannelConfigAdapter<ResolvedAccount>;
88
+ outbound?: ChannelOutboundAdapter;
89
+ [key: string]: unknown;
90
+ };
91
+
92
+ export type ChannelRegistration<ResolvedAccount = unknown> = {
93
+ plugin: ChannelPlugin<ResolvedAccount>;
94
+ dock?: unknown;
95
+ [key: string]: unknown;
96
+ };
97
+
98
+ // ---- Gateway types ----
99
+ export type ErrorShape = {
100
+ code?: string;
101
+ message?: string;
102
+ [key: string]: unknown;
103
+ };
104
+
105
+ export type RespondFn = (
106
+ ok: boolean,
107
+ payload?: unknown,
108
+ error?: ErrorShape,
109
+ meta?: Record<string, unknown>,
110
+ ) => void;
111
+
112
+ export type GatewayRequestHandlerOptions = {
113
+ req: unknown;
114
+ params: Record<string, unknown>;
115
+ client: unknown | null;
116
+ respond: RespondFn;
117
+ context: unknown;
118
+ [key: string]: unknown;
119
+ };
120
+
121
+ export type GatewayRequestHandler = (
122
+ opts: GatewayRequestHandlerOptions,
123
+ ) => Promise<void> | void;
124
+
125
+ // ---- Plugin types ----
126
+ export type PluginLogger = {
127
+ info: (...args: unknown[]) => void;
128
+ warn: (...args: unknown[]) => void;
129
+ error: (...args: unknown[]) => void;
130
+ debug: (...args: unknown[]) => void;
131
+ };
132
+
133
+ export type TtsTelephonyResult = {
134
+ success: boolean;
135
+ audioBuffer?: Buffer;
136
+ error?: string;
137
+ latencyMs?: number;
138
+ provider?: string;
139
+ outputFormat?: string;
140
+ sampleRate?: number;
141
+ };
142
+
143
+ export type PluginRuntime = {
144
+ config: {
145
+ loadConfig: () => Promise<OpenClawConfig>;
146
+ [key: string]: unknown;
147
+ };
148
+ stt: {
149
+ transcribeAudioFile: (params: {
150
+ filePath: string;
151
+ cfg: OpenClawConfig;
152
+ agentDir?: string;
153
+ mime?: string;
154
+ }) => Promise<{ text: string | undefined }>;
155
+ };
156
+ tts: {
157
+ textToSpeechTelephony: (params: {
158
+ text: string;
159
+ cfg: OpenClawConfig;
160
+ prefsPath?: string;
161
+ }) => Promise<TtsTelephonyResult>;
162
+ };
163
+ [key: string]: unknown;
164
+ };
165
+
166
+ export type OpenClawPluginServiceContext = {
167
+ config: OpenClawConfig;
168
+ workspaceDir?: string;
169
+ stateDir: string;
170
+ logger: PluginLogger;
171
+ };
172
+
173
+ export type OpenClawPluginService = {
174
+ id: string;
175
+ start: (ctx?: OpenClawPluginServiceContext) => void | Promise<void>;
176
+ stop?: (ctx?: OpenClawPluginServiceContext) => void | Promise<void>;
177
+ };
178
+
179
+ export type OpenClawPluginHttpRouteHandler = (
180
+ req: import("node:http").IncomingMessage,
181
+ res: import("node:http").ServerResponse,
182
+ ) => Promise<boolean | void> | boolean | void;
183
+
184
+ export type OpenClawPluginApi = {
185
+ id: string;
186
+ name: string;
187
+ runtime: PluginRuntime;
188
+ logger: PluginLogger;
189
+ registerChannel: (registration: ChannelRegistration | unknown) => void;
190
+ registerGatewayMethod: (
191
+ method: string,
192
+ handler: GatewayRequestHandler,
193
+ ) => void;
194
+ registerHttpRoute: (params: {
195
+ path: string;
196
+ handler: OpenClawPluginHttpRouteHandler;
197
+ auth: "gateway" | "plugin";
198
+ match?: "exact" | "prefix";
199
+ }) => void;
200
+ registerService: (service: OpenClawPluginService) => void;
201
+ [key: string]: unknown;
202
+ };
203
+
204
+ // ---- Account helpers ----
205
+ export function listConfiguredAccountIds(params: {
206
+ accounts: Record<string, unknown> | undefined;
207
+ normalizeAccountId: (accountId: string) => string;
208
+ }): string[];
209
+ }
@@ -0,0 +1,198 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { OutboundEnvelope, TextMessagePayload, AudioMessagePayload, FileMessagePayload } from "./types.js";
3
+ import { encryptEnvelopeV2 } from "./envelope-v2.js";
4
+ import type { RatchetState, DhRatchetConfig } from "./ratchet.js";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Types
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export type OutboundDeliveryResult = {
11
+ channel: string;
12
+ messageId: string;
13
+ chatId?: string;
14
+ meta?: Record<string, unknown>;
15
+ };
16
+
17
+ export type DeliverTextParams = {
18
+ backendBaseUrl: string;
19
+ pluginAuthToken: string;
20
+ accountId: string;
21
+ sessionId: string;
22
+ to: string;
23
+ text: string;
24
+ };
25
+
26
+ export type DeliverAudioParams = {
27
+ backendBaseUrl: string;
28
+ pluginAuthToken: string;
29
+ accountId: string;
30
+ sessionId: string;
31
+ to: string;
32
+ url: string;
33
+ mimeType: string;
34
+ filename: string;
35
+ durationMs?: number;
36
+ transcript?: string;
37
+ };
38
+
39
+ export type DeliverFileParams = {
40
+ backendBaseUrl: string;
41
+ pluginAuthToken: string;
42
+ accountId: string;
43
+ sessionId: string;
44
+ to: string;
45
+ url: string;
46
+ mimeType: string;
47
+ filename: string;
48
+ sizeBytes?: number;
49
+ };
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Envelope builder
53
+ // ---------------------------------------------------------------------------
54
+
55
+ export function buildOutboundEnvelope(
56
+ accountId: string,
57
+ sessionId: string,
58
+ message: TextMessagePayload | AudioMessagePayload | FileMessagePayload,
59
+ ): OutboundEnvelope {
60
+ return {
61
+ accountId,
62
+ sessionId,
63
+ deliveryId: randomUUID(),
64
+ timestamp: Date.now(),
65
+ message,
66
+ };
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // HTTP POST to backend
71
+ // ---------------------------------------------------------------------------
72
+
73
+ export type PostToBackendOptions = {
74
+ ratchetSession?: RatchetState;
75
+ dhRatchetCfg?: DhRatchetConfig;
76
+ onSessionUpdated?: (updated: RatchetState) => void;
77
+ securityEnabled?: boolean;
78
+ };
79
+
80
+ export async function postToBackend(
81
+ backendBaseUrl: string,
82
+ pluginAuthToken: string,
83
+ envelope: OutboundEnvelope,
84
+ opts: PostToBackendOptions = {},
85
+ ): Promise<OutboundDeliveryResult> {
86
+ const url = `${backendBaseUrl.replace(/\/+$/, "")}/api/v1/messages/deliver`;
87
+
88
+ let body: string;
89
+ const headers: Record<string, string> = {
90
+ "Content-Type": "application/json",
91
+ Authorization: `Bearer ${pluginAuthToken}`,
92
+ "X-Soyeht-Delivery-Id": envelope.deliveryId,
93
+ };
94
+
95
+ if (opts.ratchetSession) {
96
+ // V2 path — fail-closed: reject expired sessions
97
+ if (opts.ratchetSession.expiresAt < Date.now()) {
98
+ return {
99
+ channel: "soyeht",
100
+ messageId: envelope.deliveryId,
101
+ meta: { error: true, reason: "session_expired" },
102
+ };
103
+ }
104
+ const dhRatchetCfg = opts.dhRatchetCfg ?? { intervalMessages: 50, intervalMs: 3_600_000 };
105
+ const { envelope: v2env, updatedSession } = encryptEnvelopeV2({
106
+ session: opts.ratchetSession,
107
+ accountId: envelope.accountId,
108
+ plaintext: JSON.stringify(envelope),
109
+ dhRatchetCfg,
110
+ });
111
+ opts.onSessionUpdated?.(updatedSession);
112
+ body = JSON.stringify(v2env);
113
+ headers["X-Soyeht-Version"] = "2";
114
+ headers["X-Soyeht-Account-Id"] = v2env.accountId;
115
+ headers["X-Soyeht-Direction"] = v2env.direction;
116
+ headers["X-Soyeht-Counter"] = String(v2env.counter);
117
+ headers["X-Soyeht-Timestamp"] = String(v2env.timestamp);
118
+ if (v2env.dhRatchetKey) headers["X-Soyeht-Dh-Ratchet-Key"] = v2env.dhRatchetKey;
119
+ } else if (opts.securityEnabled) {
120
+ // Fail-closed: security required but no session
121
+ return {
122
+ channel: "soyeht",
123
+ messageId: envelope.deliveryId,
124
+ meta: { error: true, reason: "session_required" },
125
+ };
126
+ } else {
127
+ body = JSON.stringify(envelope);
128
+ }
129
+
130
+ const res = await fetch(url, {
131
+ method: "POST",
132
+ headers,
133
+ body,
134
+ signal: AbortSignal.timeout(15_000),
135
+ });
136
+
137
+ if (!res.ok) {
138
+ return {
139
+ channel: "soyeht",
140
+ messageId: envelope.deliveryId,
141
+ meta: {
142
+ error: true,
143
+ statusCode: res.status,
144
+ statusText: res.statusText,
145
+ },
146
+ };
147
+ }
148
+
149
+ return {
150
+ channel: "soyeht",
151
+ messageId: envelope.deliveryId,
152
+ };
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Deliver functions
157
+ // ---------------------------------------------------------------------------
158
+
159
+ export async function deliverTextMessage(
160
+ params: DeliverTextParams,
161
+ ): Promise<OutboundDeliveryResult> {
162
+ const message: TextMessagePayload = {
163
+ contentType: "text",
164
+ text: params.text,
165
+ };
166
+ const envelope = buildOutboundEnvelope(params.accountId, params.sessionId, message);
167
+ return postToBackend(params.backendBaseUrl, params.pluginAuthToken, envelope, {});
168
+ }
169
+
170
+ export async function deliverAudioMessage(
171
+ params: DeliverAudioParams,
172
+ ): Promise<OutboundDeliveryResult> {
173
+ const message: AudioMessagePayload = {
174
+ contentType: "audio",
175
+ renderStyle: "voice_note",
176
+ mimeType: params.mimeType,
177
+ filename: params.filename,
178
+ durationMs: params.durationMs,
179
+ url: params.url,
180
+ transcript: params.transcript,
181
+ };
182
+ const envelope = buildOutboundEnvelope(params.accountId, params.sessionId, message);
183
+ return postToBackend(params.backendBaseUrl, params.pluginAuthToken, envelope, {});
184
+ }
185
+
186
+ export async function deliverFileMessage(
187
+ params: DeliverFileParams,
188
+ ): Promise<OutboundDeliveryResult> {
189
+ const message: FileMessagePayload = {
190
+ contentType: "file",
191
+ mimeType: params.mimeType,
192
+ filename: params.filename,
193
+ sizeBytes: params.sizeBytes,
194
+ url: params.url,
195
+ };
196
+ const envelope = buildOutboundEnvelope(params.accountId, params.sessionId, message);
197
+ return postToBackend(params.backendBaseUrl, params.pluginAuthToken, envelope, {});
198
+ }