codex-imagegen-mcp 0.1.2
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/CHANGELOG.md +96 -0
- package/LICENSE +201 -0
- package/NOTICE +19 -0
- package/README.md +373 -0
- package/dist/src/auth/borrowed.js +77 -0
- package/dist/src/auth/borrowed.js.map +1 -0
- package/dist/src/auth/browser-login.js +179 -0
- package/dist/src/auth/browser-login.js.map +1 -0
- package/dist/src/auth/device-login.js +39 -0
- package/dist/src/auth/device-login.js.map +1 -0
- package/dist/src/auth/jwt.js +69 -0
- package/dist/src/auth/jwt.js.map +1 -0
- package/dist/src/auth/manager.js +289 -0
- package/dist/src/auth/manager.js.map +1 -0
- package/dist/src/auth/oauth.js +246 -0
- package/dist/src/auth/oauth.js.map +1 -0
- package/dist/src/auth/pages.js +39 -0
- package/dist/src/auth/pages.js.map +1 -0
- package/dist/src/auth/pkce.js +14 -0
- package/dist/src/auth/pkce.js.map +1 -0
- package/dist/src/auth/store.js +75 -0
- package/dist/src/auth/store.js.map +1 -0
- package/dist/src/backend/images-client.js +223 -0
- package/dist/src/backend/images-client.js.map +1 -0
- package/dist/src/backend/ratelimits.js +183 -0
- package/dist/src/backend/ratelimits.js.map +1 -0
- package/dist/src/cli.js +407 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.js +87 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/constants.js +52 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/doctor.js +122 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/errors.js +76 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/generation.js +183 -0
- package/dist/src/generation.js.map +1 -0
- package/dist/src/history.js +47 -0
- package/dist/src/history.js.map +1 -0
- package/dist/src/images/chroma.js +200 -0
- package/dist/src/images/chroma.js.map +1 -0
- package/dist/src/images/codec.js +159 -0
- package/dist/src/images/codec.js.map +1 -0
- package/dist/src/images/inputs.js +83 -0
- package/dist/src/images/inputs.js.map +1 -0
- package/dist/src/images/output.js +100 -0
- package/dist/src/images/output.js.map +1 -0
- package/dist/src/images/preview.js +20 -0
- package/dist/src/images/preview.js.map +1 -0
- package/dist/src/images/resize.js +135 -0
- package/dist/src/images/resize.js.map +1 -0
- package/dist/src/install/opencode.js +138 -0
- package/dist/src/install/opencode.js.map +1 -0
- package/dist/src/install/skill.js +128 -0
- package/dist/src/install/skill.js.map +1 -0
- package/dist/src/install/snippets.js +127 -0
- package/dist/src/install/snippets.js.map +1 -0
- package/dist/src/log.js +58 -0
- package/dist/src/log.js.map +1 -0
- package/dist/src/remove-background.js +101 -0
- package/dist/src/remove-background.js.map +1 -0
- package/dist/src/server/context.js +2 -0
- package/dist/src/server/context.js.map +1 -0
- package/dist/src/server/index.js +94 -0
- package/dist/src/server/index.js.map +1 -0
- package/dist/src/server/instructions.js +20 -0
- package/dist/src/server/instructions.js.map +1 -0
- package/dist/src/server/login-coordinator.js +73 -0
- package/dist/src/server/login-coordinator.js.map +1 -0
- package/dist/src/server/progress.js +33 -0
- package/dist/src/server/progress.js.map +1 -0
- package/dist/src/server/prompts.js +45 -0
- package/dist/src/server/prompts.js.map +1 -0
- package/dist/src/server/resources.js +73 -0
- package/dist/src/server/resources.js.map +1 -0
- package/dist/src/server/tools.js +338 -0
- package/dist/src/server/tools.js.map +1 -0
- package/dist/src/server/workspace.js +45 -0
- package/dist/src/server/workspace.js.map +1 -0
- package/dist/src/status.js +86 -0
- package/dist/src/status.js.map +1 -0
- package/dist/src/util/format.js +36 -0
- package/dist/src/util/format.js.map +1 -0
- package/dist/src/util/fs.js +117 -0
- package/dist/src/util/fs.js.map +1 -0
- package/dist/src/util/http.js +65 -0
- package/dist/src/util/http.js.map +1 -0
- package/dist/src/util/invocation.js +43 -0
- package/dist/src/util/invocation.js.map +1 -0
- package/dist/src/util/open.js +41 -0
- package/dist/src/util/open.js.map +1 -0
- package/docs/ARCHITECTURE.md +123 -0
- package/docs/AUTH.md +242 -0
- package/docs/BACKEND.md +169 -0
- package/docs/CLIENTS.md +154 -0
- package/docs/DEVELOPMENT.md +171 -0
- package/docs/README.md +84 -0
- package/docs/TOOLS.md +221 -0
- package/package.json +67 -0
- package/skill/imagegen/LICENSE.txt +201 -0
- package/skill/imagegen/SKILL.md +189 -0
- package/skill/imagegen/references/prompting.md +112 -0
- package/skill/imagegen/references/sample-prompts.md +418 -0
- package/skill/imagegen/references/tools.md +90 -0
package/README.md
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/assets/hero.jpg" width="100%" alt="Codex ImageGen MCP — a 1930s travel-poster illustration: an enormous empty picture frame stands on a desert plain and frames a rising sun behind a peak, while a small train runs along a railway toward it">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<b>Codex's image generation, in every coding agent.</b><br>
|
|
7
|
+
Generate and edit images from opencode, Claude Code, Cursor, VS Code and any other MCP client.<br>
|
|
8
|
+
It runs on the ChatGPT plan you already pay for, so there's no API key and no per-image bill.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://github.com/ShalomObongo/codex-imagegen-mcp/releases/latest"><img alt="Latest release" src="https://img.shields.io/github/v/release/ShalomObongo/codex-imagegen-mcp?style=flat-square&label=release&color=A6553B"></a>
|
|
13
|
+
<a href="https://github.com/ShalomObongo/codex-imagegen-mcp/actions/workflows/ci.yml"><img alt="CI status" src="https://img.shields.io/github/actions/workflow/status/ShalomObongo/codex-imagegen-mcp/ci.yml?branch=main&style=flat-square&label=CI"></a>
|
|
14
|
+
<img alt="MCP stdio server" src="https://img.shields.io/badge/MCP-stdio%20server-2A2523?style=flat-square">
|
|
15
|
+
<img alt="Node 22 or newer" src="https://img.shields.io/badge/node-%E2%89%A5%2022-4E6E63?style=flat-square">
|
|
16
|
+
<img alt="Runs on your ChatGPT plan" src="https://img.shields.io/badge/ChatGPT%20plan-no%20API%20key-A6553B?style=flat-square">
|
|
17
|
+
<a href="LICENSE"><img alt="Apache-2.0 license" src="https://img.shields.io/badge/license-Apache--2.0-D9A05B?style=flat-square"></a>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<a href="#quick-start"><b>Quick start</b></a> ·
|
|
22
|
+
<a href="#see-it-work"><b>See it work</b></a> ·
|
|
23
|
+
<a href="docs/TOOLS.md"><b>Tools</b></a> ·
|
|
24
|
+
<a href="docs/AUTH.md"><b>Sign-in</b></a> ·
|
|
25
|
+
<a href="docs/CLIENTS.md"><b>Clients</b></a> ·
|
|
26
|
+
<a href="docs/README.md"><b>All docs</b></a>
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
<br>
|
|
30
|
+
|
|
31
|
+
## What it does
|
|
32
|
+
|
|
33
|
+
<table>
|
|
34
|
+
<tr>
|
|
35
|
+
<td width="33%" valign="top">
|
|
36
|
+
<img src="docs/assets/badges/signin.png" width="76" alt=""><br>
|
|
37
|
+
<b>Your ChatGPT plan, not an API bill</b><br>
|
|
38
|
+
Sign in once with ChatGPT, or reuse the sign-in Codex or opencode already has. Every image counts against the plan you already pay for.
|
|
39
|
+
</td>
|
|
40
|
+
<td width="33%" valign="top">
|
|
41
|
+
<img src="docs/assets/badges/clients.png" width="76" alt=""><br>
|
|
42
|
+
<b>Works in any MCP client</b><br>
|
|
43
|
+
A standard local stdio server. opencode installs in one command; Claude Code, Cursor, VS Code, Windsurf and Gemini CLI take one pasted snippet.
|
|
44
|
+
</td>
|
|
45
|
+
<td width="33%" valign="top">
|
|
46
|
+
<img src="docs/assets/badges/transparency.png" width="76" alt=""><br>
|
|
47
|
+
<b>Real transparent PNGs</b><br>
|
|
48
|
+
Ask for <code>background: "transparent"</code> and get genuine alpha, checked on every file. A local chroma-key tool handles the rest.
|
|
49
|
+
</td>
|
|
50
|
+
</tr>
|
|
51
|
+
<tr>
|
|
52
|
+
<td width="33%" valign="top">
|
|
53
|
+
<img src="docs/assets/badges/aspect.png" width="76" alt=""><br>
|
|
54
|
+
<b>Any canvas shape</b><br>
|
|
55
|
+
Eleven aspect ratios from 21:9 to 9:21, and up to four variants of a prompt in parallel.
|
|
56
|
+
</td>
|
|
57
|
+
<td width="33%" valign="top">
|
|
58
|
+
<img src="docs/assets/badges/versions.png" width="76" alt=""><br>
|
|
59
|
+
<b>Saves into your project, safely</b><br>
|
|
60
|
+
Paths resolve to your workspace. An existing file is never overwritten; you get <code>hero-2.png</code> instead.
|
|
61
|
+
</td>
|
|
62
|
+
<td width="33%" valign="top">
|
|
63
|
+
<img src="docs/assets/badges/skill.png" width="76" alt=""><br>
|
|
64
|
+
<b>Codex's own playbook</b><br>
|
|
65
|
+
Ships the <code>imagegen</code> skill from the Codex app, adapted to these tools: prompt structure, edit invariants, save rules.
|
|
66
|
+
</td>
|
|
67
|
+
</tr>
|
|
68
|
+
</table>
|
|
69
|
+
|
|
70
|
+
## See it work
|
|
71
|
+
|
|
72
|
+
Two real opencode sessions in a scratch project. The images are exactly what came back:
|
|
73
|
+
|
|
74
|
+
<p align="center">
|
|
75
|
+
<img src="docs/assets/examples/real-session.jpg" width="100%" alt="Three real outputs: a transparent cartoon cactus sticker, the same sticker edited to wear a straw hat, and a 16:9 photographic hero of a cactus shop at sunset">
|
|
76
|
+
</p>
|
|
77
|
+
|
|
78
|
+
> **You:** I need a sticker-style illustration of a cute cartoon cactus with a transparent background for this project. Save it as `assets/cactus-sticker.png`.
|
|
79
|
+
>
|
|
80
|
+
> **opencode** · `openai/gpt-5.5` loads the `imagegen` skill → `imagegen_generate_image` with `background: "transparent"` → *"Saved `assets/cactus-sticker.png`, 1254×1254 PNG with a transparent background."*
|
|
81
|
+
>
|
|
82
|
+
> **You:** Make a 16:9 hero banner for the Cactus Shop landing page, and a variant of the sticker where the cactus wears a tiny straw hat. Keep everything else identical.
|
|
83
|
+
>
|
|
84
|
+
> **opencode** · `github-copilot/claude-sonnet-5` → `imagegen_auth_status` → `imagegen_generate_image` (`aspect_ratio: "16:9"`) and `imagegen_edit_image` (`images: ["assets/cactus-sticker.png"]`) in parallel → both saved, the edit with *"transparent background (alpha verified)"*.
|
|
85
|
+
|
|
86
|
+
**One prompt, three canvases.** `aspect_ratio` doesn't crop. The service composes a new picture for each shape:
|
|
87
|
+
|
|
88
|
+
<p align="center">
|
|
89
|
+
<img src="docs/assets/examples/aspect-ratios.jpg" width="100%" alt="The same travel-poster prompt of a train crossing a trestle bridge, generated at 1:1 (1254×1254), 16:9 (1672×941) and 9:16 (941×1672)">
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
**A green screen becomes alpha on your machine.** `remove_background` keys out a flat backdrop locally, with no network call and no quota:
|
|
93
|
+
|
|
94
|
+
<p align="center">
|
|
95
|
+
<img src="docs/assets/examples/remove-background.jpg" width="82%" alt="A striped hot-air balloon generated on a flat green backdrop, and the same balloon cut out onto a transparent checkerboard">
|
|
96
|
+
</p>
|
|
97
|
+
|
|
98
|
+
> [!TIP]
|
|
99
|
+
> Every image in this repository was made with codex-imagegen-mcp itself. That covers the posters, the badges, the logo and the examples. The prompts and the art-direction record are in [docs/assets](docs/assets/README.md).
|
|
100
|
+
|
|
101
|
+
## Quick start
|
|
102
|
+
|
|
103
|
+
> [!NOTE]
|
|
104
|
+
> You need **Node.js 22+** and a **ChatGPT plan that includes Codex** (Plus, Pro, Business, Enterprise, Edu…). Codex image generation isn't available on the Free plan.
|
|
105
|
+
|
|
106
|
+
**1 · Install.** This takes the packaged build from the latest GitHub release and puts `codex-imagegen-mcp` on your PATH.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm install --global https://github.com/ShalomObongo/codex-imagegen-mcp/releases/latest/download/codex-imagegen-mcp.tgz
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
<details>
|
|
113
|
+
<summary>Or build it from source</summary>
|
|
114
|
+
<br>
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
git clone https://github.com/ShalomObongo/codex-imagegen-mcp.git && cd codex-imagegen-mcp
|
|
118
|
+
npm ci && npm run build
|
|
119
|
+
npm link # puts `codex-imagegen-mcp` on your PATH
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
</details>
|
|
123
|
+
|
|
124
|
+
**2 · Add it to opencode.** This registers the MCP server and the skill, keeping your config's comments and formatting.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
codex-imagegen-mcp install opencode
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**3 · Sign in with ChatGPT.** Skip this if Codex or opencode is already signed in with ChatGPT.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
codex-imagegen-mcp login # opens the browser; use --device on a headless machine
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**4 · Check, then ask for pictures**
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
codex-imagegen-mcp doctor # Node, credentials, quota, opencode entry, skill
|
|
140
|
+
opencode mcp list # → ✓ imagegen connected
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
you › Make a 16:9 hero image of a lighthouse at dusk and save it to assets/hero.png
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## How it works
|
|
148
|
+
|
|
149
|
+
```mermaid
|
|
150
|
+
sequenceDiagram
|
|
151
|
+
autonumber
|
|
152
|
+
participant A as Your agent<br/>(opencode, Claude Code…)
|
|
153
|
+
participant M as imagegen MCP server<br/>(local · stdio)
|
|
154
|
+
participant O as auth.openai.com
|
|
155
|
+
participant C as chatgpt.com<br/>/backend-api/codex
|
|
156
|
+
A->>M: generate_image {prompt, aspect_ratio, background}
|
|
157
|
+
M->>M: pick credentials: own sign-in → Codex → opencode
|
|
158
|
+
opt token expires within 5 minutes
|
|
159
|
+
M->>O: refresh (under a cross-process lock)
|
|
160
|
+
O-->>M: new access token + rotated refresh token
|
|
161
|
+
end
|
|
162
|
+
M->>C: POST /images/generations · Bearer token · ChatGPT-Account-ID
|
|
163
|
+
loop every 5 s while the image renders
|
|
164
|
+
M-->>A: progress notification (keeps the client's timeout alive)
|
|
165
|
+
end
|
|
166
|
+
C-->>M: PNG + usage headers
|
|
167
|
+
M->>M: save without overwriting · verify alpha · build preview
|
|
168
|
+
M-->>A: path, size, background + a 1024 px preview
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
It sends the same request as the image tool built into the Codex app, byte for byte. As OpenAI improves Codex's image model on the server, this server gets the same upgrade. [How that was reverse-engineered →](docs/BACKEND.md)
|
|
172
|
+
|
|
173
|
+
## Signing in
|
|
174
|
+
|
|
175
|
+
| You already have… | What happens |
|
|
176
|
+
|---|---|
|
|
177
|
+
| **Codex** (CLI or desktop app) signed in with ChatGPT | Works immediately. The server borrows `~/.codex/auth.json` **read-only**. |
|
|
178
|
+
| **opencode** signed in with *OpenAI → ChatGPT Plus/Pro* | Works immediately. It borrows `~/.local/share/opencode/auth.json` read-only. |
|
|
179
|
+
| Neither, or you want an independent sign-in | Run `codex-imagegen-mcp login`, or ask your agent to call the `sign_in` tool. |
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
codex-imagegen-mcp login # browser → sign in → "You're signed in to Codex ImageGen MCP"
|
|
183
|
+
codex-imagegen-mcp login --device # SSH/headless: enter a code at auth.openai.com/codex/device
|
|
184
|
+
codex-imagegen-mcp status # account, plan, credential source and usage windows
|
|
185
|
+
codex-imagegen-mcp logout # remove and revoke this tool's own sign-in
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
> [!IMPORTANT]
|
|
189
|
+
> OpenAI refresh tokens are **single-use**. Borrowed sign-ins are therefore never refreshed or modified, so this server can't sign you out of Codex or opencode. Its own tokens are stored `0600` and refreshed under a lock shared by every server process. [Authentication in depth →](docs/AUTH.md)
|
|
190
|
+
|
|
191
|
+
## Tools
|
|
192
|
+
|
|
193
|
+
| Tool | What it does | Key parameters |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| `generate_image` | A new image from a prompt | `prompt` · `aspect_ratio` · `background` · `n` · `output_path` |
|
|
196
|
+
| `edit_image` | Edit images, or generate from 1–5 references | `images` · `prompt` · same options |
|
|
197
|
+
| `remove_background` | Cut a flat backdrop out locally (no quota) | `input_path` · `key_color` (auto) · `output_path` |
|
|
198
|
+
| `auth_status` | Sign-in, plan, credential source, usage windows | `check_usage` |
|
|
199
|
+
| `sign_in` | Start a ChatGPT sign-in and return a link or code for you | `method` (`browser` · `device`) |
|
|
200
|
+
|
|
201
|
+
The server also exposes the resources `imagegen://history`, `imagegen://images/{id}` and `imagegen://skill/*`, and the prompts `generate` and `edit`. In opencode the prompts appear as `/imagegen:generate` and `/imagegen:edit`. [Full reference →](docs/TOOLS.md)
|
|
202
|
+
|
|
203
|
+
## From the terminal
|
|
204
|
+
|
|
205
|
+
The CLI uses the same engine as the MCP tools:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
codex-imagegen-mcp generate "a watercolor fox in a snowy forest" -a 16:9 -o art/fox.png
|
|
209
|
+
codex-imagegen-mcp generate "Image 1: add a tiny straw hat; keep everything else" \
|
|
210
|
+
-i assets/cactus.png -b transparent -o assets/cactus-hat.png
|
|
211
|
+
codex-imagegen-mcp remove-bg sprite-on-green.png -o sprite.png
|
|
212
|
+
codex-imagegen-mcp config claude-code # print the setup for another client
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Other clients
|
|
216
|
+
|
|
217
|
+
| Client | Setup |
|
|
218
|
+
|---|---|
|
|
219
|
+
| **opencode** | `codex-imagegen-mcp install opencode` (automated, reversible with `uninstall`) |
|
|
220
|
+
| **Claude Code** | `claude mcp add --scope user imagegen -- node /path/to/dist/src/cli.js serve` |
|
|
221
|
+
| **Claude Desktop · Cursor · VS Code · Windsurf · Gemini CLI · Codex** | `codex-imagegen-mcp config <client>` prints a ready-to-paste config |
|
|
222
|
+
|
|
223
|
+
Per-client guidance, including where each one loads skills from: [docs/CLIENTS.md](docs/CLIENTS.md).
|
|
224
|
+
|
|
225
|
+
## Documentation
|
|
226
|
+
|
|
227
|
+
<table>
|
|
228
|
+
<tr>
|
|
229
|
+
<td width="33%" valign="top">
|
|
230
|
+
<a href="docs/TOOLS.md"><img src="docs/assets/thumbs/banner-tools.jpg" alt="Tools"></a><br>
|
|
231
|
+
<b><a href="docs/TOOLS.md">Tools</a></b><br>
|
|
232
|
+
Every tool, parameter, result field, resource and error.
|
|
233
|
+
</td>
|
|
234
|
+
<td width="33%" valign="top">
|
|
235
|
+
<a href="docs/AUTH.md"><img src="docs/assets/thumbs/banner-auth.jpg" alt="Authentication"></a><br>
|
|
236
|
+
<b><a href="docs/AUTH.md">Authentication</a></b><br>
|
|
237
|
+
PKCE and device-code sign-in, token rotation, borrowing, security.
|
|
238
|
+
</td>
|
|
239
|
+
<td width="33%" valign="top">
|
|
240
|
+
<a href="docs/CLIENTS.md"><img src="docs/assets/thumbs/banner-clients.jpg" alt="Clients"></a><br>
|
|
241
|
+
<b><a href="docs/CLIENTS.md">Clients</a></b><br>
|
|
242
|
+
opencode, Claude Code, Cursor, VS Code, Windsurf, Gemini CLI and more.
|
|
243
|
+
</td>
|
|
244
|
+
</tr>
|
|
245
|
+
<tr>
|
|
246
|
+
<td width="33%" valign="top">
|
|
247
|
+
<a href="docs/BACKEND.md"><img src="docs/assets/thumbs/banner-backend.jpg" alt="Backend"></a><br>
|
|
248
|
+
<b><a href="docs/BACKEND.md">Backend</a></b><br>
|
|
249
|
+
How Codex's image generation really works: extracted and measured.
|
|
250
|
+
</td>
|
|
251
|
+
<td width="33%" valign="top">
|
|
252
|
+
<a href="docs/ARCHITECTURE.md"><img src="docs/assets/thumbs/banner-architecture.jpg" alt="Architecture"></a><br>
|
|
253
|
+
<b><a href="docs/ARCHITECTURE.md">Architecture</a></b><br>
|
|
254
|
+
Modules, the request flow and the design decisions behind them.
|
|
255
|
+
</td>
|
|
256
|
+
<td width="33%" valign="top">
|
|
257
|
+
<a href="docs/DEVELOPMENT.md"><img src="docs/assets/thumbs/banner-development.jpg" alt="Development"></a><br>
|
|
258
|
+
<b><a href="docs/DEVELOPMENT.md">Development</a></b><br>
|
|
259
|
+
Build, the mock-backed test suite, live testing, releases.
|
|
260
|
+
</td>
|
|
261
|
+
</tr>
|
|
262
|
+
</table>
|
|
263
|
+
|
|
264
|
+
## FAQ
|
|
265
|
+
|
|
266
|
+
<details>
|
|
267
|
+
<summary><b>Does it cost anything?</b></summary>
|
|
268
|
+
<br>
|
|
269
|
+
|
|
270
|
+
No extra money. Images come out of your ChatGPT plan's Codex usage limits, the same as images you make in the Codex app. `codex-imagegen-mcp status` (or the `auth_status` tool) shows the 5-hour and weekly windows, and checking them uses no quota.
|
|
271
|
+
|
|
272
|
+
</details>
|
|
273
|
+
|
|
274
|
+
<details>
|
|
275
|
+
<summary><b>Is the output the same as Codex's built-in image tool?</b></summary>
|
|
276
|
+
<br>
|
|
277
|
+
|
|
278
|
+
Yes. The endpoint, request body and auth headers are identical to the ones the Codex desktop app sends. The service picks the model, resolution and quality itself, for Codex and for this server alike.
|
|
279
|
+
|
|
280
|
+
</details>
|
|
281
|
+
|
|
282
|
+
<details>
|
|
283
|
+
<summary><b>Can I choose <code>gpt-image-2.5</code>, an exact size or a quality level?</b></summary>
|
|
284
|
+
<br>
|
|
285
|
+
|
|
286
|
+
Not with a ChatGPT sign-in. The service ignores `model`, `size`, `quality` and `n`. That was measured, including requests for the newer `gpt-image-2.5-sunburst` and `-flare` models, and the tools deliberately don't pretend otherwise. Choose the shape with `aspect_ratio`; for exact pixels, resize or crop afterwards. Selecting a model explicitly requires the billed OpenAI Platform API, which is out of scope by design. [The measurements →](docs/BACKEND.md#newer-image-models)
|
|
287
|
+
|
|
288
|
+
</details>
|
|
289
|
+
|
|
290
|
+
<details>
|
|
291
|
+
<summary><b>Where do images go?</b></summary>
|
|
292
|
+
<br>
|
|
293
|
+
|
|
294
|
+
They go to `output_path`, relative to your workspace, when you give one. Otherwise they land in the image library at `~/.local/share/codex-imagegen-mcp/images/<date>/`. Every file is also recorded in `imagegen://history`. Existing files are never replaced unless you pass `overwrite: true`.
|
|
295
|
+
|
|
296
|
+
</details>
|
|
297
|
+
|
|
298
|
+
<details>
|
|
299
|
+
<summary><b>Will it sign me out of Codex or opencode?</b></summary>
|
|
300
|
+
<br>
|
|
301
|
+
|
|
302
|
+
No. Borrowed sign-ins are read-only: they're used while their access token is valid and are never refreshed or written. Its own sign-in (`login`) is independent. [Why that matters →](docs/AUTH.md#why-borrowed-sign-ins-are-read-only)
|
|
303
|
+
|
|
304
|
+
</details>
|
|
305
|
+
|
|
306
|
+
<details>
|
|
307
|
+
<summary><b>Is this official?</b></summary>
|
|
308
|
+
<br>
|
|
309
|
+
|
|
310
|
+
No. It's an unofficial integration that uses the same public OAuth client and internal ChatGPT endpoints as the Codex CLI, as opencode's ChatGPT sign-in also does. Those interfaces are undocumented and may change. Use it within OpenAI's Terms of Use.
|
|
311
|
+
|
|
312
|
+
</details>
|
|
313
|
+
|
|
314
|
+
## Configuration
|
|
315
|
+
|
|
316
|
+
<details>
|
|
317
|
+
<summary>Environment variables</summary>
|
|
318
|
+
<br>
|
|
319
|
+
|
|
320
|
+
| Variable | Default | Purpose |
|
|
321
|
+
|---|---|---|
|
|
322
|
+
| `CODEX_IMAGEGEN_HOME` | `~/.local/share/codex-imagegen-mcp` | Credentials, history, logs and the default image library |
|
|
323
|
+
| `CODEX_IMAGEGEN_OUTPUT_DIR` | `$CODEX_IMAGEGEN_HOME/images` | Where images go when no `output_path` is given |
|
|
324
|
+
| `CODEX_IMAGEGEN_CREDENTIALS` | `auto` | `auto` (own → Codex → opencode), or pin `own`, `codex` or `opencode` |
|
|
325
|
+
| `CODEX_IMAGEGEN_TIMEOUT_MS` | `300000` | Per-request timeout for the image service |
|
|
326
|
+
| `CODEX_IMAGEGEN_LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` or `silent`; the log is `$CODEX_IMAGEGEN_HOME/server.log` |
|
|
327
|
+
| `CODEX_IMAGEGEN_NO_BROWSER` | unset | Never open a browser automatically |
|
|
328
|
+
| `CODEX_HOME` | `~/.codex` | Where to look for a Codex sign-in |
|
|
329
|
+
| `CODEX_IMAGEGEN_ORIGINATOR` | `codex-imagegen-mcp` | The `originator` identifying this client to OpenAI |
|
|
330
|
+
|
|
331
|
+
Test-only overrides: `CODEX_IMAGEGEN_BASE_URL`, `CODEX_IMAGEGEN_USAGE_URL`, `CODEX_IMAGEGEN_AUTH_ISSUER`, `CODEX_IMAGEGEN_CLIENT_ID`, `CODEX_IMAGEGEN_OPENCODE_AUTH_FILE`.
|
|
332
|
+
|
|
333
|
+
</details>
|
|
334
|
+
|
|
335
|
+
## Troubleshooting
|
|
336
|
+
|
|
337
|
+
Start with `codex-imagegen-mcp doctor`. It checks Node, the data directory, credentials, backend reachability and quota, the sign-in ports, the opencode entry and duplicate skills.
|
|
338
|
+
|
|
339
|
+
<details>
|
|
340
|
+
<summary>Common problems</summary>
|
|
341
|
+
<br>
|
|
342
|
+
|
|
343
|
+
| Symptom | Fix |
|
|
344
|
+
|---|---|
|
|
345
|
+
| `Not signed in` | `codex-imagegen-mcp login` (or `--device`), or open Codex/opencode once so their sign-in refreshes |
|
|
346
|
+
| `usage limit … resets in …` | Wait for the reset; `status` shows the windows |
|
|
347
|
+
| `ports 1455 and 1457 are busy` | Another Codex or opencode login is waiting. Finish it, or use `login --device` |
|
|
348
|
+
| `Device-code sign-in is not enabled` | ChatGPT → Settings → Security → allow device code authorization for Codex |
|
|
349
|
+
| Tools missing in opencode | Check `opencode mcp list`, re-run `install opencode`, then restart opencode |
|
|
350
|
+
| An opaque result despite `transparent` | Retry, or generate on a flat `#00FF00` backdrop and run `remove_background` |
|
|
351
|
+
|
|
352
|
+
Still stuck? See [getting help](.github/SUPPORT.md).
|
|
353
|
+
|
|
354
|
+
</details>
|
|
355
|
+
|
|
356
|
+
## Contributing
|
|
357
|
+
|
|
358
|
+
Bug reports, docs fixes, support for more clients and backend measurements are welcome. Start with [CONTRIBUTING.md](.github/CONTRIBUTING.md); security issues go through [SECURITY.md](.github/SECURITY.md), never public issues. Every release is listed in the [changelog](CHANGELOG.md).
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
<p align="center">
|
|
363
|
+
<img src="docs/assets/logo.png" width="64" alt="The Codex ImageGen MCP emblem: a sun rising behind a peak"><br>
|
|
364
|
+
<a href="CHANGELOG.md">Changelog</a> ·
|
|
365
|
+
<a href=".github/CONTRIBUTING.md">Contributing</a> ·
|
|
366
|
+
<a href=".github/SECURITY.md">Security</a> ·
|
|
367
|
+
<a href=".github/CODE_OF_CONDUCT.md">Code of Conduct</a> ·
|
|
368
|
+
<a href=".github/SUPPORT.md">Support</a><br>
|
|
369
|
+
<sub>
|
|
370
|
+
Unofficial; not affiliated with or endorsed by OpenAI. Usage counts against your ChatGPT plan and is subject to OpenAI's Terms of Use.<br>
|
|
371
|
+
Generated images carry OpenAI's C2PA provenance metadata. Apache-2.0; the bundled skill is adapted from OpenAI Codex (Apache-2.0), see <a href="NOTICE">NOTICE</a>.
|
|
372
|
+
</sub>
|
|
373
|
+
</p>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readJsonFile } from "../util/fs.js";
|
|
2
|
+
import { isRecord, num, str } from "../util/http.js";
|
|
3
|
+
import { identityFromTokens, jwtExpiryMs } from "./jwt.js";
|
|
4
|
+
async function load(file) {
|
|
5
|
+
try {
|
|
6
|
+
const value = await readJsonFile(file);
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return { ok: false, reason: "not signed in (file not found)", missing: true };
|
|
9
|
+
return { ok: true, value };
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
return { ok: false, reason: `unreadable (${err instanceof Error ? err.message : String(err)})`, missing: false };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Codex CLI / Codex desktop app: $CODEX_HOME/auth.json (file credential store only). */
|
|
16
|
+
export async function readCodexCredential(file) {
|
|
17
|
+
const loaded = await load(file);
|
|
18
|
+
if (!loaded.ok)
|
|
19
|
+
return loaded;
|
|
20
|
+
const raw = loaded.value;
|
|
21
|
+
if (!isRecord(raw))
|
|
22
|
+
return { ok: false, reason: "unrecognized format", missing: false };
|
|
23
|
+
const mode = str(raw.auth_mode);
|
|
24
|
+
const tokens = isRecord(raw.tokens) ? raw.tokens : undefined;
|
|
25
|
+
if (mode && mode !== "chatgpt") {
|
|
26
|
+
return { ok: false, reason: `Codex is signed in with "${mode}", not a ChatGPT account`, missing: false };
|
|
27
|
+
}
|
|
28
|
+
if (!tokens) {
|
|
29
|
+
const why = str(raw.OPENAI_API_KEY) ? "Codex is signed in with an API key, not a ChatGPT account" : "no ChatGPT tokens";
|
|
30
|
+
return { ok: false, reason: why, missing: false };
|
|
31
|
+
}
|
|
32
|
+
const accessToken = str(tokens.access_token);
|
|
33
|
+
if (!accessToken)
|
|
34
|
+
return { ok: false, reason: "no access token", missing: false };
|
|
35
|
+
// Codex only sends auth headers when last_refresh is present; mirror that sanity check.
|
|
36
|
+
if (!str(raw.last_refresh))
|
|
37
|
+
return { ok: false, reason: "incomplete sign-in (no last_refresh)", missing: false };
|
|
38
|
+
const idToken = str(tokens.id_token);
|
|
39
|
+
const identity = identityFromTokens(idToken, accessToken);
|
|
40
|
+
const credential = { source: "codex", file, accessToken, identity };
|
|
41
|
+
const accountId = str(tokens.account_id) ?? identity.accountId;
|
|
42
|
+
if (accountId)
|
|
43
|
+
credential.accountId = accountId;
|
|
44
|
+
const expiresAt = jwtExpiryMs(accessToken);
|
|
45
|
+
if (expiresAt !== undefined)
|
|
46
|
+
credential.expiresAt = expiresAt;
|
|
47
|
+
return { ok: true, credential };
|
|
48
|
+
}
|
|
49
|
+
/** opencode "ChatGPT Plus/Pro" OAuth: ~/.local/share/opencode/auth.json -> { openai: { type: "oauth", ... } } */
|
|
50
|
+
export async function readOpencodeCredential(file) {
|
|
51
|
+
const loaded = await load(file);
|
|
52
|
+
if (!loaded.ok)
|
|
53
|
+
return loaded;
|
|
54
|
+
const raw = loaded.value;
|
|
55
|
+
if (!isRecord(raw))
|
|
56
|
+
return { ok: false, reason: "unrecognized format", missing: false };
|
|
57
|
+
const entry = raw.openai;
|
|
58
|
+
if (!isRecord(entry))
|
|
59
|
+
return { ok: false, reason: "no OpenAI sign-in (run `opencode auth login` → OpenAI → ChatGPT)", missing: true };
|
|
60
|
+
if (entry.type !== "oauth")
|
|
61
|
+
return { ok: false, reason: "opencode's OpenAI provider uses an API key, not a ChatGPT sign-in", missing: false };
|
|
62
|
+
const accessToken = str(entry.access);
|
|
63
|
+
if (!accessToken)
|
|
64
|
+
return { ok: false, reason: "no access token", missing: false };
|
|
65
|
+
const identity = identityFromTokens(undefined, accessToken);
|
|
66
|
+
const credential = { source: "opencode", file, accessToken, identity };
|
|
67
|
+
const accountId = str(entry.accountId) ?? identity.accountId;
|
|
68
|
+
if (accountId)
|
|
69
|
+
credential.accountId = accountId;
|
|
70
|
+
const expires = num(entry.expires);
|
|
71
|
+
const jwtExp = jwtExpiryMs(accessToken);
|
|
72
|
+
const expiresAt = expires !== undefined && jwtExp !== undefined ? Math.min(expires, jwtExp) : (expires ?? jwtExp);
|
|
73
|
+
if (expiresAt !== undefined)
|
|
74
|
+
credential.expiresAt = expiresAt;
|
|
75
|
+
return { ok: true, credential };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=borrowed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"borrowed.js","sourceRoot":"","sources":["../../../src/auth/borrowed.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { BROWSER_LOGIN_TIMEOUT_MS, OAUTH_CALLBACK_PATH, OAUTH_CALLBACK_PORTS } from "../constants.js";
|
|
3
|
+
import { ImagegenError, toImagegenError } from "../errors.js";
|
|
4
|
+
import { sleep } from "../util/http.js";
|
|
5
|
+
import { identityFromTokens } from "./jwt.js";
|
|
6
|
+
import { buildAuthorizeUrl, exchangeAuthorizationCode } from "./oauth.js";
|
|
7
|
+
import { errorPage, successPage } from "./pages.js";
|
|
8
|
+
import { createPkce, createState } from "./pkce.js";
|
|
9
|
+
function listenOnce(server, port) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const onError = (err) => {
|
|
12
|
+
server.off("listening", onListening);
|
|
13
|
+
reject(err);
|
|
14
|
+
};
|
|
15
|
+
const onListening = () => {
|
|
16
|
+
server.off("error", onError);
|
|
17
|
+
resolve();
|
|
18
|
+
};
|
|
19
|
+
server.once("error", onError);
|
|
20
|
+
server.once("listening", onListening);
|
|
21
|
+
server.listen(port, "127.0.0.1");
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Ask a stale login server (ours or Codex's) on this port to shut down, like Codex does. */
|
|
25
|
+
async function requestCancel(port) {
|
|
26
|
+
try {
|
|
27
|
+
await fetch(`http://127.0.0.1:${port}/cancel`, { signal: AbortSignal.timeout(2_000) });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* nothing listening that understands /cancel */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function bindCallbackServer(server, ports) {
|
|
34
|
+
for (const [index, port] of ports.entries()) {
|
|
35
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
36
|
+
try {
|
|
37
|
+
await listenOnce(server, port);
|
|
38
|
+
return server.address().port;
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (err.code !== "EADDRINUSE")
|
|
42
|
+
throw err;
|
|
43
|
+
if (index === 0 && attempt === 0)
|
|
44
|
+
await requestCancel(port);
|
|
45
|
+
await sleep(200);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
throw new ImagegenError("login_failed", `Could not start the local sign-in callback server: port${ports.length > 1 ? "s" : ""} ${ports.join(" and ")} ${ports.length > 1 ? "are" : "is"} busy. ` +
|
|
50
|
+
"Another Codex/opencode sign-in may be in progress — finish or close it, or use device-code sign-in instead.");
|
|
51
|
+
}
|
|
52
|
+
function friendlyAuthorizeError(error, description) {
|
|
53
|
+
if (error === "access_denied" && description.includes("missing_codex_entitlement")) {
|
|
54
|
+
return "Codex is not enabled for your ChatGPT workspace. Ask your workspace admin to enable Codex, then try again.";
|
|
55
|
+
}
|
|
56
|
+
if (error === "access_denied")
|
|
57
|
+
return `Sign-in was denied${description ? `: ${description}` : "."}`;
|
|
58
|
+
return `Sign-in failed: ${error}${description ? ` — ${description}` : ""}`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Start the browser (authorization code + PKCE) flow. Binds the loopback callback server first so
|
|
62
|
+
* the returned URL is immediately usable; the caller decides how to show/open it.
|
|
63
|
+
*/
|
|
64
|
+
export async function startBrowserLogin(options) {
|
|
65
|
+
const pkce = createPkce();
|
|
66
|
+
const state = createState();
|
|
67
|
+
const sockets = new Set();
|
|
68
|
+
let resolveResult;
|
|
69
|
+
let rejectResult;
|
|
70
|
+
const result = new Promise((resolve, reject) => {
|
|
71
|
+
resolveResult = resolve;
|
|
72
|
+
rejectResult = reject;
|
|
73
|
+
});
|
|
74
|
+
// Avoid unhandled-rejection noise if the caller never awaits (e.g. cancelled in the background).
|
|
75
|
+
result.catch(() => undefined);
|
|
76
|
+
let settled = false;
|
|
77
|
+
let timer;
|
|
78
|
+
let redirectUri = "";
|
|
79
|
+
let port = 0;
|
|
80
|
+
const server = http.createServer((req, res) => {
|
|
81
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
82
|
+
const html = (status, body) => {
|
|
83
|
+
res.writeHead(status, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", connection: "close" });
|
|
84
|
+
res.end(body);
|
|
85
|
+
};
|
|
86
|
+
if (url.pathname === OAUTH_CALLBACK_PATH) {
|
|
87
|
+
if (settled) {
|
|
88
|
+
html(200, errorPage("This sign-in attempt already finished. You can close this tab."));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const q = url.searchParams;
|
|
92
|
+
if (q.get("state") !== state) {
|
|
93
|
+
// Possibly a stale tab from an earlier attempt: reject it but keep waiting.
|
|
94
|
+
res.writeHead(400, { "content-type": "text/plain; charset=utf-8", connection: "close" });
|
|
95
|
+
res.end("State mismatch");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const error = q.get("error");
|
|
99
|
+
if (error) {
|
|
100
|
+
const message = friendlyAuthorizeError(error, q.get("error_description") ?? "");
|
|
101
|
+
html(200, errorPage(message));
|
|
102
|
+
finish(new ImagegenError("login_failed", message, { details: { error } }));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const code = q.get("code");
|
|
106
|
+
if (!code) {
|
|
107
|
+
html(200, errorPage("The authorization code is missing from the callback."));
|
|
108
|
+
finish(new ImagegenError("login_failed", "The authorization code is missing from the callback."));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
exchangeAuthorizationCode(options, { code, redirectUri, codeVerifier: pkce.verifier }).then((tokens) => {
|
|
112
|
+
const email = identityFromTokens(tokens.idToken, tokens.accessToken).email ?? "";
|
|
113
|
+
res.writeHead(302, { location: `/success?email=${encodeURIComponent(email)}`, connection: "close" });
|
|
114
|
+
res.end();
|
|
115
|
+
finish(undefined, tokens);
|
|
116
|
+
}, (err) => {
|
|
117
|
+
const e = toImagegenError(err, "login_failed");
|
|
118
|
+
html(200, errorPage(e.message));
|
|
119
|
+
finish(e);
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (url.pathname === "/success") {
|
|
124
|
+
html(200, successPage(url.searchParams.get("email") || undefined));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (url.pathname === "/cancel") {
|
|
128
|
+
res.writeHead(200, { "content-type": "text/plain; charset=utf-8", connection: "close" });
|
|
129
|
+
res.end("Login cancelled");
|
|
130
|
+
finish(new ImagegenError("login_cancelled", "Sign-in was cancelled (a newer sign-in attempt took over the callback port)."));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8", connection: "close" });
|
|
134
|
+
res.end("Not found");
|
|
135
|
+
});
|
|
136
|
+
server.on("connection", (socket) => {
|
|
137
|
+
sockets.add(socket);
|
|
138
|
+
socket.on("close", () => sockets.delete(socket));
|
|
139
|
+
});
|
|
140
|
+
const shutdown = () => {
|
|
141
|
+
server.close();
|
|
142
|
+
for (const s of sockets)
|
|
143
|
+
s.destroy();
|
|
144
|
+
};
|
|
145
|
+
function finish(err, tokens) {
|
|
146
|
+
if (settled)
|
|
147
|
+
return;
|
|
148
|
+
settled = true;
|
|
149
|
+
if (timer)
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
152
|
+
// Keep serving briefly so the browser can load /success after the redirect.
|
|
153
|
+
setTimeout(shutdown, 1_500).unref();
|
|
154
|
+
if (err)
|
|
155
|
+
rejectResult(err);
|
|
156
|
+
else
|
|
157
|
+
resolveResult(tokens);
|
|
158
|
+
}
|
|
159
|
+
const onAbort = () => finish(new ImagegenError("login_cancelled", "Sign-in was cancelled."));
|
|
160
|
+
port = await bindCallbackServer(server, options.ports ?? OAUTH_CALLBACK_PORTS);
|
|
161
|
+
redirectUri = `http://localhost:${port}${OAUTH_CALLBACK_PATH}`;
|
|
162
|
+
const timeoutMs = options.timeoutMs ?? BROWSER_LOGIN_TIMEOUT_MS;
|
|
163
|
+
timer = setTimeout(() => finish(new ImagegenError("login_failed", `Timed out after ${Math.round(timeoutMs / 60_000)} minutes waiting for the browser sign-in.`)), timeoutMs);
|
|
164
|
+
timer.unref();
|
|
165
|
+
if (options.signal?.aborted)
|
|
166
|
+
onAbort();
|
|
167
|
+
else
|
|
168
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
169
|
+
const url = buildAuthorizeUrl({
|
|
170
|
+
issuer: options.issuer,
|
|
171
|
+
clientId: options.clientId,
|
|
172
|
+
redirectUri,
|
|
173
|
+
codeChallenge: pkce.challenge,
|
|
174
|
+
state,
|
|
175
|
+
originator: options.originator,
|
|
176
|
+
});
|
|
177
|
+
return { url, redirectUri, port, result, cancel: onAbort };
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=browser-login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-login.js","sourceRoot":"","sources":["../../../src/auth/browser-login.ts"],"names":[],"mappings":"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"}
|