@rezwanul7/secret-vault 0.0.0-stage → 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.
@@ -0,0 +1,222 @@
1
+ /** Options for building a {@link Vault}. */
2
+ interface VaultOptions {
3
+ /**
4
+ * The current Vault secret, which seals every new value. At least 32
5
+ * characters of high-entropy key material, such as the output of
6
+ * `openssl rand -base64 32`.
7
+ */
8
+ secret: string;
9
+ /**
10
+ * Earlier Vault secrets, still accepted when opening Sealed values but never
11
+ * used to seal new ones. During a Rotation, move the old `secret` here until
12
+ * every value sealed with it has been re-sealed with {@link Vault.reseal}.
13
+ * Each must be at least 32 characters. A secret listed twice (here or
14
+ * alongside `secret`) is a {@link VaultConfigError}, reported by the first
15
+ * call that needs keys rather than by the constructor.
16
+ */
17
+ previousSecrets?: readonly string[];
18
+ /**
19
+ * The name of this Vault, such as `myapp:settings`. Vaults with the same
20
+ * Vault secret but different Purposes can't read each other's Sealed
21
+ * values. It must never change once values are stored.
22
+ */
23
+ purpose: string;
24
+ }
25
+ /**
26
+ * Turns Plaintexts into Sealed values and back, with AES-256-GCM on Web
27
+ * Crypto. Build one per Purpose, at module level, and reuse it.
28
+ *
29
+ * Every method that seals or opens takes a Context: where the Sealed value is
30
+ * stored, such as `settings:smtp_pass`. A Sealed value only opens under the
31
+ * Context it was sealed with. `""` is allowed and means no binding.
32
+ *
33
+ * @example
34
+ * const vault = new Vault({ secret: process.env.VAULT_SECRET!, purpose: "myapp:settings" });
35
+ * const sealed = await vault.encrypt("hunter2", "settings:smtp_pass");
36
+ * await vault.decrypt(sealed, "settings:smtp_pass"); // "hunter2"
37
+ */
38
+ declare class Vault {
39
+ #private;
40
+ /**
41
+ * Checks the options right away; keys are derived on first use, so this
42
+ * needs no `await`.
43
+ *
44
+ * @param options - The Vault secrets and Purpose.
45
+ * @throws {@link VaultConfigError} when an option breaks a rule in
46
+ * {@link VaultOptions}, except a duplicated secret, which is reported on
47
+ * first use. The message never contains a secret.
48
+ */
49
+ constructor(options: VaultOptions);
50
+ /**
51
+ * True when `value` is a Sealed value: a string starting with `vault:`. Use
52
+ * it to tell Sealed values from plain text still stored in a column.
53
+ *
54
+ * @param value - Any value.
55
+ */
56
+ static isSealed(value: unknown): value is string;
57
+ /**
58
+ * Seals a string.
59
+ *
60
+ * @param plaintext - The string to seal. May be empty.
61
+ * @param context - Where the Sealed value is stored.
62
+ * @returns The Sealed value, `vault:v1:<keyId>:s:<iv>:<ciphertext>`.
63
+ */
64
+ encrypt(plaintext: string, context: string): Promise<string>;
65
+ /**
66
+ * Opens a Sealed value made by {@link Vault.encrypt}.
67
+ *
68
+ * @param sealed - The Sealed value.
69
+ * @param context - The Context it was sealed under.
70
+ * @returns The Plaintext string.
71
+ * @throws {@link VaultDecryptError} when the value isn't a Sealed value, was
72
+ * sealed with an unknown Vault secret or another Purpose, was edited, or is
73
+ * opened under another Context, or `wrong_kind` when it isn't a string
74
+ * Sealed value. A string Sealed value that opens to bytes that aren't valid
75
+ * UTF-8 also rejects as `authentication_failed`, with the decoder's
76
+ * `TypeError` as its `cause`.
77
+ */
78
+ decrypt(sealed: string, context: string): Promise<string>;
79
+ /**
80
+ * Seals JSON data with plain `JSON.stringify`.
81
+ *
82
+ * @param value - Any value `JSON.stringify` can encode.
83
+ * @param context - Where the Sealed value is stored.
84
+ * @returns The Sealed value, `vault:v1:<keyId>:j:<iv>:<ciphertext>`.
85
+ * @throws The promise rejects with a `TypeError` when `JSON.stringify`
86
+ * can't encode `value`, such as `undefined`, a function, a `BigInt` or a
87
+ * cyclic object, before anything is encrypted. It never throws
88
+ * synchronously.
89
+ */
90
+ encryptJson(value: unknown, context: string): Promise<string>;
91
+ /**
92
+ * Opens a Sealed value made by {@link Vault.encryptJson}.
93
+ *
94
+ * @typeParam T - The type the caller expects. It is asserted, not validated.
95
+ * @param sealed - The Sealed value.
96
+ * @param context - The Context it was sealed under.
97
+ * @returns The JSON data, parsed with plain `JSON.parse`.
98
+ * @throws {@link VaultDecryptError} for the same reasons as
99
+ * {@link Vault.decrypt}, `wrong_kind` when it isn't a JSON Sealed value, or
100
+ * `invalid_json` when it opens to text that isn't JSON.
101
+ */
102
+ decryptJson<T = unknown>(sealed: string, context: string): Promise<T>;
103
+ /**
104
+ * Seals bytes.
105
+ *
106
+ * @param plaintext - The bytes to seal. May be empty.
107
+ * @param context - Where the Sealed value is stored.
108
+ * @returns The Sealed value, `vault:v1:<keyId>:b:<iv>:<ciphertext>`.
109
+ */
110
+ encryptBytes(plaintext: Uint8Array, context: string): Promise<string>;
111
+ /**
112
+ * Opens a Sealed value made by {@link Vault.encryptBytes}.
113
+ *
114
+ * @param sealed - The Sealed value.
115
+ * @param context - The Context it was sealed under.
116
+ * @returns The Plaintext bytes, as a plain `Uint8Array`.
117
+ * @throws {@link VaultDecryptError} for the same reasons as
118
+ * {@link Vault.decrypt}, or `wrong_kind` when it isn't a bytes Sealed value.
119
+ */
120
+ decryptBytes(sealed: string, context: string): Promise<Uint8Array>;
121
+ /**
122
+ * True when a Sealed value was made with a previous Vault secret, so it
123
+ * should be re-sealed with {@link Vault.reseal} before that secret is
124
+ * dropped. Reads only the key id; it doesn't open the value, so it needs no
125
+ * Context. It is async, unlike {@link Vault.isSealed}, because the current
126
+ * key id is derived on first use: `await` it.
127
+ *
128
+ * @param sealed - The Sealed value.
129
+ * @returns A promise of `true` when the key id isn't the current one.
130
+ * @throws The promise rejects with {@link VaultDecryptError} `not_sealed`
131
+ * (or `unsupported_version`) when the value isn't a Sealed value this
132
+ * package can read; its `context` is `undefined`. It never throws
133
+ * synchronously.
134
+ */
135
+ needsRotation(sealed: string): Promise<boolean>;
136
+ /**
137
+ * Opens a Sealed value with whichever Vault secret made it and seals it
138
+ * again with the current one, keeping its kind (string, JSON or bytes). Use
139
+ * it on values where {@link Vault.needsRotation} is true.
140
+ *
141
+ * @param sealed - The Sealed value.
142
+ * @param context - The Context it was sealed under; the new Sealed value
143
+ * is bound to the same Context.
144
+ * @returns A new Sealed value with the current key id.
145
+ * @throws {@link VaultDecryptError} for the same reasons as
146
+ * {@link Vault.decrypt}.
147
+ */
148
+ reseal(sealed: string, context: string): Promise<string>;
149
+ }
150
+
151
+ /**
152
+ * Base class of every error the package throws, so `instanceof VaultError`
153
+ * catches them all. The underlying error, if any, is the native `cause`.
154
+ */
155
+ declare class VaultError extends Error {
156
+ /** A stable, machine-readable code such as `VAULT_CONFIG_INVALID`. */
157
+ readonly code: string;
158
+ /**
159
+ * @param message - The error message.
160
+ * @param code - The stable error code.
161
+ * @param options - The underlying `cause`, if any.
162
+ */
163
+ constructor(message: string, code: string, options?: ErrorOptions);
164
+ }
165
+ /**
166
+ * The Vault's options break a rule in {@link VaultOptions}. Thrown by the
167
+ * `Vault` constructor, so a config mistake fails at startup, except a
168
+ * duplicated Vault secret: that is found when keys are derived, so the
169
+ * Vault's first keyed call, and every later one, rejects with this error. The
170
+ * message names the bad option, never its value.
171
+ */
172
+ declare class VaultConfigError extends VaultError {
173
+ /** Always `VAULT_CONFIG_INVALID`. */
174
+ readonly code: "VAULT_CONFIG_INVALID";
175
+ /**
176
+ * @param message - What is wrong, naming the option but never its value.
177
+ * @param options - The underlying `cause`, if any.
178
+ */
179
+ constructor(message: string, options?: ErrorOptions);
180
+ }
181
+ /**
182
+ * Why a Sealed value couldn't be opened.
183
+ *
184
+ * - `not_sealed`: the value isn't a well-formed Sealed value, such as plain
185
+ * text, a value with missing parts, or a part that isn't canonical
186
+ * base64url.
187
+ * - `unsupported_version`: a well-formed Sealed value in a format version
188
+ * this package can't read.
189
+ * - `unknown_key`: no configured Vault secret matches the Sealed value's key id.
190
+ * - `wrong_kind`: the Sealed value holds another kind of Plaintext than the method reads.
191
+ * - `authentication_failed`: the Sealed value was edited, or is opened under
192
+ * another Context or Purpose, or a string Sealed value opened to bytes that
193
+ * aren't valid UTF-8.
194
+ * - `invalid_json`: a JSON Sealed value opened to text that isn't JSON.
195
+ */
196
+ type VaultDecryptReason = "not_sealed" | "unsupported_version" | "unknown_key" | "wrong_kind" | "authentication_failed" | "invalid_json";
197
+ /** The code of a {@link VaultDecryptError}, one per {@link VaultDecryptReason}. */
198
+ type VaultDecryptErrorCode = `VAULT_DECRYPT_${Uppercase<VaultDecryptReason>}`;
199
+ /**
200
+ * A Sealed value couldn't be opened. The message names the reason and the
201
+ * Context, if there is one, never the Plaintext or the Sealed value.
202
+ */
203
+ declare class VaultDecryptError extends VaultError {
204
+ /** Why the Sealed value couldn't be opened. */
205
+ readonly reason: VaultDecryptReason;
206
+ /**
207
+ * The Context the Sealed value was opened under, or `undefined` when the
208
+ * method takes no Context (`needsRotation`).
209
+ */
210
+ readonly context: string | undefined;
211
+ /** The code for {@link VaultDecryptError.reason}, such as `VAULT_DECRYPT_UNKNOWN_KEY`. */
212
+ readonly code: VaultDecryptErrorCode;
213
+ /**
214
+ * @param reason - Why the Sealed value couldn't be opened.
215
+ * @param context - The Context it was opened under, or `undefined` when
216
+ * there is none.
217
+ * @param options - The underlying `cause`, if any.
218
+ */
219
+ constructor(reason: VaultDecryptReason, context: string | undefined, options?: ErrorOptions);
220
+ }
221
+
222
+ export { Vault, VaultConfigError, VaultDecryptError, type VaultDecryptErrorCode, type VaultDecryptReason, VaultError, type VaultOptions };
@@ -0,0 +1,222 @@
1
+ /** Options for building a {@link Vault}. */
2
+ interface VaultOptions {
3
+ /**
4
+ * The current Vault secret, which seals every new value. At least 32
5
+ * characters of high-entropy key material, such as the output of
6
+ * `openssl rand -base64 32`.
7
+ */
8
+ secret: string;
9
+ /**
10
+ * Earlier Vault secrets, still accepted when opening Sealed values but never
11
+ * used to seal new ones. During a Rotation, move the old `secret` here until
12
+ * every value sealed with it has been re-sealed with {@link Vault.reseal}.
13
+ * Each must be at least 32 characters. A secret listed twice (here or
14
+ * alongside `secret`) is a {@link VaultConfigError}, reported by the first
15
+ * call that needs keys rather than by the constructor.
16
+ */
17
+ previousSecrets?: readonly string[];
18
+ /**
19
+ * The name of this Vault, such as `myapp:settings`. Vaults with the same
20
+ * Vault secret but different Purposes can't read each other's Sealed
21
+ * values. It must never change once values are stored.
22
+ */
23
+ purpose: string;
24
+ }
25
+ /**
26
+ * Turns Plaintexts into Sealed values and back, with AES-256-GCM on Web
27
+ * Crypto. Build one per Purpose, at module level, and reuse it.
28
+ *
29
+ * Every method that seals or opens takes a Context: where the Sealed value is
30
+ * stored, such as `settings:smtp_pass`. A Sealed value only opens under the
31
+ * Context it was sealed with. `""` is allowed and means no binding.
32
+ *
33
+ * @example
34
+ * const vault = new Vault({ secret: process.env.VAULT_SECRET!, purpose: "myapp:settings" });
35
+ * const sealed = await vault.encrypt("hunter2", "settings:smtp_pass");
36
+ * await vault.decrypt(sealed, "settings:smtp_pass"); // "hunter2"
37
+ */
38
+ declare class Vault {
39
+ #private;
40
+ /**
41
+ * Checks the options right away; keys are derived on first use, so this
42
+ * needs no `await`.
43
+ *
44
+ * @param options - The Vault secrets and Purpose.
45
+ * @throws {@link VaultConfigError} when an option breaks a rule in
46
+ * {@link VaultOptions}, except a duplicated secret, which is reported on
47
+ * first use. The message never contains a secret.
48
+ */
49
+ constructor(options: VaultOptions);
50
+ /**
51
+ * True when `value` is a Sealed value: a string starting with `vault:`. Use
52
+ * it to tell Sealed values from plain text still stored in a column.
53
+ *
54
+ * @param value - Any value.
55
+ */
56
+ static isSealed(value: unknown): value is string;
57
+ /**
58
+ * Seals a string.
59
+ *
60
+ * @param plaintext - The string to seal. May be empty.
61
+ * @param context - Where the Sealed value is stored.
62
+ * @returns The Sealed value, `vault:v1:<keyId>:s:<iv>:<ciphertext>`.
63
+ */
64
+ encrypt(plaintext: string, context: string): Promise<string>;
65
+ /**
66
+ * Opens a Sealed value made by {@link Vault.encrypt}.
67
+ *
68
+ * @param sealed - The Sealed value.
69
+ * @param context - The Context it was sealed under.
70
+ * @returns The Plaintext string.
71
+ * @throws {@link VaultDecryptError} when the value isn't a Sealed value, was
72
+ * sealed with an unknown Vault secret or another Purpose, was edited, or is
73
+ * opened under another Context, or `wrong_kind` when it isn't a string
74
+ * Sealed value. A string Sealed value that opens to bytes that aren't valid
75
+ * UTF-8 also rejects as `authentication_failed`, with the decoder's
76
+ * `TypeError` as its `cause`.
77
+ */
78
+ decrypt(sealed: string, context: string): Promise<string>;
79
+ /**
80
+ * Seals JSON data with plain `JSON.stringify`.
81
+ *
82
+ * @param value - Any value `JSON.stringify` can encode.
83
+ * @param context - Where the Sealed value is stored.
84
+ * @returns The Sealed value, `vault:v1:<keyId>:j:<iv>:<ciphertext>`.
85
+ * @throws The promise rejects with a `TypeError` when `JSON.stringify`
86
+ * can't encode `value`, such as `undefined`, a function, a `BigInt` or a
87
+ * cyclic object, before anything is encrypted. It never throws
88
+ * synchronously.
89
+ */
90
+ encryptJson(value: unknown, context: string): Promise<string>;
91
+ /**
92
+ * Opens a Sealed value made by {@link Vault.encryptJson}.
93
+ *
94
+ * @typeParam T - The type the caller expects. It is asserted, not validated.
95
+ * @param sealed - The Sealed value.
96
+ * @param context - The Context it was sealed under.
97
+ * @returns The JSON data, parsed with plain `JSON.parse`.
98
+ * @throws {@link VaultDecryptError} for the same reasons as
99
+ * {@link Vault.decrypt}, `wrong_kind` when it isn't a JSON Sealed value, or
100
+ * `invalid_json` when it opens to text that isn't JSON.
101
+ */
102
+ decryptJson<T = unknown>(sealed: string, context: string): Promise<T>;
103
+ /**
104
+ * Seals bytes.
105
+ *
106
+ * @param plaintext - The bytes to seal. May be empty.
107
+ * @param context - Where the Sealed value is stored.
108
+ * @returns The Sealed value, `vault:v1:<keyId>:b:<iv>:<ciphertext>`.
109
+ */
110
+ encryptBytes(plaintext: Uint8Array, context: string): Promise<string>;
111
+ /**
112
+ * Opens a Sealed value made by {@link Vault.encryptBytes}.
113
+ *
114
+ * @param sealed - The Sealed value.
115
+ * @param context - The Context it was sealed under.
116
+ * @returns The Plaintext bytes, as a plain `Uint8Array`.
117
+ * @throws {@link VaultDecryptError} for the same reasons as
118
+ * {@link Vault.decrypt}, or `wrong_kind` when it isn't a bytes Sealed value.
119
+ */
120
+ decryptBytes(sealed: string, context: string): Promise<Uint8Array>;
121
+ /**
122
+ * True when a Sealed value was made with a previous Vault secret, so it
123
+ * should be re-sealed with {@link Vault.reseal} before that secret is
124
+ * dropped. Reads only the key id; it doesn't open the value, so it needs no
125
+ * Context. It is async, unlike {@link Vault.isSealed}, because the current
126
+ * key id is derived on first use: `await` it.
127
+ *
128
+ * @param sealed - The Sealed value.
129
+ * @returns A promise of `true` when the key id isn't the current one.
130
+ * @throws The promise rejects with {@link VaultDecryptError} `not_sealed`
131
+ * (or `unsupported_version`) when the value isn't a Sealed value this
132
+ * package can read; its `context` is `undefined`. It never throws
133
+ * synchronously.
134
+ */
135
+ needsRotation(sealed: string): Promise<boolean>;
136
+ /**
137
+ * Opens a Sealed value with whichever Vault secret made it and seals it
138
+ * again with the current one, keeping its kind (string, JSON or bytes). Use
139
+ * it on values where {@link Vault.needsRotation} is true.
140
+ *
141
+ * @param sealed - The Sealed value.
142
+ * @param context - The Context it was sealed under; the new Sealed value
143
+ * is bound to the same Context.
144
+ * @returns A new Sealed value with the current key id.
145
+ * @throws {@link VaultDecryptError} for the same reasons as
146
+ * {@link Vault.decrypt}.
147
+ */
148
+ reseal(sealed: string, context: string): Promise<string>;
149
+ }
150
+
151
+ /**
152
+ * Base class of every error the package throws, so `instanceof VaultError`
153
+ * catches them all. The underlying error, if any, is the native `cause`.
154
+ */
155
+ declare class VaultError extends Error {
156
+ /** A stable, machine-readable code such as `VAULT_CONFIG_INVALID`. */
157
+ readonly code: string;
158
+ /**
159
+ * @param message - The error message.
160
+ * @param code - The stable error code.
161
+ * @param options - The underlying `cause`, if any.
162
+ */
163
+ constructor(message: string, code: string, options?: ErrorOptions);
164
+ }
165
+ /**
166
+ * The Vault's options break a rule in {@link VaultOptions}. Thrown by the
167
+ * `Vault` constructor, so a config mistake fails at startup, except a
168
+ * duplicated Vault secret: that is found when keys are derived, so the
169
+ * Vault's first keyed call, and every later one, rejects with this error. The
170
+ * message names the bad option, never its value.
171
+ */
172
+ declare class VaultConfigError extends VaultError {
173
+ /** Always `VAULT_CONFIG_INVALID`. */
174
+ readonly code: "VAULT_CONFIG_INVALID";
175
+ /**
176
+ * @param message - What is wrong, naming the option but never its value.
177
+ * @param options - The underlying `cause`, if any.
178
+ */
179
+ constructor(message: string, options?: ErrorOptions);
180
+ }
181
+ /**
182
+ * Why a Sealed value couldn't be opened.
183
+ *
184
+ * - `not_sealed`: the value isn't a well-formed Sealed value, such as plain
185
+ * text, a value with missing parts, or a part that isn't canonical
186
+ * base64url.
187
+ * - `unsupported_version`: a well-formed Sealed value in a format version
188
+ * this package can't read.
189
+ * - `unknown_key`: no configured Vault secret matches the Sealed value's key id.
190
+ * - `wrong_kind`: the Sealed value holds another kind of Plaintext than the method reads.
191
+ * - `authentication_failed`: the Sealed value was edited, or is opened under
192
+ * another Context or Purpose, or a string Sealed value opened to bytes that
193
+ * aren't valid UTF-8.
194
+ * - `invalid_json`: a JSON Sealed value opened to text that isn't JSON.
195
+ */
196
+ type VaultDecryptReason = "not_sealed" | "unsupported_version" | "unknown_key" | "wrong_kind" | "authentication_failed" | "invalid_json";
197
+ /** The code of a {@link VaultDecryptError}, one per {@link VaultDecryptReason}. */
198
+ type VaultDecryptErrorCode = `VAULT_DECRYPT_${Uppercase<VaultDecryptReason>}`;
199
+ /**
200
+ * A Sealed value couldn't be opened. The message names the reason and the
201
+ * Context, if there is one, never the Plaintext or the Sealed value.
202
+ */
203
+ declare class VaultDecryptError extends VaultError {
204
+ /** Why the Sealed value couldn't be opened. */
205
+ readonly reason: VaultDecryptReason;
206
+ /**
207
+ * The Context the Sealed value was opened under, or `undefined` when the
208
+ * method takes no Context (`needsRotation`).
209
+ */
210
+ readonly context: string | undefined;
211
+ /** The code for {@link VaultDecryptError.reason}, such as `VAULT_DECRYPT_UNKNOWN_KEY`. */
212
+ readonly code: VaultDecryptErrorCode;
213
+ /**
214
+ * @param reason - Why the Sealed value couldn't be opened.
215
+ * @param context - The Context it was opened under, or `undefined` when
216
+ * there is none.
217
+ * @param options - The underlying `cause`, if any.
218
+ */
219
+ constructor(reason: VaultDecryptReason, context: string | undefined, options?: ErrorOptions);
220
+ }
221
+
222
+ export { Vault, VaultConfigError, VaultDecryptError, type VaultDecryptErrorCode, type VaultDecryptReason, VaultError, type VaultOptions };