@effected/config-file 0.1.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/index.js ADDED
@@ -0,0 +1,14 @@
1
+ import { ConfigCodecError } from "./ConfigCodec.js";
2
+ import { ConfigEvent, ConfigEventPayload, ConfigEvents, ConfigSourceRef } from "./ConfigEvent.js";
3
+ import { ConfigResolver } from "./ConfigResolver.js";
4
+ import { ConfigDefaultPathMissingError, ConfigFile, ConfigFileNotFoundError, ConfigFileReadError, ConfigFileWriteError, ConfigValidationError } from "./ConfigFile.js";
5
+ import { ConfigMigration, ConfigMigrationError, VersionAccess } from "./ConfigMigration.js";
6
+ import { asConfigProvider, layerConfigProvider } from "./ConfigProvider.js";
7
+ import { ConfigEncryptionError, EncryptedCodec, EncryptedCodecKey } from "./EncryptedCodec.js";
8
+ import { JsonCodec } from "./JsonCodec.js";
9
+ import { JsoncCodec } from "./JsoncCodec.js";
10
+ import { MergeStrategy } from "./MergeStrategy.js";
11
+ import { TomlCodec } from "./TomlCodec.js";
12
+ import { YamlCodec } from "./YamlCodec.js";
13
+
14
+ export { ConfigCodecError, ConfigDefaultPathMissingError, ConfigEncryptionError, ConfigEvent, ConfigEventPayload, ConfigEvents, ConfigFile, ConfigFileNotFoundError, ConfigFileReadError, ConfigFileWriteError, ConfigMigration, ConfigMigrationError, ConfigResolver, ConfigSourceRef, ConfigValidationError, EncryptedCodec, EncryptedCodecKey, JsonCodec, JsoncCodec, MergeStrategy, TomlCodec, VersionAccess, YamlCodec, asConfigProvider, layerConfigProvider };
@@ -0,0 +1,115 @@
1
+ import { Effect } from "effect";
2
+
3
+ //#region src/internal/crypto.ts
4
+ /**
5
+ * AES-GCM primitives over WebCrypto.
6
+ *
7
+ * @remarks
8
+ * This module imports nothing from the rest of the package. `EncryptedCodec.ts`
9
+ * imports these helpers, so a back-import of `ConfigEncryptionError` would close
10
+ * a cycle that Biome's error-level `noImportCycles` rule rejects. Staying
11
+ * dependency-free also keeps the module cheap to extract later.
12
+ *
13
+ * Failures surface as the plain {@link CryptoFailure} record; the public module
14
+ * lifts them into `ConfigEncryptionError`.
15
+ *
16
+ * @internal
17
+ */
18
+ /** AES-GCM's standard IV length, in bytes. @internal */
19
+ const IV_LENGTH = 12;
20
+ /** Tag a caught host failure with the phase that produced it, preserving it by identity. */
21
+ const fail = (phase) => (cause) => ({
22
+ phase,
23
+ cause
24
+ });
25
+ /**
26
+ * Copy a `Uint8Array` into a fresh `Uint8Array` backed by a plain `ArrayBuffer`,
27
+ * which is required by Web Crypto APIs that accept `BufferSource`.
28
+ *
29
+ * @internal
30
+ */
31
+ function toArrayBufferView(src) {
32
+ const buf = new ArrayBuffer(src.length);
33
+ const view = new Uint8Array(buf);
34
+ view.set(src);
35
+ return view;
36
+ }
37
+ /**
38
+ * Derive an AES-GCM key from a passphrase via PBKDF2.
39
+ *
40
+ * @remarks
41
+ * The caller memoizes this so PBKDF2 runs once per codec instance.
42
+ *
43
+ * @internal
44
+ */
45
+ const deriveKey = (passphrase, salt) => Effect.tryPromise({
46
+ try: async () => {
47
+ const enc = new TextEncoder();
48
+ const keyMaterial = await globalThis.crypto.subtle.importKey("raw", enc.encode(passphrase), "PBKDF2", false, ["deriveKey"]);
49
+ return await globalThis.crypto.subtle.deriveKey({
50
+ name: "PBKDF2",
51
+ salt: toArrayBufferView(salt),
52
+ iterations: 6e5,
53
+ hash: "SHA-256"
54
+ }, keyMaterial, {
55
+ name: "AES-GCM",
56
+ length: 256
57
+ }, false, ["encrypt", "decrypt"]);
58
+ },
59
+ catch: fail("key-derivation")
60
+ });
61
+ /**
62
+ * Decode base64 into bytes.
63
+ *
64
+ * @remarks
65
+ * `atob` is available in all modern environments (Node 20+, Bun, Deno).
66
+ *
67
+ * @internal
68
+ */
69
+ const fromBase64 = (raw) => Effect.try({
70
+ try: () => {
71
+ const binary = atob(raw);
72
+ const buf = new ArrayBuffer(binary.length);
73
+ const bytes = new Uint8Array(buf);
74
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
75
+ return bytes;
76
+ },
77
+ catch: fail("encoding")
78
+ });
79
+ /**
80
+ * Prepend the IV to the ciphertext and base64-encode the envelope.
81
+ *
82
+ * @internal
83
+ */
84
+ const toBase64 = (iv, ciphertext) => Effect.try({
85
+ try: () => {
86
+ const ciphertextBytes = new Uint8Array(ciphertext);
87
+ const resultBuf = new ArrayBuffer(12 + ciphertextBytes.length);
88
+ const result = new Uint8Array(resultBuf);
89
+ result.set(iv, 0);
90
+ result.set(ciphertextBytes, 12);
91
+ return btoa(Array.from(result, (b) => String.fromCharCode(b)).join(""));
92
+ },
93
+ catch: fail("encoding")
94
+ });
95
+ /** Decrypt AES-GCM ciphertext under `iv`. @internal */
96
+ const decrypt = (key, iv, ciphertext) => Effect.tryPromise({
97
+ try: () => globalThis.crypto.subtle.decrypt({
98
+ name: "AES-GCM",
99
+ iv
100
+ }, key, ciphertext),
101
+ catch: fail("decrypt")
102
+ });
103
+ /** Encrypt `plaintext` with AES-GCM under `iv`. @internal */
104
+ const encrypt = (key, iv, plaintext) => Effect.tryPromise({
105
+ try: () => globalThis.crypto.subtle.encrypt({
106
+ name: "AES-GCM",
107
+ iv
108
+ }, key, plaintext),
109
+ catch: fail("encrypt")
110
+ });
111
+ /** A fresh cryptographically random 12-byte IV. @internal */
112
+ const randomIv = () => globalThis.crypto.getRandomValues(new Uint8Array(/* @__PURE__ */ new ArrayBuffer(12)));
113
+
114
+ //#endregion
115
+ export { decrypt, deriveKey, encrypt, fromBase64, randomIv, toArrayBufferView, toBase64 };
@@ -0,0 +1,80 @@
1
+ //#region src/internal/deepMerge.ts
2
+ /**
3
+ * A true plain object: `{}` or `Object.create(null)`. Class instances, `Date`,
4
+ * `Map`, `Set`, `RegExp` and arrays are values, not merge targets.
5
+ *
6
+ * @remarks
7
+ * This is deliberately narrower than `typeof v === "object"`. A `Date` spread
8
+ * into a fresh object loses every internal slot and yields `{}`; a class
9
+ * instance loses its prototype, so `instanceof` fails and its getters vanish.
10
+ * Recursion is gated on this predicate so nested values of those kinds stay
11
+ * atomic — the higher-priority source wins them whole.
12
+ */
13
+ const isPlainObject = (value) => {
14
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
15
+ const proto = Object.getPrototypeOf(value);
16
+ return proto === Object.prototype || proto === null;
17
+ };
18
+ /**
19
+ * Record-like: a `[object Object]` whose fields can be merged. Admits plain
20
+ * objects and class instances (a decoded `Schema.Class` document), but not
21
+ * `Date` / `Map` / `Set` / `RegExp` / arrays, whose behaviour lives in internal
22
+ * slots that a field-wise merge would destroy.
23
+ */
24
+ const isRecordLike = (value) => typeof value === "object" && value !== null && Object.prototype.toString.call(value) === "[object Object]";
25
+ /**
26
+ * Two values may be merged only if both are record-like and share a prototype.
27
+ * Same-prototype instances merge field-wise; anything else is atomic.
28
+ *
29
+ * @remarks
30
+ * Requiring an identical prototype is what keeps the merge honest: a document
31
+ * decoded through `Schema.Class` merges with another of the same class and
32
+ * keeps its identity, and nothing else is ever silently reshaped.
33
+ */
34
+ const canMerge = (a, b) => isRecordLike(a) && isRecordLike(b) && Object.getPrototypeOf(a) === Object.getPrototypeOf(b);
35
+ const FORBIDDEN = /* @__PURE__ */ new Set([
36
+ "__proto__",
37
+ "constructor",
38
+ "prototype"
39
+ ]);
40
+ /**
41
+ * Recursively merge `source` into `target`; keys already present on `target`
42
+ * win. Nested plain objects merge; every other value is atomic.
43
+ *
44
+ * @remarks
45
+ * The result is built on `target`'s prototype rather than spread into `{}`, so
46
+ * a decoded `Schema.Class` document survives the merge as a real instance —
47
+ * `instanceof` holds and its getters still work. Without this, `load` would
48
+ * declare `Effect<A>` and hand back a structurally-equal plain object, and a
49
+ * consumer calling a class method would get a `TypeError` that typechecked.
50
+ *
51
+ * Only own enumerable keys are consulted (`Object.hasOwn`), so a prototype
52
+ * getter on `target` never shadows a real key on `source`.
53
+ */
54
+ const deepMerge = (target, source) => {
55
+ const result = Object.create(Object.getPrototypeOf(target));
56
+ for (const key of Object.keys(target)) {
57
+ if (FORBIDDEN.has(key)) continue;
58
+ define(result, key, target[key]);
59
+ }
60
+ for (const key of Object.keys(source)) {
61
+ if (FORBIDDEN.has(key)) continue;
62
+ const sourceValue = source[key];
63
+ const targetValue = result[key];
64
+ if (Object.hasOwn(result, key) && isPlainObject(targetValue) && isPlainObject(sourceValue)) define(result, key, deepMerge(targetValue, sourceValue));
65
+ else if (!Object.hasOwn(result, key)) define(result, key, sourceValue);
66
+ }
67
+ return result;
68
+ };
69
+ /** Create an own data property, never invoking a setter inherited from the prototype chain. */
70
+ const define = (target, key, value) => {
71
+ Object.defineProperty(target, key, {
72
+ value,
73
+ writable: true,
74
+ enumerable: true,
75
+ configurable: true
76
+ });
77
+ };
78
+
79
+ //#endregion
80
+ export { canMerge, deepMerge, isPlainObject };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@effected/config-file",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Composable config file loading for Effect: JSON, JSONC, YAML and TOML codecs, resolution strategies, and merge behaviors.",
6
+ "keywords": [
7
+ "config",
8
+ "config-file",
9
+ "codec",
10
+ "json",
11
+ "jsonc",
12
+ "yaml",
13
+ "toml",
14
+ "resolver",
15
+ "merge-strategy",
16
+ "effect",
17
+ "schema",
18
+ "effected"
19
+ ],
20
+ "homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/config-file#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/spencerbeggs/effected/issues"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/spencerbeggs/effected.git",
27
+ "directory": "packages/config-file"
28
+ },
29
+ "license": "MIT",
30
+ "author": {
31
+ "name": "C. Spencer Beggs",
32
+ "email": "spencer@beggs.codes",
33
+ "url": "https://spencerbeg.gs"
34
+ },
35
+ "sideEffects": false,
36
+ "type": "module",
37
+ "exports": {
38
+ ".": {
39
+ "types": "./index.d.ts",
40
+ "import": "./index.js"
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "peerDependencies": {
45
+ "@effected/jsonc": "0.1.0",
46
+ "@effected/toml": "0.1.0",
47
+ "@effected/walker": "0.1.0",
48
+ "@effected/yaml": "0.1.0",
49
+ "effect": "4.0.0-beta.98"
50
+ },
51
+ "engines": {
52
+ "node": ">=24.11.0"
53
+ }
54
+ }
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.58.9"
9
+ }
10
+ ]
11
+ }