mcp-google-multi 6.0.0-alpha.59 → 6.0.0-alpha.60

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.
@@ -99,8 +99,13 @@ export declare class RefreshStore {
99
99
  /** Rotate a presented refresh token; null if unknown OR if the presented
100
100
  * token was already rotated away (reuse => the family is revoked). The
101
101
  * record's sub is copied forward and returned so the caller can mint the
102
- * matching access token without trusting anything client-supplied. */
103
- rotate(oldToken: string, nowMs: number): {
102
+ * matching access token without trusting anything client-supplied.
103
+ *
104
+ * `accept` (optional) is called inside the store lock, before any mutation,
105
+ * with the record's sub. false: every active record of that sub is dropped
106
+ * and null returned. A throw: nothing is mutated and the error propagates,
107
+ * so the presented token stays valid. Absent: unchanged. */
108
+ rotate(oldToken: string, nowMs: number, accept?: (sub: string) => boolean): {
104
109
  token: string;
105
110
  sub: string;
106
111
  } | null;
package/dist/mcp-token.js CHANGED
@@ -175,8 +175,13 @@ export class RefreshStore {
175
175
  /** Rotate a presented refresh token; null if unknown OR if the presented
176
176
  * token was already rotated away (reuse => the family is revoked). The
177
177
  * record's sub is copied forward and returned so the caller can mint the
178
- * matching access token without trusting anything client-supplied. */
179
- rotate(oldToken, nowMs) {
178
+ * matching access token without trusting anything client-supplied.
179
+ *
180
+ * `accept` (optional) is called inside the store lock, before any mutation,
181
+ * with the record's sub. false: every active record of that sub is dropped
182
+ * and null returned. A throw: nothing is mutated and the error propagates,
183
+ * so the presented token stays valid. Absent: unchanged. */
184
+ rotate(oldToken, nowMs, accept) {
180
185
  return withFileLock(this.path, () => {
181
186
  const data = this.load();
182
187
  const rec = data.active[oldToken];
@@ -191,6 +196,13 @@ export class RefreshStore {
191
196
  }
192
197
  return null;
193
198
  }
199
+ if (accept && !accept(rec.sub)) {
200
+ for (const [t, r] of Object.entries(data.active))
201
+ if (r.sub === rec.sub)
202
+ delete data.active[t];
203
+ this.save(data);
204
+ return null;
205
+ }
194
206
  delete data.active[oldToken];
195
207
  data.spent[oldToken] = rec.family;
196
208
  const next = randomBytes(32).toString('base64url');
@@ -103,6 +103,12 @@ export interface AuthServerDeps {
103
103
  * A returned refusal answers 403 with its slug; a throw or rejection answers
104
104
  * 500 E_ALIAS_ADD_FAILED. */
105
105
  bindTenantAlias?: (bind: TenantAliasBind) => void | AliasBindRefusal | Promise<void | AliasBindRefusal>;
106
+ /** False for a subject this host no longer serves (a multi-tenant host that
107
+ * removed or suspended it): /token then issues and rotates nothing for it
108
+ * (400 invalid_grant; a rotated sub's refresh tokens are dropped). A throw
109
+ * means the answer is unknown right now: 503 temporarily_unavailable, and
110
+ * the presented refresh token is not spent. Absent: every subject is active. */
111
+ subjectActive?: (sub: string) => boolean;
106
112
  }
107
113
  export interface AuthServer {
108
114
  routes: Record<string, RouteHandler>;
package/dist/oauth-as.js CHANGED
@@ -56,6 +56,11 @@ function errorPage(res, status, slug, message) {
56
56
  res.end(`${slug}: ${message}`);
57
57
  return true;
58
58
  }
59
+ class SubjectCheckUnavailable extends Error {
60
+ }
61
+ function subjectUnavailable(res) {
62
+ return json(res, 503, { error: 'temporarily_unavailable', message: 'the sign-in could not be checked right now; retry shortly' }, { 'Retry-After': '5' });
63
+ }
59
64
  function redirect(res, location) {
60
65
  res.writeHead(302, { Location: location });
61
66
  res.end();
@@ -508,12 +513,50 @@ export function buildAuthServer(config, deps = {}) {
508
513
  return json(res, 400, { error: 'invalid_grant', message: 'PKCE verification failed' });
509
514
  }
510
515
  const access = await signAccessToken({ base, secret, iat: nowSec(), ttlSec: accessTtl, sub: code.sub });
516
+ if (deps.subjectActive) {
517
+ let active;
518
+ try {
519
+ active = deps.subjectActive(code.sub);
520
+ }
521
+ catch (e) {
522
+ log(`token deferred grant=authorization_code: subject check failed: ${e instanceof Error ? e.message : 'non-Error throw'}`);
523
+ return subjectUnavailable(res);
524
+ }
525
+ if (!active) {
526
+ log('token refused grant=authorization_code: subject inactive');
527
+ return json(res, 400, { error: 'invalid_grant', message: 'the subject is no longer provisioned' });
528
+ }
529
+ }
511
530
  const refreshToken = refresh.issue(now(), code.sub);
512
531
  log('token issued grant=authorization_code');
513
532
  return json(res, 200, { access_token: access, token_type: 'Bearer', expires_in: accessTtl, refresh_token: refreshToken, scope: 'mcp:use' });
514
533
  }
515
534
  if (grant === 'refresh_token') {
516
- const next = refresh.rotate(form.refresh_token ?? '', now());
535
+ const subjectActive = deps.subjectActive;
536
+ const accept = subjectActive
537
+ ? (sub) => {
538
+ let active;
539
+ try {
540
+ active = subjectActive(sub);
541
+ }
542
+ catch (e) {
543
+ throw new SubjectCheckUnavailable('subject check failed', { cause: e });
544
+ }
545
+ if (!active)
546
+ log('token refused grant=refresh_token: subject inactive');
547
+ return active;
548
+ }
549
+ : undefined;
550
+ let next;
551
+ try {
552
+ next = refresh.rotate(form.refresh_token ?? '', now(), accept);
553
+ }
554
+ catch (e) {
555
+ if (!(e instanceof SubjectCheckUnavailable))
556
+ throw e;
557
+ log(`token deferred grant=refresh_token: subject check failed: ${e.cause instanceof Error ? e.cause.message : 'non-Error throw'}`);
558
+ return subjectUnavailable(res);
559
+ }
517
560
  if (!next)
518
561
  return json(res, 400, { error: 'invalid_grant', message: 'unknown or rotated refresh token' });
519
562
  const access = await signAccessToken({ base, secret, iat: nowSec(), ttlSec: accessTtl, sub: next.sub });
@@ -44,7 +44,7 @@ export const KNOWN_ERROR_SLUGS = new Set([
44
44
  'insufficient_scope', 'internal', 'invalid_client', 'invalid_client_metadata',
45
45
  'invalid_grant', 'invalid_params', 'invalid_query', 'invalid_request',
46
46
  'invalid_scope', 'network_error', 'not_found', 'rate_limited',
47
- 'reauth_required', 'recipient_not_allowed', 'tenant_not_found', 'too_large', 'toolset_disabled', 'unknown_api',
47
+ 'reauth_required', 'recipient_not_allowed', 'temporarily_unavailable', 'tenant_not_found', 'too_large', 'toolset_disabled', 'unknown_api',
48
48
  'unknown_argument', 'unknown_method', 'unsupported_grant_type', 'unsupported_type',
49
49
  'untrusted_host', 'upstream_error', 'validation_error', 'write_disabled',
50
50
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-google-multi",
3
- "version": "6.0.0-alpha.59",
3
+ "version": "6.0.0-alpha.60",
4
4
  "mcpName": "io.github.bakissation/mcp-google-multi",
5
5
  "description": "Local MCP server for Google Workspace (Gmail, Drive, Calendar, Sheets, Docs, Contacts, Tasks, Meet, Search Console, +Forms/Chat/Admin) across multiple accounts — OAuth-only, encrypted token storage, deny-by-default writes.",
6
6
  "type": "module",