@terreno/api 0.19.0 → 0.20.1

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.
@@ -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: fetches secret values, using the plugin provider by default
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
- * Mark fields as secrets using schema path options:
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
- // Add a sentinel field with a unique index to enforce singleton at the DB level.
85
- // All config documents get _singleton: "config", and the unique index prevents duplicates.
86
- schema.add({
87
- _singleton: {
88
- default: "config",
89
- description: "Sentinel field enforcing singleton constraint",
90
- immutable: true,
91
- select: false,
92
- type: String,
93
- },
94
- });
95
- schema.index({ _singleton: 1 }, { unique: true });
96
- // Enforce singleton: only one document allowed (application-level guard)
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
- return [4 /*yield*/, this.constructor.exists({})];
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 e_1, _a;
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 (e_1_1) { e_1 = { error: e_1_1 }; }
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 (e_1) throw e_1.error; }
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 (race-safe)
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 config;
200
- return __generator(this, function (_a) {
201
- switch (_a.label) {
202
- case 0: return [4 /*yield*/, this.getConfig()];
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
- config = _a.sent();
205
- Object.assign(config, updates);
206
- return [4 /*yield*/, config.save()];
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
- _a.sent();
209
- return [2 /*return*/, config];
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 e_2, _a;
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 (e_2_1) { e_2 = { error: e_2_1 }; }
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 (e_2) throw e_2.error; }
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)("adds a _singleton field with unique index", function () {
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
- getSecret(secretName: string): Promise<string | null>;
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
- getSecret(secretName: string): Promise<string | null>;
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
  }