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 { BacklogTokenError, verifyBacklogToken } from './backlogOAuthClient.js';
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. A Backlog outage answered with 401 would send
49
- // every connected client through the whole authorization flow, and the
50
- // token that flow produced would fail verification just the same.
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 { BacklogTokenError, buildBacklogAuthorizationUrl, exchangeBacklogCode, refreshBacklogToken, } from './backlogOAuthClient.js';
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
- // `invalid_grant` is Backlog stating the grant is gone: revoked from
396
- // the user's settings, or a refresh token it no longer recognises.
397
- // That is the one answer that moves the client to a fresh
398
- // authorization. Reported as 503 the same failure asks it to back off
399
- // and retry a grant that can never come back, so it retries on a
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
- const grantIsGone = err instanceof BacklogTokenError &&
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backlog-mcp-server",
3
- "version": "0.20.3",
3
+ "version": "0.20.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "backlog-mcp-server": "./build/index.js"