@dokebi/cli 0.4.2 → 0.4.27
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/AI.md +279 -57
- package/README.md +35 -10
- package/catalog.json +2520 -258
- package/describe.schema.json +748 -0
- package/dist/api-app.d.ts +2 -0
- package/dist/api-app.js +35 -0
- package/dist/app-urls.js +3 -2
- package/dist/bin.js +638 -37
- package/dist/capability-guide.d.ts +30 -0
- package/dist/capability-guide.js +122 -0
- package/dist/catalog.d.ts +16 -2
- package/dist/catalog.js +19 -7
- package/dist/check/asset-rules.d.ts +6 -0
- package/dist/check/asset-rules.js +163 -0
- package/dist/check/browser-sources.d.ts +20 -0
- package/dist/check/browser-sources.js +89 -0
- package/dist/check/console-rules.d.ts +6 -0
- package/dist/check/console-rules.js +40 -0
- package/dist/check/error-rules.d.ts +21 -0
- package/dist/check/error-rules.js +212 -0
- package/dist/check/form-rules.d.ts +12 -0
- package/dist/check/form-rules.js +69 -6
- package/dist/check/formatter-rules.d.ts +2 -0
- package/dist/check/formatter-rules.js +71 -0
- package/dist/check/i18n-rules.d.ts +27 -0
- package/dist/check/i18n-rules.js +353 -0
- package/dist/check/index.d.ts +15 -1
- package/dist/check/index.js +89 -11
- package/dist/check/mail-rules.d.ts +4 -0
- package/dist/check/mail-rules.js +72 -0
- package/dist/check/module-rules.d.ts +13 -0
- package/dist/check/module-rules.js +104 -0
- package/dist/check/packages.d.ts +8 -0
- package/dist/check/packages.js +16 -0
- package/dist/check/pool-rules.d.ts +8 -0
- package/dist/check/pool-rules.js +164 -0
- package/dist/check/problems.d.ts +6 -1
- package/dist/check/problems.js +100 -2
- package/dist/check/project-rules.d.ts +23 -3
- package/dist/check/project-rules.js +265 -23
- package/dist/check/seal-rules.d.ts +6 -0
- package/dist/check/seal-rules.js +68 -0
- package/dist/check/security-rules.d.ts +10 -0
- package/dist/check/security-rules.js +450 -0
- package/dist/check/shared-state-rules.d.ts +15 -0
- package/dist/check/shared-state-rules.js +117 -0
- package/dist/check/source-rules.d.ts +26 -3
- package/dist/check/source-rules.js +249 -28
- package/dist/check/structure-rules.d.ts +23 -0
- package/dist/check/structure-rules.js +502 -0
- package/dist/check/timer-rules.d.ts +7 -0
- package/dist/check/timer-rules.js +543 -0
- package/dist/check/typescript.d.ts +35 -5
- package/dist/check/typescript.js +163 -17
- package/dist/check/vite-rules.d.ts +2 -0
- package/dist/check/vite-rules.js +136 -0
- package/dist/check/wait-rules.d.ts +16 -0
- package/dist/check/wait-rules.js +337 -0
- package/dist/commands.d.ts +7 -0
- package/dist/commands.js +381 -36
- package/dist/config-diff.d.ts +42 -0
- package/dist/config-diff.js +289 -0
- package/dist/config-edit.d.ts +35 -2
- package/dist/config-edit.js +145 -13
- package/dist/config-init.d.ts +15 -0
- package/dist/config-init.js +61 -0
- package/dist/config-paths.d.ts +22 -0
- package/dist/config-paths.js +82 -0
- package/dist/config-secrets.d.ts +46 -0
- package/dist/config-secrets.js +187 -0
- package/dist/config-show.d.ts +82 -0
- package/dist/config-show.js +563 -0
- package/dist/config-values.d.ts +8 -0
- package/dist/config-values.js +68 -0
- package/dist/configuration-scaffold.d.ts +42 -0
- package/dist/configuration-scaffold.js +258 -4
- package/dist/configuration.d.ts +63 -3
- package/dist/configuration.js +178 -4
- package/dist/deploy-scripts.d.ts +4 -0
- package/dist/deploy-scripts.js +361 -0
- package/dist/deploy-templates.d.ts +46 -0
- package/dist/deploy-templates.js +379 -0
- package/dist/deploy.d.ts +46 -0
- package/dist/deploy.js +592 -0
- package/dist/deployment.d.ts +2 -0
- package/dist/deployment.js +111 -0
- package/dist/describe-db.d.ts +22 -0
- package/dist/describe-db.js +176 -0
- package/dist/describe.d.ts +92 -0
- package/dist/describe.js +417 -0
- package/dist/dev-compose.d.ts +20 -0
- package/dist/dev-compose.js +154 -0
- package/dist/dev-guard.js +44 -0
- package/dist/dev-host.d.ts +13 -0
- package/dist/dev-host.js +55 -0
- package/dist/dev-infra.d.ts +72 -4
- package/dist/dev-infra.js +237 -17
- package/dist/dev-watch.d.ts +34 -0
- package/dist/dev-watch.js +192 -0
- package/dist/diagnostics.d.ts +10 -7
- package/dist/diagnostics.js +22 -4
- package/dist/drift.d.ts +50 -0
- package/dist/drift.js +218 -0
- package/dist/env-file.d.ts +25 -1
- package/dist/env-file.js +64 -6
- package/dist/errors.d.ts +7 -1
- package/dist/example-config.d.ts +2 -0
- package/dist/example-config.js +3 -0
- package/dist/first-admin.d.ts +43 -0
- package/dist/first-admin.js +175 -0
- package/dist/formatter-ignore.d.ts +37 -0
- package/dist/formatter-ignore.js +344 -0
- package/dist/generator.d.ts +45 -3
- package/dist/generator.js +205 -15
- package/dist/guide.d.ts +1 -1
- package/dist/guide.js +42 -21
- package/dist/install-module.d.ts +20 -2
- package/dist/install-module.js +45 -8
- package/dist/install-plan.js +5 -3
- package/dist/layout.d.ts +12 -1
- package/dist/layout.js +19 -6
- package/dist/log-writer-control.d.ts +2 -0
- package/dist/log-writer-control.js +17 -2
- package/dist/log-writer.js +43 -10
- package/dist/logging-config.d.ts +8 -3
- package/dist/logging-config.js +19 -5
- package/dist/manifest.d.ts +29 -1
- package/dist/manifest.js +84 -6
- package/dist/merge-install-defaults.d.ts +0 -1
- package/dist/merge-install-defaults.js +13 -47
- package/dist/npm-client.d.ts +40 -4
- package/dist/npm-client.js +152 -15
- package/dist/npm-paths.js +18 -4
- package/dist/openapi.d.ts +27 -0
- package/dist/openapi.js +649 -0
- package/dist/option-fixes.d.ts +39 -0
- package/dist/option-fixes.js +130 -0
- package/dist/output.d.ts +1 -1
- package/dist/output.js +5 -5
- package/dist/plugin-lifecycle.d.ts +34 -2
- package/dist/plugin-lifecycle.js +195 -16
- package/dist/plugin-options.d.ts +19 -4
- package/dist/plugin-options.js +138 -6
- package/dist/plugins-command.js +44 -7
- package/dist/process-group.d.ts +39 -0
- package/dist/process-group.js +132 -0
- package/dist/production-doctor.d.ts +3 -0
- package/dist/production-doctor.js +176 -0
- package/dist/project-guide.d.ts +19 -1
- package/dist/project-guide.js +119 -20
- package/dist/project-inspector.d.ts +15 -0
- package/dist/project-inspector.js +497 -33
- package/dist/project-state.d.ts +5 -0
- package/dist/project-state.js +8 -0
- package/dist/project.d.ts +203 -19
- package/dist/project.js +3006 -333
- package/dist/recipe-facts.d.ts +127 -0
- package/dist/recipe-facts.js +563 -0
- package/dist/recipe-login.d.ts +63 -0
- package/dist/recipe-login.js +464 -0
- package/dist/recipe-mail.d.ts +21 -0
- package/dist/recipe-mail.js +214 -0
- package/dist/recipe-members.d.ts +35 -0
- package/dist/recipe-members.js +178 -0
- package/dist/recipe-record.d.ts +82 -7
- package/dist/recipe-record.js +226 -22
- package/dist/recipe-reset.d.ts +2 -0
- package/dist/recipe-reset.js +117 -0
- package/dist/recipe-site.d.ts +48 -0
- package/dist/recipe-site.js +396 -0
- package/dist/recipe-step.d.ts +42 -0
- package/dist/recipe-step.js +89 -0
- package/dist/recipe.d.ts +56 -20
- package/dist/recipe.js +703 -131
- package/dist/registry.d.ts +98 -2
- package/dist/registry.js +171 -5
- package/dist/reloadable.d.ts +35 -0
- package/dist/reloadable.js +257 -0
- package/dist/remote-verify.d.ts +31 -0
- package/dist/remote-verify.js +204 -0
- package/dist/server.d.ts +43 -4
- package/dist/server.js +601 -52
- package/dist/settings.d.ts +48 -0
- package/dist/settings.js +168 -0
- package/dist/site-auth.d.ts +39 -0
- package/dist/site-auth.js +195 -0
- package/dist/template-fixes.d.ts +26 -0
- package/dist/template-fixes.js +285 -0
- package/dist/test-reporter.d.ts +49 -0
- package/dist/test-reporter.js +190 -0
- package/dist/transaction.d.ts +47 -0
- package/dist/transaction.js +241 -7
- package/dist/verify-tests.d.ts +98 -0
- package/dist/verify-tests.js +449 -0
- package/dist/verify.d.ts +103 -3
- package/dist/verify.js +272 -21
- package/dist/view-choice.d.ts +58 -0
- package/dist/view-choice.js +131 -0
- package/dist/vite-watch.js +4 -2
- package/package.json +8 -6
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** The option paths of one file a command changed: dotted, below `options`; a list is one path. */
|
|
2
|
+
export interface OptionPaths {
|
|
3
|
+
readonly removed: string[];
|
|
4
|
+
readonly set: string[];
|
|
5
|
+
readonly added: string[];
|
|
6
|
+
}
|
|
7
|
+
/** The leaf paths (a scalar or a list each) that differ between the options of two sources. */
|
|
8
|
+
export declare function optionChanges(before: string, after: string): OptionPaths;
|
|
9
|
+
/** Each file's changes, the files nothing changed in left out. */
|
|
10
|
+
export declare function filePaths(files: readonly (readonly [string, string | undefined, string | undefined])[]): Record<string, OptionPaths>;
|
|
11
|
+
/**
|
|
12
|
+
* A requested path as the files spell it: `<section>.<path>` with named list items turned into
|
|
13
|
+
* indexes in `source` (`logging.targets.studio.token` → `logging.targets.3.token`); as typed when
|
|
14
|
+
* the item is not there.
|
|
15
|
+
*/
|
|
16
|
+
export declare function requestedPath(source: string, section: string, path: readonly string[]): string[];
|
|
17
|
+
/**
|
|
18
|
+
* The changed paths outside the requested ones. A changed path is asked for when it is a
|
|
19
|
+
* requested path or below one, or when it is a list a requested path goes through (a list is
|
|
20
|
+
* compared whole).
|
|
21
|
+
*/
|
|
22
|
+
export declare function outsideRequested(changes: Readonly<Record<string, OptionPaths>>, requested: readonly (readonly string[])[]): string[];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// What a config command changed, by option path and file (Studio v2 report, 2026-10-24): before
|
|
2
|
+
// d48f77b1 `config unset dokebi-logger.redact …` removed seven options of the live
|
|
3
|
+
// options.logging and answered only `changed: [dokebi.yaml, dokebi.example.yaml]`. The result now
|
|
4
|
+
// names every option path a file gained, lost or changed (never a value), and a change outside
|
|
5
|
+
// what the command was asked for is refused before anything is written (PATH_MISMATCH).
|
|
6
|
+
import { parseDocument } from 'yaml';
|
|
7
|
+
import { resolveItems } from './config-edit.js';
|
|
8
|
+
import { parseProjectConfig } from './configuration.js';
|
|
9
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
10
|
+
/** The leaf paths (a scalar or a list each) that differ between the options of two sources. */
|
|
11
|
+
export function optionChanges(before, after) {
|
|
12
|
+
const removed = [];
|
|
13
|
+
const set = [];
|
|
14
|
+
const added = [];
|
|
15
|
+
const leaves = (value, path, into) => {
|
|
16
|
+
if (isObject(value) && Object.keys(value).length)
|
|
17
|
+
for (const [key, item] of Object.entries(value))
|
|
18
|
+
leaves(item, [...path, key], into);
|
|
19
|
+
else
|
|
20
|
+
into.push(path.join('.'));
|
|
21
|
+
};
|
|
22
|
+
const walk = (left, right, path, hasLeft, hasRight) => {
|
|
23
|
+
if (hasLeft && !hasRight)
|
|
24
|
+
return leaves(left, path, removed);
|
|
25
|
+
if (hasRight && !hasLeft)
|
|
26
|
+
return leaves(right, path, added);
|
|
27
|
+
if (isObject(left) &&
|
|
28
|
+
isObject(right) &&
|
|
29
|
+
(Object.keys(left).length || Object.keys(right).length)) {
|
|
30
|
+
for (const key of new Set([...Object.keys(left), ...Object.keys(right)]))
|
|
31
|
+
walk(left[key], right[key], [...path, key], Object.hasOwn(left, key), Object.hasOwn(right, key));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (JSON.stringify(left) !== JSON.stringify(right))
|
|
35
|
+
set.push(path.join('.'));
|
|
36
|
+
};
|
|
37
|
+
const a = parseProjectConfig(before).options;
|
|
38
|
+
const b = parseProjectConfig(after).options;
|
|
39
|
+
for (const key of new Set([...Object.keys(a), ...Object.keys(b)]))
|
|
40
|
+
walk(a[key], b[key], [key], Object.hasOwn(a, key), Object.hasOwn(b, key));
|
|
41
|
+
return { removed, set, added };
|
|
42
|
+
}
|
|
43
|
+
/** Each file's changes, the files nothing changed in left out. */
|
|
44
|
+
export function filePaths(files) {
|
|
45
|
+
const result = {};
|
|
46
|
+
for (const [file, before, after] of files) {
|
|
47
|
+
if (before === undefined || after === undefined || before === after)
|
|
48
|
+
continue;
|
|
49
|
+
const changes = optionChanges(before, after);
|
|
50
|
+
if (changes.removed.length || changes.set.length || changes.added.length)
|
|
51
|
+
result[file] = changes;
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A requested path as the files spell it: `<section>.<path>` with named list items turned into
|
|
57
|
+
* indexes in `source` (`logging.targets.studio.token` → `logging.targets.3.token`); as typed when
|
|
58
|
+
* the item is not there.
|
|
59
|
+
*/
|
|
60
|
+
export function requestedPath(source, section, path) {
|
|
61
|
+
const resolved = resolveItems(parseDocument(source, { version: '1.2' }), [
|
|
62
|
+
'options',
|
|
63
|
+
section,
|
|
64
|
+
...path,
|
|
65
|
+
]);
|
|
66
|
+
return resolved ? resolved.slice(1) : [section, ...path];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The changed paths outside the requested ones. A changed path is asked for when it is a
|
|
70
|
+
* requested path or below one, or when it is a list a requested path goes through (a list is
|
|
71
|
+
* compared whole).
|
|
72
|
+
*/
|
|
73
|
+
export function outsideRequested(changes, requested) {
|
|
74
|
+
const asked = requested.map((path) => path.join('.'));
|
|
75
|
+
const inside = (changed) => asked.some((path) => changed === path || changed.startsWith(`${path}.`) || path.startsWith(`${changed}.`));
|
|
76
|
+
const outside = new Set();
|
|
77
|
+
for (const paths of Object.values(changes))
|
|
78
|
+
for (const changed of [...paths.removed, ...paths.set, ...paths.added])
|
|
79
|
+
if (!inside(changed))
|
|
80
|
+
outside.add(changed);
|
|
81
|
+
return [...outside];
|
|
82
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ConfigObject } from '@dokebi/config';
|
|
2
|
+
export interface SecretEntry {
|
|
3
|
+
/** The option path (`dokebi-db.connections.main.url`). */
|
|
4
|
+
readonly path: string;
|
|
5
|
+
/** Where it is set: dokebi.yaml (or the file DOKEBI_CONFIG names), the secrets file, or nowhere. */
|
|
6
|
+
readonly source: 'config' | 'secrets' | null;
|
|
7
|
+
/** In the document's `secrets` list: the app refuses to start without it. */
|
|
8
|
+
readonly listed: boolean;
|
|
9
|
+
/** A secret a plugin's install plan generates (dokebi-session.secrets): it needs one. */
|
|
10
|
+
readonly required: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ConfigSecretsWarning {
|
|
13
|
+
readonly code: 'SECRET_IN_CONFIG_FILE' | 'SECRETS_PLAN_UNREADABLE';
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly hint: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ConfigSecretsResult {
|
|
18
|
+
/** The settings file read: dokebi.yaml, or the file DOKEBI_CONFIG names. */
|
|
19
|
+
readonly file: string;
|
|
20
|
+
/** The file DOKEBI_SECRETS names, or null. */
|
|
21
|
+
readonly secretsFile: string | null;
|
|
22
|
+
/** The document's `secrets` list. */
|
|
23
|
+
readonly listed: readonly string[];
|
|
24
|
+
readonly secrets: readonly SecretEntry[];
|
|
25
|
+
/** Listed or required paths without a value (a placeholder counts as none). */
|
|
26
|
+
readonly missing: readonly string[];
|
|
27
|
+
readonly warnings?: readonly ConfigSecretsWarning[];
|
|
28
|
+
readonly nextSteps?: ReadonlyArray<{
|
|
29
|
+
command: string;
|
|
30
|
+
why: string;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The secrets the installed plugins generate at install (their plans' `configSecrets`, such as
|
|
35
|
+
* `dokebi-session.secrets`): each plugin needs one to start.
|
|
36
|
+
*/
|
|
37
|
+
export declare function requiredSecretPaths(root: string, options: ConfigObject): Promise<{
|
|
38
|
+
paths: string[];
|
|
39
|
+
unreadable: string[];
|
|
40
|
+
}>;
|
|
41
|
+
/** The warning for plugins whose generated secrets could not be read (their install modules). */
|
|
42
|
+
export declare function unreadableWarning(plugins: readonly string[]): ConfigSecretsWarning[];
|
|
43
|
+
/** `dokebi config secrets`: secret paths by name, where each is set, and the missing ones. */
|
|
44
|
+
export declare function configSecrets(root: string, env?: NodeJS.ProcessEnv): Promise<ConfigSecretsResult>;
|
|
45
|
+
/** The result for people: one line per path. */
|
|
46
|
+
export declare function configSecretsText(result: ConfigSecretsResult): string;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// `dokebi config secrets` (D-0184): the secret option paths the app needs, by name only — where
|
|
2
|
+
// each one comes from (dokebi.yaml or the secrets file DOKEBI_SECRETS names) and which are
|
|
3
|
+
// missing. Values are never read into the result.
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { applySecrets, ConfigError, parseConfig, readConfigFile, SECRETS_VARIABLE, } from '@dokebi/config';
|
|
7
|
+
import { isPlaceholder } from './config-init.js';
|
|
8
|
+
import { configShow, readSettings } from './config-show.js';
|
|
9
|
+
import { debug } from './diagnostics.js';
|
|
10
|
+
import { CliError } from './errors.js';
|
|
11
|
+
import { parseObject } from './filesystem.js';
|
|
12
|
+
import { optionsKey } from './manifest.js';
|
|
13
|
+
import { dependencyMap } from './npm-client.js';
|
|
14
|
+
import { planLifecycle } from './plugin-lifecycle.js';
|
|
15
|
+
import { readState } from './project-state.js';
|
|
16
|
+
function valueAt(options, path) {
|
|
17
|
+
let value = options;
|
|
18
|
+
for (const key of path) {
|
|
19
|
+
if (Array.isArray(value) && /^\d+$/.test(key))
|
|
20
|
+
value = value[Number(key)];
|
|
21
|
+
else if (value && typeof value === 'object' && Object.hasOwn(value, key))
|
|
22
|
+
value = value[key];
|
|
23
|
+
else
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function empty(value) {
|
|
29
|
+
return (value === undefined ||
|
|
30
|
+
value === null ||
|
|
31
|
+
value === '' ||
|
|
32
|
+
isPlaceholder(value) ||
|
|
33
|
+
(Array.isArray(value) && (value.length === 0 || value.every(isPlaceholder))) ||
|
|
34
|
+
(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0));
|
|
35
|
+
}
|
|
36
|
+
/** The same failure as the app's start, as a CLI error with the paths and no values. */
|
|
37
|
+
function configFailure(error) {
|
|
38
|
+
return new CliError(error.code, error.message, {
|
|
39
|
+
phase: 'arguments',
|
|
40
|
+
details: {
|
|
41
|
+
issues: error.issues.map((issue) => ({
|
|
42
|
+
path: issue.instancePath
|
|
43
|
+
.split('/')
|
|
44
|
+
.slice(1)
|
|
45
|
+
.map((key) => key.replaceAll('~1', '/').replaceAll('~0', '~'))
|
|
46
|
+
.join('.'),
|
|
47
|
+
keyword: issue.keyword,
|
|
48
|
+
})),
|
|
49
|
+
fix: error.fix,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The secrets the installed plugins generate at install (their plans' `configSecrets`, such as
|
|
55
|
+
* `dokebi-session.secrets`): each plugin needs one to start.
|
|
56
|
+
*/
|
|
57
|
+
export async function requiredSecretPaths(root, options) {
|
|
58
|
+
const state = readState(parseObject(await readFile(join(root, '.dokebi/state.json'), 'utf8'), 'state'));
|
|
59
|
+
const projectPackages = Object.keys(dependencyMap(parseObject(await readFile(join(root, 'package.json'), 'utf8'), 'package.json')));
|
|
60
|
+
const paths = [];
|
|
61
|
+
// A plugin whose install module cannot run here (not installed: npm install) is named, so the
|
|
62
|
+
// list is never silently short.
|
|
63
|
+
const unreadable = [];
|
|
64
|
+
for (const plugin of state.plugins) {
|
|
65
|
+
let plan;
|
|
66
|
+
try {
|
|
67
|
+
plan = await planLifecycle({ base: root, plugin, options, plugins: state.plugins, projectPackages }, 'planInstall');
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (!(error instanceof CliError) || error.code !== 'PLUGIN_INSTALL_FAILED')
|
|
71
|
+
throw error;
|
|
72
|
+
unreadable.push(plugin.id);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
for (const path of plan.configSecrets)
|
|
76
|
+
paths.push([optionsKey(plugin), ...path].join('.'));
|
|
77
|
+
}
|
|
78
|
+
return { paths, unreadable };
|
|
79
|
+
}
|
|
80
|
+
/** The warning for plugins whose generated secrets could not be read (their install modules). */
|
|
81
|
+
export function unreadableWarning(plugins) {
|
|
82
|
+
return plugins.length
|
|
83
|
+
? [
|
|
84
|
+
{
|
|
85
|
+
code: 'SECRETS_PLAN_UNREADABLE',
|
|
86
|
+
path: plugins.join(', '),
|
|
87
|
+
hint: 'the install modules of these plugins could not run (are their packages installed? npm install), so the secrets they generate are not in this list',
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
: [];
|
|
91
|
+
}
|
|
92
|
+
/** `dokebi config secrets`: secret paths by name, where each is set, and the missing ones. */
|
|
93
|
+
export async function configSecrets(root, env = process.env) {
|
|
94
|
+
const { target, source } = await readSettings(root);
|
|
95
|
+
let document;
|
|
96
|
+
try {
|
|
97
|
+
document = parseConfig(source);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof ConfigError)
|
|
101
|
+
throw configFailure(error);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
const secretsFile = env[SECRETS_VARIABLE] || undefined;
|
|
105
|
+
// The merged options (conflicts and an unsafe file refuse as at start); the list is checked
|
|
106
|
+
// below, so a missing listed secret is reported instead of thrown.
|
|
107
|
+
let merged = document.options;
|
|
108
|
+
let secretsText;
|
|
109
|
+
let secretOptions;
|
|
110
|
+
if (secretsFile !== undefined) {
|
|
111
|
+
try {
|
|
112
|
+
merged = (await applySecrets({ schemaVersion: 1, options: document.options }, secretsFile))
|
|
113
|
+
.options;
|
|
114
|
+
secretsText = await readConfigFile(secretsFile);
|
|
115
|
+
secretOptions = parseConfig(secretsText).options;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error instanceof ConfigError)
|
|
119
|
+
throw configFailure(error);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const fromConfig = new Set((await configShow(root, undefined, { source, file: target.file })).secrets);
|
|
124
|
+
const fromSecrets = new Set(secretsText === undefined
|
|
125
|
+
? []
|
|
126
|
+
: (await configShow(root, undefined, { source: secretsText, file: secretsFile }))
|
|
127
|
+
.secrets);
|
|
128
|
+
const planned = await requiredSecretPaths(root, document.options);
|
|
129
|
+
const required = new Set(planned.paths);
|
|
130
|
+
const listed = document.secrets ?? [];
|
|
131
|
+
const paths = [...new Set([...fromConfig, ...fromSecrets, ...listed, ...required])].sort();
|
|
132
|
+
const secrets = paths.map((path) => {
|
|
133
|
+
const set = !empty(valueAt(merged, path.split('.')));
|
|
134
|
+
return {
|
|
135
|
+
path,
|
|
136
|
+
source: !set ? null : !empty(valueAt(secretOptions, path.split('.'))) ? 'secrets' : 'config',
|
|
137
|
+
listed: listed.includes(path),
|
|
138
|
+
required: required.has(path),
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
const missing = secrets.filter((entry) => entry.source === null).map((entry) => entry.path);
|
|
142
|
+
const warnings = [
|
|
143
|
+
...unreadableWarning(planned.unreadable),
|
|
144
|
+
...(secretsFile === undefined
|
|
145
|
+
? []
|
|
146
|
+
: secrets
|
|
147
|
+
.filter((entry) => entry.source === 'config' && fromConfig.has(entry.path))
|
|
148
|
+
.map((entry) => ({
|
|
149
|
+
code: 'SECRET_IN_CONFIG_FILE',
|
|
150
|
+
path: entry.path,
|
|
151
|
+
hint: `${target.file} holds this secret although ${SECRETS_VARIABLE} names a secrets file: move it there (the configuration file is the one deploy/ keeps without secrets)`,
|
|
152
|
+
}))),
|
|
153
|
+
];
|
|
154
|
+
debug('Configuration secrets listed', {
|
|
155
|
+
secrets: secrets.length,
|
|
156
|
+
missing: missing.length,
|
|
157
|
+
listed: listed.length,
|
|
158
|
+
secretsFile: secretsFile !== undefined,
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
file: target.file,
|
|
162
|
+
secretsFile: secretsFile ?? null,
|
|
163
|
+
listed,
|
|
164
|
+
secrets,
|
|
165
|
+
missing,
|
|
166
|
+
...(warnings.length ? { warnings } : {}),
|
|
167
|
+
...(missing.length
|
|
168
|
+
? {
|
|
169
|
+
nextSteps: [
|
|
170
|
+
{
|
|
171
|
+
command: secretsFile === undefined
|
|
172
|
+
? `${SECRETS_VARIABLE}=<the 0600 secrets file> npx dokebi config secrets --json`
|
|
173
|
+
: `edit ${secretsFile}`,
|
|
174
|
+
why: `set ${missing.join(', ')} (the app refuses to start without the listed ones: CONFIG_SECRETS_MISSING)`,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
}
|
|
178
|
+
: {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** The result for people: one line per path. */
|
|
182
|
+
export function configSecretsText(result) {
|
|
183
|
+
const lines = result.secrets.map((entry) => `${entry.source === null ? 'MISSING' : entry.source.padEnd(7)} ${entry.path}${entry.listed ? ' (listed)' : ''}${entry.required ? ' (required)' : ''}`);
|
|
184
|
+
return `${lines.join('\n') || 'no secret options'}\n${result.missing.length ? `missing: ${result.missing.join(', ')}` : 'no secret missing'}${(result.warnings ?? [])
|
|
185
|
+
.map((warning) => `\nwarning: ${warning.code} (${warning.path}): ${warning.hint}`)
|
|
186
|
+
.join('')}\n`;
|
|
187
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type ConfigTarget } from './configuration.js';
|
|
2
|
+
/** What a hidden value is replaced with. */
|
|
3
|
+
export declare const SECRET_MARKER = "<secret: set>";
|
|
4
|
+
/** What the password of a URL is replaced with: `scheme://user:<secret>@host`. */
|
|
5
|
+
export declare const URL_SECRET_MARKER = "<secret>";
|
|
6
|
+
/**
|
|
7
|
+
* Text with its credentials hidden by core's rule (`redaction.maskCredentials`: the password of
|
|
8
|
+
* a URL, also one with a raw `/` or `@` in it, and `Password=…` of a connection string), or
|
|
9
|
+
* undefined when it holds none. The URL keeps its user: `scheme://user:<secret>@host`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function hideCredentials(text: string): string | undefined;
|
|
12
|
+
export interface ConfigShowWarning {
|
|
13
|
+
readonly code: 'OPTIONS_SCHEMA_UNREADABLE' | 'UNKNOWN_PLUGIN' | 'UNDECLARED_SECTION' | 'ENV_DOKEBI_CONFIG_IGNORED';
|
|
14
|
+
/** The section it is about (absent for ENV_DOKEBI_CONFIG_IGNORED). */
|
|
15
|
+
readonly plugin?: string;
|
|
16
|
+
/** ENV_DOKEBI_CONFIG_IGNORED: `.env:<line>`. */
|
|
17
|
+
readonly path?: string;
|
|
18
|
+
readonly hint: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ConfigShowResult {
|
|
21
|
+
/** The settings file shown: dokebi.yaml, or the file DOKEBI_CONFIG names (M50). */
|
|
22
|
+
readonly file: string;
|
|
23
|
+
/** The asked `<plugin-id>[.<path>]`, or null for every section. */
|
|
24
|
+
readonly path: string | null;
|
|
25
|
+
/** The options (or the asked part), secrets replaced by SECRET_MARKER. */
|
|
26
|
+
readonly value: unknown;
|
|
27
|
+
/** Option paths shown with their schema default because dokebi.yaml does not set them. */
|
|
28
|
+
readonly defaults: string[];
|
|
29
|
+
/** Option paths whose value was hidden (a whole list of secrets is one path). */
|
|
30
|
+
readonly secrets: string[];
|
|
31
|
+
/**
|
|
32
|
+
* D-0154: option paths a running app reads again (the topmost marked key; everything under it
|
|
33
|
+
* is reloadable too). Any other change needs a restart.
|
|
34
|
+
*/
|
|
35
|
+
readonly reloadable: string[];
|
|
36
|
+
readonly warnings?: ConfigShowWarning[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Whether an option named `key` that holds `value` is a secret by its name: core's rule, the one
|
|
40
|
+
* the logs use (`redaction.isSecretField`, D-0047). Text, a list or an object under a secret name
|
|
41
|
+
* is; a number only under a password, PIN or code name; a boolean never. Where the schema marks
|
|
42
|
+
* the option plain (`x-dokebi-secret: false`) the name is not asked.
|
|
43
|
+
*/
|
|
44
|
+
export declare function secretOption(key: string, value: unknown): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Whether a value at an option path is a secret (`config unset` warns `SECRET_REMOVED` for one;
|
|
47
|
+
* D-0165 dropped the `SECRET_ON_COMMAND_LINE` warning it gave `--set` and `config set`). The
|
|
48
|
+
* plugin's options schema decides
|
|
49
|
+
* (AI-M24: `tokens.driver=redis`, `tokens.connection=main` and `maxTokensPerUser=5` of
|
|
50
|
+
* @dokebi/auth warned because a path word matched /token/): a value at a `writeOnly` option
|
|
51
|
+
* (`o.string({ secret: true })`), a list of them, or anything inside an object value that
|
|
52
|
+
* reaches one. Where the schema says nothing (an option it does not declare, a schema that
|
|
53
|
+
* cannot be read: `schema` undefined), the name decides by core's rule (`secretOption`): text,
|
|
54
|
+
* or text inside a list or object, under a secret name; a number only under a password, PIN or
|
|
55
|
+
* code name. Credentials by core's `maskCredentials` (a `scheme://user:password@` URL,
|
|
56
|
+
* `Password=…`) are a secret anywhere. Booleans never are.
|
|
57
|
+
*/
|
|
58
|
+
export declare function commandLineSecret(path: readonly string[], value: unknown, schema: unknown): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* The settings the app reads (M50): the file DOKEBI_CONFIG names, else dokebi.yaml. Reading one
|
|
61
|
+
* outside the project is allowed (as a linked dokebi.yaml is); a named file that is not there is
|
|
62
|
+
* READ_FAILED with the variable in the message.
|
|
63
|
+
*/
|
|
64
|
+
export declare function readSettings(root: string): Promise<{
|
|
65
|
+
target: ConfigTarget;
|
|
66
|
+
source: string;
|
|
67
|
+
}>;
|
|
68
|
+
/** `dokebi config show [<plugin-id>[.<path>]]`: options with secrets hidden. */
|
|
69
|
+
export declare function configShow(root: string, target?: {
|
|
70
|
+
readonly section: string;
|
|
71
|
+
readonly path: readonly string[];
|
|
72
|
+
},
|
|
73
|
+
/**
|
|
74
|
+
* Another document than the settings file (config diff reads the sample's secrets with it, and
|
|
75
|
+
* the settings its transaction read).
|
|
76
|
+
*/
|
|
77
|
+
from?: {
|
|
78
|
+
readonly source: string;
|
|
79
|
+
readonly file?: string;
|
|
80
|
+
}): Promise<ConfigShowResult>;
|
|
81
|
+
/** The result for people: YAML, defaults marked with a comment, secrets already hidden. */
|
|
82
|
+
export declare function configShowText(result: ConfigShowResult): string;
|