diff --git a/controllers/utils.js b/controllers/utils.js index a5c09bc5..e7be35db 100644 --- a/controllers/utils.js +++ b/controllers/utils.js @@ -121,10 +121,11 @@ const URI_DOUBLED_FILTER_KEYS = new Set(["__rerum.generatedBy", "creator"]) const TARGET_KEYS = ["target", "target.@id", "target.id", "target.source", "target.source.@id", "target.source.id"] /** - * Identity, system, and processing properties an Annotation body must never overwrite when its - * assertions are merged onto an entity. + * Identity, classification, system, and processing properties an Annotation body must never overwrite when its + * assertions are merged onto an entity. The entity stays the authority on its own '@context' and + * on the class it declares under 'type' or '@type'. */ -const PROTECTED_EXPANSION_KEYS = new Set(["@id", "id", "_id", "__rerum", "__deleted", "__proto__", "@context"]) +const PROTECTED_EXPANSION_KEYS = new Set(["@id", "id", "_id", "__rerum", "__deleted", "__proto__", "@context", "type", "@type"]) /** * Escape the RegExp metacharacters in a literal so it can be embedded in a pattern and match only diff --git a/public/API.html b/public/API.html index 3cd1a6a6..54804b09 100644 --- a/public/API.html +++ b/public/API.html @@ -1714,23 +1714,21 @@
- A record's identity, system, and processing properties are never overwritten by an Annotation. An
- Annotation body asserting @id,
+ A record's identity, classification, system, and processing properties
+ are never overwritten by an Annotation. An Annotation body asserting
+ @id,
id,
_id,
+ @context,
+ @type,
+ type,
__rerum,
- __deleted,
- __proto__, or
- @context contributes nothing. The expanded record
- answers to the same URI as the record you asked for, and reads under the same
- @context.
-
- Linked Data keywords that describe the record rather than identify it are not held back. An Annotation
- asserting type or
- @type
- contributes it like any other property, which collects the record's own value and the asserted one
- into an Array.
+ __deleted, or
+ __proto__
+ contributes nothing. The expanded record answers to the same URI as the
+ record you asked for, and reads under the same
+ @context
+ as the same class of thing it declared itself to be.
A deleted record is never expanded. Whatever it used to assert is inside its diff --git a/routes/__tests__/id.test.js b/routes/__tests__/id.test.js index 5d9f9773..60e72756 100644 --- a/routes/__tests__/id.test.js +++ b/routes/__tests__/id.test.js @@ -277,6 +277,8 @@ describe('GET /id/:id/expanded', () => { anno({ body: { __rerum: { evil: true } } }), anno({ body: { __deleted: { time: "2025-01-01T00:00:00.000" } } }), anno({ body: { "@context": "https://evil.example.org/context.json" } }), + anno({ body: { "@type": "Hijacked" } }), + anno({ body: { type: "Hijacked" } }), // An object literal with a __proto__ key sets the prototype instead of creating an own // property. JSON.parse creates the own property, which is what a MongoDB document has. anno({ body: JSON.parse('{"__proto__":{"polluted":"yes"}}') }) @@ -291,6 +293,8 @@ describe('GET /id/:id/expanded', () => { assert.strictEqual(response.body.__rerum.generatedBy, MOCK_AGENT) assert.strictEqual(response.body.__deleted, undefined) assert.strictEqual(response.body["@context"], "http://www.loc.gov/mods") + assert.strictEqual(response.body["@type"], "named-gloss", 'the record keeps the class it declared') + assert.strictEqual(response.body.type, undefined, 'an Annotation cannot add the other spelling either') assert.strictEqual(Object.hasOwn(response.body, '__proto__'), false) assert.strictEqual({}.polluted, undefined, 'Object.prototype must not be polluted') assert.strictEqual(response.headers['annotations-merged'], '0')