Skip to content

Commit 7b15124

Browse files
authored
Merge pull request #152 from kaonis/feat/147-zod-v4-migration-validation
fix(deps): validate and migrate Zod v4 compatibility across services
2 parents 1c82368 + ce7adc7 commit 7b15124

10 files changed

Lines changed: 49 additions & 22 deletions

File tree

apps/cnc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"swagger-ui-express": "^5.0.1",
4747
"winston": "^3.17.0",
4848
"ws": "^8.18.3",
49-
"zod": "^3.24.1"
49+
"zod": "^4.0.0"
5050
},
5151
"devDependencies": {
5252
"@types/better-sqlite3": "^7.6.13",

apps/cnc/src/controllers/hosts.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import { Request, Response } from 'express';
6+
import { isIP } from 'node:net';
67
import { z } from 'zod';
78
import { hostStatusSchema } from '@kaonis/woly-protocol';
89
import { HostAggregator } from '../services/hostAggregator';
@@ -11,10 +12,14 @@ import { lookupMacVendor, MAC_ADDRESS_PATTERN } from '../services/macVendorServi
1112
import logger from '../utils/logger';
1213

1314
// Validation schema for updateHost request body
15+
const ipAddressSchema = z.string().refine((value) => isIP(value) !== 0, {
16+
message: 'IP address must be a valid IPv4 or IPv6 address',
17+
});
18+
1419
const updateHostBodySchema = z.object({
1520
name: z.string().min(1).optional(),
1621
mac: z.string().regex(MAC_ADDRESS_PATTERN).optional(),
17-
ip: z.string().ip().optional(),
22+
ip: ipAddressSchema.optional(),
1823
status: hostStatusSchema.optional(),
1924
}).strict();
2025

@@ -322,7 +327,7 @@ export class HostsController {
322327
res.status(400).json({
323328
error: 'Bad Request',
324329
message: 'Invalid request body',
325-
details: parseResult.error.errors,
330+
details: parseResult.error.issues,
326331
});
327332
return;
328333
}

apps/node-agent/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"wake_on_lan": "1.0.0",
5050
"winston": "^3.17.0",
5151
"ws": "^8.18.3",
52-
"zod": "^3.24.1"
52+
"zod": "^4.0.0"
5353
},
5454
"devDependencies": {
5555
"@types/better-sqlite3": "^7.6.13",

apps/node-agent/src/middleware/__tests__/validateRequest.unit.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ describe('validateRequest middleware', () => {
6262
middleware(mockReq as Request, mockRes as Response, mockNext);
6363
}).toThrow(AppError);
6464

65+
try {
66+
middleware(mockReq as Request, mockRes as Response, mockNext);
67+
fail('Should have thrown AppError');
68+
} catch (error) {
69+
expect(error).toBeInstanceOf(AppError);
70+
expect((error as AppError).message.toLowerCase()).toContain('required');
71+
expect((error as AppError).message).toContain('email');
72+
}
73+
6574
expect(mockNext).not.toHaveBeenCalled();
6675
});
6776

apps/node-agent/src/middleware/validateRequest.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,15 @@ export const validateRequest = (schema: z.ZodTypeAny, target: ValidationTarget =
2828
} catch (error) {
2929
if (error instanceof ZodError) {
3030
// Format errors to match Joi format: comma-separated error messages with field paths
31-
const errorMessage = error.errors
31+
const errorMessage = error.issues
3232
.map((err) => {
3333
const path = err.path.length > 0 ? `"${err.path.join('.')}" ` : '';
34-
return `${path}${err.message}`;
34+
const isMissingRequiredField =
35+
err.code === 'invalid_type' &&
36+
(err as { input?: unknown }).input === undefined &&
37+
err.path.length > 0;
38+
const message = isMissingRequiredField ? 'is required' : err.message;
39+
return `${path}${message}`;
3540
})
3641
.join(', ');
3742
throw new AppError(errorMessage, 400, 'VALIDATION_ERROR');

apps/node-agent/src/validators/__tests__/hostValidator.unit.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe('hostValidator schemas', () => {
5858
const result = updateHostSchema.safeParse({});
5959
expect(result.success).toBe(false);
6060
if (!result.success) {
61-
expect(result.error.errors[0]?.message).toContain('At least one field is required');
61+
expect(result.error.issues[0]?.message).toContain('At least one field is required');
6262
}
6363
});
6464

apps/node-agent/src/validators/hostValidator.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { z } from 'zod';
2+
import { isIP } from 'node:net';
23

34
/**
45
* MAC address validation pattern
56
* Accepts formats: XX:XX:XX:XX:XX:XX or XX-XX-XX-XX-XX-XX
67
*/
78
const macAddressPattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
9+
const ipAddressSchema = z.string().refine((value) => isIP(value) !== 0, {
10+
message: 'IP address must be a valid IPv4 or IPv6 address',
11+
});
812

913
/**
1014
* Schema for validating MAC address parameter
@@ -18,7 +22,7 @@ export const macAddressSchema = z.object({
1822
*/
1923
export const addHostSchema = z.object({
2024
name: z.string().min(1, 'Hostname must be at least 1 character').max(255, 'Hostname must not exceed 255 characters').trim(),
21-
ip: z.string().ip('IP address must be a valid IPv4 or IPv6 address'),
25+
ip: ipAddressSchema,
2226
mac: z.string().regex(macAddressPattern, 'MAC address must be in format XX:XX:XX:XX:XX:XX or XX-XX-XX-XX-XX-XX'),
2327
});
2428

@@ -33,7 +37,7 @@ export const updateHostSchema = z
3337
.max(255, 'Hostname must not exceed 255 characters')
3438
.trim()
3539
.optional(),
36-
ip: z.string().ip('IP address must be a valid IPv4 or IPv6 address').optional(),
40+
ip: ipAddressSchema.optional(),
3741
mac: z
3842
.string()
3943
.regex(

docs/ROADMAP_V6.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,17 @@ Scope: New autonomous cycle after V5 completion.
66
## 1. Status Audit
77

88
### Repository and branch status
9-
- `master` synced at merge commit `9b027df` (PR #145).
9+
- `master` synced at merge commit `1c82368` (PR #151).
1010
- Active execution branch: `master`.
1111

1212
### Open issue snapshot (`kaonis/woly-server`)
1313
- #4 `Dependency Dashboard`
14-
- #144 `[Dependencies] Plan major dashboard upgrade wave (ESLint 10, TS-ESLint 8, Zod 4, npm 11)`
15-
- #146 `[Dependencies] Execute tooling major upgrade set (ESLint 9 + typescript-eslint 8)`
1614
- #147 `[Dependencies] Evaluate and stage Zod v4 migration across protocol and services`
1715
- #148 `[Dependencies] Evaluate npm 11 adoption and CI/runtime compatibility`
1816
- #150 `[Dependencies] Revisit ESLint 10 adoption after typescript-eslint compatibility`
1917

2018
### CI snapshot
21-
- Post-merge checks for `9b027df` are green (CI + CodeQL).
19+
- Post-merge checks for `1c82368` are green (CI + CodeQL).
2220
- Dependency triage workflow and audit/security gates are documented and active.
2321

2422
## 2. Iterative Phases
@@ -43,7 +41,7 @@ Acceptance criteria:
4341
- Keep lint/typecheck/test gates green across workspaces.
4442
- Document any lint rule/config migration adjustments.
4543

46-
Status: `In Progress` (2026-02-15)
44+
Status: `Completed` (2026-02-15, PR #151)
4745

4846
### Phase 3: Zod v4 migration validation
4947
Issue: #147
@@ -54,7 +52,7 @@ Acceptance criteria:
5452
- Preserve contract compatibility or document deliberate breaking changes.
5553
- Keep CI and contract/schema suites green.
5654

57-
Status: `Pending`
55+
Status: `In Progress` (2026-02-15)
5856

5957
## 3. Execution Loop Rules
6058

@@ -82,3 +80,9 @@ For each phase:
8280
- 2026-02-15: Started Phase 2 issue #146 on branch `feat/146-tooling-major-upgrade-set`.
8381
- 2026-02-15: Upgraded lint tooling to ESLint 9 + typescript-eslint 8 + eslint-config-prettier 10 and stabilized ESLint v9 compatibility for current `.eslintrc` workflow.
8482
- 2026-02-15: Ran local root gates for #146 (`npm run lint`, `npm run typecheck`, `npm run test:ci`) successfully.
83+
- 2026-02-15: Merged #146 via PR #151 and verified post-merge `master` checks green (CI + CodeQL).
84+
- 2026-02-15: Started Phase 3 issue #147 on branch `feat/147-zod-v4-migration-validation`.
85+
- 2026-02-15: Applied Zod v4 migration updates across protocol/C&C/node-agent (`zod` dependency majors, IP validation migration from `z.string().ip()` to `node:net` `isIP` refinements, `ZodError.errors` -> `ZodError.issues`).
86+
- 2026-02-15: Ran full local gates for #147 (`npm run typecheck`, `npm run test:ci`, `npm run lint`, `npm run build`) successfully.
87+
- 2026-02-15: Addressed PR #152 CI regression in node-agent validation messaging by normalizing missing-field errors back to `is required` semantics and adding unit coverage for the behavior.
88+
- 2026-02-15: Re-ran full local gates for #147 (`npm run typecheck`, `npm run test:ci`, `npm run lint`, `npm run build`) successfully after CI-fix patch.

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/protocol/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"access": "public"
3333
},
3434
"dependencies": {
35-
"zod": "^3.24.1"
35+
"zod": "^4.0.0"
3636
},
3737
"devDependencies": {
3838
"@types/jest": "^30.0.0",

0 commit comments

Comments
 (0)