📄 src/isni/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 { IsniSruResult } from './types';
18
19interface ISNIProps extends Record<string, string | null> { isniId: string | null; }
20
21type ISNIApis = {
22 'isni-sru': StandardApiAdapter<ISNIProps, IsniSruResult[]>;
23};
24
25function validateISNIMod11_2(compact: string): ValidationResult {
26 if (compact.length !== 16) return { valid: false, error: `Expected 16 chars, got ${compact.length}` };
27 let p = 0;
28 for (let i = 0; i < 15; i++) {
29 const d = parseInt(compact[i], 10);
30 if (isNaN(d)) return { valid: false, error: `Non-digit character at position ${i}` };
31 p = ((p + d) * 2) % 11;
32 }
33 const checkVal = (12 - p) % 11;
34 const checkChar = checkVal === 10 ? 'X' : String(checkVal);
35 if (compact[15].toUpperCase() !== checkChar)
36 return { valid: false, error: `Check digit mismatch: expected ${checkChar}, got ${compact[15]}` };
37 return { valid: true };
38}
39
40export const ISNIAdapter: StandardAdapter<ISNIProps, ISNIApis> = {
41 standardId: 'isni',
42 uriScheme: 'ISNI',
43
44 normalize(value: string): string {
45 return value.replace(/[\s-]/g, '');
46 },
47
48 display(value: string): string {
49 const compact = ISNIAdapter.normalize(value);
50 if (compact.length === 16)
51 return `${compact.slice(0, 4)} ${compact.slice(4, 8)} ${compact.slice(8, 12)} ${compact.slice(12)}`;
52 return compact;
53 },
54
55 validate(value: string | null | undefined): ValidationResult {
56 if (!value) return { valid: false, error: 'ISNI is absent' };
57 const compact = ISNIAdapter.normalize(value);
58 if (!/^[0-9]{15}[0-9X]$/.test(compact))
59 return { valid: false, error: `ISNI does not match pattern: ${compact}` };
60 return validateISNIMod11_2(compact);
61 },
62
63 toLinkedDataUri(value: string): string {
64 return `https://isni.org/isni/${ISNIAdapter.normalize(value)}`;
65 },
66
67 apis: {
68 'isni-sru': {
69 apiId: 'isni-sru',
70 name: 'ISNI SRU API',
71 async fetch({ isniId }): Promise<IsniSruResult[]> {
72 if (!isniId) return [];
73 const query = `pica.isn%3D${encodeURIComponent(isniId)}`;
74 const url = `https://isni.oclc.org/sru/DB=1.2/?operation=searchRetrieve&query=${query}&recordSchema=isni-b&maximumRecords=10`;
75 const res = await fetch(url, { headers: { Accept: 'application/xml' } });
76 if (!res.ok) return [];
77 const text = await res.text();
78 const isniMatch = text.match(/<isniUnformatted>([0-9X]+)<\/isniUnformatted>/);
79 const forenameMatch = text.match(/<forename>([^<]+)<\/forename>/);
80 const surnameMatch = text.match(/<surname>([^<]+)<\/surname>/);
81 if (!isniMatch) return [];
82 return [{
83 isni: isniMatch[1],
84 forename: forenameMatch?.[1],
85 surname: surnameMatch?.[1],
86 }];
87 },
88 },
89 },
90};
91