shopify-tunnel-dev 0.1.0 → 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/README.md +54 -71
- package/bin/shopify-tunnel-dev.js +1 -1
- package/package.json +1 -1
- package/src/load-config.js +10 -11
- package/src/start-tunnel.js +3 -3
package/README.md
CHANGED
|
@@ -1,47 +1,46 @@
|
|
|
1
1
|
# shopify-tunnel-dev
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Run `shopify app dev` behind a Cloudflare named tunnel with a **fixed hostname**. The team shares one
|
|
4
|
+
set of credentials; a new dev picks a hostname and runs. Nobody provisions tunnels or certificates.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
mà không phải sửa `.env` hay `account.json`. Không có dependency, cần Node >= 20.12.
|
|
6
|
+
Zero dependencies. Node >= 20.12.
|
|
8
7
|
|
|
9
|
-
##
|
|
8
|
+
## How it works
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
├─ chạy connector về http://localhost:$VITE_PORT, chờ edge nhận kết nối
|
|
17
|
-
└─ npx shopify app dev --tunnel-url https://$VITE_HOST:$VITE_PORT
|
|
18
|
-
```
|
|
10
|
+
1. Reads `DEV_HOST` / `DEV_PORT` from `.env`, shared credentials from `.cloudflared/`.
|
|
11
|
+
2. Finds the tunnel named after the first label of `DEV_HOST`, creates it on first run.
|
|
12
|
+
3. Points DNS for `DEV_HOST` at the tunnel.
|
|
13
|
+
4. Starts the connector to `http://localhost:$DEV_PORT`.
|
|
14
|
+
5. Runs `shopify app dev --tunnel-url https://$DEV_HOST:$DEV_PORT`.
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`CLOUDFLARED_BIN`.
|
|
16
|
+
TLS terminates at Cloudflare, so no local certificate is needed. `cloudflared` is used from `PATH`,
|
|
17
|
+
or downloaded to `~/.cache/shopify-tunnel-dev/` on first run (override with `CLOUDFLARED_BIN`).
|
|
23
18
|
|
|
24
|
-
##
|
|
19
|
+
## Team setup (once)
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
Use a Cloudflare account dedicated to dev (see [Security](#security)).
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cloudflared tunnel login # pick your zone, writes ~/.cloudflared/cert.pem
|
|
25
|
+
openssl rand -base64 32 # tunnel secret
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Store `cert.pem` and this `account.json` in your password manager:
|
|
30
29
|
|
|
31
30
|
```json
|
|
32
31
|
{
|
|
33
32
|
"CF_ACCOUNT_TAG": "<account id>",
|
|
34
|
-
"CF_SECRET_KEY": "<base64
|
|
35
|
-
"CF_HOST_PATTERN": "^dev-
|
|
33
|
+
"CF_SECRET_KEY": "<base64 secret>",
|
|
34
|
+
"CF_HOST_PATTERN": "^dev-myapp-[a-zA-Z0-9-]+\\.example\\.com$"
|
|
36
35
|
}
|
|
37
36
|
```
|
|
38
37
|
|
|
39
|
-
`CF_HOST_PATTERN`
|
|
38
|
+
`CF_HOST_PATTERN` is optional; use it to give each app its own prefix.
|
|
40
39
|
|
|
41
|
-
##
|
|
40
|
+
## App repo setup
|
|
42
41
|
|
|
43
42
|
```bash
|
|
44
|
-
pnpm add -D shopify-tunnel-dev
|
|
43
|
+
pnpm add -D shopify-tunnel-dev
|
|
45
44
|
```
|
|
46
45
|
|
|
47
46
|
```json
|
|
@@ -51,68 +50,52 @@ pnpm add -D shopify-tunnel-dev # hoặc git URL / pnpm link khi chưa publ
|
|
|
51
50
|
`.gitignore`:
|
|
52
51
|
|
|
53
52
|
```
|
|
54
|
-
.cloudflared
|
|
55
|
-
!.cloudflared/*.example
|
|
53
|
+
.cloudflared/
|
|
56
54
|
```
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
Shopify CLI tự cập nhật URL app theo `VITE_HOST`. Mỗi dev nên có app dev riêng trên Partner, vì URL
|
|
60
|
-
app nằm trên Shopify chứ không nằm ở tunnel.
|
|
61
|
-
|
|
62
|
-
## Dev mới
|
|
56
|
+
Give each dev their own app in the Partner Dashboard; the app URL lives on Shopify, not in the tunnel.
|
|
63
57
|
|
|
64
|
-
|
|
65
|
-
# lấy từ password manager
|
|
66
|
-
.cloudflared/cert.pem
|
|
67
|
-
.cloudflared/account.json
|
|
68
|
-
chmod 600 .cloudflared/*
|
|
69
|
-
```
|
|
58
|
+
## Per dev
|
|
70
59
|
|
|
71
|
-
`.
|
|
60
|
+
Copy `cert.pem` and `account.json` from the password manager into `.cloudflared/`
|
|
61
|
+
(`chmod 600 .cloudflared/*`), then add to `.env`:
|
|
72
62
|
|
|
73
63
|
```
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
DEV_HOST=dev-myapp-alice.example.com
|
|
65
|
+
DEV_PORT=9000
|
|
76
66
|
```
|
|
77
67
|
|
|
78
|
-
|
|
68
|
+
Run `pnpm dev`.
|
|
79
69
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
| Flag | Tác dụng |
|
|
70
|
+
| Variable | Meaning |
|
|
83
71
|
|---|---|
|
|
84
|
-
|
|
|
85
|
-
|
|
|
86
|
-
| còn lại | Chuyển tiếp cho `shopify app dev` (`--reset`, `--store=...`) |
|
|
87
|
-
|
|
88
|
-
## Chống trùng hostname
|
|
72
|
+
| `DEV_HOST` | Public hostname for your dev app. Its first label is the tunnel name (3-63 chars). |
|
|
73
|
+
| `DEV_PORT` | Local port Shopify CLI listens on; the tunnel forwards to it. |
|
|
89
74
|
|
|
90
|
-
|
|
91
|
-
cùng `VITE_HOST` sẽ bị chia request ngẫu nhiên cho nhau. Tool này dừng với lỗi khi:
|
|
75
|
+
## Options
|
|
92
76
|
|
|
93
|
-
|
|
94
|
-
|
|
77
|
+
| Flag | Effect |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `-q`, `--quick` | Random `*.trycloudflare.com` URL. Needs only `DEV_PORT`, no credentials. |
|
|
80
|
+
| `-f`, `--force` | Take over `DEV_HOST` even if it is in use. |
|
|
81
|
+
| anything else | Passed to `shopify app dev` (`--reset`, `--store=...`). |
|
|
95
82
|
|
|
96
|
-
|
|
83
|
+
## Hostname collisions
|
|
97
84
|
|
|
98
|
-
|
|
85
|
+
The tool stops with an error instead of hijacking a hostname when:
|
|
99
86
|
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
- Credential của tunnel truyền qua env (`TUNNEL_CRED_CONTENTS`), không ghi file `<id>.json` ra đĩa.
|
|
103
|
-
- Không có lệnh `start` (prod), vì prod không nên chạy qua tunnel dev.
|
|
87
|
+
- the tunnel already has a live connection from another machine, or
|
|
88
|
+
- the DNS record points at a different tunnel.
|
|
104
89
|
|
|
105
|
-
|
|
90
|
+
If your own process just died and left a stale connection, wait a few seconds or use `--force`.
|
|
106
91
|
|
|
107
|
-
|
|
108
|
-
- Ai có `CF_SECRET_KEY` thì chạy được connector cho mọi tunnel mà tool đã tạo.
|
|
92
|
+
## Security
|
|
109
93
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
tạo lại.
|
|
94
|
+
- `cert.pem` can create, delete and route **every** tunnel in the account and zone.
|
|
95
|
+
- `CF_SECRET_KEY` lets anyone run a connector for any tunnel this tool created.
|
|
113
96
|
|
|
114
|
-
|
|
97
|
+
Use a dedicated dev account and never commit either. Tunnel credentials are passed to `cloudflared`
|
|
98
|
+
via environment, never written to disk.
|
|
115
99
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
```
|
|
100
|
+
To rotate: change the secret in the password manager, delete old tunnels
|
|
101
|
+
(`cloudflared tunnel delete <name>`); the next `pnpm dev` recreates them.
|
|
@@ -9,7 +9,7 @@ const USAGE = `Usage: shopify-tunnel-dev [dev] [options] [-- shopify app dev arg
|
|
|
9
9
|
|
|
10
10
|
Options:
|
|
11
11
|
-q, --quick Random *.trycloudflare.com URL, no Cloudflare credentials needed
|
|
12
|
-
-f, --force Take over
|
|
12
|
+
-f, --force Take over DEV_HOST even if its DNS record or tunnel is in use
|
|
13
13
|
-h, --help Show this help
|
|
14
14
|
|
|
15
15
|
Any other argument (e.g. --reset, --store=foo) is passed to \`shopify app dev\`.`;
|
package/package.json
CHANGED
package/src/load-config.js
CHANGED
|
@@ -4,9 +4,8 @@ import { parseEnv } from 'node:util';
|
|
|
4
4
|
|
|
5
5
|
export class ConfigError extends Error {}
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// keys from the env file.
|
|
7
|
+
// Per-dev values come from web/.env (if present) or ./.env; team-shared values come from
|
|
8
|
+
// .cloudflared/account.json, which overrides keys from the env file.
|
|
10
9
|
export function loadConfig({ cwd = process.cwd(), quick = false } = {}) {
|
|
11
10
|
const envPath = [path.join(cwd, 'web/.env'), path.join(cwd, '.env')].find((p) => existsSync(p));
|
|
12
11
|
const cloudflaredDir = path.join(cwd, '.cloudflared');
|
|
@@ -17,28 +16,28 @@ export function loadConfig({ cwd = process.cwd(), quick = false } = {}) {
|
|
|
17
16
|
const account = existsSync(accountPath) ? readJson(accountPath) : {};
|
|
18
17
|
const vars = { ...env, ...account };
|
|
19
18
|
|
|
20
|
-
const port = Number(vars.
|
|
19
|
+
const port = Number(vars.DEV_PORT);
|
|
21
20
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
22
|
-
throw new ConfigError(`
|
|
21
|
+
throw new ConfigError(`DEV_PORT must be a port number, got "${vars.DEV_PORT ?? ''}"`);
|
|
23
22
|
}
|
|
24
23
|
if (quick) return { quick, port };
|
|
25
24
|
|
|
26
|
-
const missing = ['
|
|
25
|
+
const missing = ['DEV_HOST', 'CF_ACCOUNT_TAG', 'CF_SECRET_KEY'].filter((key) => !vars[key]);
|
|
27
26
|
if (missing.length) {
|
|
28
|
-
throw new ConfigError(`Missing ${missing.join(', ')}. Put
|
|
27
|
+
throw new ConfigError(`Missing ${missing.join(', ')}. Put DEV_HOST in .env and CF_* in ${accountPath}`);
|
|
29
28
|
}
|
|
30
29
|
if (!existsSync(certPath)) {
|
|
31
30
|
throw new ConfigError(`Missing ${certPath} (origin cert from \`cloudflared tunnel login\`)`);
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
const host = vars.
|
|
33
|
+
const host = vars.DEV_HOST.trim();
|
|
35
34
|
if (vars.CF_HOST_PATTERN && !hostPattern(vars.CF_HOST_PATTERN).test(host)) {
|
|
36
|
-
throw new ConfigError(`
|
|
35
|
+
throw new ConfigError(`DEV_HOST "${host}" does not match CF_HOST_PATTERN ${vars.CF_HOST_PATTERN}`);
|
|
37
36
|
}
|
|
38
|
-
// Tunnel name = first DNS label, same
|
|
37
|
+
// Tunnel name = first DNS label, so the same hostname always maps to the same tunnel on any machine.
|
|
39
38
|
const tunnelName = host.split('.')[0];
|
|
40
39
|
if (!host.includes('.') || !/^[a-zA-Z0-9-]{3,63}$/.test(tunnelName)) {
|
|
41
|
-
throw new ConfigError(`
|
|
40
|
+
throw new ConfigError(`DEV_HOST "${host}" must be a full hostname whose first label is 3-63 chars of [a-zA-Z0-9-]`);
|
|
42
41
|
}
|
|
43
42
|
// cloudflared rejects tunnel secrets that decode to fewer than 32 bytes.
|
|
44
43
|
if (Buffer.from(vars.CF_SECRET_KEY, 'base64').length < 32) {
|
package/src/start-tunnel.js
CHANGED
|
@@ -37,8 +37,8 @@ export async function startNamedTunnel(bin, cfg, { force = false } = {}) {
|
|
|
37
37
|
// Two connectors on one tunnel = Cloudflare load-balances requests between two laptops.
|
|
38
38
|
throw new Error(
|
|
39
39
|
`Tunnel "${cfg.tunnelName}" already has ${tunnel.connections.length} live connection(s): ` +
|
|
40
|
-
`someone else is probably running with
|
|
41
|
-
'Pick another
|
|
40
|
+
`someone else is probably running with DEV_HOST=${cfg.host}. ` +
|
|
41
|
+
'Pick another DEV_HOST, or pass --force if it is your own process that just died.',
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -84,7 +84,7 @@ async function routeDns(cf, tunnelId, host, force) {
|
|
|
84
84
|
const detail = (err.stderr || err.message).trim();
|
|
85
85
|
throw new Error(
|
|
86
86
|
`Cannot point ${host} at tunnel ${tunnelId}: ${detail}\n` +
|
|
87
|
-
'If the record belongs to another tunnel, pick another
|
|
87
|
+
'If the record belongs to another tunnel, pick another DEV_HOST or pass --force to take it over.',
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
90
|
}
|