@okrlinkhub/agent-bridge 2.0.1 → 3.0.1
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 +79 -15
- package/dist/cli/init.js +5 -7
- package/dist/cli/init.js.map +1 -1
- package/dist/client/index.d.ts +4 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +111 -32
- package/dist/client/index.js.map +1 -1
- package/dist/client/userAuth.d.ts +47 -0
- package/dist/client/userAuth.d.ts.map +1 -0
- package/dist/client/userAuth.js +122 -0
- package/dist/client/userAuth.js.map +1 -0
- package/dist/component/_generated/component.d.ts +3 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- 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/schema.d.ts +4 -1
- 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 +158 -52
- package/src/client/userAuth.test.ts +77 -0
- package/src/client/userAuth.ts +192 -0
- package/src/component/_generated/component.ts +8 -1
- package/src/component/gateway.ts +33 -0
- package/src/component/schema.ts +2 -0
package/src/client/index.ts
CHANGED
|
@@ -6,6 +6,24 @@ import type {
|
|
|
6
6
|
HttpRouter,
|
|
7
7
|
} from "convex/server";
|
|
8
8
|
import type { ComponentApi } from "../component/_generated/component.js";
|
|
9
|
+
export {
|
|
10
|
+
buildAgentBridgeStrictHeaders,
|
|
11
|
+
createAuth0TokenAdapter,
|
|
12
|
+
createCustomOidcTokenAdapter,
|
|
13
|
+
createNextAuthConvexTokenAdapter,
|
|
14
|
+
decodeJwtClaims,
|
|
15
|
+
resolveUserToken,
|
|
16
|
+
validateJwtClaims,
|
|
17
|
+
} from "./userAuth.js";
|
|
18
|
+
export type {
|
|
19
|
+
AgentBridgeStrictHeadersInput,
|
|
20
|
+
JwtClaimValidationOptions,
|
|
21
|
+
JwtClaimValidationResult,
|
|
22
|
+
JwtClaims,
|
|
23
|
+
NextAuthSessionLike,
|
|
24
|
+
TokenSource,
|
|
25
|
+
TokenSourceAdapter,
|
|
26
|
+
} from "./userAuth.js";
|
|
9
27
|
|
|
10
28
|
export type AgentBridgeFunctionType = "query" | "mutation" | "action";
|
|
11
29
|
|
|
@@ -136,8 +154,8 @@ type ExecuteRequestBody = {
|
|
|
136
154
|
|
|
137
155
|
type RegisterRoutesOptions = {
|
|
138
156
|
pathPrefix?: string;
|
|
139
|
-
|
|
140
|
-
|
|
157
|
+
serviceKeys?: Record<string, string>;
|
|
158
|
+
serviceKeysEnvVar?: string;
|
|
141
159
|
};
|
|
142
160
|
|
|
143
161
|
export function registerRoutes(
|
|
@@ -147,9 +165,10 @@ export function registerRoutes(
|
|
|
147
165
|
options?: RegisterRoutesOptions,
|
|
148
166
|
) {
|
|
149
167
|
const prefix = options?.pathPrefix ?? "/agent";
|
|
150
|
-
const
|
|
151
|
-
options?.
|
|
152
|
-
|
|
168
|
+
const configuredServiceKeys = resolveConfiguredServiceKeys({
|
|
169
|
+
serviceKeys: options?.serviceKeys,
|
|
170
|
+
serviceKeysEnvVar: options?.serviceKeysEnvVar ?? "AGENT_BRIDGE_SERVICE_KEYS_JSON",
|
|
171
|
+
});
|
|
153
172
|
const normalizedConfig = normalizeAgentBridgeConfig(bridgeConfig);
|
|
154
173
|
const availableFunctionKeys = Object.keys(normalizedConfig.functions);
|
|
155
174
|
|
|
@@ -157,8 +176,6 @@ export function registerRoutes(
|
|
|
157
176
|
path: `${prefix}/execute`,
|
|
158
177
|
method: "POST",
|
|
159
178
|
handler: httpActionGeneric(async (ctx, request) => {
|
|
160
|
-
const apiKey = request.headers.get("X-Agent-API-Key");
|
|
161
|
-
|
|
162
179
|
let body: ExecuteRequestBody;
|
|
163
180
|
try {
|
|
164
181
|
body = await request.json();
|
|
@@ -185,22 +202,22 @@ export function registerRoutes(
|
|
|
185
202
|
);
|
|
186
203
|
}
|
|
187
204
|
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
const headerValidation = validateStrictServiceHeaders({
|
|
206
|
+
request,
|
|
207
|
+
configuredServiceKeys,
|
|
208
|
+
});
|
|
209
|
+
if (!headerValidation.valid) {
|
|
210
|
+
return jsonResponse(
|
|
211
|
+
{ success: false, error: headerValidation.error },
|
|
212
|
+
headerValidation.statusCode,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const authResult = await ctx.runMutation(component.gateway.authorizeByAppKey, {
|
|
217
|
+
appKey: headerValidation.appKey,
|
|
218
|
+
functionKey,
|
|
219
|
+
estimatedCost: body.estimatedCost,
|
|
220
|
+
});
|
|
204
221
|
|
|
205
222
|
if (!authResult.authorized) {
|
|
206
223
|
const response = jsonResponse(
|
|
@@ -239,6 +256,7 @@ export function registerRoutes(
|
|
|
239
256
|
|
|
240
257
|
await ctx.runMutation(component.gateway.logAccess, {
|
|
241
258
|
agentId: authResult.agentId as never,
|
|
259
|
+
serviceId: headerValidation.serviceId,
|
|
242
260
|
functionKey,
|
|
243
261
|
args,
|
|
244
262
|
result,
|
|
@@ -255,6 +273,7 @@ export function registerRoutes(
|
|
|
255
273
|
|
|
256
274
|
await ctx.runMutation(component.gateway.logAccess, {
|
|
257
275
|
agentId: authResult.agentId as never,
|
|
276
|
+
serviceId: headerValidation.serviceId,
|
|
258
277
|
functionKey,
|
|
259
278
|
args: body.args ?? {},
|
|
260
279
|
error: errorMessage,
|
|
@@ -346,54 +365,71 @@ function jsonResponse(data: unknown, status: number): Response {
|
|
|
346
365
|
});
|
|
347
366
|
}
|
|
348
367
|
|
|
349
|
-
|
|
368
|
+
function validateStrictServiceHeaders(args: {
|
|
350
369
|
request: Request;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
370
|
+
configuredServiceKeys:
|
|
371
|
+
| { ok: true; keysByServiceId: Record<string, string> }
|
|
372
|
+
| { ok: false; error: string };
|
|
373
|
+
}):
|
|
374
|
+
| { valid: true; serviceId: string; appKey: string }
|
|
375
|
+
| { valid: false; error: string; statusCode: number } {
|
|
376
|
+
const serviceId = args.request.headers.get("X-Agent-Service-Id")?.trim();
|
|
377
|
+
const providedServiceKey = args.request.headers
|
|
378
|
+
.get("X-Agent-Service-Key")
|
|
379
|
+
?.trim();
|
|
380
|
+
const appKey = args.request.headers.get("X-Agent-App")?.trim();
|
|
381
|
+
|
|
382
|
+
if (!serviceId) {
|
|
383
|
+
return {
|
|
384
|
+
valid: false,
|
|
385
|
+
error: "Missing required header: X-Agent-Service-Id",
|
|
386
|
+
statusCode: 400,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
367
389
|
if (!providedServiceKey) {
|
|
368
390
|
return {
|
|
369
|
-
|
|
370
|
-
error: "Missing
|
|
371
|
-
statusCode:
|
|
391
|
+
valid: false,
|
|
392
|
+
error: "Missing required header: X-Agent-Service-Key",
|
|
393
|
+
statusCode: 400,
|
|
372
394
|
};
|
|
373
395
|
}
|
|
374
396
|
if (!appKey) {
|
|
375
397
|
return {
|
|
376
|
-
|
|
377
|
-
error: "Missing
|
|
398
|
+
valid: false,
|
|
399
|
+
error: "Missing required header: X-Agent-App",
|
|
378
400
|
statusCode: 400,
|
|
379
401
|
};
|
|
380
402
|
}
|
|
381
|
-
|
|
403
|
+
|
|
404
|
+
if (!args.configuredServiceKeys.ok) {
|
|
382
405
|
return {
|
|
383
|
-
|
|
384
|
-
error:
|
|
385
|
-
"Bridge service key is not configured. Set AGENT_BRIDGE_SERVICE_KEY or pass registerRoutes({ serviceKey })",
|
|
406
|
+
valid: false,
|
|
407
|
+
error: args.configuredServiceKeys.error,
|
|
386
408
|
statusCode: 500,
|
|
387
409
|
};
|
|
388
410
|
}
|
|
389
|
-
|
|
411
|
+
|
|
412
|
+
const expectedServiceKey = args.configuredServiceKeys.keysByServiceId[serviceId];
|
|
413
|
+
if (!expectedServiceKey) {
|
|
414
|
+
return {
|
|
415
|
+
valid: false,
|
|
416
|
+
error: `Unknown service id: ${serviceId}`,
|
|
417
|
+
statusCode: 401,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (providedServiceKey !== expectedServiceKey) {
|
|
390
421
|
return {
|
|
391
|
-
|
|
422
|
+
valid: false,
|
|
392
423
|
error: "Invalid service key",
|
|
393
424
|
statusCode: 401,
|
|
394
425
|
};
|
|
395
426
|
}
|
|
396
|
-
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
valid: true,
|
|
430
|
+
serviceId,
|
|
431
|
+
appKey,
|
|
432
|
+
};
|
|
397
433
|
}
|
|
398
434
|
|
|
399
435
|
function readRuntimeEnv(name: string): string | undefined {
|
|
@@ -406,3 +442,73 @@ function readRuntimeEnv(name: string): string | undefined {
|
|
|
406
442
|
const trimmed = value.trim();
|
|
407
443
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
408
444
|
}
|
|
445
|
+
|
|
446
|
+
function resolveConfiguredServiceKeys(args: {
|
|
447
|
+
serviceKeys?: Record<string, string>;
|
|
448
|
+
serviceKeysEnvVar: string;
|
|
449
|
+
}):
|
|
450
|
+
| { ok: true; keysByServiceId: Record<string, string> }
|
|
451
|
+
| { ok: false; error: string } {
|
|
452
|
+
if (args.serviceKeys) {
|
|
453
|
+
return sanitizeServiceKeysMap(args.serviceKeys);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const json = readRuntimeEnv(args.serviceKeysEnvVar);
|
|
457
|
+
if (!json) {
|
|
458
|
+
return {
|
|
459
|
+
ok: false,
|
|
460
|
+
error: `Bridge service keys are not configured. Provide registerRoutes({ serviceKeys }) or set ${args.serviceKeysEnvVar}`,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
let parsed: unknown;
|
|
465
|
+
try {
|
|
466
|
+
parsed = JSON.parse(json);
|
|
467
|
+
} catch {
|
|
468
|
+
return {
|
|
469
|
+
ok: false,
|
|
470
|
+
error: `Invalid JSON in ${args.serviceKeysEnvVar}`,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
475
|
+
return {
|
|
476
|
+
ok: false,
|
|
477
|
+
error: `${args.serviceKeysEnvVar} must be a JSON object mapping serviceId to serviceKey`,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return sanitizeServiceKeysMap(parsed as Record<string, unknown>);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function sanitizeServiceKeysMap(
|
|
485
|
+
input: Record<string, unknown>,
|
|
486
|
+
): { ok: true; keysByServiceId: Record<string, string> } | { ok: false; error: string } {
|
|
487
|
+
const keysByServiceId: Record<string, string> = {};
|
|
488
|
+
for (const [serviceIdRaw, serviceKeyRaw] of Object.entries(input)) {
|
|
489
|
+
if (typeof serviceKeyRaw !== "string") {
|
|
490
|
+
return {
|
|
491
|
+
ok: false,
|
|
492
|
+
error: `Invalid service key value for "${serviceIdRaw}"`,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
const serviceId = serviceIdRaw.trim();
|
|
496
|
+
const serviceKey = serviceKeyRaw.trim();
|
|
497
|
+
if (!serviceId || !serviceKey) {
|
|
498
|
+
return {
|
|
499
|
+
ok: false,
|
|
500
|
+
error: "Service ids and service keys cannot be empty",
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
keysByServiceId[serviceId] = serviceKey;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (Object.keys(keysByServiceId).length === 0) {
|
|
507
|
+
return {
|
|
508
|
+
ok: false,
|
|
509
|
+
error: "At least one service key must be configured",
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return { ok: true, keysByServiceId };
|
|
514
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildAgentBridgeStrictHeaders,
|
|
4
|
+
createAuth0TokenAdapter,
|
|
5
|
+
createCustomOidcTokenAdapter,
|
|
6
|
+
createNextAuthConvexTokenAdapter,
|
|
7
|
+
decodeJwtClaims,
|
|
8
|
+
resolveUserToken,
|
|
9
|
+
validateJwtClaims,
|
|
10
|
+
} from "./userAuth.js";
|
|
11
|
+
|
|
12
|
+
const TEST_JWT =
|
|
13
|
+
"eyJhbGciOiJub25lIn0.eyJzdWIiOiJ1c2VyXzEyMyIsImlzcyI6Imh0dHBzOi8vZGVtby5jb252ZXguc2l0ZSIsImF1ZCI6ImNvbnZleCIsImV4cCI6NDA3MDkwODgwMH0.";
|
|
14
|
+
|
|
15
|
+
describe("user auth helpers", () => {
|
|
16
|
+
test("builds strict headers and includes bearer when provided", () => {
|
|
17
|
+
const headers = buildAgentBridgeStrictHeaders({
|
|
18
|
+
serviceId: "openclaw-prod",
|
|
19
|
+
serviceKey: "abs_live_example",
|
|
20
|
+
appKey: "crm",
|
|
21
|
+
userToken: "jwt_token",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(headers["X-Agent-Service-Id"]).toBe("openclaw-prod");
|
|
25
|
+
expect(headers["X-Agent-Service-Key"]).toBe("abs_live_example");
|
|
26
|
+
expect(headers["X-Agent-App"]).toBe("crm");
|
|
27
|
+
expect(headers.Authorization).toBe("Bearer jwt_token");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("decodes jwt claims", () => {
|
|
31
|
+
const claims = decodeJwtClaims(TEST_JWT);
|
|
32
|
+
expect(claims?.sub).toBe("user_123");
|
|
33
|
+
expect(claims?.iss).toBe("https://demo.convex.site");
|
|
34
|
+
expect(claims?.aud).toBe("convex");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("validates jwt claims with issuer and audience", () => {
|
|
38
|
+
const validation = validateJwtClaims(TEST_JWT, {
|
|
39
|
+
expectedIssuer: "https://demo.convex.site",
|
|
40
|
+
expectedAudience: "convex",
|
|
41
|
+
nowMs: Date.UTC(2026, 0, 1),
|
|
42
|
+
});
|
|
43
|
+
expect(validation.valid).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("fails when jwt is expired", () => {
|
|
47
|
+
const expired = validateJwtClaims(TEST_JWT, {
|
|
48
|
+
nowMs: Date.UTC(2100, 0, 1),
|
|
49
|
+
});
|
|
50
|
+
expect(expired.valid).toBe(false);
|
|
51
|
+
expect(expired.reason).toBe("expired");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("resolves nextauth convex token", async () => {
|
|
55
|
+
const adapter = createNextAuthConvexTokenAdapter({
|
|
56
|
+
getSession: async () => ({ convexToken: "convex_jwt" }),
|
|
57
|
+
});
|
|
58
|
+
const token = await resolveUserToken(adapter);
|
|
59
|
+
expect(token).toBe("convex_jwt");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("resolves auth0 token", async () => {
|
|
63
|
+
const adapter = createAuth0TokenAdapter({
|
|
64
|
+
getAccessToken: async () => "auth0_jwt",
|
|
65
|
+
});
|
|
66
|
+
const token = await resolveUserToken(adapter);
|
|
67
|
+
expect(token).toBe("auth0_jwt");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("resolves custom oidc token", async () => {
|
|
71
|
+
const adapter = createCustomOidcTokenAdapter({
|
|
72
|
+
getToken: async () => "oidc_jwt",
|
|
73
|
+
});
|
|
74
|
+
const token = await resolveUserToken(adapter);
|
|
75
|
+
expect(token).toBe("oidc_jwt");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
export type TokenSource = "nextauth_convex" | "auth0" | "custom_oidc";
|
|
2
|
+
|
|
3
|
+
export type JwtAudience = string | Array<string>;
|
|
4
|
+
|
|
5
|
+
export interface JwtClaims {
|
|
6
|
+
sub?: string;
|
|
7
|
+
iss?: string;
|
|
8
|
+
aud?: JwtAudience;
|
|
9
|
+
exp?: number;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface JwtClaimValidationOptions {
|
|
14
|
+
expectedIssuer?: string;
|
|
15
|
+
expectedAudience?: string;
|
|
16
|
+
nowMs?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface JwtClaimValidationResult {
|
|
20
|
+
valid: boolean;
|
|
21
|
+
reason?: "malformed_token" | "expired" | "issuer_mismatch" | "audience_mismatch";
|
|
22
|
+
claims: JwtClaims | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AgentBridgeStrictHeadersInput {
|
|
26
|
+
serviceId: string;
|
|
27
|
+
serviceKey: string;
|
|
28
|
+
appKey: string;
|
|
29
|
+
userToken?: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface NextAuthSessionLike {
|
|
33
|
+
convexToken?: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type UserTokenResolver = () => Promise<string | null>;
|
|
37
|
+
|
|
38
|
+
export interface TokenSourceAdapter {
|
|
39
|
+
tokenSource: TokenSource;
|
|
40
|
+
resolveUserToken: UserTokenResolver;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildAgentBridgeStrictHeaders(
|
|
44
|
+
input: AgentBridgeStrictHeadersInput,
|
|
45
|
+
): Record<string, string> {
|
|
46
|
+
const headers: Record<string, string> = {
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
"X-Agent-Service-Id": input.serviceId,
|
|
49
|
+
"X-Agent-Service-Key": input.serviceKey,
|
|
50
|
+
"X-Agent-App": input.appKey,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (input.userToken) {
|
|
54
|
+
headers.Authorization = `Bearer ${input.userToken}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return headers;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function decodeJwtClaims(token: string): JwtClaims | null {
|
|
61
|
+
const parts = token.split(".");
|
|
62
|
+
if (parts.length !== 3 || !parts[1]) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const payload = decodeBase64Url(parts[1]);
|
|
68
|
+
const claims = JSON.parse(payload) as JwtClaims;
|
|
69
|
+
return claims;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function validateJwtClaims(
|
|
76
|
+
token: string,
|
|
77
|
+
options?: JwtClaimValidationOptions,
|
|
78
|
+
): JwtClaimValidationResult {
|
|
79
|
+
const claims = decodeJwtClaims(token);
|
|
80
|
+
if (!claims) {
|
|
81
|
+
return {
|
|
82
|
+
valid: false,
|
|
83
|
+
reason: "malformed_token",
|
|
84
|
+
claims: null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const nowMs = options?.nowMs ?? Date.now();
|
|
89
|
+
if (typeof claims.exp === "number") {
|
|
90
|
+
const expiresAtMs = claims.exp * 1000;
|
|
91
|
+
if (expiresAtMs <= nowMs) {
|
|
92
|
+
return {
|
|
93
|
+
valid: false,
|
|
94
|
+
reason: "expired",
|
|
95
|
+
claims,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (options?.expectedIssuer && claims.iss !== options.expectedIssuer) {
|
|
101
|
+
return {
|
|
102
|
+
valid: false,
|
|
103
|
+
reason: "issuer_mismatch",
|
|
104
|
+
claims,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (options?.expectedAudience) {
|
|
109
|
+
const audience = claims.aud;
|
|
110
|
+
const hasAudienceMatch = Array.isArray(audience)
|
|
111
|
+
? audience.includes(options.expectedAudience)
|
|
112
|
+
: audience === options.expectedAudience;
|
|
113
|
+
|
|
114
|
+
if (!hasAudienceMatch) {
|
|
115
|
+
return {
|
|
116
|
+
valid: false,
|
|
117
|
+
reason: "audience_mismatch",
|
|
118
|
+
claims,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
valid: true,
|
|
125
|
+
claims,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function createNextAuthConvexTokenAdapter(args: {
|
|
130
|
+
getSession: () => Promise<NextAuthSessionLike | null | undefined>;
|
|
131
|
+
}): TokenSourceAdapter {
|
|
132
|
+
return {
|
|
133
|
+
tokenSource: "nextauth_convex",
|
|
134
|
+
resolveUserToken: async () => {
|
|
135
|
+
const session = await args.getSession();
|
|
136
|
+
const token = session?.convexToken;
|
|
137
|
+
if (typeof token !== "string" || token.trim().length === 0) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return token;
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function createAuth0TokenAdapter(args: {
|
|
146
|
+
getAccessToken: () => Promise<string | null | undefined>;
|
|
147
|
+
}): TokenSourceAdapter {
|
|
148
|
+
return {
|
|
149
|
+
tokenSource: "auth0",
|
|
150
|
+
resolveUserToken: async () => {
|
|
151
|
+
const token = await args.getAccessToken();
|
|
152
|
+
if (typeof token !== "string" || token.trim().length === 0) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
return token;
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function createCustomOidcTokenAdapter(args: {
|
|
161
|
+
getToken: () => Promise<string | null | undefined>;
|
|
162
|
+
}): TokenSourceAdapter {
|
|
163
|
+
return {
|
|
164
|
+
tokenSource: "custom_oidc",
|
|
165
|
+
resolveUserToken: async () => {
|
|
166
|
+
const token = await args.getToken();
|
|
167
|
+
if (typeof token !== "string" || token.trim().length === 0) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return token;
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function resolveUserToken(
|
|
176
|
+
adapter: TokenSourceAdapter,
|
|
177
|
+
): Promise<string | null> {
|
|
178
|
+
return await adapter.resolveUserToken();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function decodeBase64Url(value: string): string {
|
|
182
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
183
|
+
const paddingLength = (4 - (normalized.length % 4)) % 4;
|
|
184
|
+
const padded = normalized + "=".repeat(paddingLength);
|
|
185
|
+
|
|
186
|
+
if (typeof atob === "function") {
|
|
187
|
+
return atob(padded);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const buffer = Buffer.from(padded, "base64");
|
|
191
|
+
return buffer.toString("utf-8");
|
|
192
|
+
}
|
|
@@ -112,6 +112,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
112
112
|
error?: string;
|
|
113
113
|
functionKey: string;
|
|
114
114
|
result?: any;
|
|
115
|
+
serviceId?: string;
|
|
115
116
|
timestamp: number;
|
|
116
117
|
},
|
|
117
118
|
null,
|
|
@@ -120,7 +121,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
120
121
|
queryAccessLog: FunctionReference<
|
|
121
122
|
"query",
|
|
122
123
|
"internal",
|
|
123
|
-
{
|
|
124
|
+
{
|
|
125
|
+
agentId?: string;
|
|
126
|
+
functionKey?: string;
|
|
127
|
+
limit?: number;
|
|
128
|
+
serviceId?: string;
|
|
129
|
+
},
|
|
124
130
|
Array<{
|
|
125
131
|
_id: string;
|
|
126
132
|
agentId: string;
|
|
@@ -129,6 +135,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
129
135
|
error?: string;
|
|
130
136
|
functionKey: string;
|
|
131
137
|
result?: any;
|
|
138
|
+
serviceId?: string;
|
|
132
139
|
timestamp: number;
|
|
133
140
|
}>,
|
|
134
141
|
Name
|
package/src/component/gateway.ts
CHANGED
|
@@ -100,6 +100,7 @@ export const authorizeByAppKey = mutation({
|
|
|
100
100
|
export const logAccess = mutation({
|
|
101
101
|
args: {
|
|
102
102
|
agentId: v.id("agents"),
|
|
103
|
+
serviceId: v.optional(v.string()),
|
|
103
104
|
functionKey: v.string(),
|
|
104
105
|
args: v.any(),
|
|
105
106
|
result: v.optional(v.any()),
|
|
@@ -112,6 +113,7 @@ export const logAccess = mutation({
|
|
|
112
113
|
await ctx.db.insert("agentLogs", {
|
|
113
114
|
timestamp: args.timestamp,
|
|
114
115
|
agentId: args.agentId,
|
|
116
|
+
serviceId: args.serviceId,
|
|
115
117
|
functionKey: args.functionKey,
|
|
116
118
|
args: args.args,
|
|
117
119
|
result: args.result,
|
|
@@ -128,6 +130,7 @@ export const logAccess = mutation({
|
|
|
128
130
|
export const queryAccessLog = query({
|
|
129
131
|
args: {
|
|
130
132
|
agentId: v.optional(v.id("agents")),
|
|
133
|
+
serviceId: v.optional(v.string()),
|
|
131
134
|
functionKey: v.optional(v.string()),
|
|
132
135
|
limit: v.optional(v.number()),
|
|
133
136
|
},
|
|
@@ -136,6 +139,7 @@ export const queryAccessLog = query({
|
|
|
136
139
|
_id: v.id("agentLogs"),
|
|
137
140
|
timestamp: v.number(),
|
|
138
141
|
agentId: v.id("agents"),
|
|
142
|
+
serviceId: v.optional(v.string()),
|
|
139
143
|
functionKey: v.string(),
|
|
140
144
|
args: v.any(),
|
|
141
145
|
result: v.optional(v.any()),
|
|
@@ -146,6 +150,33 @@ export const queryAccessLog = query({
|
|
|
146
150
|
handler: async (ctx, args) => {
|
|
147
151
|
const limit = args.limit ?? 50;
|
|
148
152
|
|
|
153
|
+
if (args.serviceId !== undefined) {
|
|
154
|
+
const logs = await ctx.db
|
|
155
|
+
.query("agentLogs")
|
|
156
|
+
.withIndex("by_serviceId_and_timestamp", (q) =>
|
|
157
|
+
q.eq("serviceId", args.serviceId),
|
|
158
|
+
)
|
|
159
|
+
.order("desc")
|
|
160
|
+
.take(limit);
|
|
161
|
+
|
|
162
|
+
const filteredLogs =
|
|
163
|
+
args.functionKey !== undefined
|
|
164
|
+
? logs.filter((log) => log.functionKey === args.functionKey)
|
|
165
|
+
: logs;
|
|
166
|
+
|
|
167
|
+
return filteredLogs.map((l) => ({
|
|
168
|
+
_id: l._id,
|
|
169
|
+
timestamp: l.timestamp,
|
|
170
|
+
agentId: l.agentId,
|
|
171
|
+
serviceId: l.serviceId,
|
|
172
|
+
functionKey: l.functionKey,
|
|
173
|
+
args: l.args,
|
|
174
|
+
result: l.result,
|
|
175
|
+
error: l.error,
|
|
176
|
+
duration: l.duration,
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
|
|
149
180
|
const agentId = args.agentId;
|
|
150
181
|
if (agentId !== undefined) {
|
|
151
182
|
const logs = await ctx.db
|
|
@@ -160,6 +191,7 @@ export const queryAccessLog = query({
|
|
|
160
191
|
_id: l._id,
|
|
161
192
|
timestamp: l.timestamp,
|
|
162
193
|
agentId: l.agentId,
|
|
194
|
+
serviceId: l.serviceId,
|
|
163
195
|
functionKey: l.functionKey,
|
|
164
196
|
args: l.args,
|
|
165
197
|
result: l.result,
|
|
@@ -181,6 +213,7 @@ export const queryAccessLog = query({
|
|
|
181
213
|
_id: l._id,
|
|
182
214
|
timestamp: l.timestamp,
|
|
183
215
|
agentId: l.agentId,
|
|
216
|
+
serviceId: l.serviceId,
|
|
184
217
|
functionKey: l.functionKey,
|
|
185
218
|
args: l.args,
|
|
186
219
|
result: l.result,
|
package/src/component/schema.ts
CHANGED
|
@@ -40,6 +40,7 @@ export default defineSchema({
|
|
|
40
40
|
|
|
41
41
|
agentLogs: defineTable({
|
|
42
42
|
agentId: v.id("agents"),
|
|
43
|
+
serviceId: v.optional(v.string()),
|
|
43
44
|
functionKey: v.string(),
|
|
44
45
|
args: v.any(),
|
|
45
46
|
result: v.optional(v.any()),
|
|
@@ -48,6 +49,7 @@ export default defineSchema({
|
|
|
48
49
|
timestamp: v.number(),
|
|
49
50
|
})
|
|
50
51
|
.index("by_agentId_and_timestamp", ["agentId", "timestamp"])
|
|
52
|
+
.index("by_serviceId_and_timestamp", ["serviceId", "timestamp"])
|
|
51
53
|
.index("by_functionKey", ["functionKey"])
|
|
52
54
|
.index("by_timestamp", ["timestamp"]),
|
|
53
55
|
});
|