switchroom 0.14.39 → 0.14.41

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.
@@ -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 existsSync7, readFileSync as readFileSync6 } from "node:fs";
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 existsSync6,
12155
+ existsSync as existsSync7,
12156
12156
  lstatSync,
12157
- mkdirSync as mkdirSync3,
12158
- readFileSync as readFileSync5,
12157
+ mkdirSync as mkdirSync4,
12158
+ readFileSync as readFileSync6,
12159
12159
  renameSync as renameSync3,
12160
- rmSync as rmSync4,
12161
- statSync as statSync4,
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 dirname2, join as join3, resolve as resolve6 } from "node:path";
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 ?? resolve6(this.homeRoot(), ".switchroom", "state", "auth-broker");
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
- mkdirSync3(this.stateDir, { recursive: true, mode: 448 });
13437
- mkdirSync3(join3(this.stateDir, "refresh-lease"), { recursive: true, mode: 448 });
13438
- mkdirSync3(this.socketRoot, { recursive: true, mode: 493 });
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 = join3(this.stateDir, "healthy");
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 (existsSync6(sock))
13572
+ if (existsSync7(sock))
13489
13573
  unlinkSync(sock);
13490
13574
  } catch {}
13491
13575
  }
13492
13576
  this.listeners.clear();
13493
13577
  try {
13494
- const healthyPath = join3(this.stateDir, "healthy");
13495
- if (existsSync6(healthyPath))
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 (existsSync6(sock))
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 join3(this.socketRoot, name, "sock");
13626
+ return join4(this.socketRoot, name, "sock");
13543
13627
  }
13544
13628
  consumerSocketPath(name) {
13545
- return join3(this.socketRoot, name, "sock");
13629
+ return join4(this.socketRoot, name, "sock");
13546
13630
  }
13547
13631
  operatorSocketPath() {
13548
- return join3(this.socketRoot, "operator", "sock");
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 = dirname2(sockPath);
13579
- if (existsSync6(dir)) {
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
- mkdirSync3(dir, { recursive: true, mode: 448 });
13671
+ mkdirSync4(dir, { recursive: true, mode: 448 });
13588
13672
  }
13589
- if (existsSync6(sockPath)) {
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 = readFileSync5(accountCredentialsPath(label, this.home), "utf-8");
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
- rmSync4(accountDir(label, this.home), { recursive: true, force: true });
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 = join3(this.stateDir, "refresh-lease", label);
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 = readFileSync5(accountCredentialsPath(label, this.home), "utf-8");
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 = resolve6(agentsDir, agentName);
14298
- const claudeDir = join3(agentDir, ".claude");
14299
- const targetPath = join3(claudeDir, ".credentials.json");
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 (!existsSync6(credsPath))
14597
+ if (!existsSync7(credsPath))
14350
14598
  return false;
14351
- const mirrorContent = enrichMirrorContent(readFileSync5(credsPath, "utf-8"));
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
- mkdirSync3(claudeDir, { recursive: true, mode: 448 });
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 = join3(this.stateDir, name);
14378
- if (!existsSync6(p))
14625
+ const p = join4(this.stateDir, name);
14626
+ if (!existsSync7(p))
14379
14627
  return null;
14380
14628
  try {
14381
- return JSON.parse(readFileSync5(p, "utf-8"));
14629
+ return JSON.parse(readFileSync6(p, "utf-8"));
14382
14630
  } catch {
14383
14631
  return null;
14384
14632
  }
14385
14633
  }
14386
14634
  persistQuota() {
14387
- atomicWriteJsonSync(join3(this.stateDir, "quota.json"), this.quota, 384);
14635
+ atomicWriteJsonSync(join4(this.stateDir, "quota.json"), this.quota, 384);
14388
14636
  }
14389
14637
  persistShaIndex() {
14390
- atomicWriteJsonSync(join3(this.stateDir, "sha-index.json"), this.shaIndex, 384);
14638
+ atomicWriteJsonSync(join4(this.stateDir, "sha-index.json"), this.shaIndex, 384);
14391
14639
  }
14392
14640
  persistThresholdViolations() {
14393
- atomicWriteJsonSync(join3(this.stateDir, "threshold-violations.json"), this.thresholdViolations, 384);
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 (!existsSync6(p)) {
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(readFileSync5(p, "utf-8"));
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 = join3(this.stateDir, "audit.jsonl");
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 = statSync4(path).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 (existsSync6(src)) {
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 && !existsSync7(configPath)) {
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
- readFileSync6(configPath, "utf-8");
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,