@de-otio/bibcheck 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/dist/cache/fs-cache.d.ts +55 -0
  4. package/dist/cache/fs-cache.d.ts.map +1 -0
  5. package/dist/cache/fs-cache.js +264 -0
  6. package/dist/cache/fs-cache.js.map +1 -0
  7. package/dist/canonical.d.ts +29 -0
  8. package/dist/canonical.d.ts.map +1 -0
  9. package/dist/canonical.js +132 -0
  10. package/dist/canonical.js.map +1 -0
  11. package/dist/check.d.ts +140 -0
  12. package/dist/check.d.ts.map +1 -0
  13. package/dist/check.js +646 -0
  14. package/dist/check.js.map +1 -0
  15. package/dist/cli.d.ts +19 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +357 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/config.d.ts +175 -0
  20. package/dist/config.d.ts.map +1 -0
  21. package/dist/config.js +180 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/databases/crossref.d.ts +53 -0
  24. package/dist/databases/crossref.d.ts.map +1 -0
  25. package/dist/databases/crossref.js +138 -0
  26. package/dist/databases/crossref.js.map +1 -0
  27. package/dist/databases/index.d.ts +12 -0
  28. package/dist/databases/index.d.ts.map +1 -0
  29. package/dist/databases/index.js +9 -0
  30. package/dist/databases/index.js.map +1 -0
  31. package/dist/databases/openalex.d.ts +29 -0
  32. package/dist/databases/openalex.d.ts.map +1 -0
  33. package/dist/databases/openalex.js +117 -0
  34. package/dist/databases/openalex.js.map +1 -0
  35. package/dist/databases/openlibrary.d.ts +26 -0
  36. package/dist/databases/openlibrary.d.ts.map +1 -0
  37. package/dist/databases/openlibrary.js +79 -0
  38. package/dist/databases/openlibrary.js.map +1 -0
  39. package/dist/databases/worldcat.d.ts +33 -0
  40. package/dist/databases/worldcat.d.ts.map +1 -0
  41. package/dist/databases/worldcat.js +145 -0
  42. package/dist/databases/worldcat.js.map +1 -0
  43. package/dist/doctor.d.ts +44 -0
  44. package/dist/doctor.d.ts.map +1 -0
  45. package/dist/doctor.js +386 -0
  46. package/dist/doctor.js.map +1 -0
  47. package/dist/existence.d.ts +70 -0
  48. package/dist/existence.d.ts.map +1 -0
  49. package/dist/existence.js +308 -0
  50. package/dist/existence.js.map +1 -0
  51. package/dist/http.d.ts +97 -0
  52. package/dist/http.d.ts.map +1 -0
  53. package/dist/http.js +543 -0
  54. package/dist/http.js.map +1 -0
  55. package/dist/identifiers.d.ts +44 -0
  56. package/dist/identifiers.d.ts.map +1 -0
  57. package/dist/identifiers.js +111 -0
  58. package/dist/identifiers.js.map +1 -0
  59. package/dist/index.d.ts +9 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +8 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/linkage.d.ts +29 -0
  64. package/dist/linkage.d.ts.map +1 -0
  65. package/dist/linkage.js +73 -0
  66. package/dist/linkage.js.map +1 -0
  67. package/dist/markdown/blocks.d.ts +19 -0
  68. package/dist/markdown/blocks.d.ts.map +1 -0
  69. package/dist/markdown/blocks.js +69 -0
  70. package/dist/markdown/blocks.js.map +1 -0
  71. package/dist/markdown/citekeys.d.ts +22 -0
  72. package/dist/markdown/citekeys.d.ts.map +1 -0
  73. package/dist/markdown/citekeys.js +100 -0
  74. package/dist/markdown/citekeys.js.map +1 -0
  75. package/dist/markdown/glob.d.ts +18 -0
  76. package/dist/markdown/glob.d.ts.map +1 -0
  77. package/dist/markdown/glob.js +26 -0
  78. package/dist/markdown/glob.js.map +1 -0
  79. package/dist/markdown/prose.d.ts +19 -0
  80. package/dist/markdown/prose.d.ts.map +1 -0
  81. package/dist/markdown/prose.js +81 -0
  82. package/dist/markdown/prose.js.map +1 -0
  83. package/dist/output/json.d.ts +21 -0
  84. package/dist/output/json.d.ts.map +1 -0
  85. package/dist/output/json.js +24 -0
  86. package/dist/output/json.js.map +1 -0
  87. package/dist/output/markdown.d.ts +21 -0
  88. package/dist/output/markdown.d.ts.map +1 -0
  89. package/dist/output/markdown.js +194 -0
  90. package/dist/output/markdown.js.map +1 -0
  91. package/dist/output/sarif.d.ts +31 -0
  92. package/dist/output/sarif.d.ts.map +1 -0
  93. package/dist/output/sarif.js +322 -0
  94. package/dist/output/sarif.js.map +1 -0
  95. package/dist/output/text.d.ts +27 -0
  96. package/dist/output/text.d.ts.map +1 -0
  97. package/dist/output/text.js +212 -0
  98. package/dist/output/text.js.map +1 -0
  99. package/dist/phrases/load.d.ts +34 -0
  100. package/dist/phrases/load.d.ts.map +1 -0
  101. package/dist/phrases/load.js +148 -0
  102. package/dist/phrases/load.js.map +1 -0
  103. package/dist/phrases.d.ts +27 -0
  104. package/dist/phrases.d.ts.map +1 -0
  105. package/dist/phrases.js +116 -0
  106. package/dist/phrases.js.map +1 -0
  107. package/dist/schema/csl.d.ts +429 -0
  108. package/dist/schema/csl.d.ts.map +1 -0
  109. package/dist/schema/csl.js +101 -0
  110. package/dist/schema/csl.js.map +1 -0
  111. package/dist/schema/output.d.ts +1116 -0
  112. package/dist/schema/output.d.ts.map +1 -0
  113. package/dist/schema/output.js +419 -0
  114. package/dist/schema/output.js.map +1 -0
  115. package/dist/suppression.d.ts +106 -0
  116. package/dist/suppression.d.ts.map +1 -0
  117. package/dist/suppression.js +134 -0
  118. package/dist/suppression.js.map +1 -0
  119. package/dist/version.d.ts +11 -0
  120. package/dist/version.d.ts.map +1 -0
  121. package/dist/version.js +14 -0
  122. package/dist/version.js.map +1 -0
  123. package/dist/worklist.d.ts +32 -0
  124. package/dist/worklist.d.ts.map +1 -0
  125. package/dist/worklist.js +211 -0
  126. package/dist/worklist.js.map +1 -0
  127. package/package.json +82 -0
package/dist/config.js ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * bibcheck configuration schema and loader.
3
+ *
4
+ * Reads `bibcheck.toml` from the project root, validates it against the
5
+ * Zod schema, and returns a frozen typed Config. Returns defaults when no
6
+ * config file is present.
7
+ */
8
+ import { z } from 'zod';
9
+ import { parse as parseToml } from 'smol-toml';
10
+ import { readFile, access } from 'node:fs/promises';
11
+ import { join } from 'node:path';
12
+ // ---------------------------------------------------------------------------
13
+ // Schema sections
14
+ // ---------------------------------------------------------------------------
15
+ const BibliographySchema = z.object({
16
+ file: z.string().default('docs/sources.json'),
17
+ });
18
+ const DocsSchema = z.object({
19
+ include: z.array(z.string()).default(['docs/**/*.md']),
20
+ exclude: z.array(z.string()).default([]),
21
+ });
22
+ const TrustedHostsSchema = z.object({
23
+ hosts: z.array(z.string()).default([
24
+ 'hathitrust.org',
25
+ 'archive.org',
26
+ 'oll.libertyfund.org',
27
+ 'plato.stanford.edu',
28
+ 'philpapers.org',
29
+ 'loc.gov',
30
+ 'dnb.de',
31
+ 'bnf.fr',
32
+ ]),
33
+ });
34
+ const PhrasesSchema = z.object({
35
+ file: z.string().nullable().default(null),
36
+ });
37
+ const SourceTypeEntrySchema = z.object({
38
+ warn_load_bearing: z.boolean().optional(),
39
+ allow_load_bearing: z.boolean().optional(),
40
+ /**
41
+ * T23 — source-type gating rule. When `false`, a `not-found-in-databases`
42
+ * (unverifiable-absence) result for an entry of this CSL type does NOT gate
43
+ * `bibcheck check` — e.g. a pre-DOI manuscript / archival source for which no
44
+ * DOI was ever expected. The finding is still reported (informational), never
45
+ * dropped. Omitted / `true` ⇒ the Q1 secure default (gate). Governs only the
46
+ * not-found gate; malformed identifiers, canonical issues, and metadata
47
+ * mismatches always gate (use a per-entry `bibcheck-allow` note for those).
48
+ */
49
+ gate_not_found: z.boolean().optional(),
50
+ });
51
+ const ApisSchema = z.object({
52
+ crossref_mailto: z.string().nullable().default(null),
53
+ openalex_mailto: z.string().nullable().default(null),
54
+ // Base URLs for each bibliographic database. When omitted, each DB client
55
+ // and the doctor connectivity check fall back to the real public endpoint
56
+ // (see API_BASE_DEFAULTS). Overridable (e.g. to a localhost stub) for
57
+ // hermetic testing. Validated as URLs when present; a trailing slash is
58
+ // tolerated (clients strip it). Kept optional so the effective default lives
59
+ // in one place (API_BASE_DEFAULTS) and consumers stay tolerant of an absent
60
+ // value.
61
+ crossref_base: z.string().url().optional(),
62
+ openalex_base: z.string().url().optional(),
63
+ openlibrary_base: z.string().url().optional(),
64
+ });
65
+ /**
66
+ * Effective default base URLs for the bibliographic database APIs. These are
67
+ * the real public endpoints used whenever the corresponding `[apis] *_base`
68
+ * config field is omitted. (WorldCat / OCLC Classify was removed in 0.2.0.)
69
+ */
70
+ export const API_BASE_DEFAULTS = {
71
+ crossref: 'https://api.crossref.org',
72
+ openalex: 'https://api.openalex.org',
73
+ openlibrary: 'https://openlibrary.org',
74
+ };
75
+ const CacheSchema = z.object({
76
+ dir: z.string().default('.bibcheck-cache'),
77
+ max_size_mb: z.number().nullable().default(256),
78
+ });
79
+ // ---------------------------------------------------------------------------
80
+ // Top-level schema
81
+ // ---------------------------------------------------------------------------
82
+ export const ConfigSchema = z.object({
83
+ bibliography: BibliographySchema.default({}),
84
+ docs: DocsSchema.default({}),
85
+ trusted_hosts: TrustedHostsSchema.default({}),
86
+ phrases: PhrasesSchema.default({}),
87
+ source_types: z.record(z.string(), SourceTypeEntrySchema).default({}),
88
+ edition_discipline: z.record(z.string(), z.string()).default({}),
89
+ apis: ApisSchema.default({}),
90
+ cache: CacheSchema.default({}),
91
+ });
92
+ // ---------------------------------------------------------------------------
93
+ // Error class
94
+ // ---------------------------------------------------------------------------
95
+ export class ConfigError extends Error {
96
+ constructor(message) {
97
+ super(message);
98
+ this.name = 'ConfigError';
99
+ if (Error.captureStackTrace) {
100
+ Error.captureStackTrace(this, ConfigError);
101
+ }
102
+ }
103
+ }
104
+ // ---------------------------------------------------------------------------
105
+ // Prototype-pollution guard
106
+ // ---------------------------------------------------------------------------
107
+ const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
108
+ function checkPollution(node, path) {
109
+ if (node === null || typeof node !== 'object')
110
+ return;
111
+ for (const key of Object.keys(node)) {
112
+ const childPath = path ? `${path}.${key}` : key;
113
+ if (DANGEROUS_KEYS.has(key)) {
114
+ throw new ConfigError(`Prototype pollution attempt: ${childPath}`);
115
+ }
116
+ checkPollution(node[key], childPath);
117
+ }
118
+ }
119
+ // ---------------------------------------------------------------------------
120
+ // Deep freeze helper
121
+ // ---------------------------------------------------------------------------
122
+ function deepFreeze(value) {
123
+ if (value === null || typeof value !== 'object')
124
+ return value;
125
+ Object.freeze(value);
126
+ for (const v of Object.values(value)) {
127
+ deepFreeze(v);
128
+ }
129
+ return value;
130
+ }
131
+ export async function loadConfig(opts) {
132
+ const cwd = opts?.cwd ?? process.cwd();
133
+ const configPath = opts?.path ? join(cwd, opts.path) : join(cwd, 'bibcheck.toml');
134
+ let raw;
135
+ try {
136
+ await access(configPath);
137
+ }
138
+ catch {
139
+ // File does not exist — return defaults
140
+ return deepFreeze(ConfigSchema.parse({}));
141
+ }
142
+ let contents;
143
+ try {
144
+ contents = await readFile(configPath, 'utf-8');
145
+ }
146
+ catch (err) {
147
+ const message = err instanceof Error ? err.message : String(err);
148
+ throw new ConfigError(`Failed to read config file: ${message}`);
149
+ }
150
+ // Empty file is valid — treat as all-defaults
151
+ if (contents.trim() === '') {
152
+ return deepFreeze(ConfigSchema.parse({}));
153
+ }
154
+ try {
155
+ raw = parseToml(contents);
156
+ }
157
+ catch (err) {
158
+ const message = err instanceof Error ? err.message : String(err);
159
+ throw new ConfigError(`TOML parse error in ${configPath}: ${message}`);
160
+ }
161
+ // Prototype-pollution guard
162
+ checkPollution(raw, '');
163
+ try {
164
+ return deepFreeze(ConfigSchema.parse(raw));
165
+ }
166
+ catch (err) {
167
+ if (err instanceof z.ZodError) {
168
+ const first = err.issues[0];
169
+ if (first !== undefined) {
170
+ const fieldPath = first.path.join('.');
171
+ throw new ConfigError(`${fieldPath}: ${first.message}`);
172
+ }
173
+ /* c8 ignore next */
174
+ // unreachable: ZodError always has at least one issue
175
+ throw new ConfigError(`Configuration validation failed: ${err.message}`);
176
+ }
177
+ throw err;
178
+ }
179
+ }
180
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * CrossRef DOI lookup client.
3
+ *
4
+ * Queries https://api.crossref.org/works/<doi> and returns normalised
5
+ * DatabaseLookupResult metadata. Supports the CrossRef polite pool via a
6
+ * ?mailto= query param. Results are cached (default 30-day TTL).
7
+ *
8
+ * The polite-pool email is stripped from any URL fields in the raw response
9
+ * before caching to prevent credentials leaking into evidence output.
10
+ */
11
+ import type { HttpClient } from '../http.js';
12
+ import type { Cache } from '../cache/fs-cache.js';
13
+ export interface DatabaseLookupResult {
14
+ found: boolean;
15
+ metadata: {
16
+ title?: string;
17
+ authors?: string[];
18
+ issued?: number;
19
+ publisher?: string;
20
+ doi?: string;
21
+ isbn?: string;
22
+ url?: string;
23
+ } | null;
24
+ raw: unknown;
25
+ }
26
+ export interface DatabaseClient {
27
+ readonly name: string;
28
+ }
29
+ export interface CrossRefClientOptions {
30
+ http: HttpClient;
31
+ cache: Cache;
32
+ mailto?: string | null;
33
+ /** Base URL for the CrossRef API. Defaults to the public endpoint. */
34
+ baseUrl?: string;
35
+ }
36
+ /** Remove a single trailing slash so path joins are unambiguous. */
37
+ export declare function trimTrailingSlash(url: string): string;
38
+ export interface CrossRefClient extends DatabaseClient {
39
+ readonly name: 'crossref';
40
+ lookupByDoi(doi: string, signal?: AbortSignal): Promise<DatabaseLookupResult>;
41
+ }
42
+ /**
43
+ * Strip the ?mailto= query parameter from a URL string.
44
+ * Returns the original string unchanged if it is not a valid URL.
45
+ */
46
+ export declare function stripMailto(url: string): string;
47
+ /**
48
+ * Recursively walk an unknown value and strip ?mailto= from any string
49
+ * fields that look like URLs. Returns a new value; does not mutate.
50
+ */
51
+ export declare function sanitizeMailto(value: unknown): unknown;
52
+ export declare function createCrossRefClient(opts: CrossRefClientOptions): CrossRefClient;
53
+ //# sourceMappingURL=crossref.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crossref.d.ts","sourceRoot":"","sources":["../../src/databases/crossref.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAOlD,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE;QACR,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,GAAG,IAAI,CAAC;IACT,GAAG,EAAE,OAAO,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAMD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAID,oEAAoE;AACpE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,MAAM,WAAW,cAAe,SAAQ,cAAc;IACpD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAC/E;AAmCD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAY/C;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAmBtD;AAoCD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,qBAAqB,GAAG,cAAc,CA0DhF"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * CrossRef DOI lookup client.
3
+ *
4
+ * Queries https://api.crossref.org/works/<doi> and returns normalised
5
+ * DatabaseLookupResult metadata. Supports the CrossRef polite pool via a
6
+ * ?mailto= query param. Results are cached (default 30-day TTL).
7
+ *
8
+ * The polite-pool email is stripped from any URL fields in the raw response
9
+ * before caching to prevent credentials leaking into evidence output.
10
+ */
11
+ const DEFAULT_CROSSREF_BASE = 'https://api.crossref.org';
12
+ /** Remove a single trailing slash so path joins are unambiguous. */
13
+ export function trimTrailingSlash(url) {
14
+ return url.endsWith('/') ? url.slice(0, -1) : url;
15
+ }
16
+ // ---------------------------------------------------------------------------
17
+ // Helpers
18
+ // ---------------------------------------------------------------------------
19
+ /**
20
+ * Strip the ?mailto= query parameter from a URL string.
21
+ * Returns the original string unchanged if it is not a valid URL.
22
+ */
23
+ export function stripMailto(url) {
24
+ let parsed;
25
+ try {
26
+ parsed = new URL(url);
27
+ }
28
+ catch {
29
+ return url;
30
+ }
31
+ if (parsed.searchParams.has('mailto')) {
32
+ parsed.searchParams.delete('mailto');
33
+ return parsed.toString();
34
+ }
35
+ return url;
36
+ }
37
+ /**
38
+ * Recursively walk an unknown value and strip ?mailto= from any string
39
+ * fields that look like URLs. Returns a new value; does not mutate.
40
+ */
41
+ export function sanitizeMailto(value) {
42
+ if (typeof value === 'string') {
43
+ // Only attempt stripMailto for strings that look like URLs.
44
+ if (value.startsWith('http://') || value.startsWith('https://')) {
45
+ return stripMailto(value);
46
+ }
47
+ return value;
48
+ }
49
+ if (Array.isArray(value)) {
50
+ return value.map(sanitizeMailto);
51
+ }
52
+ if (typeof value === 'object' && value !== null) {
53
+ const result = {};
54
+ for (const [k, v] of Object.entries(value)) {
55
+ result[k] = sanitizeMailto(v);
56
+ }
57
+ return result;
58
+ }
59
+ return value;
60
+ }
61
+ function isCrossRefSuccess(body) {
62
+ if (typeof body !== 'object' || body === null)
63
+ return false;
64
+ const b = body;
65
+ return b['status'] === 'ok' && typeof b['message'] === 'object' && b['message'] !== null;
66
+ }
67
+ function mapMetadata(msg) {
68
+ const authors = msg.author
69
+ ?.map((a) => [a.family, a.given].filter(Boolean).join(', '))
70
+ .filter((s) => s.length > 0);
71
+ // noUncheckedIndexedAccess: date-parts[0] could be undefined on malformed data.
72
+ const dateParts = msg.issued?.['date-parts'];
73
+ const firstPart = dateParts?.[0];
74
+ const year = firstPart?.[0];
75
+ const isbn = msg.ISBN?.[0];
76
+ const title = msg.title?.[0];
77
+ return {
78
+ title,
79
+ authors: authors && authors.length > 0 ? authors : undefined,
80
+ issued: typeof year === 'number' ? year : undefined,
81
+ publisher: msg.publisher,
82
+ doi: msg.DOI,
83
+ isbn,
84
+ url: msg.URL,
85
+ };
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ // Factory
89
+ // ---------------------------------------------------------------------------
90
+ export function createCrossRefClient(opts) {
91
+ const { http, cache, mailto } = opts;
92
+ const base = trimTrailingSlash(opts.baseUrl ?? DEFAULT_CROSSREF_BASE);
93
+ async function lookupByDoi(doi, signal) {
94
+ const cacheKey = `crossref:lookupByDoi:${doi.toLowerCase()}`;
95
+ const cached = await cache.get(cacheKey, signal);
96
+ if (cached !== null) {
97
+ return cached;
98
+ }
99
+ const encoded = encodeURIComponent(doi);
100
+ let url = `${base}/works/${encoded}`;
101
+ if (mailto) {
102
+ url += `?mailto=${encodeURIComponent(mailto)}`;
103
+ }
104
+ let response;
105
+ try {
106
+ response = await http.get(url, { signal });
107
+ }
108
+ catch (err) {
109
+ // Network errors / 5xx after retries — propagate.
110
+ throw err;
111
+ }
112
+ if (response.status === 404) {
113
+ const result = {
114
+ found: false,
115
+ metadata: null,
116
+ raw: sanitizeMailto(response.body),
117
+ };
118
+ return result;
119
+ }
120
+ if (!isCrossRefSuccess(response.body)) {
121
+ throw new Error(`CrossRef: unexpected response body for DOI "${doi}" (status ${response.status})`);
122
+ }
123
+ const sanitizedRaw = sanitizeMailto(response.body);
124
+ const metadata = mapMetadata(response.body.message);
125
+ const result = {
126
+ found: true,
127
+ metadata,
128
+ raw: sanitizedRaw,
129
+ };
130
+ await cache.set(cacheKey, result);
131
+ return result;
132
+ }
133
+ return {
134
+ name: 'crossref',
135
+ lookupByDoi,
136
+ };
137
+ }
138
+ //# sourceMappingURL=crossref.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crossref.js","sourceRoot":"","sources":["../../src/databases/crossref.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Barrel re-export for the bibliographic database clients (CrossRef,
3
+ * OpenAlex, OpenLibrary). WorldCat / OCLC Classify was removed in 0.2.0
4
+ * (decommissioned endpoint; see tmp/design-review/worldcat.md).
5
+ */
6
+ export { createCrossRefClient, stripMailto, sanitizeMailto, } from './crossref.js';
7
+ export type { DatabaseLookupResult, DatabaseClient, CrossRefClientOptions, CrossRefClient, } from './crossref.js';
8
+ export { createOpenAlexClient } from './openalex.js';
9
+ export type { OpenAlexClientOptions, OpenAlexClient } from './openalex.js';
10
+ export { createOpenLibraryClient } from './openlibrary.js';
11
+ export type { OpenLibraryClientOptions, OpenLibraryClient } from './openlibrary.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/databases/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACrB,cAAc,GACf,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,YAAY,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE3E,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,YAAY,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Barrel re-export for the bibliographic database clients (CrossRef,
3
+ * OpenAlex, OpenLibrary). WorldCat / OCLC Classify was removed in 0.2.0
4
+ * (decommissioned endpoint; see tmp/design-review/worldcat.md).
5
+ */
6
+ export { createCrossRefClient, stripMailto, sanitizeMailto, } from './crossref.js';
7
+ export { createOpenAlexClient } from './openalex.js';
8
+ export { createOpenLibraryClient } from './openlibrary.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/databases/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,cAAc,GACf,MAAM,eAAe,CAAC;AAQvB,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAGrD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * OpenAlex works client.
3
+ *
4
+ * Supports DOI lookup and title+author search against the OpenAlex API
5
+ * (https://api.openalex.org/works). Polite pool is engaged via ?mailto=.
6
+ * Results are cached at default TTL (30 days).
7
+ *
8
+ * Sanitization: ?mailto= is stripped from any URL strings in the raw response.
9
+ */
10
+ import type { HttpClient } from '../http.js';
11
+ import type { Cache } from '../cache/fs-cache.js';
12
+ import type { DatabaseLookupResult, DatabaseClient } from './crossref.js';
13
+ import { stripMailto } from './crossref.js';
14
+ export type { DatabaseLookupResult, DatabaseClient };
15
+ export interface OpenAlexClientOptions {
16
+ http: HttpClient;
17
+ cache: Cache;
18
+ mailto?: string | null;
19
+ /** Base URL for the OpenAlex API. Defaults to the public endpoint. */
20
+ baseUrl?: string;
21
+ }
22
+ export interface OpenAlexClient extends DatabaseClient {
23
+ readonly name: 'openalex';
24
+ searchByTitleAuthor(title: string, authors: string[], signal?: AbortSignal): Promise<DatabaseLookupResult>;
25
+ lookupByDoi(doi: string, signal?: AbortSignal): Promise<DatabaseLookupResult>;
26
+ }
27
+ export { stripMailto };
28
+ export declare function createOpenAlexClient(opts: OpenAlexClientOptions): OpenAlexClient;
29
+ //# sourceMappingURL=openalex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openalex.d.ts","sourceRoot":"","sources":["../../src/databases/openalex.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAqC,MAAM,eAAe,CAAC;AAG/E,YAAY,EAAE,oBAAoB,EAAE,cAAc,EAAE,CAAC;AAMrD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAID,MAAM,WAAW,cAAe,SAAQ,cAAc;IACpD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,mBAAmB,CACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,EACjB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACjC,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAC/E;AA6BD,OAAO,EAAE,WAAW,EAAE,CAAC;AAwCvB,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,qBAAqB,GAAG,cAAc,CAuFhF"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * OpenAlex works client.
3
+ *
4
+ * Supports DOI lookup and title+author search against the OpenAlex API
5
+ * (https://api.openalex.org/works). Polite pool is engaged via ?mailto=.
6
+ * Results are cached at default TTL (30 days).
7
+ *
8
+ * Sanitization: ?mailto= is stripped from any URL strings in the raw response.
9
+ */
10
+ import { stripMailto, sanitizeMailto, trimTrailingSlash } from './crossref.js';
11
+ const DEFAULT_OPENALEX_BASE = 'https://api.openalex.org';
12
+ // ---------------------------------------------------------------------------
13
+ // Helpers
14
+ // ---------------------------------------------------------------------------
15
+ // Re-export stripMailto for tests.
16
+ export { stripMailto };
17
+ function sanitizeRaw(value) {
18
+ return sanitizeMailto(value);
19
+ }
20
+ function isOpenAlexWork(body) {
21
+ if (typeof body !== 'object' || body === null)
22
+ return false;
23
+ return true;
24
+ }
25
+ function isOpenAlexSearchResponse(body) {
26
+ if (typeof body !== 'object' || body === null)
27
+ return false;
28
+ return true;
29
+ }
30
+ function mapWorkMetadata(work) {
31
+ const authors = work.authorships
32
+ ?.map((a) => a.author?.display_name ?? '')
33
+ .filter((n) => n.length > 0);
34
+ return {
35
+ title: work.display_name,
36
+ authors: authors && authors.length > 0 ? authors : undefined,
37
+ issued: typeof work.publication_year === 'number' ? work.publication_year : undefined,
38
+ doi: work.doi,
39
+ url: work.id,
40
+ };
41
+ }
42
+ function appendMailto(url, mailto) {
43
+ if (!mailto)
44
+ return url;
45
+ const separator = url.includes('?') ? '&' : '?';
46
+ return `${url}${separator}mailto=${encodeURIComponent(mailto)}`;
47
+ }
48
+ // ---------------------------------------------------------------------------
49
+ // Factory
50
+ // ---------------------------------------------------------------------------
51
+ export function createOpenAlexClient(opts) {
52
+ const { http, cache, mailto } = opts;
53
+ const base = trimTrailingSlash(opts.baseUrl ?? DEFAULT_OPENALEX_BASE);
54
+ async function lookupByDoi(doi, signal) {
55
+ const cacheKey = `openalex:lookupByDoi:${doi.toLowerCase()}`;
56
+ const cached = await cache.get(cacheKey, signal);
57
+ if (cached !== null) {
58
+ return cached;
59
+ }
60
+ const encoded = encodeURIComponent(doi);
61
+ const baseUrl = `${base}/works/doi:${encoded}`;
62
+ const url = appendMailto(baseUrl, mailto);
63
+ let response;
64
+ response = await http.get(url, { signal });
65
+ if (response.status === 404) {
66
+ return { found: false, metadata: null, raw: sanitizeRaw(response.body) };
67
+ }
68
+ if (!isOpenAlexWork(response.body)) {
69
+ throw new Error(`OpenAlex: unexpected response body for DOI "${doi}"`);
70
+ }
71
+ const sanitizedRaw = sanitizeRaw(response.body);
72
+ const metadata = mapWorkMetadata(response.body);
73
+ const result = { found: true, metadata, raw: sanitizedRaw };
74
+ await cache.set(cacheKey, result);
75
+ return result;
76
+ }
77
+ async function searchByTitleAuthor(title, authors, signal) {
78
+ const firstAuthor = authors[0] ?? '';
79
+ const cacheKey = `openalex:searchByTitleAuthor:${title.toLowerCase()}:${firstAuthor.toLowerCase()}`;
80
+ const cached = await cache.get(cacheKey, signal);
81
+ if (cached !== null) {
82
+ return cached;
83
+ }
84
+ const baseUrl = `${base}/works?search=${encodeURIComponent(title)}&filter=author.display_name.search:${encodeURIComponent(firstAuthor)}&per-page=5`;
85
+ const url = appendMailto(baseUrl, mailto);
86
+ let response;
87
+ response = await http.get(url, { signal });
88
+ if (response.status === 404) {
89
+ return { found: false, metadata: null, raw: sanitizeRaw(response.body) };
90
+ }
91
+ if (!isOpenAlexSearchResponse(response.body)) {
92
+ throw new Error(`OpenAlex: unexpected search response for title "${title}"`);
93
+ }
94
+ const results = response.body.results;
95
+ const firstResult = results?.[0];
96
+ if (!firstResult) {
97
+ const result = {
98
+ found: false,
99
+ metadata: null,
100
+ raw: sanitizeRaw(response.body),
101
+ };
102
+ await cache.set(cacheKey, result);
103
+ return result;
104
+ }
105
+ const sanitizedRaw = sanitizeRaw(response.body);
106
+ const metadata = mapWorkMetadata(firstResult);
107
+ const result = { found: true, metadata, raw: sanitizedRaw };
108
+ await cache.set(cacheKey, result);
109
+ return result;
110
+ }
111
+ return {
112
+ name: 'openalex',
113
+ lookupByDoi,
114
+ searchByTitleAuthor,
115
+ };
116
+ }
117
+ //# sourceMappingURL=openalex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openalex.js","sourceRoot":"","sources":["../../src/databases/openalex.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * OpenLibrary ISBN lookup client.
3
+ *
4
+ * Queries https://openlibrary.org/api/books?bibkeys=ISBN:<isbn>&format=json&jscmd=data
5
+ * and returns normalised DatabaseLookupResult metadata. Results are cached at
6
+ * the default TTL (30 days).
7
+ *
8
+ * An empty response object {} indicates no match (found: false). The client
9
+ * does not use a polite-pool email (OpenLibrary has no such mechanism).
10
+ */
11
+ import type { HttpClient } from '../http.js';
12
+ import type { Cache } from '../cache/fs-cache.js';
13
+ import type { DatabaseLookupResult, DatabaseClient } from './crossref.js';
14
+ export type { DatabaseLookupResult, DatabaseClient };
15
+ export interface OpenLibraryClientOptions {
16
+ http: HttpClient;
17
+ cache: Cache;
18
+ /** Base URL for the OpenLibrary API. Defaults to the public endpoint. */
19
+ baseUrl?: string;
20
+ }
21
+ export interface OpenLibraryClient extends DatabaseClient {
22
+ readonly name: 'openlibrary';
23
+ lookupByIsbn(isbn: string, signal?: AbortSignal): Promise<DatabaseLookupResult>;
24
+ }
25
+ export declare function createOpenLibraryClient(opts: OpenLibraryClientOptions): OpenLibraryClient;
26
+ //# sourceMappingURL=openlibrary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openlibrary.d.ts","sourceRoot":"","sources":["../../src/databases/openlibrary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG1E,YAAY,EAAE,oBAAoB,EAAE,cAAc,EAAE,CAAC;AAMrD,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAID,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACvD,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CACjF;AA8DD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,wBAAwB,GAAG,iBAAiB,CA2CzF"}