Skip to content

Commit 49017e0

Browse files
committed
chore: init
0 parents  commit 49017e0

10 files changed

+384
-0
lines changed

.env.example

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
GANDI_API_KEY=
2+
CLOUDFLARE_API_TOKEN=

.gitignore

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2+
3+
# Logs
4+
5+
logs
6+
_.log
7+
npm-debug.log_
8+
yarn-debug.log*
9+
yarn-error.log*
10+
lerna-debug.log*
11+
.pnpm-debug.log*
12+
13+
# Diagnostic reports (https://nodejs.org/api/report.html)
14+
15+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
16+
17+
# Runtime data
18+
19+
pids
20+
_.pid
21+
_.seed
22+
\*.pid.lock
23+
24+
# Directory for instrumented libs generated by jscoverage/JSCover
25+
26+
lib-cov
27+
28+
# Coverage directory used by tools like istanbul
29+
30+
coverage
31+
\*.lcov
32+
33+
# nyc test coverage
34+
35+
.nyc_output
36+
37+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
38+
39+
.grunt
40+
41+
# Bower dependency directory (https://bower.io/)
42+
43+
bower_components
44+
45+
# node-waf configuration
46+
47+
.lock-wscript
48+
49+
# Compiled binary addons (https://nodejs.org/api/addons.html)
50+
51+
build/Release
52+
53+
# Dependency directories
54+
55+
node_modules/
56+
jspm_packages/
57+
58+
# Snowpack dependency directory (https://snowpack.dev/)
59+
60+
web_modules/
61+
62+
# TypeScript cache
63+
64+
\*.tsbuildinfo
65+
66+
# Optional npm cache directory
67+
68+
.npm
69+
70+
# Optional eslint cache
71+
72+
.eslintcache
73+
74+
# Optional stylelint cache
75+
76+
.stylelintcache
77+
78+
# Microbundle cache
79+
80+
.rpt2_cache/
81+
.rts2_cache_cjs/
82+
.rts2_cache_es/
83+
.rts2_cache_umd/
84+
85+
# Optional REPL history
86+
87+
.node_repl_history
88+
89+
# Output of 'npm pack'
90+
91+
\*.tgz
92+
93+
# Yarn Integrity file
94+
95+
.yarn-integrity
96+
97+
# dotenv environment variable files
98+
99+
.env
100+
.env.development.local
101+
.env.test.local
102+
.env.production.local
103+
.env.local
104+
105+
# parcel-bundler cache (https://parceljs.org/)
106+
107+
.cache
108+
.parcel-cache
109+
110+
# Next.js build output
111+
112+
.next
113+
out
114+
115+
# Nuxt.js build / generate output
116+
117+
.nuxt
118+
dist
119+
120+
# Gatsby files
121+
122+
.cache/
123+
124+
# Comment in the public line in if your project uses Gatsby and not Next.js
125+
126+
# https://nextjs.org/blog/next-9-1#public-directory-support
127+
128+
# public
129+
130+
# vuepress build output
131+
132+
.vuepress/dist
133+
134+
# vuepress v2.x temp and cache directory
135+
136+
.temp
137+
.cache
138+
139+
# Docusaurus cache and generated files
140+
141+
.docusaurus
142+
143+
# Serverless directories
144+
145+
.serverless/
146+
147+
# FuseBox cache
148+
149+
.fusebox/
150+
151+
# DynamoDB Local files
152+
153+
.dynamodb/
154+
155+
# TernJS port file
156+
157+
.tern-port
158+
159+
# Stores VSCode versions used for testing VSCode extensions
160+
161+
.vscode-test
162+
163+
# yarn v2
164+
165+
.yarn/cache
166+
.yarn/unplugged
167+
.yarn/build-state.yml
168+
.yarn/install-state.gz
169+
.pnp.\*

README.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# domain-sync
2+
3+
> This is a tiny script for my use that migrates all my Gandi domains to use Cloudflare DNS.
4+
5+
To install dependencies, ensure you have the [latest version of bun](https://bun.sh/) installed, and then run:
6+
7+
```bash
8+
bun install
9+
```
10+
11+
Create a file called `.env` with your Gandi API token and a Cloudflare token with the following permissions:
12+
13+
- User > Memberships > Edit
14+
- Zone > Zone Settings > Edit
15+
- Zone > Zone > Edit
16+
17+
Then simply run:
18+
19+
```bash
20+
bun run index.ts
21+
```
22+
23+
This script is idempotent; it's safe to run more than once as it will skip domains that already have their nameservers updated to CF.

api/cloudflare.ts

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { $fetch } from 'ofetch'
2+
import { z } from 'zod'
3+
4+
const api = $fetch.create({
5+
baseURL: 'https://api.cloudflare.com/client/v4',
6+
headers: {
7+
Authorization: `Bearer ${Bun.env.CLOUDFLARE_API_TOKEN}`,
8+
},
9+
})
10+
11+
const zoneSchema = z.object({
12+
result: z.object({
13+
id: z.string(),
14+
name: z.string(),
15+
status: z.string(),
16+
paused: z.boolean(),
17+
type: z.string(),
18+
development_mode: z.number(),
19+
name_servers: z.array(z.string()),
20+
original_name_servers: z.array(z.string()),
21+
original_registrar: z.string().nullable(),
22+
original_dnshost: z.string().nullable(),
23+
modified_on: z.string(),
24+
created_on: z.string(),
25+
activated_on: z.string().nullable(),
26+
meta: z.object({
27+
step: z.number(),
28+
custom_certificate_quota: z.number(),
29+
page_rule_quota: z.number(),
30+
phishing_detected: z.boolean(),
31+
multiple_railguns_allowed: z.boolean(),
32+
}),
33+
owner: z.object({
34+
id: z.string().nullable(),
35+
type: z.string(),
36+
email: z.string().nullable(),
37+
}),
38+
account: z.object({
39+
id: z.string(),
40+
name: z.string(),
41+
}),
42+
tenant: z.object({
43+
id: z.string().nullable(),
44+
name: z.string().nullable(),
45+
}),
46+
tenant_unit: z.object({
47+
id: z.string().nullable(),
48+
}),
49+
permissions: z.array(z.string()),
50+
plan: z.object({
51+
id: z.string(),
52+
name: z.string(),
53+
price: z.number(),
54+
currency: z.string(),
55+
frequency: z.string(),
56+
is_subscribed: z.boolean(),
57+
can_subscribe: z.boolean(),
58+
legacy_id: z.string(),
59+
legacy_discount: z.boolean(),
60+
externally_managed: z.boolean(),
61+
}),
62+
}),
63+
success: z.boolean(),
64+
errors: z.array(z.unknown()),
65+
messages: z.array(z.unknown()),
66+
})
67+
68+
export async function createNewDomain(domain: string) {
69+
return await api('/zones', {
70+
method: 'POST',
71+
body: {
72+
name: domain,
73+
jump_start: true,
74+
type: 'full',
75+
},
76+
}).then(zone => zoneSchema.parse(zone))
77+
}

api/gandi.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { $fetch } from 'ofetch'
2+
import { z } from 'zod'
3+
4+
const api = $fetch.create({
5+
baseURL: 'https://api.gandi.net/v5/domain/domains',
6+
headers: {
7+
Authorization: `Apikey ${Bun.env.GANDI_API_KEY}`,
8+
},
9+
})
10+
11+
const domain = z.object({
12+
fqdn: z.string(),
13+
tld: z.string(),
14+
status: z.array(z.string()),
15+
dates: z.object({
16+
created_at: z.string(),
17+
registry_created_at: z.string(),
18+
registry_ends_at: z.string(),
19+
updated_at: z.string(),
20+
}),
21+
nameserver: z.object({
22+
current: z.string(),
23+
}),
24+
autorenew: z.boolean(),
25+
domain_owner: z.string(),
26+
orga_owner: z.string(),
27+
owner: z.string(),
28+
id: z.string(),
29+
tags: z.array(z.string()),
30+
href: z.string(),
31+
fqdn_unicode: z.string(),
32+
})
33+
34+
export async function listDomains() {
35+
return await api('/').then(domains => z.array(domain).parse(domains))
36+
}
37+
38+
export async function setNameservers(domain: string, nameservers: string[]) {
39+
return await api(`/${domain}/nameservers`, {
40+
method: 'PUT',
41+
body: {
42+
nameservers,
43+
},
44+
})
45+
}
46+
47+
export async function getNameservers(domain: string) {
48+
return (await api(`/${domain}/nameservers`)) as string[]
49+
}

bun.lockb

2.6 KB
Binary file not shown.

index.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { createNewDomain } from './api/cloudflare'
2+
import { getNameservers, listDomains, setNameservers } from './api/gandi'
3+
4+
const domains = await listDomains()
5+
6+
for (const domain of domains) {
7+
console.log(`--- ${domain.fqdn} ---`)
8+
9+
if (domain.nameserver.current === 'other') {
10+
const nameservers = await getNameservers(domain.fqdn)
11+
if (nameservers.some(n => n.includes('cloudflare'))) {
12+
continue
13+
}
14+
}
15+
16+
console.log('Creating new domain on Cloudflare')
17+
const response = await createNewDomain(domain.fqdn)
18+
19+
console.log('Setting new nameservers')
20+
await setNameservers(domain.fqdn, response.result.name_servers)
21+
}

package.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"dependencies": {
3+
"ofetch": "^1.1.0",
4+
"zod": "^3.21.4"
5+
},
6+
"devDependencies": {
7+
"bun-types": "^0.6.9"
8+
},
9+
"name": "domain-sync",
10+
"module": "index.ts",
11+
"type": "module",
12+
"peerDependencies": {
13+
"typescript": "^5.0.0"
14+
}
15+
}

prettier.config.cjs

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
semi: false,
3+
singleQuote: true,
4+
printWidth: 100,
5+
trailingComma: 'es5',
6+
arrowParens: 'avoid',
7+
}

tsconfig.json

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"compilerOptions": {
3+
"lib": ["ESNext"],
4+
"module": "esnext",
5+
"target": "esnext",
6+
"moduleResolution": "bundler",
7+
"moduleDetection": "force",
8+
"allowImportingTsExtensions": true,
9+
"strict": true,
10+
"downlevelIteration": true,
11+
"skipLibCheck": true,
12+
"jsx": "preserve",
13+
"allowSyntheticDefaultImports": true,
14+
"forceConsistentCasingInFileNames": true,
15+
"allowJs": true,
16+
"noEmit": true,
17+
"types": [
18+
"bun-types" // add Bun global
19+
]
20+
}
21+
}

0 commit comments

Comments
 (0)