@mandujs/core 0.53.3 → 0.54.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.
- package/README.md +654 -654
- package/package.json +1 -1
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -451
- package/src/diagnose/checks.ts +832 -832
- package/src/diagnose/index.ts +17 -17
- package/src/diagnose/run.ts +93 -93
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
|
@@ -1,300 +1,300 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ChatGPT OAuth 토큰 관리 — `codex login`이 만든 auth.json을 읽고 자동 refresh.
|
|
3
|
-
*
|
|
4
|
-
* 첫 로그인은 OpenAI 공식 Codex CLI가 담당:
|
|
5
|
-
* `npx @openai/codex login`
|
|
6
|
-
* → 브라우저에서 ChatGPT 로그인 → 토큰이 `~/.codex/auth.json` 또는
|
|
7
|
-
* `~/.chatgpt-local/auth.json`에 저장됨.
|
|
8
|
-
*
|
|
9
|
-
* Mandu 는 그 파일을 읽고, 만료 임박 시 refresh_token으로 자동 갱신.
|
|
10
|
-
*
|
|
11
|
-
* 이 접근의 장점:
|
|
12
|
-
* - Mandu OAuth 앱 등록 불필요. OpenAI 공식 clientId 재사용.
|
|
13
|
-
* - 사용자가 codex CLI 를 이미 설치했다면 로그인 1회로 끝.
|
|
14
|
-
* - 토큰 저장소를 OpenAI 공식 경로와 공유 → 여러 도구가 같은 세션 사용.
|
|
15
|
-
*
|
|
16
|
-
* 포팅 원본: kakao-bot-sdk `src/auth/chatgpt.ts` (EvanZhouDev/openai-oauth
|
|
17
|
-
* MIT 패턴의 단순화 버전).
|
|
18
|
-
*/
|
|
19
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
20
|
-
import { join, dirname } from "node:path";
|
|
21
|
-
import { homedir } from "node:os";
|
|
22
|
-
|
|
23
|
-
const DEFAULT_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
24
|
-
const DEFAULT_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
25
|
-
const REFRESH_EXPIRY_MARGIN_MS = 5 * 60 * 1000;
|
|
26
|
-
|
|
27
|
-
interface StoredTokens {
|
|
28
|
-
id_token?: string;
|
|
29
|
-
access_token?: string;
|
|
30
|
-
refresh_token?: string;
|
|
31
|
-
account_id?: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface AuthFile {
|
|
35
|
-
OPENAI_API_KEY?: string;
|
|
36
|
-
tokens?: StoredTokens;
|
|
37
|
-
last_refresh?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface EffectiveAuth {
|
|
41
|
-
accessToken: string;
|
|
42
|
-
accountId: string;
|
|
43
|
-
idToken?: string;
|
|
44
|
-
refreshToken?: string;
|
|
45
|
-
sourcePath: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Minimal fetch surface — matches `HttpClient` from `./oauth-flow`. */
|
|
49
|
-
export type ChatGPTHttpClient = (
|
|
50
|
-
input: string,
|
|
51
|
-
init?: RequestInit,
|
|
52
|
-
) => Promise<Response>;
|
|
53
|
-
|
|
54
|
-
export interface ChatGPTAuthOptions {
|
|
55
|
-
/** auth.json 경로 명시 override. 기본: 자동 탐색. */
|
|
56
|
-
authFilePath?: string;
|
|
57
|
-
/** OAuth client_id. 기본: ChatGPT 공식 (`app_EMoamEEZ73f0CkXaXp7hrann`). */
|
|
58
|
-
clientId?: string;
|
|
59
|
-
/** Token endpoint. 기본: `https://auth.openai.com/oauth/token`. */
|
|
60
|
-
tokenUrl?: string;
|
|
61
|
-
/** Test 주입용 — 실제 fetch 대신 stub. */
|
|
62
|
-
httpClient?: ChatGPTHttpClient;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export class ChatGPTAuth {
|
|
66
|
-
private readonly clientId: string;
|
|
67
|
-
private readonly tokenUrl: string;
|
|
68
|
-
private readonly httpClient: ChatGPTHttpClient;
|
|
69
|
-
/** 명시된 경로 또는 첫 read 시 결정된 경로. write 시 같은 경로 사용. */
|
|
70
|
-
private resolvedPath: string | null;
|
|
71
|
-
private readonly explicitPath: string | undefined;
|
|
72
|
-
|
|
73
|
-
constructor(options: ChatGPTAuthOptions = {}) {
|
|
74
|
-
this.clientId = options.clientId ?? DEFAULT_CLIENT_ID;
|
|
75
|
-
this.tokenUrl = options.tokenUrl ?? DEFAULT_TOKEN_URL;
|
|
76
|
-
this.httpClient = options.httpClient ?? fetch;
|
|
77
|
-
this.explicitPath = options.authFilePath;
|
|
78
|
-
this.resolvedPath = options.authFilePath ?? null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** 저장된 토큰이 존재하는지 (만료 무관). */
|
|
82
|
-
isAuthenticated(): boolean {
|
|
83
|
-
const data = this.readFile();
|
|
84
|
-
return Boolean(data?.tokens?.access_token);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* 유효한 access_token + account_id 반환. 만료 임박 시 자동 refresh.
|
|
89
|
-
* 토큰 없거나 refresh 실패 시 throw.
|
|
90
|
-
*/
|
|
91
|
-
async getAuth(): Promise<EffectiveAuth> {
|
|
92
|
-
let data = this.readFile();
|
|
93
|
-
if (!data) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
`[mandu brain] OpenAI auth.json not found. Expected at one of:\n${candidatePaths(this.explicitPath).join("\n")}\n` +
|
|
96
|
-
`Run \`npx @openai/codex login\` first (or \`mandu brain login\` which wraps it).`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
let accessToken = data.tokens?.access_token;
|
|
101
|
-
let idToken = data.tokens?.id_token;
|
|
102
|
-
let refreshToken = data.tokens?.refresh_token;
|
|
103
|
-
let accountId = data.tokens?.account_id ?? deriveAccountId(idToken);
|
|
104
|
-
|
|
105
|
-
if (!accessToken) {
|
|
106
|
-
throw new Error(
|
|
107
|
-
"[mandu brain] auth.json has no access_token. Re-run `npx @openai/codex login`.",
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (shouldRefresh(accessToken, data.last_refresh)) {
|
|
112
|
-
if (!refreshToken) {
|
|
113
|
-
throw new Error(
|
|
114
|
-
"[mandu brain] Token expired and no refresh_token available. Re-run `npx @openai/codex login`.",
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
const refreshed = await this.callTokenEndpoint(refreshToken);
|
|
118
|
-
accessToken = refreshed.access_token;
|
|
119
|
-
idToken = refreshed.id_token ?? idToken;
|
|
120
|
-
refreshToken = refreshed.refresh_token ?? refreshToken;
|
|
121
|
-
accountId = deriveAccountId(idToken) ?? accountId;
|
|
122
|
-
|
|
123
|
-
data = {
|
|
124
|
-
...data,
|
|
125
|
-
tokens: {
|
|
126
|
-
id_token: idToken,
|
|
127
|
-
access_token: accessToken,
|
|
128
|
-
refresh_token: refreshToken,
|
|
129
|
-
account_id: accountId,
|
|
130
|
-
},
|
|
131
|
-
last_refresh: new Date().toISOString(),
|
|
132
|
-
};
|
|
133
|
-
this.writeFile(data);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (!accountId) {
|
|
137
|
-
throw new Error(
|
|
138
|
-
"[mandu brain] Could not derive chatgpt_account_id from auth.json. Re-login required.",
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
if (!accessToken) {
|
|
142
|
-
throw new Error(
|
|
143
|
-
"[mandu brain] access_token is null after refresh (unexpected).",
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
accessToken,
|
|
149
|
-
accountId,
|
|
150
|
-
idToken,
|
|
151
|
-
refreshToken,
|
|
152
|
-
sourcePath: this.resolvedPath ?? "(unknown)",
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** 로그인 후 auth.json 이 존재하는 위치 반환 (디버그용). */
|
|
157
|
-
locateAuthFile(): string | null {
|
|
158
|
-
const candidates = candidatePaths(this.explicitPath);
|
|
159
|
-
for (const p of candidates) {
|
|
160
|
-
if (existsSync(p)) return p;
|
|
161
|
-
}
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ─── 내부 ────────────────────────────────────
|
|
166
|
-
|
|
167
|
-
private readFile(): AuthFile | null {
|
|
168
|
-
const candidates = candidatePaths(this.explicitPath);
|
|
169
|
-
for (const p of candidates) {
|
|
170
|
-
try {
|
|
171
|
-
if (!existsSync(p)) continue;
|
|
172
|
-
const text = readFileSync(p, "utf-8");
|
|
173
|
-
const parsed = JSON.parse(text) as AuthFile;
|
|
174
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
175
|
-
this.resolvedPath = p;
|
|
176
|
-
return parsed;
|
|
177
|
-
}
|
|
178
|
-
} catch {
|
|
179
|
-
// 다음 candidate 시도
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private writeFile(data: AuthFile): void {
|
|
186
|
-
const path = this.resolvedPath ?? this.explicitPath ?? defaultWritePath();
|
|
187
|
-
const dir = dirname(path);
|
|
188
|
-
if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
189
|
-
const tmp = `${path}.tmp`;
|
|
190
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2), {
|
|
191
|
-
encoding: "utf-8",
|
|
192
|
-
mode: 0o600,
|
|
193
|
-
});
|
|
194
|
-
renameSync(tmp, path);
|
|
195
|
-
this.resolvedPath = path;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
private async callTokenEndpoint(refreshToken: string): Promise<StoredTokens> {
|
|
199
|
-
const resp = await this.httpClient(this.tokenUrl, {
|
|
200
|
-
method: "POST",
|
|
201
|
-
headers: { "content-type": "application/json" },
|
|
202
|
-
body: JSON.stringify({
|
|
203
|
-
grant_type: "refresh_token",
|
|
204
|
-
refresh_token: refreshToken,
|
|
205
|
-
client_id: this.clientId,
|
|
206
|
-
scope: "openid profile email offline_access",
|
|
207
|
-
}),
|
|
208
|
-
});
|
|
209
|
-
const text = await resp.text();
|
|
210
|
-
if (!resp.ok) {
|
|
211
|
-
throw new Error(`Token refresh ${resp.status}: ${text.slice(0, 300)}`);
|
|
212
|
-
}
|
|
213
|
-
let json: { access_token?: string; refresh_token?: string; id_token?: string };
|
|
214
|
-
try {
|
|
215
|
-
json = JSON.parse(text);
|
|
216
|
-
} catch {
|
|
217
|
-
throw new Error(`Token refresh: invalid JSON: ${text.slice(0, 200)}`);
|
|
218
|
-
}
|
|
219
|
-
if (!json.access_token) {
|
|
220
|
-
throw new Error(`Token refresh: missing access_token: ${text}`);
|
|
221
|
-
}
|
|
222
|
-
return {
|
|
223
|
-
access_token: json.access_token,
|
|
224
|
-
refresh_token: json.refresh_token,
|
|
225
|
-
id_token: json.id_token,
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ─── 헬퍼 ─────────────────────────────────────
|
|
231
|
-
|
|
232
|
-
function candidatePaths(explicit?: string): string[] {
|
|
233
|
-
if (explicit) return [explicit];
|
|
234
|
-
const out: string[] = [];
|
|
235
|
-
const env1 = process.env["CHATGPT_LOCAL_HOME"];
|
|
236
|
-
const env2 = process.env["CODEX_HOME"];
|
|
237
|
-
if (env1) out.push(join(env1, "auth.json"));
|
|
238
|
-
if (env2) out.push(join(env2, "auth.json"));
|
|
239
|
-
out.push(join(homedir(), ".chatgpt-local", "auth.json"));
|
|
240
|
-
out.push(join(homedir(), ".codex", "auth.json"));
|
|
241
|
-
return [...new Set(out)];
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function defaultWritePath(): string {
|
|
245
|
-
return process.env["CHATGPT_LOCAL_HOME"]
|
|
246
|
-
? join(process.env["CHATGPT_LOCAL_HOME"]!, "auth.json")
|
|
247
|
-
: process.env["CODEX_HOME"]
|
|
248
|
-
? join(process.env["CODEX_HOME"]!, "auth.json")
|
|
249
|
-
: join(homedir(), ".chatgpt-local", "auth.json");
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function deriveAccountId(idToken?: string): string | undefined {
|
|
253
|
-
if (!idToken || !idToken.includes(".")) return undefined;
|
|
254
|
-
const parts = idToken.split(".");
|
|
255
|
-
if (parts.length < 2 || !parts[1]) return undefined;
|
|
256
|
-
try {
|
|
257
|
-
const padded = parts[1] + "=".repeat(((-parts[1].length % 4) + 4) % 4);
|
|
258
|
-
const payload = JSON.parse(
|
|
259
|
-
Buffer.from(padded, "base64url").toString("utf-8"),
|
|
260
|
-
) as Record<string, unknown>;
|
|
261
|
-
const auth = payload["https://api.openai.com/auth"];
|
|
262
|
-
if (typeof auth === "object" && auth !== null && "chatgpt_account_id" in auth) {
|
|
263
|
-
const id = (auth as Record<string, unknown>)["chatgpt_account_id"];
|
|
264
|
-
if (typeof id === "string" && id.length > 0) return id;
|
|
265
|
-
}
|
|
266
|
-
} catch {
|
|
267
|
-
// ignore
|
|
268
|
-
}
|
|
269
|
-
return undefined;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function shouldRefresh(accessToken: string, lastRefreshIso: string | undefined): boolean {
|
|
273
|
-
// JWT exp 클레임으로 우선 판단
|
|
274
|
-
const claims = parseJwtClaims(accessToken);
|
|
275
|
-
if (claims && typeof claims["exp"] === "number") {
|
|
276
|
-
const expMs = (claims["exp"] as number) * 1000;
|
|
277
|
-
if (expMs <= Date.now() + REFRESH_EXPIRY_MARGIN_MS) return true;
|
|
278
|
-
}
|
|
279
|
-
// last_refresh 기반 휴리스틱 (55분 초과)
|
|
280
|
-
if (lastRefreshIso) {
|
|
281
|
-
const last = Date.parse(lastRefreshIso);
|
|
282
|
-
if (!Number.isNaN(last) && Date.now() - last > 55 * 60 * 1000) return true;
|
|
283
|
-
}
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function parseJwtClaims(token: string): Record<string, unknown> | undefined {
|
|
288
|
-
if (!token.includes(".")) return undefined;
|
|
289
|
-
const parts = token.split(".");
|
|
290
|
-
if (parts.length !== 3 || !parts[1]) return undefined;
|
|
291
|
-
try {
|
|
292
|
-
const padded = parts[1] + "=".repeat(((-parts[1].length % 4) + 4) % 4);
|
|
293
|
-
const payload = JSON.parse(Buffer.from(padded, "base64url").toString("utf-8"));
|
|
294
|
-
return typeof payload === "object" && payload !== null
|
|
295
|
-
? (payload as Record<string, unknown>)
|
|
296
|
-
: undefined;
|
|
297
|
-
} catch {
|
|
298
|
-
return undefined;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* ChatGPT OAuth 토큰 관리 — `codex login`이 만든 auth.json을 읽고 자동 refresh.
|
|
3
|
+
*
|
|
4
|
+
* 첫 로그인은 OpenAI 공식 Codex CLI가 담당:
|
|
5
|
+
* `npx @openai/codex login`
|
|
6
|
+
* → 브라우저에서 ChatGPT 로그인 → 토큰이 `~/.codex/auth.json` 또는
|
|
7
|
+
* `~/.chatgpt-local/auth.json`에 저장됨.
|
|
8
|
+
*
|
|
9
|
+
* Mandu 는 그 파일을 읽고, 만료 임박 시 refresh_token으로 자동 갱신.
|
|
10
|
+
*
|
|
11
|
+
* 이 접근의 장점:
|
|
12
|
+
* - Mandu OAuth 앱 등록 불필요. OpenAI 공식 clientId 재사용.
|
|
13
|
+
* - 사용자가 codex CLI 를 이미 설치했다면 로그인 1회로 끝.
|
|
14
|
+
* - 토큰 저장소를 OpenAI 공식 경로와 공유 → 여러 도구가 같은 세션 사용.
|
|
15
|
+
*
|
|
16
|
+
* 포팅 원본: kakao-bot-sdk `src/auth/chatgpt.ts` (EvanZhouDev/openai-oauth
|
|
17
|
+
* MIT 패턴의 단순화 버전).
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
20
|
+
import { join, dirname } from "node:path";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
|
|
23
|
+
const DEFAULT_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
24
|
+
const DEFAULT_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
25
|
+
const REFRESH_EXPIRY_MARGIN_MS = 5 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
interface StoredTokens {
|
|
28
|
+
id_token?: string;
|
|
29
|
+
access_token?: string;
|
|
30
|
+
refresh_token?: string;
|
|
31
|
+
account_id?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface AuthFile {
|
|
35
|
+
OPENAI_API_KEY?: string;
|
|
36
|
+
tokens?: StoredTokens;
|
|
37
|
+
last_refresh?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface EffectiveAuth {
|
|
41
|
+
accessToken: string;
|
|
42
|
+
accountId: string;
|
|
43
|
+
idToken?: string;
|
|
44
|
+
refreshToken?: string;
|
|
45
|
+
sourcePath: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Minimal fetch surface — matches `HttpClient` from `./oauth-flow`. */
|
|
49
|
+
export type ChatGPTHttpClient = (
|
|
50
|
+
input: string,
|
|
51
|
+
init?: RequestInit,
|
|
52
|
+
) => Promise<Response>;
|
|
53
|
+
|
|
54
|
+
export interface ChatGPTAuthOptions {
|
|
55
|
+
/** auth.json 경로 명시 override. 기본: 자동 탐색. */
|
|
56
|
+
authFilePath?: string;
|
|
57
|
+
/** OAuth client_id. 기본: ChatGPT 공식 (`app_EMoamEEZ73f0CkXaXp7hrann`). */
|
|
58
|
+
clientId?: string;
|
|
59
|
+
/** Token endpoint. 기본: `https://auth.openai.com/oauth/token`. */
|
|
60
|
+
tokenUrl?: string;
|
|
61
|
+
/** Test 주입용 — 실제 fetch 대신 stub. */
|
|
62
|
+
httpClient?: ChatGPTHttpClient;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class ChatGPTAuth {
|
|
66
|
+
private readonly clientId: string;
|
|
67
|
+
private readonly tokenUrl: string;
|
|
68
|
+
private readonly httpClient: ChatGPTHttpClient;
|
|
69
|
+
/** 명시된 경로 또는 첫 read 시 결정된 경로. write 시 같은 경로 사용. */
|
|
70
|
+
private resolvedPath: string | null;
|
|
71
|
+
private readonly explicitPath: string | undefined;
|
|
72
|
+
|
|
73
|
+
constructor(options: ChatGPTAuthOptions = {}) {
|
|
74
|
+
this.clientId = options.clientId ?? DEFAULT_CLIENT_ID;
|
|
75
|
+
this.tokenUrl = options.tokenUrl ?? DEFAULT_TOKEN_URL;
|
|
76
|
+
this.httpClient = options.httpClient ?? fetch;
|
|
77
|
+
this.explicitPath = options.authFilePath;
|
|
78
|
+
this.resolvedPath = options.authFilePath ?? null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 저장된 토큰이 존재하는지 (만료 무관). */
|
|
82
|
+
isAuthenticated(): boolean {
|
|
83
|
+
const data = this.readFile();
|
|
84
|
+
return Boolean(data?.tokens?.access_token);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 유효한 access_token + account_id 반환. 만료 임박 시 자동 refresh.
|
|
89
|
+
* 토큰 없거나 refresh 실패 시 throw.
|
|
90
|
+
*/
|
|
91
|
+
async getAuth(): Promise<EffectiveAuth> {
|
|
92
|
+
let data = this.readFile();
|
|
93
|
+
if (!data) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`[mandu brain] OpenAI auth.json not found. Expected at one of:\n${candidatePaths(this.explicitPath).join("\n")}\n` +
|
|
96
|
+
`Run \`npx @openai/codex login\` first (or \`mandu brain login\` which wraps it).`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let accessToken = data.tokens?.access_token;
|
|
101
|
+
let idToken = data.tokens?.id_token;
|
|
102
|
+
let refreshToken = data.tokens?.refresh_token;
|
|
103
|
+
let accountId = data.tokens?.account_id ?? deriveAccountId(idToken);
|
|
104
|
+
|
|
105
|
+
if (!accessToken) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
"[mandu brain] auth.json has no access_token. Re-run `npx @openai/codex login`.",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (shouldRefresh(accessToken, data.last_refresh)) {
|
|
112
|
+
if (!refreshToken) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
"[mandu brain] Token expired and no refresh_token available. Re-run `npx @openai/codex login`.",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const refreshed = await this.callTokenEndpoint(refreshToken);
|
|
118
|
+
accessToken = refreshed.access_token;
|
|
119
|
+
idToken = refreshed.id_token ?? idToken;
|
|
120
|
+
refreshToken = refreshed.refresh_token ?? refreshToken;
|
|
121
|
+
accountId = deriveAccountId(idToken) ?? accountId;
|
|
122
|
+
|
|
123
|
+
data = {
|
|
124
|
+
...data,
|
|
125
|
+
tokens: {
|
|
126
|
+
id_token: idToken,
|
|
127
|
+
access_token: accessToken,
|
|
128
|
+
refresh_token: refreshToken,
|
|
129
|
+
account_id: accountId,
|
|
130
|
+
},
|
|
131
|
+
last_refresh: new Date().toISOString(),
|
|
132
|
+
};
|
|
133
|
+
this.writeFile(data);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!accountId) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
"[mandu brain] Could not derive chatgpt_account_id from auth.json. Re-login required.",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (!accessToken) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
"[mandu brain] access_token is null after refresh (unexpected).",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
accessToken,
|
|
149
|
+
accountId,
|
|
150
|
+
idToken,
|
|
151
|
+
refreshToken,
|
|
152
|
+
sourcePath: this.resolvedPath ?? "(unknown)",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 로그인 후 auth.json 이 존재하는 위치 반환 (디버그용). */
|
|
157
|
+
locateAuthFile(): string | null {
|
|
158
|
+
const candidates = candidatePaths(this.explicitPath);
|
|
159
|
+
for (const p of candidates) {
|
|
160
|
+
if (existsSync(p)) return p;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── 내부 ────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
private readFile(): AuthFile | null {
|
|
168
|
+
const candidates = candidatePaths(this.explicitPath);
|
|
169
|
+
for (const p of candidates) {
|
|
170
|
+
try {
|
|
171
|
+
if (!existsSync(p)) continue;
|
|
172
|
+
const text = readFileSync(p, "utf-8");
|
|
173
|
+
const parsed = JSON.parse(text) as AuthFile;
|
|
174
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
175
|
+
this.resolvedPath = p;
|
|
176
|
+
return parsed;
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
// 다음 candidate 시도
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private writeFile(data: AuthFile): void {
|
|
186
|
+
const path = this.resolvedPath ?? this.explicitPath ?? defaultWritePath();
|
|
187
|
+
const dir = dirname(path);
|
|
188
|
+
if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
189
|
+
const tmp = `${path}.tmp`;
|
|
190
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), {
|
|
191
|
+
encoding: "utf-8",
|
|
192
|
+
mode: 0o600,
|
|
193
|
+
});
|
|
194
|
+
renameSync(tmp, path);
|
|
195
|
+
this.resolvedPath = path;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private async callTokenEndpoint(refreshToken: string): Promise<StoredTokens> {
|
|
199
|
+
const resp = await this.httpClient(this.tokenUrl, {
|
|
200
|
+
method: "POST",
|
|
201
|
+
headers: { "content-type": "application/json" },
|
|
202
|
+
body: JSON.stringify({
|
|
203
|
+
grant_type: "refresh_token",
|
|
204
|
+
refresh_token: refreshToken,
|
|
205
|
+
client_id: this.clientId,
|
|
206
|
+
scope: "openid profile email offline_access",
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
const text = await resp.text();
|
|
210
|
+
if (!resp.ok) {
|
|
211
|
+
throw new Error(`Token refresh ${resp.status}: ${text.slice(0, 300)}`);
|
|
212
|
+
}
|
|
213
|
+
let json: { access_token?: string; refresh_token?: string; id_token?: string };
|
|
214
|
+
try {
|
|
215
|
+
json = JSON.parse(text);
|
|
216
|
+
} catch {
|
|
217
|
+
throw new Error(`Token refresh: invalid JSON: ${text.slice(0, 200)}`);
|
|
218
|
+
}
|
|
219
|
+
if (!json.access_token) {
|
|
220
|
+
throw new Error(`Token refresh: missing access_token: ${text}`);
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
access_token: json.access_token,
|
|
224
|
+
refresh_token: json.refresh_token,
|
|
225
|
+
id_token: json.id_token,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── 헬퍼 ─────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
function candidatePaths(explicit?: string): string[] {
|
|
233
|
+
if (explicit) return [explicit];
|
|
234
|
+
const out: string[] = [];
|
|
235
|
+
const env1 = process.env["CHATGPT_LOCAL_HOME"];
|
|
236
|
+
const env2 = process.env["CODEX_HOME"];
|
|
237
|
+
if (env1) out.push(join(env1, "auth.json"));
|
|
238
|
+
if (env2) out.push(join(env2, "auth.json"));
|
|
239
|
+
out.push(join(homedir(), ".chatgpt-local", "auth.json"));
|
|
240
|
+
out.push(join(homedir(), ".codex", "auth.json"));
|
|
241
|
+
return [...new Set(out)];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function defaultWritePath(): string {
|
|
245
|
+
return process.env["CHATGPT_LOCAL_HOME"]
|
|
246
|
+
? join(process.env["CHATGPT_LOCAL_HOME"]!, "auth.json")
|
|
247
|
+
: process.env["CODEX_HOME"]
|
|
248
|
+
? join(process.env["CODEX_HOME"]!, "auth.json")
|
|
249
|
+
: join(homedir(), ".chatgpt-local", "auth.json");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function deriveAccountId(idToken?: string): string | undefined {
|
|
253
|
+
if (!idToken || !idToken.includes(".")) return undefined;
|
|
254
|
+
const parts = idToken.split(".");
|
|
255
|
+
if (parts.length < 2 || !parts[1]) return undefined;
|
|
256
|
+
try {
|
|
257
|
+
const padded = parts[1] + "=".repeat(((-parts[1].length % 4) + 4) % 4);
|
|
258
|
+
const payload = JSON.parse(
|
|
259
|
+
Buffer.from(padded, "base64url").toString("utf-8"),
|
|
260
|
+
) as Record<string, unknown>;
|
|
261
|
+
const auth = payload["https://api.openai.com/auth"];
|
|
262
|
+
if (typeof auth === "object" && auth !== null && "chatgpt_account_id" in auth) {
|
|
263
|
+
const id = (auth as Record<string, unknown>)["chatgpt_account_id"];
|
|
264
|
+
if (typeof id === "string" && id.length > 0) return id;
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
// ignore
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function shouldRefresh(accessToken: string, lastRefreshIso: string | undefined): boolean {
|
|
273
|
+
// JWT exp 클레임으로 우선 판단
|
|
274
|
+
const claims = parseJwtClaims(accessToken);
|
|
275
|
+
if (claims && typeof claims["exp"] === "number") {
|
|
276
|
+
const expMs = (claims["exp"] as number) * 1000;
|
|
277
|
+
if (expMs <= Date.now() + REFRESH_EXPIRY_MARGIN_MS) return true;
|
|
278
|
+
}
|
|
279
|
+
// last_refresh 기반 휴리스틱 (55분 초과)
|
|
280
|
+
if (lastRefreshIso) {
|
|
281
|
+
const last = Date.parse(lastRefreshIso);
|
|
282
|
+
if (!Number.isNaN(last) && Date.now() - last > 55 * 60 * 1000) return true;
|
|
283
|
+
}
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function parseJwtClaims(token: string): Record<string, unknown> | undefined {
|
|
288
|
+
if (!token.includes(".")) return undefined;
|
|
289
|
+
const parts = token.split(".");
|
|
290
|
+
if (parts.length !== 3 || !parts[1]) return undefined;
|
|
291
|
+
try {
|
|
292
|
+
const padded = parts[1] + "=".repeat(((-parts[1].length % 4) + 4) % 4);
|
|
293
|
+
const payload = JSON.parse(Buffer.from(padded, "base64url").toString("utf-8"));
|
|
294
|
+
return typeof payload === "object" && payload !== null
|
|
295
|
+
? (payload as Record<string, unknown>)
|
|
296
|
+
: undefined;
|
|
297
|
+
} catch {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
}
|