@wspc/cli 0.0.15 → 0.0.17
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 +153 -41
- 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 +1297 -381
package/dist/cli.js
CHANGED
|
@@ -1177,10 +1177,21 @@ var V1_CRED_KEYS = [
|
|
|
1177
1177
|
"actor",
|
|
1178
1178
|
"agent_label"
|
|
1179
1179
|
];
|
|
1180
|
+
var CONSISTENCY_BOOKMARK_SERVICES = ["auth", "todo", "calendar", "email", "push"];
|
|
1181
|
+
function normalizeConsistencyBookmarks(raw) {
|
|
1182
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
1183
|
+
const out = {};
|
|
1184
|
+
const obj = raw;
|
|
1185
|
+
for (const service of CONSISTENCY_BOOKMARK_SERVICES) {
|
|
1186
|
+
if (typeof obj[service] === "string") out[service] = obj[service];
|
|
1187
|
+
}
|
|
1188
|
+
return Object.keys(out).length ? out : void 0;
|
|
1189
|
+
}
|
|
1180
1190
|
function migrateEnv(raw) {
|
|
1181
1191
|
const api_base = typeof raw.api_base === "string" ? raw.api_base : "";
|
|
1182
1192
|
const env = { api_base, accounts: {} };
|
|
1183
|
-
|
|
1193
|
+
const consistency_bookmarks = normalizeConsistencyBookmarks(raw.consistency_bookmarks);
|
|
1194
|
+
if (consistency_bookmarks) env.consistency_bookmarks = consistency_bookmarks;
|
|
1184
1195
|
if (typeof raw.client_id === "string") env.client_id = raw.client_id;
|
|
1185
1196
|
if (raw.accounts && typeof raw.accounts === "object") {
|
|
1186
1197
|
env.accounts = raw.accounts;
|
|
@@ -1248,6 +1259,49 @@ var ConfigStore = class {
|
|
|
1248
1259
|
}
|
|
1249
1260
|
await fs.writeFile(this.configFile, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
1250
1261
|
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Read-modify-write the config under a cross-process file lock. The mutator
|
|
1264
|
+
* runs against a FRESH read taken inside the lock and edits it in place, so
|
|
1265
|
+
* two writers (e.g. token refresh and the consistency-bookmark writeback)
|
|
1266
|
+
* can't clobber each other from stale snapshots — the bug that revoked whole
|
|
1267
|
+
* refresh-token families when several CLI sessions ran concurrently.
|
|
1268
|
+
*/
|
|
1269
|
+
async update(mutate) {
|
|
1270
|
+
await this.withLock(async () => {
|
|
1271
|
+
const config = await this.read();
|
|
1272
|
+
mutate(config);
|
|
1273
|
+
await this.write(config);
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
async withLock(fn) {
|
|
1277
|
+
await fs.mkdir(this.configDir, { recursive: true, mode: 448 });
|
|
1278
|
+
const lockFile = this.configFile + ".lock";
|
|
1279
|
+
const STALE_MS = 1e4;
|
|
1280
|
+
const RETRY_MS = 25;
|
|
1281
|
+
const MAX_WAIT_MS = 5e3;
|
|
1282
|
+
let waited = 0;
|
|
1283
|
+
for (; ; ) {
|
|
1284
|
+
try {
|
|
1285
|
+
const fh = await fs.open(lockFile, "wx");
|
|
1286
|
+
await fh.close();
|
|
1287
|
+
break;
|
|
1288
|
+
} catch (e) {
|
|
1289
|
+
if (e.code !== "EEXIST") throw e;
|
|
1290
|
+
const age = await fs.stat(lockFile).then((s) => Date.now() - s.mtimeMs).catch(() => Infinity);
|
|
1291
|
+
if (age > STALE_MS || waited >= MAX_WAIT_MS) {
|
|
1292
|
+
await fs.rm(lockFile, { force: true });
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
await new Promise((r) => setTimeout(r, RETRY_MS));
|
|
1296
|
+
waited += RETRY_MS;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
try {
|
|
1300
|
+
return await fn();
|
|
1301
|
+
} finally {
|
|
1302
|
+
await fs.rm(lockFile, { force: true });
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1251
1305
|
async currentEnv() {
|
|
1252
1306
|
const c = await this.read();
|
|
1253
1307
|
const name = c.current_env;
|
|
@@ -1259,8 +1313,22 @@ var ConfigStore = class {
|
|
|
1259
1313
|
};
|
|
1260
1314
|
|
|
1261
1315
|
// src/handwritten/auth/consistency-fetch.ts
|
|
1262
|
-
var HEADER = "x-consistency-bookmark";
|
|
1263
1316
|
var INVALID_BOOKMARK = "INVALID_CONSISTENCY_BOOKMARK";
|
|
1317
|
+
var SERVICE_HEADERS = {
|
|
1318
|
+
auth: "x-cb-auth",
|
|
1319
|
+
todo: "x-cb-todo",
|
|
1320
|
+
calendar: "x-cb-cal",
|
|
1321
|
+
email: "x-cb-email",
|
|
1322
|
+
push: "x-cb-push"
|
|
1323
|
+
};
|
|
1324
|
+
var SERVICE_PREFIXES = [
|
|
1325
|
+
{ service: "auth", prefix: "/auth" },
|
|
1326
|
+
{ service: "todo", prefix: "/todo" },
|
|
1327
|
+
{ service: "calendar", prefix: "/calendar" },
|
|
1328
|
+
{ service: "email", prefix: "/email" },
|
|
1329
|
+
{ service: "push", prefix: "/push" }
|
|
1330
|
+
];
|
|
1331
|
+
var KNOWN_HEADERS = Object.values(SERVICE_HEADERS);
|
|
1264
1332
|
function normalizeBasePath(pathname) {
|
|
1265
1333
|
const trimmed = pathname.replace(/\/+$/, "");
|
|
1266
1334
|
return trimmed === "" ? "/" : trimmed;
|
|
@@ -1270,6 +1338,26 @@ function isUnderApiBase(url, apiBase) {
|
|
|
1270
1338
|
const basePath = normalizeBasePath(base.pathname);
|
|
1271
1339
|
return url.origin === base.origin && (basePath === "/" || url.pathname === basePath || url.pathname.startsWith(`${basePath}/`));
|
|
1272
1340
|
}
|
|
1341
|
+
function pathWithinApiBase(url, apiBase) {
|
|
1342
|
+
const basePath = normalizeBasePath(new URL(apiBase).pathname);
|
|
1343
|
+
if (basePath === "/") return url.pathname;
|
|
1344
|
+
if (url.pathname === basePath) return "/";
|
|
1345
|
+
return url.pathname.slice(basePath.length) || "/";
|
|
1346
|
+
}
|
|
1347
|
+
function pathMatchesPrefix(pathname, prefix) {
|
|
1348
|
+
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
1349
|
+
}
|
|
1350
|
+
function serviceForPath(pathname) {
|
|
1351
|
+
return SERVICE_PREFIXES.find(({ prefix }) => pathMatchesPrefix(pathname, prefix))?.service;
|
|
1352
|
+
}
|
|
1353
|
+
function stripKnownBookmarkHeaders(request, keep) {
|
|
1354
|
+
if (!KNOWN_HEADERS.some((header) => header !== keep && request.headers.has(header))) return request;
|
|
1355
|
+
const headers = new Headers(request.headers);
|
|
1356
|
+
for (const header of KNOWN_HEADERS) {
|
|
1357
|
+
if (header !== keep) headers.delete(header);
|
|
1358
|
+
}
|
|
1359
|
+
return new Request(request, { headers });
|
|
1360
|
+
}
|
|
1273
1361
|
function isJsonContentType(contentType) {
|
|
1274
1362
|
const mediaType = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
|
|
1275
1363
|
return mediaType === "application/json" || mediaType.endsWith("+json");
|
|
@@ -1291,46 +1379,54 @@ function createConsistencyFetch(opts) {
|
|
|
1291
1379
|
const url = new URL(request.url);
|
|
1292
1380
|
const applies = isUnderApiBase(url, opts.apiBase);
|
|
1293
1381
|
let outgoing = request;
|
|
1294
|
-
let
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1382
|
+
let injectedService;
|
|
1383
|
+
const service = applies ? serviceForPath(pathWithinApiBase(url, opts.apiBase)) : void 0;
|
|
1384
|
+
const serviceHeader = service ? SERVICE_HEADERS[service] : void 0;
|
|
1385
|
+
outgoing = stripKnownBookmarkHeaders(outgoing, applies ? serviceHeader : void 0);
|
|
1386
|
+
if (applies && service) {
|
|
1387
|
+
const header = SERVICE_HEADERS[service];
|
|
1388
|
+
if (!outgoing.headers.has(header)) {
|
|
1389
|
+
const config = await opts.store.read();
|
|
1390
|
+
const bookmark = config.envs[opts.envName]?.consistency_bookmarks?.[service];
|
|
1391
|
+
if (bookmark) {
|
|
1392
|
+
const headers = new Headers(outgoing.headers);
|
|
1393
|
+
headers.set(header, bookmark);
|
|
1394
|
+
outgoing = new Request(outgoing, { headers });
|
|
1395
|
+
injectedService = service;
|
|
1396
|
+
}
|
|
1303
1397
|
}
|
|
1304
1398
|
}
|
|
1305
|
-
if (!applies && outgoing.headers.has(HEADER)) {
|
|
1306
|
-
const headers = new Headers(outgoing.headers);
|
|
1307
|
-
headers.delete(HEADER);
|
|
1308
|
-
outgoing = new Request(outgoing, { headers });
|
|
1309
|
-
}
|
|
1310
1399
|
const response = await fetchImpl(outgoing);
|
|
1311
1400
|
if (!applies) return response;
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1401
|
+
const nextBookmarks = Object.entries(SERVICE_HEADERS).flatMap(([serviceName, header]) => {
|
|
1402
|
+
const value = response.headers.get(header);
|
|
1403
|
+
return value ? [[serviceName, value]] : [];
|
|
1404
|
+
});
|
|
1405
|
+
const shouldCheckInvalidBookmark = injectedService !== void 0;
|
|
1314
1406
|
const invalidBookmark = shouldCheckInvalidBookmark ? await responseHasInvalidBookmark(response) : false;
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1407
|
+
if (nextBookmarks.length === 0 && !invalidBookmark) return response;
|
|
1408
|
+
await opts.store.update((config) => {
|
|
1409
|
+
const env = config.envs[opts.envName];
|
|
1410
|
+
if (!env) return;
|
|
1411
|
+
env.consistency_bookmarks ??= {};
|
|
1412
|
+
for (const [serviceName, value] of nextBookmarks) {
|
|
1413
|
+
env.consistency_bookmarks[serviceName] = value;
|
|
1414
|
+
}
|
|
1415
|
+
if (invalidBookmark && injectedService) {
|
|
1416
|
+
delete env.consistency_bookmarks[injectedService];
|
|
1417
|
+
}
|
|
1418
|
+
if (Object.keys(env.consistency_bookmarks).length === 0) {
|
|
1419
|
+
delete env.consistency_bookmarks;
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1326
1422
|
return response;
|
|
1327
1423
|
};
|
|
1328
1424
|
}
|
|
1329
1425
|
|
|
1330
1426
|
// src/version.ts
|
|
1331
|
-
var VERSION = "0.0.
|
|
1332
|
-
var SPEC_SHA = "
|
|
1333
|
-
var SPEC_FETCHED_AT = "2026-06-
|
|
1427
|
+
var VERSION = "0.0.17";
|
|
1428
|
+
var SPEC_SHA = "d0ccc741";
|
|
1429
|
+
var SPEC_FETCHED_AT = "2026-06-16T14:51:59.656Z";
|
|
1334
1430
|
var API_BASE = "https://api.wspc.ai";
|
|
1335
1431
|
|
|
1336
1432
|
// src/index.ts
|
|
@@ -1343,6 +1439,17 @@ var WspcAuthExpiredError = class extends Error {
|
|
|
1343
1439
|
};
|
|
1344
1440
|
|
|
1345
1441
|
// src/handwritten/auth/sdk-auth.ts
|
|
1442
|
+
var USER_AGENT = `@wspc/cli/${VERSION}`;
|
|
1443
|
+
async function expiredMessage(res) {
|
|
1444
|
+
try {
|
|
1445
|
+
const body = await res.clone().json();
|
|
1446
|
+
if (!body.error) return void 0;
|
|
1447
|
+
const detail = body.error_description ? `: ${body.error_description}` : "";
|
|
1448
|
+
return `wspc token refresh failed (${body.error}${detail}); re-authenticate via \`wspc login\``;
|
|
1449
|
+
} catch {
|
|
1450
|
+
return void 0;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1346
1453
|
function createAuthInterceptor(mode) {
|
|
1347
1454
|
if ("apiKey" in mode) {
|
|
1348
1455
|
const apiKey = mode.apiKey;
|
|
@@ -1350,6 +1457,7 @@ function createAuthInterceptor(mode) {
|
|
|
1350
1457
|
return {
|
|
1351
1458
|
async onRequest(req) {
|
|
1352
1459
|
req.headers.set("authorization", `Bearer ${apiKey}`);
|
|
1460
|
+
req.headers.set("user-agent", USER_AGENT);
|
|
1353
1461
|
return req;
|
|
1354
1462
|
},
|
|
1355
1463
|
async execute(req) {
|
|
@@ -1365,6 +1473,7 @@ function createAuthInterceptor(mode) {
|
|
|
1365
1473
|
return {
|
|
1366
1474
|
async onRequest(req) {
|
|
1367
1475
|
req.headers.set("authorization", `Bearer ${accessToken}`);
|
|
1476
|
+
req.headers.set("user-agent", USER_AGENT);
|
|
1368
1477
|
return req;
|
|
1369
1478
|
},
|
|
1370
1479
|
async execute(req) {
|
|
@@ -1372,7 +1481,10 @@ function createAuthInterceptor(mode) {
|
|
|
1372
1481
|
if (first.status !== 401) return first;
|
|
1373
1482
|
const refreshRes = await fetchImpl(`${mode.baseUrl}/auth/oauth/token`, {
|
|
1374
1483
|
method: "POST",
|
|
1375
|
-
headers: {
|
|
1484
|
+
headers: {
|
|
1485
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
1486
|
+
"user-agent": USER_AGENT
|
|
1487
|
+
},
|
|
1376
1488
|
body: new URLSearchParams({
|
|
1377
1489
|
grant_type: "refresh_token",
|
|
1378
1490
|
refresh_token: refreshToken,
|
|
@@ -1380,7 +1492,7 @@ function createAuthInterceptor(mode) {
|
|
|
1380
1492
|
})
|
|
1381
1493
|
});
|
|
1382
1494
|
if (!refreshRes.ok) {
|
|
1383
|
-
throw new WspcAuthExpiredError();
|
|
1495
|
+
throw new WspcAuthExpiredError(await expiredMessage(refreshRes));
|
|
1384
1496
|
}
|
|
1385
1497
|
const tokens = await refreshRes.json();
|
|
1386
1498
|
accessToken = tokens.access_token;
|
|
@@ -1450,13 +1562,13 @@ function buildInterceptor(store, resolved, fetchImpl) {
|
|
|
1450
1562
|
clientId,
|
|
1451
1563
|
fetchImpl,
|
|
1452
1564
|
onTokenRefresh: async ({ accessToken, refreshToken, expiresAt }) => {
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1565
|
+
await store.update((cfg) => {
|
|
1566
|
+
const a = cfg.envs[envName]?.accounts?.[email];
|
|
1567
|
+
if (!a) return;
|
|
1568
|
+
a.access_token = accessToken;
|
|
1569
|
+
a.refresh_token = refreshToken;
|
|
1570
|
+
a.access_token_expires_at = expiresAt;
|
|
1571
|
+
});
|
|
1460
1572
|
}
|
|
1461
1573
|
});
|
|
1462
1574
|
}
|