@terreno/api 0.19.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/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/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/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/secretProviders.test.ts +186 -0
- package/src/secretProviders.ts +145 -5
|
@@ -46,25 +46,108 @@ var __values = (this && this.__values) || function(o) {
|
|
|
46
46
|
};
|
|
47
47
|
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
48
48
|
};
|
|
49
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
50
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
51
|
+
if (!m) return o;
|
|
52
|
+
var i = m.call(o), r, ar = [], e;
|
|
53
|
+
try {
|
|
54
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
55
|
+
}
|
|
56
|
+
catch (error) { e = { error: error }; }
|
|
57
|
+
finally {
|
|
58
|
+
try {
|
|
59
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
60
|
+
}
|
|
61
|
+
finally { if (e) throw e.error; }
|
|
62
|
+
}
|
|
63
|
+
return ar;
|
|
64
|
+
};
|
|
65
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
66
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
67
|
+
if (ar || !(i in from)) {
|
|
68
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
69
|
+
ar[i] = from[i];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
73
|
+
};
|
|
49
74
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
-
exports.configurationPlugin = void 0;
|
|
75
|
+
exports.configurationPlugin = exports.flattenToDotPaths = void 0;
|
|
51
76
|
var errors_1 = require("./errors");
|
|
52
77
|
var logger_1 = require("./logger");
|
|
53
78
|
var plugins_1 = require("./plugins");
|
|
54
79
|
// ---------------------------------------------------------------------------
|
|
80
|
+
// Helpers
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
/**
|
|
83
|
+
* Flattens a nested patch into MongoDB-style dotted paths, recursing into plain
|
|
84
|
+
* objects only; arrays and other values are treated as leaves.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* flattenToDotPaths({a: {b: 1}}) // => [["a.b", 1]]
|
|
88
|
+
*/
|
|
89
|
+
var flattenToDotPaths = function (obj, prefix) {
|
|
90
|
+
var e_1, _a;
|
|
91
|
+
if (prefix === void 0) { prefix = ""; }
|
|
92
|
+
var out = [];
|
|
93
|
+
try {
|
|
94
|
+
for (var _b = __values(Object.entries(obj)), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
95
|
+
var _d = __read(_c.value, 2), key = _d[0], value = _d[1];
|
|
96
|
+
var path = prefix ? "".concat(prefix, ".").concat(key) : key;
|
|
97
|
+
var isPlainObject = value !== null &&
|
|
98
|
+
typeof value === "object" &&
|
|
99
|
+
!Array.isArray(value) &&
|
|
100
|
+
Object.getPrototypeOf(value) === Object.prototype;
|
|
101
|
+
if (isPlainObject) {
|
|
102
|
+
out.push.apply(out, __spreadArray([], __read((0, exports.flattenToDotPaths)(value, path)), false));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
out.push([path, value]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
110
|
+
finally {
|
|
111
|
+
try {
|
|
112
|
+
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
113
|
+
}
|
|
114
|
+
finally { if (e_1) throw e_1.error; }
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
};
|
|
118
|
+
exports.flattenToDotPaths = flattenToDotPaths;
|
|
119
|
+
/**
|
|
120
|
+
* Builds the filter used to locate the singleton document. When the schema is
|
|
121
|
+
* soft-delete aware (has a `deleted` path, e.g. via `isDeletedPlugin`), the
|
|
122
|
+
* singleton is "the one non-deleted document"; otherwise any document matches.
|
|
123
|
+
*/
|
|
124
|
+
var buildSingletonFilter = function (schema) {
|
|
125
|
+
return schema.path("deleted") ? { deleted: false } : {};
|
|
126
|
+
};
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
55
128
|
// Plugin
|
|
56
129
|
// ---------------------------------------------------------------------------
|
|
57
130
|
/**
|
|
58
131
|
* Mongoose schema plugin that adds singleton configuration behavior.
|
|
59
132
|
*
|
|
60
133
|
* Adds:
|
|
61
|
-
* - Pre-save hook enforcing exactly one document
|
|
134
|
+
* - Pre-save hook enforcing exactly one non-deleted document (soft-delete aware
|
|
135
|
+
* when the schema has a `deleted` path, e.g. via `isDeletedPlugin`)
|
|
62
136
|
* - `getConfig()` static: fetches or creates the singleton (full doc or keyed value)
|
|
63
|
-
* - `updateConfig(updates)` static: patches the singleton
|
|
137
|
+
* - `updateConfig(updates)` static: patches the singleton via `findOneAndUpdate({$set})`
|
|
138
|
+
* with dotted paths (preserves sibling subdoc fields; tolerates legacy fields)
|
|
64
139
|
* - `getSecretFields()` static: returns metadata for fields with `secret: true`
|
|
65
|
-
* - `resolveSecrets(provider?)` static:
|
|
140
|
+
* - `resolveSecrets(provider?)` static: resolves secret values into an in-memory map,
|
|
141
|
+
* using the plugin provider by default (never persists values)
|
|
142
|
+
* - Hard-delete blockers (`deleteOne`/`deleteMany`/`findOneAndDelete`); soft deletes
|
|
143
|
+
* (setting `deleted: true`) are allowed
|
|
66
144
|
*
|
|
67
|
-
*
|
|
145
|
+
* Soft deletes are allowed and a soft-deleted document does not block creating a
|
|
146
|
+
* new singleton. The `_singleton` unique index is opt-in via
|
|
147
|
+
* `enforceSingletonIndex` (default off).
|
|
148
|
+
*
|
|
149
|
+
* Mark fields as secrets using schema path options. Pin a version with the
|
|
150
|
+
* optional `secretVersion` option:
|
|
68
151
|
* ```typescript
|
|
69
152
|
* const configSchema = new Schema({
|
|
70
153
|
* apiKey: {
|
|
@@ -72,6 +155,7 @@ var plugins_1 = require("./plugins");
|
|
|
72
155
|
* description: "Third-party API key",
|
|
73
156
|
* secret: true,
|
|
74
157
|
* secretName: "my-api-key",
|
|
158
|
+
* secretVersion: "3", // optional — resolves "latest" when omitted
|
|
75
159
|
* },
|
|
76
160
|
* });
|
|
77
161
|
* configSchema.plugin(configurationPlugin, {secretProvider: new EnvSecretProvider()});
|
|
@@ -81,27 +165,34 @@ var configurationPlugin = function (schema, options) {
|
|
|
81
165
|
var pluginOptions = options !== null && options !== void 0 ? options : {};
|
|
82
166
|
// Apply findOneOrNone so the singleton lookup avoids bare Model.findOne (idempotent).
|
|
83
167
|
(0, plugins_1.findOneOrNone)(schema);
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
168
|
+
// Optionally add a sentinel field with a unique index to enforce the singleton
|
|
169
|
+
// at the database level. This is opt-in (default off) so it does not conflict
|
|
170
|
+
// with consumers that already enforce a single non-deleted document via the
|
|
171
|
+
// pre-save guard below or via their own soft-delete plugin/indexes.
|
|
172
|
+
if (pluginOptions.enforceSingletonIndex) {
|
|
173
|
+
schema.add({
|
|
174
|
+
_singleton: {
|
|
175
|
+
default: "config",
|
|
176
|
+
description: "Sentinel field enforcing singleton constraint",
|
|
177
|
+
immutable: true,
|
|
178
|
+
select: false,
|
|
179
|
+
type: String,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
schema.index({ _singleton: 1 }, { unique: true });
|
|
183
|
+
}
|
|
184
|
+
// Enforce singleton: only one non-deleted document allowed (application-level
|
|
185
|
+
// guard). Soft-delete-aware: a soft-deleted document does not block creating a
|
|
186
|
+
// new singleton.
|
|
97
187
|
schema.pre("save", function () {
|
|
98
188
|
return __awaiter(this, void 0, void 0, function () {
|
|
99
|
-
var existing;
|
|
189
|
+
var filter, existing;
|
|
100
190
|
return __generator(this, function (_a) {
|
|
101
191
|
switch (_a.label) {
|
|
102
192
|
case 0:
|
|
103
193
|
if (!this.isNew) return [3 /*break*/, 2];
|
|
104
|
-
|
|
194
|
+
filter = buildSingletonFilter(schema);
|
|
195
|
+
return [4 /*yield*/, this.constructor.exists(filter)];
|
|
105
196
|
case 1:
|
|
106
197
|
existing = _a.sent();
|
|
107
198
|
if (existing) {
|
|
@@ -135,14 +226,15 @@ var configurationPlugin = function (schema, options) {
|
|
|
135
226
|
// Static: get the singleton configuration document or a value at a path (race-safe via upsert)
|
|
136
227
|
schema.statics.getConfig = function (key) {
|
|
137
228
|
return __awaiter(this, void 0, void 0, function () {
|
|
138
|
-
var findSingleton, config, created, err_1, parts, value, parts_1, parts_1_1, part;
|
|
139
|
-
var
|
|
229
|
+
var singletonFilter, findSingleton, config, created, err_1, parts, value, parts_1, parts_1_1, part;
|
|
230
|
+
var e_2, _a;
|
|
140
231
|
var _this = this;
|
|
141
232
|
return __generator(this, function (_b) {
|
|
142
233
|
switch (_b.label) {
|
|
143
234
|
case 0:
|
|
235
|
+
singletonFilter = buildSingletonFilter(this.schema);
|
|
144
236
|
findSingleton = function () {
|
|
145
|
-
return _this.findOneOrNone(
|
|
237
|
+
return _this.findOneOrNone(singletonFilter);
|
|
146
238
|
};
|
|
147
239
|
return [4 /*yield*/, findSingleton()];
|
|
148
240
|
case 1:
|
|
@@ -181,32 +273,61 @@ var configurationPlugin = function (schema, options) {
|
|
|
181
273
|
value = value[part];
|
|
182
274
|
}
|
|
183
275
|
}
|
|
184
|
-
catch (
|
|
276
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
185
277
|
finally {
|
|
186
278
|
try {
|
|
187
279
|
if (parts_1_1 && !parts_1_1.done && (_a = parts_1.return)) _a.call(parts_1);
|
|
188
280
|
}
|
|
189
|
-
finally { if (
|
|
281
|
+
finally { if (e_2) throw e_2.error; }
|
|
190
282
|
}
|
|
191
283
|
return [2 /*return*/, value];
|
|
192
284
|
}
|
|
193
285
|
});
|
|
194
286
|
});
|
|
195
287
|
};
|
|
196
|
-
// Static: update the singleton configuration document
|
|
288
|
+
// Static: update the singleton configuration document via $set dotted paths.
|
|
289
|
+
// Flattening to dotted paths preserves sibling subdoc fields and tolerates
|
|
290
|
+
// legacy/out-of-schema fields already persisted on the document.
|
|
197
291
|
schema.statics.updateConfig = function (updates) {
|
|
198
292
|
return __awaiter(this, void 0, void 0, function () {
|
|
199
|
-
var
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
293
|
+
var singletonFilter, setFields, _a, _b, _c, path, value, updated;
|
|
294
|
+
var e_3, _d;
|
|
295
|
+
return __generator(this, function (_e) {
|
|
296
|
+
switch (_e.label) {
|
|
297
|
+
case 0:
|
|
298
|
+
singletonFilter = buildSingletonFilter(this.schema);
|
|
299
|
+
setFields = {};
|
|
300
|
+
try {
|
|
301
|
+
for (_a = __values((0, exports.flattenToDotPaths)(updates)), _b = _a.next(); !_b.done; _b = _a.next()) {
|
|
302
|
+
_c = __read(_b.value, 2), path = _c[0], value = _c[1];
|
|
303
|
+
setFields[path] = value;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (e_3_1) { e_3 = { error: e_3_1 }; }
|
|
307
|
+
finally {
|
|
308
|
+
try {
|
|
309
|
+
if (_b && !_b.done && (_d = _a.return)) _d.call(_a);
|
|
310
|
+
}
|
|
311
|
+
finally { if (e_3) throw e_3.error; }
|
|
312
|
+
}
|
|
313
|
+
// Nothing to set — return the current singleton (creating it if missing).
|
|
314
|
+
if (Object.keys(setFields).length === 0) {
|
|
315
|
+
return [2 /*return*/, this.getConfig()];
|
|
316
|
+
}
|
|
317
|
+
return [4 /*yield*/, this.findOneAndUpdate(singletonFilter, { $set: setFields }, { new: true, runValidators: true })];
|
|
203
318
|
case 1:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
319
|
+
updated = _e.sent();
|
|
320
|
+
if (updated) {
|
|
321
|
+
return [2 /*return*/, updated];
|
|
322
|
+
}
|
|
323
|
+
// No singleton yet — create one (with subdocument defaults applied), then
|
|
324
|
+
// apply the patch.
|
|
325
|
+
return [4 /*yield*/, this.getConfig()];
|
|
207
326
|
case 2:
|
|
208
|
-
|
|
209
|
-
|
|
327
|
+
// No singleton yet — create one (with subdocument defaults applied), then
|
|
328
|
+
// apply the patch.
|
|
329
|
+
_e.sent();
|
|
330
|
+
return [2 /*return*/, this.findOneAndUpdate(singletonFilter, { $set: setFields }, { new: true, runValidators: true }).orFail()];
|
|
210
331
|
}
|
|
211
332
|
});
|
|
212
333
|
});
|
|
@@ -223,6 +344,7 @@ var configurationPlugin = function (schema, options) {
|
|
|
223
344
|
path: prefix ? "".concat(prefix, ".").concat(pathName) : pathName,
|
|
224
345
|
secretName: (_a = opts.secretName) !== null && _a !== void 0 ? _a : pathName,
|
|
225
346
|
secretProvider: opts.secretProvider,
|
|
347
|
+
version: opts.secretVersion,
|
|
226
348
|
});
|
|
227
349
|
}
|
|
228
350
|
// Recurse into subschemas
|
|
@@ -238,7 +360,7 @@ var configurationPlugin = function (schema, options) {
|
|
|
238
360
|
schema.statics.resolveSecrets = function (provider) {
|
|
239
361
|
return __awaiter(this, void 0, void 0, function () {
|
|
240
362
|
var resolvedProvider, secrets, resolved, results, failCount, results_1, results_1_1, result;
|
|
241
|
-
var
|
|
363
|
+
var e_4, _a;
|
|
242
364
|
var _this = this;
|
|
243
365
|
return __generator(this, function (_b) {
|
|
244
366
|
switch (_b.label) {
|
|
@@ -254,7 +376,7 @@ var configurationPlugin = function (schema, options) {
|
|
|
254
376
|
var value;
|
|
255
377
|
return __generator(this, function (_a) {
|
|
256
378
|
switch (_a.label) {
|
|
257
|
-
case 0: return [4 /*yield*/, resolvedProvider.getSecret(meta.secretName)];
|
|
379
|
+
case 0: return [4 /*yield*/, resolvedProvider.getSecret(meta.secretName, meta.version)];
|
|
258
380
|
case 1:
|
|
259
381
|
value = _a.sent();
|
|
260
382
|
if (value !== null) {
|
|
@@ -276,12 +398,12 @@ var configurationPlugin = function (schema, options) {
|
|
|
276
398
|
}
|
|
277
399
|
}
|
|
278
400
|
}
|
|
279
|
-
catch (
|
|
401
|
+
catch (e_4_1) { e_4 = { error: e_4_1 }; }
|
|
280
402
|
finally {
|
|
281
403
|
try {
|
|
282
404
|
if (results_1_1 && !results_1_1.done && (_a = results_1.return)) _a.call(results_1);
|
|
283
405
|
}
|
|
284
|
-
finally { if (
|
|
406
|
+
finally { if (e_4) throw e_4.error; }
|
|
285
407
|
}
|
|
286
408
|
if (failCount > 0) {
|
|
287
409
|
logger_1.logger.warn("".concat(failCount, "/").concat(secrets.length, " secrets failed to resolve"));
|
|
@@ -89,6 +89,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
89
89
|
var bun_test_1 = require("bun:test");
|
|
90
90
|
var mongoose_1 = __importStar(require("mongoose"));
|
|
91
91
|
var configurationPlugin_1 = require("./configurationPlugin");
|
|
92
|
+
var plugins_1 = require("./plugins");
|
|
92
93
|
var testConfigSchema = new mongoose_1.Schema({
|
|
93
94
|
apiKey: {
|
|
94
95
|
default: "",
|
|
@@ -133,14 +134,47 @@ var simpleSchema = new mongoose_1.Schema({
|
|
|
133
134
|
});
|
|
134
135
|
simpleSchema.plugin(configurationPlugin_1.configurationPlugin);
|
|
135
136
|
var SimpleConfigModel = (0, mongoose_1.model)("SimpleConfiguration", simpleSchema);
|
|
137
|
+
// --- Schema opting into the _singleton unique index ---
|
|
138
|
+
var indexedSchema = new mongoose_1.Schema({
|
|
139
|
+
value: { default: "default", description: "A value", type: String },
|
|
140
|
+
});
|
|
141
|
+
indexedSchema.plugin(configurationPlugin_1.configurationPlugin, { enforceSingletonIndex: true });
|
|
142
|
+
var IndexedConfigModel = (0, mongoose_1.model)("IndexedConfiguration", indexedSchema);
|
|
143
|
+
// --- Soft-delete-aware schema ---
|
|
144
|
+
var softDeleteSchema = new mongoose_1.Schema({
|
|
145
|
+
value: { default: "default", description: "A value", type: String },
|
|
146
|
+
});
|
|
147
|
+
softDeleteSchema.plugin(configurationPlugin_1.configurationPlugin);
|
|
148
|
+
softDeleteSchema.plugin(plugins_1.isDeletedPlugin);
|
|
149
|
+
var SoftDeleteConfigModel = (0, mongoose_1.model)("SoftDeleteConfiguration", softDeleteSchema);
|
|
150
|
+
// --- Schema with a validated field (enum) for runValidators coverage ---
|
|
151
|
+
var validatedSchema = new mongoose_1.Schema({
|
|
152
|
+
level: {
|
|
153
|
+
default: "low",
|
|
154
|
+
description: "Severity level",
|
|
155
|
+
enum: ["low", "medium", "high"],
|
|
156
|
+
type: String,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
validatedSchema.plugin(configurationPlugin_1.configurationPlugin);
|
|
160
|
+
var ValidatedConfigModel = (0, mongoose_1.model)("ValidatedConfiguration", validatedSchema);
|
|
136
161
|
(0, bun_test_1.describe)("configurationPlugin", function () {
|
|
137
162
|
(0, bun_test_1.describe)("schema setup", function () {
|
|
138
|
-
(0, bun_test_1.it)("
|
|
163
|
+
(0, bun_test_1.it)("does not add a _singleton index by default", function () {
|
|
139
164
|
var indexes = SimpleConfigModel.schema.indexes();
|
|
140
165
|
var singletonIndex = indexes.find(function (_a) {
|
|
141
166
|
var _b = __read(_a, 1), fields = _b[0];
|
|
142
167
|
return fields._singleton !== undefined;
|
|
143
168
|
});
|
|
169
|
+
(0, bun_test_1.expect)(singletonIndex).toBeUndefined();
|
|
170
|
+
(0, bun_test_1.expect)(SimpleConfigModel.schema.path("_singleton")).toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
(0, bun_test_1.it)("adds a _singleton field with unique index when enforceSingletonIndex is true", function () {
|
|
173
|
+
var indexes = IndexedConfigModel.schema.indexes();
|
|
174
|
+
var singletonIndex = indexes.find(function (_a) {
|
|
175
|
+
var _b = __read(_a, 1), fields = _b[0];
|
|
176
|
+
return fields._singleton !== undefined;
|
|
177
|
+
});
|
|
144
178
|
(0, bun_test_1.expect)(singletonIndex).toBeDefined();
|
|
145
179
|
(0, bun_test_1.expect)(singletonIndex[1].unique).toBe(true);
|
|
146
180
|
});
|
|
@@ -256,6 +290,55 @@ var SimpleConfigModel = (0, mongoose_1.model)("SimpleConfiguration", simpleSchem
|
|
|
256
290
|
}
|
|
257
291
|
});
|
|
258
292
|
}); });
|
|
293
|
+
(0, bun_test_1.it)("passes the discovered secret version to the provider", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
294
|
+
var versionedSchema, VersionedModel, received, provider;
|
|
295
|
+
return __generator(this, function (_a) {
|
|
296
|
+
switch (_a.label) {
|
|
297
|
+
case 0:
|
|
298
|
+
versionedSchema = new mongoose_1.Schema({
|
|
299
|
+
token: {
|
|
300
|
+
default: "",
|
|
301
|
+
description: "Pinned secret",
|
|
302
|
+
secret: true,
|
|
303
|
+
secretName: "pinned-token",
|
|
304
|
+
secretVersion: "5",
|
|
305
|
+
type: String,
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
versionedSchema.plugin(configurationPlugin_1.configurationPlugin);
|
|
309
|
+
VersionedModel = (0, mongoose_1.model)("VersionedConfiguration", versionedSchema);
|
|
310
|
+
received = [];
|
|
311
|
+
provider = {
|
|
312
|
+
getSecret: function (name, version) { return __awaiter(void 0, void 0, void 0, function () {
|
|
313
|
+
return __generator(this, function (_a) {
|
|
314
|
+
received.push({ name: name, version: version });
|
|
315
|
+
return [2 /*return*/, "value"];
|
|
316
|
+
});
|
|
317
|
+
}); },
|
|
318
|
+
name: "versioned-provider",
|
|
319
|
+
};
|
|
320
|
+
return [4 /*yield*/, VersionedModel.resolveSecrets(provider)];
|
|
321
|
+
case 1:
|
|
322
|
+
_a.sent();
|
|
323
|
+
(0, bun_test_1.expect)(received).toEqual([{ name: "pinned-token", version: "5" }]);
|
|
324
|
+
return [2 /*return*/];
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}); });
|
|
328
|
+
});
|
|
329
|
+
(0, bun_test_1.describe)("flattenToDotPaths", function () {
|
|
330
|
+
(0, bun_test_1.it)("flattens nested plain objects into dotted paths", function () {
|
|
331
|
+
(0, bun_test_1.expect)((0, configurationPlugin_1.flattenToDotPaths)({ a: { b: 1 } })).toEqual([["a.b", 1]]);
|
|
332
|
+
});
|
|
333
|
+
(0, bun_test_1.it)("treats arrays as leaves", function () {
|
|
334
|
+
(0, bun_test_1.expect)((0, configurationPlugin_1.flattenToDotPaths)({ a: [1, 2] })).toEqual([["a", [1, 2]]]);
|
|
335
|
+
});
|
|
336
|
+
(0, bun_test_1.it)("treats null as a leaf and keeps top-level keys", function () {
|
|
337
|
+
(0, bun_test_1.expect)((0, configurationPlugin_1.flattenToDotPaths)({ a: null, b: "x" })).toEqual([
|
|
338
|
+
["a", null],
|
|
339
|
+
["b", "x"],
|
|
340
|
+
]);
|
|
341
|
+
});
|
|
259
342
|
});
|
|
260
343
|
(0, bun_test_1.describe)("singleton behavior (requires MongoDB)", function () {
|
|
261
344
|
var dbConnected = false;
|
|
@@ -425,6 +508,38 @@ var SimpleConfigModel = (0, mongoose_1.model)("SimpleConfiguration", simpleSchem
|
|
|
425
508
|
}
|
|
426
509
|
});
|
|
427
510
|
}); });
|
|
511
|
+
(0, bun_test_1.it)("runs schema validators on updateConfig (rejects invalid enum)", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
512
|
+
var _a, ok;
|
|
513
|
+
return __generator(this, function (_b) {
|
|
514
|
+
switch (_b.label) {
|
|
515
|
+
case 0:
|
|
516
|
+
if (!dbConnected) {
|
|
517
|
+
return [2 /*return*/];
|
|
518
|
+
}
|
|
519
|
+
_b.label = 1;
|
|
520
|
+
case 1:
|
|
521
|
+
_b.trys.push([1, 3, , 4]);
|
|
522
|
+
return [4 /*yield*/, ValidatedConfigModel.collection.drop()];
|
|
523
|
+
case 2:
|
|
524
|
+
_b.sent();
|
|
525
|
+
return [3 /*break*/, 4];
|
|
526
|
+
case 3:
|
|
527
|
+
_a = _b.sent();
|
|
528
|
+
return [3 /*break*/, 4];
|
|
529
|
+
case 4: return [4 /*yield*/, ValidatedConfigModel.getConfig()];
|
|
530
|
+
case 5:
|
|
531
|
+
_b.sent();
|
|
532
|
+
return [4 /*yield*/, (0, bun_test_1.expect)(ValidatedConfigModel.updateConfig({ level: "bogus" })).rejects.toThrow()];
|
|
533
|
+
case 6:
|
|
534
|
+
_b.sent();
|
|
535
|
+
return [4 /*yield*/, ValidatedConfigModel.updateConfig({ level: "high" })];
|
|
536
|
+
case 7:
|
|
537
|
+
ok = _b.sent();
|
|
538
|
+
(0, bun_test_1.expect)(ok.level).toBe("high");
|
|
539
|
+
return [2 /*return*/];
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}); });
|
|
428
543
|
(0, bun_test_1.it)("prevents deleteOne", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
429
544
|
var err_1;
|
|
430
545
|
return __generator(this, function (_a) {
|
|
@@ -507,4 +622,126 @@ var SimpleConfigModel = (0, mongoose_1.model)("SimpleConfiguration", simpleSchem
|
|
|
507
622
|
});
|
|
508
623
|
}); });
|
|
509
624
|
});
|
|
625
|
+
(0, bun_test_1.describe)("soft-delete-aware singleton (requires MongoDB)", function () {
|
|
626
|
+
var dbConnected = false;
|
|
627
|
+
(0, bun_test_1.beforeAll)(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
628
|
+
var _a;
|
|
629
|
+
return __generator(this, function (_b) {
|
|
630
|
+
switch (_b.label) {
|
|
631
|
+
case 0:
|
|
632
|
+
_b.trys.push([0, 4, , 5]);
|
|
633
|
+
if (!(mongoose_1.default.connection.readyState === 1)) return [3 /*break*/, 1];
|
|
634
|
+
dbConnected = true;
|
|
635
|
+
return [3 /*break*/, 3];
|
|
636
|
+
case 1: return [4 /*yield*/, mongoose_1.default.connect("mongodb://127.0.0.1/terreno-config-test", {
|
|
637
|
+
connectTimeoutMS: 3000,
|
|
638
|
+
serverSelectionTimeoutMS: 3000,
|
|
639
|
+
})];
|
|
640
|
+
case 2:
|
|
641
|
+
_b.sent();
|
|
642
|
+
dbConnected = true;
|
|
643
|
+
_b.label = 3;
|
|
644
|
+
case 3: return [3 /*break*/, 5];
|
|
645
|
+
case 4:
|
|
646
|
+
_a = _b.sent();
|
|
647
|
+
dbConnected = false;
|
|
648
|
+
return [3 /*break*/, 5];
|
|
649
|
+
case 5: return [2 /*return*/];
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
}); });
|
|
653
|
+
(0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
654
|
+
var _a;
|
|
655
|
+
return __generator(this, function (_b) {
|
|
656
|
+
switch (_b.label) {
|
|
657
|
+
case 0:
|
|
658
|
+
if (!dbConnected) {
|
|
659
|
+
return [2 /*return*/];
|
|
660
|
+
}
|
|
661
|
+
_b.label = 1;
|
|
662
|
+
case 1:
|
|
663
|
+
_b.trys.push([1, 3, , 4]);
|
|
664
|
+
return [4 /*yield*/, SoftDeleteConfigModel.collection.drop()];
|
|
665
|
+
case 2:
|
|
666
|
+
_b.sent();
|
|
667
|
+
return [3 /*break*/, 4];
|
|
668
|
+
case 3:
|
|
669
|
+
_a = _b.sent();
|
|
670
|
+
return [3 /*break*/, 4];
|
|
671
|
+
case 4: return [2 /*return*/];
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
}); });
|
|
675
|
+
(0, bun_test_1.it)("operates on the non-deleted singleton", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
676
|
+
var config, updated;
|
|
677
|
+
return __generator(this, function (_a) {
|
|
678
|
+
switch (_a.label) {
|
|
679
|
+
case 0:
|
|
680
|
+
if (!dbConnected) {
|
|
681
|
+
return [2 /*return*/];
|
|
682
|
+
}
|
|
683
|
+
return [4 /*yield*/, SoftDeleteConfigModel.getConfig()];
|
|
684
|
+
case 1:
|
|
685
|
+
config = _a.sent();
|
|
686
|
+
(0, bun_test_1.expect)(config.value).toBe("default");
|
|
687
|
+
return [4 /*yield*/, SoftDeleteConfigModel.updateConfig({ value: "live" })];
|
|
688
|
+
case 2:
|
|
689
|
+
updated = _a.sent();
|
|
690
|
+
(0, bun_test_1.expect)(updated.value).toBe("live");
|
|
691
|
+
(0, bun_test_1.expect)(updated.deleted).toBe(false);
|
|
692
|
+
return [2 /*return*/];
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
}); });
|
|
696
|
+
(0, bun_test_1.it)("allows a new singleton after the existing one is soft-deleted", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
697
|
+
var first, second;
|
|
698
|
+
return __generator(this, function (_a) {
|
|
699
|
+
switch (_a.label) {
|
|
700
|
+
case 0:
|
|
701
|
+
if (!dbConnected) {
|
|
702
|
+
return [2 /*return*/];
|
|
703
|
+
}
|
|
704
|
+
return [4 /*yield*/, SoftDeleteConfigModel.getConfig()];
|
|
705
|
+
case 1:
|
|
706
|
+
first = _a.sent();
|
|
707
|
+
// Soft delete by setting deleted: true (allowed)
|
|
708
|
+
first.deleted = true;
|
|
709
|
+
return [4 /*yield*/, first.save()];
|
|
710
|
+
case 2:
|
|
711
|
+
_a.sent();
|
|
712
|
+
return [4 /*yield*/, SoftDeleteConfigModel.getConfig()];
|
|
713
|
+
case 3:
|
|
714
|
+
second = _a.sent();
|
|
715
|
+
(0, bun_test_1.expect)(second.deleted).toBe(false);
|
|
716
|
+
(0, bun_test_1.expect)(second._id.toString()).not.toBe(first._id.toString());
|
|
717
|
+
return [2 /*return*/];
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
}); });
|
|
721
|
+
(0, bun_test_1.it)("does not let updateConfig touch a soft-deleted document", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
722
|
+
var first, updated;
|
|
723
|
+
return __generator(this, function (_a) {
|
|
724
|
+
switch (_a.label) {
|
|
725
|
+
case 0:
|
|
726
|
+
if (!dbConnected) {
|
|
727
|
+
return [2 /*return*/];
|
|
728
|
+
}
|
|
729
|
+
return [4 /*yield*/, SoftDeleteConfigModel.getConfig()];
|
|
730
|
+
case 1:
|
|
731
|
+
first = _a.sent();
|
|
732
|
+
first.deleted = true;
|
|
733
|
+
return [4 /*yield*/, first.save()];
|
|
734
|
+
case 2:
|
|
735
|
+
_a.sent();
|
|
736
|
+
return [4 /*yield*/, SoftDeleteConfigModel.updateConfig({ value: "fresh" })];
|
|
737
|
+
case 3:
|
|
738
|
+
updated = _a.sent();
|
|
739
|
+
(0, bun_test_1.expect)(updated.deleted).toBe(false);
|
|
740
|
+
(0, bun_test_1.expect)(updated.value).toBe("fresh");
|
|
741
|
+
(0, bun_test_1.expect)(updated._id.toString()).not.toBe(first._id.toString());
|
|
742
|
+
return [2 /*return*/];
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
}); });
|
|
746
|
+
});
|
|
510
747
|
});
|
|
@@ -15,7 +15,11 @@ import type { SecretProvider } from "./configurationPlugin";
|
|
|
15
15
|
*/
|
|
16
16
|
export declare class EnvSecretProvider implements SecretProvider {
|
|
17
17
|
name: string;
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a secret from an environment variable. Environment variables have no
|
|
20
|
+
* versions, so the optional `version` parameter is ignored.
|
|
21
|
+
*/
|
|
22
|
+
getSecret(secretName: string, _version?: string): Promise<string | null>;
|
|
19
23
|
}
|
|
20
24
|
/**
|
|
21
25
|
* Options for GcpSecretProvider.
|
|
@@ -43,5 +47,78 @@ export declare class GcpSecretProvider implements SecretProvider {
|
|
|
43
47
|
private client;
|
|
44
48
|
constructor(options: GcpSecretProviderOptions);
|
|
45
49
|
private getClient;
|
|
46
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Resolve a secret from Google Cloud Secret Manager.
|
|
52
|
+
*
|
|
53
|
+
* @param secretName - A short secret id (e.g. "openai-api-key") or a full
|
|
54
|
+
* resource path (e.g. "projects/p/secrets/s" or
|
|
55
|
+
* "projects/p/secrets/s/versions/3").
|
|
56
|
+
* @param version - Optional version to resolve when `secretName` is a short id
|
|
57
|
+
* (e.g. "3"). Defaults to "latest". Ignored when `secretName` already
|
|
58
|
+
* contains an explicit `/versions/...` suffix.
|
|
59
|
+
*/
|
|
60
|
+
getSecret(secretName: string, version?: string): Promise<string | null>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Secret provider that delegates to an ordered list of providers, returning the
|
|
64
|
+
* first non-null result.
|
|
65
|
+
*
|
|
66
|
+
* A provider that throws is warn-logged (secret name only — never the value) and
|
|
67
|
+
* resolution falls through to the next provider. This makes it easy to compose a
|
|
68
|
+
* primary provider with a fallback, e.g. GCP with an environment-variable
|
|
69
|
+
* fallback:
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const provider = new CompositeSecretProvider([
|
|
74
|
+
* new GcpSecretProvider({projectId: "my-project"}),
|
|
75
|
+
* new EnvSecretProvider(),
|
|
76
|
+
* ]);
|
|
77
|
+
* const key = await provider.getSecret("openai-api-key");
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export declare class CompositeSecretProvider implements SecretProvider {
|
|
81
|
+
name: string;
|
|
82
|
+
private providers;
|
|
83
|
+
constructor(providers: SecretProvider[]);
|
|
84
|
+
getSecret(secretName: string, version?: string): Promise<string | null>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Options for CachingSecretProvider.
|
|
88
|
+
*/
|
|
89
|
+
export interface CachingSecretProviderOptions {
|
|
90
|
+
/** Time-to-live for cached values, in milliseconds. Defaults to 60_000 (1 minute). */
|
|
91
|
+
ttlMs?: number;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Secret provider that wraps any provider with an in-memory TTL cache.
|
|
95
|
+
*
|
|
96
|
+
* Cache entries are keyed by `secretName@version` so that pinned versions are
|
|
97
|
+
* cached independently. `null` results (secret not found) are cached too, to
|
|
98
|
+
* avoid hammering the underlying provider for missing secrets. Secret values are
|
|
99
|
+
* never logged.
|
|
100
|
+
*
|
|
101
|
+
* Use `clear()` to drop the entire cache (e.g. on rotation) or `clearKey()` to
|
|
102
|
+
* invalidate a single secret.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* const provider = new CachingSecretProvider(
|
|
107
|
+
* new CompositeSecretProvider([gcp, env]),
|
|
108
|
+
* {ttlMs: 30_000}
|
|
109
|
+
* );
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export declare class CachingSecretProvider implements SecretProvider {
|
|
113
|
+
name: string;
|
|
114
|
+
private provider;
|
|
115
|
+
private ttlMs;
|
|
116
|
+
private cache;
|
|
117
|
+
constructor(provider: SecretProvider, options?: CachingSecretProviderOptions);
|
|
118
|
+
private cacheKey;
|
|
119
|
+
getSecret(secretName: string, version?: string): Promise<string | null>;
|
|
120
|
+
/** Clears the entire cache. Useful on secret rotation and in tests. */
|
|
121
|
+
clear(): void;
|
|
122
|
+
/** Invalidates a single cached secret by name (and optional version). */
|
|
123
|
+
clearKey(secretName: string, version?: string): void;
|
|
47
124
|
}
|