From 073f648f46d362178d1187e3592bed37b23c6f64 Mon Sep 17 00:00:00 2001 From: Jacob Gad Date: Thu, 16 Mar 2023 16:33:07 +1100 Subject: [PATCH] feat: :sparkles: fix update logic and refactor to use axios --- package-lock.json | 152 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + src/index.ts | 75 +++++++++-------------- src/schemas.ts | 41 +++++++++---- src/utils.ts | 8 +++ 5 files changed, 219 insertions(+), 58 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1c21586..89b5a99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "vercel-ddns", "version": "1.3.0", "dependencies": { + "axios": "^1.3.4", "dotenv": "^16.0.3", "zod": "^3.21.4" }, @@ -426,6 +427,21 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", + "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -497,6 +513,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -540,6 +567,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -876,6 +911,38 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1161,6 +1228,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1331,6 +1417,11 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -1897,6 +1988,21 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", + "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1953,6 +2059,14 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1985,6 +2099,11 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2248,6 +2367,21 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2460,6 +2594,19 @@ "picomatch": "^2.3.1" } }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2579,6 +2726,11 @@ "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", "dev": true }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", diff --git a/package.json b/package.json index 13cc0fb..bf0f242 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "typescript": "^4.9.5" }, "dependencies": { + "axios": "^1.3.4", "dotenv": "^16.0.3", "zod": "^3.21.4" } diff --git a/src/index.ts b/src/index.ts index e3a707b..d6cf46a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import type { Record } from './schemas'; +import { updatedDNSRecordSchema, createdDNSRecord } from './schemas'; import { recordSchema } from './schemas'; -import { log, retry } from './utils'; +import { log, vercelAxios } from './utils'; import env from './env'; import { z } from 'zod'; @@ -14,16 +15,8 @@ async function getPublicIp() { } async function getDNSRecords() { - const res = await fetch(`https://api.vercel.com/v4/domains/${env.domain}/records`, { - headers: { - Authorization: `Bearer ${env.vercelApiKey}`, - }, - method: 'get', - }); - if (!res.ok) throw new Error(res.statusText); - - const data = await res.json(); - const records = z.array(recordSchema).parse(data.records); + const res = await vercelAxios.get(`v4/domains/${env.domain}/records`); + const records = z.array(recordSchema).parse(res.data.records); log({ status: 'SUCCESS', @@ -33,59 +26,47 @@ async function getDNSRecords() { .map((r) => r.name) .join(', '), }); - log({ status: 'SUCCESS', function: 'getDNSRecords', record: data.name, data }); return records; } async function updateDNSRecord(record: Record, publicIp: string) { - const res = await fetch(`https://api.vercel.com/v1/domains/records/${record.id}`, { - body: JSON.stringify({ - name: record.name, - type: 'A', - value: publicIp, - ttl: 60, - }), - headers: { - Authorization: `Bearer ${env.vercelApiKey}`, - 'Content-Type': 'application/json', - }, - method: 'patch', + const res = await vercelAxios.patch(`v1/domains/records/${record.id}`, { + name: record.name, + type: 'A', + value: publicIp, + ttl: 60, }); - if (!res.ok) throw new Error(res.statusText); - const data = recordSchema.parse(await res.json()); - log({ status: 'SUCCESS', function: 'updateDNSRecord', record: data.name, data }); + const newRecord = updatedDNSRecordSchema.parse(res.data); + log({ + status: 'SUCCESS', + function: 'updateDNSRecord', + record: newRecord.name, + data: newRecord, + }); } async function createDNSRecord(name: string, value: string) { - const res = await fetch(`https://api.vercel.com/v2/domains/${env.domain}/records`, { - body: JSON.stringify({ - name, - type: 'A', - value, - ttl: 60, - }), - headers: { - Authorization: `Bearer ${env.vercelApiKey}`, - 'Content-Type': 'application/json', - }, - method: 'post', + const res = await vercelAxios.post(`v2/domains/${env.domain}/records`, { + name: name, + type: 'A', + value: value, + ttl: 60, }); - if (!res.ok) throw new Error(res.statusText); - const data = await res.json(); - log({ status: 'SUCCESS', function: 'createDNSRecord', data }); - return data; + const record = createdDNSRecord.parse(res.data); + log({ status: 'SUCCESS', function: 'createDNSRecord', data: record }); + return record; } async function main() { - const publicIp = await retry(getPublicIp); - const records = await retry(getDNSRecords); + const publicIp = await getPublicIp(); + const records = await getDNSRecords(); env.subdomains.forEach(async (subdomain) => { const record = records?.find((record) => record.name === subdomain); - if (!record) await retry(() => createDNSRecord(subdomain, publicIp)); - if (record && record.value !== publicIp) await retry(() => updateDNSRecord(record, publicIp)); + if (!record) await createDNSRecord(subdomain, publicIp); + if (record && record.value !== publicIp) await updateDNSRecord(record, publicIp); }); } diff --git a/src/schemas.ts b/src/schemas.ts index b1696c0..19f6c41 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -3,6 +3,18 @@ import { z } from 'zod'; export type ConsoleError = z.infer; export type Record = z.infer; +const dnsRecordType = z.union([ + z.literal('A'), + z.literal('AAAA'), + z.literal('ALIAS'), + z.literal('CAA'), + z.literal('CNAME'), + z.literal('MX'), + z.literal('SRV'), + z.literal('TXT'), + z.literal('NS'), +]); + export const consoleErrorSchema = z.object({ status: z.union([z.literal('SUCCESS'), z.literal('ERROR')]), function: z.string(), @@ -15,17 +27,7 @@ export const recordSchema = z.object({ id: z.string(), slug: z.string(), name: z.string(), - type: z.union([ - z.literal('A'), - z.literal('AAAA'), - z.literal('ALIAS'), - z.literal('CAA'), - z.literal('CNAME'), - z.literal('MX'), - z.literal('SRV'), - z.literal('TXT'), - z.literal('NS'), - ]), + type: dnsRecordType, value: z.string(), mxPriority: z.number().optional(), priority: z.number().optional(), @@ -36,3 +38,20 @@ export const recordSchema = z.object({ updatedAt: z.number().nullable(), ttl: z.number().min(60).max(2147483647).optional(), }); + +export const updatedDNSRecordSchema = z.object({ + createdAt: z.number().nullable().optional(), + creator: z.string(), + domain: z.string(), + id: z.string(), + name: z.string(), + recordType: dnsRecordType, + ttl: z.number().optional(), + type: z.union([z.literal('record'), z.literal('record-sys')]), + value: z.string(), +}); + +export const createdDNSRecord = z.object({ + uid: z.string(), + updated: z.number().optional(), +}); diff --git a/src/utils.ts b/src/utils.ts index 3329294..2d7b8f5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,13 @@ /* eslint-disable no-console */ +import axios from 'axios'; import type { ConsoleError } from './schemas'; +import env from './env'; + +export const vercelAxios = axios.create({ + baseURL: 'https://api.vercel.com/', + timeout: 10000, + headers: { Authorization: `Bearer ${env.vercelApiKey}` }, +}); export function log(props: ConsoleError) { if (props.status === 'SUCCESS') {