@tangle-network/agent-app 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/README.md +59 -0
- package/dist/billing/index.d.ts +108 -0
- package/dist/billing/index.js +7 -0
- package/dist/billing/index.js.map +1 -0
- package/dist/chunk-45MYQ3GD.js +62 -0
- package/dist/chunk-45MYQ3GD.js.map +1 -0
- package/dist/chunk-4NXVI7PW.js +32 -0
- package/dist/chunk-4NXVI7PW.js.map +1 -0
- package/dist/chunk-7P6VIHI4.js +33 -0
- package/dist/chunk-7P6VIHI4.js.map +1 -0
- package/dist/chunk-C5CREGT2.js +45 -0
- package/dist/chunk-C5CREGT2.js.map +1 -0
- package/dist/chunk-CN75FIPT.js +61 -0
- package/dist/chunk-CN75FIPT.js.map +1 -0
- package/dist/chunk-EDIQ6F55.js +274 -0
- package/dist/chunk-EDIQ6F55.js.map +1 -0
- package/dist/chunk-FS5OUVRB.js +208 -0
- package/dist/chunk-FS5OUVRB.js.map +1 -0
- package/dist/chunk-GMFPCCQZ.js +245 -0
- package/dist/chunk-GMFPCCQZ.js.map +1 -0
- package/dist/chunk-L2TG5DBW.js +74 -0
- package/dist/chunk-L2TG5DBW.js.map +1 -0
- package/dist/chunk-SIDR6BH3.js +57 -0
- package/dist/chunk-SIDR6BH3.js.map +1 -0
- package/dist/chunk-YGUNTIT5.js +48 -0
- package/dist/chunk-YGUNTIT5.js.map +1 -0
- package/dist/crypto/index.d.ts +27 -0
- package/dist/crypto/index.js +13 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/delegation/index.d.ts +50 -0
- package/dist/delegation/index.js +11 -0
- package/dist/delegation/index.js.map +1 -0
- package/dist/eval/index.d.ts +50 -0
- package/dist/eval/index.js +17 -0
- package/dist/eval/index.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +149 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/index.d.ts +84 -0
- package/dist/integrations/index.js +11 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/redact/index.d.ts +22 -0
- package/dist/redact/index.js +7 -0
- package/dist/redact/index.js.map +1 -0
- package/dist/runtime/index.d.ts +219 -0
- package/dist/runtime/index.js +17 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/stream/index.d.ts +39 -0
- package/dist/stream/index.js +35 -0
- package/dist/stream/index.js.map +1 -0
- package/dist/tangle/index.d.ts +93 -0
- package/dist/tangle/index.js +9 -0
- package/dist/tangle/index.js.map +1 -0
- package/dist/tools/index.d.ts +213 -0
- package/dist/tools/index.js +37 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types-CeWor4bQ.d.ts +120 -0
- package/dist/web/index.d.ts +51 -0
- package/dist/web/index.js +15 -0
- package/dist/web/index.js.map +1 -0
- package/package.json +101 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// src/stream/stream-normalizer.ts
|
|
2
|
+
function asRecord(value) {
|
|
3
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4
|
+
}
|
|
5
|
+
function asString(value) {
|
|
6
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
7
|
+
}
|
|
8
|
+
function resolveToolId(part) {
|
|
9
|
+
return String(
|
|
10
|
+
part.id ?? part.callID ?? part.callId ?? part.toolUseId ?? part.toolCallId ?? part.tool ?? part.name ?? `tool-${Date.now()}`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
function resolveToolName(part) {
|
|
14
|
+
return String(part.tool ?? part.name ?? "tool");
|
|
15
|
+
}
|
|
16
|
+
function normalizeTime(value) {
|
|
17
|
+
const record = asRecord(value);
|
|
18
|
+
if (!record) return void 0;
|
|
19
|
+
const start = Number(record.start ?? record.startedAt ?? record.started_at);
|
|
20
|
+
const end = Number(record.end ?? record.completedAt ?? record.completed_at);
|
|
21
|
+
if (!Number.isFinite(start) && !Number.isFinite(end)) return void 0;
|
|
22
|
+
return {
|
|
23
|
+
start: Number.isFinite(start) ? start : void 0,
|
|
24
|
+
end: Number.isFinite(end) ? end : void 0
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function normalizeToolEvent(event) {
|
|
28
|
+
if (event.type === "tool_call" || event.type === "tool.call") {
|
|
29
|
+
const data = event.data ?? {};
|
|
30
|
+
return {
|
|
31
|
+
type: "message.part.updated",
|
|
32
|
+
data: {
|
|
33
|
+
part: {
|
|
34
|
+
type: "tool",
|
|
35
|
+
id: data.id ?? data.callId ?? data.callID ?? data.name,
|
|
36
|
+
tool: data.name ?? data.tool ?? "tool",
|
|
37
|
+
input: data.arguments ?? data.input,
|
|
38
|
+
status: "running"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (event.type === "tool_result" || event.type === "tool.result") {
|
|
44
|
+
const data = event.data ?? {};
|
|
45
|
+
const error = asString(data.error);
|
|
46
|
+
return {
|
|
47
|
+
type: "message.part.updated",
|
|
48
|
+
data: {
|
|
49
|
+
part: {
|
|
50
|
+
type: "tool",
|
|
51
|
+
id: data.id ?? data.callId ?? data.callID ?? data.name,
|
|
52
|
+
tool: data.name ?? data.tool ?? "tool",
|
|
53
|
+
output: data.output,
|
|
54
|
+
error,
|
|
55
|
+
status: error ? "error" : "completed"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return event;
|
|
61
|
+
}
|
|
62
|
+
function normalizePersistedPart(rawPart) {
|
|
63
|
+
const type = String(rawPart.type ?? "");
|
|
64
|
+
if (type === "text") {
|
|
65
|
+
return {
|
|
66
|
+
type: "text",
|
|
67
|
+
text: asString(rawPart.text) ?? asString(rawPart.content) ?? ""
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (type === "reasoning") {
|
|
71
|
+
return {
|
|
72
|
+
type: "reasoning",
|
|
73
|
+
text: asString(rawPart.text) ?? asString(rawPart.content) ?? "",
|
|
74
|
+
time: normalizeTime(rawPart.time)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (type === "tool") {
|
|
78
|
+
const state = asRecord(rawPart.state);
|
|
79
|
+
const output = state?.output ?? rawPart.output;
|
|
80
|
+
const error = asString(state?.error ?? rawPart.error);
|
|
81
|
+
const status = state?.status === "completed" || rawPart.status === "completed" ? "completed" : state?.status === "error" || rawPart.status === "error" || error ? "error" : output !== void 0 ? "completed" : "running";
|
|
82
|
+
return {
|
|
83
|
+
type: "tool",
|
|
84
|
+
id: resolveToolId(rawPart),
|
|
85
|
+
tool: resolveToolName(rawPart),
|
|
86
|
+
callID: rawPart.callID != null || rawPart.callId != null ? String(rawPart.callID ?? rawPart.callId) : void 0,
|
|
87
|
+
state: {
|
|
88
|
+
status,
|
|
89
|
+
input: state?.input ?? rawPart.input,
|
|
90
|
+
output,
|
|
91
|
+
error,
|
|
92
|
+
metadata: asRecord(state?.metadata) ?? asRecord(rawPart.metadata),
|
|
93
|
+
time: normalizeTime(state?.time ?? rawPart.time)
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function getPartKey(part) {
|
|
100
|
+
const type = String(part.type ?? "unknown");
|
|
101
|
+
if (type === "tool") {
|
|
102
|
+
return `tool:${resolveToolId(part)}`;
|
|
103
|
+
}
|
|
104
|
+
if (type === "reasoning") {
|
|
105
|
+
return `reasoning:${String(part.id ?? part.partId ?? part.index ?? "current")}`;
|
|
106
|
+
}
|
|
107
|
+
return `text:${String(part.id ?? part.partId ?? part.index ?? "current")}`;
|
|
108
|
+
}
|
|
109
|
+
function mergePersistedPart(existing, incoming, delta) {
|
|
110
|
+
const type = String(incoming.type ?? "");
|
|
111
|
+
if (!existing) {
|
|
112
|
+
if (type === "text" && delta) {
|
|
113
|
+
return { type: "text", text: delta };
|
|
114
|
+
}
|
|
115
|
+
return incoming;
|
|
116
|
+
}
|
|
117
|
+
if (type === "text" && String(existing.type ?? "") === "text") {
|
|
118
|
+
return {
|
|
119
|
+
...existing,
|
|
120
|
+
...incoming,
|
|
121
|
+
text: delta ? `${String(existing.text ?? "")}${delta}` : String(incoming.text ?? "")
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (type === "reasoning" && String(existing.type ?? "") === "reasoning") {
|
|
125
|
+
const existingText = String(existing.text ?? "");
|
|
126
|
+
const incomingText = String(incoming.text ?? "");
|
|
127
|
+
return {
|
|
128
|
+
...existing,
|
|
129
|
+
...incoming,
|
|
130
|
+
text: delta && incomingText === existingText ? `${existingText}${delta}` : incomingText || existingText,
|
|
131
|
+
time: incoming.time ?? existing.time
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (type === "tool" && String(existing.type ?? "") === "tool") {
|
|
135
|
+
return {
|
|
136
|
+
...existing,
|
|
137
|
+
...incoming,
|
|
138
|
+
state: {
|
|
139
|
+
...asRecord(existing.state) ?? {},
|
|
140
|
+
...asRecord(incoming.state) ?? {},
|
|
141
|
+
time: asRecord(incoming.state)?.time ?? asRecord(existing.state)?.time
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return incoming;
|
|
146
|
+
}
|
|
147
|
+
function finalizeAssistantParts(partOrder, partMap, finalText) {
|
|
148
|
+
const parts = partOrder.map((key) => partMap.get(key)).filter((part) => Boolean(part));
|
|
149
|
+
if (!parts.some((part) => String(part.type ?? "") === "text")) {
|
|
150
|
+
if (finalText.trim()) {
|
|
151
|
+
parts.push({ type: "text", text: finalText });
|
|
152
|
+
}
|
|
153
|
+
return parts;
|
|
154
|
+
}
|
|
155
|
+
return parts.map((part) => {
|
|
156
|
+
if (String(part.type ?? "") !== "text") return part;
|
|
157
|
+
return {
|
|
158
|
+
...part,
|
|
159
|
+
text: finalText || String(part.text ?? "")
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function encodeEvent(encoder, event) {
|
|
164
|
+
return encoder.encode(`${JSON.stringify(event)}
|
|
165
|
+
`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/stream/turn-identity.ts
|
|
169
|
+
function normalizeClientTurnId(value) {
|
|
170
|
+
if (value === void 0 || value === null) return void 0;
|
|
171
|
+
if (typeof value !== "string") throw new Error("turnId must be a string");
|
|
172
|
+
const trimmed = value.trim();
|
|
173
|
+
if (!trimmed) throw new Error("turnId must not be blank");
|
|
174
|
+
if (trimmed.length > 160) throw new Error("turnId is too long");
|
|
175
|
+
if (!/^[A-Za-z0-9:_-]+$/.test(trimmed)) {
|
|
176
|
+
throw new Error("turnId contains unsupported characters");
|
|
177
|
+
}
|
|
178
|
+
return trimmed;
|
|
179
|
+
}
|
|
180
|
+
function buildUserTextParts(text, turnId) {
|
|
181
|
+
const part = { type: "text", text };
|
|
182
|
+
if (turnId) part.turnId = turnId;
|
|
183
|
+
return [part];
|
|
184
|
+
}
|
|
185
|
+
function messageHasTurnId(message, turnId) {
|
|
186
|
+
for (const part of message.parts ?? []) {
|
|
187
|
+
if (part && typeof part === "object" && String(part.turnId ?? "") === turnId) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
function resolveChatTurn(input) {
|
|
194
|
+
const { existingMessages, userContent, turnId } = input;
|
|
195
|
+
const reusableIndex = findReusableUserMessageIndex(existingMessages, userContent, turnId);
|
|
196
|
+
if (reusableIndex >= 0) {
|
|
197
|
+
return {
|
|
198
|
+
turnIndex: countUserMessages(existingMessages.slice(0, reusableIndex)),
|
|
199
|
+
shouldInsertUserMessage: false,
|
|
200
|
+
priorMessages: existingMessages.slice(0, reusableIndex),
|
|
201
|
+
userParts: buildUserTextParts(userContent, turnId)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
turnIndex: countUserMessages(existingMessages),
|
|
206
|
+
shouldInsertUserMessage: true,
|
|
207
|
+
priorMessages: existingMessages,
|
|
208
|
+
userParts: buildUserTextParts(userContent, turnId)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function findReusableUserMessageIndex(messages, userContent, turnId) {
|
|
212
|
+
if (turnId) {
|
|
213
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
214
|
+
const message = messages[index];
|
|
215
|
+
if (message?.role === "user" && messageHasTurnId(message, turnId)) return index;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const latest = messages.at(-1);
|
|
219
|
+
if (latest?.role === "user" && latest.content === userContent) {
|
|
220
|
+
return messages.length - 1;
|
|
221
|
+
}
|
|
222
|
+
return -1;
|
|
223
|
+
}
|
|
224
|
+
function countUserMessages(messages) {
|
|
225
|
+
return messages.filter((message) => message.role === "user").length;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export {
|
|
229
|
+
asRecord,
|
|
230
|
+
asString,
|
|
231
|
+
resolveToolId,
|
|
232
|
+
resolveToolName,
|
|
233
|
+
normalizeTime,
|
|
234
|
+
normalizeToolEvent,
|
|
235
|
+
normalizePersistedPart,
|
|
236
|
+
getPartKey,
|
|
237
|
+
mergePersistedPart,
|
|
238
|
+
finalizeAssistantParts,
|
|
239
|
+
encodeEvent,
|
|
240
|
+
normalizeClientTurnId,
|
|
241
|
+
buildUserTextParts,
|
|
242
|
+
messageHasTurnId,
|
|
243
|
+
resolveChatTurn
|
|
244
|
+
};
|
|
245
|
+
//# sourceMappingURL=chunk-GMFPCCQZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stream/stream-normalizer.ts","../src/stream/turn-identity.ts"],"sourcesContent":["export type JsonRecord = Record<string, unknown>\n\nexport interface StreamEvent {\n type: string\n data?: JsonRecord\n}\n\nexport function asRecord(value: unknown): JsonRecord | undefined {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? value as JsonRecord\n : undefined\n}\n\nexport function asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\nexport function resolveToolId(part: JsonRecord): string {\n return String(\n part.id ??\n part.callID ??\n part.callId ??\n part.toolUseId ??\n part.toolCallId ??\n part.tool ??\n part.name ??\n `tool-${Date.now()}`,\n )\n}\n\nexport function resolveToolName(part: JsonRecord): string {\n return String(part.tool ?? part.name ?? 'tool')\n}\n\nexport function normalizeTime(value: unknown): JsonRecord | undefined {\n const record = asRecord(value)\n if (!record) return undefined\n\n const start = Number(record.start ?? record.startedAt ?? record.started_at)\n const end = Number(record.end ?? record.completedAt ?? record.completed_at)\n if (!Number.isFinite(start) && !Number.isFinite(end)) return undefined\n\n return {\n start: Number.isFinite(start) ? start : undefined,\n end: Number.isFinite(end) ? end : undefined,\n }\n}\n\nexport function normalizeToolEvent(event: StreamEvent): StreamEvent {\n if (event.type === 'tool_call' || event.type === 'tool.call') {\n const data = event.data ?? {}\n return {\n type: 'message.part.updated',\n data: {\n part: {\n type: 'tool',\n id: data.id ?? data.callId ?? data.callID ?? data.name,\n tool: data.name ?? data.tool ?? 'tool',\n input: data.arguments ?? data.input,\n status: 'running',\n },\n },\n }\n }\n\n if (event.type === 'tool_result' || event.type === 'tool.result') {\n const data = event.data ?? {}\n const error = asString(data.error)\n return {\n type: 'message.part.updated',\n data: {\n part: {\n type: 'tool',\n id: data.id ?? data.callId ?? data.callID ?? data.name,\n tool: data.name ?? data.tool ?? 'tool',\n output: data.output,\n error,\n status: error ? 'error' : 'completed',\n },\n },\n }\n }\n\n return event\n}\n\nexport function normalizePersistedPart(rawPart: JsonRecord): JsonRecord | null {\n const type = String(rawPart.type ?? '')\n\n if (type === 'text') {\n return {\n type: 'text',\n text: asString(rawPart.text) ?? asString(rawPart.content) ?? '',\n }\n }\n\n if (type === 'reasoning') {\n return {\n type: 'reasoning',\n text: asString(rawPart.text) ?? asString(rawPart.content) ?? '',\n time: normalizeTime(rawPart.time),\n }\n }\n\n if (type === 'tool') {\n const state = asRecord(rawPart.state)\n const output = state?.output ?? rawPart.output\n const error = asString(state?.error ?? rawPart.error)\n const status =\n state?.status === 'completed' || rawPart.status === 'completed'\n ? 'completed'\n : state?.status === 'error' || rawPart.status === 'error' || error\n ? 'error'\n : output !== undefined\n ? 'completed'\n : 'running'\n\n return {\n type: 'tool',\n id: resolveToolId(rawPart),\n tool: resolveToolName(rawPart),\n callID:\n rawPart.callID != null || rawPart.callId != null\n ? String(rawPart.callID ?? rawPart.callId)\n : undefined,\n state: {\n status,\n input: state?.input ?? rawPart.input,\n output,\n error,\n metadata: asRecord(state?.metadata) ?? asRecord(rawPart.metadata),\n time: normalizeTime(state?.time ?? rawPart.time),\n },\n }\n }\n\n return null\n}\n\nexport function getPartKey(part: JsonRecord): string {\n const type = String(part.type ?? 'unknown')\n if (type === 'tool') {\n return `tool:${resolveToolId(part)}`\n }\n\n if (type === 'reasoning') {\n return `reasoning:${String(part.id ?? part.partId ?? part.index ?? 'current')}`\n }\n\n return `text:${String(part.id ?? part.partId ?? part.index ?? 'current')}`\n}\n\nexport function mergePersistedPart(existing: JsonRecord | undefined, incoming: JsonRecord, delta?: string): JsonRecord {\n const type = String(incoming.type ?? '')\n if (!existing) {\n if (type === 'text' && delta) {\n return { type: 'text', text: delta }\n }\n return incoming\n }\n\n if (type === 'text' && String(existing.type ?? '') === 'text') {\n return {\n ...existing,\n ...incoming,\n text: delta ? `${String(existing.text ?? '')}${delta}` : String(incoming.text ?? ''),\n }\n }\n\n if (type === 'reasoning' && String(existing.type ?? '') === 'reasoning') {\n const existingText = String(existing.text ?? '')\n const incomingText = String(incoming.text ?? '')\n return {\n ...existing,\n ...incoming,\n text: delta && incomingText === existingText ? `${existingText}${delta}` : incomingText || existingText,\n time: incoming.time ?? existing.time,\n }\n }\n\n if (type === 'tool' && String(existing.type ?? '') === 'tool') {\n return {\n ...existing,\n ...incoming,\n state: {\n ...(asRecord(existing.state) ?? {}),\n ...(asRecord(incoming.state) ?? {}),\n time: asRecord(incoming.state)?.time ?? asRecord(existing.state)?.time,\n },\n }\n }\n\n return incoming\n}\n\nexport function finalizeAssistantParts(\n partOrder: string[],\n partMap: Map<string, JsonRecord>,\n finalText: string,\n): JsonRecord[] {\n const parts = partOrder\n .map((key) => partMap.get(key))\n .filter((part): part is JsonRecord => Boolean(part))\n\n if (!parts.some((part) => String(part.type ?? '') === 'text')) {\n if (finalText.trim()) {\n parts.push({ type: 'text', text: finalText })\n }\n return parts\n }\n\n return parts.map((part) => {\n if (String(part.type ?? '') !== 'text') return part\n return {\n ...part,\n text: finalText || String(part.text ?? ''),\n }\n })\n}\n\nexport function encodeEvent(encoder: TextEncoder, event: StreamEvent): Uint8Array {\n return encoder.encode(`${JSON.stringify(event)}\\n`)\n}\n","import type { JsonRecord } from './stream-normalizer'\n\nexport interface PersistedChatMessageForTurn {\n id: string\n role: 'user' | 'assistant' | 'system' | 'tool'\n content: string\n parts: Array<Record<string, unknown>> | null\n}\n\nexport interface ResolvedChatTurn {\n turnIndex: number\n shouldInsertUserMessage: boolean\n priorMessages: PersistedChatMessageForTurn[]\n userParts: JsonRecord[]\n}\n\nexport function normalizeClientTurnId(value: unknown): string | undefined {\n if (value === undefined || value === null) return undefined\n if (typeof value !== 'string') throw new Error('turnId must be a string')\n const trimmed = value.trim()\n if (!trimmed) throw new Error('turnId must not be blank')\n if (trimmed.length > 160) throw new Error('turnId is too long')\n if (!/^[A-Za-z0-9:_-]+$/.test(trimmed)) {\n throw new Error('turnId contains unsupported characters')\n }\n return trimmed\n}\n\nexport function buildUserTextParts(text: string, turnId: string | undefined): JsonRecord[] {\n const part: JsonRecord = { type: 'text', text }\n if (turnId) part.turnId = turnId\n return [part]\n}\n\nexport function messageHasTurnId(message: PersistedChatMessageForTurn, turnId: string): boolean {\n for (const part of message.parts ?? []) {\n if (part && typeof part === 'object' && String(part.turnId ?? '') === turnId) {\n return true\n }\n }\n return false\n}\n\nexport function resolveChatTurn(input: {\n existingMessages: PersistedChatMessageForTurn[]\n userContent: string\n turnId?: string\n}): ResolvedChatTurn {\n const { existingMessages, userContent, turnId } = input\n const reusableIndex = findReusableUserMessageIndex(existingMessages, userContent, turnId)\n if (reusableIndex >= 0) {\n return {\n turnIndex: countUserMessages(existingMessages.slice(0, reusableIndex)),\n shouldInsertUserMessage: false,\n priorMessages: existingMessages.slice(0, reusableIndex),\n userParts: buildUserTextParts(userContent, turnId),\n }\n }\n\n return {\n turnIndex: countUserMessages(existingMessages),\n shouldInsertUserMessage: true,\n priorMessages: existingMessages,\n userParts: buildUserTextParts(userContent, turnId),\n }\n}\n\nfunction findReusableUserMessageIndex(\n messages: PersistedChatMessageForTurn[],\n userContent: string,\n turnId: string | undefined,\n): number {\n if (turnId) {\n for (let index = messages.length - 1; index >= 0; index -= 1) {\n const message = messages[index]\n if (message?.role === 'user' && messageHasTurnId(message, turnId)) return index\n }\n }\n\n const latest = messages.at(-1)\n if (latest?.role === 'user' && latest.content === userContent) {\n return messages.length - 1\n }\n\n return -1\n}\n\nfunction countUserMessages(messages: PersistedChatMessageForTurn[]): number {\n return messages.filter((message) => message.role === 'user').length\n}\n"],"mappings":";AAOO,SAAS,SAAS,OAAwC;AAC/D,SAAO,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAC7D,QACA;AACN;AAEO,SAAS,SAAS,OAAoC;AAC3D,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;AAEO,SAAS,cAAc,MAA0B;AACtD,SAAO;AAAA,IACL,KAAK,MACH,KAAK,UACL,KAAK,UACL,KAAK,aACL,KAAK,cACL,KAAK,QACL,KAAK,QACL,QAAQ,KAAK,IAAI,CAAC;AAAA,EACtB;AACF;AAEO,SAAS,gBAAgB,MAA0B;AACxD,SAAO,OAAO,KAAK,QAAQ,KAAK,QAAQ,MAAM;AAChD;AAEO,SAAS,cAAc,OAAwC;AACpE,QAAM,SAAS,SAAS,KAAK;AAC7B,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,QAAQ,OAAO,OAAO,SAAS,OAAO,aAAa,OAAO,UAAU;AAC1E,QAAM,MAAM,OAAO,OAAO,OAAO,OAAO,eAAe,OAAO,YAAY;AAC1E,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,CAAC,OAAO,SAAS,GAAG,EAAG,QAAO;AAE7D,SAAO;AAAA,IACL,OAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AAAA,IACxC,KAAK,OAAO,SAAS,GAAG,IAAI,MAAM;AAAA,EACpC;AACF;AAEO,SAAS,mBAAmB,OAAiC;AAClE,MAAI,MAAM,SAAS,eAAe,MAAM,SAAS,aAAa;AAC5D,UAAM,OAAO,MAAM,QAAQ,CAAC;AAC5B,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM;AAAA,QACJ,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,IAAI,KAAK,MAAM,KAAK,UAAU,KAAK,UAAU,KAAK;AAAA,UAClD,MAAM,KAAK,QAAQ,KAAK,QAAQ;AAAA,UAChC,OAAO,KAAK,aAAa,KAAK;AAAA,UAC9B,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,MAAM,SAAS,iBAAiB,MAAM,SAAS,eAAe;AAChE,UAAM,OAAO,MAAM,QAAQ,CAAC;AAC5B,UAAM,QAAQ,SAAS,KAAK,KAAK;AACjC,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM;AAAA,QACJ,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,IAAI,KAAK,MAAM,KAAK,UAAU,KAAK,UAAU,KAAK;AAAA,UAClD,MAAM,KAAK,QAAQ,KAAK,QAAQ;AAAA,UAChC,QAAQ,KAAK;AAAA,UACb;AAAA,UACA,QAAQ,QAAQ,UAAU;AAAA,QAC5B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,uBAAuB,SAAwC;AAC7E,QAAM,OAAO,OAAO,QAAQ,QAAQ,EAAE;AAEtC,MAAI,SAAS,QAAQ;AACnB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM,SAAS,QAAQ,IAAI,KAAK,SAAS,QAAQ,OAAO,KAAK;AAAA,IAC/D;AAAA,EACF;AAEA,MAAI,SAAS,aAAa;AACxB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM,SAAS,QAAQ,IAAI,KAAK,SAAS,QAAQ,OAAO,KAAK;AAAA,MAC7D,MAAM,cAAc,QAAQ,IAAI;AAAA,IAClC;AAAA,EACF;AAEA,MAAI,SAAS,QAAQ;AACnB,UAAM,QAAQ,SAAS,QAAQ,KAAK;AACpC,UAAM,SAAS,OAAO,UAAU,QAAQ;AACxC,UAAM,QAAQ,SAAS,OAAO,SAAS,QAAQ,KAAK;AACpD,UAAM,SACJ,OAAO,WAAW,eAAe,QAAQ,WAAW,cAChD,cACA,OAAO,WAAW,WAAW,QAAQ,WAAW,WAAW,QACzD,UACA,WAAW,SACT,cACA;AAEV,WAAO;AAAA,MACL,MAAM;AAAA,MACN,IAAI,cAAc,OAAO;AAAA,MACzB,MAAM,gBAAgB,OAAO;AAAA,MAC7B,QACE,QAAQ,UAAU,QAAQ,QAAQ,UAAU,OACxC,OAAO,QAAQ,UAAU,QAAQ,MAAM,IACvC;AAAA,MACN,OAAO;AAAA,QACL;AAAA,QACA,OAAO,OAAO,SAAS,QAAQ;AAAA,QAC/B;AAAA,QACA;AAAA,QACA,UAAU,SAAS,OAAO,QAAQ,KAAK,SAAS,QAAQ,QAAQ;AAAA,QAChE,MAAM,cAAc,OAAO,QAAQ,QAAQ,IAAI;AAAA,MACjD;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,WAAW,MAA0B;AACnD,QAAM,OAAO,OAAO,KAAK,QAAQ,SAAS;AAC1C,MAAI,SAAS,QAAQ;AACnB,WAAO,QAAQ,cAAc,IAAI,CAAC;AAAA,EACpC;AAEA,MAAI,SAAS,aAAa;AACxB,WAAO,aAAa,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,SAAS,SAAS,CAAC;AAAA,EAC/E;AAEA,SAAO,QAAQ,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,SAAS,SAAS,CAAC;AAC1E;AAEO,SAAS,mBAAmB,UAAkC,UAAsB,OAA4B;AACrH,QAAM,OAAO,OAAO,SAAS,QAAQ,EAAE;AACvC,MAAI,CAAC,UAAU;AACb,QAAI,SAAS,UAAU,OAAO;AAC5B,aAAO,EAAE,MAAM,QAAQ,MAAM,MAAM;AAAA,IACrC;AACA,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,UAAU,OAAO,SAAS,QAAQ,EAAE,MAAM,QAAQ;AAC7D,WAAO;AAAA,MACL,GAAG;AAAA,MACH,GAAG;AAAA,MACH,MAAM,QAAQ,GAAG,OAAO,SAAS,QAAQ,EAAE,CAAC,GAAG,KAAK,KAAK,OAAO,SAAS,QAAQ,EAAE;AAAA,IACrF;AAAA,EACF;AAEA,MAAI,SAAS,eAAe,OAAO,SAAS,QAAQ,EAAE,MAAM,aAAa;AACvE,UAAM,eAAe,OAAO,SAAS,QAAQ,EAAE;AAC/C,UAAM,eAAe,OAAO,SAAS,QAAQ,EAAE;AAC/C,WAAO;AAAA,MACL,GAAG;AAAA,MACH,GAAG;AAAA,MACH,MAAM,SAAS,iBAAiB,eAAe,GAAG,YAAY,GAAG,KAAK,KAAK,gBAAgB;AAAA,MAC3F,MAAM,SAAS,QAAQ,SAAS;AAAA,IAClC;AAAA,EACF;AAEA,MAAI,SAAS,UAAU,OAAO,SAAS,QAAQ,EAAE,MAAM,QAAQ;AAC7D,WAAO;AAAA,MACL,GAAG;AAAA,MACH,GAAG;AAAA,MACH,OAAO;AAAA,QACL,GAAI,SAAS,SAAS,KAAK,KAAK,CAAC;AAAA,QACjC,GAAI,SAAS,SAAS,KAAK,KAAK,CAAC;AAAA,QACjC,MAAM,SAAS,SAAS,KAAK,GAAG,QAAQ,SAAS,SAAS,KAAK,GAAG;AAAA,MACpE;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,uBACd,WACA,SACA,WACc;AACd,QAAM,QAAQ,UACX,IAAI,CAAC,QAAQ,QAAQ,IAAI,GAAG,CAAC,EAC7B,OAAO,CAAC,SAA6B,QAAQ,IAAI,CAAC;AAErD,MAAI,CAAC,MAAM,KAAK,CAAC,SAAS,OAAO,KAAK,QAAQ,EAAE,MAAM,MAAM,GAAG;AAC7D,QAAI,UAAU,KAAK,GAAG;AACpB,YAAM,KAAK,EAAE,MAAM,QAAQ,MAAM,UAAU,CAAC;AAAA,IAC9C;AACA,WAAO;AAAA,EACT;AAEA,SAAO,MAAM,IAAI,CAAC,SAAS;AACzB,QAAI,OAAO,KAAK,QAAQ,EAAE,MAAM,OAAQ,QAAO;AAC/C,WAAO;AAAA,MACL,GAAG;AAAA,MACH,MAAM,aAAa,OAAO,KAAK,QAAQ,EAAE;AAAA,IAC3C;AAAA,EACF,CAAC;AACH;AAEO,SAAS,YAAY,SAAsB,OAAgC;AAChF,SAAO,QAAQ,OAAO,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,CAAI;AACpD;;;AC9MO,SAAS,sBAAsB,OAAoC;AACxE,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,OAAM,IAAI,MAAM,yBAAyB;AACxE,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,0BAA0B;AACxD,MAAI,QAAQ,SAAS,IAAK,OAAM,IAAI,MAAM,oBAAoB;AAC9D,MAAI,CAAC,oBAAoB,KAAK,OAAO,GAAG;AACtC,UAAM,IAAI,MAAM,wCAAwC;AAAA,EAC1D;AACA,SAAO;AACT;AAEO,SAAS,mBAAmB,MAAc,QAA0C;AACzF,QAAM,OAAmB,EAAE,MAAM,QAAQ,KAAK;AAC9C,MAAI,OAAQ,MAAK,SAAS;AAC1B,SAAO,CAAC,IAAI;AACd;AAEO,SAAS,iBAAiB,SAAsC,QAAyB;AAC9F,aAAW,QAAQ,QAAQ,SAAS,CAAC,GAAG;AACtC,QAAI,QAAQ,OAAO,SAAS,YAAY,OAAO,KAAK,UAAU,EAAE,MAAM,QAAQ;AAC5E,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,gBAAgB,OAIX;AACnB,QAAM,EAAE,kBAAkB,aAAa,OAAO,IAAI;AAClD,QAAM,gBAAgB,6BAA6B,kBAAkB,aAAa,MAAM;AACxF,MAAI,iBAAiB,GAAG;AACtB,WAAO;AAAA,MACL,WAAW,kBAAkB,iBAAiB,MAAM,GAAG,aAAa,CAAC;AAAA,MACrE,yBAAyB;AAAA,MACzB,eAAe,iBAAiB,MAAM,GAAG,aAAa;AAAA,MACtD,WAAW,mBAAmB,aAAa,MAAM;AAAA,IACnD;AAAA,EACF;AAEA,SAAO;AAAA,IACL,WAAW,kBAAkB,gBAAgB;AAAA,IAC7C,yBAAyB;AAAA,IACzB,eAAe;AAAA,IACf,WAAW,mBAAmB,aAAa,MAAM;AAAA,EACnD;AACF;AAEA,SAAS,6BACP,UACA,aACA,QACQ;AACR,MAAI,QAAQ;AACV,aAAS,QAAQ,SAAS,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG;AAC5D,YAAM,UAAU,SAAS,KAAK;AAC9B,UAAI,SAAS,SAAS,UAAU,iBAAiB,SAAS,MAAM,EAAG,QAAO;AAAA,IAC5E;AAAA,EACF;AAEA,QAAM,SAAS,SAAS,GAAG,EAAE;AAC7B,MAAI,QAAQ,SAAS,UAAU,OAAO,YAAY,aAAa;AAC7D,WAAO,SAAS,SAAS;AAAA,EAC3B;AAEA,SAAO;AACT;AAEA,SAAS,kBAAkB,UAAiD;AAC1E,SAAO,SAAS,OAAO,CAAC,YAAY,QAAQ,SAAS,MAAM,EAAE;AAC/D;","names":[]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/integrations/index.ts
|
|
2
|
+
import { parseIntegrationToolName } from "@tangle-network/agent-integrations/catalog";
|
|
3
|
+
function resolveIntegrationAction(toolName) {
|
|
4
|
+
let parsed;
|
|
5
|
+
try {
|
|
6
|
+
parsed = parseIntegrationToolName(toolName);
|
|
7
|
+
} catch {
|
|
8
|
+
return void 0;
|
|
9
|
+
}
|
|
10
|
+
if (!parsed.providerId || !parsed.connectorId || !parsed.actionId) return void 0;
|
|
11
|
+
return { ...parsed, path: `${parsed.providerId}.${parsed.connectorId}.${parsed.actionId}` };
|
|
12
|
+
}
|
|
13
|
+
var HubExecClient = class {
|
|
14
|
+
baseUrl;
|
|
15
|
+
bearer;
|
|
16
|
+
fetchImpl;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
if (!options.baseUrl) throw new Error("HubExecClient: baseUrl is required");
|
|
19
|
+
if (!options.bearer) throw new Error("HubExecClient: bearer is required");
|
|
20
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
21
|
+
this.bearer = options.bearer;
|
|
22
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
23
|
+
}
|
|
24
|
+
async exec(input) {
|
|
25
|
+
const response = await this.fetchImpl(`${this.baseUrl}/v1/hub/exec`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { Authorization: `Bearer ${this.bearer}`, "Content-Type": "application/json", Accept: "application/json" },
|
|
28
|
+
body: JSON.stringify({ path: input.path, input: input.actionInput, connectionId: input.connectionId })
|
|
29
|
+
});
|
|
30
|
+
const envelope = await this.readEnvelope(response);
|
|
31
|
+
if (response.ok && envelope.success) return { succeeded: true, result: envelope.data?.result };
|
|
32
|
+
return {
|
|
33
|
+
succeeded: false,
|
|
34
|
+
code: envelope.error?.code ?? `HUB_HTTP_${response.status}`,
|
|
35
|
+
message: envelope.error?.message ?? `Hub /exec returned ${response.status}`,
|
|
36
|
+
approval: envelope.error?.details?.approval
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async readEnvelope(response) {
|
|
40
|
+
const text = await response.text();
|
|
41
|
+
if (!text) return { success: false, error: { code: `HUB_HTTP_${response.status}`, message: `Hub returned ${response.status} with no body` } };
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(text);
|
|
44
|
+
} catch {
|
|
45
|
+
return { success: false, error: { code: "HUB_BAD_RESPONSE", message: `Hub returned non-JSON (${response.status}): ${text.slice(0, 200)}` } };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
async function invokeIntegrationHub(input, deps) {
|
|
50
|
+
const env = deps.env ?? process.env;
|
|
51
|
+
const baseUrl = deps.baseUrl ?? env.TANGLE_PLATFORM_URL?.trim();
|
|
52
|
+
if (!baseUrl) return { status: 500, body: { error: "TANGLE_PLATFORM_URL is not configured" } };
|
|
53
|
+
const action = resolveIntegrationAction(input.toolName);
|
|
54
|
+
if (!action) return { status: 400, body: { error: `Unsupported integration tool: ${input.toolName}` } };
|
|
55
|
+
const bearer = await deps.apiKeyResolver(input.userId);
|
|
56
|
+
if (!bearer) return { status: 401, body: { error: "Tangle account not linked \u2014 connect integrations from the app first" } };
|
|
57
|
+
const client = new HubExecClient({ baseUrl, bearer, fetchImpl: deps.fetchImpl });
|
|
58
|
+
const outcome = await client.exec({ path: action.path, actionInput: input.args ?? {} });
|
|
59
|
+
if (outcome.succeeded) {
|
|
60
|
+
return { status: 200, body: { success: true, path: action.path, providerId: action.providerId, action: action.actionId, result: outcome.result } };
|
|
61
|
+
}
|
|
62
|
+
const status = outcome.code === "HUB_APPROVAL_REQUIRED" ? 409 : 502;
|
|
63
|
+
return {
|
|
64
|
+
status,
|
|
65
|
+
body: { success: false, path: action.path, code: outcome.code, error: outcome.message, ...outcome.approval ? { approval: outcome.approval } : {} }
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
resolveIntegrationAction,
|
|
71
|
+
HubExecClient,
|
|
72
|
+
invokeIntegrationHub
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=chunk-L2TG5DBW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/integrations/index.ts"],"sourcesContent":["/**\n * Integration-hub WIRING — the agent→hub invocation path every Tangle agent app\n * forks. The integration ENGINE (catalog, connectors, OAuth, policy) is\n * `@tangle-network/agent-integrations` (a peer dependency); this module is the\n * thin app-side wiring on top: a typed client over the platform hub's\n * `POST /v1/hub/exec`, MCP-tool-name → hub-action-path resolution, and the\n * per-turn invoke flow the `integration_invoke` tool calls.\n *\n * The product supplies its own catalog (which connectors it uses) and its own\n * per-user api-key resolver (the `apiKeyResolver` seam) — this module owns\n * neither credentials nor the action catalog.\n */\nimport { parseIntegrationToolName } from '@tangle-network/agent-integrations/catalog'\n\n/** `{ success: false }` codes the hub returns on `/exec`. */\nexport type HubExecErrorCode =\n | 'HUB_APPROVAL_REQUIRED'\n | 'HUB_POLICY_DENIED'\n | 'HUB_CONNECTION_MISSING'\n | 'HUB_CONNECTION_REVOKED'\n | 'HUB_CONFIG_MISSING'\n | 'HUB_NOT_FOUND'\n | string\n\n/** Outcome of a hub `/exec` call. Callers MUST inspect `succeeded` before\n * reading `result` — a denied or approval-gated write resolves with\n * `succeeded: false` and a populated `code`, never a thrown silent failure. */\nexport type HubExecResult =\n | { succeeded: true; result: unknown }\n | { succeeded: false; code: HubExecErrorCode; message: string; approval?: unknown }\n\nexport interface HubExecClientOptions {\n /** Platform base URL (e.g. `TANGLE_PLATFORM_URL`). */\n baseUrl: string\n /** Calling user's Tangle API key — the hub principal bearer. */\n bearer: string\n /** Test seam. Defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\n/** The provider/connector/action a hub action path addresses, plus the dotted\n * `path` the hub `/exec` endpoint expects. */\nexport interface ParsedIntegrationAction {\n providerId: string\n connectorId: string\n actionId: string\n /** `provider.connector.action`. */\n path: string\n}\n\n/**\n * Resolve an MCP tool name (the opaque `int_…` catalog name the agent calls)\n * into the dotted hub action path. Returns `undefined` when the name is not a\n * catalog integration tool, so the chat loop routes non-integration calls\n * elsewhere instead of misrouting them to the hub.\n */\nexport function resolveIntegrationAction(toolName: string): ParsedIntegrationAction | undefined {\n let parsed: { providerId: string; connectorId: string; actionId: string }\n try {\n parsed = parseIntegrationToolName(toolName)\n } catch {\n return undefined\n }\n if (!parsed.providerId || !parsed.connectorId || !parsed.actionId) return undefined\n return { ...parsed, path: `${parsed.providerId}.${parsed.connectorId}.${parsed.actionId}` }\n}\n\ninterface HubEnvelope {\n success: boolean\n data?: { result?: unknown }\n error?: { code?: string; message?: string; details?: { approval?: unknown } }\n}\n\n/** Typed client over the platform hub `/v1/hub/exec`. The hub holds the user's\n * credentials, resolves the connection from the bearer principal, evaluates\n * per-action policy (read → allow, write/destructive → approval), and runs the\n * action server-side. Never throws on a policy block — a gated write is a\n * normal `succeeded: false` outcome. */\nexport class HubExecClient {\n private readonly baseUrl: string\n private readonly bearer: string\n private readonly fetchImpl: typeof fetch\n\n constructor(options: HubExecClientOptions) {\n if (!options.baseUrl) throw new Error('HubExecClient: baseUrl is required')\n if (!options.bearer) throw new Error('HubExecClient: bearer is required')\n this.baseUrl = options.baseUrl.replace(/\\/+$/, '')\n this.bearer = options.bearer\n this.fetchImpl = options.fetchImpl ?? fetch\n }\n\n async exec(input: { path: string; actionInput?: unknown; connectionId?: string }): Promise<HubExecResult> {\n const response = await this.fetchImpl(`${this.baseUrl}/v1/hub/exec`, {\n method: 'POST',\n headers: { Authorization: `Bearer ${this.bearer}`, 'Content-Type': 'application/json', Accept: 'application/json' },\n body: JSON.stringify({ path: input.path, input: input.actionInput, connectionId: input.connectionId }),\n })\n const envelope = await this.readEnvelope(response)\n if (response.ok && envelope.success) return { succeeded: true, result: envelope.data?.result }\n return {\n succeeded: false,\n code: envelope.error?.code ?? `HUB_HTTP_${response.status}`,\n message: envelope.error?.message ?? `Hub /exec returned ${response.status}`,\n approval: envelope.error?.details?.approval,\n }\n }\n\n private async readEnvelope(response: Response): Promise<HubEnvelope> {\n const text = await response.text()\n if (!text) return { success: false, error: { code: `HUB_HTTP_${response.status}`, message: `Hub returned ${response.status} with no body` } }\n try {\n return JSON.parse(text) as HubEnvelope\n } catch {\n return { success: false, error: { code: 'HUB_BAD_RESPONSE', message: `Hub returned non-JSON (${response.status}): ${text.slice(0, 200)}` } }\n }\n }\n}\n\nexport interface HubInvokeInput {\n userId: string\n /** The MCP tool name the agent called (`int_<provider>_<connector>_<action>`). */\n toolName: string\n args?: Record<string, unknown>\n}\nexport interface HubInvokeOutcome {\n status: number\n body: Record<string, unknown>\n}\nexport interface HubInvokeDeps {\n /** Resolve the user's Tangle API key (the hub principal bearer). Required —\n * the product binds its own session-key resolver. Null → user not linked. */\n apiKeyResolver: (userId: string) => Promise<string | null>\n /** Platform base URL. Defaults to `env.TANGLE_PLATFORM_URL`. */\n baseUrl?: string\n fetchImpl?: typeof fetch\n env?: Record<string, string | undefined>\n}\n\n/**\n * Resolve + execute one integration tool call through the hub: resolve the\n * per-user bearer, map the MCP tool name to the hub action path, forward to\n * `/v1/hub/exec`, and shape the route response (200 ok / 401 not-linked /\n * 400 unknown-tool / 409 approval-required / 502 hub-error). A write that's\n * approval-gated surfaces verbatim as 409, never silently executed.\n */\nexport async function invokeIntegrationHub(input: HubInvokeInput, deps: HubInvokeDeps): Promise<HubInvokeOutcome> {\n const env = deps.env ?? (process.env as Record<string, string | undefined>)\n const baseUrl = deps.baseUrl ?? env.TANGLE_PLATFORM_URL?.trim()\n if (!baseUrl) return { status: 500, body: { error: 'TANGLE_PLATFORM_URL is not configured' } }\n\n const action = resolveIntegrationAction(input.toolName)\n if (!action) return { status: 400, body: { error: `Unsupported integration tool: ${input.toolName}` } }\n\n const bearer = await deps.apiKeyResolver(input.userId)\n if (!bearer) return { status: 401, body: { error: 'Tangle account not linked — connect integrations from the app first' } }\n\n const client = new HubExecClient({ baseUrl, bearer, fetchImpl: deps.fetchImpl })\n const outcome = await client.exec({ path: action.path, actionInput: input.args ?? {} })\n\n if (outcome.succeeded) {\n return { status: 200, body: { success: true, path: action.path, providerId: action.providerId, action: action.actionId, result: outcome.result } }\n }\n const status = outcome.code === 'HUB_APPROVAL_REQUIRED' ? 409 : 502\n return {\n status,\n body: { success: false, path: action.path, code: outcome.code, error: outcome.message, ...(outcome.approval ? { approval: outcome.approval } : {}) },\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/crypto/index.ts
|
|
2
|
+
var IV_LENGTH = 12;
|
|
3
|
+
var TAG_LENGTH = 16;
|
|
4
|
+
var ALGORITHM = "AES-GCM";
|
|
5
|
+
function decodeHexKey(keyHex) {
|
|
6
|
+
if (keyHex.length !== 64) throw new Error("encryption key must be a 64-char hex string (32 bytes)");
|
|
7
|
+
const bytes = new Uint8Array(32);
|
|
8
|
+
for (let i = 0; i < 64; i += 2) bytes[i / 2] = parseInt(keyHex.substring(i, i + 2), 16);
|
|
9
|
+
return bytes;
|
|
10
|
+
}
|
|
11
|
+
async function importKey(keyHex) {
|
|
12
|
+
const raw = decodeHexKey(keyHex);
|
|
13
|
+
return crypto.subtle.importKey("raw", raw.buffer, { name: ALGORITHM }, false, ["encrypt", "decrypt"]);
|
|
14
|
+
}
|
|
15
|
+
function toBase64(data) {
|
|
16
|
+
let binary = "";
|
|
17
|
+
for (let i = 0; i < data.length; i++) binary += String.fromCharCode(data[i]);
|
|
18
|
+
return btoa(binary);
|
|
19
|
+
}
|
|
20
|
+
function fromBase64(b64) {
|
|
21
|
+
const binary = atob(b64);
|
|
22
|
+
const bytes = new Uint8Array(binary.length);
|
|
23
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
24
|
+
return bytes;
|
|
25
|
+
}
|
|
26
|
+
async function encryptAesGcm(plaintext, keyHex) {
|
|
27
|
+
const key = await importKey(keyHex);
|
|
28
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
29
|
+
const ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv, tagLength: TAG_LENGTH * 8 }, key, new TextEncoder().encode(plaintext));
|
|
30
|
+
const result = new Uint8Array(IV_LENGTH + ciphertext.byteLength);
|
|
31
|
+
result.set(iv, 0);
|
|
32
|
+
result.set(new Uint8Array(ciphertext), IV_LENGTH);
|
|
33
|
+
return toBase64(result);
|
|
34
|
+
}
|
|
35
|
+
async function decryptAesGcm(encrypted, keyHex) {
|
|
36
|
+
const key = await importKey(keyHex);
|
|
37
|
+
const data = fromBase64(encrypted);
|
|
38
|
+
const iv = data.slice(0, IV_LENGTH);
|
|
39
|
+
const ciphertext = data.slice(IV_LENGTH);
|
|
40
|
+
const plain = await crypto.subtle.decrypt({ name: ALGORITHM, iv, tagLength: TAG_LENGTH * 8 }, key, ciphertext);
|
|
41
|
+
return new TextDecoder().decode(plain);
|
|
42
|
+
}
|
|
43
|
+
function createFieldCrypto(key) {
|
|
44
|
+
const resolve = typeof key === "function" ? key : () => key;
|
|
45
|
+
return {
|
|
46
|
+
encrypt: (s) => encryptAesGcm(s, resolve()),
|
|
47
|
+
decrypt: (s) => decryptAesGcm(s, resolve())
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
decodeHexKey,
|
|
53
|
+
encryptAesGcm,
|
|
54
|
+
decryptAesGcm,
|
|
55
|
+
createFieldCrypto
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=chunk-SIDR6BH3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/crypto/index.ts"],"sourcesContent":["/**\n * AES-256-GCM field encryption (for PII at rest — SSN/EIN/ID numbers, secrets).\n * WebCrypto only — runs on Cloudflare Workers, Node, and the browser with no\n * Node `crypto` dependency. The 32-byte key is a PARAMETER (64-char hex); the\n * framework never reads env — the product binds its own `ENCRYPTION_KEY` (this\n * is the concrete impl behind the `KeyCrypto` seam in `../billing`).\n *\n * Wire format: base64(iv ‖ ciphertext ‖ tag) — the 12-byte IV is prepended; the\n * GCM auth tag is appended by WebCrypto inside the ciphertext.\n */\n\nconst IV_LENGTH = 12\nconst TAG_LENGTH = 16\nconst ALGORITHM = 'AES-GCM'\n\n/** Validate + decode a 64-char hex key to 32 bytes. Throws on the wrong shape so\n * a misconfigured key fails loud, never silently weakens encryption. */\nexport function decodeHexKey(keyHex: string): Uint8Array {\n if (keyHex.length !== 64) throw new Error('encryption key must be a 64-char hex string (32 bytes)')\n const bytes = new Uint8Array(32)\n for (let i = 0; i < 64; i += 2) bytes[i / 2] = parseInt(keyHex.substring(i, i + 2), 16)\n return bytes\n}\n\nasync function importKey(keyHex: string): Promise<CryptoKey> {\n const raw = decodeHexKey(keyHex)\n return crypto.subtle.importKey('raw', raw.buffer as ArrayBuffer, { name: ALGORITHM } as Algorithm, false, ['encrypt', 'decrypt'])\n}\n\nfunction toBase64(data: Uint8Array): string {\n let binary = ''\n for (let i = 0; i < data.length; i++) binary += String.fromCharCode(data[i]!)\n return btoa(binary)\n}\n\nfunction fromBase64(b64: string): Uint8Array {\n const binary = atob(b64)\n const bytes = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)\n return bytes\n}\n\n/** Encrypt `plaintext` with AES-256-GCM under `keyHex`. Returns\n * base64(iv ‖ ciphertext ‖ tag). A fresh random IV per call. */\nexport async function encryptAesGcm(plaintext: string, keyHex: string): Promise<string> {\n const key = await importKey(keyHex)\n const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH))\n const ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv, tagLength: TAG_LENGTH * 8 }, key, new TextEncoder().encode(plaintext))\n const result = new Uint8Array(IV_LENGTH + ciphertext.byteLength)\n result.set(iv, 0)\n result.set(new Uint8Array(ciphertext), IV_LENGTH)\n return toBase64(result)\n}\n\n/** Decrypt a base64(iv ‖ ciphertext ‖ tag) string under `keyHex`. Throws if the\n * tag fails (tamper/wrong key). */\nexport async function decryptAesGcm(encrypted: string, keyHex: string): Promise<string> {\n const key = await importKey(keyHex)\n const data = fromBase64(encrypted)\n const iv = data.slice(0, IV_LENGTH)\n const ciphertext = data.slice(IV_LENGTH)\n const plain = await crypto.subtle.decrypt({ name: ALGORITHM, iv, tagLength: TAG_LENGTH * 8 }, key, ciphertext)\n return new TextDecoder().decode(plain)\n}\n\n/** Build a {@link import('../billing').KeyCrypto}-compatible pair bound to a key\n * (or a key-resolver, for env-backed keys resolved per call). */\nexport function createFieldCrypto(key: string | (() => string)): { encrypt(s: string): Promise<string>; decrypt(s: string): Promise<string> } {\n const resolve = typeof key === 'function' ? key : () => key\n return {\n encrypt: (s) => encryptAesGcm(s, resolve()),\n decrypt: (s) => decryptAesGcm(s, resolve()),\n }\n}\n"],"mappings":";AAWA,IAAM,YAAY;AAClB,IAAM,aAAa;AACnB,IAAM,YAAY;AAIX,SAAS,aAAa,QAA4B;AACvD,MAAI,OAAO,WAAW,GAAI,OAAM,IAAI,MAAM,wDAAwD;AAClG,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK,EAAG,OAAM,IAAI,CAAC,IAAI,SAAS,OAAO,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE;AACtF,SAAO;AACT;AAEA,eAAe,UAAU,QAAoC;AAC3D,QAAM,MAAM,aAAa,MAAM;AAC/B,SAAO,OAAO,OAAO,UAAU,OAAO,IAAI,QAAuB,EAAE,MAAM,UAAU,GAAgB,OAAO,CAAC,WAAW,SAAS,CAAC;AAClI;AAEA,SAAS,SAAS,MAA0B;AAC1C,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,IAAK,WAAU,OAAO,aAAa,KAAK,CAAC,CAAE;AAC5E,SAAO,KAAK,MAAM;AACpB;AAEA,SAAS,WAAW,KAAyB;AAC3C,QAAM,SAAS,KAAK,GAAG;AACvB,QAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;AAC1C,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,IAAK,OAAM,CAAC,IAAI,OAAO,WAAW,CAAC;AACtE,SAAO;AACT;AAIA,eAAsB,cAAc,WAAmB,QAAiC;AACtF,QAAM,MAAM,MAAM,UAAU,MAAM;AAClC,QAAM,KAAK,OAAO,gBAAgB,IAAI,WAAW,SAAS,CAAC;AAC3D,QAAM,aAAa,MAAM,OAAO,OAAO,QAAQ,EAAE,MAAM,WAAW,IAAI,WAAW,aAAa,EAAE,GAAG,KAAK,IAAI,YAAY,EAAE,OAAO,SAAS,CAAC;AAC3I,QAAM,SAAS,IAAI,WAAW,YAAY,WAAW,UAAU;AAC/D,SAAO,IAAI,IAAI,CAAC;AAChB,SAAO,IAAI,IAAI,WAAW,UAAU,GAAG,SAAS;AAChD,SAAO,SAAS,MAAM;AACxB;AAIA,eAAsB,cAAc,WAAmB,QAAiC;AACtF,QAAM,MAAM,MAAM,UAAU,MAAM;AAClC,QAAM,OAAO,WAAW,SAAS;AACjC,QAAM,KAAK,KAAK,MAAM,GAAG,SAAS;AAClC,QAAM,aAAa,KAAK,MAAM,SAAS;AACvC,QAAM,QAAQ,MAAM,OAAO,OAAO,QAAQ,EAAE,MAAM,WAAW,IAAI,WAAW,aAAa,EAAE,GAAG,KAAK,UAAU;AAC7G,SAAO,IAAI,YAAY,EAAE,OAAO,KAAK;AACvC;AAIO,SAAS,kBAAkB,KAA4G;AAC5I,QAAM,UAAU,OAAO,QAAQ,aAAa,MAAM,MAAM;AACxD,SAAO;AAAA,IACL,SAAS,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC;AAAA,IAC1C,SAAS,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC;AAAA,EAC5C;AACF;","names":[]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/tangle/index.ts
|
|
2
|
+
function buildConsentUrl(input) {
|
|
3
|
+
const base = input.endpoint.replace(/\/+$/, "");
|
|
4
|
+
const params = new URLSearchParams({
|
|
5
|
+
client_id: input.clientId,
|
|
6
|
+
redirect_uri: input.redirectUri,
|
|
7
|
+
scope: input.scopes.join(" "),
|
|
8
|
+
state: input.state,
|
|
9
|
+
response_type: "code"
|
|
10
|
+
});
|
|
11
|
+
if (input.connectionId) params.set("connection_id", input.connectionId);
|
|
12
|
+
return `${base}/cross-site/app-consent?${params.toString()}`;
|
|
13
|
+
}
|
|
14
|
+
function createBrokerTokenProvider(opts) {
|
|
15
|
+
const now = opts.now ?? (() => Date.now());
|
|
16
|
+
const skew = opts.refreshSkewMs ?? 3e4;
|
|
17
|
+
let cached = null;
|
|
18
|
+
let inflight = null;
|
|
19
|
+
async function mint() {
|
|
20
|
+
const t = await opts.client.mintBrokerToken({
|
|
21
|
+
clientId: opts.clientId,
|
|
22
|
+
clientSecret: opts.clientSecret,
|
|
23
|
+
grantId: opts.grantId,
|
|
24
|
+
ttlSeconds: opts.ttlSeconds
|
|
25
|
+
});
|
|
26
|
+
cached = { token: t.accessToken, expiresAt: now() + t.expiresIn * 1e3 };
|
|
27
|
+
return t.accessToken;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
async getToken() {
|
|
31
|
+
if (cached && now() < cached.expiresAt - skew) return cached.token;
|
|
32
|
+
if (inflight) return inflight;
|
|
33
|
+
inflight = mint().finally(() => {
|
|
34
|
+
inflight = null;
|
|
35
|
+
});
|
|
36
|
+
return inflight;
|
|
37
|
+
},
|
|
38
|
+
invalidate() {
|
|
39
|
+
cached = null;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
buildConsentUrl,
|
|
46
|
+
createBrokerTokenProvider
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=chunk-YGUNTIT5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tangle/index.ts"],"sourcesContent":["/**\n * Tangle login + the developer self-service app-registration → broker-token\n * flow, for apps built on agent-app.\n *\n * The platform (agent-dev-container integration hub) lets a developer register\n * their client app and obtain a `sk-tan-broker-` bearer to call `/v1/hub/exec`\n * on a user's connected integrations — WITHOUT being a hard-coded \"trusted app\".\n * The wire client (`TangleAppsClient` — registerApp / exchangeAuthCode /\n * mintBrokerToken) lives in `@tangle-network/agent-integrations`; this module is\n * the app-shell layer on top, and is intentionally **structural**: it depends on\n * the minter CONTRACT, not the concrete client, so it installs without the\n * agent-integrations publish and is trivially testable. A consumer constructs\n * the real client and passes it in.\n *\n * 1. {@link buildConsentUrl} — send the user through the ONE-TIME consent\n * (their Tangle session authorizes the app for a connection + scopes).\n * 2. On the callback, the consumer's client `exchangeAuthCode`s the `agc_`\n * code into the first broker token + a durable grant.\n * 3. {@link createBrokerTokenProvider} — the runtime path: a cached provider\n * that re-mints a fresh single-use broker token per `/v1/hub/exec` from the\n * durable grant using only the app credentials (no user session). Caches\n * until just before expiry so a burst of hub calls shares one mint.\n */\n\n/** A single-use hub bearer minted from a durable grant — mirrors\n * `@tangle-network/agent-integrations`'s `BrokerToken`. */\nexport interface BrokerToken {\n /** The `sk-tan-broker-…` bearer for a single `/v1/hub/exec` call. */\n accessToken: string\n /** Seconds until expiry. */\n expiresIn: number\n scope: string\n connectionId?: string\n}\n\n/** The one method the provider needs — `TangleAppsClient` satisfies it\n * structurally, so `createBrokerTokenProvider({ client: tangleAppsClient, … })`\n * type-checks without importing the concrete class. */\nexport interface BrokerTokenMinter {\n mintBrokerToken(input: { clientId: string; clientSecret: string; grantId: string; ttlSeconds?: number }): Promise<BrokerToken>\n}\n\nexport interface ConsentUrlInput {\n /** Platform base URL (e.g. https://id.tangle.tools). */\n endpoint: string\n clientId: string\n /** Must match one of the app's registered redirect URIs. */\n redirectUri: string\n /** Scopes the app is requesting for this connection (e.g. ['gmail.read']). */\n scopes: string[]\n /** Opaque CSRF/state value the callback echoes back — verify it on return. */\n state: string\n /** Optionally pre-select a specific connection to authorize. */\n connectionId?: string\n}\n\n/**\n * Build the URL to send the user to for the one-time app-consent. The user's\n * Tangle session (not the app's credentials) authorizes it; on approval the\n * platform redirects to `redirectUri?code=agc_…&state=…`.\n */\nexport function buildConsentUrl(input: ConsentUrlInput): string {\n const base = input.endpoint.replace(/\\/+$/, '')\n const params = new URLSearchParams({\n client_id: input.clientId,\n redirect_uri: input.redirectUri,\n scope: input.scopes.join(' '),\n state: input.state,\n response_type: 'code',\n })\n if (input.connectionId) params.set('connection_id', input.connectionId)\n return `${base}/cross-site/app-consent?${params.toString()}`\n}\n\nexport interface BrokerTokenProviderOptions {\n client: BrokerTokenMinter\n clientId: string\n clientSecret: string\n /** The durable grant id from the consent exchange. */\n grantId: string\n /** Requested token TTL (seconds). */\n ttlSeconds?: number\n /** Re-mint this many ms BEFORE expiry so an in-flight call never uses a\n * just-expired token. Default 30s. */\n refreshSkewMs?: number\n /** Injectable clock (ms). Default `Date.now`. */\n now?: () => number\n}\n\nexport interface BrokerTokenProvider {\n /** A valid `sk-tan-broker-` bearer, minting/refreshing as needed. */\n getToken(): Promise<string>\n /** Force the next `getToken` to re-mint (e.g. after a 401 from the hub). */\n invalidate(): void\n}\n\n/**\n * Cache + auto-refresh a broker token for one grant. A burst of hub calls\n * shares a single mint; the token is re-minted once it's within `refreshSkewMs`\n * of expiry, or on demand via {@link BrokerTokenProvider.invalidate}.\n * Concurrent `getToken` calls during a mint share the same in-flight promise\n * (no thundering herd).\n */\nexport function createBrokerTokenProvider(opts: BrokerTokenProviderOptions): BrokerTokenProvider {\n const now = opts.now ?? (() => Date.now())\n const skew = opts.refreshSkewMs ?? 30_000\n let cached: { token: string; expiresAt: number } | null = null\n let inflight: Promise<string> | null = null\n\n async function mint(): Promise<string> {\n const t = await opts.client.mintBrokerToken({\n clientId: opts.clientId,\n clientSecret: opts.clientSecret,\n grantId: opts.grantId,\n ttlSeconds: opts.ttlSeconds,\n })\n cached = { token: t.accessToken, expiresAt: now() + t.expiresIn * 1000 }\n return t.accessToken\n }\n\n return {\n async getToken() {\n if (cached && now() < cached.expiresAt - skew) return cached.token\n if (inflight) return inflight\n inflight = mint().finally(() => {\n inflight = null\n })\n return inflight\n },\n invalidate() {\n cached = null\n },\n }\n}\n"],"mappings":";AA6DO,SAAS,gBAAgB,OAAgC;AAC9D,QAAM,OAAO,MAAM,SAAS,QAAQ,QAAQ,EAAE;AAC9C,QAAM,SAAS,IAAI,gBAAgB;AAAA,IACjC,WAAW,MAAM;AAAA,IACjB,cAAc,MAAM;AAAA,IACpB,OAAO,MAAM,OAAO,KAAK,GAAG;AAAA,IAC5B,OAAO,MAAM;AAAA,IACb,eAAe;AAAA,EACjB,CAAC;AACD,MAAI,MAAM,aAAc,QAAO,IAAI,iBAAiB,MAAM,YAAY;AACtE,SAAO,GAAG,IAAI,2BAA2B,OAAO,SAAS,CAAC;AAC5D;AA+BO,SAAS,0BAA0B,MAAuD;AAC/F,QAAM,MAAM,KAAK,QAAQ,MAAM,KAAK,IAAI;AACxC,QAAM,OAAO,KAAK,iBAAiB;AACnC,MAAI,SAAsD;AAC1D,MAAI,WAAmC;AAEvC,iBAAe,OAAwB;AACrC,UAAM,IAAI,MAAM,KAAK,OAAO,gBAAgB;AAAA,MAC1C,UAAU,KAAK;AAAA,MACf,cAAc,KAAK;AAAA,MACnB,SAAS,KAAK;AAAA,MACd,YAAY,KAAK;AAAA,IACnB,CAAC;AACD,aAAS,EAAE,OAAO,EAAE,aAAa,WAAW,IAAI,IAAI,EAAE,YAAY,IAAK;AACvE,WAAO,EAAE;AAAA,EACX;AAEA,SAAO;AAAA,IACL,MAAM,WAAW;AACf,UAAI,UAAU,IAAI,IAAI,OAAO,YAAY,KAAM,QAAO,OAAO;AAC7D,UAAI,SAAU,QAAO;AACrB,iBAAW,KAAK,EAAE,QAAQ,MAAM;AAC9B,mBAAW;AAAA,MACb,CAAC;AACD,aAAO;AAAA,IACT;AAAA,IACA,aAAa;AACX,eAAS;AAAA,IACX;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM field encryption (for PII at rest — SSN/EIN/ID numbers, secrets).
|
|
3
|
+
* WebCrypto only — runs on Cloudflare Workers, Node, and the browser with no
|
|
4
|
+
* Node `crypto` dependency. The 32-byte key is a PARAMETER (64-char hex); the
|
|
5
|
+
* framework never reads env — the product binds its own `ENCRYPTION_KEY` (this
|
|
6
|
+
* is the concrete impl behind the `KeyCrypto` seam in `../billing`).
|
|
7
|
+
*
|
|
8
|
+
* Wire format: base64(iv ‖ ciphertext ‖ tag) — the 12-byte IV is prepended; the
|
|
9
|
+
* GCM auth tag is appended by WebCrypto inside the ciphertext.
|
|
10
|
+
*/
|
|
11
|
+
/** Validate + decode a 64-char hex key to 32 bytes. Throws on the wrong shape so
|
|
12
|
+
* a misconfigured key fails loud, never silently weakens encryption. */
|
|
13
|
+
declare function decodeHexKey(keyHex: string): Uint8Array;
|
|
14
|
+
/** Encrypt `plaintext` with AES-256-GCM under `keyHex`. Returns
|
|
15
|
+
* base64(iv ‖ ciphertext ‖ tag). A fresh random IV per call. */
|
|
16
|
+
declare function encryptAesGcm(plaintext: string, keyHex: string): Promise<string>;
|
|
17
|
+
/** Decrypt a base64(iv ‖ ciphertext ‖ tag) string under `keyHex`. Throws if the
|
|
18
|
+
* tag fails (tamper/wrong key). */
|
|
19
|
+
declare function decryptAesGcm(encrypted: string, keyHex: string): Promise<string>;
|
|
20
|
+
/** Build a {@link import('../billing').KeyCrypto}-compatible pair bound to a key
|
|
21
|
+
* (or a key-resolver, for env-backed keys resolved per call). */
|
|
22
|
+
declare function createFieldCrypto(key: string | (() => string)): {
|
|
23
|
+
encrypt(s: string): Promise<string>;
|
|
24
|
+
decrypt(s: string): Promise<string>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export { createFieldCrypto, decodeHexKey, decryptAesGcm, encryptAesGcm };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegated looped work — the agent-runtime "driven loop" MCP.
|
|
3
|
+
*
|
|
4
|
+
* For multi-step research or document generation, the agent dispatches a loop
|
|
5
|
+
* that runs to completion in its OWN sandbox (via @tangle-network/agent-runtime's
|
|
6
|
+
* stdio MCP, executed in the agent-driver) and returns the artifact. This is
|
|
7
|
+
* how an app's main agent "programs / delegates" without doing long mechanical
|
|
8
|
+
* work inline. It is an OPTIONAL module — an app opts in by spreading the
|
|
9
|
+
* server into its profile's `mcp` map.
|
|
10
|
+
*
|
|
11
|
+
* The shape is the portable `AgentProfileMcpServer` the sandbox SDK accepts
|
|
12
|
+
* (transport: 'stdio' → the orchestrator derives `{ type:'local', command }`).
|
|
13
|
+
* Kept structural here so this package needs no sandbox-SDK dependency.
|
|
14
|
+
*/
|
|
15
|
+
declare const DELEGATION_MCP_SERVER_KEY = "agent-runtime-delegation";
|
|
16
|
+
declare const DELEGATION_TOOLS: readonly ["delegate_code", "delegate_research", "delegate_feedback", "delegation_status", "delegation_history"];
|
|
17
|
+
/** The stdio MCP server entry — structurally an `AgentProfileMcpServer`. */
|
|
18
|
+
interface DelegationMcpServer {
|
|
19
|
+
transport: 'stdio';
|
|
20
|
+
command: string;
|
|
21
|
+
args: string[];
|
|
22
|
+
env: Record<string, string>;
|
|
23
|
+
enabled: true;
|
|
24
|
+
metadata: {
|
|
25
|
+
surface: string;
|
|
26
|
+
tools: readonly string[];
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
interface BuildDelegationOptions {
|
|
30
|
+
/** Platform API key the delegated loop authenticates with (required — the
|
|
31
|
+
* loop runs in its own sandbox and bills against this key). Omit/empty →
|
|
32
|
+
* returns undefined (fail-closed: no key, no delegation). */
|
|
33
|
+
apiKey?: string;
|
|
34
|
+
/** Extra env to forward into the delegated loop (sandbox base URL, OTel trace
|
|
35
|
+
* propagation, etc.). Only defined values are forwarded. */
|
|
36
|
+
forwardEnv?: Record<string, string | undefined>;
|
|
37
|
+
/** npm spec for the runtime MCP. Defaults to the published agent-runtime. */
|
|
38
|
+
packageSpec?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the delegation MCP server entry, keyed under
|
|
42
|
+
* {@link DELEGATION_MCP_SERVER_KEY}, or `undefined` when no platform API key is
|
|
43
|
+
* available. Spread the result into the profile's `mcp` map:
|
|
44
|
+
*
|
|
45
|
+
* const delegation = buildDelegationMcpServer({ apiKey: env.TANGLE_API_KEY, forwardEnv: env })
|
|
46
|
+
* const mcp = { ...(delegation ? { [DELEGATION_MCP_SERVER_KEY]: delegation } : {}) }
|
|
47
|
+
*/
|
|
48
|
+
declare function buildDelegationMcpServer(opts: BuildDelegationOptions): DelegationMcpServer | undefined;
|
|
49
|
+
|
|
50
|
+
export { type BuildDelegationOptions, DELEGATION_MCP_SERVER_KEY, DELEGATION_TOOLS, type DelegationMcpServer, buildDelegationMcpServer };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|