javascript-solid-server 0.0.2

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,15 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebSearch",
5
+ "WebFetch(domain:github.com)",
6
+ "WebFetch(domain:raw.githubusercontent.com)",
7
+ "Bash(cat:*)",
8
+ "WebFetch(domain:www.inrupt.com)",
9
+ "Bash(npm install:*)",
10
+ "Bash(timeout 3 node:*)",
11
+ "Bash(PORT=3030 timeout 3 node:*)",
12
+ "Bash(git commit:*)"
13
+ ]
14
+ }
15
+ }
package/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # Vision
2
+
3
+ The goal of this project is to create a hyper-modern, performant, and minimalist JavaScript Solid server. While drawing inspiration from Node Solid Server (NSS), this new implementation will address its shortcomings and prioritize scalability, modularity, and developer usability.
4
+
5
+ ## Key Objectives
6
+
7
+ - **Performance First:** Capable of handling enterprise-scale loads, targeting thousands to millions of users.
8
+ - **Minimalist Design:** Remove unused and experimental features; focus on what matters most.
9
+ - **Modularity:** Clear separation of identity, authentication, storage, and onboarding.
10
+ - **Developer Friendly:** Clean, well-documented, and extensible codebase that adheres to the Solid specification.
11
+ - **Modern Tooling:** Leverage async/await, native modules, fast HTTP servers like Fastify, and cutting-edge JavaScript runtimes.
12
+ - **HTTP Simplicity:** Prioritize simple HTTP/1.1 compatibility for maximum interoperability.
13
+ - **Frontend Agnostic:** Work with any frontend or application layer via standardized APIs.
14
+ - **Testable and CI Ready:** Fully integrated with Solid test suites and modern CI/CD pipelines.
15
+
16
+ ---
17
+
18
+ # ARCHITECTURE
19
+
20
+ ## Overview
21
+
22
+ The architecture is inspired by NSS but modernized and streamlined. Each subsystem is designed to operate independently and follow the single-responsibility principle.
23
+
24
+ ### Components
25
+
26
+ - **HTTP Layer**
27
+
28
+ - Fastify server
29
+ - Routing and middleware based on HTTP verbs and Solid operations
30
+ - Blazingly fast, with benchmarks from the start
31
+
32
+ - **Identity Provider (IDP)**
33
+
34
+ - Handles Pod based WebIDs
35
+ - Handles external WebIDs
36
+ - Minimal by default, extendable via plugins
37
+
38
+ - **Authenticaion Module (AUthn)**
39
+
40
+ - Handles WebID-based authentication, including WebID-TLS
41
+ - OIDC-compliant with modular Authentication
42
+ - Single sign-on including WebID-TLS
43
+
44
+ - **Authorization Module (Authz)**
45
+
46
+ - Supports Web Access Control (WAC)
47
+ - Token-based permissions model
48
+ - Modular Authorization system
49
+
50
+ - **Storage Engine**
51
+
52
+ - Modular backend adapters (e.g. file system, S3, memory)
53
+ - POD-level quota management (optional)
54
+ - Interoperable with existing Cloud
55
+
56
+ - **Account and Onboarding**
57
+ - API-first registration
58
+ - Public, private, invite modes
59
+ - Extensible account templates
60
+
61
+ ### Deployment Model
62
+
63
+ - Works as a single binary or serverless function
64
+ - Container-friendly (Docker, Deno, etc.)
65
+ - CLI for local dev setup and testing
66
+
67
+ ### Separation of Concerns
68
+
69
+ - Each subsystem lives in its own module/package
70
+ - Clear boundaries between IDP and storage
71
+ - Frontend-independent API endpoints
72
+
73
+ ### Compatibility
74
+
75
+ - Solid-compliant, LWS Compliant
76
+ - API parity with NSS where applicable
77
+ - API parity with CSS where applicable
78
+
79
+ ---
80
+
81
+ # MVP Implementation
82
+
83
+ This is a minimal viable product (MVP) implementation of the JavaScriptSolid server. It includes the core components needed to demonstrate the concept while omitting some of the more complex features for simplicity.
84
+
85
+ ## Getting Started
86
+
87
+ ### Prerequisites
88
+
89
+ - Node.js 18 or higher
90
+
91
+ ### Installation
92
+
93
+ ```bash
94
+ # Clone the repository
95
+ git clone https://github.com/yourusername/javascript-solid-server.git
96
+ cd javascript-solid-server
97
+
98
+ # Install dependencies
99
+ npm install
100
+ ```
101
+
102
+ ### Running the Server
103
+
104
+ ```bash
105
+ # Start the server
106
+ npm start
107
+ ```
108
+
109
+ The server will be available at http://localhost:3000 by default.
110
+
111
+ ## Features Included in MVP
112
+
113
+ - **HTTP Server**: Based on Fastify for high performance
114
+ - **Basic Identity Provider**: Simple user registration and login with JWT tokens
115
+ - **Simple Authorization**: Basic implementation of WAC (Web Access Control)
116
+ - **File-based Storage**: Local filesystem storage for Solid resources
117
+ - **Basic Solid Protocol Support**: GET, PUT, DELETE, PATCH, and HEAD operations
118
+
119
+ ## Features Omitted in MVP (to be added later)
120
+
121
+ 1. WebID-TLS Authentication
122
+ 2. Full OIDC implementation
123
+ 3. Advanced WAC features and ACL file parsing
124
+ 4. Quotas and resource limits
125
+ 5. Advanced container management
126
+ 6. SPARQL and N3 Patch support
127
+ 7. Notification systems
128
+
129
+ ## API Usage Examples
130
+
131
+ ### User Registration
132
+
133
+ ```bash
134
+ curl -X POST http://localhost:3000/register \
135
+ -H "Content-Type: application/json" \
136
+ -d '{"username": "alice", "password": "secret", "email": "alice@example.com"}'
137
+ ```
138
+
139
+ ### Login
140
+
141
+ ```bash
142
+ curl -X POST http://localhost:3000/login \
143
+ -H "Content-Type: application/json" \
144
+ -d '{"username": "alice", "password": "secret"}'
145
+ ```
146
+
147
+ ### Accessing Resources
148
+
149
+ ```bash
150
+ # Get a resource
151
+ curl -X GET http://localhost:3000/alice/profile \
152
+ -H "Authorization: Bearer YOUR_TOKEN_HERE"
153
+
154
+ # Create or update a resource
155
+ curl -X PUT http://localhost:3000/alice/profile \
156
+ -H "Authorization: Bearer YOUR_TOKEN_HERE" \
157
+ -H "Content-Type: text/turtle" \
158
+ -d '@prefix foaf: <http://xmlns.com/foaf/0.1/>. <#me> a foaf:Person; foaf:name "Alice".'
159
+ ```
160
+
161
+ ## Performance Benchmarking
162
+
163
+ This project includes a comprehensive benchmarking tool to measure server performance under various loads.
164
+
165
+ ### Running the Benchmark
166
+
167
+ ```bash
168
+ # Make sure the server is running in a separate terminal
169
+ npm start
170
+
171
+ # In another terminal, run the benchmark
172
+ npm run benchmark
173
+ ```
174
+
175
+ The benchmark will:
176
+
177
+ 1. Create multiple test users
178
+ 2. Execute various operations (register, login, read, write, delete)
179
+ 3. Measure response times for each operation type
180
+ 4. Test different concurrency levels (1, 5, 10, 50, 100 users)
181
+ 5. Calculate throughput (operations per second)
182
+
183
+ ### Visualizing Results
184
+
185
+ After running the benchmark, you can generate a visual report:
186
+
187
+ ```bash
188
+ # Generate an HTML report with charts
189
+ npm run visualize benchmark-report-[timestamp].json
190
+ ```
191
+
192
+ Open the generated HTML file in a browser to see:
193
+
194
+ - Average response times for each operation type
195
+ - Throughput metrics at different concurrency levels
196
+ - Visual charts for easy performance analysis
197
+
198
+ ### Customizing Benchmarks
199
+
200
+ You can customize the benchmark parameters in `benchmark.js`:
201
+
202
+ - Concurrent users levels
203
+ - Operations per user
204
+ - Test duration
205
+ - Test user credentials
206
+
207
+ ## Contributing
208
+
209
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,44 @@
1
+ {
2
+ "timestamp": "2025-03-31T14:25:24.234Z",
3
+ "server": "http://nostr.social:3000",
4
+ "testDuration": 30000,
5
+ "averageResponseTimes": {
6
+ "register": 49.28774029878249,
7
+ "login": 49.07647125548627,
8
+ "read": 145.50252931779679,
9
+ "write": 143.20483738812226,
10
+ "delete": 145.5004399698014
11
+ },
12
+ "throughputResults": [
13
+ {
14
+ "concurrentUsers": 1,
15
+ "operations": 300,
16
+ "duration": 629.3636230230331,
17
+ "throughput": 476.67197312581374
18
+ },
19
+ {
20
+ "concurrentUsers": 5,
21
+ "operations": 1500,
22
+ "duration": 3072.7389999628067,
23
+ "throughput": 488.16381736885444
24
+ },
25
+ {
26
+ "concurrentUsers": 10,
27
+ "operations": 3000,
28
+ "duration": 6042.516402959824,
29
+ "throughput": 496.4818959416479
30
+ },
31
+ {
32
+ "concurrentUsers": 50,
33
+ "operations": 14000,
34
+ "duration": 30000,
35
+ "throughput": 466.6666666666667
36
+ },
37
+ {
38
+ "concurrentUsers": 100,
39
+ "operations": 13900,
40
+ "duration": 30000,
41
+ "throughput": 463.3333333333333
42
+ }
43
+ ]
44
+ }
package/benchmark.js ADDED
@@ -0,0 +1,286 @@
1
+ import fetch from 'node-fetch';
2
+ import { performance } from 'perf_hooks';
3
+ import { promises as fs } from 'fs';
4
+ import path from 'path';
5
+
6
+ // Configuration
7
+ const config = {
8
+ baseUrl: 'http://nostr.social:3000',
9
+ concurrentUsers: [1, 5, 10, 50, 100], // Different concurrency levels to test
10
+ operations: 100, // Operations per user
11
+ testDuration: 30000, // 30 seconds per test
12
+ testUserPrefix: 'testuser',
13
+ testPassword: 'benchmark123',
14
+ results: {
15
+ registerTime: [],
16
+ loginTime: [],
17
+ readTime: [],
18
+ writeTime: [],
19
+ deleteTime: [],
20
+ throughput: []
21
+ }
22
+ };
23
+
24
+ // Store tokens for authenticated requests
25
+ const tokens = new Map();
26
+
27
+ // Utility function to measure execution time
28
+ async function measureTime (fn) {
29
+ const start = performance.now();
30
+ const result = await fn();
31
+ const end = performance.now();
32
+ return { result, time: end - start };
33
+ }
34
+
35
+ // Register a test user
36
+ async function registerUser (username) {
37
+ const { result, time } = await measureTime(async () => {
38
+ const response = await fetch(`${config.baseUrl}/register`, {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({
42
+ username,
43
+ password: config.testPassword,
44
+ email: `${username}@benchmark.test`
45
+ })
46
+ });
47
+ return response.json();
48
+ });
49
+
50
+ config.results.registerTime.push(time);
51
+ return result;
52
+ }
53
+
54
+ // Login a test user
55
+ async function loginUser (username) {
56
+ const { result, time } = await measureTime(async () => {
57
+ const response = await fetch(`${config.baseUrl}/login`, {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify({
61
+ username,
62
+ password: config.testPassword
63
+ })
64
+ });
65
+ return response.json();
66
+ });
67
+
68
+ config.results.loginTime.push(time);
69
+
70
+ if (result.id_token) {
71
+ tokens.set(username, result.id_token);
72
+ }
73
+
74
+ return result;
75
+ }
76
+
77
+ // Create a resource
78
+ async function createResource (username, resourcePath, content = null) {
79
+ const token = tokens.get(username);
80
+ if (!token) throw new Error(`No token for user ${username}`);
81
+
82
+ const turtleContent = content || `
83
+ @prefix foaf: <http://xmlns.com/foaf/0.1/>.
84
+ <#me> a foaf:Person;
85
+ foaf:name "${username}";
86
+ foaf:mbox <mailto:${username}@benchmark.test>.
87
+ `;
88
+
89
+ const { result, time } = await measureTime(async () => {
90
+ const response = await fetch(`${config.baseUrl}/${username}/${resourcePath}`, {
91
+ method: 'PUT',
92
+ headers: {
93
+ 'Content-Type': 'text/turtle',
94
+ 'Authorization': `Bearer ${token}`
95
+ },
96
+ body: turtleContent
97
+ });
98
+ return response.status;
99
+ });
100
+
101
+ config.results.writeTime.push(time);
102
+ return result;
103
+ }
104
+
105
+ // Read a resource
106
+ async function readResource (username, resourcePath) {
107
+ const token = tokens.get(username);
108
+ if (!token) throw new Error(`No token for user ${username}`);
109
+
110
+ const { result, time } = await measureTime(async () => {
111
+ const response = await fetch(`${config.baseUrl}/${username}/${resourcePath}`, {
112
+ method: 'GET',
113
+ headers: {
114
+ 'Authorization': `Bearer ${token}`
115
+ }
116
+ });
117
+ return response.status;
118
+ });
119
+
120
+ config.results.readTime.push(time);
121
+ return result;
122
+ }
123
+
124
+ // Delete a resource
125
+ async function deleteResource (username, resourcePath) {
126
+ const token = tokens.get(username);
127
+ if (!token) throw new Error(`No token for user ${username}`);
128
+
129
+ const { result, time } = await measureTime(async () => {
130
+ const response = await fetch(`${config.baseUrl}/${username}/${resourcePath}`, {
131
+ method: 'DELETE',
132
+ headers: {
133
+ 'Authorization': `Bearer ${token}`
134
+ }
135
+ });
136
+ return response.status;
137
+ });
138
+
139
+ config.results.deleteTime.push(time);
140
+ return result;
141
+ }
142
+
143
+ // Run benchmark for specific concurrency
144
+ async function runBenchmark (concurrentUsers) {
145
+ console.log(`\n=== Running benchmark with ${concurrentUsers} concurrent users ===`);
146
+
147
+ // Create test users
148
+ console.log('Creating test users...');
149
+ const users = [];
150
+ for (let i = 0; i < concurrentUsers; i++) {
151
+ const username = `${config.testUserPrefix}${i}`;
152
+ users.push(username);
153
+ await registerUser(username);
154
+ await loginUser(username);
155
+ }
156
+
157
+ // Prepare operation queue (read/write/delete)
158
+ const operations = [];
159
+ for (const username of users) {
160
+ for (let i = 0; i < config.operations; i++) {
161
+ const resourcePath = `benchmark/resource${i}.ttl`;
162
+ operations.push(async () => await createResource(username, resourcePath));
163
+ operations.push(async () => await readResource(username, resourcePath));
164
+ operations.push(async () => await deleteResource(username, resourcePath));
165
+ }
166
+ }
167
+
168
+ // Run operations with measured throughput
169
+ console.log(`Starting operations (${operations.length} total)...`);
170
+ const startTime = performance.now();
171
+ let completedOps = 0;
172
+ const endTime = startTime + config.testDuration;
173
+
174
+ // Create chunks of operations to run in parallel
175
+ const chunks = [];
176
+ const chunkSize = operations.length > 100 ? 100 : operations.length;
177
+
178
+ for (let i = 0; i < operations.length; i += chunkSize) {
179
+ chunks.push(operations.slice(i, i + chunkSize));
180
+ }
181
+
182
+ for (const chunk of chunks) {
183
+ if (performance.now() >= endTime) break;
184
+
185
+ await Promise.all(chunk.map(async (operation) => {
186
+ if (performance.now() < endTime) {
187
+ await operation();
188
+ completedOps++;
189
+ }
190
+ }));
191
+ }
192
+
193
+ // Calculate throughput (ops/sec)
194
+ const actualDuration = Math.min(performance.now() - startTime, config.testDuration);
195
+ const throughput = (completedOps / actualDuration) * 1000;
196
+ config.results.throughput.push({
197
+ concurrentUsers,
198
+ operations: completedOps,
199
+ duration: actualDuration,
200
+ throughput
201
+ });
202
+
203
+ console.log(`Completed ${completedOps} operations in ${actualDuration.toFixed(2)}ms`);
204
+ console.log(`Throughput: ${throughput.toFixed(2)} operations/second`);
205
+ }
206
+
207
+ // Generate report
208
+ async function generateReport () {
209
+ // Calculate averages
210
+ const averages = {
211
+ register: calculateAverage(config.results.registerTime),
212
+ login: calculateAverage(config.results.loginTime),
213
+ read: calculateAverage(config.results.readTime),
214
+ write: calculateAverage(config.results.writeTime),
215
+ delete: calculateAverage(config.results.deleteTime)
216
+ };
217
+
218
+ // Create report
219
+ const report = {
220
+ timestamp: new Date().toISOString(),
221
+ server: config.baseUrl,
222
+ testDuration: config.testDuration,
223
+ averageResponseTimes: averages,
224
+ throughputResults: config.results.throughput
225
+ };
226
+
227
+ // Save report to file
228
+ await fs.writeFile(
229
+ `benchmark-report-${new Date().toISOString().replace(/:/g, '-')}.json`,
230
+ JSON.stringify(report, null, 2)
231
+ );
232
+
233
+ // Display summary
234
+ console.log('\n=== BENCHMARK RESULTS ===');
235
+ console.log('Average Response Times (ms):');
236
+ console.log(` Register: ${averages.register.toFixed(2)} ms`);
237
+ console.log(` Login: ${averages.login.toFixed(2)} ms`);
238
+ console.log(` Read: ${averages.read.toFixed(2)} ms`);
239
+ console.log(` Write: ${averages.write.toFixed(2)} ms`);
240
+ console.log(` Delete: ${averages.delete.toFixed(2)} ms`);
241
+
242
+ console.log('\nThroughput Results:');
243
+ config.results.throughput.forEach(result => {
244
+ console.log(` ${result.concurrentUsers} users: ${result.throughput.toFixed(2)} ops/sec`);
245
+ });
246
+
247
+ console.log('\nReport saved to file.');
248
+ }
249
+
250
+ // Calculate average of an array
251
+ function calculateAverage (array) {
252
+ if (array.length === 0) return 0;
253
+ return array.reduce((sum, value) => sum + value, 0) / array.length;
254
+ }
255
+
256
+ // Main benchmark function
257
+ async function startBenchmark () {
258
+ console.log('=== JavaScript Solid Server Benchmark ===');
259
+ console.log(`Server URL: ${config.baseUrl}`);
260
+ console.log(`Test Duration: ${config.testDuration / 1000} seconds per concurrency level`);
261
+
262
+ try {
263
+ // Check if server is running
264
+ const response = await fetch(config.baseUrl);
265
+ if (response.status < 200 || response.status >= 500) {
266
+ throw new Error(`Server responded with status ${response.status}`);
267
+ }
268
+ } catch (error) {
269
+ console.error('Error connecting to server:', error.message);
270
+ console.error('Please make sure the server is running before starting the benchmark.');
271
+ return;
272
+ }
273
+
274
+ // Run tests for each concurrency level
275
+ for (const concurrentUsers of config.concurrentUsers) {
276
+ await runBenchmark(concurrentUsers);
277
+ }
278
+
279
+ // Generate final report
280
+ await generateReport();
281
+ }
282
+
283
+ // Start the benchmark
284
+ startBenchmark().catch(error => {
285
+ console.error('Benchmark error:', error);
286
+ });
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "javascript-solid-server",
3
+ "version": "0.0.2",
4
+ "description": "A minimal, fast Solid server",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "node src/index.js",
9
+ "dev": "node --watch src/index.js",
10
+ "test": "node --test"
11
+ },
12
+ "dependencies": {
13
+ "fastify": "^4.25.2",
14
+ "fs-extra": "^11.2.0"
15
+ },
16
+ "engines": {
17
+ "node": ">=18.0.0"
18
+ },
19
+ "keywords": ["solid", "ldp", "linked-data", "decentralized"],
20
+ "license": "MIT"
21
+ }
@@ -0,0 +1,110 @@
1
+ import * as storage from '../storage/filesystem.js';
2
+ import { getAllHeaders } from '../ldp/headers.js';
3
+ import { isContainer } from '../utils/url.js';
4
+
5
+ /**
6
+ * Handle POST request to container (create new resource)
7
+ */
8
+ export async function handlePost(request, reply) {
9
+ const urlPath = request.url.split('?')[0];
10
+
11
+ // Ensure target is a container
12
+ if (!isContainer(urlPath)) {
13
+ return reply.code(405).send({ error: 'POST only allowed on containers' });
14
+ }
15
+
16
+ // Check container exists
17
+ const stats = await storage.stat(urlPath);
18
+ if (!stats || !stats.isDirectory) {
19
+ // Create container if it doesn't exist
20
+ await storage.createContainer(urlPath);
21
+ }
22
+
23
+ // Get slug from header or generate UUID
24
+ const slug = request.headers.slug;
25
+ const linkHeader = request.headers.link || '';
26
+
27
+ // Check if creating a container (Link header contains ldp:Container or ldp:BasicContainer)
28
+ const isCreatingContainer = linkHeader.includes('Container') || linkHeader.includes('BasicContainer');
29
+
30
+ // Generate unique filename
31
+ const filename = await storage.generateUniqueFilename(urlPath, slug, isCreatingContainer);
32
+ const newPath = urlPath + filename + (isCreatingContainer ? '/' : '');
33
+
34
+ let success;
35
+ if (isCreatingContainer) {
36
+ success = await storage.createContainer(newPath);
37
+ } else {
38
+ // Get content from request body
39
+ let content = request.body;
40
+ if (Buffer.isBuffer(content)) {
41
+ // Already a buffer
42
+ } else if (typeof content === 'string') {
43
+ content = Buffer.from(content);
44
+ } else if (content && typeof content === 'object') {
45
+ content = Buffer.from(JSON.stringify(content));
46
+ } else {
47
+ content = Buffer.from('');
48
+ }
49
+ success = await storage.write(newPath, content);
50
+ }
51
+
52
+ if (!success) {
53
+ return reply.code(500).send({ error: 'Create failed' });
54
+ }
55
+
56
+ const location = `${request.protocol}://${request.hostname}${newPath}`;
57
+ const origin = request.headers.origin;
58
+
59
+ const headers = getAllHeaders({
60
+ isContainer: isCreatingContainer,
61
+ origin
62
+ });
63
+ headers['Location'] = location;
64
+
65
+ Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
66
+ return reply.code(201).send();
67
+ }
68
+
69
+ /**
70
+ * Create a pod (container) for a user
71
+ * POST /.pods with { "name": "alice" }
72
+ */
73
+ export async function handleCreatePod(request, reply) {
74
+ const { name } = request.body || {};
75
+
76
+ if (!name || typeof name !== 'string') {
77
+ return reply.code(400).send({ error: 'Pod name required' });
78
+ }
79
+
80
+ // Validate pod name (alphanumeric, dash, underscore)
81
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
82
+ return reply.code(400).send({ error: 'Invalid pod name. Use alphanumeric, dash, or underscore only.' });
83
+ }
84
+
85
+ const podPath = `/${name}/`;
86
+
87
+ // Check if pod already exists
88
+ if (await storage.exists(podPath)) {
89
+ return reply.code(409).send({ error: 'Pod already exists' });
90
+ }
91
+
92
+ // Create pod container
93
+ const success = await storage.createContainer(podPath);
94
+ if (!success) {
95
+ return reply.code(500).send({ error: 'Failed to create pod' });
96
+ }
97
+
98
+ const location = `${request.protocol}://${request.hostname}${podPath}`;
99
+ const origin = request.headers.origin;
100
+
101
+ const headers = getAllHeaders({ isContainer: true, origin });
102
+ headers['Location'] = location;
103
+
104
+ Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
105
+
106
+ return reply.code(201).send({
107
+ name,
108
+ url: location
109
+ });
110
+ }