From 0ce896ea3b17af1bbef26cae5a44efd7aaa02d63 Mon Sep 17 00:00:00 2001 From: Atte Hinkka Date: Thu, 8 Oct 2026 11:01:40 +0300 Subject: [PATCH 01/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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 = From 759f55309954990513f7f13252fdb5da8a510c2c Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:58:04 +0300 Subject: [PATCH 21/26] Allow lowercase U in fake YTL SSN, allow more separator characters in fake YTL SSN --- packages/validation/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 85b7c63..fe5d6a8 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -40,7 +40,7 @@ export function isSsnValid(ssn: string, ageSanityCheckForExams = false): boolean 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])$/) + .match(/^([0-9]{6})([-+]|[A-F]|[a-f]|[Yy]|[Xx]|[Ww]|[Vv]|[Uu])([\d]{3}|[Uu][\d]{2})([\dA-Ya-y])$/) if (!matched) return false const individualNumber = matched[3] @@ -81,7 +81,7 @@ export const ssnSchemaWithAgeChecks = z .toUpperCase() export function isFakeYtlSsn(ssn: string): boolean { - const fakeYtlSsnRegEx = /\d{6}(AU|-U)\d{2}\w$/ + const fakeYtlSsnRegEx = /^([0-9]{6})([-+]|[A-F]|[a-f]|[Yy]|[Xx]|[Ww]|[Vv]|[Uu])[Uu][\d]{2}([\dA-Ya-y])$/ return fakeYtlSsnRegEx.test(ssn) } From bb7d3cc5c6b747f4482946edaee1d5d3fa6a64bf Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:59:13 +0300 Subject: [PATCH 22/26] Use isFakeYtlSsn to check for fake YTL SSN inside normal validation function for added consistency --- packages/validation/src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index fe5d6a8..3ba68a6 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -43,12 +43,9 @@ export function isSsnValid(ssn: string, ageSanityCheckForExams = false): boolean .match(/^([0-9]{6})([-+]|[A-F]|[a-f]|[Yy]|[Xx]|[Ww]|[Vv]|[Uu])([\d]{3}|[Uu][\d]{2})([\dA-Ya-y])$/) if (!matched) return false - const individualNumber = matched[3] - // Special YTL-defined fake ssn. checkCode calculations won't make sense for these - if (individualNumber.startsWith('U')) return true - const birthDate = matched[1] const centurySeparator = matched[2].toUpperCase() + const individualNumber = matched[3] const checkCode = matched[4].toUpperCase() const birthDateYear = Number(birthDate.slice(-2)) @@ -67,6 +64,9 @@ export function isSsnValid(ssn: string, ageSanityCheckForExams = false): boolean if (centurySeparator21st.includes(centurySeparator) && birthDateYear > currentYear) return false } + // Special YTL-defined fake ssn. checkCode calculations won't make sense for these + if (isFakeYtlSsn(ssn)) return true + const checkCodeIndex = parseInt(birthDate.concat(individualNumber), 10) % 31 return checkCode === checkCodes[checkCodeIndex] } From ec2d47a1c25883daf7bf52222bad5f5a9b21685e Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:00:38 +0300 Subject: [PATCH 23/26] Add more tests for fake YTL SSNs --- .../validation/__tests__/validation.test.ts | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/validation/__tests__/validation.test.ts b/packages/validation/__tests__/validation.test.ts index d423e29..9851000 100644 --- a/packages/validation/__tests__/validation.test.ts +++ b/packages/validation/__tests__/validation.test.ts @@ -129,8 +129,9 @@ 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' + const over100 = ' \t231194+246W\n ' + const malformedOver100 = '\n 231194+24W \t' + const over100fakeYTL = '\n 231194+U46W \t' describe('ssnSchema', () => { test('trims and uppercases', () => { @@ -147,6 +148,13 @@ describe('validation-test', () => { test('accepts person over hundred years old', () => { assert.equal(validation.ssnSchema.safeParse(over100).success, true) }) + + test('accepts fake YTL SSN', () => { + let result = validation.ssnSchema.safeParse('\t\n231194-u46w ') + assert.equal(result.success, true) + result = validation.ssnSchema.safeParse(' 231101au46w\t\n') + assert.equal(result.success, true) + }) }) describe('ssnSchemaWithAgeChecks', () => { @@ -168,6 +176,13 @@ describe('validation-test', () => { assert.equal(result.error?.issues[0].message, 'SSN must be valid') assert.notEqual(result.error?.issues[0].message, 'SSN fails age checks') }) + + test('rejects person over 100 years old with fake YTL SSN with message', () => { + const result = validation.ssnSchemaWithAgeChecks.safeParse(over100fakeYTL) + assert.equal(result.success, false) + assert.notEqual(result.error?.issues[0].message, 'SSN must be valid') + assert.equal(result.error?.issues[0].message, 'SSN fails age checks') + }) }) }) @@ -190,6 +205,13 @@ describe('validation-test', () => { assert.equal(result.success, false) assert.equal(result.error?.issues[0].message, 'Invalid YTL fake SSN') }) + + test('accepts fake SSN even if U is lowercase', () => { + let result = validation.fakeYtlSsnSchema.safeParse('200102au00a') + assert.equal(result.success, true) + result = validation.fakeYtlSsnSchema.safeParse('200198-u00a') + assert.equal(result.success, true) + }) }) describe('isValidEmail', () => { From b59d12dfbf492c5b3cbf267bcd69b25aaf701406 Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:00:51 +0300 Subject: [PATCH 24/26] Rename test group --- packages/validation/__tests__/validation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/validation/__tests__/validation.test.ts b/packages/validation/__tests__/validation.test.ts index 9851000..66582ac 100644 --- a/packages/validation/__tests__/validation.test.ts +++ b/packages/validation/__tests__/validation.test.ts @@ -3,7 +3,7 @@ import { test, describe } from 'node:test' import assert from 'node:assert/strict' describe('validation-test', () => { - describe('validating ssn', () => { + describe('isSsnValid', () => { test('accepts valid ssn', () => { assert.equal(validation.isSsnValid('231194-246W'), true) }) From 1e0a494b8723ce1fe11d3c932ad3841f46656683 Mon Sep 17 00:00:00 2001 From: Adela Noponen <2370315+adetabs@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:03:24 +0300 Subject: [PATCH 25/26] Reorder tests for clarity and coherence --- .../validation/__tests__/validation.test.ts | 307 +++++++++--------- 1 file changed, 154 insertions(+), 153 deletions(-) diff --git a/packages/validation/__tests__/validation.test.ts b/packages/validation/__tests__/validation.test.ts index 66582ac..6c4ca60 100644 --- a/packages/validation/__tests__/validation.test.ts +++ b/packages/validation/__tests__/validation.test.ts @@ -3,130 +3,169 @@ import { test, describe } from 'node:test' import assert from 'node:assert/strict' describe('validation-test', () => { - describe('isSsnValid', () => { - test('accepts valid ssn', () => { - assert.equal(validation.isSsnValid('231194-246W'), true) - }) + describe('validation functions', () => { + describe('isSsnValid', () => { + test('accepts valid ssn', () => { + assert.equal(validation.isSsnValid('231194-246W'), true) + }) - test('accepts check char in lower case', () => { - assert.equal(validation.isSsnValid('231194-246w'), true) - }) + test('accepts check char in lower case', () => { + assert.equal(validation.isSsnValid('231194-246w'), true) + }) - test('accepts ssn not from 20th century', () => { - assert.equal(validation.isSsnValid('231194+246w'), true) - assert.equal(validation.isSsnValid('010601A0102'), true) - assert.equal(validation.isSsnValid('010601a0102'), true) - }) + test('accepts ssn not from 20th century', () => { + assert.equal(validation.isSsnValid('231194+246w'), true) + assert.equal(validation.isSsnValid('010601A0102'), true) + assert.equal(validation.isSsnValid('010601a0102'), true) + }) - test('accepts ssn from 20th century', () => { - assert.equal(validation.isSsnValid('010103A2465'), true) - assert.equal(validation.isSsnValid('010280Y246u'), true) - assert.equal(validation.isSsnValid('010280X246u'), true) - assert.equal(validation.isSsnValid('010280W246u'), true) - assert.equal(validation.isSsnValid('010280V246u'), true) - assert.equal(validation.isSsnValid('010280U246u'), true) - assert.equal(validation.isSsnValid('010280G246u'), false) - assert.equal(validation.isSsnValid('010280y246u'), true) - assert.equal(validation.isSsnValid('010280x246u'), true) - assert.equal(validation.isSsnValid('010280w246u'), true) - assert.equal(validation.isSsnValid('010280v246u'), true) - assert.equal(validation.isSsnValid('010280u246u'), true) - assert.equal(validation.isSsnValid('010280g246u'), false) - }) + test('accepts ssn from 20th century', () => { + assert.equal(validation.isSsnValid('010103A2465'), true) + assert.equal(validation.isSsnValid('010280Y246u'), true) + assert.equal(validation.isSsnValid('010280X246u'), true) + assert.equal(validation.isSsnValid('010280W246u'), true) + assert.equal(validation.isSsnValid('010280V246u'), true) + assert.equal(validation.isSsnValid('010280U246u'), true) + assert.equal(validation.isSsnValid('010280G246u'), false) + assert.equal(validation.isSsnValid('010280y246u'), true) + assert.equal(validation.isSsnValid('010280x246u'), true) + assert.equal(validation.isSsnValid('010280w246u'), true) + assert.equal(validation.isSsnValid('010280v246u'), true) + assert.equal(validation.isSsnValid('010280u246u'), true) + assert.equal(validation.isSsnValid('010280g246u'), false) + }) - test('accepts ssn from 21st century', () => { - assert.equal(validation.isSsnValid('010103A2465'), true) - assert.equal(validation.isSsnValid('010103B2465'), true) - assert.equal(validation.isSsnValid('010103C2465'), true) - assert.equal(validation.isSsnValid('010103D2465'), true) - assert.equal(validation.isSsnValid('010103E2465'), true) - assert.equal(validation.isSsnValid('010103F2465'), true) - assert.equal(validation.isSsnValid('010103a2465'), true) - assert.equal(validation.isSsnValid('010103b2465'), true) - assert.equal(validation.isSsnValid('010103c2465'), true) - assert.equal(validation.isSsnValid('010103d2465'), true) - assert.equal(validation.isSsnValid('010103e2465'), true) - assert.equal(validation.isSsnValid('010103f2465'), true) - assert.equal(validation.isSsnValid('010103Z2465'), false) - assert.equal(validation.isSsnValid('010103z2465'), false) - }) + test('accepts ssn from 21st century', () => { + assert.equal(validation.isSsnValid('010103A2465'), true) + assert.equal(validation.isSsnValid('010103B2465'), true) + assert.equal(validation.isSsnValid('010103C2465'), true) + assert.equal(validation.isSsnValid('010103D2465'), true) + assert.equal(validation.isSsnValid('010103E2465'), true) + assert.equal(validation.isSsnValid('010103F2465'), true) + assert.equal(validation.isSsnValid('010103a2465'), true) + assert.equal(validation.isSsnValid('010103b2465'), true) + assert.equal(validation.isSsnValid('010103c2465'), true) + assert.equal(validation.isSsnValid('010103d2465'), true) + assert.equal(validation.isSsnValid('010103e2465'), true) + assert.equal(validation.isSsnValid('010103f2465'), true) + assert.equal(validation.isSsnValid('010103Z2465'), false) + assert.equal(validation.isSsnValid('010103z2465'), false) + }) - test('accepts when check char is a number', () => { - assert.equal(validation.isSsnValid('130672-4400'), true) - }) + test('accepts when check char is a number', () => { + assert.equal(validation.isSsnValid('130672-4400'), true) + }) - test('accepts ssn with leading and trailing whitespace ', () => { - assert.equal(validation.isSsnValid(' 130672-4400\t\n'), true) - }) + test('accepts ssn with leading and trailing whitespace ', () => { + assert.equal(validation.isSsnValid(' 130672-4400\t\n'), true) + }) - test('rejects when check char is wrong', () => { - assert.equal(validation.isSsnValid('010601A010W'), false) - }) + test('rejects when check char is wrong', () => { + assert.equal(validation.isSsnValid('010601A010W'), false) + }) - test('rejects when check char is missing', () => { - assert.equal(validation.isSsnValid('010601A010'), false) - }) + test('rejects when check char is missing', () => { + assert.equal(validation.isSsnValid('010601A010'), false) + }) - test('rejects when century separator is missing', () => { - assert.equal(validation.isSsnValid('0106010102'), false) - }) + test('rejects when century separator is missing', () => { + assert.equal(validation.isSsnValid('0106010102'), false) + }) - test('rejects when century separator is invalid', () => { - assert.equal(validation.isSsnValid('0907184X22N'), false) - assert.equal(validation.isSsnValid('0907184x22N'), false) - }) + test('rejects when century separator is invalid', () => { + assert.equal(validation.isSsnValid('0907184X22N'), false) + assert.equal(validation.isSsnValid('0907184x22N'), false) + }) - test('rejects when there are extraneous alphabetical characters', () => { - assert.equal(validation.isSsnValid('020698-01IS'), false) - }) + test('rejects when there are extraneous alphabetical characters', () => { + assert.equal(validation.isSsnValid('020698-01IS'), false) + }) - test('rejects when ssn is too long', () => { - assert.equal(validation.isSsnValid('130672-44001'), false) - }) + test('rejects when ssn is too long', () => { + assert.equal(validation.isSsnValid('130672-44001'), false) + }) - test('rejects when ssn is too short', () => { - assert.equal(validation.isSsnValid('130672-44'), false) - }) + test('rejects when ssn is too short', () => { + assert.equal(validation.isSsnValid('130672-44'), false) + }) - test('rejects when ssn length is valid but checksum char is whitespace', () => { - assert.equal(validation.isSsnValid('130672-44 '), false) - }) + test('rejects when ssn length is valid but checksum char is whitespace', () => { + assert.equal(validation.isSsnValid('130672-44 '), false) + }) - test('accepts 010101-0101 as special case', () => { - assert.equal(validation.isSsnValid('010101-0101'), true) - assert.equal(validation.isSsnValid('010101-0101', true), true) - }) + test('accepts 010101-0101 as special case', () => { + assert.equal(validation.isSsnValid('010101-0101'), true) + assert.equal(validation.isSsnValid('010101-0101', true), true) + }) - test('accepts when person is over hundred years old, and age sanity check is off', () => { - assert.equal(validation.isSsnValid('100417-458U'), true) - assert.equal(validation.isSsnValid('231194+246w'), true) - }) + test('accepts when person is over hundred years old, and age sanity check is off', () => { + assert.equal(validation.isSsnValid('100417-458U'), true) + assert.equal(validation.isSsnValid('231194+246w'), true) + }) - test('rejects when person is over hundred years old, and age sanity check is on', () => { - 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('rejects when person is over hundred years old, and age sanity check is on', () => { + 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', () => { - assert.equal(validation.isSsnValid('071144A239E'), true) - }) + test('accepts person not yet born when age sanity check is off', () => { + assert.equal(validation.isSsnValid('071144A239E'), true) + }) - test('rejects person not yet born when age sanity check is on', () => { - assert.equal(validation.isSsnValid('071144A239E', true), false) + test('rejects person not yet born when age sanity check is on', () => { + assert.equal(validation.isSsnValid('071144A239E', true), false) + }) + + test('Accepts an artificial ssn (last part starts with 9)', () => { + assert.equal(validation.isSsnValid('010697-9184'), true) + }) + + test('Accepts a special YTL-defined fake ssn (checkCode calculation is skipped)', () => { + assert.equal(validation.isSsnValid('210999-U097'), true) + }) }) - test('Accepts an artificial ssn (last part starts with 9)', () => { - assert.equal(validation.isSsnValid('010697-9184'), true) + describe('isFakeYtlSsn', () => { + test('detects "A" century seperator fake ssn', () => assert.equal(validation.isFakeYtlSsn('200102AU000'), true)) + + test('detects "-" century seperator fake ssn', () => assert.equal(validation.isFakeYtlSsn('200192-U000'), true)) + + test('rejects valid SSN', () => assert.equal(validation.isFakeYtlSsn('231194-246W'), false)) }) - test('Accepts a special YTL-defined fake ssn (checkCode calculation is skipped)', () => { - assert.equal(validation.isSsnValid('210999-U097'), true) + describe('isValidEmail', () => { + test('Rejects invalid student email', () => { + const invalidEmails = [ + 'testuserexample.com', + 'testuserexample.com...', + 'tesdkjgqjeklqjwköejökqwjegökljeqwefiqewifqhweufhqowiuhfoqiuwehfoiuqwhefoiuqwhefoiuqhweofiuhqwoiuehfoqiuwehfouiqwehfoiuqwhefoiuqhweoiufhqwouiefhoqiuwehfoiquwehfoiuqhweiufhqwoieufhoqiuwehfoiuqhweouuiuweiqutqiopwuetiouqwepiotuqpwioeutpqwoeutpoqiwuetpoqiwuetpoquweodhlskjhfajdhlajsdhgituqwpoefiht@userexample.com', + 'erkki.emptysubdomain@.foo.bar', + 'test.user@@example.com', + 'test..user@example.com', + 'test.user@', + '.test.user@example.com', + ';test.user@example.com', + 'john.käärä@example.com', + {} as string + ] + + for (const email of invalidEmails) { + assert.equal(validation.isValidEmail(email), false) + } + }) + + test('accepts valid email', () => { + const validEmails = ['', 'a@a.com', 'test@example.com'] + + for (const email of validEmails) { + assert.equal(validation.isValidEmail(email), true) + } + }) }) }) - describe('ssn schemas', () => { // Over hundred years old: valid without age checks, rejected with them const over100 = ' \t231194+246W\n ' @@ -184,63 +223,25 @@ describe('validation-test', () => { assert.equal(result.error?.issues[0].message, 'SSN fails age checks') }) }) - }) - - describe('isFakeYtlSsn', () => { - test('detects "A" century seperator fake ssn', () => assert.equal(validation.isFakeYtlSsn('200102AU000'), true)) - - test('detects "-" century seperator fake ssn', () => assert.equal(validation.isFakeYtlSsn('200192-U000'), true)) - 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 YTL fake SSN') - }) - - test('accepts fake SSN even if U is lowercase', () => { - let result = validation.fakeYtlSsnSchema.safeParse('200102au00a') - assert.equal(result.success, true) - result = validation.fakeYtlSsnSchema.safeParse('200198-u00a') - assert.equal(result.success, true) - }) - }) - - describe('isValidEmail', () => { - test('Rejects invalid student email', () => { - const invalidEmails = [ - 'testuserexample.com', - 'testuserexample.com...', - 'tesdkjgqjeklqjwköejökqwjegökljeqwefiqewifqhweufhqowiuhfoqiuwehfoiuqwhefoiuqwhefoiuqhweofiuhqwoiuehfoqiuwehfouiqwehfoiuqwhefoiuqhweoiufhqwouiefhoqiuwehfoiquwehfoiuqhweiufhqwoieufhoqiuwehfoiuqhweouuiuweiqutqiopwuetiouqwepiotuqpwioeutpqwoeutpoqiwuetpoqiwuetpoquweodhlskjhfajdhlajsdhgituqwpoefiht@userexample.com', - 'erkki.emptysubdomain@.foo.bar', - 'test.user@@example.com', - 'test..user@example.com', - 'test.user@', - '.test.user@example.com', - ';test.user@example.com', - 'john.käärä@example.com', - {} as string - ] - - for (const email of invalidEmails) { - assert.equal(validation.isValidEmail(email), false) - } - }) + describe('fakeYtlSsnSchema', () => { + test('trims and uppercases', () => { + const result = validation.fakeYtlSsnSchema.safeParse(' 200102AU00a\n') + assert.deepEqual(result, { success: true, data: '200102AU00A' }) + }) - test('accepts valid email', () => { - const validEmails = ['', 'a@a.com', 'test@example.com'] + 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 YTL fake SSN') + }) - for (const email of validEmails) { - assert.equal(validation.isValidEmail(email), true) - } + test('accepts fake SSN even if U is lowercase', () => { + let result = validation.fakeYtlSsnSchema.safeParse('200102au00a') + assert.equal(result.success, true) + result = validation.fakeYtlSsnSchema.safeParse('200198-u00a') + assert.equal(result.success, true) + }) }) }) }) From 292a6e8b07c6453def09135269f2ce65e7399e0b Mon Sep 17 00:00:00 2001 From: Github CI Date: Wed, 14 Oct 2026 12:38:34 +0000 Subject: [PATCH 26/26] CI: Release @digabi/validation@4.1.0-feature-ssn-schemas-with-input-trimming.0 with tag feature-ssn-schemas-with-input-trimming --- package-lock.json | 2 +- packages/validation/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5e56915..0be8e99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18735,7 +18735,7 @@ }, "packages/validation": { "name": "@digabi/validation", - "version": "4.0.1-feature-ssn-schemas-with-input-trimming.0", + "version": "4.1.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 0f797ef..402c288 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@digabi/validation", - "version": "4.0.1-feature-ssn-schemas-with-input-trimming.0", + "version": "4.1.0-feature-ssn-schemas-with-input-trimming.0", "author": "Matriculation Examination Board, Finland", "license": "EUPL-1.2", "repository": {