mcp-google-multi 6.0.0-beta.2 → 6.0.0-beta.3
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/accounts.d.ts +17 -1
- package/dist/accounts.js +52 -15
- package/dist/api-probe.d.ts +5 -0
- package/dist/api-probe.js +20 -15
- package/dist/arg-normalize.js +6 -5
- package/dist/arg-strict.d.ts +7 -0
- package/dist/arg-strict.js +34 -4
- package/dist/auth.d.ts +4 -3
- package/dist/auth.js +11 -13
- package/dist/boot-gates.d.ts +26 -0
- package/dist/boot-gates.js +38 -0
- package/dist/cimd-cache.d.ts +38 -0
- package/dist/cimd-cache.js +117 -0
- package/dist/client.d.ts +32 -2
- package/dist/client.js +0 -0
- package/dist/compose.d.ts +11 -0
- package/dist/compose.js +109 -0
- package/dist/config-file.d.ts +13 -0
- package/dist/config-file.js +37 -1
- package/dist/discover.js +3 -1
- package/dist/discovery-client.d.ts +3 -0
- package/dist/discovery-client.js +25 -2
- package/dist/doctor.d.ts +6 -2
- package/dist/doctor.js +119 -23
- package/dist/executor.d.ts +3 -0
- package/dist/executor.js +4 -2
- package/dist/fanout.d.ts +9 -1
- package/dist/fanout.js +44 -10
- package/dist/fs-atomic.d.ts +11 -0
- package/dist/fs-atomic.js +219 -28
- package/dist/hint-provider.d.ts +129 -0
- package/dist/hint-provider.js +64 -0
- package/dist/http-transport.d.ts +62 -4
- package/dist/http-transport.js +343 -24
- package/dist/identity.d.ts +4 -1
- package/dist/identity.js +25 -6
- package/dist/index.js +66 -138
- package/dist/master-key.js +13 -5
- package/dist/mcp-token.d.ts +136 -19
- package/dist/mcp-token.js +569 -72
- package/dist/migrate-config.js +2 -2
- package/dist/oauth-as.d.ts +103 -3
- package/dist/oauth-as.js +374 -79
- package/dist/oauth-consent.d.ts +10 -0
- package/dist/oauth-consent.js +16 -0
- package/dist/outbound-allowlist.js +5 -2
- package/dist/reauth-hint.d.ts +1 -1
- package/dist/reauth-hint.js +12 -8
- package/dist/registry-transfer.d.ts +5 -0
- package/dist/registry-transfer.js +65 -5
- package/dist/registry.d.ts +18 -1
- package/dist/registry.js +71 -15
- package/dist/scope-catalog.d.ts +11 -0
- package/dist/scope-catalog.js +23 -6
- package/dist/scope-observability.d.ts +8 -6
- package/dist/scope-observability.js +29 -13
- package/dist/services.d.ts +15 -10
- package/dist/services.js +59 -15
- package/dist/ssrf-guard.d.ts +23 -9
- package/dist/ssrf-guard.js +101 -62
- package/dist/tenant-purge.d.ts +26 -0
- package/dist/tenant-purge.js +67 -0
- package/dist/token-store.d.ts +20 -2
- package/dist/token-store.js +0 -0
- package/dist/tools/_errors.d.ts +3 -0
- package/dist/tools/_errors.js +53 -24
- package/dist/tools/_local-files.d.ts +9 -0
- package/dist/tools/_local-files.js +18 -0
- package/dist/tools/account-wizard.d.ts +10 -1
- package/dist/tools/account-wizard.js +74 -17
- package/dist/tools/accounts-tool.d.ts +7 -0
- package/dist/tools/accounts-tool.js +34 -18
- package/dist/tools/admin.d.ts +10 -1
- package/dist/tools/admin.js +15 -11
- package/dist/tools/analytics.d.ts +10 -1
- package/dist/tools/analytics.js +13 -8
- package/dist/tools/calendar.d.ts +2 -1
- package/dist/tools/calendar.js +18 -14
- package/dist/tools/chat.d.ts +10 -1
- package/dist/tools/chat.js +13 -8
- package/dist/tools/contacts.d.ts +2 -1
- package/dist/tools/contacts.js +17 -13
- package/dist/tools/docs.d.ts +2 -1
- package/dist/tools/docs.js +34 -30
- package/dist/tools/drive.d.ts +2 -1
- package/dist/tools/drive.js +65 -50
- package/dist/tools/forms.d.ts +10 -1
- package/dist/tools/forms.js +19 -12
- package/dist/tools/generated/_shared.d.ts +2 -1
- package/dist/tools/generated/_shared.js +7 -3
- package/dist/tools/generated/admin.d.ts +2 -1
- package/dist/tools/generated/admin.js +265 -265
- package/dist/tools/generated/analytics.d.ts +2 -1
- package/dist/tools/generated/analytics.js +125 -125
- package/dist/tools/generated/appsmarket.d.ts +2 -1
- package/dist/tools/generated/appsmarket.js +5 -5
- package/dist/tools/generated/calendar.d.ts +2 -1
- package/dist/tools/generated/calendar.js +55 -55
- package/dist/tools/generated/chat.d.ts +2 -1
- package/dist/tools/generated/chat.js +95 -95
- package/dist/tools/generated/classroom.d.ts +2 -1
- package/dist/tools/generated/classroom.js +209 -209
- package/dist/tools/generated/cloudidentity.d.ts +2 -1
- package/dist/tools/generated/cloudidentity.js +133 -133
- package/dist/tools/generated/cloudsearch.d.ts +2 -1
- package/dist/tools/generated/cloudsearch.js +99 -99
- package/dist/tools/generated/contacts.d.ts +2 -1
- package/dist/tools/generated/contacts.js +29 -29
- package/dist/tools/generated/drive.d.ts +2 -1
- package/dist/tools/generated/drive.js +71 -71
- package/dist/tools/generated/driveactivity.d.ts +2 -1
- package/dist/tools/generated/driveactivity.js +3 -3
- package/dist/tools/generated/drivelabels.d.ts +2 -1
- package/dist/tools/generated/drivelabels.js +53 -53
- package/dist/tools/generated/forms.d.ts +2 -1
- package/dist/tools/generated/forms.js +7 -7
- package/dist/tools/generated/gmail.d.ts +2 -1
- package/dist/tools/generated/gmail.js +117 -117
- package/dist/tools/generated/groupsmigration.d.ts +2 -1
- package/dist/tools/generated/groupsmigration.js +3 -3
- package/dist/tools/generated/groupssettings.d.ts +2 -1
- package/dist/tools/generated/groupssettings.js +7 -7
- package/dist/tools/generated/index.d.ts +2 -1
- package/dist/tools/generated/keep.d.ts +2 -1
- package/dist/tools/generated/keep.js +15 -15
- package/dist/tools/generated/licensing.d.ts +2 -1
- package/dist/tools/generated/licensing.js +15 -15
- package/dist/tools/generated/meet.d.ts +2 -1
- package/dist/tools/generated/meet.js +27 -27
- package/dist/tools/generated/postmaster.d.ts +2 -1
- package/dist/tools/generated/postmaster.js +9 -9
- package/dist/tools/generated/reseller.d.ts +2 -1
- package/dist/tools/generated/reseller.js +35 -35
- package/dist/tools/generated/script.d.ts +2 -1
- package/dist/tools/generated/script.js +33 -33
- package/dist/tools/generated/searchconsole.d.ts +2 -1
- package/dist/tools/generated/searchconsole.js +3 -3
- package/dist/tools/generated/sheets.d.ts +2 -1
- package/dist/tools/generated/sheets.js +15 -15
- package/dist/tools/generated/tasks.d.ts +2 -1
- package/dist/tools/generated/tasks.js +5 -5
- package/dist/tools/generated/vault.d.ts +2 -1
- package/dist/tools/generated/vault.js +67 -67
- package/dist/tools/generated/workspaceevents.d.ts +2 -1
- package/dist/tools/generated/workspaceevents.js +31 -31
- package/dist/tools/gmail.d.ts +2 -1
- package/dist/tools/gmail.js +45 -33
- package/dist/tools/google-api.d.ts +2 -3
- package/dist/tools/google-api.js +30 -16
- package/dist/tools/meet.d.ts +10 -1
- package/dist/tools/meet.js +13 -9
- package/dist/tools/searchconsole.d.ts +2 -1
- package/dist/tools/searchconsole.js +17 -13
- package/dist/tools/sheets.d.ts +2 -1
- package/dist/tools/sheets.js +36 -32
- package/dist/tools/slides.d.ts +10 -1
- package/dist/tools/slides.js +17 -10
- package/dist/tools/tasks.d.ts +2 -1
- package/dist/tools/tasks.js +19 -15
- package/dist/trim.d.ts +5 -0
- package/dist/trim.js +20 -0
- package/dist/usage-metrics.d.ts +23 -0
- package/dist/usage-metrics.js +65 -47
- package/dist/write-control.js +10 -2
- package/package.json +27 -1
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);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { DenyReason, Policy } from './write-control.js';
|
|
2
|
+
export type HintCase = {
|
|
3
|
+
kind: 'reauth';
|
|
4
|
+
account: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'legacy_plaintext_token';
|
|
7
|
+
account: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'dead_refresh_token';
|
|
10
|
+
account: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'token_unreadable';
|
|
13
|
+
account: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'scopes_missing';
|
|
16
|
+
account: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'scope_not_granted';
|
|
19
|
+
account: string;
|
|
20
|
+
scope: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'scope_needs_bundle';
|
|
23
|
+
account: string;
|
|
24
|
+
scope: string;
|
|
25
|
+
bundle: string;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'scope_base_excluded';
|
|
28
|
+
account: string;
|
|
29
|
+
scope: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'scope_account_type';
|
|
32
|
+
account: string;
|
|
33
|
+
scope: string;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'scope_unknown_scope';
|
|
36
|
+
scope: string;
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'scope_unknown';
|
|
39
|
+
account: string;
|
|
40
|
+
serviceText?: string;
|
|
41
|
+
bundle?: string;
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'write_denied';
|
|
44
|
+
tool: {
|
|
45
|
+
name: string;
|
|
46
|
+
service: string;
|
|
47
|
+
cud: string;
|
|
48
|
+
};
|
|
49
|
+
reason: DenyReason;
|
|
50
|
+
policy: Policy;
|
|
51
|
+
} | {
|
|
52
|
+
kind: 'policy_summary';
|
|
53
|
+
policy: Policy;
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'outbound_denied';
|
|
56
|
+
what: string;
|
|
57
|
+
blocked: string[];
|
|
58
|
+
entries: string[];
|
|
59
|
+
} | {
|
|
60
|
+
kind: 'no_default_account';
|
|
61
|
+
aliases: string[];
|
|
62
|
+
} | {
|
|
63
|
+
kind: 'no_accounts';
|
|
64
|
+
} | {
|
|
65
|
+
kind: 'api_not_enabled';
|
|
66
|
+
api?: string;
|
|
67
|
+
service?: string;
|
|
68
|
+
project?: string;
|
|
69
|
+
googleMessage: string;
|
|
70
|
+
} | {
|
|
71
|
+
kind: 'apis_disabled';
|
|
72
|
+
services: {
|
|
73
|
+
service: string;
|
|
74
|
+
api: string;
|
|
75
|
+
}[];
|
|
76
|
+
} | {
|
|
77
|
+
kind: 'daily_quota';
|
|
78
|
+
} | {
|
|
79
|
+
kind: 'project_suspended';
|
|
80
|
+
account: string;
|
|
81
|
+
} | {
|
|
82
|
+
kind: 'invalid_scope';
|
|
83
|
+
account: string;
|
|
84
|
+
} | {
|
|
85
|
+
kind: 'no_oauth_client';
|
|
86
|
+
} | {
|
|
87
|
+
kind: 'network_error';
|
|
88
|
+
code: string;
|
|
89
|
+
} | {
|
|
90
|
+
kind: 'local_path';
|
|
91
|
+
code: string;
|
|
92
|
+
path?: string;
|
|
93
|
+
} | {
|
|
94
|
+
kind: 'internal';
|
|
95
|
+
account?: string;
|
|
96
|
+
} | {
|
|
97
|
+
kind: 'untrusted_host';
|
|
98
|
+
} | {
|
|
99
|
+
kind: 'toolset_disabled';
|
|
100
|
+
api: string;
|
|
101
|
+
service: string;
|
|
102
|
+
} | {
|
|
103
|
+
kind: 'service_not_enabled';
|
|
104
|
+
tool: string;
|
|
105
|
+
service: string;
|
|
106
|
+
bundle?: string;
|
|
107
|
+
aliases: string[];
|
|
108
|
+
admin: boolean;
|
|
109
|
+
toolsetOff: boolean;
|
|
110
|
+
awaitingRestart: boolean;
|
|
111
|
+
} | {
|
|
112
|
+
kind: 'operator_only';
|
|
113
|
+
slug: string;
|
|
114
|
+
};
|
|
115
|
+
export declare const HINT_KINDS: readonly ["reauth", "legacy_plaintext_token", "dead_refresh_token", "token_unreadable", "scopes_missing", "scope_not_granted", "scope_needs_bundle", "scope_base_excluded", "scope_account_type", "scope_unknown_scope", "scope_unknown", "write_denied", "policy_summary", "outbound_denied", "no_default_account", "no_accounts", "api_not_enabled", "apis_disabled", "daily_quota", "project_suspended", "invalid_scope", "no_oauth_client", "network_error", "local_path", "internal", "untrusted_host", "toolset_disabled", "service_not_enabled", "operator_only"];
|
|
116
|
+
/** `message` only for the kinds whose message leaks today (write_denied,
|
|
117
|
+
* no_oauth_client, local_path, internal, toolset_disabled); every other call
|
|
118
|
+
* site ignores it. `retriable` only where it may legitimately differ from
|
|
119
|
+
* the owner's default (scope_not_granted). */
|
|
120
|
+
export interface Rendered {
|
|
121
|
+
hint: string;
|
|
122
|
+
message?: string;
|
|
123
|
+
retriable?: boolean;
|
|
124
|
+
}
|
|
125
|
+
export interface HintProvider {
|
|
126
|
+
render(c: HintCase): Rendered;
|
|
127
|
+
}
|
|
128
|
+
export declare function setHintProvider(p: HintProvider | null): void;
|
|
129
|
+
export declare function hints(): HintProvider | null;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// A plain array literal, not derived from the union: deriving it would need a
|
|
2
|
+
// value-level mirror of the type anyway, and this way a missing member is
|
|
3
|
+
// caught by the completeness check below instead of silently compiling.
|
|
4
|
+
// `as const` so that check narrows to the literal tuple rather than widening
|
|
5
|
+
// every element back to the whole HintCase['kind'] union (which would make
|
|
6
|
+
// it pass trivially, whatever was listed).
|
|
7
|
+
export const HINT_KINDS = [
|
|
8
|
+
'reauth',
|
|
9
|
+
'legacy_plaintext_token',
|
|
10
|
+
'dead_refresh_token',
|
|
11
|
+
'token_unreadable',
|
|
12
|
+
'scopes_missing',
|
|
13
|
+
'scope_not_granted',
|
|
14
|
+
'scope_needs_bundle',
|
|
15
|
+
'scope_base_excluded',
|
|
16
|
+
'scope_account_type',
|
|
17
|
+
'scope_unknown_scope',
|
|
18
|
+
'scope_unknown',
|
|
19
|
+
'write_denied',
|
|
20
|
+
'policy_summary',
|
|
21
|
+
'outbound_denied',
|
|
22
|
+
'no_default_account',
|
|
23
|
+
'no_accounts',
|
|
24
|
+
'api_not_enabled',
|
|
25
|
+
'apis_disabled',
|
|
26
|
+
'daily_quota',
|
|
27
|
+
'project_suspended',
|
|
28
|
+
'invalid_scope',
|
|
29
|
+
'no_oauth_client',
|
|
30
|
+
'network_error',
|
|
31
|
+
'local_path',
|
|
32
|
+
'internal',
|
|
33
|
+
'untrusted_host',
|
|
34
|
+
'toolset_disabled',
|
|
35
|
+
'service_not_enabled',
|
|
36
|
+
'operator_only',
|
|
37
|
+
];
|
|
38
|
+
const _hintKindsComplete = true;
|
|
39
|
+
void _hintKindsComplete;
|
|
40
|
+
// Process-global, like the HTTP reauth linker: one process is either the
|
|
41
|
+
// single owner or one multi-tenant host, never both.
|
|
42
|
+
let installed = null;
|
|
43
|
+
export function setHintProvider(p) {
|
|
44
|
+
installed = p;
|
|
45
|
+
}
|
|
46
|
+
// A throwing provider must never take down the call site: every call site
|
|
47
|
+
// does `hints()?.render(...)?.hint ?? <default>`, so a render that cannot
|
|
48
|
+
// produce a Rendered falls back to the same literal a missing provider
|
|
49
|
+
// would, and the provider's own error text never reaches a client.
|
|
50
|
+
export function hints() {
|
|
51
|
+
if (!installed)
|
|
52
|
+
return null;
|
|
53
|
+
const provider = installed;
|
|
54
|
+
return {
|
|
55
|
+
render(c) {
|
|
56
|
+
try {
|
|
57
|
+
return provider.render(c);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
package/dist/http-transport.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ import type { McpServer, Transport } from "@modelcontextprotocol/server";
|
|
|
2
2
|
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
3
3
|
import type { HttpConfig } from './http-config.js';
|
|
4
4
|
import { type ArgShape, type StrictArgOptions, type ValidationEnvelopeOptions } from './arg-normalize.js';
|
|
5
|
+
export { logSafe } from './trim.js';
|
|
5
6
|
export type AuthOutcome = {
|
|
6
7
|
ok: true;
|
|
8
|
+
sub?: string;
|
|
7
9
|
} | {
|
|
8
10
|
ok: false;
|
|
9
11
|
status: number;
|
|
@@ -14,6 +16,16 @@ export type AuthOutcome = {
|
|
|
14
16
|
export type Authenticator = (req: IncomingMessage) => AuthOutcome | Promise<AuthOutcome>;
|
|
15
17
|
/** A mounted extra route (the AS endpoints, B13). Return true if it wrote a response. */
|
|
16
18
|
export type RouteHandler = (req: IncomingMessage, res: ServerResponse, url: URL) => boolean | Promise<boolean>;
|
|
19
|
+
/** What a verified subject resolves to: ITS server plus the per-request hooks
|
|
20
|
+
* bound to that server's registry (tool shapes, declared keys, the default
|
|
21
|
+
* account a validation envelope names). The metrics observers (metricsTap,
|
|
22
|
+
* onArgRename) stay host-wide and must not be bound to any one registry. */
|
|
23
|
+
export interface ServerTarget {
|
|
24
|
+
server: McpServer;
|
|
25
|
+
argShapeFor?: (tool: string) => ArgShape | undefined;
|
|
26
|
+
strictArgs?: StrictArgOptions;
|
|
27
|
+
validationEnvelope?: ValidationEnvelopeOptions;
|
|
28
|
+
}
|
|
17
29
|
export interface HttpHostOptions {
|
|
18
30
|
/** The ONE McpServer + registry built at boot (P1 / BV gap #4: never per request). */
|
|
19
31
|
server: McpServer;
|
|
@@ -21,6 +33,26 @@ export interface HttpHostOptions {
|
|
|
21
33
|
version: string;
|
|
22
34
|
ownerConfigured: boolean;
|
|
23
35
|
authenticate: Authenticator;
|
|
36
|
+
/** Tenant-resolution seam: map the verified subject to ITS server. Absent
|
|
37
|
+
* (free core) = every request dispatches to the one boot `server`. Return
|
|
38
|
+
* null for an unknown subject -> 403 tenant_not_found. Each distinct
|
|
39
|
+
* resolved server gets its own dispatch lane. With a resolver, the
|
|
40
|
+
* per-request hooks come from the resolved target ONLY: the host-level
|
|
41
|
+
* argShapeFor / strictArgs / validationEnvelope describe the boot server's
|
|
42
|
+
* registry, never another subject's. */
|
|
43
|
+
resolveServer?: (claims: {
|
|
44
|
+
sub: string;
|
|
45
|
+
}) => Promise<ServerTarget | null | {
|
|
46
|
+
refused: {
|
|
47
|
+
error: string;
|
|
48
|
+
message: string;
|
|
49
|
+
};
|
|
50
|
+
}> | ServerTarget | null | {
|
|
51
|
+
refused: {
|
|
52
|
+
error: string;
|
|
53
|
+
message: string;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
24
56
|
/** Extra routes keyed by exact pathname (AS endpoints mount here in B13). */
|
|
25
57
|
routes?: Record<string, RouteHandler>;
|
|
26
58
|
log?: (line: string) => void;
|
|
@@ -40,6 +72,11 @@ export interface HttpHostOptions {
|
|
|
40
72
|
onArgRename?: (tool: string, renames: number) => void;
|
|
41
73
|
/** Unknown-argument screening (arg-strict.ts); absent = off. */
|
|
42
74
|
strictArgs?: StrictArgOptions;
|
|
75
|
+
/** Per-server cap on /mcp requests reading their body, queued or running;
|
|
76
|
+
* the next answers 429 lane_busy before its body is read. Unset = no cap. */
|
|
77
|
+
maxQueuedPerLane?: number;
|
|
78
|
+
/** Max JSON-RPC messages in one /mcp body (default 16). */
|
|
79
|
+
maxBatchFrames?: number;
|
|
43
80
|
}
|
|
44
81
|
export declare function parseOwnerEmails(env?: NodeJS.ProcessEnv): string[];
|
|
45
82
|
/** Front guard: an Origin, if present, must be allowlisted; a Host must be
|
|
@@ -47,17 +84,37 @@ export declare function parseOwnerEmails(env?: NodeJS.ProcessEnv): string[];
|
|
|
47
84
|
* connector calls /mcp server-to-server with no Origin, BV-4). */
|
|
48
85
|
export declare function originAllowed(origin: string | undefined, allowed: string[]): boolean;
|
|
49
86
|
export declare function hostAllowed(host: string | undefined, allowed: string[]): boolean;
|
|
50
|
-
/** JSON-RPC method name(s) for an observability log line
|
|
87
|
+
/** JSON-RPC method name(s) for an observability log line: no params, no PII.
|
|
88
|
+
* The method is the caller's: anything not shaped like a method name, which
|
|
89
|
+
* includes a value whose String() throws, is logged as `<invalid>`. */
|
|
51
90
|
export declare function jsonRpcMethod(body: unknown): string;
|
|
52
91
|
export declare class HttpTransportHost {
|
|
53
92
|
private readonly opts;
|
|
54
93
|
private httpServer?;
|
|
55
|
-
private
|
|
94
|
+
private readonly locks;
|
|
95
|
+
private readonly depth;
|
|
96
|
+
private readonly sockets;
|
|
97
|
+
private readonly pending;
|
|
98
|
+
private readonly served;
|
|
99
|
+
private readonly handling;
|
|
100
|
+
private closing;
|
|
101
|
+
private graceful?;
|
|
56
102
|
constructor(opts: HttpHostOptions);
|
|
57
|
-
private
|
|
103
|
+
private serializeFor;
|
|
58
104
|
private log;
|
|
59
105
|
start(): Promise<void>;
|
|
60
|
-
|
|
106
|
+
/** Without graceMs, every connection is cut at once, even during a graceful
|
|
107
|
+
* close. With it, the host stops accepting, lets in-flight requests finish
|
|
108
|
+
* and their responses finish writing, then resolves once each client has
|
|
109
|
+
* closed its side; whatever is still open after graceMs is cut. A repeat
|
|
110
|
+
* call with graceMs during a graceful close joins it, keeping the first
|
|
111
|
+
* grace. graceMs must be a number from 0 to 2^31-1 ms (about 24.8 days, the
|
|
112
|
+
* longest delay a timer holds); anything else, Infinity included, rejects
|
|
113
|
+
* with a TypeError and leaves the host as it was. */
|
|
114
|
+
close(opts?: {
|
|
115
|
+
graceMs?: number;
|
|
116
|
+
}): Promise<void>;
|
|
117
|
+
private closeGracefully;
|
|
61
118
|
/** The bound port (useful when listening on port 0 in tests). */
|
|
62
119
|
address(): {
|
|
63
120
|
port: number;
|
|
@@ -65,6 +122,7 @@ export declare class HttpTransportHost {
|
|
|
65
122
|
private handle;
|
|
66
123
|
private frontGuard;
|
|
67
124
|
private mcp;
|
|
125
|
+
private admitted;
|
|
68
126
|
private health;
|
|
69
127
|
private readJson;
|
|
70
128
|
private fail;
|