@fgv/ts-extras 5.0.2 → 5.1.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/index.browser.js +6 -2
  2. package/dist/index.js +5 -1
  3. package/dist/packlets/ai-assist/apiClient.js +484 -0
  4. package/dist/packlets/ai-assist/converters.js +121 -0
  5. package/dist/packlets/ai-assist/index.js +10 -0
  6. package/dist/packlets/ai-assist/model.js +90 -0
  7. package/dist/packlets/ai-assist/registry.js +145 -0
  8. package/dist/packlets/ai-assist/toolFormats.js +160 -0
  9. package/dist/packlets/crypto-utils/constants.js +48 -0
  10. package/dist/packlets/crypto-utils/converters.js +155 -0
  11. package/dist/packlets/crypto-utils/directEncryptionProvider.js +86 -0
  12. package/dist/packlets/crypto-utils/encryptedFile.js +161 -0
  13. package/dist/packlets/crypto-utils/index.browser.js +41 -0
  14. package/dist/packlets/crypto-utils/index.js +41 -0
  15. package/dist/packlets/crypto-utils/keystore/converters.js +84 -0
  16. package/dist/packlets/crypto-utils/keystore/index.js +31 -0
  17. package/dist/packlets/crypto-utils/keystore/keyStore.js +758 -0
  18. package/dist/packlets/crypto-utils/keystore/model.js +64 -0
  19. package/dist/packlets/crypto-utils/model.js +39 -0
  20. package/dist/packlets/crypto-utils/nodeCryptoProvider.js +159 -0
  21. package/dist/packlets/experimental/formatter.js +1 -1
  22. package/dist/packlets/mustache/index.js +23 -0
  23. package/dist/packlets/mustache/interfaces.js +25 -0
  24. package/dist/packlets/mustache/mustacheTemplate.js +242 -0
  25. package/dist/packlets/record-jar/recordJarHelpers.js +1 -1
  26. package/dist/packlets/yaml/converters.js +46 -0
  27. package/dist/packlets/yaml/index.js +23 -0
  28. package/dist/packlets/zip-file-tree/index.js +1 -0
  29. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +43 -2
  30. package/dist/packlets/zip-file-tree/zipFileTreeWriter.js +40 -0
  31. package/dist/ts-extras.d.ts +1990 -112
  32. package/dist/tsdoc-metadata.json +1 -1
  33. package/lib/index.browser.d.ts +3 -1
  34. package/lib/index.browser.js +6 -1
  35. package/lib/index.d.ts +5 -1
  36. package/lib/index.js +9 -1
  37. package/lib/packlets/ai-assist/apiClient.d.ts +60 -0
  38. package/lib/packlets/ai-assist/apiClient.js +488 -0
  39. package/lib/packlets/ai-assist/converters.d.ts +55 -0
  40. package/lib/packlets/ai-assist/converters.js +124 -0
  41. package/lib/packlets/ai-assist/index.d.ts +10 -0
  42. package/lib/packlets/ai-assist/index.js +33 -0
  43. package/lib/packlets/ai-assist/model.d.ts +222 -0
  44. package/lib/packlets/ai-assist/model.js +95 -0
  45. package/lib/packlets/ai-assist/registry.d.ts +25 -0
  46. package/lib/packlets/ai-assist/registry.js +150 -0
  47. package/lib/packlets/ai-assist/toolFormats.d.ts +44 -0
  48. package/lib/packlets/ai-assist/toolFormats.js +166 -0
  49. package/lib/packlets/crypto-utils/constants.d.ts +26 -0
  50. package/lib/packlets/crypto-utils/constants.js +51 -0
  51. package/lib/packlets/crypto-utils/converters.d.ts +58 -0
  52. package/lib/packlets/crypto-utils/converters.js +192 -0
  53. package/lib/packlets/crypto-utils/directEncryptionProvider.d.ts +69 -0
  54. package/lib/packlets/crypto-utils/directEncryptionProvider.js +90 -0
  55. package/lib/packlets/crypto-utils/encryptedFile.d.ts +88 -0
  56. package/lib/packlets/crypto-utils/encryptedFile.js +201 -0
  57. package/lib/packlets/crypto-utils/index.browser.d.ts +14 -0
  58. package/lib/packlets/crypto-utils/index.browser.js +91 -0
  59. package/lib/packlets/crypto-utils/index.d.ts +15 -0
  60. package/lib/packlets/crypto-utils/index.js +88 -0
  61. package/lib/packlets/crypto-utils/keystore/converters.d.ts +29 -0
  62. package/lib/packlets/crypto-utils/keystore/converters.js +87 -0
  63. package/lib/packlets/crypto-utils/keystore/index.d.ts +9 -0
  64. package/lib/packlets/crypto-utils/keystore/index.js +71 -0
  65. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +239 -0
  66. package/lib/packlets/crypto-utils/keystore/keyStore.js +795 -0
  67. package/lib/packlets/crypto-utils/keystore/model.d.ts +245 -0
  68. package/lib/packlets/crypto-utils/keystore/model.js +68 -0
  69. package/lib/packlets/crypto-utils/model.d.ts +236 -0
  70. package/lib/packlets/crypto-utils/model.js +76 -0
  71. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +62 -0
  72. package/lib/packlets/crypto-utils/nodeCryptoProvider.js +196 -0
  73. package/lib/packlets/experimental/formatter.d.ts +1 -1
  74. package/lib/packlets/experimental/formatter.js +1 -1
  75. package/lib/packlets/mustache/index.d.ts +3 -0
  76. package/lib/packlets/mustache/index.js +27 -0
  77. package/lib/packlets/mustache/interfaces.d.ts +97 -0
  78. package/lib/packlets/mustache/interfaces.js +26 -0
  79. package/lib/packlets/mustache/mustacheTemplate.d.ts +76 -0
  80. package/lib/packlets/mustache/mustacheTemplate.js +249 -0
  81. package/lib/packlets/record-jar/recordJarHelpers.js +1 -1
  82. package/lib/packlets/yaml/converters.d.ts +9 -0
  83. package/lib/packlets/yaml/converters.js +82 -0
  84. package/lib/packlets/yaml/index.d.ts +2 -0
  85. package/lib/packlets/yaml/index.js +39 -0
  86. package/lib/packlets/zip-file-tree/index.d.ts +1 -0
  87. package/lib/packlets/zip-file-tree/index.js +15 -0
  88. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +31 -2
  89. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +42 -1
  90. package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts +27 -0
  91. package/lib/packlets/zip-file-tree/zipFileTreeWriter.js +43 -0
  92. package/package.json +37 -18
@@ -0,0 +1,90 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ // ============================================================================
21
+ // AiPrompt
22
+ // ============================================================================
23
+ /**
24
+ * A structured AI prompt with system/user split for direct API calls,
25
+ * and a lazily-constructed combined version for copy/paste workflows.
26
+ * @public
27
+ */
28
+ export class AiPrompt {
29
+ constructor(user, system) {
30
+ this.system = system;
31
+ this.user = user;
32
+ }
33
+ /** Combined single-string version (user + system joined) for copy/paste. */
34
+ get combined() {
35
+ return `${this.user}\n\n${this.system}`;
36
+ }
37
+ }
38
+ /**
39
+ * All valid {@link ModelSpecKey} values.
40
+ * @public
41
+ */
42
+ export const allModelSpecKeys = ['base', 'tools', 'image'];
43
+ /**
44
+ * Default context key used as fallback when resolving a {@link ModelSpec}.
45
+ * @public
46
+ */
47
+ export const MODEL_SPEC_BASE_KEY = 'base';
48
+ /**
49
+ * Resolves a {@link ModelSpec} to a concrete model string given an optional context key.
50
+ *
51
+ * @remarks
52
+ * Resolution rules:
53
+ * 1. If the spec is a string, return it directly (context is irrelevant).
54
+ * 2. If the spec is an object and the context key exists, recurse into that branch.
55
+ * 3. Otherwise, fall back to the {@link MODEL_SPEC_BASE_KEY | 'base'} key.
56
+ * 4. If neither context nor `'base'` exists, use the first available value.
57
+ *
58
+ * @param spec - The model specification to resolve
59
+ * @param context - Optional context key (e.g. `'tools'`)
60
+ * @returns The resolved model string
61
+ * @public
62
+ */
63
+ export function resolveModel(spec, context) {
64
+ if (typeof spec === 'string') {
65
+ return spec;
66
+ }
67
+ // Try the requested context key first
68
+ if (context !== undefined && context in spec) {
69
+ return resolveModel(spec[context]);
70
+ }
71
+ // Fall back to 'base'
72
+ if (MODEL_SPEC_BASE_KEY in spec) {
73
+ return resolveModel(spec[MODEL_SPEC_BASE_KEY]);
74
+ }
75
+ // Last resort: first value in the record
76
+ const first = Object.values(spec)[0];
77
+ /* c8 ignore next 3 - defensive: only reachable with empty object (prevented by converter) */
78
+ if (first === undefined) {
79
+ return '';
80
+ }
81
+ return resolveModel(first);
82
+ }
83
+ /**
84
+ * Default AI assist settings (copy-paste only).
85
+ * @public
86
+ */
87
+ export const DEFAULT_AI_ASSIST = {
88
+ providers: [{ provider: 'copy-paste' }]
89
+ };
90
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1,145 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ /**
21
+ * Centralized provider registry — single source of truth for all AI provider metadata.
22
+ * @packageDocumentation
23
+ */
24
+ import { fail, succeed } from '@fgv/ts-utils';
25
+ // ============================================================================
26
+ // Built-in providers
27
+ // ============================================================================
28
+ /**
29
+ * All known AI provider descriptors. Copy-paste first, then alphabetical.
30
+ * @internal
31
+ */
32
+ const BUILTIN_PROVIDERS = [
33
+ {
34
+ id: 'copy-paste',
35
+ label: 'Copy / Paste',
36
+ buttonLabel: 'AI Assist | Copy',
37
+ needsSecret: false,
38
+ apiFormat: 'openai',
39
+ baseUrl: '',
40
+ defaultModel: '',
41
+ supportedTools: [],
42
+ corsRestricted: false
43
+ },
44
+ {
45
+ id: 'anthropic',
46
+ label: 'Anthropic Claude',
47
+ buttonLabel: 'AI Assist | Claude',
48
+ needsSecret: true,
49
+ apiFormat: 'anthropic',
50
+ baseUrl: 'https://api.anthropic.com/v1',
51
+ defaultModel: 'claude-sonnet-4-5-20250929',
52
+ supportedTools: ['web_search'],
53
+ corsRestricted: false
54
+ },
55
+ {
56
+ id: 'google-gemini',
57
+ label: 'Google Gemini',
58
+ buttonLabel: 'AI Assist | Gemini',
59
+ needsSecret: true,
60
+ apiFormat: 'gemini',
61
+ baseUrl: 'https://generativelanguage.googleapis.com/v1beta',
62
+ defaultModel: 'gemini-2.5-flash',
63
+ supportedTools: ['web_search'],
64
+ corsRestricted: false
65
+ },
66
+ {
67
+ id: 'groq',
68
+ label: 'Groq',
69
+ buttonLabel: 'AI Assist | Groq',
70
+ needsSecret: true,
71
+ apiFormat: 'openai',
72
+ baseUrl: 'https://api.groq.com/openai/v1',
73
+ defaultModel: 'llama-3.3-70b-versatile',
74
+ supportedTools: [],
75
+ corsRestricted: false
76
+ },
77
+ {
78
+ id: 'mistral',
79
+ label: 'Mistral',
80
+ buttonLabel: 'AI Assist | Mistral',
81
+ needsSecret: true,
82
+ apiFormat: 'openai',
83
+ baseUrl: 'https://api.mistral.ai/v1',
84
+ defaultModel: 'mistral-large-latest',
85
+ supportedTools: [],
86
+ corsRestricted: false
87
+ },
88
+ {
89
+ id: 'openai',
90
+ label: 'OpenAI',
91
+ buttonLabel: 'AI Assist | OpenAI',
92
+ needsSecret: true,
93
+ apiFormat: 'openai',
94
+ baseUrl: 'https://api.openai.com/v1',
95
+ defaultModel: 'gpt-4o',
96
+ supportedTools: ['web_search'],
97
+ corsRestricted: false
98
+ },
99
+ {
100
+ id: 'xai-grok',
101
+ label: 'xAI Grok',
102
+ buttonLabel: 'AI Assist | Grok',
103
+ needsSecret: true,
104
+ apiFormat: 'openai',
105
+ baseUrl: 'https://api.x.ai/v1',
106
+ defaultModel: { base: 'grok-4-1-fast', tools: 'grok-4-1-fast-reasoning' },
107
+ supportedTools: ['web_search'],
108
+ corsRestricted: true
109
+ }
110
+ ];
111
+ /**
112
+ * Index for O(1) lookup by id.
113
+ * @internal
114
+ */
115
+ const PROVIDER_BY_ID = new Map(BUILTIN_PROVIDERS.map((d) => [d.id, d]));
116
+ // ============================================================================
117
+ // Public API
118
+ // ============================================================================
119
+ /**
120
+ * All valid provider ID values, in the same order as the registry.
121
+ * @public
122
+ */
123
+ export const allProviderIds = BUILTIN_PROVIDERS.map((d) => d.id);
124
+ /**
125
+ * Get all known provider descriptors. Copy-paste first, then alphabetical.
126
+ * @returns All built-in provider descriptors
127
+ * @public
128
+ */
129
+ export function getProviderDescriptors() {
130
+ return BUILTIN_PROVIDERS;
131
+ }
132
+ /**
133
+ * Get a provider descriptor by id.
134
+ * @param id - The provider identifier
135
+ * @returns The descriptor, or a failure if the provider is unknown
136
+ * @public
137
+ */
138
+ export function getProviderDescriptor(id) {
139
+ const descriptor = PROVIDER_BY_ID.get(id);
140
+ if (!descriptor) {
141
+ return fail(`unknown AI provider: ${id}`);
142
+ }
143
+ return succeed(descriptor);
144
+ }
145
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,160 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ // ============================================================================
21
+ // Tool resolution
22
+ // ============================================================================
23
+ /**
24
+ * Resolves the effective tools for a completion call.
25
+ *
26
+ * - If per-call tools are provided, they override settings-level tools entirely.
27
+ * - Otherwise, settings-level enabled tools are used.
28
+ * - Only tools supported by the provider are included.
29
+ * - Returns an empty array if no tools are enabled (= no tools sent).
30
+ *
31
+ * @param descriptor - The provider descriptor (used to filter by supported tools)
32
+ * @param settingsTools - Tool enablement from provider settings (optional)
33
+ * @param perCallTools - Per-call tool override (optional)
34
+ * @returns The resolved list of tool configs to include in the request
35
+ * @public
36
+ */
37
+ export function resolveEffectiveTools(descriptor, settingsTools, perCallTools) {
38
+ const supported = new Set(descriptor.supportedTools);
39
+ if (perCallTools !== undefined) {
40
+ return perCallTools.filter((t) => supported.has(t.type));
41
+ }
42
+ if (settingsTools === undefined) {
43
+ return [];
44
+ }
45
+ return settingsTools
46
+ .filter((e) => e.enabled && supported.has(e.type))
47
+ .map((e) => { var _a; return (_a = e.config) !== null && _a !== void 0 ? _a : { type: e.type }; });
48
+ }
49
+ // ============================================================================
50
+ // OpenAI / xAI Responses API format
51
+ // ============================================================================
52
+ /**
53
+ * Formats a web search tool config for the xAI/OpenAI Responses API.
54
+ * @internal
55
+ */
56
+ function webSearchToResponsesApi(config) {
57
+ const tool = { type: 'web_search' };
58
+ if (config.allowedDomains || config.blockedDomains) {
59
+ const filters = {};
60
+ if (config.allowedDomains) {
61
+ filters.allowed_domains = [...config.allowedDomains];
62
+ }
63
+ if (config.blockedDomains) {
64
+ filters.excluded_domains = [...config.blockedDomains];
65
+ }
66
+ tool.filters = filters;
67
+ }
68
+ if (config.enableImageUnderstanding) {
69
+ tool.enable_image_understanding = true;
70
+ }
71
+ return tool;
72
+ }
73
+ /**
74
+ * Formats tool configs for the xAI/OpenAI Responses API.
75
+ * @param tools - The resolved tool configs
76
+ * @returns Provider-native tool objects for the `tools` request field
77
+ * @public
78
+ */
79
+ export function toResponsesApiTools(tools) {
80
+ return tools.map((t) => {
81
+ switch (t.type) {
82
+ case 'web_search':
83
+ return webSearchToResponsesApi(t);
84
+ /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
85
+ default: {
86
+ const _exhaustive = t.type;
87
+ return { type: String(_exhaustive) };
88
+ }
89
+ }
90
+ });
91
+ }
92
+ // ============================================================================
93
+ // Anthropic Messages API format
94
+ // ============================================================================
95
+ /**
96
+ * Formats a web search tool config for the Anthropic Messages API.
97
+ * @internal
98
+ */
99
+ function webSearchToAnthropic(config) {
100
+ const tool = {
101
+ type: 'web_search_20250305',
102
+ name: 'web_search'
103
+ };
104
+ if (config.maxUses !== undefined) {
105
+ tool.max_uses = config.maxUses;
106
+ }
107
+ if (config.allowedDomains) {
108
+ tool.allowed_domains = [...config.allowedDomains];
109
+ }
110
+ if (config.blockedDomains) {
111
+ tool.blocked_domains = [...config.blockedDomains];
112
+ }
113
+ return tool;
114
+ }
115
+ /**
116
+ * Formats tool configs for the Anthropic Messages API.
117
+ * @param tools - The resolved tool configs
118
+ * @returns Provider-native tool objects for the `tools` request field
119
+ * @public
120
+ */
121
+ export function toAnthropicTools(tools) {
122
+ return tools.map((t) => {
123
+ switch (t.type) {
124
+ case 'web_search':
125
+ return webSearchToAnthropic(t);
126
+ /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
127
+ default: {
128
+ const _exhaustive = t.type;
129
+ return { type: String(_exhaustive) };
130
+ }
131
+ }
132
+ });
133
+ }
134
+ // ============================================================================
135
+ // Gemini generateContent API format
136
+ // ============================================================================
137
+ /**
138
+ * Formats tool configs for the Gemini generateContent API.
139
+ * Gemini uses `google_search` for search grounding — no per-tool config options.
140
+ * @param tools - The resolved tool configs
141
+ * @returns Provider-native tool objects for the `tools` request field
142
+ * @public
143
+ */
144
+ export function toGeminiTools(tools) {
145
+ const result = [];
146
+ for (const t of tools) {
147
+ switch (t.type) {
148
+ case 'web_search':
149
+ result.push({ google_search: {} });
150
+ break;
151
+ /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
152
+ default: {
153
+ const _exhaustive = t.type;
154
+ result.push({ type: String(_exhaustive) });
155
+ }
156
+ }
157
+ }
158
+ return result;
159
+ }
160
+ //# sourceMappingURL=toolFormats.js.map
@@ -0,0 +1,48 @@
1
+ // Copyright (c) 2024 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ // ============================================================================
21
+ // Constants
22
+ // ============================================================================
23
+ /**
24
+ * Current format version for encrypted files.
25
+ * @public
26
+ */
27
+ export const ENCRYPTED_FILE_FORMAT = 'encrypted-collection-v1';
28
+ /**
29
+ * Default encryption algorithm.
30
+ * @public
31
+ */
32
+ export const DEFAULT_ALGORITHM = 'AES-256-GCM';
33
+ /**
34
+ * Key size in bytes for AES-256.
35
+ * @public
36
+ */
37
+ export const AES_256_KEY_SIZE = 32;
38
+ /**
39
+ * IV size in bytes for GCM mode.
40
+ * @public
41
+ */
42
+ export const GCM_IV_SIZE = 12;
43
+ /**
44
+ * Auth tag size in bytes for GCM mode.
45
+ * @public
46
+ */
47
+ export const GCM_AUTH_TAG_SIZE = 16;
48
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1,155 @@
1
+ // Copyright (c) 2024 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ import { Converters as JsonConverters } from '@fgv/ts-json-base';
21
+ import { Converters, fail, succeed } from '@fgv/ts-utils';
22
+ import * as Constants from './constants';
23
+ // ============================================================================
24
+ // Base Converters
25
+ // ============================================================================
26
+ /**
27
+ * Converter for {@link CryptoUtils.EncryptionAlgorithm | encryption algorithm} values.
28
+ * @public
29
+ */
30
+ export const encryptionAlgorithm = Converters.enumeratedValue([Constants.DEFAULT_ALGORITHM]);
31
+ /**
32
+ * Converter for {@link CryptoUtils.EncryptedFileFormat | encrypted file format} version.
33
+ * @public
34
+ */
35
+ export const encryptedFileFormat = Converters.enumeratedValue([
36
+ Constants.ENCRYPTED_FILE_FORMAT
37
+ ]);
38
+ /**
39
+ * Converter for {@link CryptoUtils.EncryptedFileErrorMode | encrypted file error mode}.
40
+ * @public
41
+ */
42
+ export const encryptedFileErrorMode = Converters.enumeratedValue(['fail', 'skip', 'warn']);
43
+ /**
44
+ * Converter for {@link CryptoUtils.KeyDerivationFunction | key derivation function} type.
45
+ * @public
46
+ */
47
+ export const keyDerivationFunction = Converters.enumeratedValue(['pbkdf2']);
48
+ /**
49
+ * Converter for {@link CryptoUtils.IKeyDerivationParams | key derivation parameters}.
50
+ * @public
51
+ */
52
+ export const keyDerivationParams = Converters.object({
53
+ kdf: keyDerivationFunction,
54
+ salt: Converters.string,
55
+ iterations: Converters.number
56
+ });
57
+ /**
58
+ * Converter for base64 strings (validates format).
59
+ * @public
60
+ */
61
+ export const base64String = Converters.string.withConstraint((value) => {
62
+ // Basic base64 validation - check for valid characters and padding
63
+ const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
64
+ if (!base64Regex.test(value)) {
65
+ return fail('Invalid base64 encoding');
66
+ }
67
+ return succeed(value);
68
+ });
69
+ // ============================================================================
70
+ // Uint8Array Converter
71
+ // ============================================================================
72
+ /**
73
+ * Converter which converts a base64 string to a Uint8Array.
74
+ * @public
75
+ */
76
+ export const uint8ArrayFromBase64 = Converters.string.map((base64) => {
77
+ try {
78
+ // Use Buffer in Node.js environment, atob in browser
79
+ if (typeof Buffer !== 'undefined') {
80
+ return succeed(Uint8Array.from(Buffer.from(base64, 'base64')));
81
+ }
82
+ /* c8 ignore start - Browser-only fallback cannot be tested in Node.js environment */
83
+ const binaryString = atob(base64);
84
+ const bytes = new Uint8Array(binaryString.length);
85
+ for (let i = 0; i < binaryString.length; i++) {
86
+ bytes[i] = binaryString.charCodeAt(i);
87
+ }
88
+ return succeed(bytes);
89
+ }
90
+ catch (e) {
91
+ // This catch is for browser's atob() which throws on invalid base64
92
+ // Node's Buffer.from() doesn't throw, it ignores invalid characters
93
+ const message = e instanceof Error ? e.message : String(e);
94
+ return fail(`Invalid base64: ${message}`);
95
+ }
96
+ /* c8 ignore stop */
97
+ });
98
+ // ============================================================================
99
+ // Named Secret Converter
100
+ // ============================================================================
101
+ /**
102
+ * Converter for {@link CryptoUtils.INamedSecret | named secret} from JSON representation.
103
+ * Expects key as base64 string in JSON, converts to Uint8Array.
104
+ * @public
105
+ */
106
+ export const namedSecret = Converters.object({
107
+ name: Converters.string,
108
+ key: uint8ArrayFromBase64
109
+ });
110
+ /**
111
+ * Base converter for encrypted file structure (without typed metadata).
112
+ */
113
+ const baseEncryptedFileConverter = Converters.object({
114
+ format: encryptedFileFormat,
115
+ secretName: Converters.string,
116
+ algorithm: encryptionAlgorithm,
117
+ iv: base64String,
118
+ authTag: base64String,
119
+ encryptedData: base64String,
120
+ keyDerivation: keyDerivationParams,
121
+ metadata: JsonConverters.jsonValue
122
+ }, { optionalFields: ['keyDerivation', 'metadata'] });
123
+ /**
124
+ * Creates a converter for {@link CryptoUtils.IEncryptedFile | encrypted files} with optional typed metadata.
125
+ * @typeParam TMetadata - Type of optional unencrypted metadata
126
+ * @param metadataConverter - Optional converter for validating metadata field
127
+ * @returns A converter that validates and converts encrypted file structures
128
+ * @public
129
+ */
130
+ export function createEncryptedFileConverter(metadataConverter) {
131
+ return Converters.generic((from) => {
132
+ // First validate base structure
133
+ const baseResult = baseEncryptedFileConverter.convert(from);
134
+ if (baseResult.isFailure()) {
135
+ return fail(baseResult.message);
136
+ }
137
+ const base = baseResult.value;
138
+ // Validate metadata with specific converter if provided and metadata exists
139
+ if (metadataConverter !== undefined && base.metadata !== undefined) {
140
+ const metaResult = metadataConverter.convert(base.metadata);
141
+ if (metaResult.isFailure()) {
142
+ return fail(`Invalid metadata: ${metaResult.message}`);
143
+ }
144
+ return succeed(Object.assign(Object.assign({}, base), { metadata: metaResult.value }));
145
+ }
146
+ // Return as-is (metadata is either undefined or untyped JsonValue)
147
+ return succeed(base);
148
+ });
149
+ }
150
+ /**
151
+ * Default converter for encrypted files without typed metadata.
152
+ * @public
153
+ */
154
+ export const encryptedFile = createEncryptedFileConverter();
155
+ //# sourceMappingURL=converters.js.map
@@ -0,0 +1,86 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ import { fail, succeed } from '@fgv/ts-utils';
21
+ import { createEncryptedFile } from './encryptedFile';
22
+ /**
23
+ * An {@link IEncryptionProvider} that uses a pre-supplied key and crypto provider.
24
+ *
25
+ * This is useful when you have the raw encryption key from an external source
26
+ * (e.g. a `SecretProvider` callback, password derivation, or a one-shot
27
+ * operation) and don't want to open a full KeyStore.
28
+ *
29
+ * Optionally bound to a specific secret name for safety: if a `boundSecretName`
30
+ * is provided, calls to `encryptByName` with a different name will fail.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const provider = DirectEncryptionProvider.create({
35
+ * cryptoProvider: nodeCryptoProvider,
36
+ * key: myKey,
37
+ * boundSecretName: 'my-collection'
38
+ * }).orThrow();
39
+ *
40
+ * const encrypted = await provider.encryptByName('my-collection', jsonContent);
41
+ * ```
42
+ *
43
+ * @public
44
+ */
45
+ export class DirectEncryptionProvider {
46
+ constructor(params) {
47
+ this._cryptoProvider = params.cryptoProvider;
48
+ this._key = params.key;
49
+ this._boundSecretName = params.boundSecretName;
50
+ }
51
+ /**
52
+ * Creates a new DirectEncryptionProvider.
53
+ * @param params - Provider configuration
54
+ * @returns Success with provider, or Failure if parameters are invalid
55
+ * @public
56
+ */
57
+ static create(params) {
58
+ if (params.key.length === 0) {
59
+ return fail('Encryption key cannot be empty');
60
+ }
61
+ return succeed(new DirectEncryptionProvider(params));
62
+ }
63
+ /**
64
+ * The secret name this provider is bound to, if any.
65
+ * @public
66
+ */
67
+ get boundSecretName() {
68
+ return this._boundSecretName;
69
+ }
70
+ /**
71
+ * {@inheritDoc IEncryptionProvider.encryptByName}
72
+ */
73
+ async encryptByName(secretName, content, metadata) {
74
+ if (this._boundSecretName !== undefined && secretName !== this._boundSecretName) {
75
+ return fail(`Secret name mismatch: requested '${secretName}' but provider is bound to '${this._boundSecretName}'`);
76
+ }
77
+ return createEncryptedFile({
78
+ content,
79
+ secretName,
80
+ key: this._key,
81
+ cryptoProvider: this._cryptoProvider,
82
+ metadata
83
+ });
84
+ }
85
+ }
86
+ //# sourceMappingURL=directEncryptionProvider.js.map