bunnyquery 1.0.6 → 1.0.7

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.
Files changed (2) hide show
  1. package/bunnyquery.js +93 -8
  2. package/package.json +1 -1
package/bunnyquery.js CHANGED
@@ -492,6 +492,50 @@
492
492
  return 'Request failed. Please try again.';
493
493
  }
494
494
 
495
+ // Detect MCP / upstream "your access token is no longer valid" failures
496
+ // so we can transparently re-mint the MCP token from the live skapi
497
+ // session and retry the call once. Patterns observed in the wild:
498
+ // - skapi-js inside the MCP server throws "Token has expired"
499
+ // (surfaces here as part of err.message or response body text)
500
+ // - MCP returns OAuth-style error codes: invalid_token / expired_token
501
+ // - Proxy wraps it with INVALID_REQUEST + 401
502
+ function _isAuthExpiredError(input) {
503
+ if (!input) return false;
504
+ const blobs = [];
505
+ const push = (v) => { if (typeof v === 'string' && v) blobs.push(v); };
506
+ if (typeof input === 'string') push(input);
507
+ else {
508
+ push(input.message);
509
+ push(input.code);
510
+ if (input.error) {
511
+ push(input.error.message);
512
+ push(input.error.code);
513
+ push(input.error.type);
514
+ }
515
+ if (input.body) {
516
+ push(input.body.message);
517
+ if (input.body.error) {
518
+ push(input.body.error.message);
519
+ push(input.body.error.code);
520
+ push(input.body.error.type);
521
+ }
522
+ }
523
+ if (typeof input.status === 'number' && input.status === 401) return true;
524
+ if (typeof input.status_code === 'number' && input.status_code === 401) return true;
525
+ }
526
+ const hay = blobs.join(' | ').toLowerCase();
527
+ if (!hay) return false;
528
+ return (
529
+ hay.includes('token has expired') ||
530
+ hay.includes('token is expired') ||
531
+ hay.includes('expired_token') ||
532
+ hay.includes('invalid_token') ||
533
+ hay.includes('unauthorized') ||
534
+ hay.includes('not authorized') ||
535
+ (hay.includes('invalid_request') && hay.includes('token'))
536
+ );
537
+ }
538
+
495
539
  // Mirror of agent.vue's isErrorResponseBody — detects provider/proxy
496
540
  // error payloads that should render as a red error bubble even when
497
541
  // no exception was thrown.
@@ -1090,12 +1134,24 @@
1090
1134
 
1091
1135
  // ---- history ------------------------------------------------------
1092
1136
  async _loadFirstHistoryPage() {
1137
+ const fetchPage = () => getChatHistory(
1138
+ this.skapi,
1139
+ { service: this.serviceId, owner: this.ownerId, platform: this.platform },
1140
+ { ascending: false }
1141
+ );
1093
1142
  try {
1094
- const res = await getChatHistory(
1095
- this.skapi,
1096
- { service: this.serviceId, owner: this.ownerId, platform: this.platform },
1097
- { ascending: false }
1098
- );
1143
+ let res;
1144
+ try {
1145
+ res = await fetchPage();
1146
+ } catch (err) {
1147
+ if (_isAuthExpiredError(err)) {
1148
+ this.oauth.clearToken();
1149
+ await this.oauth.exchangeSession(this.skapi.session);
1150
+ res = await fetchPage();
1151
+ } else {
1152
+ throw err;
1153
+ }
1154
+ }
1099
1155
  this.startKeyHistory = res && res.startKeyHistory;
1100
1156
  const list = this._filterByClearHorizon((res && res.list) || []);
1101
1157
  // If the clear horizon has filtered the entire first page,
@@ -1374,13 +1430,42 @@ The same pattern applies to other formats: \`\`\`my-data.json, \`\`\`index.html,
1374
1430
  model: this.model || undefined,
1375
1431
  };
1376
1432
 
1377
- const result = this.platform === 'claude'
1378
- ? await buildClaudeRequest(this.skapi, args)
1379
- : await buildOpenAIRequest(this.skapi, args);
1433
+ const argsBuilder = () => (this.platform === 'claude'
1434
+ ? buildClaudeRequest(this.skapi, args)
1435
+ : buildOpenAIRequest(this.skapi, args));
1436
+
1437
+ let result;
1438
+ try {
1439
+ result = await argsBuilder();
1440
+ } catch (err) {
1441
+ if (_isAuthExpiredError(err)) {
1442
+ // The MCP-side skapi session expired. Re-mint a fresh
1443
+ // MCP token bundle from the live host skapi session
1444
+ // (skapi-js auto-refreshes its own tokens), then retry
1445
+ // the call once.
1446
+ try {
1447
+ this.oauth.clearToken();
1448
+ await this.oauth.exchangeSession(this.skapi.session);
1449
+ } catch (reauthErr) {
1450
+ throw reauthErr;
1451
+ }
1452
+ result = await argsBuilder();
1453
+ } else {
1454
+ throw err;
1455
+ }
1456
+ }
1380
1457
 
1381
1458
  // The proxy may resolve with an error-shaped body instead of
1382
1459
  // throwing; surface that as an error bubble with the
1383
1460
  // upstream message rather than dropping it.
1461
+ if (isErrorResponseBody(result) && _isAuthExpiredError(result)) {
1462
+ try {
1463
+ this.oauth.clearToken();
1464
+ await this.oauth.exchangeSession(this.skapi.session);
1465
+ result = await argsBuilder();
1466
+ } catch (_reauthErr) { /* fall through to normal error rendering */ }
1467
+ }
1468
+
1384
1469
  if (isErrorResponseBody(result)) {
1385
1470
  const errMsg = getErrorMessage(result);
1386
1471
  const last = this.messages[this.messages.length - 1];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunnyquery",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "BunnyQuery AI chat client",
5
5
  "main": "bunnyquery.js",
6
6
  "files": [