@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.
@@ -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", "appKey" | "enabled" | "name" | "rateLimit" | "apiKeyHash" | "lastUsed" | "createdAt">, {
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
- error?: string | undefined;
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" | "duration" | "error" | "result" | "timestamp" | `args.${string}` | `result.${string}`>, {
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,wBAiDG"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/component/schema.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,wBAmDG"}
@@ -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
@@ -9,7 +9,7 @@
9
9
  "bugs": {
10
10
  "url": "https://github.com/okrlinkhub/agent-bridge/issues"
11
11
  },
12
- "version": "2.0.1",
12
+ "version": "3.0.0",
13
13
  "license": "Apache-2.0",
14
14
  "keywords": [
15
15
  "convex",
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
- serviceKey:
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) For multi-app OpenClaw deployments, set one service secret in Railway:
98
- - AGENT_BRIDGE_SERVICE_KEY=<shared-secret>
99
- - OpenClaw sends X-Agent-Service-Key and X-Agent-App headers
100
- - Bridge resolves app -> agent internally (keep legacy X-Agent-API-Key for compatibility)
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() {
@@ -7,7 +7,10 @@ import {
7
7
 
8
8
  function setupExecuteHandler(
9
9
  config: AgentBridgeConfig,
10
- options?: { serviceKey?: string },
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
- serviceKey: options?.serviceKey,
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 modes", () => {
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("uses legacy api key flow when X-Agent-API-Key is present", async () => {
59
- const executeHandler = setupExecuteHandler(config, { serviceKey: "svc_key" });
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-API-Key": "legacy_key",
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, "authorizeRequestRef", {
86
- apiKey: "legacy_key",
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 401 when service key is invalid", async () => {
93
- const executeHandler = setupExecuteHandler(config, { serviceKey: "svc_key" });
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": "wrong_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(401);
133
+ expect(response.status).toBe(400);
113
134
  expect(runMutation).not.toHaveBeenCalled();
114
135
  });
115
136
 
116
- test("returns app-level authorization error when app is missing", async () => {
117
- const executeHandler = setupExecuteHandler(config, { serviceKey: "svc_key" });
118
- const runMutation = vi.fn().mockResolvedValue({
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-Key": "svc_key",
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(404);
141
- expect(runMutation).toHaveBeenCalledWith("authorizeByAppKeyRef", {
142
- appKey: "crm",
143
- functionKey: "demo.list",
144
- estimatedCost: undefined,
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
  });
@@ -136,8 +136,8 @@ type ExecuteRequestBody = {
136
136
 
137
137
  type RegisterRoutesOptions = {
138
138
  pathPrefix?: string;
139
- serviceKey?: string;
140
- serviceKeyEnvVar?: string;
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 expectedServiceKey =
151
- options?.serviceKey ??
152
- readRuntimeEnv(options?.serviceKeyEnvVar ?? "AGENT_BRIDGE_SERVICE_KEY");
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 authResult = apiKey
189
- ? await ctx.runMutation(component.gateway.authorizeRequest, {
190
- apiKey,
191
- functionKey,
192
- estimatedCost: body.estimatedCost,
193
- })
194
- : await authorizeWithServiceHeaders({
195
- request,
196
- expectedServiceKey,
197
- authorizeByAppKey: (appKey) =>
198
- ctx.runMutation(component.gateway.authorizeByAppKey, {
199
- appKey,
200
- functionKey,
201
- estimatedCost: body.estimatedCost,
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
- async function authorizeWithServiceHeaders(args: {
350
+ function validateStrictServiceHeaders(args: {
350
351
  request: Request;
351
- expectedServiceKey?: string;
352
- authorizeByAppKey: (
353
- appKey: string,
354
- ) => Promise<
355
- | { authorized: true; agentId: string }
356
- | {
357
- authorized: false;
358
- error: string;
359
- statusCode: number;
360
- agentId?: string;
361
- retryAfterSeconds?: number;
362
- }
363
- >;
364
- }) {
365
- const providedServiceKey = args.request.headers.get("X-Agent-Service-Key");
366
- const appKey = args.request.headers.get("X-Agent-App");
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
- authorized: false as const,
370
- error: "Missing authentication header: X-Agent-Service-Key",
371
- statusCode: 401,
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
- authorized: false as const,
377
- error: "Missing routing header: X-Agent-App",
380
+ valid: false,
381
+ error: "Missing required header: X-Agent-App",
378
382
  statusCode: 400,
379
383
  };
380
384
  }
381
- if (!args.expectedServiceKey) {
385
+
386
+ if (!args.configuredServiceKeys.ok) {
382
387
  return {
383
- authorized: false as const,
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
- if (providedServiceKey !== args.expectedServiceKey) {
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
- authorized: false as const,
404
+ valid: false,
392
405
  error: "Invalid service key",
393
406
  statusCode: 401,
394
407
  };
395
408
  }
396
- return await args.authorizeByAppKey(appKey);
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
+ }