@terreno/api 0.18.0 → 0.20.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/CHANGELOG.md +25 -0
- package/dist/api.test.js +18 -8
- package/dist/auth.d.ts +5 -5
- package/dist/auth.js +123 -131
- package/dist/configuration.test.js +289 -10
- package/dist/configurationApp.d.ts +72 -5
- package/dist/configurationApp.js +168 -48
- package/dist/configurationPlugin.d.ts +64 -7
- package/dist/configurationPlugin.js +161 -39
- package/dist/configurationPlugin.test.js +238 -1
- package/dist/expressServer.test.js +0 -1
- package/dist/openApi.d.ts +6 -6
- package/dist/openApi.js +21 -21
- package/dist/populate.test.js +23 -0
- package/dist/realtime/queryMatcher.js +0 -6
- package/dist/realtime/queryStore.js +3 -11
- package/dist/realtime/realtime.test.js +41 -34
- package/dist/secretProviders.d.ts +79 -2
- package/dist/secretProviders.js +177 -9
- package/dist/secretProviders.test.d.ts +1 -0
- package/dist/secretProviders.test.js +391 -0
- package/package.json +1 -1
- package/src/actions.openApi.test.ts +1 -1
- package/src/actions.ts +0 -1
- package/src/api.test.ts +10 -2
- package/src/auth.ts +19 -19
- package/src/configuration.test.ts +171 -7
- package/src/configurationApp.ts +213 -30
- package/src/configurationPlugin.test.ts +174 -2
- package/src/configurationPlugin.ts +157 -28
- package/src/expressServer.test.ts +0 -1
- package/src/openApi.ts +21 -21
- package/src/populate.test.ts +25 -0
- package/src/realtime/queryMatcher.ts +0 -6
- package/src/realtime/queryStore.ts +1 -10
- package/src/realtime/realtime.test.ts +24 -24
- package/src/realtime/realtimeApp.ts +0 -1
- package/src/realtime/registry.ts +0 -1
- package/src/realtime/types.ts +0 -4
- package/src/secretProviders.test.ts +186 -0
- package/src/secretProviders.ts +145 -5
package/dist/configurationApp.js
CHANGED
|
@@ -90,9 +90,11 @@ var api_1 = require("./api");
|
|
|
90
90
|
var auth_1 = require("./auth");
|
|
91
91
|
var errors_1 = require("./errors");
|
|
92
92
|
var logger_1 = require("./logger");
|
|
93
|
+
var permissions_1 = require("./permissions");
|
|
93
94
|
var populate_1 = require("./populate");
|
|
94
95
|
/**
|
|
95
|
-
* Middleware that requires the user to be an admin.
|
|
96
|
+
* Middleware that requires the user to be an admin. Used as the default guard
|
|
97
|
+
* for every configuration route when no custom `permissions` are supplied.
|
|
96
98
|
*/
|
|
97
99
|
var requireAdmin = function (req, _res, next) {
|
|
98
100
|
var _a;
|
|
@@ -102,6 +104,43 @@ var requireAdmin = function (req, _res, next) {
|
|
|
102
104
|
}
|
|
103
105
|
next();
|
|
104
106
|
};
|
|
107
|
+
/**
|
|
108
|
+
* Builds an Express middleware that AND-combines terreno permission functions
|
|
109
|
+
* (the same {@link PermissionMethod} contract used by `modelRouter`). The
|
|
110
|
+
* configuration singleton has no per-object ownership, so the loaded config
|
|
111
|
+
* document is passed as the permission object.
|
|
112
|
+
*/
|
|
113
|
+
var buildPermissionMiddleware = function (perms, method, loadObj) {
|
|
114
|
+
return (0, api_1.asyncHandler)(function (req, _res, next) { return __awaiter(void 0, void 0, void 0, function () {
|
|
115
|
+
var obj, _a, allowed;
|
|
116
|
+
return __generator(this, function (_b) {
|
|
117
|
+
switch (_b.label) {
|
|
118
|
+
case 0:
|
|
119
|
+
if (!loadObj) return [3 /*break*/, 2];
|
|
120
|
+
return [4 /*yield*/, loadObj()];
|
|
121
|
+
case 1:
|
|
122
|
+
_a = _b.sent();
|
|
123
|
+
return [3 /*break*/, 3];
|
|
124
|
+
case 2:
|
|
125
|
+
_a = undefined;
|
|
126
|
+
_b.label = 3;
|
|
127
|
+
case 3:
|
|
128
|
+
obj = _a;
|
|
129
|
+
return [4 /*yield*/, (0, permissions_1.checkPermissions)(method, perms, req.user, obj)];
|
|
130
|
+
case 4:
|
|
131
|
+
allowed = _b.sent();
|
|
132
|
+
if (!allowed) {
|
|
133
|
+
throw new errors_1.APIError({
|
|
134
|
+
status: 403,
|
|
135
|
+
title: "Access to configuration denied",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
next();
|
|
139
|
+
return [2 /*return*/];
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}); });
|
|
143
|
+
};
|
|
105
144
|
var extractFieldMeta = function (properties, required, schema, prefix, fieldOverrides) {
|
|
106
145
|
var e_1, _a;
|
|
107
146
|
var _b, _c, _d;
|
|
@@ -183,6 +222,49 @@ var redactSecrets = function (obj, secretFields) {
|
|
|
183
222
|
}
|
|
184
223
|
return redacted;
|
|
185
224
|
};
|
|
225
|
+
/**
|
|
226
|
+
* Removes secret field values from an incoming update body so a secret value can
|
|
227
|
+
* never be written to the configuration document through the update path.
|
|
228
|
+
* Copies nodes along each secret path to avoid mutating the caller's object.
|
|
229
|
+
*/
|
|
230
|
+
var stripSecretFields = function (obj, secretFields) {
|
|
231
|
+
var e_3, _a;
|
|
232
|
+
var stripped = __assign({}, obj);
|
|
233
|
+
try {
|
|
234
|
+
for (var secretFields_2 = __values(secretFields), secretFields_2_1 = secretFields_2.next(); !secretFields_2_1.done; secretFields_2_1 = secretFields_2.next()) {
|
|
235
|
+
var field = secretFields_2_1.value;
|
|
236
|
+
var parts = field.path.split(".");
|
|
237
|
+
var current = stripped;
|
|
238
|
+
for (var i = 0; i < parts.length - 1; i++) {
|
|
239
|
+
if (!current) {
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
var part = parts[i];
|
|
243
|
+
var nested = current[part];
|
|
244
|
+
if (nested != null && typeof nested === "object") {
|
|
245
|
+
var copy = __assign({}, nested);
|
|
246
|
+
current[part] = copy;
|
|
247
|
+
current = copy;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
current = null;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (current != null) {
|
|
255
|
+
delete current[parts[parts.length - 1]];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (e_3_1) { e_3 = { error: e_3_1 }; }
|
|
260
|
+
finally {
|
|
261
|
+
try {
|
|
262
|
+
if (secretFields_2_1 && !secretFields_2_1.done && (_a = secretFields_2.return)) _a.call(secretFields_2);
|
|
263
|
+
}
|
|
264
|
+
finally { if (e_3) throw e_3.error; }
|
|
265
|
+
}
|
|
266
|
+
return stripped;
|
|
267
|
+
};
|
|
186
268
|
/**
|
|
187
269
|
* Converts a camelCase or PascalCase string into a display-friendly title.
|
|
188
270
|
*/
|
|
@@ -197,11 +279,21 @@ var toDisplayName = function (name) {
|
|
|
197
279
|
*
|
|
198
280
|
* Inspects the Mongoose configuration model to auto-generate:
|
|
199
281
|
* - `GET {basePath}/meta` — Schema metadata (sections, fields, types, descriptions)
|
|
200
|
-
* - `GET {basePath}` — Current configuration values
|
|
201
|
-
* - `PATCH {basePath}` — Update configuration values
|
|
202
|
-
* - `POST {basePath}/
|
|
282
|
+
* - `GET {basePath}` — Current configuration values (secret values redacted)
|
|
283
|
+
* - `PATCH {basePath}` — Update configuration values (secret fields stripped; never written)
|
|
284
|
+
* - `POST {basePath}/list-secrets` (alias `POST {basePath}/validate-secrets`) —
|
|
285
|
+
* Read-only status of each secret field (whether the provider can resolve it).
|
|
286
|
+
* This endpoint never resolves values into the document and returns no secret values.
|
|
287
|
+
*
|
|
288
|
+
* By default all endpoints require `Permissions.IsAdmin`. Supply `permissions`
|
|
289
|
+
* to gate routes with a consumer's own permission functions, and `preUpdate`/
|
|
290
|
+
* `postUpdate` hooks to validate and audit-log changes. This makes
|
|
291
|
+
* `ConfigurationApp` suitable as a single, consumer-owned configuration surface
|
|
292
|
+
* that can replace a bespoke config router.
|
|
203
293
|
*
|
|
204
|
-
*
|
|
294
|
+
* Secret values never touch the database, logs, audit payloads, or API
|
|
295
|
+
* responses: secret fields are stripped from incoming updates and redacted on
|
|
296
|
+
* every read.
|
|
205
297
|
*
|
|
206
298
|
* Nested subschemas in the model become separate sections in the metadata,
|
|
207
299
|
* making them renderable as cards/accordions in the admin UI.
|
|
@@ -223,7 +315,10 @@ var toDisplayName = function (name) {
|
|
|
223
315
|
* const AppConfig = mongoose.model("AppConfig", configSchema);
|
|
224
316
|
*
|
|
225
317
|
* new TerrenoApp({ userModel: User })
|
|
226
|
-
* .configure(AppConfig
|
|
318
|
+
* .configure(AppConfig, {
|
|
319
|
+
* permissions: {read: [IsStaff], update: [IsSuperUser]},
|
|
320
|
+
* postUpdate: (config, prevValue, req) => auditLog(req.user, prevValue, config),
|
|
321
|
+
* })
|
|
227
322
|
* .start();
|
|
228
323
|
* ```
|
|
229
324
|
*/
|
|
@@ -231,6 +326,18 @@ var ConfigurationApp = /** @class */ (function () {
|
|
|
231
326
|
function ConfigurationApp(options) {
|
|
232
327
|
this.options = options;
|
|
233
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Resolves the guard middleware for a route: the consumer's terreno permission
|
|
331
|
+
* functions when supplied, otherwise the default admin-only guard.
|
|
332
|
+
*/
|
|
333
|
+
ConfigurationApp.prototype.guardFor = function (route, method) {
|
|
334
|
+
var _a;
|
|
335
|
+
var perms = (_a = this.options.permissions) === null || _a === void 0 ? void 0 : _a[route];
|
|
336
|
+
if (perms && perms.length > 0) {
|
|
337
|
+
return buildPermissionMiddleware(perms, method);
|
|
338
|
+
}
|
|
339
|
+
return requireAdmin;
|
|
340
|
+
};
|
|
234
341
|
ConfigurationApp.prototype.register = function (app) {
|
|
235
342
|
var _this = this;
|
|
236
343
|
var _a, _b, _c;
|
|
@@ -240,14 +347,14 @@ var ConfigurationApp = /** @class */ (function () {
|
|
|
240
347
|
// Build metadata by inspecting the schema
|
|
241
348
|
var meta = this.buildMetadata(ConfigModel, schema);
|
|
242
349
|
// GET /configuration/meta — schema metadata for the frontend
|
|
243
|
-
app.get("".concat(basePath, "/meta"), (0, auth_1.authenticateMiddleware)(),
|
|
350
|
+
app.get("".concat(basePath, "/meta"), (0, auth_1.authenticateMiddleware)(), this.guardFor("meta", "read"), function (_req, res) {
|
|
244
351
|
return res.json(meta);
|
|
245
352
|
});
|
|
246
353
|
var ConfigStatics = ConfigModel;
|
|
247
354
|
// Discover secret fields once at registration time
|
|
248
355
|
var secretFields = (_c = (_b = ConfigStatics.getSecretFields) === null || _b === void 0 ? void 0 : _b.call(ConfigStatics)) !== null && _c !== void 0 ? _c : [];
|
|
249
356
|
// GET /configuration — current values (secrets redacted)
|
|
250
|
-
app.get("".concat(basePath), (0, auth_1.authenticateMiddleware)(),
|
|
357
|
+
app.get("".concat(basePath), (0, auth_1.authenticateMiddleware)(), this.guardFor("read", "read"), (0, api_1.asyncHandler)(function (_req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
251
358
|
var config, data;
|
|
252
359
|
return __generator(this, function (_a) {
|
|
253
360
|
switch (_a.label) {
|
|
@@ -259,61 +366,74 @@ var ConfigurationApp = /** @class */ (function () {
|
|
|
259
366
|
}
|
|
260
367
|
});
|
|
261
368
|
}); }));
|
|
262
|
-
// PATCH /configuration — update values (secrets redacted in response)
|
|
263
|
-
app.patch("".concat(basePath), (0, auth_1.authenticateMiddleware)(),
|
|
264
|
-
var _a, _s, _i, _v, safeBody, config, data;
|
|
369
|
+
// PATCH /configuration — update values (secret fields stripped; secrets redacted in response)
|
|
370
|
+
app.patch("".concat(basePath), (0, auth_1.authenticateMiddleware)(), this.guardFor("update", "update"), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
371
|
+
var _a, _s, _i, _v, rest, safeBody, prevValue, before, config, data;
|
|
265
372
|
var _b, _c;
|
|
266
373
|
return __generator(this, function (_d) {
|
|
267
374
|
switch (_d.label) {
|
|
268
375
|
case 0:
|
|
269
|
-
_a = req.body, _s = _a._singleton, _i = _a._id, _v = _a.__v,
|
|
270
|
-
|
|
376
|
+
_a = req.body, _s = _a._singleton, _i = _a._id, _v = _a.__v, rest = __rest(_a, ["_singleton", "_id", "__v"]);
|
|
377
|
+
safeBody = stripSecretFields(rest, secretFields);
|
|
378
|
+
if (!this.options.preUpdate) return [3 /*break*/, 2];
|
|
379
|
+
return [4 /*yield*/, this.options.preUpdate(safeBody, req)];
|
|
271
380
|
case 1:
|
|
381
|
+
safeBody = _d.sent();
|
|
382
|
+
// Re-strip after the hook: preUpdate receives the raw request and could
|
|
383
|
+
// otherwise (re)introduce secret paths. Secrets must never persist here.
|
|
384
|
+
safeBody = stripSecretFields(safeBody, secretFields);
|
|
385
|
+
_d.label = 2;
|
|
386
|
+
case 2:
|
|
387
|
+
prevValue = {};
|
|
388
|
+
if (!this.options.postUpdate) return [3 /*break*/, 4];
|
|
389
|
+
return [4 /*yield*/, ConfigStatics.getConfig()];
|
|
390
|
+
case 3:
|
|
391
|
+
before = _d.sent();
|
|
392
|
+
prevValue = redactSecrets(before.toJSON(), secretFields);
|
|
393
|
+
_d.label = 4;
|
|
394
|
+
case 4: return [4 /*yield*/, ConfigStatics.updateConfig(safeBody)];
|
|
395
|
+
case 5:
|
|
272
396
|
config = _d.sent();
|
|
273
397
|
logger_1.logger.info("Configuration updated by ".concat((_c = (_b = req.user) === null || _b === void 0 ? void 0 : _b.email) !== null && _c !== void 0 ? _c : "unknown"));
|
|
274
398
|
data = redactSecrets(config.toJSON(), secretFields);
|
|
275
|
-
return [
|
|
399
|
+
if (!this.options.postUpdate) return [3 /*break*/, 7];
|
|
400
|
+
return [4 /*yield*/, this.options.postUpdate(data, prevValue, req)];
|
|
401
|
+
case 6:
|
|
402
|
+
_d.sent();
|
|
403
|
+
_d.label = 7;
|
|
404
|
+
case 7: return [2 /*return*/, res.json({ data: data })];
|
|
276
405
|
}
|
|
277
406
|
});
|
|
278
407
|
}); }));
|
|
279
|
-
// POST /configuration/list-secrets —
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
var
|
|
283
|
-
return __generator(this, function (
|
|
284
|
-
switch (
|
|
408
|
+
// POST /configuration/list-secrets — read-only status of each secret field.
|
|
409
|
+
// Never resolves values into the document and never returns secret values.
|
|
410
|
+
var validateSecretsHandler = (0, api_1.asyncHandler)(function (_req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
411
|
+
var resolved, fields;
|
|
412
|
+
return __generator(this, function (_a) {
|
|
413
|
+
switch (_a.label) {
|
|
285
414
|
case 0: return [4 /*yield*/, ConfigStatics.resolveSecrets()];
|
|
286
415
|
case 1:
|
|
287
|
-
resolved =
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
return [4 /*yield*/, ConfigStatics.updateConfig(updates)];
|
|
304
|
-
case 2:
|
|
305
|
-
_c.sent();
|
|
306
|
-
logger_1.logger.info("Refreshed ".concat(resolved.size, "/").concat(secretFields.length, " secrets"));
|
|
307
|
-
_c.label = 3;
|
|
308
|
-
case 3: return [2 /*return*/, res.json({
|
|
309
|
-
message: "Resolved ".concat(resolved.size, "/").concat(secretFields.length, " secrets."),
|
|
310
|
-
resolved: resolved.size,
|
|
311
|
-
secretFields: secretFields.map(function (s) { return ({ path: s.path, secretName: s.secretName }); }),
|
|
312
|
-
total: secretFields.length,
|
|
313
|
-
})];
|
|
416
|
+
resolved = _a.sent();
|
|
417
|
+
fields = secretFields.map(function (s) { return ({
|
|
418
|
+
isConfigured: resolved.has(s.path),
|
|
419
|
+
path: s.path,
|
|
420
|
+
resolvable: resolved.has(s.path),
|
|
421
|
+
secretName: s.secretName,
|
|
422
|
+
version: s.version,
|
|
423
|
+
}); });
|
|
424
|
+
logger_1.logger.info("Validated ".concat(resolved.size, "/").concat(secretFields.length, " secrets (read-only)"));
|
|
425
|
+
return [2 /*return*/, res.json({
|
|
426
|
+
message: "".concat(resolved.size, "/").concat(secretFields.length, " secrets resolvable."),
|
|
427
|
+
resolved: resolved.size,
|
|
428
|
+
secretFields: fields,
|
|
429
|
+
total: secretFields.length,
|
|
430
|
+
})];
|
|
314
431
|
}
|
|
315
432
|
});
|
|
316
|
-
}); })
|
|
433
|
+
}); });
|
|
434
|
+
app.post("".concat(basePath, "/list-secrets"), (0, auth_1.authenticateMiddleware)(), this.guardFor("listSecrets", "read"), validateSecretsHandler);
|
|
435
|
+
// Accurate alias for the read-only validation semantics.
|
|
436
|
+
app.post("".concat(basePath, "/validate-secrets"), (0, auth_1.authenticateMiddleware)(), this.guardFor("listSecrets", "read"), validateSecretsHandler);
|
|
317
437
|
logger_1.logger.info("Configuration routes mounted at ".concat(basePath));
|
|
318
438
|
};
|
|
319
439
|
/**
|
|
@@ -6,13 +6,27 @@ export interface SecretFieldMeta {
|
|
|
6
6
|
path: string;
|
|
7
7
|
secretProvider?: string;
|
|
8
8
|
secretName: string;
|
|
9
|
+
/**
|
|
10
|
+
* Optional secret version to pin resolution to. When omitted the provider
|
|
11
|
+
* resolves the latest version. Discovered from the `secretVersion` schema
|
|
12
|
+
* path option.
|
|
13
|
+
*/
|
|
14
|
+
version?: string;
|
|
9
15
|
}
|
|
10
16
|
/**
|
|
11
17
|
* Interface for adapters that resolve secret values from external providers.
|
|
12
18
|
*/
|
|
13
19
|
export interface SecretProvider {
|
|
14
20
|
name: string;
|
|
15
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a secret value by name. Returns `null` when the secret is not found.
|
|
23
|
+
*
|
|
24
|
+
* @param secretName - The secret identifier (short name or provider-specific path).
|
|
25
|
+
* @param version - Optional version to pin resolution to. Providers that do not
|
|
26
|
+
* support versioning (e.g. environment variables) ignore this parameter. When
|
|
27
|
+
* omitted, the latest version is resolved.
|
|
28
|
+
*/
|
|
29
|
+
getSecret(secretName: string, version?: string): Promise<string | null>;
|
|
16
30
|
}
|
|
17
31
|
/**
|
|
18
32
|
* Options passed to configurationPlugin.
|
|
@@ -23,6 +37,18 @@ export interface ConfigurationPluginOptions {
|
|
|
23
37
|
* Typically set during app startup so the model can resolve secrets on demand.
|
|
24
38
|
*/
|
|
25
39
|
secretProvider?: SecretProvider;
|
|
40
|
+
/**
|
|
41
|
+
* When `true`, adds a `_singleton` sentinel field with a unique index to
|
|
42
|
+
* enforce the singleton constraint at the database level.
|
|
43
|
+
*
|
|
44
|
+
* Defaults to `false`. Leave this off when the consuming app already enforces
|
|
45
|
+
* a single non-deleted document via the pre-save guard (the default behavior)
|
|
46
|
+
* or via its own indexes/soft-delete plugin, to avoid double-enforcement and
|
|
47
|
+
* conflicting indexes.
|
|
48
|
+
*
|
|
49
|
+
* @defaultValue false
|
|
50
|
+
*/
|
|
51
|
+
enforceSingletonIndex?: boolean;
|
|
26
52
|
}
|
|
27
53
|
/**
|
|
28
54
|
* All dot-notation paths for a type T.
|
|
@@ -50,14 +76,26 @@ export interface ConfigurationStatics<T extends object> {
|
|
|
50
76
|
getConfig(): Promise<T & Document>;
|
|
51
77
|
/** Get a specific value by dot-notation key. */
|
|
52
78
|
getConfig<P extends Paths<T>>(key: P): Promise<PathValue<T, P>>;
|
|
53
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Update the singleton configuration document.
|
|
81
|
+
*
|
|
82
|
+
* The patch is flattened into MongoDB dotted paths and applied with
|
|
83
|
+
* `findOneAndUpdate({$set})`. This preserves sibling fields inside nested
|
|
84
|
+
* subdocuments when a partial nested patch is supplied, and tolerates legacy /
|
|
85
|
+
* out-of-schema fields already persisted on the document (unlike a full
|
|
86
|
+
* `doc.save()`, which throws under `strict: "throw"`).
|
|
87
|
+
*/
|
|
54
88
|
updateConfig(updates: DeepPartial<T>): Promise<T & Document>;
|
|
55
89
|
/** Get secret field metadata discovered from the schema. */
|
|
56
90
|
getSecretFields(): SecretFieldMeta[];
|
|
57
91
|
/**
|
|
58
92
|
* Resolve all secret field values from a provider.
|
|
59
93
|
* Uses the provider passed here, or falls back to the one configured in the plugin options.
|
|
60
|
-
* Returns
|
|
94
|
+
* Returns an **in-memory** map of path -> value for programmatic use (startup
|
|
95
|
+
* self-checks, request-time resolution).
|
|
96
|
+
*
|
|
97
|
+
* This method never persists resolved values. Secret material must never be
|
|
98
|
+
* written to the configuration document.
|
|
61
99
|
*/
|
|
62
100
|
resolveSecrets(provider?: SecretProvider): Promise<Map<string, string>>;
|
|
63
101
|
}
|
|
@@ -77,17 +115,35 @@ export interface ConfigurationStatics<T extends object> {
|
|
|
77
115
|
*/
|
|
78
116
|
export interface ConfigurationModel<T extends object> extends Model<T>, ConfigurationStatics<T> {
|
|
79
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Flattens a nested patch into MongoDB-style dotted paths, recursing into plain
|
|
120
|
+
* objects only; arrays and other values are treated as leaves.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* flattenToDotPaths({a: {b: 1}}) // => [["a.b", 1]]
|
|
124
|
+
*/
|
|
125
|
+
export declare const flattenToDotPaths: (obj: Record<string, unknown>, prefix?: string) => Array<[string, unknown]>;
|
|
80
126
|
/**
|
|
81
127
|
* Mongoose schema plugin that adds singleton configuration behavior.
|
|
82
128
|
*
|
|
83
129
|
* Adds:
|
|
84
|
-
* - Pre-save hook enforcing exactly one document
|
|
130
|
+
* - Pre-save hook enforcing exactly one non-deleted document (soft-delete aware
|
|
131
|
+
* when the schema has a `deleted` path, e.g. via `isDeletedPlugin`)
|
|
85
132
|
* - `getConfig()` static: fetches or creates the singleton (full doc or keyed value)
|
|
86
|
-
* - `updateConfig(updates)` static: patches the singleton
|
|
133
|
+
* - `updateConfig(updates)` static: patches the singleton via `findOneAndUpdate({$set})`
|
|
134
|
+
* with dotted paths (preserves sibling subdoc fields; tolerates legacy fields)
|
|
87
135
|
* - `getSecretFields()` static: returns metadata for fields with `secret: true`
|
|
88
|
-
* - `resolveSecrets(provider?)` static:
|
|
136
|
+
* - `resolveSecrets(provider?)` static: resolves secret values into an in-memory map,
|
|
137
|
+
* using the plugin provider by default (never persists values)
|
|
138
|
+
* - Hard-delete blockers (`deleteOne`/`deleteMany`/`findOneAndDelete`); soft deletes
|
|
139
|
+
* (setting `deleted: true`) are allowed
|
|
140
|
+
*
|
|
141
|
+
* Soft deletes are allowed and a soft-deleted document does not block creating a
|
|
142
|
+
* new singleton. The `_singleton` unique index is opt-in via
|
|
143
|
+
* `enforceSingletonIndex` (default off).
|
|
89
144
|
*
|
|
90
|
-
* Mark fields as secrets using schema path options
|
|
145
|
+
* Mark fields as secrets using schema path options. Pin a version with the
|
|
146
|
+
* optional `secretVersion` option:
|
|
91
147
|
* ```typescript
|
|
92
148
|
* const configSchema = new Schema({
|
|
93
149
|
* apiKey: {
|
|
@@ -95,6 +151,7 @@ export interface ConfigurationModel<T extends object> extends Model<T>, Configur
|
|
|
95
151
|
* description: "Third-party API key",
|
|
96
152
|
* secret: true,
|
|
97
153
|
* secretName: "my-api-key",
|
|
154
|
+
* secretVersion: "3", // optional — resolves "latest" when omitted
|
|
98
155
|
* },
|
|
99
156
|
* });
|
|
100
157
|
* configSchema.plugin(configurationPlugin, {secretProvider: new EnvSecretProvider()});
|