@opsee/mcp-server 0.7.1 → 0.7.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/package.json
CHANGED
|
@@ -59,7 +59,6 @@ describe("OpseeClientStore", () => {
|
|
|
59
59
|
|
|
60
60
|
const registered = await clientStore.registerClient(makeClient());
|
|
61
61
|
expect(registered.client_id).toBeTruthy();
|
|
62
|
-
expect(registered.client_secret).toBeTruthy();
|
|
63
62
|
|
|
64
63
|
const fetched = await clientStore.getClient(registered.client_id);
|
|
65
64
|
expect(fetched).toBeDefined();
|
|
@@ -68,6 +67,52 @@ describe("OpseeClientStore", () => {
|
|
|
68
67
|
expect(fetched?.redirect_uris).toContain(REDIRECT_URI);
|
|
69
68
|
});
|
|
70
69
|
|
|
70
|
+
test("public clients (auth method 'none') are not issued a client_secret", async () => {
|
|
71
|
+
const store = new InMemoryKVStore();
|
|
72
|
+
const clientStore = new OpseeClientStore(store);
|
|
73
|
+
|
|
74
|
+
// makeClient() registers with token_endpoint_auth_method "none". Issuing a
|
|
75
|
+
// secret would make the SDK's token endpoint require it and reject the
|
|
76
|
+
// PKCE-only token request with invalid_client.
|
|
77
|
+
const registered = await clientStore.registerClient(makeClient());
|
|
78
|
+
expect(registered.client_secret).toBeUndefined();
|
|
79
|
+
|
|
80
|
+
const fetched = await clientStore.getClient(registered.client_id);
|
|
81
|
+
expect(fetched?.client_secret).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("confidential clients still receive a client_secret", async () => {
|
|
85
|
+
const store = new InMemoryKVStore();
|
|
86
|
+
const clientStore = new OpseeClientStore(store);
|
|
87
|
+
|
|
88
|
+
const confidential = {
|
|
89
|
+
...makeClient(),
|
|
90
|
+
token_endpoint_auth_method: "client_secret_post",
|
|
91
|
+
} as unknown as Omit<
|
|
92
|
+
OAuthClientInformationFull,
|
|
93
|
+
"client_id" | "client_id_issued_at"
|
|
94
|
+
>;
|
|
95
|
+
|
|
96
|
+
const registered = await clientStore.registerClient(confidential);
|
|
97
|
+
expect(registered.client_secret).toBeTruthy();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("registrations persist without a TTL", async () => {
|
|
101
|
+
vi.useFakeTimers();
|
|
102
|
+
try {
|
|
103
|
+
const store = new InMemoryKVStore();
|
|
104
|
+
const clientStore = new OpseeClientStore(store);
|
|
105
|
+
|
|
106
|
+
const registered = await clientStore.registerClient(makeClient());
|
|
107
|
+
// Well past any old 30-day expiry — the registration must still resolve,
|
|
108
|
+
// otherwise long-idle clients hit invalid_client at /authorize.
|
|
109
|
+
vi.advanceTimersByTime(60 * 24 * 60 * 60 * 1000);
|
|
110
|
+
expect(await clientStore.getClient(registered.client_id)).toBeDefined();
|
|
111
|
+
} finally {
|
|
112
|
+
vi.useRealTimers();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
71
116
|
test("getClient returns undefined for an unknown client (no empty-uri fallback)", async () => {
|
|
72
117
|
const store = new InMemoryKVStore();
|
|
73
118
|
const clientStore = new OpseeClientStore(store);
|
package/src/auth/kv-store.ts
CHANGED
|
@@ -87,16 +87,24 @@ export function createKVStore(): KVStore {
|
|
|
87
87
|
port: parseInt(process.env.REDIS_PORT || "6379", 10),
|
|
88
88
|
password: process.env.REDIS_PASSWORD || undefined,
|
|
89
89
|
db: parseInt(process.env.REDIS_DB || "0", 10),
|
|
90
|
-
|
|
91
|
-
//
|
|
90
|
+
// ElastiCache with in-transit encryption requires TLS. ServerName must be
|
|
91
|
+
// set so SNI/cert validation matches the endpoint host.
|
|
92
|
+
tls: process.env.REDIS_TLS === "true" ? { servername: host } : undefined,
|
|
93
|
+
// Fail fast instead of hanging the request forever. Without these a
|
|
94
|
+
// misconfigured connection (e.g. plaintext to a TLS-only server) leaves
|
|
95
|
+
// commands queued indefinitely, so awaiting a GET/SET never resolves.
|
|
92
96
|
maxRetriesPerRequest: 3,
|
|
97
|
+
connectTimeout: 10_000,
|
|
98
|
+
commandTimeout: 5_000,
|
|
93
99
|
});
|
|
94
100
|
|
|
95
101
|
redis.on("error", (err: Error) => {
|
|
96
102
|
console.error("[oauth] Redis error:", err.message);
|
|
97
103
|
});
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
// "ready" (not "connect") means commands will actually execute — a plaintext
|
|
105
|
+
// socket to a TLS-only server fires "connect" but never becomes ready.
|
|
106
|
+
redis.on("ready", () => {
|
|
107
|
+
console.log(`[oauth] Redis ready at ${host}`);
|
|
100
108
|
});
|
|
101
109
|
|
|
102
110
|
return new RedisKVStore(redis);
|
|
@@ -40,7 +40,6 @@ const CODE_KEY = (code: string) => `oauth:code:${code}`;
|
|
|
40
40
|
const REVOKED_KEY = (token: string) => `oauth:revoked:${token}`;
|
|
41
41
|
|
|
42
42
|
const FLOW_TTL_SECONDS = 10 * 60; // pending auths + auth codes: 10 minutes
|
|
43
|
-
const CLIENT_TTL_SECONDS = 30 * 24 * 60 * 60; // dynamic client registrations: 30 days
|
|
44
43
|
const REVOKED_FALLBACK_TTL_SECONDS = 24 * 60 * 60; // if token expiry is unknown
|
|
45
44
|
|
|
46
45
|
export class OpseeClientStore implements OAuthRegisteredClientsStore {
|
|
@@ -66,18 +65,26 @@ export class OpseeClientStore implements OAuthRegisteredClientsStore {
|
|
|
66
65
|
client: Omit<OAuthClientInformationFull, "client_id" | "client_id_issued_at">,
|
|
67
66
|
): Promise<OAuthClientInformationFull> {
|
|
68
67
|
const clientId = randomUUID();
|
|
69
|
-
const clientSecret = randomBytes(32).toString("hex");
|
|
70
68
|
const registered: OAuthClientInformationFull = {
|
|
71
69
|
...client,
|
|
72
70
|
client_id: clientId,
|
|
73
|
-
client_secret: clientSecret,
|
|
74
71
|
client_id_issued_at: Math.floor(Date.now() / 1000),
|
|
75
72
|
};
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)
|
|
73
|
+
|
|
74
|
+
// Only confidential clients get a secret. A public client registers with
|
|
75
|
+
// token_endpoint_auth_method "none" and authenticates via PKCE alone. If we
|
|
76
|
+
// minted a secret for it anyway, the SDK's token endpoint would then *require*
|
|
77
|
+
// that secret (see authenticateClient) and reject the PKCE-only token request
|
|
78
|
+
// with invalid_client — breaking every public client (e.g. Claude).
|
|
79
|
+
if (client.token_endpoint_auth_method !== "none") {
|
|
80
|
+
registered.client_secret = randomBytes(32).toString("hex");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Persist without a TTL. Clients cache their Dynamic Client Registration
|
|
84
|
+
// indefinitely and the DCR response advertises no expiry, so a server-side
|
|
85
|
+
// TTL guarantees that any client idle past the TTL comes back with a
|
|
86
|
+
// client_id we no longer recognise and gets invalid_client at /authorize.
|
|
87
|
+
await this.store.set(CLIENT_KEY(clientId), JSON.stringify(registered));
|
|
81
88
|
return registered;
|
|
82
89
|
}
|
|
83
90
|
}
|