@aexol/spectral 0.9.41 → 0.9.42

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.
@@ -1 +1 @@
1
- {"version":3,"file":"auth-helper.d.ts","sourceRoot":"","sources":["../src/auth-helper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAOrB,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAkED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,MAAM,CAAC,EAAE,MAAM,EACf,SAAS,GAAE,MAAyB,GACnC,OAAO,CAAC,WAAW,CAAC,CAyDtB;AAMD,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAUjD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CA6CrE"}
1
+ {"version":3,"file":"auth-helper.d.ts","sourceRoot":"","sources":["../src/auth-helper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAOrB,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA8FD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,MAAM,CAAC,EAAE,MAAM,EACf,SAAS,GAAE,MAAyB,GACnC,OAAO,CAAC,WAAW,CAAC,CA8DtB;AAMD,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAUjD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAkDrE"}
@@ -33,6 +33,34 @@ function deriveLandingUrl(backendUrl) {
33
33
  }
34
34
  return DEFAULT_BACKEND_URL;
35
35
  }
36
+ function decodeJwtExp(jwt) {
37
+ const parts = jwt.split(".");
38
+ if (parts.length < 2)
39
+ return null;
40
+ try {
41
+ const payloadJson = Buffer.from(parts[1], "base64url").toString("utf8");
42
+ const payload = JSON.parse(payloadJson);
43
+ return typeof payload.exp === "number" ? payload.exp : null;
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ function isJwtExpired(jwt, skewSec = 60) {
50
+ const exp = decodeJwtExp(jwt);
51
+ if (exp === null)
52
+ return true;
53
+ return Date.now() / 1000 + skewSec >= exp;
54
+ }
55
+ function validateCallbackToken(token) {
56
+ if (!token || token.split(".").length !== 3) {
57
+ return { ok: false, reason: "Authorization callback returned an invalid token format." };
58
+ }
59
+ if (isJwtExpired(token)) {
60
+ return { ok: false, reason: "Authorization callback returned an expired token." };
61
+ }
62
+ return { ok: true };
63
+ }
36
64
  /**
37
65
  * Start a temporary HTTP server to receive the OAuth callback.
38
66
  * Returns the server and a promise that resolves with the JWT token.
@@ -104,6 +132,10 @@ export async function runOAuthFlow(apiUrl, timeoutMs = OAUTH_TIMEOUT_MS) {
104
132
  }
105
133
  });
106
134
  const token = await tokenPromise;
135
+ const validation = validateCallbackToken(token);
136
+ if (!validation.ok) {
137
+ return { ok: false, reason: validation.reason };
138
+ }
107
139
  // Save the token, preserving existing config (especially teamApiKey).
108
140
  const cfg = existingCfg ?? {
109
141
  apiUrl: resolvedApiUrl,
@@ -159,19 +191,25 @@ export async function ensureAuthenticated() {
159
191
  const token = existingCfg?.teamApiKey || existingCfg?.userJwt || "";
160
192
  // If we have a token, verify it still works
161
193
  if (token) {
162
- try {
163
- const client = new AexolMcpClient(apiUrl, token);
164
- await client.listTools();
165
- // Token is still valid
166
- return { ok: true, config: existingCfg };
194
+ // If the stored user JWT is locally known to be expired, skip the network roundtrip
195
+ if (existingCfg?.userJwt && isJwtExpired(existingCfg.userJwt)) {
196
+ // fall through to OAuth
167
197
  }
168
- catch (err) {
169
- if (!isAuthError(err)) {
170
- // Non-auth error (network, server down) — don't re-auth, just report
171
- const msg = err instanceof Error ? err.message : String(err);
172
- return { ok: false, config: existingCfg, reason: `Backend unreachable: ${msg}` };
198
+ else {
199
+ try {
200
+ const client = new AexolMcpClient(apiUrl, token);
201
+ await client.listTools();
202
+ // Token is still valid
203
+ return { ok: true, config: existingCfg };
204
+ }
205
+ catch (err) {
206
+ if (!isAuthError(err)) {
207
+ // Non-auth error (network, server down) — don't re-auth, just report
208
+ const msg = err instanceof Error ? err.message : String(err);
209
+ return { ok: false, config: existingCfg, reason: `Backend unreachable: ${msg}` };
210
+ }
211
+ // Token is invalid/expired — fall through to OAuth
173
212
  }
174
- // Token is invalid/expired — fall through to OAuth
175
213
  }
176
214
  }
177
215
  // No valid token — run OAuth flow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.9.41",
3
+ "version": "0.9.42",
4
4
  "description": "AI coding agent for Aexol with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -75,6 +75,7 @@
75
75
  "@silvia-odwyer/photon-node": "^0.3.3",
76
76
  "better-sqlite3": "^12.9.0",
77
77
  "chalk": "^5.6.2",
78
+ "cheerio": "^1.2.0",
78
79
  "cross-spawn": "^7.0.6",
79
80
  "diff": "^8.0.0",
80
81
  "get-east-asian-width": "^1.3.0",