📄 src/orcid/index.ts
D-OPEN SOVEREIGN
1/*
2 * Copyright (c) 2026 DataPond D-Library Pty Ltd. ("The Sanctuary")
3 * Non-Profit Public Library — The World Library
4 *
5 * D-SAFE Certification issued by POND Enterprise.
6 * Published under the D-Open Code Sovereign Licence v1.0 framework
7 * DataPond D-Library All exclusive Right reserved on modifiying the code - CC Attribution to data pond, Non modifiable, Non Commercial.
8 * https://registry.world.bibliotech.com/licence
9 * Source code donated to DataPond D-Library by it's author: data pond.
10 *
11 * Technical Guardian ("The Shield"): Pond Enterprise Pty Ltd. (ACN 694 747 987)
12 * All liability claims about D-SAFE certification issues coming from the published D-Safe direction must be addressed with Pond Enterprise Pty Ltd.
13 * Code Modification automatically voids the certified liability protection provided by Pond Enterprise.
14 */
15import type { StandardAdapter, StandardApiAdapter, ValidationResult }
16 from '@the_library/canonical-schemas/types/StandardAdapter';
17import type { OrcidPublicRecordResult, OrcidPublicSearchResult } from './types';
18
19interface ORCIDProps extends Record<string, string | null> { orcidId: string | null; }
20
21type ORCIDApis = {
22 'orcid-lookup': StandardApiAdapter<ORCIDProps, OrcidPublicRecordResult>;
23 'orcid-public-api': StandardApiAdapter<ORCIDProps, OrcidPublicSearchResult[]>;
24};
25
26function validateORCIDMod11_2(compact: string): ValidationResult {
27 if (compact.length !== 16) return { valid: false, error: `Expected 16 chars, got ${compact.length}` };
28 let p = 0;
29 for (let i = 0; i < 15; i++) {
30 const d = parseInt(compact[i], 10);
31 if (isNaN(d)) return { valid: false, error: `Non-digit at position ${i}` };
32 p = ((p + d) * 2) % 11;
33 }
34 const checkVal = (12 - p) % 11;
35 const checkChar = checkVal === 10 ? 'X' : String(checkVal);
36 if (compact[15].toUpperCase() !== checkChar)
37 return { valid: false, error: `Check digit mismatch: expected ${checkChar}, got ${compact[15]}` };
38 return { valid: true };
39}
40
41const ORCID_API_BASE = 'https://pub.orcid.org/v3.0';
42const ORCID_HEADERS = { Accept: 'application/vnd.orcid+json' };
43
44export const ORCIDAdapter: StandardAdapter<ORCIDProps, ORCIDApis> = {
45 standardId: 'orcid',
46 uriScheme: 'ORCID',
47
48 normalize(value: string): string {
49 const clean = value.replace(/\s/g, '');
50 if (clean.length === 16 && !clean.includes('-'))
51 return `${clean.slice(0, 4)}-${clean.slice(4, 8)}-${clean.slice(8, 12)}-${clean.slice(12)}`;
52 return clean;
53 },
54
55 display(value: string): string {
56 return ORCIDAdapter.normalize(value);
57 },
58
59 validate(value: string | null | undefined): ValidationResult {
60 if (!value) return { valid: false, error: 'ORCID iD is absent' };
61 const normalized = ORCIDAdapter.normalize(value);
62 if (!/^[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]$/.test(normalized))
63 return { valid: false, error: `ORCID iD does not match pattern: ${normalized}` };
64 return validateORCIDMod11_2(normalized.replace(/-/g, ''));
65 },
66
67 toLinkedDataUri(value: string): string {
68 return `https://orcid.org/${ORCIDAdapter.normalize(value)}`;
69 },
70
71 apis: {
72 'orcid-lookup': {
73 apiId: 'orcid-lookup',
74 name: 'ORCID Public Record API',
75 async fetch({ orcidId }): Promise<OrcidPublicRecordResult> {
76 if (!orcidId) return {};
77 const res = await fetch(`${ORCID_API_BASE}/${orcidId}/record`, { headers: ORCID_HEADERS });
78 if (!res.ok) return {};
79 const data = await res.json() as Record<string, unknown>;
80 const person = (data.person ?? {}) as Record<string, unknown>;
81 const name = (person.name ?? {}) as Record<string, unknown>;
82 return {
83 orcidId,
84 givenNames: (name['given-names'] as { value?: string } | undefined)?.value,
85 familyName: (name['family-name'] as { value?: string } | undefined)?.value,
86 biography: (person.biography as { content?: string } | undefined)?.content ?? undefined,
87 };
88 },
89 },
90 'orcid-public-api': {
91 apiId: 'orcid-public-api',
92 name: 'ORCID Public Search API',
93 async fetch({ orcidId }): Promise<OrcidPublicSearchResult[]> {
94 if (!orcidId) return [];
95 const res = await fetch(
96 `${ORCID_API_BASE}/search?q=orcid:${orcidId}&rows=10`,
97 { headers: ORCID_HEADERS }
98 );
99 if (!res.ok) return [];
100 const data = await res.json() as { result?: unknown[] };
101 return (data.result ?? [])
102 .map((r: unknown) => {
103 const item = r as Record<string, unknown>;
104 const oid = (item['orcid-identifier'] as Record<string, unknown> | undefined)?.path as string;
105 return { orcidId: oid ?? '' };
106 })
107 .filter(r => r.orcidId);
108 },
109 },
110 },
111};
112