@fgv/ts-extras 5.1.0-21 → 5.1.0-23
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/dist/packlets/ai-assist/index.js +2 -0
- package/dist/packlets/ai-assist/index.js.map +1 -1
- package/dist/packlets/ai-assist/jsonCompletion.js +95 -0
- package/dist/packlets/ai-assist/jsonCompletion.js.map +1 -0
- package/dist/packlets/ai-assist/jsonResponse.js +149 -0
- package/dist/packlets/ai-assist/jsonResponse.js.map +1 -0
- package/dist/packlets/crypto-utils/keystore/keyStore.js +81 -0
- package/dist/packlets/crypto-utils/keystore/keyStore.js.map +1 -1
- package/dist/packlets/crypto-utils/model.js.map +1 -1
- package/dist/packlets/crypto-utils/nodeCryptoProvider.js +9 -1
- package/dist/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -1
- package/dist/packlets/csv/csvFileHelpers.js +0 -14
- package/dist/packlets/csv/csvFileHelpers.js.map +1 -1
- package/dist/packlets/csv/csvHelpers.js +14 -0
- package/dist/packlets/csv/csvHelpers.js.map +1 -1
- package/dist/packlets/csv/index.browser.js +1 -3
- package/dist/packlets/csv/index.browser.js.map +1 -1
- package/dist/packlets/record-jar/index.browser.js +1 -3
- package/dist/packlets/record-jar/index.browser.js.map +1 -1
- package/dist/packlets/record-jar/recordJarFileHelpers.js +0 -18
- package/dist/packlets/record-jar/recordJarFileHelpers.js.map +1 -1
- package/dist/packlets/record-jar/recordJarHelpers.js +18 -0
- package/dist/packlets/record-jar/recordJarHelpers.js.map +1 -1
- package/dist/ts-extras.d.ts +239 -5
- package/lib/packlets/ai-assist/index.d.ts +2 -0
- package/lib/packlets/ai-assist/index.d.ts.map +1 -1
- package/lib/packlets/ai-assist/index.js +7 -1
- package/lib/packlets/ai-assist/index.js.map +1 -1
- package/lib/packlets/ai-assist/jsonCompletion.d.ts +93 -0
- package/lib/packlets/ai-assist/jsonCompletion.d.ts.map +1 -0
- package/lib/packlets/ai-assist/jsonCompletion.js +99 -0
- package/lib/packlets/ai-assist/jsonCompletion.js.map +1 -0
- package/lib/packlets/ai-assist/jsonResponse.d.ts +91 -0
- package/lib/packlets/ai-assist/jsonResponse.d.ts.map +1 -0
- package/lib/packlets/ai-assist/jsonResponse.js +154 -0
- package/lib/packlets/ai-assist/jsonResponse.js.map +1 -0
- package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +43 -1
- package/lib/packlets/crypto-utils/keystore/keyStore.d.ts.map +1 -1
- package/lib/packlets/crypto-utils/keystore/keyStore.js +81 -0
- package/lib/packlets/crypto-utils/keystore/keyStore.js.map +1 -1
- package/lib/packlets/crypto-utils/model.d.ts +10 -1
- package/lib/packlets/crypto-utils/model.d.ts.map +1 -1
- package/lib/packlets/crypto-utils/model.js.map +1 -1
- package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +7 -1
- package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts.map +1 -1
- package/lib/packlets/crypto-utils/nodeCryptoProvider.js +8 -0
- package/lib/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -1
- package/lib/packlets/csv/csvFileHelpers.d.ts +0 -10
- package/lib/packlets/csv/csvFileHelpers.d.ts.map +1 -1
- package/lib/packlets/csv/csvFileHelpers.js +0 -15
- package/lib/packlets/csv/csvFileHelpers.js.map +1 -1
- package/lib/packlets/csv/csvHelpers.d.ts +10 -0
- package/lib/packlets/csv/csvHelpers.d.ts.map +1 -1
- package/lib/packlets/csv/csvHelpers.js +15 -0
- package/lib/packlets/csv/csvHelpers.js.map +1 -1
- package/lib/packlets/csv/index.browser.d.ts +0 -1
- package/lib/packlets/csv/index.browser.d.ts.map +1 -1
- package/lib/packlets/csv/index.browser.js +1 -5
- package/lib/packlets/csv/index.browser.js.map +1 -1
- package/lib/packlets/record-jar/index.browser.d.ts +0 -1
- package/lib/packlets/record-jar/index.browser.d.ts.map +1 -1
- package/lib/packlets/record-jar/index.browser.js +1 -5
- package/lib/packlets/record-jar/index.browser.js.map +1 -1
- package/lib/packlets/record-jar/recordJarFileHelpers.d.ts +0 -11
- package/lib/packlets/record-jar/recordJarFileHelpers.d.ts.map +1 -1
- package/lib/packlets/record-jar/recordJarFileHelpers.js +0 -19
- package/lib/packlets/record-jar/recordJarFileHelpers.js.map +1 -1
- package/lib/packlets/record-jar/recordJarHelpers.d.ts +11 -0
- package/lib/packlets/record-jar/recordJarHelpers.d.ts.map +1 -1
- package/lib/packlets/record-jar/recordJarHelpers.js +19 -0
- package/lib/packlets/record-jar/recordJarHelpers.js.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-tolerant extraction and converters for LLM responses.
|
|
3
|
+
*
|
|
4
|
+
* Models commonly wrap JSON output in Markdown code fences, add a
|
|
5
|
+
* "Sure, here's the JSON:" preamble, or trail off with prose after the
|
|
6
|
+
* closing brace. These helpers normalize that quirk on the read side so every
|
|
7
|
+
* AiAssist consumer can reach a validated `T` from raw model text without
|
|
8
|
+
* reimplementing the same fence-stripping logic.
|
|
9
|
+
*
|
|
10
|
+
* Scope: strip wrappers (fences, prose, BOM, whitespace). Out of scope: repair
|
|
11
|
+
* malformed JSON (missing commas, unquoted keys, smart quotes, etc.).
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
import { type Converter, Result, type Validator } from '@fgv/ts-utils';
|
|
16
|
+
import { type JsonValue } from '@fgv/ts-json-base';
|
|
17
|
+
/**
|
|
18
|
+
* A function that pulls a JSON-shaped substring out of arbitrary model text.
|
|
19
|
+
* Implementations strip whatever wrappers the model added (fences, preamble,
|
|
20
|
+
* trailing prose) and return the JSON-shaped substring ready for `JSON.parse`.
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
export type JsonTextExtractor = (text: string) => Result<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Default {@link AiAssist.JsonTextExtractor | extractor} for LLM responses. Tolerates:
|
|
26
|
+
*
|
|
27
|
+
* - Leading/trailing whitespace and a leading byte-order mark.
|
|
28
|
+
* - Markdown code fences (with or without a language tag).
|
|
29
|
+
* - Conversational preamble before the first `{` or `[`.
|
|
30
|
+
* - Trailing prose after the matched closing `}` or `]`.
|
|
31
|
+
*
|
|
32
|
+
* Out of scope: repairing malformed JSON, handling smart quotes, etc.
|
|
33
|
+
*
|
|
34
|
+
* @param text - Raw model output.
|
|
35
|
+
* @returns A `Result<string>` containing the JSON-shaped substring, or a
|
|
36
|
+
* `Failure` if no JSON-shaped substring was found.
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export declare const extractJsonText: JsonTextExtractor;
|
|
40
|
+
/**
|
|
41
|
+
* Options shared by every {@link AiAssist.fencedStringifiedJson} call.
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
export interface IFencedStringifiedJsonExtractorOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Optional pre-parse extractor. Defaults to {@link AiAssist.extractJsonText}.
|
|
47
|
+
* Provide a custom extractor to handle response shapes the default does not
|
|
48
|
+
* understand.
|
|
49
|
+
*/
|
|
50
|
+
readonly extractor?: JsonTextExtractor;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Options for the validating overload of {@link AiAssist.fencedStringifiedJson}.
|
|
54
|
+
* `inner` is required so the typed `Converter<T>` return value can never lie
|
|
55
|
+
* about the runtime shape.
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
export interface IFencedStringifiedJsonOptions<T> extends IFencedStringifiedJsonExtractorOptions {
|
|
59
|
+
/** Inner converter or validator applied to the parsed JSON value. */
|
|
60
|
+
readonly inner: Converter<T> | Validator<T>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Creates a `Converter` that accepts raw LLM response text, runs it through a
|
|
64
|
+
* tolerant extractor (default: {@link AiAssist.extractJsonText}), parses the
|
|
65
|
+
* extracted substring as JSON, and applies an optional inner converter or
|
|
66
|
+
* validator.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* const converter = fencedStringifiedJson({ inner: myShapeConverter });
|
|
71
|
+
* const result = converter.convert(llmText); // Result<MyShape>
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @param options - Optional extractor; omit to keep the default. Without an
|
|
75
|
+
* `inner` step, the converter resolves to the parsed `JsonValue`.
|
|
76
|
+
* @returns A `Converter<JsonValue>`.
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
export declare function fencedStringifiedJson(options?: IFencedStringifiedJsonExtractorOptions): Converter<JsonValue>;
|
|
80
|
+
/**
|
|
81
|
+
* Creates a `Converter` that accepts raw LLM response text, runs it through a
|
|
82
|
+
* tolerant extractor (default: {@link AiAssist.extractJsonText}), parses the
|
|
83
|
+
* extracted substring as JSON, and applies the supplied inner converter or
|
|
84
|
+
* validator.
|
|
85
|
+
*
|
|
86
|
+
* @param options - Required `inner` converter/validator and optional extractor.
|
|
87
|
+
* @returns A `Converter<T>`.
|
|
88
|
+
* @public
|
|
89
|
+
*/
|
|
90
|
+
export declare function fencedStringifiedJson<T>(options: IFencedStringifiedJsonOptions<T>): Converter<T>;
|
|
91
|
+
//# sourceMappingURL=jsonResponse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonResponse.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/jsonResponse.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAc,KAAK,SAAS,EAAQ,MAAM,EAAW,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAClG,OAAO,EAAoC,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAErF;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC;AA+DjE;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,EAAE,iBA6B7B,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,sCAAsC;IACrD;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,iBAAiB,CAAC;CACxC;AAED;;;;;GAKG;AACH,MAAM,WAAW,6BAA6B,CAAC,CAAC,CAAE,SAAQ,sCAAsC;IAC9F,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;CAC7C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,sCAAsC,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;AAC9G;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,OAAO,EAAE,6BAA6B,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright (c) 2026 Erik Fortune
|
|
3
|
+
//
|
|
4
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
// in the Software without restriction, including without limitation the rights
|
|
7
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
// furnished to do so, subject to the following conditions:
|
|
10
|
+
//
|
|
11
|
+
// The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
// copies or substantial portions of the Software.
|
|
13
|
+
//
|
|
14
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
// SOFTWARE.
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.extractJsonText = void 0;
|
|
23
|
+
exports.fencedStringifiedJson = fencedStringifiedJson;
|
|
24
|
+
/**
|
|
25
|
+
* JSON-tolerant extraction and converters for LLM responses.
|
|
26
|
+
*
|
|
27
|
+
* Models commonly wrap JSON output in Markdown code fences, add a
|
|
28
|
+
* "Sure, here's the JSON:" preamble, or trail off with prose after the
|
|
29
|
+
* closing brace. These helpers normalize that quirk on the read side so every
|
|
30
|
+
* AiAssist consumer can reach a validated `T` from raw model text without
|
|
31
|
+
* reimplementing the same fence-stripping logic.
|
|
32
|
+
*
|
|
33
|
+
* Scope: strip wrappers (fences, prose, BOM, whitespace). Out of scope: repair
|
|
34
|
+
* malformed JSON (missing commas, unquoted keys, smart quotes, etc.).
|
|
35
|
+
*
|
|
36
|
+
* @packageDocumentation
|
|
37
|
+
*/
|
|
38
|
+
const ts_utils_1 = require("@fgv/ts-utils");
|
|
39
|
+
const ts_json_base_1 = require("@fgv/ts-json-base");
|
|
40
|
+
const FENCED_BLOCK = /```[A-Za-z0-9_-]*\s*\r?\n([\s\S]*?)\r?\n?```/;
|
|
41
|
+
const BOM = /^\uFEFF/;
|
|
42
|
+
// Full RFC 8259 grammar so the extractor only succeeds when the entire
|
|
43
|
+
// candidate parses as a JSON primitive (instead of just starting like one).
|
|
44
|
+
const JSON_NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
45
|
+
// eslint-disable-next-line no-control-regex
|
|
46
|
+
const JSON_STRING = /^"(?:[^"\\\u0000-\u001F]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"$/;
|
|
47
|
+
const JSON_KEYWORD = /^(?:true|false|null)$/;
|
|
48
|
+
function stripBom(text) {
|
|
49
|
+
return text.replace(BOM, '');
|
|
50
|
+
}
|
|
51
|
+
function findBalancedJsonSubstring(text) {
|
|
52
|
+
// Walk the text once tracking string state. The first '{' or '[' that is
|
|
53
|
+
// *outside* a quoted string is the candidate start; from there, count
|
|
54
|
+
// matching close characters while ignoring delimiters that appear inside
|
|
55
|
+
// strings.
|
|
56
|
+
let inString = false;
|
|
57
|
+
let escape = false;
|
|
58
|
+
let start = -1;
|
|
59
|
+
let open = '';
|
|
60
|
+
let close = '';
|
|
61
|
+
let depth = 0;
|
|
62
|
+
for (let i = 0; i < text.length; i++) {
|
|
63
|
+
const ch = text.charAt(i);
|
|
64
|
+
if (inString) {
|
|
65
|
+
if (escape) {
|
|
66
|
+
escape = false;
|
|
67
|
+
}
|
|
68
|
+
else if (ch === '\\') {
|
|
69
|
+
escape = true;
|
|
70
|
+
}
|
|
71
|
+
else if (ch === '"') {
|
|
72
|
+
inString = false;
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (ch === '"') {
|
|
77
|
+
inString = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (start < 0) {
|
|
81
|
+
if (ch === '{' || ch === '[') {
|
|
82
|
+
start = i;
|
|
83
|
+
open = ch;
|
|
84
|
+
close = ch === '{' ? '}' : ']';
|
|
85
|
+
depth = 1;
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (ch === open) {
|
|
90
|
+
depth++;
|
|
91
|
+
}
|
|
92
|
+
else if (ch === close) {
|
|
93
|
+
depth--;
|
|
94
|
+
if (depth === 0) {
|
|
95
|
+
return text.slice(start, i + 1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Default {@link AiAssist.JsonTextExtractor | extractor} for LLM responses. Tolerates:
|
|
103
|
+
*
|
|
104
|
+
* - Leading/trailing whitespace and a leading byte-order mark.
|
|
105
|
+
* - Markdown code fences (with or without a language tag).
|
|
106
|
+
* - Conversational preamble before the first `{` or `[`.
|
|
107
|
+
* - Trailing prose after the matched closing `}` or `]`.
|
|
108
|
+
*
|
|
109
|
+
* Out of scope: repairing malformed JSON, handling smart quotes, etc.
|
|
110
|
+
*
|
|
111
|
+
* @param text - Raw model output.
|
|
112
|
+
* @returns A `Result<string>` containing the JSON-shaped substring, or a
|
|
113
|
+
* `Failure` if no JSON-shaped substring was found.
|
|
114
|
+
* @public
|
|
115
|
+
*/
|
|
116
|
+
const extractJsonText = (text) => {
|
|
117
|
+
if (typeof text !== 'string') {
|
|
118
|
+
return (0, ts_utils_1.fail)('extractJsonText: input must be a string.');
|
|
119
|
+
}
|
|
120
|
+
const stripped = stripBom(text).trim();
|
|
121
|
+
if (stripped.length === 0) {
|
|
122
|
+
return (0, ts_utils_1.fail)('extractJsonText: input is empty.');
|
|
123
|
+
}
|
|
124
|
+
const fenced = FENCED_BLOCK.exec(stripped);
|
|
125
|
+
const candidate = fenced ? fenced[1].trim() : stripped;
|
|
126
|
+
if (candidate.length === 0) {
|
|
127
|
+
return (0, ts_utils_1.fail)('extractJsonText: no JSON content found.');
|
|
128
|
+
}
|
|
129
|
+
// Whole-candidate primitive check runs before the brace scan so that a
|
|
130
|
+
// valid JSON string containing braces (e.g. `"text with { }"`) is returned
|
|
131
|
+
// intact instead of being mangled into the first balanced `{ }` match.
|
|
132
|
+
if (JSON_KEYWORD.test(candidate) || JSON_NUMBER.test(candidate) || JSON_STRING.test(candidate)) {
|
|
133
|
+
return (0, ts_utils_1.succeed)(candidate);
|
|
134
|
+
}
|
|
135
|
+
const balanced = findBalancedJsonSubstring(candidate);
|
|
136
|
+
if (balanced !== undefined) {
|
|
137
|
+
return (0, ts_utils_1.succeed)(balanced);
|
|
138
|
+
}
|
|
139
|
+
return (0, ts_utils_1.fail)('extractJsonText: no JSON-shaped substring found.');
|
|
140
|
+
};
|
|
141
|
+
exports.extractJsonText = extractJsonText;
|
|
142
|
+
function fencedStringifiedJson(options) {
|
|
143
|
+
var _a;
|
|
144
|
+
const extractor = (_a = options === null || options === void 0 ? void 0 : options.extractor) !== null && _a !== void 0 ? _a : exports.extractJsonText;
|
|
145
|
+
const inner = options === null || options === void 0 ? void 0 : options.inner;
|
|
146
|
+
const parser = inner !== undefined ? ts_json_base_1.Converters.stringifiedJson(inner) : ts_json_base_1.Converters.stringifiedJson();
|
|
147
|
+
return new ts_utils_1.Conversion.BaseConverter((from) => {
|
|
148
|
+
if (typeof from !== 'string') {
|
|
149
|
+
return (0, ts_utils_1.fail)('fencedStringifiedJson: input must be a string.');
|
|
150
|
+
}
|
|
151
|
+
return extractor(from).onSuccess((extracted) => parser.convert(extracted));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=jsonResponse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonResponse.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/jsonResponse.ts"],"names":[],"mappings":";AAAA,kCAAkC;AAClC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AACxE,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,kDAAkD;AAClD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,yEAAyE;AACzE,gFAAgF;AAChF,gFAAgF;AAChF,YAAY;;;AA4LZ,sDAcC;AAxMD;;;;;;;;;;;;;GAaG;AAEH,4CAAkG;AAClG,oDAAqF;AAUrF,MAAM,YAAY,GAAW,8CAA8C,CAAC;AAC5E,MAAM,GAAG,GAAW,SAAS,CAAC;AAC9B,uEAAuE;AACvE,4EAA4E;AAC5E,MAAM,WAAW,GAAW,+CAA+C,CAAC;AAC5E,4CAA4C;AAC5C,MAAM,WAAW,GAAW,gEAAgE,CAAC;AAC7F,MAAM,YAAY,GAAW,uBAAuB,CAAC;AAErD,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,yBAAyB,CAAC,IAAY;IAC7C,yEAAyE;IACzE,sEAAsE;IACtE,yEAAyE;IACzE,WAAW;IACX,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC;IACf,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC1B,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,GAAG,KAAK,CAAC;YACjB,CAAC;iBAAM,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;gBACvB,MAAM,GAAG,IAAI,CAAC;YAChB,CAAC;iBAAM,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;gBACtB,QAAQ,GAAG,KAAK,CAAC;YACnB,CAAC;YACD,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,QAAQ,GAAG,IAAI,CAAC;YAChB,SAAS;QACX,CAAC;QACD,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;gBAC7B,KAAK,GAAG,CAAC,CAAC;gBACV,IAAI,GAAG,EAAE,CAAC;gBACV,KAAK,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;gBAC/B,KAAK,GAAG,CAAC,CAAC;YACZ,CAAC;YACD,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAChB,KAAK,EAAE,CAAC;QACV,CAAC;aAAM,IAAI,EAAE,KAAK,KAAK,EAAE,CAAC;YACxB,KAAK,EAAE,CAAC;YACR,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;gBAChB,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YAClC,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACI,MAAM,eAAe,GAAsB,CAAC,IAAY,EAAkB,EAAE;IACjF,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAA,eAAI,EAAC,0CAA0C,CAAC,CAAC;IAC1D,CAAC;IACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IACvC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAA,eAAI,EAAC,kCAAkC,CAAC,CAAC;IAClD,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;IAEvD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,IAAA,eAAI,EAAC,yCAAyC,CAAC,CAAC;IACzD,CAAC;IAED,uEAAuE;IACvE,2EAA2E;IAC3E,uEAAuE;IACvE,IAAI,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAC/F,OAAO,IAAA,kBAAO,EAAC,SAAS,CAAC,CAAC;IAC5B,CAAC;IAED,MAAM,QAAQ,GAAG,yBAAyB,CAAC,SAAS,CAAC,CAAC;IACtD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,IAAA,kBAAO,EAAC,QAAQ,CAAC,CAAC;IAC3B,CAAC;IAED,OAAO,IAAA,eAAI,EAAC,kDAAkD,CAAC,CAAC;AAClE,CAAC,CAAC;AA7BW,QAAA,eAAe,mBA6B1B;AAuDF,SAAgB,qBAAqB,CACnC,OAAmF;;IAEnF,MAAM,SAAS,GAAsB,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,SAAS,mCAAI,uBAAe,CAAC;IAC3E,MAAM,KAAK,GAAI,OAAwD,aAAxD,OAAO,uBAAP,OAAO,CAAmD,KAAK,CAAC;IAC/E,MAAM,MAAM,GACV,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,yBAAkB,CAAC,eAAe,CAAI,KAAK,CAAC,CAAC,CAAC,CAAC,yBAAkB,CAAC,eAAe,EAAE,CAAC;IAE5G,OAAO,IAAI,qBAAU,CAAC,aAAa,CAAgB,CAAC,IAAa,EAAyB,EAAE;QAC1F,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,OAAO,IAAA,eAAI,EAAC,gDAAgD,CAAC,CAAC;QAChE,CAAC;QACD,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;IAC7E,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * JSON-tolerant extraction and converters for LLM responses.\n *\n * Models commonly wrap JSON output in Markdown code fences, add a\n * \"Sure, here's the JSON:\" preamble, or trail off with prose after the\n * closing brace. These helpers normalize that quirk on the read side so every\n * AiAssist consumer can reach a validated `T` from raw model text without\n * reimplementing the same fence-stripping logic.\n *\n * Scope: strip wrappers (fences, prose, BOM, whitespace). Out of scope: repair\n * malformed JSON (missing commas, unquoted keys, smart quotes, etc.).\n *\n * @packageDocumentation\n */\n\nimport { Conversion, type Converter, fail, Result, succeed, type Validator } from '@fgv/ts-utils';\nimport { Converters as JsonBaseConverters, type JsonValue } from '@fgv/ts-json-base';\n\n/**\n * A function that pulls a JSON-shaped substring out of arbitrary model text.\n * Implementations strip whatever wrappers the model added (fences, preamble,\n * trailing prose) and return the JSON-shaped substring ready for `JSON.parse`.\n * @public\n */\nexport type JsonTextExtractor = (text: string) => Result<string>;\n\nconst FENCED_BLOCK: RegExp = /```[A-Za-z0-9_-]*\\s*\\r?\\n([\\s\\S]*?)\\r?\\n?```/;\nconst BOM: RegExp = /^\\uFEFF/;\n// Full RFC 8259 grammar so the extractor only succeeds when the entire\n// candidate parses as a JSON primitive (instead of just starting like one).\nconst JSON_NUMBER: RegExp = /^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$/;\n// eslint-disable-next-line no-control-regex\nconst JSON_STRING: RegExp = /^\"(?:[^\"\\\\\\u0000-\\u001F]|\\\\(?:[\"\\\\/bfnrt]|u[0-9a-fA-F]{4}))*\"$/;\nconst JSON_KEYWORD: RegExp = /^(?:true|false|null)$/;\n\nfunction stripBom(text: string): string {\n return text.replace(BOM, '');\n}\n\nfunction findBalancedJsonSubstring(text: string): string | undefined {\n // Walk the text once tracking string state. The first '{' or '[' that is\n // *outside* a quoted string is the candidate start; from there, count\n // matching close characters while ignoring delimiters that appear inside\n // strings.\n let inString = false;\n let escape = false;\n let start = -1;\n let open = '';\n let close = '';\n let depth = 0;\n for (let i = 0; i < text.length; i++) {\n const ch = text.charAt(i);\n if (inString) {\n if (escape) {\n escape = false;\n } else if (ch === '\\\\') {\n escape = true;\n } else if (ch === '\"') {\n inString = false;\n }\n continue;\n }\n if (ch === '\"') {\n inString = true;\n continue;\n }\n if (start < 0) {\n if (ch === '{' || ch === '[') {\n start = i;\n open = ch;\n close = ch === '{' ? '}' : ']';\n depth = 1;\n }\n continue;\n }\n if (ch === open) {\n depth++;\n } else if (ch === close) {\n depth--;\n if (depth === 0) {\n return text.slice(start, i + 1);\n }\n }\n }\n return undefined;\n}\n\n/**\n * Default {@link AiAssist.JsonTextExtractor | extractor} for LLM responses. Tolerates:\n *\n * - Leading/trailing whitespace and a leading byte-order mark.\n * - Markdown code fences (with or without a language tag).\n * - Conversational preamble before the first `{` or `[`.\n * - Trailing prose after the matched closing `}` or `]`.\n *\n * Out of scope: repairing malformed JSON, handling smart quotes, etc.\n *\n * @param text - Raw model output.\n * @returns A `Result<string>` containing the JSON-shaped substring, or a\n * `Failure` if no JSON-shaped substring was found.\n * @public\n */\nexport const extractJsonText: JsonTextExtractor = (text: string): Result<string> => {\n if (typeof text !== 'string') {\n return fail('extractJsonText: input must be a string.');\n }\n const stripped = stripBom(text).trim();\n if (stripped.length === 0) {\n return fail('extractJsonText: input is empty.');\n }\n\n const fenced = FENCED_BLOCK.exec(stripped);\n const candidate = fenced ? fenced[1].trim() : stripped;\n\n if (candidate.length === 0) {\n return fail('extractJsonText: no JSON content found.');\n }\n\n // Whole-candidate primitive check runs before the brace scan so that a\n // valid JSON string containing braces (e.g. `\"text with { }\"`) is returned\n // intact instead of being mangled into the first balanced `{ }` match.\n if (JSON_KEYWORD.test(candidate) || JSON_NUMBER.test(candidate) || JSON_STRING.test(candidate)) {\n return succeed(candidate);\n }\n\n const balanced = findBalancedJsonSubstring(candidate);\n if (balanced !== undefined) {\n return succeed(balanced);\n }\n\n return fail('extractJsonText: no JSON-shaped substring found.');\n};\n\n/**\n * Options shared by every {@link AiAssist.fencedStringifiedJson} call.\n * @public\n */\nexport interface IFencedStringifiedJsonExtractorOptions {\n /**\n * Optional pre-parse extractor. Defaults to {@link AiAssist.extractJsonText}.\n * Provide a custom extractor to handle response shapes the default does not\n * understand.\n */\n readonly extractor?: JsonTextExtractor;\n}\n\n/**\n * Options for the validating overload of {@link AiAssist.fencedStringifiedJson}.\n * `inner` is required so the typed `Converter<T>` return value can never lie\n * about the runtime shape.\n * @public\n */\nexport interface IFencedStringifiedJsonOptions<T> extends IFencedStringifiedJsonExtractorOptions {\n /** Inner converter or validator applied to the parsed JSON value. */\n readonly inner: Converter<T> | Validator<T>;\n}\n\n/**\n * Creates a `Converter` that accepts raw LLM response text, runs it through a\n * tolerant extractor (default: {@link AiAssist.extractJsonText}), parses the\n * extracted substring as JSON, and applies an optional inner converter or\n * validator.\n *\n * @example\n * ```ts\n * const converter = fencedStringifiedJson({ inner: myShapeConverter });\n * const result = converter.convert(llmText); // Result<MyShape>\n * ```\n *\n * @param options - Optional extractor; omit to keep the default. Without an\n * `inner` step, the converter resolves to the parsed `JsonValue`.\n * @returns A `Converter<JsonValue>`.\n * @public\n */\nexport function fencedStringifiedJson(options?: IFencedStringifiedJsonExtractorOptions): Converter<JsonValue>;\n/**\n * Creates a `Converter` that accepts raw LLM response text, runs it through a\n * tolerant extractor (default: {@link AiAssist.extractJsonText}), parses the\n * extracted substring as JSON, and applies the supplied inner converter or\n * validator.\n *\n * @param options - Required `inner` converter/validator and optional extractor.\n * @returns A `Converter<T>`.\n * @public\n */\nexport function fencedStringifiedJson<T>(options: IFencedStringifiedJsonOptions<T>): Converter<T>;\nexport function fencedStringifiedJson<T>(\n options?: IFencedStringifiedJsonExtractorOptions | IFencedStringifiedJsonOptions<T>\n): Converter<T | JsonValue> {\n const extractor: JsonTextExtractor = options?.extractor ?? extractJsonText;\n const inner = (options as IFencedStringifiedJsonOptions<T> | undefined)?.inner;\n const parser: Converter<T | JsonValue> =\n inner !== undefined ? JsonBaseConverters.stringifiedJson<T>(inner) : JsonBaseConverters.stringifiedJson();\n\n return new Conversion.BaseConverter<T | JsonValue>((from: unknown): Result<T | JsonValue> => {\n if (typeof from !== 'string') {\n return fail('fencedStringifiedJson: input must be a string.');\n }\n return extractor(from).onSuccess((extracted) => parser.convert(extracted));\n });\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { JsonValue } from '@fgv/ts-json-base';
|
|
2
2
|
import { Result } from '@fgv/ts-utils';
|
|
3
|
-
import { ICryptoProvider, IEncryptedFile, IEncryptionConfig, IEncryptionProvider, SecretProvider } from '../model';
|
|
3
|
+
import { ICryptoProvider, IEncryptedFile, IEncryptionConfig, IEncryptionProvider, IKeyDerivationParams, SecretProvider } from '../model';
|
|
4
4
|
import { IAddKeyPairOptions, IAddKeyPairResult, IAddSecretFromPasswordOptions, IAddSecretFromPasswordResult, IAddSecretOptions, IAddSecretResult, IImportKeyOptions, IImportSecretOptions, IKeyStoreCreateParams, IKeyStoreEntry, IKeyStoreFile, IKeyStoreOpenParams, IRemoveSecretResult, KeyStoreLockState, KeyStoreSecretType } from './model';
|
|
5
5
|
/**
|
|
6
6
|
* Password-protected key store for managing encryption secrets.
|
|
@@ -196,6 +196,41 @@ export declare class KeyStore implements IEncryptionProvider {
|
|
|
196
196
|
* @public
|
|
197
197
|
*/
|
|
198
198
|
addSecretFromPassword(name: string, password: string, options?: IAddSecretFromPasswordOptions): Promise<Result<IAddSecretFromPasswordResult>>;
|
|
199
|
+
/**
|
|
200
|
+
* Verifies that a candidate password derives the same key material currently
|
|
201
|
+
* stored under `name`, using the supplied
|
|
202
|
+
* {@link CryptoUtils.IKeyDerivationParams | key derivation parameters}.
|
|
203
|
+
*
|
|
204
|
+
* The keystore does not persist per-slot key derivation parameters with the
|
|
205
|
+
* entry — callers receive them from `addSecretFromPassword` and store them
|
|
206
|
+
* alongside the encrypted artifact (or wherever else makes sense). Pass
|
|
207
|
+
* those same parameters here for verification.
|
|
208
|
+
*
|
|
209
|
+
* Re-derives a key from `password` + `keyDerivation`, then compares it to
|
|
210
|
+
* the stored key material in constant time. Restricted to entries of type
|
|
211
|
+
* `'encryption-key'` — the type produced by `addSecretFromPassword`. Other
|
|
212
|
+
* symmetric types (`'api-key'`) and asymmetric entries are rejected so
|
|
213
|
+
* the boolean result reflects "this slot accepts this password" rather
|
|
214
|
+
* than an incidental byte-equality match against unrelated material.
|
|
215
|
+
*
|
|
216
|
+
* Note: the keystore does not currently flag whether an `'encryption-key'`
|
|
217
|
+
* entry was actually password-derived (vs. random via `addSecret` or raw
|
|
218
|
+
* via `importSecret`). A `true` result therefore means "the candidate
|
|
219
|
+
* password produces the same 32 bytes currently stored", which is what
|
|
220
|
+
* the equivalent consumer-side helper (`verifyGatePassword`) already
|
|
221
|
+
* implies for entries it manages.
|
|
222
|
+
*
|
|
223
|
+
* @param name - Name of the secret to verify against
|
|
224
|
+
* @param password - Candidate password to test
|
|
225
|
+
* @param keyDerivation - The key derivation parameters returned by
|
|
226
|
+
* `addSecretFromPassword` when the secret was created. Only
|
|
227
|
+
* `kdf: 'pbkdf2'` is supported.
|
|
228
|
+
* @returns Success(true) when the candidate matches the stored key,
|
|
229
|
+
* Success(false) when it does not, Failure if locked, secret missing,
|
|
230
|
+
* wrong type, unsupported `kdf`, or key derivation fails
|
|
231
|
+
* @public
|
|
232
|
+
*/
|
|
233
|
+
verifySecretFromPassword(name: string, password: string, keyDerivation: IKeyDerivationParams): Promise<Result<boolean>>;
|
|
199
234
|
/**
|
|
200
235
|
* Removes a secret by name. Vault-first: the in-memory vault entry is dropped
|
|
201
236
|
* before any storage cleanup runs. For asymmetric-keypair entries, best-effort
|
|
@@ -342,6 +377,13 @@ export declare class KeyStore implements IEncryptionProvider {
|
|
|
342
377
|
* @returns A warning string if storage cleanup failed, otherwise undefined.
|
|
343
378
|
*/
|
|
344
379
|
private _releaseEntryResources;
|
|
380
|
+
/**
|
|
381
|
+
* Constant-time byte comparison. Returns false immediately for length
|
|
382
|
+
* mismatch (length is not secret); for equal-length inputs, walks the full
|
|
383
|
+
* buffer accumulating differences via XOR so the running time does not leak
|
|
384
|
+
* the position of the first differing byte.
|
|
385
|
+
*/
|
|
386
|
+
private static _timingSafeEqual;
|
|
345
387
|
/**
|
|
346
388
|
* Mints a fresh UUID v4 storage handle using the crypto provider's
|
|
347
389
|
* {@link CryptoUtils.ICryptoProvider.generateRandomBytes | generateRandomBytes}.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keyStore.d.ts","sourceRoot":"","sources":["../../../../src/packlets/crypto-utils/keystore/keyStore.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"keyStore.d.ts","sourceRoot":"","sources":["../../../../src/packlets/crypto-utils/keystore/keyStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -505,6 +505,68 @@ class KeyStore {
|
|
|
505
505
|
}
|
|
506
506
|
});
|
|
507
507
|
}
|
|
508
|
+
/**
|
|
509
|
+
* Verifies that a candidate password derives the same key material currently
|
|
510
|
+
* stored under `name`, using the supplied
|
|
511
|
+
* {@link CryptoUtils.IKeyDerivationParams | key derivation parameters}.
|
|
512
|
+
*
|
|
513
|
+
* The keystore does not persist per-slot key derivation parameters with the
|
|
514
|
+
* entry — callers receive them from `addSecretFromPassword` and store them
|
|
515
|
+
* alongside the encrypted artifact (or wherever else makes sense). Pass
|
|
516
|
+
* those same parameters here for verification.
|
|
517
|
+
*
|
|
518
|
+
* Re-derives a key from `password` + `keyDerivation`, then compares it to
|
|
519
|
+
* the stored key material in constant time. Restricted to entries of type
|
|
520
|
+
* `'encryption-key'` — the type produced by `addSecretFromPassword`. Other
|
|
521
|
+
* symmetric types (`'api-key'`) and asymmetric entries are rejected so
|
|
522
|
+
* the boolean result reflects "this slot accepts this password" rather
|
|
523
|
+
* than an incidental byte-equality match against unrelated material.
|
|
524
|
+
*
|
|
525
|
+
* Note: the keystore does not currently flag whether an `'encryption-key'`
|
|
526
|
+
* entry was actually password-derived (vs. random via `addSecret` or raw
|
|
527
|
+
* via `importSecret`). A `true` result therefore means "the candidate
|
|
528
|
+
* password produces the same 32 bytes currently stored", which is what
|
|
529
|
+
* the equivalent consumer-side helper (`verifyGatePassword`) already
|
|
530
|
+
* implies for entries it manages.
|
|
531
|
+
*
|
|
532
|
+
* @param name - Name of the secret to verify against
|
|
533
|
+
* @param password - Candidate password to test
|
|
534
|
+
* @param keyDerivation - The key derivation parameters returned by
|
|
535
|
+
* `addSecretFromPassword` when the secret was created. Only
|
|
536
|
+
* `kdf: 'pbkdf2'` is supported.
|
|
537
|
+
* @returns Success(true) when the candidate matches the stored key,
|
|
538
|
+
* Success(false) when it does not, Failure if locked, secret missing,
|
|
539
|
+
* wrong type, unsupported `kdf`, or key derivation fails
|
|
540
|
+
* @public
|
|
541
|
+
*/
|
|
542
|
+
async verifySecretFromPassword(name, password, keyDerivation) {
|
|
543
|
+
if (!this._secrets) {
|
|
544
|
+
return (0, ts_utils_1.fail)('Key store is locked');
|
|
545
|
+
}
|
|
546
|
+
if (!password || password.length === 0) {
|
|
547
|
+
return (0, ts_utils_1.fail)('Password cannot be empty');
|
|
548
|
+
}
|
|
549
|
+
if (keyDerivation.kdf !== 'pbkdf2') {
|
|
550
|
+
return (0, ts_utils_1.fail)(`Unsupported kdf '${keyDerivation.kdf}' (expected 'pbkdf2')`);
|
|
551
|
+
}
|
|
552
|
+
const entry = this._secrets.get(name);
|
|
553
|
+
if (!entry) {
|
|
554
|
+
return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
|
|
555
|
+
}
|
|
556
|
+
if (entry.type !== 'encryption-key') {
|
|
557
|
+
return (0, ts_utils_1.fail)(`Secret '${name}' is not a password-verifiable encryption key (type: ${entry.type})`);
|
|
558
|
+
}
|
|
559
|
+
const saltResult = this._cryptoProvider.fromBase64(keyDerivation.salt);
|
|
560
|
+
if (saltResult.isFailure()) {
|
|
561
|
+
return (0, ts_utils_1.fail)(`Invalid salt: ${saltResult.message}`);
|
|
562
|
+
}
|
|
563
|
+
const derivedResult = await this._cryptoProvider.deriveKey(password, saltResult.value, keyDerivation.iterations);
|
|
564
|
+
/* c8 ignore next 3 - crypto provider errors covered in nodeCryptoProvider tests */
|
|
565
|
+
if (derivedResult.isFailure()) {
|
|
566
|
+
return (0, ts_utils_1.fail)(`Key derivation failed: ${derivedResult.message}`);
|
|
567
|
+
}
|
|
568
|
+
return (0, ts_utils_1.succeed)(KeyStore._timingSafeEqual(derivedResult.value, entry.key));
|
|
569
|
+
}
|
|
508
570
|
/**
|
|
509
571
|
* Removes a secret by name. Vault-first: the in-memory vault entry is dropped
|
|
510
572
|
* before any storage cleanup runs. For asymmetric-keypair entries, best-effort
|
|
@@ -1085,6 +1147,25 @@ class KeyStore {
|
|
|
1085
1147
|
entry.key.fill(0);
|
|
1086
1148
|
return undefined;
|
|
1087
1149
|
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Constant-time byte comparison. Returns false immediately for length
|
|
1152
|
+
* mismatch (length is not secret); for equal-length inputs, walks the full
|
|
1153
|
+
* buffer accumulating differences via XOR so the running time does not leak
|
|
1154
|
+
* the position of the first differing byte.
|
|
1155
|
+
*/
|
|
1156
|
+
static _timingSafeEqual(a, b) {
|
|
1157
|
+
/* c8 ignore next 3 - defensive: callers in this class only compare
|
|
1158
|
+
PBKDF2-derived 32-byte keys against encryption-key entries (also 32 bytes) */
|
|
1159
|
+
if (a.length !== b.length) {
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
let diff = 0;
|
|
1163
|
+
for (let i = 0; i < a.length; i++) {
|
|
1164
|
+
// eslint-disable-next-line no-bitwise
|
|
1165
|
+
diff |= a[i] ^ b[i];
|
|
1166
|
+
}
|
|
1167
|
+
return diff === 0;
|
|
1168
|
+
}
|
|
1088
1169
|
/**
|
|
1089
1170
|
* Mints a fresh UUID v4 storage handle using the crypto provider's
|
|
1090
1171
|
* {@link CryptoUtils.ICryptoProvider.generateRandomBytes | generateRandomBytes}.
|