@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 +28 -0
- package/dist/index.cjs +91 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +42 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
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":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|