From 0ce896ea3b17af1bbef26cae5a44efd7aaa02d63 Mon Sep 17 00:00:00 2001 From: Atte Hinkka Date: Thu, 8 Oct 2026 11:01:40 +0300 Subject: [PATCH 01/20] Improve and add more SSN related Zod schemas There's currently quite a few places where SSN inputs have spaces in them. There's no reason for not properly normalizing them in one place. --- .../validation/__tests__/validation.test.ts | 48 +++++++++++++++++++ packages/validation/src/index.ts | 14 ++++-- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/packages/validation/__tests__/validation.test.ts b/packages/validation/__tests__/validation.test.ts index 4594760..cc3c63c 100644 --- a/packages/validation/__tests__/validation.test.ts +++ b/packages/validation/__tests__/validation.test.ts @@ -124,6 +124,41 @@ describe('validation-test', () => { }) }) + describe('ssn schemas', () => { + // Over hundred years old: valid without age checks, rejected with them + const over100 = '231194+246W' + + describe('ssnSchema', () => { + test('trims and uppercases', () => { + const result = validation.ssnSchema.safeParse(' 231194-246w\t\n') + assert.deepEqual(result, { success: true, data: '231194-246W' }) + }) + + test('rejects invalid ssn with message', () => { + const result = validation.ssnSchema.safeParse('231194-246X') + assert.equal(result.success, false) + assert.equal(result.error?.issues[0].message, 'Invalid ssn') + }) + + test('accepts person over hundred years old', () => { + assert.equal(validation.ssnSchema.safeParse(over100).success, true) + }) + }) + + describe('ssnSchemaWithAgeChecks', () => { + test('trims and uppercases', () => { + const result = validation.ssnSchemaWithAgeChecks.safeParse(' 010601a0102 ') + assert.deepEqual(result, { success: true, data: '010601A0102' }) + }) + + test('rejects person over hundred years old with message', () => { + const result = validation.ssnSchemaWithAgeChecks.safeParse(over100) + assert.equal(result.success, false) + assert.equal(result.error?.issues[0].message, 'Invalid ssn') + }) + }) + }) + describe('isFakeYtlSsn', () => { test('detects "A" century seperator fake ssn', () => assert.equal(validation.isFakeYtlSsn('200102AU000'), true)) @@ -132,6 +167,19 @@ describe('validation-test', () => { test('rejects valid ssn', () => assert.equal(validation.isFakeYtlSsn('231194-246W'), false)) }) + describe('fakeYtlSsnSchema', () => { + test('trims and uppercases', () => { + const result = validation.fakeYtlSsnSchema.safeParse(' 200102AU00a\n') + assert.deepEqual(result, { success: true, data: '200102AU00A' }) + }) + + test('rejects non-fake ssn with message', () => { + const result = validation.fakeYtlSsnSchema.safeParse('231194-246W') + assert.equal(result.success, false) + assert.equal(result.error?.issues[0].message, 'Invalid fake ssn') + }) + }) + describe('isValidEmail', () => { test('Rejects invalid student email', () => { const invalidEmails = [ diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 9e34487..547046a 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -36,7 +36,6 @@ export function isSsnValid(ssn: string, ageSanityCheckForExams = false): boolean } const centurySeparator21st = ['A', 'B', 'C', 'D', 'E', 'F'] - const specialFakeSsnToBeAllowed = '010101-0101' const matched = ssn .trim() .match(/^([0-9]{6})([-+]|[A-F]|[a-f]|[Yy]|[Xx]|[Ww]|[Vv]|[Uu])([\d]{3}|U[\d]{2})([\dA-Ya-y])$/) @@ -56,7 +55,8 @@ export function isSsnValid(ssn: string, ageSanityCheckForExams = false): boolean // We don't want to always do these checks, for example if we validate // data which is not related to current matricular exams if (ageSanityCheckForExams) { - if (ssn.trim() === specialFakeSsnToBeAllowed) return true + const specialFakeSsnToBeAllowed = '010101-0101' + if (specialFakeSsnToBeAllowed === ssn.trim().toUpperCase()) return true // Do not allow people over hundred years old (accidental use of - instead of A) if (centurySeparator === '+') return false if (centurySeparator === '-' && birthDateYear < currentYear) return false @@ -69,7 +69,13 @@ export function isSsnValid(ssn: string, ageSanityCheckForExams = false): boolean return checkCode === checkCodes[checkCodeIndex] } -export const zodSsn = z.string().refine(isSsnValid, { message: 'Invalid ssn' }).toUpperCase() +export const ssnSchema = z.string().trim().refine(isSsnValid, { message: 'Invalid ssn' }).toUpperCase() + +export const ssnSchemaWithAgeChecks = z + .string() + .trim() + .refine(ssn => isSsnValid(ssn, true), { message: 'Invalid ssn' }) + .toUpperCase() export function isFakeYtlSsn(ssn: string): boolean { const fakeYtlSsnRegEx = /\d{6}(AU|-U)\d{2}\w$/ @@ -77,6 +83,8 @@ export function isFakeYtlSsn(ssn: string): boolean { return fakeYtlSsnRegEx.test(ssn) } +export const fakeYtlSsnSchema = z.string().trim().refine(isFakeYtlSsn, { message: 'Invalid fake ssn' }).toUpperCase() + export function isValidEmail(email: string): boolean { const emailRegex = /^[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/ From 9fe925b9af682c106ffa8c98328b47ca74dd463a Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:13:54 +0300 Subject: [PATCH 02/20] Use error message variable `message` property is deprecated in favour of `error` --- packages/validation/src/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 547046a..d30539b 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -69,12 +69,13 @@ export function isSsnValid(ssn: string, ageSanityCheckForExams = false): boolean return checkCode === checkCodes[checkCodeIndex] } -export const ssnSchema = z.string().trim().refine(isSsnValid, { message: 'Invalid ssn' }).toUpperCase() +const ssnError = 'SSN must be valid' +export const ssnSchema = z.string({ error: ssnError }).trim().refine(isSsnValid, { error: ssnError }).toUpperCase() export const ssnSchemaWithAgeChecks = z .string() .trim() - .refine(ssn => isSsnValid(ssn, true), { message: 'Invalid ssn' }) + .refine(ssn => isSsnValid(ssn, true), { error: ssnError }) .toUpperCase() export function isFakeYtlSsn(ssn: string): boolean { @@ -83,7 +84,7 @@ export function isFakeYtlSsn(ssn: string): boolean { return fakeYtlSsnRegEx.test(ssn) } -export const fakeYtlSsnSchema = z.string().trim().refine(isFakeYtlSsn, { message: 'Invalid fake ssn' }).toUpperCase() +export const fakeYtlSsnSchema = z.string().trim().refine(isFakeYtlSsn, { error: 'Invalid fake ssn' }).toUpperCase() export function isValidEmail(email: string): boolean { const emailRegex = From 08fd17a2d7f60285f1d195c1d4157f690dbcccf0 Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:24:54 +0300 Subject: [PATCH 03/20] Rename symbol --- packages/user-management/src/user-types.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/user-management/src/user-types.ts b/packages/user-management/src/user-types.ts index baf8e6d..864e009 100644 --- a/packages/user-management/src/user-types.ts +++ b/packages/user-management/src/user-types.ts @@ -1,6 +1,6 @@ import z from 'zod' import { RoleSchema } from './validations' -import { zodSsn } from '@digabi/validation' +import { ssnSchema } from '@digabi/validation' import { PermissionGrantSchema } from './permissions' export type Role = z.infer @@ -19,8 +19,8 @@ export const PrincipalOrganizationSchema = z.object({ export type Impersonation = z.infer export const ImpersonationSchema = z.object({ id: z.uuid(), - realSsn: zodSsn, - impersonatedSsn: zodSsn.or(z.literal('')), + realSsn: ssnSchema, + impersonatedSsn: ssnSchema.or(z.literal('')), explanation: z.string(), validityStart: z.string(), validityEnd: z.string(), @@ -32,7 +32,7 @@ export const MockUserSchema = z.object({ username: z.string(), lastname: z.string(), firstname: z.string(), - ssn: zodSsn, + ssn: ssnSchema, henkiloOid: z.string(), principalOrganization: z.array(PrincipalOrganizationSchema), roles: z.array(z.object({ role: z.string() })).optional() @@ -143,7 +143,7 @@ export const UserSchoolSchema = z.object({ export type User = z.infer export const UserSchema = StoredUserDetailsSchema.extend({ - ssn: zodSsn, + ssn: ssnSchema, userAccountId: z.string(), schools: z.array(UserSchoolSchema), censoring: CensoringSchema.optional(), @@ -152,7 +152,7 @@ export const UserSchema = StoredUserDetailsSchema.extend({ export type UserToUpsert = z.infer export const UserToUpsertSchema = UserDetailsSchema.extend({ - ssn: zodSsn, + ssn: ssnSchema, schools: z.array(UserSchoolToUpsertSchema), censoring: CensoringSchema.optional() }).strict() @@ -185,7 +185,7 @@ export const UserForAuthenticationSchema = z.xor([UserSchema, ImpersonatedUserSc export type UserWithSchoolRoleAndSchoolId = z.infer export const UserWithSchoolRoleAndSchoolIdSchema = z.object({ userAccountId: z.string(), - ssn: zodSsn, + ssn: ssnSchema, userDetails: UserDetailsSchema, name: z.string(), allowedExams: z.array(z.string()), From b4a030440f9aa09c6d87c06ad4746e822b18fe12 Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:32:22 +0300 Subject: [PATCH 04/20] Fix test --- packages/validation/__tests__/validation.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/validation/__tests__/validation.test.ts b/packages/validation/__tests__/validation.test.ts index cc3c63c..b5d2457 100644 --- a/packages/validation/__tests__/validation.test.ts +++ b/packages/validation/__tests__/validation.test.ts @@ -137,7 +137,7 @@ describe('validation-test', () => { test('rejects invalid ssn with message', () => { const result = validation.ssnSchema.safeParse('231194-246X') assert.equal(result.success, false) - assert.equal(result.error?.issues[0].message, 'Invalid ssn') + assert.equal(result.error?.issues[0].message, 'SSN must be valid') }) test('accepts person over hundred years old', () => { @@ -154,7 +154,7 @@ describe('validation-test', () => { test('rejects person over hundred years old with message', () => { const result = validation.ssnSchemaWithAgeChecks.safeParse(over100) assert.equal(result.success, false) - assert.equal(result.error?.issues[0].message, 'Invalid ssn') + assert.equal(result.error?.issues[0].message, 'SSN must be valid') }) }) }) From b23ce7e3ba9c3b9fa8fccd39143331aa9ad7f2c5 Mon Sep 17 00:00:00 2001 From: Github CI Date: Thu, 8 Oct 2026 13:36:45 +0000 Subject: [PATCH 05/20] CI: Release @digabi/validation@4.0.0-feature-ssn-schemas-with-input-trimming.0 with tag feature-ssn-schemas-with-input-trimming --- package-lock.json | 21 ++++++++++++++++++++- packages/validation/package.json | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8a75172..a751080 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18700,6 +18700,16 @@ "express": "^5.1.0" } }, + "packages/testing/node_modules/@digabi/validation": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@digabi/validation/-/validation-3.2.0.tgz", + "integrity": "sha512-EOPf5Ocr4z4D4gGVeWWy+0R+y6IQUoCQdgd4RTI/DoERgymT4L4mxxqI2JBIihd8WtiiUiir1niY4ho0zxeItg==", + "dev": true, + "license": "EUPL-1.2", + "dependencies": { + "zod": "^4.4.3" + } + }, "packages/typescript-config": { "name": "@digabi/typescript-config", "version": "3.0.0", @@ -18714,9 +18724,18 @@ "zod": "^4.4.3" } }, + "packages/user-management/node_modules/@digabi/validation": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@digabi/validation/-/validation-3.2.0.tgz", + "integrity": "sha512-EOPf5Ocr4z4D4gGVeWWy+0R+y6IQUoCQdgd4RTI/DoERgymT4L4mxxqI2JBIihd8WtiiUiir1niY4ho0zxeItg==", + "license": "EUPL-1.2", + "dependencies": { + "zod": "^4.4.3" + } + }, "packages/validation": { "name": "@digabi/validation", - "version": "3.2.0", + "version": "4.0.0-feature-ssn-schemas-with-input-trimming.0", "license": "EUPL-1.2", "dependencies": { "zod": "^4.4.3" diff --git a/packages/validation/package.json b/packages/validation/package.json index ffc362c..034a7a9 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@digabi/validation", - "version": "3.2.0", + "version": "4.0.0-feature-ssn-schemas-with-input-trimming.0", "author": "Matriculation Examination Board, Finland", "license": "EUPL-1.2", "repository": { From 5d5d880c873057b5087912dc1d2409cdef5cd561 Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:34:08 +0300 Subject: [PATCH 06/20] Bump dependency --- packages/user-management/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/user-management/package.json b/packages/user-management/package.json index bbe2199..194a4dc 100644 --- a/packages/user-management/package.json +++ b/packages/user-management/package.json @@ -27,7 +27,7 @@ "access": "public" }, "dependencies": { - "@digabi/validation": "^3.1.0", + "@digabi/validation": "^4.0.0", "zod": "^4.4.3" } } From f54282e8e815724bfd69661b439eedaf51600730 Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:42:46 +0300 Subject: [PATCH 07/20] Add prerelease dependency --- packages/user-management/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/user-management/package.json b/packages/user-management/package.json index 194a4dc..cb722c3 100644 --- a/packages/user-management/package.json +++ b/packages/user-management/package.json @@ -27,7 +27,7 @@ "access": "public" }, "dependencies": { - "@digabi/validation": "^4.0.0", + "@digabi/validation": "4.0.0-feature-ssn-schemas-with-input-trimming.0", "zod": "^4.4.3" } } From 96ee776f69c13e49510d20dfeb032c166ee2112b Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:46:15 +0300 Subject: [PATCH 08/20] Workaround --- packages/user-management/src/user-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/user-management/src/user-types.ts b/packages/user-management/src/user-types.ts index 864e009..261d84e 100644 --- a/packages/user-management/src/user-types.ts +++ b/packages/user-management/src/user-types.ts @@ -1,6 +1,6 @@ import z from 'zod' import { RoleSchema } from './validations' -import { ssnSchema } from '@digabi/validation' +import { zodSsn as ssnSchema } from '@digabi/validation' import { PermissionGrantSchema } from './permissions' export type Role = z.infer From c9464e46334062f852b92e31eb538619e58a3fff Mon Sep 17 00:00:00 2001 From: Github CI Date: Thu, 8 Oct 2026 13:50:20 +0000 Subject: [PATCH 09/20] CI: Release @digabi/user-management@0.14.0-feature-ssn-schemas-with-input-trimming.0 with tag feature-ssn-schemas-with-input-trimming --- package-lock.json | 13 ++----------- packages/user-management/package.json | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index a751080..1b2c679 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18717,19 +18717,10 @@ }, "packages/user-management": { "name": "@digabi/user-management", - "version": "0.13.0", - "license": "EUPL-1.2", - "dependencies": { - "@digabi/validation": "^3.1.0", - "zod": "^4.4.3" - } - }, - "packages/user-management/node_modules/@digabi/validation": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@digabi/validation/-/validation-3.2.0.tgz", - "integrity": "sha512-EOPf5Ocr4z4D4gGVeWWy+0R+y6IQUoCQdgd4RTI/DoERgymT4L4mxxqI2JBIihd8WtiiUiir1niY4ho0zxeItg==", + "version": "0.14.0-feature-ssn-schemas-with-input-trimming.0", "license": "EUPL-1.2", "dependencies": { + "@digabi/validation": "4.0.0-feature-ssn-schemas-with-input-trimming.0", "zod": "^4.4.3" } }, diff --git a/packages/user-management/package.json b/packages/user-management/package.json index cb722c3..f8c245b 100644 --- a/packages/user-management/package.json +++ b/packages/user-management/package.json @@ -1,6 +1,6 @@ { "name": "@digabi/user-management", - "version": "0.13.0", + "version": "0.14.0-feature-ssn-schemas-with-input-trimming.0", "author": "Matriculation Examination Board, Finland", "license": "EUPL-1.2", "repository": { From 02544f0b701a4034017a633489064f2b01d72daa Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:52:44 +0300 Subject: [PATCH 10/20] Revert "Workaround" This reverts commit 96ee776f69c13e49510d20dfeb032c166ee2112b. --- packages/user-management/src/user-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/user-management/src/user-types.ts b/packages/user-management/src/user-types.ts index 261d84e..864e009 100644 --- a/packages/user-management/src/user-types.ts +++ b/packages/user-management/src/user-types.ts @@ -1,6 +1,6 @@ import z from 'zod' import { RoleSchema } from './validations' -import { zodSsn as ssnSchema } from '@digabi/validation' +import { ssnSchema } from '@digabi/validation' import { PermissionGrantSchema } from './permissions' export type Role = z.infer From 5799883b90aa1a49b08fe903c021488f6431254a Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:58:25 +0300 Subject: [PATCH 11/20] Use proper matching mode for prerelease tag --- packages/user-management/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/user-management/package.json b/packages/user-management/package.json index f8c245b..257c560 100644 --- a/packages/user-management/package.json +++ b/packages/user-management/package.json @@ -27,7 +27,7 @@ "access": "public" }, "dependencies": { - "@digabi/validation": "4.0.0-feature-ssn-schemas-with-input-trimming.0", + "@digabi/validation": "^4.0.0-feature-ssn-schemas-with-input-trimming.0", "zod": "^4.4.3" } } From 4a0e0f59d5db5b5c929780996169df19353913bb Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:24:05 +0300 Subject: [PATCH 12/20] Clarify error messages --- .../validation/__tests__/validation.test.ts | 4 ++-- packages/validation/src/index.ts | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/validation/__tests__/validation.test.ts b/packages/validation/__tests__/validation.test.ts index b5d2457..7ba9eeb 100644 --- a/packages/validation/__tests__/validation.test.ts +++ b/packages/validation/__tests__/validation.test.ts @@ -154,7 +154,7 @@ describe('validation-test', () => { test('rejects person over hundred years old with message', () => { const result = validation.ssnSchemaWithAgeChecks.safeParse(over100) assert.equal(result.success, false) - assert.equal(result.error?.issues[0].message, 'SSN must be valid') + assert.equal(result.error?.issues[0].message, 'SSN over 100 years old') }) }) }) @@ -176,7 +176,7 @@ describe('validation-test', () => { test('rejects non-fake ssn with message', () => { const result = validation.fakeYtlSsnSchema.safeParse('231194-246W') assert.equal(result.success, false) - assert.equal(result.error?.issues[0].message, 'Invalid fake ssn') + assert.equal(result.error?.issues[0].message, 'Invalid YTL fake SSN') }) }) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index d30539b..e5e87a8 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -69,13 +69,16 @@ export function isSsnValid(ssn: string, ageSanityCheckForExams = false): boolean return checkCode === checkCodes[checkCodeIndex] } -const ssnError = 'SSN must be valid' -export const ssnSchema = z.string({ error: ssnError }).trim().refine(isSsnValid, { error: ssnError }).toUpperCase() +export const ssnSchema = z + .string({ error: 'SSN must be valid' }) + .trim() + .refine(isSsnValid, { error: 'SSN must be valid' }) + .toUpperCase() export const ssnSchemaWithAgeChecks = z - .string() + .string({ error: 'SSN must be valid' }) .trim() - .refine(ssn => isSsnValid(ssn, true), { error: ssnError }) + .refine(ssn => isSsnValid(ssn, true), { error: 'SSN over 100 years old' }) .toUpperCase() export function isFakeYtlSsn(ssn: string): boolean { @@ -84,7 +87,11 @@ export function isFakeYtlSsn(ssn: string): boolean { return fakeYtlSsnRegEx.test(ssn) } -export const fakeYtlSsnSchema = z.string().trim().refine(isFakeYtlSsn, { error: 'Invalid fake ssn' }).toUpperCase() +export const fakeYtlSsnSchema = z + .string({ error: 'Invalid YTL fake SSN' }) + .trim() + .refine(isFakeYtlSsn, { error: 'Invalid YTL fake SSN' }) + .toUpperCase() export function isValidEmail(email: string): boolean { const emailRegex = From d0121875fc7f24d1bfee4634988ee3e41d7b07a5 Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:39:45 +0300 Subject: [PATCH 13/20] Add additional 20th century separators --- packages/validation/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index e5e87a8..6079845 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -36,6 +36,8 @@ export function isSsnValid(ssn: string, ageSanityCheckForExams = false): boolean } const centurySeparator21st = ['A', 'B', 'C', 'D', 'E', 'F'] + const centurySeparator20th = ['-', 'Y', 'X', 'W', 'V', 'U'] + const matched = ssn .trim() .match(/^([0-9]{6})([-+]|[A-F]|[a-f]|[Yy]|[Xx]|[Ww]|[Vv]|[Uu])([\d]{3}|U[\d]{2})([\dA-Ya-y])$/) @@ -59,7 +61,7 @@ export function isSsnValid(ssn: string, ageSanityCheckForExams = false): boolean if (specialFakeSsnToBeAllowed === ssn.trim().toUpperCase()) return true // Do not allow people over hundred years old (accidental use of - instead of A) if (centurySeparator === '+') return false - if (centurySeparator === '-' && birthDateYear < currentYear) return false + if (centurySeparator20th.includes(centurySeparator) && birthDateYear < currentYear) return false // Do not allow people not yet born if (centurySeparator21st.includes(centurySeparator) && birthDateYear > currentYear) return false From 35d55aedd6c1f9cb3485d3f076b6288b376233db Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:20:57 +0300 Subject: [PATCH 14/20] Remove custom error message on string type requirement --- packages/validation/src/index.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 6079845..d1502ce 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -71,14 +71,10 @@ export function isSsnValid(ssn: string, ageSanityCheckForExams = false): boolean return checkCode === checkCodes[checkCodeIndex] } -export const ssnSchema = z - .string({ error: 'SSN must be valid' }) - .trim() - .refine(isSsnValid, { error: 'SSN must be valid' }) - .toUpperCase() +export const ssnSchema = z.string().trim().refine(isSsnValid, { error: 'SSN must be valid' }).toUpperCase() export const ssnSchemaWithAgeChecks = z - .string({ error: 'SSN must be valid' }) + .string() .trim() .refine(ssn => isSsnValid(ssn, true), { error: 'SSN over 100 years old' }) .toUpperCase() From ace5cc9a8af9a35d6655c71f307a45e9f748ad04 Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:21:33 +0300 Subject: [PATCH 15/20] Add two-stage check to ssnSchemaWithAgeChecks, refine error message --- packages/validation/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index d1502ce..8f4845c 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -76,7 +76,8 @@ export const ssnSchema = z.string().trim().refine(isSsnValid, { error: 'SSN must export const ssnSchemaWithAgeChecks = z .string() .trim() - .refine(ssn => isSsnValid(ssn, true), { error: 'SSN over 100 years old' }) + .refine(isSsnValid, { error: 'SSN must be valid', abort: true }) + .refine(ssn => isSsnValid(ssn, true), { error: 'SSN fails age checks' }) .toUpperCase() export function isFakeYtlSsn(ssn: string): boolean { From 5478f3aa7cd726bebf5de5b87c5e1822e5148396 Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:22:18 +0300 Subject: [PATCH 16/20] Test every 20th century separator --- packages/validation/__tests__/validation.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/validation/__tests__/validation.test.ts b/packages/validation/__tests__/validation.test.ts index 7ba9eeb..afd6609 100644 --- a/packages/validation/__tests__/validation.test.ts +++ b/packages/validation/__tests__/validation.test.ts @@ -103,8 +103,11 @@ describe('validation-test', () => { }) test('rejects when person is over hundred years old, and age sanity check is on', () => { - assert.equal(validation.isSsnValid('100417-458U', true), false) assert.equal(validation.isSsnValid('231194+246w', true), false) + for (const separator of ['-', 'Y', 'X', 'W', 'V', 'U']) { + assert.equal(validation.isSsnValid(`010120${separator}123K`, true), false, separator) + assert.equal(validation.isSsnValid(`010180${separator}1232`, true), true, separator) + } }) test('accepts person not yet born when age sanity check is off', () => { From 7eab39b7c3101ca61648f5610b2e81897d7bc360 Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:23:27 +0300 Subject: [PATCH 17/20] Separate test cases for person over 100 years old with valid and invalid SSN --- packages/validation/__tests__/validation.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/validation/__tests__/validation.test.ts b/packages/validation/__tests__/validation.test.ts index afd6609..d423e29 100644 --- a/packages/validation/__tests__/validation.test.ts +++ b/packages/validation/__tests__/validation.test.ts @@ -130,6 +130,7 @@ describe('validation-test', () => { describe('ssn schemas', () => { // Over hundred years old: valid without age checks, rejected with them const over100 = '231194+246W' + const malformedOver100 = '231194+24W' describe('ssnSchema', () => { test('trims and uppercases', () => { @@ -157,7 +158,15 @@ describe('validation-test', () => { test('rejects person over hundred years old with message', () => { const result = validation.ssnSchemaWithAgeChecks.safeParse(over100) assert.equal(result.success, false) - assert.equal(result.error?.issues[0].message, 'SSN over 100 years old') + assert.notEqual(result.error?.issues[0].message, 'SSN must be valid') + assert.equal(result.error?.issues[0].message, 'SSN fails age checks') + }) + + test('rejects person over hundred years old with invalid SSN with a message about invalidity instead of age checks', () => { + const result = validation.ssnSchemaWithAgeChecks.safeParse(malformedOver100) + assert.equal(result.success, false) + assert.equal(result.error?.issues[0].message, 'SSN must be valid') + assert.notEqual(result.error?.issues[0].message, 'SSN fails age checks') }) }) }) From c3c6f4ee71c4b7d246422ddc1bc40eae137542dd Mon Sep 17 00:00:00 2001 From: Github CI Date: Mon, 12 Oct 2026 13:56:41 +0000 Subject: [PATCH 18/20] CI: Release @digabi/validation@4.0.1-feature-ssn-schemas-with-input-trimming.0 with tag feature-ssn-schemas-with-input-trimming --- package-lock.json | 13 +++++++++++-- packages/validation/package.json | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1b2c679..5e56915 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18720,13 +18720,22 @@ "version": "0.14.0-feature-ssn-schemas-with-input-trimming.0", "license": "EUPL-1.2", "dependencies": { - "@digabi/validation": "4.0.0-feature-ssn-schemas-with-input-trimming.0", + "@digabi/validation": "^4.0.0-feature-ssn-schemas-with-input-trimming.0", + "zod": "^4.4.3" + } + }, + "packages/user-management/node_modules/@digabi/validation": { + "version": "4.0.0-feature-ssn-schemas-with-input-trimming.0", + "resolved": "https://registry.npmjs.org/@digabi/validation/-/validation-4.0.0-feature-ssn-schemas-with-input-trimming.0.tgz", + "integrity": "sha512-6xR5HxzG8pILBbbIyHFl3qJ253gNB+C7yiYJ8MZ+T73bHAsBkHy2D3oVF7lTy0RYAlFOlfTQ79odaO/KwWbDuw==", + "license": "EUPL-1.2", + "dependencies": { "zod": "^4.4.3" } }, "packages/validation": { "name": "@digabi/validation", - "version": "4.0.0-feature-ssn-schemas-with-input-trimming.0", + "version": "4.0.1-feature-ssn-schemas-with-input-trimming.0", "license": "EUPL-1.2", "dependencies": { "zod": "^4.4.3" diff --git a/packages/validation/package.json b/packages/validation/package.json index 034a7a9..0f797ef 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@digabi/validation", - "version": "4.0.0-feature-ssn-schemas-with-input-trimming.0", + "version": "4.0.1-feature-ssn-schemas-with-input-trimming.0", "author": "Matriculation Examination Board, Finland", "license": "EUPL-1.2", "repository": { From 4d92c3156f58f3430282fb8c6a86381e5e8fe52d Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:18:39 +0300 Subject: [PATCH 19/20] Remove custom error message on string type requirement Co-authored-by: Atte Hinkka <306564+ahinkka@users.noreply.github.com> --- packages/validation/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 8f4845c..968fdf0 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -87,7 +87,7 @@ export function isFakeYtlSsn(ssn: string): boolean { } export const fakeYtlSsnSchema = z - .string({ error: 'Invalid YTL fake SSN' }) + .string() .trim() .refine(isFakeYtlSsn, { error: 'Invalid YTL fake SSN' }) .toUpperCase() From 7fa464202039a524c7dca720233a441c1c2dd048 Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:21:45 +0300 Subject: [PATCH 20/20] Format --- packages/validation/src/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 968fdf0..85b7c63 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -86,11 +86,7 @@ export function isFakeYtlSsn(ssn: string): boolean { return fakeYtlSsnRegEx.test(ssn) } -export const fakeYtlSsnSchema = z - .string() - .trim() - .refine(isFakeYtlSsn, { error: 'Invalid YTL fake SSN' }) - .toUpperCase() +export const fakeYtlSsnSchema = z.string().trim().refine(isFakeYtlSsn, { error: 'Invalid YTL fake SSN' }).toUpperCase() export function isValidEmail(email: string): boolean { const emailRegex =