@flowscripter/dynamic-cli-framework 12.0.1 → 12.2.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.
Files changed (80) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -0
  4. package/dist/src/cli/BaseCLI.d.ts.map +1 -1
  5. package/dist/src/cli/BaseCLI.js +3 -1
  6. package/dist/src/cli/BaseCLIFeatureOptions.d.ts +6 -0
  7. package/dist/src/cli/BaseCLIFeatureOptions.d.ts.map +1 -1
  8. package/dist/src/runtime/runner.d.ts.map +1 -1
  9. package/dist/src/runtime/runner.js +4 -1
  10. package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts +33 -15
  11. package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts.map +1 -1
  12. package/dist/src/service/configuration/ConfigurationServiceProvider.js +80 -173
  13. package/dist/src/service/configuration/DefaultConfigurationService.d.ts +78 -0
  14. package/dist/src/service/configuration/DefaultConfigurationService.d.ts.map +1 -0
  15. package/dist/src/service/configuration/DefaultConfigurationService.js +296 -0
  16. package/dist/src/service/configuration/DefaultKeyValueService.d.ts.map +1 -1
  17. package/dist/src/service/configuration/DefaultKeyValueService.js +7 -3
  18. package/dist/src/service/configuration/DefaultSecretService.d.ts +17 -2
  19. package/dist/src/service/configuration/DefaultSecretService.d.ts.map +1 -1
  20. package/dist/src/service/configuration/DefaultSecretService.js +44 -5
  21. package/dist/src/service/configuration/KeyValueServiceProvider.d.ts +3 -1
  22. package/dist/src/service/configuration/KeyValueServiceProvider.d.ts.map +1 -1
  23. package/dist/src/service/configuration/KeyValueServiceProvider.js +6 -3
  24. package/dist/src/service/configuration/command/ConfigDumpSubCommand.d.ts +3 -7
  25. package/dist/src/service/configuration/command/ConfigDumpSubCommand.d.ts.map +1 -1
  26. package/dist/src/service/configuration/command/ConfigDumpSubCommand.js +5 -10
  27. package/dist/src/service/configuration/command/ConfigGetSubCommand.d.ts +16 -0
  28. package/dist/src/service/configuration/command/ConfigGetSubCommand.d.ts.map +1 -0
  29. package/dist/src/service/configuration/command/ConfigGetSubCommand.js +137 -0
  30. package/dist/src/service/configuration/command/ConfigSetSubCommand.d.ts +18 -0
  31. package/dist/src/service/configuration/command/ConfigSetSubCommand.d.ts.map +1 -0
  32. package/dist/src/service/configuration/command/ConfigSetSubCommand.js +289 -0
  33. package/dist/src/service/configuration/command/ConfigUnsetSubCommand.d.ts +15 -0
  34. package/dist/src/service/configuration/command/ConfigUnsetSubCommand.d.ts.map +1 -0
  35. package/dist/src/service/configuration/command/ConfigUnsetSubCommand.js +101 -0
  36. package/dist/src/service/configuration/configTargets.d.ts +91 -0
  37. package/dist/src/service/configuration/configTargets.d.ts.map +1 -0
  38. package/dist/src/service/configuration/configTargets.js +348 -0
  39. package/dist/src/service/configuration/format/ConfigFormat.d.ts +42 -0
  40. package/dist/src/service/configuration/format/ConfigFormat.d.ts.map +1 -0
  41. package/dist/src/service/configuration/format/ConfigFormat.js +0 -0
  42. package/dist/src/service/configuration/format/JsonConfigFormat.d.ts +7 -0
  43. package/dist/src/service/configuration/format/JsonConfigFormat.d.ts.map +1 -0
  44. package/dist/src/service/configuration/format/JsonConfigFormat.js +17 -0
  45. package/dist/src/service/configuration/format/TomlConfigFormat.d.ts +10 -0
  46. package/dist/src/service/configuration/format/TomlConfigFormat.d.ts.map +1 -0
  47. package/dist/src/service/configuration/format/TomlConfigFormat.js +78 -0
  48. package/dist/src/service/configuration/format/YamlConfigFormat.d.ts +9 -0
  49. package/dist/src/service/configuration/format/YamlConfigFormat.d.ts.map +1 -0
  50. package/dist/src/service/configuration/format/YamlConfigFormat.js +63 -0
  51. package/dist/src/service/configuration/resolveSecrets.d.ts.map +1 -1
  52. package/dist/src/service/configuration/resolveSecrets.js +7 -1
  53. package/dist/toml.d.ts +2 -0
  54. package/dist/toml.d.ts.map +1 -0
  55. package/dist/toml.js +1 -0
  56. package/dist/yaml.d.ts +2 -0
  57. package/dist/yaml.d.ts.map +1 -0
  58. package/dist/yaml.js +1 -0
  59. package/index.ts +3 -0
  60. package/package.json +27 -3
  61. package/src/cli/BaseCLI.ts +4 -0
  62. package/src/cli/BaseCLIFeatureOptions.ts +6 -0
  63. package/src/runtime/runner.ts +4 -1
  64. package/src/service/configuration/ConfigurationServiceProvider.ts +108 -214
  65. package/src/service/configuration/DefaultConfigurationService.ts +378 -0
  66. package/src/service/configuration/DefaultKeyValueService.ts +9 -3
  67. package/src/service/configuration/DefaultSecretService.ts +51 -8
  68. package/src/service/configuration/KeyValueServiceProvider.ts +7 -2
  69. package/src/service/configuration/command/ConfigDumpSubCommand.ts +6 -10
  70. package/src/service/configuration/command/ConfigGetSubCommand.ts +198 -0
  71. package/src/service/configuration/command/ConfigSetSubCommand.ts +401 -0
  72. package/src/service/configuration/command/ConfigUnsetSubCommand.ts +126 -0
  73. package/src/service/configuration/configTargets.ts +481 -0
  74. package/src/service/configuration/format/ConfigFormat.ts +42 -0
  75. package/src/service/configuration/format/JsonConfigFormat.ts +22 -0
  76. package/src/service/configuration/format/TomlConfigFormat.ts +92 -0
  77. package/src/service/configuration/format/YamlConfigFormat.ts +72 -0
  78. package/src/service/configuration/resolveSecrets.ts +7 -1
  79. package/toml.ts +1 -0
  80. package/yaml.ts +1 -0
@@ -0,0 +1,78 @@
1
+ import { type CommandRegistry, type ConfigurationService, type SingleValueType, type ValueNode, type Values } from "@flowscripter/dynamic-cli-framework-api";
2
+ import { type ConfigTarget } from "./configTargets.ts";
3
+ import type DefaultSecretService from "./DefaultSecretService.ts";
4
+ import type ConfigFormat from "./format/ConfigFormat.ts";
5
+ export type KeyValueServiceScopeType = "command" | "service";
6
+ /**
7
+ * Owns the configuration data read from (and written back to) the configuration file: the default
8
+ * command argument values and the raw per-scope key-value data.
9
+ *
10
+ * The `config` group commands are constructed with this instance. It must never be registered in the
11
+ * {@link Context}: it exposes every command's defaults, the raw key-value data of every scope and secret
12
+ * resolution. Only a location-only view is registered under {@link CONFIGURATION_SERVICE_ID}.
13
+ */
14
+ export default class DefaultConfigurationService implements ConfigurationService {
15
+ #private;
16
+ readonly envVarsEnabled: boolean;
17
+ readonly configFormat: ConfigFormat;
18
+ configLocation: string | undefined;
19
+ defaultsData: Map<string, Values | SingleValueType>;
20
+ /**
21
+ * @param envVarsEnabled whether environment variables may override stored default values.
22
+ * @param configFormat the syntax of the configuration file.
23
+ * @param commandRegistry optional registry of all commands, required for {@link getConfigTargets}.
24
+ */
25
+ constructor(envVarsEnabled: boolean, configFormat: ConfigFormat, commandRegistry?: CommandRegistry);
26
+ setConfigLocation(location: string): void;
27
+ setDefaultsSecretService(secretService: DefaultSecretService): void;
28
+ /**
29
+ * Resolve any secret sentinels within the value using the OS secret store (if enabled).
30
+ *
31
+ * @param commandKey the key of the command under `defaults` the value belongs to: a sentinel
32
+ * may only refer to a secret stored for this command.
33
+ */
34
+ resolveDefaultSecrets<T>(value: T, commandKey: string): Promise<T>;
35
+ /**
36
+ * Get-or-create the underlying, raw {@link ValueNode} `Map` for the given scope.
37
+ *
38
+ * @param scopeType whether `scopeKey` is a command name or a service/task ID.
39
+ * @param scopeKey the command name or service/task ID to scope the key-value data to.
40
+ */
41
+ getKeyValueData(scopeType: KeyValueServiceScopeType, scopeKey: string): Map<string, ValueNode>;
42
+ /**
43
+ * Write the configuration file, if `isDirty` is true.
44
+ *
45
+ * @param isDirty whether any scoped KeyValueService has unwritten changes.
46
+ */
47
+ flushIfDirty(isDirty: boolean): Promise<void>;
48
+ /**
49
+ * Determine the configuration location (defaulting to `$HOME/.<application_name>.<format extension>`)
50
+ * and read the configuration from it if it exists.
51
+ *
52
+ * @param applicationName the name of the CLI.
53
+ */
54
+ init(applicationName: string): Promise<void>;
55
+ getConfigString(): string;
56
+ /**
57
+ * The configuration text with secret sentinels and the values of SECRET typed arguments of
58
+ * known commands masked.
59
+ */
60
+ getMaskedConfigString(): string;
61
+ /**
62
+ * The commands whose defaults can be managed via the `config` group commands.
63
+ */
64
+ getConfigTargets(): Map<string, ConfigTarget>;
65
+ /**
66
+ * Replace (or remove, if `build` returns `undefined`) the stored defaults of a command and write the
67
+ * configuration file immediately. Memory, file and secret store are left unchanged if anything fails.
68
+ *
69
+ * @param commandKey the key of the command under `defaults`.
70
+ * @param secrets plain text secrets to store in the OS secret store, keyed to be used for naming.
71
+ * @param build creates the new value given the sentinels (one per secret) to reference the stored secrets.
72
+ */
73
+ commitDefault(commandKey: string, secrets: ReadonlyArray<{
74
+ key: ReadonlyArray<string>;
75
+ value: string;
76
+ }>, build: (sentinels: ReadonlyArray<string>) => Values | SingleValueType | undefined): Promise<void>;
77
+ }
78
+ //# sourceMappingURL=DefaultConfigurationService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DefaultConfigurationService.d.ts","sourceRoot":"","sources":["../../../../src/service/configuration/DefaultConfigurationService.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,MAAM,EACZ,MAAM,yCAAyC,CAAC;AAEjD,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,oBAAoB,MAAM,2BAA2B,CAAC;AAClE,OAAO,KAAK,YAAY,MAAM,0BAA0B,CAAC;AAMzD,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG,SAAS,CAAC;AAE7D;;;;;;;GAOG;AACH,MAAM,CAAC,OAAO,OAAO,2BAA4B,YAAW,oBAAoB;;IAC9E,SAAgB,cAAc,EAAE,OAAO,CAAC;IACxC,SAAgB,YAAY,EAAE,YAAY,CAAC;IAGpC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IAInC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAAC,CAAa;IAcvE;;;;OAIG;IACH,YACE,cAAc,EAAE,OAAO,EACvB,YAAY,EAAE,YAAY,EAC1B,eAAe,CAAC,EAAE,eAAe,EAKlC;IAEM,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE/C;IAEM,wBAAwB,CAAC,aAAa,EAAE,oBAAoB,GAAG,IAAI,CAEzE;IAED;;;;;OAKG;IACU,qBAAqB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAmB9E;IAED;;;;;OAKG;IACI,eAAe,CACpB,SAAS,EAAE,wBAAwB,EACnC,QAAQ,EAAE,MAAM,GACf,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAOxB;IAED;;;;OAIG;IACU,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAQzD;IAED;;;;;OAKG;IACU,IAAI,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCxD;IAiEM,eAAe,IAAI,MAAM,CAE/B;IAED;;;OAGG;IACI,qBAAqB,IAAI,MAAM,CAarC;IAED;;OAEG;IACI,gBAAgB,IAAI,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAInD;IAED;;;;;;;OAOG;IACU,aAAa,CACxB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,aAAa,CAAC;QAAE,GAAG,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,EACrE,KAAK,EAAE,CAAC,SAAS,EAAE,aAAa,CAAC,MAAM,CAAC,KAAK,MAAM,GAAG,eAAe,GAAG,SAAS,GAChF,OAAO,CAAC,IAAI,CAAC,CA6Cf;CAmCF"}
@@ -0,0 +1,296 @@
1
+ import process from "node:process";
2
+ import path from "node:path";
3
+ import fs from "node:fs/promises";
4
+ import { SECRET_SENTINEL_PREFIX, } from "@flowscripter/dynamic-cli-framework-api";
5
+ import getLogger from "../../util/logger.js";
6
+ import { collectSentinels, getConfigTargets, maskDefault, } from "./configTargets.js";
7
+ import resolveSecrets from "./resolveSecrets.js";
8
+ const logger = getLogger("DefaultConfigurationService");
9
+ /**
10
+ * Owns the configuration data read from (and written back to) the configuration file: the default
11
+ * command argument values and the raw per-scope key-value data.
12
+ *
13
+ * The `config` group commands are constructed with this instance. It must never be registered in the
14
+ * {@link Context}: it exposes every command's defaults, the raw key-value data of every scope and secret
15
+ * resolution. Only a location-only view is registered under {@link CONFIGURATION_SERVICE_ID}.
16
+ */
17
+ export default class DefaultConfigurationService {
18
+ envVarsEnabled;
19
+ configFormat;
20
+ // the location of the currently managed configuration data
21
+ configLocation;
22
+ // the configuration data used when setting command defaults. SingleValueType is for
23
+ // GlobalCommandArgument values, Values is for SubCommandArgument values.
24
+ defaultsData = new Map();
25
+ // the format specific handle for the text last read or written, used to preserve comments and layout on write
26
+ #configHandle;
27
+ #commandKeyValueData = new Map();
28
+ #serviceKeyValueData = new Map();
29
+ // used only to resolve secret sentinels embedded in default command argument values and to store
30
+ // SECRET values set via the `config` group commands
31
+ #defaultsSecretService;
32
+ #commandRegistry;
33
+ /**
34
+ * @param envVarsEnabled whether environment variables may override stored default values.
35
+ * @param configFormat the syntax of the configuration file.
36
+ * @param commandRegistry optional registry of all commands, required for {@link getConfigTargets}.
37
+ */
38
+ constructor(envVarsEnabled, configFormat, commandRegistry) {
39
+ this.envVarsEnabled = envVarsEnabled;
40
+ this.configFormat = configFormat;
41
+ this.#commandRegistry = commandRegistry;
42
+ }
43
+ setConfigLocation(location) {
44
+ this.configLocation = location;
45
+ }
46
+ setDefaultsSecretService(secretService) {
47
+ this.#defaultsSecretService = secretService;
48
+ }
49
+ /**
50
+ * Resolve any secret sentinels within the value using the OS secret store (if enabled).
51
+ *
52
+ * @param commandKey the key of the command under `defaults` the value belongs to: a sentinel
53
+ * may only refer to a secret stored for this command.
54
+ */
55
+ async resolveDefaultSecrets(value, commandKey) {
56
+ if (this.#defaultsSecretService === undefined) {
57
+ return value;
58
+ }
59
+ const secretService = this.#defaultsSecretService;
60
+ return await resolveSecrets(value, async (bunSecretName) => {
61
+ if (!secretService.ownsSecretName(bunSecretName, commandKey)) {
62
+ throw new Error(`Secret sentinel '${SECRET_SENTINEL_PREFIX}${bunSecretName}' does not refer to a secret stored for command: '${commandKey}'`);
63
+ }
64
+ const secretValue = await secretService.getSecret(bunSecretName);
65
+ if (secretValue === null) {
66
+ throw new Error(`Secret not found in OS secret store for sentinel: '${SECRET_SENTINEL_PREFIX}${bunSecretName}'`);
67
+ }
68
+ return secretValue;
69
+ });
70
+ }
71
+ /**
72
+ * Get-or-create the underlying, raw {@link ValueNode} `Map` for the given scope.
73
+ *
74
+ * @param scopeType whether `scopeKey` is a command name or a service/task ID.
75
+ * @param scopeKey the command name or service/task ID to scope the key-value data to.
76
+ */
77
+ getKeyValueData(scopeType, scopeKey) {
78
+ const keyValueData = scopeType === "command" ? this.#commandKeyValueData : this.#serviceKeyValueData;
79
+ if (!keyValueData.has(scopeKey)) {
80
+ keyValueData.set(scopeKey, new Map());
81
+ }
82
+ return keyValueData.get(scopeKey);
83
+ }
84
+ /**
85
+ * Write the configuration file, if `isDirty` is true.
86
+ *
87
+ * @param isDirty whether any scoped KeyValueService has unwritten changes.
88
+ */
89
+ async flushIfDirty(isDirty) {
90
+ if (!isDirty) {
91
+ return;
92
+ }
93
+ if (this.configLocation === undefined) {
94
+ throw new Error("Attempt to write updated config with no configLocation set");
95
+ }
96
+ await this.#writeConfig(this.configLocation);
97
+ }
98
+ /**
99
+ * Determine the configuration location (defaulting to `$HOME/.<application_name>.<format extension>`)
100
+ * and read the configuration from it if it exists.
101
+ *
102
+ * @param applicationName the name of the CLI.
103
+ */
104
+ async init(applicationName) {
105
+ let isDefault = false;
106
+ if (this.configLocation === undefined) {
107
+ isDefault = true;
108
+ const home = process.env["HOME"];
109
+ if (home) {
110
+ this.configLocation = path.join(home, `.${applicationName.replace(/\W/g, "")}.${this.configFormat.fileExtension}`);
111
+ }
112
+ }
113
+ if (this.configLocation === undefined) {
114
+ return;
115
+ }
116
+ let fileInfo;
117
+ try {
118
+ fileInfo = await fs.lstat(this.configLocation);
119
+ }
120
+ catch (err) {
121
+ const error = err;
122
+ if (isDefault) {
123
+ logger.debug("Default config file location: %s does not exist or is not visible - ignoring: %s", this.configLocation, error.message);
124
+ return;
125
+ }
126
+ else {
127
+ throw new Error(`Config file location: '${this.configLocation}' doesn't exist or not visible: ${error.message}`);
128
+ }
129
+ }
130
+ if (fileInfo.isDirectory()) {
131
+ throw new Error(`Config file location: '${this.configLocation}' is a directory and not a file!`);
132
+ }
133
+ await this.#readConfig(this.configLocation);
134
+ }
135
+ async #readConfig(location) {
136
+ try {
137
+ logger.debug("Reading config from: %s", location);
138
+ const data = await fs.readFile(location);
139
+ const { data: config, handle } = this.configFormat.parse(data.toString());
140
+ this.#configHandle = handle;
141
+ this.defaultsData.clear();
142
+ this.#commandKeyValueData.clear();
143
+ this.#serviceKeyValueData.clear();
144
+ if (config.defaults !== undefined) {
145
+ const defaults = config.defaults;
146
+ Object.keys(defaults).forEach((commandName) => {
147
+ this.defaultsData.set(commandName, defaults[commandName]);
148
+ logger.debug("Set default argument values for command name: %s", commandName);
149
+ });
150
+ }
151
+ if (config["key-values"] !== undefined) {
152
+ const keyValues = config["key-values"];
153
+ if (keyValues.commands !== undefined) {
154
+ const commandKeyValues = keyValues.commands;
155
+ Object.keys(commandKeyValues).forEach((commandName) => {
156
+ this.#commandKeyValueData.set(commandName, new Map(Object.entries(commandKeyValues[commandName])));
157
+ logger.debug("Set default argument values for command name: %s", commandName);
158
+ });
159
+ }
160
+ if (keyValues.services !== undefined) {
161
+ const serviceKeyValues = keyValues.services;
162
+ Object.keys(serviceKeyValues).forEach((serviceId) => {
163
+ this.#serviceKeyValueData.set(serviceId, new Map(Object.entries(serviceKeyValues[serviceId])));
164
+ logger.debug("Set default argument values for service ID: %s", serviceId);
165
+ });
166
+ }
167
+ }
168
+ }
169
+ catch (err) {
170
+ throw new Error(`Failed to read config at location: '${this.configLocation}': ${err}`);
171
+ }
172
+ }
173
+ async #writeConfig(location) {
174
+ logger.debug("Writing config to: %s", location);
175
+ try {
176
+ const config = this.#getConfigData();
177
+ const text = this.#configHandle === undefined
178
+ ? this.configFormat.serialize(config)
179
+ : this.configFormat.apply(this.#configHandle, config);
180
+ // owner-only when the file is created; an existing file keeps its mode
181
+ await fs.writeFile(location, text, { mode: 0o600 });
182
+ this.#configHandle = this.configFormat.parse(text).handle;
183
+ }
184
+ catch (err) {
185
+ throw new Error(`Failed to write config at location: '${this.configLocation}': ${err}`);
186
+ }
187
+ }
188
+ getConfigString() {
189
+ return this.configFormat.serialize(this.#getConfigData());
190
+ }
191
+ /**
192
+ * The configuration text with secret sentinels and the values of SECRET typed arguments of
193
+ * known commands masked.
194
+ */
195
+ getMaskedConfigString() {
196
+ const data = this.#getConfigData();
197
+ const targets = this.getConfigTargets();
198
+ const masked = maskDefault(undefined, data);
199
+ if (data.defaults !== undefined) {
200
+ masked.defaults = Object.fromEntries(Object.entries(data.defaults).map(([key, value]) => [
201
+ key,
202
+ maskDefault(targets.get(key), value),
203
+ ]));
204
+ }
205
+ return this.configFormat.serialize(masked);
206
+ }
207
+ /**
208
+ * The commands whose defaults can be managed via the `config` group commands.
209
+ */
210
+ getConfigTargets() {
211
+ return this.#commandRegistry === undefined
212
+ ? new Map()
213
+ : getConfigTargets(this.#commandRegistry);
214
+ }
215
+ /**
216
+ * Replace (or remove, if `build` returns `undefined`) the stored defaults of a command and write the
217
+ * configuration file immediately. Memory, file and secret store are left unchanged if anything fails.
218
+ *
219
+ * @param commandKey the key of the command under `defaults`.
220
+ * @param secrets plain text secrets to store in the OS secret store, keyed to be used for naming.
221
+ * @param build creates the new value given the sentinels (one per secret) to reference the stored secrets.
222
+ */
223
+ async commitDefault(commandKey, secrets, build) {
224
+ if (this.configLocation === undefined) {
225
+ throw new Error("Unable to write config: no config location is set and $HOME is not defined");
226
+ }
227
+ if (secrets.length > 0 && this.#defaultsSecretService === undefined) {
228
+ throw new Error("Unable to store a SECRET value: the secret service is not enabled");
229
+ }
230
+ const secretService = this.#defaultsSecretService;
231
+ const previous = this.defaultsData.get(commandKey);
232
+ const restores = [];
233
+ const sentinels = [];
234
+ try {
235
+ for (const { key, value } of secrets) {
236
+ const name = secretService.secretName(key);
237
+ const old = await secretService.getSecret(name);
238
+ await secretService.setSecret(key, value);
239
+ restores.push(() => old === null ? secretService.deleteSecret(name) : secretService.setSecret(key, old));
240
+ sentinels.push(`${SECRET_SENTINEL_PREFIX}${name}`);
241
+ }
242
+ const next = build(sentinels);
243
+ if (next === undefined) {
244
+ this.defaultsData.delete(commandKey);
245
+ }
246
+ else {
247
+ this.defaultsData.set(commandKey, next);
248
+ }
249
+ await this.#writeConfig(this.configLocation);
250
+ const kept = new Set(collectSentinels(next));
251
+ for (const sentinel of collectSentinels(previous)) {
252
+ if (!kept.has(sentinel) && secretService !== undefined) {
253
+ await secretService.deleteSecret(sentinel.slice(SECRET_SENTINEL_PREFIX.length));
254
+ }
255
+ }
256
+ }
257
+ catch (err) {
258
+ if (previous === undefined) {
259
+ this.defaultsData.delete(commandKey);
260
+ }
261
+ else {
262
+ this.defaultsData.set(commandKey, previous);
263
+ }
264
+ for (const restore of restores.reverse()) {
265
+ await restore().catch(() => undefined);
266
+ }
267
+ throw err;
268
+ }
269
+ }
270
+ #getConfigData() {
271
+ const config = {};
272
+ if (this.defaultsData.size > 0) {
273
+ config.defaults = Object.fromEntries(this.defaultsData);
274
+ }
275
+ if (this.#commandKeyValueData.size > 0 || this.#serviceKeyValueData.size > 0) {
276
+ config["key-values"] = {};
277
+ if (this.#commandKeyValueData.size > 0) {
278
+ config["key-values"].commands = {};
279
+ for (const [commandName, keyValueData] of this.#commandKeyValueData.entries()) {
280
+ if (keyValueData.size > 0) {
281
+ config["key-values"].commands[commandName] = Object.fromEntries(keyValueData);
282
+ }
283
+ }
284
+ }
285
+ if (this.#serviceKeyValueData.size > 0) {
286
+ config["key-values"].services = {};
287
+ for (const [serviceId, keyValueData] of this.#serviceKeyValueData.entries()) {
288
+ if (keyValueData.size > 0) {
289
+ config["key-values"].services[serviceId] = Object.fromEntries(keyValueData);
290
+ }
291
+ }
292
+ }
293
+ }
294
+ return config;
295
+ }
296
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultKeyValueService.d.ts","sourceRoot":"","sources":["../../../../src/service/configuration/DefaultKeyValueService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,eAAe,EACf,iBAAiB,EAClB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,oBAAoB,MAAM,2BAA2B,CAAC;AAGlE;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,sBAAuB,YAAW,eAAe;;IAMpE;;;;;OAKG;IACH,YACE,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,EACpC,aAAa,CAAC,EAAE,oBAAoB,EACpC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAKrC;IAEM,OAAO,IAAI,OAAO,CAExB;IAEM,SAAS,IAAI,IAAI,CAEvB;IAEY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAGlC;IAEY,GAAG,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAsBzE;IAEM,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAExC;IAEY,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAGrE;IAsCY,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO9C;CAsBF"}
1
+ {"version":3,"file":"DefaultKeyValueService.d.ts","sourceRoot":"","sources":["../../../../src/service/configuration/DefaultKeyValueService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,eAAe,EACf,iBAAiB,EAClB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,oBAAoB,MAAM,2BAA2B,CAAC;AAGlE;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,sBAAuB,YAAW,eAAe;;IAMpE;;;;;OAKG;IACH,YACE,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,EACpC,aAAa,CAAC,EAAE,oBAAoB,EACpC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAKrC;IAEM,OAAO,IAAI,OAAO,CAExB;IAEM,SAAS,IAAI,IAAI,CAEvB;IAEY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAGlC;IAEY,GAAG,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CA2BzE;IAEM,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAExC;IAEY,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAGrE;IAqCY,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO9C;CAwBF"}
@@ -40,6 +40,9 @@ export default class DefaultKeyValueService {
40
40
  if (!this.#secretService) {
41
41
  throw new Error("Secret sentinel found but no secret service is available");
42
42
  }
43
+ if (!this.#secretService.ownsSecretName(bunSecretName)) {
44
+ throw new Error(`Secret sentinel for key: '${key}' refers to a secret outside of this scope`);
45
+ }
43
46
  const secretValue = await this.#secretService.getSecret(bunSecretName);
44
47
  if (secretValue === null) {
45
48
  throw new Error(`Secret not found in OS secret store for key: '${key}'`);
@@ -65,8 +68,7 @@ export default class DefaultKeyValueService {
65
68
  if (!this.#secretService) {
66
69
  throw new Error("Attempt to set a secret but no secret service is available");
67
70
  }
68
- const secretName = [key, ...path].join("_");
69
- const bunSecretName = await this.#secretService.setSecret(secretName, JSON.stringify(value.value));
71
+ const bunSecretName = await this.#secretService.setSecret([key, ...path], JSON.stringify(value.value));
70
72
  return SECRET_SENTINEL_PREFIX + bunSecretName;
71
73
  }
72
74
  if (Array.isArray(value)) {
@@ -100,7 +102,9 @@ export default class DefaultKeyValueService {
100
102
  if (typeof value === "string" && value.startsWith(SECRET_SENTINEL_PREFIX)) {
101
103
  if (this.#secretService) {
102
104
  const bunSecretName = value.slice(SECRET_SENTINEL_PREFIX.length);
103
- await this.#secretService.deleteSecret(bunSecretName);
105
+ if (this.#secretService.ownsSecretName(bunSecretName)) {
106
+ await this.#secretService.deleteSecret(bunSecretName);
107
+ }
104
108
  }
105
109
  return;
106
110
  }
@@ -16,8 +16,23 @@ export interface SecretsApi {
16
16
  }
17
17
  export default class DefaultSecretService implements SecretService {
18
18
  #private;
19
- constructor(serviceName: string, scope: string, secretsApi?: SecretsApi);
20
- setSecret(key: string, value: string): Promise<string>;
19
+ /**
20
+ * @param scope the components identifying the scope, e.g. `["command", "a"]`.
21
+ */
22
+ constructor(serviceName: string, scope: ReadonlyArray<string>, secretsApi?: SecretsApi);
23
+ /**
24
+ * The name under which a secret with the provided key is stored. The scope and key components
25
+ * are escaped and delimited so that distinct (scope, key) pairs never produce the same name.
26
+ *
27
+ * @param key a single key or the components of a key, e.g. a key and a path within its value.
28
+ */
29
+ secretName(key: string | ReadonlyArray<string>): string;
30
+ /**
31
+ * Whether the stored secret name was created by this scope, and, if `firstKeyComponent` is
32
+ * provided, with that value as the first component of its key.
33
+ */
34
+ ownsSecretName(bunSecretName: string, firstKeyComponent?: string): boolean;
35
+ setSecret(key: string | ReadonlyArray<string>, value: string): Promise<string>;
21
36
  getSecret(bunSecretName: string): Promise<string | null>;
22
37
  deleteSecret(bunSecretName: string): Promise<boolean>;
23
38
  hasSecret(bunSecretName: string): Promise<boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultSecretService.d.ts","sourceRoot":"","sources":["../../../../src/service/configuration/DefaultSecretService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAM7E,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxE,GAAG,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,MAAM,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtE;AAED,MAAM,CAAC,OAAO,OAAO,oBAAqB,YAAW,aAAa;;IAKhE,YAAY,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,EAStE;IAMY,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAclE;IAEY,SAAS,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKpE;IAEY,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAKjE;IAEY,SAAS,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO9D;CACF"}
1
+ {"version":3,"file":"DefaultSecretService.d.ts","sourceRoot":"","sources":["../../../../src/service/configuration/DefaultSecretService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAM7E,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxE,GAAG,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,MAAM,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtE;AAmBD,MAAM,CAAC,OAAO,OAAO,oBAAqB,YAAW,aAAa;;IAKhE;;OAEG;IACH,YAAY,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,EAAE,UAAU,EASrF;IAMD;;;;;OAKG;IACI,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,GAAG,MAAM,CAG7D;IAED;;;OAGG;IACI,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,iBAAiB,CAAC,EAAE,MAAM,GAAG,OAAO,CAMhF;IAEY,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc1F;IAEY,SAAS,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKpE;IAEY,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAKjE;IAEY,SAAS,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO9D;CACF"}
@@ -1,23 +1,62 @@
1
1
  import { secrets as bunSecrets } from "bun";
2
2
  const MAX_NAME_LENGTH = 255;
3
3
  const MAX_VALUE_BYTES = 2047;
4
+ /**
5
+ * Injective escape of a name component to [A-Za-z0-9_]: alphanumerics are kept and every other
6
+ * character (including `_`) becomes `_<UPPERCASE HEX CODE POINT>_`.
7
+ */
8
+ function escapeComponent(value) {
9
+ let result = "";
10
+ for (const char of value) {
11
+ result += /[a-zA-Z0-9]/.test(char)
12
+ ? char
13
+ : `_${char.codePointAt(0).toString(16).toUpperCase()}_`;
14
+ }
15
+ return result;
16
+ }
17
+ // `Z` is not a hex digit so `_Z` can never be mistaken for the start of an escape.
18
+ const SEPARATOR = "_Z";
4
19
  export default class DefaultSecretService {
5
20
  #serviceName;
6
21
  #secrets;
7
22
  #scope;
23
+ /**
24
+ * @param scope the components identifying the scope, e.g. `["command", "a"]`.
25
+ */
8
26
  constructor(serviceName, scope, secretsApi) {
9
- this.#serviceName = DefaultSecretService.#sanitize(serviceName);
27
+ this.#serviceName = serviceName.replace(/[^a-zA-Z0-9_]/g, "_");
10
28
  if (this.#serviceName.length > MAX_NAME_LENGTH) {
11
29
  throw new Error(`Service name exceeds ${MAX_NAME_LENGTH} characters after sanitization: '${this.#serviceName}'`);
12
30
  }
13
- this.#scope = DefaultSecretService.#sanitize(scope);
31
+ this.#scope = scope;
14
32
  this.#secrets = secretsApi ?? bunSecrets;
15
33
  }
16
- static #sanitize(value) {
17
- return value.replace(/[^a-zA-Z0-9_]/g, "_");
34
+ #scopePrefix() {
35
+ return this.#scope.map((c) => escapeComponent(c) + SEPARATOR).join("");
36
+ }
37
+ /**
38
+ * The name under which a secret with the provided key is stored. The scope and key components
39
+ * are escaped and delimited so that distinct (scope, key) pairs never produce the same name.
40
+ *
41
+ * @param key a single key or the components of a key, e.g. a key and a path within its value.
42
+ */
43
+ secretName(key) {
44
+ const parts = typeof key === "string" ? [key] : key;
45
+ return this.#scopePrefix() + parts.map(escapeComponent).join(SEPARATOR);
46
+ }
47
+ /**
48
+ * Whether the stored secret name was created by this scope, and, if `firstKeyComponent` is
49
+ * provided, with that value as the first component of its key.
50
+ */
51
+ ownsSecretName(bunSecretName, firstKeyComponent) {
52
+ if (firstKeyComponent === undefined) {
53
+ return bunSecretName.startsWith(this.#scopePrefix());
54
+ }
55
+ const base = this.#scopePrefix() + escapeComponent(firstKeyComponent);
56
+ return bunSecretName === base || bunSecretName.startsWith(base + SEPARATOR);
18
57
  }
19
58
  async setSecret(key, value) {
20
- const name = this.#scope + "_" + DefaultSecretService.#sanitize(key);
59
+ const name = this.secretName(key);
21
60
  if (name.length > MAX_NAME_LENGTH) {
22
61
  throw new Error(`Secret name exceeds ${MAX_NAME_LENGTH} characters: '${name}'`);
23
62
  }
@@ -2,6 +2,7 @@ import type { ServiceInfo, ServiceProvider } from "@flowscripter/dynamic-cli-fra
2
2
  import type { Context } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import type { CLIConfig } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import type { KeyValueService } from "@flowscripter/dynamic-cli-framework-api";
5
+ import { type SecretsApi } from "./DefaultSecretService.ts";
5
6
  import type ConfigurationServiceProvider from "./ConfigurationServiceProvider.ts";
6
7
  import type { KeyValueServiceScopeType } from "./ConfigurationServiceProvider.ts";
7
8
  /**
@@ -75,8 +76,9 @@ export default class KeyValueServiceProvider implements ServiceProvider {
75
76
  * backs this provider's data - an internal wiring reference, never exposed further.
76
77
  * @param keyValueServiceEnabled optionally provide a {@link KeyValueService} implementation: `configurationServiceProvider.configEnabled` must be true in this case
77
78
  * @param secretServiceEnabled optionally enable OS-native secret storage via Bun.secrets: `configurationServiceProvider.configEnabled` must be true in this case
79
+ * @param secretsApi optionally override the OS secret store.
78
80
  */
79
- constructor(servicePriority: number, configurationServiceProvider: ConfigurationServiceProvider, keyValueServiceEnabled?: boolean, secretServiceEnabled?: boolean);
81
+ constructor(servicePriority: number, configurationServiceProvider: ConfigurationServiceProvider, keyValueServiceEnabled?: boolean, secretServiceEnabled?: boolean, secretsApi?: SecretsApi);
80
82
  getServiceInfo(cliConfig: CLIConfig): Promise<ServiceInfo>;
81
83
  /**
82
84
  * Return the {@link KeyValueService} bound to the given scope, creating (and caching) it on
@@ -1 +1 @@
1
- {"version":3,"file":"KeyValueServiceProvider.d.ts","sourceRoot":"","sources":["../../../../src/service/configuration/KeyValueServiceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EACV,eAAe,EAGhB,MAAM,yCAAyC,CAAC;AAIjD,OAAO,KAAK,4BAA4B,MAAM,mCAAmC,CAAC;AAClF,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAoClF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,MAAM,CAAC,OAAO,OAAO,uBAAwB,YAAW,eAAe;;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAwB;IAClD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IAIjC,SAAgB,sBAAsB,EAAE,OAAO,CAAC;IAChD,SAAgB,oBAAoB,EAAE,OAAO,CAAC;IAW9C;;;;;;;;OAQG;IACH,YACE,eAAe,EAAE,MAAM,EACvB,4BAA4B,EAAE,4BAA4B,EAC1D,sBAAsB,UAAQ,EAC9B,oBAAoB,UAAQ,EAY7B;IAEM,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAUhE;IAED;;;;;;;;;;OAUG;IACI,wBAAwB,CAC7B,SAAS,EAAE,wBAAwB,EACnC,QAAQ,EAAE,MAAM,GACf,eAAe,CAwBjB;IAED;;;;;;;;;;;;;;OAcG;IACI,kBAAkB,CACvB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,wBAAwB,EACnC,QAAQ,EAAE,MAAM,GACf,OAAO,CAcT;IAqBY,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CASxD;CACF"}
1
+ {"version":3,"file":"KeyValueServiceProvider.d.ts","sourceRoot":"","sources":["../../../../src/service/configuration/KeyValueServiceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EACV,eAAe,EAGhB,MAAM,yCAAyC,CAAC;AAGjD,OAA6B,EAAE,KAAK,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAClF,OAAO,KAAK,4BAA4B,MAAM,mCAAmC,CAAC;AAClF,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAoClF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,MAAM,CAAC,OAAO,OAAO,uBAAwB,YAAW,eAAe;;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAwB;IAClD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IAIjC,SAAgB,sBAAsB,EAAE,OAAO,CAAC;IAChD,SAAgB,oBAAoB,EAAE,OAAO,CAAC;IAa9C;;;;;;;;;OASG;IACH,YACE,eAAe,EAAE,MAAM,EACvB,4BAA4B,EAAE,4BAA4B,EAC1D,sBAAsB,UAAQ,EAC9B,oBAAoB,UAAQ,EAC5B,UAAU,CAAC,EAAE,UAAU,EAaxB;IAEM,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAUhE;IAED;;;;;;;;;;OAUG;IACI,wBAAwB,CAC7B,SAAS,EAAE,wBAAwB,EACnC,QAAQ,EAAE,MAAM,GACf,eAAe,CAwBjB;IAED;;;;;;;;;;;;;;OAcG;IACI,kBAAkB,CACvB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,wBAAwB,EACnC,QAAQ,EAAE,MAAM,GACf,OAAO,CAcT;IAqBY,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CASxD;CACF"}
@@ -1,6 +1,6 @@
1
1
  import { KEY_VALUE_SERVICE_ID, SHUTDOWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
2
2
  import DefaultKeyValueService from "./DefaultKeyValueService.js";
3
- import DefaultSecretService from "./DefaultSecretService.js";
3
+ import DefaultSecretService, {} from "./DefaultSecretService.js";
4
4
  import { KEY_VALUE_FLUSH_SHUTDOWN_TASK_PRIORITY } from "../../runtime/lifecycle/priorities.js";
5
5
  /**
6
6
  * Registered under {@link KEY_VALUE_SERVICE_ID} in the plain, undecorated {@link Context} purely
@@ -95,6 +95,7 @@ export default class KeyValueServiceProvider {
95
95
  // re-pointed. See getScopedKeyValueService().
96
96
  #commandScopedKeyValueServices = new Map();
97
97
  #serviceScopedKeyValueServices = new Map();
98
+ #secretsApi;
98
99
  // used to construct scoped DefaultSecretService instances on demand.
99
100
  #cliConfigName;
100
101
  /**
@@ -105,8 +106,9 @@ export default class KeyValueServiceProvider {
105
106
  * backs this provider's data - an internal wiring reference, never exposed further.
106
107
  * @param keyValueServiceEnabled optionally provide a {@link KeyValueService} implementation: `configurationServiceProvider.configEnabled` must be true in this case
107
108
  * @param secretServiceEnabled optionally enable OS-native secret storage via Bun.secrets: `configurationServiceProvider.configEnabled` must be true in this case
109
+ * @param secretsApi optionally override the OS secret store.
108
110
  */
109
- constructor(servicePriority, configurationServiceProvider, keyValueServiceEnabled = false, secretServiceEnabled = false) {
111
+ constructor(servicePriority, configurationServiceProvider, keyValueServiceEnabled = false, secretServiceEnabled = false, secretsApi) {
110
112
  if (!configurationServiceProvider.configEnabled && keyValueServiceEnabled) {
111
113
  throw new Error("configEnabled must be true if keyValueServiceEnabled is true");
112
114
  }
@@ -116,6 +118,7 @@ export default class KeyValueServiceProvider {
116
118
  this.servicePriority = servicePriority;
117
119
  this.#configurationServiceProvider = configurationServiceProvider;
118
120
  this.keyValueServiceEnabled = keyValueServiceEnabled;
121
+ this.#secretsApi = secretsApi;
119
122
  this.secretServiceEnabled = secretServiceEnabled;
120
123
  }
121
124
  getServiceInfo(cliConfig) {
@@ -152,7 +155,7 @@ export default class KeyValueServiceProvider {
152
155
  }
153
156
  const keyValueData = this.#configurationServiceProvider.getKeyValueData(scopeType, scopeKey);
154
157
  const secretService = this.secretServiceEnabled
155
- ? new DefaultSecretService(this.#cliConfigName, `${scopeType}_${scopeKey}`)
158
+ ? new DefaultSecretService(this.#cliConfigName, [scopeType, scopeKey], this.#secretsApi)
156
159
  : undefined;
157
160
  const scopedService = new DefaultKeyValueService(keyValueData, secretService, () => this.#flushDirtyScopes());
158
161
  scopedServices.set(scopeKey, scopedService);
@@ -1,15 +1,11 @@
1
+ import type DefaultConfigurationService from "../DefaultConfigurationService.ts";
1
2
  import type { Context } from "@flowscripter/dynamic-cli-framework-api";
2
3
  import type { Option } from "@flowscripter/dynamic-cli-framework-api";
3
4
  import type { Positional } from "@flowscripter/dynamic-cli-framework-api";
4
5
  import type { SubCommand } from "@flowscripter/dynamic-cli-framework-api";
5
6
  /**
6
7
  * Member of the `config` group which dumps the configuration loaded by
7
- * {@link ConfigurationServiceProvider}.
8
- *
9
- * Takes a direct reference to {@link ConfigurationServiceProvider.getConfigString}, wired by the
10
- * provider that constructs this command, rather than looking it up via {@link Context} -
11
- * deliberately, since a full config dump is not something every command/task should be able to
12
- * reach through a general lookup (see {@link ConfigurationService}'s doc comment).
8
+ * {@link DefaultConfigurationService}, with secrets masked.
13
9
  */
14
10
  export default class ConfigDumpSubCommand implements SubCommand {
15
11
  #private;
@@ -17,7 +13,7 @@ export default class ConfigDumpSubCommand implements SubCommand {
17
13
  readonly description = "Dump configuration values";
18
14
  readonly options: ReadonlyArray<Option>;
19
15
  readonly positionals: ReadonlyArray<Positional>;
20
- constructor(getConfigString: () => string);
16
+ constructor(configurationService: DefaultConfigurationService);
21
17
  execute(context: Context): Promise<void>;
22
18
  }
23
19
  //# sourceMappingURL=ConfigDumpSubCommand.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConfigDumpSubCommand.d.ts","sourceRoot":"","sources":["../../../../../src/service/configuration/command/ConfigDumpSubCommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yCAAyC,CAAC;AACtE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAM1E;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,OAAO,oBAAqB,YAAW,UAAU;;IAC7D,QAAQ,CAAC,IAAI,UAAU;IACvB,QAAQ,CAAC,WAAW,+BAA+B;IACnD,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAM;IAC7C,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,UAAU,CAAC,CAAM;IAIrD,YAAmB,eAAe,EAAE,MAAM,MAAM,EAE/C;IAEY,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CASpD;CACF"}
1
+ {"version":3,"file":"ConfigDumpSubCommand.d.ts","sourceRoot":"","sources":["../../../../../src/service/configuration/command/ConfigDumpSubCommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,2BAA2B,MAAM,mCAAmC,CAAC;AACjF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yCAAyC,CAAC;AACtE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAM1E;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,oBAAqB,YAAW,UAAU;;IAC7D,QAAQ,CAAC,IAAI,UAAU;IACvB,QAAQ,CAAC,WAAW,+BAA+B;IACnD,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAM;IAC7C,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,UAAU,CAAC,CAAM;IAIrD,YAAmB,oBAAoB,EAAE,2BAA2B,EAEnE;IAEY,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CASpD;CACF"}