@zeroexcore/tuna 0.1.0 → 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/README.md +161 -0
- package/package.json +1 -1
- package/.turbo/turbo-test.log +0 -14
- package/.turbo/turbo-typecheck.log +0 -4
- package/tests/unit/config.test.ts +0 -176
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -18
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# @zeroexcore/tuna
|
|
2
|
+
|
|
3
|
+
**Cloudflare Tunnels for humans.** Wrap any dev command with a secure, persistent tunnel.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@zeroexcore/tuna)
|
|
6
|
+
[](https://github.com/zeroexcore/tuna/actions/workflows/ci.yml)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
## Links
|
|
10
|
+
|
|
11
|
+
- **Website:** [tuna.oxc.sh](https://tuna.oxc.sh)
|
|
12
|
+
- **Documentation:** [docs.tuna.oxc.sh](https://docs.tuna.oxc.sh)
|
|
13
|
+
- **GitHub:** [github.com/zeroexcore/tuna](https://github.com/zeroexcore/tuna)
|
|
14
|
+
|
|
15
|
+
## What is tuna?
|
|
16
|
+
|
|
17
|
+
tuna wraps your development server commands with automatic Cloudflare Tunnel setup. No more manual tunnel configuration, DNS management, or random ngrok URLs.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Before: Manual tunnel setup
|
|
21
|
+
cloudflared tunnel create my-tunnel
|
|
22
|
+
cloudflared tunnel route dns my-tunnel my-app.example.com
|
|
23
|
+
cloudflared tunnel run my-tunnel &
|
|
24
|
+
vite dev
|
|
25
|
+
|
|
26
|
+
# After: Just prefix with tuna
|
|
27
|
+
tuna vite dev
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Your local server is instantly available at `https://my-app.example.com`.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **Free custom domains** - Use your own domain, no random URLs
|
|
35
|
+
- **Persistent tunnels** - Runs as a service, survives terminal restarts
|
|
36
|
+
- **Team collaboration** - `$USER` variable gives each dev their own subdomain
|
|
37
|
+
- **Zero Trust Access** - Restrict access by email/domain in config
|
|
38
|
+
- **Transparent wrapper** - Colors, TTY, exit codes all preserved
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install -g @zeroexcore/tuna
|
|
44
|
+
# or
|
|
45
|
+
pnpm add -g @zeroexcore/tuna
|
|
46
|
+
# or
|
|
47
|
+
npx @zeroexcore/tuna <command>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
### 1. Login to Cloudflare
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
tuna --login
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
You'll need a Cloudflare API token with these permissions:
|
|
59
|
+
- Account → Cloudflare Tunnel → Edit
|
|
60
|
+
- Account → Access: Apps and Policies → Edit
|
|
61
|
+
- Zone → DNS → Edit
|
|
62
|
+
- Account → Account Settings → Read
|
|
63
|
+
|
|
64
|
+
### 2. Configure your project
|
|
65
|
+
|
|
66
|
+
Add to `package.json`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"tuna": {
|
|
71
|
+
"forward": "my-app.example.com",
|
|
72
|
+
"port": 3000
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or use the interactive setup:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
tuna --init
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 3. Run your dev server
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
tuna vite dev
|
|
87
|
+
# or
|
|
88
|
+
tuna npm run dev
|
|
89
|
+
# or
|
|
90
|
+
tuna next dev
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
That's it! Your local server is now available at your custom domain.
|
|
94
|
+
|
|
95
|
+
## Team Collaboration
|
|
96
|
+
|
|
97
|
+
Use `$USER` to give each developer their own subdomain:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"tuna": {
|
|
102
|
+
"forward": "$USER-api.example.com",
|
|
103
|
+
"port": 3000
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- Alice runs `tuna vite dev` → `https://alice-api.example.com`
|
|
109
|
+
- Bob runs `tuna vite dev` → `https://bob-api.example.com`
|
|
110
|
+
|
|
111
|
+
## Access Control
|
|
112
|
+
|
|
113
|
+
Restrict who can access your tunnel:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"tuna": {
|
|
118
|
+
"forward": "staging.example.com",
|
|
119
|
+
"port": 3000,
|
|
120
|
+
"access": ["@mycompany.com", "contractor@gmail.com"]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Commands
|
|
126
|
+
|
|
127
|
+
| Command | Description |
|
|
128
|
+
|---------|-------------|
|
|
129
|
+
| `tuna <command>` | Wrap command with tunnel |
|
|
130
|
+
| `tuna --init` | Interactive project setup |
|
|
131
|
+
| `tuna --login` | Setup Cloudflare credentials |
|
|
132
|
+
| `tuna --list` | List all tunnels |
|
|
133
|
+
| `tuna --stop` | Stop cloudflared service |
|
|
134
|
+
| `tuna --delete [name]` | Delete tunnel |
|
|
135
|
+
| `tuna --help` | Show help |
|
|
136
|
+
| `tuna --version` | Show version |
|
|
137
|
+
|
|
138
|
+
## Configuration
|
|
139
|
+
|
|
140
|
+
| Field | Type | Required | Description |
|
|
141
|
+
|-------|------|:--------:|-------------|
|
|
142
|
+
| `forward` | `string` | Yes | Domain to expose (supports `$USER`, `$TUNA_USER`) |
|
|
143
|
+
| `port` | `number` | Yes | Local port to forward |
|
|
144
|
+
| `access` | `string[]` | No | Email addresses/domains for access control |
|
|
145
|
+
|
|
146
|
+
## Requirements
|
|
147
|
+
|
|
148
|
+
- Node.js 18+
|
|
149
|
+
- macOS (Linux/Windows support planned)
|
|
150
|
+
- A Cloudflare account with a domain
|
|
151
|
+
|
|
152
|
+
## Security
|
|
153
|
+
|
|
154
|
+
- **Credentials** stored in macOS Keychain with biometric auth
|
|
155
|
+
- **No secrets** in package.json or environment variables
|
|
156
|
+
- **Zero Trust Access** for fine-grained access control
|
|
157
|
+
- **TLS everywhere** via Cloudflare
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT - see [LICENSE](https://github.com/zeroexcore/tuna/blob/main/LICENSE) for details.
|
package/package.json
CHANGED
package/.turbo/turbo-test.log
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @zeroexcore/tuna@0.1.0 test /home/runner/work/tuna/tuna/packages/cli
|
|
3
|
-
> vitest run
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[1m[46m RUN [49m[22m [36mv4.0.18 [39m[90m/home/runner/work/tuna/tuna/packages/cli[39m
|
|
7
|
-
|
|
8
|
-
[32m✓[39m tests/unit/config.test.ts [2m([22m[2m23 tests[22m[2m)[22m[32m 9[2mms[22m[39m
|
|
9
|
-
|
|
10
|
-
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
11
|
-
[2m Tests [22m [1m[32m23 passed[39m[22m[90m (23)[39m
|
|
12
|
-
[2m Start at [22m 07:11:47
|
|
13
|
-
[2m Duration [22m 198ms[2m (transform 47ms, setup 0ms, import 66ms, tests 9ms, environment 0ms)[22m
|
|
14
|
-
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for config module
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
-
import { interpolateEnvVars, validateConfig, generateTunnelName } from '../../src/lib/config.ts';
|
|
7
|
-
import type { TunaConfig } from '../../src/types/index.ts';
|
|
8
|
-
|
|
9
|
-
describe('interpolateEnvVars', () => {
|
|
10
|
-
let originalEnv: NodeJS.ProcessEnv;
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
originalEnv = { ...process.env };
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
process.env = originalEnv;
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('should interpolate $USER', () => {
|
|
21
|
-
process.env.USER = 'alice';
|
|
22
|
-
const result = interpolateEnvVars('$USER-api.example.com');
|
|
23
|
-
expect(result).toBe('alice-api.example.com');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should interpolate $TUNA_USER with priority over $USER', () => {
|
|
27
|
-
process.env.USER = 'alice';
|
|
28
|
-
process.env.TUNA_USER = 'alice-dev';
|
|
29
|
-
const result = interpolateEnvVars('$USER-api.example.com');
|
|
30
|
-
expect(result).toBe('alice-dev-api.example.com');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should interpolate $TUNA_USER directly', () => {
|
|
34
|
-
process.env.TUNA_USER = 'custom-user';
|
|
35
|
-
const result = interpolateEnvVars('$TUNA_USER-api.example.com');
|
|
36
|
-
expect(result).toBe('custom-user-api.example.com');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should interpolate $HOME', () => {
|
|
40
|
-
process.env.HOME = '/Users/alice';
|
|
41
|
-
const result = interpolateEnvVars('$HOME/.config');
|
|
42
|
-
expect(result).toBe('/Users/alice/.config');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should fallback to "unknown" when $USER is not set', () => {
|
|
46
|
-
delete process.env.USER;
|
|
47
|
-
delete process.env.TUNA_USER;
|
|
48
|
-
const result = interpolateEnvVars('$USER-api.example.com');
|
|
49
|
-
expect(result).toBe('unknown-api.example.com');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should handle multiple placeholders', () => {
|
|
53
|
-
process.env.USER = 'alice';
|
|
54
|
-
process.env.HOME = '/Users/alice';
|
|
55
|
-
const result = interpolateEnvVars('$USER-$HOME');
|
|
56
|
-
expect(result).toBe('alice-/Users/alice');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should interpolate in middle of string', () => {
|
|
60
|
-
process.env.USER = 'alice';
|
|
61
|
-
const result = interpolateEnvVars('app-$USER.staging.example.com');
|
|
62
|
-
expect(result).toBe('app-alice.staging.example.com');
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe('validateConfig', () => {
|
|
67
|
-
it('should pass valid config', () => {
|
|
68
|
-
const config: TunaConfig = {
|
|
69
|
-
forward: 'my-app.example.com',
|
|
70
|
-
port: 3000,
|
|
71
|
-
};
|
|
72
|
-
expect(() => validateConfig(config)).not.toThrow();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should throw on missing forward', () => {
|
|
76
|
-
const config = {
|
|
77
|
-
port: 3000,
|
|
78
|
-
} as TunaConfig;
|
|
79
|
-
expect(() => validateConfig(config)).toThrow('Missing "forward"');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('should throw on missing port', () => {
|
|
83
|
-
const config = {
|
|
84
|
-
forward: 'my-app.example.com',
|
|
85
|
-
} as TunaConfig;
|
|
86
|
-
expect(() => validateConfig(config)).toThrow('Missing "port"');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should throw on non-number port', () => {
|
|
90
|
-
const config = {
|
|
91
|
-
forward: 'my-app.example.com',
|
|
92
|
-
port: '3000' as any,
|
|
93
|
-
};
|
|
94
|
-
expect(() => validateConfig(config)).toThrow('"port" must be a number');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should throw on port out of range (too low)', () => {
|
|
98
|
-
const config: TunaConfig = {
|
|
99
|
-
forward: 'my-app.example.com',
|
|
100
|
-
port: 0,
|
|
101
|
-
};
|
|
102
|
-
expect(() => validateConfig(config)).toThrow('"port" must be between 1 and 65535');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('should throw on port out of range (too high)', () => {
|
|
106
|
-
const config: TunaConfig = {
|
|
107
|
-
forward: 'my-app.example.com',
|
|
108
|
-
port: 65536,
|
|
109
|
-
};
|
|
110
|
-
expect(() => validateConfig(config)).toThrow('"port" must be between 1 and 65535');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('should throw on invalid domain format', () => {
|
|
114
|
-
const config: TunaConfig = {
|
|
115
|
-
forward: 'invalid..domain.com',
|
|
116
|
-
port: 3000,
|
|
117
|
-
};
|
|
118
|
-
expect(() => validateConfig(config)).toThrow('Invalid domain format');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('should throw on unresolved environment variables', () => {
|
|
122
|
-
const config: TunaConfig = {
|
|
123
|
-
forward: '$USER-api.example.com',
|
|
124
|
-
port: 3000,
|
|
125
|
-
};
|
|
126
|
-
expect(() => validateConfig(config)).toThrow('Unresolved environment variables');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('should pass with subdomain', () => {
|
|
130
|
-
const config: TunaConfig = {
|
|
131
|
-
forward: 'api.staging.example.com',
|
|
132
|
-
port: 3000,
|
|
133
|
-
};
|
|
134
|
-
expect(() => validateConfig(config)).not.toThrow();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('should pass with hyphenated domain', () => {
|
|
138
|
-
const config: TunaConfig = {
|
|
139
|
-
forward: 'my-app-api.example.com',
|
|
140
|
-
port: 3000,
|
|
141
|
-
};
|
|
142
|
-
expect(() => validateConfig(config)).not.toThrow();
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe('generateTunnelName', () => {
|
|
147
|
-
it('should generate name with tuna prefix', () => {
|
|
148
|
-
const result = generateTunnelName('my-app.example.com');
|
|
149
|
-
expect(result).toBe('tuna-my-app-example-com');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('should sanitize domain with dots', () => {
|
|
153
|
-
const result = generateTunnelName('api.staging.example.com');
|
|
154
|
-
expect(result).toBe('tuna-api-staging-example-com');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should lowercase everything', () => {
|
|
158
|
-
const result = generateTunnelName('MyApp.Example.Com');
|
|
159
|
-
expect(result).toBe('tuna-myapp-example-com');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('should replace special characters', () => {
|
|
163
|
-
const result = generateTunnelName('alice@api.example.com');
|
|
164
|
-
expect(result).toBe('tuna-alice-api-example-com');
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('should handle consecutive special chars', () => {
|
|
168
|
-
const result = generateTunnelName('my..app..example.com');
|
|
169
|
-
expect(result).toBe('tuna-my--app--example-com');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('should work with user-interpolated domain', () => {
|
|
173
|
-
const result = generateTunnelName('alice-api.example.com');
|
|
174
|
-
expect(result).toBe('tuna-alice-api-example-com');
|
|
175
|
-
});
|
|
176
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": ["ES2022"],
|
|
6
|
-
"moduleResolution": "bundler",
|
|
7
|
-
"strict": true,
|
|
8
|
-
"esModuleInterop": true,
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
"forceConsistentCasingInFileNames": true,
|
|
11
|
-
"resolveJsonModule": true,
|
|
12
|
-
"allowImportingTsExtensions": true,
|
|
13
|
-
"noEmit": true,
|
|
14
|
-
"types": ["node", "vitest/globals"]
|
|
15
|
-
},
|
|
16
|
-
"include": ["src/**/*", "tests/**/*"],
|
|
17
|
-
"exclude": ["node_modules"]
|
|
18
|
-
}
|
package/vitest.config.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vitest/config';
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
globals: true,
|
|
6
|
-
environment: 'node',
|
|
7
|
-
coverage: {
|
|
8
|
-
provider: 'v8',
|
|
9
|
-
reporter: ['text', 'html'],
|
|
10
|
-
exclude: [
|
|
11
|
-
'node_modules/',
|
|
12
|
-
'tests/',
|
|
13
|
-
'**/*.test.ts',
|
|
14
|
-
'**/*.spec.ts',
|
|
15
|
-
],
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
});
|