@blamejs/blamejs-shop 0.2.31 → 0.3.1
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 +6 -0
- package/lib/admin.js +56 -1
- package/lib/asset-manifest.json +7 -7
- package/lib/security-middleware.js +74 -0
- package/lib/storefront.js +82 -2
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +9 -12
- package/lib/vendor/blamejs/SECURITY.md +1 -0
- package/lib/vendor/blamejs/api-snapshot.json +6 -2
- package/lib/vendor/blamejs/examples/wiki/lib/build-app.js +19 -13
- package/lib/vendor/blamejs/lib/app.js +57 -7
- package/lib/vendor/blamejs/lib/audit.js +1 -0
- package/lib/vendor/blamejs/lib/cert.js +71 -9
- package/lib/vendor/blamejs/lib/middleware/cookies.js +4 -0
- package/lib/vendor/blamejs/lib/middleware/csp-nonce.js +4 -0
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +29 -1
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +3 -0
- package/lib/vendor/blamejs/lib/network-tls.js +81 -5
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.45.json +31 -0
- package/lib/vendor/blamejs/release-notes/v0.13.46.json +35 -0
- package/lib/vendor/blamejs/test/50-integration.js +202 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cert.test.js +44 -0
- package/package.json +1 -1
|
@@ -287,6 +287,200 @@ async function testCreateAppMiddlewareDisableable() {
|
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
// Extract a 64-hex csrf token for `name` from a response's Set-Cookie(s).
|
|
291
|
+
function _csrfCookieFrom(resp, name) {
|
|
292
|
+
var sc = resp.headers["set-cookie"];
|
|
293
|
+
if (!sc) return null;
|
|
294
|
+
var arr = Array.isArray(sc) ? sc : [sc];
|
|
295
|
+
var re = new RegExp("(?:^|;\\s*)" + name + "=([a-f0-9]{64})");
|
|
296
|
+
for (var i = 0; i < arr.length; i++) {
|
|
297
|
+
var m = re.exec(arr[i]);
|
|
298
|
+
if (m) return m[1];
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function testCreateAppSecurityDefaultsWired() {
|
|
304
|
+
// createApp wires cookies + cspNonce + fetchMetadata + bodyParser + csrf
|
|
305
|
+
// ON by default (Core Rule §3). Verify cspNonce patches the CSP, csrf
|
|
306
|
+
// issues a double-submit cookie + enforces it, and the stateless-skip
|
|
307
|
+
// lets token-API / cookieless callers through.
|
|
308
|
+
process.env.BLAMEJS_SKIP_NTP_CHECK = "1";
|
|
309
|
+
process.env.BLAMEJS_AUDIT_SIGNING_MODE = "plaintext";
|
|
310
|
+
b.cluster._resetForTest();
|
|
311
|
+
b.audit._resetForTest();
|
|
312
|
+
b.vault._resetForTest();
|
|
313
|
+
b.db._resetForTest();
|
|
314
|
+
var dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-app-sec-"));
|
|
315
|
+
var app = await b.createApp({
|
|
316
|
+
dataDir: dataDir,
|
|
317
|
+
vault: { mode: "plaintext" },
|
|
318
|
+
db: { atRest: "plain", auditSigning: { mode: "plaintext" } },
|
|
319
|
+
schema: [],
|
|
320
|
+
middleware: { botGuard: false }, // bot-guard refuses non-browser-y test clients
|
|
321
|
+
routes: function (r) {
|
|
322
|
+
r.get("/", function (req, res) { b.render.text(res, "OK"); });
|
|
323
|
+
r.post("/act", function (req, res) { b.render.text(res, "DID"); });
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
var addr = await app.listen({ port: 0, host: "127.0.0.1" });
|
|
327
|
+
try {
|
|
328
|
+
var g = await _appGet(addr.port, "/");
|
|
329
|
+
check("security defaults: cspNonce patched the CSP header",
|
|
330
|
+
typeof g.headers["content-security-policy"] === "string" &&
|
|
331
|
+
g.headers["content-security-policy"].indexOf("nonce-") !== -1);
|
|
332
|
+
var token = _csrfCookieFrom(g, "csrf");
|
|
333
|
+
check("security defaults: csrf double-submit cookie issued",
|
|
334
|
+
typeof token === "string" && token.length === 64);
|
|
335
|
+
|
|
336
|
+
// Cookie-bearing POST without a token → enforced (403).
|
|
337
|
+
var noTok = await b.httpClient.request({
|
|
338
|
+
method: "POST", url: "http://127.0.0.1:" + addr.port + "/act",
|
|
339
|
+
headers: { "Cookie": "csrf=" + token }, body: Buffer.from(""),
|
|
340
|
+
responseMode: "always-resolve",
|
|
341
|
+
allowedProtocols: b.safeUrl.ALLOW_HTTP_ALL, allowInternal: true,
|
|
342
|
+
});
|
|
343
|
+
check("security defaults: cookie'd POST without token → 403", noTok.statusCode === 403);
|
|
344
|
+
|
|
345
|
+
// Cookie + matching header token → validates (200).
|
|
346
|
+
var withTok = await b.httpClient.request({
|
|
347
|
+
method: "POST", url: "http://127.0.0.1:" + addr.port + "/act",
|
|
348
|
+
headers: { "Cookie": "csrf=" + token, "X-CSRF-Token": token }, body: Buffer.from(""),
|
|
349
|
+
responseMode: "always-resolve",
|
|
350
|
+
allowedProtocols: b.safeUrl.ALLOW_HTTP_ALL, allowInternal: true,
|
|
351
|
+
});
|
|
352
|
+
check("security defaults: cookie'd POST with valid token → 200", withTok.statusCode === 200);
|
|
353
|
+
|
|
354
|
+
// Cookieless POST → not CSRF-able → skipStateless skip (200).
|
|
355
|
+
var cookieless = await b.httpClient.request({
|
|
356
|
+
method: "POST", url: "http://127.0.0.1:" + addr.port + "/act",
|
|
357
|
+
body: Buffer.from(""), responseMode: "always-resolve",
|
|
358
|
+
allowedProtocols: b.safeUrl.ALLOW_HTTP_ALL, allowInternal: true,
|
|
359
|
+
});
|
|
360
|
+
check("security defaults: cookieless POST skipped → 200", cookieless.statusCode === 200);
|
|
361
|
+
|
|
362
|
+
// Bearer-authenticated POST (even WITH a cookie) → token-auth, not
|
|
363
|
+
// CSRF-able → skipStateless skip (200).
|
|
364
|
+
var bearer = await b.httpClient.request({
|
|
365
|
+
method: "POST", url: "http://127.0.0.1:" + addr.port + "/act",
|
|
366
|
+
headers: { "Cookie": "csrf=" + token, "Authorization": "Bearer test.token.value" },
|
|
367
|
+
body: Buffer.from(""), responseMode: "always-resolve",
|
|
368
|
+
allowedProtocols: b.safeUrl.ALLOW_HTTP_ALL, allowInternal: true,
|
|
369
|
+
});
|
|
370
|
+
check("security defaults: bearer-auth POST skipped → 200", bearer.statusCode === 200);
|
|
371
|
+
} finally {
|
|
372
|
+
await app.shutdown();
|
|
373
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function testCreateAppCsrfCustomNaming() {
|
|
378
|
+
// The default csrf wiring is configurable — operator cookie/field names
|
|
379
|
+
// flow straight through opts.middleware.csrf, nothing static is baked in.
|
|
380
|
+
process.env.BLAMEJS_SKIP_NTP_CHECK = "1";
|
|
381
|
+
process.env.BLAMEJS_AUDIT_SIGNING_MODE = "plaintext";
|
|
382
|
+
b.cluster._resetForTest();
|
|
383
|
+
b.audit._resetForTest();
|
|
384
|
+
b.vault._resetForTest();
|
|
385
|
+
b.db._resetForTest();
|
|
386
|
+
var dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-app-csrfname-"));
|
|
387
|
+
var app = await b.createApp({
|
|
388
|
+
dataDir: dataDir,
|
|
389
|
+
vault: { mode: "plaintext" },
|
|
390
|
+
db: { atRest: "plain", auditSigning: { mode: "plaintext" } },
|
|
391
|
+
schema: [],
|
|
392
|
+
middleware: { botGuard: false, csrf: { cookie: { name: "app_csrf" }, fieldName: "tok" } },
|
|
393
|
+
routes: function (r) { r.get("/", function (req, res) { b.render.text(res, "OK"); }); },
|
|
394
|
+
});
|
|
395
|
+
var addr = await app.listen({ port: 0, host: "127.0.0.1" });
|
|
396
|
+
try {
|
|
397
|
+
var g = await _appGet(addr.port, "/");
|
|
398
|
+
check("csrf custom naming: operator cookie name issued through the default wiring",
|
|
399
|
+
typeof _csrfCookieFrom(g, "app_csrf") === "string");
|
|
400
|
+
check("csrf custom naming: default 'csrf' cookie name not used",
|
|
401
|
+
_csrfCookieFrom(g, "csrf") === null);
|
|
402
|
+
} finally {
|
|
403
|
+
await app.shutdown();
|
|
404
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function testCreateAppSecurityDisableAudits() {
|
|
409
|
+
// Disabling a security default leaves an audit trace (app.middleware.
|
|
410
|
+
// disabled) and actually drops the middleware.
|
|
411
|
+
process.env.BLAMEJS_SKIP_NTP_CHECK = "1";
|
|
412
|
+
process.env.BLAMEJS_AUDIT_SIGNING_MODE = "plaintext";
|
|
413
|
+
b.cluster._resetForTest();
|
|
414
|
+
b.audit._resetForTest();
|
|
415
|
+
b.vault._resetForTest();
|
|
416
|
+
b.db._resetForTest();
|
|
417
|
+
var dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-app-disable-"));
|
|
418
|
+
var emitted = [];
|
|
419
|
+
var realSafeEmit = b.audit.safeEmit;
|
|
420
|
+
b.audit.safeEmit = function (ev) { emitted.push(ev); return realSafeEmit.call(b.audit, ev); };
|
|
421
|
+
var app;
|
|
422
|
+
try {
|
|
423
|
+
app = await b.createApp({
|
|
424
|
+
dataDir: dataDir,
|
|
425
|
+
vault: { mode: "plaintext" },
|
|
426
|
+
db: { atRest: "plain", auditSigning: { mode: "plaintext" } },
|
|
427
|
+
schema: [],
|
|
428
|
+
middleware: { botGuard: false, csrf: false },
|
|
429
|
+
routes: function (r) { r.get("/", function (req, res) { b.render.text(res, "OK"); }); },
|
|
430
|
+
});
|
|
431
|
+
var disabled = emitted
|
|
432
|
+
.filter(function (e) { return e && e.action === "app.middleware.disabled"; })
|
|
433
|
+
.map(function (e) { return e.metadata && e.metadata.middleware; });
|
|
434
|
+
check("disable audit: csrf disable emits app.middleware.disabled",
|
|
435
|
+
disabled.indexOf("csrf") !== -1);
|
|
436
|
+
check("disable audit: botGuard disable emits app.middleware.disabled",
|
|
437
|
+
disabled.indexOf("botGuard") !== -1);
|
|
438
|
+
var addr = await app.listen({ port: 0, host: "127.0.0.1" });
|
|
439
|
+
var g = await _appGet(addr.port, "/");
|
|
440
|
+
check("disable audit: csrf off → no csrf cookie issued",
|
|
441
|
+
_csrfCookieFrom(g, "csrf") === null);
|
|
442
|
+
} finally {
|
|
443
|
+
b.audit.safeEmit = realSafeEmit;
|
|
444
|
+
if (app) await app.shutdown();
|
|
445
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function testCreateAppCsrfIdempotent() {
|
|
450
|
+
// An operator who ALSO mounts csrf inside opts.routes must not double-
|
|
451
|
+
// apply — the second mount is a no-op (single cookie issued).
|
|
452
|
+
process.env.BLAMEJS_SKIP_NTP_CHECK = "1";
|
|
453
|
+
process.env.BLAMEJS_AUDIT_SIGNING_MODE = "plaintext";
|
|
454
|
+
b.cluster._resetForTest();
|
|
455
|
+
b.audit._resetForTest();
|
|
456
|
+
b.vault._resetForTest();
|
|
457
|
+
b.db._resetForTest();
|
|
458
|
+
var dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-app-idem-"));
|
|
459
|
+
var app = await b.createApp({
|
|
460
|
+
dataDir: dataDir,
|
|
461
|
+
vault: { mode: "plaintext" },
|
|
462
|
+
db: { atRest: "plain", auditSigning: { mode: "plaintext" } },
|
|
463
|
+
schema: [],
|
|
464
|
+
middleware: { botGuard: false },
|
|
465
|
+
routes: function (r) {
|
|
466
|
+
r.use(b.middleware.csrfProtect({ cookie: true })); // redundant — createApp already wired csrf
|
|
467
|
+
r.get("/", function (req, res) { b.render.text(res, "OK"); });
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
var addr = await app.listen({ port: 0, host: "127.0.0.1" });
|
|
471
|
+
try {
|
|
472
|
+
var g = await _appGet(addr.port, "/");
|
|
473
|
+
var sc = g.headers["set-cookie"];
|
|
474
|
+
var arr = Array.isArray(sc) ? sc : (sc ? [sc] : []);
|
|
475
|
+
var csrfCookies = arr.filter(function (c) { return /^csrf=/.test(c); });
|
|
476
|
+
check("csrf idempotent: redundant route-level mount issues a single csrf cookie",
|
|
477
|
+
csrfCookies.length === 1);
|
|
478
|
+
} finally {
|
|
479
|
+
await app.shutdown();
|
|
480
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
290
484
|
async function testCreateAppRoutesCallback() {
|
|
291
485
|
process.env.BLAMEJS_SKIP_NTP_CHECK = "1";
|
|
292
486
|
process.env.BLAMEJS_AUDIT_SIGNING_MODE = "plaintext";
|
|
@@ -462,6 +656,10 @@ async function run() {
|
|
|
462
656
|
await testCreateAppMinimalBoot();
|
|
463
657
|
await testCreateAppDefaultMiddleware();
|
|
464
658
|
await testCreateAppMiddlewareDisableable();
|
|
659
|
+
await testCreateAppSecurityDefaultsWired();
|
|
660
|
+
await testCreateAppCsrfCustomNaming();
|
|
661
|
+
await testCreateAppSecurityDisableAudits();
|
|
662
|
+
await testCreateAppCsrfIdempotent();
|
|
465
663
|
await testCreateAppRoutesCallback();
|
|
466
664
|
await testCreateAppWithJobs();
|
|
467
665
|
await testCreateAppShutdown();
|
|
@@ -480,6 +678,10 @@ module.exports = {
|
|
|
480
678
|
testCreateAppMinimalBoot: testCreateAppMinimalBoot,
|
|
481
679
|
testCreateAppDefaultMiddleware: testCreateAppDefaultMiddleware,
|
|
482
680
|
testCreateAppMiddlewareDisableable: testCreateAppMiddlewareDisableable,
|
|
681
|
+
testCreateAppSecurityDefaultsWired: testCreateAppSecurityDefaultsWired,
|
|
682
|
+
testCreateAppCsrfCustomNaming: testCreateAppCsrfCustomNaming,
|
|
683
|
+
testCreateAppSecurityDisableAudits: testCreateAppSecurityDisableAudits,
|
|
684
|
+
testCreateAppCsrfIdempotent: testCreateAppCsrfIdempotent,
|
|
483
685
|
testCreateAppRoutesCallback: testCreateAppRoutesCallback,
|
|
484
686
|
testCreateAppWithJobs: testCreateAppWithJobs,
|
|
485
687
|
testCreateAppShutdown: testCreateAppShutdown,
|
|
@@ -245,6 +245,49 @@ function testFactoryRefusesBadOpts() {
|
|
|
245
245
|
});
|
|
246
246
|
check("cert name '" + JSON.stringify(badName) + "' refused as path-segment", e && e.code === "cert/bad-cert-name");
|
|
247
247
|
});
|
|
248
|
+
|
|
249
|
+
// ---- compliance posture validation ----
|
|
250
|
+
// opts.compliance names are validated against b.compliance.KNOWN_POSTURES
|
|
251
|
+
// at create() so a typo is caught at boot rather than silently recorded.
|
|
252
|
+
var goodChallenge = { type: "http-01", provision: function () {}, cleanup: function () {} };
|
|
253
|
+
var eBadPosture = threw(function () {
|
|
254
|
+
b.cert.create({
|
|
255
|
+
storage: { type: "sealed-disk", rootDir: _tmpDir(), vault: _ephemeralVault() },
|
|
256
|
+
acme: { directory: "https://example/", accountKey: "auto" },
|
|
257
|
+
certs: [{ name: "m", domains: ["a.com"], challenge: goodChallenge }],
|
|
258
|
+
compliance: ["not-a-real-posture"],
|
|
259
|
+
audit: false,
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
check("unknown compliance posture → cert/unknown-compliance-posture",
|
|
263
|
+
eBadPosture && eBadPosture.code === "cert/unknown-compliance-posture");
|
|
264
|
+
|
|
265
|
+
var eGoodPosture = threw(function () {
|
|
266
|
+
b.cert.create({
|
|
267
|
+
storage: { type: "sealed-disk", rootDir: _tmpDir(), vault: _ephemeralVault() },
|
|
268
|
+
acme: { directory: "https://example/", accountKey: "auto" },
|
|
269
|
+
certs: [{ name: "m", domains: ["a.com"], challenge: goodChallenge }],
|
|
270
|
+
compliance: ["hipaa", "pci-dss"],
|
|
271
|
+
audit: false,
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
check("known compliance postures accepted", !eGoodPosture);
|
|
275
|
+
|
|
276
|
+
// ---- b.network.tls.ocsp.fetch composition surface ----
|
|
277
|
+
check("b.network.tls.ocsp.fetch is a function",
|
|
278
|
+
typeof b.network.tls.ocsp.fetch === "function");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// b.network.tls.ocsp.fetch rejects on missing leafPem/issuerPem rather than
|
|
282
|
+
// issuing an outbound request with undefined inputs.
|
|
283
|
+
async function testOcspFetchRejectsBadInput() {
|
|
284
|
+
var rejected = false;
|
|
285
|
+
try {
|
|
286
|
+
await b.network.tls.ocsp.fetch({});
|
|
287
|
+
} catch (_e) {
|
|
288
|
+
rejected = true;
|
|
289
|
+
}
|
|
290
|
+
check("ocsp.fetch({}) rejects (no leafPem/issuerPem)", rejected);
|
|
248
291
|
}
|
|
249
292
|
|
|
250
293
|
// ---- Sealed-disk storage roundtrip ----
|
|
@@ -699,6 +742,7 @@ async function testCorruptAccountKeyClearError() {
|
|
|
699
742
|
async function run() {
|
|
700
743
|
testSurface();
|
|
701
744
|
testFactoryRefusesBadOpts();
|
|
745
|
+
await testOcspFetchRejectsBadInput();
|
|
702
746
|
await testStorageRoundtrip();
|
|
703
747
|
await testSniCallback();
|
|
704
748
|
await testSniWildcardSingleLabel();
|
package/package.json
CHANGED