@iamken/cloudtunnel 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/README.md +57 -0
- package/dist/chunk-2TCFCMJS.js +27 -0
- package/dist/chunk-2TCFCMJS.js.map +1 -0
- package/dist/chunk-UPBVRXLF.js +146 -0
- package/dist/chunk-UPBVRXLF.js.map +1 -0
- package/dist/chunk-YLTQB4F7.js +57 -0
- package/dist/chunk-YLTQB4F7.js.map +1 -0
- package/dist/dns-PAPFSYFP.js +21 -0
- package/dist/dns-PAPFSYFP.js.map +1 -0
- package/dist/index.js +1245 -0
- package/dist/index.js.map +1 -0
- package/dist/zones-YNGQYXAF.js +11 -0
- package/dist/zones-YNGQYXAF.js.map +1 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# cloudtunnel
|
|
2
|
+
|
|
3
|
+
Expose a local port at a public HTTPS subdomain on **your own Cloudflare domains** — nport-style, but the tunnel and DNS live in your Cloudflare account. Manage every tunnel subdomain across the whole account from one CLI.
|
|
4
|
+
|
|
5
|
+
## Quickstart
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -g @iamken/cloudtunnel
|
|
9
|
+
|
|
10
|
+
cloudtunnel login # once — paste a token (or set CLOUDFLARE_API_TOKEN); account + zone auto-resolved
|
|
11
|
+
cloudtunnel 3000 # → https://brave-otter-1a2b.example.com is live
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`cloudtunnel 3000` is shorthand for `cloudtunnel up 3000`. Press **Ctrl-C** to stop the connector — the subdomain is **kept** (delete it with `cloudtunnel rm`). Want the nport-style throwaway behaviour? add `--ephemeral`.
|
|
15
|
+
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
`cloudtunnel` uses **native Cloudflare Tunnel**: it creates a remotely-managed tunnel via the Cloudflare API, points a proxied CNAME at it (`<id>.cfargotunnel.com`), and runs the `cloudflared` connector for you. Traffic flows `browser → Cloudflare edge → cloudflared → your local port`.
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
| Command | Does |
|
|
23
|
+
|---------|------|
|
|
24
|
+
| `cloudtunnel login` | Authenticate; auto-resolve account + default zone. `--status` to inspect. |
|
|
25
|
+
| `cloudtunnel <port>` / `up <port>` | Create + serve a subdomain. Flags: `-s/--subdomain`, `-d/--domain`, `--hostname`, `--detach`, `--ephemeral`, `--proto`. Pick a domain interactively when you have several and none is passed. |
|
|
26
|
+
| `cloudtunnel ls [--all]` | List tunnel subdomains. `--all` also scans the account for unmanaged ones. |
|
|
27
|
+
| `cloudtunnel update <name> --port <p>` | Repoint a subdomain to a new port — **zero downtime** (hot-reload, no restart). |
|
|
28
|
+
| `cloudtunnel rm <name> [--dry-run] [--force]` | Delete a subdomain (stops connector, removes tunnel + DNS). |
|
|
29
|
+
| `cloudtunnel status <name>` | Tunnel health + connector state. |
|
|
30
|
+
| `cloudtunnel down [name] [--all]` | Stop connector(s); keep the tunnel + DNS. |
|
|
31
|
+
| `cloudtunnel gc [--yes]` | Prune crash orphans. |
|
|
32
|
+
| `cloudtunnel zones` | List the domains in your account. |
|
|
33
|
+
|
|
34
|
+
## Authentication
|
|
35
|
+
|
|
36
|
+
Create a **Cloudflare API token** with these scopes (least-privilege; a zone-scoped token limited to the domains you tunnel into is recommended):
|
|
37
|
+
|
|
38
|
+
- Account · **Cloudflare Tunnel** · Edit
|
|
39
|
+
- Account · **Account Settings** · Read
|
|
40
|
+
- Zone · **DNS** · Edit
|
|
41
|
+
- Zone · **Zone** · Read
|
|
42
|
+
|
|
43
|
+
Provide it via (in order of precedence): `CLOUDFLARE_API_TOKEN` env var → `cloudtunnel login --token-stdin` → the hidden prompt from `cloudtunnel login`. The token is **never** passed on the command line (it would leak into `ps` / shell history) and is stored `0600`. Rotate it if a shared host is compromised.
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
- Node.js ≥ 20
|
|
48
|
+
- `cloudflared` on your `PATH` — install via `brew install cloudflared` / your package manager, or from <https://github.com/cloudflare/cloudflared/releases>. (Auto-download is built in and fetches a pinned, SHA256-verified binary, but stays disabled until release checksums are pinned — so treat `cloudflared` as a prerequisite for now.)
|
|
49
|
+
|
|
50
|
+
## Troubleshooting
|
|
51
|
+
|
|
52
|
+
- **HTTP 1016 / subdomain not loading** — the connector isn't running. Start it with `cloudtunnel <port> --name <sub>` (re-attaches) or check `cloudtunnel status <sub>`.
|
|
53
|
+
- **"grey-clouded" error** — the zone couldn't proxy the record; cfargotunnel routing needs an orange-cloud (proxied) CNAME.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CliError,
|
|
4
|
+
cfPaginate,
|
|
5
|
+
cfRequest
|
|
6
|
+
} from "./chunk-UPBVRXLF.js";
|
|
7
|
+
|
|
8
|
+
// src/cloudflare/zones.ts
|
|
9
|
+
function listZones(token) {
|
|
10
|
+
return cfPaginate(token, "/zones");
|
|
11
|
+
}
|
|
12
|
+
async function resolveZone(token, domain) {
|
|
13
|
+
const env = await cfRequest(token, "GET", `/zones?name=${encodeURIComponent(domain)}`);
|
|
14
|
+
const zone = env.result?.[0];
|
|
15
|
+
if (!zone) {
|
|
16
|
+
throw new CliError(`Zone not found: ${domain}`, {
|
|
17
|
+
hint: "run `cloudtunnel zones` to see the domains in this account"
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return zone;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
listZones,
|
|
25
|
+
resolveZone
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=chunk-2TCFCMJS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cloudflare/zones.ts"],"sourcesContent":["import { cfPaginate, cfRequest } from \"./client.js\";\nimport type { Zone } from \"./types.js\";\nimport { CliError } from \"../ui/errors.js\";\n\n/** All zones (domains) in the account, across pages. */\nexport function listZones(token: string): Promise<Zone[]> {\n return cfPaginate<Zone>(token, \"/zones\");\n}\n\n/** Resolve a domain name to its zone; throws actionably if not in the account. */\nexport async function resolveZone(token: string, domain: string): Promise<Zone> {\n const env = await cfRequest<Zone[]>(token, \"GET\", `/zones?name=${encodeURIComponent(domain)}`);\n const zone = env.result?.[0];\n if (!zone) {\n throw new CliError(`Zone not found: ${domain}`, {\n hint: \"run `cloudtunnel zones` to see the domains in this account\",\n });\n }\n return zone;\n}\n"],"mappings":";;;;;;;;AAKO,SAAS,UAAU,OAAgC;AACxD,SAAO,WAAiB,OAAO,QAAQ;AACzC;AAGA,eAAsB,YAAY,OAAe,QAA+B;AAC9E,QAAM,MAAM,MAAM,UAAkB,OAAO,OAAO,eAAe,mBAAmB,MAAM,CAAC,EAAE;AAC7F,QAAM,OAAO,IAAI,SAAS,CAAC;AAC3B,MAAI,CAAC,MAAM;AACT,UAAM,IAAI,SAAS,mBAAmB,MAAM,IAAI;AAAA,MAC9C,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/ui/errors.ts
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
var CliError = class extends Error {
|
|
6
|
+
exitCode;
|
|
7
|
+
hint;
|
|
8
|
+
/** HTTP status when this wraps a Cloudflare API error (lets callers tell a
|
|
9
|
+
* genuine 404 "already gone" from a transient failure). */
|
|
10
|
+
status;
|
|
11
|
+
constructor(message, opts = {}) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "CliError";
|
|
14
|
+
this.exitCode = opts.exitCode ?? 1;
|
|
15
|
+
this.hint = opts.hint;
|
|
16
|
+
this.status = opts.status;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
function reportError(err) {
|
|
20
|
+
if (err instanceof CliError) {
|
|
21
|
+
console.error(pc.red(`\u2717 ${err.message}`));
|
|
22
|
+
if (err.hint) console.error(pc.dim(` \u2192 ${err.hint}`));
|
|
23
|
+
return err.exitCode;
|
|
24
|
+
}
|
|
25
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26
|
+
console.error(pc.red(`\u2717 ${message}`));
|
|
27
|
+
console.error(pc.dim(" (unexpected error \u2014 please report if this persists)"));
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/config/store.ts
|
|
32
|
+
import { chmodSync, readFileSync, writeFileSync } from "fs";
|
|
33
|
+
|
|
34
|
+
// src/config/paths.ts
|
|
35
|
+
import envPaths from "env-paths";
|
|
36
|
+
import { mkdirSync } from "fs";
|
|
37
|
+
import { join } from "path";
|
|
38
|
+
var paths = envPaths("cloudtunnel", { suffix: "" });
|
|
39
|
+
var configDir = paths.config;
|
|
40
|
+
var configFile = join(configDir, "config.json");
|
|
41
|
+
var registryFile = join(configDir, "tunnels.json");
|
|
42
|
+
var profilesFile = join(configDir, "profiles.json");
|
|
43
|
+
var binDir = join(configDir, "bin");
|
|
44
|
+
var logDir = join(configDir, "logs");
|
|
45
|
+
function ensureDirs() {
|
|
46
|
+
for (const dir of [configDir, binDir, logDir]) {
|
|
47
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/config/store.ts
|
|
52
|
+
function loadConfig() {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(configFile, "utf8"));
|
|
55
|
+
} catch {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function saveConfig(config) {
|
|
60
|
+
ensureDirs();
|
|
61
|
+
writeFileSync(configFile, JSON.stringify(config, null, 2), { mode: 384 });
|
|
62
|
+
chmodSync(configFile, 384);
|
|
63
|
+
}
|
|
64
|
+
function getCredentials() {
|
|
65
|
+
const config = loadConfig();
|
|
66
|
+
const apiToken = process.env.CLOUDFLARE_API_TOKEN ?? config.apiToken;
|
|
67
|
+
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? config.accountId;
|
|
68
|
+
if (!apiToken) {
|
|
69
|
+
throw new CliError("Not authenticated with Cloudflare.", {
|
|
70
|
+
hint: "run `cloudtunnel login` (or set CLOUDFLARE_API_TOKEN)"
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return { apiToken, accountId, defaultZone: config.defaultZone };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/cloudflare/client.ts
|
|
77
|
+
var API_BASE = "https://api.cloudflare.com/client/v4";
|
|
78
|
+
var RETRYABLE = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
79
|
+
var MAX_ATTEMPTS = 4;
|
|
80
|
+
function resolveCf() {
|
|
81
|
+
const creds = getCredentials();
|
|
82
|
+
if (!creds.accountId) {
|
|
83
|
+
throw new CliError("No Cloudflare account id resolved.", {
|
|
84
|
+
hint: "run `cloudtunnel login` (or set CLOUDFLARE_ACCOUNT_ID)"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return { token: creds.apiToken, accountId: creds.accountId };
|
|
88
|
+
}
|
|
89
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
90
|
+
async function cfRequest(token, method, path, body) {
|
|
91
|
+
for (let attempt = 0; ; attempt++) {
|
|
92
|
+
let res;
|
|
93
|
+
try {
|
|
94
|
+
res = await fetch(`${API_BASE}${path}`, {
|
|
95
|
+
method,
|
|
96
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
97
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
98
|
+
});
|
|
99
|
+
} catch {
|
|
100
|
+
throw new CliError("Could not reach the Cloudflare API (network error).");
|
|
101
|
+
}
|
|
102
|
+
if (RETRYABLE.has(res.status) && attempt < MAX_ATTEMPTS) {
|
|
103
|
+
const retryAfter = Number(res.headers.get("retry-after")) || 0;
|
|
104
|
+
await sleep(retryAfter > 0 ? retryAfter * 1e3 : 2 ** attempt * 500);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const env = await res.json().catch(() => ({}));
|
|
108
|
+
if (!res.ok || !env.success) {
|
|
109
|
+
const msg = env.errors?.[0]?.message ?? `HTTP ${res.status}`;
|
|
110
|
+
throw new CliError(`Cloudflare API error: ${msg}`, { status: res.status });
|
|
111
|
+
}
|
|
112
|
+
return env;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function cfPaginate(token, basePath) {
|
|
116
|
+
const sep = basePath.includes("?") ? "&" : "?";
|
|
117
|
+
const out = [];
|
|
118
|
+
for (let page = 1; ; page++) {
|
|
119
|
+
const env = await cfRequest(token, "GET", `${basePath}${sep}per_page=50&page=${page}`);
|
|
120
|
+
const items = env.result ?? [];
|
|
121
|
+
out.push(...items);
|
|
122
|
+
const perPage = env.result_info?.per_page ?? 50;
|
|
123
|
+
const totalPages = env.result_info?.total_pages;
|
|
124
|
+
const more = totalPages ? page < totalPages : items.length === perPage;
|
|
125
|
+
if (items.length === 0 || !more) break;
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
CliError,
|
|
132
|
+
reportError,
|
|
133
|
+
configFile,
|
|
134
|
+
registryFile,
|
|
135
|
+
profilesFile,
|
|
136
|
+
binDir,
|
|
137
|
+
logDir,
|
|
138
|
+
ensureDirs,
|
|
139
|
+
loadConfig,
|
|
140
|
+
saveConfig,
|
|
141
|
+
getCredentials,
|
|
142
|
+
resolveCf,
|
|
143
|
+
cfRequest,
|
|
144
|
+
cfPaginate
|
|
145
|
+
};
|
|
146
|
+
//# sourceMappingURL=chunk-UPBVRXLF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ui/errors.ts","../src/config/store.ts","../src/config/paths.ts","../src/cloudflare/client.ts"],"sourcesContent":["import pc from \"picocolors\";\n\n/**\n * A user-facing CLI error. The message is printed as-is (no stack trace) and\n * `exitCode` drives the process exit code. Use `hint` to tell the user exactly\n * which command to run next — every error should be actionable.\n */\nexport class CliError extends Error {\n readonly exitCode: number;\n readonly hint?: string;\n /** HTTP status when this wraps a Cloudflare API error (lets callers tell a\n * genuine 404 \"already gone\" from a transient failure). */\n readonly status?: number;\n\n constructor(message: string, opts: { exitCode?: number; hint?: string; status?: number } = {}) {\n super(message);\n this.name = \"CliError\";\n this.exitCode = opts.exitCode ?? 1;\n this.hint = opts.hint;\n this.status = opts.status;\n }\n}\n\n/** Print an error and return the exit code. Known CliErrors print cleanly; the\n * rest print their message plus a note that it was unexpected. */\nexport function reportError(err: unknown): number {\n if (err instanceof CliError) {\n console.error(pc.red(`✗ ${err.message}`));\n if (err.hint) console.error(pc.dim(` → ${err.hint}`));\n return err.exitCode;\n }\n const message = err instanceof Error ? err.message : String(err);\n console.error(pc.red(`✗ ${message}`));\n console.error(pc.dim(\" (unexpected error — please report if this persists)\"));\n return 1;\n}\n","import { chmodSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { CliError } from \"../ui/errors.js\";\nimport { configFile, ensureDirs } from \"./paths.js\";\n\nexport interface CloudtunnelConfig {\n apiToken?: string;\n accountId?: string;\n defaultZone?: string;\n}\n\nexport interface Credentials {\n apiToken: string;\n accountId?: string;\n defaultZone?: string;\n}\n\n/** Read config.json (missing/invalid → empty config, never throws). */\nexport function loadConfig(): CloudtunnelConfig {\n try {\n return JSON.parse(readFileSync(configFile, \"utf8\")) as CloudtunnelConfig;\n } catch {\n return {};\n }\n}\n\n/** Persist config.json with owner-only perms (0600). */\nexport function saveConfig(config: CloudtunnelConfig): void {\n ensureDirs();\n writeFileSync(configFile, JSON.stringify(config, null, 2), { mode: 0o600 });\n chmodSync(configFile, 0o600); // enforce even if the file pre-existed\n}\n\n/**\n * Resolve credentials: env vars override the config file. Throws an actionable\n * CliError if no API token is available anywhere.\n */\nexport function getCredentials(): Credentials {\n const config = loadConfig();\n const apiToken = process.env.CLOUDFLARE_API_TOKEN ?? config.apiToken;\n const accountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? config.accountId;\n if (!apiToken) {\n throw new CliError(\"Not authenticated with Cloudflare.\", {\n hint: \"run `cloudtunnel login` (or set CLOUDFLARE_API_TOKEN)\",\n });\n }\n return { apiToken, accountId, defaultZone: config.defaultZone };\n}\n","import envPaths from \"env-paths\";\nimport { mkdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n// `~/.config/cloudtunnel/` (XDG). suffix:'' avoids env-paths' default \"-nodejs\".\nconst paths = envPaths(\"cloudtunnel\", { suffix: \"\" });\n\nexport const configDir = paths.config;\nexport const configFile = join(configDir, \"config.json\");\nexport const registryFile = join(configDir, \"tunnels.json\");\nexport const profilesFile = join(configDir, \"profiles.json\");\nexport const binDir = join(configDir, \"bin\");\nexport const logDir = join(configDir, \"logs\");\n\n/** Create the app dirs with owner-only perms (secrets live here). Idempotent. */\nexport function ensureDirs(): void {\n for (const dir of [configDir, binDir, logDir]) {\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n}\n","import { CliError } from \"../ui/errors.js\";\nimport { getCredentials } from \"../config/store.js\";\n\nconst API_BASE = \"https://api.cloudflare.com/client/v4\";\nconst RETRYABLE = new Set([429, 500, 502, 503, 504]);\nconst MAX_ATTEMPTS = 4;\n\n/** Resolved Cloudflare context for account-scoped calls. */\nexport interface Cf {\n token: string;\n accountId: string;\n}\n\ninterface CfEnvelope<T> {\n success: boolean;\n result: T;\n result_info?: { page: number; total_pages?: number; per_page: number; count: number };\n errors?: { message: string }[];\n}\n\n/** Token + account id required for tunnel ops. Throws actionably if unresolved. */\nexport function resolveCf(): Cf {\n const creds = getCredentials();\n if (!creds.accountId) {\n throw new CliError(\"No Cloudflare account id resolved.\", {\n hint: \"run `cloudtunnel login` (or set CLOUDFLARE_ACCOUNT_ID)\",\n });\n }\n return { token: creds.apiToken, accountId: creds.accountId };\n}\n\nconst sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));\n\n/**\n * Single Cloudflare API call with backoff on 429/5xx (honors Retry-After).\n * Errors are sanitized — the bearer token is only ever sent in the header and\n * never appears in a thrown message.\n */\nexport async function cfRequest<T>(\n token: string,\n method: string,\n path: string,\n body?: unknown,\n): Promise<CfEnvelope<T>> {\n for (let attempt = 0; ; attempt++) {\n let res: Response;\n try {\n res = await fetch(`${API_BASE}${path}`, {\n method,\n headers: { Authorization: `Bearer ${token}`, \"Content-Type\": \"application/json\" },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n } catch {\n throw new CliError(\"Could not reach the Cloudflare API (network error).\");\n }\n if (RETRYABLE.has(res.status) && attempt < MAX_ATTEMPTS) {\n const retryAfter = Number(res.headers.get(\"retry-after\")) || 0;\n await sleep(retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 500);\n continue;\n }\n const env = (await res.json().catch(() => ({}))) as CfEnvelope<T>;\n if (!res.ok || !env.success) {\n const msg = env.errors?.[0]?.message ?? `HTTP ${res.status}`;\n throw new CliError(`Cloudflare API error: ${msg}`, { status: res.status });\n }\n return env;\n }\n}\n\n/**\n * Fetch every page of a list endpoint. Robust when `total_pages` is absent:\n * stops when a page returns fewer than per_page items.\n */\nexport async function cfPaginate<T>(token: string, basePath: string): Promise<T[]> {\n const sep = basePath.includes(\"?\") ? \"&\" : \"?\";\n const out: T[] = [];\n for (let page = 1; ; page++) {\n const env = await cfRequest<T[]>(token, \"GET\", `${basePath}${sep}per_page=50&page=${page}`);\n const items = env.result ?? [];\n out.push(...items);\n const perPage = env.result_info?.per_page ?? 50;\n const totalPages = env.result_info?.total_pages;\n const more = totalPages ? page < totalPages : items.length === perPage;\n if (items.length === 0 || !more) break;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CliError,
|
|
4
|
+
cfPaginate,
|
|
5
|
+
cfRequest
|
|
6
|
+
} from "./chunk-UPBVRXLF.js";
|
|
7
|
+
|
|
8
|
+
// src/cloudflare/dns.ts
|
|
9
|
+
var MANAGED_DNS_COMMENT = "managed-by:cloudtunnel";
|
|
10
|
+
function cargoTarget(tunnelId) {
|
|
11
|
+
return `${tunnelId}.cfargotunnel.com`;
|
|
12
|
+
}
|
|
13
|
+
function isManagedDns(record) {
|
|
14
|
+
return record.comment === MANAGED_DNS_COMMENT;
|
|
15
|
+
}
|
|
16
|
+
async function createCname(token, zoneId, name, tunnelId) {
|
|
17
|
+
const env = await cfRequest(token, "POST", `/zones/${zoneId}/dns_records`, {
|
|
18
|
+
type: "CNAME",
|
|
19
|
+
name,
|
|
20
|
+
content: cargoTarget(tunnelId),
|
|
21
|
+
proxied: true,
|
|
22
|
+
ttl: 1,
|
|
23
|
+
comment: MANAGED_DNS_COMMENT
|
|
24
|
+
});
|
|
25
|
+
if (env.result.proxied !== true) {
|
|
26
|
+
throw new CliError(`DNS record for ${name} was created grey-clouded (not proxied).`, {
|
|
27
|
+
hint: "cfargotunnel routing requires a proxied (orange-cloud) record"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return env.result;
|
|
31
|
+
}
|
|
32
|
+
async function findCname(token, zoneId, fqdn) {
|
|
33
|
+
const env = await cfRequest(
|
|
34
|
+
token,
|
|
35
|
+
"GET",
|
|
36
|
+
`/zones/${zoneId}/dns_records?type=CNAME&name=${encodeURIComponent(fqdn)}`
|
|
37
|
+
);
|
|
38
|
+
return env.result?.[0];
|
|
39
|
+
}
|
|
40
|
+
async function listCargoCnames(token, zoneId) {
|
|
41
|
+
const all = await cfPaginate(token, `/zones/${zoneId}/dns_records?type=CNAME`);
|
|
42
|
+
return all.filter((r) => r.content.endsWith(".cfargotunnel.com"));
|
|
43
|
+
}
|
|
44
|
+
async function deleteDnsRecord(token, zoneId, recordId) {
|
|
45
|
+
await cfRequest(token, "DELETE", `/zones/${zoneId}/dns_records/${recordId}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
MANAGED_DNS_COMMENT,
|
|
50
|
+
cargoTarget,
|
|
51
|
+
isManagedDns,
|
|
52
|
+
createCname,
|
|
53
|
+
findCname,
|
|
54
|
+
listCargoCnames,
|
|
55
|
+
deleteDnsRecord
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=chunk-YLTQB4F7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cloudflare/dns.ts"],"sourcesContent":["import { cfPaginate, cfRequest } from \"./client.js\";\nimport type { DnsRecord } from \"./types.js\";\nimport { CliError } from \"../ui/errors.js\";\n\n/** DNS records created by cloudtunnel carry this comment (ownership marker). */\nexport const MANAGED_DNS_COMMENT = \"managed-by:cloudtunnel\";\n\nexport function cargoTarget(tunnelId: string): string {\n return `${tunnelId}.cfargotunnel.com`;\n}\n\nexport function isManagedDns(record: DnsRecord): boolean {\n return record.comment === MANAGED_DNS_COMMENT;\n}\n\n/** Create the proxied CNAME that routes a subdomain to the tunnel. Asserts the\n * record came back orange-clouded (grey-cloud would silently break routing). */\nexport async function createCname(\n token: string,\n zoneId: string,\n name: string,\n tunnelId: string,\n): Promise<DnsRecord> {\n const env = await cfRequest<DnsRecord>(token, \"POST\", `/zones/${zoneId}/dns_records`, {\n type: \"CNAME\",\n name,\n content: cargoTarget(tunnelId),\n proxied: true,\n ttl: 1,\n comment: MANAGED_DNS_COMMENT,\n });\n if (env.result.proxied !== true) {\n throw new CliError(`DNS record for ${name} was created grey-clouded (not proxied).`, {\n hint: \"cfargotunnel routing requires a proxied (orange-cloud) record\",\n });\n }\n return env.result;\n}\n\nexport async function findCname(\n token: string,\n zoneId: string,\n fqdn: string,\n): Promise<DnsRecord | undefined> {\n const env = await cfRequest<DnsRecord[]>(\n token,\n \"GET\",\n `/zones/${zoneId}/dns_records?type=CNAME&name=${encodeURIComponent(fqdn)}`,\n );\n return env.result?.[0];\n}\n\n/** CNAMEs in the zone that point at a cfargotunnel target (any tool's). */\nexport async function listCargoCnames(token: string, zoneId: string): Promise<DnsRecord[]> {\n const all = await cfPaginate<DnsRecord>(token, `/zones/${zoneId}/dns_records?type=CNAME`);\n return all.filter((r) => r.content.endsWith(\".cfargotunnel.com\"));\n}\n\nexport async function deleteDnsRecord(token: string, zoneId: string, recordId: string): Promise<void> {\n await cfRequest<unknown>(token, \"DELETE\", `/zones/${zoneId}/dns_records/${recordId}`);\n}\n"],"mappings":";;;;;;;;AAKO,IAAM,sBAAsB;AAE5B,SAAS,YAAY,UAA0B;AACpD,SAAO,GAAG,QAAQ;AACpB;AAEO,SAAS,aAAa,QAA4B;AACvD,SAAO,OAAO,YAAY;AAC5B;AAIA,eAAsB,YACpB,OACA,QACA,MACA,UACoB;AACpB,QAAM,MAAM,MAAM,UAAqB,OAAO,QAAQ,UAAU,MAAM,gBAAgB;AAAA,IACpF,MAAM;AAAA,IACN;AAAA,IACA,SAAS,YAAY,QAAQ;AAAA,IAC7B,SAAS;AAAA,IACT,KAAK;AAAA,IACL,SAAS;AAAA,EACX,CAAC;AACD,MAAI,IAAI,OAAO,YAAY,MAAM;AAC/B,UAAM,IAAI,SAAS,kBAAkB,IAAI,4CAA4C;AAAA,MACnF,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACA,SAAO,IAAI;AACb;AAEA,eAAsB,UACpB,OACA,QACA,MACgC;AAChC,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA;AAAA,IACA,UAAU,MAAM,gCAAgC,mBAAmB,IAAI,CAAC;AAAA,EAC1E;AACA,SAAO,IAAI,SAAS,CAAC;AACvB;AAGA,eAAsB,gBAAgB,OAAe,QAAsC;AACzF,QAAM,MAAM,MAAM,WAAsB,OAAO,UAAU,MAAM,yBAAyB;AACxF,SAAO,IAAI,OAAO,CAAC,MAAM,EAAE,QAAQ,SAAS,mBAAmB,CAAC;AAClE;AAEA,eAAsB,gBAAgB,OAAe,QAAgB,UAAiC;AACpG,QAAM,UAAmB,OAAO,UAAU,UAAU,MAAM,gBAAgB,QAAQ,EAAE;AACtF;","names":[]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
MANAGED_DNS_COMMENT,
|
|
4
|
+
cargoTarget,
|
|
5
|
+
createCname,
|
|
6
|
+
deleteDnsRecord,
|
|
7
|
+
findCname,
|
|
8
|
+
isManagedDns,
|
|
9
|
+
listCargoCnames
|
|
10
|
+
} from "./chunk-YLTQB4F7.js";
|
|
11
|
+
import "./chunk-UPBVRXLF.js";
|
|
12
|
+
export {
|
|
13
|
+
MANAGED_DNS_COMMENT,
|
|
14
|
+
cargoTarget,
|
|
15
|
+
createCname,
|
|
16
|
+
deleteDnsRecord,
|
|
17
|
+
findCname,
|
|
18
|
+
isManagedDns,
|
|
19
|
+
listCargoCnames
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=dns-PAPFSYFP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|