@optimatist/langlangbot-connector 0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Optimatist Corp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # @optimatist/langlangbot-connector
2
+
3
+ TypeScript HTTP/SSE client for the LangLangBot sidecar gateway API.
4
+
5
+ Used by `@optimatist/langlangbot-openclaw` and other host adapters that speak to a local
6
+ LangLangBot sidecar over HTTPS.
7
+
8
+ ## License
9
+
10
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,163 @@
1
+ export type GatewayEventType = "conversation_opened" | "user_message" | "assistant_delta" | "assistant_message" | "tool_call" | "cancelled" | "approval_requested" | "approval_resolved";
2
+ export type ApprovalAction = {
3
+ decision: string;
4
+ label: string;
5
+ style?: string;
6
+ };
7
+ /**
8
+ * Wire/API approval kind — extensible string, not a closed union.
9
+ *
10
+ * Conventions:
11
+ * - OpenClaw native: `openclaw.exec`, `openclaw.plugin`
12
+ * - Gateway intent (Finance Gateway and future systems): manifest action id or
13
+ * `intent:<gateway>:<action>`; canonical payload in `metadata` (intent_id, intent_hash, …)
14
+ */
15
+ export type ApprovalKind = string;
16
+ /** Built-in OpenClaw approval kinds (sidecar polls → exec/plugin.approval.resolve). */
17
+ export declare const OpenClawApprovalKind: {
18
+ readonly Exec: "openclaw.exec";
19
+ readonly Plugin: "openclaw.plugin";
20
+ };
21
+ export type BuiltInOpenClawApprovalKind = (typeof OpenClawApprovalKind)[keyof typeof OpenClawApprovalKind];
22
+ /** Normalize legacy aliases from early Phase 2 drafts. */
23
+ export declare function normalizeApprovalKind(kind: string): ApprovalKind;
24
+ export declare function isOpenClawApprovalKind(kind: string): boolean;
25
+ export type OpenClawApprovalDecision = "allow-once" | "allow-always" | "deny";
26
+ export declare function isOpenClawApprovalDecision(value: string): value is OpenClawApprovalDecision;
27
+ export type ApprovalPluginEvent = {
28
+ type: "approval_decided";
29
+ approval_id: string;
30
+ decision: string;
31
+ } | {
32
+ type: "approval_resolved";
33
+ approval_id: string;
34
+ decision?: string | null;
35
+ };
36
+ export type RegisterPendingApprovalInput = {
37
+ approvalId: string;
38
+ kind: ApprovalKind;
39
+ conversationId?: string;
40
+ title: string;
41
+ description?: string;
42
+ actions?: ApprovalAction[];
43
+ metadata?: Record<string, unknown>;
44
+ expiresAt: string;
45
+ };
46
+ export type ApprovalDecisionPoll = {
47
+ status: "pending" | "decided" | "resolved" | "expired";
48
+ decision?: OpenClawApprovalDecision;
49
+ decidedAt?: string;
50
+ };
51
+ export type PluginConnectionEndpoint = {
52
+ transport: string;
53
+ address: string;
54
+ port: number;
55
+ };
56
+ export type PluginConnectionCurrentResponse = {
57
+ conversation_id: string;
58
+ status: "observed";
59
+ transport: string;
60
+ remote_addr?: string | null;
61
+ observed_at: string;
62
+ matched_endpoint?: PluginConnectionEndpoint | null;
63
+ published_endpoints: PluginConnectionEndpoint[];
64
+ } | {
65
+ conversation_id: string;
66
+ status: "unknown";
67
+ message: string;
68
+ published_endpoints: PluginConnectionEndpoint[];
69
+ } | {
70
+ status: "error";
71
+ message: string;
72
+ };
73
+ export type GatewayEvent = {
74
+ type: "conversation_opened";
75
+ conversation_id: string;
76
+ created_at: string;
77
+ } | {
78
+ type: "user_message";
79
+ message_id: string;
80
+ text: string;
81
+ received_at: string;
82
+ } | {
83
+ type: "assistant_delta";
84
+ text: string;
85
+ created_at: string;
86
+ } | {
87
+ type: "assistant_message";
88
+ message_id: string;
89
+ text: string;
90
+ created_at: string;
91
+ } | {
92
+ type: "tool_call";
93
+ tool_call_id: string;
94
+ tool_name: string;
95
+ summary: string;
96
+ created_at: string;
97
+ } | {
98
+ type: "cancelled";
99
+ reason?: string | null;
100
+ cancelled_at: string;
101
+ } | {
102
+ type: "approval_requested";
103
+ approval_id: string;
104
+ kind: string;
105
+ title: string;
106
+ description?: string | null;
107
+ actions: ApprovalAction[];
108
+ expires_at: string;
109
+ metadata?: Record<string, unknown>;
110
+ created_at: string;
111
+ } | {
112
+ type: "approval_resolved";
113
+ approval_id: string;
114
+ decision?: string | null;
115
+ resolved_at: string;
116
+ };
117
+ export type InboundMessage = {
118
+ conversationId: string;
119
+ messageId: string;
120
+ text: string;
121
+ receivedAt: string;
122
+ /** Base64 operator surface id attested by LangLangBot after ODA session.open. */
123
+ operatorSurfaceId?: string;
124
+ };
125
+ export type HealthStatus = {
126
+ status: string;
127
+ server_time?: string;
128
+ };
129
+ export type Unsubscribe = () => void;
130
+ export type LanglangbotSidecarOptions = {
131
+ baseUrl: string;
132
+ pluginToken?: string;
133
+ /** Trust self-signed Agent cert (loopback OpenClaw plugin only). */
134
+ insecureTls?: boolean;
135
+ /** SPKI pin (`sha256/<hex>`) for Operator-style verification. */
136
+ tlsFingerprint?: string;
137
+ /** Dev only: allow `http://` base URLs. */
138
+ allowInsecureHttp?: boolean;
139
+ fetchImpl?: typeof fetch;
140
+ };
141
+ export declare class LanglangbotSidecar {
142
+ readonly baseUrl: string;
143
+ readonly pluginToken?: string;
144
+ private readonly fetchImpl;
145
+ constructor(opts: LanglangbotSidecarOptions);
146
+ private headers;
147
+ health(): Promise<HealthStatus>;
148
+ subscribeInbound(onMessage: (evt: InboundMessage) => void, onError?: (err: Error) => void): Unsubscribe;
149
+ subscribeApprovalPluginEvents(onEvent: (evt: ApprovalPluginEvent) => void, onError?: (err: Error) => void): Unsubscribe;
150
+ subscribeConversationEvents(conversationId: string, onEvent: (evt: GatewayEvent) => void, onError?: (err: Error) => void): Unsubscribe;
151
+ sendDelta(conversationId: string, text: string): Promise<void>;
152
+ registerApprovalPending(input: RegisterPendingApprovalInput): Promise<{
153
+ approval_id: string;
154
+ status: string;
155
+ }>;
156
+ getApprovalDecision(approvalId: string): Promise<ApprovalDecisionPoll>;
157
+ markApprovalResolved(approvalId: string, decision?: OpenClawApprovalDecision): Promise<void>;
158
+ getPluginConnectionCurrent(conversationId: string): Promise<PluginConnectionCurrentResponse>;
159
+ sendMessage(conversationId: string, text: string, messageId?: string): Promise<{
160
+ message_id: string;
161
+ }>;
162
+ }
163
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js ADDED
@@ -0,0 +1,411 @@
1
+ /** Built-in OpenClaw approval kinds (sidecar polls → exec/plugin.approval.resolve). */
2
+ export const OpenClawApprovalKind = {
3
+ Exec: "openclaw.exec",
4
+ Plugin: "openclaw.plugin",
5
+ };
6
+ /** Normalize legacy aliases from early Phase 2 drafts. */
7
+ export function normalizeApprovalKind(kind) {
8
+ const trimmed = kind.trim();
9
+ switch (trimmed) {
10
+ case "exec":
11
+ case "openclaw_exec":
12
+ return OpenClawApprovalKind.Exec;
13
+ case "plugin":
14
+ case "openclaw_plugin":
15
+ return OpenClawApprovalKind.Plugin;
16
+ default:
17
+ return trimmed;
18
+ }
19
+ }
20
+ export function isOpenClawApprovalKind(kind) {
21
+ const normalized = normalizeApprovalKind(kind);
22
+ return (normalized === OpenClawApprovalKind.Exec ||
23
+ normalized === OpenClawApprovalKind.Plugin);
24
+ }
25
+ export function isOpenClawApprovalDecision(value) {
26
+ return (value === "allow-once" ||
27
+ value === "allow-always" ||
28
+ value === "deny");
29
+ }
30
+ import { assertHttpsBaseUrl } from "./endpoint-url.js";
31
+ import { createInsecureTlsFetch, createPinnedTlsFetch, } from "./tls-pin.js";
32
+ function normalizeBaseUrl(baseUrl) {
33
+ return baseUrl.replace(/\/+$/, "");
34
+ }
35
+ function createFetchImpl(opts) {
36
+ const base = normalizeBaseUrl(opts.baseUrl);
37
+ if (!opts.allowInsecureHttp && !base.startsWith("https://")) {
38
+ if (base.startsWith("http://")) {
39
+ throw new Error("sidecar baseUrl must use https:// (set allowInsecureHttp for dev only)");
40
+ }
41
+ assertHttpsBaseUrl(base);
42
+ }
43
+ if (opts.tlsFingerprint) {
44
+ return createPinnedTlsFetch({
45
+ tlsFingerprint: opts.tlsFingerprint,
46
+ insecureTls: opts.insecureTls,
47
+ });
48
+ }
49
+ if (opts.insecureTls) {
50
+ return createInsecureTlsFetch();
51
+ }
52
+ return fetch;
53
+ }
54
+ function parseGatewayEvent(raw) {
55
+ if (!raw || typeof raw !== "object") {
56
+ return null;
57
+ }
58
+ const event = raw;
59
+ const type = event.type;
60
+ if (typeof type !== "string") {
61
+ return null;
62
+ }
63
+ switch (type) {
64
+ case "conversation_opened":
65
+ return {
66
+ type,
67
+ conversation_id: String(event.conversation_id),
68
+ created_at: String(event.created_at),
69
+ };
70
+ case "user_message":
71
+ return {
72
+ type,
73
+ message_id: String(event.message_id),
74
+ text: String(event.text),
75
+ received_at: String(event.received_at),
76
+ };
77
+ case "assistant_delta":
78
+ return {
79
+ type,
80
+ text: String(event.text),
81
+ created_at: String(event.created_at),
82
+ };
83
+ case "assistant_message":
84
+ return {
85
+ type,
86
+ message_id: String(event.message_id),
87
+ text: String(event.text),
88
+ created_at: String(event.created_at),
89
+ };
90
+ case "tool_call":
91
+ return {
92
+ type,
93
+ tool_call_id: String(event.tool_call_id),
94
+ tool_name: String(event.tool_name),
95
+ summary: String(event.summary),
96
+ created_at: String(event.created_at),
97
+ };
98
+ case "cancelled":
99
+ return {
100
+ type,
101
+ reason: event.reason == null ? null : String(event.reason),
102
+ cancelled_at: String(event.cancelled_at),
103
+ };
104
+ case "approval_requested":
105
+ return {
106
+ type,
107
+ approval_id: String(event.approval_id),
108
+ kind: String(event.kind),
109
+ title: String(event.title),
110
+ description: event.description == null ? undefined : String(event.description),
111
+ actions: Array.isArray(event.actions)
112
+ ? event.actions
113
+ : [],
114
+ expires_at: String(event.expires_at),
115
+ metadata: event.metadata && typeof event.metadata === "object"
116
+ ? event.metadata
117
+ : undefined,
118
+ created_at: String(event.created_at),
119
+ };
120
+ case "approval_resolved":
121
+ return {
122
+ type,
123
+ approval_id: String(event.approval_id),
124
+ decision: event.decision == null ? undefined : String(event.decision),
125
+ resolved_at: String(event.resolved_at),
126
+ };
127
+ default:
128
+ return null;
129
+ }
130
+ }
131
+ function parseApprovalPluginEvent(raw) {
132
+ if (!raw || typeof raw !== "object") {
133
+ return null;
134
+ }
135
+ const event = raw;
136
+ const type = event.type;
137
+ if (type === "approval_decided") {
138
+ const approvalId = event.approval_id;
139
+ const decision = event.decision;
140
+ if (typeof approvalId !== "string" || typeof decision !== "string") {
141
+ return null;
142
+ }
143
+ return { type, approval_id: approvalId, decision };
144
+ }
145
+ if (type === "approval_resolved") {
146
+ const approvalId = event.approval_id;
147
+ if (typeof approvalId !== "string") {
148
+ return null;
149
+ }
150
+ return {
151
+ type,
152
+ approval_id: approvalId,
153
+ decision: event.decision == null ? undefined : String(event.decision),
154
+ };
155
+ }
156
+ return null;
157
+ }
158
+ function startReconnectingSse(params) {
159
+ const controller = new AbortController();
160
+ void (async () => {
161
+ let attempt = 0;
162
+ while (!controller.signal.aborted) {
163
+ try {
164
+ const response = await params.connect(controller.signal);
165
+ if (!response.ok) {
166
+ throw new Error(`${params.errorLabel}: ${response.status}`);
167
+ }
168
+ attempt = 0;
169
+ await consumeSse(response, (_eventName, data) => params.onData(data), controller.signal);
170
+ }
171
+ catch (err) {
172
+ if (controller.signal.aborted) {
173
+ return;
174
+ }
175
+ const error = err instanceof Error ? err : new Error(String(err));
176
+ params.onError?.(error);
177
+ attempt += 1;
178
+ const delayMs = Math.min(30_000, 1_000 * attempt);
179
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
180
+ }
181
+ }
182
+ })();
183
+ return () => controller.abort();
184
+ }
185
+ async function consumeSse(response, onEvent, signal) {
186
+ if (!response.body) {
187
+ throw new Error("SSE response has no body");
188
+ }
189
+ const reader = response.body.getReader();
190
+ const decoder = new TextDecoder();
191
+ let buffer = "";
192
+ let eventName = "message";
193
+ let dataLines = [];
194
+ const flush = () => {
195
+ if (dataLines.length === 0) {
196
+ return;
197
+ }
198
+ const payload = dataLines.length === 1 ? dataLines[0] : dataLines.join("\n");
199
+ onEvent(eventName, payload);
200
+ eventName = "message";
201
+ dataLines = [];
202
+ };
203
+ while (!signal?.aborted) {
204
+ const { done, value } = await reader.read();
205
+ if (done) {
206
+ flush();
207
+ break;
208
+ }
209
+ buffer += decoder.decode(value, { stream: true });
210
+ let newlineIndex = buffer.indexOf("\n");
211
+ while (newlineIndex >= 0) {
212
+ const line = buffer.slice(0, newlineIndex).replace(/\r$/, "");
213
+ buffer = buffer.slice(newlineIndex + 1);
214
+ if (line === "") {
215
+ flush();
216
+ }
217
+ else if (line.startsWith("event:")) {
218
+ eventName = line.slice(6).trim();
219
+ }
220
+ else if (line.startsWith("data:")) {
221
+ dataLines.push(line.slice(5).trimStart());
222
+ }
223
+ newlineIndex = buffer.indexOf("\n");
224
+ }
225
+ }
226
+ }
227
+ export class LanglangbotSidecar {
228
+ baseUrl;
229
+ pluginToken;
230
+ fetchImpl;
231
+ constructor(opts) {
232
+ this.baseUrl = normalizeBaseUrl(opts.baseUrl);
233
+ this.pluginToken = opts.pluginToken;
234
+ this.fetchImpl = opts.fetchImpl ?? createFetchImpl(opts);
235
+ }
236
+ headers(extra) {
237
+ const headers = {
238
+ "content-type": "application/json",
239
+ ...extra,
240
+ };
241
+ if (this.pluginToken) {
242
+ headers["x-langlangbot-plugin-token"] = this.pluginToken;
243
+ }
244
+ return headers;
245
+ }
246
+ async health() {
247
+ const response = await this.fetchImpl(`${this.baseUrl}/health`);
248
+ if (!response.ok) {
249
+ throw new Error(`health check failed: ${response.status}`);
250
+ }
251
+ return (await response.json());
252
+ }
253
+ subscribeInbound(onMessage, onError) {
254
+ return startReconnectingSse({
255
+ errorLabel: "inbound SSE failed",
256
+ onError,
257
+ connect: (signal) => this.fetchImpl(`${this.baseUrl}/v1/inbound/events`, {
258
+ headers: this.headers({ accept: "text/event-stream" }),
259
+ signal,
260
+ }),
261
+ onData: (data) => {
262
+ try {
263
+ const parsed = JSON.parse(data);
264
+ if (!parsed.conversation_id ||
265
+ !parsed.message_id ||
266
+ typeof parsed.text !== "string") {
267
+ return;
268
+ }
269
+ onMessage({
270
+ conversationId: parsed.conversation_id,
271
+ messageId: parsed.message_id,
272
+ text: parsed.text,
273
+ receivedAt: parsed.received_at ?? new Date().toISOString(),
274
+ operatorSurfaceId: parsed.operator_surface_id?.trim() || undefined,
275
+ });
276
+ }
277
+ catch (err) {
278
+ if (err instanceof SyntaxError) {
279
+ return;
280
+ }
281
+ throw err;
282
+ }
283
+ },
284
+ });
285
+ }
286
+ subscribeApprovalPluginEvents(onEvent, onError) {
287
+ return startReconnectingSse({
288
+ errorLabel: "approval plugin SSE failed",
289
+ onError,
290
+ connect: (signal) => this.fetchImpl(`${this.baseUrl}/v1/approvals/plugin/events`, {
291
+ headers: this.headers({ accept: "text/event-stream" }),
292
+ signal,
293
+ }),
294
+ onData: (data) => {
295
+ try {
296
+ const parsed = parseApprovalPluginEvent(JSON.parse(data));
297
+ if (parsed) {
298
+ onEvent(parsed);
299
+ }
300
+ }
301
+ catch (err) {
302
+ if (err instanceof SyntaxError) {
303
+ return;
304
+ }
305
+ throw err;
306
+ }
307
+ },
308
+ });
309
+ }
310
+ subscribeConversationEvents(conversationId, onEvent, onError) {
311
+ return startReconnectingSse({
312
+ errorLabel: "conversation SSE failed",
313
+ onError,
314
+ connect: (signal) => this.fetchImpl(`${this.baseUrl}/v1/conversations/${conversationId}/events`, {
315
+ headers: this.headers({ accept: "text/event-stream" }),
316
+ signal,
317
+ }),
318
+ onData: (data) => {
319
+ try {
320
+ const parsed = parseGatewayEvent(JSON.parse(data));
321
+ if (parsed) {
322
+ onEvent(parsed);
323
+ }
324
+ }
325
+ catch (err) {
326
+ if (err instanceof SyntaxError) {
327
+ return;
328
+ }
329
+ throw err;
330
+ }
331
+ },
332
+ });
333
+ }
334
+ async sendDelta(conversationId, text) {
335
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/conversations/${conversationId}/outbound/delta`, {
336
+ method: "POST",
337
+ headers: this.headers(),
338
+ body: JSON.stringify({ text }),
339
+ });
340
+ if (!response.ok) {
341
+ throw new Error(`sendDelta failed: ${response.status}`);
342
+ }
343
+ }
344
+ async registerApprovalPending(input) {
345
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/approvals/pending`, {
346
+ method: "POST",
347
+ headers: this.headers(),
348
+ body: JSON.stringify({
349
+ approval_id: input.approvalId,
350
+ kind: normalizeApprovalKind(input.kind),
351
+ conversation_id: input.conversationId,
352
+ title: input.title,
353
+ description: input.description,
354
+ actions: input.actions,
355
+ metadata: input.metadata ?? {},
356
+ expires_at: input.expiresAt,
357
+ }),
358
+ });
359
+ if (!response.ok) {
360
+ throw new Error(`registerApprovalPending failed: ${response.status}`);
361
+ }
362
+ return (await response.json());
363
+ }
364
+ async getApprovalDecision(approvalId) {
365
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/approvals/${encodeURIComponent(approvalId)}/decision`, { headers: this.headers() });
366
+ if (!response.ok) {
367
+ throw new Error(`getApprovalDecision failed: ${response.status}`);
368
+ }
369
+ const body = (await response.json());
370
+ return {
371
+ status: body.status,
372
+ decision: body.decision,
373
+ decidedAt: body.decided_at,
374
+ };
375
+ }
376
+ async markApprovalResolved(approvalId, decision) {
377
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/approvals/${encodeURIComponent(approvalId)}/resolved`, {
378
+ method: "POST",
379
+ headers: this.headers(),
380
+ body: JSON.stringify({ decision }),
381
+ });
382
+ if (!response.ok) {
383
+ throw new Error(`markApprovalResolved failed: ${response.status}`);
384
+ }
385
+ }
386
+ async getPluginConnectionCurrent(conversationId) {
387
+ const params = new URLSearchParams({ conversation_id: conversationId });
388
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/plugin/connection/current?${params}`, {
389
+ method: "GET",
390
+ headers: this.headers(),
391
+ });
392
+ if (!response.ok) {
393
+ throw new Error(`getPluginConnectionCurrent failed: ${response.status}`);
394
+ }
395
+ return response.json();
396
+ }
397
+ async sendMessage(conversationId, text, messageId) {
398
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/conversations/${conversationId}/outbound/message`, {
399
+ method: "POST",
400
+ headers: this.headers(),
401
+ body: JSON.stringify({
402
+ text,
403
+ message_id: messageId,
404
+ }),
405
+ });
406
+ if (!response.ok) {
407
+ throw new Error(`sendMessage failed: ${response.status}`);
408
+ }
409
+ return (await response.json());
410
+ }
411
+ }
@@ -0,0 +1,15 @@
1
+ export declare const HTTPS_SCHEME = "https";
2
+ export declare const TLS_FINGERPRINT_PREFIX = "sha256/";
3
+ export type DirectEndpointLike = {
4
+ address: string;
5
+ port: number;
6
+ scheme?: string;
7
+ tls_fingerprint?: string;
8
+ };
9
+ /** Reject plaintext HTTP endpoints (production Operator policy). */
10
+ export declare function assertHttpsEndpoint(endpoint: DirectEndpointLike): void;
11
+ export declare function validateTlsFingerprint(fingerprint: string | undefined): void;
12
+ export declare function buildDirectEndpointUrl(endpoint: DirectEndpointLike): string;
13
+ /** Reject plaintext HTTP base URLs (Operator / production policy). */
14
+ export declare function assertHttpsBaseUrl(baseUrl: string): void;
15
+ //# sourceMappingURL=endpoint-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint-url.d.ts","sourceRoot":"","sources":["../src/endpoint-url.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,UAAU,CAAC;AACpC,eAAO,MAAM,sBAAsB,YAAY,CAAC;AAGhD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,oEAAoE;AACpE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAQtE;AAED,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,IAAI,CAYN;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,CAG3E;AAED,sEAAsE;AACtE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAYxD"}
@@ -0,0 +1,41 @@
1
+ export const HTTPS_SCHEME = "https";
2
+ export const TLS_FINGERPRINT_PREFIX = "sha256/";
3
+ const TLS_FINGERPRINT_HEX_RE = /^[0-9a-fA-F]{64}$/;
4
+ /** Reject plaintext HTTP endpoints (production Operator policy). */
5
+ export function assertHttpsEndpoint(endpoint) {
6
+ const scheme = endpoint.scheme?.trim().toLowerCase();
7
+ if (scheme !== HTTPS_SCHEME) {
8
+ throw new Error(`direct endpoint must use scheme=https (got ${scheme ?? "missing"})`);
9
+ }
10
+ validateTlsFingerprint(endpoint.tls_fingerprint);
11
+ }
12
+ export function validateTlsFingerprint(fingerprint) {
13
+ const trimmed = fingerprint?.trim();
14
+ if (!trimmed) {
15
+ throw new Error("tls_fingerprint is required");
16
+ }
17
+ if (!trimmed.startsWith(TLS_FINGERPRINT_PREFIX)) {
18
+ throw new Error("tls_fingerprint must start with sha256/");
19
+ }
20
+ const hex = trimmed.slice(TLS_FINGERPRINT_PREFIX.length);
21
+ if (!TLS_FINGERPRINT_HEX_RE.test(hex)) {
22
+ throw new Error("tls_fingerprint must be sha256/ followed by 64 hex chars");
23
+ }
24
+ }
25
+ export function buildDirectEndpointUrl(endpoint) {
26
+ assertHttpsEndpoint(endpoint);
27
+ return `${HTTPS_SCHEME}://${endpoint.address}:${endpoint.port}`;
28
+ }
29
+ /** Reject plaintext HTTP base URLs (Operator / production policy). */
30
+ export function assertHttpsBaseUrl(baseUrl) {
31
+ let parsed;
32
+ try {
33
+ parsed = new URL(baseUrl);
34
+ }
35
+ catch {
36
+ throw new Error(`invalid base URL: ${baseUrl}`);
37
+ }
38
+ if (parsed.protocol !== `${HTTPS_SCHEME}:`) {
39
+ throw new Error(`base URL must use scheme=https (got ${parsed.protocol.replace(":", "")})`);
40
+ }
41
+ }
@@ -0,0 +1,4 @@
1
+ export { LanglangbotSidecar, type ApprovalAction, type ApprovalDecisionPoll, type ApprovalPluginEvent, type ApprovalKind, isOpenClawApprovalDecision, type BuiltInOpenClawApprovalKind, OpenClawApprovalKind, isOpenClawApprovalKind, normalizeApprovalKind, type GatewayEvent, type GatewayEventType, type HealthStatus, type InboundMessage, type LanglangbotSidecarOptions, type OpenClawApprovalDecision, type PluginConnectionCurrentResponse, type PluginConnectionEndpoint, type RegisterPendingApprovalInput, type Unsubscribe, } from "./client.js";
2
+ export { HTTPS_SCHEME, TLS_FINGERPRINT_PREFIX, assertHttpsBaseUrl, assertHttpsEndpoint, buildDirectEndpointUrl, validateTlsFingerprint, type DirectEndpointLike, } from "./endpoint-url.js";
3
+ export { TlsTrustStore, createDirectAgentFetch, createInsecureTlsFetch, createPinnedTlsFetch, fingerprintsMatch, normalizeTlsFingerprint, spkiFingerprintFromCertDer, undiciFetchWithAgent, type DirectAgentClientOptions, type PinnedTlsFetchOptions, } from "./tls-pin.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,0BAA0B,EAC1B,KAAK,2BAA2B,EAChC,oBAAoB,EACpB,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,EACpC,KAAK,wBAAwB,EAC7B,KAAK,4BAA4B,EACjC,KAAK,WAAW,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,YAAY,EACZ,sBAAsB,EACtB,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,sBAAsB,EACtB,KAAK,kBAAkB,GACxB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,EACjB,uBAAuB,EACvB,0BAA0B,EAC1B,oBAAoB,EACpB,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,GAC3B,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { LanglangbotSidecar, isOpenClawApprovalDecision, OpenClawApprovalKind, isOpenClawApprovalKind, normalizeApprovalKind, } from "./client.js";
2
+ export { HTTPS_SCHEME, TLS_FINGERPRINT_PREFIX, assertHttpsBaseUrl, assertHttpsEndpoint, buildDirectEndpointUrl, validateTlsFingerprint, } from "./endpoint-url.js";
3
+ export { TlsTrustStore, createDirectAgentFetch, createInsecureTlsFetch, createPinnedTlsFetch, fingerprintsMatch, normalizeTlsFingerprint, spkiFingerprintFromCertDer, undiciFetchWithAgent, } from "./tls-pin.js";
@@ -0,0 +1,42 @@
1
+ import { Agent } from "undici";
2
+ import { type DirectEndpointLike } from "./endpoint-url.js";
3
+ /** SHA-256(SPKI DER) — must match langlangbot `tls::fingerprint_from_cert_pem`. */
4
+ export declare function spkiFingerprintFromCertDer(certDer: Buffer): string;
5
+ export declare function normalizeTlsFingerprint(fingerprint: string): string;
6
+ export declare function fingerprintsMatch(expected: string, observed: string): boolean;
7
+ /** Per-surface SPKI pins established at enrollment. */
8
+ export declare class TlsTrustStore {
9
+ private readonly pins;
10
+ pin(surfaceId: string, tlsFingerprint: string): void;
11
+ get(surfaceId: string): string | undefined;
12
+ has(surfaceId: string): boolean;
13
+ unpin(surfaceId: string): void;
14
+ /**
15
+ * Returns true when Makway reports a new fingerprint for a previously pinned surface.
16
+ * Operator must require Owner re-confirmation before accepting the new pin.
17
+ */
18
+ fingerprintChanged(surfaceId: string, tlsFingerprint: string): boolean;
19
+ /** Same as `pin`; name signals Owner re-confirmation after fingerprint change. */
20
+ confirmPin(surfaceId: string, tlsFingerprint: string): void;
21
+ }
22
+ export type PinnedTlsFetchOptions = {
23
+ tlsFingerprint: string;
24
+ /** Dev only — skip SPKI pin verification. */
25
+ insecureTls?: boolean;
26
+ };
27
+ export declare function undiciFetchWithAgent(agent: Agent): (input: RequestInfo | URL, init?: RequestInit) => ReturnType<typeof fetch>;
28
+ export declare function createInsecureTlsFetch(): typeof fetch;
29
+ export declare function createPinnedTlsFetch(opts: PinnedTlsFetchOptions): typeof fetch;
30
+ export type DirectAgentClientOptions = {
31
+ endpoint: DirectEndpointLike;
32
+ surfaceId: string;
33
+ trustStore: TlsTrustStore;
34
+ /** Dev only. */
35
+ insecureTls?: boolean;
36
+ };
37
+ /**
38
+ * Operator client for a pinned HTTPS direct endpoint.
39
+ * Requires enrollment pin in `trustStore` unless `insecureTls` (dev).
40
+ */
41
+ export declare function createDirectAgentFetch(opts: DirectAgentClientOptions): typeof fetch;
42
+ //# sourceMappingURL=tls-pin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tls-pin.d.ts","sourceRoot":"","sources":["../src/tls-pin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAA+D,MAAM,QAAQ,CAAC;AAI5F,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAE3B,mFAAmF;AACnF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKlE;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAOnE;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE7E;AAED,uDAAuD;AACvD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA6B;IAElD,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI;IAIpD,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI1C,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAI/B,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI9B;;;OAGG;IACH,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO;IAQtE,kFAAkF;IAClF,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI;CAG5D;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,6CAA6C;IAC7C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAIF,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,KAAK,GACX,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,UAAU,CAAC,OAAO,KAAK,CAAC,CAM5E;AAED,wBAAgB,sBAAsB,IAAI,OAAO,KAAK,CAGrD;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,qBAAqB,GAC1B,OAAO,KAAK,CAuBd;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,aAAa,CAAC;IAC1B,gBAAgB;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,wBAAwB,GAC7B,OAAO,KAAK,CAkBd"}
@@ -0,0 +1,102 @@
1
+ import { Agent, fetch as undiciFetch } from "undici";
2
+ import { createHash, X509Certificate } from "node:crypto";
3
+ import { TLS_FINGERPRINT_PREFIX, validateTlsFingerprint, } from "./endpoint-url.js";
4
+ /** SHA-256(SPKI DER) — must match langlangbot `tls::fingerprint_from_cert_pem`. */
5
+ export function spkiFingerprintFromCertDer(certDer) {
6
+ const x509 = new X509Certificate(certDer);
7
+ const spkiDer = x509.publicKey.export({ type: "spki", format: "der" });
8
+ const hex = createHash("sha256").update(spkiDer).digest("hex");
9
+ return `${TLS_FINGERPRINT_PREFIX}${hex}`;
10
+ }
11
+ export function normalizeTlsFingerprint(fingerprint) {
12
+ validateTlsFingerprint(fingerprint);
13
+ const hex = fingerprint
14
+ .trim()
15
+ .slice(TLS_FINGERPRINT_PREFIX.length)
16
+ .toLowerCase();
17
+ return `${TLS_FINGERPRINT_PREFIX}${hex}`;
18
+ }
19
+ export function fingerprintsMatch(expected, observed) {
20
+ return normalizeTlsFingerprint(expected) === normalizeTlsFingerprint(observed);
21
+ }
22
+ /** Per-surface SPKI pins established at enrollment. */
23
+ export class TlsTrustStore {
24
+ pins = new Map();
25
+ pin(surfaceId, tlsFingerprint) {
26
+ this.pins.set(surfaceId, normalizeTlsFingerprint(tlsFingerprint));
27
+ }
28
+ get(surfaceId) {
29
+ return this.pins.get(surfaceId);
30
+ }
31
+ has(surfaceId) {
32
+ return this.pins.has(surfaceId);
33
+ }
34
+ unpin(surfaceId) {
35
+ this.pins.delete(surfaceId);
36
+ }
37
+ /**
38
+ * Returns true when Makway reports a new fingerprint for a previously pinned surface.
39
+ * Operator must require Owner re-confirmation before accepting the new pin.
40
+ */
41
+ fingerprintChanged(surfaceId, tlsFingerprint) {
42
+ const existing = this.pins.get(surfaceId);
43
+ if (!existing) {
44
+ return false;
45
+ }
46
+ return !fingerprintsMatch(existing, tlsFingerprint);
47
+ }
48
+ /** Same as `pin`; name signals Owner re-confirmation after fingerprint change. */
49
+ confirmPin(surfaceId, tlsFingerprint) {
50
+ this.pin(surfaceId, tlsFingerprint);
51
+ }
52
+ }
53
+ export function undiciFetchWithAgent(agent) {
54
+ return (input, init) => undiciFetch(input, {
55
+ ...init,
56
+ dispatcher: agent,
57
+ });
58
+ }
59
+ export function createInsecureTlsFetch() {
60
+ const agent = new Agent({ connect: { rejectUnauthorized: false } });
61
+ return undiciFetchWithAgent(agent);
62
+ }
63
+ export function createPinnedTlsFetch(opts) {
64
+ if (opts.insecureTls) {
65
+ return createInsecureTlsFetch();
66
+ }
67
+ const expected = normalizeTlsFingerprint(opts.tlsFingerprint);
68
+ const agent = new Agent({
69
+ connect: {
70
+ // CA validation disabled; SPKI pin is enforced in checkServerIdentity.
71
+ rejectUnauthorized: false,
72
+ checkServerIdentity(_host, cert) {
73
+ const der = cert.raw;
74
+ const observed = spkiFingerprintFromCertDer(der);
75
+ if (!fingerprintsMatch(expected, observed)) {
76
+ return new Error(`TLS fingerprint mismatch: expected ${expected}, got ${observed}`);
77
+ }
78
+ return undefined;
79
+ },
80
+ },
81
+ });
82
+ return undiciFetchWithAgent(agent);
83
+ }
84
+ /**
85
+ * Operator client for a pinned HTTPS direct endpoint.
86
+ * Requires enrollment pin in `trustStore` unless `insecureTls` (dev).
87
+ */
88
+ export function createDirectAgentFetch(opts) {
89
+ if (opts.insecureTls) {
90
+ return createInsecureTlsFetch();
91
+ }
92
+ const pinned = opts.trustStore.get(opts.surfaceId);
93
+ if (!pinned) {
94
+ throw new Error(`no TLS pin for surface ${opts.surfaceId}; complete enrollment first`);
95
+ }
96
+ const reported = opts.endpoint.tls_fingerprint;
97
+ if (reported && opts.trustStore.fingerprintChanged(opts.surfaceId, reported)) {
98
+ throw new Error(`TLS fingerprint changed for surface ${opts.surfaceId}; Owner must re-confirm`);
99
+ }
100
+ const fingerprint = reported ? normalizeTlsFingerprint(reported) : pinned;
101
+ return createPinnedTlsFetch({ tlsFingerprint: fingerprint });
102
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@optimatist/langlangbot-connector",
3
+ "version": "0.1.0",
4
+ "description": "HTTP/SSE client for the LangLangBot gateway API",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/OptimatistOpenSource/openclaw-langlangbot.git",
9
+ "directory": "packages/connector"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "type": "module",
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "LICENSE"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.json",
29
+ "prepack": "npm run build"
30
+ },
31
+ "dependencies": {
32
+ "undici": "^7.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.15.0",
36
+ "typescript": "^5.8.3"
37
+ },
38
+ "engines": {
39
+ "node": ">=20"
40
+ }
41
+ }