create-kelpie 0.4.0 → 0.4.1
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 +1 -0
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +1 -0
- package/dist/scaffold.js.map +1 -1
- package/package.json +1 -1
- package/src/scaffold.ts +1 -0
- package/templates/README.md +15 -6
- package/templates/env +11 -1
- package/templates/package.json +1 -0
- package/templates/src/reseal.ts +107 -0
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ package.json @kelpie/server and @kelpie/ui, pinned to this version
|
|
|
20
20
|
kelpie.config.ts the server module list
|
|
21
21
|
kelpie.ui.config.ts the UI module list
|
|
22
22
|
src/server.ts the entry point: config, modules, migrations, serve
|
|
23
|
+
src/reseal.ts re-seals stored secrets after a SECRET_ENCRYPTION_KEY change
|
|
23
24
|
web/index.html the web entry
|
|
24
25
|
web/main.tsx
|
|
25
26
|
vite.config.ts dev server, proxying /v1 to the API
|
package/dist/scaffold.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,0FAA0F;IAC1F,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,qEAAqE;IACrE,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAA;IACpC,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED,wEAAwE;AACxE,qBAAa,aAAc,SAAQ,KAAK;IACtC,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;
|
|
1
|
+
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,0FAA0F;IAC1F,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,qEAAqE;IACrE,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAA;IACpC,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED,wEAAwE;AACxE,qBAAa,aAAc,SAAQ,KAAK;IACtC,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AA4DD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAkBtD;AAED,gGAAgG;AAChG,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,SAAS,MAAM,EAAE,CAsBpE"}
|
package/dist/scaffold.js
CHANGED
|
@@ -29,6 +29,7 @@ const TEMPLATE_FILES = new Map([
|
|
|
29
29
|
['kelpie.config.ts', 'kelpie.config.ts'],
|
|
30
30
|
['kelpie.ui.config.ts', 'kelpie.ui.config.ts'],
|
|
31
31
|
['src/server.ts', 'src/server.ts'],
|
|
32
|
+
['src/reseal.ts', 'src/reseal.ts'],
|
|
32
33
|
['web/index.html', 'web/index.html'],
|
|
33
34
|
['web/main.tsx', 'web/main.tsx'],
|
|
34
35
|
['vite.config.ts', 'vite.config.ts'],
|
package/dist/scaffold.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACzF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,+EAA+E;AAC/E,MAAM,iBAAiB,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,eAAe,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AAmBlF,wEAAwE;AACxE,MAAM,OAAO,aAAc,SAAQ,KAAK;IACtC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,eAAe,CAAA;IAC7B,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,cAAc,GAAgC,IAAI,GAAG,CAAC;IAC1D,CAAC,cAAc,EAAE,cAAc,CAAC;IAChC,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;IACxC,CAAC,qBAAqB,EAAE,qBAAqB,CAAC;IAC9C,CAAC,eAAe,EAAE,eAAe,CAAC;IAClC,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IACpC,CAAC,cAAc,EAAE,cAAc,CAAC;IAChC,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IACpC,CAAC,sBAAsB,EAAE,sBAAsB,CAAC;IAChD,CAAC,mBAAmB,EAAE,mBAAmB,CAAC;IAC1C,CAAC,WAAW,EAAE,WAAW,CAAC;IAC1B,CAAC,KAAK,EAAE,MAAM,CAAC;IACf,CAAC,WAAW,EAAE,YAAY,CAAC;CAC5B,CAAC,CAAA;AAEF,gEAAgE;AAChE,MAAM,eAAe,GAAG,oBAAoB,CAAA;AAE5C,kFAAkF;AAClF,MAAM,gBAAgB,GAAG,qBAAqB,CAAA;AAE9C,SAAS,SAAS,CAAC,OAAwB;IACzC,OAAO,IAAI,GAAG,CAAC;QACb,CAAC,kBAAkB,EAAE,OAAO,CAAC,WAAW,CAAC;QACzC,CAAC,kBAAkB,EAAE,OAAO,CAAC,WAAW,CAAC;QACzC,CAAC,kBAAkB,EAAE,OAAO,CAAC,WAAW,CAAC;QACzC,CAAC,mBAAmB,EAAE,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QACnD,CAAC,gBAAgB,EAAE,OAAO,CAAC,SAAS,CAAC;QACrC,CAAC,UAAU,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAClC,CAAC,cAAc,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACzC,CAAC,2BAA2B,EAAE,OAAO,CAAC,mBAAmB,CAAC;KAC3D,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,MAAM,CAAC,QAAgB,EAAE,MAAmC,EAAE,MAAc;IACnF,IAAI,QAAQ,GAAG,QAAQ,CAAA;IAEvB,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;QACpC,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;IAC9C,CAAC;IAED,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAEhD,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,MAAM,IAAI,aAAa,CAAC,gBAAgB,MAAM,mBAAmB,QAAQ,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAA;IACrG,CAAC;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC9C,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3B,OAAM;IACR,CAAC;IAED,MAAM,OAAO,GAAG,WAAW,CAAC,SAAS,CAAC,CAAA;IAEtC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,aAAa,CACrB,GAAG,SAAS,2BAA2B,OAAO,CAAC,MAAM,QAAQ,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;YACjG,YAAY;YACZ,OAAO;iBACJ,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;iBACX,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,KAAK,GAAG,CAAC;iBAC5B,IAAI,CAAC,IAAI,CAAC;YACb,+DAA+D,CAClE,CAAA;IACH,CAAC;AACH,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,QAAQ,CAAC,OAAwB;IAC/C,cAAc,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IAEjC,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,CAAA;IACjC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,CAAA;IAErC,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,KAAK,CAAC,GAAG,CAAC,eAAe,EAAE,eAAe,CAAC,CAAA;IAC7C,CAAC;IAED,MAAM,OAAO,GAAa,EAAE,CAAA;IAE5B,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,CAAA;QACtE,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;QAEnD,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACpD,aAAa,CAAC,WAAW,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;QAC5D,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACtB,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC"}
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/scaffold.ts
CHANGED
|
@@ -50,6 +50,7 @@ const TEMPLATE_FILES: ReadonlyMap<string, string> = new Map([
|
|
|
50
50
|
['kelpie.config.ts', 'kelpie.config.ts'],
|
|
51
51
|
['kelpie.ui.config.ts', 'kelpie.ui.config.ts'],
|
|
52
52
|
['src/server.ts', 'src/server.ts'],
|
|
53
|
+
['src/reseal.ts', 'src/reseal.ts'],
|
|
53
54
|
['web/index.html', 'web/index.html'],
|
|
54
55
|
['web/main.tsx', 'web/main.tsx'],
|
|
55
56
|
['vite.config.ts', 'vite.config.ts'],
|
package/templates/README.md
CHANGED
|
@@ -40,8 +40,10 @@ Then open <http://localhost:__WEB_PORT__/signup> and create an account.
|
|
|
40
40
|
Passwords need at least 12 characters. Signup names your workspace, invites your
|
|
41
41
|
team, and leaves you on People with a starter handbook in place.
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
API's log
|
|
43
|
+
This default configuration doesn't send email. `EMAIL_PROVIDER=log` prints
|
|
44
|
+
invitation and password-reset links to the API's log instead of mailing them;
|
|
45
|
+
copy them from there to follow either flow. Set `EMAIL_PROVIDER=smtp` below to
|
|
46
|
+
send them for real.
|
|
45
47
|
|
|
46
48
|
## Adding a module
|
|
47
49
|
|
|
@@ -51,8 +53,9 @@ on an unknown id or an unmet dependency rather than starting without it.
|
|
|
51
53
|
|
|
52
54
|
## Configuration
|
|
53
55
|
|
|
54
|
-
Everything below is required unless marked optional
|
|
55
|
-
stops the service at boot and
|
|
56
|
+
Everything below is required unless marked optional or conditional on another
|
|
57
|
+
variable's value. A missing or invalid value stops the service at boot and
|
|
58
|
+
lists every problem at once.
|
|
56
59
|
|
|
57
60
|
| Variable | Values |
|
|
58
61
|
| --- | --- |
|
|
@@ -62,8 +65,13 @@ stops the service at boot and lists every problem at once.
|
|
|
62
65
|
| `WEB_PORT` | The Vite dev server's port. Development only |
|
|
63
66
|
| `DATABASE_URL` | A `postgres://` or `postgresql://` connection string |
|
|
64
67
|
| `LOG_LEVEL` | `debug`, `info`, `warn`, or `error` |
|
|
65
|
-
| `EMAIL_PROVIDER` | `log`.
|
|
68
|
+
| `EMAIL_PROVIDER` | `log` or `smtp`. `log` writes invitations and resets to the log instead of sending them |
|
|
66
69
|
| `EMAIL_FROM` | The address transactional mail comes from |
|
|
70
|
+
| `SMTP_HOST` | Required when `EMAIL_PROVIDER=smtp`. The mail server to connect to |
|
|
71
|
+
| `SMTP_PORT` | Required when `EMAIL_PROVIDER=smtp`. The mail server's port |
|
|
72
|
+
| `SMTP_SECURE` | Required when `EMAIL_PROVIDER=smtp`. `true` or `false`. Whether to connect over TLS from the start (typically port 465) rather than upgrading with STARTTLS (typically port 587 or 25) |
|
|
73
|
+
| `SMTP_USER` | Required when `EMAIL_PROVIDER=smtp`. The SMTP username |
|
|
74
|
+
| `SMTP_PASSWORD` | Required when `EMAIL_PROVIDER=smtp`. The SMTP password |
|
|
67
75
|
| `SECRET_ENCRYPTION_KEY` | 32 bytes of base64, generated for this project |
|
|
68
76
|
| `SECRET_ENCRYPTION_KEY_PREVIOUS` | Optional. Set only while rotating the key above |
|
|
69
77
|
| `WEBHOOK_DELIVERY_RETENTION_DAYS` | Optional, default 30 |
|
|
@@ -81,7 +89,8 @@ unreadable, so rotate rather than replace:
|
|
|
81
89
|
`SECRET_ENCRYPTION_KEY`.
|
|
82
90
|
2. Deploy. New secrets seal under the new key; existing ones still read with the
|
|
83
91
|
previous one.
|
|
84
|
-
3.
|
|
92
|
+
3. Run `npm run reseal`. It rewrites every row still sealed under the old key.
|
|
93
|
+
Safe to run more than once.
|
|
85
94
|
4. Remove `SECRET_ENCRYPTION_KEY_PREVIOUS` and deploy again.
|
|
86
95
|
|
|
87
96
|
## Deploying
|
package/templates/env
CHANGED
|
@@ -22,6 +22,16 @@ DATABASE_URL=__DATABASE_URL__
|
|
|
22
22
|
SECRET_ENCRYPTION_KEY=__SECRET_ENCRYPTION_KEY__
|
|
23
23
|
|
|
24
24
|
# `log` writes invitations and password resets to the log instead of sending
|
|
25
|
-
# them.
|
|
25
|
+
# them. `smtp` sends for real, through the server and credentials below.
|
|
26
26
|
EMAIL_PROVIDER=log
|
|
27
27
|
EMAIL_FROM=__EMAIL_FROM__
|
|
28
|
+
|
|
29
|
+
# Required only when EMAIL_PROVIDER=smtp; ignored otherwise. SMTP_SECURE is
|
|
30
|
+
# "true" for a server that expects TLS from the first byte (usually port 465),
|
|
31
|
+
# "false" for one you connect to in plain text and that upgrades with STARTTLS
|
|
32
|
+
# (usually port 587 or 25).
|
|
33
|
+
# SMTP_HOST=smtp.example.com
|
|
34
|
+
# SMTP_PORT=587
|
|
35
|
+
# SMTP_SECURE=false
|
|
36
|
+
# SMTP_USER=
|
|
37
|
+
# SMTP_PASSWORD=
|
package/templates/package.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"dev:api": "node --watch --env-file-if-exists=.env src/server.ts",
|
|
12
12
|
"dev:web": "vite",
|
|
13
13
|
"start": "node --env-file-if-exists=.env src/server.ts",
|
|
14
|
+
"reseal": "node --env-file-if-exists=.env src/reseal.ts",
|
|
14
15
|
"build": "vite build",
|
|
15
16
|
"typecheck": "tsc --project tsconfig.server.json && tsc --project tsconfig.web.json"
|
|
16
17
|
},
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConfigurationError,
|
|
3
|
+
connectDatabase,
|
|
4
|
+
createLogger,
|
|
5
|
+
createSecretCipher,
|
|
6
|
+
loadConfig,
|
|
7
|
+
resealStoredSecrets,
|
|
8
|
+
secretEncryptionConfigSchema,
|
|
9
|
+
} from '@kelpie/server'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Re-seals stored secrets after `SECRET_ENCRYPTION_KEY` changes.
|
|
13
|
+
*
|
|
14
|
+
* The rotation procedure, in full:
|
|
15
|
+
*
|
|
16
|
+
* 1. Keep the current key. Add the new one as `SECRET_ENCRYPTION_KEY` and move
|
|
17
|
+
* the old value to `SECRET_ENCRYPTION_KEY_PREVIOUS`.
|
|
18
|
+
* 2. Deploy. Deliveries keep signing: new secrets seal under the new key, and
|
|
19
|
+
* existing ones still open under the previous one.
|
|
20
|
+
* 3. Run `npm run reseal`. It rewrites every row still sealed under the old key.
|
|
21
|
+
* 4. Remove `SECRET_ENCRYPTION_KEY_PREVIOUS` and deploy again.
|
|
22
|
+
*
|
|
23
|
+
* Safe to run at any point, including with no previous key set, where it reports
|
|
24
|
+
* that everything is already current and writes nothing.
|
|
25
|
+
*
|
|
26
|
+
* A script rather than a subcommand because the repository has no CLI surface
|
|
27
|
+
* yet. When self-host packaging adds one, this becomes a subcommand of it and
|
|
28
|
+
* the body moves across unchanged.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
function report(message: string): void {
|
|
32
|
+
process.stdout.write(`${message}\n`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function reportFatal(message: string): void {
|
|
36
|
+
process.stderr.write(`${message}\n`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function reseal(): Promise<number> {
|
|
40
|
+
const config = loadConfig(process.env)
|
|
41
|
+
const logger = createLogger(config.logLevel)
|
|
42
|
+
const database = connectDatabase(config.databaseUrl, logger)
|
|
43
|
+
|
|
44
|
+
// Validated here rather than trusted, so a mistyped key fails before the pass
|
|
45
|
+
// opens a single row instead of reporting every row unreadable.
|
|
46
|
+
const secretConfig = secretEncryptionConfigSchema.safeParse(process.env)
|
|
47
|
+
|
|
48
|
+
if (!secretConfig.success) {
|
|
49
|
+
await database.close()
|
|
50
|
+
|
|
51
|
+
throw new ConfigurationError(
|
|
52
|
+
secretConfig.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`),
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const hasPrevious =
|
|
57
|
+
secretConfig.data.SECRET_ENCRYPTION_KEY_PREVIOUS !== undefined &&
|
|
58
|
+
secretConfig.data.SECRET_ENCRYPTION_KEY_PREVIOUS.trim().length > 0
|
|
59
|
+
|
|
60
|
+
if (!hasPrevious) {
|
|
61
|
+
report('SECRET_ENCRYPTION_KEY_PREVIOUS is not set. Nothing sealed under an older key can be read.')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const outcome = await resealStoredSecrets(database.db, createSecretCipher(secretConfig.data))
|
|
66
|
+
|
|
67
|
+
for (const column of outcome.columns) {
|
|
68
|
+
report(
|
|
69
|
+
`${column.label}: ${String(column.examined)} examined, ${String(column.resealed)} re-sealed, ${String(column.unreadable.length)} unreadable`,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
for (const id of column.unreadable) {
|
|
73
|
+
reportFatal(` unreadable: ${id}`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (outcome.unreadable > 0) {
|
|
78
|
+
reportFatal(
|
|
79
|
+
`\n${String(outcome.unreadable)} value(s) opened under neither key. They were sealed under a key that is not configured, or the rows have been altered. Nothing here can recover them: set SECRET_ENCRYPTION_KEY_PREVIOUS to the key they were sealed with and run this again, or have those records re-created.`,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return 1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
report(
|
|
86
|
+
outcome.resealed === 0
|
|
87
|
+
? `\nNothing to do: all ${String(outcome.examined)} value(s) are already sealed under the current key.`
|
|
88
|
+
: `\nRe-sealed ${String(outcome.resealed)} of ${String(outcome.examined)} value(s). You can now remove SECRET_ENCRYPTION_KEY_PREVIOUS.`,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return 0
|
|
92
|
+
} finally {
|
|
93
|
+
await database.close()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
process.exit(await reseal())
|
|
99
|
+
} catch (error: unknown) {
|
|
100
|
+
if (error instanceof ConfigurationError) {
|
|
101
|
+
reportFatal(error.message)
|
|
102
|
+
reportFatal('Set SECRET_ENCRYPTION_KEY, and SECRET_ENCRYPTION_KEY_PREVIOUS if you are mid-rotation.')
|
|
103
|
+
process.exit(1)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw error
|
|
107
|
+
}
|