@youkno/edge-cli 1.21.290 → 1.21.292

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/lib/auth.js CHANGED
@@ -151,6 +151,38 @@ async function authRefreshToken(cfg, refreshToken) {
151
151
  async function authLogoutRefreshToken(cfg, refreshToken) {
152
152
  await requestTokenEndpoint(cfg, "logout", { refreshToken });
153
153
  }
154
+ /**
155
+ * Turn whatever the console callback handed us into first-party tokens.
156
+ *
157
+ * The console redirects with `?jwt=<firebase id token>`, and parseCallbackQuery reads `jwt` as
158
+ * accessToken -- so the callback never populates `firebaseToken` and we used to store that Firebase
159
+ * token raw. Two consequences, both bad: a Firebase ID token lives exactly one hour and Google
160
+ * decides that, not us; and the callback carries no refresh token, so every refresh path in
161
+ * resolveAccessToken is guarded off and the session cannot recover. The CLI simply died after an
162
+ * hour with "Firebase ID token has expired" and the only cure was logging in again.
163
+ *
164
+ * Exchanging it gets a first-party access token plus a refresh token (30 days by default), which
165
+ * resolveAccessToken already rotates automatically.
166
+ *
167
+ * Falls back to storing the token as-is when the exchange is unavailable -- an older server without
168
+ * /auth/exchange, or one with auth.issueFirstPartyTokens disabled -- so login still works there,
169
+ * with the same one-hour life it always had.
170
+ */
171
+ async function exchangeCallbackToken(cfg, callbackToken) {
172
+ try {
173
+ return await authExchangeFirebaseToken(cfg, callbackToken);
174
+ }
175
+ catch (err) {
176
+ const reason = err instanceof Error ? err.message : String(err);
177
+ process.stdout.write(`Warning: could not exchange for a first-party token (${reason}).\n` +
178
+ `Falling back to the raw callback token, which expires in about an hour and cannot be refreshed.\n`);
179
+ return {
180
+ accessToken: callbackToken,
181
+ accessTokenExpiresInSec: 3600,
182
+ refreshTokenExpiresInSec: 0
183
+ };
184
+ }
185
+ }
154
186
  async function logout(cfg, email) {
155
187
  const db = readDb();
156
188
  const scope = getScope(db, cfg);
@@ -344,14 +376,19 @@ async function login(cfg) {
344
376
  if (parsed.firebaseToken) {
345
377
  tokenResp = await authExchangeFirebaseToken(cfg, parsed.firebaseToken);
346
378
  }
347
- else if (parsed.accessToken) {
379
+ else if (parsed.accessToken && parsed.refreshToken) {
380
+ // Already a first-party pair; nothing to exchange.
348
381
  tokenResp = {
349
382
  accessToken: parsed.accessToken,
350
383
  refreshToken: parsed.refreshToken,
351
384
  accessTokenExpiresInSec: 3600,
352
- refreshTokenExpiresInSec: parsed.refreshToken ? 2_592_000 : 0
385
+ refreshTokenExpiresInSec: 2_592_000
353
386
  };
354
387
  }
388
+ else if (parsed.accessToken) {
389
+ // No refresh token alongside it means this is the console's `?jwt=` Firebase token.
390
+ tokenResp = await exchangeCallbackToken(cfg, parsed.accessToken);
391
+ }
355
392
  if (!tokenResp?.accessToken) {
356
393
  throw new Error("Authentication callback did not include usable token data.");
357
394
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youkno/edge-cli",
3
- "version": "1.21.290",
3
+ "version": "1.21.292",
4
4
  "description": "Cross-platform Edge CLI",
5
5
  "bin": "dist/index.js",
6
6
  "publishConfig": {
package/src/lib/auth.ts CHANGED
@@ -187,6 +187,40 @@ async function authLogoutRefreshToken(cfg: EffectiveConfig, refreshToken: string
187
187
  await requestTokenEndpoint(cfg, "logout", { refreshToken });
188
188
  }
189
189
 
190
+ /**
191
+ * Turn whatever the console callback handed us into first-party tokens.
192
+ *
193
+ * The console redirects with `?jwt=<firebase id token>`, and parseCallbackQuery reads `jwt` as
194
+ * accessToken -- so the callback never populates `firebaseToken` and we used to store that Firebase
195
+ * token raw. Two consequences, both bad: a Firebase ID token lives exactly one hour and Google
196
+ * decides that, not us; and the callback carries no refresh token, so every refresh path in
197
+ * resolveAccessToken is guarded off and the session cannot recover. The CLI simply died after an
198
+ * hour with "Firebase ID token has expired" and the only cure was logging in again.
199
+ *
200
+ * Exchanging it gets a first-party access token plus a refresh token (30 days by default), which
201
+ * resolveAccessToken already rotates automatically.
202
+ *
203
+ * Falls back to storing the token as-is when the exchange is unavailable -- an older server without
204
+ * /auth/exchange, or one with auth.issueFirstPartyTokens disabled -- so login still works there,
205
+ * with the same one-hour life it always had.
206
+ */
207
+ async function exchangeCallbackToken(cfg: EffectiveConfig, callbackToken: string): Promise<TokenResponse> {
208
+ try {
209
+ return await authExchangeFirebaseToken(cfg, callbackToken);
210
+ } catch (err) {
211
+ const reason = err instanceof Error ? err.message : String(err);
212
+ process.stdout.write(
213
+ `Warning: could not exchange for a first-party token (${reason}).\n` +
214
+ `Falling back to the raw callback token, which expires in about an hour and cannot be refreshed.\n`
215
+ );
216
+ return {
217
+ accessToken: callbackToken,
218
+ accessTokenExpiresInSec: 3600,
219
+ refreshTokenExpiresInSec: 0
220
+ };
221
+ }
222
+ }
223
+
190
224
  export async function logout(cfg: EffectiveConfig, email?: string): Promise<string> {
191
225
  const db = readDb();
192
226
  const scope = getScope(db, cfg);
@@ -404,13 +438,17 @@ export async function login(cfg: EffectiveConfig): Promise<string> {
404
438
  let tokenResp: TokenResponse | undefined;
405
439
  if (parsed.firebaseToken) {
406
440
  tokenResp = await authExchangeFirebaseToken(cfg, parsed.firebaseToken);
407
- } else if (parsed.accessToken) {
441
+ } else if (parsed.accessToken && parsed.refreshToken) {
442
+ // Already a first-party pair; nothing to exchange.
408
443
  tokenResp = {
409
444
  accessToken: parsed.accessToken,
410
445
  refreshToken: parsed.refreshToken,
411
446
  accessTokenExpiresInSec: 3600,
412
- refreshTokenExpiresInSec: parsed.refreshToken ? 2_592_000 : 0
447
+ refreshTokenExpiresInSec: 2_592_000
413
448
  };
449
+ } else if (parsed.accessToken) {
450
+ // No refresh token alongside it means this is the console's `?jwt=` Firebase token.
451
+ tokenResp = await exchangeCallbackToken(cfg, parsed.accessToken);
414
452
  }
415
453
 
416
454
  if (!tokenResp?.accessToken) {