@agent-native/core 0.201.0-nightly-20261005153421 → 0.201.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 +22 -22
- package/dist/a2a/server.d.ts +2 -2
- package/dist/a2a/server.js +13 -2
- package/dist/agent/thread-data-builder.d.ts +4 -0
- package/dist/agent/thread-data-builder.js +8 -0
- package/dist/agent-sql/analysis.d.ts +31 -0
- package/dist/agent-sql/analysis.js +169 -0
- package/dist/agent-sql/index.d.ts +4 -0
- package/dist/agent-sql/index.js +4 -0
- package/dist/agent-sql/lexer.d.ts +37 -0
- package/dist/agent-sql/lexer.js +357 -0
- package/dist/agent-sql/postgres.d.ts +51 -0
- package/dist/agent-sql/postgres.js +197 -0
- package/dist/agent-sql/query.d.ts +30 -0
- package/dist/agent-sql/query.js +514 -0
- package/dist/catalog.json +24 -24
- package/dist/client/analytics/index.d.ts +1 -1
- package/dist/client/analytics/index.js +1 -1
- package/dist/client/analytics.d.ts +8 -0
- package/dist/client/analytics.js +17 -2
- package/dist/client/session-replay.js +3 -2
- package/dist/identity/offboard.js +31 -3
- package/dist/identity/rekey.d.ts +12 -1
- package/dist/identity/rekey.js +77 -6
- package/dist/identity/retired-emails.d.ts +23 -0
- package/dist/identity/retired-emails.js +55 -0
- package/dist/mcp/actions/create-org-service-token.js +20 -8
- package/dist/mcp/actions/service-token-access.d.ts +1 -0
- package/dist/mcp/actions/service-token-access.js +41 -24
- package/dist/mcp/build-server.d.ts +20 -6
- package/dist/mcp/build-server.js +131 -35
- package/dist/mcp/connect-route.d.ts +4 -3
- package/dist/mcp/connect-route.js +140 -82
- package/dist/mcp/connect-store.d.ts +17 -11
- package/dist/mcp/connect-store.js +88 -60
- package/dist/mcp/credential-issuance.d.ts +23 -0
- package/dist/mcp/credential-issuance.js +77 -0
- package/dist/mcp/credential-membership.d.ts +26 -0
- package/dist/mcp/credential-membership.js +67 -0
- package/dist/mcp/oauth-route.js +133 -45
- package/dist/mcp/oauth-store.d.ts +15 -5
- package/dist/mcp/oauth-store.js +119 -34
- package/dist/mcp/oauth-token.d.ts +4 -0
- package/dist/mcp/oauth-token.js +5 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/mcp/server.js +14 -0
- package/dist/onboarding/default-steps.js +1 -1
- package/dist/org/handlers.d.ts +1 -1
- package/dist/org/index.d.ts +2 -2
- package/dist/org/membership.d.ts +3 -1
- package/dist/org/membership.js +5 -2
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +10 -10
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resource-changes/store.d.ts +2 -0
- package/dist/resource-changes/store.js +4 -0
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/db/exec.js +28 -101
- package/dist/scripts/db/patch.js +14 -34
- package/dist/scripts/db/postgres-client.d.ts +8 -1
- package/dist/scripts/db/postgres-client.js +20 -6
- package/dist/scripts/db/query.js +31 -33
- package/dist/scripts/db/safety.d.ts +47 -2
- package/dist/scripts/db/safety.js +383 -137
- package/dist/scripts/db/scoping.d.ts +1 -0
- package/dist/scripts/db/scoping.js +3 -2
- package/dist/search/index-store.d.ts +21 -0
- package/dist/search/index-store.js +47 -0
- package/dist/search/indexer.js +35 -32
- package/dist/search/registry.d.ts +6 -3
- package/dist/search/registry.js +30 -8
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-chat-plugin.js +18 -6
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/agent-run-context.js +9 -0
- package/dist/server/attribution.d.ts +2 -0
- package/dist/server/attribution.js +7 -1
- package/dist/server/auth.js +9 -0
- package/dist/server/credential-membership-unavailable.d.ts +13 -0
- package/dist/server/credential-membership-unavailable.js +22 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/onboarding-html.js +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/recap-image-route.js +4 -0
- package/dist/server/release-schema.js +4 -0
- package/dist/shared/docs-url.d.ts +3 -1
- package/dist/shared/docs-url.js +6 -3
- package/dist/shared/environment-lanes.d.ts +8 -0
- package/dist/shared/environment-lanes.js +26 -0
- package/dist/templates/chat/.agents/skills/security/SKILL.md +9 -2
- package/dist/templates/chat/.agents/skills/storing-data/SKILL.md +12 -7
- package/dist/templates/chat/README.md +2 -2
- package/dist/templates/default/.agents/skills/security/SKILL.md +9 -2
- package/dist/templates/default/.agents/skills/storing-data/SKILL.md +12 -7
- package/dist/templates/default/app/routes/home.tsx +5 -1
- package/dist/templates/headless/.agents/skills/security/SKILL.md +9 -2
- package/dist/templates/headless/.agents/skills/storing-data/SKILL.md +12 -7
- package/dist/templates/workspace-core/.agents/skills/security/SKILL.md +9 -2
- package/dist/templates/workspace-core/.agents/skills/storing-data/SKILL.md +12 -7
- package/dist/tracking/providers.js +13 -1
- package/docs/content/authentication.mdx +5 -1
- package/docs/content/durable-background-runs.mdx +17 -8
- package/docs/content/external-agents.mdx +3 -1
- package/docs/content/mcp-protocol.mdx +6 -2
- package/docs/content/security.mdx +1 -1
- package/package.json +5 -4
- package/src/templates/chat/.agents/skills/security/SKILL.md +9 -2
- package/src/templates/chat/.agents/skills/storing-data/SKILL.md +12 -7
- package/src/templates/chat/README.md +2 -2
- package/src/templates/default/.agents/skills/security/SKILL.md +9 -2
- package/src/templates/default/.agents/skills/storing-data/SKILL.md +12 -7
- package/src/templates/default/app/routes/home.tsx +5 -1
- package/src/templates/headless/.agents/skills/security/SKILL.md +9 -2
- package/src/templates/headless/.agents/skills/storing-data/SKILL.md +12 -7
- package/src/templates/workspace-core/.agents/skills/security/SKILL.md +9 -2
- package/src/templates/workspace-core/.agents/skills/storing-data/SKILL.md +12 -7
package/dist/mcp/build-server.js
CHANGED
|
@@ -1985,38 +1985,117 @@ function mcpAudienceList(resource) {
|
|
|
1985
1985
|
}
|
|
1986
1986
|
return out;
|
|
1987
1987
|
}
|
|
1988
|
-
|
|
1988
|
+
/**
|
|
1989
|
+
* Null while the connect token is active. A revoked or jti-less token is
|
|
1990
|
+
* refused, and unreadable revocation state answers a retryable 503.
|
|
1991
|
+
*/
|
|
1992
|
+
async function refuseInactiveConnectToken(jti) {
|
|
1989
1993
|
if (!jti)
|
|
1990
|
-
return false;
|
|
1994
|
+
return { authed: false };
|
|
1995
|
+
const { isJtiRevoked } = await import("./connect-store.js");
|
|
1991
1996
|
try {
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1997
|
+
return (await isJtiRevoked(jti)) ? { authed: false } : null;
|
|
1998
|
+
}
|
|
1999
|
+
catch (error) {
|
|
2000
|
+
console.error("[mcp] Connect-token revocation check failed; refusing the token:", error);
|
|
2001
|
+
return { authed: false, unavailable: true };
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Records a connect token's use once the request is admitted, so a refused
|
|
2006
|
+
* call (revoked, removed member, membership check unavailable) never moves
|
|
2007
|
+
* `last_used_at`.
|
|
2008
|
+
*/
|
|
2009
|
+
async function markConnectTokenUsed(jti) {
|
|
2010
|
+
if (!jti)
|
|
2011
|
+
return;
|
|
2012
|
+
try {
|
|
2013
|
+
const { touchTokenUsed } = await import("./connect-store.js");
|
|
1995
2014
|
void touchTokenUsed(jti);
|
|
1996
2015
|
}
|
|
1997
2016
|
catch {
|
|
1998
|
-
//
|
|
1999
|
-
// passed; this only gates explicit revokes.
|
|
2017
|
+
// coercion-ok: last_used_at is informational; failing to record it must not refuse an admitted request.
|
|
2000
2018
|
}
|
|
2001
|
-
return true;
|
|
2002
2019
|
}
|
|
2003
|
-
|
|
2020
|
+
/**
|
|
2021
|
+
* `connectJti` is set only for connect tokens. Any other token without an
|
|
2022
|
+
* `org_id` claim is `unclaimed`, and its org is resolved later from
|
|
2023
|
+
* `org_domain` or the caller's email.
|
|
2024
|
+
*/
|
|
2025
|
+
async function resolveConnectTokenOrgId(connectJti, claimedOrgId) {
|
|
2026
|
+
let stored;
|
|
2027
|
+
if (connectJti) {
|
|
2028
|
+
const { lookupConnectTokenOrg } = await import("./connect-store.js");
|
|
2029
|
+
stored = await lookupConnectTokenOrg(connectJti);
|
|
2030
|
+
if (stored.status === "unavailable")
|
|
2031
|
+
return stored;
|
|
2032
|
+
}
|
|
2004
2033
|
if (claimedOrgId !== undefined) {
|
|
2005
|
-
return {
|
|
2034
|
+
return {
|
|
2035
|
+
status: "claimed",
|
|
2036
|
+
orgId: claimedOrgId,
|
|
2037
|
+
...(stored?.status === "found" ? { storedConnectToken: stored } : {}),
|
|
2038
|
+
};
|
|
2006
2039
|
}
|
|
2007
|
-
|
|
2008
|
-
return { status: "missing" };
|
|
2009
|
-
const { lookupConnectTokenOrg } = await import("./connect-store.js");
|
|
2010
|
-
return lookupConnectTokenOrg(jti);
|
|
2040
|
+
return stored ?? { status: "unclaimed" };
|
|
2011
2041
|
}
|
|
2012
2042
|
function orgIdFromConnectTokenResolution(resolution) {
|
|
2013
|
-
if (resolution.status === "claimed")
|
|
2014
|
-
return resolution.orgId;
|
|
2015
|
-
if (resolution.status === "found") {
|
|
2043
|
+
if (resolution.status === "claimed" || resolution.status === "found") {
|
|
2016
2044
|
return resolution.orgId;
|
|
2017
2045
|
}
|
|
2046
|
+
// A connect token with no row here was not issued for any org this app
|
|
2047
|
+
// knows. Its `org_domain` claim must not grant that domain's org, so it
|
|
2048
|
+
// runs Personal.
|
|
2049
|
+
if (resolution.status === "missing")
|
|
2050
|
+
return null;
|
|
2018
2051
|
return undefined;
|
|
2019
2052
|
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Credentials this app issues (MCP OAuth access tokens and connect tokens)
|
|
2055
|
+
* carry the organization chosen when they were issued: the signed `org_id`
|
|
2056
|
+
* claim, or the stored org of a connect token. Membership can end after
|
|
2057
|
+
* issuance, so that org is admitted only while the subject is still a member
|
|
2058
|
+
* here. The action-route bearer path reuses verifyAuth and gets the same
|
|
2059
|
+
* check.
|
|
2060
|
+
*
|
|
2061
|
+
* They also carry the subject's address, which an email change retires; a
|
|
2062
|
+
* credential signed for it before the change is refused, Personal or not.
|
|
2063
|
+
*
|
|
2064
|
+
* Cross-app A2A JWTs, first-party MCP tokens included, are not checked: their
|
|
2065
|
+
* `org_id`, like `org_domain`, is the signing app's assertion, and the caller
|
|
2066
|
+
* may have no membership row in this app's database.
|
|
2067
|
+
*/
|
|
2068
|
+
async function admitIssuedCredential(result, requestOrigin, orgResolution, issuedAt) {
|
|
2069
|
+
const { checkCredentialEmailRetirement, checkCredentialOrgMembership } = await import("./credential-membership.js");
|
|
2070
|
+
if (result.identity.userEmail) {
|
|
2071
|
+
const retirement = await checkCredentialEmailRetirement({
|
|
2072
|
+
email: result.identity.userEmail,
|
|
2073
|
+
issuedAt,
|
|
2074
|
+
});
|
|
2075
|
+
if (retirement !== "current")
|
|
2076
|
+
return retirement === "unavailable"
|
|
2077
|
+
? { authed: false, unavailable: true }
|
|
2078
|
+
: { authed: false };
|
|
2079
|
+
}
|
|
2080
|
+
const orgId = result.identity.orgId;
|
|
2081
|
+
if (typeof orgId !== "string" || !orgId)
|
|
2082
|
+
return result;
|
|
2083
|
+
const membership = await checkCredentialOrgMembership({
|
|
2084
|
+
orgId,
|
|
2085
|
+
email: result.identity.userEmail,
|
|
2086
|
+
requestOrigin,
|
|
2087
|
+
...(orgResolution.status === "found"
|
|
2088
|
+
? { storedConnectToken: orgResolution }
|
|
2089
|
+
: orgResolution.status === "claimed" && orgResolution.storedConnectToken
|
|
2090
|
+
? { storedConnectToken: orgResolution.storedConnectToken }
|
|
2091
|
+
: {}),
|
|
2092
|
+
});
|
|
2093
|
+
if (membership === "member")
|
|
2094
|
+
return result;
|
|
2095
|
+
return membership === "unavailable"
|
|
2096
|
+
? { authed: false, unavailable: true }
|
|
2097
|
+
: { authed: false };
|
|
2098
|
+
}
|
|
2020
2099
|
/**
|
|
2021
2100
|
* Verify the inbound auth header. Returns:
|
|
2022
2101
|
* - { authed: true, identity } when verified — `identity` is derived from
|
|
@@ -2038,6 +2117,10 @@ function orgIdFromConnectTokenResolution(resolution) {
|
|
|
2038
2117
|
* is consulted ONLY on the static-token / dev-open path (never to influence
|
|
2039
2118
|
* verified JWT identity), so the install flow runs tools as the configured
|
|
2040
2119
|
* owner instead of an unscoped anonymous caller.
|
|
2120
|
+
*
|
|
2121
|
+
* A credential this app issued that names an organization is admitted only
|
|
2122
|
+
* while its subject is still a member, and none is admitted for an address an
|
|
2123
|
+
* email change retired after signing (see `admitIssuedCredential`).
|
|
2041
2124
|
*/
|
|
2042
2125
|
export async function verifyAuth(authHeader, ownerEmailHeader, options = {}) {
|
|
2043
2126
|
const accessTokens = getAccessTokens();
|
|
@@ -2046,18 +2129,19 @@ export async function verifyAuth(authHeader, ownerEmailHeader, options = {}) {
|
|
|
2046
2129
|
if (token) {
|
|
2047
2130
|
const oauthIdentity = await verifyMcpOAuthAccessToken(token, options.resourceUrl);
|
|
2048
2131
|
if (oauthIdentity) {
|
|
2049
|
-
if (oauthIdentity.clientId === MCP_CONNECT_OAUTH_CLIENT_ID
|
|
2050
|
-
|
|
2051
|
-
|
|
2132
|
+
if (oauthIdentity.clientId === MCP_CONNECT_OAUTH_CLIENT_ID) {
|
|
2133
|
+
const refused = await refuseInactiveConnectToken(oauthIdentity.jti);
|
|
2134
|
+
if (refused)
|
|
2135
|
+
return refused;
|
|
2052
2136
|
}
|
|
2053
2137
|
const orgResolution = await resolveConnectTokenOrgId(oauthIdentity.clientId === MCP_CONNECT_OAUTH_CLIENT_ID
|
|
2054
2138
|
? oauthIdentity.jti
|
|
2055
2139
|
: undefined, oauthIdentity.orgId);
|
|
2056
2140
|
if (orgResolution.status === "unavailable") {
|
|
2057
|
-
return { authed: false };
|
|
2141
|
+
return { authed: false, unavailable: true };
|
|
2058
2142
|
}
|
|
2059
2143
|
const orgId = orgIdFromConnectTokenResolution(orgResolution);
|
|
2060
|
-
|
|
2144
|
+
const admitted = await admitIssuedCredential({
|
|
2061
2145
|
authed: true,
|
|
2062
2146
|
identity: {
|
|
2063
2147
|
userEmail: oauthIdentity.userEmail,
|
|
@@ -2068,7 +2152,12 @@ export async function verifyAuth(authHeader, ownerEmailHeader, options = {}) {
|
|
|
2068
2152
|
},
|
|
2069
2153
|
fullSurface: true,
|
|
2070
2154
|
fullCatalog: oauthIdentity.catalogScope === "full",
|
|
2071
|
-
};
|
|
2155
|
+
}, options.requestOrigin, orgResolution, oauthIdentity.issuedAt);
|
|
2156
|
+
if (admitted.authed &&
|
|
2157
|
+
oauthIdentity.clientId === MCP_CONNECT_OAUTH_CLIENT_ID) {
|
|
2158
|
+
await markConnectTokenUsed(oauthIdentity.jti);
|
|
2159
|
+
}
|
|
2160
|
+
return admitted;
|
|
2072
2161
|
}
|
|
2073
2162
|
}
|
|
2074
2163
|
if (accessTokens.length === 0 && !hasA2ASecret && !token) {
|
|
@@ -2092,25 +2181,25 @@ export async function verifyAuth(authHeader, ownerEmailHeader, options = {}) {
|
|
|
2092
2181
|
// Connect-minted tokens (scope === "mcp-connect") carry a random `jti`
|
|
2093
2182
|
// and are individually revocable. Only these tokens hit the revoke
|
|
2094
2183
|
// store — ordinary A2A delegation JWTs skip the DB lookup entirely so
|
|
2095
|
-
// the hot path is unchanged.
|
|
2096
|
-
// cryptographically verified, so failing open here only widens the
|
|
2097
|
-
// explicit-revoke gate, never the trust boundary.
|
|
2184
|
+
// the hot path is unchanged.
|
|
2098
2185
|
if (tokenScope === MCP_CONNECT_SCOPE) {
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2186
|
+
const refused = await refuseInactiveConnectToken(payload.jti);
|
|
2187
|
+
if (refused)
|
|
2188
|
+
return refused;
|
|
2102
2189
|
}
|
|
2103
2190
|
const orgIdClaim = parseMcpOAuthOrgIdClaim(payload);
|
|
2104
2191
|
if (!orgIdClaim)
|
|
2105
2192
|
return { authed: false };
|
|
2106
|
-
const
|
|
2193
|
+
const firstPartyMcp = payload.agent_native_first_party_mcp === true;
|
|
2194
|
+
const orgResolution = await resolveConnectTokenOrgId(tokenScope === MCP_CONNECT_SCOPE &&
|
|
2195
|
+
(!firstPartyMcp || orgIdClaim.orgId === undefined)
|
|
2107
2196
|
? payload.jti
|
|
2108
2197
|
: undefined, orgIdClaim.orgId);
|
|
2109
2198
|
if (orgResolution.status === "unavailable") {
|
|
2110
|
-
return { authed: false };
|
|
2199
|
+
return { authed: false, unavailable: true };
|
|
2111
2200
|
}
|
|
2112
2201
|
const orgId = orgIdFromConnectTokenResolution(orgResolution);
|
|
2113
|
-
|
|
2202
|
+
const verified = {
|
|
2114
2203
|
authed: true,
|
|
2115
2204
|
identity: {
|
|
2116
2205
|
userEmail: typeof payload.sub === "string" ? payload.sub : undefined,
|
|
@@ -2118,13 +2207,20 @@ export async function verifyAuth(authHeader, ownerEmailHeader, options = {}) {
|
|
|
2118
2207
|
orgDomain: typeof payload.org_domain === "string"
|
|
2119
2208
|
? payload.org_domain
|
|
2120
2209
|
: undefined,
|
|
2121
|
-
...(
|
|
2122
|
-
? { firstPartyMcp: true }
|
|
2123
|
-
: {}),
|
|
2210
|
+
...(firstPartyMcp ? { firstPartyMcp: true } : {}),
|
|
2124
2211
|
},
|
|
2125
2212
|
fullSurface: true,
|
|
2126
2213
|
fullCatalog: payload.catalog_scope === "full",
|
|
2127
2214
|
};
|
|
2215
|
+
// First-party MCP tokens share the connect scope but are minted by a
|
|
2216
|
+
// sibling app per call, so they are cross-app A2A tokens.
|
|
2217
|
+
const admitted = tokenScope === MCP_CONNECT_SCOPE && !firstPartyMcp
|
|
2218
|
+
? await admitIssuedCredential(verified, options.requestOrigin, orgResolution, typeof payload.iat === "number" ? payload.iat : undefined)
|
|
2219
|
+
: verified;
|
|
2220
|
+
if (admitted.authed && tokenScope === MCP_CONNECT_SCOPE) {
|
|
2221
|
+
await markConnectTokenUsed(payload.jti);
|
|
2222
|
+
}
|
|
2223
|
+
return admitted;
|
|
2128
2224
|
}
|
|
2129
2225
|
if (accessTokens.length === 0 && !hasA2ASecret) {
|
|
2130
2226
|
if (options.allowDevOpen === false) {
|
|
@@ -16,9 +16,10 @@ export interface McpConnectRouteOptions {
|
|
|
16
16
|
* random `jti` is stored, so the standard revocation path
|
|
17
17
|
* (`isJtiRevoked` in `verifyAuth`) applies to service tokens identically.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* The `create-org-service-token` action gates on org owner/admin before
|
|
20
|
+
* calling this. Offboarding can remove that admin before the mint, so the
|
|
21
|
+
* creator's role is rechecked under the membership lock offboarding takes,
|
|
22
|
+
* and the token is recorded in the same transaction.
|
|
22
23
|
*/
|
|
23
24
|
export declare function mintOrgServiceToken(params: {
|
|
24
25
|
serviceName: string;
|
|
@@ -7,9 +7,12 @@ import { resolveLocaleFromRequest } from "../localization/server.js";
|
|
|
7
7
|
import { localeDirection, normalizeLocaleCode, } from "../localization/shared.js";
|
|
8
8
|
import { getOrgDomain } from "../org/context.js";
|
|
9
9
|
import { getSession, getConfiguredLoginHtml, isLoopbackRequest, } from "../server/auth.js";
|
|
10
|
+
import { CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE } from "../server/credential-membership-unavailable.js";
|
|
11
|
+
import { readDeployCredentialEnv } from "../server/credential-provider.js";
|
|
10
12
|
import { readBody } from "../server/h3-helpers.js";
|
|
11
13
|
import { MCP_CONNECT_MCP_URL_TEMPLATE, getMcpConnectGuides, getMcpStaticTokenFallback, interpolateMcpConnectTemplate, resolveMcpConnectGuideId, } from "../shared/mcp-connect-content.js";
|
|
12
|
-
import { recordMintedToken, listTokens, revokeToken, normalizeServiceName, serviceIdentityEmail, createDeviceCode, getDeviceCode, getDeviceCodeByUserCode, approveDeviceCode, consumeDeviceCode, claimDeviceCodeForMint, finishDeviceCodeMint,
|
|
14
|
+
import { ensureConnectTables, recordMintedToken, listTokens, revokeToken, normalizeServiceName, serviceIdentityEmail, createDeviceCode, getDeviceCode, getDeviceCodeByUserCode, approveDeviceCode, consumeDeviceCode, claimDeviceCodeForMint, finishDeviceCodeMint, expireDeviceCode, MCP_CONNECT_OAUTH_CLIENT_ID, MCP_CONNECT_SCOPE, DEFAULT_TOKEN_TTL_DAYS, MIN_TOKEN_TTL_DAYS, MAX_TOKEN_TTL_DAYS, DEVICE_CODE_TTL_MS, } from "./connect-store.js";
|
|
15
|
+
import { McpCredentialIssuanceError, withMcpCredentialIssuance, } from "./credential-issuance.js";
|
|
13
16
|
import { MCP_OAUTH_DEFAULT_SCOPE, signMcpOAuthAccessToken, } from "./oauth-token.js";
|
|
14
17
|
import { resolveMcpOrgChoices } from "./org-choice.js";
|
|
15
18
|
import { MCP_PUBLIC_ROUTE_PREFIX } from "./route-paths.js";
|
|
@@ -21,6 +24,22 @@ function json(body, status = 200) {
|
|
|
21
24
|
headers: { "Content-Type": "application/json" },
|
|
22
25
|
});
|
|
23
26
|
}
|
|
27
|
+
function issuanceErrorResponse(err) {
|
|
28
|
+
if (err.reason === "not-member") {
|
|
29
|
+
return json({ error: "Choose an organization you belong to." }, 403);
|
|
30
|
+
}
|
|
31
|
+
const response = json({ error: CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE }, 503);
|
|
32
|
+
response.headers.set("Retry-After", "5");
|
|
33
|
+
return response;
|
|
34
|
+
}
|
|
35
|
+
async function prepareConnectIssuance() {
|
|
36
|
+
try {
|
|
37
|
+
await ensureConnectTables();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new McpCredentialIssuanceError("unavailable", { cause: error });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
24
43
|
function html(body, status = 200) {
|
|
25
44
|
return new Response(body, {
|
|
26
45
|
status,
|
|
@@ -91,7 +110,7 @@ function canUseDevOpenConnect(event) {
|
|
|
91
110
|
// by spoofing `Host: localhost`.
|
|
92
111
|
return (isLoopbackRequest(event) &&
|
|
93
112
|
isLoopbackOrigin(deriveOrigin(event)) &&
|
|
94
|
-
!
|
|
113
|
+
!readDeployCredentialEnv("A2A_SECRET")?.trim() &&
|
|
95
114
|
!process.env.ACCESS_TOKEN?.trim() &&
|
|
96
115
|
!process.env.ACCESS_TOKENS?.trim());
|
|
97
116
|
}
|
|
@@ -125,26 +144,33 @@ function clampTtlDays(input) {
|
|
|
125
144
|
*/
|
|
126
145
|
async function mintConnectToken(params) {
|
|
127
146
|
const orgDomain = await resolveOrgDomain(params.orgId);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
147
|
+
await prepareConnectIssuance();
|
|
148
|
+
return withMcpCredentialIssuance({
|
|
149
|
+
email: params.email,
|
|
131
150
|
orgId: params.orgId,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
151
|
+
requestOrigin: params.requestOrigin,
|
|
152
|
+
}, async (tx) => {
|
|
153
|
+
const jti = randomUUID();
|
|
154
|
+
const token = await signConnectToken({
|
|
155
|
+
ownerEmail: params.email,
|
|
156
|
+
orgId: params.orgId,
|
|
157
|
+
orgDomain,
|
|
158
|
+
appUrl: params.appUrl,
|
|
159
|
+
expiresIn: `${params.ttlDays}d`,
|
|
160
|
+
jti,
|
|
161
|
+
...(params.catalogScope === "full" ? { catalogScope: "full" } : {}),
|
|
162
|
+
});
|
|
163
|
+
await recordMintedToken({
|
|
164
|
+
jti,
|
|
165
|
+
ownerEmail: params.email,
|
|
166
|
+
orgId: params.orgId ?? null,
|
|
167
|
+
label: params.label,
|
|
168
|
+
}, tx);
|
|
169
|
+
return { token, jti };
|
|
143
170
|
});
|
|
144
|
-
return { token, jti };
|
|
145
171
|
}
|
|
146
172
|
async function signConnectToken(params) {
|
|
147
|
-
if (
|
|
173
|
+
if (readDeployCredentialEnv("A2A_SECRET")?.trim()) {
|
|
148
174
|
return signA2AToken(params.ownerEmail, params.orgDomain, undefined, {
|
|
149
175
|
preferGlobalSecret: true,
|
|
150
176
|
expiresIn: params.expiresIn,
|
|
@@ -183,35 +209,44 @@ async function signConnectToken(params) {
|
|
|
183
209
|
* random `jti` is stored, so the standard revocation path
|
|
184
210
|
* (`isJtiRevoked` in `verifyAuth`) applies to service tokens identically.
|
|
185
211
|
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
212
|
+
* The `create-org-service-token` action gates on org owner/admin before
|
|
213
|
+
* calling this. Offboarding can remove that admin before the mint, so the
|
|
214
|
+
* creator's role is rechecked under the membership lock offboarding takes,
|
|
215
|
+
* and the token is recorded in the same transaction.
|
|
189
216
|
*/
|
|
190
217
|
export async function mintOrgServiceToken(params) {
|
|
191
218
|
const serviceName = normalizeServiceName(params.serviceName);
|
|
192
219
|
const serviceEmail = serviceIdentityEmail(serviceName, params.orgId);
|
|
193
220
|
const orgDomain = await resolveOrgDomain(params.orgId);
|
|
194
221
|
const ttlDays = clampTtlDays(params.ttlDays ?? DEFAULT_TOKEN_TTL_DAYS);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
222
|
+
await prepareConnectIssuance();
|
|
223
|
+
return withMcpCredentialIssuance({
|
|
224
|
+
email: params.createdBy,
|
|
198
225
|
orgId: params.orgId,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
jti
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
226
|
+
requestOrigin: params.appUrl,
|
|
227
|
+
roles: ["owner", "admin"],
|
|
228
|
+
}, async (tx) => {
|
|
229
|
+
const jti = randomUUID();
|
|
230
|
+
const token = await signConnectToken({
|
|
231
|
+
ownerEmail: serviceEmail,
|
|
232
|
+
orgId: params.orgId,
|
|
233
|
+
orgDomain,
|
|
234
|
+
appUrl: params.appUrl,
|
|
235
|
+
expiresIn: `${ttlDays}d`,
|
|
236
|
+
jti,
|
|
237
|
+
includeOrgIdClaim: true,
|
|
238
|
+
});
|
|
239
|
+
const id = await recordMintedToken({
|
|
240
|
+
jti,
|
|
241
|
+
ownerEmail: serviceEmail,
|
|
242
|
+
orgId: params.orgId,
|
|
243
|
+
label: `Service token: ${serviceName}`,
|
|
244
|
+
kind: "service",
|
|
245
|
+
serviceName,
|
|
246
|
+
createdBy: params.createdBy,
|
|
247
|
+
}, tx);
|
|
248
|
+
return { token, jti, id, serviceName, serviceEmail, ttlDays };
|
|
213
249
|
});
|
|
214
|
-
return { token, jti, id, serviceName, serviceEmail, ttlDays };
|
|
215
250
|
}
|
|
216
251
|
function mcpResultPayload(appUrl, options, auth) {
|
|
217
252
|
const mcpUrl = mcpResourceUrl(appUrl);
|
|
@@ -1124,7 +1159,8 @@ export async function handleMcpConnect(event, subpath, options = {}) {
|
|
|
1124
1159
|
const session = await getSession(event);
|
|
1125
1160
|
if (!session?.email)
|
|
1126
1161
|
return json({ error: "Unauthorized" }, 401);
|
|
1127
|
-
if (!
|
|
1162
|
+
if (!readDeployCredentialEnv("A2A_SECRET")?.trim() &&
|
|
1163
|
+
canUseDevOpenConnect(event)) {
|
|
1128
1164
|
return json(mcpResultPayload(appUrl, options, { ownerEmail: session.email }));
|
|
1129
1165
|
}
|
|
1130
1166
|
const body = ((await readBody(event).catch(() => ({}))) ?? {});
|
|
@@ -1143,11 +1179,14 @@ export async function handleMcpConnect(event, subpath, options = {}) {
|
|
|
1143
1179
|
label,
|
|
1144
1180
|
ttlDays,
|
|
1145
1181
|
appUrl,
|
|
1182
|
+
requestOrigin: origin,
|
|
1146
1183
|
...(catalogScope ? { catalogScope } : {}),
|
|
1147
1184
|
});
|
|
1148
1185
|
return json(mcpResultPayload(appUrl, options, { token }));
|
|
1149
1186
|
}
|
|
1150
|
-
catch {
|
|
1187
|
+
catch (err) {
|
|
1188
|
+
if (err instanceof McpCredentialIssuanceError)
|
|
1189
|
+
return issuanceErrorResponse(err);
|
|
1151
1190
|
return json({ error: "Failed to mint token." }, 500);
|
|
1152
1191
|
}
|
|
1153
1192
|
}
|
|
@@ -1207,7 +1246,20 @@ export async function handleMcpConnect(event, subpath, options = {}) {
|
|
|
1207
1246
|
return json({ error: "Choose an organization you belong to." }, 403);
|
|
1208
1247
|
}
|
|
1209
1248
|
const selectedOrgId = requestedOrgId ?? defaultOrganizationId ?? null;
|
|
1210
|
-
|
|
1249
|
+
let result;
|
|
1250
|
+
try {
|
|
1251
|
+
await prepareConnectIssuance();
|
|
1252
|
+
result = await withMcpCredentialIssuance({
|
|
1253
|
+
email: session.email,
|
|
1254
|
+
orgId: selectedOrgId,
|
|
1255
|
+
requestOrigin: origin,
|
|
1256
|
+
}, (tx) => approveDeviceCode(userCode, session.email, selectedOrgId, tx));
|
|
1257
|
+
}
|
|
1258
|
+
catch (err) {
|
|
1259
|
+
if (err instanceof McpCredentialIssuanceError)
|
|
1260
|
+
return issuanceErrorResponse(err);
|
|
1261
|
+
return json({ error: "Failed to mint token." }, 500);
|
|
1262
|
+
}
|
|
1211
1263
|
if (result === "not_found") {
|
|
1212
1264
|
return json({ error: "Unknown device code." }, 404);
|
|
1213
1265
|
}
|
|
@@ -1242,35 +1294,43 @@ export async function handleMcpConnect(event, subpath, options = {}) {
|
|
|
1242
1294
|
!row.ownerEmail) {
|
|
1243
1295
|
return json({ status: "pending" });
|
|
1244
1296
|
}
|
|
1245
|
-
if (!process.env.A2A_SECRET?.trim() && canUseDevOpenConnect(event)) {
|
|
1246
|
-
const consumed = await consumeDeviceCode(deviceCode, `dev-open-${randomUUID()}`);
|
|
1247
|
-
if (!consumed) {
|
|
1248
|
-
const fresh = await getDeviceCode(deviceCode);
|
|
1249
|
-
if (fresh?.status === "consumed")
|
|
1250
|
-
return json({ status: "consumed" });
|
|
1251
|
-
return json({ status: "pending" });
|
|
1252
|
-
}
|
|
1253
|
-
return json({
|
|
1254
|
-
status: "approved",
|
|
1255
|
-
...mcpResultPayload(appUrl, options, {
|
|
1256
|
-
ownerEmail: row.ownerEmail,
|
|
1257
|
-
catalogScope: row.catalogScope,
|
|
1258
|
-
}),
|
|
1259
|
-
});
|
|
1260
|
-
}
|
|
1261
1297
|
try {
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1298
|
+
const devOpen = !readDeployCredentialEnv("A2A_SECRET")?.trim() &&
|
|
1299
|
+
canUseDevOpenConnect(event);
|
|
1300
|
+
const orgDomain = await resolveOrgDomain(row.orgId ?? undefined);
|
|
1301
|
+
await prepareConnectIssuance();
|
|
1302
|
+
return await withMcpCredentialIssuance({
|
|
1303
|
+
email: row.ownerEmail,
|
|
1304
|
+
orgId: row.orgId,
|
|
1305
|
+
requestOrigin: origin,
|
|
1306
|
+
}, async (tx) => {
|
|
1307
|
+
const jti = randomUUID();
|
|
1308
|
+
const claimed = devOpen
|
|
1309
|
+
? await consumeDeviceCode(deviceCode, jti, tx)
|
|
1310
|
+
: await claimDeviceCodeForMint(deviceCode, jti, tx);
|
|
1311
|
+
if (!claimed) {
|
|
1312
|
+
const fresh = await getDeviceCode(deviceCode, tx);
|
|
1313
|
+
if (fresh?.status === "consumed")
|
|
1314
|
+
return json({ status: "consumed" });
|
|
1315
|
+
if (fresh?.status === "expired" ||
|
|
1316
|
+
(fresh?.expiresAt != null && fresh.expiresAt < Date.now()))
|
|
1317
|
+
return json({ status: "expired" });
|
|
1318
|
+
return json({ status: "pending" });
|
|
1319
|
+
}
|
|
1320
|
+
if (claimed.ownerEmail !== row.ownerEmail ||
|
|
1321
|
+
claimed.orgId !== row.orgId) {
|
|
1322
|
+
throw new McpCredentialIssuanceError("not-member");
|
|
1323
|
+
}
|
|
1324
|
+
if (devOpen) {
|
|
1325
|
+
return json({
|
|
1326
|
+
status: "approved",
|
|
1327
|
+
...mcpResultPayload(appUrl, options, {
|
|
1328
|
+
ownerEmail: row.ownerEmail,
|
|
1329
|
+
catalogScope: claimed.catalogScope,
|
|
1330
|
+
}),
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
const token = await signConnectToken({
|
|
1274
1334
|
ownerEmail: claimed.ownerEmail,
|
|
1275
1335
|
orgId: claimed.orgId,
|
|
1276
1336
|
orgDomain,
|
|
@@ -1286,21 +1346,19 @@ export async function handleMcpConnect(event, subpath, options = {}) {
|
|
|
1286
1346
|
ownerEmail: claimed.ownerEmail,
|
|
1287
1347
|
orgId: claimed.orgId,
|
|
1288
1348
|
label: "Device connection",
|
|
1289
|
-
});
|
|
1290
|
-
if (!(await finishDeviceCodeMint(deviceCode, jti))) {
|
|
1291
|
-
|
|
1349
|
+
}, tx);
|
|
1350
|
+
if (!(await finishDeviceCodeMint(deviceCode, jti, tx))) {
|
|
1351
|
+
throw new McpCredentialIssuanceError("unavailable");
|
|
1292
1352
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
}
|
|
1298
|
-
return json({
|
|
1299
|
-
status: "approved",
|
|
1300
|
-
...mcpResultPayload(appUrl, options, { token }),
|
|
1353
|
+
return json({
|
|
1354
|
+
status: "approved",
|
|
1355
|
+
...mcpResultPayload(appUrl, options, { token }),
|
|
1356
|
+
});
|
|
1301
1357
|
});
|
|
1302
1358
|
}
|
|
1303
|
-
catch {
|
|
1359
|
+
catch (err) {
|
|
1360
|
+
if (err instanceof McpCredentialIssuanceError)
|
|
1361
|
+
return issuanceErrorResponse(err);
|
|
1304
1362
|
return json({ status: "error", error: "Failed to mint token." }, 500);
|
|
1305
1363
|
}
|
|
1306
1364
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* like an absent authorization. `CREATE TABLE IF NOT EXISTS` only — strictly
|
|
17
17
|
* additive, never DROP / ALTER (shared prod DB rule).
|
|
18
18
|
*/
|
|
19
|
+
import { type DbExec } from "../db/client.js";
|
|
19
20
|
export declare const MCP_CONNECT_SCOPE = "mcp-connect";
|
|
20
21
|
export declare const MCP_CONNECT_OAUTH_CLIENT_ID = "agent-native-connect";
|
|
21
22
|
export declare const DEVICE_CODE_TTL_MS: number;
|
|
@@ -25,6 +26,7 @@ export declare const MAX_TOKEN_TTL_DAYS = 365;
|
|
|
25
26
|
export declare const DEVICE_START_MAX = 20;
|
|
26
27
|
export declare const DEVICE_START_WINDOW_MS = 60000;
|
|
27
28
|
export declare function ensureTable(): Promise<void>;
|
|
29
|
+
export declare function ensureConnectTables(): Promise<void>;
|
|
28
30
|
export interface MintedTokenRow {
|
|
29
31
|
id: string;
|
|
30
32
|
jti: string;
|
|
@@ -53,18 +55,22 @@ export declare function recordMintedToken(params: {
|
|
|
53
55
|
kind?: "personal" | "service";
|
|
54
56
|
serviceName?: string | null;
|
|
55
57
|
createdBy?: string | null;
|
|
56
|
-
}): Promise<string>;
|
|
58
|
+
}, exec?: DbExec): Promise<string>;
|
|
57
59
|
/**
|
|
58
60
|
* Returns true when the given `jti` corresponds to a token that has been
|
|
59
61
|
* revoked. Fails OPEN on a store/DB error: a transient Neon WS drop must not
|
|
60
62
|
* lock every connected agent out. Signature verification is unaffected — this
|
|
61
63
|
* is only the post-verify revoke check (see `verifyAuth` in build-server.ts).
|
|
62
64
|
*/
|
|
65
|
+
/**
|
|
66
|
+
* Throws when the revocation state can't be read. Answering "not revoked"
|
|
67
|
+
* instead would let a transient read failure admit a revoked token.
|
|
68
|
+
*/
|
|
63
69
|
export declare function isJtiRevoked(jti: string): Promise<boolean>;
|
|
64
|
-
export type
|
|
70
|
+
export type StoredConnectTokenIdentity = Pick<MintedTokenRow, "kind" | "ownerEmail" | "orgId">;
|
|
71
|
+
export type ConnectTokenOrgLookup = ({
|
|
65
72
|
status: "found";
|
|
66
|
-
|
|
67
|
-
} | {
|
|
73
|
+
} & StoredConnectTokenIdentity) | {
|
|
68
74
|
status: "missing";
|
|
69
75
|
} | {
|
|
70
76
|
status: "unavailable";
|
|
@@ -105,11 +111,11 @@ export interface DeviceCodeRow {
|
|
|
105
111
|
consumedAt: number | null;
|
|
106
112
|
}
|
|
107
113
|
export declare function createDeviceCode(catalogScope?: "full" | null): Promise<DeviceCodeRow>;
|
|
108
|
-
export declare function getDeviceCode(deviceCode: string): Promise<DeviceCodeRow | null>;
|
|
109
|
-
export declare function getDeviceCodeByUserCode(userCode: string): Promise<DeviceCodeRow | null>;
|
|
110
|
-
export declare function approveDeviceCode(userCode: string, ownerEmail: string, orgId: string | null): Promise<DeviceCodeRow | "not_found" | "expired" | "already">;
|
|
111
|
-
export declare function consumeDeviceCode(deviceCode: string, tokenJti: string): Promise<DeviceCodeRow | null>;
|
|
112
|
-
export declare function claimDeviceCodeForMint(deviceCode: string, tokenJti: string): Promise<DeviceCodeRow | null>;
|
|
113
|
-
export declare function finishDeviceCodeMint(deviceCode: string, tokenJti: string): Promise<boolean>;
|
|
114
|
-
export declare function releaseDeviceCodeMint(deviceCode: string, tokenJti: string): Promise<void>;
|
|
114
|
+
export declare function getDeviceCode(deviceCode: string, exec?: DbExec): Promise<DeviceCodeRow | null>;
|
|
115
|
+
export declare function getDeviceCodeByUserCode(userCode: string, exec?: DbExec): Promise<DeviceCodeRow | null>;
|
|
116
|
+
export declare function approveDeviceCode(userCode: string, ownerEmail: string, orgId: string | null, exec?: DbExec): Promise<DeviceCodeRow | "not_found" | "expired" | "already">;
|
|
117
|
+
export declare function consumeDeviceCode(deviceCode: string, tokenJti: string, exec?: DbExec): Promise<DeviceCodeRow | null>;
|
|
118
|
+
export declare function claimDeviceCodeForMint(deviceCode: string, tokenJti: string, exec?: DbExec): Promise<DeviceCodeRow | null>;
|
|
119
|
+
export declare function finishDeviceCodeMint(deviceCode: string, tokenJti: string, exec?: DbExec): Promise<boolean>;
|
|
120
|
+
export declare function releaseDeviceCodeMint(deviceCode: string, tokenJti: string, exec?: DbExec): Promise<void>;
|
|
115
121
|
export declare function expireDeviceCode(deviceCode: string): Promise<void>;
|