dsh-grok-provider 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/CHANGELOG.md +15 -0
- package/CONTRIBUTING.md +51 -0
- package/LICENSE +21 -0
- package/README.en.md +200 -0
- package/README.md +200 -0
- package/SECURITY.md +35 -0
- package/dist/client/client.js +215 -0
- package/dist/host/index.mjs +147 -0
- package/dist/internal/account-dashboard.mjs +67 -0
- package/dist/internal/auth-controller.mjs +184 -0
- package/dist/internal/auth-registry.mjs +64 -0
- package/dist/internal/auth-rpc.mjs +118 -0
- package/dist/internal/billing-summary.mjs +105 -0
- package/dist/internal/credential-source.mjs +168 -0
- package/dist/internal/grok-adapter.mjs +160 -0
- package/dist/internal/grok-command-handler.mjs +62 -0
- package/dist/internal/grok-command.mjs +17 -0
- package/dist/internal/grok-transport.mjs +279 -0
- package/dist/internal/model-catalog.mjs +142 -0
- package/dist/internal/official-auth-driver.mjs +40 -0
- package/dist/internal/official-cli-auth.mjs +231 -0
- package/dist/internal/official-cli-verifier.mjs +62 -0
- package/dist/internal/official-credential-loader.mjs +64 -0
- package/dist/internal/provider-runtime.mjs +43 -0
- package/dist/internal/responses-codec.mjs +357 -0
- package/dist/internal/responses-request.mjs +246 -0
- package/dist/internal/responses-sse.mjs +123 -0
- package/docs/01-product-requirements.md +128 -0
- package/docs/02-architecture-options.md +180 -0
- package/docs/03-security-threat-model.md +243 -0
- package/docs/04-harness-contract.md +325 -0
- package/docs/05-test-plan.md +224 -0
- package/docs/06-release-plan.md +182 -0
- package/docs/07-decision-gate.md +76 -0
- package/docs/08-upstream-cli-1.0.5-evidence.md +98 -0
- package/docs/09-implementation-status.md +37 -0
- package/docs/README.md +60 -0
- package/docs/adr/0001-auth-and-transport-route.md +86 -0
- package/docs/adr/0002-v0.1-scope.md +46 -0
- package/docs/adr/0003-dual-authentication.md +77 -0
- package/docs/adr/0004-dynamic-model-catalog.md +34 -0
- package/docs/adr/0005-official-cli-only-authentication.md +36 -0
- package/docs/adr/0006-account-dashboard.md +84 -0
- package/grok-provider.patch.yml +3 -0
- package/package.json +92 -0
- package/types/index.d.ts +9 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
const BASE_URL = "https://cli-chat-proxy.grok.com"
|
|
2
|
+
const MAX_JSON_RESPONSE_BYTES = 256 * 1024
|
|
3
|
+
const MAX_REQUEST_BYTES = 16 * 1024 * 1024
|
|
4
|
+
const DEFAULT_MODEL_TIMEOUT_MS = 30 * 1000
|
|
5
|
+
const DEFAULT_BILLING_TIMEOUT_MS = 15 * 1000
|
|
6
|
+
const DEFAULT_RESPONSE_TIMEOUT_MS = 10 * 60 * 1000
|
|
7
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647
|
|
8
|
+
|
|
9
|
+
export class GrokTransportError extends Error {
|
|
10
|
+
constructor(status) {
|
|
11
|
+
super("The Grok Build transport request failed")
|
|
12
|
+
this.name = "GrokTransportError"
|
|
13
|
+
if (status !== undefined) this.status = status
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createGrokTransport({
|
|
18
|
+
credentialSource,
|
|
19
|
+
fetch,
|
|
20
|
+
attributionHeaders,
|
|
21
|
+
clientIdentifier,
|
|
22
|
+
clientVersion,
|
|
23
|
+
modelTimeoutMs = DEFAULT_MODEL_TIMEOUT_MS,
|
|
24
|
+
billingTimeoutMs = DEFAULT_BILLING_TIMEOUT_MS,
|
|
25
|
+
responseTimeoutMs = DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
26
|
+
}) {
|
|
27
|
+
if (
|
|
28
|
+
!credentialSource ||
|
|
29
|
+
typeof credentialSource.withAccessToken !== "function" ||
|
|
30
|
+
typeof fetch !== "function" ||
|
|
31
|
+
typeof attributionHeaders !== "function" ||
|
|
32
|
+
!isHeaderValue(clientIdentifier) ||
|
|
33
|
+
!isHeaderValue(clientVersion) ||
|
|
34
|
+
!isTimeout(modelTimeoutMs) ||
|
|
35
|
+
!isTimeout(billingTimeoutMs) ||
|
|
36
|
+
!isTimeout(responseTimeoutMs)
|
|
37
|
+
) {
|
|
38
|
+
throw new TypeError("Invalid Grok transport dependencies")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return Object.freeze({
|
|
42
|
+
async getBilling({ signal } = {}) {
|
|
43
|
+
const deadline = createDeadline(signal, billingTimeoutMs)
|
|
44
|
+
try {
|
|
45
|
+
return await credentialSource.withAccessToken(async (accessToken, metadata) => {
|
|
46
|
+
if (!isPlainObject(metadata) || !isHeaderValue(metadata.userId)) throw new GrokTransportError()
|
|
47
|
+
let response
|
|
48
|
+
try {
|
|
49
|
+
const headers = buildHeaders({ accessToken, attributionHeaders, clientIdentifier, clientVersion })
|
|
50
|
+
headers.set("x-userid", metadata.userId)
|
|
51
|
+
response = await fetch(`${BASE_URL}/v1/billing?format=credits`, {
|
|
52
|
+
method: "GET",
|
|
53
|
+
redirect: "error",
|
|
54
|
+
headers,
|
|
55
|
+
signal: deadline.signal,
|
|
56
|
+
})
|
|
57
|
+
validateJsonResponse(response)
|
|
58
|
+
if (response.status !== 200) throw new GrokTransportError(response.status)
|
|
59
|
+
return await readBoundedText(response, MAX_JSON_RESPONSE_BYTES)
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error instanceof GrokTransportError) throw error
|
|
62
|
+
if (signal?.aborted && error?.name === "AbortError") throw error
|
|
63
|
+
throw new GrokTransportError()
|
|
64
|
+
} finally {
|
|
65
|
+
response = undefined
|
|
66
|
+
metadata = undefined
|
|
67
|
+
accessToken = undefined
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
} finally {
|
|
71
|
+
deadline.dispose()
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
async listModels({ signal } = {}) {
|
|
76
|
+
const deadline = createDeadline(signal, modelTimeoutMs)
|
|
77
|
+
try {
|
|
78
|
+
return await credentialSource.withAccessToken(async (accessToken) => {
|
|
79
|
+
let response
|
|
80
|
+
try {
|
|
81
|
+
response = await fetch(`${BASE_URL}/v1/models`, {
|
|
82
|
+
method: "GET",
|
|
83
|
+
redirect: "error",
|
|
84
|
+
headers: buildHeaders({
|
|
85
|
+
accessToken,
|
|
86
|
+
attributionHeaders,
|
|
87
|
+
clientIdentifier,
|
|
88
|
+
clientVersion,
|
|
89
|
+
}),
|
|
90
|
+
signal: deadline.signal,
|
|
91
|
+
})
|
|
92
|
+
validateJsonResponse(response)
|
|
93
|
+
if (response.status !== 200) throw new GrokTransportError(response.status)
|
|
94
|
+
return await readBoundedText(response, MAX_JSON_RESPONSE_BYTES)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error instanceof GrokTransportError) throw error
|
|
97
|
+
if (signal?.aborted && error?.name === "AbortError") throw error
|
|
98
|
+
throw new GrokTransportError()
|
|
99
|
+
} finally {
|
|
100
|
+
response = undefined
|
|
101
|
+
accessToken = undefined
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
} finally {
|
|
105
|
+
deadline.dispose()
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async *streamResponses(request, { signal } = {}) {
|
|
110
|
+
if (!isPlainObject(request) || request.stream !== true || request.store !== false) {
|
|
111
|
+
throw new TypeError("Invalid Grok Responses request")
|
|
112
|
+
}
|
|
113
|
+
let body
|
|
114
|
+
try {
|
|
115
|
+
body = JSON.stringify(request)
|
|
116
|
+
} catch {
|
|
117
|
+
throw new TypeError("Invalid Grok Responses request")
|
|
118
|
+
}
|
|
119
|
+
if (Buffer.byteLength(body, "utf8") > MAX_REQUEST_BYTES) {
|
|
120
|
+
throw new TypeError("Invalid Grok Responses request")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let response
|
|
124
|
+
let source
|
|
125
|
+
const deadline = createDeadline(signal, responseTimeoutMs)
|
|
126
|
+
try {
|
|
127
|
+
response = await credentialSource.withAccessToken(async (accessToken) => {
|
|
128
|
+
const headers = buildHeaders({
|
|
129
|
+
accessToken,
|
|
130
|
+
attributionHeaders,
|
|
131
|
+
clientIdentifier,
|
|
132
|
+
clientVersion,
|
|
133
|
+
})
|
|
134
|
+
headers.set("Content-Type", "application/json")
|
|
135
|
+
return fetch(`${BASE_URL}/v1/responses`, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
redirect: "error",
|
|
138
|
+
headers,
|
|
139
|
+
body,
|
|
140
|
+
signal: deadline.signal,
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
validateEventStreamResponse(response)
|
|
144
|
+
if (response.status !== 200) throw new GrokTransportError(response.status)
|
|
145
|
+
source = response.body
|
|
146
|
+
for await (const chunk of source) {
|
|
147
|
+
if (!(chunk instanceof Uint8Array)) throw new GrokTransportError(response.status)
|
|
148
|
+
yield chunk
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof GrokTransportError) throw error
|
|
152
|
+
if (signal?.aborted && error?.name === "AbortError") throw error
|
|
153
|
+
throw new GrokTransportError()
|
|
154
|
+
} finally {
|
|
155
|
+
deadline.dispose()
|
|
156
|
+
source = undefined
|
|
157
|
+
response = undefined
|
|
158
|
+
body = undefined
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createDeadline(callerSignal, timeoutMs) {
|
|
165
|
+
if (callerSignal !== undefined && (
|
|
166
|
+
callerSignal === null ||
|
|
167
|
+
typeof callerSignal !== "object" ||
|
|
168
|
+
typeof callerSignal.addEventListener !== "function" ||
|
|
169
|
+
typeof callerSignal.removeEventListener !== "function" ||
|
|
170
|
+
typeof callerSignal.aborted !== "boolean"
|
|
171
|
+
)) throw new TypeError("Invalid Grok transport abort signal")
|
|
172
|
+
const controller = new AbortController()
|
|
173
|
+
const abortFromCaller = () => controller.abort(callerSignal.reason)
|
|
174
|
+
if (callerSignal?.aborted) abortFromCaller()
|
|
175
|
+
else callerSignal?.addEventListener("abort", abortFromCaller, { once: true })
|
|
176
|
+
const timer = setTimeout(() => controller.abort(new DOMException("Timed out", "TimeoutError")), timeoutMs)
|
|
177
|
+
return {
|
|
178
|
+
signal: controller.signal,
|
|
179
|
+
dispose() {
|
|
180
|
+
clearTimeout(timer)
|
|
181
|
+
callerSignal?.removeEventListener("abort", abortFromCaller)
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isTimeout(value) {
|
|
187
|
+
return Number.isSafeInteger(value) && value > 0 && value <= MAX_TIMER_DELAY_MS
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function buildHeaders({ accessToken, attributionHeaders, clientIdentifier, clientVersion }) {
|
|
191
|
+
if (!isHeaderValue(accessToken)) throw new GrokTransportError()
|
|
192
|
+
const attribution = attributionHeaders()
|
|
193
|
+
if (!isPlainObject(attribution)) throw new GrokTransportError()
|
|
194
|
+
|
|
195
|
+
const headers = new Headers()
|
|
196
|
+
for (const [name, value] of Object.entries(attribution)) {
|
|
197
|
+
if (!isHeaderValue(name) || !isHeaderValue(value)) throw new GrokTransportError()
|
|
198
|
+
headers.set(name, value)
|
|
199
|
+
}
|
|
200
|
+
headers.set("Accept", "application/json")
|
|
201
|
+
headers.set("Authorization", `Bearer ${accessToken}`)
|
|
202
|
+
headers.set("X-XAI-Token-Auth", "xai-grok-cli")
|
|
203
|
+
headers.set("x-grok-client-version", clientVersion)
|
|
204
|
+
headers.set("x-grok-client-identifier", clientIdentifier)
|
|
205
|
+
return headers
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function validateJsonResponse(response) {
|
|
209
|
+
validateResponseShape(response)
|
|
210
|
+
const contentType = response.headers.get("content-type")
|
|
211
|
+
if (typeof contentType !== "string" || !contentType.toLowerCase().startsWith("application/json")) {
|
|
212
|
+
throw new GrokTransportError(response.status)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function validateEventStreamResponse(response) {
|
|
217
|
+
validateResponseShape(response)
|
|
218
|
+
const contentType = response.headers.get("content-type")
|
|
219
|
+
if (typeof contentType !== "string" || !contentType.toLowerCase().startsWith("text/event-stream")) {
|
|
220
|
+
throw new GrokTransportError(response.status)
|
|
221
|
+
}
|
|
222
|
+
if (!response.body || typeof response.body[Symbol.asyncIterator] !== "function") {
|
|
223
|
+
throw new GrokTransportError(response.status)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function validateResponseShape(response) {
|
|
228
|
+
if (
|
|
229
|
+
response === null ||
|
|
230
|
+
typeof response !== "object" ||
|
|
231
|
+
!Number.isSafeInteger(response.status) ||
|
|
232
|
+
response.status < 100 ||
|
|
233
|
+
response.status > 599 ||
|
|
234
|
+
!response.headers ||
|
|
235
|
+
typeof response.headers.get !== "function"
|
|
236
|
+
) {
|
|
237
|
+
throw new GrokTransportError()
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function readBoundedText(response, maxBytes) {
|
|
242
|
+
if (!response.body || typeof response.body.getReader !== "function") {
|
|
243
|
+
if (typeof response.text !== "function") throw new GrokTransportError(response.status)
|
|
244
|
+
const text = await response.text()
|
|
245
|
+
if (typeof text !== "string" || Buffer.byteLength(text, "utf8") > maxBytes) {
|
|
246
|
+
throw new GrokTransportError(response.status)
|
|
247
|
+
}
|
|
248
|
+
return text
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const reader = response.body.getReader()
|
|
252
|
+
const decoder = new TextDecoder("utf-8", { fatal: true })
|
|
253
|
+
let byteLength = 0
|
|
254
|
+
let text = ""
|
|
255
|
+
try {
|
|
256
|
+
while (true) {
|
|
257
|
+
const { done, value } = await reader.read()
|
|
258
|
+
if (done) break
|
|
259
|
+
if (!(value instanceof Uint8Array)) throw new GrokTransportError(response.status)
|
|
260
|
+
byteLength += value.byteLength
|
|
261
|
+
if (byteLength > maxBytes) throw new GrokTransportError(response.status)
|
|
262
|
+
text += decoder.decode(value, { stream: true })
|
|
263
|
+
}
|
|
264
|
+
text += decoder.decode()
|
|
265
|
+
return text
|
|
266
|
+
} finally {
|
|
267
|
+
reader.releaseLock()
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function isHeaderValue(value) {
|
|
272
|
+
return typeof value === "string" && value.length > 0 && value.length <= 16 * 1024 && !/[\r\n\0]/u.test(value)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isPlainObject(value) {
|
|
276
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false
|
|
277
|
+
const prototype = Object.getPrototypeOf(value)
|
|
278
|
+
return prototype === Object.prototype || prototype === null
|
|
279
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
2
|
+
const MAX_CATALOG_BYTES = 256 * 1024
|
|
3
|
+
const MAX_MODELS = 256
|
|
4
|
+
const MAX_REASONING_EFFORTS = 16
|
|
5
|
+
|
|
6
|
+
export class InvalidModelCatalogError extends Error {
|
|
7
|
+
constructor() {
|
|
8
|
+
super("The Grok model catalog response is invalid or unsupported")
|
|
9
|
+
this.name = "InvalidModelCatalogError"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseModelCatalogResponse(raw, { provider }) {
|
|
14
|
+
if (typeof raw !== "string" || Buffer.byteLength(raw, "utf8") > MAX_CATALOG_BYTES) {
|
|
15
|
+
throw new InvalidModelCatalogError()
|
|
16
|
+
}
|
|
17
|
+
if (typeof provider !== "string" || provider.length === 0) {
|
|
18
|
+
throw new TypeError("A provider id is required")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let value
|
|
22
|
+
try {
|
|
23
|
+
value = JSON.parse(raw)
|
|
24
|
+
} catch {
|
|
25
|
+
throw new InvalidModelCatalogError()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!isPlainObject(value) || value.object !== "list" || !Array.isArray(value.data)) {
|
|
29
|
+
throw new InvalidModelCatalogError()
|
|
30
|
+
}
|
|
31
|
+
if (value.data.length > MAX_MODELS) throw new InvalidModelCatalogError()
|
|
32
|
+
|
|
33
|
+
const seen = new Set()
|
|
34
|
+
return value.data.map((model) => {
|
|
35
|
+
const entry = parseModel(model, provider)
|
|
36
|
+
if (seen.has(entry.resolvedModelInfo.id)) throw new InvalidModelCatalogError()
|
|
37
|
+
seen.add(entry.resolvedModelInfo.id)
|
|
38
|
+
return entry
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseModel(model, provider) {
|
|
43
|
+
if (
|
|
44
|
+
!isPlainObject(model) ||
|
|
45
|
+
typeof model.id !== "string" ||
|
|
46
|
+
!MODEL_ID_PATTERN.test(model.id) ||
|
|
47
|
+
typeof model.name !== "string" ||
|
|
48
|
+
model.name.length === 0 ||
|
|
49
|
+
model.name.length > 128 ||
|
|
50
|
+
(model.description !== undefined && (
|
|
51
|
+
typeof model.description !== "string" ||
|
|
52
|
+
model.description.length > 1024
|
|
53
|
+
)) ||
|
|
54
|
+
!Number.isSafeInteger(model.context_window) ||
|
|
55
|
+
model.context_window <= 0 ||
|
|
56
|
+
model.context_window > 10_000_000 ||
|
|
57
|
+
model.api_backend !== "responses"
|
|
58
|
+
) {
|
|
59
|
+
throw new InvalidModelCatalogError()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const resolvedModelInfo = {
|
|
63
|
+
provider,
|
|
64
|
+
id: model.id,
|
|
65
|
+
name: model.name,
|
|
66
|
+
...(model.description === undefined ? {} : { description: model.description }),
|
|
67
|
+
inputModalities: ["text"],
|
|
68
|
+
context: { contextWindow: model.context_window },
|
|
69
|
+
...parseReasoning(model),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
backend: model.api_backend,
|
|
74
|
+
resolvedModelInfo,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseReasoning(model) {
|
|
79
|
+
if (model.supports_reasoning_effort !== true) return {}
|
|
80
|
+
if (!Array.isArray(model.reasoning_efforts)) throw new InvalidModelCatalogError()
|
|
81
|
+
if (
|
|
82
|
+
model.reasoning_efforts.length === 0 ||
|
|
83
|
+
model.reasoning_efforts.length > MAX_REASONING_EFFORTS
|
|
84
|
+
) {
|
|
85
|
+
throw new InvalidModelCatalogError()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const seen = new Set()
|
|
89
|
+
let defaultEffort
|
|
90
|
+
const efforts = model.reasoning_efforts.map((effort) => {
|
|
91
|
+
if (
|
|
92
|
+
!isPlainObject(effort) ||
|
|
93
|
+
typeof effort.id !== "string" ||
|
|
94
|
+
!MODEL_ID_PATTERN.test(effort.id) ||
|
|
95
|
+
effort.value !== effort.id ||
|
|
96
|
+
typeof effort.label !== "string" ||
|
|
97
|
+
effort.label.length === 0 ||
|
|
98
|
+
effort.label.length > 128 ||
|
|
99
|
+
(effort.description !== undefined && (
|
|
100
|
+
typeof effort.description !== "string" ||
|
|
101
|
+
effort.description.length > 512
|
|
102
|
+
)) ||
|
|
103
|
+
typeof effort.default !== "boolean" ||
|
|
104
|
+
seen.has(effort.id)
|
|
105
|
+
) {
|
|
106
|
+
throw new InvalidModelCatalogError()
|
|
107
|
+
}
|
|
108
|
+
seen.add(effort.id)
|
|
109
|
+
if (effort.default) {
|
|
110
|
+
if (defaultEffort !== undefined) throw new InvalidModelCatalogError()
|
|
111
|
+
defaultEffort = effort.id
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
id: effort.id,
|
|
115
|
+
name: effort.label,
|
|
116
|
+
...(effort.description === undefined ? {} : { description: effort.description }),
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
if (model.reasoning_effort !== undefined) {
|
|
121
|
+
if (typeof model.reasoning_effort !== "string" || !seen.has(model.reasoning_effort)) {
|
|
122
|
+
throw new InvalidModelCatalogError()
|
|
123
|
+
}
|
|
124
|
+
if (defaultEffort !== undefined && defaultEffort !== model.reasoning_effort) {
|
|
125
|
+
throw new InvalidModelCatalogError()
|
|
126
|
+
}
|
|
127
|
+
defaultEffort = model.reasoning_effort
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
reasoning: {
|
|
132
|
+
efforts,
|
|
133
|
+
...(defaultEffort === undefined ? {} : { defaultEffort }),
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isPlainObject(value) {
|
|
139
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false
|
|
140
|
+
const prototype = Object.getPrototypeOf(value)
|
|
141
|
+
return prototype === Object.prototype || prototype === null
|
|
142
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const OUTCOMES = new Set(["succeeded", "cancelled"])
|
|
2
|
+
|
|
3
|
+
export function createOfficialAuthDriver({ officialAuth, credentialSource }) {
|
|
4
|
+
if (
|
|
5
|
+
!officialAuth ||
|
|
6
|
+
typeof officialAuth.login !== "function" ||
|
|
7
|
+
typeof officialAuth.logout !== "function" ||
|
|
8
|
+
!credentialSource ||
|
|
9
|
+
typeof credentialSource.withAccessToken !== "function"
|
|
10
|
+
) throw new TypeError("Invalid official Grok authentication driver dependencies")
|
|
11
|
+
|
|
12
|
+
return Object.freeze({
|
|
13
|
+
async begin({ signal } = {}) {
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
completion: (async () => {
|
|
16
|
+
const outcome = await officialAuth.login({ signal })
|
|
17
|
+
validateOutcome(outcome)
|
|
18
|
+
if (outcome.kind !== "succeeded") return outcome
|
|
19
|
+
await credentialSource.withAccessToken(async () => undefined)
|
|
20
|
+
return outcome
|
|
21
|
+
})(),
|
|
22
|
+
})
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async logout({ signal } = {}) {
|
|
26
|
+
const outcome = await officialAuth.logout({ signal })
|
|
27
|
+
validateOutcome(outcome)
|
|
28
|
+
return outcome
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validateOutcome(value) {
|
|
34
|
+
if (
|
|
35
|
+
value === null ||
|
|
36
|
+
typeof value !== "object" ||
|
|
37
|
+
!OUTCOMES.has(value.kind) ||
|
|
38
|
+
Object.keys(value).length !== 1
|
|
39
|
+
) throw new TypeError("Invalid official Grok authentication outcome")
|
|
40
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
const EXPECTED_VERSION = "grok 1.0.5 (5115b46bc909)"
|
|
4
|
+
const GRACE_MS = 5_000
|
|
5
|
+
const DEFAULT_VERSION_TIMEOUT_MS = 10 * 1000
|
|
6
|
+
const DEFAULT_LOGIN_TIMEOUT_MS = 5 * 60 * 1000
|
|
7
|
+
const DEFAULT_LOGOUT_TIMEOUT_MS = 2 * 60 * 1000
|
|
8
|
+
const DEFAULT_REFRESH_TIMEOUT_MS = 30 * 1000
|
|
9
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647
|
|
10
|
+
|
|
11
|
+
export class OfficialCliAuthError extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super("The official Grok CLI login could not be completed")
|
|
14
|
+
this.name = "OfficialCliAuthError"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createOfficialCliAuth({
|
|
19
|
+
subprocess,
|
|
20
|
+
platform,
|
|
21
|
+
homeDir,
|
|
22
|
+
verifyExecutable,
|
|
23
|
+
versionTimeoutMs = DEFAULT_VERSION_TIMEOUT_MS,
|
|
24
|
+
loginTimeoutMs = DEFAULT_LOGIN_TIMEOUT_MS,
|
|
25
|
+
logoutTimeoutMs = DEFAULT_LOGOUT_TIMEOUT_MS,
|
|
26
|
+
refreshTimeoutMs = DEFAULT_REFRESH_TIMEOUT_MS,
|
|
27
|
+
}) {
|
|
28
|
+
if (
|
|
29
|
+
!subprocess ||
|
|
30
|
+
typeof subprocess.resolveExecutable !== "function" ||
|
|
31
|
+
typeof subprocess.spawn !== "function" ||
|
|
32
|
+
(platform !== "darwin" && platform !== "win32") ||
|
|
33
|
+
typeof homeDir !== "string" ||
|
|
34
|
+
homeDir.length === 0 ||
|
|
35
|
+
typeof verifyExecutable !== "function" ||
|
|
36
|
+
!isTimeout(versionTimeoutMs) ||
|
|
37
|
+
!isTimeout(loginTimeoutMs) ||
|
|
38
|
+
!isTimeout(logoutTimeoutMs) ||
|
|
39
|
+
!isTimeout(refreshTimeoutMs)
|
|
40
|
+
) {
|
|
41
|
+
throw new TypeError("Invalid official Grok CLI auth dependencies")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const pathApi = platform === "win32" ? path.win32 : path.posix
|
|
45
|
+
const grokHome = pathApi.join(homeDir, ".grok")
|
|
46
|
+
const candidate = pathApi.join(grokHome, "bin", platform === "win32" ? "grok.exe" : "grok")
|
|
47
|
+
const cliEnvironment = buildCliEnvironment(platform, homeDir)
|
|
48
|
+
|
|
49
|
+
const runAction = async (argvTail, callerSignal, timeoutMs) => {
|
|
50
|
+
let preparationDeadline = createDeadline(callerSignal, versionTimeoutMs)
|
|
51
|
+
let actionDeadline
|
|
52
|
+
let resolved
|
|
53
|
+
try {
|
|
54
|
+
resolved = await subprocess.resolveExecutable(candidate, {}, preparationDeadline.signal)
|
|
55
|
+
if (typeof resolved !== "string" || !pathApi.isAbsolute(resolved)) throw new OfficialCliAuthError()
|
|
56
|
+
await verifyExecutable({ candidate, resolved, grokHome, platform })
|
|
57
|
+
|
|
58
|
+
const version = await runCollected(subprocess, {
|
|
59
|
+
argv: [resolved, "--version"],
|
|
60
|
+
cwd: grokHome,
|
|
61
|
+
maxBytes: 4 * 1024,
|
|
62
|
+
signal: preparationDeadline.signal,
|
|
63
|
+
env: cliEnvironment,
|
|
64
|
+
})
|
|
65
|
+
if (version.stdout.trim() !== EXPECTED_VERSION || version.stderr.length !== 0) {
|
|
66
|
+
throw new OfficialCliAuthError()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
preparationDeadline.dispose()
|
|
70
|
+
preparationDeadline = undefined
|
|
71
|
+
actionDeadline = createDeadline(callerSignal, timeoutMs)
|
|
72
|
+
await runCollected(subprocess, {
|
|
73
|
+
argv: [resolved, ...argvTail],
|
|
74
|
+
cwd: grokHome,
|
|
75
|
+
maxBytes: 64 * 1024,
|
|
76
|
+
signal: actionDeadline.signal,
|
|
77
|
+
env: cliEnvironment,
|
|
78
|
+
})
|
|
79
|
+
return Object.freeze({ kind: "succeeded" })
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (callerSignal?.aborted) {
|
|
82
|
+
return Object.freeze({ kind: "cancelled" })
|
|
83
|
+
}
|
|
84
|
+
if (error instanceof OfficialCliAuthError || error instanceof TypeError) throw error
|
|
85
|
+
throw new OfficialCliAuthError()
|
|
86
|
+
} finally {
|
|
87
|
+
actionDeadline?.dispose()
|
|
88
|
+
preparationDeadline?.dispose()
|
|
89
|
+
resolved = undefined
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return Object.freeze({
|
|
94
|
+
login({ signal } = {}) {
|
|
95
|
+
return runAction(["login", "--oauth"], signal, loginTimeoutMs)
|
|
96
|
+
},
|
|
97
|
+
logout({ signal } = {}) {
|
|
98
|
+
return runAction(["logout"], signal, logoutTimeoutMs)
|
|
99
|
+
},
|
|
100
|
+
refresh({ signal } = {}) {
|
|
101
|
+
return runAction(["models"], signal, refreshTimeoutMs)
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createDeadline(callerSignal, timeoutMs) {
|
|
107
|
+
const controller = new AbortController()
|
|
108
|
+
const abortFromCaller = () => controller.abort(callerSignal.reason)
|
|
109
|
+
if (callerSignal?.aborted) abortFromCaller()
|
|
110
|
+
else callerSignal?.addEventListener("abort", abortFromCaller, { once: true })
|
|
111
|
+
const timer = setTimeout(() => controller.abort(new DOMException("Timed out", "TimeoutError")), timeoutMs)
|
|
112
|
+
return {
|
|
113
|
+
signal: controller.signal,
|
|
114
|
+
dispose() {
|
|
115
|
+
clearTimeout(timer)
|
|
116
|
+
callerSignal?.removeEventListener("abort", abortFromCaller)
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isTimeout(value) {
|
|
122
|
+
return Number.isSafeInteger(value) && value > 0 && value <= MAX_TIMER_DELAY_MS
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function runCollected(subprocess, { argv, cwd, maxBytes, signal, env }) {
|
|
126
|
+
let handle
|
|
127
|
+
let stdout
|
|
128
|
+
let stderr
|
|
129
|
+
try {
|
|
130
|
+
handle = subprocess.spawn({
|
|
131
|
+
argv,
|
|
132
|
+
cwd,
|
|
133
|
+
stdio: {
|
|
134
|
+
stdin: "ignore",
|
|
135
|
+
stdout: { maxBytes },
|
|
136
|
+
stderr: { maxBytes },
|
|
137
|
+
},
|
|
138
|
+
graceMs: GRACE_MS,
|
|
139
|
+
signal,
|
|
140
|
+
env,
|
|
141
|
+
})
|
|
142
|
+
if (!isProcessHandle(handle)) throw new OfficialCliAuthError()
|
|
143
|
+
const outcome = await handle.done
|
|
144
|
+
if (!isOutcome(outcome) || outcome.exitCode !== 0 || outcome.signal !== null) {
|
|
145
|
+
throw new OfficialCliAuthError()
|
|
146
|
+
}
|
|
147
|
+
stdout = readCollected(handle.collected.stdout)
|
|
148
|
+
stderr = readCollected(handle.collected.stderr)
|
|
149
|
+
return { stdout, stderr }
|
|
150
|
+
} finally {
|
|
151
|
+
if (handle && typeof handle.waitForExit === "function") {
|
|
152
|
+
const exited = await handle.waitForExit()
|
|
153
|
+
if (exited !== true && !signal?.aborted) throw new OfficialCliAuthError()
|
|
154
|
+
}
|
|
155
|
+
stderr = undefined
|
|
156
|
+
stdout = undefined
|
|
157
|
+
handle = undefined
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildCliEnvironment(platform, homeDir) {
|
|
162
|
+
const environment = {}
|
|
163
|
+
const fixedTombstones = [
|
|
164
|
+
"BROWSER",
|
|
165
|
+
"GROK_AUTH_PROVIDER_COMMAND",
|
|
166
|
+
"GROK_CONFIG",
|
|
167
|
+
"GROK_CONFIG_PATH",
|
|
168
|
+
"GROK_HOME",
|
|
169
|
+
"NODE_EXTRA_CA_CERTS",
|
|
170
|
+
"NODE_OPTIONS",
|
|
171
|
+
"NODE_PATH",
|
|
172
|
+
"SSLKEYLOGFILE",
|
|
173
|
+
"XAI_API_KEY",
|
|
174
|
+
]
|
|
175
|
+
for (const name of fixedTombstones) environment[name] = undefined
|
|
176
|
+
for (const name of Object.keys(process.env)) {
|
|
177
|
+
if (/^(?:GROK_|XAI_|DYLD_|LD_)/iu.test(name)) environment[name] = undefined
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (platform === "darwin") {
|
|
181
|
+
environment.HOME = homeDir
|
|
182
|
+
environment.PATH = "/usr/bin:/bin:/usr/sbin:/sbin"
|
|
183
|
+
} else {
|
|
184
|
+
const inheritedRoot = process.env.SystemRoot ?? process.env.SYSTEMROOT
|
|
185
|
+
const systemRoot = typeof inheritedRoot === "string" && path.win32.isAbsolute(inheritedRoot) &&
|
|
186
|
+
inheritedRoot.length <= 260 && !/[\r\n\0]/u.test(inheritedRoot)
|
|
187
|
+
? path.win32.normalize(inheritedRoot)
|
|
188
|
+
: "C:\\Windows"
|
|
189
|
+
environment.USERPROFILE = homeDir
|
|
190
|
+
environment.PATH = [
|
|
191
|
+
path.win32.join(systemRoot, "System32"),
|
|
192
|
+
systemRoot,
|
|
193
|
+
path.win32.join(systemRoot, "System32", "Wbem"),
|
|
194
|
+
].join(";")
|
|
195
|
+
environment.PATHEXT = ".COM;.EXE;.BAT;.CMD"
|
|
196
|
+
environment.COMSPEC = path.win32.join(systemRoot, "System32", "cmd.exe")
|
|
197
|
+
}
|
|
198
|
+
return Object.freeze(environment)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function readCollected(reader) {
|
|
202
|
+
if (!reader || typeof reader.readFrom !== "function") throw new OfficialCliAuthError()
|
|
203
|
+
const output = reader.readFrom(0)
|
|
204
|
+
if (
|
|
205
|
+
!output ||
|
|
206
|
+
typeof output.text !== "string" ||
|
|
207
|
+
output.lossy !== false ||
|
|
208
|
+
output.spillPath !== undefined
|
|
209
|
+
) throw new OfficialCliAuthError()
|
|
210
|
+
return output.text
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isProcessHandle(handle) {
|
|
214
|
+
return (
|
|
215
|
+
handle !== null &&
|
|
216
|
+
typeof handle === "object" &&
|
|
217
|
+
handle.done &&
|
|
218
|
+
typeof handle.done.then === "function" &&
|
|
219
|
+
handle.collected &&
|
|
220
|
+
typeof handle.waitForExit === "function"
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isOutcome(outcome) {
|
|
225
|
+
return (
|
|
226
|
+
outcome !== null &&
|
|
227
|
+
typeof outcome === "object" &&
|
|
228
|
+
(Number.isSafeInteger(outcome.exitCode) || outcome.exitCode === null) &&
|
|
229
|
+
(typeof outcome.signal === "string" || outcome.signal === null)
|
|
230
|
+
)
|
|
231
|
+
}
|