@glbgpt/cli 0.1.0-test.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 +111 -0
- package/dist/src/auth/authClient.d.ts +32 -0
- package/dist/src/auth/authClient.js +88 -0
- package/dist/src/auth/authClient.js.map +1 -0
- package/dist/src/auth/credentialStore.d.ts +22 -0
- package/dist/src/auth/credentialStore.js +51 -0
- package/dist/src/auth/credentialStore.js.map +1 -0
- package/dist/src/auth/loginFlow.d.ts +30 -0
- package/dist/src/auth/loginFlow.js +43 -0
- package/dist/src/auth/loginFlow.js.map +1 -0
- package/dist/src/auth/loopbackServer.d.ts +41 -0
- package/dist/src/auth/loopbackServer.js +141 -0
- package/dist/src/auth/loopbackServer.js.map +1 -0
- package/dist/src/auth/token.d.ts +8 -0
- package/dist/src/auth/token.js +15 -0
- package/dist/src/auth/token.js.map +1 -0
- package/dist/src/commands/accountCommand.d.ts +2 -0
- package/dist/src/commands/accountCommand.js +56 -0
- package/dist/src/commands/accountCommand.js.map +1 -0
- package/dist/src/commands/configCommand.d.ts +4 -0
- package/dist/src/commands/configCommand.js +53 -0
- package/dist/src/commands/configCommand.js.map +1 -0
- package/dist/src/commands/execCommand.d.ts +32 -0
- package/dist/src/commands/execCommand.js +122 -0
- package/dist/src/commands/execCommand.js.map +1 -0
- package/dist/src/commands/loginCommand.d.ts +11 -0
- package/dist/src/commands/loginCommand.js +96 -0
- package/dist/src/commands/loginCommand.js.map +1 -0
- package/dist/src/commands/mediaCommand.d.ts +41 -0
- package/dist/src/commands/mediaCommand.js +277 -0
- package/dist/src/commands/mediaCommand.js.map +1 -0
- package/dist/src/config/configFile.d.ts +30 -0
- package/dist/src/config/configFile.js +54 -0
- package/dist/src/config/configFile.js.map +1 -0
- package/dist/src/config/env.d.ts +6 -0
- package/dist/src/config/env.js +24 -0
- package/dist/src/config/env.js.map +1 -0
- package/dist/src/config/paths.d.ts +6 -0
- package/dist/src/config/paths.js +16 -0
- package/dist/src/config/paths.js.map +1 -0
- package/dist/src/core/catalog.d.ts +49 -0
- package/dist/src/core/catalog.js +96 -0
- package/dist/src/core/catalog.js.map +1 -0
- package/dist/src/core/httpChatBackend.d.ts +31 -0
- package/dist/src/core/httpChatBackend.js +134 -0
- package/dist/src/core/httpChatBackend.js.map +1 -0
- package/dist/src/core/id.d.ts +2 -0
- package/dist/src/core/id.js +5 -0
- package/dist/src/core/id.js.map +1 -0
- package/dist/src/core/runChat.d.ts +28 -0
- package/dist/src/core/runChat.js +49 -0
- package/dist/src/core/runChat.js.map +1 -0
- package/dist/src/core/session.d.ts +44 -0
- package/dist/src/core/session.js +70 -0
- package/dist/src/core/session.js.map +1 -0
- package/dist/src/core/sessionStore.d.ts +39 -0
- package/dist/src/core/sessionStore.js +106 -0
- package/dist/src/core/sessionStore.js.map +1 -0
- package/dist/src/core/toolRegistry.d.ts +19 -0
- package/dist/src/core/toolRegistry.js +53 -0
- package/dist/src/core/toolRegistry.js.map +1 -0
- package/dist/src/core/tools/generateImage.d.ts +10 -0
- package/dist/src/core/tools/generateImage.js +34 -0
- package/dist/src/core/tools/generateImage.js.map +1 -0
- package/dist/src/core/turnLoop.d.ts +31 -0
- package/dist/src/core/turnLoop.js +155 -0
- package/dist/src/core/turnLoop.js.map +1 -0
- package/dist/src/core/types.d.ts +137 -0
- package/dist/src/core/types.js +9 -0
- package/dist/src/core/types.js.map +1 -0
- package/dist/src/exec/input.d.ts +35 -0
- package/dist/src/exec/input.js +85 -0
- package/dist/src/exec/input.js.map +1 -0
- package/dist/src/exec/output.d.ts +39 -0
- package/dist/src/exec/output.js +84 -0
- package/dist/src/exec/output.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +253 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcp/server.d.ts +38 -0
- package/dist/src/mcp/server.js +99 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/media/aiApiClient.d.ts +53 -0
- package/dist/src/media/aiApiClient.js +97 -0
- package/dist/src/media/aiApiClient.js.map +1 -0
- package/dist/src/media/errors.d.ts +7 -0
- package/dist/src/media/errors.js +70 -0
- package/dist/src/media/errors.js.map +1 -0
- package/dist/src/media/executor.d.ts +50 -0
- package/dist/src/media/executor.js +55 -0
- package/dist/src/media/executor.js.map +1 -0
- package/dist/src/media/generateImageTool.d.ts +19 -0
- package/dist/src/media/generateImageTool.js +120 -0
- package/dist/src/media/generateImageTool.js.map +1 -0
- package/dist/src/media/manifest.d.ts +23 -0
- package/dist/src/media/manifest.js +558 -0
- package/dist/src/media/manifest.js.map +1 -0
- package/dist/src/media/resolution.d.ts +13 -0
- package/dist/src/media/resolution.js +42 -0
- package/dist/src/media/resolution.js.map +1 -0
- package/dist/src/media/taskLedger.d.ts +17 -0
- package/dist/src/media/taskLedger.js +44 -0
- package/dist/src/media/taskLedger.js.map +1 -0
- package/dist/src/media/taskManager.d.ts +52 -0
- package/dist/src/media/taskManager.js +130 -0
- package/dist/src/media/taskManager.js.map +1 -0
- package/dist/src/media/types.d.ts +80 -0
- package/dist/src/media/types.js +15 -0
- package/dist/src/media/types.js.map +1 -0
- package/dist/src/tui/App.d.ts +34 -0
- package/dist/src/tui/App.js +540 -0
- package/dist/src/tui/App.js.map +1 -0
- package/dist/src/tui/MultilineInput.d.ts +36 -0
- package/dist/src/tui/MultilineInput.js +221 -0
- package/dist/src/tui/MultilineInput.js.map +1 -0
- package/dist/src/tui/atPopup.d.ts +21 -0
- package/dist/src/tui/atPopup.js +55 -0
- package/dist/src/tui/atPopup.js.map +1 -0
- package/dist/src/tui/attachments.d.ts +24 -0
- package/dist/src/tui/attachments.js +91 -0
- package/dist/src/tui/attachments.js.map +1 -0
- package/dist/src/tui/branding.d.ts +22 -0
- package/dist/src/tui/branding.js +119 -0
- package/dist/src/tui/branding.js.map +1 -0
- package/dist/src/tui/chatController.d.ts +188 -0
- package/dist/src/tui/chatController.js +614 -0
- package/dist/src/tui/chatController.js.map +1 -0
- package/dist/src/tui/commands.d.ts +13 -0
- package/dist/src/tui/commands.js +22 -0
- package/dist/src/tui/commands.js.map +1 -0
- package/dist/src/tui/confirmationBridge.d.ts +23 -0
- package/dist/src/tui/confirmationBridge.js +47 -0
- package/dist/src/tui/confirmationBridge.js.map +1 -0
- package/dist/src/tui/inputHistory.d.ts +7 -0
- package/dist/src/tui/inputHistory.js +64 -0
- package/dist/src/tui/inputHistory.js.map +1 -0
- package/dist/src/tui/markdown.d.ts +1 -0
- package/dist/src/tui/markdown.js +143 -0
- package/dist/src/tui/markdown.js.map +1 -0
- package/dist/src/tui/models.d.ts +30 -0
- package/dist/src/tui/models.js +54 -0
- package/dist/src/tui/models.js.map +1 -0
- package/dist/src/tui/run.d.ts +29 -0
- package/dist/src/tui/run.js +212 -0
- package/dist/src/tui/run.js.map +1 -0
- package/dist/src/tui/slashMedia.d.ts +36 -0
- package/dist/src/tui/slashMedia.js +86 -0
- package/dist/src/tui/slashMedia.js.map +1 -0
- package/dist/src/tui/visualLines.d.ts +38 -0
- package/dist/src/tui/visualLines.js +110 -0
- package/dist/src/tui/visualLines.js.map +1 -0
- package/dist/src/util/clipboardImage.d.ts +6 -0
- package/dist/src/util/clipboardImage.js +69 -0
- package/dist/src/util/clipboardImage.js.map +1 -0
- package/dist/src/util/hyperlink.d.ts +10 -0
- package/dist/src/util/hyperlink.js +33 -0
- package/dist/src/util/hyperlink.js.map +1 -0
- package/dist/src/util/imageRef.d.ts +15 -0
- package/dist/src/util/imageRef.js +46 -0
- package/dist/src/util/imageRef.js.map +1 -0
- package/dist/src/util/inlineImage.d.ts +13 -0
- package/dist/src/util/inlineImage.js +56 -0
- package/dist/src/util/inlineImage.js.map +1 -0
- package/dist/src/util/openBrowser.d.ts +5 -0
- package/dist/src/util/openBrowser.js +23 -0
- package/dist/src/util/openBrowser.js.map +1 -0
- package/dist/src/util/retry.d.ts +43 -0
- package/dist/src/util/retry.js +116 -0
- package/dist/src/util/retry.js.map +1 -0
- package/dist/src/version.d.ts +6 -0
- package/dist/src/version.js +16 -0
- package/dist/src/version.js.map +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# GlobalGPT CLI
|
|
2
|
+
|
|
3
|
+
GlobalGPT in your terminal — chat, image and video generation, and an `exec` entry point for
|
|
4
|
+
scripts and other agents. You sign in with your GlobalGPT account (no API key); billing is your
|
|
5
|
+
normal account balance.
|
|
6
|
+
|
|
7
|
+
> Test build (`0.1.0-test.x`), pointing at `https://tw.broly.ai`. Requires Node.js ≥ 18.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @glbgpt/cli@test
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Installs two interchangeable commands: **`glbgpt`** and **`globalgpt`**.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
glbgpt login # authorize in your browser
|
|
19
|
+
glbgpt # start chatting
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
Run `glbgpt <command> --help` for the full flags of any command.
|
|
25
|
+
|
|
26
|
+
### Chat
|
|
27
|
+
|
|
28
|
+
| Command | What it does |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `glbgpt` | Start the interactive chat TUI. |
|
|
31
|
+
| `glbgpt "<prompt>"` | Start the TUI with a first message. |
|
|
32
|
+
| `glbgpt resume last` · `glbgpt resume <id>` | Resume a previous conversation. |
|
|
33
|
+
|
|
34
|
+
Inside the TUI, type `/` for commands: `/model` `/models` `/image` `/video` `/tasks` `/status`
|
|
35
|
+
`/account` `/transactions` `/cost` `/login` `/logout` `/new` `/resume` `/help` `/exit`.
|
|
36
|
+
Attach images with `@image` (last generation), `Ctrl+V` or `@clipboard`, or `@./photo.png`.
|
|
37
|
+
Multi-line: end a line with `\` then Enter (or Ctrl+J); plain Enter sends; Esc interrupts.
|
|
38
|
+
|
|
39
|
+
### Account
|
|
40
|
+
|
|
41
|
+
| Command | What it does |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `glbgpt login [--no-browser] [--code]` | Sign in. `--no-browser` prints the URL; `--code` pastes a one-time code (headless/SSH). |
|
|
44
|
+
| `glbgpt logout` | Sign out and remove stored credentials. |
|
|
45
|
+
| `glbgpt whoami` | Show the signed-in account. |
|
|
46
|
+
| `glbgpt account` | Membership and credit balance. |
|
|
47
|
+
| `glbgpt account transactions` | Recent spend (authoritative billing). |
|
|
48
|
+
|
|
49
|
+
### Images and video
|
|
50
|
+
|
|
51
|
+
| Command | What it does |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `glbgpt image "<prompt>"` | Generate an image (asks before spending). |
|
|
54
|
+
| `glbgpt video "<prompt>"` | Generate a video (asks before spending). |
|
|
55
|
+
|
|
56
|
+
Common flags: `-m/--model`, `--ar 16:9`, `--res 2K`, `--seed N`, `--negative-prompt "..."`,
|
|
57
|
+
`-i ./ref.png` (reference image), `--duration N` and `--audio/--no-audio` (video),
|
|
58
|
+
`-o/--out <dir>`, `--estimate` (price only, don't generate), `--detach` (submit and print the
|
|
59
|
+
task id without waiting).
|
|
60
|
+
|
|
61
|
+
### Models and tasks
|
|
62
|
+
|
|
63
|
+
| Command | What it does |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `glbgpt model list [chat\|image\|video]` | List available models. |
|
|
66
|
+
| `glbgpt model show <model>` | Parameters and limits for one model. |
|
|
67
|
+
| `glbgpt task list` | Recent generation tasks. |
|
|
68
|
+
| `glbgpt task show last` · `glbgpt task show <id>` | A task's status. |
|
|
69
|
+
| `glbgpt task wait last` | Block until a task finishes. |
|
|
70
|
+
| `glbgpt task download last` | Download a finished result. |
|
|
71
|
+
|
|
72
|
+
### exec — non-interactive
|
|
73
|
+
|
|
74
|
+
One request in, the model's answer out. For scripts and external agents; never opens the TUI.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
glbgpt exec "capital of France? one word" # plain text on stdout
|
|
78
|
+
echo "summarize this" | glbgpt exec - # prompt from stdin
|
|
79
|
+
glbgpt exec "what is in this image?" -i shot.png
|
|
80
|
+
glbgpt exec "extract facts" --json # one result object
|
|
81
|
+
glbgpt exec "say hi" --jsonl # streaming events, one JSON per line
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Flags: `-m/--model`, `--system <text>`, `--file <path...>`, `-i/--image <path|url...>`,
|
|
85
|
+
`--output-schema <file>` (structured output), `--json`, `--jsonl`, `-o/--output-last-message <file>`.
|
|
86
|
+
Exit code is `0` on success, non-zero on a usage/auth/turn error.
|
|
87
|
+
|
|
88
|
+
### mcp — use from other agents
|
|
89
|
+
|
|
90
|
+
`glbgpt mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server over stdio,
|
|
91
|
+
exposing a `glbgpt_chat` tool. Register it with any MCP client, e.g. Claude Code:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
claude mcp add glbgpt -- glbgpt mcp
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### config
|
|
98
|
+
|
|
99
|
+
| Command | What it does |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `glbgpt config list` | Show persisted settings. |
|
|
102
|
+
| `glbgpt config get <key>` · `set <key> <value>` · `unset <key>` | Manage settings. |
|
|
103
|
+
|
|
104
|
+
Keys: `api-base-url`, `web-base-url`, `lang`. One-shot overrides via env vars
|
|
105
|
+
(`GLBGPT_BASE_URL`, `GLBGPT_WEB_URL`, `GLBGPT_LANG`, `GLBGPT_TOKEN`) always win.
|
|
106
|
+
|
|
107
|
+
## Notes
|
|
108
|
+
|
|
109
|
+
Credentials are stored at `~/.config/glbgpt/credentials.json` (dir `0700`, file `0600`). The
|
|
110
|
+
token is long-lived; on a 401 the CLI asks you to `glbgpt login` again. This is a pre-release
|
|
111
|
+
test build — please report anything that looks wrong.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface UserInfo {
|
|
2
|
+
email?: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
balance?: number | string;
|
|
5
|
+
plan?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface BillRecord {
|
|
8
|
+
model: string;
|
|
9
|
+
/** Cost in USD (1 credit ≈ $0.001). */
|
|
10
|
+
cost: number;
|
|
11
|
+
accountType?: string;
|
|
12
|
+
createdAt?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface AuthClientOptions {
|
|
15
|
+
/** GlobalGPT API base, e.g. https://testtapi2.broly.ai. */
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
fetchImpl?: typeof fetch;
|
|
18
|
+
}
|
|
19
|
+
export declare class AuthClient {
|
|
20
|
+
private readonly baseUrl;
|
|
21
|
+
private readonly fetchImpl;
|
|
22
|
+
private readonly host;
|
|
23
|
+
constructor(opts: AuthClientOptions);
|
|
24
|
+
/** fetch against the account API, with a friendly message on a transient network failure. */
|
|
25
|
+
private request;
|
|
26
|
+
/** Exchange a one-time code (temp token) for the real JWT. */
|
|
27
|
+
exchange(code: string): Promise<string>;
|
|
28
|
+
/** Fetch recent billing records (for `account transactions`). */
|
|
29
|
+
bills(token: string, page?: number, size?: number): Promise<BillRecord[]>;
|
|
30
|
+
/** Fetch the signed-in user's info (for whoami and the post-login greeting). */
|
|
31
|
+
userInfo(token: string): Promise<UserInfo>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Talks to the GlobalGPT account API (/user-api/*, chatpal deployment) for the login flow.
|
|
3
|
+
*
|
|
4
|
+
* Response envelope is `{ code, message, data }` with success `code === 1` (HTTP is typically 200
|
|
5
|
+
* even for business errors). We parse defensively: success is decided by the presence of the
|
|
6
|
+
* expected payload, not solely the code.
|
|
7
|
+
*/
|
|
8
|
+
import { fetchOrDescribe, hostOf } from '../util/retry.js';
|
|
9
|
+
export class AuthClient {
|
|
10
|
+
baseUrl;
|
|
11
|
+
fetchImpl;
|
|
12
|
+
host;
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
|
|
15
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
16
|
+
this.host = hostOf(this.baseUrl);
|
|
17
|
+
}
|
|
18
|
+
/** fetch against the account API, with a friendly message on a transient network failure. */
|
|
19
|
+
request(pathname, init) {
|
|
20
|
+
return fetchOrDescribe(this.fetchImpl, `${this.baseUrl}${pathname}`, init, this.host);
|
|
21
|
+
}
|
|
22
|
+
/** Exchange a one-time code (temp token) for the real JWT. */
|
|
23
|
+
async exchange(code) {
|
|
24
|
+
const res = await this.request('/user-api/api/temp-token/exchange', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
body: JSON.stringify({ tempToken: code }),
|
|
28
|
+
});
|
|
29
|
+
const body = await parseJson(res);
|
|
30
|
+
const token = body?.data?.token;
|
|
31
|
+
if (typeof token !== 'string' || !token) {
|
|
32
|
+
throw new Error(errorMessage(body) ?? 'Login code is invalid or already used.');
|
|
33
|
+
}
|
|
34
|
+
return token;
|
|
35
|
+
}
|
|
36
|
+
/** Fetch recent billing records (for `account transactions`). */
|
|
37
|
+
async bills(token, page = 1, size = 20) {
|
|
38
|
+
const res = await this.request(`/user-api/api/user/bill?page=${page}&size=${size}`, {
|
|
39
|
+
method: 'GET',
|
|
40
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
41
|
+
});
|
|
42
|
+
const body = await parseJson(res);
|
|
43
|
+
const rows = body?.data?.data;
|
|
44
|
+
if (!Array.isArray(rows)) {
|
|
45
|
+
throw new Error(errorMessage(body) ?? 'Failed to fetch transactions.');
|
|
46
|
+
}
|
|
47
|
+
return rows.map((r) => ({
|
|
48
|
+
model: r.model ?? '(unknown)',
|
|
49
|
+
cost: Number(r.cost ?? 0),
|
|
50
|
+
accountType: r.account_type,
|
|
51
|
+
createdAt: r.created_at,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
/** Fetch the signed-in user's info (for whoami and the post-login greeting). */
|
|
55
|
+
async userInfo(token) {
|
|
56
|
+
const res = await this.request('/user-api/api/user/info', {
|
|
57
|
+
method: 'GET',
|
|
58
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
59
|
+
});
|
|
60
|
+
const body = await parseJson(res);
|
|
61
|
+
const data = body?.data;
|
|
62
|
+
if (!data || typeof data !== 'object') {
|
|
63
|
+
throw new Error(errorMessage(body) ?? 'Failed to fetch account info.');
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
email: data.email,
|
|
67
|
+
name: data.name,
|
|
68
|
+
balance: data.balance,
|
|
69
|
+
plan: data.subscriptionTag ?? data.plan,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function parseJson(res) {
|
|
74
|
+
const text = await res.text();
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(text);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return { message: text || `HTTP ${res.status}` };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function errorMessage(body) {
|
|
83
|
+
if (body && typeof body.message === 'string' && body.message && body.message !== 'success') {
|
|
84
|
+
return body.message;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=authClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authClient.js","sourceRoot":"","sources":["../../../src/auth/authClient.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAsB3D,MAAM,OAAO,UAAU;IACJ,OAAO,CAAS;IAChB,SAAS,CAAe;IACxB,IAAI,CAAS;IAE9B,YAAY,IAAuB;QACjC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAChD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC;QACzC,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IAED,6FAA6F;IACrF,OAAO,CAAC,QAAgB,EAAE,IAAiB;QACjD,OAAO,eAAe,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,IAAI,CAAC,OAAO,GAAG,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACxF,CAAC;IAED,8DAA8D;IAC9D,KAAK,CAAC,QAAQ,CAAC,IAAY;QACzB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,mCAAmC,EAAE;YAClE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;SAC1C,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,KAAK,GAAG,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC;QAChC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,wCAAwC,CAAC,CAAC;QAClF,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,iEAAiE;IACjE,KAAK,CAAC,KAAK,CAAC,KAAa,EAAE,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,EAAE;QAC5C,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,gCAAgC,IAAI,SAAS,IAAI,EAAE,EAAE;YAClF,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE;SAC9C,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;QAC9B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,+BAA+B,CAAC,CAAC;QACzE,CAAC;QACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC;YAC3B,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,WAAW;YAC7B,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;YACzB,WAAW,EAAE,CAAC,CAAC,YAAY;YAC3B,SAAS,EAAE,CAAC,CAAC,UAAU;SACxB,CAAC,CAAC,CAAC;IACN,CAAC;IAED,gFAAgF;IAChF,KAAK,CAAC,QAAQ,CAAC,KAAa;QAC1B,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,yBAAyB,EAAE;YACxD,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE;SAC9C,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,IAAI,EAAE,IAAI,CAAC;QACxB,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YACtC,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,+BAA+B,CAAC,CAAC;QACzE,CAAC;QACD,OAAO;YACL,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,IAAI,EAAE,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,IAAI;SACxC,CAAC;IACJ,CAAC;CACF;AAED,KAAK,UAAU,SAAS,CAAC,GAAa;IACpC,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,OAAO,EAAE,IAAI,IAAI,QAAQ,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC;IACnD,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,IAAS;IAC7B,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QAC3F,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Stored GlobalGPT credentials. */
|
|
2
|
+
export interface Credentials {
|
|
3
|
+
token: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
savedAt: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Local credential storage at ~/.config/glbgpt/credentials.json (dir 0700, file 0600).
|
|
9
|
+
*
|
|
10
|
+
* The JWT is long-lived and there is no refresh; on a 401 the CLI prompts the user to
|
|
11
|
+
* `glbgpt login` again. `logout` removes this file (the server does not revoke the JWT).
|
|
12
|
+
*/
|
|
13
|
+
export declare class CredentialStore {
|
|
14
|
+
private readonly file;
|
|
15
|
+
private readonly dir;
|
|
16
|
+
constructor(file?: string, dir?: string);
|
|
17
|
+
save(creds: Credentials): Promise<void>;
|
|
18
|
+
load(): Promise<Credentials | null>;
|
|
19
|
+
clear(): Promise<boolean>;
|
|
20
|
+
/** Directory the credentials file lives in (exposed for logging). */
|
|
21
|
+
location(): string;
|
|
22
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { configDir, credentialsPath } from '../config/paths.js';
|
|
4
|
+
/**
|
|
5
|
+
* Local credential storage at ~/.config/glbgpt/credentials.json (dir 0700, file 0600).
|
|
6
|
+
*
|
|
7
|
+
* The JWT is long-lived and there is no refresh; on a 401 the CLI prompts the user to
|
|
8
|
+
* `glbgpt login` again. `logout` removes this file (the server does not revoke the JWT).
|
|
9
|
+
*/
|
|
10
|
+
export class CredentialStore {
|
|
11
|
+
file;
|
|
12
|
+
dir;
|
|
13
|
+
constructor(file = credentialsPath(), dir = configDir()) {
|
|
14
|
+
this.file = file;
|
|
15
|
+
this.dir = dir;
|
|
16
|
+
}
|
|
17
|
+
async save(creds) {
|
|
18
|
+
await fs.mkdir(this.dir, { recursive: true, mode: 0o700 });
|
|
19
|
+
// Atomic write via a fresh 0600 temp file + rename, so the token is never briefly written
|
|
20
|
+
// with looser permissions (an existing file's mode is not reapplied by writeFile), and a
|
|
21
|
+
// crash mid-write cannot corrupt the credentials file.
|
|
22
|
+
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
23
|
+
await fs.writeFile(tmp, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
24
|
+
await fs.chmod(tmp, 0o600);
|
|
25
|
+
await fs.rename(tmp, this.file);
|
|
26
|
+
}
|
|
27
|
+
async load() {
|
|
28
|
+
try {
|
|
29
|
+
const raw = await fs.readFile(this.file, 'utf8');
|
|
30
|
+
const creds = JSON.parse(raw);
|
|
31
|
+
return creds.token ? creds : null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async clear() {
|
|
38
|
+
try {
|
|
39
|
+
await fs.rm(this.file);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Directory the credentials file lives in (exposed for logging). */
|
|
47
|
+
location() {
|
|
48
|
+
return path.dirname(this.file);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=credentialStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentialStore.js","sourceRoot":"","sources":["../../../src/auth/credentialStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAShE;;;;;GAKG;AACH,MAAM,OAAO,eAAe;IAEP;IACA;IAFnB,YACmB,OAAe,eAAe,EAAE,EAChC,MAAc,SAAS,EAAE;QADzB,SAAI,GAAJ,IAAI,CAA4B;QAChC,QAAG,GAAH,GAAG,CAAsB;IACzC,CAAC;IAEJ,KAAK,CAAC,IAAI,CAAC,KAAkB;QAC3B,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3D,0FAA0F;QAC1F,yFAAyF;QACzF,uDAAuD;QACvD,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,MAAM,CAAC;QAC9C,MAAM,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACzE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC3B,MAAM,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACjD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAgB,CAAC;YAC7C,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;QACpC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,qEAAqE;IACrE,QAAQ;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;CACF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CredentialStore } from './credentialStore.js';
|
|
2
|
+
import type { UserInfo } from './authClient.js';
|
|
3
|
+
/** Minimal account-API surface the login flow needs (real AuthClient satisfies this). */
|
|
4
|
+
export interface AuthApi {
|
|
5
|
+
exchange(code: string): Promise<string>;
|
|
6
|
+
userInfo(token: string): Promise<UserInfo>;
|
|
7
|
+
}
|
|
8
|
+
export interface BrowserLoginDeps {
|
|
9
|
+
/** Web frontend base that serves the /cli-auth page (may differ from the API base). */
|
|
10
|
+
webBaseUrl: string;
|
|
11
|
+
lang: string;
|
|
12
|
+
authClient: AuthApi;
|
|
13
|
+
credentialStore: CredentialStore;
|
|
14
|
+
/** Open (or print) the authorization URL. Injected for testing / --no-browser. */
|
|
15
|
+
openUrl: (url: string) => void | Promise<void>;
|
|
16
|
+
genState?: () => string;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface LoginResult {
|
|
20
|
+
email?: string;
|
|
21
|
+
authUrl: string;
|
|
22
|
+
}
|
|
23
|
+
/** Main login flow: loopback + state. Returns once credentials are stored. */
|
|
24
|
+
export declare function browserLogin(deps: BrowserLoginDeps): Promise<LoginResult>;
|
|
25
|
+
export interface CodeLoginDeps {
|
|
26
|
+
authClient: AuthApi;
|
|
27
|
+
credentialStore: CredentialStore;
|
|
28
|
+
}
|
|
29
|
+
/** Fallback login: exchange a code the user pasted from the web page. */
|
|
30
|
+
export declare function codeLogin(deps: CodeLoginDeps, code: string): Promise<LoginResult>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { LoopbackServer } from './loopbackServer.js';
|
|
3
|
+
/** Main login flow: loopback + state. Returns once credentials are stored. */
|
|
4
|
+
export async function browserLogin(deps) {
|
|
5
|
+
const state = (deps.genState ?? defaultState)();
|
|
6
|
+
const server = new LoopbackServer(state, deps.timeoutMs);
|
|
7
|
+
const { redirectUri } = await server.listen();
|
|
8
|
+
const authUrl = `${trimSlash(deps.webBaseUrl)}/${deps.lang}/cli-auth` +
|
|
9
|
+
`?redirect_uri=${encodeURIComponent(redirectUri)}&state=${encodeURIComponent(state)}`;
|
|
10
|
+
try {
|
|
11
|
+
await deps.openUrl(authUrl);
|
|
12
|
+
const code = await server.waitForCode();
|
|
13
|
+
const info = await finishLogin(deps.authClient, deps.credentialStore, code);
|
|
14
|
+
return { email: info.email, authUrl };
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
server.close();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Fallback login: exchange a code the user pasted from the web page. */
|
|
21
|
+
export async function codeLogin(deps, code) {
|
|
22
|
+
const info = await finishLogin(deps.authClient, deps.credentialStore, code.trim());
|
|
23
|
+
return { email: info.email, authUrl: '' };
|
|
24
|
+
}
|
|
25
|
+
async function finishLogin(authClient, store, code) {
|
|
26
|
+
const token = await authClient.exchange(code);
|
|
27
|
+
let info = {};
|
|
28
|
+
try {
|
|
29
|
+
info = await authClient.userInfo(token);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// A token that cannot fetch user info is still usable; store it without the email.
|
|
33
|
+
}
|
|
34
|
+
await store.save({ token, email: info.email, savedAt: Date.now() });
|
|
35
|
+
return info;
|
|
36
|
+
}
|
|
37
|
+
function defaultState() {
|
|
38
|
+
return crypto.randomBytes(16).toString('hex');
|
|
39
|
+
}
|
|
40
|
+
function trimSlash(url) {
|
|
41
|
+
return url.replace(/\/+$/, '');
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=loginFlow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loginFlow.js","sourceRoot":"","sources":["../../../src/auth/loginFlow.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA2BrD,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAsB;IACvD,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,YAAY,CAAC,EAAE,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACzD,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;IAC9C,MAAM,OAAO,GACX,GAAG,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,IAAI,WAAW;QACrD,iBAAiB,kBAAkB,CAAC,WAAW,CAAC,UAAU,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC;IACxF,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;QACxC,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;QAC5E,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC;IACxC,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,KAAK,EAAE,CAAC;IACjB,CAAC;AACH,CAAC;AAOD,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAmB,EAAE,IAAY;IAC/D,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACnF,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;AAC5C,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,UAAmB,EAAE,KAAsB,EAAE,IAAY;IAClF,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC9C,IAAI,IAAI,GAAa,EAAE,CAAC;IACxB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,mFAAmF;IACrF,CAAC;IACD,MAAM,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACpE,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,YAAY;IACnB,OAAO,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAChD,CAAC;AAED,SAAS,SAAS,CAAC,GAAW;IAC5B,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local loopback HTTP server that receives the browser redirect for the login flow.
|
|
3
|
+
*
|
|
4
|
+
* Binds 127.0.0.1 on an OS-assigned port and waits for a single
|
|
5
|
+
* `GET /callback?code=<one-time-code>&state=<state>` request. The one-time code is delivered
|
|
6
|
+
* to the local machine, so a remote attacker cannot receive it (phishing resistance); `state`
|
|
7
|
+
* guards against CSRF/stale callbacks.
|
|
8
|
+
*
|
|
9
|
+
* Hardening (we have no PKCE endpoint, so the loopback surface itself carries the weight —
|
|
10
|
+
* see docs; Codex/Claude Code lean on PKCE instead and skip some of these):
|
|
11
|
+
* - Only `GET /callback` from a loopback `Host` header is honored; everything else 404s and
|
|
12
|
+
* the server keeps listening (a stray/hostile local request can't hijack or DoS the login).
|
|
13
|
+
* - `state` is compared in constant time.
|
|
14
|
+
* - A state mismatch responds 400 but does NOT settle the login — the real callback is still
|
|
15
|
+
* awaited (Codex's resilient shape, not Claude Code's fail-closed reject).
|
|
16
|
+
*/
|
|
17
|
+
export declare class LoopbackServer {
|
|
18
|
+
private readonly expectedState;
|
|
19
|
+
private readonly timeoutMs;
|
|
20
|
+
private server;
|
|
21
|
+
private port;
|
|
22
|
+
private settled;
|
|
23
|
+
private timer;
|
|
24
|
+
private readonly result;
|
|
25
|
+
private resolveResult;
|
|
26
|
+
private rejectResult;
|
|
27
|
+
constructor(expectedState: string, timeoutMs?: number);
|
|
28
|
+
/** Bind the server and return the redirect URI the browser will be sent back to. */
|
|
29
|
+
listen(): Promise<{
|
|
30
|
+
redirectUri: string;
|
|
31
|
+
}>;
|
|
32
|
+
/** Resolve with the one-time code once the browser hits the callback with a matching state. */
|
|
33
|
+
waitForCode(): Promise<string>;
|
|
34
|
+
close(): void;
|
|
35
|
+
private handle;
|
|
36
|
+
/** Host must be a loopback literal on our port (127.0.0.1 / localhost / [::1]). */
|
|
37
|
+
private isLoopbackHost;
|
|
38
|
+
/** Constant-time state comparison (avoids leaking match progress via timing). */
|
|
39
|
+
private stateMatches;
|
|
40
|
+
private settle;
|
|
41
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
/**
|
|
4
|
+
* Local loopback HTTP server that receives the browser redirect for the login flow.
|
|
5
|
+
*
|
|
6
|
+
* Binds 127.0.0.1 on an OS-assigned port and waits for a single
|
|
7
|
+
* `GET /callback?code=<one-time-code>&state=<state>` request. The one-time code is delivered
|
|
8
|
+
* to the local machine, so a remote attacker cannot receive it (phishing resistance); `state`
|
|
9
|
+
* guards against CSRF/stale callbacks.
|
|
10
|
+
*
|
|
11
|
+
* Hardening (we have no PKCE endpoint, so the loopback surface itself carries the weight —
|
|
12
|
+
* see docs; Codex/Claude Code lean on PKCE instead and skip some of these):
|
|
13
|
+
* - Only `GET /callback` from a loopback `Host` header is honored; everything else 404s and
|
|
14
|
+
* the server keeps listening (a stray/hostile local request can't hijack or DoS the login).
|
|
15
|
+
* - `state` is compared in constant time.
|
|
16
|
+
* - A state mismatch responds 400 but does NOT settle the login — the real callback is still
|
|
17
|
+
* awaited (Codex's resilient shape, not Claude Code's fail-closed reject).
|
|
18
|
+
*/
|
|
19
|
+
export class LoopbackServer {
|
|
20
|
+
expectedState;
|
|
21
|
+
timeoutMs;
|
|
22
|
+
server = null;
|
|
23
|
+
port = 0;
|
|
24
|
+
settled = false;
|
|
25
|
+
timer = null;
|
|
26
|
+
result;
|
|
27
|
+
resolveResult;
|
|
28
|
+
rejectResult;
|
|
29
|
+
constructor(expectedState, timeoutMs = 300_000) {
|
|
30
|
+
this.expectedState = expectedState;
|
|
31
|
+
this.timeoutMs = timeoutMs;
|
|
32
|
+
// Create the deferred up front so a callback that arrives before waitForCode() is not lost.
|
|
33
|
+
this.result = new Promise((resolve, reject) => {
|
|
34
|
+
this.resolveResult = resolve;
|
|
35
|
+
this.rejectResult = reject;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Bind the server and return the redirect URI the browser will be sent back to. */
|
|
39
|
+
async listen() {
|
|
40
|
+
this.server = http.createServer((req, res) => this.handle(req, res));
|
|
41
|
+
this.server.on('error', () => { }); // ignore post-bind socket errors
|
|
42
|
+
await new Promise((resolve, reject) => {
|
|
43
|
+
const onError = (err) => reject(err);
|
|
44
|
+
this.server.once('error', onError);
|
|
45
|
+
this.server.listen(0, '127.0.0.1', () => {
|
|
46
|
+
this.server.off('error', onError);
|
|
47
|
+
resolve();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
this.port = this.server.address().port;
|
|
51
|
+
return { redirectUri: `http://127.0.0.1:${this.port}/callback` };
|
|
52
|
+
}
|
|
53
|
+
/** Resolve with the one-time code once the browser hits the callback with a matching state. */
|
|
54
|
+
waitForCode() {
|
|
55
|
+
if (!this.settled && !this.timer) {
|
|
56
|
+
this.timer = setTimeout(() => {
|
|
57
|
+
this.settle(() => this.rejectResult(new Error('Timed out waiting for browser authorization.')));
|
|
58
|
+
}, this.timeoutMs);
|
|
59
|
+
if (typeof this.timer.unref === 'function')
|
|
60
|
+
this.timer.unref();
|
|
61
|
+
}
|
|
62
|
+
return this.result;
|
|
63
|
+
}
|
|
64
|
+
close() {
|
|
65
|
+
if (this.timer)
|
|
66
|
+
clearTimeout(this.timer);
|
|
67
|
+
this.server?.closeAllConnections?.();
|
|
68
|
+
this.server?.close();
|
|
69
|
+
this.server = null;
|
|
70
|
+
}
|
|
71
|
+
handle(req, res) {
|
|
72
|
+
// Defense-in-depth against DNS-rebinding: only accept GET from a loopback Host. A page on
|
|
73
|
+
// a hostile domain that resolves to 127.0.0.1 cannot forge a loopback Host header.
|
|
74
|
+
if (req.method !== 'GET' || !this.isLoopbackHost(req.headers.host)) {
|
|
75
|
+
respond(res, 404, page('Not found', 'You can close this tab.'));
|
|
76
|
+
return; // keep listening
|
|
77
|
+
}
|
|
78
|
+
const url = new URL(req.url ?? '/', `http://127.0.0.1:${this.port}`);
|
|
79
|
+
if (url.pathname !== '/callback') {
|
|
80
|
+
respond(res, 404, page('Not found', 'You can close this tab.'));
|
|
81
|
+
return; // keep listening
|
|
82
|
+
}
|
|
83
|
+
const state = url.searchParams.get('state');
|
|
84
|
+
// A mismatched/absent state is a stale or hostile hit — reject THIS request but keep the
|
|
85
|
+
// login alive for the genuine callback (do not settle).
|
|
86
|
+
if (!state || !this.stateMatches(state)) {
|
|
87
|
+
respond(res, 400, page('Authorization failed', 'State mismatch. You can close this tab.'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const code = url.searchParams.get('code');
|
|
91
|
+
const error = url.searchParams.get('error');
|
|
92
|
+
if (error) {
|
|
93
|
+
respond(res, 400, page('Authorization failed', 'You can close this tab and return to the terminal.'));
|
|
94
|
+
this.settle(() => this.rejectResult(new Error(`Authorization was denied (${error}).`)));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!code) {
|
|
98
|
+
respond(res, 400, page('Authorization failed', 'Missing code. You can close this tab.'));
|
|
99
|
+
this.settle(() => this.rejectResult(new Error('Authorization callback did not include a code.')));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
respond(res, 200, page('Signed in to GlobalGPT CLI', 'You can close this tab and return to the terminal.'));
|
|
103
|
+
this.settle(() => this.resolveResult(code));
|
|
104
|
+
}
|
|
105
|
+
/** Host must be a loopback literal on our port (127.0.0.1 / localhost / [::1]). */
|
|
106
|
+
isLoopbackHost(host) {
|
|
107
|
+
if (!host)
|
|
108
|
+
return false;
|
|
109
|
+
const suffix = `:${this.port}`;
|
|
110
|
+
if (!host.endsWith(suffix))
|
|
111
|
+
return false;
|
|
112
|
+
const name = host.slice(0, -suffix.length);
|
|
113
|
+
return name === '127.0.0.1' || name === 'localhost' || name === '[::1]';
|
|
114
|
+
}
|
|
115
|
+
/** Constant-time state comparison (avoids leaking match progress via timing). */
|
|
116
|
+
stateMatches(state) {
|
|
117
|
+
const a = Buffer.from(state);
|
|
118
|
+
const b = Buffer.from(this.expectedState);
|
|
119
|
+
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
120
|
+
}
|
|
121
|
+
settle(action) {
|
|
122
|
+
if (this.settled)
|
|
123
|
+
return;
|
|
124
|
+
this.settled = true;
|
|
125
|
+
if (this.timer)
|
|
126
|
+
clearTimeout(this.timer);
|
|
127
|
+
action();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function respond(res, status, html) {
|
|
131
|
+
// Force Connection: close — a kept-alive socket would wedge the next login on the same port
|
|
132
|
+
// (Codex hits this with fixed ports; cheap insurance for us too).
|
|
133
|
+
res.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8', Connection: 'close' });
|
|
134
|
+
res.end(html);
|
|
135
|
+
}
|
|
136
|
+
function page(title, body) {
|
|
137
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head>` +
|
|
138
|
+
`<body style="font-family:system-ui;max-width:32rem;margin:4rem auto;text-align:center">` +
|
|
139
|
+
`<h2>${title}</h2><p>${body}</p></body></html>`;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=loopbackServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loopbackServer.js","sourceRoot":"","sources":["../../../src/auth/loopbackServer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { CredentialStore } from './credentialStore.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the JWT to use for API calls.
|
|
4
|
+
*
|
|
5
|
+
* Precedence: the GLBGPT_TOKEN env var wins if set (explicit override / CI escape hatch);
|
|
6
|
+
* otherwise the token stored by `glbgpt login`. Returns null when neither is available.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveToken(store?: CredentialStore): Promise<string | null>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CredentialStore } from './credentialStore.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the JWT to use for API calls.
|
|
4
|
+
*
|
|
5
|
+
* Precedence: the GLBGPT_TOKEN env var wins if set (explicit override / CI escape hatch);
|
|
6
|
+
* otherwise the token stored by `glbgpt login`. Returns null when neither is available.
|
|
7
|
+
*/
|
|
8
|
+
export async function resolveToken(store = new CredentialStore()) {
|
|
9
|
+
const env = process.env.GLBGPT_TOKEN;
|
|
10
|
+
if (env && env.trim())
|
|
11
|
+
return env.trim();
|
|
12
|
+
const creds = await store.load();
|
|
13
|
+
return creds?.token ?? null;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../../src/auth/token.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAyB,IAAI,eAAe,EAAE;IAC/E,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;IACrC,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE;QAAE,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;IACjC,OAAO,KAAK,EAAE,KAAK,IAAI,IAAI,CAAC;AAC9B,CAAC"}
|