📄 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