From cedc6ae9d4a4cc23117a7fc9c5f0265d662787cf Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sun, 29 Mar 2026 17:55:42 -0700 Subject: [PATCH 1/8] chore(release): v0.8.9 --- lib/group.js | 18 ++++++++++++++++++ lib/user.js | 10 ++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/group.js b/lib/group.js index 0e2e3cd..4f4ce23 100644 --- a/lib/group.js +++ b/lib/group.js @@ -73,6 +73,24 @@ export const GET_list_res = Joi.object({ meta: shared.meta, }) +export const GET_list_req = Joi.object({ + parent_gid: pid, + name: name, + deleted: Joi.boolean(), +}) + +export const GET_list_res = Joi.object({ + group: Joi.array().items( + Joi.object({ + id: pid, + name: name, + parent_gid: pid, + deleted: Joi.boolean(), + }), + ), + meta: shared.meta, +}) + export const POST = Joi.object({ id: id, name: name, diff --git a/lib/user.js b/lib/user.js index 91d8cc1..7cfef73 100644 --- a/lib/user.js +++ b/lib/user.js @@ -74,6 +74,16 @@ export const PUT = Joi.object({ inherit_group_permissions: Joi.boolean(), }) +export const PUT = Joi.object({ + first_name: Joi.string().min(1), + last_name: Joi.string().min(1), + username: username, + email: email, + password: password, + is_admin: Joi.boolean(), + deleted: Joi.boolean(), +}) + export const DELETE = Joi.object({ id: id, }) From 9a61c14b4f74dc63bccfdd35a74d55844043541e Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Mon, 13 Apr 2026 09:04:24 -0500 Subject: [PATCH 2/8] doc(CHANGELOG): add commit messages for 0.8.10 --- .release | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.release b/.release index a6911a9..9069128 160000 --- a/.release +++ b/.release @@ -1 +1 @@ -Subproject commit a6911a90f1b15486fb319d844341421c78035b2a +Subproject commit 9069128a8cbbab42c2b50989c630700397c4aba1 From a745286ce59f2b163795b1b8efb6719807f8ac70 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Mon, 13 Apr 2026 09:10:11 -0500 Subject: [PATCH 3/8] deps: bump versions, update ci with perms --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c6f0584..9bef9b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nictool/validate", - "version": "0.8.10", + "version": "0.8.9", "description": "NicTool Object Validation", "type": "module", "files": [ From 6a53b14f2727c063c6c0c6a0396162f3a0764902 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Mon, 13 Apr 2026 09:20:18 -0500 Subject: [PATCH 4/8] resolve dupes --- lib/group.js | 18 ------------------ lib/user.js | 10 ---------- 2 files changed, 28 deletions(-) diff --git a/lib/group.js b/lib/group.js index 4f4ce23..0e2e3cd 100644 --- a/lib/group.js +++ b/lib/group.js @@ -73,24 +73,6 @@ export const GET_list_res = Joi.object({ meta: shared.meta, }) -export const GET_list_req = Joi.object({ - parent_gid: pid, - name: name, - deleted: Joi.boolean(), -}) - -export const GET_list_res = Joi.object({ - group: Joi.array().items( - Joi.object({ - id: pid, - name: name, - parent_gid: pid, - deleted: Joi.boolean(), - }), - ), - meta: shared.meta, -}) - export const POST = Joi.object({ id: id, name: name, diff --git a/lib/user.js b/lib/user.js index 7cfef73..91d8cc1 100644 --- a/lib/user.js +++ b/lib/user.js @@ -74,16 +74,6 @@ export const PUT = Joi.object({ inherit_group_permissions: Joi.boolean(), }) -export const PUT = Joi.object({ - first_name: Joi.string().min(1), - last_name: Joi.string().min(1), - username: username, - email: email, - password: password, - is_admin: Joi.boolean(), - deleted: Joi.boolean(), -}) - export const DELETE = Joi.object({ id: id, }) From eed8b5abe1ab0fa29c2eb3cb0e8d11c5399aee32 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sun, 19 Jul 2026 23:53:37 -0700 Subject: [PATCH 5/8] doc(CONTRIBUTORS): updated --- .github/workflows/ci.yml | 6 --- .github/workflows/coverage.yml | 14 ++++++ .gitmodules | 2 +- CONTRIBUTORS.md | 4 +- lib/nameserver.js | 55 +++++++++++++++++++++ lib/nameserver.test.js | 87 +++++++++++++++++++++++++++++++++- lib/zone_record.js | 7 ++- lib/zone_record.test.js | 53 +++++++++++++++++++++ package.json | 7 +-- 9 files changed, 220 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c1fbee..c86c910 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,12 +13,6 @@ jobs: 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 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..3bb2ed5 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,14 @@ +name: Coverage + +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: [main] + +jobs: + coverage: + uses: NicTool/.github/.github/workflows/coverage.yml@main + secrets: inherit + permissions: + contents: read diff --git a/.gitmodules b/.gitmodules index a8e94cb..1647327 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule ".release"] path = .release - url = git@github.com:msimerson/.release.git + url = https://github.com/msimerson/.release.git diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 549c44c..f8b631f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -2,8 +2,8 @@ This handcrafted artisanal software is brought to you by: -|
msimerson (29)| -| :---: | +|
msimerson (30) |
burning-bush-dev (1) | +| :---: | :---: | this file is generated by [.release](https://github.com/msimerson/.release). Contribute to this project to get your GitHub profile included here. diff --git a/lib/nameserver.js b/lib/nameserver.js index dce8a4d..9c311e1 100644 --- a/lib/nameserver.js +++ b/lib/nameserver.js @@ -18,10 +18,55 @@ 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(), @@ -33,6 +78,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(), @@ -58,6 +108,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(), diff --git a/lib/nameserver.test.js b/lib/nameserver.test.js index e84ac15..0e56376 100644 --- a/lib/nameserver.test.js +++ b/lib/nameserver.test.js @@ -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 @@ -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)) diff --git a/lib/zone_record.js b/lib/zone_record.js index d5be903..5fb0699 100644 --- a/lib/zone_record.js +++ b/lib/zone_record.js @@ -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(''), @@ -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 }) diff --git a/lib/zone_record.test.js b/lib/zone_record.test.js index 858e8da..bd050d8 100644 --- a/lib/zone_record.test.js +++ b/lib/zone_record.test.js @@ -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) + }) + }) }) diff --git a/package.json b/package.json index 9bef9b4..073b7db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nictool/validate", - "version": "0.8.9", + "version": "0.8.10", "description": "NicTool Object Validation", "type": "module", "files": [ @@ -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", @@ -63,4 +64,4 @@ "trailingComma": "all", "printWidth": 100 } -} \ No newline at end of file +} From 3014504bf6e4dc4edf8ba60c775b980cfcf6fc18 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sun, 19 Jul 2026 23:54:58 -0700 Subject: [PATCH 6/8] deps: bump --- CHANGELOG.md | 3 ++- package.json | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e65643f..ee3d4d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ 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 - deps: bump versions ### [0.8.9] - 2026-03-29 @@ -118,7 +119,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 diff --git a/package.json b/package.json index 073b7db..527a649 100644 --- a/package.json +++ b/package.json @@ -49,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": { From 0000f19076cbf7b65f9d71297a2d485bb4cb8d49 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sun, 19 Jul 2026 23:55:04 -0700 Subject: [PATCH 7/8] chore: format --- CONTRIBUTORS.md | 2 +- lib/nameserver.js | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f8b631f..4d55feb 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -3,7 +3,7 @@ This handcrafted artisanal software is brought to you by: |
msimerson (30) |
burning-bush-dev (1) | -| :---: | :---: | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | this file is generated by [.release](https://github.com/msimerson/.release). Contribute to this project to get your GitHub profile included here. diff --git a/lib/nameserver.js b/lib/nameserver.js index 9c311e1..9230fcb 100644 --- a/lib/nameserver.js +++ b/lib/nameserver.js @@ -34,17 +34,13 @@ export const listen = Joi.array().items( ) export const publisher = Joi.object({ - type: Joi.string() - .valid('memory', 'rfc1035', 'tinydns-cdb', 'powerdns-db') - .required(), + 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(), + 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), From 336410e5bd20bcba0bea3b7ca06b799ef36f32dd Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sun, 19 Jul 2026 23:58:51 -0700 Subject: [PATCH 8/8] zone_rec: add pagination options --- .github/workflows/ci.yml | 7 +++---- .github/workflows/coverage.yml | 5 +++-- CHANGELOG.md | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c86c910..a0f5f83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,14 +7,13 @@ on: env: CI: true +permissions: + contents: read + jobs: lint: uses: NicTool/.github/.github/workflows/lint.yml@main - permissions: - contents: read test: uses: NicTool/.github/.github/workflows/test.yml@main secrets: inherit - permissions: - contents: read diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 3bb2ed5..348329d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -6,9 +6,10 @@ on: push: branches: [main] +permissions: + contents: read + jobs: coverage: uses: NicTool/.github/.github/workflows/coverage.yml@main secrets: inherit - permissions: - contents: read diff --git a/CHANGELOG.md b/CHANGELOG.md index ee3d4d9..6e1fda1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - 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