@davidvornholt/standards 0.20.0 → 0.22.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/README.md +4 -3
- package/package.json +17 -1
- package/src/cli.ts +1 -1
- package/src/creds-add.ts +19 -1
- package/src/creds-args.ts +9 -0
- package/src/creds-cloudflare-account.ts +2 -2
- package/src/creds-cloudflare-api.ts +6 -10
- package/src/creds-cloudflare-id.ts +10 -0
- package/src/creds-cloudflare-request.ts +31 -0
- package/src/creds-cloudflare-zone.ts +3 -3
- package/src/creds-commands.ts +13 -2
- package/src/creds-dest.ts +40 -25
- package/src/creds-naming.ts +13 -0
- package/src/creds-plan-renew.ts +45 -13
- package/src/creds-plan-report.ts +87 -0
- package/src/creds-plan-run.ts +39 -26
- package/src/creds-plan-types.ts +19 -0
- package/src/creds-plan-unmanaged.ts +102 -0
- package/src/creds-plan.ts +25 -21
- package/src/creds-revoke.ts +162 -0
- package/src/creds-store-lock-holder.ts +8 -1
- package/src/creds-store-lock.ts +2 -1
- package/src/creds-target.ts +57 -0
- package/src/dev-env-brokered-allowlist.ts +44 -0
- package/src/dev-env-brokered-preserve.ts +65 -0
- package/src/dev-env-brokered-refresh.ts +129 -0
- package/src/dev-env-brokered-resolve.ts +158 -0
- package/src/dev-env-brokered.ts +83 -0
- package/src/dev-env-compose.ts +129 -0
- package/src/dev-env-destination-duplicates.ts +10 -10
- package/src/dev-env-destination-gitignore.ts +22 -0
- package/src/dev-env-destination.ts +52 -30
- package/src/dev-env-document.ts +104 -22
- package/src/dev-env-dotenv.ts +50 -5
- package/src/dev-env-plain-layer.ts +53 -0
- package/src/dev-env-reconciliation.ts +170 -0
- package/src/dev-env-secrets.ts +41 -0
- package/src/dev-env-transaction-recovery.ts +74 -11
- package/src/dev-env-transaction.ts +61 -15
- package/src/dev-env.ts +132 -79
package/src/dev-env.ts
CHANGED
|
@@ -1,127 +1,172 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
1
|
import { existsSync } from 'node:fs';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import {
|
|
4
|
+
type DevEnvRunOptions,
|
|
5
|
+
renderDevEnvPreservingUnsafeReferences,
|
|
6
|
+
} from './dev-env-brokered-preserve';
|
|
7
|
+
import { resolveBrokeredReferences } from './dev-env-brokered-resolve';
|
|
8
|
+
import { composeDevEnv } from './dev-env-compose';
|
|
9
|
+
import {
|
|
10
|
+
type DevEnvRemoval,
|
|
5
11
|
type DevEnvWrite,
|
|
6
12
|
devEnvDestinationProblems,
|
|
7
13
|
} from './dev-env-destination';
|
|
14
|
+
import { devEnvGitIgnoreProblem } from './dev-env-destination-gitignore';
|
|
8
15
|
import { parseDevEnvDocument } from './dev-env-document';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const SOPS_ARGS = ['--decrypt', '--output-type', 'json', DEV_SECRETS_FILE];
|
|
16
|
+
import type { DevEnvPlainInput } from './dev-env-plain-layer';
|
|
17
|
+
import { readPlainLayer } from './dev-env-plain-layer';
|
|
18
|
+
import { planDevEnvRemovals } from './dev-env-reconciliation';
|
|
19
|
+
import { DEV_SECRETS_FILE, readDevSecrets } from './dev-env-secrets';
|
|
20
|
+
import { applyDevEnvChanges } from './dev-env-transaction';
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
| { readonly ok: false; readonly problem: string };
|
|
22
|
+
export const DEV_CONFIG_FILE = 'config/dev.yaml';
|
|
23
|
+
export const DEV_LOCAL_FILE = 'config/dev.local.yaml';
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const detail = result.errorMessage ?? result.stderr.trim();
|
|
26
|
-
return {
|
|
27
|
-
ok: false,
|
|
28
|
-
problem: detail
|
|
29
|
-
? `could not decrypt ${DEV_SECRETS_FILE}: ${detail}`
|
|
30
|
-
: `could not decrypt ${DEV_SECRETS_FILE}`,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
try {
|
|
34
|
-
return { ok: true, value: JSON.parse(result.stdout) as unknown };
|
|
35
|
-
} catch (error) {
|
|
36
|
-
return {
|
|
37
|
-
ok: false,
|
|
38
|
-
problem: `could not parse decrypted ${DEV_SECRETS_FILE} as JSON: ${
|
|
39
|
-
error instanceof Error ? error.message : String(error)
|
|
40
|
-
}`,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
25
|
+
export type DevEnvInputs = {
|
|
26
|
+
readonly config: DevEnvPlainInput;
|
|
27
|
+
readonly secrets: unknown;
|
|
28
|
+
readonly local: DevEnvPlainInput;
|
|
43
29
|
};
|
|
44
30
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (result.error !== undefined || result.status === null) {
|
|
53
|
-
return `cannot run git to verify ${rel} is gitignored`;
|
|
54
|
-
}
|
|
55
|
-
if (result.status === 0) {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
if (result.status === 1) {
|
|
59
|
-
return `${rel} is not gitignored; ignore it before generating dev env files`;
|
|
60
|
-
}
|
|
61
|
-
return `cannot verify ${rel} is gitignored (git check-ignore exited ${result.status})`;
|
|
62
|
-
};
|
|
31
|
+
const documentProblems = (
|
|
32
|
+
source: string,
|
|
33
|
+
input: DevEnvPlainInput,
|
|
34
|
+
): ReadonlyArray<string> =>
|
|
35
|
+
input === null
|
|
36
|
+
? []
|
|
37
|
+
: parseDevEnvDocument(input.raw, source, 'configuration').problems;
|
|
63
38
|
|
|
64
39
|
export type DevEnvPlan = {
|
|
65
40
|
readonly writes: ReadonlyArray<DevEnvWrite>;
|
|
41
|
+
readonly removals: ReadonlyArray<DevEnvRemoval>;
|
|
66
42
|
readonly problems: ReadonlyArray<string>;
|
|
67
43
|
};
|
|
68
44
|
|
|
69
|
-
export const
|
|
45
|
+
export const planDevEnvChanges = (
|
|
70
46
|
consumer: string,
|
|
71
|
-
|
|
47
|
+
inputs: DevEnvInputs,
|
|
48
|
+
options: DevEnvRunOptions = { preservedBrokeredReferences: new Set() },
|
|
72
49
|
): DevEnvPlan => {
|
|
73
|
-
const
|
|
74
|
-
|
|
50
|
+
const layer = (source: string, input: DevEnvPlainInput) =>
|
|
51
|
+
input === null
|
|
52
|
+
? null
|
|
53
|
+
: {
|
|
54
|
+
source,
|
|
55
|
+
document: parseDevEnvDocument(input.raw, source, 'configuration'),
|
|
56
|
+
};
|
|
57
|
+
const composed = composeDevEnv(
|
|
58
|
+
layer(DEV_CONFIG_FILE, inputs.config),
|
|
59
|
+
{
|
|
60
|
+
source: DEV_SECRETS_FILE,
|
|
61
|
+
document: parseDevEnvDocument(
|
|
62
|
+
inputs.secrets,
|
|
63
|
+
DEV_SECRETS_FILE,
|
|
64
|
+
'secrets',
|
|
65
|
+
),
|
|
66
|
+
},
|
|
67
|
+
layer(DEV_LOCAL_FILE, inputs.local),
|
|
68
|
+
);
|
|
69
|
+
const resolved = resolveBrokeredReferences(
|
|
70
|
+
consumer,
|
|
71
|
+
composed.targets,
|
|
72
|
+
composed.brokeredReferences,
|
|
73
|
+
options.preservedBrokeredReferences,
|
|
74
|
+
);
|
|
75
|
+
const problems: Array<string> = [...composed.problems, ...resolved.problems];
|
|
75
76
|
const writes: Array<DevEnvWrite> = [];
|
|
76
|
-
for (const target of
|
|
77
|
+
for (const target of resolved.targets) {
|
|
77
78
|
const workspaceDir = `${target.group}/${target.workspace}`;
|
|
78
79
|
const rel = `${workspaceDir}/.env.local`;
|
|
79
80
|
if (existsSync(join(consumer, workspaceDir, 'package.json'))) {
|
|
80
|
-
const ignoreProblem =
|
|
81
|
+
const ignoreProblem = devEnvGitIgnoreProblem(consumer, rel);
|
|
81
82
|
if (ignoreProblem === null) {
|
|
82
|
-
|
|
83
|
+
const composedTarget = composed.targets.find(
|
|
84
|
+
(candidate) =>
|
|
85
|
+
candidate.group === target.group &&
|
|
86
|
+
candidate.workspace === target.workspace,
|
|
87
|
+
);
|
|
88
|
+
const rendered = renderDevEnvPreservingUnsafeReferences({
|
|
89
|
+
consumer,
|
|
83
90
|
rel,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
sourcePath: `${target.group}.${target.workspace}`,
|
|
92
|
+
sources: target.sources,
|
|
93
|
+
resolvedEnv: target.env,
|
|
94
|
+
composedEnv: composedTarget?.env ?? {},
|
|
95
|
+
preservedReferences: options.preservedBrokeredReferences,
|
|
89
96
|
});
|
|
97
|
+
if (rendered.ok) {
|
|
98
|
+
writes.push({ rel, content: rendered.content });
|
|
99
|
+
} else {
|
|
100
|
+
problems.push(rendered.problem);
|
|
101
|
+
}
|
|
90
102
|
} else {
|
|
91
103
|
problems.push(ignoreProblem);
|
|
92
104
|
}
|
|
93
105
|
} else {
|
|
94
106
|
problems.push(
|
|
95
|
-
`${
|
|
107
|
+
`${target.sources.join(' + ')} defines ${target.group}.${target.workspace}, but ${workspaceDir}/package.json does not exist`,
|
|
96
108
|
);
|
|
97
109
|
}
|
|
98
110
|
}
|
|
99
|
-
|
|
111
|
+
const removalPlan = planDevEnvRemovals(consumer, writes);
|
|
112
|
+
return {
|
|
113
|
+
writes,
|
|
114
|
+
removals: removalPlan.removals,
|
|
115
|
+
problems: [...problems, ...removalPlan.problems],
|
|
116
|
+
};
|
|
100
117
|
};
|
|
101
118
|
|
|
102
|
-
export const runDevEnv = async (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
export const runDevEnv = async (
|
|
120
|
+
consumer: string,
|
|
121
|
+
options: DevEnvRunOptions = { preservedBrokeredReferences: new Set() },
|
|
122
|
+
): Promise<boolean> => {
|
|
123
|
+
const config = readPlainLayer(consumer, DEV_CONFIG_FILE);
|
|
124
|
+
const secrets = readDevSecrets(consumer);
|
|
125
|
+
const local = readPlainLayer(consumer, DEV_LOCAL_FILE);
|
|
126
|
+
const localIgnoreProblem = local.present
|
|
127
|
+
? devEnvGitIgnoreProblem(consumer, DEV_LOCAL_FILE)
|
|
128
|
+
: null;
|
|
129
|
+
const inputProblems = [
|
|
130
|
+
...config.problems,
|
|
131
|
+
...(secrets.ok ? [] : [secrets.problem]),
|
|
132
|
+
...local.problems,
|
|
133
|
+
...(localIgnoreProblem === null ? [] : [localIgnoreProblem]),
|
|
134
|
+
...documentProblems(DEV_CONFIG_FILE, config.input),
|
|
135
|
+
...(secrets.ok
|
|
136
|
+
? parseDevEnvDocument(secrets.value, DEV_SECRETS_FILE, 'secrets').problems
|
|
137
|
+
: []),
|
|
138
|
+
...documentProblems(DEV_LOCAL_FILE, local.input),
|
|
139
|
+
];
|
|
140
|
+
if (inputProblems.length > 0 || !secrets.ok) {
|
|
141
|
+
console.error(`standards dev-env: ${inputProblems.length} problem(s):`);
|
|
142
|
+
console.error(inputProblems.map((problem) => ` - ${problem}`).join('\n'));
|
|
112
143
|
return false;
|
|
113
144
|
}
|
|
114
|
-
const plan =
|
|
145
|
+
const plan = planDevEnvChanges(
|
|
146
|
+
consumer,
|
|
147
|
+
{
|
|
148
|
+
config: config.input,
|
|
149
|
+
secrets: secrets.value,
|
|
150
|
+
local: local.input,
|
|
151
|
+
},
|
|
152
|
+
options,
|
|
153
|
+
);
|
|
115
154
|
const problems = [
|
|
116
155
|
...plan.problems,
|
|
117
|
-
...(await devEnvDestinationProblems(consumer,
|
|
156
|
+
...(await devEnvDestinationProblems(consumer, [
|
|
157
|
+
...plan.writes,
|
|
158
|
+
...plan.removals,
|
|
159
|
+
])),
|
|
118
160
|
];
|
|
119
161
|
if (problems.length > 0) {
|
|
120
162
|
console.error(`standards dev-env: ${problems.length} problem(s):`);
|
|
121
163
|
console.error(problems.map((problem) => ` - ${problem}`).join('\n'));
|
|
122
164
|
return false;
|
|
123
165
|
}
|
|
124
|
-
const generated = await
|
|
166
|
+
const generated = await applyDevEnvChanges(consumer, [
|
|
167
|
+
...plan.writes,
|
|
168
|
+
...plan.removals,
|
|
169
|
+
]);
|
|
125
170
|
if (!generated.ok) {
|
|
126
171
|
console.error('standards dev-env: generation failed:');
|
|
127
172
|
console.error(
|
|
@@ -140,8 +185,16 @@ export const runDevEnv = async (consumer: string): Promise<boolean> => {
|
|
|
140
185
|
for (const write of plan.writes) {
|
|
141
186
|
console.log(` wrote ${write.rel}`);
|
|
142
187
|
}
|
|
188
|
+
for (const removal of plan.removals) {
|
|
189
|
+
console.log(` removed ${removal.rel}`);
|
|
190
|
+
}
|
|
191
|
+
const inputFiles = [
|
|
192
|
+
...(config.input === null ? [] : [DEV_CONFIG_FILE]),
|
|
193
|
+
DEV_SECRETS_FILE,
|
|
194
|
+
...(local.input === null ? [] : [DEV_LOCAL_FILE]),
|
|
195
|
+
];
|
|
143
196
|
console.log(
|
|
144
|
-
`standards dev-env: generated ${plan.writes.length} env file(s) from ${
|
|
197
|
+
`standards dev-env: generated ${plan.writes.length} and removed ${plan.removals.length} env file(s) from ${inputFiles.join(' + ')}`,
|
|
145
198
|
);
|
|
146
199
|
return true;
|
|
147
200
|
};
|