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 @@

Entity Expansion

record said and what each Annotation added.

- 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')