@lde/iiif-validator 0.1.0 → 0.1.2

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.
@@ -0,0 +1,2 @@
1
+ export { validateManifest, type ManifestValidation, type ManifestValidationReason, type ValidateManifestOptions, } from './validateManifest.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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' | 'binary-content' | '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,GAChB,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,CAwC7B"}
@@ -0,0 +1,108 @@
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
+ // A manifest is JSON. When the server announces a binary media type, skip
25
+ // reading the body: a sampled `schema:contentUrl` can dereference to the
26
+ // full-resolution image, audio or video asset, and `response.json()` would
27
+ // buffer the whole thing before failing to parse it — wasting bandwidth and
28
+ // time in the pipeline that calls this for every sampled manifest. Cancel the
29
+ // stream so the connection is freed without the download.
30
+ if (isBinaryMedia(response.headers.get('content-type'))) {
31
+ await response.body?.cancel();
32
+ return { valid: false, reason: 'binary-content' };
33
+ }
34
+ let body;
35
+ try {
36
+ body = await response.json();
37
+ }
38
+ catch {
39
+ return { valid: false, reason: 'invalid-json' };
40
+ }
41
+ if (isPresentationManifest(body)) {
42
+ return { valid: true, reason: 'valid-manifest' };
43
+ }
44
+ return { valid: false, reason: 'not-a-manifest' };
45
+ }
46
+ /**
47
+ * Whether a `Content-Type` header announces a binary media asset (image, audio
48
+ * or video) rather than a JSON document. Used to skip downloading non-manifest
49
+ * media. A missing or ambiguous type (e.g. `text/plain`, `application/octet-stream`)
50
+ * returns `false` so a manifest served with an odd type is still parsed.
51
+ */
52
+ function isBinaryMedia(contentType) {
53
+ if (contentType === null)
54
+ return false;
55
+ const mediaType = contentType.toLowerCase();
56
+ return (mediaType.startsWith('image/') ||
57
+ mediaType.startsWith('audio/') ||
58
+ mediaType.startsWith('video/'));
59
+ }
60
+ /**
61
+ * Classify a thrown `fetch` error. An aborted request (our own
62
+ * `AbortSignal.timeout` firing, surfaced as `AbortError`/`TimeoutError`)
63
+ * counts as a timeout; anything else (DNS failure, connection refused, TLS) is
64
+ * a network error.
65
+ */
66
+ function classifyFetchError(error) {
67
+ if (error instanceof Error &&
68
+ (error.name === 'AbortError' || error.name === 'TimeoutError')) {
69
+ return 'timeout';
70
+ }
71
+ return 'network-error';
72
+ }
73
+ /**
74
+ * Structural check: the document declares an IIIF Presentation `@context` and
75
+ * a manifest `type` (`Manifest` in v3, `sc:Manifest` in v2). The version
76
+ * segment of the context is not constrained, matching the forwards-compatible
77
+ * spirit of the detection query.
78
+ */
79
+ function isPresentationManifest(body) {
80
+ if (typeof body !== 'object' || body === null)
81
+ return false;
82
+ const document = body;
83
+ return (hasPresentationContext(document['@context']) && hasManifestType(document));
84
+ }
85
+ function hasPresentationContext(context) {
86
+ return contextStrings(context).some((value) => value.includes(IIIF_PRESENTATION_CONTEXT));
87
+ }
88
+ /**
89
+ * Flatten a JSON-LD `@context` to the string IRIs it contains. The value may
90
+ * be a string, an array (mixing strings and objects), or a single object.
91
+ */
92
+ function contextStrings(context) {
93
+ if (typeof context === 'string')
94
+ return [context];
95
+ if (Array.isArray(context)) {
96
+ return context.flatMap((entry) => contextStrings(entry));
97
+ }
98
+ if (typeof context === 'object' && context !== null) {
99
+ return Object.values(context).flatMap((entry) => contextStrings(entry));
100
+ }
101
+ return [];
102
+ }
103
+ function hasManifestType(document) {
104
+ const types = [document['type'], document['@type']]
105
+ .flatMap((value) => (Array.isArray(value) ? value : [value]))
106
+ .filter((value) => typeof value === 'string');
107
+ return types.includes('Manifest') || types.includes('sc:Manifest');
108
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/iiif-validator",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/ldelements/lde.git",
6
6
  "directory": "packages/iiif-validator"