@markmnl/fmsg-mcp 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/http.js +2 -2
- package/dist/oauth/errors.d.ts +2 -0
- package/dist/oauth/errors.js +4 -0
- package/dist/oauth/issuer.d.ts +2 -0
- package/dist/oauth/issuer.js +6 -2
- package/dist/oauth/provider.js +3 -2
- package/package.json +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Over OAuth, the owner comes from the `owner` claim the IdP puts on the app's access token
|
|
6
|
+
(and on the exchanged Web API token), so agents that sign in (ChatGPT connectors) include
|
|
7
|
+
their owner like API-key callers do. 1.2.0 only read the API-key token's claim, so a
|
|
8
|
+
signed-in sub-address sent without its owner and `whoami` showed none.
|
|
5
9
|
- A sub-address (such as `@user_agent@example.com`, owned by `@user@example.com`) now sends to
|
|
6
10
|
its owner by default: `send_message`, `reply` and `create_draft` add the owner as a recipient so
|
|
7
11
|
they can follow and reply to the thread, with a new `include_owner` argument (default true) to
|
package/dist/http.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createMcpHandler, hostHeaderValidationResponse, localhostAllowedHostnam
|
|
|
4
4
|
import { ApiKeyCallerProvider, FMSG_SCOPE } from "./auth.js";
|
|
5
5
|
import { createFmsgMcpServer } from "./server.js";
|
|
6
6
|
import { OAuthCallerProvider } from "./oauth/provider.js";
|
|
7
|
-
import { invalidToken, insufficientScope, missingToken, OAuthRequestError, oauthErrorResponse, oauthRequestState } from "./oauth/errors.js";
|
|
7
|
+
import { invalidToken, insufficientScope, missingToken, notBearer, OAuthRequestError, oauthErrorResponse, oauthRequestState } from "./oauth/errors.js";
|
|
8
8
|
import { MESSAGING_SCOPES, READ_SCOPE, WRITE_SCOPE, requestScopes } from "./oauth/scopes.js";
|
|
9
9
|
import { DOWNLOAD_PATH, downloadBaseUrl, downloadHeaders, parseDownloadPath } from "./download.js";
|
|
10
10
|
import { CHECKSUM_HEADER, UPLOAD_DRAIN_IDLE_MS, UPLOAD_PATH, UploadBodyError, decodedUploadBody, drainableBody, parseUploadPath, uploadEncoding } from "./upload.js";
|
|
@@ -350,7 +350,7 @@ export function createHttpServer(config, log = (l) => console.error(l)) {
|
|
|
350
350
|
try {
|
|
351
351
|
const bearer = /^Bearer ([A-Za-z0-9._~+\/-]+=*)$/iu.exec(authorization);
|
|
352
352
|
if (!bearer)
|
|
353
|
-
throw authorization ? invalidToken() :
|
|
353
|
+
throw !authorization ? missingToken() : /^Bearer\s/iu.test(authorization) ? invalidToken() : notBearer(!!apiKeyProvider);
|
|
354
354
|
const authenticated = await oauthProvider.verifyAccessToken(bearer[1]);
|
|
355
355
|
release = () => oauthProvider.release(authenticated);
|
|
356
356
|
let parsedBody;
|
package/dist/oauth/errors.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export declare class OAuthRequestError extends Error {
|
|
|
14
14
|
constructor(status: 401 | 403 | 500 | 503, code: string, message: string, scopes?: string[]);
|
|
15
15
|
}
|
|
16
16
|
export declare const invalidToken: () => OAuthRequestError;
|
|
17
|
+
/** An Authorization header without the Bearer scheme, such as a bare API key. */
|
|
18
|
+
export declare const notBearer: (apiKeys: boolean) => OAuthRequestError;
|
|
17
19
|
export declare const missingToken: () => OAuthRequestError;
|
|
18
20
|
export declare const insufficientScope: (scopes: string[]) => OAuthRequestError;
|
|
19
21
|
export declare function oauthErrorResponse(error: OAuthRequestError, metadataUrl: string): Response;
|
package/dist/oauth/errors.js
CHANGED
|
@@ -27,6 +27,10 @@ export class OAuthRequestError extends Error {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
export const invalidToken = () => new OAuthRequestError(401, "invalid_token", "The OAuth connection must be refreshed or reconnected.");
|
|
30
|
+
/** An Authorization header without the Bearer scheme, such as a bare API key. */
|
|
31
|
+
export const notBearer = (apiKeys) => new OAuthRequestError(401, "invalid_token", apiKeys
|
|
32
|
+
? "The Authorization header must be `Bearer <token>`; send an API key as `Authorization: Bearer fmsgk_...`."
|
|
33
|
+
: "The Authorization header must be `Bearer <token>`.");
|
|
30
34
|
export const missingToken = () => new OAuthRequestError(401, "invalid_token", "No access token was sent; send this MCP connection's Authorization header.");
|
|
31
35
|
export const insufficientScope = (scopes) => new OAuthRequestError(403, "insufficient_scope", "The OAuth connection does not authorize this operation.", scopes);
|
|
32
36
|
export function oauthErrorResponse(error, metadataUrl) {
|
package/dist/oauth/issuer.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { AccessToken } from "../client/types.js";
|
|
2
2
|
import { type OAuthConfig } from "./config.js";
|
|
3
|
+
/** The verified app token: who it acts as and, for a sub-address, the `owner` claim the IdP puts on it. */
|
|
3
4
|
export type OAuthIdentity = {
|
|
4
5
|
address: string;
|
|
6
|
+
owner?: string;
|
|
5
7
|
expiresAtMs: number;
|
|
6
8
|
scopes: string[];
|
|
7
9
|
clientId: string;
|
package/dist/oauth/issuer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRemoteJWKSet, customFetch, decodeJwt, decodeProtectedHeader, jwtVerify, errors } from "jose";
|
|
2
2
|
import { normalizeFmsgAddress } from "../address.js";
|
|
3
3
|
import { readBytes } from "../client/stream.js";
|
|
4
|
+
import { ownerOf } from "../client/token-provider.js";
|
|
4
5
|
import { oauthUrl } from "./config.js";
|
|
5
6
|
import { invalidToken, OAuthRequestError } from "./errors.js";
|
|
6
7
|
import { parseScopes } from "./scopes.js";
|
|
@@ -103,7 +104,8 @@ export class OAuthIssuer {
|
|
|
103
104
|
const expiresAtMs = Math.floor(payload.exp * 1000);
|
|
104
105
|
if (audience !== this.config.resourceUrl || !address || !scopes || !Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now())
|
|
105
106
|
throw invalidToken();
|
|
106
|
-
|
|
107
|
+
const owner = ownerOf(payload, address);
|
|
108
|
+
return { address, ...(owner ? { owner } : {}), scopes, expiresAtMs, clientId: typeof payload.client_id === "string" ? payload.client_id : "oauth-client" };
|
|
107
109
|
}
|
|
108
110
|
catch (error) {
|
|
109
111
|
if (error instanceof OAuthRequestError)
|
|
@@ -173,7 +175,9 @@ export class OAuthIssuer {
|
|
|
173
175
|
const expiresAtMs = Math.min(startedAt + body.expires_in * 1000, claims.exp * 1000, identity.expiresAtMs, startedAt + EXCHANGE_CACHE_MAX_MS);
|
|
174
176
|
if (expiresAtMs <= Date.now())
|
|
175
177
|
throw new Error();
|
|
176
|
-
|
|
178
|
+
// The exchanged token names the owner too; the app token's claim is the fallback.
|
|
179
|
+
const owner = ownerOf(claims, identity.address) ?? identity.owner;
|
|
180
|
+
return { accessToken: body.access_token, address: identity.address, ...(owner ? { owner } : {}), expiresAtMs };
|
|
177
181
|
}
|
|
178
182
|
catch {
|
|
179
183
|
if (identity.expiresAtMs <= Date.now())
|
package/dist/oauth/provider.js
CHANGED
|
@@ -45,7 +45,8 @@ export class OAuthCallerProvider {
|
|
|
45
45
|
close: () => { subject = ""; },
|
|
46
46
|
}, { allowInsecureHttp: this.config.allowInsecureHttp, reconnectOnTokenExpiry: true });
|
|
47
47
|
entry = { key, identity, active: 0, lastUsed: Date.now(), caller: {
|
|
48
|
-
client, address: identity.address,
|
|
48
|
+
client, address: identity.address, ...(identity.owner ? { owner: identity.owner } : {}),
|
|
49
|
+
tokenExpiresAt: async () => (await client.getToken()).expiresAtMs,
|
|
49
50
|
} };
|
|
50
51
|
this.entries.set(key, entry);
|
|
51
52
|
}
|
|
@@ -55,7 +56,7 @@ export class OAuthCallerProvider {
|
|
|
55
56
|
const lease = randomUUID();
|
|
56
57
|
this.leases.set(lease, entry);
|
|
57
58
|
return { token: key, clientId: identity.clientId, scopes: identity.scopes, expiresAt: identity.expiresAtMs / 1000,
|
|
58
|
-
extra: { callerLease: lease, address: identity.address } };
|
|
59
|
+
extra: { callerLease: lease, address: identity.address, ...(identity.owner ? { owner: identity.owner } : {}) } };
|
|
59
60
|
}
|
|
60
61
|
entryFor(auth) {
|
|
61
62
|
const lease = auth?.extra?.callerLease;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markmnl/fmsg-mcp",
|
|
3
3
|
"mcpName": "io.github.markmnl/fmsg-mcp",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.3",
|
|
5
5
|
"description": "MCP server for fmsg: send and receive federated messages from any AI agent via a deployed fmsg Web API",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
package/server.json
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
"url": "https://github.com/markmnl/fmsg-mcp",
|
|
9
9
|
"source": "github"
|
|
10
10
|
},
|
|
11
|
-
"version": "1.2.
|
|
11
|
+
"version": "1.2.3",
|
|
12
12
|
"packages": [
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
16
16
|
"identifier": "@markmnl/fmsg-mcp",
|
|
17
|
-
"version": "1.2.
|
|
17
|
+
"version": "1.2.3",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
20
20
|
},
|