backend-manager 5.11.6 → 5.12.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.
@@ -96,7 +96,9 @@ URL citations live in the returned `output` (message content) as `annotations` o
96
96
 
97
97
  `provider: 'test'` is the AI analog of the `test` payment processor: a first-class provider that suites drive with directives in the LAST user message, so consumer routes exercise their full loop (Firestore writes, usage, locks, tool execution) against the real emulator with zero paid API calls. It **refuses to run outside development/testing**.
98
98
 
99
- Directives form a sequence consumed across loop turns (call N executes directive N-1, indexed by assistant turns after the last user turn). Directive values must not contain `]]` internally (a trailing JSON `]` is fine):
99
+ Directives form a sequence consumed across loop turns (call N executes directive N-1, indexed by assistant turns after the last user turn). Directive values must not contain `]]` internally (a trailing JSON `]` is fine).
100
+
101
+ The directive source is resolved in order: the last `messages[]` user turn → `message.content` → **`message.settings` values** (flattened raw). That last fallback makes path-based routes (`message: { path, settings }` — the idiomatic BEM prompt-template style) scriptable as-is: embed the directive in whatever request field gets interpolated into the template (e.g. a chat's `message`, a quiz's `topic`).
100
102
 
101
103
  | Directive | Behavior |
102
104
  |---|---|
@@ -27,6 +27,8 @@ For anonymous-to-owner billing, call `setUser()` BEFORE `validate()` — validat
27
27
 
28
28
  Limits are always specified as **monthly** values in product config (e.g., `limits.requests = 100` means 100/month).
29
29
 
30
+ **Negative limits are unlimited.** `limits.requests = -1` means the metric is never rate-limited for that product — `validate()` always resolves and no daily caps apply. A limit of `0` (or a metric missing from `limits`) always rejects.
31
+
30
32
  By default, limits are enforced with **daily caps** to prevent users from burning their entire monthly quota in a single day. Two checks are applied:
31
33
 
32
34
  1. **Flat daily cap**: `ceil(monthlyLimit / daysInMonth)` — max uses per day
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.11.6",
3
+ "version": "5.12.0",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "files": [
@@ -207,14 +207,61 @@ class ServeCommand extends BaseCommand {
207
207
  const certFile = certFiles.find((f) => !f.includes('-key.pem'));
208
208
 
209
209
  if (keyFile && certFile) {
210
- this.log(chalk.gray(' Using existing mkcert certificates from .temp/certs/'));
211
- return { key: keyFile, cert: certFile };
210
+ const problem = this._checkCertProblem(certFile);
211
+
212
+ if (!problem) {
213
+ this.log(chalk.gray(' Using existing mkcert certificates from .temp/certs/'));
214
+ return { key: keyFile, cert: certFile };
215
+ }
216
+
217
+ // Stale certs (expired, or issued by a DIFFERENT machine's mkcert CA — e.g.
218
+ // .temp copied over from another Mac) make browsers reject the proxy outright,
219
+ // so the only thing that responds is the internal plain-HTTP firebase port.
220
+ // Wipe and regenerate against THIS machine's trusted CA.
221
+ this.log(chalk.yellow(` Existing certificates are not usable (${problem}) — regenerating...`));
222
+ jetpack.remove(certsDir);
223
+ jetpack.dir(certsDir);
212
224
  }
213
225
 
214
226
  // Try to generate with mkcert
215
227
  return this._generateMkcertCerts(certsDir);
216
228
  }
217
229
 
230
+ // Returns a reason string when the existing cert must be regenerated, or null
231
+ // when it's usable: unexpired AND signed by this machine's trusted mkcert root CA.
232
+ _checkCertProblem(certFile) {
233
+ const { X509Certificate } = require('crypto');
234
+ const { execSync } = require('child_process');
235
+
236
+ let cert;
237
+ try {
238
+ cert = new X509Certificate(fs.readFileSync(certFile));
239
+ } catch (e) {
240
+ return 'unreadable certificate';
241
+ }
242
+
243
+ if (new Date(cert.validTo) <= new Date()) {
244
+ return `expired ${cert.validTo}`;
245
+ }
246
+
247
+ // Verify the signature chains to the CURRENT mkcert root CA. If mkcert (or its
248
+ // root) isn't available we can't verify — keep the existing certs rather than
249
+ // breaking the no-mkcert fallback path.
250
+ try {
251
+ const caRoot = execSync('mkcert -CAROOT', { encoding: 'utf8' }).trim();
252
+ const ca = new X509Certificate(fs.readFileSync(path.join(caRoot, 'rootCA.pem')));
253
+
254
+ if (!cert.verify(ca.publicKey)) {
255
+ const issuerCN = cert.issuer.split('\n').find((line) => line.startsWith('CN=')) || cert.issuer;
256
+ return `issued by a different CA (${issuerCN})`;
257
+ }
258
+ } catch (e) {
259
+ return null;
260
+ }
261
+
262
+ return null;
263
+ }
264
+
218
265
  async _generateMkcertCerts(certsDir) {
219
266
  try {
220
267
  await powertools.execute('which mkcert', { log: false });
@@ -170,6 +170,13 @@ Usage.prototype.validate = function (name, options) {
170
170
  return _reject();
171
171
  }
172
172
 
173
+ // Negative limits are unlimited (product config convention: -1)
174
+ if (allowed < 0) {
175
+ self.log(`Usage.validate(): Unlimited limit (${allowed}) for ${name}`);
176
+
177
+ return resolve(true);
178
+ }
179
+
173
180
  // Check if they have a white list key
174
181
  const hasWhitelistKey = self.options.whitelistKeys.some((key) => key && key === self?.user?.api?.privateKey);
175
182
  if (hasWhitelistKey) {
@@ -367,9 +374,9 @@ Usage.prototype.getDailyAllowance = function (name) {
367
374
  return null;
368
375
  }
369
376
 
370
- // Get the monthly limit
377
+ // Get the monthly limit (negative limits are unlimited — no daily cap)
371
378
  const monthlyLimit = self.getLimit(name);
372
- if (!monthlyLimit) {
379
+ if (!monthlyLimit || monthlyLimit < 0) {
373
380
  return null;
374
381
  }
375
382
 
@@ -55,7 +55,12 @@ TestProvider.prototype.request = async function (options) {
55
55
 
56
56
  const messages = Array.isArray(options.messages) ? options.messages : [];
57
57
  const lastUserMessage = [...messages].reverse().find((m) => m.role === 'user' && typeof m.content === 'string');
58
- const scriptSource = lastUserMessage?.content || stringifyLoose(options.message?.content) || '';
58
+ // Path-based messages ({ path, settings }) are rendered inside the real
59
+ // providers, so directives arrive via the settings values — read them too.
60
+ const scriptSource = lastUserMessage?.content
61
+ || stringifyLoose(options.message?.content)
62
+ || stringifyLoose(options.message?.settings)
63
+ || '';
59
64
 
60
65
  const { steps, cleanText } = parseScript(scriptSource);
61
66
 
@@ -230,6 +235,15 @@ function stringifyLoose(content) {
230
235
  return content.map((c) => c?.text || '').join('\n');
231
236
  }
232
237
 
238
+ // Plain objects (e.g. prompt-template settings) flatten to their raw values
239
+ // so directives embedded in them stay parseable (JSON.stringify would escape
240
+ // the quotes inside a directive's JSON payload)
241
+ if (content && typeof content === 'object') {
242
+ return Object.values(content)
243
+ .map((value) => (typeof value === 'string' ? value : `${value}`))
244
+ .join('\n');
245
+ }
246
+
233
247
  return content ? String(content) : '';
234
248
  }
235
249