rasler 0.1.0 → 0.2.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rasler",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "RASLer — RASL protocol node implementation with content-addressed storage and MASL support",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Wes Biggs <github@wbig.gs>",
@@ -20,6 +20,24 @@
20
20
  ],
21
21
  "type": "module",
22
22
  "main": "src/index.js",
23
+ "exports": {
24
+ ".": "./src/index.js",
25
+ "./server": "./src/server.js",
26
+ "./storage/store": "./src/storage/store.js",
27
+ "./storage/local-db": "./src/storage/local-db.js",
28
+ "./storage/local-blobs": "./src/storage/local-blobs.js",
29
+ "./handlers/rasl": "./src/handlers/rasl.js",
30
+ "./handlers/operator": "./src/handlers/operator.js",
31
+ "./routes/rasl": "./src/routes/rasl.js",
32
+ "./routes/operator": "./src/routes/operator.js",
33
+ "./routes/mountPoints": "./src/routes/mountPoints.js",
34
+ "./masl/document": "./src/masl/document.js",
35
+ "./crypto/cid": "./src/crypto/cid.js",
36
+ "./middleware/auth": "./src/middleware/auth.js",
37
+ "./util/normalizeMountPath": "./src/util/normalizeMountPath.js",
38
+ "./util/parseSize": "./src/util/parseSize.js",
39
+ "./util/parseJsonConfig": "./src/util/parseJsonConfig.js"
40
+ },
23
41
  "files": [
24
42
  "src/",
25
43
  "openapi.json",
@@ -28,9 +46,9 @@
28
46
  "scripts": {
29
47
  "start": "node src/index.js",
30
48
  "lint": "eslint src/ scripts/ test/",
31
- "test": "npm run lint && NODE_OPTIONS=--experimental-vm-modules jest --runInBand",
32
- "test:unit": "NODE_OPTIONS=--experimental-vm-modules jest test/unit --runInBand",
33
- "test:integration": "NODE_OPTIONS=--experimental-vm-modules jest test/integration --runInBand",
49
+ "test": "npm run lint && vitest run",
50
+ "test:unit": "vitest run test/unit",
51
+ "test:integration": "vitest run test/integration",
34
52
  "generate:openapi": "node scripts/generate-openapi.js",
35
53
  "generate:docs": "node scripts/generate-docs.js",
36
54
  "get-in-the-car": "node scripts/get-in-the-car.js"
@@ -40,27 +58,18 @@
40
58
  "@ipld/dag-cbor": "10.0.1",
41
59
  "express": "5.2.1",
42
60
  "micromatch": "4.0.8",
43
- "multer": "2.1.1",
61
+ "multer": "2.2.0",
44
62
  "multiformats": "14.0.0",
45
63
  "swagger-ui-express": "5.0.1"
46
64
  },
47
65
  "devDependencies": {
48
66
  "@eslint/js": "10.0.1",
49
- "@jest/globals": "30.4.1",
50
- "eslint": "10.4.1",
67
+ "eslint": "10.5.0",
51
68
  "globals": "17.6.0",
52
- "jest": "30.4.2",
53
- "marked": "18.0.4",
69
+ "marked": "18.0.5",
54
70
  "supertest": "7.2.2",
55
- "swagger-jsdoc": "6.3.0"
56
- },
57
- "jest": {
58
- "transform": {},
59
- "testEnvironment": "node",
60
- "forceExit": true,
61
- "testMatch": [
62
- "<rootDir>/test/**/*.test.js"
63
- ]
71
+ "swagger-jsdoc": "6.3.0",
72
+ "vitest": "4.1.9"
64
73
  },
65
74
  "engines": {
66
75
  "node": ">=23.4.0"
@@ -0,0 +1,290 @@
1
+ import { CarReader } from '@ipld/car';
2
+ import { base32 } from 'multiformats/bases/base32';
3
+ import * as dagCbor from '@ipld/dag-cbor';
4
+ import { computeDataCid, computeMaslCid, isMaslCid } from '../crypto/cid.js';
5
+ import { createSingleMasl, parseMasl, maslLinkedCids, maslIsBundle } from '../masl/document.js';
6
+ import { normalizeMountPath } from '../util/normalizeMountPath.js';
7
+
8
+ function isCarFilename(name) {
9
+ return name?.toLowerCase().endsWith('.car');
10
+ }
11
+
12
+ function isCarMimeType(type) {
13
+ return type === 'application/vnd.ipld.car' || type === 'application/car';
14
+ }
15
+
16
+ export function isCarFile({ name, type }) {
17
+ return isCarMimeType(type) || isCarFilename(name);
18
+ }
19
+
20
+ // Extract, verify, and return all blocks from a CAR file buffer.
21
+ // Returns { maslCid, blocks: Map<cidStr, Uint8Array>, links } or throws { status, error, ...extra }.
22
+ export async function readCar(fileBuffer) {
23
+ let reader;
24
+ try {
25
+ reader = await CarReader.fromBytes(fileBuffer);
26
+ } catch {
27
+ throw { status: 400, error: 'Invalid CAR file' };
28
+ }
29
+
30
+ const blocks = new Map();
31
+ try {
32
+ for await (const { cid, bytes } of reader.blocks()) {
33
+ const cidStr = cid.toString(base32);
34
+ const isMasl = cid.code === dagCbor.code;
35
+ const actual = isMasl ? await computeMaslCid(bytes) : await computeDataCid(bytes);
36
+ if (actual !== cidStr) throw { status: 400, error: `CID mismatch for block ${cidStr}` };
37
+ blocks.set(cidStr, bytes);
38
+ }
39
+ } catch (err) {
40
+ if (err.status) throw err;
41
+ throw { status: 400, error: `Failed to read CAR blocks: ${err.message}` };
42
+ }
43
+
44
+ const roots = await reader.getRoots();
45
+ const maslRoot = roots.find(cid => cid.code === dagCbor.code);
46
+ if (!maslRoot) throw { status: 400, error: 'CAR root must be a MASL CID (dag-cbor codec)' };
47
+
48
+ const maslCid = maslRoot.toString(base32);
49
+ if (!blocks.has(maslCid)) throw { status: 400, error: 'MASL root block is missing from the CAR' };
50
+
51
+ let doc;
52
+ try { doc = parseMasl(blocks.get(maslCid)); } catch {
53
+ throw { status: 400, error: 'Failed to parse MASL document' };
54
+ }
55
+
56
+ const links = maslLinkedCids(doc);
57
+ const missing = links.filter(l => !blocks.has(l.cid)).map(l => l.cid);
58
+ if (missing.length > 0) throw { status: 400, error: 'CAR is missing linked data CIDs', missing };
59
+
60
+ return { maslCid, blocks, links };
61
+ }
62
+
63
+ // files: Array<{ name: string, type: string, bytes: Uint8Array }>
64
+ // Returns { status, body }
65
+ export async function handleUpload(store, files) {
66
+ if (!files || files.length === 0) {
67
+ return { status: 400, body: { error: 'No files provided' } };
68
+ }
69
+
70
+ const uploads = [];
71
+
72
+ for (const file of files) {
73
+ if (isCarFile(file)) {
74
+ let parsed;
75
+ try {
76
+ parsed = await readCar(file.bytes);
77
+ } catch (err) {
78
+ return {
79
+ status: err.status ?? 400,
80
+ body: err.missing ? { error: err.error, missing: err.missing } : { error: err.error },
81
+ };
82
+ }
83
+
84
+ const { maslCid, blocks, links } = parsed;
85
+ const totalBytes = [...blocks.values()].reduce((n, b) => n + b.length, 0);
86
+ if (await store.getPoolAvailable() < totalBytes) {
87
+ if (!await store.evictIfNeeded(totalBytes)) {
88
+ return { status: 507, body: { error: 'Insufficient storage' } };
89
+ }
90
+ }
91
+
92
+ await store.putContent(maslCid, blocks.get(maslCid));
93
+ for (const link of links) {
94
+ await store.putContent(link.cid, blocks.get(link.cid), { maslCid });
95
+ }
96
+ uploads.push({ filename: file.name, maslCid });
97
+
98
+ } else {
99
+ const { bytes, name = 'upload', type = 'application/octet-stream' } = file;
100
+ const size = bytes.length;
101
+
102
+ let dataCid;
103
+ try { dataCid = await computeDataCid(bytes); } catch {
104
+ return { status: 500, body: { error: `CID computation failed for ${name}` } };
105
+ }
106
+
107
+ let maslResult;
108
+ try {
109
+ maslResult = await createSingleMasl({ name, type, size, dataCid });
110
+ } catch {
111
+ return { status: 500, body: { error: `MASL creation failed for ${name}` } };
112
+ }
113
+
114
+ const { cborBytes, maslCid } = maslResult;
115
+ const totalBytes = bytes.length + cborBytes.length;
116
+ if (await store.getPoolAvailable() < totalBytes) {
117
+ if (!await store.evictIfNeeded(totalBytes)) {
118
+ return { status: 507, body: { error: 'Insufficient storage' } };
119
+ }
120
+ }
121
+
122
+ await store.putContent(dataCid, bytes, { maslCid });
123
+ await store.putContent(maslCid, cborBytes);
124
+ uploads.push({ filename: name, maslCid });
125
+ }
126
+ }
127
+
128
+ return { status: 200, body: { uploads } };
129
+ }
130
+
131
+ // cids: string[]
132
+ // Returns { status, body }
133
+ export async function handlePin(store, cids) {
134
+ if (!Array.isArray(cids) || cids.length === 0) {
135
+ return { status: 400, body: { error: 'cids must be a non-empty array' } };
136
+ }
137
+
138
+ const pinned = new Set();
139
+
140
+ for (const cid of cids) {
141
+ const entry = await store.getContent(cid);
142
+ if (!entry) return { status: 404, body: { error: `CID not found: ${cid}` } };
143
+
144
+ await store.setPinned(cid, true);
145
+ pinned.add(cid);
146
+
147
+ const maslCid = entry.meta.masl_cid;
148
+ if (maslCid && await store.hasContent(maslCid)) {
149
+ await store.setPinned(maslCid, true);
150
+ pinned.add(maslCid);
151
+ }
152
+
153
+ for (const row of await store.listContent()) {
154
+ if (row.masl_cid === cid) {
155
+ await store.setPinned(row.cid, true);
156
+ pinned.add(row.cid);
157
+ }
158
+ }
159
+ }
160
+
161
+ return { status: 200, body: { pinned: [...pinned] } };
162
+ }
163
+
164
+ export async function handleUnpin(store, cid) {
165
+ const entry = await store.getContent(cid);
166
+ if (!entry) return { status: 200, body: { status: 'not found' } };
167
+
168
+ await store.setPinned(cid, false);
169
+
170
+ if (entry.meta.masl_cid && await store.hasContent(entry.meta.masl_cid)) {
171
+ await store.setPinned(entry.meta.masl_cid, false);
172
+ }
173
+
174
+ for (const row of await store.listContent()) {
175
+ if (row.masl_cid === cid) await store.setPinned(row.cid, false);
176
+ }
177
+
178
+ return { status: 200, body: { status: 'ok' } };
179
+ }
180
+
181
+ export async function handleListContent(store, limit, cursor) {
182
+ const items = await store.listContentPage(limit, cursor);
183
+ const total = await store.countContent();
184
+ const nextCursor = items.length === limit ? items[items.length - 1].cid : null;
185
+ return {
186
+ status: 200,
187
+ body: {
188
+ total,
189
+ items: items.map(row => ({
190
+ cid: row.cid,
191
+ maslCid: row.masl_cid ?? null,
192
+ size: row.size,
193
+ pinned: row.pinned === 1,
194
+ lastRequested: row.last_requested ?? null,
195
+ })),
196
+ nextCursor,
197
+ },
198
+ };
199
+ }
200
+
201
+ export async function handleGetContent(store, cid) {
202
+ const meta = await store.getContentMeta(cid);
203
+ if (!meta) return { status: 404, body: { error: 'CID not found' } };
204
+ return {
205
+ status: 200,
206
+ body: {
207
+ cid: meta.cid,
208
+ maslCid: meta.masl_cid ?? null,
209
+ size: meta.size,
210
+ pinned: meta.pinned === 1,
211
+ lastRequested: meta.last_requested ?? null,
212
+ },
213
+ };
214
+ }
215
+
216
+ export async function handleDeleteContent(store, cid) {
217
+ const entry = await store.getContent(cid);
218
+ if (!entry) return { status: 404, body: { error: 'CID not found' } };
219
+
220
+ const deleted = [];
221
+
222
+ if (isMaslCid(cid)) {
223
+ let linkedCids = [];
224
+ try {
225
+ linkedCids = maslLinkedCids(parseMasl(entry.bytes)).map(l => l.cid);
226
+ } catch {
227
+ // Unparseable MASL — fall through and delete just the MASL itself
228
+ }
229
+ for (const linkedCid of linkedCids) {
230
+ if (await store.hasContent(linkedCid)) {
231
+ await store.deleteContent(linkedCid);
232
+ deleted.push(linkedCid);
233
+ }
234
+ }
235
+ }
236
+
237
+ await store.deleteContent(cid);
238
+ deleted.push(cid);
239
+
240
+ return { status: 200, body: { deleted } };
241
+ }
242
+
243
+ export async function handleSetMountPoint(store, hostnameParam, prefixParam) {
244
+ return async function (maslCid) {
245
+ if (!maslCid || typeof maslCid !== 'string') {
246
+ return { status: 400, body: { error: 'maslCid is required' } };
247
+ }
248
+ if (!isMaslCid(maslCid)) {
249
+ return { status: 400, body: { error: 'maslCid must be a dag-cbor CID' } };
250
+ }
251
+ const entry = await store.getContent(maslCid);
252
+ if (!entry) return { status: 404, body: { error: 'CID not held locally' } };
253
+ let doc;
254
+ try { doc = parseMasl(entry.bytes); } catch {
255
+ return { status: 400, body: { error: 'Failed to parse MASL document' } };
256
+ }
257
+ if (!maslIsBundle(doc)) {
258
+ return { status: 400, body: { error: 'maslCid must refer to a bundle MASL' } };
259
+ }
260
+ const hostname = hostnameParam === '-' ? '' : hostnameParam;
261
+ const prefix = normalizeMountPath(prefixParam || '/');
262
+ await store.setPinned(maslCid, true);
263
+ await store.setMountPoint(hostname, prefix, maslCid);
264
+ return { status: 200, body: { hostname: hostname || null, mountPath: prefix || '/', maslCid } };
265
+ };
266
+ }
267
+
268
+ export async function handleDeleteMountPoint(store, hostnameParam, prefixParam) {
269
+ const hostname = hostnameParam === '-' ? '' : hostnameParam;
270
+ const prefix = normalizeMountPath(prefixParam || '/');
271
+ const exists = store.runtimeMountPoints.some(mp => mp.hostname === hostname && mp.prefix === prefix);
272
+ if (!exists) {
273
+ return { status: 404, body: { error: 'Runtime virtual host mapping not found' } };
274
+ }
275
+ await store.deleteMountPoint(hostname, prefix);
276
+ return { status: 200, body: { status: 'ok' } };
277
+ }
278
+
279
+ export async function handleGetStatus(store, selfOrigin) {
280
+ return {
281
+ origin: selfOrigin,
282
+ storage: {
283
+ totalCapacity: store.totalCapacity,
284
+ poolUsed: await store.getPoolUsed(),
285
+ poolAvailable: await store.getPoolAvailable(),
286
+ pinnedUsed: await store.getPinnedUsed(),
287
+ pinnedCount: await store.countPinned(),
288
+ },
289
+ };
290
+ }
@@ -0,0 +1,119 @@
1
+ import {
2
+ parseMasl, maslContentHeaders, maslIsBundle,
3
+ resolveBundleEntry, maslLinkedCids, findBundleHeadersForCid,
4
+ } from '../masl/document.js';
5
+ import { isMaslCid, cidToUnencodedDigest } from '../crypto/cid.js';
6
+
7
+ // Resolves a RASL request for the given CID and path suffix.
8
+ //
9
+ // pathSuffix is the portion of the URL after the CID:
10
+ // '' → path-free form (raw bytes)
11
+ // '/' → bundle root
12
+ // '/foo.css' → bundle resource
13
+ //
14
+ // Returns one of:
15
+ // null → local miss; caller should delegate (next())
16
+ // { status, headers, bytes } → complete in-memory response
17
+ // { status, headers, stream, size } → streaming response (stream is platform-specific)
18
+ // { status, body: { error, ...extra } } → error response
19
+ export async function resolveRaslRequest(store, cid, pathSuffix) {
20
+ const meta = await store.getContentMeta(cid);
21
+ if (!meta) return null;
22
+
23
+ await store.recordRequest(cid);
24
+
25
+ const isPathFree = pathSuffix === '';
26
+
27
+ if (isPathFree) {
28
+ if (isMaslCid(cid)) {
29
+ const entry = await store.getContent(cid);
30
+ if (!entry) return null;
31
+ return {
32
+ status: 200,
33
+ headers: {
34
+ 'content-type': 'application/octet-stream',
35
+ 'unencoded-digest': cidToUnencodedDigest(cid),
36
+ },
37
+ bytes: entry.bytes,
38
+ };
39
+ }
40
+
41
+ let headers = { 'content-type': 'application/octet-stream' };
42
+ if (meta.masl_cid) {
43
+ const maslEntry = await store.getContent(meta.masl_cid);
44
+ if (maslEntry) {
45
+ try {
46
+ const doc = parseMasl(maslEntry.bytes);
47
+ headers = maslIsBundle(doc)
48
+ ? (findBundleHeadersForCid(doc, cid) ?? headers)
49
+ : maslContentHeaders(doc);
50
+ } catch { /* ignore */ }
51
+ }
52
+ }
53
+
54
+ const streamResult = await store.getContentStream(cid);
55
+ if (!streamResult) return { status: 502, body: { error: 'Content unavailable' } };
56
+ return {
57
+ status: 200,
58
+ headers: { ...headers, 'unencoded-digest': cidToUnencodedDigest(cid) },
59
+ stream: streamResult.stream,
60
+ size: streamResult.meta.size,
61
+ };
62
+ }
63
+
64
+ // Path-bearing: resolve against MASL document structure.
65
+ const path = pathSuffix;
66
+
67
+ if (isMaslCid(cid)) {
68
+ const entry = await store.getContent(cid);
69
+ if (!entry) return null;
70
+ let doc;
71
+ try { doc = parseMasl(entry.bytes); } catch {
72
+ return { status: 500, body: { error: 'Invalid MASL document' } };
73
+ }
74
+
75
+ if (maslIsBundle(doc)) {
76
+ const resolved = resolveBundleEntry(doc, path);
77
+ if (!resolved) return { status: 404, body: { error: 'Not found' } };
78
+ await store.recordRequest(resolved.cid);
79
+ const streamResult = await store.getContentStream(resolved.cid);
80
+ if (!streamResult) return { status: 502, body: { error: 'Content unavailable' } };
81
+ return {
82
+ status: 200,
83
+ headers: { ...resolved.headers, 'unencoded-digest': cidToUnencodedDigest(resolved.cid) },
84
+ stream: streamResult.stream,
85
+ size: streamResult.meta.size,
86
+ };
87
+ }
88
+
89
+ // Single mode: only path '/' is valid.
90
+ const links = maslLinkedCids(doc);
91
+ const srcCid = links.length > 0 ? links[0].cid : null;
92
+ if (srcCid) {
93
+ if (path !== '/') return { status: 404, body: { error: 'Not found' } };
94
+ await store.recordRequest(srcCid);
95
+ const streamResult = await store.getContentStream(srcCid);
96
+ if (!streamResult) return { status: 502, body: { error: 'Content unavailable' } };
97
+ return {
98
+ status: 200,
99
+ headers: { ...maslContentHeaders(doc), 'unencoded-digest': cidToUnencodedDigest(srcCid) },
100
+ stream: streamResult.stream,
101
+ size: streamResult.meta.size,
102
+ };
103
+ }
104
+ }
105
+
106
+ // Non-MASL CID (or unrecognised DRISL map): only path '/' returns raw bytes.
107
+ if (path !== '/') return { status: 404, body: { error: 'Not found' } };
108
+ const streamResult = await store.getContentStream(cid);
109
+ if (!streamResult) return { status: 502, body: { error: 'Content unavailable' } };
110
+ return {
111
+ status: 200,
112
+ headers: {
113
+ 'content-type': 'application/octet-stream',
114
+ 'unencoded-digest': cidToUnencodedDigest(cid),
115
+ },
116
+ stream: streamResult.stream,
117
+ size: streamResult.meta.size,
118
+ };
119
+ }
package/src/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import config from './config.js';
2
2
  import { openDb } from './storage/db.js';
3
+ import { makeLocalDb } from './storage/local-db.js';
4
+ import { makeLocalBlobs } from './storage/local-blobs.js';
3
5
  import { Store } from './storage/store.js';
4
6
  import { createApp, finalizeApp } from './server.js';
5
7
  import { makeRaslNotFoundHandler } from './routes/rasl.js';
@@ -8,15 +10,13 @@ import { indexStaticRoots } from './static.js';
8
10
  import { startStaticRootWatchers } from './watcher.js';
9
11
 
10
12
  function main() {
11
- const db = openDb(config.dataDir);
12
- const store = new Store(db, config.dataDir, config.totalCapacity, {
13
+ const rawDb = openDb(config.dataDir);
14
+ const db = makeLocalDb(rawDb);
15
+ const blobs = makeLocalBlobs(config.dataDir);
16
+ const store = new Store(db, blobs, config.totalCapacity, {
13
17
  staticRoots: config.staticRoots,
14
18
  });
15
19
 
16
- // Index static roots in the background so the server starts immediately.
17
- // Watchers start after the initial index so startup writes don't trigger
18
- // spurious re-indexes. On a warm restart (no file changes) the mtime cache
19
- // makes the window negligible.
20
20
  if (config.staticRoots.length > 0) {
21
21
  indexStaticRoots(config.staticRoots, store, { maxHistory: config.staticMaxHistory })
22
22
  .then(() => startStaticRootWatchers(config.staticRoots, store, { maxHistory: config.staticMaxHistory }));
@@ -4,10 +4,6 @@ import { parseMasl, resolveBundleEntry } from '../masl/document.js';
4
4
  import { cidToUnencodedDigest } from '../crypto/cid.js';
5
5
  import { OPERATOR_SECRET_HEADER } from '../middleware/auth.js';
6
6
 
7
- // Returns the first mount point whose hostname and prefix match the request,
8
- // or null. mountPoints must be sorted: longer prefix first, specific hostname
9
- // before wildcard (hostname='') at equal prefix lengths.
10
- // hostname='' matches any Host: value (or no Host: header).
11
7
  function findMountPoint(mountPoints, hostname, path) {
12
8
  for (const mp of mountPoints) {
13
9
  if (mp.hostname !== '' && mp.hostname !== hostname) continue;
@@ -18,28 +14,17 @@ function findMountPoint(mountPoints, hostname, path) {
18
14
  return null;
19
15
  }
20
16
 
21
- // Serves content by resolving the request path against the bundle MASL for
22
- // the matching mount point. Sits before the RASL router so browser clients
23
- // can use normal URLs; RASL paths are passed through unchanged.
24
- //
25
- // The MASL CID is read from store.staticRootMasls on every request, so it
26
- // reflects the current indexed version automatically after any re-index.
27
17
  export function makeMountPointRouter({ store, mountPoints, selfOrigin }) {
28
18
  const router = Router();
29
19
 
30
- router.use((req, res, next) => {
20
+ router.use(async (req, res, next) => {
31
21
  if (req.method !== 'GET' && req.method !== 'HEAD') return next();
32
- // Leave RASL retrieval paths to the RASL router.
33
22
  if (req.path.startsWith('/.well-known/rasl/')) return next();
34
- // Operator API requests (identified by the secret header) bypass mount-point
35
- // routing so authenticated operator paths are always reachable even when a
36
- // wildcard mount is configured.
37
23
  if (req.headers[OPERATOR_SECRET_HEADER]) return next();
38
24
 
39
25
  let maslCid;
40
26
  let maslPath;
41
27
 
42
- // Runtime mappings take priority over static-root mappings.
43
28
  const runtimeMp = findMountPoint(store.runtimeMountPoints, req.hostname, req.path);
44
29
  if (runtimeMp) {
45
30
  maslCid = runtimeMp.maslCid;
@@ -57,7 +42,7 @@ export function makeMountPointRouter({ store, mountPoints, selfOrigin }) {
57
42
  maslPath = staticMp.prefix ? req.path.slice(staticMp.prefix.length) || '/' : req.path;
58
43
  }
59
44
 
60
- const maslEntry = store.getContent(maslCid);
45
+ const maslEntry = await store.getContent(maslCid);
61
46
  if (!maslEntry) return res.status(503).json({ error: 'MASL unavailable' });
62
47
 
63
48
  let doc;
@@ -68,10 +53,10 @@ export function makeMountPointRouter({ store, mountPoints, selfOrigin }) {
68
53
  const resolved = resolveBundleEntry(doc, maslPath);
69
54
  if (!resolved) return res.status(404).send('Not found');
70
55
 
71
- store.recordRequest(maslCid);
72
- store.recordRequest(resolved.cid);
56
+ await store.recordRequest(maslCid);
57
+ await store.recordRequest(resolved.cid);
73
58
 
74
- const result = store.getContentStream(resolved.cid);
59
+ const result = await store.getContentStream(resolved.cid);
75
60
  if (!result) return res.status(502).json({ error: 'Content unavailable' });
76
61
 
77
62
  res.set(resolved.headers);