@effected/config-file 0.18.0 → 0.19.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/ConfigEvent.js +7 -0
- package/ConfigFile.js +24 -5
- package/ConfigMigration.js +5 -1
- package/ConfigProvider.js +5 -1
- package/ConfigResolver.js +14 -7
- package/EncryptedCodec.js +12 -5
- package/README.md +29 -3
- package/index.d.ts +168 -41
- package/internal/crypto.js +67 -26
- package/package.json +4 -4
package/ConfigEvent.js
CHANGED
|
@@ -30,6 +30,13 @@ const ConfigSourceRef = Schema.Struct({
|
|
|
30
30
|
* `reason` string, so every field a subscriber might branch on survives; a
|
|
31
31
|
* subscriber that wants prose can read `error.message`.
|
|
32
32
|
*
|
|
33
|
+
* `error` is typed `unknown` (`Schema.Defect()`) on purpose. `ParseFailed` and
|
|
34
|
+
* `StringifyFailed` carry whatever the service's codec failed with, and under
|
|
35
|
+
* a decorated codec that is not always a `ConfigCodecError`: an
|
|
36
|
+
* `EncryptedCodec` failure arrives as `ConfigEncryptionError`, a migration
|
|
37
|
+
* failure as `ConfigMigrationError`. Narrow by `_tag` or `instanceof`, never
|
|
38
|
+
* by assuming the codec error.
|
|
39
|
+
*
|
|
33
40
|
* @public
|
|
34
41
|
*/
|
|
35
42
|
const ConfigEventPayload = Schema.Union([
|
package/ConfigFile.js
CHANGED
|
@@ -292,8 +292,13 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
292
292
|
* `target` is the file this text is destined for, when there is one. It is
|
|
293
293
|
* only ever used to NAME the file in an error — `encode` passes `none` and
|
|
294
294
|
* its errors honestly carry no path.
|
|
295
|
+
*
|
|
296
|
+
* `onCodecFailure` observes a failure of the codec's `stringify` step and
|
|
297
|
+
* nothing else. The codec's error type is the caller's `CE`, which may be a
|
|
298
|
+
* decorator's widened union, so a codec failure cannot be recognized by tag
|
|
299
|
+
* after the fact — it is observed where it happens.
|
|
295
300
|
*/
|
|
296
|
-
const encodeTo = (value, target, encodeOptions) => Effect.gen(function* () {
|
|
301
|
+
const encodeTo = (value, target, encodeOptions, onCodecFailure) => Effect.gen(function* () {
|
|
297
302
|
const encoded = yield* Schema.encodeEffect(options.schema)(value).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new ConfigValidationError({
|
|
298
303
|
path: target,
|
|
299
304
|
issue: error.issue
|
|
@@ -301,7 +306,7 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
301
306
|
const serialized = yield* options.codec.stringify(encoded).pipe(Effect.mapError((error) => Option.match(target, {
|
|
302
307
|
onNone: () => error,
|
|
303
308
|
onSome: (file) => withCodecPath(error, file)
|
|
304
|
-
})));
|
|
309
|
+
})), Effect.tapError(onCodecFailure));
|
|
305
310
|
return prependHeader(serialized, encodeOptions?.header);
|
|
306
311
|
});
|
|
307
312
|
/**
|
|
@@ -312,18 +317,18 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
312
317
|
* failed writes must not see a dry run's codec failure.
|
|
313
318
|
*/
|
|
314
319
|
const encodeAndWrite = (value, target, encodeOptions) => Effect.gen(function* () {
|
|
315
|
-
const serialized = yield* encodeTo(value, Option.some(target), encodeOptions
|
|
320
|
+
const serialized = yield* encodeTo(value, Option.some(target), encodeOptions, (error) => emit({
|
|
316
321
|
_tag: "StringifyFailed",
|
|
317
322
|
codec: options.codec.name,
|
|
318
323
|
error
|
|
319
|
-
})
|
|
324
|
+
}));
|
|
320
325
|
yield* fs.writeFileString(target, serialized).pipe(Effect.mapError((cause) => new ConfigFileWriteError({
|
|
321
326
|
path: target,
|
|
322
327
|
cause
|
|
323
328
|
})));
|
|
324
329
|
});
|
|
325
330
|
const encode = Effect.fn("ConfigFile.encode")(function* (value, encodeOptions) {
|
|
326
|
-
return yield* encodeTo(value, Option.none(), encodeOptions);
|
|
331
|
+
return yield* encodeTo(value, Option.none(), encodeOptions, () => Effect.void);
|
|
327
332
|
});
|
|
328
333
|
/**
|
|
329
334
|
* Resolve `defaultPath`, `mkdir -p` its parent and write. Shared by `save`
|
|
@@ -447,9 +452,23 @@ var ConfigFile = class {
|
|
|
447
452
|
/**
|
|
448
453
|
* Create a uniquely-keyed service class for one config schema.
|
|
449
454
|
*
|
|
455
|
+
* @remarks
|
|
456
|
+
* The optional third type argument is the codec's error channel. Omit it
|
|
457
|
+
* for the built-in codecs. A service over a decorated codec names the
|
|
458
|
+
* decorator's widened error, because the key is declared before the codec
|
|
459
|
+
* is chosen and cannot infer it: wiring `EncryptedCodec(JsonCodec, key)`
|
|
460
|
+
* into a key declared without it is a compile error, since that service's
|
|
461
|
+
* methods would fail with a `ConfigEncryptionError` its type never mentions.
|
|
462
|
+
*
|
|
450
463
|
* @example
|
|
451
464
|
* ```ts
|
|
452
465
|
* class AppConfig extends ConfigFile.Service<AppConfig, AppShape>()("app/Config") {}
|
|
466
|
+
*
|
|
467
|
+
* class Secrets extends ConfigFile.Service<
|
|
468
|
+
* Secrets,
|
|
469
|
+
* SecretsShape,
|
|
470
|
+
* ConfigCodecError | ConfigEncryptionError
|
|
471
|
+
* >()("app/Secrets") {}
|
|
453
472
|
* ```
|
|
454
473
|
*/
|
|
455
474
|
static Service = Service;
|
package/ConfigMigration.js
CHANGED
|
@@ -88,7 +88,11 @@ var ConfigMigration = class {
|
|
|
88
88
|
* The returned codec's error channel **widens** to include
|
|
89
89
|
* {@link ConfigMigrationError} rather than flattening migration failures into
|
|
90
90
|
* the inner codec's error — the reason the {@link (ConfigCodec:interface)} seam is generic
|
|
91
|
-
* in its error type.
|
|
91
|
+
* in its error type. The inner codec's own error type is kept as-is, so
|
|
92
|
+
* wrapping an `EncryptedCodec` yields
|
|
93
|
+
* `ConfigCodec<ConfigCodecError | ConfigEncryptionError | ConfigMigrationError>`,
|
|
94
|
+
* and the result goes straight into `ConfigFile.layer`, `testLayer` or
|
|
95
|
+
* `read`.
|
|
92
96
|
*
|
|
93
97
|
* @example
|
|
94
98
|
* ```ts
|
package/ConfigProvider.js
CHANGED
|
@@ -42,6 +42,9 @@ import { ConfigProvider, Effect } from "effect";
|
|
|
42
42
|
* Descent by own property is also why a prototype getter and `__proto__` are
|
|
43
43
|
* both unreachable through the returned provider.
|
|
44
44
|
*
|
|
45
|
+
* `CE` is the service's codec error, so a service over a decorated codec
|
|
46
|
+
* keeps its widened failure in this effect's error channel.
|
|
47
|
+
*
|
|
45
48
|
* @example
|
|
46
49
|
* ```ts
|
|
47
50
|
* import { asConfigProvider } from "@effected/config-file";
|
|
@@ -73,7 +76,8 @@ const asConfigProvider = (service) => Effect.map(service.load, (value) => Config
|
|
|
73
76
|
*
|
|
74
77
|
* The load happens when the layer is built, and a {@link ConfigFileNotFoundError}
|
|
75
78
|
* surfaces in the layer's error channel rather than degrading to an empty
|
|
76
|
-
* provider — the same honesty {@link asConfigProvider} keeps.
|
|
79
|
+
* provider — the same honesty {@link asConfigProvider} keeps. A service over a
|
|
80
|
+
* decorated codec carries its widened codec error `CE` there too.
|
|
77
81
|
*
|
|
78
82
|
* @example
|
|
79
83
|
* ```ts
|
package/ConfigResolver.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, FileSystem, Option, Path } from "effect";
|
|
1
|
+
import { Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
2
2
|
import { Walker } from "@effected/walker";
|
|
3
3
|
|
|
4
4
|
// src/ConfigResolver.ts
|
|
@@ -104,18 +104,25 @@ const isGitRoot = (dir) => Effect.gen(function* () {
|
|
|
104
104
|
const path = yield* Path.Path;
|
|
105
105
|
return yield* fs.exists(path.join(dir, ".git"));
|
|
106
106
|
});
|
|
107
|
+
/**
|
|
108
|
+
* Decode a `package.json` text to `Some` exactly when it is a JSON object
|
|
109
|
+
* carrying a `workspaces` key, whatever that key's value (`null` included).
|
|
110
|
+
*
|
|
111
|
+
* @remarks
|
|
112
|
+
* Field presence is all a root check needs, so the struct is
|
|
113
|
+
* `{ workspaces: Unknown }` rather than a `package-json` dependency. It is
|
|
114
|
+
* built once at module scope; `decodeUnknownOption` never throws, which is
|
|
115
|
+
* what keeps a malformed manifest from leaking a defect through a predicate
|
|
116
|
+
* whose error channel is `PlatformError`.
|
|
117
|
+
*/
|
|
118
|
+
const decodeWorkspacesManifest = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Struct({ workspaces: Schema.Unknown })));
|
|
107
119
|
/** A workspace root is marked by `pnpm-workspace.yaml`, or a `package.json` with a `workspaces` field. */
|
|
108
120
|
const isWorkspaceRoot = (dir) => Effect.gen(function* () {
|
|
109
121
|
const fs = yield* FileSystem.FileSystem;
|
|
110
122
|
const path = yield* Path.Path;
|
|
111
123
|
if (yield* fs.exists(path.join(dir, "pnpm-workspace.yaml"))) return true;
|
|
112
124
|
const pkgPath = path.join(dir, "package.json");
|
|
113
|
-
if (yield* fs.exists(pkgPath))
|
|
114
|
-
const content = yield* fs.readFileString(pkgPath);
|
|
115
|
-
try {
|
|
116
|
-
if ("workspaces" in JSON.parse(content)) return true;
|
|
117
|
-
} catch {}
|
|
118
|
-
}
|
|
125
|
+
if (yield* fs.exists(pkgPath)) return Option.isSome(decodeWorkspacesManifest(yield* fs.readFileString(pkgPath)));
|
|
119
126
|
return false;
|
|
120
127
|
});
|
|
121
128
|
const gitRoot = (options) => rootAnchored("git", isGitRoot, options);
|
package/EncryptedCodec.js
CHANGED
|
@@ -83,9 +83,17 @@ const keyEffect = (keySource) => keySource._tag === "CryptoKey" ? keySource.key
|
|
|
83
83
|
* the remainder is the ciphertext, and the plaintext is handed to the inner
|
|
84
84
|
* codec's `parse`.
|
|
85
85
|
*
|
|
86
|
+
* The envelope is decoded with core's `effect/encoding/Base64`. Whitespace
|
|
87
|
+
* around it (a trailing newline, a BOM) is tolerated; whitespace inside it, or
|
|
88
|
+
* missing padding, fails with phase `"encoding"`. `stringify` never produces
|
|
89
|
+
* either.
|
|
90
|
+
*
|
|
86
91
|
* The error channel **widens** to `E | ConfigEncryptionError` rather than
|
|
87
92
|
* flattening — the inner codec's failures stay distinguishable from
|
|
88
|
-
* cryptographic ones.
|
|
93
|
+
* cryptographic ones. The result plugs straight into `ConfigFile.layer`,
|
|
94
|
+
* `ConfigFile.testLayer` and `ConfigFile.read`, whose error unions widen with
|
|
95
|
+
* it; a `ConfigFile.Service` key names the widened error as its third type
|
|
96
|
+
* argument.
|
|
89
97
|
*
|
|
90
98
|
* @example
|
|
91
99
|
* ```ts
|
|
@@ -99,8 +107,7 @@ const keyEffect = (keySource) => keySource._tag === "CryptoKey" ? keySource.key
|
|
|
99
107
|
*/
|
|
100
108
|
function EncryptedCodec(inner, keySource) {
|
|
101
109
|
const name = `encrypted(${inner.name})`;
|
|
102
|
-
const
|
|
103
|
-
const getKey = Effect.onExit(resolveKey, (exit) => Exit.isSuccess(exit) ? Effect.void : invalidateKey);
|
|
110
|
+
const getKey = Effect.runSync(Effect.cachedWithTTL(keyEffect(keySource), (exit) => Exit.isSuccess(exit) ? Duration.infinity : Duration.zero));
|
|
104
111
|
return {
|
|
105
112
|
name,
|
|
106
113
|
parse: (raw) => Effect.gen(function* () {
|
|
@@ -119,9 +126,9 @@ function EncryptedCodec(inner, keySource) {
|
|
|
119
126
|
const key = yield* getKey;
|
|
120
127
|
const serialized = yield* inner.stringify(value);
|
|
121
128
|
const encoded = new TextEncoder().encode(serialized);
|
|
122
|
-
const iv = randomIv
|
|
129
|
+
const iv = yield* Effect.mapError(randomIv, toPublic);
|
|
123
130
|
const ciphertext = yield* Effect.mapError(encrypt(key, iv, encoded), toPublic);
|
|
124
|
-
return
|
|
131
|
+
return toBase64(iv, ciphertext);
|
|
125
132
|
})
|
|
126
133
|
};
|
|
127
134
|
}
|
package/README.md
CHANGED
|
@@ -187,11 +187,20 @@ Four codecs ship in the package, each a free-standing named export:
|
|
|
187
187
|
|
|
188
188
|
One install covers every format, and you still pay only for the parser you name. The codecs are free-standing exports rather than properties of a namespace object, so importing `TomlCodec` never references the YAML or JSONC bindings, their parsing engines are unreachable from your entrypoint and a bundler drops them. A JSON-only application ships no parser at all.
|
|
189
189
|
|
|
190
|
-
Codecs compose. `EncryptedCodec` wraps any codec with AES-GCM, and `ConfigMigration.make` wraps any codec so parsed content is brought up to the latest version. Each *widens* the error channel rather than flattening its failures into the inner codec's error:
|
|
190
|
+
Codecs compose. `EncryptedCodec` wraps any codec with AES-GCM, and `ConfigMigration.make` wraps any codec so parsed content is brought up to the latest version. Each *widens* the error channel rather than flattening its failures into the inner codec's error, and a decorated codec plugs into `ConfigFile.layer`, `ConfigFile.testLayer` and `ConfigFile.read` like any other: their error unions widen with it. A service key names the widened error as its third type argument:
|
|
191
191
|
|
|
192
192
|
```ts
|
|
193
|
-
import {
|
|
194
|
-
import {
|
|
193
|
+
import type { ConfigCodec } from "@effected/config-file";
|
|
194
|
+
import {
|
|
195
|
+
ConfigFile,
|
|
196
|
+
ConfigMigration,
|
|
197
|
+
ConfigResolver,
|
|
198
|
+
EncryptedCodec,
|
|
199
|
+
EncryptedCodecKey,
|
|
200
|
+
JsonCodec,
|
|
201
|
+
MergeStrategy,
|
|
202
|
+
} from "@effected/config-file";
|
|
203
|
+
import { Effect, Schema } from "effect";
|
|
195
204
|
|
|
196
205
|
const migrating = ConfigMigration.make({
|
|
197
206
|
codec: JsonCodec,
|
|
@@ -206,8 +215,25 @@ const migrating = ConfigMigration.make({
|
|
|
206
215
|
|
|
207
216
|
// `parse` now fails with ConfigCodecError | ConfigMigrationError | ConfigEncryptionError.
|
|
208
217
|
export const secret = EncryptedCodec(migrating, EncryptedCodecKey.fromPassphrase("hunter2", new Uint8Array(16)));
|
|
218
|
+
|
|
219
|
+
const Settings = Schema.Struct({ port: Schema.Number });
|
|
220
|
+
// Read back from the codec value: ConfigCodecError | ConfigMigrationError | ConfigEncryptionError.
|
|
221
|
+
type SecretError = ConfigCodec.ErrorOf<typeof secret>;
|
|
222
|
+
|
|
223
|
+
class SecretSettings extends ConfigFile.Service<SecretSettings, typeof Settings.Type, SecretError>()(
|
|
224
|
+
"app/SecretSettings",
|
|
225
|
+
) {}
|
|
226
|
+
|
|
227
|
+
export const SecretSettingsLive = ConfigFile.layer(SecretSettings, {
|
|
228
|
+
schema: Settings,
|
|
229
|
+
codec: secret,
|
|
230
|
+
resolvers: [ConfigResolver.explicitPath("./settings.enc")],
|
|
231
|
+
strategy: MergeStrategy.firstMatch(),
|
|
232
|
+
});
|
|
209
233
|
```
|
|
210
234
|
|
|
235
|
+
Leave the third type argument off and the `ConfigFile.layer` call is a compile error: that service's methods would fail with a `ConfigEncryptionError` its type never names. The envelope's base64 is decoded by core's `effect/encoding/Base64`, so whitespace around it is tolerated while whitespace inside it, or missing padding, fails with `ConfigEncryptionError` phase `"encoding"`.
|
|
236
|
+
|
|
211
237
|
## Features
|
|
212
238
|
|
|
213
239
|
- `ConfigFile.Service` / `ConfigFile.layer` / `ConfigFile.testLayer` — a per-schema service class and its layers. `testLayer` seeds files into a temp directory and wires the *real* implementation over them, so tests exercise the actual pipeline rather than a stub that can drift from it.
|
package/index.d.ts
CHANGED
|
@@ -66,6 +66,43 @@ interface ConfigCodec<E = ConfigCodecError> {
|
|
|
66
66
|
/** Turn a document back into file content. */
|
|
67
67
|
readonly stringify: (value: unknown) => Effect.Effect<string, E>;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Type-level helpers over the `ConfigCodec` interface.
|
|
71
|
+
*
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
74
|
+
declare namespace ConfigCodec {
|
|
75
|
+
/**
|
|
76
|
+
* The error channel of a codec value, read back from its type.
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* A decorated codec's error union is already on the value —
|
|
80
|
+
* `EncryptedCodec(...)` over `ConfigMigration.make(...)` is a
|
|
81
|
+
* `ConfigCodec<ConfigCodecError | ConfigEncryptionError | ConfigMigrationError>`
|
|
82
|
+
* — and a `ConfigFile.Service` key has to name the same union as its third
|
|
83
|
+
* type argument. Spelling it out by hand drifts the moment the codec
|
|
84
|
+
* changes; `ConfigCodec.ErrorOf<typeof codec>` cannot.
|
|
85
|
+
*
|
|
86
|
+
* Anything that is not a `ConfigCodec` resolves to `never`.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* import type { ConfigCodec } from "@effected/config-file";
|
|
91
|
+
* import { ConfigFile, ConfigMigration, EncryptedCodec, EncryptedCodecKey, JsonCodec } from "@effected/config-file";
|
|
92
|
+
*
|
|
93
|
+
* const codec = EncryptedCodec(
|
|
94
|
+
* ConfigMigration.make({ codec: JsonCodec, migrations: [] }),
|
|
95
|
+
* EncryptedCodecKey.fromPassphrase("hunter2", new Uint8Array(16)),
|
|
96
|
+
* );
|
|
97
|
+
*
|
|
98
|
+
* // Stays in sync with the codec: ConfigCodecError | ConfigMigrationError | ConfigEncryptionError.
|
|
99
|
+
* class Secrets extends ConfigFile.Service<Secrets, { port: number }, ConfigCodec.ErrorOf<typeof codec>>()(
|
|
100
|
+
* "app/Secrets",
|
|
101
|
+
* ) {}
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
type ErrorOf<C> = C extends ConfigCodec<infer E> ? E : never;
|
|
105
|
+
}
|
|
69
106
|
/**
|
|
70
107
|
* A reference to one configuration source that contributed to a load.
|
|
71
108
|
*
|
|
@@ -95,6 +132,13 @@ export declare const ConfigSourceRef: Schema.Struct<{
|
|
|
95
132
|
* `reason` string, so every field a subscriber might branch on survives; a
|
|
96
133
|
* subscriber that wants prose can read `error.message`.
|
|
97
134
|
*
|
|
135
|
+
* `error` is typed `unknown` (`Schema.Defect()`) on purpose. `ParseFailed` and
|
|
136
|
+
* `StringifyFailed` carry whatever the service's codec failed with, and under
|
|
137
|
+
* a decorated codec that is not always a `ConfigCodecError`: an
|
|
138
|
+
* `EncryptedCodec` failure arrives as `ConfigEncryptionError`, a migration
|
|
139
|
+
* failure as `ConfigMigrationError`. Narrow by `_tag` or `instanceof`, never
|
|
140
|
+
* by assuming the codec error.
|
|
141
|
+
*
|
|
98
142
|
* @public
|
|
99
143
|
*/
|
|
100
144
|
export declare const ConfigEventPayload: Schema.Union<readonly [Schema.TaggedStruct<"Discovered", {
|
|
@@ -637,30 +681,39 @@ export declare class ConfigValidationError extends ConfigValidationError_base {
|
|
|
637
681
|
/**
|
|
638
682
|
* The failure modes of the full discovery-and-load path.
|
|
639
683
|
*
|
|
684
|
+
* @remarks
|
|
685
|
+
* `CE` is the codec's error channel. It defaults to {@link ConfigCodecError},
|
|
686
|
+
* the error of the four built-in codecs; a decorated codec widens it. Under
|
|
687
|
+
* `EncryptedCodec(JsonCodec, key)` it is
|
|
688
|
+
* `ConfigCodecError | ConfigEncryptionError`, and the union widens with it
|
|
689
|
+
* rather than flattening the decorator's failure into a codec error.
|
|
690
|
+
*
|
|
640
691
|
* @public
|
|
641
692
|
*/
|
|
642
|
-
type ConfigLoadError = ConfigFileNotFoundError | ConfigFileReadError |
|
|
693
|
+
type ConfigLoadError<CE = ConfigCodecError> = ConfigFileNotFoundError | ConfigFileReadError | CE | ConfigValidationError;
|
|
643
694
|
/**
|
|
644
695
|
* The failure modes of reading one known path.
|
|
645
696
|
*
|
|
646
697
|
* @remarks
|
|
647
698
|
* Deliberately excludes {@link ConfigFileNotFoundError}: every method typed with
|
|
648
699
|
* this union either takes an explicit path or treats "nothing found" as success.
|
|
700
|
+
* `CE` is the codec's error channel, as on {@link ConfigLoadError}.
|
|
649
701
|
*
|
|
650
702
|
* @public
|
|
651
703
|
*/
|
|
652
|
-
type ConfigReadError = ConfigFileReadError |
|
|
704
|
+
type ConfigReadError<CE = ConfigCodecError> = ConfigFileReadError | CE | ConfigValidationError;
|
|
653
705
|
/**
|
|
654
706
|
* The failure modes of encoding and writing one known path.
|
|
655
707
|
*
|
|
656
708
|
* @remarks
|
|
657
709
|
* Deliberately excludes {@link ConfigFileNotFoundError} — the path is explicit,
|
|
658
710
|
* so there is nothing to discover — and {@link ConfigDefaultPathMissingError},
|
|
659
|
-
* because no default path is consulted.
|
|
711
|
+
* because no default path is consulted. `CE` is the codec's error channel, as
|
|
712
|
+
* on {@link ConfigLoadError}.
|
|
660
713
|
*
|
|
661
714
|
* @public
|
|
662
715
|
*/
|
|
663
|
-
type ConfigWriteError = ConfigFileWriteError |
|
|
716
|
+
type ConfigWriteError<CE = ConfigCodecError> = ConfigFileWriteError | CE | ConfigValidationError;
|
|
664
717
|
/**
|
|
665
718
|
* The failure modes of {@link ConfigFileShape.encode}: everything on the write
|
|
666
719
|
* path except the write itself.
|
|
@@ -668,10 +721,11 @@ type ConfigWriteError = ConfigFileWriteError | ConfigCodecError | ConfigValidati
|
|
|
668
721
|
* @remarks
|
|
669
722
|
* Deliberately excludes {@link ConfigFileWriteError} — nothing touches the
|
|
670
723
|
* filesystem — so a `--dry-run` caller's error channel is honest about that.
|
|
724
|
+
* `CE` is the codec's error channel, as on {@link ConfigLoadError}.
|
|
671
725
|
*
|
|
672
726
|
* @public
|
|
673
727
|
*/
|
|
674
|
-
type ConfigEncodeError = ConfigCodecError | ConfigValidationError;
|
|
728
|
+
type ConfigEncodeError<CE = ConfigCodecError> = CE | ConfigValidationError;
|
|
675
729
|
/**
|
|
676
730
|
* Options shared by {@link ConfigFileShape.encode} and {@link ConfigFileShape.write}.
|
|
677
731
|
*
|
|
@@ -694,32 +748,40 @@ interface ConfigEncodeOptions {
|
|
|
694
748
|
readonly header?: string;
|
|
695
749
|
}
|
|
696
750
|
/**
|
|
697
|
-
* The failure modes of {@link ConfigFileShape.save}.
|
|
751
|
+
* The failure modes of {@link ConfigFileShape.save}. `CE` is the codec's error
|
|
752
|
+
* channel, as on {@link ConfigLoadError}.
|
|
698
753
|
*
|
|
699
754
|
* @public
|
|
700
755
|
*/
|
|
701
|
-
type ConfigSaveError = ConfigWriteError | ConfigDefaultPathMissingError;
|
|
756
|
+
type ConfigSaveError<CE = ConfigCodecError> = ConfigWriteError<CE> | ConfigDefaultPathMissingError;
|
|
702
757
|
/**
|
|
703
|
-
* The failure modes of {@link ConfigFileShape.update}, which loads and then
|
|
758
|
+
* The failure modes of {@link ConfigFileShape.update}, which loads and then
|
|
759
|
+
* saves. `CE` is the codec's error channel, as on {@link ConfigLoadError}.
|
|
704
760
|
*
|
|
705
761
|
* @public
|
|
706
762
|
*/
|
|
707
|
-
type ConfigUpdateError = ConfigLoadError | ConfigFileWriteError | ConfigDefaultPathMissingError;
|
|
763
|
+
type ConfigUpdateError<CE = ConfigCodecError> = ConfigLoadError<CE> | ConfigFileWriteError | ConfigDefaultPathMissingError;
|
|
708
764
|
/**
|
|
709
|
-
* The config file service, generic over the decoded config type `A
|
|
765
|
+
* The config file service, generic over the decoded config type `A` and the
|
|
766
|
+
* codec's error channel `CE`.
|
|
710
767
|
*
|
|
711
768
|
* @remarks
|
|
712
769
|
* Error unions are narrowed per method: `loadOrDefault` cannot fail with
|
|
713
770
|
* {@link ConfigFileNotFoundError} because that is the branch it handles, and
|
|
714
771
|
* `discover` treats an empty result as success.
|
|
715
772
|
*
|
|
773
|
+
* `CE` defaults to {@link ConfigCodecError}, so a service over a built-in
|
|
774
|
+
* codec never names it. A service over a decorated codec —
|
|
775
|
+
* `EncryptedCodec(...)`, `ConfigMigration.make(...)` — names the decorator's
|
|
776
|
+
* widened error here, and every codec-facing method carries it.
|
|
777
|
+
*
|
|
716
778
|
* @public
|
|
717
779
|
*/
|
|
718
|
-
interface ConfigFileShape<A> {
|
|
780
|
+
interface ConfigFileShape<A, CE = ConfigCodecError> {
|
|
719
781
|
/** Discover, decode and merge the highest-priority config source. */
|
|
720
|
-
readonly load: Effect.Effect<A, ConfigLoadError
|
|
782
|
+
readonly load: Effect.Effect<A, ConfigLoadError<CE>>;
|
|
721
783
|
/** Read, decode and validate one explicit path. */
|
|
722
|
-
readonly loadFrom: (path: string) => Effect.Effect<A, ConfigReadError
|
|
784
|
+
readonly loadFrom: (path: string) => Effect.Effect<A, ConfigReadError<CE>>;
|
|
723
785
|
/**
|
|
724
786
|
* Every source the resolver chain found, in priority order. Empty is success.
|
|
725
787
|
*
|
|
@@ -728,7 +790,7 @@ interface ConfigFileShape<A> {
|
|
|
728
790
|
* being silently skipped: silently skipping a corrupt file would mean running
|
|
729
791
|
* on the wrong config. This is deliberate.
|
|
730
792
|
*/
|
|
731
|
-
readonly discover: Effect.Effect<ReadonlyArray<ConfigSource<A>>, ConfigReadError
|
|
793
|
+
readonly discover: Effect.Effect<ReadonlyArray<ConfigSource<A>>, ConfigReadError<CE>>;
|
|
732
794
|
/**
|
|
733
795
|
* Like {@link ConfigFileShape.load}, but yields `defaultValue` when nothing is found.
|
|
734
796
|
*
|
|
@@ -736,7 +798,7 @@ interface ConfigFileShape<A> {
|
|
|
736
798
|
* `defaultValue` is returned as-is: neither the schema nor `options.validate`
|
|
737
799
|
* is applied to it. It is trusted caller input, not a discovered document.
|
|
738
800
|
*/
|
|
739
|
-
readonly loadOrDefault: (defaultValue: A) => Effect.Effect<A, ConfigReadError
|
|
801
|
+
readonly loadOrDefault: (defaultValue: A) => Effect.Effect<A, ConfigReadError<CE>>;
|
|
740
802
|
/** Decode and validate an in-memory value. */
|
|
741
803
|
readonly validate: (value: unknown) => Effect.Effect<A, ConfigValidationError>;
|
|
742
804
|
/**
|
|
@@ -749,7 +811,7 @@ interface ConfigFileShape<A> {
|
|
|
749
811
|
* `path`, and a validation failure's `path` is `Option.none()`: there is no
|
|
750
812
|
* file to name.
|
|
751
813
|
*/
|
|
752
|
-
readonly encode: (value: A, options?: ConfigEncodeOptions) => Effect.Effect<string, ConfigEncodeError
|
|
814
|
+
readonly encode: (value: A, options?: ConfigEncodeOptions) => Effect.Effect<string, ConfigEncodeError<CE>>;
|
|
753
815
|
/**
|
|
754
816
|
* Encode `value` and write it to an explicit `path`.
|
|
755
817
|
*
|
|
@@ -759,12 +821,12 @@ interface ConfigFileShape<A> {
|
|
|
759
821
|
* `write` targets a path the caller already vouched for. `options` are the
|
|
760
822
|
* same as {@link ConfigFileShape.encode}'s, so the two stay in lockstep.
|
|
761
823
|
*/
|
|
762
|
-
readonly write: (value: A, path: string, options?: ConfigEncodeOptions) => Effect.Effect<void, ConfigWriteError
|
|
824
|
+
readonly write: (value: A, path: string, options?: ConfigEncodeOptions) => Effect.Effect<void, ConfigWriteError<CE>>;
|
|
763
825
|
/**
|
|
764
826
|
* Resolve `defaultPath`, `mkdir -p` its parent, encode `value` into it, and
|
|
765
827
|
* return the path written.
|
|
766
828
|
*/
|
|
767
|
-
readonly save: (value: A) => Effect.Effect<string, ConfigSaveError
|
|
829
|
+
readonly save: (value: A) => Effect.Effect<string, ConfigSaveError<CE>>;
|
|
768
830
|
/**
|
|
769
831
|
* Load the current value, apply `fn`, {@link ConfigFileShape.save} the result
|
|
770
832
|
* and return it.
|
|
@@ -773,7 +835,7 @@ interface ConfigFileShape<A> {
|
|
|
773
835
|
* With `defaultValue` the load cannot fail with
|
|
774
836
|
* {@link ConfigFileNotFoundError}; without it, it can.
|
|
775
837
|
*/
|
|
776
|
-
readonly update: (fn: (current: A) => A, defaultValue?: A) => Effect.Effect<A, ConfigUpdateError
|
|
838
|
+
readonly update: (fn: (current: A) => A, defaultValue?: A) => Effect.Effect<A, ConfigUpdateError<CE>>;
|
|
777
839
|
}
|
|
778
840
|
/**
|
|
779
841
|
* Options for {@link ConfigFile.layer}.
|
|
@@ -782,9 +844,13 @@ interface ConfigFileShape<A> {
|
|
|
782
844
|
* `RR` is the union of the resolvers' requirements. It flows into the layer's
|
|
783
845
|
* `R` rather than being cast away.
|
|
784
846
|
*
|
|
847
|
+
* `CE` is the codec's error channel, inferred from `codec`. It defaults to
|
|
848
|
+
* {@link ConfigCodecError}; a decorated codec widens it, and the service's
|
|
849
|
+
* error unions widen with it.
|
|
850
|
+
*
|
|
785
851
|
* @public
|
|
786
852
|
*/
|
|
787
|
-
interface ConfigFileOptions<A, I, RR> {
|
|
853
|
+
interface ConfigFileOptions<A, I, RR, CE = ConfigCodecError> {
|
|
788
854
|
/**
|
|
789
855
|
* The schema every discovered document is decoded through.
|
|
790
856
|
*
|
|
@@ -795,8 +861,15 @@ interface ConfigFileOptions<A, I, RR> {
|
|
|
795
861
|
* requirements.
|
|
796
862
|
*/
|
|
797
863
|
readonly schema: Schema.Codec<A, I>;
|
|
798
|
-
/**
|
|
799
|
-
|
|
864
|
+
/**
|
|
865
|
+
* How file content becomes an unknown document, and back.
|
|
866
|
+
*
|
|
867
|
+
* @remarks
|
|
868
|
+
* Any `ConfigCodec<CE>`, decorated ones included: an
|
|
869
|
+
* `EncryptedCodec(JsonCodec, key)` or a `ConfigMigration.make(...)` codec
|
|
870
|
+
* goes here directly.
|
|
871
|
+
*/
|
|
872
|
+
readonly codec: ConfigCodec<CE>;
|
|
800
873
|
/** The resolver chain, in priority order. */
|
|
801
874
|
readonly resolvers: ReadonlyArray<ConfigResolver<RR>>;
|
|
802
875
|
/** How several discovered sources become one value. */
|
|
@@ -863,13 +936,21 @@ interface ConfigFileOptions<A, I, RR> {
|
|
|
863
936
|
* {@link ConfigDefaultPathMissingError} under this layer — the honest answer.
|
|
864
937
|
* Exercise the write path with {@link ConfigFile.layer} instead.
|
|
865
938
|
*
|
|
939
|
+
* `CE` is the codec's error channel, as on {@link ConfigFileOptions}.
|
|
940
|
+
*
|
|
866
941
|
* @public
|
|
867
942
|
*/
|
|
868
|
-
interface ConfigFileTestOptions<A, I> {
|
|
943
|
+
interface ConfigFileTestOptions<A, I, CE = ConfigCodecError> {
|
|
869
944
|
/** The schema every seeded document is decoded through. */
|
|
870
945
|
readonly schema: Schema.Codec<A, I>;
|
|
871
|
-
/**
|
|
872
|
-
|
|
946
|
+
/**
|
|
947
|
+
* How file content becomes an unknown document, and back.
|
|
948
|
+
*
|
|
949
|
+
* @remarks
|
|
950
|
+
* Any `ConfigCodec<CE>`, decorated ones included, exactly as on
|
|
951
|
+
* {@link ConfigFileOptions.codec}.
|
|
952
|
+
*/
|
|
953
|
+
readonly codec: ConfigCodec<CE>;
|
|
873
954
|
/** How several discovered sources become one value. */
|
|
874
955
|
readonly strategy: MergeStrategy<A>;
|
|
875
956
|
/**
|
|
@@ -886,9 +967,13 @@ interface ConfigFileTestOptions<A, I> {
|
|
|
886
967
|
/**
|
|
887
968
|
* Options for {@link ConfigFile.read}.
|
|
888
969
|
*
|
|
970
|
+
* @remarks
|
|
971
|
+
* `CE` is the codec's error channel, inferred from `codec`, as on
|
|
972
|
+
* {@link ConfigFileOptions}.
|
|
973
|
+
*
|
|
889
974
|
* @public
|
|
890
975
|
*/
|
|
891
|
-
interface ConfigReadOptions<A, I> {
|
|
976
|
+
interface ConfigReadOptions<A, I, CE = ConfigCodecError> {
|
|
892
977
|
/** The schema the document is decoded through. */
|
|
893
978
|
readonly schema: Schema.Codec<A, I>;
|
|
894
979
|
/**
|
|
@@ -900,8 +985,11 @@ interface ConfigReadOptions<A, I> {
|
|
|
900
985
|
* free-standing-codec tree-shaking guarantee: a consumer that only ever
|
|
901
986
|
* passes `JsonCodec` never references the JSONC, YAML or TOML modules, so
|
|
902
987
|
* their engines stay out of the bundle.
|
|
988
|
+
*
|
|
989
|
+
* A decorated codec (`EncryptedCodec`, `ConfigMigration.make`) is accepted,
|
|
990
|
+
* and `read`'s error channel widens to its error.
|
|
903
991
|
*/
|
|
904
|
-
readonly codec: ConfigCodec
|
|
992
|
+
readonly codec: ConfigCodec<CE>;
|
|
905
993
|
/**
|
|
906
994
|
* Parse options for the decode, chiefly `onExcessProperty`.
|
|
907
995
|
*
|
|
@@ -922,12 +1010,26 @@ export declare class ConfigFile {
|
|
|
922
1010
|
/**
|
|
923
1011
|
* Create a uniquely-keyed service class for one config schema.
|
|
924
1012
|
*
|
|
1013
|
+
* @remarks
|
|
1014
|
+
* The optional third type argument is the codec's error channel. Omit it
|
|
1015
|
+
* for the built-in codecs. A service over a decorated codec names the
|
|
1016
|
+
* decorator's widened error, because the key is declared before the codec
|
|
1017
|
+
* is chosen and cannot infer it: wiring `EncryptedCodec(JsonCodec, key)`
|
|
1018
|
+
* into a key declared without it is a compile error, since that service's
|
|
1019
|
+
* methods would fail with a `ConfigEncryptionError` its type never mentions.
|
|
1020
|
+
*
|
|
925
1021
|
* @example
|
|
926
1022
|
* ```ts
|
|
927
1023
|
* class AppConfig extends ConfigFile.Service<AppConfig, AppShape>()("app/Config") {}
|
|
1024
|
+
*
|
|
1025
|
+
* class Secrets extends ConfigFile.Service<
|
|
1026
|
+
* Secrets,
|
|
1027
|
+
* SecretsShape,
|
|
1028
|
+
* ConfigCodecError | ConfigEncryptionError
|
|
1029
|
+
* >()("app/Secrets") {}
|
|
928
1030
|
* ```
|
|
929
1031
|
*/
|
|
930
|
-
static readonly Service: <Self, A>() => <const Id extends string>(id: Id) => Context.ServiceClass<Self, Id, ConfigFileShape<A>>;
|
|
1032
|
+
static readonly Service: <Self, A, CE = ConfigCodecError>() => <const Id extends string>(id: Id) => Context.ServiceClass<Self, Id, ConfigFileShape<A, CE>>;
|
|
931
1033
|
/**
|
|
932
1034
|
* Build the live layer for a config service class.
|
|
933
1035
|
*
|
|
@@ -965,7 +1067,7 @@ export declare class ConfigFile {
|
|
|
965
1067
|
* });
|
|
966
1068
|
* ```
|
|
967
1069
|
*/
|
|
968
|
-
static readonly layer: <Self, A, I, RR = never, S extends ConfigFileShape<A> = ConfigFileShape<A>>(tag: Context.Key<Self, S> & Context.Key<Self, ConfigFileShape<A>> & ([ConfigFileShape<NoInfer<A>>] extends [S] ? unknown : never), options: ConfigFileOptions<A, I, RR>) => Layer.Layer<Self, never, FileSystem.FileSystem | Path.Path | RR>;
|
|
1070
|
+
static readonly layer: <Self, A, I, RR = never, CE = ConfigCodecError, S extends ConfigFileShape<A, CE> = ConfigFileShape<A, CE>>(tag: Context.Key<Self, S> & Context.Key<Self, ConfigFileShape<A, CE>> & ([ConfigFileShape<NoInfer<A>, NoInfer<CE>>] extends [S] ? unknown : never), options: ConfigFileOptions<A, I, RR, CE>) => Layer.Layer<Self, never, FileSystem.FileSystem | Path.Path | RR>;
|
|
969
1071
|
/**
|
|
970
1072
|
* A scoped layer that seeds `files` into a temp directory, wires the **real**
|
|
971
1073
|
* live implementation over them, and removes the directory when the scope
|
|
@@ -999,7 +1101,7 @@ export declare class ConfigFile {
|
|
|
999
1101
|
* }).pipe(Layer.provide(NodeServices.layer));
|
|
1000
1102
|
* ```
|
|
1001
1103
|
*/
|
|
1002
|
-
static readonly testLayer: <Self, A, I, S extends ConfigFileShape<A> = ConfigFileShape<A>>(tag: Context.Key<Self, S> & Context.Key<Self, ConfigFileShape<A>> & ([ConfigFileShape<NoInfer<A>>] extends [S] ? unknown : never), options: ConfigFileTestOptions<A, I>) => Layer.Layer<Self, never, FileSystem.FileSystem | Path.Path>;
|
|
1104
|
+
static readonly testLayer: <Self, A, I, CE = ConfigCodecError, S extends ConfigFileShape<A, CE> = ConfigFileShape<A, CE>>(tag: Context.Key<Self, S> & Context.Key<Self, ConfigFileShape<A, CE>> & ([ConfigFileShape<NoInfer<A>, NoInfer<CE>>] extends [S] ? unknown : never), options: ConfigFileTestOptions<A, I, CE>) => Layer.Layer<Self, never, FileSystem.FileSystem | Path.Path>;
|
|
1003
1105
|
/**
|
|
1004
1106
|
* Read, decode and validate one explicit path — no service, no layer, no tag.
|
|
1005
1107
|
*
|
|
@@ -1037,7 +1139,7 @@ export declare class ConfigFile {
|
|
|
1037
1139
|
* });
|
|
1038
1140
|
* ```
|
|
1039
1141
|
*/
|
|
1040
|
-
static readonly read: <A, I>(path: string, options: ConfigReadOptions<A, I>) => Effect.Effect<A, ConfigReadError
|
|
1142
|
+
static readonly read: <A, I, CE = ConfigCodecError>(path: string, options: ConfigReadOptions<A, I, CE>) => Effect.Effect<A, ConfigReadError<CE>, FileSystem.FileSystem>;
|
|
1041
1143
|
}
|
|
1042
1144
|
declare const ConfigMigrationError_base: Schema.Class<ConfigMigrationError, Schema.TaggedStruct<"ConfigMigrationError", {
|
|
1043
1145
|
/** The target version of the step that failed. `0` when reading the version failed. */
|
|
@@ -1092,10 +1194,19 @@ export interface VersionAccess {
|
|
|
1092
1194
|
export declare const VersionAccess: {
|
|
1093
1195
|
readonly default: VersionAccess;
|
|
1094
1196
|
};
|
|
1095
|
-
/**
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1197
|
+
/**
|
|
1198
|
+
* Options for {@link ConfigMigration.make}.
|
|
1199
|
+
*
|
|
1200
|
+
* @remarks
|
|
1201
|
+
* `CE` is the wrapped codec's error channel, inferred from `codec`. It
|
|
1202
|
+
* defaults to `ConfigCodecError`; wrapping an already-decorated codec carries
|
|
1203
|
+
* its widened error through.
|
|
1204
|
+
*
|
|
1205
|
+
* @public
|
|
1206
|
+
*/
|
|
1207
|
+
interface ConfigMigrationOptions<CE = ConfigCodecError> {
|
|
1208
|
+
/** The codec being wrapped — any `ConfigCodec<CE>`, decorated ones included. */
|
|
1209
|
+
readonly codec: ConfigCodec<CE>;
|
|
1099
1210
|
/** The migration steps; they run in ascending `version` order. */
|
|
1100
1211
|
readonly migrations: ReadonlyArray<ConfigFileMigration>;
|
|
1101
1212
|
/** How the version is read and written; defaults to {@link (VersionAccess:variable).default}, a top-level `version` field. */
|
|
@@ -1111,7 +1222,11 @@ export declare class ConfigMigration {
|
|
|
1111
1222
|
* The returned codec's error channel **widens** to include
|
|
1112
1223
|
* {@link ConfigMigrationError} rather than flattening migration failures into
|
|
1113
1224
|
* the inner codec's error — the reason the {@link (ConfigCodec:interface)} seam is generic
|
|
1114
|
-
* in its error type.
|
|
1225
|
+
* in its error type. The inner codec's own error type is kept as-is, so
|
|
1226
|
+
* wrapping an `EncryptedCodec` yields
|
|
1227
|
+
* `ConfigCodec<ConfigCodecError | ConfigEncryptionError | ConfigMigrationError>`,
|
|
1228
|
+
* and the result goes straight into `ConfigFile.layer`, `testLayer` or
|
|
1229
|
+
* `read`.
|
|
1115
1230
|
*
|
|
1116
1231
|
* @example
|
|
1117
1232
|
* ```ts
|
|
@@ -1130,7 +1245,7 @@ export declare class ConfigMigration {
|
|
|
1130
1245
|
* });
|
|
1131
1246
|
* ```
|
|
1132
1247
|
*/
|
|
1133
|
-
static readonly make: (options: ConfigMigrationOptions) => ConfigCodec<
|
|
1248
|
+
static readonly make: <CE = ConfigCodecError>(options: ConfigMigrationOptions<CE>) => ConfigCodec<CE | ConfigMigrationError>;
|
|
1134
1249
|
}
|
|
1135
1250
|
/**
|
|
1136
1251
|
* Expose a loaded, merged, schema-validated document as a `ConfigProvider`,
|
|
@@ -1173,6 +1288,9 @@ export declare class ConfigMigration {
|
|
|
1173
1288
|
* Descent by own property is also why a prototype getter and `__proto__` are
|
|
1174
1289
|
* both unreachable through the returned provider.
|
|
1175
1290
|
*
|
|
1291
|
+
* `CE` is the service's codec error, so a service over a decorated codec
|
|
1292
|
+
* keeps its widened failure in this effect's error channel.
|
|
1293
|
+
*
|
|
1176
1294
|
* @example
|
|
1177
1295
|
* ```ts
|
|
1178
1296
|
* import { asConfigProvider } from "@effected/config-file";
|
|
@@ -1187,7 +1305,7 @@ export declare class ConfigMigration {
|
|
|
1187
1305
|
*
|
|
1188
1306
|
* @public
|
|
1189
1307
|
*/
|
|
1190
|
-
export declare const asConfigProvider: <A>(service: ConfigFileShape<A>) => Effect.Effect<ConfigProvider.ConfigProvider, ConfigLoadError
|
|
1308
|
+
export declare const asConfigProvider: <A, CE = ConfigCodecError>(service: ConfigFileShape<A, CE>) => Effect.Effect<ConfigProvider.ConfigProvider, ConfigLoadError<CE>>;
|
|
1191
1309
|
/**
|
|
1192
1310
|
* Options for {@link layerConfigProvider}.
|
|
1193
1311
|
*
|
|
@@ -1220,7 +1338,8 @@ interface LayerConfigProviderOptions {
|
|
|
1220
1338
|
*
|
|
1221
1339
|
* The load happens when the layer is built, and a {@link ConfigFileNotFoundError}
|
|
1222
1340
|
* surfaces in the layer's error channel rather than degrading to an empty
|
|
1223
|
-
* provider — the same honesty {@link asConfigProvider} keeps.
|
|
1341
|
+
* provider — the same honesty {@link asConfigProvider} keeps. A service over a
|
|
1342
|
+
* decorated codec carries its widened codec error `CE` there too.
|
|
1224
1343
|
*
|
|
1225
1344
|
* @example
|
|
1226
1345
|
* ```ts
|
|
@@ -1229,7 +1348,7 @@ interface LayerConfigProviderOptions {
|
|
|
1229
1348
|
*
|
|
1230
1349
|
* @public
|
|
1231
1350
|
*/
|
|
1232
|
-
export declare const layerConfigProvider: <Self, A>(tag: Context.Key<Self, ConfigFileShape<A>>, options?: LayerConfigProviderOptions) => Layer.Layer<never, ConfigLoadError
|
|
1351
|
+
export declare const layerConfigProvider: <Self, A, CE = ConfigCodecError>(tag: Context.Key<Self, ConfigFileShape<A, CE>>, options?: LayerConfigProviderOptions) => Layer.Layer<never, ConfigLoadError<CE>, Self>;
|
|
1233
1352
|
declare const ConfigEncryptionError_base: Schema.Class<ConfigEncryptionError, Schema.TaggedStruct<"ConfigEncryptionError", {
|
|
1234
1353
|
/** Which cryptographic stage failed. */
|
|
1235
1354
|
readonly phase: Schema.Literals<readonly ["key-derivation", "encrypt", "decrypt", "encoding"]>;
|
|
@@ -1309,9 +1428,17 @@ export declare const EncryptedCodecKey: {
|
|
|
1309
1428
|
* the remainder is the ciphertext, and the plaintext is handed to the inner
|
|
1310
1429
|
* codec's `parse`.
|
|
1311
1430
|
*
|
|
1431
|
+
* The envelope is decoded with core's `effect/encoding/Base64`. Whitespace
|
|
1432
|
+
* around it (a trailing newline, a BOM) is tolerated; whitespace inside it, or
|
|
1433
|
+
* missing padding, fails with phase `"encoding"`. `stringify` never produces
|
|
1434
|
+
* either.
|
|
1435
|
+
*
|
|
1312
1436
|
* The error channel **widens** to `E | ConfigEncryptionError` rather than
|
|
1313
1437
|
* flattening — the inner codec's failures stay distinguishable from
|
|
1314
|
-
* cryptographic ones.
|
|
1438
|
+
* cryptographic ones. The result plugs straight into `ConfigFile.layer`,
|
|
1439
|
+
* `ConfigFile.testLayer` and `ConfigFile.read`, whose error unions widen with
|
|
1440
|
+
* it; a `ConfigFile.Service` key names the widened error as its third type
|
|
1441
|
+
* argument.
|
|
1315
1442
|
*
|
|
1316
1443
|
* @example
|
|
1317
1444
|
* ```ts
|
package/internal/crypto.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
+
import * as Base64 from "effect/encoding/Base64";
|
|
2
3
|
|
|
3
4
|
// src/internal/crypto.ts
|
|
4
5
|
/**
|
|
5
|
-
* AES-GCM primitives over WebCrypto
|
|
6
|
+
* AES-GCM primitives over WebCrypto, with base64 from core's
|
|
7
|
+
* `effect/encoding/Base64`.
|
|
6
8
|
*
|
|
7
9
|
* @remarks
|
|
8
10
|
* This module imports nothing from the rest of the package. `EncryptedCodec.ts`
|
|
@@ -59,39 +61,57 @@ const deriveKey = (passphrase, salt) => Effect.tryPromise({
|
|
|
59
61
|
catch: fail("key-derivation")
|
|
60
62
|
});
|
|
61
63
|
/**
|
|
62
|
-
* Decode base64 into bytes.
|
|
64
|
+
* Decode the base64 envelope into bytes.
|
|
63
65
|
*
|
|
64
66
|
* @remarks
|
|
65
|
-
*
|
|
67
|
+
* Delegates to core's `effect/encoding/Base64` rather than hand-rolling
|
|
68
|
+
* `atob` and a char-code loop. Core is stricter than `atob`: it strips only
|
|
69
|
+
* CR and LF, so an embedded space or tab, or missing padding, fails here with
|
|
70
|
+
* phase `"encoding"`. The text is trimmed first, so whitespace AROUND the
|
|
71
|
+
* envelope — the realistic residue of a hand edit or an editor's trailing
|
|
72
|
+
* newline — is still tolerated, and `trim` also drops a leading BOM or NBSP,
|
|
73
|
+
* which `atob` rejected. `EncryptedCodec` itself always writes padded,
|
|
74
|
+
* whitespace-free output, so being strict inside the text costs nothing.
|
|
75
|
+
*
|
|
76
|
+
* The decoded view is backed by a plain `ArrayBuffer`; callers that hand a
|
|
77
|
+
* slice of it to WebCrypto get an `ArrayBuffer`-backed view from `slice`.
|
|
66
78
|
*
|
|
67
79
|
* @internal
|
|
68
80
|
*/
|
|
69
|
-
const fromBase64 = (raw) => Effect.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
const fromBase64 = (raw) => Effect.mapError(Effect.fromResult(Base64.decode(raw.trim())), (error) => fail("encoding")(withoutInput(error)));
|
|
82
|
+
/**
|
|
83
|
+
* Rebuild core's `EncodingError` as a plain `Error` that keeps only its message.
|
|
84
|
+
*
|
|
85
|
+
* @remarks
|
|
86
|
+
* Core's `EncodingError` stores the whole stripped input as `input`, and the
|
|
87
|
+
* public `ConfigEncryptionError` keeps its cause by identity. Passed through
|
|
88
|
+
* as is, a PLAINTEXT config handed to an encrypted codec (a file that was
|
|
89
|
+
* never encrypted, or a resolver pointing at the wrong file) would carry
|
|
90
|
+
* every secret in it into the typed error, the `ParseFailed` event payload,
|
|
91
|
+
* and any log that inspects or serializes the error. Core's messages name at
|
|
92
|
+
* most one offending character, never the input.
|
|
93
|
+
*/
|
|
94
|
+
const withoutInput = (error) => {
|
|
95
|
+
const cause = new Error(error.message);
|
|
96
|
+
cause.name = error._tag;
|
|
97
|
+
return cause;
|
|
98
|
+
};
|
|
79
99
|
/**
|
|
80
100
|
* Prepend the IV to the ciphertext and base64-encode the envelope.
|
|
81
101
|
*
|
|
102
|
+
* @remarks
|
|
103
|
+
* Synchronous and total: encoding bytes to base64 cannot fail, so there is no
|
|
104
|
+
* error channel to carry.
|
|
105
|
+
*
|
|
82
106
|
* @internal
|
|
83
107
|
*/
|
|
84
|
-
const toBase64 = (iv, ciphertext) =>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return btoa(Array.from(result, (b) => String.fromCharCode(b)).join(""));
|
|
92
|
-
},
|
|
93
|
-
catch: fail("encoding")
|
|
94
|
-
});
|
|
108
|
+
const toBase64 = (iv, ciphertext) => {
|
|
109
|
+
const ciphertextBytes = new Uint8Array(ciphertext);
|
|
110
|
+
const result = new Uint8Array(12 + ciphertextBytes.length);
|
|
111
|
+
result.set(iv, 0);
|
|
112
|
+
result.set(ciphertextBytes, 12);
|
|
113
|
+
return Base64.encode(result);
|
|
114
|
+
};
|
|
95
115
|
/** Decrypt AES-GCM ciphertext under `iv`. @internal */
|
|
96
116
|
const decrypt = (key, iv, ciphertext) => Effect.tryPromise({
|
|
97
117
|
try: () => globalThis.crypto.subtle.decrypt({
|
|
@@ -108,7 +128,28 @@ const encrypt = (key, iv, plaintext) => Effect.tryPromise({
|
|
|
108
128
|
}, key, plaintext),
|
|
109
129
|
catch: fail("encrypt")
|
|
110
130
|
});
|
|
111
|
-
/**
|
|
112
|
-
|
|
131
|
+
/**
|
|
132
|
+
* A fresh cryptographically random 12-byte IV, drawn from WebCrypto.
|
|
133
|
+
*
|
|
134
|
+
* @remarks
|
|
135
|
+
* Deliberately NOT core's `Crypto.randomBytes` in `R`, which is otherwise the
|
|
136
|
+
* kit's rule for any contract core declares. The IV is an AES-GCM nonce:
|
|
137
|
+
* repeating one under the same key destroys GCM's confidentiality and
|
|
138
|
+
* integrity. A swappable random source makes that a wiring accident away —
|
|
139
|
+
* `Crypto.make`'s own documentation example returns zero-filled bytes, and a
|
|
140
|
+
* seeded `TestCrypto` layer that reached production would repeat IVs. The
|
|
141
|
+
* `ConfigCodec` seam also has no `R` channel to carry the service. This is a
|
|
142
|
+
* recorded, security-motivated exception, not a precedent for other
|
|
143
|
+
* randomness.
|
|
144
|
+
*
|
|
145
|
+
* A runtime without `globalThis.crypto` fails typed with phase `"encrypt"`,
|
|
146
|
+
* like the neighbouring WebCrypto steps, rather than escaping as a defect.
|
|
147
|
+
*
|
|
148
|
+
* @internal
|
|
149
|
+
*/
|
|
150
|
+
const randomIv = Effect.try({
|
|
151
|
+
try: () => globalThis.crypto.getRandomValues(new Uint8Array(/* @__PURE__ */ new ArrayBuffer(12))),
|
|
152
|
+
catch: fail("encrypt")
|
|
153
|
+
});
|
|
113
154
|
|
|
114
155
|
export { decrypt, deriveKey, encrypt, fromBase64, randomIv, toArrayBufferView, toBase64 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/config-file",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Composable config file loading for Effect: JSON, JSONC, YAML and TOML codecs, resolution strategies, and merge behaviors.",
|
|
6
6
|
"keywords": [
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@effected/glob": "^0.14.0",
|
|
47
|
-
"@effected/jsonc": "^0.
|
|
48
|
-
"@effected/toml": "^0.
|
|
47
|
+
"@effected/jsonc": "^0.20.0",
|
|
48
|
+
"@effected/toml": "^0.16.0",
|
|
49
49
|
"@effected/walker": "^0.19.0",
|
|
50
|
-
"@effected/yaml": "^0.
|
|
50
|
+
"@effected/yaml": "^0.25.0",
|
|
51
51
|
"effect": "^4.0.0"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|