@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/client.js ADDED
@@ -0,0 +1,248 @@
1
+ window.__ModuleLoader__.load({
2
+ id: '@goodandready/dsh-subscriptions',
3
+ factory: (require) => {
4
+ var module = { exports: {} }
5
+ var exports = module.exports
6
+ const React = require('react')
7
+
8
+ const CSS =
9
+ '.dsub-wrap{display:flex;flex-direction:column;gap:22px;padding:4px 0;max-width:760px}' +
10
+ '.dsub-block{display:flex;flex-direction:column;gap:10px}' +
11
+ '.dsub-h{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
12
+ '.dsub-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}' +
13
+ '.dsub-card{display:flex;flex-direction:column;gap:8px;padding:10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px}' +
14
+ '.dsub-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}' +
15
+ '.dsub-row input,.dsub-field input,.dsub-row select{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 8px;font-size:13px}' +
16
+ '.dsub-grow{flex:1;min-width:180px}' +
17
+ '.dsub-field{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--dsw-alias-label-secondary)}' +
18
+ '.dsub-mini{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:6px;height:28px;padding:0 8px;cursor:pointer}' +
19
+ '.dsub-save{background:var(--dsw-alias-brand-primary);color:#fff;border:none;border-radius:6px;padding:7px 14px;font-size:13px;cursor:pointer}' +
20
+ '.dsub-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}' +
21
+ '.dsub-bad{font-size:12px;color:var(--dsw-alias-state-error-primary)}' +
22
+ '.dsub-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap}' +
23
+ '.dsub-badge-on{color:var(--dsw-alias-state-success-primary);border-color:currentColor}' +
24
+ '.dsub-badge-warn{color:var(--dsw-alias-state-warning-primary);border-color:currentColor}' +
25
+ '.dsub-link{background:none;border:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;padding:0}' +
26
+ '.dsub-verify{font-size:12px;color:var(--dsw-alias-state-warning-primary)}' +
27
+ '.dsub-verify a{color:var(--dsw-alias-brand-primary)}'
28
+
29
+ const cssId = 'dsh-subscriptions/settings.module.css'
30
+ if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin-css="' + cssId + '"]')) {
31
+ const tag = document.createElement('style')
32
+ tag.textContent = CSS
33
+ tag.setAttribute('data-plugin', 'dsh-subscriptions')
34
+ tag.dataset.pluginCss = cssId
35
+ document.head.appendChild(tag)
36
+ }
37
+
38
+ function badgeFor(account) {
39
+ if (!account || !account.configured) return 'Not connected'
40
+ if (account.validationUrl) return 'Verify account'
41
+ if (account.cooldownUntil && account.cooldownUntil > Date.now()) return 'Cooling down'
42
+ if (account.usagePercent != null && account.usagePercent >= 100) return 'Usage 100%'
43
+ return 'Connected'
44
+ }
45
+
46
+ function SubsSection() {
47
+ const [draft, setDraft] = React.useState(null)
48
+ const [accounts, setAccounts] = React.useState([])
49
+ const [providers, setProviders] = React.useState([])
50
+ const [paste, setPaste] = React.useState({})
51
+ const [saved, setSaved] = React.useState(false)
52
+ const [err, setErr] = React.useState('')
53
+
54
+ const applyPayload = (data) => {
55
+ setDraft(JSON.parse(JSON.stringify((data && data.config) || {})))
56
+ setAccounts((data && data.accounts) || [])
57
+ setProviders((data && data.providers) || [])
58
+ }
59
+
60
+ const reload = () => fetch('/dsh-subscriptions/config', { cache: 'no-store' })
61
+ .then((res) => res.json())
62
+ .then(applyPayload)
63
+
64
+ React.useEffect(() => {
65
+ let alive = true
66
+ reload().catch((e) => { if (alive) setErr(String(e && e.message ? e.message : e)) })
67
+ return () => { alive = false }
68
+ }, [])
69
+
70
+ if (!draft) return React.createElement('div', { className: 'dsub-wrap' }, 'Loading\u2026')
71
+
72
+ const slots = Array.isArray(draft.slots) ? draft.slots : []
73
+ const setSlots = (next) => setDraft((d) => Object.assign({}, d, { slots: next }))
74
+ const accountOf = (provider, index) => accounts.find((a) => a.provider === provider && a.index === index) || {}
75
+
76
+ const save = async () => {
77
+ setErr(''); setSaved(false)
78
+ const res = await fetch('/dsh-subscriptions/config', {
79
+ method: 'PUT',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify(draft),
82
+ })
83
+ const data = await res.json().catch(() => ({}))
84
+ if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
85
+ applyPayload(data)
86
+ setSaved(true); setTimeout(() => setSaved(false), 2000)
87
+ }
88
+
89
+ const connect = async (provider, index) => {
90
+ setErr('')
91
+ const res = await fetch('/dsh-subscriptions/oauth/start?provider=' + encodeURIComponent(provider) + '&index=' + encodeURIComponent(index), { cache: 'no-store' })
92
+ const data = await res.json().catch(() => ({}))
93
+ if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
94
+ if (data.url) window.open(data.url, '_blank', 'noopener')
95
+ }
96
+
97
+ const complete = async (provider, index) => {
98
+ setErr('')
99
+ const key = provider + ':' + index
100
+ const url = paste[key] || ''
101
+ const res = await fetch('/dsh-subscriptions/oauth/complete', {
102
+ method: 'POST',
103
+ headers: { 'Content-Type': 'application/json' },
104
+ body: JSON.stringify({ provider: provider, index: index, url: url }),
105
+ })
106
+ const data = await res.json().catch(() => ({}))
107
+ if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
108
+ setAccounts(data.accounts || [])
109
+ setPaste((p) => Object.assign({}, p, { [key]: '' }))
110
+ }
111
+
112
+ const logout = async (provider, index) => {
113
+ setErr('')
114
+ const res = await fetch('/dsh-subscriptions/logout', {
115
+ method: 'POST',
116
+ headers: { 'Content-Type': 'application/json' },
117
+ body: JSON.stringify({ provider: provider, index: index }),
118
+ })
119
+ const data = await res.json().catch(() => ({}))
120
+ if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
121
+ setAccounts(data.accounts || [])
122
+ }
123
+
124
+ const addSlot = (provider) => {
125
+ const used = slots.filter((s) => s.provider === provider).map((s) => s.index)
126
+ let index = 1
127
+ while (used.indexOf(index) >= 0) index++
128
+ setSlots(slots.concat([{ provider: provider, index: index, label: '' }]))
129
+ }
130
+
131
+ const names = providers.length ? providers : [
132
+ { id: 'codex', name: 'ChatGPT Codex' },
133
+ { id: 'claude', name: 'Claude' },
134
+ { id: 'grok', name: 'Grok' },
135
+ { id: 'antigravity', name: 'Antigravity' },
136
+ ]
137
+
138
+ return React.createElement('div', { className: 'dsub-wrap' },
139
+ React.createElement('div', { className: 'dsub-block' },
140
+ React.createElement('div', { className: 'dsub-h' }, 'Subscriptions'),
141
+ React.createElement('div', { className: 'dsub-sub' },
142
+ 'Log in with a consumer subscription. Tokens stay on the host credentials store. The browser never reads them back. After Connect, if the provider lands on localhost or a vendor page, paste the redirected URL or the code here.'),
143
+ React.createElement('label', { className: 'dsub-row' },
144
+ React.createElement('input', {
145
+ type: 'checkbox',
146
+ checked: !!draft.useWebCallback,
147
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { useWebCallback: e.target.checked })),
148
+ }),
149
+ React.createElement('span', null, 'Use this Web UI origin as OAuth redirect_uri'),
150
+ ),
151
+ React.createElement('div', { className: 'dsub-sub' },
152
+ 'Leave off unless you registered your own OAuth client for this origin. Vendor CLI clients usually require their published redirect URI plus the paste step.'),
153
+ ),
154
+ names.map((prov) => {
155
+ const rows = slots
156
+ .map((slot, i) => ({ slot: slot, i: i }))
157
+ .filter((row) => row.slot.provider === prov.id)
158
+ return React.createElement('div', { className: 'dsub-block', key: prov.id },
159
+ React.createElement('div', { className: 'dsub-h' }, prov.name),
160
+ rows.map((row) => {
161
+ const account = accountOf(row.slot.provider, row.slot.index)
162
+ const key = row.slot.provider + ':' + row.slot.index
163
+ const on = !!account.configured
164
+ return React.createElement('div', { className: 'dsub-card', key: key },
165
+ React.createElement('div', { className: 'dsub-row' },
166
+ React.createElement('input', {
167
+ className: 'dsub-grow',
168
+ value: row.slot.label || '',
169
+ placeholder: account.label || ('Account ' + row.slot.index),
170
+ onChange: (e) => {
171
+ const next = slots.slice()
172
+ next[row.i] = Object.assign({}, next[row.i], { label: e.target.value })
173
+ setSlots(next)
174
+ },
175
+ }),
176
+ React.createElement('span', { className: 'dsub-badge' + (account.validationUrl ? ' dsub-badge-warn' : (on ? ' dsub-badge-on' : '')) }, badgeFor(account)),
177
+ React.createElement('button', {
178
+ type: 'button', className: 'dsub-mini',
179
+ onClick: () => connect(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
180
+ }, on ? 'Reconnect' : 'Connect'),
181
+ React.createElement('button', {
182
+ type: 'button', className: 'dsub-mini',
183
+ disabled: !on,
184
+ onClick: () => logout(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
185
+ }, 'Disconnect'),
186
+ React.createElement('button', {
187
+ type: 'button', className: 'dsub-mini', title: 'Remove slot',
188
+ onClick: () => {
189
+ const slot = row.slot
190
+ Promise.resolve(on ? logout(slot.provider, slot.index) : null)
191
+ .then(() => setSlots(slots.filter((_, k) => k !== row.i)))
192
+ .catch((e) => setErr(String(e.message || e)))
193
+ },
194
+ }, '\u00d7'),
195
+ ),
196
+ React.createElement('div', { className: 'dsub-row' },
197
+ React.createElement('input', {
198
+ className: 'dsub-grow',
199
+ value: paste[key] || '',
200
+ placeholder: 'Paste redirected URL or code',
201
+ onChange: (e) => setPaste((p) => Object.assign({}, p, { [key]: e.target.value })),
202
+ }),
203
+ React.createElement('button', {
204
+ type: 'button', className: 'dsub-mini',
205
+ onClick: () => complete(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
206
+ }, 'Submit code'),
207
+ ),
208
+ account.accountNotice ? React.createElement('div', { className: 'dsub-verify' }, account.accountNotice) : null,
209
+ account.validationUrl ? React.createElement('div', { className: 'dsub-verify' },
210
+ 'Google requires one-time account verification. ',
211
+ React.createElement('a', { href: account.validationUrl, target: '_blank', rel: 'noopener noreferrer' }, 'Open verification link'),
212
+ ' then reconnect.',
213
+ ) : null,
214
+ account.paidTierName ? React.createElement('span', { className: 'dsub-sub' }, 'Plan: ' + account.paidTierName) : null,
215
+ account.ref ? React.createElement('span', { className: 'dsub-sub' }, 'Stored as ' + account.ref) : null,
216
+ )
217
+ }),
218
+ React.createElement('button', {
219
+ type: 'button', className: 'dsub-mini',
220
+ onClick: () => addSlot(prov.id),
221
+ }, '+ Add account'),
222
+ )
223
+ }),
224
+ React.createElement('div', { className: 'dsub-row' },
225
+ React.createElement('button', {
226
+ type: 'button', className: 'dsub-save',
227
+ onClick: () => save().catch((e) => setErr(String(e.message || e))),
228
+ }, 'Save'),
229
+ saved ? React.createElement('span', { className: 'dsub-ok' }, 'Saved') : null,
230
+ err ? React.createElement('span', { className: 'dsub-bad' }, err) : null,
231
+ ),
232
+ )
233
+ }
234
+
235
+ function registerSettings(ctx) {
236
+ ctx.slots.inject('settings.section', () => ctx.slots.register(
237
+ { name: 'settings.section', id: '@goodandready/dsh-subscriptions', order: 28, label: () => 'Subscriptions', inject: () => ({ ctx: ctx }) },
238
+ SubsSection,
239
+ ))
240
+ }
241
+
242
+ exports.inject = ['slots']
243
+ exports.apply = function apply(ctx) {
244
+ registerSettings(ctx)
245
+ }
246
+ return module.exports
247
+ },
248
+ })
@@ -0,0 +1,250 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { httpError, readJson } from './wire.js'
3
+ import { validationFromLoadCodeAssist, validationFromHttpError, noticeFromLoadCodeAssist, validationRequiredError } from './google-validation.js'
4
+ import { asUsageSnapshot, deepestUsedPercent } from './usage.js'
5
+
6
+ export const CODE_ASSIST_PROD = 'https://cloudcode-pa.googleapis.com/v1internal'
7
+ export const CODE_ASSIST_STREAM = 'https://daily-cloudcode-pa.googleapis.com/v1internal'
8
+ export const CODE_ASSIST = CODE_ASSIST_PROD
9
+
10
+ function antigravityPlatform() {
11
+ if (process.platform === 'win32') return 'WINDOWS'
12
+ if (process.platform === 'darwin') return 'MACOS'
13
+ return 'LINUX'
14
+ }
15
+
16
+ export function antigravityMetadata(projectId) {
17
+ const meta = {
18
+ ideType: 'ANTIGRAVITY',
19
+ platform: antigravityPlatform(),
20
+ pluginType: 'GEMINI',
21
+ }
22
+ if (projectId) meta.duetProject = projectId
23
+ return meta
24
+ }
25
+
26
+ export function antigravityIdentityHeaders(projectId) {
27
+ const uaPlat = process.platform === 'darwin' ? 'darwin/amd64' : process.platform === 'win32' ? 'windows/amd64' : 'linux/amd64'
28
+ return {
29
+ 'user-agent': `antigravity/1.18.3 ${uaPlat}`,
30
+ 'X-Goog-Api-Client': 'google-cloud-sdk vscode_cloudshelleditor/0.1',
31
+ 'Client-Metadata': JSON.stringify(antigravityMetadata(projectId)),
32
+ }
33
+ }
34
+
35
+ export function assistHeaders(token, extra) {
36
+ return {
37
+ Authorization: `Bearer ${token}`,
38
+ 'Content-Type': 'application/json',
39
+ Accept: 'application/json',
40
+ ...(extra || {}),
41
+ }
42
+ }
43
+
44
+ export async function postAssist(fetchImpl, method, token, body, extraHeaders) {
45
+ const res = await fetchImpl(`${CODE_ASSIST}:${method}`, {
46
+ method: 'POST',
47
+ headers: assistHeaders(token, extraHeaders),
48
+ body: JSON.stringify(body || {}),
49
+ })
50
+ return readJson(res)
51
+ }
52
+
53
+ function sleep(ms) {
54
+ return new Promise((resolve) => setTimeout(resolve, ms))
55
+ }
56
+
57
+ function projectFrom(obj) {
58
+ if (!obj || typeof obj !== 'object') return ''
59
+ if (typeof obj.cloudaicompanionProject === 'string') return obj.cloudaicompanionProject
60
+ const nested = obj.cloudaicompanionProject
61
+ if (nested && typeof nested === 'object') return String(nested.id || nested.name || '')
62
+ if (obj.response && obj.response.cloudaicompanionProject) {
63
+ const r = obj.response.cloudaicompanionProject
64
+ return typeof r === 'string' ? r : String(r.id || r.name || '')
65
+ }
66
+ return ''
67
+ }
68
+
69
+ export function accountFromLoad(load) {
70
+ const current = load && load.currentTier && typeof load.currentTier === 'object' ? load.currentTier : {}
71
+ const paidTierId = load?.paidTier?.id ? String(load.paidTier.id) : ''
72
+ return {
73
+ projectId: projectFrom(load),
74
+ tierId: String(current.id || paidTierId || 'free-tier'),
75
+ paidTierId,
76
+ paidTierName: load?.paidTier?.name ? String(load.paidTier.name) : '',
77
+ }
78
+ }
79
+
80
+ export async function resolveProjectId(fetchImpl, token, metadata, extraHeaders, currentProjectId) {
81
+ const load = await postAssist(fetchImpl, 'loadCodeAssist', token, { metadata }, extraHeaders)
82
+ const validation = validationFromLoadCodeAssist(load)
83
+ if (validation) throw validationRequiredError(validation)
84
+ const account = accountFromLoad(load)
85
+ if (account.projectId) return { ...account, load }
86
+ if (currentProjectId) return { ...account, projectId: currentProjectId, load }
87
+ return { ...account, projectId: '', load }
88
+ }
89
+
90
+ function pickOnboardTier(load) {
91
+ const current = load?.currentTier
92
+ const paidTierId = load?.paidTier?.id ? String(load.paidTier.id) : ''
93
+ if (paidTierId === 'g1-pro-tier') return 'standard-tier'
94
+ for (const tier of load?.allowedTiers || []) {
95
+ if (tier?.isDefault && tier?.id) return String(tier.id)
96
+ }
97
+ return String(current?.id || 'free-tier')
98
+ }
99
+
100
+ export async function discoverProject(fetchImpl, token, metadata, extraHeaders) {
101
+ const resolved = await resolveProjectId(fetchImpl, token, metadata, extraHeaders, '')
102
+ const { load } = resolved
103
+ const account = accountFromLoad(load)
104
+ if (account.projectId) {
105
+ return {
106
+ projectId: account.projectId,
107
+ tierId: account.paidTierId || account.tierId,
108
+ paidTierId: account.paidTierId,
109
+ paidTierName: account.paidTierName,
110
+ }
111
+ }
112
+ const tierId = pickOnboardTier(load)
113
+ const onboardBody = {
114
+ tierId,
115
+ metadata,
116
+ ...(tierId !== 'free-tier' && tierId !== 'FREE' ? { cloudaicompanionProject: metadata.duetProject } : {}),
117
+ }
118
+ let op = await postAssist(fetchImpl, 'onboardUser', token, onboardBody, extraHeaders).catch(() => null)
119
+ let n = 0
120
+ while (op && op.done === false && op.name && n < 8) {
121
+ await sleep(400)
122
+ try {
123
+ op = await postAssist(fetchImpl, 'loadCodeAssist', token, { metadata }, extraHeaders)
124
+ } catch {
125
+ break
126
+ }
127
+ n += 1
128
+ }
129
+ const projectId = projectFrom(op) || account.projectId
130
+ const after = accountFromLoad(op && op.response ? op.response : load)
131
+ return {
132
+ projectId,
133
+ tierId: after.paidTierId || tierId,
134
+ paidTierId: after.paidTierId,
135
+ paidTierName: after.paidTierName,
136
+ }
137
+ }
138
+
139
+ function catalogFallback(fallbackIds, provider) {
140
+ return (fallbackIds || []).map((row) => {
141
+ if (typeof row === 'string') return { provider, id: row, name: row }
142
+ const id = row && (row.id || row.name)
143
+ if (!id) return null
144
+ return { provider, id, name: row.name || id }
145
+ }).filter(Boolean)
146
+ }
147
+
148
+ function catalogRows(json) {
149
+ const raw = json?.models ?? json?.availableModels ?? json?.data ?? []
150
+ if (Array.isArray(raw)) return raw.map((row) => (typeof row === 'string' ? { id: row } : row))
151
+ if (raw && typeof raw === 'object') {
152
+ return Object.entries(raw).map(([id, row]) => ({ ...(row || {}), id }))
153
+ }
154
+ return []
155
+ }
156
+
157
+ export async function fetchAvailableModels(fetchImpl, token, extraHeaders, fallbackIds, provider) {
158
+ try {
159
+ const json = await postAssist(fetchImpl, 'fetchAvailableModels', token, {}, extraHeaders)
160
+ const out = []
161
+ for (const row of catalogRows(json)) {
162
+ if (!row) continue
163
+ if (typeof row === 'string') {
164
+ if (row && !row.startsWith('chat_')) out.push({ provider, id: row, name: row })
165
+ continue
166
+ }
167
+ if (row.isInternal) continue
168
+ const raw = String(row.id || row.name || row.model || '').replace(/^models\//, '')
169
+ if (!raw || raw.startsWith('chat_')) continue
170
+ const display = row.displayName ?? row.display_name
171
+ if (display === '') continue
172
+ out.push({
173
+ provider,
174
+ id: raw,
175
+ name: display || raw,
176
+ ...(row.recommended ? { description: 'Recommended' } : {}),
177
+ ...(row.maxTokens ? { contextWindow: row.maxTokens } : {}),
178
+ })
179
+ }
180
+ if (out.length) {
181
+ out.sort((a, b) => Number(Boolean(b.description)) - Number(Boolean(a.description)) || a.name.localeCompare(b.name))
182
+ return out
183
+ }
184
+ } catch { /* catalog fallback */ }
185
+ return catalogFallback(fallbackIds, provider)
186
+ }
187
+
188
+ export async function retrieveQuotaPercent(fetchImpl, token, extraHeaders) {
189
+ try {
190
+ const json = await postAssist(fetchImpl, 'retrieveUserQuota', token, {}, extraHeaders)
191
+ return asUsageSnapshot(deepestUsedPercent(json))
192
+ } catch {
193
+ return null
194
+ }
195
+ }
196
+
197
+ const G1_CREDIT_TYPE = 'GOOGLE_ONE_AI'
198
+
199
+ export function streamEnvelope({ projectId, model, request, userAgent, sessionId, paidTierId }) {
200
+ const session = sessionId || randomUUID()
201
+ const body = { ...(request || {}) }
202
+ if (!body.session_id) body.session_id = session
203
+ const envelope = {
204
+ ...(projectId ? { project: projectId } : {}),
205
+ model,
206
+ user_prompt_id: randomUUID(),
207
+ request: body,
208
+ userAgent,
209
+ }
210
+ if (paidTierId === 'g1-pro-tier') envelope.enabled_credit_types = [G1_CREDIT_TYPE]
211
+ return { envelope, sessionId: session }
212
+ }
213
+
214
+ export { httpError }
215
+
216
+ export async function inspectGoogleAccount(fetchImpl, token, { metadata, extraHeaders, projectId }) {
217
+ try {
218
+ const { projectId: resolvedProject, load } = await resolveProjectId(
219
+ fetchImpl,
220
+ token,
221
+ metadata,
222
+ extraHeaders,
223
+ projectId || '',
224
+ )
225
+ const validation = validationFromLoadCodeAssist(load)
226
+ if (validation) return { validation, notice: null, projectId: resolvedProject }
227
+ const notice = noticeFromLoadCodeAssist(load)
228
+ const account = accountFromLoad(load)
229
+ return {
230
+ validation: null,
231
+ notice,
232
+ projectId: resolvedProject,
233
+ paidTierId: account.paidTierId,
234
+ paidTierName: account.paidTierName,
235
+ }
236
+ } catch (err) {
237
+ if (err && err.code === 'VALIDATION_REQUIRED' && err.validationUrl) {
238
+ return {
239
+ validation: {
240
+ message: String(err.message || ''),
241
+ validationUrl: err.validationUrl,
242
+ learnMoreUrl: err.learnMoreUrl || '',
243
+ },
244
+ notice: null,
245
+ projectId: projectId || '',
246
+ }
247
+ }
248
+ return { validation: null, notice: null, projectId: projectId || '' }
249
+ }
250
+ }
@@ -0,0 +1,67 @@
1
+ const TYPE_MAP = {
2
+ string: 'string',
3
+ number: 'number',
4
+ integer: 'integer',
5
+ boolean: 'boolean',
6
+ array: 'array',
7
+ object: 'object',
8
+ }
9
+
10
+ function asType(value) {
11
+ if (Array.isArray(value)) {
12
+ const nullable = value.includes('null')
13
+ const first = value.find((item) => item && item !== 'null')
14
+ return { type: TYPE_MAP[String(first || 'string').toLowerCase()] || 'string', nullable }
15
+ }
16
+ if (typeof value === 'string' && TYPE_MAP[value.toLowerCase()]) {
17
+ return { type: TYPE_MAP[value.toLowerCase()], nullable: false }
18
+ }
19
+ return null
20
+ }
21
+
22
+ export function toGeminiSchema(schema, depth) {
23
+ const level = Number(depth) || 0
24
+ if (level > 12) return { type: 'string' }
25
+ if (!schema || typeof schema !== 'object') return { type: 'string' }
26
+ if (Array.isArray(schema)) return toGeminiSchema(schema[0] || { type: 'string' }, level + 1)
27
+
28
+ const typed = asType(schema.type)
29
+ let type = typed ? typed.type : null
30
+ if (!type) {
31
+ if (schema.properties) type = 'object'
32
+ else if (schema.items) type = 'array'
33
+ else type = 'string'
34
+ }
35
+ const out = { type }
36
+ if (typed && typed.nullable) out.nullable = true
37
+ if (schema.description) out.description = String(schema.description)
38
+ if (type === 'object') {
39
+ const props = schema.properties
40
+ if (props && typeof props === 'object' && !Array.isArray(props)) {
41
+ out.properties = {}
42
+ for (const [key, value] of Object.entries(props)) {
43
+ out.properties[key] = toGeminiSchema(value, level + 1)
44
+ }
45
+ }
46
+ if (Array.isArray(schema.required)) {
47
+ out.required = schema.required.filter((item) => typeof item === 'string')
48
+ }
49
+ }
50
+ if (type === 'array') {
51
+ const items = schema.items
52
+ out.items = toGeminiSchema(Array.isArray(items) ? items[0] : (items || { type: 'string' }), level + 1)
53
+ }
54
+ if (Array.isArray(schema.enum) && schema.enum.length) {
55
+ out.enum = schema.enum.map((item) => String(item))
56
+ }
57
+ return out
58
+ }
59
+
60
+ export function geminiFunctionDeclarations(tools) {
61
+ if (!Array.isArray(tools) || !tools.length) return undefined
62
+ return tools.map((tool) => ({
63
+ name: tool.name,
64
+ description: tool.description || '',
65
+ parameters: toGeminiSchema(tool.parameters || { type: 'object' }),
66
+ }))
67
+ }
@@ -0,0 +1,117 @@
1
+ const CLOUDCODE_DOMAINS = [
2
+ 'cloudcode-pa.googleapis.com',
3
+ 'staging-cloudcode-pa.googleapis.com',
4
+ 'autopush-cloudcode-pa.googleapis.com',
5
+ ]
6
+
7
+ export function isCloudCodeDomain(domain) {
8
+ const sanitized = String(domain || '').replace(/[^a-zA-Z0-9.-]/g, '')
9
+ return CLOUDCODE_DOMAINS.includes(sanitized)
10
+ }
11
+
12
+ export function parseGoogleApiError(bodyText) {
13
+ try {
14
+ const json = JSON.parse(String(bodyText || ''))
15
+ const err = json.error || json
16
+ if (!err || typeof err !== 'object') return null
17
+ return {
18
+ code: err.code,
19
+ message: String(err.message || ''),
20
+ status: String(err.status || ''),
21
+ details: Array.isArray(err.details) ? err.details : [],
22
+ }
23
+ } catch {
24
+ return null
25
+ }
26
+ }
27
+
28
+ function metadataUrl(metadata) {
29
+ if (!metadata || typeof metadata !== 'object') return ''
30
+ return String(
31
+ metadata.validation_url
32
+ || metadata.validation_link
33
+ || metadata.validationUrl
34
+ || '',
35
+ )
36
+ }
37
+
38
+ export function validationFromLoadCodeAssist(load) {
39
+ if (!load) return null
40
+ const tiers = load.ineligibleTiers
41
+ if (Array.isArray(tiers) && tiers.length) {
42
+ const tier = tiers.find((row) => row && (
43
+ row.reasonCode === 'VALIDATION_REQUIRED'
44
+ || row.reason === 'VALIDATION_REQUIRED'
45
+ ) && (row.validationUrl || row.validation_url))
46
+ if (tier) {
47
+ return {
48
+ message: String(tier.reasonMessage || 'Verify your account to continue.'),
49
+ validationUrl: String(tier.validationUrl || tier.validation_url),
50
+ learnMoreUrl: String(tier.learnMoreUrl || tier.learn_more_url || ''),
51
+ }
52
+ }
53
+ }
54
+ return null
55
+ }
56
+
57
+ export function noticeFromLoadCodeAssist(load) {
58
+ if (!load || load.currentTier) return null
59
+ const tiers = load.ineligibleTiers
60
+ if (!Array.isArray(tiers) || !tiers.length) return null
61
+ const tier = tiers[0]
62
+ if (!tier) return null
63
+ const code = String(tier.reasonCode || tier.reason || '')
64
+ const message = String(tier.reasonMessage || '')
65
+ if (!message) return null
66
+ if (code === 'VALIDATION_REQUIRED') return null
67
+ return { code, message }
68
+ }
69
+
70
+ export function validationFromHttpError(status, bodyText) {
71
+ if (status !== 403) return null
72
+ const api = parseGoogleApiError(bodyText)
73
+ if (!api) return null
74
+ const info = api.details.find((d) => d['@type'] === 'type.googleapis.com/google.rpc.ErrorInfo')
75
+ if (!info || info.reason !== 'VALIDATION_REQUIRED' || !isCloudCodeDomain(info.domain)) return null
76
+
77
+ const help = api.details.find((d) => d['@type'] === 'type.googleapis.com/google.rpc.Help')
78
+ let validationUrl = metadataUrl(info.metadata)
79
+ let message = String(info.metadata?.validation_error_message || api.message || 'Verify your account to continue.')
80
+ if (help?.links?.length) {
81
+ const verify = help.links.find((link) => {
82
+ const desc = String(link.description || '').toLowerCase()
83
+ return desc.includes('verify') || !desc.includes('learn more')
84
+ }) || help.links[0]
85
+ validationUrl = validationUrl || String(verify?.url || '')
86
+ message = String(verify?.description || message)
87
+ }
88
+ if (!validationUrl) return null
89
+ let learnMoreUrl = String(info.metadata?.validation_learn_more_url || '')
90
+ if (!learnMoreUrl && help?.links?.length) {
91
+ const more = help.links.find((link) => {
92
+ if (String(link.description || '').toLowerCase().trim() === 'learn more') return true
93
+ try { return new URL(link.url).hostname === 'support.google.com' } catch { return false }
94
+ })
95
+ if (more) learnMoreUrl = String(more.url || '')
96
+ }
97
+ return { message, validationUrl, learnMoreUrl }
98
+ }
99
+
100
+ export function validationRequiredError(details) {
101
+ const err = new Error(`${details.message} Verify at ${details.validationUrl}`)
102
+ err.code = 'VALIDATION_REQUIRED'
103
+ err.validationUrl = details.validationUrl
104
+ if (details.learnMoreUrl) err.learnMoreUrl = details.learnMoreUrl
105
+ return err
106
+ }
107
+
108
+
109
+ export function googleRateLimitMessage(status, bodyText) {
110
+ if (status !== 429) return ''
111
+ const api = parseGoogleApiError(bodyText)
112
+ const msg = String(api?.message || bodyText || '')
113
+ if (/resource has been exhausted|RESOURCE_EXHAUSTED|quota/i.test(msg)) {
114
+ return 'Google Antigravity quota or rate limit reached. Wait a few minutes, check usage in antigravity.google, or upgrade Google AI Pro.'
115
+ }
116
+ return ''
117
+ }