yamlock 0.2.9 → 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.
@@ -1,5 +1,11 @@
1
1
  import { createHash, getCipherInfo, getCiphers, randomBytes } from 'node:crypto';
2
2
 
3
+ import {
4
+ YAMLOCK_ERROR_CODES,
5
+ YamlockPayloadError,
6
+ YamlockValidationError
7
+ } from '../errors.js';
8
+
3
9
  export const YAMLOCK_PREFIX = 'yl';
4
10
  export const YAMLOCK_DELIMITER = '|';
5
11
  export const DEFAULT_ALGORITHM = 'aes-256-cbc';
@@ -26,20 +32,32 @@ export function listSupportedAlgorithms() {
26
32
  */
27
33
  export function ensureAlgorithm(algorithm) {
28
34
  if (!algorithm) {
29
- throw new Error('Encryption algorithm is required.');
35
+ throw new YamlockValidationError('Encryption algorithm is required.', {
36
+ code: YAMLOCK_ERROR_CODES.UNSUPPORTED_ALGORITHM
37
+ });
30
38
  }
31
39
 
32
40
  if (!listSupportedAlgorithms().includes(algorithm)) {
33
- throw new Error(`Unsupported algorithm: ${algorithm}`);
41
+ throw new YamlockValidationError(`Unsupported algorithm: ${algorithm}`, {
42
+ code: YAMLOCK_ERROR_CODES.UNSUPPORTED_ALGORITHM
43
+ });
34
44
  }
35
45
 
36
46
  return algorithm;
37
47
  }
38
48
 
39
49
  function normalizeAlgorithmInput(input) {
40
- if (!input || typeof input === 'string') {
50
+ if (input === undefined || typeof input === 'string') {
41
51
  return { algorithm: input };
42
52
  }
53
+
54
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
55
+ throw new YamlockValidationError(
56
+ 'Algorithm options must be an object or algorithm string.',
57
+ { code: YAMLOCK_ERROR_CODES.INVALID_OPTIONS }
58
+ );
59
+ }
60
+
43
61
  return input;
44
62
  }
45
63
 
@@ -61,6 +79,19 @@ export function resolveAlgorithmOptions(input) {
61
79
  const ivLength = normalized.ivLength ?? preset.ivLength ?? cipherInfo.ivLength ?? 16;
62
80
  const authTagLength = normalized.authTagLength ?? preset.authTagLength ?? 0;
63
81
 
82
+ for (const [name, value, minimum] of [
83
+ ['keyLength', keyLength, 1],
84
+ ['ivLength', ivLength, 0],
85
+ ['authTagLength', authTagLength, 0]
86
+ ]) {
87
+ if (!Number.isInteger(value) || value < minimum) {
88
+ throw new YamlockValidationError(
89
+ `${name} must be an integer greater than or equal to ${minimum}.`,
90
+ { code: YAMLOCK_ERROR_CODES.INVALID_OPTIONS }
91
+ );
92
+ }
93
+ }
94
+
64
95
  return {
65
96
  algorithm: algorithmName,
66
97
  keyLength,
@@ -76,13 +107,19 @@ export function resolveAlgorithmOptions(input) {
76
107
  * @returns {Buffer}
77
108
  */
78
109
  export function deriveKey(secret, { algorithm, keyLength }) {
79
- if (secret === undefined || secret === null || secret === '') {
80
- throw new Error('Encryption key is required to derive a cipher key.');
110
+ if (
111
+ (!Buffer.isBuffer(secret) && typeof secret !== 'string') ||
112
+ secret.length === 0
113
+ ) {
114
+ throw new YamlockValidationError(
115
+ 'Encryption key must be a non-empty string or Buffer.',
116
+ { code: YAMLOCK_ERROR_CODES.INVALID_KEY }
117
+ );
81
118
  }
82
119
 
83
120
  const baseBuffer = Buffer.isBuffer(secret)
84
121
  ? secret
85
- : Buffer.from(String(secret), 'utf8');
122
+ : Buffer.from(secret, 'utf8');
86
123
 
87
124
  const normalizedAlgorithm = ensureAlgorithm(algorithm);
88
125
  const requiredLength = keyLength ?? getCipherInfo(normalizedAlgorithm)?.keyLength ?? 32;
@@ -129,11 +166,14 @@ export function generateIv({ algorithm, ivLength }) {
129
166
  * @returns {string}
130
167
  */
131
168
  export function encodeFieldPathSalt(fieldPath) {
132
- if (!fieldPath) {
133
- throw new Error('Field path is required to create a salt.');
169
+ if (typeof fieldPath !== 'string' || fieldPath.length === 0) {
170
+ throw new YamlockValidationError(
171
+ 'Field path must be a non-empty string.',
172
+ { code: YAMLOCK_ERROR_CODES.INVALID_FIELD_PATH }
173
+ );
134
174
  }
135
175
 
136
- return Buffer.from(String(fieldPath), 'utf8').toString('base64');
176
+ return Buffer.from(fieldPath, 'utf8').toString('base64');
137
177
  }
138
178
 
139
179
  /**
@@ -143,7 +183,9 @@ export function encodeFieldPathSalt(fieldPath) {
143
183
  */
144
184
  export function decodeFieldPathSalt(salt) {
145
185
  if (!salt) {
146
- throw new Error('Salt value is required.');
186
+ throw new YamlockPayloadError('Salt value is required.', {
187
+ code: YAMLOCK_ERROR_CODES.INVALID_PAYLOAD
188
+ });
147
189
  }
148
190
 
149
191
  return Buffer.from(String(salt), 'base64').toString('utf8');
@@ -160,7 +202,10 @@ export function decodeFieldPathSalt(salt) {
160
202
  */
161
203
  export function formatPayload({ algorithm, salt, iv, data }) {
162
204
  if (!algorithm || !salt || !iv || !data) {
163
- throw new Error('Algorithm, salt, IV, and data are required to format payload.');
205
+ throw new YamlockValidationError(
206
+ 'Algorithm, salt, IV, and data are required to format payload.',
207
+ { code: YAMLOCK_ERROR_CODES.INVALID_OPTIONS }
208
+ );
164
209
  }
165
210
 
166
211
  return [
@@ -191,17 +236,23 @@ export function isYamlockPayload(candidate) {
191
236
  */
192
237
  export function parsePayload(value) {
193
238
  if (!isYamlockPayload(value)) {
194
- throw new Error('Value is not a yamlock payload.');
239
+ throw new YamlockPayloadError('Value is not a yamlock payload.', {
240
+ code: YAMLOCK_ERROR_CODES.INVALID_PAYLOAD
241
+ });
195
242
  }
196
243
 
197
244
  const parts = value.split(YAMLOCK_DELIMITER);
198
245
  if (parts.length !== 5) {
199
- throw new Error('Malformed yamlock payload.');
246
+ throw new YamlockPayloadError('Malformed yamlock payload.', {
247
+ code: YAMLOCK_ERROR_CODES.INVALID_PAYLOAD
248
+ });
200
249
  }
201
250
 
202
251
  const [, algorithm, salt, ivBase64, dataBase64] = parts;
203
252
  if (!algorithm || !salt || !ivBase64 || !dataBase64) {
204
- throw new Error('Malformed yamlock payload segments.');
253
+ throw new YamlockPayloadError('Malformed yamlock payload segments.', {
254
+ code: YAMLOCK_ERROR_CODES.INVALID_PAYLOAD
255
+ });
205
256
  }
206
257
 
207
258
  return {
package/dist/errors.js ADDED
@@ -0,0 +1,59 @@
1
+ const ERROR_CODE_PATTERN = /^ERR_[A-Z0-9_]+$/;
2
+
3
+ export const YAMLOCK_ERROR_CODES = Object.freeze({
4
+ ALREADY_ENCRYPTED: 'ERR_ALREADY_ENCRYPTED',
5
+ AUTHENTICATION_FAILED: 'ERR_AUTHENTICATION_FAILED',
6
+ CIRCULAR_CONFIG: 'ERR_CIRCULAR_CONFIG',
7
+ DECRYPTION_FAILED: 'ERR_DECRYPTION_FAILED',
8
+ FIELD_PATH_MISMATCH: 'ERR_FIELD_PATH_MISMATCH',
9
+ INVALID_CONFIG_OPTIONS: 'ERR_INVALID_CONFIG_OPTIONS',
10
+ INVALID_CONFIG_ROOT: 'ERR_INVALID_CONFIG_ROOT',
11
+ INVALID_EXISTING_PAYLOAD_POLICY: 'ERR_INVALID_EXISTING_PAYLOAD_POLICY',
12
+ INVALID_FIELD_PATH: 'ERR_INVALID_FIELD_PATH',
13
+ INVALID_KEY: 'ERR_INVALID_KEY',
14
+ INVALID_MODE: 'ERR_INVALID_MODE',
15
+ INVALID_NON_STRING_POLICY: 'ERR_INVALID_NON_STRING_POLICY',
16
+ INVALID_OPTIONS: 'ERR_INVALID_OPTIONS',
17
+ INVALID_PATH_SEGMENTS: 'ERR_INVALID_PATH_SEGMENTS',
18
+ INVALID_PATH_SERIALIZER: 'ERR_INVALID_PATH_SERIALIZER',
19
+ INVALID_PATHS: 'ERR_INVALID_PATHS',
20
+ INVALID_PAYLOAD: 'ERR_INVALID_PAYLOAD',
21
+ INVALID_VALUE: 'ERR_INVALID_VALUE',
22
+ NON_STRING_VALUE: 'ERR_NON_STRING_VALUE',
23
+ PATH_COLLISION: 'ERR_PATH_COLLISION',
24
+ PAYLOAD_TOO_LARGE: 'ERR_PAYLOAD_TOO_LARGE',
25
+ UNSUPPORTED_ALGORITHM: 'ERR_UNSUPPORTED_ALGORITHM',
26
+ UNSUPPORTED_CONFIG_VALUE: 'ERR_UNSUPPORTED_CONFIG_VALUE',
27
+ UNSUPPORTED_PAYLOAD: 'ERR_UNSUPPORTED_PAYLOAD',
28
+ UNSUPPORTED_PAYLOAD_VERSION: 'ERR_UNSUPPORTED_PAYLOAD_VERSION',
29
+ VALUE_TOO_LARGE: 'ERR_VALUE_TOO_LARGE'
30
+ });
31
+
32
+ export class YamlockError extends Error {
33
+ constructor(message, { code, cause } = {}) {
34
+ if (typeof code !== 'string' || !ERROR_CODE_PATTERN.test(code)) {
35
+ throw new TypeError('YamlockError requires an ERR_* code.');
36
+ }
37
+
38
+ super(message, cause === undefined ? undefined : { cause });
39
+ this.name = new.target.name;
40
+ this.code = code;
41
+ }
42
+ }
43
+
44
+ export class YamlockValidationError extends YamlockError {}
45
+
46
+ export class YamlockPayloadError extends YamlockError {}
47
+
48
+ export class YamlockAuthenticationError extends YamlockPayloadError {
49
+ constructor(message = 'Payload authentication failed.', options = {}) {
50
+ super(message, {
51
+ ...options,
52
+ code: YAMLOCK_ERROR_CODES.AUTHENTICATION_FAILED
53
+ });
54
+ }
55
+ }
56
+
57
+ export class YamlockDecryptionError extends YamlockError {}
58
+
59
+ export class YamlockConfigError extends YamlockValidationError {}
@@ -0,0 +1,123 @@
1
+ import type { Buffer } from 'node:buffer';
2
+
3
+ export type YamlockKey = string | Buffer;
4
+ export type YamlockPathSegment = string | number;
5
+ export type YamlockFormatVersion = 1 | 2;
6
+ export type YamlockNonStringPolicy = 'ignore' | 'stringify' | 'error';
7
+ export type YamlockExistingPayloadPolicy = 'preserve' | 'error' | 'encrypt';
8
+ export type YamlockConfig = Record<string, unknown> | unknown[];
9
+
10
+ export interface YamlockCryptoOptions {
11
+ algorithm?: string;
12
+ keyLength?: number;
13
+ ivLength?: number;
14
+ authTagLength?: number;
15
+ formatVersion?: YamlockFormatVersion;
16
+ }
17
+
18
+ export type YamlockCryptoOptionsInput = string | YamlockCryptoOptions;
19
+
20
+ export interface ProcessConfigCommonOptions {
21
+ key: YamlockKey;
22
+ algorithm?: YamlockCryptoOptionsInput;
23
+ algorithmOptions?: YamlockCryptoOptions;
24
+ formatVersion?: YamlockFormatVersion;
25
+ nonStringPolicy?: YamlockNonStringPolicy;
26
+ pathSerializer?: (segments: YamlockPathSegment[]) => string;
27
+ paths?: string[];
28
+ parentPath?: YamlockPathSegment[];
29
+ }
30
+
31
+ export interface EncryptProcessConfigOptions extends ProcessConfigCommonOptions {
32
+ mode: 'encrypt';
33
+ existingPayloadPolicy?: YamlockExistingPayloadPolicy;
34
+ }
35
+
36
+ export interface DecryptProcessConfigOptions extends ProcessConfigCommonOptions {
37
+ mode: 'decrypt';
38
+ existingPayloadPolicy?: never;
39
+ }
40
+
41
+ export type ProcessConfigOptions =
42
+ | EncryptProcessConfigOptions
43
+ | DecryptProcessConfigOptions;
44
+
45
+ export const YAMLOCK_ERROR_CODES: Readonly<{
46
+ ALREADY_ENCRYPTED: 'ERR_ALREADY_ENCRYPTED';
47
+ AUTHENTICATION_FAILED: 'ERR_AUTHENTICATION_FAILED';
48
+ CIRCULAR_CONFIG: 'ERR_CIRCULAR_CONFIG';
49
+ DECRYPTION_FAILED: 'ERR_DECRYPTION_FAILED';
50
+ FIELD_PATH_MISMATCH: 'ERR_FIELD_PATH_MISMATCH';
51
+ INVALID_CONFIG_OPTIONS: 'ERR_INVALID_CONFIG_OPTIONS';
52
+ INVALID_CONFIG_ROOT: 'ERR_INVALID_CONFIG_ROOT';
53
+ INVALID_EXISTING_PAYLOAD_POLICY: 'ERR_INVALID_EXISTING_PAYLOAD_POLICY';
54
+ INVALID_FIELD_PATH: 'ERR_INVALID_FIELD_PATH';
55
+ INVALID_KEY: 'ERR_INVALID_KEY';
56
+ INVALID_MODE: 'ERR_INVALID_MODE';
57
+ INVALID_NON_STRING_POLICY: 'ERR_INVALID_NON_STRING_POLICY';
58
+ INVALID_OPTIONS: 'ERR_INVALID_OPTIONS';
59
+ INVALID_PATH_SEGMENTS: 'ERR_INVALID_PATH_SEGMENTS';
60
+ INVALID_PATH_SERIALIZER: 'ERR_INVALID_PATH_SERIALIZER';
61
+ INVALID_PATHS: 'ERR_INVALID_PATHS';
62
+ INVALID_PAYLOAD: 'ERR_INVALID_PAYLOAD';
63
+ INVALID_VALUE: 'ERR_INVALID_VALUE';
64
+ NON_STRING_VALUE: 'ERR_NON_STRING_VALUE';
65
+ PATH_COLLISION: 'ERR_PATH_COLLISION';
66
+ PAYLOAD_TOO_LARGE: 'ERR_PAYLOAD_TOO_LARGE';
67
+ UNSUPPORTED_ALGORITHM: 'ERR_UNSUPPORTED_ALGORITHM';
68
+ UNSUPPORTED_CONFIG_VALUE: 'ERR_UNSUPPORTED_CONFIG_VALUE';
69
+ UNSUPPORTED_PAYLOAD: 'ERR_UNSUPPORTED_PAYLOAD';
70
+ UNSUPPORTED_PAYLOAD_VERSION: 'ERR_UNSUPPORTED_PAYLOAD_VERSION';
71
+ VALUE_TOO_LARGE: 'ERR_VALUE_TOO_LARGE';
72
+ }>;
73
+
74
+ export type YamlockErrorCode =
75
+ (typeof YAMLOCK_ERROR_CODES)[keyof typeof YAMLOCK_ERROR_CODES];
76
+
77
+ export interface YamlockErrorOptions {
78
+ code: YamlockErrorCode | `ERR_${string}`;
79
+ cause?: unknown;
80
+ }
81
+
82
+ export class YamlockError extends Error {
83
+ constructor(message: string, options: YamlockErrorOptions);
84
+ code: YamlockErrorCode | `ERR_${string}`;
85
+ }
86
+
87
+ export class YamlockValidationError extends YamlockError {}
88
+ export class YamlockPayloadError extends YamlockError {}
89
+
90
+ export class YamlockAuthenticationError extends YamlockPayloadError {
91
+ constructor(message?: string, options?: { cause?: unknown });
92
+ code: 'ERR_AUTHENTICATION_FAILED';
93
+ }
94
+
95
+ export class YamlockDecryptionError extends YamlockError {}
96
+ export class YamlockConfigError extends YamlockValidationError {}
97
+
98
+ export function encryptValue(
99
+ value: string,
100
+ key: YamlockKey,
101
+ fieldPath: string,
102
+ algorithmOptions?: YamlockCryptoOptionsInput
103
+ ): string;
104
+
105
+ export function decryptValue(
106
+ encryptedValue: string,
107
+ key: YamlockKey,
108
+ fieldPath: string,
109
+ algorithmOptions?: YamlockCryptoOptionsInput
110
+ ): string;
111
+
112
+ export function processConfig<T extends YamlockConfig>(
113
+ node: T,
114
+ options: ProcessConfigOptions & { nonStringPolicy: 'stringify' }
115
+ ): YamlockConfig;
116
+
117
+ export function processConfig<T extends YamlockConfig>(
118
+ node: T,
119
+ options: ProcessConfigOptions
120
+ ): T;
121
+
122
+ export function serializePath(segments: YamlockPathSegment[]): string;
123
+ export function getSupportedAlgorithms(): string[];
package/dist/index.js CHANGED
@@ -1,4 +1,14 @@
1
1
  export { encryptValue } from './crypto/encrypt.js';
2
2
  export { decryptValue } from './crypto/decrypt.js';
3
3
  export { processConfig } from './utils/config.js';
4
+ export { serializePath } from './utils/path.js';
4
5
  export { listSupportedAlgorithms as getSupportedAlgorithms } from './crypto/utils.js';
6
+ export {
7
+ YAMLOCK_ERROR_CODES,
8
+ YamlockAuthenticationError,
9
+ YamlockConfigError,
10
+ YamlockDecryptionError,
11
+ YamlockError,
12
+ YamlockPayloadError,
13
+ YamlockValidationError
14
+ } from './errors.js';