@kittymi/openclaw-generic-http 0.1.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.
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/channel/account.d.ts +7 -0
- package/dist/channel/account.js +18 -0
- package/dist/channel/capabilities.d.ts +10 -0
- package/dist/channel/capabilities.js +11 -0
- package/dist/channel/host-adapter.d.ts +28 -0
- package/dist/channel/host-adapter.js +36 -0
- package/dist/channel/lifecycle.d.ts +18 -0
- package/dist/channel/lifecycle.js +28 -0
- package/dist/channel/plugin.d.ts +46 -0
- package/dist/channel/plugin.js +120 -0
- package/dist/channel/probe.d.ts +19 -0
- package/dist/channel/probe.js +149 -0
- package/dist/channel/resolve.d.ts +30 -0
- package/dist/channel/resolve.js +98 -0
- package/dist/channel/stream.d.ts +35 -0
- package/dist/channel/stream.js +127 -0
- package/dist/config/host-config-schema.d.ts +21 -0
- package/dist/config/host-config-schema.js +80 -0
- package/dist/config/loader.d.ts +7 -0
- package/dist/config/loader.js +38 -0
- package/dist/config/schema.d.ts +48 -0
- package/dist/config/schema.js +1 -0
- package/dist/errors/codes.d.ts +11 -0
- package/dist/errors/codes.js +10 -0
- package/dist/errors/exceptions.d.ts +7 -0
- package/dist/errors/exceptions.js +12 -0
- package/dist/inbound/mapper.d.ts +21 -0
- package/dist/inbound/mapper.js +22 -0
- package/dist/inbound/validator.d.ts +4 -0
- package/dist/inbound/validator.js +114 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +31 -0
- package/dist/mapping/conversation-mapper.d.ts +1 -0
- package/dist/mapping/conversation-mapper.js +3 -0
- package/dist/mapping/sender-mapper.d.ts +1 -0
- package/dist/mapping/sender-mapper.js +3 -0
- package/dist/mapping/thread-mapper.d.ts +1 -0
- package/dist/mapping/thread-mapper.js +7 -0
- package/dist/openclaw-entry.d.ts +276 -0
- package/dist/openclaw-entry.js +728 -0
- package/dist/outbound/client.d.ts +6 -0
- package/dist/outbound/client.js +1 -0
- package/dist/outbound/controller.d.ts +15 -0
- package/dist/outbound/controller.js +28 -0
- package/dist/outbound/http-client.d.ts +23 -0
- package/dist/outbound/http-client.js +150 -0
- package/dist/outbound/mapper.d.ts +29 -0
- package/dist/outbound/mapper.js +19 -0
- package/dist/outbound/mock-client.d.ts +18 -0
- package/dist/outbound/mock-client.js +26 -0
- package/dist/outbound/sender.d.ts +3 -0
- package/dist/outbound/sender.js +5 -0
- package/dist/protocol/attachments.d.ts +10 -0
- package/dist/protocol/attachments.js +56 -0
- package/dist/protocol/dto.d.ts +46 -0
- package/dist/protocol/dto.js +1 -0
- package/dist/protocol/serializer.d.ts +1 -0
- package/dist/protocol/serializer.js +3 -0
- package/dist/security/nonce-store.d.ts +30 -0
- package/dist/security/nonce-store.js +32 -0
- package/dist/security/signer.d.ts +10 -0
- package/dist/security/signer.js +20 -0
- package/dist/security/verifier.d.ts +2 -0
- package/dist/security/verifier.js +20 -0
- package/dist/setup-entry.d.ts +351 -0
- package/dist/setup-entry.js +73 -0
- package/dist/utils/json.d.ts +1 -0
- package/dist/utils/json.js +3 -0
- package/dist/utils/log.d.ts +1 -0
- package/dist/utils/log.js +3 -0
- package/dist/utils/time.d.ts +1 -0
- package/dist/utils/time.js +3 -0
- package/openclaw.config.schema.json +80 -0
- package/openclaw.plugin.json +175 -0
- package/package.json +72 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { GenericHttpAccountConfig } from "../config/schema.js";
|
|
2
|
+
export interface ResolveRequest {
|
|
3
|
+
accountId?: string | null;
|
|
4
|
+
kind: "conversation" | "sender";
|
|
5
|
+
query: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ResolveResult {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
kind: ResolveRequest["kind"];
|
|
11
|
+
}
|
|
12
|
+
export interface ResolveResponse {
|
|
13
|
+
success: true;
|
|
14
|
+
results: ResolveResult[];
|
|
15
|
+
}
|
|
16
|
+
export interface ResolveAccountOptions {
|
|
17
|
+
fetchImpl?: typeof fetch;
|
|
18
|
+
nowEpochSeconds?: () => number;
|
|
19
|
+
nonceFactory?: () => string;
|
|
20
|
+
requestIdFactory?: () => string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Minimal local resolve fallback.
|
|
24
|
+
*
|
|
25
|
+
* The first runtime loop has no external lookup client yet, so if callers
|
|
26
|
+
* already have a stable ID they can pass it through this method and still get a
|
|
27
|
+
* protocol-shaped resolve response.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveLocally(request: ResolveRequest): ResolveResponse;
|
|
30
|
+
export declare function resolveRemotely(accountConfig: GenericHttpAccountConfig, request: ResolveRequest, options?: ResolveAccountOptions): Promise<ResolveResponse>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { serializeProtocolObject } from "../protocol/serializer.js";
|
|
3
|
+
import { signPayload } from "../security/signer.js";
|
|
4
|
+
function buildSignedHeaders(accountConfig, method, path, rawBody, timestamp, nonce, requestId) {
|
|
5
|
+
const signingSecret = accountConfig.outboundSecret ?? accountConfig.signingSecret ?? "";
|
|
6
|
+
const signature = signPayload(signingSecret, {
|
|
7
|
+
method,
|
|
8
|
+
path,
|
|
9
|
+
timestamp,
|
|
10
|
+
nonce,
|
|
11
|
+
rawBody
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
accept: "application/json",
|
|
15
|
+
"content-type": "application/json",
|
|
16
|
+
"x-request-id": requestId,
|
|
17
|
+
"x-generic-http-version": "1",
|
|
18
|
+
"x-timestamp": timestamp,
|
|
19
|
+
"x-nonce": nonce,
|
|
20
|
+
"x-signature": signature,
|
|
21
|
+
"x-api-key": accountConfig.apiKey ?? ""
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Minimal local resolve fallback.
|
|
26
|
+
*
|
|
27
|
+
* The first runtime loop has no external lookup client yet, so if callers
|
|
28
|
+
* already have a stable ID they can pass it through this method and still get a
|
|
29
|
+
* protocol-shaped resolve response.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveLocally(request) {
|
|
32
|
+
const query = request.query.trim();
|
|
33
|
+
if (query === "") {
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
results: []
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
success: true,
|
|
41
|
+
results: [
|
|
42
|
+
{
|
|
43
|
+
id: query,
|
|
44
|
+
name: query,
|
|
45
|
+
kind: request.kind
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export async function resolveRemotely(accountConfig, request, options = {}) {
|
|
51
|
+
const query = request.query.trim();
|
|
52
|
+
if (query === "") {
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
results: []
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const resolvedOptions = {
|
|
59
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
60
|
+
nowEpochSeconds: options.nowEpochSeconds ?? (() => Math.floor(Date.now() / 1000)),
|
|
61
|
+
nonceFactory: options.nonceFactory ?? (() => randomUUID()),
|
|
62
|
+
requestIdFactory: options.requestIdFactory ?? (() => randomUUID())
|
|
63
|
+
};
|
|
64
|
+
const endpoint = new URL("/resolve", accountConfig.baseUrl);
|
|
65
|
+
const rawBody = serializeProtocolObject({
|
|
66
|
+
accountId: request.accountId ?? undefined,
|
|
67
|
+
kind: request.kind,
|
|
68
|
+
query
|
|
69
|
+
});
|
|
70
|
+
const timestamp = String(resolvedOptions.nowEpochSeconds());
|
|
71
|
+
const nonce = resolvedOptions.nonceFactory();
|
|
72
|
+
const requestId = resolvedOptions.requestIdFactory();
|
|
73
|
+
const headers = buildSignedHeaders(accountConfig, "POST", endpoint.pathname, rawBody, timestamp, nonce, requestId);
|
|
74
|
+
const response = await resolvedOptions.fetchImpl(endpoint.toString(), {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers,
|
|
77
|
+
body: rawBody
|
|
78
|
+
});
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(`POST /resolve failed with ${response.status} ${response.statusText}`);
|
|
81
|
+
}
|
|
82
|
+
const payload = (await response.json());
|
|
83
|
+
if (payload.success !== true || !Array.isArray(payload.results)) {
|
|
84
|
+
throw new Error("POST /resolve returned an invalid response payload");
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
results: payload.results
|
|
89
|
+
.filter((item) => typeof item.id === "string" &&
|
|
90
|
+
typeof item.name === "string" &&
|
|
91
|
+
(item.kind === "conversation" || item.kind === "sender"))
|
|
92
|
+
.map((item) => ({
|
|
93
|
+
id: item.id,
|
|
94
|
+
name: item.name,
|
|
95
|
+
kind: item.kind
|
|
96
|
+
}))
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { InboundMessageRequestDto } from "../protocol/dto.js";
|
|
2
|
+
import { type NormalizedInboundMessageEvent } from "../inbound/mapper.js";
|
|
3
|
+
import type { GenericHttpAccountConfig } from "../config/schema.js";
|
|
4
|
+
export interface StreamPullOptions {
|
|
5
|
+
fetchImpl?: typeof fetch;
|
|
6
|
+
nowEpochSeconds?: () => number;
|
|
7
|
+
nonceFactory?: () => string;
|
|
8
|
+
requestIdFactory?: () => string;
|
|
9
|
+
limit?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface StreamAckOptions {
|
|
12
|
+
fetchImpl?: typeof fetch;
|
|
13
|
+
nowEpochSeconds?: () => number;
|
|
14
|
+
nonceFactory?: () => string;
|
|
15
|
+
requestIdFactory?: () => string;
|
|
16
|
+
}
|
|
17
|
+
export interface PulledInboundMessage {
|
|
18
|
+
eventId: string;
|
|
19
|
+
accountId: string;
|
|
20
|
+
receivedAt: string;
|
|
21
|
+
request: InboundMessageRequestDto;
|
|
22
|
+
normalizedEvent: NormalizedInboundMessageEvent;
|
|
23
|
+
}
|
|
24
|
+
export interface PullInboundMessagesResult {
|
|
25
|
+
success: true;
|
|
26
|
+
accountId: string;
|
|
27
|
+
items: PulledInboundMessage[];
|
|
28
|
+
}
|
|
29
|
+
export interface AckInboundMessagesResult {
|
|
30
|
+
success: true;
|
|
31
|
+
accountId: string;
|
|
32
|
+
ackedEventIds: string[];
|
|
33
|
+
}
|
|
34
|
+
export declare function pullInboundMessages(accountId: string, accountConfig: GenericHttpAccountConfig, options?: StreamPullOptions): Promise<PullInboundMessagesResult>;
|
|
35
|
+
export declare function ackInboundMessages(accountId: string, eventIds: string[], accountConfig: GenericHttpAccountConfig, options?: StreamAckOptions): Promise<AckInboundMessagesResult>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { serializeProtocolObject } from "../protocol/serializer.js";
|
|
3
|
+
import { signPayload } from "../security/signer.js";
|
|
4
|
+
import { mapInboundMessage } from "../inbound/mapper.js";
|
|
5
|
+
import { validateInboundMessage } from "../inbound/validator.js";
|
|
6
|
+
function buildSignedHeaders(accountConfig, method, path, rawBody, timestamp, nonce, requestId) {
|
|
7
|
+
const signingSecret = accountConfig.outboundSecret ?? accountConfig.signingSecret ?? "";
|
|
8
|
+
const signature = signPayload(signingSecret, {
|
|
9
|
+
method,
|
|
10
|
+
path,
|
|
11
|
+
timestamp,
|
|
12
|
+
nonce,
|
|
13
|
+
rawBody
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
accept: "application/json, text/event-stream",
|
|
17
|
+
"content-type": "application/json",
|
|
18
|
+
"x-request-id": requestId,
|
|
19
|
+
"x-generic-http-version": "1",
|
|
20
|
+
"x-timestamp": timestamp,
|
|
21
|
+
"x-nonce": nonce,
|
|
22
|
+
"x-signature": signature,
|
|
23
|
+
"x-api-key": accountConfig.apiKey ?? ""
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function resolveRequiredOptions(options) {
|
|
27
|
+
return {
|
|
28
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
29
|
+
nowEpochSeconds: options.nowEpochSeconds ?? (() => Math.floor(Date.now() / 1000)),
|
|
30
|
+
nonceFactory: options.nonceFactory ?? (() => randomUUID()),
|
|
31
|
+
requestIdFactory: options.requestIdFactory ?? (() => randomUUID())
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function parseSseEvents(raw) {
|
|
35
|
+
const chunks = raw
|
|
36
|
+
.split(/\r?\n\r?\n/)
|
|
37
|
+
.map((chunk) => chunk.trim())
|
|
38
|
+
.filter((chunk) => chunk !== "");
|
|
39
|
+
return chunks.map((chunk) => {
|
|
40
|
+
const lines = chunk.split(/\r?\n/);
|
|
41
|
+
let eventName = "message";
|
|
42
|
+
const dataLines = [];
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
if (line.startsWith("event:")) {
|
|
45
|
+
eventName = line.slice("event:".length).trim();
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (line.startsWith("data:")) {
|
|
49
|
+
dataLines.push(line.slice("data:".length).trim());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
event: eventName,
|
|
54
|
+
data: dataLines.join("\n")
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export async function pullInboundMessages(accountId, accountConfig, options = {}) {
|
|
59
|
+
const resolvedOptions = resolveRequiredOptions(options);
|
|
60
|
+
const endpoint = new URL("/stream/inbound", accountConfig.baseUrl);
|
|
61
|
+
endpoint.searchParams.set("accountId", accountId);
|
|
62
|
+
endpoint.searchParams.set("limit", String(options.limit ?? 10));
|
|
63
|
+
const timestamp = String(resolvedOptions.nowEpochSeconds());
|
|
64
|
+
const nonce = resolvedOptions.nonceFactory();
|
|
65
|
+
const requestId = resolvedOptions.requestIdFactory();
|
|
66
|
+
const headers = buildSignedHeaders(accountConfig, "GET", endpoint.pathname, "", timestamp, nonce, requestId);
|
|
67
|
+
const response = await resolvedOptions.fetchImpl(endpoint.toString(), {
|
|
68
|
+
method: "GET",
|
|
69
|
+
headers
|
|
70
|
+
});
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
throw new Error(`GET /stream/inbound failed with ${response.status} ${response.statusText}`);
|
|
73
|
+
}
|
|
74
|
+
const rawSse = await response.text();
|
|
75
|
+
const items = parseSseEvents(rawSse)
|
|
76
|
+
.filter((entry) => entry.event === "inbound-message")
|
|
77
|
+
.map((entry) => JSON.parse(entry.data))
|
|
78
|
+
.map((entry) => {
|
|
79
|
+
validateInboundMessage(entry.request);
|
|
80
|
+
return {
|
|
81
|
+
eventId: entry.eventId,
|
|
82
|
+
accountId: entry.accountId,
|
|
83
|
+
receivedAt: entry.receivedAt,
|
|
84
|
+
request: entry.request,
|
|
85
|
+
normalizedEvent: mapInboundMessage(entry.request)
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
success: true,
|
|
90
|
+
accountId,
|
|
91
|
+
items
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export async function ackInboundMessages(accountId, eventIds, accountConfig, options = {}) {
|
|
95
|
+
const normalizedEventIds = eventIds
|
|
96
|
+
.map((eventId) => eventId.trim())
|
|
97
|
+
.filter((eventId) => eventId !== "");
|
|
98
|
+
const resolvedOptions = resolveRequiredOptions(options);
|
|
99
|
+
const endpoint = new URL("/stream/acks", accountConfig.baseUrl);
|
|
100
|
+
const rawBody = serializeProtocolObject({
|
|
101
|
+
accountId,
|
|
102
|
+
eventIds: normalizedEventIds
|
|
103
|
+
});
|
|
104
|
+
const timestamp = String(resolvedOptions.nowEpochSeconds());
|
|
105
|
+
const nonce = resolvedOptions.nonceFactory();
|
|
106
|
+
const requestId = resolvedOptions.requestIdFactory();
|
|
107
|
+
const headers = buildSignedHeaders(accountConfig, "POST", endpoint.pathname, rawBody, timestamp, nonce, requestId);
|
|
108
|
+
const response = await resolvedOptions.fetchImpl(endpoint.toString(), {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers,
|
|
111
|
+
body: rawBody
|
|
112
|
+
});
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
throw new Error(`POST /stream/acks failed with ${response.status} ${response.statusText}`);
|
|
115
|
+
}
|
|
116
|
+
const payload = (await response.json());
|
|
117
|
+
if (payload.success !== true ||
|
|
118
|
+
payload.accountId !== accountId ||
|
|
119
|
+
!Array.isArray(payload.ackedEventIds)) {
|
|
120
|
+
throw new Error("POST /stream/acks returned an invalid response payload");
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
success: true,
|
|
124
|
+
accountId,
|
|
125
|
+
ackedEventIds: payload.ackedEventIds.filter((eventId) => typeof eventId === "string")
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface GenericHttpHostConfigSchemaProperty {
|
|
2
|
+
type?: string;
|
|
3
|
+
title?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
format?: string;
|
|
6
|
+
minimum?: number;
|
|
7
|
+
default?: unknown;
|
|
8
|
+
properties?: Record<string, GenericHttpHostConfigSchemaProperty>;
|
|
9
|
+
required?: string[];
|
|
10
|
+
additionalProperties?: boolean | GenericHttpHostConfigSchemaProperty;
|
|
11
|
+
}
|
|
12
|
+
export interface GenericHttpHostConfigSchema {
|
|
13
|
+
$schema: string;
|
|
14
|
+
type: "object";
|
|
15
|
+
title: string;
|
|
16
|
+
description: string;
|
|
17
|
+
properties: Record<string, GenericHttpHostConfigSchemaProperty>;
|
|
18
|
+
required: string[];
|
|
19
|
+
additionalProperties: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare const genericHttpHostConfigSchema: GenericHttpHostConfigSchema;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export const genericHttpHostConfigSchema = {
|
|
2
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
type: "object",
|
|
4
|
+
title: "Generic HTTP Channel Config",
|
|
5
|
+
description: "Configuration for the OpenClaw generic HTTP channel plugin using bridge/relay webhook and stream ingress.",
|
|
6
|
+
properties: {
|
|
7
|
+
enabled: {
|
|
8
|
+
type: "boolean",
|
|
9
|
+
title: "Enabled",
|
|
10
|
+
description: "Whether the generic HTTP channel is enabled.",
|
|
11
|
+
default: false
|
|
12
|
+
},
|
|
13
|
+
defaultAccount: {
|
|
14
|
+
type: "string",
|
|
15
|
+
title: "Default Account",
|
|
16
|
+
description: "Default account ID used when the host does not specify one."
|
|
17
|
+
},
|
|
18
|
+
accounts: {
|
|
19
|
+
type: "object",
|
|
20
|
+
title: "Accounts",
|
|
21
|
+
description: "Per-account bridge and outbound transport settings.",
|
|
22
|
+
additionalProperties: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
baseUrl: {
|
|
26
|
+
type: "string",
|
|
27
|
+
format: "uri",
|
|
28
|
+
title: "Bridge Base URL",
|
|
29
|
+
description: "Bridge or relay base URL used for probe, resolve, stream ingress, and outbound delivery."
|
|
30
|
+
},
|
|
31
|
+
apiKey: {
|
|
32
|
+
type: "string",
|
|
33
|
+
title: "API Key",
|
|
34
|
+
description: "Optional shared credential for bridge authentication."
|
|
35
|
+
},
|
|
36
|
+
signingSecret: {
|
|
37
|
+
type: "string",
|
|
38
|
+
title: "Signing Secret",
|
|
39
|
+
description: "Shared secret used for signing outbound and stream transport requests."
|
|
40
|
+
},
|
|
41
|
+
inboundSecret: {
|
|
42
|
+
type: "string",
|
|
43
|
+
title: "Inbound Secret",
|
|
44
|
+
description: "Optional dedicated secret for inbound webhook validation on the bridge side."
|
|
45
|
+
},
|
|
46
|
+
outboundSecret: {
|
|
47
|
+
type: "string",
|
|
48
|
+
title: "Outbound Secret",
|
|
49
|
+
description: "Optional dedicated secret for outbound signing when outbound trust is split from inbound."
|
|
50
|
+
},
|
|
51
|
+
connectTimeoutMillis: {
|
|
52
|
+
type: "number",
|
|
53
|
+
minimum: 0,
|
|
54
|
+
title: "Connect Timeout (ms)",
|
|
55
|
+
description: "HTTP connect timeout in milliseconds.",
|
|
56
|
+
default: 5000
|
|
57
|
+
},
|
|
58
|
+
readTimeoutMillis: {
|
|
59
|
+
type: "number",
|
|
60
|
+
minimum: 0,
|
|
61
|
+
title: "Read Timeout (ms)",
|
|
62
|
+
description: "HTTP read timeout in milliseconds.",
|
|
63
|
+
default: 10000
|
|
64
|
+
},
|
|
65
|
+
maxRetries: {
|
|
66
|
+
type: "number",
|
|
67
|
+
minimum: 0,
|
|
68
|
+
title: "Max Retries",
|
|
69
|
+
description: "Maximum retry count for retryable outbound HTTP failures.",
|
|
70
|
+
default: 0
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
required: ["baseUrl"],
|
|
74
|
+
additionalProperties: false
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
required: ["enabled", "defaultAccount", "accounts"],
|
|
79
|
+
additionalProperties: false
|
|
80
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GenericHttpPluginConfig } from "./schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Normalize partially-hydrated config input into the runtime shape expected by
|
|
4
|
+
* the plugin. The plugin can then rely on explicit defaults instead of
|
|
5
|
+
* scattering fallback behavior across transport, validation, and routing code.
|
|
6
|
+
*/
|
|
7
|
+
export declare function loadConfig(rawConfig?: Partial<GenericHttpPluginConfig>): GenericHttpPluginConfig;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
2
|
+
const DEFAULT_CONNECT_TIMEOUT_MILLIS = 5000;
|
|
3
|
+
const DEFAULT_READ_TIMEOUT_MILLIS = 10000;
|
|
4
|
+
function normalizeAccountConfig(account) {
|
|
5
|
+
return {
|
|
6
|
+
baseUrl: account.baseUrl ?? "",
|
|
7
|
+
apiKey: account.apiKey,
|
|
8
|
+
signingSecret: account.signingSecret,
|
|
9
|
+
inboundSecret: account.inboundSecret,
|
|
10
|
+
outboundSecret: account.outboundSecret,
|
|
11
|
+
connectTimeoutMillis: account.connectTimeoutMillis ?? DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
|
12
|
+
readTimeoutMillis: account.readTimeoutMillis ?? DEFAULT_READ_TIMEOUT_MILLIS,
|
|
13
|
+
maxRetries: account.maxRetries ?? 0
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Normalize partially-hydrated config input into the runtime shape expected by
|
|
18
|
+
* the plugin. The plugin can then rely on explicit defaults instead of
|
|
19
|
+
* scattering fallback behavior across transport, validation, and routing code.
|
|
20
|
+
*/
|
|
21
|
+
export function loadConfig(rawConfig = {}) {
|
|
22
|
+
const defaultAccount = rawConfig.defaultAccount ?? DEFAULT_ACCOUNT_ID;
|
|
23
|
+
const rawAccounts = rawConfig.accounts ?? {};
|
|
24
|
+
const normalizedAccounts = {};
|
|
25
|
+
Object.entries(rawAccounts).forEach(([accountId, accountConfig]) => {
|
|
26
|
+
normalizedAccounts[accountId] = normalizeAccountConfig(accountConfig);
|
|
27
|
+
});
|
|
28
|
+
// Always materialize the declared default account so downstream routing code
|
|
29
|
+
// does not need a separate existence fallback.
|
|
30
|
+
if (normalizedAccounts[defaultAccount] === undefined) {
|
|
31
|
+
normalizedAccounts[defaultAccount] = normalizeAccountConfig({});
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
enabled: rawConfig.enabled ?? false,
|
|
35
|
+
defaultAccount,
|
|
36
|
+
accounts: normalizedAccounts
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-account runtime configuration for the generic HTTP channel.
|
|
3
|
+
*
|
|
4
|
+
* Keep this shape close to the public docs so operators can map config files,
|
|
5
|
+
* CLI setup, and runtime behavior without translating between models.
|
|
6
|
+
*/
|
|
7
|
+
export interface GenericHttpAccountConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Base URL of the third-party bridge service that receives outbound calls
|
|
10
|
+
* and exposes health/probe endpoints.
|
|
11
|
+
*/
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
/**
|
|
14
|
+
* Shared API credential used for basic transport authentication.
|
|
15
|
+
*/
|
|
16
|
+
apiKey?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Shared secret used to sign outbound requests and verify inbound requests.
|
|
19
|
+
*/
|
|
20
|
+
signingSecret?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Optional dedicated secret for inbound validation when callers should not
|
|
23
|
+
* share the same secret used for outbound plugin requests.
|
|
24
|
+
*/
|
|
25
|
+
inboundSecret?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Optional dedicated secret for outbound signing when rotation or separation
|
|
28
|
+
* of trust domains is required.
|
|
29
|
+
*/
|
|
30
|
+
outboundSecret?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Connection timeout for outbound HTTP calls, in milliseconds.
|
|
33
|
+
*/
|
|
34
|
+
connectTimeoutMillis?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Read timeout for outbound HTTP calls, in milliseconds.
|
|
37
|
+
*/
|
|
38
|
+
readTimeoutMillis?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Maximum retry attempts for retryable outbound transport failures.
|
|
41
|
+
*/
|
|
42
|
+
maxRetries?: number;
|
|
43
|
+
}
|
|
44
|
+
export interface GenericHttpPluginConfig {
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
defaultAccount: string;
|
|
47
|
+
accounts: Record<string, GenericHttpAccountConfig>;
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const ERROR_CODES: {
|
|
2
|
+
readonly INVALID_REQUEST: "INVALID_REQUEST";
|
|
3
|
+
readonly MISSING_REQUIRED_FIELD: "MISSING_REQUIRED_FIELD";
|
|
4
|
+
readonly INVALID_FIELD_FORMAT: "INVALID_FIELD_FORMAT";
|
|
5
|
+
readonly INVALID_CREDENTIAL: "INVALID_CREDENTIAL";
|
|
6
|
+
readonly INVALID_SIGNATURE: "INVALID_SIGNATURE";
|
|
7
|
+
readonly TIMESTAMP_EXPIRED: "TIMESTAMP_EXPIRED";
|
|
8
|
+
readonly NONCE_REPLAYED: "NONCE_REPLAYED";
|
|
9
|
+
readonly INTERNAL_ERROR: "INTERNAL_ERROR";
|
|
10
|
+
};
|
|
11
|
+
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const ERROR_CODES = {
|
|
2
|
+
INVALID_REQUEST: "INVALID_REQUEST",
|
|
3
|
+
MISSING_REQUIRED_FIELD: "MISSING_REQUIRED_FIELD",
|
|
4
|
+
INVALID_FIELD_FORMAT: "INVALID_FIELD_FORMAT",
|
|
5
|
+
INVALID_CREDENTIAL: "INVALID_CREDENTIAL",
|
|
6
|
+
INVALID_SIGNATURE: "INVALID_SIGNATURE",
|
|
7
|
+
TIMESTAMP_EXPIRED: "TIMESTAMP_EXPIRED",
|
|
8
|
+
NONCE_REPLAYED: "NONCE_REPLAYED",
|
|
9
|
+
INTERNAL_ERROR: "INTERNAL_ERROR"
|
|
10
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ErrorCode } from "./codes.js";
|
|
2
|
+
export declare class GenericHttpPluginError extends Error {
|
|
3
|
+
readonly code: ErrorCode;
|
|
4
|
+
readonly details?: Record<string, unknown>;
|
|
5
|
+
readonly retryable: boolean;
|
|
6
|
+
constructor(code: ErrorCode, message: string, details?: Record<string, unknown>, retryable?: boolean);
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class GenericHttpPluginError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
details;
|
|
4
|
+
retryable;
|
|
5
|
+
constructor(code, message, details, retryable = false) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "GenericHttpPluginError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.details = details;
|
|
10
|
+
this.retryable = retryable;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AttachmentDto, InboundMessageRequestDto } from "../protocol/dto.js";
|
|
2
|
+
export interface NormalizedInboundMessageEvent {
|
|
3
|
+
eventId: string;
|
|
4
|
+
eventType: "message.created";
|
|
5
|
+
accountId: string;
|
|
6
|
+
conversationId: string;
|
|
7
|
+
conversationType: string;
|
|
8
|
+
conversationTitle: string | null;
|
|
9
|
+
threadId: string | null;
|
|
10
|
+
senderId: string;
|
|
11
|
+
senderName: string | null;
|
|
12
|
+
senderType: string;
|
|
13
|
+
messageId: string;
|
|
14
|
+
text: string | null;
|
|
15
|
+
attachments: AttachmentDto[];
|
|
16
|
+
replyToMessageId: string | null;
|
|
17
|
+
occurredAt: string | null;
|
|
18
|
+
idempotencyKey: string | null;
|
|
19
|
+
metadata: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export declare function mapInboundMessage(request: InboundMessageRequestDto): NormalizedInboundMessageEvent;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function mapInboundMessage(request) {
|
|
2
|
+
return {
|
|
3
|
+
eventId: request.eventId,
|
|
4
|
+
eventType: "message.created",
|
|
5
|
+
accountId: request.accountId,
|
|
6
|
+
conversationId: request.conversation.conversationId,
|
|
7
|
+
conversationType: request.conversation.type,
|
|
8
|
+
conversationTitle: request.conversation.title ?? null,
|
|
9
|
+
threadId: request.threadId ?? null,
|
|
10
|
+
senderId: request.sender.id,
|
|
11
|
+
senderName: request.sender.name ?? null,
|
|
12
|
+
senderType: request.sender.type,
|
|
13
|
+
messageId: request.message.messageId,
|
|
14
|
+
text: request.message.text ?? null,
|
|
15
|
+
attachments: request.message.attachments ?? [],
|
|
16
|
+
replyToMessageId: request.message.replyToMessageId ?? null,
|
|
17
|
+
occurredAt: request.occurredAt ?? null,
|
|
18
|
+
idempotencyKey: request.idempotencyKey ?? null,
|
|
19
|
+
// Keep metadata object-shaped for downstream handlers even when omitted.
|
|
20
|
+
metadata: request.metadata ?? {}
|
|
21
|
+
};
|
|
22
|
+
}
|