@lde/iiif-validator 0.1.0 → 0.1.1
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.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/validateManifest.d.ts +34 -0
- package/dist/validateManifest.d.ts.map +1 -0
- package/dist/validateManifest.js +84 -0
- package/package.json +1 -1
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,GAC7B,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { validateManifest, } from './validateManifest.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coarse outcome of a manifest validation. On failure the reason describes
|
|
3
|
+
* *what kind* of failure occurred, not a detailed diagnosis — only enough to
|
|
4
|
+
* tell apart an unreachable host from a malformed document.
|
|
5
|
+
*/
|
|
6
|
+
export type ManifestValidationReason = 'valid-manifest' | 'timeout' | 'network-error' | 'http-error' | 'invalid-json' | 'not-a-manifest';
|
|
7
|
+
/**
|
|
8
|
+
* Verdict returned by {@link validateManifest}.
|
|
9
|
+
*/
|
|
10
|
+
export interface ManifestValidation {
|
|
11
|
+
/** Whether the URL dereferenced to a valid IIIF Presentation Manifest. */
|
|
12
|
+
valid: boolean;
|
|
13
|
+
/** Coarse classification of the outcome. */
|
|
14
|
+
reason: ManifestValidationReason;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Options for {@link validateManifest}.
|
|
18
|
+
*/
|
|
19
|
+
export interface ValidateManifestOptions {
|
|
20
|
+
/**
|
|
21
|
+
* `fetch` implementation to use. Injectable for testing; defaults to the
|
|
22
|
+
* global `fetch`.
|
|
23
|
+
*/
|
|
24
|
+
fetch?: typeof globalThis.fetch;
|
|
25
|
+
/** Per-request timeout in milliseconds. Defaults to 10 000. */
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Dereference a URL and check whether it is a valid IIIF Presentation
|
|
30
|
+
* Manifest. Never throws; every outcome is reported as a
|
|
31
|
+
* {@link ManifestValidation}.
|
|
32
|
+
*/
|
|
33
|
+
export declare function validateManifest(url: string, options?: ValidateManifestOptions): Promise<ManifestValidation>;
|
|
34
|
+
//# sourceMappingURL=validateManifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateManifest.d.ts","sourceRoot":"","sources":["../src/validateManifest.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAChC,gBAAgB,GAChB,SAAS,GACT,eAAe,GACf,YAAY,GACZ,cAAc,GACd,gBAAgB,CAAC;AAErB;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,0EAA0E;IAC1E,KAAK,EAAE,OAAO,CAAC;IACf,4CAA4C;IAC5C,MAAM,EAAE,wBAAwB,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CA6B7B"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const IIIF_PRESENTATION_CONTEXT = 'iiif.io/api/presentation/';
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
3
|
+
/**
|
|
4
|
+
* Dereference a URL and check whether it is a valid IIIF Presentation
|
|
5
|
+
* Manifest. Never throws; every outcome is reported as a
|
|
6
|
+
* {@link ManifestValidation}.
|
|
7
|
+
*/
|
|
8
|
+
export async function validateManifest(url, options) {
|
|
9
|
+
const doFetch = options?.fetch ?? globalThis.fetch;
|
|
10
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
11
|
+
let response;
|
|
12
|
+
try {
|
|
13
|
+
response = await doFetch(url, {
|
|
14
|
+
headers: { Accept: 'application/ld+json, application/json' },
|
|
15
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
return { valid: false, reason: classifyFetchError(error) };
|
|
20
|
+
}
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
return { valid: false, reason: 'http-error' };
|
|
23
|
+
}
|
|
24
|
+
let body;
|
|
25
|
+
try {
|
|
26
|
+
body = await response.json();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return { valid: false, reason: 'invalid-json' };
|
|
30
|
+
}
|
|
31
|
+
if (isPresentationManifest(body)) {
|
|
32
|
+
return { valid: true, reason: 'valid-manifest' };
|
|
33
|
+
}
|
|
34
|
+
return { valid: false, reason: 'not-a-manifest' };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Classify a thrown `fetch` error. An aborted request (our own
|
|
38
|
+
* `AbortSignal.timeout` firing, surfaced as `AbortError`/`TimeoutError`)
|
|
39
|
+
* counts as a timeout; anything else (DNS failure, connection refused, TLS) is
|
|
40
|
+
* a network error.
|
|
41
|
+
*/
|
|
42
|
+
function classifyFetchError(error) {
|
|
43
|
+
if (error instanceof Error &&
|
|
44
|
+
(error.name === 'AbortError' || error.name === 'TimeoutError')) {
|
|
45
|
+
return 'timeout';
|
|
46
|
+
}
|
|
47
|
+
return 'network-error';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Structural check: the document declares an IIIF Presentation `@context` and
|
|
51
|
+
* a manifest `type` (`Manifest` in v3, `sc:Manifest` in v2). The version
|
|
52
|
+
* segment of the context is not constrained, matching the forwards-compatible
|
|
53
|
+
* spirit of the detection query.
|
|
54
|
+
*/
|
|
55
|
+
function isPresentationManifest(body) {
|
|
56
|
+
if (typeof body !== 'object' || body === null)
|
|
57
|
+
return false;
|
|
58
|
+
const document = body;
|
|
59
|
+
return (hasPresentationContext(document['@context']) && hasManifestType(document));
|
|
60
|
+
}
|
|
61
|
+
function hasPresentationContext(context) {
|
|
62
|
+
return contextStrings(context).some((value) => value.includes(IIIF_PRESENTATION_CONTEXT));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Flatten a JSON-LD `@context` to the string IRIs it contains. The value may
|
|
66
|
+
* be a string, an array (mixing strings and objects), or a single object.
|
|
67
|
+
*/
|
|
68
|
+
function contextStrings(context) {
|
|
69
|
+
if (typeof context === 'string')
|
|
70
|
+
return [context];
|
|
71
|
+
if (Array.isArray(context)) {
|
|
72
|
+
return context.flatMap((entry) => contextStrings(entry));
|
|
73
|
+
}
|
|
74
|
+
if (typeof context === 'object' && context !== null) {
|
|
75
|
+
return Object.values(context).flatMap((entry) => contextStrings(entry));
|
|
76
|
+
}
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
function hasManifestType(document) {
|
|
80
|
+
const types = [document['type'], document['@type']]
|
|
81
|
+
.flatMap((value) => (Array.isArray(value) ? value : [value]))
|
|
82
|
+
.filter((value) => typeof value === 'string');
|
|
83
|
+
return types.includes('Manifest') || types.includes('sc:Manifest');
|
|
84
|
+
}
|