javascript-solid-server 0.0.10 → 0.0.12

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.
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Solid Conformance Tests (Simplified)
3
+ *
4
+ * Tests based on solid/solid-crud-tests but using Bearer token auth.
5
+ * Covers the same MUST requirements from the Solid Protocol spec.
6
+ */
7
+
8
+ import { describe, it, before, after } from 'node:test';
9
+ import assert from 'node:assert';
10
+ import {
11
+ startTestServer,
12
+ stopTestServer,
13
+ request,
14
+ createTestPod,
15
+ assertStatus
16
+ } from './helpers.js';
17
+
18
+ describe('Solid Protocol Conformance', () => {
19
+ let token;
20
+
21
+ before(async () => {
22
+ // Enable conneg for full Turtle support (required for Solid conformance)
23
+ await startTestServer({ conneg: true });
24
+ const pod = await createTestPod('conformance');
25
+ token = pod.token;
26
+ });
27
+
28
+ after(async () => {
29
+ await stopTestServer();
30
+ });
31
+
32
+ describe('MUST: Create non-container using POST', () => {
33
+ it('creates the resource and returns 201', async () => {
34
+ const res = await request('/conformance/public/', {
35
+ method: 'POST',
36
+ headers: {
37
+ 'Content-Type': 'text/turtle',
38
+ 'Slug': 'post-created.ttl'
39
+ },
40
+ body: '<#hello> <#linked> <#world> .',
41
+ auth: 'conformance'
42
+ });
43
+ assertStatus(res, 201);
44
+ assert.ok(res.headers.get('Location'), 'Should return Location header');
45
+ });
46
+
47
+ it('adds the resource to container listing', async () => {
48
+ const res = await request('/conformance/public/');
49
+ const body = await res.text();
50
+ assert.ok(body.includes('post-created.ttl'), 'Container should list the resource');
51
+ });
52
+ });
53
+
54
+ describe('MUST: Create non-container using PUT', () => {
55
+ it('creates the resource', async () => {
56
+ const res = await request('/conformance/public/put-created.ttl', {
57
+ method: 'PUT',
58
+ headers: { 'Content-Type': 'text/turtle' },
59
+ body: '<#hello> <#linked> <#world> .',
60
+ auth: 'conformance'
61
+ });
62
+ assert.ok([200, 201, 204].includes(res.status), `Expected 2xx, got ${res.status}`);
63
+ });
64
+
65
+ it('adds the resource to container listing', async () => {
66
+ const res = await request('/conformance/public/');
67
+ const body = await res.text();
68
+ assert.ok(body.includes('put-created.ttl'), 'Container should list the resource');
69
+ });
70
+ });
71
+
72
+ describe('MUST: Create container using PUT', () => {
73
+ it('creates container with trailing slash', async () => {
74
+ // First create a resource inside the new container (creates container implicitly)
75
+ const res = await request('/conformance/public/new-container/test.ttl', {
76
+ method: 'PUT',
77
+ headers: { 'Content-Type': 'text/turtle' },
78
+ body: '<#test> <#is> <#here> .',
79
+ auth: 'conformance'
80
+ });
81
+ assert.ok([200, 201, 204].includes(res.status));
82
+
83
+ // Container should exist
84
+ const containerRes = await request('/conformance/public/new-container/');
85
+ assertStatus(containerRes, 200);
86
+ });
87
+ });
88
+
89
+ describe('MUST: Update using PUT', () => {
90
+ it('overwrites existing resource', async () => {
91
+ // Create
92
+ await request('/conformance/public/update-test.ttl', {
93
+ method: 'PUT',
94
+ headers: { 'Content-Type': 'text/turtle' },
95
+ body: '<#v> <#is> "1" .',
96
+ auth: 'conformance'
97
+ });
98
+
99
+ // Update
100
+ const res = await request('/conformance/public/update-test.ttl', {
101
+ method: 'PUT',
102
+ headers: { 'Content-Type': 'text/turtle' },
103
+ body: '<#v> <#is> "2" .',
104
+ auth: 'conformance'
105
+ });
106
+ assertStatus(res, 204);
107
+
108
+ // Verify
109
+ const getRes = await request('/conformance/public/update-test.ttl');
110
+ const body = await getRes.text();
111
+ assert.ok(body.includes('"2"'), 'Resource should be updated');
112
+ });
113
+ });
114
+
115
+ describe('MUST: Update using PATCH (N3)', () => {
116
+ it('adds triple to existing resource', async () => {
117
+ // Create resource with context for cleaner patch matching
118
+ await request('/conformance/public/patch-test.json', {
119
+ method: 'PUT',
120
+ headers: { 'Content-Type': 'application/ld+json' },
121
+ body: JSON.stringify({
122
+ '@context': { 'ex': 'http://example.org/' },
123
+ '@id': '#me',
124
+ 'ex:name': 'Test'
125
+ }),
126
+ auth: 'conformance'
127
+ });
128
+
129
+ const patch = `
130
+ @prefix solid: <http://www.w3.org/ns/solid/terms#>.
131
+ @prefix ex: <http://example.org/>.
132
+ _:patch a solid:InsertDeletePatch;
133
+ solid:inserts { <#me> ex:added "yes" }.
134
+ `;
135
+ const res = await request('/conformance/public/patch-test.json', {
136
+ method: 'PATCH',
137
+ headers: { 'Content-Type': 'text/n3' },
138
+ body: patch,
139
+ auth: 'conformance'
140
+ });
141
+ assertStatus(res, 204);
142
+
143
+ const getRes = await request('/conformance/public/patch-test.json', {
144
+ headers: { 'Accept': 'application/ld+json' }
145
+ });
146
+ const data = await getRes.json();
147
+ // Check for either prefixed or full URI form
148
+ const added = data['ex:added'] || data['http://example.org/added'];
149
+ assert.strictEqual(added, 'yes');
150
+ });
151
+ });
152
+
153
+ describe('MUST: Update using PATCH (SPARQL Update)', () => {
154
+ it('modifies resource with INSERT DATA', async () => {
155
+ await request('/conformance/public/sparql-test.json', {
156
+ method: 'PUT',
157
+ headers: { 'Content-Type': 'application/ld+json' },
158
+ body: JSON.stringify({ '@id': '#item' }),
159
+ auth: 'conformance'
160
+ });
161
+
162
+ const sparql = `
163
+ PREFIX ex: <http://example.org/>
164
+ INSERT DATA { <#item> ex:status "active" }
165
+ `;
166
+ const res = await request('/conformance/public/sparql-test.json', {
167
+ method: 'PATCH',
168
+ headers: { 'Content-Type': 'application/sparql-update' },
169
+ body: sparql,
170
+ auth: 'conformance'
171
+ });
172
+ assertStatus(res, 204);
173
+ });
174
+ });
175
+
176
+ describe('MUST: Delete resource', () => {
177
+ it('deletes and returns 204', async () => {
178
+ await request('/conformance/public/to-delete.ttl', {
179
+ method: 'PUT',
180
+ headers: { 'Content-Type': 'text/turtle' },
181
+ body: '<#x> <#y> <#z> .',
182
+ auth: 'conformance'
183
+ });
184
+
185
+ const res = await request('/conformance/public/to-delete.ttl', {
186
+ method: 'DELETE',
187
+ auth: 'conformance'
188
+ });
189
+ assertStatus(res, 204);
190
+
191
+ const getRes = await request('/conformance/public/to-delete.ttl');
192
+ assertStatus(getRes, 404);
193
+ });
194
+
195
+ it('removes from container listing', async () => {
196
+ const res = await request('/conformance/public/');
197
+ const body = await res.text();
198
+ assert.ok(!body.includes('to-delete.ttl'), 'Should not be in listing');
199
+ });
200
+ });
201
+
202
+ describe('MUST: LDP Headers', () => {
203
+ it('includes Link rel=type for containers', async () => {
204
+ const res = await request('/conformance/public/');
205
+ const link = res.headers.get('Link');
206
+ assert.ok(link.includes('ldp#BasicContainer'), 'Should have BasicContainer type');
207
+ assert.ok(link.includes('ldp#Container'), 'Should have Container type');
208
+ });
209
+
210
+ it('includes Link rel=type for resources', async () => {
211
+ const res = await request('/conformance/public/put-created.ttl');
212
+ const link = res.headers.get('Link');
213
+ assert.ok(link.includes('ldp#Resource'), 'Should have Resource type');
214
+ });
215
+
216
+ it('includes Link rel=acl', async () => {
217
+ const res = await request('/conformance/public/put-created.ttl');
218
+ const link = res.headers.get('Link');
219
+ assert.ok(link.includes('rel="acl"'), 'Should have acl link');
220
+ });
221
+
222
+ it('includes ETag header', async () => {
223
+ const res = await request('/conformance/public/put-created.ttl');
224
+ assert.ok(res.headers.get('ETag'), 'Should have ETag');
225
+ });
226
+
227
+ it('includes Allow header on OPTIONS', async () => {
228
+ const res = await request('/conformance/public/', { method: 'OPTIONS' });
229
+ const allow = res.headers.get('Allow');
230
+ assert.ok(allow.includes('GET'), 'Should allow GET');
231
+ assert.ok(allow.includes('POST'), 'Should allow POST');
232
+ });
233
+
234
+ it('includes Accept-Post for containers', async () => {
235
+ const res = await request('/conformance/public/', { method: 'OPTIONS' });
236
+ assert.ok(res.headers.get('Accept-Post'), 'Should have Accept-Post');
237
+ });
238
+
239
+ it('includes Accept-Put for resources', async () => {
240
+ const res = await request('/conformance/public/put-created.ttl', { method: 'OPTIONS' });
241
+ assert.ok(res.headers.get('Accept-Put'), 'Should have Accept-Put');
242
+ });
243
+
244
+ it('includes Accept-Patch for resources', async () => {
245
+ const res = await request('/conformance/public/put-created.ttl', { method: 'OPTIONS' });
246
+ const acceptPatch = res.headers.get('Accept-Patch');
247
+ assert.ok(acceptPatch, 'Should have Accept-Patch');
248
+ assert.ok(acceptPatch.includes('text/n3'), 'Should accept N3');
249
+ });
250
+ });
251
+
252
+ describe('MUST: WAC Headers', () => {
253
+ it('includes WAC-Allow header', async () => {
254
+ const res = await request('/conformance/public/');
255
+ assert.ok(res.headers.get('WAC-Allow'), 'Should have WAC-Allow');
256
+ });
257
+ });
258
+
259
+ describe('MUST: Conditional Requests', () => {
260
+ it('returns 304 for If-None-Match on GET', async () => {
261
+ const res1 = await request('/conformance/public/put-created.ttl');
262
+ const etag = res1.headers.get('ETag');
263
+
264
+ const res2 = await request('/conformance/public/put-created.ttl', {
265
+ headers: { 'If-None-Match': etag }
266
+ });
267
+ assertStatus(res2, 304);
268
+ });
269
+
270
+ it('returns 412 for If-Match mismatch on PUT', async () => {
271
+ const res = await request('/conformance/public/put-created.ttl', {
272
+ method: 'PUT',
273
+ headers: {
274
+ 'Content-Type': 'text/turtle',
275
+ 'If-Match': '"wrong-etag"'
276
+ },
277
+ body: '<#new> <#data> <#here> .',
278
+ auth: 'conformance'
279
+ });
280
+ assertStatus(res, 412);
281
+ });
282
+
283
+ it('returns 412 for If-None-Match: * on existing resource', async () => {
284
+ const res = await request('/conformance/public/put-created.ttl', {
285
+ method: 'PUT',
286
+ headers: {
287
+ 'Content-Type': 'text/turtle',
288
+ 'If-None-Match': '*'
289
+ },
290
+ body: '<#new> <#data> <#here> .',
291
+ auth: 'conformance'
292
+ });
293
+ assertStatus(res, 412);
294
+ });
295
+ });
296
+
297
+ describe('MUST: CORS Headers', () => {
298
+ it('includes Access-Control-Allow-Origin', async () => {
299
+ const res = await request('/conformance/public/', {
300
+ headers: { 'Origin': 'https://example.com' }
301
+ });
302
+ const acao = res.headers.get('Access-Control-Allow-Origin');
303
+ assert.ok(acao, 'Should have ACAO header');
304
+ });
305
+
306
+ it('includes Access-Control-Expose-Headers', async () => {
307
+ const res = await request('/conformance/public/', {
308
+ headers: { 'Origin': 'https://example.com' }
309
+ });
310
+ const expose = res.headers.get('Access-Control-Expose-Headers');
311
+ assert.ok(expose, 'Should expose headers');
312
+ assert.ok(expose.includes('Location'), 'Should expose Location');
313
+ assert.ok(expose.includes('Link'), 'Should expose Link');
314
+ });
315
+
316
+ it('handles preflight OPTIONS', async () => {
317
+ const res = await request('/conformance/public/', {
318
+ method: 'OPTIONS',
319
+ headers: {
320
+ 'Origin': 'https://example.com',
321
+ 'Access-Control-Request-Method': 'PUT'
322
+ }
323
+ });
324
+ assertStatus(res, 204);
325
+ assert.ok(res.headers.get('Access-Control-Allow-Methods'), 'Should have Allow-Methods');
326
+ });
327
+ });
328
+
329
+ describe('MUST: Content Negotiation', () => {
330
+ it('returns JSON-LD by default for RDF resources', async () => {
331
+ const res = await request('/conformance/public/patch-test.json');
332
+ const ct = res.headers.get('Content-Type');
333
+ assert.ok(ct.includes('application/ld+json') || ct.includes('application/json'));
334
+ });
335
+ });
336
+
337
+ describe('SHOULD: WebSocket Notifications', () => {
338
+ it('includes Updates-Via header when enabled', async () => {
339
+ // Note: requires server started with notifications: true
340
+ // This test documents the expected behavior
341
+ const res = await request('/conformance/', { method: 'OPTIONS' });
342
+ // Updates-Via may or may not be present depending on server config
343
+ const updatesVia = res.headers.get('Updates-Via');
344
+ if (updatesVia) {
345
+ assert.ok(updatesVia.includes('ws'), 'Should be WebSocket URL');
346
+ }
347
+ });
348
+ });
349
+ });
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Identity Provider Tests
3
+ */
4
+
5
+ import { describe, it, before, after, beforeEach } from 'node:test';
6
+ import assert from 'node:assert';
7
+ import { createServer } from '../src/server.js';
8
+ import fs from 'fs-extra';
9
+ import path from 'path';
10
+
11
+ const TEST_PORT = 3099;
12
+ const TEST_HOST = 'localhost';
13
+ const BASE_URL = `http://${TEST_HOST}:${TEST_PORT}`;
14
+ const DATA_DIR = './test-data-idp';
15
+
16
+ describe('Identity Provider', () => {
17
+ let server;
18
+
19
+ before(async () => {
20
+ // Clean up any existing test data
21
+ await fs.remove(DATA_DIR);
22
+ await fs.ensureDir(DATA_DIR);
23
+
24
+ // Create server with IdP enabled
25
+ server = createServer({
26
+ logger: false,
27
+ root: DATA_DIR,
28
+ idp: true,
29
+ idpIssuer: BASE_URL,
30
+ });
31
+
32
+ await server.listen({ port: TEST_PORT, host: TEST_HOST });
33
+ });
34
+
35
+ after(async () => {
36
+ await server.close();
37
+ await fs.remove(DATA_DIR);
38
+ });
39
+
40
+ describe('OIDC Discovery', () => {
41
+ it('should serve /.well-known/openid-configuration', async () => {
42
+ const res = await fetch(`${BASE_URL}/.well-known/openid-configuration`);
43
+ assert.strictEqual(res.status, 200);
44
+
45
+ const config = await res.json();
46
+ assert.strictEqual(config.issuer, BASE_URL);
47
+ assert.ok(config.authorization_endpoint);
48
+ assert.ok(config.token_endpoint);
49
+ assert.ok(config.jwks_uri);
50
+ });
51
+
52
+ it('should include required Solid-OIDC endpoints', async () => {
53
+ const res = await fetch(`${BASE_URL}/.well-known/openid-configuration`);
54
+ const config = await res.json();
55
+
56
+ assert.ok(config.registration_endpoint, 'should have registration endpoint');
57
+ assert.ok(config.scopes_supported.includes('webid'), 'should support webid scope');
58
+ assert.ok(config.dpop_signing_alg_values_supported, 'should support DPoP');
59
+ });
60
+
61
+ it('should serve /.well-known/jwks.json', async () => {
62
+ const res = await fetch(`${BASE_URL}/.well-known/jwks.json`);
63
+ assert.strictEqual(res.status, 200);
64
+
65
+ const jwks = await res.json();
66
+ assert.ok(Array.isArray(jwks.keys));
67
+ assert.ok(jwks.keys.length > 0, 'should have at least one key');
68
+ // Keys should be public (no 'd' component)
69
+ assert.ok(!jwks.keys[0].d, 'should not expose private key component');
70
+ });
71
+ });
72
+
73
+ describe('Pod Creation with IdP', () => {
74
+ it('should require email when IdP is enabled', async () => {
75
+ const res = await fetch(`${BASE_URL}/.pods`, {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify({ name: 'noemail' }),
79
+ });
80
+
81
+ assert.strictEqual(res.status, 400);
82
+ const body = await res.json();
83
+ assert.ok(body.error.includes('Email'));
84
+ });
85
+
86
+ it('should require password when IdP is enabled', async () => {
87
+ const res = await fetch(`${BASE_URL}/.pods`, {
88
+ method: 'POST',
89
+ headers: { 'Content-Type': 'application/json' },
90
+ body: JSON.stringify({ name: 'nopass', email: 'test@example.com' }),
91
+ });
92
+
93
+ assert.strictEqual(res.status, 400);
94
+ const body = await res.json();
95
+ assert.ok(body.error.includes('Password'));
96
+ });
97
+
98
+ it('should require minimum password length', async () => {
99
+ const res = await fetch(`${BASE_URL}/.pods`, {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({ name: 'shortpass', email: 'test@example.com', password: 'short' }),
103
+ });
104
+
105
+ assert.strictEqual(res.status, 400);
106
+ const body = await res.json();
107
+ assert.ok(body.error.includes('8'));
108
+ });
109
+
110
+ it('should create pod with account', async () => {
111
+ const uniqueId = Date.now();
112
+ const res = await fetch(`${BASE_URL}/.pods`, {
113
+ method: 'POST',
114
+ headers: { 'Content-Type': 'application/json' },
115
+ body: JSON.stringify({
116
+ name: `idpuser${uniqueId}`,
117
+ email: `idpuser${uniqueId}@example.com`,
118
+ password: 'securepassword123',
119
+ }),
120
+ });
121
+
122
+ assert.strictEqual(res.status, 201);
123
+ const body = await res.json();
124
+
125
+ assert.strictEqual(body.name, `idpuser${uniqueId}`);
126
+ assert.ok(body.webId.includes(`idpuser${uniqueId}`));
127
+ assert.ok(body.podUri.includes(`idpuser${uniqueId}`));
128
+ assert.ok(body.idpIssuer, 'should include IdP issuer');
129
+ assert.ok(body.loginUrl, 'should include login URL');
130
+ // Should NOT have simple token when IdP is enabled
131
+ assert.ok(!body.token, 'should not have simple token');
132
+ });
133
+
134
+ it('should reject duplicate email', async () => {
135
+ const uniqueId = Date.now();
136
+ const duplicateEmail = `duplicate${uniqueId}@example.com`;
137
+
138
+ // First user
139
+ await fetch(`${BASE_URL}/.pods`, {
140
+ method: 'POST',
141
+ headers: { 'Content-Type': 'application/json' },
142
+ body: JSON.stringify({
143
+ name: `first${uniqueId}`,
144
+ email: duplicateEmail,
145
+ password: 'password123',
146
+ }),
147
+ });
148
+
149
+ // Second user with same email
150
+ const res = await fetch(`${BASE_URL}/.pods`, {
151
+ method: 'POST',
152
+ headers: { 'Content-Type': 'application/json' },
153
+ body: JSON.stringify({
154
+ name: `second${uniqueId}`,
155
+ email: duplicateEmail,
156
+ password: 'password456',
157
+ }),
158
+ });
159
+
160
+ assert.strictEqual(res.status, 409);
161
+ const body = await res.json();
162
+ assert.ok(body.error.includes('Email'));
163
+ });
164
+ });
165
+
166
+ describe('Login Interaction', () => {
167
+ it('should respond to authorization endpoint', async () => {
168
+ // Start an authorization flow
169
+ // Various responses are acceptable - 302/303 (redirect), 400 (bad request), 404 (no route)
170
+ // This just verifies the server handles the request
171
+ const res = await fetch(`${BASE_URL}/idp/auth?client_id=test&redirect_uri=http://localhost&response_type=code&scope=openid`, {
172
+ redirect: 'manual',
173
+ });
174
+
175
+ // oidc-provider mounted via middie may return different status codes
176
+ // The important thing is it doesn't crash and returns a valid HTTP response
177
+ assert.ok(res.status >= 200 && res.status < 600, `got valid HTTP status ${res.status}`);
178
+ });
179
+ });
180
+ });
181
+
182
+ describe('Identity Provider - Accounts', () => {
183
+ let server;
184
+ const ACCOUNTS_DATA_DIR = './test-data-idp-accounts';
185
+
186
+ before(async () => {
187
+ await fs.remove(ACCOUNTS_DATA_DIR);
188
+ await fs.ensureDir(ACCOUNTS_DATA_DIR);
189
+
190
+ server = createServer({
191
+ logger: false,
192
+ root: ACCOUNTS_DATA_DIR,
193
+ idp: true,
194
+ idpIssuer: `http://${TEST_HOST}:${TEST_PORT + 1}`,
195
+ });
196
+
197
+ await server.listen({ port: TEST_PORT + 1, host: TEST_HOST });
198
+ });
199
+
200
+ after(async () => {
201
+ await server.close();
202
+ await fs.remove(ACCOUNTS_DATA_DIR);
203
+ });
204
+
205
+ it('should store account data in .idp directory', async () => {
206
+ const uniqueName = `stored${Date.now()}`;
207
+ const uniqueEmail = `stored${Date.now()}@example.com`;
208
+
209
+ const res = await fetch(`http://${TEST_HOST}:${TEST_PORT + 1}/.pods`, {
210
+ method: 'POST',
211
+ headers: { 'Content-Type': 'application/json' },
212
+ body: JSON.stringify({
213
+ name: uniqueName,
214
+ email: uniqueEmail,
215
+ password: 'password123',
216
+ }),
217
+ });
218
+
219
+ assert.strictEqual(res.status, 201, 'pod creation should succeed');
220
+
221
+ // Check that account data exists
222
+ const accountsDir = path.join(ACCOUNTS_DATA_DIR, '.idp', 'accounts');
223
+ const exists = await fs.pathExists(accountsDir);
224
+ assert.ok(exists, 'accounts directory should exist');
225
+
226
+ // Check email index
227
+ const emailIndex = await fs.readJson(path.join(accountsDir, '_email_index.json'));
228
+ assert.ok(emailIndex[uniqueEmail], 'email index should contain account');
229
+ });
230
+
231
+ it('should hash passwords', async () => {
232
+ const uniqueName = `hashed${Date.now()}`;
233
+ const uniqueEmail = `hashed${Date.now()}@example.com`;
234
+
235
+ const res = await fetch(`http://${TEST_HOST}:${TEST_PORT + 1}/.pods`, {
236
+ method: 'POST',
237
+ headers: { 'Content-Type': 'application/json' },
238
+ body: JSON.stringify({
239
+ name: uniqueName,
240
+ email: uniqueEmail,
241
+ password: 'mypassword',
242
+ }),
243
+ });
244
+
245
+ assert.strictEqual(res.status, 201, 'pod creation should succeed');
246
+
247
+ // Read account file
248
+ const accountsDir = path.join(ACCOUNTS_DATA_DIR, '.idp', 'accounts');
249
+ const emailIndex = await fs.readJson(path.join(accountsDir, '_email_index.json'));
250
+ const accountId = emailIndex[uniqueEmail];
251
+ const account = await fs.readJson(path.join(accountsDir, `${accountId}.json`));
252
+
253
+ // Password should be hashed, not plain text
254
+ assert.ok(account.passwordHash, 'should have passwordHash');
255
+ assert.ok(account.passwordHash.startsWith('$2'), 'should be bcrypt hash');
256
+ assert.ok(!account.password, 'should not store plain password');
257
+ });
258
+ });