@qbix/q 1.0.5
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/LICENSE +21 -0
- package/README.md +678 -0
- package/dist/Metrics.js +2873 -0
- package/dist/Metrics.min.js +95 -0
- package/dist/Q.js +15677 -0
- package/dist/Q.min.js +385 -0
- package/dist/Q.minimal.js +11594 -0
- package/dist/Q.minimal.min.js +286 -0
- package/dist/handlebars-v4.0.10.min.js +29 -0
- package/dist/handlebars.minimal.min.js +1 -0
- package/dist/img/hints/rotate-left.gif +0 -0
- package/dist/img/hints/swipe-down.gif +0 -0
- package/dist/img/hints/swipe-up.gif +0 -0
- package/dist/img/hints/tap.gif +0 -0
- package/dist/img/throbbers/loading.gif +0 -0
- package/dist/jquery.minimal.min.js +18 -0
- package/dist/methods/Q/Audio/load.js +29 -0
- package/dist/methods/Q/Audio/loadVoices.js +36 -0
- package/dist/methods/Q/Audio/play.js +47 -0
- package/dist/methods/Q/Audio/speak.js +149 -0
- package/dist/methods/Q/Crypto/delegate.js +186 -0
- package/dist/methods/Q/Crypto/internalKeypair.js +170 -0
- package/dist/methods/Q/Crypto/sign.js +200 -0
- package/dist/methods/Q/Crypto/verify.js +212 -0
- package/dist/methods/Q/Crypto/verifyDelegated.js +214 -0
- package/dist/methods/Q/Data/Bloom/_internal.js +163 -0
- package/dist/methods/Q/Data/Bloom/create.js +23 -0
- package/dist/methods/Q/Data/Bloom/fromBase64.js +21 -0
- package/dist/methods/Q/Data/Bloom/fromBytes.js +15 -0
- package/dist/methods/Q/Data/Bloom/fromElements.js +33 -0
- package/dist/methods/Q/Data/Merkle/_internal.js +68 -0
- package/dist/methods/Q/Data/Merkle/build.js +29 -0
- package/dist/methods/Q/Data/Merkle/proof.js +50 -0
- package/dist/methods/Q/Data/Merkle/verify.js +32 -0
- package/dist/methods/Q/Data/Prolly/_internal.js +190 -0
- package/dist/methods/Q/Data/Prolly/build.js +28 -0
- package/dist/methods/Q/Data/Prolly/delete.js +28 -0
- package/dist/methods/Q/Data/Prolly/diff.js +70 -0
- package/dist/methods/Q/Data/Prolly/get.js +38 -0
- package/dist/methods/Q/Data/Prolly/set.js +32 -0
- package/dist/methods/Q/Data/compress.js +45 -0
- package/dist/methods/Q/Data/decompress.js +35 -0
- package/dist/methods/Q/Data/decrypt.js +55 -0
- package/dist/methods/Q/Data/derive.js +76 -0
- package/dist/methods/Q/Data/digest.js +29 -0
- package/dist/methods/Q/Data/encrypt.js +61 -0
- package/dist/methods/Q/Data/generateKey.js +40 -0
- package/dist/methods/Q/Data/hkdf.js +44 -0
- package/dist/methods/Q/Data/importKey.js +34 -0
- package/dist/methods/Q/Data/sign.js +42 -0
- package/dist/methods/Q/Data/verify.js +45 -0
- package/dist/methods/Q/Onboarding/handle.js +50 -0
- package/dist/methods/Q/Onboarding/start.js +165 -0
- package/dist/methods/Q/Onboarding/stop.js +21 -0
- package/dist/methods/Q/Sandbox/run.js +392 -0
- package/dist/methods/Q/Tool/define/component.js +218 -0
- package/dist/methods/Q/globalMemoryWalk.js +82 -0
- package/dist/methods/Q/leaves.js +34 -0
- package/dist/methods/Q/registerWebComponent.js +0 -0
- package/dist/methods/Q/sanitize.js +142 -0
- package/dist/test.html +6 -0
- package/dist/tools/Q/lazyload.js +433 -0
- package/package.json +26 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Q.exports(function (Q) {
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verify typed signatures (Q-native or EIP-712).
|
|
5
|
+
*
|
|
6
|
+
* Always returns a boolean.
|
|
7
|
+
*
|
|
8
|
+
* Optional recovery:
|
|
9
|
+
* - If options.recovered is an object, recovered signer info
|
|
10
|
+
* will be written into it (when supported by the format).
|
|
11
|
+
*
|
|
12
|
+
* @static
|
|
13
|
+
* @method verify
|
|
14
|
+
* @param {Object} options
|
|
15
|
+
* @param {String} [options.format='ES256']
|
|
16
|
+
* @param {Object} options.domain
|
|
17
|
+
* @param {Object} options.types
|
|
18
|
+
* @param {Object} options.message
|
|
19
|
+
* @param {String|Uint8Array} options.signature
|
|
20
|
+
* @param {String} [options.address] Expected signer address (EIP712)
|
|
21
|
+
* @param {Uint8Array} [options.publicKey] Expected signer public key (ES256)
|
|
22
|
+
* @param {Object} [options.recovered] If provided, signer info is written here
|
|
23
|
+
* @return {Q.Promise} resolves to true or false
|
|
24
|
+
*/
|
|
25
|
+
return function Q_Crypto_verify(options) {
|
|
26
|
+
|
|
27
|
+
return new Q.Promise(async function (resolve) {
|
|
28
|
+
|
|
29
|
+
if (!options) {
|
|
30
|
+
throw new Error("options required");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const format = options.format || "ES256";
|
|
34
|
+
|
|
35
|
+
/* =================================================
|
|
36
|
+
* Ethereum / EIP-712
|
|
37
|
+
* ================================================= */
|
|
38
|
+
if (format === "EIP712") {
|
|
39
|
+
|
|
40
|
+
if (typeof options.primaryType !== "string") {
|
|
41
|
+
throw new Error("primaryType required for eip712 verification");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let recoveredAddress;
|
|
45
|
+
|
|
46
|
+
// Prefer ethers if available (ecosystem parity)
|
|
47
|
+
if (
|
|
48
|
+
globalThis.ethers &&
|
|
49
|
+
typeof globalThis.ethers.verifyTypedData === "function"
|
|
50
|
+
) {
|
|
51
|
+
try {
|
|
52
|
+
recoveredAddress = globalThis.ethers.verifyTypedData(
|
|
53
|
+
options.domain || {},
|
|
54
|
+
options.types,
|
|
55
|
+
options.message,
|
|
56
|
+
options.signature
|
|
57
|
+
);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
resolve(false);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
} else {
|
|
64
|
+
|
|
65
|
+
// Spec-correct fallback
|
|
66
|
+
try {
|
|
67
|
+
const [{ hashTypedData }, { keccak_256 }] = await Promise.all([
|
|
68
|
+
import(Q.url("{{Q}}/src/js/crypto/eip712.js")),
|
|
69
|
+
import(Q.url("{{Q}}/src/js/crypto/sha3.js"))
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const digest = hashTypedData(
|
|
73
|
+
options.domain || {},
|
|
74
|
+
options.primaryType,
|
|
75
|
+
options.message,
|
|
76
|
+
options.types
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const secp = await import(
|
|
80
|
+
Q.url("{{Q}}/src/js/crypto/secp256k1.js")
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
let sigBytes = options.signature;
|
|
84
|
+
|
|
85
|
+
if (typeof sigBytes === "string") {
|
|
86
|
+
sigBytes = Q.Data.fromHex(sigBytes);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!(sigBytes instanceof Uint8Array)) {
|
|
90
|
+
resolve(false);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Accept 65-byte RSV → drop v
|
|
95
|
+
if (sigBytes.length === 65) {
|
|
96
|
+
sigBytes = sigBytes.slice(0, 64);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (sigBytes.length !== 64) {
|
|
100
|
+
resolve(false);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const sig = secp.secp256k1.Signature.fromCompact(sigBytes);
|
|
105
|
+
|
|
106
|
+
// Enforce low-s (EIP-2)
|
|
107
|
+
if (sig.s > secp.secp256k1.CURVE.n / 2n) {
|
|
108
|
+
resolve(false);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const pub = sig
|
|
113
|
+
.recoverPublicKey(digest)
|
|
114
|
+
.toRawBytes(false); // uncompressed
|
|
115
|
+
|
|
116
|
+
if (!secp.secp256k1.verify(sig, digest, pub)) {
|
|
117
|
+
resolve(false);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Address = last 20 bytes of keccak(pub[1:])
|
|
122
|
+
const addrBytes = keccak_256(pub.slice(1)).slice(-20);
|
|
123
|
+
recoveredAddress =
|
|
124
|
+
"0x" +
|
|
125
|
+
[...addrBytes].map(b =>
|
|
126
|
+
b.toString(16).padStart(2, "0")
|
|
127
|
+
).join("");
|
|
128
|
+
|
|
129
|
+
} catch (e) {
|
|
130
|
+
resolve(false);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (options.recovered && typeof options.recovered === "object") {
|
|
136
|
+
options.recovered.address = recoveredAddress;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (options.address) {
|
|
140
|
+
resolve(
|
|
141
|
+
recoveredAddress.toLowerCase() ===
|
|
142
|
+
options.address.toLowerCase()
|
|
143
|
+
);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
resolve(true);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* =================================================
|
|
152
|
+
* es256 (P-256 + SHA-256)
|
|
153
|
+
* =================================================
|
|
154
|
+
*
|
|
155
|
+
* es256 signatures are DER-encoded ECDSA (WebCrypto compatible)
|
|
156
|
+
*/
|
|
157
|
+
if (format === "ES256") {
|
|
158
|
+
|
|
159
|
+
if (!(options.publicKey instanceof Uint8Array)) {
|
|
160
|
+
throw new Error("ES256 verify requires publicKey (Uint8Array)");
|
|
161
|
+
}
|
|
162
|
+
if (!(options.signature instanceof Uint8Array)) {
|
|
163
|
+
throw new Error("ES256 verify requires signature (Uint8Array)");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const payload = {
|
|
167
|
+
domain: options.domain || {},
|
|
168
|
+
primaryType: options.primaryType,
|
|
169
|
+
types: options.types,
|
|
170
|
+
message: options.message
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Deep canonicalization (must match sign.js)
|
|
174
|
+
const canonical = Q.serialize(payload);
|
|
175
|
+
const data = new TextEncoder().encode(canonical);
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const digestBytes = await Q.Data.digest("SHA-256", data);
|
|
179
|
+
|
|
180
|
+
const key = await crypto.subtle.importKey(
|
|
181
|
+
"raw",
|
|
182
|
+
options.publicKey,
|
|
183
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
184
|
+
false,
|
|
185
|
+
["verify"]
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const ok = await crypto.subtle.verify(
|
|
190
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
191
|
+
key,
|
|
192
|
+
options.signature, // DER-encoded ECDSA
|
|
193
|
+
digestBytes
|
|
194
|
+
);
|
|
195
|
+
resolve(ok);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
resolve(false);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return;
|
|
201
|
+
|
|
202
|
+
} catch (e) {
|
|
203
|
+
resolve(false);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
throw new Error("Unknown signature format: " + format);
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
});
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Q.exports(function (Q) {
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verify a single delegation step.
|
|
5
|
+
*
|
|
6
|
+
* Verifies exactly ONE level of delegation. Intended to be called
|
|
7
|
+
* repeatedly to validate a delegation chain.
|
|
8
|
+
*
|
|
9
|
+
* Guarantees:
|
|
10
|
+
* - The derived secret matches `statement.secretHash`
|
|
11
|
+
* - The delegation statement was correctly signed
|
|
12
|
+
* - The signer matches the parent identity declared in the statement
|
|
13
|
+
*
|
|
14
|
+
* Does NOT verify:
|
|
15
|
+
* - Parent legitimacy beyond this step
|
|
16
|
+
* - Expiration or revocation policy
|
|
17
|
+
*
|
|
18
|
+
* Secret hash algorithm matches the signing format:
|
|
19
|
+
* - EIP712 → keccak256(derivedSecret)
|
|
20
|
+
* - ES256 → SHA-256(derivedSecret)
|
|
21
|
+
* This must match delegate.js exactly.
|
|
22
|
+
*
|
|
23
|
+
* @static
|
|
24
|
+
* @method verifyDelegated
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} options
|
|
27
|
+
* @param {String} [options.format="ES256"] "ES256" or "EIP712".
|
|
28
|
+
* @param {Object} options.statement Delegation statement.
|
|
29
|
+
* @param {String|Uint8Array} options.signature Signature bytes.
|
|
30
|
+
* @param {Uint8Array} options.derivedSecret Derived child secret.
|
|
31
|
+
* @param {Uint8Array} [options.parentPublicKey] Expected parent key (ES256).
|
|
32
|
+
* @param {Object} [options.domain] EIP-712 domain (EIP712 only).
|
|
33
|
+
* @param {Object} [options.recovered] Optional recovered signer output.
|
|
34
|
+
*
|
|
35
|
+
* @return {Q.Promise<Boolean>}
|
|
36
|
+
*/
|
|
37
|
+
return function Q_Crypto_verifyDelegated(options) {
|
|
38
|
+
|
|
39
|
+
return new Q.Promise(function (resolve, reject) {
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
|
|
43
|
+
if (!options) {
|
|
44
|
+
throw new Error("options required");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const format = options.format || "ES256";
|
|
48
|
+
const statement = options.statement;
|
|
49
|
+
const derivedSecret = options.derivedSecret;
|
|
50
|
+
|
|
51
|
+
// -------------------------------------------------
|
|
52
|
+
// Validate inputs
|
|
53
|
+
// -------------------------------------------------
|
|
54
|
+
if (!statement || typeof statement !== "object") {
|
|
55
|
+
throw new Error("statement required");
|
|
56
|
+
}
|
|
57
|
+
if (!(derivedSecret instanceof Uint8Array)) {
|
|
58
|
+
throw new Error("derivedSecret must be Uint8Array");
|
|
59
|
+
}
|
|
60
|
+
if (typeof statement.parent !== "string") {
|
|
61
|
+
throw new Error("statement.parent required");
|
|
62
|
+
}
|
|
63
|
+
if (typeof statement.label !== "string") {
|
|
64
|
+
throw new Error("statement.label required");
|
|
65
|
+
}
|
|
66
|
+
if (typeof statement.issuedTime !== "number") {
|
|
67
|
+
throw new Error("statement.issuedTime required");
|
|
68
|
+
}
|
|
69
|
+
if (typeof statement.secretHash !== "string") {
|
|
70
|
+
throw new Error("statement.secretHash required");
|
|
71
|
+
}
|
|
72
|
+
if ("context" in statement && typeof statement.context !== "string") {
|
|
73
|
+
throw new Error("statement.context must be string when present");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const context = statement.context || "";
|
|
77
|
+
|
|
78
|
+
// -------------------------------------------------
|
|
79
|
+
// Verify secret binding
|
|
80
|
+
// Algorithm matches delegate.js: keccak256 for EIP712, SHA-256 for ES256
|
|
81
|
+
// -------------------------------------------------
|
|
82
|
+
const secretHashPromise = format === "EIP712"
|
|
83
|
+
? import(Q.url("{{Q}}/src/js/crypto/sha3.js"))
|
|
84
|
+
.then(function ({ keccak_256 }) {
|
|
85
|
+
return Q.Data.toHex(keccak_256(derivedSecret));
|
|
86
|
+
})
|
|
87
|
+
: Q.Data.digest("SHA-256", derivedSecret)
|
|
88
|
+
.then(function (h) {
|
|
89
|
+
return Q.Data.toHex(h);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
secretHashPromise.then(function (actualSecretHashHex) {
|
|
93
|
+
|
|
94
|
+
if (actualSecretHashHex !== statement.secretHash) {
|
|
95
|
+
resolve(false);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// -------------------------------------------------
|
|
100
|
+
// Schema — must match delegate.js types exactly
|
|
101
|
+
// -------------------------------------------------
|
|
102
|
+
const types = {
|
|
103
|
+
EIP712Domain: [
|
|
104
|
+
{ name: "name", type: "string" },
|
|
105
|
+
{ name: "version", type: "string" },
|
|
106
|
+
{ name: "salt", type: "bytes32" }
|
|
107
|
+
],
|
|
108
|
+
Delegation: [
|
|
109
|
+
{
|
|
110
|
+
name: "parent",
|
|
111
|
+
type: format === "EIP712" ? "address" : "bytes32"
|
|
112
|
+
},
|
|
113
|
+
{ name: "label", type: "string" },
|
|
114
|
+
{ name: "issuedTime", type: "uint64" },
|
|
115
|
+
{ name: "context", type: "string" },
|
|
116
|
+
{ name: "secretHash", type: "bytes32" }
|
|
117
|
+
]
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// -------------------------------------------------
|
|
121
|
+
// Normalize signature
|
|
122
|
+
// -------------------------------------------------
|
|
123
|
+
let signature = options.signature;
|
|
124
|
+
if (typeof signature === "string") {
|
|
125
|
+
signature = Q.Data.fromBase64(signature);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// -------------------------------------------------
|
|
129
|
+
// EIP712 path
|
|
130
|
+
// -------------------------------------------------
|
|
131
|
+
if (format === "EIP712") {
|
|
132
|
+
|
|
133
|
+
return Q.Crypto.verify({
|
|
134
|
+
format: "EIP712",
|
|
135
|
+
domain: options.domain || {},
|
|
136
|
+
types: types,
|
|
137
|
+
primaryType: "Delegation",
|
|
138
|
+
message: Q.extend({}, statement, { context: context }),
|
|
139
|
+
signature: signature,
|
|
140
|
+
recovered: options.recovered
|
|
141
|
+
}).then(function (verified) {
|
|
142
|
+
|
|
143
|
+
if (!verified) {
|
|
144
|
+
resolve(false);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const recoveredAddress =
|
|
149
|
+
options.recovered && options.recovered.address;
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
!recoveredAddress ||
|
|
153
|
+
recoveredAddress.toLowerCase() !==
|
|
154
|
+
statement.parent.toLowerCase()
|
|
155
|
+
) {
|
|
156
|
+
resolve(false);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
resolve(true);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// -------------------------------------------------
|
|
165
|
+
// ES256 path
|
|
166
|
+
// -------------------------------------------------
|
|
167
|
+
if (!(options.parentPublicKey instanceof Uint8Array)) {
|
|
168
|
+
throw new Error("parentPublicKey required for ES256");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const pubBytes = Q.Data.toUint8Array(options.parentPublicKey);
|
|
172
|
+
|
|
173
|
+
return Q.Data.digest("SHA-256", pubBytes)
|
|
174
|
+
.then(function (digest) {
|
|
175
|
+
|
|
176
|
+
const expectedParent = Q.Data.toHex(digest);
|
|
177
|
+
|
|
178
|
+
if (expectedParent !== statement.parent) {
|
|
179
|
+
resolve(false);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return Q.Crypto.verify({
|
|
184
|
+
format: "ES256",
|
|
185
|
+
domain: options.domain || {},
|
|
186
|
+
types: types,
|
|
187
|
+
primaryType: "Delegation",
|
|
188
|
+
message: Q.extend({}, statement, { context: context }),
|
|
189
|
+
signature: signature,
|
|
190
|
+
publicKey: pubBytes
|
|
191
|
+
}).then(function (verified) {
|
|
192
|
+
|
|
193
|
+
if (!verified) {
|
|
194
|
+
resolve(false);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (options.recovered && typeof options.recovered === "object") {
|
|
199
|
+
options.recovered.publicKey = pubBytes;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
resolve(true);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
}).catch(reject);
|
|
207
|
+
|
|
208
|
+
} catch (e) {
|
|
209
|
+
reject(e);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for Q.Data.Bloom method files.
|
|
3
|
+
* Loaded once via options.require and passed as _ to each method file.
|
|
4
|
+
*
|
|
5
|
+
* Exports a plain object containing:
|
|
6
|
+
* - BloomFilter constructor (used by all four method files)
|
|
7
|
+
* - _positions, _optimalParams, _setBit, _testBit, _fromUint8Array
|
|
8
|
+
*/
|
|
9
|
+
Q.exports(function (Q) {
|
|
10
|
+
|
|
11
|
+
var LN2 = Math.LN2;
|
|
12
|
+
var LN2S = LN2 * LN2;
|
|
13
|
+
|
|
14
|
+
// -------------------------------------------------------------------------
|
|
15
|
+
// Sizing
|
|
16
|
+
// -------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
function _optimalParams(n, p) {
|
|
19
|
+
var m = Math.ceil(-n * Math.log(p) / LN2S);
|
|
20
|
+
var k = Math.max(1, Math.round((m / n) * LN2));
|
|
21
|
+
return { m: m, k: k };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// -------------------------------------------------------------------------
|
|
25
|
+
// Bit array ops
|
|
26
|
+
// -------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
function _setBit(bits, pos) { bits[pos >>> 3] |= (1 << (pos & 7)); }
|
|
29
|
+
function _testBit(bits, pos) { return (bits[pos >>> 3] & (1 << (pos & 7))) !== 0; }
|
|
30
|
+
|
|
31
|
+
// -------------------------------------------------------------------------
|
|
32
|
+
// Kirsch-Mitzenmacher double hashing
|
|
33
|
+
// Two SHA-256 calls -> k positions, as effective as k independent hashes.
|
|
34
|
+
// h_i(x) = (h1(x) + i * h2(x)) mod m
|
|
35
|
+
// -------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
function _positions(element, k, m) {
|
|
38
|
+
var enc = new TextEncoder();
|
|
39
|
+
return Promise.all([
|
|
40
|
+
Q.Data.digest('SHA-256', enc.encode('\x00' + element)),
|
|
41
|
+
Q.Data.digest('SHA-256', enc.encode('\x01' + element))
|
|
42
|
+
]).then(function (digests) {
|
|
43
|
+
var view = function (d) {
|
|
44
|
+
return ((d[0] << 24) | (d[1] << 16) | (d[2] << 8) | d[3]) >>> 0;
|
|
45
|
+
};
|
|
46
|
+
var h1 = view(digests[0]);
|
|
47
|
+
var h2 = view(digests[1]);
|
|
48
|
+
var positions = [];
|
|
49
|
+
for (var i = 0; i < k; i++) {
|
|
50
|
+
var combined = ((h1 >>> 0) + Math.imul(i, h2 >>> 0)) >>> 0;
|
|
51
|
+
positions.push(combined % m);
|
|
52
|
+
}
|
|
53
|
+
return positions;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// -------------------------------------------------------------------------
|
|
58
|
+
// BloomFilter class
|
|
59
|
+
// -------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
function BloomFilter(bits, k, m, count) {
|
|
62
|
+
this._bits = bits;
|
|
63
|
+
this._k = k;
|
|
64
|
+
this._m = m;
|
|
65
|
+
this._count = count || 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
BloomFilter.prototype.add = Q.promisify(function (element, callback) {
|
|
69
|
+
var self = this;
|
|
70
|
+
_positions(element, self._k, self._m).then(function (positions) {
|
|
71
|
+
positions.forEach(function (pos) { _setBit(self._bits, pos); });
|
|
72
|
+
self._count++;
|
|
73
|
+
callback(null);
|
|
74
|
+
}).catch(callback);
|
|
75
|
+
}, false, 1);
|
|
76
|
+
|
|
77
|
+
BloomFilter.prototype.has = Q.promisify(function (element, callback) {
|
|
78
|
+
var self = this;
|
|
79
|
+
_positions(element, self._k, self._m).then(function (positions) {
|
|
80
|
+
callback(null, positions.every(function (pos) { return _testBit(self._bits, pos); }));
|
|
81
|
+
}).catch(callback);
|
|
82
|
+
}, false, 1);
|
|
83
|
+
|
|
84
|
+
BloomFilter.prototype.hasMany = Q.promisify(function (elements, callback) {
|
|
85
|
+
var self = this;
|
|
86
|
+
Promise.all(elements.map(function (el) {
|
|
87
|
+
return _positions(el, self._k, self._m).then(function (positions) {
|
|
88
|
+
return positions.every(function (pos) { return _testBit(self._bits, pos); });
|
|
89
|
+
});
|
|
90
|
+
})).then(function (results) {
|
|
91
|
+
callback(null, results);
|
|
92
|
+
}).catch(callback);
|
|
93
|
+
}, false, 1);
|
|
94
|
+
|
|
95
|
+
BloomFilter.prototype.merge = Q.promisify(function (other, callback) {
|
|
96
|
+
if (other._m !== this._m || other._k !== this._k) {
|
|
97
|
+
return callback(new Error('Q.Data.Bloom.merge: incompatible filters'));
|
|
98
|
+
}
|
|
99
|
+
for (var i = 0; i < this._bits.length; i++) {
|
|
100
|
+
this._bits[i] |= other._bits[i];
|
|
101
|
+
}
|
|
102
|
+
this._count += other._count;
|
|
103
|
+
callback(null);
|
|
104
|
+
}, false, 1);
|
|
105
|
+
|
|
106
|
+
BloomFilter.prototype.falsePositiveRate = function () {
|
|
107
|
+
return Math.pow(1 - Math.exp(-this._k * this._count / this._m), this._k);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
BloomFilter.prototype.elementCount = function () { return this._count; };
|
|
111
|
+
|
|
112
|
+
BloomFilter.prototype.toBytes = function () {
|
|
113
|
+
var out = new Uint8Array(9 + this._bits.length);
|
|
114
|
+
out[0] = (this._m >>> 24) & 0xff;
|
|
115
|
+
out[1] = (this._m >>> 16) & 0xff;
|
|
116
|
+
out[2] = (this._m >>> 8) & 0xff;
|
|
117
|
+
out[3] = this._m & 0xff;
|
|
118
|
+
out[4] = this._k & 0xff;
|
|
119
|
+
out[5] = (this._count >>> 24) & 0xff;
|
|
120
|
+
out[6] = (this._count >>> 16) & 0xff;
|
|
121
|
+
out[7] = (this._count >>> 8) & 0xff;
|
|
122
|
+
out[8] = this._count & 0xff;
|
|
123
|
+
out.set(this._bits, 9);
|
|
124
|
+
return out;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
BloomFilter.prototype.toBase64 = function () {
|
|
128
|
+
return Q.Data.toBase64(this.toBytes());
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// -------------------------------------------------------------------------
|
|
132
|
+
// Deserialise helper shared by fromBytes + fromBase64
|
|
133
|
+
// -------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
function _fromUint8Array(bytes, callback) {
|
|
136
|
+
if (bytes.length < 9) {
|
|
137
|
+
return callback(new Error('Q.Data.Bloom: buffer too short'));
|
|
138
|
+
}
|
|
139
|
+
var view = function (d, o) { return ((d[o] << 24) | (d[o+1] << 16) | (d[o+2] << 8) | d[o+3]) >>> 0; };
|
|
140
|
+
var m = view(bytes, 0);
|
|
141
|
+
var k = bytes[4];
|
|
142
|
+
var count = view(bytes, 5);
|
|
143
|
+
var bits = new Uint8Array(bytes.buffer, bytes.byteOffset + 9);
|
|
144
|
+
if (bits.length < Math.ceil(m / 8)) {
|
|
145
|
+
return callback(new Error('Q.Data.Bloom: truncated bit array'));
|
|
146
|
+
}
|
|
147
|
+
callback(null, new BloomFilter(new Uint8Array(bits), k, m, count));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// -------------------------------------------------------------------------
|
|
151
|
+
// Exported helpers object
|
|
152
|
+
// -------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
BloomFilter: BloomFilter,
|
|
156
|
+
optimalParams: _optimalParams,
|
|
157
|
+
positions: _positions,
|
|
158
|
+
setBit: _setBit,
|
|
159
|
+
testBit: _testBit,
|
|
160
|
+
fromUint8Array: _fromUint8Array
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Q.Data.Bloom.create
|
|
3
|
+
* Create a new empty Bloom filter sized for n elements at false positive rate p.
|
|
4
|
+
*
|
|
5
|
+
* @param {Number} n
|
|
6
|
+
* @param {Number} [p=0.01]
|
|
7
|
+
* @param {Function} [callback] (err, BloomFilter)
|
|
8
|
+
* @return {Q.Promise<BloomFilter>}
|
|
9
|
+
*/
|
|
10
|
+
Q.exports(function (Q, _) {
|
|
11
|
+
|
|
12
|
+
return Q.promisify(function (n, p, callback) {
|
|
13
|
+
if (typeof p === 'function') { callback = p; p = 0.01; }
|
|
14
|
+
p = p || 0.01;
|
|
15
|
+
if (n <= 0) { return callback(new Error('Q.Data.Bloom.create: n must be > 0')); }
|
|
16
|
+
if (p <= 0 || p >= 1) { return callback(new Error('Q.Data.Bloom.create: p must be in (0,1)')); }
|
|
17
|
+
|
|
18
|
+
var params = _.optimalParams(n, p);
|
|
19
|
+
var bits = new Uint8Array(Math.ceil(params.m / 8));
|
|
20
|
+
callback(null, new _.BloomFilter(bits, params.k, params.m, 0));
|
|
21
|
+
}, false, 2);
|
|
22
|
+
|
|
23
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Q.Data.Bloom.fromBase64
|
|
3
|
+
* Deserialise a BloomFilter from a base64 string produced by filter.toBase64().
|
|
4
|
+
*
|
|
5
|
+
* @param {String} base64
|
|
6
|
+
* @param {Function} [callback] (err, BloomFilter)
|
|
7
|
+
* @return {Q.Promise<BloomFilter>}
|
|
8
|
+
*/
|
|
9
|
+
Q.exports(function (Q, _) {
|
|
10
|
+
|
|
11
|
+
return Q.promisify(function (base64, callback) {
|
|
12
|
+
var bytes;
|
|
13
|
+
try {
|
|
14
|
+
bytes = Q.Data.fromBase64(base64);
|
|
15
|
+
} catch (e) {
|
|
16
|
+
return callback(e);
|
|
17
|
+
}
|
|
18
|
+
_.fromUint8Array(bytes, callback);
|
|
19
|
+
}, false, 1);
|
|
20
|
+
|
|
21
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Q.Data.Bloom.fromBytes
|
|
3
|
+
* Deserialise a BloomFilter from a Uint8Array produced by filter.toBytes().
|
|
4
|
+
*
|
|
5
|
+
* @param {Uint8Array} bytes
|
|
6
|
+
* @param {Function} [callback] (err, BloomFilter)
|
|
7
|
+
* @return {Q.Promise<BloomFilter>}
|
|
8
|
+
*/
|
|
9
|
+
Q.exports(function (Q, _) {
|
|
10
|
+
|
|
11
|
+
return Q.promisify(function (bytes, callback) {
|
|
12
|
+
_.fromUint8Array(bytes, callback);
|
|
13
|
+
}, false, 1);
|
|
14
|
+
|
|
15
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Q.Data.Bloom.fromElements
|
|
3
|
+
* Build a Bloom filter from an existing array of elements in one call.
|
|
4
|
+
* All SHA-256 calls run in parallel for speed.
|
|
5
|
+
*
|
|
6
|
+
* @param {Array<String>} elements
|
|
7
|
+
* @param {Number} [p=0.01]
|
|
8
|
+
* @param {Function} [callback] (err, BloomFilter)
|
|
9
|
+
* @return {Q.Promise<BloomFilter>}
|
|
10
|
+
*/
|
|
11
|
+
Q.exports(function (Q, _) {
|
|
12
|
+
|
|
13
|
+
return Q.promisify(function (elements, p, callback) {
|
|
14
|
+
if (typeof p === 'function') { callback = p; p = 0.01; }
|
|
15
|
+
p = p || 0.01;
|
|
16
|
+
|
|
17
|
+
if (!elements || !elements.length) {
|
|
18
|
+
return callback(new Error('Q.Data.Bloom.fromElements: no elements provided'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var params = _.optimalParams(elements.length, p);
|
|
22
|
+
var bits = new Uint8Array(Math.ceil(params.m / 8));
|
|
23
|
+
|
|
24
|
+
Promise.all(elements.map(function (el) {
|
|
25
|
+
return _.positions(el, params.k, params.m).then(function (positions) {
|
|
26
|
+
positions.forEach(function (pos) { _.setBit(bits, pos); });
|
|
27
|
+
});
|
|
28
|
+
})).then(function () {
|
|
29
|
+
callback(null, new _.BloomFilter(bits, params.k, params.m, elements.length));
|
|
30
|
+
}).catch(callback);
|
|
31
|
+
}, false, 2);
|
|
32
|
+
|
|
33
|
+
});
|