a2a-mesh 1.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/LICENSE +171 -0
- package/README.md +22 -0
- package/dist/auth/index.cjs +8 -0
- package/dist/auth/index.d.cts +2 -0
- package/dist/auth/index.d.mts +2 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.mjs +2 -0
- package/dist/index.cjs +1428 -0
- package/dist/index.d.cts +1589 -0
- package/dist/index.d.mts +1589 -0
- package/dist/index.d.ts +1589 -0
- package/dist/index.mjs +1392 -0
- package/dist/middleware/index.cjs +9 -0
- package/dist/middleware/index.d.cts +46 -0
- package/dist/middleware/index.d.mts +46 -0
- package/dist/middleware/index.d.ts +46 -0
- package/dist/middleware/index.mjs +1 -0
- package/dist/shared/a2a-mesh.B8CFEj_l.cjs +101 -0
- package/dist/shared/a2a-mesh.CDuHtAfx.d.cts +70 -0
- package/dist/shared/a2a-mesh.CDuHtAfx.d.mts +70 -0
- package/dist/shared/a2a-mesh.CDuHtAfx.d.ts +70 -0
- package/dist/shared/a2a-mesh.CpP0sKxA.mjs +95 -0
- package/dist/shared/a2a-mesh.DMPwsFk8.cjs +121 -0
- package/dist/shared/a2a-mesh.DOHcVRN8.mjs +119 -0
- package/dist/telemetry/index.cjs +27 -0
- package/dist/telemetry/index.d.cts +7 -0
- package/dist/telemetry/index.d.mts +7 -0
- package/dist/telemetry/index.d.ts +7 -0
- package/dist/telemetry/index.mjs +24 -0
- package/package.json +90 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const middleware_index = require('../shared/a2a-mesh.B8CFEj_l.cjs');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
exports.InMemoryRateLimitStore = middleware_index.InMemoryRateLimitStore;
|
|
8
|
+
exports.RedisRateLimitStore = middleware_index.RedisRateLimitStore;
|
|
9
|
+
exports.createRateLimiter = middleware_index.createRateLimiter;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file rateLimiter.ts
|
|
5
|
+
* Express middleware for per-client request throttling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface RateLimitConfig {
|
|
9
|
+
windowMs: number;
|
|
10
|
+
maxRequests: number;
|
|
11
|
+
keyGenerator?: (req: Request) => string;
|
|
12
|
+
}
|
|
13
|
+
interface RateLimitState {
|
|
14
|
+
count: number;
|
|
15
|
+
resetTime: number;
|
|
16
|
+
}
|
|
17
|
+
interface RateLimitStore {
|
|
18
|
+
increment(key: string, windowMs: number): Promise<RateLimitState>;
|
|
19
|
+
}
|
|
20
|
+
declare class InMemoryRateLimitStore implements RateLimitStore {
|
|
21
|
+
private readonly state;
|
|
22
|
+
increment(key: string, windowMs: number): Promise<RateLimitState>;
|
|
23
|
+
}
|
|
24
|
+
interface RedisRateLimitClient {
|
|
25
|
+
get(key: string): Promise<string | null>;
|
|
26
|
+
incr(key: string): Promise<number>;
|
|
27
|
+
pexpire(key: string, ttl: number): Promise<number>;
|
|
28
|
+
pttl(key: string): Promise<number>;
|
|
29
|
+
}
|
|
30
|
+
declare class RedisRateLimitStore implements RateLimitStore {
|
|
31
|
+
private readonly client;
|
|
32
|
+
constructor(client: RedisRateLimitClient);
|
|
33
|
+
increment(key: string, windowMs: number): Promise<RateLimitState>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create an Express middleware that enforces per-client request limits and emits JSON-RPC errors.
|
|
37
|
+
*
|
|
38
|
+
* @param config Partial limiter configuration.
|
|
39
|
+
* @param store Optional backing store implementation.
|
|
40
|
+
* @returns Express-compatible async middleware.
|
|
41
|
+
* @since 1.0.0
|
|
42
|
+
*/
|
|
43
|
+
declare function createRateLimiter(config?: Partial<RateLimitConfig>, store?: RateLimitStore): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
44
|
+
|
|
45
|
+
export { InMemoryRateLimitStore, RedisRateLimitStore, createRateLimiter };
|
|
46
|
+
export type { RateLimitConfig, RateLimitState, RateLimitStore, RedisRateLimitClient };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file rateLimiter.ts
|
|
5
|
+
* Express middleware for per-client request throttling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface RateLimitConfig {
|
|
9
|
+
windowMs: number;
|
|
10
|
+
maxRequests: number;
|
|
11
|
+
keyGenerator?: (req: Request) => string;
|
|
12
|
+
}
|
|
13
|
+
interface RateLimitState {
|
|
14
|
+
count: number;
|
|
15
|
+
resetTime: number;
|
|
16
|
+
}
|
|
17
|
+
interface RateLimitStore {
|
|
18
|
+
increment(key: string, windowMs: number): Promise<RateLimitState>;
|
|
19
|
+
}
|
|
20
|
+
declare class InMemoryRateLimitStore implements RateLimitStore {
|
|
21
|
+
private readonly state;
|
|
22
|
+
increment(key: string, windowMs: number): Promise<RateLimitState>;
|
|
23
|
+
}
|
|
24
|
+
interface RedisRateLimitClient {
|
|
25
|
+
get(key: string): Promise<string | null>;
|
|
26
|
+
incr(key: string): Promise<number>;
|
|
27
|
+
pexpire(key: string, ttl: number): Promise<number>;
|
|
28
|
+
pttl(key: string): Promise<number>;
|
|
29
|
+
}
|
|
30
|
+
declare class RedisRateLimitStore implements RateLimitStore {
|
|
31
|
+
private readonly client;
|
|
32
|
+
constructor(client: RedisRateLimitClient);
|
|
33
|
+
increment(key: string, windowMs: number): Promise<RateLimitState>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create an Express middleware that enforces per-client request limits and emits JSON-RPC errors.
|
|
37
|
+
*
|
|
38
|
+
* @param config Partial limiter configuration.
|
|
39
|
+
* @param store Optional backing store implementation.
|
|
40
|
+
* @returns Express-compatible async middleware.
|
|
41
|
+
* @since 1.0.0
|
|
42
|
+
*/
|
|
43
|
+
declare function createRateLimiter(config?: Partial<RateLimitConfig>, store?: RateLimitStore): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
44
|
+
|
|
45
|
+
export { InMemoryRateLimitStore, RedisRateLimitStore, createRateLimiter };
|
|
46
|
+
export type { RateLimitConfig, RateLimitState, RateLimitStore, RedisRateLimitClient };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file rateLimiter.ts
|
|
5
|
+
* Express middleware for per-client request throttling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface RateLimitConfig {
|
|
9
|
+
windowMs: number;
|
|
10
|
+
maxRequests: number;
|
|
11
|
+
keyGenerator?: (req: Request) => string;
|
|
12
|
+
}
|
|
13
|
+
interface RateLimitState {
|
|
14
|
+
count: number;
|
|
15
|
+
resetTime: number;
|
|
16
|
+
}
|
|
17
|
+
interface RateLimitStore {
|
|
18
|
+
increment(key: string, windowMs: number): Promise<RateLimitState>;
|
|
19
|
+
}
|
|
20
|
+
declare class InMemoryRateLimitStore implements RateLimitStore {
|
|
21
|
+
private readonly state;
|
|
22
|
+
increment(key: string, windowMs: number): Promise<RateLimitState>;
|
|
23
|
+
}
|
|
24
|
+
interface RedisRateLimitClient {
|
|
25
|
+
get(key: string): Promise<string | null>;
|
|
26
|
+
incr(key: string): Promise<number>;
|
|
27
|
+
pexpire(key: string, ttl: number): Promise<number>;
|
|
28
|
+
pttl(key: string): Promise<number>;
|
|
29
|
+
}
|
|
30
|
+
declare class RedisRateLimitStore implements RateLimitStore {
|
|
31
|
+
private readonly client;
|
|
32
|
+
constructor(client: RedisRateLimitClient);
|
|
33
|
+
increment(key: string, windowMs: number): Promise<RateLimitState>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create an Express middleware that enforces per-client request limits and emits JSON-RPC errors.
|
|
37
|
+
*
|
|
38
|
+
* @param config Partial limiter configuration.
|
|
39
|
+
* @param store Optional backing store implementation.
|
|
40
|
+
* @returns Express-compatible async middleware.
|
|
41
|
+
* @since 1.0.0
|
|
42
|
+
*/
|
|
43
|
+
declare function createRateLimiter(config?: Partial<RateLimitConfig>, store?: RateLimitStore): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
44
|
+
|
|
45
|
+
export { InMemoryRateLimitStore, RedisRateLimitStore, createRateLimiter };
|
|
46
|
+
export type { RateLimitConfig, RateLimitState, RateLimitStore, RedisRateLimitClient };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { I as InMemoryRateLimitStore, R as RedisRateLimitStore, c as createRateLimiter } from '../shared/a2a-mesh.CpP0sKxA.mjs';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ErrorCodes = {
|
|
4
|
+
ParseError: -32700,
|
|
5
|
+
InvalidRequest: -32600,
|
|
6
|
+
MethodNotFound: -32601,
|
|
7
|
+
InvalidParams: -32602,
|
|
8
|
+
InternalError: -32603,
|
|
9
|
+
TaskNotFound: -32004,
|
|
10
|
+
PushNotificationNotSupported: -32010,
|
|
11
|
+
UnsupportedOperation: -32011,
|
|
12
|
+
RateLimitExceeded: -32029,
|
|
13
|
+
Unauthorized: -32040,
|
|
14
|
+
ExtensionRequired: -32041
|
|
15
|
+
};
|
|
16
|
+
class JsonRpcError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
data;
|
|
19
|
+
constructor(code, message, data) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.data = data;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class InMemoryRateLimitStore {
|
|
27
|
+
state = /* @__PURE__ */ new Map();
|
|
28
|
+
async increment(key, windowMs) {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const current = this.state.get(key);
|
|
31
|
+
if (!current || current.resetTime <= now) {
|
|
32
|
+
const nextState2 = { count: 1, resetTime: now + windowMs };
|
|
33
|
+
this.state.set(key, nextState2);
|
|
34
|
+
return nextState2;
|
|
35
|
+
}
|
|
36
|
+
const nextState = { ...current, count: current.count + 1 };
|
|
37
|
+
this.state.set(key, nextState);
|
|
38
|
+
return nextState;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
class RedisRateLimitStore {
|
|
42
|
+
constructor(client) {
|
|
43
|
+
this.client = client;
|
|
44
|
+
}
|
|
45
|
+
async increment(key, windowMs) {
|
|
46
|
+
const current = await this.client.get(key);
|
|
47
|
+
if (current === null) {
|
|
48
|
+
await this.client.incr(key);
|
|
49
|
+
await this.client.pexpire(key, windowMs);
|
|
50
|
+
return {
|
|
51
|
+
count: 1,
|
|
52
|
+
resetTime: Date.now() + windowMs
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const count = await this.client.incr(key);
|
|
56
|
+
const ttl = await this.client.pttl(key);
|
|
57
|
+
return {
|
|
58
|
+
count,
|
|
59
|
+
resetTime: Date.now() + Math.max(ttl, 0)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function getClientIp(req) {
|
|
64
|
+
const forwarded = req.headers["x-forwarded-for"];
|
|
65
|
+
if (typeof forwarded === "string" && forwarded.length > 0) {
|
|
66
|
+
return forwarded.split(",")[0]?.trim() ?? "unknown";
|
|
67
|
+
}
|
|
68
|
+
return req.ip || req.socket.remoteAddress || "unknown";
|
|
69
|
+
}
|
|
70
|
+
function createRateLimiter(config = {}, store = new InMemoryRateLimitStore()) {
|
|
71
|
+
const windowMs = config.windowMs ?? 6e4;
|
|
72
|
+
const maxRequests = config.maxRequests ?? 100;
|
|
73
|
+
const keyGenerator = config.keyGenerator ?? getClientIp;
|
|
74
|
+
return async (req, res, next) => {
|
|
75
|
+
const key = keyGenerator(req);
|
|
76
|
+
const state = await store.increment(key, windowMs);
|
|
77
|
+
const remaining = Math.max(maxRequests - state.count, 0);
|
|
78
|
+
res.setHeader("X-RateLimit-Limit", String(maxRequests));
|
|
79
|
+
res.setHeader("X-RateLimit-Remaining", String(remaining));
|
|
80
|
+
res.setHeader("X-RateLimit-Reset", String(Math.floor(state.resetTime / 1e3)));
|
|
81
|
+
if (state.count > maxRequests) {
|
|
82
|
+
res.status(429).json({
|
|
83
|
+
jsonrpc: "2.0",
|
|
84
|
+
error: {
|
|
85
|
+
code: ErrorCodes.RateLimitExceeded,
|
|
86
|
+
message: "Too Many Requests",
|
|
87
|
+
data: { retryAfterMs: Math.max(state.resetTime - Date.now(), 0) }
|
|
88
|
+
},
|
|
89
|
+
id: req.body && typeof req.body === "object" && "id" in req.body ? req.body.id : null
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
next();
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
exports.ErrorCodes = ErrorCodes;
|
|
98
|
+
exports.InMemoryRateLimitStore = InMemoryRateLimitStore;
|
|
99
|
+
exports.JsonRpcError = JsonRpcError;
|
|
100
|
+
exports.RedisRateLimitStore = RedisRateLimitStore;
|
|
101
|
+
exports.createRateLimiter = createRateLimiter;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file auth.ts
|
|
5
|
+
* Authentication and authorization related types.
|
|
6
|
+
*/
|
|
7
|
+
interface BaseAuthScheme {
|
|
8
|
+
id: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
interface ApiKeyAuthScheme extends BaseAuthScheme {
|
|
12
|
+
type: 'apiKey';
|
|
13
|
+
in: 'header' | 'query';
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
interface HttpAuthScheme extends BaseAuthScheme {
|
|
17
|
+
type: 'http';
|
|
18
|
+
scheme: 'bearer';
|
|
19
|
+
bearerFormat?: string;
|
|
20
|
+
}
|
|
21
|
+
interface OpenIdConnectAuthScheme extends BaseAuthScheme {
|
|
22
|
+
type: 'openIdConnect';
|
|
23
|
+
openIdConnectUrl: string;
|
|
24
|
+
audience?: string | string[];
|
|
25
|
+
issuer?: string;
|
|
26
|
+
jwksUri?: string;
|
|
27
|
+
algorithms?: string[];
|
|
28
|
+
}
|
|
29
|
+
type AuthScheme = ApiKeyAuthScheme | HttpAuthScheme | OpenIdConnectAuthScheme;
|
|
30
|
+
interface ApiKeyCredentialSource {
|
|
31
|
+
[schemeId: string]: string | string[];
|
|
32
|
+
}
|
|
33
|
+
interface AuthValidationResult {
|
|
34
|
+
schemeId: string;
|
|
35
|
+
subject?: string;
|
|
36
|
+
claims?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @file JwtAuthMiddleware.ts
|
|
41
|
+
* Authentication middleware backed by OIDC discovery, JWKS and API keys.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
interface JwtAuthMiddlewareOptions {
|
|
45
|
+
securitySchemes: AuthScheme[];
|
|
46
|
+
security?: Record<string, string[]>[];
|
|
47
|
+
apiKeys?: ApiKeyCredentialSource;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Authentication middleware that evaluates A2A security schemes against incoming requests.
|
|
51
|
+
*
|
|
52
|
+
* Supports API keys, HTTP bearer tokens, and OIDC discovery with JWKS validation.
|
|
53
|
+
*
|
|
54
|
+
* @since 1.0.0
|
|
55
|
+
*/
|
|
56
|
+
declare class JwtAuthMiddleware {
|
|
57
|
+
private readonly options;
|
|
58
|
+
private readonly remoteSets;
|
|
59
|
+
constructor(options: JwtAuthMiddlewareOptions);
|
|
60
|
+
authenticateRequest(req: Request): Promise<AuthValidationResult>;
|
|
61
|
+
middleware(): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
62
|
+
private validateApiKey;
|
|
63
|
+
private validateOidcToken;
|
|
64
|
+
private validateBearerToken;
|
|
65
|
+
private readBearerToken;
|
|
66
|
+
private decodeJwtWithoutValidation;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { JwtAuthMiddleware as J };
|
|
70
|
+
export type { AuthScheme as A, BaseAuthScheme as B, HttpAuthScheme as H, OpenIdConnectAuthScheme as O, JwtAuthMiddlewareOptions as a, ApiKeyAuthScheme as b, ApiKeyCredentialSource as c, AuthValidationResult as d };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file auth.ts
|
|
5
|
+
* Authentication and authorization related types.
|
|
6
|
+
*/
|
|
7
|
+
interface BaseAuthScheme {
|
|
8
|
+
id: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
interface ApiKeyAuthScheme extends BaseAuthScheme {
|
|
12
|
+
type: 'apiKey';
|
|
13
|
+
in: 'header' | 'query';
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
interface HttpAuthScheme extends BaseAuthScheme {
|
|
17
|
+
type: 'http';
|
|
18
|
+
scheme: 'bearer';
|
|
19
|
+
bearerFormat?: string;
|
|
20
|
+
}
|
|
21
|
+
interface OpenIdConnectAuthScheme extends BaseAuthScheme {
|
|
22
|
+
type: 'openIdConnect';
|
|
23
|
+
openIdConnectUrl: string;
|
|
24
|
+
audience?: string | string[];
|
|
25
|
+
issuer?: string;
|
|
26
|
+
jwksUri?: string;
|
|
27
|
+
algorithms?: string[];
|
|
28
|
+
}
|
|
29
|
+
type AuthScheme = ApiKeyAuthScheme | HttpAuthScheme | OpenIdConnectAuthScheme;
|
|
30
|
+
interface ApiKeyCredentialSource {
|
|
31
|
+
[schemeId: string]: string | string[];
|
|
32
|
+
}
|
|
33
|
+
interface AuthValidationResult {
|
|
34
|
+
schemeId: string;
|
|
35
|
+
subject?: string;
|
|
36
|
+
claims?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @file JwtAuthMiddleware.ts
|
|
41
|
+
* Authentication middleware backed by OIDC discovery, JWKS and API keys.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
interface JwtAuthMiddlewareOptions {
|
|
45
|
+
securitySchemes: AuthScheme[];
|
|
46
|
+
security?: Record<string, string[]>[];
|
|
47
|
+
apiKeys?: ApiKeyCredentialSource;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Authentication middleware that evaluates A2A security schemes against incoming requests.
|
|
51
|
+
*
|
|
52
|
+
* Supports API keys, HTTP bearer tokens, and OIDC discovery with JWKS validation.
|
|
53
|
+
*
|
|
54
|
+
* @since 1.0.0
|
|
55
|
+
*/
|
|
56
|
+
declare class JwtAuthMiddleware {
|
|
57
|
+
private readonly options;
|
|
58
|
+
private readonly remoteSets;
|
|
59
|
+
constructor(options: JwtAuthMiddlewareOptions);
|
|
60
|
+
authenticateRequest(req: Request): Promise<AuthValidationResult>;
|
|
61
|
+
middleware(): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
62
|
+
private validateApiKey;
|
|
63
|
+
private validateOidcToken;
|
|
64
|
+
private validateBearerToken;
|
|
65
|
+
private readBearerToken;
|
|
66
|
+
private decodeJwtWithoutValidation;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { JwtAuthMiddleware as J };
|
|
70
|
+
export type { AuthScheme as A, BaseAuthScheme as B, HttpAuthScheme as H, OpenIdConnectAuthScheme as O, JwtAuthMiddlewareOptions as a, ApiKeyAuthScheme as b, ApiKeyCredentialSource as c, AuthValidationResult as d };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file auth.ts
|
|
5
|
+
* Authentication and authorization related types.
|
|
6
|
+
*/
|
|
7
|
+
interface BaseAuthScheme {
|
|
8
|
+
id: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
interface ApiKeyAuthScheme extends BaseAuthScheme {
|
|
12
|
+
type: 'apiKey';
|
|
13
|
+
in: 'header' | 'query';
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
interface HttpAuthScheme extends BaseAuthScheme {
|
|
17
|
+
type: 'http';
|
|
18
|
+
scheme: 'bearer';
|
|
19
|
+
bearerFormat?: string;
|
|
20
|
+
}
|
|
21
|
+
interface OpenIdConnectAuthScheme extends BaseAuthScheme {
|
|
22
|
+
type: 'openIdConnect';
|
|
23
|
+
openIdConnectUrl: string;
|
|
24
|
+
audience?: string | string[];
|
|
25
|
+
issuer?: string;
|
|
26
|
+
jwksUri?: string;
|
|
27
|
+
algorithms?: string[];
|
|
28
|
+
}
|
|
29
|
+
type AuthScheme = ApiKeyAuthScheme | HttpAuthScheme | OpenIdConnectAuthScheme;
|
|
30
|
+
interface ApiKeyCredentialSource {
|
|
31
|
+
[schemeId: string]: string | string[];
|
|
32
|
+
}
|
|
33
|
+
interface AuthValidationResult {
|
|
34
|
+
schemeId: string;
|
|
35
|
+
subject?: string;
|
|
36
|
+
claims?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @file JwtAuthMiddleware.ts
|
|
41
|
+
* Authentication middleware backed by OIDC discovery, JWKS and API keys.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
interface JwtAuthMiddlewareOptions {
|
|
45
|
+
securitySchemes: AuthScheme[];
|
|
46
|
+
security?: Record<string, string[]>[];
|
|
47
|
+
apiKeys?: ApiKeyCredentialSource;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Authentication middleware that evaluates A2A security schemes against incoming requests.
|
|
51
|
+
*
|
|
52
|
+
* Supports API keys, HTTP bearer tokens, and OIDC discovery with JWKS validation.
|
|
53
|
+
*
|
|
54
|
+
* @since 1.0.0
|
|
55
|
+
*/
|
|
56
|
+
declare class JwtAuthMiddleware {
|
|
57
|
+
private readonly options;
|
|
58
|
+
private readonly remoteSets;
|
|
59
|
+
constructor(options: JwtAuthMiddlewareOptions);
|
|
60
|
+
authenticateRequest(req: Request): Promise<AuthValidationResult>;
|
|
61
|
+
middleware(): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
62
|
+
private validateApiKey;
|
|
63
|
+
private validateOidcToken;
|
|
64
|
+
private validateBearerToken;
|
|
65
|
+
private readBearerToken;
|
|
66
|
+
private decodeJwtWithoutValidation;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { JwtAuthMiddleware as J };
|
|
70
|
+
export type { AuthScheme as A, BaseAuthScheme as B, HttpAuthScheme as H, OpenIdConnectAuthScheme as O, JwtAuthMiddlewareOptions as a, ApiKeyAuthScheme as b, ApiKeyCredentialSource as c, AuthValidationResult as d };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const ErrorCodes = {
|
|
2
|
+
ParseError: -32700,
|
|
3
|
+
InvalidRequest: -32600,
|
|
4
|
+
MethodNotFound: -32601,
|
|
5
|
+
InvalidParams: -32602,
|
|
6
|
+
InternalError: -32603,
|
|
7
|
+
TaskNotFound: -32004,
|
|
8
|
+
PushNotificationNotSupported: -32010,
|
|
9
|
+
UnsupportedOperation: -32011,
|
|
10
|
+
RateLimitExceeded: -32029,
|
|
11
|
+
Unauthorized: -32040,
|
|
12
|
+
ExtensionRequired: -32041
|
|
13
|
+
};
|
|
14
|
+
class JsonRpcError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
data;
|
|
17
|
+
constructor(code, message, data) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.data = data;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class InMemoryRateLimitStore {
|
|
25
|
+
state = /* @__PURE__ */ new Map();
|
|
26
|
+
async increment(key, windowMs) {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const current = this.state.get(key);
|
|
29
|
+
if (!current || current.resetTime <= now) {
|
|
30
|
+
const nextState2 = { count: 1, resetTime: now + windowMs };
|
|
31
|
+
this.state.set(key, nextState2);
|
|
32
|
+
return nextState2;
|
|
33
|
+
}
|
|
34
|
+
const nextState = { ...current, count: current.count + 1 };
|
|
35
|
+
this.state.set(key, nextState);
|
|
36
|
+
return nextState;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
class RedisRateLimitStore {
|
|
40
|
+
constructor(client) {
|
|
41
|
+
this.client = client;
|
|
42
|
+
}
|
|
43
|
+
async increment(key, windowMs) {
|
|
44
|
+
const current = await this.client.get(key);
|
|
45
|
+
if (current === null) {
|
|
46
|
+
await this.client.incr(key);
|
|
47
|
+
await this.client.pexpire(key, windowMs);
|
|
48
|
+
return {
|
|
49
|
+
count: 1,
|
|
50
|
+
resetTime: Date.now() + windowMs
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const count = await this.client.incr(key);
|
|
54
|
+
const ttl = await this.client.pttl(key);
|
|
55
|
+
return {
|
|
56
|
+
count,
|
|
57
|
+
resetTime: Date.now() + Math.max(ttl, 0)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function getClientIp(req) {
|
|
62
|
+
const forwarded = req.headers["x-forwarded-for"];
|
|
63
|
+
if (typeof forwarded === "string" && forwarded.length > 0) {
|
|
64
|
+
return forwarded.split(",")[0]?.trim() ?? "unknown";
|
|
65
|
+
}
|
|
66
|
+
return req.ip || req.socket.remoteAddress || "unknown";
|
|
67
|
+
}
|
|
68
|
+
function createRateLimiter(config = {}, store = new InMemoryRateLimitStore()) {
|
|
69
|
+
const windowMs = config.windowMs ?? 6e4;
|
|
70
|
+
const maxRequests = config.maxRequests ?? 100;
|
|
71
|
+
const keyGenerator = config.keyGenerator ?? getClientIp;
|
|
72
|
+
return async (req, res, next) => {
|
|
73
|
+
const key = keyGenerator(req);
|
|
74
|
+
const state = await store.increment(key, windowMs);
|
|
75
|
+
const remaining = Math.max(maxRequests - state.count, 0);
|
|
76
|
+
res.setHeader("X-RateLimit-Limit", String(maxRequests));
|
|
77
|
+
res.setHeader("X-RateLimit-Remaining", String(remaining));
|
|
78
|
+
res.setHeader("X-RateLimit-Reset", String(Math.floor(state.resetTime / 1e3)));
|
|
79
|
+
if (state.count > maxRequests) {
|
|
80
|
+
res.status(429).json({
|
|
81
|
+
jsonrpc: "2.0",
|
|
82
|
+
error: {
|
|
83
|
+
code: ErrorCodes.RateLimitExceeded,
|
|
84
|
+
message: "Too Many Requests",
|
|
85
|
+
data: { retryAfterMs: Math.max(state.resetTime - Date.now(), 0) }
|
|
86
|
+
},
|
|
87
|
+
id: req.body && typeof req.body === "object" && "id" in req.body ? req.body.id : null
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
next();
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { ErrorCodes as E, InMemoryRateLimitStore as I, JsonRpcError as J, RedisRateLimitStore as R, createRateLimiter as c };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const jose = require('jose');
|
|
4
|
+
|
|
5
|
+
class JwtAuthMiddleware {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
remoteSets = /* @__PURE__ */ new Map();
|
|
10
|
+
async authenticateRequest(req) {
|
|
11
|
+
const securityRequirements = this.options.security && this.options.security.length > 0 ? this.options.security : [Object.fromEntries(this.options.securitySchemes.map((scheme) => [scheme.id, []]))];
|
|
12
|
+
let lastError;
|
|
13
|
+
for (const requirement of securityRequirements) {
|
|
14
|
+
try {
|
|
15
|
+
for (const schemeId of Object.keys(requirement)) {
|
|
16
|
+
const scheme = this.options.securitySchemes.find((item) => item.id === schemeId);
|
|
17
|
+
if (!scheme) {
|
|
18
|
+
throw new Error(`Unknown security scheme: ${schemeId}`);
|
|
19
|
+
}
|
|
20
|
+
if (scheme.type === "apiKey") {
|
|
21
|
+
return this.validateApiKey(req, scheme);
|
|
22
|
+
}
|
|
23
|
+
if (scheme.type === "http") {
|
|
24
|
+
return this.validateBearerToken(req);
|
|
25
|
+
}
|
|
26
|
+
if (scheme.type === "openIdConnect") {
|
|
27
|
+
return this.validateOidcToken(req, scheme);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw lastError ?? new Error("Authentication failed");
|
|
35
|
+
}
|
|
36
|
+
middleware() {
|
|
37
|
+
return async (req, res, next) => {
|
|
38
|
+
try {
|
|
39
|
+
const authResult = await this.authenticateRequest(req);
|
|
40
|
+
Object.assign(req, { auth: authResult });
|
|
41
|
+
next();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
res.status(401).json({
|
|
44
|
+
jsonrpc: "2.0",
|
|
45
|
+
error: {
|
|
46
|
+
code: -32040,
|
|
47
|
+
message: "Unauthorized",
|
|
48
|
+
data: { reason: String(error) }
|
|
49
|
+
},
|
|
50
|
+
id: req.body && typeof req.body === "object" && "id" in req.body ? req.body.id : null
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
validateApiKey(req, scheme) {
|
|
56
|
+
const expected = this.options.apiKeys?.[scheme.id];
|
|
57
|
+
const validValues = Array.isArray(expected) ? expected : expected ? [expected] : [];
|
|
58
|
+
if (validValues.length === 0) {
|
|
59
|
+
throw new Error(`No API key configured for scheme ${scheme.id}`);
|
|
60
|
+
}
|
|
61
|
+
const incoming = scheme.in === "header" ? req.header(scheme.name) : typeof req.query[scheme.name] === "string" ? req.query[scheme.name] : void 0;
|
|
62
|
+
if (typeof incoming !== "string" || !validValues.includes(incoming)) {
|
|
63
|
+
throw new Error("Invalid API key");
|
|
64
|
+
}
|
|
65
|
+
return { schemeId: scheme.id };
|
|
66
|
+
}
|
|
67
|
+
async validateOidcToken(req, scheme) {
|
|
68
|
+
const token = this.readBearerToken(req);
|
|
69
|
+
const discoveryResponse = await fetch(scheme.openIdConnectUrl);
|
|
70
|
+
if (!discoveryResponse.ok) {
|
|
71
|
+
throw new Error(`Failed to fetch OIDC configuration: ${discoveryResponse.status}`);
|
|
72
|
+
}
|
|
73
|
+
const discovery = await discoveryResponse.json();
|
|
74
|
+
const jwksUri = scheme.jwksUri ?? discovery.jwks_uri;
|
|
75
|
+
if (!jwksUri) {
|
|
76
|
+
throw new Error("OIDC configuration is missing jwks_uri");
|
|
77
|
+
}
|
|
78
|
+
let remoteSet = this.remoteSets.get(jwksUri);
|
|
79
|
+
if (!remoteSet) {
|
|
80
|
+
remoteSet = jose.createRemoteJWKSet(new URL(jwksUri));
|
|
81
|
+
this.remoteSets.set(jwksUri, remoteSet);
|
|
82
|
+
}
|
|
83
|
+
const verifyOptions = {
|
|
84
|
+
...scheme.audience ? { audience: scheme.audience } : {},
|
|
85
|
+
...scheme.issuer ?? discovery.issuer ? { issuer: scheme.issuer ?? discovery.issuer } : {},
|
|
86
|
+
algorithms: scheme.algorithms ?? ["RS256", "ES256"]
|
|
87
|
+
};
|
|
88
|
+
const { payload } = await jose.jwtVerify(token, remoteSet, verifyOptions);
|
|
89
|
+
return {
|
|
90
|
+
schemeId: scheme.id,
|
|
91
|
+
...payload.sub ? { subject: payload.sub } : {},
|
|
92
|
+
claims: payload
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async validateBearerToken(req) {
|
|
96
|
+
const token = this.readBearerToken(req);
|
|
97
|
+
const payload = this.decodeJwtWithoutValidation(token);
|
|
98
|
+
return {
|
|
99
|
+
schemeId: "bearer",
|
|
100
|
+
...payload.sub ? { subject: payload.sub } : {},
|
|
101
|
+
claims: payload
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
readBearerToken(req) {
|
|
105
|
+
const header = req.header("authorization");
|
|
106
|
+
if (!header || !header.toLowerCase().startsWith("bearer ")) {
|
|
107
|
+
throw new Error("Missing bearer token");
|
|
108
|
+
}
|
|
109
|
+
return header.slice("bearer ".length).trim();
|
|
110
|
+
}
|
|
111
|
+
decodeJwtWithoutValidation(token) {
|
|
112
|
+
const parts = token.split(".");
|
|
113
|
+
if (parts.length < 2) {
|
|
114
|
+
throw new Error("Invalid JWT");
|
|
115
|
+
}
|
|
116
|
+
const payloadJson = Buffer.from(parts[1] ?? "", "base64url").toString("utf8");
|
|
117
|
+
return JSON.parse(payloadJson);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
exports.JwtAuthMiddleware = JwtAuthMiddleware;
|