@effected/config-file 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ConfigCodec.js +1 -2
- package/ConfigEvent.js +8 -2
- package/ConfigFile.js +25 -7
- package/ConfigMigration.js +6 -3
- package/ConfigProvider.js +6 -3
- package/ConfigResolver.js +15 -9
- package/EncryptedCodec.js +12 -5
- package/JsonCodec.js +1 -2
- package/JsoncCodec.js +1 -2
- package/MergeStrategy.js +1 -2
- package/README.md +28 -3
- package/TomlCodec.js +1 -2
- package/YamlCodec.js +1 -2
- package/index.d.ts +131 -65
- package/internal/crypto.js +68 -28
- package/internal/deepMerge.js +1 -2
- package/package.json +6 -6
package/ConfigCodec.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// src/ConfigCodec.ts
|
|
4
4
|
/**
|
|
5
5
|
* Indicates that a codec failed to parse or stringify configuration content.
|
|
6
6
|
*
|
|
@@ -43,5 +43,4 @@ var ConfigCodecError = class extends Schema.TaggedError()("ConfigCodecError", {
|
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
//#endregion
|
|
47
46
|
export { ConfigCodecError };
|
package/ConfigEvent.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Effect, Layer, PubSub, Schema } from "effect";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// src/ConfigEvent.ts
|
|
4
4
|
/**
|
|
5
5
|
* A reference to one configuration source that contributed to a load.
|
|
6
6
|
*
|
|
@@ -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([
|
|
@@ -114,5 +121,4 @@ var ConfigEvents = class ConfigEvents extends Context.Service()("@effected/confi
|
|
|
114
121
|
}));
|
|
115
122
|
};
|
|
116
123
|
|
|
117
|
-
//#endregion
|
|
118
124
|
export { ConfigEvent, ConfigEventPayload, ConfigEvents, ConfigSourceRef };
|
package/ConfigFile.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ConfigEvent } from "./ConfigEvent.js";
|
|
|
3
3
|
import { ConfigResolver } from "./ConfigResolver.js";
|
|
4
4
|
import { Context, DateTime, Effect, FileSystem, Layer, Option, Path, PubSub, Schema, Semaphore } from "effect";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// src/ConfigFile.ts
|
|
7
7
|
/**
|
|
8
8
|
* Indicates that the resolver chain produced no configuration source.
|
|
9
9
|
*
|
|
@@ -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;
|
|
@@ -565,5 +584,4 @@ var ConfigFile = class {
|
|
|
565
584
|
static read = read;
|
|
566
585
|
};
|
|
567
586
|
|
|
568
|
-
//#endregion
|
|
569
587
|
export { ConfigDefaultPathMissingError, ConfigFile, ConfigFileNotFoundError, ConfigFileReadError, ConfigFileWriteError, ConfigValidationError };
|
package/ConfigMigration.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// src/ConfigMigration.ts
|
|
4
4
|
/**
|
|
5
5
|
* Indicates that a versioned config migration failed.
|
|
6
6
|
*
|
|
@@ -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
|
|
@@ -110,5 +114,4 @@ var ConfigMigration = class {
|
|
|
110
114
|
static make = make;
|
|
111
115
|
};
|
|
112
116
|
|
|
113
|
-
//#endregion
|
|
114
117
|
export { ConfigMigration, ConfigMigrationError, VersionAccess };
|
package/ConfigProvider.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConfigProvider, Effect } from "effect";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// src/ConfigProvider.ts
|
|
4
4
|
/**
|
|
5
5
|
* Expose a loaded, merged, schema-validated document as a `ConfigProvider`,
|
|
6
6
|
* so it can be read through `Config.String("port")` and layered beneath other
|
|
@@ -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
|
|
@@ -87,5 +91,4 @@ const layerConfigProvider = (tag, options) => {
|
|
|
87
91
|
return ConfigProvider.layerAdd(provider, { asPrimary: options?.asPrimary });
|
|
88
92
|
};
|
|
89
93
|
|
|
90
|
-
//#endregion
|
|
91
94
|
export { asConfigProvider, layerConfigProvider };
|
package/ConfigResolver.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
|
|
5
5
|
/**
|
|
6
6
|
* Build a resolver from a probe-reporting lookup, deriving `resolveMatch` and
|
|
7
7
|
* `resolve` from it.
|
|
@@ -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);
|
|
@@ -185,5 +192,4 @@ var ConfigResolver = class {
|
|
|
185
192
|
static systemEtc = systemEtc;
|
|
186
193
|
};
|
|
187
194
|
|
|
188
|
-
//#endregion
|
|
189
195
|
export { ConfigResolver };
|
package/EncryptedCodec.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decrypt, deriveKey, encrypt, fromBase64, randomIv, toBase64 } from "./internal/crypto.js";
|
|
2
2
|
import { Duration, Effect, Exit, Schema } from "effect";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// src/EncryptedCodec.ts
|
|
5
5
|
/**
|
|
6
6
|
* Indicates that an encryption, decryption, key-derivation or base64 step
|
|
7
7
|
* failed.
|
|
@@ -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
|
|
@@ -119,12 +127,11 @@ function EncryptedCodec(inner, keySource) {
|
|
|
119
127
|
const key = yield* getKey;
|
|
120
128
|
const serialized = yield* inner.stringify(value);
|
|
121
129
|
const encoded = new TextEncoder().encode(serialized);
|
|
122
|
-
const iv = randomIv
|
|
130
|
+
const iv = yield* Effect.mapError(randomIv, toPublic);
|
|
123
131
|
const ciphertext = yield* Effect.mapError(encrypt(key, iv, encoded), toPublic);
|
|
124
|
-
return
|
|
132
|
+
return toBase64(iv, ciphertext);
|
|
125
133
|
})
|
|
126
134
|
};
|
|
127
135
|
}
|
|
128
136
|
|
|
129
|
-
//#endregion
|
|
130
137
|
export { ConfigEncryptionError, EncryptedCodec, EncryptedCodecKey };
|
package/JsonCodec.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ConfigCodecError } from "./ConfigCodec.js";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// src/JsonCodec.ts
|
|
5
5
|
/**
|
|
6
6
|
* A `ConfigCodec` backed by the host `JSON` global: plain JSON as
|
|
7
7
|
* configuration file content.
|
|
@@ -34,5 +34,4 @@ const JsonCodec = {
|
|
|
34
34
|
})
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
//#endregion
|
|
38
37
|
export { JsonCodec };
|
package/JsoncCodec.js
CHANGED
|
@@ -2,7 +2,7 @@ import { ConfigCodecError } from "./ConfigCodec.js";
|
|
|
2
2
|
import { Effect } from "effect";
|
|
3
3
|
import { Jsonc } from "@effected/jsonc";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// src/JsoncCodec.ts
|
|
6
6
|
/**
|
|
7
7
|
* A `ConfigCodec` backed by `@effected/jsonc`: JSON with comments and
|
|
8
8
|
* trailing commas.
|
|
@@ -34,5 +34,4 @@ const JsoncCodec = {
|
|
|
34
34
|
})
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
//#endregion
|
|
38
37
|
export { JsoncCodec };
|
package/MergeStrategy.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { canMerge, deepMerge } from "./internal/deepMerge.js";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// src/MergeStrategy.ts
|
|
5
5
|
const firstMatch = () => ({
|
|
6
6
|
name: "first-match",
|
|
7
7
|
resolve: (sources) => Effect.succeed(sources[0].value)
|
|
@@ -57,5 +57,4 @@ const MergeStrategy = {
|
|
|
57
57
|
layeredMerge
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
//#endregion
|
|
61
60
|
export { MergeStrategy };
|
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 { ConfigCodecError, ConfigEncryptionError, ConfigMigrationError } 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,24 @@ 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
|
+
type SecretError = ConfigCodecError | ConfigMigrationError | ConfigEncryptionError;
|
|
221
|
+
|
|
222
|
+
class SecretSettings extends ConfigFile.Service<SecretSettings, typeof Settings.Type, SecretError>()(
|
|
223
|
+
"app/SecretSettings",
|
|
224
|
+
) {}
|
|
225
|
+
|
|
226
|
+
export const SecretSettingsLive = ConfigFile.layer(SecretSettings, {
|
|
227
|
+
schema: Settings,
|
|
228
|
+
codec: secret,
|
|
229
|
+
resolvers: [ConfigResolver.explicitPath("./settings.enc")],
|
|
230
|
+
strategy: MergeStrategy.firstMatch(),
|
|
231
|
+
});
|
|
209
232
|
```
|
|
210
233
|
|
|
234
|
+
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"`.
|
|
235
|
+
|
|
211
236
|
## Features
|
|
212
237
|
|
|
213
238
|
- `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/TomlCodec.js
CHANGED
|
@@ -2,7 +2,7 @@ import { ConfigCodecError } from "./ConfigCodec.js";
|
|
|
2
2
|
import { Effect } from "effect";
|
|
3
3
|
import { Toml } from "@effected/toml";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// src/TomlCodec.ts
|
|
6
6
|
/**
|
|
7
7
|
* A `ConfigCodec` backed by `@effected/toml`.
|
|
8
8
|
*
|
|
@@ -35,5 +35,4 @@ const TomlCodec = {
|
|
|
35
35
|
})))
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
//#endregion
|
|
39
38
|
export { TomlCodec };
|
package/YamlCodec.js
CHANGED
|
@@ -2,7 +2,7 @@ import { ConfigCodecError } from "./ConfigCodec.js";
|
|
|
2
2
|
import { Effect } from "effect";
|
|
3
3
|
import { Yaml } from "@effected/yaml";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// src/YamlCodec.ts
|
|
6
6
|
/**
|
|
7
7
|
* A `ConfigCodec` backed by `@effected/yaml`.
|
|
8
8
|
*
|
|
@@ -30,5 +30,4 @@ const YamlCodec = {
|
|
|
30
30
|
})))
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
//#endregion
|
|
34
33
|
export { YamlCodec };
|
package/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ConfigProvider, Context, Effect, FileSystem, Layer, Option, Path, PubSub, Schema, SchemaAST } from "effect";
|
|
2
|
-
//#region src/ConfigCodec.d.ts
|
|
3
2
|
declare const ConfigCodecError_base: Schema.Class<ConfigCodecError, Schema.TaggedStruct<"ConfigCodecError", {
|
|
4
3
|
/** The codec that failed, e.g. `"json"`. */
|
|
5
4
|
readonly codec: Schema.String;
|
|
@@ -67,8 +66,6 @@ interface ConfigCodec<E = ConfigCodecError> {
|
|
|
67
66
|
/** Turn a document back into file content. */
|
|
68
67
|
readonly stringify: (value: unknown) => Effect.Effect<string, E>;
|
|
69
68
|
}
|
|
70
|
-
//#endregion
|
|
71
|
-
//#region src/ConfigEvent.d.ts
|
|
72
69
|
/**
|
|
73
70
|
* A reference to one configuration source that contributed to a load.
|
|
74
71
|
*
|
|
@@ -98,6 +95,13 @@ export declare const ConfigSourceRef: Schema.Struct<{
|
|
|
98
95
|
* `reason` string, so every field a subscriber might branch on survives; a
|
|
99
96
|
* subscriber that wants prose can read `error.message`.
|
|
100
97
|
*
|
|
98
|
+
* `error` is typed `unknown` (`Schema.Defect()`) on purpose. `ParseFailed` and
|
|
99
|
+
* `StringifyFailed` carry whatever the service's codec failed with, and under
|
|
100
|
+
* a decorated codec that is not always a `ConfigCodecError`: an
|
|
101
|
+
* `EncryptedCodec` failure arrives as `ConfigEncryptionError`, a migration
|
|
102
|
+
* failure as `ConfigMigrationError`. Narrow by `_tag` or `instanceof`, never
|
|
103
|
+
* by assuming the codec error.
|
|
104
|
+
*
|
|
101
105
|
* @public
|
|
102
106
|
*/
|
|
103
107
|
export declare const ConfigEventPayload: Schema.Union<readonly [Schema.TaggedStruct<"Discovered", {
|
|
@@ -243,8 +247,6 @@ export declare class ConfigEvents extends ConfigEvents_base {
|
|
|
243
247
|
*/
|
|
244
248
|
static readonly layer: Layer.Layer<ConfigEvents>;
|
|
245
249
|
}
|
|
246
|
-
//#endregion
|
|
247
|
-
//#region src/ConfigResolver.d.ts
|
|
248
250
|
/**
|
|
249
251
|
* A composable config file resolver: one lookup strategy.
|
|
250
252
|
*
|
|
@@ -474,8 +476,6 @@ export declare class ConfigResolver {
|
|
|
474
476
|
readonly dir?: string;
|
|
475
477
|
}) => ConfigResolver<FileSystem.FileSystem | Path.Path>;
|
|
476
478
|
}
|
|
477
|
-
//#endregion
|
|
478
|
-
//#region src/MergeStrategy.d.ts
|
|
479
479
|
/**
|
|
480
480
|
* A single configuration source discovered during a resolver-chain pass.
|
|
481
481
|
*
|
|
@@ -541,8 +541,6 @@ export declare const MergeStrategy: {
|
|
|
541
541
|
readonly firstMatch: <A>() => MergeStrategy<A>;
|
|
542
542
|
readonly layeredMerge: <A>() => MergeStrategy<A>;
|
|
543
543
|
};
|
|
544
|
-
//#endregion
|
|
545
|
-
//#region src/ConfigFile.d.ts
|
|
546
544
|
declare const ConfigFileNotFoundError_base: Schema.Class<ConfigFileNotFoundError, Schema.TaggedStruct<"ConfigFileNotFoundError", {
|
|
547
545
|
/** The names of the resolvers that were probed, in order. */
|
|
548
546
|
readonly searched: Schema.$Array<Schema.String>;
|
|
@@ -646,30 +644,39 @@ export declare class ConfigValidationError extends ConfigValidationError_base {
|
|
|
646
644
|
/**
|
|
647
645
|
* The failure modes of the full discovery-and-load path.
|
|
648
646
|
*
|
|
647
|
+
* @remarks
|
|
648
|
+
* `CE` is the codec's error channel. It defaults to {@link ConfigCodecError},
|
|
649
|
+
* the error of the four built-in codecs; a decorated codec widens it. Under
|
|
650
|
+
* `EncryptedCodec(JsonCodec, key)` it is
|
|
651
|
+
* `ConfigCodecError | ConfigEncryptionError`, and the union widens with it
|
|
652
|
+
* rather than flattening the decorator's failure into a codec error.
|
|
653
|
+
*
|
|
649
654
|
* @public
|
|
650
655
|
*/
|
|
651
|
-
type ConfigLoadError = ConfigFileNotFoundError | ConfigFileReadError |
|
|
656
|
+
type ConfigLoadError<CE = ConfigCodecError> = ConfigFileNotFoundError | ConfigFileReadError | CE | ConfigValidationError;
|
|
652
657
|
/**
|
|
653
658
|
* The failure modes of reading one known path.
|
|
654
659
|
*
|
|
655
660
|
* @remarks
|
|
656
661
|
* Deliberately excludes {@link ConfigFileNotFoundError}: every method typed with
|
|
657
662
|
* this union either takes an explicit path or treats "nothing found" as success.
|
|
663
|
+
* `CE` is the codec's error channel, as on {@link ConfigLoadError}.
|
|
658
664
|
*
|
|
659
665
|
* @public
|
|
660
666
|
*/
|
|
661
|
-
type ConfigReadError = ConfigFileReadError |
|
|
667
|
+
type ConfigReadError<CE = ConfigCodecError> = ConfigFileReadError | CE | ConfigValidationError;
|
|
662
668
|
/**
|
|
663
669
|
* The failure modes of encoding and writing one known path.
|
|
664
670
|
*
|
|
665
671
|
* @remarks
|
|
666
672
|
* Deliberately excludes {@link ConfigFileNotFoundError} — the path is explicit,
|
|
667
673
|
* so there is nothing to discover — and {@link ConfigDefaultPathMissingError},
|
|
668
|
-
* because no default path is consulted.
|
|
674
|
+
* because no default path is consulted. `CE` is the codec's error channel, as
|
|
675
|
+
* on {@link ConfigLoadError}.
|
|
669
676
|
*
|
|
670
677
|
* @public
|
|
671
678
|
*/
|
|
672
|
-
type ConfigWriteError = ConfigFileWriteError |
|
|
679
|
+
type ConfigWriteError<CE = ConfigCodecError> = ConfigFileWriteError | CE | ConfigValidationError;
|
|
673
680
|
/**
|
|
674
681
|
* The failure modes of {@link ConfigFileShape.encode}: everything on the write
|
|
675
682
|
* path except the write itself.
|
|
@@ -677,10 +684,11 @@ type ConfigWriteError = ConfigFileWriteError | ConfigCodecError | ConfigValidati
|
|
|
677
684
|
* @remarks
|
|
678
685
|
* Deliberately excludes {@link ConfigFileWriteError} — nothing touches the
|
|
679
686
|
* filesystem — so a `--dry-run` caller's error channel is honest about that.
|
|
687
|
+
* `CE` is the codec's error channel, as on {@link ConfigLoadError}.
|
|
680
688
|
*
|
|
681
689
|
* @public
|
|
682
690
|
*/
|
|
683
|
-
type ConfigEncodeError = ConfigCodecError | ConfigValidationError;
|
|
691
|
+
type ConfigEncodeError<CE = ConfigCodecError> = CE | ConfigValidationError;
|
|
684
692
|
/**
|
|
685
693
|
* Options shared by {@link ConfigFileShape.encode} and {@link ConfigFileShape.write}.
|
|
686
694
|
*
|
|
@@ -703,32 +711,40 @@ interface ConfigEncodeOptions {
|
|
|
703
711
|
readonly header?: string;
|
|
704
712
|
}
|
|
705
713
|
/**
|
|
706
|
-
* The failure modes of {@link ConfigFileShape.save}.
|
|
714
|
+
* The failure modes of {@link ConfigFileShape.save}. `CE` is the codec's error
|
|
715
|
+
* channel, as on {@link ConfigLoadError}.
|
|
707
716
|
*
|
|
708
717
|
* @public
|
|
709
718
|
*/
|
|
710
|
-
type ConfigSaveError = ConfigWriteError | ConfigDefaultPathMissingError;
|
|
719
|
+
type ConfigSaveError<CE = ConfigCodecError> = ConfigWriteError<CE> | ConfigDefaultPathMissingError;
|
|
711
720
|
/**
|
|
712
|
-
* The failure modes of {@link ConfigFileShape.update}, which loads and then
|
|
721
|
+
* The failure modes of {@link ConfigFileShape.update}, which loads and then
|
|
722
|
+
* saves. `CE` is the codec's error channel, as on {@link ConfigLoadError}.
|
|
713
723
|
*
|
|
714
724
|
* @public
|
|
715
725
|
*/
|
|
716
|
-
type ConfigUpdateError = ConfigLoadError | ConfigFileWriteError | ConfigDefaultPathMissingError;
|
|
726
|
+
type ConfigUpdateError<CE = ConfigCodecError> = ConfigLoadError<CE> | ConfigFileWriteError | ConfigDefaultPathMissingError;
|
|
717
727
|
/**
|
|
718
|
-
* The config file service, generic over the decoded config type `A
|
|
728
|
+
* The config file service, generic over the decoded config type `A` and the
|
|
729
|
+
* codec's error channel `CE`.
|
|
719
730
|
*
|
|
720
731
|
* @remarks
|
|
721
732
|
* Error unions are narrowed per method: `loadOrDefault` cannot fail with
|
|
722
733
|
* {@link ConfigFileNotFoundError} because that is the branch it handles, and
|
|
723
734
|
* `discover` treats an empty result as success.
|
|
724
735
|
*
|
|
736
|
+
* `CE` defaults to {@link ConfigCodecError}, so a service over a built-in
|
|
737
|
+
* codec never names it. A service over a decorated codec —
|
|
738
|
+
* `EncryptedCodec(...)`, `ConfigMigration.make(...)` — names the decorator's
|
|
739
|
+
* widened error here, and every codec-facing method carries it.
|
|
740
|
+
*
|
|
725
741
|
* @public
|
|
726
742
|
*/
|
|
727
|
-
interface ConfigFileShape<A> {
|
|
743
|
+
interface ConfigFileShape<A, CE = ConfigCodecError> {
|
|
728
744
|
/** Discover, decode and merge the highest-priority config source. */
|
|
729
|
-
readonly load: Effect.Effect<A, ConfigLoadError
|
|
745
|
+
readonly load: Effect.Effect<A, ConfigLoadError<CE>>;
|
|
730
746
|
/** Read, decode and validate one explicit path. */
|
|
731
|
-
readonly loadFrom: (path: string) => Effect.Effect<A, ConfigReadError
|
|
747
|
+
readonly loadFrom: (path: string) => Effect.Effect<A, ConfigReadError<CE>>;
|
|
732
748
|
/**
|
|
733
749
|
* Every source the resolver chain found, in priority order. Empty is success.
|
|
734
750
|
*
|
|
@@ -737,7 +753,7 @@ interface ConfigFileShape<A> {
|
|
|
737
753
|
* being silently skipped: silently skipping a corrupt file would mean running
|
|
738
754
|
* on the wrong config. This is deliberate.
|
|
739
755
|
*/
|
|
740
|
-
readonly discover: Effect.Effect<ReadonlyArray<ConfigSource<A>>, ConfigReadError
|
|
756
|
+
readonly discover: Effect.Effect<ReadonlyArray<ConfigSource<A>>, ConfigReadError<CE>>;
|
|
741
757
|
/**
|
|
742
758
|
* Like {@link ConfigFileShape.load}, but yields `defaultValue` when nothing is found.
|
|
743
759
|
*
|
|
@@ -745,7 +761,7 @@ interface ConfigFileShape<A> {
|
|
|
745
761
|
* `defaultValue` is returned as-is: neither the schema nor `options.validate`
|
|
746
762
|
* is applied to it. It is trusted caller input, not a discovered document.
|
|
747
763
|
*/
|
|
748
|
-
readonly loadOrDefault: (defaultValue: A) => Effect.Effect<A, ConfigReadError
|
|
764
|
+
readonly loadOrDefault: (defaultValue: A) => Effect.Effect<A, ConfigReadError<CE>>;
|
|
749
765
|
/** Decode and validate an in-memory value. */
|
|
750
766
|
readonly validate: (value: unknown) => Effect.Effect<A, ConfigValidationError>;
|
|
751
767
|
/**
|
|
@@ -758,7 +774,7 @@ interface ConfigFileShape<A> {
|
|
|
758
774
|
* `path`, and a validation failure's `path` is `Option.none()`: there is no
|
|
759
775
|
* file to name.
|
|
760
776
|
*/
|
|
761
|
-
readonly encode: (value: A, options?: ConfigEncodeOptions) => Effect.Effect<string, ConfigEncodeError
|
|
777
|
+
readonly encode: (value: A, options?: ConfigEncodeOptions) => Effect.Effect<string, ConfigEncodeError<CE>>;
|
|
762
778
|
/**
|
|
763
779
|
* Encode `value` and write it to an explicit `path`.
|
|
764
780
|
*
|
|
@@ -768,12 +784,12 @@ interface ConfigFileShape<A> {
|
|
|
768
784
|
* `write` targets a path the caller already vouched for. `options` are the
|
|
769
785
|
* same as {@link ConfigFileShape.encode}'s, so the two stay in lockstep.
|
|
770
786
|
*/
|
|
771
|
-
readonly write: (value: A, path: string, options?: ConfigEncodeOptions) => Effect.Effect<void, ConfigWriteError
|
|
787
|
+
readonly write: (value: A, path: string, options?: ConfigEncodeOptions) => Effect.Effect<void, ConfigWriteError<CE>>;
|
|
772
788
|
/**
|
|
773
789
|
* Resolve `defaultPath`, `mkdir -p` its parent, encode `value` into it, and
|
|
774
790
|
* return the path written.
|
|
775
791
|
*/
|
|
776
|
-
readonly save: (value: A) => Effect.Effect<string, ConfigSaveError
|
|
792
|
+
readonly save: (value: A) => Effect.Effect<string, ConfigSaveError<CE>>;
|
|
777
793
|
/**
|
|
778
794
|
* Load the current value, apply `fn`, {@link ConfigFileShape.save} the result
|
|
779
795
|
* and return it.
|
|
@@ -782,7 +798,7 @@ interface ConfigFileShape<A> {
|
|
|
782
798
|
* With `defaultValue` the load cannot fail with
|
|
783
799
|
* {@link ConfigFileNotFoundError}; without it, it can.
|
|
784
800
|
*/
|
|
785
|
-
readonly update: (fn: (current: A) => A, defaultValue?: A) => Effect.Effect<A, ConfigUpdateError
|
|
801
|
+
readonly update: (fn: (current: A) => A, defaultValue?: A) => Effect.Effect<A, ConfigUpdateError<CE>>;
|
|
786
802
|
}
|
|
787
803
|
/**
|
|
788
804
|
* Options for {@link ConfigFile.layer}.
|
|
@@ -791,9 +807,13 @@ interface ConfigFileShape<A> {
|
|
|
791
807
|
* `RR` is the union of the resolvers' requirements. It flows into the layer's
|
|
792
808
|
* `R` rather than being cast away.
|
|
793
809
|
*
|
|
810
|
+
* `CE` is the codec's error channel, inferred from `codec`. It defaults to
|
|
811
|
+
* {@link ConfigCodecError}; a decorated codec widens it, and the service's
|
|
812
|
+
* error unions widen with it.
|
|
813
|
+
*
|
|
794
814
|
* @public
|
|
795
815
|
*/
|
|
796
|
-
interface ConfigFileOptions<A, I, RR> {
|
|
816
|
+
interface ConfigFileOptions<A, I, RR, CE = ConfigCodecError> {
|
|
797
817
|
/**
|
|
798
818
|
* The schema every discovered document is decoded through.
|
|
799
819
|
*
|
|
@@ -804,8 +824,15 @@ interface ConfigFileOptions<A, I, RR> {
|
|
|
804
824
|
* requirements.
|
|
805
825
|
*/
|
|
806
826
|
readonly schema: Schema.Codec<A, I>;
|
|
807
|
-
/**
|
|
808
|
-
|
|
827
|
+
/**
|
|
828
|
+
* How file content becomes an unknown document, and back.
|
|
829
|
+
*
|
|
830
|
+
* @remarks
|
|
831
|
+
* Any `ConfigCodec<CE>`, decorated ones included: an
|
|
832
|
+
* `EncryptedCodec(JsonCodec, key)` or a `ConfigMigration.make(...)` codec
|
|
833
|
+
* goes here directly.
|
|
834
|
+
*/
|
|
835
|
+
readonly codec: ConfigCodec<CE>;
|
|
809
836
|
/** The resolver chain, in priority order. */
|
|
810
837
|
readonly resolvers: ReadonlyArray<ConfigResolver<RR>>;
|
|
811
838
|
/** How several discovered sources become one value. */
|
|
@@ -872,13 +899,21 @@ interface ConfigFileOptions<A, I, RR> {
|
|
|
872
899
|
* {@link ConfigDefaultPathMissingError} under this layer — the honest answer.
|
|
873
900
|
* Exercise the write path with {@link ConfigFile.layer} instead.
|
|
874
901
|
*
|
|
902
|
+
* `CE` is the codec's error channel, as on {@link ConfigFileOptions}.
|
|
903
|
+
*
|
|
875
904
|
* @public
|
|
876
905
|
*/
|
|
877
|
-
interface ConfigFileTestOptions<A, I> {
|
|
906
|
+
interface ConfigFileTestOptions<A, I, CE = ConfigCodecError> {
|
|
878
907
|
/** The schema every seeded document is decoded through. */
|
|
879
908
|
readonly schema: Schema.Codec<A, I>;
|
|
880
|
-
/**
|
|
881
|
-
|
|
909
|
+
/**
|
|
910
|
+
* How file content becomes an unknown document, and back.
|
|
911
|
+
*
|
|
912
|
+
* @remarks
|
|
913
|
+
* Any `ConfigCodec<CE>`, decorated ones included, exactly as on
|
|
914
|
+
* {@link ConfigFileOptions.codec}.
|
|
915
|
+
*/
|
|
916
|
+
readonly codec: ConfigCodec<CE>;
|
|
882
917
|
/** How several discovered sources become one value. */
|
|
883
918
|
readonly strategy: MergeStrategy<A>;
|
|
884
919
|
/**
|
|
@@ -895,9 +930,13 @@ interface ConfigFileTestOptions<A, I> {
|
|
|
895
930
|
/**
|
|
896
931
|
* Options for {@link ConfigFile.read}.
|
|
897
932
|
*
|
|
933
|
+
* @remarks
|
|
934
|
+
* `CE` is the codec's error channel, inferred from `codec`, as on
|
|
935
|
+
* {@link ConfigFileOptions}.
|
|
936
|
+
*
|
|
898
937
|
* @public
|
|
899
938
|
*/
|
|
900
|
-
interface ConfigReadOptions<A, I> {
|
|
939
|
+
interface ConfigReadOptions<A, I, CE = ConfigCodecError> {
|
|
901
940
|
/** The schema the document is decoded through. */
|
|
902
941
|
readonly schema: Schema.Codec<A, I>;
|
|
903
942
|
/**
|
|
@@ -909,8 +948,11 @@ interface ConfigReadOptions<A, I> {
|
|
|
909
948
|
* free-standing-codec tree-shaking guarantee: a consumer that only ever
|
|
910
949
|
* passes `JsonCodec` never references the JSONC, YAML or TOML modules, so
|
|
911
950
|
* their engines stay out of the bundle.
|
|
951
|
+
*
|
|
952
|
+
* A decorated codec (`EncryptedCodec`, `ConfigMigration.make`) is accepted,
|
|
953
|
+
* and `read`'s error channel widens to its error.
|
|
912
954
|
*/
|
|
913
|
-
readonly codec: ConfigCodec
|
|
955
|
+
readonly codec: ConfigCodec<CE>;
|
|
914
956
|
/**
|
|
915
957
|
* Parse options for the decode, chiefly `onExcessProperty`.
|
|
916
958
|
*
|
|
@@ -931,12 +973,26 @@ export declare class ConfigFile {
|
|
|
931
973
|
/**
|
|
932
974
|
* Create a uniquely-keyed service class for one config schema.
|
|
933
975
|
*
|
|
976
|
+
* @remarks
|
|
977
|
+
* The optional third type argument is the codec's error channel. Omit it
|
|
978
|
+
* for the built-in codecs. A service over a decorated codec names the
|
|
979
|
+
* decorator's widened error, because the key is declared before the codec
|
|
980
|
+
* is chosen and cannot infer it: wiring `EncryptedCodec(JsonCodec, key)`
|
|
981
|
+
* into a key declared without it is a compile error, since that service's
|
|
982
|
+
* methods would fail with a `ConfigEncryptionError` its type never mentions.
|
|
983
|
+
*
|
|
934
984
|
* @example
|
|
935
985
|
* ```ts
|
|
936
986
|
* class AppConfig extends ConfigFile.Service<AppConfig, AppShape>()("app/Config") {}
|
|
987
|
+
*
|
|
988
|
+
* class Secrets extends ConfigFile.Service<
|
|
989
|
+
* Secrets,
|
|
990
|
+
* SecretsShape,
|
|
991
|
+
* ConfigCodecError | ConfigEncryptionError
|
|
992
|
+
* >()("app/Secrets") {}
|
|
937
993
|
* ```
|
|
938
994
|
*/
|
|
939
|
-
static readonly Service: <Self, A>() => <const Id extends string>(id: Id) => Context.ServiceClass<Self, Id, ConfigFileShape<A>>;
|
|
995
|
+
static readonly Service: <Self, A, CE = ConfigCodecError>() => <const Id extends string>(id: Id) => Context.ServiceClass<Self, Id, ConfigFileShape<A, CE>>;
|
|
940
996
|
/**
|
|
941
997
|
* Build the live layer for a config service class.
|
|
942
998
|
*
|
|
@@ -974,7 +1030,7 @@ export declare class ConfigFile {
|
|
|
974
1030
|
* });
|
|
975
1031
|
* ```
|
|
976
1032
|
*/
|
|
977
|
-
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>;
|
|
1033
|
+
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>;
|
|
978
1034
|
/**
|
|
979
1035
|
* A scoped layer that seeds `files` into a temp directory, wires the **real**
|
|
980
1036
|
* live implementation over them, and removes the directory when the scope
|
|
@@ -1008,7 +1064,7 @@ export declare class ConfigFile {
|
|
|
1008
1064
|
* }).pipe(Layer.provide(NodeServices.layer));
|
|
1009
1065
|
* ```
|
|
1010
1066
|
*/
|
|
1011
|
-
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>;
|
|
1067
|
+
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>;
|
|
1012
1068
|
/**
|
|
1013
1069
|
* Read, decode and validate one explicit path — no service, no layer, no tag.
|
|
1014
1070
|
*
|
|
@@ -1046,10 +1102,8 @@ export declare class ConfigFile {
|
|
|
1046
1102
|
* });
|
|
1047
1103
|
* ```
|
|
1048
1104
|
*/
|
|
1049
|
-
static readonly read: <A, I>(path: string, options: ConfigReadOptions<A, I>) => Effect.Effect<A, ConfigReadError
|
|
1105
|
+
static readonly read: <A, I, CE = ConfigCodecError>(path: string, options: ConfigReadOptions<A, I, CE>) => Effect.Effect<A, ConfigReadError<CE>, FileSystem.FileSystem>;
|
|
1050
1106
|
}
|
|
1051
|
-
//#endregion
|
|
1052
|
-
//#region src/ConfigMigration.d.ts
|
|
1053
1107
|
declare const ConfigMigrationError_base: Schema.Class<ConfigMigrationError, Schema.TaggedStruct<"ConfigMigrationError", {
|
|
1054
1108
|
/** The target version of the step that failed. `0` when reading the version failed. */
|
|
1055
1109
|
readonly version: Schema.Number;
|
|
@@ -1103,10 +1157,19 @@ export interface VersionAccess {
|
|
|
1103
1157
|
export declare const VersionAccess: {
|
|
1104
1158
|
readonly default: VersionAccess;
|
|
1105
1159
|
};
|
|
1106
|
-
/**
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1160
|
+
/**
|
|
1161
|
+
* Options for {@link ConfigMigration.make}.
|
|
1162
|
+
*
|
|
1163
|
+
* @remarks
|
|
1164
|
+
* `CE` is the wrapped codec's error channel, inferred from `codec`. It
|
|
1165
|
+
* defaults to `ConfigCodecError`; wrapping an already-decorated codec carries
|
|
1166
|
+
* its widened error through.
|
|
1167
|
+
*
|
|
1168
|
+
* @public
|
|
1169
|
+
*/
|
|
1170
|
+
interface ConfigMigrationOptions<CE = ConfigCodecError> {
|
|
1171
|
+
/** The codec being wrapped — any `ConfigCodec<CE>`, decorated ones included. */
|
|
1172
|
+
readonly codec: ConfigCodec<CE>;
|
|
1110
1173
|
/** The migration steps; they run in ascending `version` order. */
|
|
1111
1174
|
readonly migrations: ReadonlyArray<ConfigFileMigration>;
|
|
1112
1175
|
/** How the version is read and written; defaults to {@link (VersionAccess:variable).default}, a top-level `version` field. */
|
|
@@ -1122,7 +1185,11 @@ export declare class ConfigMigration {
|
|
|
1122
1185
|
* The returned codec's error channel **widens** to include
|
|
1123
1186
|
* {@link ConfigMigrationError} rather than flattening migration failures into
|
|
1124
1187
|
* the inner codec's error — the reason the {@link (ConfigCodec:interface)} seam is generic
|
|
1125
|
-
* in its error type.
|
|
1188
|
+
* in its error type. The inner codec's own error type is kept as-is, so
|
|
1189
|
+
* wrapping an `EncryptedCodec` yields
|
|
1190
|
+
* `ConfigCodec<ConfigCodecError | ConfigEncryptionError | ConfigMigrationError>`,
|
|
1191
|
+
* and the result goes straight into `ConfigFile.layer`, `testLayer` or
|
|
1192
|
+
* `read`.
|
|
1126
1193
|
*
|
|
1127
1194
|
* @example
|
|
1128
1195
|
* ```ts
|
|
@@ -1141,10 +1208,8 @@ export declare class ConfigMigration {
|
|
|
1141
1208
|
* });
|
|
1142
1209
|
* ```
|
|
1143
1210
|
*/
|
|
1144
|
-
static readonly make: (options: ConfigMigrationOptions) => ConfigCodec<
|
|
1211
|
+
static readonly make: <CE = ConfigCodecError>(options: ConfigMigrationOptions<CE>) => ConfigCodec<CE | ConfigMigrationError>;
|
|
1145
1212
|
}
|
|
1146
|
-
//#endregion
|
|
1147
|
-
//#region src/ConfigProvider.d.ts
|
|
1148
1213
|
/**
|
|
1149
1214
|
* Expose a loaded, merged, schema-validated document as a `ConfigProvider`,
|
|
1150
1215
|
* so it can be read through `Config.String("port")` and layered beneath other
|
|
@@ -1186,6 +1251,9 @@ export declare class ConfigMigration {
|
|
|
1186
1251
|
* Descent by own property is also why a prototype getter and `__proto__` are
|
|
1187
1252
|
* both unreachable through the returned provider.
|
|
1188
1253
|
*
|
|
1254
|
+
* `CE` is the service's codec error, so a service over a decorated codec
|
|
1255
|
+
* keeps its widened failure in this effect's error channel.
|
|
1256
|
+
*
|
|
1189
1257
|
* @example
|
|
1190
1258
|
* ```ts
|
|
1191
1259
|
* import { asConfigProvider } from "@effected/config-file";
|
|
@@ -1200,7 +1268,7 @@ export declare class ConfigMigration {
|
|
|
1200
1268
|
*
|
|
1201
1269
|
* @public
|
|
1202
1270
|
*/
|
|
1203
|
-
export declare const asConfigProvider: <A>(service: ConfigFileShape<A>) => Effect.Effect<ConfigProvider.ConfigProvider, ConfigLoadError
|
|
1271
|
+
export declare const asConfigProvider: <A, CE = ConfigCodecError>(service: ConfigFileShape<A, CE>) => Effect.Effect<ConfigProvider.ConfigProvider, ConfigLoadError<CE>>;
|
|
1204
1272
|
/**
|
|
1205
1273
|
* Options for {@link layerConfigProvider}.
|
|
1206
1274
|
*
|
|
@@ -1233,7 +1301,8 @@ interface LayerConfigProviderOptions {
|
|
|
1233
1301
|
*
|
|
1234
1302
|
* The load happens when the layer is built, and a {@link ConfigFileNotFoundError}
|
|
1235
1303
|
* surfaces in the layer's error channel rather than degrading to an empty
|
|
1236
|
-
* provider — the same honesty {@link asConfigProvider} keeps.
|
|
1304
|
+
* provider — the same honesty {@link asConfigProvider} keeps. A service over a
|
|
1305
|
+
* decorated codec carries its widened codec error `CE` there too.
|
|
1237
1306
|
*
|
|
1238
1307
|
* @example
|
|
1239
1308
|
* ```ts
|
|
@@ -1242,9 +1311,7 @@ interface LayerConfigProviderOptions {
|
|
|
1242
1311
|
*
|
|
1243
1312
|
* @public
|
|
1244
1313
|
*/
|
|
1245
|
-
export declare const layerConfigProvider: <Self, A>(tag: Context.Key<Self, ConfigFileShape<A>>, options?: LayerConfigProviderOptions) => Layer.Layer<never, ConfigLoadError
|
|
1246
|
-
//#endregion
|
|
1247
|
-
//#region src/EncryptedCodec.d.ts
|
|
1314
|
+
export declare const layerConfigProvider: <Self, A, CE = ConfigCodecError>(tag: Context.Key<Self, ConfigFileShape<A, CE>>, options?: LayerConfigProviderOptions) => Layer.Layer<never, ConfigLoadError<CE>, Self>;
|
|
1248
1315
|
declare const ConfigEncryptionError_base: Schema.Class<ConfigEncryptionError, Schema.TaggedStruct<"ConfigEncryptionError", {
|
|
1249
1316
|
/** Which cryptographic stage failed. */
|
|
1250
1317
|
readonly phase: Schema.Literals<readonly ["key-derivation", "encrypt", "decrypt", "encoding"]>;
|
|
@@ -1324,9 +1391,17 @@ export declare const EncryptedCodecKey: {
|
|
|
1324
1391
|
* the remainder is the ciphertext, and the plaintext is handed to the inner
|
|
1325
1392
|
* codec's `parse`.
|
|
1326
1393
|
*
|
|
1394
|
+
* The envelope is decoded with core's `effect/encoding/Base64`. Whitespace
|
|
1395
|
+
* around it (a trailing newline, a BOM) is tolerated; whitespace inside it, or
|
|
1396
|
+
* missing padding, fails with phase `"encoding"`. `stringify` never produces
|
|
1397
|
+
* either.
|
|
1398
|
+
*
|
|
1327
1399
|
* The error channel **widens** to `E | ConfigEncryptionError` rather than
|
|
1328
1400
|
* flattening — the inner codec's failures stay distinguishable from
|
|
1329
|
-
* cryptographic ones.
|
|
1401
|
+
* cryptographic ones. The result plugs straight into `ConfigFile.layer`,
|
|
1402
|
+
* `ConfigFile.testLayer` and `ConfigFile.read`, whose error unions widen with
|
|
1403
|
+
* it; a `ConfigFile.Service` key names the widened error as its third type
|
|
1404
|
+
* argument.
|
|
1330
1405
|
*
|
|
1331
1406
|
* @example
|
|
1332
1407
|
* ```ts
|
|
@@ -1339,8 +1414,6 @@ export declare const EncryptedCodecKey: {
|
|
|
1339
1414
|
* @public
|
|
1340
1415
|
*/
|
|
1341
1416
|
export declare function EncryptedCodec<E>(inner: ConfigCodec<E>, keySource: EncryptedCodecKey): ConfigCodec<E | ConfigEncryptionError>;
|
|
1342
|
-
//#endregion
|
|
1343
|
-
//#region src/JsonCodec.d.ts
|
|
1344
1417
|
/**
|
|
1345
1418
|
* A `ConfigCodec` backed by the host `JSON` global: plain JSON as
|
|
1346
1419
|
* configuration file content.
|
|
@@ -1354,8 +1427,6 @@ export declare function EncryptedCodec<E>(inner: ConfigCodec<E>, keySource: Encr
|
|
|
1354
1427
|
* @public
|
|
1355
1428
|
*/
|
|
1356
1429
|
export declare const JsonCodec: ConfigCodec;
|
|
1357
|
-
//#endregion
|
|
1358
|
-
//#region src/JsoncCodec.d.ts
|
|
1359
1430
|
/**
|
|
1360
1431
|
* A `ConfigCodec` backed by `@effected/jsonc`: JSON with comments and
|
|
1361
1432
|
* trailing commas.
|
|
@@ -1371,8 +1442,6 @@ export declare const JsonCodec: ConfigCodec;
|
|
|
1371
1442
|
* @public
|
|
1372
1443
|
*/
|
|
1373
1444
|
export declare const JsoncCodec: ConfigCodec;
|
|
1374
|
-
//#endregion
|
|
1375
|
-
//#region src/TomlCodec.d.ts
|
|
1376
1445
|
/**
|
|
1377
1446
|
* A `ConfigCodec` backed by `@effected/toml`.
|
|
1378
1447
|
*
|
|
@@ -1392,8 +1461,6 @@ export declare const JsoncCodec: ConfigCodec;
|
|
|
1392
1461
|
* @public
|
|
1393
1462
|
*/
|
|
1394
1463
|
export declare const TomlCodec: ConfigCodec;
|
|
1395
|
-
//#endregion
|
|
1396
|
-
//#region src/YamlCodec.d.ts
|
|
1397
1464
|
/**
|
|
1398
1465
|
* A `ConfigCodec` backed by `@effected/yaml`.
|
|
1399
1466
|
*
|
|
@@ -1408,6 +1475,5 @@ export declare const TomlCodec: ConfigCodec;
|
|
|
1408
1475
|
* @public
|
|
1409
1476
|
*/
|
|
1410
1477
|
export declare const YamlCodec: ConfigCodec;
|
|
1411
|
-
//#endregion
|
|
1412
1478
|
export type { ConfigCodec, ConfigEncodeError, ConfigEncodeOptions, ConfigEventsShape, ConfigFileMigration, ConfigFileOptions, ConfigFileShape, ConfigFileTestOptions, ConfigLoadError, ConfigMatch, ConfigMigrationOptions, ConfigProbe, ConfigReadError, ConfigReadOptions, ConfigSaveError, ConfigSource, ConfigUpdateError, ConfigWriteError, LayerConfigProviderOptions, NonEmptySources, UpwardWalkOptions };
|
|
1413
1479
|
//# sourceMappingURL=index.d.ts.map
|
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,8 +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
|
-
//#endregion
|
|
115
155
|
export { decrypt, deriveKey, encrypt, fromBase64, randomIv, toArrayBufferView, toBase64 };
|
package/internal/deepMerge.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/internal/deepMerge.ts
|
|
2
2
|
/**
|
|
3
3
|
* A true plain object: `{}` or `Object.create(null)`. Class instances, `Date`,
|
|
4
4
|
* `Map`, `Set`, `RegExp` and arrays are values, not merge targets.
|
|
@@ -76,5 +76,4 @@ const define = (target, key, value) => {
|
|
|
76
76
|
});
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
//#endregion
|
|
80
79
|
export { canMerge, deepMerge, isPlainObject };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/config-file",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
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": [
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
"./package.json": "./package.json"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@effected/glob": "^0.
|
|
47
|
-
"@effected/jsonc": "^0.
|
|
48
|
-
"@effected/toml": "^0.
|
|
49
|
-
"@effected/walker": "^0.
|
|
50
|
-
"@effected/yaml": "^0.
|
|
46
|
+
"@effected/glob": "^0.14.0",
|
|
47
|
+
"@effected/jsonc": "^0.20.0",
|
|
48
|
+
"@effected/toml": "^0.16.0",
|
|
49
|
+
"@effected/walker": "^0.19.0",
|
|
50
|
+
"@effected/yaml": "^0.24.0",
|
|
51
51
|
"effect": "^4.0.0"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|