joopjs 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +678 -0
- package/README.md +583 -0
- package/dist/a11y.service-C-DQQfgO.d.mts +143 -0
- package/dist/a11y.service-CauEJrJe.d.ts +143 -0
- package/dist/adapters-B6slG6hQ.d.mts +84 -0
- package/dist/adapters-B6slG6hQ.d.ts +84 -0
- package/dist/aes.service-CkoupAww.d.mts +95 -0
- package/dist/aes.service-CkoupAww.d.ts +95 -0
- package/dist/ai/index.d.mts +99 -0
- package/dist/ai/index.d.ts +99 -0
- package/dist/ai/index.js +307 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/index.mjs +304 -0
- package/dist/ai/index.mjs.map +1 -0
- package/dist/analytics/index.d.mts +42 -0
- package/dist/analytics/index.d.ts +42 -0
- package/dist/analytics/index.js +139 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/analytics/index.mjs +136 -0
- package/dist/analytics/index.mjs.map +1 -0
- package/dist/angular/index.d.mts +148 -0
- package/dist/angular/index.d.ts +148 -0
- package/dist/angular/index.js +122 -0
- package/dist/angular/index.js.map +1 -0
- package/dist/angular/index.mjs +101 -0
- package/dist/angular/index.mjs.map +1 -0
- package/dist/api/index.d.mts +128 -0
- package/dist/api/index.d.ts +128 -0
- package/dist/api/index.js +1358 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/index.mjs +1332 -0
- package/dist/api/index.mjs.map +1 -0
- package/dist/auth/index.d.mts +105 -0
- package/dist/auth/index.d.ts +105 -0
- package/dist/auth/index.js +989 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/index.mjs +979 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/auth.service-DNVB-L4U.d.mts +16 -0
- package/dist/auth.service-PjUUSUIt.d.ts +16 -0
- package/dist/banking/index.d.mts +1530 -0
- package/dist/banking/index.d.ts +1530 -0
- package/dist/banking/index.js +4739 -0
- package/dist/banking/index.js.map +1 -0
- package/dist/banking/index.mjs +4661 -0
- package/dist/banking/index.mjs.map +1 -0
- package/dist/cache/index.d.mts +40 -0
- package/dist/cache/index.d.ts +40 -0
- package/dist/cache/index.js +174 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/index.mjs +172 -0
- package/dist/cache/index.mjs.map +1 -0
- package/dist/client-profile.service-BuPeXVp5.d.mts +28 -0
- package/dist/client-profile.service-D5bRRYQp.d.ts +28 -0
- package/dist/config.models-Cqg04fAQ.d.mts +84 -0
- package/dist/config.models-Cqg04fAQ.d.ts +84 -0
- package/dist/config.service-CrCvI-JS.d.ts +31 -0
- package/dist/config.service-Cz4QQLlf.d.mts +31 -0
- package/dist/core/index.d.mts +4 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +631 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/index.mjs +619 -0
- package/dist/core/index.mjs.map +1 -0
- package/dist/crypto-utils-DriNhLdx.d.mts +30 -0
- package/dist/crypto-utils-DriNhLdx.d.ts +30 -0
- package/dist/data-storage.service-DT6xaTxE.d.ts +51 -0
- package/dist/data-storage.service-LvhGRCmw.d.mts +51 -0
- package/dist/deeplink/index.d.mts +39 -0
- package/dist/deeplink/index.d.ts +39 -0
- package/dist/deeplink/index.js +268 -0
- package/dist/deeplink/index.js.map +1 -0
- package/dist/deeplink/index.mjs +265 -0
- package/dist/deeplink/index.mjs.map +1 -0
- package/dist/deeplink.service-Ctd5u243.d.mts +35 -0
- package/dist/deeplink.service-uUuTnY9_.d.ts +35 -0
- package/dist/dev/index.d.mts +20 -0
- package/dist/dev/index.d.ts +20 -0
- package/dist/dev/index.js +51 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/dev/index.mjs +49 -0
- package/dist/dev/index.mjs.map +1 -0
- package/dist/device/index.d.mts +108 -0
- package/dist/device/index.d.ts +108 -0
- package/dist/device/index.js +960 -0
- package/dist/device/index.js.map +1 -0
- package/dist/device/index.mjs +951 -0
- package/dist/device/index.mjs.map +1 -0
- package/dist/differential-privacy-BcAv1G80.d.mts +210 -0
- package/dist/differential-privacy-C8mAUjZr.d.ts +210 -0
- package/dist/encryption/index.d.mts +75 -0
- package/dist/encryption/index.d.ts +75 -0
- package/dist/encryption/index.js +605 -0
- package/dist/encryption/index.js.map +1 -0
- package/dist/encryption/index.mjs +598 -0
- package/dist/encryption/index.mjs.map +1 -0
- package/dist/form-validator-3tkmzr_o.d.mts +72 -0
- package/dist/form-validator-3tkmzr_o.d.ts +72 -0
- package/dist/forms/index.d.mts +59 -0
- package/dist/forms/index.d.ts +59 -0
- package/dist/forms/index.js +446 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/forms/index.mjs +442 -0
- package/dist/forms/index.mjs.map +1 -0
- package/dist/i18n/index.d.mts +37 -0
- package/dist/i18n/index.d.ts +37 -0
- package/dist/i18n/index.js +147 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/index.mjs +145 -0
- package/dist/i18n/index.mjs.map +1 -0
- package/dist/idempotency.service-_6LqhivP.d.mts +372 -0
- package/dist/idempotency.service-eOKoISRD.d.ts +372 -0
- package/dist/index-B_ksKpS1.d.mts +202 -0
- package/dist/index-CqDKWTUP.d.mts +28 -0
- package/dist/index-CqDKWTUP.d.ts +28 -0
- package/dist/index-DFqEoX_l.d.ts +202 -0
- package/dist/index-Dz0gOur2.d.mts +36 -0
- package/dist/index-Dz0gOur2.d.ts +36 -0
- package/dist/index.d.mts +1336 -0
- package/dist/index.d.ts +1336 -0
- package/dist/index.js +19464 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +19155 -0
- package/dist/index.mjs.map +1 -0
- package/dist/india/index.d.mts +75 -0
- package/dist/india/index.d.ts +75 -0
- package/dist/india/index.js +325 -0
- package/dist/india/index.js.map +1 -0
- package/dist/india/index.mjs +303 -0
- package/dist/india/index.mjs.map +1 -0
- package/dist/joop-Bx7Iwj5p.d.mts +155 -0
- package/dist/joop-CA3DMeOO.d.ts +155 -0
- package/dist/native-bridge/index.d.mts +27 -0
- package/dist/native-bridge/index.d.ts +27 -0
- package/dist/native-bridge/index.js +98 -0
- package/dist/native-bridge/index.js.map +1 -0
- package/dist/native-bridge/index.mjs +96 -0
- package/dist/native-bridge/index.mjs.map +1 -0
- package/dist/network/index.d.mts +85 -0
- package/dist/network/index.d.ts +85 -0
- package/dist/network/index.js +454 -0
- package/dist/network/index.js.map +1 -0
- package/dist/network/index.mjs +451 -0
- package/dist/network/index.mjs.map +1 -0
- package/dist/network-monitor-BIwPSXme.d.mts +179 -0
- package/dist/network-monitor-Bqp2hvZr.d.ts +179 -0
- package/dist/notification.service-Dm4fvfZf.d.mts +25 -0
- package/dist/notification.service-tEMKatWJ.d.ts +25 -0
- package/dist/observability/index.d.mts +179 -0
- package/dist/observability/index.d.ts +179 -0
- package/dist/observability/index.js +559 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/index.mjs +552 -0
- package/dist/observability/index.mjs.map +1 -0
- package/dist/oidc-client-DIJcClmB.d.mts +190 -0
- package/dist/oidc-client-DxhyE59t.d.ts +190 -0
- package/dist/platform/index.d.mts +73 -0
- package/dist/platform/index.d.ts +73 -0
- package/dist/platform/index.js +127 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/index.mjs +125 -0
- package/dist/platform/index.mjs.map +1 -0
- package/dist/pwa/index.d.mts +31 -0
- package/dist/pwa/index.d.ts +31 -0
- package/dist/pwa/index.js +247 -0
- package/dist/pwa/index.js.map +1 -0
- package/dist/pwa/index.mjs +244 -0
- package/dist/pwa/index.mjs.map +1 -0
- package/dist/react/index.d.mts +133 -0
- package/dist/react/index.d.ts +133 -0
- package/dist/react/index.js +632 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +630 -0
- package/dist/react/index.mjs.map +1 -0
- package/dist/router/index.d.mts +39 -0
- package/dist/router/index.d.ts +39 -0
- package/dist/router/index.js +168 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/index.mjs +166 -0
- package/dist/router/index.mjs.map +1 -0
- package/dist/security/index.d.mts +206 -0
- package/dist/security/index.d.ts +206 -0
- package/dist/security/index.js +1297 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/index.mjs +1285 -0
- package/dist/security/index.mjs.map +1 -0
- package/dist/session/index.d.mts +115 -0
- package/dist/session/index.d.ts +115 -0
- package/dist/session/index.js +297 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/index.mjs +292 -0
- package/dist/session/index.mjs.map +1 -0
- package/dist/state/index.d.mts +43 -0
- package/dist/state/index.d.ts +43 -0
- package/dist/state/index.js +156 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/index.mjs +152 -0
- package/dist/state/index.mjs.map +1 -0
- package/dist/statement-parser-BHQtXwCM.d.ts +260 -0
- package/dist/statement-parser-C2qNmb49.d.mts +260 -0
- package/dist/storage/index.d.mts +40 -0
- package/dist/storage/index.d.ts +40 -0
- package/dist/storage/index.js +256 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/index.mjs +252 -0
- package/dist/storage/index.mjs.map +1 -0
- package/dist/sync/index.d.mts +69 -0
- package/dist/sync/index.d.ts +69 -0
- package/dist/sync/index.js +330 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/index.mjs +323 -0
- package/dist/sync/index.mjs.map +1 -0
- package/dist/sync-engine-DCIMRG5s.d.ts +61 -0
- package/dist/sync-engine-DZqyKHkK.d.mts +61 -0
- package/dist/theme/index.d.mts +53 -0
- package/dist/theme/index.d.ts +53 -0
- package/dist/theme/index.js +169 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/index.mjs +167 -0
- package/dist/theme/index.mjs.map +1 -0
- package/dist/ui/index.d.mts +66 -0
- package/dist/ui/index.d.ts +66 -0
- package/dist/ui/index.js +811 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +803 -0
- package/dist/ui/index.mjs.map +1 -0
- package/dist/utilities/index.d.mts +199 -0
- package/dist/utilities/index.d.ts +199 -0
- package/dist/utilities/index.js +1991 -0
- package/dist/utilities/index.js.map +1 -0
- package/dist/utilities/index.mjs +1923 -0
- package/dist/utilities/index.mjs.map +1 -0
- package/dist/validation/index.d.mts +60 -0
- package/dist/validation/index.d.ts +60 -0
- package/dist/validation/index.js +460 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/index.mjs +455 -0
- package/dist/validation/index.mjs.map +1 -0
- package/dist/vue/index.d.mts +135 -0
- package/dist/vue/index.d.ts +135 -0
- package/dist/vue/index.js +621 -0
- package/dist/vue/index.js.map +1 -0
- package/dist/vue/index.mjs +619 -0
- package/dist/vue/index.mjs.map +1 -0
- package/dist/watermark.service-Detur5tq.d.ts +235 -0
- package/dist/watermark.service-QNegMeQZ.d.mts +235 -0
- package/dist/workers/index.d.mts +42 -0
- package/dist/workers/index.d.ts +42 -0
- package/dist/workers/index.js +359 -0
- package/dist/workers/index.js.map +1 -0
- package/dist/workers/index.mjs +356 -0
- package/dist/workers/index.mjs.map +1 -0
- package/dist/workflow/index.d.mts +99 -0
- package/dist/workflow/index.d.ts +99 -0
- package/dist/workflow/index.js +282 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/index.mjs +279 -0
- package/dist/workflow/index.mjs.map +1 -0
- package/package.json +226 -0
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/security/secure-storage.ts
|
|
4
|
+
var MemoryBackend = class {
|
|
5
|
+
store = /* @__PURE__ */ new Map();
|
|
6
|
+
getItem(key) {
|
|
7
|
+
return this.store.get(key) ?? null;
|
|
8
|
+
}
|
|
9
|
+
setItem(key, value) {
|
|
10
|
+
this.store.set(key, value);
|
|
11
|
+
}
|
|
12
|
+
removeItem(key) {
|
|
13
|
+
this.store.delete(key);
|
|
14
|
+
}
|
|
15
|
+
key(index) {
|
|
16
|
+
return Array.from(this.store.keys())[index] ?? null;
|
|
17
|
+
}
|
|
18
|
+
get length() {
|
|
19
|
+
return this.store.size;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
function deviceFingerprint() {
|
|
23
|
+
if (typeof window === "undefined") return "server-side";
|
|
24
|
+
return [
|
|
25
|
+
navigator.userAgent,
|
|
26
|
+
String(screen.width),
|
|
27
|
+
String(screen.height),
|
|
28
|
+
String(screen.colorDepth),
|
|
29
|
+
Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
30
|
+
navigator.language
|
|
31
|
+
].join("|");
|
|
32
|
+
}
|
|
33
|
+
async function deriveKey(fingerprint, salt) {
|
|
34
|
+
const enc = new TextEncoder();
|
|
35
|
+
const keyMat = await crypto.subtle.importKey("raw", enc.encode(fingerprint), "PBKDF2", false, ["deriveKey"]);
|
|
36
|
+
return crypto.subtle.deriveKey(
|
|
37
|
+
{ name: "PBKDF2", salt: enc.encode(salt), iterations: 1e5, hash: "SHA-256" },
|
|
38
|
+
keyMat,
|
|
39
|
+
{ name: "AES-GCM", length: 256 },
|
|
40
|
+
false,
|
|
41
|
+
["encrypt", "decrypt"]
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
async function encryptValue(key, plaintext) {
|
|
45
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
46
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
47
|
+
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoded);
|
|
48
|
+
const combined = new Uint8Array(12 + ciphertext.byteLength);
|
|
49
|
+
combined.set(iv);
|
|
50
|
+
combined.set(new Uint8Array(ciphertext), 12);
|
|
51
|
+
return btoa(String.fromCharCode(...combined));
|
|
52
|
+
}
|
|
53
|
+
async function decryptValue(key, data) {
|
|
54
|
+
const combined = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
|
|
55
|
+
const iv = combined.slice(0, 12);
|
|
56
|
+
const ciphertext = combined.slice(12);
|
|
57
|
+
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
|
|
58
|
+
return new TextDecoder().decode(decrypted);
|
|
59
|
+
}
|
|
60
|
+
var JoopSecureStorage = class {
|
|
61
|
+
ns;
|
|
62
|
+
salt;
|
|
63
|
+
backend;
|
|
64
|
+
_key;
|
|
65
|
+
constructor(config = {}) {
|
|
66
|
+
this.ns = config.namespace ?? "joop";
|
|
67
|
+
this.salt = config.salt ?? (typeof location !== "undefined" ? location.hostname : "joopjs") + ":" + this.ns;
|
|
68
|
+
const b = config.backend ?? "localStorage";
|
|
69
|
+
if (b === "memory" || typeof window === "undefined") {
|
|
70
|
+
this.backend = new MemoryBackend();
|
|
71
|
+
} else if (b === "sessionStorage") {
|
|
72
|
+
this.backend = sessionStorage;
|
|
73
|
+
} else {
|
|
74
|
+
this.backend = localStorage;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async cryptoKey() {
|
|
78
|
+
if (!this._key) this._key = await deriveKey(deviceFingerprint(), this.salt);
|
|
79
|
+
return this._key;
|
|
80
|
+
}
|
|
81
|
+
storageKey(key) {
|
|
82
|
+
return `${this.ns}:${key}`;
|
|
83
|
+
}
|
|
84
|
+
async setItem(key, value) {
|
|
85
|
+
const encrypted = await encryptValue(await this.cryptoKey(), JSON.stringify(value));
|
|
86
|
+
this.backend.setItem(this.storageKey(key), encrypted);
|
|
87
|
+
}
|
|
88
|
+
async getItem(key) {
|
|
89
|
+
const raw = this.backend.getItem(this.storageKey(key));
|
|
90
|
+
if (raw === null) return null;
|
|
91
|
+
try {
|
|
92
|
+
const decrypted = await decryptValue(await this.cryptoKey(), raw);
|
|
93
|
+
return JSON.parse(decrypted);
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async removeItem(key) {
|
|
99
|
+
this.backend.removeItem(this.storageKey(key));
|
|
100
|
+
}
|
|
101
|
+
async has(key) {
|
|
102
|
+
return this.backend.getItem(this.storageKey(key)) !== null;
|
|
103
|
+
}
|
|
104
|
+
async keys() {
|
|
105
|
+
const prefix = `${this.ns}:`;
|
|
106
|
+
const result = [];
|
|
107
|
+
for (let i = 0; i < this.backend.length; i++) {
|
|
108
|
+
const k = this.backend.key(i);
|
|
109
|
+
if (k?.startsWith(prefix)) result.push(k.slice(prefix.length));
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
async clear() {
|
|
114
|
+
const ks = await this.keys();
|
|
115
|
+
ks.forEach((k) => this.backend.removeItem(this.storageKey(k)));
|
|
116
|
+
}
|
|
117
|
+
async setItems(entries) {
|
|
118
|
+
await Promise.all(Object.entries(entries).map(([k, v]) => this.setItem(k, v)));
|
|
119
|
+
}
|
|
120
|
+
async getAll() {
|
|
121
|
+
const ks = await this.keys();
|
|
122
|
+
const pairs = await Promise.all(ks.map(async (k) => [k, await this.getItem(k)]));
|
|
123
|
+
return Object.fromEntries(pairs);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// src/events/index.ts
|
|
128
|
+
var JoopSubject = class {
|
|
129
|
+
_listeners = [];
|
|
130
|
+
subscribe(listener) {
|
|
131
|
+
this._listeners.push(listener);
|
|
132
|
+
return () => {
|
|
133
|
+
this._listeners = this._listeners.filter((l) => l !== listener);
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
next(value) {
|
|
137
|
+
for (const listener of this._listeners) {
|
|
138
|
+
listener(value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
asObservable() {
|
|
142
|
+
return new JoopObservable((listener) => this.subscribe(listener));
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
var JoopBehaviorSubject = class extends JoopSubject {
|
|
146
|
+
_value;
|
|
147
|
+
constructor(initialValue) {
|
|
148
|
+
super();
|
|
149
|
+
this._value = initialValue;
|
|
150
|
+
}
|
|
151
|
+
getValue() {
|
|
152
|
+
return this._value;
|
|
153
|
+
}
|
|
154
|
+
next(value) {
|
|
155
|
+
this._value = value;
|
|
156
|
+
super.next(value);
|
|
157
|
+
}
|
|
158
|
+
subscribe(listener) {
|
|
159
|
+
listener(this._value);
|
|
160
|
+
return super.subscribe(listener);
|
|
161
|
+
}
|
|
162
|
+
asObservable() {
|
|
163
|
+
return new JoopObservable((listener) => this.subscribe(listener));
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
var JoopObservable = class {
|
|
167
|
+
constructor(_subscribeFn) {
|
|
168
|
+
this._subscribeFn = _subscribeFn;
|
|
169
|
+
}
|
|
170
|
+
_subscribeFn;
|
|
171
|
+
subscribe(listener) {
|
|
172
|
+
return this._subscribeFn(listener);
|
|
173
|
+
}
|
|
174
|
+
/** Returns the current value without subscribing (only meaningful for BehaviorSubject-backed observables). */
|
|
175
|
+
getOnce() {
|
|
176
|
+
let result;
|
|
177
|
+
const unsub = this.subscribe((v) => {
|
|
178
|
+
result = v;
|
|
179
|
+
});
|
|
180
|
+
unsub();
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// src/security/behavioral-biometrics.ts
|
|
186
|
+
var JoopBehavioralBiometrics = class {
|
|
187
|
+
_running = false;
|
|
188
|
+
_profile$ = new JoopBehaviorSubject(null);
|
|
189
|
+
_anomaly$ = new JoopSubject();
|
|
190
|
+
_listeners = [];
|
|
191
|
+
// Raw samples
|
|
192
|
+
_keyIntervals = [];
|
|
193
|
+
_dwellTimes = [];
|
|
194
|
+
_mouseSpeeds = [];
|
|
195
|
+
_mouseAccels = [];
|
|
196
|
+
_scrollVelocities = [];
|
|
197
|
+
_pressures = [];
|
|
198
|
+
// State
|
|
199
|
+
_lastKeyDown = 0;
|
|
200
|
+
_lastKeyDownMap = /* @__PURE__ */ new Map();
|
|
201
|
+
_lastMouse = { x: 0, y: 0, t: 0, speed: 0 };
|
|
202
|
+
_lastScroll = { y: 0, t: 0 };
|
|
203
|
+
_baseline = null;
|
|
204
|
+
start() {
|
|
205
|
+
if (this._running) return;
|
|
206
|
+
this._running = true;
|
|
207
|
+
this._attach("keydown", this._onKeyDown.bind(this));
|
|
208
|
+
this._attach("keyup", this._onKeyUp.bind(this));
|
|
209
|
+
this._attach("mousemove", this._onMouseMove.bind(this));
|
|
210
|
+
this._attach("scroll", this._onScroll.bind(this), { passive: true });
|
|
211
|
+
this._attach("touchstart", this._onTouch.bind(this), { passive: true });
|
|
212
|
+
}
|
|
213
|
+
stop() {
|
|
214
|
+
this._running = false;
|
|
215
|
+
for (const remove of this._listeners) remove();
|
|
216
|
+
this._listeners = [];
|
|
217
|
+
}
|
|
218
|
+
reset() {
|
|
219
|
+
this._keyIntervals = [];
|
|
220
|
+
this._dwellTimes = [];
|
|
221
|
+
this._mouseSpeeds = [];
|
|
222
|
+
this._mouseAccels = [];
|
|
223
|
+
this._scrollVelocities = [];
|
|
224
|
+
this._pressures = [];
|
|
225
|
+
this._baseline = null;
|
|
226
|
+
this._profile$.next(null);
|
|
227
|
+
}
|
|
228
|
+
setBaseline(profile) {
|
|
229
|
+
this._baseline = profile ?? this._profile$.getValue();
|
|
230
|
+
}
|
|
231
|
+
getScore() {
|
|
232
|
+
const p = this._profile$.getValue();
|
|
233
|
+
if (!p || p.sampleCount < 20) return { human: 0.5, bot: 0.5, confidence: 0 };
|
|
234
|
+
let humanScore = 0;
|
|
235
|
+
let checks = 0;
|
|
236
|
+
if (p.sampleCount >= 5) {
|
|
237
|
+
const cv = _cv(this._keyIntervals);
|
|
238
|
+
humanScore += cv > 0.15 ? 1 : cv > 0.05 ? 0.5 : 0;
|
|
239
|
+
checks++;
|
|
240
|
+
}
|
|
241
|
+
if (this._mouseSpeeds.length >= 10) {
|
|
242
|
+
const cv = _cv(this._mouseSpeeds);
|
|
243
|
+
humanScore += cv > 0.2 ? 1 : cv > 0.08 ? 0.5 : 0;
|
|
244
|
+
checks++;
|
|
245
|
+
}
|
|
246
|
+
if (this._scrollVelocities.length >= 5) {
|
|
247
|
+
const cv = _cv(this._scrollVelocities);
|
|
248
|
+
humanScore += cv > 0.1 ? 1 : 0.3;
|
|
249
|
+
checks++;
|
|
250
|
+
}
|
|
251
|
+
const confidence = Math.min(1, p.sampleCount / 100);
|
|
252
|
+
const human = checks ? humanScore / checks : 0.5;
|
|
253
|
+
return { human, bot: 1 - human, confidence };
|
|
254
|
+
}
|
|
255
|
+
getProfile() {
|
|
256
|
+
return this._profile$.getValue();
|
|
257
|
+
}
|
|
258
|
+
profile$() {
|
|
259
|
+
return this._profile$.asObservable();
|
|
260
|
+
}
|
|
261
|
+
anomaly$() {
|
|
262
|
+
return this._anomaly$.asObservable();
|
|
263
|
+
}
|
|
264
|
+
/** Similarity to another profile (0=different, 1=identical) */
|
|
265
|
+
compare(other) {
|
|
266
|
+
const p = this._profile$.getValue();
|
|
267
|
+
if (!p) return 0;
|
|
268
|
+
const delta = (a, b) => 1 - Math.min(1, Math.abs(a - b) / (Math.max(a, b) || 1));
|
|
269
|
+
return (delta(p.avgTypingInterval, other.avgTypingInterval) + delta(p.mouseSpeed, other.mouseSpeed) + delta(p.scrollVelocity, other.scrollVelocity)) / 3;
|
|
270
|
+
}
|
|
271
|
+
_onKeyDown(e) {
|
|
272
|
+
const now = Date.now();
|
|
273
|
+
if (this._lastKeyDown > 0) this._keyIntervals.push(now - this._lastKeyDown);
|
|
274
|
+
this._lastKeyDownMap.set(e.key, now);
|
|
275
|
+
this._lastKeyDown = now;
|
|
276
|
+
this._update();
|
|
277
|
+
}
|
|
278
|
+
_onKeyUp(e) {
|
|
279
|
+
const down = this._lastKeyDownMap.get(e.key);
|
|
280
|
+
if (down) {
|
|
281
|
+
this._dwellTimes.push(Date.now() - down);
|
|
282
|
+
this._lastKeyDownMap.delete(e.key);
|
|
283
|
+
}
|
|
284
|
+
this._update();
|
|
285
|
+
}
|
|
286
|
+
_onMouseMove(e) {
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
const { x: lx, y: ly, t: lt, speed: ls } = this._lastMouse;
|
|
289
|
+
if (lt > 0) {
|
|
290
|
+
const dx = e.clientX - lx, dy = e.clientY - ly, dt = now - lt;
|
|
291
|
+
if (dt > 0) {
|
|
292
|
+
const speed = Math.sqrt(dx * dx + dy * dy) / dt;
|
|
293
|
+
const accel = Math.abs(speed - ls) / dt;
|
|
294
|
+
this._mouseSpeeds.push(speed);
|
|
295
|
+
this._mouseAccels.push(accel);
|
|
296
|
+
this._lastMouse = { x: e.clientX, y: e.clientY, t: now, speed };
|
|
297
|
+
this._update();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
this._lastMouse = { x: e.clientX, y: e.clientY, t: now, speed: 0 };
|
|
302
|
+
}
|
|
303
|
+
_onScroll() {
|
|
304
|
+
const now = Date.now();
|
|
305
|
+
const y = window.scrollY;
|
|
306
|
+
const { y: ly, t: lt } = this._lastScroll;
|
|
307
|
+
if (lt > 0 && now - lt > 0) {
|
|
308
|
+
this._scrollVelocities.push(Math.abs(y - ly) / (now - lt));
|
|
309
|
+
this._update();
|
|
310
|
+
}
|
|
311
|
+
this._lastScroll = { y, t: now };
|
|
312
|
+
}
|
|
313
|
+
_onTouch(e) {
|
|
314
|
+
if (e.touches[0] && e.touches[0].force !== void 0) {
|
|
315
|
+
const f = e.touches[0].force;
|
|
316
|
+
this._pressures.push(f);
|
|
317
|
+
}
|
|
318
|
+
this._update();
|
|
319
|
+
}
|
|
320
|
+
_update() {
|
|
321
|
+
const count = this._keyIntervals.length + this._mouseSpeeds.length;
|
|
322
|
+
const profile = {
|
|
323
|
+
avgTypingInterval: _mean(this._keyIntervals),
|
|
324
|
+
avgDwellTime: _mean(this._dwellTimes),
|
|
325
|
+
mouseSpeed: _mean(this._mouseSpeeds),
|
|
326
|
+
mouseAcceleration: _mean(this._mouseAccels),
|
|
327
|
+
scrollVelocity: _mean(this._scrollVelocities),
|
|
328
|
+
touchPressureAvg: _mean(this._pressures),
|
|
329
|
+
sampleCount: count,
|
|
330
|
+
capturedAt: Date.now()
|
|
331
|
+
};
|
|
332
|
+
this._profile$.next(profile);
|
|
333
|
+
if (this._baseline && count >= 30) {
|
|
334
|
+
const { avgTypingInterval: bt } = this._baseline;
|
|
335
|
+
const devs = _stddev(this._keyIntervals.slice(-20));
|
|
336
|
+
if (bt > 0 && devs > 0) {
|
|
337
|
+
const z = Math.abs(profile.avgTypingInterval - bt) / devs;
|
|
338
|
+
if (z > 2.5) this._anomaly$.next({ type: "typing", deviation: z, value: profile.avgTypingInterval, baseline: bt });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
_attach(event, handler, opts) {
|
|
343
|
+
document.addEventListener(event, handler, opts);
|
|
344
|
+
this._listeners.push(() => document.removeEventListener(event, handler));
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
function _mean(arr) {
|
|
348
|
+
return arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
|
|
349
|
+
}
|
|
350
|
+
function _stddev(arr) {
|
|
351
|
+
if (arr.length < 2) return 0;
|
|
352
|
+
const m = _mean(arr);
|
|
353
|
+
return Math.sqrt(arr.reduce((s, v) => s + (v - m) ** 2, 0) / (arr.length - 1));
|
|
354
|
+
}
|
|
355
|
+
function _cv(arr) {
|
|
356
|
+
const m = _mean(arr);
|
|
357
|
+
return m ? _stddev(arr) / m : 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/security/risk-engine.ts
|
|
361
|
+
var BUILT_IN_FACTORS = [
|
|
362
|
+
{
|
|
363
|
+
name: "failed_attempts",
|
|
364
|
+
weight: 0.25,
|
|
365
|
+
evaluate: (ctx) => Math.min(1, (ctx.failedAttempts ?? 0) / 5)
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: "biometrics",
|
|
369
|
+
weight: 0.2,
|
|
370
|
+
evaluate: (ctx) => {
|
|
371
|
+
if (ctx.biometricScore === void 0) return 0.3;
|
|
372
|
+
return 1 - ctx.biometricScore;
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: "mfa",
|
|
377
|
+
weight: 0.15,
|
|
378
|
+
evaluate: (ctx) => ctx.mfaVerified ? 0 : 0.5
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
name: "known_device",
|
|
382
|
+
weight: 0.15,
|
|
383
|
+
evaluate: (ctx) => ctx.knownDevice === false ? 0.7 : 0
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: "vpn",
|
|
387
|
+
weight: 0.1,
|
|
388
|
+
evaluate: (ctx) => ctx.vpnDetected ? 0.6 : 0
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: "time_anomaly",
|
|
392
|
+
weight: 0.1,
|
|
393
|
+
evaluate: (ctx) => {
|
|
394
|
+
const h = ctx.hourOfDay ?? 12;
|
|
395
|
+
return h >= 1 && h <= 5 ? 0.4 : 0;
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: "transaction_amount",
|
|
400
|
+
weight: 0.05,
|
|
401
|
+
evaluate: (ctx) => {
|
|
402
|
+
const amt = ctx.transactionAmount ?? 0;
|
|
403
|
+
if (amt > 1e5) return 0.9;
|
|
404
|
+
if (amt > 1e4) return 0.5;
|
|
405
|
+
if (amt > 1e3) return 0.2;
|
|
406
|
+
return 0;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
];
|
|
410
|
+
var JoopRiskEngine = class {
|
|
411
|
+
_factors = [...BUILT_IN_FACTORS];
|
|
412
|
+
_score$ = new JoopBehaviorSubject(null);
|
|
413
|
+
_highRiskHandlers = [];
|
|
414
|
+
_stepUpHandler = null;
|
|
415
|
+
_stepUpThreshold = 70;
|
|
416
|
+
/** Replace all factors */
|
|
417
|
+
configure(factors) {
|
|
418
|
+
this._factors = factors;
|
|
419
|
+
}
|
|
420
|
+
/** Add or update a single named factor */
|
|
421
|
+
addFactor(name, weight, evaluate) {
|
|
422
|
+
const idx = this._factors.findIndex((f) => f.name === name);
|
|
423
|
+
if (idx >= 0) this._factors[idx] = { name, weight, evaluate };
|
|
424
|
+
else this._factors.push({ name, weight, evaluate });
|
|
425
|
+
}
|
|
426
|
+
removeFactor(name) {
|
|
427
|
+
this._factors = this._factors.filter((f) => f.name !== name);
|
|
428
|
+
}
|
|
429
|
+
evaluate(ctx) {
|
|
430
|
+
const totalWeight = this._factors.reduce((s, f) => s + f.weight, 0) || 1;
|
|
431
|
+
const factors = {};
|
|
432
|
+
let weighted = 0;
|
|
433
|
+
for (const f of this._factors) {
|
|
434
|
+
const raw = Math.max(0, Math.min(1, f.evaluate(ctx)));
|
|
435
|
+
factors[f.name] = raw;
|
|
436
|
+
weighted += raw * (f.weight / totalWeight);
|
|
437
|
+
}
|
|
438
|
+
const total = Math.round(weighted * 100);
|
|
439
|
+
const level = total >= 75 ? "critical" : total >= 50 ? "high" : total >= 25 ? "medium" : "low";
|
|
440
|
+
const score = { total, level, factors, context: ctx, timestamp: Date.now() };
|
|
441
|
+
this._score$.next(score);
|
|
442
|
+
for (const { threshold, handler } of this._highRiskHandlers) {
|
|
443
|
+
if (total >= threshold) handler(score);
|
|
444
|
+
}
|
|
445
|
+
if (this._stepUpHandler && total >= this._stepUpThreshold) this._stepUpHandler(score);
|
|
446
|
+
return score;
|
|
447
|
+
}
|
|
448
|
+
score$() {
|
|
449
|
+
return this._score$.asObservable();
|
|
450
|
+
}
|
|
451
|
+
lastScore() {
|
|
452
|
+
return this._score$.getValue();
|
|
453
|
+
}
|
|
454
|
+
/** Register a handler triggered when score ≥ threshold */
|
|
455
|
+
onHighRisk(threshold, handler) {
|
|
456
|
+
const entry = { threshold, handler };
|
|
457
|
+
this._highRiskHandlers.push(entry);
|
|
458
|
+
return () => {
|
|
459
|
+
this._highRiskHandlers = this._highRiskHandlers.filter((h) => h !== entry);
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/** Configure automatic step-up auth trigger */
|
|
463
|
+
onStepUpRequired(threshold, handler) {
|
|
464
|
+
this._stepUpThreshold = threshold;
|
|
465
|
+
this._stepUpHandler = handler;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// src/security/pii-scanner.ts
|
|
470
|
+
var BUILT_IN_PATTERNS = [
|
|
471
|
+
{ name: "credit_card", regex: /\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})\b/g },
|
|
472
|
+
{ name: "ssn", regex: /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/g },
|
|
473
|
+
{ name: "email", regex: /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g },
|
|
474
|
+
{ name: "phone_us", regex: /(?:\+1[\s-]?)?(?:\(\d{3}\)|\d{3})[-.\s]?\d{3}[-.\s]?\d{4}\b/g },
|
|
475
|
+
{ name: "iban", regex: /\b[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}(?:[A-Z0-9]{0,16})?\b/g },
|
|
476
|
+
{ name: "passport", regex: /\b[A-Z]{1,2}[0-9]{6,9}\b/g },
|
|
477
|
+
{ name: "ip_address", regex: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g },
|
|
478
|
+
{ name: "jwt", regex: /eyJ[A-Za-z0-9_\-]+\.eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+/g },
|
|
479
|
+
{ name: "api_key", regex: /\b(?:sk|pk|api|key)[_\-]?[a-zA-Z0-9]{20,}\b/gi }
|
|
480
|
+
];
|
|
481
|
+
var DEFAULT_REPLACER = {
|
|
482
|
+
credit_card: "[CARD-REDACTED]",
|
|
483
|
+
ssn: "[SSN-REDACTED]",
|
|
484
|
+
email: "[EMAIL-REDACTED]",
|
|
485
|
+
phone_us: "[PHONE-REDACTED]",
|
|
486
|
+
iban: "[IBAN-REDACTED]",
|
|
487
|
+
passport: "[PASSPORT-REDACTED]",
|
|
488
|
+
ip_address: "[IP-REDACTED]",
|
|
489
|
+
jwt: "[JWT-REDACTED]",
|
|
490
|
+
api_key: "[KEY-REDACTED]"
|
|
491
|
+
};
|
|
492
|
+
var JoopPIIScanner = class {
|
|
493
|
+
_patterns = [...BUILT_IN_PATTERNS];
|
|
494
|
+
/** Add or replace a custom pattern */
|
|
495
|
+
addPattern(name, regex) {
|
|
496
|
+
const idx = this._patterns.findIndex((p) => p.name === name);
|
|
497
|
+
if (idx >= 0) this._patterns[idx] = { name, regex };
|
|
498
|
+
else this._patterns.push({ name, regex });
|
|
499
|
+
}
|
|
500
|
+
removePattern(name) {
|
|
501
|
+
this._patterns = this._patterns.filter((p) => p.name !== name);
|
|
502
|
+
}
|
|
503
|
+
/** Return all PII findings in text */
|
|
504
|
+
scan(text) {
|
|
505
|
+
const findings = [];
|
|
506
|
+
for (const { name, regex } of this._patterns) {
|
|
507
|
+
const r = new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : regex.flags + "g");
|
|
508
|
+
r.lastIndex = 0;
|
|
509
|
+
let m;
|
|
510
|
+
while ((m = r.exec(text)) !== null) {
|
|
511
|
+
findings.push({ type: name, value: m[0], start: m.index, end: m.index + m[0].length });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return findings.sort((a, b) => a.start - b.start);
|
|
515
|
+
}
|
|
516
|
+
/** Replace all PII in text. Replacement can be a string or function(type) */
|
|
517
|
+
redact(text, replacement) {
|
|
518
|
+
const findings = this.scan(text);
|
|
519
|
+
if (findings.length === 0) return text;
|
|
520
|
+
let result = text;
|
|
521
|
+
for (const f of [...findings].reverse()) {
|
|
522
|
+
const repl = replacement ? typeof replacement === "string" ? replacement : replacement(f.type) : DEFAULT_REPLACER[f.type] ?? `[${f.type.toUpperCase()}-REDACTED]`;
|
|
523
|
+
result = result.slice(0, f.start) + repl + result.slice(f.end);
|
|
524
|
+
}
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
527
|
+
/** Returns true if the text contains any PII */
|
|
528
|
+
contains(text) {
|
|
529
|
+
return this.scan(text).length > 0;
|
|
530
|
+
}
|
|
531
|
+
/** Hash PII values (SHA-256 truncated) — returns text with hashed placeholders */
|
|
532
|
+
async pseudonymize(text) {
|
|
533
|
+
const findings = this.scan(text);
|
|
534
|
+
if (findings.length === 0) return text;
|
|
535
|
+
const cache = /* @__PURE__ */ new Map();
|
|
536
|
+
const hash = async (val) => {
|
|
537
|
+
if (cache.has(val)) return cache.get(val);
|
|
538
|
+
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(val));
|
|
539
|
+
const h = Array.from(new Uint8Array(buf)).slice(0, 8).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
540
|
+
cache.set(val, h);
|
|
541
|
+
return h;
|
|
542
|
+
};
|
|
543
|
+
let result = text;
|
|
544
|
+
for (const f of [...findings].reverse()) {
|
|
545
|
+
const h = await hash(f.value);
|
|
546
|
+
result = result.slice(0, f.start) + `[${f.type}:${h}]` + result.slice(f.end);
|
|
547
|
+
}
|
|
548
|
+
return result;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// src/security/secret-sharing.ts
|
|
553
|
+
var PRIME = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F");
|
|
554
|
+
var JoopSecretSharing = class {
|
|
555
|
+
/**
|
|
556
|
+
* Split a secret string into `n` shares, requiring `k` shares to reconstruct.
|
|
557
|
+
* Uses Shamir's Secret Sharing over GF(PRIME).
|
|
558
|
+
*/
|
|
559
|
+
split(secret, n, k) {
|
|
560
|
+
if (k > n) throw new Error("k must be \u2264 n");
|
|
561
|
+
if (k < 2) throw new Error("k must be at least 2");
|
|
562
|
+
const secretBytes = new TextEncoder().encode(secret);
|
|
563
|
+
const secretBig = _bytesToBigInt(secretBytes);
|
|
564
|
+
if (secretBig >= PRIME) throw new Error("Secret too large for this field");
|
|
565
|
+
const coefficients = [secretBig];
|
|
566
|
+
for (let i = 1; i < k; i++) {
|
|
567
|
+
coefficients.push(_randomBigInt(PRIME));
|
|
568
|
+
}
|
|
569
|
+
const shares = [];
|
|
570
|
+
for (let x = 1; x <= n; x++) {
|
|
571
|
+
let y = BigInt(0);
|
|
572
|
+
const xBig = BigInt(x);
|
|
573
|
+
for (let i = 0; i < coefficients.length; i++) {
|
|
574
|
+
y = _modAdd(y, _modMul(coefficients[i], _modPow(xBig, BigInt(i), PRIME), PRIME), PRIME);
|
|
575
|
+
}
|
|
576
|
+
shares.push({ x, y });
|
|
577
|
+
}
|
|
578
|
+
return shares;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Reconstruct the secret from k or more shares using Lagrange interpolation.
|
|
582
|
+
*/
|
|
583
|
+
combine(shares) {
|
|
584
|
+
if (shares.length < 2) throw new Error("At least 2 shares required");
|
|
585
|
+
let secret = BigInt(0);
|
|
586
|
+
for (let i = 0; i < shares.length; i++) {
|
|
587
|
+
const xi = BigInt(shares[i].x);
|
|
588
|
+
let num = BigInt(1);
|
|
589
|
+
let den = BigInt(1);
|
|
590
|
+
for (let j = 0; j < shares.length; j++) {
|
|
591
|
+
if (i === j) continue;
|
|
592
|
+
const xj = BigInt(shares[j].x);
|
|
593
|
+
num = _modMul(num, _modSub(BigInt(0), xj, PRIME), PRIME);
|
|
594
|
+
den = _modMul(den, _modSub(xi, xj, PRIME), PRIME);
|
|
595
|
+
}
|
|
596
|
+
const lagrange = _modMul(num, _modPow(den, PRIME - BigInt(2), PRIME), PRIME);
|
|
597
|
+
secret = _modAdd(secret, _modMul(shares[i].y, lagrange, PRIME), PRIME);
|
|
598
|
+
}
|
|
599
|
+
const bytes = _bigIntToBytes(secret);
|
|
600
|
+
return new TextDecoder().decode(bytes);
|
|
601
|
+
}
|
|
602
|
+
/** Verify that a set of shares produces a consistent secret */
|
|
603
|
+
verify(sharesA, sharesB) {
|
|
604
|
+
try {
|
|
605
|
+
return this.combine(sharesA) === this.combine(sharesB);
|
|
606
|
+
} catch {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/** Serialize shares to base64 strings for transport */
|
|
611
|
+
serializeShare(share) {
|
|
612
|
+
return btoa(JSON.stringify({ x: share.x, y: share.y.toString(16) }));
|
|
613
|
+
}
|
|
614
|
+
/** Deserialize a base64 share */
|
|
615
|
+
deserializeShare(encoded) {
|
|
616
|
+
const { x, y } = JSON.parse(atob(encoded));
|
|
617
|
+
return { x, y: BigInt(`0x${y}`) };
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
function _modAdd(a, b, p) {
|
|
621
|
+
return ((a + b) % p + p) % p;
|
|
622
|
+
}
|
|
623
|
+
function _modSub(a, b, p) {
|
|
624
|
+
return ((a - b) % p + p) % p;
|
|
625
|
+
}
|
|
626
|
+
function _modMul(a, b, p) {
|
|
627
|
+
return a * b % p;
|
|
628
|
+
}
|
|
629
|
+
function _modPow(base, exp, mod) {
|
|
630
|
+
let result = BigInt(1);
|
|
631
|
+
base = base % mod;
|
|
632
|
+
while (exp > 0n) {
|
|
633
|
+
if (exp % 2n === 1n) result = _modMul(result, base, mod);
|
|
634
|
+
exp >>= 1n;
|
|
635
|
+
base = _modMul(base, base, mod);
|
|
636
|
+
}
|
|
637
|
+
return result;
|
|
638
|
+
}
|
|
639
|
+
function _bytesToBigInt(bytes) {
|
|
640
|
+
let result = 0n;
|
|
641
|
+
for (const byte of bytes) result = result << 8n | BigInt(byte);
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
function _bigIntToBytes(n) {
|
|
645
|
+
if (n === 0n) return new Uint8Array([0]);
|
|
646
|
+
const hex = n.toString(16).padStart(2, "0");
|
|
647
|
+
const padded = hex.length % 2 ? "0" + hex : hex;
|
|
648
|
+
const bytes = new Uint8Array(padded.length / 2);
|
|
649
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(padded.slice(i * 2, i * 2 + 2), 16);
|
|
650
|
+
return bytes;
|
|
651
|
+
}
|
|
652
|
+
function _randomBigInt(max) {
|
|
653
|
+
const bytes = new Uint8Array(32);
|
|
654
|
+
crypto.getRandomValues(bytes);
|
|
655
|
+
return _bytesToBigInt(bytes) % max;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/security/differential-privacy.ts
|
|
659
|
+
var JoopDifferentialPrivacy = class {
|
|
660
|
+
/**
|
|
661
|
+
* Add Laplace noise to a numeric value.
|
|
662
|
+
* @param value - the true value
|
|
663
|
+
* @param sensitivity - L1 sensitivity (max change in output from one record change)
|
|
664
|
+
* @param epsilon - privacy budget (smaller = more privacy)
|
|
665
|
+
*/
|
|
666
|
+
addLaplaceNoise(value, sensitivity, epsilon) {
|
|
667
|
+
const b = sensitivity / epsilon;
|
|
668
|
+
return value + _sampleLaplace(b);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Privatize a count (integer ≥ 0).
|
|
672
|
+
* Returns a rounded, non-negative noisy count.
|
|
673
|
+
*/
|
|
674
|
+
privatizeCount(count, epsilon = 1) {
|
|
675
|
+
const noisy = this.addLaplaceNoise(count, 1, epsilon);
|
|
676
|
+
return Math.max(0, Math.round(noisy));
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Privatize an average over an array of values.
|
|
680
|
+
* @param values - the actual values
|
|
681
|
+
* @param epsilon - privacy budget
|
|
682
|
+
* @param valueRange - [min, max] of each value (for sensitivity calculation)
|
|
683
|
+
*/
|
|
684
|
+
privatizeAverage(values, epsilon = 1, valueRange = [0, 1]) {
|
|
685
|
+
if (values.length === 0) return 0;
|
|
686
|
+
const sensitivity = (valueRange[1] - valueRange[0]) / values.length;
|
|
687
|
+
const trueAvg = values.reduce((s, v) => s + v, 0) / values.length;
|
|
688
|
+
return this.addLaplaceNoise(trueAvg, sensitivity, epsilon);
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Privatize a sum.
|
|
692
|
+
*/
|
|
693
|
+
privatizeSum(values, epsilon = 1, sensitivity = 1) {
|
|
694
|
+
const trueSum = values.reduce((s, v) => s + v, 0);
|
|
695
|
+
return this.addLaplaceNoise(trueSum, sensitivity, epsilon);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Randomized response for boolean values (local differential privacy).
|
|
699
|
+
* Each individual flips their true value with probability p = 1/(1+e^epsilon).
|
|
700
|
+
* @returns a potentially flipped boolean
|
|
701
|
+
*/
|
|
702
|
+
randomizedResponse(value, epsilon = 1) {
|
|
703
|
+
const p = 1 / (1 + Math.exp(epsilon));
|
|
704
|
+
return Math.random() < p ? !value : value;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Randomized response for categorical values (k-ary randomized response).
|
|
708
|
+
* Returns the true value with probability p, and a random other value with 1-p.
|
|
709
|
+
*/
|
|
710
|
+
randomizedResponseCategorical(value, domain, epsilon = 1) {
|
|
711
|
+
const k = domain.length;
|
|
712
|
+
if (k < 2) return value;
|
|
713
|
+
const p = Math.exp(epsilon) / (Math.exp(epsilon) + k - 1);
|
|
714
|
+
if (Math.random() < p) return value;
|
|
715
|
+
const others = domain.filter((v) => v !== value);
|
|
716
|
+
return others[Math.floor(Math.random() * others.length)];
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Gaussian mechanism: add N(0, sigma^2) noise where sigma = sqrt(2*ln(1.25/delta)) * sensitivity / epsilon
|
|
720
|
+
* Provides (epsilon, delta)-DP.
|
|
721
|
+
*/
|
|
722
|
+
addGaussianNoise(value, sensitivity, epsilon, delta = 1e-5) {
|
|
723
|
+
const sigma = Math.sqrt(2 * Math.log(1.25 / delta)) * sensitivity / epsilon;
|
|
724
|
+
return value + _sampleGaussian(sigma);
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Compute the epsilon required to achieve (epsilon)-DP for Laplace mechanism.
|
|
728
|
+
*/
|
|
729
|
+
requiredEpsilon(sensitivity, targetNoise) {
|
|
730
|
+
return sensitivity / targetNoise;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Privacy amplification: effective epsilon after subsampling fraction q of the data.
|
|
734
|
+
*/
|
|
735
|
+
amplifiedEpsilon(epsilon, samplingRate) {
|
|
736
|
+
return Math.log(1 + samplingRate * (Math.exp(epsilon) - 1));
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
function _sampleLaplace(b) {
|
|
740
|
+
const u = Math.random() - 0.5;
|
|
741
|
+
return -b * Math.sign(u) * Math.log(1 - 2 * Math.abs(u));
|
|
742
|
+
}
|
|
743
|
+
function _sampleGaussian(sigma) {
|
|
744
|
+
const u1 = 1 - Math.random();
|
|
745
|
+
const u2 = Math.random();
|
|
746
|
+
return sigma * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/security/screen-security.ts
|
|
750
|
+
var JoopScreenSecurityService = class {
|
|
751
|
+
_config;
|
|
752
|
+
_listeners = /* @__PURE__ */ new Map();
|
|
753
|
+
_active = false;
|
|
754
|
+
_devtoolsTimer = null;
|
|
755
|
+
// Elements registered as sensitive
|
|
756
|
+
_sensitiveElements = /* @__PURE__ */ new Set();
|
|
757
|
+
_blurOverlay = null;
|
|
758
|
+
constructor(config = {}) {
|
|
759
|
+
this._config = {
|
|
760
|
+
blurOnTabSwitch: config.blurOnTabSwitch ?? true,
|
|
761
|
+
clipboardProtection: config.clipboardProtection ?? false,
|
|
762
|
+
devtoolsDetection: config.devtoolsDetection ?? false,
|
|
763
|
+
screenshotWarning: config.screenshotWarning ?? false
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
activate() {
|
|
767
|
+
if (this._active) return;
|
|
768
|
+
this._active = true;
|
|
769
|
+
if (this._config.blurOnTabSwitch) {
|
|
770
|
+
document.addEventListener("visibilitychange", this._onVisibilityChange);
|
|
771
|
+
}
|
|
772
|
+
if (this._config.screenshotWarning) {
|
|
773
|
+
document.addEventListener("keyup", this._onKeyUp);
|
|
774
|
+
}
|
|
775
|
+
if (this._config.devtoolsDetection) {
|
|
776
|
+
this._startDevtoolsDetection();
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
deactivate() {
|
|
780
|
+
if (!this._active) return;
|
|
781
|
+
this._active = false;
|
|
782
|
+
document.removeEventListener("visibilitychange", this._onVisibilityChange);
|
|
783
|
+
document.removeEventListener("keyup", this._onKeyUp);
|
|
784
|
+
if (this._devtoolsTimer) {
|
|
785
|
+
clearInterval(this._devtoolsTimer);
|
|
786
|
+
this._devtoolsTimer = null;
|
|
787
|
+
}
|
|
788
|
+
this._removeOverlay();
|
|
789
|
+
}
|
|
790
|
+
registerSensitive(element) {
|
|
791
|
+
this._sensitiveElements.add(element);
|
|
792
|
+
if (this._config.clipboardProtection) {
|
|
793
|
+
element.addEventListener("copy", this._blockCopy);
|
|
794
|
+
element.addEventListener("cut", this._blockCopy);
|
|
795
|
+
}
|
|
796
|
+
return () => this.unregisterSensitive(element);
|
|
797
|
+
}
|
|
798
|
+
unregisterSensitive(element) {
|
|
799
|
+
this._sensitiveElements.delete(element);
|
|
800
|
+
element.removeEventListener("copy", this._blockCopy);
|
|
801
|
+
element.removeEventListener("cut", this._blockCopy);
|
|
802
|
+
}
|
|
803
|
+
on(event, handler) {
|
|
804
|
+
if (!this._listeners.has(event)) this._listeners.set(event, /* @__PURE__ */ new Set());
|
|
805
|
+
this._listeners.get(event).add(handler);
|
|
806
|
+
return () => this._listeners.get(event)?.delete(handler);
|
|
807
|
+
}
|
|
808
|
+
isDevtoolsOpen() {
|
|
809
|
+
const threshold = 160;
|
|
810
|
+
return window.outerWidth - window.innerWidth > threshold || window.outerHeight - window.innerHeight > threshold;
|
|
811
|
+
}
|
|
812
|
+
destroy() {
|
|
813
|
+
this.deactivate();
|
|
814
|
+
this._sensitiveElements.clear();
|
|
815
|
+
this._listeners.clear();
|
|
816
|
+
}
|
|
817
|
+
_onVisibilityChange = () => {
|
|
818
|
+
if (document.hidden) {
|
|
819
|
+
this._showOverlay();
|
|
820
|
+
this._emit("tab-hidden");
|
|
821
|
+
} else {
|
|
822
|
+
this._removeOverlay();
|
|
823
|
+
this._emit("tab-visible");
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
_onKeyUp = (e) => {
|
|
827
|
+
if (e.key === "PrintScreen" || e.code === "PrintScreen") {
|
|
828
|
+
this._emit("screenshot-attempt");
|
|
829
|
+
this._showOverlay();
|
|
830
|
+
setTimeout(() => this._removeOverlay(), 300);
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
_blockCopy = (e) => {
|
|
834
|
+
e.preventDefault();
|
|
835
|
+
this._emit("clipboard-blocked");
|
|
836
|
+
};
|
|
837
|
+
_startDevtoolsDetection() {
|
|
838
|
+
this._devtoolsTimer = setInterval(() => {
|
|
839
|
+
if (this.isDevtoolsOpen()) this._emit("devtools-open");
|
|
840
|
+
}, 1e3);
|
|
841
|
+
}
|
|
842
|
+
_showOverlay() {
|
|
843
|
+
if (this._blurOverlay) return;
|
|
844
|
+
const overlay = document.createElement("div");
|
|
845
|
+
overlay.style.cssText = `
|
|
846
|
+
position: fixed; inset: 0; z-index: 999999;
|
|
847
|
+
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
|
|
848
|
+
background: rgba(0,0,0,0.4); display: flex;
|
|
849
|
+
align-items: center; justify-content: center;
|
|
850
|
+
`;
|
|
851
|
+
overlay.setAttribute("aria-hidden", "true");
|
|
852
|
+
const msg = document.createElement("div");
|
|
853
|
+
msg.textContent = "Switch back to continue";
|
|
854
|
+
msg.style.cssText = "color:#fff; font-size:18px; font-weight:600; pointer-events:none;";
|
|
855
|
+
overlay.appendChild(msg);
|
|
856
|
+
document.body.appendChild(overlay);
|
|
857
|
+
this._blurOverlay = overlay;
|
|
858
|
+
}
|
|
859
|
+
_removeOverlay() {
|
|
860
|
+
if (this._blurOverlay) {
|
|
861
|
+
this._blurOverlay.remove();
|
|
862
|
+
this._blurOverlay = null;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
_emit(event) {
|
|
866
|
+
this._listeners.get(event)?.forEach((fn) => fn());
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
// src/security/request-signing.service.ts
|
|
871
|
+
async function _hmac(secret, message) {
|
|
872
|
+
const enc = new TextEncoder();
|
|
873
|
+
const key = await crypto.subtle.importKey(
|
|
874
|
+
"raw",
|
|
875
|
+
enc.encode(secret),
|
|
876
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
877
|
+
false,
|
|
878
|
+
["sign"]
|
|
879
|
+
);
|
|
880
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(message));
|
|
881
|
+
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
882
|
+
}
|
|
883
|
+
function _uuid() {
|
|
884
|
+
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
885
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
886
|
+
const r = Math.random() * 16 | 0;
|
|
887
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
var JoopRequestSigningService = class {
|
|
891
|
+
_secret;
|
|
892
|
+
_maxAgeMs;
|
|
893
|
+
constructor(config) {
|
|
894
|
+
this._secret = config.secret;
|
|
895
|
+
this._maxAgeMs = config.maxAgeMs ?? 3e4;
|
|
896
|
+
}
|
|
897
|
+
/** Sign a payload. Returns the signed envelope to send in the request body. */
|
|
898
|
+
async sign(data) {
|
|
899
|
+
const nonce = _uuid();
|
|
900
|
+
const timestamp = Date.now();
|
|
901
|
+
const message = `${nonce}.${timestamp}.${JSON.stringify(data)}`;
|
|
902
|
+
const signature = await _hmac(this._secret, message);
|
|
903
|
+
return { data, nonce, timestamp, signature };
|
|
904
|
+
}
|
|
905
|
+
/** Verify a signed payload (for testing/debugging — production verification runs server-side). */
|
|
906
|
+
async verify(payload) {
|
|
907
|
+
const age = Date.now() - payload.timestamp;
|
|
908
|
+
if (age > this._maxAgeMs) return { valid: false, reason: `Request expired (${age}ms old)` };
|
|
909
|
+
if (age < -5e3) return { valid: false, reason: "Timestamp in the future" };
|
|
910
|
+
const message = `${payload.nonce}.${payload.timestamp}.${JSON.stringify(payload.data)}`;
|
|
911
|
+
const expected = await _hmac(this._secret, message);
|
|
912
|
+
if (expected !== payload.signature) return { valid: false, reason: "Signature mismatch" };
|
|
913
|
+
return { valid: true };
|
|
914
|
+
}
|
|
915
|
+
/** Add the signed envelope as the request body, returning updated fetch init. */
|
|
916
|
+
async signRequest(data, init = {}) {
|
|
917
|
+
const signed = await this.sign(data);
|
|
918
|
+
return {
|
|
919
|
+
...init,
|
|
920
|
+
method: init.method ?? "POST",
|
|
921
|
+
headers: { "Content-Type": "application/json", ...init.headers ?? {} },
|
|
922
|
+
body: JSON.stringify(signed)
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
// src/security/secure-clipboard.ts
|
|
928
|
+
var DEFAULT_SENSITIVE_PATTERNS = [
|
|
929
|
+
/\b\d{13,19}\b/,
|
|
930
|
+
// card numbers
|
|
931
|
+
/\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b/,
|
|
932
|
+
// IBAN
|
|
933
|
+
/\b\d{3}-\d{2}-\d{4}\b/,
|
|
934
|
+
// SSN
|
|
935
|
+
/^\d{6,8}$/,
|
|
936
|
+
// OTP / PIN
|
|
937
|
+
/\b\d{4}\s\d{4}\s\d{4}\s\d{4}\b/
|
|
938
|
+
// formatted card
|
|
939
|
+
];
|
|
940
|
+
var JoopSecureClipboard = class {
|
|
941
|
+
_config;
|
|
942
|
+
_current = null;
|
|
943
|
+
_timer = null;
|
|
944
|
+
_event$ = new JoopSubject();
|
|
945
|
+
_visibilityHandler = null;
|
|
946
|
+
constructor(config = {}) {
|
|
947
|
+
this._config = {
|
|
948
|
+
defaultTtlMs: config.defaultTtlMs ?? 3e4,
|
|
949
|
+
sensitivePatterns: config.sensitivePatterns ?? DEFAULT_SENSITIVE_PATTERNS,
|
|
950
|
+
autoClearOnHide: config.autoClearOnHide ?? true
|
|
951
|
+
};
|
|
952
|
+
if (this._config.autoClearOnHide && typeof document !== "undefined") {
|
|
953
|
+
this._visibilityHandler = () => {
|
|
954
|
+
if (document.visibilityState === "hidden") this.clear("expired");
|
|
955
|
+
};
|
|
956
|
+
document.addEventListener("visibilitychange", this._visibilityHandler);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
// ── Write ─────────────────────────────────────────────────────────────────
|
|
960
|
+
async copy(text, options = {}) {
|
|
961
|
+
const ttl = options.ttlMs ?? this._config.defaultTtlMs;
|
|
962
|
+
const now = Date.now();
|
|
963
|
+
const entry = {
|
|
964
|
+
text,
|
|
965
|
+
copiedAt: now,
|
|
966
|
+
expiresAt: now + ttl,
|
|
967
|
+
label: options.label,
|
|
968
|
+
isSensitive: this._detectSensitive(text)
|
|
969
|
+
};
|
|
970
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
971
|
+
await navigator.clipboard.writeText(text);
|
|
972
|
+
}
|
|
973
|
+
this._cancelTimer();
|
|
974
|
+
this._current = entry;
|
|
975
|
+
this._event$.next({ type: "copied", entry: { ...entry } });
|
|
976
|
+
this._timer = setTimeout(() => this.clear("expired"), ttl);
|
|
977
|
+
}
|
|
978
|
+
// ── Read ──────────────────────────────────────────────────────────────────
|
|
979
|
+
async read() {
|
|
980
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.readText) {
|
|
981
|
+
try {
|
|
982
|
+
return await navigator.clipboard.readText();
|
|
983
|
+
} catch {
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return this._current?.text ?? null;
|
|
987
|
+
}
|
|
988
|
+
// ── Clear ─────────────────────────────────────────────────────────────────
|
|
989
|
+
async clear(reason = "manual") {
|
|
990
|
+
this._cancelTimer();
|
|
991
|
+
const entry = this._current;
|
|
992
|
+
this._current = null;
|
|
993
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
994
|
+
try {
|
|
995
|
+
await navigator.clipboard.writeText("");
|
|
996
|
+
} catch {
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
this._event$.next({ type: reason === "expired" ? "expired" : "cleared", entry: entry ?? void 0 });
|
|
1000
|
+
}
|
|
1001
|
+
// ── Introspection ─────────────────────────────────────────────────────────
|
|
1002
|
+
getCurrent() {
|
|
1003
|
+
if (!this._current) return null;
|
|
1004
|
+
if (Date.now() > this._current.expiresAt) {
|
|
1005
|
+
this.clear("expired");
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
return { ...this._current };
|
|
1009
|
+
}
|
|
1010
|
+
isExpired() {
|
|
1011
|
+
return !this._current || Date.now() > this._current.expiresAt;
|
|
1012
|
+
}
|
|
1013
|
+
getRemainingMs() {
|
|
1014
|
+
if (!this._current) return 0;
|
|
1015
|
+
return Math.max(0, this._current.expiresAt - Date.now());
|
|
1016
|
+
}
|
|
1017
|
+
isSensitive(text) {
|
|
1018
|
+
return this._detectSensitive(text);
|
|
1019
|
+
}
|
|
1020
|
+
// ── Observable ────────────────────────────────────────────────────────────
|
|
1021
|
+
events$() {
|
|
1022
|
+
return this._event$.asObservable();
|
|
1023
|
+
}
|
|
1024
|
+
// ── Cleanup ───────────────────────────────────────────────────────────────
|
|
1025
|
+
destroy() {
|
|
1026
|
+
this._cancelTimer();
|
|
1027
|
+
if (this._visibilityHandler && typeof document !== "undefined") {
|
|
1028
|
+
document.removeEventListener("visibilitychange", this._visibilityHandler);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
// ── Internals ─────────────────────────────────────────────────────────────
|
|
1032
|
+
_detectSensitive(text) {
|
|
1033
|
+
return this._config.sensitivePatterns.some((p) => p.test(text.trim()));
|
|
1034
|
+
}
|
|
1035
|
+
_cancelTimer() {
|
|
1036
|
+
if (this._timer !== null) {
|
|
1037
|
+
clearTimeout(this._timer);
|
|
1038
|
+
this._timer = null;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
// src/security/anti-tamper.service.ts
|
|
1044
|
+
var SEVERITY = {
|
|
1045
|
+
"devtools": "medium",
|
|
1046
|
+
"dom-injection": "high",
|
|
1047
|
+
"prototype-pollution": "critical",
|
|
1048
|
+
"console-override": "medium",
|
|
1049
|
+
"function-override": "high"
|
|
1050
|
+
};
|
|
1051
|
+
var SEVERITY_ORDER = ["low", "medium", "high", "critical"];
|
|
1052
|
+
var JoopAntiTamperService = class {
|
|
1053
|
+
_monitor = null;
|
|
1054
|
+
_tamper$ = new JoopSubject();
|
|
1055
|
+
_alertHandlers = [];
|
|
1056
|
+
_lastReport = null;
|
|
1057
|
+
_nativeFetch = typeof fetch !== "undefined" ? fetch : null;
|
|
1058
|
+
_nativeConsoleLog = typeof console !== "undefined" ? console.log : null;
|
|
1059
|
+
// ── Checks ────────────────────────────────────────────────────────────────
|
|
1060
|
+
checkDevTools() {
|
|
1061
|
+
let detected = false;
|
|
1062
|
+
let detail;
|
|
1063
|
+
if (typeof window !== "undefined") {
|
|
1064
|
+
const threshold = 160;
|
|
1065
|
+
if (window.outerWidth - window.innerWidth > threshold || window.outerHeight - window.innerHeight > threshold) {
|
|
1066
|
+
detected = true;
|
|
1067
|
+
detail = "Window size delta suggests DevTools panel is open";
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return { type: "devtools", detected, severity: SEVERITY["devtools"], detail };
|
|
1071
|
+
}
|
|
1072
|
+
checkDomInjection() {
|
|
1073
|
+
let detected = false;
|
|
1074
|
+
let detail;
|
|
1075
|
+
if (typeof document !== "undefined") {
|
|
1076
|
+
const scripts = document.querySelectorAll("script[src]");
|
|
1077
|
+
const suspicious = Array.from(scripts).filter((s) => {
|
|
1078
|
+
const src = s.src;
|
|
1079
|
+
return src && !src.startsWith(location.origin) && !src.startsWith("chrome-extension://");
|
|
1080
|
+
});
|
|
1081
|
+
if (suspicious.length > 0) {
|
|
1082
|
+
detected = true;
|
|
1083
|
+
detail = `${suspicious.length} external script(s) found: ${suspicious.map((s) => s.src.split("/")[2]).join(", ")}`;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return { type: "dom-injection", detected, severity: SEVERITY["dom-injection"], detail };
|
|
1087
|
+
}
|
|
1088
|
+
checkPrototypePollution() {
|
|
1089
|
+
const probes = ["__proto__", "constructor", "toString", "valueOf", "hasOwnProperty"];
|
|
1090
|
+
const polluted = [];
|
|
1091
|
+
for (const key of probes) {
|
|
1092
|
+
const obj = {};
|
|
1093
|
+
if (typeof obj[key] !== (key === "__proto__" ? "object" : "function")) {
|
|
1094
|
+
polluted.push(key);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
for (const key in {}) {
|
|
1098
|
+
polluted.push(key);
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
const detected = polluted.length > 0;
|
|
1102
|
+
return { type: "prototype-pollution", detected, severity: SEVERITY["prototype-pollution"], detail: detected ? `Polluted keys: ${polluted.join(", ")}` : void 0 };
|
|
1103
|
+
}
|
|
1104
|
+
checkConsoleOverride() {
|
|
1105
|
+
let detected = false;
|
|
1106
|
+
let detail;
|
|
1107
|
+
if (typeof console !== "undefined" && this._nativeConsoleLog) {
|
|
1108
|
+
const isNative = Function.prototype.toString.call(console.log).includes("[native code]");
|
|
1109
|
+
if (!isNative && console.log !== this._nativeConsoleLog) {
|
|
1110
|
+
detected = true;
|
|
1111
|
+
detail = "console.log has been overridden";
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return { type: "console-override", detected, severity: SEVERITY["console-override"], detail };
|
|
1115
|
+
}
|
|
1116
|
+
checkFunctionOverride() {
|
|
1117
|
+
let detected = false;
|
|
1118
|
+
let detail;
|
|
1119
|
+
const overridden = [];
|
|
1120
|
+
if (typeof fetch !== "undefined" && this._nativeFetch && fetch !== this._nativeFetch) {
|
|
1121
|
+
overridden.push("fetch");
|
|
1122
|
+
}
|
|
1123
|
+
if (typeof XMLHttpRequest !== "undefined") {
|
|
1124
|
+
const isNative = Function.prototype.toString.call(XMLHttpRequest.prototype.open).includes("[native code]");
|
|
1125
|
+
if (!isNative) overridden.push("XMLHttpRequest.open");
|
|
1126
|
+
}
|
|
1127
|
+
if (overridden.length > 0) {
|
|
1128
|
+
detected = true;
|
|
1129
|
+
detail = `Overridden: ${overridden.join(", ")}`;
|
|
1130
|
+
}
|
|
1131
|
+
return { type: "function-override", detected, severity: SEVERITY["function-override"], detail };
|
|
1132
|
+
}
|
|
1133
|
+
// ── Integrity scan ────────────────────────────────────────────────────────
|
|
1134
|
+
checkIntegrity() {
|
|
1135
|
+
const checks = [
|
|
1136
|
+
this.checkDevTools(),
|
|
1137
|
+
this.checkDomInjection(),
|
|
1138
|
+
this.checkPrototypePollution(),
|
|
1139
|
+
this.checkConsoleOverride(),
|
|
1140
|
+
this.checkFunctionOverride()
|
|
1141
|
+
];
|
|
1142
|
+
const threats = checks.filter((c) => c.detected);
|
|
1143
|
+
const severities = threats.map((c) => SEVERITY_ORDER.indexOf(c.severity));
|
|
1144
|
+
const highestIdx = severities.length > 0 ? Math.max(...severities) : -1;
|
|
1145
|
+
const report = {
|
|
1146
|
+
timestamp: Date.now(),
|
|
1147
|
+
checks,
|
|
1148
|
+
threatCount: threats.length,
|
|
1149
|
+
highestSeverity: highestIdx >= 0 ? SEVERITY_ORDER[highestIdx] : null
|
|
1150
|
+
};
|
|
1151
|
+
this._lastReport = report;
|
|
1152
|
+
if (report.threatCount > 0) {
|
|
1153
|
+
this._alertHandlers.forEach((h) => h(report));
|
|
1154
|
+
this._tamper$.next(report);
|
|
1155
|
+
}
|
|
1156
|
+
return report;
|
|
1157
|
+
}
|
|
1158
|
+
isTampered() {
|
|
1159
|
+
return (this._lastReport?.threatCount ?? 0) > 0;
|
|
1160
|
+
}
|
|
1161
|
+
getLastReport() {
|
|
1162
|
+
return this._lastReport;
|
|
1163
|
+
}
|
|
1164
|
+
// ── Monitoring ────────────────────────────────────────────────────────────
|
|
1165
|
+
startMonitoring(intervalMs = 1e4) {
|
|
1166
|
+
if (this._monitor !== null) return;
|
|
1167
|
+
this.checkIntegrity();
|
|
1168
|
+
this._monitor = setInterval(() => this.checkIntegrity(), intervalMs);
|
|
1169
|
+
}
|
|
1170
|
+
stopMonitoring() {
|
|
1171
|
+
if (this._monitor !== null) {
|
|
1172
|
+
clearInterval(this._monitor);
|
|
1173
|
+
this._monitor = null;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
isMonitoring() {
|
|
1177
|
+
return this._monitor !== null;
|
|
1178
|
+
}
|
|
1179
|
+
// ── Events ────────────────────────────────────────────────────────────────
|
|
1180
|
+
onTamperDetected(handler) {
|
|
1181
|
+
this._alertHandlers.push(handler);
|
|
1182
|
+
return () => {
|
|
1183
|
+
this._alertHandlers = this._alertHandlers.filter((h) => h !== handler);
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
tamper$() {
|
|
1187
|
+
return this._tamper$.asObservable();
|
|
1188
|
+
}
|
|
1189
|
+
destroy() {
|
|
1190
|
+
this.stopMonitoring();
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
// src/security/cert-pinning.service.ts
|
|
1195
|
+
var JoopCertPinningService = class {
|
|
1196
|
+
_pins = /* @__PURE__ */ new Map();
|
|
1197
|
+
_violations = [];
|
|
1198
|
+
_violation$ = new JoopSubject();
|
|
1199
|
+
_violationHandlers = [];
|
|
1200
|
+
// ── Pin management ────────────────────────────────────────────────────────
|
|
1201
|
+
addPin(pin) {
|
|
1202
|
+
const hostname = this._normalizeHostname(pin.hostname);
|
|
1203
|
+
this._pins.set(hostname, { ...pin, hostname, addedAt: Date.now(), fingerprints: pin.fingerprints.map((f) => f.toLowerCase().replace(/:/g, "")) });
|
|
1204
|
+
}
|
|
1205
|
+
removePin(hostname) {
|
|
1206
|
+
this._pins.delete(this._normalizeHostname(hostname));
|
|
1207
|
+
}
|
|
1208
|
+
getPin(hostname) {
|
|
1209
|
+
return this._pins.get(this._normalizeHostname(hostname));
|
|
1210
|
+
}
|
|
1211
|
+
getAllPins() {
|
|
1212
|
+
return Array.from(this._pins.values());
|
|
1213
|
+
}
|
|
1214
|
+
// ── Validation ────────────────────────────────────────────────────────────
|
|
1215
|
+
validate(url, fingerprint) {
|
|
1216
|
+
const hostname = this._hostnameFromUrl(url);
|
|
1217
|
+
const pin = this._findPin(hostname);
|
|
1218
|
+
if (!pin) return { valid: true, pinned: false };
|
|
1219
|
+
if (pin.expiresAt && Date.now() > pin.expiresAt) {
|
|
1220
|
+
return { valid: true, pinned: false };
|
|
1221
|
+
}
|
|
1222
|
+
const normalized = fingerprint.toLowerCase().replace(/:/g, "");
|
|
1223
|
+
const matched = pin.fingerprints.includes(normalized);
|
|
1224
|
+
if (!matched) {
|
|
1225
|
+
const violation = {
|
|
1226
|
+
hostname,
|
|
1227
|
+
receivedFingerprint: normalized,
|
|
1228
|
+
expectedFingerprints: pin.fingerprints,
|
|
1229
|
+
timestamp: Date.now(),
|
|
1230
|
+
url
|
|
1231
|
+
};
|
|
1232
|
+
this._violations.push(violation);
|
|
1233
|
+
this._violationHandlers.forEach((h) => h(violation));
|
|
1234
|
+
this._violation$.next(violation);
|
|
1235
|
+
return { valid: false, pinned: true, violation, pin };
|
|
1236
|
+
}
|
|
1237
|
+
return { valid: true, pinned: true, pin };
|
|
1238
|
+
}
|
|
1239
|
+
validateResponse(url, response) {
|
|
1240
|
+
const fingerprint = response.headers.get("x-cert-fingerprint") ?? "";
|
|
1241
|
+
if (!fingerprint) return { valid: true, pinned: false };
|
|
1242
|
+
return this.validate(url, fingerprint);
|
|
1243
|
+
}
|
|
1244
|
+
// ── Expiry ────────────────────────────────────────────────────────────────
|
|
1245
|
+
getExpiring(withinMs = 7 * 24 * 36e5) {
|
|
1246
|
+
const now = Date.now();
|
|
1247
|
+
return this.getAllPins().filter((p) => p.expiresAt && p.expiresAt - now <= withinMs && p.expiresAt > now);
|
|
1248
|
+
}
|
|
1249
|
+
isExpired(hostname) {
|
|
1250
|
+
const pin = this.getPin(hostname);
|
|
1251
|
+
return !!(pin?.expiresAt && Date.now() > pin.expiresAt);
|
|
1252
|
+
}
|
|
1253
|
+
// ── Violations ────────────────────────────────────────────────────────────
|
|
1254
|
+
getViolations() {
|
|
1255
|
+
return [...this._violations];
|
|
1256
|
+
}
|
|
1257
|
+
clearViolations() {
|
|
1258
|
+
this._violations = [];
|
|
1259
|
+
}
|
|
1260
|
+
onViolation(handler) {
|
|
1261
|
+
this._violationHandlers.push(handler);
|
|
1262
|
+
return () => {
|
|
1263
|
+
this._violationHandlers = this._violationHandlers.filter((h) => h !== handler);
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
violations$() {
|
|
1267
|
+
return this._violation$.asObservable();
|
|
1268
|
+
}
|
|
1269
|
+
// ── Internals ─────────────────────────────────────────────────────────────
|
|
1270
|
+
_normalizeHostname(hostname) {
|
|
1271
|
+
return hostname.toLowerCase().replace(/^https?:\/\//, "").split("/")[0].split("?")[0];
|
|
1272
|
+
}
|
|
1273
|
+
_hostnameFromUrl(url) {
|
|
1274
|
+
try {
|
|
1275
|
+
return new URL(url).hostname.toLowerCase();
|
|
1276
|
+
} catch {
|
|
1277
|
+
return this._normalizeHostname(url);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
_findPin(hostname) {
|
|
1281
|
+
return this._pins.get(hostname) ?? this._pins.get(`*.${hostname.split(".").slice(1).join(".")}`);
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
exports.JoopAntiTamperService = JoopAntiTamperService;
|
|
1286
|
+
exports.JoopBehavioralBiometrics = JoopBehavioralBiometrics;
|
|
1287
|
+
exports.JoopCertPinningService = JoopCertPinningService;
|
|
1288
|
+
exports.JoopDifferentialPrivacy = JoopDifferentialPrivacy;
|
|
1289
|
+
exports.JoopPIIScanner = JoopPIIScanner;
|
|
1290
|
+
exports.JoopRequestSigningService = JoopRequestSigningService;
|
|
1291
|
+
exports.JoopRiskEngine = JoopRiskEngine;
|
|
1292
|
+
exports.JoopScreenSecurityService = JoopScreenSecurityService;
|
|
1293
|
+
exports.JoopSecretSharing = JoopSecretSharing;
|
|
1294
|
+
exports.JoopSecureClipboard = JoopSecureClipboard;
|
|
1295
|
+
exports.JoopSecureStorage = JoopSecureStorage;
|
|
1296
|
+
//# sourceMappingURL=index.js.map
|
|
1297
|
+
//# sourceMappingURL=index.js.map
|