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/accounts.d.ts
CHANGED
|
@@ -18,7 +18,11 @@ export interface AccountSet {
|
|
|
18
18
|
defaultAccount?: string;
|
|
19
19
|
defaultAccountSource?: 'env' | 'config';
|
|
20
20
|
}
|
|
21
|
-
export declare function resolveAccounts(env?: NodeJS.ProcessEnv, filePath?: string, onInvalid?: 'exit' | 'throw'
|
|
21
|
+
export declare function resolveAccounts(env?: NodeJS.ProcessEnv, filePath?: string, onInvalid?: 'exit' | 'throw', opts?: {
|
|
22
|
+
tokenDir?: string;
|
|
23
|
+
emptyOk?: boolean;
|
|
24
|
+
}): AccountSet;
|
|
25
|
+
export declare function fileStamp(filePath: string, version: number): string;
|
|
22
26
|
/** Live accessor: dispatch-time readers use this, never a captured snapshot. */
|
|
23
27
|
export declare function getAccountSet(): AccountSet;
|
|
24
28
|
/** Re-resolve after a config.json mutation and swap the live set. */
|
|
@@ -45,6 +49,18 @@ export declare function unknownAliasMessage(aliases: readonly string[], selector
|
|
|
45
49
|
export declare function accountAliasSchemaFor(aliases: readonly string[]): z.ZodType<string>;
|
|
46
50
|
/** Shared, load-time snapshot used by every tool's `account` field. */
|
|
47
51
|
export declare const accountAliasSchema: z.ZodType<string>;
|
|
52
|
+
/** Live account argument: validates at PARSE time against the accessor's
|
|
53
|
+
* CURRENT aliases, so an alias added mid-session (account_add, or a tenant
|
|
54
|
+
* alias link) is immediately valid on already-registered tools — a baked
|
|
55
|
+
* z.enum freezes the boot-time set until restart. An empty set stays
|
|
56
|
+
* permissive (same empty-safe rule as above); dispatch still resolves the
|
|
57
|
+
* alias against the live registry. */
|
|
58
|
+
export declare function accountArgLive(aliases: () => readonly string[]): z.ZodType<string>;
|
|
59
|
+
/** The live membership check behind accountArgLive and the fan-out field;
|
|
60
|
+
* `selectors` also admits "*". An empty set stays permissive. */
|
|
61
|
+
export declare function liveAccountCheck(aliases: () => readonly string[], selectors: boolean): z.core.$ZodCheck<string>;
|
|
62
|
+
/** True for a field built on liveAccountCheck, optional or not. */
|
|
63
|
+
export declare function isLiveAccountField(field: unknown): boolean;
|
|
48
64
|
/**
|
|
49
65
|
* BR-4: the stdio/http SERVER never boots with an empty registry — a fresh user
|
|
50
66
|
* bootstraps via env / `migrate-config` / `account import` / `auth` first. The
|
package/dist/accounts.js
CHANGED
|
@@ -2,8 +2,8 @@ import path from 'node:path';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { loadEnvFiles } from './env-load.js';
|
|
5
|
-
import { CONFIG_VERSION, configDir, configFilePath, failStartup, loadConfigFile, mutateConfigFile } from './config-file.js';
|
|
6
|
-
import {
|
|
5
|
+
import { ALIAS_RE, CONFIG_VERSION, configDir, configFilePath, failStartup, isReservedAlias, loadConfigFile, mutateConfigFile } from './config-file.js';
|
|
6
|
+
import { closestBundle, isKnownBundle, resolveBundleAliases } from './scope-catalog.js';
|
|
7
7
|
const envLoad = loadEnvFiles();
|
|
8
8
|
const defaultTokenDir = path.join(configDir(), 'tokens');
|
|
9
9
|
const tokenDir = process.env.TOKEN_STORE_PATH
|
|
@@ -15,14 +15,14 @@ export function getTokenDir() {
|
|
|
15
15
|
function parseCsv(value) {
|
|
16
16
|
return (value ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
17
17
|
}
|
|
18
|
-
function accountPaths(alias) {
|
|
18
|
+
function accountPaths(alias, dir = tokenDir) {
|
|
19
19
|
return {
|
|
20
|
-
tokenPath: path.join(
|
|
21
|
-
encPath: path.join(
|
|
20
|
+
tokenPath: path.join(dir, alias, 'token.json'),
|
|
21
|
+
encPath: path.join(dir, `${alias}.enc`),
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
/** v5 env parser, guards verbatim. Format: GOOGLE_ACCOUNTS="alias1:email1,alias2:email2". */
|
|
25
|
-
function parseEnvAccounts(raw, adminAliases) {
|
|
25
|
+
function parseEnvAccounts(raw, adminAliases, dir = tokenDir) {
|
|
26
26
|
const configs = {};
|
|
27
27
|
const aliases = [];
|
|
28
28
|
for (const entry of raw.split(',')) {
|
|
@@ -40,7 +40,7 @@ function parseEnvAccounts(raw, adminAliases) {
|
|
|
40
40
|
}
|
|
41
41
|
// Restrict alias to a safe charset so it can't escape `tokenDir` via path traversal
|
|
42
42
|
// (e.g. "../../etc/passwd:foo@bar.com" in .env).
|
|
43
|
-
if (
|
|
43
|
+
if (!ALIAS_RE.test(alias) || isReservedAlias(alias)) {
|
|
44
44
|
throw new Error(`Invalid alias "${alias}". Allowed characters: letters, digits, underscore, hyphen.`);
|
|
45
45
|
}
|
|
46
46
|
if (aliases.includes(alias)) {
|
|
@@ -49,7 +49,7 @@ function parseEnvAccounts(raw, adminAliases) {
|
|
|
49
49
|
aliases.push(alias);
|
|
50
50
|
configs[alias] = {
|
|
51
51
|
email,
|
|
52
|
-
...accountPaths(alias),
|
|
52
|
+
...accountPaths(alias, dir),
|
|
53
53
|
admin: adminAliases.includes(alias) || undefined,
|
|
54
54
|
source: 'env',
|
|
55
55
|
};
|
|
@@ -74,7 +74,11 @@ function noAccountsMessage() {
|
|
|
74
74
|
* empty behaves as unset, mirroring GOOGLE_ACCOUNTS semantics.
|
|
75
75
|
*/
|
|
76
76
|
let warnedLegacy = false;
|
|
77
|
-
export function resolveAccounts(env = process.env, filePath = configFilePath(), onInvalid = 'exit'
|
|
77
|
+
export function resolveAccounts(env = process.env, filePath = configFilePath(), onInvalid = 'exit',
|
|
78
|
+
// emptyOk: zero accounts is a valid set, not an error, even with 'throw'
|
|
79
|
+
// (a context that has linked nothing yet). An invalid file still fails.
|
|
80
|
+
opts = {}) {
|
|
81
|
+
const dir = opts.tokenDir ?? tokenDir;
|
|
78
82
|
const adminEnv = parseCsv(env.GOOGLE_ADMIN_ACCOUNTS);
|
|
79
83
|
const rawEnv = env.GOOGLE_ACCOUNTS;
|
|
80
84
|
const fail = (slug, message) => {
|
|
@@ -91,7 +95,7 @@ export function resolveAccounts(env = process.env, filePath = configFilePath(),
|
|
|
91
95
|
if (bundle === 'admin') {
|
|
92
96
|
fail('E_UNKNOWN_BUNDLE', '"admin" is not a global bundle: grant it per account via GOOGLE_ADMIN_ACCOUNTS or an "admin: true" scope profile.');
|
|
93
97
|
}
|
|
94
|
-
if (!(bundle
|
|
98
|
+
if (!isKnownBundle(bundle)) {
|
|
95
99
|
const hint = closestBundle(bundle);
|
|
96
100
|
fail('E_UNKNOWN_BUNDLE', `unknown bundle "${bundle}" in GOOGLE_OPTIONAL_SCOPES${hint ? ` — did you mean "${hint}"?` : ''}`);
|
|
97
101
|
}
|
|
@@ -102,7 +106,7 @@ export function resolveAccounts(env = process.env, filePath = configFilePath(),
|
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
if (rawEnv && rawEnv.trim() !== '') {
|
|
105
|
-
const { aliases, configs } = parseEnvAccounts(rawEnv, adminEnv);
|
|
109
|
+
const { aliases, configs } = parseEnvAccounts(rawEnv, adminEnv, dir);
|
|
106
110
|
materializeFirstRun(aliases, configs, filePath);
|
|
107
111
|
const def = resolveDefaultAccount(env, null, aliases, fail);
|
|
108
112
|
return {
|
|
@@ -129,7 +133,7 @@ export function resolveAccounts(env = process.env, filePath = configFilePath(),
|
|
|
129
133
|
// empty (BR-4), enforced by assertServerAccountsConfigured() in index.ts.
|
|
130
134
|
// The dispatch-path reload ('throw') still throws so a mid-session emptied
|
|
131
135
|
// config.json keeps the last-good registry (BR-7) instead of dropping tools.
|
|
132
|
-
if (onInvalid === 'throw') {
|
|
136
|
+
if (onInvalid === 'throw' && !opts.emptyOk) {
|
|
133
137
|
throw new Error(`E_NO_ACCOUNTS_CONFIGURED: ${noAccountsMessage()}`);
|
|
134
138
|
}
|
|
135
139
|
return {
|
|
@@ -148,7 +152,7 @@ export function resolveAccounts(env = process.env, filePath = configFilePath(),
|
|
|
148
152
|
for (const [name, profile] of Object.entries(config?.scopeProfiles ?? {})) {
|
|
149
153
|
const bundles = resolveBundleAliases(profile.bundles);
|
|
150
154
|
for (const bundle of bundles) {
|
|
151
|
-
if (!(bundle
|
|
155
|
+
if (!isKnownBundle(bundle)) {
|
|
152
156
|
const hint = closestBundle(bundle);
|
|
153
157
|
fail('E_UNKNOWN_BUNDLE', `unknown bundle "${bundle}" in scope profile "${name}"${hint ? ` — did you mean "${hint}"?` : ''}`);
|
|
154
158
|
}
|
|
@@ -167,7 +171,7 @@ export function resolveAccounts(env = process.env, filePath = configFilePath(),
|
|
|
167
171
|
aliases.push(alias);
|
|
168
172
|
configs[alias] = {
|
|
169
173
|
email: entry.email,
|
|
170
|
-
...accountPaths(alias),
|
|
174
|
+
...accountPaths(alias, dir),
|
|
171
175
|
scopeProfile: entry.scopeProfile,
|
|
172
176
|
admin: adminEnv.length > 0 ? adminEnv.includes(alias) : entry.admin,
|
|
173
177
|
source: 'config',
|
|
@@ -196,7 +200,7 @@ function resolveDefaultAccount(env, fromConfig, aliases, fail) {
|
|
|
196
200
|
}
|
|
197
201
|
return { defaultAccount: value, defaultAccountSource: fromEnv ? 'env' : 'config' };
|
|
198
202
|
}
|
|
199
|
-
function fileStamp(filePath, version) {
|
|
203
|
+
export function fileStamp(filePath, version) {
|
|
200
204
|
try {
|
|
201
205
|
return `${version}:${fs.statSync(filePath).mtimeMs}`;
|
|
202
206
|
}
|
|
@@ -283,6 +287,8 @@ export const ACCOUNTS = current.aliases;
|
|
|
283
287
|
* enum actually accepts, and naming a runtime-added alias as valid while
|
|
284
288
|
* rejecting it would be a lie. */
|
|
285
289
|
export function unknownAliasMessage(aliases, selectors = false) {
|
|
290
|
+
if (aliases.length === 0)
|
|
291
|
+
return 'No Google account is configured yet. Add one with account_add.';
|
|
286
292
|
const all = selectors ? '; "*" for all accounts' : '';
|
|
287
293
|
const csv = selectors && aliases.length > 1 ? `; or a CSV subset like "${aliases.slice(0, 2).join(',')}"` : '';
|
|
288
294
|
return `Unknown account alias. Valid: ${aliases.join(', ')}${all}${csv}. Run account_list if accounts changed since this server started.`;
|
|
@@ -301,6 +307,37 @@ export function accountAliasSchemaFor(aliases) {
|
|
|
301
307
|
}
|
|
302
308
|
/** Shared, load-time snapshot used by every tool's `account` field. */
|
|
303
309
|
export const accountAliasSchema = accountAliasSchemaFor(ACCOUNTS);
|
|
310
|
+
/** Live account argument: validates at PARSE time against the accessor's
|
|
311
|
+
* CURRENT aliases, so an alias added mid-session (account_add, or a tenant
|
|
312
|
+
* alias link) is immediately valid on already-registered tools — a baked
|
|
313
|
+
* z.enum freezes the boot-time set until restart. An empty set stays
|
|
314
|
+
* permissive (same empty-safe rule as above); dispatch still resolves the
|
|
315
|
+
* alias against the live registry. */
|
|
316
|
+
export function accountArgLive(aliases) {
|
|
317
|
+
return z.string().check(liveAccountCheck(aliases, false));
|
|
318
|
+
}
|
|
319
|
+
// The check instance survives the clones .describe() and .optional() make,
|
|
320
|
+
// so it is what marks a field as an account selector for the registry.
|
|
321
|
+
const LIVE_ACCOUNT_CHECKS = new WeakSet();
|
|
322
|
+
/** The live membership check behind accountArgLive and the fan-out field;
|
|
323
|
+
* `selectors` also admits "*". An empty set stays permissive. */
|
|
324
|
+
export function liveAccountCheck(aliases, selectors) {
|
|
325
|
+
const check = z.superRefine((value, ctx) => {
|
|
326
|
+
const current = aliases();
|
|
327
|
+
if (current.length === 0 || current.includes(value) || (selectors && value === '*'))
|
|
328
|
+
return;
|
|
329
|
+
ctx.addIssue({ code: 'custom', message: unknownAliasMessage(current, selectors) });
|
|
330
|
+
});
|
|
331
|
+
LIVE_ACCOUNT_CHECKS.add(check);
|
|
332
|
+
return check;
|
|
333
|
+
}
|
|
334
|
+
/** True for a field built on liveAccountCheck, optional or not. */
|
|
335
|
+
export function isLiveAccountField(field) {
|
|
336
|
+
let def = field?._zod?.def;
|
|
337
|
+
if (def?.type === 'optional')
|
|
338
|
+
def = def.innerType?._zod?.def;
|
|
339
|
+
return def?.checks?.some((c) => LIVE_ACCOUNT_CHECKS.has(c)) ?? false;
|
|
340
|
+
}
|
|
304
341
|
/**
|
|
305
342
|
* BR-4: the stdio/http SERVER never boots with an empty registry — a fresh user
|
|
306
343
|
* bootstraps via env / `migrate-config` / `account import` / `auth` first. The
|
package/dist/api-probe.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getClient } from './client.js';
|
|
2
|
+
import { readToken } from './token-store.js';
|
|
1
3
|
import type { ApiProbeResult } from './doctor.js';
|
|
2
4
|
export interface ApiProbeSpec {
|
|
3
5
|
service: string;
|
|
@@ -15,4 +17,7 @@ export interface ApiProbeDeps {
|
|
|
15
17
|
grantedScopes: (alias: string) => string[];
|
|
16
18
|
request: (alias: string, url: string) => Promise<void>;
|
|
17
19
|
}
|
|
20
|
+
/** Probe deps over one context's token reads and client; the module-level
|
|
21
|
+
* pair is the single owner's. */
|
|
22
|
+
export declare function apiProbeDepsFor(readTokenFn: typeof readToken, getClientFn: typeof getClient): ApiProbeDeps;
|
|
18
23
|
export declare function probeApiEnablement(alias: string, deps?: ApiProbeDeps): Promise<ApiProbeResult[]>;
|
package/dist/api-probe.js
CHANGED
|
@@ -24,21 +24,26 @@ export const API_PROBES = [
|
|
|
24
24
|
export function planProbes(granted, probes = API_PROBES) {
|
|
25
25
|
return probes.filter((p) => granted.some((s) => p.scopePrefixes.some((prefix) => s.startsWith(prefix))));
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
/** Probe deps over one context's token reads and client; the module-level
|
|
28
|
+
* pair is the single owner's. */
|
|
29
|
+
export function apiProbeDepsFor(readTokenFn, getClientFn) {
|
|
30
|
+
return {
|
|
31
|
+
grantedScopes: (alias) => {
|
|
32
|
+
try {
|
|
33
|
+
const scope = readTokenFn(alias)?.scope;
|
|
34
|
+
return typeof scope === 'string' ? scope.split(' ').filter(Boolean) : [];
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
request: async (alias, url) => {
|
|
41
|
+
const auth = await getClientFn(alias);
|
|
42
|
+
await auth.request({ url, timeout: 10_000 });
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const DEFAULT_DEPS = apiProbeDepsFor(readToken, getClient);
|
|
42
47
|
export async function probeApiEnablement(alias, deps = DEFAULT_DEPS) {
|
|
43
48
|
const results = [];
|
|
44
49
|
for (const spec of planProbes(deps.grantedScopes(alias))) {
|
package/dist/arg-normalize.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { screenArguments, unknownArgEnvelope, } from './arg-strict.js';
|
|
2
|
-
import { sliceClean } from './trim.js';
|
|
1
|
+
import { echoName, echoNames, screenArguments, unknownArgEnvelope, } from './arg-strict.js';
|
|
2
|
+
import { logSafe, sliceClean } from './trim.js';
|
|
3
3
|
// The SDK synthesizes input-validation failures as isError RESULTS, skipping
|
|
4
4
|
// the handler entirely: no handler of ours runs, so no envelope exists and the
|
|
5
5
|
// client gets free text. The text is prose in both SDK eras (v1 prefixed it
|
|
@@ -115,8 +115,9 @@ export function screenMessage(msg, opts, log = (l) => process.stderr.write(`${l}
|
|
|
115
115
|
if (screened.unknown.length === 0 && screened.redundant.length === 0)
|
|
116
116
|
return { action: 'forward', msg };
|
|
117
117
|
const all = [...screened.unknown.map((u) => u.sent), ...screened.redundant];
|
|
118
|
-
// Key names only; argument VALUES never reach the log.
|
|
119
|
-
|
|
118
|
+
// Key names only; argument VALUES never reach the log. The names are the
|
|
119
|
+
// caller's, so they are escaped here and only here: the envelope is JSON.
|
|
120
|
+
log(`[args] ${tool}: undeclared ${logSafe(echoNames(all), 600)}${opts.mode === 'warn' ? ' (dropped)' : ' (rejected)'}`);
|
|
120
121
|
try {
|
|
121
122
|
// Only ever a DECLARED key or the literal placeholder, so the metrics
|
|
122
123
|
// closed-vocabulary rule holds: the caller's key is never persisted.
|
|
@@ -132,7 +133,7 @@ export function screenMessage(msg, opts, log = (l) => process.stderr.write(`${l}
|
|
|
132
133
|
const siblings = screened.unknown.some((u) => u.suggestions.length > 0)
|
|
133
134
|
? []
|
|
134
135
|
: (opts.siblingsFor?.(tool, screened.unknown.map((u) => u.sent)) ?? []);
|
|
135
|
-
const envelope = unknownArgEnvelope(tool, screened.unknown, declared, typeof account === 'string' ? account : undefined, siblings);
|
|
136
|
+
const envelope = unknownArgEnvelope(tool, screened.unknown, declared, typeof account === 'string' ? echoName(account) : undefined, siblings);
|
|
136
137
|
return {
|
|
137
138
|
action: 'reject',
|
|
138
139
|
response: {
|
package/dist/arg-strict.d.ts
CHANGED
|
@@ -14,6 +14,13 @@ export declare const STRICT_EXEMPT_TOOLS: ReadonlySet<string>;
|
|
|
14
14
|
* tools: no declared key starts with `_` or contains `/`, so neither rule can
|
|
15
15
|
* shadow a real parameter. */
|
|
16
16
|
export declare function isExemptKey(key: string): boolean;
|
|
17
|
+
/** A call can carry thousands of unknown keys, or one of megabytes: only the
|
|
18
|
+
* first few earn a did-you-mean, and the log line and the envelope echo a
|
|
19
|
+
* bounded prefix of each name. The reject decision itself is never capped. */
|
|
20
|
+
export declare const MAX_SUGGESTED_KEYS = 8;
|
|
21
|
+
export declare function echoName(name: string): string;
|
|
22
|
+
/** The first MAX_SUGGESTED_KEYS names, each capped, then "and N more". */
|
|
23
|
+
export declare function echoNames(names: readonly string[], quote?: boolean): string;
|
|
17
24
|
/**
|
|
18
25
|
* Rank declared keys for an unknown key. Tiers, best non-empty tier wins:
|
|
19
26
|
* 0 same key modulo case and separators
|
package/dist/arg-strict.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// and never touches a VALUE: it only decides that a key is undeclared, and
|
|
10
10
|
// suggests what the caller probably meant. Rewriting would risk sending a real
|
|
11
11
|
// value to Google on a guess, which is strictly worse than refusing.
|
|
12
|
-
import { editDistance } from './scope-catalog.js';
|
|
12
|
+
import { editDistance, SUGGEST_MAX_INPUT } from './scope-catalog.js';
|
|
13
|
+
import { sliceClean } from './trim.js';
|
|
13
14
|
/**
|
|
14
15
|
* `GOOGLE_ARG_UNKNOWN`: reject | warn | off. A typo in the VALUE still falls
|
|
15
16
|
* back to `warn`, not to the default: an operator who misspells the setting
|
|
@@ -59,6 +60,20 @@ export function isExemptKey(key) {
|
|
|
59
60
|
* alone; they match far too much to be useful. */
|
|
60
61
|
const GENERIC = new Set(['id', 'name', 'title', 'body', 'parent', 'text', 'content', 'type', 'value', 'key', 'data']);
|
|
61
62
|
const flatten = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
63
|
+
/** A call can carry thousands of unknown keys, or one of megabytes: only the
|
|
64
|
+
* first few earn a did-you-mean, and the log line and the envelope echo a
|
|
65
|
+
* bounded prefix of each name. The reject decision itself is never capped. */
|
|
66
|
+
export const MAX_SUGGESTED_KEYS = 8;
|
|
67
|
+
const MAX_ECHO_CHARS = 64;
|
|
68
|
+
export function echoName(name) {
|
|
69
|
+
return name.length > MAX_ECHO_CHARS ? `${sliceClean(name, MAX_ECHO_CHARS)}...` : name;
|
|
70
|
+
}
|
|
71
|
+
/** The first MAX_SUGGESTED_KEYS names, each capped, then "and N more". */
|
|
72
|
+
export function echoNames(names, quote = false) {
|
|
73
|
+
const shown = names.slice(0, MAX_SUGGESTED_KEYS).map((n) => (quote ? `"${echoName(n)}"` : echoName(n)));
|
|
74
|
+
const rest = names.length - shown.length;
|
|
75
|
+
return rest > 0 ? `${shown.join(', ')} and ${rest} more` : shown.join(', ');
|
|
76
|
+
}
|
|
62
77
|
function tokens(s) {
|
|
63
78
|
return s
|
|
64
79
|
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
@@ -76,6 +91,8 @@ function tokens(s) {
|
|
|
76
91
|
* distance 6, well past any sane threshold, which is why tier 1 exists.
|
|
77
92
|
*/
|
|
78
93
|
export function suggestKeys(unknown, declared, limit = 2) {
|
|
94
|
+
if (unknown.length > SUGGEST_MAX_INPUT)
|
|
95
|
+
return [];
|
|
79
96
|
const uFlat = flatten(unknown);
|
|
80
97
|
const uTok = tokens(unknown);
|
|
81
98
|
const tiers = [[], [], [], []];
|
|
@@ -94,8 +111,12 @@ export function suggestKeys(unknown, declared, limit = 2) {
|
|
|
94
111
|
tiers[2].push({ key, rank: Math.abs(kFlat.length - uFlat.length) });
|
|
95
112
|
continue;
|
|
96
113
|
}
|
|
114
|
+
const maxDist = Math.max(1, Math.floor(uFlat.length / 4));
|
|
115
|
+
// The distance is at least the length gap: skip the DP when that alone fails.
|
|
116
|
+
if (Math.abs(uFlat.length - kFlat.length) > maxDist)
|
|
117
|
+
continue;
|
|
97
118
|
const d = editDistance(uFlat, kFlat);
|
|
98
|
-
if (d <=
|
|
119
|
+
if (d <= maxDist)
|
|
99
120
|
tiers[3].push({ key, rank: d });
|
|
100
121
|
}
|
|
101
122
|
const tier = tiers.find((t) => t.length > 0);
|
|
@@ -117,10 +138,19 @@ export function screenArguments(tool, args, declared) {
|
|
|
117
138
|
if (STRICT_EXEMPT_TOOLS.has(tool) || declared.length === 0)
|
|
118
139
|
return out;
|
|
119
140
|
const declaredSet = new Set(declared);
|
|
141
|
+
// Tier 0 (same key modulo case and separators) is a map lookup, so every key
|
|
142
|
+
// gets it and a redundant twin is recognised past the ranking budget too.
|
|
143
|
+
// A near-miss duplicate (a typo sent next to its declared key) is recognised
|
|
144
|
+
// only within the budget: a call needs 9 of them before one is refused.
|
|
145
|
+
const byFlat = new Map();
|
|
146
|
+
for (const k of declared)
|
|
147
|
+
byFlat.set(flatten(k), [...(byFlat.get(flatten(k)) ?? []), k]);
|
|
148
|
+
let ranked = 0;
|
|
120
149
|
for (const key of Object.keys(args)) {
|
|
121
150
|
if (declaredSet.has(key) || isExemptKey(key))
|
|
122
151
|
continue;
|
|
123
|
-
const
|
|
152
|
+
const twins = key.length <= SUGGEST_MAX_INPUT ? byFlat.get(flatten(key)) : undefined;
|
|
153
|
+
const suggestions = twins ? twins.slice(0, 2) : ranked++ < MAX_SUGGESTED_KEYS ? suggestKeys(key, declared) : [];
|
|
124
154
|
// The caller sent both spellings, so the declared one already won and the
|
|
125
155
|
// outcome is what it has always been. Never fail a call that works today.
|
|
126
156
|
if (suggestions.some((s) => s in args)) {
|
|
@@ -133,7 +163,7 @@ export function screenArguments(tool, args, declared) {
|
|
|
133
163
|
}
|
|
134
164
|
/** The agent-facing envelope. Values are never included, only key names. */
|
|
135
165
|
export function unknownArgEnvelope(tool, unknown, declared, account, siblings = []) {
|
|
136
|
-
const names = unknown.map((u) =>
|
|
166
|
+
const names = echoNames(unknown.map((u) => u.sent), true);
|
|
137
167
|
const accepts = `This tool accepts: ${declared.join(', ')}.`;
|
|
138
168
|
const suggested = unknown.flatMap((u) => u.suggestions);
|
|
139
169
|
let hint;
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
import type { AccountSet } from './accounts.js';
|
|
1
2
|
import { ADMIN_SCOPES } from './scope-catalog.js';
|
|
2
3
|
export declare const BASE_SCOPES: string[];
|
|
3
4
|
export declare const OPTIONAL_SCOPE_BUNDLES: Record<string, string[]>;
|
|
4
5
|
export { ADMIN_SCOPES };
|
|
5
6
|
/** Union of every account's resolved bundles: a service registers if ANY
|
|
6
7
|
* account can authorize it; per-account authz happens at call time (BR2). */
|
|
7
|
-
export declare function getOptionalBundles(): string[];
|
|
8
|
+
export declare function getOptionalBundles(set?: AccountSet): string[];
|
|
8
9
|
/** Aliases granted ADMIN_SCOPES: per-account admin flag (env
|
|
9
10
|
* GOOGLE_ADMIN_ACCOUNTS overrides config.json at resolve) OR the account's
|
|
10
11
|
* scope profile carrying admin (boolean or "admin" bundle) — equivalent forms. */
|
|
11
|
-
export declare function getAdminAccounts(): string[];
|
|
12
|
+
export declare function getAdminAccounts(set?: AccountSet): string[];
|
|
12
13
|
/** Scopes are fixed at consent time: changing an account's profile (or the
|
|
13
14
|
* legacy env) changes its consent set and requires re-running auth. Evaluated
|
|
14
15
|
* per account: `work` can carry admin + gmail_settings while `personal` is
|
|
15
16
|
* never asked for them. */
|
|
16
|
-
export declare function resolveScopesForAccount(alias: string): string[];
|
|
17
|
+
export declare function resolveScopesForAccount(alias: string, set?: AccountSet): string[];
|
|
17
18
|
export declare function runAuthFlow(args: string[]): Promise<void>;
|
package/dist/auth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { openUrl } from './open-url.js';
|
|
3
3
|
import { ACCOUNTS, getAccountSet } from './accounts.js';
|
|
4
|
-
import { ADMIN_SCOPES, BUNDLE_CATALOG, closestBundle, resolveBundleAliases } from './scope-catalog.js';
|
|
4
|
+
import { ADMIN_SCOPES, BUNDLE_CATALOG, closestBundle, isKnownBundle, resolveBundleAliases } from './scope-catalog.js';
|
|
5
5
|
import { resolveMasterKey } from './master-key.js';
|
|
6
6
|
import { writeToken } from './token-store.js';
|
|
7
7
|
import { buildConsentClient, openLoopbackConsent, TESTING_MODE_WARNING } from './oauth-consent.js';
|
|
@@ -48,18 +48,17 @@ function legacyGlobalProfile() {
|
|
|
48
48
|
if (bundle === 'admin') {
|
|
49
49
|
throw new Error('E_UNKNOWN_BUNDLE: "admin" is not a global bundle: grant it per account via GOOGLE_ADMIN_ACCOUNTS or an "admin: true" scope profile.');
|
|
50
50
|
}
|
|
51
|
-
if (!(bundle
|
|
51
|
+
if (!isKnownBundle(bundle)) {
|
|
52
52
|
const hint = closestBundle(bundle);
|
|
53
53
|
throw new Error(`E_UNKNOWN_BUNDLE: unknown bundle "${bundle}" in GOOGLE_OPTIONAL_SCOPES${hint ? ` — did you mean "${hint}"?` : ''}`);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
return { bundles };
|
|
57
57
|
}
|
|
58
|
-
function profileForAccount(alias) {
|
|
58
|
+
function profileForAccount(alias, set = getAccountSet()) {
|
|
59
59
|
const legacy = legacyGlobalProfile();
|
|
60
60
|
if (legacy)
|
|
61
61
|
return legacy;
|
|
62
|
-
const set = getAccountSet();
|
|
63
62
|
const name = set.configs[alias]?.scopeProfile ?? 'base';
|
|
64
63
|
// hasOwn: a profile named like an Object.prototype member must never
|
|
65
64
|
// resolve to the inherited function.
|
|
@@ -67,14 +66,13 @@ function profileForAccount(alias) {
|
|
|
67
66
|
}
|
|
68
67
|
/** Union of every account's resolved bundles: a service registers if ANY
|
|
69
68
|
* account can authorize it; per-account authz happens at call time (BR2). */
|
|
70
|
-
export function getOptionalBundles() {
|
|
69
|
+
export function getOptionalBundles(set = getAccountSet()) {
|
|
71
70
|
const legacy = legacyGlobalProfile();
|
|
72
71
|
if (legacy)
|
|
73
72
|
return legacy.bundles.filter(b => b !== 'admin');
|
|
74
73
|
const union = new Set();
|
|
75
|
-
const set = getAccountSet();
|
|
76
74
|
for (const alias of set.aliases) {
|
|
77
|
-
for (const b of profileForAccount(alias).bundles) {
|
|
75
|
+
for (const b of profileForAccount(alias, set).bundles) {
|
|
78
76
|
if (b !== 'admin')
|
|
79
77
|
union.add(b);
|
|
80
78
|
}
|
|
@@ -84,12 +82,12 @@ export function getOptionalBundles() {
|
|
|
84
82
|
/** Aliases granted ADMIN_SCOPES: per-account admin flag (env
|
|
85
83
|
* GOOGLE_ADMIN_ACCOUNTS overrides config.json at resolve) OR the account's
|
|
86
84
|
* scope profile carrying admin (boolean or "admin" bundle) — equivalent forms. */
|
|
87
|
-
export function getAdminAccounts() {
|
|
88
|
-
const { aliases, configs } =
|
|
85
|
+
export function getAdminAccounts(set = getAccountSet()) {
|
|
86
|
+
const { aliases, configs } = set;
|
|
89
87
|
return aliases.filter((a) => {
|
|
90
88
|
if (configs[a].admin === true)
|
|
91
89
|
return true;
|
|
92
|
-
const p = profileForAccount(a);
|
|
90
|
+
const p = profileForAccount(a, set);
|
|
93
91
|
return p.admin === true || p.bundles.includes('admin');
|
|
94
92
|
});
|
|
95
93
|
}
|
|
@@ -97,15 +95,15 @@ export function getAdminAccounts() {
|
|
|
97
95
|
* legacy env) changes its consent set and requires re-running auth. Evaluated
|
|
98
96
|
* per account: `work` can carry admin + gmail_settings while `personal` is
|
|
99
97
|
* never asked for them. */
|
|
100
|
-
export function resolveScopesForAccount(alias) {
|
|
101
|
-
const profile = profileForAccount(alias);
|
|
98
|
+
export function resolveScopesForAccount(alias, set = getAccountSet()) {
|
|
99
|
+
const profile = profileForAccount(alias, set);
|
|
102
100
|
const scopes = profile.includesBase === false ? [] : [...BASE_SCOPES];
|
|
103
101
|
for (const bundle of profile.bundles) {
|
|
104
102
|
if (bundle === 'admin')
|
|
105
103
|
continue;
|
|
106
104
|
scopes.push(...BUNDLE_CATALOG[bundle].scopes);
|
|
107
105
|
}
|
|
108
|
-
if (getAdminAccounts().includes(alias)) {
|
|
106
|
+
if (getAdminAccounts(set).includes(alias)) {
|
|
109
107
|
scopes.push(...ADMIN_SCOPES);
|
|
110
108
|
}
|
|
111
109
|
return Array.from(new Set(scopes));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface MtBootGates {
|
|
2
|
+
/** True = multi-tenant deployment: the server boots with zero accounts
|
|
3
|
+
* (tenants provision in-band) and the provisioning gate below replaces the
|
|
4
|
+
* flat owner-allowlist requirement. */
|
|
5
|
+
multiTenant: boolean;
|
|
6
|
+
/** Fail-fast replacement for the E_OWNER_EMAILS_REQUIRED check. MUST refuse
|
|
7
|
+
* (exit/throw) when no valid provisioning mechanism is configured — an
|
|
8
|
+
* ungated HTTP endpoint must never boot, whatever the condition checked. */
|
|
9
|
+
assertProvisioningGate: () => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function setMtBootGates(g: MtBootGates | null): void;
|
|
12
|
+
export declare function mtBootGates(): MtBootGates | null;
|
|
13
|
+
export declare function isMultiTenantBoot(): boolean;
|
|
14
|
+
/** BR-4 applies to stdio always, and to HTTP only when single-owner: a
|
|
15
|
+
* multi-tenant HTTP deploy starts empty by design. */
|
|
16
|
+
export declare function accountsAssertRequired(wantStdio: boolean, multiTenant: boolean): boolean;
|
|
17
|
+
/** A set GOOGLE_ACCOUNTS on a multi-tenant boot is refused at DEPLOY time:
|
|
18
|
+
* the env registry is process-wide, so it would graft one operator's accounts
|
|
19
|
+
* onto every tenant and turn every tenant's first wizard call into a dead
|
|
20
|
+
* end (the per-call E_ENV_ACCOUNTS_MODE guard), N confusing failures instead
|
|
21
|
+
* of one clear one. */
|
|
22
|
+
export declare function assertNoEnvAccountsMode(env?: NodeJS.ProcessEnv): void;
|
|
23
|
+
/** Same class, second vector: GOOGLE_OPTIONAL_SCOPES is a process-wide legacy
|
|
24
|
+
* global scope profile, so on a multi-tenant box one operator's scope choice
|
|
25
|
+
* would silently apply to every tenant's consent. */
|
|
26
|
+
export declare function assertNoEnvOptionalScopesMode(env?: NodeJS.ProcessEnv): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// S1.16: multi-tenant boot gates. The free core never sets these — every
|
|
2
|
+
// helper reduces to today's single-owner behavior when no gates are
|
|
3
|
+
// installed. Deliberately a side-effect-free module (accounts.ts/auth.ts
|
|
4
|
+
// resolve env at import time) so the tenancy module can import it cheaply.
|
|
5
|
+
import { failStartup } from './config-file.js';
|
|
6
|
+
let gates = null;
|
|
7
|
+
export function setMtBootGates(g) {
|
|
8
|
+
gates = g;
|
|
9
|
+
}
|
|
10
|
+
export function mtBootGates() {
|
|
11
|
+
return gates;
|
|
12
|
+
}
|
|
13
|
+
export function isMultiTenantBoot() {
|
|
14
|
+
return gates?.multiTenant === true;
|
|
15
|
+
}
|
|
16
|
+
/** BR-4 applies to stdio always, and to HTTP only when single-owner: a
|
|
17
|
+
* multi-tenant HTTP deploy starts empty by design. */
|
|
18
|
+
export function accountsAssertRequired(wantStdio, multiTenant) {
|
|
19
|
+
return wantStdio || !multiTenant;
|
|
20
|
+
}
|
|
21
|
+
/** A set GOOGLE_ACCOUNTS on a multi-tenant boot is refused at DEPLOY time:
|
|
22
|
+
* the env registry is process-wide, so it would graft one operator's accounts
|
|
23
|
+
* onto every tenant and turn every tenant's first wizard call into a dead
|
|
24
|
+
* end (the per-call E_ENV_ACCOUNTS_MODE guard), N confusing failures instead
|
|
25
|
+
* of one clear one. */
|
|
26
|
+
export function assertNoEnvAccountsMode(env = process.env) {
|
|
27
|
+
if (env.GOOGLE_ACCOUNTS?.trim()) {
|
|
28
|
+
failStartup('E_ENV_ACCOUNTS_MODE_MT', 'GOOGLE_ACCOUNTS must not be set on a multi-tenant deployment; accounts are linked in-band per tenant. Unset it and restart.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Same class, second vector: GOOGLE_OPTIONAL_SCOPES is a process-wide legacy
|
|
32
|
+
* global scope profile, so on a multi-tenant box one operator's scope choice
|
|
33
|
+
* would silently apply to every tenant's consent. */
|
|
34
|
+
export function assertNoEnvOptionalScopesMode(env = process.env) {
|
|
35
|
+
if (env.GOOGLE_OPTIONAL_SCOPES?.trim()) {
|
|
36
|
+
failStartup('E_ENV_OPTIONAL_SCOPES_MT', 'GOOGLE_OPTIONAL_SCOPES must not be set on a multi-tenant deployment; scopes are chosen per tenant at consent. Unset it and restart.');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const CIMD_TTL_MS: number;
|
|
2
|
+
/** The oldest validated document still served while the cap is full or the issuer cannot answer. */
|
|
3
|
+
export declare const CIMD_MAX_STALE_MS: number;
|
|
4
|
+
/** Fetches at once. A lookup can outlive the fetch that gave up on it, so ssrf-guard caps its own lookups apart. */
|
|
5
|
+
export declare const CIMD_MAX_IN_FLIGHT = 4;
|
|
6
|
+
export declare const CIMD_CACHE_MAX = 256;
|
|
7
|
+
export type CimdDocument = Record<string, unknown>;
|
|
8
|
+
type CimdError = {
|
|
9
|
+
error: 'E_CIMD_SSRF_BLOCKED' | 'E_CIMD_INVALID';
|
|
10
|
+
};
|
|
11
|
+
/** `fresh` is true only for the request whose fetch produced the document. */
|
|
12
|
+
export type CimdResolution = {
|
|
13
|
+
doc: CimdDocument;
|
|
14
|
+
fresh: boolean;
|
|
15
|
+
} | {
|
|
16
|
+
busy: true;
|
|
17
|
+
} | CimdError;
|
|
18
|
+
export interface CimdCacheDeps {
|
|
19
|
+
fetch: (clientId: string) => Promise<CimdDocument>;
|
|
20
|
+
now: () => number;
|
|
21
|
+
log: (line: string) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare class CimdClientCache {
|
|
24
|
+
private readonly deps;
|
|
25
|
+
private readonly entries;
|
|
26
|
+
private readonly inFlight;
|
|
27
|
+
private busyLoggedAt;
|
|
28
|
+
private busySuppressed;
|
|
29
|
+
constructor(deps: CimdCacheDeps);
|
|
30
|
+
get size(): number;
|
|
31
|
+
/** `validate` runs once per fetch, in the request that started it, and every
|
|
32
|
+
* request waiting on that fetch gets the same answer. */
|
|
33
|
+
resolve(clientId: string, validate: (doc: CimdDocument) => boolean): Promise<CimdResolution>;
|
|
34
|
+
private fetchFor;
|
|
35
|
+
private store;
|
|
36
|
+
private noteBusy;
|
|
37
|
+
}
|
|
38
|
+
export {};
|