@fonderie/config 3.0.0 → 4.0.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 +84 -15
- package/brain/outcomes.md +59 -0
- package/brain/signatures.md +72 -1
- package/dist/{index-B2jOiL09.d.cts → index-CLaJRR69.d.cts} +14 -1
- package/dist/{index-B2jOiL09.d.ts → index-CLaJRR69.d.ts} +14 -1
- package/dist/index.cjs +410 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -5
- package/dist/index.d.ts +40 -5
- package/dist/index.js +391 -38
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +11 -0
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/middlewares/index.js +1 -0
- package/dist/middlewares/index.js.map +1 -1
- package/dist/migrations/sql/002_config_versioning.sql +20 -0
- package/dist/migrations/sql/003_secrets.sql +32 -0
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +27 -1
- package/dist/types.d.ts +27 -1
- package/package.json +82 -79
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# @fonderie/config
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
A Postgres-backed **control plane** for the settings and secrets your app changes
|
|
4
|
+
without a deploy — feature flags, remote config, and secrets — each **versioned,
|
|
5
|
+
with optimistic concurrency, revisions, rollback, and near-instant propagation**.
|
|
6
|
+
Manage it over an admin HTTP surface or the `fonderie` CLI.
|
|
5
7
|
|
|
6
8
|
## Install
|
|
7
9
|
|
|
@@ -9,35 +11,102 @@ TTL-cached snapshots, changeable at runtime without a deploy.
|
|
|
9
11
|
npm install @fonderie/config
|
|
10
12
|
```
|
|
11
13
|
|
|
12
|
-
##
|
|
14
|
+
## Read config at runtime
|
|
15
|
+
|
|
16
|
+
Register the module and read live values through `ctx.meta` — a per-environment
|
|
17
|
+
snapshot (with `'all'` as the base, env-specific overrides on top) kept fresh by
|
|
18
|
+
a poll floor and, when a `connectionUrl` is given, **`LISTEN/NOTIFY` push** so a
|
|
19
|
+
change lands in milliseconds.
|
|
13
20
|
|
|
14
21
|
```ts
|
|
15
22
|
import { FonderieApp, defineConfig } from '@fonderie/core';
|
|
16
|
-
import {
|
|
23
|
+
import { PGAdapter } from '@fonderie/store';
|
|
24
|
+
import { ConfigModule, getConfig } from '@fonderie/config';
|
|
17
25
|
|
|
26
|
+
const store = new PGAdapter(process.env.DATABASE_URL);
|
|
18
27
|
const app = await new FonderieApp(defineConfig({}))
|
|
19
|
-
.register(new ConfigModule(
|
|
28
|
+
.register(new ConfigModule(store, {
|
|
29
|
+
environment: 'production',
|
|
30
|
+
connectionUrl: process.env.DATABASE_URL, // opt-in push invalidation
|
|
31
|
+
}))
|
|
20
32
|
.boot();
|
|
33
|
+
|
|
34
|
+
// in a handler:
|
|
35
|
+
const on = getConfig(ctx, 'feature.new-onboarding', false);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Manage it (versioned + optimistic concurrency + rollback)
|
|
39
|
+
|
|
40
|
+
Every write bumps a monotonic `version`, appends an immutable revision, and
|
|
41
|
+
records the actor. Pass `ifVersion` for a compare-and-swap — the write commits
|
|
42
|
+
only if the version matches, else throws `ConfigConflictError` (reject-and-retry).
|
|
43
|
+
Writes are advisory-locked per `(key, environment)`, so concurrent creators of the
|
|
44
|
+
same key are serialized. `rollbackConfigEntry` rolls *forward* to a past value.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import {
|
|
48
|
+
setConfigEntry, getConfigEntry, rollbackConfigEntry,
|
|
49
|
+
listConfigRevisions, ConfigConflictError,
|
|
50
|
+
} from '@fonderie/config';
|
|
51
|
+
|
|
52
|
+
await setConfigEntry({ key: 'rate.limit', value: 100, ifVersion: 3, actor: 'ada' }, store);
|
|
53
|
+
const revs = await listConfigRevisions('rate.limit', 'all', store);
|
|
54
|
+
await rollbackConfigEntry({ key: 'rate.limit', toVersion: 2, actor: 'ada' }, store);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Secrets (masked, encryptable)
|
|
58
|
+
|
|
59
|
+
Same lifecycle, a separate `fonderie_secrets` table + read path so config can
|
|
60
|
+
never leak a secret. `getSecret`/`listSecrets` return **metadata only** (never the
|
|
61
|
+
value); values are **encrypted at rest** via a pluggable encryptor; `revealSecret`
|
|
62
|
+
is the single decrypt path.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { setSecret, revealSecret, createAesGcmEncryptor } from '@fonderie/config';
|
|
66
|
+
|
|
67
|
+
const enc = createAesGcmEncryptor(process.env.SECRET_KEY); // 32-byte hex, or omit for masked-only
|
|
68
|
+
await setSecret({ key: 'stripe.key', value: 'sk_live_…', actor: 'ada' }, store, enc);
|
|
69
|
+
const value = await revealSecret('stripe.key', 'all', store, enc);
|
|
21
70
|
```
|
|
22
71
|
|
|
72
|
+
## Admin HTTP surface
|
|
73
|
+
|
|
74
|
+
Set `adminToken` and the module registers `Bearer`-guarded routes (fail-closed —
|
|
75
|
+
no token, no surface): `GET/PUT/DELETE /admin/config[/:key]`,
|
|
76
|
+
`GET /admin/config/:key/revisions`, `POST /admin/config/:key/rollback`, and the
|
|
77
|
+
same for `/admin/secrets/*` (`PUT` honours `ifVersion` → **409** on conflict;
|
|
78
|
+
secret reads masked; `POST /admin/secrets/:key/reveal` decrypts).
|
|
79
|
+
|
|
23
80
|
```ts
|
|
24
|
-
|
|
81
|
+
new ConfigModule(store, { adminToken: process.env.ADMIN_TOKEN, secretEncryptor: enc });
|
|
25
82
|
```
|
|
26
83
|
|
|
27
|
-
|
|
28
|
-
|
|
84
|
+
## CLI
|
|
85
|
+
|
|
86
|
+
A thin client over that surface (`FONDERIE_ADMIN_URL` + `FONDERIE_ADMIN_TOKEN`):
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
fonderie config set feature.new-onboarding true --if-version 3
|
|
90
|
+
fonderie config history feature.new-onboarding
|
|
91
|
+
fonderie config rollback feature.new-onboarding --to-version 2
|
|
92
|
+
fonderie secret set stripe.key sk_live_… && fonderie secret reveal stripe.key
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Dependencies
|
|
96
|
+
|
|
97
|
+
Zero runtime dependencies. Peers only: `@fonderie/core`, `@fonderie/store`, and
|
|
98
|
+
`pg` (for the LISTEN client) — all already present in a Fonderie app.
|
|
29
99
|
|
|
30
100
|
## Why this exists
|
|
31
101
|
|
|
32
|
-
You've shipped this plumbing before — auth, teams, billing, messaging —
|
|
33
|
-
|
|
34
|
-
|
|
102
|
+
You've shipped this plumbing before — auth, teams, billing, messaging — and the
|
|
103
|
+
next project will ask for it again. Fonderie packages it once: plain TypeScript
|
|
104
|
+
modules for
|
|
35
105
|
[`@fonderie/core`](https://github.com/fonderie-js/sdk/tree/main/packages/core),
|
|
36
|
-
PostgreSQL-backed, self-hosted, MIT. No external control plane, no
|
|
37
|
-
|
|
106
|
+
PostgreSQL-backed, self-hosted, MIT. No external control plane, no per-seat
|
|
107
|
+
anything. Register the modules you need; skip the ones you don't.
|
|
38
108
|
|
|
39
|
-
**This package owns** how behavior
|
|
40
|
-
config read live by the other bricks.
|
|
109
|
+
**This package owns** how behavior — and secrets — change without a deploy.
|
|
41
110
|
|
|
42
111
|
Browse the whole set at
|
|
43
112
|
[fonderie-js/sdk](https://github.com/fonderie-js/sdk) · follow
|
package/brain/outcomes.md
CHANGED
|
@@ -19,7 +19,66 @@ environment TEXT NOT NULL DEFAULT 'all'
|
|
|
19
19
|
description TEXT
|
|
20
20
|
active BOOLEAN NOT NULL DEFAULT true
|
|
21
21
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
22
|
+
version INT NOT NULL DEFAULT 1
|
|
23
|
+
updated_by TEXT
|
|
24
|
+
-- UNIQUE (key, environment)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### `fonderie_config_revisions`
|
|
28
|
+
|
|
29
|
+
```sql
|
|
30
|
+
key TEXT NOT NULL
|
|
31
|
+
environment TEXT NOT NULL DEFAULT 'all'
|
|
32
|
+
value TEXT NOT NULL
|
|
33
|
+
version INT NOT NULL
|
|
34
|
+
actor TEXT
|
|
35
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
36
|
+
-- PRIMARY KEY (key, environment, version)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### `fonderie_secret_revisions`
|
|
40
|
+
|
|
41
|
+
```sql
|
|
42
|
+
key TEXT NOT NULL
|
|
43
|
+
environment TEXT NOT NULL DEFAULT 'all'
|
|
44
|
+
value TEXT NOT NULL
|
|
45
|
+
version INT NOT NULL
|
|
46
|
+
actor TEXT
|
|
47
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
48
|
+
-- PRIMARY KEY (key, environment, version)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### `fonderie_secrets`
|
|
52
|
+
|
|
53
|
+
```sql
|
|
54
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
|
|
55
|
+
key TEXT NOT NULL
|
|
56
|
+
value TEXT NOT NULL
|
|
57
|
+
environment TEXT NOT NULL DEFAULT 'all'
|
|
58
|
+
description TEXT
|
|
59
|
+
active BOOLEAN NOT NULL DEFAULT true
|
|
60
|
+
version INT NOT NULL DEFAULT 1
|
|
61
|
+
updated_by TEXT
|
|
62
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
22
63
|
-- UNIQUE (key, environment)
|
|
23
64
|
```
|
|
24
65
|
|
|
25
66
|
Raw SQL ships in `node_modules/@fonderie/config/dist/migrations/sql/` — read it there if you must; never download tarballs.
|
|
67
|
+
|
|
68
|
+
## HTTP routes registered
|
|
69
|
+
|
|
70
|
+
| Method | Path | Middleware chain (auth / validation / handler) |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| GET | `/admin/config` | `g(async (ctx) => { const rows = await listConfigEntries(envOf(ctx) ?? null, store); return setApiResponse(HTTP.OK, 'CONFIG_LISTED', 'Config entries', rows); })` |
|
|
73
|
+
| DELETE | `/admin/config/:key` | `g(async (ctx) => { const ok = await deleteConfigEntry(keyOf(ctx), envOf(ctx) ?? 'all', store); return setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? 'DELETED' : 'NOT_FOUND', ok ? 'Deleted' : 'No such config entry'); })` |
|
|
74
|
+
| GET | `/admin/config/:key` | `g(async (ctx) => { const row = await getConfigEntry(keyOf(ctx), envOf(ctx) ?? 'all', store); return row ? setApiResponse(HTTP.OK, 'CONFIG_ENTRY', 'Config entry', row) : setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such config entry'); })` |
|
|
75
|
+
| PUT | `/admin/config/:key` | `g(async (ctx) => { const b = body(ctx); if (!('value' in b)) { return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.value is required'); } try { const row = await setConfigEntry( { key: keyOf(ctx), value: b['value'], ...writeOpts(ctx, b) }, store, ); return setApiResponse(HTTP.OK, 'CONFIG_SET', 'Config entry saved', row); } catch (err) { return conflictOr(err); } })` |
|
|
76
|
+
| GET | `/admin/config/:key/revisions` | `g(async (ctx) => { const revs = await listConfigRevisions(keyOf(ctx), envOf(ctx) ?? 'all', store); return setApiResponse(HTTP.OK, 'REVISIONS', 'Config revisions', revs); })` |
|
|
77
|
+
| POST | `/admin/config/:key/rollback` | `g(async (ctx) => { const b = body(ctx); const toVersion = Number(b['toVersion']); if (!Number.isInteger(toVersion)) { return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.toVersion (int) is required'); } const row = await rollbackConfigEntry( rollbackOpts(ctx, b, toVersion), store, ); return setApiResponse(HTTP.OK, 'ROLLED_BACK', `Rolled back to v${toVersion}`, row); })` |
|
|
78
|
+
| GET | `/admin/secrets` | `g(async (ctx) => { const rows = await listSecrets(envOf(ctx) ?? null, store); return setApiResponse(HTTP.OK, 'SECRETS_LISTED', 'Secrets (masked)', rows); })` |
|
|
79
|
+
| DELETE | `/admin/secrets/:key` | `g(async (ctx) => { const ok = await deleteSecret(keyOf(ctx), envOf(ctx) ?? 'all', store); return setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? 'DELETED' : 'NOT_FOUND', ok ? 'Deleted' : 'No such secret'); })` |
|
|
80
|
+
| GET | `/admin/secrets/:key` | `g(async (ctx) => { const row = await getSecret(keyOf(ctx), envOf(ctx) ?? 'all', store); return row ? setApiResponse(HTTP.OK, 'SECRET', 'Secret (masked)', row) : setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such secret'); })` |
|
|
81
|
+
| PUT | `/admin/secrets/:key` | `g(async (ctx) => { const b = body(ctx); if (typeof b['value'] !== 'string') { return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.value (string) is required'); } try { const row = await setSecret( { key: keyOf(ctx), value: b['value'], ...writeOpts(ctx, b) }, store, encryptor, ); return setApiResponse(HTTP.OK, 'SECRET_SET', 'Secret saved (masked)', row); } catch (err) { return conflictOr(err); } })` |
|
|
82
|
+
| POST | `/admin/secrets/:key/reveal` | `g(async (ctx) => { const value = await revealSecret(keyOf(ctx), envOf(ctx) ?? 'all', store, encryptor); return value === null ? setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such secret') : setApiResponse(HTTP.OK, 'SECRET_REVEALED', 'Decrypted secret value', { value }); })` |
|
|
83
|
+
| GET | `/admin/secrets/:key/revisions` | `g(async (ctx) => { const revs = await listSecretRevisions(keyOf(ctx), envOf(ctx) ?? 'all', store); return setApiResponse(HTTP.OK, 'REVISIONS', 'Secret revisions', revs); })` |
|
|
84
|
+
| POST | `/admin/secrets/:key/rollback` | `g(async (ctx) => { const b = body(ctx); const toVersion = Number(b['toVersion']); if (!Number.isInteger(toVersion)) { return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.toVersion (int) is required'); } const row = await rollbackSecret( rollbackOpts(ctx, b, toVersion), store, ); return setApiResponse(HTTP.OK, 'ROLLED_BACK', `Rolled back to v${toVersion}`, row); })` |
|
package/brain/signatures.md
CHANGED
|
@@ -12,6 +12,8 @@ new ConfigModule(store: IStoreAdapter, options?: IConfigOptions): ConfigModule
|
|
|
12
12
|
.manager: RemoteConfigManager
|
|
13
13
|
.install(app: IFonderieApp): Promise<void>
|
|
14
14
|
|
|
15
|
+
function buildAdminRoutes(store: IStoreAdapter, adminToken: string, encryptor?: ISecretEncryptor): [string, string, Middleware][]
|
|
16
|
+
|
|
15
17
|
new RemoteConfigManager(store: IStoreAdapter, options?: IConfigOptions): RemoteConfigManager
|
|
16
18
|
.boot(): Promise<void>
|
|
17
19
|
.stop(): void
|
|
@@ -30,27 +32,96 @@ function listConfigEntries(environment: string | null, store: IStoreAdapter): Pr
|
|
|
30
32
|
|
|
31
33
|
function getConfigEntry(key: string, environment: string, store: IStoreAdapter): Promise<IConfigEntry | null>
|
|
32
34
|
|
|
33
|
-
function setConfigEntry(opts: { key: string; value: unknown; environment?: string; description?: string; active?: boolean; }, store: IStoreAdapter): Promise<IConfigEntry>
|
|
35
|
+
function setConfigEntry(opts: { key: string; value: unknown; environment?: string; description?: string; active?: boolean; ifVersion?: number; actor?: string; }, store: IStoreAdapter): Promise<IConfigEntry>
|
|
34
36
|
|
|
35
37
|
function deleteConfigEntry(key: string, environment: string, store: IStoreAdapter): Promise<boolean>
|
|
36
38
|
|
|
39
|
+
function rollbackConfigEntry(opts: { key: string; environment?: string; toVersion: number; actor?: string; }, store: IStoreAdapter): Promise<IConfigEntry>
|
|
40
|
+
|
|
41
|
+
function listConfigRevisions(key: string, environment: string, store: IStoreAdapter): Promise<IConfigRevision[]>
|
|
42
|
+
|
|
43
|
+
new ConfigConflictError(key: string, scope: string | null, currentVersion: number | null, expectedVersion: number): VersionConflictError
|
|
44
|
+
.key: string
|
|
45
|
+
.scope: string | null
|
|
46
|
+
.currentVersion: number | null
|
|
47
|
+
.expectedVersion: number
|
|
48
|
+
.name: string
|
|
49
|
+
.message: string
|
|
50
|
+
.stack: string
|
|
51
|
+
.cause: unknown
|
|
52
|
+
|
|
37
53
|
interface IConfigEntry {
|
|
38
54
|
key: string;
|
|
39
55
|
value: unknown;
|
|
40
56
|
environment: string;
|
|
41
57
|
description: string | null;
|
|
42
58
|
active: boolean;
|
|
59
|
+
version: number;
|
|
60
|
+
updatedBy: string | null;
|
|
43
61
|
updatedAt: string;
|
|
44
62
|
}
|
|
45
63
|
|
|
64
|
+
interface IConfigRevision {
|
|
65
|
+
key: string;
|
|
66
|
+
environment: string;
|
|
67
|
+
value: unknown;
|
|
68
|
+
version: number;
|
|
69
|
+
actor: string | null;
|
|
70
|
+
createdAt: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
46
73
|
interface IConfigSnapshot {
|
|
47
74
|
entries: Record<string, unknown>;
|
|
48
75
|
fetchedAt: Date;
|
|
49
76
|
}
|
|
50
77
|
|
|
78
|
+
function listSecrets(environment: string | null, store: IStoreAdapter): Promise<ISecretEntry[]>
|
|
79
|
+
|
|
80
|
+
function getSecret(key: string, environment: string, store: IStoreAdapter): Promise<ISecretEntry | null>
|
|
81
|
+
|
|
82
|
+
function revealSecret(key: string, environment: string, store: IStoreAdapter, encryptor?: ISecretEncryptor): Promise<string | null>
|
|
83
|
+
|
|
84
|
+
function setSecret(opts: { key: string; value: string; environment?: string; description?: string; active?: boolean; ifVersion?: number; actor?: string; }, store: IStoreAdapter, encryptor?: ISecretEncryptor): Promise<...>
|
|
85
|
+
|
|
86
|
+
function rollbackSecret(opts: { key: string; environment?: string; toVersion: number; actor?: string; }, store: IStoreAdapter): Promise<ISecretEntry>
|
|
87
|
+
|
|
88
|
+
function listSecretRevisions(key: string, environment: string, store: IStoreAdapter): Promise<ISecretRevision[]>
|
|
89
|
+
|
|
90
|
+
function deleteSecret(key: string, environment: string, store: IStoreAdapter): Promise<boolean>
|
|
91
|
+
|
|
92
|
+
interface ISecretEntry {
|
|
93
|
+
key: string;
|
|
94
|
+
environment: string;
|
|
95
|
+
description: string | null;
|
|
96
|
+
active: boolean;
|
|
97
|
+
version: number;
|
|
98
|
+
updatedBy: string | null;
|
|
99
|
+
updatedAt: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface ISecretRevision {
|
|
103
|
+
key: string;
|
|
104
|
+
environment: string;
|
|
105
|
+
version: number;
|
|
106
|
+
actor: string | null;
|
|
107
|
+
createdAt: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const noopEncryptor: ISecretEncryptor
|
|
111
|
+
|
|
112
|
+
function createAesGcmEncryptor(keyHex: string): ISecretEncryptor
|
|
113
|
+
|
|
114
|
+
interface ISecretEncryptor {
|
|
115
|
+
encrypt(plain: string): string;
|
|
116
|
+
decrypt(cipher: string): string;
|
|
117
|
+
}
|
|
118
|
+
|
|
51
119
|
interface IConfigOptions {
|
|
52
120
|
ttl?: number;
|
|
53
121
|
environment?: string;
|
|
54
122
|
table?: string;
|
|
123
|
+
connectionUrl?: string;
|
|
124
|
+
adminToken?: string;
|
|
125
|
+
secretEncryptor?: ISecretEncryptor;
|
|
55
126
|
}
|
|
56
127
|
```
|
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
import { Middleware } from '@fonderie/core';
|
|
2
2
|
import { IStoreAdapter } from '@fonderie/store';
|
|
3
3
|
|
|
4
|
+
interface ISecretEncryptor {
|
|
5
|
+
encrypt(plain: string): string;
|
|
6
|
+
decrypt(cipher: string): string;
|
|
7
|
+
}
|
|
8
|
+
declare const noopEncryptor: ISecretEncryptor;
|
|
9
|
+
declare function createAesGcmEncryptor(keyHex: string): ISecretEncryptor;
|
|
10
|
+
|
|
4
11
|
interface IConfigOptions {
|
|
5
12
|
ttl?: number;
|
|
6
13
|
environment?: string;
|
|
7
14
|
table?: string;
|
|
15
|
+
connectionUrl?: string;
|
|
16
|
+
adminToken?: string;
|
|
17
|
+
secretEncryptor?: ISecretEncryptor;
|
|
8
18
|
}
|
|
9
19
|
|
|
10
20
|
declare class RemoteConfigManager {
|
|
11
21
|
private store;
|
|
12
22
|
private snapshot;
|
|
13
23
|
private interval;
|
|
24
|
+
private listenClient;
|
|
14
25
|
private environment;
|
|
15
26
|
private ttl;
|
|
16
27
|
private table;
|
|
28
|
+
private connectionUrl;
|
|
17
29
|
constructor(store: IStoreAdapter, options?: IConfigOptions);
|
|
18
30
|
boot(): Promise<void>;
|
|
31
|
+
private startListening;
|
|
19
32
|
stop(): void;
|
|
20
33
|
get<T>(key: string, fallback: T): T;
|
|
21
34
|
all(): Record<string, unknown>;
|
|
@@ -29,4 +42,4 @@ declare function getConfig(ctx: {
|
|
|
29
42
|
meta: Record<string, unknown>;
|
|
30
43
|
}, key: string, fallback?: unknown): unknown;
|
|
31
44
|
|
|
32
|
-
export { CONFIG_MANAGER_KEY as C, type IConfigOptions as I, RemoteConfigManager as R, configContextMiddleware as c, getConfig as g };
|
|
45
|
+
export { CONFIG_MANAGER_KEY as C, type IConfigOptions as I, RemoteConfigManager as R, type ISecretEncryptor as a, createAesGcmEncryptor as b, configContextMiddleware as c, getConfig as g, noopEncryptor as n };
|
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
import { Middleware } from '@fonderie/core';
|
|
2
2
|
import { IStoreAdapter } from '@fonderie/store';
|
|
3
3
|
|
|
4
|
+
interface ISecretEncryptor {
|
|
5
|
+
encrypt(plain: string): string;
|
|
6
|
+
decrypt(cipher: string): string;
|
|
7
|
+
}
|
|
8
|
+
declare const noopEncryptor: ISecretEncryptor;
|
|
9
|
+
declare function createAesGcmEncryptor(keyHex: string): ISecretEncryptor;
|
|
10
|
+
|
|
4
11
|
interface IConfigOptions {
|
|
5
12
|
ttl?: number;
|
|
6
13
|
environment?: string;
|
|
7
14
|
table?: string;
|
|
15
|
+
connectionUrl?: string;
|
|
16
|
+
adminToken?: string;
|
|
17
|
+
secretEncryptor?: ISecretEncryptor;
|
|
8
18
|
}
|
|
9
19
|
|
|
10
20
|
declare class RemoteConfigManager {
|
|
11
21
|
private store;
|
|
12
22
|
private snapshot;
|
|
13
23
|
private interval;
|
|
24
|
+
private listenClient;
|
|
14
25
|
private environment;
|
|
15
26
|
private ttl;
|
|
16
27
|
private table;
|
|
28
|
+
private connectionUrl;
|
|
17
29
|
constructor(store: IStoreAdapter, options?: IConfigOptions);
|
|
18
30
|
boot(): Promise<void>;
|
|
31
|
+
private startListening;
|
|
19
32
|
stop(): void;
|
|
20
33
|
get<T>(key: string, fallback: T): T;
|
|
21
34
|
all(): Record<string, unknown>;
|
|
@@ -29,4 +42,4 @@ declare function getConfig(ctx: {
|
|
|
29
42
|
meta: Record<string, unknown>;
|
|
30
43
|
}, key: string, fallback?: unknown): unknown;
|
|
31
44
|
|
|
32
|
-
export { CONFIG_MANAGER_KEY as C, type IConfigOptions as I, RemoteConfigManager as R, configContextMiddleware as c, getConfig as g };
|
|
45
|
+
export { CONFIG_MANAGER_KEY as C, type IConfigOptions as I, RemoteConfigManager as R, type ISecretEncryptor as a, createAesGcmEncryptor as b, configContextMiddleware as c, getConfig as g, noopEncryptor as n };
|