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,64 @@
|
|
|
1
|
+
export class AuthModeUnavailableError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super("The official Grok authentication source is unavailable")
|
|
4
|
+
this.name = "AuthModeUnavailableError"
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createAuthRegistry({ createTransport }) {
|
|
9
|
+
if (typeof createTransport !== "function") {
|
|
10
|
+
throw new TypeError("A Grok transport factory is required")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let entry
|
|
14
|
+
let generation = 0
|
|
15
|
+
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
install(source) {
|
|
18
|
+
if (!source || typeof source !== "object") throw new TypeError("Invalid Grok credential source")
|
|
19
|
+
const token = Object.freeze({})
|
|
20
|
+
const transport = createTransport(source)
|
|
21
|
+
if (!transport || typeof transport !== "object") throw new TypeError("Invalid Grok transport")
|
|
22
|
+
entry = { source, transport, token }
|
|
23
|
+
generation += 1
|
|
24
|
+
let disposed = false
|
|
25
|
+
return () => {
|
|
26
|
+
if (disposed) return
|
|
27
|
+
disposed = true
|
|
28
|
+
if (entry?.token === token) {
|
|
29
|
+
entry = undefined
|
|
30
|
+
generation += 1
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
invalidate() {
|
|
36
|
+
generation += 1
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
getGeneration() {
|
|
40
|
+
if (entry === undefined) throw new AuthModeUnavailableError()
|
|
41
|
+
return Object.freeze({ id: generation, transport: entry.transport })
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
async status() {
|
|
45
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
46
|
+
const snapshot = entry
|
|
47
|
+
const snapshotGeneration = generation
|
|
48
|
+
let available = false
|
|
49
|
+
if (snapshot !== undefined && typeof snapshot.source.withAccessToken === "function") {
|
|
50
|
+
try {
|
|
51
|
+
await snapshot.source.withAccessToken(() => undefined)
|
|
52
|
+
available = true
|
|
53
|
+
} catch {
|
|
54
|
+
available = false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (entry === snapshot && generation === snapshotGeneration) {
|
|
58
|
+
return Object.freeze({ generation: snapshotGeneration, available })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return Object.freeze({ generation, available: false })
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { AuthDriverUnavailableError, AuthLoginBusyError } from "./auth-controller.mjs"
|
|
2
|
+
|
|
3
|
+
export function createAuthRpcHandler({ controller, dashboard }) {
|
|
4
|
+
if (
|
|
5
|
+
!controller ||
|
|
6
|
+
typeof controller.status !== "function" ||
|
|
7
|
+
typeof controller.beginLogin !== "function" ||
|
|
8
|
+
typeof controller.cancel !== "function" ||
|
|
9
|
+
typeof controller.logout !== "function" ||
|
|
10
|
+
typeof dashboard !== "function"
|
|
11
|
+
) throw new TypeError("Invalid Grok auth RPC controller")
|
|
12
|
+
|
|
13
|
+
return async function handleAuthRpc(endpoint, payload, signal) {
|
|
14
|
+
if (signal?.aborted) return cancelled()
|
|
15
|
+
try {
|
|
16
|
+
if (endpoint === "status") {
|
|
17
|
+
if (!hasExactKeys(payload, [])) return badRequest()
|
|
18
|
+
const status = serializable(await controller.status())
|
|
19
|
+
return ok({ kind: "status", status })
|
|
20
|
+
}
|
|
21
|
+
if (endpoint === "dashboard") {
|
|
22
|
+
if (!hasExactKeys(payload, [])) return badRequest()
|
|
23
|
+
return ok({ kind: "dashboard", dashboard: serializable(await dashboard({ signal })) })
|
|
24
|
+
}
|
|
25
|
+
if (endpoint === "login") {
|
|
26
|
+
if (!hasExactKeys(payload, [])) return badRequest()
|
|
27
|
+
const session = await controller.beginLogin()
|
|
28
|
+
const status = serializable(session.public)
|
|
29
|
+
return ok({ kind: "login-started", status, sessionId: status.sessionId })
|
|
30
|
+
}
|
|
31
|
+
if (endpoint === "cancel") {
|
|
32
|
+
const cancellation = parseCancelPayload(payload)
|
|
33
|
+
if (cancellation === undefined) return badRequest()
|
|
34
|
+
return ok({
|
|
35
|
+
kind: controller.cancel(cancellation.sessionId) ? "cancelled" : "not-running",
|
|
36
|
+
status: serializable(await controller.status()),
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
if (endpoint === "logout") {
|
|
40
|
+
if (!hasExactKeys(payload, [])) return badRequest()
|
|
41
|
+
const outcome = await controller.logout({ signal })
|
|
42
|
+
if (outcome.kind === "confirmation-required") {
|
|
43
|
+
return ok({
|
|
44
|
+
kind: "logout-confirmation-required",
|
|
45
|
+
confirmationId: outcome.confirmationId,
|
|
46
|
+
expiresAt: outcome.expiresAt,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
return ok({ kind: `logout-${outcome.kind}`, status: serializable(await controller.status()) })
|
|
50
|
+
}
|
|
51
|
+
return badRequest()
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error instanceof AuthLoginBusyError) {
|
|
54
|
+
return ok({ kind: "busy", status: serializable(await controller.status()) })
|
|
55
|
+
}
|
|
56
|
+
if (error instanceof AuthDriverUnavailableError) {
|
|
57
|
+
return ok({ kind: "unavailable", status: serializable(await controller.status()) })
|
|
58
|
+
}
|
|
59
|
+
return signal?.aborted ? cancelled() : internal()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseCancelPayload(payload) {
|
|
65
|
+
if (
|
|
66
|
+
!hasExactKeys(payload, ["sessionId"]) ||
|
|
67
|
+
typeof payload.sessionId !== "string" ||
|
|
68
|
+
payload.sessionId.length === 0 ||
|
|
69
|
+
payload.sessionId.length > 128
|
|
70
|
+
) return undefined
|
|
71
|
+
return payload
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function hasExactKeys(value, expected) {
|
|
75
|
+
if (!isPlainObject(value)) return false
|
|
76
|
+
const keys = Object.keys(value).sort()
|
|
77
|
+
const sorted = [...expected].sort()
|
|
78
|
+
return keys.length === sorted.length && keys.every((key, index) => key === sorted[index])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function serializable(value) {
|
|
82
|
+
const text = JSON.stringify(value)
|
|
83
|
+
if (typeof text !== "string" || Buffer.byteLength(text, "utf8") > 64 * 1024) {
|
|
84
|
+
throw new TypeError("Invalid Grok auth RPC response")
|
|
85
|
+
}
|
|
86
|
+
return JSON.parse(text)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ok(value) {
|
|
90
|
+
return { ok: true, value }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function badRequest() {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
error: { code: "bad-request", message: "Invalid Grok auth RPC request", details: { issues: [] } },
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function cancelled() {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
error: { code: "cancelled", message: "The Grok auth operation was cancelled", details: {} },
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function internal() {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
error: { code: "internal", message: "The Grok auth operation failed", details: {} },
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isPlainObject(value) {
|
|
115
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false
|
|
116
|
+
const prototype = Object.getPrototypeOf(value)
|
|
117
|
+
return prototype === Object.prototype || prototype === null
|
|
118
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const MAX_BILLING_BYTES = 256 * 1024
|
|
2
|
+
|
|
3
|
+
export class InvalidBillingResponseError extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super("The Grok billing response is invalid or unsupported")
|
|
6
|
+
this.name = "InvalidBillingResponseError"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parseBillingSummary(raw) {
|
|
11
|
+
if (typeof raw !== "string" || Buffer.byteLength(raw, "utf8") > MAX_BILLING_BYTES) fail()
|
|
12
|
+
let value
|
|
13
|
+
try { value = JSON.parse(raw) } catch { fail() }
|
|
14
|
+
if (!isPlainObject(value) || (value.config !== null && !isPlainObject(value.config))) fail()
|
|
15
|
+
if (value.config === null) return Object.freeze({ state: "unavailable" })
|
|
16
|
+
|
|
17
|
+
const config = value.config
|
|
18
|
+
const period = parsePeriod(config)
|
|
19
|
+
const usedPercent = parseUsage(config, period)
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
state: "ready",
|
|
22
|
+
...(usedPercent === undefined ? {} : {
|
|
23
|
+
usedPercent,
|
|
24
|
+
remainingPercent: normalizePercent(100 - usedPercent),
|
|
25
|
+
}),
|
|
26
|
+
...period,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseUsage(config, period) {
|
|
31
|
+
if (config.creditUsagePercent !== undefined) {
|
|
32
|
+
if (!isPercent(config.creditUsagePercent)) fail()
|
|
33
|
+
return config.creditUsagePercent
|
|
34
|
+
}
|
|
35
|
+
// GetGrokCreditsConfig is protobuf-backed. Its JSON projection omits the
|
|
36
|
+
// scalar zero value, while retaining the typed period that owns it.
|
|
37
|
+
if (
|
|
38
|
+
period.periodKind !== undefined
|
|
39
|
+
&& period.periodStart !== undefined
|
|
40
|
+
&& period.resetsAt !== undefined
|
|
41
|
+
) return 0
|
|
42
|
+
if (config.monthlyLimit === undefined && config.used === undefined) return undefined
|
|
43
|
+
const limit = parseCent(config.monthlyLimit)
|
|
44
|
+
const used = parseCent(config.used)
|
|
45
|
+
if (limit <= 0 || used < 0 || used > limit) fail()
|
|
46
|
+
return normalizePercent((used / limit) * 100)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parsePeriod(config) {
|
|
50
|
+
if (config.currentPeriod !== undefined) {
|
|
51
|
+
if (!isPlainObject(config.currentPeriod)) fail()
|
|
52
|
+
const current = config.currentPeriod
|
|
53
|
+
const periodKind = current.type === "USAGE_PERIOD_TYPE_WEEKLY" ? "weekly"
|
|
54
|
+
: current.type === "USAGE_PERIOD_TYPE_MONTHLY" ? "monthly" : undefined
|
|
55
|
+
if (current.type !== undefined && (
|
|
56
|
+
typeof current.type !== "string" || current.type.length === 0 || current.type.length > 128
|
|
57
|
+
)) fail()
|
|
58
|
+
const periodStart = parseOptionalDateTime(current.start)
|
|
59
|
+
const resetsAt = parseOptionalDateTime(current.end)
|
|
60
|
+
return {
|
|
61
|
+
...(periodKind === undefined ? {} : { periodKind }),
|
|
62
|
+
...(periodStart === undefined ? {} : { periodStart }),
|
|
63
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const periodStart = parseOptionalDateTime(config.billingPeriodStart)
|
|
67
|
+
const resetsAt = parseOptionalDateTime(config.billingPeriodEnd)
|
|
68
|
+
if (periodStart === undefined && resetsAt === undefined) return {}
|
|
69
|
+
return {
|
|
70
|
+
periodKind: "monthly",
|
|
71
|
+
...(periodStart === undefined ? {} : { periodStart }),
|
|
72
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseCent(value) {
|
|
77
|
+
if (!isPlainObject(value) || !Number.isSafeInteger(value.val)) fail()
|
|
78
|
+
return value.val
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseOptionalDateTime(value) {
|
|
82
|
+
if (value === undefined) return undefined
|
|
83
|
+
if (typeof value !== "string") fail()
|
|
84
|
+
const time = Date.parse(value)
|
|
85
|
+
if (!Number.isFinite(time)) fail()
|
|
86
|
+
return new Date(time).toISOString()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizePercent(value) {
|
|
90
|
+
return Math.round(value * 100) / 100
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isPercent(value) {
|
|
94
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function fail() {
|
|
98
|
+
throw new InvalidBillingResponseError()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isPlainObject(value) {
|
|
102
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false
|
|
103
|
+
const prototype = Object.getPrototypeOf(value)
|
|
104
|
+
return prototype === Object.prototype || prototype === null
|
|
105
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828"
|
|
2
|
+
const EXPIRY_SKEW_MS = 5 * 60 * 1000
|
|
3
|
+
|
|
4
|
+
export const GROK_CLI_1_0_5_AUTH_CONTRACT = Object.freeze({
|
|
5
|
+
authMode: "oidc",
|
|
6
|
+
clientId: CLIENT_ID,
|
|
7
|
+
issuer: "https://auth.x.ai",
|
|
8
|
+
scope: `https://auth.x.ai::${CLIENT_ID}`,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export class UnsupportedCredentialError extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super("The Grok credential does not match the supported authentication contract")
|
|
14
|
+
this.name = "UnsupportedCredentialError"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class CredentialFileTooLargeError extends Error {
|
|
19
|
+
constructor() {
|
|
20
|
+
super("The Grok credential file exceeds the 64 KiB limit")
|
|
21
|
+
this.name = "CredentialFileTooLargeError"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class CredentialRefreshRequiredError extends Error {}
|
|
26
|
+
|
|
27
|
+
export function createCredentialSource({ contract, load, now, refresh }) {
|
|
28
|
+
if (
|
|
29
|
+
!contract ||
|
|
30
|
+
typeof load !== "function" ||
|
|
31
|
+
typeof now !== "function" ||
|
|
32
|
+
(refresh !== undefined && typeof refresh !== "function")
|
|
33
|
+
) {
|
|
34
|
+
throw new TypeError("Invalid credential source dependencies")
|
|
35
|
+
}
|
|
36
|
+
let activeRefresh
|
|
37
|
+
|
|
38
|
+
const refreshOnce = async () => {
|
|
39
|
+
if (activeRefresh === undefined) {
|
|
40
|
+
activeRefresh = Promise.resolve().then(refresh).finally(() => {
|
|
41
|
+
activeRefresh = undefined
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
return activeRefresh
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
async withAccessToken(operation) {
|
|
49
|
+
if (typeof operation !== "function") {
|
|
50
|
+
throw new TypeError("Access-token operation must be a function")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let raw
|
|
54
|
+
let rawText
|
|
55
|
+
let parsed
|
|
56
|
+
let accessToken
|
|
57
|
+
let metadata
|
|
58
|
+
let refreshAttempted = false
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
while (accessToken === undefined) {
|
|
62
|
+
try {
|
|
63
|
+
raw = await load()
|
|
64
|
+
if (utf8ByteLength(raw) > 64 * 1024) {
|
|
65
|
+
throw new CredentialFileTooLargeError()
|
|
66
|
+
}
|
|
67
|
+
rawText = decodeUtf8(raw)
|
|
68
|
+
parsed = JSON.parse(rawText)
|
|
69
|
+
|
|
70
|
+
if (!isPlainObject(parsed)) throw new UnsupportedCredentialError()
|
|
71
|
+
|
|
72
|
+
const entries = Object.entries(parsed)
|
|
73
|
+
if (entries.length !== 1 || entries[0][0] !== contract.scope) {
|
|
74
|
+
throw new UnsupportedCredentialError()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const record = entries[0][1]
|
|
78
|
+
if (
|
|
79
|
+
!isPlainObject(record) ||
|
|
80
|
+
record.auth_mode !== contract.authMode ||
|
|
81
|
+
record.oidc_issuer !== contract.issuer ||
|
|
82
|
+
record.oidc_client_id !== contract.clientId ||
|
|
83
|
+
typeof record.key !== "string" ||
|
|
84
|
+
record.key.length === 0 ||
|
|
85
|
+
!isDateTime(record.expires_at)
|
|
86
|
+
) {
|
|
87
|
+
throw new UnsupportedCredentialError()
|
|
88
|
+
}
|
|
89
|
+
const currentTime = now()
|
|
90
|
+
if (!(currentTime instanceof Date) || !Number.isFinite(currentTime.getTime())) {
|
|
91
|
+
throw new TypeError("Invalid credential source clock")
|
|
92
|
+
}
|
|
93
|
+
if (!isFutureDateTime(record.expires_at, currentTime)) throw new CredentialRefreshRequiredError()
|
|
94
|
+
|
|
95
|
+
accessToken = record.key
|
|
96
|
+
metadata = Object.freeze({
|
|
97
|
+
...(isHeaderValue(record.user_id) ? { userId: record.user_id } : {}),
|
|
98
|
+
})
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (
|
|
101
|
+
error instanceof CredentialRefreshRequiredError &&
|
|
102
|
+
refresh !== undefined &&
|
|
103
|
+
!refreshAttempted
|
|
104
|
+
) {
|
|
105
|
+
refreshAttempted = true
|
|
106
|
+
try {
|
|
107
|
+
await refreshOnce()
|
|
108
|
+
} catch {
|
|
109
|
+
throw new UnsupportedCredentialError()
|
|
110
|
+
}
|
|
111
|
+
raw = undefined
|
|
112
|
+
rawText = undefined
|
|
113
|
+
parsed = undefined
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
error instanceof UnsupportedCredentialError ||
|
|
118
|
+
error instanceof CredentialFileTooLargeError ||
|
|
119
|
+
error instanceof TypeError
|
|
120
|
+
) {
|
|
121
|
+
throw error
|
|
122
|
+
}
|
|
123
|
+
throw new UnsupportedCredentialError()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return await operation(accessToken, metadata)
|
|
128
|
+
} finally {
|
|
129
|
+
metadata = undefined
|
|
130
|
+
accessToken = undefined
|
|
131
|
+
parsed = undefined
|
|
132
|
+
rawText = undefined
|
|
133
|
+
raw = undefined
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isHeaderValue(value) {
|
|
140
|
+
return typeof value === "string" && value.length > 0 && value.length <= 16 * 1024 && !/[\r\n\0]/u.test(value)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function utf8ByteLength(value) {
|
|
144
|
+
if (typeof value === "string") return Buffer.byteLength(value, "utf8")
|
|
145
|
+
if (value instanceof Uint8Array) return value.byteLength
|
|
146
|
+
throw new TypeError("Credential loader must return a string or Uint8Array")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function decodeUtf8(value) {
|
|
150
|
+
if (typeof value === "string") return value
|
|
151
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(value)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isPlainObject(value) {
|
|
155
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false
|
|
156
|
+
const prototype = Object.getPrototypeOf(value)
|
|
157
|
+
return prototype === Object.prototype || prototype === null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isFutureDateTime(value, now) {
|
|
161
|
+
if (typeof value !== "string") return false
|
|
162
|
+
const expiresAt = Date.parse(value)
|
|
163
|
+
return Number.isFinite(expiresAt) && expiresAt - now.getTime() > EXPIRY_SKEW_MS
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isDateTime(value) {
|
|
167
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value))
|
|
168
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { parseModelCatalogResponse } from "./model-catalog.mjs"
|
|
2
|
+
import { createResponsesEventDecoder } from "./responses-codec.mjs"
|
|
3
|
+
import { encodeResponsesRequest } from "./responses-request.mjs"
|
|
4
|
+
import { parseResponsesSse } from "./responses-sse.mjs"
|
|
5
|
+
|
|
6
|
+
export class GrokAdapterError extends Error {
|
|
7
|
+
constructor() {
|
|
8
|
+
super("The requested Grok model or adapter generation is unavailable")
|
|
9
|
+
this.name = "GrokAdapterError"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createGrokAdapter({ getGeneration, mapError = (error) => error }) {
|
|
14
|
+
if (typeof getGeneration !== "function") {
|
|
15
|
+
throw new TypeError("A Grok adapter generation source is required")
|
|
16
|
+
}
|
|
17
|
+
if (typeof mapError !== "function") throw new TypeError("Invalid Grok adapter error mapper")
|
|
18
|
+
|
|
19
|
+
const captureGeneration = () => {
|
|
20
|
+
const generation = getGeneration()
|
|
21
|
+
if (
|
|
22
|
+
!isPlainObject(generation) ||
|
|
23
|
+
!generation.transport ||
|
|
24
|
+
typeof generation.transport.listModels !== "function" ||
|
|
25
|
+
typeof generation.transport.streamResponses !== "function"
|
|
26
|
+
) {
|
|
27
|
+
throw new GrokAdapterError()
|
|
28
|
+
}
|
|
29
|
+
return generation
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const resolveWithGeneration = async (generation, provider, model, signal) => {
|
|
33
|
+
try {
|
|
34
|
+
requireProvider(provider)
|
|
35
|
+
if (typeof model !== "string" || model.length === 0) throw new GrokAdapterError()
|
|
36
|
+
const entries = await discover(generation, provider, signal)
|
|
37
|
+
const match = entries.find((entry) => entry.resolvedModelInfo.id === model)
|
|
38
|
+
if (match === undefined) throw new GrokAdapterError()
|
|
39
|
+
return match.resolvedModelInfo
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw mapError(error)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
providerInfo(provider) {
|
|
47
|
+
requireProvider(provider)
|
|
48
|
+
return Object.freeze({ id: "grok", name: "Grok Build" })
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
providerRetryPolicy(provider) {
|
|
52
|
+
requireProvider(provider)
|
|
53
|
+
return undefined
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
async listModels(provider) {
|
|
57
|
+
try {
|
|
58
|
+
requireProvider(provider)
|
|
59
|
+
const entries = await discover(captureGeneration(), provider)
|
|
60
|
+
return Object.freeze(entries.map((entry) => entry.resolvedModelInfo))
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw mapError(error)
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async resolveModel(provider, model, signal) {
|
|
67
|
+
return resolveWithGeneration(captureGeneration(), provider, model, signal)
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async prepareCall(provider, model, signal) {
|
|
71
|
+
const generation = captureGeneration()
|
|
72
|
+
const resolvedModel = await resolveWithGeneration(generation, provider, model, signal)
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
model: resolvedModel,
|
|
75
|
+
stream(options) {
|
|
76
|
+
try {
|
|
77
|
+
validatePreparedOptions(options, resolvedModel)
|
|
78
|
+
return streamWithGeneration(generation, options, mapError)
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw mapError(error)
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
stream(options) {
|
|
87
|
+
try {
|
|
88
|
+
return streamWithGeneration(captureGeneration(), options, mapError)
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw mapError(error)
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function discover(generation, provider, signal) {
|
|
97
|
+
const raw = await generation.transport.listModels({ signal })
|
|
98
|
+
return parseModelCatalogResponse(raw, { provider }).map((entry) => Object.freeze({
|
|
99
|
+
backend: entry.backend,
|
|
100
|
+
resolvedModelInfo: freezeModel(entry.resolvedModelInfo),
|
|
101
|
+
}))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function* streamWithGeneration(generation, options, mapError) {
|
|
105
|
+
try {
|
|
106
|
+
requireProvider(options?.provider)
|
|
107
|
+
const request = encodeResponsesRequest(options)
|
|
108
|
+
const decoder = createResponsesEventDecoder()
|
|
109
|
+
for await (const event of parseResponsesSse(
|
|
110
|
+
generation.transport.streamResponses(request, { signal: options.signal }),
|
|
111
|
+
)) {
|
|
112
|
+
for (const chunk of decoder.push(event)) yield chunk
|
|
113
|
+
}
|
|
114
|
+
decoder.finish()
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw mapError(error)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validatePreparedOptions(options, resolvedModel) {
|
|
121
|
+
if (!isPlainObject(options) || options.provider !== "grok" || options.model !== resolvedModel.id) {
|
|
122
|
+
throw new GrokAdapterError()
|
|
123
|
+
}
|
|
124
|
+
if (options.reasoningEffort !== undefined) {
|
|
125
|
+
const efforts = resolvedModel.reasoning?.efforts
|
|
126
|
+
if (!Array.isArray(efforts) || !efforts.some((effort) => effort.id === options.reasoningEffort)) {
|
|
127
|
+
throw new GrokAdapterError()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function freezeModel(model) {
|
|
133
|
+
const frozen = {
|
|
134
|
+
...model,
|
|
135
|
+
inputModalities: model.inputModalities === undefined
|
|
136
|
+
? undefined
|
|
137
|
+
: Object.freeze([...model.inputModalities]),
|
|
138
|
+
context: model.context === undefined ? undefined : Object.freeze({ ...model.context }),
|
|
139
|
+
reasoning: model.reasoning === undefined
|
|
140
|
+
? undefined
|
|
141
|
+
: Object.freeze({
|
|
142
|
+
...model.reasoning,
|
|
143
|
+
efforts: Object.freeze(model.reasoning.efforts.map((effort) => Object.freeze({ ...effort }))),
|
|
144
|
+
}),
|
|
145
|
+
}
|
|
146
|
+
if (frozen.inputModalities === undefined) delete frozen.inputModalities
|
|
147
|
+
if (frozen.context === undefined) delete frozen.context
|
|
148
|
+
if (frozen.reasoning === undefined) delete frozen.reasoning
|
|
149
|
+
return Object.freeze(frozen)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function requireProvider(provider) {
|
|
153
|
+
if (provider !== "grok") throw new GrokAdapterError()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isPlainObject(value) {
|
|
157
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false
|
|
158
|
+
const prototype = Object.getPrototypeOf(value)
|
|
159
|
+
return prototype === Object.prototype || prototype === null
|
|
160
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { parseGrokCommandInput } from "./grok-command.mjs"
|
|
2
|
+
|
|
3
|
+
const USAGE = "Usage: /grok status|login|cancel|logout"
|
|
4
|
+
|
|
5
|
+
export function createGrokCommandHandler({ controller }) {
|
|
6
|
+
if (
|
|
7
|
+
!controller ||
|
|
8
|
+
typeof controller.status !== "function" ||
|
|
9
|
+
typeof controller.beginLogin !== "function" ||
|
|
10
|
+
typeof controller.cancel !== "function" ||
|
|
11
|
+
typeof controller.logout !== "function"
|
|
12
|
+
) throw new TypeError("Invalid Grok command controller")
|
|
13
|
+
|
|
14
|
+
return async function handleGrokCommand({ rawInput, signal } = {}) {
|
|
15
|
+
const command = parseGrokCommandInput(rawInput)
|
|
16
|
+
if (command === undefined) return error(USAGE)
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
if (command.verb === "status") {
|
|
20
|
+
const status = await controller.status()
|
|
21
|
+
return success(`Grok official CLI auth: ${renderStatus(status)}`)
|
|
22
|
+
}
|
|
23
|
+
if (command.verb === "login") {
|
|
24
|
+
const session = await controller.beginLogin()
|
|
25
|
+
const outcome = await session.wait({ signal })
|
|
26
|
+
return outcome.kind === "succeeded"
|
|
27
|
+
? success("Grok official CLI login succeeded")
|
|
28
|
+
: error(`Grok official CLI login ${outcome.kind}`)
|
|
29
|
+
}
|
|
30
|
+
if (command.verb === "cancel") {
|
|
31
|
+
const sessionId = (await controller.status()).session?.sessionId
|
|
32
|
+
return typeof sessionId === "string" && controller.cancel(sessionId)
|
|
33
|
+
? success("Grok login cancellation requested")
|
|
34
|
+
: error("No Grok login is running")
|
|
35
|
+
}
|
|
36
|
+
if (command.verb === "logout") {
|
|
37
|
+
const outcome = await controller.logout({ signal })
|
|
38
|
+
return outcome.kind === "succeeded"
|
|
39
|
+
? success("Grok official CLI logout succeeded")
|
|
40
|
+
: outcome.kind === "confirmation-required"
|
|
41
|
+
? success("Repeat /grok logout within 30 seconds to confirm")
|
|
42
|
+
: error(`Grok official CLI logout ${outcome.kind}`)
|
|
43
|
+
}
|
|
44
|
+
return error(USAGE)
|
|
45
|
+
} catch {
|
|
46
|
+
return error("The Grok authentication operation failed")
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderStatus(status) {
|
|
52
|
+
if (status.session?.state === "running") return "running"
|
|
53
|
+
return status.driver && status.available ? "ready" : "unavailable"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function success(text) {
|
|
57
|
+
return Object.freeze({ kind: "success", text })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function error(text) {
|
|
61
|
+
return Object.freeze({ kind: "error", text })
|
|
62
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const GRAMMAR = Object.freeze([
|
|
2
|
+
{ pattern: /^\s+status$/u, build: () => ({ verb: "status" }) },
|
|
3
|
+
{ pattern: /^\s+login$/u, build: () => ({ verb: "login" }) },
|
|
4
|
+
{ pattern: /^\s+cancel$/u, build: () => ({ verb: "cancel" }) },
|
|
5
|
+
{ pattern: /^\s+logout$/u, build: () => ({ verb: "logout" }) },
|
|
6
|
+
])
|
|
7
|
+
|
|
8
|
+
export function parseGrokCommandInput(rawInput) {
|
|
9
|
+
if (typeof rawInput !== "string" || rawInput.length > 256 || /[\r\n\0]/u.test(rawInput)) {
|
|
10
|
+
return undefined
|
|
11
|
+
}
|
|
12
|
+
for (const rule of GRAMMAR) {
|
|
13
|
+
const match = rule.pattern.exec(rawInput)
|
|
14
|
+
if (match !== null) return Object.freeze(rule.build(match))
|
|
15
|
+
}
|
|
16
|
+
return undefined
|
|
17
|
+
}
|