@opsee/mcp-server 0.7.2 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opsee/mcp-server",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Opsee MCP server — manage projects, tasks, docs, and cycles from AI coding environments",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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);
@@ -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
- await this.store.set(
77
- CLIENT_KEY(clientId),
78
- JSON.stringify(registered),
79
- CLIENT_TTL_SECONDS,
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
  }