@goodandready/dsh-subscriptions 0.6.50 → 0.6.51

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/lib/blob.js CHANGED
@@ -2,6 +2,33 @@ function asString(value) {
2
2
  return value == null ? "" : String(value)
3
3
  }
4
4
 
5
+ export function normalizeExpiresAt(raw) {
6
+ if (raw == null || raw === "") return 0
7
+ if (typeof raw === "number") {
8
+ if (!Number.isFinite(raw) || raw <= 0) return 0
9
+ return raw < 1e11 ? Math.floor(raw * 1000) : Math.floor(raw)
10
+ }
11
+ if (typeof raw === "string") {
12
+ const trimmed = raw.trim()
13
+ if (!trimmed) return 0
14
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
15
+ const num = Number(trimmed)
16
+ if (!Number.isFinite(num) || num <= 0) return 0
17
+ return num < 1e11 ? Math.floor(num * 1000) : Math.floor(num)
18
+ }
19
+ const parsed = Date.parse(trimmed)
20
+ if (Number.isFinite(parsed) && parsed > 0) {
21
+ return parsed
22
+ }
23
+ return 0
24
+ }
25
+ if (raw instanceof Date) {
26
+ const t = raw.getTime()
27
+ return Number.isFinite(t) && t > 0 ? t : 0
28
+ }
29
+ return 0
30
+ }
31
+
5
32
  export function serializeBlob(obj) {
6
33
  if (!obj || typeof obj !== "object") throw new Error("invalid oauth blob")
7
34
  const accessToken = asString(obj.accessToken)
@@ -13,11 +40,14 @@ export function serializeBlob(obj) {
13
40
  ...obj,
14
41
  accessToken,
15
42
  refreshToken,
16
- expiresAt: Number(obj.expiresAt) || 0,
43
+ expiresAt: normalizeExpiresAt(obj.expiresAt),
17
44
  label: asString(obj.label),
18
45
  email: asString(obj.email),
19
46
  accountId: asString(obj.accountId),
20
47
  projectId: asString(obj.projectId),
48
+ ...(obj.clientId ? { clientId: asString(obj.clientId) } : {}),
49
+ ...(obj.clientSecret ? { clientSecret: asString(obj.clientSecret) } : {}),
50
+ ...(obj.idToken ? { idToken: asString(obj.idToken) } : {}),
21
51
  ...(Array.isArray(obj.usage) ? { usage: obj.usage } : {}),
22
52
  ...(obj.usageAt ? { usageAt: Number(obj.usageAt) } : {}),
23
53
  })
@@ -30,11 +60,14 @@ export function parseBlob(text) {
30
60
  ...obj,
31
61
  accessToken: asString(obj.accessToken),
32
62
  refreshToken: asString(obj.refreshToken),
33
- expiresAt: Number(obj.expiresAt) || 0,
63
+ expiresAt: normalizeExpiresAt(obj.expiresAt),
34
64
  label: asString(obj.label),
35
65
  email: asString(obj.email),
36
66
  accountId: asString(obj.accountId),
37
67
  projectId: asString(obj.projectId),
68
+ ...(obj.clientId ? { clientId: asString(obj.clientId) } : {}),
69
+ ...(obj.clientSecret ? { clientSecret: asString(obj.clientSecret) } : {}),
70
+ ...(obj.idToken ? { idToken: asString(obj.idToken) } : {}),
38
71
  ...(Array.isArray(obj.usage) ? { usage: obj.usage } : {}),
39
72
  ...(obj.usageAt ? { usageAt: Number(obj.usageAt) } : {}),
40
73
  }
@@ -2,6 +2,8 @@ import { readFile } from 'node:fs/promises'
2
2
  import { homedir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
  import { readClaudeCredentials } from './vendors/claude-cli.js'
5
+ import { normalizeExpiresAt } from './blob.js'
6
+ import { isGoogleClientId, extractGoogleClientIdFromJwt, emailFromToken } from './jwt.js'
5
7
 
6
8
  // Test seam: a fixture root overrides the real home for the duration of a
7
9
  // public call. Undefined (the default) keeps production behaviour.
@@ -67,17 +69,20 @@ async function discoverLocalCliSessionsInner() {
67
69
  // 3. Antigravity / Gemini CLI
68
70
  const agyPaths = [
69
71
  homeFile('.gemini', 'antigravity-cli', 'antigravity-oauth-token'),
72
+ homeFile('.gemini', 'oauth_creds.json'),
70
73
  homeFile('.cli-proxy-api', 'antigravity.json'),
71
74
  ]
72
75
  for (const p of agyPaths) {
73
76
  const agy = await readJson(p)
74
77
  const tok = agy && agy.token ? agy.token : agy
75
78
  if (tok && (tok.access_token || tok.accessToken)) {
79
+ const idToken = agy?.id_token || agy?.idToken || tok?.id_token || tok?.idToken || ''
80
+ const email = agy?.email || tok?.account || (idToken ? emailFromToken(idToken) : '') || ''
76
81
  detected.antigravity = {
77
82
  provider: 'antigravity',
78
83
  source: 'file',
79
84
  path: p,
80
- email: agy.email || tok.account || '',
85
+ email: email || 'Antigravity User',
81
86
  hasRefreshToken: !!(tok.refresh_token || tok.refreshToken),
82
87
  }
83
88
  break
@@ -331,12 +336,35 @@ export async function loadLocalCliBlob(provider, options = {}) {
331
336
 
332
337
  if (canonical === 'antigravity') {
333
338
  const tok = raw.token || raw
339
+ const idToken = raw.id_token || raw.idToken || tok.id_token || tok.idToken || ''
340
+
341
+ const directClientId = raw.client_id || raw.clientId || tok.client_id || tok.clientId ||
342
+ raw.installed?.client_id || raw.web?.client_id || ''
343
+ let clientId = ''
344
+ if (typeof directClientId === 'string' && isGoogleClientId(directClientId)) {
345
+ clientId = directClientId.trim()
346
+ } else if (idToken) {
347
+ clientId = extractGoogleClientIdFromJwt(idToken)
348
+ }
349
+
350
+ const directSecret = raw.client_secret || raw.clientSecret || tok.client_secret || tok.clientSecret ||
351
+ raw.installed?.client_secret || raw.web?.client_secret || ''
352
+ const clientSecret = typeof directSecret === 'string' ? directSecret.trim() : ''
353
+
354
+ const rawExpiry = tok.expiry ?? tok.expires_at ?? tok.expiresAt ?? tok.expiry_date ?? raw.expiry_date ?? raw.expiry ?? (tok.expires_in ? Date.now() + Number(tok.expires_in) * 1000 : 0)
355
+ const expiresAt = normalizeExpiresAt(rawExpiry)
356
+
357
+ const email = raw.email || tok.account || (idToken ? emailFromToken(idToken) : '') || ''
358
+
334
359
  return {
335
360
  accessToken: tok.access_token || tok.accessToken || '',
336
361
  refreshToken: tok.refresh_token || tok.refreshToken || '',
337
- expiresAt: tok.expiry || tok.expires_at || tok.expiresAt || (Date.now() + 3600 * 1000),
338
- email: raw.email || tok.account || '',
362
+ expiresAt,
363
+ email,
339
364
  projectId: raw.project_id || raw.projectId || tok.project_id || '',
365
+ ...(clientId ? { clientId } : {}),
366
+ ...(clientSecret ? { clientSecret } : {}),
367
+ ...(idToken ? { idToken } : {}),
340
368
  }
341
369
  }
342
370
 
package/lib/jwt.js CHANGED
@@ -26,3 +26,36 @@ export function emailFromToken(token) {
26
26
  if (!payload) return ''
27
27
  return String(payload.email || payload.preferred_username || '')
28
28
  }
29
+
30
+ export function isGoogleClientId(str) {
31
+ if (typeof str !== 'string') return false
32
+ const trimmed = str.trim()
33
+ return /^[0-9]+-[a-zA-Z0-9_-]+\.apps\.googleusercontent\.com$/.test(trimmed)
34
+ }
35
+
36
+ export function extractGoogleClientIdFromJwt(token) {
37
+ const payload = decodeJwtPayload(token)
38
+ if (!payload || typeof payload !== 'object') return ''
39
+
40
+ const azp = typeof payload.azp === 'string' ? payload.azp.trim() : ''
41
+ const rawAud = payload.aud
42
+
43
+ const audiences = Array.isArray(rawAud)
44
+ ? rawAud.filter(a => typeof a === 'string').map(a => a.trim())
45
+ : (typeof rawAud === 'string' && rawAud.trim() ? [rawAud.trim()] : [])
46
+
47
+ if (azp) {
48
+ if (!isGoogleClientId(azp)) return ''
49
+ if (audiences.length > 0 && !audiences.includes(azp)) {
50
+ return ''
51
+ }
52
+ return azp
53
+ }
54
+
55
+ const validAuds = audiences.filter(isGoogleClientId)
56
+ if (validAuds.length === 1 && (audiences.length === 1 || audiences.every(a => a === validAuds[0]))) {
57
+ return validAuds[0]
58
+ }
59
+
60
+ return ''
61
+ }
@@ -109,15 +109,25 @@ export async function exchangeCode(cfg, pkce, code, fetchImpl) {
109
109
  redirect_uri: cfg.redirectUri,
110
110
  code_verifier: pkce.verifier,
111
111
  }, fetchImpl)
112
- const blob = tokenBlobFromOAuth(json)
112
+ const blob = tokenBlobFromOAuth(json, {
113
+ clientId: cfg.clientId,
114
+ ...(cfg.clientSecret ? { clientSecret: cfg.clientSecret } : {}),
115
+ })
113
116
  const labeled = { ...blob, email: blob.email || emailFromToken(blob.accessToken), label: blob.label || 'Antigravity' }
114
117
  return withProject(labeled, fetchImpl)
115
118
  }
116
119
 
117
120
  export async function refresh(cfg, blob, fetchImpl) {
121
+ const clientId = (cfg?.clientId && String(cfg.clientId).trim()) || (blob?.clientId && String(blob.clientId).trim()) || ''
122
+ const clientSecret = (cfg?.clientSecret && String(cfg.clientSecret).trim()) || (blob?.clientSecret && String(blob.clientSecret).trim()) || ''
123
+
124
+ if (!clientId) {
125
+ throw new Error('Google OAuth refresh requires a Client ID. Configure antigravityClientId in plugin settings or re-import credentials with valid OAuth client metadata.')
126
+ }
127
+
118
128
  const json = await formTokenRequest(TOKEN, {
119
- client_id: cfg.clientId,
120
- client_secret: cfg.clientSecret || '',
129
+ client_id: clientId,
130
+ ...(clientSecret ? { client_secret: clientSecret } : {}),
121
131
  grant_type: 'refresh_token',
122
132
  refresh_token: blob.refreshToken,
123
133
  }, fetchImpl)
@@ -128,6 +138,8 @@ export async function refresh(cfg, blob, fetchImpl) {
128
138
  paidTierId: blob.paidTierId,
129
139
  paidTierName: blob.paidTierName,
130
140
  sessionId: blob.sessionId,
141
+ clientId,
142
+ ...(clientSecret ? { clientSecret } : {}),
131
143
  })
132
144
  }
133
145
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.50",
3
+ "version": "0.6.51",
4
4
  "description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
5
5
  "license": "MIT",
6
6
  "type": "module",