@fdkey/mcp 0.2.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/CHANGELOG.md +47 -0
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/guard.d.ts +17 -0
- package/dist/guard.d.ts.map +1 -0
- package/dist/guard.js +49 -0
- package/dist/guard.js.map +1 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +409 -0
- package/dist/index.js.map +1 -0
- package/dist/router-static.d.ts +13 -0
- package/dist/router-static.d.ts.map +1 -0
- package/dist/router-static.js +16 -0
- package/dist/router-static.js.map +1 -0
- package/dist/session-store.d.ts +45 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +71 -0
- package/dist/session-store.js.map +1 -0
- package/dist/types.d.ts +156 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/vps-client.d.ts +42 -0
- package/dist/vps-client.d.ts.map +1 -0
- package/dist/vps-client.js +90 -0
- package/dist/vps-client.js.map +1 -0
- package/dist/vps-router.d.ts +29 -0
- package/dist/vps-router.d.ts.map +1 -0
- package/dist/vps-router.js +146 -0
- package/dist/vps-router.js.map +1 -0
- package/dist/well-known.d.ts +14 -0
- package/dist/well-known.d.ts.map +1 -0
- package/dist/well-known.js +42 -0
- package/dist/well-known.js.map +1 -0
- package/package.json +72 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export type Policy = {
|
|
2
|
+
type: 'once_per_session';
|
|
3
|
+
} | {
|
|
4
|
+
type: 'each_call';
|
|
5
|
+
} | {
|
|
6
|
+
type: 'every_minutes';
|
|
7
|
+
minutes: number;
|
|
8
|
+
};
|
|
9
|
+
export type PolicyShorthand = 'once_per_session' | 'each_call';
|
|
10
|
+
export interface ProtectEntry {
|
|
11
|
+
policy: Policy | PolicyShorthand;
|
|
12
|
+
}
|
|
13
|
+
export interface FdkeyConfig {
|
|
14
|
+
/** Integrator's VPS API key (Bearer token). Required. */
|
|
15
|
+
apiKey: string;
|
|
16
|
+
/** Tools that require verification, keyed by tool name. */
|
|
17
|
+
protect?: Record<string, ProtectEntry>;
|
|
18
|
+
/** 'easy' | 'medium' | 'hard' — passed to VPS on challenge fetch. Default: 'medium' */
|
|
19
|
+
difficulty?: 'easy' | 'medium' | 'hard';
|
|
20
|
+
/** What happens when the agent fails the puzzle. Default: 'block' */
|
|
21
|
+
onFail?: 'block' | 'allow';
|
|
22
|
+
/** What happens when the FDKEY VPS is unreachable. Default: 'allow'
|
|
23
|
+
* (fail-open) — the protected tool runs as if no verification were
|
|
24
|
+
* required, so an FDKEY outage doesn't brick integrator workflows.
|
|
25
|
+
* Set to 'block' if your threat model prefers fail-closed. */
|
|
26
|
+
onVpsError?: 'block' | 'allow';
|
|
27
|
+
/** When true, blocked-tool errors embed the puzzle data so the agent can submit without a separate fdkey_get_challenge call. Default: false */
|
|
28
|
+
inlineChallenge?: boolean;
|
|
29
|
+
/** Skip discovery and use this VPS URL directly (local dev, self-hosted). */
|
|
30
|
+
vpsUrl?: string;
|
|
31
|
+
/** Override the Cloudflare CDN discovery URL. */
|
|
32
|
+
discoveryUrl?: string;
|
|
33
|
+
/** Arbitrary string-keyed dimensions forwarded to FDKEY on every
|
|
34
|
+
* challenge request, stored as `tags` on the session row. Useful for
|
|
35
|
+
* multi-tenant setups (`tenant_id`), env labels (`env: prod`), A/B
|
|
36
|
+
* experiments, deployment markers, etc.
|
|
37
|
+
*
|
|
38
|
+
* IMPORTANT: tags travel to FDKEY's servers and may end up in our
|
|
39
|
+
* analytics database. **Never put end-user PII in here.** Bounded
|
|
40
|
+
* server-side at 16 keys, 50 chars/key, 200 chars/value — extra
|
|
41
|
+
* fields are rejected with HTTP 400. */
|
|
42
|
+
tags?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
export interface SessionState {
|
|
45
|
+
/** True after any successful submit on this connection. Never reset to false. */
|
|
46
|
+
verified: boolean;
|
|
47
|
+
/** Timestamp of the most recent successful verification (puzzle solve). */
|
|
48
|
+
verifiedAt: number | null;
|
|
49
|
+
/** Timestamp (ms epoch) of the last access. Drives the LRU + TTL
|
|
50
|
+
* eviction in the per-server session map so the SDK doesn't leak
|
|
51
|
+
* session entries on long-lived shared servers. Touched by every
|
|
52
|
+
* call into `getSession(...)`. */
|
|
53
|
+
lastTouchedAt: number;
|
|
54
|
+
/** True after a successful submit; consumed (set false) by the next each_call tool call. */
|
|
55
|
+
freshVerificationAvailable: boolean;
|
|
56
|
+
/** Internal: the active VPS challenge ID for this connection. Agent never sees this. */
|
|
57
|
+
pendingChallengeId: string | null;
|
|
58
|
+
/** Decoded JWT payload from the most recent successful verification. Surfaced to
|
|
59
|
+
* integrator tool handlers via getFdkeyContext(). The raw JWT itself is never stored. */
|
|
60
|
+
lastClaims: Record<string, unknown> | null;
|
|
61
|
+
/** AI client identification captured from MCP `initialize` handshake.
|
|
62
|
+
* Lazy-copied from the closure-scope `latestClientInfo` on the first tool call
|
|
63
|
+
* for this session (so it reflects whichever client most recently completed
|
|
64
|
+
* initialize on this server instance). Forwarded to the VPS on /v1/challenge. */
|
|
65
|
+
clientInfo: {
|
|
66
|
+
name: string;
|
|
67
|
+
version: string;
|
|
68
|
+
title?: string;
|
|
69
|
+
capabilities?: Record<string, unknown>;
|
|
70
|
+
} | null;
|
|
71
|
+
/** Negotiated MCP protocol version captured from the initialize request
|
|
72
|
+
* params (e.g. "2025-03-26"). Filled by the InitializeRequestSchema
|
|
73
|
+
* interceptor in withFdkey(). */
|
|
74
|
+
protocolVersion: string | null;
|
|
75
|
+
/** The MCP-Session-Id header value from HTTP Streamable transport, or the
|
|
76
|
+
* literal `'stdio'` for stdio transport. Captured from `extra.sessionId` on
|
|
77
|
+
* the first tool call. Forwarded to the VPS on /v1/challenge. */
|
|
78
|
+
mcpSessionId: string | null;
|
|
79
|
+
/** Inferred MCP transport flavor: stdio if no sessionId on the handler extra,
|
|
80
|
+
* http otherwise, unknown if we haven't seen a tool call yet. */
|
|
81
|
+
transport: 'stdio' | 'http' | 'unknown';
|
|
82
|
+
}
|
|
83
|
+
/** Agent block forwarded to the VPS in the /v1/challenge request body.
|
|
84
|
+
* Mirrors the fields stored in `vps_sessions.agent_info.agent`. */
|
|
85
|
+
export interface AgentMeta {
|
|
86
|
+
client_name?: string;
|
|
87
|
+
client_version?: string;
|
|
88
|
+
client_title?: string;
|
|
89
|
+
client_capabilities?: Record<string, unknown>;
|
|
90
|
+
protocol_version?: string;
|
|
91
|
+
mcp_session_id?: string;
|
|
92
|
+
transport?: 'stdio' | 'http' | 'unknown';
|
|
93
|
+
}
|
|
94
|
+
/** Integrator block forwarded to the VPS — facts about the MCP server +
|
|
95
|
+
* SDK version that's calling us. Stored in `vps_sessions.agent_info.integrator`
|
|
96
|
+
* alongside the VPS-observed `ip` + `user_agent`. */
|
|
97
|
+
export interface IntegratorMeta {
|
|
98
|
+
server_name?: string;
|
|
99
|
+
server_version?: string;
|
|
100
|
+
sdk_version?: string;
|
|
101
|
+
}
|
|
102
|
+
/** Combined per-challenge metadata bundle. Computed by withFdkey() each time
|
|
103
|
+
* a challenge is fetched and passed to VpsClient.fetchChallenge() as a single
|
|
104
|
+
* object so the wire format can grow without churning the call signature. */
|
|
105
|
+
export interface ChallengeMeta {
|
|
106
|
+
agent?: AgentMeta;
|
|
107
|
+
integrator?: IntegratorMeta;
|
|
108
|
+
tags?: Record<string, string>;
|
|
109
|
+
}
|
|
110
|
+
/** Returned by `IVpsRouter.getTarget()`. Encapsulates everything a caller
|
|
111
|
+
* needs to make a TLS request that lands on the correct VPS in the fleet:
|
|
112
|
+
* a stable URL, an optional dispatcher pinned to a chosen IP (Node-only;
|
|
113
|
+
* undefined on Workers/Bun/Deno or when StaticRouter is used), and the IP
|
|
114
|
+
* itself for failure tracking.
|
|
115
|
+
*
|
|
116
|
+
* `dispatcher` is typed `unknown` so this module — and any module that
|
|
117
|
+
* imports it — never touches the undici types. The runtime check happens
|
|
118
|
+
* inside vps-client.ts which casts it back when passing to fetch. */
|
|
119
|
+
export interface RoutingTarget {
|
|
120
|
+
url: string;
|
|
121
|
+
dispatcher?: unknown;
|
|
122
|
+
ip?: string;
|
|
123
|
+
}
|
|
124
|
+
export interface IVpsRouter {
|
|
125
|
+
getTarget(): Promise<RoutingTarget>;
|
|
126
|
+
recordFailure(ip: string | undefined): void;
|
|
127
|
+
}
|
|
128
|
+
/** A VPS in the fleet, as listed in cdn.fdkey.com/endpoints.json.
|
|
129
|
+
* All FDKEY VPSs serve HTTPS for the same hostname (`api.fdkey.com`); the
|
|
130
|
+
* SDK pins each connection to a specific IP and presents `api.fdkey.com`
|
|
131
|
+
* as the SNI value, so the cert validates regardless of which IP we pick.
|
|
132
|
+
* This is the standard SDK-driven multi-region routing pattern (MongoDB
|
|
133
|
+
* driver, AWS SDK, etc.) and means adding a VPS = adding an entry here,
|
|
134
|
+
* with zero DNS work. */
|
|
135
|
+
export interface VpsEndpoint {
|
|
136
|
+
/** Public IPv4 of the VPS. */
|
|
137
|
+
ip: string;
|
|
138
|
+
/** Region tag for analytics + admin-key env-var derivation. */
|
|
139
|
+
region: string;
|
|
140
|
+
/** Selection weight (currently informational; sort uses error+latency). */
|
|
141
|
+
weight: number;
|
|
142
|
+
/** Marker for graceful decommissioning. SDK ignores deprecated entries. */
|
|
143
|
+
deprecated?: boolean;
|
|
144
|
+
}
|
|
145
|
+
export interface WellKnownKey {
|
|
146
|
+
alg: string;
|
|
147
|
+
kid: string;
|
|
148
|
+
public_key_pem: string;
|
|
149
|
+
}
|
|
150
|
+
export interface WellKnownPayload {
|
|
151
|
+
issuer: string;
|
|
152
|
+
keys: WellKnownKey[];
|
|
153
|
+
jwt_default_lifetime_seconds: number;
|
|
154
|
+
}
|
|
155
|
+
export declare function normalisePolicy(p: Policy | PolicyShorthand): Policy;
|
|
156
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GACd;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,GAC5B;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/C,MAAM,MAAM,eAAe,GAAG,kBAAkB,GAAG,WAAW,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,GAAG,eAAe,CAAC;CAClC;AAED,MAAM,WAAW,WAAW;IAC1B,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACvC,uFAAuF;IACvF,UAAU,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IACxC,qEAAqE;IACrE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAC3B;;;mEAG+D;IAC/D,UAAU,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAC/B,+IAA+I;IAC/I,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;6CAQyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,iFAAiF;IACjF,QAAQ,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;uCAGmC;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,4FAA4F;IAC5F,0BAA0B,EAAE,OAAO,CAAC;IACpC,wFAAwF;IACxF,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC;8FAC0F;IAC1F,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC3C;;;sFAGkF;IAClF,UAAU,EAAE;QACV,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACxC,GAAG,IAAI,CAAC;IACT;;sCAEkC;IAClC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B;;sEAEkE;IAClE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;sEACkE;IAClE,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;CACzC;AAED;oEACoE;AACpE,MAAM,WAAW,SAAS;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;CAC1C;AAED;;sDAEsD;AACtD,MAAM,WAAW,cAAc;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;8EAE8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED;;;;;;;;sEAQsE;AACtE,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IACpC,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;CAC7C;AAED;;;;;;0BAM0B;AAC1B,MAAM,WAAW,WAAW;IAC1B,8BAA8B;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,4BAA4B,EAAE,MAAM,CAAC;CACtC;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,eAAe,GAAG,MAAM,CAGnE"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAmKA,MAAM,UAAU,eAAe,CAAC,CAA2B;IACzD,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC9C,OAAO,CAAC,CAAC;AACX,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ChallengeMeta, IVpsRouter } from './types.js';
|
|
2
|
+
export interface ChallengeResponse {
|
|
3
|
+
challenge_id: string;
|
|
4
|
+
expires_at: string;
|
|
5
|
+
expires_in_seconds?: number;
|
|
6
|
+
difficulty: string;
|
|
7
|
+
types_served: string[];
|
|
8
|
+
header?: string;
|
|
9
|
+
puzzles: Record<string, unknown>;
|
|
10
|
+
footer?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface SubmitResponse {
|
|
13
|
+
verified: boolean;
|
|
14
|
+
jwt?: string;
|
|
15
|
+
types_passed?: number;
|
|
16
|
+
types_served?: number;
|
|
17
|
+
required_to_pass?: number;
|
|
18
|
+
breakdown?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export declare class VpsHttpError extends Error {
|
|
21
|
+
readonly status: number;
|
|
22
|
+
readonly body: {
|
|
23
|
+
error?: string;
|
|
24
|
+
message?: string;
|
|
25
|
+
[k: string]: unknown;
|
|
26
|
+
};
|
|
27
|
+
constructor(status: number, body: {
|
|
28
|
+
error?: string;
|
|
29
|
+
message?: string;
|
|
30
|
+
[k: string]: unknown;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export declare class VpsClient {
|
|
34
|
+
private readonly router;
|
|
35
|
+
private readonly apiKey;
|
|
36
|
+
private readonly difficulty;
|
|
37
|
+
constructor(router: IVpsRouter, apiKey: string, difficulty: string);
|
|
38
|
+
fetchChallenge(meta?: ChallengeMeta): Promise<ChallengeResponse>;
|
|
39
|
+
submitAnswers(challengeId: string, answers: Record<string, unknown>): Promise<SubmitResponse>;
|
|
40
|
+
private post;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=vps-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vps-client.d.ts","sourceRoot":"","sources":["../src/vps-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE5D,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,OAAO,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAMD,qBAAa,YAAa,SAAQ,KAAK;aAEnB,MAAM,EAAE,MAAM;aACd,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE;gBADhE,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE;CAInF;AAED,qBAAa,SAAS;IAElB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAFV,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM;IAG/B,cAAc,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAehE,aAAa,CACjB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,CAAC,cAAc,CAAC;YAOZ,IAAI;CA0CnB"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
function hasValue(obj) {
|
|
2
|
+
return Object.values(obj).some((v) => v !== undefined && v !== null);
|
|
3
|
+
}
|
|
4
|
+
export class VpsHttpError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
body;
|
|
7
|
+
constructor(status, body) {
|
|
8
|
+
super(body.error ?? `HTTP ${status}`);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.body = body;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class VpsClient {
|
|
14
|
+
router;
|
|
15
|
+
apiKey;
|
|
16
|
+
difficulty;
|
|
17
|
+
constructor(router, apiKey, difficulty) {
|
|
18
|
+
this.router = router;
|
|
19
|
+
this.apiKey = apiKey;
|
|
20
|
+
this.difficulty = difficulty;
|
|
21
|
+
}
|
|
22
|
+
async fetchChallenge(meta) {
|
|
23
|
+
const target = await this.router.getTarget();
|
|
24
|
+
const body = {
|
|
25
|
+
difficulty: this.difficulty,
|
|
26
|
+
client_type: 'mcp',
|
|
27
|
+
};
|
|
28
|
+
// Only include each block if at least one field inside is populated —
|
|
29
|
+
// keeps the wire payload clean when the caller has no metadata yet
|
|
30
|
+
// (e.g. challenge fetched before any tool call has fired oninitialized).
|
|
31
|
+
if (meta?.agent && hasValue(meta.agent))
|
|
32
|
+
body.agent = meta.agent;
|
|
33
|
+
if (meta?.integrator && hasValue(meta.integrator))
|
|
34
|
+
body.integrator = meta.integrator;
|
|
35
|
+
if (meta?.tags && Object.keys(meta.tags).length > 0)
|
|
36
|
+
body.tags = meta.tags;
|
|
37
|
+
return this.post(target, '/v1/challenge', body);
|
|
38
|
+
}
|
|
39
|
+
async submitAnswers(challengeId, answers) {
|
|
40
|
+
const target = await this.router.getTarget();
|
|
41
|
+
return this.post(target, '/v1/submit', {
|
|
42
|
+
challenge_id: challengeId, answers,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async post(target, path, body) {
|
|
46
|
+
const fullUrl = `${target.url}${path}`;
|
|
47
|
+
// Build init separately so we only attach `dispatcher` when present.
|
|
48
|
+
// Workers/Bun/Deno don't have undici Agent, so dispatcher will be
|
|
49
|
+
// undefined and the global fetch is invoked with a clean RequestInit.
|
|
50
|
+
const init = {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify(body),
|
|
57
|
+
signal: AbortSignal.timeout(10000),
|
|
58
|
+
};
|
|
59
|
+
if (target.dispatcher)
|
|
60
|
+
init.dispatcher = target.dispatcher;
|
|
61
|
+
let res;
|
|
62
|
+
try {
|
|
63
|
+
res = await fetch(fullUrl, init);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
// Network failure / timeout — surface as failure to caller, mark endpoint
|
|
67
|
+
this.router.recordFailure(target.ip);
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
const text = await res.text();
|
|
71
|
+
let parsed = {};
|
|
72
|
+
if (text) {
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(text);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
parsed = { _raw: text };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
// 4xx = client/state error from VPS — do NOT mark endpoint as failed
|
|
82
|
+
// 5xx = server error — mark endpoint as failed for failover
|
|
83
|
+
if (res.status >= 500)
|
|
84
|
+
this.router.recordFailure(target.ip);
|
|
85
|
+
throw new VpsHttpError(res.status, parsed);
|
|
86
|
+
}
|
|
87
|
+
return parsed;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=vps-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vps-client.js","sourceRoot":"","sources":["../src/vps-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { IVpsRouter, RoutingTarget } from './types.js';
|
|
2
|
+
/** Multi-VPS discovery + IP-pinning router. Fetch endpoint list from cdn.fdkey.com, parallel-probe
|
|
3
|
+
* each IP via HEAD https://api.fdkey.com/health pinned to that IP, sort
|
|
4
|
+
* by (error count ASC, latency ASC), pick winner. Re-probe every hour or
|
|
5
|
+
* on `recordFailure(ip)`. */
|
|
6
|
+
export declare class VpsRouter implements IVpsRouter {
|
|
7
|
+
private readonly discoveryUrl;
|
|
8
|
+
private endpointCache;
|
|
9
|
+
private selectedIp;
|
|
10
|
+
private dispatchers;
|
|
11
|
+
private latencies;
|
|
12
|
+
private errorCounts;
|
|
13
|
+
private nextProbe;
|
|
14
|
+
constructor(discoveryUrl?: string);
|
|
15
|
+
getTarget(): Promise<RoutingTarget>;
|
|
16
|
+
recordFailure(ip: string | undefined): void;
|
|
17
|
+
/** Build (and cache) an undici Agent that pins all connections to `ip`
|
|
18
|
+
* while leaving SNI and cert validation to use the URL's hostname.
|
|
19
|
+
* Dispatchers are reused across calls — creating a new one per request
|
|
20
|
+
* would defeat the connection-pooling benefits. */
|
|
21
|
+
private dispatcherFor;
|
|
22
|
+
private refreshEndpoints;
|
|
23
|
+
private fetchEndpoints;
|
|
24
|
+
/** HEAD https://api.fdkey.com/health pinned per-IP. We use a per-call
|
|
25
|
+
* ad-hoc dispatcher (not the cached one) so a probe failure doesn't
|
|
26
|
+
* leave a soured connection in the pool. */
|
|
27
|
+
private probeAll;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=vps-router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vps-router.d.ts","sourceRoot":"","sources":["../src/vps-router.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAe,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAqCzE;;;8BAG8B;AAC9B,qBAAa,SAAU,YAAW,UAAU;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,SAAS,CAA6B;IAC9C,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,SAAS,CAAK;gBAEV,YAAY,CAAC,EAAE,MAAM;IAI3B,SAAS,IAAI,OAAO,CAAC,aAAa,CAAC;IAYzC,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAQ3C;;;wDAGoD;IACpD,OAAO,CAAC,aAAa;YAuBP,gBAAgB;YAsBhB,cAAc;IAmB5B;;iDAE6C;YAC/B,QAAQ;CAgCvB"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Agent, fetch } from 'undici';
|
|
2
|
+
const DEFAULT_DISCOVERY_URL = 'https://cdn.fdkey.com/endpoints.json';
|
|
3
|
+
/** All FDKEY VPSs serve TLS for this hostname. The SDK uses it as the SNI
|
|
4
|
+
* value when connecting to any IP from the discovery list — every box in
|
|
5
|
+
* the fleet holds a Let's Encrypt cert for this name (acquired via the
|
|
6
|
+
* DNS-01 challenge so multiple boxes can share the cert without
|
|
7
|
+
* fighting over HTTP-01). */
|
|
8
|
+
const FDKEY_API_HOSTNAME = 'api.fdkey.com';
|
|
9
|
+
const PROBE_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
10
|
+
const DISCOVERY_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
11
|
+
/** Multi-VPS discovery + IP-pinning router. Fetch endpoint list from cdn.fdkey.com, parallel-probe
|
|
12
|
+
* each IP via HEAD https://api.fdkey.com/health pinned to that IP, sort
|
|
13
|
+
* by (error count ASC, latency ASC), pick winner. Re-probe every hour or
|
|
14
|
+
* on `recordFailure(ip)`. */
|
|
15
|
+
export class VpsRouter {
|
|
16
|
+
discoveryUrl;
|
|
17
|
+
endpointCache = null;
|
|
18
|
+
selectedIp = null;
|
|
19
|
+
dispatchers = new Map();
|
|
20
|
+
latencies = new Map();
|
|
21
|
+
errorCounts = new Map();
|
|
22
|
+
nextProbe = 0;
|
|
23
|
+
constructor(discoveryUrl) {
|
|
24
|
+
this.discoveryUrl = discoveryUrl ?? DEFAULT_DISCOVERY_URL;
|
|
25
|
+
}
|
|
26
|
+
async getTarget() {
|
|
27
|
+
if (!this.selectedIp || Date.now() >= this.nextProbe) {
|
|
28
|
+
await this.refreshEndpoints();
|
|
29
|
+
}
|
|
30
|
+
const ip = this.selectedIp;
|
|
31
|
+
return {
|
|
32
|
+
url: `https://${FDKEY_API_HOSTNAME}`,
|
|
33
|
+
dispatcher: this.dispatcherFor(ip),
|
|
34
|
+
ip,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
recordFailure(ip) {
|
|
38
|
+
if (!ip)
|
|
39
|
+
return;
|
|
40
|
+
this.errorCounts.set(ip, (this.errorCounts.get(ip) ?? 0) + 1);
|
|
41
|
+
if (this.selectedIp === ip) {
|
|
42
|
+
this.selectedIp = null; // force re-selection next call
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Build (and cache) an undici Agent that pins all connections to `ip`
|
|
46
|
+
* while leaving SNI and cert validation to use the URL's hostname.
|
|
47
|
+
* Dispatchers are reused across calls — creating a new one per request
|
|
48
|
+
* would defeat the connection-pooling benefits. */
|
|
49
|
+
dispatcherFor(ip) {
|
|
50
|
+
let d = this.dispatchers.get(ip);
|
|
51
|
+
if (d)
|
|
52
|
+
return d;
|
|
53
|
+
const lookup = (_host, opts, cb) => {
|
|
54
|
+
if (opts.all) {
|
|
55
|
+
cb(null, [{ address: ip, family: 4 }]);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
cb(null, ip, 4);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
d = new Agent({
|
|
62
|
+
connect: {
|
|
63
|
+
// Hand undici a custom resolver: regardless of what hostname is
|
|
64
|
+
// being requested, return this IP. The TLS handshake still uses
|
|
65
|
+
// the URL's hostname for SNI + cert verification, which is what
|
|
66
|
+
// makes the IP-pin trick work.
|
|
67
|
+
lookup,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
this.dispatchers.set(ip, d);
|
|
71
|
+
return d;
|
|
72
|
+
}
|
|
73
|
+
async refreshEndpoints() {
|
|
74
|
+
const endpoints = await this.fetchEndpoints();
|
|
75
|
+
const active = endpoints.filter((e) => !e.deprecated);
|
|
76
|
+
if (active.length === 0) {
|
|
77
|
+
throw new Error('fdkey: no active VPS endpoints found in discovery list');
|
|
78
|
+
}
|
|
79
|
+
const results = await this.probeAll(active);
|
|
80
|
+
// Sort by (error_count ASC, latency ASC). Endpoints that didn't respond
|
|
81
|
+
// to the probe get latency = Infinity and rank last among equal error
|
|
82
|
+
// counts — but they're still candidates if everything else is dead.
|
|
83
|
+
results.sort((a, b) => {
|
|
84
|
+
const ea = this.errorCounts.get(a.ip) ?? 0;
|
|
85
|
+
const eb = this.errorCounts.get(b.ip) ?? 0;
|
|
86
|
+
if (ea !== eb)
|
|
87
|
+
return ea - eb;
|
|
88
|
+
return (a.latencyMs ?? Infinity) - (b.latencyMs ?? Infinity);
|
|
89
|
+
});
|
|
90
|
+
this.selectedIp = results[0].ip;
|
|
91
|
+
this.nextProbe = Date.now() + PROBE_INTERVAL_MS;
|
|
92
|
+
}
|
|
93
|
+
async fetchEndpoints() {
|
|
94
|
+
if (this.endpointCache &&
|
|
95
|
+
Date.now() - this.endpointCache.fetchedAt < DISCOVERY_CACHE_TTL_MS) {
|
|
96
|
+
return this.endpointCache.endpoints;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const res = await fetch(this.discoveryUrl, { signal: AbortSignal.timeout(5000) });
|
|
100
|
+
if (!res.ok)
|
|
101
|
+
throw new Error(`discovery fetch ${res.status}`);
|
|
102
|
+
const data = (await res.json());
|
|
103
|
+
this.endpointCache = { endpoints: data, fetchedAt: Date.now() };
|
|
104
|
+
return data;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
if (this.endpointCache)
|
|
108
|
+
return this.endpointCache.endpoints; // stale ok
|
|
109
|
+
throw new Error(`fdkey: cannot reach discovery URL and no cached endpoints: ${err}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** HEAD https://api.fdkey.com/health pinned per-IP. We use a per-call
|
|
113
|
+
* ad-hoc dispatcher (not the cached one) so a probe failure doesn't
|
|
114
|
+
* leave a soured connection in the pool. */
|
|
115
|
+
async probeAll(endpoints) {
|
|
116
|
+
return Promise.all(endpoints.map(async (e) => {
|
|
117
|
+
const start = Date.now();
|
|
118
|
+
try {
|
|
119
|
+
const probeLookup = (_h, opts, cb) => {
|
|
120
|
+
if (opts.all) {
|
|
121
|
+
cb(null, [{ address: e.ip, family: 4 }]);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
cb(null, e.ip, 4);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const probeAgent = new Agent({
|
|
128
|
+
connect: { lookup: probeLookup },
|
|
129
|
+
});
|
|
130
|
+
await fetch(`https://${FDKEY_API_HOSTNAME}/health`, {
|
|
131
|
+
method: 'HEAD',
|
|
132
|
+
signal: AbortSignal.timeout(3000),
|
|
133
|
+
dispatcher: probeAgent,
|
|
134
|
+
});
|
|
135
|
+
await probeAgent.close();
|
|
136
|
+
const latencyMs = Date.now() - start;
|
|
137
|
+
this.latencies.set(e.ip, latencyMs);
|
|
138
|
+
return { ip: e.ip, latencyMs };
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return { ip: e.ip, latencyMs: null };
|
|
142
|
+
}
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=vps-router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vps-router.js","sourceRoot":"","sources":["../src/vps-router.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type KeyLike } from 'jose';
|
|
2
|
+
import type { IVpsRouter } from './types.js';
|
|
3
|
+
/** Fetches and caches the public-key list at `${vpsBase}/.well-known/fdkey.json`.
|
|
4
|
+
* Goes through the VpsRouter so the request lands on the same IP currently
|
|
5
|
+
* serving production traffic (and uses the same dispatcher / SNI / cert
|
|
6
|
+
* handling — just like challenge/submit calls). */
|
|
7
|
+
export declare class WellKnownClient {
|
|
8
|
+
private readonly router;
|
|
9
|
+
private cache;
|
|
10
|
+
constructor(router: IVpsRouter);
|
|
11
|
+
getKey(kid: string): Promise<KeyLike | null>;
|
|
12
|
+
private refresh;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=well-known.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"well-known.d.ts","sourceRoot":"","sources":["../src/well-known.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,MAAM,CAAC;AAChD,OAAO,KAAK,EAAoB,UAAU,EAAE,MAAM,YAAY,CAAC;AAS/D;;;oDAGoD;AACpD,qBAAa,eAAe;IAGd,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,OAAO,CAAC,KAAK,CAAyB;gBAET,MAAM,EAAE,UAAU;IAEzC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;YAUpC,OAAO;CAmBtB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { importSPKI } from 'jose';
|
|
2
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
3
|
+
/** Fetches and caches the public-key list at `${vpsBase}/.well-known/fdkey.json`.
|
|
4
|
+
* Goes through the VpsRouter so the request lands on the same IP currently
|
|
5
|
+
* serving production traffic (and uses the same dispatcher / SNI / cert
|
|
6
|
+
* handling — just like challenge/submit calls). */
|
|
7
|
+
export class WellKnownClient {
|
|
8
|
+
router;
|
|
9
|
+
cache = null;
|
|
10
|
+
constructor(router) {
|
|
11
|
+
this.router = router;
|
|
12
|
+
}
|
|
13
|
+
async getKey(kid) {
|
|
14
|
+
if (this.cache && Date.now() - this.cache.fetchedAt < CACHE_TTL_MS) {
|
|
15
|
+
const k = this.cache.keys.get(kid);
|
|
16
|
+
if (k)
|
|
17
|
+
return k;
|
|
18
|
+
// kid not in cache — may have just rotated, refetch once
|
|
19
|
+
}
|
|
20
|
+
await this.refresh();
|
|
21
|
+
return this.cache.keys.get(kid) ?? null;
|
|
22
|
+
}
|
|
23
|
+
async refresh() {
|
|
24
|
+
const target = await this.router.getTarget();
|
|
25
|
+
const init = {
|
|
26
|
+
signal: AbortSignal.timeout(5000),
|
|
27
|
+
};
|
|
28
|
+
if (target.dispatcher)
|
|
29
|
+
init.dispatcher = target.dispatcher;
|
|
30
|
+
const res = await fetch(`${target.url}/.well-known/fdkey.json`, init);
|
|
31
|
+
if (!res.ok)
|
|
32
|
+
throw new Error(`fdkey: well-known fetch failed ${res.status}`);
|
|
33
|
+
const payload = (await res.json());
|
|
34
|
+
const keys = new Map();
|
|
35
|
+
for (const k of payload.keys) {
|
|
36
|
+
const key = await importSPKI(k.public_key_pem, k.alg);
|
|
37
|
+
keys.set(k.kid, key);
|
|
38
|
+
}
|
|
39
|
+
this.cache = { keys, fetchedAt: Date.now() };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=well-known.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"well-known.js","sourceRoot":"","sources":["../src/well-known.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAgB,MAAM,MAAM,CAAC;AAGhD,MAAM,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,SAAS;AAO9C;;;oDAGoD;AACpD,MAAM,OAAO,eAAe;IAGG;IAFrB,KAAK,GAAoB,IAAI,CAAC;IAEtC,YAA6B,MAAkB;QAAlB,WAAM,GAAN,MAAM,CAAY;IAAG,CAAC;IAEnD,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,YAAY,EAAE,CAAC;YACnE,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACnC,IAAI,CAAC;gBAAE,OAAO,CAAC,CAAC;YAChB,yDAAyD;QAC3D,CAAC;QACD,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACrB,OAAO,IAAI,CAAC,KAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;IAC3C,CAAC;IAEO,KAAK,CAAC,OAAO;QACnB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QAC7C,MAAM,IAAI,GAA2C;YACnD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;SAClC,CAAC;QACF,IAAI,MAAM,CAAC,UAAU;YAAE,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;QAC3D,MAAM,GAAG,GAAG,MAAM,KAAK,CACrB,GAAG,MAAM,CAAC,GAAG,yBAAyB,EACtC,IAAmB,CACpB,CAAC;QACF,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QAC7E,MAAM,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAqB,CAAC;QACvD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAmB,CAAC;QACxC,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YAC7B,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;YACtD,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACvB,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IAC/C,CAAC;CACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fdkey/mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "FDKEY verification middleware for MCP servers — gate AI-agent access behind LLM-only puzzles. Runs on Node 18+, Cloudflare Workers, Bun, and Deno.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsc --watch",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"prepublishOnly": "tsc"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"jose": "^5.10.0",
|
|
25
|
+
"zod": "^3.24.0"
|
|
26
|
+
},
|
|
27
|
+
"optionalDependencies": {
|
|
28
|
+
"undici": "^8.2.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
32
|
+
"@types/node": "^25.6.0",
|
|
33
|
+
"typescript": "^5.8.0",
|
|
34
|
+
"undici": "^8.2.0",
|
|
35
|
+
"vitest": "^2.1.0"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"README.md",
|
|
40
|
+
"CHANGELOG.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"keywords": [
|
|
44
|
+
"mcp",
|
|
45
|
+
"model-context-protocol",
|
|
46
|
+
"fdkey",
|
|
47
|
+
"captcha",
|
|
48
|
+
"verification",
|
|
49
|
+
"ai-agent",
|
|
50
|
+
"anti-bot",
|
|
51
|
+
"middleware",
|
|
52
|
+
"cloudflare-workers",
|
|
53
|
+
"edge"
|
|
54
|
+
],
|
|
55
|
+
"author": "FDKEY",
|
|
56
|
+
"license": "MIT",
|
|
57
|
+
"homepage": "https://fdkey.com",
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "git+https://github.com/fdkey/sdks.git",
|
|
61
|
+
"directory": "typescript"
|
|
62
|
+
},
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/fdkey/sdks/issues"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=18.17"
|
|
68
|
+
},
|
|
69
|
+
"publishConfig": {
|
|
70
|
+
"access": "public"
|
|
71
|
+
}
|
|
72
|
+
}
|