byok-vault 0.1.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/LICENSE +21 -0
- package/README.md +161 -0
- package/dist/circuit-breaker.d.ts +20 -0
- package/dist/circuit-breaker.d.ts.map +1 -0
- package/dist/circuit-breaker.js +58 -0
- package/dist/circuit-breaker.js.map +1 -0
- package/dist/crypto.d.ts +19 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +82 -0
- package/dist/crypto.js.map +1 -0
- package/dist/encoding.d.ts +5 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +26 -0
- package/dist/encoding.js.map +1 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +49 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/storage.d.ts +29 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +95 -0
- package/dist/storage.js.map +1 -0
- package/dist/vault.d.ts +43 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +198 -0
- package/dist/vault.js.map +1 -0
- package/docs/HUMANS.md +80 -0
- package/docs/LLMS.md +114 -0
- package/llms.txt +25 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ra
|
|
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,161 @@
|
|
|
1
|
+
# byok-vault
|
|
2
|
+
|
|
3
|
+
Browser-native BYOK vault for serverless/local-first AI apps.
|
|
4
|
+
|
|
5
|
+
## Security Reality Check (Read First)
|
|
6
|
+
|
|
7
|
+
- This project has **not** been formally audited.
|
|
8
|
+
- It protects against passive issues (plaintext keys in storage, accidental exposure, low-effort scraping).
|
|
9
|
+
- It does **not** protect against active in-origin script injection (XSS). If malicious JS executes on your origin, it can still intercept decrypted keys in-flight.
|
|
10
|
+
- `sessionStorage` caching is a UX optimization to reduce passphrase prompts, **not** a stronger security boundary.
|
|
11
|
+
|
|
12
|
+
If your threat model requires resistance to active injection attacks, use a server-side proxy.
|
|
13
|
+
|
|
14
|
+
## What It Provides
|
|
15
|
+
|
|
16
|
+
- AES-GCM encryption at rest in `localStorage`.
|
|
17
|
+
- PBKDF2 key derivation (default `200,000` iterations) with per-user random salt.
|
|
18
|
+
- Scoped key access via `withKey(async (key) => { ... })`.
|
|
19
|
+
- Optional token circuit breaker with:
|
|
20
|
+
- pre-flight soft check (`requestedTokens`)
|
|
21
|
+
- post-call hard accounting (`reportUsage(tokens)`)
|
|
22
|
+
- dev warning when `withKey` finishes without `reportUsage`.
|
|
23
|
+
- `nuke()` reset flow to clear encrypted key and session state.
|
|
24
|
+
|
|
25
|
+
## Why Use This
|
|
26
|
+
|
|
27
|
+
Most BYOK apps choose between two bad defaults:
|
|
28
|
+
|
|
29
|
+
- plaintext key entry in the browser (trust-killing UX), or
|
|
30
|
+
- rolling custom client-side crypto where implementation mistakes are common.
|
|
31
|
+
|
|
32
|
+
`byok-vault` is useful when you want browser-native key handling with opinionated defaults:
|
|
33
|
+
|
|
34
|
+
- encrypted-at-rest storage with per-key random salt and AES-GCM,
|
|
35
|
+
- scoped key access (`withKey`) instead of wide key plumbing through app code,
|
|
36
|
+
- built-in token budget circuit breaker (`requestedTokens` + `reportUsage`).
|
|
37
|
+
|
|
38
|
+
Use this if your threat model is client-side BYOK with passive exposure concerns.
|
|
39
|
+
Do not use this as an active-XSS defense; use a server-side proxy for that.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install byok-vault
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { BYOKVault } from "byok-vault";
|
|
51
|
+
|
|
52
|
+
const vault = new BYOKVault({
|
|
53
|
+
maxTokens: 30_000,
|
|
54
|
+
minPassphraseLength: 8
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await vault.setKey(userApiKey, userPassphrase);
|
|
58
|
+
await vault.unlock(userPassphrase);
|
|
59
|
+
|
|
60
|
+
await vault.withKey(
|
|
61
|
+
async (key) => {
|
|
62
|
+
const response = await fetch("https://api.example.com/llm", {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
Authorization: `Bearer ${key}`,
|
|
66
|
+
"Content-Type": "application/json"
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({ prompt: "hello" })
|
|
69
|
+
}).then((r) => r.json());
|
|
70
|
+
|
|
71
|
+
const used = response.usage.total_tokens;
|
|
72
|
+
vault.reportUsage(used); // hard usage accounting
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
requestedTokens: 1200 // optional soft pre-flight estimate
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Circuit Breaker Notes
|
|
81
|
+
|
|
82
|
+
- `requestedTokens` check is a soft guardrail based on your estimate.
|
|
83
|
+
- `reportUsage(tokens)` is the hard truth.
|
|
84
|
+
- When limit is exceeded, the **next** request is blocked with a hard error.
|
|
85
|
+
- In dev mode, vault warns if `withKey` returns successfully without `reportUsage`.
|
|
86
|
+
|
|
87
|
+
## Provider Usage Parsing Snippets
|
|
88
|
+
|
|
89
|
+
OpenAI-style:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const tokens = response.usage?.total_tokens ?? 0;
|
|
93
|
+
vault.reportUsage(tokens);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Anthropic-style:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const input = response.usage?.input_tokens ?? 0;
|
|
100
|
+
const output = response.usage?.output_tokens ?? 0;
|
|
101
|
+
vault.reportUsage(input + output);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## API
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
new BYOKVault(options?)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Options:
|
|
111
|
+
|
|
112
|
+
- `namespace?: string` storage key prefix (default `byok-vault`)
|
|
113
|
+
- `minPassphraseLength?: number` default `8`
|
|
114
|
+
- `pbkdf2Iterations?: number` default `200000`
|
|
115
|
+
- `maxTokens?: number` enables circuit breaker
|
|
116
|
+
- `devMode?: boolean` defaults to `NODE_ENV !== "production"` when available
|
|
117
|
+
- `localStorage?: Storage` / `sessionStorage?: Storage` for testing/custom storage
|
|
118
|
+
- `logger?: { warn(message: string): void }` custom warning sink
|
|
119
|
+
|
|
120
|
+
Methods:
|
|
121
|
+
|
|
122
|
+
- `setKey(apiKey, passphrase): Promise<void>`
|
|
123
|
+
- `unlock(passphrase): Promise<void>`
|
|
124
|
+
- `withKey(callback, { requestedTokens?, passphrase? }): Promise<T>`
|
|
125
|
+
- `reportUsage(tokens): void`
|
|
126
|
+
- `getUsage(): number`
|
|
127
|
+
- `getRemainingTokens(): number`
|
|
128
|
+
- `getMaxTokens(): number | null`
|
|
129
|
+
- `hasStoredKey(): boolean`
|
|
130
|
+
- `isLocked(): boolean`
|
|
131
|
+
- `getEncryptedBlob(): EncryptedKeyBlob | null`
|
|
132
|
+
- `lock(): void`
|
|
133
|
+
- `nuke(): void`
|
|
134
|
+
|
|
135
|
+
## Threat Model and Limitations
|
|
136
|
+
|
|
137
|
+
- JavaScript cannot force immediate memory zeroization of strings; decrypted keys can remain in heap memory until GC.
|
|
138
|
+
- Passphrase quality matters. A short PIN (for example 4 digits) is brute-forceable even with high PBKDF2 iteration counts.
|
|
139
|
+
- PBKDF2 iteration count has a hard floor at `200000`; lower values throw at construction time.
|
|
140
|
+
- This package intentionally has zero runtime dependencies, but still has normal dev dependencies for build/test tooling.
|
|
141
|
+
|
|
142
|
+
## Development
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm install
|
|
146
|
+
npm run typecheck
|
|
147
|
+
npm test
|
|
148
|
+
npm run build
|
|
149
|
+
npm run pack:check
|
|
150
|
+
npm run demo
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Sample Project (Gemini)
|
|
154
|
+
|
|
155
|
+
See `examples/local-first-byok-sample/README.md` for a separate sample app that uses this package with Gemini API calls.
|
|
156
|
+
|
|
157
|
+
## Human + LLM Docs
|
|
158
|
+
|
|
159
|
+
- Human integration guide: `docs/HUMANS.md`
|
|
160
|
+
- LLM reference: `docs/LLMS.md`
|
|
161
|
+
- LLM index file: `llms.txt`
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface CircuitBreakerOptions {
|
|
2
|
+
maxTokens: number;
|
|
3
|
+
storage: Storage;
|
|
4
|
+
storageKey: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class CircuitBreaker {
|
|
7
|
+
private usage;
|
|
8
|
+
private readonly maxTokens;
|
|
9
|
+
private readonly storage;
|
|
10
|
+
private readonly storageKey;
|
|
11
|
+
constructor(options: CircuitBreakerOptions);
|
|
12
|
+
assertCanProceed(requestedTokens?: number): void;
|
|
13
|
+
reportUsage(tokens: number): void;
|
|
14
|
+
getUsage(): number;
|
|
15
|
+
getMaxTokens(): number;
|
|
16
|
+
getRemainingTokens(): number;
|
|
17
|
+
reset(): void;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=circuit-breaker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.d.ts","sourceRoot":"","sources":["../src/circuit-breaker.ts"],"names":[],"mappings":"AAEA,UAAU,qBAAqB;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAUD,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAExB,OAAO,EAAE,qBAAqB;IAU1C,gBAAgB,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI;IAqBhD,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAQjC,QAAQ,IAAI,MAAM;IAIlB,YAAY,IAAI,MAAM;IAItB,kBAAkB,IAAI,MAAM;IAI5B,KAAK,IAAI,IAAI;CAId"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { CircuitBreakerLimitError, InvalidUsageReportError } from "./errors.js";
|
|
2
|
+
function parseUsage(raw) {
|
|
3
|
+
if (!raw) {
|
|
4
|
+
return 0;
|
|
5
|
+
}
|
|
6
|
+
const parsed = Number.parseInt(raw, 10);
|
|
7
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
8
|
+
}
|
|
9
|
+
export class CircuitBreaker {
|
|
10
|
+
usage;
|
|
11
|
+
maxTokens;
|
|
12
|
+
storage;
|
|
13
|
+
storageKey;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
if (!Number.isFinite(options.maxTokens) || options.maxTokens <= 0) {
|
|
16
|
+
throw new Error("maxTokens must be a finite number greater than zero.");
|
|
17
|
+
}
|
|
18
|
+
this.maxTokens = Math.floor(options.maxTokens);
|
|
19
|
+
this.storage = options.storage;
|
|
20
|
+
this.storageKey = options.storageKey;
|
|
21
|
+
this.usage = parseUsage(this.storage.getItem(this.storageKey));
|
|
22
|
+
}
|
|
23
|
+
assertCanProceed(requestedTokens) {
|
|
24
|
+
if (this.usage >= this.maxTokens) {
|
|
25
|
+
throw new CircuitBreakerLimitError(`Token limit reached (${this.usage}/${this.maxTokens}). Reset or nuke before sending another request.`);
|
|
26
|
+
}
|
|
27
|
+
if (requestedTokens === undefined) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!Number.isFinite(requestedTokens) || requestedTokens < 0) {
|
|
31
|
+
throw new InvalidUsageReportError();
|
|
32
|
+
}
|
|
33
|
+
if (this.usage + Math.floor(requestedTokens) > this.maxTokens) {
|
|
34
|
+
throw new CircuitBreakerLimitError(`Pre-flight estimate would exceed token limit (${this.usage} + ${Math.floor(requestedTokens)} > ${this.maxTokens}).`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
reportUsage(tokens) {
|
|
38
|
+
if (!Number.isFinite(tokens) || tokens < 0) {
|
|
39
|
+
throw new InvalidUsageReportError();
|
|
40
|
+
}
|
|
41
|
+
this.usage += Math.floor(tokens);
|
|
42
|
+
this.storage.setItem(this.storageKey, String(this.usage));
|
|
43
|
+
}
|
|
44
|
+
getUsage() {
|
|
45
|
+
return this.usage;
|
|
46
|
+
}
|
|
47
|
+
getMaxTokens() {
|
|
48
|
+
return this.maxTokens;
|
|
49
|
+
}
|
|
50
|
+
getRemainingTokens() {
|
|
51
|
+
return Math.max(this.maxTokens - this.usage, 0);
|
|
52
|
+
}
|
|
53
|
+
reset() {
|
|
54
|
+
this.usage = 0;
|
|
55
|
+
this.storage.removeItem(this.storageKey);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=circuit-breaker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.js","sourceRoot":"","sources":["../src/circuit-breaker.ts"],"names":[],"mappings":"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"}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const DEFAULT_PBKDF2_ITERATIONS = 200000;
|
|
2
|
+
export interface EncryptedKeyBlob {
|
|
3
|
+
version: 1;
|
|
4
|
+
iterations: number;
|
|
5
|
+
salt: string;
|
|
6
|
+
iv: string;
|
|
7
|
+
ciphertext: string;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
}
|
|
10
|
+
interface EncryptOptions {
|
|
11
|
+
iterations?: number;
|
|
12
|
+
saltBytes?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function deriveKeyBits(passphrase: string, salt: Uint8Array, iterations?: number): Promise<Uint8Array>;
|
|
15
|
+
export declare function decryptWithKeyBits(blob: EncryptedKeyBlob, keyBits: Uint8Array): Promise<string>;
|
|
16
|
+
export declare function encryptKey(key: string, passphrase: string, options?: EncryptOptions): Promise<EncryptedKeyBlob>;
|
|
17
|
+
export declare function decryptKey(blob: EncryptedKeyBlob, passphrase: string): Promise<string>;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,yBAAyB,SAAU,CAAC;AAKjD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,cAAc;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAoBD,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,UAAU,EAChB,UAAU,SAA4B,GACrC,OAAO,CAAC,UAAU,CAAC,CAoBrB;AA+BD,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,MAAM,CAAC,CAmBjB;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CAa3B;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,gBAAgB,EACtB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAIjB"}
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { base64ToBytes, bytesToBase64, bytesToUtf8, utf8ToBytes } from "./encoding.js";
|
|
2
|
+
import { WrongPassphraseError } from "./errors.js";
|
|
3
|
+
export const DEFAULT_PBKDF2_ITERATIONS = 200_000;
|
|
4
|
+
const DEFAULT_SALT_BYTES = 16;
|
|
5
|
+
const IV_BYTES = 12;
|
|
6
|
+
function assertWebCrypto() {
|
|
7
|
+
if (!globalThis.crypto?.subtle) {
|
|
8
|
+
throw new Error("Web Crypto API is not available in this environment.");
|
|
9
|
+
}
|
|
10
|
+
return globalThis.crypto;
|
|
11
|
+
}
|
|
12
|
+
function getRandomBytes(size) {
|
|
13
|
+
const cryptoProvider = assertWebCrypto();
|
|
14
|
+
const random = new Uint8Array(size);
|
|
15
|
+
cryptoProvider.getRandomValues(random);
|
|
16
|
+
return random;
|
|
17
|
+
}
|
|
18
|
+
function asBufferSource(bytes) {
|
|
19
|
+
return bytes;
|
|
20
|
+
}
|
|
21
|
+
export async function deriveKeyBits(passphrase, salt, iterations = DEFAULT_PBKDF2_ITERATIONS) {
|
|
22
|
+
const cryptoProvider = assertWebCrypto();
|
|
23
|
+
const passphraseKey = await cryptoProvider.subtle.importKey("raw", asBufferSource(utf8ToBytes(passphrase)), "PBKDF2", false, ["deriveBits"]);
|
|
24
|
+
const keyBits = await cryptoProvider.subtle.deriveBits({
|
|
25
|
+
name: "PBKDF2",
|
|
26
|
+
hash: "SHA-256",
|
|
27
|
+
salt: asBufferSource(salt),
|
|
28
|
+
iterations
|
|
29
|
+
}, passphraseKey, 256);
|
|
30
|
+
return new Uint8Array(keyBits);
|
|
31
|
+
}
|
|
32
|
+
async function importAesGcmKey(keyBits) {
|
|
33
|
+
const cryptoProvider = assertWebCrypto();
|
|
34
|
+
return cryptoProvider.subtle.importKey("raw", asBufferSource(keyBits), { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
35
|
+
}
|
|
36
|
+
async function encryptWithKeyBits(keyBits, plaintext) {
|
|
37
|
+
const cryptoProvider = assertWebCrypto();
|
|
38
|
+
const iv = getRandomBytes(IV_BYTES);
|
|
39
|
+
const key = await importAesGcmKey(keyBits);
|
|
40
|
+
const ciphertext = await cryptoProvider.subtle.encrypt({
|
|
41
|
+
name: "AES-GCM",
|
|
42
|
+
iv: asBufferSource(iv)
|
|
43
|
+
}, key, asBufferSource(utf8ToBytes(plaintext)));
|
|
44
|
+
return { iv, ciphertext: new Uint8Array(ciphertext) };
|
|
45
|
+
}
|
|
46
|
+
export async function decryptWithKeyBits(blob, keyBits) {
|
|
47
|
+
const cryptoProvider = assertWebCrypto();
|
|
48
|
+
const key = await importAesGcmKey(keyBits);
|
|
49
|
+
try {
|
|
50
|
+
const plaintext = await cryptoProvider.subtle.decrypt({
|
|
51
|
+
name: "AES-GCM",
|
|
52
|
+
iv: asBufferSource(base64ToBytes(blob.iv))
|
|
53
|
+
}, key, asBufferSource(base64ToBytes(blob.ciphertext)));
|
|
54
|
+
return bytesToUtf8(new Uint8Array(plaintext));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error instanceof Error && error.name === "OperationError") {
|
|
58
|
+
throw new WrongPassphraseError();
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function encryptKey(key, passphrase, options = {}) {
|
|
64
|
+
const salt = getRandomBytes(options.saltBytes ?? DEFAULT_SALT_BYTES);
|
|
65
|
+
const iterations = options.iterations ?? DEFAULT_PBKDF2_ITERATIONS;
|
|
66
|
+
const keyBits = await deriveKeyBits(passphrase, salt, iterations);
|
|
67
|
+
const encrypted = await encryptWithKeyBits(keyBits, key);
|
|
68
|
+
return {
|
|
69
|
+
version: 1,
|
|
70
|
+
iterations,
|
|
71
|
+
salt: bytesToBase64(salt),
|
|
72
|
+
iv: bytesToBase64(encrypted.iv),
|
|
73
|
+
ciphertext: bytesToBase64(encrypted.ciphertext),
|
|
74
|
+
createdAt: new Date().toISOString()
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export async function decryptKey(blob, passphrase) {
|
|
78
|
+
const salt = base64ToBytes(blob.salt);
|
|
79
|
+
const keyBits = await deriveKeyBits(passphrase, salt, blob.iterations);
|
|
80
|
+
return decryptWithKeyBits(blob, keyBits);
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function utf8ToBytes(value: string): Uint8Array;
|
|
2
|
+
export declare function bytesToUtf8(value: Uint8Array): string;
|
|
3
|
+
export declare function bytesToBase64(bytes: Uint8Array): string;
|
|
4
|
+
export declare function base64ToBytes(value: string): Uint8Array;
|
|
5
|
+
//# sourceMappingURL=encoding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoding.d.ts","sourceRoot":"","sources":["../src/encoding.ts"],"names":[],"mappings":"AAGA,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAErD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAErD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAQvD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAOvD"}
|
package/dist/encoding.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const encoder = new TextEncoder();
|
|
2
|
+
const decoder = new TextDecoder();
|
|
3
|
+
export function utf8ToBytes(value) {
|
|
4
|
+
return encoder.encode(value);
|
|
5
|
+
}
|
|
6
|
+
export function bytesToUtf8(value) {
|
|
7
|
+
return decoder.decode(value);
|
|
8
|
+
}
|
|
9
|
+
export function bytesToBase64(bytes) {
|
|
10
|
+
let binary = "";
|
|
11
|
+
const chunkSize = 0x8000;
|
|
12
|
+
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
13
|
+
const chunk = bytes.subarray(index, index + chunkSize);
|
|
14
|
+
binary += String.fromCharCode(...chunk);
|
|
15
|
+
}
|
|
16
|
+
return btoa(binary);
|
|
17
|
+
}
|
|
18
|
+
export function base64ToBytes(value) {
|
|
19
|
+
const binary = atob(value);
|
|
20
|
+
const output = new Uint8Array(binary.length);
|
|
21
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
22
|
+
output[index] = binary.charCodeAt(index);
|
|
23
|
+
}
|
|
24
|
+
return output;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=encoding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoding.js","sourceRoot":"","sources":["../src/encoding.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;AAClC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;AAElC,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAiB;IAC3C,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAiB;IAC7C,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,MAAM,SAAS,GAAG,MAAM,CAAC;IACzB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,SAAS,EAAE,CAAC;QAC7D,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC,CAAC;QACvD,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC7C,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACtD,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare class BYOKVaultError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
constructor(code: string, message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class PassphrasePolicyError extends BYOKVaultError {
|
|
6
|
+
constructor(minLength: number);
|
|
7
|
+
}
|
|
8
|
+
export declare class PBKDF2PolicyError extends BYOKVaultError {
|
|
9
|
+
constructor(minIterations: number);
|
|
10
|
+
}
|
|
11
|
+
export declare class KeyNotFoundError extends BYOKVaultError {
|
|
12
|
+
constructor();
|
|
13
|
+
}
|
|
14
|
+
export declare class VaultLockedError extends BYOKVaultError {
|
|
15
|
+
constructor();
|
|
16
|
+
}
|
|
17
|
+
export declare class WrongPassphraseError extends BYOKVaultError {
|
|
18
|
+
constructor();
|
|
19
|
+
}
|
|
20
|
+
export declare class InvalidUsageReportError extends BYOKVaultError {
|
|
21
|
+
constructor();
|
|
22
|
+
}
|
|
23
|
+
export declare class CircuitBreakerLimitError extends BYOKVaultError {
|
|
24
|
+
constructor(message: string);
|
|
25
|
+
}
|
|
26
|
+
export declare class CircuitBreakerDisabledError extends BYOKVaultError {
|
|
27
|
+
constructor();
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAK1C;AAED,qBAAa,qBAAsB,SAAQ,cAAc;gBAC3C,SAAS,EAAE,MAAM;CAM9B;AAED,qBAAa,iBAAkB,SAAQ,cAAc;gBACvC,aAAa,EAAE,MAAM;CAMlC;AAED,qBAAa,gBAAiB,SAAQ,cAAc;;CAInD;AAED,qBAAa,gBAAiB,SAAQ,cAAc;;CAOnD;AAED,qBAAa,oBAAqB,SAAQ,cAAc;;CAOvD;AAED,qBAAa,uBAAwB,SAAQ,cAAc;;CAO1D;AAED,qBAAa,wBAAyB,SAAQ,cAAc;gBAC9C,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,2BAA4B,SAAQ,cAAc;;CAO9D"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export class BYOKVaultError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = this.constructor.name;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class PassphrasePolicyError extends BYOKVaultError {
|
|
10
|
+
constructor(minLength) {
|
|
11
|
+
super("PASSPHRASE_POLICY", `Passphrase must be at least ${minLength} characters.`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class PBKDF2PolicyError extends BYOKVaultError {
|
|
15
|
+
constructor(minIterations) {
|
|
16
|
+
super("PBKDF2_POLICY", `pbkdf2Iterations must be a finite integer greater than or equal to ${minIterations}.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class KeyNotFoundError extends BYOKVaultError {
|
|
20
|
+
constructor() {
|
|
21
|
+
super("KEY_NOT_FOUND", "No encrypted API key is stored in the vault.");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class VaultLockedError extends BYOKVaultError {
|
|
25
|
+
constructor() {
|
|
26
|
+
super("VAULT_LOCKED", "Vault is locked. Call unlock(passphrase) or pass a passphrase to withKey.");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export class WrongPassphraseError extends BYOKVaultError {
|
|
30
|
+
constructor() {
|
|
31
|
+
super("WRONG_PASSPHRASE", "Could not decrypt key. The passphrase appears to be incorrect.");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class InvalidUsageReportError extends BYOKVaultError {
|
|
35
|
+
constructor() {
|
|
36
|
+
super("INVALID_USAGE_REPORT", "Token usage must be a finite number greater than or equal to zero.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class CircuitBreakerLimitError extends BYOKVaultError {
|
|
40
|
+
constructor(message) {
|
|
41
|
+
super("CIRCUIT_BREAKER_LIMIT", message);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export class CircuitBreakerDisabledError extends BYOKVaultError {
|
|
45
|
+
constructor() {
|
|
46
|
+
super("CIRCUIT_BREAKER_DISABLED", "Circuit breaker is disabled because maxTokens was not configured.");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,IAAI,CAAS;IAEtB,YAAY,IAAY,EAAE,OAAe;QACvC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;IACpC,CAAC;CACF;AAED,MAAM,OAAO,qBAAsB,SAAQ,cAAc;IACvD,YAAY,SAAiB;QAC3B,KAAK,CACH,mBAAmB,EACnB,+BAA+B,SAAS,cAAc,CACvD,CAAC;IACJ,CAAC;CACF;AAED,MAAM,OAAO,iBAAkB,SAAQ,cAAc;IACnD,YAAY,aAAqB;QAC/B,KAAK,CACH,eAAe,EACf,sEAAsE,aAAa,GAAG,CACvF,CAAC;IACJ,CAAC;CACF;AAED,MAAM,OAAO,gBAAiB,SAAQ,cAAc;IAClD;QACE,KAAK,CAAC,eAAe,EAAE,8CAA8C,CAAC,CAAC;IACzE,CAAC;CACF;AAED,MAAM,OAAO,gBAAiB,SAAQ,cAAc;IAClD;QACE,KAAK,CACH,cAAc,EACd,2EAA2E,CAC5E,CAAC;IACJ,CAAC;CACF;AAED,MAAM,OAAO,oBAAqB,SAAQ,cAAc;IACtD;QACE,KAAK,CACH,kBAAkB,EAClB,gEAAgE,CACjE,CAAC;IACJ,CAAC;CACF;AAED,MAAM,OAAO,uBAAwB,SAAQ,cAAc;IACzD;QACE,KAAK,CACH,sBAAsB,EACtB,oEAAoE,CACrE,CAAC;IACJ,CAAC;CACF;AAED,MAAM,OAAO,wBAAyB,SAAQ,cAAc;IAC1D,YAAY,OAAe;QACzB,KAAK,CAAC,uBAAuB,EAAE,OAAO,CAAC,CAAC;IAC1C,CAAC;CACF;AAED,MAAM,OAAO,2BAA4B,SAAQ,cAAc;IAC7D;QACE,KAAK,CACH,0BAA0B,EAC1B,mEAAmE,CACpE,CAAC;IACJ,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { DEFAULT_PBKDF2_ITERATIONS, decryptKey, decryptWithKeyBits, deriveKeyBits, encryptKey } from "./crypto.js";
|
|
2
|
+
export { CircuitBreaker } from "./circuit-breaker.js";
|
|
3
|
+
export { BYOKVault } from "./vault.js";
|
|
4
|
+
export { BYOKVaultError, CircuitBreakerDisabledError, CircuitBreakerLimitError, InvalidUsageReportError, KeyNotFoundError, PBKDF2PolicyError, PassphrasePolicyError, VaultLockedError, WrongPassphraseError } from "./errors.js";
|
|
5
|
+
export type { EncryptedKeyBlob } from "./crypto.js";
|
|
6
|
+
export type { BYOKVaultOptions, WithKeyOptions } from "./vault.js";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EACzB,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,UAAU,EACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EACL,cAAc,EACd,2BAA2B,EAC3B,wBAAwB,EACxB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { DEFAULT_PBKDF2_ITERATIONS, decryptKey, decryptWithKeyBits, deriveKeyBits, encryptKey } from "./crypto.js";
|
|
2
|
+
export { CircuitBreaker } from "./circuit-breaker.js";
|
|
3
|
+
export { BYOKVault } from "./vault.js";
|
|
4
|
+
export { BYOKVaultError, CircuitBreakerDisabledError, CircuitBreakerLimitError, InvalidUsageReportError, KeyNotFoundError, PBKDF2PolicyError, PassphrasePolicyError, VaultLockedError, WrongPassphraseError } from "./errors.js";
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EACzB,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,UAAU,EACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EACL,cAAc,EACd,2BAA2B,EAC3B,wBAAwB,EACxB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EncryptedKeyBlob } from "./crypto.js";
|
|
2
|
+
export interface StorageKeys {
|
|
3
|
+
encryptedKey: string;
|
|
4
|
+
sessionKey: string;
|
|
5
|
+
tokenUsage: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function getStorageKeys(namespace: string): StorageKeys;
|
|
8
|
+
export declare function resolveStorage(kind: "localStorage" | "sessionStorage", fallback?: Storage): Storage;
|
|
9
|
+
export declare class EncryptedKeyStorage {
|
|
10
|
+
private readonly storage;
|
|
11
|
+
private readonly key;
|
|
12
|
+
constructor(storage: Storage, key: string);
|
|
13
|
+
get(): EncryptedKeyBlob | null;
|
|
14
|
+
set(blob: EncryptedKeyBlob): void;
|
|
15
|
+
clear(): void;
|
|
16
|
+
}
|
|
17
|
+
export declare class SessionKeyCache {
|
|
18
|
+
private readonly storage;
|
|
19
|
+
private readonly key;
|
|
20
|
+
constructor(storage: Storage, key: string);
|
|
21
|
+
load(salt: string, iterations: number): Uint8Array | null;
|
|
22
|
+
save(payload: {
|
|
23
|
+
salt: string;
|
|
24
|
+
iterations: number;
|
|
25
|
+
keyBits: Uint8Array;
|
|
26
|
+
}): void;
|
|
27
|
+
clear(): void;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAQpD,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,CAM7D;AAiBD,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,gBAAgB,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CASnG;AAED,qBAAa,mBAAmB;IAClB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAAW,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAtC,OAAO,EAAE,OAAO,EAAmB,GAAG,EAAE,MAAM;IAE3E,GAAG,IAAI,gBAAgB,GAAG,IAAI;IAa9B,GAAG,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI;IAIjC,KAAK,IAAI,IAAI;CAGd;AAED,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,OAAO;IAAW,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAtC,OAAO,EAAE,OAAO,EAAmB,GAAG,EAAE,MAAM;IAE3E,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAoBzD,IAAI,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,UAAU,CAAA;KAAE,GAAG,IAAI;IAS9E,KAAK,IAAI,IAAI;CAGd"}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { base64ToBytes, bytesToBase64 } from "./encoding.js";
|
|
2
|
+
export function getStorageKeys(namespace) {
|
|
3
|
+
return {
|
|
4
|
+
encryptedKey: `${namespace}:encrypted-key`,
|
|
5
|
+
sessionKey: `${namespace}:derived-key`,
|
|
6
|
+
tokenUsage: `${namespace}:token-usage`
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function isEncryptedKeyBlob(input) {
|
|
10
|
+
if (!input || typeof input !== "object") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const blob = input;
|
|
14
|
+
return (blob.version === 1 &&
|
|
15
|
+
typeof blob.iterations === "number" &&
|
|
16
|
+
typeof blob.salt === "string" &&
|
|
17
|
+
typeof blob.iv === "string" &&
|
|
18
|
+
typeof blob.ciphertext === "string" &&
|
|
19
|
+
typeof blob.createdAt === "string");
|
|
20
|
+
}
|
|
21
|
+
export function resolveStorage(kind, fallback) {
|
|
22
|
+
if (fallback) {
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
const candidate = globalThis[kind];
|
|
26
|
+
if (!candidate) {
|
|
27
|
+
throw new Error(`${kind} is not available. Provide it through constructor options.`);
|
|
28
|
+
}
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
export class EncryptedKeyStorage {
|
|
32
|
+
storage;
|
|
33
|
+
key;
|
|
34
|
+
constructor(storage, key) {
|
|
35
|
+
this.storage = storage;
|
|
36
|
+
this.key = key;
|
|
37
|
+
}
|
|
38
|
+
get() {
|
|
39
|
+
const raw = this.storage.getItem(this.key);
|
|
40
|
+
if (!raw) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
return isEncryptedKeyBlob(parsed) ? parsed : null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
set(blob) {
|
|
52
|
+
this.storage.setItem(this.key, JSON.stringify(blob));
|
|
53
|
+
}
|
|
54
|
+
clear() {
|
|
55
|
+
this.storage.removeItem(this.key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export class SessionKeyCache {
|
|
59
|
+
storage;
|
|
60
|
+
key;
|
|
61
|
+
constructor(storage, key) {
|
|
62
|
+
this.storage = storage;
|
|
63
|
+
this.key = key;
|
|
64
|
+
}
|
|
65
|
+
load(salt, iterations) {
|
|
66
|
+
const raw = this.storage.getItem(this.key);
|
|
67
|
+
if (!raw) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(raw);
|
|
72
|
+
if (parsed.salt !== salt ||
|
|
73
|
+
parsed.iterations !== iterations ||
|
|
74
|
+
typeof parsed.keyBits !== "string") {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return base64ToBytes(parsed.keyBits);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
save(payload) {
|
|
84
|
+
const record = {
|
|
85
|
+
salt: payload.salt,
|
|
86
|
+
iterations: payload.iterations,
|
|
87
|
+
keyBits: bytesToBase64(payload.keyBits)
|
|
88
|
+
};
|
|
89
|
+
this.storage.setItem(this.key, JSON.stringify(record));
|
|
90
|
+
}
|
|
91
|
+
clear() {
|
|
92
|
+
this.storage.removeItem(this.key);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAe7D,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC9C,OAAO;QACL,YAAY,EAAE,GAAG,SAAS,gBAAgB;QAC1C,UAAU,EAAE,GAAG,SAAS,cAAc;QACtC,UAAU,EAAE,GAAG,SAAS,cAAc;KACvC,CAAC;AACJ,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAc;IACxC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACxC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,IAAI,GAAG,KAAgC,CAAC;IAC9C,OAAO,CACL,IAAI,CAAC,OAAO,KAAK,CAAC;QAClB,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ;QACnC,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ;QAC7B,OAAO,IAAI,CAAC,EAAE,KAAK,QAAQ;QAC3B,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ;QACnC,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,CACnC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAuC,EAAE,QAAkB;IACxF,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,MAAM,SAAS,GAAI,UAAsC,CAAC,IAAI,CAAC,CAAC;IAChE,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,4DAA4D,CAAC,CAAC;IACvF,CAAC;IACD,OAAO,SAAoB,CAAC;AAC9B,CAAC;AAED,MAAM,OAAO,mBAAmB;IACD;IAAmC;IAAhE,YAA6B,OAAgB,EAAmB,GAAW;QAA9C,YAAO,GAAP,OAAO,CAAS;QAAmB,QAAG,GAAH,GAAG,CAAQ;IAAG,CAAC;IAE/E,GAAG;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC/B,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,GAAG,CAAC,IAAsB;QACxB,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACpC,CAAC;CACF;AAED,MAAM,OAAO,eAAe;IACG;IAAmC;IAAhE,YAA6B,OAAgB,EAAmB,GAAW;QAA9C,YAAO,GAAP,OAAO,CAAS;QAAmB,QAAG,GAAH,GAAG,CAAQ;IAAG,CAAC;IAE/E,IAAI,CAAC,IAAY,EAAE,UAAkB;QACnC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAkB,CAAC;YAChD,IACE,MAAM,CAAC,IAAI,KAAK,IAAI;gBACpB,MAAM,CAAC,UAAU,KAAK,UAAU;gBAChC,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,EAClC,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC;YACD,OAAO,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACvC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAkE;QACrE,MAAM,MAAM,GAAkB;YAC5B,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC;SACxC,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACpC,CAAC;CACF"}
|
package/dist/vault.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type EncryptedKeyBlob } from "./crypto.js";
|
|
2
|
+
export interface WithKeyOptions {
|
|
3
|
+
requestedTokens?: number;
|
|
4
|
+
passphrase?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface BYOKVaultOptions {
|
|
7
|
+
namespace?: string;
|
|
8
|
+
minPassphraseLength?: number;
|
|
9
|
+
pbkdf2Iterations?: number;
|
|
10
|
+
maxTokens?: number;
|
|
11
|
+
devMode?: boolean;
|
|
12
|
+
localStorage?: Storage;
|
|
13
|
+
sessionStorage?: Storage;
|
|
14
|
+
logger?: Pick<Console, "warn">;
|
|
15
|
+
}
|
|
16
|
+
export declare class BYOKVault {
|
|
17
|
+
private readonly keyStorage;
|
|
18
|
+
private readonly sessionCache;
|
|
19
|
+
private readonly minPassphraseLength;
|
|
20
|
+
private readonly pbkdf2Iterations;
|
|
21
|
+
private readonly breaker?;
|
|
22
|
+
private readonly devMode;
|
|
23
|
+
private readonly logger;
|
|
24
|
+
private readonly scopes;
|
|
25
|
+
constructor(options?: BYOKVaultOptions);
|
|
26
|
+
setKey(apiKey: string, passphrase: string): Promise<void>;
|
|
27
|
+
unlock(passphrase: string): Promise<void>;
|
|
28
|
+
withKey<T>(callback: (decryptedKey: string) => Promise<T> | T, options?: WithKeyOptions): Promise<T>;
|
|
29
|
+
reportUsage(tokens: number): void;
|
|
30
|
+
getUsage(): number;
|
|
31
|
+
getRemainingTokens(): number;
|
|
32
|
+
getMaxTokens(): number | null;
|
|
33
|
+
hasStoredKey(): boolean;
|
|
34
|
+
isLocked(): boolean;
|
|
35
|
+
getEncryptedBlob(): EncryptedKeyBlob | null;
|
|
36
|
+
lock(): void;
|
|
37
|
+
nuke(): void;
|
|
38
|
+
private assertPassphrase;
|
|
39
|
+
private requireStoredBlob;
|
|
40
|
+
private resolveDecryptedKey;
|
|
41
|
+
private decryptOrThrowWrongPassphrase;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=vault.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault.d.ts","sourceRoot":"","sources":["../src/vault.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,gBAAgB,EACtB,MAAM,aAAa,CAAC;AAuBrB,MAAM,WAAW,cAAc;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;CAChC;AAUD,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAsB;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkB;IAC/C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;gBAE/B,OAAO,GAAE,gBAAqB;IAsCpC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BzD,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAazC,OAAO,CAAC,CAAC,EACb,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAClD,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,CAAC,CAAC;IAuBb,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAWjC,QAAQ,IAAI,MAAM;IAIlB,kBAAkB,IAAI,MAAM;IAI5B,YAAY,IAAI,MAAM,GAAG,IAAI;IAI7B,YAAY,IAAI,OAAO;IAIvB,QAAQ,IAAI,OAAO;IAQnB,gBAAgB,IAAI,gBAAgB,GAAG,IAAI;IAI3C,IAAI,IAAI,IAAI;IAKZ,IAAI,IAAI,IAAI;IAOZ,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,iBAAiB;YAQX,mBAAmB;YAkCnB,6BAA6B;CAa5C"}
|
package/dist/vault.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { CircuitBreaker } from "./circuit-breaker.js";
|
|
2
|
+
import { base64ToBytes } from "./encoding.js";
|
|
3
|
+
import { DEFAULT_PBKDF2_ITERATIONS, decryptWithKeyBits, deriveKeyBits, encryptKey } from "./crypto.js";
|
|
4
|
+
import { CircuitBreakerDisabledError, KeyNotFoundError, PBKDF2PolicyError, PassphrasePolicyError, VaultLockedError, WrongPassphraseError } from "./errors.js";
|
|
5
|
+
import { EncryptedKeyStorage, SessionKeyCache, getStorageKeys, resolveStorage } from "./storage.js";
|
|
6
|
+
const DEFAULT_NAMESPACE = "byok-vault";
|
|
7
|
+
const DEFAULT_MIN_PASSPHRASE_LENGTH = 8;
|
|
8
|
+
function inferDevMode() {
|
|
9
|
+
const processCandidate = globalThis.process;
|
|
10
|
+
if (!processCandidate?.env?.NODE_ENV) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
return processCandidate.env.NODE_ENV !== "production";
|
|
14
|
+
}
|
|
15
|
+
export class BYOKVault {
|
|
16
|
+
keyStorage;
|
|
17
|
+
sessionCache;
|
|
18
|
+
minPassphraseLength;
|
|
19
|
+
pbkdf2Iterations;
|
|
20
|
+
breaker;
|
|
21
|
+
devMode;
|
|
22
|
+
logger;
|
|
23
|
+
scopes = [];
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
const namespace = options.namespace ?? DEFAULT_NAMESPACE;
|
|
26
|
+
const localStorage = resolveStorage("localStorage", options.localStorage);
|
|
27
|
+
const sessionStorage = resolveStorage("sessionStorage", options.sessionStorage);
|
|
28
|
+
const keys = getStorageKeys(namespace);
|
|
29
|
+
this.keyStorage = new EncryptedKeyStorage(localStorage, keys.encryptedKey);
|
|
30
|
+
this.sessionCache = new SessionKeyCache(sessionStorage, keys.sessionKey);
|
|
31
|
+
this.minPassphraseLength = options.minPassphraseLength ?? DEFAULT_MIN_PASSPHRASE_LENGTH;
|
|
32
|
+
if (!Number.isFinite(this.minPassphraseLength) ||
|
|
33
|
+
!Number.isInteger(this.minPassphraseLength) ||
|
|
34
|
+
this.minPassphraseLength < 1) {
|
|
35
|
+
throw new Error("minPassphraseLength must be an integer greater than or equal to 1.");
|
|
36
|
+
}
|
|
37
|
+
this.pbkdf2Iterations = options.pbkdf2Iterations ?? DEFAULT_PBKDF2_ITERATIONS;
|
|
38
|
+
if (!Number.isFinite(this.pbkdf2Iterations) ||
|
|
39
|
+
!Number.isInteger(this.pbkdf2Iterations) ||
|
|
40
|
+
this.pbkdf2Iterations < DEFAULT_PBKDF2_ITERATIONS) {
|
|
41
|
+
throw new PBKDF2PolicyError(DEFAULT_PBKDF2_ITERATIONS);
|
|
42
|
+
}
|
|
43
|
+
this.devMode = options.devMode ?? inferDevMode();
|
|
44
|
+
this.logger = options.logger ?? console;
|
|
45
|
+
if (options.maxTokens !== undefined) {
|
|
46
|
+
this.breaker = new CircuitBreaker({
|
|
47
|
+
maxTokens: options.maxTokens,
|
|
48
|
+
storage: sessionStorage,
|
|
49
|
+
storageKey: keys.tokenUsage
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async setKey(apiKey, passphrase) {
|
|
54
|
+
this.assertPassphrase(passphrase);
|
|
55
|
+
if (!apiKey) {
|
|
56
|
+
throw new Error("apiKey cannot be empty.");
|
|
57
|
+
}
|
|
58
|
+
const blob = await encryptKey(apiKey, passphrase, {
|
|
59
|
+
iterations: this.pbkdf2Iterations
|
|
60
|
+
});
|
|
61
|
+
this.keyStorage.set(blob);
|
|
62
|
+
const keyBits = await deriveKeyBits(passphrase, base64ToBytes(blob.salt), blob.iterations);
|
|
63
|
+
// sessionStorage caching is only for passphrase UX; it is not an extra security boundary.
|
|
64
|
+
this.sessionCache.save({
|
|
65
|
+
salt: blob.salt,
|
|
66
|
+
iterations: blob.iterations,
|
|
67
|
+
keyBits
|
|
68
|
+
});
|
|
69
|
+
this.breaker?.reset();
|
|
70
|
+
}
|
|
71
|
+
async unlock(passphrase) {
|
|
72
|
+
this.assertPassphrase(passphrase);
|
|
73
|
+
const blob = this.requireStoredBlob();
|
|
74
|
+
const salt = base64ToBytes(blob.salt);
|
|
75
|
+
const keyBits = await deriveKeyBits(passphrase, salt, blob.iterations);
|
|
76
|
+
await this.decryptOrThrowWrongPassphrase(blob, keyBits);
|
|
77
|
+
this.sessionCache.save({
|
|
78
|
+
salt: blob.salt,
|
|
79
|
+
iterations: blob.iterations,
|
|
80
|
+
keyBits
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async withKey(callback, options = {}) {
|
|
84
|
+
this.breaker?.assertCanProceed(options.requestedTokens);
|
|
85
|
+
const scope = { reported: false };
|
|
86
|
+
let callbackCompleted = false;
|
|
87
|
+
this.scopes.push(scope);
|
|
88
|
+
try {
|
|
89
|
+
// If malicious script runs in-origin (XSS), it can still read this value in-flight.
|
|
90
|
+
// This API narrows exposure windows; it does not eliminate active injection risk.
|
|
91
|
+
const decryptedKey = await this.resolveDecryptedKey(options.passphrase);
|
|
92
|
+
const result = await callback(decryptedKey);
|
|
93
|
+
callbackCompleted = true;
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
this.scopes.pop();
|
|
98
|
+
if (this.breaker && this.devMode && callbackCompleted && !scope.reported) {
|
|
99
|
+
this.logger.warn("[byok-vault] withKey completed without reportUsage(tokens). Circuit breaker accounting is incomplete.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
reportUsage(tokens) {
|
|
104
|
+
if (!this.breaker) {
|
|
105
|
+
throw new CircuitBreakerDisabledError();
|
|
106
|
+
}
|
|
107
|
+
this.breaker.reportUsage(tokens);
|
|
108
|
+
const activeScope = this.scopes.at(-1);
|
|
109
|
+
if (activeScope) {
|
|
110
|
+
activeScope.reported = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
getUsage() {
|
|
114
|
+
return this.breaker?.getUsage() ?? 0;
|
|
115
|
+
}
|
|
116
|
+
getRemainingTokens() {
|
|
117
|
+
return this.breaker?.getRemainingTokens() ?? Number.POSITIVE_INFINITY;
|
|
118
|
+
}
|
|
119
|
+
getMaxTokens() {
|
|
120
|
+
return this.breaker ? this.breaker.getMaxTokens() : null;
|
|
121
|
+
}
|
|
122
|
+
hasStoredKey() {
|
|
123
|
+
return this.keyStorage.get() !== null;
|
|
124
|
+
}
|
|
125
|
+
isLocked() {
|
|
126
|
+
const blob = this.keyStorage.get();
|
|
127
|
+
if (!blob) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return this.sessionCache.load(blob.salt, blob.iterations) === null;
|
|
131
|
+
}
|
|
132
|
+
getEncryptedBlob() {
|
|
133
|
+
return this.keyStorage.get();
|
|
134
|
+
}
|
|
135
|
+
lock() {
|
|
136
|
+
this.sessionCache.clear();
|
|
137
|
+
this.scopes.length = 0;
|
|
138
|
+
}
|
|
139
|
+
nuke() {
|
|
140
|
+
this.keyStorage.clear();
|
|
141
|
+
this.sessionCache.clear();
|
|
142
|
+
this.breaker?.reset();
|
|
143
|
+
this.scopes.length = 0;
|
|
144
|
+
}
|
|
145
|
+
assertPassphrase(passphrase) {
|
|
146
|
+
if (passphrase.length < this.minPassphraseLength) {
|
|
147
|
+
throw new PassphrasePolicyError(this.minPassphraseLength);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
requireStoredBlob() {
|
|
151
|
+
const blob = this.keyStorage.get();
|
|
152
|
+
if (!blob) {
|
|
153
|
+
throw new KeyNotFoundError();
|
|
154
|
+
}
|
|
155
|
+
return blob;
|
|
156
|
+
}
|
|
157
|
+
async resolveDecryptedKey(passphrase) {
|
|
158
|
+
const blob = this.requireStoredBlob();
|
|
159
|
+
const cachedBits = this.sessionCache.load(blob.salt, blob.iterations);
|
|
160
|
+
if (cachedBits) {
|
|
161
|
+
try {
|
|
162
|
+
return await decryptWithKeyBits(blob, cachedBits);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error instanceof WrongPassphraseError) {
|
|
166
|
+
this.sessionCache.clear();
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!passphrase) {
|
|
174
|
+
throw new VaultLockedError();
|
|
175
|
+
}
|
|
176
|
+
this.assertPassphrase(passphrase);
|
|
177
|
+
const keyBits = await deriveKeyBits(passphrase, base64ToBytes(blob.salt), blob.iterations);
|
|
178
|
+
const decryptedKey = await this.decryptOrThrowWrongPassphrase(blob, keyBits);
|
|
179
|
+
this.sessionCache.save({
|
|
180
|
+
salt: blob.salt,
|
|
181
|
+
iterations: blob.iterations,
|
|
182
|
+
keyBits
|
|
183
|
+
});
|
|
184
|
+
return decryptedKey;
|
|
185
|
+
}
|
|
186
|
+
async decryptOrThrowWrongPassphrase(blob, keyBits) {
|
|
187
|
+
try {
|
|
188
|
+
return await decryptWithKeyBits(blob, keyBits);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (error instanceof WrongPassphraseError) {
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=vault.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault.js","sourceRoot":"","sources":["../src/vault.ts"],"names":[],"mappings":"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"}
|
package/docs/HUMANS.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Human Docs: byok-vault
|
|
2
|
+
|
|
3
|
+
This guide is for developers integrating `byok-vault` into real apps.
|
|
4
|
+
|
|
5
|
+
## What This Library Solves
|
|
6
|
+
|
|
7
|
+
- Encrypts user API keys in-browser before writing to `localStorage`.
|
|
8
|
+
- Lets you scope decrypted key access to one callback with `withKey(...)`.
|
|
9
|
+
- Adds optional per-session token budget tracking via a circuit breaker.
|
|
10
|
+
|
|
11
|
+
It is not a backend replacement for high-security threat models with active XSS risk.
|
|
12
|
+
|
|
13
|
+
## Quick Integration Checklist
|
|
14
|
+
|
|
15
|
+
1. Ask user for API key and passphrase.
|
|
16
|
+
2. Save once with `await vault.setKey(apiKey, passphrase)`.
|
|
17
|
+
3. For each request, call `vault.withKey(...)`.
|
|
18
|
+
4. If breaker enabled, call `vault.reportUsage(tokens)` after each successful provider response.
|
|
19
|
+
5. Add reset UI that calls `vault.nuke()`.
|
|
20
|
+
|
|
21
|
+
## Minimal Usage Pattern
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { BYOKVault } from "byok-vault";
|
|
25
|
+
|
|
26
|
+
const vault = new BYOKVault({
|
|
27
|
+
maxTokens: 30_000
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await vault.setKey(userApiKey, userPassphrase);
|
|
31
|
+
|
|
32
|
+
await vault.withKey(
|
|
33
|
+
async (key) => {
|
|
34
|
+
const response = await fetch("/your-provider-call", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
37
|
+
}).then((r) => r.json());
|
|
38
|
+
|
|
39
|
+
const used = response.usage?.total_tokens ?? 0;
|
|
40
|
+
vault.reportUsage(used);
|
|
41
|
+
},
|
|
42
|
+
{ requestedTokens: 1200 }
|
|
43
|
+
);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## UX Recommendations
|
|
47
|
+
|
|
48
|
+
- Explain passphrase purpose clearly: protects key at rest in browser storage.
|
|
49
|
+
- Enforce strong passphrase UX (default floor is 8 chars, consider stronger copy and meter).
|
|
50
|
+
- Show current token usage and remaining budget if breaker is enabled.
|
|
51
|
+
- Provide a visible "reset vault" control wired to `nuke()`.
|
|
52
|
+
|
|
53
|
+
## Security Boundaries (Plain English)
|
|
54
|
+
|
|
55
|
+
- `sessionStorage` caching is convenience only, not stronger security.
|
|
56
|
+
- If hostile JS executes in your origin, it can still intercept keys in-flight.
|
|
57
|
+
- Decrypted strings can remain in JS memory until garbage collection.
|
|
58
|
+
- This package is not formally audited.
|
|
59
|
+
|
|
60
|
+
## Common Mistakes
|
|
61
|
+
|
|
62
|
+
- Enabling `maxTokens` but forgetting `reportUsage(tokens)`.
|
|
63
|
+
- Treating `requestedTokens` pre-flight as exact accounting.
|
|
64
|
+
- Assuming this protects against active XSS.
|
|
65
|
+
- Lowering PBKDF2 iterations below `200000` (constructor throws).
|
|
66
|
+
|
|
67
|
+
## Error Handling You Should Surface
|
|
68
|
+
|
|
69
|
+
- `PASSPHRASE_POLICY`: passphrase too short.
|
|
70
|
+
- `WRONG_PASSPHRASE`: user entered incorrect passphrase.
|
|
71
|
+
- `VAULT_LOCKED`: no cached session key and no passphrase provided.
|
|
72
|
+
- `CIRCUIT_BREAKER_LIMIT`: budget exhausted/pre-flight blocked.
|
|
73
|
+
- `KEY_NOT_FOUND`: no stored key yet.
|
|
74
|
+
|
|
75
|
+
## Production Readiness Checklist
|
|
76
|
+
|
|
77
|
+
- Add CSP and strict input sanitization in your app to reduce XSS risk.
|
|
78
|
+
- Instrument `reportUsage` code path and alert on missing usage reporting.
|
|
79
|
+
- Use `pack:check` and CI before publishing changes.
|
|
80
|
+
- Document threat model to users in product copy.
|
package/docs/LLMS.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# LLM Docs: byok-vault
|
|
2
|
+
|
|
3
|
+
This file is a machine-oriented reference for code assistants and agents.
|
|
4
|
+
|
|
5
|
+
## Package Identity
|
|
6
|
+
|
|
7
|
+
- Name: `byok-vault`
|
|
8
|
+
- Runtime deps: none
|
|
9
|
+
- Environment: browser Web Crypto API (`crypto.subtle`), `localStorage`, `sessionStorage`
|
|
10
|
+
- Primary class: `BYOKVault`
|
|
11
|
+
|
|
12
|
+
## Core Guarantees
|
|
13
|
+
|
|
14
|
+
- Stored API keys are encrypted at rest using AES-GCM.
|
|
15
|
+
- Per-key salt is random and unique per encryption operation.
|
|
16
|
+
- AES key material is derived via PBKDF2 (SHA-256), default and enforced floor: `200000` iterations.
|
|
17
|
+
- Decrypted key is only provided inside `withKey(callback)`.
|
|
18
|
+
|
|
19
|
+
## Non-Goals / Limits
|
|
20
|
+
|
|
21
|
+
- No defense against active XSS in same origin.
|
|
22
|
+
- No hard provider SDK integrations.
|
|
23
|
+
- No authoritative token accounting without `reportUsage(tokens)`.
|
|
24
|
+
|
|
25
|
+
## Canonical API Contracts
|
|
26
|
+
|
|
27
|
+
### Constructor
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
new BYOKVault(options?)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Important options:
|
|
34
|
+
|
|
35
|
+
- `namespace?: string` -> storage key prefix
|
|
36
|
+
- `minPassphraseLength?: number` -> integer >= 1 (default 8)
|
|
37
|
+
- `pbkdf2Iterations?: number` -> integer >= 200000
|
|
38
|
+
- `maxTokens?: number` -> enables circuit breaker if set
|
|
39
|
+
- `devMode?: boolean`
|
|
40
|
+
- `localStorage?: Storage`, `sessionStorage?: Storage`
|
|
41
|
+
- `logger?: { warn(message: string): void }`
|
|
42
|
+
|
|
43
|
+
### Methods
|
|
44
|
+
|
|
45
|
+
- `setKey(apiKey, passphrase): Promise<void>`
|
|
46
|
+
- `unlock(passphrase): Promise<void>`
|
|
47
|
+
- `withKey(callback, { requestedTokens?, passphrase? }): Promise<T>`
|
|
48
|
+
- `reportUsage(tokens): void`
|
|
49
|
+
- `getUsage(): number`
|
|
50
|
+
- `getRemainingTokens(): number`
|
|
51
|
+
- `getMaxTokens(): number | null`
|
|
52
|
+
- `hasStoredKey(): boolean`
|
|
53
|
+
- `isLocked(): boolean`
|
|
54
|
+
- `getEncryptedBlob(): EncryptedKeyBlob | null`
|
|
55
|
+
- `lock(): void`
|
|
56
|
+
- `nuke(): void`
|
|
57
|
+
|
|
58
|
+
## Circuit Breaker Semantics
|
|
59
|
+
|
|
60
|
+
- `requestedTokens` is pre-flight estimate only.
|
|
61
|
+
- `reportUsage(tokens)` is post-call hard accounting.
|
|
62
|
+
- Breaker blocks the next request when usage already at/over budget.
|
|
63
|
+
- In dev mode, warning is emitted if `withKey` returns successfully without `reportUsage`.
|
|
64
|
+
- If callback throws, missing `reportUsage` warning is not emitted.
|
|
65
|
+
|
|
66
|
+
## Error Codes
|
|
67
|
+
|
|
68
|
+
- `PASSPHRASE_POLICY`
|
|
69
|
+
- `PBKDF2_POLICY`
|
|
70
|
+
- `KEY_NOT_FOUND`
|
|
71
|
+
- `VAULT_LOCKED`
|
|
72
|
+
- `WRONG_PASSPHRASE`
|
|
73
|
+
- `INVALID_USAGE_REPORT`
|
|
74
|
+
- `CIRCUIT_BREAKER_LIMIT`
|
|
75
|
+
- `CIRCUIT_BREAKER_DISABLED`
|
|
76
|
+
|
|
77
|
+
## Correct Usage Pattern (Agent Guidance)
|
|
78
|
+
|
|
79
|
+
1. Construct `BYOKVault`.
|
|
80
|
+
2. Save key once via `setKey(...)`.
|
|
81
|
+
3. Use `withKey(...)` around each provider call.
|
|
82
|
+
4. Parse provider usage from response and call `reportUsage(tokens)` if breaker enabled.
|
|
83
|
+
5. Use `nuke()` for full reset; use `lock()` for session-only lock.
|
|
84
|
+
|
|
85
|
+
## Anti-Patterns (Do Not Generate)
|
|
86
|
+
|
|
87
|
+
- Do not store plaintext API keys in storage.
|
|
88
|
+
- Do not call `withKey` and omit `reportUsage` when `maxTokens` is configured.
|
|
89
|
+
- Do not claim this library mitigates active XSS.
|
|
90
|
+
- Do not set `pbkdf2Iterations < 200000`.
|
|
91
|
+
|
|
92
|
+
## Provider Usage Parsing Examples
|
|
93
|
+
|
|
94
|
+
OpenAI style:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const tokens = response.usage?.total_tokens ?? 0;
|
|
98
|
+
vault.reportUsage(tokens);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Anthropic style:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const tokens =
|
|
105
|
+
(response.usage?.input_tokens ?? 0) + (response.usage?.output_tokens ?? 0);
|
|
106
|
+
vault.reportUsage(tokens);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Gemini style:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const tokens = response.usageMetadata?.totalTokenCount ?? 0;
|
|
113
|
+
vault.reportUsage(tokens);
|
|
114
|
+
```
|
package/llms.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# byok-vault
|
|
2
|
+
|
|
3
|
+
Browser-native BYOK vault for local-first/serverless AI apps.
|
|
4
|
+
|
|
5
|
+
## Preferred Docs
|
|
6
|
+
|
|
7
|
+
- README: `README.md`
|
|
8
|
+
- Human integration guide: `docs/HUMANS.md`
|
|
9
|
+
- LLM reference: `docs/LLMS.md`
|
|
10
|
+
- Gemini sample app: `examples/local-first-byok-sample/README.md`
|
|
11
|
+
|
|
12
|
+
## Stable Facts
|
|
13
|
+
|
|
14
|
+
- Primary API surface is `BYOKVault`.
|
|
15
|
+
- Encryption at rest: AES-GCM.
|
|
16
|
+
- Key derivation: PBKDF2 SHA-256 with minimum `200000` iterations.
|
|
17
|
+
- Circuit breaker is optional and requires `reportUsage(tokens)` to be meaningful.
|
|
18
|
+
- Library is not a defense against active XSS in same-origin execution context.
|
|
19
|
+
|
|
20
|
+
## Safe Generation Rules For Agents
|
|
21
|
+
|
|
22
|
+
- Use `withKey(async (key) => { ... })` for provider calls.
|
|
23
|
+
- If `maxTokens` is set, include `vault.reportUsage(tokens)` after successful responses.
|
|
24
|
+
- Do not generate claims that this package is formally audited.
|
|
25
|
+
- Do not claim it eliminates XSS risk.
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "byok-vault",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser-native BYOK vault with encrypted storage and token circuit breaker.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"docs",
|
|
17
|
+
"llms.txt"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "npm run build:lib",
|
|
22
|
+
"build:lib": "tsc -p tsconfig.build.json",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"pack:check": "node ./scripts/check-pack.mjs",
|
|
27
|
+
"demo": "vite --config demo/vite.config.ts",
|
|
28
|
+
"demo:build": "vite build --config demo/vite.config.ts"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"byok",
|
|
32
|
+
"vault",
|
|
33
|
+
"browser",
|
|
34
|
+
"aes-gcm",
|
|
35
|
+
"pbkdf2"
|
|
36
|
+
],
|
|
37
|
+
"author": "Ra <ravicity999@gmail.com>",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.15.19",
|
|
41
|
+
"jsdom": "^25.0.1",
|
|
42
|
+
"typescript": "^5.8.3",
|
|
43
|
+
"vite": "^5.4.19",
|
|
44
|
+
"vitest": "^2.1.9"
|
|
45
|
+
}
|
|
46
|
+
}
|