@hirey/hi-mcp-server 0.1.23 → 0.1.26
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 +20 -0
- package/dist/apiKey.d.ts +9 -0
- package/dist/apiKey.d.ts.map +1 -0
- package/dist/apiKey.js +51 -0
- package/dist/apiKeyExchange.d.ts +9 -0
- package/dist/apiKeyExchange.d.ts.map +1 -0
- package/dist/apiKeyExchange.js +65 -0
- package/dist/defaultReplyRoute.d.ts +4 -2
- package/dist/defaultReplyRoute.d.ts.map +1 -1
- package/dist/defaultReplyRoute.js +3 -0
- package/dist/installDefaults.d.ts +2 -1
- package/dist/installDefaults.d.ts.map +1 -1
- package/dist/installDefaults.js +14 -3
- package/dist/oauthRequestAuth.d.ts +67 -0
- package/dist/oauthRequestAuth.d.ts.map +1 -0
- package/dist/oauthRequestAuth.js +131 -0
- package/dist/receiver-command.d.ts +2 -1
- package/dist/receiver-command.d.ts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +970 -72
- package/dist/state.d.ts +2 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +77 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,20 @@
|
|
|
22
22
|
|
|
23
23
|
## 对外暴露的两类工具
|
|
24
24
|
|
|
25
|
+
默认 `HI_MCP_TOOL_SURFACE=full` 会暴露下面两类工具,适合 OpenClaw、Claude Code、Claude Desktop、本地 Codex MCP 等需要安装/诊断/事件消费能力的 MCP-first 宿主。
|
|
26
|
+
|
|
27
|
+
ChatGPT App / OpenAI Apps SDK 的 hosted OAuth 入口应使用 `/mcp/chatgpt`,或在专用部署里设置 `HI_MCP_TOOL_SURFACE=chatgpt_app`。这个 surface 不暴露 `hi_agent_*` 安装和事件控制面,只暴露一个小而明确的业务工具集,方便 ChatGPT 选择工具、降低审核解释成本,也避免把本地安装语义带进公共 App:
|
|
28
|
+
|
|
29
|
+
- `listing_taxonomy`
|
|
30
|
+
- `agent_listings`
|
|
31
|
+
- `matching_sessions`
|
|
32
|
+
- `pairings`
|
|
33
|
+
- `thread_meetings`
|
|
34
|
+
- `faq_search`
|
|
35
|
+
- `faq_get`
|
|
36
|
+
- `content_render`
|
|
37
|
+
- `content_get`
|
|
38
|
+
|
|
25
39
|
### 1. 控制面工具
|
|
26
40
|
|
|
27
41
|
这些工具都以 `hi_agent_*` 命名,保持和 gateway 控制面同一语义边界:
|
|
@@ -117,6 +131,11 @@ npm install -g @hirey/hi-mcp-server
|
|
|
117
131
|
- `HI_MCP_TRANSPORT`
|
|
118
132
|
- 可选。`http` 或 `stdio`。
|
|
119
133
|
- 默认 `http`。
|
|
134
|
+
- `HI_MCP_TOOL_SURFACE`
|
|
135
|
+
- 可选。`full` 或 `chatgpt_app`。
|
|
136
|
+
- 默认 `full`。
|
|
137
|
+
- `chatgpt_app` 只暴露 ChatGPT App 审核/用户体验需要的业务工具,并在 `tools/call` 路径同步拒绝隐藏工具。
|
|
138
|
+
- 在共享 hosted 服务里,推荐保留默认 `full`,并把 ChatGPT App 的 MCP URL 配成 `/mcp/chatgpt`;这样 `/mcp` 仍可服务 Codex/OpenClaw/Claude 的完整工具面。
|
|
120
139
|
- `HI_MCP_HOST`
|
|
121
140
|
- `http` 模式监听地址,默认 `127.0.0.1`。
|
|
122
141
|
- `HI_MCP_PORT`
|
|
@@ -163,6 +182,7 @@ hi-mcp-server
|
|
|
163
182
|
MCP endpoint:
|
|
164
183
|
|
|
165
184
|
- `POST /mcp`
|
|
185
|
+
- `POST /mcp/chatgpt`(ChatGPT App 专用裁剪工具面)
|
|
166
186
|
- `GET /healthz`
|
|
167
187
|
|
|
168
188
|
### Stdio
|
package/dist/apiKey.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const HI_API_KEY_PREFIX = "hi_ak_";
|
|
2
|
+
export type HiApiKeyMaterial = {
|
|
3
|
+
clientId: string;
|
|
4
|
+
clientSecret: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function isHiApiKey(token: unknown): boolean;
|
|
7
|
+
export declare function encodeHiApiKey(clientId: string, clientSecret: string): string;
|
|
8
|
+
export declare function decodeHiApiKey(key: unknown): HiApiKeyMaterial | null;
|
|
9
|
+
//# sourceMappingURL=apiKey.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiKey.d.ts","sourceRoot":"","sources":["../src/apiKey.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAE1C,MAAM,MAAM,gBAAgB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1E,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAElD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAM7E;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAapE"}
|
package/dist/apiKey.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Portable, revocable Hi API key for hosts that send a STATIC bearer to the
|
|
2
|
+
// remote /mcp endpoint instead of doing OAuth — e.g. Codex's
|
|
3
|
+
// `codex mcp add hi --url https://mcp.hirey.ai/mcp --bearer-token-env-var HI_API_KEY`.
|
|
4
|
+
//
|
|
5
|
+
// Why this exists: Codex persists its remote-MCP OAuth token in its OWN local
|
|
6
|
+
// store (~/.codex/.credentials.json) with a non-atomic write + token rotation.
|
|
7
|
+
// A crash/kill mid-write loses the file → the credential "mysteriously vanishes"
|
|
8
|
+
// and the user is forced to re-login (and lands on a fresh anonymous agent). An
|
|
9
|
+
// API key sidesteps that entirely: the credential is a NON-rotating
|
|
10
|
+
// client_credentials pair that lives in a user-controlled env var (never in
|
|
11
|
+
// Codex's fragile store), and is revocable by revoking the underlying client.
|
|
12
|
+
//
|
|
13
|
+
// Format: "hi_ak_" + base64url(JSON({ v:1, id:<client_id>, secret:<client_secret> })).
|
|
14
|
+
// The /mcp edge detects the prefix, exchanges the pair for a short-lived JWT via
|
|
15
|
+
// the standard client_credentials grant, and replays THAT JWT to downstream Hi
|
|
16
|
+
// APIs — so nothing downstream needs to learn about API keys; they keep seeing
|
|
17
|
+
// an ordinary access token. Revocation is free: revoke the OAuth client and the
|
|
18
|
+
// next exchange fails closed.
|
|
19
|
+
//
|
|
20
|
+
// This module is intentionally dependency-free (no SDK / network) so the codec
|
|
21
|
+
// is trivially unit-testable; the network exchange + cache lives in server.ts.
|
|
22
|
+
export const HI_API_KEY_PREFIX = 'hi_ak_';
|
|
23
|
+
export function isHiApiKey(token) {
|
|
24
|
+
return typeof token === 'string' && token.startsWith(HI_API_KEY_PREFIX) && token.length > HI_API_KEY_PREFIX.length;
|
|
25
|
+
}
|
|
26
|
+
export function encodeHiApiKey(clientId, clientSecret) {
|
|
27
|
+
const id = String(clientId || '').trim();
|
|
28
|
+
const secret = String(clientSecret || '').trim();
|
|
29
|
+
if (!id || !secret)
|
|
30
|
+
throw new Error('encodeHiApiKey: client_id and client_secret are required');
|
|
31
|
+
const payload = JSON.stringify({ v: 1, id, secret });
|
|
32
|
+
return HI_API_KEY_PREFIX + Buffer.from(payload, 'utf8').toString('base64url');
|
|
33
|
+
}
|
|
34
|
+
export function decodeHiApiKey(key) {
|
|
35
|
+
if (!isHiApiKey(key))
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
const json = Buffer.from(key.slice(HI_API_KEY_PREFIX.length), 'base64url').toString('utf8');
|
|
39
|
+
const obj = JSON.parse(json);
|
|
40
|
+
if (!obj || typeof obj !== 'object')
|
|
41
|
+
return null;
|
|
42
|
+
const clientId = String(obj.id || '').trim();
|
|
43
|
+
const clientSecret = String(obj.secret || '').trim();
|
|
44
|
+
if (!clientId || !clientSecret)
|
|
45
|
+
return null;
|
|
46
|
+
return { clientId, clientSecret };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type ApiKeyExchanger = (apiKey: string) => Promise<string | null>;
|
|
2
|
+
export declare function createApiKeyExchanger(opts: {
|
|
3
|
+
tokenUrl: string;
|
|
4
|
+
resource: string[];
|
|
5
|
+
now?: () => number;
|
|
6
|
+
fetchImpl?: typeof fetch;
|
|
7
|
+
maxEntries?: number;
|
|
8
|
+
}): ApiKeyExchanger;
|
|
9
|
+
//# sourceMappingURL=apiKeyExchange.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiKeyExchange.d.ts","sourceRoot":"","sources":["../src/apiKeyExchange.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAIzE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,eAAe,CA4ClB"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// /mcp edge exchanger for Hi API keys. When a host (e.g. Codex via
|
|
2
|
+
// `--bearer-token-env-var`) sends a static `hi_ak_...` bearer, the edge can't
|
|
3
|
+
// verify it as a JWT — it's a portable client_credentials pair. This module
|
|
4
|
+
// exchanges it for a SHORT-LIVED access token via the standard
|
|
5
|
+
// client_credentials grant (requesting the canonical MCP `resource` so the
|
|
6
|
+
// token's `aud` matches what the edge verifier enforces), caches the JWT until
|
|
7
|
+
// shortly before expiry, and hands it back so the edge can verify + replay it
|
|
8
|
+
// downstream exactly like a normal OAuth bearer.
|
|
9
|
+
//
|
|
10
|
+
// Fail-closed: any bad key / revoked client / auth outage returns null, which
|
|
11
|
+
// the caller turns into a 401 — never a partial / unauthenticated pass.
|
|
12
|
+
import { decodeHiApiKey, isHiApiKey } from './apiKey.js';
|
|
13
|
+
export function createApiKeyExchanger(opts) {
|
|
14
|
+
const cache = new Map();
|
|
15
|
+
const now = opts.now || (() => Date.now());
|
|
16
|
+
const doFetch = opts.fetchImpl || fetch;
|
|
17
|
+
const maxEntries = opts.maxEntries ?? 5000;
|
|
18
|
+
return async (apiKey) => {
|
|
19
|
+
if (!isHiApiKey(apiKey) || !opts.tokenUrl)
|
|
20
|
+
return null;
|
|
21
|
+
const cached = cache.get(apiKey);
|
|
22
|
+
if (cached && cached.expAt > now())
|
|
23
|
+
return cached.token;
|
|
24
|
+
const material = decodeHiApiKey(apiKey);
|
|
25
|
+
if (!material)
|
|
26
|
+
return null;
|
|
27
|
+
try {
|
|
28
|
+
const body = {
|
|
29
|
+
grant_type: 'client_credentials',
|
|
30
|
+
client_id: material.clientId,
|
|
31
|
+
client_secret: material.clientSecret,
|
|
32
|
+
};
|
|
33
|
+
// RFC 8707: bind the issued token's aud to the MCP resource so the edge
|
|
34
|
+
// verifier (which checks aud == its resource) accepts it.
|
|
35
|
+
if (opts.resource.length > 0)
|
|
36
|
+
body.resource = opts.resource;
|
|
37
|
+
const resp = await doFetch(opts.tokenUrl, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'content-type': 'application/json' },
|
|
40
|
+
body: JSON.stringify(body),
|
|
41
|
+
});
|
|
42
|
+
if (!resp.ok)
|
|
43
|
+
return null;
|
|
44
|
+
const json = await resp.json().catch(() => null);
|
|
45
|
+
const token = String(json?.access_token || '').trim();
|
|
46
|
+
if (!token)
|
|
47
|
+
return null;
|
|
48
|
+
const expiresIn = Number(json?.expires_in || 3600);
|
|
49
|
+
// Bound memory: drop expired entries, then hard-clear if still at cap.
|
|
50
|
+
if (cache.size >= maxEntries) {
|
|
51
|
+
for (const [k, v] of cache) {
|
|
52
|
+
if (v.expAt <= now())
|
|
53
|
+
cache.delete(k);
|
|
54
|
+
}
|
|
55
|
+
if (cache.size >= maxEntries)
|
|
56
|
+
cache.clear();
|
|
57
|
+
}
|
|
58
|
+
cache.set(apiKey, { token, expAt: now() + Math.max(30, expiresIn - 60) * 1000 });
|
|
59
|
+
return token;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type HostKind = 'openclaw' | 'generic' | 'codex' | 'claude_code' | 'claude_chat' | 'claude_cowork' | 'claude_desktop' | 'chatgpt_app';
|
|
2
|
+
export declare function isOpenClawHost(hostKind: HostKind): hostKind is 'openclaw';
|
|
1
3
|
export type DefaultReplyDeliveryContext = {
|
|
2
4
|
channel: string | null;
|
|
3
5
|
to: string | null;
|
|
@@ -5,7 +7,7 @@ export type DefaultReplyDeliveryContext = {
|
|
|
5
7
|
thread_id: string | null;
|
|
6
8
|
};
|
|
7
9
|
export declare function resolveInstallDefaultReplyDeliveryContext(args: {
|
|
8
|
-
hostKind:
|
|
10
|
+
hostKind: HostKind;
|
|
9
11
|
hasSessionKey: boolean;
|
|
10
12
|
defaultReplyChannel?: unknown;
|
|
11
13
|
defaultReplyTo?: unknown;
|
|
@@ -13,7 +15,7 @@ export declare function resolveInstallDefaultReplyDeliveryContext(args: {
|
|
|
13
15
|
defaultReplyThreadId?: unknown;
|
|
14
16
|
}): DefaultReplyDeliveryContext | null;
|
|
15
17
|
export declare function resolveInstallRouteMissingPolicy(args: {
|
|
16
|
-
hostKind:
|
|
18
|
+
hostKind: HostKind;
|
|
17
19
|
explicitRouteMissingPolicy?: unknown;
|
|
18
20
|
defaultReplyRoute?: Record<string, unknown> | null;
|
|
19
21
|
}): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaultReplyRoute.d.ts","sourceRoot":"","sources":["../src/defaultReplyRoute.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"defaultReplyRoute.d.ts","sourceRoot":"","sources":["../src/defaultReplyRoute.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,QAAQ,GAChB,UAAU,GACV,SAAS,GACT,OAAO,GACP,aAAa,GACb,aAAa,GACb,eAAe,GACf,gBAAgB,GAChB,aAAa,CAAC;AAElB,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,IAAI,UAAU,CAEzE;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,CAAC;AAEF,wBAAgB,yCAAyC,CAAC,IAAI,EAAE;IAC9D,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,OAAO,CAAC;IACvB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,GAAG,2BAA2B,GAAG,IAAI,CAqErC;AAED,wBAAgB,gCAAgC,CAAC,IAAI,EAAE;IACrD,QAAQ,EAAE,QAAQ,CAAC;IACnB,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACpD,GAAG;IACF,EAAE,EAAE,IAAI,CAAC;IACT,kBAAkB,EAAE,MAAM,CAAC;CAC5B,GAAG;IACF,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,sCAAsC,CAAC;CAC/C,CAcA"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
function normalizeText(input) {
|
|
2
2
|
return String(input || '').trim();
|
|
3
3
|
}
|
|
4
|
+
export function isOpenClawHost(hostKind) {
|
|
5
|
+
return hostKind === 'openclaw';
|
|
6
|
+
}
|
|
4
7
|
export function resolveInstallDefaultReplyDeliveryContext(args) {
|
|
5
8
|
const explicit = {
|
|
6
9
|
channel: normalizeText(args.defaultReplyChannel) || null,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { HostKind } from './defaultReplyRoute.js';
|
|
1
2
|
export declare const DEFAULT_OPENCLAW_INSTALL_DISPLAY_NAME = "OpenClaw Hi Agent";
|
|
2
3
|
export declare const DEFAULT_GENERIC_INSTALL_DISPLAY_NAME = "Hi Agent";
|
|
3
4
|
export declare function resolveInstallDisplayName(args: {
|
|
4
5
|
explicitDisplayName?: unknown;
|
|
5
|
-
hostKind:
|
|
6
|
+
hostKind: HostKind;
|
|
6
7
|
}): string;
|
|
7
8
|
//# sourceMappingURL=installDefaults.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installDefaults.d.ts","sourceRoot":"","sources":["../src/installDefaults.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"installDefaults.d.ts","sourceRoot":"","sources":["../src/installDefaults.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAMvD,eAAO,MAAM,qCAAqC,sBAAsB,CAAC;AACzE,eAAO,MAAM,oCAAoC,aAAa,CAAC;AAe/D,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,EAAE,QAAQ,CAAC;CACpB,UAIA"}
|
package/dist/installDefaults.js
CHANGED
|
@@ -3,11 +3,22 @@ function normalizeText(input) {
|
|
|
3
3
|
}
|
|
4
4
|
export const DEFAULT_OPENCLAW_INSTALL_DISPLAY_NAME = 'OpenClaw Hi Agent';
|
|
5
5
|
export const DEFAULT_GENERIC_INSTALL_DISPLAY_NAME = 'Hi Agent';
|
|
6
|
+
// Per-host defaults so the agent shows up in Hi's admin panel with a stable
|
|
7
|
+
// human-readable label that matches the host the user actually onboarded
|
|
8
|
+
// from. None of these carry openclaw-style install semantics; only the
|
|
9
|
+
// display name changes.
|
|
10
|
+
const HOST_KIND_DISPLAY_NAME_DEFAULTS = {
|
|
11
|
+
openclaw: DEFAULT_OPENCLAW_INSTALL_DISPLAY_NAME,
|
|
12
|
+
codex: 'Codex Hi Agent',
|
|
13
|
+
claude_code: 'Claude Code Hi Agent',
|
|
14
|
+
claude_chat: 'Claude Hi Agent',
|
|
15
|
+
claude_cowork: 'Cowork Hi Agent',
|
|
16
|
+
claude_desktop: 'Claude Desktop Hi Agent',
|
|
17
|
+
chatgpt_app: 'ChatGPT Hi Agent',
|
|
18
|
+
};
|
|
6
19
|
export function resolveInstallDisplayName(args) {
|
|
7
20
|
const explicitDisplayName = normalizeText(args.explicitDisplayName);
|
|
8
21
|
if (explicitDisplayName)
|
|
9
22
|
return explicitDisplayName;
|
|
10
|
-
return args.hostKind
|
|
11
|
-
? DEFAULT_OPENCLAW_INSTALL_DISPLAY_NAME
|
|
12
|
-
: DEFAULT_GENERIC_INSTALL_DISPLAY_NAME;
|
|
23
|
+
return HOST_KIND_DISPLAY_NAME_DEFAULTS[args.hostKind] || DEFAULT_GENERIC_INSTALL_DISPLAY_NAME;
|
|
13
24
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type express from 'express';
|
|
3
|
+
import { type JWTPayload, type JWTVerifyResult } from 'jose';
|
|
4
|
+
export type RequestAuth = {
|
|
5
|
+
bearer: string;
|
|
6
|
+
subjectId: string;
|
|
7
|
+
claims: JWTPayload;
|
|
8
|
+
};
|
|
9
|
+
export declare const requestAuthStorage: AsyncLocalStorage<RequestAuth | null>;
|
|
10
|
+
export declare function getCurrentRequestAuth(): RequestAuth | null;
|
|
11
|
+
export type OAuthVerifierConfig = {
|
|
12
|
+
issuer: string;
|
|
13
|
+
audience: string | string[];
|
|
14
|
+
jwksUri?: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function buildOAuthVerifier(config: OAuthVerifierConfig): (token: string) => Promise<JWTVerifyResult<JWTPayload>>;
|
|
17
|
+
export declare function extractBearer(req: express.Request): string | null;
|
|
18
|
+
export declare function buildWwwAuthenticateChallenge(input: {
|
|
19
|
+
protectedResourceMetadataUrl: string;
|
|
20
|
+
scope?: string[];
|
|
21
|
+
error?: 'invalid_token' | 'insufficient_scope';
|
|
22
|
+
errorDescription?: string;
|
|
23
|
+
}): string;
|
|
24
|
+
export type ProtectedResourceMetadata = {
|
|
25
|
+
resource: string;
|
|
26
|
+
authorization_servers: string[];
|
|
27
|
+
scopes_supported: string[];
|
|
28
|
+
bearer_methods_supported: ['header'];
|
|
29
|
+
resource_documentation?: string;
|
|
30
|
+
};
|
|
31
|
+
export type AuthorizationServerMetadataAlias = {
|
|
32
|
+
issuer: string;
|
|
33
|
+
authorization_endpoint: string;
|
|
34
|
+
token_endpoint: string;
|
|
35
|
+
registration_endpoint: string;
|
|
36
|
+
jwks_uri: string;
|
|
37
|
+
response_types_supported: ['code'];
|
|
38
|
+
response_modes_supported: ['query'];
|
|
39
|
+
grant_types_supported: ['authorization_code', 'refresh_token', 'client_credentials'];
|
|
40
|
+
token_endpoint_auth_methods_supported: ['none', 'client_secret_basic', 'client_secret_post'];
|
|
41
|
+
code_challenge_methods_supported: ['S256'];
|
|
42
|
+
scopes_supported: string[];
|
|
43
|
+
resource_indicators_supported: true;
|
|
44
|
+
service_documentation?: string;
|
|
45
|
+
};
|
|
46
|
+
export declare function buildAuthorizationServerMetadataAlias(input: {
|
|
47
|
+
issuer: string;
|
|
48
|
+
authorizationServer: string;
|
|
49
|
+
scopes: string[];
|
|
50
|
+
serviceDocumentationUrl?: string;
|
|
51
|
+
}): AuthorizationServerMetadataAlias;
|
|
52
|
+
export declare function buildProtectedResourceMetadata(input: {
|
|
53
|
+
resource: string;
|
|
54
|
+
authorizationServer: string;
|
|
55
|
+
scopes: string[];
|
|
56
|
+
resourceDocumentationUrl?: string;
|
|
57
|
+
}): ProtectedResourceMetadata;
|
|
58
|
+
export type BearerVerifyOutcome = {
|
|
59
|
+
ok: true;
|
|
60
|
+
auth: RequestAuth;
|
|
61
|
+
} | {
|
|
62
|
+
ok: false;
|
|
63
|
+
error: 'invalid_token';
|
|
64
|
+
description: string;
|
|
65
|
+
};
|
|
66
|
+
export declare function verifyRequestBearer(req: express.Request, verifier: (token: string) => Promise<JWTVerifyResult<JWTPayload>>, apiKeyExchanger?: (apiKey: string) => Promise<string | null>): Promise<BearerVerifyOutcome>;
|
|
67
|
+
//# sourceMappingURL=oauthRequestAuth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauthRequestAuth.d.ts","sourceRoot":"","sources":["../src/oauthRequestAuth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
3
|
+
import { HI_API_KEY_PREFIX } from './apiKey.js';
|
|
4
|
+
function normalizeText(input) {
|
|
5
|
+
return String(input || '').trim();
|
|
6
|
+
}
|
|
7
|
+
// Per-request identity for the multi-tenant HTTP /mcp surface. Tool handlers
|
|
8
|
+
// pull the bearer out of this storage instead of loading a disk-resident
|
|
9
|
+
// identity, so the same hi-mcp-server process can serve any number of
|
|
10
|
+
// Codex / Claude Code installs concurrently.
|
|
11
|
+
export const requestAuthStorage = new AsyncLocalStorage();
|
|
12
|
+
export function getCurrentRequestAuth() {
|
|
13
|
+
return requestAuthStorage.getStore() || null;
|
|
14
|
+
}
|
|
15
|
+
function trimTrailingSlash(value) {
|
|
16
|
+
return value.replace(/\/+$/, '');
|
|
17
|
+
}
|
|
18
|
+
export function buildOAuthVerifier(config) {
|
|
19
|
+
const issuer = trimTrailingSlash(config.issuer);
|
|
20
|
+
const audience = config.audience;
|
|
21
|
+
const jwksUri = config.jwksUri || `${issuer}/.well-known/jwks.json`;
|
|
22
|
+
const jwks = createRemoteJWKSet(new URL(jwksUri), {
|
|
23
|
+
cacheMaxAge: 5 * 60_000,
|
|
24
|
+
cooldownDuration: 30_000,
|
|
25
|
+
});
|
|
26
|
+
return async function verify(token) {
|
|
27
|
+
return await jwtVerify(token, jwks, {
|
|
28
|
+
issuer,
|
|
29
|
+
audience,
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function extractBearer(req) {
|
|
34
|
+
const header = String(req.headers.authorization || '');
|
|
35
|
+
if (!header)
|
|
36
|
+
return null;
|
|
37
|
+
const lower = header.toLowerCase();
|
|
38
|
+
if (!lower.startsWith('bearer '))
|
|
39
|
+
return null;
|
|
40
|
+
const token = header.slice('bearer '.length).trim();
|
|
41
|
+
return token || null;
|
|
42
|
+
}
|
|
43
|
+
// RFC 6750 / RFC 9728 — the 401 response challenge that points
|
|
44
|
+
// MCP clients at our oauth-protected-resource metadata. Codex / Claude
|
|
45
|
+
// Code parse this exact header to start their auto-discovery flow.
|
|
46
|
+
export function buildWwwAuthenticateChallenge(input) {
|
|
47
|
+
const parts = [];
|
|
48
|
+
if (input.error) {
|
|
49
|
+
parts.push(`error="${input.error}"`);
|
|
50
|
+
if (input.errorDescription) {
|
|
51
|
+
parts.push(`error_description="${input.errorDescription.replace(/"/g, '\\"')}"`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (input.scope && input.scope.length > 0) {
|
|
55
|
+
parts.push(`scope="${input.scope.join(' ')}"`);
|
|
56
|
+
}
|
|
57
|
+
parts.push(`resource_metadata="${input.protectedResourceMetadataUrl}"`);
|
|
58
|
+
return `Bearer ${parts.join(', ')}`;
|
|
59
|
+
}
|
|
60
|
+
// Compatibility alias for clients that still probe RFC 8414 authorization
|
|
61
|
+
// metadata on the MCP resource host before trying RFC 9728 protected-resource
|
|
62
|
+
// discovery. The canonical metadata is served by hi-auth; this mirrors the
|
|
63
|
+
// same endpoints without making the MCP server an authorization server.
|
|
64
|
+
export function buildAuthorizationServerMetadataAlias(input) {
|
|
65
|
+
const base = trimTrailingSlash(input.authorizationServer);
|
|
66
|
+
return {
|
|
67
|
+
issuer: trimTrailingSlash(input.issuer),
|
|
68
|
+
authorization_endpoint: `${base}/oauth/authorize`,
|
|
69
|
+
token_endpoint: `${base}/oauth/token`,
|
|
70
|
+
registration_endpoint: `${base}/oauth/register`,
|
|
71
|
+
jwks_uri: `${base}/.well-known/jwks.json`,
|
|
72
|
+
response_types_supported: ['code'],
|
|
73
|
+
response_modes_supported: ['query'],
|
|
74
|
+
grant_types_supported: ['authorization_code', 'refresh_token', 'client_credentials'],
|
|
75
|
+
token_endpoint_auth_methods_supported: ['none', 'client_secret_basic', 'client_secret_post'],
|
|
76
|
+
code_challenge_methods_supported: ['S256'],
|
|
77
|
+
scopes_supported: input.scopes,
|
|
78
|
+
resource_indicators_supported: true,
|
|
79
|
+
...(input.serviceDocumentationUrl ? { service_documentation: input.serviceDocumentationUrl } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// RFC 9728 §3 — minimum required surface: `resource` (canonical URI, exact
|
|
83
|
+
// string that tokens must `aud`-match), `authorization_servers` (so MCP
|
|
84
|
+
// clients know where to discover AS metadata). Everything else is optional.
|
|
85
|
+
export function buildProtectedResourceMetadata(input) {
|
|
86
|
+
return {
|
|
87
|
+
resource: input.resource,
|
|
88
|
+
authorization_servers: [trimTrailingSlash(input.authorizationServer)],
|
|
89
|
+
scopes_supported: input.scopes,
|
|
90
|
+
bearer_methods_supported: ['header'],
|
|
91
|
+
...(input.resourceDocumentationUrl ? { resource_documentation: input.resourceDocumentationUrl } : {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export async function verifyRequestBearer(req, verifier,
|
|
95
|
+
// Optional: when the inbound bearer is a Hi API key (hi_ak_...) instead of a
|
|
96
|
+
// JWT, exchange it for a short-lived access token first, then verify + replay
|
|
97
|
+
// THAT downstream. Lets static-bearer hosts (Codex --bearer-token-env-var) use
|
|
98
|
+
// a non-rotating, revocable credential that never touches their fragile local
|
|
99
|
+
// OAuth store. When omitted, only JWT bearers are accepted (unchanged).
|
|
100
|
+
apiKeyExchanger) {
|
|
101
|
+
let bearer = extractBearer(req);
|
|
102
|
+
if (!bearer) {
|
|
103
|
+
return { ok: false, error: 'invalid_token', description: 'missing_bearer' };
|
|
104
|
+
}
|
|
105
|
+
if (apiKeyExchanger && bearer.startsWith(HI_API_KEY_PREFIX)) {
|
|
106
|
+
const exchanged = await apiKeyExchanger(bearer);
|
|
107
|
+
if (!exchanged) {
|
|
108
|
+
return { ok: false, error: 'invalid_token', description: 'api_key_invalid_or_revoked' };
|
|
109
|
+
}
|
|
110
|
+
bearer = exchanged;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const { payload } = await verifier(bearer);
|
|
114
|
+
const subjectId = normalizeText(payload.sub);
|
|
115
|
+
if (!subjectId) {
|
|
116
|
+
return { ok: false, error: 'invalid_token', description: 'missing_sub' };
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
auth: {
|
|
121
|
+
bearer,
|
|
122
|
+
subjectId,
|
|
123
|
+
claims: payload,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
129
|
+
return { ok: false, error: 'invalid_token', description: message };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import type { HostKind } from './defaultReplyRoute.js';
|
|
1
2
|
export declare function resolveCanonicalOpenClawReceiverBinaryPath(homeDir?: string): string;
|
|
2
3
|
export declare function buildInstallReceiverCommandArgv(args: {
|
|
3
4
|
explicitArgv: string[];
|
|
4
5
|
receiverCommand: string;
|
|
5
|
-
hostKind:
|
|
6
|
+
hostKind: HostKind;
|
|
6
7
|
enableLocalReceiver: boolean;
|
|
7
8
|
homeDir?: string;
|
|
8
9
|
}): string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"receiver-command.d.ts","sourceRoot":"","sources":["../src/receiver-command.ts"],"names":[],"mappings":"AAGA,wBAAgB,0CAA0C,CAAC,OAAO,GAAE,MAAqB,GAAG,MAAM,CAEjG;AAED,wBAAgB,+BAA+B,CAAC,IAAI,EAAE;IACpD,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"receiver-command.d.ts","sourceRoot":"","sources":["../src/receiver-command.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAEvD,wBAAgB,0CAA0C,CAAC,OAAO,GAAE,MAAqB,GAAG,MAAM,CAEjG;AAED,wBAAgB,+BAA+B,CAAC,IAAI,EAAE;IACpD,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,QAAQ,CAAC;IACnB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,EAAE,CAmBX"}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AAgDA,OAAO,EASL,KAAK,6BAA6B,EAMnC,MAAM,YAAY,CAAC;AAsrBpB,wBAAgB,oBAAoB,yCAEnC"}
|