@coffre/cli 0.1.13 → 0.1.15
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/README.md +3 -0
- package/dist/{connect-BXDkiWOL.js → connect-k2qEwjqZ.js} +1 -1
- package/dist/database-url-CJ9yqoi_.js +213 -0
- package/dist/deployment-Di8BG9qY.js +1456 -0
- package/dist/init-2xGelVG1.js +71 -0
- package/dist/{keys-BCKUEb-Y.js → keys-DDdMwm0N.js} +2 -352
- package/dist/main.js +361 -202
- package/dist/migrate-ERNNgYO0.js +163 -0
- package/dist/{setup-BMmFAR3N.js → setup-Bs8q8Ily.js} +17 -1810
- package/dist/templates/node/README.md +4 -2
- package/dist/templates/node/package.json +4 -3
- package/dist/templates/workers/README.md +4 -2
- package/dist/templates/workers/package.json +5 -4
- package/dist/tty-BDa14JX2.js +283 -0
- package/dist/update-eYzLTNoL.js +334 -0
- package/dist/version-CPk6zTTK.js +317 -0
- package/package.json +5 -5
- package/dist/{connect-tekAM4hC.js → connect-C4wevMJ5.js} +34 -34
package/dist/main.js
CHANGED
|
@@ -1,12 +1,270 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { n as init, t as KINDS } from "./init-2xGelVG1.js";
|
|
3
|
+
import { a as keys } from "./keys-DDdMwm0N.js";
|
|
4
|
+
import { a as openTerminal, c as release, d as style, r as hiddenLine, t as Cancelled, u as select } from "./tty-BDa14JX2.js";
|
|
3
5
|
import { i as isRole, r as assignableToEnvironment, t as ROLES } from "./access-CO1kGjWW.js";
|
|
4
6
|
import { inspect, parseArgs, promisify } from "node:util";
|
|
5
7
|
import { execFile, spawn } from "node:child_process";
|
|
6
|
-
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { homedir, hostname } from "node:os";
|
|
8
|
+
import { appendFileSync, chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { EOL, homedir, hostname } from "node:os";
|
|
8
10
|
import { dirname, join, resolve } from "node:path";
|
|
9
|
-
import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes, timingSafeEqual } from "node:crypto";
|
|
11
|
+
import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
12
|
+
/**
|
|
13
|
+
* The line to show, or null: nothing pending, or nothing told. An instance
|
|
14
|
+
* older than this notice has no `instance` on `/me` at all.
|
|
15
|
+
*/
|
|
16
|
+
function noticeFor(origin, instance) {
|
|
17
|
+
if (instance === null || instance === void 0 || !Array.isArray(instance.migrations?.known)) return null;
|
|
18
|
+
const pending = instance.migrations.known.slice(instance.migrations.applied);
|
|
19
|
+
if (pending.length === 0) return null;
|
|
20
|
+
return `${pending.length === 1 ? "1 migration" : `${pending.length} migrations`} pending on ${origin} (${pending.join(", ")}): run \`coffre migrate\``;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The notice for `origin`, at most once a day: null without asking when it
|
|
24
|
+
* was asked less than a day ago; otherwise `me` is asked, and the time kept,
|
|
25
|
+
* whatever the answer. A failure to ask says nothing, and is not kept.
|
|
26
|
+
*/
|
|
27
|
+
async function dailyNotice(origin, now, checked, me) {
|
|
28
|
+
const last = checked.read()[origin];
|
|
29
|
+
if (last !== void 0 && now - last < 864e5) return null;
|
|
30
|
+
let instance;
|
|
31
|
+
try {
|
|
32
|
+
({instance} = await me());
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
checked.write({
|
|
37
|
+
...checked.read(),
|
|
38
|
+
[origin]: now
|
|
39
|
+
});
|
|
40
|
+
return noticeFor(origin, instance);
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region ../core/dist/dotenv.js
|
|
44
|
+
const KEY_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
45
|
+
function parseDotenv(input) {
|
|
46
|
+
const entries = [];
|
|
47
|
+
const problems = [];
|
|
48
|
+
const seen = /* @__PURE__ */ new Set();
|
|
49
|
+
const lines = input.split(/\r\n|\n|\r/);
|
|
50
|
+
for (let index = 0; index < lines.length; index++) {
|
|
51
|
+
const lineNumber = index + 1;
|
|
52
|
+
const trimmed = lines[index].trim();
|
|
53
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
54
|
+
const withoutExport = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
|
|
55
|
+
const equals = withoutExport.indexOf("=");
|
|
56
|
+
if (equals === -1) {
|
|
57
|
+
problems.push({
|
|
58
|
+
line: lineNumber,
|
|
59
|
+
text: trimmed,
|
|
60
|
+
reason: "no \"=\" on this line"
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
65
|
+
if (!KEY_RE.test(key)) {
|
|
66
|
+
problems.push({
|
|
67
|
+
line: lineNumber,
|
|
68
|
+
text: key,
|
|
69
|
+
reason: "key must match ^[A-Za-z_][A-Za-z0-9_]*$"
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
let rest = withoutExport.slice(equals + 1).trim();
|
|
74
|
+
let value;
|
|
75
|
+
if (rest.startsWith("\"")) {
|
|
76
|
+
const closing = findClosingQuote(rest, "\"");
|
|
77
|
+
if (closing === -1) {
|
|
78
|
+
problems.push({
|
|
79
|
+
line: lineNumber,
|
|
80
|
+
text: key,
|
|
81
|
+
reason: "unterminated double quote (multi-line values are not supported)"
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
value = unescapeDoubleQuoted(rest.slice(1, closing));
|
|
86
|
+
if (trailingIsSignificant(rest.slice(closing + 1))) {
|
|
87
|
+
problems.push({
|
|
88
|
+
line: lineNumber,
|
|
89
|
+
text: key,
|
|
90
|
+
reason: "unexpected text after the closing quote"
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
} else if (rest.startsWith("'")) {
|
|
95
|
+
const closing = rest.indexOf("'", 1);
|
|
96
|
+
if (closing === -1) {
|
|
97
|
+
problems.push({
|
|
98
|
+
line: lineNumber,
|
|
99
|
+
text: key,
|
|
100
|
+
reason: "unterminated single quote (multi-line values are not supported)"
|
|
101
|
+
});
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
value = rest.slice(1, closing);
|
|
105
|
+
if (trailingIsSignificant(rest.slice(closing + 1))) {
|
|
106
|
+
problems.push({
|
|
107
|
+
line: lineNumber,
|
|
108
|
+
text: key,
|
|
109
|
+
reason: "unexpected text after the closing quote"
|
|
110
|
+
});
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
const comment = rest.indexOf(" #");
|
|
115
|
+
if (comment !== -1) rest = rest.slice(0, comment);
|
|
116
|
+
value = rest.trim();
|
|
117
|
+
}
|
|
118
|
+
if (value.includes("\0")) {
|
|
119
|
+
problems.push({
|
|
120
|
+
line: lineNumber,
|
|
121
|
+
text: key,
|
|
122
|
+
reason: "value contains a NUL byte, which cannot be passed in an environment"
|
|
123
|
+
});
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (seen.has(key)) {
|
|
127
|
+
problems.push({
|
|
128
|
+
line: lineNumber,
|
|
129
|
+
text: key,
|
|
130
|
+
reason: "duplicate key in this file"
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
seen.add(key);
|
|
135
|
+
entries.push({
|
|
136
|
+
key,
|
|
137
|
+
value,
|
|
138
|
+
line: lineNumber
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
entries,
|
|
143
|
+
problems
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** True when what follows a closing quote is neither whitespace nor a comment. */
|
|
147
|
+
function trailingIsSignificant(rest) {
|
|
148
|
+
const trimmed = rest.trim();
|
|
149
|
+
return trimmed !== "" && !trimmed.startsWith("#");
|
|
150
|
+
}
|
|
151
|
+
function findClosingQuote(text, quote) {
|
|
152
|
+
for (let index = 1; index < text.length; index++) {
|
|
153
|
+
if (text[index] === "\\") {
|
|
154
|
+
index++;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (text[index] === quote) return index;
|
|
158
|
+
}
|
|
159
|
+
return -1;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Expand escapes inside a double-quoted value.
|
|
163
|
+
*
|
|
164
|
+
* Deliberately minimal: only the escapes that are necessary. `\n` and `\r`
|
|
165
|
+
* because multi-line values (PEM keys) cannot otherwise be expressed on one
|
|
166
|
+
* line, and `\\` `\"` `\'` because quoting needs them.
|
|
167
|
+
*
|
|
168
|
+
* EVERYTHING ELSE IS PRESERVED VERBATIM, backslash included. The obvious
|
|
169
|
+
* implementation -- returning the character and dropping the backslash for
|
|
170
|
+
* unknown escapes -- silently mangles credentials: `"C:\path\to\file"` came
|
|
171
|
+
* out as `C:path<TAB>ofile`, and `"\d+\w"` as `d+w`. A secrets store that
|
|
172
|
+
* quietly rewrites what you gave it is worse than one that refuses.
|
|
173
|
+
*
|
|
174
|
+
* A value that should contain no interpretation at all belongs in single
|
|
175
|
+
* quotes, which are literal.
|
|
176
|
+
*/
|
|
177
|
+
function unescapeDoubleQuoted(text) {
|
|
178
|
+
return text.replace(/\\(.)/g, (match, character) => {
|
|
179
|
+
switch (character) {
|
|
180
|
+
case "n": return "\n";
|
|
181
|
+
case "r": return "\r";
|
|
182
|
+
case "\\":
|
|
183
|
+
case "\"":
|
|
184
|
+
case "'": return character;
|
|
185
|
+
default: return match;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/** Values that read the same unquoted: no spaces, quotes, `#`, `$` or backslashes. */
|
|
190
|
+
const PLAIN_VALUE = /^[A-Za-z0-9_./:@+,=%?-]*$/;
|
|
191
|
+
/**
|
|
192
|
+
* Write entries as a .env file that `parseDotenv` reads back exactly.
|
|
193
|
+
*
|
|
194
|
+
* Plain values stay bare; anything else is single-quoted, which is literal;
|
|
195
|
+
* values holding a quote or a line break are double-quoted with the four
|
|
196
|
+
* escapes the parser knows (`\\`, `\"`, `\n`, `\r`).
|
|
197
|
+
*/
|
|
198
|
+
function formatDotenv(entries) {
|
|
199
|
+
let out = "";
|
|
200
|
+
for (const [key, value] of entries) {
|
|
201
|
+
assertEnvironmentEntry(key, value);
|
|
202
|
+
out += `${key}=${quoteDotenv(value)}\n`;
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
/** Reject names and values that cannot be passed intact in an environment. */
|
|
207
|
+
function assertEnvironmentEntry(key, value) {
|
|
208
|
+
if (!KEY_RE.test(key)) throw new Error(`${JSON.stringify(key)} is not a valid variable name`);
|
|
209
|
+
if (value.includes("\0")) throw new Error(`${key} contains a NUL byte`);
|
|
210
|
+
}
|
|
211
|
+
function quoteDotenv(value) {
|
|
212
|
+
if (PLAIN_VALUE.test(value)) return value;
|
|
213
|
+
if (!/['\n\r]/.test(value)) return `'${value}'`;
|
|
214
|
+
return `"${value.replace(/[\\"\n\r]/g, (character) => {
|
|
215
|
+
switch (character) {
|
|
216
|
+
case "\n": return "\\n";
|
|
217
|
+
case "\r": return "\\r";
|
|
218
|
+
default: return `\\${character}`;
|
|
219
|
+
}
|
|
220
|
+
})}"`;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Write entries as POSIX shell `export` statements, for
|
|
224
|
+
* `eval "$(coffre export … --format shell)"`. Single quotes are the only
|
|
225
|
+
* shell quoting with no expansion inside; a single quote itself is written
|
|
226
|
+
* as `'\''`.
|
|
227
|
+
*/
|
|
228
|
+
function formatShellExports(entries) {
|
|
229
|
+
let out = "";
|
|
230
|
+
for (const [key, value] of entries) {
|
|
231
|
+
assertEnvironmentEntry(key, value);
|
|
232
|
+
out += `export ${key}='${value.replaceAll("'", "'\\''")}'\n`;
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/github-env.ts
|
|
238
|
+
/** The runner decodes %, CR and LF in command data, in this escaping order.
|
|
239
|
+
* https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts
|
|
240
|
+
*/
|
|
241
|
+
function githubMasks(entries) {
|
|
242
|
+
let out = "";
|
|
243
|
+
for (const [, value] of entries) for (const mask of /* @__PURE__ */ new Set([value, ...value.split(/\r\n|\r|\n/)])) {
|
|
244
|
+
if (mask === "") continue;
|
|
245
|
+
out += `::add-mask::${mask.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A")}\n`;
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
/** Multiline-safe environment records, consumed by GitHub after the step.
|
|
250
|
+
* https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#multiline-strings
|
|
251
|
+
* Match the host's framing newline, including on Windows, so a trailing CR
|
|
252
|
+
* in the value is not mistaken for part of that newline.
|
|
253
|
+
*/
|
|
254
|
+
function githubEnvironment(entries, nextDelimiter = () => `coffre_${randomUUID()}`, newline = EOL) {
|
|
255
|
+
let out = "";
|
|
256
|
+
for (const [key, value] of entries) {
|
|
257
|
+
assertEnvironmentEntry(key, value);
|
|
258
|
+
if (key.toUpperCase() === "NODE_OPTIONS") throw new Error(`${key} cannot be set through GITHUB_ENV`);
|
|
259
|
+
let delimiter;
|
|
260
|
+
do
|
|
261
|
+
delimiter = nextDelimiter();
|
|
262
|
+
while (value.includes(delimiter));
|
|
263
|
+
out += `${key}<<${delimiter}${newline}${value}${newline}${delimiter}${newline}`;
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
//#endregion
|
|
10
268
|
//#region ../client/dist/index.js
|
|
11
269
|
/** The API's error shape, `{ error, message }`, with the HTTP status. */
|
|
12
270
|
const COFFRE_ERROR = Symbol.for("@coffre/client:CoffreError");
|
|
@@ -1521,199 +1779,6 @@ async function pickCheck() {
|
|
|
1521
1779
|
}
|
|
1522
1780
|
}
|
|
1523
1781
|
//#endregion
|
|
1524
|
-
//#region ../core/dist/dotenv.js
|
|
1525
|
-
const KEY_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
1526
|
-
function parseDotenv(input) {
|
|
1527
|
-
const entries = [];
|
|
1528
|
-
const problems = [];
|
|
1529
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1530
|
-
const lines = input.split(/\r\n|\n|\r/);
|
|
1531
|
-
for (let index = 0; index < lines.length; index++) {
|
|
1532
|
-
const lineNumber = index + 1;
|
|
1533
|
-
const trimmed = lines[index].trim();
|
|
1534
|
-
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
1535
|
-
const withoutExport = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
|
|
1536
|
-
const equals = withoutExport.indexOf("=");
|
|
1537
|
-
if (equals === -1) {
|
|
1538
|
-
problems.push({
|
|
1539
|
-
line: lineNumber,
|
|
1540
|
-
text: trimmed,
|
|
1541
|
-
reason: "no \"=\" on this line"
|
|
1542
|
-
});
|
|
1543
|
-
continue;
|
|
1544
|
-
}
|
|
1545
|
-
const key = withoutExport.slice(0, equals).trim();
|
|
1546
|
-
if (!KEY_RE.test(key)) {
|
|
1547
|
-
problems.push({
|
|
1548
|
-
line: lineNumber,
|
|
1549
|
-
text: key,
|
|
1550
|
-
reason: "key must match ^[A-Za-z_][A-Za-z0-9_]*$"
|
|
1551
|
-
});
|
|
1552
|
-
continue;
|
|
1553
|
-
}
|
|
1554
|
-
let rest = withoutExport.slice(equals + 1).trim();
|
|
1555
|
-
let value;
|
|
1556
|
-
if (rest.startsWith("\"")) {
|
|
1557
|
-
const closing = findClosingQuote(rest, "\"");
|
|
1558
|
-
if (closing === -1) {
|
|
1559
|
-
problems.push({
|
|
1560
|
-
line: lineNumber,
|
|
1561
|
-
text: key,
|
|
1562
|
-
reason: "unterminated double quote (multi-line values are not supported)"
|
|
1563
|
-
});
|
|
1564
|
-
continue;
|
|
1565
|
-
}
|
|
1566
|
-
value = unescapeDoubleQuoted(rest.slice(1, closing));
|
|
1567
|
-
if (trailingIsSignificant(rest.slice(closing + 1))) {
|
|
1568
|
-
problems.push({
|
|
1569
|
-
line: lineNumber,
|
|
1570
|
-
text: key,
|
|
1571
|
-
reason: "unexpected text after the closing quote"
|
|
1572
|
-
});
|
|
1573
|
-
continue;
|
|
1574
|
-
}
|
|
1575
|
-
} else if (rest.startsWith("'")) {
|
|
1576
|
-
const closing = rest.indexOf("'", 1);
|
|
1577
|
-
if (closing === -1) {
|
|
1578
|
-
problems.push({
|
|
1579
|
-
line: lineNumber,
|
|
1580
|
-
text: key,
|
|
1581
|
-
reason: "unterminated single quote (multi-line values are not supported)"
|
|
1582
|
-
});
|
|
1583
|
-
continue;
|
|
1584
|
-
}
|
|
1585
|
-
value = rest.slice(1, closing);
|
|
1586
|
-
if (trailingIsSignificant(rest.slice(closing + 1))) {
|
|
1587
|
-
problems.push({
|
|
1588
|
-
line: lineNumber,
|
|
1589
|
-
text: key,
|
|
1590
|
-
reason: "unexpected text after the closing quote"
|
|
1591
|
-
});
|
|
1592
|
-
continue;
|
|
1593
|
-
}
|
|
1594
|
-
} else {
|
|
1595
|
-
const comment = rest.indexOf(" #");
|
|
1596
|
-
if (comment !== -1) rest = rest.slice(0, comment);
|
|
1597
|
-
value = rest.trim();
|
|
1598
|
-
}
|
|
1599
|
-
if (value.includes("\0")) {
|
|
1600
|
-
problems.push({
|
|
1601
|
-
line: lineNumber,
|
|
1602
|
-
text: key,
|
|
1603
|
-
reason: "value contains a NUL byte, which cannot be passed in an environment"
|
|
1604
|
-
});
|
|
1605
|
-
continue;
|
|
1606
|
-
}
|
|
1607
|
-
if (seen.has(key)) {
|
|
1608
|
-
problems.push({
|
|
1609
|
-
line: lineNumber,
|
|
1610
|
-
text: key,
|
|
1611
|
-
reason: "duplicate key in this file"
|
|
1612
|
-
});
|
|
1613
|
-
continue;
|
|
1614
|
-
}
|
|
1615
|
-
seen.add(key);
|
|
1616
|
-
entries.push({
|
|
1617
|
-
key,
|
|
1618
|
-
value,
|
|
1619
|
-
line: lineNumber
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
return {
|
|
1623
|
-
entries,
|
|
1624
|
-
problems
|
|
1625
|
-
};
|
|
1626
|
-
}
|
|
1627
|
-
/** True when what follows a closing quote is neither whitespace nor a comment. */
|
|
1628
|
-
function trailingIsSignificant(rest) {
|
|
1629
|
-
const trimmed = rest.trim();
|
|
1630
|
-
return trimmed !== "" && !trimmed.startsWith("#");
|
|
1631
|
-
}
|
|
1632
|
-
function findClosingQuote(text, quote) {
|
|
1633
|
-
for (let index = 1; index < text.length; index++) {
|
|
1634
|
-
if (text[index] === "\\") {
|
|
1635
|
-
index++;
|
|
1636
|
-
continue;
|
|
1637
|
-
}
|
|
1638
|
-
if (text[index] === quote) return index;
|
|
1639
|
-
}
|
|
1640
|
-
return -1;
|
|
1641
|
-
}
|
|
1642
|
-
/**
|
|
1643
|
-
* Expand escapes inside a double-quoted value.
|
|
1644
|
-
*
|
|
1645
|
-
* Deliberately minimal: only the escapes that are necessary. `\n` and `\r`
|
|
1646
|
-
* because multi-line values (PEM keys) cannot otherwise be expressed on one
|
|
1647
|
-
* line, and `\\` `\"` `\'` because quoting needs them.
|
|
1648
|
-
*
|
|
1649
|
-
* EVERYTHING ELSE IS PRESERVED VERBATIM, backslash included. The obvious
|
|
1650
|
-
* implementation -- returning the character and dropping the backslash for
|
|
1651
|
-
* unknown escapes -- silently mangles credentials: `"C:\path\to\file"` came
|
|
1652
|
-
* out as `C:path<TAB>ofile`, and `"\d+\w"` as `d+w`. A secrets store that
|
|
1653
|
-
* quietly rewrites what you gave it is worse than one that refuses.
|
|
1654
|
-
*
|
|
1655
|
-
* A value that should contain no interpretation at all belongs in single
|
|
1656
|
-
* quotes, which are literal.
|
|
1657
|
-
*/
|
|
1658
|
-
function unescapeDoubleQuoted(text) {
|
|
1659
|
-
return text.replace(/\\(.)/g, (match, character) => {
|
|
1660
|
-
switch (character) {
|
|
1661
|
-
case "n": return "\n";
|
|
1662
|
-
case "r": return "\r";
|
|
1663
|
-
case "\\":
|
|
1664
|
-
case "\"":
|
|
1665
|
-
case "'": return character;
|
|
1666
|
-
default: return match;
|
|
1667
|
-
}
|
|
1668
|
-
});
|
|
1669
|
-
}
|
|
1670
|
-
/** Values that read the same unquoted: no spaces, quotes, `#`, `$` or backslashes. */
|
|
1671
|
-
const PLAIN_VALUE = /^[A-Za-z0-9_./:@+,=%?-]*$/;
|
|
1672
|
-
/**
|
|
1673
|
-
* Write entries as a .env file that `parseDotenv` reads back exactly.
|
|
1674
|
-
*
|
|
1675
|
-
* Plain values stay bare; anything else is single-quoted, which is literal;
|
|
1676
|
-
* values holding a quote or a line break are double-quoted with the four
|
|
1677
|
-
* escapes the parser knows (`\\`, `\"`, `\n`, `\r`).
|
|
1678
|
-
*/
|
|
1679
|
-
function formatDotenv(entries) {
|
|
1680
|
-
let out = "";
|
|
1681
|
-
for (const [key, value] of entries) {
|
|
1682
|
-
assertWritable(key, value);
|
|
1683
|
-
out += `${key}=${quoteDotenv(value)}\n`;
|
|
1684
|
-
}
|
|
1685
|
-
return out;
|
|
1686
|
-
}
|
|
1687
|
-
function assertWritable(key, value) {
|
|
1688
|
-
if (!KEY_RE.test(key)) throw new Error(`${JSON.stringify(key)} is not a valid variable name`);
|
|
1689
|
-
if (value.includes("\0")) throw new Error(`${key} contains a NUL byte`);
|
|
1690
|
-
}
|
|
1691
|
-
function quoteDotenv(value) {
|
|
1692
|
-
if (PLAIN_VALUE.test(value)) return value;
|
|
1693
|
-
if (!/['\n\r]/.test(value)) return `'${value}'`;
|
|
1694
|
-
return `"${value.replace(/[\\"\n\r]/g, (character) => {
|
|
1695
|
-
switch (character) {
|
|
1696
|
-
case "\n": return "\\n";
|
|
1697
|
-
case "\r": return "\\r";
|
|
1698
|
-
default: return `\\${character}`;
|
|
1699
|
-
}
|
|
1700
|
-
})}"`;
|
|
1701
|
-
}
|
|
1702
|
-
/**
|
|
1703
|
-
* Write entries as POSIX shell `export` statements, for
|
|
1704
|
-
* `eval "$(coffre export … --format shell)"`. Single quotes are the only
|
|
1705
|
-
* shell quoting with no expansion inside; a single quote itself is written
|
|
1706
|
-
* as `'\''`.
|
|
1707
|
-
*/
|
|
1708
|
-
function formatShellExports(entries) {
|
|
1709
|
-
let out = "";
|
|
1710
|
-
for (const [key, value] of entries) {
|
|
1711
|
-
assertWritable(key, value);
|
|
1712
|
-
out += `export ${key}='${value.replaceAll("'", "'\\''")}'\n`;
|
|
1713
|
-
}
|
|
1714
|
-
return out;
|
|
1715
|
-
}
|
|
1716
|
-
//#endregion
|
|
1717
1782
|
//#region src/main.ts
|
|
1718
1783
|
/**
|
|
1719
1784
|
* coffre CLI.
|
|
@@ -1797,8 +1862,59 @@ function client(to = target()) {
|
|
|
1797
1862
|
transport: (request) => send(request, to)
|
|
1798
1863
|
});
|
|
1799
1864
|
}
|
|
1865
|
+
/** Whether this run has asked about pending migrations yet: once, before its first request. */
|
|
1866
|
+
let noticed = false;
|
|
1867
|
+
/** The commands that say nothing of pending migrations: migrate says as much itself; login has no session yet; verify sends only what it checks. */
|
|
1868
|
+
const QUIET = /* @__PURE__ */ new Set([
|
|
1869
|
+
"migrate",
|
|
1870
|
+
"login",
|
|
1871
|
+
"verify"
|
|
1872
|
+
]);
|
|
1873
|
+
/**
|
|
1874
|
+
* A line on stderr, at most once a day per instance, when its database is
|
|
1875
|
+
* behind the code it runs (`notice.ts`). Not for a machine's credential:
|
|
1876
|
+
* only owners are told. Whatever goes wrong here, the command goes on.
|
|
1877
|
+
*/
|
|
1878
|
+
async function noticePending(to) {
|
|
1879
|
+
if (noticed) return;
|
|
1880
|
+
noticed = true;
|
|
1881
|
+
if (QUIET.has(command ?? "") || process.env.COFFRE_TOKEN?.trim() || to.credential.kind === "access-service-token") return;
|
|
1882
|
+
const line = await dailyNotice(to.origin, Date.now(), checkedFile, async () => {
|
|
1883
|
+
const response = await fetch(`${to.origin}/api/me`, {
|
|
1884
|
+
headers: await headersFor(to),
|
|
1885
|
+
redirect: "manual"
|
|
1886
|
+
});
|
|
1887
|
+
if (!response.ok || !isJsonContentType(response.headers.get("content-type"))) throw new Error("no answer");
|
|
1888
|
+
return await response.json();
|
|
1889
|
+
}).catch(() => null);
|
|
1890
|
+
if (line !== null) {
|
|
1891
|
+
const s = style(process.stderr);
|
|
1892
|
+
process.stderr.write(`${s.dim(`coffre: ${line}`)}\n`);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
const CHECKED_PATH = join(homedir(), ".coffre", "checked.json");
|
|
1896
|
+
/** When each instance was last asked about its migrations: nothing secret, beside the credentials. */
|
|
1897
|
+
const checkedFile = {
|
|
1898
|
+
read() {
|
|
1899
|
+
try {
|
|
1900
|
+
return JSON.parse(readFileSync(CHECKED_PATH, "utf8"));
|
|
1901
|
+
} catch {
|
|
1902
|
+
return {};
|
|
1903
|
+
}
|
|
1904
|
+
},
|
|
1905
|
+
write(checked) {
|
|
1906
|
+
try {
|
|
1907
|
+
mkdirSync(dirname(CHECKED_PATH), {
|
|
1908
|
+
recursive: true,
|
|
1909
|
+
mode: 448
|
|
1910
|
+
});
|
|
1911
|
+
writeFileSync(CHECKED_PATH, `${JSON.stringify(checked)}\n`, { mode: 384 });
|
|
1912
|
+
} catch {}
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1800
1915
|
/** One request; every way it can fail is explained in terms of what to do next. */
|
|
1801
1916
|
async function send(request, to) {
|
|
1917
|
+
await noticePending(to);
|
|
1802
1918
|
let response;
|
|
1803
1919
|
try {
|
|
1804
1920
|
response = await fetch(request, { redirect: "manual" });
|
|
@@ -2108,14 +2224,22 @@ async function exportEnv(args) {
|
|
|
2108
2224
|
} },
|
|
2109
2225
|
allowPositionals: true
|
|
2110
2226
|
});
|
|
2111
|
-
const usage = "usage: coffre export <project>/<environment> [--format dotenv|json|shell]";
|
|
2227
|
+
const usage = "usage: coffre export <project>/<environment> [--format dotenv|json|shell|github]";
|
|
2112
2228
|
if (!positionals[0]) fail(usage);
|
|
2113
2229
|
const format = values.format;
|
|
2114
|
-
if (format !== "dotenv" && format !== "json" && format !== "shell") fail(usage);
|
|
2230
|
+
if (format !== "dotenv" && format !== "json" && format !== "shell" && format !== "github") fail(usage);
|
|
2231
|
+
const githubEnv = process.env.GITHUB_ENV;
|
|
2232
|
+
if (format === "github" && !githubEnv) fail("--format github requires GITHUB_ENV (run it in a GitHub Actions step)");
|
|
2115
2233
|
const { project, environment } = parsePath(positionals[0]);
|
|
2116
2234
|
const revealed = await client().secrets.reveal(`${project}/${environment}`);
|
|
2117
2235
|
const entries = Object.entries(revealed.values).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2118
|
-
|
|
2236
|
+
if (format === "github") {
|
|
2237
|
+
process.stdout.write(githubMasks(entries));
|
|
2238
|
+
attempt(() => appendFileSync(githubEnv, githubEnvironment(entries), {
|
|
2239
|
+
encoding: "utf8",
|
|
2240
|
+
mode: 384
|
|
2241
|
+
}));
|
|
2242
|
+
} else process.stdout.write(format === "json" ? `${JSON.stringify(Object.fromEntries(entries), null, 2)}\n` : attempt(() => format === "shell" ? formatShellExports(entries) : formatDotenv(entries)));
|
|
2119
2243
|
if (process.stderr.isTTY) {
|
|
2120
2244
|
const count = entries.length;
|
|
2121
2245
|
process.stderr.write(`coffre: read ${count} secret${count === 1 ? "" : "s"} from ${project}/${environment}; each read is in the audit log under your name\n`);
|
|
@@ -2392,6 +2516,30 @@ function initProject(args) {
|
|
|
2392
2516
|
const at = dir === process.cwd() ? null : positionals[0] ?? null;
|
|
2393
2517
|
process.stdout.write(`\nA ${kind === "workers" ? "Cloudflare Workers" : "Node"} deployment of coffre ${version}${at === null ? " here" : ` in ${at}`}. README.md takes it from there:\n\n${at === null ? "" : ` cd ${/\s/.test(at) ? `'${at}'` : at}\n`} pnpm install\n`);
|
|
2394
2518
|
}
|
|
2519
|
+
/**
|
|
2520
|
+
* The instance `coffre migrate` works on: the current one, or the one
|
|
2521
|
+
* `--url` names, with the session saved for it.
|
|
2522
|
+
*/
|
|
2523
|
+
function migrateTarget(url) {
|
|
2524
|
+
if (url !== void 0) process.env.COFFRE_API_URL = url;
|
|
2525
|
+
const to = target();
|
|
2526
|
+
const api = client(to);
|
|
2527
|
+
return {
|
|
2528
|
+
origin: to.origin,
|
|
2529
|
+
me: () => api.me(),
|
|
2530
|
+
ready: async () => {
|
|
2531
|
+
try {
|
|
2532
|
+
return await (await fetch(`${to.origin}/readyz`, { redirect: "manual" })).json();
|
|
2533
|
+
} catch {
|
|
2534
|
+
return {
|
|
2535
|
+
ok: false,
|
|
2536
|
+
heartbeatAgeSeconds: null,
|
|
2537
|
+
checkpointed: false
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2395
2543
|
const USAGE = `coffre - secrets, with an audit log
|
|
2396
2544
|
|
|
2397
2545
|
New deployment
|
|
@@ -2403,6 +2551,11 @@ const USAGE = `coffre - secrets, with an audit log
|
|
|
2403
2551
|
Cloudflare too, and in an empty directory, the deployment
|
|
2404
2552
|
coffre keys [--json] the app key, vault key and vault ID alone, shown the same way
|
|
2405
2553
|
|
|
2554
|
+
Upgrade
|
|
2555
|
+
coffre update [--yes] this CLI, and in a deployment, its coffre packages
|
|
2556
|
+
coffre migrate [--url <url>] [--yes] the instance's database, to the schema its version ships,
|
|
2557
|
+
with the owner's connection string, asked for hidden
|
|
2558
|
+
|
|
2406
2559
|
Session
|
|
2407
2560
|
coffre login [<url>] [--no-browser] sign in, and make <url> the current instance
|
|
2408
2561
|
coffre logout [<url>]
|
|
@@ -2414,7 +2567,7 @@ const USAGE = `coffre - secrets, with an audit log
|
|
|
2414
2567
|
coffre get <project>/<environment>/<KEY>
|
|
2415
2568
|
coffre set <project>/<environment>/<KEY> [value] (reads stdin if omitted)
|
|
2416
2569
|
coffre run <project>/<environment> -- <command>
|
|
2417
|
-
coffre export <project>/<environment> [--format dotenv|json|shell]
|
|
2570
|
+
coffre export <project>/<environment> [--format dotenv|json|shell|github]
|
|
2418
2571
|
coffre history <project>/<environment>/<KEY>
|
|
2419
2572
|
coffre rollback <project>/<environment>/<KEY> <version>
|
|
2420
2573
|
coffre import <project>/<environment> [--file .env] [--apply]
|
|
@@ -2453,7 +2606,13 @@ switch (command) {
|
|
|
2453
2606
|
await keys(rest);
|
|
2454
2607
|
break;
|
|
2455
2608
|
case "setup":
|
|
2456
|
-
await (await import("./setup-
|
|
2609
|
+
await (await import("./setup-Bs8q8Ily.js")).setup(rest);
|
|
2610
|
+
break;
|
|
2611
|
+
case "update":
|
|
2612
|
+
await (await import("./update-eYzLTNoL.js")).update(rest);
|
|
2613
|
+
break;
|
|
2614
|
+
case "migrate":
|
|
2615
|
+
await (await import("./migrate-ERNNgYO0.js")).migrate(rest, migrateTarget);
|
|
2457
2616
|
break;
|
|
2458
2617
|
case "login":
|
|
2459
2618
|
await login(rest);
|