@zodal/dials-store-secret 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/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # @zodal/dials-store-secret
2
+
3
+ The secret side of [zodal-dials](https://github.com/i2mint/zodal-dials)' content/metadata **bifurcation** — route secret values to a separate backend, never the config store.
4
+
5
+ ```bash
6
+ npm i @zodal/dials-store-secret @zodal/dials-core
7
+ ```
8
+
9
+ ```ts
10
+ import { createMemorySecretBackend, createSensitiveSettingsProvider, revealSetting } from '@zodal/dials-store-secret';
11
+ import { createJsoncStore } from '@zodal/dials-store-jsonc';
12
+
13
+ const secrets = createMemorySecretBackend(); // or an OS-keychain / Vault backend
14
+ const provider = createSensitiveSettingsProvider({
15
+ config: createJsoncStore({ path: 'settings.jsonc' }),
16
+ secrets,
17
+ sensitivityFor: dials.sensitivityFor,
18
+ });
19
+
20
+ await provider.save(layer); // non-secret -> config file; secret -> backend (JSON-encoded)
21
+ await provider.load(); // config values + a masked SecretRef per secret (never plaintext)
22
+ await revealSetting(secrets, 'network.apiKey'); // explicit, decoded reveal
23
+ ```
24
+
25
+ - **`createMemorySecretBackend()`** — a reference `SecretBackend` (in-memory; dev/test). Real backends (keychain, Vault, encrypted file) implement the same interface.
26
+ - **`createSensitiveSettingsProvider`** — a bifurcated `LayerStore`: splits the layer on save (secrets → backend, JSON-encoded so object-valued secrets survive; config → config store), surfaces masked `SecretRef`s on load. Secrets-first writes; throws (not silently drops) on a read-only config; `UNSET` deletes a secret.
27
+
28
+ Part of the [zodal-dials](https://github.com/i2mint/zodal-dials) ecosystem.
package/dist/index.cjs ADDED
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createMemorySecretBackend: () => createMemorySecretBackend,
24
+ createSensitiveSettingsProvider: () => createSensitiveSettingsProvider,
25
+ revealSetting: () => revealSetting
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var import_dials_core = require("@zodal/dials-core");
29
+ function createMemorySecretBackend(initial = {}) {
30
+ const store = new Map(Object.entries(initial));
31
+ return {
32
+ has: (key) => Promise.resolve(store.has(key)),
33
+ get: (key) => Promise.resolve((0, import_dials_core.makeSecretRef)(key, store.has(key))),
34
+ reveal: (key) => Promise.resolve(store.get(key)),
35
+ set: (key, value) => {
36
+ store.set(key, value);
37
+ return Promise.resolve((0, import_dials_core.makeSecretRef)(key, true));
38
+ },
39
+ delete: (key) => {
40
+ store.delete(key);
41
+ return Promise.resolve();
42
+ },
43
+ list: () => Promise.resolve([...store.keys()])
44
+ };
45
+ }
46
+ async function revealSetting(secrets, key) {
47
+ const raw = await secrets.reveal(key);
48
+ if (raw === void 0) return void 0;
49
+ try {
50
+ return JSON.parse(raw);
51
+ } catch {
52
+ return raw;
53
+ }
54
+ }
55
+ function createSensitiveSettingsProvider(options) {
56
+ const { config, secrets, sensitivityFor } = options;
57
+ const scope = options.scope ?? config.scope;
58
+ return {
59
+ scope,
60
+ getCapabilities: () => {
61
+ const capabilities = config.getCapabilities();
62
+ return { readable: capabilities.readable, writable: capabilities.writable, watchable: capabilities.watchable };
63
+ },
64
+ async load() {
65
+ const layer = { ...await config.load() };
66
+ for (const key of await secrets.list()) {
67
+ layer[key] = await secrets.get(key);
68
+ }
69
+ return layer;
70
+ },
71
+ async save(layer) {
72
+ const { config: configPart, secrets: secretPart } = (0, import_dials_core.splitBySensitivity)(layer, sensitivityFor);
73
+ const hasConfigPart = Object.keys(configPart).length > 0;
74
+ if (hasConfigPart && !config.save) {
75
+ throw new Error("@zodal/dials-store-secret: the config store is read-only; cannot persist non-secret settings");
76
+ }
77
+ for (const [key, value] of Object.entries(secretPart)) {
78
+ if ((0, import_dials_core.isUnset)(value)) await secrets.delete(key);
79
+ else if (value !== void 0 && value !== null) await secrets.set(key, JSON.stringify(value));
80
+ }
81
+ if (hasConfigPart) await config.save?.(configPart);
82
+ }
83
+ };
84
+ }
85
+ // Annotate the CommonJS export names for ESM import in node:
86
+ 0 && (module.exports = {
87
+ createMemorySecretBackend,
88
+ createSensitiveSettingsProvider,
89
+ revealSetting
90
+ });
91
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @zodal/dials-store-secret — the secret side of zodal-dials' content/metadata bifurcation.\n *\n * - `createMemorySecretBackend()` — a reference `SecretBackend` (in-memory; dev/test). A real backend\n * (OS keychain, Vault, encrypted file) implements the same interface. A backend stores opaque\n * string blobs; it is encoding-agnostic.\n * - `createSensitiveSettingsProvider({ config, secrets, sensitivityFor })` — composes a config\n * `LayerStore` + a `SecretBackend` into ONE `LayerStore` that routes secret values to the backend\n * (never to the config store). The provider stores each secret value JSON-ENCODED (so an object/\n * array-valued secret — which the container fail-safe classification produces — survives losslessly);\n * use `revealSetting` to decode it back. On `load`, config values come back plus a MASKED\n * `SecretRef` for every secret the backend holds — never plaintext.\n *\n * Cross-store `save` is best-effort, not atomic: secrets are written first, so a secret-write failure\n * aborts before the visible config part is committed. A read-only config store causes `save` to throw\n * (rather than silently drop) when there is a non-secret part to persist.\n */\n\nimport { isUnset, makeSecretRef, splitBySensitivity } from '@zodal/dials-core';\nimport type { Layer, LayerStore, LayerStoreCapabilities, SecretBackend, Sensitivity, SettingKey } from '@zodal/dials-core';\n\n/** An in-memory `SecretBackend` (reference implementation; dev/test). Holds string blobs in a Map. */\nexport function createMemorySecretBackend(initial: Record<SettingKey, string> = {}): SecretBackend {\n const store = new Map<SettingKey, string>(Object.entries(initial));\n return {\n has: (key) => Promise.resolve(store.has(key)),\n get: (key) => Promise.resolve(makeSecretRef(key, store.has(key))),\n reveal: (key) => Promise.resolve(store.get(key)),\n set: (key, value) => {\n store.set(key, value);\n return Promise.resolve(makeSecretRef(key, true));\n },\n delete: (key) => {\n store.delete(key);\n return Promise.resolve();\n },\n list: () => Promise.resolve([...store.keys()]),\n };\n}\n\n/**\n * Reveal a secret's ORIGINAL value (the inverse of how `createSensitiveSettingsProvider` stores it:\n * JSON-encoded). Returns undefined if the key is unset. Falls back to the raw string if it is not\n * valid JSON (tolerating values written outside the provider).\n */\nexport async function revealSetting(secrets: SecretBackend, key: SettingKey): Promise<unknown> {\n const raw = await secrets.reveal(key);\n if (raw === undefined) return undefined;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\nexport interface SensitiveSettingsProviderOptions {\n /** The config `LayerStore` for non-secret values. */\n config: LayerStore;\n /** The `SecretBackend` for secret values. */\n secrets: SecretBackend;\n /** Classify a setting's sensitivity (e.g. `dials.sensitivityFor`). */\n sensitivityFor: (key: SettingKey) => Sensitivity;\n /** Scope id. Default: the config store's scope. */\n scope?: string;\n}\n\n/** Compose a config `LayerStore` + a `SecretBackend` into one bifurcated `LayerStore`. */\nexport function createSensitiveSettingsProvider(options: SensitiveSettingsProviderOptions): LayerStore {\n const { config, secrets, sensitivityFor } = options;\n const scope = options.scope ?? config.scope;\n\n return {\n scope,\n getCapabilities: (): LayerStoreCapabilities => {\n // The config store gates a FULL write (the non-secret part needs it); mirror it conservatively.\n const capabilities = config.getCapabilities();\n return { readable: capabilities.readable, writable: capabilities.writable, watchable: capabilities.watchable };\n },\n\n async load(): Promise<Layer> {\n // Copy the config store's returned object — never mutate it (we overlay masked refs onto it).\n const layer: Layer = { ...(await config.load()) };\n // Overlay a MASKED SecretRef for every secret the backend holds (never plaintext).\n for (const key of await secrets.list()) {\n layer[key] = await secrets.get(key);\n }\n return layer;\n },\n\n async save(layer: Layer): Promise<void> {\n const { config: configPart, secrets: secretPart } = splitBySensitivity(layer, sensitivityFor);\n const hasConfigPart = Object.keys(configPart).length > 0;\n if (hasConfigPart && !config.save) {\n throw new Error('@zodal/dials-store-secret: the config store is read-only; cannot persist non-secret settings');\n }\n // Secrets first: a secret-write failure aborts before the visible config part is committed.\n // Values are JSON-encoded so object/array-valued secrets survive losslessly (see revealSetting).\n for (const [key, value] of Object.entries(secretPart)) {\n if (isUnset(value)) await secrets.delete(key);\n else if (value !== undefined && value !== null) await secrets.set(key, JSON.stringify(value));\n }\n if (hasConfigPart) await config.save?.(configPart);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBA,wBAA2D;AAIpD,SAAS,0BAA0B,UAAsC,CAAC,GAAkB;AACjG,QAAM,QAAQ,IAAI,IAAwB,OAAO,QAAQ,OAAO,CAAC;AACjE,SAAO;AAAA,IACL,KAAK,CAAC,QAAQ,QAAQ,QAAQ,MAAM,IAAI,GAAG,CAAC;AAAA,IAC5C,KAAK,CAAC,QAAQ,QAAQ,YAAQ,iCAAc,KAAK,MAAM,IAAI,GAAG,CAAC,CAAC;AAAA,IAChE,QAAQ,CAAC,QAAQ,QAAQ,QAAQ,MAAM,IAAI,GAAG,CAAC;AAAA,IAC/C,KAAK,CAAC,KAAK,UAAU;AACnB,YAAM,IAAI,KAAK,KAAK;AACpB,aAAO,QAAQ,YAAQ,iCAAc,KAAK,IAAI,CAAC;AAAA,IACjD;AAAA,IACA,QAAQ,CAAC,QAAQ;AACf,YAAM,OAAO,GAAG;AAChB,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAAA,IACA,MAAM,MAAM,QAAQ,QAAQ,CAAC,GAAG,MAAM,KAAK,CAAC,CAAC;AAAA,EAC/C;AACF;AAOA,eAAsB,cAAc,SAAwB,KAAmC;AAC7F,QAAM,MAAM,MAAM,QAAQ,OAAO,GAAG;AACpC,MAAI,QAAQ,OAAW,QAAO;AAC9B,MAAI;AACF,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAcO,SAAS,gCAAgC,SAAuD;AACrG,QAAM,EAAE,QAAQ,SAAS,eAAe,IAAI;AAC5C,QAAM,QAAQ,QAAQ,SAAS,OAAO;AAEtC,SAAO;AAAA,IACL;AAAA,IACA,iBAAiB,MAA8B;AAE7C,YAAM,eAAe,OAAO,gBAAgB;AAC5C,aAAO,EAAE,UAAU,aAAa,UAAU,UAAU,aAAa,UAAU,WAAW,aAAa,UAAU;AAAA,IAC/G;AAAA,IAEA,MAAM,OAAuB;AAE3B,YAAM,QAAe,EAAE,GAAI,MAAM,OAAO,KAAK,EAAG;AAEhD,iBAAW,OAAO,MAAM,QAAQ,KAAK,GAAG;AACtC,cAAM,GAAG,IAAI,MAAM,QAAQ,IAAI,GAAG;AAAA,MACpC;AACA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,KAAK,OAA6B;AACtC,YAAM,EAAE,QAAQ,YAAY,SAAS,WAAW,QAAI,sCAAmB,OAAO,cAAc;AAC5F,YAAM,gBAAgB,OAAO,KAAK,UAAU,EAAE,SAAS;AACvD,UAAI,iBAAiB,CAAC,OAAO,MAAM;AACjC,cAAM,IAAI,MAAM,8FAA8F;AAAA,MAChH;AAGA,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,UAAU,GAAG;AACrD,gBAAI,2BAAQ,KAAK,EAAG,OAAM,QAAQ,OAAO,GAAG;AAAA,iBACnC,UAAU,UAAa,UAAU,KAAM,OAAM,QAAQ,IAAI,KAAK,KAAK,UAAU,KAAK,CAAC;AAAA,MAC9F;AACA,UAAI,cAAe,OAAM,OAAO,OAAO,UAAU;AAAA,IACnD;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,42 @@
1
+ import { LayerStore, SecretBackend, SettingKey, Sensitivity } from '@zodal/dials-core';
2
+
3
+ /**
4
+ * @zodal/dials-store-secret — the secret side of zodal-dials' content/metadata bifurcation.
5
+ *
6
+ * - `createMemorySecretBackend()` — a reference `SecretBackend` (in-memory; dev/test). A real backend
7
+ * (OS keychain, Vault, encrypted file) implements the same interface. A backend stores opaque
8
+ * string blobs; it is encoding-agnostic.
9
+ * - `createSensitiveSettingsProvider({ config, secrets, sensitivityFor })` — composes a config
10
+ * `LayerStore` + a `SecretBackend` into ONE `LayerStore` that routes secret values to the backend
11
+ * (never to the config store). The provider stores each secret value JSON-ENCODED (so an object/
12
+ * array-valued secret — which the container fail-safe classification produces — survives losslessly);
13
+ * use `revealSetting` to decode it back. On `load`, config values come back plus a MASKED
14
+ * `SecretRef` for every secret the backend holds — never plaintext.
15
+ *
16
+ * Cross-store `save` is best-effort, not atomic: secrets are written first, so a secret-write failure
17
+ * aborts before the visible config part is committed. A read-only config store causes `save` to throw
18
+ * (rather than silently drop) when there is a non-secret part to persist.
19
+ */
20
+
21
+ /** An in-memory `SecretBackend` (reference implementation; dev/test). Holds string blobs in a Map. */
22
+ declare function createMemorySecretBackend(initial?: Record<SettingKey, string>): SecretBackend;
23
+ /**
24
+ * Reveal a secret's ORIGINAL value (the inverse of how `createSensitiveSettingsProvider` stores it:
25
+ * JSON-encoded). Returns undefined if the key is unset. Falls back to the raw string if it is not
26
+ * valid JSON (tolerating values written outside the provider).
27
+ */
28
+ declare function revealSetting(secrets: SecretBackend, key: SettingKey): Promise<unknown>;
29
+ interface SensitiveSettingsProviderOptions {
30
+ /** The config `LayerStore` for non-secret values. */
31
+ config: LayerStore;
32
+ /** The `SecretBackend` for secret values. */
33
+ secrets: SecretBackend;
34
+ /** Classify a setting's sensitivity (e.g. `dials.sensitivityFor`). */
35
+ sensitivityFor: (key: SettingKey) => Sensitivity;
36
+ /** Scope id. Default: the config store's scope. */
37
+ scope?: string;
38
+ }
39
+ /** Compose a config `LayerStore` + a `SecretBackend` into one bifurcated `LayerStore`. */
40
+ declare function createSensitiveSettingsProvider(options: SensitiveSettingsProviderOptions): LayerStore;
41
+
42
+ export { type SensitiveSettingsProviderOptions, createMemorySecretBackend, createSensitiveSettingsProvider, revealSetting };
@@ -0,0 +1,42 @@
1
+ import { LayerStore, SecretBackend, SettingKey, Sensitivity } from '@zodal/dials-core';
2
+
3
+ /**
4
+ * @zodal/dials-store-secret — the secret side of zodal-dials' content/metadata bifurcation.
5
+ *
6
+ * - `createMemorySecretBackend()` — a reference `SecretBackend` (in-memory; dev/test). A real backend
7
+ * (OS keychain, Vault, encrypted file) implements the same interface. A backend stores opaque
8
+ * string blobs; it is encoding-agnostic.
9
+ * - `createSensitiveSettingsProvider({ config, secrets, sensitivityFor })` — composes a config
10
+ * `LayerStore` + a `SecretBackend` into ONE `LayerStore` that routes secret values to the backend
11
+ * (never to the config store). The provider stores each secret value JSON-ENCODED (so an object/
12
+ * array-valued secret — which the container fail-safe classification produces — survives losslessly);
13
+ * use `revealSetting` to decode it back. On `load`, config values come back plus a MASKED
14
+ * `SecretRef` for every secret the backend holds — never plaintext.
15
+ *
16
+ * Cross-store `save` is best-effort, not atomic: secrets are written first, so a secret-write failure
17
+ * aborts before the visible config part is committed. A read-only config store causes `save` to throw
18
+ * (rather than silently drop) when there is a non-secret part to persist.
19
+ */
20
+
21
+ /** An in-memory `SecretBackend` (reference implementation; dev/test). Holds string blobs in a Map. */
22
+ declare function createMemorySecretBackend(initial?: Record<SettingKey, string>): SecretBackend;
23
+ /**
24
+ * Reveal a secret's ORIGINAL value (the inverse of how `createSensitiveSettingsProvider` stores it:
25
+ * JSON-encoded). Returns undefined if the key is unset. Falls back to the raw string if it is not
26
+ * valid JSON (tolerating values written outside the provider).
27
+ */
28
+ declare function revealSetting(secrets: SecretBackend, key: SettingKey): Promise<unknown>;
29
+ interface SensitiveSettingsProviderOptions {
30
+ /** The config `LayerStore` for non-secret values. */
31
+ config: LayerStore;
32
+ /** The `SecretBackend` for secret values. */
33
+ secrets: SecretBackend;
34
+ /** Classify a setting's sensitivity (e.g. `dials.sensitivityFor`). */
35
+ sensitivityFor: (key: SettingKey) => Sensitivity;
36
+ /** Scope id. Default: the config store's scope. */
37
+ scope?: string;
38
+ }
39
+ /** Compose a config `LayerStore` + a `SecretBackend` into one bifurcated `LayerStore`. */
40
+ declare function createSensitiveSettingsProvider(options: SensitiveSettingsProviderOptions): LayerStore;
41
+
42
+ export { type SensitiveSettingsProviderOptions, createMemorySecretBackend, createSensitiveSettingsProvider, revealSetting };
package/dist/index.js ADDED
@@ -0,0 +1,64 @@
1
+ // src/index.ts
2
+ import { isUnset, makeSecretRef, splitBySensitivity } from "@zodal/dials-core";
3
+ function createMemorySecretBackend(initial = {}) {
4
+ const store = new Map(Object.entries(initial));
5
+ return {
6
+ has: (key) => Promise.resolve(store.has(key)),
7
+ get: (key) => Promise.resolve(makeSecretRef(key, store.has(key))),
8
+ reveal: (key) => Promise.resolve(store.get(key)),
9
+ set: (key, value) => {
10
+ store.set(key, value);
11
+ return Promise.resolve(makeSecretRef(key, true));
12
+ },
13
+ delete: (key) => {
14
+ store.delete(key);
15
+ return Promise.resolve();
16
+ },
17
+ list: () => Promise.resolve([...store.keys()])
18
+ };
19
+ }
20
+ async function revealSetting(secrets, key) {
21
+ const raw = await secrets.reveal(key);
22
+ if (raw === void 0) return void 0;
23
+ try {
24
+ return JSON.parse(raw);
25
+ } catch {
26
+ return raw;
27
+ }
28
+ }
29
+ function createSensitiveSettingsProvider(options) {
30
+ const { config, secrets, sensitivityFor } = options;
31
+ const scope = options.scope ?? config.scope;
32
+ return {
33
+ scope,
34
+ getCapabilities: () => {
35
+ const capabilities = config.getCapabilities();
36
+ return { readable: capabilities.readable, writable: capabilities.writable, watchable: capabilities.watchable };
37
+ },
38
+ async load() {
39
+ const layer = { ...await config.load() };
40
+ for (const key of await secrets.list()) {
41
+ layer[key] = await secrets.get(key);
42
+ }
43
+ return layer;
44
+ },
45
+ async save(layer) {
46
+ const { config: configPart, secrets: secretPart } = splitBySensitivity(layer, sensitivityFor);
47
+ const hasConfigPart = Object.keys(configPart).length > 0;
48
+ if (hasConfigPart && !config.save) {
49
+ throw new Error("@zodal/dials-store-secret: the config store is read-only; cannot persist non-secret settings");
50
+ }
51
+ for (const [key, value] of Object.entries(secretPart)) {
52
+ if (isUnset(value)) await secrets.delete(key);
53
+ else if (value !== void 0 && value !== null) await secrets.set(key, JSON.stringify(value));
54
+ }
55
+ if (hasConfigPart) await config.save?.(configPart);
56
+ }
57
+ };
58
+ }
59
+ export {
60
+ createMemorySecretBackend,
61
+ createSensitiveSettingsProvider,
62
+ revealSetting
63
+ };
64
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @zodal/dials-store-secret — the secret side of zodal-dials' content/metadata bifurcation.\n *\n * - `createMemorySecretBackend()` — a reference `SecretBackend` (in-memory; dev/test). A real backend\n * (OS keychain, Vault, encrypted file) implements the same interface. A backend stores opaque\n * string blobs; it is encoding-agnostic.\n * - `createSensitiveSettingsProvider({ config, secrets, sensitivityFor })` — composes a config\n * `LayerStore` + a `SecretBackend` into ONE `LayerStore` that routes secret values to the backend\n * (never to the config store). The provider stores each secret value JSON-ENCODED (so an object/\n * array-valued secret — which the container fail-safe classification produces — survives losslessly);\n * use `revealSetting` to decode it back. On `load`, config values come back plus a MASKED\n * `SecretRef` for every secret the backend holds — never plaintext.\n *\n * Cross-store `save` is best-effort, not atomic: secrets are written first, so a secret-write failure\n * aborts before the visible config part is committed. A read-only config store causes `save` to throw\n * (rather than silently drop) when there is a non-secret part to persist.\n */\n\nimport { isUnset, makeSecretRef, splitBySensitivity } from '@zodal/dials-core';\nimport type { Layer, LayerStore, LayerStoreCapabilities, SecretBackend, Sensitivity, SettingKey } from '@zodal/dials-core';\n\n/** An in-memory `SecretBackend` (reference implementation; dev/test). Holds string blobs in a Map. */\nexport function createMemorySecretBackend(initial: Record<SettingKey, string> = {}): SecretBackend {\n const store = new Map<SettingKey, string>(Object.entries(initial));\n return {\n has: (key) => Promise.resolve(store.has(key)),\n get: (key) => Promise.resolve(makeSecretRef(key, store.has(key))),\n reveal: (key) => Promise.resolve(store.get(key)),\n set: (key, value) => {\n store.set(key, value);\n return Promise.resolve(makeSecretRef(key, true));\n },\n delete: (key) => {\n store.delete(key);\n return Promise.resolve();\n },\n list: () => Promise.resolve([...store.keys()]),\n };\n}\n\n/**\n * Reveal a secret's ORIGINAL value (the inverse of how `createSensitiveSettingsProvider` stores it:\n * JSON-encoded). Returns undefined if the key is unset. Falls back to the raw string if it is not\n * valid JSON (tolerating values written outside the provider).\n */\nexport async function revealSetting(secrets: SecretBackend, key: SettingKey): Promise<unknown> {\n const raw = await secrets.reveal(key);\n if (raw === undefined) return undefined;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\nexport interface SensitiveSettingsProviderOptions {\n /** The config `LayerStore` for non-secret values. */\n config: LayerStore;\n /** The `SecretBackend` for secret values. */\n secrets: SecretBackend;\n /** Classify a setting's sensitivity (e.g. `dials.sensitivityFor`). */\n sensitivityFor: (key: SettingKey) => Sensitivity;\n /** Scope id. Default: the config store's scope. */\n scope?: string;\n}\n\n/** Compose a config `LayerStore` + a `SecretBackend` into one bifurcated `LayerStore`. */\nexport function createSensitiveSettingsProvider(options: SensitiveSettingsProviderOptions): LayerStore {\n const { config, secrets, sensitivityFor } = options;\n const scope = options.scope ?? config.scope;\n\n return {\n scope,\n getCapabilities: (): LayerStoreCapabilities => {\n // The config store gates a FULL write (the non-secret part needs it); mirror it conservatively.\n const capabilities = config.getCapabilities();\n return { readable: capabilities.readable, writable: capabilities.writable, watchable: capabilities.watchable };\n },\n\n async load(): Promise<Layer> {\n // Copy the config store's returned object — never mutate it (we overlay masked refs onto it).\n const layer: Layer = { ...(await config.load()) };\n // Overlay a MASKED SecretRef for every secret the backend holds (never plaintext).\n for (const key of await secrets.list()) {\n layer[key] = await secrets.get(key);\n }\n return layer;\n },\n\n async save(layer: Layer): Promise<void> {\n const { config: configPart, secrets: secretPart } = splitBySensitivity(layer, sensitivityFor);\n const hasConfigPart = Object.keys(configPart).length > 0;\n if (hasConfigPart && !config.save) {\n throw new Error('@zodal/dials-store-secret: the config store is read-only; cannot persist non-secret settings');\n }\n // Secrets first: a secret-write failure aborts before the visible config part is committed.\n // Values are JSON-encoded so object/array-valued secrets survive losslessly (see revealSetting).\n for (const [key, value] of Object.entries(secretPart)) {\n if (isUnset(value)) await secrets.delete(key);\n else if (value !== undefined && value !== null) await secrets.set(key, JSON.stringify(value));\n }\n if (hasConfigPart) await config.save?.(configPart);\n },\n };\n}\n"],"mappings":";AAkBA,SAAS,SAAS,eAAe,0BAA0B;AAIpD,SAAS,0BAA0B,UAAsC,CAAC,GAAkB;AACjG,QAAM,QAAQ,IAAI,IAAwB,OAAO,QAAQ,OAAO,CAAC;AACjE,SAAO;AAAA,IACL,KAAK,CAAC,QAAQ,QAAQ,QAAQ,MAAM,IAAI,GAAG,CAAC;AAAA,IAC5C,KAAK,CAAC,QAAQ,QAAQ,QAAQ,cAAc,KAAK,MAAM,IAAI,GAAG,CAAC,CAAC;AAAA,IAChE,QAAQ,CAAC,QAAQ,QAAQ,QAAQ,MAAM,IAAI,GAAG,CAAC;AAAA,IAC/C,KAAK,CAAC,KAAK,UAAU;AACnB,YAAM,IAAI,KAAK,KAAK;AACpB,aAAO,QAAQ,QAAQ,cAAc,KAAK,IAAI,CAAC;AAAA,IACjD;AAAA,IACA,QAAQ,CAAC,QAAQ;AACf,YAAM,OAAO,GAAG;AAChB,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAAA,IACA,MAAM,MAAM,QAAQ,QAAQ,CAAC,GAAG,MAAM,KAAK,CAAC,CAAC;AAAA,EAC/C;AACF;AAOA,eAAsB,cAAc,SAAwB,KAAmC;AAC7F,QAAM,MAAM,MAAM,QAAQ,OAAO,GAAG;AACpC,MAAI,QAAQ,OAAW,QAAO;AAC9B,MAAI;AACF,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAcO,SAAS,gCAAgC,SAAuD;AACrG,QAAM,EAAE,QAAQ,SAAS,eAAe,IAAI;AAC5C,QAAM,QAAQ,QAAQ,SAAS,OAAO;AAEtC,SAAO;AAAA,IACL;AAAA,IACA,iBAAiB,MAA8B;AAE7C,YAAM,eAAe,OAAO,gBAAgB;AAC5C,aAAO,EAAE,UAAU,aAAa,UAAU,UAAU,aAAa,UAAU,WAAW,aAAa,UAAU;AAAA,IAC/G;AAAA,IAEA,MAAM,OAAuB;AAE3B,YAAM,QAAe,EAAE,GAAI,MAAM,OAAO,KAAK,EAAG;AAEhD,iBAAW,OAAO,MAAM,QAAQ,KAAK,GAAG;AACtC,cAAM,GAAG,IAAI,MAAM,QAAQ,IAAI,GAAG;AAAA,MACpC;AACA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,KAAK,OAA6B;AACtC,YAAM,EAAE,QAAQ,YAAY,SAAS,WAAW,IAAI,mBAAmB,OAAO,cAAc;AAC5F,YAAM,gBAAgB,OAAO,KAAK,UAAU,EAAE,SAAS;AACvD,UAAI,iBAAiB,CAAC,OAAO,MAAM;AACjC,cAAM,IAAI,MAAM,8FAA8F;AAAA,MAChH;AAGA,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,UAAU,GAAG;AACrD,YAAI,QAAQ,KAAK,EAAG,OAAM,QAAQ,OAAO,GAAG;AAAA,iBACnC,UAAU,UAAa,UAAU,KAAM,OAAM,QAAQ,IAAI,KAAK,KAAK,UAAU,KAAK,CAAC;AAAA,MAC9F;AACA,UAAI,cAAe,OAAM,OAAO,OAAO,UAAU;AAAA,IACnD;AAAA,EACF;AACF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@zodal/dials-store-secret",
3
+ "version": "0.1.0",
4
+ "description": "Secret backend + bifurcation provider for zodal-dials — routes secret values to a separate backend (content/metadata bifurcation)",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "peerDependencies": {
28
+ "@zodal/core": "^0.1.2",
29
+ "@zodal/dials-core": "^0.1.0"
30
+ },
31
+ "devDependencies": {
32
+ "@zodal/core": "^0.1.2",
33
+ "tsup": "^8.0.0",
34
+ "typescript": "^5.7.0",
35
+ "vitest": "^3.0.0",
36
+ "@zodal/dials-core": "0.1.0"
37
+ },
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "test": "vitest run",
41
+ "typecheck": "tsc --noEmit"
42
+ }
43
+ }