switchroom 0.14.39 → 0.14.40
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/auth-broker/index.js +294 -46
- package/dist/cli/drive-write-pretool.mjs +25 -1
- package/dist/cli/switchroom.js +63 -6
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +103 -11
- package/telegram-plugin/gateway/gateway.ts +81 -3
- package/telegram-plugin/gateway/inbound-delivery-confirm.ts +96 -0
- package/telegram-plugin/tests/inbound-delivery-confirm.test.ts +109 -0
- package/telegram-plugin/uat/scenarios/inbound-no-drop-rapid-fire-dm.test.ts +64 -0
|
@@ -6932,7 +6932,7 @@ var require_public_api = __commonJS((exports) => {
|
|
|
6932
6932
|
});
|
|
6933
6933
|
|
|
6934
6934
|
// src/auth/broker/index.ts
|
|
6935
|
-
import { existsSync as
|
|
6935
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
|
|
6936
6936
|
|
|
6937
6937
|
// src/config/loader.ts
|
|
6938
6938
|
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
|
|
@@ -12152,20 +12152,20 @@ import * as net from "node:net";
|
|
|
12152
12152
|
import {
|
|
12153
12153
|
chmodSync,
|
|
12154
12154
|
chownSync as chownSync2,
|
|
12155
|
-
existsSync as
|
|
12155
|
+
existsSync as existsSync7,
|
|
12156
12156
|
lstatSync,
|
|
12157
|
-
mkdirSync as
|
|
12158
|
-
readFileSync as
|
|
12157
|
+
mkdirSync as mkdirSync4,
|
|
12158
|
+
readFileSync as readFileSync6,
|
|
12159
12159
|
renameSync as renameSync3,
|
|
12160
|
-
rmSync as
|
|
12161
|
-
statSync as
|
|
12160
|
+
rmSync as rmSync5,
|
|
12161
|
+
statSync as statSync5,
|
|
12162
12162
|
unlinkSync,
|
|
12163
12163
|
writeFileSync as writeFileSync2
|
|
12164
12164
|
} from "node:fs";
|
|
12165
12165
|
import { closeSync as closeSync2, openSync as openSync2, writeSync as writeSync2 } from "node:fs";
|
|
12166
12166
|
import * as constants2 from "node:constants";
|
|
12167
12167
|
import { createHash as createHash2 } from "node:crypto";
|
|
12168
|
-
import { dirname as
|
|
12168
|
+
import { dirname as dirname3, join as join4, resolve as resolve7 } from "node:path";
|
|
12169
12169
|
|
|
12170
12170
|
// src/agents/compose.ts
|
|
12171
12171
|
import { createHash } from "node:crypto";
|
|
@@ -13021,6 +13021,74 @@ function listGoogleAccounts(stateDir) {
|
|
|
13021
13021
|
}).filter((name) => existsSync5(join2(root, name, "credentials.json")));
|
|
13022
13022
|
}
|
|
13023
13023
|
|
|
13024
|
+
// src/auth/broker/microsoft-storage.ts
|
|
13025
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync5, rmSync as rmSync4, statSync as statSync4 } from "node:fs";
|
|
13026
|
+
import { dirname as dirname2, join as join3, resolve as resolve6 } from "node:path";
|
|
13027
|
+
function normalizeMicrosoftAccountForStorage(account) {
|
|
13028
|
+
return account.trim().toLowerCase();
|
|
13029
|
+
}
|
|
13030
|
+
function validateMicrosoftAccountLabel(account) {
|
|
13031
|
+
if (typeof account !== "string" || account.length === 0) {
|
|
13032
|
+
throw new Error(`Microsoft account label must be a non-empty string`);
|
|
13033
|
+
}
|
|
13034
|
+
if (account !== account.trim()) {
|
|
13035
|
+
throw new Error(`Microsoft account label must not have leading/trailing whitespace`);
|
|
13036
|
+
}
|
|
13037
|
+
if (/[\x00-\x1f\x7f]/.test(account)) {
|
|
13038
|
+
throw new Error(`Microsoft account label '${account.replace(/[\x00-\x1f\x7f]/g, "?")}' contains control characters; email shape rejects them.`);
|
|
13039
|
+
}
|
|
13040
|
+
if (!/^[^@\s:/\\]+@[^@\s:/\\]+\.[^@\s:/\\]+$/.test(account)) {
|
|
13041
|
+
throw new Error(`Microsoft account label '${account}' is not a valid email shape. Expected like 'alice@outlook.com' or 'alice@contoso.com' (no slashes, colons, or whitespace).`);
|
|
13042
|
+
}
|
|
13043
|
+
}
|
|
13044
|
+
function microsoftAccountDir(stateDir, account) {
|
|
13045
|
+
return resolve6(stateDir, "microsoft", normalizeMicrosoftAccountForStorage(account));
|
|
13046
|
+
}
|
|
13047
|
+
function microsoftAccountCredentialsPath(stateDir, account) {
|
|
13048
|
+
return join3(microsoftAccountDir(stateDir, account), "credentials.json");
|
|
13049
|
+
}
|
|
13050
|
+
function microsoftAccountExists(stateDir, account) {
|
|
13051
|
+
return existsSync6(microsoftAccountCredentialsPath(stateDir, account));
|
|
13052
|
+
}
|
|
13053
|
+
function readMicrosoftAccountCredentials(stateDir, account) {
|
|
13054
|
+
const path = microsoftAccountCredentialsPath(stateDir, account);
|
|
13055
|
+
if (!existsSync6(path))
|
|
13056
|
+
return null;
|
|
13057
|
+
try {
|
|
13058
|
+
const raw = readFileSync5(path, "utf-8");
|
|
13059
|
+
const parsed = JSON.parse(raw);
|
|
13060
|
+
if (!parsed?.microsoftOauth?.accessToken)
|
|
13061
|
+
return null;
|
|
13062
|
+
return parsed;
|
|
13063
|
+
} catch {
|
|
13064
|
+
return null;
|
|
13065
|
+
}
|
|
13066
|
+
}
|
|
13067
|
+
function writeMicrosoftAccountCredentials(stateDir, account, credentials) {
|
|
13068
|
+
const path = microsoftAccountCredentialsPath(stateDir, account);
|
|
13069
|
+
mkdirSync3(dirname2(path), { recursive: true, mode: 448 });
|
|
13070
|
+
atomicWriteFileSync(path, JSON.stringify(credentials, null, 2), 384);
|
|
13071
|
+
return path;
|
|
13072
|
+
}
|
|
13073
|
+
function removeMicrosoftAccount(stateDir, account) {
|
|
13074
|
+
const dir = microsoftAccountDir(stateDir, account);
|
|
13075
|
+
if (existsSync6(dir)) {
|
|
13076
|
+
rmSync4(dir, { recursive: true, force: true });
|
|
13077
|
+
}
|
|
13078
|
+
}
|
|
13079
|
+
function listMicrosoftAccounts(stateDir) {
|
|
13080
|
+
const root = join3(stateDir, "microsoft");
|
|
13081
|
+
if (!existsSync6(root))
|
|
13082
|
+
return [];
|
|
13083
|
+
return readdirSync4(root).filter((name) => {
|
|
13084
|
+
try {
|
|
13085
|
+
return statSync4(join3(root, name)).isDirectory();
|
|
13086
|
+
} catch {
|
|
13087
|
+
return false;
|
|
13088
|
+
}
|
|
13089
|
+
}).filter((name) => existsSync6(join3(root, name, "credentials.json")));
|
|
13090
|
+
}
|
|
13091
|
+
|
|
13024
13092
|
// src/auth/broker/provider.ts
|
|
13025
13093
|
class ProviderRegistry {
|
|
13026
13094
|
providers = new Map;
|
|
@@ -13195,6 +13263,11 @@ var ListGoogleAccountsRequestSchema = exports_external.object({
|
|
|
13195
13263
|
op: exports_external.literal("list-google-accounts"),
|
|
13196
13264
|
id: exports_external.string().min(1)
|
|
13197
13265
|
});
|
|
13266
|
+
var ListMicrosoftAccountsRequestSchema = exports_external.object({
|
|
13267
|
+
v: exports_external.literal(PROTOCOL_VERSION),
|
|
13268
|
+
op: exports_external.literal("list-microsoft-accounts"),
|
|
13269
|
+
id: exports_external.string().min(1)
|
|
13270
|
+
});
|
|
13198
13271
|
var ProbeQuotaRequestSchema = exports_external.object({
|
|
13199
13272
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
13200
13273
|
op: exports_external.literal("probe-quota"),
|
|
@@ -13212,6 +13285,7 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
|
|
|
13212
13285
|
RmAccountRequestSchema,
|
|
13213
13286
|
SetOverrideRequestSchema,
|
|
13214
13287
|
ListGoogleAccountsRequestSchema,
|
|
13288
|
+
ListMicrosoftAccountsRequestSchema,
|
|
13215
13289
|
ProbeQuotaRequestSchema
|
|
13216
13290
|
]);
|
|
13217
13291
|
var GetCredentialsDataSchema = exports_external.object({
|
|
@@ -13276,6 +13350,16 @@ var GoogleAccountStateSchema = exports_external.object({
|
|
|
13276
13350
|
var ListGoogleAccountsDataSchema = exports_external.object({
|
|
13277
13351
|
accounts: exports_external.array(GoogleAccountStateSchema)
|
|
13278
13352
|
});
|
|
13353
|
+
var MicrosoftAccountStateSchema = exports_external.object({
|
|
13354
|
+
account: exports_external.string(),
|
|
13355
|
+
expiresAt: exports_external.number(),
|
|
13356
|
+
scope: exports_external.string(),
|
|
13357
|
+
clientId: exports_external.string(),
|
|
13358
|
+
accountType: exports_external.enum(["personal", "work"])
|
|
13359
|
+
});
|
|
13360
|
+
var ListMicrosoftAccountsDataSchema = exports_external.object({
|
|
13361
|
+
accounts: exports_external.array(MicrosoftAccountStateSchema)
|
|
13362
|
+
});
|
|
13279
13363
|
var ErrorBodySchema = exports_external.object({
|
|
13280
13364
|
code: exports_external.enum([
|
|
13281
13365
|
"FORBIDDEN",
|
|
@@ -13405,7 +13489,7 @@ class AuthBroker {
|
|
|
13405
13489
|
this.now = opts.now ?? nowMs;
|
|
13406
13490
|
this.operatorUid = opts.operatorUid;
|
|
13407
13491
|
this.fetcher = opts.fetcher;
|
|
13408
|
-
this.stateDir = opts.stateDir ??
|
|
13492
|
+
this.stateDir = opts.stateDir ?? resolve7(this.homeRoot(), ".switchroom", "state", "auth-broker");
|
|
13409
13493
|
this.socketRoot = opts.socketRoot ?? AUTH_BROKER_ROOT;
|
|
13410
13494
|
this.providers = new ProviderRegistry;
|
|
13411
13495
|
this.providers.register(new AnthropicProvider);
|
|
@@ -13433,9 +13517,9 @@ class AuthBroker {
|
|
|
13433
13517
|
}
|
|
13434
13518
|
async start() {
|
|
13435
13519
|
process.umask(63);
|
|
13436
|
-
|
|
13437
|
-
|
|
13438
|
-
|
|
13520
|
+
mkdirSync4(this.stateDir, { recursive: true, mode: 448 });
|
|
13521
|
+
mkdirSync4(join4(this.stateDir, "refresh-lease"), { recursive: true, mode: 448 });
|
|
13522
|
+
mkdirSync4(this.socketRoot, { recursive: true, mode: 493 });
|
|
13439
13523
|
this.loadStateFromDisk();
|
|
13440
13524
|
this.assertDriftFree();
|
|
13441
13525
|
for (const agentName of Object.keys(this.config.agents ?? {})) {
|
|
@@ -13462,7 +13546,7 @@ class AuthBroker {
|
|
|
13462
13546
|
}
|
|
13463
13547
|
if (!this.opts.skipHealthyMarker) {
|
|
13464
13548
|
try {
|
|
13465
|
-
const healthyPath =
|
|
13549
|
+
const healthyPath = join4(this.stateDir, "healthy");
|
|
13466
13550
|
writeFileSync2(healthyPath, String(this.now()) + `
|
|
13467
13551
|
`, { mode: 384 });
|
|
13468
13552
|
} catch (err) {
|
|
@@ -13485,14 +13569,14 @@ class AuthBroker {
|
|
|
13485
13569
|
lis.server.close();
|
|
13486
13570
|
} catch {}
|
|
13487
13571
|
try {
|
|
13488
|
-
if (
|
|
13572
|
+
if (existsSync7(sock))
|
|
13489
13573
|
unlinkSync(sock);
|
|
13490
13574
|
} catch {}
|
|
13491
13575
|
}
|
|
13492
13576
|
this.listeners.clear();
|
|
13493
13577
|
try {
|
|
13494
|
-
const healthyPath =
|
|
13495
|
-
if (
|
|
13578
|
+
const healthyPath = join4(this.stateDir, "healthy");
|
|
13579
|
+
if (existsSync7(healthyPath))
|
|
13496
13580
|
unlinkSync(healthyPath);
|
|
13497
13581
|
} catch {}
|
|
13498
13582
|
}
|
|
@@ -13516,7 +13600,7 @@ class AuthBroker {
|
|
|
13516
13600
|
lis.server.close();
|
|
13517
13601
|
} catch {}
|
|
13518
13602
|
try {
|
|
13519
|
-
if (
|
|
13603
|
+
if (existsSync7(sock))
|
|
13520
13604
|
unlinkSync(sock);
|
|
13521
13605
|
} catch {}
|
|
13522
13606
|
this.listeners.delete(sock);
|
|
@@ -13539,13 +13623,13 @@ class AuthBroker {
|
|
|
13539
13623
|
}
|
|
13540
13624
|
}
|
|
13541
13625
|
agentSocketPath(name) {
|
|
13542
|
-
return
|
|
13626
|
+
return join4(this.socketRoot, name, "sock");
|
|
13543
13627
|
}
|
|
13544
13628
|
consumerSocketPath(name) {
|
|
13545
|
-
return
|
|
13629
|
+
return join4(this.socketRoot, name, "sock");
|
|
13546
13630
|
}
|
|
13547
13631
|
operatorSocketPath() {
|
|
13548
|
-
return
|
|
13632
|
+
return join4(this.socketRoot, "operator", "sock");
|
|
13549
13633
|
}
|
|
13550
13634
|
async bindAgentListener(agentName) {
|
|
13551
13635
|
if (RESERVED_NAMES.has(agentName)) {
|
|
@@ -13575,8 +13659,8 @@ class AuthBroker {
|
|
|
13575
13659
|
await this.bindListener(sockPath, operatorUid, 384, { kind: "operator" });
|
|
13576
13660
|
}
|
|
13577
13661
|
async bindListener(sockPath, targetUid, sockMode, identity2) {
|
|
13578
|
-
const dir =
|
|
13579
|
-
if (
|
|
13662
|
+
const dir = dirname3(sockPath);
|
|
13663
|
+
if (existsSync7(dir)) {
|
|
13580
13664
|
try {
|
|
13581
13665
|
chownSync2(dir, 0, 0);
|
|
13582
13666
|
} catch {}
|
|
@@ -13584,9 +13668,9 @@ class AuthBroker {
|
|
|
13584
13668
|
chmodSync(dir, 448);
|
|
13585
13669
|
} catch {}
|
|
13586
13670
|
} else {
|
|
13587
|
-
|
|
13671
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
13588
13672
|
}
|
|
13589
|
-
if (
|
|
13673
|
+
if (existsSync7(sockPath)) {
|
|
13590
13674
|
try {
|
|
13591
13675
|
unlinkSync(sockPath);
|
|
13592
13676
|
} catch {}
|
|
@@ -13667,6 +13751,10 @@ class AuthBroker {
|
|
|
13667
13751
|
await this.opGoogleGetCredentials(socket, reqId, identity2);
|
|
13668
13752
|
break;
|
|
13669
13753
|
}
|
|
13754
|
+
if (provider === "microsoft") {
|
|
13755
|
+
await this.opMicrosoftGetCredentials(socket, reqId, identity2);
|
|
13756
|
+
break;
|
|
13757
|
+
}
|
|
13670
13758
|
socket.write(encodeError(reqId, "INTERNAL", `unhandled provider '${provider}' in get-credentials dispatch`));
|
|
13671
13759
|
break;
|
|
13672
13760
|
}
|
|
@@ -13719,6 +13807,11 @@ class AuthBroker {
|
|
|
13719
13807
|
await this.opGoogleAddAccount(socket, reqId, identity2, req.label, googleCreds, req.replace ?? false);
|
|
13720
13808
|
break;
|
|
13721
13809
|
}
|
|
13810
|
+
if (provider === "microsoft") {
|
|
13811
|
+
const microsoftCreds = req.credentials;
|
|
13812
|
+
await this.opMicrosoftAddAccount(socket, reqId, identity2, req.label, microsoftCreds, req.replace ?? false);
|
|
13813
|
+
break;
|
|
13814
|
+
}
|
|
13722
13815
|
socket.write(encodeError(reqId, "INTERNAL", `unhandled provider '${provider}' in add-account dispatch`));
|
|
13723
13816
|
break;
|
|
13724
13817
|
}
|
|
@@ -13736,6 +13829,10 @@ class AuthBroker {
|
|
|
13736
13829
|
await this.opGoogleRmAccount(socket, reqId, identity2, req.label);
|
|
13737
13830
|
break;
|
|
13738
13831
|
}
|
|
13832
|
+
if (provider === "microsoft") {
|
|
13833
|
+
await this.opMicrosoftRmAccount(socket, reqId, identity2, req.label);
|
|
13834
|
+
break;
|
|
13835
|
+
}
|
|
13739
13836
|
socket.write(encodeError(reqId, "INTERNAL", `unhandled provider '${provider}' in rm-account dispatch`));
|
|
13740
13837
|
break;
|
|
13741
13838
|
}
|
|
@@ -13745,6 +13842,9 @@ class AuthBroker {
|
|
|
13745
13842
|
case "list-google-accounts":
|
|
13746
13843
|
await this.opListGoogleAccounts(socket, reqId, identity2);
|
|
13747
13844
|
break;
|
|
13845
|
+
case "list-microsoft-accounts":
|
|
13846
|
+
await this.opListMicrosoftAccounts(socket, reqId, identity2);
|
|
13847
|
+
break;
|
|
13748
13848
|
case "probe-quota":
|
|
13749
13849
|
await this.opProbeQuota(socket, reqId, identity2, req.accounts, req.timeoutMs);
|
|
13750
13850
|
break;
|
|
@@ -13965,7 +14065,7 @@ class AuthBroker {
|
|
|
13965
14065
|
return;
|
|
13966
14066
|
}
|
|
13967
14067
|
this.chownAccountFilesIfRoot(label);
|
|
13968
|
-
const contents =
|
|
14068
|
+
const contents = readFileSync6(accountCredentialsPath(label, this.home), "utf-8");
|
|
13969
14069
|
this.shaIndex[label] = sha256Hex(contents);
|
|
13970
14070
|
this.lastWrittenExpiresAt.set(label, credentials.claudeAiOauth?.expiresAt);
|
|
13971
14071
|
this.persistShaIndex();
|
|
@@ -13995,7 +14095,7 @@ class AuthBroker {
|
|
|
13995
14095
|
return;
|
|
13996
14096
|
}
|
|
13997
14097
|
try {
|
|
13998
|
-
|
|
14098
|
+
rmSync5(accountDir(label, this.home), { recursive: true, force: true });
|
|
13999
14099
|
} catch (err) {
|
|
14000
14100
|
socket.write(encodeError(id, "INTERNAL", err.message));
|
|
14001
14101
|
return;
|
|
@@ -14098,6 +14198,110 @@ class AuthBroker {
|
|
|
14098
14198
|
this.audit({ op: "rm-account", identity: identity2, account: label, ok: true });
|
|
14099
14199
|
socket.write(encodeSuccess(id, { label }));
|
|
14100
14200
|
}
|
|
14201
|
+
async opMicrosoftGetCredentials(socket, id, identity2) {
|
|
14202
|
+
if (identity2.kind !== "agent") {
|
|
14203
|
+
socket.write(encodeError(id, "INVALID_ARGS", `Microsoft get-credentials is per-agent only (caller kind '${identity2.kind}' not supported); use the agent's per-agent socket bind`));
|
|
14204
|
+
return;
|
|
14205
|
+
}
|
|
14206
|
+
const agentName = identity2.name;
|
|
14207
|
+
const agent = (this.config.agents ?? {})[agentName];
|
|
14208
|
+
const account = agent?.microsoft_workspace?.account;
|
|
14209
|
+
if (!account) {
|
|
14210
|
+
this.audit({ op: "get-credentials", identity: identity2, ok: false, error: "no-microsoft-account-configured" });
|
|
14211
|
+
socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `agent '${agentName}' has no microsoft_workspace.account configured in switchroom.yaml`));
|
|
14212
|
+
return;
|
|
14213
|
+
}
|
|
14214
|
+
const ma = this.config.microsoft_accounts;
|
|
14215
|
+
const enabledFor = ma?.[account]?.enabled_for ?? [];
|
|
14216
|
+
if (!enabledFor.includes(agentName)) {
|
|
14217
|
+
this.audit({ op: "get-credentials", identity: identity2, account, ok: false, error: "acl-deny" });
|
|
14218
|
+
socket.write(encodeError(id, "FORBIDDEN", `agent '${agentName}' not in microsoft_accounts['${account}'].enabled_for[] — operator must run \`switchroom auth microsoft enable ${account} ${agentName}\``));
|
|
14219
|
+
return;
|
|
14220
|
+
}
|
|
14221
|
+
const creds = readMicrosoftAccountCredentials(this.stateDir, account);
|
|
14222
|
+
if (!creds) {
|
|
14223
|
+
this.audit({ op: "get-credentials", identity: identity2, account, ok: false, error: "missing-credentials" });
|
|
14224
|
+
socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `no Microsoft credentials for account '${account}' — operator must run \`switchroom auth microsoft account add ${account}\``));
|
|
14225
|
+
return;
|
|
14226
|
+
}
|
|
14227
|
+
const expiresAt = creds.microsoftOauth?.expiresAt;
|
|
14228
|
+
this.audit({ op: "get-credentials", identity: identity2, account, ok: true });
|
|
14229
|
+
socket.write(encodeSuccess(id, { account, credentials: creds, expiresAt }));
|
|
14230
|
+
}
|
|
14231
|
+
async opMicrosoftAddAccount(socket, id, identity2, label, credentials, replace) {
|
|
14232
|
+
if (!this.isAdmin(identity2)) {
|
|
14233
|
+
this.audit({ op: "add-account", identity: identity2, account: label, ok: false, error: "FORBIDDEN" });
|
|
14234
|
+
this.respondForbidden(socket, id, "add-account requires admin");
|
|
14235
|
+
return;
|
|
14236
|
+
}
|
|
14237
|
+
try {
|
|
14238
|
+
validateMicrosoftAccountLabel(label);
|
|
14239
|
+
} catch (err) {
|
|
14240
|
+
socket.write(encodeError(id, "INVALID_ARGS", err.message));
|
|
14241
|
+
return;
|
|
14242
|
+
}
|
|
14243
|
+
if (microsoftAccountExists(this.stateDir, label) && !replace) {
|
|
14244
|
+
this.audit({ op: "add-account", identity: identity2, account: label, ok: false, error: "ACCOUNT_ALREADY_EXISTS" });
|
|
14245
|
+
socket.write(encodeError(id, "ACCOUNT_ALREADY_EXISTS", `microsoft account '${label}' already exists; pass replace:true to overwrite`));
|
|
14246
|
+
return;
|
|
14247
|
+
}
|
|
14248
|
+
try {
|
|
14249
|
+
writeMicrosoftAccountCredentials(this.stateDir, label, credentials);
|
|
14250
|
+
} catch (err) {
|
|
14251
|
+
socket.write(encodeError(id, "INTERNAL", err.message));
|
|
14252
|
+
return;
|
|
14253
|
+
}
|
|
14254
|
+
const expiresAt = credentials.microsoftOauth?.expiresAt;
|
|
14255
|
+
this.audit({ op: "add-account", identity: identity2, account: label, ok: true, replace });
|
|
14256
|
+
socket.write(encodeSuccess(id, { label, expiresAt }));
|
|
14257
|
+
}
|
|
14258
|
+
async opMicrosoftRmAccount(socket, id, identity2, label) {
|
|
14259
|
+
if (!this.isAdmin(identity2)) {
|
|
14260
|
+
this.audit({ op: "rm-account", identity: identity2, account: label, ok: false, error: "FORBIDDEN" });
|
|
14261
|
+
this.respondForbidden(socket, id, "rm-account requires admin");
|
|
14262
|
+
return;
|
|
14263
|
+
}
|
|
14264
|
+
try {
|
|
14265
|
+
validateMicrosoftAccountLabel(label);
|
|
14266
|
+
} catch (err) {
|
|
14267
|
+
socket.write(encodeError(id, "INVALID_ARGS", err.message));
|
|
14268
|
+
return;
|
|
14269
|
+
}
|
|
14270
|
+
if (!microsoftAccountExists(this.stateDir, label)) {
|
|
14271
|
+
socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `microsoft account '${label}' not found`));
|
|
14272
|
+
return;
|
|
14273
|
+
}
|
|
14274
|
+
const ma = this.config.microsoft_accounts;
|
|
14275
|
+
const enabledFor = ma?.[label]?.enabled_for ?? [];
|
|
14276
|
+
if (enabledFor.length > 0) {
|
|
14277
|
+
socket.write(encodeError(id, "INVALID_ARGS", `microsoft account '${label}' is still enabled for agents: ${enabledFor.join(", ")}. Run \`auth microsoft disable ${label} all\` first.`));
|
|
14278
|
+
return;
|
|
14279
|
+
}
|
|
14280
|
+
try {
|
|
14281
|
+
removeMicrosoftAccount(this.stateDir, label);
|
|
14282
|
+
} catch (err) {
|
|
14283
|
+
socket.write(encodeError(id, "INTERNAL", err.message));
|
|
14284
|
+
return;
|
|
14285
|
+
}
|
|
14286
|
+
this.audit({ op: "rm-account", identity: identity2, account: label, ok: true });
|
|
14287
|
+
socket.write(encodeSuccess(id, { label }));
|
|
14288
|
+
}
|
|
14289
|
+
async opListMicrosoftAccounts(socket, id, identity2) {
|
|
14290
|
+
const accounts = listMicrosoftAccounts(this.stateDir).map((account) => {
|
|
14291
|
+
const creds = readMicrosoftAccountCredentials(this.stateDir, account);
|
|
14292
|
+
if (!creds)
|
|
14293
|
+
return null;
|
|
14294
|
+
return {
|
|
14295
|
+
account,
|
|
14296
|
+
expiresAt: creds.microsoftOauth.expiresAt,
|
|
14297
|
+
scope: creds.microsoftOauth.scope,
|
|
14298
|
+
clientId: creds.microsoftOauth.clientId,
|
|
14299
|
+
accountType: creds.microsoftOauth.accountType
|
|
14300
|
+
};
|
|
14301
|
+
}).filter((entry) => entry !== null).sort((a, b) => a.account.localeCompare(b.account));
|
|
14302
|
+
this.audit({ op: "list-microsoft-accounts", identity: identity2, ok: true });
|
|
14303
|
+
socket.write(encodeSuccess(id, { accounts }));
|
|
14304
|
+
}
|
|
14101
14305
|
async opSetOverride(socket, id, identity2, agentName, account) {
|
|
14102
14306
|
if (!this.isAdmin(identity2)) {
|
|
14103
14307
|
this.audit({ op: "set-override", identity: identity2, account: account ?? undefined, ok: false, error: "FORBIDDEN" });
|
|
@@ -14142,6 +14346,15 @@ class AuthBroker {
|
|
|
14142
14346
|
}
|
|
14143
14347
|
}
|
|
14144
14348
|
}
|
|
14349
|
+
if (this.providers.has("microsoft")) {
|
|
14350
|
+
for (const account of listMicrosoftAccounts(this.stateDir)) {
|
|
14351
|
+
try {
|
|
14352
|
+
await this.refreshOneMicrosoftAccount(account, false);
|
|
14353
|
+
} catch (err) {
|
|
14354
|
+
this.logErr(`refresh-tick microsoft:${account}: ${err.message}`);
|
|
14355
|
+
}
|
|
14356
|
+
}
|
|
14357
|
+
}
|
|
14145
14358
|
}
|
|
14146
14359
|
async refreshOneGoogleAccount(account, force) {
|
|
14147
14360
|
const leaseKey = `google:${account}`;
|
|
@@ -14177,6 +14390,41 @@ class AuthBroker {
|
|
|
14177
14390
|
this.refreshInFlight.delete(leaseKey);
|
|
14178
14391
|
}
|
|
14179
14392
|
}
|
|
14393
|
+
async refreshOneMicrosoftAccount(account, force) {
|
|
14394
|
+
const leaseKey = `microsoft:${account}`;
|
|
14395
|
+
if (this.refreshInFlight.has(leaseKey))
|
|
14396
|
+
return { kind: "noop" };
|
|
14397
|
+
const credsBefore = readMicrosoftAccountCredentials(this.stateDir, account);
|
|
14398
|
+
if (!credsBefore) {
|
|
14399
|
+
return { kind: "noop" };
|
|
14400
|
+
}
|
|
14401
|
+
const provider = this.providers.lookup("microsoft");
|
|
14402
|
+
const onDiskExpires = provider.extractExpiresAt(credsBefore);
|
|
14403
|
+
if (!force) {
|
|
14404
|
+
const remaining = (onDiskExpires ?? 0) - this.now();
|
|
14405
|
+
if (onDiskExpires !== undefined && remaining > REFRESH_THRESHOLD_MS) {
|
|
14406
|
+
return { kind: "noop" };
|
|
14407
|
+
}
|
|
14408
|
+
}
|
|
14409
|
+
this.refreshInFlight.add(leaseKey);
|
|
14410
|
+
try {
|
|
14411
|
+
const refreshToken = credsBefore.microsoftOauth.refreshToken;
|
|
14412
|
+
const result = await provider.refresh({
|
|
14413
|
+
refreshToken,
|
|
14414
|
+
accountEmail: account,
|
|
14415
|
+
clientId: credsBefore.microsoftOauth.clientId,
|
|
14416
|
+
priorCredentials: credsBefore
|
|
14417
|
+
});
|
|
14418
|
+
if (!result.ok) {
|
|
14419
|
+
return { kind: "failed", error: `${result.kind}: ${result.detail}` };
|
|
14420
|
+
}
|
|
14421
|
+
const newCreds = result.rawCredentials;
|
|
14422
|
+
writeMicrosoftAccountCredentials(this.stateDir, account, newCreds);
|
|
14423
|
+
return { kind: "refreshed", newExpiresAt: result.expiresAt };
|
|
14424
|
+
} finally {
|
|
14425
|
+
this.refreshInFlight.delete(leaseKey);
|
|
14426
|
+
}
|
|
14427
|
+
}
|
|
14180
14428
|
async refreshOneAccount(label, force) {
|
|
14181
14429
|
if (this.refreshInFlight.has(label))
|
|
14182
14430
|
return { kind: "noop" };
|
|
@@ -14194,7 +14442,7 @@ class AuthBroker {
|
|
|
14194
14442
|
return { kind: "noop" };
|
|
14195
14443
|
}
|
|
14196
14444
|
}
|
|
14197
|
-
const leasePath =
|
|
14445
|
+
const leasePath = join4(this.stateDir, "refresh-lease", label);
|
|
14198
14446
|
let leaseFd = null;
|
|
14199
14447
|
try {
|
|
14200
14448
|
leaseFd = openSync2(leasePath, constants2.O_RDWR | constants2.O_CREAT, 384);
|
|
@@ -14211,7 +14459,7 @@ class AuthBroker {
|
|
|
14211
14459
|
const creds = readAccountCredentials(label, this.home);
|
|
14212
14460
|
const newExpiresAt = creds?.claudeAiOauth?.expiresAt ?? outcome.newExpiresAt;
|
|
14213
14461
|
this.lastWrittenExpiresAt.set(label, newExpiresAt);
|
|
14214
|
-
const contents =
|
|
14462
|
+
const contents = readFileSync6(accountCredentialsPath(label, this.home), "utf-8");
|
|
14215
14463
|
this.shaIndex[label] = sha256Hex(contents);
|
|
14216
14464
|
this.persistShaIndex();
|
|
14217
14465
|
this.fanoutToAffectedAgents(label);
|
|
@@ -14294,9 +14542,9 @@ class AuthBroker {
|
|
|
14294
14542
|
}
|
|
14295
14543
|
resolveMirrorPathsSafe(agentName) {
|
|
14296
14544
|
const agentsDir = resolveAgentsDir(this.config);
|
|
14297
|
-
const agentDir =
|
|
14298
|
-
const claudeDir =
|
|
14299
|
-
const targetPath =
|
|
14545
|
+
const agentDir = resolve7(agentsDir, agentName);
|
|
14546
|
+
const claudeDir = join4(agentDir, ".claude");
|
|
14547
|
+
const targetPath = join4(claudeDir, ".credentials.json");
|
|
14300
14548
|
const refuse = (component, reason) => {
|
|
14301
14549
|
this.logErr(`fanout ${agentName}: REFUSING mirror — ${component} ${reason} ` + `(symlink-guard #1393; agent-owned tree may be attacker-poisoned)`);
|
|
14302
14550
|
this.audit({
|
|
@@ -14346,14 +14594,14 @@ class AuthBroker {
|
|
|
14346
14594
|
}
|
|
14347
14595
|
mirrorAccountToAgent(label, agentName) {
|
|
14348
14596
|
const credsPath = accountCredentialsPath(label, this.home);
|
|
14349
|
-
if (!
|
|
14597
|
+
if (!existsSync7(credsPath))
|
|
14350
14598
|
return false;
|
|
14351
|
-
const mirrorContent = enrichMirrorContent(
|
|
14599
|
+
const mirrorContent = enrichMirrorContent(readFileSync6(credsPath, "utf-8"));
|
|
14352
14600
|
const safe = this.resolveMirrorPathsSafe(agentName);
|
|
14353
14601
|
if (!safe)
|
|
14354
14602
|
return false;
|
|
14355
14603
|
const { claudeDir, targetPath } = safe;
|
|
14356
|
-
|
|
14604
|
+
mkdirSync4(claudeDir, { recursive: true, mode: 448 });
|
|
14357
14605
|
try {
|
|
14358
14606
|
atomicWriteFileSync(targetPath, mirrorContent, 384);
|
|
14359
14607
|
try {
|
|
@@ -14374,32 +14622,32 @@ class AuthBroker {
|
|
|
14374
14622
|
this.thresholdViolations = this.readJson("threshold-violations.json") ?? {};
|
|
14375
14623
|
}
|
|
14376
14624
|
readJson(name) {
|
|
14377
|
-
const p =
|
|
14378
|
-
if (!
|
|
14625
|
+
const p = join4(this.stateDir, name);
|
|
14626
|
+
if (!existsSync7(p))
|
|
14379
14627
|
return null;
|
|
14380
14628
|
try {
|
|
14381
|
-
return JSON.parse(
|
|
14629
|
+
return JSON.parse(readFileSync6(p, "utf-8"));
|
|
14382
14630
|
} catch {
|
|
14383
14631
|
return null;
|
|
14384
14632
|
}
|
|
14385
14633
|
}
|
|
14386
14634
|
persistQuota() {
|
|
14387
|
-
atomicWriteJsonSync(
|
|
14635
|
+
atomicWriteJsonSync(join4(this.stateDir, "quota.json"), this.quota, 384);
|
|
14388
14636
|
}
|
|
14389
14637
|
persistShaIndex() {
|
|
14390
|
-
atomicWriteJsonSync(
|
|
14638
|
+
atomicWriteJsonSync(join4(this.stateDir, "sha-index.json"), this.shaIndex, 384);
|
|
14391
14639
|
}
|
|
14392
14640
|
persistThresholdViolations() {
|
|
14393
|
-
atomicWriteJsonSync(
|
|
14641
|
+
atomicWriteJsonSync(join4(this.stateDir, "threshold-violations.json"), this.thresholdViolations, 384);
|
|
14394
14642
|
}
|
|
14395
14643
|
assertDriftFree() {
|
|
14396
14644
|
for (const label of Object.keys(this.shaIndex)) {
|
|
14397
14645
|
const p = accountCredentialsPath(label, this.home);
|
|
14398
|
-
if (!
|
|
14646
|
+
if (!existsSync7(p)) {
|
|
14399
14647
|
this.logErr(`DRIFT_DETECTED ${label}: index entry but no on-disk credentials`);
|
|
14400
14648
|
process.exit(1);
|
|
14401
14649
|
}
|
|
14402
|
-
const got = sha256Hex(
|
|
14650
|
+
const got = sha256Hex(readFileSync6(p, "utf-8"));
|
|
14403
14651
|
if (got !== this.shaIndex[label]) {
|
|
14404
14652
|
this.logErr(`DRIFT_DETECTED ${label}: sha256 mismatch (recover with 'switchroom auth add ${label} --replace')`);
|
|
14405
14653
|
process.exit(1);
|
|
@@ -14423,7 +14671,7 @@ class AuthBroker {
|
|
|
14423
14671
|
error: entry.error,
|
|
14424
14672
|
replace: entry.replace
|
|
14425
14673
|
});
|
|
14426
|
-
const auditPath =
|
|
14674
|
+
const auditPath = join4(this.stateDir, "audit.jsonl");
|
|
14427
14675
|
try {
|
|
14428
14676
|
this.rotateAuditIfLarge(auditPath);
|
|
14429
14677
|
const line = row + `
|
|
@@ -14455,7 +14703,7 @@ class AuthBroker {
|
|
|
14455
14703
|
rotateAuditIfLarge(path) {
|
|
14456
14704
|
let size = 0;
|
|
14457
14705
|
try {
|
|
14458
|
-
size =
|
|
14706
|
+
size = statSync5(path).size;
|
|
14459
14707
|
} catch {
|
|
14460
14708
|
return;
|
|
14461
14709
|
}
|
|
@@ -14464,7 +14712,7 @@ class AuthBroker {
|
|
|
14464
14712
|
for (let i = AUDIT_KEEP - 1;i >= 1; i--) {
|
|
14465
14713
|
const src = `${path}.${i}`;
|
|
14466
14714
|
const dst = `${path}.${i + 1}`;
|
|
14467
|
-
if (
|
|
14715
|
+
if (existsSync7(src)) {
|
|
14468
14716
|
try {
|
|
14469
14717
|
renameSync3(src, dst);
|
|
14470
14718
|
} catch {}
|
|
@@ -14558,14 +14806,14 @@ async function main() {
|
|
|
14558
14806
|
const flags = parseFlags(argv);
|
|
14559
14807
|
const operatorUid = flags.operatorUid ?? operatorUidFromEnv();
|
|
14560
14808
|
const configPath = process.env.SWITCHROOM_CONFIG;
|
|
14561
|
-
if (configPath !== undefined && !
|
|
14809
|
+
if (configPath !== undefined && !existsSync8(configPath)) {
|
|
14562
14810
|
process.stderr.write(`auth-broker fatal: SWITCHROOM_CONFIG='${configPath}' does not exist
|
|
14563
14811
|
`);
|
|
14564
14812
|
process.exit(1);
|
|
14565
14813
|
}
|
|
14566
14814
|
if (configPath !== undefined) {
|
|
14567
14815
|
try {
|
|
14568
|
-
|
|
14816
|
+
readFileSync7(configPath, "utf-8");
|
|
14569
14817
|
} catch (err) {
|
|
14570
14818
|
process.stderr.write(`auth-broker fatal: failed to read ${configPath}: ${err.message}
|
|
14571
14819
|
`);
|
|
@@ -4000,7 +4000,7 @@ function decodeResponse(line) {
|
|
|
4000
4000
|
}
|
|
4001
4001
|
return ResponseSchema.parse(parsed);
|
|
4002
4002
|
}
|
|
4003
|
-
var MAX_FRAME_BYTES, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ProbeQuotaRequestSchema, RequestSchema, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema, ResponseSchema;
|
|
4003
|
+
var MAX_FRAME_BYTES, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ListMicrosoftAccountsRequestSchema, ProbeQuotaRequestSchema, RequestSchema, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, MicrosoftAccountStateSchema, ListMicrosoftAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema, ResponseSchema;
|
|
4004
4004
|
var init_protocol = __esm(() => {
|
|
4005
4005
|
init_zod();
|
|
4006
4006
|
MAX_FRAME_BYTES = 64 * 1024;
|
|
@@ -4104,6 +4104,11 @@ var init_protocol = __esm(() => {
|
|
|
4104
4104
|
op: exports_external.literal("list-google-accounts"),
|
|
4105
4105
|
id: exports_external.string().min(1)
|
|
4106
4106
|
});
|
|
4107
|
+
ListMicrosoftAccountsRequestSchema = exports_external.object({
|
|
4108
|
+
v: exports_external.literal(PROTOCOL_VERSION),
|
|
4109
|
+
op: exports_external.literal("list-microsoft-accounts"),
|
|
4110
|
+
id: exports_external.string().min(1)
|
|
4111
|
+
});
|
|
4107
4112
|
ProbeQuotaRequestSchema = exports_external.object({
|
|
4108
4113
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
4109
4114
|
op: exports_external.literal("probe-quota"),
|
|
@@ -4121,6 +4126,7 @@ var init_protocol = __esm(() => {
|
|
|
4121
4126
|
RmAccountRequestSchema,
|
|
4122
4127
|
SetOverrideRequestSchema,
|
|
4123
4128
|
ListGoogleAccountsRequestSchema,
|
|
4129
|
+
ListMicrosoftAccountsRequestSchema,
|
|
4124
4130
|
ProbeQuotaRequestSchema
|
|
4125
4131
|
]);
|
|
4126
4132
|
GetCredentialsDataSchema = exports_external.object({
|
|
@@ -4185,6 +4191,16 @@ var init_protocol = __esm(() => {
|
|
|
4185
4191
|
ListGoogleAccountsDataSchema = exports_external.object({
|
|
4186
4192
|
accounts: exports_external.array(GoogleAccountStateSchema)
|
|
4187
4193
|
});
|
|
4194
|
+
MicrosoftAccountStateSchema = exports_external.object({
|
|
4195
|
+
account: exports_external.string(),
|
|
4196
|
+
expiresAt: exports_external.number(),
|
|
4197
|
+
scope: exports_external.string(),
|
|
4198
|
+
clientId: exports_external.string(),
|
|
4199
|
+
accountType: exports_external.enum(["personal", "work"])
|
|
4200
|
+
});
|
|
4201
|
+
ListMicrosoftAccountsDataSchema = exports_external.object({
|
|
4202
|
+
accounts: exports_external.array(MicrosoftAccountStateSchema)
|
|
4203
|
+
});
|
|
4188
4204
|
ErrorBodySchema = exports_external.object({
|
|
4189
4205
|
code: exports_external.enum([
|
|
4190
4206
|
"FORBIDDEN",
|
|
@@ -4308,6 +4324,14 @@ class AuthBrokerClient {
|
|
|
4308
4324
|
});
|
|
4309
4325
|
return data;
|
|
4310
4326
|
}
|
|
4327
|
+
async listMicrosoftAccounts() {
|
|
4328
|
+
const data = await this.send({
|
|
4329
|
+
v: PROTOCOL_VERSION,
|
|
4330
|
+
id: randomUUID(),
|
|
4331
|
+
op: "list-microsoft-accounts"
|
|
4332
|
+
});
|
|
4333
|
+
return data;
|
|
4334
|
+
}
|
|
4311
4335
|
async probeQuota(accounts, timeoutMs) {
|
|
4312
4336
|
const data = await this.send({
|
|
4313
4337
|
v: PROTOCOL_VERSION,
|