@noy-db/on-threat 0.1.0-pre.3

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vLannaAi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @noy-db/on-threat
2
+
3
+ [![npm](https://img.shields.io/npm/v/%40noy-db/on-threat.svg)](https://www.npmjs.com/package/@noy-db/on-threat)
4
+
5
+ > Threat-response primitives for noy-db
6
+
7
+ Part of [**`@noy-db/hub`**](https://www.npmjs.com/package/@noy-db/hub) — the zero-knowledge, offline-first, encrypted document store.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @noy-db/hub @noy-db/on-threat
13
+ ```
14
+
15
+ ## What it is
16
+
17
+ Threat-response primitives for noy-db — multi-attempt lockout, duress-passphrase data destruction, duress-passphrase honeypot decoy. Pure stateful helpers; the caller coordinates keyring + audit-ledger integration. Part of the @noy-db/on-* authentication family.
18
+
19
+ ## Status
20
+
21
+ **Pre-release** (`0.1.0-pre.1`). API may change before `1.0`.
22
+
23
+ ## Documentation
24
+
25
+ See the [main repository](https://github.com/vLannaAi/noy-db#readme) for setup, examples, and the full subsystem catalog.
26
+
27
+ - Source — [`packages/on-threat`](https://github.com/vLannaAi/noy-db/tree/main/packages/on-threat)
28
+ - Issues — [github.com/vLannaAi/noy-db/issues](https://github.com/vLannaAi/noy-db/issues)
29
+ - Spec — [`SPEC.md`](https://github.com/vLannaAi/noy-db/blob/main/SPEC.md)
30
+
31
+ ## License
32
+
33
+ [MIT](./LICENSE) © vLannaAi
package/dist/index.cjs ADDED
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ checkDuress: () => checkDuress,
24
+ checkHoneypot: () => checkHoneypot,
25
+ enrollDuress: () => enrollDuress,
26
+ enrollHoneypot: () => enrollHoneypot,
27
+ initialLockoutState: () => initialLockoutState,
28
+ isLocked: () => isLocked,
29
+ recordFailure: () => recordFailure,
30
+ recordSuccess: () => recordSuccess
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+ function initialLockoutState() {
34
+ return {
35
+ failures: 0,
36
+ windowStart: null,
37
+ lockedUntil: null,
38
+ strikes: 0,
39
+ wiped: false
40
+ };
41
+ }
42
+ function recordFailure(state, config = {}) {
43
+ const threshold = config.threshold ?? 5;
44
+ const windowMs = config.windowMs ?? 15 * 60 * 1e3;
45
+ const cooldownMs = config.cooldownMs ?? 5 * 60 * 1e3;
46
+ const maxStrikes = config.maxStrikes ?? 3;
47
+ const now = /* @__PURE__ */ new Date();
48
+ const nowMs = now.getTime();
49
+ if (state.lockedUntil) {
50
+ const unlockMs = new Date(state.lockedUntil).getTime();
51
+ if (nowMs < unlockMs) {
52
+ return { locked: true, unlockAt: state.lockedUntil };
53
+ }
54
+ state.lockedUntil = null;
55
+ state.windowStart = null;
56
+ state.failures = 0;
57
+ }
58
+ if (!state.windowStart) {
59
+ state.windowStart = now.toISOString();
60
+ } else {
61
+ const windowStartMs = new Date(state.windowStart).getTime();
62
+ if (nowMs - windowStartMs > windowMs) {
63
+ state.windowStart = now.toISOString();
64
+ state.failures = 0;
65
+ }
66
+ }
67
+ state.failures += 1;
68
+ if (state.failures >= threshold) {
69
+ state.strikes += 1;
70
+ if (state.strikes >= maxStrikes) {
71
+ state.wiped = true;
72
+ return { locked: true, wipe: true };
73
+ }
74
+ const unlockAt = new Date(nowMs + cooldownMs).toISOString();
75
+ state.lockedUntil = unlockAt;
76
+ return { locked: true, unlockAt };
77
+ }
78
+ return { locked: false, remainingAttempts: threshold - state.failures };
79
+ }
80
+ function recordSuccess(state) {
81
+ state.failures = 0;
82
+ state.windowStart = null;
83
+ state.lockedUntil = null;
84
+ }
85
+ function isLocked(state, now = /* @__PURE__ */ new Date()) {
86
+ if (state.wiped) return true;
87
+ if (!state.lockedUntil) return false;
88
+ return now.getTime() < new Date(state.lockedUntil).getTime();
89
+ }
90
+ async function enrollDuress(passphrase) {
91
+ return hashWithFreshSalt(passphrase);
92
+ }
93
+ async function checkDuress(input, digest, salt) {
94
+ const computed = await hashWithSalt(input, salt);
95
+ return constantTimeEqual(computed, digest);
96
+ }
97
+ var enrollHoneypot = enrollDuress;
98
+ var checkHoneypot = checkDuress;
99
+ async function hashWithFreshSalt(passphrase) {
100
+ const saltBytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
101
+ const salt = toHex(saltBytes);
102
+ const digest = await hashWithSalt(passphrase, salt);
103
+ return { digest, salt };
104
+ }
105
+ async function hashWithSalt(passphrase, saltHex) {
106
+ const enc = new TextEncoder();
107
+ const passBytes = enc.encode(passphrase);
108
+ const saltBytes = fromHex(saltHex);
109
+ const keyMaterial = await globalThis.crypto.subtle.importKey(
110
+ "raw",
111
+ passBytes,
112
+ "PBKDF2",
113
+ false,
114
+ ["deriveBits"]
115
+ );
116
+ const bits = await globalThis.crypto.subtle.deriveBits(
117
+ { name: "PBKDF2", salt: saltBytes, iterations: 2e5, hash: "SHA-256" },
118
+ keyMaterial,
119
+ 256
120
+ );
121
+ return toHex(new Uint8Array(bits));
122
+ }
123
+ function constantTimeEqual(a, b) {
124
+ if (a.length !== b.length) return false;
125
+ let diff = 0;
126
+ for (let i = 0; i < a.length; i++) {
127
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
128
+ }
129
+ return diff === 0;
130
+ }
131
+ function toHex(bytes) {
132
+ let out = "";
133
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
134
+ return out;
135
+ }
136
+ function fromHex(hex) {
137
+ const out = new Uint8Array(hex.length / 2);
138
+ for (let i = 0; i < out.length; i++) {
139
+ out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
140
+ }
141
+ return out;
142
+ }
143
+ // Annotate the CommonJS export names for ESM import in node:
144
+ 0 && (module.exports = {
145
+ checkDuress,
146
+ checkHoneypot,
147
+ enrollDuress,
148
+ enrollHoneypot,
149
+ initialLockoutState,
150
+ isLocked,
151
+ recordFailure,
152
+ recordSuccess
153
+ });
154
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/on-threat** — threat-response primitives for noy-db.\n *\n * Three independent, opt-in mechanisms (all of which are pure logic —\n * the caller coordinates persistence and keyring actions):\n *\n * 1. **{@link LockoutPolicy}** — N wrong passphrases within\n * a window trigger lockout, cooldown, or wipe. Pure state\n * machine; caller persists the `LockoutState` between attempts.\n *\n * 2. **{@link checkDuress}** — compare an entered\n * passphrase against a stored `duressDigest` to trigger\n * data-destruct mode. The action itself (DEK purge + keyring\n * delete) is delegated to a caller-supplied `onDuress` handler;\n * this package only detects the match.\n *\n * 3. **{@link checkHoneypot}** — alternate duress\n * passphrase that surfaces a decoy vault instead of wiping.\n * Same detection pattern as `checkDuress`, but the caller's\n * handler routes to the pre-seeded honeypot vault.\n *\n * None of the three mechanisms require server cooperation — everything\n * runs against local state. The combination gives a plausible-deniability\n * model: lockout protects against brute force; duress protects against\n * coercion; honeypot protects against inspection by showing something\n * that survives scrutiny long enough to get to safety.\n *\n * @packageDocumentation\n */\n\n// ─── Lockout policy ────────────────────────────────────────────────\n\nexport interface LockoutConfig {\n /** Failed attempts within the window before lockout activates. Default 5. */\n readonly threshold?: number\n /** Window in ms during which attempts accumulate. Default 15 minutes. */\n readonly windowMs?: number\n /** Cooldown in ms after the threshold is hit. Default 5 minutes. */\n readonly cooldownMs?: number\n /**\n * Strikes before terminal action. After this many lockout rounds the\n * state enters `'wipe'`, signalling the caller to destroy the vault.\n * Default 3.\n */\n readonly maxStrikes?: number\n}\n\n/**\n * Persistent lockout state. Caller stores this next to the keyring and\n * passes it through every `recordAttempt` / `isLocked` call.\n */\nexport interface LockoutState {\n /** Count of failed attempts in the current window. */\n failures: number\n /** ISO timestamp of the first failure in the current window. */\n windowStart: string | null\n /** ISO timestamp when the current lockout ends (null when not locked). */\n lockedUntil: string | null\n /** How many rounds of lockout have fired so far. */\n strikes: number\n /** Once the policy decides wipe, this latches to true until explicitly reset. */\n wiped: boolean\n}\n\n/** Seed a fresh lockout state for a new keyring. */\nexport function initialLockoutState(): LockoutState {\n return {\n failures: 0,\n windowStart: null,\n lockedUntil: null,\n strikes: 0,\n wiped: false,\n }\n}\n\nexport interface AttemptOutcome {\n /** Is the keyring currently locked after recording this failure? */\n readonly locked: boolean\n /** When does the lock expire? */\n readonly unlockAt?: string\n /** Did this failure trip the terminal wipe? Caller must destroy the vault. */\n readonly wipe?: boolean\n /** Remaining failures until the next lockout trip. */\n readonly remainingAttempts?: number\n}\n\n/** Record a failed unlock attempt and update `state` in place. */\nexport function recordFailure(state: LockoutState, config: LockoutConfig = {}): AttemptOutcome {\n const threshold = config.threshold ?? 5\n const windowMs = config.windowMs ?? 15 * 60 * 1000\n const cooldownMs = config.cooldownMs ?? 5 * 60 * 1000\n const maxStrikes = config.maxStrikes ?? 3\n\n const now = new Date()\n const nowMs = now.getTime()\n\n // Still inside an active lockout — pass-through.\n if (state.lockedUntil) {\n const unlockMs = new Date(state.lockedUntil).getTime()\n if (nowMs < unlockMs) {\n return { locked: true, unlockAt: state.lockedUntil }\n }\n // Lockout expired but failures not reset — treat as reset of window.\n state.lockedUntil = null\n state.windowStart = null\n state.failures = 0\n }\n\n // Start or advance the window.\n if (!state.windowStart) {\n state.windowStart = now.toISOString()\n } else {\n const windowStartMs = new Date(state.windowStart).getTime()\n if (nowMs - windowStartMs > windowMs) {\n state.windowStart = now.toISOString()\n state.failures = 0\n }\n }\n state.failures += 1\n\n if (state.failures >= threshold) {\n state.strikes += 1\n if (state.strikes >= maxStrikes) {\n state.wiped = true\n return { locked: true, wipe: true }\n }\n const unlockAt = new Date(nowMs + cooldownMs).toISOString()\n state.lockedUntil = unlockAt\n return { locked: true, unlockAt }\n }\n return { locked: false, remainingAttempts: threshold - state.failures }\n}\n\n/** Note a successful unlock. Resets window + failure count; `strikes` + `wiped` latch. */\nexport function recordSuccess(state: LockoutState): void {\n state.failures = 0\n state.windowStart = null\n state.lockedUntil = null\n // strikes + wiped deliberately persist — a successful unlock doesn't\n // erase the history that the keyring was under attack.\n}\n\n/** Check whether the keyring is currently locked without recording a failure. */\nexport function isLocked(state: LockoutState, now: Date = new Date()): boolean {\n if (state.wiped) return true\n if (!state.lockedUntil) return false\n return now.getTime() < new Date(state.lockedUntil).getTime()\n}\n\n// ─── Duress passphrase (data destruct) ────────────────────────────\n\n/**\n * Enroll a duress passphrase. Returns a `{ digest, salt }` pair to\n * persist alongside the keyring. On every unlock attempt the caller\n * runs `checkDuress(input, digest, salt)` BEFORE the normal PBKDF2\n * unlock — a match means the user entered the duress phrase, and the\n * caller should invoke the wipe handler.\n *\n * **Why hash-compare rather than wrap-attempt?** The duress passphrase\n * is intentionally a distinct secret from the real unlock passphrase —\n * wrapping a key against both would require storing a decoy DEK, which\n * defeats the \"destroy on match\" semantics. Hashing with a dedicated\n * salt lets us detect the duress passphrase without revealing anything\n * about the real one.\n */\nexport async function enrollDuress(passphrase: string): Promise<{ digest: string; salt: string }> {\n return hashWithFreshSalt(passphrase)\n}\n\n/** Returns true when `input` matches the enrolled duress passphrase. */\nexport async function checkDuress(input: string, digest: string, salt: string): Promise<boolean> {\n const computed = await hashWithSalt(input, salt)\n return constantTimeEqual(computed, digest)\n}\n\n// ─── Duress passphrase (honeypot) ─────────────────────────────────\n\n/**\n * Same enroll shape as `enrollDuress` — the caller keeps a separate\n * `{ digest, salt }` pair for the honeypot passphrase and routes\n * matches to a pre-seeded decoy vault instead of a wipe action.\n *\n * In practice a consumer configures either the destruct path OR the\n * honeypot path for a given keyring; shipping both primitives as a\n * single package keeps the security surface consistent and cuts\n * package proliferation.\n */\nexport const enrollHoneypot = enrollDuress\nexport const checkHoneypot = checkDuress\n\n// ─── internals ─────────────────────────────────────────────────────────\n\nasync function hashWithFreshSalt(passphrase: string): Promise<{ digest: string; salt: string }> {\n const saltBytes = globalThis.crypto.getRandomValues(new Uint8Array(16))\n const salt = toHex(saltBytes)\n const digest = await hashWithSalt(passphrase, salt)\n return { digest, salt }\n}\n\nasync function hashWithSalt(passphrase: string, saltHex: string): Promise<string> {\n const enc = new TextEncoder()\n const passBytes = enc.encode(passphrase)\n const saltBytes = fromHex(saltHex)\n // PBKDF2-SHA256 with 200k iterations — same ballpark as noy-db's 600k\n // for KEK derivation but this hash is just for passphrase comparison,\n // not key material. 200k keeps the UI snappy on low-end devices.\n const keyMaterial = await globalThis.crypto.subtle.importKey(\n 'raw',\n passBytes as BufferSource,\n 'PBKDF2',\n false,\n ['deriveBits'],\n )\n const bits = await globalThis.crypto.subtle.deriveBits(\n { name: 'PBKDF2', salt: saltBytes as BufferSource, iterations: 200_000, hash: 'SHA-256' },\n keyMaterial,\n 256,\n )\n return toHex(new Uint8Array(bits))\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i++) {\n diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n }\n return diff === 0\n}\n\nfunction toHex(bytes: Uint8Array): string {\n let out = ''\n for (const b of bytes) out += b.toString(16).padStart(2, '0')\n return out\n}\n\nfunction fromHex(hex: string): Uint8Array {\n const out = new Uint8Array(hex.length / 2)\n for (let i = 0; i < out.length; i++) {\n out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16)\n }\n return out\n}\n\n// The `LockoutPolicy` type alias kept for docstring cross-reference.\nexport type LockoutPolicy = LockoutConfig\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiEO,SAAS,sBAAoC;AAClD,SAAO;AAAA,IACL,UAAU;AAAA,IACV,aAAa;AAAA,IACb,aAAa;AAAA,IACb,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AACF;AAcO,SAAS,cAAc,OAAqB,SAAwB,CAAC,GAAmB;AAC7F,QAAM,YAAY,OAAO,aAAa;AACtC,QAAM,WAAW,OAAO,YAAY,KAAK,KAAK;AAC9C,QAAM,aAAa,OAAO,cAAc,IAAI,KAAK;AACjD,QAAM,aAAa,OAAO,cAAc;AAExC,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,QAAQ,IAAI,QAAQ;AAG1B,MAAI,MAAM,aAAa;AACrB,UAAM,WAAW,IAAI,KAAK,MAAM,WAAW,EAAE,QAAQ;AACrD,QAAI,QAAQ,UAAU;AACpB,aAAO,EAAE,QAAQ,MAAM,UAAU,MAAM,YAAY;AAAA,IACrD;AAEA,UAAM,cAAc;AACpB,UAAM,cAAc;AACpB,UAAM,WAAW;AAAA,EACnB;AAGA,MAAI,CAAC,MAAM,aAAa;AACtB,UAAM,cAAc,IAAI,YAAY;AAAA,EACtC,OAAO;AACL,UAAM,gBAAgB,IAAI,KAAK,MAAM,WAAW,EAAE,QAAQ;AAC1D,QAAI,QAAQ,gBAAgB,UAAU;AACpC,YAAM,cAAc,IAAI,YAAY;AACpC,YAAM,WAAW;AAAA,IACnB;AAAA,EACF;AACA,QAAM,YAAY;AAElB,MAAI,MAAM,YAAY,WAAW;AAC/B,UAAM,WAAW;AACjB,QAAI,MAAM,WAAW,YAAY;AAC/B,YAAM,QAAQ;AACd,aAAO,EAAE,QAAQ,MAAM,MAAM,KAAK;AAAA,IACpC;AACA,UAAM,WAAW,IAAI,KAAK,QAAQ,UAAU,EAAE,YAAY;AAC1D,UAAM,cAAc;AACpB,WAAO,EAAE,QAAQ,MAAM,SAAS;AAAA,EAClC;AACA,SAAO,EAAE,QAAQ,OAAO,mBAAmB,YAAY,MAAM,SAAS;AACxE;AAGO,SAAS,cAAc,OAA2B;AACvD,QAAM,WAAW;AACjB,QAAM,cAAc;AACpB,QAAM,cAAc;AAGtB;AAGO,SAAS,SAAS,OAAqB,MAAY,oBAAI,KAAK,GAAY;AAC7E,MAAI,MAAM,MAAO,QAAO;AACxB,MAAI,CAAC,MAAM,YAAa,QAAO;AAC/B,SAAO,IAAI,QAAQ,IAAI,IAAI,KAAK,MAAM,WAAW,EAAE,QAAQ;AAC7D;AAkBA,eAAsB,aAAa,YAA+D;AAChG,SAAO,kBAAkB,UAAU;AACrC;AAGA,eAAsB,YAAY,OAAe,QAAgB,MAAgC;AAC/F,QAAM,WAAW,MAAM,aAAa,OAAO,IAAI;AAC/C,SAAO,kBAAkB,UAAU,MAAM;AAC3C;AAcO,IAAM,iBAAiB;AACvB,IAAM,gBAAgB;AAI7B,eAAe,kBAAkB,YAA+D;AAC9F,QAAM,YAAY,WAAW,OAAO,gBAAgB,IAAI,WAAW,EAAE,CAAC;AACtE,QAAM,OAAO,MAAM,SAAS;AAC5B,QAAM,SAAS,MAAM,aAAa,YAAY,IAAI;AAClD,SAAO,EAAE,QAAQ,KAAK;AACxB;AAEA,eAAe,aAAa,YAAoB,SAAkC;AAChF,QAAM,MAAM,IAAI,YAAY;AAC5B,QAAM,YAAY,IAAI,OAAO,UAAU;AACvC,QAAM,YAAY,QAAQ,OAAO;AAIjC,QAAM,cAAc,MAAM,WAAW,OAAO,OAAO;AAAA,IACjD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,CAAC,YAAY;AAAA,EACf;AACA,QAAM,OAAO,MAAM,WAAW,OAAO,OAAO;AAAA,IAC1C,EAAE,MAAM,UAAU,MAAM,WAA2B,YAAY,KAAS,MAAM,UAAU;AAAA,IACxF;AAAA,IACA;AAAA,EACF;AACA,SAAO,MAAM,IAAI,WAAW,IAAI,CAAC;AACnC;AAEA,SAAS,kBAAkB,GAAW,GAAoB;AACxD,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,YAAQ,EAAE,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC;AAAA,EAC1C;AACA,SAAO,SAAS;AAClB;AAEA,SAAS,MAAM,OAA2B;AACxC,MAAI,MAAM;AACV,aAAW,KAAK,MAAO,QAAO,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAC5D,SAAO;AACT;AAEA,SAAS,QAAQ,KAAyB;AACxC,QAAM,MAAM,IAAI,WAAW,IAAI,SAAS,CAAC;AACzC,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,QAAI,CAAC,IAAI,OAAO,SAAS,IAAI,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE;AAAA,EAC1D;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * **@noy-db/on-threat** — threat-response primitives for noy-db.
3
+ *
4
+ * Three independent, opt-in mechanisms (all of which are pure logic —
5
+ * the caller coordinates persistence and keyring actions):
6
+ *
7
+ * 1. **{@link LockoutPolicy}** — N wrong passphrases within
8
+ * a window trigger lockout, cooldown, or wipe. Pure state
9
+ * machine; caller persists the `LockoutState` between attempts.
10
+ *
11
+ * 2. **{@link checkDuress}** — compare an entered
12
+ * passphrase against a stored `duressDigest` to trigger
13
+ * data-destruct mode. The action itself (DEK purge + keyring
14
+ * delete) is delegated to a caller-supplied `onDuress` handler;
15
+ * this package only detects the match.
16
+ *
17
+ * 3. **{@link checkHoneypot}** — alternate duress
18
+ * passphrase that surfaces a decoy vault instead of wiping.
19
+ * Same detection pattern as `checkDuress`, but the caller's
20
+ * handler routes to the pre-seeded honeypot vault.
21
+ *
22
+ * None of the three mechanisms require server cooperation — everything
23
+ * runs against local state. The combination gives a plausible-deniability
24
+ * model: lockout protects against brute force; duress protects against
25
+ * coercion; honeypot protects against inspection by showing something
26
+ * that survives scrutiny long enough to get to safety.
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+ interface LockoutConfig {
31
+ /** Failed attempts within the window before lockout activates. Default 5. */
32
+ readonly threshold?: number;
33
+ /** Window in ms during which attempts accumulate. Default 15 minutes. */
34
+ readonly windowMs?: number;
35
+ /** Cooldown in ms after the threshold is hit. Default 5 minutes. */
36
+ readonly cooldownMs?: number;
37
+ /**
38
+ * Strikes before terminal action. After this many lockout rounds the
39
+ * state enters `'wipe'`, signalling the caller to destroy the vault.
40
+ * Default 3.
41
+ */
42
+ readonly maxStrikes?: number;
43
+ }
44
+ /**
45
+ * Persistent lockout state. Caller stores this next to the keyring and
46
+ * passes it through every `recordAttempt` / `isLocked` call.
47
+ */
48
+ interface LockoutState {
49
+ /** Count of failed attempts in the current window. */
50
+ failures: number;
51
+ /** ISO timestamp of the first failure in the current window. */
52
+ windowStart: string | null;
53
+ /** ISO timestamp when the current lockout ends (null when not locked). */
54
+ lockedUntil: string | null;
55
+ /** How many rounds of lockout have fired so far. */
56
+ strikes: number;
57
+ /** Once the policy decides wipe, this latches to true until explicitly reset. */
58
+ wiped: boolean;
59
+ }
60
+ /** Seed a fresh lockout state for a new keyring. */
61
+ declare function initialLockoutState(): LockoutState;
62
+ interface AttemptOutcome {
63
+ /** Is the keyring currently locked after recording this failure? */
64
+ readonly locked: boolean;
65
+ /** When does the lock expire? */
66
+ readonly unlockAt?: string;
67
+ /** Did this failure trip the terminal wipe? Caller must destroy the vault. */
68
+ readonly wipe?: boolean;
69
+ /** Remaining failures until the next lockout trip. */
70
+ readonly remainingAttempts?: number;
71
+ }
72
+ /** Record a failed unlock attempt and update `state` in place. */
73
+ declare function recordFailure(state: LockoutState, config?: LockoutConfig): AttemptOutcome;
74
+ /** Note a successful unlock. Resets window + failure count; `strikes` + `wiped` latch. */
75
+ declare function recordSuccess(state: LockoutState): void;
76
+ /** Check whether the keyring is currently locked without recording a failure. */
77
+ declare function isLocked(state: LockoutState, now?: Date): boolean;
78
+ /**
79
+ * Enroll a duress passphrase. Returns a `{ digest, salt }` pair to
80
+ * persist alongside the keyring. On every unlock attempt the caller
81
+ * runs `checkDuress(input, digest, salt)` BEFORE the normal PBKDF2
82
+ * unlock — a match means the user entered the duress phrase, and the
83
+ * caller should invoke the wipe handler.
84
+ *
85
+ * **Why hash-compare rather than wrap-attempt?** The duress passphrase
86
+ * is intentionally a distinct secret from the real unlock passphrase —
87
+ * wrapping a key against both would require storing a decoy DEK, which
88
+ * defeats the "destroy on match" semantics. Hashing with a dedicated
89
+ * salt lets us detect the duress passphrase without revealing anything
90
+ * about the real one.
91
+ */
92
+ declare function enrollDuress(passphrase: string): Promise<{
93
+ digest: string;
94
+ salt: string;
95
+ }>;
96
+ /** Returns true when `input` matches the enrolled duress passphrase. */
97
+ declare function checkDuress(input: string, digest: string, salt: string): Promise<boolean>;
98
+ /**
99
+ * Same enroll shape as `enrollDuress` — the caller keeps a separate
100
+ * `{ digest, salt }` pair for the honeypot passphrase and routes
101
+ * matches to a pre-seeded decoy vault instead of a wipe action.
102
+ *
103
+ * In practice a consumer configures either the destruct path OR the
104
+ * honeypot path for a given keyring; shipping both primitives as a
105
+ * single package keeps the security surface consistent and cuts
106
+ * package proliferation.
107
+ */
108
+ declare const enrollHoneypot: typeof enrollDuress;
109
+ declare const checkHoneypot: typeof checkDuress;
110
+ type LockoutPolicy = LockoutConfig;
111
+
112
+ export { type AttemptOutcome, type LockoutConfig, type LockoutPolicy, type LockoutState, checkDuress, checkHoneypot, enrollDuress, enrollHoneypot, initialLockoutState, isLocked, recordFailure, recordSuccess };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * **@noy-db/on-threat** — threat-response primitives for noy-db.
3
+ *
4
+ * Three independent, opt-in mechanisms (all of which are pure logic —
5
+ * the caller coordinates persistence and keyring actions):
6
+ *
7
+ * 1. **{@link LockoutPolicy}** — N wrong passphrases within
8
+ * a window trigger lockout, cooldown, or wipe. Pure state
9
+ * machine; caller persists the `LockoutState` between attempts.
10
+ *
11
+ * 2. **{@link checkDuress}** — compare an entered
12
+ * passphrase against a stored `duressDigest` to trigger
13
+ * data-destruct mode. The action itself (DEK purge + keyring
14
+ * delete) is delegated to a caller-supplied `onDuress` handler;
15
+ * this package only detects the match.
16
+ *
17
+ * 3. **{@link checkHoneypot}** — alternate duress
18
+ * passphrase that surfaces a decoy vault instead of wiping.
19
+ * Same detection pattern as `checkDuress`, but the caller's
20
+ * handler routes to the pre-seeded honeypot vault.
21
+ *
22
+ * None of the three mechanisms require server cooperation — everything
23
+ * runs against local state. The combination gives a plausible-deniability
24
+ * model: lockout protects against brute force; duress protects against
25
+ * coercion; honeypot protects against inspection by showing something
26
+ * that survives scrutiny long enough to get to safety.
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+ interface LockoutConfig {
31
+ /** Failed attempts within the window before lockout activates. Default 5. */
32
+ readonly threshold?: number;
33
+ /** Window in ms during which attempts accumulate. Default 15 minutes. */
34
+ readonly windowMs?: number;
35
+ /** Cooldown in ms after the threshold is hit. Default 5 minutes. */
36
+ readonly cooldownMs?: number;
37
+ /**
38
+ * Strikes before terminal action. After this many lockout rounds the
39
+ * state enters `'wipe'`, signalling the caller to destroy the vault.
40
+ * Default 3.
41
+ */
42
+ readonly maxStrikes?: number;
43
+ }
44
+ /**
45
+ * Persistent lockout state. Caller stores this next to the keyring and
46
+ * passes it through every `recordAttempt` / `isLocked` call.
47
+ */
48
+ interface LockoutState {
49
+ /** Count of failed attempts in the current window. */
50
+ failures: number;
51
+ /** ISO timestamp of the first failure in the current window. */
52
+ windowStart: string | null;
53
+ /** ISO timestamp when the current lockout ends (null when not locked). */
54
+ lockedUntil: string | null;
55
+ /** How many rounds of lockout have fired so far. */
56
+ strikes: number;
57
+ /** Once the policy decides wipe, this latches to true until explicitly reset. */
58
+ wiped: boolean;
59
+ }
60
+ /** Seed a fresh lockout state for a new keyring. */
61
+ declare function initialLockoutState(): LockoutState;
62
+ interface AttemptOutcome {
63
+ /** Is the keyring currently locked after recording this failure? */
64
+ readonly locked: boolean;
65
+ /** When does the lock expire? */
66
+ readonly unlockAt?: string;
67
+ /** Did this failure trip the terminal wipe? Caller must destroy the vault. */
68
+ readonly wipe?: boolean;
69
+ /** Remaining failures until the next lockout trip. */
70
+ readonly remainingAttempts?: number;
71
+ }
72
+ /** Record a failed unlock attempt and update `state` in place. */
73
+ declare function recordFailure(state: LockoutState, config?: LockoutConfig): AttemptOutcome;
74
+ /** Note a successful unlock. Resets window + failure count; `strikes` + `wiped` latch. */
75
+ declare function recordSuccess(state: LockoutState): void;
76
+ /** Check whether the keyring is currently locked without recording a failure. */
77
+ declare function isLocked(state: LockoutState, now?: Date): boolean;
78
+ /**
79
+ * Enroll a duress passphrase. Returns a `{ digest, salt }` pair to
80
+ * persist alongside the keyring. On every unlock attempt the caller
81
+ * runs `checkDuress(input, digest, salt)` BEFORE the normal PBKDF2
82
+ * unlock — a match means the user entered the duress phrase, and the
83
+ * caller should invoke the wipe handler.
84
+ *
85
+ * **Why hash-compare rather than wrap-attempt?** The duress passphrase
86
+ * is intentionally a distinct secret from the real unlock passphrase —
87
+ * wrapping a key against both would require storing a decoy DEK, which
88
+ * defeats the "destroy on match" semantics. Hashing with a dedicated
89
+ * salt lets us detect the duress passphrase without revealing anything
90
+ * about the real one.
91
+ */
92
+ declare function enrollDuress(passphrase: string): Promise<{
93
+ digest: string;
94
+ salt: string;
95
+ }>;
96
+ /** Returns true when `input` matches the enrolled duress passphrase. */
97
+ declare function checkDuress(input: string, digest: string, salt: string): Promise<boolean>;
98
+ /**
99
+ * Same enroll shape as `enrollDuress` — the caller keeps a separate
100
+ * `{ digest, salt }` pair for the honeypot passphrase and routes
101
+ * matches to a pre-seeded decoy vault instead of a wipe action.
102
+ *
103
+ * In practice a consumer configures either the destruct path OR the
104
+ * honeypot path for a given keyring; shipping both primitives as a
105
+ * single package keeps the security surface consistent and cuts
106
+ * package proliferation.
107
+ */
108
+ declare const enrollHoneypot: typeof enrollDuress;
109
+ declare const checkHoneypot: typeof checkDuress;
110
+ type LockoutPolicy = LockoutConfig;
111
+
112
+ export { type AttemptOutcome, type LockoutConfig, type LockoutPolicy, type LockoutState, checkDuress, checkHoneypot, enrollDuress, enrollHoneypot, initialLockoutState, isLocked, recordFailure, recordSuccess };
package/dist/index.js ADDED
@@ -0,0 +1,122 @@
1
+ // src/index.ts
2
+ function initialLockoutState() {
3
+ return {
4
+ failures: 0,
5
+ windowStart: null,
6
+ lockedUntil: null,
7
+ strikes: 0,
8
+ wiped: false
9
+ };
10
+ }
11
+ function recordFailure(state, config = {}) {
12
+ const threshold = config.threshold ?? 5;
13
+ const windowMs = config.windowMs ?? 15 * 60 * 1e3;
14
+ const cooldownMs = config.cooldownMs ?? 5 * 60 * 1e3;
15
+ const maxStrikes = config.maxStrikes ?? 3;
16
+ const now = /* @__PURE__ */ new Date();
17
+ const nowMs = now.getTime();
18
+ if (state.lockedUntil) {
19
+ const unlockMs = new Date(state.lockedUntil).getTime();
20
+ if (nowMs < unlockMs) {
21
+ return { locked: true, unlockAt: state.lockedUntil };
22
+ }
23
+ state.lockedUntil = null;
24
+ state.windowStart = null;
25
+ state.failures = 0;
26
+ }
27
+ if (!state.windowStart) {
28
+ state.windowStart = now.toISOString();
29
+ } else {
30
+ const windowStartMs = new Date(state.windowStart).getTime();
31
+ if (nowMs - windowStartMs > windowMs) {
32
+ state.windowStart = now.toISOString();
33
+ state.failures = 0;
34
+ }
35
+ }
36
+ state.failures += 1;
37
+ if (state.failures >= threshold) {
38
+ state.strikes += 1;
39
+ if (state.strikes >= maxStrikes) {
40
+ state.wiped = true;
41
+ return { locked: true, wipe: true };
42
+ }
43
+ const unlockAt = new Date(nowMs + cooldownMs).toISOString();
44
+ state.lockedUntil = unlockAt;
45
+ return { locked: true, unlockAt };
46
+ }
47
+ return { locked: false, remainingAttempts: threshold - state.failures };
48
+ }
49
+ function recordSuccess(state) {
50
+ state.failures = 0;
51
+ state.windowStart = null;
52
+ state.lockedUntil = null;
53
+ }
54
+ function isLocked(state, now = /* @__PURE__ */ new Date()) {
55
+ if (state.wiped) return true;
56
+ if (!state.lockedUntil) return false;
57
+ return now.getTime() < new Date(state.lockedUntil).getTime();
58
+ }
59
+ async function enrollDuress(passphrase) {
60
+ return hashWithFreshSalt(passphrase);
61
+ }
62
+ async function checkDuress(input, digest, salt) {
63
+ const computed = await hashWithSalt(input, salt);
64
+ return constantTimeEqual(computed, digest);
65
+ }
66
+ var enrollHoneypot = enrollDuress;
67
+ var checkHoneypot = checkDuress;
68
+ async function hashWithFreshSalt(passphrase) {
69
+ const saltBytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
70
+ const salt = toHex(saltBytes);
71
+ const digest = await hashWithSalt(passphrase, salt);
72
+ return { digest, salt };
73
+ }
74
+ async function hashWithSalt(passphrase, saltHex) {
75
+ const enc = new TextEncoder();
76
+ const passBytes = enc.encode(passphrase);
77
+ const saltBytes = fromHex(saltHex);
78
+ const keyMaterial = await globalThis.crypto.subtle.importKey(
79
+ "raw",
80
+ passBytes,
81
+ "PBKDF2",
82
+ false,
83
+ ["deriveBits"]
84
+ );
85
+ const bits = await globalThis.crypto.subtle.deriveBits(
86
+ { name: "PBKDF2", salt: saltBytes, iterations: 2e5, hash: "SHA-256" },
87
+ keyMaterial,
88
+ 256
89
+ );
90
+ return toHex(new Uint8Array(bits));
91
+ }
92
+ function constantTimeEqual(a, b) {
93
+ if (a.length !== b.length) return false;
94
+ let diff = 0;
95
+ for (let i = 0; i < a.length; i++) {
96
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
97
+ }
98
+ return diff === 0;
99
+ }
100
+ function toHex(bytes) {
101
+ let out = "";
102
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
103
+ return out;
104
+ }
105
+ function fromHex(hex) {
106
+ const out = new Uint8Array(hex.length / 2);
107
+ for (let i = 0; i < out.length; i++) {
108
+ out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
109
+ }
110
+ return out;
111
+ }
112
+ export {
113
+ checkDuress,
114
+ checkHoneypot,
115
+ enrollDuress,
116
+ enrollHoneypot,
117
+ initialLockoutState,
118
+ isLocked,
119
+ recordFailure,
120
+ recordSuccess
121
+ };
122
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/on-threat** — threat-response primitives for noy-db.\n *\n * Three independent, opt-in mechanisms (all of which are pure logic —\n * the caller coordinates persistence and keyring actions):\n *\n * 1. **{@link LockoutPolicy}** — N wrong passphrases within\n * a window trigger lockout, cooldown, or wipe. Pure state\n * machine; caller persists the `LockoutState` between attempts.\n *\n * 2. **{@link checkDuress}** — compare an entered\n * passphrase against a stored `duressDigest` to trigger\n * data-destruct mode. The action itself (DEK purge + keyring\n * delete) is delegated to a caller-supplied `onDuress` handler;\n * this package only detects the match.\n *\n * 3. **{@link checkHoneypot}** — alternate duress\n * passphrase that surfaces a decoy vault instead of wiping.\n * Same detection pattern as `checkDuress`, but the caller's\n * handler routes to the pre-seeded honeypot vault.\n *\n * None of the three mechanisms require server cooperation — everything\n * runs against local state. The combination gives a plausible-deniability\n * model: lockout protects against brute force; duress protects against\n * coercion; honeypot protects against inspection by showing something\n * that survives scrutiny long enough to get to safety.\n *\n * @packageDocumentation\n */\n\n// ─── Lockout policy ────────────────────────────────────────────────\n\nexport interface LockoutConfig {\n /** Failed attempts within the window before lockout activates. Default 5. */\n readonly threshold?: number\n /** Window in ms during which attempts accumulate. Default 15 minutes. */\n readonly windowMs?: number\n /** Cooldown in ms after the threshold is hit. Default 5 minutes. */\n readonly cooldownMs?: number\n /**\n * Strikes before terminal action. After this many lockout rounds the\n * state enters `'wipe'`, signalling the caller to destroy the vault.\n * Default 3.\n */\n readonly maxStrikes?: number\n}\n\n/**\n * Persistent lockout state. Caller stores this next to the keyring and\n * passes it through every `recordAttempt` / `isLocked` call.\n */\nexport interface LockoutState {\n /** Count of failed attempts in the current window. */\n failures: number\n /** ISO timestamp of the first failure in the current window. */\n windowStart: string | null\n /** ISO timestamp when the current lockout ends (null when not locked). */\n lockedUntil: string | null\n /** How many rounds of lockout have fired so far. */\n strikes: number\n /** Once the policy decides wipe, this latches to true until explicitly reset. */\n wiped: boolean\n}\n\n/** Seed a fresh lockout state for a new keyring. */\nexport function initialLockoutState(): LockoutState {\n return {\n failures: 0,\n windowStart: null,\n lockedUntil: null,\n strikes: 0,\n wiped: false,\n }\n}\n\nexport interface AttemptOutcome {\n /** Is the keyring currently locked after recording this failure? */\n readonly locked: boolean\n /** When does the lock expire? */\n readonly unlockAt?: string\n /** Did this failure trip the terminal wipe? Caller must destroy the vault. */\n readonly wipe?: boolean\n /** Remaining failures until the next lockout trip. */\n readonly remainingAttempts?: number\n}\n\n/** Record a failed unlock attempt and update `state` in place. */\nexport function recordFailure(state: LockoutState, config: LockoutConfig = {}): AttemptOutcome {\n const threshold = config.threshold ?? 5\n const windowMs = config.windowMs ?? 15 * 60 * 1000\n const cooldownMs = config.cooldownMs ?? 5 * 60 * 1000\n const maxStrikes = config.maxStrikes ?? 3\n\n const now = new Date()\n const nowMs = now.getTime()\n\n // Still inside an active lockout — pass-through.\n if (state.lockedUntil) {\n const unlockMs = new Date(state.lockedUntil).getTime()\n if (nowMs < unlockMs) {\n return { locked: true, unlockAt: state.lockedUntil }\n }\n // Lockout expired but failures not reset — treat as reset of window.\n state.lockedUntil = null\n state.windowStart = null\n state.failures = 0\n }\n\n // Start or advance the window.\n if (!state.windowStart) {\n state.windowStart = now.toISOString()\n } else {\n const windowStartMs = new Date(state.windowStart).getTime()\n if (nowMs - windowStartMs > windowMs) {\n state.windowStart = now.toISOString()\n state.failures = 0\n }\n }\n state.failures += 1\n\n if (state.failures >= threshold) {\n state.strikes += 1\n if (state.strikes >= maxStrikes) {\n state.wiped = true\n return { locked: true, wipe: true }\n }\n const unlockAt = new Date(nowMs + cooldownMs).toISOString()\n state.lockedUntil = unlockAt\n return { locked: true, unlockAt }\n }\n return { locked: false, remainingAttempts: threshold - state.failures }\n}\n\n/** Note a successful unlock. Resets window + failure count; `strikes` + `wiped` latch. */\nexport function recordSuccess(state: LockoutState): void {\n state.failures = 0\n state.windowStart = null\n state.lockedUntil = null\n // strikes + wiped deliberately persist — a successful unlock doesn't\n // erase the history that the keyring was under attack.\n}\n\n/** Check whether the keyring is currently locked without recording a failure. */\nexport function isLocked(state: LockoutState, now: Date = new Date()): boolean {\n if (state.wiped) return true\n if (!state.lockedUntil) return false\n return now.getTime() < new Date(state.lockedUntil).getTime()\n}\n\n// ─── Duress passphrase (data destruct) ────────────────────────────\n\n/**\n * Enroll a duress passphrase. Returns a `{ digest, salt }` pair to\n * persist alongside the keyring. On every unlock attempt the caller\n * runs `checkDuress(input, digest, salt)` BEFORE the normal PBKDF2\n * unlock — a match means the user entered the duress phrase, and the\n * caller should invoke the wipe handler.\n *\n * **Why hash-compare rather than wrap-attempt?** The duress passphrase\n * is intentionally a distinct secret from the real unlock passphrase —\n * wrapping a key against both would require storing a decoy DEK, which\n * defeats the \"destroy on match\" semantics. Hashing with a dedicated\n * salt lets us detect the duress passphrase without revealing anything\n * about the real one.\n */\nexport async function enrollDuress(passphrase: string): Promise<{ digest: string; salt: string }> {\n return hashWithFreshSalt(passphrase)\n}\n\n/** Returns true when `input` matches the enrolled duress passphrase. */\nexport async function checkDuress(input: string, digest: string, salt: string): Promise<boolean> {\n const computed = await hashWithSalt(input, salt)\n return constantTimeEqual(computed, digest)\n}\n\n// ─── Duress passphrase (honeypot) ─────────────────────────────────\n\n/**\n * Same enroll shape as `enrollDuress` — the caller keeps a separate\n * `{ digest, salt }` pair for the honeypot passphrase and routes\n * matches to a pre-seeded decoy vault instead of a wipe action.\n *\n * In practice a consumer configures either the destruct path OR the\n * honeypot path for a given keyring; shipping both primitives as a\n * single package keeps the security surface consistent and cuts\n * package proliferation.\n */\nexport const enrollHoneypot = enrollDuress\nexport const checkHoneypot = checkDuress\n\n// ─── internals ─────────────────────────────────────────────────────────\n\nasync function hashWithFreshSalt(passphrase: string): Promise<{ digest: string; salt: string }> {\n const saltBytes = globalThis.crypto.getRandomValues(new Uint8Array(16))\n const salt = toHex(saltBytes)\n const digest = await hashWithSalt(passphrase, salt)\n return { digest, salt }\n}\n\nasync function hashWithSalt(passphrase: string, saltHex: string): Promise<string> {\n const enc = new TextEncoder()\n const passBytes = enc.encode(passphrase)\n const saltBytes = fromHex(saltHex)\n // PBKDF2-SHA256 with 200k iterations — same ballpark as noy-db's 600k\n // for KEK derivation but this hash is just for passphrase comparison,\n // not key material. 200k keeps the UI snappy on low-end devices.\n const keyMaterial = await globalThis.crypto.subtle.importKey(\n 'raw',\n passBytes as BufferSource,\n 'PBKDF2',\n false,\n ['deriveBits'],\n )\n const bits = await globalThis.crypto.subtle.deriveBits(\n { name: 'PBKDF2', salt: saltBytes as BufferSource, iterations: 200_000, hash: 'SHA-256' },\n keyMaterial,\n 256,\n )\n return toHex(new Uint8Array(bits))\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i++) {\n diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n }\n return diff === 0\n}\n\nfunction toHex(bytes: Uint8Array): string {\n let out = ''\n for (const b of bytes) out += b.toString(16).padStart(2, '0')\n return out\n}\n\nfunction fromHex(hex: string): Uint8Array {\n const out = new Uint8Array(hex.length / 2)\n for (let i = 0; i < out.length; i++) {\n out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16)\n }\n return out\n}\n\n// The `LockoutPolicy` type alias kept for docstring cross-reference.\nexport type LockoutPolicy = LockoutConfig\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@noy-db/on-threat",
3
+ "version": "0.1.0-pre.3",
4
+ "description": "Threat-response primitives for noy-db — multi-attempt lockout, duress-passphrase data destruction, duress-passphrase honeypot decoy. Pure stateful helpers; the caller coordinates keyring + audit-ledger integration. Part of the @noy-db/on-* authentication family.",
5
+ "license": "MIT",
6
+ "author": "vLannaAi <vicio@lanna.ai>",
7
+ "homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/on-threat#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/vLannaAi/noy-db.git",
11
+ "directory": "packages/on-threat"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/vLannaAi/noy-db/issues"
15
+ },
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "exports": {
19
+ ".": {
20
+ "import": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/index.d.cts",
26
+ "default": "./dist/index.cjs"
27
+ }
28
+ }
29
+ },
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ },
41
+ "peerDependencies": {
42
+ "@noy-db/hub": "0.1.0-pre.3"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^22.0.0",
46
+ "@noy-db/hub": "0.1.0-pre.3"
47
+ },
48
+ "keywords": [
49
+ "noy-db",
50
+ "on-threat",
51
+ "lockout",
52
+ "duress",
53
+ "honeypot",
54
+ "security"
55
+ ],
56
+ "publishConfig": {
57
+ "access": "public",
58
+ "tag": "latest"
59
+ },
60
+ "scripts": {
61
+ "build": "tsup",
62
+ "test": "vitest run",
63
+ "lint": "eslint src/",
64
+ "typecheck": "tsc --noEmit"
65
+ }
66
+ }