@zeroexcore/tuna 0.1.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/.turbo/turbo-test.log +14 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/package.json +60 -0
- package/src/commands/delete.ts +193 -0
- package/src/commands/init.ts +219 -0
- package/src/commands/list.ts +159 -0
- package/src/commands/login.ts +133 -0
- package/src/commands/run.ts +241 -0
- package/src/commands/stop.ts +44 -0
- package/src/index.ts +129 -0
- package/src/lib/access.ts +191 -0
- package/src/lib/api.ts +383 -0
- package/src/lib/cloudflared.ts +279 -0
- package/src/lib/config.ts +125 -0
- package/src/lib/credentials.ts +67 -0
- package/src/lib/dns.ts +164 -0
- package/src/lib/service.ts +354 -0
- package/src/types/cloudflare.ts +155 -0
- package/src/types/config.ts +33 -0
- package/src/types/index.ts +6 -0
- package/tests/unit/config.test.ts +176 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +18 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service management - launchd service for persistent cloudflared tunnels
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, writeFileSync, readFileSync, unlinkSync } from 'fs';
|
|
6
|
+
import { homedir, platform } from 'os';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { execa } from 'execa';
|
|
9
|
+
import * as yaml from 'js-yaml';
|
|
10
|
+
import {
|
|
11
|
+
getExecutablePath,
|
|
12
|
+
getTunaDir,
|
|
13
|
+
getTunnelCredentialsPath,
|
|
14
|
+
getTunnelConfigPath,
|
|
15
|
+
ensureDirectories,
|
|
16
|
+
} from './cloudflared.ts';
|
|
17
|
+
import type { IngressConfig, IngressRule, ServiceStatus, TunnelCredentials } from '../types/index.ts';
|
|
18
|
+
|
|
19
|
+
const LAUNCHD_LABEL = 'com.tuna.cloudflared';
|
|
20
|
+
const LAUNCH_AGENTS_DIR = join(homedir(), 'Library', 'LaunchAgents');
|
|
21
|
+
const PLIST_PATH = join(LAUNCH_AGENTS_DIR, `${LAUNCHD_LABEL}.plist`);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate ingress configuration for a tunnel
|
|
25
|
+
*/
|
|
26
|
+
export function generateIngressConfig(
|
|
27
|
+
tunnelId: string,
|
|
28
|
+
hostname: string,
|
|
29
|
+
port: number
|
|
30
|
+
): IngressConfig {
|
|
31
|
+
const credentialsFile = getTunnelCredentialsPath(tunnelId);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
tunnel: tunnelId,
|
|
35
|
+
credentials_file: credentialsFile,
|
|
36
|
+
ingress: [
|
|
37
|
+
{
|
|
38
|
+
hostname,
|
|
39
|
+
service: `http://localhost:${port}`,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
service: 'http_status:404',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Write ingress configuration to YAML file
|
|
50
|
+
* Note: cloudflared expects 'credentials-file' (hyphen) not 'credentials_file' (underscore)
|
|
51
|
+
*/
|
|
52
|
+
export function writeIngressConfig(tunnelId: string, config: IngressConfig): string {
|
|
53
|
+
ensureDirectories();
|
|
54
|
+
const configPath = getTunnelConfigPath(tunnelId);
|
|
55
|
+
|
|
56
|
+
// Convert to cloudflared's expected format (hyphenated keys)
|
|
57
|
+
const cloudflaredConfig = {
|
|
58
|
+
tunnel: config.tunnel,
|
|
59
|
+
'credentials-file': config.credentials_file,
|
|
60
|
+
ingress: config.ingress,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const yamlContent = yaml.dump(cloudflaredConfig, {
|
|
64
|
+
lineWidth: -1, // Don't wrap lines
|
|
65
|
+
noRefs: true,
|
|
66
|
+
});
|
|
67
|
+
writeFileSync(configPath, yamlContent, { mode: 0o600 });
|
|
68
|
+
return configPath;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read existing ingress configuration
|
|
73
|
+
*/
|
|
74
|
+
export function readIngressConfig(tunnelId: string): IngressConfig | null {
|
|
75
|
+
const configPath = getTunnelConfigPath(tunnelId);
|
|
76
|
+
if (!existsSync(configPath)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
80
|
+
return yaml.load(content) as IngressConfig;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Add or update an ingress rule in the configuration
|
|
85
|
+
*/
|
|
86
|
+
export function updateIngressRule(
|
|
87
|
+
config: IngressConfig,
|
|
88
|
+
hostname: string,
|
|
89
|
+
port: number
|
|
90
|
+
): IngressConfig {
|
|
91
|
+
const newRule: IngressRule = {
|
|
92
|
+
hostname,
|
|
93
|
+
service: `http://localhost:${port}`,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Find existing rule for this hostname
|
|
97
|
+
const existingIndex = config.ingress.findIndex((r) => r.hostname === hostname);
|
|
98
|
+
|
|
99
|
+
if (existingIndex >= 0) {
|
|
100
|
+
// Update existing rule
|
|
101
|
+
config.ingress[existingIndex] = newRule;
|
|
102
|
+
} else {
|
|
103
|
+
// Add new rule before the catch-all 404
|
|
104
|
+
const catchAllIndex = config.ingress.findIndex((r) => !r.hostname);
|
|
105
|
+
if (catchAllIndex >= 0) {
|
|
106
|
+
config.ingress.splice(catchAllIndex, 0, newRule);
|
|
107
|
+
} else {
|
|
108
|
+
config.ingress.push(newRule);
|
|
109
|
+
config.ingress.push({ service: 'http_status:404' });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return config;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Remove an ingress rule from the configuration
|
|
118
|
+
*/
|
|
119
|
+
export function removeIngressRule(config: IngressConfig, hostname: string): IngressConfig {
|
|
120
|
+
config.ingress = config.ingress.filter((r) => r.hostname !== hostname);
|
|
121
|
+
return config;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Save tunnel credentials to file
|
|
126
|
+
*/
|
|
127
|
+
export function saveTunnelCredentials(
|
|
128
|
+
tunnelId: string,
|
|
129
|
+
accountId: string,
|
|
130
|
+
tunnelSecret: string
|
|
131
|
+
): string {
|
|
132
|
+
ensureDirectories();
|
|
133
|
+
const credentialsPath = getTunnelCredentialsPath(tunnelId);
|
|
134
|
+
|
|
135
|
+
const credentials: TunnelCredentials = {
|
|
136
|
+
AccountTag: accountId,
|
|
137
|
+
TunnelSecret: tunnelSecret,
|
|
138
|
+
TunnelID: tunnelId,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2), { mode: 0o600 });
|
|
142
|
+
return credentialsPath;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if tunnel credentials exist
|
|
147
|
+
*/
|
|
148
|
+
export function tunnelCredentialsExist(tunnelId: string): boolean {
|
|
149
|
+
return existsSync(getTunnelCredentialsPath(tunnelId));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Delete tunnel credentials file
|
|
154
|
+
*/
|
|
155
|
+
export function deleteTunnelCredentials(tunnelId: string): boolean {
|
|
156
|
+
const path = getTunnelCredentialsPath(tunnelId);
|
|
157
|
+
if (existsSync(path)) {
|
|
158
|
+
unlinkSync(path);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Delete tunnel config file
|
|
166
|
+
*/
|
|
167
|
+
export function deleteTunnelConfig(tunnelId: string): boolean {
|
|
168
|
+
const path = getTunnelConfigPath(tunnelId);
|
|
169
|
+
if (existsSync(path)) {
|
|
170
|
+
unlinkSync(path);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Generate launchd plist content for the service
|
|
178
|
+
*/
|
|
179
|
+
function generatePlistContent(configPath: string): string {
|
|
180
|
+
const cloudflaredPath = getExecutablePath();
|
|
181
|
+
|
|
182
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
183
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
184
|
+
<plist version="1.0">
|
|
185
|
+
<dict>
|
|
186
|
+
<key>Label</key>
|
|
187
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
188
|
+
<key>ProgramArguments</key>
|
|
189
|
+
<array>
|
|
190
|
+
<string>${cloudflaredPath}</string>
|
|
191
|
+
<string>tunnel</string>
|
|
192
|
+
<string>--config</string>
|
|
193
|
+
<string>${configPath}</string>
|
|
194
|
+
<string>run</string>
|
|
195
|
+
</array>
|
|
196
|
+
<key>RunAtLoad</key>
|
|
197
|
+
<true/>
|
|
198
|
+
<key>KeepAlive</key>
|
|
199
|
+
<true/>
|
|
200
|
+
<key>StandardOutPath</key>
|
|
201
|
+
<string>${getTunaDir()}/cloudflared.log</string>
|
|
202
|
+
<key>StandardErrorPath</key>
|
|
203
|
+
<string>${getTunaDir()}/cloudflared.err</string>
|
|
204
|
+
</dict>
|
|
205
|
+
</plist>`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Install cloudflared as a launchd service (macOS only)
|
|
210
|
+
*/
|
|
211
|
+
export async function installService(tunnelId: string): Promise<void> {
|
|
212
|
+
if (platform() !== 'darwin') {
|
|
213
|
+
throw new Error('Service installation is only supported on macOS');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const configPath = getTunnelConfigPath(tunnelId);
|
|
217
|
+
if (!existsSync(configPath)) {
|
|
218
|
+
throw new Error(`Tunnel config not found: ${configPath}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Create LaunchAgents directory if it doesn't exist
|
|
222
|
+
const { mkdirSync } = await import('fs');
|
|
223
|
+
if (!existsSync(LAUNCH_AGENTS_DIR)) {
|
|
224
|
+
mkdirSync(LAUNCH_AGENTS_DIR, { recursive: true });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Write plist file
|
|
228
|
+
const plistContent = generatePlistContent(configPath);
|
|
229
|
+
writeFileSync(PLIST_PATH, plistContent);
|
|
230
|
+
|
|
231
|
+
// Load the service
|
|
232
|
+
await execa('launchctl', ['load', PLIST_PATH]);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Uninstall the cloudflared launchd service
|
|
237
|
+
*/
|
|
238
|
+
export async function uninstallService(): Promise<void> {
|
|
239
|
+
if (platform() !== 'darwin') {
|
|
240
|
+
throw new Error('Service management is only supported on macOS');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!existsSync(PLIST_PATH)) {
|
|
244
|
+
return; // Not installed
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
// Unload the service
|
|
249
|
+
await execa('launchctl', ['unload', PLIST_PATH]);
|
|
250
|
+
} catch {
|
|
251
|
+
// Ignore errors if service wasn't loaded
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Remove plist file
|
|
255
|
+
if (existsSync(PLIST_PATH)) {
|
|
256
|
+
unlinkSync(PLIST_PATH);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Start the cloudflared service
|
|
262
|
+
*/
|
|
263
|
+
export async function startService(): Promise<void> {
|
|
264
|
+
if (platform() !== 'darwin') {
|
|
265
|
+
throw new Error('Service management is only supported on macOS');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!existsSync(PLIST_PATH)) {
|
|
269
|
+
throw new Error('Service not installed. Run tuna first to set up the tunnel.');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await execa('launchctl', ['start', LAUNCHD_LABEL]);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Stop the cloudflared service
|
|
277
|
+
*/
|
|
278
|
+
export async function stopService(): Promise<void> {
|
|
279
|
+
if (platform() !== 'darwin') {
|
|
280
|
+
throw new Error('Service management is only supported on macOS');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!existsSync(PLIST_PATH)) {
|
|
284
|
+
return; // Not installed
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
await execa('launchctl', ['stop', LAUNCHD_LABEL]);
|
|
289
|
+
} catch {
|
|
290
|
+
// Ignore errors if service wasn't running
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Restart the cloudflared service
|
|
296
|
+
*/
|
|
297
|
+
export async function restartService(): Promise<void> {
|
|
298
|
+
await stopService();
|
|
299
|
+
await startService();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get the current service status
|
|
304
|
+
*/
|
|
305
|
+
export async function getServiceStatus(): Promise<ServiceStatus> {
|
|
306
|
+
if (platform() !== 'darwin') {
|
|
307
|
+
return { running: false };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!existsSync(PLIST_PATH)) {
|
|
311
|
+
return { running: false };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const result = await execa('launchctl', ['list', LAUNCHD_LABEL]);
|
|
316
|
+
const output = result.stdout;
|
|
317
|
+
|
|
318
|
+
// Parse PID from output
|
|
319
|
+
// Format: "PID\tStatus\tLabel"
|
|
320
|
+
const match = output.match(/^(\d+)/);
|
|
321
|
+
const pid = match ? parseInt(match[1], 10) : undefined;
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
running: pid !== undefined && pid > 0,
|
|
325
|
+
pid,
|
|
326
|
+
};
|
|
327
|
+
} catch {
|
|
328
|
+
return { running: false };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Check if the service is installed
|
|
334
|
+
*/
|
|
335
|
+
export function isServiceInstalled(): boolean {
|
|
336
|
+
return existsSync(PLIST_PATH);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Update the service configuration (reload with new config)
|
|
341
|
+
*/
|
|
342
|
+
export async function updateServiceConfig(tunnelId: string): Promise<void> {
|
|
343
|
+
const status = await getServiceStatus();
|
|
344
|
+
|
|
345
|
+
// Update plist with new config path
|
|
346
|
+
const configPath = getTunnelConfigPath(tunnelId);
|
|
347
|
+
const plistContent = generatePlistContent(configPath);
|
|
348
|
+
writeFileSync(PLIST_PATH, plistContent);
|
|
349
|
+
|
|
350
|
+
if (status.running) {
|
|
351
|
+
// Reload service to pick up new config
|
|
352
|
+
await restartService();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for Cloudflare API and tunnel structures
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface Credentials {
|
|
6
|
+
apiToken: string;
|
|
7
|
+
accountId: string;
|
|
8
|
+
domain: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Tunnel {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
created_at: string;
|
|
15
|
+
deleted_at?: string;
|
|
16
|
+
status: 'healthy' | 'down' | 'degraded' | 'inactive';
|
|
17
|
+
connections?: Connection[];
|
|
18
|
+
conns_active_at?: string;
|
|
19
|
+
conns_inactive_at?: string;
|
|
20
|
+
account_tag?: string;
|
|
21
|
+
tun_type?: string;
|
|
22
|
+
remote_config?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Connection {
|
|
26
|
+
id: string;
|
|
27
|
+
client_id: string;
|
|
28
|
+
client_version?: string;
|
|
29
|
+
colo_name: string;
|
|
30
|
+
opened_at: string;
|
|
31
|
+
origin_ip: string;
|
|
32
|
+
uuid?: string;
|
|
33
|
+
is_pending_reconnect?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DnsRecord {
|
|
37
|
+
id?: string;
|
|
38
|
+
type: 'CNAME' | 'A' | 'AAAA';
|
|
39
|
+
name: string;
|
|
40
|
+
content: string;
|
|
41
|
+
proxied: boolean;
|
|
42
|
+
ttl: number;
|
|
43
|
+
zone_id?: string;
|
|
44
|
+
zone_name?: string;
|
|
45
|
+
comment?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface Zone {
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
status: string;
|
|
52
|
+
paused?: boolean;
|
|
53
|
+
type?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface IngressRule {
|
|
57
|
+
hostname?: string;
|
|
58
|
+
service: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface IngressConfig {
|
|
62
|
+
tunnel: string;
|
|
63
|
+
credentials_file: string;
|
|
64
|
+
ingress: IngressRule[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ServiceStatus {
|
|
68
|
+
running: boolean;
|
|
69
|
+
pid?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface TunnelCredentials {
|
|
73
|
+
AccountTag: string;
|
|
74
|
+
TunnelSecret: string;
|
|
75
|
+
TunnelID: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CloudflareApiResponse<T> {
|
|
79
|
+
success: boolean;
|
|
80
|
+
errors: CloudflareApiError[];
|
|
81
|
+
messages: CloudflareApiMessage[];
|
|
82
|
+
result: T;
|
|
83
|
+
result_info?: {
|
|
84
|
+
count: number;
|
|
85
|
+
page: number;
|
|
86
|
+
per_page: number;
|
|
87
|
+
total_count: number;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface CloudflareApiError {
|
|
92
|
+
code: number;
|
|
93
|
+
message: string;
|
|
94
|
+
documentation_url?: string;
|
|
95
|
+
source?: {
|
|
96
|
+
pointer: string;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface CloudflareApiMessage {
|
|
101
|
+
code: number;
|
|
102
|
+
message: string;
|
|
103
|
+
documentation_url?: string;
|
|
104
|
+
source?: {
|
|
105
|
+
pointer: string;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Zero Trust Access types
|
|
110
|
+
|
|
111
|
+
export interface AccessApplication {
|
|
112
|
+
id: string;
|
|
113
|
+
name: string;
|
|
114
|
+
domain: string;
|
|
115
|
+
type: 'self_hosted' | 'saas' | 'ssh' | 'vnc' | 'bookmark';
|
|
116
|
+
session_duration?: string;
|
|
117
|
+
created_at?: string;
|
|
118
|
+
updated_at?: string;
|
|
119
|
+
aud?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface AccessPolicy {
|
|
123
|
+
id: string;
|
|
124
|
+
name: string;
|
|
125
|
+
decision: 'allow' | 'deny' | 'bypass' | 'non_identity';
|
|
126
|
+
include: AccessRule[];
|
|
127
|
+
exclude?: AccessRule[];
|
|
128
|
+
require?: AccessRule[];
|
|
129
|
+
precedence?: number;
|
|
130
|
+
created_at?: string;
|
|
131
|
+
updated_at?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Access rule types - union of different rule kinds
|
|
135
|
+
export type AccessRule =
|
|
136
|
+
| { email: { email: string } }
|
|
137
|
+
| { email_domain: { domain: string } }
|
|
138
|
+
| { everyone: Record<string, never> }
|
|
139
|
+
| { ip: { ip: string } }
|
|
140
|
+
| { group: { id: string } };
|
|
141
|
+
|
|
142
|
+
export interface AccessApplicationCreate {
|
|
143
|
+
name: string;
|
|
144
|
+
domain: string;
|
|
145
|
+
type: 'self_hosted';
|
|
146
|
+
session_duration?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface AccessPolicyCreate {
|
|
150
|
+
name: string;
|
|
151
|
+
decision: 'allow' | 'deny' | 'bypass' | 'non_identity';
|
|
152
|
+
include: AccessRule[];
|
|
153
|
+
exclude?: AccessRule[];
|
|
154
|
+
require?: AccessRule[];
|
|
155
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for Tuna configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Access control configuration for Zero Trust
|
|
7
|
+
* Array of emails and/or email domains:
|
|
8
|
+
* - Strings starting with @ are email domains: "@company.com"
|
|
9
|
+
* - Other strings are specific emails: "alice@gmail.com"
|
|
10
|
+
* Example: ["@company.com", "bob@contractor.io"]
|
|
11
|
+
*/
|
|
12
|
+
export type AccessConfig = string[];
|
|
13
|
+
|
|
14
|
+
export interface TunaConfig {
|
|
15
|
+
forward: string; // Domain to expose (e.g., my-app.example.com or $USER-app.example.com)
|
|
16
|
+
port: number; // Local port to forward to
|
|
17
|
+
access?: AccessConfig; // Optional Zero Trust access control
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PackageJson {
|
|
21
|
+
name?: string;
|
|
22
|
+
version?: string;
|
|
23
|
+
tuna?: TunaConfig;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parsed access config - separated into emails and domains
|
|
29
|
+
*/
|
|
30
|
+
export interface ParsedAccessConfig {
|
|
31
|
+
emails: string[]; // ["alice@gmail.com", "bob@contractor.io"]
|
|
32
|
+
emailDomains: string[]; // ["company.com"] (without @)
|
|
33
|
+
}
|