@segmentstream/agent 0.1.4 → 0.2.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/README.md +64 -21
- package/dist/extension/instructions.md +11 -0
- package/dist/extension/lib/attachment-tools.d.ts +68 -0
- package/dist/extension/lib/attachment-tools.mjs +110 -0
- package/dist/extension/lib/attachments.d.ts +96 -0
- package/dist/extension/lib/attachments.mjs +233 -0
- package/dist/extension/lib/connection-proxy.d.ts +14 -1
- package/dist/extension/lib/connection-proxy.mjs +135 -39
- package/dist/extension/lib/environment.d.ts +13 -0
- package/dist/extension/lib/environment.mjs +8 -1
- package/dist/extension/lib/identities.d.ts +22 -1
- package/dist/extension/lib/identities.mjs +20 -0
- package/dist/extension/lib/runtime-identity.d.ts +22 -0
- package/dist/extension/lib/runtime-identity.mjs +62 -0
- package/dist/extension/lib/telegram/adapter.d.ts +6 -0
- package/dist/extension/lib/telegram/adapter.mjs +56 -6
- package/dist/extension/lib/telegram/chats.d.ts +46 -0
- package/dist/extension/lib/telegram/chats.mjs +123 -0
- package/dist/extension/lib/telegram/files.d.ts +25 -0
- package/dist/extension/lib/telegram/files.mjs +77 -0
- package/dist/extension/lib/telegram/inbound-request.d.ts +22 -1
- package/dist/extension/lib/telegram/inbound-request.mjs +30 -1
- package/dist/extension/lib/telegram/inbound.d.ts +17 -3
- package/dist/extension/lib/telegram/inbound.mjs +80 -0
- package/dist/extension/lib/telegram/proxy-fetch.mjs +3 -2
- package/dist/extension/lib/telegram/requests.d.ts +71 -0
- package/dist/extension/lib/telegram/requests.mjs +173 -0
- package/dist/extension/lib/telegram/voice.d.ts +2 -2
- package/dist/extension/lib/telegram/voice.mjs +2 -2
- package/dist/extension/tools/attachments_discard.d.ts +30 -0
- package/dist/extension/tools/attachments_discard.mjs +22 -0
- package/dist/extension/tools/attachments_list.d.ts +30 -0
- package/dist/extension/tools/attachments_list.mjs +25 -0
- package/dist/extension/tools/attachments_promote.d.ts +36 -0
- package/dist/extension/tools/attachments_promote.mjs +69 -0
- package/dist/extension/tools/attachments_read.d.ts +9 -0
- package/dist/extension/tools/attachments_read.mjs +43 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.mjs +5 -1
- package/package.json +3 -2
|
@@ -5,6 +5,8 @@ const __filename = __eveFileURLToPath(import.meta.url);
|
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
const MAXIMUM_PROXY_BODY_BYTES = 2097152;
|
|
8
|
+
const MAXIMUM_DOWNLOAD_BYTES = 20971520;
|
|
9
|
+
const PROXY_STREAM_MEDIA_TYPE = "application/vnd.segmentstream.proxy-stream.v1+ndjson";
|
|
8
10
|
const CALL_TIMEOUT_MS = 4e4;
|
|
9
11
|
var ConnectionProxyError = class extends Error {
|
|
10
12
|
name = "ConnectionProxyError";
|
|
@@ -55,59 +57,153 @@ function connectionProxy(input) {
|
|
|
55
57
|
const { environment, connection } = input;
|
|
56
58
|
const fetchImpl = input.fetch ?? fetch;
|
|
57
59
|
const url = `${`${environment.segmentstreamUrl}/api/internal/v1/workspaces/${environment.workspaceId}`}/ontology/connections/${encodeURIComponent(connection)}/request`;
|
|
60
|
+
async function send(call, accept) {
|
|
61
|
+
if (call.body.byteLength > 2097152) throw new ConnectionProxyError({
|
|
62
|
+
connection,
|
|
63
|
+
code: "invalid_request",
|
|
64
|
+
message: "The request body exceeds the proxy's 2 MiB limit."
|
|
65
|
+
});
|
|
66
|
+
const token = await input.token();
|
|
67
|
+
try {
|
|
68
|
+
return await fetchImpl(url, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
"x-segmentstream-runtime-identity": token,
|
|
72
|
+
"x-segmentstream-organization-id": environment.organizationId,
|
|
73
|
+
"content-type": "application/json",
|
|
74
|
+
accept
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
...call.api === void 0 ? {} : { api: call.api },
|
|
78
|
+
method: call.method,
|
|
79
|
+
path: call.path,
|
|
80
|
+
headers: call.headers,
|
|
81
|
+
body: Buffer.from(call.body).toString("base64")
|
|
82
|
+
}),
|
|
83
|
+
redirect: "error",
|
|
84
|
+
signal: AbortSignal.timeout(CALL_TIMEOUT_MS)
|
|
85
|
+
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new ConnectionProxyError({
|
|
88
|
+
connection,
|
|
89
|
+
code: error instanceof DOMException && error.name === "TimeoutError" ? "timeout" : "unavailable",
|
|
90
|
+
message: "SegmentStream's connection proxy could not be reached.",
|
|
91
|
+
cause: error
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function buffered(response) {
|
|
96
|
+
const text = await response.text();
|
|
97
|
+
let value;
|
|
98
|
+
try {
|
|
99
|
+
value = JSON.parse(text);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (response.ok) throw new ConnectionProxyError({
|
|
102
|
+
connection,
|
|
103
|
+
code: "invalid_response",
|
|
104
|
+
message: "The connection proxy answered with an invalid response.",
|
|
105
|
+
cause: error
|
|
106
|
+
});
|
|
107
|
+
throw refusal(connection, response.status, void 0, error);
|
|
108
|
+
}
|
|
109
|
+
if (!response.ok) throw refusal(connection, response.status, value);
|
|
110
|
+
return decodeReply(connection, value);
|
|
111
|
+
}
|
|
58
112
|
return {
|
|
59
113
|
connection,
|
|
60
114
|
async request(call) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
let response;
|
|
115
|
+
return buffered(await send(call, "application/json"));
|
|
116
|
+
},
|
|
117
|
+
async download(call, maxBytes = MAXIMUM_DOWNLOAD_BYTES) {
|
|
118
|
+
const response = await send(call, PROXY_STREAM_MEDIA_TYPE);
|
|
119
|
+
const type = response.headers.get("content-type") ?? "";
|
|
120
|
+
if (!response.ok || !type.startsWith("application/vnd.segmentstream.proxy-stream.v1+ndjson") || response.body === null) return buffered(response);
|
|
68
121
|
try {
|
|
69
|
-
|
|
70
|
-
method: "POST",
|
|
71
|
-
headers: {
|
|
72
|
-
"x-segmentstream-runtime-identity": token,
|
|
73
|
-
"x-segmentstream-organization-id": environment.organizationId,
|
|
74
|
-
"content-type": "application/json",
|
|
75
|
-
accept: "application/json"
|
|
76
|
-
},
|
|
77
|
-
body: JSON.stringify({
|
|
78
|
-
...call.api === void 0 ? {} : { api: call.api },
|
|
79
|
-
method: call.method,
|
|
80
|
-
path: call.path,
|
|
81
|
-
headers: call.headers,
|
|
82
|
-
body: Buffer.from(call.body).toString("base64")
|
|
83
|
-
}),
|
|
84
|
-
redirect: "error",
|
|
85
|
-
signal: AbortSignal.timeout(CALL_TIMEOUT_MS)
|
|
86
|
-
});
|
|
122
|
+
return await framedReply(connection, response.body, maxBytes);
|
|
87
123
|
} catch (error) {
|
|
124
|
+
if (error instanceof ConnectionProxyError) throw error;
|
|
88
125
|
throw new ConnectionProxyError({
|
|
89
126
|
connection,
|
|
90
127
|
code: error instanceof DOMException && error.name === "TimeoutError" ? "timeout" : "unavailable",
|
|
91
|
-
message: "SegmentStream's connection proxy
|
|
128
|
+
message: "SegmentStream's connection proxy stopped relaying the response.",
|
|
92
129
|
cause: error
|
|
93
130
|
});
|
|
94
131
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function framedReply(connection, body, maxBytes) {
|
|
136
|
+
const invalid = () => new ConnectionProxyError({
|
|
137
|
+
connection,
|
|
138
|
+
code: "invalid_response",
|
|
139
|
+
message: "The connection proxy answered with an invalid stream."
|
|
140
|
+
});
|
|
141
|
+
const reader = body.getReader();
|
|
142
|
+
const decoder = new TextDecoder();
|
|
143
|
+
const chunks = [];
|
|
144
|
+
let head;
|
|
145
|
+
let size = 0;
|
|
146
|
+
let pending = "";
|
|
147
|
+
let ended = false;
|
|
148
|
+
try {
|
|
149
|
+
while (!ended) {
|
|
150
|
+
const next = await reader.read();
|
|
151
|
+
if (next.done) break;
|
|
152
|
+
pending += decoder.decode(next.value, { stream: true });
|
|
153
|
+
for (let newline = pending.indexOf("\n"); newline >= 0; newline = pending.indexOf("\n")) {
|
|
154
|
+
const line = pending.slice(0, newline);
|
|
155
|
+
pending = pending.slice(newline + 1);
|
|
156
|
+
if (line.trim() === "") continue;
|
|
157
|
+
let frame;
|
|
158
|
+
try {
|
|
159
|
+
frame = JSON.parse(line);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw new ConnectionProxyError({
|
|
162
|
+
connection,
|
|
163
|
+
code: "invalid_response",
|
|
164
|
+
message: "The connection proxy answered with an invalid stream.",
|
|
165
|
+
cause: error
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (!isRecord(frame)) throw invalid();
|
|
169
|
+
if (head === void 0) {
|
|
170
|
+
if (typeof frame.statusCode !== "number" || !isHeaderPairs(frame.headers)) throw invalid();
|
|
171
|
+
head = {
|
|
172
|
+
status: frame.statusCode,
|
|
173
|
+
headers: frame.headers
|
|
174
|
+
};
|
|
175
|
+
} else if (typeof frame.data === "string") {
|
|
176
|
+
const chunk = Buffer.from(frame.data, "base64");
|
|
177
|
+
size += chunk.byteLength;
|
|
178
|
+
if (size > maxBytes) throw new ConnectionProxyError({
|
|
179
|
+
connection,
|
|
180
|
+
code: "too_large",
|
|
181
|
+
message: `The response exceeds the ${Math.floor(maxBytes / 1024 / 1024)} MiB download limit.`
|
|
182
|
+
});
|
|
183
|
+
chunks.push(chunk);
|
|
184
|
+
} else if (frame.end === true) {
|
|
185
|
+
ended = true;
|
|
186
|
+
break;
|
|
187
|
+
} else if (typeof frame.error === "string") throw new ConnectionProxyError({
|
|
101
188
|
connection,
|
|
102
|
-
code:
|
|
103
|
-
message: "
|
|
104
|
-
cause: error
|
|
189
|
+
code: frame.error,
|
|
190
|
+
message: "SegmentStream's connection proxy failed while relaying the response."
|
|
105
191
|
});
|
|
106
|
-
throw
|
|
192
|
+
else throw invalid();
|
|
107
193
|
}
|
|
108
|
-
if (!response.ok) throw refusal(connection, response.status, value);
|
|
109
|
-
return decodeReply(connection, value);
|
|
110
194
|
}
|
|
195
|
+
} finally {
|
|
196
|
+
await reader.cancel().catch(() => void 0);
|
|
197
|
+
}
|
|
198
|
+
if (head === void 0 || !ended) throw new ConnectionProxyError({
|
|
199
|
+
connection,
|
|
200
|
+
code: "unavailable",
|
|
201
|
+
message: "The connection proxy's stream ended before the response did."
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
status: head.status,
|
|
205
|
+
headers: head.headers,
|
|
206
|
+
body: new Uint8Array(Buffer.concat(chunks))
|
|
111
207
|
};
|
|
112
208
|
}
|
|
113
|
-
export { ConnectionProxyError, MAXIMUM_PROXY_BODY_BYTES, connectionProxy };
|
|
209
|
+
export { ConnectionProxyError, MAXIMUM_DOWNLOAD_BYTES, MAXIMUM_PROXY_BODY_BYTES, PROXY_STREAM_MEDIA_TYPE, connectionProxy };
|
|
@@ -20,6 +20,19 @@ export interface ProxyEnvironment {
|
|
|
20
20
|
readonly organizationId: string;
|
|
21
21
|
}
|
|
22
22
|
export declare function proxyEnvironment(environment?: Environment): ProxyEnvironment;
|
|
23
|
+
/**
|
|
24
|
+
* Whom the agent trusts as a caller of its own routes: Vercel OIDC tokens of the functions of this
|
|
25
|
+
* deployment's project, such as the ontology service's. The platform sets the issuer and the audience
|
|
26
|
+
* on the project for the ontology service; Vercel sets `VERCEL_PROJECT_ID`.
|
|
27
|
+
*/
|
|
28
|
+
export interface RuntimeIdentityEnvironment {
|
|
29
|
+
/** The team's issuer, `https://oidc.vercel.com/<team slug>`, without a trailing slash. */
|
|
30
|
+
readonly issuer: string;
|
|
31
|
+
/** The audience Vercel issues function tokens for: `https://vercel.com/<team slug>`. */
|
|
32
|
+
readonly audience: string;
|
|
33
|
+
readonly projectId: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function runtimeIdentityEnvironment(environment?: Environment): RuntimeIdentityEnvironment;
|
|
23
36
|
/** The secret Telegram echoes in `X-Telegram-Bot-Api-Secret-Token`; `undefined` when unset. */
|
|
24
37
|
export declare function webhookSecret(environment?: Environment): string | undefined;
|
|
25
38
|
/**
|
|
@@ -33,6 +33,13 @@ function proxyEnvironment(environment = process.env) {
|
|
|
33
33
|
organizationId: required(environment, "SEGMENTSTREAM_ORGANIZATION_ID")
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
function runtimeIdentityEnvironment(environment = process.env) {
|
|
37
|
+
return {
|
|
38
|
+
issuer: withoutTrailingSlashes(required(environment, "SEGMENTSTREAM_RUNTIME_OIDC_ISSUER")),
|
|
39
|
+
audience: required(environment, "SEGMENTSTREAM_RUNTIME_OIDC_AUDIENCE"),
|
|
40
|
+
projectId: required(environment, "VERCEL_PROJECT_ID")
|
|
41
|
+
};
|
|
42
|
+
}
|
|
36
43
|
function webhookSecret(environment = process.env) {
|
|
37
44
|
const secret = environment.SEGMENTSTREAM_AGENT_WEBHOOK_SECRET;
|
|
38
45
|
return secret === void 0 || secret === "" ? void 0 : secret;
|
|
@@ -43,4 +50,4 @@ function channelsEnabled(environment = process.env) {
|
|
|
43
50
|
function isPreview(environment = process.env) {
|
|
44
51
|
return environment.VERCEL_ENV === "preview";
|
|
45
52
|
}
|
|
46
|
-
export { AgentEnvironmentError, appOrigin, channelsEnabled, isPreview, proxyEnvironment, webhookSecret };
|
|
53
|
+
export { AgentEnvironmentError, appOrigin, channelsEnabled, isPreview, proxyEnvironment, runtimeIdentityEnvironment, webhookSecret };
|
|
@@ -3,7 +3,8 @@ import type { RuntimeToken } from "./runtime-token.js";
|
|
|
3
3
|
* The ontology service's pairing routes (`agent-design.md`, section 13, "Identity"). The agent
|
|
4
4
|
* calls them as the deployment, with its OIDC token: it looks up which member a chat account is
|
|
5
5
|
* paired with, and asks for a pairing code when it is paired with nobody. The member approves the
|
|
6
|
-
* code on the app's `/agent/connect` page.
|
|
6
|
+
* code on the app's `/agent/connect` page. It also reports the group chats it is in to the
|
|
7
|
+
* ontology's `agent_chat` registry.
|
|
7
8
|
*/
|
|
8
9
|
/** A member as the ontology resolves a paired chat account, with their current roles. */
|
|
9
10
|
export interface PairedMember {
|
|
@@ -33,12 +34,32 @@ export interface ChatAccount {
|
|
|
33
34
|
readonly provider: string;
|
|
34
35
|
readonly externalId: string;
|
|
35
36
|
}
|
|
37
|
+
/** A group chat in the ontology's `agent_chat` registry, with what members said it is for. */
|
|
38
|
+
export interface AgentChat {
|
|
39
|
+
readonly id: string;
|
|
40
|
+
readonly title: string;
|
|
41
|
+
readonly purpose: string | null;
|
|
42
|
+
readonly whenToUse: string | null;
|
|
43
|
+
}
|
|
36
44
|
export interface AgentIdentities {
|
|
37
45
|
lookup(account: ChatAccount): Promise<IdentityLookup>;
|
|
38
46
|
/** Issues a pairing code, or returns the account's pending one while it has time left. */
|
|
39
47
|
createPairing(account: ChatAccount & {
|
|
40
48
|
readonly display?: string;
|
|
41
49
|
}): Promise<Pairing>;
|
|
50
|
+
/**
|
|
51
|
+
* Records that the agent saw the group chat, and the paired member who wrote, if any, or the
|
|
52
|
+
* paired member who left it, whom the registry unlinks from the chat, or that the agent itself
|
|
53
|
+
* left it (`agentLeft`), which unlinks every member, or that the chat was the group
|
|
54
|
+
* `migratedFrom` before it became a supergroup, whose record the registry moves to the new id.
|
|
55
|
+
*/
|
|
56
|
+
seenChat(chat: ChatAccount & {
|
|
57
|
+
readonly title: string;
|
|
58
|
+
readonly memberId?: string;
|
|
59
|
+
readonly leftMemberId?: string;
|
|
60
|
+
readonly agentLeft?: true;
|
|
61
|
+
readonly migratedFrom?: string;
|
|
62
|
+
}): Promise<AgentChat>;
|
|
42
63
|
}
|
|
43
64
|
/** The ontology service refused or failed a pairing call. Carries its error code, not its body. */
|
|
44
65
|
export declare class AgentIdentityError extends Error {
|
|
@@ -54,6 +54,21 @@ function pairing(value) {
|
|
|
54
54
|
expiresAt: value.expires_at
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
function nullableString(value) {
|
|
58
|
+
return value === null || typeof value === "string";
|
|
59
|
+
}
|
|
60
|
+
function agentChat(value) {
|
|
61
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.title !== "string" || !nullableString(value.purpose) || !nullableString(value.whenToUse)) throw new AgentIdentityError({
|
|
62
|
+
code: "invalid_response",
|
|
63
|
+
message: "The ontology service answered the chat report with an invalid body."
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
id: value.id,
|
|
67
|
+
title: value.title,
|
|
68
|
+
purpose: value.purpose,
|
|
69
|
+
whenToUse: value.whenToUse
|
|
70
|
+
};
|
|
71
|
+
}
|
|
57
72
|
async function refusal(response, call) {
|
|
58
73
|
const status = response.status;
|
|
59
74
|
let value;
|
|
@@ -127,6 +142,11 @@ function agentIdentities(input) {
|
|
|
127
142
|
const created = pairing(await response.json());
|
|
128
143
|
remember(pending, key, created);
|
|
129
144
|
return created;
|
|
145
|
+
},
|
|
146
|
+
async seenChat(chat) {
|
|
147
|
+
const response = await call("/chats/seen", { body: chat });
|
|
148
|
+
if (!response.ok) throw await refusal(response, "chat report");
|
|
149
|
+
return agentChat(await response.json());
|
|
130
150
|
}
|
|
131
151
|
};
|
|
132
152
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type JWTVerifyGetKey } from "jose";
|
|
2
|
+
import type { RuntimeIdentityEnvironment } from "./environment.js";
|
|
3
|
+
export type RuntimeVerification = {
|
|
4
|
+
readonly status: "valid";
|
|
5
|
+
}
|
|
6
|
+
/** `reason` names what failed, a jose code or a claim name, never a value. */
|
|
7
|
+
| {
|
|
8
|
+
readonly status: "invalid";
|
|
9
|
+
readonly reason: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The key set could not be read: not the token's fault. `reason` is the jose code, or the error's
|
|
13
|
+
* name and message from fetching the key set, never the token.
|
|
14
|
+
*/
|
|
15
|
+
| {
|
|
16
|
+
readonly status: "unavailable";
|
|
17
|
+
readonly reason: string;
|
|
18
|
+
};
|
|
19
|
+
export type RuntimeTokenVerifier = (token: string) => Promise<RuntimeVerification>;
|
|
20
|
+
/** The issuer's key set, at `<issuer>/.well-known/jwks` as Vercel publishes it; jose caches it. */
|
|
21
|
+
export declare function runtimeKeySet(issuer: string): JWTVerifyGetKey;
|
|
22
|
+
export declare function runtimeTokenVerifier(config: RuntimeIdentityEnvironment, keySet?: JWTVerifyGetKey): RuntimeTokenVerifier;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { createRemoteJWKSet, errors, jwtVerify } from "jose";
|
|
8
|
+
const CLOCK_TOLERANCE_SECONDS = 5;
|
|
9
|
+
const JWKS_TIMEOUT_MS = 5e3;
|
|
10
|
+
function runtimeKeySet(issuer) {
|
|
11
|
+
return createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks`), { timeoutDuration: JWKS_TIMEOUT_MS });
|
|
12
|
+
}
|
|
13
|
+
function keySetUnavailable(error) {
|
|
14
|
+
if (!(error instanceof errors.JOSEError)) return true;
|
|
15
|
+
return error instanceof errors.JWKSTimeout || error instanceof errors.JWKSInvalid || error.code === errors.JOSEError.code;
|
|
16
|
+
}
|
|
17
|
+
function unavailableReason(error) {
|
|
18
|
+
if (error instanceof errors.JOSEError) return error.code;
|
|
19
|
+
return error instanceof Error ? `${error.name}: ${error.message}` : "unknown";
|
|
20
|
+
}
|
|
21
|
+
function failureReason(error) {
|
|
22
|
+
if (error instanceof errors.JWTClaimValidationFailed || error instanceof errors.JWTExpired) return `claim:${error.claim}`;
|
|
23
|
+
return error instanceof errors.JOSEError ? error.code : "invalid";
|
|
24
|
+
}
|
|
25
|
+
function runtimeTokenVerifier(config, keySet = runtimeKeySet(config.issuer)) {
|
|
26
|
+
return async (token) => {
|
|
27
|
+
try {
|
|
28
|
+
const { payload } = await jwtVerify(token, keySet, {
|
|
29
|
+
algorithms: ["RS256"],
|
|
30
|
+
issuer: config.issuer,
|
|
31
|
+
audience: config.audience,
|
|
32
|
+
clockTolerance: CLOCK_TOLERANCE_SECONDS,
|
|
33
|
+
requiredClaims: [
|
|
34
|
+
"exp",
|
|
35
|
+
"iat",
|
|
36
|
+
"iss",
|
|
37
|
+
"aud",
|
|
38
|
+
"project_id",
|
|
39
|
+
"environment"
|
|
40
|
+
]
|
|
41
|
+
});
|
|
42
|
+
if (payload.project_id !== config.projectId) return {
|
|
43
|
+
status: "invalid",
|
|
44
|
+
reason: "claim:project_id"
|
|
45
|
+
};
|
|
46
|
+
if (payload.environment !== "production") return {
|
|
47
|
+
status: "invalid",
|
|
48
|
+
reason: "claim:environment"
|
|
49
|
+
};
|
|
50
|
+
return { status: "valid" };
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return keySetUnavailable(error) ? {
|
|
53
|
+
status: "unavailable",
|
|
54
|
+
reason: unavailableReason(error)
|
|
55
|
+
} : {
|
|
56
|
+
status: "invalid",
|
|
57
|
+
reason: failureReason(error)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export { runtimeKeySet, runtimeTokenVerifier };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type TelegramChannel } from "eve/channels/telegram";
|
|
2
2
|
import type { SessionSettings, TelegramSettings } from "../../config.js";
|
|
3
|
+
import { type AgentAttachments } from "../attachments.js";
|
|
3
4
|
import { type Environment } from "../environment.js";
|
|
5
|
+
import { type RuntimeTokenVerifier } from "../runtime-identity.js";
|
|
4
6
|
import { type RuntimeToken } from "../runtime-token.js";
|
|
5
7
|
import { type ObservedMessages } from "./observed.js";
|
|
6
8
|
import { type DecideFunction } from "./router.js";
|
|
@@ -27,8 +29,12 @@ export interface TelegramAdapterOptions {
|
|
|
27
29
|
readonly fetch?: typeof fetch;
|
|
28
30
|
readonly transcribe?: Transcriber;
|
|
29
31
|
readonly observed?: ObservedMessages;
|
|
32
|
+
/** Verifies callers of the messages route; tests pass one on a local key set. */
|
|
33
|
+
readonly verify?: RuntimeTokenVerifier;
|
|
30
34
|
/** Reads the tail of a session's durable stream; tests pass a fake. */
|
|
31
35
|
readonly history?: HistoryReader;
|
|
36
|
+
/** The attachment store; tests pass a fake. */
|
|
37
|
+
readonly attachments?: AgentAttachments;
|
|
32
38
|
}
|
|
33
39
|
/** The Telegram channel of the extension. */
|
|
34
40
|
export declare function segmentstreamTelegram(options: TelegramAdapterOptions): TelegramChannel;
|
|
@@ -4,11 +4,14 @@ import { createRequire as __eveCreateRequire } from "node:module";
|
|
|
4
4
|
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
|
-
import { appOrigin, channelsEnabled, proxyEnvironment, webhookSecret } from "../environment.mjs";
|
|
8
|
-
import {
|
|
9
|
-
import { agentIdentities } from "../identities.mjs";
|
|
7
|
+
import { appOrigin, channelsEnabled, proxyEnvironment, runtimeIdentityEnvironment, webhookSecret } from "../environment.mjs";
|
|
8
|
+
import { MAXIMUM_DOWNLOAD_BYTES, connectionProxy } from "../connection-proxy.mjs";
|
|
10
9
|
import { runtimeToken } from "../runtime-token.mjs";
|
|
10
|
+
import { agentAttachments, parseAttachmentUrl } from "../attachments.mjs";
|
|
11
|
+
import { agentIdentities } from "../identities.mjs";
|
|
12
|
+
import { runtimeTokenVerifier } from "../runtime-identity.mjs";
|
|
11
13
|
import { botIdentityCache } from "./bot.mjs";
|
|
14
|
+
import { agentChats } from "./chats.mjs";
|
|
12
15
|
import { observedMessages } from "./observed.mjs";
|
|
13
16
|
import { decisionRouter } from "./router.mjs";
|
|
14
17
|
import { scopedAddress, sessionPolicy } from "./sessions.mjs";
|
|
@@ -16,6 +19,7 @@ import { downloadTelegramFile, gatewayTranscriber } from "./voice.mjs";
|
|
|
16
19
|
import { handleInboundMessage } from "./inbound.mjs";
|
|
17
20
|
import { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText } from "./inbound-request.mjs";
|
|
18
21
|
import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
|
|
22
|
+
import { messagesRoute } from "./requests.mjs";
|
|
19
23
|
import { deliverUnlessSilent } from "./silent.mjs";
|
|
20
24
|
import { telegramWebhookVerifier } from "./webhook.mjs";
|
|
21
25
|
import { telegramChannel } from "eve/channels/telegram";
|
|
@@ -42,6 +46,7 @@ function segmentstreamTelegram(options) {
|
|
|
42
46
|
const bot = botIdentityCache();
|
|
43
47
|
const proxies = new Map();
|
|
44
48
|
let identities;
|
|
49
|
+
let verify = options.verify;
|
|
45
50
|
function settings() {
|
|
46
51
|
const configured = options.settings();
|
|
47
52
|
if (configured === null) throw new Error("The Telegram channel is not configured in agent/segmentstream.json.");
|
|
@@ -69,6 +74,16 @@ function segmentstreamTelegram(options) {
|
|
|
69
74
|
});
|
|
70
75
|
return identities;
|
|
71
76
|
}
|
|
77
|
+
const chats = agentChats({ identities: pairing });
|
|
78
|
+
let store = options.attachments;
|
|
79
|
+
function attachments() {
|
|
80
|
+
store ??= agentAttachments({
|
|
81
|
+
origin: appOrigin(environment),
|
|
82
|
+
token,
|
|
83
|
+
...options.fetch === void 0 ? {} : { fetch: options.fetch }
|
|
84
|
+
});
|
|
85
|
+
return store;
|
|
86
|
+
}
|
|
72
87
|
const channel = telegramChannel({
|
|
73
88
|
api: {
|
|
74
89
|
apiBaseUrl: TELEGRAM_PROXY_ORIGIN,
|
|
@@ -79,7 +94,7 @@ function segmentstreamTelegram(options) {
|
|
|
79
94
|
botToken: TELEGRAM_PLACEHOLDER_TOKEN,
|
|
80
95
|
webhookVerifier: telegramWebhookVerifier(() => webhookSecret(environment))
|
|
81
96
|
},
|
|
82
|
-
uploadPolicy: { maxBytes:
|
|
97
|
+
uploadPolicy: { maxBytes: MAXIMUM_DOWNLOAD_BYTES },
|
|
83
98
|
onMessage: (context, message) => handleInboundMessage({
|
|
84
99
|
context,
|
|
85
100
|
message,
|
|
@@ -100,21 +115,55 @@ function segmentstreamTelegram(options) {
|
|
|
100
115
|
proxy: proxy(),
|
|
101
116
|
fileId
|
|
102
117
|
}),
|
|
118
|
+
attachments: attachments(),
|
|
103
119
|
observed,
|
|
120
|
+
chats,
|
|
104
121
|
connectUrl: (code) => `${appOrigin(environment)}/agent/connect?code=${encodeURIComponent(code)}`
|
|
105
122
|
}
|
|
106
123
|
}),
|
|
107
124
|
events: { "message.completed": deliverUnlessSilent }
|
|
108
125
|
});
|
|
126
|
+
const adapter = channel.adapter;
|
|
127
|
+
const fetchTelegramFile = adapter?.fetchFile;
|
|
128
|
+
async function fetchFile(url, context) {
|
|
129
|
+
const reference = parseAttachmentUrl(url);
|
|
130
|
+
if (reference === void 0) return fetchTelegramFile === void 0 ? null : fetchTelegramFile(url, context);
|
|
131
|
+
const sessionId = sessionIdOf(context);
|
|
132
|
+
const { attachment, bytes } = await attachments().read({
|
|
133
|
+
memberId: reference.memberId,
|
|
134
|
+
conversation: reference.conversation,
|
|
135
|
+
...sessionId === void 0 ? {} : { sessionId }
|
|
136
|
+
}, reference.id);
|
|
137
|
+
return {
|
|
138
|
+
bytes: Buffer.from(bytes),
|
|
139
|
+
mediaType: attachment.contentType,
|
|
140
|
+
filename: attachment.filename
|
|
141
|
+
};
|
|
142
|
+
}
|
|
109
143
|
return {
|
|
110
144
|
...channel,
|
|
111
|
-
|
|
145
|
+
...adapter === void 0 ? {} : { adapter: {
|
|
146
|
+
...adapter,
|
|
147
|
+
fetchFile
|
|
148
|
+
} },
|
|
149
|
+
routes: [...channel.routes.map((route) => gatedRoute({
|
|
112
150
|
route,
|
|
113
151
|
options,
|
|
114
152
|
environment
|
|
115
|
-
}))
|
|
153
|
+
})), messagesRoute({
|
|
154
|
+
channel,
|
|
155
|
+
enabled: () => channelsEnabled(environment),
|
|
156
|
+
configured: () => options.settings() !== null,
|
|
157
|
+
verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment)),
|
|
158
|
+
sessionPolicy: policy,
|
|
159
|
+
scope: () => options.sessions().scope
|
|
160
|
+
})]
|
|
116
161
|
};
|
|
117
162
|
}
|
|
163
|
+
function sessionIdOf(context) {
|
|
164
|
+
const session = typeof context === "object" && context !== null ? context.session : void 0;
|
|
165
|
+
return typeof session?.id === "string" && session.id !== "" ? session.id : void 0;
|
|
166
|
+
}
|
|
118
167
|
function gatedRoute(input) {
|
|
119
168
|
const { route, options, environment } = input;
|
|
120
169
|
if (route.transport === "websocket") return route;
|
|
@@ -129,6 +178,7 @@ function gatedRoute(input) {
|
|
|
129
178
|
resolveSession: args.resolveSession,
|
|
130
179
|
from: args.from
|
|
131
180
|
};
|
|
181
|
+
inbound.waitUntil = args.waitUntil;
|
|
132
182
|
return runInboundRequest(inbound, () => route.handler(request, {
|
|
133
183
|
...args,
|
|
134
184
|
from: (address) => withReplacedText(args.from(scoped(address)), inbound)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AgentChat, AgentIdentities } from "../identities.js";
|
|
2
|
+
/** How long a chat's report stands before the next message reports it again. */
|
|
3
|
+
export declare const REPORT_INTERVAL_MS: number;
|
|
4
|
+
export interface ChatReport {
|
|
5
|
+
readonly chatId: string;
|
|
6
|
+
readonly title: string;
|
|
7
|
+
readonly memberId?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface AgentChats {
|
|
10
|
+
/** Reports the chat when due. Resolves when done; never rejects. */
|
|
11
|
+
seen(report: ChatReport): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Reports that the paired member left the chat, so the registry unlinks them, and forgets them
|
|
14
|
+
* here, so that a later message from them links them again. Resolves when done; never rejects.
|
|
15
|
+
*/
|
|
16
|
+
left(report: ChatReport & {
|
|
17
|
+
readonly memberId: string;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Reports that the agent itself left the chat, so the registry unlinks every member, and forgets
|
|
21
|
+
* the members reported here, so that each links again when they write after the agent is back.
|
|
22
|
+
* Resolves when done; never rejects.
|
|
23
|
+
*/
|
|
24
|
+
agentLeft(report: {
|
|
25
|
+
readonly chatId: string;
|
|
26
|
+
readonly title: string;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Reports that the group `migratedFrom` became the supergroup `chatId`, so the registry moves its
|
|
30
|
+
* record, and forgets the old chat here. Resolves when done; never rejects.
|
|
31
|
+
*/
|
|
32
|
+
migrated(report: {
|
|
33
|
+
readonly chatId: string;
|
|
34
|
+
readonly title: string;
|
|
35
|
+
readonly migratedFrom: string;
|
|
36
|
+
}): Promise<void>;
|
|
37
|
+
/** The registry's latest answer for the chat, if this process has one. */
|
|
38
|
+
chat(chatId: string): AgentChat | undefined;
|
|
39
|
+
}
|
|
40
|
+
export declare function agentChats(input: {
|
|
41
|
+
readonly identities: () => AgentIdentities;
|
|
42
|
+
readonly now?: () => number;
|
|
43
|
+
readonly log?: (message: string, error: unknown) => void;
|
|
44
|
+
}): AgentChats;
|
|
45
|
+
/** The registry's facts as turn context: only the fields it holds. */
|
|
46
|
+
export declare function chatContext(chat: AgentChat | undefined): ReadonlyArray<string>;
|