@agent-native/core 0.18.0 → 0.19.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/README.md +1 -11
- package/dist/a2a/client.d.ts +7 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +3 -0
- package/dist/a2a/client.js.map +1 -1
- package/dist/cli/connect.d.ts +94 -0
- package/dist/cli/connect.d.ts.map +1 -0
- package/dist/cli/connect.js +443 -0
- package/dist/cli/connect.js.map +1 -0
- package/dist/cli/index.js +16 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-config-writers.d.ts +71 -0
- package/dist/cli/mcp-config-writers.d.ts.map +1 -0
- package/dist/cli/mcp-config-writers.js +210 -0
- package/dist/cli/mcp-config-writers.js.map +1 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +11 -63
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts +6 -1
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +5 -4
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +6 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +20 -10
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/conversation/AgentConversation.d.ts +18 -0
- package/dist/client/conversation/AgentConversation.d.ts.map +1 -0
- package/dist/client/conversation/AgentConversation.js +94 -0
- package/dist/client/conversation/AgentConversation.js.map +1 -0
- package/dist/client/conversation/AgentConversation.spec.d.ts +2 -0
- package/dist/client/conversation/AgentConversation.spec.d.ts.map +1 -0
- package/dist/client/conversation/AgentConversation.spec.js +69 -0
- package/dist/client/conversation/AgentConversation.spec.js.map +1 -0
- package/dist/client/conversation/index.d.ts +4 -0
- package/dist/client/conversation/index.d.ts.map +1 -0
- package/dist/client/conversation/index.js +3 -0
- package/dist/client/conversation/index.js.map +1 -0
- package/dist/client/conversation/types.d.ts +54 -0
- package/dist/client/conversation/types.d.ts.map +1 -0
- package/dist/client/conversation/types.js +2 -0
- package/dist/client/conversation/types.js.map +1 -0
- package/dist/client/conversation/use-near-bottom-autoscroll.d.ts +15 -0
- package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -0
- package/dist/client/conversation/use-near-bottom-autoscroll.js +66 -0
- package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/resources/ResourceTree.d.ts.map +1 -1
- package/dist/client/resources/ResourceTree.js +2 -2
- package/dist/client/resources/ResourceTree.js.map +1 -1
- package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
- package/dist/client/resources/ResourcesPanel.js +4 -28
- package/dist/client/resources/ResourcesPanel.js.map +1 -1
- package/dist/code-agents/index.d.ts +1 -0
- package/dist/code-agents/index.d.ts.map +1 -1
- package/dist/code-agents/index.js +1 -0
- package/dist/code-agents/index.js.map +1 -1
- package/dist/code-agents/transcript-normalizer.d.ts +50 -0
- package/dist/code-agents/transcript-normalizer.d.ts.map +1 -0
- package/dist/code-agents/transcript-normalizer.js +356 -0
- package/dist/code-agents/transcript-normalizer.js.map +1 -0
- package/dist/extensions/schema.d.ts +1 -1
- package/dist/mcp/build-server.d.ts +20 -3
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +120 -15
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/builtin-tools.d.ts +8 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +115 -13
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/mcp/connect-route.d.ts +43 -0
- package/dist/mcp/connect-route.d.ts.map +1 -0
- package/dist/mcp/connect-route.js +638 -0
- package/dist/mcp/connect-route.js.map +1 -0
- package/dist/mcp/connect-store.d.ts +132 -0
- package/dist/mcp/connect-store.d.ts.map +1 -0
- package/dist/mcp/connect-store.js +434 -0
- package/dist/mcp/connect-store.js.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +23 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/stdio.d.ts.map +1 -1
- package/dist/mcp/stdio.js +1 -0
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +1 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/auth.d.ts +17 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +192 -49
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts +43 -0
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +25 -0
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +12 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +42 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/identity-sso-store.d.ts +86 -0
- package/dist/server/identity-sso-store.d.ts.map +1 -0
- package/dist/server/identity-sso-store.js +243 -0
- package/dist/server/identity-sso-store.js.map +1 -0
- package/dist/server/identity-sso.d.ts +78 -0
- package/dist/server/identity-sso.d.ts.map +1 -0
- package/dist/server/identity-sso.js +425 -0
- package/dist/server/identity-sso.js.map +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +2 -1
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/server/open-route.d.ts.map +1 -1
- package/dist/server/open-route.js +36 -5
- package/dist/server/open-route.js.map +1 -1
- package/dist/server/request-context.d.ts +8 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/sharing/schema.d.ts +1 -1
- package/docs/content/code-agents-ui.md +14 -3
- package/docs/content/cross-app-sso.md +118 -0
- package/docs/content/external-agents.md +130 -51
- package/docs/content/migration-workbench.md +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-table store for the "connect external agents" feature.
|
|
3
|
+
*
|
|
4
|
+
* Two additive, dialect-agnostic tables back the browser **Connect** page and
|
|
5
|
+
* the OAuth-style **device-code flow** a CLI drives:
|
|
6
|
+
*
|
|
7
|
+
* - `mcp_connect_tokens` — one row per minted MCP token. We never store the
|
|
8
|
+
* token value (it's a signed JWT); only its `jti` so revocation is a
|
|
9
|
+
* SQL lookup. Revoking sets `revoked_at`; the row is never deleted.
|
|
10
|
+
* - `mcp_device_codes` — short-lived (10 min) device/user code pairs for
|
|
11
|
+
* the OAuth 2.0 device-authorization-style CLI flow. Single-use
|
|
12
|
+
* (`consumed_at`), rate-limited at creation.
|
|
13
|
+
*
|
|
14
|
+
* Mirrors `application-state/store.ts`: lazy `ensureTable()`, `getDbExec()`,
|
|
15
|
+
* `isPostgres()` dialect branching for upserts, `isConnectionError()` swallow
|
|
16
|
+
* so a transient Neon WS drop never 500s. `CREATE TABLE IF NOT EXISTS` only —
|
|
17
|
+
* strictly additive, never DROP / ALTER (shared prod DB rule).
|
|
18
|
+
*/
|
|
19
|
+
import { getDbExec, isConnectionError, intType, } from "../db/client.js";
|
|
20
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
21
|
+
let _initPromise;
|
|
22
|
+
/**
|
|
23
|
+
* Scope claim that marks a connect-minted token (vs. an ordinary A2A
|
|
24
|
+
* delegation JWT). Only tokens carrying this scope go through the revoke
|
|
25
|
+
* lookup in `verifyAuth` — defined here so both `connect-route.ts` and
|
|
26
|
+
* `build-server.ts` import it from the leaf store without a cycle.
|
|
27
|
+
*/
|
|
28
|
+
export const MCP_CONNECT_SCOPE = "mcp-connect";
|
|
29
|
+
/** Device codes are valid for 10 minutes. */
|
|
30
|
+
export const DEVICE_CODE_TTL_MS = 10 * 60_000;
|
|
31
|
+
/** Default minted-token lifetime. Configurable per-request 1–365 days. */
|
|
32
|
+
export const DEFAULT_TOKEN_TTL_DAYS = 90;
|
|
33
|
+
export const MIN_TOKEN_TTL_DAYS = 1;
|
|
34
|
+
export const MAX_TOKEN_TTL_DAYS = 365;
|
|
35
|
+
/**
|
|
36
|
+
* Rate limit for `device/start`: at most this many device codes may be created
|
|
37
|
+
* within `DEVICE_START_WINDOW_MS`. Unauthenticated endpoint — keep it tight so
|
|
38
|
+
* a hostile client can't flood the table or brute-force user codes.
|
|
39
|
+
*/
|
|
40
|
+
export const DEVICE_START_MAX = 20;
|
|
41
|
+
export const DEVICE_START_WINDOW_MS = 60_000;
|
|
42
|
+
async function ensureTable() {
|
|
43
|
+
if (!_initPromise) {
|
|
44
|
+
_initPromise = (async () => {
|
|
45
|
+
const client = getDbExec();
|
|
46
|
+
// Additive only. Never DROP / ALTER — this DB is shared across every
|
|
47
|
+
// deploy context (preview/branch/prod) for hosted templates.
|
|
48
|
+
await client.execute(`
|
|
49
|
+
CREATE TABLE IF NOT EXISTS mcp_connect_tokens (
|
|
50
|
+
id TEXT PRIMARY KEY,
|
|
51
|
+
jti TEXT UNIQUE NOT NULL,
|
|
52
|
+
owner_email TEXT NOT NULL,
|
|
53
|
+
org_id TEXT,
|
|
54
|
+
label TEXT,
|
|
55
|
+
created_at ${intType()},
|
|
56
|
+
last_used_at ${intType()},
|
|
57
|
+
revoked_at ${intType()}
|
|
58
|
+
)
|
|
59
|
+
`);
|
|
60
|
+
await client.execute(`
|
|
61
|
+
CREATE TABLE IF NOT EXISTS mcp_device_codes (
|
|
62
|
+
device_code TEXT PRIMARY KEY,
|
|
63
|
+
user_code TEXT NOT NULL,
|
|
64
|
+
owner_email TEXT,
|
|
65
|
+
org_id TEXT,
|
|
66
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
67
|
+
token_jti TEXT,
|
|
68
|
+
created_at ${intType()},
|
|
69
|
+
expires_at ${intType()},
|
|
70
|
+
consumed_at ${intType()}
|
|
71
|
+
)
|
|
72
|
+
`);
|
|
73
|
+
})().catch((err) => {
|
|
74
|
+
// Don't cache a rejected init. A transient DB blip should let the next
|
|
75
|
+
// connect/mint/revoke call retry rather than wedging the process.
|
|
76
|
+
_initPromise = undefined;
|
|
77
|
+
throw err;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return _initPromise;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Persist a record of a minted token. The token value itself (a signed JWT)
|
|
84
|
+
* is NEVER stored — only its `jti`, so revocation is a cheap SQL lookup.
|
|
85
|
+
*/
|
|
86
|
+
export async function recordMintedToken(params) {
|
|
87
|
+
await ensureTable();
|
|
88
|
+
const client = getDbExec();
|
|
89
|
+
const id = randomUUID();
|
|
90
|
+
await client.execute({
|
|
91
|
+
sql: `INSERT INTO mcp_connect_tokens (id, jti, owner_email, org_id, label, created_at, last_used_at, revoked_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
92
|
+
args: [
|
|
93
|
+
id,
|
|
94
|
+
params.jti,
|
|
95
|
+
params.ownerEmail,
|
|
96
|
+
params.orgId ?? null,
|
|
97
|
+
params.label ?? null,
|
|
98
|
+
Date.now(),
|
|
99
|
+
null,
|
|
100
|
+
null,
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
return id;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Returns true when the given `jti` corresponds to a token that has been
|
|
107
|
+
* revoked. Fails OPEN on a store/DB error: a transient Neon WS drop must not
|
|
108
|
+
* lock every connected agent out. Signature verification is unaffected — this
|
|
109
|
+
* is only the post-verify revoke check (see `verifyAuth` in build-server.ts).
|
|
110
|
+
*/
|
|
111
|
+
export async function isJtiRevoked(jti) {
|
|
112
|
+
try {
|
|
113
|
+
await ensureTable();
|
|
114
|
+
const client = getDbExec();
|
|
115
|
+
const { rows } = await client.execute({
|
|
116
|
+
sql: `SELECT revoked_at FROM mcp_connect_tokens WHERE jti = ?`,
|
|
117
|
+
args: [jti],
|
|
118
|
+
});
|
|
119
|
+
if (rows.length === 0)
|
|
120
|
+
return false;
|
|
121
|
+
const revokedAt = rows[0].revoked_at ?? rows[0].revokedAt;
|
|
122
|
+
return revokedAt != null;
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
// Fail open: a DB blip must not turn every minted token into a 401.
|
|
126
|
+
// (Signature checks already passed; this only gates explicit revokes.)
|
|
127
|
+
if (isConnectionError(err))
|
|
128
|
+
return false;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export async function listTokens(ownerEmail) {
|
|
133
|
+
try {
|
|
134
|
+
await ensureTable();
|
|
135
|
+
const client = getDbExec();
|
|
136
|
+
const { rows } = await client.execute({
|
|
137
|
+
sql: `SELECT id, jti, owner_email, org_id, label, created_at, last_used_at, revoked_at FROM mcp_connect_tokens WHERE owner_email = ? ORDER BY created_at DESC`,
|
|
138
|
+
args: [ownerEmail],
|
|
139
|
+
});
|
|
140
|
+
return rows.map((r) => ({
|
|
141
|
+
id: r.id,
|
|
142
|
+
jti: r.jti,
|
|
143
|
+
ownerEmail: (r.owner_email ?? r.ownerEmail),
|
|
144
|
+
orgId: (r.org_id ?? r.orgId ?? null),
|
|
145
|
+
label: (r.label ?? null),
|
|
146
|
+
createdAt: numOrNull(r.created_at ?? r.createdAt),
|
|
147
|
+
lastUsedAt: numOrNull(r.last_used_at ?? r.lastUsedAt),
|
|
148
|
+
revokedAt: numOrNull(r.revoked_at ?? r.revokedAt),
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
if (isConnectionError(err))
|
|
153
|
+
return [];
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Revoke a token, but ONLY if it is owned by `ownerEmail` (the caller). The
|
|
159
|
+
* `owner_email = ?` predicate is the access scope — a caller can never revoke
|
|
160
|
+
* another user's token. Idempotent: re-revoking keeps the first timestamp.
|
|
161
|
+
* Returns true when a row was actually transitioned to revoked.
|
|
162
|
+
*/
|
|
163
|
+
export async function revokeToken(ownerEmail, id) {
|
|
164
|
+
await ensureTable();
|
|
165
|
+
const client = getDbExec();
|
|
166
|
+
const result = await client.execute({
|
|
167
|
+
sql: `UPDATE mcp_connect_tokens SET revoked_at = ? WHERE id = ? AND owner_email = ? AND revoked_at IS NULL`,
|
|
168
|
+
args: [Date.now(), id, ownerEmail],
|
|
169
|
+
});
|
|
170
|
+
return result.rowsAffected > 0;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Best-effort: stamp `last_used_at` for a token. Swallows all errors — this is
|
|
174
|
+
* pure telemetry and must never affect the auth path.
|
|
175
|
+
*/
|
|
176
|
+
export async function touchTokenUsed(jti) {
|
|
177
|
+
try {
|
|
178
|
+
await ensureTable();
|
|
179
|
+
const client = getDbExec();
|
|
180
|
+
await client.execute({
|
|
181
|
+
sql: `UPDATE mcp_connect_tokens SET last_used_at = ? WHERE jti = ?`,
|
|
182
|
+
args: [Date.now(), jti],
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// last_used_at is informational only — never throw from the hot path.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const USER_CODE_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; // Crockford-ish base32, no 0/1/O/I
|
|
190
|
+
/** Crypto-random short human-typable code, formatted `XXXX-XXXX`. */
|
|
191
|
+
function generateUserCode() {
|
|
192
|
+
const bytes = randomBytes(8);
|
|
193
|
+
let out = "";
|
|
194
|
+
for (let i = 0; i < 8; i++) {
|
|
195
|
+
out += USER_CODE_ALPHABET[bytes[i] % USER_CODE_ALPHABET.length];
|
|
196
|
+
if (i === 3)
|
|
197
|
+
out += "-";
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
function generateDeviceCode() {
|
|
202
|
+
return randomBytes(32).toString("base64url");
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Create a new device+user code pair. Rate-limited: at most
|
|
206
|
+
* `DEVICE_START_MAX` codes within `DEVICE_START_WINDOW_MS`. The window count
|
|
207
|
+
* is a coarse global cap (this endpoint is unauthenticated) — enough to stop
|
|
208
|
+
* table flooding / user-code brute force without per-IP plumbing.
|
|
209
|
+
*
|
|
210
|
+
* Throws `RATE_LIMITED` when the cap is exceeded so the route can map it to a
|
|
211
|
+
* 429.
|
|
212
|
+
*/
|
|
213
|
+
export async function createDeviceCode() {
|
|
214
|
+
await ensureTable();
|
|
215
|
+
const client = getDbExec();
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
try {
|
|
218
|
+
const { rows } = await client.execute({
|
|
219
|
+
sql: `SELECT COUNT(*) AS n FROM mcp_device_codes WHERE created_at > ?`,
|
|
220
|
+
args: [now - DEVICE_START_WINDOW_MS],
|
|
221
|
+
});
|
|
222
|
+
const n = Number(rows[0]?.n ?? rows[0]?.["COUNT(*)"] ?? 0);
|
|
223
|
+
if (Number.isFinite(n) && n >= DEVICE_START_MAX) {
|
|
224
|
+
throw new Error("RATE_LIMITED");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
if (err?.message === "RATE_LIMITED")
|
|
229
|
+
throw err;
|
|
230
|
+
// A read failure here should not block legitimate device starts — the
|
|
231
|
+
// single-use + short-TTL design is the primary protection. Continue.
|
|
232
|
+
}
|
|
233
|
+
const deviceCode = generateDeviceCode();
|
|
234
|
+
const userCode = generateUserCode();
|
|
235
|
+
const expiresAt = now + DEVICE_CODE_TTL_MS;
|
|
236
|
+
await client.execute({
|
|
237
|
+
sql: `INSERT INTO mcp_device_codes (device_code, user_code, owner_email, org_id, status, token_jti, created_at, expires_at, consumed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
238
|
+
args: [
|
|
239
|
+
deviceCode,
|
|
240
|
+
userCode,
|
|
241
|
+
null,
|
|
242
|
+
null,
|
|
243
|
+
"pending",
|
|
244
|
+
null,
|
|
245
|
+
now,
|
|
246
|
+
expiresAt,
|
|
247
|
+
null,
|
|
248
|
+
],
|
|
249
|
+
});
|
|
250
|
+
return {
|
|
251
|
+
deviceCode,
|
|
252
|
+
userCode,
|
|
253
|
+
ownerEmail: null,
|
|
254
|
+
orgId: null,
|
|
255
|
+
status: "pending",
|
|
256
|
+
tokenJti: null,
|
|
257
|
+
createdAt: now,
|
|
258
|
+
expiresAt,
|
|
259
|
+
consumedAt: null,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function mapDeviceRow(r) {
|
|
263
|
+
return {
|
|
264
|
+
deviceCode: (r.device_code ?? r.deviceCode),
|
|
265
|
+
userCode: (r.user_code ?? r.userCode),
|
|
266
|
+
ownerEmail: (r.owner_email ?? r.ownerEmail ?? null),
|
|
267
|
+
orgId: (r.org_id ?? r.orgId ?? null),
|
|
268
|
+
status: (r.status ?? "pending"),
|
|
269
|
+
tokenJti: (r.token_jti ?? r.tokenJti ?? null),
|
|
270
|
+
createdAt: numOrNull(r.created_at ?? r.createdAt),
|
|
271
|
+
expiresAt: numOrNull(r.expires_at ?? r.expiresAt),
|
|
272
|
+
consumedAt: numOrNull(r.consumed_at ?? r.consumedAt),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
export async function getDeviceCode(deviceCode) {
|
|
276
|
+
try {
|
|
277
|
+
await ensureTable();
|
|
278
|
+
const client = getDbExec();
|
|
279
|
+
const { rows } = await client.execute({
|
|
280
|
+
sql: `SELECT * FROM mcp_device_codes WHERE device_code = ?`,
|
|
281
|
+
args: [deviceCode],
|
|
282
|
+
});
|
|
283
|
+
if (rows.length === 0)
|
|
284
|
+
return null;
|
|
285
|
+
return mapDeviceRow(rows[0]);
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
if (isConnectionError(err))
|
|
289
|
+
return null;
|
|
290
|
+
throw err;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
export async function getDeviceCodeByUserCode(userCode) {
|
|
294
|
+
try {
|
|
295
|
+
await ensureTable();
|
|
296
|
+
const client = getDbExec();
|
|
297
|
+
const { rows } = await client.execute({
|
|
298
|
+
sql: `SELECT * FROM mcp_device_codes WHERE user_code = ?`,
|
|
299
|
+
args: [userCode],
|
|
300
|
+
});
|
|
301
|
+
if (rows.length === 0)
|
|
302
|
+
return null;
|
|
303
|
+
return mapDeviceRow(rows[0]);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
if (isConnectionError(err))
|
|
307
|
+
return null;
|
|
308
|
+
throw err;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Bind the logged-in user (email + org) to a pending device code, identified
|
|
313
|
+
* by its human-typable `user_code`. Only transitions a non-expired, still
|
|
314
|
+
* `pending` row. Returns the bound row, or a string error code:
|
|
315
|
+
* - `not_found` — no such user_code
|
|
316
|
+
* - `expired` — past its TTL
|
|
317
|
+
* - `already` — already approved/consumed (not re-bindable)
|
|
318
|
+
*/
|
|
319
|
+
export async function approveDeviceCode(userCode, ownerEmail, orgId) {
|
|
320
|
+
await ensureTable();
|
|
321
|
+
const client = getDbExec();
|
|
322
|
+
const row = await getDeviceCodeByUserCode(userCode);
|
|
323
|
+
if (!row)
|
|
324
|
+
return "not_found";
|
|
325
|
+
if ((row.expiresAt ?? 0) < Date.now())
|
|
326
|
+
return "expired";
|
|
327
|
+
if (row.status !== "pending")
|
|
328
|
+
return "already";
|
|
329
|
+
const result = await client.execute({
|
|
330
|
+
sql: `UPDATE mcp_device_codes SET status = 'approved', owner_email = ?, org_id = ? WHERE user_code = ? AND status = 'pending'`,
|
|
331
|
+
args: [ownerEmail, orgId, userCode],
|
|
332
|
+
});
|
|
333
|
+
if (result.rowsAffected === 0) {
|
|
334
|
+
// Lost a race with another approve — re-read to report the real state.
|
|
335
|
+
const fresh = await getDeviceCodeByUserCode(userCode);
|
|
336
|
+
return fresh && fresh.status !== "pending" ? "already" : "not_found";
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
...row,
|
|
340
|
+
status: "approved",
|
|
341
|
+
ownerEmail,
|
|
342
|
+
orgId,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Atomically transition an approved device code to consumed and stamp the
|
|
347
|
+
* minted token's jti. Single-use: only succeeds when the row is currently
|
|
348
|
+
* `approved` (not already consumed). Returns the pre-consume row on success,
|
|
349
|
+
* or null when it could not be consumed (already consumed / not approved /
|
|
350
|
+
* gone). The caller mints the token only after this returns a row.
|
|
351
|
+
*/
|
|
352
|
+
export async function consumeDeviceCode(deviceCode, tokenJti) {
|
|
353
|
+
await ensureTable();
|
|
354
|
+
const client = getDbExec();
|
|
355
|
+
const row = await getDeviceCode(deviceCode);
|
|
356
|
+
if (!row)
|
|
357
|
+
return null;
|
|
358
|
+
if (row.status !== "approved")
|
|
359
|
+
return null;
|
|
360
|
+
const result = await client.execute({
|
|
361
|
+
sql: `UPDATE mcp_device_codes SET status = 'consumed', token_jti = ?, consumed_at = ? WHERE device_code = ? AND status = 'approved'`,
|
|
362
|
+
args: [tokenJti, Date.now(), deviceCode],
|
|
363
|
+
});
|
|
364
|
+
if (result.rowsAffected === 0)
|
|
365
|
+
return null; // lost the single-use race
|
|
366
|
+
return row;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Claim an approved device code for token minting without making it terminal.
|
|
370
|
+
* If signing or token recording fails, callers release this back to approved
|
|
371
|
+
* so the CLI can retry the poll instead of being stuck at "consumed".
|
|
372
|
+
*/
|
|
373
|
+
export async function claimDeviceCodeForMint(deviceCode, tokenJti) {
|
|
374
|
+
await ensureTable();
|
|
375
|
+
const client = getDbExec();
|
|
376
|
+
const row = await getDeviceCode(deviceCode);
|
|
377
|
+
if (!row || row.status !== "approved")
|
|
378
|
+
return null;
|
|
379
|
+
const result = await client.execute({
|
|
380
|
+
sql: `UPDATE mcp_device_codes SET status = 'minting', token_jti = ?, consumed_at = ? WHERE device_code = ? AND status = 'approved'`,
|
|
381
|
+
args: [tokenJti, Date.now(), deviceCode],
|
|
382
|
+
});
|
|
383
|
+
if (result.rowsAffected === 0)
|
|
384
|
+
return null;
|
|
385
|
+
return row;
|
|
386
|
+
}
|
|
387
|
+
export async function finishDeviceCodeMint(deviceCode, tokenJti) {
|
|
388
|
+
await ensureTable();
|
|
389
|
+
const client = getDbExec();
|
|
390
|
+
const result = await client.execute({
|
|
391
|
+
sql: `UPDATE mcp_device_codes SET status = 'consumed' WHERE device_code = ? AND status = 'minting' AND token_jti = ?`,
|
|
392
|
+
args: [deviceCode, tokenJti],
|
|
393
|
+
});
|
|
394
|
+
return result.rowsAffected > 0;
|
|
395
|
+
}
|
|
396
|
+
export async function releaseDeviceCodeMint(deviceCode, tokenJti) {
|
|
397
|
+
try {
|
|
398
|
+
await ensureTable();
|
|
399
|
+
const client = getDbExec();
|
|
400
|
+
await client.execute({
|
|
401
|
+
sql: `UPDATE mcp_device_codes SET status = 'approved', token_jti = NULL, consumed_at = NULL WHERE device_code = ? AND status = 'minting' AND token_jti = ?`,
|
|
402
|
+
args: [deviceCode, tokenJti],
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// The next poll will keep returning pending for a minting row until a
|
|
407
|
+
// later cleanup/retry path can observe or repair it. Do not throw here.
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Best-effort: flip an expired, still-pending/approved row to `expired` so
|
|
412
|
+
* the poll endpoint can report a clean terminal state. Swallows errors.
|
|
413
|
+
*/
|
|
414
|
+
export async function expireDeviceCode(deviceCode) {
|
|
415
|
+
try {
|
|
416
|
+
await ensureTable();
|
|
417
|
+
const client = getDbExec();
|
|
418
|
+
await client.execute({
|
|
419
|
+
sql: `UPDATE mcp_device_codes SET status = 'expired' WHERE device_code = ? AND status IN ('pending','approved')`,
|
|
420
|
+
args: [deviceCode],
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
// The poll handler already treats past-TTL rows as expired regardless of
|
|
425
|
+
// whether this housekeeping write lands.
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function numOrNull(v) {
|
|
429
|
+
if (v == null)
|
|
430
|
+
return null;
|
|
431
|
+
const n = Number(v);
|
|
432
|
+
return Number.isFinite(n) ? n : null;
|
|
433
|
+
}
|
|
434
|
+
//# sourceMappingURL=connect-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect-store.js","sourceRoot":"","sources":["../../src/mcp/connect-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Framework-table store for the \"connect external agents\" feature.\n *\n * Two additive, dialect-agnostic tables back the browser **Connect** page and\n * the OAuth-style **device-code flow** a CLI drives:\n *\n * - `mcp_connect_tokens` — one row per minted MCP token. We never store the\n * token value (it's a signed JWT); only its `jti` so revocation is a\n * SQL lookup. Revoking sets `revoked_at`; the row is never deleted.\n * - `mcp_device_codes` — short-lived (10 min) device/user code pairs for\n * the OAuth 2.0 device-authorization-style CLI flow. Single-use\n * (`consumed_at`), rate-limited at creation.\n *\n * Mirrors `application-state/store.ts`: lazy `ensureTable()`, `getDbExec()`,\n * `isPostgres()` dialect branching for upserts, `isConnectionError()` swallow\n * so a transient Neon WS drop never 500s. `CREATE TABLE IF NOT EXISTS` only —\n * strictly additive, never DROP / ALTER (shared prod DB rule).\n */\n\nimport {\n getDbExec,\n isConnectionError,\n isPostgres,\n intType,\n} from \"../db/client.js\";\nimport { randomBytes, randomUUID } from \"node:crypto\";\n\nlet _initPromise: Promise<void> | undefined;\n\n/**\n * Scope claim that marks a connect-minted token (vs. an ordinary A2A\n * delegation JWT). Only tokens carrying this scope go through the revoke\n * lookup in `verifyAuth` — defined here so both `connect-route.ts` and\n * `build-server.ts` import it from the leaf store without a cycle.\n */\nexport const MCP_CONNECT_SCOPE = \"mcp-connect\";\n\n/** Device codes are valid for 10 minutes. */\nexport const DEVICE_CODE_TTL_MS = 10 * 60_000;\n\n/** Default minted-token lifetime. Configurable per-request 1–365 days. */\nexport const DEFAULT_TOKEN_TTL_DAYS = 90;\nexport const MIN_TOKEN_TTL_DAYS = 1;\nexport const MAX_TOKEN_TTL_DAYS = 365;\n\n/**\n * Rate limit for `device/start`: at most this many device codes may be created\n * within `DEVICE_START_WINDOW_MS`. Unauthenticated endpoint — keep it tight so\n * a hostile client can't flood the table or brute-force user codes.\n */\nexport const DEVICE_START_MAX = 20;\nexport const DEVICE_START_WINDOW_MS = 60_000;\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n // Additive only. Never DROP / ALTER — this DB is shared across every\n // deploy context (preview/branch/prod) for hosted templates.\n await client.execute(`\n CREATE TABLE IF NOT EXISTS mcp_connect_tokens (\n id TEXT PRIMARY KEY,\n jti TEXT UNIQUE NOT NULL,\n owner_email TEXT NOT NULL,\n org_id TEXT,\n label TEXT,\n created_at ${intType()},\n last_used_at ${intType()},\n revoked_at ${intType()}\n )\n `);\n await client.execute(`\n CREATE TABLE IF NOT EXISTS mcp_device_codes (\n device_code TEXT PRIMARY KEY,\n user_code TEXT NOT NULL,\n owner_email TEXT,\n org_id TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n token_jti TEXT,\n created_at ${intType()},\n expires_at ${intType()},\n consumed_at ${intType()}\n )\n `);\n })().catch((err) => {\n // Don't cache a rejected init. A transient DB blip should let the next\n // connect/mint/revoke call retry rather than wedging the process.\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\n// ---------------------------------------------------------------------------\n// Minted-token records\n// ---------------------------------------------------------------------------\n\nexport interface MintedTokenRow {\n id: string;\n jti: string;\n ownerEmail: string;\n orgId: string | null;\n label: string | null;\n createdAt: number | null;\n lastUsedAt: number | null;\n revokedAt: number | null;\n}\n\n/**\n * Persist a record of a minted token. The token value itself (a signed JWT)\n * is NEVER stored — only its `jti`, so revocation is a cheap SQL lookup.\n */\nexport async function recordMintedToken(params: {\n jti: string;\n ownerEmail: string;\n orgId?: string | null;\n label?: string | null;\n}): Promise<string> {\n await ensureTable();\n const client = getDbExec();\n const id = randomUUID();\n await client.execute({\n sql: `INSERT INTO mcp_connect_tokens (id, jti, owner_email, org_id, label, created_at, last_used_at, revoked_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n id,\n params.jti,\n params.ownerEmail,\n params.orgId ?? null,\n params.label ?? null,\n Date.now(),\n null,\n null,\n ],\n });\n return id;\n}\n\n/**\n * Returns true when the given `jti` corresponds to a token that has been\n * revoked. Fails OPEN on a store/DB error: a transient Neon WS drop must not\n * lock every connected agent out. Signature verification is unaffected — this\n * is only the post-verify revoke check (see `verifyAuth` in build-server.ts).\n */\nexport async function isJtiRevoked(jti: string): Promise<boolean> {\n try {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT revoked_at FROM mcp_connect_tokens WHERE jti = ?`,\n args: [jti],\n });\n if (rows.length === 0) return false;\n const revokedAt = rows[0].revoked_at ?? rows[0].revokedAt;\n return revokedAt != null;\n } catch (err) {\n // Fail open: a DB blip must not turn every minted token into a 401.\n // (Signature checks already passed; this only gates explicit revokes.)\n if (isConnectionError(err)) return false;\n return false;\n }\n}\n\nexport async function listTokens(\n ownerEmail: string,\n): Promise<MintedTokenRow[]> {\n try {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT id, jti, owner_email, org_id, label, created_at, last_used_at, revoked_at FROM mcp_connect_tokens WHERE owner_email = ? ORDER BY created_at DESC`,\n args: [ownerEmail],\n });\n return rows.map((r: any) => ({\n id: r.id as string,\n jti: r.jti as string,\n ownerEmail: (r.owner_email ?? r.ownerEmail) as string,\n orgId: (r.org_id ?? r.orgId ?? null) as string | null,\n label: (r.label ?? null) as string | null,\n createdAt: numOrNull(r.created_at ?? r.createdAt),\n lastUsedAt: numOrNull(r.last_used_at ?? r.lastUsedAt),\n revokedAt: numOrNull(r.revoked_at ?? r.revokedAt),\n }));\n } catch (err) {\n if (isConnectionError(err)) return [];\n throw err;\n }\n}\n\n/**\n * Revoke a token, but ONLY if it is owned by `ownerEmail` (the caller). The\n * `owner_email = ?` predicate is the access scope — a caller can never revoke\n * another user's token. Idempotent: re-revoking keeps the first timestamp.\n * Returns true when a row was actually transitioned to revoked.\n */\nexport async function revokeToken(\n ownerEmail: string,\n id: string,\n): Promise<boolean> {\n await ensureTable();\n const client = getDbExec();\n const result = await client.execute({\n sql: `UPDATE mcp_connect_tokens SET revoked_at = ? WHERE id = ? AND owner_email = ? AND revoked_at IS NULL`,\n args: [Date.now(), id, ownerEmail],\n });\n return result.rowsAffected > 0;\n}\n\n/**\n * Best-effort: stamp `last_used_at` for a token. Swallows all errors — this is\n * pure telemetry and must never affect the auth path.\n */\nexport async function touchTokenUsed(jti: string): Promise<void> {\n try {\n await ensureTable();\n const client = getDbExec();\n await client.execute({\n sql: `UPDATE mcp_connect_tokens SET last_used_at = ? WHERE jti = ?`,\n args: [Date.now(), jti],\n });\n } catch {\n // last_used_at is informational only — never throw from the hot path.\n }\n}\n\n// ---------------------------------------------------------------------------\n// Device-code flow (OAuth 2.0 device-authorization style)\n// ---------------------------------------------------------------------------\n\nexport interface DeviceCodeRow {\n deviceCode: string;\n userCode: string;\n ownerEmail: string | null;\n orgId: string | null;\n status: \"pending\" | \"approved\" | \"minting\" | \"consumed\" | \"expired\";\n tokenJti: string | null;\n createdAt: number | null;\n expiresAt: number | null;\n consumedAt: number | null;\n}\n\nconst USER_CODE_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\"; // Crockford-ish base32, no 0/1/O/I\n\n/** Crypto-random short human-typable code, formatted `XXXX-XXXX`. */\nfunction generateUserCode(): string {\n const bytes = randomBytes(8);\n let out = \"\";\n for (let i = 0; i < 8; i++) {\n out += USER_CODE_ALPHABET[bytes[i] % USER_CODE_ALPHABET.length];\n if (i === 3) out += \"-\";\n }\n return out;\n}\n\nfunction generateDeviceCode(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\n/**\n * Create a new device+user code pair. Rate-limited: at most\n * `DEVICE_START_MAX` codes within `DEVICE_START_WINDOW_MS`. The window count\n * is a coarse global cap (this endpoint is unauthenticated) — enough to stop\n * table flooding / user-code brute force without per-IP plumbing.\n *\n * Throws `RATE_LIMITED` when the cap is exceeded so the route can map it to a\n * 429.\n */\nexport async function createDeviceCode(): Promise<DeviceCodeRow> {\n await ensureTable();\n const client = getDbExec();\n\n const now = Date.now();\n try {\n const { rows } = await client.execute({\n sql: `SELECT COUNT(*) AS n FROM mcp_device_codes WHERE created_at > ?`,\n args: [now - DEVICE_START_WINDOW_MS],\n });\n const n = Number(rows[0]?.n ?? rows[0]?.[\"COUNT(*)\"] ?? 0);\n if (Number.isFinite(n) && n >= DEVICE_START_MAX) {\n throw new Error(\"RATE_LIMITED\");\n }\n } catch (err: any) {\n if (err?.message === \"RATE_LIMITED\") throw err;\n // A read failure here should not block legitimate device starts — the\n // single-use + short-TTL design is the primary protection. Continue.\n }\n\n const deviceCode = generateDeviceCode();\n const userCode = generateUserCode();\n const expiresAt = now + DEVICE_CODE_TTL_MS;\n await client.execute({\n sql: `INSERT INTO mcp_device_codes (device_code, user_code, owner_email, org_id, status, token_jti, created_at, expires_at, consumed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n deviceCode,\n userCode,\n null,\n null,\n \"pending\",\n null,\n now,\n expiresAt,\n null,\n ],\n });\n return {\n deviceCode,\n userCode,\n ownerEmail: null,\n orgId: null,\n status: \"pending\",\n tokenJti: null,\n createdAt: now,\n expiresAt,\n consumedAt: null,\n };\n}\n\nfunction mapDeviceRow(r: any): DeviceCodeRow {\n return {\n deviceCode: (r.device_code ?? r.deviceCode) as string,\n userCode: (r.user_code ?? r.userCode) as string,\n ownerEmail: (r.owner_email ?? r.ownerEmail ?? null) as string | null,\n orgId: (r.org_id ?? r.orgId ?? null) as string | null,\n status: (r.status ?? \"pending\") as DeviceCodeRow[\"status\"],\n tokenJti: (r.token_jti ?? r.tokenJti ?? null) as string | null,\n createdAt: numOrNull(r.created_at ?? r.createdAt),\n expiresAt: numOrNull(r.expires_at ?? r.expiresAt),\n consumedAt: numOrNull(r.consumed_at ?? r.consumedAt),\n };\n}\n\nexport async function getDeviceCode(\n deviceCode: string,\n): Promise<DeviceCodeRow | null> {\n try {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT * FROM mcp_device_codes WHERE device_code = ?`,\n args: [deviceCode],\n });\n if (rows.length === 0) return null;\n return mapDeviceRow(rows[0]);\n } catch (err) {\n if (isConnectionError(err)) return null;\n throw err;\n }\n}\n\nexport async function getDeviceCodeByUserCode(\n userCode: string,\n): Promise<DeviceCodeRow | null> {\n try {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT * FROM mcp_device_codes WHERE user_code = ?`,\n args: [userCode],\n });\n if (rows.length === 0) return null;\n return mapDeviceRow(rows[0]);\n } catch (err) {\n if (isConnectionError(err)) return null;\n throw err;\n }\n}\n\n/**\n * Bind the logged-in user (email + org) to a pending device code, identified\n * by its human-typable `user_code`. Only transitions a non-expired, still\n * `pending` row. Returns the bound row, or a string error code:\n * - `not_found` — no such user_code\n * - `expired` — past its TTL\n * - `already` — already approved/consumed (not re-bindable)\n */\nexport async function approveDeviceCode(\n userCode: string,\n ownerEmail: string,\n orgId: string | null,\n): Promise<DeviceCodeRow | \"not_found\" | \"expired\" | \"already\"> {\n await ensureTable();\n const client = getDbExec();\n const row = await getDeviceCodeByUserCode(userCode);\n if (!row) return \"not_found\";\n if ((row.expiresAt ?? 0) < Date.now()) return \"expired\";\n if (row.status !== \"pending\") return \"already\";\n\n const result = await client.execute({\n sql: `UPDATE mcp_device_codes SET status = 'approved', owner_email = ?, org_id = ? WHERE user_code = ? AND status = 'pending'`,\n args: [ownerEmail, orgId, userCode],\n });\n if (result.rowsAffected === 0) {\n // Lost a race with another approve — re-read to report the real state.\n const fresh = await getDeviceCodeByUserCode(userCode);\n return fresh && fresh.status !== \"pending\" ? \"already\" : \"not_found\";\n }\n return {\n ...row,\n status: \"approved\",\n ownerEmail,\n orgId,\n };\n}\n\n/**\n * Atomically transition an approved device code to consumed and stamp the\n * minted token's jti. Single-use: only succeeds when the row is currently\n * `approved` (not already consumed). Returns the pre-consume row on success,\n * or null when it could not be consumed (already consumed / not approved /\n * gone). The caller mints the token only after this returns a row.\n */\nexport async function consumeDeviceCode(\n deviceCode: string,\n tokenJti: string,\n): Promise<DeviceCodeRow | null> {\n await ensureTable();\n const client = getDbExec();\n const row = await getDeviceCode(deviceCode);\n if (!row) return null;\n if (row.status !== \"approved\") return null;\n const result = await client.execute({\n sql: `UPDATE mcp_device_codes SET status = 'consumed', token_jti = ?, consumed_at = ? WHERE device_code = ? AND status = 'approved'`,\n args: [tokenJti, Date.now(), deviceCode],\n });\n if (result.rowsAffected === 0) return null; // lost the single-use race\n return row;\n}\n\n/**\n * Claim an approved device code for token minting without making it terminal.\n * If signing or token recording fails, callers release this back to approved\n * so the CLI can retry the poll instead of being stuck at \"consumed\".\n */\nexport async function claimDeviceCodeForMint(\n deviceCode: string,\n tokenJti: string,\n): Promise<DeviceCodeRow | null> {\n await ensureTable();\n const client = getDbExec();\n const row = await getDeviceCode(deviceCode);\n if (!row || row.status !== \"approved\") return null;\n const result = await client.execute({\n sql: `UPDATE mcp_device_codes SET status = 'minting', token_jti = ?, consumed_at = ? WHERE device_code = ? AND status = 'approved'`,\n args: [tokenJti, Date.now(), deviceCode],\n });\n if (result.rowsAffected === 0) return null;\n return row;\n}\n\nexport async function finishDeviceCodeMint(\n deviceCode: string,\n tokenJti: string,\n): Promise<boolean> {\n await ensureTable();\n const client = getDbExec();\n const result = await client.execute({\n sql: `UPDATE mcp_device_codes SET status = 'consumed' WHERE device_code = ? AND status = 'minting' AND token_jti = ?`,\n args: [deviceCode, tokenJti],\n });\n return result.rowsAffected > 0;\n}\n\nexport async function releaseDeviceCodeMint(\n deviceCode: string,\n tokenJti: string,\n): Promise<void> {\n try {\n await ensureTable();\n const client = getDbExec();\n await client.execute({\n sql: `UPDATE mcp_device_codes SET status = 'approved', token_jti = NULL, consumed_at = NULL WHERE device_code = ? AND status = 'minting' AND token_jti = ?`,\n args: [deviceCode, tokenJti],\n });\n } catch {\n // The next poll will keep returning pending for a minting row until a\n // later cleanup/retry path can observe or repair it. Do not throw here.\n }\n}\n\n/**\n * Best-effort: flip an expired, still-pending/approved row to `expired` so\n * the poll endpoint can report a clean terminal state. Swallows errors.\n */\nexport async function expireDeviceCode(deviceCode: string): Promise<void> {\n try {\n await ensureTable();\n const client = getDbExec();\n await client.execute({\n sql: `UPDATE mcp_device_codes SET status = 'expired' WHERE device_code = ? AND status IN ('pending','approved')`,\n args: [deviceCode],\n });\n } catch {\n // The poll handler already treats past-TTL rows as expired regardless of\n // whether this housekeeping write lands.\n }\n}\n\nfunction numOrNull(v: unknown): number | null {\n if (v == null) return null;\n const n = Number(v);\n return Number.isFinite(n) ? n : null;\n}\n"]}
|
package/dist/mcp/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,yBAAyB,EACzB,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,kBAAkB,EAClB,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAK3B,OAAO,EACL,yBAAyB,EACzB,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,kBAAkB,GACnB,CAAC;AACF,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC;AAM7D;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,GAAG,EACb,MAAM,EAAE,SAAS,EACjB,WAAW,SAAmB,GAC7B,IAAI,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,yBAAyB,EACzB,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,kBAAkB,EAClB,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAK3B,OAAO,EACL,yBAAyB,EACzB,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,kBAAkB,GACnB,CAAC;AACF,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC;AAM7D;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,GAAG,EACb,MAAM,EAAE,SAAS,EACjB,WAAW,SAAmB,GAC7B,IAAI,CAwHN"}
|
package/dist/mcp/server.js
CHANGED
|
@@ -31,10 +31,14 @@ export function mountMCP(nitroApp, config, routePrefix = "/_agent-native") {
|
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
const method = getMethod(event);
|
|
34
|
-
// Auth check —
|
|
35
|
-
//
|
|
34
|
+
// Auth check — extracts the caller's identity from the JWT (`sub`),
|
|
35
|
+
// or, on the static-token / dev-open path, from the forwarded
|
|
36
|
+
// `X-Agent-Native-Owner-Email` hint the stdio proxy sends (the
|
|
37
|
+
// `agent-native mcp install` flow). Without this the install flow
|
|
38
|
+
// would run every tool unscoped (userEmail === undefined).
|
|
36
39
|
const authHeader = getRequestHeader(event, "authorization");
|
|
37
|
-
const
|
|
40
|
+
const ownerEmailHeader = getRequestHeader(event, "x-agent-native-owner-email");
|
|
41
|
+
const authResult = await verifyAuth(authHeader, ownerEmailHeader);
|
|
38
42
|
if (!authResult.authed) {
|
|
39
43
|
setResponseStatus(event, 401);
|
|
40
44
|
return { error: "Unauthorized" };
|
|
@@ -84,7 +88,22 @@ export function mountMCP(nitroApp, config, routePrefix = "/_agent-native") {
|
|
|
84
88
|
setResponseStatus(event, 501);
|
|
85
89
|
return { error: "MCP requires Node runtime" };
|
|
86
90
|
}
|
|
87
|
-
|
|
91
|
+
try {
|
|
92
|
+
await transport.handleRequest(nodeReq, nodeRes, body);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
// The SDK transport writes directly to the Node response. If the
|
|
96
|
+
// socket is already closed/ended (client disconnected, or the host
|
|
97
|
+
// stream layer also flushed), Node throws ERR_STREAM_WRITE_AFTER_END
|
|
98
|
+
// *after* the MCP payload was already delivered correctly. Swallow
|
|
99
|
+
// that benign post-flush write so an external agent disconnecting
|
|
100
|
+
// mid-stream can never take down the server process; rethrow
|
|
101
|
+
// anything else.
|
|
102
|
+
if (err?.code !== "ERR_STREAM_WRITE_AFTER_END")
|
|
103
|
+
throw err;
|
|
104
|
+
if (process.env.DEBUG)
|
|
105
|
+
console.log("[mcp] ignored post-flush ERR_STREAM_WRITE_AFTER_END (client disconnected)");
|
|
106
|
+
}
|
|
88
107
|
// Prevent H3 from double-writing the response
|
|
89
108
|
event._handled = true;
|
|
90
109
|
}));
|
package/dist/mcp/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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","sourcesContent":["import { getH3App } from \"../server/framework-request-handler.js\";\nimport {\n defineEventHandler,\n setResponseStatus,\n getMethod,\n getRequestHeader,\n} from \"h3\";\nimport { readBody } from \"../server/h3-helpers.js\";\nimport {\n createMCPServerForRequest,\n verifyAuth,\n getAccessTokens,\n resolveOrgIdFromDomain,\n buildLinkArtifacts,\n type MCPConfig,\n type MCPCallerIdentity,\n type MCPRequestMeta,\n} from \"./build-server.js\";\n\n// Re-export the shared MCP server builder + types so the stdio transport and\n// any (future) external importer of `@agent-native/core/mcp` keep resolving\n// against `./server.js` exactly as before this refactor.\nexport {\n createMCPServerForRequest,\n verifyAuth,\n getAccessTokens,\n resolveOrgIdFromDomain,\n buildLinkArtifacts,\n};\nexport type { MCPConfig, MCPCallerIdentity, MCPRequestMeta };\n\n// ---------------------------------------------------------------------------\n// mountMCP — register MCP Streamable HTTP endpoint on H3/Nitro\n// ---------------------------------------------------------------------------\n\n/**\n * Mount an MCP remote server on an H3/Nitro app.\n *\n * Endpoint: `{routePrefix}/mcp` (default `/_agent-native/mcp`)\n *\n * Uses stateless Streamable HTTP transport — no in-memory sessions,\n * compatible with serverless deployments.\n *\n * Auth: Bearer token matching ACCESS_TOKEN/ACCESS_TOKENS or JWT via A2A_SECRET.\n * No auth required when neither is configured (dev mode).\n */\nexport function mountMCP(\n nitroApp: any,\n config: MCPConfig,\n routePrefix = \"/_agent-native\",\n): void {\n getH3App(nitroApp).use(\n `${routePrefix}/mcp`,\n defineEventHandler(async (event) => {\n const pathname = event.url?.pathname || \"/\";\n const subpath = pathname.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n if (subpath) {\n // Let management/status routes mounted under /_agent-native/mcp/*\n // handle their own requests instead of treating them as MCP protocol\n // traffic.\n return;\n }\n\n const method = getMethod(event);\n\n // Auth check — also extracts the caller's identity from the JWT so\n // downstream tools run inside `runWithRequestContext`.\n const authHeader = getRequestHeader(event, \"authorization\");\n const authResult = await verifyAuth(authHeader);\n if (!authResult.authed) {\n setResponseStatus(event, 401);\n return { error: \"Unauthorized\" };\n }\n\n // Stateless mode: only POST is meaningful\n if (method === \"DELETE\") {\n setResponseStatus(event, 204);\n return \"\";\n }\n\n if (method === \"GET\") {\n // SSE stream endpoint — not used in stateless mode but the SDK\n // handles it gracefully. Let it through for protocol compliance.\n }\n\n if (method !== \"POST\" && method !== \"GET\") {\n setResponseStatus(event, 405);\n return { error: \"Method not allowed\" };\n }\n\n // Read body for POST (GET has no body)\n const body = method === \"POST\" ? await readBody(event) : undefined;\n\n // Create per-request stateless transport + server\n const { StreamableHTTPServerTransport } =\n await import(\"@modelcontextprotocol/sdk/server/streamableHttp.js\");\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined, // stateless\n });\n // Derive the running app's origin so relative deep links become\n // absolute URLs the external agent can open (same approach as A2A).\n const forwardedProto = getRequestHeader(event, \"x-forwarded-proto\");\n const host = getRequestHeader(event, \"host\");\n const proto =\n forwardedProto?.split(\",\")[0]?.trim() ||\n (host && /^(localhost|127\\.0\\.0\\.1)(:|$)/.test(host)\n ? \"http\"\n : \"https\");\n const origin = host ? `${proto}://${host}` : undefined;\n const targetHeader = getRequestHeader(\n event,\n \"x-agent-native-open-target\",\n )?.toLowerCase();\n const target =\n targetHeader === \"desktop\" ||\n targetHeader === \"terminal\" ||\n targetHeader === \"browser\"\n ? (targetHeader as MCPRequestMeta[\"target\"])\n : undefined;\n\n const server = await createMCPServerForRequest(\n config,\n authResult.identity,\n { origin, target },\n );\n await server.connect(transport);\n\n // Delegate to the transport — it writes directly to the Node response.\n // MCP's HTTP transport requires Node streams; this route is Node-only.\n const nodeReq =\n (event as any).node?.req ?? (event as any).req?.runtime?.node?.req;\n const nodeRes =\n (event as any).node?.res ?? (event as any).req?.runtime?.node?.res;\n if (!nodeReq || !nodeRes) {\n setResponseStatus(event, 501);\n return { error: \"MCP requires Node runtime\" };\n }\n await transport.handleRequest(nodeReq, nodeRes, body);\n\n // Prevent H3 from double-writing the response\n (event as any)._handled = true;\n }),\n );\n\n if (process.env.DEBUG)\n console.log(\n `[mcp] Mounted MCP server at ${routePrefix}/mcp (${Object.keys(config.actions).length} tools${config.askAgent ? \" + ask-agent\" : \"\"})`,\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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","sourcesContent":["import { getH3App } from \"../server/framework-request-handler.js\";\nimport {\n defineEventHandler,\n setResponseStatus,\n getMethod,\n getRequestHeader,\n} from \"h3\";\nimport { readBody } from \"../server/h3-helpers.js\";\nimport {\n createMCPServerForRequest,\n verifyAuth,\n getAccessTokens,\n resolveOrgIdFromDomain,\n buildLinkArtifacts,\n type MCPConfig,\n type MCPCallerIdentity,\n type MCPRequestMeta,\n} from \"./build-server.js\";\n\n// Re-export the shared MCP server builder + types so the stdio transport and\n// any (future) external importer of `@agent-native/core/mcp` keep resolving\n// against `./server.js` exactly as before this refactor.\nexport {\n createMCPServerForRequest,\n verifyAuth,\n getAccessTokens,\n resolveOrgIdFromDomain,\n buildLinkArtifacts,\n};\nexport type { MCPConfig, MCPCallerIdentity, MCPRequestMeta };\n\n// ---------------------------------------------------------------------------\n// mountMCP — register MCP Streamable HTTP endpoint on H3/Nitro\n// ---------------------------------------------------------------------------\n\n/**\n * Mount an MCP remote server on an H3/Nitro app.\n *\n * Endpoint: `{routePrefix}/mcp` (default `/_agent-native/mcp`)\n *\n * Uses stateless Streamable HTTP transport — no in-memory sessions,\n * compatible with serverless deployments.\n *\n * Auth: Bearer token matching ACCESS_TOKEN/ACCESS_TOKENS or JWT via A2A_SECRET.\n * No auth required when neither is configured (dev mode).\n */\nexport function mountMCP(\n nitroApp: any,\n config: MCPConfig,\n routePrefix = \"/_agent-native\",\n): void {\n getH3App(nitroApp).use(\n `${routePrefix}/mcp`,\n defineEventHandler(async (event) => {\n const pathname = event.url?.pathname || \"/\";\n const subpath = pathname.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n if (subpath) {\n // Let management/status routes mounted under /_agent-native/mcp/*\n // handle their own requests instead of treating them as MCP protocol\n // traffic.\n return;\n }\n\n const method = getMethod(event);\n\n // Auth check — extracts the caller's identity from the JWT (`sub`),\n // or, on the static-token / dev-open path, from the forwarded\n // `X-Agent-Native-Owner-Email` hint the stdio proxy sends (the\n // `agent-native mcp install` flow). Without this the install flow\n // would run every tool unscoped (userEmail === undefined).\n const authHeader = getRequestHeader(event, \"authorization\");\n const ownerEmailHeader = getRequestHeader(\n event,\n \"x-agent-native-owner-email\",\n );\n const authResult = await verifyAuth(authHeader, ownerEmailHeader);\n if (!authResult.authed) {\n setResponseStatus(event, 401);\n return { error: \"Unauthorized\" };\n }\n\n // Stateless mode: only POST is meaningful\n if (method === \"DELETE\") {\n setResponseStatus(event, 204);\n return \"\";\n }\n\n if (method === \"GET\") {\n // SSE stream endpoint — not used in stateless mode but the SDK\n // handles it gracefully. Let it through for protocol compliance.\n }\n\n if (method !== \"POST\" && method !== \"GET\") {\n setResponseStatus(event, 405);\n return { error: \"Method not allowed\" };\n }\n\n // Read body for POST (GET has no body)\n const body = method === \"POST\" ? await readBody(event) : undefined;\n\n // Create per-request stateless transport + server\n const { StreamableHTTPServerTransport } =\n await import(\"@modelcontextprotocol/sdk/server/streamableHttp.js\");\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined, // stateless\n });\n // Derive the running app's origin so relative deep links become\n // absolute URLs the external agent can open (same approach as A2A).\n const forwardedProto = getRequestHeader(event, \"x-forwarded-proto\");\n const host = getRequestHeader(event, \"host\");\n const proto =\n forwardedProto?.split(\",\")[0]?.trim() ||\n (host && /^(localhost|127\\.0\\.0\\.1)(:|$)/.test(host)\n ? \"http\"\n : \"https\");\n const origin = host ? `${proto}://${host}` : undefined;\n const targetHeader = getRequestHeader(\n event,\n \"x-agent-native-open-target\",\n )?.toLowerCase();\n const target =\n targetHeader === \"desktop\" ||\n targetHeader === \"terminal\" ||\n targetHeader === \"browser\"\n ? (targetHeader as MCPRequestMeta[\"target\"])\n : undefined;\n\n const server = await createMCPServerForRequest(\n config,\n authResult.identity,\n { origin, target },\n );\n await server.connect(transport);\n\n // Delegate to the transport — it writes directly to the Node response.\n // MCP's HTTP transport requires Node streams; this route is Node-only.\n const nodeReq =\n (event as any).node?.req ?? (event as any).req?.runtime?.node?.req;\n const nodeRes =\n (event as any).node?.res ?? (event as any).req?.runtime?.node?.res;\n if (!nodeReq || !nodeRes) {\n setResponseStatus(event, 501);\n return { error: \"MCP requires Node runtime\" };\n }\n try {\n await transport.handleRequest(nodeReq, nodeRes, body);\n } catch (err: any) {\n // The SDK transport writes directly to the Node response. If the\n // socket is already closed/ended (client disconnected, or the host\n // stream layer also flushed), Node throws ERR_STREAM_WRITE_AFTER_END\n // *after* the MCP payload was already delivered correctly. Swallow\n // that benign post-flush write so an external agent disconnecting\n // mid-stream can never take down the server process; rethrow\n // anything else.\n if (err?.code !== \"ERR_STREAM_WRITE_AFTER_END\") throw err;\n if (process.env.DEBUG)\n console.log(\n \"[mcp] ignored post-flush ERR_STREAM_WRITE_AFTER_END (client disconnected)\",\n );\n }\n\n // Prevent H3 from double-writing the response\n (event as any)._handled = true;\n }),\n );\n\n if (process.env.DEBUG)\n console.log(\n `[mcp] Mounted MCP server at ${routePrefix}/mcp (${Object.keys(config.actions).length} tools${config.askAgent ? \" + ask-agent\" : \"\"})`,\n );\n}\n"]}
|
package/dist/mcp/stdio.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../src/mcp/stdio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,MAAM,WAAW,kBAAkB;IACjC,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qDAAqD;IACrD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;
|
|
1
|
+
{"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../src/mcp/stdio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,MAAM,WAAW,kBAAkB;IACjC,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qDAAqD;IACrD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAqMD;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,IAAI,GAAE,kBAAuB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAef"}
|
package/dist/mcp/stdio.js
CHANGED
|
@@ -166,6 +166,7 @@ async function runStandalone(opts) {
|
|
|
166
166
|
log(`Standalone: discovered ${Object.keys(actions).length} action(s) in ${cwd}`);
|
|
167
167
|
const server = await createMCPServerForRequest({
|
|
168
168
|
name: appId.charAt(0).toUpperCase() + appId.slice(1),
|
|
169
|
+
appId,
|
|
169
170
|
description: `Agent-native ${appId} app (standalone MCP)`,
|
|
170
171
|
actions,
|
|
171
172
|
// No askAgent in standalone — there is no running engine/runtime here.
|