@blamejs/core 0.13.17 → 0.13.18
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/CHANGELOG.md +2 -0
- package/README.md +1 -1
- package/lib/middleware/body-parser.js +95 -49
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.18 (2026-05-27) — **`bodyParser` multipart can buffer uploads in memory — no tmp directory for serverless / read-only filesystems.** The multipart/form-data sub-parser previously streamed every file part to a tmp directory on disk (os.tmpdir() by default), which fails on a read-only or ephemeral serverless filesystem. A new multipart.storage option selects where file parts land: "disk" (default, unchanged — req.files[].path points at a tmp file cleaned up on response end) or "memory" (req.files[].buffer holds the assembled bytes, with no filesystem access at all). Both modes enforce the same per-file (fileSize), per-field, and total-request (totalSize) caps, so memory mode adds no new memory-exhaustion surface. The file object shape is stable across both modes — disk sets path with buffer null, memory sets buffer with path null — so a handler branches on whichever is non-null. An invalid storage value is rejected when the middleware is constructed. **Added:** *`bodyParser` multipart `storage: "memory"` — buffer uploads in RAM instead of a tmp directory* — `b.middleware.bodyParser({ multipart: { storage: "memory" } })` buffers each uploaded file part in memory and exposes it as `req.files[].buffer` (a Buffer), with no `os.tmpdir()` write and no tmp-file cleanup — the read-only / serverless path. The default `storage: "disk"` is unchanged: file parts stream to a tmp file, `req.files[].path` points at it, and it is removed when the response finishes. Both modes apply the existing `fileSize` / per-field `maxBytes` / `totalSize` caps and SHA3-512 hash each part during streaming, so memory mode is bounded by the same limits and adds no new DoS surface. The `req.files[]` shape is stable across modes (disk: `path` set, `buffer` null; memory: `buffer` set, `path` null). A `storage` value other than `"disk"` or `"memory"` throws a `TypeError` at construction.
|
|
12
|
+
|
|
11
13
|
- v0.13.17 (2026-05-27) — **Template engine can render from a string with no views directory — for serverless / read-only filesystems.** b.template.create previously required a viewsDir that exists on disk, and rendering always read the template (and its layout/partials) from that directory — unusable on a read-only or ephemeral serverless filesystem where the templates aren't on disk. The engine now accepts a source string directly: viewsDir is optional, and the returned engine exposes renderString(source, data?, opts?) and compileString(source, opts?) that compile and render from a string with no disk read. {% extends %} and {{> partial}} in a string source resolve through an operator-supplied opts.resolve(name) -> string callback (without it, an extends throws a clear error and a missing partial inlines empty, matching the file path). The same HTML-escaping, expression grammar, and extends/partial-depth caps apply. The file-backed render / compile / precompileAll still work exactly as before when a viewsDir is configured, and now refuse with a clear error when one isn't. **Added:** *`engine.renderString` / `engine.compileString` — render templates from a string, no viewsDir* — `b.template.create({})` (no `viewsDir`) returns a string-only engine; `renderString(source, data?, { resolve })` and `compileString(source, { resolve })` compile and render from a source string with zero filesystem access — the read-only / serverless path. `{% extends %}` and `{{> partial}}` resolve through `opts.resolve(name) -> string`. The HTML escaping, grammar, and depth caps are identical to the file path. When a `viewsDir` IS configured, `render`/`compile`/`precompileAll` behave exactly as before; without one they refuse with `viewsDir not configured`. `renderString(source, { resolve })` may omit the data argument — an opts object carrying a function `resolve` is recognized as opts, not data. **Security:** *Vendored `@simplewebauthn/server` refreshed 13.3.0 → 13.3.1* — The vendored WebAuthn server bundle (`b.auth.passkey`'s registration/authentication verification) is refreshed to the latest upstream patch, with the MANIFEST version, CPE, and SHA-256 integrity hashes updated and the bundle re-verified.
|
|
12
14
|
|
|
13
15
|
- v0.13.16 (2026-05-27) — **`b.mail.agent` docs now describe the facade accurately, and not-yet-wired verbs point to the primitive to use.** b.mail.agent's module documentation claimed it was "the standardization contract for every mail protocol" that JMAP / IMAP / POP3 all route through — but no protocol server actually dispatches through the agent (the framework's own JMAP EmailSubmission handler composes b.mail.send.deliver directly), and the compose / send / reply / forward, sieve.list / sieve.activate, identity / vacation / mdn.* and export / job / import verbs throw mail-agent/not-implemented. The docs are corrected to describe what the agent is: a mailbox-access facade (RBAC + posture + audit + dispatch around a mail store) whose read surface plus the mailbox-mutation and Sieve-upload methods are wired, with the remaining verbs not yet routed through it. Those verbs' error message now names the underlying primitive to compose directly (b.mail.send.deliver, b.mail.sieve, b.mailMdn, …) instead of citing a version tag that had long passed. The public WIRED_AT export (a method→version map that no longer reflected reality) is replaced by COMPOSE_HINT (a method→primitive-to-compose map). No behaviour change: the same methods are wired or throw exactly as before. **Changed:** *`b.mail.agent` documentation corrected; not-implemented errors point to the primitive to compose* — The `@module` / `@card` no longer claim the agent is the universal protocol-dispatch contract — it's documented as a mailbox-access facade with a wired read + mutation + Sieve-upload surface, and the compose/send/identity/vacation/MDN/export verbs documented as not yet routed through it (compose the underlying primitive directly until a protocol server adopts the agent). The `mail-agent/not-implemented` error now names that primitive (e.g. `b.mail.send.deliver`) rather than a passed version tag. **Removed:** *`b.mail.agent.WIRED_AT` export replaced by `COMPOSE_HINT`* — The `WIRED_AT` export mapped each method to a framework version that was supposed to "light it up" — versions that have all shipped without the wiring, so the map was misleading. It is replaced by `COMPOSE_HINT`, mapping each not-yet-wired method to the primitive an operator composes directly. Operators reading `b.mail.agent.WIRED_AT` should read `b.mail.agent.COMPOSE_HINT` instead (pre-1.0: no compatibility shim).
|
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
125
125
|
- Rate-limit
|
|
126
126
|
- Security headers with `Permissions-Policy` defaults denying storage-access / browsing-topics / private-aggregation / controlled-frame
|
|
127
127
|
- CSP nonce
|
|
128
|
-
- Body parser
|
|
128
|
+
- Body parser — JSON / urlencoded / text / multipart; multipart file parts stream to a tmp dir or buffer in memory (`storage: "memory"`) for read-only / serverless filesystems
|
|
129
129
|
- Compression
|
|
130
130
|
- SSE
|
|
131
131
|
- Request log
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
* text: { limit: b.constants.BYTES.mib(1), charset: "utf-8" },
|
|
38
38
|
* raw: { limit: b.constants.BYTES.mib(10), contentTypes: ["application/octet-stream"] },
|
|
39
39
|
* multipart: {
|
|
40
|
-
*
|
|
40
|
+
* storage: "disk", // "disk" → req.files[].path; "memory" → req.files[].buffer (serverless / read-only fs)
|
|
41
|
+
* tmpDir: os.tmpdir(), // disk mode only
|
|
41
42
|
* fileSize: b.constants.BYTES.mib(10),
|
|
42
43
|
* totalSize: b.constants.BYTES.mib(50),
|
|
43
44
|
* fileCount: 20,
|
|
@@ -184,7 +185,8 @@ var DEFAULTS = Object.freeze({
|
|
|
184
185
|
contentTypes: ["application/octet-stream"],
|
|
185
186
|
},
|
|
186
187
|
multipart: {
|
|
187
|
-
|
|
188
|
+
storage: "disk", // "disk" (tmp files) | "memory" (req.files[].buffer)
|
|
189
|
+
tmpDir: null, // resolved per-instance from os.tmpdir() (disk mode only)
|
|
188
190
|
fileSize: C.BYTES.mib(10),
|
|
189
191
|
totalSize: C.BYTES.mib(50),
|
|
190
192
|
fileCount: 20,
|
|
@@ -680,16 +682,23 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
680
682
|
true, HTTP_STATUS.BAD_REQUEST
|
|
681
683
|
);
|
|
682
684
|
}
|
|
685
|
+
// storage: "memory" buffers file parts in RAM (capped by fileSize ×
|
|
686
|
+
// fileCount, the same DoS bound as disk mode) and exposes each file as
|
|
687
|
+
// req.files[].buffer with no filesystem touch — the read-only /
|
|
688
|
+
// serverless path. "disk" (default) streams to tmp files as before.
|
|
689
|
+
var useMemory = opts.storage === "memory";
|
|
683
690
|
// Resolve tmpDir per-request so directory-creation failure surfaces as a
|
|
684
|
-
// structured error rather than a deferred fs throw.
|
|
685
|
-
var tmpDir = opts.tmpDir || nodePath.join(os.tmpdir(), "blamejs-uploads");
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
691
|
+
// structured error rather than a deferred fs throw (disk mode only).
|
|
692
|
+
var tmpDir = useMemory ? null : (opts.tmpDir || nodePath.join(os.tmpdir(), "blamejs-uploads"));
|
|
693
|
+
if (!useMemory) {
|
|
694
|
+
try { atomicFile.ensureDir(tmpDir, 0o700); }
|
|
695
|
+
catch (e) {
|
|
696
|
+
throw new BodyParserError(
|
|
697
|
+
"body-parser/multipart-tmpdir",
|
|
698
|
+
"could not create multipart tmp dir '" + tmpDir + "': " + ((e && e.message) || String(e)),
|
|
699
|
+
true, 500
|
|
700
|
+
);
|
|
701
|
+
}
|
|
693
702
|
}
|
|
694
703
|
|
|
695
704
|
var boundaryBuf = Buffer.from("--" + boundary);
|
|
@@ -721,7 +730,8 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
721
730
|
var currentFd = null;
|
|
722
731
|
var currentSize = 0;
|
|
723
732
|
var currentHash = null;
|
|
724
|
-
var currentBuf = null; //
|
|
733
|
+
var currentBuf = null; // in-memory accumulator (text fields always; file parts when useMemory)
|
|
734
|
+
var currentIsFile = false; // file part (has a filename) vs text field — drives finalize shape
|
|
725
735
|
var currentDiscarded = false; // true when fileFilter rejected the part — body bytes are
|
|
726
736
|
// still consumed (we have to read past them to find the next
|
|
727
737
|
// boundary) but never written to disk.
|
|
@@ -737,6 +747,7 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
737
747
|
currentSize = 0;
|
|
738
748
|
currentHash = null;
|
|
739
749
|
currentBuf = null;
|
|
750
|
+
currentIsFile = false;
|
|
740
751
|
currentDiscarded = false;
|
|
741
752
|
currentEffectiveLimit = 0;
|
|
742
753
|
}
|
|
@@ -765,7 +776,8 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
765
776
|
if (currentFd !== null) { try { nodeFs.closeSync(currentFd); } catch (_e) { /* fd already closed */ } currentFd = null; }
|
|
766
777
|
if (currentTmpPath) { try { nodeFs.unlinkSync(currentTmpPath); } catch (_e) { /* tmp file already removed */ } }
|
|
767
778
|
for (var i = 0; i < files.length; i++) {
|
|
768
|
-
|
|
779
|
+
// Memory-mode files have no path (buffer only) — nothing to unlink.
|
|
780
|
+
if (files[i].path) { try { nodeFs.unlinkSync(files[i].path); } catch (_e) { /* tmp file already removed */ } }
|
|
769
781
|
}
|
|
770
782
|
}
|
|
771
783
|
|
|
@@ -943,17 +955,24 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
943
955
|
}
|
|
944
956
|
}
|
|
945
957
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
958
|
+
currentIsFile = true;
|
|
959
|
+
if (useMemory) {
|
|
960
|
+
// Buffer the file in RAM — no filesystem touch. Bounded by
|
|
961
|
+
// currentEffectiveLimit (per-file) and totalSize (per-request).
|
|
962
|
+
currentBuf = [];
|
|
963
|
+
} else {
|
|
964
|
+
// Generate the tmp path — never derived from the
|
|
965
|
+
// operator-supplied filename.
|
|
966
|
+
var unique = bCrypto.generateToken(C.BYTES.bytes(16));
|
|
967
|
+
currentTmpPath = nodePath.join(tmpDir, "blamejs-up-" + unique);
|
|
968
|
+
try {
|
|
969
|
+
currentFd = nodeFs.openSync(currentTmpPath, "wx", 0o600);
|
|
970
|
+
} catch (e) {
|
|
971
|
+
done(new BodyParserError("body-parser/multipart-tmp-open",
|
|
972
|
+
"could not open multipart tmp file: " + ((e && e.message) || String(e)),
|
|
973
|
+
true, 500));
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
957
976
|
}
|
|
958
977
|
currentHash = nodeCrypto.createHash("sha3-512");
|
|
959
978
|
currentSize = 0;
|
|
@@ -1004,8 +1023,10 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
1004
1023
|
true, 413));
|
|
1005
1024
|
return;
|
|
1006
1025
|
}
|
|
1007
|
-
} else if (
|
|
1008
|
-
// File part — write to disk
|
|
1026
|
+
} else if (currentIsFile) {
|
|
1027
|
+
// File part — write to disk (currentFd) or accumulate in
|
|
1028
|
+
// memory (useMemory). Same per-file + total-request caps
|
|
1029
|
+
// either way, so memory mode adds no new DoS surface.
|
|
1009
1030
|
currentSize += bodyChunk.length;
|
|
1010
1031
|
if (currentSize > currentEffectiveLimit) {
|
|
1011
1032
|
var perFieldFile = (perField && perField[currentField] &&
|
|
@@ -1024,16 +1045,20 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
1024
1045
|
true, 413));
|
|
1025
1046
|
return;
|
|
1026
1047
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1048
|
+
if (currentFd !== null) {
|
|
1049
|
+
try {
|
|
1050
|
+
var written = 0;
|
|
1051
|
+
while (written < bodyChunk.length) {
|
|
1052
|
+
written += nodeFs.writeSync(currentFd, bodyChunk, written, bodyChunk.length - written);
|
|
1053
|
+
}
|
|
1054
|
+
} catch (e) {
|
|
1055
|
+
done(new BodyParserError("body-parser/multipart-tmp-write",
|
|
1056
|
+
"multipart tmp write failed: " + ((e && e.message) || String(e)),
|
|
1057
|
+
true, 500));
|
|
1058
|
+
return;
|
|
1031
1059
|
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
"multipart tmp write failed: " + ((e && e.message) || String(e)),
|
|
1035
|
-
true, 500));
|
|
1036
|
-
return;
|
|
1060
|
+
} else {
|
|
1061
|
+
currentBuf.push(bodyChunk);
|
|
1037
1062
|
}
|
|
1038
1063
|
currentHash.update(bodyChunk);
|
|
1039
1064
|
} else {
|
|
@@ -1067,17 +1092,27 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
1067
1092
|
if (currentDiscarded) {
|
|
1068
1093
|
// fileFilter rejected — already recorded in filesRejected; no
|
|
1069
1094
|
// tmp file was opened, nothing to clean up here.
|
|
1070
|
-
} else if (
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1095
|
+
} else if (currentIsFile) {
|
|
1096
|
+
// Stable shape across both modes: disk gets path (buffer null),
|
|
1097
|
+
// memory gets buffer (path null). Operators branch on whichever
|
|
1098
|
+
// is non-null.
|
|
1099
|
+
var fileEntry = {
|
|
1074
1100
|
field: currentField,
|
|
1075
1101
|
filename: currentFilename,
|
|
1076
1102
|
mimeType: currentMime,
|
|
1077
|
-
path:
|
|
1103
|
+
path: null,
|
|
1104
|
+
buffer: null,
|
|
1078
1105
|
size: currentSize,
|
|
1079
1106
|
hash: currentHash.digest("hex"),
|
|
1080
|
-
}
|
|
1107
|
+
};
|
|
1108
|
+
if (currentFd !== null) {
|
|
1109
|
+
try { nodeFs.closeSync(currentFd); } catch (_e) { /* fd already closed */ }
|
|
1110
|
+
currentFd = null;
|
|
1111
|
+
fileEntry.path = currentTmpPath;
|
|
1112
|
+
} else {
|
|
1113
|
+
fileEntry.buffer = Buffer.concat(currentBuf);
|
|
1114
|
+
}
|
|
1115
|
+
files.push(fileEntry);
|
|
1081
1116
|
} else {
|
|
1082
1117
|
// Field part — flatten + decode UTF-8.
|
|
1083
1118
|
var fbuf = Buffer.concat(currentBuf);
|
|
@@ -1106,6 +1141,7 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
1106
1141
|
currentSize = 0;
|
|
1107
1142
|
currentHash = null;
|
|
1108
1143
|
currentBuf = null;
|
|
1144
|
+
currentIsFile = false;
|
|
1109
1145
|
currentDiscarded = false;
|
|
1110
1146
|
currentEffectiveLimit = 0;
|
|
1111
1147
|
state = MP_AFTER_BD;
|
|
@@ -1159,11 +1195,13 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
1159
1195
|
* sub-parsers ship: JSON (via `safe-json` — POISONED_KEYS stripped,
|
|
1160
1196
|
* depth + size caps), urlencoded, text, raw octet-stream, and
|
|
1161
1197
|
* multipart/form-data. Multipart streams file parts to a tmp dir
|
|
1162
|
-
*
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1198
|
+
* (`storage: "disk"`, default) or buffers them in RAM
|
|
1199
|
+
* (`storage: "memory"` — for read-only / serverless filesystems,
|
|
1200
|
+
* exposing `req.files[].buffer` instead of `.path`), with per-file +
|
|
1201
|
+
* total-request size caps, filename sanitization, SHA3-512 hashing
|
|
1202
|
+
* during streaming, and tmp-file cleanup on response end. Defends
|
|
1203
|
+
* against RFC 9112 §6.1 request smuggling before any body bytes are
|
|
1204
|
+
* read. Each sub-parser can be disabled by passing `false` in its slot.
|
|
1167
1205
|
*
|
|
1168
1206
|
* @opts
|
|
1169
1207
|
* {
|
|
@@ -1172,8 +1210,8 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
1172
1210
|
* text: false | { limit, charset, contentTypes },
|
|
1173
1211
|
* raw: false | { limit, contentTypes },
|
|
1174
1212
|
* multipart: false | {
|
|
1175
|
-
* tmpDir, fileSize, totalSize, fileCount, fieldCount,
|
|
1176
|
-
* mimeAllowlist, fileFilter, fields, audit, contentTypes,
|
|
1213
|
+
* storage, tmpDir, fileSize, totalSize, fileCount, fieldCount,
|
|
1214
|
+
* fieldSize, mimeAllowlist, fileFilter, fields, audit, contentTypes,
|
|
1177
1215
|
* },
|
|
1178
1216
|
* keepRawBody: boolean, // expose req.bodyRaw for webhook signing
|
|
1179
1217
|
* }
|
|
@@ -1202,6 +1240,11 @@ function create(opts) {
|
|
|
1202
1240
|
var textOpts = _resolve("text");
|
|
1203
1241
|
var rawOpts = _resolve("raw");
|
|
1204
1242
|
var multipartOpts = _resolve("multipart");
|
|
1243
|
+
if (multipartOpts && multipartOpts.storage !== "disk" && multipartOpts.storage !== "memory") {
|
|
1244
|
+
throw new TypeError(
|
|
1245
|
+
"middleware.bodyParser: multipart.storage must be \"disk\" or \"memory\" (got " +
|
|
1246
|
+
JSON.stringify(multipartOpts.storage) + ")");
|
|
1247
|
+
}
|
|
1205
1248
|
var keepRawBody = !!opts.keepRawBody;
|
|
1206
1249
|
|
|
1207
1250
|
return async function bodyParser(req, res, next) {
|
|
@@ -1255,7 +1298,10 @@ function create(opts) {
|
|
|
1255
1298
|
if (cleanedUp) return;
|
|
1256
1299
|
cleanedUp = true;
|
|
1257
1300
|
for (var i = 0; i < mpResult.files.length; i++) {
|
|
1258
|
-
|
|
1301
|
+
// Memory-mode files (storage: "memory") have no path — nothing to unlink.
|
|
1302
|
+
if (mpResult.files[i].path) {
|
|
1303
|
+
try { nodeFs.unlinkSync(mpResult.files[i].path); } catch (_e) { /* tmp file already removed */ }
|
|
1304
|
+
}
|
|
1259
1305
|
}
|
|
1260
1306
|
}
|
|
1261
1307
|
res.on("finish", cleanup);
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:85f02e1d-6e57-4585-9067-a882fb442bc0",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-27T19:
|
|
8
|
+
"timestamp": "2026-05-27T19:52:58.359Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.13.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.13.18",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.13.
|
|
25
|
+
"version": "0.13.18",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.13.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.13.18",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.13.
|
|
57
|
+
"ref": "@blamejs/core@0.13.18",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|