open-museum-mcp 0.6.0 → 0.7.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.
- package/dist/clearanceTool.d.ts +25 -0
- package/dist/clearanceTool.js +24 -0
- package/dist/clearanceTool.js.map +1 -0
- package/dist/core/clearance/envelope.d.ts +26 -0
- package/dist/core/clearance/envelope.js +15 -0
- package/dist/core/clearance/envelope.js.map +1 -0
- package/dist/core/clearance/jcs.d.ts +8 -0
- package/dist/core/clearance/jcs.js +65 -0
- package/dist/core/clearance/jcs.js.map +1 -0
- package/dist/core/clearance/licenseMap.d.ts +38 -0
- package/dist/core/clearance/licenseMap.js +68 -0
- package/dist/core/clearance/licenseMap.js.map +1 -0
- package/dist/core/clearance/manifest.d.ts +104 -0
- package/dist/core/clearance/manifest.js +112 -0
- package/dist/core/clearance/manifest.js.map +1 -0
- package/dist/core/federation.d.ts +23 -0
- package/dist/core/federation.js +32 -4
- package/dist/core/federation.js.map +1 -1
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -1
- package/dist/fetchers/met.js +8 -1
- package/dist/fetchers/met.js.map +1 -1
- package/dist/server.js +19 -1
- package/dist/server.js.map +1 -1
- package/package.json +4 -1
- package/spec/clearance/VERSIONING.md +77 -0
- package/spec/clearance/v0.1/advisory-entry.schema.json +30 -0
- package/spec/clearance/v0.1/clearance-manifest.schema.json +213 -0
- package/spec/clearance/v0.1/context.jsonld +88 -0
- package/spec/clearance/v0.1/examples/cc0-accepted.json +107 -0
- package/spec/clearance/v0.1/examples/deny-unrecognized.json +78 -0
- package/spec/clearance/v0.1/rules.md +106 -0
- package/spec/clearance/v0.1/spec.md +212 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Federation } from './core/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Input schema for the `clearance_record` MCP tool.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately does NOT regex-validate the id against ID_REGEX, unlike
|
|
7
|
+
* `get_artwork` and `cite`. The clearance tool's contract is that a non-cleared
|
|
8
|
+
* work — including a malformed id — returns a definitive DENY manifest, not an
|
|
9
|
+
* error. `federation.clearanceManifest` emits that deny for an invalid id, so
|
|
10
|
+
* gating the id here would wrongly convert a contractual deny into a ZodError.
|
|
11
|
+
*/
|
|
12
|
+
export declare const ClearanceInput: z.ZodObject<{
|
|
13
|
+
id: z.ZodString;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
/**
|
|
16
|
+
* Handle a `clearance_record` call. Returns a normal (non-error) tool result
|
|
17
|
+
* for every id: a cleared work yields a permitted manifest, a non-cleared or
|
|
18
|
+
* malformed id yields a deny manifest. Both are valid answers.
|
|
19
|
+
*/
|
|
20
|
+
export declare function handleClearanceRecord(federation: Federation, args: unknown): Promise<{
|
|
21
|
+
content: {
|
|
22
|
+
type: "text";
|
|
23
|
+
text: string;
|
|
24
|
+
}[];
|
|
25
|
+
}>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Input schema for the `clearance_record` MCP tool.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately does NOT regex-validate the id against ID_REGEX, unlike
|
|
6
|
+
* `get_artwork` and `cite`. The clearance tool's contract is that a non-cleared
|
|
7
|
+
* work — including a malformed id — returns a definitive DENY manifest, not an
|
|
8
|
+
* error. `federation.clearanceManifest` emits that deny for an invalid id, so
|
|
9
|
+
* gating the id here would wrongly convert a contractual deny into a ZodError.
|
|
10
|
+
*/
|
|
11
|
+
export const ClearanceInput = z.object({
|
|
12
|
+
id: z.string(),
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* Handle a `clearance_record` call. Returns a normal (non-error) tool result
|
|
16
|
+
* for every id: a cleared work yields a permitted manifest, a non-cleared or
|
|
17
|
+
* malformed id yields a deny manifest. Both are valid answers.
|
|
18
|
+
*/
|
|
19
|
+
export async function handleClearanceRecord(federation, args) {
|
|
20
|
+
const input = ClearanceInput.parse(args);
|
|
21
|
+
const env = await federation.clearanceManifest(input.id);
|
|
22
|
+
return { content: [{ type: 'text', text: JSON.stringify(env, null, 2) }] };
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=clearanceTool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clearanceTool.js","sourceRoot":"","sources":["../src/clearanceTool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;CACf,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,UAAsB,EAAE,IAAa;IAC/E,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACzD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AACtF,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier-0 envelope: integrity, not authenticity. It wraps a pure Clearance
|
|
3
|
+
* Manifest payload with a hash computed over the payload's RFC 8785 (JCS)
|
|
4
|
+
* canonical bytes. The hash lives HERE, never inside the payload — payload
|
|
5
|
+
* purity is a design invariant (the payload never carries its own hash, its
|
|
6
|
+
* signature, or any commercial data).
|
|
7
|
+
*
|
|
8
|
+
* This is what the distributed OSS MCP emits by default; it ships no key.
|
|
9
|
+
* Tiers 1/2 (C2PA signing) wrap the same payload with a signature — the payload
|
|
10
|
+
* shape is unchanged across tiers.
|
|
11
|
+
*/
|
|
12
|
+
export interface Tier0Envelope<T = unknown> {
|
|
13
|
+
tier: 0;
|
|
14
|
+
payload: T;
|
|
15
|
+
integrity: {
|
|
16
|
+
alg: 'sha-256';
|
|
17
|
+
jcs: true;
|
|
18
|
+
hash: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Wrap a payload in a Tier-0 envelope. Canonicalizes FIRST (RFC 8785), then
|
|
23
|
+
* hashes the canonical bytes with Web Crypto (`crypto.subtle`, Workers-safe).
|
|
24
|
+
* Throws if the payload is not canonicalizable JSON.
|
|
25
|
+
*/
|
|
26
|
+
export declare function wrapTier0<T>(payload: T): Promise<Tier0Envelope<T>>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { canonicalizeToBytes } from './jcs.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wrap a payload in a Tier-0 envelope. Canonicalizes FIRST (RFC 8785), then
|
|
4
|
+
* hashes the canonical bytes with Web Crypto (`crypto.subtle`, Workers-safe).
|
|
5
|
+
* Throws if the payload is not canonicalizable JSON.
|
|
6
|
+
*/
|
|
7
|
+
export async function wrapTier0(payload) {
|
|
8
|
+
const bytes = canonicalizeToBytes(payload);
|
|
9
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
10
|
+
const hash = Array.from(new Uint8Array(digest))
|
|
11
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
12
|
+
.join('');
|
|
13
|
+
return { tier: 0, payload, integrity: { alg: 'sha-256', jcs: true, hash } };
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../../src/core/clearance/envelope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAmB/C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAI,OAAU;IAC3C,MAAM,KAAK,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IAC5D,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;SAC5C,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;SAC3C,IAAI,CAAC,EAAE,CAAC,CAAC;IACZ,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;AAC9E,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonicalize a JSON-compatible value to its RFC 8785 string form.
|
|
3
|
+
* Throws on values JSON cannot represent (functions, symbols, undefined,
|
|
4
|
+
* non-finite numbers) — a Clearance Manifest payload must be canonicalizable.
|
|
5
|
+
*/
|
|
6
|
+
export declare function canonicalize(value: unknown): string;
|
|
7
|
+
/** Canonicalize and encode to UTF-8 bytes (ArrayBuffer-backed, for Web Crypto). */
|
|
8
|
+
export declare function canonicalizeToBytes(value: unknown): Uint8Array<ArrayBuffer>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// RFC 8785 JSON Canonicalization Scheme (JCS).
|
|
2
|
+
//
|
|
3
|
+
// Produces the canonical UTF-8 serialization of a JSON value: object keys sorted
|
|
4
|
+
// by UTF-16 code unit, ECMAScript number serialization, minimal string escaping,
|
|
5
|
+
// no insignificant whitespace. This is the byte sequence a Clearance Manifest's
|
|
6
|
+
// Tier-0 envelope hashes over.
|
|
7
|
+
//
|
|
8
|
+
// Lifted verbatim (ported to TypeScript) from the PIF sibling standard's vetted
|
|
9
|
+
// dependency-free verifier (pif-spec/verifier/jcs.mjs). Runs unchanged in Node,
|
|
10
|
+
// the browser, and Cloudflare Workers — no dependencies, no `node:` imports.
|
|
11
|
+
//
|
|
12
|
+
// IMPORTANT (the contract the spec must warn implementers about): canonicalize
|
|
13
|
+
// FIRST, then hash the canonical bytes. Never hash the raw serialized string —
|
|
14
|
+
// JSON.stringify on the same object can produce different bytes (key order,
|
|
15
|
+
// whitespace) and therefore a different, non-interoperable hash.
|
|
16
|
+
/**
|
|
17
|
+
* Canonicalize a JSON-compatible value to its RFC 8785 string form.
|
|
18
|
+
* Throws on values JSON cannot represent (functions, symbols, undefined,
|
|
19
|
+
* non-finite numbers) — a Clearance Manifest payload must be canonicalizable.
|
|
20
|
+
*/
|
|
21
|
+
export function canonicalize(value) {
|
|
22
|
+
if (value === null)
|
|
23
|
+
return 'null';
|
|
24
|
+
const t = typeof value;
|
|
25
|
+
if (t === 'boolean')
|
|
26
|
+
return value ? 'true' : 'false';
|
|
27
|
+
if (t === 'number') {
|
|
28
|
+
if (!Number.isFinite(value)) {
|
|
29
|
+
throw new Error('JCS: non-finite numbers are not permitted in JSON');
|
|
30
|
+
}
|
|
31
|
+
// JSON.stringify uses ECMAScript Number-to-string, which is what JCS mandates.
|
|
32
|
+
return JSON.stringify(value);
|
|
33
|
+
}
|
|
34
|
+
if (t === 'string') {
|
|
35
|
+
// ECMAScript JSON string escaping (control chars as \u00XX, correct surrogate
|
|
36
|
+
// handling) is exactly the escaping RFC 8785 specifies.
|
|
37
|
+
//
|
|
38
|
+
// PORTABILITY NOTE for non-JS implementers (RFC 8785 §3.2.2.2): escape ONLY the
|
|
39
|
+
// two mandatory characters (" -> \", \ -> \\) and the C0 control range U+0000..U+001F,
|
|
40
|
+
// using the short forms \b \t \n \f \r where they exist and \u00XX (lowercase hex)
|
|
41
|
+
// otherwise. Do NOT escape the forward solidus "/", and do NOT escape any non-ASCII
|
|
42
|
+
// character; codepoints >= U+0080 are emitted as raw UTF-8. Over-escaping (e.g.
|
|
43
|
+
// \uXXXX for every non-ASCII char, as some language JSON encoders do by default) is
|
|
44
|
+
// the most common interop bug and will produce a different byte string, hence a
|
|
45
|
+
// different hash. V8's JSON.stringify already implements exactly this.
|
|
46
|
+
return JSON.stringify(value);
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
return '[' + value.map(canonicalize).join(',') + ']';
|
|
50
|
+
}
|
|
51
|
+
if (t === 'object') {
|
|
52
|
+
// Object.keys() then default Array sort: lexicographic by UTF-16 code unit,
|
|
53
|
+
// which is the ordering RFC 8785 requires.
|
|
54
|
+
const obj = value;
|
|
55
|
+
const keys = Object.keys(obj).sort();
|
|
56
|
+
const members = keys.map((k) => JSON.stringify(k) + ':' + canonicalize(obj[k]));
|
|
57
|
+
return '{' + members.join(',') + '}';
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`JCS: unsupported value of type ${t}`);
|
|
60
|
+
}
|
|
61
|
+
/** Canonicalize and encode to UTF-8 bytes (ArrayBuffer-backed, for Web Crypto). */
|
|
62
|
+
export function canonicalizeToBytes(value) {
|
|
63
|
+
return new TextEncoder().encode(canonicalize(value));
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=jcs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jcs.js","sourceRoot":"","sources":["../../../src/core/clearance/jcs.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,EAAE;AACF,iFAAiF;AACjF,iFAAiF;AACjF,gFAAgF;AAChF,+BAA+B;AAC/B,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAChF,6EAA6E;AAC7E,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,iEAAiE;AAEjE;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAElC,MAAM,CAAC,GAAG,OAAO,KAAK,CAAC;IAEvB,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IAErD,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;QACnB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;QACvE,CAAC;QACD,+EAA+E;QAC/E,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;QACnB,8EAA8E;QAC9E,wDAAwD;QACxD,EAAE;QACF,gFAAgF;QAChF,uFAAuF;QACvF,mFAAmF;QACnF,oFAAoF;QACpF,gFAAgF;QAChF,oFAAoF;QACpF,gFAAgF;QAChF,uEAAuE;QACvE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACvD,CAAC;IAED,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;QACnB,4EAA4E;QAC5E,2CAA2C;QAC3C,MAAM,GAAG,GAAG,KAAgC,CAAC;QAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAChF,OAAO,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACvC,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,EAAE,CAAC,CAAC;AACzD,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,mBAAmB,CAAC,KAAc;IAChD,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { LicenseType, RightsConfidence } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The structured `basis` for a single clearance determination. Shape is normative
|
|
4
|
+
* (schema-enforced): `rule`, `inputs`, and `summary` are all required —
|
|
5
|
+
* honesty-by-architecture. A determination that cannot name its rule, cite its
|
|
6
|
+
* inputs, and state its reasoning is not a valid determination.
|
|
7
|
+
*
|
|
8
|
+
* `rule` is a stable id that resolves as a fragment under the rule registry:
|
|
9
|
+
* `https://openclearance.org/v0.1/rules#<rule>`. The set of ids is OPEN, not a
|
|
10
|
+
* closed enum — an unrecognised id yields a non-fatal `unrecognised_rule`
|
|
11
|
+
* advisory at verification time, never a schema rejection. See
|
|
12
|
+
* `spec/clearance/v0.1/rules.md`.
|
|
13
|
+
*/
|
|
14
|
+
export interface ClearanceBasis {
|
|
15
|
+
rule: string;
|
|
16
|
+
inputs: {
|
|
17
|
+
field: string;
|
|
18
|
+
value: unknown;
|
|
19
|
+
}[];
|
|
20
|
+
summary: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ClearanceDecision {
|
|
23
|
+
statement: string | null;
|
|
24
|
+
commercialReproduction: {
|
|
25
|
+
permitted: boolean;
|
|
26
|
+
basis: ClearanceBasis;
|
|
27
|
+
};
|
|
28
|
+
derivatives: {
|
|
29
|
+
permitted: boolean;
|
|
30
|
+
basis: ClearanceBasis;
|
|
31
|
+
};
|
|
32
|
+
attributionRequired: {
|
|
33
|
+
required: boolean;
|
|
34
|
+
basis: ClearanceBasis;
|
|
35
|
+
};
|
|
36
|
+
confidence: RightsConfidence;
|
|
37
|
+
}
|
|
38
|
+
export declare function clearanceForLicense(type: LicenseType): ClearanceDecision;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The ONLY place clearance determinations live. Fail-closed: a license type not
|
|
2
|
+
// listed here resolves to all-false / default-deny. Adding a recognised
|
|
3
|
+
// permissive license later is a single entry here + a rule-registry row + a test.
|
|
4
|
+
const PERMISSIVE = {
|
|
5
|
+
CC0: {
|
|
6
|
+
statement: 'https://creativecommons.org/publicdomain/zero/1.0/',
|
|
7
|
+
rulePrefix: 'cc0',
|
|
8
|
+
summaryNoun: 'CC0 public-domain dedication',
|
|
9
|
+
},
|
|
10
|
+
// The gate only emits type=PD for the worldwide Creative Commons Public
|
|
11
|
+
// Domain Mark (Europeana `rights` = .../publicdomain/mark/1.0/) and the
|
|
12
|
+
// Wikimedia PD/PDM templates — never a jurisdiction-scoped rightsstatements.org
|
|
13
|
+
// value. The Public Domain Mark URI is therefore the accurate, gate-aligned
|
|
14
|
+
// statement; the old US-scoped NoC-US URI claimed a narrower (and incorrect)
|
|
15
|
+
// scope the gate never asserts.
|
|
16
|
+
PD: {
|
|
17
|
+
statement: 'https://creativecommons.org/publicdomain/mark/1.0/',
|
|
18
|
+
rulePrefix: 'pd',
|
|
19
|
+
summaryNoun: 'Public Domain Mark status',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
export function clearanceForLicense(type) {
|
|
23
|
+
const ok = PERMISSIVE[type];
|
|
24
|
+
const inputs = [{ field: 'license.type', value: type }];
|
|
25
|
+
if (ok) {
|
|
26
|
+
return {
|
|
27
|
+
statement: ok.statement,
|
|
28
|
+
commercialReproduction: {
|
|
29
|
+
permitted: true,
|
|
30
|
+
basis: {
|
|
31
|
+
rule: `${ok.rulePrefix}-grants-commercial`,
|
|
32
|
+
inputs,
|
|
33
|
+
summary: `${ok.summaryNoun} permits all uses, including commercial.`,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
derivatives: {
|
|
37
|
+
permitted: true,
|
|
38
|
+
basis: {
|
|
39
|
+
rule: `${ok.rulePrefix}-grants-derivatives`,
|
|
40
|
+
inputs,
|
|
41
|
+
summary: `${ok.summaryNoun} permits modification and derivative works.`,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
attributionRequired: {
|
|
45
|
+
required: false,
|
|
46
|
+
basis: {
|
|
47
|
+
rule: `${ok.rulePrefix}-waives-attribution`,
|
|
48
|
+
inputs,
|
|
49
|
+
summary: `${ok.summaryNoun} requires no attribution as a condition of reuse.`,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
confidence: 'high',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const denyBasis = {
|
|
56
|
+
rule: 'default-deny',
|
|
57
|
+
inputs,
|
|
58
|
+
summary: `unrecognized or non-permissive license type '${type}' ⇒ default deny`,
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
statement: null,
|
|
62
|
+
commercialReproduction: { permitted: false, basis: denyBasis },
|
|
63
|
+
derivatives: { permitted: false, basis: denyBasis },
|
|
64
|
+
attributionRequired: { required: false, basis: denyBasis },
|
|
65
|
+
confidence: 'low',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=licenseMap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"licenseMap.js","sourceRoot":"","sources":["../../../src/core/clearance/licenseMap.ts"],"names":[],"mappings":"AA4BA,gFAAgF;AAChF,wEAAwE;AACxE,kFAAkF;AAClF,MAAM,UAAU,GAEZ;IACF,GAAG,EAAE;QACH,SAAS,EAAE,oDAAoD;QAC/D,UAAU,EAAE,KAAK;QACjB,WAAW,EAAE,8BAA8B;KAC5C;IACD,wEAAwE;IACxE,wEAAwE;IACxE,gFAAgF;IAChF,4EAA4E;IAC5E,6EAA6E;IAC7E,gCAAgC;IAChC,EAAE,EAAE;QACF,SAAS,EAAE,oDAAoD;QAC/D,UAAU,EAAE,IAAI;QAChB,WAAW,EAAE,2BAA2B;KACzC;CACF,CAAC;AAEF,MAAM,UAAU,mBAAmB,CAAC,IAAiB;IACnD,MAAM,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAC5B,MAAM,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAExD,IAAI,EAAE,EAAE,CAAC;QACP,OAAO;YACL,SAAS,EAAE,EAAE,CAAC,SAAS;YACvB,sBAAsB,EAAE;gBACtB,SAAS,EAAE,IAAI;gBACf,KAAK,EAAE;oBACL,IAAI,EAAE,GAAG,EAAE,CAAC,UAAU,oBAAoB;oBAC1C,MAAM;oBACN,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,0CAA0C;iBACrE;aACF;YACD,WAAW,EAAE;gBACX,SAAS,EAAE,IAAI;gBACf,KAAK,EAAE;oBACL,IAAI,EAAE,GAAG,EAAE,CAAC,UAAU,qBAAqB;oBAC3C,MAAM;oBACN,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,6CAA6C;iBACxE;aACF;YACD,mBAAmB,EAAE;gBACnB,QAAQ,EAAE,KAAK;gBACf,KAAK,EAAE;oBACL,IAAI,EAAE,GAAG,EAAE,CAAC,UAAU,qBAAqB;oBAC3C,MAAM;oBACN,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,mDAAmD;iBAC9E;aACF;YACD,UAAU,EAAE,MAAM;SACnB,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAmB;QAChC,IAAI,EAAE,cAAc;QACpB,MAAM;QACN,OAAO,EAAE,gDAAgD,IAAI,kBAAkB;KAChF,CAAC;IACF,OAAO;QACL,SAAS,EAAE,IAAI;QACf,sBAAsB,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE;QAC9D,WAAW,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE;QACnD,mBAAmB,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE;QAC1D,UAAU,EAAE,KAAK;KAClB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Artist, ArtworkImages, RightsConfidence, ValidationResult } from '../../types.js';
|
|
2
|
+
import { type ClearanceBasis } from './licenseMap.js';
|
|
3
|
+
/**
|
|
4
|
+
* The pure Clearance Manifest payload (JSON-LD). It never contains its own hash,
|
|
5
|
+
* its signature, or any commercial data — integrity lives in the Tier-0 envelope
|
|
6
|
+
* (see envelope.ts), commerce in a sibling vendor assertion that references this
|
|
7
|
+
* payload by content hash. The payload is byte-identical to what the engine
|
|
8
|
+
* emits and independently verifiable.
|
|
9
|
+
*
|
|
10
|
+
* Field names mirror the engine's canonical Artwork vocabulary wherever they map
|
|
11
|
+
* (`artist`, `displayDate`, `imageUrls`, `imageOpenAccess`, `metadataOpenAccess`,
|
|
12
|
+
* `museum`) so the open-museum.art data-model reconciliation is a thin mapping,
|
|
13
|
+
* not a translation. JSON-LD semantics are supplied by aliasing these terms to
|
|
14
|
+
* schema.org / Dublin Core IRIs in context.jsonld — the `@context` is the sole
|
|
15
|
+
* normative authority; `specVersion` is human-readable convenience only.
|
|
16
|
+
*/
|
|
17
|
+
export interface ClearanceManifestPayload {
|
|
18
|
+
'@context': string[];
|
|
19
|
+
type: 'ClearanceManifest';
|
|
20
|
+
specVersion: string;
|
|
21
|
+
work: ClearanceWork;
|
|
22
|
+
source: ClearanceSource;
|
|
23
|
+
rights: ClearanceRights;
|
|
24
|
+
clearance: ClearanceBlock;
|
|
25
|
+
verification: ClearanceVerification;
|
|
26
|
+
/** Omitted for rejected records, which carry no identified Artwork to cite. */
|
|
27
|
+
citation?: ClearanceCitation;
|
|
28
|
+
}
|
|
29
|
+
export interface ClearanceWork {
|
|
30
|
+
id: string;
|
|
31
|
+
title?: string;
|
|
32
|
+
artist?: Artist;
|
|
33
|
+
displayDate?: string;
|
|
34
|
+
yearStart?: number | null;
|
|
35
|
+
yearEnd?: number | null;
|
|
36
|
+
medium?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ClearanceMuseum {
|
|
39
|
+
code: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
url?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface ClearanceSource {
|
|
44
|
+
museum: ClearanceMuseum;
|
|
45
|
+
apiUrl?: string;
|
|
46
|
+
pageUrl?: string;
|
|
47
|
+
originalUrl?: string;
|
|
48
|
+
imageUrls?: ArtworkImages;
|
|
49
|
+
}
|
|
50
|
+
export interface ClearanceRights {
|
|
51
|
+
statement: string | null;
|
|
52
|
+
sourceApiValue: {
|
|
53
|
+
field: string;
|
|
54
|
+
value: unknown;
|
|
55
|
+
} | null;
|
|
56
|
+
imageOpenAccess: boolean;
|
|
57
|
+
metadataOpenAccess: boolean;
|
|
58
|
+
confidence: RightsConfidence;
|
|
59
|
+
}
|
|
60
|
+
export interface ClearanceBlock {
|
|
61
|
+
commercialReproduction: {
|
|
62
|
+
permitted: boolean;
|
|
63
|
+
basis: ClearanceBasis;
|
|
64
|
+
};
|
|
65
|
+
derivatives: {
|
|
66
|
+
permitted: boolean;
|
|
67
|
+
basis: ClearanceBasis;
|
|
68
|
+
};
|
|
69
|
+
attributionRequired: {
|
|
70
|
+
required: boolean;
|
|
71
|
+
basis: ClearanceBasis;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export interface ClearanceVerification {
|
|
75
|
+
determinedBy: {
|
|
76
|
+
actor: string;
|
|
77
|
+
role: string;
|
|
78
|
+
};
|
|
79
|
+
tool: string;
|
|
80
|
+
determinedAt: string;
|
|
81
|
+
ruleContext: string;
|
|
82
|
+
determinationSource: {
|
|
83
|
+
type: string;
|
|
84
|
+
field?: string;
|
|
85
|
+
url?: string;
|
|
86
|
+
retrievedAt: string;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export interface ClearanceCitation {
|
|
90
|
+
full: string;
|
|
91
|
+
caption: string;
|
|
92
|
+
short: string;
|
|
93
|
+
}
|
|
94
|
+
export interface BuildOptions {
|
|
95
|
+
/** Engine version string for the `verification.tool` provenance field. */
|
|
96
|
+
engineVersion: string;
|
|
97
|
+
/**
|
|
98
|
+
* Generation timestamp, used only where no determination timestamp exists in
|
|
99
|
+
* the data (the deny path has no `license.verifiedAt`). Injected so manifests
|
|
100
|
+
* are deterministic and reproducible as conformance fixtures.
|
|
101
|
+
*/
|
|
102
|
+
now: string;
|
|
103
|
+
}
|
|
104
|
+
export declare function buildClearancePayload(result: ValidationResult, opts: BuildOptions): ClearanceManifestPayload;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { cite } from '../../cite.js';
|
|
2
|
+
import { clearanceForLicense } from './licenseMap.js';
|
|
3
|
+
const CONTEXT = [
|
|
4
|
+
'https://schema.org/',
|
|
5
|
+
'http://purl.org/dc/terms/',
|
|
6
|
+
'https://openclearance.org/v0.1/context.jsonld',
|
|
7
|
+
];
|
|
8
|
+
const TYPE = 'ClearanceManifest';
|
|
9
|
+
const SPEC_VERSION = '0.1';
|
|
10
|
+
const TOOL_NAME = 'open-museum-mcp';
|
|
11
|
+
/** Split `<museum>.<field path>` on the first dot. `met.isPublicDomain` → field `isPublicDomain`. */
|
|
12
|
+
function apiFieldOf(verificationSource) {
|
|
13
|
+
const i = verificationSource.indexOf('.');
|
|
14
|
+
return i < 0 ? verificationSource : verificationSource.slice(i + 1);
|
|
15
|
+
}
|
|
16
|
+
export function buildClearancePayload(result, opts) {
|
|
17
|
+
return result.status === 'accepted'
|
|
18
|
+
? buildAccepted(result.artwork, opts)
|
|
19
|
+
: buildRejected(result.rejection, opts);
|
|
20
|
+
}
|
|
21
|
+
function buildAccepted(art, opts) {
|
|
22
|
+
const decision = clearanceForLicense(art.license.type);
|
|
23
|
+
const field = apiFieldOf(art.license.verificationSource);
|
|
24
|
+
return {
|
|
25
|
+
'@context': CONTEXT,
|
|
26
|
+
type: TYPE,
|
|
27
|
+
specVersion: SPEC_VERSION,
|
|
28
|
+
work: {
|
|
29
|
+
id: art.id,
|
|
30
|
+
title: art.title,
|
|
31
|
+
artist: art.artist,
|
|
32
|
+
displayDate: art.displayDate,
|
|
33
|
+
yearStart: art.yearStart,
|
|
34
|
+
yearEnd: art.yearEnd,
|
|
35
|
+
medium: art.medium,
|
|
36
|
+
},
|
|
37
|
+
source: {
|
|
38
|
+
museum: { code: art.museum.code, name: art.museum.name, url: art.museum.url },
|
|
39
|
+
apiUrl: art.source.apiUrl,
|
|
40
|
+
pageUrl: art.source.pageUrl,
|
|
41
|
+
...(art.source.originalUrl ? { originalUrl: art.source.originalUrl } : {}),
|
|
42
|
+
imageUrls: art.imageUrls,
|
|
43
|
+
},
|
|
44
|
+
rights: {
|
|
45
|
+
statement: decision.statement,
|
|
46
|
+
sourceApiValue: { field, value: art.license.rawValue },
|
|
47
|
+
imageOpenAccess: art.imageOpenAccess,
|
|
48
|
+
metadataOpenAccess: art.metadataOpenAccess,
|
|
49
|
+
confidence: decision.confidence,
|
|
50
|
+
},
|
|
51
|
+
clearance: {
|
|
52
|
+
commercialReproduction: decision.commercialReproduction,
|
|
53
|
+
derivatives: decision.derivatives,
|
|
54
|
+
attributionRequired: decision.attributionRequired,
|
|
55
|
+
},
|
|
56
|
+
verification: {
|
|
57
|
+
determinedBy: { actor: `museum:${art.museum.code}`, role: 'rights-source' },
|
|
58
|
+
tool: `${TOOL_NAME}@${opts.engineVersion} · ${art.license.verificationSource}`,
|
|
59
|
+
determinedAt: art.license.verifiedAt,
|
|
60
|
+
ruleContext: `${art.license.verificationSource}='${art.license.rawValue}' ⇒ ${art.license.type}`,
|
|
61
|
+
determinationSource: {
|
|
62
|
+
type: 'api-field',
|
|
63
|
+
field,
|
|
64
|
+
url: art.source.apiUrl,
|
|
65
|
+
retrievedAt: art.license.verifiedAt,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
citation: {
|
|
69
|
+
full: cite(art, 'full'),
|
|
70
|
+
caption: cite(art, 'caption'),
|
|
71
|
+
short: cite(art, 'short'),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function buildRejected(rej, opts) {
|
|
76
|
+
// The deny determination (all-false booleans, default-deny rule, low
|
|
77
|
+
// confidence) comes from the single source of truth; the rejection supplies
|
|
78
|
+
// the case-specific evidence (the gate's verbatim reason) into each basis.
|
|
79
|
+
const decision = clearanceForLicense('UNKNOWN');
|
|
80
|
+
const basis = {
|
|
81
|
+
rule: 'default-deny',
|
|
82
|
+
inputs: [{ field: 'rejection.reason', value: rej.reason }],
|
|
83
|
+
summary: `rights gate rejected '${rej.id}': ${rej.reason} ⇒ default deny`,
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
'@context': CONTEXT,
|
|
87
|
+
type: TYPE,
|
|
88
|
+
specVersion: SPEC_VERSION,
|
|
89
|
+
work: { id: rej.id },
|
|
90
|
+
source: { museum: { code: rej.museumCode } },
|
|
91
|
+
rights: {
|
|
92
|
+
statement: decision.statement,
|
|
93
|
+
sourceApiValue: null,
|
|
94
|
+
imageOpenAccess: false,
|
|
95
|
+
metadataOpenAccess: false,
|
|
96
|
+
confidence: decision.confidence,
|
|
97
|
+
},
|
|
98
|
+
clearance: {
|
|
99
|
+
commercialReproduction: { permitted: false, basis },
|
|
100
|
+
derivatives: { permitted: false, basis },
|
|
101
|
+
attributionRequired: { required: false, basis },
|
|
102
|
+
},
|
|
103
|
+
verification: {
|
|
104
|
+
determinedBy: { actor: 'engine:open-museum-mcp', role: 'rights-gate' },
|
|
105
|
+
tool: `${TOOL_NAME}@${opts.engineVersion} · rights-gate`,
|
|
106
|
+
determinedAt: opts.now,
|
|
107
|
+
ruleContext: `strict default deny: ${rej.reason}`,
|
|
108
|
+
determinationSource: { type: 'rights-gate-default', retrievedAt: opts.now },
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../../src/core/clearance/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,8 @@ import { type CiteStyle } from '../cite.js';
|
|
|
3
3
|
import type { Fetcher } from '../fetchers/types.js';
|
|
4
4
|
import type { Artwork } from '../types.js';
|
|
5
5
|
import type { CacheStore } from './cache.js';
|
|
6
|
+
import { type Tier0Envelope } from './clearance/envelope.js';
|
|
7
|
+
import { type ClearanceManifestPayload } from './clearance/manifest.js';
|
|
6
8
|
export declare const ID_REGEX: RegExp;
|
|
7
9
|
/**
|
|
8
10
|
* Parsed parameters for a federation search. Shared by every front door (MCP
|
|
@@ -58,12 +60,33 @@ export interface FederationOptions {
|
|
|
58
60
|
* diagnostic hook, not an error path. The MCP server logs to stderr here.
|
|
59
61
|
*/
|
|
60
62
|
onReject?: (id: string, reason: string) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Engine version string stamped into a Clearance Manifest's
|
|
65
|
+
* `verification.tool` provenance field. The host (MCP server) supplies its
|
|
66
|
+
* real package version; defaults to a placeholder so the core stays usable
|
|
67
|
+
* without one.
|
|
68
|
+
*/
|
|
69
|
+
engineVersion?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Clock for the generation timestamp used where no determination timestamp
|
|
72
|
+
* exists in the data (the clearance deny path has no `license.verifiedAt`).
|
|
73
|
+
* Injectable so emitted manifests are deterministic in tests and fixtures.
|
|
74
|
+
*/
|
|
75
|
+
clock?: () => string;
|
|
61
76
|
}
|
|
62
77
|
export interface Federation {
|
|
63
78
|
readonly fetchers: Record<string, Fetcher>;
|
|
64
79
|
search(params: SearchParams): Promise<SearchResult>;
|
|
65
80
|
getArtwork(id: string): Promise<FetchOutcome>;
|
|
66
81
|
cite(id: string, style: CiteStyle): Promise<CiteOutcome>;
|
|
82
|
+
/**
|
|
83
|
+
* Emit a portable, fail-closed Clearance Manifest (rights-clearance +
|
|
84
|
+
* provenance + citation) for an artwork id, wrapped in a Tier-0 integrity
|
|
85
|
+
* envelope. A non-cleared work — rejected by the rights gate, an unknown
|
|
86
|
+
* museum, or an invalid id — returns a definitive *deny* manifest, never an
|
|
87
|
+
* error: a deny is a valid answer.
|
|
88
|
+
*/
|
|
89
|
+
clearanceManifest(id: string): Promise<Tier0Envelope<ClearanceManifestPayload>>;
|
|
67
90
|
}
|
|
68
91
|
/**
|
|
69
92
|
* Build a federation over a set of museum fetchers and a cache. This is the
|
package/dist/core/federation.js
CHANGED
|
@@ -2,6 +2,8 @@ import { z } from 'zod';
|
|
|
2
2
|
import { cite as citeArtwork } from '../cite.js';
|
|
3
3
|
import { dedupeWikimediaUploads } from '../dedupe.js';
|
|
4
4
|
import { filterByYearRange } from '../yearFilter.js';
|
|
5
|
+
import { wrapTier0 } from './clearance/envelope.js';
|
|
6
|
+
import { buildClearancePayload } from './clearance/manifest.js';
|
|
5
7
|
// Museum IDs follow `<code>:<segment>(/<segment>)*`. Each segment is
|
|
6
8
|
// alphanumeric, underscore, or hyphen. The four numeric-ID museums (Met,
|
|
7
9
|
// Cleveland, AIC, Wikimedia) match a single all-digit segment; Europeana's
|
|
@@ -14,6 +16,13 @@ export const ID_REGEX = /^[a-z]+:(?!.*\.\.)[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/
|
|
|
14
16
|
// cap, we'd hammer the upstream and risk rate-limit errors. 8 is empirically
|
|
15
17
|
// gentle and keeps wall-clock time within the same order of magnitude.
|
|
16
18
|
const DEFAULT_FETCH_CONCURRENCY = 8;
|
|
19
|
+
// Overfetch buffer: how many candidate IDs to pull per requested result before
|
|
20
|
+
// the rights gate and image filter thin them down. Bumped from 2x to 3x after
|
|
21
|
+
// the Met search stopped pre-filtering to public-domain upstream — more fetched
|
|
22
|
+
// records are now rejected by the gate post-fetch, so a larger candidate pool is
|
|
23
|
+
// needed to still fill a page. Applied only when has_image is set (the common
|
|
24
|
+
// path); the cache key carries the resolved overfetch count.
|
|
25
|
+
const OVERFETCH_FACTOR = 3;
|
|
17
26
|
/**
|
|
18
27
|
* Parsed parameters for a federation search. Shared by every front door (MCP
|
|
19
28
|
* tool, HTTP endpoint) so validation lives in one place. The date bounds gate
|
|
@@ -57,8 +66,8 @@ async function withConcurrency(items, limit, fn) {
|
|
|
57
66
|
return results;
|
|
58
67
|
}
|
|
59
68
|
// The cache key includes the overfetch count, not the user-facing `limit`.
|
|
60
|
-
// That means limit:5 and limit:6 produce different keys (since 5*
|
|
61
|
-
// 6*
|
|
69
|
+
// That means limit:5 and limit:6 produce different keys (since 5*3=15 vs
|
|
70
|
+
// 6*3=18). The trade-off: more cache rows, but each row is guaranteed to
|
|
62
71
|
// hold enough IDs to satisfy a request at its overfetch tier even after
|
|
63
72
|
// rights-gate rejections. Bucketing would need explicit refill logic.
|
|
64
73
|
function searchCacheKey(query, museum, hasImage, overFetch) {
|
|
@@ -75,6 +84,8 @@ export function createFederation(opts) {
|
|
|
75
84
|
const { fetchers, cache } = opts;
|
|
76
85
|
const concurrency = opts.concurrency ?? DEFAULT_FETCH_CONCURRENCY;
|
|
77
86
|
const onReject = opts.onReject;
|
|
87
|
+
const engineVersion = opts.engineVersion ?? '0.0.0';
|
|
88
|
+
const clock = opts.clock ?? (() => new Date().toISOString());
|
|
78
89
|
async function getArtwork(id) {
|
|
79
90
|
if (!ID_REGEX.test(id)) {
|
|
80
91
|
return { ok: false, reason: `invalid artwork id: ${id}` };
|
|
@@ -105,7 +116,7 @@ export function createFederation(opts) {
|
|
|
105
116
|
if (fetcherList.length === 0) {
|
|
106
117
|
throw new UnknownMuseumError(params.museum ?? '');
|
|
107
118
|
}
|
|
108
|
-
const overFetch = params.has_image ? params.limit *
|
|
119
|
+
const overFetch = params.has_image ? params.limit * OVERFETCH_FACTOR : params.limit;
|
|
109
120
|
const cacheKey = searchCacheKey(params.query, params.museum, params.has_image, overFetch);
|
|
110
121
|
let allIds = await cache.getQuery(cacheKey);
|
|
111
122
|
if (!allIds) {
|
|
@@ -132,6 +143,23 @@ export function createFederation(opts) {
|
|
|
132
143
|
return { ok: false, reason: out.reason };
|
|
133
144
|
return { ok: true, citation: citeArtwork(out.artwork, style) };
|
|
134
145
|
}
|
|
135
|
-
|
|
146
|
+
async function clearanceManifest(id) {
|
|
147
|
+
const buildOpts = { engineVersion, now: clock() };
|
|
148
|
+
const code = id.includes(':') ? id.slice(0, id.indexOf(':')) : '';
|
|
149
|
+
const deny = (reason) => wrapTier0(buildClearancePayload({ status: 'rejected', rejection: { id, museumCode: code, reason, rawSnapshot: null } }, buildOpts));
|
|
150
|
+
if (!ID_REGEX.test(id))
|
|
151
|
+
return deny(`invalid artwork id: ${id}`);
|
|
152
|
+
const fetcher = fetchers[code];
|
|
153
|
+
if (!fetcher)
|
|
154
|
+
return deny(`unknown museum code: ${code}`);
|
|
155
|
+
// Determinations are cheap and version-bound, so the manifest path does not
|
|
156
|
+
// touch the object cache — it always reflects the current rights gate.
|
|
157
|
+
const raw = await fetcher.getRaw(id);
|
|
158
|
+
const result = fetcher.normalize(raw);
|
|
159
|
+
if (result.status === 'rejected')
|
|
160
|
+
onReject?.(id, result.rejection.reason);
|
|
161
|
+
return wrapTier0(buildClearancePayload(result, buildOpts));
|
|
162
|
+
}
|
|
163
|
+
return { fetchers, search, getArtwork, cite, clearanceManifest };
|
|
136
164
|
}
|
|
137
165
|
//# sourceMappingURL=federation.js.map
|