@camstack/addon-cloudflare-tunnel 0.1.14 → 0.1.16
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/dist/cloudflare-tunnel.addon.js +1125 -74
- package/dist/cloudflare-tunnel.addon.js.map +1 -1
- package/dist/cloudflare-tunnel.addon.mjs +1124 -5
- package/dist/cloudflare-tunnel.addon.mjs.map +1 -1
- package/dist/index.js +6 -162
- package/dist/index.mjs +2 -87
- package/package.json +5 -3
- package/dist/chunk-VHOC5TFB.mjs +0 -55
- package/dist/chunk-VHOC5TFB.mjs.map +0 -1
- package/dist/cloudflare-tunnel.addon.d.mts +0 -19
- package/dist/cloudflare-tunnel.addon.d.ts +0 -19
- package/dist/index.d.mts +0 -25
- package/dist/index.d.ts +0 -25
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
1
|
+
{"version":3,"file":"cloudflare-tunnel.addon.mjs","names":[],"sources":["../src/cloudflare-tunnel.ts","../src/cloudflare-api.ts","../src/cloudflare-actions.ts","../src/cloudflare-tunnel.addon.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport { spawn, type ChildProcessByStdio } from 'node:child_process'\nimport type { Readable } from 'node:stream'\n\nimport { EventCategory } from '@camstack/types'\nimport type { IScopedLogger, IEventBus } from '@camstack/types'\n\ntype CloudflaredChild = ChildProcessByStdio<null, Readable, Readable>\n\n/**\n * Endpoint shape the orchestrator + tRPC NetworkEndpointSchema expect.\n * Stricter than the legacy server-network.ts INetworkEndpoint (which\n * had id/type/provider/internal/capabilities/priority/status — none of\n * those are in the wire schema). The cap-mount validates against this.\n */\ninterface CloudflareEndpoint {\n url: string\n hostname: string\n port: number\n protocol: 'http' | 'https'\n}\n\ninterface CloudflareStatus {\n connected: boolean\n endpoint: CloudflareEndpoint | null\n error?: string\n}\n\nexport interface CloudflareTunnelConfig {\n /** 'quick' = cloudflared --url ... (random trycloudflare URL).\n * 'custom' = server-managed named tunnel pointed at `customHostname`. */\n readonly mode: 'quick' | 'custom'\n\n /** Local port to expose through the tunnel. */\n readonly localPort: number\n\n /** Local hostname the tunnel should target. Empty = `127.0.0.1`.\n * Set automatically by the addon from the `local-network` cap so\n * Docker sidecar deployments hit the hub container's eth0 instead\n * of an unreachable loopback. */\n readonly localHost: string\n\n // ── Custom-mode persisted state (filled by enableCustom action) ──\n // All declared non-optional so `BaseAddon.defaults` can list them and\n // `resolveConfig` rehydrates them from disk. Empty string `''` is the\n // \"not yet set\" sentinel — `handleStart` checks for falsy.\n /** Cloudflare account id (probed during enableCustom). */\n readonly customAccountId: string\n /** Cloudflare tunnel id (returned by POST cfd_tunnel). */\n readonly customTunnelId: string\n /** JWT used by `cloudflared tunnel run --token` for this tunnel. */\n readonly customTunnelToken: string\n /** Zone id the CNAME was created in (for cleanup). */\n readonly customZoneId: string\n /** Zone name (e.g. \"example.com\") — used to compose the FQDN and as\n * a display hint in the UI. Cached from the zone lookup so we don't\n * need to hit Cloudflare every status read. */\n readonly customZoneName: string\n /** DNS record id of the CNAME (for cleanup on disable). */\n readonly customDnsRecordId: string\n /** Fully-qualified public hostname (e.g. \"camstack.example.com\") that\n * resolves to this CamStack hub via the cnamed tunnel. */\n readonly customHostname: string\n\n // ── Form-only transient state (re-entered on each provisioning run) ──\n /** Cloudflare API token, persisted only so the form can roundtrip it\n * inside one editing session. Cleared by `disableCustom`. */\n readonly customApiToken: string\n}\n\n/**\n * Direct child_process.spawn() driver for the `cloudflared` binary.\n *\n * Why no IProcessManager: `IKernelServices` doesn't expose one to addons,\n * and ProcessConfig doesn't have an output-callback hook. We need stdout\n * to live-parse the Quick-tunnel public URL, so spawn directly.\n *\n * Lifecycle:\n * - start() spawns the binary, attaches stdout/stderr line forwarders\n * (every line → `this.logger.info` with `tags.topic='tunnel'`),\n * watches for the `https://*.trycloudflare.com` line on Quick mode,\n * and updates `this.endpoint.url` when found.\n * - stop() sends SIGTERM, escalates to SIGKILL after 5s.\n * - Auto-restart on unexpected exit, capped at 5 retries.\n */\n\nconst QUICK_URL_REGEX = /\\bhttps:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com\\b/i\n\nexport class CloudflareTunnelService {\n readonly id = 'cloudflare-tunnel'\n readonly type = 'cloudflare'\n\n private endpoint: CloudflareEndpoint | null = null\n private lastError: string | undefined\n private child: CloudflaredChild | null = null\n private restartCount = 0\n private intentionalStop = false\n private static readonly MAX_RESTARTS = 5\n private static readonly STOP_GRACE_MS = 5_000\n\n constructor(\n private readonly config: CloudflareTunnelConfig,\n private readonly logger: IScopedLogger,\n private readonly eventBus: IEventBus,\n ) {}\n\n async start(): Promise<CloudflareEndpoint> {\n this.logger.info('Starting Cloudflare tunnel', {\n meta: { mode: this.config.mode, localPort: this.config.localPort },\n tags: { topic: 'tunnel', phase: 'starting' },\n })\n\n if (this.config.mode === 'custom') {\n if (!this.config.customTunnelToken) {\n const err = new Error('Custom tunnel not configured — run \"Enable\" from settings first')\n this.logger.error(err.message, { tags: { topic: 'tunnel', phase: 'config-error' } })\n throw err\n }\n if (!this.config.customHostname) {\n const err = new Error('Custom tunnel missing public hostname — re-run \"Enable\"')\n this.logger.error(err.message, { tags: { topic: 'tunnel', phase: 'config-error' } })\n throw err\n }\n }\n\n if (this.child !== null) {\n this.logger.warn('Cloudflare tunnel already running — refusing to spawn a duplicate', {\n tags: { topic: 'tunnel', phase: 'already-running' },\n })\n if (this.endpoint) return this.endpoint\n }\n\n this.intentionalStop = false\n this.restartCount = 0\n this.lastError = undefined\n this.spawnChild()\n\n // Quick mode: placeholder until cloudflared's stdout reports the\n // real *.trycloudflare.com URL. Custom mode: we already know the\n // public hostname (set by enableCustom), so reflect it immediately.\n const placeholderHost =\n this.config.mode === 'custom'\n ? this.config.customHostname!\n : 'pending.trycloudflare.com'\n\n this.endpoint = {\n url: `https://${placeholderHost}`,\n hostname: placeholderHost,\n port: 443,\n protocol: 'https',\n }\n\n this.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'addon', id: 'cloudflare-tunnel' },\n category: EventCategory.NetworkTunnelStarted,\n data: { url: this.endpoint.url },\n })\n\n return this.endpoint\n }\n\n async stop(): Promise<void> {\n this.logger.info('Stopping Cloudflare tunnel', {\n meta: { hadProcess: this.child !== null, hadEndpoint: this.endpoint !== null },\n tags: { topic: 'tunnel', phase: 'stopping' },\n })\n\n this.intentionalStop = true\n if (this.child !== null) {\n const child = this.child\n this.child = null\n try {\n child.kill('SIGTERM')\n // Escalate to SIGKILL if it doesn't exit within the grace window.\n const killTimer = setTimeout(() => {\n if (!child.killed) {\n this.logger.warn('cloudflared did not exit within grace — SIGKILL', {\n tags: { topic: 'tunnel', phase: 'force-kill' },\n })\n try { child.kill('SIGKILL') } catch { /* ignore */ }\n }\n }, CloudflareTunnelService.STOP_GRACE_MS)\n await new Promise<void>((resolve) => {\n child.once('exit', () => { clearTimeout(killTimer); resolve() })\n })\n } catch (err) {\n this.logger.warn('cloudflared process stop reported an error', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n tags: { topic: 'tunnel', phase: 'stop-error' },\n })\n }\n }\n this.endpoint = null\n\n this.logger.info('Cloudflare tunnel stopped', {\n tags: { topic: 'tunnel', phase: 'stopped' },\n })\n\n this.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'addon', id: 'cloudflare-tunnel' },\n category: EventCategory.NetworkTunnelStopped,\n data: {},\n })\n }\n\n getEndpoint(): CloudflareEndpoint | null {\n return this.endpoint\n }\n\n getStatus(): CloudflareStatus {\n const status: CloudflareStatus = {\n connected: this.endpoint !== null,\n endpoint: this.endpoint,\n }\n if (this.lastError !== undefined) status.error = this.lastError\n return status\n }\n\n // ── Internals ─────────────────────────────────────────────────\n\n private spawnChild(): void {\n const args =\n this.config.mode === 'quick'\n ? ['tunnel', '--url', `http://${this.config.localHost || '127.0.0.1'}:${this.config.localPort}`]\n : ['tunnel', 'run', '--token', this.config.customTunnelToken!]\n\n let child: CloudflaredChild\n try {\n child = spawn('cloudflared', args, { stdio: ['ignore', 'pipe', 'pipe'] })\n } catch (err) {\n this.logger.error('Failed to spawn cloudflared (binary missing?)', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n tags: { topic: 'tunnel', phase: 'spawn-error' },\n })\n throw err\n }\n\n this.child = child\n this.logger.info('cloudflared spawned', {\n meta: { pid: child.pid, args: args.join(' ') },\n tags: { topic: 'tunnel', phase: 'spawned' },\n })\n\n // Forward each line of stdout/stderr to the addon logger. cloudflared\n // intermixes JSON-structured messages with human-readable ones; we\n // emit them as raw text in `meta.line` and let the operator filter.\n const forwardLines = (stream: NodeJS.ReadableStream, level: 'info' | 'warn'): void => {\n let buf = ''\n stream.setEncoding('utf8')\n stream.on('data', (chunk: string) => {\n buf += chunk\n const lines = buf.split('\\n')\n buf = lines.pop() ?? ''\n for (const line of lines) {\n if (!line.trim()) continue\n // Live Quick-tunnel URL detection. cloudflared logs it once on\n // startup; we capture the FIRST match and update the endpoint\n // so the UI reflects the real public domain.\n if (this.config.mode === 'quick') {\n const match = line.match(QUICK_URL_REGEX)\n if (match && this.endpoint && this.endpoint.url !== match[0]) {\n try {\n const parsed = new URL(match[0])\n this.endpoint = {\n url: match[0],\n hostname: parsed.hostname,\n port: parsed.port ? Number(parsed.port) : (parsed.protocol === 'https:' ? 443 : 80),\n protocol: parsed.protocol === 'https:' ? 'https' : 'http',\n }\n } catch {\n // Fallback if URL constructor rejects the captured string\n this.endpoint = { ...this.endpoint, url: match[0] }\n }\n this.logger.info('Quick tunnel URL ready', {\n meta: { url: match[0] },\n tags: { topic: 'tunnel', phase: 'quick-url-ready' },\n })\n this.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'addon', id: 'cloudflare-tunnel' },\n category: EventCategory.NetworkTunnelStarted,\n data: { url: match[0], updated: true },\n })\n }\n }\n if (level === 'warn') {\n this.logger.warn(line, { tags: { topic: 'tunnel', stream: 'stderr' } })\n } else {\n this.logger.info(line, { tags: { topic: 'tunnel', stream: 'stdout' } })\n }\n }\n })\n }\n forwardLines(child.stdout, 'info')\n // cloudflared writes everything to stderr by convention — keep it\n // logged at info level rather than warn so the noise doesn't drown\n // out real warnings. Only true failures (exit) get warn/error.\n forwardLines(child.stderr, 'info')\n\n child.on('exit', (code, signal) => {\n this.logger.info('cloudflared exited', {\n meta: { code, signal, intentional: this.intentionalStop },\n tags: { topic: 'tunnel', phase: 'exited' },\n })\n if (this.child === child) this.child = null\n\n if (!this.intentionalStop && this.restartCount < CloudflareTunnelService.MAX_RESTARTS) {\n this.restartCount++\n const backoffMs = Math.min(1_000 * Math.pow(2, this.restartCount), 30_000)\n this.logger.warn('cloudflared crashed — restarting with backoff', {\n meta: { attempt: this.restartCount, backoffMs },\n tags: { topic: 'tunnel', phase: 'restarting' },\n })\n setTimeout(() => {\n if (!this.intentionalStop) {\n try { this.spawnChild() } catch (err) {\n this.logger.error('cloudflared restart failed', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n tags: { topic: 'tunnel', phase: 'restart-error' },\n })\n }\n }\n }, backoffMs)\n } else if (!this.intentionalStop) {\n this.logger.error('cloudflared crashed too many times — giving up', {\n meta: { restartCount: this.restartCount },\n tags: { topic: 'tunnel', phase: 'gave-up' },\n })\n this.endpoint = null\n }\n })\n\n child.on('error', (err) => {\n this.logger.error('cloudflared process error', {\n meta: { error: err.message },\n tags: { topic: 'tunnel', phase: 'process-error' },\n })\n })\n }\n}\n","/**\n * Cloudflare API client — Tunnel + DNS Zones operations needed by the\n * Custom-tunnel flow.\n *\n * Auth: user-pasted API token (created once in the Cloudflare dashboard\n * with `Account.Cloudflare Tunnel:Edit` + `Zone.DNS:Edit` scopes).\n * The token is treated as opaque and never logged.\n *\n * Endpoints used:\n * - GET /accounts — discover account id\n * - GET /zones — list zones (interactive picker)\n * - POST /accounts/{accountId}/cfd_tunnel — create tunnel\n * - DELETE /accounts/{accountId}/cfd_tunnel/{id} — delete tunnel\n * - PUT /accounts/{accountId}/cfd_tunnel/{id}/configurations — set ingress\n * - POST /zones/{zoneId}/dns_records — create CNAME\n * - DELETE /zones/{zoneId}/dns_records/{id} — cleanup\n *\n * The wrappers throw `CloudflareApiError` with the `code` + first `message`\n * from the response envelope so callers can surface actionable errors\n * (e.g. \"Invalid API token\" vs \"Insufficient permissions\").\n */\nimport { randomUUID } from 'node:crypto'\n\nconst API_BASE = 'https://api.cloudflare.com/client/v4'\n\nexport interface CloudflareZone {\n readonly id: string\n readonly name: string\n readonly status: string\n /** Present on zone responses; lets us derive the account id without\n * needing User:Read on the token. */\n readonly account?: { readonly id: string; readonly name: string }\n}\n\nexport interface CloudflareAccount {\n readonly id: string\n readonly name: string\n}\n\nexport interface CloudflareTunnel {\n readonly id: string\n readonly name: string\n readonly token: string\n readonly accountTag: string\n}\n\nexport interface CloudflareTunnelIngress {\n readonly hostname?: string\n readonly service: string\n readonly path?: string\n}\n\nexport interface CloudflareDnsRecord {\n readonly id: string\n readonly name: string\n readonly content: string\n readonly type: string\n}\n\nexport class CloudflareApiError extends Error {\n constructor(\n public readonly status: number,\n public readonly code: number | undefined,\n message: string,\n public readonly raw?: unknown,\n ) {\n super(message)\n this.name = 'CloudflareApiError'\n }\n}\n\ninterface CloudflareEnvelope<T> {\n readonly success: boolean\n readonly errors?: readonly { code: number; message: string }[]\n readonly result?: T\n}\n\nexport class CloudflareApi {\n constructor(private readonly token: string) {}\n\n /**\n * Resolve the account behind the token.\n *\n * Cloudflare's `GET /accounts` requires `User:Read` (or a multi-account\n * token); a token scoped narrowly to `Account → Cloudflare Tunnel:Edit\n * + Zone → DNS:Edit` on a single account returns an empty list. We\n * fall back to listing zones and reading `zone.account` — zones always\n * carry account metadata so this works regardless of token scopes.\n */\n async getAccount(): Promise<CloudflareAccount> {\n try {\n const accounts = await this.req<readonly CloudflareAccount[]>('GET', '/accounts')\n if (accounts.length > 0) return accounts[0]!\n } catch (err) {\n // 403 (token has no User:Read) is the common case here — fall\n // through to the zone-based derivation rather than surfacing it\n // to the operator.\n if (!(err instanceof CloudflareApiError) || err.status !== 403) throw err\n }\n\n const zones = await this.listZones()\n for (const z of zones) {\n if (z.account?.id) return { id: z.account.id, name: z.account.name ?? z.account.id }\n }\n throw new CloudflareApiError(\n 404,\n undefined,\n 'Token cannot reach any account — make sure it has Account → Cloudflare Tunnel:Edit AND Zone → DNS:Edit',\n )\n }\n\n /** List every zone the token can reach. */\n async listZones(): Promise<readonly CloudflareZone[]> {\n return this.req<readonly CloudflareZone[]>('GET', '/zones?per_page=50')\n }\n\n /**\n * Look up an existing tunnel by name. Returns the first non-deleted\n * match. Used by `enableCustom` to make tunnel creation idempotent —\n * a previous attempt that crashed mid-flow may have left a\n * 'camstack' tunnel behind; this lets us reuse / delete-and-recreate\n * rather than failing with \"tunnel with this name already exists\".\n */\n async findTunnelByName(accountId: string, name: string): Promise<CloudflareTunnel | null> {\n const list = await this.req<readonly CloudflareTunnel[]>(\n 'GET',\n `/accounts/${accountId}/cfd_tunnel?name=${encodeURIComponent(name)}&is_deleted=false`,\n )\n return list.length > 0 ? list[0]! : null\n }\n\n /**\n * Cloudflare's `GET /accounts/{id}/cfd_tunnel/{id}/token` returns the\n * connector JWT for an existing tunnel. We use it when reusing a\n * previously-created tunnel — the create-tunnel response embeds the\n * token, but lookups by name only return metadata.\n */\n async getTunnelToken(accountId: string, tunnelId: string): Promise<string> {\n return this.req<string>('GET', `/accounts/${accountId}/cfd_tunnel/${tunnelId}/token`)\n }\n\n async createTunnel(accountId: string, name: string): Promise<CloudflareTunnel> {\n return this.req<CloudflareTunnel>('POST', `/accounts/${accountId}/cfd_tunnel`, {\n name,\n // Cloudflare requires a random tunnel_secret (base64, ≥32 bytes) for\n // legacy clients. The new `--token` flow embeds creds in the JWT\n // returned by the create call; the secret is still required by the\n // API contract but is unused by `cloudflared run --token`.\n tunnel_secret: Buffer.from(randomUUID() + randomUUID()).toString('base64'),\n config_src: 'cloudflare',\n })\n }\n\n async deleteTunnel(accountId: string, tunnelId: string): Promise<void> {\n await this.req<unknown>('DELETE', `/accounts/${accountId}/cfd_tunnel/${tunnelId}`)\n }\n\n async putTunnelConfiguration(\n accountId: string,\n tunnelId: string,\n ingress: readonly CloudflareTunnelIngress[],\n ): Promise<void> {\n await this.req<unknown>('PUT', `/accounts/${accountId}/cfd_tunnel/${tunnelId}/configurations`, {\n config: { ingress },\n })\n }\n\n async createDnsRecord(\n zoneId: string,\n record: { name: string; content: string; type: 'CNAME' | 'A'; proxied?: boolean },\n ): Promise<CloudflareDnsRecord> {\n return this.req<CloudflareDnsRecord>('POST', `/zones/${zoneId}/dns_records`, {\n type: record.type,\n name: record.name,\n content: record.content,\n proxied: record.proxied ?? true,\n ttl: 1, // automatic (Cloudflare-managed when proxied)\n })\n }\n\n async deleteDnsRecord(zoneId: string, recordId: string): Promise<void> {\n await this.req<unknown>('DELETE', `/zones/${zoneId}/dns_records/${recordId}`)\n }\n\n // ── Internals ──────────────────────────────────────────────────\n\n private async req<T>(method: string, path: string, body?: unknown): Promise<T> {\n const res = await fetch(`${API_BASE}${path}`, {\n method,\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n })\n const text = await res.text()\n let parsed: CloudflareEnvelope<T> | undefined\n try { parsed = text ? JSON.parse(text) as CloudflareEnvelope<T> : undefined } catch { parsed = undefined }\n if (!res.ok || !parsed?.success) {\n const firstError = parsed?.errors?.[0]\n throw new CloudflareApiError(\n res.status,\n firstError?.code,\n firstError?.message ?? `Cloudflare API ${method} ${path} failed (${res.status})`,\n parsed,\n )\n }\n return parsed.result as T\n }\n}\n","/**\n * Cloudflare Tunnel — customActions catalog.\n *\n * Exposes the server-managed flow as discrete steps that the admin-ui\n * settings form drives via `api.addons.custom.{query,mutate}({ addonId,\n * action, input })`:\n *\n * validateToken — probe the API token, return account name + id\n * (used by the settings form as an inline \"Connect\"\n * button; populates the zone picker on success).\n *\n * listZones — return the zones the token can reach\n * (drives the interactive \"Domain\" select in Custom\n * mode; this is the option-source for the new\n * `addon-action-select` form-builder field type).\n *\n * enableCustom — server-managed setup: create the tunnel via the\n * Cloudflare API, write the public CNAME, persist the\n * tunnel JWT, and switch the addon into 'custom' mode\n * so subsequent starts pick the persisted token.\n *\n * disableCustom — tear down: best-effort delete the DNS record + the\n * tunnel via the API and reset persisted state to the\n * Quick-tunnel default.\n *\n * Quick mode needs NO actions — `start`/`stop` on the network-access cap\n * is enough (cloudflared invents the public URL).\n */\nimport { z } from 'zod'\nimport { customAction, defineCustomActions } from '@camstack/types'\n\nconst ZoneSchema = z.object({\n id: z.string(),\n name: z.string(),\n status: z.string(),\n})\n\nexport const cloudflareTunnelActions = defineCustomActions({\n validateToken: customAction(\n z.object({ token: z.string().min(20) }),\n z.object({\n ok: z.literal(true),\n accountId: z.string(),\n accountName: z.string(),\n }),\n { kind: 'mutation' },\n ),\n\n listZones: customAction(\n z.object({ token: z.string().min(20) }),\n z.object({ zones: z.array(ZoneSchema).readonly() }),\n { kind: 'mutation' },\n ),\n\n enableCustom: customAction(\n z.object({\n token: z.string().min(20),\n zoneId: z.string().min(1),\n hostname: z.string().min(1),\n localPort: z.number().int().min(1).max(65535),\n }),\n z.object({\n ok: z.literal(true),\n tunnelId: z.string(),\n hostname: z.string(),\n }),\n { kind: 'mutation' },\n ),\n\n disableCustom: customAction(\n // `.optional()` because tRPC/superjson elides empty-object inputs\n // on the wire; the server-side parse sees `undefined`. Accept both\n // `{}` and `undefined` so the form-builder doesn't have to know.\n z.object({}).optional(),\n z.object({ ok: z.literal(true) }),\n { kind: 'mutation' },\n ),\n\n /**\n * Bridge to the hub-only `local-network` cap so the settings UI can\n * populate an `addon-action-select` field with operator-pinnable\n * candidate addresses for the tunnel ingress. Returned shape matches\n * what the form-builder's `addon-action-select` expects:\n * `{ addresses: [{ value, label, description }] }`.\n */\n listLocalAddresses: customAction(\n z.object({}).optional(),\n z.object({\n addresses: z.array(z.object({\n value: z.string(),\n label: z.string(),\n description: z.string(),\n })).readonly(),\n }),\n ),\n})\n\nexport type CloudflareTunnelActions = typeof cloudflareTunnelActions\n","import type { AddonInitResult } from '@camstack/types'\nimport { BaseAddon, networkAccessCapability } from '@camstack/types'\nimport type { z } from 'zod'\nimport { CloudflareTunnelService, type CloudflareTunnelConfig } from './cloudflare-tunnel'\nimport { CloudflareApi } from './cloudflare-api'\nimport { cloudflareTunnelActions, type CloudflareTunnelActions } from './cloudflare-actions'\n\n// Re-export under the conventional name so the kernel addon-loader can\n// discover the catalog directly off the entry module (the manifest's\n// `entry` points at this file, NOT `index.ts`, so a re-export there\n// would be invisible to the hub).\nexport { cloudflareTunnelActions as customActions } from './cloudflare-actions'\n\n/**\n * Cloudflare Tunnel — exposes CamStack via Cloudflare's network.\n *\n * Two flows, both driven by a single `start()` button on the Remote\n * Access page:\n *\n * • Quick (`mode='quick'`) — `start()` spawns `cloudflared tunnel --url\n * http://localhost:<port>`; cloudflared invents a random\n * `*.trycloudflare.com` URL and we surface it back to the operator.\n *\n * • Custom (`mode='custom'`) — operator fills API token + zone +\n * hostname in settings, then hits `start()`. The addon (server-side)\n * auto-provisions: creates / reuses the tunnel, writes the CNAME,\n * persists the JWT, then spawns `cloudflared tunnel run --token <jwt>`.\n * Subsequent starts skip the provisioning step (token is cached).\n *\n * `listZones` is exposed as a customAction so the settings form's\n * `addon-action-select` zone picker can populate options live.\n */\nexport class CloudflareTunnelAddon extends BaseAddon<CloudflareTunnelConfig> {\n private service: CloudflareTunnelService | null = null\n\n constructor() {\n // ALL config keys must appear here — `BaseAddon.resolveConfig`\n // only re-reads keys present in `defaults`, anything else stays\n // stuck on its constructor value regardless of what's on disk.\n // Custom-mode fields default to empty strings so the type guard\n // in `handleStart` (\"token / zoneId / hostname missing\") doubles\n // as a \"filled by operator?\" check.\n super({\n mode: 'quick',\n localPort: 0, // 0 = auto-detect from CAMSTACK_PORT / PORT env at start\n localHost: '', // empty = auto-detect from local-network cap at start\n customApiToken: '',\n customAccountId: '',\n customTunnelId: '',\n customTunnelToken: '',\n customZoneId: '',\n customZoneName: '',\n customDnsRecordId: '',\n customHostname: 'camstack',\n })\n }\n\n /**\n * Resolve the local hub HTTP port the tunnel must front. Order:\n * 1. Explicit `localPort` in addon config (> 0) — operator override.\n * 2. `CAMSTACK_PORT` env (set by the bootstrap config manager).\n * 3. `PORT` env (legacy / .env fallback).\n * 4. 4000 — current CamStack default.\n */\n private resolveLocalPort(): number {\n if (this.config.localPort && this.config.localPort > 0) return this.config.localPort\n const camstack = Number.parseInt(process.env['CAMSTACK_PORT'] ?? '', 10)\n if (Number.isFinite(camstack) && camstack > 0) return camstack\n const port = Number.parseInt(process.env['PORT'] ?? '', 10)\n if (Number.isFinite(port) && port > 0) return port\n return 4000\n }\n\n /**\n * Ask the `local-network` cap for the preferred LAN address. Fallback\n * is `127.0.0.1` when the cap is unreachable / not yet mounted.\n * Cached for the duration of one provisioning call.\n */\n private async resolveLocalHost(): Promise<string> {\n try {\n const api = this.ctx.api as unknown as {\n localNetwork?: {\n getPreferred?: { query: () => Promise<{ address: string } | null> }\n }\n }\n const preferred = await api.localNetwork?.getPreferred?.query()\n if (preferred?.address) return preferred.address\n } catch (err) {\n this.ctx.logger.warn('local-network getPreferred failed — falling back to localhost', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n tags: { topic: 'tunnel', phase: 'localhost-fallback' },\n })\n }\n return '127.0.0.1'\n }\n\n // `local-network` runs in-process on the hub while this addon may\n // be forked. We don't push the FQDN directly — `local-network`\n // subscribes to `NetworkTunnelStarted/Stopped` events on the bus\n // (the service already emits them) and tracks the hostname there.\n\n protected async onInitialize(): Promise<AddonInitResult<CloudflareTunnelActions>> {\n const localHost = await this.resolveLocalHost()\n this.service = new CloudflareTunnelService(\n { ...this.config, localPort: this.resolveLocalPort(), localHost },\n this.ctx.logger,\n this.ctx.eventBus,\n )\n this.ctx.logger.info('Cloudflare Tunnel addon initialized', {\n meta: { mode: this.config.mode, hasCustomToken: !!this.config.customTunnelToken },\n })\n\n // Wrap the service so `start()` runs custom-mode provisioning lazily\n // when the operator hits the Start button — single entry point, no\n // separate \"Enable\" affordance. `getStatus`/`stop` pass through.\n const provider = {\n start: () => this.handleStart(),\n stop: () => this.requireService().stop(),\n getStatus: () => this.requireService().getStatus(),\n }\n\n return {\n providers: [{ capability: networkAccessCapability, provider }],\n customActions: cloudflareTunnelActions,\n actionHandlers: {\n validateToken: async (input) => this.validateToken(input),\n listZones: async (input) => this.listZones(input),\n enableCustom: async (input) => this.enableCustom(input),\n disableCustom: async () => this.disableCustom(),\n listLocalAddresses: async () => this.listLocalAddresses(),\n },\n }\n }\n\n protected async onShutdown(): Promise<void> {\n if (this.service) {\n await this.service.stop().catch(() => undefined)\n this.service = null\n }\n }\n\n protected async onConfigChanged(): Promise<void> {\n // Rebuild the service against the new config so `start()` picks up\n // the latest mode + persisted custom-tunnel state. If the tunnel\n // was running, transparently restart it on the new config so an\n // operator who edits (e.g.) the local-host select doesn't have to\n // re-hit Start manually. Tunnel state events (`NetworkTunnelStarted`\n // / `Stopped`) keep firing so the local-network cap's public-FQDN\n // tracker stays in sync.\n const wasRunning = this.service?.getStatus().connected === true\n if (this.service) {\n await this.service.stop().catch(() => undefined)\n }\n const localHost = await this.resolveLocalHost()\n this.service = new CloudflareTunnelService(\n { ...this.config, localPort: this.resolveLocalPort(), localHost },\n this.ctx.logger,\n this.ctx.eventBus,\n )\n if (wasRunning) {\n this.ctx.logger.info('config changed while running — re-spawning tunnel', {\n tags: { topic: 'tunnel', phase: 'config-reload' },\n })\n try {\n await this.handleStart()\n } catch (err) {\n this.ctx.logger.error('config-reload restart failed', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n tags: { topic: 'tunnel', phase: 'config-reload-error' },\n })\n }\n }\n }\n\n // ── network-access provider wrapper ────────────────────────────────\n\n /**\n * Start the tunnel, provisioning custom mode first if needed. This is\n * the single button the orchestrator (Remote Access page) calls; the\n * addon decides whether the underlying spawn is quick or token-based.\n */\n private async handleStart() {\n // Re-provision when:\n // • token missing (first time), OR\n // • the persisted hostname isn't a FQDN yet (migration from the\n // pre-FQDN schema — operator's existing config had `camstack`\n // instead of `camstack.example.com`). Cloudflare's ingress\n // rule matches the literal hostname so a non-FQDN stays\n // permanently unreachable until re-provision.\n const needsReprovision =\n this.config.mode === 'custom' &&\n (!this.config.customTunnelToken || !this.config.customHostname.includes('.'))\n if (needsReprovision) {\n this.ctx.logger.info('start: custom mode needs (re)provisioning', {\n meta: {\n hasToken: !!this.config.customTunnelToken,\n hostname: this.config.customHostname,\n },\n tags: { topic: 'tunnel', phase: 'auto-provision' },\n })\n const token = this.config.customApiToken\n const zoneId = this.config.customZoneId\n const hostname = this.config.customHostname\n if (!token || !zoneId || !hostname) {\n const missing = [\n !token && 'API token',\n !zoneId && 'zone',\n !hostname && 'hostname',\n ].filter(Boolean).join(', ')\n const err = new Error(`Custom tunnel needs: ${missing}. Fill the settings above first.`)\n this.ctx.logger.error('start: custom mode config incomplete', {\n meta: { missing },\n tags: { topic: 'tunnel', phase: 'auto-provision-incomplete' },\n })\n throw err\n }\n await this.enableCustom({\n token,\n zoneId,\n hostname,\n localPort: this.resolveLocalPort(),\n })\n }\n return this.requireService().start()\n }\n\n private requireService(): CloudflareTunnelService {\n if (!this.service) throw new Error('Cloudflare tunnel service not initialized')\n return this.service\n }\n\n // ── Action handlers ────────────────────────────────────────────────\n\n private async validateToken(\n input: z.infer<CloudflareTunnelActions['validateToken']['input']>,\n ): Promise<z.infer<CloudflareTunnelActions['validateToken']['output']>> {\n this.ctx.logger.info('validateToken: probing API token', {\n tags: { topic: 'tunnel', phase: 'validate-token' },\n })\n const api = new CloudflareApi(input.token)\n try {\n const account = await api.getAccount()\n this.ctx.logger.info('validateToken: token OK', {\n meta: { accountId: account.id, accountName: account.name },\n tags: { topic: 'tunnel', phase: 'validate-token-ok' },\n })\n return { ok: true, accountId: account.id, accountName: account.name }\n } catch (err) {\n this.ctx.logger.error('validateToken: probe failed', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n tags: { topic: 'tunnel', phase: 'validate-token-error' },\n })\n throw err\n }\n }\n\n /**\n * Surface the local-network cap's interface list to the form-builder\n * `addon-action-select` field so the operator can pin which address\n * the tunnel ingress should target (Docker sidecar, multi-NIC host,\n * etc). Empty `value` represents \"auto — fall back to\n * local-network.getPreferred()\".\n */\n private async listLocalAddresses(): Promise<{ addresses: readonly { value: string; label: string; description: string }[] }> {\n try {\n const api = this.ctx.api as unknown as {\n localNetwork?: { list: { query: () => Promise<{ interfaces: ReadonlyArray<{ name: string; address: string; kind: string; family: string; preferred: boolean; internal: boolean }> }> } }\n }\n const res = await api.localNetwork?.list?.query()\n const interfaces = res?.interfaces ?? []\n const options = interfaces\n // Drop internal/link-local — they're useless for tunnel ingress.\n .filter((i) => !i.internal && !i.address.startsWith('169.254.') && i.family === 'IPv4')\n .map((i) => ({\n value: i.address,\n label: `${i.name} — ${i.address}`,\n description: `${i.kind}${i.preferred ? ' · auto-preferred' : ''}`,\n }))\n // Prepend an explicit \"auto\" option so operators can opt back into\n // the heuristic without manually clearing the field.\n return {\n addresses: [\n { value: '', label: 'Auto (use local-network preferred)', description: 're-evaluated on every start' },\n ...options,\n ],\n }\n } catch (err) {\n this.ctx.logger.warn('listLocalAddresses: local-network cap unreachable', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n tags: { topic: 'tunnel', phase: 'list-local-addresses-error' },\n })\n return { addresses: [{ value: '', label: 'Auto (use local-network preferred)', description: 'fallback' }] }\n }\n }\n\n private async listZones(\n input: z.infer<CloudflareTunnelActions['listZones']['input']>,\n ): Promise<z.infer<CloudflareTunnelActions['listZones']['output']>> {\n this.ctx.logger.info('listZones: fetching zones', {\n tags: { topic: 'tunnel', phase: 'list-zones' },\n })\n const api = new CloudflareApi(input.token)\n try {\n const zones = await api.listZones()\n this.ctx.logger.info('listZones: returned zones', {\n meta: { count: zones.length },\n tags: { topic: 'tunnel', phase: 'list-zones-ok' },\n })\n return { zones: zones.map((z) => ({ id: z.id, name: z.name, status: z.status })) }\n } catch (err) {\n this.ctx.logger.error('listZones: fetch failed', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n tags: { topic: 'tunnel', phase: 'list-zones-error' },\n })\n throw err\n }\n }\n\n /**\n * Server-managed custom-tunnel provisioning. Idempotent — reuses an\n * existing 'camstack' tunnel by name and treats \"DNS record already\n * exists\" as success. Persists tunnel id + JWT + DNS record id back\n * into the addon config so subsequent `start()` calls skip provisioning.\n *\n * Kept exposed as a customAction (rather than purely internal) for\n * scripting / manual re-provisioning from external clients; the UI\n * now drives provisioning via the `start()` Start button instead.\n */\n private async enableCustom(\n input: z.infer<CloudflareTunnelActions['enableCustom']['input']>,\n ): Promise<z.infer<CloudflareTunnelActions['enableCustom']['output']>> {\n this.ctx.logger.info('enableCustom: starting', {\n meta: { hostname: input.hostname, zoneId: input.zoneId, localPort: input.localPort },\n tags: { topic: 'tunnel', phase: 'enable-start' },\n })\n const api = new CloudflareApi(input.token)\n\n try {\n // Resolve zone name so we can build a fully-qualified hostname.\n // Cloudflare's DNS API accepts a short name and prepends the zone\n // on its side; but for the persisted endpoint URL + the tunnel\n // ingress hostname we need the FQDN, otherwise we display\n // \"https://camstack\" instead of \"https://camstack.example.com\".\n const zones = await api.listZones()\n const zone = zones.find((z) => z.id === input.zoneId)\n if (!zone) {\n throw new Error(`Selected zone ${input.zoneId} is not visible to this token`)\n }\n const subdomain = input.hostname.trim()\n // Strip a possibly-pasted full FQDN suffix (e.g. operator wrote\n // \"camstack.example.com\"); resulting subdomain matches DNS rules.\n const subBare = subdomain.endsWith(`.${zone.name}`)\n ? subdomain.slice(0, -1 - zone.name.length)\n : subdomain\n const fqdn = subBare === '@' || subBare === '' ? zone.name : `${subBare}.${zone.name}`\n this.ctx.logger.info('enableCustom: resolved FQDN', {\n meta: { subBare, zoneName: zone.name, fqdn },\n tags: { topic: 'tunnel', phase: 'enable-fqdn' },\n })\n\n const account = await api.getAccount()\n this.ctx.logger.info('enableCustom: account resolved', {\n meta: { accountId: account.id, accountName: account.name },\n tags: { topic: 'tunnel', phase: 'enable-account' },\n })\n\n // If we already have a CNAME persisted (from a prior run) AND\n // the operator is now provisioning a *different* hostname/zone,\n // tear the old CNAME down — keep the tunnel itself (it gets\n // repointed at the new hostname below).\n if (this.config.customDnsRecordId && this.config.customZoneId &&\n this.config.customHostname && this.config.customHostname.toLowerCase() !== fqdn.toLowerCase()) {\n this.ctx.logger.info('enableCustom: removing previous DNS record (hostname changed)', {\n meta: { from: this.config.customHostname, to: input.hostname },\n tags: { topic: 'tunnel', phase: 'enable-dns-cleanup' },\n })\n try {\n await api.deleteDnsRecord(this.config.customZoneId, this.config.customDnsRecordId)\n } catch (err) {\n this.ctx.logger.warn('enableCustom: previous DNS cleanup failed (continuing)', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n tags: { topic: 'tunnel', phase: 'enable-dns-cleanup-failed' },\n })\n }\n }\n\n // Reuse or create the `camstack` tunnel.\n const TUNNEL_NAME = 'camstack'\n let tunnel = await api.findTunnelByName(account.id, TUNNEL_NAME)\n if (tunnel) {\n this.ctx.logger.info('enableCustom: reusing existing tunnel by name', {\n meta: { tunnelId: tunnel.id, name: tunnel.name },\n tags: { topic: 'tunnel', phase: 'enable-tunnel-reused' },\n })\n const token = await api.getTunnelToken(account.id, tunnel.id)\n tunnel = { ...tunnel, token }\n } else {\n tunnel = await api.createTunnel(account.id, TUNNEL_NAME)\n this.ctx.logger.info('enableCustom: tunnel created', {\n meta: { tunnelId: tunnel.id, name: tunnel.name },\n tags: { topic: 'tunnel', phase: 'enable-tunnel-created' },\n })\n }\n\n const localHost = await this.resolveLocalHost()\n await api.putTunnelConfiguration(account.id, tunnel.id, [\n { hostname: fqdn, service: `http://${localHost}:${input.localPort}` },\n { service: 'http_status:404' },\n ])\n this.ctx.logger.info('enableCustom: tunnel ingress configured', {\n meta: { hostname: fqdn, localHost, localPort: input.localPort },\n tags: { topic: 'tunnel', phase: 'enable-ingress' },\n })\n\n let dns\n try {\n dns = await api.createDnsRecord(input.zoneId, {\n type: 'CNAME',\n name: fqdn,\n content: `${tunnel.id}.cfargotunnel.com`,\n proxied: true,\n })\n this.ctx.logger.info('enableCustom: DNS record created', {\n meta: { recordId: dns.id, hostname: fqdn },\n tags: { topic: 'tunnel', phase: 'enable-dns-created' },\n })\n } catch (err) {\n if (err instanceof Error && /already exists/i.test(err.message)) {\n this.ctx.logger.warn('enableCustom: DNS record already exists — assuming it points at this tunnel', {\n meta: { hostname: fqdn },\n tags: { topic: 'tunnel', phase: 'enable-dns-existing' },\n })\n dns = { id: '', name: fqdn, content: '', type: 'CNAME' as const }\n } else {\n throw err\n }\n }\n\n const patch: Partial<CloudflareTunnelConfig> = {\n mode: 'custom',\n localPort: input.localPort,\n customAccountId: account.id,\n customTunnelId: tunnel.id,\n customTunnelToken: tunnel.token,\n customZoneId: input.zoneId,\n customZoneName: zone.name,\n customDnsRecordId: dns.id,\n customHostname: fqdn,\n }\n await this.updateGlobalSettings(patch)\n\n this.ctx.logger.info('enableCustom: complete', {\n meta: { tunnelId: tunnel.id, hostname: fqdn },\n tags: { topic: 'tunnel', phase: 'enable-complete' },\n })\n return { ok: true, tunnelId: tunnel.id, hostname: fqdn }\n } catch (err) {\n this.ctx.logger.error('enableCustom: failed', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n tags: { topic: 'tunnel', phase: 'enable-error' },\n })\n throw err\n }\n }\n\n private async disableCustom(): Promise<z.infer<CloudflareTunnelActions['disableCustom']['output']>> {\n const reset: Partial<CloudflareTunnelConfig> = {\n mode: 'quick',\n localHost: '',\n customAccountId: '',\n customTunnelId: '',\n customTunnelToken: '',\n customZoneId: '',\n customZoneName: '',\n customDnsRecordId: '',\n customHostname: 'camstack',\n customApiToken: '',\n }\n this.ctx.logger.info('disableCustom: reset to quick mode', {\n tags: { topic: 'tunnel', phase: 'disable' },\n })\n await this.updateGlobalSettings(reset)\n return { ok: true }\n }\n\n protected globalSettingsSchema() {\n return this.schema({\n sections: [\n {\n id: 'mode',\n title: 'Tunnel Mode',\n // Auto-save every change — Start (single entry point) reads\n // persisted config; no Save button means no \"filled the form\n // but forgot to commit\" footguns.\n immediate: true,\n fields: [\n this.field({\n type: 'select',\n key: 'mode',\n label: 'Mode',\n description: 'Quick: random *.trycloudflare.com URL, no account. Custom: server-managed named tunnel on your own domain (requires a Cloudflare account + API token). Hit Start on the Remote Access page to launch — Custom mode auto-provisions on first start. The local hub port is auto-detected from CAMSTACK_PORT / PORT env.',\n default: 'quick',\n options: [\n { value: 'quick', label: 'Quick Tunnel', description: 'Temporary public URL, no account required' },\n { value: 'custom', label: 'Custom Domain', description: 'Persistent tunnel on your own zone' },\n ],\n }),\n {\n type: 'addon-action-select' as const,\n key: 'localHost',\n label: 'Local Address',\n description: 'Which local host address cloudflared should forward traffic to. \"Auto\" follows the local-network preferred interface — pin a specific address only when running multi-NIC or Docker sidecar setups.',\n // `''` = \"Auto (use local-network preferred)\" — first option\n // the `listLocalAddresses` handler prepends. Pinning a\n // default here means a freshly-installed addon doesn't\n // accidentally show the first probed IP as if the operator\n // had selected it.\n default: '',\n addonId: 'cloudflare-tunnel',\n action: 'listLocalAddresses',\n mapOption: { value: 'value', label: 'label', description: 'description' },\n emptyResultsMessage: 'No local addresses available — fix the local-network cap first.',\n },\n ],\n },\n {\n id: 'custom',\n title: 'Custom Domain Setup',\n immediate: true,\n fields: [\n {\n type: 'info' as const,\n key: 'customHelp',\n label: 'How to get an API token',\n format: 'html' as const,\n content:\n 'Create a token at ' +\n '<a href=\"https://dash.cloudflare.com/profile/api-tokens\">' +\n 'dash.cloudflare.com/profile/api-tokens</a>:' +\n '<ul>' +\n '<li>Click <strong>Create Token</strong> → use the <em>Custom token</em> template.</li>' +\n '<li>Permissions: <code>Account → Cloudflare Tunnel → Edit</code> AND ' +\n '<code>Zone → DNS → Edit</code>.</li>' +\n '<li>Account Resources: <code>Include → your account</code>. ' +\n 'Zone Resources: <code>Include → the zone</code> you want.</li>' +\n '<li>Continue → Create → copy the token, paste it below.</li>' +\n '</ul>',\n variant: 'info' as const,\n showWhen: { field: 'mode', equals: 'custom' },\n },\n this.field({\n type: 'password',\n key: 'customApiToken',\n label: 'Cloudflare API Token',\n description: 'Used to provision + manage the tunnel.',\n showToggle: true,\n showWhen: { field: 'mode', equals: 'custom' },\n }),\n {\n type: 'addon-action-select' as const,\n key: 'customZoneId',\n label: 'Zone',\n description: 'Pick the Cloudflare zone (root domain) the tunnel will sit on.',\n addonId: 'cloudflare-tunnel',\n action: 'listZones',\n paramsFromForm: { token: 'customApiToken' },\n refreshOn: ['customApiToken'],\n mapOption: { value: 'id', label: 'name', description: 'status' },\n emptyParamsMessage: 'Paste your API token above first.',\n emptyResultsMessage: 'Token has no zones with DNS:Edit permission.',\n showWhen: { field: 'mode', equals: 'custom' },\n },\n this.field({\n type: 'text',\n key: 'customHostname',\n label: 'Sub-domain',\n description: 'Sub-domain to prepend to the selected zone (e.g. \"camstack\" → camstack.yourdomain.com). Use \"@\" or empty for the apex.',\n default: 'camstack',\n placeholder: 'camstack',\n showWhen: { field: 'mode', equals: 'custom' },\n }),\n ],\n },\n ],\n })\n }\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,162 +1,6 @@
|
|
|
1
|
-
"
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/index.ts
|
|
21
|
-
var index_exports = {};
|
|
22
|
-
__export(index_exports, {
|
|
23
|
-
CloudflareTunnelAddon: () => CloudflareTunnelAddon,
|
|
24
|
-
CloudflareTunnelService: () => CloudflareTunnelService
|
|
25
|
-
});
|
|
26
|
-
module.exports = __toCommonJS(index_exports);
|
|
27
|
-
|
|
28
|
-
// src/cloudflare-tunnel.ts
|
|
29
|
-
var import_node_crypto = require("crypto");
|
|
30
|
-
var import_types = require("@camstack/types");
|
|
31
|
-
var CloudflareTunnelService = class {
|
|
32
|
-
constructor(config, logger, eventBus, processManager) {
|
|
33
|
-
this.config = config;
|
|
34
|
-
this.logger = logger;
|
|
35
|
-
this.eventBus = eventBus;
|
|
36
|
-
this.processManager = processManager;
|
|
37
|
-
}
|
|
38
|
-
config;
|
|
39
|
-
logger;
|
|
40
|
-
eventBus;
|
|
41
|
-
processManager;
|
|
42
|
-
id = "cloudflare-tunnel";
|
|
43
|
-
type = "cloudflare";
|
|
44
|
-
endpoint = null;
|
|
45
|
-
processId = null;
|
|
46
|
-
async start() {
|
|
47
|
-
this.logger.info("Starting Cloudflare tunnel", { meta: { mode: this.config.mode } });
|
|
48
|
-
const processConfig = {
|
|
49
|
-
id: "cloudflared-tunnel",
|
|
50
|
-
label: "Cloudflare Tunnel",
|
|
51
|
-
command: "cloudflared",
|
|
52
|
-
args: this.config.mode === "quick" ? ["tunnel", "--url", `http://localhost:${this.config.localPort}`] : ["tunnel", "run", "--token", this.config.namedTunnelToken],
|
|
53
|
-
autoRestart: true,
|
|
54
|
-
maxRestarts: 5
|
|
55
|
-
};
|
|
56
|
-
this.processManager.register(processConfig);
|
|
57
|
-
await this.processManager.start("cloudflared-tunnel");
|
|
58
|
-
this.processId = "cloudflared-tunnel";
|
|
59
|
-
const publicUrl = this.config.mode === "named" ? "https://tunnel.example.com" : "https://pending.trycloudflare.com";
|
|
60
|
-
this.endpoint = {
|
|
61
|
-
id: "cloudflare-tunnel",
|
|
62
|
-
type: "tunnel",
|
|
63
|
-
provider: "cloudflare",
|
|
64
|
-
url: publicUrl,
|
|
65
|
-
internal: false,
|
|
66
|
-
capabilities: {
|
|
67
|
-
supportsWebRTC: false,
|
|
68
|
-
supportsWebSocket: true,
|
|
69
|
-
supportsSSE: true
|
|
70
|
-
},
|
|
71
|
-
priority: 50,
|
|
72
|
-
status: "online"
|
|
73
|
-
};
|
|
74
|
-
this.eventBus.emit({
|
|
75
|
-
id: (0, import_node_crypto.randomUUID)(),
|
|
76
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
77
|
-
source: { type: "addon", id: "cloudflare-tunnel" },
|
|
78
|
-
category: import_types.EventCategory.NetworkTunnelStarted,
|
|
79
|
-
data: { url: publicUrl }
|
|
80
|
-
});
|
|
81
|
-
return this.endpoint;
|
|
82
|
-
}
|
|
83
|
-
async stop() {
|
|
84
|
-
if (this.processId) {
|
|
85
|
-
await this.processManager.stop(this.processId);
|
|
86
|
-
}
|
|
87
|
-
this.endpoint = null;
|
|
88
|
-
this.eventBus.emit({
|
|
89
|
-
id: (0, import_node_crypto.randomUUID)(),
|
|
90
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
91
|
-
source: { type: "addon", id: "cloudflare-tunnel" },
|
|
92
|
-
category: import_types.EventCategory.NetworkTunnelStopped,
|
|
93
|
-
data: {}
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
getEndpoint() {
|
|
97
|
-
return this.endpoint;
|
|
98
|
-
}
|
|
99
|
-
getStatus() {
|
|
100
|
-
return {
|
|
101
|
-
connected: this.endpoint !== null,
|
|
102
|
-
publicUrl: this.endpoint?.url
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// src/cloudflare-tunnel.addon.ts
|
|
108
|
-
var import_types2 = require("@camstack/types");
|
|
109
|
-
var CloudflareTunnelAddon = class extends import_types2.BaseAddon {
|
|
110
|
-
constructor() {
|
|
111
|
-
super({ mode: "quick", namedTunnelToken: "", localPort: 3e3 });
|
|
112
|
-
}
|
|
113
|
-
async onInitialize() {
|
|
114
|
-
this.ctx.logger.info("Cloudflare Tunnel addon initialized");
|
|
115
|
-
return [{ capability: import_types2.networkAccessCapability, provider: this }];
|
|
116
|
-
}
|
|
117
|
-
globalSettingsSchema() {
|
|
118
|
-
return this.schema({
|
|
119
|
-
sections: [{
|
|
120
|
-
id: "tunnel",
|
|
121
|
-
title: "Tunnel Settings",
|
|
122
|
-
fields: [
|
|
123
|
-
this.field({
|
|
124
|
-
type: "select",
|
|
125
|
-
key: "mode",
|
|
126
|
-
label: "Tunnel Mode",
|
|
127
|
-
description: "Quick mode creates a temporary public URL; Named mode uses a persistent named tunnel.",
|
|
128
|
-
default: "quick",
|
|
129
|
-
options: [
|
|
130
|
-
{ value: "quick", label: "Quick Tunnel", description: "Temporary public URL, no Cloudflare account required" },
|
|
131
|
-
{ value: "named", label: "Named Tunnel", description: "Persistent tunnel using a Cloudflare tunnel token" }
|
|
132
|
-
]
|
|
133
|
-
}),
|
|
134
|
-
this.field({
|
|
135
|
-
type: "password",
|
|
136
|
-
key: "namedTunnelToken",
|
|
137
|
-
label: "Tunnel Token",
|
|
138
|
-
description: "Token from your Cloudflare Zero Trust dashboard (required for Named Tunnel mode)",
|
|
139
|
-
showToggle: true,
|
|
140
|
-
showWhen: { field: "mode", equals: "named" }
|
|
141
|
-
}),
|
|
142
|
-
this.field({
|
|
143
|
-
type: "number",
|
|
144
|
-
key: "localPort",
|
|
145
|
-
label: "Local Port",
|
|
146
|
-
description: "The local port that the tunnel will expose publicly",
|
|
147
|
-
min: 1,
|
|
148
|
-
max: 65535,
|
|
149
|
-
step: 1,
|
|
150
|
-
default: 3e3
|
|
151
|
-
})
|
|
152
|
-
]
|
|
153
|
-
}]
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
158
|
-
0 && (module.exports = {
|
|
159
|
-
CloudflareTunnelAddon,
|
|
160
|
-
CloudflareTunnelService
|
|
161
|
-
});
|
|
162
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_cloudflare_tunnel_addon = require("./cloudflare-tunnel.addon.js");
|
|
3
|
+
exports.CloudflareTunnelAddon = require_cloudflare_tunnel_addon.CloudflareTunnelAddon;
|
|
4
|
+
exports.CloudflareTunnelService = require_cloudflare_tunnel_addon.CloudflareTunnelService;
|
|
5
|
+
exports.cloudflareTunnelActions = require_cloudflare_tunnel_addon.cloudflareTunnelActions;
|
|
6
|
+
exports.customActions = require_cloudflare_tunnel_addon.cloudflareTunnelActions;
|
package/dist/index.mjs
CHANGED
|
@@ -1,87 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
} from "./chunk-VHOC5TFB.mjs";
|
|
4
|
-
|
|
5
|
-
// src/cloudflare-tunnel.ts
|
|
6
|
-
import { randomUUID } from "crypto";
|
|
7
|
-
import { EventCategory } from "@camstack/types";
|
|
8
|
-
var CloudflareTunnelService = class {
|
|
9
|
-
constructor(config, logger, eventBus, processManager) {
|
|
10
|
-
this.config = config;
|
|
11
|
-
this.logger = logger;
|
|
12
|
-
this.eventBus = eventBus;
|
|
13
|
-
this.processManager = processManager;
|
|
14
|
-
}
|
|
15
|
-
config;
|
|
16
|
-
logger;
|
|
17
|
-
eventBus;
|
|
18
|
-
processManager;
|
|
19
|
-
id = "cloudflare-tunnel";
|
|
20
|
-
type = "cloudflare";
|
|
21
|
-
endpoint = null;
|
|
22
|
-
processId = null;
|
|
23
|
-
async start() {
|
|
24
|
-
this.logger.info("Starting Cloudflare tunnel", { meta: { mode: this.config.mode } });
|
|
25
|
-
const processConfig = {
|
|
26
|
-
id: "cloudflared-tunnel",
|
|
27
|
-
label: "Cloudflare Tunnel",
|
|
28
|
-
command: "cloudflared",
|
|
29
|
-
args: this.config.mode === "quick" ? ["tunnel", "--url", `http://localhost:${this.config.localPort}`] : ["tunnel", "run", "--token", this.config.namedTunnelToken],
|
|
30
|
-
autoRestart: true,
|
|
31
|
-
maxRestarts: 5
|
|
32
|
-
};
|
|
33
|
-
this.processManager.register(processConfig);
|
|
34
|
-
await this.processManager.start("cloudflared-tunnel");
|
|
35
|
-
this.processId = "cloudflared-tunnel";
|
|
36
|
-
const publicUrl = this.config.mode === "named" ? "https://tunnel.example.com" : "https://pending.trycloudflare.com";
|
|
37
|
-
this.endpoint = {
|
|
38
|
-
id: "cloudflare-tunnel",
|
|
39
|
-
type: "tunnel",
|
|
40
|
-
provider: "cloudflare",
|
|
41
|
-
url: publicUrl,
|
|
42
|
-
internal: false,
|
|
43
|
-
capabilities: {
|
|
44
|
-
supportsWebRTC: false,
|
|
45
|
-
supportsWebSocket: true,
|
|
46
|
-
supportsSSE: true
|
|
47
|
-
},
|
|
48
|
-
priority: 50,
|
|
49
|
-
status: "online"
|
|
50
|
-
};
|
|
51
|
-
this.eventBus.emit({
|
|
52
|
-
id: randomUUID(),
|
|
53
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
54
|
-
source: { type: "addon", id: "cloudflare-tunnel" },
|
|
55
|
-
category: EventCategory.NetworkTunnelStarted,
|
|
56
|
-
data: { url: publicUrl }
|
|
57
|
-
});
|
|
58
|
-
return this.endpoint;
|
|
59
|
-
}
|
|
60
|
-
async stop() {
|
|
61
|
-
if (this.processId) {
|
|
62
|
-
await this.processManager.stop(this.processId);
|
|
63
|
-
}
|
|
64
|
-
this.endpoint = null;
|
|
65
|
-
this.eventBus.emit({
|
|
66
|
-
id: randomUUID(),
|
|
67
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
68
|
-
source: { type: "addon", id: "cloudflare-tunnel" },
|
|
69
|
-
category: EventCategory.NetworkTunnelStopped,
|
|
70
|
-
data: {}
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
getEndpoint() {
|
|
74
|
-
return this.endpoint;
|
|
75
|
-
}
|
|
76
|
-
getStatus() {
|
|
77
|
-
return {
|
|
78
|
-
connected: this.endpoint !== null,
|
|
79
|
-
publicUrl: this.endpoint?.url
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
export {
|
|
84
|
-
CloudflareTunnelAddon,
|
|
85
|
-
CloudflareTunnelService
|
|
86
|
-
};
|
|
87
|
-
//# sourceMappingURL=index.mjs.map
|
|
1
|
+
import { CloudflareTunnelAddon, customActions as cloudflareTunnelActions, t as CloudflareTunnelService } from "./cloudflare-tunnel.addon.mjs";
|
|
2
|
+
export { CloudflareTunnelAddon, CloudflareTunnelService, cloudflareTunnelActions, cloudflareTunnelActions as customActions };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-cloudflare-tunnel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Cloudflare Tunnel addon for CamStack",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"dist"
|
|
48
48
|
],
|
|
49
49
|
"scripts": {
|
|
50
|
-
"build": "
|
|
50
|
+
"build": "vite build",
|
|
51
51
|
"dev": "tsup --watch",
|
|
52
52
|
"typecheck": "tsc --noEmit",
|
|
53
53
|
"publish": "npm publish --access public"
|
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@camstack/types": "*",
|
|
60
60
|
"tsup": "^8.0.0",
|
|
61
|
-
"typescript": "~5.9.0"
|
|
61
|
+
"typescript": "~5.9.0",
|
|
62
|
+
"vite": "^8.0.11",
|
|
63
|
+
"vite-plugin-dts": "^5.0.0"
|
|
62
64
|
}
|
|
63
65
|
}
|
package/dist/chunk-VHOC5TFB.mjs
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
// src/cloudflare-tunnel.addon.ts
|
|
2
|
-
import { BaseAddon, networkAccessCapability } from "@camstack/types";
|
|
3
|
-
var CloudflareTunnelAddon = class extends BaseAddon {
|
|
4
|
-
constructor() {
|
|
5
|
-
super({ mode: "quick", namedTunnelToken: "", localPort: 3e3 });
|
|
6
|
-
}
|
|
7
|
-
async onInitialize() {
|
|
8
|
-
this.ctx.logger.info("Cloudflare Tunnel addon initialized");
|
|
9
|
-
return [{ capability: networkAccessCapability, provider: this }];
|
|
10
|
-
}
|
|
11
|
-
globalSettingsSchema() {
|
|
12
|
-
return this.schema({
|
|
13
|
-
sections: [{
|
|
14
|
-
id: "tunnel",
|
|
15
|
-
title: "Tunnel Settings",
|
|
16
|
-
fields: [
|
|
17
|
-
this.field({
|
|
18
|
-
type: "select",
|
|
19
|
-
key: "mode",
|
|
20
|
-
label: "Tunnel Mode",
|
|
21
|
-
description: "Quick mode creates a temporary public URL; Named mode uses a persistent named tunnel.",
|
|
22
|
-
default: "quick",
|
|
23
|
-
options: [
|
|
24
|
-
{ value: "quick", label: "Quick Tunnel", description: "Temporary public URL, no Cloudflare account required" },
|
|
25
|
-
{ value: "named", label: "Named Tunnel", description: "Persistent tunnel using a Cloudflare tunnel token" }
|
|
26
|
-
]
|
|
27
|
-
}),
|
|
28
|
-
this.field({
|
|
29
|
-
type: "password",
|
|
30
|
-
key: "namedTunnelToken",
|
|
31
|
-
label: "Tunnel Token",
|
|
32
|
-
description: "Token from your Cloudflare Zero Trust dashboard (required for Named Tunnel mode)",
|
|
33
|
-
showToggle: true,
|
|
34
|
-
showWhen: { field: "mode", equals: "named" }
|
|
35
|
-
}),
|
|
36
|
-
this.field({
|
|
37
|
-
type: "number",
|
|
38
|
-
key: "localPort",
|
|
39
|
-
label: "Local Port",
|
|
40
|
-
description: "The local port that the tunnel will expose publicly",
|
|
41
|
-
min: 1,
|
|
42
|
-
max: 65535,
|
|
43
|
-
step: 1,
|
|
44
|
-
default: 3e3
|
|
45
|
-
})
|
|
46
|
-
]
|
|
47
|
-
}]
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export {
|
|
53
|
-
CloudflareTunnelAddon
|
|
54
|
-
};
|
|
55
|
-
//# sourceMappingURL=chunk-VHOC5TFB.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cloudflare-tunnel.addon.ts"],"sourcesContent":["import type { ProviderRegistration } from '@camstack/types'\nimport { BaseAddon, networkAccessCapability } from '@camstack/types'\n\ninterface TunnelConfig {\n readonly mode: 'quick' | 'named'\n readonly namedTunnelToken: string\n readonly localPort: number\n}\n\n/**\n * Cloudflare Tunnel — exposes CamStack via Cloudflare's network.\n * Settings appear under Cluster → NodeDetail → Settings.\n */\nexport class CloudflareTunnelAddon extends BaseAddon<TunnelConfig> {\n constructor() {\n super({ mode: 'quick', namedTunnelToken: '', localPort: 3000 })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.ctx.logger.info('Cloudflare Tunnel addon initialized')\n return [{ capability: networkAccessCapability, provider: this }]\n }\n\n protected globalSettingsSchema() {\n return this.schema({\n sections: [{\n id: 'tunnel',\n title: 'Tunnel Settings',\n fields: [\n this.field({\n type: 'select',\n key: 'mode',\n label: 'Tunnel Mode',\n description: 'Quick mode creates a temporary public URL; Named mode uses a persistent named tunnel.',\n default: 'quick',\n options: [\n { value: 'quick', label: 'Quick Tunnel', description: 'Temporary public URL, no Cloudflare account required' },\n { value: 'named', label: 'Named Tunnel', description: 'Persistent tunnel using a Cloudflare tunnel token' },\n ],\n }),\n this.field({\n type: 'password',\n key: 'namedTunnelToken',\n label: 'Tunnel Token',\n description: 'Token from your Cloudflare Zero Trust dashboard (required for Named Tunnel mode)',\n showToggle: true,\n showWhen: { field: 'mode', equals: 'named' },\n }),\n this.field({\n type: 'number',\n key: 'localPort',\n label: 'Local Port',\n description: 'The local port that the tunnel will expose publicly',\n min: 1, max: 65535, step: 1, default: 3000,\n }),\n ],\n }],\n })\n }\n}\n"],"mappings":";AACA,SAAS,WAAW,+BAA+B;AAY5C,IAAM,wBAAN,cAAoC,UAAwB;AAAA,EACjE,cAAc;AACZ,UAAM,EAAE,MAAM,SAAS,kBAAkB,IAAI,WAAW,IAAK,CAAC;AAAA,EAChE;AAAA,EAEA,MAAgB,eAAgD;AAC9D,SAAK,IAAI,OAAO,KAAK,qCAAqC;AAC1D,WAAO,CAAC,EAAE,YAAY,yBAAyB,UAAU,KAAK,CAAC;AAAA,EACjE;AAAA,EAEU,uBAAuB;AAC/B,WAAO,KAAK,OAAO;AAAA,MACjB,UAAU,CAAC;AAAA,QACT,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,QAAQ;AAAA,UACN,KAAK,MAAM;AAAA,YACT,MAAM;AAAA,YACN,KAAK;AAAA,YACL,OAAO;AAAA,YACP,aAAa;AAAA,YACb,SAAS;AAAA,YACT,SAAS;AAAA,cACP,EAAE,OAAO,SAAS,OAAO,gBAAgB,aAAa,uDAAuD;AAAA,cAC7G,EAAE,OAAO,SAAS,OAAO,gBAAgB,aAAa,oDAAoD;AAAA,YAC5G;AAAA,UACF,CAAC;AAAA,UACD,KAAK,MAAM;AAAA,YACT,MAAM;AAAA,YACN,KAAK;AAAA,YACL,OAAO;AAAA,YACP,aAAa;AAAA,YACb,YAAY;AAAA,YACZ,UAAU,EAAE,OAAO,QAAQ,QAAQ,QAAQ;AAAA,UAC7C,CAAC;AAAA,UACD,KAAK,MAAM;AAAA,YACT,MAAM;AAAA,YACN,KAAK;AAAA,YACL,OAAO;AAAA,YACP,aAAa;AAAA,YACb,KAAK;AAAA,YAAG,KAAK;AAAA,YAAO,MAAM;AAAA,YAAG,SAAS;AAAA,UACxC,CAAC;AAAA,QACH;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACF;","names":[]}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import * as _camstack_types from '@camstack/types';
|
|
2
|
-
import { BaseAddon, ProviderRegistration } from '@camstack/types';
|
|
3
|
-
|
|
4
|
-
interface TunnelConfig {
|
|
5
|
-
readonly mode: 'quick' | 'named';
|
|
6
|
-
readonly namedTunnelToken: string;
|
|
7
|
-
readonly localPort: number;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Cloudflare Tunnel — exposes CamStack via Cloudflare's network.
|
|
11
|
-
* Settings appear under Cluster → NodeDetail → Settings.
|
|
12
|
-
*/
|
|
13
|
-
declare class CloudflareTunnelAddon extends BaseAddon<TunnelConfig> {
|
|
14
|
-
constructor();
|
|
15
|
-
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
16
|
-
protected globalSettingsSchema(): _camstack_types.ConfigUISchema;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export { CloudflareTunnelAddon };
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import * as _camstack_types from '@camstack/types';
|
|
2
|
-
import { BaseAddon, ProviderRegistration } from '@camstack/types';
|
|
3
|
-
|
|
4
|
-
interface TunnelConfig {
|
|
5
|
-
readonly mode: 'quick' | 'named';
|
|
6
|
-
readonly namedTunnelToken: string;
|
|
7
|
-
readonly localPort: number;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Cloudflare Tunnel — exposes CamStack via Cloudflare's network.
|
|
11
|
-
* Settings appear under Cluster → NodeDetail → Settings.
|
|
12
|
-
*/
|
|
13
|
-
declare class CloudflareTunnelAddon extends BaseAddon<TunnelConfig> {
|
|
14
|
-
constructor();
|
|
15
|
-
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
16
|
-
protected globalSettingsSchema(): _camstack_types.ConfigUISchema;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export { CloudflareTunnelAddon };
|
package/dist/index.d.mts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { INetworkAccessProvider, IScopedLogger, IEventBus, IProcessManager, INetworkEndpoint, NetworkAccessStatus } from '@camstack/types';
|
|
2
|
-
export { CloudflareTunnelAddon } from './cloudflare-tunnel.addon.mjs';
|
|
3
|
-
|
|
4
|
-
interface CloudflareTunnelConfig {
|
|
5
|
-
readonly mode: 'quick' | 'named';
|
|
6
|
-
readonly namedTunnelToken?: string;
|
|
7
|
-
readonly localPort: number;
|
|
8
|
-
}
|
|
9
|
-
declare class CloudflareTunnelService implements INetworkAccessProvider {
|
|
10
|
-
private readonly config;
|
|
11
|
-
private readonly logger;
|
|
12
|
-
private readonly eventBus;
|
|
13
|
-
private readonly processManager;
|
|
14
|
-
readonly id = "cloudflare-tunnel";
|
|
15
|
-
readonly type = "cloudflare";
|
|
16
|
-
private endpoint;
|
|
17
|
-
private processId;
|
|
18
|
-
constructor(config: CloudflareTunnelConfig, logger: IScopedLogger, eventBus: IEventBus, processManager: IProcessManager);
|
|
19
|
-
start(): Promise<INetworkEndpoint>;
|
|
20
|
-
stop(): Promise<void>;
|
|
21
|
-
getEndpoint(): INetworkEndpoint | null;
|
|
22
|
-
getStatus(): NetworkAccessStatus;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export { type CloudflareTunnelConfig, CloudflareTunnelService };
|
package/dist/index.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { INetworkAccessProvider, IScopedLogger, IEventBus, IProcessManager, INetworkEndpoint, NetworkAccessStatus } from '@camstack/types';
|
|
2
|
-
export { CloudflareTunnelAddon } from './cloudflare-tunnel.addon.js';
|
|
3
|
-
|
|
4
|
-
interface CloudflareTunnelConfig {
|
|
5
|
-
readonly mode: 'quick' | 'named';
|
|
6
|
-
readonly namedTunnelToken?: string;
|
|
7
|
-
readonly localPort: number;
|
|
8
|
-
}
|
|
9
|
-
declare class CloudflareTunnelService implements INetworkAccessProvider {
|
|
10
|
-
private readonly config;
|
|
11
|
-
private readonly logger;
|
|
12
|
-
private readonly eventBus;
|
|
13
|
-
private readonly processManager;
|
|
14
|
-
readonly id = "cloudflare-tunnel";
|
|
15
|
-
readonly type = "cloudflare";
|
|
16
|
-
private endpoint;
|
|
17
|
-
private processId;
|
|
18
|
-
constructor(config: CloudflareTunnelConfig, logger: IScopedLogger, eventBus: IEventBus, processManager: IProcessManager);
|
|
19
|
-
start(): Promise<INetworkEndpoint>;
|
|
20
|
-
stop(): Promise<void>;
|
|
21
|
-
getEndpoint(): INetworkEndpoint | null;
|
|
22
|
-
getStatus(): NetworkAccessStatus;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export { type CloudflareTunnelConfig, CloudflareTunnelService };
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/cloudflare-tunnel.ts","../src/cloudflare-tunnel.addon.ts"],"sourcesContent":["export { CloudflareTunnelService } from './cloudflare-tunnel'\nexport type { CloudflareTunnelConfig } from './cloudflare-tunnel'\nexport { CloudflareTunnelAddon } from './cloudflare-tunnel.addon'\n","import { randomUUID } from 'node:crypto'\nimport { EventCategory } from '@camstack/types'\nimport type {\n IScopedLogger, IEventBus,\n INetworkAccessProvider, INetworkEndpoint, NetworkAccessStatus,\n IProcessManager, ProcessConfig,\n} from '@camstack/types'\n\nexport interface CloudflareTunnelConfig {\n readonly mode: 'quick' | 'named'\n readonly namedTunnelToken?: string\n readonly localPort: number\n}\n\nexport class CloudflareTunnelService implements INetworkAccessProvider {\n readonly id = 'cloudflare-tunnel'\n readonly type = 'cloudflare'\n\n private endpoint: INetworkEndpoint | null = null\n private processId: string | null = null\n\n constructor(\n private readonly config: CloudflareTunnelConfig,\n private readonly logger: IScopedLogger,\n private readonly eventBus: IEventBus,\n private readonly processManager: IProcessManager,\n ) {}\n\n async start(): Promise<INetworkEndpoint> {\n this.logger.info('Starting Cloudflare tunnel', { meta: { mode: this.config.mode } })\n\n const processConfig: ProcessConfig = {\n id: 'cloudflared-tunnel',\n label: 'Cloudflare Tunnel',\n command: 'cloudflared',\n args:\n this.config.mode === 'quick'\n ? ['tunnel', '--url', `http://localhost:${this.config.localPort}`]\n : ['tunnel', 'run', '--token', this.config.namedTunnelToken!],\n autoRestart: true,\n maxRestarts: 5,\n }\n\n this.processManager.register(processConfig)\n await this.processManager.start('cloudflared-tunnel')\n this.processId = 'cloudflared-tunnel'\n\n const publicUrl =\n this.config.mode === 'named'\n ? 'https://tunnel.example.com'\n : 'https://pending.trycloudflare.com'\n\n this.endpoint = {\n id: 'cloudflare-tunnel',\n type: 'tunnel',\n provider: 'cloudflare',\n url: publicUrl,\n internal: false,\n capabilities: {\n supportsWebRTC: false,\n supportsWebSocket: true,\n supportsSSE: true,\n },\n priority: 50,\n status: 'online',\n }\n\n this.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'addon', id: 'cloudflare-tunnel' },\n category: EventCategory.NetworkTunnelStarted,\n data: { url: publicUrl },\n })\n\n return this.endpoint\n }\n\n async stop(): Promise<void> {\n if (this.processId) {\n await this.processManager.stop(this.processId)\n }\n this.endpoint = null\n\n this.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'addon', id: 'cloudflare-tunnel' },\n category: EventCategory.NetworkTunnelStopped,\n data: {},\n })\n }\n\n getEndpoint(): INetworkEndpoint | null {\n return this.endpoint\n }\n\n getStatus(): NetworkAccessStatus {\n return {\n connected: this.endpoint !== null,\n publicUrl: this.endpoint?.url,\n }\n }\n}\n","import type { ProviderRegistration } from '@camstack/types'\nimport { BaseAddon, networkAccessCapability } from '@camstack/types'\n\ninterface TunnelConfig {\n readonly mode: 'quick' | 'named'\n readonly namedTunnelToken: string\n readonly localPort: number\n}\n\n/**\n * Cloudflare Tunnel — exposes CamStack via Cloudflare's network.\n * Settings appear under Cluster → NodeDetail → Settings.\n */\nexport class CloudflareTunnelAddon extends BaseAddon<TunnelConfig> {\n constructor() {\n super({ mode: 'quick', namedTunnelToken: '', localPort: 3000 })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.ctx.logger.info('Cloudflare Tunnel addon initialized')\n return [{ capability: networkAccessCapability, provider: this }]\n }\n\n protected globalSettingsSchema() {\n return this.schema({\n sections: [{\n id: 'tunnel',\n title: 'Tunnel Settings',\n fields: [\n this.field({\n type: 'select',\n key: 'mode',\n label: 'Tunnel Mode',\n description: 'Quick mode creates a temporary public URL; Named mode uses a persistent named tunnel.',\n default: 'quick',\n options: [\n { value: 'quick', label: 'Quick Tunnel', description: 'Temporary public URL, no Cloudflare account required' },\n { value: 'named', label: 'Named Tunnel', description: 'Persistent tunnel using a Cloudflare tunnel token' },\n ],\n }),\n this.field({\n type: 'password',\n key: 'namedTunnelToken',\n label: 'Tunnel Token',\n description: 'Token from your Cloudflare Zero Trust dashboard (required for Named Tunnel mode)',\n showToggle: true,\n showWhen: { field: 'mode', equals: 'named' },\n }),\n this.field({\n type: 'number',\n key: 'localPort',\n label: 'Local Port',\n description: 'The local port that the tunnel will expose publicly',\n min: 1, max: 65535, step: 1, default: 3000,\n }),\n ],\n }],\n })\n }\n}\n"],"mappings":"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","names":["import_types"]}
|