@push.rocks/smartai 2.3.0 → 4.0.1

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,351 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import {
5
+ parseOpenAiChatGptTokenInfo,
6
+ refreshOpenAiChatGptTokenData,
7
+ } from '../ts/smartai.auth.openai.js';
8
+ import type {
9
+ IOpenAiChatGptAuthOptions,
10
+ IOpenAiChatGptTokenData,
11
+ IOpenAiChatGptTokenInfo,
12
+ } from '../ts/smartai.interfaces.js';
13
+
14
+ export type TOpenAiChatGptAuthSource = 'smartai' | 'opencode' | 'codex';
15
+ export type TOpenAiChatGptAuthFileFormat = TOpenAiChatGptAuthSource | 'auto';
16
+
17
+ export interface IOpenAiChatGptAuthSourceConfig {
18
+ source: TOpenAiChatGptAuthSource;
19
+ filePath?: string;
20
+ format?: TOpenAiChatGptAuthFileFormat;
21
+ writeBack?: boolean;
22
+ }
23
+
24
+ export interface IOpenAiChatGptAuthSourceInspection {
25
+ source: TOpenAiChatGptAuthSource;
26
+ filePath: string;
27
+ exists: boolean;
28
+ usable: boolean;
29
+ expired?: boolean;
30
+ accountId?: string;
31
+ email?: string;
32
+ plan?: string;
33
+ expiresAt?: string;
34
+ error?: string;
35
+ }
36
+
37
+ export interface IInspectOpenAiChatGptAuthSourcesOptions {
38
+ sources?: Array<TOpenAiChatGptAuthSource | IOpenAiChatGptAuthSourceConfig>;
39
+ homeDir?: string;
40
+ now?: Date;
41
+ }
42
+
43
+ export interface IResolveOpenAiChatGptAuthOptions extends IInspectOpenAiChatGptAuthSourcesOptions {
44
+ refresh?: 'ifNeeded' | false;
45
+ writeBack?: Partial<Record<TOpenAiChatGptAuthSource, boolean>>;
46
+ authOptions?: IOpenAiChatGptAuthOptions;
47
+ }
48
+
49
+ export interface IResolvedOpenAiChatGptAuth {
50
+ source: TOpenAiChatGptAuthSource;
51
+ filePath: string;
52
+ tokenData: IOpenAiChatGptTokenData;
53
+ refreshed: boolean;
54
+ }
55
+
56
+ interface INormalizedOpenAiChatGptAuthSourceConfig {
57
+ source: TOpenAiChatGptAuthSource;
58
+ filePath: string;
59
+ format: TOpenAiChatGptAuthFileFormat;
60
+ writeBack?: boolean;
61
+ }
62
+
63
+ const defaultSources: TOpenAiChatGptAuthSource[] = ['smartai', 'opencode', 'codex'];
64
+ const refreshWindowMs = 5 * 60 * 1000;
65
+
66
+ export const getDefaultOpenAiChatGptAuthPath = (
67
+ source: TOpenAiChatGptAuthSource,
68
+ homeDir = os.homedir(),
69
+ ): string => {
70
+ switch (source) {
71
+ case 'smartai':
72
+ return path.join(homeDir, '.git.zone', 'ide', 'openai-chatgpt-auth.json');
73
+ case 'opencode':
74
+ return path.join(homeDir, '.local', 'share', 'opencode', 'auth.json');
75
+ case 'codex':
76
+ return path.join(homeDir, '.codex', 'auth.json');
77
+ default:
78
+ throw new Error(`Unsupported OpenAI ChatGPT auth source: ${source satisfies never}`);
79
+ }
80
+ };
81
+
82
+ export const normalizeOpenAiChatGptAuth = (
83
+ input: unknown,
84
+ format: TOpenAiChatGptAuthFileFormat = 'auto',
85
+ ): IOpenAiChatGptTokenData | undefined => {
86
+ if (format === 'auto') {
87
+ return normalizeOpenAiChatGptAuth(input, 'smartai')
88
+ ?? normalizeOpenAiChatGptAuth(input, 'opencode')
89
+ ?? normalizeOpenAiChatGptAuth(input, 'codex');
90
+ }
91
+
92
+ if (!isRecord(input)) return undefined;
93
+ if (format === 'opencode') {
94
+ return normalizeOpenCodeAuth(input);
95
+ }
96
+ return normalizeTokenObject(isRecord(input.tokens) ? input.tokens : input);
97
+ };
98
+
99
+ export const readOpenAiChatGptAuthFile = async (
100
+ filePath: string,
101
+ format: TOpenAiChatGptAuthFileFormat = 'auto',
102
+ ): Promise<IOpenAiChatGptTokenData | undefined> => {
103
+ const parsed = JSON.parse(await fs.readFile(filePath, 'utf8')) as unknown;
104
+ return normalizeOpenAiChatGptAuth(parsed, format);
105
+ };
106
+
107
+ export const inspectOpenAiChatGptAuthSources = async (
108
+ options: IInspectOpenAiChatGptAuthSourcesOptions = {},
109
+ ): Promise<IOpenAiChatGptAuthSourceInspection[]> => {
110
+ const now = options.now ?? new Date();
111
+ const sourceConfigs = normalizeSourceConfigs(options.sources, options.homeDir);
112
+ return Promise.all(sourceConfigs.map(async (sourceConfig) => {
113
+ try {
114
+ const tokenData = await readOpenAiChatGptAuthFile(sourceConfig.filePath!, sourceConfig.format ?? sourceConfig.source);
115
+ if (!tokenData) {
116
+ return {
117
+ source: sourceConfig.source,
118
+ filePath: sourceConfig.filePath!,
119
+ exists: true,
120
+ usable: false,
121
+ error: 'No OpenAI ChatGPT auth token found in file.',
122
+ };
123
+ }
124
+ return toInspection(sourceConfig.source, sourceConfig.filePath!, tokenData, now);
125
+ } catch (error) {
126
+ const nodeError = error as NodeJS.ErrnoException;
127
+ return {
128
+ source: sourceConfig.source,
129
+ filePath: sourceConfig.filePath!,
130
+ exists: nodeError.code !== 'ENOENT',
131
+ usable: false,
132
+ error: nodeError.code === 'ENOENT' ? undefined : nodeError.message,
133
+ };
134
+ }
135
+ }));
136
+ };
137
+
138
+ export const resolveOpenAiChatGptAuth = async (
139
+ options: IResolveOpenAiChatGptAuthOptions = {},
140
+ ): Promise<IResolvedOpenAiChatGptAuth | undefined> => {
141
+ const now = options.now ?? new Date();
142
+ const sourceConfigs = normalizeSourceConfigs(options.sources, options.homeDir);
143
+ const refresh = options.refresh ?? 'ifNeeded';
144
+
145
+ for (const sourceConfig of sourceConfigs) {
146
+ let tokenData: IOpenAiChatGptTokenData | undefined;
147
+ try {
148
+ tokenData = await readOpenAiChatGptAuthFile(sourceConfig.filePath!, sourceConfig.format ?? sourceConfig.source);
149
+ } catch (error) {
150
+ const nodeError = error as NodeJS.ErrnoException;
151
+ if (nodeError.code === 'ENOENT') continue;
152
+ continue;
153
+ }
154
+ if (!tokenData) continue;
155
+
156
+ const shouldRefresh = refresh === 'ifNeeded' && shouldRefreshToken(tokenData, now);
157
+ const writeBack = sourceConfig.writeBack ?? options.writeBack?.[sourceConfig.source] ?? sourceConfig.source === 'smartai';
158
+ if (!shouldRefresh) {
159
+ return { source: sourceConfig.source, filePath: sourceConfig.filePath!, tokenData, refreshed: false };
160
+ }
161
+
162
+ if (!writeBack) {
163
+ if (!isExpired(tokenData, now)) {
164
+ return { source: sourceConfig.source, filePath: sourceConfig.filePath!, tokenData, refreshed: false };
165
+ }
166
+ continue;
167
+ }
168
+
169
+ try {
170
+ const refreshed = await refreshOpenAiChatGptTokenData(tokenData, options.authOptions ?? {});
171
+ await writeOpenAiChatGptAuthFile(sourceConfig.filePath!, refreshed, sourceConfig.format ?? sourceConfig.source);
172
+ return { source: sourceConfig.source, filePath: sourceConfig.filePath!, tokenData: refreshed, refreshed: true };
173
+ } catch {
174
+ continue;
175
+ }
176
+ }
177
+
178
+ return undefined;
179
+ };
180
+
181
+ export const writeOpenAiChatGptAuthFile = async (
182
+ filePath: string,
183
+ tokenData: IOpenAiChatGptTokenData,
184
+ format: TOpenAiChatGptAuthFileFormat = 'smartai',
185
+ ): Promise<void> => {
186
+ const current = await readJsonFileIfExists(filePath);
187
+ const payload = format === 'opencode'
188
+ ? toOpenCodeAuthFile(current, tokenData)
189
+ : format === 'codex'
190
+ ? toCodexAuthFile(current, tokenData)
191
+ : toSmartAiAuthFile(tokenData);
192
+ await writeJsonAtomic(filePath, payload);
193
+ };
194
+
195
+ const normalizeSourceConfigs = (
196
+ sources: Array<TOpenAiChatGptAuthSource | IOpenAiChatGptAuthSourceConfig> | undefined,
197
+ homeDir = os.homedir(),
198
+ ): INormalizedOpenAiChatGptAuthSourceConfig[] => {
199
+ return (sources ?? defaultSources).map((sourceInput) => {
200
+ const sourceConfig = typeof sourceInput === 'string' ? { source: sourceInput } : sourceInput;
201
+ return {
202
+ source: sourceConfig.source,
203
+ filePath: sourceConfig.filePath ?? getDefaultOpenAiChatGptAuthPath(sourceConfig.source, homeDir),
204
+ format: sourceConfig.format ?? sourceConfig.source,
205
+ writeBack: sourceConfig.writeBack,
206
+ };
207
+ });
208
+ };
209
+
210
+ const toInspection = (
211
+ source: TOpenAiChatGptAuthSource,
212
+ filePath: string,
213
+ tokenData: IOpenAiChatGptTokenData,
214
+ now: Date,
215
+ ): IOpenAiChatGptAuthSourceInspection => {
216
+ const expired = isExpired(tokenData, now);
217
+ return {
218
+ source,
219
+ filePath,
220
+ exists: true,
221
+ usable: !expired,
222
+ expired,
223
+ accountId: tokenData.accountId ?? tokenData.tokenInfo.chatgptAccountId,
224
+ email: tokenData.tokenInfo.email,
225
+ plan: tokenData.tokenInfo.chatgptPlanType,
226
+ expiresAt: tokenData.tokenInfo.expiresAt,
227
+ };
228
+ };
229
+
230
+ const normalizeTokenObject = (input: Record<string, unknown>): IOpenAiChatGptTokenData | undefined => {
231
+ const accessToken = stringValue(input.accessToken) ?? stringValue(input.access_token);
232
+ const refreshToken = stringValue(input.refreshToken) ?? stringValue(input.refresh_token);
233
+ const idToken = stringValue(input.idToken) ?? stringValue(input.id_token) ?? stringValue((input.tokenInfo as Record<string, unknown> | undefined)?.rawJwt);
234
+ const accountId = stringValue(input.accountId) ?? stringValue(input.account_id);
235
+ return createTokenDataFromValues({ accessToken, refreshToken, idToken, accountId });
236
+ };
237
+
238
+ const normalizeOpenCodeAuth = (input: Record<string, unknown>): IOpenAiChatGptTokenData | undefined => {
239
+ if (!isRecord(input.openai)) return undefined;
240
+ const accessToken = stringValue(input.openai.access);
241
+ const refreshToken = stringValue(input.openai.refresh);
242
+ const accountId = stringValue(input.openai.accountId);
243
+ const expiresAt = typeof input.openai.expires === 'number' ? new Date(input.openai.expires).toISOString() : undefined;
244
+ return createTokenDataFromValues({ accessToken, refreshToken, accountId, fallbackExpiresAt: expiresAt });
245
+ };
246
+
247
+ const createTokenDataFromValues = (input: {
248
+ accessToken?: string;
249
+ refreshToken?: string;
250
+ idToken?: string;
251
+ accountId?: string;
252
+ fallbackExpiresAt?: string;
253
+ }): IOpenAiChatGptTokenData | undefined => {
254
+ if (!input.accessToken || !input.refreshToken) return undefined;
255
+ const parseToken = input.idToken ?? input.accessToken;
256
+ const tokenInfo = parseTokenInfo(parseToken, input.accountId, input.fallbackExpiresAt);
257
+ return {
258
+ accessToken: input.accessToken,
259
+ refreshToken: input.refreshToken,
260
+ idToken: input.idToken,
261
+ accountId: input.accountId ?? tokenInfo.chatgptAccountId,
262
+ tokenInfo,
263
+ };
264
+ };
265
+
266
+ const parseTokenInfo = (token: string, accountId?: string, fallbackExpiresAt?: string): IOpenAiChatGptTokenInfo => {
267
+ try {
268
+ const tokenInfo = parseOpenAiChatGptTokenInfo(token);
269
+ return {
270
+ ...tokenInfo,
271
+ chatgptAccountId: tokenInfo.chatgptAccountId ?? accountId,
272
+ expiresAt: tokenInfo.expiresAt ?? fallbackExpiresAt,
273
+ };
274
+ } catch {
275
+ return {
276
+ chatgptAccountId: accountId,
277
+ chatgptAccountIsFedramp: false,
278
+ expiresAt: fallbackExpiresAt,
279
+ rawJwt: token,
280
+ };
281
+ }
282
+ };
283
+
284
+ const toSmartAiAuthFile = (tokenData: IOpenAiChatGptTokenData): Record<string, unknown> => ({
285
+ accessToken: tokenData.accessToken,
286
+ refreshToken: tokenData.refreshToken,
287
+ idToken: tokenData.idToken,
288
+ accountId: tokenData.accountId,
289
+ tokenInfo: tokenData.tokenInfo,
290
+ });
291
+
292
+ const toCodexAuthFile = (current: Record<string, unknown>, tokenData: IOpenAiChatGptTokenData): Record<string, unknown> => ({
293
+ ...current,
294
+ OPENAI_API_KEY: current.OPENAI_API_KEY ?? null,
295
+ tokens: {
296
+ ...(isRecord(current.tokens) ? current.tokens : {}),
297
+ id_token: tokenData.idToken,
298
+ access_token: tokenData.accessToken,
299
+ refresh_token: tokenData.refreshToken,
300
+ account_id: tokenData.accountId ?? tokenData.tokenInfo.chatgptAccountId,
301
+ },
302
+ last_refresh: new Date().toISOString(),
303
+ });
304
+
305
+ const toOpenCodeAuthFile = (current: Record<string, unknown>, tokenData: IOpenAiChatGptTokenData): Record<string, unknown> => ({
306
+ ...current,
307
+ openai: {
308
+ ...(isRecord(current.openai) ? current.openai : {}),
309
+ type: 'oauth',
310
+ refresh: tokenData.refreshToken,
311
+ access: tokenData.accessToken,
312
+ expires: tokenData.tokenInfo.expiresAt ? Date.parse(tokenData.tokenInfo.expiresAt) : undefined,
313
+ accountId: tokenData.accountId ?? tokenData.tokenInfo.chatgptAccountId,
314
+ },
315
+ });
316
+
317
+ const shouldRefreshToken = (tokenData: IOpenAiChatGptTokenData, now: Date): boolean => {
318
+ if (!tokenData.tokenInfo.expiresAt) return false;
319
+ return Date.parse(tokenData.tokenInfo.expiresAt) - now.getTime() < refreshWindowMs;
320
+ };
321
+
322
+ const isExpired = (tokenData: IOpenAiChatGptTokenData, now: Date): boolean => {
323
+ if (!tokenData.tokenInfo.expiresAt) return false;
324
+ return Date.parse(tokenData.tokenInfo.expiresAt) <= now.getTime();
325
+ };
326
+
327
+ const readJsonFileIfExists = async (filePath: string): Promise<Record<string, unknown>> => {
328
+ try {
329
+ const parsed = JSON.parse(await fs.readFile(filePath, 'utf8')) as unknown;
330
+ return isRecord(parsed) ? parsed : {};
331
+ } catch (error) {
332
+ const nodeError = error as NodeJS.ErrnoException;
333
+ if (nodeError.code === 'ENOENT') return {};
334
+ throw error;
335
+ }
336
+ };
337
+
338
+ const writeJsonAtomic = async (filePath: string, payload: unknown): Promise<void> => {
339
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
340
+ await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
341
+ await fs.writeFile(tempPath, `${JSON.stringify(payload, undefined, 2)}\n`, { mode: 0o600 });
342
+ await fs.rename(tempPath, filePath);
343
+ };
344
+
345
+ const isRecord = (value: unknown): value is Record<string, unknown> => {
346
+ return !!value && typeof value === 'object' && !Array.isArray(value);
347
+ };
348
+
349
+ const stringValue = (value: unknown): string | undefined => {
350
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
351
+ };