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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/orcid-account/orcidAccountResolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const orcidAccountResolvers = {
): Promise<ResolverOrcidAccount | null> => {
if (!isOrcidId(id)) {
throw new Error(
`Invalid ORCID identifier: '${id}'. Expected format: 0000-0000-0000-000X`,
`Invalid ORCID identifier: '${id}'. See https://support.orcid.org/hc/en-us/articles/360006897674-Structure-of-the-ORCID-Identifier.`,
);
}

Expand Down
54 changes: 49 additions & 5 deletions src/utils/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,9 +293,53 @@ export function isAccountId(id: string): boolean {
);
}

// ORCID
export function isOrcidId(id: string): boolean {
// ORCID ID format: 0000-0000-0000-000X where X can be 0-9 or X
const orcidPattern = /^\d{4}-\d{4}-\d{4}-\d{3}[\dX]$/;
return orcidPattern.test(id);
/**
* Validates an ORCID iD.
*
* An ORCID iD is a 16-character string that follows a specific structure:
* - It consists of 15 digits followed by a check digit (0-9 or 'X').
* - It is often formatted with hyphens, e.g., "0000-0002-1825-0097".
* - The validation uses the ISO 7064 11,2 checksum algorithm.
*
* @param {string} orcid The ORCID iD string to validate.
* @returns {boolean} True if the ORCID iD is valid, false otherwise.
*/
export function isOrcidId(orcidId: string): boolean {
if (typeof orcidId !== 'string') {
return false;
}

// Remove hyphens and whitespace to get the base 16 characters.
const baseStr: string = orcidId.replace(/[-\s]/g, '');

// An ORCID must be 16 characters long and match the pattern:
// 15 digits followed by a final character that is a digit or 'X'.
const orcidPattern: RegExp = /^\d{15}[\dX]$/;
if (!orcidPattern.test(baseStr.toUpperCase())) {
return false;
}

// --- Checksum Calculation (ISO 7064 11,2) ---

let total: number = 0;
// Iterate over the first 15 digits of the ORCID.
for (let i = 0; i < 15; i++) {
const digit: number = parseInt(baseStr[i], 10);
total = (total + digit) * 2;
}

// Calculate the remainder when divided by 11.
const remainder: number = total % 11;
// Subtract the remainder from 12.
const result: number = (12 - remainder) % 11;

// Determine the correct check digit from the result.
// If the result is 10, the check digit is 'X'. Otherwise, it's the digit itself.
const calculatedCheckDigit: string = result === 10 ? 'X' : String(result);

// Get the actual check digit from the input string.
const actualCheckDigit: string = baseStr.charAt(15).toUpperCase();

// Compare the calculated check digit with the actual one.
return calculatedCheckDigit === actualCheckDigit;
}
9 changes: 4 additions & 5 deletions tests/utils/assert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@ import { isOrcidId } from '../../src/utils/assert';
describe('isOrcidId', () => {
test('should return true for valid ORCID IDs', () => {
// Valid ORCID ID patterns
expect(isOrcidId('0000-0000-0000-0000')).toBe(true);
expect(isOrcidId('0000-0000-0000-000X')).toBe(true);
expect(isOrcidId('1234-5678-9012-3456')).toBe(true);
expect(isOrcidId('0000-0002-1825-009X')).toBe(true);
expect(isOrcidId('0000-0003-1527-0030')).toBe(true);
expect(isOrcidId('0009-0007-1106-8413')).toBe(true);
expect(isOrcidId('0000-0002-9079-593X')).toBe(true);
});

test('should return false for invalid ORCID IDs', () => {
Expand All @@ -31,5 +28,7 @@ describe('isOrcidId', () => {
expect(isOrcidId('')).toBe(false); // Empty string
expect(isOrcidId('abc-def-ghi-jkl')).toBe(false); // Letters instead of digits
expect(isOrcidId('0000-0000-0000-000@')).toBe(false); // Invalid special character
expect(isOrcidId('0000-0001-5109-370X')).toBe(false); // Incorrect X position
expect(isOrcidId('0000-0002-1825-0098')).toBe(false); // Fails checksum
});
});