Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,13 @@ on:
env:
CI: true

permissions:
contents: read

jobs:
lint:
uses: NicTool/.github/.github/workflows/lint.yml@main
permissions:
contents: read

coverage:
uses: NicTool/.github/.github/workflows/coverage.yml@main
secrets: inherit
permissions:
contents: read

test:
uses: NicTool/.github/.github/workflows/test.yml@main
secrets: inherit
permissions:
contents: read
15 changes: 15 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Coverage

on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches: [main]

permissions:
contents: read

jobs:
coverage:
uses: NicTool/.github/.github/workflows/coverage.yml@main
secrets: inherit
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[submodule ".release"]
path = .release
url = git@github.com:msimerson/.release.git
url = https://github.com/msimerson/.release.git
2 changes: 1 addition & 1 deletion .release
Submodule .release updated 3 files
+0 −12 js/bots.txt
+20 −24 js/contributors.cjs
+45 −9 start.sh
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
### [0.8.10] - 2026-04-13

- delegation: add validation schemas for GET, POST, PUT, DELETE (#22)
- feat: add nameserver type native
- zone_rec: add pagination options
- deps: bump versions

### [0.8.9] - 2026-03-29
Expand Down Expand Up @@ -118,7 +120,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
[0.5.0]: https://github.com/NicTool/validate/releases/tag/0.5.0
[0.6.0]: https://github.com/NicTool/validate/releases/tag/0.6.0
[0.6.1]: https://github.com/NicTool/validate/releases/tag/0.6.1
[0.6.3]: https://github.com/NicTool/validate/releases/tag/0.6.3
[0.6.3]: https://github.com/NicTool/validate/releases/tag/v0.6.3
[0.7.0]: https://github.com/NicTool/validate/releases/tag/0.7.0
[0.7.1]: https://github.com/NicTool/validate/releases/tag/v0.7.1
[0.7.2]: https://github.com/NicTool/validate/releases/tag/v0.7.2
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

This handcrafted artisanal software is brought to you by:

| <img height="80" src="https://avatars.githubusercontent.com/u/261635?v=4"><br><a href="https://github.com/msimerson">msimerson</a> (<a href="https://github.com/NicTool/validate/commits?author=msimerson">29</a>)|
| :---: |
| <img height="80" src="https://avatars.githubusercontent.com/u/261635?v=4"><br><a href="https://github.com/msimerson">msimerson</a> (<a href="https://github.com/NicTool/validate/commits?author=msimerson">30</a>) | <img height="80" src="https://avatars.githubusercontent.com/u/258583966?v=4"><br><a href="https://github.com/burning-bush-dev">burning-bush-dev</a> (<a href="https://github.com/NicTool/validate/commits?author=burning-bush-dev">1</a>) |
| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |

<sub>this file is generated by [.release](https://github.com/msimerson/.release).
Contribute to this project to get your GitHub profile included here.</sub>
51 changes: 51 additions & 0 deletions lib/nameserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,51 @@ export const type = Joi.string().valid(
'maradns',
'powerdns',
'dynect',
'native',
)

export const remote_login = Joi.string().empty('').max(127)

// --- Runtime configuration: listen sockets, publisher, transport, dnssec ---

export const listen = Joi.array().items(
Joi.object({
address: Joi.alternatives(shared.ipv4, shared.ipv6).required(),
port: shared.uint16.min(1).required(),
proto: Joi.string().valid('udp', 'tcp').default('udp'),
}),
)

export const publisher = Joi.object({
type: Joi.string().valid('memory', 'rfc1035', 'tinydns-cdb', 'powerdns-db').required(),
path: Joi.string().empty('').max(1024),
database: Joi.string().empty('').max(255),
}).unknown(true)

export const transport = Joi.object({
type: Joi.string().valid('noop', 'axfr', 'rsync', 'db-replication').required(),
interval: shared.uint32.default(300),
cooldown: shared.uint16.default(5),
master: Joi.string().empty('').max(255),
tsig_key: Joi.string().empty('').max(255),
remote: Joi.string().empty('').max(255),
ssh_key: Joi.string().empty('').max(1024),
}).unknown(true)

export const dnssec = Joi.object({
enabled: Joi.boolean().default(false),
algorithm: Joi.string().valid(
'RSASHA256',
'RSASHA512',
'ECDSAP256SHA256',
'ECDSAP384SHA384',
'ED25519',
'ED448',
),
keyset: Joi.string().empty('').max(1024),
nsec3: Joi.boolean().default(false),
})

export const v3 = Joi.object({
id: id,
gid: shared.uint32.required(),
Expand All @@ -33,6 +74,11 @@ export const v3 = Joi.object({
remote_login: remote_login,
logdir: Joi.string().empty('').max(255),
datadir: Joi.string().empty('').min(2).max(255),
engine: type,
listen: listen,
publisher: publisher,
transport: transport,
dnssec: dnssec,
export: Joi.object({
interval: shared.uint16,
serials: Joi.boolean(),
Expand All @@ -58,6 +104,11 @@ export const PUT = Joi.object({
address: shared.ipv4,
address6: shared.ipv6.empty(''),
remote_login: remote_login,
engine: type,
listen: listen,
publisher: publisher,
transport: transport,
dnssec: dnssec,
export: Joi.object({
interval: shared.uint16,
serials: Joi.boolean(),
Expand Down
87 changes: 85 additions & 2 deletions lib/nameserver.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('nameserver', function () {
assert.deepEqual(value, testCase)
})

for (const n of ['bind', 'djbdns', 'knot', 'nsd', 'maradns', 'powerdns', 'dynect']) {
for (const n of ['bind', 'djbdns', 'knot', 'nsd', 'maradns', 'powerdns', 'dynect', 'native']) {
it(`accepts valid: ${n}`, () => {
const testCase = JSON.parse(JSON.stringify(testNS))
testCase.export.type = n
Expand All @@ -99,13 +99,96 @@ describe('nameserver', function () {

assert.strictEqual(
error.message,
'"export.type" must be one of [bind, djbdns, knot, nsd, maradns, powerdns, dynect]',
'"export.type" must be one of [bind, djbdns, knot, nsd, maradns, powerdns, dynect, native]',
)
assert.deepEqual(value, testCase)
})
}
})

describe('listen', function () {
it('accepts an array of {address, port, proto}', () => {
const testCase = JSON.parse(JSON.stringify(testNS))
testCase.listen = [
{ address: '127.0.0.1', port: 53, proto: 'udp' },
{ address: '127.0.0.1', port: 53, proto: 'tcp' },
]
const { error } = schema.validate(testCase)
assert.ifError(error)
})

it('rejects an invalid proto', () => {
const testCase = JSON.parse(JSON.stringify(testNS))
testCase.listen = [{ address: '127.0.0.1', port: 53, proto: 'http' }]
const { error } = schema.validate(testCase)
assert.match(error.message, /"listen\[0\]\.proto" must be one of/)
})

it('rejects out-of-range port', () => {
const testCase = JSON.parse(JSON.stringify(testNS))
testCase.listen = [{ address: '127.0.0.1', port: 70000 }]
const { error } = schema.validate(testCase)
assert.match(error.message, /"listen\[0\]\.port"/)
})
})

describe('publisher', function () {
for (const t of ['memory', 'rfc1035', 'tinydns-cdb', 'powerdns-db']) {
it(`accepts type=${t}`, () => {
const testCase = JSON.parse(JSON.stringify(testNS))
testCase.publisher = { type: t }
const { error } = schema.validate(testCase)
assert.ifError(error)
})
}

it('rejects unknown publisher type', () => {
const testCase = JSON.parse(JSON.stringify(testNS))
testCase.publisher = { type: 'floppy' }
const { error } = schema.validate(testCase)
assert.match(error.message, /"publisher\.type" must be one of/)
})
})

describe('transport', function () {
for (const t of ['noop', 'axfr', 'rsync', 'db-replication']) {
it(`accepts type=${t}`, () => {
const testCase = JSON.parse(JSON.stringify(testNS))
testCase.transport = { type: t, interval: 0, cooldown: 2 }
const { error } = schema.validate(testCase)
assert.ifError(error)
})
}

it('rejects unknown transport type', () => {
const testCase = JSON.parse(JSON.stringify(testNS))
testCase.transport = { type: 'sneakernet' }
const { error } = schema.validate(testCase)
assert.match(error.message, /"transport\.type" must be one of/)
})
})

describe('dnssec', function () {
it('accepts a valid dnssec block', () => {
const testCase = JSON.parse(JSON.stringify(testNS))
testCase.dnssec = {
enabled: true,
algorithm: 'ECDSAP256SHA256',
keyset: './keys/ns1',
nsec3: true,
}
const { error } = schema.validate(testCase)
assert.ifError(error)
})

it('rejects an unknown algorithm', () => {
const testCase = JSON.parse(JSON.stringify(testNS))
testCase.dnssec = { enabled: true, algorithm: 'ROT13' }
const { error } = schema.validate(testCase)
assert.match(error.message, /"dnssec\.algorithm" must be one of/)
})
})

describe('gid', function () {
it(`accepts valid`, () => {
const testCase = JSON.parse(JSON.stringify(testNS))
Expand Down
7 changes: 6 additions & 1 deletion lib/zone_record.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export const v3 = Joi.object({

'time signed': [Joi.number(), Joi.string().empty('')],

timestamp: Joi.date(),
timestamp: Joi.date().allow(null),

txt: Joi.string().empty(''),

Expand All @@ -226,6 +226,11 @@ export const v3 = Joi.object({
export const GET_req = Joi.object({
id: shared.uint32,
zid: shared.uint32,
search: Joi.string().max(255).allow(''),
limit: shared.uint32,
offset: shared.uint32,
sort_by: Joi.string().valid('id', 'owner', 'type', 'ttl'),
sort_dir: Joi.string().lowercase().valid('asc', 'desc'),
deleted: Joi.boolean(),
}).options({ allowUnknown: true })

Expand Down
53 changes: 53 additions & 0 deletions lib/zone_record.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,57 @@ describe('zone_record', function () {
assert.deepEqual(missing, [])
})
})

describe('timestamp', function () {
const base = {
id: 1,
zid: 1,
owner: 'www.example.com.',
ttl: 300,
type: 'A',
address: '1.2.3.4',
}

it('accepts a null or omitted timestamp (the store drops invalid ones)', () => {
assert.equal(schema.validate({ ...base, timestamp: null }).error, undefined)
assert.equal(schema.validate(base).error, undefined)
})

it('accepts a real date', () => {
const { error } = schema.validate({ ...base, timestamp: '2026-01-19T00:00:00Z' })
assert.equal(error, undefined)
})
})

describe('GET_req', function () {
const GET_req = zoneRecordSchema.GET_req

it('accepts pagination, search and sort params', () => {
const { error } = GET_req.validate({
zid: 5,
search: 'www',
limit: 50,
offset: 100,
sort_by: 'owner',
sort_dir: 'desc',
})
assert.equal(error, undefined)
})

it('lowercases sort_dir and accepts an empty search', () => {
const { value, error } = GET_req.validate({ zid: 5, sort_dir: 'DESC', search: '' })
assert.equal(error, undefined)
assert.equal(value.sort_dir, 'desc')
})

it('rejects an unknown sort_by column', () => {
const { error } = GET_req.validate({ zid: 5, sort_by: 'address' })
assert.ok(error)
})

it('rejects a negative offset', () => {
const { error } = GET_req.validate({ zid: 5, offset: -1 })
assert.ok(error)
})
})
})
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"versions": "npx npm-dep-mgr check",
"versions:fix": "npx npm-dep-mgr update",
"watch": "node --test --watch",
"test:coverage": "npx c8 --reporter=text --reporter=text-summary npm test"
"test:coverage": "node --test --experimental-test-coverage",
"test:coverage:lcov": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info"
},
"repository": {
"type": "git",
Expand All @@ -48,13 +49,13 @@
},
"homepage": "https://github.com/NicTool/validate#readme",
"devDependencies": {
"eslint": "^10.2.0",
"eslint": "^10.7.0",
"@eslint/js": "^10.0.1",
"eslint-config-prettier": "^10.1.8",
"globals": "^17.5.0"
"globals": "^17.7.0"
},
"dependencies": {
"joi": "^18.1.2",
"joi": "^18.2.3",
"joi-password": "^4.3.0"
},
"prettier": {
Expand All @@ -63,4 +64,4 @@
"trailingComma": "all",
"printWidth": 100
}
}
}
Loading