mcp-google-multi 6.0.0-alpha.58 → 6.0.0-alpha.59
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/dist/fs-atomic.d.ts +11 -0
- package/dist/fs-atomic.js +219 -28
- package/dist/oauth-as.d.ts +22 -4
- package/dist/oauth-as.js +19 -6
- package/package.json +1 -1
package/dist/fs-atomic.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/** Which process instance holds a lock: PID namespace id, start time in
|
|
2
|
+
* clock ticks since boot, and a boot id prefix. A part this process cannot
|
|
3
|
+
* read is `host` or `0` (all three without /proc). */
|
|
4
|
+
interface LockIdentity {
|
|
5
|
+
ns: string;
|
|
6
|
+
start: string;
|
|
7
|
+
boot: string;
|
|
8
|
+
}
|
|
9
|
+
/** Test hook: act as a process with this identity (undefined restores the real one). */
|
|
10
|
+
export declare function __setLockIdentityForTest(identity?: LockIdentity): void;
|
|
1
11
|
export declare function withFileLock<T>(absPath: string, fn: () => T, label?: string): T;
|
|
2
12
|
export declare function atomicWriteFileSync(absPath: string, contents: string, mode?: number): void;
|
|
3
13
|
export declare function atomicWriteWithLock(absPath: string, contents: string, mode?: number): void;
|
|
14
|
+
export {};
|
package/dist/fs-atomic.js
CHANGED
|
@@ -5,23 +5,216 @@ const LOCK_TIMEOUT_MS = 5_000;
|
|
|
5
5
|
const LOCK_RETRY_MS = 10;
|
|
6
6
|
const RENAME_ATTEMPTS = 5;
|
|
7
7
|
const RENAME_RETRY_MS = 20;
|
|
8
|
+
// A holder this process cannot identify (another PID namespace, another boot,
|
|
9
|
+
// or no /proc) is dead only once its lock is this old.
|
|
10
|
+
const LOCK_LEASE_MS = 60_000;
|
|
11
|
+
// Breaking a lock takes microseconds, so an older breaker file was left by a crash.
|
|
12
|
+
const BREAKER_STALE_MS = 2_000;
|
|
13
|
+
const PROCESS_START_MS = performance.timeOrigin;
|
|
14
|
+
const OWNER_NAME = /^(\d+)\.(\d+|host)\.(\d+)\.([0-9a-f]{8}|0)\.[0-9a-f]{12}\.owner$/;
|
|
15
|
+
function startOf(pid) {
|
|
16
|
+
try {
|
|
17
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
18
|
+
const start = stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19];
|
|
19
|
+
return /^\d+$/.test(start ?? '') ? start : undefined;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function selfIdentity() {
|
|
26
|
+
const identity = { ns: 'host', start: '0', boot: '0' };
|
|
27
|
+
try {
|
|
28
|
+
identity.ns = /^pid:\[(\d+)\]$/.exec(fs.readlinkSync('/proc/self/ns/pid'))?.[1] ?? 'host';
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// no /proc
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const boot = fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').replace(/[^0-9a-f]/g, '').slice(0, 8);
|
|
35
|
+
if (boot.length === 8)
|
|
36
|
+
identity.boot = boot;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// no /proc
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
// A /proc mounted for another PID namespace describes other processes,
|
|
43
|
+
// so no pid can be looked up in it.
|
|
44
|
+
if (fs.readlinkSync('/proc/self') === String(process.pid))
|
|
45
|
+
identity.start = startOf('self') ?? '0';
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// no /proc
|
|
49
|
+
}
|
|
50
|
+
return identity;
|
|
51
|
+
}
|
|
52
|
+
let SELF = selfIdentity();
|
|
53
|
+
/** Test hook: act as a process with this identity (undefined restores the real one). */
|
|
54
|
+
export function __setLockIdentityForTest(identity) {
|
|
55
|
+
SELF = identity ?? selfIdentity();
|
|
56
|
+
}
|
|
8
57
|
function sleep(ms) {
|
|
9
58
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
10
59
|
}
|
|
60
|
+
function sameFile(a, b) {
|
|
61
|
+
return a.ino === b.ino && a.dev === b.dev;
|
|
62
|
+
}
|
|
63
|
+
function isEnoent(error) {
|
|
64
|
+
return error.code === 'ENOENT';
|
|
65
|
+
}
|
|
66
|
+
// Undefined when the lock changed hands while it was being read.
|
|
67
|
+
function readHolder(lock) {
|
|
68
|
+
const before = fs.lstatSync(lock, { bigint: true });
|
|
69
|
+
const body = before.isFile() ? fs.readFileSync(lock, 'utf8') : '';
|
|
70
|
+
let owner;
|
|
71
|
+
if (before.isFile()) {
|
|
72
|
+
const dir = path.dirname(lock);
|
|
73
|
+
const prefix = `${path.basename(lock)}.`;
|
|
74
|
+
for (const name of fs.readdirSync(dir)) {
|
|
75
|
+
if (!name.startsWith(prefix))
|
|
76
|
+
continue;
|
|
77
|
+
const m = OWNER_NAME.exec(name.slice(prefix.length));
|
|
78
|
+
if (!m || m[1] !== body)
|
|
79
|
+
continue;
|
|
80
|
+
const ownerPath = path.join(dir, name);
|
|
81
|
+
try {
|
|
82
|
+
if (sameFile(fs.lstatSync(ownerPath, { bigint: true }), before)) {
|
|
83
|
+
owner = { path: ownerPath, ns: m[2], start: m[3], boot: m[4] };
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (!isEnoent(error))
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const after = fs.lstatSync(lock, { bigint: true });
|
|
94
|
+
if (!sameFile(before, after))
|
|
95
|
+
return undefined;
|
|
96
|
+
return { body, ino: after.ino, dev: after.dev, mtimeNs: after.mtimeNs, mtimeMs: Number(after.mtimeNs) / 1e6, owner };
|
|
97
|
+
}
|
|
98
|
+
// The owner file's random name tells two holders apart even when the second
|
|
99
|
+
// lock reuses the first one's freed inode.
|
|
100
|
+
function sameHolder(a, b) {
|
|
101
|
+
return sameFile(a, b) && a.body === b.body && a.mtimeNs === b.mtimeNs && a.owner?.path === b.owner?.path;
|
|
102
|
+
}
|
|
103
|
+
function probeDead(pid) {
|
|
104
|
+
try {
|
|
105
|
+
process.kill(pid, 0);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const code = error.code;
|
|
110
|
+
// EPERM: PID exists but is not signalable (another user's process).
|
|
111
|
+
if (code === 'ESRCH')
|
|
112
|
+
return true;
|
|
113
|
+
if (code !== 'EPERM')
|
|
114
|
+
throw error;
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function holderDead(holder, now) {
|
|
119
|
+
const pid = Number(holder.body);
|
|
120
|
+
if (!(Number.isSafeInteger(pid) && pid > 0))
|
|
121
|
+
return true;
|
|
122
|
+
const leaseOver = now - holder.mtimeMs >= LOCK_LEASE_MS;
|
|
123
|
+
const owner = holder.owner;
|
|
124
|
+
// No owner file: an older writer, whose namespace is unknown.
|
|
125
|
+
if (!owner)
|
|
126
|
+
return probeDead(pid) || leaseOver;
|
|
127
|
+
if (owner.ns !== SELF.ns || owner.boot !== SELF.boot)
|
|
128
|
+
return leaseOver;
|
|
129
|
+
// Two unreadable parts compare equal without naming the same namespace or
|
|
130
|
+
// boot, so only a fully known identity is looked up by start time.
|
|
131
|
+
if (SELF.ns !== 'host' && SELF.boot !== '0' && SELF.start !== '0' && owner.start !== '0') {
|
|
132
|
+
// A live namespace id is never shared, so the recorded pid is ours to
|
|
133
|
+
// look up: a restarted container reuses both the id and the pid, but not
|
|
134
|
+
// the start time.
|
|
135
|
+
const start = startOf(pid);
|
|
136
|
+
if (start !== undefined)
|
|
137
|
+
return start !== owner.start;
|
|
138
|
+
return probeDead(pid) || leaseOver;
|
|
139
|
+
}
|
|
140
|
+
if (pid === process.pid)
|
|
141
|
+
return holder.mtimeMs < PROCESS_START_MS;
|
|
142
|
+
return probeDead(pid) || leaseOver;
|
|
143
|
+
}
|
|
144
|
+
// One waiter at a time breaks a dead lock: two breakers could otherwise both
|
|
145
|
+
// see the dead holder, and the second would unlink the first one's new lock.
|
|
146
|
+
// False when another waiter is breaking it.
|
|
147
|
+
function breakLock(lock, holder) {
|
|
148
|
+
const breaker = `${lock}.break`;
|
|
149
|
+
// A token, not the inode: a breaker created after ours was removed can
|
|
150
|
+
// get our freed inode.
|
|
151
|
+
const token = `${process.pid}.${randomBytes(6).toString('hex')}`;
|
|
152
|
+
try {
|
|
153
|
+
fs.writeFileSync(breaker, token, { mode: 0o600, flag: 'wx' });
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (error.code !== 'EEXIST')
|
|
157
|
+
throw error;
|
|
158
|
+
clearStaleBreaker(breaker);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const current = readHolder(lock);
|
|
163
|
+
if (current && sameHolder(current, holder)) {
|
|
164
|
+
fs.rmSync(lock, { force: true });
|
|
165
|
+
if (holder.owner)
|
|
166
|
+
fs.rmSync(holder.owner.path, { force: true });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (!isEnoent(error))
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
releaseBreaker(breaker, token);
|
|
175
|
+
}
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
// A symlink, or a breaker dated the other side of a clock step, would
|
|
179
|
+
// otherwise block every break.
|
|
180
|
+
function clearStaleBreaker(breaker) {
|
|
181
|
+
try {
|
|
182
|
+
const st = fs.lstatSync(breaker);
|
|
183
|
+
if (!st.isFile() || Math.abs(Date.now() - st.mtimeMs) >= BREAKER_STALE_MS)
|
|
184
|
+
fs.rmSync(breaker, { force: true });
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (!isEnoent(error))
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function releaseBreaker(breaker, token) {
|
|
192
|
+
try {
|
|
193
|
+
if (fs.readFileSync(breaker, 'utf8') === token)
|
|
194
|
+
fs.rmSync(breaker, { force: true });
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (!isEnoent(error))
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
11
201
|
// Hardlink-based advisory lock, generalized path-keyed from the token store's
|
|
12
|
-
// alias-keyed original
|
|
13
|
-
//
|
|
202
|
+
// alias-keyed original. The lock body is the bare pid, as older readers expect;
|
|
203
|
+
// the owner file stays linked while held and names the holder's process
|
|
204
|
+
// instance. See docs/internals.md.
|
|
14
205
|
export function withFileLock(absPath, fn, label = `lock: ${absPath}`) {
|
|
15
206
|
const dir = path.dirname(absPath);
|
|
16
207
|
const lock = path.join(dir, `.${path.basename(absPath)}.lock`);
|
|
17
|
-
const ownerFile = `${lock}.${process.pid}.${randomBytes(6).toString('hex')}.owner`;
|
|
208
|
+
const ownerFile = `${lock}.${process.pid}.${SELF.ns}.${SELF.start}.${SELF.boot}.${randomBytes(6).toString('hex')}.owner`;
|
|
18
209
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
19
210
|
fs.writeFileSync(ownerFile, String(process.pid), { mode: 0o600, flag: 'wx' });
|
|
20
211
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
212
|
+
let linked = false;
|
|
21
213
|
try {
|
|
22
214
|
while (true) {
|
|
23
215
|
try {
|
|
24
216
|
fs.linkSync(ownerFile, lock);
|
|
217
|
+
linked = true;
|
|
25
218
|
break;
|
|
26
219
|
}
|
|
27
220
|
catch (error) {
|
|
@@ -29,33 +222,13 @@ export function withFileLock(absPath, fn, label = `lock: ${absPath}`) {
|
|
|
29
222
|
if (err.code !== 'EEXIST')
|
|
30
223
|
throw error;
|
|
31
224
|
try {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
let ownerDead = !(Number.isSafeInteger(owner) && owner > 0);
|
|
35
|
-
if (!ownerDead) {
|
|
36
|
-
try {
|
|
37
|
-
process.kill(owner, 0);
|
|
38
|
-
}
|
|
39
|
-
catch (ownerError) {
|
|
40
|
-
const code = ownerError.code;
|
|
41
|
-
// EPERM: PID exists but is not signalable (recycled by another
|
|
42
|
-
// user); treat as alive, never break a lock we cannot verify.
|
|
43
|
-
if (code === 'ESRCH')
|
|
44
|
-
ownerDead = true;
|
|
45
|
-
else if (code !== 'EPERM')
|
|
46
|
-
throw ownerError;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (ownerDead) {
|
|
50
|
-
if (fs.readFileSync(lock, 'utf8') === observedOwner)
|
|
51
|
-
fs.rmSync(lock, { force: true });
|
|
225
|
+
const holder = readHolder(lock);
|
|
226
|
+
if (holder && holderDead(holder, Date.now()) && breakLock(lock, holder))
|
|
52
227
|
continue;
|
|
53
|
-
}
|
|
54
228
|
}
|
|
55
229
|
catch (readError) {
|
|
56
|
-
if (readError
|
|
230
|
+
if (isEnoent(readError))
|
|
57
231
|
continue;
|
|
58
|
-
}
|
|
59
232
|
throw readError;
|
|
60
233
|
}
|
|
61
234
|
if (Date.now() >= deadline) {
|
|
@@ -66,14 +239,32 @@ export function withFileLock(absPath, fn, label = `lock: ${absPath}`) {
|
|
|
66
239
|
}
|
|
67
240
|
}
|
|
68
241
|
finally {
|
|
69
|
-
|
|
242
|
+
if (!linked)
|
|
243
|
+
fs.rmSync(ownerFile, { force: true });
|
|
70
244
|
}
|
|
245
|
+
let bound = false;
|
|
71
246
|
try {
|
|
247
|
+
// Some network and FUSE filesystems give each name of one file its own
|
|
248
|
+
// inode number; there the owner file cannot vouch for the lock.
|
|
249
|
+
bound = sameFile(fs.lstatSync(ownerFile, { bigint: true }), fs.lstatSync(lock, { bigint: true }));
|
|
72
250
|
return fn();
|
|
73
251
|
}
|
|
74
252
|
finally {
|
|
75
|
-
|
|
253
|
+
releaseLock(lock, ownerFile, bound);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// A holder whose lock was broken (its owner file removed with it) must not
|
|
257
|
+
// remove its successor's lock.
|
|
258
|
+
function releaseLock(lock, ownerFile, bound) {
|
|
259
|
+
try {
|
|
260
|
+
if (!bound || sameFile(fs.lstatSync(ownerFile, { bigint: true }), fs.lstatSync(lock, { bigint: true })))
|
|
261
|
+
fs.rmSync(lock, { force: true });
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
if (!isEnoent(error))
|
|
265
|
+
throw error;
|
|
76
266
|
}
|
|
267
|
+
fs.rmSync(ownerFile, { force: true });
|
|
77
268
|
}
|
|
78
269
|
export function atomicWriteFileSync(absPath, contents, mode = 0o600) {
|
|
79
270
|
const dir = path.dirname(absPath);
|
package/dist/oauth-as.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export declare const DCR_MAX_REDIRECT_URIS = 10;
|
|
|
6
6
|
export interface GoogleExchangeResult {
|
|
7
7
|
tokens: Record<string, unknown>;
|
|
8
8
|
email?: string;
|
|
9
|
+
/** Google's stable account id; unlike the address it is never reassigned. */
|
|
10
|
+
sub?: string;
|
|
11
|
+
/** Google Workspace hosted domain; absent for a consumer account. */
|
|
12
|
+
hd?: string;
|
|
9
13
|
}
|
|
10
14
|
export interface AuthServerConfig {
|
|
11
15
|
base: string;
|
|
@@ -31,6 +35,7 @@ export interface TenantAliasBind {
|
|
|
31
35
|
bundles?: string[];
|
|
32
36
|
nonce?: string;
|
|
33
37
|
tokens: Record<string, unknown>;
|
|
38
|
+
sub?: string;
|
|
34
39
|
}
|
|
35
40
|
/** A binder's deliberate refusal (policy, not failure): /callback answers 403
|
|
36
41
|
* with this slug and message. The message reaches an unauthenticated browser. */
|
|
@@ -40,8 +45,15 @@ export interface AliasBindRefusal {
|
|
|
40
45
|
message: string;
|
|
41
46
|
};
|
|
42
47
|
}
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
export interface GoogleIdentity {
|
|
49
|
+
email?: string;
|
|
50
|
+
sub?: string;
|
|
51
|
+
hd?: string;
|
|
52
|
+
}
|
|
53
|
+
/** Only trust the id_token identity when Google marks the email verified (#8).
|
|
54
|
+
* The token comes straight from Google's token endpoint, so its claims are
|
|
55
|
+
* read unsigned. A malformed sub or hd is dropped, never passed on. */
|
|
56
|
+
export declare function verifiedIdentityFromIdToken(idToken?: string | null): GoogleIdentity | undefined;
|
|
45
57
|
export declare function verifiedEmailFromIdToken(idToken?: string | null): string | undefined;
|
|
46
58
|
export interface AuthServerDeps {
|
|
47
59
|
fetchCimd?: (clientId: string) => Promise<Record<string, unknown>>;
|
|
@@ -76,8 +88,14 @@ export interface AuthServerDeps {
|
|
|
76
88
|
aliasEmail?: (alias: string) => string | undefined;
|
|
77
89
|
/** Map a verified sign-in email to the subject its tokens are minted under.
|
|
78
90
|
* Default = the MCP_OWNER_EMAILS allowlist -> {sub:'owner'}; null = denied.
|
|
79
|
-
* The tenant resolver (invite claim / email->tenant lookup) plugs in here.
|
|
80
|
-
|
|
91
|
+
* The tenant resolver (invite claim / email->tenant lookup) plugs in here.
|
|
92
|
+
* `identity` carries Google's `sub` and `hd` when the exchange returned
|
|
93
|
+
* them, so a resolver can key on the stable account id rather than an
|
|
94
|
+
* address that can be reassigned. */
|
|
95
|
+
resolveSubject?: (email: string, identity?: {
|
|
96
|
+
sub?: string;
|
|
97
|
+
hd?: string;
|
|
98
|
+
}) => {
|
|
81
99
|
sub: string;
|
|
82
100
|
} | null;
|
|
83
101
|
/** alias_add completion: persist the new alias's config row and tokens under
|
package/dist/oauth-as.js
CHANGED
|
@@ -17,21 +17,34 @@ const CLAUDE_AI_FIXED_CALLBACK = 'https://claude.ai/api/mcp/auth_callback';
|
|
|
17
17
|
export const DCR_MAX_CLIENTS = 1000;
|
|
18
18
|
export const DCR_MAX_REDIRECT_URIS = 10;
|
|
19
19
|
const DCR_MAX_URI_LEN = 2048;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const OIDC_SUB = /^[\x21-\x7e]{1,255}$/;
|
|
21
|
+
const HOSTED_DOMAIN = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/i;
|
|
22
|
+
/** Only trust the id_token identity when Google marks the email verified (#8).
|
|
23
|
+
* The token comes straight from Google's token endpoint, so its claims are
|
|
24
|
+
* read unsigned. A malformed sub or hd is dropped, never passed on. */
|
|
25
|
+
export function verifiedIdentityFromIdToken(idToken) {
|
|
23
26
|
if (!idToken)
|
|
24
27
|
return undefined;
|
|
25
28
|
try {
|
|
26
29
|
const [, payload] = idToken.split('.');
|
|
27
30
|
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8'));
|
|
28
31
|
const verified = claims.email_verified === true || claims.email_verified === 'true';
|
|
29
|
-
|
|
32
|
+
if (!verified)
|
|
33
|
+
return undefined;
|
|
34
|
+
const identity = { email: claims.email };
|
|
35
|
+
if (typeof claims.sub === 'string' && OIDC_SUB.test(claims.sub))
|
|
36
|
+
identity.sub = claims.sub;
|
|
37
|
+
if (typeof claims.hd === 'string' && claims.hd.length <= 253 && HOSTED_DOMAIN.test(claims.hd))
|
|
38
|
+
identity.hd = claims.hd;
|
|
39
|
+
return identity;
|
|
30
40
|
}
|
|
31
41
|
catch {
|
|
32
42
|
return undefined;
|
|
33
43
|
}
|
|
34
44
|
}
|
|
45
|
+
export function verifiedEmailFromIdToken(idToken) {
|
|
46
|
+
return verifiedIdentityFromIdToken(idToken)?.email;
|
|
47
|
+
}
|
|
35
48
|
// --- helpers ----------------------------------------------------------------
|
|
36
49
|
function json(res, status, body, extraHeaders = {}) {
|
|
37
50
|
res.writeHead(status, { 'Content-Type': 'application/json', ...extraHeaders });
|
|
@@ -383,7 +396,7 @@ export function buildAuthServer(config, deps = {}) {
|
|
|
383
396
|
return errorPage(res, 403, 'access_denied', 'Google did not return an email for the signed-in account');
|
|
384
397
|
let refusal;
|
|
385
398
|
try {
|
|
386
|
-
const out = await deps.bindTenantAlias({ tenantId: st.tenantId, alias: st.alias, email: boundEmail, bundles: st.bundles, nonce: st.nonce, tokens: exchanged.tokens });
|
|
399
|
+
const out = await deps.bindTenantAlias({ tenantId: st.tenantId, alias: st.alias, email: boundEmail, bundles: st.bundles, nonce: st.nonce, tokens: exchanged.tokens, sub: exchanged.sub });
|
|
387
400
|
refusal = out?.refused;
|
|
388
401
|
}
|
|
389
402
|
catch (e) {
|
|
@@ -452,7 +465,7 @@ export function buildAuthServer(config, deps = {}) {
|
|
|
452
465
|
// signed into the authz code and flows into every token minted from it.
|
|
453
466
|
const email = (exchanged.email ?? '').toLowerCase();
|
|
454
467
|
const iss = encodeURIComponent(base);
|
|
455
|
-
const subject = email ? resolveSubject(email) : null;
|
|
468
|
+
const subject = email ? resolveSubject(email, { sub: exchanged.sub, hd: exchanged.hd }) : null;
|
|
456
469
|
if (!subject) {
|
|
457
470
|
const sep = st.redirect_uri.includes('?') ? '&' : '?';
|
|
458
471
|
const s = st.client_state ? `&state=${encodeURIComponent(st.client_state)}` : '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-multi",
|
|
3
|
-
"version": "6.0.0-alpha.
|
|
3
|
+
"version": "6.0.0-alpha.59",
|
|
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",
|