@blamejs/blamejs-shop 0.1.34 → 0.1.36
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 +4 -0
- package/README.md +2 -0
- package/lib/asset-manifest.json +7 -3
- package/lib/currency-display.js +201 -4
- package/lib/storefront.js +672 -40
- package/lib/vendor/MANIFEST.json +3 -3
- package/lib/vendor/blamejs/CHANGELOG.md +6 -0
- package/lib/vendor/blamejs/README.md +3 -0
- package/lib/vendor/blamejs/api-snapshot.json +93 -2
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +1 -0
- package/lib/vendor/blamejs/index.js +4 -0
- package/lib/vendor/blamejs/lib/ai-aedt-bias-audit.js +180 -0
- package/lib/vendor/blamejs/lib/crdt.js +453 -0
- package/lib/vendor/blamejs/lib/crypto-xwing.js +213 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.3.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.13.4.json +23 -0
- package/lib/vendor/blamejs/release-notes/v0.13.5.json +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-aedt-bias-audit.test.js +107 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crdt.test.js +158 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-xwing.test.js +120 -0
- package/package.json +2 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.13.3",
|
|
4
|
+
"date": "2026-05-26",
|
|
5
|
+
"headline": "`b.crypto.xwing` — X-Wing hybrid post-quantum KEM",
|
|
6
|
+
"summary": "b.crypto.xwing adds the X-Wing hybrid key-encapsulation mechanism (draft-connolly-cfrg-xwing-kem): it runs ML-KEM-768 and X25519 side by side and binds their shared secrets with SHA3-256, so an encapsulated key stays secure as long as either ML-KEM-768 or X25519 holds. That is the conservative shape for moving off classical ECDH today — a harvest-now-decrypt-later attacker must break the lattice KEM, and a hypothetical ML-KEM break still leaves X25519 standing. keygen() produces a 32-byte decapsulation seed and a 1216-byte public key; encapsulate(publicKey) returns a 1120-byte ciphertext and a 32-byte shared secret; decapsulate(secretKey, ciphertext) recovers it. The X-Wing combiner is frozen, but its specification is still an IETF Internet-Draft, so this primitive is marked experimental and sits beside the existing pre-RFC post-quantum HPKE drafts; it composes the framework's vendored ML-KEM-768 and X25519 with SHA3 and adds no new cryptographic core. The combiner is known-answer-tested byte-for-byte against the draft's definition.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.crypto.xwing` — X-Wing hybrid PQ/T KEM (experimental)",
|
|
13
|
+
"body": "`keygen(seed?)` → `{ publicKey (1216 B), secretKey (32-byte seed) }`; `encapsulate(publicKey, eseed?)` → `{ ciphertext (1120 B), sharedSecret (32 B) }`; `decapsulate(secretKey, ciphertext)` → the 32-byte shared secret. Both `keygen` and `encapsulate` accept an optional seed for deterministic operation. The combiner — `SHA3-256(ssMLKEM ‖ ssX25519 ‖ ctX25519 ‖ pkX25519 ‖ label)` — is exposed as `combiner` for advanced use. Marked `experimental` while draft-connolly-cfrg-xwing-kem remains an Internet-Draft; the algorithm itself is frozen."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.13.4",
|
|
4
|
+
"date": "2026-05-26",
|
|
5
|
+
"headline": "`b.crdt` — conflict-free replicated data types",
|
|
6
|
+
"summary": "b.crdt adds state-based Conflict-free Replicated Data Types: data structures that independent replicas update without coordination and still converge to the same value once they have exchanged state. Each type's merge is a join over a semilattice — commutative, associative, and idempotent — so replicas can merge in any order, any number of times, and agree, which makes these the substrate for active/active cluster state, offline-first clients that reconcile on reconnect, and eventually-consistent counters, sets, and maps. The release ships the full state-based family: grow-only and positive-negative counters (gCounter / pnCounter), grow-only, two-phase, and observed-remove sets (gSet / twoPSet / orSet), a last-write-wins register (lwwRegister), and an observed-remove map (orMap). Every type exposes the same contract — local mutators, merge(other) that returns a converged instance without mutating either operand, value() for the materialized value, and state() / fromState() for a JSON-serializable form to snapshot via b.archive or b.backup or ship to a peer — and carries a replicaId so per-replica contributions stay distinct.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.crdt` — state-based CvRDT counters, sets, register, and map",
|
|
13
|
+
"body": "`b.crdt.gCounter` / `pnCounter` (grow-only and increment/decrement counters), `b.crdt.gSet` / `twoPSet` / `orSet` (grow-only, two-phase, and observed-remove sets — `orSet` supports re-add and resolves a concurrent add-vs-remove as add-wins), `b.crdt.lwwRegister` (last-write-wins with a deterministic replicaId tie-break), and `b.crdt.orMap` (observed-remove keys with last-write-wins values). Each exposes `merge` / `value` / `state` / `fromState` and converges by the CvRDT laws. `orSet` and `orMap` accept `tombstoneRetention` to bound tombstone memory against a remove flood."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"outOfScope": [
|
|
19
|
+
"Operation-based sequence CRDTs (RGA) — a different formal class (CmRDT) requiring a causal-delivery channel; ships when collaborative-text / ordered-list editing is needed.",
|
|
20
|
+
"Delta-state mutators — a bandwidth optimization over full-state merge; ships under large-state replication pressure.",
|
|
21
|
+
"The event-bus replicator for live multi-node auto-sync — the state-based types merge manually standalone; ships when live multi-node synchronization is wired, together with the causal-delivery option and its detector."
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.13.5",
|
|
4
|
+
"date": "2026-05-26",
|
|
5
|
+
"headline": "`b.ai.aedtBiasAudit` — NYC Local Law 144 bias audit",
|
|
6
|
+
"summary": "b.ai.aedtBiasAudit computes the bias-audit figures New York City Local Law 144 requires before an Automated Employment Decision Tool may screen candidates (NYC Admin. Code §20-870 et seq.; DCWP rules 6 RCNY §5-300). Given the per-category counts an independent auditor collected — selected/total for a pass-fail tool, or scored-above-the-overall-median/total for a continuous-score tool — it returns the selection (or scoring) rate, the impact ratio (each group's rate divided by the most-selected group's rate), and an adverse-impact flag (impact ratio below the EEOC four-fifths threshold of 0.8) for every group, across the sex, race/ethnicity, and intersectional dimensions, plus the most-selected group per dimension and an overall flag. Categories under 2% of the audited data are marked excluded per DCWP discretion. It is a pure calculation that produces exactly the figures the annual published summary must contain — the law mandates the calculation, not any particular remediation. The relevant compliance postures (nyc-ll144, and ca-tfaia for California SB 53) were already in the catalog.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.ai.aedtBiasAudit` — Local Law 144 selection/scoring rates and four-fifths impact ratios",
|
|
13
|
+
"body": "`b.ai.aedtBiasAudit({ type, metadata, categories, minCategoryShare? })` where `type` is `\"selection\"` (group entries `{ selected, total }`) or `\"scoring\"` (`{ scoredAboveMedian, total }`). Returns per-group rate, impact ratio, and `adverseImpact` flag across the `sex`, `raceEthnicity`, and `intersectional` dimensions, plus the most-selected group per dimension and an `anyAdverseImpact` summary. Categories below `minCategoryShare` (2% default) are excluded from the impact-ratio basis. Throws `AedtBiasAuditError` on malformed input."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 0 — b.ai.aedtBiasAudit (NYC Local Law 144 AEDT bias audit).
|
|
4
|
+
*
|
|
5
|
+
* Oracle: the EEOC four-fifths rule and the DCWP §5-300 impact-ratio formula.
|
|
6
|
+
* The canonical worked example — a tool passing 60% of men and 42% of women —
|
|
7
|
+
* yields an impact ratio of 42/60 = 0.70 for women, below the 0.8 threshold.
|
|
8
|
+
* Boundary, scoring-rate, intersectional, sub-2% exclusion, and the
|
|
9
|
+
* most-selected basis are each asserted against the formula.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
var b = require("../../index");
|
|
13
|
+
var helpers = require("../helpers");
|
|
14
|
+
var check = helpers.check;
|
|
15
|
+
function code(fn) { try { fn(); return "NO-THROW"; } catch (e) { return e.code; } }
|
|
16
|
+
function row(report, dim, cat) { return report.results[dim].filter(function (r) { return r.category === cat; })[0]; }
|
|
17
|
+
|
|
18
|
+
var MD = { tool: "ResumeRanker v3", auditor: "Acme Audit LLC", auditDate: "2026-05-26" };
|
|
19
|
+
|
|
20
|
+
function testSurface() {
|
|
21
|
+
check("b.ai.aedtBiasAudit is a function", typeof b.ai.aedtBiasAudit === "function");
|
|
22
|
+
check("b.ai.aedtBiasAudit.FOUR_FIFTHS is 0.8", b.ai.aedtBiasAudit.FOUR_FIFTHS === 0.8);
|
|
23
|
+
check("b.ai.aedtBiasAudit.AedtBiasAuditError is a class", typeof b.ai.aedtBiasAudit.AedtBiasAuditError === "function");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function testFourFifthsWorkedExample() {
|
|
27
|
+
var r = b.ai.aedtBiasAudit({
|
|
28
|
+
type: "selection",
|
|
29
|
+
metadata: MD,
|
|
30
|
+
categories: { sex: { Male: { selected: 60, total: 100 }, Female: { selected: 42, total: 100 } } },
|
|
31
|
+
});
|
|
32
|
+
var f = row(r, "sex", "Female"), m = row(r, "sex", "Male");
|
|
33
|
+
check("Female selection rate 0.42", f.rate === 0.42);
|
|
34
|
+
check("Female impact ratio 0.70 (42/60)", Math.abs(f.impactRatio - 0.7) < 1e-9);
|
|
35
|
+
check("Female flagged adverse (< 0.8)", f.adverseImpact === true);
|
|
36
|
+
check("Male is the most-selected (ratio 1)", m.impactRatio === 1 && m.adverseImpact === false);
|
|
37
|
+
check("most-selected sex group is Male", r.summary.mostSelected.sex === "Male");
|
|
38
|
+
check("anyAdverseImpact true", r.summary.anyAdverseImpact === true);
|
|
39
|
+
check("adverseImpactGroups names Female", r.summary.adverseImpactGroups.length === 1 && r.summary.adverseImpactGroups[0].category === "Female");
|
|
40
|
+
check("metadata echoed", r.metadata.tool === MD.tool && r.metadata.auditor === MD.auditor && r.metadata.distributionDate === null);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function testFourFifthsBoundary() {
|
|
44
|
+
// Exactly 0.80 is NOT adverse (strictly-less-than threshold); 0.79 is.
|
|
45
|
+
var at = b.ai.aedtBiasAudit({ type: "selection", metadata: MD, categories: { sex: { X: { selected: 100, total: 100 }, Y: { selected: 80, total: 100 } } } });
|
|
46
|
+
check("impact ratio exactly 0.80 is not adverse", row(at, "sex", "Y").adverseImpact === false);
|
|
47
|
+
var below = b.ai.aedtBiasAudit({ type: "selection", metadata: MD, categories: { sex: { X: { selected: 100, total: 100 }, Y: { selected: 79, total: 100 } } } });
|
|
48
|
+
check("impact ratio 0.79 is adverse", row(below, "sex", "Y").adverseImpact === true);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function testScoringRate() {
|
|
52
|
+
// Scoring tools: rate = scored-above-overall-median / total.
|
|
53
|
+
var r = b.ai.aedtBiasAudit({ type: "scoring", metadata: MD, categories: { sex: { M: { scoredAboveMedian: 50, total: 100 }, F: { scoredAboveMedian: 30, total: 100 } } } });
|
|
54
|
+
check("scoring rate F 0.30", row(r, "sex", "F").rate === 0.3);
|
|
55
|
+
check("scoring impact ratio F 0.60 (30/50)", Math.abs(row(r, "sex", "F").impactRatio - 0.6) < 1e-9);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function testIntersectionalAndRace() {
|
|
59
|
+
var r = b.ai.aedtBiasAudit({
|
|
60
|
+
type: "selection",
|
|
61
|
+
metadata: MD,
|
|
62
|
+
categories: {
|
|
63
|
+
raceEthnicity: { "White": { selected: 80, total: 100 }, "Black or African American": { selected: 48, total: 100 } },
|
|
64
|
+
intersectional: { "Male - White": { selected: 90, total: 100 }, "Female - Black or African American": { selected: 45, total: 100 } },
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
check("race dimension computed", row(r, "raceEthnicity", "Black or African American").impactRatio === 0.6);
|
|
68
|
+
check("intersectional dimension computed", row(r, "intersectional", "Female - Black or African American").impactRatio === 0.5);
|
|
69
|
+
check("intersectional adverse flagged", row(r, "intersectional", "Female - Black or African American").adverseImpact === true);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function testSubTwoPercentExclusion() {
|
|
73
|
+
var r = b.ai.aedtBiasAudit({ type: "selection", metadata: MD, categories: { sex: { Big: { selected: 90, total: 1000 }, Tiny: { selected: 0, total: 10 } } } });
|
|
74
|
+
check("sub-2% category excluded", row(r, "sex", "Tiny").excluded === true);
|
|
75
|
+
check("excluded category has null impact ratio", row(r, "sex", "Tiny").impactRatio === null);
|
|
76
|
+
check("excluded category not flagged adverse", row(r, "sex", "Tiny").adverseImpact === false);
|
|
77
|
+
// A higher minCategoryShare excludes more.
|
|
78
|
+
var r2 = b.ai.aedtBiasAudit({ type: "selection", metadata: MD, minCategoryShare: 0, categories: { sex: { Big: { selected: 90, total: 1000 }, Tiny: { selected: 0, total: 10 } } } });
|
|
79
|
+
check("minCategoryShare 0 includes Tiny", row(r2, "sex", "Tiny").excluded === false);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function testErrors() {
|
|
83
|
+
check("bad type throws", code(function () { b.ai.aedtBiasAudit({ type: "x", metadata: MD, categories: { sex: { M: { selected: 1, total: 1 } } } }); }) === "aedt/bad-type");
|
|
84
|
+
check("missing metadata throws", code(function () { b.ai.aedtBiasAudit({ type: "selection", metadata: {}, categories: { sex: { M: { selected: 1, total: 1 } } } }); }) === "aedt/bad-metadata");
|
|
85
|
+
check("no categories throws", code(function () { b.ai.aedtBiasAudit({ type: "selection", metadata: MD, categories: {} }); }) === "aedt/no-categories");
|
|
86
|
+
check("selected > total throws", code(function () { b.ai.aedtBiasAudit({ type: "selection", metadata: MD, categories: { sex: { M: { selected: 5, total: 2 } } } }); }) === "aedt/bad-count");
|
|
87
|
+
check("negative count throws", code(function () { b.ai.aedtBiasAudit({ type: "selection", metadata: MD, categories: { sex: { M: { selected: -1, total: 2 } } } }); }) === "aedt/bad-count");
|
|
88
|
+
check("bad minCategoryShare throws", code(function () { b.ai.aedtBiasAudit({ type: "selection", metadata: MD, minCategoryShare: 1, categories: { sex: { M: { selected: 1, total: 1 } } } }); }) === "aedt/bad-share");
|
|
89
|
+
// A typoed/unknown option must surface as AedtBiasAuditError (not a generic
|
|
90
|
+
// Error from validateOpts), so e.code / instanceof handling is reliable.
|
|
91
|
+
check("unknown opt throws aedt/bad-opts", code(function () { b.ai.aedtBiasAudit({ type: "selection", metadata: MD, categories: { sex: { M: { selected: 1, total: 1 } } }, minCategoryshare: 0.01 }); }) === "aedt/bad-opts");
|
|
92
|
+
var threw = null;
|
|
93
|
+
try { b.ai.aedtBiasAudit({ type: "selection", metadata: MD, categories: { sex: { M: { selected: 1, total: 1 } } }, bogus: 1 }); } catch (e) { threw = e; }
|
|
94
|
+
check("unknown opt error is an AedtBiasAuditError", threw instanceof b.ai.aedtBiasAudit.AedtBiasAuditError);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function run() {
|
|
98
|
+
testSurface();
|
|
99
|
+
testFourFifthsWorkedExample();
|
|
100
|
+
testFourFifthsBoundary();
|
|
101
|
+
testScoringRate();
|
|
102
|
+
testIntersectionalAndRace();
|
|
103
|
+
testSubTwoPercentExclusion();
|
|
104
|
+
testErrors();
|
|
105
|
+
}
|
|
106
|
+
module.exports = { run: run };
|
|
107
|
+
if (require.main === module) { run().then(function () { console.log("[ai-aedt-bias-audit] OK — " + helpers.getChecks() + " checks passed"); }, function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }); }
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 0 — b.crdt (state-based CvRDTs).
|
|
4
|
+
*
|
|
5
|
+
* Oracle: the CRDT correctness laws themselves. A state-based CvRDT's merge is
|
|
6
|
+
* a join over a semilattice, so for every type merge must be commutative,
|
|
7
|
+
* associative, and idempotent, and two replicas that apply concurrent ops then
|
|
8
|
+
* merge must converge to the same value. Those laws ARE the specification
|
|
9
|
+
* (Shapiro et al.); each is asserted below alongside worked examples.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
var b = require("../../index");
|
|
13
|
+
var helpers = require("../helpers");
|
|
14
|
+
var check = helpers.check;
|
|
15
|
+
var crdt = b.crdt;
|
|
16
|
+
function code(fn) { try { fn(); return "NO-THROW"; } catch (e) { return e.code; } }
|
|
17
|
+
function val(c) { return JSON.stringify(c.value()); }
|
|
18
|
+
|
|
19
|
+
// Assert the three merge laws for a type, given three instances carrying
|
|
20
|
+
// concurrent (disjoint-replica) ops.
|
|
21
|
+
function laws(label, a, c, d) {
|
|
22
|
+
check(label + ": commutative", val(a.merge(c)) === val(c.merge(a)));
|
|
23
|
+
check(label + ": associative", val(a.merge(c).merge(d)) === val(a.merge(c.merge(d))));
|
|
24
|
+
check(label + ": idempotent", val(a.merge(a)) === val(a));
|
|
25
|
+
// Convergence: cross-merge of two replicas lands on one value both ways.
|
|
26
|
+
check(label + ": converges", val(a.merge(c)) === val(c.merge(a)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function testSurface() {
|
|
30
|
+
// Reference each primitive by its full b.crdt.* path (coverage gate).
|
|
31
|
+
check("b.crdt.gCounter is a factory", typeof b.crdt.gCounter === "function" && typeof b.crdt.gCounter.fromState === "function");
|
|
32
|
+
check("b.crdt.pnCounter is a factory", typeof b.crdt.pnCounter === "function" && typeof b.crdt.pnCounter.fromState === "function");
|
|
33
|
+
check("b.crdt.gSet is a factory", typeof b.crdt.gSet === "function" && typeof b.crdt.gSet.fromState === "function");
|
|
34
|
+
check("b.crdt.twoPSet is a factory", typeof b.crdt.twoPSet === "function" && typeof b.crdt.twoPSet.fromState === "function");
|
|
35
|
+
check("b.crdt.orSet is a factory", typeof b.crdt.orSet === "function" && typeof b.crdt.orSet.fromState === "function");
|
|
36
|
+
check("b.crdt.lwwRegister is a factory", typeof b.crdt.lwwRegister === "function" && typeof b.crdt.lwwRegister.fromState === "function");
|
|
37
|
+
check("b.crdt.orMap is a factory", typeof b.crdt.orMap === "function" && typeof b.crdt.orMap.fromState === "function");
|
|
38
|
+
check("b.crdt.CrdtError is a class", typeof b.crdt.CrdtError === "function");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function testGCounter() {
|
|
42
|
+
var a = crdt.gCounter({ replicaId: "a" }).inc(3);
|
|
43
|
+
var c = crdt.gCounter({ replicaId: "c" }).inc(5);
|
|
44
|
+
var d = crdt.gCounter({ replicaId: "d" }).inc(1);
|
|
45
|
+
check("gCounter sum across replicas", a.merge(c).value() === 8);
|
|
46
|
+
laws("gCounter", a, c, d);
|
|
47
|
+
check("gCounter inc default is 1", crdt.gCounter({ replicaId: "a" }).inc().value() === 1);
|
|
48
|
+
check("gCounter rejects negative inc", code(function () { crdt.gCounter().inc(-1); }) === "crdt/bad-value");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function testPNCounter() {
|
|
52
|
+
var a = crdt.pnCounter({ replicaId: "a" }).inc(5).dec(2);
|
|
53
|
+
var c = crdt.pnCounter({ replicaId: "c" }).inc(1);
|
|
54
|
+
var d = crdt.pnCounter({ replicaId: "d" }).dec(3);
|
|
55
|
+
check("pnCounter value", a.value() === 3);
|
|
56
|
+
check("pnCounter merged value", a.merge(c).merge(d).value() === 1);
|
|
57
|
+
laws("pnCounter", a, c, d);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function testGSet() {
|
|
61
|
+
var a = crdt.gSet({ replicaId: "a" }).add("x").add("y");
|
|
62
|
+
var c = crdt.gSet({ replicaId: "c" }).add("z");
|
|
63
|
+
var d = crdt.gSet({ replicaId: "d" }).add("x");
|
|
64
|
+
check("gSet union", val(a.merge(c)) === JSON.stringify(["x", "y", "z"]));
|
|
65
|
+
check("gSet has", a.has("x") && !a.has("q"));
|
|
66
|
+
check("gSet supports structured elements", crdt.gSet().add({ k: 1 }).has({ k: 1 }));
|
|
67
|
+
laws("gSet", a, c, d);
|
|
68
|
+
// value() order must converge for structured elements regardless of merge
|
|
69
|
+
// order (sorted by the encoded key, not the decoded value).
|
|
70
|
+
var s1 = crdt.gSet().add({ id: 2 }).add({ id: 1 });
|
|
71
|
+
var s2 = crdt.gSet().add({ id: 3 });
|
|
72
|
+
check("gSet structured-element value order converges", JSON.stringify(s1.merge(s2).value()) === JSON.stringify(s2.merge(s1).value()));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function testTwoPSet() {
|
|
76
|
+
var s = crdt.twoPSet().add("a").add("b").remove("a");
|
|
77
|
+
check("twoPSet remove", val(s) === JSON.stringify(["b"]));
|
|
78
|
+
check("twoPSet remove-wins (no re-add)", !s.add("a").has("a"));
|
|
79
|
+
var a = crdt.twoPSet({ replicaId: "a" }).add("x").remove("x");
|
|
80
|
+
var c = crdt.twoPSet({ replicaId: "c" }).add("y");
|
|
81
|
+
var d = crdt.twoPSet({ replicaId: "d" }).add("z");
|
|
82
|
+
laws("twoPSet", a, c, d);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function testORSet() {
|
|
86
|
+
// Concurrent re-add survives a remove that did not observe it.
|
|
87
|
+
var a = crdt.orSet({ replicaId: "a" }).add("x").add("y");
|
|
88
|
+
var c = crdt.orSet.fromState(a.state(), { replicaId: "c" });
|
|
89
|
+
a.remove("x");
|
|
90
|
+
c.add("x");
|
|
91
|
+
check("orSet concurrent re-add survives remove", a.merge(c).value().indexOf("x") !== -1);
|
|
92
|
+
// A remove that observed the add wins.
|
|
93
|
+
var s = crdt.orSet().add("z");
|
|
94
|
+
check("orSet observed remove drops element", s.remove("z").value().indexOf("z") === -1);
|
|
95
|
+
laws("orSet", crdt.orSet({ replicaId: "a" }).add("p"), crdt.orSet({ replicaId: "c" }).add("q"), crdt.orSet({ replicaId: "d" }).add("r"));
|
|
96
|
+
// tombstoneRetention cap is accepted and bounds the tombstone set.
|
|
97
|
+
var capped = crdt.orSet({ replicaId: "a", tombstoneRetention: 2 });
|
|
98
|
+
capped.add("1").add("2").add("3").remove("1").remove("2").remove("3");
|
|
99
|
+
check("orSet tombstoneRetention bounds the set", capped.state().tombstones.length <= 2);
|
|
100
|
+
check("orSet rejects bad tombstoneRetention", code(function () { crdt.orSet({ tombstoneRetention: -1 }); }) === "crdt/bad-value");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function testLWWRegister() {
|
|
104
|
+
var a = crdt.lwwRegister({ replicaId: "a" }).set("first", 1);
|
|
105
|
+
var c = crdt.lwwRegister({ replicaId: "c" }).set("second", 2);
|
|
106
|
+
check("lwwRegister higher ts wins", a.merge(c).value() === "second");
|
|
107
|
+
// Tie-break by replicaId (deterministic).
|
|
108
|
+
var r1 = crdt.lwwRegister({ replicaId: "a" }).set("a", 100);
|
|
109
|
+
var r2 = crdt.lwwRegister({ replicaId: "c" }).set("c", 100);
|
|
110
|
+
check("lwwRegister tie-break by replicaId", r1.merge(r2).value() === "c" && r2.merge(r1).value() === "c");
|
|
111
|
+
laws("lwwRegister",
|
|
112
|
+
crdt.lwwRegister({ replicaId: "a" }).set("x", 5),
|
|
113
|
+
crdt.lwwRegister({ replicaId: "c" }).set("y", 9),
|
|
114
|
+
crdt.lwwRegister({ replicaId: "d" }).set("z", 3));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function testORMap() {
|
|
118
|
+
var a = crdt.orMap({ replicaId: "a" }).set("k1", "v1", 10);
|
|
119
|
+
var c = crdt.orMap({ replicaId: "c" }).set("k1", "v2", 20).set("k2", "x", 5);
|
|
120
|
+
var merged = a.merge(c);
|
|
121
|
+
check("orMap concurrent key write resolves LWW", merged.get("k1") === "v2");
|
|
122
|
+
check("orMap disjoint keys both present", merged.has("k2") && val(merged) === JSON.stringify({ k1: "v2", k2: "x" }));
|
|
123
|
+
check("orMap commutative", val(a.merge(c)) === val(c.merge(a)));
|
|
124
|
+
// Remove a key, then converge.
|
|
125
|
+
var p = crdt.orMap.fromState(merged.state(), { replicaId: "a" }).remove("k2");
|
|
126
|
+
check("orMap remove converges", val(p.merge(merged)) === JSON.stringify({ k1: "v2" }));
|
|
127
|
+
check("orMap rejects non-string key", code(function () { crdt.orMap().set(5, "v"); }) === "crdt/bad-key");
|
|
128
|
+
// Removing a key clears its register, so a re-add starts clean — a later set
|
|
129
|
+
// with a lower timestamp than the pre-remove value still wins on this replica.
|
|
130
|
+
var rr = crdt.orMap({ replicaId: "a" }).set("k", "old", 100);
|
|
131
|
+
rr.remove("k");
|
|
132
|
+
rr.set("k", "new", 50); // lower ts than the removed "old"@100
|
|
133
|
+
check("orMap remove clears register so re-add wins", rr.get("k") === "new");
|
|
134
|
+
check("orMap re-add appears in value", JSON.stringify(rr.value()) === JSON.stringify({ k: "new" }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function testStateRoundTrip() {
|
|
138
|
+
["gCounter", "pnCounter", "gSet", "twoPSet", "orSet", "lwwRegister", "orMap"].forEach(function (t) {
|
|
139
|
+
var inst = crdt[t]({ replicaId: "a" });
|
|
140
|
+
var s = inst.state();
|
|
141
|
+
check(t + ": fromState round-trips", JSON.stringify(crdt[t].fromState(s, { replicaId: "a" }).state()) === JSON.stringify(s));
|
|
142
|
+
check(t + ": fromState rejects a mismatched type", code(function () { crdt[t].fromState({ type: "WRONG" }); }) === "crdt/type-mismatch");
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function run() {
|
|
147
|
+
testSurface();
|
|
148
|
+
testGCounter();
|
|
149
|
+
testPNCounter();
|
|
150
|
+
testGSet();
|
|
151
|
+
testTwoPSet();
|
|
152
|
+
testORSet();
|
|
153
|
+
testLWWRegister();
|
|
154
|
+
testORMap();
|
|
155
|
+
testStateRoundTrip();
|
|
156
|
+
}
|
|
157
|
+
module.exports = { run: run };
|
|
158
|
+
if (require.main === module) { run().then(function () { console.log("[crdt] OK — " + helpers.getChecks() + " checks passed"); }, function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }); }
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 0 — b.crypto.xwing (X-Wing hybrid PQ/T KEM, draft-connolly-cfrg-xwing-kem).
|
|
4
|
+
*
|
|
5
|
+
* Oracle. The X-Wing-specific contribution is the combiner — SHA3-256 over a
|
|
6
|
+
* fixed concatenation plus a fixed label — so that is known-answer-tested
|
|
7
|
+
* byte-for-byte against an independent SHA3-256. The ML-KEM-768 and X25519
|
|
8
|
+
* halves are pre-validated vendored primitives; the composition is checked by
|
|
9
|
+
* full encaps → decaps agreement, by deterministic reproducibility from fixed
|
|
10
|
+
* seeds (regression-anchored to pinned digests), and by implicit-rejection
|
|
11
|
+
* behaviour on a tampered ciphertext.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
var nodeCrypto = require("crypto");
|
|
15
|
+
var b = require("../../index");
|
|
16
|
+
var helpers = require("../helpers");
|
|
17
|
+
var check = helpers.check;
|
|
18
|
+
function code(fn) { try { fn(); return "NO-THROW"; } catch (e) { return e.code; } }
|
|
19
|
+
|
|
20
|
+
var x = b.crypto.xwing;
|
|
21
|
+
|
|
22
|
+
function testSurface() {
|
|
23
|
+
check("xwing.keygen is fn", typeof x.keygen === "function");
|
|
24
|
+
check("xwing.encapsulate is fn", typeof x.encapsulate === "function");
|
|
25
|
+
check("xwing.decapsulate is fn", typeof x.decapsulate === "function");
|
|
26
|
+
check("xwing.combiner is fn", typeof x.combiner === "function");
|
|
27
|
+
check("xwing.NAME is X-Wing", x.NAME === "X-Wing");
|
|
28
|
+
check("xwing.XWingError is class", typeof x.XWingError === "function");
|
|
29
|
+
check("SIZES pk 1216", x.SIZES.publicKey === 1216);
|
|
30
|
+
check("SIZES ct 1120", x.SIZES.ciphertext === 1120);
|
|
31
|
+
check("SIZES sk 32", x.SIZES.secretKey === 32);
|
|
32
|
+
check("SIZES ss 32", x.SIZES.sharedSecret === 32);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function testCombinerKAT() {
|
|
36
|
+
// Byte-exact against the draft: SHA3-256(ssM ‖ ssX ‖ ctX ‖ pkX ‖ label),
|
|
37
|
+
// label = the six bytes 5c2e2f2f5e5c ("\./" "/^\").
|
|
38
|
+
var ssM = Buffer.alloc(32, 0x11), ssX = Buffer.alloc(32, 0x22),
|
|
39
|
+
ctX = Buffer.alloc(32, 0x33), pkX = Buffer.alloc(32, 0x44);
|
|
40
|
+
var ref = nodeCrypto.createHash("sha3-256")
|
|
41
|
+
.update(Buffer.concat([ssM, ssX, ctX, pkX, Buffer.from("5c2e2f2f5e5c", "hex")])).digest();
|
|
42
|
+
check("combiner matches independent SHA3-256 + label byte-for-byte", x.combiner(ssM, ssX, ctX, pkX).equals(ref));
|
|
43
|
+
// Order matters: swapping two inputs changes the output.
|
|
44
|
+
check("combiner is order-sensitive", !x.combiner(ssX, ssM, ctX, pkX).equals(ref));
|
|
45
|
+
check("combiner rejects a short input", code(function () { x.combiner(Buffer.alloc(31), ssX, ctX, pkX); }) === "xwing/bad-input");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function testSizes() {
|
|
49
|
+
var kp = x.keygen();
|
|
50
|
+
check("public key is 1216 bytes", kp.publicKey.length === 1216);
|
|
51
|
+
check("secret key (seed) is 32 bytes", kp.secretKey.length === 32);
|
|
52
|
+
var enc = x.encapsulate(kp.publicKey);
|
|
53
|
+
check("ciphertext is 1120 bytes", enc.ciphertext.length === 1120);
|
|
54
|
+
check("shared secret is 32 bytes", enc.sharedSecret.length === 32);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function testRoundTrip() {
|
|
58
|
+
for (var i = 0; i < 5; i++) {
|
|
59
|
+
var kp = x.keygen();
|
|
60
|
+
var enc = x.encapsulate(kp.publicKey);
|
|
61
|
+
var ss = x.decapsulate(kp.secretKey, enc.ciphertext);
|
|
62
|
+
check("round-trip " + i + ": decaps recovers the encaps shared secret", ss.equals(enc.sharedSecret));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function testDeterminism() {
|
|
67
|
+
var seed = Buffer.alloc(32, 1);
|
|
68
|
+
check("keygen(seed) is deterministic", x.keygen(seed).publicKey.equals(x.keygen(seed).publicKey));
|
|
69
|
+
var pk = x.keygen(seed).publicKey;
|
|
70
|
+
// Distinct eseed halves so the value depends on the draft's split order
|
|
71
|
+
// (eseed[0:32] = ML-KEM coins, eseed[32:64] = X25519 ephemeral).
|
|
72
|
+
var eseed = Buffer.concat([Buffer.alloc(32, 2), Buffer.alloc(32, 3)]);
|
|
73
|
+
var a = x.encapsulate(pk, eseed), c2 = x.encapsulate(pk, eseed);
|
|
74
|
+
check("encapsulate(pk, eseed) is deterministic", a.ciphertext.equals(c2.ciphertext) && a.sharedSecret.equals(c2.sharedSecret));
|
|
75
|
+
check("deterministic encaps round-trips", x.decapsulate(seed, a.ciphertext).equals(a.sharedSecret));
|
|
76
|
+
|
|
77
|
+
// Regression anchors: pinned digests of the full deterministic flow. A change
|
|
78
|
+
// to the combiner, the seed expansion, the eseed split order, or the wire
|
|
79
|
+
// framing breaks these.
|
|
80
|
+
check("keygen(0x01) public key digest is stable",
|
|
81
|
+
nodeCrypto.createHash("sha3-256").update(pk).digest("hex") === "60068c4c0bfc7421bb1cb4a4202bf0ef75ee27e61bf2f6b08780869485cc736a");
|
|
82
|
+
check("deterministic ciphertext digest is stable",
|
|
83
|
+
nodeCrypto.createHash("sha3-256").update(a.ciphertext).digest("hex") === "1422e82c4307fcb8117b28ceaf686241cbe48d1a5546e05cd1aa7401d5fc2624");
|
|
84
|
+
check("deterministic shared secret is stable",
|
|
85
|
+
a.sharedSecret.toString("hex") === "356f5611bb146e8baf7fa61410552a9c724a170b8a0d4e742fac19161d8fdf4a");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function testImplicitRejection() {
|
|
89
|
+
var kp = x.keygen();
|
|
90
|
+
var enc = x.encapsulate(kp.publicKey);
|
|
91
|
+
// Wrong key: ML-KEM implicit rejection yields a different 32-byte secret, no throw.
|
|
92
|
+
var wrong = x.decapsulate(x.keygen().secretKey, enc.ciphertext);
|
|
93
|
+
check("wrong key yields a 32-byte secret (no throw)", Buffer.isBuffer(wrong) && wrong.length === 32);
|
|
94
|
+
check("wrong key yields a different secret", !wrong.equals(enc.sharedSecret));
|
|
95
|
+
// Tampered ciphertext: still no throw, different secret.
|
|
96
|
+
var bad = Buffer.from(enc.ciphertext); bad[0] ^= 0xff; bad[bad.length - 1] ^= 0xff;
|
|
97
|
+
var ssBad = x.decapsulate(kp.secretKey, bad);
|
|
98
|
+
check("tampered ciphertext yields a different secret without throwing", !ssBad.equals(enc.sharedSecret));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function testErrors() {
|
|
102
|
+
var kp = x.keygen();
|
|
103
|
+
check("keygen rejects a short seed", code(function () { x.keygen(Buffer.alloc(16)); }) === "xwing/bad-seed");
|
|
104
|
+
check("encapsulate rejects a short pubkey", code(function () { x.encapsulate(Buffer.alloc(100)); }) === "xwing/bad-public-key");
|
|
105
|
+
check("encapsulate rejects a short eseed", code(function () { x.encapsulate(kp.publicKey, Buffer.alloc(32)); }) === "xwing/bad-eseed");
|
|
106
|
+
check("decapsulate rejects a short seed", code(function () { x.decapsulate(Buffer.alloc(16), Buffer.alloc(1120)); }) === "xwing/bad-seed");
|
|
107
|
+
check("decapsulate rejects a short ct", code(function () { x.decapsulate(kp.secretKey, Buffer.alloc(100)); }) === "xwing/bad-ciphertext");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function run() {
|
|
111
|
+
testSurface();
|
|
112
|
+
testCombinerKAT();
|
|
113
|
+
testSizes();
|
|
114
|
+
testRoundTrip();
|
|
115
|
+
testDeterminism();
|
|
116
|
+
testImplicitRejection();
|
|
117
|
+
testErrors();
|
|
118
|
+
}
|
|
119
|
+
module.exports = { run: run };
|
|
120
|
+
if (require.main === module) { run().then(function () { console.log("[crypto-xwing] OK — " + helpers.getChecks() + " checks passed"); }, function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/blamejs-shop",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
4
4
|
"description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"test": "node test/smoke.js",
|
|
9
9
|
"vendor": "bash scripts/vendor-update.sh",
|
|
10
10
|
"sync-assets": "node scripts/sync-r2-assets.js",
|
|
11
|
-
"
|
|
11
|
+
"release": "node scripts/release.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=24.14.1"
|