relmio 0.2.0
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 +190 -0
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/SPEC.md +140 -0
- package/docs/architecture.md +127 -0
- package/docs/brand.md +43 -0
- package/docs/images/brand/relmio-concept-source.png +0 -0
- package/docs/images/brand/relmio-mark.svg +16 -0
- package/docs/images/setup/01-local-sign-in-ready.png +0 -0
- package/docs/images/setup/02-vps-identity-confirmed.png +0 -0
- package/docs/images/setup/03-n8n-detected.png +0 -0
- package/docs/images/setup/04-install-plan.png +0 -0
- package/docs/images/setup/05-bridge-ready.png +0 -0
- package/docs/maintenance.md +157 -0
- package/docs/manual-install.md +282 -0
- package/docs/n8n-configuration.md +199 -0
- package/docs/npm-publish.md +285 -0
- package/docs/roadmap.md +109 -0
- package/docs/security.md +105 -0
- package/docs/troubleshooting.md +193 -0
- package/docs/video-outline.md +152 -0
- package/package.json +45 -0
- package/scripts/build-npm-package.js +112 -0
- package/scripts/check-release-metadata.js +100 -0
- package/scripts/check-syntax.js +59 -0
- package/scripts/preview.js +69 -0
- package/src/cli.js +57 -0
- package/src/domain/safety.js +59 -0
- package/src/domain/templates.js +65 -0
- package/src/domain/validation.js +76 -0
- package/src/infrastructure/ssh.js +268 -0
- package/src/services/discovery.js +96 -0
- package/src/services/installer.js +239 -0
- package/src/services/oauth.js +351 -0
- package/src/ui/app.js +526 -0
- package/src/ui/index.html +440 -0
- package/src/ui/styles.css +645 -0
- package/src/ui/time.js +15 -0
- package/src/web/server.js +489 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Start with the exact symptom you see. Do not delete or rebuild n8n while
|
|
4
|
+
troubleshooting this sidecar. Do not remove an already working manual OAuth
|
|
5
|
+
sidecar until the wizard-managed endpoint has passed a real n8n request.
|
|
6
|
+
|
|
7
|
+
Always keep a current export or backup of your n8n workflows before using the
|
|
8
|
+
wizard or any manual VPS command. The documented commands are sidecar-only and
|
|
9
|
+
do not delete, restart, or rebuild n8n, but they still access your VPS and write
|
|
10
|
+
files there.
|
|
11
|
+
|
|
12
|
+
## Confirm the local package first
|
|
13
|
+
|
|
14
|
+
Run these commands on your own computer, not on the VPS:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
node --version
|
|
18
|
+
npm view relmio version
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Node must be version 22 or newer. Then close every old wizard terminal and
|
|
22
|
+
browser tab and start the newest published build:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx --yes --ignore-scripts relmio@latest
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Keep the terminal open. If the browser does not open automatically, copy the
|
|
29
|
+
newest printed `http://127.0.0.1:...` URL into the browser. That URL contains a
|
|
30
|
+
temporary setup token: do not post it in an issue or screenshot.
|
|
31
|
+
|
|
32
|
+
You do not need to sign in to npm, configure npm 2FA, or own this package to
|
|
33
|
+
run its public `npx` command. npm authentication is required only for the
|
|
34
|
+
maintainer who publishes a release.
|
|
35
|
+
|
|
36
|
+
## Quick VPS checks
|
|
37
|
+
|
|
38
|
+
On the VPS:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
docker compose \
|
|
42
|
+
--project-name n8n-openai-oauth \
|
|
43
|
+
--file /docker/n8n-openai-oauth/docker-compose.yml \
|
|
44
|
+
ps
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
docker compose \
|
|
49
|
+
--project-name n8n-openai-oauth \
|
|
50
|
+
--file /docker/n8n-openai-oauth/docker-compose.yml \
|
|
51
|
+
logs --tail=50 openai-oauth
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Check whether Docker published the sidecar port:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
docker compose \
|
|
58
|
+
--project-name n8n-openai-oauth \
|
|
59
|
+
--file /docker/n8n-openai-oauth/docker-compose.yml \
|
|
60
|
+
port openai-oauth 10531
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Success is no output. `10531/tcp` shown in `docker ps` without a host address
|
|
64
|
+
is only an internal container port. A value such as `0.0.0.0:10531` or
|
|
65
|
+
`127.0.0.1:10531` is a real host mapping and must be investigated rather than
|
|
66
|
+
bypassed.
|
|
67
|
+
|
|
68
|
+
## Symptom table
|
|
69
|
+
|
|
70
|
+
| Symptom | Meaning | Fix |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `node: command not found`, `node is not recognized`, or Node is older than 22 | The local runtime is missing or unsupported. | Install Node.js 22 or newer on the local computer, open a new terminal, and rerun the `@latest` command. Do not install it on the VPS for the wizard. |
|
|
73
|
+
| The browser did not open | The automatic browser launch failed, but the local server may still be running. | Keep the newest terminal open and copy its newest `127.0.0.1` setup URL into the browser. Do not reuse a URL from a closed terminal. |
|
|
74
|
+
| An old wizard page reports an invalid or expired setup session | The local server was closed or a newer wizard run created a different one-time session token. | Close the old page and use only the URL printed by the currently running terminal. |
|
|
75
|
+
| `npx` appears to run an older wizard | An old terminal or tab is still active, or the package was run without an explicit tag. | Close old runs, check `npm view relmio version`, then run `npx --yes --ignore-scripts relmio@latest`. |
|
|
76
|
+
| `This sign-in request expired` | The OAuth tab is old or the five-minute callback window ended. | Close the old tab and select **Refresh ChatGPT sign-in** from the newest active wizard. |
|
|
77
|
+
| An **OpenAI OAuth** extension page says the sign-in request expired | A browser extension intercepted the `localhost:1455` callback that belongs to the wizard's fresh login. | Temporarily disable the **Sign in with ChatGPT** or **OpenAI OAuth** extension, then select **Refresh ChatGPT sign-in** in the wizard. Re-enable the extension afterward if you still use it elsewhere. |
|
|
78
|
+
| `ChatGPT sign-in did not finish` appears immediately when refreshing an existing credential | Wizard versions through `0.1.3` attempted to reuse `~/.codex/auth.json`, but the bridge CLI requires an interactive terminal before replacing that file. | Update to `0.1.4` or newer. The wizard signs in through its own new credential file and leaves the Codex app credential untouched. |
|
|
79
|
+
| The wizard keeps showing `Waiting for browser sign-in` after approval | Older versions waited for the OAuth helper process to close even after its credential file was ready. | Update to `0.1.5` or newer. Confirm the new **Credential updated** time appears before continuing. |
|
|
80
|
+
| **Credential updated** still shows the old time | The callback reached an old/expired tab, was intercepted, or a different wizard session is open. | Close every old OAuth and wizard tab. Keep one current wizard open, select **Refresh ChatGPT sign-in**, and complete only the newly opened page. |
|
|
81
|
+
| The fresh login cannot bind `localhost:1455` or reports the address is in use | Another OAuth helper or extension process already owns the local callback port. | Close other OAuth login tools and stale wizard processes, then retry. On macOS/Linux, inspect without killing anything using `lsof -nP -iTCP:1455 -sTCP:LISTEN`. |
|
|
82
|
+
| `SSH connection failed. Check the address, password, firewall, and confirmed fingerprint.` | The TCP connection, password authentication, or confirmed host identity did not succeed. | Copy the full address and port from the provider, confirm root password login is enabled, check the provider firewall, rescan and compare the fingerprint, then test `ssh -p 22 root@YOUR_VPS_IP` from the same computer. |
|
|
83
|
+
| The VPS accepts only an SSH key or passkey | The current wizard supports live password authentication, not SSH keys. | Use the manual installation path or a provider-approved password-authenticated administrator account. Do not weaken SSH security or upload a private key into the wizard. |
|
|
84
|
+
| The SSH fingerprint changed | The server was rebuilt, its host keys changed, or the connection may be reaching a different host. | Stop. Verify the address and the new fingerprint through the VPS provider console before confirming it. Never bypass the comparison. |
|
|
85
|
+
| The wizard cannot find n8n | No running container matches the supported n8n image discovery. | Run `docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'` on the VPS. Start or repair n8n through its own deployment process; do not make the wizard recreate it. |
|
|
86
|
+
| No shared Docker network is listed | n8n is not attached to a usable user-defined network. | Inspect n8n's networks and choose one the sidecar can join. Do not publish port `10531` as a workaround. |
|
|
87
|
+
| Safety check says the sidecar published a host port even though Docker shows only `10531/tcp` | Older checks could interpret Docker Compose's internal-only `PublishedPort: 0` marker as a host binding. | Update to `0.1.4` or newer. The wizard now reads the full publisher metadata and still rejects any real host binding. |
|
|
88
|
+
| Safety check reports a real host binding such as `0.0.0.0:10531` | A manual or altered Compose configuration published the port. The latest wizard attempts to stop and remove only its sidecar project before reporting the failure. | Do not bypass the safety check. Confirm the sidecar project is down with the commands above, remove the `ports:` mapping from that sidecar only, and redeploy it without touching n8n. If cleanup could not be confirmed, stop and inspect `/docker/n8n-openai-oauth` before retrying. |
|
|
89
|
+
| `zsh: no matches found: root@**...**` | The hidden-IP asterisks were copied literally. | Use the real IP with no asterisks: `root@YOUR_VPS_IP`. |
|
|
90
|
+
| SSH appears frozen while typing a password | Terminals intentionally show no password characters. | Type the password carefully and press Return. Do not test by typing random visible text. |
|
|
91
|
+
| SSH appears to do nothing | The IP may be incomplete, port 22 may be blocked, or SSH is waiting. | Copy the complete IP from Hostinger. Wait up to 15 seconds, then press Control+C and retry. |
|
|
92
|
+
| `No such file or directory` after local `chown` | A VPS path was used in the local Terminal. | SSH into the VPS first, then run `chown` there. |
|
|
93
|
+
| `No auth file was found at /home/node/.codex/auth.json` | The file is missing, copied to the wrong directory, or the parent directory blocks user `node`. | Verify the mount, owner, and modes using the commands below. |
|
|
94
|
+
| `unknown instruction: "--host"` | The Dockerfile `CMD` JSON was split across Dockerfile instructions. | Replace it with the exact one-line `CMD` from the manual guide. |
|
|
95
|
+
| n8n credential says it cannot connect with `127.0.0.1` | `127.0.0.1` inside n8n is the n8n container, not the sidecar. | Use `http://n8n-openai-oauth:10531/v1`. |
|
|
96
|
+
| Logs show `ENOENT` for `/home/node/.local` | An older wizard release used a read-only root filesystem without a writable app-data directory. | Update to the latest wizard and run the approved install again. It safely refreshes a wizard-managed sidecar. |
|
|
97
|
+
| Network command prints `proxy` | That is the network name, not an empty result. | Select or enter `proxy`. |
|
|
98
|
+
| Logs show repeated “No auth file” and later show “endpoint ready” | `docker compose logs` contains old and new entries. | Read the newest lines at the bottom. The final “endpoint ready” state wins. |
|
|
99
|
+
| n8n requires an API key | The n8n credential UI requires a non-empty value even though the bridge does not. | Enter `local-only`; it is a placeholder, not an OpenAI key. |
|
|
100
|
+
| n8n reports `ECONNREFUSED`, `ENOTFOUND`, or “Couldn’t connect” | The Base URL is wrong, the sidecar is unhealthy, or n8n and the sidecar do not share a network. | Use exactly `http://n8n-openai-oauth:10531/v1`, inspect both container networks, and check the sidecar health/logs. |
|
|
101
|
+
| Models do not appear in n8n | Credential test, network, auth, or model compatibility may be failing. | Verify `/v1/models` inside the sidecar, then retry the n8n credential. |
|
|
102
|
+
| Responses API request fails but models work | The n8n node or bridge version may be incompatible. | Confirm the project is pinned to `openai-oauth@2.0.0`. Try a basic `/v1/responses` request; use chat completions only as a compatibility fallback. |
|
|
103
|
+
| Wizard refuses the install directory | `/docker/n8n-openai-oauth` exists without the wizard marker. | Nothing was overwritten. Move the old directory to a backup name or finish the manual installation; do not delete it blindly. |
|
|
104
|
+
| A manually created `openai-oauth` container already works | It usually does not block the wizard because the wizard uses a separate project, directory, and collision-resistant hostname. | Keep the working deployment until the new endpoint passes a test. If an exact directory, project, container, or network alias collides, move or rename only the old sidecar after backing it up; never remove n8n. |
|
|
105
|
+
|
|
106
|
+
## Check the OAuth file safely
|
|
107
|
+
|
|
108
|
+
Do not run `cat` on the file. Check only its metadata:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
ls -ldn /docker/n8n-openai-oauth/auth
|
|
112
|
+
ls -ln /docker/n8n-openai-oauth/auth/auth.json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Expected:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
auth directory: owner 1000, group 1000, mode drwx------
|
|
119
|
+
auth.json: owner 1000, group 1000, mode -rw-------
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Fix on the VPS:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
chown 1000:1000 /docker/n8n-openai-oauth/auth
|
|
126
|
+
chmod 700 /docker/n8n-openai-oauth/auth
|
|
127
|
+
chown 1000:1000 /docker/n8n-openai-oauth/auth/auth.json
|
|
128
|
+
chmod 600 /docker/n8n-openai-oauth/auth/auth.json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Check the mount
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
docker inspect n8n-openai-oauth-openai-oauth-1 \
|
|
135
|
+
--format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Expected:
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
/docker/n8n-openai-oauth/auth -> /home/node/.codex
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The generated container name can differ. Find it with:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
docker compose \
|
|
148
|
+
--project-name n8n-openai-oauth \
|
|
149
|
+
--file /docker/n8n-openai-oauth/docker-compose.yml \
|
|
150
|
+
ps
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Check the shared network
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
docker inspect n8n-n8n-1 \
|
|
157
|
+
--format '{{range $name, $_ := .NetworkSettings.Networks}}{{println $name}}{{end}}'
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
docker inspect n8n-openai-oauth-openai-oauth-1 \
|
|
162
|
+
--format '{{range $name, $_ := .NetworkSettings.Networks}}{{println $name}}{{end}}'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
At least one name must match. For the Hostinger setup used during development,
|
|
166
|
+
that name was `proxy`.
|
|
167
|
+
|
|
168
|
+
## Check from n8n without installing curl
|
|
169
|
+
|
|
170
|
+
Do not modify the n8n image merely to add a diagnostic tool. Use Node if it is
|
|
171
|
+
available in the n8n container:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
docker exec n8n-n8n-1 \
|
|
175
|
+
node -e 'fetch("http://n8n-openai-oauth:10531/v1/models").then(async (response) => { console.log(response.status); console.log(await response.text()); }).catch((error) => { console.error(error.message); process.exit(1); })'
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
This is a read-only diagnostic request; it does not install anything or
|
|
179
|
+
restart n8n.
|
|
180
|
+
|
|
181
|
+
## Responses API setting
|
|
182
|
+
|
|
183
|
+
On OpenAI Chat Model node version 1.3, leave **Use Responses API** on. Earlier
|
|
184
|
+
node versions do not show that switch and use Chat Completions by default.
|
|
185
|
+
Upstream supports both `/v1/responses` and `/v1/chat/completions`. Turn the
|
|
186
|
+
switch off only as a temporary compatibility test if:
|
|
187
|
+
|
|
188
|
+
- `/v1/models` works;
|
|
189
|
+
- the node is definitely calling the correct Base URL; and
|
|
190
|
+
- the error specifically concerns `/v1/responses`.
|
|
191
|
+
|
|
192
|
+
If chat completions work but Responses does not, record the n8n version, node
|
|
193
|
+
version, bridge logs, and sanitized error before changing anything else.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# YouTube walkthrough outline
|
|
2
|
+
|
|
3
|
+
This outline explains the project without overstating what the unofficial
|
|
4
|
+
OAuth bridge provides. Adjust the timing to match your recording.
|
|
5
|
+
|
|
6
|
+
## Suggested title
|
|
7
|
+
|
|
8
|
+
> Relmio: connect self-hosted n8n to a private OpenAI-compatible OAuth sidecar
|
|
9
|
+
|
|
10
|
+
Avoid titles that promise a free API key, unlimited usage, or guaranteed
|
|
11
|
+
credits. This project does not create an OpenAI Platform API key.
|
|
12
|
+
|
|
13
|
+
## Seven-minute structure
|
|
14
|
+
|
|
15
|
+
### 0:00 — The problem
|
|
16
|
+
|
|
17
|
+
- n8n's OpenAI credential expects an API key and supports a custom Base URL.
|
|
18
|
+
- You already have a self-hosted n8n Docker deployment.
|
|
19
|
+
- You want a private, experimental bridge without editing or restarting n8n.
|
|
20
|
+
|
|
21
|
+
On screen: the README warning and the final architecture diagram.
|
|
22
|
+
|
|
23
|
+
### 0:40 — The safety promise
|
|
24
|
+
|
|
25
|
+
- The wizard runs on the user's own computer.
|
|
26
|
+
- It creates a second Docker Compose project.
|
|
27
|
+
- It never edits the n8n Compose file or image.
|
|
28
|
+
- It never publishes sidecar port `10531`.
|
|
29
|
+
- It requires an SSH fingerprint check and final plan approval.
|
|
30
|
+
|
|
31
|
+
On screen: the **Sidecar-only guarantee** and Step 4 review screen.
|
|
32
|
+
|
|
33
|
+
### 1:30 — Why the design works
|
|
34
|
+
|
|
35
|
+
Use this simple explanation:
|
|
36
|
+
|
|
37
|
+
> n8n sends an OpenAI-shaped request to a private Docker hostname. The
|
|
38
|
+
> sidecar translates that request into the upstream OAuth-authenticated flow
|
|
39
|
+
> and returns an OpenAI-compatible response. Docker networking keeps the
|
|
40
|
+
> endpoint between containers instead of publishing it on the internet.
|
|
41
|
+
|
|
42
|
+
On screen: the Mermaid flowchart in the README.
|
|
43
|
+
|
|
44
|
+
### 2:20 — Start the npm package
|
|
45
|
+
|
|
46
|
+
Run locally:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx --yes --ignore-scripts relmio@latest
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Explain that:
|
|
53
|
+
|
|
54
|
+
- `npx` downloads and runs the published version for this session;
|
|
55
|
+
- the wizard binds to `127.0.0.1`;
|
|
56
|
+
- the printed setup URL is private and temporary;
|
|
57
|
+
- the terminal must remain open until the wizard finishes.
|
|
58
|
+
|
|
59
|
+
### 2:55 — Sign in and verify freshness
|
|
60
|
+
|
|
61
|
+
- Complete the newest ChatGPT sign-in page.
|
|
62
|
+
- Temporarily disable an OAuth browser extension only if it intercepts the
|
|
63
|
+
`localhost:1455` callback.
|
|
64
|
+
- Show the **Credential updated** timestamp.
|
|
65
|
+
- Explain that the wizard uses
|
|
66
|
+
`~/.n8n-openai-oauth/auth.json`, not `~/.codex/auth.json`.
|
|
67
|
+
|
|
68
|
+
Never show the auth file, browser session URL, cookies, or account details.
|
|
69
|
+
|
|
70
|
+
### 3:40 — Connect to the VPS safely
|
|
71
|
+
|
|
72
|
+
- Enter the complete address and SSH port.
|
|
73
|
+
- Check the SHA-256 fingerprint before the password field unlocks.
|
|
74
|
+
- Explain that the password is used only for the live SSH connection and is
|
|
75
|
+
not saved.
|
|
76
|
+
|
|
77
|
+
Blur or replace the real address, fingerprint, username, and provider account
|
|
78
|
+
details in the recording.
|
|
79
|
+
|
|
80
|
+
### 4:25 — Detect n8n and approve the plan
|
|
81
|
+
|
|
82
|
+
- Select the running n8n container.
|
|
83
|
+
- Select the network shared with the sidecar.
|
|
84
|
+
- Read the **will** and **will not** columns.
|
|
85
|
+
- Approve the exact plan.
|
|
86
|
+
|
|
87
|
+
Emphasize that discovery is read-only and remote writes begin only after
|
|
88
|
+
approval.
|
|
89
|
+
|
|
90
|
+
### 5:15 — Configure n8n
|
|
91
|
+
|
|
92
|
+
Use:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
API Key: local-only
|
|
96
|
+
Organization ID: leave empty
|
|
97
|
+
Base URL: http://n8n-openai-oauth:10531/v1
|
|
98
|
+
Add Custom Header: off
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Then add an OpenAI Chat Model to the AI Agent or Basic LLM Chain. For Chat
|
|
102
|
+
Model node version 1.3, keep **Use Responses API** on. If the switch is absent,
|
|
103
|
+
use the earlier node version's default Chat Completions behavior. Test one
|
|
104
|
+
simple prompt before adding tools. The model list depends on the signed-in
|
|
105
|
+
account and may change.
|
|
106
|
+
|
|
107
|
+
### 6:10 — Refresh and troubleshoot
|
|
108
|
+
|
|
109
|
+
- Run the same `@latest` command to refresh a session or update the
|
|
110
|
+
wizard-managed sidecar.
|
|
111
|
+
- If n8n cannot connect, check the shared network and exact Base URL.
|
|
112
|
+
- If sign-in expires, close the old tab and begin again from the active
|
|
113
|
+
wizard.
|
|
114
|
+
- If SSH fails, verify the address, port, firewall, password, and fingerprint.
|
|
115
|
+
|
|
116
|
+
Point viewers to [Troubleshooting](troubleshooting.md).
|
|
117
|
+
|
|
118
|
+
### 6:45 — Close with the limitations
|
|
119
|
+
|
|
120
|
+
- This is unofficial and experimental.
|
|
121
|
+
- It does not create an OpenAI Platform API key.
|
|
122
|
+
- ChatGPT and OpenAI API billing are separate products.
|
|
123
|
+
- Eligibility, models, and rate limits can change.
|
|
124
|
+
- Users must protect the OAuth credential and follow current terms.
|
|
125
|
+
|
|
126
|
+
## Recording safety checklist
|
|
127
|
+
|
|
128
|
+
- Use a disposable or sanitized demo VPS.
|
|
129
|
+
- Use a reserved documentation address such as `192.0.2.10` in mock screens.
|
|
130
|
+
- Hide browser address bars containing wizard session tokens.
|
|
131
|
+
- Never open or print `auth.json`.
|
|
132
|
+
- Never record an SSH password, private key, cookie, passkey prompt, or 2FA
|
|
133
|
+
code.
|
|
134
|
+
- Blur real fingerprints, hostnames, account email addresses, and workflow
|
|
135
|
+
data.
|
|
136
|
+
- Confirm that port `10531` has no host mapping.
|
|
137
|
+
- Show the version used in the recording:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npm view relmio version
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Reference links for the description
|
|
144
|
+
|
|
145
|
+
- [`openai-oauth` v2.0.0](https://github.com/EvanZhouDev/openai-oauth/releases/tag/v2.0.0)
|
|
146
|
+
- [n8n OpenAI credential documentation](https://docs.n8n.io/integrations/builtin/credentials/openai/)
|
|
147
|
+
- [Docker Compose networking](https://docs.docker.com/compose/how-tos/networking/)
|
|
148
|
+
- [OpenAI: using Codex with a ChatGPT plan](https://help.openai.com/en/articles/11369540-using-codex-with-your-chatgpt-plan)
|
|
149
|
+
- [OpenAI Terms of Use](https://openai.com/policies/terms-of-use/)
|
|
150
|
+
|
|
151
|
+
Add the final video URL to the README only after the video is published. Do
|
|
152
|
+
not invent or reuse an unrelated tutorial link.
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "relmio",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Turn a supported ChatGPT/Codex OAuth sign-in into a private OpenAI-compatible endpoint, starting with self-hosted n8n.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Demonbane18/n8n-openai-oauth-setup.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Demonbane18/n8n-openai-oauth-setup/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/Demonbane18/n8n-openai-oauth-setup#readme",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"packageManager": "npm@10.9.8",
|
|
19
|
+
"bin": {
|
|
20
|
+
"relmio": "src/cli.js",
|
|
21
|
+
"planrelay": "src/cli.js",
|
|
22
|
+
"n8n-openai-oauth-setup": "src/cli.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"scripts",
|
|
27
|
+
"docs",
|
|
28
|
+
"README.md",
|
|
29
|
+
"SPEC.md",
|
|
30
|
+
"CHANGELOG.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"start": "node src/cli.js",
|
|
35
|
+
"preview": "node scripts/preview.js",
|
|
36
|
+
"package:build": "node scripts/build-npm-package.js",
|
|
37
|
+
"test": "node --test",
|
|
38
|
+
"lint": "node scripts/check-syntax.js",
|
|
39
|
+
"release:check": "node scripts/check-release-metadata.js",
|
|
40
|
+
"check": "npm run lint && npm run release:check && npm test"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"ssh2": "1.17.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import {
|
|
5
|
+
copyFile,
|
|
6
|
+
cp,
|
|
7
|
+
mkdir,
|
|
8
|
+
mkdtemp,
|
|
9
|
+
readFile,
|
|
10
|
+
rm,
|
|
11
|
+
} from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { basename, join, resolve } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { promisify } from "node:util";
|
|
16
|
+
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
19
|
+
const sourceRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
20
|
+
const defaultOutputDirectory = join(sourceRoot, "dist", "npm");
|
|
21
|
+
|
|
22
|
+
async function copyPublishEntry({ entry, stagingDirectory }) {
|
|
23
|
+
const source = join(sourceRoot, entry);
|
|
24
|
+
const target = join(stagingDirectory, entry);
|
|
25
|
+
|
|
26
|
+
if (entry === "README.md") {
|
|
27
|
+
await copyFile(join(sourceRoot, "npm", "README.md"), target);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await cp(source, target, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function stageNpmPackage(stagingDirectory) {
|
|
35
|
+
const packageJson = JSON.parse(
|
|
36
|
+
await readFile(join(sourceRoot, "package.json"), "utf8"),
|
|
37
|
+
);
|
|
38
|
+
const publishEntries = new Set([
|
|
39
|
+
"package.json",
|
|
40
|
+
...(packageJson.files ?? []),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
await mkdir(stagingDirectory, { recursive: true });
|
|
44
|
+
for (const entry of publishEntries) {
|
|
45
|
+
await copyPublishEntry({ entry, stagingDirectory });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return packageJson;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function buildNpmPackage({
|
|
52
|
+
outputDirectory = defaultOutputDirectory,
|
|
53
|
+
} = {}) {
|
|
54
|
+
const stagingDirectory = await mkdtemp(join(tmpdir(), "relmio-package-"));
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const packageJson = await stageNpmPackage(stagingDirectory);
|
|
58
|
+
await mkdir(outputDirectory, { recursive: true });
|
|
59
|
+
|
|
60
|
+
const expectedFilename = `${packageJson.name}-${packageJson.version}.tgz`;
|
|
61
|
+
const expectedPath = join(outputDirectory, expectedFilename);
|
|
62
|
+
await rm(expectedPath, { force: true });
|
|
63
|
+
|
|
64
|
+
const { stdout } = await execFileAsync(
|
|
65
|
+
npmCommand,
|
|
66
|
+
[
|
|
67
|
+
"pack",
|
|
68
|
+
stagingDirectory,
|
|
69
|
+
"--json",
|
|
70
|
+
"--ignore-scripts",
|
|
71
|
+
"--pack-destination",
|
|
72
|
+
outputDirectory,
|
|
73
|
+
],
|
|
74
|
+
{
|
|
75
|
+
cwd: sourceRoot,
|
|
76
|
+
env: {
|
|
77
|
+
...process.env,
|
|
78
|
+
npm_config_cache: join(stagingDirectory, ".npm-cache"),
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
const [packedPackage] = JSON.parse(stdout);
|
|
83
|
+
const tarballPath = resolve(outputDirectory, packedPackage.filename);
|
|
84
|
+
|
|
85
|
+
if (basename(tarballPath) !== expectedFilename) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`npm produced ${basename(tarballPath)} instead of ${expectedFilename}.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { packageJson, packedPackage, tarballPath };
|
|
92
|
+
} finally {
|
|
93
|
+
await rm(stagingDirectory, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function main() {
|
|
98
|
+
const outputArgument = process.argv[2];
|
|
99
|
+
const outputDirectory =
|
|
100
|
+
outputArgument === undefined
|
|
101
|
+
? defaultOutputDirectory
|
|
102
|
+
: resolve(process.cwd(), outputArgument);
|
|
103
|
+
const result = await buildNpmPackage({ outputDirectory });
|
|
104
|
+
console.log(result.tarballPath);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
108
|
+
main().catch((error) => {
|
|
109
|
+
console.error(error.message);
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
const RELEASE_HEADING = /^## \[([^\]]+)\] - \d{4}-\d{2}-\d{2}$/mu;
|
|
5
|
+
const SEMVER =
|
|
6
|
+
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*)|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:(?:0|[1-9]\d*)|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
|
|
7
|
+
|
|
8
|
+
export function validateReleaseMetadata({
|
|
9
|
+
packageJson,
|
|
10
|
+
packageLock,
|
|
11
|
+
changelog,
|
|
12
|
+
tag,
|
|
13
|
+
requirePackageLock = true,
|
|
14
|
+
}) {
|
|
15
|
+
const version = packageJson?.version;
|
|
16
|
+
if (typeof version !== "string" || !SEMVER.test(version)) {
|
|
17
|
+
throw new Error("package.json must contain a valid semantic version.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (packageLock || requirePackageLock) {
|
|
21
|
+
if (packageLock?.version !== version) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`package-lock.json version ${packageLock?.version ?? "missing"} does not match ${version}.`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (packageLock?.packages?.[""]?.version !== version) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`package-lock.json root package version ${packageLock?.packages?.[""]?.version ?? "missing"} does not match ${version}.`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const changelogVersion = changelog.match(RELEASE_HEADING)?.[1];
|
|
34
|
+
if (changelogVersion !== version) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`newest CHANGELOG.md version ${changelogVersion ?? "missing"} does not match ${version}.`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (tag && tag !== `v${version}`) {
|
|
41
|
+
throw new Error(`release tag ${tag} does not match v${version}.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { version };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readOptionalFile(path) {
|
|
48
|
+
try {
|
|
49
|
+
return await readFile(path, "utf8");
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.code === "ENOENT") {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function pathExists(path) {
|
|
59
|
+
try {
|
|
60
|
+
await access(path);
|
|
61
|
+
return true;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error.code === "ENOENT") {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function main() {
|
|
71
|
+
const [packageJsonContents, packageLockContents, changelog, isGitCheckout] =
|
|
72
|
+
await Promise.all([
|
|
73
|
+
readFile("package.json", "utf8"),
|
|
74
|
+
readOptionalFile("package-lock.json"),
|
|
75
|
+
readFile("CHANGELOG.md", "utf8"),
|
|
76
|
+
pathExists(".git"),
|
|
77
|
+
]);
|
|
78
|
+
const tag =
|
|
79
|
+
process.env.GITHUB_REF_TYPE === "tag"
|
|
80
|
+
? process.env.GITHUB_REF_NAME
|
|
81
|
+
: undefined;
|
|
82
|
+
const result = validateReleaseMetadata({
|
|
83
|
+
packageJson: JSON.parse(packageJsonContents),
|
|
84
|
+
packageLock:
|
|
85
|
+
packageLockContents === undefined
|
|
86
|
+
? undefined
|
|
87
|
+
: JSON.parse(packageLockContents),
|
|
88
|
+
changelog,
|
|
89
|
+
tag,
|
|
90
|
+
requirePackageLock: isGitCheckout,
|
|
91
|
+
});
|
|
92
|
+
console.log(`Release metadata is synchronized at v${result.version}.`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
96
|
+
main().catch((error) => {
|
|
97
|
+
console.error(error.message);
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { extname, join } from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
const roots = ["src", "test", "scripts"];
|
|
6
|
+
|
|
7
|
+
async function collectJavaScriptFiles(directory) {
|
|
8
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
9
|
+
const files = [];
|
|
10
|
+
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
const path = join(directory, entry.name);
|
|
13
|
+
if (entry.isDirectory()) {
|
|
14
|
+
files.push(...(await collectJavaScriptFiles(path)));
|
|
15
|
+
} else if (extname(entry.name) === ".js") {
|
|
16
|
+
files.push(path);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return files;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function checkFile(path) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const child = spawn(process.execPath, ["--check", path], {
|
|
26
|
+
stdio: "inherit",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
child.once("error", reject);
|
|
30
|
+
child.once("exit", (code) => {
|
|
31
|
+
if (code === 0) {
|
|
32
|
+
resolve();
|
|
33
|
+
} else {
|
|
34
|
+
reject(new Error(`Syntax check failed for ${path}`));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const files = (
|
|
41
|
+
await Promise.all(
|
|
42
|
+
roots.map(async (root) => {
|
|
43
|
+
try {
|
|
44
|
+
return await collectJavaScriptFiles(root);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code === "ENOENT") {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
).flat();
|
|
54
|
+
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
await checkFile(file);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`Syntax checked ${files.length} JavaScript files.`);
|