@wspc/cli 0.0.15 → 0.0.16

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/cli.js CHANGED
@@ -1248,6 +1248,49 @@ var ConfigStore = class {
1248
1248
  }
1249
1249
  await fs.writeFile(this.configFile, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
1250
1250
  }
1251
+ /**
1252
+ * Read-modify-write the config under a cross-process file lock. The mutator
1253
+ * runs against a FRESH read taken inside the lock and edits it in place, so
1254
+ * two writers (e.g. token refresh and the consistency-bookmark writeback)
1255
+ * can't clobber each other from stale snapshots — the bug that revoked whole
1256
+ * refresh-token families when several CLI sessions ran concurrently.
1257
+ */
1258
+ async update(mutate) {
1259
+ await this.withLock(async () => {
1260
+ const config = await this.read();
1261
+ mutate(config);
1262
+ await this.write(config);
1263
+ });
1264
+ }
1265
+ async withLock(fn) {
1266
+ await fs.mkdir(this.configDir, { recursive: true, mode: 448 });
1267
+ const lockFile = this.configFile + ".lock";
1268
+ const STALE_MS = 1e4;
1269
+ const RETRY_MS = 25;
1270
+ const MAX_WAIT_MS = 5e3;
1271
+ let waited = 0;
1272
+ for (; ; ) {
1273
+ try {
1274
+ const fh = await fs.open(lockFile, "wx");
1275
+ await fh.close();
1276
+ break;
1277
+ } catch (e) {
1278
+ if (e.code !== "EEXIST") throw e;
1279
+ const age = await fs.stat(lockFile).then((s) => Date.now() - s.mtimeMs).catch(() => Infinity);
1280
+ if (age > STALE_MS || waited >= MAX_WAIT_MS) {
1281
+ await fs.rm(lockFile, { force: true });
1282
+ continue;
1283
+ }
1284
+ await new Promise((r) => setTimeout(r, RETRY_MS));
1285
+ waited += RETRY_MS;
1286
+ }
1287
+ }
1288
+ try {
1289
+ return await fn();
1290
+ } finally {
1291
+ await fs.rm(lockFile, { force: true });
1292
+ }
1293
+ }
1251
1294
  async currentEnv() {
1252
1295
  const c = await this.read();
1253
1296
  const name = c.current_env;
@@ -1293,8 +1336,8 @@ function createConsistencyFetch(opts) {
1293
1336
  let outgoing = request;
1294
1337
  let injectedStoredBookmark = false;
1295
1338
  if (applies && !outgoing.headers.has(HEADER)) {
1296
- const config2 = await opts.store.read();
1297
- const bookmark = config2.envs[opts.envName]?.consistency_bookmark;
1339
+ const config = await opts.store.read();
1340
+ const bookmark = config.envs[opts.envName]?.consistency_bookmark;
1298
1341
  if (bookmark) {
1299
1342
  const headers = new Headers(outgoing.headers);
1300
1343
  headers.set(HEADER, bookmark);
@@ -1314,23 +1357,23 @@ function createConsistencyFetch(opts) {
1314
1357
  const invalidBookmark = shouldCheckInvalidBookmark ? await responseHasInvalidBookmark(response) : false;
1315
1358
  const shouldClearBookmark = invalidBookmark;
1316
1359
  if (!nextBookmark && !shouldClearBookmark) return response;
1317
- const config = await opts.store.read();
1318
- const env = config.envs[opts.envName];
1319
- if (!env) return response;
1320
- if (nextBookmark) {
1321
- env.consistency_bookmark = nextBookmark;
1322
- } else if (shouldClearBookmark) {
1323
- delete env.consistency_bookmark;
1324
- }
1325
- await opts.store.write(config);
1360
+ await opts.store.update((config) => {
1361
+ const env = config.envs[opts.envName];
1362
+ if (!env) return;
1363
+ if (nextBookmark) {
1364
+ env.consistency_bookmark = nextBookmark;
1365
+ } else if (shouldClearBookmark) {
1366
+ delete env.consistency_bookmark;
1367
+ }
1368
+ });
1326
1369
  return response;
1327
1370
  };
1328
1371
  }
1329
1372
 
1330
1373
  // src/version.ts
1331
- var VERSION = "0.0.15";
1332
- var SPEC_SHA = "dac502bc";
1333
- var SPEC_FETCHED_AT = "2026-06-15T16:41:41.264Z";
1374
+ var VERSION = "0.0.16";
1375
+ var SPEC_SHA = "b505a817";
1376
+ var SPEC_FETCHED_AT = "2026-06-16T07:42:43.911Z";
1334
1377
  var API_BASE = "https://api.wspc.ai";
1335
1378
 
1336
1379
  // src/index.ts
@@ -1343,6 +1386,17 @@ var WspcAuthExpiredError = class extends Error {
1343
1386
  };
1344
1387
 
1345
1388
  // src/handwritten/auth/sdk-auth.ts
1389
+ var USER_AGENT = `@wspc/cli/${VERSION}`;
1390
+ async function expiredMessage(res) {
1391
+ try {
1392
+ const body = await res.clone().json();
1393
+ if (!body.error) return void 0;
1394
+ const detail = body.error_description ? `: ${body.error_description}` : "";
1395
+ return `wspc token refresh failed (${body.error}${detail}); re-authenticate via \`wspc login\``;
1396
+ } catch {
1397
+ return void 0;
1398
+ }
1399
+ }
1346
1400
  function createAuthInterceptor(mode) {
1347
1401
  if ("apiKey" in mode) {
1348
1402
  const apiKey = mode.apiKey;
@@ -1350,6 +1404,7 @@ function createAuthInterceptor(mode) {
1350
1404
  return {
1351
1405
  async onRequest(req) {
1352
1406
  req.headers.set("authorization", `Bearer ${apiKey}`);
1407
+ req.headers.set("user-agent", USER_AGENT);
1353
1408
  return req;
1354
1409
  },
1355
1410
  async execute(req) {
@@ -1365,6 +1420,7 @@ function createAuthInterceptor(mode) {
1365
1420
  return {
1366
1421
  async onRequest(req) {
1367
1422
  req.headers.set("authorization", `Bearer ${accessToken}`);
1423
+ req.headers.set("user-agent", USER_AGENT);
1368
1424
  return req;
1369
1425
  },
1370
1426
  async execute(req) {
@@ -1372,7 +1428,10 @@ function createAuthInterceptor(mode) {
1372
1428
  if (first.status !== 401) return first;
1373
1429
  const refreshRes = await fetchImpl(`${mode.baseUrl}/auth/oauth/token`, {
1374
1430
  method: "POST",
1375
- headers: { "content-type": "application/x-www-form-urlencoded" },
1431
+ headers: {
1432
+ "content-type": "application/x-www-form-urlencoded",
1433
+ "user-agent": USER_AGENT
1434
+ },
1376
1435
  body: new URLSearchParams({
1377
1436
  grant_type: "refresh_token",
1378
1437
  refresh_token: refreshToken,
@@ -1380,7 +1439,7 @@ function createAuthInterceptor(mode) {
1380
1439
  })
1381
1440
  });
1382
1441
  if (!refreshRes.ok) {
1383
- throw new WspcAuthExpiredError();
1442
+ throw new WspcAuthExpiredError(await expiredMessage(refreshRes));
1384
1443
  }
1385
1444
  const tokens = await refreshRes.json();
1386
1445
  accessToken = tokens.access_token;
@@ -1450,13 +1509,13 @@ function buildInterceptor(store, resolved, fetchImpl) {
1450
1509
  clientId,
1451
1510
  fetchImpl,
1452
1511
  onTokenRefresh: async ({ accessToken, refreshToken, expiresAt }) => {
1453
- const cfg = await store.read();
1454
- const a = cfg.envs[envName]?.accounts?.[email];
1455
- if (!a) return;
1456
- a.access_token = accessToken;
1457
- a.refresh_token = refreshToken;
1458
- a.access_token_expires_at = expiresAt;
1459
- await store.write(cfg);
1512
+ await store.update((cfg) => {
1513
+ const a = cfg.envs[envName]?.accounts?.[email];
1514
+ if (!a) return;
1515
+ a.access_token = accessToken;
1516
+ a.refresh_token = refreshToken;
1517
+ a.access_token_expires_at = expiresAt;
1518
+ });
1460
1519
  }
1461
1520
  });
1462
1521
  }