@agent-native/core 0.18.1 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -11
- package/dist/a2a/caller-auth.d.ts +1 -0
- package/dist/a2a/caller-auth.d.ts.map +1 -1
- package/dist/a2a/caller-auth.js +1 -1
- package/dist/a2a/caller-auth.js.map +1 -1
- 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/agent/production-agent.d.ts +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +34 -2
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/code-agent-executor.d.ts.map +1 -1
- package/dist/cli/code-agent-executor.js +47 -256
- package/dist/cli/code-agent-executor.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/AgentPanel.d.ts +3 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +4 -4
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +3 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +22 -66
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +4 -1
- package/dist/client/MultiTabAssistantChat.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/dynamic-suggestions.d.ts +43 -0
- package/dist/client/dynamic-suggestions.d.ts.map +1 -0
- package/dist/client/dynamic-suggestions.js +344 -0
- package/dist/client/dynamic-suggestions.js.map +1 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -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/client/settings/SettingsPanel.js +2 -2
- package/dist/client/settings/SettingsPanel.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/coding-tools/index.d.ts +31 -0
- package/dist/coding-tools/index.d.ts.map +1 -0
- package/dist/coding-tools/index.js +411 -0
- package/dist/coding-tools/index.js.map +1 -0
- package/dist/extensions/schema.d.ts +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +30 -0
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +85 -26
- 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 +744 -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/org-directory.d.ts +83 -0
- package/dist/mcp/org-directory.d.ts.map +1 -0
- package/dist/mcp/org-directory.js +201 -0
- package/dist/mcp/org-directory.js.map +1 -0
- package/dist/mcp/server.d.ts +38 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +208 -77
- package/dist/mcp/server.js.map +1 -1
- package/dist/scripts/dev/index.d.ts +6 -4
- package/dist/scripts/dev/index.d.ts.map +1 -1
- package/dist/scripts/dev/index.js +28 -13
- package/dist/scripts/dev/index.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +6 -6
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +32 -32
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-teams.js +2 -2
- package/dist/server/agent-teams.js.map +1 -1
- package/dist/server/agents-bundle.d.ts +3 -3
- package/dist/server/agents-bundle.js +5 -5
- package/dist/server/agents-bundle.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 +149 -33
- 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/sentry.d.ts.map +1 -1
- package/dist/server/sentry.js +17 -2
- package/dist/server/sentry.js.map +1 -1
- package/dist/sharing/schema.d.ts +1 -1
- package/docs/content/client.md +15 -0
- package/docs/content/code-agents-ui.md +25 -4
- package/docs/content/cross-app-sso.md +118 -0
- package/docs/content/drop-in-agent.md +3 -1
- package/docs/content/external-agents.md +130 -51
- package/docs/content/frames.md +1 -1
- package/docs/content/migration-workbench.md +6 -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"]}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org-directory discovery for the generic cross-app MCP verbs
|
|
3
|
+
* (`list_apps` / `ask_app` in `builtin-tools.ts`).
|
|
4
|
+
*
|
|
5
|
+
* Phase 3b of cross-app auto-wiring. Today the cross-app verbs resolve sibling
|
|
6
|
+
* apps from *local workspace* info only (`workspace-resolve.ts`), so the mail
|
|
7
|
+
* agent can only reach the calendar agent in a local dev workspace. When the
|
|
8
|
+
* deployment runs against an org directory (Dispatch is also the identity hub
|
|
9
|
+
* for the org), this module discovers the org's *deployed* sibling apps so the
|
|
10
|
+
* same verbs work cross-app in production with ZERO manual setup.
|
|
11
|
+
*
|
|
12
|
+
* ## The directory request
|
|
13
|
+
*
|
|
14
|
+
* GET <directoryOrigin>/_agent-native/org/apps
|
|
15
|
+
* Auth Authorization: Bearer <org A2A token> (same signed token A2A peers
|
|
16
|
+
* already mint — reuses `resolveA2ACallerAuth()`; the org A2A secret /
|
|
17
|
+
* global `A2A_SECRET` is loaded exactly how outgoing A2A calls load it)
|
|
18
|
+
* ⇒ { org, apps: [ { id, name, url, a2aUrl, capabilities? } ] }
|
|
19
|
+
* (allow-listed first-party apps only, prod URLs — enforced by the
|
|
20
|
+
* authority side, Phase 3a, on Dispatch)
|
|
21
|
+
*
|
|
22
|
+
* ## Resolution + safety model
|
|
23
|
+
*
|
|
24
|
+
* - The directory origin is read from env: `AGENT_NATIVE_ORG_DIRECTORY_URL`
|
|
25
|
+
* (dedicated) or `AGENT_NATIVE_IDENTITY_HUB_URL` (Dispatch is also the
|
|
26
|
+
* identity hub). When *neither* is set the feature is simply inactive —
|
|
27
|
+
* `fetchOrgApps()` returns `[]` and nothing changes anywhere (asserted by
|
|
28
|
+
* a test). This makes the whole feature opt-in and back-compat.
|
|
29
|
+
* - On ANY error (no env, unreachable, 401, non-2xx, bad JSON, no signed
|
|
30
|
+
* token) `fetchOrgApps()` returns `[]` and NEVER throws — the cross-app
|
|
31
|
+
* verbs degrade silently to their exact current local-only behavior.
|
|
32
|
+
* - A short in-memory TTL cache (default 60s) keyed by directory origin and
|
|
33
|
+
* caller identity/org scope so sibling app lists never cross tenants.
|
|
34
|
+
* Empty authenticated results are cached too (with a shorter TTL) so a
|
|
35
|
+
* transient failure doesn't hammer the directory on every call.
|
|
36
|
+
* - No secrets are ever logged.
|
|
37
|
+
*
|
|
38
|
+
* Bundled alongside `mountMCP` (no Node-only top-level imports). The A2A
|
|
39
|
+
* caller-auth + a2a client are dynamically imported inside `fetchOrgApps()`.
|
|
40
|
+
*/
|
|
41
|
+
export interface OrgApp {
|
|
42
|
+
/** Canonical app id, e.g. `calendar`. */
|
|
43
|
+
id: string;
|
|
44
|
+
/** Human-readable name, e.g. `Calendar`. */
|
|
45
|
+
name: string;
|
|
46
|
+
/** Deployed app origin/URL, e.g. `https://calendar.acme.com`. */
|
|
47
|
+
url: string;
|
|
48
|
+
/**
|
|
49
|
+
* A2A endpoint to route `ask_app` to. The authority side returns this; we
|
|
50
|
+
* fall back to the app `url` (the A2A client appends `/_agent-native/a2a`).
|
|
51
|
+
*/
|
|
52
|
+
a2aUrl: string;
|
|
53
|
+
/** Optional capability hints the authority side may include. */
|
|
54
|
+
capabilities?: string[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the org-directory origin from env. Returns `null` when neither env
|
|
58
|
+
* var is set — the caller treats `null` as "feature inactive".
|
|
59
|
+
*
|
|
60
|
+
* `env` is injectable for tests; defaults to `process.env`.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveOrgDirectoryOrigin(env?: NodeJS.ProcessEnv): string | null;
|
|
63
|
+
/**
|
|
64
|
+
* Fetch the org's first-party sibling apps from the org directory.
|
|
65
|
+
*
|
|
66
|
+
* - Returns `[]` (never throws) on ANY failure or when the directory env is
|
|
67
|
+
* unset — the cross-app verbs then keep their exact local-only behavior.
|
|
68
|
+
* - Short in-memory TTL cache so it isn't fetched on every tool call.
|
|
69
|
+
* - Strips the current app from the result (compared by id and by origin) so
|
|
70
|
+
* `list_apps` / `ask_app` never offer to route to themselves.
|
|
71
|
+
*
|
|
72
|
+
* @param opts.selfId Current app id (so it's stripped from the result).
|
|
73
|
+
* @param opts.selfOrigin Current app origin (so it's stripped by origin too).
|
|
74
|
+
* @param opts.env Injectable env (tests). Defaults to `process.env`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function fetchOrgApps(opts?: {
|
|
77
|
+
selfId?: string;
|
|
78
|
+
selfOrigin?: string;
|
|
79
|
+
env?: NodeJS.ProcessEnv;
|
|
80
|
+
}): Promise<OrgApp[]>;
|
|
81
|
+
/** Test-only: clear the in-memory cache between cases. */
|
|
82
|
+
export declare function _resetOrgDirectoryCache(): void;
|
|
83
|
+
//# sourceMappingURL=org-directory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org-directory.d.ts","sourceRoot":"","sources":["../../src/mcp/org-directory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,MAAM,WAAW,MAAM;IACrB,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAeD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,GAAG,IAAI,CAcf;AAwDD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,IAAI,CAAC,EAAE;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAmEpB;AAED,0DAA0D;AAC1D,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C"}
|