@cardelli/ambit 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/esm/cli/commands/create/index.d.ts +2 -0
  2. package/esm/cli/commands/create/index.d.ts.map +1 -0
  3. package/esm/cli/commands/create/index.js +292 -0
  4. package/esm/cli/commands/create/machine.d.ts +33 -0
  5. package/esm/cli/commands/create/machine.d.ts.map +1 -0
  6. package/esm/cli/commands/create/machine.js +162 -0
  7. package/esm/cli/commands/deploy/index.d.ts +2 -0
  8. package/esm/cli/commands/deploy/index.d.ts.map +1 -0
  9. package/esm/cli/commands/deploy/index.js +290 -0
  10. package/esm/cli/commands/deploy/machine.d.ts +52 -0
  11. package/esm/cli/commands/deploy/machine.d.ts.map +1 -0
  12. package/esm/cli/commands/deploy/machine.js +116 -0
  13. package/esm/cli/commands/deploy/modes.d.ts +18 -0
  14. package/esm/cli/commands/deploy/modes.d.ts.map +1 -0
  15. package/esm/cli/commands/deploy/modes.js +152 -0
  16. package/esm/cli/commands/destroy/app.d.ts +2 -0
  17. package/esm/cli/commands/destroy/app.d.ts.map +1 -0
  18. package/esm/cli/commands/destroy/app.js +173 -0
  19. package/esm/cli/commands/destroy/index.d.ts +2 -0
  20. package/esm/cli/commands/destroy/index.d.ts.map +1 -0
  21. package/esm/cli/commands/destroy/index.js +63 -0
  22. package/esm/cli/commands/destroy/network.d.ts +2 -0
  23. package/esm/cli/commands/destroy/network.d.ts.map +1 -0
  24. package/esm/cli/commands/destroy/network.js +210 -0
  25. package/esm/cli/commands/doctor.d.ts.map +1 -0
  26. package/esm/cli/commands/doctor.js +295 -0
  27. package/esm/{src/cli → cli}/commands/list.d.ts.map +1 -1
  28. package/esm/{src/cli → cli}/commands/list.js +39 -54
  29. package/esm/cli/commands/status.d.ts.map +1 -0
  30. package/esm/cli/commands/status.js +331 -0
  31. package/esm/cli/mod.d.ts.map +1 -0
  32. package/esm/{src/cli → cli}/mod.js +4 -4
  33. package/esm/deno.d.ts +4 -18
  34. package/esm/deno.js +5 -19
  35. package/esm/deps/jsr.io/@std/path/1.1.4/constants.d.ts +1 -1
  36. package/esm/deps/jsr.io/@zod/zod/4.3.6/src/v4/core/json-schema-generator.d.ts +1 -1
  37. package/esm/lib/args.d.ts +11 -0
  38. package/esm/lib/args.d.ts.map +1 -0
  39. package/esm/lib/args.js +28 -0
  40. package/esm/lib/cli.d.ts +0 -2
  41. package/esm/lib/cli.d.ts.map +1 -1
  42. package/esm/lib/cli.js +41 -27
  43. package/esm/lib/command.d.ts +21 -49
  44. package/esm/lib/command.d.ts.map +1 -1
  45. package/esm/lib/command.js +55 -95
  46. package/esm/lib/machine.d.ts +11 -0
  47. package/esm/lib/machine.d.ts.map +1 -0
  48. package/esm/lib/machine.js +15 -0
  49. package/esm/lib/output.d.ts +3 -2
  50. package/esm/lib/output.d.ts.map +1 -1
  51. package/esm/lib/output.js +25 -11
  52. package/esm/lib/result.d.ts +18 -7
  53. package/esm/lib/result.d.ts.map +1 -1
  54. package/esm/lib/result.js +46 -1
  55. package/esm/main.d.ts +6 -6
  56. package/esm/main.d.ts.map +1 -1
  57. package/esm/main.js +7 -9
  58. package/esm/providers/fly.d.ts +81 -0
  59. package/esm/providers/fly.d.ts.map +1 -0
  60. package/esm/providers/fly.js +372 -0
  61. package/esm/providers/tailscale.d.ts +31 -0
  62. package/esm/providers/tailscale.d.ts.map +1 -0
  63. package/esm/providers/tailscale.js +150 -0
  64. package/esm/{src/schemas → schemas}/fly.d.ts +1 -11
  65. package/esm/schemas/fly.d.ts.map +1 -0
  66. package/esm/{src/schemas → schemas}/fly.js +14 -56
  67. package/esm/{src/schemas → schemas}/tailscale.d.ts +1 -2
  68. package/esm/schemas/tailscale.d.ts.map +1 -0
  69. package/esm/{src/schemas → schemas}/tailscale.js +2 -3
  70. package/esm/src/{docker/router → router}/Dockerfile +0 -11
  71. package/esm/src/router/start.sh +101 -0
  72. package/esm/util/constants.d.ts +13 -0
  73. package/esm/util/constants.d.ts.map +1 -0
  74. package/esm/util/constants.js +34 -0
  75. package/esm/{src → util}/credentials.d.ts +0 -1
  76. package/esm/util/credentials.d.ts.map +1 -0
  77. package/esm/{src → util}/credentials.js +3 -5
  78. package/esm/{src → util}/discovery.d.ts +20 -4
  79. package/esm/util/discovery.d.ts.map +1 -0
  80. package/esm/{src → util}/discovery.js +38 -15
  81. package/esm/util/fly-transforms.d.ts +27 -0
  82. package/esm/util/fly-transforms.d.ts.map +1 -0
  83. package/esm/util/fly-transforms.js +87 -0
  84. package/esm/{src → util}/guard.d.ts +1 -2
  85. package/esm/util/guard.d.ts.map +1 -0
  86. package/esm/{src → util}/guard.js +27 -27
  87. package/esm/util/naming.d.ts +5 -0
  88. package/esm/util/naming.d.ts.map +1 -0
  89. package/esm/util/naming.js +12 -0
  90. package/esm/{src → util}/resolve.d.ts +2 -3
  91. package/esm/util/resolve.d.ts.map +1 -0
  92. package/esm/{src → util}/resolve.js +1 -2
  93. package/esm/util/session.d.ts +16 -0
  94. package/esm/util/session.d.ts.map +1 -0
  95. package/esm/util/session.js +19 -0
  96. package/esm/util/tailscale-local.d.ts +13 -0
  97. package/esm/util/tailscale-local.d.ts.map +1 -0
  98. package/esm/util/tailscale-local.js +63 -0
  99. package/esm/{src → util}/template.d.ts +2 -4
  100. package/esm/util/template.d.ts.map +1 -0
  101. package/esm/{src → util}/template.js +14 -17
  102. package/package.json +1 -43
  103. package/esm/lib/paths.d.ts +0 -3
  104. package/esm/lib/paths.d.ts.map +0 -1
  105. package/esm/lib/paths.js +0 -5
  106. package/esm/src/cli/commands/create.d.ts +0 -2
  107. package/esm/src/cli/commands/create.d.ts.map +0 -1
  108. package/esm/src/cli/commands/create.js +0 -294
  109. package/esm/src/cli/commands/deploy.d.ts +0 -2
  110. package/esm/src/cli/commands/deploy.d.ts.map +0 -1
  111. package/esm/src/cli/commands/deploy.js +0 -426
  112. package/esm/src/cli/commands/destroy.d.ts +0 -2
  113. package/esm/src/cli/commands/destroy.d.ts.map +0 -1
  114. package/esm/src/cli/commands/destroy.js +0 -340
  115. package/esm/src/cli/commands/doctor.d.ts.map +0 -1
  116. package/esm/src/cli/commands/doctor.js +0 -141
  117. package/esm/src/cli/commands/status.d.ts.map +0 -1
  118. package/esm/src/cli/commands/status.js +0 -152
  119. package/esm/src/cli/mod.d.ts.map +0 -1
  120. package/esm/src/credentials.d.ts.map +0 -1
  121. package/esm/src/discovery.d.ts.map +0 -1
  122. package/esm/src/docker/router/start.sh +0 -146
  123. package/esm/src/guard.d.ts.map +0 -1
  124. package/esm/src/providers/fly.d.ts +0 -70
  125. package/esm/src/providers/fly.d.ts.map +0 -1
  126. package/esm/src/providers/fly.js +0 -411
  127. package/esm/src/providers/tailscale.d.ts +0 -31
  128. package/esm/src/providers/tailscale.d.ts.map +0 -1
  129. package/esm/src/providers/tailscale.js +0 -195
  130. package/esm/src/resolve.d.ts.map +0 -1
  131. package/esm/src/schemas/config.d.ts +0 -5
  132. package/esm/src/schemas/config.d.ts.map +0 -1
  133. package/esm/src/schemas/config.js +0 -22
  134. package/esm/src/schemas/fly.d.ts.map +0 -1
  135. package/esm/src/schemas/tailscale.d.ts.map +0 -1
  136. package/esm/src/template.d.ts.map +0 -1
  137. /package/esm/{src/cli → cli}/commands/doctor.d.ts +0 -0
  138. /package/esm/{src/cli → cli}/commands/list.d.ts +0 -0
  139. /package/esm/{src/cli → cli}/commands/status.d.ts +0 -0
  140. /package/esm/{src/cli → cli}/mod.d.ts +0 -0
  141. /package/esm/src/{docker/router → router}/fly.toml +0 -0
@@ -1,14 +1,13 @@
1
1
  // =============================================================================
2
2
  // Fly.io CLI Response Schemas
3
3
  // =============================================================================
4
- import "../../_dnt.polyfills.js";
5
- import { z } from "../../deps/jsr.io/@zod/zod/4.3.6/src/index.js";
4
+ import { z } from "../deps/jsr.io/@zod/zod/4.3.6/src/index.js";
6
5
  // =============================================================================
7
6
  // Auth Response
8
7
  // =============================================================================
9
8
  export const FlyAuthSchema = z.object({
10
9
  email: z.string(),
11
- }).passthrough();
10
+ }).loose();
12
11
  // =============================================================================
13
12
  // App Schemas
14
13
  // =============================================================================
@@ -18,7 +17,7 @@ export const FlyAppSchema = z.object({
18
17
  Organization: z.object({
19
18
  Slug: z.string(),
20
19
  }).optional(),
21
- }).passthrough();
20
+ }).loose();
22
21
  export const FlyAppsListSchema = z.array(FlyAppSchema);
23
22
  // =============================================================================
24
23
  // App Status
@@ -28,7 +27,7 @@ export const FlyStatusSchema = z.object({
28
27
  Name: z.string().optional(),
29
28
  Hostname: z.string().optional(),
30
29
  Deployed: z.boolean().optional(),
31
- }).passthrough();
30
+ }).loose();
32
31
  // =============================================================================
33
32
  // Machine Schemas
34
33
  // =============================================================================
@@ -36,7 +35,7 @@ export const FlyMachineGuestSchema = z.object({
36
35
  cpu_kind: z.string(),
37
36
  cpus: z.number(),
38
37
  memory_mb: z.number(),
39
- }).passthrough();
38
+ }).loose();
40
39
  export const FlyMachineConfigSchema = z.object({
41
40
  guest: FlyMachineGuestSchema.optional(),
42
41
  metadata: z.record(z.string(), z.string()).optional(),
@@ -45,11 +44,11 @@ export const FlyMachineConfigSchema = z.object({
45
44
  ports: z.array(z.object({
46
45
  port: z.number(),
47
46
  handlers: z.array(z.string()).optional(),
48
- }).passthrough()).optional(),
47
+ }).loose()).optional(),
49
48
  protocol: z.string().optional(),
50
49
  internal_port: z.number().optional(),
51
- }).passthrough()).optional(),
52
- }).passthrough();
50
+ }).loose()).optional(),
51
+ }).loose();
53
52
  export const FlyMachineSchema = z.object({
54
53
  id: z.string(),
55
54
  name: z.string(),
@@ -59,7 +58,7 @@ export const FlyMachineSchema = z.object({
59
58
  config: FlyMachineConfigSchema.optional(),
60
59
  created_at: z.string().optional(),
61
60
  updated_at: z.string().optional(),
62
- }).passthrough();
61
+ }).loose();
63
62
  export const FlyMachinesListSchema = z.array(FlyMachineSchema);
64
63
  // =============================================================================
65
64
  // Organization Schemas
@@ -71,55 +70,14 @@ export const FlyOrgsSchema = z.record(z.string(), z.string());
71
70
  export const FlyDeploySchema = z.object({
72
71
  ID: z.string().optional(),
73
72
  Status: z.string().optional(),
74
- }).passthrough();
75
- // =============================================================================
76
- // Machine State Mapping
77
- // =============================================================================
78
- /**
79
- * Map Fly machine state to internal state.
80
- * Fly states: created, starting, started, stopping, stopped, destroying, destroyed
81
- */
82
- export const mapFlyMachineState = (flyState) => {
83
- switch (flyState.toLowerCase()) {
84
- case "started":
85
- return "running";
86
- case "stopped":
87
- case "suspended":
88
- return "frozen";
89
- case "created":
90
- case "starting":
91
- return "creating";
92
- case "destroying":
93
- case "destroyed":
94
- case "failed":
95
- return "failed";
96
- default:
97
- return "creating";
98
- }
99
- };
100
- // =============================================================================
101
- // Machine Size Mapping
102
- // =============================================================================
103
- /**
104
- * Map Fly guest config to machine size enum.
105
- */
106
- export const mapFlyMachineSize = (guest) => {
107
- if (!guest)
108
- return "shared-cpu-1x";
109
- const cpus = guest.cpus;
110
- if (cpus >= 4)
111
- return "shared-cpu-4x";
112
- if (cpus >= 2)
113
- return "shared-cpu-2x";
114
- return "shared-cpu-1x";
115
- };
73
+ }).loose();
116
74
  // =============================================================================
117
75
  // IP Schemas
118
76
  // =============================================================================
119
77
  export const FlyIpNetworkSchema = z.object({
120
78
  Name: z.string(),
121
79
  Organization: z.object({ Slug: z.string() }).optional(),
122
- }).passthrough();
80
+ }).loose();
123
81
  export const FlyIpSchema = z.object({
124
82
  ID: z.string().optional(),
125
83
  Address: z.string(),
@@ -127,7 +85,7 @@ export const FlyIpSchema = z.object({
127
85
  Region: z.string().optional(),
128
86
  CreatedAt: z.string().optional(),
129
87
  Network: FlyIpNetworkSchema.optional(),
130
- }).passthrough();
88
+ }).loose();
131
89
  export const FlyIpListSchema = z.array(FlyIpSchema);
132
90
  // =============================================================================
133
91
  // REST API Schemas (Machines API - api.machines.dev)
@@ -137,8 +95,8 @@ export const FlyAppInfoSchema = z.object({
137
95
  network: z.string(),
138
96
  status: z.string(),
139
97
  organization: z.object({ slug: z.string() }).optional(),
140
- }).passthrough();
98
+ }).loose();
141
99
  export const FlyAppInfoListSchema = z.object({
142
100
  total_apps: z.number(),
143
101
  apps: z.array(FlyAppInfoSchema),
144
- }).passthrough();
102
+ }).loose();
@@ -1,5 +1,4 @@
1
- import "../../_dnt.polyfills.js";
2
- import { z } from "../../deps/jsr.io/@zod/zod/4.3.6/src/index.js";
1
+ import { z } from "../deps/jsr.io/@zod/zod/4.3.6/src/index.js";
3
2
  export declare const TailscaleAuthKeySchema: z.ZodObject<{
4
3
  key: z.ZodString;
5
4
  id: z.ZodOptional<z.ZodString>;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tailscale.d.ts","sourceRoot":"","sources":["../../src/schemas/tailscale.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,4CAA4C,CAAC;AAM/D,eAAO,MAAM,sBAAsB;;;;;iBAKjC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAMtE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;iBAcxB,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;iBAErC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAM9E,eAAO,MAAM,qBAAqB;;;iBAGhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAMpE,eAAO,MAAM,6BAA6B;;iBAExC,CAAC;AAEH,eAAO,MAAM,uBAAuB,mDAGnC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAMxE,eAAO,MAAM,oBAAoB;;iBAE/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAMlE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,eAAO,MAAM,oBAAoB,GAAI,MAAM,mBAAmB,KAAG,MA+BhE,CAAC"}
@@ -1,8 +1,7 @@
1
1
  // =============================================================================
2
2
  // Tailscale API Response Schemas
3
3
  // =============================================================================
4
- import "../../_dnt.polyfills.js";
5
- import { z } from "../../deps/jsr.io/@zod/zod/4.3.6/src/index.js";
4
+ import { z } from "../deps/jsr.io/@zod/zod/4.3.6/src/index.js";
6
5
  // =============================================================================
7
6
  // Auth Key Schemas
8
7
  // =============================================================================
@@ -29,7 +28,7 @@ export const TailscaleDeviceSchema = z.object({
29
28
  tags: z.array(z.string()).optional(),
30
29
  advertisedRoutes: z.array(z.string()).optional(),
31
30
  enabledRoutes: z.array(z.string()).optional(),
32
- }).passthrough();
31
+ }).loose();
33
32
  export const TailscaleDevicesListSchema = z.object({
34
33
  devices: z.array(TailscaleDeviceSchema),
35
34
  });
@@ -6,7 +6,6 @@ FROM alpine:3.23.3
6
6
 
7
7
  ARG TAILSCALE_VERSION=1.94.1
8
8
  ARG COREDNS_VERSION=1.14.1
9
- ARG MICROSOCKS_VERSION=1.0.5
10
9
 
11
10
  RUN apk add --no-cache \
12
11
  bash \
@@ -25,16 +24,6 @@ RUN cd /tmp \
25
24
  && mv coredns /usr/local/bin/coredns \
26
25
  && chmod +x /usr/local/bin/coredns
27
26
 
28
- RUN apk add --no-cache gcc musl-dev make \
29
- && cd /tmp \
30
- && wget -q "https://github.com/rofl0r/microsocks/archive/refs/tags/v${MICROSOCKS_VERSION}.tar.gz" \
31
- && tar xzf "v${MICROSOCKS_VERSION}.tar.gz" \
32
- && cd "microsocks-${MICROSOCKS_VERSION}" \
33
- && make \
34
- && cp microsocks /usr/local/bin/ \
35
- && cd / && rm -rf /tmp/microsocks* /tmp/v${MICROSOCKS_VERSION}.tar.gz \
36
- && apk del gcc musl-dev make
37
-
38
27
  RUN mkdir -p /var/lib/tailscale /var/run/tailscale /etc/coredns
39
28
 
40
29
  COPY start.sh /start.sh
@@ -0,0 +1,101 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # =============================================================================
5
+ # ambit - Self-Configuring Tailscale Subnet Router
6
+ # =============================================================================
7
+ # State is persisted to /var/lib/tailscale via Fly volume.
8
+ # On first run: authenticates with a pre-minted auth key, advertises routes.
9
+ # On restart: reuses existing state, no new device created.
10
+ # The router never receives the user's API token — only a single-use,
11
+ # tag-scoped auth key that expires after 5 minutes.
12
+ # =============================================================================
13
+
14
+ echo "Router: Enabling IP Forwarding"
15
+ echo 'net.ipv4.ip_forward = 1' | tee -a /etc/sysctl.conf
16
+ echo 'net.ipv6.conf.all.forwarding = 1' | tee -a /etc/sysctl.conf
17
+ sysctl -p /etc/sysctl.conf
18
+
19
+ PRIVATE_IP=$(grep fly-local-6pn /etc/hosts | awk '{print $1}')
20
+
21
+ # tailscaled's built-in SOCKS5 proxy routes through the WireGuard tunnel
22
+ # directly (no tun device needed). This handles both TCP routing and DNS
23
+ # resolution through the tailnet — MagicDNS, split DNS, everything.
24
+ echo "Router: Starting Tailscaled with SOCKS5 Proxy on [${PRIVATE_IP}]:1080"
25
+ /usr/local/bin/tailscaled \
26
+ --state=/var/lib/tailscale/tailscaled.state \
27
+ --socket=/var/run/tailscale/tailscaled.sock \
28
+ --socks5-server=[${PRIVATE_IP}]:1080 &
29
+
30
+ # Wait for tailscaled to be ready
31
+ sleep 3
32
+
33
+ echo "Router: Extracting Fly.io Subnet"
34
+ SUBNET=$(grep fly-local-6pn /etc/hosts | awk '{print $1}' | cut -d: -f1-3)::/48
35
+ echo "Router: Subnet ${SUBNET}"
36
+
37
+ if /usr/local/bin/tailscale status --json 2>/dev/null | jq -e '.BackendState == "Running"' > /dev/null 2>&1; then
38
+ echo "Router: Already Authenticated (Using Persisted State)"
39
+
40
+ /usr/local/bin/tailscale up \
41
+ --hostname="${FLY_APP_NAME:-ambit}" \
42
+ --advertise-routes="${SUBNET}"
43
+ else
44
+ # First run - authenticate with pre-minted auth key
45
+ if [ -z "${TAILSCALE_AUTHKEY}" ]; then
46
+ echo "Router: ERROR - No TAILSCALE_AUTHKEY Provided"
47
+ exit 1
48
+ fi
49
+
50
+ echo "Router: Authenticating to Tailscale"
51
+ /usr/local/bin/tailscale up \
52
+ --authkey="${TAILSCALE_AUTHKEY}" \
53
+ --hostname="${FLY_APP_NAME:-ambit}" \
54
+ --advertise-routes="${SUBNET}"
55
+ fi
56
+
57
+ echo "Router: Fully Configured"
58
+
59
+ # Get Tailscale IPv4 for CoreDNS bind — split DNS queries arrive here
60
+ TAILSCALE_IP=$(/usr/local/bin/tailscale ip -4)
61
+ echo "Router: Tailscale IP ${TAILSCALE_IP}"
62
+
63
+ echo "Router: Starting DNS Proxy"
64
+
65
+ # Generate Corefile for CoreDNS
66
+ # Binds to the Fly private IP and the Tailscale IP. Split DNS queries from
67
+ # tailnet clients arrive on the Tailscale IP. Does NOT bind to all interfaces,
68
+ # so tailscaled's MagicDNS resolver on 100.100.100.100:53 remains unblocked.
69
+ #
70
+ # Rewrites NETWORK_NAME TLD to .flycast before forwarding to Fly DNS.
71
+ # When ROUTER_ID is set, workload app names are suffixed: app.network ->
72
+ # app-ROUTER_ID.flycast. This ties workloads to their router and avoids
73
+ # name collisions across networks.
74
+ if [ -n "${NETWORK_NAME}" ] && [ -n "${ROUTER_ID}" ]; then
75
+ echo "Router: DNS Rewrite *.${NETWORK_NAME} -> *-${ROUTER_ID}.flycast"
76
+ cat > /etc/coredns/Corefile <<EOF
77
+ .:53 {
78
+ bind ${PRIVATE_IP} ${TAILSCALE_IP}
79
+ rewrite name regex (.+)\.${NETWORK_NAME}\. {1}-${ROUTER_ID}.flycast. answer auto
80
+ forward . fdaa::3
81
+ }
82
+ EOF
83
+ elif [ -n "${NETWORK_NAME}" ]; then
84
+ echo "Router: DNS Rewrite ${NETWORK_NAME} -> flycast"
85
+ cat > /etc/coredns/Corefile <<EOF
86
+ .:53 {
87
+ bind ${PRIVATE_IP} ${TAILSCALE_IP}
88
+ rewrite name suffix .${NETWORK_NAME}. .flycast. answer auto
89
+ forward . fdaa::3
90
+ }
91
+ EOF
92
+ else
93
+ cat > /etc/coredns/Corefile <<EOF
94
+ .:53 {
95
+ bind ${PRIVATE_IP} ${TAILSCALE_IP}
96
+ forward . fdaa::3
97
+ }
98
+ EOF
99
+ fi
100
+
101
+ exec /usr/local/bin/coredns -conf /etc/coredns/Corefile
@@ -0,0 +1,13 @@
1
+ export declare const ROUTER_APP_PREFIX = "ambit-";
2
+ export declare const DEFAULT_FLY_NETWORK = "default";
3
+ export declare const ROUTER_DOCKER_DIR: string;
4
+ export declare const SOCKS_PROXY_PORT = 1080;
5
+ export declare const FLY_PRIVATE_SUBNET = "fdaa::/16";
6
+ export declare const TAILSCALE_API_KEY_PREFIX = "tskey-api-";
7
+ export declare const ENV_TAILSCALE_API_KEY = "TAILSCALE_API_KEY";
8
+ export declare const FLYCTL_INSTALL_URL = "https://fly.io/docs/flyctl/install/";
9
+ export declare const SECRET_TAILSCALE_AUTHKEY = "TAILSCALE_AUTHKEY";
10
+ export declare const SECRET_NETWORK_NAME = "NETWORK_NAME";
11
+ export declare const SECRET_ROUTER_ID = "ROUTER_ID";
12
+ export declare const SECRET_AMBIT_OUTBOUND_PROXY = "AMBIT_OUTBOUND_PROXY";
13
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/util/constants.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAE1C,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C,eAAO,MAAM,iBAAiB,QACnB,CAAC;AAMZ,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAM9C,eAAO,MAAM,wBAAwB,eAAe,CAAC;AAErD,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAMzD,eAAO,MAAM,kBAAkB,wCAAwC,CAAC;AAMxE,eAAO,MAAM,wBAAwB,sBAAsB,CAAC;AAC5D,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAClD,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAM5C,eAAO,MAAM,2BAA2B,yBAAyB,CAAC"}
@@ -0,0 +1,34 @@
1
+ // =============================================================================
2
+ // Constants - Shared Magic Values
3
+ // =============================================================================
4
+ // =============================================================================
5
+ // Router / App Identity
6
+ // =============================================================================
7
+ export const ROUTER_APP_PREFIX = "ambit-";
8
+ export const DEFAULT_FLY_NETWORK = "default";
9
+ export const ROUTER_DOCKER_DIR = new URL("../router", globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url)
10
+ .pathname;
11
+ // =============================================================================
12
+ // Networking
13
+ // =============================================================================
14
+ export const SOCKS_PROXY_PORT = 1080;
15
+ export const FLY_PRIVATE_SUBNET = "fdaa::/16";
16
+ // =============================================================================
17
+ // Tailscale
18
+ // =============================================================================
19
+ export const TAILSCALE_API_KEY_PREFIX = "tskey-api-";
20
+ export const ENV_TAILSCALE_API_KEY = "TAILSCALE_API_KEY";
21
+ // =============================================================================
22
+ // External URLs
23
+ // =============================================================================
24
+ export const FLYCTL_INSTALL_URL = "https://fly.io/docs/flyctl/install/";
25
+ // =============================================================================
26
+ // Fly.io Secret Names (set on router machines)
27
+ // =============================================================================
28
+ export const SECRET_TAILSCALE_AUTHKEY = "TAILSCALE_AUTHKEY";
29
+ export const SECRET_NETWORK_NAME = "NETWORK_NAME";
30
+ export const SECRET_ROUTER_ID = "ROUTER_ID";
31
+ // =============================================================================
32
+ // Fly.io Secret Names (set on workload machines)
33
+ // =============================================================================
34
+ export const SECRET_AMBIT_OUTBOUND_PROXY = "AMBIT_OUTBOUND_PROXY";
@@ -1,4 +1,3 @@
1
- import "../_dnt.polyfills.js";
2
1
  export interface CredentialStore {
3
2
  getTailscaleApiKey(): Promise<string | null>;
4
3
  setTailscaleApiKey(key: string): Promise<void>;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/util/credentials.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,eAAe;IAC9B,kBAAkB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAQD,eAAO,MAAM,2BAA2B,QAAO,eA0B9C,CAAC;AAMF,eAAO,MAAM,kBAAkB,QAAO,eAerC,CAAC;AAMF;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAC5B,KAAK;IAAE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAAC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAA;CAAE,KAC1D,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE,CAyBlC,CAAC"}
@@ -1,11 +1,10 @@
1
1
  // =============================================================================
2
2
  // Credential Store - Persistent Tailscale API Key Storage
3
3
  // =============================================================================
4
- import "../_dnt.polyfills.js";
5
4
  import * as dntShim from "../_dnt.shims.js";
6
5
  import { z } from "../deps/jsr.io/@zod/zod/4.3.6/src/index.js";
7
- import { commandExists, ensureConfigDir, fileExists } from "../lib/cli.js";
8
- import { getConfigDir } from "./schemas/config.js";
6
+ import { commandExists, ensureConfigDir, fileExists, getConfigDir } from "../lib/cli.js";
7
+ import { ENV_TAILSCALE_API_KEY } from "./constants.js";
9
8
  // =============================================================================
10
9
  // Schema
11
10
  // =============================================================================
@@ -46,8 +45,7 @@ export const getCredentialStore = () => {
46
45
  const fileStore = createConfigCredentialStore();
47
46
  return {
48
47
  async getTailscaleApiKey() {
49
- // Environment variable takes priority
50
- const envKey = dntShim.Deno.env.get("TAILSCALE_API_KEY");
48
+ const envKey = dntShim.Deno.env.get(ENV_TAILSCALE_API_KEY);
51
49
  if (envKey)
52
50
  return envKey;
53
51
  return await fileStore.getTailscaleApiKey();
@@ -1,11 +1,11 @@
1
- import "../_dnt.polyfills.js";
2
- import type { FlyProvider } from "./providers/fly.js";
3
- import type { TailscaleProvider } from "./providers/tailscale.js";
1
+ import type { FlyProvider } from "../providers/fly.js";
2
+ import type { TailscaleProvider } from "../providers/tailscale.js";
4
3
  /** A router app discovered from the Fly REST API. */
5
4
  export interface RouterApp {
6
5
  appName: string;
7
6
  network: string;
8
7
  org: string;
8
+ routerId: string;
9
9
  }
10
10
  /** Machine state for a router, from the Fly Machines API. */
11
11
  export interface RouterMachineInfo {
@@ -33,10 +33,26 @@ export interface WorkloadApp {
33
33
  }
34
34
  /** List all non-router apps on a specific custom network in an org. */
35
35
  export declare const listWorkloadAppsOnNetwork: (fly: FlyProvider, org: string, network: string) => Promise<WorkloadApp[]>;
36
- /** Find a specific workload app by name, optionally verifying network. */
36
+ /** Find a specific workload app by logical name, optionally scoped to a network.
37
+ * When a network is provided, resolves the router-suffixed Fly app name
38
+ * (e.g. "thing" on network "lab" with routerId "abc123" → "thing-abc123"). */
37
39
  export declare const findWorkloadApp: (fly: FlyProvider, org: string, appName: string, network?: string) => Promise<WorkloadApp | null>;
38
40
  /** Get machine info for a router app. Returns null if no machines exist. */
39
41
  export declare const getRouterMachineInfo: (fly: FlyProvider, appName: string) => Promise<RouterMachineInfo | null>;
40
42
  /** Get tailscale device info for a router. Returns null if not found. */
41
43
  export declare const getRouterTailscaleInfo: (tailscale: TailscaleProvider, appName: string) => Promise<RouterTailscaleInfo | null>;
44
+ /** A router app with its machine and Tailscale state hydrated. */
45
+ export type RouterWithInfo = RouterApp & {
46
+ machine: RouterMachineInfo | null;
47
+ tailscale: RouterTailscaleInfo | null;
48
+ };
49
+ /**
50
+ * Discover all routers in an org and hydrate each with machine + Tailscale state.
51
+ * Shows a spinner while fetching. Fetches all routers in parallel.
52
+ */
53
+ export declare const discoverRouters: (out: {
54
+ spinner(msg: string): {
55
+ success(msg: string): void;
56
+ };
57
+ }, fly: FlyProvider, tailscale: TailscaleProvider, org: string) => Promise<RouterWithInfo[]>;
42
58
  //# sourceMappingURL=discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/util/discovery.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAWnE,qDAAqD;AACrD,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,2CAA2C;AAC3C,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAMD,wDAAwD;AACxD,eAAO,MAAM,cAAc,GACzB,KAAK,WAAW,EAChB,KAAK,MAAM,KACV,OAAO,CAAC,SAAS,EAAE,CAerB,CAAC;AAEF,kDAAkD;AAClD,eAAO,MAAM,aAAa,GACxB,KAAK,WAAW,EAChB,KAAK,MAAM,EACX,SAAS,MAAM,KACd,OAAO,CAAC,SAAS,GAAG,IAAI,CAG1B,CAAC;AAMF,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,uEAAuE;AACvE,eAAO,MAAM,yBAAyB,GACpC,KAAK,WAAW,EAChB,KAAK,MAAM,EACX,SAAS,MAAM,KACd,OAAO,CAAC,WAAW,EAAE,CAcvB,CAAC;AAEF;;+EAE+E;AAC/E,eAAO,MAAM,eAAe,GAC1B,KAAK,WAAW,EAChB,KAAK,MAAM,EACX,SAAS,MAAM,EACf,UAAU,MAAM,KACf,OAAO,CAAC,WAAW,GAAG,IAAI,CA4B5B,CAAC;AAMF,4EAA4E;AAC5E,eAAO,MAAM,oBAAoB,GAC/B,KAAK,WAAW,EAChB,SAAS,MAAM,KACd,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAclC,CAAC;AAMF,yEAAyE;AACzE,eAAO,MAAM,sBAAsB,GACjC,WAAW,iBAAiB,EAC5B,SAAS,MAAM,KACd,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAcpC,CAAC;AAMF,kEAAkE;AAClE,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG;IACvC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAClC,SAAS,EAAE,mBAAmB,GAAG,IAAI,CAAC;CACvC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,KAAK;IAAE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CAAE,EAC7D,KAAK,WAAW,EAChB,WAAW,iBAAiB,EAC5B,KAAK,MAAM,KACV,OAAO,CAAC,cAAc,EAAE,CAa1B,CAAC"}
@@ -12,26 +12,23 @@
12
12
  // Commands compose them into whatever view they need.
13
13
  //
14
14
  // =============================================================================
15
- import "../_dnt.polyfills.js";
16
- import { extractSubnet } from "./schemas/config.js";
17
- // =============================================================================
18
- // Constants
19
- // =============================================================================
20
- const ROUTER_APP_PREFIX = "ambit-";
21
- const DEFAULT_NETWORK = "default";
15
+ import { getRouterSuffix, getWorkloadAppName } from "./naming.js";
16
+ import { extractSubnet } from "./fly-transforms.js";
17
+ import { DEFAULT_FLY_NETWORK, ROUTER_APP_PREFIX, } from "./constants.js";
22
18
  // =============================================================================
23
19
  // 1. Which routers exist? (Fly REST API)
24
20
  // =============================================================================
25
21
  /** List all ambit apps on custom networks in an org. */
26
22
  export const listRouterApps = async (fly, org) => {
27
- const apps = await fly.listAppsWithNetwork(org);
23
+ const apps = await fly.apps.listWithNetwork(org);
28
24
  return apps
29
25
  .filter((app) => app.name.startsWith(ROUTER_APP_PREFIX) &&
30
- app.network !== DEFAULT_NETWORK)
26
+ app.network !== DEFAULT_FLY_NETWORK)
31
27
  .map((app) => ({
32
28
  appName: app.name,
33
29
  network: app.network,
34
30
  org: app.organization?.slug ?? org,
31
+ routerId: getRouterSuffix(app.name, app.network),
35
32
  }));
36
33
  };
37
34
  /** Find the router app for a specific network. */
@@ -41,7 +38,7 @@ export const findRouterApp = async (fly, org, network) => {
41
38
  };
42
39
  /** List all non-router apps on a specific custom network in an org. */
43
40
  export const listWorkloadAppsOnNetwork = async (fly, org, network) => {
44
- const apps = await fly.listAppsWithNetwork(org);
41
+ const apps = await fly.apps.listWithNetwork(org);
45
42
  return apps
46
43
  .filter((app) => !app.name.startsWith(ROUTER_APP_PREFIX) &&
47
44
  app.network === network)
@@ -51,20 +48,32 @@ export const listWorkloadAppsOnNetwork = async (fly, org, network) => {
51
48
  org: app.organization?.slug ?? org,
52
49
  }));
53
50
  };
54
- /** Find a specific workload app by name, optionally verifying network. */
51
+ /** Find a specific workload app by logical name, optionally scoped to a network.
52
+ * When a network is provided, resolves the router-suffixed Fly app name
53
+ * (e.g. "thing" on network "lab" with routerId "abc123" → "thing-abc123"). */
55
54
  export const findWorkloadApp = async (fly, org, appName, network) => {
56
- const apps = await fly.listAppsWithNetwork(org);
55
+ const apps = await fly.apps.listWithNetwork(org);
57
56
  const workloads = apps
58
57
  .filter((app) => !app.name.startsWith(ROUTER_APP_PREFIX) &&
59
- app.network !== DEFAULT_NETWORK)
58
+ app.network !== DEFAULT_FLY_NETWORK)
60
59
  .map((app) => ({
61
60
  appName: app.name,
62
61
  network: app.network,
63
62
  org: app.organization?.slug ?? org,
64
63
  }));
65
64
  if (network) {
65
+ // Resolve the router's suffix so we can match the suffixed Fly app name
66
+ const router = await findRouterApp(fly, org, network);
67
+ if (router) {
68
+ const suffixedName = getWorkloadAppName(appName, router.routerId);
69
+ const found = workloads.find((a) => a.appName === suffixedName && a.network === network);
70
+ if (found)
71
+ return found;
72
+ }
73
+ // Fallback: try exact match (for pre-suffix apps or direct Fly name)
66
74
  return workloads.find((a) => a.appName === appName && a.network === network) ?? null;
67
75
  }
76
+ // Without network, try exact match only
68
77
  return workloads.find((a) => a.appName === appName) ?? null;
69
78
  };
70
79
  // =============================================================================
@@ -72,7 +81,7 @@ export const findWorkloadApp = async (fly, org, appName, network) => {
72
81
  // =============================================================================
73
82
  /** Get machine info for a router app. Returns null if no machines exist. */
74
83
  export const getRouterMachineInfo = async (fly, appName) => {
75
- const machines = await fly.listMachines(appName);
84
+ const machines = await fly.machines.list(appName);
76
85
  const machine = machines[0];
77
86
  if (!machine)
78
87
  return null;
@@ -91,7 +100,7 @@ export const getRouterMachineInfo = async (fly, appName) => {
91
100
  /** Get tailscale device info for a router. Returns null if not found. */
92
101
  export const getRouterTailscaleInfo = async (tailscale, appName) => {
93
102
  try {
94
- const device = await tailscale.getDeviceByHostname(appName);
103
+ const device = await tailscale.devices.getByHostname(appName);
95
104
  if (!device)
96
105
  return null;
97
106
  return {
@@ -105,3 +114,17 @@ export const getRouterTailscaleInfo = async (tailscale, appName) => {
105
114
  return null;
106
115
  }
107
116
  };
117
+ /**
118
+ * Discover all routers in an org and hydrate each with machine + Tailscale state.
119
+ * Shows a spinner while fetching. Fetches all routers in parallel.
120
+ */
121
+ export const discoverRouters = async (out, fly, tailscale, org) => {
122
+ const spinner = out.spinner("Discovering Routers");
123
+ const routerApps = await listRouterApps(fly, org);
124
+ spinner.success(`Found ${routerApps.length} Router${routerApps.length !== 1 ? "s" : ""}`);
125
+ return Promise.all(routerApps.map(async (app) => ({
126
+ ...app,
127
+ machine: await getRouterMachineInfo(fly, app.appName),
128
+ tailscale: await getRouterTailscaleInfo(tailscale, app.appName),
129
+ })));
130
+ };
@@ -0,0 +1,27 @@
1
+ import type { FlyMachine } from "../schemas/fly.js";
2
+ import { type FlyMachineGuestSchema } from "../schemas/fly.js";
3
+ import type { z } from "../deps/jsr.io/@zod/zod/4.3.6/src/index.js";
4
+ /**
5
+ * Map Fly machine state to internal state.
6
+ * Fly states: created, starting, started, stopping, stopped, destroying, destroyed
7
+ */
8
+ export declare const mapFlyMachineState: (flyState: string) => "creating" | "running" | "frozen" | "failed";
9
+ /**
10
+ * Map Fly guest config to machine size enum.
11
+ */
12
+ export declare const mapFlyMachineSize: (guest?: z.infer<typeof FlyMachineGuestSchema>) => "shared-cpu-1x" | "shared-cpu-2x" | "shared-cpu-4x";
13
+ import type { MachineResult } from "../providers/fly.js";
14
+ /** Map raw Fly machines to internal MachineResult format. */
15
+ export declare const mapMachines: (raw: FlyMachine[]) => MachineResult[];
16
+ import type { MachineSize } from "../providers/fly.js";
17
+ export declare const getSizeConfig: (size: MachineSize) => {
18
+ cpus: number;
19
+ memoryMb: number;
20
+ };
21
+ /**
22
+ * Pull the last non-empty, non-decoration line from fly stderr.
23
+ * Fly often prints progress lines then the actual error at the end.
24
+ */
25
+ export declare const extractErrorDetail: (stderr: string) => string;
26
+ export declare const extractSubnet: (privateIp: string) => string;
27
+ //# sourceMappingURL=fly-transforms.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fly-transforms.d.ts","sourceRoot":"","sources":["../../src/util/fly-transforms.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,KAAK,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,4CAA4C,CAAC;AAMpE;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAC7B,UAAU,MAAM,KACf,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,QAiBtC,CAAC;AAMF;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,KAC5C,eAAe,GAAG,eAAe,GAAG,eAOtC,CAAC;AAMF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,6DAA6D;AAC7D,eAAO,MAAM,WAAW,GAAI,KAAK,UAAU,EAAE,KAAG,aAAa,EAQ5D,CAAC;AAMF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,eAAO,MAAM,aAAa,GACxB,MAAM,WAAW,KAChB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CASlC,CAAC;AAMF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,KAAG,MAOnD,CAAC;AAMF,eAAO,MAAM,aAAa,GAAI,WAAW,MAAM,KAAG,MAKjD,CAAC"}