javascript-solid-server 0.0.183 → 0.0.185
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/.claude/settings.local.json +3 -1
- package/package.json +1 -1
- package/src/rdf/turtle.js +0 -0
- package/src/webid/profile.js +13 -1
- package/test/turtle.test.js +89 -0
- package/test/webid.test.js +77 -0
|
@@ -411,7 +411,9 @@
|
|
|
411
411
|
"Bash(cp /tmp/consent-preview.html ~/consent-preview.html)",
|
|
412
412
|
"Read(//home/melvin/**)",
|
|
413
413
|
"Bash(/usr/bin/chromium-browser --headless --no-sandbox --disable-gpu --hide-scrollbars --window-size=520,900 --screenshot=consent.png file:///home/melvin/consent-preview.html)",
|
|
414
|
-
"Bash(sed -i '0,/\"version\": \"0.0.181\"/{s/\"version\": \"0.0.181\"/\"version\": \"0.0.182\"/}' package-lock.json)"
|
|
414
|
+
"Bash(sed -i '0,/\"version\": \"0.0.181\"/{s/\"version\": \"0.0.181\"/\"version\": \"0.0.182\"/}' package-lock.json)",
|
|
415
|
+
"WebFetch(domain:losos.org)",
|
|
416
|
+
"Bash(sed -i '0,/\"version\": \"0.0.183\"/{s/\"version\": \"0.0.183\"/\"version\": \"0.0.184\"/}' package-lock.json)"
|
|
415
417
|
]
|
|
416
418
|
}
|
|
417
419
|
}
|
package/package.json
CHANGED
package/src/rdf/turtle.js
CHANGED
|
Binary file
|
package/src/webid/profile.js
CHANGED
|
@@ -75,7 +75,19 @@ export function generateProfileJsonLd({ webId, name, podUri, issuer }) {
|
|
|
75
75
|
'authentication': { '@id': 'cid:authentication', '@type': '@id', '@container': '@set' },
|
|
76
76
|
'assertionMethod': { '@id': 'cid:assertionMethod', '@type': '@id', '@container': '@set' },
|
|
77
77
|
'publicKeyJwk': { '@id': 'cid:publicKeyJwk', '@type': '@json' },
|
|
78
|
-
'publicKeyMultibase': { '@id': 'cid:publicKeyMultibase' }
|
|
78
|
+
'publicKeyMultibase': { '@id': 'cid:publicKeyMultibase' },
|
|
79
|
+
// CID v1 verificationMethod *class* names (#417). Without these
|
|
80
|
+
// term mappings, an app PATCHing in a VM with `type: "Multikey"`
|
|
81
|
+
// (the spec-example shape) emits a bare relative-IRI `<Multikey>`
|
|
82
|
+
// when JSS conneg-converts the profile to Turtle — which then
|
|
83
|
+
// resolves against the document's base URL to a fictional class
|
|
84
|
+
// like `<pod>/profile/Multikey`. Mapping the class names here
|
|
85
|
+
// means the bare term `"Multikey"` in JSON-LD expands correctly
|
|
86
|
+
// (cid:Multikey → https://www.w3.org/ns/cid/v1#Multikey) for both
|
|
87
|
+
// JSON-LD processors AND our Turtle conneg layer. Naive JSON
|
|
88
|
+
// readers comparing `type === "Multikey"` continue to work.
|
|
89
|
+
'Multikey': 'cid:Multikey',
|
|
90
|
+
'JsonWebKey': 'cid:JsonWebKey'
|
|
79
91
|
},
|
|
80
92
|
'@id': webId,
|
|
81
93
|
'@type': ['foaf:Person', 'schema:Person'],
|
package/test/turtle.test.js
CHANGED
|
@@ -153,6 +153,95 @@ describe('turtle converter — unit (#320 follow-ups)', () => {
|
|
|
153
153
|
`node n3 should appear:\n${content}`);
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
+
it('emits a space before `;` and `.` terminators (#419)', async () => {
|
|
157
|
+
// The de-facto convention in W3C spec examples and Apache Jena
|
|
158
|
+
// RIOT is to separate the statement-terminator from the previous
|
|
159
|
+
// token by a space. n3.js packs them; JSS post-processes the
|
|
160
|
+
// output to add the space.
|
|
161
|
+
//
|
|
162
|
+
// Use TWO predicates on the same subject so n3.js emits a `;`
|
|
163
|
+
// continuation (multiple triples on one subject). If a future
|
|
164
|
+
// N3 upgrade ever switched to "one triple per statement" style
|
|
165
|
+
// and dropped `;` entirely, this test would otherwise pass
|
|
166
|
+
// vacuously. The presence-of-terminator assertions below pin
|
|
167
|
+
// that behavior.
|
|
168
|
+
const doc = {
|
|
169
|
+
'@context': { 'foaf': 'http://xmlns.com/foaf/0.1/' },
|
|
170
|
+
'@id': 'https://example.test/alice',
|
|
171
|
+
'foaf:name': 'Alice',
|
|
172
|
+
'foaf:age': 30,
|
|
173
|
+
};
|
|
174
|
+
const { content } = await fromJsonLd(doc, 'text/turtle', 'https://example.test/', true);
|
|
175
|
+
// Pin "at least one ` ;` and one ` .` exists" with a literal
|
|
176
|
+
// SPACE — not just any whitespace. The intended output style
|
|
177
|
+
// (matching W3C Turtle 1.1 spec examples) is a single space
|
|
178
|
+
// separator: `value ;` / `value .`. Allowing `\n;` or `\t;`
|
|
179
|
+
// would let the test pass on visually-different output.
|
|
180
|
+
assert.match(content, / ;/, `output must contain at least one " ;" terminator (space-prefixed), got:\n${content}`);
|
|
181
|
+
assert.match(content, / \.(?:\s|$)/, `output must contain at least one " ." terminator (space-prefixed), got:\n${content}`);
|
|
182
|
+
// Every `;` must be preceded by a literal space. Same for `.`
|
|
183
|
+
// at end-of-statement. Reject anything else (newline, tab,
|
|
184
|
+
// packed-against-token).
|
|
185
|
+
const offendingSemi = /[^ ];/.test(content);
|
|
186
|
+
const offendingDot = /[^ ]\.\s*$/m.test(content) || /[^ ]\.\n/.test(content);
|
|
187
|
+
assert.ok(!offendingSemi, `every ; must be preceded by a single space, got:\n${content}`);
|
|
188
|
+
assert.ok(!offendingDot, `every . at line/doc end must be preceded by a single space, got:\n${content}`);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('does NOT add a space inside literals containing `;` or `.` (#419 safety)', async () => {
|
|
192
|
+
// Critical correctness test: the post-pass must NOT corrupt
|
|
193
|
+
// literal values. A literal "foo;bar" with the post-pass naïvely
|
|
194
|
+
// applied would become "foo ;bar" — silent data corruption.
|
|
195
|
+
//
|
|
196
|
+
// Assert by VALUE, not by lexical form. A quote-agnostic parser
|
|
197
|
+
// round-trip survives any future N3 writer style change
|
|
198
|
+
// (single vs double quotes, long-string `"""..."""`, etc.).
|
|
199
|
+
const doc = {
|
|
200
|
+
'@context': { 'ex': 'https://example.test/ns#' },
|
|
201
|
+
'@id': 'https://example.test/s',
|
|
202
|
+
'ex:semicolonInside': 'foo;bar',
|
|
203
|
+
'ex:dotInside': 'has.dot',
|
|
204
|
+
'ex:both': 'a;b.c',
|
|
205
|
+
};
|
|
206
|
+
const { content } = await fromJsonLd(doc, 'text/turtle', 'https://example.test/', true);
|
|
207
|
+
// Round-trip: parse the emitted Turtle, walk the quads, assert
|
|
208
|
+
// the literal values came back exactly as authored.
|
|
209
|
+
const { Parser } = await import('n3');
|
|
210
|
+
const parser = new Parser({ baseIRI: 'https://example.test/' });
|
|
211
|
+
const quads = parser.parse(content);
|
|
212
|
+
const objectsByPredicate = new Map();
|
|
213
|
+
for (const q of quads) {
|
|
214
|
+
if (q.object.termType !== 'Literal') continue;
|
|
215
|
+
objectsByPredicate.set(q.predicate.value, q.object.value);
|
|
216
|
+
}
|
|
217
|
+
assert.strictEqual(objectsByPredicate.get('https://example.test/ns#semicolonInside'), 'foo;bar',
|
|
218
|
+
`literal value for ex:semicolonInside must be "foo;bar"`);
|
|
219
|
+
assert.strictEqual(objectsByPredicate.get('https://example.test/ns#dotInside'), 'has.dot',
|
|
220
|
+
`literal value for ex:dotInside must be "has.dot"`);
|
|
221
|
+
assert.strictEqual(objectsByPredicate.get('https://example.test/ns#both'), 'a;b.c',
|
|
222
|
+
`literal value for ex:both must be "a;b.c"`);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('does NOT add a space inside an IRI containing `;` (#419 safety)', async () => {
|
|
226
|
+
// An IRI's content is bracketed by `<...>` — the post-pass
|
|
227
|
+
// shouldn't touch what's inside. Assert by value via parser
|
|
228
|
+
// round-trip so we don't depend on N3 writer formatting.
|
|
229
|
+
const doc = {
|
|
230
|
+
'@context': { 'ex': 'https://example.test/ns#' },
|
|
231
|
+
'@id': 'https://example.test/s',
|
|
232
|
+
'ex:rel': { '@id': 'https://example.test/path;with;semis' },
|
|
233
|
+
};
|
|
234
|
+
const { content } = await fromJsonLd(doc, 'text/turtle', 'https://example.test/', true);
|
|
235
|
+
const { Parser } = await import('n3');
|
|
236
|
+
const parser = new Parser({ baseIRI: 'https://example.test/' });
|
|
237
|
+
const quads = parser.parse(content);
|
|
238
|
+
const rels = quads
|
|
239
|
+
.filter(q => q.predicate.value === 'https://example.test/ns#rel')
|
|
240
|
+
.map(q => q.object.value);
|
|
241
|
+
assert.deepStrictEqual(rels, ['https://example.test/path;with;semis'],
|
|
242
|
+
`IRI value must round-trip with internal ; intact`);
|
|
243
|
+
});
|
|
244
|
+
|
|
156
245
|
it('cyclical nested node reference does not hang', async () => {
|
|
157
246
|
// Two nested nodes reference each other. BFS must not loop.
|
|
158
247
|
const a = { '@id': 'https://example.test/a', 'ex:knows': null };
|
package/test/webid.test.js
CHANGED
|
@@ -87,6 +87,30 @@ describe('WebID Profile', () => {
|
|
|
87
87
|
assert.strictEqual(ctx.publicKeyJwk['@type'], '@json');
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
it('declares CID v1 class names (Multikey, JsonWebKey) as flat aliases (#417)', async () => {
|
|
91
|
+
// Without these mappings, an app PATCHing in a VM with the
|
|
92
|
+
// spec-example shape `{type: "Multikey", ...}` produces a bare
|
|
93
|
+
// relative-IRI `<Multikey>` in the Turtle conneg output, which
|
|
94
|
+
// resolves to a fictional class on the pod's own host (e.g.
|
|
95
|
+
// `<pod>/profile/Multikey` instead of `cid:Multikey`).
|
|
96
|
+
//
|
|
97
|
+
// The flat-alias shape (`"Multikey": "cid:Multikey"`) makes
|
|
98
|
+
// bare-term emission work correctly through both JSON-LD
|
|
99
|
+
// expansion AND our Turtle conneg layer — and matches the
|
|
100
|
+
// "JSON-LD with flat context aliases" pattern consumers like
|
|
101
|
+
// LOSOS / LION rely on.
|
|
102
|
+
const res = await request(profilePath);
|
|
103
|
+
const jsonLd = await res.json();
|
|
104
|
+
const ctx = jsonLd['@context'];
|
|
105
|
+
for (const cls of ['Multikey', 'JsonWebKey']) {
|
|
106
|
+
const mapping = ctx[cls];
|
|
107
|
+
assert.ok(mapping, `@context must define class alias \`${cls}\``);
|
|
108
|
+
const id = typeof mapping === 'string' ? mapping : mapping['@id'];
|
|
109
|
+
assert.match(id, new RegExp(`^(cid:${cls}|https://www\\.w3\\.org/ns/cid/v1#${cls})$`),
|
|
110
|
+
`${cls} must map to the CID v1 namespace`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
90
114
|
it('declares self-control via controller === @id (#386 Phase A)', async () => {
|
|
91
115
|
const res = await request(profilePath);
|
|
92
116
|
const jsonLd = await res.json();
|
|
@@ -216,6 +240,59 @@ describe('WebID Profile — Turtle conneg (#320)', () => {
|
|
|
216
240
|
await stopTestServer();
|
|
217
241
|
});
|
|
218
242
|
|
|
243
|
+
it('Turtle conneg: generated profile @context expands bare Multikey/JsonWebKey terms (#417)', async () => {
|
|
244
|
+
// Combine the production profile generator's @context with a
|
|
245
|
+
// synthetic VM (the spec-example shape `{type: "Multikey", ...}`)
|
|
246
|
+
// and run it through the same conneg path the live profile
|
|
247
|
+
// would. Asserts: the bare-term type expands to the CID v1
|
|
248
|
+
// namespace, not to a relative IRI that resolves to a fake
|
|
249
|
+
// class on the pod's host.
|
|
250
|
+
const { generateProfile } = await import('../src/webid/profile.js');
|
|
251
|
+
const { fromJsonLd } = await import('../src/rdf/conneg.js');
|
|
252
|
+
const webId = 'https://example.test/profile/card.jsonld#me';
|
|
253
|
+
const profile = generateProfile({
|
|
254
|
+
webId,
|
|
255
|
+
name: 'mk-test',
|
|
256
|
+
podUri: 'https://example.test/',
|
|
257
|
+
issuer: 'https://example.test/',
|
|
258
|
+
});
|
|
259
|
+
// Inject a Multikey VM authored with the spec-example bare-term
|
|
260
|
+
// type — this is the shape the bug surfaces on.
|
|
261
|
+
const vmId = webId.replace('#me', '#nostr-key-1');
|
|
262
|
+
profile.verificationMethod = [{
|
|
263
|
+
id: vmId,
|
|
264
|
+
type: 'Multikey',
|
|
265
|
+
controller: webId,
|
|
266
|
+
publicKeyMultibase: 'fe70102de7ec0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab',
|
|
267
|
+
}];
|
|
268
|
+
profile.authentication = [vmId];
|
|
269
|
+
|
|
270
|
+
const { content: ttl } = await fromJsonLd(profile, 'text/turtle', 'https://example.test/', true);
|
|
271
|
+
|
|
272
|
+
// Pre-#417: would emit `a <Multikey>` (relative — resolves to
|
|
273
|
+
// `https://example.test/profile/Multikey`).
|
|
274
|
+
// After #417: the @context maps `Multikey -> cid:Multikey`, so
|
|
275
|
+
// the converter expands the bare term to the CID v1 IRI.
|
|
276
|
+
assert.ok(
|
|
277
|
+
ttl.includes('cid:Multikey') || ttl.includes('cid/v1#Multikey'),
|
|
278
|
+
`Turtle must emit a CID-namespaced Multikey class, got:\n${ttl}`,
|
|
279
|
+
);
|
|
280
|
+
assert.ok(
|
|
281
|
+
!/\ba\s+<Multikey>\s*[;.]/.test(ttl),
|
|
282
|
+
`Turtle must NOT emit bare <Multikey> (resolves to fictional class), got:\n${ttl}`,
|
|
283
|
+
);
|
|
284
|
+
// Don't regress #416: the VM block + publicKeyMultibase must
|
|
285
|
+
// still survive the conversion.
|
|
286
|
+
assert.ok(
|
|
287
|
+
ttl.includes('publicKeyMultibase') || ttl.includes('cid/v1#publicKeyMultibase'),
|
|
288
|
+
`Turtle must include cid:publicKeyMultibase, got:\n${ttl}`,
|
|
289
|
+
);
|
|
290
|
+
assert.ok(
|
|
291
|
+
ttl.includes('fe70102de7ec'),
|
|
292
|
+
`Turtle must include the publicKeyMultibase value, got:\n${ttl}`,
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
|
|
219
296
|
it('Turtle variant includes cid:service with lws:OpenIdProvider and serviceEndpoint', async () => {
|
|
220
297
|
const res = await request('/webidturtletest/profile/card.jsonld', {
|
|
221
298
|
headers: { Accept: 'text/turtle' }
|