dsh-openai-subscription 0.1.2 → 0.1.4
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/README.md +36 -12
- package/README.zh.md +36 -12
- package/dist/client.d.ts +8 -0
- package/dist/client.js +507 -170
- package/dist/driver.d.ts +12 -0
- package/dist/driver.js +136 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +58 -0
- package/dist/host.d.ts +32 -14
- package/dist/host.js +242 -312
- package/dist/models.js +91 -55
- package/dist/oauth.js +2 -2
- package/dist/platform.d.ts +17 -0
- package/dist/platform.js +279 -0
- package/package.json +2 -2
package/dist/driver.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ShellExecutor } from '@deepseek-ai/dsh-shell';
|
|
2
|
+
import { type OAuthCredential } from './oauth.js';
|
|
3
|
+
export declare const VERIFICATION_URL = "https://auth.openai.com/codex/device";
|
|
4
|
+
export declare const DRIVER_DEVICE = "\nconst out = (o) => process.stdout.write(JSON.stringify(o) + '\\n')\nprocess.stdout.on('error', () => {})\ntry {\n const { pathToFileURL } = await import('node:url')\n const { openaiCodexOAuth } = await import(pathToFileURL(process.argv[1]).href)\n const credential = await openaiCodexOAuth.login({\n signal: AbortSignal.timeout(15 * 60 * 1000),\n notify: (n) => out({ ...n, type: 'notice' }),\n prompt: async (p) => {\n if (p.type === 'select') return 'device_code'\n throw new Error('Unsupported authorization interaction')\n },\n })\n out({ type: 'result', credential })\n} catch (error) {\n out({ type: 'error', message: error instanceof Error ? error.message : String(error) })\n}\n";
|
|
5
|
+
export declare const DRIVER_REFRESH = "\nconst out = (o) => process.stdout.write(JSON.stringify(o) + '\\n')\nprocess.stdout.on('error', () => {})\ntry {\n const { readFileSync } = await import('node:fs')\n const { pathToFileURL } = await import('node:url')\n const { openaiCodexOAuth } = await import(pathToFileURL(process.argv[1]).href)\n const credential = JSON.parse(readFileSync(0, 'utf8') || '{}')\n const refreshed = await openaiCodexOAuth.refresh(credential, AbortSignal.timeout(90 * 1000))\n out({ type: 'result', credential: refreshed })\n} catch (error) {\n out({ type: 'error', message: error instanceof Error ? error.message : String(error) })\n}\n";
|
|
6
|
+
export declare function parseDriverMessage(line: string): Record<string, unknown> | null;
|
|
7
|
+
/** Bounded streaming reader; disposal/cancellation always reaps its process. */
|
|
8
|
+
export declare function runDeviceDriver(shell: ShellExecutor, modulePath: string, signal: AbortSignal, notify: (code: string, url: string) => void, options?: {
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
codeTimeoutMs?: number;
|
|
11
|
+
pollMs?: number;
|
|
12
|
+
}): Promise<OAuthCredential | null>;
|
package/dist/driver.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
import { SubscriptionError, oauthFailureCode } from './errors.js';
|
|
3
|
+
import { normalizeOAuthCredential } from './oauth.js';
|
|
4
|
+
import { buildNodeCommand } from './platform.js';
|
|
5
|
+
export const VERIFICATION_URL = 'https://auth.openai.com/codex/device';
|
|
6
|
+
const MAX_OUTPUT = 256 * 1024;
|
|
7
|
+
const DEVICE_TIMEOUT = 15 * 60 * 1000;
|
|
8
|
+
// Catch import failures as well as OAuth errors. Tokens only travel over stdin/stdout,
|
|
9
|
+
// never in command arguments or environment variables.
|
|
10
|
+
export const DRIVER_DEVICE = `
|
|
11
|
+
const out = (o) => process.stdout.write(JSON.stringify(o) + '\\n')
|
|
12
|
+
process.stdout.on('error', () => {})
|
|
13
|
+
try {
|
|
14
|
+
const { pathToFileURL } = await import('node:url')
|
|
15
|
+
const { openaiCodexOAuth } = await import(pathToFileURL(process.argv[1]).href)
|
|
16
|
+
const credential = await openaiCodexOAuth.login({
|
|
17
|
+
signal: AbortSignal.timeout(15 * 60 * 1000),
|
|
18
|
+
notify: (n) => out({ ...n, type: 'notice' }),
|
|
19
|
+
prompt: async (p) => {
|
|
20
|
+
if (p.type === 'select') return 'device_code'
|
|
21
|
+
throw new Error('Unsupported authorization interaction')
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
out({ type: 'result', credential })
|
|
25
|
+
} catch (error) {
|
|
26
|
+
out({ type: 'error', message: error instanceof Error ? error.message : String(error) })
|
|
27
|
+
}
|
|
28
|
+
`;
|
|
29
|
+
export const DRIVER_REFRESH = `
|
|
30
|
+
const out = (o) => process.stdout.write(JSON.stringify(o) + '\\n')
|
|
31
|
+
process.stdout.on('error', () => {})
|
|
32
|
+
try {
|
|
33
|
+
const { readFileSync } = await import('node:fs')
|
|
34
|
+
const { pathToFileURL } = await import('node:url')
|
|
35
|
+
const { openaiCodexOAuth } = await import(pathToFileURL(process.argv[1]).href)
|
|
36
|
+
const credential = JSON.parse(readFileSync(0, 'utf8') || '{}')
|
|
37
|
+
const refreshed = await openaiCodexOAuth.refresh(credential, AbortSignal.timeout(90 * 1000))
|
|
38
|
+
out({ type: 'result', credential: refreshed })
|
|
39
|
+
} catch (error) {
|
|
40
|
+
out({ type: 'error', message: error instanceof Error ? error.message : String(error) })
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
export function parseDriverMessage(line) {
|
|
44
|
+
try {
|
|
45
|
+
const value = JSON.parse(line);
|
|
46
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
47
|
+
? value : null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Bounded streaming reader; disposal/cancellation always reaps its process. */
|
|
54
|
+
export async function runDeviceDriver(shell, modulePath, signal, notify, options = {}) {
|
|
55
|
+
if (signal.aborted)
|
|
56
|
+
return null;
|
|
57
|
+
const timeout = AbortSignal.timeout(options.timeoutMs ?? DEVICE_TIMEOUT);
|
|
58
|
+
const combined = AbortSignal.any([signal, timeout]);
|
|
59
|
+
const proc = shell.start(shell.resolve({
|
|
60
|
+
command: buildNodeCommand(DRIVER_DEVICE, modulePath),
|
|
61
|
+
signal: combined,
|
|
62
|
+
stdoutMaxBytes: MAX_OUTPUT,
|
|
63
|
+
}));
|
|
64
|
+
let buffer = '';
|
|
65
|
+
let total = 0;
|
|
66
|
+
let codeReceived = false;
|
|
67
|
+
let credential = null;
|
|
68
|
+
const codeDeadline = Date.now() + (options.codeTimeoutMs ?? 45_000);
|
|
69
|
+
try {
|
|
70
|
+
while (true) {
|
|
71
|
+
if (signal.aborted)
|
|
72
|
+
return null;
|
|
73
|
+
if (timeout.aborted || (!codeReceived && Date.now() >= codeDeadline))
|
|
74
|
+
throw new SubscriptionError('timeout');
|
|
75
|
+
const read = proc.readOutput();
|
|
76
|
+
total += Buffer.byteLength(read.delta);
|
|
77
|
+
if (read.lossy || total > MAX_OUTPUT)
|
|
78
|
+
throw new SubscriptionError('invalid-response');
|
|
79
|
+
buffer += read.delta;
|
|
80
|
+
// Read final unterminated JSON too, then drain any bytes that arrive
|
|
81
|
+
// between this read and observing process completion.
|
|
82
|
+
const lines = buffer.split('\n');
|
|
83
|
+
buffer = lines.pop() ?? '';
|
|
84
|
+
if (proc.status !== 'running' && buffer.trim()) {
|
|
85
|
+
lines.push(buffer);
|
|
86
|
+
buffer = '';
|
|
87
|
+
}
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
const message = parseDriverMessage(line);
|
|
90
|
+
if (message === null)
|
|
91
|
+
continue;
|
|
92
|
+
if (message.type === 'error')
|
|
93
|
+
throw new SubscriptionError(oauthFailureCode(message.message));
|
|
94
|
+
if (message.type === 'result') {
|
|
95
|
+
credential = normalizeOAuthCredential(message.credential);
|
|
96
|
+
if (credential === null)
|
|
97
|
+
throw new SubscriptionError('invalid-response');
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
if (message.type === 'notice' && typeof message.userCode === 'string' && /^[A-Za-z0-9-]{3,32}$/.test(message.userCode)) {
|
|
101
|
+
codeReceived = true;
|
|
102
|
+
// Never forward an arbitrary URL supplied by a dependency.
|
|
103
|
+
notify(message.userCode, VERIFICATION_URL);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (credential !== null)
|
|
107
|
+
break;
|
|
108
|
+
if (proc.status !== 'running') {
|
|
109
|
+
// A close between readOutput() and status can make final bytes available.
|
|
110
|
+
const tail = proc.readOutput();
|
|
111
|
+
if (tail.delta || tail.lossy) {
|
|
112
|
+
total += Buffer.byteLength(tail.delta);
|
|
113
|
+
if (tail.lossy || total > MAX_OUTPUT)
|
|
114
|
+
throw new SubscriptionError('invalid-response');
|
|
115
|
+
buffer += tail.delta;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
throw new SubscriptionError(proc.sandbox?.denied ? 'access-denied' : 'process-exited');
|
|
119
|
+
}
|
|
120
|
+
await delay(options.pollMs ?? 250, undefined, { signal: combined }).catch(() => { });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
try {
|
|
125
|
+
if (proc.status === 'running')
|
|
126
|
+
proc.kill();
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await proc.done;
|
|
130
|
+
}
|
|
131
|
+
// Sandbox facts may only be stamped at process close, after a result line.
|
|
132
|
+
if (!signal.aborted && proc.sandbox?.denied)
|
|
133
|
+
throw new SubscriptionError('access-denied');
|
|
134
|
+
}
|
|
135
|
+
return signal.aborted ? null : credential;
|
|
136
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Stable, non-sensitive failures shared by the host's public boundaries. */
|
|
2
|
+
export declare const FAILURE_CODES: readonly ['credentials-unavailable', 'shell-unavailable', 'timer-unavailable', 'component-unavailable', 'runtime-unsupported', 'busy', 'invalid-method', 'not-connected', 'not-refreshable', 'device-auth-disabled', 'access-denied', 'authorization-expired', 'rate-limited', 'network', 'timeout', 'invalid-response', 'process-exited', 'credential-write-failed', 'credential-changed', 'settings-unavailable', 'models-unavailable', 'models-empty', 'models-confirmation-required', 'settings-conflict', 'settings-write-failed', 'ownership-save-failed', 'cancelled', 'unknown'];
|
|
3
|
+
export type FailureCode = typeof FAILURE_CODES[number];
|
|
4
|
+
/** The marker survives RPC implementations which serialize only Error.message. */
|
|
5
|
+
export declare class SubscriptionError extends Error {
|
|
6
|
+
readonly code: FailureCode;
|
|
7
|
+
constructor(code: FailureCode);
|
|
8
|
+
}
|
|
9
|
+
export declare function failureCode(error: unknown, fallback?: FailureCode): FailureCode;
|
|
10
|
+
/** Classify internal OAuth diagnostics, but never return the diagnostic itself. */
|
|
11
|
+
export declare function oauthFailureCode(error: unknown): FailureCode;
|
|
12
|
+
/** Logs deliberately omit raw server responses, paths, tokens and account ids. */
|
|
13
|
+
export declare function logFailure(operation: string, error: unknown): void;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Stable, non-sensitive failures shared by the host's public boundaries. */
|
|
2
|
+
export const FAILURE_CODES = [
|
|
3
|
+
'credentials-unavailable', 'shell-unavailable', 'timer-unavailable', 'component-unavailable',
|
|
4
|
+
'runtime-unsupported', 'busy', 'invalid-method', 'not-connected', 'not-refreshable',
|
|
5
|
+
'device-auth-disabled', 'access-denied', 'authorization-expired', 'rate-limited',
|
|
6
|
+
'network', 'timeout', 'invalid-response', 'process-exited', 'credential-write-failed',
|
|
7
|
+
'credential-changed', 'settings-unavailable', 'models-unavailable', 'models-empty',
|
|
8
|
+
'models-confirmation-required', 'settings-conflict', 'settings-write-failed',
|
|
9
|
+
'ownership-save-failed', 'cancelled', 'unknown',
|
|
10
|
+
];
|
|
11
|
+
/** The marker survives RPC implementations which serialize only Error.message. */
|
|
12
|
+
export class SubscriptionError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(code) {
|
|
15
|
+
super(`[openai-subscription:${code}]`);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.name = 'SubscriptionError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function failureCode(error, fallback = 'unknown') {
|
|
21
|
+
if (error instanceof SubscriptionError)
|
|
22
|
+
return error.code;
|
|
23
|
+
const code = typeof error === 'object' && error !== null ? error.code : undefined;
|
|
24
|
+
if (code === 'SETTINGS_CONFLICT')
|
|
25
|
+
return 'settings-conflict';
|
|
26
|
+
if (code === 'EACCES' || code === 'EPERM')
|
|
27
|
+
return 'access-denied';
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
/** Classify internal OAuth diagnostics, but never return the diagnostic itself. */
|
|
31
|
+
export function oauthFailureCode(error) {
|
|
32
|
+
if (error instanceof SubscriptionError)
|
|
33
|
+
return error.code;
|
|
34
|
+
const message = typeof error === 'string' ? error
|
|
35
|
+
: error instanceof Error ? error.message : '';
|
|
36
|
+
const text = message.slice(0, 8192);
|
|
37
|
+
if (/access_denied|access denied|permission denied|\b403\b|sandbox.*den/i.test(text))
|
|
38
|
+
return 'access-denied';
|
|
39
|
+
if (/expired_token|invalid_grant|invalid_token|\b401\b|token.*expired/i.test(text))
|
|
40
|
+
return 'authorization-expired';
|
|
41
|
+
if (/authorization_pending/i.test(text))
|
|
42
|
+
return 'unknown';
|
|
43
|
+
if (/\b404\b|not enabled|device.{0,20}disabled/i.test(text))
|
|
44
|
+
return 'device-auth-disabled';
|
|
45
|
+
if (/\b429\b|rate.limit|slow_down|too many requests/i.test(text))
|
|
46
|
+
return 'rate-limited';
|
|
47
|
+
if (/timeout|timed? ?out|TimeoutError/i.test(text))
|
|
48
|
+
return 'timeout';
|
|
49
|
+
if (/fetch failed|network|ENOTFOUND|ECONN|EAI_AGAIN|certificate|TLS|proxy/i.test(text))
|
|
50
|
+
return 'network';
|
|
51
|
+
if (/ERR_MODULE_NOT_FOUND|Cannot find (module|package)|does not provide an export/i.test(text))
|
|
52
|
+
return 'component-unavailable';
|
|
53
|
+
return 'unknown';
|
|
54
|
+
}
|
|
55
|
+
/** Logs deliberately omit raw server responses, paths, tokens and account ids. */
|
|
56
|
+
export function logFailure(operation, error) {
|
|
57
|
+
console.error(`[openai-subscription] ${operation}: ${failureCode(error)}`);
|
|
58
|
+
}
|
package/dist/host.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { authModuleCandidates } from './platform.js';
|
|
2
|
+
import { type FailureCode } from './errors.js';
|
|
1
3
|
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
2
4
|
import type { Context } from '@deepseek-ai/cordis';
|
|
3
5
|
/** Stable progress keys let the browser localize notices without exposing diagnostics. */
|
|
@@ -5,6 +7,7 @@ type FlowNoticeKind = 'requesting-code' | 'enter-code' | 'refreshing' | 'models-
|
|
|
5
7
|
/** Host-side notice queued for the polling client. Mirrors `AuthorizationNotice`. */
|
|
6
8
|
interface FlowNotice {
|
|
7
9
|
kind?: FlowNoticeKind;
|
|
10
|
+
errorCode?: FailureCode;
|
|
8
11
|
message: string;
|
|
9
12
|
url?: string;
|
|
10
13
|
code?: string;
|
|
@@ -15,24 +18,28 @@ interface FlowState {
|
|
|
15
18
|
done: boolean;
|
|
16
19
|
outcome: 'authorized' | 'cancelled' | 'failed' | null;
|
|
17
20
|
error: string | null;
|
|
21
|
+
errorCode?: FailureCode;
|
|
22
|
+
completedAt?: number;
|
|
18
23
|
controller: AbortController;
|
|
19
24
|
task: Promise<void> | null;
|
|
20
25
|
}
|
|
21
26
|
/** `openaiSubscription/status` reply: semantic UI facts, never raw account metadata. */
|
|
22
|
-
|
|
23
|
-
configured:
|
|
27
|
+
interface StatusResult {
|
|
28
|
+
configured: boolean;
|
|
24
29
|
ready: boolean;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
unavailableReason?: FailureCode;
|
|
31
|
+
refreshable?: boolean;
|
|
32
|
+
credentialState?: 'valid' | 'expired' | 'unknown';
|
|
33
|
+
cleanupAvailable?: boolean;
|
|
34
|
+
flowPending?: boolean;
|
|
35
|
+
modelsSynced?: boolean;
|
|
36
|
+
modelCount?: number;
|
|
37
|
+
}
|
|
32
38
|
/** `openaiSubscription/authorize` reply. */
|
|
33
39
|
type AuthorizeResult = {
|
|
34
40
|
started: false;
|
|
35
41
|
error: string;
|
|
42
|
+
errorCode: FailureCode;
|
|
36
43
|
} | {
|
|
37
44
|
started: true;
|
|
38
45
|
};
|
|
@@ -40,6 +47,7 @@ type AuthorizeResult = {
|
|
|
40
47
|
interface ModelSyncResult {
|
|
41
48
|
synced: true;
|
|
42
49
|
count: number;
|
|
50
|
+
warningCode?: 'ownership-save-failed';
|
|
43
51
|
}
|
|
44
52
|
/** `openaiSubscription/poll` reply. */
|
|
45
53
|
type PollResult = {
|
|
@@ -53,6 +61,7 @@ type PollResult = {
|
|
|
53
61
|
notices: FlowNotice[];
|
|
54
62
|
outcome: FlowState['outcome'];
|
|
55
63
|
error: string | null;
|
|
64
|
+
errorCode?: FailureCode;
|
|
56
65
|
};
|
|
57
66
|
declare class OpenAISubscriptionController extends TypertRemoteService {
|
|
58
67
|
private registeredAuthorization;
|
|
@@ -62,13 +71,18 @@ declare class OpenAISubscriptionController extends TypertRemoteService {
|
|
|
62
71
|
private syncingModels;
|
|
63
72
|
private modelSyncController;
|
|
64
73
|
private modelDiscovery;
|
|
74
|
+
private disconnecting;
|
|
65
75
|
constructor(ctx: Context);
|
|
66
76
|
private credentials;
|
|
67
77
|
private shell;
|
|
68
|
-
private timer;
|
|
69
78
|
private authorization;
|
|
70
79
|
private llm;
|
|
71
80
|
private ensureAuthorizationFlow;
|
|
81
|
+
/**
|
|
82
|
+
* Locate the OpenAI auth module without spawning a shell. Probes the pi-ai
|
|
83
|
+
* copy shipped inside the running DSH first; returns '' when nothing hits.
|
|
84
|
+
*/
|
|
85
|
+
private probeInProcessModule;
|
|
72
86
|
private locateAuthModule;
|
|
73
87
|
private runDevice;
|
|
74
88
|
private runRefresh;
|
|
@@ -76,23 +90,27 @@ declare class OpenAISubscriptionController extends TypertRemoteService {
|
|
|
76
90
|
private mirrorToPiAi;
|
|
77
91
|
/** Keep one cancellable model sync in flight so login, refresh, and UI actions cannot race. */
|
|
78
92
|
private synchronizeModels;
|
|
93
|
+
/** Bound this read even if an older adapter ignores AbortSignal. No writes are raced. */
|
|
94
|
+
private readBuiltinModels;
|
|
79
95
|
/** Fetch the account-scoped catalog and replace only this plugin's previous rows. */
|
|
80
96
|
private performModelSync;
|
|
81
97
|
/** Remove only unchanged dynamic rows, preserving every local model and profile field. */
|
|
82
98
|
private removeManagedPiRoute;
|
|
83
99
|
private beginLogin;
|
|
84
100
|
private registerAuthorizationFlow;
|
|
85
|
-
private shq;
|
|
86
101
|
status(): Promise<StatusResult>;
|
|
87
|
-
|
|
102
|
+
private readStatus;
|
|
103
|
+
authorize(method?: unknown): Promise<AuthorizeResult>;
|
|
88
104
|
poll(): Promise<PollResult>;
|
|
89
105
|
cancel(): Promise<{
|
|
90
106
|
ok: true;
|
|
91
107
|
}>;
|
|
92
|
-
syncModels(): Promise<ModelSyncResult>;
|
|
93
|
-
/**
|
|
108
|
+
syncModels(confirmed?: unknown): Promise<ModelSyncResult>;
|
|
109
|
+
/** Serialize disconnect with authorization and sync, including other tabs. */
|
|
94
110
|
logout(): Promise<{
|
|
95
111
|
ok: true;
|
|
96
112
|
}>;
|
|
113
|
+
/** Cancel active authorization and remove plugin-managed credentials and settings. */
|
|
114
|
+
private performLogout;
|
|
97
115
|
}
|
|
98
116
|
export default OpenAISubscriptionController;
|