@okrlinkhub/agent-bridge 2.0.1 → 3.0.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 +19 -15
- package/dist/cli/init.js +5 -7
- package/dist/cli/init.js.map +1 -1
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +110 -32
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +3 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/agents.d.ts +2 -2
- package/dist/component/gateway.d.ts +3 -0
- package/dist/component/gateway.d.ts.map +1 -1
- package/dist/component/gateway.js +27 -0
- package/dist/component/gateway.js.map +1 -1
- package/dist/component/permissions.d.ts +1 -1
- package/dist/component/schema.d.ts +8 -5
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +2 -0
- package/dist/component/schema.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/init.ts +5 -7
- package/src/client/index.test.ts +172 -24
- package/src/client/index.ts +140 -52
- package/src/component/_generated/component.ts +8 -1
- package/src/component/gateway.ts +33 -0
- package/src/component/schema.ts +2 -0
|
@@ -2,10 +2,10 @@ declare const _default: import("convex/server").SchemaDefinition<{
|
|
|
2
2
|
agents: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
3
3
|
appKey?: string | undefined;
|
|
4
4
|
lastUsed?: number | undefined;
|
|
5
|
-
enabled: boolean;
|
|
6
5
|
name: string;
|
|
7
|
-
rateLimit: number;
|
|
8
6
|
apiKeyHash: string;
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
rateLimit: number;
|
|
9
9
|
createdAt: number;
|
|
10
10
|
}, {
|
|
11
11
|
name: import("convex/values").VString<string, "required">;
|
|
@@ -15,7 +15,7 @@ declare const _default: import("convex/server").SchemaDefinition<{
|
|
|
15
15
|
rateLimit: import("convex/values").VFloat64<number, "required">;
|
|
16
16
|
lastUsed: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
17
17
|
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
18
|
-
}, "required", "
|
|
18
|
+
}, "required", "name" | "appKey" | "apiKeyHash" | "enabled" | "rateLimit" | "lastUsed" | "createdAt">, {
|
|
19
19
|
by_apiKeyHash: ["apiKeyHash", "_creationTime"];
|
|
20
20
|
by_appKey: ["appKey", "_creationTime"];
|
|
21
21
|
by_enabled: ["enabled", "_creationTime"];
|
|
@@ -56,8 +56,9 @@ declare const _default: import("convex/server").SchemaDefinition<{
|
|
|
56
56
|
by_key: ["key", "_creationTime"];
|
|
57
57
|
}, {}, {}>;
|
|
58
58
|
agentLogs: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
59
|
-
|
|
59
|
+
serviceId?: string | undefined;
|
|
60
60
|
result?: any;
|
|
61
|
+
error?: string | undefined;
|
|
61
62
|
agentId: import("convex/values").GenericId<"agents">;
|
|
62
63
|
functionKey: string;
|
|
63
64
|
args: any;
|
|
@@ -65,14 +66,16 @@ declare const _default: import("convex/server").SchemaDefinition<{
|
|
|
65
66
|
timestamp: number;
|
|
66
67
|
}, {
|
|
67
68
|
agentId: import("convex/values").VId<import("convex/values").GenericId<"agents">, "required">;
|
|
69
|
+
serviceId: import("convex/values").VString<string | undefined, "optional">;
|
|
68
70
|
functionKey: import("convex/values").VString<string, "required">;
|
|
69
71
|
args: import("convex/values").VAny<any, "required", string>;
|
|
70
72
|
result: import("convex/values").VAny<any, "optional", string>;
|
|
71
73
|
error: import("convex/values").VString<string | undefined, "optional">;
|
|
72
74
|
duration: import("convex/values").VFloat64<number, "required">;
|
|
73
75
|
timestamp: import("convex/values").VFloat64<number, "required">;
|
|
74
|
-
}, "required", "agentId" | "functionKey" | "args" | "
|
|
76
|
+
}, "required", "agentId" | "serviceId" | "functionKey" | "args" | "result" | "error" | "duration" | "timestamp" | `args.${string}` | `result.${string}`>, {
|
|
75
77
|
by_agentId_and_timestamp: ["agentId", "timestamp", "_creationTime"];
|
|
78
|
+
by_serviceId_and_timestamp: ["serviceId", "timestamp", "_creationTime"];
|
|
76
79
|
by_functionKey: ["functionKey", "_creationTime"];
|
|
77
80
|
by_timestamp: ["timestamp", "_creationTime"];
|
|
78
81
|
}, {}, {}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/component/schema.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/component/schema.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,wBAmDG"}
|
package/dist/component/schema.js
CHANGED
|
@@ -30,6 +30,7 @@ export default defineSchema({
|
|
|
30
30
|
}).index("by_key", ["key"]),
|
|
31
31
|
agentLogs: defineTable({
|
|
32
32
|
agentId: v.id("agents"),
|
|
33
|
+
serviceId: v.optional(v.string()),
|
|
33
34
|
functionKey: v.string(),
|
|
34
35
|
args: v.any(),
|
|
35
36
|
result: v.optional(v.any()),
|
|
@@ -38,6 +39,7 @@ export default defineSchema({
|
|
|
38
39
|
timestamp: v.number(),
|
|
39
40
|
})
|
|
40
41
|
.index("by_agentId_and_timestamp", ["agentId", "timestamp"])
|
|
42
|
+
.index("by_serviceId_and_timestamp", ["serviceId", "timestamp"])
|
|
41
43
|
.index("by_functionKey", ["functionKey"])
|
|
42
44
|
.index("by_timestamp", ["timestamp"]),
|
|
43
45
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/component/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,CAAC,EAAE,MAAM,eAAe,CAAC;AAElC,eAAe,YAAY,CAAC;IAC1B,MAAM,EAAE,WAAW,CAAC;QAClB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAChB,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC9B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;QACtB,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;QACpB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;QACrB,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAChC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;KACtB,CAAC;SACC,KAAK,CAAC,eAAe,EAAE,CAAC,YAAY,CAAC,CAAC;SACtC,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC;SAC9B,KAAK,CAAC,YAAY,EAAE,CAAC,SAAS,CAAC,CAAC;IAEnC,gBAAgB,EAAE,WAAW,CAAC;QAC5B,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC;QACvB,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE;QAC3B,UAAU,EAAE,CAAC,CAAC,KAAK,CACjB,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,EAClB,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,EACjB,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAC1B;QACD,eAAe,EAAE,CAAC,CAAC,QAAQ,CACzB,CAAC,CAAC,MAAM,CAAC;YACP,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE;YAC3B,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACpC,CAAC,CACH;QACD,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;KACtB,CAAC,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC,SAAS,CAAC,CAAC;IAEnC,cAAc,EAAE,WAAW,CAAC;QAC1B,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;QACf,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;QACpB,eAAe,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;KACxC,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC;IAE3B,SAAS,EAAE,WAAW,CAAC;QACrB,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC;QACvB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;QACvB,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE;QACb,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;QAC3B,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC7B,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;QACpB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;KACtB,CAAC;SACC,KAAK,CAAC,0BAA0B,EAAE,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;SAC3D,KAAK,CAAC,gBAAgB,EAAE,CAAC,aAAa,CAAC,CAAC;SACxC,KAAK,CAAC,cAAc,EAAE,CAAC,WAAW,CAAC,CAAC;CACxC,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/component/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,CAAC,EAAE,MAAM,eAAe,CAAC;AAElC,eAAe,YAAY,CAAC;IAC1B,MAAM,EAAE,WAAW,CAAC;QAClB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAChB,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC9B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;QACtB,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;QACpB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;QACrB,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAChC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;KACtB,CAAC;SACC,KAAK,CAAC,eAAe,EAAE,CAAC,YAAY,CAAC,CAAC;SACtC,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC;SAC9B,KAAK,CAAC,YAAY,EAAE,CAAC,SAAS,CAAC,CAAC;IAEnC,gBAAgB,EAAE,WAAW,CAAC;QAC5B,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC;QACvB,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE;QAC3B,UAAU,EAAE,CAAC,CAAC,KAAK,CACjB,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,EAClB,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,EACjB,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAC1B;QACD,eAAe,EAAE,CAAC,CAAC,QAAQ,CACzB,CAAC,CAAC,MAAM,CAAC;YACP,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE;YAC3B,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACpC,CAAC,CACH;QACD,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;KACtB,CAAC,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC,SAAS,CAAC,CAAC;IAEnC,cAAc,EAAE,WAAW,CAAC;QAC1B,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;QACf,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;QACpB,eAAe,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;KACxC,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC;IAE3B,SAAS,EAAE,WAAW,CAAC;QACrB,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC;QACvB,SAAS,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QACjC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;QACvB,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE;QACb,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;QAC3B,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC7B,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;QACpB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;KACtB,CAAC;SACC,KAAK,CAAC,0BAA0B,EAAE,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;SAC3D,KAAK,CAAC,4BAA4B,EAAE,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;SAC/D,KAAK,CAAC,gBAAgB,EAAE,CAAC,aAAa,CAAC,CAAC;SACxC,KAAK,CAAC,cAAc,EAAE,CAAC,WAAW,CAAC,CAAC;CACxC,CAAC,CAAC"}
|
package/package.json
CHANGED
package/src/cli/init.ts
CHANGED
|
@@ -60,9 +60,7 @@ export function registerAgentBridgeRoutes(
|
|
|
60
60
|
) {
|
|
61
61
|
registerRoutes(http, components.agentBridge, config, {
|
|
62
62
|
pathPrefix: "/agent",
|
|
63
|
-
|
|
64
|
-
(globalThis as { process?: { env?: Record<string, string> } }).process?.env
|
|
65
|
-
?.AGENT_BRIDGE_SERVICE_KEY,
|
|
63
|
+
serviceKeysEnvVar: "AGENT_BRIDGE_SERVICE_KEYS_JSON",
|
|
66
64
|
});
|
|
67
65
|
}
|
|
68
66
|
`;
|
|
@@ -94,10 +92,10 @@ export default http;
|
|
|
94
92
|
- components.agentBridge.permissions.setAgentPermissions
|
|
95
93
|
- components.agentBridge.permissions.setFunctionOverrides
|
|
96
94
|
|
|
97
|
-
4)
|
|
98
|
-
-
|
|
99
|
-
- OpenClaw
|
|
100
|
-
-
|
|
95
|
+
4) Configure strict service auth map (required):
|
|
96
|
+
- AGENT_BRIDGE_SERVICE_KEYS_JSON={"openclaw-prod":"<key>","openclaw-staging":"<key>"}
|
|
97
|
+
- OpenClaw must send X-Agent-Service-Id, X-Agent-Service-Key and X-Agent-App
|
|
98
|
+
- Legacy X-Agent-API-Key is not supported in strict mode
|
|
101
99
|
`;
|
|
102
100
|
|
|
103
101
|
async function main() {
|
package/src/client/index.test.ts
CHANGED
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
|
|
8
8
|
function setupExecuteHandler(
|
|
9
9
|
config: AgentBridgeConfig,
|
|
10
|
-
options?: {
|
|
10
|
+
options?: {
|
|
11
|
+
serviceKeys?: Record<string, string>;
|
|
12
|
+
serviceKeysEnvVar?: string;
|
|
13
|
+
},
|
|
11
14
|
) {
|
|
12
15
|
const routes: Array<{
|
|
13
16
|
path: string;
|
|
@@ -34,7 +37,8 @@ function setupExecuteHandler(
|
|
|
34
37
|
|
|
35
38
|
registerRoutes(http as never, component as never, config, {
|
|
36
39
|
pathPrefix: "/agent",
|
|
37
|
-
|
|
40
|
+
serviceKeys: options?.serviceKeys,
|
|
41
|
+
serviceKeysEnvVar: options?.serviceKeysEnvVar,
|
|
38
42
|
});
|
|
39
43
|
const executeRoute = routes.find(
|
|
40
44
|
(route) => route.path === "/agent/execute" && route.method === "POST",
|
|
@@ -45,7 +49,9 @@ function setupExecuteHandler(
|
|
|
45
49
|
return executeRoute.handler;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
|
-
describe("registerRoutes auth
|
|
52
|
+
describe("registerRoutes strict service auth", () => {
|
|
53
|
+
const originalEnv = process.env.AGENT_BRIDGE_SERVICE_KEYS_JSON;
|
|
54
|
+
|
|
49
55
|
const config = defineAgentBridgeConfig({
|
|
50
56
|
functions: {
|
|
51
57
|
"demo.list": {
|
|
@@ -55,8 +61,10 @@ describe("registerRoutes auth modes", () => {
|
|
|
55
61
|
},
|
|
56
62
|
});
|
|
57
63
|
|
|
58
|
-
test("
|
|
59
|
-
const executeHandler = setupExecuteHandler(config, {
|
|
64
|
+
test("authorizes with strict headers and service id map", async () => {
|
|
65
|
+
const executeHandler = setupExecuteHandler(config, {
|
|
66
|
+
serviceKeys: { "railway-a": "svc_key_a" },
|
|
67
|
+
});
|
|
60
68
|
const runMutation = vi
|
|
61
69
|
.fn()
|
|
62
70
|
.mockResolvedValueOnce({
|
|
@@ -72,7 +80,9 @@ describe("registerRoutes auth modes", () => {
|
|
|
72
80
|
method: "POST",
|
|
73
81
|
headers: {
|
|
74
82
|
"Content-Type": "application/json",
|
|
75
|
-
"X-Agent-
|
|
83
|
+
"X-Agent-Service-Id": "railway-a",
|
|
84
|
+
"X-Agent-Service-Key": "svc_key_a",
|
|
85
|
+
"X-Agent-App": "crm",
|
|
76
86
|
},
|
|
77
87
|
body: JSON.stringify({
|
|
78
88
|
functionKey: "demo.list",
|
|
@@ -82,15 +92,26 @@ describe("registerRoutes auth modes", () => {
|
|
|
82
92
|
);
|
|
83
93
|
|
|
84
94
|
expect(response.status).toBe(200);
|
|
85
|
-
expect(runMutation).toHaveBeenNthCalledWith(1, "
|
|
86
|
-
|
|
95
|
+
expect(runMutation).toHaveBeenNthCalledWith(1, "authorizeByAppKeyRef", {
|
|
96
|
+
appKey: "crm",
|
|
87
97
|
functionKey: "demo.list",
|
|
88
98
|
estimatedCost: undefined,
|
|
89
99
|
});
|
|
100
|
+
expect(runMutation).toHaveBeenNthCalledWith(2, "logAccessRef", {
|
|
101
|
+
agentId: "agent_1",
|
|
102
|
+
serviceId: "railway-a",
|
|
103
|
+
functionKey: "demo.list",
|
|
104
|
+
args: {},
|
|
105
|
+
result: { ok: true },
|
|
106
|
+
duration: expect.any(Number),
|
|
107
|
+
timestamp: expect.any(Number),
|
|
108
|
+
});
|
|
90
109
|
});
|
|
91
110
|
|
|
92
|
-
test("returns
|
|
93
|
-
const executeHandler = setupExecuteHandler(config, {
|
|
111
|
+
test("returns 400 when service id header is missing", async () => {
|
|
112
|
+
const executeHandler = setupExecuteHandler(config, {
|
|
113
|
+
serviceKeys: { "railway-a": "svc_key_a" },
|
|
114
|
+
});
|
|
94
115
|
const runMutation = vi.fn();
|
|
95
116
|
|
|
96
117
|
const response = await executeHandler(
|
|
@@ -99,7 +120,7 @@ describe("registerRoutes auth modes", () => {
|
|
|
99
120
|
method: "POST",
|
|
100
121
|
headers: {
|
|
101
122
|
"Content-Type": "application/json",
|
|
102
|
-
"X-Agent-Service-Key": "
|
|
123
|
+
"X-Agent-Service-Key": "svc_key_a",
|
|
103
124
|
"X-Agent-App": "crm",
|
|
104
125
|
},
|
|
105
126
|
body: JSON.stringify({
|
|
@@ -109,17 +130,15 @@ describe("registerRoutes auth modes", () => {
|
|
|
109
130
|
}),
|
|
110
131
|
);
|
|
111
132
|
|
|
112
|
-
expect(response.status).toBe(
|
|
133
|
+
expect(response.status).toBe(400);
|
|
113
134
|
expect(runMutation).not.toHaveBeenCalled();
|
|
114
135
|
});
|
|
115
136
|
|
|
116
|
-
test("returns
|
|
117
|
-
const executeHandler = setupExecuteHandler(config, {
|
|
118
|
-
|
|
119
|
-
authorized: false,
|
|
120
|
-
error: "App crm is not registered",
|
|
121
|
-
statusCode: 404,
|
|
137
|
+
test("returns 400 when service key header is missing", async () => {
|
|
138
|
+
const executeHandler = setupExecuteHandler(config, {
|
|
139
|
+
serviceKeys: { "railway-a": "svc_key_a" },
|
|
122
140
|
});
|
|
141
|
+
const runMutation = vi.fn();
|
|
123
142
|
|
|
124
143
|
const response = await executeHandler(
|
|
125
144
|
{ runMutation, runQuery: vi.fn(), runAction: vi.fn() },
|
|
@@ -127,7 +146,7 @@ describe("registerRoutes auth modes", () => {
|
|
|
127
146
|
method: "POST",
|
|
128
147
|
headers: {
|
|
129
148
|
"Content-Type": "application/json",
|
|
130
|
-
"X-Agent-Service-
|
|
149
|
+
"X-Agent-Service-Id": "railway-a",
|
|
131
150
|
"X-Agent-App": "crm",
|
|
132
151
|
},
|
|
133
152
|
body: JSON.stringify({
|
|
@@ -137,11 +156,140 @@ describe("registerRoutes auth modes", () => {
|
|
|
137
156
|
}),
|
|
138
157
|
);
|
|
139
158
|
|
|
140
|
-
expect(response.status).toBe(
|
|
141
|
-
expect(runMutation).
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
159
|
+
expect(response.status).toBe(400);
|
|
160
|
+
expect(runMutation).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("returns 400 when app header is missing", async () => {
|
|
164
|
+
const executeHandler = setupExecuteHandler(config, {
|
|
165
|
+
serviceKeys: { "railway-a": "svc_key_a" },
|
|
145
166
|
});
|
|
167
|
+
const runMutation = vi.fn();
|
|
168
|
+
|
|
169
|
+
const response = await executeHandler(
|
|
170
|
+
{ runMutation, runQuery: vi.fn(), runAction: vi.fn() },
|
|
171
|
+
new Request("https://example.com/agent/execute", {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: {
|
|
174
|
+
"Content-Type": "application/json",
|
|
175
|
+
"X-Agent-Service-Id": "railway-a",
|
|
176
|
+
"X-Agent-Service-Key": "svc_key_a",
|
|
177
|
+
},
|
|
178
|
+
body: JSON.stringify({
|
|
179
|
+
functionKey: "demo.list",
|
|
180
|
+
args: {},
|
|
181
|
+
}),
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(response.status).toBe(400);
|
|
186
|
+
expect(runMutation).not.toHaveBeenCalled();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("returns 401 when service id is not configured", async () => {
|
|
190
|
+
const executeHandler = setupExecuteHandler(config, {
|
|
191
|
+
serviceKeys: { "railway-a": "svc_key_a" },
|
|
192
|
+
});
|
|
193
|
+
const runMutation = vi.fn();
|
|
194
|
+
|
|
195
|
+
const response = await executeHandler(
|
|
196
|
+
{ runMutation, runQuery: vi.fn(), runAction: vi.fn() },
|
|
197
|
+
new Request("https://example.com/agent/execute", {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: {
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
"X-Agent-Service-Id": "railway-b",
|
|
202
|
+
"X-Agent-Service-Key": "svc_key_b",
|
|
203
|
+
"X-Agent-App": "crm",
|
|
204
|
+
},
|
|
205
|
+
body: JSON.stringify({
|
|
206
|
+
functionKey: "demo.list",
|
|
207
|
+
args: {},
|
|
208
|
+
}),
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(response.status).toBe(401);
|
|
213
|
+
expect(runMutation).not.toHaveBeenCalled();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("returns 401 when service key mismatches configured service id", async () => {
|
|
217
|
+
const executeHandler = setupExecuteHandler(config, {
|
|
218
|
+
serviceKeys: { "railway-a": "svc_key_a" },
|
|
219
|
+
});
|
|
220
|
+
const runMutation = vi.fn();
|
|
221
|
+
|
|
222
|
+
const response = await executeHandler(
|
|
223
|
+
{ runMutation, runQuery: vi.fn(), runAction: vi.fn() },
|
|
224
|
+
new Request("https://example.com/agent/execute", {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: {
|
|
227
|
+
"Content-Type": "application/json",
|
|
228
|
+
"X-Agent-Service-Id": "railway-a",
|
|
229
|
+
"X-Agent-Service-Key": "wrong_key",
|
|
230
|
+
"X-Agent-App": "crm",
|
|
231
|
+
},
|
|
232
|
+
body: JSON.stringify({
|
|
233
|
+
functionKey: "demo.list",
|
|
234
|
+
args: {},
|
|
235
|
+
}),
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
expect(response.status).toBe(401);
|
|
240
|
+
expect(runMutation).not.toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("returns 500 when service key map is missing", async () => {
|
|
244
|
+
delete process.env.AGENT_BRIDGE_SERVICE_KEYS_JSON;
|
|
245
|
+
const executeHandler = setupExecuteHandler(config);
|
|
246
|
+
const runMutation = vi.fn();
|
|
247
|
+
|
|
248
|
+
const response = await executeHandler(
|
|
249
|
+
{ runMutation, runQuery: vi.fn(), runAction: vi.fn() },
|
|
250
|
+
new Request("https://example.com/agent/execute", {
|
|
251
|
+
method: "POST",
|
|
252
|
+
headers: {
|
|
253
|
+
"Content-Type": "application/json",
|
|
254
|
+
"X-Agent-Service-Id": "railway-a",
|
|
255
|
+
"X-Agent-Service-Key": "svc_key_a",
|
|
256
|
+
"X-Agent-App": "crm",
|
|
257
|
+
},
|
|
258
|
+
body: JSON.stringify({
|
|
259
|
+
functionKey: "demo.list",
|
|
260
|
+
args: {},
|
|
261
|
+
}),
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
expect(response.status).toBe(500);
|
|
266
|
+
expect(runMutation).not.toHaveBeenCalled();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("returns 500 when env service key map is invalid JSON", async () => {
|
|
270
|
+
process.env.AGENT_BRIDGE_SERVICE_KEYS_JSON = "not-json";
|
|
271
|
+
const executeHandler = setupExecuteHandler(config);
|
|
272
|
+
const runMutation = vi.fn();
|
|
273
|
+
|
|
274
|
+
const response = await executeHandler(
|
|
275
|
+
{ runMutation, runQuery: vi.fn(), runAction: vi.fn() },
|
|
276
|
+
new Request("https://example.com/agent/execute", {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: {
|
|
279
|
+
"Content-Type": "application/json",
|
|
280
|
+
"X-Agent-Service-Id": "railway-a",
|
|
281
|
+
"X-Agent-Service-Key": "svc_key_a",
|
|
282
|
+
"X-Agent-App": "crm",
|
|
283
|
+
},
|
|
284
|
+
body: JSON.stringify({
|
|
285
|
+
functionKey: "demo.list",
|
|
286
|
+
args: {},
|
|
287
|
+
}),
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
expect(response.status).toBe(500);
|
|
292
|
+
expect(runMutation).not.toHaveBeenCalled();
|
|
293
|
+
process.env.AGENT_BRIDGE_SERVICE_KEYS_JSON = originalEnv;
|
|
146
294
|
});
|
|
147
295
|
});
|
package/src/client/index.ts
CHANGED
|
@@ -136,8 +136,8 @@ type ExecuteRequestBody = {
|
|
|
136
136
|
|
|
137
137
|
type RegisterRoutesOptions = {
|
|
138
138
|
pathPrefix?: string;
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
serviceKeys?: Record<string, string>;
|
|
140
|
+
serviceKeysEnvVar?: string;
|
|
141
141
|
};
|
|
142
142
|
|
|
143
143
|
export function registerRoutes(
|
|
@@ -147,9 +147,10 @@ export function registerRoutes(
|
|
|
147
147
|
options?: RegisterRoutesOptions,
|
|
148
148
|
) {
|
|
149
149
|
const prefix = options?.pathPrefix ?? "/agent";
|
|
150
|
-
const
|
|
151
|
-
options?.
|
|
152
|
-
|
|
150
|
+
const configuredServiceKeys = resolveConfiguredServiceKeys({
|
|
151
|
+
serviceKeys: options?.serviceKeys,
|
|
152
|
+
serviceKeysEnvVar: options?.serviceKeysEnvVar ?? "AGENT_BRIDGE_SERVICE_KEYS_JSON",
|
|
153
|
+
});
|
|
153
154
|
const normalizedConfig = normalizeAgentBridgeConfig(bridgeConfig);
|
|
154
155
|
const availableFunctionKeys = Object.keys(normalizedConfig.functions);
|
|
155
156
|
|
|
@@ -157,8 +158,6 @@ export function registerRoutes(
|
|
|
157
158
|
path: `${prefix}/execute`,
|
|
158
159
|
method: "POST",
|
|
159
160
|
handler: httpActionGeneric(async (ctx, request) => {
|
|
160
|
-
const apiKey = request.headers.get("X-Agent-API-Key");
|
|
161
|
-
|
|
162
161
|
let body: ExecuteRequestBody;
|
|
163
162
|
try {
|
|
164
163
|
body = await request.json();
|
|
@@ -185,22 +184,22 @@ export function registerRoutes(
|
|
|
185
184
|
);
|
|
186
185
|
}
|
|
187
186
|
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
187
|
+
const headerValidation = validateStrictServiceHeaders({
|
|
188
|
+
request,
|
|
189
|
+
configuredServiceKeys,
|
|
190
|
+
});
|
|
191
|
+
if (!headerValidation.valid) {
|
|
192
|
+
return jsonResponse(
|
|
193
|
+
{ success: false, error: headerValidation.error },
|
|
194
|
+
headerValidation.statusCode,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const authResult = await ctx.runMutation(component.gateway.authorizeByAppKey, {
|
|
199
|
+
appKey: headerValidation.appKey,
|
|
200
|
+
functionKey,
|
|
201
|
+
estimatedCost: body.estimatedCost,
|
|
202
|
+
});
|
|
204
203
|
|
|
205
204
|
if (!authResult.authorized) {
|
|
206
205
|
const response = jsonResponse(
|
|
@@ -239,6 +238,7 @@ export function registerRoutes(
|
|
|
239
238
|
|
|
240
239
|
await ctx.runMutation(component.gateway.logAccess, {
|
|
241
240
|
agentId: authResult.agentId as never,
|
|
241
|
+
serviceId: headerValidation.serviceId,
|
|
242
242
|
functionKey,
|
|
243
243
|
args,
|
|
244
244
|
result,
|
|
@@ -255,6 +255,7 @@ export function registerRoutes(
|
|
|
255
255
|
|
|
256
256
|
await ctx.runMutation(component.gateway.logAccess, {
|
|
257
257
|
agentId: authResult.agentId as never,
|
|
258
|
+
serviceId: headerValidation.serviceId,
|
|
258
259
|
functionKey,
|
|
259
260
|
args: body.args ?? {},
|
|
260
261
|
error: errorMessage,
|
|
@@ -346,54 +347,71 @@ function jsonResponse(data: unknown, status: number): Response {
|
|
|
346
347
|
});
|
|
347
348
|
}
|
|
348
349
|
|
|
349
|
-
|
|
350
|
+
function validateStrictServiceHeaders(args: {
|
|
350
351
|
request: Request;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
352
|
+
configuredServiceKeys:
|
|
353
|
+
| { ok: true; keysByServiceId: Record<string, string> }
|
|
354
|
+
| { ok: false; error: string };
|
|
355
|
+
}):
|
|
356
|
+
| { valid: true; serviceId: string; appKey: string }
|
|
357
|
+
| { valid: false; error: string; statusCode: number } {
|
|
358
|
+
const serviceId = args.request.headers.get("X-Agent-Service-Id")?.trim();
|
|
359
|
+
const providedServiceKey = args.request.headers
|
|
360
|
+
.get("X-Agent-Service-Key")
|
|
361
|
+
?.trim();
|
|
362
|
+
const appKey = args.request.headers.get("X-Agent-App")?.trim();
|
|
363
|
+
|
|
364
|
+
if (!serviceId) {
|
|
365
|
+
return {
|
|
366
|
+
valid: false,
|
|
367
|
+
error: "Missing required header: X-Agent-Service-Id",
|
|
368
|
+
statusCode: 400,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
367
371
|
if (!providedServiceKey) {
|
|
368
372
|
return {
|
|
369
|
-
|
|
370
|
-
error: "Missing
|
|
371
|
-
statusCode:
|
|
373
|
+
valid: false,
|
|
374
|
+
error: "Missing required header: X-Agent-Service-Key",
|
|
375
|
+
statusCode: 400,
|
|
372
376
|
};
|
|
373
377
|
}
|
|
374
378
|
if (!appKey) {
|
|
375
379
|
return {
|
|
376
|
-
|
|
377
|
-
error: "Missing
|
|
380
|
+
valid: false,
|
|
381
|
+
error: "Missing required header: X-Agent-App",
|
|
378
382
|
statusCode: 400,
|
|
379
383
|
};
|
|
380
384
|
}
|
|
381
|
-
|
|
385
|
+
|
|
386
|
+
if (!args.configuredServiceKeys.ok) {
|
|
382
387
|
return {
|
|
383
|
-
|
|
384
|
-
error:
|
|
385
|
-
"Bridge service key is not configured. Set AGENT_BRIDGE_SERVICE_KEY or pass registerRoutes({ serviceKey })",
|
|
388
|
+
valid: false,
|
|
389
|
+
error: args.configuredServiceKeys.error,
|
|
386
390
|
statusCode: 500,
|
|
387
391
|
};
|
|
388
392
|
}
|
|
389
|
-
|
|
393
|
+
|
|
394
|
+
const expectedServiceKey = args.configuredServiceKeys.keysByServiceId[serviceId];
|
|
395
|
+
if (!expectedServiceKey) {
|
|
396
|
+
return {
|
|
397
|
+
valid: false,
|
|
398
|
+
error: `Unknown service id: ${serviceId}`,
|
|
399
|
+
statusCode: 401,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (providedServiceKey !== expectedServiceKey) {
|
|
390
403
|
return {
|
|
391
|
-
|
|
404
|
+
valid: false,
|
|
392
405
|
error: "Invalid service key",
|
|
393
406
|
statusCode: 401,
|
|
394
407
|
};
|
|
395
408
|
}
|
|
396
|
-
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
valid: true,
|
|
412
|
+
serviceId,
|
|
413
|
+
appKey,
|
|
414
|
+
};
|
|
397
415
|
}
|
|
398
416
|
|
|
399
417
|
function readRuntimeEnv(name: string): string | undefined {
|
|
@@ -406,3 +424,73 @@ function readRuntimeEnv(name: string): string | undefined {
|
|
|
406
424
|
const trimmed = value.trim();
|
|
407
425
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
408
426
|
}
|
|
427
|
+
|
|
428
|
+
function resolveConfiguredServiceKeys(args: {
|
|
429
|
+
serviceKeys?: Record<string, string>;
|
|
430
|
+
serviceKeysEnvVar: string;
|
|
431
|
+
}):
|
|
432
|
+
| { ok: true; keysByServiceId: Record<string, string> }
|
|
433
|
+
| { ok: false; error: string } {
|
|
434
|
+
if (args.serviceKeys) {
|
|
435
|
+
return sanitizeServiceKeysMap(args.serviceKeys);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const json = readRuntimeEnv(args.serviceKeysEnvVar);
|
|
439
|
+
if (!json) {
|
|
440
|
+
return {
|
|
441
|
+
ok: false,
|
|
442
|
+
error: `Bridge service keys are not configured. Provide registerRoutes({ serviceKeys }) or set ${args.serviceKeysEnvVar}`,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let parsed: unknown;
|
|
447
|
+
try {
|
|
448
|
+
parsed = JSON.parse(json);
|
|
449
|
+
} catch {
|
|
450
|
+
return {
|
|
451
|
+
ok: false,
|
|
452
|
+
error: `Invalid JSON in ${args.serviceKeysEnvVar}`,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
457
|
+
return {
|
|
458
|
+
ok: false,
|
|
459
|
+
error: `${args.serviceKeysEnvVar} must be a JSON object mapping serviceId to serviceKey`,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return sanitizeServiceKeysMap(parsed as Record<string, unknown>);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function sanitizeServiceKeysMap(
|
|
467
|
+
input: Record<string, unknown>,
|
|
468
|
+
): { ok: true; keysByServiceId: Record<string, string> } | { ok: false; error: string } {
|
|
469
|
+
const keysByServiceId: Record<string, string> = {};
|
|
470
|
+
for (const [serviceIdRaw, serviceKeyRaw] of Object.entries(input)) {
|
|
471
|
+
if (typeof serviceKeyRaw !== "string") {
|
|
472
|
+
return {
|
|
473
|
+
ok: false,
|
|
474
|
+
error: `Invalid service key value for "${serviceIdRaw}"`,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
const serviceId = serviceIdRaw.trim();
|
|
478
|
+
const serviceKey = serviceKeyRaw.trim();
|
|
479
|
+
if (!serviceId || !serviceKey) {
|
|
480
|
+
return {
|
|
481
|
+
ok: false,
|
|
482
|
+
error: "Service ids and service keys cannot be empty",
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
keysByServiceId[serviceId] = serviceKey;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (Object.keys(keysByServiceId).length === 0) {
|
|
489
|
+
return {
|
|
490
|
+
ok: false,
|
|
491
|
+
error: "At least one service key must be configured",
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return { ok: true, keysByServiceId };
|
|
496
|
+
}
|