backlog-mcp-server 0.20.3 → 0.20.4
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.
|
@@ -24,6 +24,32 @@ export declare class BacklogTokenError extends Error {
|
|
|
24
24
|
readonly errorCode?: string | undefined;
|
|
25
25
|
constructor(message: string, status?: number | undefined, errorCode?: string | undefined);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether the failure is Backlog stating the grant is gone.
|
|
29
|
+
*
|
|
30
|
+
* The one answer that means the client should stop retrying and start a fresh
|
|
31
|
+
* authorization. Everything else leaves the grant's fate unknown, and a client
|
|
32
|
+
* told to re-authorize on a transient failure throws away a grant that was
|
|
33
|
+
* still alive.
|
|
34
|
+
*
|
|
35
|
+
* Read from `errorCode` rather than inferred from the status, because the
|
|
36
|
+
* status cannot separate the two rejections a token endpoint makes:
|
|
37
|
+
* `invalid_client` rejects the *server's* own credentials, which is the
|
|
38
|
+
* operator's misconfiguration and not the client's grant — re-authorizing would
|
|
39
|
+
* fail at the same wall. A bare 400 with no readable code is still a dead
|
|
40
|
+
* grant: that is what the status means on this endpoint when nothing more
|
|
41
|
+
* specific is said.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isGrantGone(err: unknown): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Whether Backlog rejected the credential, as opposed to failing to answer.
|
|
46
|
+
*
|
|
47
|
+
* Only a rejection means the caller should authenticate again. An outage
|
|
48
|
+
* treated as a rejection sends every connected client through the whole
|
|
49
|
+
* authorization flow, and the credential that flow produces fails the same way
|
|
50
|
+
* — after the client has already lost the one it had.
|
|
51
|
+
*/
|
|
52
|
+
export declare function isTokenRejected(err: unknown): boolean;
|
|
27
53
|
export declare function buildBacklogAuthorizationUrl(config: BacklogOAuthConfig, redirectUri: string, state: string): string;
|
|
28
54
|
export declare function exchangeBacklogCode(config: BacklogOAuthConfig, code: string, redirectUri: string): Promise<BacklogTokenData>;
|
|
29
55
|
export declare function refreshBacklogToken(config: BacklogOAuthConfig, refreshToken: string): Promise<BacklogTokenData>;
|
|
@@ -51,6 +51,39 @@ function readOAuthErrorCode(body) {
|
|
|
51
51
|
}
|
|
52
52
|
return undefined;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether the failure is Backlog stating the grant is gone.
|
|
56
|
+
*
|
|
57
|
+
* The one answer that means the client should stop retrying and start a fresh
|
|
58
|
+
* authorization. Everything else leaves the grant's fate unknown, and a client
|
|
59
|
+
* told to re-authorize on a transient failure throws away a grant that was
|
|
60
|
+
* still alive.
|
|
61
|
+
*
|
|
62
|
+
* Read from `errorCode` rather than inferred from the status, because the
|
|
63
|
+
* status cannot separate the two rejections a token endpoint makes:
|
|
64
|
+
* `invalid_client` rejects the *server's* own credentials, which is the
|
|
65
|
+
* operator's misconfiguration and not the client's grant — re-authorizing would
|
|
66
|
+
* fail at the same wall. A bare 400 with no readable code is still a dead
|
|
67
|
+
* grant: that is what the status means on this endpoint when nothing more
|
|
68
|
+
* specific is said.
|
|
69
|
+
*/
|
|
70
|
+
export function isGrantGone(err) {
|
|
71
|
+
return (err instanceof BacklogTokenError &&
|
|
72
|
+
(err.errorCode === 'invalid_grant' ||
|
|
73
|
+
(err.status === 400 && err.errorCode === undefined)));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether Backlog rejected the credential, as opposed to failing to answer.
|
|
77
|
+
*
|
|
78
|
+
* Only a rejection means the caller should authenticate again. An outage
|
|
79
|
+
* treated as a rejection sends every connected client through the whole
|
|
80
|
+
* authorization flow, and the credential that flow produces fails the same way
|
|
81
|
+
* — after the client has already lost the one it had.
|
|
82
|
+
*/
|
|
83
|
+
export function isTokenRejected(err) {
|
|
84
|
+
return (err instanceof BacklogTokenError &&
|
|
85
|
+
(err.status === 401 || err.status === 403));
|
|
86
|
+
}
|
|
54
87
|
export function buildBacklogAuthorizationUrl(config, redirectUri, state) {
|
|
55
88
|
const params = new URLSearchParams({
|
|
56
89
|
response_type: 'code',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2025 Nulab inc.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
-
import {
|
|
3
|
+
import { isTokenRejected, verifyBacklogToken } from './backlogOAuthClient.js';
|
|
4
4
|
import { hasBacklogAuthErrorBeenReported, runWithAccessToken, } from './backlogAuthContext.js';
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
6
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
@@ -45,12 +45,9 @@ export function createBearerAuthMiddleware(store, config, mcpPath) {
|
|
|
45
45
|
}
|
|
46
46
|
catch (err) {
|
|
47
47
|
// Only Backlog rejecting the token means this client should
|
|
48
|
-
// authenticate again
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
const rejected = err instanceof BacklogTokenError &&
|
|
52
|
-
(err.status === 401 || err.status === 403);
|
|
53
|
-
if (!rejected) {
|
|
48
|
+
// authenticate again; `isTokenRejected` holds why the distinction
|
|
49
|
+
// matters.
|
|
50
|
+
if (!isTokenRejected(err)) {
|
|
54
51
|
logger.error({ err }, 'Could not verify the bearer token with Backlog');
|
|
55
52
|
c.header('Retry-After', '30');
|
|
56
53
|
return c.json({
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { randomUUID, randomBytes, createHash } from 'node:crypto';
|
|
4
4
|
import { Hono } from 'hono';
|
|
5
|
-
import {
|
|
5
|
+
import { buildBacklogAuthorizationUrl, exchangeBacklogCode, isGrantGone, refreshBacklogToken, } from './backlogOAuthClient.js';
|
|
6
6
|
import { logger } from '../utils/logger.js';
|
|
7
7
|
const AUTH_CODE_TTL_MS = 10 * 60 * 1000;
|
|
8
8
|
const REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
@@ -392,31 +392,16 @@ export function createOAuthRoutes(config, store, mcpPath) {
|
|
|
392
392
|
});
|
|
393
393
|
}
|
|
394
394
|
catch (err) {
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
// schedule forever. The consumed entry stays consumed in this branch:
|
|
401
|
-
// what it holds is a refresh token Backlog has already disowned, and
|
|
402
|
-
// keeping it until its TTL lapses only hands the next attempt the same
|
|
403
|
-
// dead credential.
|
|
404
|
-
//
|
|
405
|
-
// The code is read from the body rather than inferred from the status,
|
|
406
|
-
// because the status cannot separate the two rejections the token
|
|
407
|
-
// endpoint makes: `invalid_client` rejects *this server's* credentials,
|
|
408
|
-
// which is the operator's misconfiguration and not the client's grant,
|
|
409
|
-
// and re-authorizing would fail at the same wall. A bare 400 with no
|
|
410
|
-
// readable code is still treated as a dead grant — that is what the
|
|
411
|
-
// status means on this endpoint when nothing more specific is said.
|
|
395
|
+
// A dead grant is the one failure the client can act on, and
|
|
396
|
+
// `isGrantGone` holds the reasoning for which failures those are. The
|
|
397
|
+
// consumed entry stays consumed here: what it holds is a refresh token
|
|
398
|
+
// Backlog has already disowned, and keeping it until its TTL lapses
|
|
399
|
+
// only hands the next attempt the same dead credential.
|
|
412
400
|
//
|
|
413
401
|
// Everything else leaves the grant's fate unknown — unreachable, a
|
|
414
402
|
// timeout, a 5xx, a rejected client secret — so the entry goes back and
|
|
415
403
|
// the client is told to retry.
|
|
416
|
-
|
|
417
|
-
(err.errorCode === 'invalid_grant' ||
|
|
418
|
-
(err.status === 400 && err.errorCode === undefined));
|
|
419
|
-
if (grantIsGone) {
|
|
404
|
+
if (isGrantGone(err)) {
|
|
420
405
|
logger.warn({ err, clientId }, 'Backlog no longer recognizes the refresh grant; the client must authorize again');
|
|
421
406
|
return c.json(oauthError('invalid_grant', 'Backlog no longer recognizes this grant. A new authorization is required.'), 400);
|
|
422
407
|
}
|
package/build/lib.d.ts
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
* is the counter-example worth remembering: it reads the override file from disk,
|
|
12
12
|
* so it belongs to the CLI and is deliberately absent below. Consumers on other
|
|
13
13
|
* runtimes pass their own overrides to `createDescriptionHelper`.
|
|
14
|
+
*
|
|
15
|
+
* The OAuth exports are the token calls and the two predicates that say what a
|
|
16
|
+
* failure means. The routes and the middleware are absent: they are built
|
|
17
|
+
* against this package's synchronous `TokenStore` and Hono, so a consumer on
|
|
18
|
+
* its own storage cannot reuse them. The judgement travels, the plumbing does
|
|
19
|
+
* not.
|
|
14
20
|
*/
|
|
15
21
|
export { allTools } from './tools/tools.js';
|
|
16
22
|
export { composeToolHandler } from './handlers/builders/composeToolHandler.js';
|
|
@@ -19,9 +25,12 @@ export { createDescriptionHelper } from './createDescriptionHelper.js';
|
|
|
19
25
|
export { backlogErrorHandler } from './backlog/backlogErrorHandler.js';
|
|
20
26
|
export { buildToolSchema } from './types/tool.js';
|
|
21
27
|
export { isErrorLike } from './types/result.js';
|
|
28
|
+
export { BacklogTokenError, buildBacklogAuthorizationUrl, exchangeBacklogCode, isGrantGone, isTokenRejected, refreshBacklogToken, verifyBacklogToken, } from './auth/backlogOAuthClient.js';
|
|
22
29
|
export type { ComposeOptions } from './handlers/builders/composeToolHandler.js';
|
|
23
30
|
export type { ComposeNativeContentOptions } from './handlers/builders/composeNativeContentToolHandler.js';
|
|
24
31
|
export type { DescriptionHelper } from './createDescriptionHelper.js';
|
|
25
32
|
export type { ToolDefinition, NativeContentToolDefinition, } from './types/tool.js';
|
|
26
33
|
export type { Toolset, ToolsetGroup } from './types/toolsets.js';
|
|
27
34
|
export type { ErrorLike, SafeResult } from './types/result.js';
|
|
35
|
+
export type { BacklogOAuthConfig } from './auth/backlogOAuthConfig.js';
|
|
36
|
+
export type { BacklogTokenData } from './auth/tokenStore.js';
|
package/build/lib.js
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
* is the counter-example worth remembering: it reads the override file from disk,
|
|
12
12
|
* so it belongs to the CLI and is deliberately absent below. Consumers on other
|
|
13
13
|
* runtimes pass their own overrides to `createDescriptionHelper`.
|
|
14
|
+
*
|
|
15
|
+
* The OAuth exports are the token calls and the two predicates that say what a
|
|
16
|
+
* failure means. The routes and the middleware are absent: they are built
|
|
17
|
+
* against this package's synchronous `TokenStore` and Hono, so a consumer on
|
|
18
|
+
* its own storage cannot reuse them. The judgement travels, the plumbing does
|
|
19
|
+
* not.
|
|
14
20
|
*/
|
|
15
21
|
export { allTools } from './tools/tools.js';
|
|
16
22
|
export { composeToolHandler } from './handlers/builders/composeToolHandler.js';
|
|
@@ -19,3 +25,4 @@ export { createDescriptionHelper } from './createDescriptionHelper.js';
|
|
|
19
25
|
export { backlogErrorHandler } from './backlog/backlogErrorHandler.js';
|
|
20
26
|
export { buildToolSchema } from './types/tool.js';
|
|
21
27
|
export { isErrorLike } from './types/result.js';
|
|
28
|
+
export { BacklogTokenError, buildBacklogAuthorizationUrl, exchangeBacklogCode, isGrantGone, isTokenRejected, refreshBacklogToken, verifyBacklogToken, } from './auth/backlogOAuthClient.js';
|