opencode-ext-connector 0.5.0 → 0.7.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.
@@ -0,0 +1,118 @@
1
+ import { constants } from "node:fs";
2
+ import { open } from "node:fs/promises";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { z } from "zod";
5
+ const XaiAccessRecordSchema = z.discriminatedUnion("state", [
6
+ z
7
+ .object({
8
+ schema_version: z.literal(1),
9
+ provider: z.literal("xai"),
10
+ state: z.literal("ready"),
11
+ access: z.string().min(1),
12
+ expires: z.number().int().nonnegative(),
13
+ })
14
+ .strict()
15
+ .readonly(),
16
+ z
17
+ .object({
18
+ schema_version: z.literal(1),
19
+ provider: z.literal("xai"),
20
+ state: z.literal("unavailable"),
21
+ })
22
+ .strict()
23
+ .readonly(),
24
+ ]);
25
+ const UnavailableState = Object.freeze({ kind: "unavailable" });
26
+ class XaiAccessStateInvariantError extends Error {
27
+ name = "XaiAccessStateInvariantError";
28
+ constructor() {
29
+ super("unexpected xAI access state");
30
+ }
31
+ }
32
+ function assertNeverAccessState(_state) {
33
+ throw new XaiAccessStateInvariantError();
34
+ }
35
+ function currentProcessUid() {
36
+ return typeof process.getuid === "function" ? process.getuid() : undefined;
37
+ }
38
+ async function openAccessFile(path) {
39
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
40
+ return {
41
+ stat: async () => {
42
+ const metadata = await handle.stat();
43
+ return {
44
+ regular: metadata.isFile(),
45
+ links: metadata.nlink,
46
+ mode: metadata.mode & 0o7777,
47
+ ownerUid: metadata.uid,
48
+ };
49
+ },
50
+ readText: () => handle.readFile("utf8"),
51
+ close: () => handle.close(),
52
+ };
53
+ }
54
+ export function resolveXaiAccessPath(env) {
55
+ const dataHome = env["XDG_DATA_HOME"];
56
+ if (dataHome !== undefined && dataHome.length > 0) {
57
+ return isAbsolute(dataHome) ? join(dataHome, "opencode", "xai-access.json") : null;
58
+ }
59
+ const home = env["HOME"];
60
+ return home !== undefined && home.length > 0 && isAbsolute(home)
61
+ ? join(home, ".local", "share", "opencode", "xai-access.json")
62
+ : null;
63
+ }
64
+ function parsedState(raw) {
65
+ const parsedJson = JSON.parse(raw);
66
+ const record = XaiAccessRecordSchema.safeParse(parsedJson);
67
+ if (!record.success)
68
+ return UnavailableState;
69
+ switch (record.data.state) {
70
+ case "ready":
71
+ return Object.freeze({
72
+ kind: "ready",
73
+ access: record.data.access,
74
+ expires: record.data.expires,
75
+ });
76
+ case "unavailable":
77
+ return UnavailableState;
78
+ default:
79
+ return assertNeverAccessState(record.data);
80
+ }
81
+ }
82
+ export async function readXaiAccessState(options) {
83
+ const path = resolveXaiAccessPath(options.env);
84
+ if (path === null)
85
+ return UnavailableState;
86
+ const openFile = options.openFile ?? openAccessFile;
87
+ let file;
88
+ try {
89
+ file = await openFile(path);
90
+ }
91
+ catch (error) {
92
+ if (error instanceof Error)
93
+ return UnavailableState;
94
+ throw error;
95
+ }
96
+ let state = UnavailableState;
97
+ try {
98
+ const metadata = await file.stat();
99
+ const runtimeUid = (options.currentUid ?? currentProcessUid)();
100
+ const ownerMatches = runtimeUid === undefined || metadata.ownerUid === runtimeUid;
101
+ if (metadata.regular && metadata.links === 1 && metadata.mode === 0o600 && ownerMatches) {
102
+ state = parsedState(await file.readText());
103
+ }
104
+ }
105
+ catch (error) {
106
+ if (!(error instanceof Error))
107
+ throw error;
108
+ }
109
+ try {
110
+ await file.close();
111
+ }
112
+ catch (error) {
113
+ if (error instanceof Error)
114
+ return UnavailableState;
115
+ throw error;
116
+ }
117
+ return state;
118
+ }
@@ -0,0 +1,13 @@
1
+ import type { Clock } from "../../core/clock.js";
2
+ import { type AsyncDisposableHandle } from "../../core/lifecycle.js";
3
+ import type { ProcessSupervisor } from "../../core/process.js";
4
+ export type XaiAuthorityObserverOptions = {
5
+ readonly enabled: boolean;
6
+ readonly clock: Clock;
7
+ readonly env: Readonly<Record<string, string | undefined>>;
8
+ readonly processSupervisor: ProcessSupervisor;
9
+ readonly pollMs?: number;
10
+ readonly retryMs?: number;
11
+ readonly readAuthFile?: (path: string) => Promise<string>;
12
+ };
13
+ export declare function createXaiAuthorityObserver(options: XaiAuthorityObserverOptions): AsyncDisposableHandle;
@@ -0,0 +1,165 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { z } from "zod";
4
+ import { InvalidArgumentError, OperationCancelledError } from "../../core/errors.js";
5
+ import { createAsyncDisposable } from "../../core/lifecycle.js";
6
+ import { opencodeAuthJsonPaths } from "../../opencode/auth-store.js";
7
+ const AUTH_SYNC_HELPER = "opensandbox-xai-auth-sync";
8
+ const AUTH_SYNC_PATH = "/usr/local/bin:/usr/bin:/bin";
9
+ const AuthRootSchema = z.record(z.string(), z.unknown());
10
+ function errorCode(error) {
11
+ return "code" in error ? Reflect.get(error, "code") : undefined;
12
+ }
13
+ function commandEnvironment(env, home) {
14
+ const dataHome = env["XDG_DATA_HOME"];
15
+ return dataHome === undefined || dataHome.length === 0 || !isAbsolute(dataHome)
16
+ ? Object.freeze({ HOME: home, PATH: AUTH_SYNC_PATH })
17
+ : Object.freeze({ HOME: home, PATH: AUTH_SYNC_PATH, XDG_DATA_HOME: dataHome });
18
+ }
19
+ class XaiAuthorityInvariantError extends Error {
20
+ name = "XaiAuthorityInvariantError";
21
+ constructor() {
22
+ super("unexpected xAI authority state");
23
+ }
24
+ }
25
+ function assertNeverAuthorityState(_state) {
26
+ throw new XaiAuthorityInvariantError();
27
+ }
28
+ export function createXaiAuthorityObserver(options) {
29
+ const pollMs = options.pollMs ?? 1_000;
30
+ const retryMs = options.retryMs ?? 5_000;
31
+ if (!Number.isSafeInteger(pollMs) || pollMs <= 0)
32
+ throw new InvalidArgumentError("pollMs");
33
+ if (!Number.isSafeInteger(retryMs) || retryMs <= 0)
34
+ throw new InvalidArgumentError("retryMs");
35
+ if (!options.enabled)
36
+ return createAsyncDisposable(() => undefined);
37
+ const home = options.env["HOME"];
38
+ if (home === undefined || home.length === 0 || !isAbsolute(home)) {
39
+ return createAsyncDisposable(() => undefined);
40
+ }
41
+ const dataHome = options.env["XDG_DATA_HOME"];
42
+ if (dataHome !== undefined && dataHome.length > 0 && !isAbsolute(dataHome)) {
43
+ return createAsyncDisposable(() => undefined);
44
+ }
45
+ const [authPath] = opencodeAuthJsonPaths(options.env);
46
+ if (authPath === undefined || !isAbsolute(authPath))
47
+ return createAsyncDisposable(() => undefined);
48
+ const helper = join(home, ".local", "bin", AUTH_SYNC_HELPER);
49
+ const environment = commandEnvironment(options.env, home);
50
+ const loadAuth = options.readAuthFile ?? ((path) => readFile(path, "utf8"));
51
+ const controller = new AbortController();
52
+ let scheduled;
53
+ let activeProcess;
54
+ let activeEvaluation;
55
+ let synchronizedFingerprint;
56
+ let disposalStarted = false;
57
+ const arm = (delayMs) => {
58
+ if (disposalStarted)
59
+ return;
60
+ scheduled?.cancel();
61
+ scheduled = options.clock.schedule(delayMs, () => {
62
+ scheduled = undefined;
63
+ startEvaluation();
64
+ });
65
+ };
66
+ const observe = async () => {
67
+ let raw;
68
+ try {
69
+ raw = await loadAuth(authPath);
70
+ }
71
+ catch (error) {
72
+ if (error instanceof Error && errorCode(error) === "ENOENT") {
73
+ return { kind: "ready", fingerprint: "null" };
74
+ }
75
+ if (error instanceof Error)
76
+ return { kind: "retry" };
77
+ throw error;
78
+ }
79
+ try {
80
+ const parsedJson = JSON.parse(raw);
81
+ const root = AuthRootSchema.safeParse(parsedJson);
82
+ return root.success
83
+ ? { kind: "ready", fingerprint: JSON.stringify(root.data["xai"] ?? null) }
84
+ : { kind: "retry" };
85
+ }
86
+ catch (error) {
87
+ if (error instanceof SyntaxError)
88
+ return { kind: "retry" };
89
+ throw error;
90
+ }
91
+ };
92
+ const invoke = async () => {
93
+ const process = await options.processSupervisor.start({ executable: helper, arguments: [], cwd: null, environment }, controller.signal);
94
+ activeProcess = process;
95
+ try {
96
+ const exit = await process.wait(controller.signal);
97
+ switch (exit.kind) {
98
+ case "code":
99
+ return exit.code === 0;
100
+ case "signal":
101
+ return false;
102
+ default:
103
+ return assertNeverAuthorityState(exit);
104
+ }
105
+ }
106
+ finally {
107
+ activeProcess = undefined;
108
+ await process.dispose();
109
+ }
110
+ };
111
+ const evaluate = async () => {
112
+ const observation = await observe();
113
+ if (disposalStarted)
114
+ return;
115
+ switch (observation.kind) {
116
+ case "retry":
117
+ arm(retryMs);
118
+ return;
119
+ case "ready":
120
+ if (observation.fingerprint === synchronizedFingerprint) {
121
+ arm(pollMs);
122
+ return;
123
+ }
124
+ if (await invoke())
125
+ synchronizedFingerprint = observation.fingerprint;
126
+ if (!disposalStarted) {
127
+ arm(observation.fingerprint === synchronizedFingerprint ? pollMs : retryMs);
128
+ }
129
+ return;
130
+ default:
131
+ return assertNeverAuthorityState(observation);
132
+ }
133
+ };
134
+ const startEvaluation = () => {
135
+ if (disposalStarted || activeEvaluation !== undefined)
136
+ return;
137
+ const operation = evaluate().catch((error) => {
138
+ if (error instanceof OperationCancelledError && disposalStarted)
139
+ return;
140
+ if (error instanceof Error) {
141
+ arm(retryMs);
142
+ return;
143
+ }
144
+ throw error;
145
+ });
146
+ activeEvaluation = operation;
147
+ void operation.then(() => {
148
+ if (activeEvaluation === operation)
149
+ activeEvaluation = undefined;
150
+ }, () => {
151
+ if (activeEvaluation === operation)
152
+ activeEvaluation = undefined;
153
+ });
154
+ };
155
+ const disposal = createAsyncDisposable(async () => {
156
+ disposalStarted = true;
157
+ scheduled?.cancel();
158
+ scheduled = undefined;
159
+ controller.abort();
160
+ await activeProcess?.terminate();
161
+ await activeEvaluation;
162
+ });
163
+ startEvaluation();
164
+ return { dispose: disposal.dispose, [Symbol.asyncDispose]: disposal[Symbol.asyncDispose] };
165
+ }
@@ -0,0 +1,23 @@
1
+ import type { Clock } from "../../core/clock.js";
2
+ export type NetworkFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
3
+ export type XaiConsumerAuthOptions = {
4
+ readonly env: Readonly<Record<string, string | undefined>>;
5
+ readonly clock: Clock;
6
+ readonly networkFetch: NetworkFetch;
7
+ };
8
+ export type XaiConsumerAuthHook = {
9
+ readonly provider: "xai";
10
+ readonly loader: (getAuth: () => Promise<unknown>) => Promise<{
11
+ readonly apiKey: string;
12
+ readonly fetch: NetworkFetch;
13
+ } | {
14
+ readonly apiKey?: never;
15
+ readonly fetch?: never;
16
+ }>;
17
+ readonly methods: [];
18
+ };
19
+ export declare class XaiAccessUnavailableError extends Error {
20
+ readonly name = "XaiAccessUnavailableError";
21
+ constructor();
22
+ }
23
+ export declare function createXaiConsumerAuth(options: XaiConsumerAuthOptions): XaiConsumerAuthHook;
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ import { readXaiAccessState } from "./access-state.js";
3
+ const XAI_CONSUMER_MARKER = "cli-session:xai";
4
+ const XAI_DUMMY_API_KEY = "xai-access-file";
5
+ const XaiConsumerMarkerSchema = z
6
+ .object({ type: z.literal("api"), key: z.literal(XAI_CONSUMER_MARKER) })
7
+ .strict();
8
+ export class XaiAccessUnavailableError extends Error {
9
+ name = "XaiAccessUnavailableError";
10
+ constructor() {
11
+ super("xAI consumer access is unavailable");
12
+ }
13
+ }
14
+ function assertNeverAccessState(_state) {
15
+ throw new XaiAccessUnavailableError();
16
+ }
17
+ function createConsumerFetch(options) {
18
+ return async (input, init) => {
19
+ const state = await readXaiAccessState({ env: options.env });
20
+ let access;
21
+ switch (state.kind) {
22
+ case "ready":
23
+ if (state.expires <= options.clock.nowMs())
24
+ throw new XaiAccessUnavailableError();
25
+ access = state.access;
26
+ break;
27
+ case "unavailable":
28
+ throw new XaiAccessUnavailableError();
29
+ default:
30
+ return assertNeverAccessState(state);
31
+ }
32
+ const headers = new Headers(input instanceof Request ? input.headers : undefined);
33
+ const initHeaders = new Headers(init?.headers);
34
+ initHeaders.forEach((value, key) => {
35
+ headers.set(key, value);
36
+ });
37
+ headers.set("authorization", `Bearer ${access}`);
38
+ return options.networkFetch(input, init === undefined ? { headers } : { ...init, headers });
39
+ };
40
+ }
41
+ export function createXaiConsumerAuth(options) {
42
+ return {
43
+ provider: "xai",
44
+ loader: async (getAuth) => {
45
+ const marker = XaiConsumerMarkerSchema.safeParse(await getAuth());
46
+ return marker.success
47
+ ? { apiKey: XAI_DUMMY_API_KEY, fetch: createConsumerFetch(options) }
48
+ : {};
49
+ },
50
+ methods: [],
51
+ };
52
+ }
package/dist/server.d.ts CHANGED
@@ -4,6 +4,7 @@ export declare const claudeAuthServer: V1Plugin;
4
4
  export declare const cursorAuthServer: V1Plugin;
5
5
  export declare const commandCodeAuthServer: V1Plugin;
6
6
  export declare const ollamaAuthServer: V1Plugin;
7
+ export declare const xaiAuthServer: V1Plugin;
7
8
  export type ConnectorPluginModule = {
8
9
  readonly id: "opencode-ext-connector";
9
10
  readonly server: V1Plugin;
package/dist/server.js CHANGED
@@ -14,6 +14,8 @@ import { createProductionProcessSupervisor } from "./process/production-supervis
14
14
  import { createClaudeCredentialAuthorityScheduler } from "./providers/claude/credential-authority-scheduler.js";
15
15
  import { writeClaudeCredentials } from "./providers/claude/writeback.js";
16
16
  import { productionOllamaFetch } from "./providers/ollama/http.js";
17
+ import { createXaiAuthorityObserver } from "./providers/xai/authority-observer.js";
18
+ import { createXaiConsumerAuth } from "./providers/xai/consumer-auth.js";
17
19
  const env = process.env;
18
20
  const transport = createFetchHttpTransport();
19
21
  const authStore = createOpenCodeAuthStore({ env });
@@ -86,10 +88,17 @@ export const connectorServer = async (input, options) => {
86
88
  processSupervisor,
87
89
  logger,
88
90
  });
91
+ const xaiAuthority = createXaiAuthorityObserver({
92
+ enabled: connectorOptions.xaiOAuth?.mode === "authority",
93
+ clock,
94
+ env,
95
+ processSupervisor,
96
+ });
89
97
  const dispose = hooks.dispose;
90
98
  const disposal = createAsyncDisposable(async () => {
91
99
  const results = await Promise.allSettled([
92
100
  Promise.resolve().then(() => credentialAuthority.dispose()),
101
+ Promise.resolve().then(() => xaiAuthority.dispose()),
93
102
  Promise.resolve().then(() => processSupervisor.dispose()),
94
103
  Promise.resolve().then(() => dispose?.()),
95
104
  Promise.resolve().then(disposeV1LanguageRuntime),
@@ -129,6 +138,12 @@ export const ollamaAuthServer = async (_input, options) => {
129
138
  }).find((candidate) => candidate.id === "ollama");
130
139
  return entry === undefined ? {} : buildV1AuthHooks(entry, providerDeps, options);
131
140
  };
141
+ export const xaiAuthServer = async (_input, options) => {
142
+ const connectorOptions = parseConnectorOptions(pickConnectorOptionsInput(options));
143
+ return connectorOptions.xaiOAuth?.mode === "consumer"
144
+ ? { auth: createXaiConsumerAuth({ env, clock, networkFetch: globalThis.fetch }) }
145
+ : {};
146
+ };
132
147
  export const plugin = {
133
148
  id: "opencode-ext-connector",
134
149
  server: connectorServer,
package/dist/xai.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { Plugin as V1Plugin } from "@opencode-ai/plugin";
2
+ export declare const xaiAuthServer: V1Plugin;
package/dist/xai.js ADDED
@@ -0,0 +1 @@
1
+ export const xaiAuthServer = async (input, options) => (await import("./server.js")).xaiAuthServer(input, options);