@coffre/cli 0.1.1 → 0.1.2
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 +4 -1
- package/dist/access-CO1kGjWW.js +74 -0
- package/dist/connect-BONR52al.js +770 -0
- package/dist/connect-BZdPYNbZ.js +2 -0
- package/dist/esm-B69mMLcR.js +2 -0
- package/dist/esm-Bts-esAl.js +4202 -0
- package/dist/keys-D9CG_wYV.js +64 -0
- package/dist/main.js +9 -136
- package/dist/migrations/postgres/0000_baseline.sql +505 -0
- package/dist/migrations/postgres/meta/0000_snapshot.json +1939 -0
- package/dist/migrations/postgres/meta/_journal.json +13 -0
- package/dist/migrator-W1a-ooWi.js +37 -0
- package/dist/node-postgres-BlNvYfq_.js +2 -0
- package/dist/node-postgres-_y-EeSHQ.js +2831 -0
- package/dist/portable-CPh2thNN.js +87 -0
- package/dist/relations-CJCGPRst.js +2712 -0
- package/dist/schema-sqlite-Byp7B3yI.js +1078 -0
- package/dist/setup-DPHVyr2N.js +521 -0
- package/dist/templates/node/README.md +37 -47
- package/dist/templates/node/package.json +3 -3
- package/dist/templates/node/server.env.example +3 -2
- package/dist/templates/node/src/server.ts +1 -1
- package/dist/templates/node/src/vault.ts +5 -3
- package/dist/templates/node/vault.env.example +4 -5
- package/dist/templates/workers/README.md +40 -55
- package/dist/templates/workers/app/wrangler.jsonc +1 -1
- package/dist/templates/workers/package.json +4 -4
- package/dist/templates/workers/vault/src/worker.ts +5 -4
- package/dist/templates/workers/vault/wrangler.jsonc +4 -3
- package/package.json +7 -4
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
//#region src/keys.ts
|
|
4
|
+
/**
|
|
5
|
+
* Two fresh keys, each 32 random bytes in base64, and an id for the KEK
|
|
6
|
+
* dated to the day, so that a rotation, even one the same month, gets a new
|
|
7
|
+
* one: the vault refuses two KEKs with the same id.
|
|
8
|
+
*/
|
|
9
|
+
function generateKeys(now = /* @__PURE__ */ new Date()) {
|
|
10
|
+
const key = () => randomBytes(32).toString("base64");
|
|
11
|
+
return {
|
|
12
|
+
KEK_ID: `kek-${now.toISOString().slice(0, 10)}`,
|
|
13
|
+
KEK: key(),
|
|
14
|
+
AUDIT_CHAIN_KEY: key()
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** What each key is for, as comments: `coffre keys` and `coffre setup` both say it. */
|
|
18
|
+
const KEYS_EXPLAINED = `# Two keys, one for each component, so that the app, which faces the
|
|
19
|
+
# network, never holds what decrypts a value:
|
|
20
|
+
#
|
|
21
|
+
# KEK the vault's. Decrypts every value, and signs the vault's
|
|
22
|
+
# log entries, member rows and checkpoints. Lose it, and
|
|
23
|
+
# every value is lost for good.
|
|
24
|
+
# AUDIT_CHAIN_KEY the app's. Signs the app's log entries, sessions and
|
|
25
|
+
# tokens. Lose it, and everyone is signed out and the log
|
|
26
|
+
# stops verifying.
|
|
27
|
+
# KEK_ID names the KEK; not secret.
|
|
28
|
+
#
|
|
29
|
+
# Whoever has the KEK and a copy of the database has every value: keep it
|
|
30
|
+
# apart from the backups.
|
|
31
|
+
`;
|
|
32
|
+
/** The keys as a dotenv block, then, as comments, what each is for and where it goes. */
|
|
33
|
+
function formatKeys(keys) {
|
|
34
|
+
return `KEK_ID=${keys.KEK_ID}
|
|
35
|
+
KEK=${keys.KEK}
|
|
36
|
+
AUDIT_CHAIN_KEY=${keys.AUDIT_CHAIN_KEY}
|
|
37
|
+
|
|
38
|
+
# Save all three in your password manager now. They are shown once, and
|
|
39
|
+
# coffre keeps no copy.
|
|
40
|
+
#
|
|
41
|
+
${KEYS_EXPLAINED}#
|
|
42
|
+
# On Workers, KEK_ID is a var in vault/wrangler.jsonc, and KEK and
|
|
43
|
+
# AUDIT_CHAIN_KEY are Worker secrets (wrangler secret put). On Node, KEK_ID
|
|
44
|
+
# and KEK go in vault.env, and AUDIT_CHAIN_KEY in server.env.
|
|
45
|
+
#
|
|
46
|
+
# These are for a new deployment. To rotate a deployment's KEK, take only
|
|
47
|
+
# KEK_ID and KEK, and keep the old pair in previousKeks, for what it wrapped
|
|
48
|
+
# and signed. AUDIT_CHAIN_KEY cannot be changed.
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
function keys(args) {
|
|
52
|
+
const { values } = parseArgs({
|
|
53
|
+
args,
|
|
54
|
+
options: { json: {
|
|
55
|
+
type: "boolean",
|
|
56
|
+
default: false
|
|
57
|
+
} },
|
|
58
|
+
allowPositionals: false
|
|
59
|
+
});
|
|
60
|
+
const fresh = generateKeys();
|
|
61
|
+
process.stdout.write(values.json ? `${JSON.stringify(fresh)}\n` : formatKeys(fresh));
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { generateKeys as n, keys as r, KEYS_EXPLAINED as t };
|
package/dist/main.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { r as keys } from "./keys-D9CG_wYV.js";
|
|
3
|
+
import { i as isRole, r as assignableToEnvironment, t as ROLES } from "./access-CO1kGjWW.js";
|
|
2
4
|
import { parseArgs, promisify } from "node:util";
|
|
3
5
|
import { execFile, spawn } from "node:child_process";
|
|
4
6
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
7
|
import { homedir, hostname } from "node:os";
|
|
6
8
|
import { basename, dirname, join, resolve } from "node:path";
|
|
7
9
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { randomBytes } from "node:crypto";
|
|
9
10
|
//#region src/init.ts
|
|
10
11
|
const KINDS = ["workers", "node"];
|
|
11
12
|
/** Where a template lives: in the package when published, else in the repository. */
|
|
@@ -69,67 +70,6 @@ function init(kind, target, version) {
|
|
|
69
70
|
return written;
|
|
70
71
|
}
|
|
71
72
|
//#endregion
|
|
72
|
-
//#region src/keys.ts
|
|
73
|
-
/**
|
|
74
|
-
* Three fresh keys, each 32 random bytes in base64, and an id for the KEK
|
|
75
|
-
* dated to the day, so that a rotation, even one the same month, gets a new
|
|
76
|
-
* one: the vault refuses two KEKs with the same id.
|
|
77
|
-
*/
|
|
78
|
-
function generateKeys(now = /* @__PURE__ */ new Date()) {
|
|
79
|
-
const key = () => randomBytes(32).toString("base64");
|
|
80
|
-
return {
|
|
81
|
-
KEK_ID: `kek-${now.toISOString().slice(0, 10)}`,
|
|
82
|
-
KEK: key(),
|
|
83
|
-
SIGNING_KEY: key(),
|
|
84
|
-
AUDIT_CHAIN_KEY: key()
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
/** The keys as a dotenv block, then, as comments, what each is for and where it goes. */
|
|
88
|
-
function formatKeys(keys) {
|
|
89
|
-
return `KEK_ID=${keys.KEK_ID}
|
|
90
|
-
KEK=${keys.KEK}
|
|
91
|
-
SIGNING_KEY=${keys.SIGNING_KEY}
|
|
92
|
-
AUDIT_CHAIN_KEY=${keys.AUDIT_CHAIN_KEY}
|
|
93
|
-
|
|
94
|
-
# Save all four in your password manager now. They are shown once, and
|
|
95
|
-
# coffre keeps no copy.
|
|
96
|
-
#
|
|
97
|
-
# The vault gets KEK_ID, KEK and SIGNING_KEY; the app gets AUDIT_CHAIN_KEY.
|
|
98
|
-
# They are kept apart so that the app, which faces the network, never holds
|
|
99
|
-
# what decrypts a value: whoever has the KEK and a copy of the database has
|
|
100
|
-
# every value.
|
|
101
|
-
#
|
|
102
|
-
# KEK decrypts every value. Lose it, and every value is lost.
|
|
103
|
-
# SIGNING_KEY signs the vault's log entries, checkpoints and member
|
|
104
|
-
# rows. Lose it, and the log stops verifying and every
|
|
105
|
-
# member is refused.
|
|
106
|
-
# AUDIT_CHAIN_KEY signs the app's log entries, sessions and tokens. Lose
|
|
107
|
-
# it, and the log stops verifying and everyone is signed
|
|
108
|
-
# out.
|
|
109
|
-
# KEK_ID names the KEK; not secret.
|
|
110
|
-
#
|
|
111
|
-
# On Workers, KEK_ID is a var in vault/wrangler.jsonc, and the other three are
|
|
112
|
-
# Worker secrets (wrangler secret put). On Node, they go in vault.env and
|
|
113
|
-
# server.env.
|
|
114
|
-
#
|
|
115
|
-
# These are for a new deployment. To rotate a deployment's KEK, take only
|
|
116
|
-
# KEK_ID and KEK, and keep the old pair in previousKeks: its SIGNING_KEY and
|
|
117
|
-
# AUDIT_CHAIN_KEY cannot be changed.
|
|
118
|
-
`;
|
|
119
|
-
}
|
|
120
|
-
function keys(args) {
|
|
121
|
-
const { values } = parseArgs({
|
|
122
|
-
args,
|
|
123
|
-
options: { json: {
|
|
124
|
-
type: "boolean",
|
|
125
|
-
default: false
|
|
126
|
-
} },
|
|
127
|
-
allowPositionals: false
|
|
128
|
-
});
|
|
129
|
-
const fresh = generateKeys();
|
|
130
|
-
process.stdout.write(values.json ? `${JSON.stringify(fresh)}\n` : formatKeys(fresh));
|
|
131
|
-
}
|
|
132
|
-
//#endregion
|
|
133
73
|
//#region src/instance.ts
|
|
134
74
|
const MODES = ["signin", "cloudflare"];
|
|
135
75
|
function emptyStore() {
|
|
@@ -528,79 +468,6 @@ async function planImport(coffre, path, entries) {
|
|
|
528
468
|
};
|
|
529
469
|
}
|
|
530
470
|
//#endregion
|
|
531
|
-
//#region ../core/dist/access.js
|
|
532
|
-
/**
|
|
533
|
-
* Who may do what: seven permissions and the six roles that bundle them.
|
|
534
|
-
*
|
|
535
|
-
* Deliberately constants and not tables or a policy language. A grant names
|
|
536
|
-
* one role for one member on one place, and a check is a lookup in this file,
|
|
537
|
-
* so authorisation stays something you can read end to end.
|
|
538
|
-
*/
|
|
539
|
-
const PERMISSIONS = [
|
|
540
|
-
"secret.read",
|
|
541
|
-
"secret.write",
|
|
542
|
-
"secret.archive",
|
|
543
|
-
"audit.read",
|
|
544
|
-
"environment.manage",
|
|
545
|
-
"grant.manage",
|
|
546
|
-
"project.manage"
|
|
547
|
-
];
|
|
548
|
-
/**
|
|
549
|
-
* Permissions that only make sense on a project. Granting
|
|
550
|
-
* `environment.manage` on one environment would be incoherent: what it
|
|
551
|
-
* authorises is creating that environment's siblings.
|
|
552
|
-
*/
|
|
553
|
-
const PROJECT_ONLY_PERMISSIONS = [
|
|
554
|
-
"environment.manage",
|
|
555
|
-
"grant.manage",
|
|
556
|
-
"project.manage"
|
|
557
|
-
];
|
|
558
|
-
const ROLES = {
|
|
559
|
-
viewer: {
|
|
560
|
-
name: "Viewer",
|
|
561
|
-
description: "Read secret values.",
|
|
562
|
-
permissions: ["secret.read"]
|
|
563
|
-
},
|
|
564
|
-
developer: {
|
|
565
|
-
name: "Developer",
|
|
566
|
-
description: "Read and write secrets.",
|
|
567
|
-
permissions: ["secret.read", "secret.write"]
|
|
568
|
-
},
|
|
569
|
-
maintainer: {
|
|
570
|
-
name: "Maintainer",
|
|
571
|
-
description: "Read, write and retire secrets, and manage environments.",
|
|
572
|
-
permissions: [
|
|
573
|
-
"secret.read",
|
|
574
|
-
"secret.write",
|
|
575
|
-
"secret.archive",
|
|
576
|
-
"environment.manage"
|
|
577
|
-
]
|
|
578
|
-
},
|
|
579
|
-
"access-manager": {
|
|
580
|
-
name: "Access manager",
|
|
581
|
-
description: "Manage who has access. Cannot read secret values.",
|
|
582
|
-
permissions: ["grant.manage"]
|
|
583
|
-
},
|
|
584
|
-
auditor: {
|
|
585
|
-
name: "Auditor",
|
|
586
|
-
description: "Read the audit log. Cannot read secret values.",
|
|
587
|
-
permissions: ["audit.read"]
|
|
588
|
-
},
|
|
589
|
-
owner: {
|
|
590
|
-
name: "Owner",
|
|
591
|
-
description: "Everything, including reading secret values.",
|
|
592
|
-
permissions: [...PERMISSIONS]
|
|
593
|
-
}
|
|
594
|
-
};
|
|
595
|
-
Object.keys(ROLES);
|
|
596
|
-
function isRole(value) {
|
|
597
|
-
return Object.hasOwn(ROLES, value);
|
|
598
|
-
}
|
|
599
|
-
/** Roles that hold a project-only permission can only be granted on a project. */
|
|
600
|
-
function assignableToEnvironment(role) {
|
|
601
|
-
return !ROLES[role].permissions.some((permission) => PROJECT_ONLY_PERMISSIONS.includes(permission));
|
|
602
|
-
}
|
|
603
|
-
//#endregion
|
|
604
471
|
//#region ../core/dist/dotenv.js
|
|
605
472
|
const KEY_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
606
473
|
function parseDotenv(input) {
|
|
@@ -1593,7 +1460,10 @@ const USAGE = `coffre - secrets, with an audit log
|
|
|
1593
1460
|
New deployment
|
|
1594
1461
|
coffre init --workers [<dir>] two Cloudflare Workers: the app and its vault
|
|
1595
1462
|
coffre init --node [<dir>] a Node server, and its vault beside it
|
|
1596
|
-
coffre
|
|
1463
|
+
coffre setup [--reset-passwords] [--json]
|
|
1464
|
+
its database logins, migrations and keys, in one go,
|
|
1465
|
+
shown once (the connection string is asked for)
|
|
1466
|
+
coffre keys [--json] its keys and the KEK's id alone, made here and shown once
|
|
1597
1467
|
|
|
1598
1468
|
Session
|
|
1599
1469
|
coffre login [<url>] [--no-browser] sign in, and make <url> the current instance
|
|
@@ -1645,6 +1515,9 @@ switch (command) {
|
|
|
1645
1515
|
case "keys":
|
|
1646
1516
|
keys(rest);
|
|
1647
1517
|
break;
|
|
1518
|
+
case "setup":
|
|
1519
|
+
await (await import("./setup-DPHVyr2N.js")).setup(rest);
|
|
1520
|
+
break;
|
|
1648
1521
|
case "login":
|
|
1649
1522
|
await login(rest);
|
|
1650
1523
|
break;
|