@lde/iiif-validator 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # IIIF Validator
2
2
 
3
- Validates that a URL dereferences to a valid [IIIF Presentation](https://iiif.io/api/presentation/) Manifest. A small, dependency-light building block for Linked Data tooling that needs to tell a _declared_ IIIF manifest apart from one that actually resolves and parses.
3
+ Validates that a URL dereferences to a valid [IIIF Presentation](https://iiif.io/api/presentation/) Manifest. A small building block for Linked Data tooling that needs to tell a _declared_ IIIF manifest apart from one that actually resolves, parses, and loads in a real viewer.
4
4
 
5
5
  ```ts
6
6
  import { validateManifest } from '@lde/iiif-validator';
@@ -22,15 +22,18 @@ interface ManifestValidation {
22
22
  | 'network-error'
23
23
  | 'http-error'
24
24
  | 'invalid-json'
25
- | 'not-a-manifest';
25
+ | 'binary-content'
26
+ | 'not-a-manifest'
27
+ | 'does-not-load';
26
28
  }
27
29
  ```
28
30
 
29
31
  ## Behaviour
30
32
 
31
- - **Dereference over HTTP** with `Accept: application/ld+json, application/json`, following redirects, using the global `fetch` with an `AbortSignal` timeout (default 10 000 ms). Both are injectable via the options argument (`fetch`, `timeoutMs`).
32
- - **Lightweight, version-aware structural check.** A document is valid when the response is HTTP 2xx, the body parses as JSON, its `@context` references an IIIF Presentation context, and its `type`/`@type` indicates a manifest – accepting both v3 (`Manifest`) and v2 (`sc:Manifest`). The `@context` value may be a string, an array, or an object; all forms are handled. The version segment of the context is accepted version-agnostically.
33
- - **Strict failure semantics, no retries.** A timeout, network error, non-2xx status, unparseable body, missing IIIF `@context`, or wrong `type` all yield `valid: false` with the corresponding coarse `reason`. There is no deep JSON Schema validation and no dependency on the hosted IIIF Presentation Validator service.
33
+ - **Dereference over HTTP** with `Accept: */*`, following redirects, using the global `fetch` with an `AbortSignal` timeout (default 10 000 ms). The wildcard mirrors what real IIIF viewers send (the browser `fetch` default); a JSON-specific `Accept` would be more correct but trips up hosts that do backwards content negotiation – serving the manifest to `*/*` while returning 404 for a JSON-specific request. Both `fetch` and `timeoutMs` are injectable via the options argument.
34
+ - **Version-aware structural check.** A document is manifest-shaped when the response is HTTP 2xx, the body parses as JSON, its `@context` references an IIIF Presentation context, and its `type`/`@type` indicates a manifest – accepting both v3 (`Manifest`) and v2 (`sc:Manifest`). The `@context` value may be a string, an array, or an object; all forms are handled. The version segment of the context is accepted version-agnostically.
35
+ - **Viewer-load gate.** Being manifest-shaped is not enough: a document can pass every structural check yet fail to load in the dominant Vault/`@iiif/parser`-based viewers (Mirador 4, Clover, Theseus), which eagerly upgrade every manifest to Presentation 3 and normalise the whole tree on load. A single structural slip – e.g. a `null` where an `AnnotationPage` belongs – crashes that pass, so the manifest renders in no such viewer. The validator reproduces that load path with `@iiif/parser` (`upgrade()` then `normalize()`); if it throws, the verdict is `does-not-load`. `upgrade()` is a no-op for v3, so this one path covers both v2 and v3. There are only two tiers valid or invalid; cosmetic deviations that still load (a non-canonical `rights` URI, `image/jpg` instead of `image/jpeg`) stay valid.
36
+ - **Strict failure semantics, no retries.** A timeout, network error, non-2xx status, unparseable body, binary media type, missing IIIF `@context`, wrong `type`, or a document that does not load all yield `valid: false` with the corresponding coarse `reason`. There is no deep JSON Schema validation and no dependency on the hosted IIIF Presentation Validator service.
34
37
 
35
38
  ## Options
36
39
 
@@ -3,7 +3,7 @@
3
3
  * *what kind* of failure occurred, not a detailed diagnosis — only enough to
4
4
  * tell apart an unreachable host from a malformed document.
5
5
  */
6
- export type ManifestValidationReason = 'valid-manifest' | 'timeout' | 'network-error' | 'http-error' | 'invalid-json' | 'binary-content' | 'not-a-manifest';
6
+ export type ManifestValidationReason = 'valid-manifest' | 'timeout' | 'network-error' | 'http-error' | 'invalid-json' | 'binary-content' | 'not-a-manifest' | 'does-not-load';
7
7
  /**
8
8
  * Verdict returned by {@link validateManifest}.
9
9
  */
@@ -1 +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"}
1
+ {"version":3,"file":"validateManifest.d.ts","sourceRoot":"","sources":["../src/validateManifest.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAChC,gBAAgB,GAChB,SAAS,GACT,eAAe,GACf,YAAY,GACZ,cAAc,GACd,gBAAgB,GAChB,gBAAgB,GAChB,eAAe,CAAC;AAEpB;;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,CAmD7B"}
@@ -1,3 +1,5 @@
1
+ import { normalize } from '@iiif/parser/presentation-3';
2
+ import { upgrade } from '@iiif/parser/upgrader';
1
3
  const IIIF_PRESENTATION_CONTEXT = 'iiif.io/api/presentation/';
2
4
  const DEFAULT_TIMEOUT_MS = 10_000;
3
5
  /**
@@ -11,7 +13,13 @@ export async function validateManifest(url, options) {
11
13
  let response;
12
14
  try {
13
15
  response = await doFetch(url, {
14
- headers: { Accept: 'application/ld+json, application/json' },
16
+ // Request with `Accept: */*`, matching what real IIIF viewers (Mirador,
17
+ // Universal Viewer) send via the browser `fetch` default. A more specific
18
+ // `application/ld+json` is technically correct, but some manifest hosts do
19
+ // backwards content negotiation: they serve the manifest to `*/*` yet 404
20
+ // a JSON-specific request. Asking for anything keeps the validator as
21
+ // permissive as the viewers whose access it stands in for.
22
+ headers: { Accept: '*/*' },
15
23
  signal: AbortSignal.timeout(timeoutMs),
16
24
  });
17
25
  }
@@ -38,10 +46,34 @@ export async function validateManifest(url, options) {
38
46
  catch {
39
47
  return { valid: false, reason: 'invalid-json' };
40
48
  }
41
- if (isPresentationManifest(body)) {
42
- return { valid: true, reason: 'valid-manifest' };
49
+ if (!isPresentationManifest(body)) {
50
+ return { valid: false, reason: 'not-a-manifest' };
51
+ }
52
+ if (!loadsInViewer(body)) {
53
+ return { valid: false, reason: 'does-not-load' };
54
+ }
55
+ return { valid: true, reason: 'valid-manifest' };
56
+ }
57
+ /**
58
+ * Whether a manifest-shaped document survives the load path that real IIIF
59
+ * viewers run. The dominant Vault/`@iiif/parser`-based viewers (Mirador 4,
60
+ * Clover, Theseus) eagerly upgrade every manifest to Presentation 3 and
61
+ * normalise the whole tree on load; a structural deviation that crashes that
62
+ * pass — e.g. a `null` where an `AnnotationPage` belongs — makes the manifest
63
+ * fail to load even though it parses as JSON and is manifest-shaped. Running
64
+ * the same `upgrade()` then `normalize()` here reproduces that load: a throw
65
+ * from either step means no viewer would render it. `upgrade()` is
66
+ * version-agnostic (a no-op for documents already in v3), so this single path
67
+ * covers both v2 (`sc:Manifest`) and v3 documents.
68
+ */
69
+ function loadsInViewer(body) {
70
+ try {
71
+ normalize(upgrade(body));
72
+ return true;
73
+ }
74
+ catch {
75
+ return false;
43
76
  }
44
- return { valid: false, reason: 'not-a-manifest' };
45
77
  }
46
78
  /**
47
79
  * Whether a `Content-Type` header announces a binary media asset (image, audio
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/iiif-validator",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/ldelements/lde.git",
6
6
  "directory": "packages/iiif-validator"
@@ -24,6 +24,7 @@
24
24
  "!**/*.tsbuildinfo"
25
25
  ],
26
26
  "dependencies": {
27
+ "@iiif/parser": "^2.2.10",
27
28
  "tslib": "^2.3.0"
28
29
  }
29
30
  }