mcp-google-multi 6.0.0-beta.3 → 6.0.0-beta.5
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/README.md +20 -6
- package/dist/accounts.d.ts +3 -3
- package/dist/accounts.js +18 -31
- package/dist/arg-normalize.js +15 -35
- package/dist/arg-strict.js +8 -28
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +5 -10
- package/dist/boot-gates.d.ts +1 -1
- package/dist/boot-gates.js +3 -5
- package/dist/cimd-cache.js +5 -11
- package/dist/client-config.d.ts +2 -2
- package/dist/client-config.js +8 -14
- package/dist/client.js +0 -0
- package/dist/compose.d.ts +8 -1
- package/dist/compose.js +11 -11
- package/dist/config-file.js +7 -12
- package/dist/discover.js +6 -12
- package/dist/discovery-client.js +14 -33
- package/dist/doctor.d.ts +3 -3
- package/dist/doctor.js +12 -18
- package/dist/env-load.js +2 -5
- package/dist/executor.js +2 -4
- package/dist/fanout.js +8 -17
- package/dist/fs-atomic.d.ts +0 -3
- package/dist/fs-atomic.js +104 -22
- package/dist/hint-provider.d.ts +4 -1
- package/dist/hint-provider.js +4 -10
- package/dist/http-config.d.ts +2 -2
- package/dist/http-config.js +3 -7
- package/dist/http-transport.d.ts +5 -5
- package/dist/http-transport.js +39 -92
- package/dist/identity.d.ts +2 -2
- package/dist/identity.js +6 -9
- package/dist/index.js +19 -31
- package/dist/master-key.d.ts +3 -3
- package/dist/master-key.js +11 -22
- package/dist/mcp-token.d.ts +40 -16
- package/dist/mcp-token.js +121 -44
- package/dist/metrics-cli.d.ts +1 -1
- package/dist/metrics-cli.js +1 -1
- package/dist/metrics-tap.js +2 -5
- package/dist/migrate-config.d.ts +1 -2
- package/dist/migrate-config.js +3 -5
- package/dist/net-tuning.js +2 -4
- package/dist/oauth-as.d.ts +7 -2
- package/dist/oauth-as.js +45 -78
- package/dist/oauth-consent.js +4 -10
- package/dist/outbound-allowlist.js +4 -9
- package/dist/reauth-hint.js +2 -5
- package/dist/registry-transfer.d.ts +1 -1
- package/dist/registry-transfer.js +5 -13
- package/dist/registry.d.ts +3 -9
- package/dist/registry.js +97 -76
- package/dist/scope-catalog.d.ts +5 -0
- package/dist/scope-catalog.js +17 -7
- package/dist/scope-observability.d.ts +2 -3
- package/dist/scope-observability.js +8 -14
- package/dist/services.d.ts +3 -0
- package/dist/services.js +5 -10
- package/dist/ssrf-guard.js +11 -22
- package/dist/tenant-purge.js +2 -4
- package/dist/token-store.js +0 -0
- package/dist/tools/_coerce.d.ts +4 -4
- package/dist/tools/_errors.js +22 -39
- package/dist/tools/_local-files.js +4 -8
- package/dist/tools/account-wizard.d.ts +4 -3
- package/dist/tools/account-wizard.js +48 -54
- package/dist/tools/accounts-tool.d.ts +8 -1
- package/dist/tools/accounts-tool.js +18 -7
- package/dist/tools/admin.js +8 -4
- package/dist/tools/analytics.js +32 -16
- package/dist/tools/calendar.d.ts +5 -0
- package/dist/tools/calendar.js +63 -7
- package/dist/tools/chat.js +2 -3
- package/dist/tools/contacts.js +19 -14
- package/dist/tools/docs.js +45 -21
- package/dist/tools/drive.js +72 -39
- package/dist/tools/forms.js +2 -3
- package/dist/tools/generated/gmail.js +0 -14
- package/dist/tools/generated/method-map.js +1 -1
- package/dist/tools/gmail-mime.d.ts +1 -1
- package/dist/tools/gmail-mime.js +7 -12
- package/dist/tools/gmail.d.ts +3 -3
- package/dist/tools/gmail.js +135 -52
- package/dist/tools/google-api.js +5 -7
- package/dist/tools/meet.js +2 -3
- package/dist/tools/searchconsole.js +2 -3
- package/dist/tools/sheets.d.ts +10 -0
- package/dist/tools/sheets.js +111 -5
- package/dist/tools/slides.js +2 -3
- package/dist/tools/tasks.js +2 -3
- package/dist/trim.js +2 -3
- package/dist/usage-metrics.js +5 -12
- package/dist/write-control.d.ts +4 -8
- package/dist/write-control.js +18 -6
- package/package.json +24 -22
- package/skills/google-workspace/SKILL.md +162 -0
- package/skills/google-workspace/reference/admin.md +105 -0
- package/skills/google-workspace/reference/analytics.md +70 -0
- package/skills/google-workspace/reference/appsmarket.md +5 -0
- package/skills/google-workspace/reference/calendar.md +175 -0
- package/skills/google-workspace/reference/chat.md +59 -0
- package/skills/google-workspace/reference/classroom.md +5 -0
- package/skills/google-workspace/reference/cloudidentity.md +5 -0
- package/skills/google-workspace/reference/cloudsearch.md +5 -0
- package/skills/google-workspace/reference/contacts.md +138 -0
- package/skills/google-workspace/reference/docs.md +395 -0
- package/skills/google-workspace/reference/drive.md +485 -0
- package/skills/google-workspace/reference/driveactivity.md +5 -0
- package/skills/google-workspace/reference/drivelabels.md +5 -0
- package/skills/google-workspace/reference/forms.md +92 -0
- package/skills/google-workspace/reference/gmail.md +305 -0
- package/skills/google-workspace/reference/groupsmigration.md +5 -0
- package/skills/google-workspace/reference/groupssettings.md +5 -0
- package/skills/google-workspace/reference/keep.md +5 -0
- package/skills/google-workspace/reference/licensing.md +5 -0
- package/skills/google-workspace/reference/meet.md +68 -0
- package/skills/google-workspace/reference/postmaster.md +5 -0
- package/skills/google-workspace/reference/reseller.md +5 -0
- package/skills/google-workspace/reference/script.md +5 -0
- package/skills/google-workspace/reference/searchconsole.md +128 -0
- package/skills/google-workspace/reference/sheets.md +430 -0
- package/skills/google-workspace/reference/slides.md +67 -0
- package/skills/google-workspace/reference/tasks.md +167 -0
- package/skills/google-workspace/reference/vault.md +5 -0
- package/skills/google-workspace/reference/workspaceevents.md +5 -0
package/dist/master-key.js
CHANGED
|
@@ -5,10 +5,8 @@ import { createRequire } from 'node:module';
|
|
|
5
5
|
import { configDir, failStartup } from './config-file.js';
|
|
6
6
|
import { atomicWriteFileSync, withFileLock } from './fs-atomic.js';
|
|
7
7
|
import { getTokenDir } from './accounts.js';
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// failure (no prebuild, native load error, headless Linux without a Secret
|
|
11
|
-
// Service) as "keychain unavailable", falling through to the 0600 file.
|
|
8
|
+
// Native optionalDependency, loaded lazily and synchronously (the token
|
|
9
|
+
// store's key read is sync); EVERY failure falls through to the 0600 file.
|
|
12
10
|
const require_ = createRequire(import.meta.url);
|
|
13
11
|
function systemKeychain(account) {
|
|
14
12
|
try {
|
|
@@ -32,8 +30,7 @@ function systemKeychain(account) {
|
|
|
32
30
|
}
|
|
33
31
|
}
|
|
34
32
|
// Default keychain factory, replaceable ONLY for tests: unit tests must never
|
|
35
|
-
// read or write the developer's real OS keyring
|
|
36
|
-
// test fixture key into a real GNOME keyring — never again).
|
|
33
|
+
// read or write the developer's real OS keyring.
|
|
37
34
|
let keychainFactory = systemKeychain;
|
|
38
35
|
export function __setKeychainFactoryForTest(fn) {
|
|
39
36
|
keychainFactory = fn ?? systemKeychain;
|
|
@@ -46,14 +43,8 @@ function anyEncFileUnder(dir) {
|
|
|
46
43
|
return false;
|
|
47
44
|
}
|
|
48
45
|
}
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
// one client) must still stop a fresh key from being minted. TOKEN_STORE_PATH
|
|
52
|
-
// may relocate the token dir outside configDir(), so scan it directly; the
|
|
53
|
-
// recursive configDir() walk then covers BOTH the tenant-namespaced token root
|
|
54
|
-
// (tenants/<id>) AND any top-level master-key-encrypted registry beside it
|
|
55
|
-
// (e.g. a downstream tenant map / invite store). Minting a fresh MASTER_KEY
|
|
56
|
-
// over any existing .enc bricks it irreversibly, so the scan must see them all.
|
|
46
|
+
// Globs the token dir rather than iterating registered aliases, so an orphan
|
|
47
|
+
// .enc still stops a fresh key from being minted. See docs/internals.md.
|
|
57
48
|
function anyTokenFileExists() {
|
|
58
49
|
return anyEncFileUnder(getTokenDir()) || anyEncFileUnder(configDir());
|
|
59
50
|
}
|
|
@@ -98,7 +89,7 @@ function resolveKey(envName, fileName, hardGuard, deps, onFail = 'exit') {
|
|
|
98
89
|
const fromFile = readKeyFile(filePath);
|
|
99
90
|
if (fromFile)
|
|
100
91
|
return { key: fromFile, provenance: 'file' };
|
|
101
|
-
// Generate-on-setup. The hard guard
|
|
92
|
+
// Generate-on-setup. The hard guard: minting a fresh
|
|
102
93
|
// MASTER_KEY while <alias>.enc files exist would silently brick them all.
|
|
103
94
|
if (hardGuard && hasAnyToken()) {
|
|
104
95
|
fail('E_MASTER_KEY_MISSING_TOKENS_EXIST', `no ${envName} found in env, keychain or ${filePath}, but encrypted tokens exist under ${getTokenDir()}. ` +
|
|
@@ -115,10 +106,8 @@ function resolveKey(envName, fileName, hardGuard, deps, onFail = 'exit') {
|
|
|
115
106
|
return { key: racedFile, provenance: 'file' };
|
|
116
107
|
// Exactly randomBytes(32) base64: hits deriveKey's 32-byte fast path.
|
|
117
108
|
const generated = randomBytes(32).toString('base64');
|
|
118
|
-
// The 0600 file is ALWAYS written
|
|
119
|
-
//
|
|
120
|
-
// lives only there bricks every token at the next boot. The file is the
|
|
121
|
-
// durable sink; byte-exact read-back gates any use of the key.
|
|
109
|
+
// The 0600 file is ALWAYS written (some keychain backends don't survive
|
|
110
|
+
// a reboot); byte-exact read-back gates any use of the key.
|
|
122
111
|
atomicWriteFileSync(filePath, `${generated}\n`, 0o600);
|
|
123
112
|
if (readKeyFile(filePath) !== generated) {
|
|
124
113
|
fail('E_KEY_PROVISIONING_FAILED', `generated ${envName} could not be read back byte-exact from ${filePath}; refusing to encrypt anything with an unverified key.`);
|
|
@@ -151,7 +140,7 @@ function resolveKey(envName, fileName, hardGuard, deps, onFail = 'exit') {
|
|
|
151
140
|
}
|
|
152
141
|
let cachedMaster = null;
|
|
153
142
|
let cachedJwt = null;
|
|
154
|
-
/**
|
|
143
|
+
/** Single choke point: no other module may read the env vars directly.
|
|
155
144
|
* Boot/CLI form: failures exit with a clean stderr slug ("fatal at startup"). */
|
|
156
145
|
export function resolveMasterKey(deps) {
|
|
157
146
|
if (deps)
|
|
@@ -189,7 +178,7 @@ export function resolveJwtKey(deps) {
|
|
|
189
178
|
}
|
|
190
179
|
let mirrored = false;
|
|
191
180
|
/**
|
|
192
|
-
*
|
|
181
|
+
* After the FIRST successful token decrypt with an env-sourced key,
|
|
193
182
|
* mirror it into the keychain (only when the keychain holds no entry) so the
|
|
194
183
|
* key survives an env loss. Gating on a successful decrypt means a stale or
|
|
195
184
|
* wrong env key is never mirrored. Best-effort: failures are doctor material,
|
|
@@ -211,7 +200,7 @@ export function noteSuccessfulDecrypt() {
|
|
|
211
200
|
// Keychain backend unavailable; env stays the working source.
|
|
212
201
|
}
|
|
213
202
|
}
|
|
214
|
-
/**
|
|
203
|
+
/** Reset: drop the generated MASTER_KEY material (0600 file + keychain
|
|
215
204
|
* entry) so the next run re-provisions. Never removes an env-provided key (the
|
|
216
205
|
* deployer owns that; `env:true` flags it so reset can warn). The caller MUST
|
|
217
206
|
* have wiped every .enc token first — regenerating with tokens present would
|
package/dist/mcp-token.d.ts
CHANGED
|
@@ -88,7 +88,7 @@ export declare class ReplayGuard {
|
|
|
88
88
|
reserve(jti: string, ttlMs: number, nowMs: number): 'ok' | 'full';
|
|
89
89
|
/** Record a jti as spent. A full guard refuses instead of evicting a live
|
|
90
90
|
* jti: a flood of spends would otherwise make a spent artifact usable again,
|
|
91
|
-
* such as a callback the browser binding refused
|
|
91
|
+
* such as a callback the browser binding refused. `artifactExpMs`
|
|
92
92
|
* keeps the record alive as long as the artifact itself, even when the clock
|
|
93
93
|
* stepped back between its signing and this spend. */
|
|
94
94
|
spend(jti: string, ttlMs: number, nowMs: number, artifactExpMs?: number): ReplaySpend;
|
|
@@ -107,12 +107,31 @@ export interface RefreshRecord {
|
|
|
107
107
|
issuedAt: number;
|
|
108
108
|
family: string;
|
|
109
109
|
}
|
|
110
|
+
/** A gen-1-behind token presented within this long of the rotation that
|
|
111
|
+
* retired it, from the same client, can be a concurrent duplicate rather
|
|
112
|
+
* than theft. See docs/internals.md. */
|
|
113
|
+
export declare const REPLAY_WINDOW_MS = 30000;
|
|
114
|
+
/** The store's own MAC key for `masterKey`, memoized. Exported so a caller
|
|
115
|
+
* that never touches a RefreshStore (the AS computing a request's
|
|
116
|
+
* clientFingerprint) can key it the same way the store does. */
|
|
117
|
+
export declare function refreshMacKey(masterKey: string): Buffer;
|
|
110
118
|
/**
|
|
111
119
|
* The family tag of a refresh token this `masterKey`'s store minted, or null
|
|
112
120
|
* for anything else. Derives the key once; the returned function does no I/O,
|
|
113
121
|
* so a host can key a pre-auth rate limit on it.
|
|
114
122
|
*/
|
|
115
123
|
export declare function refreshFamilyTagger(masterKey: string): (token: string) => string | null;
|
|
124
|
+
/**
|
|
125
|
+
* A correlation tag for the client presenting a refresh token:
|
|
126
|
+
* HMAC-SHA256(key, client_id + "\n" + user-agent + "\n" + address), truncated
|
|
127
|
+
* to 16 bytes and base64url-encoded. `key` is the store's own MAC key
|
|
128
|
+
* (refreshMacKey), so computing it needs MASTER_KEY: a reader of the store
|
|
129
|
+
* file alone (`by` is stored, not these inputs) cannot brute-force the
|
|
130
|
+
* client_id/user-agent/address that produced it. It is still not a secret:
|
|
131
|
+
* anyone who can see a live request can send the same inputs and get the
|
|
132
|
+
* same tag. Pure, no I/O.
|
|
133
|
+
*/
|
|
134
|
+
export declare function clientFingerprint(clientId: string, userAgent: string, address: string, key: Buffer): string;
|
|
116
135
|
/** Boot check for a refresh store file: returns when it is absent or this
|
|
117
136
|
* release can read it; throws when it does not decrypt under `masterKey`
|
|
118
137
|
* (E_REFRESH_STORE_UNREADABLE) or a newer release wrote it
|
|
@@ -138,10 +157,10 @@ export interface RefreshStoreOptions {
|
|
|
138
157
|
* Persisted (encrypted under MASTER_KEY) refresh-token store. Rotates on every
|
|
139
158
|
* use, and a token carries its family and generation under a MAC, so
|
|
140
159
|
* presenting any generation the family has rotated past is REUSE: the family
|
|
141
|
-
* is revoked for the rest of its life
|
|
160
|
+
* is revoked for the rest of its life. The store keeps one
|
|
142
161
|
* record per family, whatever its rotation count. Every read-modify-write goes
|
|
143
162
|
* through a file lock so concurrent stdio+HTTP processes can't lost-update or
|
|
144
|
-
* double-spend
|
|
163
|
+
* double-spend. See docs/internals.md.
|
|
145
164
|
*/
|
|
146
165
|
export declare class RefreshStore {
|
|
147
166
|
private readonly path;
|
|
@@ -165,19 +184,15 @@ export declare class RefreshStore {
|
|
|
165
184
|
private load;
|
|
166
185
|
private save;
|
|
167
186
|
private emit;
|
|
168
|
-
/** Evict `sub`'s own least-recently-used sessions (both formats) so at
|
|
169
|
-
* most `limit` of its live sessions remain; dropping one here is the same
|
|
170
|
-
* as deleting it from `data.families` / `data.active` on a revoke, so it
|
|
171
|
-
* can never be presented again (reuse detection for every OTHER family is
|
|
172
|
-
* untouched). Only `sub`'s own entries are ever inspected.
|
|
173
|
-
*
|
|
174
|
-
* `keepKey` is the family just issued: it is excluded from the ranking so
|
|
175
|
-
* a clock step back, or a concurrent rotate's `usedAt = max(usedAt, nowMs)`
|
|
176
|
-
* racing ahead of it under the lock, can never make the brand-new family
|
|
177
|
-
* look like the least recently used one and drop itself. */
|
|
178
187
|
private enforceFamilyCap;
|
|
179
188
|
private newFamilyId;
|
|
180
|
-
issue(nowMs: number, sub: string): string;
|
|
189
|
+
issue(nowMs: number, sub: string, fingerprint?: string): string;
|
|
190
|
+
/** A presented token exactly one generation behind `f`, inside the replay
|
|
191
|
+
* window, from the fingerprint that retired it, whose derived successor
|
|
192
|
+
* matches `f.cur`: a concurrent duplicate request rather than theft (see
|
|
193
|
+
* docs/internals.md). Null for anything else at that generation, including
|
|
194
|
+
* a different rand (a genuinely different gen-1 token) or no fingerprint. */
|
|
195
|
+
private concurrentDuplicate;
|
|
181
196
|
/** Rotate a presented refresh token; null if it is unknown, malformed, or
|
|
182
197
|
* rotated away (reuse => the family is revoked). The record's sub is copied
|
|
183
198
|
* forward and returned so the caller can mint the matching access token
|
|
@@ -186,8 +201,17 @@ export declare class RefreshStore {
|
|
|
186
201
|
* `accept` (optional) is called inside the store lock, before any mutation,
|
|
187
202
|
* with the record's sub. false: every session of that sub is dropped and
|
|
188
203
|
* null returned. A throw: nothing is written and the error propagates, so
|
|
189
|
-
* the presented token stays valid. Absent: unchanged.
|
|
190
|
-
|
|
204
|
+
* the presented token stays valid. Absent: unchanged.
|
|
205
|
+
*
|
|
206
|
+
* `fingerprint` (optional) names the presenting client (clientFingerprint).
|
|
207
|
+
* Absent: a rotated-away token is always reuse, as before. Present: a
|
|
208
|
+
* token exactly one generation behind, from the same fingerprint, presented
|
|
209
|
+
* no earlier than and at most REPLAY_WINDOW_MS after the rotation that
|
|
210
|
+
* retired it, is a concurrent duplicate: it gets back the SAME successor
|
|
211
|
+
* token, with no change to the family (not even `usedAt`). Anything else
|
|
212
|
+
* rotated-away is still reuse, including a clock that has stepped back
|
|
213
|
+
* past `usedAt`, which cannot be told apart from a genuinely stale replay. */
|
|
214
|
+
rotate(oldToken: string, nowMs: number, accept?: (sub: string) => boolean, fingerprint?: string): {
|
|
191
215
|
token: string;
|
|
192
216
|
sub: string;
|
|
193
217
|
} | null;
|
package/dist/mcp-token.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// `state` and authorization code, the in-memory single-use replay guard, and
|
|
4
|
-
// the rotated refresh-token store. Keyed by the provisioned MCP_JWT_KEY
|
|
5
|
-
// (B5), so tokens survive a restart as long as the key persists.
|
|
1
|
+
// Token model: the MCP access token (HS256 JWT, jose-only), signed state and
|
|
2
|
+
// authorization code, the replay guard, and the rotated refresh-token store.
|
|
6
3
|
import { createHash, createHmac, hkdfSync, randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
|
|
7
4
|
import { existsSync, readFileSync } from 'node:fs';
|
|
8
5
|
import { basename } from 'node:path';
|
|
@@ -126,7 +123,7 @@ export class ReplayGuard {
|
|
|
126
123
|
}
|
|
127
124
|
/** Record a jti as spent. A full guard refuses instead of evicting a live
|
|
128
125
|
* jti: a flood of spends would otherwise make a spent artifact usable again,
|
|
129
|
-
* such as a callback the browser binding refused
|
|
126
|
+
* such as a callback the browser binding refused. `artifactExpMs`
|
|
130
127
|
* keeps the record alive as long as the artifact itself, even when the clock
|
|
131
128
|
* stepped back between its signing and this spend. */
|
|
132
129
|
spend(jti, ttlMs, nowMs, artifactExpMs = 0) {
|
|
@@ -165,10 +162,8 @@ export class ReplayGuard {
|
|
|
165
162
|
if (exp < this.earliest)
|
|
166
163
|
this.earliest = exp;
|
|
167
164
|
}
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
// once a second. Walking the map from its head on every call is not cheap: a
|
|
171
|
-
// Map iterator steps over every deleted slot until the table is rebuilt.
|
|
165
|
+
// Swept only when at the cap (a refusal otherwise costs O(1)), and at most
|
|
166
|
+
// once a second: a Map iterator steps over every deleted slot until rebuilt.
|
|
172
167
|
room(nowMs) {
|
|
173
168
|
if (this.seen.size < this.cap)
|
|
174
169
|
return true;
|
|
@@ -190,31 +185,42 @@ export class ReplayGuard {
|
|
|
190
185
|
}
|
|
191
186
|
/** Store files this release writes; an absent `format` is an earlier release's. */
|
|
192
187
|
const REFRESH_FORMAT = 2;
|
|
193
|
-
// r1 token: "r1." + base64url(fid 16 || gen uint32 BE || rand 16 || HMAC-SHA256
|
|
194
|
-
//
|
|
195
|
-
// token can never be read as an earlier release's 43-character token.
|
|
188
|
+
// r1 token: "r1." + base64url(fid 16 || gen uint32 BE || rand 16 || HMAC-SHA256).
|
|
189
|
+
// The dot is outside the base64url alphabet, so it can't be read as a legacy 43-char token.
|
|
196
190
|
const R1_PREFIX = 'r1.';
|
|
197
191
|
const R1_LENGTH = 94;
|
|
198
192
|
const R1_BODY = 36;
|
|
199
193
|
const R1_BYTES = 68;
|
|
200
194
|
const R1_MAC_DOMAIN = Buffer.from('mcp-google-multi refresh r1\n');
|
|
195
|
+
// Domain-separated from R1_MAC_DOMAIN: a successor derived under this key
|
|
196
|
+
// must never verify as a current token's own MAC, or vice versa.
|
|
197
|
+
const R1_SUCCESSOR_DOMAIN = Buffer.from('mcp-google-multi refresh r1 successor\n');
|
|
198
|
+
// Domain-separated from both of the above: a fingerprint must never verify as
|
|
199
|
+
// either a token MAC or a successor derivation under the same key.
|
|
200
|
+
const R1_FINGERPRINT_DOMAIN = Buffer.from('mcp-google-multi refresh r1 fingerprint\n');
|
|
201
201
|
const R1_KEY_INFO = 'mcp-google-multi:refresh-mac:v1';
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
// guesses offline at hash speed. scrypt gives that branch a work factor.
|
|
202
|
+
// A passphrase key (deriveKey's bare-sha256 branch) would otherwise let one
|
|
203
|
+
// leaked r1 token test guesses offline at hash speed; scrypt adds a work factor.
|
|
205
204
|
const R1_SCRYPT_SALT = 'mcp-google-multi:refresh-mac:scrypt:v1';
|
|
206
205
|
const R1_SCRYPT = { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
|
|
207
206
|
const GEN_MAX = 0xffffffff;
|
|
208
207
|
const LEGACY_TOKEN = /^[A-Za-z0-9_-]{43}$/;
|
|
209
208
|
/** With a lifetime set, a stored time further ahead than this is pulled back to the clock. */
|
|
210
209
|
const CLOCK_SKEW_MS = 5 * 60_000;
|
|
210
|
+
/** A gen-1-behind token presented within this long of the rotation that
|
|
211
|
+
* retired it, from the same client, can be a concurrent duplicate rather
|
|
212
|
+
* than theft. See docs/internals.md. */
|
|
213
|
+
export const REPLAY_WINDOW_MS = 30_000;
|
|
211
214
|
const B64URL = /^[A-Za-z0-9_-]+$/;
|
|
212
215
|
const sha256 = (b) => createHash('sha256').update(b).digest();
|
|
213
216
|
/** A non-secret tag for log lines and rate-limit keys: never the token. */
|
|
214
217
|
const familyTag = (family) => sha256(family).toString('hex').slice(0, 8);
|
|
215
218
|
// Keyed on the master key: scrypt runs once per process, not once per store.
|
|
216
219
|
const macKeyMemo = new Map();
|
|
217
|
-
|
|
220
|
+
/** The store's own MAC key for `masterKey`, memoized. Exported so a caller
|
|
221
|
+
* that never touches a RefreshStore (the AS computing a request's
|
|
222
|
+
* clientFingerprint) can key it the same way the store does. */
|
|
223
|
+
export function refreshMacKey(masterKey) {
|
|
218
224
|
const hit = macKeyMemo.get(masterKey);
|
|
219
225
|
if (hit)
|
|
220
226
|
return hit;
|
|
@@ -240,7 +246,7 @@ function parseR1(t, key) {
|
|
|
240
246
|
if (!timingSafeEqual(buf.subarray(R1_BODY), r1Mac(key, buf.subarray(0, R1_BODY))))
|
|
241
247
|
return null;
|
|
242
248
|
const fidBytes = buf.subarray(0, 16);
|
|
243
|
-
return { fidBytes, fid: fidBytes.toString('base64url'), gen: buf.readUInt32BE(16), digest: sha256(buf) };
|
|
249
|
+
return { fidBytes, fid: fidBytes.toString('base64url'), gen: buf.readUInt32BE(16), digest: sha256(buf), body: buf.subarray(0, R1_BODY) };
|
|
244
250
|
}
|
|
245
251
|
function mintR1(key, fid, gen) {
|
|
246
252
|
const body = Buffer.alloc(R1_BODY);
|
|
@@ -250,6 +256,24 @@ function mintR1(key, fid, gen) {
|
|
|
250
256
|
const buf = Buffer.concat([body, r1Mac(key, body)]);
|
|
251
257
|
return { token: R1_PREFIX + buf.toString('base64url'), cur: sha256(buf).toString('base64url') };
|
|
252
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* The token a rotation of `presentedBody` (a parsed token's fid||gen||rand)
|
|
261
|
+
* mints: generation + 1, and a random part derived (not drawn) from the
|
|
262
|
+
* presented body, so re-deriving it from a retired token and comparing the
|
|
263
|
+
* digest against a family's stored `cur` tells a concurrent duplicate from a
|
|
264
|
+
* different gen-1 token. Domain-separated from `r1Mac` so neither function's
|
|
265
|
+
* output can be read as the other's. See docs/internals.md.
|
|
266
|
+
*/
|
|
267
|
+
function mintR1Successor(key, presentedBody) {
|
|
268
|
+
const rand = createHmac('sha256', key).update(R1_SUCCESSOR_DOMAIN).update(presentedBody).digest().subarray(0, 16);
|
|
269
|
+
const body = Buffer.alloc(R1_BODY);
|
|
270
|
+
presentedBody.copy(body, 0, 0, 16); // fid
|
|
271
|
+
body.writeUInt32BE(presentedBody.readUInt32BE(16) + 1, 16); // gen + 1
|
|
272
|
+
rand.copy(body, 20);
|
|
273
|
+
const buf = Buffer.concat([body, r1Mac(key, body)]);
|
|
274
|
+
const digest = sha256(buf);
|
|
275
|
+
return { token: R1_PREFIX + buf.toString('base64url'), cur: digest.toString('base64url'), digest };
|
|
276
|
+
}
|
|
253
277
|
/**
|
|
254
278
|
* The family tag of a refresh token this `masterKey`'s store minted, or null
|
|
255
279
|
* for anything else. Derives the key once; the returned function does no I/O,
|
|
@@ -262,6 +286,24 @@ export function refreshFamilyTagger(masterKey) {
|
|
|
262
286
|
return p ? familyTag(p.fidBytes) : null;
|
|
263
287
|
};
|
|
264
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* A correlation tag for the client presenting a refresh token:
|
|
291
|
+
* HMAC-SHA256(key, client_id + "\n" + user-agent + "\n" + address), truncated
|
|
292
|
+
* to 16 bytes and base64url-encoded. `key` is the store's own MAC key
|
|
293
|
+
* (refreshMacKey), so computing it needs MASTER_KEY: a reader of the store
|
|
294
|
+
* file alone (`by` is stored, not these inputs) cannot brute-force the
|
|
295
|
+
* client_id/user-agent/address that produced it. It is still not a secret:
|
|
296
|
+
* anyone who can see a live request can send the same inputs and get the
|
|
297
|
+
* same tag. Pure, no I/O.
|
|
298
|
+
*/
|
|
299
|
+
export function clientFingerprint(clientId, userAgent, address, key) {
|
|
300
|
+
return createHmac('sha256', key).update(R1_FINGERPRINT_DOMAIN).update(`${clientId}\n${userAgent}\n${address}`).digest().subarray(0, 16).toString('base64url');
|
|
301
|
+
}
|
|
302
|
+
function timingSafeStringEqual(a, b) {
|
|
303
|
+
const ab = Buffer.from(a, 'utf-8');
|
|
304
|
+
const bb = Buffer.from(b, 'utf-8');
|
|
305
|
+
return ab.length === bb.length && timingSafeEqual(ab, bb);
|
|
306
|
+
}
|
|
265
307
|
function ownEntries(o) {
|
|
266
308
|
// An own `__proto__` key (JSON.parse makes one) would set the prototype when copied.
|
|
267
309
|
return o !== null && typeof o === 'object' && !Array.isArray(o) ? Object.entries(o).filter(([k]) => k !== '__proto__') : [];
|
|
@@ -284,7 +326,8 @@ function isFamilyRecord(key, r) {
|
|
|
284
326
|
Number.isInteger(x.gen) && x.gen >= 0 && x.gen <= GEN_MAX &&
|
|
285
327
|
typeof x.cur === 'string' && x.cur.length === 43 && isCanonicalB64(x.cur, 32) &&
|
|
286
328
|
Number.isFinite(x.createdAt) && Number.isFinite(x.usedAt) &&
|
|
287
|
-
(x.legacy === undefined || typeof x.legacy === 'string')
|
|
329
|
+
(x.legacy === undefined || typeof x.legacy === 'string') &&
|
|
330
|
+
(x.by === undefined || typeof x.by === 'string'));
|
|
288
331
|
}
|
|
289
332
|
function formatError(file, format) {
|
|
290
333
|
const name = basename(file);
|
|
@@ -304,16 +347,19 @@ function checkFormat(file, d) {
|
|
|
304
347
|
throw formatError(file, d.format);
|
|
305
348
|
}
|
|
306
349
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
* `dirty` says something was dropped. */
|
|
350
|
+
// Only well-formed own entries survive, so no malformed record reaches a
|
|
351
|
+
// liveness test or the token codec; `dirty` says something was dropped.
|
|
310
352
|
function decodeData(d) {
|
|
311
353
|
const data = { families: {}, active: {}, spent: {} };
|
|
312
354
|
const x = (d ?? {});
|
|
313
355
|
let dropped = 0;
|
|
314
356
|
for (const [k, f] of ownEntries(x.families)) {
|
|
315
357
|
if (isFamilyRecord(k, f)) {
|
|
316
|
-
data.families[k] = {
|
|
358
|
+
data.families[k] = {
|
|
359
|
+
sub: f.sub, gen: f.gen, cur: f.cur, createdAt: f.createdAt, usedAt: f.usedAt,
|
|
360
|
+
...(f.legacy === undefined ? {} : { legacy: f.legacy }),
|
|
361
|
+
...(f.by === undefined ? {} : { by: f.by }),
|
|
362
|
+
};
|
|
317
363
|
}
|
|
318
364
|
else
|
|
319
365
|
dropped += 1;
|
|
@@ -378,10 +424,10 @@ function lifetimeMs(name, v) {
|
|
|
378
424
|
* Persisted (encrypted under MASTER_KEY) refresh-token store. Rotates on every
|
|
379
425
|
* use, and a token carries its family and generation under a MAC, so
|
|
380
426
|
* presenting any generation the family has rotated past is REUSE: the family
|
|
381
|
-
* is revoked for the rest of its life
|
|
427
|
+
* is revoked for the rest of its life. The store keeps one
|
|
382
428
|
* record per family, whatever its rotation count. Every read-modify-write goes
|
|
383
429
|
* through a file lock so concurrent stdio+HTTP processes can't lost-update or
|
|
384
|
-
* double-spend
|
|
430
|
+
* double-spend. See docs/internals.md.
|
|
385
431
|
*/
|
|
386
432
|
export class RefreshStore {
|
|
387
433
|
path;
|
|
@@ -504,20 +550,12 @@ export class RefreshStore {
|
|
|
504
550
|
this.log(line);
|
|
505
551
|
}
|
|
506
552
|
catch {
|
|
507
|
-
//
|
|
553
|
+
// log sink failure ignored
|
|
508
554
|
}
|
|
509
555
|
}
|
|
510
556
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
* as deleting it from `data.families` / `data.active` on a revoke, so it
|
|
514
|
-
* can never be presented again (reuse detection for every OTHER family is
|
|
515
|
-
* untouched). Only `sub`'s own entries are ever inspected.
|
|
516
|
-
*
|
|
517
|
-
* `keepKey` is the family just issued: it is excluded from the ranking so
|
|
518
|
-
* a clock step back, or a concurrent rotate's `usedAt = max(usedAt, nowMs)`
|
|
519
|
-
* racing ahead of it under the lock, can never make the brand-new family
|
|
520
|
-
* look like the least recently used one and drop itself. */
|
|
557
|
+
// Evicts sub's own least-recently-used sessions down to `limit`; keepKey (the
|
|
558
|
+
// family just issued) is excluded from ranking. See docs/internals.md.
|
|
521
559
|
enforceFamilyCap(data, sub, limit, lines, keepKey) {
|
|
522
560
|
const entries = [];
|
|
523
561
|
for (const [k, f] of Object.entries(data.families)) {
|
|
@@ -556,7 +594,7 @@ export class RefreshStore {
|
|
|
556
594
|
while (Object.hasOwn(data.families, fid.toString('base64url')));
|
|
557
595
|
return fid;
|
|
558
596
|
}
|
|
559
|
-
issue(nowMs, sub) {
|
|
597
|
+
issue(nowMs, sub, fingerprint) {
|
|
560
598
|
if (typeof sub !== 'string' || sub === '')
|
|
561
599
|
throw new Error('RefreshStore.issue: sub must be a non-empty string');
|
|
562
600
|
const key = this.key();
|
|
@@ -566,7 +604,7 @@ export class RefreshStore {
|
|
|
566
604
|
const fid = this.newFamilyId(data);
|
|
567
605
|
const fidKey = fid.toString('base64url');
|
|
568
606
|
const { token, cur } = mintR1(key, fid, 0);
|
|
569
|
-
data.families[fidKey] = { sub, gen: 0, cur, createdAt: nowMs, usedAt: nowMs };
|
|
607
|
+
data.families[fidKey] = { sub, gen: 0, cur, createdAt: nowMs, usedAt: nowMs, ...(fingerprint === undefined ? {} : { by: fingerprint }) };
|
|
570
608
|
if (this.maxFamiliesPerSub !== undefined)
|
|
571
609
|
this.enforceFamilyCap(data, sub, this.maxFamiliesPerSub, lines, fidKey);
|
|
572
610
|
this.save(data);
|
|
@@ -574,6 +612,25 @@ export class RefreshStore {
|
|
|
574
612
|
return token;
|
|
575
613
|
});
|
|
576
614
|
}
|
|
615
|
+
/** A presented token exactly one generation behind `f`, inside the replay
|
|
616
|
+
* window, from the fingerprint that retired it, whose derived successor
|
|
617
|
+
* matches `f.cur`: a concurrent duplicate request rather than theft (see
|
|
618
|
+
* docs/internals.md). Null for anything else at that generation, including
|
|
619
|
+
* a different rand (a genuinely different gen-1 token) or no fingerprint. */
|
|
620
|
+
concurrentDuplicate(r1, f, nowMs, fingerprint, key) {
|
|
621
|
+
if (fingerprint === undefined || f.by === undefined)
|
|
622
|
+
return null;
|
|
623
|
+
if (r1.gen !== f.gen - 1)
|
|
624
|
+
return null;
|
|
625
|
+
if (nowMs < f.usedAt || nowMs - f.usedAt > REPLAY_WINDOW_MS)
|
|
626
|
+
return null;
|
|
627
|
+
if (!timingSafeStringEqual(fingerprint, f.by))
|
|
628
|
+
return null;
|
|
629
|
+
const succ = mintR1Successor(key, r1.body);
|
|
630
|
+
if (!timingSafeEqual(succ.digest, Buffer.from(f.cur, 'base64url')))
|
|
631
|
+
return null;
|
|
632
|
+
return { token: succ.token };
|
|
633
|
+
}
|
|
577
634
|
/** Rotate a presented refresh token; null if it is unknown, malformed, or
|
|
578
635
|
* rotated away (reuse => the family is revoked). The record's sub is copied
|
|
579
636
|
* forward and returned so the caller can mint the matching access token
|
|
@@ -582,8 +639,17 @@ export class RefreshStore {
|
|
|
582
639
|
* `accept` (optional) is called inside the store lock, before any mutation,
|
|
583
640
|
* with the record's sub. false: every session of that sub is dropped and
|
|
584
641
|
* null returned. A throw: nothing is written and the error propagates, so
|
|
585
|
-
* the presented token stays valid. Absent: unchanged.
|
|
586
|
-
|
|
642
|
+
* the presented token stays valid. Absent: unchanged.
|
|
643
|
+
*
|
|
644
|
+
* `fingerprint` (optional) names the presenting client (clientFingerprint).
|
|
645
|
+
* Absent: a rotated-away token is always reuse, as before. Present: a
|
|
646
|
+
* token exactly one generation behind, from the same fingerprint, presented
|
|
647
|
+
* no earlier than and at most REPLAY_WINDOW_MS after the rotation that
|
|
648
|
+
* retired it, is a concurrent duplicate: it gets back the SAME successor
|
|
649
|
+
* token, with no change to the family (not even `usedAt`). Anything else
|
|
650
|
+
* rotated-away is still reuse, including a clock that has stepped back
|
|
651
|
+
* past `usedAt`, which cannot be told apart from a genuinely stale replay. */
|
|
652
|
+
rotate(oldToken, nowMs, accept, fingerprint) {
|
|
587
653
|
const key = this.key();
|
|
588
654
|
const r1 = parseR1(oldToken, key);
|
|
589
655
|
if (!r1 && !(typeof oldToken === 'string' && LEGACY_TOKEN.test(oldToken) && !this.legacyEmpty))
|
|
@@ -611,9 +677,16 @@ export class RefreshStore {
|
|
|
611
677
|
lines.push(`refresh family ${why === 'reuse' ? 'revoked' : 'dropped'}: ${why} tag=${familyTag(r1.fidBytes)}`);
|
|
612
678
|
return done(null, true);
|
|
613
679
|
};
|
|
614
|
-
// Older: a rotated-away token
|
|
615
|
-
if (r1.gen < f.gen)
|
|
616
|
-
|
|
680
|
+
// Older: a rotated-away token verified by MAC as server-minted, unless a concurrent duplicate.
|
|
681
|
+
if (r1.gen < f.gen) {
|
|
682
|
+
const duplicate = this.concurrentDuplicate(r1, f, nowMs, fingerprint, key);
|
|
683
|
+
if (!duplicate)
|
|
684
|
+
return drop('reuse');
|
|
685
|
+
if (accept && !accept(f.sub))
|
|
686
|
+
return refuse(f.sub);
|
|
687
|
+
lines.push(`refresh family replayed tag=${familyTag(r1.fidBytes)}`);
|
|
688
|
+
return done({ token: duplicate.token, sub: f.sub }, dirty);
|
|
689
|
+
}
|
|
617
690
|
// Newer, or the same generation with another random part: the store
|
|
618
691
|
// lost a write the client saw. Not evidence of theft.
|
|
619
692
|
if (r1.gen > f.gen)
|
|
@@ -624,11 +697,15 @@ export class RefreshStore {
|
|
|
624
697
|
return refuse(f.sub);
|
|
625
698
|
if (f.gen === GEN_MAX)
|
|
626
699
|
return drop('overflow');
|
|
627
|
-
const next =
|
|
700
|
+
const next = mintR1Successor(key, r1.body);
|
|
628
701
|
f.gen += 1;
|
|
629
702
|
f.cur = next.cur;
|
|
630
703
|
// now() is read before the lock wait, so a later writer can hold an earlier time.
|
|
631
704
|
f.usedAt = Math.max(f.usedAt, nowMs);
|
|
705
|
+
if (fingerprint === undefined)
|
|
706
|
+
delete f.by;
|
|
707
|
+
else
|
|
708
|
+
f.by = fingerprint;
|
|
632
709
|
return done({ token: next.token, sub: f.sub }, true);
|
|
633
710
|
}
|
|
634
711
|
const rec = Object.hasOwn(data.active, oldToken) ? data.active[oldToken] : undefined;
|
package/dist/metrics-cli.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ interface PromotionRow {
|
|
|
15
15
|
calls: number;
|
|
16
16
|
kind: 'curation-candidate' | 'visibility-failure' | 'generation-gap';
|
|
17
17
|
}
|
|
18
|
-
/** The promotion-queue view: ranked evidence, a human decides
|
|
18
|
+
/** The promotion-queue view: ranked evidence, a human decides. */
|
|
19
19
|
export declare function classifyPromotion(merged: DayAgg, data: PromotionData): PromotionRow[];
|
|
20
20
|
export declare function runMetricsCli(argv: string[], deps: MetricsCliDeps): number;
|
|
21
21
|
export {};
|
package/dist/metrics-cli.js
CHANGED
|
@@ -87,7 +87,7 @@ function renderReport(merged, days, out) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
const tail = (id) => id.split('.').slice(1).join('.');
|
|
90
|
-
/** The promotion-queue view: ranked evidence, a human decides
|
|
90
|
+
/** The promotion-queue view: ranked evidence, a human decides. */
|
|
91
91
|
export function classifyPromotion(merged, data) {
|
|
92
92
|
const curated = new Set();
|
|
93
93
|
for (const id of data.curatedIds) {
|
package/dist/metrics-tap.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { isSchemaValidationText } from './arg-normalize.js';
|
|
2
2
|
const PENDING_CAP = 1_000;
|
|
3
|
-
//
|
|
4
|
-
// the
|
|
5
|
-
// withValidationEnvelope sits BELOW it and converts that same frame into an
|
|
6
|
-
// envelope for the client, so classification here and the contract on the wire
|
|
7
|
-
// stay independent.
|
|
3
|
+
// This tap sees the ORIGINAL prose; withValidationEnvelope sits BELOW it and
|
|
4
|
+
// converts the same frame for the client, so the two stay independent.
|
|
8
5
|
export { isSchemaValidationText };
|
|
9
6
|
/**
|
|
10
7
|
* @param isKnownTool membership test against the REGISTERED tool set. The
|
package/dist/migrate-config.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Synthesize config.json accounts{} from the v5 env (GOOGLE_ACCOUNTS +
|
|
3
3
|
* GOOGLE_ADMIN_ACCOUNTS). Never deletes or edits env; idempotent (a re-run
|
|
4
|
-
* that changes nothing says so).
|
|
5
|
-
* migrated by their own slices' commands.
|
|
4
|
+
* that changes nothing says so).
|
|
6
5
|
*/
|
|
7
6
|
export declare function runMigrateConfig(env?: NodeJS.ProcessEnv): void;
|
package/dist/migrate-config.js
CHANGED
|
@@ -6,8 +6,7 @@ function parseCsv(value) {
|
|
|
6
6
|
/**
|
|
7
7
|
* Synthesize config.json accounts{} from the v5 env (GOOGLE_ACCOUNTS +
|
|
8
8
|
* GOOGLE_ADMIN_ACCOUNTS). Never deletes or edits env; idempotent (a re-run
|
|
9
|
-
* that changes nothing says so).
|
|
10
|
-
* migrated by their own slices' commands.
|
|
9
|
+
* that changes nothing says so).
|
|
11
10
|
*/
|
|
12
11
|
export function runMigrateConfig(env = process.env) {
|
|
13
12
|
const raw = env.GOOGLE_ACCOUNTS;
|
|
@@ -35,7 +34,7 @@ export function runMigrateConfig(env = process.env) {
|
|
|
35
34
|
}
|
|
36
35
|
desired[alias] = { email };
|
|
37
36
|
}
|
|
38
|
-
// Legacy global scopes
|
|
37
|
+
// Legacy global scopes become an explicit named profile every account
|
|
39
38
|
// points at, so removing the env var later changes nothing.
|
|
40
39
|
const legacyNames = parseCsv(env.GOOGLE_OPTIONAL_SCOPES);
|
|
41
40
|
let legacyProfile;
|
|
@@ -59,8 +58,7 @@ export function runMigrateConfig(env = process.env) {
|
|
|
59
58
|
const filePath = configFilePath(env);
|
|
60
59
|
const before = loadConfigFile(filePath);
|
|
61
60
|
// Merge per-alias: env owns the alias set and emails, but file-only fields
|
|
62
|
-
// (scopeProfile, admin) survive for retained aliases
|
|
63
|
-
// precedence, where unset GOOGLE_ADMIN_ACCOUNTS preserves file admin flags.
|
|
61
|
+
// (scopeProfile, admin) survive for retained aliases.
|
|
64
62
|
const existing = before?.accounts ?? {};
|
|
65
63
|
const dropped = Object.keys(existing).filter((a) => !(a in desired));
|
|
66
64
|
for (const [alias, entry] of Object.entries(desired)) {
|
package/dist/net-tuning.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import net from 'node:net';
|
|
2
|
-
// Node's happy-eyeballs
|
|
3
|
-
//
|
|
4
|
-
// broken-IPv6 links that aborts EVERY Google call while curl works. Raise the
|
|
5
|
-
// process default unless the user tuned it themselves. Why: docs/internals.md.
|
|
2
|
+
// Node's happy-eyeballs 250ms-per-attempt default aborts EVERY Google call on
|
|
3
|
+
// a high-latency or broken-IPv6 link while curl works. See docs/internals.md.
|
|
6
4
|
export const CONNECT_ATTEMPT_TIMEOUT_MS = 2000;
|
|
7
5
|
const USER_FLAGS = ['--network-family-autoselection-attempt-timeout', '--no-network-family-autoselection'];
|
|
8
6
|
/** Pure decision: the timeout to apply, or null to leave Node's setting alone. */
|
package/dist/oauth-as.d.ts
CHANGED
|
@@ -60,7 +60,7 @@ export interface GoogleIdentity {
|
|
|
60
60
|
sub?: string;
|
|
61
61
|
hd?: string;
|
|
62
62
|
}
|
|
63
|
-
/** Only trust the id_token identity when Google marks the email verified
|
|
63
|
+
/** Only trust the id_token identity when Google marks the email verified.
|
|
64
64
|
* The token comes straight from Google's token endpoint, so its claims are
|
|
65
65
|
* read unsigned. A malformed sub or hd is dropped, never passed on. */
|
|
66
66
|
export declare function verifiedIdentityFromIdToken(idToken?: string | null): GoogleIdentity | undefined;
|
|
@@ -91,6 +91,11 @@ export interface AuthServerDeps {
|
|
|
91
91
|
}>;
|
|
92
92
|
replayGuard?: ReplayGuard;
|
|
93
93
|
refreshStore?: RefreshStore;
|
|
94
|
+
/** The presenting client's address, for the refresh-rotation fingerprint
|
|
95
|
+
* (clientFingerprint) that tells a concurrent duplicate request from a
|
|
96
|
+
* different holder of the same token. Default: the socket's remote
|
|
97
|
+
* address. A host behind a trusted proxy wires its own resolver here. */
|
|
98
|
+
clientAddress?: (req: IncomingMessage) => string | undefined;
|
|
94
99
|
now?: () => number;
|
|
95
100
|
/** Server-side log (never sent to callers) for auth failures / SSRF blocks. */
|
|
96
101
|
log?: (line: string) => void;
|
|
@@ -146,7 +151,7 @@ export interface AuthServer {
|
|
|
146
151
|
* start a re-auth nor learn which aliases exist or what they hold. */
|
|
147
152
|
reauthLink: (alias: string) => string;
|
|
148
153
|
}
|
|
149
|
-
/**
|
|
154
|
+
/** Exact redirect_uri match, with RFC 8252 §7.3 port-agnostic loopback and
|
|
150
155
|
* the fixed claude.ai callback. */
|
|
151
156
|
export declare function redirectAllowed(redirectUri: string, docRedirectUris: string[]): boolean;
|
|
152
157
|
export declare function buildAuthServer(config: AuthServerConfig, deps?: AuthServerDeps): AuthServer;
|