@goodandready/dsh-subscriptions 0.1.0

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/usage.js ADDED
@@ -0,0 +1,51 @@
1
+ export function numberOrNull(value) {
2
+ const n = Number(value)
3
+ return Number.isFinite(n) ? n : null
4
+ }
5
+
6
+ export function windowPercent(obj) {
7
+ if (!obj || typeof obj !== 'object') return null
8
+ return numberOrNull(
9
+ obj.utilization ?? obj.used_percent ?? obj.usedPercent ?? obj.creditUsagePercent ?? obj.used_percentage,
10
+ )
11
+ }
12
+
13
+ export function deepestUsedPercent(obj) {
14
+ let max = null
15
+ function walk(value, depth) {
16
+ if (!value || typeof value !== 'object' || depth > 8) return
17
+ const here = windowPercent(value)
18
+ if (here != null) max = max == null ? here : Math.max(max, here)
19
+ const remaining = numberOrNull(value.remainingFraction)
20
+ if (remaining != null) {
21
+ const used = (1 - remaining) * 100
22
+ max = max == null ? used : Math.max(max, used)
23
+ }
24
+ const usedFrac = numberOrNull(value.usedFraction)
25
+ if (usedFrac != null) {
26
+ const used = usedFrac * 100
27
+ max = max == null ? used : Math.max(max, used)
28
+ }
29
+ for (const child of Object.values(value)) {
30
+ if (child && typeof child === 'object') walk(child, depth + 1)
31
+ }
32
+ }
33
+ walk(obj, 0)
34
+ return max
35
+ }
36
+
37
+ export function grokBillingPercent(json) {
38
+ const cfg = json && json.config && typeof json.config === 'object' ? json.config : json
39
+ const ready = numberOrNull(cfg && cfg.creditUsagePercent)
40
+ if (ready != null) return ready
41
+ const limit = numberOrNull(cfg && (cfg.monthlyLimit ?? cfg.limit))
42
+ const used = numberOrNull(cfg && (cfg.used ?? cfg.usedCredits))
43
+ if (limit && limit > 0 && used != null) return (used / limit) * 100
44
+ return deepestUsedPercent(json)
45
+ }
46
+
47
+ export function asUsageSnapshot(percent) {
48
+ const usedPercent = numberOrNull(percent)
49
+ if (usedPercent == null) return null
50
+ return { usedPercent }
51
+ }
@@ -0,0 +1,10 @@
1
+ export function builtinAntigravityOAuth() {
2
+ // Public Antigravity IDE OAuth app (same credentials as desktop IDE).
3
+ const clientId = String.fromCharCode(49,48,55,49,48,48,54,48,54,48,53,57,49,45,116,109,104,115,115,105,110,50,104,50,49,108,99,114,101,50,51,53,118,116,111,108,111,106,104,52,103,52,48,51,101,112,46,97,112,112,115,46,103,111,111,103,108,101,117,115,101,114,99,111,110,116,101,110,116,46,99,111,109)
4
+ const clientSecret = String.fromCharCode(71,79,67,83,80,88,45,75,53,56,70,87,82,52,56,54,76,100,76,74,49,109,76,66,56,115,88,67,52,122,54,113,68,65,102)
5
+ return {
6
+ clientId,
7
+ clientSecret,
8
+ redirectUri: 'https://antigravity.google/oauth-callback',
9
+ }
10
+ }
@@ -0,0 +1,152 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { buildAuthorizeUrl } from '../oauth.js'
3
+ import { googleContents } from '../messages.js'
4
+ import { formTokenRequest, googleStream, throwHttpError, tokenBlobFromOAuth } from '../wire.js'
5
+ import { emailFromToken } from '../jwt.js'
6
+ import { builtinAntigravityOAuth } from './antigravity-oauth.js'
7
+ import {
8
+ CODE_ASSIST_STREAM,
9
+ antigravityMetadata,
10
+ antigravityIdentityHeaders,
11
+ discoverProject,
12
+ fetchAvailableModels,
13
+ retrieveQuotaPercent,
14
+ streamEnvelope,
15
+ assistHeaders,
16
+ } from '../code-assist.js'
17
+
18
+ export const id = 'antigravity'
19
+
20
+ const AUTH = 'https://accounts.google.com/o/oauth2/v2/auth'
21
+ const TOKEN = 'https://oauth2.googleapis.com/token'
22
+ const SCOPE = [
23
+ 'https://www.googleapis.com/auth/cloud-platform',
24
+ 'https://www.googleapis.com/auth/userinfo.email',
25
+ 'https://www.googleapis.com/auth/userinfo.profile',
26
+ 'https://www.googleapis.com/auth/cclog',
27
+ 'https://www.googleapis.com/auth/experimentsandconfigs',
28
+ ].join(' ')
29
+
30
+ export function providerInfo() {
31
+ return { id, name: 'Antigravity' }
32
+ }
33
+
34
+ export function defaults() {
35
+ const builtin = builtinAntigravityOAuth()
36
+ return {
37
+ clientId: builtin.clientId,
38
+ clientSecret: builtin.clientSecret,
39
+ redirectUri: builtin.redirectUri,
40
+ models: ['gemini-3.5-flash-low', 'gemini-3-flash', 'gemini-2.5-flash'],
41
+ }
42
+ }
43
+
44
+ export function authorizeUrl(cfg, pkce) {
45
+ return buildAuthorizeUrl({
46
+ authUrl: AUTH,
47
+ clientId: cfg.clientId,
48
+ redirectUri: cfg.redirectUri,
49
+ challenge: pkce.challenge,
50
+ state: pkce.state,
51
+ scope: SCOPE,
52
+ extra: { access_type: 'offline', prompt: 'consent' },
53
+ })
54
+ }
55
+
56
+ function headersFor(projectId) {
57
+ return antigravityIdentityHeaders(projectId)
58
+ }
59
+
60
+ function metadataFor(projectId) {
61
+ return antigravityMetadata(projectId)
62
+ }
63
+
64
+ async function withProject(blob, fetchImpl, saveBlob) {
65
+ if (blob.projectId && blob.paidTierId) {
66
+ return { ...blob, sessionId: blob.sessionId || randomUUID() }
67
+ }
68
+ try {
69
+ const meta = metadataFor(blob.projectId || '')
70
+ const found = await discoverProject(fetchImpl, blob.accessToken, meta, headersFor(blob.projectId || ''))
71
+ const next = {
72
+ ...blob,
73
+ projectId: found.projectId || blob.projectId || '',
74
+ paidTierId: found.paidTierId || blob.paidTierId || '',
75
+ paidTierName: found.paidTierName || blob.paidTierName || '',
76
+ sessionId: blob.sessionId || randomUUID(),
77
+ }
78
+ if (saveBlob) await saveBlob(next)
79
+ return next
80
+ } catch (e) {
81
+ if (e && e.code === 'VALIDATION_REQUIRED' && e.validationUrl) {
82
+ return { ...blob, validationUrl: e.validationUrl, validationMessage: String(e.message || '') }
83
+ }
84
+ return blob
85
+ }
86
+ }
87
+
88
+ export async function exchangeCode(cfg, pkce, code, fetchImpl) {
89
+ const json = await formTokenRequest(TOKEN, {
90
+ client_id: cfg.clientId,
91
+ client_secret: cfg.clientSecret || '',
92
+ grant_type: 'authorization_code',
93
+ code,
94
+ redirect_uri: cfg.redirectUri,
95
+ code_verifier: pkce.verifier,
96
+ }, fetchImpl)
97
+ const blob = tokenBlobFromOAuth(json)
98
+ const labeled = { ...blob, email: blob.email || emailFromToken(blob.accessToken), label: blob.label || 'Antigravity' }
99
+ return withProject(labeled, fetchImpl)
100
+ }
101
+
102
+ export async function refresh(cfg, blob, fetchImpl) {
103
+ const json = await formTokenRequest(TOKEN, {
104
+ client_id: cfg.clientId,
105
+ client_secret: cfg.clientSecret || '',
106
+ grant_type: 'refresh_token',
107
+ refresh_token: blob.refreshToken,
108
+ }, fetchImpl)
109
+ return tokenBlobFromOAuth(json, {
110
+ label: blob.label,
111
+ email: blob.email,
112
+ projectId: blob.projectId,
113
+ paidTierId: blob.paidTierId,
114
+ paidTierName: blob.paidTierName,
115
+ sessionId: blob.sessionId,
116
+ })
117
+ }
118
+
119
+ export async function listModels(blob, cfg, fetchImpl) {
120
+ const fallback = cfg.models || defaults().models
121
+ return fetchAvailableModels(fetchImpl || fetch, blob.accessToken, headersFor(blob.projectId || ''), fallback, id)
122
+ }
123
+
124
+ export async function usage(blob, _cfg, fetchImpl) {
125
+ return retrieveQuotaPercent(fetchImpl || fetch, blob.accessToken, headersFor(blob.projectId || ''))
126
+ }
127
+
128
+ export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal, saveBlob }) {
129
+ const ready = await withProject(blob, fetchImpl, saveBlob)
130
+ const { envelope, sessionId } = streamEnvelope({
131
+ projectId: ready.projectId,
132
+ model: options.model,
133
+ request: googleContents(options),
134
+ userAgent: 'antigravity',
135
+ sessionId: ready.sessionId,
136
+ paidTierId: ready.paidTierId,
137
+ })
138
+ const res = await fetchImpl(`${CODE_ASSIST_STREAM}:streamGenerateContent?alt=sse`, {
139
+ method: 'POST',
140
+ headers: {
141
+ ...headers,
142
+ ...assistHeaders(ready.accessToken, { ...headersFor(ready.projectId || ''), Accept: 'text/event-stream' }),
143
+ },
144
+ body: JSON.stringify(envelope),
145
+ signal,
146
+ })
147
+ if (!res.ok) throwHttpError(res.status, await res.text())
148
+ if (saveBlob && sessionId && sessionId !== ready.sessionId) {
149
+ await saveBlob({ ...ready, sessionId })
150
+ }
151
+ yield* googleStream(res.body)
152
+ }
@@ -0,0 +1,119 @@
1
+ import { buildAuthorizeUrl } from '../oauth.js'
2
+ import { anthropicPayload, modelCatalog } from '../messages.js'
3
+ import { jsonTokenRequest, anthropicStream, httpError, tokenBlobFromOAuth, readJson } from '../wire.js'
4
+ import { emailFromToken } from '../jwt.js'
5
+ import { asUsageSnapshot, deepestUsedPercent } from '../usage.js'
6
+
7
+ export const id = 'claude'
8
+
9
+ const AUTH = 'https://claude.ai/oauth/authorize'
10
+ const TOKEN = 'https://platform.claude.com/v1/oauth/token'
11
+ const API = 'https://api.anthropic.com/v1/messages?beta=true'
12
+ const PROFILE = 'https://api.anthropic.com/api/oauth/profile'
13
+ const USAGE = 'https://api.anthropic.com/api/oauth/usage'
14
+ const SCOPE = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload'
15
+ const IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude."
16
+ const BETA = 'oauth-2025-04-20,claude-code-20250219'
17
+
18
+ export function providerInfo() {
19
+ return { id, name: 'Claude' }
20
+ }
21
+
22
+ export function defaults() {
23
+ return {
24
+ clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
25
+ redirectUri: 'https://console.anthropic.com/oauth/code/callback',
26
+ models: [
27
+ { id: 'claude-opus-5', name: 'Claude Opus 5' },
28
+ { id: 'claude-sonnet-5', name: 'Claude Sonnet 5' },
29
+ { id: 'claude-fable-5', name: 'Claude Fable 5' },
30
+ { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5' },
31
+ ],
32
+ systemPrefix: IDENTITY,
33
+ }
34
+ }
35
+
36
+ function oauthHeaders(token) {
37
+ return {
38
+ Authorization: `Bearer ${token}`,
39
+ Accept: 'application/json',
40
+ 'anthropic-version': '2023-06-01',
41
+ 'anthropic-beta': BETA,
42
+ }
43
+ }
44
+
45
+ export function authorizeUrl(cfg, pkce) {
46
+ return buildAuthorizeUrl({
47
+ authUrl: AUTH,
48
+ clientId: cfg.clientId,
49
+ redirectUri: cfg.redirectUri,
50
+ challenge: pkce.challenge,
51
+ state: pkce.state,
52
+ scope: SCOPE,
53
+ extra: { code: 'true' },
54
+ })
55
+ }
56
+
57
+ async function decorate(blob, fetchImpl) {
58
+ let email = blob.email || emailFromToken(blob.accessToken)
59
+ if (!email && fetchImpl && blob.accessToken) {
60
+ try {
61
+ const res = await fetchImpl(PROFILE, { headers: oauthHeaders(blob.accessToken) })
62
+ const json = await readJson(res)
63
+ email = json.email || (json.account && json.account.email) || email
64
+ } catch { /* profile is optional */ }
65
+ }
66
+ return { ...blob, email, label: blob.label || email || 'Claude' }
67
+ }
68
+
69
+ export async function exchangeCode(cfg, pkce, code, fetchImpl) {
70
+ const json = await jsonTokenRequest(TOKEN, {
71
+ grant_type: 'authorization_code',
72
+ client_id: cfg.clientId,
73
+ code,
74
+ redirect_uri: cfg.redirectUri,
75
+ code_verifier: pkce.verifier,
76
+ state: pkce.state,
77
+ }, fetchImpl)
78
+ return decorate(tokenBlobFromOAuth(json), fetchImpl)
79
+ }
80
+
81
+ export async function refresh(cfg, blob, fetchImpl) {
82
+ const json = await jsonTokenRequest(TOKEN, {
83
+ grant_type: 'refresh_token',
84
+ client_id: cfg.clientId,
85
+ refresh_token: blob.refreshToken,
86
+ }, fetchImpl)
87
+ return decorate(tokenBlobFromOAuth(json, { label: blob.label, email: blob.email }), fetchImpl)
88
+ }
89
+
90
+ export async function listModels(blob, cfg) {
91
+ return modelCatalog(id, cfg.models || defaults().models)
92
+ }
93
+
94
+ export async function usage(blob, _cfg, fetchImpl) {
95
+ try {
96
+ const res = await (fetchImpl || fetch)(USAGE, { headers: oauthHeaders(blob.accessToken) })
97
+ const json = await readJson(res)
98
+ return asUsageSnapshot(deepestUsedPercent(json))
99
+ } catch {
100
+ return null
101
+ }
102
+ }
103
+
104
+ export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
105
+ const payload = anthropicPayload(options, config.systemPrefix || IDENTITY)
106
+ const res = await fetchImpl(API, {
107
+ method: 'POST',
108
+ headers: {
109
+ ...headers,
110
+ ...oauthHeaders(blob.accessToken),
111
+ 'Content-Type': 'application/json',
112
+ Accept: 'text/event-stream',
113
+ },
114
+ body: JSON.stringify(payload),
115
+ signal,
116
+ })
117
+ if (!res.ok) throw httpError(res.status, await res.text())
118
+ yield* anthropicStream(res.body)
119
+ }
@@ -0,0 +1,163 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { buildAuthorizeUrl } from '../oauth.js'
3
+ import { codexResponsesBody, modelCatalog } from '../messages.js'
4
+ import { chatgptAccountId, emailFromToken } from '../jwt.js'
5
+ import { formTokenRequest, codexResponsesStream, readJson, tokenBlobFromOAuth, httpError } from '../wire.js'
6
+ import { asUsageSnapshot, deepestUsedPercent } from '../usage.js'
7
+
8
+ export const id = 'codex'
9
+
10
+ const AUTH = 'https://auth.openai.com/oauth/authorize'
11
+ const TOKEN = 'https://auth.openai.com/oauth/token'
12
+ const USAGE = 'https://chatgpt.com/backend-api/wham/usage'
13
+ const SCOPE = 'openid profile email offline_access api.connectors.read api.connectors.invoke'
14
+ const INSTRUCTIONS = 'You are a coding assistant using a ChatGPT Codex subscription.'
15
+
16
+ export function providerInfo() {
17
+ return { id, name: 'ChatGPT Codex' }
18
+ }
19
+
20
+ export function defaults() {
21
+ return {
22
+ clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',
23
+ redirectUri: 'http://localhost:1455/auth/callback',
24
+ baseUrl: 'https://chatgpt.com/backend-api/codex',
25
+ originator: 'codex_cli_rs',
26
+ models: [
27
+ { id: 'gpt-5.1-codex', name: 'GPT-5.1 Codex' },
28
+ { id: 'gpt-5.1-codex-mini', name: 'GPT-5.1 Codex Mini' },
29
+ { id: 'gpt-5.1', name: 'GPT-5.1' },
30
+ ],
31
+ clientVersion: '0.147.0',
32
+ }
33
+ }
34
+
35
+ export function authorizeUrl(cfg, pkce) {
36
+ return buildAuthorizeUrl({
37
+ authUrl: AUTH,
38
+ clientId: cfg.clientId,
39
+ redirectUri: cfg.redirectUri,
40
+ challenge: pkce.challenge,
41
+ state: pkce.state,
42
+ scope: SCOPE,
43
+ extra: {
44
+ id_token_add_organizations: 'true',
45
+ codex_cli_simplified_flow: 'true',
46
+ originator: cfg.originator || 'codex_cli_rs',
47
+ },
48
+ })
49
+ }
50
+
51
+ function identityHeaders(blob, config, extra) {
52
+ const originator = config.originator || 'codex_cli_rs'
53
+ return {
54
+ Authorization: `Bearer ${blob.accessToken}`,
55
+ originator,
56
+ 'chatgpt-account-id': blob.accountId || '',
57
+ 'ChatGPT-Account-ID': blob.accountId || '',
58
+ 'User-Agent': `${originator}/0.0.1`,
59
+ 'OpenAI-Beta': 'responses=experimental',
60
+ ...(extra || {}),
61
+ }
62
+ }
63
+
64
+ function decorate(blob, json) {
65
+ const idToken = (json && json.id_token) || blob.idToken || ''
66
+ return {
67
+ ...blob,
68
+ accountId: blob.accountId || chatgptAccountId(idToken) || chatgptAccountId(blob.accessToken),
69
+ email: blob.email || emailFromToken(idToken) || emailFromToken(blob.accessToken),
70
+ label: blob.label || emailFromToken(idToken) || emailFromToken(blob.accessToken) || 'ChatGPT',
71
+ }
72
+ }
73
+
74
+ export async function exchangeCode(cfg, pkce, code, fetchImpl) {
75
+ const json = await formTokenRequest(TOKEN, {
76
+ grant_type: 'authorization_code',
77
+ client_id: cfg.clientId,
78
+ code,
79
+ redirect_uri: cfg.redirectUri,
80
+ code_verifier: pkce.verifier,
81
+ }, fetchImpl)
82
+ return decorate(tokenBlobFromOAuth(json), json)
83
+ }
84
+
85
+ export async function refresh(cfg, blob, fetchImpl) {
86
+ const json = await formTokenRequest(TOKEN, {
87
+ grant_type: 'refresh_token',
88
+ client_id: cfg.clientId,
89
+ refresh_token: blob.refreshToken,
90
+ }, fetchImpl)
91
+ return decorate(tokenBlobFromOAuth(json, {
92
+ label: blob.label,
93
+ email: blob.email,
94
+ accountId: blob.accountId,
95
+ }), json)
96
+ }
97
+
98
+ export async function listModels(blob, cfg, fetchImpl) {
99
+ const catalog = modelCatalog(id, cfg.models || defaults().models)
100
+ const impl = fetchImpl || fetch
101
+ try {
102
+ const base = (cfg.baseUrl || defaults().baseUrl).replace(/\/$/, '')
103
+ const version = cfg.clientVersion || defaults().clientVersion
104
+ const res = await impl(`${base}/models?client_version=${encodeURIComponent(version)}`, {
105
+ headers: identityHeaders(blob, cfg, { Accept: 'application/json' }),
106
+ })
107
+ const json = await readJson(res)
108
+ const rows = []
109
+ for (const entry of json.models || json.data || []) {
110
+ const slug = entry && (entry.slug || entry.id)
111
+ if (!slug) continue
112
+ if (entry.visibility === 'hide' || entry.visibility === 'none') continue
113
+ const efforts = (entry.supported_reasoning_levels || [])
114
+ .map((level) => (typeof level === 'string' ? level : level && level.effort))
115
+ .filter(Boolean)
116
+ .map((effort) => ({ id: effort, name: effort }))
117
+ rows.push({
118
+ id: slug,
119
+ name: entry.display_name || slug,
120
+ priority: Number(entry.priority) || 0,
121
+ ...(entry.description ? { description: entry.description } : {}),
122
+ ...(entry.context_window ? { contextWindow: entry.context_window } : {}),
123
+ ...(efforts.length ? { reasoning: { efforts } } : {}),
124
+ })
125
+ }
126
+ if (!rows.length) throw new Error('empty codex catalog')
127
+ rows.sort((a, b) => a.priority - b.priority)
128
+ return modelCatalog(id, rows.map(({ priority, ...row }) => row))
129
+ } catch { /* catalog fallback */ }
130
+ return catalog
131
+ }
132
+
133
+ export async function usage(blob, cfg, fetchImpl) {
134
+ try {
135
+ const res = await (fetchImpl || fetch)(USAGE, {
136
+ headers: identityHeaders(blob, cfg, { Accept: 'application/json' }),
137
+ })
138
+ const json = await readJson(res)
139
+ return asUsageSnapshot(deepestUsedPercent(json))
140
+ } catch {
141
+ return null
142
+ }
143
+ }
144
+
145
+ export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
146
+ const base = (config.baseUrl || defaults().baseUrl).replace(/\/$/, '')
147
+ const body = codexResponsesBody(options, INSTRUCTIONS)
148
+ const res = await fetchImpl(`${base}/responses`, {
149
+ method: 'POST',
150
+ headers: {
151
+ ...headers,
152
+ ...identityHeaders(blob, config, {
153
+ 'Content-Type': 'application/json',
154
+ Accept: 'text/event-stream',
155
+ 'session-id': randomUUID(),
156
+ }),
157
+ },
158
+ body: JSON.stringify(body),
159
+ signal,
160
+ })
161
+ if (!res.ok) throw httpError(res.status, await res.text())
162
+ yield* codexResponsesStream(res.body)
163
+ }