@ainote/mcp 1.3.4 → 1.3.6

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.
@@ -1,41 +1,41 @@
1
- import { getConfig } from '../core/config.js';
2
-
3
- /**
4
- * Synchronous variant: only consults env var. Kept for callers that cannot
5
- * await (e.g. the stdio bootstrap path that hasn't loaded the async config).
6
- */
7
- export function authenticateWithMcpKey() {
8
- const config = getConfig();
9
-
10
- if (!config.api.key) {
11
- throw new Error('AINOTE_API_KEY is required to authenticate MCP requests. Run `ainote-mcp login` or set AINOTE_API_KEY.');
12
- }
13
-
14
- return {
15
- type: 'mcpKey',
16
- apiKey: config.api.key
17
- };
18
- }
19
-
20
- /**
21
- * Async variant: falls back to the local CLI token store (populated by
22
- * `ainote-mcp login`) when no env var is set.
23
- */
24
- export async function authenticateWithMcpKeyAsync() {
25
- const envKey = process.env.AINOTE_API_KEY;
26
- if (envKey) {
27
- return { type: 'mcpKey', apiKey: envKey };
28
- }
29
-
30
- try {
31
- const { getCredentials } = await import('./token-store.js');
32
- const { resolveApiUrl } = await import('../core/config-manager.js');
33
- const baseUrl = resolveApiUrl();
34
- const creds = await getCredentials(baseUrl);
35
- if (creds && creds.mcpKey) {
36
- return { type: 'mcpKey', apiKey: creds.mcpKey };
37
- }
38
- } catch { /* fall through */ }
39
-
40
- throw new Error('AINOTE_API_KEY is required to authenticate MCP requests. Run `ainote-mcp login` or set AINOTE_API_KEY.');
41
- }
1
+ import { getConfig } from '../core/config.js';
2
+
3
+ /**
4
+ * Synchronous variant: only consults env var. Kept for callers that cannot
5
+ * await (e.g. the stdio bootstrap path that hasn't loaded the async config).
6
+ */
7
+ export function authenticateWithMcpKey() {
8
+ const config = getConfig();
9
+
10
+ if (!config.api.key) {
11
+ throw new Error('AINOTE_API_KEY is required to authenticate MCP requests. Run `ainote-mcp login` or set AINOTE_API_KEY.');
12
+ }
13
+
14
+ return {
15
+ type: 'mcpKey',
16
+ apiKey: config.api.key
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Async variant: falls back to the local CLI token store (populated by
22
+ * `ainote-mcp login`) when no env var is set.
23
+ */
24
+ export async function authenticateWithMcpKeyAsync() {
25
+ const envKey = process.env.AINOTE_API_KEY;
26
+ if (envKey) {
27
+ return { type: 'mcpKey', apiKey: envKey };
28
+ }
29
+
30
+ try {
31
+ const { getCredentials } = await import('./token-store.js');
32
+ const { resolveApiUrl } = await import('../core/config-manager.js');
33
+ const baseUrl = resolveApiUrl();
34
+ const creds = await getCredentials(baseUrl);
35
+ if (creds && creds.mcpKey) {
36
+ return { type: 'mcpKey', apiKey: creds.mcpKey };
37
+ }
38
+ } catch { /* fall through */ }
39
+
40
+ throw new Error('AINOTE_API_KEY is required to authenticate MCP requests. Run `ainote-mcp login` or set AINOTE_API_KEY.');
41
+ }
package/lib/auth/oauth.js CHANGED
@@ -1,18 +1,18 @@
1
- export function authenticateWithOAuth(headers = {}) {
2
- const authHeader = headers['authorization'] || headers['Authorization'];
3
-
4
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
5
- throw new Error('Missing or invalid Bearer token for OAuth authentication');
6
- }
7
-
8
- const token = authHeader.substring('Bearer '.length).trim();
9
-
10
- if (!token) {
11
- throw new Error('Bearer token is empty');
12
- }
13
-
14
- return {
15
- type: 'oauth',
16
- token
17
- };
18
- }
1
+ export function authenticateWithOAuth(headers = {}) {
2
+ const authHeader = headers['authorization'] || headers['Authorization'];
3
+
4
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
5
+ throw new Error('Missing or invalid Bearer token for OAuth authentication');
6
+ }
7
+
8
+ const token = authHeader.substring('Bearer '.length).trim();
9
+
10
+ if (!token) {
11
+ throw new Error('Bearer token is empty');
12
+ }
13
+
14
+ return {
15
+ type: 'oauth',
16
+ token
17
+ };
18
+ }
@@ -1,78 +1,85 @@
1
- /**
2
- * Access token rotation against the ainote backend.
3
- *
4
- * Endpoint: POST /api/auth/token/refresh
5
- * Body: { refresh_token: "rt_..." }
6
- * Response: { access_token, refresh_token, expires_in }
7
- *
8
- * Concurrency: an in-process mutex (module-level Promise) ensures that
9
- * parallel callers wait on a single in-flight refresh instead of issuing
10
- * duplicate requests (which would otherwise burn through refresh-token
11
- * rotation and invalidate the family).
12
- */
13
- import axios from 'axios';
14
-
15
- let inFlight = null;
16
-
17
- export class RefreshError extends Error {
18
- constructor(message, { status, body } = {}) {
19
- super(message);
20
- this.name = 'RefreshError';
21
- this.status = status;
22
- this.body = body;
23
- }
24
- }
25
-
26
- async function performRefresh({ apiUrl, refreshToken }) {
27
- let response;
28
- try {
29
- response = await axios.post(
30
- `${apiUrl}/api/auth/token/refresh`,
31
- { refresh_token: refreshToken },
32
- {
33
- headers: { 'Content-Type': 'application/json' },
34
- timeout: 30000,
35
- validateStatus: (s) => s >= 200 && s < 500
36
- }
37
- );
38
- } catch (err) {
39
- throw new RefreshError(`Refresh request failed: ${err.message}`);
40
- }
41
-
42
- if (response.status < 200 || response.status >= 300) {
43
- const body = response.data || {};
44
- const msg = body.error_description || body.error || body.message ||
45
- `Refresh failed with HTTP ${response.status}`;
46
- throw new RefreshError(msg, { status: response.status, body });
47
- }
48
-
49
- const body = response.data || {};
50
- if (!body.access_token) {
51
- throw new RefreshError('Refresh response missing access_token', { body });
52
- }
53
- return {
54
- access_token: body.access_token,
55
- refresh_token: body.refresh_token || refreshToken,
56
- expires_in: Number(body.expires_in) || undefined
57
- };
58
- }
59
-
60
- /**
61
- * Rotate the access token. Concurrent callers share one in-flight request.
62
- *
63
- * @param {Object} args
64
- * @param {string} args.apiUrl
65
- * @param {string} args.refreshToken
66
- * @returns {Promise<{access_token: string, refresh_token: string, expires_in?: number}>}
67
- */
68
- export async function refreshAccessToken({ apiUrl, refreshToken }) {
69
- if (!refreshToken) {
70
- throw new RefreshError('No refresh token available — login required.');
71
- }
72
- if (inFlight) return inFlight;
73
- inFlight = performRefresh({ apiUrl, refreshToken })
74
- .finally(() => {
75
- inFlight = null;
76
- });
77
- return inFlight;
78
- }
1
+ /**
2
+ * Access token rotation against the ainote backend.
3
+ *
4
+ * Endpoint: POST /api/auth/token/refresh
5
+ * Body: { refresh_token: "rt_..." }
6
+ * Response: { success, data: { access_token, refresh_token, expires_in } }
7
+ * (legacy: same fields at the top level)
8
+ *
9
+ * Concurrency: an in-process mutex (module-level Promise) ensures that
10
+ * parallel callers wait on a single in-flight refresh instead of issuing
11
+ * duplicate requests (which would otherwise burn through refresh-token
12
+ * rotation and invalidate the family).
13
+ */
14
+ import axios from 'axios';
15
+ import { errorMessage } from './error-message.js';
16
+
17
+ let inFlight = null;
18
+
19
+ export class RefreshError extends Error {
20
+ constructor(message, { status, body } = {}) {
21
+ super(message);
22
+ this.name = 'RefreshError';
23
+ this.status = status;
24
+ this.body = body;
25
+ }
26
+ }
27
+
28
+ async function performRefresh({ apiUrl, refreshToken }) {
29
+ let response;
30
+ try {
31
+ response = await axios.post(
32
+ `${apiUrl}/api/auth/token/refresh`,
33
+ { refresh_token: refreshToken },
34
+ {
35
+ headers: { 'Content-Type': 'application/json' },
36
+ timeout: 30000,
37
+ validateStatus: (s) => s >= 200 && s < 500
38
+ }
39
+ );
40
+ } catch (err) {
41
+ throw new RefreshError(`Refresh request failed: ${err.message}`);
42
+ }
43
+
44
+ if (response.status < 200 || response.status >= 300) {
45
+ const body = response.data || {};
46
+ const msg = errorMessage(body) ||
47
+ `Refresh failed with HTTP ${response.status}`;
48
+ throw new RefreshError(msg, { status: response.status, body });
49
+ }
50
+
51
+ const body = response.data || {};
52
+ // Server wraps the payload as { success, data: { access_token, ... } };
53
+ // older servers returned the fields at the top level.
54
+ const accessToken = body.data?.access_token ?? body.access_token;
55
+ const nextRefreshToken = body.data?.refresh_token ?? body.refresh_token;
56
+ const expiresIn = body.data?.expires_in ?? body.expires_in;
57
+ if (!accessToken) {
58
+ throw new RefreshError('Refresh response missing access_token', { body });
59
+ }
60
+ return {
61
+ access_token: accessToken,
62
+ refresh_token: nextRefreshToken || refreshToken,
63
+ expires_in: Number(expiresIn) || undefined
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Rotate the access token. Concurrent callers share one in-flight request.
69
+ *
70
+ * @param {Object} args
71
+ * @param {string} args.apiUrl
72
+ * @param {string} args.refreshToken
73
+ * @returns {Promise<{access_token: string, refresh_token: string, expires_in?: number}>}
74
+ */
75
+ export async function refreshAccessToken({ apiUrl, refreshToken }) {
76
+ if (!refreshToken) {
77
+ throw new RefreshError('No refresh token available — login required.');
78
+ }
79
+ if (inFlight) return inFlight;
80
+ inFlight = performRefresh({ apiUrl, refreshToken })
81
+ .finally(() => {
82
+ inFlight = null;
83
+ });
84
+ return inFlight;
85
+ }
@@ -1,31 +1,31 @@
1
- import { authenticateWithMcpKey } from './mcp-key.js';
2
- import { authenticateWithOAuth } from './oauth.js';
3
- import { getFeatureFlags } from '../core/config.js';
4
-
5
- export function resolveAuthContext(transportType, headers = {}) {
6
- if (transportType === 'stdio') {
7
- try {
8
- return authenticateWithMcpKey();
9
- } catch {
10
- // No API key configured — allow anonymous mode for onboarding tools
11
- return { type: 'anonymous' };
12
- }
13
- }
14
-
15
- const features = getFeatureFlags();
16
-
17
- if (transportType === 'sse' && features.oauthAuth) {
18
- try {
19
- return authenticateWithOAuth(headers);
20
- } catch {
21
- // Fall through to MCP key or anonymous
22
- }
23
- }
24
-
25
- // Default fallback to MCP key, then anonymous for onboarding
26
- try {
27
- return authenticateWithMcpKey();
28
- } catch {
29
- return { type: 'anonymous' };
30
- }
31
- }
1
+ import { authenticateWithMcpKey } from './mcp-key.js';
2
+ import { authenticateWithOAuth } from './oauth.js';
3
+ import { getFeatureFlags } from '../core/config.js';
4
+
5
+ export function resolveAuthContext(transportType, headers = {}) {
6
+ if (transportType === 'stdio') {
7
+ try {
8
+ return authenticateWithMcpKey();
9
+ } catch {
10
+ // No API key configured — allow anonymous mode for onboarding tools
11
+ return { type: 'anonymous' };
12
+ }
13
+ }
14
+
15
+ const features = getFeatureFlags();
16
+
17
+ if (transportType === 'sse' && features.oauthAuth) {
18
+ try {
19
+ return authenticateWithOAuth(headers);
20
+ } catch {
21
+ // Fall through to MCP key or anonymous
22
+ }
23
+ }
24
+
25
+ // Default fallback to MCP key, then anonymous for onboarding
26
+ try {
27
+ return authenticateWithMcpKey();
28
+ } catch {
29
+ return { type: 'anonymous' };
30
+ }
31
+ }