@saastemly/voidcommerce 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalog.js +2 -1
- package/dist/cli.js +300 -58
- package/dist/deploy/github.d.ts +85 -0
- package/dist/deploy/index.d.ts +12 -0
- package/dist/deploy/keys.d.ts +77 -0
- package/dist/deploy/link.d.ts +2 -0
- package/dist/deploy/secrets.d.ts +59 -10
- package/dist/generate/ci.d.ts +33 -18
- package/dist/generate/index.d.ts +2 -0
- package/dist/index-0v6na3yp.js +35 -0
- package/dist/{index-84wq4qp8.js → index-5m3t0zfc.js} +244 -503
- package/dist/{index-z4qazajn.js → index-844b3qn9.js} +1 -4
- package/dist/index-b9b4dawy.js +797 -0
- package/dist/index-mpr7gm6k.js +4597 -0
- package/dist/{index-zh3tmj08.js → index-pz6m2hkm.js} +1 -1
- package/dist/index.js +12 -9
- package/dist/keys-b91c72zp.js +19 -0
- package/dist/manifest.js +3 -2
- package/package.json +2 -1
- package/src/cli.ts +17 -1
- package/src/deploy/github.ts +198 -0
- package/src/deploy/index.ts +109 -8
- package/src/deploy/keys.ts +276 -0
- package/src/deploy/link.ts +185 -0
- package/src/deploy/secrets.ts +230 -26
- package/src/dotenvx-primitives.d.ts +14 -0
- package/src/generate/ci.ts +168 -121
- package/src/generate/index.ts +43 -3
- package/src/generate/strict.ts +2 -0
- package/src/init.ts +1 -0
- package/src/regenerate.ts +1 -0
package/src/deploy/secrets.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { delimiter, dirname, join } from "node:path";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import color from "picocolors";
|
|
6
6
|
import { allEnvKeys } from "../generate/env";
|
|
7
|
+
import { committedPublicKey, provisionKey } from "./keys";
|
|
7
8
|
import type { Project } from "../project";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -19,9 +20,11 @@ import type { Project } from "../project";
|
|
|
19
20
|
*
|
|
20
21
|
* dotenvx closes it. `.env.secrets` holds ciphertext and a public key, and
|
|
21
22
|
* it is COMMITTED: the key names are readable, the values are not, and a
|
|
22
|
-
* diff shows when a secret changed without showing what it changed to.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* diff shows when a secret changed without showing what it changed to.
|
|
24
|
+
*
|
|
25
|
+
* Because dotenvx is asymmetric, SETTING a secret needs only the committed
|
|
26
|
+
* public key — no credential, no login, no key file. The private key lives
|
|
27
|
+
* in one place, GitHub Actions, and only the deploy uses it. See `./keys.ts`.
|
|
25
28
|
*
|
|
26
29
|
* ── Why the values still become worker SECRETS ───────────────────────────
|
|
27
30
|
*
|
|
@@ -34,14 +37,18 @@ import type { Project } from "../project";
|
|
|
34
37
|
* applies ADDITIVELY, so a secret this file does not name is left alone
|
|
35
38
|
* rather than deleted.
|
|
36
39
|
*
|
|
37
|
-
* ── The
|
|
40
|
+
* ── The tradeoffs, stated ────────────────────────────────────────────────
|
|
41
|
+
*
|
|
42
|
+
* Ciphertext in git is permanent. A key that ever leaks reads every secret
|
|
43
|
+
* in the history, including ones that were rotated — which is not true of
|
|
44
|
+
* `wrangler secret put`, where a rotation genuinely retires the old value.
|
|
45
|
+
*
|
|
46
|
+
* And GitHub will not hand a secret back once set, so re-keying means
|
|
47
|
+
* entering the values again. That is the right trade: a key worth rotating
|
|
48
|
+
* is a key that may have leaked, and the values then need replacing anyway.
|
|
38
49
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* which is not true of `wrangler secret put`, where a rotation genuinely
|
|
42
|
-
* retires the old value. Rotating the dotenvx key re-encrypts the present,
|
|
43
|
-
* not the past. That is the price of a shop that rebuilds from a checkout,
|
|
44
|
-
* and it should be a decision rather than a surprise.
|
|
50
|
+
* Both are the price of a shop that rebuilds from a checkout, and both
|
|
51
|
+
* should be decisions rather than surprises.
|
|
45
52
|
*/
|
|
46
53
|
|
|
47
54
|
export const SECRETS_FILE = ".env.secrets";
|
|
@@ -65,7 +72,7 @@ export function findDotenvx(from: string): string | null {
|
|
|
65
72
|
: null;
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
function run(cmd: string, args: string[], cwd: string, env: Record<string, string> = {}): Promise<{ code: number; out: string }> {
|
|
75
|
+
export function run(cmd: string, args: string[], cwd: string, env: Record<string, string> = {}): Promise<{ code: number; out: string }> {
|
|
69
76
|
return new Promise((resolve) => {
|
|
70
77
|
const child = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, ...env } });
|
|
71
78
|
let out = "";
|
|
@@ -80,6 +87,28 @@ function run(cmd: string, args: string[], cwd: string, env: Record<string, strin
|
|
|
80
87
|
});
|
|
81
88
|
}
|
|
82
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Put a public key at the top of `.env.secrets`, creating the file if it is
|
|
92
|
+
* not there yet.
|
|
93
|
+
*
|
|
94
|
+
* Only the public half is ever written to disk, and it is meant to be
|
|
95
|
+
* committed: it is what lets anyone with a clone SET a secret without
|
|
96
|
+
* holding a credential.
|
|
97
|
+
*/
|
|
98
|
+
export function committedPublicKeyInto(root: string, publicKey: string): void {
|
|
99
|
+
const path = join(root, SECRETS_FILE);
|
|
100
|
+
const line = `DOTENV_PUBLIC_KEY_SECRETS="${publicKey}"`;
|
|
101
|
+
if (!existsSync(path)) {
|
|
102
|
+
writeFileSync(path, `${line}\n`, { mode: 0o600 });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const body = readFileSync(path, "utf8");
|
|
106
|
+
writeFileSync(
|
|
107
|
+
path,
|
|
108
|
+
/^\s*DOTENV_PUBLIC_KEY_SECRETS\s*=.*$/m.test(body) ? body.replace(/^\s*DOTENV_PUBLIC_KEY_SECRETS\s*=.*$/m, line) : `${line}\n${body}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
83
112
|
/**
|
|
84
113
|
* The names a `.env.secrets` declares, read WITHOUT decrypting.
|
|
85
114
|
*
|
|
@@ -129,15 +158,42 @@ export async function decryptSecrets(project: Project): Promise<DecryptedSecrets
|
|
|
129
158
|
const root = project.root;
|
|
130
159
|
if (!existsSync(join(root, SECRETS_FILE))) return { error: `${SECRETS_FILE} does not exist — \`vc secrets init\` writes one` };
|
|
131
160
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
161
|
+
// The private key comes from the environment, which in the normal case is
|
|
162
|
+
// GitHub Actions injecting the repository secret. A laptop has no reason
|
|
163
|
+
// to hold it, so a missing one is explained rather than treated as a fault.
|
|
164
|
+
const privateKey = process.env[PRIVATE_KEY_VAR];
|
|
165
|
+
if (!privateKey) {
|
|
135
166
|
return {
|
|
136
|
-
error:
|
|
167
|
+
error:
|
|
168
|
+
`${PRIVATE_KEY_VAR} is not set, so ${SECRETS_FILE} cannot be decrypted here.\n` +
|
|
169
|
+
` That is the normal state on a laptop: the key lives in GitHub Actions and\n` +
|
|
170
|
+
` only the deploy uses it. Push, and the workflow does this step.\n` +
|
|
171
|
+
` To deploy by hand anyway, export the key for one command.`,
|
|
137
172
|
};
|
|
138
173
|
}
|
|
174
|
+
const expected = committedPublicKey(root);
|
|
175
|
+
if (expected) {
|
|
176
|
+
const { publicKeyFor } = await import("./keys");
|
|
177
|
+
const derived = await publicKeyFor(privateKey);
|
|
178
|
+
if (derived && derived.toLowerCase() !== expected.toLowerCase()) {
|
|
179
|
+
return {
|
|
180
|
+
error:
|
|
181
|
+
`${PRIVATE_KEY_VAR} does not open this repository.\n` +
|
|
182
|
+
` it derives: ${derived.slice(0, 16)}…\n` +
|
|
183
|
+
` encrypted under: ${expected.slice(0, 16)}…\n` +
|
|
184
|
+
` It belongs to a different shop, or the repository has been re-keyed since.`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Last, deliberately: "that key belongs to another shop" is more use than
|
|
190
|
+
// "install dotenvx" when both are true.
|
|
191
|
+
const dotenvx = findDotenvx(root);
|
|
192
|
+
if (!dotenvx) return { error: "dotenvx is not installed. `bun add -d @dotenvx/dotenvx`" };
|
|
139
193
|
|
|
140
|
-
const result = await run(dotenvx, ["decrypt", "-f", SECRETS_FILE, "--stdout"], root
|
|
194
|
+
const result = await run(dotenvx, ["decrypt", "-f", SECRETS_FILE, "--stdout"], root, {
|
|
195
|
+
[PRIVATE_KEY_VAR]: privateKey,
|
|
196
|
+
});
|
|
141
197
|
if (result.code !== 0) return { error: `dotenvx could not decrypt ${SECRETS_FILE}: ${result.out.trim().split("\n").slice(-2).join(" ")}` };
|
|
142
198
|
|
|
143
199
|
// Keep only the shop's own keys: dotenvx echoes the public key back, and
|
|
@@ -173,6 +229,7 @@ export async function secretsCommand(project: Project, args: string[]): Promise<
|
|
|
173
229
|
const bare = plaintextSecretNames(root);
|
|
174
230
|
|
|
175
231
|
if (args.includes("--init")) return initSecrets(project);
|
|
232
|
+
if (args[0] === "set") return setSecretValue(project, args.slice(1));
|
|
176
233
|
|
|
177
234
|
if (!existsSync(join(root, SECRETS_FILE))) {
|
|
178
235
|
console.log(
|
|
@@ -211,10 +268,26 @@ export async function secretsCommand(project: Project, args: string[]): Promise<
|
|
|
211
268
|
}
|
|
212
269
|
|
|
213
270
|
/** Write a `.env.secrets` with every required key as the `unset` sentinel. */
|
|
214
|
-
async function initSecrets(project: Project): Promise<number> {
|
|
271
|
+
export async function initSecrets(project: Project): Promise<number> {
|
|
215
272
|
const path = join(project.root, SECRETS_FILE);
|
|
216
|
-
|
|
217
|
-
|
|
273
|
+
|
|
274
|
+
// `vc link` leaves a file holding nothing but the public key, so "exists"
|
|
275
|
+
// is not the same as "is already set up". Refusing on existence alone
|
|
276
|
+
// stranded anyone who linked first, which is the order the docs recommend.
|
|
277
|
+
if (existsSync(path) && declaredSecretNames(project.root).size > 0) {
|
|
278
|
+
console.error(`vc: ${SECRETS_FILE} already declares secrets; not overwriting it.\n \`vc secrets set KEY\` changes one.`);
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// The PUBLIC key goes in the file so encryption uses this shop's pair
|
|
283
|
+
// rather than inventing one and writing a .env.keys nobody asked for. Its
|
|
284
|
+
// private half goes straight to GitHub and is never seen again — so an
|
|
285
|
+
// existing key is REUSED, never replaced: replacing it would orphan
|
|
286
|
+
// whatever it has already encrypted.
|
|
287
|
+
const existing = committedPublicKey(project.root);
|
|
288
|
+
const made = existing ? ({ ok: true, publicKey: existing } as const) : await provisionKey(project);
|
|
289
|
+
if (!made.ok) {
|
|
290
|
+
console.error(`\nvc: ${made.reason}\n`);
|
|
218
291
|
return 1;
|
|
219
292
|
}
|
|
220
293
|
const required = allEnvKeys(project.manifest).filter((key) => !key.plaintext);
|
|
@@ -225,20 +298,151 @@ async function initSecrets(project: Project): Promise<number> {
|
|
|
225
298
|
"# secret changed without showing what it changed to. `unset` is the",
|
|
226
299
|
"# documented placeholder and preflight refuses it.",
|
|
227
300
|
"#",
|
|
228
|
-
"#
|
|
229
|
-
"#
|
|
301
|
+
"# Setting a value needs NO credential: dotenvx encrypts with the public key",
|
|
302
|
+
"# below, which is right here in the repository. Only the deploy decrypts,",
|
|
303
|
+
"# and the private key for that lives in GitHub Actions.",
|
|
304
|
+
"#",
|
|
305
|
+
"# `vc secrets set KEY` is the way to change one — it reads the value from",
|
|
306
|
+
"# the terminal, so it never reaches a command line where `ps` would show it.",
|
|
307
|
+
"",
|
|
308
|
+
`DOTENV_PUBLIC_KEY_SECRETS="${made.publicKey}"`,
|
|
230
309
|
"",
|
|
231
310
|
...required.flatMap((key) => [`# ${key.breaks}${key.where ? ` — from: ${key.where}` : ""}`, `${key.key}=unset`, ""]),
|
|
232
311
|
].join("\n");
|
|
233
312
|
writeFileSync(path, body, { mode: 0o600 });
|
|
234
313
|
|
|
235
314
|
console.log(
|
|
236
|
-
`\n${color.green("+")} ${SECRETS_FILE} — ${required.length} keys, all \`unset\`\n
|
|
315
|
+
`\n${color.green("+")} ${SECRETS_FILE} — ${required.length} keys, all \`unset\`\n` +
|
|
316
|
+
`${existing ? color.dim(`= ${PRIVATE_KEY_VAR} was already on GitHub; reused it`) : `${color.green("+")} ${PRIVATE_KEY_VAR} stored on GitHub; the public key is in the file`}\n\n` +
|
|
237
317
|
`Next:\n` +
|
|
238
|
-
`
|
|
239
|
-
`
|
|
240
|
-
` 3. commit ${SECRETS_FILE}; never commit .env.keys\n` +
|
|
241
|
-
` 4. set ${PRIVATE_KEY_VAR} where the deploy runs\n`,
|
|
318
|
+
`${required.map((key) => ` ${color.cyan(`vc secrets set ${key.key}`)}`).join("\n")}\n\n` +
|
|
319
|
+
`then commit ${SECRETS_FILE}. There is no key file to keep out of git.\n`,
|
|
242
320
|
);
|
|
243
321
|
return 0;
|
|
244
322
|
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* `vc secrets set KEY` — change one value, with no credential of any kind.
|
|
327
|
+
*
|
|
328
|
+
* This is the command the whole design exists to make possible. dotenvx
|
|
329
|
+
* encrypts with the public key sitting in the committed file, so a
|
|
330
|
+
* contributor with a clone and nothing else can rotate the Stripe key. They
|
|
331
|
+
* cannot read the one that is there, which is exactly right.
|
|
332
|
+
*
|
|
333
|
+
* The value is read from the terminal or from stdin, never from argv: an
|
|
334
|
+
* argument is visible in `ps` to every process on the machine while the
|
|
335
|
+
* command runs, and shells keep it in history besides.
|
|
336
|
+
*/
|
|
337
|
+
export async function setSecretValue(project: Project, args: string[]): Promise<number> {
|
|
338
|
+
const root = project.root;
|
|
339
|
+
const name = args.find((arg) => !arg.startsWith("-"));
|
|
340
|
+
if (!name) {
|
|
341
|
+
console.error(`\nvc: which secret? \`vc secrets set STRIPE_SECRET_KEY\`\n`);
|
|
342
|
+
return 1;
|
|
343
|
+
}
|
|
344
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(name)) {
|
|
345
|
+
console.error(`\nvc: "${name}" is not a key name — they are SHOUTING_SNAKE_CASE.\n`);
|
|
346
|
+
return 1;
|
|
347
|
+
}
|
|
348
|
+
if (!existsSync(join(root, SECRETS_FILE))) {
|
|
349
|
+
console.error(`\nvc: no ${SECRETS_FILE} yet. \`vc secrets --init\` writes one.\n`);
|
|
350
|
+
return 1;
|
|
351
|
+
}
|
|
352
|
+
const publicKey = committedPublicKey(root);
|
|
353
|
+
if (!publicKey) {
|
|
354
|
+
console.error(`\nvc: ${SECRETS_FILE} has no DOTENV_PUBLIC_KEY_SECRETS, so there is nothing to encrypt to.\n \`vc keys --init\` makes a key and puts the public half here.\n`);
|
|
355
|
+
return 1;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// A known key gets its consequence shown, so a person setting one knows
|
|
359
|
+
// what it is for without leaving the terminal.
|
|
360
|
+
const known = allEnvKeys(project.manifest).find((key) => key.key === name);
|
|
361
|
+
const value = await readValue(name, known?.breaks, known?.where);
|
|
362
|
+
if (value === null) return 1;
|
|
363
|
+
|
|
364
|
+
const written = await encryptInto(root, publicKey, name, value);
|
|
365
|
+
if (!written.ok) {
|
|
366
|
+
console.error(`\nvc: ${written.error}\n`);
|
|
367
|
+
return 1;
|
|
368
|
+
}
|
|
369
|
+
const stillBare = plaintextSecretNames(root).includes(name);
|
|
370
|
+
if (stillBare) {
|
|
371
|
+
console.error(`\n${color.red("✗")} ${name} was written in the CLEAR — the public key did not encrypt it.\n Do not commit. Check DOTENV_PUBLIC_KEY_SECRETS in ${SECRETS_FILE}.\n`);
|
|
372
|
+
return 1;
|
|
373
|
+
}
|
|
374
|
+
console.log(`\n${color.green("✓")} ${name} encrypted into ${SECRETS_FILE}. Commit it.\n`);
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** The value, from a masked prompt or from a pipe. Never from argv. */
|
|
379
|
+
async function readValue(name: string, breaks?: string, where?: string): Promise<string | null> {
|
|
380
|
+
if (!process.stdin.isTTY) {
|
|
381
|
+
const chunks: Buffer[] = [];
|
|
382
|
+
for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
|
|
383
|
+
const piped = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
384
|
+
if (!piped) {
|
|
385
|
+
console.error(`\nvc: nothing on stdin to set ${name} to.\n`);
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
return piped;
|
|
389
|
+
}
|
|
390
|
+
const p = await import("@clack/prompts");
|
|
391
|
+
if (breaks) console.log(`\n${color.dim(breaks)}${where ? color.dim(` — from: ${where}`) : ""}`);
|
|
392
|
+
const answer = await p.password({
|
|
393
|
+
message: name,
|
|
394
|
+
validate: (input) => (input && input.length > 0 ? undefined : "a secret with no value is not a secret"),
|
|
395
|
+
});
|
|
396
|
+
if (p.isCancel(answer)) {
|
|
397
|
+
console.log(color.dim("\ncancelled; nothing changed.\n"));
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
return String(answer);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Encrypt one value into `.env.secrets`, in process.
|
|
406
|
+
*
|
|
407
|
+
* Deliberately NOT `dotenvx set NAME value -f …`: that takes the value as an
|
|
408
|
+
* argument, and an argument is visible in `ps` to every other process on the
|
|
409
|
+
* machine for as long as the call runs. This uses the same secp256k1 the CLI
|
|
410
|
+
* uses — it is the CLI's own library — so what it writes is byte-compatible
|
|
411
|
+
* with what `dotenvx decrypt` expects.
|
|
412
|
+
*/
|
|
413
|
+
export async function encryptInto(root: string, publicKey: string, name: string, value: string): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
414
|
+
const { encrypt } = await import("@dotenvx/primitives");
|
|
415
|
+
let ciphertext: string;
|
|
416
|
+
try {
|
|
417
|
+
ciphertext = encrypt(publicKey, value);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
return { ok: false, error: `could not encrypt to the key in ${SECRETS_FILE}: ${String(error)}` };
|
|
420
|
+
}
|
|
421
|
+
// A belt-and-braces check on someone else's library, because the failure
|
|
422
|
+
// it guards against is writing a secret to a committed file in the clear.
|
|
423
|
+
//
|
|
424
|
+
// The containment half is gated on length on purpose: base64 output will
|
|
425
|
+
// contain almost any short string by chance, so checking it for a
|
|
426
|
+
// two-character value rejects perfectly good ciphertext. Twelve is above
|
|
427
|
+
// the length at which a coincidental match stops being plausible.
|
|
428
|
+
const looksWrong = !ciphertext.startsWith("encrypted:") || ciphertext === value || (value.length >= 12 && ciphertext.includes(value));
|
|
429
|
+
if (looksWrong) {
|
|
430
|
+
return { ok: false, error: `refusing to write ${name} — the result does not look encrypted.` };
|
|
431
|
+
}
|
|
432
|
+
upsertLine(join(root, SECRETS_FILE), name, ciphertext);
|
|
433
|
+
return { ok: true };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Replace `NAME=…` in a dotenv file, or add it if it is not there.
|
|
438
|
+
*
|
|
439
|
+
* Written by hand rather than with a parser because everything else in the
|
|
440
|
+
* file — the comments explaining what each secret breaks, the order — is
|
|
441
|
+
* meant for a person to read, and a round-trip through a parser would
|
|
442
|
+
* flatten all of it.
|
|
443
|
+
*/
|
|
444
|
+
function upsertLine(path: string, name: string, value: string): void {
|
|
445
|
+
const body = readFileSync(path, "utf8");
|
|
446
|
+
const pattern = new RegExp(`^\\s*${name}\\s*=.*$`, "m");
|
|
447
|
+
writeFileSync(path, pattern.test(body) ? body.replace(pattern, `${name}=${value}`) : `${body.replace(/\n*$/, "\n")}${name}=${value}\n`);
|
|
448
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dotenvx/primitives` ships no types. Only one function is used here, and
|
|
3
|
+
* declaring just that is better than an `any` import: the shape is asserted
|
|
4
|
+
* in one place a reader can check against the package.
|
|
5
|
+
*/
|
|
6
|
+
declare module "@dotenvx/primitives" {
|
|
7
|
+
/** The secp256k1 public key for a private key, both as lowercase hex. */
|
|
8
|
+
export function derive(privateKey: string): string;
|
|
9
|
+
export function keypair(): { publicKey: string; privateKey: string };
|
|
10
|
+
/** Ciphertext for a value, given only the PUBLIC key. Note the argument order. */
|
|
11
|
+
export function encrypt(publicKey: string, value: string): string;
|
|
12
|
+
/** The value back, given the private key. Throws when the key is the wrong one. */
|
|
13
|
+
export function decrypt(privateKey: string, ciphertext: string): string;
|
|
14
|
+
}
|