@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 +82 -23
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +39 -3
- package/dist/index.js +24 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/spec/openapi.json +1273 -357
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
|
|
1297
|
-
const 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
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
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.
|
|
1332
|
-
var SPEC_SHA = "
|
|
1333
|
-
var SPEC_FETCHED_AT = "2026-06-
|
|
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: {
|
|
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
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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
|
}
|